常见知识点和易错点:不可变对象
二、如何理解不可变对象是线程安全
不可变对象和不可变的对象引用
千万不要把这两个东西混为一谈!我们用一段demo来说明一下两者的差别:
public class ImmutableObjectDemo {
public static ImmutableObject immutableObject = new ImmutableObject("immutable-test");
public static final StringBuilder finalObject = new StringBuilder("final-test");
public static void main(String[] args) {
System.out.println(immutableObject.getValue());
finalObject.append(" was modified");
System.out.println(finalObject.toString());
}
}
class ImmutableObject {
private final StringBuilder sb;
public ImmutableObject(String value) {
sb = new StringBuilder();
sb.append(value);
}
public String getValue() {
return sb.toString();
}
}
我们创建了两个变量:immutableObject
、finalObject
, 并且后者我们使用了final
关键字进行了修饰,表示这是一个不可变引用! 好的,我们的问题展开了:
这里我所说final只是用于表示这个引用不可变!而非对象不可变!正如代码中演示的,一个没有使用final修饰的引用指向的ImmutableObject
对象,在对象创建后你基本无法修改对象的任何属性(因为类在设计的时候就只为用户提供了getValue()
方法和一个构造方法。)我们可以将其看作为一个状态只读的对象,简称为“不可变对象”!相反,另一个使用了final修饰的引用指向的StringBuilder
对象,我们依然可以通过StringBuild.append()
方法修改对象状态。
通过这个栗子足以帮你区分不可变对象与不可变引用了!总结一下:
final
修饰的是引用,只是引用的指向是不可变的!(如果final修饰的是基本类型, 例如int、double,则只能在初始声明的时候就给出值,并且后面不允许修改!)如果final引用指向的对象是可变的(例如demo中的
StringBuilder
对象),对象是可以修改!
如何理解String对象是不可变对象
区分开不可变引用与不可变对象后,这里插入一个题外话,这个问题也是在Java SE学习阶段很容易被问起的?
如何理解String对象是不可变对象?
老实说,我也是最近才把这个问题捋地差不多:)既然提到了不可变对象,我猜String类的设计思路【状态一旦创建就不可再修改, 状态只读】应该与demo中的ImmutableObject
差不多。
当你打开String源码,翻看类结构后你会发现这样几个要点:
- 有一个
private final
修饰名为value
的byte[]
,注释说明其是用于存储字符串的值的数组! - 基本上所有的方法,返回类型都是
String
,并且在这些方法中很少出现return this;
也就是大部分情况下都是使用return new String();
即大部分情况下,调用API后返回的都是全新的String对象!甚至是对字符串进行拼接都会产生新对象!这也是为什么推荐使用StringBuilder保存可变字符串量,而不推荐使用String! - 除了String提供的公开/非公开构造方法中对
this.value
进行了赋值,其他地方均没有对其修改!
单凭1,3条可以看出String
确实和demo中的ImmutablueObject
设计相同(当然不是因为我牛逼,我只是提前学习了String的设计方式,然后自己模仿出来的类:)String对象的所有“状态”(属性)基本在创建的时候就固定下来了,如果要修改,就只能创建一个新的对象! 所以我们将String
的实例看作是不可变的对象!
其实引发这个问题或者说是歧义的主要问题还是混淆了不可变的对象与不可变的引用!
String str = "Hello, String!";
str = "Hi, String!";
以上代码也就是罪恶的源头,“明明说String是不可变对象, 为什么还可以重新赋值?!”
现在我们可以明确的回答了:“以上两次‘赋值操作’实际只是在修改引用的指向,并不是在修改原String的值!这两个字符串分别对应两个String对象,str这个引用没有使用final修饰,所以是可以自由修改指向的,但是它所指向的对象都是不可变对象!”。所以这与String是不可变对象屁关系没有!

为什么不可变对象是线程安全的
首先我们要清楚利用不可变对象是从哪个方面来应对线程安全问题的,目前我们常见的线程安全问题都是由**“竞态问题”**引起的,即一个线程在修改对象的可变状态时,另一个线程正好要读取或者修改同一个对象的状态,此时后者就有可能读取到的是一个过期的数据,因为可能前者的修改操作还未完成写回这一步骤!
明确要目标问题后,就可以分析不可变对象是如何瓦解这个问题的:**不允许修改发生!**从根本遏止现象发生:)
甚至你可以搭配使用final
或者volatile
修饰引用,来保证引用不可变或者修改引用其他线程立马可见!
但是如果你只是使用了final
修饰引用,然而如果引用指向的是一个可变的对象,那么线程安全是无法保证的,对象的操作仍然需要使用同步!