一.各种类型数据在内存中的存储方式
如果你对什么是值类型什么是引用类型和内部存储不清楚,可以看下面这一小节。
java数据类型可以分为两大类:基本类型(primitive types)和引用类型(reference types)。关系整理一下如下图:
从局部变量/方法参数开始讲起:
局部变量和方法参数在jvm中的储存方法是相同的,都是在栈上开辟空间来储存的,随着进入方法开辟,退出方法回收。以32位JVM为例,boolean/byte/short/char/int/float以及引用都是分配4字节空间,long/double分配8字节空间。对于每个方法来说,最多占用多少空间是一定的,这在编译时就可以计算好。
我们都知道JVM内存模型中有,stack和heap的存在,但是更准确的说,是每个线程都分配一个独享的stack,所有线程共享一个heap。对于每个方法的局部变量来说,是绝对无法被其他方法,甚至其他线程的同一方法所访问到的,更遑论修改。
当我们在方法中声明一个 int i = 0,或者 Object obj = null 时,仅仅涉及stack,不影响到heap,当我们 new Object() 时,会在heap中开辟一段内存并初始化Object对象。当我们将这个对象赋予obj变量时,仅仅是stack中代表obj的那4个字节变更为这个对象的地址。
数组类型引用和对象:
当我们声明一个数组时,如int[] arr = new int[10],因为数组也是对象,arr实际上是引用,stack上仅仅占用4字节空间,new int[10]会在heap中开辟一个数组对象,然后arr指向它。当我们声明一个二维数组时,如 int[][] arr2 = new int[2][4],arr2同样仅在stack中占用4个字节,会在内存中开辟一个长度为2的,类型为int[]的数组,然后arr2指向这个数组。这个数组内部有两个引用(大小为4字节),分别指向两个长度为4的类型为int的数组。
关于String:
原本回答中关于String的图解是简化过的,实际上String对象内部仅需要维护三个变量,char[] chars, int startIndex, int length。而chars在某些情况下是可以共用的。但是因为String被设计成为了不可变类型,所以你思考时把String对象简化考虑也是可以的。String str = new String("hello")
当然某些JVM实现会把"hello"字面量生成的String对象放到常量池中,而常量池中的对象可以实际分配在heap中,有些实现也许会分配在方法区,当然这对我们理解影响不大。
二.弄清值传递和值类型概念
首先值传递与引用传递,在计算机领域是专有名词,如果你没有专门了解过,一般很难自行悟出其含义。而且在理解下面的解释时,请不要把任何概念往你所熟悉的语言功能上套。很容易产生误解。比如Reference,请当个全新的概念,它和C#引用类型中的引用,和C++的&,一点儿关系都没有。
值传递和引用传递,属于函数调用时参数的求值策略(Evaluation Strategy),这是对调用函数时,求值和传值的方式的描述,而非传递的内容的类型(内容指:是值类型还是引用类型,是值还是指针)。
值类型/引用类型,是用于区分两种内存分配方式,值类型在调用栈上分配,引用类型在堆上分配。
值类型/引用类型描述内存分配方式,值传递和引用传递描述参数求值策略,两者之间无任何依赖或约束关系,不要弄混淆。
而且,除了值传递和引用传递,还有一些其它的求值策略。这些求值策略的划分依据是:求值的时机(调用前还是调用中)和值本身的传递方式。详见下表:
上面给出的传值方式的表述有些单薄,下表列出了一些二者在行为表象上的区别。
这些行为,与参数类型是值类型还是引用类型无关。对于值传递,无论是值类型还是引用类型,都会在调用栈上创建一个副本,不同是,对于值类型而言,这个副本就是整个原始值的复制。而对于引用类型而言,由于引用类型的实例在堆中,在栈上只有它的一个引用(一般情况下是指针),其副本也只是这个引用的复制,而不是整个原始对象的复制。
这便引出了值类型和引用类型(这不是在说值传递)的最大区别:值类型用做参数会被复制,但是很多人误以为这个区别是值类型的特性。其实这是值传递带来的效果,和值类型本身没有关系。只是最终结果是这样。
根据上面的解释我们就可以明明白白的看下值传递与引用传递的概念
三.值传递与引用传递
值传递:是指调用函数时将实际参数复制一份传入到函数中,这样函数中如果对参数修改,不会影响实际参数的值。
引用传递:是指调用函数的时候将实际参数的地址传递给函中,这样函数中如果对参数修改,将会影响实际参数的值。
实际参数与形式参数
四.举例java值传递
1.基本数据类型值传递
public class Swap {
public static void main(String[] args) {
int x = 10;
int y = 20;
swap(x, y);
System.out.println("x(2) = " + x);
System.out.println("y(2) = " + y);
}
public static void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
System.out.println("x(1) = " + x);
System.out.println("y(1) = " + y);
}
}
/*输出
x(1) = 20
y(1) = 10
x(2) = 10
y(2) = 20
*/
---------------------
每一个函数都有自己的帧栈,同一个线程的函数在一个栈里,实际在调用函数swap(x,y)时,在栈中copy了一份x,y传给swap函数,所以swap对拷贝数据的修改并不会影响主函数main中的x,y的值。
这个很容易理解,对于基本数据类型 short int long float double char byte boolean这八种按值传递调用函数并不会改变在原函数中的值。
2.引用数据类型的按值传递
引用数据数据类型分为三种:①接口 ②类 ③数组对于引用数据类型的按值传递先给出一个实例对比实例进行分析。
public static void main(String[] args) {
int []a={10,20};
System.out.println("a[0] :"a[0]+"a[1] : "+a[1]);//a[0]=10,a[1]=20;
swap(a, 0, 1);
System.out.println("a[0] :"a[0]+"a[1] : "+a[1]);//a[0]=20,a[1]=10;
}
public static void swap(int []a,int i,int j){
int temp=a[I];
a[i]=a[j];
a[j]=temp;
System.out.println("a[0] :"a[0]+"a[1] : "+a[1]);//a[0]=20,a[1]=10;
}
//输出
/*a[0]=10 a[1]=20
a[0]=20 a[1]=10
a[0]=20 a[1]=10
*/
---------------------
运行程序后发现,swap函数对a[0] ,a[1]的操作竟然影响到了main函数中的a[0] ,a[1]的值,用图来解释一下。
我们知道 对象是保存在堆中。引用指向堆上的对象,也就是说引用的值为对象在栈上的内存地址,在调用swap(a, 0, 1)时实际参数a是引用类型,传递的是一个引用类型的地址,所以copy了a在栈中的引用,所以现在在栈中就有两个地址同时指向了堆中的一个对象,因此在swap中对数组元素进行了交换操作,会影响main中的数据。
上面实际传递的内容是对象的引用,java把对象的引用复制了一份,但是没有把值复制,但是依然属于值传递
总结
java中方法参数传递方式是按值传递。
如果参数是基本类型,传递的是基本类型的字面量值的拷贝。
如果参数是引用类型,传递的是实际参数在栈中地址值的拷贝。