关于后端:StringBuilder-比-String-快空嘴白牙的证据呢

55次阅读

共计 9863 个字符,预计需要花费 25 分钟才能阅读完成。

作者:小傅哥
博客:https://bugstack.cn

积淀、分享、成长,让本人和别人都能有所播种!????

一、前言

聊的是八股的文,干的是搬砖的活!

面我的题开发都用不到,你为什么要问?可能这是大部分程序员求职时的经验,甚至也是大家厌恶和焦躁的点。明明给的是拧螺丝的钱、明明做的是写 CRUD 的事、明明担的是成工具的人!

明明 … 有很多,可明明公司不会招 5 年开发做 3 年教训的事、明明公司也更喜爱具备附加价值的研发。有些小公司不好说,但在一些互联网大厂中,咱们都心愿招聘到具备造就价值的,也更喜爱能疾速打怪降级的,也更违心让这样的人承当更大的职责。

但,你酸了! 他人看源码你打游戏、他人学算法你刷某音、他人写博客你浪 98。所以,没有把工夫用到个人成长上,就始终会被他人榨取。

二、面试题

谢飞机,总感觉本人有技术瓶颈、有常识盲区,然而又不晓得在哪。所以约面试官聊天,尽管也面不过来!

面试官:飞机,你又抱着大脸,来白嫖我了啦?

谢飞机:嘿嘿,我须要常识,我渴。

面试官:好,那明天聊聊最罕用的 String 吧,你怎么初始化一个字符串类型。

谢飞机String str = "abc";

面试官:还有吗?

谢飞机:还有?啊,这样 String str = new String("abc"); ????

面试官:还有吗?

谢飞机:啊!?还有!不晓得了!

面试官 :你不懂 String,你没看过源码。还能够这样;new String(new char[]{'c', 'd'}); 回家再学学吧,下次记得给我买 百事 ,我不喝 可口

三、StringBuilder 比 String 快吗?

1. StringBuilder 比 String 快,证据呢?

老子代码一把梭,总有人絮叨这么搞不好,那 StringBuilder 到底那快了!

1.1 String

long startTime = System.currentTimeMillis();
String str = "";
for (int i = 0; i < 1000000; i++) {str += i;}
System.out.println("String 耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");

1.2 StringBuilder

long startTime = System.currentTimeMillis();
StringBuilder str = new StringBuilder();
for (int i = 0; i < 1000000; i++) {str.append(i);
}
System.out.println("StringBuilder 耗时" + (System.currentTimeMillis() - startTime) + "毫秒");

1.3 StringBuffer

long startTime = System.currentTimeMillis();
StringBuffer str = new StringBuffer();
for (int i = 0; i < 1000000; i++) {str.append(i);
}
System.out.println("StringBuffer 耗时" + (System.currentTimeMillis() - startTime) + "毫秒");

综上,别离应用了 StringStringBuilderStringBuffer,做字符串链接操作(100 个、1000 个、1 万个、10 万个、100 万个),记录每种形式的耗时。最终汇总图表如下;

从上图能够得出以下论断;

  1. String 字符串链接是耗时的,尤其数据量大的时候,几乎没法应用了。这是做试验,根本也不会有人这么干!
  2. StringBuilderStringBuffer,因为没有产生多线程竞争也就没有???? 锁降级,所以两个类耗时简直雷同,当然在单线程下更举荐应用 StringBuilder

2. StringBuilder 比 String 快,为什么?

String str = "";
for (int i = 0; i < 10000; i++) {str += i;}

这段代码就是三种字符串拼接形式,最慢的一种。不是说这种 + 加的符号,会被优化成 StringBuilder 吗,那怎么还慢?

的确会被 JVM 编译期优化,但优化成什么样子了呢,先看下字节码指令;javap -c ApiTest.class

一看指令码,这不是在循环里 (if_icmpgt) 给我 newStringBuilder 了吗,怎么还这么慢呢?再认真看,其实你会发现,这 new 是在循环里吗呀,咱们把这段代码写进去再看看;

String str = "";
for (int i = 0; i < 10000; i++) {str = new StringBuilder().append(str).append(i).toString();}

当初再看这段代码就很清晰了,所有的字符串链接操作,都须要实例化一次 StringBuilder,所以十分耗时。 并且你能够验证,这样写代码耗时与字符串间接链接是一样的。 所以把StringBuilder 提到上一层 for 循环外更快。

四、String 源码剖析

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
     
    ...
}

1. 初始化

在与 谢飞机 的面试题中,咱们聊到了 String 初始化的问题,依照个别咱们利用的频次上,能想到的只有间接赋值,String str = "abc"; ,但因为 String 的底层数据结构是数组char value[],所以它的初始化形式也会有很多跟数组相干的,如下;

String str_01 = "abc";
System.out.println("默认形式:" + str_01);

String str_02 = new String(new char[]{'a', 'b', 'c'});
System.out.println("char 形式:" + str_02);

String str_03 = new String(new int[]{0x61, 0x62, 0x63}, 0, 3);
System.out.println("int 形式:" + str_03);

String str_04 = new String(new byte[]{0x61, 0x62, 0x63});
System.out.println("byte 形式:" + str_04);

以上这些形式都能够初始化,并且最终的后果是统一的,abc。如果说初始化的形式没用让你感触到它是数据结构,那么 str_01.charAt(0); 呢,只有你往源码里一点,就会发现它是 O(1) 的工夫复杂度从数组中获取元素,所以效率也是十分高,源码如下;

public char charAt(int index) {if ((index < 0) || (index >= value.length)) {throw new StringIndexOutOfBoundsException(index);
    }
    return value[index];
}

2. 不可变(final)

字符串创立后是不可变的,你看到的 + 加号 连贯操作,都是创立了新的对象把数据寄存过来,通过源码就能够看到;

从源码中能够看到,String 的类和用于寄存字符串的办法都用了 final 润饰,也就是创立了当前,这些都是不可变的。

举个例子

String str_01 = "abc";
String str_02 = "abc" + "def";
String str_03 = str_01 + "def";

不思考其余状况,对于程序初始化。以上这些代码 str_01str_02str_03,都会初始化几个对象呢?其实这个初始化几个对象从侧面就是反馈对象是否可变性。

接下来咱们把下面代码反编译,通过指令码看到底创立了几个对象。

反编译下

  public void test_00();
    Code:
       0: ldc           #2                  // String abc
       2: astore_1
       3: ldc           #3                  // String abcdef
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: ldc           #7                  // String def
      19: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      25: astore_3
      26: return
  • str_01 = "abc",指令码:0: ldc,创立了一个对象。
  • str_02 = "abc" + "def",指令码:3: ldc // String abcdef,得益于 JVM 编译期的优化,两个字符串会进行相连,创立一个对象存储。
  • str_03 = str_01 + "def",指令码:invokevirtual,这个就不一样了,它须要把两个字符串相连,会创立 StringBuilder 对象,直至最初 toString:() 操作,共创立了三个对象。

所以,咱们看到,字符串的创立是不能被批改的,相连操作会创立出新对象。

3. intern()

3.1 经典题目

String str_1 = new String("ab");
String str_2 = new String("ab");
String str_3 = "ab";

System.out.println(str_1 == str_2);
System.out.println(str_1 == str_2.intern());
System.out.println(str_1.intern() == str_2.intern());
System.out.println(str_1 == str_3);
System.out.println(str_1.intern() == str_3);

这是一道经典的 String 字符串面试题,乍一看可能还会有点晕。答案如下;

false
false
true
false
true

3.2 源码剖析

看了答案有点感觉了吗,其实可能你理解办法 intern(),这里先看下它的源码;

/**
 * Returns a canonical representation for the string object.
 * <p>
 * A pool of strings, initially empty, is maintained privately by the
 * class {@code String}.
 * <p>
 * When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.
 * <p>
 * It follows that for any two strings {@code s} and {@code t},
 * {@code s.intern() == t.intern()} is {@code true}
 * if and only if {@code s.equals(t)} is {@code true}.
 * <p>
 * All literal strings and string-valued constant expressions are
 * interned. String literals are defined in section 3.10.5 of the
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * @return  a string that has the same contents as this string, but is
 *          guaranteed to be from a pool of unique strings.
 */
public native String intern();

这段代码和正文什么意思呢?

native,阐明 intern() 是一个本地办法,底层通过 JNI 调用 C ++ 语言编写的性能。

openjdk8jdksrcsharenativejavalangString.c

Java_java_lang_String_intern(JNIEnv *env, jobject this)  
{return JVM_InternString(env, this);  
}  

oop result = StringTable::intern(string, CHECK_NULL);

oop StringTable::intern(Handle string_or_null, jchar* name,  
                        int len, TRAPS) {unsigned int hashValue = java_lang_String::hash_string(name, len);  
  int index = the_table()->hash_to_index(hashValue);  
  oop string = the_table()->lookup(index, name, len, hashValue);  
  if (string != NULL) return string;   
  return the_table()->basic_add(index, string_or_null, name, len,  
                                hashValue, CHECK_NULL);  
}  
  • 代码块有点长这里只截取了局部内容,源码能够学习开源 jdk 代码,连贯:https://codeload.github.com/abhijangda/OpenJDK8/zip/master
  • C++ 这段代码有点像 HashMap 的哈希桶 + 链表的数据结构,用来寄存字符串,所以如果哈希值抵触重大,就会导致链表过长。这在咱们解说 hashMap 中曾经介绍,能够回看 HashMap 源码
  • StringTable 是一个固定长度的数组 1009 个大小,jdk1.6 不可调、jdk1.7 能够设置-XX:StringTableSize,按需调整。

3.3 问题图解

看图谈话,如下;

  1. 先说 ==,根底类型比对的是值,援用类型比对的是地址。另外,equal 比对的是哈希值。
  2. 两个 new 进去的对象,地址必定不同,所以是 false。
  3. intern(),间接把值推动了常量池,所以两个对象都做了 intern() 操作后,比对是常量池里的值。
  4. str_3 = "ab",赋值,JVM 编译器做了优化,不会从新创建对象,间接援用常量池里的值。所以str_1.intern() == str_3,比对后果是 true。

了解了这个构造,基本不须要死记硬背应答面试,让懂了就是真的懂,大脑也会跟着愉悦。

五、StringBuilder 源码剖析

1. 初始化

new StringBuilder();
new StringBuilder(16);
new StringBuilder("abc");

这几种形式都能够初始化,你能够传一个初始化容量,也能够初始化一个默认的字符串。它的源码如下;

public StringBuilder() {super(16);
}

AbstractStringBuilder(int capacity) {value = new char[capacity];
}

定睛一看,这就是在初始化数组呀!那是不操作起来跟应用 ArrayList 似的呀!

2. 增加元素

stringBuilder.append("a");
stringBuilder.append("b");
stringBuilder.append("c");

增加元素的操作很简略,应用 append 即可,那么它是怎么往数组中寄存的呢,须要扩容吗?

2.1 入口办法

public AbstractStringBuilder append(String str) {if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}
  • 这个是 public final class StringBuilder extends AbstractStringBuilder,的父类与 StringBuffer 共用这个办法。
  • 这里包含了容量检测、元素拷贝、记录 count 数量。

2.2 扩容操作

ensureCapacityInternal(count + len);

/**
 * 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);
}

如上,StringBuilder,就跟操作数组的原理一样,都须要检测容量大小,按需扩容。扩容的容量是 n * 2 + 2,另外把原有元素拷贝到新新数组中。

2.3 填充元素

str.getChars(0, len, value, count);

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    // ...
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

增加元素的形式是基于 System.arraycopy 拷贝操作进行的,这是一个本地办法。

2.4 toString()

既然 stringBuilder 是数组,那么它是怎么转换成字符串的呢?

stringBuilder.toString();

@Override
public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

其实须要用到它是 String 字符串的时候,就是应用 String 的构造函数传递数组进行转换的,这个办法在咱们下面解说 String 的时候曾经介绍过。

六、StringBuffer 源码剖析

StringBufferStringBuilder,API 的应用和底层实现上基本一致,维度不同的是 StringBuffer 加了 synchronized ???? 锁,所以它是线程平安的。源码如下;

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

那么,synchronized 不是重量级锁吗,JVM 对它有什么优化呢?

其实为了缩小取得锁与开释锁带来的性能损耗,从而引入了偏差锁、轻量级锁、重量级锁来进行优化,它的进行一个锁降级,如下图 (此图引自互联网用户: 韭韭韭韭菜,画的十分优良);

  1. 从无锁状态开始,当线程进入 synchronized 同步代码块,会查看对象头和栈帧内是否有以后线下 ID 编号,无则应用 CAS 替换。
  2. 解锁时,会应用 CASDisplaced Mark Word 替换回到对象头,如果胜利,则示意竞争没有产生,反之则示意以后锁存在竞争锁就会升级成重量级锁。
  3. 另外,大多数状况下锁???? 是不产生竞争的,根本由一个线程持有。所以,为了防止取得锁与开释锁带来的性能损耗,所以引入锁降级,降级后不能降级。

七、罕用 API

序号 办法 形容
1 str.concat("cde") 字符串连贯,替换 + 号
2 str.length() 获取长度
3 isEmpty() 判空
4 str.charAt(0) 获取指定地位元素
5 str.codePointAt(0) 获取指定地位元素,并返回 ascii 码值
6 str.getBytes() 获取 byte[]
7 str.equals(“abc”) 比拟
8 str.equalsIgnoreCase(“AbC”) 疏忽大小写,比对
9 str.startsWith(“a”) 开始地位值判断
10 str.endsWith(“c”) 结尾地位值判断
11 str.indexOf(“b”) 判断元素地位,开始地位
12 str.lastIndexOf(“b”) 判断元素地位,结尾地位
13 str.substring(0, 1) 截取
14 str.split(“,”) 拆分,能够反对正则
15 str.replace(“a”,”d”)、replaceAll 替换
16 str.toUpperCase() 转大写
17 str.toLowerCase() 转小写
18 str.toCharArray() 转数组
19 String.format(str, “”) 格式化,%s、%c、%b、%d、%x、%o、%f、%a、%e、%g、%h、%%、%n、%tx
20 str.valueOf(“123”) 转字符串
21 trim() 格式化,首尾去空格
22 str.hashCode() 获取哈希值

八、总结

  • 业精于勤, 荒于嬉,你学到的常识不肯定只是为了面试筹备,还更应该是拓展本人的技术深度和广度。这个过程可能很苦楚,但总得须要某一个烧脑的过程,才让其余更多的常识学起来更加容易。
  • 本文介绍了 String、StringBuilder、StringBuffer,的数据结构和源码剖析,更加透彻的了解后,也能更加精确的应用,不会被因为不懂而犯错误。
  • 想把代码写好,至多要有这四面内容,包含;数据结构、算法、源码、设计模式,这四方面在加上业务教训与集体视线,能力真的把一个需要、一个大我的项目写的具备良好的扩展性和易维护性。

九、系列举荐

  • 握草,你居然在代码里下毒!
  • 一次代码评审,差点过不了试用期!
  • LinkedList 插入速度比 ArrayList 快?你确定吗?
  • 重学 Java 设计模式(22 个实在开发场景)
  • 面经手册(上最快的车,拿最贵的 offer)

正文完
 0