不可变类String--阅读源码从jdk开始

不可变类

在日常java开发中,String是用得最多的类之一。对于jdk的String类的设计方式值得我们去思考和学习。

 

String类是一个不可变类,java平台的类库中包含的不可变类,如:String、基本类型的包装类(Integer等)、BigInteger和BigDecimal。为什么要设计不可变类呢?它们不容易出错,更加安全(比如作为HashMap的key),而且更加易于设计、实现和使用。

 

我们阅读String的源码在理解String的源码之前,先看下不可变类设计的5条原则:

1、不要提供任何可以修改对象状态的方法。

2、保证类不会被扩展(不能被继承)。

3、使所有的域都成为私有的。

4、使所有的域都是final的。

5、确保对任何可变组件的互斥访问。

 

根据这5点原则来看String类的源码(基于jdk1.8)。

成员变量

String的两个主要成员变量

private final char value[];//

private int hash;//首次调用String的hashcode方法后,会被缓存起来,防止后面再重新计算。

 

可以看到都是私有的满足“原则3”,String的主要成员变量 value(char类型的数组)是final的满足“原则4”。即:成员变量value在首次赋值之后,就不能被再次赋值(一般是在构造方法中赋值,或在静态实例化工程方法中赋值)。

 

有人会说成员变量hash不是final的,其实它只是对象首次调用hashcode方法后,用来缓存该对象的hash值,避免下次使用时重新计算(关于hashcode方法的重写规则可以参考这里)。看下String的hashcode实现:

    

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {//如果hash不为0,且String不为空,直接使用以前计算好的hash值。否则重新计算
            char val[] = value;
 
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;//只需要赋值一次
        }
        return h;
}

构造方法

 

前面已经说了,由于成员变量value是final的,所以String的构造方法的主要作用就是给value 赋值。

默认构造方法:

 

public String() {
        this.value = "".value;//让value指向””字符串的value的引用。
    }

参数为String的构造方法: 

 

public String(String original) {
        this.value = original.value; //本身就是不可变的
        this.hash = original.hash;
    }

参数为char型的数组的构造方法: 

 

public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);//copy一个新的数组,防止直接应用外部传入的可变对象
    }

public String(char value[], int offset, int count){
    //类似 省略
}

这里采用的是Arrays.copyOf来生成一个新的数组,为成员变量value赋值。为什么不能直接赋值呢(采用 this.value =value),因为参数char value[]是可变的,如果直接赋值,当参数数组发生变化时,就会影响到新生成的String对象,着就破坏的String的“不可变性”。这一点满足不可变类设计原则5。

 

包级私有的构造方法:

 

String(char[] value, boolean share) {//该构造方法会破坏“不可变型”,因此是包级私有的,我们无法使用
        // assert share : "unshared not supported"; 
        this.value = value;
    }

 如果这个构造方法是公有的,就破坏了不可变性。说白了这个构造方法是,给写jdk的大神使用的。

 

 

参数为StringBuffer的构造方法:

 

public String(StringBuffer buffer) {
        synchronized(buffer) {
            this.value = Arrays.copyOf(buffer.getValue(), buffer.length());//copy一个新数组对象
        }
    }

参数为StringBulder的构造方法

 

public String(StringBuilder builder) {
        this.value = Arrays.copyOf(builder.getValue(), builder.length());//copy一个新的数组对象
    }

还有其他几个参数为byte数组的构造方法,以及用得较少的基于ascii码和代码点构造方法。这里就不再一一列举。

 

小结:不可变类的构造方法设计,不要直接引用参数传入的“不可变”对象,而是采用copy的方式,重新生成一个新的对象。

 

修改String的方法

 

其实jdk没有暴露能直接修改String内部成员变的方法,这里所谓的修改String的方法 其实是通过生成一个新的String来实现,而不是真正意义生的修改。比如:

substring方法,实际上是生成一个新的String

 

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);//创建一个新的string
    }

concat字符串连接方法,通过copy生成一个新的string:

 

public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true); //创建一个新的string
    }

replace方法,其实是先创建一个新的char数组,在这个基础上进行替换,再根据这个新char数组生成一个新的String对象:

 

public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {//找到替换位置
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];//创建一个新的char数组
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];//把老字符串中的所有字符 copy到新的char数组
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;//在新的char数组中进行替换
                    i++;
                }
                return new String(buf, true);//使用新的char数组创建一个新的字符串
            }
        }
        return this;
    }

这些方法,都是设计不可变类 规则5的体现

 

总结

 

阅读完整个类的3000多行代码,没有任何其他可供修改的String内容的公有方法(public),因此String类的设计满足规则1。

 

最后看下String类的定义public final class String,该类是不能被继承的,因此String类的设计满足规则2。

按照不可变类的5个设计原则,再参考jdk的不可变类(String、Integer等)的实现方式,就能设计出自己的高效的不可变类。

 

 

 

猜你喜欢

转载自moon-walker.iteye.com/blog/2375522
今日推荐