数组#
数组和容器#
在 Java 中,数组是一种 效率最高 的存储和随机访问对象引用序列的方式。 数组就是一个简单的线性序列,这使得元素访问非常迅速。 但是,为这种速度所付出的代价是数组对象的大小被固定,并且在其生命周期中不可改变。
在泛型之前,其他的容器类在处理对象时,都将它们视作没有任何具体类型,也就是说,它们将这些对象都当作
Object
来处理。
有了泛型之后,容器就可以指定并检查它们所持有对象的类型,并且有了自动包装机制,容器还能够持有基本类型。
容器和泛型对数组产生了极大的冲击,并且现在的容器在除了性能之外的各个方面都碾压数组。 通常,即使当你可以让泛型与数组以某种方式一起工作时,在编译器你最终也会得到 “不受检查” 的警告信息。
针对大多数场景,用的更多的是容器,而不是数组,除非你对性能有更高的要求。
数组初始化#
创建基本类型的 一维数组,很简单。如下所示:
1//: initialization/ArraysOfPrimitives.java
2import static net.mindview.util.Print.*;
3
4public class ArraysOfPrimitives {
5public static void main(String[] args) {
6 int[] a1 = { 1, 2, 3, 4, 5 };
7 int[] a2;
8 a2 = a1;
9 for(int i = 0; i < a2.length; i++)
10 a2[i] = a2[i] + 1;
11 for(int i = 0; i < a1.length; i++)
12 print("a1[" + i + "] = " + a1[i]);
13}
14} /* Output:
15a1[0] = 2
16a1[1] = 3
17a1[2] = 4
18a1[3] = 5
19a1[4] = 6
20*///:~
如果你创建的是 非基本类型的数组,那么你就创建了一个引用数组(存放引用的数组)。
如果数组中存储的是对象的引用,那么我们就称之为对象数组。 对象数组和基本类型数组在使用上几乎是相同的,唯一的区别就是对象数组保存的是对象的引用。
无论使用哪种类型的数组,数组标识符其实只是一个引用,表示堆中的一个真实对象的别名(这部分有争议,需要修改)。这个真实对象中保存了指向其他对象的引用。
C/C++ 无法返回整个数组,只能返回指向数组的指针,但这使数组的生命周期难以控制,容易内存泄漏。 而 Java 可以直接返回一个数组对象(也是一个引用),使用完成后垃圾回收器会自动清理。
1//: initialization/ArrayClassObj.java
2// Creating an array of nonprimitive objects.
3import java.util.*;
4import static net.mindview.util.Print.*;
5
6public class ArrayClassObj {
7 public static void main(String[] args) {
8 Random rand = new Random(47);
9 Integer[] a = new Integer[rand.nextInt(20)]; // a 是一个引用
10 print("length of a = " + a.length);
11 for(int i = 0; i < a.length; i++)
12 a[i] = rand.nextInt(500); // 自动包装,指的是基本类型自动转为包装类
13 print(Arrays.toString(a));
14 }
15} /* Output: (Sample)
16length of a = 18
17[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]
18*///:~
创建基本类型的 多维数组,可以使用花括号将每个向量分隔开(每个向量的维度可以不相等)。
//: arrays/AutoboxingArrays.java
import java.util.*;
public class AutoboxingArrays {
public static void main(String[] args) {
Integer[][] a = { // Autoboxing:
{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 },
{ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30 },
{ 51, 52, 53, 54, 55, 56, 57, 58, 59, 60 },
{ 71, 72, 73, 74, 75, 76, 77, 78, 79, 80 },
};
System.out.println(Arrays.deepToString(a));
}
} /* Output:
[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
[21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
[51, 52, 53, 54, 55, 56, 57, 58, 59, 60],
[71, 72, 73, 74, 75, 76, 77, 78, 79, 80]]
*///:~
可变参数列表#
可变参数列表其实是一个数组,因此,可以用 foreach
来进行遍历,见如下代码。
1//: initialization/NewVarArgs.java
2// Using array syntax to create variable argument lists.
3
4public class NewVarArgs {
5 // static void printArray(Object[] args) { // 老语法,main 函数就在使用
6 static void printArray(Object... args) { // 新语法
7 for(Object obj : args)
8 System.out.print(obj + " ");
9 System.out.println();
10 }
11 public static void main(String[] args) {
12 // Can take individual elements:
13 printArray(new Integer(47), new Float(3.14), new Double(11.11));
14 printArray(47, 3.14F, 11.11);
15 printArray("one", "two", "three");
16 printArray(new A(), new A(), new A());
17 // Or an array:
18 printArray((Object[])new Integer[]{ 1, 2, 3, 4 });
19 printArray(); // Empty list is OK
20 }
21} /* Output: (75% match)
2247 3.14 11.11
2347 3.14 11.11
24one two three
25A@1bab50a A@c3c749 A@150bd4d
261 2 3 4
27*///:~
除此之外,可变参数列表支持自动包装机制(自动包装指的是基本类型自动转为包装类)。
但是,可变参数列表使重载变得更复杂了。
如果给定 f()
,编译器不知道该调用 f(Character... args)
还是 f(Integer... args)
。
这个问题可以通过添加非可变参数来解决 f(float i, Character... args)
。
数组的判空方法#
一维数组: array.length == 0
二维数组: array.length == 0 || array[0].length == 0
数组复制和排序#
复制数组: System.arraycopy()
。
数组的比较: Arrays.equals()
。
数组元素的比较,有两种方式:
实现
java.lang.Comparable
接口。自建
Comparator
接口,并提供compare()
和equals()
方法声明。
数组排序,分情况讨论:
基本类型
Arrays.sort()
可以排序。自定义类型需要实现
java.lang.Comparable
接口。
在已排序的数组中查找: Arrays.binarySearch()
。
数组与泛型#
通常,数组与泛型不能很好地结合,取而代之的是容器和泛型的结合。 如果你非要结合数组和泛型,也不是不可以,但不推荐使用,故本小节仅作为了解知识即可。
不能实例化具有参数化类型的数组。 因为编译器会进行 类型擦除,而数组又必须知道它所持有的确切类型,以强制保证类型安全。 因此下述代码并不合法。
Peel<Banana>[] peels = new Peel<Banana>[10]; // Illegal
但是,你可以参数化数组本身的类型:
//: arrays/ParameterizedArrayType.java
class ClassParameter<T> {
public T[] f(T[] arg) { return arg; }
}
class MethodParameter {
public static <T> T[] f(T[] arg) { return arg; }
}
public class ParameterizedArrayType {
public static void main(String[] args) {
Integer[] ints = { 1, 2, 3, 4, 5 };
Double[] doubles = { 1.1, 2.2, 3.3, 4.4, 5.5 };
Integer[] ints2 = new ClassParameter<Integer>().f(ints); // 参数化类须人为指定参数类型
Double[] doubles2 = new ClassParameter<Double>().f(doubles);
ints2 = MethodParameter.f(ints); // 参数化方法会自动识别实参类型
doubles2 = MethodParameter.f(doubles);
}
} ///:~
阅读上述代码可知,使用参数化方法比使用参数化类更加方便。
由于擦除的存在,我们将不能创建泛型数组。因为移除类型信息后,不能创建类型未知的数组。
但是,你可以创建 Object
数组,然后将其转型。
//: arrays/ArrayOfGenericType.java
// Arrays of generic types won't compile.
public class ArrayOfGenericType<T> {
T[] array; // OK
@SuppressWarnings("unchecked")
public ArrayOfGenericType(int size) {
//! array = new T[size]; // Illegal, unknown type
array = (T[])new Object[size]; // "unchecked" Warning
}
// Illegal:
//! public <U> U[] makeArray() { return new U[10]; }
} ///:~
一般而言,泛型在类或方法的边界处很有效,而在类或方法的内部,擦除通常会使泛型变得不适用。