【Java学习笔记系列】深入学习数组与相关知识点总结

深入学习数组与相关知识点总结


虽然之前对数组大致都了解,因为这不是一个困难的知识点,只是一个很普通的基础知识,但是真正运用起来总感觉缺了些什么,那可能是之前对数组的印象一直都停留在能用模糊的概念上,所以今天我们就好好的总结一下数组的相关知识,我这里大致分为三个模块。

  • 数组的概念知识
  • 数组的定义和初始化
  • 数组工具类的应用

数组的概念知识


什么是数组?
数组只是相同类型的,用一个标识符名称封装在一起的一个基本类型序列或对象序列。

数组类型:

  • 基本类型数组 (用于存放基本类型数据的数组)
  • 对象类型数组 (用于Object类型的数组)

数组的特性

  • 数组和类对象一样,实例化的数组和实例化的对象都存储在内存的堆中。
  • 数组对象的大小是固定,是数组实例化的时候已经赋予了的,不允许再被改变,越界是会报运行时异常的
  • 数组对象因为存放在堆中,所以是具有默认值的,如int类型为0,引用类型为Null
  • -

数组与其他种类的容器之间的区别

  • 效率
    (数组是一种效率最高的存储和随机访问对象引用序列的方法,数据就是一个简单的线性序列,这是的元素访问非常迅速)
  • 保存基本类型的能力
    (随着容器可以使用泛型,容器也可以保存基本类型了,所以这不再是数组唯一的特性)

数组的定义和初始化


数组变量的定义

要定义一个数组,只需要在类型名后加上一对空方括号[]

  • int[] a;
  • int a[];

两种格式的含义是一样的,第二种格式符合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,既还不指向任何地址。

数组工具类几种常用的应用


Arrays.fill()

这是一个作用十分有限的功能,相对于基本类型数组来说,只能使用同一个值来填充各个位置。相对于对象数组来说,就是将对象数组的各个部分都引用同一个地址。通过查看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()方法是一个用来复制数组的方法,返回一个新数组对象的引用。那么在说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代表要复制源数组中的多大的长度,既要复制的元素个数。

所以由此可知,我们上是从源数组s10位置开始复制,复制到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()方法是截取源数组i17个元素,并组合成一个新的数组对象,返回新的数组对象的引用给目标数组变量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.equals()

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()

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.binarySearch()

有待

Arrays.asList()

关于Arrays.asList()方法的使用,我查了很多的资料,发现很多地方都在说这个方法的坑。也的确存在一些坑。所以我这里就顺带的数一下,有时间再专门整理一个贴。

总的来说这个Arrays.asList()方法是将一个数组转换成一个集合的实例。

  • 该方法不适用于基本数据类型(byte,short,int,long,float,double,boolean),直接使用会存在一定的问题。
  • 该方法将数组与集合关联在一起,当修改其中一个,也会影响到另一个,因为它们两个使用的是同一个地址
  • 不支持add和remove方法,返回的是一个只可读的“ArrayList”实例,此ArrayList非彼ArrayList.不是我们通常所说的ArrayList

关于基本数据类型不能使用的问题,我暂时还没有想明白,所以暂时这里就不讨论了。

测试代码:

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没有重写父类AbstractListaddremove功能。所以就相当于直接从父类AbstractList继承下addremove方法。我们看看AbstractList类的addremove的源码,我们就一清二楚了。

//AbstractList类相关方法的源码
 public void add(int index, E element) {
     throw new UnsupportedOperationException();
 }


 public E remove(int index) {
     throw new UnsupportedOperationException();
 }

看了源码我们就一目了然了,使用removeadd方法会导致报UnsupportedOperationException异常,因为该方法没有得到重写…so.明白了吧


参考资料

《Java编程思想》第四版
《Java语言程序设计》基础篇
Array.asList:数组转list时你一定要知道的“陷阱”!
Arrays.asList()源码剖析


在此感谢参考过的网站和博客的作者,谢谢!!

猜你喜欢

转载自blog.csdn.net/snailmann/article/details/80333044