文章java final关键字中讨论了final的作用。其中提到,对于一个对象FinalClass objRef = new FinalClass;
,限制的只是引用objRef
不能再指向其它对象,而objRef
所指向的对象的内容是可以被改变的。那么如何才能做到一个对象真正的不可变呢(类似C++中的const Class const &ref
)?在java中是靠一组设计规范来实现的,在语言层面没有直接的限制手段。
不可变类的定义
简单来讲,对外不提供setter方法,或者setter/getter方法返回对象/成员的一个副本,对象就可以称为immutable对象。oracle官方文档Immutable Objects对不可变对象进行了定义说明,核心在于该对象一经创建则其状态不再可变。
典型的不可变类的使用场景:
例如String,String在传递过程中,都是以副本的形式copy传递,因此无论传递后如何被修改,修改的都是新的String对象,而老的String对象则是不会被修改的。(这样在传递过程中会产生大量的副本对象,不要担心,java的gc机制会回收不用的对象,效率先不讲)
在Java中,对于String、包装器这些类,我们经常会用他们来作为HashMap的key,试想一下如果这些类是可变的,将会发生什么?后果不可预知,这将会大大增加Java代码编写的难度。
下面是一个不可变对象使用的例子:
class InnerClass {
private final int para;
InnerClass(int para) {
this.para = para;
}
InnerClass(InnerClass obj) {
para = obj.para;
}
int GetPara() {
return para;
}
}
public class ImmutableClass {
private final int val;
private final InnerClass innerObj;
ImmutableClass(int val, InnerClass obj) {
this.val = val;
this.innerObj = obj;
}
// 获取内部成员对象,返回副本
InnerClass GetInnerObj() {
return new InnerClass(innerObj);
}
// 获取内部成员变量,返回副本
int GetVal() {
return val;
}
// 修改内部成员对象,返回副本
ImmutableClass ModInnerObj(InnerClass obj) {
return new ImmutableClass(val, obj);
}
// 修改内部成员变量,返回副本
ImmutableClass ModVal(int val) {
return new ImmutableClass(val, innerObj);
}
void Show() {
System.out.format("ImmutableClass::val = %d, InnerClass::para = %d\n", val, innerObj.GetPara());
}
public static void main(String... args) {
ImmutableClass immutableObj1 = new ImmutableClass(10, new InnerClass(20));
ImmutableClass immutableObj2 = immutableObj1.ModVal(30);
ImmutableClass immutableObj3 = immutableObj2.ModInnerObj(new InnerClass(40));
System.out.println("immutableObj1:");
immutableObj1.Show();
System.out.println("immutableObj2:");
immutableObj2.Show();
System.out.println("immutableObj3:");
immutableObj3.Show();
}
}
输出
immutableObj1:
ImmutableClass::val = 10, InnerClass::para = 20
immutableObj2:
ImmutableClass::val = 30, InnerClass::para = 20
immutableObj3:
ImmutableClass::val = 30, InnerClass::para = 40
可见,无论如何,每一次对不可变对象的修改操作,都不会影响其它的不可变对象。
不可变,并非语言机制上的严格不可变。下面我们再来看下,如何用反射机制绕过这种不可变性的限制:
import java.lang.reflect.Field;
public class ReflactImmutableClass {
public static void main(String... args) {
ImmutableClass immutableObj = new ImmutableClass(5, new InnerClass(10));
System.out.println("before immutableObj.val = " + immutableObj.GetVal());
try {
Field filed = immutableObj.getClass().getDeclaredField("val");
filed.setAccessible(true);
filed.set(immutableObj, 100);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println("after immutableObj.val = " + immutableObj.GetVal());
}
}
执行结果:
before immutableObj.val = 5
after immutableObj.val = 100
不可变对象的成员确实被修改了。
java不可变类定义规范:
- 不可变类的成员变量必须是private(最好用final修饰)
- 不对外提供setter方法,如果提供,则返回值为当前对象的副本。
- 对外提供的getter方法,不能返回本对象和任何内部成员的this引用。
- 不可变类内部对于引用类型的拷贝必须是深拷贝。
- 不可变类最好不要再被继承?
- 如果不可变类包含了可变类的对象,那么需要确保返回的是可变类对象的副本。
- 不要试图通过反射机制打破不可变类的限制,不可变类的实现本身是一种设计约束,好的软件设计要遵循这些设计契约。
不可变类和数据共享
很多对于不可变对象的解释,没有回答一个问题:不可变对象是为了防止多线程同时对数据读写带来的不一致性,那么如果需要多线程间共享对象,如果每次返回的是当前对象的一个副本,那么如何保证两个线程间能够共享到对方的修改?
答案是,不可变对象解决的问题并不是多线程共享对象的问题,虽然二者都跟并发编程有关。如果要实现多线程间对象共享,则需要使用加锁机制。
下面是一个多线程数据共享的例子(更多分析见并发编程):
// 参考https://www.cnblogs.com/john8169/p/9780560.html
public class MulteThreadShareData {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(shareData).start();
new Thread(shareData).start();
}
static class ShareData implements Runnable {
int count = 100;
@Override
public void run() {
while (count > 0) {
decrease();
}
}
public synchronized void decrease() {
count--;
System.out.println(Thread.currentThread().getName() + "this count: " + count);
}
}
}
网友评论