java中 按值传递 与 按址传递 的一些问题
最近再写Java代码时遇到了一个比较有意思的问题,就是在Java中赋值时到底是按值传递还是按地址传递,之前写代码时困扰了我有一段时间。那么首先就要先解释一下按值传递和按地址传递有什么区别。
1、名词解释
- 按值传递:当变量是基本类型变量的时候,是按值传递。
举个例子,相当于你写了作业,然后把作业拍照借给其他同学抄到他的作业上,这个过程中“答案”就实现了一次按值传递,不管那个同学怎么抄,你的答案都不会变。 - 按址传递(引用传递):当变量为引用类型变量,按址传递。
相当于你写了作业,你的答案在你的作业本上,然后你的同学很奇葩的说这是你和他共同合作的作业,他的作业本上没有答案,答案没有传递,他只是获得了一个答案的位置——你的作业本。当他修改答案时,相当于你的答案也被修改了。
基本类型 | byte | short | int | long | float | double | char | boolean |
引用类型 | 类(Object,String…) | 数组(…) | 接口(…) |
“0x10"是地址,指向的内容是字符串"text”
2、赋值的过程
对于基本类型来说,赋值就是把值从一个变量拷贝到另一个变量下的过程,但是对于引用类型来说,赋值不是直接把内容拷贝到新的变量,而是把指向这个变量的地址拷贝到新变量下。如图
3、问题实例
这是举例的一段java代码,我这里简化了一下:
import java.util.Arrays;
public class tttry {
public static void main(String[] args) {
int[][] array ={{2,3,1},
{3,2,1},
{2,1,3}};
///创建一个二维数组
int n = 3;
int[] a = new int[n] ;
int[] b = new int[n];
a = array[0];
Arrays.sort(a);
///对a进行排序
for(int i = 0 ; i < n ; i ++){
b[i] = array[i][0];
}
Arrays.sort(b);
对b进行排序
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
System.out.println(Arrays.equals(a, b));
///比较数组
}
}
这段代码的目的是比较一个二维数组中第一行和第一列的元素是否相同(不要求顺序),虽然看上去很明显第一列[2,3,2]和第一行[2,3,1]不一样,但是运行这段代码之后却会输出[1,2,3],[1,2,3],True。而造成这个问题的原因就是数组是一种 引用数据类型。也就是说,当我对数组a进行排序的同时,我也改变了二维数组array里面的第一行,从而导致数组b的值和我的预期出现了差别,如图:
4、解决方式
其实解决方式很简单,问题的核心就是需要按值赋值,但是数组的”等号“是按地址赋值,那么只要换一个方式达成赋值这一个目的就行了。我这里采用的是新建一个赋值的方法,将数字一个一个按照索引赋值进数组。
public static void arrayAssignment(int[] array3 , int[] array4){
for(int i = 0 ; i < array4.length ; i++){
array3[i] = array4[i];
}
}///通过循环,将被复制的数组的值通过索引一个一个赋值到目标数组
5、问题拓展
在这个问题中其实还可以引出一些其他概念性的问题,例如什么叫实例、什么叫变量、实例变量是怎么创建的,等。
- 变量:程序中用来存储值的标识符。每个变量都有一个特定的类型,决定了变量能够存储的值的类型和操作。变量可以是基本数据类型,如 int、char、boolean 等;也可以是引用类型,如对象或数组。
- 实例:通常指的是一个类在内存中被实例化后的对象。每个实例都有其自己的状态(也就是字段或属性)和行为(也就是方法)。实例的创建是通过使用 new 关键字和构造器来完成的。
实例变量的创建过程可以主要分为几步:
- 声明:声明一个类类型的变量。这个变量将被用来引用对象。
- 实例化:使用关键字 new 来创建类的一个新对象。
- 初始化:使用构造器(constructor)初始化新创建的对象
public class Test {
private int num;
public Test(int num) {
this.num = num;
}///创建构造器
public static void main(String[] args) {
Test testObj = new Test(31);
}///创建Test类的对象并初始化
}
在上述代码中 Test testObj = new Test(10);
是创建Test类的一个新对象并进行初始化的语句。
Test testObj
是声明,声明了一个名为testObj的Test类型的变量。new Test(10)
是实例化和初始化,使用new
关键字创建了一个新的Test对象,并通过构造器初始化。new
关键字会在堆内存中为Test对象分配空间。testObj
变量(位于栈内存)存储了Test对象(位于堆内存)的引用地址,从而可以访问和操作该对象。
这是我的个人理解,如有错误,欢迎指正
6、问题深入
那么为什么Java程序中会有两种不同的数据储存方式呢?这其实和java中jvm的内存分布有关系。jvm的内存区大致可以分成三个区:堆(heap)、栈(stack)、方法区(method)。其中和这个问题最相关的是**栈(stank)和堆(heap)**两个区。
- 栈(stack):是一块线程私有的空间,一个栈,一般由三部分组成:局部变量表、操作数据栈和帧数据区
- 堆(heap):用于存放由new创建的对象和数组。堆是JVM管理的内存中最大的一块,只有一个堆区并且被所有线程共享,目的是为了存放对象实例,几乎所有的对象实例都在这里分配。
而堆和栈其中一个重要的区别就是:栈的内存是顺序分配的,而且定长,所以不存在内存回收的问题;堆的内存是随机分配的,而且不定长度,所以需要进行内存回收(另一个GC进程,定期根据Stack中保存的4字节对象地址扫描Heap ,定位Heap 中这些对象,进行一些优化(例如合并空闲内存块什么的),并把没有定位的内存刷新)。
而从数据类型的角度也可以印证这一点,对于能够存入栈的基本类型来说,每一个基本类型都是定长的,如图
这一点正好与栈分配内存时定长的的特征相符,反观其他引用类型的数据例如数组、类等,数据长度不定(通常会更长),并且更改的频率还更高,显然更适合存放在不定长且空间更大的堆中,而将长度确定的储存地址压入栈里。
有关java内存空间的内容还有很多,我自己也还没有全部清楚,就不多说了。
7、总结
在上方的文章里我总共叙述了我学到的这几个点:
变量,实例,实例的创建,基本数据类型和引用数据类型,传递数据时按值传递和按址传递的区别,jvm中栈和堆的区别
以上只是我的个人理解,欢迎指正
多,我自己也还没有全部清楚,就不多说了。
7、总结
在上方的文章里我总共叙述了我学到的这几个点:
变量,实例,实例的创建,基本数据类型和引用数据类型,传递数据时按值传递和按址传递的区别,jvm中栈和堆的区别
以上只是我的个人理解,欢迎指正
学到的文章:
https://blog.youkuaiyun.com/u010648555/article/details/54946502
https://www.zhihu.com/question/31203609
https://www.cnblogs.com/aaabbbcccddd/p/14539063.html