好长时间没有认真写博客了,过去的一年挺忙的。负责过数据库、线上运维环境、写代码、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)));
}
}

运行结果:

  1. function [com.string.StrTest$$Lambda$1/1198108795] time cost is 0
  2. function [com.string.StrTest$$Lambda$2/1706234378] time cost is 2339
  3. function [com.string.StrTest$$Lambda$3/1867750575] time cost is 1
  4. 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源码解读的更多相关文章

  1. String,StringBuffer,StringBuilder源码分析

    1.类结构 String Diagrams StringBuffer Diagrams StringBuilder Diagrams 通过以上Diagrams可以看出,String,StringBuf ...

  2. [源码]String StringBuffer StringBudlider(2)StringBuffer StringBuilder源码分析

      纵骑横飞 章仕烜   昨天比较忙 今天把StringBuffer StringBulider的源码分析 献上   在讲 StringBuffer StringBuilder 之前 ,我们先看一下 ...

  3. Stringbuffer与Stringbuilder源码学习和对比

    >>String/StringBuffer/StringBuilder的异同 (1)相同点观察源码会发现,三个类都是被final修饰的,是不可被继承的.(2)不同点String的对象是不可 ...

  4. String、StringBuffer、StringBuilder源码分析

    利用反编译具体看看"+"的过程 1 public class Test 2 { 3 public static void main(String[] args) 4 { 5 int ...

  5. String,StringBuffer和StringBuilder源码解析[基于JDK6]

    最近指导几位新人,学习了一下String,StringBuffer和StringBuilder类,从反馈的结果来看,总体感觉学习的深度不够,没有读出东西.其实,JDK的源码是越读越有味的.下面总结一下 ...

  6. String,StringBuffer,StringBuilder的区别及其源码分析

    String,StringBuffer,StringBuilder的区别这个问题几乎是面试必问的题,这里做了一些总结: 1.先来分析一下这三个类之间的关系 乍一看它们都是用于处理字符串的java类,而 ...

  7. String StringBuffer StringBuilder的异同

    1.String与StrIngBuffer StringBuilder的主要区别在于StrIng是不可变对象,每次对String对象进行修改之后,相对于重新创建一个对象. String源码解读: pr ...

  8. [置顶] String StringBuffer StringBuilder的区别剖析

    这是一道很常见的面试题目,至少我遇到过String/StringBuffer/StringBuilder的区别:String是不可变的对象(final)类型,每一次对String对象的更改均是生成一个 ...

  9. String类的源码分析

    之前面试的时候被问到有没有看过String类的源码,楼主当时就慌了,回来赶紧补一课. 1.构造器(构造方法) String类提供了很多不同的构造器,分别对应了不同的字符串初始化方法,此处从源码中摘录如 ...

随机推荐

  1. 钉钉的收费 [钉钉深圳研发团队 denny/2016.01.06/ 59888745@qq.com]

    普通用户(个人) 团队 企业 1.免费额度为每月通话100分钟.每天发DING 5次. 1.   每月通话300分钟,每天发DING   10次. 2.   群组最多可达1500人 1.   该公司所 ...

  2. angular.js ng-class-even ng-class-odd ng-cloak(没啥技术含量)

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  3. PCL还是SAP?

    When you first created the Hello solution in Visual Studio, you had a choice of two application temp ...

  4. CSS基本语法

    这里主要介绍Bootstrap里用到的CSS语法,以便在源码分析时更容易理解和学习.Bootstrap的CSS组件的核心就是选择器的定义以及在各自优先级上的处理. 优先级 如何确定CSS的优先级,这里 ...

  5. NPOI系列

    NPOI(1):Asp.net操作Excel NPOI(2):基础 NPOI格式设置 设置Excel的自动筛选功能 Npoi导出xlsx Npoi读取xlsx

  6. Redirecting Console.WriteLine() to Textbox

    I'm building this application in Visual Studio 2010 using C#. Basically there are 2 files, form1.cs ...

  7. No.6__C#

    第六周 周一:今天特别开心,因为来公司的第一个任务完成了,虽然是在组长的帮助下完成的.但是,还是有很多收获,在实际工作中遇到的问题和麻烦远远超出了书本知识 有些问题简直让人抓狂.现在,上班空余期间,也 ...

  8. day6_1

    一.加密模块 1.hashlib >>> data=hashlib.md5() >>> data.update(b'hello') >>> pri ...

  9. 41. Unique Binary Search Trees && Unique Binary Search Trees II

    Unique Binary Search Trees Given n, how many structurally unique BST's (binary search trees) that st ...

  10. 62. Divide Two Integers

    Divide Two Integers Divide two integers without using multiplication, division and mod operator. 思路: ...