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)

我使用具有 100_000_000 个元素的模运算对循环检索的原语进行了基准测试。以上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.UnsupportedOperationExceptionexception。此方法还接受单个数组作为参数。

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 相比,它们的内存占用更少。

ImmutableIntArray ImmutableLongArray ImmutableDoubleArray

他们也有一个建设者。示例:

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&lt;Integer&gt; Ints.asList(int... backingArray)

例子:

List&lt;Integer&gt; x1 = Ints.asList(0, 1, 2, 3) List&lt;Integer&gt; 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 的 @mauhiz Arrays.asList不是不可修改的。 docs.oracle.com/javase/7/docs/api/java/util/… "返回一个由指定数组支持的固定大小的列表。(将返回的列表“写入”到数组中。)" @JasonS 好吧,Arrays.asList() 确实 似乎 不可修改,因为您不能在返回的 java.util.Arrays.ArrayList(不要混淆java.util.ArrayList) - 这些操作只是没有实现。也许@mauhiz 试图这么说?但是当然你可以用List&lt;&gt; aslist = Arrays.asList(...); aslist.set(index, element) 修改现有元素,所以java.util.Arrays.ArrayList 肯定是不是不可修改的,Q.E.D.添加注释只是为了强调asList 的结果与普通ArrayList 的结果之间的差异

以上是关于Java中的不可变数组的主要内容,如果未能解决你的问题,请参考以下文章

java之可变数组

具有深度嵌套层次结构的不可变 NSDictionary:更改键的值?

图说:为什么Java中的字符串被定义为不可变的

Java中的不可变类

Java中的可变长度(动态)数组

使用Scala中的不可变值查找路径是不是存在于图形中