空检查链与捕获空指针异常



一个Web服务返回一个巨大的XML,我需要访问它的深度嵌套字段。例如:

return wsObject.getFoo().getBar().getBaz().getInt()

问题是getFoo()getBar()getBaz()可能都返回null

但是,如果我在所有情况下都检查null,代码会变得非常冗长且难以阅读。此外,我可能会错过某些字段的检查。

if (wsObject.getFoo() == null) return -1;
if (wsObject.getFoo().getBar() == null) return -1;
// maybe also do something with wsObject.getFoo().getBar()
if (wsObject.getFoo().getBar().getBaz() == null) return -1;
return wsObject.getFoo().getBar().getBaz().getInt();

可以写吗

try {
return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
return -1;
}

或者这被认为是一种反模式?

捕获NullPointerException是一件非常有问题的事情,因为它们几乎可能发生在任何地方。很容易从错误中获取一个,偶然捕获它并继续,就好像一切正常一样,从而隐藏了一个真正的问题。处理起来非常棘手,所以最好完全避免。(例如,考虑自动取消装箱的空Integer

我建议你改用Optional类。当您想要使用存在或不存在的值时,这通常是最佳方法。

使用它,您可以像这样编写代码:

public Optional<Integer> m(Ws wsObject) {
return Optional.ofNullable(wsObject.getFoo()) // Here you get Optional.empty() if the Foo is null
.map(f -> f.getBar()) // Here you transform the optional or get empty if the Bar is null
.map(b -> b.getBaz())
.map(b -> b.getInt());
// Add this if you want to return null instead of an empty optional if any is null
// .orElse(null);
// Or this if you want to throw an exception instead
// .orElseThrow(SomeApplicationException::new);
}

为什么是可选的?

对可能不存在的值使用Optionals 而不是null会使这一事实对读者非常明显和清晰,并且类型系统将确保您不会意外忘记它。

您还可以访问更方便地处理此类值的方法,例如maporElse


缺勤是有效还是错误?

但也要考虑中间方法返回 null 是否是有效结果,或者这是否是错误的标志。如果它始终是一个错误,那么抛出异常可能比返回特殊值更好,或者中间方法本身抛出异常。


也许更多可选?

另一方面,如果中间方法中缺少的值有效,也许您也可以为它们切换到Optionals?

然后你可以像这样使用它们:

public Optional<Integer> mo(Ws wsObject) {
return wsObject.getFoo()
.flatMap(f -> f.getBar())
.flatMap(b -> b.getBaz())
.flatMap(b -> b.getInt());        
}

为什么不是可选的?

我能想到不使用Optional的唯一原因是这是否在代码的真正性能关键部分,以及垃圾收集开销是否被证明是一个问题。这是因为每次执行代码时都会分配一些Optional对象,VM可能无法优化这些对象。在这种情况下,您原来的 if 测试可能会更好。

我建议考虑Objects.requireNonNull(T obj, String message).您可以为每个异常构建带有详细消息的链,例如

requireNonNull(requireNonNull(requireNonNull(
wsObject, "wsObject is null")
.getFoo(), "getFoo() is null")
.getBar(), "getBar() is null");

我建议您不要使用特殊的返回值,例如-1。这不是Java风格。Java设计了异常机制,以避免这种来自C语言的老式方式。

投掷NullPointerException也不是最好的选择。您可以提供自己的异常(使其处于选中状态以保证它将由用户处理,或者取消选中以更简单的方式处理它)或使用您正在使用的 XML 解析器中的特定异常。

假设类结构确实超出了我们的控制范围,我认为捕获问题中建议的 NPE 确实是一个合理的解决方案,除非性能是一个主要问题。一个小的改进可能是包装抛出/捕获逻辑以避免混乱:

static <T> T get(Supplier<T> supplier, T defaultValue) {
try {
return supplier.get();
} catch (NullPointerException e) {
return defaultValue;
}
}

现在您可以简单地执行以下操作:

return get(() -> wsObject.getFoo().getBar().getBaz().getInt(), -1);

正如汤姆在评论中已经指出的那样,

以下声明不遵守得墨忒耳定律,

wsObject.getFoo().getBar().getBaz().getInt()

你想要的是int,你可以从Foo得到它。得墨忒耳定律说,永远不要和陌生人说话。对于您的情况,您可以将实际实现隐藏在FooBar的引擎盖下。

现在,您可以在Foo中创建方法以从Baz获取int。最终,Foo将具有Bar,并且Bar我们可以访问Int,而无需将Baz直接暴露给Foo。因此,空检查可能分为不同的类,并且只有必需的属性才会在类之间共享。

我的回答几乎与@janki相同,但我想稍微修改代码片段,如下所示:

if (wsObject.getFoo() != null && wsObject.getFoo().getBar() != null && wsObject.getFoo().getBar().getBaz() != null) 
return wsObject.getFoo().getBar().getBaz().getInt();
else
return something or throw exception;

如果该对象有可能为 null,也可以为wsObject添加空检查。

你说有些方法"可能会返回null",但没有说它们在什么情况下返回null。你说你抓住了NullPointerException但你没有说你为什么抓住它。缺乏信息表明您不清楚例外是什么以及为什么它们优于替代方案。

考虑一个旨在执行操作的类方法,但由于超出其控制范围的情况,该方法不能保证它会执行该操作(实际上 Java 中的所有方法都是这种情况)。我们调用该方法并返回。调用该方法的代码需要知道它是否成功。怎么会知道?它的结构如何应对成功或失败这两种可能性?

使用异常,我们可以编写将成功作为后置条件的方法。如果该方法返回,则表示成功。 如果它引发异常,则表示它失败了。为了清楚起见,这是一个巨大的胜利。我们可以编写代码来清楚地处理正常的成功案例,并将所有错误处理代码移动到catch子句中。通常发现,方法如何或为什么不成功的详细信息对调用方并不重要,因此相同的catch子句可用于处理多种类型的失败。而且经常发生的情况是,方法根本不需要捕获异常,但只需允许它们传播到调用方即可。由于程序错误导致的异常属于后一类;很少有方法可以在出现错误时做出适当的反应。

因此,那些返回的方法null.

  • null值是否表示代码中的错误?如果是这样,则根本不应该捕获异常。而且你的代码不应该试图对自己进行二次猜测。只需在假设它会起作用的情况下写清楚简洁的内容。方法调用链是否清晰简洁?然后使用它们。
  • null值是否表示程序的输入无效?如果是这样,则NullPointerException不是合适的异常,因为通常它是为指示错误而保留的。您可能希望引发从IllegalArgumentException(如果需要未经检查的异常)或IOException(如果需要已检查的异常)派生的自定义异常。当输入无效时,您的程序是否需要提供详细的语法错误消息?如果是这样,检查每个方法的null返回值,然后引发适当的诊断异常是唯一可以做的事情。如果程序不需要提供详细的诊断,则将方法调用链接在一起,捕获任何NullPointerException,然后引发自定义异常是最清晰和最简洁的。

其中一个答案声称链式方法调用违反了得墨忒耳定律,因此很糟糕。这种说法是错误的。

  • 在程序设计方面,关于什么是好什么是坏,实际上没有任何绝对的规则。只有启发式:在很多时间(甚至几乎所有时间)都是正确的规则。编程技能的一部分是知道什么时候可以打破这些规则。因此,"这违反了规则X"的简短断言根本不是真正的答案。这是应该打破规则的情况之一吗?
  • 得墨忒耳定律实际上是关于 API 或类接口设计的规则。在设计类时,具有抽象层次结构很有用。您具有使用语言基元直接执行操作并在比语言基元更高级的抽象中表示对象的低级类。您具有委托给低级别类的中级类,并在比低级别类更高的级别实现操作和表示。您有委托给中级类的高级类,并实现更高级别的操作和抽象。(我在这里只讨论了三个抽象级别,但更多是可能的)。这允许您的代码在每个级别以适当的抽象来表达自身,从而隐藏复杂性。得墨忒耳定律的基本原理是,如果你有一个方法调用链,这表明你有一个高级类通过一个中级类直接处理低级细节,因此你的中级类没有提供高级类所需的中级抽象操作。但似乎不是你在这里遇到的情况:你没有在方法调用链中设计类,它们是一些自动生成的 XML 序列化代码的结果(对吗?),并且调用链不是通过抽象层次结构降序的,因为反序列化的 XML 都处于抽象层次结构的同一级别(对吗?

正如其他人所说,尊重得墨忒耳定律绝对是解决方案的一部分。 另一部分,只要有可能,是更改这些链接的方法,以便它们无法返回null。 你可以通过返回一个空String、一个空Collection或其他一些虚拟对象来避免返回null,这些对象意味着或执行调用者对null执行的任何操作。

为了提高可读性,您可能需要使用多个变量,例如

Foo theFoo;
Bar theBar;
Baz theBaz;
theFoo = wsObject.getFoo();
if ( theFoo == null ) {
// Exit.
}
theBar = theFoo.getBar();
if ( theBar == null ) {
// Exit.
}
theBaz = theBar.getBaz();
if ( theBaz == null ) {
// Exit.
}
return theBaz.getInt();

不要抓住NullPointerException.你不知道它来自哪里(我知道在你的情况下不太可能,但也许是其他东西扔了它),而且它很慢。 您希望访问指定的字段,为此,所有其他字段都必须不为 null。这是检查每个字段的完美正当理由。我可能会在一个if中检查它,然后创建一个可读性的方法。正如其他人指出的那样,已经返回 -1 是非常老派的,但我不知道您是否有理由(例如与另一个系统交谈)。

public int callService() {
...
if(isValid(wsObject)){
return wsObject.getFoo().getBar().getBaz().getInt();
}
return -1;
}

public boolean isValid(WsObject wsObject) {
if(wsObject.getFoo() != null &&
wsObject.getFoo().getBar() != null &&
wsObject.getFoo().getBar().getBaz() != null) {
return true;
}
return false;
}

编辑:如果它违背得墨忒耳定律是有争议的,因为WsObject可能只是一个数据结构(检查 https://stackoverflow.com/a/26021695/1528880)。

如果您不想重构代码并且可以使用 Java 8,则可以使用方法引用。

先做一个简单的演示(请原谅静态内部类)

public class JavaApplication14 
{
static class Baz
{
private final int _int;
public Baz(int value){ _int = value; }
public int getInt(){ return _int; }
}
static class Bar
{
private final Baz _baz;
public Bar(Baz baz){ _baz = baz; }
public Baz getBar(){ return _baz; }   
}
static class Foo
{
private final Bar _bar;
public Foo(Bar bar){ _bar = bar; }
public Bar getBar(){ return _bar; }   
}
static class WSObject
{
private final Foo _foo;
public WSObject(Foo foo){ _foo = foo; }
public Foo getFoo(){ return _foo; }
}
interface Getter<T, R>
{
R get(T value);
}
static class GetterResult<R>
{
public R result;
public int lastIndex;
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) 
{
WSObject wsObject = new WSObject(new Foo(new Bar(new Baz(241))));
WSObject wsObjectNull = new WSObject(new Foo(null));
GetterResult<Integer> intResult
= getterChain(wsObject, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);
GetterResult<Integer> intResult2
= getterChain(wsObjectNull, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);

System.out.println(intResult.result);
System.out.println(intResult.lastIndex);
System.out.println();
System.out.println(intResult2.result);
System.out.println(intResult2.lastIndex);
// TODO code application logic here
}
public static <R, V1, V2, V3, V4> GetterResult<R>
getterChain(V1 value, Getter<V1, V2> g1, Getter<V2, V3> g2, Getter<V3, V4> g3, Getter<V4, R> g4)
{
GetterResult result = new GetterResult<>();
Object tmp = value;

if (tmp == null)
return result;
tmp = g1.get((V1)tmp);
result.lastIndex++;

if (tmp == null)
return result;
tmp = g2.get((V2)tmp);
result.lastIndex++;
if (tmp == null)
return result;
tmp = g3.get((V3)tmp);
result.lastIndex++;
if (tmp == null)
return result;
tmp = g4.get((V4)tmp);
result.lastIndex++;

result.result = (R)tmp;
return result;
}
}

输出

241
4


空 2

接口Getter只是一个功能接口,您可以使用任何等效接口。
GetterResult类中,为清楚起见,去除了访问器,保存 getter 链的结果(如果有)或最后一个调用的 getter 的索引。

方法getterChain是一段简单的样板代码,可以自动生成(或在需要时手动生成)。
我构建了代码,使重复块不言而喻。


这不是一个完美的解决方案,因为您仍然需要为每个数量的 getter 定义一个getterChain重载。

我会重构代码,但如果不能,并且您经常发现自己使用长getter链,则可以考虑构建一个具有从2到10个getter的重载的类。

我想添加一个答案,重点关注错误的含义。空异常本身不提供任何有意义的完整错误。所以我建议避免直接与他们打交道。

在数千种情况下,您的代码可能会出错:无法连接到数据库,IO异常,网络错误...如果您一一处理它们(就像这里的空检查一样),那就太麻烦了。

在代码中:

wsObject.getFoo().getBar().getBaz().getInt();

即使您知道哪个字段为空,您也不知道出了什么问题。也许 Bar 为空,但它是预期的吗?还是数据错误?想想那些阅读你的代码的人

就像在xenteros的回答中一样,我建议使用自定义的未经检查的异常。例如,在这种情况下:Foo 可以为空(有效数据),但 Bar 和 Baz 永远不应该为空(无效数据)

代码可以重写:

void myFunction()
{
try 
{
if (wsObject.getFoo() == null)
{
throw new FooNotExistException();
}
return wsObject.getFoo().getBar().getBaz().getInt();
}
catch (Exception ex)
{
log.error(ex.Message, ex); // Write log to track whatever exception happening
throw new OperationFailedException("The requested operation failed")
}
}

void Main()
{
try
{
myFunction();
}
catch(FooNotExistException)
{
// Show error: "Your foo does not exist, please check"
}
catch(OperationFailedException)
{
// Show error: "Operation failed, please contact our support"
}
}

NullPointerException是一个运行时异常,所以一般来说不建议捕获它,而是避免它。

您必须在要调用该方法的任何位置捕获异常(否则它将向上传播堆栈)。尽管如此,如果在您的情况下您可以继续使用值为 -1 的结果,并且您确定它不会传播,因为您没有使用任何可能为 null 的"片段",那么对我来说似乎正确的是抓住它

编辑:

我同意 @xenteros 后面的答案,最好启动您自己的异常而不是返回 -1,例如,您可以将其称为InvalidXMLException

从昨天开始就一直在关注这篇文章。

我一直在评论/投票评论,这些评论说,抓住 NPE 是不好的。这就是我一直这样做的原因。

package com.todelete;
public class Test {
public static void main(String[] args) {
Address address = new Address();
address.setSomeCrap(null);
Person person = new Person();
person.setAddress(address);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
try {
System.out.println(person.getAddress().getSomeCrap().getCrap());
} catch (NullPointerException npe) {
}
}
long endTime = System.currentTimeMillis();
System.out.println((endTime - startTime) / 1000F);
long startTime1 = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
if (person != null) {
Address address1 = person.getAddress();
if (address1 != null) {
SomeCrap someCrap2 = address1.getSomeCrap();
if (someCrap2 != null) {
System.out.println(someCrap2.getCrap());
}
}
}
}
long endTime1 = System.currentTimeMillis();
System.out.println((endTime1 - startTime1) / 1000F);
}
}

public class Person {
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}

package com.todelete;
public class Address {
private SomeCrap someCrap;
public SomeCrap getSomeCrap() {
return someCrap;
}
public void setSomeCrap(SomeCrap someCrap) {
this.someCrap = someCrap;
}
}

package com.todelete;
public class SomeCrap {
private String crap;
public String getCrap() {
return crap;
}
public void setCrap(String crap) {
this.crap = crap;
}
}

输出

3.216

0.002

我在这里看到了一个明显的赢家。进行 if 检查比捕获异常便宜得多。我已经看到了Java-8的工作方式。考虑到当前 70% 的应用程序仍在 Java-7 上运行,我添加了这个答案。

底线对于任何任务关键型应用程序,处理 NPE 的成本很高。

如果效率是一个问题,那么应该考虑"捕获"选项。 如果不能使用"catch",因为它会传播(如"SCouto"所述),则使用局部变量以避免多次调用方法getFoo()getBar()getBaz()

值得考虑创建自己的异常。我们称之为MyOperationFailedException。您可以抛出它,而是返回一个值。结果将是相同的 - 您将退出该函数,但您不会返回硬编码值 -1,这是 Java 反模式。在Java中,我们使用异常。

try {
return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
throw new MyOperationFailedException();
}

编辑:

根据评论中的讨论,让我在我之前的想法中添加一些内容。在此代码中有两种可能性。一个是你接受 null,另一个是,这是一个错误。

如果出现错误,则可以在断点不够使用其他结构调试代码以进行调试。

如果可以接受,则不关心此 null 出现的位置。如果这样做,则绝对不应该链接这些请求。

你拥有的方法很长,但非常可读。 如果我是来到你的代码库的新开发人员,我可以相当快地看到你在做什么。 在我看来,大多数其他答案(包括捕获异常)似乎并没有使事情更具可读性,有些则降低了可读性。

鉴于您可能无法控制生成的源代码,并且假设您确实只需要在这里和那里访问一些深度嵌套的字段,那么我建议您使用方法包装每个深度嵌套的访问。

private int getFooBarBazInt() {
if (wsObject.getFoo() == null) return -1;
if (wsObject.getFoo().getBar() == null) return -1;
if (wsObject.getFoo().getBar().getBaz() == null) return -1;
return wsObject.getFoo().getBar().getBaz().getInt();
}

如果您发现自己编写了很多这样的方法,或者如果您发现自己很想创建这些公共静态方法,那么我将创建一个单独的对象模型,按照您希望的方式嵌套,仅包含您关心的字段,并从 Web 服务对象模型转换为您的对象模型。

当您与远程 Web 服务通信时,通常有一个"远程域"和一个"应用程序域"并在两者之间切换。 远程域通常受 Web 协议的限制(例如,不能在纯 RESTful 服务中来回发送帮助程序方法,并且深度嵌套的对象模型很常见,以避免多个 API 调用),因此不适合在客户端中直接使用。

例如:

public static class MyFoo {
private int barBazInt;
public MyFoo(Foo foo) {
this.barBazInt = parseBarBazInt();
}
public int getBarBazInt() {
return barBazInt;
}
private int parseFooBarBazInt(Foo foo) {
if (foo() == null) return -1;
if (foo().getBar() == null) return -1;
if (foo().getBar().getBaz() == null) return -1;
return foo().getBar().getBaz().getInt();
}
}
return wsObject.getFooBarBazInt();

通过应用得墨忒耳定律,

class WsObject
{
FooObject foo;
..
Integer getFooBarBazInt()
{
if(foo != null) return foo.getBarBazInt();
else return null;
}
}
class FooObject
{
BarObject bar;
..
Integer getBarBazInt()
{
if(bar != null) return bar.getBazInt();
else return null;
}
}
class BarObject
{
BazObject baz;
..
Integer getBazInt()
{
if(baz != null) return baz.getInt();
else return null;
}
}
class BazObject
{
Integer myInt;
..
Integer getInt()
{
return myInt;
}
}

给出的答案似乎与所有其他答案不同。

我建议您在if秒内检查NULL

原因:

我们不应该给我们的程序留下任何崩溃的机会。 空指针由系统生成。系统的行为 生成的异常无法预测。你不应该离开你的 当您已经有处理方式时,程序在系统手中 它由你自己。并放置异常处理机制以提高安全性。!!

为了使您的代码易于阅读,请尝试检查条件:

if (wsObject.getFoo() == null || wsObject.getFoo().getBar() == null || wsObject.getFoo().getBar().getBaz() == null) 
return -1;
else 
return wsObject.getFoo().getBar().getBaz().getInt();

编辑:

这里需要将这些值存储wsObject.getFoo()wsObject.getFoo().getBar()wsObject.getFoo().getBar().getBaz()一些变量。我没有这样做,因为我不知道回报 该函数的类型。

任何建议将不胜感激..!!

我写了一个名为Snag的类,它允许你定义一个路径来导航对象树。以下是其使用示例:

Snag<Car, String> ENGINE_NAME = Snag.createForAndReturn(Car.class, String.class).toGet("engine.name").andReturnNullIfMissing();

这意味着实例ENGINE_NAME将有效地调用传递给它的实例Car?.getEngine()?.getName(),如果任何引用返回null,则返回null

final String name =  ENGINE_NAME.get(firstCar);

它没有在 Maven 上发布,但如果有人觉得这很有用,它就在这里(当然没有保证!

这有点基本,但它似乎可以完成这项工作。显然,对于支持安全导航或Optional的最新版本的Java和其他JVM语言,它已经过时了。

相关内容

  • 没有找到相关文章

最新更新