作者:小傅哥
博客: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) + "毫秒");
综上,别离应用了 String
、StringBuilder
、StringBuffer
,做字符串链接操作(100 个、1000 个、1 万个、10 万个、100 万个),记录每种形式的耗时。最终汇总图表如下;
从上图能够得出以下论断;
String
字符串链接是耗时的,尤其数据量大的时候,几乎没法应用了。这是做试验,根本也不会有人这么干!StringBuilder
、StringBuffer
,因为没有产生多线程竞争也就没有???? 锁降级,所以两个类耗时简直雷同,当然在单线程下更举荐应用StringBuilder
。
2. StringBuilder 比 String 快,为什么?
String str = "";
for (int i = 0; i < 10000; i++) {str += i;}
这段代码就是三种字符串拼接形式,最慢的一种。不是说这种 +
加的符号,会被优化成 StringBuilder
吗,那怎么还慢?
的确会被 JVM 编译期优化,但优化成什么样子了呢,先看下字节码指令;javap -c ApiTest.class
一看指令码,这不是在循环里 (if_icmpgt) 给我 new
了 StringBuilder
了吗,怎么还这么慢呢?再认真看,其实你会发现,这 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_01
、str_02
、str_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™ 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 问题图解
看图谈话,如下;
- 先说
==
,根底类型比对的是值,援用类型比对的是地址。另外,equal 比对的是哈希值。 - 两个 new 进去的对象,地址必定不同,所以是 false。
- intern(),间接把值推动了常量池,所以两个对象都做了
intern()
操作后,比对是常量池里的值。 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 源码剖析
StringBuffer
与 StringBuilder
,API 的应用和底层实现上基本一致,维度不同的是 StringBuffer
加了 synchronized
???? 锁,所以它是线程平安的。源码如下;
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
那么,synchronized
不是重量级锁吗,JVM 对它有什么优化呢?
其实为了缩小取得锁与开释锁带来的性能损耗,从而引入了偏差锁、轻量级锁、重量级锁来进行优化,它的进行一个锁降级,如下图 (此图引自互联网用户: 韭韭韭韭菜,画的十分优良);
- 从无锁状态开始,当线程进入
synchronized
同步代码块,会查看对象头和栈帧内是否有以后线下 ID 编号,无则应用CAS
替换。 - 解锁时,会应用
CAS
将Displaced Mark Word
替换回到对象头,如果胜利,则示意竞争没有产生,反之则示意以后锁存在竞争锁就会升级成重量级锁。 - 另外,大多数状况下锁???? 是不产生竞争的,根本由一个线程持有。所以,为了防止取得锁与开释锁带来的性能损耗,所以引入锁降级,降级后不能降级。
七、罕用 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)