Java中的不可变数组
Posted
技术标签:
【中文标题】Java中的不可变数组【英文标题】:Immutable array in Java 【发布时间】:2011-04-11 16:29:20 【问题描述】:Java 中的原始数组是否有不可变的替代方案?制作一个原始数组final
实际上并不能阻止人们做类似的事情
final int[] array = new int[] 0, 1, 2, 3;
array[0] = 42;
我希望数组的元素是不可更改的。
【问题讨论】:
帮自己一个忙,停止在 Java 中使用数组来处理除 1) io 2) 繁重的数字运算 3) 如果您需要实现自己的列表/集合(这很少见我>)。它们非常不灵活且过时......正如您刚刚在这个问题中发现的那样。 @Whaley,以及不需要“动态”数组的性能和代码。数组在很多地方仍然有用,这并不罕见。 @Colin:是的,但它们受到严格限制;最好养成思考“我真的需要一个数组还是可以使用 List 代替?”的习惯 @Colin:仅在需要时进行优化。当您发现自己花了半分钟时间添加一些东西时,例如,扩大了一个数组,或者您在 for 循环的范围之外保留了一些索引,您已经浪费了您老板的一些时间。首先构建,在需要的时间和地点进行优化 - 在大多数应用程序中,这不是用数组替换列表。 好吧,为什么没有人提到int[]
和 new int[]
比 List<Integer>
和 new ArrayList<Integer>
更容易输入? XD
【参考方案1】:
实现java.util.function.IntUnaryOperator
:
class ImmutableArray implements IntUnaryOperator
private final int[] array;
ImmutableArray(int[] array)
this.array = Arrays.copyOf(array, array.length);
@Override
public int applyAsInt(int index)
return array[index];
访问数组:array[i]
变为 immutableArray.applyAsInt(i)
。
PrimitiveArray
耗时约220ms;原始数组没有显着差异。 ArrayList
上的相同操作耗时 480 毫秒,加载过程耗时 21 秒,第一次尝试耗尽了我的堆空间,我不得不在 JVM 上增加这个设置。 PrimitiveArray 的加载耗时 2 秒。
迭代
如果你想迭代,实现Iterable
并提供
public java.util.PrimitiveIterator.OfInt iterator() return Arrays.stream(array).iterator();
这提供了对int nextInt
方法的访问。
您还可以从PrimitiveIterator
获得方法forEachRemaining(PrimitiveConsumer)
,它有助于替换现有的增强 for 循环。
使用 PrimitiveIterator.OfInt
手动迭代产生的性能约为 300 毫秒。
【讨论】:
【参考方案2】:Java9 中的of(E... elements) 方法可用于仅使用一行来创建不可变列表:
List<Integer> items = List.of(1,2,3,4,5);
上述方法返回一个包含任意数量元素的不可变列表。将任何整数添加到此列表中都会导致java.lang.UnsupportedOperationException
exception。此方法还接受单个数组作为参数。
String[] array = ... ;
List<String[]> list = List.<String[]>of(array);
【讨论】:
【参考方案3】:另一个答案
static class ImmutableArray<T>
private final T[] array;
private ImmutableArray(T[] a)
array = Arrays.copyOf(a, a.length);
public static <T> ImmutableArray<T> from(T[] a)
return new ImmutableArray<T>(a);
public T get(int index)
return array[index];
final ImmutableArray<String> sample = ImmutableArray.from(new String[]"a", "b", "c");
【讨论】:
在构造函数中复制该数组有什么用,因为我们没有提供任何 setter 方法? 输入数组的内容以后还可以修改。我们想保留它的原始状态,所以我们复制它。【参考方案4】:从 Java 9 开始,您可以使用 List.of(...)
、JavaDoc。
这个方法返回一个不可变的List
,效率很高。
【讨论】:
【参考方案5】:从 Guava 22 开始,您可以使用包 com.google.common.primitives
中的三个新类,与 ImmutableList
相比,它们的内存占用更少。
他们也有一个建设者。示例:
int size = 2;
ImmutableLongArray longArray = ImmutableLongArray.builder(size)
.add(1L)
.add(2L)
.build();
或者,如果在编译时知道大小:
ImmutableLongArray longArray = ImmutableLongArray.of(1L, 2L);
这是获取 Java 基元数组的不可变视图的另一种方法。
【讨论】:
【参考方案6】:我的建议是不要使用数组或unmodifiableList
,而是使用Guava 的ImmutableList,它就是为此目的而存在的。
ImmutableList<Integer> values = ImmutableList.of(0, 1, 2, 3);
【讨论】:
+1 Guava 的 ImmutableList 比 Collections.unmodifiableList 还要好,因为它是一个单独的类型。 ImmutableList 通常更好(这取决于用例),因为它是不可变的。 Collections.unmodifiableList 不是不可变的。相反,它是一种观点,即接收者不能改变,但原始来源 CAN 改变。 @CharlieCollins,如果没有办法访问原始源,Collections.unmodifiableList
是否足以使 List 不可变?
@savanibharat 是的。【参考方案7】:
虽然Collections.unmodifiableList()
确实有效,但有时您可能有一个大型库,其中已经定义了返回数组的方法(例如String[]
)。
为了防止破坏它们,您实际上可以定义将存储值的辅助数组:
public class Test
private final String[] original;
private final String[] auxiliary;
/** constructor */
public Test(String[] _values)
original = new String[_values.length];
// Pre-allocated array.
auxiliary = new String[_values.length];
System.arraycopy(_values, 0, original, 0, _values.length);
/** Get array values. */
public String[] getValues()
// No need to call clone() - we pre-allocated auxiliary.
System.arraycopy(original, 0, auxiliary, 0, original.length);
return auxiliary;
测试:
Test test = new Test(new String[]"a", "b", "C");
System.out.println(Arrays.asList(test.getValues()));
String[] values = test.getValues();
values[0] = "foobar";
// At this point, "foobar" exist in "auxiliary" but since we are
// copying "original" to "auxiliary" for each call, the next line
// will print the original values "a", "b", "c".
System.out.println(Arrays.asList(test.getValues()));
不完美,但至少你有“伪不可变数组”(从类的角度来看),这不会破坏相关代码。
【讨论】:
【参考方案8】:如果您想同时避免可变性和装箱,就没有办法开箱即用。但是您可以创建一个类,其中包含原始数组并通过方法提供对元素的只读访问。
【讨论】:
【参考方案9】:在某些情况下,使用 Google Guava 库中的这个静态方法会更轻:List<Integer> Ints.asList(int... backingArray)
例子:
List<Integer> x1 = Ints.asList(0, 1, 2, 3)
List<Integer> x1 = Ints.asList(new int[] 0, 1, 2, 3)
【讨论】:
【参考方案10】:有一种方法可以在 Java 中创建不可变数组:
final String[] IMMUTABLE = new String[0];
(显然)包含 0 个元素的数组不能被变异。
如果您使用List.toArray
方法将List
转换为数组,这实际上会派上用场。因为即使是空数组也会占用一些内存,您可以通过创建一个常量空数组并始终将其传递给toArray
方法来节省内存分配。如果您传递的数组没有足够的空间,该方法将分配一个新数组,但如果有(列表为空),它将返回您传递的数组,允许您在调用@987654325 时重用该数组@在一个空的List
上。
final static String[] EMPTY_STRING_ARRAY = new String[0];
List<String> emptyList = new ArrayList<String>();
return emptyList.toArray(EMPTY_STRING_ARRAY); // returns EMPTY_STRING_ARRAY
【讨论】:
但这并不能帮助 OP 实现一个包含数据的不可变数组。 @Sridhar-Sarnobat 这就是答案的重点。 Java 中没有办法创建一个包含数据的不可变数组。 我更喜欢这种更简单的语法:private static final String[] EMPTY_STRING_ARRAY = ; (显然我还没有弄清楚如何做“迷你Markdown”。预览按钮会很好。)【参考方案11】:嗯.. 数组作为常量(如果它们是)作为变体参数传递很有用。
【讨论】:
什么是“变体参数”?没听说过。 使用数组作为常量是正是许多人失败的地方。引用是常量,但数组的内容是可变的。一位呼叫者/客户可能会更改您的“常量”的内容。这可能不是您想要允许的。使用 ImmutableList。 @CharlieCollins 甚至可以通过反射来破解。 Java 在可变性方面是不安全的语言......这不会改变。 @SargeBorsch 确实如此,但是任何基于反射的黑客行为都应该知道,他们通过修改 API 发布者可能不希望他们访问的东西来玩火。然而,如果 API 返回int[]
,调用者可能会假设他们可以使用该数组做他们想做的事情,而不会影响 API 的内部结构。【参考方案12】:
正如其他人所指出的,Java 中不能有不可变数组。
如果您绝对需要一个返回不影响原始数组的数组的方法,那么您需要每次都克隆该数组:
public int[] getFooArray()
return fooArray == null ? null : fooArray.clone();
显然这是相当昂贵的(因为您每次调用 getter 时都会创建一个完整的副本),但是如果您不能更改接口(例如使用 List
)并且不能冒险客户改变你的内部结构,那么它可能是必要的。
这种技术称为制作防御性副本。
【讨论】:
他在哪里提到他需要一个吸气剂? @Erik:他没有,但这是不可变数据结构的一个非常常见的用例(我修改了答案以引用一般方法,因为该解决方案适用于任何地方,即使它更常见在吸气剂中)。 这里用clone()
还是Arrays.copy()
更好?
据我记忆,根据 Joshua Bloch 的《Effective Java》,clone() 绝对是首选方式。
第 13 条的最后一句,“明智地覆盖克隆”:“通常,复制功能最好由构造函数或工厂提供。此规则的一个明显例外是数组,最好使用克隆方法。”【参考方案13】:
如果您需要(出于性能原因或节省内存)本机“int”而不是“java.lang.Integer”,那么您可能需要编写自己的包装类。网上有各种 IntArray 实现,但没有一个(我发现)是不可变的:Koders IntArray、Lucene IntArray。可能还有其他人。
【讨论】:
【参考方案14】:不,这是不可能的。但是,可以这样做:
List<Integer> temp = new ArrayList<Integer>();
temp.add(Integer.valueOf(0));
temp.add(Integer.valueOf(2));
temp.add(Integer.valueOf(3));
temp.add(Integer.valueOf(4));
List<Integer> immutable = Collections.unmodifiableList(temp);
这需要使用包装器,并且是一个列表,而不是一个数组,但是你会得到最接近的。
【讨论】:
不需要写所有valueOf()
s,自动装箱会处理这些。 Arrays.asList(0, 2, 3, 4)
也会更简洁。
@Joachim:使用valueOf()
的目的是利用内部整数对象缓存来减少内存消耗/回收。
@Esko:阅读自动装箱规范。它做的事情完全一样,所以这里没有区别。
@John 你永远不会让 NPE 将 int
转换为 Integer
;只需要小心反过来。
@John:你说得对,这可能很危险。但与其完全避免,不如了解危险并避免它。【参考方案15】:
不适用于原始数组。您需要使用 List 或其他一些数据结构:
List<Integer> items = Collections.unmodifiableList(Arrays.asList(0,1,2,3));
【讨论】:
不知何故我从来不知道 Arrays.asList(T ...)。我想我现在可以摆脱我的 ListUtils.list(T ...) 了。 顺便说一下,Arrays.asList 给出了一个不可修改的列表 @tony ArrayList 不是 java.util 的 @mauhizArrays.asList
是不是不可修改的。 docs.oracle.com/javase/7/docs/api/java/util/… "返回一个由指定数组支持的固定大小的列表。(将返回的列表“写入”到数组中。)"
@JasonS 好吧,Arrays.asList()
确实 似乎 不可修改,因为您不能在返回的 java.util.Arrays.ArrayList
(不要混淆与java.util.ArrayList
) - 这些操作只是没有实现。也许@mauhiz 试图这么说?但是当然你可以用List<> aslist = Arrays.asList(...); aslist.set(index, element)
修改现有元素,所以java.util.Arrays.ArrayList
肯定是不是不可修改的,Q.E.D.添加注释只是为了强调asList
的结果与普通ArrayList
的结果之间的差异以上是关于Java中的不可变数组的主要内容,如果未能解决你的问题,请参考以下文章