String、StringBuffer、StringBuilder源码解读
序
好长时间没有认真写博客了,过去的一年挺忙的。负责过数据库、线上运维环境、写代码、Code review等等东西挺多。
学习了不少多方面的东西,不过还是需要回归实际、加强内功,方能扛鼎。
去年学习Mysql列举了大纲,书写了一部分。后来进入到工作状态,就没有继续书写。当然其实没有书写的内容部分已经总结到了公司内部的wiki中,或者在工作过程中大半也应用过,也懒得书写下来了。看什么时候又有心情,重新回顾总结一下吧。
下一步的学习计划
数据结构、算法、源代码解读、多线程(哎,学无止境)
为什么先说String呢?
其实绝大部分业务开发过程中String都是最常用的类。常常利用JProfiler这类工具做内存分析时,能看到char[](为什么是char[]在接下来的源码解读中会有提现)能站到70%以上。
类关系图
简要对比
差别 | String | StringBuffer | StringBuilder |
---|---|---|---|
常量 / 变量 | 常量 | 变量 | 变量 |
线程是否安全 | 安全 | 安全 | 非安全 |
所在内存区域 | Constant String Pool(常量池) | heap | heap |
是否能被继承 | 否 | 否 | 否 |
代码行数 | 3157 | 718 | 448 |
使用场景 | 在字符串不经常变化的场景 | 在频繁进行字符串运算(如拼接、替换、删除等), 并且运行在多线程环境 |
在频繁进行字符串运算(如拼接、替换、和删除等), 并且运行在单线程的环境 |
场景举例 | 常量的声明、少量的变量运算 | XML 解析、HTTP 参数解析和封装 | SQL 语句的拼装、JSON 封装 |
从代码行数来上说String类更大,其中大量的方法重载拓展了篇幅。同时注释文档详细,注释的行文风格常常看到一个简短的定义之后,紧跟一个由that或the引导的定语从句(定语从句一般皆放在被它所修饰的名(代)词之后)。
例:
- /**
- * Allocates a new {@code String} that contains characters from a subarray
- * of the <a href="Character.html#unicode">Unicode code point</a> array
- * argument. The {@code offset} argument is the index of the first code
- * point of the subarray and the {@code count} argument specifies the
- * length of the subarray. The contents of the subarray are converted to
- * {@code char}s; subsequent modification of the {@code int} array does not
- * affect the newly created string.
- **/
AbstractStringBuilder :StringBuffer类与StringBuilder类都继承了AbstractStringBuilder,抽象父类里实现了除toString以外的所有方法。
StringBuilder:自己重写了方法之后,全都在方法内super.function(),未做任何扩展。同时从类名语义上来说String构建者,所以没有subString方法看来也合情合理;
StringBuffer:在重写方法的同时,几乎所有方法都添加了synchronized同步关键字;
常量与变量解释
String类是依赖一个私有字符常量表实现的;
- public final class String
- implements java.io.Serializable, Comparable<String>, CharSequence {
- /** The value is used for character storage. */
- private final char value[];
StringBuffer与StringBuilder都是继承AbstractStringBuilder,然而AbstractStringBuilder类是依赖一个字符变量表实现的;
- abstract class AbstractStringBuilder implements Appendable, CharSequence {
- /**
- * The value is used for character storage.
- */
- char[] value;
线程安全分析
为什么String是线程安全的?
首先,String是依赖字符常量表实现的;
其次,所有对String发生修改的方法返回值都是一个新的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];
- for (int j = 0; j < i; j++) {
- buf[j] = val[j];
- }
- while (i < len) {
- char c = val[i];
- buf[i] = (c == oldChar) ? newChar : c;
- i++;
- }
- return new String(buf, true);
- }
- }
- return this;
- }
为什么实现了以上提到的两点就是线程安全的呢?
以StringBuilder类append方法为示例,第19行将需要添加的value,通过arraycopy方法复制到dst中。
- AbstractStringBuilder append(AbstractStringBuilder asb) {
- if (asb == null)
- return appendNull();
- int len = asb.length();
- ensureCapacityInternal(count + len);
- asb.getChars(0, len, value, count);//value为char [] value,StringBuilder依赖字符变量表实现
- count += len;
- return this;
- }
- public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
- {
- if (srcBegin < 0)
- throw new StringIndexOutOfBoundsException(srcBegin);
- if ((srcEnd < 0) || (srcEnd > count))
- throw new StringIndexOutOfBoundsException(srcEnd);
- if (srcBegin > srcEnd)
- throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
- System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
- }
场景假设:
假设有A、B两个线程,StringBuilder初始值为"1";
A线程:执行append("2");
B线程:执行append("3");
过程分析:
CPU在执行了部分A线程的逻辑,刚好执行到第19行,此时B线程已经执行完毕;
导致A线程开始执行append("2")时,StringBuilder为"1";
执行到一半StringBuilder变成了"13";
最后结果得到为"132";
过程图示:
哎,感觉没能选择一个较好的例子解释这个问题。肯定会有一部分同学懂这部分原理的觉得讲得太浅,不懂的同学可能依然不明所以。在之后的篇幅中,会仔细讲述线程安全这块内容。
性能分析
常常来说在大家的印象中,String做字符串连接是比较低效的行为。甚至在很多性能优化的经典中,都提到过切莫在迭代中使用字符串拼接操作。
这是为什么呢?
在人们通常的认识中String为常量,对常量做更改时必然需要重新开辟内存空间,以容纳新生成的String内容。如果在迭代场景中使用字符串拼接操作,那么就会大量无谓的开辟内存空间,然后在生成新的String对象后,又释放已丢失引用的String对象。
但是事实真是如此么?
测试代码:
- import java.util.function.Supplier;
- /**
- * @auth snifferhu
- * @date 16/9/24 18:50
- */
- public class StrTest {
- private final static int TIMES = 30000;// 测试循环次数
- private static Supplier<CharSequence> sigleStringAppend = () -> {
- String tmp = "a" + "b" + "c";
- return tmp;
- };
- private static Supplier<CharSequence> stringAppend = () -> {
- String tmp = "1";
- for (int i = 0; i < TIMES; i++) {
- tmp+= "add";
- }
- return tmp;
- };
- private static Supplier<CharSequence> stringBufferAppend = () -> {
- StringBuffer tmp = new StringBuffer("1");
- for (int i = 0; i < TIMES; i++) {
- tmp.append("add");
- }
- return tmp;
- };
- private static Supplier<CharSequence> stringBuilderAppend = () -> {
- StringBuilder tmp = new StringBuilder("1");
- for (int i = 0; i < TIMES; i++) {
- tmp.append("add");
- }
- return tmp;
- };
- public static void main(String[] args) {
- timerWarpper(sigleStringAppend);
- timerWarpper(stringAppend);
- timerWarpper(stringBufferAppend);
- timerWarpper(stringBuilderAppend);
- }
- public static void timerWarpper(Supplier<CharSequence> supplier){
- Long start = System.currentTimeMillis();
- supplier.get();
- System.out.println(String.format("function [%s] time cost is %s" ,
- supplier.getClass().getCanonicalName() ,
- (System.currentTimeMillis() - start)));
- }
- }
运行结果:
function [com.string.StrTest$$Lambda$1/1198108795] time cost is 0
function [com.string.StrTest$$Lambda$2/1706234378] time cost is 2339
function [com.string.StrTest$$Lambda$3/1867750575] time cost is 1
function [com.string.StrTest$$Lambda$4/2046562095] time cost is 1
从结果看来简单的String拼接在1毫秒内完成,StringBuffer与StringBuilder耗时为1,String类在迭代拼接操作中消耗了极长的时间为2339毫秒。
能够得出结论:迭代中使用字符串拼接操作确实是极为消耗时间的操作。
hashCode
String类中将hashCode缓存放在了私有变量hash,算是一种提升性能的手段,因为String本身是常量不会改变,也不担心hashCode会出错。
- /** Cache the hash code for the string */
- private int hash; // Default to 0
- public int hashCode() {
- int h = hash;
- if (h == 0 && value.length > 0) {
- char val[] = value;
- for (int i = 0; i < value.length; i++) {
- h = 31 * h + val[i];
- }
- hash = h;
- }
- return h;
- }
StringBuffer与StringBuilder类并未重写hashCode方法;
equals
String类先利用"=="比较内存地址,再判断是否属于String类型,最后逐一比较每一个字节内容;
- public boolean equals(Object anObject) {
- if (this == anObject) {
- return true;
- }
- if (anObject instanceof String) {
- String anotherString = (String)anObject;
- int n = value.length;
- if (n == anotherString.value.length) {
- char v1[] = value;
- char v2[] = anotherString.value;
- int i = 0;
- while (n-- != 0) {
- if (v1[i] != v2[i])
- return false;
- i++;
- }
- return true;
- }
- }
- return false;
- }
StringBuffer与StringBuilder类并未重写equals方法;
toString
在toString方法实现上,它们各有千秋。String类直接返回自己。
- /**
- * This object (which is already a string!) is itself returned.
- *
- * @return the string itself.
- */
- public String toString() {
- return this;
- }
StringBuffer类为了保障线程安全,添加了同步关键字;
同时为了提升性能利用私有变量缓存内容,并且本地缓存不能被序列化;
在每次修改StringBuffer时,都会将toStringCache置空。
- /**
- * A cache of the last value returned by toString. Cleared
- * whenever the StringBuffer is modified.
- */
- private transient char[] toStringCache;
- @Override
- public synchronized String toString() {
- if (toStringCache == null) {
- toStringCache = Arrays.copyOfRange(value, 0, count);
- }
- return new String(toStringCache, true);
- }
valueOf
为什么可以挑出这个方法讲述呢?
这是个静态方法,对于很多类来说都有toString方法,亦能达到类似的效果;
在此做了一个容错处理,判断是否为null,保障不会报错;
- public static String valueOf(Object obj) {
- return (obj == null) ? "null" : obj.toString();
- }
在StringBuffer类、StringBuilder类中,没有valueOf方法,不过在insert方法中调用到了valueOf;
在这是有坑点的,当传入的值为null时,它结果给我插入了"null"。大家伙切记。
- public synchronized StringBuffer insert(int offset, Object obj) {
- toStringCache = null;
- super.insert(offset, String.valueOf(obj));
- return this;
- }
subString
StringBuffer、StringBuilder类依然是继承AbstractStringBuilder类实现,StringBuffer略有不同则是添加了同步关键字;值得细细品味的是异常处理,明确的语义能够让人准确定位问题。
- public String substring(int start, int end) {
- if (start < 0)
- throw new StringIndexOutOfBoundsException(start);
- if (end > count)
- throw new StringIndexOutOfBoundsException(end);
- if (start > end)
- throw new StringIndexOutOfBoundsException(end - start);
- return new String(value, start, end - start);
- }
相对而言String类的实现,在最后抛出新对象时,做了判断确定是否需要真的新生成对象,值得可取的性能优化点;
同时因为返回类型为String,AbstractStringBuilder类没法学String一样抛出this;
说来说去都需要新生成String对象所以就省去了这个判断。
- public String substring(int beginIndex, int endIndex) {
- if (beginIndex < 0) {
- throw new StringIndexOutOfBoundsException(beginIndex);
- }
- if (endIndex > value.length) {
- throw new StringIndexOutOfBoundsException(endIndex);
- }
- int subLen = endIndex - beginIndex;
- if (subLen < 0) {
- throw new StringIndexOutOfBoundsException(subLen);
- }
- return ((beginIndex == 0) && (endIndex == value.length)) ? this
- : new String(value, beginIndex, subLen);
- }
replace
String类实现replace方法,先判断新旧是否一致提升效率,棒棒哒!
while循环查找第一个与oldChar相同的表地址;
为了提升性能做了本地缓存buf,同时因为value本身是常量也不用怕修改过程中被篡改了。
- 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];
- for (int j = 0; j < i; j++) {
- buf[j] = val[j];
- }
- while (i < len) {
- char c = val[i];
- buf[i] = (c == oldChar) ? newChar : c;
- i++;
- }
- return new String(buf, true);
- }
- }
- return this;
- }
StringBuffer、StringBuilder对应的方法入参和出参都与String不同;
在校验完长度之后,就调用ensureCapacityInternal做表扩展;
利用System.arraycopy的时候,因为StringBuilder没做同步,会有arraycopy执行的同时value被篡改,导致长度不合适的情况;
- public AbstractStringBuilder replace(int start, int end, String str) {
- if (start < 0)
- throw new StringIndexOutOfBoundsException(start);
- if (start > count)
- throw new StringIndexOutOfBoundsException("start > length()");
- if (start > end)
- throw new StringIndexOutOfBoundsException("start > end");
- if (end > count)
- end = count;
- int len = str.length();
- int newCount = count + len - (end - start);
- ensureCapacityInternal(newCount);
- System.arraycopy(value, end, value, start + len, count - end);
- str.getChars(value, start);
- count = newCount;
- return this;
- }
- /**
- * This method has the same contract as ensureCapacity, but is
- * never synchronized.
- */
- private void ensureCapacityInternal(int minimumCapacity) {
- // overflow-conscious code
- if (minimumCapacity - value.length > 0)
- expandCapacity(minimumCapacity);
- }
- /**
- * This implements the expansion semantics of ensureCapacity with no
- * size check or synchronization.
- */
- void expandCapacity(int minimumCapacity) {
- int newCapacity = value.length * 2 + 2;
- if (newCapacity - minimumCapacity < 0)
- newCapacity = minimumCapacity;
- if (newCapacity < 0) {
- if (minimumCapacity < 0) // overflow
- throw new OutOfMemoryError();
- newCapacity = Integer.MAX_VALUE;
- }
- value = Arrays.copyOf(value, newCapacity);
- }
trim
String类在实现trim巧妙的地方在于用char直接做小于等于的比较,经过验证他们底层会转化为int类型,然后比较的是他们的ascii码。
- public String trim() {
- int len = value.length;
- int st = 0;
- char[] val = value; /* avoid getfield opcode */
- while ((st < len) && (val[st] <= ' ')) {
- st++;
- }
- while ((st < len) && (val[len - 1] <= ' ')) {
- len--;
- }
- return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
- }
String、StringBuffer、StringBuilder源码解读的更多相关文章
- String,StringBuffer,StringBuilder源码分析
1.类结构 String Diagrams StringBuffer Diagrams StringBuilder Diagrams 通过以上Diagrams可以看出,String,StringBuf ...
- [源码]String StringBuffer StringBudlider(2)StringBuffer StringBuilder源码分析
纵骑横飞 章仕烜 昨天比较忙 今天把StringBuffer StringBulider的源码分析 献上 在讲 StringBuffer StringBuilder 之前 ,我们先看一下 ...
- Stringbuffer与Stringbuilder源码学习和对比
>>String/StringBuffer/StringBuilder的异同 (1)相同点观察源码会发现,三个类都是被final修饰的,是不可被继承的.(2)不同点String的对象是不可 ...
- String、StringBuffer、StringBuilder源码分析
利用反编译具体看看"+"的过程 1 public class Test 2 { 3 public static void main(String[] args) 4 { 5 int ...
- String,StringBuffer和StringBuilder源码解析[基于JDK6]
最近指导几位新人,学习了一下String,StringBuffer和StringBuilder类,从反馈的结果来看,总体感觉学习的深度不够,没有读出东西.其实,JDK的源码是越读越有味的.下面总结一下 ...
- String,StringBuffer,StringBuilder的区别及其源码分析
String,StringBuffer,StringBuilder的区别这个问题几乎是面试必问的题,这里做了一些总结: 1.先来分析一下这三个类之间的关系 乍一看它们都是用于处理字符串的java类,而 ...
- String StringBuffer StringBuilder的异同
1.String与StrIngBuffer StringBuilder的主要区别在于StrIng是不可变对象,每次对String对象进行修改之后,相对于重新创建一个对象. String源码解读: pr ...
- [置顶] String StringBuffer StringBuilder的区别剖析
这是一道很常见的面试题目,至少我遇到过String/StringBuffer/StringBuilder的区别:String是不可变的对象(final)类型,每一次对String对象的更改均是生成一个 ...
- String类的源码分析
之前面试的时候被问到有没有看过String类的源码,楼主当时就慌了,回来赶紧补一课. 1.构造器(构造方法) String类提供了很多不同的构造器,分别对应了不同的字符串初始化方法,此处从源码中摘录如 ...
随机推荐
- 【uTenux实验】信号量
信号量(semaphore)是一个用来指示可用的资源并将可用资源的数量以数值的形式表示出来的对象.当使用一组资源时,信号量用来实现互斥控制和同步.uTenux提供了信号量出来的API,可以很方便地使用 ...
- 黄聪:Access-Control-Allow-Origin,JS跨域解决办法
.htaccess添加下面代码: <IfModule mod_headers.c> Header add Access-Control-Allow-Origin "*" ...
- 闲谈Tomcat性能优化
Tomcat在各位JavaWeb从业者常常就是默认的开发环境,但是Tomcat的默认配置作为生产环境,尤其是内存和线程的配置,默认都很低,容易成为性能瓶颈. 幸好Tomcat还有很多的提升空间.下文介 ...
- 数据结构&算法-双向链表
1.引言 双向链表原理和单链表差不多,就是操作相对繁琐一些,简单画了几个图,把思想弄清楚就好. 2.正文 这里介绍一下插入和删除的原理,相信这两个操作搞清楚的话,其他操作也没什么问题. 2.1插入节点 ...
- 34. Convert Sorted List to Binary Search Tree && Convert Sorted Array to Binary Search Tree
Convert Sorted List to Binary Search Tree OJ: https://oj.leetcode.com/problems/convert-sorted-list-t ...
- Jmeter组件2. Timer 定时器
关于定时器,首先明确几个概念 定时器在每个Sampler执行之前执行 定时器有作用域,同一个作用域内的定时器会在域内Sampler执行之前都执行掉 如果要让某定时器只作用于一个Sampler,将定时器 ...
- google打不开啦,咋办?
前言:以前开发的时候一直使用google浏览器,好像是两年前的某一天突然间发现google搜索不能访问了,我喜欢将自己觉得有趣的网页做成标签页,google不能访问只能先换别的了,firefox也挺不 ...
- nodejs Express 4.x req.body req.query req.params 三种获取参数的方法
第一种情况:http://localhost:3000/1,我们可以用req.params.(应该是跟路由有关,待) 第二种情况:http://localhost:3000/?id=1,用req.qu ...
- 项目名 的在JSP或JAVA中的另类写法
在JSP页面中${pageContext.request.contextPath } 表示项目名<form action="${pageContext.request.contextP ...
- The prefix "context" for element "context:component-scan" is not bound.
在beans里面加上下面信息: xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLo ...