Java 基礎常見知識點&面試題總結(中),2022 最新版!| JavaGuide

JavaGuide 發表於 2022-06-13
Java 面試

你好,我是 Guide。秋招即將到來,我對 JavaGuide 的內容進行了重構完善,公眾號同步一下最新更新,希望能夠幫助你。

上篇:Java 基礎常見知識點&面試題總結(上),2022 最新版!

原文地址: https://javaguide.cn/java/basis/java-basic-questions-02.html(如果文章圖片顯示異常,請點此連結閱讀)

物件導向基礎

物件導向和麵向過程的區別

兩者的主要區別在於解決問題的方式不同:

  • 程式導向把解決問題的過程拆成一個個方法,通過一個個方法的執行解決問題。
  • 物件導向會先抽象出物件,然後用物件執行方法的方式解決問題。

另外,物件導向開發的程式一般更易維護、易複用、易擴充套件。

相關 issue : 程式導向 :程式導向效能比物件導向高??

成員變數與區域性變數的區別

  • 語法形式 :從語法形式上看,成員變數是屬於類的,而區域性變數是在程式碼塊或方法中定義的變數或是方法的引數;成員變數可以被 public,private,static 等修飾符所修飾,而區域性變數不能被訪問控制修飾符及 static 所修飾;但是,成員變數和區域性變數都能被 final 所修飾。
  • 儲存方式 :從變數在記憶體中的儲存方式來看,如果成員變數是使用 static 修飾的,那麼這個成員變數是屬於類的,如果沒有使用 static 修飾,這個成員變數是屬於例項的。而物件存在於堆記憶體,區域性變數則存在於棧記憶體。
  • 生存時間 :從變數在記憶體中的生存時間上看,成員變數是物件的一部分,它隨著物件的建立而存在,而區域性變數隨著方法的呼叫而自動生成,隨著方法的呼叫結束而消亡。
  • 預設值 :從變數是否有預設值來看,成員變數如果沒有被賦初始值,則會自動以型別的預設值而賦值(一種情況例外:被 final 修飾的成員變數也必須顯式地賦值),而區域性變數則不會自動賦值。

建立一個物件用什麼運算子?物件實體與物件引用有何不同?

new 運算子,new 建立物件例項(物件例項在堆記憶體中),物件引用指向物件例項(物件引用存放在棧記憶體中)。

一個物件引用可以指向 0 個或 1 個物件(一根繩子可以不繫氣球,也可以系一個氣球);一個物件可以有 n 個引用指向它(可以用 n 條繩子繫住一個氣球)。

物件的相等和引用相等的區別

  • 物件的相等一般比較的是記憶體中存放的內容是否相等。
  • 引用相等一般比較的是他們指向的記憶體地址是否相等。

類的構造方法的作用是什麼?

構造方法是一種特殊的方法,主要作用是完成物件的初始化工作。

如果一個類沒有宣告構造方法,該程式能正確執行嗎?

如果一個類沒有宣告構造方法,也可以執行!因為一個類即使沒有宣告構造方法也會有預設的不帶引數的構造方法。如果我們自己新增了類的構造方法(無論是否有參),Java 就不會再新增預設的無引數的構造方法了,我們一直在不知不覺地使用構造方法,這也是為什麼我們在建立物件的時候後面要加一個括號(因為要呼叫無參的構造方法)。如果我們過載了有參的構造方法,記得都要把無參的構造方法也寫出來(無論是否用到),因為這可以幫助我們在建立物件的時候少踩坑。

構造方法有哪些特點?是否可被 override?

構造方法特點如下:

  • 名字與類名相同。
  • 沒有返回值,但不能用 void 宣告建構函式。
  • 生成類的物件時自動執行,無需呼叫。

構造方法不能被 override(重寫),但是可以 overload(過載),所以你可以看到一個類中有多個建構函式的情況。

物件導向三大特徵

封裝

封裝是指把一個物件的狀態資訊(也就是屬性)隱藏在物件內部,不允許外部物件直接訪問物件的內部資訊。但是可以提供一些可以被外界訪問的方法來操作屬性。就好像我們看不到掛在牆上的空調的內部的零件資訊(也就是屬性),但是可以通過遙控器(方法)來控制空調。如果屬性不想被外界訪問,我們大可不必提供方法給外界訪問。但是如果一個類沒有提供給外界訪問的方法,那麼這個類也沒有什麼意義了。就好像如果沒有空調遙控器,那麼我們就無法操控空凋製冷,空調本身就沒有意義了(當然現在還有很多其他方法 ,這裡只是為了舉例子)。

public class Student {
    private int id;//id屬性私有化
    private String name;//name屬性私有化

    //獲取id的方法
    public int getId() {
        return id;
    }

    //設定id的方法
    public void setId(int id) {
        this.id = id;
    }

    //獲取name的方法
    public String getName() {
        return name;
    }

    //設定name的方法
    public void setName(String name) {
        this.name = name;
    }
}

繼承

不同型別的物件,相互之間經常有一定數量的共同點。例如,小明同學、小紅同學、小李同學,都共享學生的特性(班級、學號等)。同時,每一個物件還定義了額外的特性使得他們與眾不同。例如小明的數學比較好,小紅的性格惹人喜愛;小李的力氣比較大。繼承是使用已存在的類的定義作為基礎建立新類的技術,新類的定義可以增加新的資料或新的功能,也可以用父類的功能,但不能選擇性地繼承父類。通過使用繼承,可以快速地建立新的類,可以提高程式碼的重用,程式的可維護性,節省大量建立新類的時間 ,提高我們的開發效率。

關於繼承如下 3 點請記住:

  1. 子類擁有父類物件所有的屬性和方法(包括私有屬性和私有方法),但是父類中的私有屬性和方法子類是無法訪問,只是擁有
  2. 子類可以擁有自己屬性和方法,即子類可以對父類進行擴充套件。
  3. 子類可以用自己的方式實現父類的方法。(以後介紹)。

多型

多型,顧名思義,表示一個物件具有多種的狀態,具體表現為父類的引用指向子類的例項。

多型的特點:

  • 物件型別和引用型別之間具有繼承(類)/實現(介面)的關係;
  • 引用型別變數發出的方法呼叫的到底是哪個類中的方法,必須在程式執行期間才能確定;
  • 多型不能呼叫“只在子類存在但在父類不存在”的方法;
  • 如果子類重寫了父類的方法,真正執行的是子類覆蓋的方法,如果子類沒有覆蓋父類的方法,執行的是父類的方法。

介面和抽象類有什麼共同點和區別?

共同點

  • 都不能被例項化。
  • 都可以包含抽象方法。
  • 都可以有預設實現的方法(Java 8 可以用 default 關鍵字在介面中定義預設方法)。

區別

  • 介面主要用於對類的行為進行約束,你實現了某個介面就具有了對應的行為。抽象類主要用於程式碼複用,強調的是所屬關係(比如說我們抽象了一個傳送簡訊的抽象類,)。
  • 一個類只能繼承一個類,但是可以實現多個介面。
  • 介面中的成員變數只能是 public static final 型別的,不能被修改且必須有初始值,而抽象類的成員變數預設 default,可在子類中被重新定義,也可被重新賦值。

深拷貝和淺拷貝區別瞭解嗎?什麼是引用拷貝?

關於深拷貝和淺拷貝區別,我這裡先給結論:

  • 淺拷貝:淺拷貝會在堆上建立一個新的物件(區別於引用拷貝的一點),不過,如果原物件內部的屬性是引用型別的話,淺拷貝會直接複製內部物件的引用地址,也就是說拷貝物件和原物件共用同一個內部物件。
  • 深拷貝 :深拷貝會完全複製整個物件,包括這個物件所包含的內部物件。

上面的結論沒有完全理解的話也沒關係,我們來看一個具體的案例!

淺拷貝

淺拷貝的示例程式碼如下,我們這裡實現了 Cloneable 介面,並重寫了 clone() 方法。

clone() 方法的實現很簡單,直接呼叫的是父類 Objectclone() 方法。

public class Address implements Cloneable{
    private String name;
    // 省略建構函式、Getter&Setter方法
    @Override
    public Address clone() {
        try {
            return (Address) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

public class Person implements Cloneable {
    private Address address;
    // 省略建構函式、Getter&Setter方法
    @Override
    public Person clone() {
        try {
            Person person = (Person) super.clone();
            return person;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

測試 :

Person person1 = new Person(new Address("武漢"));
Person person1Copy = person1.clone();
// true
System.out.println(person1.getAddress() == person1Copy.getAddress());

從輸出結構就可以看出, person1 的克隆物件和 person1 使用的仍然是同一個 Address 物件。

深拷貝

這裡我們簡單對 Person 類的 clone() 方法進行修改,連帶著要把 Person 物件內部的 Address 物件一起復制。

@Override
public Person clone() {
    try {
        Person person = (Person) super.clone();
        person.setAddress(person.getAddress().clone());
        return person;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

測試 :

Person person1 = new Person(new Address("武漢"));
Person person1Copy = person1.clone();
// false
System.out.println(person1.getAddress() == person1Copy.getAddress());

從輸出結構就可以看出,雖然 person1 的克隆物件和 person1 包含的 Address 物件已經是不同的了。

那什麼是引用拷貝呢? 簡單來說,引用拷貝就是兩個不同的引用指向同一個物件。

我專門畫了一張圖來描述淺拷貝、深拷貝、引用拷貝:

Java 基礎常見知識點&面試題總結(中),2022 最新版!| JavaGuide

Java 常見類

Object

Object 類的常見方法有哪些?

Object 類是一個特殊的類,是所有類的父類。它主要提供了以下 11 個方法:

/**
 * native 方法,用於返回當前執行時物件的 Class 物件,使用了 final 關鍵字修飾,故不允許子類重寫。
 */
public final native Class<?> getClass()
/**
 * native 方法,用於返回物件的雜湊碼,主要使用在雜湊表中,比如 JDK 中的HashMap。
 */
public native int hashCode()
/**
 * 用於比較 2 個物件的記憶體地址是否相等,String 類對該方法進行了重寫以用於比較字串的值是否相等。
 */
public boolean equals(Object obj)
/**
 * naitive 方法,用於建立並返回當前物件的一份拷貝。
 */
protected native Object clone() throws CloneNotSupportedException
/**
 * 返回類的名字例項的雜湊碼的 16 進位制的字串。建議 Object 所有的子類都重寫這個方法。
 */
public String toString()
/**
 * native 方法,並且不能重寫。喚醒一個在此物件監視器上等待的執行緒(監視器相當於就是鎖的概念)。如果有多個執行緒在等待只會任意喚醒一個。
 */
public final native void notify()
/**
 * native 方法,並且不能重寫。跟 notify 一樣,唯一的區別就是會喚醒在此物件監視器上等待的所有執行緒,而不是一個執行緒。
 */
public final native void notifyAll()
/**
 * native方法,並且不能重寫。暫停執行緒的執行。注意:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 ,timeout 是等待時間。
 */
public final native void wait(long timeout) throws InterruptedException
/**
 * 多了 nanos 引數,這個參數列示額外時間(以毫微秒為單位,範圍是 0-999999)。 所以超時的時間還需要加上 nanos 毫秒。。
 */
public final void wait(long timeout, int nanos) throws InterruptedException
/**
 * 跟之前的2個wait方法一樣,只不過該方法一直等待,沒有超時時間這個概念
 */
public final void wait() throws InterruptedException
/**
 * 例項被垃圾回收器回收的時候觸發的操作
 */
protected void finalize() throws Throwable { }

== 和 equals() 的區別

== 對於基本型別和引用型別的作用效果是不同的:

  • 對於基本資料型別來說,== 比較的是值。
  • 對於引用資料型別來說,== 比較的是物件的記憶體地址。

因為 Java 只有值傳遞,所以,對於 == 來說,不管是比較基本資料型別,還是引用資料型別的變數,其本質比較的都是值,只是引用型別變數存的值是物件的地址。

equals() 不能用於判斷基本資料型別的變數,只能用來判斷兩個物件是否相等。equals()方法存在於Object類中,而Object類是所有類的直接或間接父類,因此所有的類都有equals()方法。

Objectequals() 方法:

public boolean equals(Object obj) {
     return (this == obj);
}

equals() 方法存在兩種使用情況:

  • 類沒有重寫 equals()方法 :通過equals()比較該類的兩個物件時,等價於通過“==”比較這兩個物件,使用的預設是 Objectequals()方法。
  • 類重寫了 equals()方法 :一般我們都重寫 equals()方法來比較兩個物件中的屬性是否相等;若它們的屬性相等,則返回 true(即,認為這兩個物件相等)。

舉個例子(這裡只是為了舉例。實際上,你按照下面這種寫法的話,像 IDEA 這種比較智慧的 IDE 都會提示你將 == 換成 equals() ):

String a = new String("ab"); // a 為一個引用
String b = new String("ab"); // b為另一個引用,物件的內容一樣
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 從常量池中查詢
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true

String 中的 equals 方法是被重寫過的,因為 Objectequals 方法是比較的物件的記憶體地址,而 Stringequals 方法比較的是物件的值。

當建立 String 型別的物件時,虛擬機器會在常量池中查詢有沒有已經存在的值和要建立的值相同的物件,如果有就把它賦給當前引用。如果沒有就在常量池中重新建立一個 String 物件。

Stringequals()方法:

public boolean equals(Object anObject) {
    if (this == anObject) {
        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;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

hashCode() 有什麼用?

hashCode() 的作用是獲取雜湊碼(int 整數),也稱為雜湊碼。這個雜湊碼的作用是確定該物件在雜湊表中的索引位置。

hashCode()定義在 JDK 的 Object 類中,這就意味著 Java 中的任何類都包含有 hashCode() 函式。另外需要注意的是: ObjecthashCode() 方法是本地方法,也就是用 C 語言或 C++ 實現的,該方法通常用來將物件的記憶體地址轉換為整數之後返回。

public native int hashCode();

雜湊表儲存的是鍵值對(key-value),它的特點是:能根據“鍵”快速的檢索出對應的“值”。這其中就利用到了雜湊碼!(可以快速找到所需要的物件)

為什麼要有 hashCode?

我們以“HashSet 如何檢查重複”為例子來說明為什麼要有 hashCode

下面這段內容摘自我的 Java 啟蒙書《Head First Java》:

當你把物件加入 HashSet 時,HashSet 會先計算物件的 hashCode 值來判斷物件加入的位置,同時也會與其他已經加入的物件的 hashCode 值作比較,如果沒有相符的 hashCodeHashSet 會假設物件沒有重複出現。但是如果發現有相同 hashCode 值的物件,這時會呼叫 equals() 方法來檢查 hashCode 相等的物件是否真的相同。如果兩者相同,HashSet 就不會讓其加入操作成功。如果不同的話,就會重新雜湊到其他位置。這樣我們就大大減少了 equals 的次數,相應就大大提高了執行速度。

其實, hashCode()equals()都是用於比較兩個物件是否相等。

那為什麼 JDK 還要同時提供這兩個方法呢?

這是因為在一些容器(比如 HashMapHashSet)中,有了 hashCode() 之後,判斷元素是否在對應容器中的效率會更高(參考新增元素進HashSet的過程)!

我們在前面也提到了新增元素進HashSet的過程,如果 HashSet 在對比的時候,同樣的 hashCode 有多個物件,它會繼續使用 equals() 來判斷是否真的相同。也就是說 hashCode 幫助我們大大縮小了查詢成本。

那為什麼不只提供 hashCode() 方法呢?

這是因為兩個物件的hashCode 值相等並不代表兩個物件就相等。

那為什麼兩個物件有相同的 hashCode 值,它們也不一定是相等的?

因為 hashCode() 所使用的雜湊演算法也許剛好會讓多個物件傳回相同的雜湊值。越糟糕的雜湊演算法越容易碰撞,但這也與資料值域分佈的特性有關(所謂雜湊碰撞也就是指的是不同的物件得到相同的 hashCode )。

總結下來就是 :

  • 如果兩個物件的hashCode 值相等,那這兩個物件不一定相等(雜湊碰撞)。
  • 如果兩個物件的hashCode 值相等並且equals()方法也返回 true,我們才認為這兩個物件相等。
  • 如果兩個物件的hashCode 值不相等,我們就可以直接認為這兩個物件不相等。

相信大家看了我前面對 hashCode()equals() 的介紹之後,下面這個問題已經難不倒你們了。

為什麼重寫 equals() 時必須重寫 hashCode() 方法?

因為兩個相等的物件的 hashCode 值必須是相等。也就是說如果 equals 方法判斷兩個物件是相等的,那這兩個物件的 hashCode 值也要相等。

如果重寫 equals() 時沒有重寫 hashCode() 方法的話就可能會導致 equals 方法判斷是相等的兩個物件,hashCode 值卻不相等。

思考 :重寫 equals() 時沒有重寫 hashCode() 方法的話,使用 HashMap 可能會出現什麼問題。

總結

  • equals 方法判斷兩個物件是相等的,那這兩個物件的 hashCode 值也要相等。
  • 兩個物件有相同的 hashCode 值,他們也不一定是相等的(雜湊碰撞)。

更多關於 hashCode()equals() 的內容可以檢視:Java hashCode() 和 equals()的若干問題解答

String

String、StringBuffer、StringBuilder 的區別?

可變性

String 是不可變的(後面會詳細分析原因)。

StringBuilderStringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字元陣列儲存字串,不過沒有使用 finalprivate 關鍵字修飾,最關鍵的是這個 AbstractStringBuilder 類還提供了很多修改字串的方法比如 append 方法。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    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;
    }
  	//...
}

執行緒安全性

String 中的物件是不可變的,也就可以理解為常量,執行緒安全。AbstractStringBuilderStringBuilderStringBuffer 的公共父類,定義了一些字串的基本操作,如 expandCapacityappendinsertindexOf 等公共方法。StringBuffer 對方法加了同步鎖或者對呼叫的方法加了同步鎖,所以是執行緒安全的。StringBuilder 並沒有對方法進行加同步鎖,所以是非執行緒安全的。

效能

每次對 String 型別進行改變的時候,都會生成一個新的 String 物件,然後將指標指向新的 String 物件。StringBuffer 每次都會對 StringBuffer 物件本身進行操作,而不是生成新的物件並改變物件引用。相同情況下使用 StringBuilder 相比使用 StringBuffer 僅能獲得 10%~15% 左右的效能提升,但卻要冒多執行緒不安全的風險。

對於三者使用的總結:

  1. 操作少量的資料: 適用 String
  2. 單執行緒操作字串緩衝區下操作大量資料: 適用 StringBuilder
  3. 多執行緒操作字串緩衝區下操作大量資料: 適用 StringBuffer

String 為什麼是不可變的?

String 類中使用 final 關鍵字修飾字元陣列來儲存字串,所以String 物件是不可變的。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
	//...
}

🐛 修正 : 我們知道被 final 關鍵字修飾的類不能被繼承,修飾的方法不能被重寫,修飾的變數是基本資料型別則值不能改變,修飾的變數是引用型別則不能再指向其他物件。因此,final 關鍵字修飾的陣列儲存字串並不是 String 不可變的根本原因,因為這個陣列儲存的字串是可變的(final 修飾引用型別變數的情況)。

String 真正不可變有下面幾點原因:

  1. 儲存字串的陣列被 final 修飾且為私有的,並且String 類沒有提供/暴露修改這個字串的方法。
  2. String 類被 final 修飾導致其不能被繼承,進而避免了子類破壞 String 不可變。

相關閱讀:如何理解 String 型別值的不可變? - 知乎提問

補充(來自issue 675):在 Java 9 之後,StringStringBuilderStringBuffer 的實現改用 byte 陣列儲存字串。

public final class String implements java.io.Serializable,Comparable<String>, CharSequence {
    // @Stable 註解表示變數最多被修改一次,稱為“穩定的”。
    @Stable
    private final byte[] value;
}

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    byte[] value;

}

Java 9 為何要將 String 的底層實現由 char[] 改成了 byte[] ?

新版的 String 其實支援兩個編碼方案: Latin-1 和 UTF-16。如果字串中包含的漢字沒有超過 Latin-1 可表示範圍內的字元,那就會使用 Latin-1 作為編碼方案。Latin-1 編碼方案下,byte 佔一個位元組(8 位),char 佔用 2 個位元組(16),byte 相較 char 節省一半的記憶體空間。

JDK 官方就說了絕大部分字串物件只包含 Latin-1 可表示的字元。

Java 基礎常見知識點&面試題總結(中),2022 最新版!| JavaGuide

如果字串中包含的漢字超過 Latin-1 可表示範圍內的字元,bytechar 所佔用的空間是一樣的。

這是官方的介紹:https://openjdk.java.net/jeps/254

字串拼接用“+” 還是 StringBuilder?

Java 語言本身並不支援運算子過載,“+”和“+=”是專門為 String 類過載過的運算子,也是 Java 中僅有的兩個過載過的元素符。

String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;

上面的程式碼對應的位元組碼如下:

Java 基礎常見知識點&面試題總結(中),2022 最新版!| JavaGuide

可以看出,字串物件通過“+”的字串拼接方式,實際上是通過 StringBuilder 呼叫 append() 方法實現的,拼接完成之後呼叫 toString() 得到一個 String 物件 。

不過,在迴圈內使用“+”進行字串的拼接的話,存在比較明顯的缺陷:編譯器不會建立單個 StringBuilder 以複用,會導致建立過多的 StringBuilder 物件

String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {
    s += arr[i];
}
System.out.println(s);

StringBuilder 物件是在迴圈內部被建立的,這意味著每迴圈一次就會建立一個 StringBuilder 物件。

Java 基礎常見知識點&面試題總結(中),2022 最新版!| JavaGuide

如果直接使用 StringBuilder 物件進行字串拼接的話,就不會存在這個問題了。

String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {
    s.append(value);
}
System.out.println(s);

Java 基礎常見知識點&面試題總結(中),2022 最新版!| JavaGuide

如果你使用 IDEA 的話,IDEA 自帶的程式碼檢查機制也會提示你修改程式碼。

String#equals() 和 Object#equals() 有何區別?

String 中的 equals 方法是被重寫過的,比較的是 String 字串的值是否相等。 Objectequals 方法是比較的物件的記憶體地址。

字串常量池的作用瞭解嗎?

字串常量池 是 JVM 為了提升效能和減少記憶體消耗針對字串(String 類)專門開闢的一塊區域,主要目的是為了避免字串的重複建立。

// 在堆中建立字串物件”ab“
// 將字串物件”ab“的引用儲存在字串常量池中
String aa = "ab";
// 直接返回字串常量池中字串物件”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

更多關於字串常量池的介紹可以看一下 Java 記憶體區域詳解 這篇文章。

String s1 = new String("abc");這句話建立了幾個字串物件?

會建立 1 或 2 個字串物件。

1、如果字串常量池中不存在字串物件“abc”的引用,那麼會在堆中建立 2 個字串物件“abc”。

示例程式碼(JDK 1.8):

String s1 = new String("abc");

對應的位元組碼:

Java 基礎常見知識點&面試題總結(中),2022 最新版!| JavaGuide

ldc 命令用於判斷字串常量池中是否儲存了對應的字串物件的引用,如果儲存了的話直接返回,如果沒有儲存的話,會在堆中建立對應的字串物件並將該字串物件的引用儲存到字串常量池中。

2、如果字串常量池中已存在字串物件“abc”的引用,則只會在堆中建立 1 個字串物件“abc”。

示例程式碼(JDK 1.8):

// 字串常量池中已存在字串物件“abc”的引用
String s1 = "abc";
// 下面這段程式碼只會在堆中建立 1 個字串物件“abc”
String s2 = new String("abc");

對應的位元組碼:

Java 基礎常見知識點&面試題總結(中),2022 最新版!| JavaGuide

這裡就不對上面的位元組碼進行詳細註釋了,7 這個位置的 ldc 命令不會在堆中建立新的字串物件“abc”,這是因為 0 這個位置已經執行了一次 ldc 命令,已經在堆中建立過一次字串物件“abc”了。7 這個位置執行 ldc 命令會直接返回字串常量池中字串物件“abc”對應的引用。

intern 方法有什麼作用?

String.intern() 是一個 native(本地)方法,其作用是將指定的字串物件的引用儲存在字串常量池中,可以簡單分為兩種情況:

  • 如果字串常量池中儲存了對應的字串物件的引用,就直接返回該引用。
  • 如果字串常量池中沒有儲存了對應的字串物件的引用,那就在常量池中建立一個指向該字串物件的引用並返回。

示例程式碼(JDK 1.8) :

// 在堆中建立字串物件”Java“
// 將字串物件”Java“的引用儲存在字串常量池中
String s1 = "Java";
// 直接返回字串常量池中字串物件”Java“對應的引用
String s2 = s1.intern();
// 會在堆中在單獨建立一個字串物件
String s3 = new String("Java");
// 直接返回字串常量池中字串物件”Java“對應的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一個物件
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的物件
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一個物件
System.out.println(s1 == s4); //true

String 型別的變數和常量做“+”運算時發生了什麼?

先來看字串不加 final 關鍵字拼接的情況(JDK1.8):

String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false

注意 :比較 String 字串的值是否相等,可以使用 equals() 方法。 String 中的 equals 方法是被重寫過的。 Objectequals 方法是比較的物件的記憶體地址,而 Stringequals 方法比較的是字串的值是否相等。如果你使用 == 比較兩個字串是否相等的話,IDEA 還是提示你使用 equals() 方法替換。

Java 基礎常見知識點&面試題總結(中),2022 最新版!| JavaGuide

對於編譯期可以確定值的字串,也就是常量字串 ,jvm 會將其存入字串常量池。並且,字串常量拼接得到的字串常量在編譯階段就已經被存放字串常量池,這個得益於編譯器的優化。

在編譯過程中,Javac 編譯器(下文中統稱為編譯器)會進行一個叫做 常量摺疊(Constant Folding) 的程式碼優化。《深入理解 Java 虛擬機器》中是也有介紹到:

Java 基礎常見知識點&面試題總結(中),2022 最新版!| JavaGuide

常量摺疊會把常量表示式的值求出來作為常量嵌在最終生成的程式碼中,這是 Javac 編譯器會對原始碼做的極少量優化措施之一(程式碼優化幾乎都在即時編譯器中進行)。

對於 String str3 = "str" + "ing"; 編譯器會給你優化成 String str3 = "string";

並不是所有的常量都會進行摺疊,只有編譯器在程式編譯期就可以確定值的常量才可以:

  • 基本資料型別( bytebooleanshortcharintfloatlongdouble)以及字串常量。
  • final 修飾的基本資料型別和字串變數
  • 字串通過 “+”拼接得到的字串、基本資料型別之間算數運算(加減乘除)、基本資料型別的位運算(<<、>>、>>> )

引用的值在程式編譯期是無法確定的,編譯器無法對其進行優化。

物件引用和“+”的字串拼接方式,實際上是通過 StringBuilder 呼叫 append() 方法實現的,拼接完成之後呼叫 toString() 得到一個 String 物件 。

String str4 = new StringBuilder().append(str1).append(str2).toString();

我們在平時寫程式碼的時候,儘量避免多個字串物件拼接,因為這樣會重新建立物件。如果需要改變字串的話,可以使用 StringBuilder 或者 StringBuffer

不過,字串使用 final 關鍵字宣告之後,可以讓編譯器當做常量來處理。

示例程式碼:

final String str1 = "str";
final String str2 = "ing";
// 下面兩個表示式其實是等價的
String c = "str" + "ing";// 常量池中的物件
String d = str1 + str2; // 常量池中的物件
System.out.println(c == d);// true

final 關鍵字修改之後的 String 會被編譯器當做常量來處理,編譯器在程式編譯期就可以確定它的值,其效果就相當於訪問常量。

如果 ,編譯器在執行時才能知道其確切值的話,就無法對其優化。

示例程式碼(str2 在執行時才能確定其值):

final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的物件
String d = str1 + str2; // 在堆上建立的新的物件
System.out.println(c == d);// false
public static String getStr() {
      return "ing";
}

參考

後記

近期文章精選

走近作者

如果本文對你有幫助的話,歡迎點贊&在看&分享,這對我繼續分享&創作優質文章非常重要。感謝🙏🏻

相關文章