Java 中的不可变数组

对于 Java 中的基元数组,是否存在不可变的替代方案?创建一个基本数组 final实际上并不能阻止执行以下操作

final int[] array = new int[] {0, 1, 2, 3};
array[0] = 42;

我希望数组的元素是不可更改的。

123695 次浏览

不能使用基元数组。您需要使用 List 或其他数据结构:

List<Integer> items = Collections.unmodifiableList(Arrays.asList(0,1,2,3));

不,这是不可能的。然而,人们可以这样做:

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

这需要使用包装器,并且是一个 List,而不是一个数组,但是这是最接近的。

正如其他人所指出的,在 Java 中不能使用不可变的数组。

如果您绝对需要一个返回不影响原始数组的数组的方法,那么每次都需要克隆该数组:

public int[] getFooArray() {
return fooArray == null ? null : fooArray.clone();
}

显然,这是相当昂贵的(因为您将在每次调用 getter 时创建一个完整的副本) ,但是如果您不能更改接口(例如使用 List) ,并且不能冒险让客户机更改您的内部结构,那么这可能是必要的。

这种技术被称为防御性拷贝。

我的建议是不要使用数组或者 unmodifiableList,而是使用 番石榴不可变列表,它就是为此而存在的。

ImmutableList<Integer> values = ImmutableList.of(0, 1, 2, 3);

如果(出于性能原因或为了节省内存)需要本机‘ int’而不是‘ java.lang’。那么您可能需要编写自己的包装类。网络上有各种 IntArray 实现,但没有一个(我发现的)是不可变的: Koders IntArrayLucene IntArray。可能还有其他人。

数组作为常量(如果是的话)作为变量参数传递是很有用的。

有一种在 Java 中创建不可变数组的方法:

final String[] IMMUTABLE = new String[0];

具有0个元素的阵列(显然)不能被突变。

如果使用 List.toArray方法将 List转换为数组,这实际上很方便。因为即使是一个空数组也会占用一些内存,所以您可以通过创建一个常量空数组来节省内存分配,并且总是将其传递给 toArray方法。如果您传递的数组没有足够的空间,那么这个方法将分配一个新的数组,但是如果有空间(列表为空) ,它将返回您传递的数组,允许您在空的 List上调用 toArray时重用该数组。

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

又一个答案

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"});
}

在某些情况下,使用来自 GoogleGuava 库: 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})

如果您希望同时避免可变性和装箱,那么就没有出路了。但是您可以创建一个类,该类在内部保存基元数组,并通过方法提供对元素的只读访问。

虽然 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()));

虽然不完美,但至少您有“伪不可变数组”(从类的角度来看) ,而且这不会破坏相关的代码。

由于 Guava22,从包 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 原语数组不可变视图的另一种方法。

从 Java9开始,您可以使用 List.of(...)JavaDoc

此方法返回一个不可变的 List,并且非常有效。

Java9中的 (元素)方法可以用一行代码创建不可变的列表:

List<Integer> items = List.of(1,2,3,4,5);

上面的方法返回一个不可变的列表,其中包含任意数量的元素。向列表中添加任何整数都会导致 java.lang.UnsupportedOperationException异常。此方法还接受单个数组作为参数。

String[] array = ... ;
List<String[]> list = List.<String[]>of(array);

实施 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手动迭代可以获得约300ms 的性能。