不可变类String--阅读源码从jdk开始 博客分类: java基础 java String不可变类
不可变类
在日常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等)的实现方式,就能设计出自己的高效的不可变类。
上一篇: Android自定义View实现开关按钮