由递归引发的java值传递和不可变对象问题
最近翻看二叉树,想着实现二叉树的各种相关方法。再写java递归时出现小插曲。
最开始的实现形式:
想通过利用函数式接口在方法内定义index的全局变量;
很明显,编译报错。
修改后仍有问题的递归:
一般递归的成立条件:
1、明确的递归出口,存在边界;
2、能使问题规模变小,也就是递归方法实际参数需要变化。
递归方法的index值不会如预期的进一层递归方法就+1变化;
因为java只有值传递没有引用传递,导致以上递归方法的index最多只能变化到2;
因为index++这一行 实际上相当于,
int temp = index.intValue();
tamp++;
index = Integer.valueOf(temp);
index是一个新的对象,于是index在以上方法最多加到2;
1、java的值传递
首先抛出结论:java没有引用传递,只有值传递;java方法只是复制了实际参数的值,基本类型直接复制基本类型值,引用类型复制引用地址;
以前错误的认为,java中参数是基本类型的是值传递,引用类型即为引用传递。
举例:
public void sawp(int a, int b) {
int temp = a;
a = b;
b = temp;
}
public void test() {
int a = 10;
int b = 20;
System.out.println("交换前:a:" + a + " b:" + b);
sawp(a,b);
System.out.println("交换后:a:" + a + " b:" + b);
}
//交换前:a:10 b:20
//交换后:a:10 b:20
很明显不会变;
class User {
int age;
String sex;
//省略构造和get/set方法
}
public void sawp(User a, User b) {
User temp = a;
a = b;
b = temp;
}
public void test() {
User bob = new User(18,"M");
User lily = new User(20,"F");
System.out.println("交换前:bob:" + bob.toString() + " lily:" + lily.toString());
sawp(bob,lily);
System.out.println("交换前:bob:" + bob.toString() + " lily:" + lily.toString());
}
//交换前:bob:age:18 sex:M lily:age:20 sex:F
//交换后:bob:age:18 sex:M lily:age:20 sex:F
结果也不会变;
因为swap方法的实参是复制了bob和lily对象的地址
假如bob的地址是**@7dc5e7b4** lily的地址是**@3d012ddd**
那么swap方法只是把地址复制传给了形式参数 a = @7dc5e7b4
b = @3d012ddd;
swap 方法中 a 和 b 变量互相交换地址,方法结束之后并不能改变bob和lily对象。
执行swap方法:
swap方法结束后,临时副本user1和user2被回收:
那么很显然,java只有值传递,但我们可以利用方法复制实参地址去改变引用对象内部成员属性;
例如:
public void update(User a) {
a.setAge(20);
a.setSex("F");
}
public void test() {
User bob = new User(18,"M");
System.out.println("修改前:bob:" + bob.toString());
update(bob);
System.out.println("修改后:bob:" + bob.toString());
}
//修改前:bob:age:18 sex:M
//修改后:bob:age:20 sex:F
成功修改了bob对象;
但是:
public void update(String a) {
a = "lily";
}
public void test() {
String bob = "bob";
System.out.println("修改前:bob:" + bob);
update(bob);
System.out.println("修改后:bob:" + bob);
}
//修改前:bob:bob
//修改后:bob:bob
没有修改bob对象;
这是因为a = “lily”; 实际上是 a = new String(“lily”)
像String,Integer等都不能通过这种方式来改变实参的值;
因为String,Integer类的对象的值不能被改变;
String,Integer属于不可变类;
不可变类是指所有的成员变量都由final修饰;
2、不可变类
什么是不可变类
一旦一个类的对象被创建出来,在其整个生命周期中,它的成员变量就不可以被修改,java库中包含的不可变类有:String、BigInteger、BigDecimal,Integer,Long。
不可变类的优缺点(为什么要不可变)
1、效率,例如字符串常量池,将一些字符常量放在常量池中重复使用,为了避免相同的字符串重新创建相同的对象,节省存储空间。
2、线程安全,不可变对象天生是线程安全的,在不同线程共享对象,不需要同步机制,因为对象的值是固定的。
缺点:
资源开销,对象需要频繁的修改属性,则每一次修改都会新创建一个对象,产生大量的资源开销。
如何设计不可变类
1、类使用final修饰符修饰,保证类不能被继承。
因为如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。
2、类的成员变量都应该是private final的,保证成员变量不可改变。
3、不提供修改成员变量的方法,例如setter方法。
4、getter方法不能返回对象本身,要返回对象的拷贝,防止对象外泄。
例如String 的char[]属性,返回的是char[]的拷贝;
5、如果提供修改方法,修改对象的属性时要返回新对象。
6、对成员变量的初始化通过构造器进行,并进行深拷贝。
反射改变不可变类的属性
但是不可变并不是真的不可变,通过反射仍然可以改变其属性的值。
3、解决问题
1、此时立即想到利用全局变量,定义index全局变量,再去执行;实现很简单但不优雅;
2、定义一个可变对象,对象中存放index属性,达到修改的目的;
3、
都不优雅,在不改成非递归的前提能不能规避这个问题,
一种解决方案:
// 先序 递归 带#字符串 创建
public BiTreeNode PreAInitBitTree(String biTreeStringA) {
BiTreeNode root = new BiTreeNode();
PreAInitBitTreeChildMethod(root, biTreeStringA.toCharArray(), new int[1]);
return root;
}
public void PreAInitBitTreeChildMethod(BiTreeNode node, char[] biTreeChar, int[] indexArr) {
int index = indexArr[0];
char data = biTreeChar[index];
indexArr[0] = ++index;
if (data == 35 || data == '#') {
node = null;
return;
} else {
BiTreeNode lNode = new BiTreeNode();
BiTreeNode rNode = new BiTreeNode();
node.data = data;
node.lChild = lNode;
node.rChild = rNode;
}
PreAInitBitTreeChildMethod(node.lChild, biTreeChar, indexArr);
PreAInitBitTreeChildMethod(node.rChild, biTreeChar, indexArr);
}