如何安全地复制收藏集?


9

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

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。)


2
安全是什么意思?一般而言,集合框架中的类往往工作类似,但javadocs中指定了例外。复制构造函数与任何其他构造函数一样“安全”。您是否有特定的想法,因为询问集合副本构造函数是否安全听起来很具体?

1
好吧,NavigableSet其他Comparable基于集合的集合有时可以检测到类是否未compareTo()正确实现并引发异常。不清楚不可信参数的含义。您的意思是说邪恶的人会制作坏字符串的集合,当您将它们复制到集合中时,会发生不好的事情吗?不,集合框架非常可靠,从1.2开始就存在。
Kayaman

1
@JesseWilson,您可以在不侵入其内部的情况下折衷许多标准集合,HashSet(以及所有其他散列集合)通常依赖于hashCode元素实现的正确性/完整性,TreeSet并且PriorityQueue依赖于Comparator(而且您甚至无法创建一个等效副本而不接受自定义比较器(如果存在),则EnumSet信任特定enum类型的完整性,该类型在编译后从未进行过验证,因此,不是由生成javac或手工生成的类文件可以颠覆它。
Holger

1
在你的例子,你有new TreeSet<>(strs)哪里strsNavigableSet。这不是批量复制,因为结果TreeSet将使用源的比较器,这甚至对于保留语义是必要的。如果只处理包含的元素toArray()就可以了,那就去吧;它甚至可以保持迭代顺序。如果您对“采用元素,验证元素,使用元素”没问题,那么您甚至无需复制。当您要验证所有元素,然后使用所有元素时,问题就开始了。然后,您将无法信任TreeSet带有自定义比较器的副本
Holger

1
checkcast对于每个元素,唯一具有a效果的批量复制操作具有toArray特定类型。我们总是以它结束。通用集合甚至都不知道其实际元素类型,因此其副本构造函数无法提供类似的功能。当然,您可以将任何支票推迟到正确的使用前,但是,我不知道您的问题是针对什么。当您可以在使用元素之前进行检查并立即失败时,就不需要“语义完整性”。
霍尔格

Answers:


12

对于普通API(例如Collection 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实例列表。由于类型擦除和原始类型,列表实现方面甚至无法修复。

您可以归咎于的另一件事ArrayList是对传入集合的toArray实现的信任。TreeMap不会以相同的方式受到影响,但这仅仅是因为传递数组不会获得这种性能提升,就像构造时一样ArrayList。这两个类都不保证构造函数中的保护。

通常,假设每个角落都有故意的恶意代码,尝试编写代码都是没有意义的。它可以做很多事情来保护一切。此类保护仅对确实封装了可以使恶意调用者访问某些内容的动作的代码有用,而没有此代码就无法访问该行为。

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

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它仅包含字符串,并且在构造后不会被其他代码修改。

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

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

因此,应在从数组中检索特定元素之后或对整个结果集合进行任何一致性检查。


2
Java的安全模型通过向代码授予堆栈上所有代码的权限集的交集来工作,因此,当代码的调用者使您的代码执行意外的事情时,它仍然没有获得比最初更多的权限。因此,它只会使您的代码执行恶意代码本可以在没有您的代码的情况下完成的工作。您只需要加固您打算通过AccessController.doPrivileged(…)等方式使用提升的特权运行的代码即可。但是与applet安全性相关的bug的清单很长,这向我们提示了为何放弃该技术的原因……
Holger

1
但是我应该插入“像Collection API这样的普通API”,因为这就是我在回答中关注的重点。
Holger

2
为什么您应该对显然与安全性不相关的代码进行加固,使其免受特权代码的影响,而特权代码却确实允许恶意收集实现潜入?在调用您的代码之前和之后,该假想的调用方仍将受到恶意行为的攻击。甚至不会注意到您的代码是行为正确的唯一代码。new ArrayList<>(…)假设正确的集合实现,可以将其用作副本构造函数。太早了,解决安全问题不是您的责任。受损的硬件呢?操作系统?多线程怎么样?
Holger

2
我不是在提倡“没有安全性”,而是在正确的地方提供安全性,而不是在事后尝试修复损坏的环境。一个有趣的说法是,“ 有许多集合不能正确实现其父类型 ”,但是它已经走得太远了,无法寻求证明,进一步扩大了它的范围。原来的问题已经完全回答了;您现在提出的要点从来都不是其中的一部分。如前所述,List.copyOf(strs)在这方面,它不依赖传入集合的正确性,而以明显的价格。ArrayList是每天合理的折衷方案。
霍尔格

4
它清楚地表明,对于所有“类似的静态创建方法和流”,没有这样的规范。因此,如果您想绝对安全,就必须toArray()自欺欺人,因为数组不能具有覆盖行为,然后创建数组的集合副本,例如new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))List.of(strs.toArray(new String[0]))。两者都还具有强制元素类型的副作用。我个人认为,它们永远不会允许copyOf破坏不可变的集合,但是答案中有其他选择。
Holger

1

我希望将此信息留在评论中,但我没有足够的声誉,对不起:)我将尽我所能解释尽可能详细。

代替了const在C ++中使用类似修饰符的功能来标记不应修改对象内容的成员函数,在Java中最初使用了“不变性”的概念。封装(或OCP,开-闭原理)应该用于防止对象的任何意外突变(更改)。当然,反射API可以解决这个问题;直接内存访问的作用相同;那更多是关于射击自己的腿:)

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

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


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


只读接口和不变性的概念是正交的。C ++和C的要点是它们不支持语义完整性。还复制对象/结构参数-const&对此进行了狡猾的优化。如果要通过,Iterator那么实际上会强制执行逐元素的副本,但这并不好。使用forEachRemaining/ forEach显然将是一场彻底的灾难。(我也不得不提到Iterator有一种remove方法。)
汤姆·霍顿

如果查看Scala集合库,则在可变接口和不可变接口之间有严格的区别。尽管(我想)这样做是出于完全不同的原因,但是仍然证明了如何实现安全性。只读接口在语义上假设不可变,这就是我要说的。(我同意Iterable'实际上并不是一成不变的,但是看不到任何问题forEach*
亚历山大
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.