引言
在之前,我知道的关于String,StringBuffer,StringBuilder的知识点大概如下
- String是不可变的(修改String时,不会在原有的内存地址修改,而是重新指向一个新对象),String用final修饰,不可继承,String本质上是个final的char[]数组,所以char[]数组的内存地址不会被修改,而且String 也没有对外暴露修改char[]数组的方法.不可变性可以保证线程安全以及字符串串常量池的实现.频繁的增删操作是不建议使用String的.
- StringBuffer是线程安全的,多线程建议使用这个.
- StringBuilder是非线程安全的,单线程使用这个更快.
对于上面这些结论,我也不知道从哪里来的,,,,感觉好像是前辈的经验吧,,,好了,废话不多说,直接上代码吧.
String源码分析
看下继承结构源码:
1 | public final class String |
可以看到String是final的,不允许继承.里面用来存储value的是一个final数组,也是不允许修改的.String有很多方法,下面就String类常用方法进行分析.
构造方法
1 | public String() { |
可以看到默认的构造器是构建的空字符串,其实所有的构造器就是给value数组赋初值.
字符串长度
返回该字符串的长度,这太简单了,就是返回value数组的长度.
1 | public int length() { |
字符串某一位置字符
返回字符串中指定位置的字符;
1 | public char charAt(int index) { |
提取子串
用String类的substring方法可以提取字符串中的子串,简单分析一下substring(int beginIndex, int endIndex)吧,该方法从beginIndex位置起,从当前字符串中取出到endIndex-1位置的字符作为一个 新的字符串(重新new了一个String) 返回.
方法内部是将数组进行部分复制完成的,所以该方法不会对原有的数组进行更改.
1 | public String substring(int beginIndex, int endIndex) { |
字符串比较
compareTo(String anotherString)
该方法是对字符串内容按字典顺序进行大小比较,通过返回的整数值指明当前字符串与参数字符串的大小关系.若当前对象比参数大则返回正整数,反之返回负整数,相等返回0.
主要是挨个字符进行比较
1 | public int compareTo(String anotherString) { |
compareToIgnore(String anotherString)
与compareTo()方法相似,但忽略大小写.
实现:从下面的源码可以看出,最终实现是通过一个内部类CaseInsensitiveComparator,它实现了Comparator和Serializable接口,并实现了compare()方法,里面的实现方法和上面的compareTo()方法差不多,只不过忽略大小写.
1 | public int compareToIgnoreCase(String str) { |
equals(Object anotherObject)
比较当前字符串和参数字符串,在两个字符串相等的时候返回true,否则返回false.
大体实现思路:
- 先判断引用是否相同
- 再判断该Object对象是否是String的实例
- 再判断两个字符串的长度是否一致
- 最后挨个字符进行比较
1 | public boolean equals(Object anObject) { |
equalsIgnoreCase(String anotherString)
与equals方法相似,但忽略大小写.但是这里要稍微复杂一点,因为牵连到另一个方法regionMatches(),没关系,下面跟着我一起慢慢分析.
在equalsIgnoreCase()方法里面首先是校验引用值是否一致,再判断否为空,紧接着判断长度是否一致,最后通过regionMatches()方法测试两个字符串每个字符是否相等(忽略大小写).
在regionMatches()方法中其实还是比较简单的,就是逐字符进行比较,当需要进行忽略大小写时,如果遇到不相等的2字符,先统一转成大写进行比较,如果相同则继续比较下一个,不相同则转成小写再判断是否一致.
1 | public boolean equalsIgnoreCase(String anotherString) { |
字符串连接
将指定字符串联到此字符串的结尾,效果等价于”+”.
实现思路:构建一个新数组,先将原来的数组复制进新数组里面,再将需要连接的字符串复制进新数组里面(存放到后面).
1 | public String concat(String str) { |
字符串中单个字符查找
indexOf(int ch/String str)
返回指定字符在此字符串中第一次出现处的索引,在该对象表示的字符序列中第一次出现该字符的索引,如果未出现该字符,则返回 -1。
其实该方法最后是调用的
indexOf(ch/str, 0);
该方法放到下面进行分析.indexOf(int ch/String str, int fromIndex)
该方法与第一种类似,区别在于该方法从fromIndex位置向后查找.
先分析indexOf(int ch, int fromIndex),该方法是查找ch在fromIndex索引之后第一次出现的索引.主要就是逐个字符进行比较,相同则返回索引.如果未找到则返回-1.
1 | public int indexOf(int ch, int fromIndex) { |
再来分析indexOf(String str, int fromIndex),该方法功能是从指定的索引处开始,返回第一次出现的指定子字符串在此字符串中的索引.
大体思路:
有点类似于字符串查找子串,先在当前字符串中找到与目标字符串的第一个字符相同的索引处
再从此索引出发循环遍历目标字符串后面的字符.
如果全部相同,则返回下标;如果不全部相同,则重复步骤1
文字可能描述不清楚,上图片
我们要在beautifulauful中查找ful,那么步骤是首先找到f,再匹配后面的
ul
部分,找到则返回索引,未找到则继续查找.
1 | public int indexOf(String str, int fromIndex) { |
lastIndexOf(int ch/String str)
该方法与第一种类似,区别在于该方法从字符串的末尾位置向前查找.
实现方法也与第一种是类似的,只不过是从后往前查找.
1 | public int lastIndexOf(int ch) { |
lastIndexOf(int ch/String str, int fromIndex)
该方法与第二种方法类似,区别在于该方法从fromIndex位置向前查找.
实现思路:这里要稍微复杂一点,相当于从后往前查找指定子串.上图吧
图画的有点丑,哈哈. 假设我们需要在
StringBuffer
中查找ABuff
中的子串Buff
,因为Buff
的长度是4,所以我们最大的索引可能值是图中的rightIndex.然后我们就开始在source数组中匹配目标字符串的最后一个字符,匹配到后,再逐个字符进行比较剩余的字符,如果全部匹配,则返回索引.未全部匹配,则再次在source数组中寻找与目标字符串最后一个字符相等的字符,然后找到后继续匹配除去最后一个字符剩余的字符串. 唉~叙述的不是特别清晰,看代码吧,代码比我说的清晰..
1 | public int lastIndexOf(String str, int fromIndex) { |
字符串中字符的替换
replace(char oldChar, char newChar)
功能:用字符newChar替换当前字符串中所有的oldChar字符,并返回一个新的字符串.
大体思路:- 首先判断oldChar与newChar是否相同,相同的话就没必要进行后面的操作了
- 从最前面开始匹配与oldChar相匹配的字符,记录索引为i
- 如果上面的i是正常范围内(小于len),新建一个数组,长度为len(原来的字符串的长度),将i索引前面的字符逐一复制进新数组里面,然后循环
i<=x<len
的字符,将字符逐一复制进新数组,但是这次的复制有规则,即如果那个字符与oldChar相同那么新数组对应索引处就放newChar. - 最后通过新建的数组new一个String对象返回
思考:一开始我觉得第二步好像没什么必要性,没有第二步其实也能实现.但是,仔细想想,假设原字符串没有查找到与oldChar匹配的字符,那么我们就可以规避去新建一个数组,从而节约了不必要的开销.可以,很棒,我们就是要追求极致的性能,减少浪费资源.
小细节:源码中有一个小细节,注释中有一句
avoid getfield opcode
,意思是避免getfield操作码?
感觉那句代码就是拷贝了一个引用副本啊,有什么高大上的作用?查阅文章https://blog.csdn.net/gaopu12345/article/details/52084218 后发现答案:在一个方法中需要大量引用实例域变量的时候,使用方法中的局部变量代替引用可以减少getfield操作的次数,提高性能。
1 | public String replace(char oldChar, char newChar) { |
其他类方法
trim()
功能:截去字符串两端的空格,但对于中间的空格不处理
大体实现:记录前面有st个空格,最后有多少个空格,那么长度就减去多少个空格,最后根据上面的这2个数据去切割字符串.
1 | public String trim() { |
startsWith(String prefix)或endsWith(String suffix)
功能:用来比较当前字符串的起始字符或子字符串prefix和终止字符或子字符串suffix是否和当前字符串相同,重载方法中同时还可以指定比较的开始位置offset.
思路:比较简单,就直接看代码了,有详细注释.
1 | public boolean startsWith(String prefix) { |
contains(String str)
功能:判断参数s是否被包含在字符串中,并返回一个布尔类型的值.
思路:其实就是利用已经实现好的indexOf()去查找是否包含.源码中对于已实现的东西利用率还是非常高的.我们要多学习.
1 | public boolean contains(CharSequence s) { |
基本类型转换为字符串类型
这部分代码一看就懂,都是一句代码解决.
1 | public static String valueOf(Object obj) { |
注意事项
最后注意一下:Android 6.0(23) 源码中,String类的实现被替换了,具体调用的时候,会调用一个StringFactory来生成一个String.
来看下Android源码中String,,我擦,,这…..直接抛错误UnsupportedOperationException
,可能是因为Oracle告Google的原因吧..
1 | public String() { |
我们平时开发APP时都是使用的java.lang包下面的String,上面的问题一般不会遇到,但是作为Android开发者还是要了解一下.
AbstractStringBuilder源码分析
先看看类StringBuffer和StringBuilder的继承结构
可以看到StringBuffer和StringBuilder都是继承了AbstractStringBuilder.所以这里先分析一下AbstractStringBuilder.
在这基类里面真实的保存了StringBuffer和StringBuilder操作的实际数据内容,数据内容其实是一个char[] value;
数组,在其构造方法中其实就是初始化该字符数组.
1 | char[] value; |
扩容
既然数据内容(上面的value数组)是在AbstractStringBuilder里面的,那么很多操作我觉得应该也是在父类里面,比如扩容,下面我们看看源码
1 | public void ensureCapacity(int minimumCapacity) { |
可以看到这里的扩容方式是 = 以前的大小*2+2,其他的细节方法中已给出详细注释.
追加
举一个比较有代表性的添加,详细注释在代码中
1 | /** |
增加
这里的大体思想是和以前大一的时候用C语言在数组中插入数据是一样的.
这里假设需要插入的字符串s,插入在目标字符串desOffset处,插入的长度是len.首先将需要插入处的desOffset~desOffset+len往后挪,挪到desOffset+len处,然后在desOffset处插入目标字符串.
大体思想就是这样,是不是觉得很熟悉?? ヽ( ̄▽ ̄)ノ
下面这个方法是上面思路的具体实现,详细的逻辑分析已经放到代码注释中.
1 | //插入字符串,从dstOffset索引处开始插入,插入内容为s中的[start,end]字符串 |
删除
源码里面的删除操作实际上是复制,比如下面这个方法删除start到end之间的字符,实际是将以end开始的字符复制到start处,并且将数组的长度记录count减去len个
1 | //删除从start到end索引区间( [start,end)前闭后开区间 )内内容 |
切割
我擦,,,,原来StringBuffer的切割效率并不高嘛,其实就是new了一个String….
1 | public String substring(int start, int end) { |
改
改其实就是对其替换,而在源码中替换最终的实现其实是复制(还是复制..( ̄▽ ̄)~*
).
大体思路: 假设需要将字符串str替换value数组中的start-end中,这时只需将end后面的字符往后移动,在中间腾出一个坑,用于存放需要替换的str字符串.最后将str放到value数组中start索引处.
1 | public AbstractStringBuilder replace(int start, int end, String str) { |
查询
查询是最简单的,就是返回数组中相应索引处的值.
1 | public char charAt(int index) { |
StringBuffer源码分析
定义
1 | public final class StringBuffer |
StringBuffer和StringBuilder都是相同的继承结构.都是继承了AbstractStringBuilder.
StringBuffer和StringBuilder构造方法,可以看到默认大小是16,
1 | public StringBuffer() { |
1. 我们先来看看StringBuffer的append方法
啥,不就是调用父类的append方法嘛..
但是,请 注意:前面说了StringBuffer是线程安全的,为什么,源码里面使用了synchronized给方法加锁了.
1 | public synchronized StringBuffer append(boolean b) { |
StringBuffer的其他方法
几乎都是所有方法都加了synchronized,几乎都是调用的父类的方法.
1 | public synchronized StringBuffer delete(int start, int end) { |
StringBuilder分析
定义
1 | public final class StringBuilder |
我们先来看看StringBuilder的append方法
啥,还是调用父类的append方法嘛..
但是,请 注意:前面说了StringBuilder不是线程安全的,为什么,源码里面没有使用synchronized进行加锁.
1 | public StringBuilder append(boolean b) { |
StringBuilder的其他方法
也是全部调用的父类方法. 但是是没有加锁的.
1 | public StringBuilder delete(int start, int end) { |
总结
- String,StringBuffer,StringBuilder最终底层存储与操作的都是char数组.但是String里面的char数组是final的,而StringBuffer,StringBuilder不是,也就是说,String是不可变的,想要新的字符串只能重新生成String.而StringBuffer和StringBuilder只需要修改底层的char数组就行.相对来说,开销要小很多.
- String的大多数方法都是重新new一个新String对象返回,频繁重新生成容易生成很多垃圾.
- 还是那句古话,StringBuffer是线程安全的,StringBuilder是线程不安全的.因为StringBuffer的方法是加了synchronized锁起来了的,而StringBuilder没有.
- 增删比较多时用StringBuffer或StringBuilder(注意单线程与多线程)。实际情况按需而取吧,既然已经知道了里面的原理。
学习源码我们能从中收获什么
- Java的源码都是经过上千万(我乱说的..哈哈)的程序员校验过的,不管是算法、命名、doc文档、写作风格等等都非常规范,值得我们借鉴与深思。还有很多很多的小技巧。
- 下次在使用时能按需而取,追求性能。
- 避免项目中的很多错误的发生。