Java中数组的特性
Java中的数组有一些令人十分困惑的地方。在Java中,数组是引用数据类型,引用类型的数据被创建时,首先要在栈上给其引用(句柄)分配一块内存,而它的具体信息都存储在堆内存上,然后由栈上面的引用指向堆中对象的地址。而令人困惑之处在于:数组是引用数据类型,那它是不是对象呢?
我们从以下几点进行探讨:
-
要判断数组是不是对象,那么首先明确什么是对象,也就是对象的定义。在较高的层面上,对象是根据某个类创建出来的一个实例,表示某类事物中一个具体的个体。对象具有各种属性,并且具有一些特定的行为。而在较低的层面上,站在计算机的角度,对象就是内存中的一个内存块,在这个内存块封装了一些数据,也就是类中定义的各个属性,所以,对象是用来封装数据的。
然而,在较高的层面上,数组不是某类事物中的一个具体的个体,而是多个个体的集合,那么它应该不是对象。而在计算机的角度,数组也是一个内存块,也封装了一些数据,这样的话也可以称之为对象。 -
在使用数组的引用变量时,我们可以用它来调用一些属性和方法:
int[] arr = {1,2,3}; System.out.println(arr.length); //3 Class c = arr.getClass(); System.out.println(c.getName()); //[I int[] arr2 = arr.clone(); System.out.println(Arrays.toString(arr2)); //[1, 2, 3] System.out.println(arr.hashCode());
可以发现,除了数组独有的length属性外,其他的所有方法都来自Object这个类,这说明数组继承了Object类,接下来验证我们的想法:
int[] arr = {1,2,3}; Object obj = arr; //向上转型,可以,编译器没有报错,可以使用obj里面的方法。 int[] arr2 = (int[]) obj; //向下转型,也可以。 System.out.println(obj instanceof int[]) //用instanceof判断obj是不是int[]类型,结果true
所以到这里,我们基本可以得到这样一个结论:数组是一个特殊的类型,有Class实例对象,继承了Object类的很多方法,只是这个类型显得比较奇怪。我们没有自己创建这个类,也没有在Java的标准库中找到这个类。这只能有一个解释,那就是虚拟机自动创建了数组类型,可以把数组类型和8种基本数据类型一样, 当做java的内建类型,基本数据类型也有自己的Class实例对象,包括void也有。这种类型的命名规则是这样的:
每一维度用一个[表示;开头两个[,就代表是二维数组。
[ 后面是数组中元素的类型(包括基本数据类型和引用数据类型)
在java语言层面上,arr是数组,也是一个对象,那么他的类型应该是int[],这样说是合理的。但是在JVM中,他的类型为[I。顺便说一句普通的类在JVM里的类型为 包名+类名,也就是全限定名。
看到了这里,你以为事情真的这么简单?不不不,来看一个例子:
public class Test01 {
public static void main(String[] args) {
String[] s = {"abc","123"};
Object obj = s; //obj指向s,没毛病,String[]是Object的子类,父类变量指向子类的引用
Object[] objs = s; //objs也可以指向s,这说明String[]也是Object[]的子类,这显然不符合Java的单继承
//结果为java.lang.Object,我们通过String[]的Class对象得到它的父类的Class对象可以得到他的父类是Object,这说明String[]的直接父类就是Object
System.out.println(s.getClass().getSuperclass().getName());
}
}
既然String[]的直接父类是Object,那么对于Object[]类型的引用也可以指向String[]类型的对象,只可能是因为这是Java的一种特性,赋予我们的一种特权。只要有继承关系,都可以这么使用,比如:
Student[] s;
Person[] p = s;
当然对于多维数组,这种情况也奏效:
String[][] s = {{"123","abc"},{"456","def"}};
Object[][] objs = s2;
但是对于基本数据类型就不可以了:
//因为基本类型不是引用类型,Object不是它们的父类,在这里自动装箱不起作用
int[] i = {1,2,3};
Object[] objs1 = i; //报错
//但是可以这么使用
Object[] objs2 = {1,'c',true}; //也就是Object类型的数组可以接受任意类型的值,包括基本数据类型,基本数据类型可以自动装箱
Java为什么会为数组提供这样一种语法特性呢?也就是说这种语法有什么作用?
所以这种特性主要是用于方法中参数的传递。Object数组中存放的就是原数组同维度的数据
public class Test01 {
public static void main(String[] args) {
String[] strs = {"123","456","abc"};
test(strs);
}
/*
如果不传递数组,而是依次传递各个值,会使方法参数列表变得冗长。如果使用具体的数组类型,如String[],那么就限定了类型,失去了灵活性。
所以传递数组类型是一种比较好的方式。
*/
public static void test(Object[] objects){
for (Object object : objects) {
System.out.println(object);
}
}
}
但是如果没有上面的数组特性(如果有两个类A和B,如果B继承了A,那么A[]类型的引用就可以指向B[]类型的对象),那么数组类型就只能通过Object类型接收,这样就无法在方法内部访问或遍历数组中的各个元素
为什么说同维度呢?这是什么意思?来看看下面这种情况:
public class Test04 {
public static void main(String[] args) {
String[][][] arr = {{{"123","abc"},{"456","def"}},{{"789","ghi"},{"kfj","yyd"}}};
Object[] objects = arr;
for (Object object : objects) {
System.out.println(object);
}
}
}
傻眼了吧,还有这种操作???
输出的结果是:
[[Ljava.lang.String;@1b6d3586
[[Ljava.lang.String;@4554617c
其实这就是这个三维数组的第二维在内存中的 数据类型+哈希值,即三维数组就是一维数组,只不过三维数组中存放的不是具体的值,而是一组二维数组的引用。
所以对待数组,有时候我们要把它看为数组,有时候得把它看为对象,至于到底是数组还是对象,那就要看Java的设计者了。
为什么java要设计这样迷惑人的特性呢?太折磨人了吧!!!
一维数组
定义
//方式一(推荐):数据类型[] 数组名
int[] arr1;
//方式二:数据类型 数组名[];
String arr2[];
静态初始化
//方式一:数据类型[] 数组名 = {值1,值2,值3...};
int[] arr3 = {1,2,3};
//方式二:数据类型[] 数组名 = new 数据类型[]{值1,值2,值3...};
int arr4 = new int[]{1,2,3};
动态初始化
//数据类型[] 数组名 = new 数据类型[数组长度];
int[] arr5 = new int[5];
二维数组
定义
//数据类型[][] 数组名
int[][] arr1;
静态初始化
//方式一:数据类型[][] 数组名 = {{值1,值2,值3...},{值1,值2,值3...}...}
int[][] arr2 = {{1,2,3},{4,5,6}};
//方式二:数据类型[][] 数组名 = new 数据类型[][]{{值1,值2,值3...},{值1,值2,值3...}...}
int[][] arr3 = new int[][]{{1,2,3},{4,5,6}};
动态初始化
//方式一:数据类型[][] 数组名 = new 数据类型[第一维的长度][第二维的长度]
int[][] arr4 = new int[2][3];
//方式二:数据类型[][] 数组名 = new 数据类型[第一维的长度][],第二维在另行创建
int [][] arr5 = new int[2][];
Arrays工具类
转字符串
//一维数组转字符串
String toString(基本数据类型或者Object[] arr);
//多维数组转字符串
String deepToString(Object[] arr);
填充数组
//全部填充
void fill(int[] arr,int value);
//部分填充,左闭右开
void fill(int[] arr,int fromIndex,int toIndex,int value);
排序
//默认排序
void sort(基本数据类型或Object[] arr);
//根据指定排序器排序
<T> void sort(T[] a, Comparator<? super T> c);
//部分排序,左闭右开
void sort(int[] arr,int fromIndex,int toIndex);
//根据指定排序器排序,部分排序
<T> void sort(T[] a,int fromIndex,int toIndex,g Comparator<? super T> c);
//并行排序,我的理解是排序所有元素同时进行
void parallelSort(基本数据类型或Object[] arr);
数组比较
//比较的是值
boolean equals(基本数据类型或者Object[] arr1,基本数据类型或者Object[] arr2);
//多维数组的比较
boolean deepEquals(Object[] arr1,Object[] arr2);
数组拷贝
/*
基本数据类型数组的拷贝,指定长度
original表示原数组,newLength表示新数组的长度,不足的补默认值。
*/
基本数据类型[] copyOf(基本数据类型[] original,int newLength);
T[] copyOf(T[] original,int newLength);
/*
基本数据类型数组的拷贝,指定范围,左闭右开
original表示原数组
*/
基本数据类型[] copyOfRange(基本数据类型[] original,int fromIndex,int toIndex);
二分查找
//全局查找,元素必须有序
基本数据类型 binarySearch(基本数据类型[] arr,基本数据类型 key);
Object binarySearch(Object[] arr,Object key);
T binarySearch(T[] arr,T key,Comparator<? extends T> c); //指定排序器,元素无需有序
//部分查找,元素必须有序
基本数据类型 binarySearch(基本数据类型[] arr,int fromIndex,int toIndex,基本数据类型 key);
Object binarySearch(Object[] arr,int fromIndex,int toIndex,Object key);
T binarySearch(T[] arr,T key,int fromIndex,int toIndex,Comparator<? extends T> c); //指定排序器,元素无需有序
数组转列表
//如果arr是基本数据类型数组,asList会把这个数组的引用加入列表中,并不会对每个元素进行装箱操作,这时需要使用包装类。
List<T> asList(T... arr);
对数组元素进行指定运算
array = new int[]{3, 10, 4, 0, 2};
Arrays.parallelPrefix(array, (x,y)->(x+y)); //[3, 13, 17, 17, 19]
Arrays.parallelSetAll(array, (x)->(x*x)); //[0, 1, 4, 9, 16]
Arrays.setAll(array, (x)->(x%3)); //[0, 1, 2, 0, 1], 与parallelSetAll相比只是不并行