使用 Java 7 避免嵌套数据中出现 null 的更好方法

Better method for avoiding null in nested data with Java 7

我必须分析一个巨大的数据流,其中通常包含不完整的数据。目前,代码在多个级别散布着空检查,因为在任何级别都可能存在不完整的数据。

例如,我可能必须检索: Model.getDestination().getDevice().getName()

我试图创建一个方法来尝试将空检查减少到一个方法,我输入:

IsValid(Model.getDestination(), Model.getDestination().getDevice(), Model.getDestination().getDevice().getName())

此方法失败,因为它在发送之前评估所有参数,而不是像一次检查每个参数 Model.getDestination() != null && Model.getDestination().getDevice() != null && etc

但是有没有一种方法可以让我通过 Model.getDestination().getDevice().getName() 并在每个级别进行检查,而无需在通过之前对其进行评估或拆分?

我真正想让它做的是,如果有 null/nullexception 它应该安静地 return "",并继续处理传入数据

我知道在 Java 8 中有一些方法可以优雅地做到这一点,但我仍然坚持使用 Java 7

您可以使用 try-catch。因为你的情况不需要处理,like

try{
    if(IsValid(Model.getDestination(), Model.getDestination().getDevice(), Model.getDestination().getDevice().getName())){

}catch(Exception e){
    //do nothing
}

或者,您可以通过仅传递 Model 对象

来改进 isValid 方法
boolean isValid(Model model){
    return (model != null && model.getDestination() != null && model.getDestination().getDevice() != null && model.getDestination().getDevice().getName() != null)
}

选项 1 - if 语句

您已经在问题中提供了。我认为像下面这样使用 am if statement 是完全可以接受的:

Model.getDestination() != null && Model.getDestination().getDevice() != null && etc

选项 2 - javax 验证并检查结果 - 发送前

您可以使用 javax validation

参见:https://www.baeldung.com/javax-validation

您可以使用 @NotNull.

注释您想要的字段

然后你可以使用编程验证。

您可以查看验证结果是否有问题。

示例:

所以在你的 class 你会做:

@NotNull
Public String Destination;

您可以将您的对象提供给验证器:

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

Set<ConstraintViolation<Model>> violations = validator.validate(Model);

for (ConstraintViolation<User> violation : violations) {
    log.error(violation.getMessage()); 
}

选项 3 - fromNullable 和 Maps(如果你有 Java 8)

我要从 https://softwareengineering.stackexchange.com/questions/255503/null-checking-whilst-navigating-object-hierarchies 拿这个。这与您的问题非常相似。

import java.util.Optional;

Optional.fromNullable(model)
               .map(Model::getDestination)
               .map(Lounge::getDevice)
               .ifPresent(letter -> .... do what you want ...);

选项 4 - 仅使用 try/catch

由于异常的缓慢,每个人都讨厌这个。

所以你想简化 Model.getDestination().getDevice().getName()。首先,我想列出一些不应该做的事情: 不要使用异常。不要写 IsValid 方法,因为它根本不起作用,因为所有函数(或方法)在 Java 中都是严格的:这意味着每次调用函数时,都会计算所有参数在将它们传递给函数之前。

在Swift中我会写let name = Model.getDestination()?.getDevice()?.getName() ?? ""。在 Haskell 中,它就像 name <- (destination >>= getDevice >>= getName) <|> Just "" (假设是 Maybe monad)。这与 Java 代码具有不同的语义:

if(Model.getDestination() && Model.getDestination().getDevice() && Model.getDestination().getDevice().getName() {
    String name = Model.getDestination().getDevice().getName();
    System.out.println("We got a name: "+name);
}

因为此代码段调用 getDestination() 4 次,getDevice() 3 次,getName() 2 次。这不仅仅是性能影响:1)它引入了竞争条件。 2) 如果任何方法有副作用,您不希望它们被多次调用。 3) 它让一切都更难调试。

唯一正确的做法是这样的:

Destination dest = Model.getDestination();
Device device = null;
String name = null;
if(dest != null) {
    device = dest.getDevice();
    if(device != null) {
        name = device.getName();
    }
}
if(name == null) {
    name = "";
}

此代码将 name 设置为 Model.getDestination().getDevice().getName(),或者如果这些方法中的任何一个调用 return null,它将 name 设置为“”。我认为正确性比可读性更重要,特别是对于生产应用程序(甚至例如代码恕我直言)。上面的Swift或者Haskell代码相当于那个Java代码.

如果您有一个生产应用程序,我想您已经在做类似的事情,因为与它根本不同的一切都容易出错。

每个更好的解决方案都必须提供相同的语义,并且它不得多次调用任何方法(getDestination、getDevice、getName)。

也就是说,我不认为您可以使用 Java 7.

来简化代码

当然,您可以做的是缩短调用链:例如如果您经常需要此功能,您可以在 Destination 上创建一个方法 getDeviceName()。这是否使代码更具可读性取决于具体情况。

强迫你写这么低的代码也有好处:你可以做常见的子表达式消除,你会看到它的好处,因为它会让代码更短。例如。如果你有:

String name1 = Model.getDevice().getConnection().getContext().getName();
String name2 = Model.getDevice().getConnection().getContext().getLabel();

你可以将它们简化为

Context ctx = Model.getDevice().getConnection().getContext();
String name1 = ctx.getName();
String name2 = ctx.getLabel();

第二个片段有 3 行,而第一个片段只有两行。但是,如果您展开这两个片段以包含空值检查,您会发现第二个版本实际上要短得多。 (懒惰现在不做了。)

因此(关于 Optional-chaining),Java7 将使具有性能意识的编码器的代码看起来更好,而许多更高级的语言会产生编写慢代码的动机。 (当然,您也可以在高级语言中执行公共子表达式消除(您可能应该这样做),但根据我的经验,大多数开发人员更不愿意在高级语言中执行此操作。而在汇编程序中,一切都经过优化,因为更好的性能通常意味着您必须编写更少的代码,并且您编写的代码更容易理解。)

简而言之,我们都会使用具有内置可选链的语言,我们都会负责任地使用它,而不会产生性能问题和竞争条件。

我在深层嵌套结构中遇到过类似的问题,如果我有机会引入额外的结构来导航底层数据,我想,我已经做到了。

这是 C#,同时有一个保存 navigation/Elvis 运算符,我们将徒劳地等待 Java(建议 Java 7 但 discarded. Groovy has it btw.). Also looks like there are arguments against using Elvis,即使你有)。 lambdas(和扩展方法)也没有真正改善事情。此外,其他所有方法在此处的其他帖子中都被认为是丑陋的。

因此我提出了一个纯粹用于导航的二级结构,每个元素都有一个 getValue() 方法来访问原始结构(@Michael 提出的快捷方式也是直接添加这种方式)。允许您像这样空保存导航:

 Model model = new Model(new Destination(null));

 Destination destination = model.getDestination().getValue(); // destination is not null
 Device device = model.getDestination().getDevice().getValue(); // device will be null, no NPE
 String name = destination.getDevice().getName().getValue(); // name will be null, no NPE

 NavDevice navDevice = model.getDestination().getDevice(); // returns an ever non-null NavDevice, not a Device
 String name = navDevice.getValue().getName(); // cause an NPE by circumventing the navigation structure

具有直接的原始结构

    class Destination {

        private final Device device;

        public Destination(Device device) {
            this.device = device;
        }

        public Device getDevice() {
            return device;
        }
    }

    class Device {

        private final String name;

        private Device(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }

以及用于保存导航目的的二级结构。

显然这是有争议的,因为您始终可以直接访问原始结构并 运行 进入 NPE。但就可读性而言,也许我仍然会接受它,特别是对于大型结构,其中的 ifs 或 optionals 灌木丛真的很碍眼(如果你必须说,这很重要,实际上在这里实现了哪些业务规则)。

memory/speed 可以通过对每种类型仅使用一个导航对象并在您导航时重新设置它们的内部结构以适应底层对象来反驳这一论点。

    class Model {

        private final Destination destination;

        private Model(Destination destination) {
            this.destination = destination;
        }

        public NavDestination getDestination() {
            return new NavDestination(destination);
        }
    }

    class NavDestination {

        private final Destination value;

        private NavDestination(Destination value) {
            this.value = value;
        }

        public Destination getValue() {
            return value;
        }

        public NavDevice getDevice() {
            return new NavDevice(value == null ? null : value.getDevice());
        }

    }

    class NavDevice {

        private final Device value;

        private NavDevice(Device value) {
            this.value = value;
        }

        public Device getValue() {
            return value;
        }

        public NavName getName() {
            return new NavName(value == null ? null : value.getName());
        }
    }

    class NavName {

        private final String value;

        private NavName(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }

    }