可选:Java8处理Null的方法


对于那些已经在Java或C/C++编程过一段时间的人来说,最烦人的事情之一就是试图调试由于访问空对象而导致的崩溃。虽然null的概念是使编程语言正常工作,处理偏离正常“快乐”路径(包括错误处理)所必需的,但它对实现解决方案没有任何帮助。然而,我们必须花费大量的时间来处理和防止空值,以制作健壮的软件。今天,我们将看一看可选如何在总体上改进我们的代码,然后快速看一看它的API。

Null是未初始化的类成员字段或静态对象的默认值,我们重新分配回Null以释放内存。它还用于哨兵值,如指示无数据。问题是当我们试图访问空值时,我们会得到一个异常。然后,我们将尝试确定该值是否未初始化,从而导致某些其他代码的错误,或者它是否是我们的代码没有正确处理的一个哨兵值。有时这会导致错误的修复,或者在修复上犹豫不决。这段代码可能看起来很熟悉:

public class ImportantData
{
  private Data fileData; // Not constructor initialised

  ...

  // Call first before using csvData
  public void load(String fname)
  {
    try
    {
       fileData = loadCSVFromFile(fname);
    }
    catch (IOException e)
    {
      // Should at least have:
      // System.err.println("Can't load " + fname);
    }
  }

...
}

这就是‘我不能解决如何处理这个’模式。通常我们这样做只是为了让代码运行,因为处理错误可能并不简单,还没有具体说明,或者我们正在进行概念验证。这样的代码在敏捷的“总是可演示的”开发模型中变得更有可能。当我们试图构建这段代码时,很容易忘记重温这些快捷方式,并且很难再次找到它们,除非我们始终标记它们。更糟糕的是,当捕获到异常时,处理程序是空的,甚至没有消息,因此我们会得到静默失败。Java关于必须捕获检查过的异常的规则进一步加剧了这一问题,诱惑我们使用一种快捷方式。测试甚至可能不会突出问题,因为这是一个例外情况,可能需要其他东西来出错,然后我们才会出现故障。

如果文件数据不能为空,我们当然应该进行检查。我们可以使用assert,但在生产中会禁用它。除非空间或时间很重要,否则最好是防御性的。最好是早点发现问题,不要让问题继续下去,搞砸其他事情。在Java7之前,我们必须执行以下操作:

try
{
   fileData = loadCSVFromFile(fname);
}
catch (IOException e)
{
  // Should at least have:
  // System.err.println("Can't load " + fname);
}

if (fileData == null)
{
  throw new NullPointerException("fileData can't be null!");
}

这也将帮助我们处理静默IOException捕获,因为fileData在那里也将为null。

在Java7中,我们可以做得更好,用内置的:

Objects.requireNonNull(fileData, "fileData can't be null!");

这更短,记录了我们的意图,即fileData不能为null,并防止null对象在代码后面引起麻烦。requireNonNull有两个版本,一个有消息,另一个没有消息,完全转换为旧的Java等效版本。

Java8增加了可选功能,使我们能够更好地处理空值,并区分没有结果和未初始化/发生错误。让我们对代码进行如下更改:

public class ImportantData
{
  private Optional<Data> fileData; // Not constructor initialised

  ...

  // Call first before using csvData
  public void load(String fname)
  {
    // assume fileData is uninitialised at this point

    try
    {
       fileData = Optional.of(loadCSVFromFile(fname));
    }
    catch (IOException e)
    {
      // Should at least have:
      // System.err.println("Can't load " + fname);
    }

    Objects.requireNonNull(fileData, "fileData can't be null!");
  }

...
}

现在,我们使用可选的静态'of'方法来包装数据对象(可选只能使用静态方法初始化)。如果尝试包装空值,“of”方法将引发NullPointerException。我们不妨将此作为一个免费的安全检查,因为代码会在那里崩溃,然后。如果以后我们必须更加健壮,我们可以搜索optional.of来定位需要检查NullPointerException的所有位置。

一旦我们最终完成了fileData并需要将其释放到垃圾收集器,我们就不能仅仅更改可选的内容(因为它不能被重新分配),我们需要更改fileData引用的内容。我们可以考虑使用一个特殊的sentinel对象来指示它被释放,而不是使用null,因为null可能会被误认为从未初始化(即从未调用load)。

假设loadCSVFromFIle返回null是可以接受的,这可能是为了表示一个空文件。如果不使用可选的包装,我们无法区分空文件,文件未被找到,文件已损坏,或者从未调用load。如果我们没有正确地处理这些异常,我们以后就无法知道fileData为空的原因,以及是否应该处理它,还是应该更早地处理它。因此,我们没有记录我们的意图,常常让别人来理解我们的意思。这可能导致错误的修复。

可选帮助解决此问题,但要换行空值,必须替换

...
       fileData = Optional.of(loadCSVFromFile(fname));
...

...
       fileData = Optional.ofNullable(loadCSVFromFile(fname));
...

由于将null传递给可选的'of'方法会引发NullPointerException,因此我们必须使用也包装null的ofNullable。在引擎盖下面,如果向它传递null,则返回optional.empty()。我们现在可以区分未初始化的fileData(由于异常或load未被调用)和缺少任何数据的文件之间的区别。

注意:这个示例假设我们不能更改loadCSVFromFile,但是如果我们可以,我们将返回其中的可选项,而不是在之后包装它。这也将使API的用户不必决定是用'of'还是OfNullable换行。

可选的允许我们更容易地处理空对象,因为有一些有用的支持函数来减少“if(object!=null){…。”这可能会使代码变得很难理解。

现在让我们看看Optional的API。注意,也有专门的选项:OptionalInt,OptionalDouble和OptionalLong,它们的API非常相似。首先,我们将从创建(包装对象)和展开它们开始:

public static void main(String[] args)
{
    Optional<String> opt = Optional.of("hello");
    System.out.println("Test1: " + opt.get());

    try
    {
        Optional.of(null);
    }
    catch (NullPointerException e)
    {
        System.out.println(
           "Test2: Can't wrap a null object with of");
    }

    Optional<String> optNull = Optional.ofNullable(null);

    try
    {
        System.out.println(optNull.get());
    }
    catch (NoSuchElementException e)
    {
        System.out.println(
           "Test3: Can't unwrap a null object with get");
    }

    Optional<String> optEmpty = Optional.empty();

    try
    {
        System.out.println(optEmpty.get());
    }
    catch (NoSuchElementException e)
    {
        System.out.println(
           "Test4: Can't unwrap an empty Optional with get");
    }
}

上面有四个测试:

1,第一个显示了对象的包装,我们通过调用静态的'of'方法来包装对象,然后用get(在专用选项中是getAs)进行检索。
2,第二个表示我们不能用'of'包装空对象,如果我们尝试,我们会得到一个NullPointerException。因此,当我们确定null是不可能的,或者如果是,我们希望抛出NullPointerException时,应该使用“of”。如果允许null,我们必须使用ofNullable代替。
3,和4。第三个和第四个实际上是相同的情况,因为当ofNullable包装一个null时,会返回optional.empty()。我们也可以直接调用空方法。这些测试表明,如果使用get方法来解包optional.empty(),它将引发一个NoSuchelementException。

需要注意的一点是,专门化版本(例如OptionalInt)没有ofNullable,尽管我们仍然可以进行测试并手动获取OptionalInt.Empty()。相应地,该API使用int而不是integer。

由于我们可能需要检查一个可选项是否为空,因此可以为此使用isPresent()测试。API明确指出,我们永远不应该对optional.empty()执行==检查,因为它不能保证是单例。

如果我们想要解包一个可能为null的可选项,我们应该使用orElse来给它一个默认值(可以为null)。

public class OptionalTest2
{
        public static void main(String[] args)
        {
                Optional<String> opt = Optional.of("found");
                System.out.println(opt.isPresent());
                System.out.println(opt.orElse("not found"));

                Optional<String> optNull = Optional.ofNullable(null);
                System.out.println(optNull.isPresent());
                System.out.println(optNull.orElse("default"));

                Optional<String> optEmpty = Optional.empty();
                System.out.println(optEmpty.isPresent());
                System.out.println(optEmpty.orElse("default"));
        }
}

除了使用orElse显式提供默认值的代码之外,我们还可以调用orElseGet从供应商那里获取值。还有一个orElseThrow,在该方法中,供应商传递将提供一个适当的异常,还有一个ifPresent方法,该方法仅在可选方法包装了一个值时才将值传递给供应商。下一个示例演示了这些:

public class OptionalTest3
{
    private static class MySupplier implements Supplier<String>
    {
        @Override
        public String get()
        {
            return "Supplier returned this";
        }
    }

    private static class MyExceptionSupplier implements
            Supplier<IllegalArgumentException>
    {
        @Override
        public IllegalArgumentException get()
        {
            return new IllegalArgumentException();
        }
    }

    private static class MyConsumer implements Consumer<String>
    {
        @Override
        public void accept(String t)
        {
            System.out.println("Consumed: " + t);
        }
    }

    public static void main(String[] args)
    {
        Optional<String> opt = Optional.of("found");
        System.out.println(opt.orElseGet(new MySupplier()));
        System.out.println(opt.orElseThrow(
                                         new MyExceptionSupplier()));
        opt.ifPresent(new MyConsumer());

        Optional<String> optNull = Optional.ofNullable(null);
        System.out.println(optNull.orElseGet(new MySupplier()));

        try
        {
            System.out.println(optNull.orElseThrow(
                                         new MyExceptionSupplier()));
        }
        catch (IllegalArgumentException e)
        {
            System.out.println("Exception caught");
        }

        // This one won't use the consumer
        optNull.ifPresent(new MyConsumer());
    }
}

必须以这种方式检索和检查是否存在值,尽管最初是单调乏味的,但这使我们更多地考虑如果值为null该怎么办。至少比不使用可选的更短。

请注意,在最新的示例中,如果我们包装的是Integer而不是String,则不能使用IntSupplier或intConsumer。这是因为orElseGet和Optional的ifPresent分别需要一个扩展或超整数的类型(包括Integer或course)。IntSupplier和IntConsumer不扩展Supplier和Consumer,所以我们不能替换它们。不过,专门的OptionalInt确实需要一个IntSupplier和IntConsumer。

有几个有用的函数方法(令人惊讶的是它们还没有添加到包装器类或编号中):filter,map和flatmap。FlatMap处理映射函数已经返回一个可选函数,因此不再对其进行包装的情况。相反,Optional的map将包装映射函数返回的任何内容。

如果谓词不匹配,Filter将返回Optional.Empty()。如果可选项已经为空,谓词就不会被检查,尽管我们不应该担心这一点,因为谓词应该只是逻辑测试,而不会有副作用。

下面是一个快速的过程:

public static void main(String args[])
{
    Optional<String> hiMsg = Optional.of("hi");

    Optional<String> hiThereMsg = hiMsg.map(x -> x + " there!");

    System.out.println(hiMsg.get()); // Original

    System.out.println(hiThereMsg.get()); // Mapped

    System.out.println(hiThereMsg.filter(x -> x.equals("hi there!"))
                 .orElse("Bye!"));

    // Filter test fails returning Optional.empty()
    System.out.println(hiThereMsg.filter(x -> x.equals("yo there!"))
                .orElse("Bye!"));

    // The Optional gets wrapped
    Optional<Optional<String>> byeMessage = hiThereMsg
                                 .map(x -> Optional.of("Bye bye!"));

    // No extra wrapping
    Optional<String> byeMessage2 = hiThereMsg
                             .flatMap(x -> Optional.of("Bye bye!"));

    System.out.println(byeMessage.get().get());
    System.out.println(byeMessage2.get());

    // This would be an error since the
    // mapping has to return Optional
    // hiThereMsg.flatMap(x -> "Bye bye!");

    // We can change the wrapped type
    Optional<Integer> five = hiThereMsg.map(x -> 5);
    System.out.println(five.get());

    Optional<Integer> six = hiThereMsg.flatMap(x -> Optional.of(6));
    System.out.println(six.get());
}

最后,来自Java文档本身的警告:“这是一个基于值的类;在Optional得实例上使用标识敏感操作(包括引用相等==),标识哈希代码或同步可能会产生不可预测得结果,应避免。‘简言之,不要尝试在Optional上使用==,hashCode或synchronized。Normal.equals可以使用,但是如果你期望匹配,那么你正在比较的对象也需要是可选的。如果两个选项都是optional.empty(),则认为匹配。

不久将会有更多关于可选如何与新的函数式编程特性相适应的信息。