不可变对象(immutable objects):一旦对象被创建,它们的状态就不能被改变(包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变),每次对他们的改变都是产生了新的对象。JDK本身就自带了immutable类,比如String,Integer以及其他包装类。
遵循原则:
1. 类添加final修饰符,保证类不被继承。
如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。
2. 保证所有成员变量必须私有,并且加上final修饰
通过这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第4点弥补这个不足。
3. 不提供改变成员变量的方法,包括setter
避免通过其他接口改变成员变量的值,破坏不可变特性。
4.通过构造器初始化所有成员,进行深拷贝(deep copy)
如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。例如:
public final class ImmutableDemo { private final int[] myArray; public ImmutableDemo(int[] array) { // this.myArray = array; wrong this.myArray = array.clone(); // 采用深度copy来创建一个新的对象保证不会通过传入的array来修改myArray的数组元素 } }
5. 在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝
这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。
优点:
- Immutable对象是线程安全的,可以不用被synchronize就在并发环境中共享
- Immutable对象简化了程序开发,因为它无需使用额外的锁机制就可以在线程间共享
- Immutable对象提高了程序的性能,因为它减少了synchroinzed的使用
- Immutable对象是可以被重复使用的,你可以将它们缓存起来重复使用,就像字符串字面量和整型数字一样。你可以使用静态工厂方法来提供类似于valueOf()这样的方法,它可以从缓存中返回一个已经存在的Immutable对象,而不是重新创建一个。
缺点:
由于不可变对象不能修改重用,会制造大量垃圾,字符串就是一个典型的例子,但合理的使用immutable对象会创造很大的价值。
String不可变类
Java的String类是不可变对象(immutable object),即创建后不可以改变的对象。一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。特别要注意的是,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。如果你需要频繁地修改一个字符串对象,可以使用StringBuffer或者StringBuilder,否则将会浪费大量时间进行垃圾回收,因为每次都会创建一个新的字符串。
String的不可变特性主要为了满足常量池、线程安全、类加载的需求。
String s = "abcd"; // 可以修改变量s的引用,因为s不是final类型的变量(初始化之后不能更改),但是s指向的堆内存中的对象是不能更改的,因为它的类型是不可变的String类 String s2 = s; // s2保存了和s相同的引用值,他们指向同一个对象。 s = s.concat("ef"); // 重新创建一个string对象的引用 s.toUpperCase(); // 此处并没有改变“abcd“的值,而是创建了一个新的String类“ABCD”,然后将新的实例的指向变量s
相对于可变对象,String作为不可变对象有很多优势:
1) 不可变对象可以提高String Pool的效率和安全性。如果一个对象是不可变的,那么拷贝该对象的内容时,只需复制地址而不用复制它本身,需要很小的内存效率也很高。
2) 不可变对象对于多线程是安全的,因为在多线程的情况下,一个可变对象的值(堆中的实例)很可能被其他进程改变,这样会造成不可预期的结果,而使用不可变对象就可以避免这种情况。
3) String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,可能被黑客们改变字符串变量指向的对象的值,从而引起各种安全隐患。
4) 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,如果变量改变了它的对象的内容,那么其它指向这个对象的变量的值也会一起改变。
5) 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
class User { String name; public User(String name) { this.name = name; } public void setName(String name) { this.name = name; } public String toString() { return name; } } class UserWithHashCode extends User{ public UserWithHashCode(String name) { super(name); } public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; User other = (User) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } } public class Test { public static void main(String[] args) { // HashMap put和get方法都用key的hashcode去判断是否为同一个对象,自定义类默认hashcode为对象的地址。 // String类的hashcode是根据字符串的值来计算的,所以值相同的字符串hashcode也一样。 Map<String, Integer> map1 = new HashMap<String, Integer>(); String str = "key1"; map1.put(str, 1); print(map1, str); // Map: {key1=1}, value of key:1 str = "key2"; print(map1, str); // Map: {key1=1}, value of key:null 变量s指向新的对象 str = new String("key1"); print(map1, str); // Map: {key1=1}, value of key:1 变量s指向原来的对象,map里的key不会改变。 Map<User, Integer> map2 = new HashMap<User, Integer>(); User user = new User("Mike"); map2.put(user, 1); print(map2, user); // Map: {Mike=1}, value of key:1 user.setName("Sara"); print(map2, user); // Map: {Sara=1}, value of key:1 user指向的对象保持不变 user.setName("Mike"); print(map2, new User("Mike")); // Map: {Mike=1}, value of key:null 此处使用新的对象,因此取不到值 Map<UserWithHashCode, Integer> map3 = new HashMap<UserWithHashCode, Integer>(); map3.put(new UserWithHashCode("lily"), 1); print(map3, new UserWithHashCode("Mike")); // Map: {lily=1}, value of key:null print(map3, new UserWithHashCode("lily")); // Map: {lily=1}, value of key:1 重写hashCode和equals方法, 使得name相同的对象相等 } }
Stringl类的源码:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0
String的成员变量是private final的,即初始化之后不可改变。在这几个成员中value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,通常我们无法访问到这个私有成员value的引用,更不能更改其数组元素。但是反射可以获取String对象中的value属性,进而改变数组结构。
//创建字符串"Hello World", 并赋给引用s String s = "Hello World"; //获取String类中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); //改变value属性的访问权限 valueFieldOfString.setAccessible(true); //获取s对象上的value属性的值 char[] value = (char[]) valueFieldOfString.get(s); //改变value所引用的数组中的第5个字符: Hello_World value[5] = '_';
在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化。也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。
参考:
JAVA不可变类(immutable)机制与String的不可变性