java中的不可变类与String的不可变机制

不可变类

不可变类的意思是创建该类的实例后,该实例的实例变量是不可改变的。

Java提供的8个包装类和java.lang.String类都是不可变类,当创建它们的实例后,其实例的实例变量不可改变

创建自定义的不可变类,需满足以下规则:
  1. 使用private和final修饰符来修饰该类的成员变量,保证类不会被继承

  2. 仅为类的成员变量提供getter方法,不提供setter方法.为了避免通过getter对类的内部可变成员对象进行直接操作导致成员变量发生改变,我们要在getter方法中,返回克隆对象而不是对象本身,并返回对象的拷贝。

  3. 如果有必要,重写Object类的equals()方法和hashCode()方法。equals()方法根据关键成员变量来作为两个对象是否相 等的标准,除此之外,还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等。

  4. 提供带参数的构造器,用于根据传入参数初始化类里的成员变量,注意进行深拷贝。

    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;
    }
    ...
}
  1. String类被final修饰,不可继承
  2. string内部所有成员都设置为私有变量
  3. 不存在value的setter
  4. 并将value和offset设置为final。
  5. 当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量.
  6. 获取value时不是直接返回对象引用,而是返回对象的copy.

String对象的不可变性的优缺点

从上一节分析,String数据不可变类,那设置这样的特性有什么好处呢?我总结为以下几点:

  1. 字符串常量池的需要.
    字符串常量池可以将一些字符常量放在常量池中重复使用,避免每次都重新创建相同的对象、节省存储空间。但如果字符串是可变的,此时相同内容的String还指向常量池的同一个内存空间,当某个变量改变了该内存的值时,其他遍历的值也会发生改变。所以不符合常量池设计的初衷。

2. 线程安全考虑。
同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

扫描二维码关注公众号,回复: 4999791 查看本文章
  1. 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。

  2. 支持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

猜你喜欢

转载自blog.csdn.net/mulinsen77/article/details/86556425