如何确定一个Java集合是否包含null

3
我想编写一个方法,该方法以集合作为参数。但是,集合不能包含null。如果它包含null,那么当不再清楚null来自何处时,这将导致后续出现空指针异常。因此,我想验证输入。天真的方法是:
public void doSomething(Collection<?> collection) {
    if (collection.contains(null))
        throw new IllegalArgumentException("collection cannot contain null!");

    // do something
}

然而,如果集合不支持 null 值,Collection.contains 可能会抛出 NullPointerException。事实上,Java 自己的不可变集合就是这样做的。因此,在朴素的方法中,以下代码将不起作用:

doSomething(List.of("a", "b", "c")); // NullPointerException!

有几种可能的解决方案,但没有一种感觉是正确的。

例如,我可以自己搜索null

public void doSomething(Collection<?> collection) {
    for (var e : collection) {
        if (e == null)
            throw new IllegalArgumentException("collection cannot contain null!");
    }
    // do something
}

但是现在我正在进行线性搜索,尽管该集合可能是一个 HashSet 或 TreeSet,这会提供更好的性能。

或者我可以捕捉 NullPointerException 异常:

public void doSomething(Collection<?> collection) {
    try {
        if (collection.contains(null))
            throw new IllegalArgumentException("collection cannot contain null!");
    } catch (NullPointerException e) {
        // Apparently, collection doesn't support null. This is good!
    }
    // do something
}

但这感觉非常不安全。我真的能确定那个NullPointerException是由于集合不支持null引起的,还是我只是吞掉了一个可以提供有关错误信息的异常?

有没有一种好的方法来确定集合是否包含null?


2
让我们花点时间思考一下,在使用集合的contains方法时,你可能会因为什么其他原因而得到NullPointerException。就我所知,唯一的其他情况是如果集合本身为null。因此,对我来说,这似乎是一个不错的选择,除非你需要在集合为null的情况下执行其他操作。 - DevWithZachary
2
为什么您认为带有消息的 IllegalArgumentException 比内在告诉我们问题是 nullNullPointerException 更好? - Holger
3个回答

4

我认为用IllegalArgumentException替换NullPointerException并不是一种改进。这需要我们读取消息来获取专用异常类型内在告诉我们的信息。

当然,尽早检查传入的参数,避免将问题源与异常发生的地方分离开来,是一件好事。例如,当我使用:

public static void main(String[] args) {
    new NullCheckExample().doSomething(Arrays.asList("foo", "bar", null));
}

public void doSomething(Collection<?> collection) {
    collection.forEach(Objects::requireNonNull);

    // do something
}

我理解

Exception in thread "main" java.lang.NullPointerException
    at java.base/java.util.Objects.requireNonNull(Objects.java:208)
    at java.base/java.util.Arrays$ArrayList.forEach(Arrays.java:4204)
    at NullCheckExample.doSomething(NullCheckExample.java:12)
    at NullCheckExample.main(NullCheckExample.java:8)

我认为它足够表达,以帮助追踪原因。

现在,如果null不可能出现,有没有避免线性搜索成本的方法?理论上是有的。我们可以这样做:

public void doSomething(Collection<?> collection) {
    Spliterator<?> sp = collection.spliterator();
    if(sp.hasCharacteristics(Spliterator.NONNULL)) {
        // yeah, null is impossible
    }
    else if(sp.hasCharacteristics(Spliterator.SORTED) && sp.getComparator() == null) {
        // natural order precludes null too
    }
    else {
        // here we have to check,
        // collection.forEach(Objects::requireNonNull)
        // or 
        // if(collection.contains(null)) throw ...
    }

    // do something
}

这段代码适用于某些集合,例如并发集合或者TreeSet。问题是,它不能适用于List.of(…) 或者 Set.of(…)这种类型的集合。在参考实现(OpenJDK)中,spliterator方法没有被重新定义,这不仅意味着Stream操作效率较低,而且我们也无法获得NONNULLIMMUTABLE特征。

因此,在这些情况下,我们最终会进行不必要的线性搜索,或者需要处理contains(null)抛出NullPointerException的情况(通常情况下,而不是错误情况下)。

根据您的需求,您可能需要像这样做:

public void doSomething(Collection<?> collection) {
    List<?> workingCopy = List.copyOf(collection);

    // do something
}

这里进行了一次复制操作,但它确保您免受之后对集合的修改影响,这可能是必要的,以确保一致性。拷贝副本保证不含 null 且不会改变。如果调用者将 List.of(…) 的结果传递给此方法,则已经满足条件,copyOf 将成为无操作,并返回参数。因此,在大多数情况下,您预期输入为不可变列表时,这是最佳选择。

但是,如果您要说两者都不是令人满意的解决方案,我会同意。应该有有效的方式来防止出现 null。拒绝查询 null 的尝试很好,但不应该导致没有有效的方式来确保没有 null。令人困惑的是,尽管其实现在大多数情况下只需要一行代码即可,但不可变集合中没有有效的 spliterator()forEach 实现。


异常的类型并不是我提出问题的原因,而是抛出异常的时间。我更喜欢在我做错事情的时候(比如传递了一个包含空值的集合),立即得到异常提示,而不是在以后的某个时间点(甚至可能是在另一个方法调用期间)才发现空值元素,并且还要确定这个空值来自哪里。 - Hoopje

1

list.stream().anyMatch(Objects::isNull)

如果它包含空值,则返回true

0
如果您的Java版本是>= 8,则可以使用流API:
List<YourObject> newList = userListCollection.stream().filter(Objects::nonNull).collect(Collectors.toList);

我认为,stream()方法针对每种集合类型进行了优化,因此您不应该遇到性能问题。但是,您仍然需要检查整个集合是否为空。


2
流并不是魔法。这仍然会处理所有元素,而这正是OP想要避免的。而且,令人惊讶的是,在不可变集合的情况下,即List.of(…)的情况下,OP提到的流并没有被优化。 - Holger

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