不可变类
不可变类的意思是创建该类的实例后,该实例的实例变量是不可改变的。
Java提供的8个包装类和java.lang.String类都是不可变类,当创建它们的实例后,其实例的实例变量不可改变
创建自定义的不可变类,需满足以下规则:
-
使用private和final修饰符来修饰该类的成员变量,保证类不会被继承
-
仅为类的成员变量提供getter方法,不提供setter方法.为了避免通过getter对类的内部可变成员对象进行直接操作导致成员变量发生改变,我们要在getter方法中,返回克隆对象而不是对象本身,并返回对象的拷贝。
-
如果有必要,重写Object类的equals()方法和hashCode()方法。equals()方法根据关键成员变量来作为两个对象是否相 等的标准,除此之外,还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等。
-
提供带参数的构造器,用于根据传入参数初始化类里的成员变量,注意进行深拷贝。
public final class MyImmutableDemo {
private final int[] myArray;
public MyImmutableDemo(int[] array) {
this.myArray = array.clone(); //错误写法:this.myArray=array;
}
}
设计一个不可变类,尤其要注意其引用类型的成员变量。如果引用类型的成员变量的类是可变的,就必须采取必要的措施来保护该成员变量所引用的对象不会被修改,这样才能创建真正的不可变类。
String对象的不可变性
string对象在内存创建后就不可改变,不可变对象的创建一般满足以上5个原则,我们看看String代码是如何实现的。
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
....
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length); // deep copy操作
}
...
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
...
}
- String类被final修饰,不可继承
- string内部所有成员都设置为私有变量
- 不存在value的setter
- 并将value和offset设置为final。
- 当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量.
- 获取value时不是直接返回对象引用,而是返回对象的copy.
String对象的不可变性的优缺点
从上一节分析,String数据不可变类,那设置这样的特性有什么好处呢?我总结为以下几点:
- 字符串常量池的需要.
字符串常量池可以将一些字符常量放在常量池中重复使用,避免每次都重新创建相同的对象、节省存储空间。但如果字符串是可变的,此时相同内容的String还指向常量池的同一个内存空间,当某个变量改变了该内存的值时,其他遍历的值也会发生改变。所以不符合常量池设计的初衷。
2. 线程安全考虑。
同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
-
类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。
-
支持hash映射和缓存。
因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
需要注意的是,String对象并不是真的不可变,我们还是可以通过反射机制来修改不可变的对象的。代码例子如下:
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + 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个字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
输出:s = Hello World
s = Hello_World
部分转自http://www.cnblogs.com/jaylon/p/5721571.html