Java基礎-從新認識字符串

來,進來的Java程序猿,咱們認識一下。html

我是俗世遊子,在外流浪多年的Java程序猿java

從新認識String

首先,咱們來看個小栗子,保證你看了既陌生又熟悉:面試

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

這裏,我想到了一道常見的面試題:已上面爲例安全

  1. s1 == s2 ?
  2. s1 == s3 ?
  3. s2一共建立了幾個對象?

並且,我想讓你們想想,若是讓大家來介紹String,會如何介紹呢?多線程

  • String 類型不屬於Java的8種基本數據類型,類不可被繼承
  • String是一個不可變對象,API方法返回的實際上是一個新的String對象oracle

  • 底層經過char類型的數組來存儲

好,那麼接下來咱們就好好的剖析下這個String類型app

從源碼看String

總體結構

首先,咱們來看看String的結構圖ide

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;

    public String() {
        this.value = "".value;
    }
    // 其餘的構造方法
}

咱們能夠看到,整個String類被final修飾,咱們都知道,被final修飾過的類或者對象

  • 不可被繼承

咱們經過查看其構造方法能夠發現,一樣被final修飾的char數組存儲着咱們定義的字符串,看下圖也能夠看得出字符串的一個存儲結構

String底層結構

因此說咱們經過chatAt()方法的下標可以獲得指定的字符,緣由就在於字符串是經過char數組來儲存的。

一樣的,char數組被final修飾,權限是private,並且沒有提供對外設置的方法

  • String是一個不可變的對象

可是咱們要明白一點,這裏的不可變指的是:底層存儲char數組在內存中地址引用不可變,可是其自己的內容咱們是能夠經過一些手段來改變的,好比:反射

若是咱們之後也想實現一個不可變的類,就能夠參考String來實現

具體方法

咱們在看源碼的時候,咱們要重點看一看做者在實現一些方法時的思路,有什麼地方的想法是咱們能夠借鑑的,好用在咱們之後的代碼設計中(逼格高)

做爲重寫方法出現頻率最高的兩個方法,咱們就看String是如何實現的

1. hashCode()

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,在計算上能夠進行移位操做,效率較高

2. equals()

在Java開發中,咱們判斷二者是否相等的時候,使用的兩種方式

  • ==
  • equals

而咱們在比較字符串的時候會推薦使用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

還有對比的一點:對比的是內存空間地址是否相等。

這裏應該怎麼說呢?下面說

3. intern()

/* 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()對比相等的話,那麼就直接返回字符串在常量池中的內存地址給變量,看下圖

intern

從實際案例分析String

回到最初我給你們列出的那個面試題:

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

你們親自嘗試下,看看我輸出的對不對

s1 == s3

講解這一點以前,須要先跟你們說明下String字符串在內存空間中的一個存放

s1==s3

String定義的字符串會存放到一個叫作常量池的地方,這個常量池在JDK1.7以後放在了堆空間中。

首先,s1="abc",會在常量池中開闢一塊空間存放字符串abc,而後將abc的引用地址指向s1。

接下來是s3="abc",這裏和以前有區別:若是常量池中存在當前字符串,那麼就直接將當前字符串的引用地址再指向定義的對象。若是不存在,就先存放字符串而後再指向引用地址

也就是說,s1和s3雖然定義了兩個變量,可是在內存空間中它們指向的地址都是同樣的,因此說s1==s3

s1 != s2

s1!=s2

s1仍是以前的s1

s2是經過new來定義出來的變量,這樣在堆空間中會開闢一塊新內存,構造方法傳入'abc'字符串,常量池中的abc字符串的引用地址會先指向堆空間開闢的內存空間,而後new出來的地址再指向s2,這和直接從常量池中引用的地址確定是不同的。

因此s1 != s2

可是,若是調用了intern()以後,s2的空間地址會直接指向常量池中字符串的地址,和s1的空間地址就是同樣的,因此s1和s2也就相等了

這裏也從側面印證出了==還會對比內存空間中的引用地址是否相等

s2 建立了幾個對象

String s2 = new String("abc");

這裏就不用多說了吧,看上面的圖就知道了,建立了2個對象

可修改字符串

上面咱們說到,String是不可變對象,在進行字符串操做時每次都會產生新的對象,這樣存在一些缺點:

  • 操做效率低下,每次操做生成新的對象,而後變量的引用地址也要跟着變化
  • 內存浪費嚴重

針對這些問題,Java爲咱們提供了另外兩個新的操做字符串的類,

  • StringBuffer
  • StringBuilder

從功能和API方法上講,這兩個類沒有任何區別,都是用來操做字符串的類而且能夠屢次修改,並不會產生新的對象。

那爲何還會有兩個不一樣的類呢? 咱們經過源碼來看下其中的區別

特性

結構

首先,二者都繼承自 AbstractStringBuilder

StringBuffer組成

StringBuilder組成

並且,經過查看其父類源碼,咱們能夠發現一點:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

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

StringBufferStringBuilder在底層存儲結構上,和String沒有區別,並且,兩類一樣被final修飾,

  • 一樣說明了該類不可被繼承

惟一不一樣是:

  • 咱們能夠指定該char數組的初始長度,好比
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");

直接看源碼,經過構造方法查看二者區別:

  • 若是是無參構造方法的話,那麼char數組的初始長度爲:16
  • 若是添加了默認初始值的構造方法的話,那麼char數組的初始長度爲:當前字符串的長度 + 16

線程安全性

咱們抽出其中經常使用的一個方法來看看: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;
}

經過查看二者的方法,很明顯的一個區別:

  • StringBuffer 的方法存在 synchronized, 並且若是咱們翻一下StringBuffer的源碼的話,咱們會發現其全部的方法都存在這個關鍵字

對線程有了解的童鞋都知道這個關鍵字的意義:同步鎖,對應方式是同步方法

  • 表示當前方法是線程安全的方法
  • StringBuilder方法進行對比的話,那麼在執行效率上就略低一些

因此說,若是咱們在多線程環境下須要對字符串進行操做的話,那麼優先推薦採用StringBuffer,其餘方面的話,對效率沒要求二者都行,不然的話,就推薦採用StringBuilder

關於append(),這裏要額外說一點:

咱們前面說過,StringBufferStringBuilder構造方法是會傳遞數組的初始長度,那麼咱們來看看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方法,推薦直接查看官方文檔:

StringBuffer文檔介紹

StringBuilder文檔介紹

String文檔介紹

相關文章
相關標籤/搜索