我想很多人如果从c/c++转向java开发的,并且有些项目经验的,对于函数中参数传递方式都会先入为主的,将c/c++那套分析参数传递方式搬过来理解java的参数传递方式,而且大部分都能解释的通,恩,开始我也是这么认为的,但后面看面试题,发现原来我一直错了,java中只存在值传递,只存在值传递!!!其实并不存在引用传递,因为java中是没有指针的。
很多人存在的错解:
Java中的基本类型是指byte/short/int/long/float/double/char/boolean八个类型,这八大类型由于都是不可变的,所以在作为参数传递时,即使在方法内部被修改了值,该参数的原值也是不变的(因为对这八个类型的修改会重新生成一个新的对象),由于这一特性所以被叫做值传递。
引用传递是针对一个Object对象(数组,类,接口,String)作为形参传递,在传递时将对象的引用地址作为参数传递给方法,在方法内部就可以通过该引用地址(实参)访问对象,然后修改对象内容,但是如果将改地址重新指向另一个对象,这时外面的对象并未受到影响。
首先科普一下Java中的数据类型分为引用数据类型和基本数据类型
1、java的八大基本数据类型:
byte:Java中最小的数据类型,在内存中占8位(bit),即1个字节,取值范围-128~127,默认值0
short:短整型,在内存中占16位,即2个字节,取值范围-32768~32717,默认值0
int:整型,用于存储整数,在内在中占32位,即4个字节,取值范围-2147483648~2147483647,默认值0
long:长整型,在内存中占64位,即8个字节-2^63~2^63-1,默认值0L
float:浮点型,在内存中占32位,即4个字节,用于存储带小数点的数字(与double的区别在于float类型有效小数点只有6~7位),默认值0
double:双精度浮点型,用于存储带有小数点的数字,在内存中占64位,即8个字节,默认值0
char:字符型,用于存储单个字符,占16位,即2个字节,取值范围0~65535,默认值为空
boolean:布尔类型,占1个字节,用于判断真或假(仅有两个值,即true、false),默认值false
2、引用数据类型分为:,String,类,接口,数组;
3、类型详细讲解:
整数类型:byte、short、int、long都是表示整数的,只不过他们的取值范围不一样。
byte(字节类型) 一个byte 8位,取值范围为-128~127,占用1个字节(-2的7次方到2的7次方-1)默认是0
short(短整型) 一个short 16位,取值范围为-32768~32767,占用2个字节(-2的15次方到2的15次方-1)默认是0
int(整型) 一个int 32位,取值范围为(-2147483648~2147483647),占用4个字节(-2的31次方到2的31次方-1)默认是0
long(长整型) 一个long 64位,取值范围为(-9223372036854774808~9223372036854774807),占用8个字节(-2的63次方到2的63次方-1)默认是0L或0l推荐用大写;
所以,注意,敲黑板了,函数中的形参就可以是这两种基本数据类型(当然还可以是复杂的数据结构类型),一是基本数据类型,二是引用数据类型。形参是基本数据类型是值传递,这无可争议,但很多人在实践中发现但形参是对象(数组,类,接口,String)的传递时,如果在函数中改变这几种类型的对象时,函数外的这个对象的值也会发生相应改变(String类型特殊,后面会单独解释),这个跟c/c++的引用传递有没有很像?有木有?是有,但也不要被这个假象所蒙蔽,不要忘记了java没有指针,那何来引用传递一说?实际上这个传入函数的值是对象引用的拷贝,即传递的是引用的地址值,所以还是按值传递。
下面是网上找的某位博主的事例,我已验证,需要补充的我也补上了,自己比较懒,就没有自己创建事例
下面先上测试的代码,后面再做个简单分析。
public class Test {
public static class User {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return "User [firstName=" + firstName + ", lastName=" + lastName
+ "]";
}
}
/**
* @param args
*/
public static void main(String[] args) {
int a1 = 1;
System.out.println("Before change value,a1 is :" + a1);
changeValue(a1);
System.out.println("After change value,a1 is :" + a1);
int a2 = 1;
System.out.println("Before change ref,a2 is :" + a2);
changeRef(a2);
System.out.println("After change ref,a2 is :" + a2);
Long b1 = 100000L;
System.out.println("Before change value,b1 is :" + b1);
changeValue(b1);
System.out.println("After change value,b1 is :" + b1);
User u1 = new User();
u1.setFirstName("Jim");
u1.setLastName("Green");
System.out.println("Before change value,user1 is :" + u1.toString());
changeValue(u1);
System.out.println("After change value,user1 is :" + u1.toString());
User u2 = new User();
u2.setFirstName("Lily");
u2.setLastName("Green");
System.out.println("Before change ref,user2 is :" + u2.toString());
changeRef(u2);
System.out.println("After change ref,user2 is :" + u2.toString());
List list1 = new ArrayList();
list1.add("zhangsan");
System.out.println("Before change value,list1 is :" + list1);
changeValue(list1);
System.out.println("After change value,list1 is :" + list1);
List list2 = new ArrayList();
list2.add("zhangsan");
System.out.println("Before change ref,list2 is :" + list2);
changeRef(list2);
System.out.println("After change ref,list2 is :" + list2);
}
public static void changeValue(int a) {
a = 9;
}
public static void changeRef(int a) {
int b = 9;
a = b;
}
public static void changeValue(Long a) {
a = new Long(100);
}
public static void changeValue(User u) {
u.setLastName("White");
}
public static void changeRef(User u) {
User u2 = new User();
u2.setFirstName("Kate");
u2.setLastName("Green");
u = u2;
}
public static void changeValue(List list) {
list.set(0, "lisi");
}
public static void changeRef(List list) {
List list1 = new ArrayList();
list1.add("wangwu");
list = list1;
System.out.println("After change value in function,list1 is :" + list);
}
}
传入的参数测试了3种情况:
1.值对象传递 2.引用对象 3.集合对象传递
第35~43行测试了函数修改传入值时对外部值的影响。值对象存在函数的调用栈中,changeValue和changeRef内的 值与外部调用的实际上是2个值,不论怎么改变,都不会影响main函数内a的值。执行输入如下:
Before change value,a1 is :1
After change value,a1 is :1
Before change ref,a2 is :1
After change ref,a2 is :1
User对象存储在堆中,在函数调用栈中保存着对其的引用,changeValue函数利用保存的引用直接修改了堆中对象的值,所以会影响外部(main)函数中user1的值;但是changeRef修改了函数内user2的引用地址,但该修改不会对堆上原有的user2对象产生任何影响。执行结果如下:
Before change value,user1 is :User [firstName=Jim, lastName=Green]
After change value,user1 is :User [firstName=Jim, lastName=White]
Before change ref,user2 is :User [firstName=Lily, lastName=Green]
After change ref,user2 is :User [firstName=Lily, lastName=Green]
集合对象与引用对象的原理一样。在方法内,引用的参数list引用的对象其实是发生了改变,但如果在函数体内参数的引用发生改变,对于函数外的这个引用的对象还是不发生改变。
Before change value,list1 is :[zhangsan]
After change value,list1 is :[lisi]
Before change ref,list2 is :[zhangsan]
After change value in function,list is :[wangwu]
After change ref,list2 is :[zhangsan]
总结:Java的函数传参方式都是值传递,唯一区别是形参是基本数据类型,还是引用数据类型,而这两种数据类型造成在函数体中修改了参数的额值,在函数体外是否跟着发生改变,根本原因是,两种数据类型变量在内存中存储的方式不一样,基本类型的变量保存原始值,即它代表的值就是数值本身;而引用类型的变量保存的是引用值,"引用值"指向内存空间的地址,代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。即基本类型的变量直接保存在函数调用栈中,而对象的变量的值其实是保存在堆中,然后将这个对象在堆中的值的地址引用存放到函数调用栈中。
接下来是解释为什么说String虽然说是引用类型,但如果作为函数参数,在函数体发生改变为什么函数外不发生改变的原因。
首先我们来看下面这段代码:
public class Test1 {
String a = "123";
public static void change(Test1 test) {
test.a="abc";
}
public static void main(String[] args) {
Test1 test1=new Test1();
System.out.println(test1.a);
change(test1);
System.out.println(test1.a);
}
}
结果输出123 abc 相信大家都能做对这道题目。Java是按引用传递的,在函数里面可以修改对象的值。我们再看下面的代码:
public class Test2{
public static void main(String[] args) {
String str = "123";
System.out.println(str);
change(str);
System.out.println(str);
}
public static void change(String str){
str = "abc";
}
}
结果输出123 123,???
误区1:
有人说可能是这里String定义的方式有问题,因为这里的定义方式String str = “123”,可能存在问题,java会将其存放在字符串常量池中(当再次声明一个同样内容的字符串时将会使用字符串常量池中原来的那个内存,而不会重新分配)
写成
java String str = new String(“123”);
str是指向堆区的内存会传递引用。可是结果依然打印 123 123
误区2:String是不可变的
还有人查看String文档发现
Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared.For example:
String str = "abc";
is equivalent to:
char data[] = {'a', 'b', 'c'}; String str = new String(data);
可以看到官方文档解释了因为String是不可以改的。所以有人认为这就是在函数里面无法修改String值得原因。
请看下面的代码
public class Test3{
public static void main(String[] args) {
String str = new String("123");
str = new String("abc");
System.out.println(str);
}
}
按照这种理解,这里的String应该还打印123而不是abc。显然这不是瞎扯么。
文档中说StringBuffer是可变的我们用StringBuffer试试
public class Test4{
public static void main(String[] args) {
StringBuffer str = new StringBuffer("123");
System.out.println(str);
change(str);
System.out.println(str);
}
public static void change(StringBuffer str){
str = new StringBuffer("abc");
// str.append(abc);
}
}
依然打印123 123,可见根本不是这个原因,并且对可变不可变的理解有误。
注意Java中所说的按引用传递实质上是传递该对象的地址,该地址其实是按值传递的,通过这个地址可以修改其指向内存处对象的值。改变该地址的值毫无意义,只会失去对真实对象的掌控。
再来解释一下产生上面误区2的原因。文档中说String不可变StringBuffer可变的意思是堆区的那片内存的可变性。
对于String类,通过引用无法修改之前在堆区申请的那段内存,大小是固定的,也就是不能修改他的值,因为他的底层是char数组。当再次给变量new一个值时,他会指向另一个堆区内存,从表面上看也是改变了值。
对于StringBuffer,程序员可以根据实际使用继续分配更多内存,例如调用append方法,这就是可变的意思。比如上面代码4中函数里面的注释那句是可以修改String的值的。
参照博客:
https://blog.csdn.net/sad_orc/article/details/19079993
https://www.cnblogs.com/coderising/p/5697986.html
https://blog.csdn.net/cauchyweierstrass/article/details/49047217