虽然之前对数组大致都了解,因为这不是一个困难的知识点,只是一个很普通的基础知识,但是真正运用起来总感觉缺了些什么,那可能是之前对数组的印象一直都停留在能用模糊的概念上,所以今天我们就好好的总结一下数组的相关知识,我这里大致分为三个模块。
什么是数组?
数组只是相同类型的,用一个标识符名称封装在一起的一个基本类型序列或对象序列。
数组类型:
数组的特性
数组与其他种类的容器之间的区别
要定义一个数组,只需要在类型名后加上一对空方括号[]
两种格式的含义是一样的,第二种格式符合C和C++程序员的习惯,不过,前一种格式或许更合理,毕竟它表明类型是一个XX类型的数组。在Java开发中,我更倾向于第一种定义格式。
为了简单简洁,这里的数组都为基本类型的一维数组,多维或是对象数组其实方式都差不多,我就仅在补充上稍微带过。
知识前提:
数组的初始化中,有两个东西需要我们提前了解一下。
[]
(dimension expressions,维度表达式) - 用于表示这是几维数组,每维中大小时多少{...,...,...}
(array initializer,数组初始化器)- 用于帮助数组进行批量初始化数组初始化的三种方式:
int[] array1 = {1,2,3,4}; //数组初始化器定义了大小和内容
int[] array2 = new int [] {1,2,3,4}; //数组初始化器定义了大小和内容
int[] array3 = new int [4]; //维度表达式定义大小
System.out.println(Arrays.toString(array1));
System.out.println(Arrays.toString(array2));
System.out.println(Arrays.toString(array3));
结果
[1, 2, 3, 4] //int[] array1 = {1,2,3,4};
[1, 2, 3, 4] //int[] array2 = new int [] {1,2,3,4};
[0, 0, 0, 0] //int[] array4 = new int [4];
我们可以看出第一种方式和第二种方式的结果是一样的,这种使用数组初始化器初始化的方式,我们称为静态初始化,都是通过给数组批量初始化固定个数的内容,而且第一二种初始化方式的本质其实是一样,虽然源代码略有不同,但是反编译之后的代码其实都是第一种定义方式
//反编译后得到的代码
int[] array1 = { 1, 2, 3, 4 };
int[] array2 = { 1, 2, 3, 4 };
第三种方式属于用维度表达式来定义这一维度的大小,仅仅定义大小而没有内容。我们称为动态初始化,所以我们可以看到输出的结果是一个一维,length为4,默认值为0的数组。
要注意的地方:
(一)维度表达式和数组初始化器初始化大小时必须二选一
数组的初始化有几种方式,但是每种方式都必须为数组定义好大小。可以是方括号中的数字[10]
(dimension expressions,维度表达式),或则大括号的内容的个数{...,...,...}
(array initializer,数组初始化器),初始化大小时,只能二选一,不然是会报编译错误的
int[] array1 = new int [4] {1,2,3,4}; //error
//Cannot define dimension expressions when an array initializer is provided
int[] array2 = new int [4]; //right
int[] array3 = new int [] {1,2,3,4} //right
当数组初始化器被提供的时候,不允许再在维度表达式定义大小,因为有可能存在这种冲突情况
int[] array1 = new int [10] {1,2,3,4} //error
//维度表达式定义的大小和数组初始化器初始化的内容的个数不符合 10 != 4,这样就存在冲突了
(二)大小为0的数组
int[] array1 = {};
int[] array2 = new int [] {};
int[] array3 = new int [0] ;
//它们的输出结果都是 {} ,不然任何数组内容,length为0
Java是允许这样定义数组的,但是目前我还不知道这样定义数组的好处是什么,因为数组可以说是一个大小不变的对象。起始大小为0就代表堆中的这个数组的大小永远都为0。这样子看来这种数组的存在毫无意义可言。如果你尝试引用这种数组,是会得到运行时异常的。
int[] array1 = {};
array1[0] = 1;
数组越界异常Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
,大小为0的数组不存在下标为0具体内容。
(三)数组的length属性
所有数组(无论是对象类型还是基本类型)都有一个固定成员,可以通过它获知数组中包含了多少个元素,但不能对其进行改变,这个成员就是length.
int[] array1 = new int [10];
Sysytem.out.println(array1.length); //output : 10
补充:
多维数组跟一维数组的,对象数组和基本类型数组的定义和初始化过程基本上都类似。我们这里就直接通过多维对象数组来介绍一下。
Integer[][] array = new Integer[][] {
{new Integer(1),new Integer(2),new Integer(3)},
{new Integer(4),new Integer(5),new Integer(6)},
{new Integer(7),new Integer(8),new Integer(9)}
};
for(Integer[] a :array){
System.out.println(Arrays.toString(a));
}
输出结果
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
所以我们可以看到定义和初始化其实是差不多了。不过我们也要注意多维数组一点的是
Integer[][] array1 = new Integer[][4]; //error,编译错误
//Cannot specify an array dimension after an empty dimension
Integer[][] array2 = new Integer[4][]; //right,相当定义了
第二种定义输出的结果是4个null,不是4个{}。所以第二种的定义方式并不是初始化了4个大小为0的数组。而是定义了4个一维数组,再四个第一维的数组中分别定义了一个数组变量,该变量指向null,既还不指向任何地址。
这是一个作用十分有限的功能,相对于基本类型数组来说,只能使用同一个值来填充各个位置。相对于对象数组来说,就是将对象数组的各个部分都引用同一个地址。通过查看fill()
方法的源码就可以了解的很清楚。
测试代码:
public class FillDemo {
public static void main(String[] args) {
int[] i1 = new int[5];
Integer[] i2 = new Integer[10];
Arrays.fill(i1, 1);
Arrays.fill(i2, new Integer(2));
System.out.println(Arrays.toString(i1));
System.out.println(Arrays.toString(i2));
}
}
结果:
[1, 1, 1, 1, 1]
[2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
我们可以看到
Arrays.copyOf()
方法是一个用来复制数组的方法,返回一个新数组对象的引用。那么在说Arrays.copyOf()
方法之前,我们先来说一下Java标准库中提供的System.arraycopy()
方法,用它来复制数组比用for复制快的多。
public static void ArrayCopy(){
int[] i1 = new int[5];
int[] i2 = new int[10];
Arrays.fill(i1, 1);
System.arraycopy(i1, 0, i2, 0, i1.length);
System.out.println(Arrays.toString(i2));
//output:[1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
}
由上面的代码和输出,我们可以初步知道该函数是将i1
数组的五个元素复制到i2
数组中。我们来看一下arraycopy
的具体源码
//源码
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
发现这是一个native方法,既本地方法,不是由Java语言实现的方法,没有返回值,所以我们这么就不管它的具体实现,我们来看arraycopy
的五个参数。第一个参数src
代表源数组,既被复制的数组。第二个参数srcPos
代表要从源数组的那个索引位置开始复制数据。第三个参数dest
代表目标数组,既被赋值的数组。第四个参数destPos
代表被复制的部分从目标数组的什么位置开始拷贝进去。第五个参数length
代表要复制源数组中的多大的长度,既要复制的元素个数。
所以由此可知,我们上是从源数组s1
的0
位置开始复制,复制到s2
目标数组中,从目标数组的0
位置开始拷贝进去,总共复制的元素i1.length
个
特别注意,要注意数组的越界行为,避免出现java.lang.ArrayIndexOutOfBoundsException异常
我们学习了System.arraycopy
的方法之后,我们再来看回Arrays.copyOf()
方法
public static void CopyOf(){
int[] i1 = new int[10];
int[] i2 = new int[5];
Arrays.fill(i1, 1);
i2 = Arrays.copyOf(i1, 7);
System.out.println(Arrays.toString(i2));
//output : [1, 1, 1, 1, 1, 1, 1]
}
由代码和输出,我们可以看出Arrays.copyOf()
方法是截取源数组i1
中7
个元素,并组合成一个新的数组对象,返回新的数组对象的引用给目标数组变量i2
那么虽然Arrays.copyOf()
和System.arraycopy()
使用方式,返回值,参数列表不同,但他们的功能是相似的,那么它们之间是什么关系呢?我们来看一下Arrays.copyOf()
方法的源码
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));
return copy;
}
我们惊奇的发现,实际Arrays.copyOf()
方法的底层实现使用的仍然是System.arraycopy()
.所以我们可以这么说copyOf()
是根据某种具体场景将System.arraycopy()
封装起来的一个实现。
注意:
需要注意的是,Arrays.copy
和System.arraycopy
方法都不是一个深拷贝的方法,只是取原数组元素的引用重新组成一个新的数组对象,这个新的数组对象的地址倒是不同。但其中相应元素的地址任然是同一个。对象的Object.clone()
方法对于数组而言也是一个浅拷贝,这跟Arrays.copyOf()
和System.arraycopy()
方法特性一样。
当然这里也有一种观点是说Arrays.copy
,System.arraycopy
,Object.clone
都是一个深拷贝,这里的深拷贝是说的是整个数组对象的地址变了,也就是存放数组元素的躯壳变了。要从这个角度来说,这的确也是一个深拷贝。所以就看你从什么角度出发了。
Arrays.equals()方法是一个用于比较整个数组的值是否相同的方法,数组相等的条件是元素个数必须相等,对应位置的元素的值也相等(非引用地址对比)。
//基本类型数组
int[] i1 = new int[]{1,2,3,4};
int[] i2 = new int[]{1,2,3,4};
System.out.println(i1.equals(i2)); //false
System.out.println(Arrays.equals(i1, i2)); //true
//对象数组
Integer[] i3 = new Integer[]{new Integer(1),new Integer(2)};
Integer[] i4 = new Integer[]{new Integer(1),new Integer(2)};
System.out.println(i3.equals(i4)); //false
System.out.println(Arrays.equals(i3, i4)); //true
由上,我们可以看到如果直接使用数组的equals
方法去比较,即使元素个数相同,对应位置元素的值也相同,也是false
。但是使用Arrays.equals
去比较却是true
,这是为什么呢?我们分别查看一下源码。
//数组原生equals方法
public boolean equals(Object obj) {
return (this == obj);
}
以上是数组原生的equals
方法,我们都知道数组实际也是一个对象,所以说白了数组的实现也是继承于Object
类,但是数组不像其他的包装类,如Integer
类,重写了equals
方法。所以数组的equals
方法就是没做任何改动,直接拿的Object
类的equals
方法来使用的。所以在数组原生的equals
方法中,我们可以看到,它使用==去比较,比较的是传过来的对象的地址和当前对象的地址。所以自然是false
.
//Arrays工具类int类型的equals重载方法
public static boolean equals(int[] a, int[] a2) {
if (a==a2)
return true;
if (a==null || a2==null)
return false;
int length = a.length;
if (a2.length != length)
return false;
for (int i=0; i<length; i++)
if (a[i] != a2[i])
return false;
return true;
}
以上是Arrays
类的关于Int
型的equals
方法,我们可以看到它的4
个判断流程,首先比较两个数组的地址是否相等,再比较两个数组的引用是否都为Null
,再比较两个数组的长度是否相等,再比较两个数组对应位置的值是否一致。所以可想而知,为true
。当然Arrays
类关于不同的类型,基本类型,包装类型都有自己不同equals
重载方法。因为太多了,就不一一罗列。有兴趣的,可以自己看源码研究一下。
Arrays.sort()方法是工具类提供的数组排序方法。对于基本类型数组排序使用的是(DualPivotQuicksort)“快速排序算法”,对于对象数组使用的是(legacyMergeSort)“稳定归并排序” 和(ComparableTimSort)比较分类方法,所以无需担心排序的性能。这都是做过优化的。
Arrays.sort()的使用方式可以比较多种多样,目前我就简单的说一下,有时间再专门针对这里写了一个总结。
public static void main(String[] args) {
int[] i1 = new int[]{1,3,2,4};
Arrays.sort(i1);
System.out.println(Arrays.toString(i1));
Integer[] i3 = new Integer[]{new Integer(1),new Integer(3),new Integer(2),new Integer(4)};
Arrays.sort(i3);
System.out.println(Arrays.toString(i3));
}
结果
[1, 2, 3, 4]
[1, 2, 3, 4]
我们可以看到默认的排序方式是升序的,是从大到小排序的。因为Arrays.sort()没有专门的降序排序的实现,如果是基本类型数组,就从后遍历赋值给另一个数组,如果是对象数组,可以通过实现Comparator方法来完成,比较麻烦一些。
public class SortDemo {
public static void main(String[] args) {
MyComparator comparator = new MyComparator();
Integer[] i3 = new Integer[]{new Integer(1),new Integer(3),new Integer(2),new Integer(4)};
Arrays.sort(i3,comparator);
System.out.println(Arrays.toString(i3));
//output : [4,3,2,1]
}
}
class MyComparator implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
//如果o1小于o2,我们就返回正值,如果o1大于o2我们就返回负值,
//这样颠倒一下,就可以实现反向排序了
if(o1 < o2) {
return 1;
}else if(o1 > o2) {
return -1;
}else {
return 0;
}
}
}
有待
关于Arrays.asList()
方法的使用,我查了很多的资料,发现很多地方都在说这个方法的坑。也的确存在一些坑。所以我这里就顺带的数一下,有时间再专门整理一个贴。
总的来说这个Arrays.asList()
方法是将一个数组转换成一个集合的实例。
关于基本数据类型不能使用的问题,我暂时还没有想明白,所以暂时这里就不讨论了。
测试代码:
public static void method1 (){
Integer[] i = {1,2,3};
List list = Arrays.asList(i);
System.out.println("修改前Array"+Arrays.toString(i));
System.out.println("修改前list"+list);
i[1]=0;
System.out.println("修改后Array"+Arrays.toString(i));
System.out.println("修改后list"+list);
}
结果:
修改前Array[1, 2, 3]
修改前list[1, 2, 3]
修改后Array[1, 0, 3]
修改后list[1, 0, 3]
从上面看,我们就能得出结果,的确Arrays.asList(i)
得到的集合会跟数组i
关联起来,你更新,我就更新。这是为什么呢?我们来查看一下源码asList
的源码
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
asList()
方返回了一个ArrayList<>()
的实例对象,这好像没有关系,我们点击这个ArrayList
。却发现这里面别有洞天,这不是一个不同的ArrayList,既不是我们平时所常用java.util
包下的ArrayList
.而是Arrays类中的静态内部类ArrayList…这就牛逼啦。
//静态内部类ArrayList的构造方法
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
在点进Objects.requireNonNull()方法
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
由此我们可以看出来,当数组i从asList(i)
中传入,然后到静态内部类ArrayList
的构造方法,再到构造方法中的Objects.requireNonNull()
方法,i数组的引用一直给传递下去,最后判断该引用是否为Null,如果不是则直接返回该引用到List
变量list
中。所以list
所指向的地址和i数组指向的地址其实是同一个地址…所以才会互相牵连。
又为什么返回的这个list
不能扩展呢?那是因为这个Arrays
类的静态内部类ArrayList
没有重写父类AbstractList
的add
和remove
功能。所以就相当于直接从父类AbstractList
继承下add
和remove
方法。我们看看AbstractList
类的add
和remove
的源码,我们就一清二楚了。
//AbstractList类相关方法的源码
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
看了源码我们就一目了然了,使用remove
和add
方法会导致报UnsupportedOperationException
异常,因为该方法没有得到重写…so.明白了吧