数组的基本概念
数组的定义和性质
数组(array)是一种最简单的复合数据类型,它是有序
数据的集合,数组中的每个元素都具有相同的数据类型
,可以用一个统一的数组名和不同的下标来确定数组中唯一的元素。根据数组的维度,可以将其分为一维数组、二维数组和多维数组等。
结合以上定义,Java语言中的数组具有如下三个特性:
一致性:数组只能保存相同数据类型元素。
有序性:数组中的元素是有序的,通过下标进行访问。
不可变性:数组一旦进行初始化,则长度(数组中元素的个数)不可变。
Java中数组索引从0开始,且数组属于引用数据类型
,在使用前一定要开辟空间(实例化),否则编译器会报NullPointerException
异常信息:
public class ArrayDemo {
public static void main(String[] args){
int[] arr=null;
System.out.println(arr[x]);
}
}
数组的初始化
数组的初始化分为静态初始化和动态初始化两种方式。
- 静态初始化:静态初始化是指由程序员在初始化数组时为数组每个元素赋值,由系统决定数组的长度。
数组的静态初始化有两种方式,具体示例如下:
array = new int[ ]{1,2,3,4,5};
int[ ] array = {1,2,3,4,5};
- 动态初始化:动态初始化是指由程序员在初始化数组时指定数组的长度,由系统为数组元素分配初始值。
数组动态初始化,具体示例如下:
int[ ] array = new int[10]; // 动态初始化数组
上述示例中的格式会在数组声明的同时分配一块内存空间供该数组使用,其中数组长度是10,由于每个元素都为int型数据类型,因此上例中数组占用的内存共有104=40个字节。此外,动态初始化数组时,其元素会根据它的数据类型被设置为默认的初始值。
数组的引用传递
数组是引用数据类型,就一定可以发生引用传递,而现在的引用传递的本质也一定是:同一块堆内存空间可以被不同的栈内存所指向。
范例:定义一个方法,实现数组元素2。通过该方法,来查看引用传递的过程。
public class ArrayDemo{
public static void main(String[] args){
int[] data=init();
inc(data);
printArray(data);
}
public static void inc(int[] arr){
for(int i=0;i<a.length;i++)
arr[i]*=2;
}
//此时的方法希望可以返回一个数组类型,所以返回值类型定义为整形数组
public static int[] init() {
return new int[] {1, 2, 3, 4, 6};
}
//定义一个专门进行数组输出的方法
public static void printArray(int temp[]) {
for (int i = 0; i < temp.length; i++) {
System.out.print(temp[i] + "、");
}
}
}
Java中的Arrays工具类
类函数简介
该类位于java.util
包,是用来操作数组的一个工具类。它提供的操作主要包括下面几类:
- 填充数组:通过
fill()
方法 - 数组排序:通过
sort()
方法,按升序排序 - 数组比较判等:通过
equals()
方法 - 有序数组的二分查找:通过
binarySearch()
方法 - 数组转列表:通过
asList()
方法 - 数组复制:通过
copyOf()
或者copyOfRange()
方法 - 打印数组元素:通过
toString()
方法 - 计算数组的哈希值:通过
hashCode()
或者deepHashCode()
方法
Java中的Arrays类对所有基本类型都做了兼容,下面以int
型数组为例,来做部分讲解。
public class Arrays{
//默认升序的排序算法
//串行排序算法,底层实现是双轴快速排序,后两个参数可选
public static void sort(int[] a,int fromIndex, int toIndex)
//并行排序算法,底层实现是归并排序,需要额外空间,后三个参数可选
public static <T> void parallelSort(T[] a,int fromIndex,int toIndex,Comparator<? super T> cmp)
/*----------------------------------------------------------*/
//有序数组的二分查找,范围参数可选
public static int binarySearch(int[] a,int fromIndex,int toIndex,int key)
/*----------------------------------------------------------*/
//可变参数转列表
public static <T> List<T> asList(T... a)
/*----------------------------------------------------------*/
//数组拷贝,底层调用System.arraycopy()这个native方法
public static int[] copyOf(int[] original,int newLength)
/*----------------------------------------------------------*/
//数组内容转字符串打印
public static String toString(int[] a)
//高维数组转字符串,递归转换
public static String deepToString(Object[] a)
/*----------------------------------------------------------*/
//数组元素依次比较,判等
public static boolean equals(int[] a1,int[] a2)
//高维数组判等,递归判断
public static boolean deepEquals(Object[] a1,Object[] a2)
/*----------------------------------------------------------*/
//数组对象的hash码计算
public static int hashCode(int[] a)
//高维数组的hash码计算,递归计算
public static int deepHashCode(Object[] a)
/*----------------------------------------------------------*/
//数组填充指定值,中间两个范围参数可选
public static void fill(int[] a,int fromIndex,int toIndex,int val)
}
类函数源码分析
1. 排序算法
排序算法的源码比较复杂,就不展开讲了。简单总结一下双轴快速排序的实现思路:
首先检查数组的长度,比一个阈值小的时候直接使用双轴快排。其它情况下,先检查数组中数据的顺序连续性。把数组中连续升序或者连续降序的信息记录下来,顺便把连续降序的部分倒置。这样数据就被切割成一段段连续升序的数列。如果顺序连续性好,直接使用TimSort算法。TimSort算法的核心在于利用数列中的原始顺序,所以可以提高很多效率。顺序连续性不好的数组直接使用了 双轴快排 + 成对插入排序。成对插入排序是插入排序的改进版,它采用了同时插入两个元素的方式调高效率。双轴快排是从传统的单轴快排到3-way快排演化过来的。
2. 二分查找
public static int binarySearch(int[] a,int key){
return binarySearch0(a,0,a.length,key);
}
public static int binarySearch(int[] a,int fromIndex,int toIndex,int key){
rangCheck(a.length,fromIndex,toIndex);
return binarySearch0(a,fromIndex,toIndex,key);
}
//二分查找
public static int binarySearch0(int[] a,int fromIndex,int toIndex,int key){
int low=fromIndex;
int high=toIndex-1;//赋值时toIndex的值一定要注意
while(low<=high){
int mid=(high+low)>>>1
if(key>a[mid]){
low=mid+1;
}else if(key<a[mid]){
high=mid-1;
}else{
return mid;//找到了key
}
}
return -(low+1);//没有找到key
}
其实大致上就是常规的二分查找的写法,主要注意没有找到 key 时的返回值,为 -(low + 1)
。其负数表示如果 key 存在,则 key 应该在数组的哪个位置(从 1 开始)上。
3.转列表
//转列表函数
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
//为了方便看类的结构,省略了成员函数的函数体
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size()
@Override
public Object[] toArray()
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a)
@Override
public E get(int index)
@Override
public E set(int index, E element)
@Override
public int indexOf(Object o)
@Override
public boolean contains(Object o)
@Override
public Spliterator<E> spliterator()
@Override
public void forEach(Consumer<? super E> action)
@Override
public void replaceAll(UnaryOperator<E> operator)
@Override
public void sort(Comparator<? super E> c)
}
注意这里 asList()
函数返回的 ArrayList
是 Arrays
的内部私有静态类,而不是我们平时用的在 java.util
包下ArrayList
。这里的 ArrayList
是 java.util
包下的 ArrayList
的精简版,其长度固定不包括增删操作。
这个函数的主要用途如下:
先假设有这样一个情景:
ArrayList<Integer> t=new ArrayList<>();
t.add(e1);
t.add(e2);
t.add(target);
return t;
假如后面不会对这个 t 列表进行增删操作的话,可以简化为下面这一句代码:
return Arrays.asList(e1,e2,target);
4.数组复制
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;
}
其底层调用的是 java.lang
包下的 System
类的 arraycopy()
方法,这个方法是一个本地 (native) 方法。源码如下:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
根据我自己的效率测试,如果需要多次数组复制操作,用这个本地方法的效率是比较高的。
5.数组填充
数组填充其实就是对数组元素进行批量赋值的操作。
源码如下:
public static void fill(int[] a, int fromIndex, int toIndex, int val) {
rangeCheck(a.length, fromIndex, toIndex);//判断是否越界
for (int i = fromIndex; i < toIndex; i++)
a[i] = val;
}