作者:小傅哥
博客: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 字符串面试题,乍一看可能还会有点晕。答案如下;

falsefalsetruefalsetrue

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();

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

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

六、StringBuffer 源码剖析

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

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

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

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

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

七、罕用API

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

八、总结

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

九、系列举荐

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