Java基礎常見知識&面試題總結2

面向對象基礎

面向對象和面向過程的區(qū)別

兩者的主要區(qū)別在于解決問題的方式不同:

  • 面向過程把解決問題的過程拆成一個個方法,通過一個個方法的執(zhí)行解決問題。
  • 面向對象會先抽象出對象,然后用對象執(zhí)行方法的方式解決問題。

另外,面向對象開發(fā)的程序一般更易維護、易復用、易擴展。

成員變量與局部變量的區(qū)別

  • 語法形式 :從語法形式上看,成員變量是屬于類的,而局部變量是在代碼塊或方法中定義的變量或是方法的參數(shù);成員變量可以被 public,private,static 等修飾符所修飾,而局部變量不能被訪問控制修飾符及 static 所修飾;但是,成員變量和局部變量都能被 final 所修飾。
  • 存儲方式 :從變量在內存中的存儲方式來看,如果成員變量是使用 static 修飾的,那么這個成員變量是屬于類的,如果沒有使用 static 修飾,這個成員變量是屬于實例的。而對象存在于堆內存,局部變量則存在于棧內存。
  • 生存時間 :從變量在內存中的生存時間上看,成員變量是對象的一部分,它隨著對象的創(chuàng)建而存在,而局部變量隨著方法的調用而自動生成,隨著方法的調用結束而消亡。
  • 默認值 :從變量是否有默認值來看,成員變量如果沒有被賦初始值,則會自動以類型的默認值而賦值(一種情況例外:被 final 修飾的成員變量也必須顯式地賦值),而局部變量則不會自動賦值。

創(chuàng)建一個對象用什么運算符?對象實體與對象引用有何不同?

new 運算符,new 創(chuàng)建對象實例(對象實例在堆內存中),對象引用指向對象實例(對象引用存放在棧內存中)。

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

對象的相等和引用相等的區(qū)別

  • 對象的相等一般比較的是內存中存放的內容是否相等。
  • 引用相等一般比較的是他們指向的內存地址是否相等。

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

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

如果一個類沒有聲明構造方法,該程序能正確執(zhí)行嗎?

如果一個類沒有聲明構造方法,也可以執(zhí)行!因為一個類即使沒有聲明構造方法也會有默認的不帶參數(shù)的構造方法。如果我們自己添加了類的構造方法(無論是否有參),Java 就不會再添加默認的無參數(shù)的構造方法了,這時候,就不能直接 new 一個對象而不傳遞參數(shù)了,所以我們一直在不知不覺地使用構造方法,這也是為什么我們在創(chuàng)建對象的時候后面要加一個括號(因為要調用無參的構造方法)。如果我們重載了有參的構造方法,記得都要把無參的構造方法也寫出來(無論是否用到),因為這可以幫助我們在創(chuàng)建對象的時候少踩坑。

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

構造方法特點如下:

  • 名字與類名相同。
  • 沒有返回值,但不能用 void 聲明構造函數(shù)。
  • 生成類的對象時自動執(zhí)行,無需調用。

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

面向對象三大特征

封裝

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

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;
    }
}

繼承

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

關于繼承如下 3 點請記?。?/strong>

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

多態(tài)

多態(tài),顧名思義,表示一個對象具有多種的狀態(tài),具體表現(xiàn)為父類的引用指向子類的實例。

多態(tài)的特點:

  • 對象類型和引用類型之間具有繼承(類)/實現(xiàn)(接口)的關系;
  • 引用類型變量發(fā)出的方法調用的到底是哪個類中的方法,必須在程序運行期間才能確定;
  • 多態(tài)不能調用“只在子類存在但在父類不存在”的方法;
  • 如果子類重寫了父類的方法,真正執(zhí)行的是子類覆蓋的方法,如果子類沒有覆蓋父類的方法,執(zhí)行的是父類的方法。

接口和抽象類有什么共同點和區(qū)別?

共同點

  • 都不能被實例化。
  • 都可以包含抽象方法。
  • 都可以有默認實現(xiàn)的方法(Java 8 可以用 default 關鍵在接口中定義默認方法)。

區(qū)別

  • 接口主要用于對類的行為進行約束,你實現(xiàn)了某個接口就具有了對應的行為。抽象類主要用于代碼復用,強調的是所屬關系(比如說我們抽象了一個發(fā)送短信的抽象類,)。
  • 一個類只能繼承一個類,但是可以實現(xiàn)多個接口。
  • 接口中的成員變量只能是 public static final 類型的,不能被修改且必須有初始值,而抽象類的成員變量默認 default,可在子類中被重新定義,也可被重新賦值。

深拷貝和淺拷貝區(qū)別了解嗎?什么是引用拷貝?

關于深拷貝和淺拷貝區(qū)別,我這里先給結論:

  • 淺拷貝:淺拷貝會在堆上創(chuàng)建一個新的對象(區(qū)別于引用拷貝的一點),不過,如果原對象內部的屬性是引用類型的話,淺拷貝會直接復制內部對象的引用地址,也就是說拷貝對象和原對象共用同一個內部對象。
  • 深拷貝 :深拷貝會完全復制整個對象,包括這個對象所包含的內部對象。

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

淺拷貝

淺拷貝的示例代碼如下,我們這里實現(xiàn)了 Cloneable 接口,并重寫了 clone() 方法。

clone() 方法的實現(xiàn)很簡單,直接調用的是父類 Objectclone() 方法。

public class Address implements Cloneable{
    private String name;
    // 省略構造函數(shù)、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;
    // 省略構造函數(shù)、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 對象已經是不同的了。

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

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

image.png

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 方法,用于創(chuàng)建并返回當前對象的一份拷貝。
 */
protected native Object clone() throws CloneNotSupportedException
/**
 * 返回類的名字實例的哈希碼的 16 進制的字符串。建議 Object 所有的子類都重寫這個方法。
 */
public String toString()
/**
 * native 方法,并且不能重寫。喚醒一個在此對象監(jiān)視器上等待的線程(監(jiān)視器相當于就是鎖的概念)。如果有多個線程在等待只會任意喚醒一個。
 */
public final native void notify()
/**
 * native 方法,并且不能重寫。跟 notify 一樣,唯一的區(qū)別就是會喚醒在此對象監(jiān)視器上等待的所有線程,而不是一個線程。
 */
public final native void notifyAll()
/**
 * native方法,并且不能重寫。暫停線程的執(zhí)行。注意:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 ,timeout 是等待時間。
 */
public final native void wait(long timeout) throws InterruptedException
/**
 * 多了 nanos 參數(shù),這個參數(shù)表示額外時間(以毫微秒為單位,范圍是 0-999999)。 所以超時的時間還需要加上 nanos 毫秒。。
 */
public final void wait(long timeout, int nanos) throws InterruptedException
/**
 * 跟之前的2個wait方法一樣,只不過該方法一直等待,沒有超時時間這個概念
 */
public final void wait() throws InterruptedException
/**
 * 實例被垃圾回收器回收的時候觸發(fā)的操作
 */
protected void finalize() throws Throwable { }

== 和 equals() 的區(qū)別

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

  • 對于基本數(shù)據(jù)類型來說,== 比較的是值。
  • 對于引用數(shù)據(jù)類型來說,== 比較的是對象的內存地址。

因為 Java 只有值傳遞,所以,對于 == 來說,不管是比較基本數(shù)據(jù)類型,還是引用數(shù)據(jù)類型的變量,其本質比較的都是值,只是引用類型變量存的值是對象的地址。

equals() 不能用于判斷基本數(shù)據(jù)類型的變量,只能用來判斷兩個對象是否相等。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 方法比較的是對象的值。

當創(chuàng)建 String 類型的對象時,虛擬機會在常量池中查找有沒有已經存在的值和要創(chuàng)建的值相同的對象,如果有就把它賦給當前引用。如果沒有就在常量池中重新創(chuàng)建一個 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 整數(shù)),也稱為散列碼。這個哈希碼的作用是確定該對象在哈希表中的索引位置。

hashCode()定義在 JDK 的 Object 類中,這就意味著 Java 中的任何類都包含有 hashCode() 函數(shù)。另外需要注意的是: ObjecthashCode() 方法是本地方法,也就是用 C 語言或 C++ 實現(xiàn)的,該方法通常用來將對象的內存地址轉換為整數(shù)之后返回。

public native int hashCode();

散列表存儲的是鍵值對(key-value),它的特點是:能根據(jù)“鍵”快速的檢索出對應的“值”。這其中就利用到了散列碼?。梢钥焖僬业剿枰膶ο螅?/strong>

為什么要有 hashCode?

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

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

當你把對象加入 HashSet 時,HashSet 會先計算對象的 hashCode 值來判斷對象加入的位置,同時也會與其他已經加入的對象的 hashCode 值作比較,如果沒有相符的 hashCode,HashSet 會假設對象沒有重復出現(xiàn)。但是如果發(fā)現(xiàn)有相同 hashCode 值的對象,這時會調用 equals() 方法來檢查 hashCode 相等的對象是否真的相同。如果兩者相同,HashSet 就不會讓其加入操作成功。如果不同的話,就會重新散列到其他位置。這樣我們就大大減少了 equals 的次數(shù),相應就大大提高了執(zhí)行速度。

其實, hashCode()equals()都是用于比較兩個對象是否相等。

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

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

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

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

這是因為兩個對象的hashCode 值相等并不代表兩個對象就相等。

那為什么兩個對象有相同的 hashCode 值,它們也不一定是相等的?

因為 hashCode() 所使用的哈希算法也許剛好會讓多個對象傳回相同的哈希值。越糟糕的哈希算法越容易碰撞,但這也與數(shù)據(jù)值域分布的特性有關(所謂哈希碰撞也就是指的是不同的對象得到相同的 hashCode )。

總結下來就是 :

  • 如果兩個對象的hashCode 值相等,那這兩個對象不一定相等(哈希碰撞)。
  • 如果兩個對象的hashCode 值相等并且equals()方法也返回 true,我們才認為這兩個對象相等。
  • 如果兩個對象的hashCode 值不相等,我們就可以直接認為這兩個對象不相等。

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

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

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

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

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

總結

  • equals 方法判斷兩個對象是相等的,那這兩個對象的 hashCode 值也要相等。
  • 兩個對象有相同的 hashCode 值,他們也不一定是相等的(哈希碰撞)。

String

String、StringBuffer、StringBuilder 的區(qū)別?

可變性

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

StringBuilderStringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字符數(shù)組保存字符串,不過沒有使用 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 的公共父類,定義了一些字符串的基本操作,如 expandCapacity、appendinsert、indexOf 等公共方法。StringBuffer 對方法加了同步鎖或者對調用的方法加了同步鎖,所以是線程安全的。StringBuilder 并沒有對方法進行加同步鎖,所以是非線程安全的。

性能

每次對 String 類型進行改變的時候,都會生成一個新的 String 對象,然后將指針指向新的 String 對象。StringBuffer 每次都會對 StringBuffer 對象本身進行操作,而不是生成新的對象并改變對象引用。相同情況下使用 StringBuilder 相比使用 StringBuffer 僅能獲得 10%~15% 左右的性能提升,但卻要冒多線程不安全的風險。

對于三者使用的總結:

  1. 操作少量的數(shù)據(jù): 適用 String
  2. 單線程操作字符串緩沖區(qū)下操作大量數(shù)據(jù): 適用 StringBuilder
  3. 多線程操作字符串緩沖區(qū)下操作大量數(shù)據(jù): 適用 StringBuffer

String 為什么是不可變的?

String 類中使用 final 關鍵字修飾字符數(shù)組來保存字符串,所以String 對象是不可變的。

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

?? 修正 : 我們知道被 final 關鍵字修飾的類不能被繼承,修飾的方法不能被重寫,修飾的變量是基本數(shù)據(jù)類型則值不能改變,修飾的變量是引用類型則不能再指向其他對象。因此,final 關鍵字修飾的數(shù)組保存字符串并不是 String 不可變的根本原因,因為這個數(shù)組保存的字符串是可變的(final 修飾引用類型變量的情況)。

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

  1. 保存字符串的數(shù)組被 final 修飾且為私有的,并且String 類沒有提供/暴露修改這個字符串的方法。
  2. String 類被 final 修飾導致其不能被繼承,進而避免了子類破壞 String 不可變。

在 Java 9 之后,String 、StringBuilderStringBuffer 的實現(xiàn)改用 byte 數(shù)組存儲字符串。

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

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

}

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

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

JDK 官方就說了絕大部分字符串對象只包含 Latin-1 可表示的字符。

image.png

如果字符串中包含的漢字超過 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;

對象引用和“+”的字符串拼接方式,實際上是通過 StringBuilder 調用 append() 方法實現(xiàn)的,拼接完成之后調用 toString() 得到一個 String 對象 。

image.png

不過,在循環(huán)內使用“+”進行字符串的拼接的話,存在比較明顯的缺陷:編譯器不會創(chuàng)建單個 StringBuilder 以復用,會導致創(chuàng)建過多的 StringBuilder 對象。

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

StringBuilder 對象是在循環(huán)內部被創(chuàng)建的,這意味著每循環(huán)一次就會創(chuàng)建一個 StringBuilder 對象。

image.png

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

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

String 中的 equals 方法是被重寫過的,比較的是 String 字符串的值是否相等。 Objectequals 方法是比較的對象的內存地址。

字符串常量池的作用了解嗎?

字符串常量池 是 JVM 為了提升性能和減少內存消耗針對字符串(String 類)專門開辟的一塊區(qū)域,主要目的是為了避免字符串的重復創(chuàng)建。

// 在堆中創(chuàng)建字符串對象”ab“
// 將字符串對象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串對象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

String s1 = new String("abc");這句話創(chuàng)建了幾個字符串對象?

會創(chuàng)建 1 或 2 個字符串。

1、如果字符串常量池中不存在字符串對象“abc”的引用,那么會在堆中創(chuàng)建 2 個字符串對象“abc”。

示例代碼(JDK 1.8):

String s1 = new String("abc");

對應的字節(jié)碼:

image.png

ldc 命令用于判斷字符串常量池中是否保存了對應的字符串對象的引用,如果保存了的話直接返回,如果沒有保存的話,會在堆中創(chuàng)建對應的字符串對象并將該字符串對象的引用保存到字符串常量池中。

2、如果字符串常量池中已存在字符串對象“abc”的引用,則只會在堆中創(chuàng)建 1 個字符串對象“abc”。

示例代碼(JDK 1.8):

// 字符串常量池中已存在字符串對象“abc”的引用
String s1 = "abc";
// 下面這段代碼只會在堆中創(chuàng)建 1 個字符串對象“abc”
String s2 = new String("abc");

對應的字節(jié)碼:

image.png

這里就不對上面的字節(jié)碼進行詳細注釋了,7 這個位置的 ldc 命令不會在堆中創(chuàng)建新的字符串對象“abc”,這是因為 0 這個位置已經執(zhí)行了一次 ldc 命令,已經在堆中創(chuàng)建過一次字符串對象“abc”了。7 這個位置執(zhí)行 ldc 命令會直接返回字符串常量池中字符串對象“abc”對應的引用。

intern 方法有什么作用?

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

  • 如果字符串常量池中保存了對應的字符串對象的引用,就直接返回該引用。
  • 如果字符串常量池中沒有保存了對應的字符串對象的引用,那就在常量池中創(chuàng)建一個指向該字符串對象的引用并返回。

示例代碼(JDK 1.8) :

// 在堆中創(chuàng)建字符串對象”Java“
// 將字符串對象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串對象”Java“對應的引用
String s2 = s1.intern();
// 會在堆中在單獨創(chuàng)建一個字符串對象
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 類型的變量和常量做“+”運算時發(fā)生了什么?

先來看字符串不加 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() 方法替換。

image.png

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

在編譯過程中,Javac 編譯器(下文中統(tǒng)稱為編譯器)會進行一個叫做 常量折疊(Constant Folding) 的代碼優(yōu)化?!渡钊肜斫?Java 虛擬機》中是也有介紹到:

image.png

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

對于 String str3 = "str" + "ing"; 編譯器會給你優(yōu)化成 String str3 = "string"; 。

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

  • 基本數(shù)據(jù)類型( byte、booleanshort、char、int、float、long、double)以及字符串常量。
  • final 修飾的基本數(shù)據(jù)類型和字符串變量
  • 字符串通過 “+”拼接得到的字符串、基本數(shù)據(jù)類型之間算數(shù)運算(加減乘除)、基本數(shù)據(jù)類型的位運算(<<、>>、>>> )

引用的值在程序編譯期是無法確定的,編譯器無法對其進行優(yōu)化。

對象引用和“+”的字符串拼接方式,實際上是通過 StringBuilder 調用 append() 方法實現(xiàn)的,拼接完成之后調用 toString() 得到一個 String 對象 。

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

我們在平時寫代碼的時候,盡量避免多個字符串對象拼接,因為這樣會重新創(chuàng)建對象。如果需要改變字符串的話,可以使用 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 會被編譯器當做常量來處理,編譯器在程序編譯期就可以確定它的值,其效果就相當于訪問常量。

如果 ,編譯器在運行時才能知道其確切值的話,就無法對其優(yōu)化。

示例代碼(str2 在運行時才能確定其值):

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

在此我向大家推薦一個架構學習交流圈。交流學習微信:539413949(里面有大量的面試題及答案)里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發(fā)、高性能、分布式、微服務架構的原理,JVM性能優(yōu)化、分布式架構等這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容