检查“get”调用链中的空值问题

94

假设我想执行以下命令:

house.getFloor(0).getWall(WEST).getDoor().getDoorknob();
为了避免空指针异常,我需要在以下情况下执行以下操作:
if (house != null && house.getFloor(0) && house.getFloor(0).getWall(WEST) != null
  && house.getFloor(0).getWall(WEST).getDoor() != null) ...

是否有一种方法或已经存在的Utils类可以更优雅地完成这个任务,就像以下代码一样?

checkForNull(house.getFloor(0).getWall(WEST).getDoor().getDoorknob());

6
如果你遵循了德米特法则就好了。 - Oded
35
我虽然参与了一个已有的项目,但不能按照我或任何希腊神祇的法则重新设计它。 - user321068
1
对于其他人的疑问:特别是,一个对象应该避免调用另一个方法返回的对象的方法。...这个法则可以简单地表述为“只使用一个点”。https://en.wikipedia.org/wiki/Law_of_Demeter - James Daily
2
迪米特法则是一个可怕的想法(这就是为什么,谢天谢地,没有人使用它!),会导致容易出错的混乱代码。它也是反直觉的:如果每个直觉都告诉你不要编写那样的类,那么你可能真的不应该这样做。但我真的不明白它如何能帮助空值安全链接。它只是将多个空值检查推入了方法 House :: getDoorknob(Floor floor,CompassPoint wall,DoorType doorType)中。而那个庞大的方法的无用性就说明了一切。 - barneypitt
我的观点是,这是一种糟糕的类设计方式。直觉设计几乎总是最好的设计。将数据以你期望的方式呈现给你的设计是最好的设计。与人类思考实体及其属性的方式相匹配(它们被链接在一起)的设计是最好的设计。迪米特法则是不良设计的反面。 - barneypitt
显示剩余5条评论
11个回答

144
如果您无法避免违反Demeter法则(LoD),如所选答案所述,并且Java 8引入Optional,那么处理get链中的null可能是最佳实践。 Optional类型将使您能够在一系列map操作(其中包含get调用)中进行管道传输。空值检查在幕后自动处理。
例如,当对象未初始化时,不会进行print(),也不会抛出异常。所有这些都将在幕后轻松处理。当对象初始化时,将进行打印。
System.out.println("----- Not Initialized! -----");

Optional.ofNullable(new Outer())
        .map(out -> out.getNested())
        .map(nest -> nest.getInner())
        .map(in -> in.getFoo())
        .ifPresent(foo -> System.out.println("foo: " + foo)); //no print

System.out.println("----- Let's Initialize! -----");

Optional.ofNullable(new OuterInit())
        .map(out -> out.getNestedInit())
        .map(nest -> nest.getInnerInit())
        .map(in -> in.getFoo())
        .ifPresent(foo -> System.out.println("foo: " + foo)); //will print!

class Outer {
    Nested nested;
    Nested getNested() {
        return nested;
    }
}
class Nested {
    Inner inner;
    Inner getInner() {
        return inner;
    }
}
class Inner {
    String foo = "yeah!";
    String getFoo() {
        return foo;
    }
}

class OuterInit {
    NestedInit nested = new NestedInit();
    NestedInit getNestedInit() {
        return nested;
    }
}
class NestedInit {
    InnerInit inner = new InnerInit();
    InnerInit getInnerInit() {
        return inner;
    }
}
class InnerInit {
    String foo = "yeah!";
    String getFoo() {
        return foo;
    }
}

所以,使用您的getter链,它将如下所示:
Optional.ofNullable(house)
        .map(house -> house.getFloor(0))
        .map(floorZero -> floorZero.getWall(WEST))
        .map(wallWest -> wallWest.getDoor())
        .map(door -> wallWest.getDoor())

它的返回值将类似于Optional<Door>,这将使您更加安全地工作,而无需担心空指针异常。

3
太妙了!就像一个可选项的构建器,如果链条中缺少任何一个链接,就会导致链条停止并使可选项包含null值。 - nom-mon-ir
5
说实话,这应该是正确的答案,而不是当前的答案!感谢您提供的精彩解释。 - Ahmed Hamdy
如果您有Java 8+可用,这是正确的操作方式。 在 Optional 链的末尾,如果您需要传递或处理非 Optional 类型,则仍然可以执行 .orElse(null); 操作。对于使用 ofNullable/map 进行链式操作而言,确实没有任何不利影响,这应该被认为是一个可接受的答案。 - Michael Peterson
外星魔法!!! - Peter S.

24
为了检查一系列的获取是否为null,您可能需要从闭包中调用您的代码。闭包调用代码如下所示:
public static <T> T opt(Supplier<T> statement) {       
    try {
        return statement.get();
    } catch (NullPointerException exc) {
        return null;
    }   
}

您可以使用以下语法来调用它:

Doorknob knob = opt(() -> house.getFloor(0).getWall(WEST).getDoor().getDoorknob());

这段代码也是类型安全的,通常按预期工作:

  1. 如果链中所有对象都不为null,则返回指定类型的实际值。
  2. 如果链中任何一个对象为null,则返回null

您可以将opt方法放入共享的util类中,并在应用程序的任何地方使用它。


6
毫无疑问,这是一种非常棘手的处理方法,但将空指针异常作为处理方式是一种非常糟糕的做法,因为你可能会意外地处理到其他东西。你可以阅读《Effective Java》中相关的章节以获得更好的理解。 - MD. Sahib Bin Mahboob
7
空指针异常比空值检查昂贵吗? - user3044440
1
除非您使用的是真正旧版本的Java(<8,其中Optional不可用),否则请不要采用此方法。请参阅涉及Optional.ofNullable()/.map()的答案。 - Michael Peterson
这种方法在你有一个复杂的数据结构,并且大部分数据是可选的时候非常有效。就像访问嵌套的JAXB XML元素一样。不过有两点需要注意:使用.getFloor(0)时,我会以相同的方式处理IndexOutOfBoundsException。返回一个Optional<T>会更加优雅。 - sanya

14

最好的方法是避免这个链。如果您不熟悉迪米特法则(LoD),我认为您应该了解一下。你提供了一个完美的例子,说明这个消息链与它不应该知道任何信息的类之间关系过于亲密。

迪米特法则:http://zh.wikipedia.org/wiki/迪米特法则


56
这个答案没有指导如何避免代码中的环状依赖,并假设OP有时间/权限重新设计现有代码。 - K--

9

当然,你可以将整个表达式包装在try-catch块中,但这是不好的做法。更好的方法是使用空对象模式。使用此模式,如果你的房子没有0楼,它将返回一个像普通楼层一样的楼层,但没有实际内容;当要求不存在的墙时,楼层会返回类似“空”墙等等。


1
但是如果一堵墙没有门,那么返回null是合乎逻辑的。否则,你需要像hasDoor()这样的方法来知道实际上没有门,因为当你要求时,你只会得到一个假门。 - Robin
@Robin,这并不是“假”的墙。它是不存在的墙。而且(与null不同),它的行为就像真正的墙一样,因此在某些方面很有用,null无法做到这点。 - Carl Manaster
使用空对象模式是一个不错的选择,如果你不关心最终是否会发生某些事情(+1)。 - Bozho
6
基本上,你将一个“快速失败”的 NPE 转换成了“可能在未来的某个时间和地点失败”?有些情况下,空对象是有意义的(例如空集合),但我认为它们完全不适合作为 null 的通用替代品。 - Michael Borgwardt
不,我希望有一堵墙,但不一定要有门(挑剔一点)。我描述了这种方法在特定示例中存在的问题,因为并非所有的墙都包含门,并且使用Null Object模式确定是否有门的能力更加复杂。@Michael Borgwardt在他的评论中概括了这个问题。我的经验是,这种模式在可以应用的应用程序方面相当有限。 - Robin

5

确保那些逻辑上不能为null的事物不为空。例如,房子总是有一面西墙。为了避免状态异常,可以编写方法来检查你希望的状态是否存在:

if (wall.hasDoor()) {
   wall.getDoor().etc();
}

这实质上是一个空指针检查,但并不总是如此。

关键是在你有一个null的情况下应该做些什么。例如 - return或抛出IllegalStateException

而你不应该做的事情 - 不要捕获NullPointerException。运行时异常不应该被捕获 - 不能期望从中恢复,也不应该依赖异常来控制逻辑流程。想象一下,你实际上并不希望某个东西为null,然后你捕获(并记录)一个NullPointerException。这将不是非常有用的信息,因为此时许多东西可能为null


4

是的,这个问题在答案 https://dev59.com/mXA75IYBdhLWcg3wH1XP#41145698 中得到了很好的解决。 - Michael Peterson

1

你无法编写一个checkForNull方法来实现这个功能(这不是Java中方法调用和参数评估的工作方式)。

你可以将链式语句分解为多个语句,并在每个步骤检查。然而,也许更好的解决方案是一开始就不要让这些方法返回null。有一种叫做Null Object Pattern的东西,你可能想使用它。

相关问题


0
您可以像下面这样拥有一个通用方法:
public static <T> void ifPresentThen(final Supplier<T> supplier, final Consumer<T> consumer) {
    T value;
    try {
        value = supplier.get();
    } catch (NullPointerException e) {
        // Don't consume "then"
        return;
    }
    consumer.accept(value);
}

现在你就能够做到了

ifPresentThen(
    () -> house.getFloor(0).getWall(WEST).getDoor().getDoorknob(),
    doorKnob -> doSomething());

-1

使用Supplier实现空指针try/catch,您可以将其发送到所有的get链中

public static <T> T getValue(Supplier<T> getFunction, T defaultValue) {
    try {
        return getFunction.get();
    } catch (NullPointerException ex) {
        return defaultValue;
    }
}

然后以这种方式调用它。

ObjectHelper.getValue(() -> object1.getObject2().getObject3().getObject4()));

我忘记了... T defaultValue 用于在调用时发送默认值,或者删除以直接返回 null。 - Omar Ruiz
4
这与Seleznov在上面的回答有何不同? - ChuckB

-3

虽然这是一个很老的问题,但我仍然想提出我的建议:

我建议你不要在一个方法调用链中从房子深处获取门把手,而是尝试让门把手由调用代码提供给这个类,或者创建一个专门用于此目的的中央查找设施(例如门把手服务)。

松耦合设计的简化示例:

class Architect {

    FloorContractor floorContractor;

    void build(House house) {
        for(Floor floor: house.getFloors()) {
            floorContractor.build(floor);
        }
    }    
}

class FloorContractor {

    DoorMaker doorMaker;

    void build(Floor floor) {
        for(Wall wall: floor.getWalls()) {
            if (wall.hasDoor()) {
                doorMaker.build(wall.getDoor());
            }
        }
    } 
}

class DoorMaker {

    Tool tool;

    void build(Door door) {
        tool.build(door.getFrame());
        tool.build(door.getHinges());
        tool.build(door.getDoorKnob());
    }        
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接