如何安全地复制收藏夹?

How can I safely copy collections?

提问人:Tom Hawtin - tackline 提问时间:3/9/2020 最后编辑:Tom Hawtin - tackline 更新时间:3/14/2020 访问量:801

问:

过去,我曾说过要安全地复制集合,请执行以下操作:

public static void doThing(List<String> strs) {
    List<String> newStrs = new ArrayList<>(strs);

public static void doThing(NavigableSet<String> strs) {
    NavigableSet<String> newStrs = new TreeSet<>(strs);

但是,这些“复制”构造函数,类似的静态创建方法和流,真的安全吗,规则在哪里指定?我所说的安全是指 Java 语言提供的基本语义完整性保证,以及针对恶意调用者强制执行的集合,假设有合理的支持并且没有缺陷。SecurityManager

我对投掷、、、等方法感到满意,甚至可能挂起。ConcurrentModificationExceptionNullPointerExceptionIllegalArgumentExceptionClassCastException

我选择了不可变类型参数作为示例。对于这个问题,我对具有自己的陷阱的可变类型集合的深度副本不感兴趣。String

(需要明确的是,我已经查看了 OpenJDK 源代码,并对 ArrayListTreeSet 有某种答案。

Java 集合 复制构造函数

评论

2赞 Kayaman 3/9/2020
安全是什么意思?一般来说,集合框架中的类往往工作类似,只是在 javadocs 中指定了例外。复制构造函数与任何其他构造函数一样“安全”。你有没有特别想到的事情,因为问集合副本构造函数是否安全听起来非常具体?
1赞 Kayaman 3/9/2020
好吧,其他基于集合的集合有时可以检测类是否未正确实现并引发异常。目前还不清楚你所说的不可信的论点是什么意思。你的意思是一个坏人制作了一个坏字符串的集合,当你把它们复制到你的集合中时,会发生不好的事情?不,集合框架非常可靠,它从 1.2 开始就已经存在了。NavigableSetComparablecompareTo()
1赞 Holger 3/13/2020
@JesseWilson你可以在不入侵其内部的情况下破坏许多标准集合,(以及所有其他哈希集合)依赖于元素实现的正确性/完整性,并依赖于(如果有的话,你甚至不能在不接受自定义比较器的情况下创建等效副本),信任特定类型的完整性,这在编译后永远不会被验证, 因此,一个类文件,不是用手生成的,也不是手工制作的,可以颠覆它。HashSethashCodeTreeSetPriorityQueueComparatorEnumSetenumjavac
1赞 Holger 3/13/2020
在您的示例中,您有 where 是 .这不是大容量复制,因为生成的比较器将使用源的比较器,这甚至是保留语义所必需的。如果您只处理包含的元素就可以了,那么这是要走的路;它甚至会保持迭代顺序。当你对“获取元素、验证元素、使用元素”感到满意时,你甚至不需要制作副本。当您想要验证所有元素,然后使用所有元素时,问题就开始了。然后,你不能相信带有自定义比较器的副本new TreeSet<>(strs)strsNavigableSetTreeSettoArray()TreeSet
1赞 Holger 3/16/2020
唯一对每个元素具有 a 效果的大容量复制操作是使用特定类型。我们总是以此为终点。泛型集合甚至不知道其实际元素类型,因此其复制构造函数无法提供类似的功能。当然,您可以将任何检查推迟到正确的使用,但是,我不知道您的问题的目的是什么。你不需要“语义完整性”,当你在使用元素之前立即检查和失败时。checkcasttoArray

答:

12赞 Holger 3/9/2020 #1

在普通 API(如集合 API)中,没有真正的保护措施来防止在同一 JVM 中运行的故意恶意代码。

可以很容易地证明:

public static void main(String[] args) throws InterruptedException {
    Object[] array = { "foo", "bar", "baz", "and", "another", "string" };
    array[array.length - 1] = new Object() {
        @Override
        public String toString() {
            Collections.shuffle(Arrays.asList(array));
            return "string";
        }
    };
    doThing(new ArrayList<String>() {
        @Override public Object[] toArray() {
            return array;
        }
    });
}

public static void doThing(List<String> strs) {
    List<String> newStrs = new ArrayList<>(strs);

    System.out.println("made a safe copy " + newStrs);
    for(int i = 0; i < 10; i++) {
        System.out.println(newStrs);
    }
}
made a safe copy [foo, bar, baz, and, another, string]
[bar, and, string, string, another, foo]
[and, baz, bar, string, string, string]
[another, baz, and, foo, bar, string]
[another, bar, and, foo, string, and]
[another, baz, string, another, and, foo]
[string, and, another, foo, string, foo]
[baz, string, foo, and, baz, string]
[bar, another, string, and, another, baz]
[bar, string, foo, string, baz, and]
[bar, string, bar, another, and, foo]

正如你所看到的,期望 a 并不能保证实际获得实例列表。由于类型擦除和原始类型,列表实现端甚至无法修复。List<String>String

另一件事,你可以责怪 的构造函数,是对传入集合实现的信任。 不会以同样的方式受到影响,而只是因为传递数组没有这样的性能提升,就像在构造 .这两个类都不保证构造函数中的保护。ArrayListtoArrayTreeMapArrayList

通常,尝试编写代码是没有意义的,假设每个角落都有故意的恶意代码。它可以做的太多了,可以防止一切。这种保护只对真正封装了可能让恶意调用者访问某些内容的操作的代码有用,如果没有此代码,它就无法访问。

如果您需要特定代码的安全性,请使用

public static void doThing(List<String> strs) {
    String[] content = strs.toArray(new String[0]);
    List<String> newStrs = new ArrayList<>(Arrays.asList(content));

    System.out.println("made a safe copy " + newStrs);
    for(int i = 0; i < 10; i++) {
        System.out.println(newStrs);
    }
}

然后,您可以确定它只包含字符串,并且在构造后不会被其他代码修改。newStrs

或者与 Java 9 或更高版本一起使用
请注意,Java 10 也这样做,但其文档并未说明它不能保证不信任传入集合的方法。因此,调用 ,如果它返回基于数组的列表,它肯定会制作一个副本,更安全。
List<String> newStrs = List.of(strs.toArray(new String[0]));List.copyOf(strs)toArrayList.of(…)

由于调用方无法更改方式,因此数组工作,将传入的集合转储到数组中,然后用它填充新集合,将始终使副本安全。由于集合可以保存对返回的数组的引用,如上所述,因此它可以在复制阶段更改它,但不能影响集合中的副本。

因此,任何一致性检查都应该在从数组中检索特定元素或整个生成的集合后进行。

评论

2赞 Holger 3/9/2020
Java 的安全模型的工作原理是向代码授予堆栈上所有代码的权限集的交集,因此当代码的调用者让你的代码执行意想不到的事情时,它仍然不会获得比最初更多的权限。因此,它只会使您的代码执行恶意代码在没有您的代码的情况下也可以做的事情。您只需通过等方式强化您打算以提升的权限运行的代码。但是一长串与小程序安全相关的错误给了我们一个提示,为什么这项技术被放弃了......AccessController.doPrivileged(…)
1赞 Holger 3/9/2020
但是我应该插入“在普通的 API 中,如集合 API”,因为这是我在答案中关注的重点。
2赞 Holger 3/10/2020
为什么要强化显然与安全无关的代码,以防特权代码允许恶意集合实现溜进来?在调用代码之前和之后,该假设的调用方仍会受到恶意行为的影响。它甚至不会注意到您的代码是唯一行为正常的代码。假设集合实现正确,则可以用作复制构造函数。当为时已晚时,您没有义务解决安全问题。受损的硬件怎么办?操作系统?多线程怎么样?new ArrayList<>(…)
2赞 Holger 3/10/2020
我不是在提倡“没有安全”,而是在正确的地方提供安全,而不是在事后试图修复一个破碎的环境。这是一个有趣的说法,即“有许多集合没有正确实现它们的超类型”,但它已经走得太远了,要求证明,进一步扩展了这一点。原来的问题已经完全回答了;你现在带来的要点从来都不是其中的一部分。如前所述,不依赖于传入的集合在这方面的正确性,价格显而易见。 是日常合理的妥协。List.copyOf(strs)ArrayList
5赞 Holger 3/10/2020
它清楚地表明,对于所有“类似的静态创建方法和流”,都没有这样的规范。因此,如果你想绝对安全,你必须调用自己,因为数组不能有覆盖行为,然后创建数组的集合副本,比如 or .两者都具有强制执行元素类型的副作用。就我个人而言,我不认为他们会允许妥协不可变的集合,但答案中有替代方案。toArray()new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))List.of(strs.toArray(new String[0]))copyOf
1赞 Alexander 3/14/2020 #2

我宁愿在评论中留下这些信息,但我没有足够的声誉,对不起:)然后我会尽量详细地解释它。

在 Java 中,最初使用的不是像 C++ 中那样的修饰符来标记不应该修改对象内容的成员函数,而是使用了“不变性”的概念。封装(或OCP,开闭原则)应该防止物体的任何意外突变(变化)。当然,反射 API 可以解决这个问题;直接内存访问也是如此;这更多的是关于拍摄自己的腿:)const

java.util.Collection本身是可变接口:它有应该修改集合的方法。当然,程序员可能会将集合包装成一些东西,这些东西会抛出......所有运行时异常都会发生,因为另一个程序员无法读取 javadoc,它清楚地表明该集合是不可变的。add

我决定使用类型在我的接口中公开不可变集合。在语义上,集合不具有“可变性”这样的特征。不过,您(很可能)能够通过流修改基础集合。java.util.IterableIterable


JIC,以不可变的方式公开映射(映射的方法符合此定义)java.util.Function<K,V>get

评论

0赞 Tom Hawtin - tackline 3/14/2020
只读接口和不可变性的概念是正交的。C++ 和 C 的要点是它们不支持语义完整性。还复制了对象/结构参数 - const & 是一个狡猾的优化。如果你要通过一个,那么这实际上会强制一个元素副本,但这并不好。使用 / 显然将是一场彻头彻尾的灾难。(我还不得不提,这是有方法的。IteratorforEachRemainingforEachIteratorremove
0赞 Alexander 3/15/2020
如果看一下 Scala 集合库,就会发现可变接口和不可变接口之间有严格的区别。虽然(我想)它是出于完全不同的原因而这样做的,但仍然展示了如何实现安全性。只读接口在语义上假设不可变性,这就是我想说的。(我同意 实际上不是不可变的,但没有看到任何问题IterableforEach*)