本节内容为昨天未讲完的内容
本节知识大纲:未提到知识点后续补充
一,多维数组
1-1:多维数组的声明以及赋值
1-1-1:多维数组的声明和初始化
这里我们通过二维数组类一探多维数组,其实本质上而言,不管数组的维数,它都是由一维数组慢慢拼凑起来
的。
1-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] [];
结论1
声明二维数组时,我们可以理解为声明的是一个一维数组,比如声明的数组为new int[3] [],其实就是声明了一
个int[3]数组,这个数组的每个元素都是一个新的数组对象,而此时元素存储的每个数组对象的长度还没有指
定。所以在获取元素时,由于二维数组是int类型,所以访问二维数组的元素时获取的值还是默认值0。这里之
所以在数组的位置上写addr只是为了方便后期演示。
内存分析2
结论2
arrs = new int[3] [4],创建一个数组对象,数组中的元素个数是3个,每个元素的存储是一个新的数组,新数组
的长度是4。其实也可以看成是一个三行4列的二维表格。而这个二维数组的索引就是通过索引依次去获取以及
修改等。
1-1-1-2:二维数组的声明第二种方式(动态初始化)
代码演示1
public class Test01 {
public static void main(String[] args) {
//初始化的第三种方式:
//初始化一个数组,数组的长度是3,每个元素都是一个数组,数组索引是1的指向的新数组的长度是1,以后以此 类推
String[][] strs = new String[][]{{"你好","我好"},{"嘿嘿","呵呵"},{"码歌","老薛"}}; }
}
结论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 = {{"你好","我好"},{"嘿嘿","呵呵"},{"码歌","老薛"}};
}
}
这里的内存分析和上图是一致的。
1-2:多维数组的CRUD以及遍历
1-2-1:多维数组的填充值
1-2-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]);
}
}
1-2-1-1-2 打印结果
获取二维数组中的第一个元素:null 查看二维数组中的第一个元素数组中的第一个元素:null
1-2-1-1-3:结论
声明的二维数组本质上就是一个一维数组中的每个元素存储的还是一个数组
在第二步获取二维数组的第一个元素,由于创建方式的问题,其实本质上而言,二维数组的每个元素还不是一
个一维数组的地址,而只是int的默认值0,这个千万要注意
在第三步通过new 数组的方式给二维数组的每个元素填充值
通过strs[索引][索引]去获取或者填充值
1-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,并没有可以通过索引访问的内容,所以报错。所以访问 时,只能访问二维数组的第一个元素
1-2-1-3:通过循环充值
1-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
//循环二维数组的每个元素中的一维数组的长度
for(int j = 0;j
arrs[i][j] = (int)(Math.random()*40);
}
}
//3:查看二维数组中的元素值
System.out.println("查看二维数组中第2个元素数组中的第1个位置上的值是:"+arrs[1][0]);
}
}
1-2-1-3-2:打印结果
打印结果:查看二维数组中第2个元素数组中的第1个位置上的值是:7
1-2-1-3-3:结论
其实二维数组我们可以看做一个二维表格,上述通过循环填充值的代码,其实就是一个表格如下图:
1-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]);
}
}
1-2-3:多维数组的迭代
通过普通for循环和foreach循环依次迭代二维数组
1-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
for (int j = 0;j
arrs[i][j] = (int)(Math.random()*60);
}
}
//3:查看二维数组中的元素值
System.out.println("=======通过for循环迭代=======");
for(int i = 0;i
System.out.print((i+1)+"行\t");
for (int j = 0;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();
}
}
}
1-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
二。数组和可变参数作为形式参数的区别
2-1:为什么需要可变参数
2-1-1:可变参数的演变
需求:计算两个数相加 计算三个数相加 计算四个数等等,此时我们需要定义多个重载方法完成功能,在jdk5之
前,我们可以通过数组完成该功能
2-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
2-1-1-2 问题
每次调用时,都需要对于实际参数进行包装,不够简单。这种做法可以有效的达到“让方法可以接受个数可变的
参数”的目的,只是调用时的形式不够简单
2-1-2:可变参数的定义
2-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
2-1-2-2:结论
调用变得更加简单一点,而且实际方法中使用时也是通过数组的方式解析可变参数的值。
2-1-2:可变参数的使用规则
可变的使用,其实本质上和使用数组是一致的。数组如何操作,可变参数的使用也如何操作即可.
2-1-2-1:可变参数的定义规则
定义可变参数是,就是形参列表中通过type...type_names 这样的形式去定义。
2-1-2-2:可变参数定义时一些问题
2-1-2-2-1:定义可变参数的方法不能包含多个可变参数
测试用例:
public static void function(String... strs,int ... arrs){}
问题:
编译时报错,Vararg parameter must be the last in the list。要确保可变参数在参数列表的最后一个位置。
因为可变参数无法确保传入的实参到底是多少个,所以本质上Java的方法调用还是要确保实参和形参的个数、
顺序、类型要匹配到,不然无法调用
2-1-2-2-2:定义可变参数的方法的可变参数要在最后定义
测试用例
public static void function(String... strs,int num){}
问题:
这的问题和上述问题是一致的,还是有序无法确定int类型的实际参数在调用时是具体是第几个。
2-1-3:可变参数的优势
2-2-1:重载方法调用时,可变参数的调用顺序
2-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("我是个可变参数的方法");
}
}
结果
我是两个个参数的方法 int int
结论
方法调用时首先会进行精确匹配,是参合形参完全匹配的
如果不存在这样的方法,则会采用最优最近方式去匹配,上述例子因为重载方法包含了两个参数的方法,分别
是(int,int)和(int,long)这里会调用离的最近的(int,int),这里的近可以理解为short 和 int的距离要近于short到
long
可变参数的方法,如果上述的两个方法都不存在,则会调用到,也就意味着可变参数的方法调用在最后才会被
调用到。因为在调用可变参数的时候,编译器需要将实参编译为对应的数组,在进行调用。
2-2-2:数组和可变参数同时存在重写方法
2-2-2-1:重写方法的要求
一定要发生继承关系
方法的修饰符子类的要大于或者等于父类的修饰符
方法的返回值子类的小于或者等于父类的返回值类型
方法的名称和子类和父类必须保持一致
方法的参数签名子类的要和父类的一致(包含个数、顺序、类型)
2-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类反编译的代码如下,
你看懂了吗?
PS:本质上最后编译的.class文件中我们发现可变参数会变成一个与之对应的数组。
2-2-2-3:问题
测试用例
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数组,在进行方法调用。请看下图反编译之后
的结果:
测试用例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的方法调用就严格要求类型匹配,所以这里就
报错,类型不匹配。
重点:所以以后在去重写可变参数的方法时,一定要慎重,尽量不要这么干。
2-2-3:重载方法中定义可变参数问题
这个问题在[2-2-1]中已经提及过,就是当出现可变参数方法的时候,千万小心,因为后期的维护时,一不小心
就会调入坑中。
2-2-4:可变参数+null值问题的重载方法
这是原腾讯的一道笔试题,通过阅读请找出问题原因以及解决方案?
2-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 + "]");
}
}
2-2-4-2:原因描述
这里出现问题的原因是由于null值是可以转换为任意引用类型。导致这里方法调用就会出现二义性,编译器不
知道要调用那个方法。所以在调用时需要手动绕开,比如想要调用String时,需要编写 String[] str = null 。这
样程序编译时就知道调用的是fun(String,String...)
2-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,...)。
2-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也不晓得要调用哪个方法。