为了安全起见,是否有必要复制列表?

Is it necessary to copy a list to be safe?

提问人:k314159 提问时间:11/13/2023 最后编辑:Basil Bourquek314159 更新时间:11/15/2023 访问量:170

问:

Stream.toList 的实现(和文档)是这样的:

Collections.unmodifiableList(new ArrayList<>(Arrays.asList(this.toArray())))

我想知道为什么需要将返回的列表复制到新的.仅仅返回以下内容还不够吗?Arrays.asListArrayList

Collections.unmodifiableList(Arrays.asList(this.toArray()))

我想知道,如果我编写一个方法返回它像这样创建的列表,如果我不费心制作它的防御性副本,会有什么问题吗?

Java 列表

评论

3赞 k314159 11/13/2023
对于由于“基于意见”而推荐“关闭”的人:它怎么可能是基于意见的?省略复制列表是不安全的,也不是不安全的。如果它不安全,您应该能够描述一个可能危及安全性的场景。如果它是安全的,你应该能够证明它。那里没有任何基于意见的东西。
0赞 TylerH 11/15/2023
可能是因为您没有使用“安全”手段进行定义。你认为安全的东西,别人可能认为不安全。在确定这一点之前,这个问题是一个见仁见智的问题。它也不止一点点广泛。此外,您在第一个问题中所说的“足够”是什么意思?够什么?编译?要跑?为了达到预期的结果?别的?

答:

-3赞 JSelser 11/13/2023 #1

我没有答案,但我觉得它的实现很有趣,因为它还在里面创建了一个列表:Arrays.asList()

    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

我想知道发生了什么,也许是其中之一:“这仅适用于某些虚拟机,因此无法保证”

如果你不是在写一个库,老实说,我不介意太多。 顺便说一句,为了真正的安全,还需要是不可变的。T

编辑:有趣的东西,显然这是一个不同的,没什么奇怪的ArrayListArrayList

评论

8赞 BambooleanLogic 11/13/2023
使用的不是普通的,而是在自身内部定义的一个不相关的实现,恰好也被调用。因此,使用 an 创造的存在并不奇怪。ArrayListArraysjava.util.ArrayListArraysArrayListj.u.ArrayListArrays.ArrayList
3赞 Rifat Rubayatul Islam 11/13/2023 #2

由可修改集合支持的不可修改视图

来自 Javadoc(强调我的):

不可修改的视图集合是不可修改的集合,也是后备集合的视图。如上所述,其 mutator 方法引发 UnsupportedOperationException,而读取和查询方法则委托给后备集合。其效果是提供对后备集合的只读访问。这对于组件提供对内部集合的读取访问权限非常有用,同时防止他们意外修改此类集合。不可修改的视图集合的示例包括 Collections.unmodifiableCollection、Collections.unmodifiableList 和相关方法返回的集合。

请注意,对后备集合的更改可能仍是可能的,如果发生更改,则通过不可修改的视图可以看到这些更改。因此,不可修改的视图集合不一定是不可变的。但是,如果不可修改视图的后备集合实际上是不可变的,或者对后备集合的唯一引用是通过不可修改的视图,则可以认为该视图实际上是不可变的。

由于返回后备列表的 a,因此仍然可以修改后备列表,这将影响不可修改的列表。现在,该列表又由方法中提供的数组支持。所以,一切都取决于.由于它位于接口的默认方法中,因此它是否返回可修改的内容取决于流的实际实现。这就是为什么它需要被复制。Collections.unmodifiableListunmodifiable viewArrays.asListthis.toArray()StreamtoArray()

评论

0赞 k314159 11/13/2023
“仍然可以修改支持列表” - 是的,但这个支持列表是什么?它是 返回的列表。你怎么可能修改这个列表?对我来说,这似乎是不可修改的,因为它是不可访问的,它是在 API 方法中创建的东西,除非通过包装器才能出来。Arrays.asList(this.toArray())unmodifiable
3赞 BambooleanLogic 11/13/2023
@k314159 这是接口中的默认方法。不能保证会返回一个全新的数组。如果实现重用数组实例,则将追溯更改返回的列表(因为它只是底层数组的包装器)。因此,重点是 1) 获取数组,2) 创建一个适配器以使其可用作列表,以及 3) 在不与原始数组没有任何连接的情况下制作该列表的安全副本。(是的,重用数组是愚蠢的,但默认方法不能只是对实现细节做出疯狂的假设。toArrayStreamArrays.asList
2赞 k314159 11/13/2023
谢谢。似乎 Java API(在 OpenJDK 中)中的所有 Stream 实现都定义了创建一个全新的数组。但是,我猜你是说没有什么可以阻止用户编写自己的 Stream 实现来返回现有数组,因此必须复制它。toArray()
1赞 Rob Spoor 11/13/2023
@k314159正确。这些实现创建了全新的数组,但如前所述,规范不需要它,这允许其他实现返回相同的实例。
0赞 Magic Developement 11/13/2023 #3

在您提供的实现中创建新 ArrayList 的原因是为了确保返回的列表确实不可修改。Arrays.asList 方法返回由原始数组支持的固定大小的列表。

获取不可修改列表的另一种更现代的方法是使用 Stream API 中的 Collectors.toUnmodifiableList() 方法。例如:

Arrays.stream(this.toArray()).collect(Collectors.toUnmodifiableList());

此方法直接生成不可修改的列表,而无需创建中间 ArrayList。我希望这个建议能有所帮助。

评论

1赞 k314159 11/13/2023
谢谢,这是一个关于 .然而,在 Java 16 中引入的 比 (Java 10) 更“现代”。当你写 IntelliJ 时,IDEA 建议将其更改为 .toUnmodifiableList()Stream.toList()Collectors.toUnmodifiableList()collect(Collectors.toUnmodifiableList())toList()
1赞 user85421 11/13/2023
还要注意不允许元素;虽然这些是允许的Collectors.toUnmodifiableList()nulltoList()
9赞 rzwitserloot 11/13/2023 #4

它有一个目的

我想知道为什么需要将 Arrays.asList 返回的列表复制到新的 ArrayList。仅仅返回以下内容还不够吗?

我们在谈论哪个?如果我们谈论的是 中的那个,你是对的:该方法的 javadoc 保证它是一个新分配的数组,因此,不需要进行防御性复制。但是,这里不涉及:我们谈论的是 .它的 javadoc 要短得多:.toArray()j.u.CollectiontoArray()toArray()j.u.s.Stream.toArray()

    /**
     * Returns an array containing the elements of this stream.
     *
     * <p>This is a <a href="package-summary.html#StreamOps">terminal
     * operation</a>.
     *
     * @return an array, whose {@linkplain Class#getComponentType runtime component
     * type} is {@code Object}, containing the elements of this stream
     */

与此不同的是,这个 javadoc 对该数组的性质没有任何注释。因此,显然,javadoc 中的默认 impl 并不假定数组一定是“安全的”(“安全”是指:以后不会被修改)。j.u.Collection toArray()Stream

我想你知道,但要非常清楚:

Arrays.asList几乎总是一个错误。这是一个两全其美的列表:它会在你传递给它的数组周围创建一个轻量级包装器。所以,有效(因为这是可能的,而且确实是这样实现的),但不能(因为你不能改变数组的大小)。该列表不是不可变的(即,如果将其传递给您直接控制之外的代码,则需要广泛记录,或者制作防御性副本),但也不是功能齐全的列表。改用它,它使列表完全不可变 - 您可以将列表传递给您喜欢的任何人,而不必担心失去对完整性的控制。set()foo[x] = value;.add()List.of

编辑:并解释为什么它的 ) 而不是 - 那些静态方法上的不可变列表 make throw NPEs when 在那里,但流可以有 ,所以你不能使用它。此编辑由 @Rob Spoor 的评论带给您:)Collections.unmodifiableList(new ArrayList<...List.copyOfListnullnull

不过,这并不像你想象的那么大

请注意,您找到的代码只是默认实现 - 任何特定的 stream 实现都可以自由地想出更好的方法来做到这一点。几乎每个你必然会遇到的实现都会覆盖定义。例如,如果你调用 arraylist 的 ,你最终会得到这个实现,它来自 streamops,并且与核心库中所有集合使用的代码路径相同。因此,您最终使用的实际代码是这样的:stream()

    @Override
    public List<P_OUT> toList() {
        return SharedSecrets.getJavaUtilCollectionAccess()
        .listFromTrustedArrayNullsAllowed(this.toArray());
    }

这里的“可信数组”指的是传递给它的数组保证不会从你下面改变的概念。编译器无法强制执行该保证,但 arraylist 的(事实上,所有未损坏的集合,因为 javadoc 需要它! 创建新的数组,因此这样做是安全的,而且效率要高得多(避免了一堆不必要的副本)。j.u.CollectiontoArray()

评论

1赞 Rob Spoor 11/13/2023
如果您知道没有元素,那么“改用”是一个很好的建议。由于 的默认方法必须假定流可以包含元素,因此不能使用它。这是一个很好的替代方法,可以基于潜在的可变数组创建不可修改的列表。List.ofnullStreamnullCollections.unmodifiableList(new ArrayList<>(Arrays.asList(array)))
1赞 BambooleanLogic 11/13/2023
人们还可以注意到,默认实现(多年后被采用)可能是一种折衷方案,其中优先级(从高到低)是 1) 绝对确保这适用于过去几年编写的每个遵守合同的实现,2) 因为它只存在于向后兼容性中,并且它通常会被更好的实现覆盖, 不要让它比它必须的更聪明或更复杂,3)只要确保它不会太慢。就我个人而言,我发现实现是一个很好的平衡。Stream.toList()
2赞 samabcde 11/13/2023 #5

在谈论安全时,我们通常指的是对象是否不可变

  • unmodifiableList -> 不可修改 这里只保证调用方不能改变包装列表的状态,并且源的更改仍然会反映在包装列表中。
  • new ArrayList<> -> 防御性复制意味着修改源不会改变复制的状态,反之亦然。
  • 不可修改 + 防御性复制 ->不可变,您可以假设列表始终包含相同的对象引用列表。

举例说明差异

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class UnmodifiableListCopy {
    public static void main(String[] args) {
        String[] source = new String[]{"dummy"};
        List<String> unmodifiable = unmodifiableListWithoutCopy(source);
        List<String> immutable = unmodifiableListWithCopy(source);
        System.out.println("unmodifiable:" + unmodifiable);
        System.out.println("immutable:" + immutable);
        source[0] = "modified";
        System.out.println("--- change source ---");
        System.out.println("unmodifiable:" + unmodifiable);
        System.out.println("immutable:" + immutable);
    }

    private static <T> List<T> unmodifiableListWithoutCopy(T[] arr) {
        return Collections.unmodifiableList(Arrays.asList(arr));
    }

    private static <T> List<T> unmodifiableListWithCopy(T[] arr) {
        return Collections.unmodifiableList(new ArrayList<>(Arrays.asList(arr)));
    }
}

运行代码时应看到以下内容。

unmodifiable:[dummy]
immutable:[dummy]
--- change source ---
unmodifiable:[modified]
immutable:[dummy]

参考