摘要:
本文主要阐述了 Java 中与数组的知识,主要从 数组与容器、数组的创建与初始化、数组与泛型、数组与可变参数列表、Arrays 工具类的实用功能 五个方面来对数组进行详细说明和总结。
一. 要点概览
- 数组与容器
- 数组的创建与初始化
- 数组与泛型
- 数组与可变参数列表
- Arrays 工具类的实用功能
- 总结
二. 数组与容器
在 Java 中,数组和容器都可以持有对象,那么,数组与容器的区别是什么呢?当我们需要持有对象时,我们在什么情形下应优先选用数组,而又在什么情况下优先选用容器呢?
在 Java 的初始版本中,尺寸固定的数组绝对是必需的,不仅因为 Java 设计者选择在 Java 中要包含基本类型和一些基于性能方面的考量,而且还因为初始版本对容器的支持非常少。因此,在 Java 早期版本中,选择包含数组总是合理的,具体表现在以下三方面:
效率
在 Java 中,数组是一种效率最高的存储和随机访问对象引用序列的方式。数组就是一个简单的线性序列,这使得元素访问非常快速。但是,为这种速度所付出的代价是数组对象的大小被固定,并且在其生命周期中不可改变。由于 ArrayList 可以实现空间的自动分配并且更为灵活,所以,我们通常应首选 ArrayList 而不是数组,但这种弹性需要开销。因此,ArrayList 的效率比数组低的多。
类型
在 JDK 1.5 之前,Java 并未引进泛型。所以,泛型之前的容器类在处理对象时,都将它们视作没有任何具体类型,即将这些对象都当作 Object 来处理。数组之所以优于泛型之前的容器,就是因为你可以创建一个数组去持有某种具体的类型。这意味着你可以通过编译期的类型检查来防止插入错误类型和抽取不当类型。当然,无论在编译时还是运行时,Java都会阻止你向对象发送不恰当的消息。所以,并不是说哪种方法更不安全,只是若编译时就能够指出错误,会显得更为优雅。
保存基本类型的能力
数组可以持有基本类型,而泛型之前的容器则不能。
在 JDK 1.5 之后,Java 引进了泛型和自动包装机制(泛型可以保证产生类型安全的容器,自动包装机制使容器可以持有基本类型),这使得现在的容器在除了性能之外的各个方面都使得数组相形见绌。另外,泛型对数组是极大的威胁,而且通常状况下,二者不能很好地结合(不能实例化具有参数化类型的数组)。
因此,当我们使用最近的Java版本编程时,应优选容器而非数组。只有在已证明性能成为问题并且切换到数组对性能提高有所帮助时,我们才应该将程序重构为使用数组。
三. 数组的创建与初始化
1、数组基础
数组标识符只是一个引用,指向在堆中创建的一个真实对象,这个对象用以保存指向其他对象的引用或基本类型的值;
对象数组保存的是引用,基本类型数组直接保存基本类型的值;
“[ ]” 语法是访问数组对象的唯一方式;
2、创建与初始化
- 作为数组初始化的一部分隐式创建
String[] strs = { ... }; // 创建方式 1 --- 花括号内对象的个数就是数组大小
System.out.println(strs.length); // 输出 0
- 使用 new 表达式显式创建
String[] strs = new String[5]; //创建方式 2
String[] ss = new String[]{ ... }; //创建方式 3
3、多维数组
多维数组的本质: 数组的元素仍是数组
// 创建方式 1
int[][] a = {
{ 1, 2, 3, },
{ 4, 5, 6, },
};
// 创建方式 2
int[][][] a = new int[2][2][4];
//粗糙数组:每个向量具有任意长度
Random rand = new Random(47);
// 3-D array with varied-length vectors:
int[][][] a = new int[rand.nextInt(7)][][];
for(int i = 0; i < a.length; i++) {
a[i] = new int[rand.nextInt(5)][];
for(int j = 0; j < a[i].length; j++)
a[i][j] = new int[rand.nextInt(5)];
}
四、数组与泛型
- 数组与泛型不能很好的结合,也就是说,不能实例化具有参数化类型的数组 ;
T[] first = new T[3]; // ERROR
A<String>[] arrays = new A<String>[4]; // ERROR: Cannot create a generic array of A<String>
- 可以创建泛型数组引用 ;
A<String>[] arrays; // OK
- 数组是协变的 ;
Object[] objs = new String[3]; // OK
总之,泛型容器总是比泛型数组更好的选择。
五、数组与可变参数类型
1、可变参数类型概念
Java SE5 添加了可变参数类型 ( Variable Argument Type ),形式为 “Type… args”,只可用作方法的参数。可变参数列表适用于参数个数不确定但类型确定的情形 ( java 把可变参数当做数组处理 )。特别需要注意的是,可变参数列表必须位于最后一项 (即最多只支持一个可变参数)。当可变参数列表个数多余一个时,必将有一个不是最后一项,所以只支持一个可变参数。因为可变参数列表的参数个数不确定,所以当其后边还有相同类型参数时,Java 无法区分传入的参数属于前一个可变参数还是后边的参数,所以只能让可变参数位于最后一项。
// 代码示例
public class TestVarArgus {
public static void dealArray(int... intArray){
for (int i : intArray)
System.out.print(i +" ");
System.out.println();
}
public static void main(String args[]){
dealArray();
dealArray(1);
dealArray(1, 2, 3);
}
}/* Output:
1
1 2 3
*///:~
可变参数列表具有以下特点:
只能出现在方法参数列表的最后;
… 位于变量类型和变量名之间,前后有无空格都可以;
调用可变参数所在的方法时,编译器会为该可变参数隐式创建一个数组,从而我们可以在方法体中以数组的形式访问可变参数列表 (编译器把可变参数当做数组进行处理)。
2、可变参数类型与数组的兼容性
- 编译器认为数组类型和可变参数类型是相同的,即二者不能重载;
public class TestVarArgus {
public static void dealArray(int... intArray){
for (int i : intArray)
System.out.print(i +" ");
System.out.println();
}
//ERROR : Duplicate method dealArray(int[]) in type TestVarArgus
public static void dealArray(int[] intArray){
for (int i : intArray)
System.out.print(i +" ");
System.out.println();
}
public static void main(String args[]){
dealArray();
dealArray(1);
dealArray(1, 2, 3);
}
}
- 可变参数是兼容数组类型参数的,但是数组类型参数却无法兼容可变参数;
// 代码示例 1 : 给参数为可变参数类型的方法传递数组
public class TestVarArgus {
public static void dealArray(int... intArray){
for (int i : intArray)
System.out.print(i +" ");
System.out.println();
}
public static void main(String args[]){
int[] intArray = {1, 2, 3};
dealArray(intArray); // OK
}
}
// 代码示例 2 : 给参数为数组类型的方法传递可变参数
public class TestVarArgus {
public static void dealArray(int[] intArray){
for (int i : intArray)
System.out.print(i +" ");
System.out.println();
}
public static void main(String args[]){
dealArray(1, 2, 3); // ERROR
}
}
其实,对于示例代码 2 而言,只是需要一个定义为 dealArray(int, int, int)的方法或者一个定义为 dealArray(int… )的方法。所以,自然就无法去匹配具有数组类型的参数 dealArray( int[] ) 方法了。
参数匹配原则:能匹配定长的方法,那么 优先 匹配该方法 ;
public class TestVarArgus { //含有不定参数的那个重载方法是最后被选中的 public static void dealArray(int... intArray){ System.out.println("Bad"); } public static void dealArray(int count, int count2){ System.out.println("Bingo"); } public static void main(String args[]){ dealArray(1, 2); } } /* Output: Bingo *///:~
就连我们耳熟能详的 main 函数的参数也能改写成可变参数类型的形式:public static void main(String… args) .
六、Arrays 工具类的实用功能
1、复制数组
Function | Introduction | Note |
---|---|---|
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length) | 从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束 | 若复制对象数组,那么只是复制了对象的引用,而不是对象本身的拷贝(浅复制);该方法不会执行自动包装和自动拆包,所以两个数组必须具有相同的确切类型;须明确自行新建立一个数组对象,作为副本 |
copyOf(T[] original, int newLength) | 复制指定的数组,截取或用 null 填充 | 底层调用的还是 System.arraycopy;返回一个新的数组对象,若新数组的长度超过原数组的长度,则保留数组默认值 |
注意:
对于以上两个方法:
若复制对象数组,那么只是复制了对象的引用,而不是对象本身的拷贝;
这两个方法不会执行自动包装和自动拆包,所以两个数组必须具有相同的确切类型。
2、数组的字符串方式表示
方法: Arrays.toString(Object[] a) / Arrays.deepToString(Object[] a)
作用: 返回指定数组内容的字符串表示形式:前者适用于一维数组,或者适用于多维数组
3、数组的比较
方法: Arrays.equals(Object[] a, Object[] a2) / deepEquals(Object[] a1, Object[] a2) (多维)
作用: 比较两个数组:元素类型相同,元素个数相等,对应位置的元素相同;
注意:
- 通过对每个元素使用 equals() 作比较来判断;
- 对于基本类型,使用的是基本类型的包装器类的 equals() 方法(对于 int 类型使用 Integer.equals() 作比较);
- 使用 equals() 方法比较原则:是不是同一个对象,是不是同一个类型,是不是具有相同的内容。
int[] a1 = new int[10];
int[] a2 = new int[10];
Arrays.fill(a1, 47);
Arrays.fill(a2, 47);
print(Arrays.equals(a1, a2)); //true
4、数组的排序
使用内置的排序方法,就可以对任意的基本类型数组排序;也可以对任意的对象数组进行排序,只要该对象实现了 Comparable 接口或具有相关联的 Comparator (独立的实现该接口的类)。
方法: Arrays.sort(Object[] a) / Arrays.sort(Object[] a, int fromIndex, int toIndex)
Arrays.sort(T[] a, Comparator<? super T> c) / Arrays.sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c)
作用: 对数组内元素进行升序排序 (默认)
String[] sa = Generated.array(new String[20], new RandomGenerator.String(5));
// 利用 String 内置的比较器(已实现 Comparable 接口):字典序(大写字母开头的词都放在前面输出,之后才是小写字母开头的词)
Arrays.sort(sa); // ... ,WHkjU, YNzbr, bkIna, cQrGs, ...
.
// 利用 Comparator 比较 : Collections.reverseOrder() : 现有顺序的逆序
Arrays.sort(sa, Collections.reverseOrder());
// 利用 Comparator 比较 : String.CASE_INSENSITIVE_ORDER : 忽略大小写将单词一起进行排序
Arrays.sort(sa, String.CASE_INSENSITIVE_ORDER);
Java 标准类库中的排序算法针对正排序的特殊类型进行了优化 ———— 针对基本类型设计的“快排” 和 针对对象设计的“稳定归并排序”。所以,无需担心排序的性能,除非你可以证明排序部分的确是程序效率的瓶颈。
5、在已排序的数组中查找
若数组已经 排好序,就可以使用该方法执行快速查找;若对未排序的数组使用该方法,将产生不可预料的结果。
方法: binarySearch(Object[] a, Object key) / binarySearch(T[] a, T key, Comparator<? super T> c)
作用: 使用 二分搜索法 来搜索指定数组,以获得指定对象。在进行此调用之前,必须根据元素的自然顺序对数组进行升序排序(通过 sort(Object[]) 方法); 使用二分搜索法来搜索指定数组,以获得指定对象。在进行此调用之前,必须根据指定的比较器(通过 sort(T[], Comparator) 方法)对数组进行多态升序排序。
注意:
对 已经有序 的数组进行查找;
若找到了目标,方法返回的值不小于0 ; 否则,它产生的负返回值表示在此排序下应插入的位置;
“sort(Object[])” 与 “binarySearch(Object[] a, Object key)” 对应,“sort(T[], Comparator)” 与 binarySearch(T[] a, T key, Comparator<? super T> c) 对应。
若数组包含重复元素,则该方法无法保证找到的是哪一个元素;若需要对无重复元素的数组进行排序,可使用 TreeSet(保持排序顺序) 或 LinkedHashSet(保持插入顺序)进行排序。除非它们成为程序的瓶颈,否则不需要自己维护数组。
6、填充数组
方法: fill(Object[] a, Object val)
作用: 只能使用同一个值填充各个位置,而针对对象而言,就是复制同一个对象的引用进行填充
7、数组与容器的转化
方法: asList(T… a)
作用: 返回一个 受指定数组支持的固定大小的列表
注意:
所得到的 List 是固定大小的,因为其底层表示即为该数组,因此不能调整大小。因此,调用 add/remove 方法会抛出 java.lang.UnsupportedOperationException (可选操作)。
因此,Arrays.asList() 的真正意义在于:将其结果作为构造器参数传递给任何 Collection (或者使用 addAll 方法、Collections.addAll 静态方法),这样可以生成一个动态的容器。
七、总结
原则:
优先选择使用容器而不是数组;
数组与可变参数用作方法参数时,优先使用可变参数会使得程序更解耦;
Arrays 工具类的实用功能会使我们在编程上事半功倍。
引用:
《Java 编程思想(第四版)》
Java 可变参数
Java方法的可变参数个数