7 值传递和引用传递
文章目录
- 7 值传递和引用传递
- 1. 基本概念
- 2. Java的值传递
- 3. 证明Java值传递
- 4. 总结
- 5. 启示
- 8 Java可变参数
- 1. 基本使用
- 2. 注意事项
- ①二维数组的引入
- 1. 二维数组的引入
- 2. 理解二维数组
- ②二维数组的使用
- 1. 二维数组的声明和初始化
- 1.2 初始化(initialization)
- 2. 二维数组的访问
- 2.1 数组长度
- 2.2 访问二维数组中的某个一维数组
- 2.3 获取指定位置的元素
- ③二维数组的操作
- 3.1 数组的遍历
- 3.2 杨辉三角
- 递归
- 汉诺塔问题
1. 基本概念
调用方法时,我们需要传递实参给方法,在方法内部会改变这些实参的值,那么方法执行完毕后
这些实参的值会怎么变呢?
首先介绍两种值传递的方式
- 值传递(call by value),指的是调用方法时,传入的是实参的拷贝而不是实参本身
- 这样做的结果是,方法改变的是实参拷贝的值,原本实参的值是不会变的
- 引用传递(call by reference),指的是调用方法时,传入的是实参的变量地址
- 注意,基本数据类型也是有地址的
- 这样做的结果,方法改变的是实参本身的值,原来实参的值会跟着变化
Java到底是哪一种传递模式?又或者对基本数据类型是值传递,对引用类型是引用传递?
对于这个问题,首先要搞清楚一些概念
- 引用数据类型和基本数据类型的区别
- 基本数据类型,值就直接保存在栈上的变量中
- 引用数据类型,栈上变量保存的是堆上对象的地址,也就是引用
- 赋值运算符“=”的作用
- 基本数据类型,赋值符号会直接覆盖原本的值,原先的值就没了
- 引用数据类型,赋值符号会直接覆盖引用中原先保存的地址值,原先的地址被覆盖掉
- 注意,原先引用地址所指向的对象不会发生变化(重点)
2. Java的值传递
实际上严格来说,Java只存在值传递,方法得到的是变量的拷贝,而不是变量的地址,因为如果在方法中改变传递过来的引用,并不会对main里的引用造成任何的影响。
- 对于基本数据类型而言,方法调用过程如下
- 在调用的方法的栈帧中初始化一个实参的副本
- 对这个副本进行操作,原先main方法栈帧的变量的值不会发生变化
- 并且这个方法执行完毕后,随着方法出栈,这个实参的副本也被销毁了
- 对于引用数据类型而言,方法调用过程如下
- 在调用的方法的栈帧中声明一个引用的拷贝,因为是拷贝,所以这个引用中存的内存地址和原先的引用一样
- 结果就是,这个副本仍然指向了原先引用所指向的对象
- 对这个引用的副本进行操作,原先main方法栈帧的引用不会发生变化,但由于指向的是同一对象
- 结果就是,引用和引用拷贝指向的对象本身被改变了
- 方法结束后,这个引用拷贝也被销毁了
3. 证明Java值传递
很多程序语言(尤其是C/C++)提供了两种传参方式,导致很多人也认为Java也是两种传参方式
这里举一个反例
- 声明初始化两个数组
- 提供一个交换数组的方法swap
public static void swapArray(int[] arr1,int[] arr2){
int[] temp;
temp = arr1;
arr1 = arr2;
arr2 = temp;
}
// 在main中,两个数组的值并没有发生交换,也就是说在swapArray中数组的地址虽然交换了,但是原本数组的地址并未发生交换
结论:Java当中只存在值传递
- 方法的调用过程如下
- 首先在调用方法的栈帧中声明两个数组的引用拷贝,仍指向原先的数组
- 交换这两个拷贝的值,实际上就是交换它们指向的数组,这里两个拷贝完成了数组交换
- 原先的数组引用不受影响
- 方法结束后,这两个引用拷贝被销毁
4. 总结
总结:Java方法对方法参数能做什么?
- 不能修改基本数据类型的参数的值(局部变量)
- 实际开发中也不会真的有让你把基本数据类型通过方法去改变
- 可以改变引用数据类型中对象里的数据(称之为改变对象的状态,改变对象的属性值)
- 无法直接改变参数引用所指向的地址(引用指向的对象)
5. 启示
写在最后
不要纠结概念,Java到底是什么传值方式?即便是到了今天,仍然会有人在网络上争吵
有些人会说:Java方法传值的本质是值传递不错,但是对于引用类型,确实可以改变对象里的数据
应该叫引用传递。所以Java是值传递和引用传递共存,表面有引用传递,本质是值传递
- 不要和喷子和杠精对线,就告诉他们他们是正确的
- 理解本质,关注原理
- 概念是死的,思想是活跃的
8 Java可变参数
Java1.5增加了新特性,可变参数
适用于参数个数不确定,类型确定的情况,Java会自动把可变参数当作数组处理
1. 基本使用
怎么使用可变参数?
- 可变参数用于形参列表中,并且只能出现在形参列表的最后
- 语法
[修饰符列表] 返回值类型 方法名 (形式参数列表,数据类型... 变量名){
//方法体
}
- 可变参数的三个点位于数据类型和变量名之间,前后有无空格都可以
- 最好是前无空格,后有空格
- 调用可变参数的方法时,编译器为该可变参数隐含创建一个数组,在方法体中以数组的形式访问可变参数
- 可变参数对本质是数组(参考代码),可变参数对原理是jvm底层把输入的不限定个数的参数,封装成数组
2. 注意事项
注意事项
- 调用方法时,如果有一个固定参数的方法匹配的同时,也可以与可变参数的方法匹配,则选择固定参数的方法
- 调用方法时,如果出现两个可变参数的方法都能匹配,则报错,这两个方法都无法调用了
- 严格避免带有可变参数的方法发生重载,否则很容易导致方法永远无法调用
- 一个方法只能有一个可变长参数,并且这个可变长参数必须是该方法的最后一个参数
①二维数组的引入
1. 二维数组的引入
假设我们要建立一张表用来存储班级学生的成绩,对于这种表格形式的数据,可以使用二维数组来存储
一班学生成绩 | 二班学生成绩 | 三班学生成绩 | 四班同学成绩 | … |
90 | 86 | 78 | 90 | |
77 | 86 | 76 | 54 | |
86 | 67 | 87 | 60 | |
… |
2. 理解二维数组
怎么理解二(多)维数组?
- 一维数组可以看成上面表格中的一列数据
- 二维数组可以看成上面的表格数据
- 实际上表格是由很多列数据组成的
- 二维数组就是由很多一维数组组成的
- 二维数组(two-dimensional-array),就是某个一维数组作为另一个数组的元素而存在了
-** 二维数组是一维数组的数组**
- 本质上来说,从内存角度看,并没有多维数组的概念
②二维数组的使用
二维数组的理解
- 二维数组是一维数组的数组
- 内存中并没有存在真正的二维数组,只不过是一维数组中装了一维数组
1. 二维数组的声明和初始化
要想使用二维数组,首先要进行声明和初始化
一些奇怪的面试题目
int a,b; int[] m,n[]; //相当于定义了一个m[], 和一个n[][]
1.2 初始化(initialization)
初始化二维数组的三种格式
静态初始化
动态初始化格式一
二维数组名 = new 数据类型[m][n];
通过这种方式创建的二维数组,里面的每个一维数组的长度都相同
动态初始化格式二
二维数组名 = new 数据类型[m][];
m代表二维数组当中,一维数组的个数
没有直接给出一维数组的长度,可以动态给出
该形式定义的二维数组仍不能使用,还需要手动初始化二维数组中的每一个一维数组
double[][] arr5 = new double[3][]; arr5[0] = new double[5]; arr5[1] = new double[9]; arr5[2] = new double[]{1, 2, 3}; ```
错误的格式
除了三种正确的,都是错误的,例如下面
// 二维数组名 = new 数据类型[][n]; 错误❌ // 二维数组名 = new 数据类型[m][n]{{元素1,元素2,元素..},{元素1..}...}; 错误❌
2. 二维数组的访问
二维数组访问元素和一维数组并无实质不同,只不过是一维数组套一维数组
2.1 数组长度
获取二维数组的长度
语法
二维数组名.length;
二维数组的长度实质上是其中一维数组的个数
二维数组的长度和动态初始化中的m数值相等
获取二维数组中一维数组的长度
语法
二维数组名[m].length;
只有二维数组中的一维数组完成初始化,才能够使用这种形式
- 不然会空指针异常
2.2 访问二维数组中的某个一维数组
语法
二维数组名[m];
只有二维数组中的一维数组完成初始化,才能够使用这种形式
m必须是一个存在的下标,不然会数组越界
获取一维数组后,可以进行各种一维数组的操作
2.3 获取指定位置的元素
语法
二维数组名[m][n];
3.1 数组的遍历
遍历二维数组(traverse)
public static void traverseTwoDArray2(int[][] arr) {
System.out.print("[");
//一维数组使用for循环,二维数组使用双层for循环去遍历
//外层for应该遍历二维数组中的一维数组
for (int i = 0; i < arr.length; i++) {
//内层for应该遍历每一个一维数组
for (int j = 0; j < arr[i].length; j++) {
//这里就是具体元素的值
if (j == 0 && j != arr[i].length - 1) {
//如果是每个数组中的开头元素且不是最后一个元素
System.out.print("[" + arr[i][j] + ", ");
} else if (j == 0) {
//是开头也是最后的元素
System.out.print("[" + arr[i][j] + "], ");
} else if ((j == arr[i].length - 1 && i != arr.length - 1)) {
//如果是每个一维数组的末尾元素,除最后一个
System.out.print(arr[i][j] + "], ");
} else if ((i == arr.length - 1) && (j == arr[arr.length - 1].length - 1)) {
//如果是整个二维数组的最后一个元素
System.out.print(arr[i][j] + "]");
} else {
//普通的在中间的元素
System.out.print(arr[i][j] + " ,");
}
}
}
System.out.println("]");
}
数组工具类中也有这样的功能
Arrays.deepToString(数组)
3.2 杨辉三角
打印杨辉三角
南宋时期数学家杨辉,根据北宋时期的数学家贾宪的作品(现已遗失),发现的一种几何排列规律
被称之为“杨辉三角”,“贾宪三角”,“帕斯卡三角” 16xx 明末清初
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
...
- 规律就是
- 每一行的元素和行数一样,即行数等于列数
- 且第一行,第二行固定为1
- 从第三行开始
- 每一行的开头和结尾都是1
- 其余位置的值,是上一行同列元素和同列-1列元素之和
- 于是
- 声明动态初始化一个二维数组,再用for循环分配每个一维数组的长度
- 其中每个一维数组的长度等于该数组在二维数组中的下标+1
- 为每一个一维数组的首位元素赋值1
- 为其他元素赋值
- 需要注意从第三行开始,并且每一列的两边不用赋值(因为已经赋值过了)
- 元素值 = 上一行同列的元素值 + 上一行上一列的元素值
- 遍历
//1.声明一个二维数组,长度为10
int[][] arr = new int[10][];
//2.为二维数组的每一个元素分配内存空间:第一个元素的长度为1,第二个元素的长度为2
for(int i=0;i<arr.length;i++){
arr[i] = new int[i+1];
}
//3.为二维数组的第一个元素和最后一个元素赋值为1
for(int i=0;i<arr.length;i++){
arr[i][0] = 1;
arr[i][arr[i].length-1] = 1 //注意:这里是arr[i].length-1 !!!
}
//4.为二维数组的其他元素赋值
for(int i=2;i<arr.length;i++){
for(int j=1;j<i;j++){
arr[i][j] = arr[i-1][j-1]+arr[i-1][j];
}
}
//5.打印
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + "\t");
}
System.out.println("");
}
}
概念上不做过多解释,不难理解
汉诺塔问题
public class Hanoi {
public static void main(String[] args) {
System.out.println(count(3));
}
public static int count(int n){
if(n == 1) return 1;
return count(n-1) + 1 + count(n-1);
}
}