String源码解析
类定义
在定义之后不能被改变,字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享它们
1 | public final class String |
- java.io.Serializable
这个序列化接口没有任何方法和域,仅用于标识序列化的语意 - Comparable
这个接口只有一个compareTo(T o),用于对两个实例化对象比较大小 - CharSequence
这个接口是一个只读的字符序列,包括length(), charAt(int index), subSequence(int start, int end)这几个API接口,值得一提的是,StringBuffer和StringBuild也是实现了改接口
增补字符说明
这里说明一下,16 位unicode编码的所有 65536 个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是,Unicode 标准已扩展到包含多达 1,112,064 个字符。那些超出原来的16 位限制的字符被称作增补字符。Java的char类型是固定16bits的。代码点在U+0000 — U+FFFF之内到是可以用一个char完整的表示出一个字符。但代码点在U+FFFF之外的,一个char无论如何无法表示一个完整字符。这样用char类型来获取字符串中的那些代码点在U+FFFF之外的字符就会出现问题。
增补字符是代码点在 U+10000 至 U+10FFFF 范围之间的字符,也就是那些使用原始的 Unicode 的 16 位设计无法表示的字符。从 U+0000 至 U+FFFF 之间的字符集有时候被称为基本多语言面 (BMP UBasic Multilingual Plane )。因此,每一个 Unicode 字符要么属于 BMP,要么属于增补字符
主要变量
1 | /** The value is used for character storage. */ |
从上述变量可以看到,value[]是存储String内容的,即当使用String str = “abc”时,本质上”abc”存储在char类型的数组中。由于声明为final,所以一旦初始化后就不能进行更改
String s = “a”; s = “b” 但是,这并不是对s的修改,而是重新指向了新的字符串, 从这里我们也能知道,String其实就是用char[]实现的。
1 | /** Cache the hash code for the string */ |
而hash是String实例化的hashcode的一个缓存。因为String经常被用于比较,比如在HashMap中。如果每次进行比较都重新计算hashcode的值的话,那无疑是比较麻烦的,而保存一个hashcode的缓存无疑能优化这样的操作
1 | private static final ObjectStreamField[] serialPersistentFields = |
因为String实现了Serializable接口,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)
最后,这个CASE_INSENSITIVE_ORDER在下面内部类中会说到,其根本就是持有一个静态内部类,用于忽略大小写得比较两个字符串
内部类
String只有一个内部类:
1 | private static class CaseInsensitiveComparator |
疑问:String类中已经有一个compareTo的方法,为什么还要有CaseInsensitiveComparator的内部静态类 – 为了代码复用
观察这个内部类发现:
- 他和compareTo方法是有差别的,这个内部类的方法在比较时忽略大小写的,而且是一个单例的,可以简单得用它来比较两个String,因为String类提供一个变量:CASE_INSENSITIVE_ORDER 来持有这个内部类,这样当要比较两个String时可以通过这个变量来调用
- 可以看到String类中提供的compareToIgnoreCase方法其实就是调用这个内部类里面的方法实现的。这就是代码复用的一个例子
1
2
3public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}构造方法
String类中重载的构造方法比较多,此处列举常用的构造方法:基于字符串构造String
- 空串初始化一个新的字符串,value属性的值为””(空串)的字符数组
1
2
3public String() {
this.value = "".value;
} - 有参示例:String str=new String(“abc”);
1
2
3
4public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
主要步骤:
- 在Java堆上为String对象申请内存
- 尝试从常量池中获取”abc”字符串,如果常量池中不存在,则在常量池中新建”abc”字符串,并返回
- 调用构造方法,初始化String对象
- 字面常量和String对象的比较
1
2String s1 = "abc";
String s2 = new String("abc");
基于字符数组构造String
1 | public String(char value[]) { |
其实String就是使用字符数组(char[])实现的。所以我们可以使用一个字符数组来创建一个String,那么这里值得注意的是,当我们使用字符数组创建String的时候,会用到Arrays.copyOf方法和Arrays.copyOfRange方法。这两个方法是将原有的字符数组中的内容逐一的复制到String中的字符数组中。同样,我们也可以用一个String类型的对象来初始化一个String。这里将直接将源String中的value和hash两个属性直接赋值给目标String。因为String一旦定义之后是不可以改变的,所以也就不用担心改变源String的值会影响到目标String的值
当然,在使用字符数组来创建一个新的String对象的时候,不仅可以使用整个字符数组,也可以使用字符数组的一部分,只要多传入两个参数int offset和int count就可以了
基于字节数组构造String
在Java中,String实例中保存有一个char[]字符数组,char[]字符数组是以unicode码来存储的,String 和 char 为内存形式,byte是网络传输或存储的序列化形式。所以在很多传输和存储的过程中需要将byte[]数组和String进行相互转化。所以,String提供了一系列重载的构造方法来将一个字符数组转化成String,提到byte[]和String之间的相互转换就不得不关注编码问题。String(byte[] bytes, Charset charset)是指通过charset来解码指定的byte数组,将其解码成unicode的char[]数组,构造成新的String
这里的bytes字节流是使用charset进行编码的,想要将他转换成unicode的char[]数组,而又保证不出现乱码,那就要指定其解码方式
同样使用字节数组来构造String也有很多种形式,按照是否指定解码方式分的话可以分为两种:
1 | String(byte bytes[]) String(byte bytes[], int offset, int length) |
如果我们在使用byte[]构造String的时候,使用的是下面这四种构造方法(带有charsetName或者charset参数)的一种的话,那么就会使用StringCoding.decode方法进行解码,使用的解码的字符集就是我们指定的charsetName或者charset。 我们在使用byte[]构造String的时候,如果没有指明解码使用的字符集的话,那么StringCoding的decode方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作。主要体现代码如下:
1 | static char[] decode(byte[] ba, int off, int len) { |
基于StringBuffer与StringBuilder构造String
作为String的两个“兄弟”,StringBuffer和StringBuider也可以被当做构造String的参数。
1 | public String(StringBuffer buffer) { |
当然,这两个构造方法是很少用到的,至少我从来没有使用过,因为当我们有了StringBuffer或者StringBuilfer对象之后可以直接使用他们的toString方法来得到String。关于效率问题,Java的官方文档有提到说使用StringBuilder的toString方法会更快一些,原因是StringBuffer的toString方法是synchronized的,在牺牲了效率的情况下保证了线程安全
一个特殊的保护类型的构造方法
String除了提供了很多公有的供程序员使用的构造方法以外,还提供了一个保护类型的构造方法(Java 7),我们看一下他是怎么样的:
1 | String(char[] value, boolean share) { |
从代码中我们可以看出,该方法和 String(char[] value)有两点区别,第一个,该方法多了一个参数: boolean share,其实这个参数在方法体中根本没被使用,也给了注释,目前不支持使用false,只使用true。那么可以断定,加入这个share的只是为了区分于String(char[] value)方法,不加这个参数就没办法定义这个函数,只有参数不能才能进行重载。那么,第二个区别就是具体的方法实现不同。我们前面提到过,String(char[] value)方法在创建String的时候会用到 会用到Arrays的copyOf方法将value中的内容逐一复制到String当中,而这个String(char[] value, boolean share)方法则是直接将value的引用赋值给String的value。那么也就是说,这个方法构造出来的String和参数传过来的char[] value共享同一个数组。 那么,为什么Java会提供这样一个方法呢? 首先,我们分析一下使用该构造函数的好处:
首先,性能好,这个很简单,一个是直接给数组赋值(相当于直接将String的value的指针指向char[]数组),一个是逐一拷贝。当然是直接赋值快了。
其次,共享内部数组节约内存
但是,该方法之所以设置为protected,是因为一旦该方法设置为公有,在外面可以访问的话,那就破坏了字符串的不可变性。例如如下YY情形:
1 | char[] arr = new char[] {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; |
如果构造方法没有对arr进行拷贝,那么其他人就可以在字符串外部修改该数组,由于它们引用的是同一个数组,因此对arr的修改就相当于修改了字符串
所以,从安全性角度考虑,他也是安全的。对于调用他的方法来说,由于无论是原字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全
在Java 7 之有很多String里面的方法都使用这种“性能好的、节约内存的、安全”的构造函数。比如:substring、replace、concat、valueOf等方法(实际上他们使用的是public String(char[], int, int)方法,原理和本方法相同,已经被本方法取代)
在Java 7中,substring已经不再使用这种“优秀”的方法了,为什么呢虽然这种方法有很多优点,但是他有一个致命的缺点,对于sun公司的程序员来说是一个零容忍的bug,那就是他很有可能造成内存泄露。 看一个例子,假设一个方法从某个地方(文件、数据库或网络)取得了一个很长的字符串,然后对其进行解析并提取其中的一小段内容,这种情况经常发生在网页抓取或进行日志分析的时候。下面是示例代码
1 | String aLongString = "...a very long string..."; |
在这里aLongString只是临时的,真正有用的是aPart,其长度只有20个字符,但是它的内部数组却是从aLongString那里共享的,因此虽然aLongString本身可以被回收,但它的内部数组却不能(如下图)。这就导致了内存泄漏。如果一个程序中这种情况经常发生有可能会导致严重的后果,如内存溢出,或性能下降
新的实现虽然损失了性能,而且浪费了一些存储空间,但却保证了字符串的内部数组可以和字符串对象一起被回收,从而防止发生内存泄漏,因此新的substring比原来的更健壮。
虽然substring方法已经为了其鲁莽性放弃使用这种share数组的方法,但是这种share数组的方法还是有一些其他方法在使用的,这是为什么呢?首先呢,这种方式构造对应有很多好处,其次呢,其他的方法不会将数组长度变短,也就不会有前面说的那种内存泄露的情况(内存泄露是指不用的内存没有办法被释放,比如说concat方法和replace方法,他们不会导致元数组中有大量空间不被使用,因为他们一个是拼接字符串,一个是替换字符串内容,不会将字符数组的长度变得很短!
主要方法
equals方法
equals()是string能成为广泛用于Map[key,value]中key的关键所在
1 | public boolean equals(Object anObject) { |
String重写了父类Object的equals方法:
- 先判断地址是否相等(地址相等情况下直接返回true)
- 在判断是否是String类型,不是则返回false
- 是String先判断长度
- 再比较值,把值赋给char数组,遍历两个char数组比较
该方法首先判断this == anObject ?,也就是说判断要比较的对象和当前对象是不是同一个对象,如果是直接返回true,如不是再继续比较,然后在判断anObject是不是String类型的,如果不是,直接返回false,如果是再继续比较,到了能终于比较字符数组的时候,他还是先比较了两个数组的长度,不一样直接返回false,一样再逐一比较值。 虽然代码写的内容比较多,但是可以很大程度上提高比较的效率。值得学习~~!!!
此外除equals()外,还有只比较内容的contentEquals();
contentEquals只比较内容
1 | public boolean contentEquals(CharSequence cs) { |
这个主要是用来比较String和StringBuffer或者StringBuild的内容是否一样。可以看到传入参数是CharSequence ,这也说明了StringBuffer和StringBuild同样是实现了CharSequence。源码中先判断参数是从哪一个类实例化来的,再根据不同的情况采用不同的方案,不过其实大体都是采用上面那个for循环的方式来进行判断两字符串是否内容相同
contentEquals有两个重载,StringBuffer需要考虑线程安全问题,再加锁之后调用contentEquals((CharSequence) sb)方法。contentEquals((CharSequence) sb)则分两种情况,一种是cs instanceof AbstractStringBuilder,另外一种是参数是String类型。具体比较方式几乎和equals方法类似,先做“宏观”比较,在做“微观”比较
equalsIgnoreCase忽略大小写
1 | 使用一个三目运算符和&&操作代替了多个if语句 |
hashcode方法
1 | public int hashCode() { |
hashCode的实现其实就是使用数学公式:
1 | s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] |
s[i]是string的第i个字符,n是String的长度。那为什么这里用31,而不是其它数呢? 计算机的乘法涉及到移位计算。当一个数乘以2时,就直接拿该数左移一位即可!选择31原因是因为31是一个素数!
在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址,所谓“冲突”。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就更长,从而降低了查询效率!所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。
31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化,使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits!
在java乘法中如果数字相乘过大会导致溢出的问题,从而导致数据的丢失.而31则是素数(质数)而且不是很长的数字,最终它被选择为相乘的系数的原因不过与此!
在Java中,整型数是32位的,也就是说最多有2^32= 4294967296个整数,将任意一个字符串,经过hashCode计算之后,得到的整数应该在这4294967296数之中。那么,最多有 4294967297个不同的字符串作hashCode之后,肯定有两个结果是一样的, hashCode可以保证相同的字符串的hash值肯定相同,但是,hash值相同并不一定是value值就相同
实现字符序列charSequence的方法
- length() – 获取字符串长度
- charAt(int index) – 返回字符数组的index下标的元素
Character类的静态方法实现
- isEmpty() – 判断字符串是否为空
- codePointAt(int index) – 返回到指定索引的代码点
- codePointBefore(int index) – 返回到指定索引的前一代码点
- codePointCount(int beginIndex, int endIndex) – 返回指定起始索引到结束索引之间的字符个数
- offsetByCodePoints(int index, int codePointOffset) – 返回指定索引加上codepointOffset后得到的索引值
说明:这几个函数用得比较少,并且可以看到其本质上都是用Character这个类的一些静态方法来实现。这些功能在平常并不经常使用,个人认为,如果使用的话那应该是在对未知字符串进行处理,且重点在异常处理上
数组复制
将字符串复制到dst数组中,复制到dst数组中的起始位置可以指定,需要注意的是该方法并没有检测复制到dst数组后是否越界
- getChars(char dst[], int dstBegin)
- getChars(int srcBegin, int srcEnd, char dst[], int dstBegin)
这两个重载方法本质上都是调用System.arraycopy()这个函数,包括在jdk很多其他源码中都是这样,比如ThreadPoolExcuter,看似有很多个重载,其实本质上都是调用同样的一个函数,只是会给你不同的默认初始值。
获取当前字符串的二进制getBytes
- getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin)
- byte[] getBytes(String charsetName)
- byte[] getBytes()
在创建String的时候,可以使用byte[]数组,将一个字节数组转换成字符串,同样,我们可以将一个字符串转换成字节数组,那么String提供了很多重载的getBytes方法。但是,值得注意的是,在使用这些方法的时候一定要注意编码问题。比如:
1 | String s = "你好,世界!"; |
这段代码在不同的平台上运行得到结果是不一样的。由于我们没有指定编码方式,所以在该方法对字符串进行编码的时候就会使用系统的默认编码方式,比如在中文操作系统中可能会使用GBK或者GB2312进行编码,在英文操作系统中有可能使用iso-8859-1进行编码。这样写出来的代码就和机器环境有很强的关联性了,所以,为了避免不必要的麻烦,我们要指定编码方式。如使用以下方式:
1 | String s = "你好,世界!"; |
compareTo方法
1 | public int compareTo(String anotherString) { |
这就是String对comparable接口中方法实现。其核心就说while循环,通过从第一个开始比较每一个字符,当遇到第一个较小字符时,判定该字符串小;但在较小长度的字符数组每个字符都和另一个字符串的每个字符相等,那么字符串长度较大的大
regionMatches区域匹配是否相等
1 | public boolean regionMatches(int toffset, String other, int ooffset, int len) { |
比较该字符串和其他一个字符串分别从指定地点开始的n个字符是否相等,看代码可知道其原理还是通过一个while去循环对应区域进行判断,但在比较之前会做判定,判断给定参数是否越界
判断起始字符串startsWith和endsWith
1 | public boolean startsWith(String prefix, int toffset) { |
判断当前字符串是否以某一段其他字符串开始的,和其他字符串比较方法一样,其实就是通过一个while来循环比较
indexOf和contains
1 | public int indexOf(int ch) { |
可以看到这里在if中有一句ch < Character.MIN_SUPPLEMENTARY_CODE_POINT而在Character中看到public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000;这表明在java中char存储的值通常都是比ox010000小的,就是BMP类型的字符。而当比这个值大的时候,就是增补字符了,那么会调用Character先判断是否是有效的字符,再进一步处理
1 | public int lastIndexOf(int ch, int fromIndex) { |
lastIndexOf和indexOf基本一致,只是顺序反过来,外带增补字符的处理
1 | static int indexOf(char[] source, int sourceOffset, int sourceCount, |
这个是上面indexOf的一个重载,主要是实现找到某个子串在当前字符串的起始位置,若没找到,则返回-1。
大致说下这里的实现思路:先是进行一系列的初始判定,比如子串长度不能大于当前字符串。然后在当前字符串中找到子串的第一个字符的位置 i ,从这个位置开始,和子串每一个字符比较。若完全匹配,则返回结果,如果在这个过程中,某个字符不匹配,则从 i+1 的位置开始继续寻找子串第一个字符的位置,后继续比较
contains则是能用indexOf查找出,则就存在
substring–截取子串
1 | public String substring(int beginIndex) { |
这个方法可以返回字符串中一个子串,看最后一行可以发现,其实就是指定头尾,然后构造一个新的字符串
前面我们介绍过,java 7 中的substring方法使用String(value, beginIndex, subLen)方法创建一个新的String并返回,这个方法会将原来的char[]中的值逐一复制到新的String中,两个数组并不是共享的,虽然这样做损失一些性能,但是有效地避免了内存泄露
concat–拼接字符串
1 | public String concat(String str) { |
concat的作用是将str拼接到当前字符串后面,通过代码也可以看出其实就是建一个新的字符串
replace系列
1 | public String replace(char oldChar, char newChar) { |
替换操作,主要是将原来字符串中的oldChar全部替换成newChar。看这里实现,主要是先找到第一个所要替换的字符串的位置 i ,将i之前的字符直接复制到一个新char数组。然后从 i 开始再对每一个字符进行判断是不是所要替换的字符
1)replace的参数是char和CharSequence,即可以支持字符的替换,也支持字符串的替换
2)replaceAll和replaceFirst的参数是regex,即基于规则表达式的替换,比如,可以通过replaceAll(“\d”, “”)把一个字符串所有的数字字符都换成星号;
*相同点是都是全部替换,即把源字符串中的某一字符或字符串全部换成指定的字符或字符串, **如果只想替换第一次出现的,可以使用 replaceFirst(),这个方法也是基于规则表达式的替换,但与replaceAll()不同的是,只替换第一次出现的字符串; 另外,如果replaceAll()和replaceFirst()所用的参数据不是基于规则表达式的,则与replace()替换字符串的效果是一样的,即这两者也支持字符串的操作;
1 | public boolean matches(String regex) { |
这几个方法都是使用了正则的方式来进行处理的。包括最后一个虽然参数不用提供正则规则,但内部其实也是使用了Pattern类的正则操作
copyValueOf 和 valueOf
String的底层是由char[]实现的:通过一个char[]类型的value属性!早期的String构造器的实现呢,不会拷贝数组的,直接将参数的char[]数组作为String的value属性。然后test[0] = ‘A’;将导致字符串的变化。为了避免这个问题,提供了copyValueOf方法,每次都拷贝成新的字符数组来构造新的String对象。但是现在的String对象,在构造器中就通过拷贝新数组实现了,所以这两个方面在本质上已经没区别了
spilt
这个方法看起来比较复杂,但其实我们一般都不会用到那一大串的内容,一般我们用到最后那一句return Pattern.compile(regex).split(this, limit); 即同样是使用Pattern的正则方式去解析并拆分成字符串数组。
那么进到那些复杂的代码里面需要什么条件呢,看那个if:
- 如果regex只有一位,且不为列出的特殊字符;
- 如regex有两位,第一位为转义字符且第二位不是数字或字母,“|”表示或,即只要ch小于0或者大于9任一成立,小于a或者大于z任一成立,小于A或大于Z任一成立
- 第三个是不属于utf-16之间的字符
其中的关系为( (1 || 2) && 3 ),光看第三点就知道这是为了应对特殊情况的。其实也就是使用一个ArrayList
join
1 | public static String join(CharSequence delimiter, CharSequence... elements) { |
trim
1 | public String trim() { |
这个函数平时用的应该比较多,删除字符串前后的空格,原理是通过找出前后第一个不是空格的字符串,返回原字符串的该子串。
intern方法
1 | public native String intern(); |
注:方法注释会有写到,意思就是调用方法时,如果常量池有当前String的值,就返回这个值,没有就加进去,返回这个值的引用
示例:
1 | String str1="a"; |
重点:两个字符串常量或者字面量相加,不会new新的字符串,其他相加则是新值,(如 String str5=str1+”b”;)。因为在jvm翻译为二进制代码时,会自动优化,把两个值后边的结果先合并,再保存为一个常量
“+”字符的重载
“+”是java中唯一的重载运算符,底层使用的是StringBuilder类的append方法和toString方法
1 | public static void main(String[] args) { |
Java语言提供了对字符串连接运算符的特别支持(+),该符号也可用于将其他类型转换成字符串。字符串的连接实际上是通过StringBuffer或者StringBuilder的append()方法来实现的,字符串的转换通过toString方法实现,该方法由 Object 类定义,并可被 Java 中的所有类继承。除非另有说明,传递一个空参数在这类构造函数或方法会导致NullPointerException异常被抛出。
String表示一个字符串通过UTF-16(unicode)格式,补充字符通过代理对(参见Character类的 Unicode Character Representations 获取更多的信息)表示。索引值参考字符编码单元,所以补充字符在String中占两个位置。
String.valueOf和Integer.toString的区别
接下来我们看以下这段代码,我们有三种方式将一个int类型的变量变成呢过String类型,那么他们有什么区别?
1 | int i = 5; |
1、第三行和第四行没有任何区别,因为String.valueOf(i)也是调用Integer.toString(i)来实现的。
2、第二行代码其实是String i1 = (new StringBuilder()).append(i).toString();,首先创建一个StringBuilder对象,然后再调用append方法,再调用toString方法。