【问题标题】:Handling null pointers and throwing exceptions in streams在流中处理空指针和抛出异常
【发布时间】:2015-07-10 06:54:32
【问题描述】:

让我们考虑一个只包含一个Integer 属性的Parent 类。 我创建了 6 个父类对象和一个空变量。然后我将这些对象添加到列表中。

我想通过Integer属性的值来检索对应的对象。 我为此使用了 Java 8 Streams。

Predicate<Parent> predicate = e -> e.getId() == 100; // sample attribute value
result = list.stream().filter(predicate).collect(Collectors.toList());

但是我得到了NullPointerException,所以我编辑了代码:

list.stream().filter(h -> h!=null).filter(predicate).collect(Collectors.toList());

但是如果任何对象为空,我想抛出异常。如果列表中没有对象为空,那么我想从列表中检索相应的对象。

如何使用 Java 8 Streams 使用单个语句来实现这一点?

【问题讨论】:

  • 您的第一个答案就是这样做对吗?那你为什么要过滤掉null 的值呢?
  • 为什么不简单地捕获 NPE,将其包装在您自己的异常中,然后抛出该异常?
  • @Buurman:捕获 NPE 实际上非常危险:如果代码中存在编程错误并且由于其他原因存在 NPE,该怎么办?那么它会被误认为列表中没有Parent
  • @Lii:这很容易检查:catch(NullPointerException ex){ if(list.contains(null)) throw new CustomException(); else throw ex; },这样可以保留其他 NPE。检查有一些成本,但这并不重要,因为它只在错误的情况下执行。
  • @Holger:嗯...我真的不认为这是一个好习惯。当列表也包含 null 时,可能会触发意外的 null 取消引用。我会坚持始终避免捕获 NPE 的规则。太容易出错了,因为它们基本上可以扔到任何地方。棘手的例子:如果e.getId() 碰巧返回一个空的Integer

标签: java lambda java-8 nullpointerexception


【解决方案1】:

JB Nizet 的回答还可以,但它使用map 只是为了它的副作用,而不是用于映射操作,这有点奇怪。当您只对某些东西的副作用感兴趣时,可以使用一种方法,例如抛出异常:peek

List<Parent> filtered = list.stream()
    .peek(Objects::requireNonNull)
    .filter(predicate)
    .collect(Collectors.toList());

如果您想要自己的异常,只需在其中放一个 lambda:

List<Parent> filtered = list.stream()
    .peek(p -> { if (p == null) throw new MyException(); })
    .filter(predicate)
    .collect(Collectors.toList());

检查的异常

如果检查了您的异常,您可以预先检查 null,如果您不介意遍历列表两次。这可能是您的最佳选择,但可能并不总是可行。

if (list.contains(null)) throw new MyCheckedException();

你也可以在你的流管道中抛出一个未经检查的异常,捕获它然后抛出一个经过检查的异常:

try {
    ...
        .peek(p -> { if (p == null) throw new MyException(); })
    ...
} catch (MyException exc) {
    throw new MyCheckedException();
}

偷袭

或者你可以走优雅但有争议的道路,使用偷偷摸摸的方法

但要小心!这种技术绕过了检查异常系统,您应该知道自己在做什么。一定要声明周围的方法抛出MyCheckedException!如果你不这样做,编译器不会警告你,如果检查的异常出现在不应该出现的地方,它可能会导致奇怪的错误。

@SuppressWarnings("unchecked")
public <T extends Throwable> void throwSneakily(Throwable t) throws T {
    throw (T) t;
}

public void m() throws MyCheckedException {
    List<Parent> filtered = list.stream()
        .peek(p -> { if (p == null) throwSneakily(new MyCheckedException()); })
        .filter(predicate)
        .collect(Collectors.toList());
}

【讨论】:

  • @Codebender:没错。如果必须检查,则可能需要使用偷偷摸摸的 throw 方法。
  • 偷偷摸摸的投掷不像你写的那样起作用。在您的代码中,编译器将完美地推断出正确的检查异常并抱怨。除此之外,peek 旨在支持调试,但不是生产代码逻辑的一部分。
  • @Holger,我认为在生产代码中使用peek 可能没问题,如果您设置已处理的元素以进行进一步操作并且没有其他副作用,如names.stream().map(Node::new).peek(n -&gt; n.setParent(this)).forEach(this::addNode) 而不是更长的names.stream().map(name -&gt; {Node n = new Node(name); n.setParent(this); return n;}).forEach(this::addNode) .
  • @Holger "偷偷摸摸的投掷不起作用":你说得对,我犯了一个错误,我已经更新了。 “除此之外,peek 旨在支持调试”:文档说“此方法的存在主要是为了支持调试”。如果没有实际问题我觉得还可以。
  • @Tagir Valeev:当然,使用map 执行消费者并不是更好。但是为什么要比较两种错误的方法呢?在您的示例中,正确的解决方案是 names.stream().map(Node::new).forEach(n -&gt; {n.setParent(this); addNode(n);})
【解决方案2】:

让我们从最简单的解决方案开始:

if(list.contains(null)) throw new MyException();
result = list.stream().filter(predicate).collect(Collectors.toList());

如果您怀疑该列表包含nulls,甚至有专门的异常类型来标记这种情况,那么预检查是最干净的解决方案。如果谓词更改为可以处理 nulls 的内容,或者当您使用可能在遇到后续 null 之前结束的短路流操作时,这可确保此类条件不会静默保留。

如果列表中出现null 仍然被认为是不应该发生的编程错误,但您只是想更改异常类型(我无法想象这样做的真正原因),您可能只是捕获并翻译异常:

try {
    result = list.stream().filter(predicate).collect(Collectors.toList());
}
catch(NullPointerException ex) {
    if(list.contains(null)) // ensure that we don’t hide another programming error
        throw new MyException();
    else throw ex;
}

null 引用确实发生的假设下有效。如前所述,如果您怀疑该列表包含null,您应该更喜欢预先检查。

【讨论】:

  • 我也会在这里添加关于捕获 NPE 的评论:我真的不认为这是一个好习惯。我会坚持始终避免捕获 NPE 的规则。太容易出错了,因为它们基本上可以扔到任何地方。在此示例中,当列表还包含 null 时,可能会触发意外的 null 取消引用。棘手的例子:如果e.getId() 恰好返回一个空的Integer
  • @Lii:这不是问题,因为如果您有两个编程错误,那么标记哪一个并不重要。正如我在答案中已经说过的那样,这个解决方案仅适用于其他异常类型与 NPE 具有相似语义(编程错误)的用例,因此我看不到更改异常类型的真正价值。而且我不知道这是否真的比滥用peek或使用偷偷摸摸的投掷技术更糟糕......
  • 这就是为什么我在我的解决方案中抛出了另一个异常类,它用 try-catch 包围了流操作。
  • 但是列表中有一个空元素很可能(或至少不一定)不是编程错误。只是应该由异常报告的异常情况。
  • 查看我回答的第一部分。如果列表中有空元素可能不是编程错误,则应使用预检查。我在答案中明确表示,第二部分仅适用于“如果列表中出现null 仍被视为编程错误”(尽管有异常类型转换)。
【解决方案3】:

::map vs ::peek

在生产代码中使用 Stream#map 并且永远不要使用 Stream#peek 方法。正如 Javadoc 所说,peekmainly to support debugging ...where the stream implementation is able to optimize away..., the action will not be invoked for those elements. REF:javadoc

java.utils.Objects#requireNonNull

  list.stream()
      .map(Objects::requireNonNull)
      .filter(predicate)

这段代码清楚地将验证部分和过滤器部分分开。它将在异常调用堆栈中包含“requireNonNull”帧。虽然,按照 java.util 的设计,它会增加 NPE exceptionoin 类型。参考:javadoc。 SRC:openjdk


requireNonNull 带有静态消息

  list.stream()
      .map(item -> requireNonNull(item, "Null item in the list."))
      .filter(predicate)

导入静态 java.util.Objects.requireNonNull;

参考:javadoc。 SRC:openjdk


requireNonNull 与自定义消息供应商

  list.stream()
      .map(item -> requireNonNull(item, () -> "Expected non-null items. Was: " + list))
      .filter(predicate)

参考:javadoc。 SRC:openjdk


自定义验证器

  list.stream()
      .map(this::requireItemNonNull)
      .filter(predicate)
      ...


private Parent requireItemNonNull(Parent item) {
  if (null == item) throw new IllegalStateException("Expected: non null items. Was: " + this.list);
  return item;

全部验证

如果您只需要验证所有项目,请在最后使用收集器。例如。 toArray 作为流将实际迭代推迟到“收集”时间:

  list.stream()
      .map(this::validateItemNonNull)
      .toArray();

【讨论】:

    【解决方案4】:
    Function<Parent, Parent> throwingIdentity = p -> {
        if (p == null) {
            throw new MyException();
        }
        return p;
    };
    
    List<Parent> filtered = 
        list.stream()
            .map(throwingIdentity)
            .filter(predicate)
            .collect(Collectors.toList());
    

    【讨论】:

      猜你喜欢
      • 2017-06-27
      • 1970-01-01
      • 2011-05-13
      • 2020-11-22
      • 2020-08-10
      • 2013-03-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多