欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

不可变类String--阅读源码从jdk开始 博客分类: java基础 java String不可变类 

程序员文章站 2024-03-02 11:04:16
...

不可变类

 

在日常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等)的实现方式,就能设计出自己的高效的不可变类。