提问人:Tom Hawtin - tackline 提问时间:3/9/2020 最后编辑:Tom Hawtin - tackline 更新时间:3/14/2020 访问量:801
如何安全地复制收藏夹?
How can I safely copy collections?
问:
过去,我曾说过要安全地复制集合,请执行以下操作:
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
我对投掷、、、等方法感到满意,甚至可能挂起。ConcurrentModificationException
NullPointerException
IllegalArgumentException
ClassCastException
我选择了不可变类型参数作为示例。对于这个问题,我对具有自己的陷阱的可变类型集合的深度副本不感兴趣。String
答:
在普通 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
另一件事,你可以责怪 的构造函数,是对传入集合实现的信任。 不会以同样的方式受到影响,而只是因为传递数组没有这样的性能提升,就像在构造 .这两个类都不保证构造函数中的保护。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
或者与 Java 9 或更高版本一起使用
请注意,Java 10 也这样做,但其文档并未说明它不能保证不信任传入集合的方法。因此,调用 ,如果它返回基于数组的列表,它肯定会制作一个副本,更安全。List<String> newStrs = List.of(strs.toArray(new String[0]));
List.copyOf(strs)
toArray
List.of(…)
由于调用方无法更改方式,因此数组工作,将传入的集合转储到数组中,然后用它填充新集合,将始终使副本安全。由于集合可以保存对返回的数组的引用,如上所述,因此它可以在复制阶段更改它,但不能影响集合中的副本。
因此,任何一致性检查都应该在从数组中检索特定元素或整个生成的集合后进行。
评论
AccessController.doPrivileged(…)
new ArrayList<>(…)
List.copyOf(strs)
ArrayList
toArray()
new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))
List.of(strs.toArray(new String[0]))
copyOf
我宁愿在评论中留下这些信息,但我没有足够的声誉,对不起:)然后我会尽量详细地解释它。
在 Java 中,最初使用的不是像 C++ 中那样的修饰符来标记不应该修改对象内容的成员函数,而是使用了“不变性”的概念。封装(或OCP,开闭原则)应该防止物体的任何意外突变(变化)。当然,反射 API 可以解决这个问题;直接内存访问也是如此;这更多的是关于拍摄自己的腿:)const
java.util.Collection
本身是可变接口:它有应该修改集合的方法。当然,程序员可能会将集合包装成一些东西,这些东西会抛出......所有运行时异常都会发生,因为另一个程序员无法读取 javadoc,它清楚地表明该集合是不可变的。add
我决定使用类型在我的接口中公开不可变集合。在语义上,集合不具有“可变性”这样的特征。不过,您(很可能)能够通过流修改基础集合。java.util.Iterable
Iterable
JIC,以不可变的方式公开映射(映射的方法符合此定义)java.util.Function<K,V>
get
评论
Iterator
forEachRemaining
forEach
Iterator
remove
Iterable
forEach*
)
评论
NavigableSet
Comparable
compareTo()
HashSet
hashCode
TreeSet
PriorityQueue
Comparator
EnumSet
enum
javac
new TreeSet<>(strs)
strs
NavigableSet
TreeSet
toArray()
TreeSet
checkcast
toArray