Java 8方式检查对象中的null对象和null值

13
如何将此函数重写为更符合Java 8规范的可选项?或者我应该将其保留为原样?
public void setMemory(ArrayList<Integer> memory) {
    if (memory == null)
        throw new IllegalArgumentException("ERROR: memory object can't be null.");
    if (memory.contains(null))
        throw new IllegalArgumentException("ERROR: memory object can't contain null value.");

    this.memory = memory;
}

2
如果你仍然想抛出两个不同的自定义异常,我不知道如何使它更短。在我看来,现在这样已经很好了。 - tobias_k
4
我更喜欢通过在参数中添加前置条件作为Javadoc注释,将责任(按合同设计)交给方法调用者,而不是对空值进行O(n)检查。 - d.j.brown
3
Objects.requireNonNull是一个Java方法,用于检查传递给它的对象是否为null,如果为null,则会抛出NullPointerException异常。 - Eugene
2
@shinjw 我同意,Optionals 会使事情变得更加复杂,特别是第二个检查甚至不是一个空值检查。有些答案显示了其他方法来使这些检查更具表现力。 - Malte Hartwig
8
请注意,如果您在不复制的情况下存储调用方提供的集合的引用,则调用者在调用“setMemory”之后仍可以添加“null”。 - Holger
显示剩余7条评论
9个回答

8

你有一个模式 condition -> throw an exception 可以移动到一个方法中:


你可以将其转化为一个方法:
private void checkOrElseThrow(boolean condition, Supplier<? extends RuntimeException> exceptionSupplier) {
    if (condition) {
        throw exceptionSupplier.get();
    }
}

public void setMemory(List<Integer> memory) {

    checkOrElseThrow(memory == null, () -> new IllegalArgumentException("message #1"));
    checkOrElseThrow(memory.contains(null), () -> new IllegalArgumentException("message #2"));

    this.memory = memory;
}

如果异常的类型不会改变,只传递异常消息是合理的(感谢 @tobias_k 指出):

private void checkOrElseThrow(boolean condition, String exceptionMessage) {
    if (condition) {
        throw new IllegalArgumentException(exceptionMessage);
    }
}

public void setMemory(List<Integer> memory) {

    checkOrElseThrow(memory == null, "message #1");
    checkOrElseThrow(memory.contains(null), "message #2");

    this.memory = memory;
}

checkAndThrow 应该接受一个谓词,以我的看法。 - Michael
@Michael,它将添加一个通用参数和一个额外的参数。此外,条件将始终被计算。 - Andrew Tobilko
2
如果 OP 真的想要更改代码,我更喜欢这个答案。示例中的空值检查已经足够好了,但是在整个应用程序中使用时,考虑使用此方法可能会更具表现力。查看那些试图使用 optionals 进行某些操作的答案,我并没有真正看到多少好处,因为其中一些检查甚至不是空值检查(例如示例中的 contains 检查)。 - Malte Hartwig
2
假设 OP 想在这两种情况下抛出相同类型的异常,那么你也可以通过将错误消息传递给该方法而不是一个供应商来实现。目前,它并没有比 OP 的代码更短或更易读,只是放在了一行中。 - tobias_k
谢谢提供另一种实现功能的方法,但我认为新方法接收布尔表达式和字符串消息有点过于复杂,与if + throw相比。我可能错了,还在学习Java,但这是我想到的。 - Jump3r

7
如果你想坚持使用IllegalArgumentException,并且你的类路径上有guava,你可以使用以下代码:

Preconditions.checkArgument(memory != null, 
            "ERROR: memory object can't be null.");
Preconditions.checkArgument(!memory.contains(null), 
            "ERROR: memory object can't contain null value.");

如果你希望针对不同的条件显示不同的错误信息,你不能使用Optional

但是,如果你可以接受只有一个错误消息,那么可以这样做:

this.memory = Optional.ofNullable(memory)
            .filter(x -> !x.contains(null))
            .orElseThrow(() -> new IllegalArgumentException(
                         "memory object is null or contains null values"));

2
我认为没有必要使用两个不同的错误消息(如果需要,应该重新考虑),所以我认为你的“Optional”例子是表达这种情况的最佳方式。 - Michael
@Michael 在我看来绝对不行。假设可以访问到完全相同的源代码,那么知道问题的确切位置比任何信息都更重要。如果缺少/不足的信息,我需要检查版本,将堆栈跟踪复制到IDE中并进行单击。不知道对象本身或其内容是否为null会使我考虑(多达)两倍的原因。 - maaartinus
@maaartinus同意,我个人在这里遇到的问题是OP抛出了IAE而不是NPE——对我来说,后者更好。 - Eugene
1
@Eugene 我也会投票给NPE,但我并不在意。实际上应该有一个INAE扩展两者,但我们不能这样做。我们可以有一个INAE extends NPE,但我不是介绍它的人。:D +++ 我更喜欢任何黑客,将两个原因合并为一个,包括ImmmutableList.copyOf(memory)(短小精悍而愚蠢;不满足OP的要求)。 - maaartinus
@maaartinus 然后只需将 ofNullable 更改为 of - 如果列表为 null,则会出现 NPE,如果列表包含 null,则会出现 IAE。 - Michael

6

对于第一个情况,我会使用Objects.requireNonNull()

我认为Optional不适用于这里,因为null是非法值。


可能会抛出NullPointerException,这可能不是OP想要的。 - Eugene
我假设OP希望基于“对象不能为空”错误消息实现这种行为 :) - ps-aux
一个 require-non-null 正是 OP 所期望的。为了防止后面出现 NPE,抛出 NPE 是一种正确(反常)的快速失败策略。 - Joop Eggen
6
我总是更喜欢一种不需要解析消息就能知道发生了什么的异常类型,即NullPointerException。可能值得为OP的用例添加实际示例,例如Objects.requireNonNull(memory, "ERROR: memory对象不能为空。");memory.forEach(o -> Objects.requireNonNull(o, "ERROR: memory对象不能包含空值。"));我认为这些演示了相对于复杂的Optional(滥用)来说,这些也是最简洁和直观的结构。更不用说性能差异了... - Holger

5

对于这种情况,我通常避免使用Optional,因为它往往会使事情变得不清晰。

但首先,我想提到的是,原始代码允许调用者保留现在是包含类的内部字段memory的引用。也许您相信调用者不会恶意操作,但调用者可能会意外地重用作为参数传递的列表。即便进行了细致的参数检查,如果发生这种情况,memory列表最终可能包含空值。或者,它可能会意外更改,导致其他故障。

解决方案是对参数列表进行防御性拷贝。直接的做法如下:

public void setMemory(ArrayList<Integer> memory) {
    if (memory == null)
        throw new IllegalArgumentException("memory is null");

    List<Integer> temp = new ArrayList<>(memory);

    if (temp.contains(null))
        throw new IllegalArgumentException("memory contains null");

    this.memory = temp;
}

请注意,在检查之前,复制的内容被存储在本地变量temp中。显然,在列表检查是否包含空值之前,你不希望将其存储到字段中。但是应该在副本上检查是否包含空值,而不是在参数列表上检查,否则,调用者可能会在检查后但在复制之前修改列表。(是的,这有点多虑。)
如果您不关心确切的异常消息,可以缩短如下:
public void setMemory(ArrayList<Integer> memory) {
    List<Integer> temp;
    if (memory == null || ((temp = new ArrayList<>(memory)).contains(null)))
        throw new IllegalArgumentException("memory is or contains null");
    this.memory = temp;
}

现在这段代码可以使用Optional进行重写:
public void setMemory(ArrayList<Integer> memory) {
    this.memory = Optional.ofNullable(memory)
                          .map(ArrayList::new)
                          .filter(list -> ! list.contains(null))
                          .orElseThrow(() -> new IllegalArgumentException("memory is or contains null"));
}

与我经常看到的Optional的常规滥用相比,这个并不太糟糕。这里的链接用于避免创建本地变量,这是一个小小的优势。逻辑相当简单,特别是如果一个人在头脑中有Optional。然而,我对重新访问这段代码(比如说一个月后)感到有些担忧。你可能需要花一段时间来凝视它,并使自己相信它做了你打算做的事情。
最后,还有一些通用的风格注释。
  1. 通常的偏好(至少在JDK中)是对这些情况使用NullPointerException。这些示例中我坚持使用IllegalArgumentException,因为这是OP正在使用的。

  2. 我建议使用List<Integer>代替参数类型和可能的字段类型ArrayList<Integer>。这将使得在适当的情况下可以使用不可修改的列表(例如,使用JDK 9的List.of)。


我在选择正确的异常时遵循了这个链接:(https://dev59.com/HHVD5IYBdhLWcg3wXaYd#47710),所以我有了IAE而不是NPE。将参数类型更改为List并复制传递的ArrayList似乎是一个好建议,谢谢。 - Jump3r
关于防御性复制的观点很好,但是你在布尔条件中的赋值让我崩溃了 :) 这是一个聪明的想法,但为了可读性,我会选择 if-else-assignment-if 的流程。 - Andrew Tobilko
2
当您遵循使用NullPointerException的建议并且不关心消息时,它就像这样简单:memory = new ArrayList<>(memory);/* (copies and throws NPE if memory is null) */ memory.forEach(Objects::requireNonNull); this.memory = memory;。哦,值得一提的是,参数应该优先使用抽象类型,即List而不是ArrayList,特别是当我们创建一个具有所需类型ArrayList的防御性副本时。 - Holger
我认为复制内存不是一个好主意。你已经检查了内存不为空,所以可以使用 memory.contains(null) - Richard Chan

2

首先,使用更通用的列表类型作为输入参数可能是一个好主意,因此请更改您的实现:

public void setMemory(List<Integer> memory) {
    //stuff
}

正如其他人所提到的那样,对于每个“set”操作都检查空值有点过头了。

如果这个“内存列表”来自你的一些代码并且你可以使用guava,那么可能使用guava的不可变列表。当有人试图向你的列表中添加“null”时,该列表会抛出异常。

ImmutableList.of( //your Integers)

如果您无法使用Guava,但仍想使用该方法,您可以编写自己的列表实现,为您执行此空值检查。

最后,如果这些都不适用于您,只需保留您的代码即可。它非常易于阅读,每个人都知道您正在做什么。使用Optionals可能会非常令人困惑,正如您在其他答案中所看到的那样。


1

使用可选项的一行代码:

public void setMemory(ArrayList<Integer> memory) {
    this.memory = Optional.ofNullable(memory).map((a) -> Optional.ofNullable(a.contains(null) ? null : a).orElseThrow(() -> new IllegalArgumentException("ERROR: memory object can't contain null value."))).orElseThrow(() -> new IllegalArgumentException("ERROR: memory object can't be null."));
}

如果你不使用换行符,一切都可以写成单行代码 ;) - Michael
好的,IDE自动格式化程序不会破坏那一行。那么称它们为“无大括号”、“单语句”、“单块”怎么样:P - Marcos Zolnowski

1

非常抱歉又要添加一个答案,但基于阅读问题的评论,可能有一种更好的方法可以更改方法的签名:用IntStream替换ArrayList<Integer>:

public void setMemory(@NonNull IntStream input) {
    Objects.requireNonNull(input);

    this.memory = ...; // collect the stream into the storage
}

原始流不需要(取消)装箱成本。
这样,您就不必担心调用者在您的脚下更改列表内容,并且将能够选择适合整数存储的存储方式,如我在其他答案中所解释的那样(甚至可以懒惰地解析流内容!)。

假设我将坚持使用List,那么intStream.boxed().collect(Collectors.toList())会对性能产生负面影响吗?将其装箱到List中是否会导致O(n)复制并分配给内存字段?我不理解“原始流不会产生(取消)装箱成本。”,但是你仍然必须从流中逐个获取并将其装箱到List中,或者我理解错了?无论如何,感谢提供另一种方法。我只能添加这个List将包含许多小的(<128)整数。 - Jump3r
我只能补充一点,这个列表将包含许多小的(<128)整数。在这种情况下,您应该继续使用ArrayList。关于(取消)装箱的主要问题不是内存使用/复制,而是GC负担。由于所有的整数都适合于The Great Integer Cache,您不需要担心阻塞GC。 - user1643723

1
请勿使用可选项,它们在这里对您没有好处。
相反,在ArrayList的位置上使用更合适的类型。在集合中存储整数会产生(不)装箱成本,并且当不允许空值时没有意义。
一些可能更适合您需求的集合库:
1. HPPC集合(我最喜欢的,但API与Java Collection框架不兼容) 2. Koloboke 3. Fastutil
所有这些库都为原始数据类型提供了专门的列表、映射和其他容器的实现。这些实现通常比涉及ArrayList的任何内容快得多(除非您的ArrayList中的所有整数都足够小以适合全局Integer实例缓存)。
作为一个很好的副作用,使用专门的原始整数列表不会默认允许调用者存储null值。

-1

如果您需要两个不同的异常和更多的功能风格,那么我的解决方案可能只能用于此。但它看起来很复杂,甚至更长。

.map(e -> false) 将列表元素(在本例中为整数)映射到布尔值,这是 filter() 所需的。

this.memory = Optional.ofNullable(memory)
            .orElseThrow(() -> new IllegalArgumentException("ERROR: memory object can't be null."))
            .stream()
            .filter(element -> 
                    Optional.ofNullable(element)
                    .map(e -> true)
                    .orElseThrow(
                            () -> new IllegalArgumentException("ERROR: memory object can't contain null value.")))
            .collect(Collectors.toList());

7
我想知道下一个处理这段代码的人会对它有何看法。 - Eugene
1
@Eugene 还提出了一个问题,如果你要创建一个新的 List,为什么不直接删除 null 值并继续进行。 - Michael
@Eugene 您是正确的,您的解决方案更加优雅,但如果您需要在空元素的情况下获得精确的异常消息,则存在问题。 - Mikita Herasiutsin
map(e -> false) 做了什么?你不觉得它比原始代码更复杂吗? - Andrew Tobilko
@AndrewTobilko 它将列表的元素(在这种情况下是整数)映射到布尔值,这对于filter(方法)是必需的。所以这是一个不太正规的技巧... - Mikita Herasiutsin
将映射到false意味着过滤器将始终失败,导致的列表始终为空。你应该映射到true。更好的方法是,删除hacky映射,并将过滤器更改为映射。 - Michael

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