数组中你可能不知道的一些事,数组和可变参数和重载方法的坑

本文总共13700字,估计阅读时间34分钟。如果你已经会了一部分内容,请查看文末数组常见问题。

本文属于老薛原创内容,转载请注明出处:https://www.jianshu.com/p/0dfca97cc728

数组

一、本章内容知识点概括

本文总共13700字,估计阅读时间34分钟。如果你已经会了一部分内容,请查看文末数组常见问题。

13457029-0e7a76a54de8b216.png
码歌数组章节概括

二、数组基础知识

2-1:什么是数组

数组本质就是用来存储一组数的器皿。

2-1-1:数组的定义

在内存中开辟一块连续的内存区域,用来存储相同数据类型有序集合。

PS:这里有两点我们需要注意我们在后续章节中会发现,现在简单做个普及:

  • 相同数据类型 :数组中存储的数据是相同的数据类型数据,不能出现多种数据类型,但是引用类型(Object)除外。这里大家可以思考一下为什么这么说?
  • 有序 : 数组存储元素在内存层面上讲是有序的,因为是连续的存储空间;另一方面数组获取值是通过索引去实现,索引也是有序的。

2-2: 数组的定义以及赋值

2-2-1:数组的定义

2-2-1-1:数组定义第一种方式:
public class ArraysTest01 {
    public static void main(String[] args) {
        //声明一个数组
        int[] arrs ;// 声明了一个int类型的数组
    }
}

PS小结:定义格式 数据类型 [] 变量名,例子 int [] arrs ;

2-2-1-2:数组定义第二种方式(不推荐使用):
public class ArraysTest01 {
    public static void main(String[] args) {
        //第二种声明方式
        String strs[];// 声明一个String类型的数组
    }
}

PS小结: 定义格式 数据类型 变量名[],例子int arrs[]

2-2-1-3:考虑为什么第二种方式不推荐

数组本质上也是一种数据类型,所以第一种写法更能体现这种关系。所以很多语言中对于数组的定义,第二种写法编译都是错误的,比如在C#中是无法使用第二种声明方式的。

2-2-2:数组的初始化

当声明一个数组之后,我们需要使用时,本质上和使用局部变量是一样的,我们无法将一个未初始化的数组变量直接使用,所以需要初始化动作

2-2-2-1:静态初始化
2-2-2-1-1:第一种方式
public class ArraysTest02 {
    public static void main(String[] args) {
        //1:静态初始化:
        // 会在存储中开辟存储3个字符串对象的数组,
        // 并且这个三个元素的值也确定了分别是java、htlm、css。
        String[] strs = new String[]{"java","html","css"};
        System.out.println("查看当前数组的长度:"+strs.length);
    }
}

输出内容:

查看当前数组的长度是:3

strs.length用来显示当前数组的元素个数或者叫数组长度

PS小结:

类型 结构 例子
结构 type[] type_names = new type[]{e1,e2,....} String[] strs = new String[]{"java","html","css"}
2-2-2-1-2:第二种方式
public class ArraysTest02 {
    public static void main(String[] args) {
        //1:静态初始化第二种方式:
        //会开辟存储三个字符串对象的字符串数组
        String[] names = {"张三","李四","老薛"};
        System.out.println("names数组的长度:"+names.length);
    }
}

输出内容:

names数组的长度:3

PS小结:

类型 结构 例子
结构 type[] type_names = {e1,e2,....} String[] strs = {"java","html","css"}
2-2-2-2:动态初始化
public class ArraysTest02 {
    public static void main(String[] args) {
         //3:动态初始化:
   // 在内存中开辟一个存放5个数据的int类型的数组 将地址赋值给arrs变量存放
        int[] arrs = new int[5];
        //4:打印输出数组
        System.out.println(arrs);
    }
}

输出内容:

[I@6bdf28bb

PS小结:

打印内容 含义
[ 以后遇到对象输出时,"["开头,证明当前对象是一个数组类型
I 代表当前数组的类型是int类型
@ 自动拼接
6bdf28bb 当前数组对象的hash值

2-2-3:常见错误

  • 声明和初始化数组时,忘记指定长度
int[] arrs = new int[]; //编译错误
  • 重新给数组赋值时,不能通过{}指定,语法不允许
int[] arrs = new int[4];
arrs = {1,20,23};//语法不允许
  • 声明数组时,小心数组的长度,出现越界异常
int[] arrs = new int[4];
System.out.println(arrs[5]);  //java.lang.ArrayIndexOutOfBoundsException异常

2-3:数组常见属性

2-3-1: 数组length属性

数组在声明赋值时,都需要指定当前数组的长度,通过length属性获取当前数组的长度。

public class ArraysTest03 {
    public static void main(String[] args) {
        //1:声明一个int类型的数组
        int[] arrs = new int[5];//声明了一个存放5个数据的int类型的数组 名字叫arrs
        //2:查看数组中的元素
        System.out.println("查看数组中的元素:"+arrs[3]);
        //3:获取当前数组的长度
        System.out.println("获取数组的长度:"+arrs.length);
     }
}

2-3-2:数组的索引属性

数组中的元素是有序说的有序称是索引有序。
数组中存储的元素,都存在一个下标,我们可以通过下标去获取、修改以及添加元素到数组中去。一定要注意数组的索引是从0开始的,从length-1结束。数组的索引是[0,length-1]

public class ArraysTest03 {
    public static void main(String[] args) {
        //1:声明一个int类型的数组
        int[] arrs = new int[5];
        //2:查看数组中的元素
        System.out.println("查看数组中的元素:"+arrs[3]);
        //3:获取当前数组的长度
        System.out.println("获取数组的长度:"+arrs.length);
        //4:通过索引获取数组中第5个元素的值
        System.out.println("查看数组中第5个元素的值:"+arrs[4]);
        //5:通过索引改变对应位置上的元素值
        arrs[4] = 10;
        System.out.println("索引是4的元素,也就是第5个元素的值是:"+arrs[4]);
    }
}

2-3-3:内存分析

2-3-3-1:代码实例:
public class ArraysTest05 {
    public static void main(String[] args) {
        //1:声明一个存放5个int类型元素的数组
        int[] arrs = new int[5];
        //2:指定数组索引是0的,也就是第一个元素的值是10
        arrs[0] = 10;
        //3:获取索引是1的,也就是第2个元素的值
        System.out.println("查看第2个元素的值是:"+arrs[1]);
    }
}
2-3-3-2:内存展示01
13457029-aa8f8ebebb92564c.png
第一步

ps:第一步,创建int数组对象时,内存结构

  • 1>、会在堆内存当中开辟5个长度的连续的存储空间,用来存储int类型的元素。
  • 2>、由于当前数组的类型是int类型,所以每个存储空间的元素都是int的默认值0填充。如果是String类型则默认值为null。
  • 3>、将堆内存的地址付给arrs变量存储
2-3-3-3:内存展示02
13457029-054ced2dd4feb3f4.png
第二步

ps:第二步,将索引是1的元素的位置改为10,注意索引是从0开始,length-1结束

在按照元素的索引获取元素值时,注意图中的索引,我们都是安装元素的索引对于数组中的元素进行获取和修改。

2-3-3-4:为什么说数组是不可变的
2-3-3-4-1:测试用例
public class ArraysTest05 {
    public static void main(String[] args) {
        //1:声明一个存放5个int类型元素的数组
        int[] arrs = new int[5];
        //2:指定数组索引是0的,也就是第一个元素的值是10
        arrs[0] = 10;
        //3:获取索引是1的,也就是第2个元素的值
        System.out.println("查看第2个元素的值是:"+arrs[1]);
        //4:重新赋值一个新的数组 这里是改变了数组长度吗?并没有
        arrs = new int[7];
        //5:给arrs第6个元素赋值
        arrs[5] = 33;
        //6:输出索引是5的元素及第6个元素
        System.out.println(arrs[5]);
    }
}

2-3-3-4-2:内存分析
13457029-aed2396200bfa9a6.png
内存分析
2-3-3-4-3:结论

结论:数组一旦声明,其长度是不可变得,我们通常说的改变长度其实都是通过重新定义一个新的数组,改变局部变量的引用而已。

2-4: 针对于数组的CRUD

2-4-1:数组的修改以及获取元素

2-4-1-1:获取元素(通过索引)
public class ArraysTest06 {
    public static void main(String[] args) {
        //1:声明一个存放3个String字符串对象的数组
        String[] strs = new String[3];
        //2:获取strs数组中的第一个元素
        System.out.println("strs数组中的第一个元素是:"+strs[0]);
    }
}

​ PS:输出内容

strs数组中的第一个元素是:null
2-4-1-2:修改元素(通过索引)
public class ArraysTest06 {
    public static void main(String[] args) {
        //1:声明一个存放3个String字符串对象的数组
        String[] strs = new String[3];
        //2:修改数组的第一个元素的值
        strs[0] = "老薛好帅";
        //3:获取strs数组中的第一个元素
        System.out.println("strs数组中的第一个元素是:"+strs[0]);
    }
}

PS:输出内容

strs数组中的第一个元素是:老薛好帅

2-4-2:数组添加元素和删除元素的细节

这里我们暂且不细述添加和删除元素的细节,在后续章节再做介绍,但是这里我们给一点思路,删除和添加元素时需要查看原来的数组的大小,一般情况下我们都会返回一个新的数组。

2-5:数组的遍历方式

2-5-1:数组的迭代第一种(普通for循环)

public class ArraysTest04 {
    public static void main(String[] args) {
        //1:声明一个数组
        int[] arrs = new int[6];

        //2:通过索引一次给数组中的元素填充值
        int value = 10;
        for(int i = 0;i<arrs.length;i++){
            arrs[i] = value+(i*2);
        }

        System.out.println("当前数组的长度是:"+arrs.length);
        //3:遍历迭代当前数组  依次获取数组中的值
        for(int i = 0;i<arrs.length;i++){
            System.out.println("索引是"+i+"的元素的是:"+arrs[i]);
        }
    }
}

PS:通过普通for循环,通过索引填充以及迭代当前数组的元素。

2-5-2:数组的迭代第二种(foreach)

public class ArraysTest04 {
    public static void main(String[] args) {

        //1:声明一个数组
        int[] arrs = new int[6];

        //2:通过索引一次给数组中的元素填充值
        int value = 10;
        for(int i = 0;i<arrs.length;i++){
            arrs[i] = value+(i*2);
        }
        //3:通过foreach迭代当前数组
        System.out.println("jdk1.5之后,提供了增强for循环,专门用来获取集合中的元素");
        for(int num:arrs){
            System.out.println(num);
        }
    }
}

PS:通过foreach循环迭代数组中的元素。但是千万注意,foreach循环对于和索引有关的操作无能为力,它只能做为迭代和遍历使用。

三、多维数组

3-1:多维数组的声明以及赋值

3-1-1:多维数组的声明和初始化

这里我们通过二维数组类一探多维数组,其实本质上而言,不管数组的维数,它都是由一维数组慢慢拼凑起来的。

3-1-1-1:二维数组的声明第一种方式(静态初始化)

这里我们将二维数组的声明和初始化放在一起来演示

代码演示1
public class Test01 {
    public static void main(String[] args) {

        //1:声明多维数组
        //声明一个二维数组arrs 其实这里也可以理解为一个一维数组,只不过数组中包含的内容是一个数组
        int[][] arrs;

        //2:数组的初始化:
        //指定当前数组的长度。声明了一个一维数组,数组中包含三个元素 每个元素都是一个新的数组
        arrs = new int[3][];

        //3:初始化第二种方式:
        //声明一个数组,数组长度是3,每个元素中存放的是一个长度为4的新的数组 有点类似表格的展示
        arrs = new int[3][4];
    }
}
内存分析1

第二行代码初始化方式内存分析,及arrs = new int[3] [];

13457029-01095f1698397db9.png
第一步
结论1

声明二维数组时,我们可以理解为声明的是一个一维数组,比如声明的数组为new int[3] [],其实就是声明了一个int[3]数组,这个数组的每个元素都是一个新的数组对象,而此时元素存储的每个数组对象的长度还没有指定。所以在获取元素时,由于二维数组是int类型,所以访问二维数组的元素时获取的值还是默认值0。这里之所以在数组的位置上写addr只是为了方便后期演示。

内存分析2

第三行代码初始化方式内存分析,及arrs = new int[3] [4];

13457029-ccecd663f27d2478.png
image
结论2

arrs = new int[3] [4],创建一个数组对象,数组中的元素个数是3个,每个元素的存储是一个新的数组,新数组的长度是4。其实也可以看成是一个三行4列的二维表格。而这个二维数组的索引就是通过索引依次去获取以及修改等。

3-1-1-2:二维数组的声明第一种方式(动态初始化)
代码演示1
public class Test01 {
    public static void main(String[] args) {
        //初始化的第三种方式:
        //初始化一个数组,数组的长度是3,每个元素都是一个数组,数组索引是1的指向的新数组的长度是1,以后以此类推
        String[][] strs = new String[][]{{"你好","我好"},{"嘿嘿","呵呵"},{"码歌","老薛"}};
    }
}
内存分析1
13457029-0af9321482277b82.png
动态创建
结论1

1:会在堆内存中开辟一个连续的存储空间,存储当前的多维数组。

2:数组的每个元素其实存储的是一个新的一维数组。存储的是一维数组的地址。

3:根据声明方式,新的一维数组中的元素个数是两个,所以会开辟三个新的一维数组,开始给新的一维数组的每个元素赋值null

4:通过静态初始化的方式,新的一维数组的元素开始正常赋值,比如:strs[0] 代表的是二维数组的第一个元素,str[0][0]代表二维数组的第一个元素中存储的一维数组的第一个元素,赋值你好,str[0][1]代表二维数组的第一个元素中存储的一维数组的第二个元素,赋值我好。然后依次类推

代码演示2:(第二种静态初始化)
public class Test01 {
    public static void main(String[] args) {
        //初始化的第三种方式:
        //初始化一个数组,数组的长度是3,每个元素都是一个数组,数组索引是1的指向的新数组的长度是1,以后以此类推
        String[][] strs = {{"你好","我好"},{"嘿嘿","呵呵"},{"码歌","老薛"}};
    }
}
内存分析2:

这里的内存分析和上图是一致的。

3-2:多维数组的CRUD以及遍历

3-2-1:多维数组的填充值

3-2-1-1:简单的填充值
3-2-1-1-1:测试用例
public class Test02 {
    public static void main(String[] args) {
        //1:声明一个二维数组,存放3个一维数组
        String[][] strs = new String[3][];
        //2:获取二维数组中的第一元素
        System.out.println("获取二维数组中的第一个元素:"+strs[0]);
         //3:给二维数组中的每个元素指定意味数组的长度
        //3-1:指定二维数组的第一个元素是一个长度为2的数组
        strs[0] = new String[2];
        //3-2:指定二维数组的第二个元素是一个长度为2的数组
        strs[1] = new String[2];
        //3-3:指定二维数组的第三个元素是一个长度为2的数组
        strs[2] = new String[2];
         //4:查看二维数组中的第一个元素,该元素的第一个位置上的元素值
        System.out.println("查看二维数组中的第一个元素数组中的第一个元素:"+strs[0][0]);
3-2-1-1-2:打印结果
获取二维数组中的第一个元素:null
查看二维数组中的第一个元素数组中的第一个元素:null
3-2-1-1-3:结论
  • 声明的二维数组本质上就是一个一维数组中的每个元素存储的还是一个数组
  • 第二步获取二维数组的第一个元素,由于创建方式的问题,其实本质上而言,二维数组的每个元素还不是一个一维数组的地址,而只是int的默认值0,这个千万要注意
  • 第三步通过new 数组的方式给二维数组的每个元素填充值
  • 通过strs[索引][索引]去获取或者填充值
3-2-1-2:常见的错误
很多人在这里犯错,觉得通过上述方式可以声明的就是二维数组,通过
   索引访问二维数组中的一个元素:通过strs[0][0],那么结果是什么呢?
System.out.println("获取二维数组中的第一个数组的第一个元素:"+strs[0][0]);
报错:Exception in thread "main"java.lang.NullPointerException
at com.mage.arrays.multi.Test02.main(Test02.java:16)
因为在声明二维数组的时候,只指定了二维数组中的存储元素是3个,但是这三个元素中存储的一维数组并没有指定长度,也就意味着,这里访问第一个元素时,第一个元素是0,并没有可以通过索引访问的内容,所以报错。所以访问时,只能访问二维数组的第一个元素
3-2-1-3:通过循环填充值
3-2-1-3-1:测试用例
public class Test03 {
    public static void main(String[] args) {

        //1:声明二维数组,且通过循环填充值
 //声明的二维数组,包含三个元素,每个元素中存储一个包含了4个元素的一维数组
        int[][] arrs = new int[3][4];

        //2:通过循环填充值
        //循环二维数组的长度
        for(int i = 0;i<arrs.length;i++){
            //循环二维数组的每个元素中的一维数组的长度
            for(int j = 0;j<arrs[i].length;j++){
                ////往指定的位置上填充值
                arrs[i][j] = (int)(Math.random()*40);
            }
        }

        //3:查看二维数组中的元素值
        System.out.println("查看二维数组中第2个元素数组中的第1个位置上的值是:"+arrs[1][0]);
     }
}
3-2-1-3-2:打印结果
打印结果:查看二维数组中第2个元素数组中的第1个位置上的值是:7
3-2-1-3-3:结论

其实二维数组我们可以看做一个二维表格,上述通过循环填充值的代码,其实就是一个表格如下图:

13457029-eba09524836f510f.png
表格

3-2-2:多维数组修改、查看元素

我们通过索引去查看以及修改元素

测试代码:

public class Test02 {
    public static void main(String[] args) {
        //1:声明一个二维数组,存放3个一维数组 每个数组长度为4
        String[][] strs = new String[3][4];
        //2:通过索引给二维数组的第一个元素数组中的第二个元素填充值
        strs[0][1] = "嘿嘿";
        System.out.println("查看二维数组中的第一个元素位置上的数组的第二位位置上的值是:"+strs[0][1]);
    }
}

3-2-3:多维数组的迭代

通过普通for循环和foreach循环依次迭代二维数组

3-2-3-1:测试用例
public class Test04 {
    public static void main(String[] args) {

        //1:声明二维数组,且通过循环填充值
//声明的二维数组,包含三个元素,每个元素中存储一个包含了4个元素的一维数组
        int[][] arrs = new int[4][5];

        //2:通过循环填充值
        for(int i = 0;i<arrs.length;i++){
            for (int j = 0;j<arrs[i].length;j++){
                arrs[i][j] = (int)(Math.random()*60);
            }
        }

        //3:查看二维数组中的元素值
        System.out.println("=======通过for循环迭代=======");
        for(int i = 0;i<arrs.length;i++){
            System.out.print((i+1)+"行\t");
            for (int j = 0;j<arrs[i].length;j++){
                System.out.print(arrs[i][j]+"\t");
            }
            System.out.println();
        }

        //4:查看二维数组中的元素值
        System.out.println("=======通过foreach循环迭代=======");
        for(int[] arr:arrs){
            for (int value:arr){
                System.out.print(value+"\t");
            }
            System.out.println();
        }
    }
}
3-2-3-2:打印结果
=======通过for循环迭代=======
1行  40  6   16  12  29  
2行  12  50  22  48  16  
3行  21  54  5   44  20  
4行  6   34  28  3   16  
=======通过foreach循环迭代=======
40  6   16  12  29  
12  50  22  48  16  
21  54  5   44  20  
6   34  28  3   16  

四、数组和可变参数作为形式参数的区别

4-1:为什么需要可变参数

4-1-1:可变参数的演变

需求:计算两个数相加 计算三个数相加 计算四个数等等,此时我们需要定义多个重载方法完成功能,在jdk5之前,我们可以通过数组完成该功能

4-1-1-1:测试用例
public class Test01 {
    public static void main(String[] args) {
        //1:定义调用方法实参
        int num1 = 10;
        int num2 = 20;
        int num3 = 30;
        
        //2:将多个值通过数组包装
        int[] arrs = new int[]{num1,num2,num3};

        //3:调用相加的方法完成功能
        add(arrs);
    }
    //定义方法完成该功能
    public static void add(int[] arrs){
        int totle = 0;
        for(int num:arrs){
            totle += num;
        }
        System.out.println("多个值相加结果是:"+totle);
    }
}
打印结果:多个值相加结果是:60
4-1-1-2:问题

每次调用时,都需要对于实际参数进行包装,不够简单。这种做法可以有效的达到“让方法可以接受个数可变的参数”的目的,只是调用时的形式不够简单

4-1-2:可变参数的定义

4-1-2-1:测试用例

定义可变参数方法完成功能

public class Test02 {
    public static void main(String[] args) {
        /**
         * jdk5 之后支持可变参数 使得调用变得更加简单
         */
        //1:定义调用方法实参
        int num1 = 10;
        int num2 = 20;
        int num3 = 30;

        //2:调用add方法
        add(num1,num2,num3);

    }
    //定义可变参数的方法
    public static void add(int... arrs){
        int totle = 0;
        for(int num:arrs){
            totle += num;
        }
        System.out.println("多个值相加结果是:"+totle);
    }
}
打印结果:多个值相加结果是:60
4-1-2-2:结论

调用变得更加简单一点,而且实际方法中使用时也是通过数组的方式解析可变参数的值。

4-1-2:可变参数的使用规则

可变的使用,其实本质上和使用数组是一致的。数组如何操作,可变参数的使用也如何操作即可。

4-1-2-1:可变参数的定义规则

定义可变参数是,就是形参列表中通过<span style="color:red">type...type_names</span> 这样的形式去定义。

4-1-2-2:可变参数定义时一些问题
4-1-2-2-1:定义可变参数的方法不能包含多个可变参数

测试用例:

public static void function(String... strs,int ... arrs){}

问题:

编译时报错,Vararg parameter must be the last in the list。要确保可变参数在参数列表的最后一个位置。因为可变参数无法确保传入的实参到底是多少个,所以本质上Java的方法调用还是要确保实参和形参的个数、顺序、类型要匹配到,不然无法调用

4-1-2-2-2:定义可变参数的方法的可变参数要在最后定义

测试用例:

public static void function(String... strs,int num){}

问题:

这的问题和上述问题是一致的,还是有序无法确定int类型的实际参数在调用时是具体是第几个。

4-1-3:可变参数的优势

  • 调用形式变得更加简单
  • 调用者可以根据自己的需要传入合适的参数即可
  • 避免通过数组调用时需要构造数组的过程

4-2:可变参数的一些坑

<h4 id="4.2.1">4-2-1:重载方法调用时,可变参数的调用顺序</h4>

4-2-1-1:测试用例:
public class Test03 {
    public static void main(String[] args) {
        short s = 20;
        fun(10,s);
    }

    public static void fun(int num1,int num2){
        System.out.println("我是两个个参数的方法 int int");
    }

    public static void fun(int num1,long num2){
        System.out.println("我是两个个参数的方法 int long");
    }

    public static void fun(int ... num){
        System.out.println("我是个可变参数的方法");
    }
}
4-2-1-2:打印结果
我是两个个参数的方法 int int
4-2-1-3:结论
  • 方法调用时首先会进行精确匹配,是参合形参完全匹配的
  • 如果不存在这样的方法,则会采用最优最近方式去匹配,上述例子因为重载方法包含了两个参数的方法,分别是(int,int)和(int,long)这里会调用离的最近的(int,int),这里的近可以理解为short 和 int的距离要近于short到long
  • 可变参数的方法,如果上述的两个方法都不存在,则会调用到,也就意味着可变参数的方法调用在最后才会被调用到。因为在调用可变参数的时候,编译器需要将实参编译为对应的数组,在进行调用。

4-2-2:数组和可变参数同时存在重写方法

4-2-2-1:重写方法的要求
  • 一定要发生继承关系
  • 方法的修饰符子类的要大于或者等于父类的修饰符
  • 方法的返回值子类的小于或者等于父类的返回值类型
  • 方法的名称和子类和父类必须保持一致
  • 方法的参数签名子类的要和父类的一致(包含个数、顺序、类型)
4-2-2-2:编写测试用例
需求:

提供商品打折功能,父类定义了打折的方法,子类根据需要重写父类的方法。不过这里父类的方法形参通过可变参数定义,子类重写的方法通过数组定义。

编写测试用例
class F{
    void fun(int price,int ... discount){
        System.out.println("F.fun");
    }
}
class S extends F{
    @Override
    void fun(int price, int[] discount) {
        System.out.println("S.fun");
    }
}
问题

注意这里并不会出现报错,很多人好奇的原因是由于这里子类重写的方法的参数列表和父类的不一样呀,为什么@Override难道不会报错吗?这里注意确实不会报错,我们通过反编译工具可以看到F类反编译的代码如下,你看懂了吗?

13457029-ac2865caf516457a.png
F反编译之后

PS:本质上最后编译的.class文件中我们发现可变参数会变成一个与之对应的数组。

4-2-2-3:问题
编写测试用例1:
public class Test04 {
    public static void main(String[] args) {
        F f = new S();
        f.fun(10,10);
        S s = new S();
    }
}
测试用例输出结果
S.fun
结果分析

这里使用到了多态,在真正运行时,会执行子类中的fun方法,但是参数列表是根据父类确定的。而此时父类中的方法是可变参数,这时会把传入的10编译器会猜测为数组。因为可变参数可以接受多个值,所以根据传入的参数,其实这里会对于输入的10进行包装,将其封装为一个int数组,在进行方法调用。请看下图反编译之后的结果:

13457029-31fc11b6177cf0a9.png
测试类的反编译工具
编写测试用例2:
public class Test04 {
    public static void main(String[] args) {
        F f = new S();
        f.fun(10,10);
        S s = new S();
        s.fun(10,10);
    }
}
测试用例输出结果
编译报错,Wrong 2nd argument type. Found: 'int', required: 'int[]' 
结果分析

这里调用时,由于直接指定了子类调用该方法,但是注意子类中的方法的参数列表是个数组,本身也是一种数据类型,编译器无法将一个10直接转为一个数组类型。本身Java的方法调用就严格要求类型匹配,所以这里就报错,类型不匹配。

<span style="color:green">重点:所以以后在去重写可变参数的方法时,一定要慎重,尽量不要这么干。</span>

4-2-3:重载方法中定义可变参数问题

这个问题在<span style="color:green">[4-2-1]</span>中已经提及过,就是当出现可变参数方法的时候,千万小心,因为后期的维护时,一不小心就会调入坑中。

4-2-4:可变参数+null值问题的重载方法

这是原腾讯的一道笔试题,通过阅读请找出问题原因以及解决方案?

4-2-4-1:一下测试用例会不会出问题?原因是什么?
public class Test05 {
    public static void main(String[] args) {
        //调用该方法会出现什么问题
        fun("",null);
    }

    public static void fun(String str,String ... strs){
        System.out.println("str = [" + str + "], strs = [" + strs + "]");
    }
    public static void fun(String str,Integer ... ins){
        System.out.println("str = [" + str + "], ins = [" + ins + "]");
    }
}
4-2-4-2:原因描述

这里出现问题的原因是由于null值是可以转换为任意引用类型。导致这里方法调用就会出现二义性,编译器不知道要调用那个方法。所以在调用时需要手动绕开,比如想要调用String时,需要编写 String[] str = null 。这样程序编译时就知道调用的是fun(String,String...)

4-2-4-3:增加难度 一下代码会出问题吗?
public class Test06 {
    public static void main(String[] args) {
        invoke(null,1);//会调用哪个方法呢?
    }

    static void invoke(Object obj,Object ... args ){
        System.out.println("obj = [" + obj + "], args = [" + args + "]");
    }
    static void invoke(String str,Object obj,Object ... args){
        System.out.println("str = [" + str + "], obj = [" + obj + "], args = [" + args + "]");
    }
}

这里会选择调用invoke(String,...),注意重载方法调用时由于null既可以作为Object类型,也可以作为String类型。那么这里遵守的规则就是查看继承关系,由于String是继承Object,所以会选择invoke(String,...)。

4-2-4-4:好奇害死猫
public class Test07 {
    public static void main(String[] args) {
        invoke(null,1);//会调用哪个方法呢?
    }

    static void invoke(Integer obj,Object ... args ){
        System.out.println("obj = [" + obj + "], args = [" + args + "]");
    }
    static void invoke(String str,Object obj,Object ... args){
        System.out.println("str = [" + str + "], obj = [" + obj + "], args = [" + args + "]");
    }
}

这里会就会报错,存在二义性,导致JVM也不晓得要调用哪个方法。

猜你喜欢

转载自blog.csdn.net/weixin_33716941/article/details/87490174