來,進來的Java程序猿,咱們認識一下。html
我是俗世遊子,在外流浪多年的Java程序猿java
首先,咱們來看個小栗子,保證你看了既陌生又熟悉:面試
public class St { public static void main(String[] args) { System.out.println("Hello World! ! !"); } }
熟悉不,有沒有感受回到了剛剛入門的時刻?api
上面,咱們輸出了一個字符串,在以後的開發生涯中,用String定義的字符串對象也頻繁的出如今咱們的代碼中,好比下面的兩種方式,就是咱們使用的定義方式:數組
String s1 = "abc"; String s3 = "abc";
String s2 = new String("abc"); // ----- s1 == s2 true s2.intern();
這裏,我想到了一道常見的面試題:已上面爲例安全
- s1 == s2 ?
- s1 == s3 ?
- s2一共建立了幾個對象?
並且,我想讓你們想想,若是讓大家來介紹String,會如何介紹呢?多線程
String是一個不可變對象,API方法返回的實際上是一個新的String對象oracle
好,那麼接下來咱們就好好的剖析下這個String類型app
首先,咱們來看看String的結構圖ide
咱們來看看實現源碼:
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; public String() { this.value = "".value; } // 其餘的構造方法 }
咱們能夠看到,整個String類被final修飾,咱們都知道,被final修飾過的類或者對象
咱們經過查看其構造方法能夠發現,一樣被final修飾的char數組存儲着咱們定義的字符串,看下圖也能夠看得出字符串的一個存儲結構
因此說咱們經過chatAt()
方法的下標可以獲得指定的字符,緣由就在於字符串是經過char數組來儲存的。
一樣的,char數組被final修飾,權限是private,並且沒有提供對外設置的方法
可是咱們要明白一點,這裏的不可變指的是:底層存儲char數組在內存中地址引用不可變,可是其自己的內容咱們是能夠經過一些手段來改變的,好比:反射
若是咱們之後也想實現一個不可變的類,就能夠參考String來實現
咱們在看源碼的時候,咱們要重點看一看做者在實現一些方法時的思路,有什麼地方的想法是咱們能夠借鑑的,好用在咱們之後的代碼設計中(逼格高)
做爲重寫方法出現頻率最高的兩個方法,咱們就看String是如何實現的
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; }
爲何在計算的時候,採用的是 31 ?
很簡單,咱們都知道,計算機的底層數據都是0和1,而31的二進制數值正好是 11111,在計算上能夠進行移位操做,效率較高
在Java開發中,咱們判斷二者是否相等的時候,使用的兩種方式
而咱們在比較字符串的時候會推薦使用equals,下面是String中的實現方式
public boolean equals(Object anObject) { if (this == anObject) { /**若是是當前對象,就直接返回true*/ 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; /**循環字符數組進行item的驗證*/ while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
方法實現上仍是比較簡單的。對比的char數組每一個值是否相等。
若是之後有這樣的需求:判斷二者是否相等?那麼咱們就能夠參考上面的實現,從底層結構出發,咱們就能夠經過底層結構快速想到貼合實際的思路。
因此說,看源碼重點是學習這種思路
那有人就會問了,既然equals比較的是具體內容,那麼==比較的是什麼呢?
其實,若是對比項是基本數據類型, 那麼對比的就是基本數據類型的值是否相等。好比 5 == 5 = true
還有對比的一點:對比的是內存空間地址是否相等。
這裏應該怎麼說呢?下面說
/* 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. */ public native String intern();
簡單一點來講,若是定義的字符串在常量池中定義過,而且經過equals()
對比相等的話,那麼就直接返回字符串在常量池中的內存地址給變量,看下圖
回到最初我給你們列出的那個面試題:
String s1 = "abc"; String s3 = "abc"; String s2 = new String("abc"); System.out.println(s1 == s3); true System.out.println(s1 == s2); false s2 = s2.intern(); System.out.println(s1 == s2); true
你們親自嘗試下,看看我輸出的對不對
講解這一點以前,須要先跟你們說明下String字符串在內存空間中的一個存放
String定義的字符串會存放到一個叫作常量池的地方,這個常量池在JDK1.7以後放在了堆空間中。
首先,s1="abc"
,會在常量池中開闢一塊空間存放字符串abc,而後將abc的引用地址指向s1。
接下來是s3="abc"
,這裏和以前有區別:若是常量池中存在當前字符串,那麼就直接將當前字符串的引用地址再指向定義的對象。若是不存在,就先存放字符串而後再指向引用地址
也就是說,s1和s3雖然定義了兩個變量,可是在內存空間中它們指向的地址都是同樣的,因此說s1==s3
s1仍是以前的s1
s2是經過new來定義出來的變量,這樣在堆空間中會開闢一塊新內存,構造方法傳入'abc'字符串,常量池中的abc字符串的引用地址會先指向堆空間開闢的內存空間,而後new出來的地址再指向s2,這和直接從常量池中引用的地址確定是不同的。
因此s1 != s2
可是,若是調用了
intern()
以後,s2的空間地址會直接指向常量池中字符串的地址,和s1的空間地址就是同樣的,因此s1和s2也就相等了這裏也從側面印證出了==還會對比內存空間中的引用地址是否相等
String s2 = new String("abc");
這裏就不用多說了吧,看上面的圖就知道了,建立了2個對象
上面咱們說到,String是不可變對象,在進行字符串操做時每次都會產生新的對象,這樣存在一些缺點:
針對這些問題,Java爲咱們提供了另外兩個新的操做字符串的類,
從功能和API方法上講,這兩個類沒有任何區別,都是用來操做字符串的類而且能夠屢次修改,並不會產生新的對象。
那爲何還會有兩個不一樣的類呢? 咱們經過源碼來看下其中的區別
首先,二者都繼承自 AbstractStringBuilder
並且,經過查看其父類源碼,咱們能夠發現一點:
abstract class AbstractStringBuilder implements Appendable, CharSequence { /** * The value is used for character storage. */ char[] value; AbstractStringBuilder(int capacity) { value = new char[capacity]; } }
StringBuffer和StringBuilder在底層存儲結構上,和String沒有區別,並且,兩類一樣被final
修飾,
惟一不一樣是:
StringBuffer stringBuffer = new StringBuffer(20); StringBuilder stringBuilder = new StringBuilder(20);
還有一點:相信咱們不少人都這樣寫過:
/** 無參方法 */ StringBuffer stringBuffer = new StringBuffer(); StringBuilder stringBuilder = new StringBuilder();
/** 默認初始值方法 */ StringBuffer stringBuffer = new StringBuffer("abc"); StringBuilder stringBuilder = new StringBuilder("abc");
直接看源碼,經過構造方法查看二者區別:
咱們抽出其中經常使用的一個方法來看看:append()
StringBuffer 的 append()
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
StringBuilder 的 append()
@Override public StringBuilder append(String str) { super.append(str); return this; }
經過查看二者的方法,很明顯的一個區別:
synchronized
, 並且若是咱們翻一下StringBuffer的源碼的話,咱們會發現其全部的方法都存在這個關鍵字對線程有了解的童鞋都知道這個關鍵字的意義:同步鎖,對應方式是同步方法
因此說,若是咱們在多線程環境下須要對字符串進行操做的話,那麼優先推薦採用StringBuffer,其餘方面的話,對效率沒要求二者都行,不然的話,就推薦採用StringBuilder
關於append(),這裏要額外說一點:
咱們前面說過,StringBuffer和StringBuilder構造方法是會傳遞數組的初始長度,那麼咱們來看看append方法是如何進行長度擴容的(沒錯,不止ArrayList會擴容):
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; } private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } }
針對數組擴容,很簡單的邏輯:
到這裏,咱們關於字符串的內容也就完結了,關於具體的API方法,推薦直接查看官方文檔: