Java高頻面試題(2023最新整理)

程式設計師大彬發表於2023-04-05

Java的特點

Java是一門物件導向的程式語言。物件導向和麵向過程的區別參考下一個問題。

Java具有平臺獨立性和移植性

  • Java有一句口號:Write once, run anywhere,一次編寫、到處執行。這也是Java的魅力所在。而實現這種特性的正是Java虛擬機器JVM。已編譯的Java程式可以在任何帶有JVM的平臺上執行。你可以在windows平臺編寫程式碼,然後拿到linux上執行。只要你在編寫完程式碼後,將程式碼編譯成.class檔案,再把class檔案打成Java包,這個jar包就可以在不同的平臺上執行了。

Java具有穩健性

  • Java是一個強型別語言,它允許擴充套件編譯時檢查潛在型別不匹配問題的功能。Java要求顯式的方法宣告,它不支援C風格的隱式宣告。這些嚴格的要求保證編譯程式能捕捉呼叫錯誤,這就導致更可靠的程式。
  • 異常處理是Java中使得程式更穩健的另一個特徵。異常是某種類似於錯誤的異常條件出現的訊號。使用try/catch/finally語句,程式設計師可以找到出錯的處理程式碼,這就簡化了出錯處理和恢復的任務。

Java是如何實現跨平臺的?

Java是透過JVM(Java虛擬機器)實現跨平臺的。

JVM可以理解成一個軟體,不同的平臺有不同的版本。我們編寫的Java程式碼,編譯後會生成.class 檔案(位元組碼檔案)。Java虛擬機器就是負責將位元組碼檔案翻譯成特定平臺下的機器碼,透過JVM翻譯成機器碼之後才能執行。不同平臺下編譯生成的位元組碼是一樣的,但是由JVM翻譯成的機器碼卻不一樣。

只要在不同平臺上安裝對應的JVM,就可以執行位元組碼檔案,執行我們編寫的Java程式。

因此,執行Java程式必須有JVM的支援,因為編譯的結果不是機器碼,必須要經過JVM的翻譯才能執行。

Java 與 C++ 的區別

  • Java 是純粹的面嚮物件語言,所有的物件都繼承自 java.lang.Object,C++ 相容 C ,不但支援物件導向也支援程式導向。
  • Java 透過虛擬機器從而實現跨平臺特性, C++ 依賴於特定的平臺。
  • Java 沒有指標,它的引用可以理解為安全指標,而 C++ 具有和 C 一樣的指標。
  • Java 支援自動垃圾回收,而 C++ 需要手動回收。
  • Java 不支援多重繼承,只能透過實現多個介面來達到相同目的,而 C++ 支援多重繼承。

JDK/JRE/JVM三者的關係

JVM

英文名稱(Java Virtual Machine),就是我們耳熟能詳的 Java 虛擬機器。Java 能夠跨平臺執行的核心在於 JVM 。

本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~

Github地址:https://github.com/Tyson0314/Java-learning

所有的java程式會首先被編譯為.class的類檔案,這種類檔案可以在虛擬機器上執行。也就是說class檔案並不直接與機器的作業系統互動,而是經過虛擬機器間接與作業系統互動,由虛擬機器將程式解釋給本地系統執行。

針對不同的系統有不同的 jvm 實現,有 Linux 版本的 jvm 實現,也有Windows 版本的 jvm 實現,但是同一段程式碼在編譯後的位元組碼是一樣的。這就是Java能夠跨平臺,實現一次編寫,多處執行的原因所在。

JRE

英文名稱(Java Runtime Environment),就是Java 執行時環境。我們編寫的Java程式必須要在JRE才能執行。它主要包含兩個部分,JVM 和 Java 核心類庫。

JRE是Java的執行環境,並不是一個開發環境,所以沒有包含任何開發工具,如編譯器和偵錯程式等。

如果你只是想執行Java程式,而不是開發Java程式的話,那麼你只需要安裝JRE即可。

JDK

英文名稱(Java Development Kit),就是 Java 開發工具包

學過Java的同學,都應該安裝過JDK。當我們安裝完JDK之後,目錄結構是這樣的

可以看到,JDK目錄下有個JRE,也就是JDK中已經整合了 JRE,不用單獨安裝JRE。

另外,JDK中還有一些好用的工具,如jinfo,jps,jstack等。

最後,總結一下JDK/JRE/JVM,他們三者的關係

JRE = JVM + Java 核心類庫

JDK = JRE + Java工具 + 編譯器 + 偵錯程式

Java程式是編譯執行還是解釋執行?

先看看什麼是編譯型語言和解釋型語言。

編譯型語言

在程式執行之前,透過編譯器將源程式編譯成機器碼可執行的二進位制,以後執行這個程式時,就不用再進行編譯了。

優點:編譯器一般會有預編譯的過程對程式碼進行最佳化。因為編譯只做一次,執行時不需要編譯,所以編譯型語言的程式執行效率高,可以脫離語言環境獨立執行。

缺點:編譯之後如果需要修改就需要整個模組重新編譯。編譯的時候根據對應的執行環境生成機器碼,不同的作業系統之間移植就會有問題,需要根據執行的作業系統環境編譯不同的可執行檔案。

總結:執行速度快、效率高;依靠編譯器、跨平臺性差些。

代表語言:C、C++、Pascal、Object-C以及Swift。

解釋型語言

定義:解釋型語言的原始碼不是直接翻譯成機器碼,而是先翻譯成中間程式碼,再由直譯器對中間程式碼進行解釋執行。在執行的時候才將源程式翻譯成機器碼,翻譯一句,然後執行一句,直至結束。

優點:

  1. 有良好的平臺相容性,在任何環境中都可以執行,前提是安裝瞭直譯器(如虛擬機器)。
  2. 靈活,修改程式碼的時候直接修改就可以,可以快速部署,不用停機維護。

缺點:每次執行的時候都要解釋一遍,效能上不如編譯型語言。

總結:解釋型語言執行速度慢、效率低;依靠直譯器、跨平臺性好。

代表語言:JavaScript、Python、Erlang、PHP、Perl、Ruby。

對於Java這種語言,它的原始碼會先透過javac編譯成位元組碼,再透過jvm將位元組碼轉換成機器碼執行,即解釋執行 和編譯執行配合使用,所以可以稱為混合型或者半編譯型。

最全面的Java面試網站

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

物件導向和麵向過程是一種軟體開發思想。

  • 程式導向就是分析出解決問題所需要的步驟,然後用函式按這些步驟實現,使用的時候依次呼叫就可以了。
  • 物件導向是把構成問題事務分解成各個物件,分別設計這些物件,然後將他們組裝成有完整功能的系統。程式導向只用函式實現,物件導向是用類實現各個功能模組。

以五子棋為例,程式導向的設計思路就是首先分析問題的步驟:

1、開始遊戲,2、黑子先走,3、繪製畫面,4、判斷輸贏,5、輪到白子,6、繪製畫面,7、判斷輸贏,8、返回步驟2,9、輸出最後結果。
把上面每個步驟用分別的函式來實現,問題就解決了。

而物件導向的設計則是從另外的思路來解決問題。整個五子棋可以分為:

  1. 黑白雙方
  2. 棋盤系統,負責繪製畫面
  3. 規則系統,負責判定諸如犯規、輸贏等。

黑白雙方負責接受使用者的輸入,並告知棋盤系統棋子佈局發生變化,棋盤系統接收到了棋子的變化的資訊就負責在螢幕上面顯示出這種變化,同時利用規則系統來對棋局進行判定。

物件導向有哪些特性?

物件導向四大特性:封裝,繼承,多型,抽象

1、封裝就是將類的資訊隱藏在類內部,不允許外部程式直接訪問,而是透過該類的方法實現對隱藏資訊的操作和訪問。 良好的封裝能夠減少耦合。

2、繼承是從已有的類中派生出新的類,新的類繼承父類的屬性和行為,並能擴充套件新的能力,大大增加程式的重用性和易維護性。在Java中是單繼承的,也就是說一個子類只有一個父類。

3、多型是同一個行為具有多個不同表現形式的能力。在不修改程式程式碼的情況下改變程式執行時繫結的程式碼。實現多型的三要素:繼承、重寫、父類引用指向子類物件。

  • 靜態多型性:透過過載實現,相同的方法有不同的引數列表,可以根據引數的不同,做出不同的處理。
  • 動態多型性:在子類中重寫父類的方法。執行期間判斷所引用物件的實際型別,根據其實際型別呼叫相應的方法。

4、抽象。把客觀事物用程式碼抽象出來。

物件導向程式設計的六大原則?

  • 物件單一職責:我們設計建立的物件,必須職責明確,比如商品類,裡面相關的屬性和方法都必須跟商品相關,不能出現訂單等不相關的內容。這裡的類可以是模組、類庫、程式集,而不單單指類。
  • 裡式替換原則:子類能夠完全替代父類,反之則不行。通常用於實現介面時運用。因為子類能夠完全替代基(父)類,那麼這樣父類就擁有很多子類,在後續的程式擴充套件中就很容易進行擴充套件,程式完全不需要進行修改即可進行擴充套件。比如IA的實現為A,因為專案需求變更,現在需要新的實現,直接在容器注入處更換介面即可.
  • 迪米特法則,也叫最小原則,或者說最小耦合。通常在設計程式或開發程式的時候,儘量要高內聚,低耦合。當兩個類進行互動的時候,會產生依賴。而迪米特法則就是建議這種依賴越少越好。就像建構函式注入父類物件時一樣,當需要依賴某個物件時,並不在意其內部是怎麼實現的,而是在容器中注入相應的實現,既符合裡式替換原則,又起到了解耦的作用。
  • 開閉原則:開放擴充套件,封閉修改。當專案需求發生變更時,要儘可能的不去對原有的程式碼進行修改,而在原有的基礎上進行擴充套件。
  • 依賴倒置原則:高層模組不應該直接依賴於底層模組的具體實現,而應該依賴於底層的抽象。介面和抽象類不應該依賴於實現類,而實現類依賴介面或抽象類。
  • 介面隔離原則:一個物件和另外一個物件互動的過程中,依賴的內容最小。也就是說在介面設計的時候,在遵循物件單一職責的情況下,儘量減少介面的內容。

簡潔版

  • 單一職責:物件設計要求獨立,不能設計萬能物件。
  • 開閉原則:物件修改最小化。
  • 裡式替換:程式擴充套件中抽象被具體可以替換(介面、父類、可以被實現類物件、子類替換物件)
  • 迪米特:高內聚,低耦合。儘量不要依賴細節。
  • 依賴倒置:面向抽象程式設計。也就是引數傳遞,或者返回值,可以使用父類型別或者介面型別。從廣義上講:基於介面程式設計,提前設計好介面框架。
  • 介面隔離:介面設計大小要適中。過大導致汙染,過小,導致呼叫麻煩。

陣列到底是不是物件?

先說說物件的概念。物件是根據某個類建立出來的一個例項,表示某類事物中一個具體的個體。

物件具有各種屬性,並且具有一些特定的行為。站在計算機的角度,物件就是記憶體中的一個記憶體塊,在這個記憶體塊封裝了一些資料,也就是類中定義的各個屬性。

所以,物件是用來封裝資料的。

java中的陣列具有java中其他物件的一些基本特點。比如封裝了一些資料,可以訪問屬性,也可以呼叫方法。

因此,可以說,陣列是物件。

也可以透過程式碼驗證陣列是物件的事實。比如以下的程式碼,輸出結果為java.lang.Object。

Class clz = int[].class;
System.out.println(clz.getSuperclass().getName());

由此,可以看出,陣列類的父類就是Object類,那麼可以推斷出陣列就是物件。

Java的基本資料型別有哪些?

  • byte,8bit
  • char,16bit
  • short,16bit
  • int,32bit
  • float,32bit
  • long,64bit
  • double,64bit
  • boolean,只有兩個值:true、false,可以使⽤用 1 bit 來儲存
簡單型別booleanbytecharshortIntlongfloatdouble
二進位制位數18161632643264
包裝類BooleanByteCharacterShortIntegerLongFloatDouble

在Java規範中,沒有明確指出boolean的大小。在《Java虛擬機器規範》給出了單個boolean佔4個位元組,和boolean陣列1個位元組的定義,具體 還要看虛擬機器實現是否按照規範來,因此boolean佔用1個位元組或者4個位元組都是有可能的。

為什麼不能用浮點型表示金額?

由於計算機中儲存的小數其實是十進位制的小數的近似值,並不是準確值,所以,千萬不要在程式碼中使用浮點數來表示金額等重要的指標。

建議使用BigDecimal或者Long來表示金額。

什麼是值傳遞和引用傳遞?

  • 值傳遞是對基本型變數而言的,傳遞的是該變數的一個副本,改變副本不影響原變數。
  • 引用傳遞一般是對於物件型變數而言的,傳遞的是該物件地址的一個副本,並不是原物件本身,兩者指向同一片記憶體空間。所以對引用物件進行操作會同時改變原物件。

java中不存在引用傳遞,只有值傳遞。即不存在變數a指向變數b,變數b指向物件的這種情況。

瞭解Java的包裝型別嗎?為什麼需要包裝類?

Java 是一種面嚮物件語言,很多地方都需要使用物件而不是基本資料型別。比如,在集合類中,我們是無法將 int 、double 等型別放進去的。因為集合的容器要求元素是 Object 型別。

為了讓基本型別也具有物件的特徵,就出現了包裝型別。相當於將基本型別包裝起來,使得它具有了物件的性質,並且為其新增了屬性和方法,豐富了基本型別的操作。

給大家分享一個Github倉庫,上面有大彬整理的300多本經典的計算機書籍PDF,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演算法、機器學習、程式設計人生等,可以star一下,下次找書直接在上面搜尋,倉庫持續更新中~

Github地址https://github.com/Tyson0314/java-books

自動裝箱和拆箱

Java中基礎資料型別與它們對應的包裝類見下表:

原始型別包裝型別
booleanBoolean
byteByte
charCharacter
floatFloat
intInteger
longLong
shortShort
doubleDouble

裝箱:將基礎型別轉化為包裝型別。

拆箱:將包裝型別轉化為基礎型別。

當基礎型別與它們的包裝類有如下幾種情況時,編譯器會自動幫我們進行裝箱或拆箱:

  • 賦值操作(裝箱或拆箱)
  • 進行加減乘除混合運算 (拆箱)
  • 進行>,<,==比較運算(拆箱)
  • 呼叫equals進行比較(裝箱)
  • ArrayList、HashMap等集合類新增基礎型別資料時(裝箱)

示例程式碼:

Integer x = 1; // 裝箱 調⽤ Integer.valueOf(1)
int y = x; // 拆箱 調⽤了 X.intValue()

下面看一道常見的面試題:

Integer a = 100;
Integer b = 100;
System.out.println(a == b);

Integer c = 200;
Integer d = 200;
System.out.println(c == d);

輸出:

true
false

為什麼第三個輸出是false?看看 Integer 類的原始碼就知道啦。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

Integer c = 200; 會呼叫 調⽤Integer.valueOf(200)。而從Integer的valueOf()原始碼可以看到,這裡的實現並不是簡單的new Integer,而是用IntegerCache做一個cache。

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;
    }
    ...
}

這是IntegerCache靜態程式碼塊中的一段,預設Integer cache 的下限是-128,上限預設127。當賦值100給Integer時,剛好在這個範圍內,所以從cache中取對應的Integer並返回,所以a和b返回的是同一個物件,所以==比較是相等的,當賦值200給Integer時,不在cache 的範圍內,所以會new Integer並返回,當然==比較的結果是不相等的。

String 為什麼不可變?

先看看什麼是不可變的物件。

如果一個物件,在它建立完成之後,不能再改變它的狀態,那麼這個物件就是不可變的。不能改變狀態的意思是,不能改變物件內的成員變數,包括基本資料型別的值不能改變,引用型別的變數不能指向其他的物件,引用型別指向的物件的狀態也不能改變。

接著來看Java8 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
}

從原始碼可以看出,String物件其實在內部就是一個個字元,儲存在這個value陣列裡面的。

value陣列用final修飾,final 修飾的變數,值不能被修改。因此value不可以指向其他物件。

String類內部所有的欄位都是私有的,也就是被private修飾。而且String沒有對外提供修改內部狀態的方法,因此value陣列不能改變。

所以,String是不可變的。

那為什麼String要設計成不可變的?

主要有以下幾點原因:

  1. 執行緒安全。同一個字串例項可以被多個執行緒共享,因為字串不可變,本身就是執行緒安全的。
  2. 支援hash對映和快取。因為String的hash值經常會使用到,比如作為 Map 的鍵,不可變的特性使得 hash 值也不會變,不需要重新計算。
  3. 出於安全考慮。網路地址URL、檔案路徑path、密碼通常情況下都是以String型別儲存,假若String不是固定不變的,將會引起各種安全隱患。比如將密碼用String的型別儲存,那麼它將一直留在記憶體中,直到垃圾收集器把它清除。假如String類不是固定不變的,那麼這個密碼可能會被改變,導致出現安全隱患。
  4. 字串常量池最佳化。String物件建立之後,會快取到字串常量池中,下次需要建立同樣的物件時,可以直接返回快取的引用。

既然我們的String是不可變的,它內部還有很多substring, replace, replaceAll這些操作的方法。這些方法好像會改變String物件?怎麼解釋呢?

其實不是的,我們每次呼叫replace等方法,其實會在堆記憶體中建立了一個新的物件。然後其value陣列引用指向不同的物件。

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

主要是為了節約String佔用的記憶體

在大部分Java程式的堆記憶體中,String佔用的空間最大,並且絕大多數String只有Latin-1字元,這些Latin-1字元只需要1個位元組就夠了。

而在JDK9之前,JVM因為String使用char陣列儲存,每個char佔2個位元組,所以即使字串只需要1位元組,它也要按照2位元組進行分配,浪費了一半的記憶體空間。

到了JDK9之後,對於每個字串,會先判斷它是不是隻有Latin-1字元,如果是,就按照1位元組的規格進行分配記憶體,如果不是,就按照2位元組的規格進行分配,這樣便提高了記憶體使用率,同時GC次數也會減少,提升效率。

不過Latin-1編碼集支援的字元有限,比如不支援中文字元,因此對於中文字串,用的是UTF16編碼(兩個位元組),所以用byte[]和char[]實現沒什麼區別。

String, StringBuffer 和 StringBuilder區別

1. 可變性

  • String 不可變
  • StringBuffer 和 StringBuilder 可變

2. 執行緒安全

  • String 不可變,因此是執行緒安全的
  • StringBuilder 不是執行緒安全的
  • StringBuffer 是執行緒安全的,內部使用 synchronized 進行同步
最全面的Java面試網站

什麼是StringJoiner?

StringJoiner是 Java 8 新增的一個 API,它基於 StringBuilder 實現,用於實現對字串之間透過分隔符拼接的場景。

StringJoiner 有兩個構造方法,第一個構造要求依次傳入分隔符、字首和字尾。第二個構造則只要求傳入分隔符即可(字首和字尾預設為空字串)。

StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
StringJoiner(CharSequence delimiter)

有些字串拼接場景,使用 StringBuffer 或 StringBuilder 則顯得比較繁瑣。

比如下面的例子:

List<Integer> values = Arrays.asList(1, 3, 5);
StringBuilder sb = new StringBuilder("(");

for (int i = 0; i < values.size(); i++) {
    sb.append(values.get(i));
    if (i != values.size() -1) {
        sb.append(",");
    }
}

sb.append(")");

而透過StringJoiner來實現拼接List的各個元素,程式碼看起來更加簡潔。

List<Integer> values = Arrays.asList(1, 3, 5);
StringJoiner sj = new StringJoiner(",", "(", ")");

for (Integer value : values) {
    sj.add(value.toString());
}

另外,像平時經常使用的Collectors.joining(","),底層就是透過StringJoiner實現的。

原始碼如下:

public static Collector<CharSequence, ?, String> joining(
    CharSequence delimiter,CharSequence prefix,CharSequence suffix) {
    return new CollectorImpl<>(
            () -> new StringJoiner(delimiter, prefix, suffix),
            StringJoiner::add, StringJoiner::merge,
            StringJoiner::toString, CH_NOID);
}

String 類的常用方法有哪些?

  • indexOf():返回指定字元的索引。
  • charAt():返回指定索引處的字元。
  • replace():字串替換。
  • trim():去除字串兩端空白。
  • split():分割字串,返回一個分割後的字串陣列。
  • getBytes():返回字串的 byte 型別陣列。
  • length():返回字串長度。
  • toLowerCase():將字串轉成小寫字母。
  • toUpperCase():將字串轉成大寫字元。
  • substring():擷取字串。
  • equals():字串比較。

new String("dabin")會建立幾個物件?

使用這種方式會建立兩個字串物件(前提是字串常量池中沒有 "dabin" 這個字串物件)。

  • "dabin" 屬於字串字面量,因此編譯時期會在字串常量池中建立一個字串物件,指向這個 "dabin" 字串字面量;
  • 使用 new 的方式會在堆中建立一個字串物件。

什麼是字串常量池?

字串常量池(String Pool)儲存著所有字串字面量,這些字面量在編譯時期就確定。字串常量池位於堆記憶體中,專門用來儲存字串常量。在建立字串時,JVM首先會檢查字串常量池,如果該字串已經存在池中,則返回其引用,如果不存在,則建立此字串並放入池中,並返回其引用。

String最大長度是多少?

String類提供了一個length方法,返回值為int型別,而int的取值上限為2^31 -1。

所以理論上String的最大長度為2^31 -1。

達到這個長度的話需要多大的記憶體嗎

String內部是使用一個char陣列來維護字元序列的,一個char佔用兩個位元組。如果說String最大長度是2^31 -1的話,那麼最大的字串佔用記憶體空間約等於4GB。

也就是說,我們需要有大於4GB的JVM執行記憶體才行。

那String一般都儲存在JVM的哪塊區域呢

字串在JVM中的儲存分兩種情況,一種是String物件,儲存在JVM的堆疊中。一種是字串常量,儲存在常量池裡面。

什麼情況下字串會儲存在常量池呢

當透過字面量進行字串宣告時,比如String s = "程式新大彬";,這個字串在編譯之後會以常量的形式進入到常量池。

那常量池中的字串最大長度是2^31-1嗎

不是的,常量池對String的長度是有另外限制的。。Java中的UTF-8編碼的Unicode字串在常量池中以CONSTANT_Utf8型別表示。

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

length在這裡就是代表字串的長度,length的型別是u2,u2是無符號的16位整數,也就是說最大長度可以做到2^16-1 即 65535。

不過javac編譯器做了限制,需要length < 65535。所以字串常量在常量池中的最大長度是65535 - 1 = 65534。

最後總結一下:

String在不同的狀態下,具有不同的長度限制。

  • 字串常量長度不能超過65534
  • 堆內字串的長度不超過2^31-1

Object常用方法有哪些?

Java面試經常會出現的一道題目,Object的常用方法。下面給大家整理一下。

Object常用方法有:toString()equals()hashCode()clone()等。

toString

預設輸出物件地址。

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public static void main(String[] args) {
        System.out.println(new Person(18, "程式設計師大彬").toString());
    }
    //output
    //me.tyson.java.core.Person@4554617c
}

可以重寫toString方法,按照重寫邏輯輸出物件值。

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return name + ":" + age;
    }

    public static void main(String[] args) {
        System.out.println(new Person(18, "程式設計師大彬").toString());
    }
    //output
    //程式設計師大彬:18
}

equals

預設比較兩個引用變數是否指向同一個物件(記憶體地址)。

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
       this.age = age;
       this.name = name;
    }

    public static void main(String[] args) {
        String name = "程式設計師大彬";
        Person p1 = new Person(18, name);
        Person p2 = new Person(18, name);

        System.out.println(p1.equals(p2));
    }
    //output
    //false
}

可以重寫equals方法,按照age和name是否相等來判斷:

public class Person {
    private int age;
    private String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof Person) {
            Person p = (Person) o;
            return age == p.age && name.equals(p.name);
        }
        return false;
    }

    public static void main(String[] args) {
        String name = "程式設計師大彬";
        Person p1 = new Person(18, name);
        Person p2 = new Person(18, name);

        System.out.println(p1.equals(p2));
    }
    //output
    //true
}

hashCode

將與物件相關的資訊對映成一個雜湊值,預設的實現hashCode值是根據記憶體地址換算出來。

public class Cat {
    public static void main(String[] args) {
        System.out.println(new Cat().hashCode());
    }
    //out
    //1349277854
}

clone

Java賦值是複製物件引用,如果我們想要得到一個物件的副本,使用賦值操作是無法達到目的的。Object物件有個clone()方法,實現了對

象中各個屬性的複製,但它的可見範圍是protected的。

protected native Object clone() throws CloneNotSupportedException;

所以實體類使用克隆的前提是:

  • 實現Cloneable介面,這是一個標記介面,自身沒有方法,這應該是一種約定。呼叫clone方法時,會判斷有沒有實現Cloneable介面,沒有實現Cloneable的話會拋異常CloneNotSupportedException。
  • 覆蓋clone()方法,可見性提升為public。
public class Cat implements Cloneable {
    private String name;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat c = new Cat();
        c.name = "程式設計師大彬";
        Cat cloneCat = (Cat) c.clone();
        c.name = "大彬";
        System.out.println(cloneCat.name);
    }
    //output
    //程式設計師大彬
}

getClass

返回此 Object 的執行時類,常用於java反射機制。

public class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Person p = new Person("程式設計師大彬");
        Class clz = p.getClass();
        System.out.println(clz);
        //獲取類名
        System.out.println(clz.getName());
    }
    /**
     * class com.tyson.basic.Person
     * com.tyson.basic.Person
     */
}

wait

當前執行緒呼叫物件的wait()方法之後,當前執行緒會釋放物件鎖,進入等待狀態。等待其他執行緒呼叫此物件的notify()/notifyAll()喚醒或者等待超時時間wait(long timeout)自動喚醒。執行緒需要獲取obj物件鎖之後才能呼叫 obj.wait()。

notify

obj.notify()喚醒在此物件上等待的單個執行緒,選擇是任意性的。notifyAll()喚醒在此物件上等待的所有執行緒。

講講深複製和淺複製?

淺複製:拷⻉物件和原始物件的引⽤型別引用同⼀個物件。

以下例子,Cat物件裡面有個Person物件,呼叫clone之後,克隆物件和原物件的Person引用的是同一個物件,這就是淺複製。

public class Cat implements Cloneable {
    private String name;
    private Person owner;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat c = new Cat();
        Person p = new Person(18, "程式設計師大彬");
        c.owner = p;

        Cat cloneCat = (Cat) c.clone();
        p.setName("大彬");
        System.out.println(cloneCat.owner.getName());
    }
    //output
    //大彬
}

深複製:複製物件和原始物件的引用型別引用不同的物件。

以下例子,在clone函式中不僅呼叫了super.clone,而且呼叫Person物件的clone方法(Person也要實現Cloneable介面並重寫clone方法),從而實現了深複製。可以看到,複製物件的值不會受到原物件的影響。

public class Cat implements Cloneable {
    private String name;
    private Person owner;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Cat c = null;
        c = (Cat) super.clone();
        c.owner = (Person) owner.clone();//複製Person物件
        return c;
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Cat c = new Cat();
        Person p = new Person(18, "程式設計師大彬");
        c.owner = p;

        Cat cloneCat = (Cat) c.clone();
        p.setName("大彬");
        System.out.println(cloneCat.owner.getName());
    }
    //output
    //程式設計師大彬
}

兩個物件的hashCode()相同,則 equals()是否也一定為 true?

equals與hashcode的關係:

  1. 如果兩個物件呼叫equals比較返回true,那麼它們的hashCode值一定要相同;
  2. 如果兩個物件的hashCode相同,它們並不一定相同。

hashcode方法主要是用來提升物件比較的效率,先進行hashcode()的比較,如果不相同,那就不必在進行equals的比較,這樣就大大減少了equals比較的次數,當比較物件的數量很大的時候能提升效率。

為什麼重寫 equals 時一定要重寫 hashCode?

之所以重寫equals()要重寫hashcode(),是為了保證equals()方法返回true的情況下hashcode值也要一致,如果重寫了equals()沒有重寫hashcode(),就會出現兩個物件相等但hashcode()不相等的情況。這樣,當用其中的一個物件作為鍵儲存到hashMap、hashTable或hashSet中,再以另一個物件作為鍵值去查詢他們的時候,則會查詢不到。

Java建立物件有幾種方式?

Java建立物件有以下幾種方式:

  • 用new語句建立物件。
  • 使用反射,使用Class.newInstance()建立物件。
  • 呼叫物件的clone()方法。
  • 運用反序列化手段,呼叫java.io.ObjectInputStream物件的readObject()方法。

說說類例項化的順序

Java中類例項化順序:

  1. 靜態屬性,靜態程式碼塊。
  2. 普通屬性,普通程式碼塊。
  3. 構造方法。
public class LifeCycle {
    // 靜態屬性
    private static String staticField = getStaticField();

    // 靜態程式碼塊
    static {
        System.out.println(staticField);
        System.out.println("靜態程式碼塊初始化");
    }

    // 普通屬性
    private String field = getField();

    // 普通程式碼塊
    {
        System.out.println(field);
        System.out.println("普通程式碼塊初始化");
    }

    // 構造方法
    public LifeCycle() {
        System.out.println("構造方法初始化");
    }

    // 靜態方法
    public static String getStaticField() {
        String statiFiled = "靜態屬性初始化";
        return statiFiled;
    }

    // 普通方法
    public String getField() {
        String filed = "普通屬性初始化";
        return filed;
    }

    public static void main(String[] argc) {
        new LifeCycle();
    }

    /**
     *      靜態屬性初始化
     *      靜態程式碼塊初始化
     *      普通屬性初始化
     *      普通程式碼塊初始化
     *      構造方法初始化
     */
}

equals和==有什麼區別?

  • 對於基本資料型別,==比較的是他們的值。基本資料型別沒有equal方法;
  • 對於複合資料型別,==比較的是它們的存放地址(是否是同一個物件)。equals()預設比較地址值,重寫的話按照重寫邏輯去比較。

常見的關鍵字有哪些?

static

static可以用來修飾類的成員方法、類的成員變數。

static變數也稱作靜態變數,靜態變數和非靜態變數的區別是:靜態變數被所有的物件所共享,在記憶體中只有一個副本,它當且僅當在類初次載入時會被初始化。而非靜態變數是物件所擁有的,在建立物件的時候被初始化,存在多個副本,各個物件擁有的副本互不影響。

以下例子,age為非靜態變數,則p1列印結果是:Name:zhangsan, Age:10;若age使用static修飾,則p1列印結果是:Name:zhangsan, Age:12,因為static變數在記憶體只有一個副本。

public class Person {
    String name;
    int age;
    
    public String toString() {
        return "Name:" + name + ", Age:" + age;
    }
    
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.name = "zhangsan";
        p1.age = 10;
        Person p2 = new Person();
        p2.name = "lisi";
        p2.age = 12;
        System.out.println(p1);
        System.out.println(p2);
    }
    /**Output
     * Name:zhangsan, Age:10
     * Name:lisi, Age:12
     *///~
}

static方法一般稱作靜態方法。靜態方法不依賴於任何物件就可以進行訪問,透過類名即可呼叫靜態方法。

public class Utils {
    public static void print(String s) {
        System.out.println("hello world: " + s);
    }

    public static void main(String[] args) {
        Utils.print("程式設計師大彬");
    }
}

靜態程式碼塊只會在類載入的時候執行一次。以下例子,startDate和endDate在類載入的時候進行賦值。

class Person  {
    private Date birthDate;
    private static Date startDate, endDate;
    static{
        startDate = Date.valueOf("2008");
        endDate = Date.valueOf("2021");
    }

    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
}

靜態內部類

在靜態方法裡,使用⾮靜態內部類依賴於外部類的實例,也就是說需要先建立外部類實例,才能用這個實例去建立非靜態內部類。⽽靜態內部類不需要。

public class OuterClass {
    class InnerClass {
    }
    static class StaticInnerClass {
    }
    public static void main(String[] args) {
        // 在靜態方法裡,不能直接使用OuterClass.this去建立InnerClass的例項
        // 需要先建立OuterClass的例項o,然後透過o建立InnerClass的例項
        // InnerClass innerClass = new InnerClass();
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        StaticInnerClass staticInnerClass = new StaticInnerClass();

        outerClass.test();
    }
    
    public void nonStaticMethod() {
        InnerClass innerClass = new InnerClass();
        System.out.println("nonStaticMethod...");
    }
}

final

  1. 基本資料型別用final修飾,則不能修改,是常量;物件引用用final修飾,則引用只能指向該物件,不能指向別的物件,但是物件本身可以修改。
  2. final修飾的方法不能被子類重寫
  3. final修飾的類不能被繼承。

this

this.屬性名稱指訪問類中的成員變數,可以用來區分成員變數和區域性變數。如下程式碼所示,this.name訪問類Person當前例項的變數。

/**
 * @description:
 * @author: 程式設計師大彬
 * @time: 2021-08-17 00:29
 */
public class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

this.方法名稱用來訪問本類的方法。以下程式碼中,this.born()呼叫類 Person 的當前例項的方法。

/**
 * @description:
 * @author: 程式設計師大彬
 * @time: 2021-08-17 00:29
 */
public class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.born();
        this.name = name;
        this.age = age;
    }

    void born() {
    }
}

super

super 關鍵字用於在子類中訪問父類的變數和方法。

class A {
    protected String name = "大彬";

    public void getName() {
        System.out.println("父類:" + name);
    }
}

public class B extends A {
    @Override
    public void getName() {
        System.out.println(super.name);
        super.getName();
    }

    public static void main(String[] args) {
        B b = new B();
        b.getName();
    }
    /**
     * 大彬
     * 父類:大彬
     */
}

在子類B中,我們重寫了父類的getName()方法,如果在重寫的getName()方法中我們要呼叫父類的相同方法,必須要透過super關鍵字顯式指出。

final, finally, finalize 的區別

  • final 用於修飾屬性、方法和類, 分別表示屬性不能被重新賦值,方法不可被覆蓋,類不可被繼承。
  • finally 是異常處理語句結構的一部分,一般以try-catch-finally出現,finally程式碼塊表示總是被執行。
  • finalize 是Object類的一個方法,該方法一般由垃圾回收器來呼叫,當我們呼叫System.gc()方法的時候,由垃圾回收器呼叫finalize()方法,回收垃圾,JVM並不保證此方法總被呼叫。

final關鍵字的作用?

  • final 修飾的類不能被繼承。
  • final 修飾的方法不能被重寫。
  • final 修飾的變數叫常量,常量必須初始化,初始化之後值就不能被修改。

方法過載和重寫的區別?

同個類中的多個方法可以有相同的方法名稱,但是有不同的引數列表,這就稱為方法過載。引數列表又叫引數簽名,包括引數的型別、引數的個數、引數的順序,只要有一個不同就叫做引數列表不同。

過載是物件導向的一個基本特性。

public class OverrideTest {
    void setPerson() { }
    
    void setPerson(String name) {
        //set name
    }
    
    void setPerson(String name, int age) {
        //set name and age
    }
}

方法的重寫描述的是父類和子類之間的。當父類的功能無法滿足子類的需求,可以在子類對方法進行重寫。方法重寫時, 方法名與形參列表必須一致。

如下程式碼,Person為父類,Student為子類,在Student中重寫了dailyTask方法。

public class Person {
    private String name;
    
    public void dailyTask() {
        System.out.println("work eat sleep");
    }
}


public class Student extends Person {
    @Override
    public void dailyTask() {
        System.out.println("study eat sleep");
    }
}

介面與抽象類區別?

1、語法層面上的區別

  • 抽象類可以有方法實現,而介面的方法中只能是抽象方法(Java 8 之後介面方法可以有預設實現);
  • 抽象類中的成員變數可以是各種型別的,介面中的成員變數只能是public static final型別;
  • 介面中不能含有靜態程式碼塊以及靜態方法,而抽象類可以有靜態程式碼塊和靜態方法(Java 8之後介面可以有靜態方法);
  • 一個類只能繼承一個抽象類,而一個類卻可以實現多個介面。

2、設計層面上的區別

  • 抽象層次不同。抽象類是對整個類整體進行抽象,包括屬性、行為,但是介面只是對類行為進行抽象。繼承抽象類是一種"是不是"的關係,而介面實現則是 "有沒有"的關係。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而介面實現則是具備不具備的關係,比如鳥是否能飛。
  • 繼承抽象類的是具有相似特點的類,而實現介面的卻可以不同的類。

門和警報的例子:

class AlarmDoor extends Door implements Alarm {
    //code
}

class BMWCar extends Car implements Alarm {
    //code
}

常見的Exception有哪些?

常見的RuntimeException:

  1. ClassCastException //型別轉換異常
  2. IndexOutOfBoundsException //陣列越界異常
  3. NullPointerException //空指標
  4. ArrayStoreException //陣列儲存異常
  5. NumberFormatException //數字格式化異常
  6. ArithmeticException //數學運算異常

checked Exception:

  1. NoSuchFieldException //反射異常,沒有對應的欄位
  2. ClassNotFoundException //類沒有找到異常
  3. IllegalAccessException //安全許可權異常,可能是反射時呼叫了private方法

Error和Exception的區別?

Error:JVM 無法解決的嚴重問題,如棧溢位StackOverflowError、記憶體溢位OOM等。程式無法處理的錯誤。

Exception:其它因程式設計錯誤或偶然的外在因素導致的一般性問題。可以在程式碼中進行處理。如:空指標異常、陣列下標越界等。

執行時異常和非執行時異常(checked)的區別?

unchecked exception包括RuntimeExceptionError類,其他所有異常稱為檢查(checked)異常。

  1. RuntimeException由程式錯誤導致,應該修正程式避免這類異常發生。
  2. checked Exception由具體的環境(讀取的檔案不存在或檔案為空或sql異常)導致的異常。必須進行處理,不然編譯不透過,可以catch或者throws。

throw和throws的區別?

  • throw:用於丟擲一個具體的異常物件。
  • throws:用在方法簽名中,用於宣告該方法可能丟擲的異常。子類方法丟擲的異常範圍更加小,或者根本不拋異常。

透過故事講清楚NIO

下面透過一個例子來講解下。

假設某銀行只有10個職員。該銀行的業務流程分為以下4個步驟:

1) 顧客填申請表(5分鐘);

2) 職員稽核(1分鐘);

3) 職員叫保安去金庫取錢(3分鐘);

4) 職員列印票據,並將錢和票據返回給顧客(1分鐘)。

下面我們看看銀行不同的工作方式對其工作效率到底有何影響。

首先是BIO方式。

每來一個顧客,馬上由一位職員來接待處理,並且這個職員需要負責以上4個完整流程。當超過10個顧客時,剩餘的顧客需要排隊等候。

一個職員處理一個顧客需要10分鐘(5+1+3+1)時間。一個小時(60分鐘)能處理6個顧客,一共10個職員,那就是隻能處理60個顧客。

可以看到銀行職員的工作狀態並不飽和,比如在第1步,其實是處於等待中。

這種工作其實就是BIO,每次來一個請求(顧客),就分配到執行緒池中由一個執行緒(職員)處理,如果超出了執行緒池的最大上限(10個),就扔到佇列等待 。

那麼如何提高銀行的吞吐量呢?

思路就是:分而治之,將任務拆分開來,由專門的人負責專門的任務。

具體來講,銀行專門指派一名職員A,A的工作就是每當有顧客到銀行,他就遞上表格讓顧客填寫。每當有顧客填好表後,A就將其隨機指派給剩餘的9名職員完成後續步驟。

這種方式下,假設顧客非常多,職員A的工作處於飽和中,他不斷的將填好表的顧客帶到櫃檯處理。

櫃檯一個職員5分鐘能處理完一個顧客,一個小時9名職員能處理:9*(60/5)=108。

可見工作方式的轉變能帶來效率的極大提升。

這種工作方式其實就NIO的思路。

下圖是非常經典的NIO說明圖,mainReactor執行緒負責監聽server socket,接收新連線,並將建立的socket分派給subReactor

subReactor可以是一個執行緒,也可以是執行緒池,負責多路分離已連線的socket,讀寫網路資料。這裡的讀寫網路資料可類比顧客填表這一耗時動作,對具體的業務處理功能,其扔給worker執行緒池完成

可以看到典型NIO有三類執行緒,分別是mainReactor執行緒、subReactor執行緒、work執行緒。

不同的執行緒幹專業的事情,最終每個執行緒都沒空著,系統的吞吐量自然就上去了。

那這個流程還有沒有什麼可以提高的地方呢?

可以看到,在這個業務流程裡邊第3個步驟,職員叫保安去金庫取錢(3分鐘)。這3分鐘櫃檯職員是在等待中度過的,可以把這3分鐘利用起來。

還是分而治之的思路,指派1個職員B來專門負責第3步驟。

每當櫃檯員工完成第2步時,就通知職員B來負責與保安溝通取錢。這時候櫃檯員工可以繼續處理下一個顧客。

當職員B拿到錢之後,通知顧客錢已經到櫃檯了,讓顧客重新排隊處理,當櫃檯職員再次服務該顧客時,發現該顧客前3步已經完成,直接執行第4步即可。

在當今web服務中,經常需要透過RPC或者Http等方式呼叫第三方服務,這裡對應的就是第3步,如果這步耗時較長,透過非同步方式將能極大降低資源使用率。

NIO+非同步的方式能讓少量的執行緒做大量的事情。這適用於很多應用場景,比如代理服務、api服務、長連線服務等等。這些應用如果用同步方式將耗費大量機器資源。

不過雖然NIO+非同步能提高系統吞吐量,但其並不能讓一個請求的等待時間下降,相反可能會增加等待時間。

最後,NIO基本思想總結起來就是:分而治之,將任務拆分開來,由專門的人負責專門的任務

BIO/NIO/AIO區別的區別?

同步阻塞IO : 使用者程式發起一個IO操作以後,必須等待IO操作的真正完成後,才能繼續執行。

同步非阻塞IO: 客戶端與伺服器透過Channel連線,採用多路複用器輪詢註冊的Channel。提高吞吐量和可靠性。使用者程式發起一個IO操作以後,可做其它事情,但使用者程式需要輪詢IO操作是否完成,這樣造成不必要的CPU資源浪費。

非同步非阻塞IO: 非阻塞非同步通訊模式,NIO的升級版,採用非同步通道實現非同步通訊,其read和write方法均是非同步方法。使用者程式發起一個IO操作,然後立即返回,等IO操作真正的完成以後,應用程式會得到IO操作完成的通知。類似Future模式。

守護執行緒是什麼?

  • 守護執行緒是執行在後臺的一種特殊程式。
  • 它獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。
  • 在 Java 中垃圾回收執行緒就是特殊的守護執行緒。

Java支援多繼承嗎?

java中,類不支援多繼承。介面才支援多繼承。介面的作用是擴充物件功能。當一個子介面繼承了多個父介面時,說明子介面擴充了多個功能。當一個類實現該介面時,就擴充了多個的功能。

Java不支援多繼承的原因:

  • 出於安全性的考慮,如果子類繼承的多個父類裡面有相同的方法或者屬性,子類將不知道具體要繼承哪個。
  • Java提供了介面和內部類以達到實現多繼承功能,彌補單繼承的缺陷。

如何實現物件克隆?

  • 實現Cloneable介面,重寫 clone() 方法。這種方式是淺複製,即如果類中屬性有自定義引用型別,只複製引用,不複製引用指向的物件。如果物件的屬性的Class也實現 Cloneable 介面,那麼在克隆物件時也會克隆屬性,即深複製。
  • 結合序列化,深複製。
  • 透過org.apache.commons中的工具類BeanUtilsPropertyUtils進行物件複製。

同步和非同步的區別?

同步:發出一個呼叫時,在沒有得到結果之前,該呼叫就不返回。

非同步:在呼叫發出後,被呼叫者返回結果之後會通知呼叫者,或透過回撥函式處理這個呼叫。

阻塞和非阻塞的區別?

阻塞和非阻塞關注的是執行緒的狀態。

阻塞呼叫是指呼叫結果返回之前,當前執行緒會被掛起。呼叫執行緒只有在得到結果之後才會恢復執行。

非阻塞呼叫指在不能立刻得到結果之前,該呼叫不會阻塞當前執行緒。

舉個例子,理解下同步、阻塞、非同步、非阻塞的區別:

同步就是燒開水,要自己來看開沒開;非同步就是水開了,然後水壺響了通知你水開了(回撥通知)。阻塞是燒開水的過程中,你不能幹其他事情,必須在旁邊等著;非阻塞是燒開水的過程裡可以幹其他事情。

Java8的新特性有哪些?

  • Lambda 表示式:Lambda允許把函式作為一個方法的引數
  • Stream API :新新增的Stream API(java.util.stream) 把真正的函數語言程式設計風格引入到Java中
  • 預設方法:預設方法就是一個在介面裡面有了一個實現的方法。
  • Optional 類 :Optional 類已經成為 Java 8 類庫的一部分,用來解決空指標異常。
  • Date Time API :加強對日期與時間的處理。
Java8 新特性總結

序列化和反序列化

  • 序列化:把物件轉換為位元組序列的過程稱為物件的序列化.
  • 反序列化:把位元組序列恢復為物件的過程稱為物件的反序列化.

什麼時候需要用到序列化和反序列化呢?

當我們只在本地 JVM 裡執行下 Java 例項,這個時候是不需要什麼序列化和反序列化的,但當我們需要將記憶體中的物件持久化到磁碟,資料庫中時,當我們需要與瀏覽器進行互動時,當我們需要實現 RPC 時,這個時候就需要序列化和反序列化了.

前兩個需要用到序列化和反序列化的場景,是不是讓我們有一個很大的疑問? 我們在與瀏覽器互動時,還有將記憶體中的物件持久化到資料庫中時,好像都沒有去進行序列化和反序列化,因為我們都沒有實現 Serializable 介面,但一直正常執行.

下面先給出結論:

只要我們對記憶體中的物件進行持久化或網路傳輸,這個時候都需要序列化和反序列化.

理由:

伺服器與瀏覽器互動時真的沒有用到 Serializable 介面嗎? JSON 格式實際上就是將一個物件轉化為字串,所以伺服器與瀏覽器互動時的資料格式其實是字串,我們來看來 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;

    ......
}

String 型別實現了 Serializable 介面,並顯示指定 serialVersionUID 的值.

然後我們再來看物件持久化到資料庫中時的情況,Mybatis 資料庫對映檔案裡的 insert 程式碼:

<insert id="insertUser" parameterType="org.tyshawn.bean.User">
    INSERT INTO t\_user(name,age) VALUES (#{name},#{age})
</insert>

實際上我們並不是將整個物件持久化到資料庫中,而是將物件中的屬性持久化到資料庫中,而這些屬性(如Date/String)都實現了 Serializable 介面。

實現序列化和反序列化為什麼要實現 Serializable 介面?

在 Java 中實現了 Serializable 介面後, JVM 在類載入的時候就會發現我們實現了這個介面,然後在初始化例項物件的時候就會在底層幫我們實現序列化和反序列化。

如果被寫物件型別不是String、陣列、Enum,並且沒有實現Serializable介面,那麼在進行序列化的時候,將丟擲NotSerializableException。原始碼如下:

// remaining cases
if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
    writeOrdinaryObject(obj, desc, unshared);
} else {
    if (extendedDebugInfo) {
        throw new NotSerializableException(
            cl.getName() + "\n" + debugInfoStack.toString());
    } else {
        throw new NotSerializableException(cl.getName());
    }
}

實現 Serializable 介面之後,為什麼還要顯示指定 serialVersionUID 的值?

如果不顯示指定 serialVersionUID,JVM 在序列化時會根據屬性自動生成一個 serialVersionUID,然後與屬性一起序列化,再進行持久化或網路傳輸. 在反序列化時,JVM 會再根據屬性自動生成一個新版 serialVersionUID,然後將這個新版 serialVersionUID 與序列化時生成的舊版 serialVersionUID 進行比較,如果相同則反序列化成功,否則報錯.

如果顯示指定了 serialVersionUID,JVM 在序列化和反序列化時仍然都會生成一個 serialVersionUID,但值為我們顯示指定的值,這樣在反序列化時新舊版本的 serialVersionUID 就一致了.

如果我們的類寫完後不再修改,那麼不指定serialVersionUID,不會有問題,但這在實際開發中是不可能的,我們的類會不斷迭代,一旦類被修改了,那舊物件反序列化就會報錯。 所以在實際開發中,我們都會顯示指定一個 serialVersionUID。

static 屬性為什麼不會被序列化?

因為序列化是針對物件而言的,而 static 屬性優先於物件存在,隨著類的載入而載入,所以不會被序列化.

看到這個結論,是不是有人會問,serialVersionUID 也被 static 修飾,為什麼 serialVersionUID 會被序列化? 其實 serialVersionUID 屬性並沒有被序列化,JVM 在序列化物件時會自動生成一個 serialVersionUID,然後將我們顯示指定的 serialVersionUID 屬性值賦給自動生成的 serialVersionUID.

transient關鍵字的作用?

Java語言的關鍵字,變數修飾符,如果用transient宣告一個例項變數,當物件儲存時,它的值不需要維持。

也就是說被transient修飾的成員變數,在序列化的時候其值會被忽略,在被反序列化後, transient 變數的值被設為初始值, 如 int 型的是 0,物件型的是 null。

什麼是反射?

動態獲取的資訊以及動態呼叫物件的方法的功能稱為Java語言的反射機制。

在執行狀態中,對於任意一個類,能夠知道這個類的所有屬性和方法。對於任意一個物件,能夠呼叫它的任意一個方法和屬性。

反射有哪些應用場景呢?

  1. JDBC連線資料庫時使用Class.forName()透過反射載入資料庫的驅動程式
  2. Eclispe、IDEA等開發工具利用反射動態解析物件的型別與結構,動態提示物件的屬性和方法
  3. Web伺服器中利用反射呼叫了Sevlet的service方法
  4. JDK動態代理底層依賴反射實現

講講什麼是泛型?

Java泛型是JDK 5中引⼊的⼀個新特性, 允許在定義類和介面的時候使⽤型別引數。宣告的型別引數在使⽤時⽤具體的型別來替換。

泛型最⼤的好處是可以提⾼程式碼的復⽤性。以List介面為例,我們可以將String、 Integer等型別放⼊List中, 如不⽤泛型, 存放String型別要寫⼀個List介面, 存放Integer要寫另外⼀個List介面, 泛型可以很好的解決這個問題。

如何停止一個正在執行的執行緒?

有幾種方式。

1、使用執行緒的stop方法

使用stop()方法可以強制終止執行緒。不過stop是一個被廢棄掉的方法,不推薦使用。

使用Stop方法,會一直向上傳播ThreadDeath異常,從而使得目標執行緒解鎖所有鎖住的監視器,即釋放掉所有的物件鎖。使得之前被鎖住的物件得不到同步的處理,因此可能會造成資料不一致的問題。

2、使用interrupt方法中斷執行緒,該方法只是告訴執行緒要終止,但最終何時終止取決於計算機。呼叫interrupt方法僅僅是在當前執行緒中打了一個停止的標記,並不是真的停止執行緒。

接著呼叫 Thread.currentThread().isInterrupted()方法,可以用來判斷當前執行緒是否被終止,透過這個判斷我們可以做一些業務邏輯處理,通常如果isInterrupted返回true的話,會拋一箇中斷異常,然後透過try-catch捕獲。

3、設定標誌位

設定標誌位,當標識位為某個值時,使執行緒正常退出。設定標誌位是用到了共享變數的方式,為了保證共享變數在記憶體中的可見性,可以使用volatile修飾它,這樣的話,變數取值始終會從主存中獲取最新值。

但是這種volatile標記共享變數的方式,線上程發生阻塞時是無法完成響應的。比如呼叫Thread.sleep() 方法之後,執行緒處於不可執行狀態,即便是主執行緒修改了共享變數的值,該執行緒此時根本無法檢查迴圈標誌,所以也就無法實現執行緒中斷。

因此,interrupt() 加上手動拋異常的方式是目前中斷一個正在執行的執行緒最為正確的方式了。

什麼是跨域?

簡單來講,跨域是指從一個域名的網頁去請求另一個域名的資源。由於有同源策略的關係,一般是不允許這麼直接訪問的。但是,很多場景經常會有跨域訪問的需求,比如,在前後端分離的模式下,前後端的域名是不一致的,此時就會發生跨域問題。

那什麼是同源策略呢

所謂同源是指"協議+域名+埠"三者相同,即便兩個不同的域名指向同一個ip地址,也非同源。

同源策略限制以下幾種行為:

1. Cookie、LocalStorage 和 IndexDB 無法讀取
2. DOM 和 Js物件無法獲得
3. AJAX 請求不能傳送

為什麼要有同源策略

舉個例子,假如你剛剛在網銀輸入賬號密碼,檢視了自己的餘額,然後再去訪問其他帶顏色的網站,這個網站可以訪問剛剛的網銀站點,並且獲取賬號密碼,那後果可想而知。因此,從安全的角度來講,同源策略是有利於保護網站資訊的。

跨域問題怎麼解決呢?

嗯,有以下幾種方法:

CORS,跨域資源共享

CORS(Cross-origin resource sharing),跨域資源共享。CORS 其實是瀏覽器制定的一個規範,瀏覽器會自動進行 CORS 通訊,它的實現主要在服務端,透過一些 HTTP Header 來限制可以訪問的域,例如頁面 A 需要訪問 B 伺服器上的資料,如果 B 伺服器 上宣告瞭允許 A 的域名訪問,那麼從 A 到 B 的跨域請求就可以完成。

@CrossOrigin註解

如果專案使用的是Springboot,可以在Controller類上新增一個 @CrossOrigin(origins ="*") 註解就可以實現對當前controller 的跨域訪問了,當然這個標籤也可以加到方法上,或者直接加到入口類上對所有介面進行跨域處理。注意SpringMVC的版本要在4.2或以上版本才支援@CrossOrigin。

nginx反向代理介面跨域

nginx反向代理跨域原理如下: 首先同源策略是瀏覽器的安全策略,不是HTTP協議的一部分。伺服器端呼叫HTTP介面只是使用HTTP協議,不會執行JS指令碼,不需要同源策略,也就不存在跨越問題。

nginx反向代理介面跨域實現思路如下:透過nginx配置一個代理伺服器(域名與domain1相同,埠不同)做跳板機,反向代理訪問domain2介面,並且可以順便修改cookie中domain資訊,方便當前域cookie寫入,實現跨域登入。

// proxy伺服器
server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie裡域名
        index  index.html index.htm;
        
        add_header Access-Control-Allow-Origin http://www.domain1.com;
    }
}

這樣我們的前端代理只要訪問 http:www.domain1.com:81/*就可以了。

透過jsonp跨域

通常為了減輕web伺服器的負載,我們把js、css,img等靜態資源分離到另一臺獨立域名的伺服器上,在html頁面中再透過相應的標籤從不同域名下載入靜態資源,這是瀏覽器允許的操作,基於此原理,我們可以透過動態建立script,再請求一個帶參網址實現跨域通訊。

設計介面要注意什麼?

  1. 介面引數校驗。介面必須校驗引數,比如入參是否允許為空,入參長度是否符合預期。
  2. 設計介面時,充分考慮介面的可擴充套件性。思考介面是否可以複用,怎樣保持介面的可擴充套件性。
  3. 序列呼叫考慮改並行呼叫。比如設計一個商城首頁介面,需要查商品資訊、營銷資訊、使用者資訊等等。如果是序列一個一個查,那耗時就比較大了。這種場景是可以改為並行呼叫的,降低介面耗時。
  4. 介面是否需要防重處理。涉及到資料庫修改的,要考慮防重處理,可以使用資料庫防重表,以唯一流水號作為唯一索引。
  5. 日誌列印全面,入參出參,介面耗時,記錄好日誌,方便甩鍋。
  6. 修改舊介面時,注意相容性設計
  7. 異常處理得當。使用finally關閉流資源、使用log列印而不是e.printStackTrace()、不要吞異常等等
  8. 是否需要考慮限流。限流為了保護系統,防止流量洪峰超過系統的承載能力。

過濾器和攔截器有什麼區別?

1、實現原理不同

過濾器和攔截器底層實現不同。過濾器是基於函式回撥的,攔截器是基於Java的反射機制(動態代理)實現的。一般自定義的過濾器中都會實現一個doFilter()方法,這個方法有一個FilterChain引數,而實際上它是一個回撥介面。

2、使用範圍不同

過濾器實現的是 javax.servlet.Filter 介面,而這個介面是在Servlet規範中定義的,也就是說過濾器Filter的使用要依賴於Tomcat等容器,導致它只能在web程式中使用。而攔截器是一個Spring元件,並由Spring容器管理,並不依賴Tomcat等容器,是可以單獨使用的。攔截器不僅能應用在web程式中,也可以用於Application、Swing等程式中。

3、使用的場景不同

因為攔截器更接近業務系統,所以攔截器主要用來實現專案中的業務判斷的,比如:日誌記錄、許可權判斷等業務。而過濾器通常是用來實現通用功能過濾的,比如:敏感詞過濾、響應資料壓縮等功能。

4、觸發時機不同

過濾器Filter是在請求進入容器後,但在進入servlet之前進行預處理,請求結束是在servlet處理完以後。

攔截器 Interceptor 是在請求進入servlet後,在進入Controller之前進行預處理的,Controller 中渲染了對應的檢視之後請求結束。

5、攔截的請求範圍不同

請求的執行順序是:請求進入容器 -> 進入過濾器 -> 進入 Servlet -> 進入攔截器 -> 執行控制器。可以看到過濾器和攔截器的執行時機也是不同的,過濾器會先執行,然後才會執行攔截器,最後才會進入真正的要呼叫的方法。

參考連結:https://segmentfault.com/a/1190000022833940

對接第三方介面要考慮什麼?

嗯,需要考慮以下幾點:

  1. 確認介面對接的網路協議,是https/http或者自定義的私有協議等。
  2. 約定好資料傳參、響應格式(如application/json),弱型別對接強型別語言時要特別注意
  3. 介面安全方面,要確定身份校驗方式,使用token、證照校驗等
  4. 確認是否需要介面呼叫失敗後的重試機制,保證資料傳輸的最終一致性。
  5. 日誌記錄要全面。介面出入引數,以及解析之後的引數值,都要用日誌記錄下來,方便定位問題(甩鍋)。

參考:https://blog.csdn.net/gzt19881123/article/details/108791034

後端介面效能最佳化有哪些方法?

有以下這些方法:

1、最佳化索引。給where條件的關鍵欄位,或者order by後面的排序欄位,加索引。

2、最佳化sql語句。比如避免使用select *、批次操作、避免深分頁、提升group by的效率等

3、避免大事務。使用@Transactional註解這種宣告式事務的方式提供事務功能,容易造成大事務,引發其他的問題。應該避免在事務中一次性處理太多資料,將一些跟事務無關的邏輯放到事務外面執行。

4、非同步處理。剝離主邏輯和副邏輯,副邏輯可以非同步執行,非同步寫庫。比如使用者購買的商品發貨了,需要發簡訊通知,簡訊通知是副流程,可以非同步執行,以免影響主流程的執行。

5、降低鎖粒度。在併發場景下,多個執行緒同時修改資料,造成資料不一致的情況。這種情況下,一般會加鎖解決。但如果鎖加得不好,導致鎖的粒度太粗,也會非常影響介面效能。

6、加快取。如果表資料量非常大的話,直接從資料庫查詢資料,效能會非常差。可以使用Redismemcached提升查詢效能,從而提高介面效能。

7、分庫分表。當系統發展到一定的階段,使用者併發量大,會有大量的資料庫請求,需要佔用大量的資料庫連線,同時會帶來磁碟IO的效能瓶頸問題。或者資料庫表資料非常大,SQL查詢即使走了索引,也很耗時。這時,可以透過分庫分表解決。分庫用於解決資料庫連線資源不足問題,和磁碟IO的效能瓶頸問題。分表用於解決單表資料量太大,sql語句查詢資料時,即使走了索引也非常耗時問題。

8、避免在迴圈中查詢資料庫。迴圈查詢資料庫,非常耗時,最好能在一次查詢中獲取所有需要的資料。

為什麼在阿里巴巴Java開發手冊中強制要求使用包裝型別定義屬性呢?

嗯,以布林欄位為例,當我們沒有設定物件的欄位的值的時候,Boolean型別的變數會設定預設值為null,而boolean型別的變數會設定預設值為false

也就是說,包裝型別的預設值都是null,而基本資料型別的預設值是一個固定值,如boolean是false,byte、short、int、long是0,float是0.0f等。

舉一個例子,比如有一個扣費系統,扣費時需要從外部的定價系統中讀取一個費率的值,我們預期該介面的返回值中會包含一個浮點型的費率欄位。當我們取到這個值得時候就使用公式:金額*費率=費用 進行計算,計算結果進行劃扣。

如果由於計費系統異常,他可能會返回個預設值,如果這個欄位是Double型別的話,該預設值為null,如果該欄位是double型別的話,該預設值為0.0。

如果扣費系統對於該費率返回值沒做特殊處理的話,拿到null值進行計算會直接報錯,阻斷程式。拿到0.0可能就直接進行計算,得出介面為0後進行扣費了。這種異常情況就無法被感知。

那我可以對0.0做特殊判斷,如果是0就阻斷報錯,這樣是否可以呢?

不對,這時候就會產生一個問題,如果允許費率是0的場景又怎麼處理呢?

使用基本資料型別只會讓方案越來越複雜,坑越來越多。

這種使用包裝型別定義變數的方式,透過異常來阻斷程式,進而可以被識別到這種線上問題。如果使用基本資料型別的話,系統可能不會報錯,進而認為無異常。

因此,建議在POJO和RPC的返回值中使用包裝型別。

8招讓介面效能提升100倍

池化思想

如果你每次需要用到執行緒,都去建立,就會有增加一定的耗時,而執行緒池可以重複利用執行緒,避免不必要的耗時。

比如TCP三次握手,它為了減少效能損耗,引入了Keep-Alive長連線,避免頻繁的建立和銷燬連線。

拒絕阻塞等待

如果你呼叫一個系統B的介面,但是它處理業務邏輯,耗時需要10s甚至更多。然後你是一直阻塞等待,直到系統B的下游介面返回,再繼續你的下一步操作嗎?這樣顯然不合理

參考IO多路複用模型。即我們不用阻塞等待系統B的介面,而是先去做別的操作。等系統B的介面處理完,透過事件回撥通知,我們介面收到通知再進行對應的業務操作即可。

遠端呼叫由序列改為並行

比如設計一個商城首頁介面,需要查商品資訊、營銷資訊、使用者資訊等等。如果是序列一個一個查,那耗時就比較大了。這種場景是可以改為並行呼叫的,降低介面耗時。

鎖粒度避免過粗

在高併發場景,為了防止超賣等情況,我們經常需要加鎖來保護共享資源。但是,如果加鎖的粒度過粗,是很影響介面效能的。

不管你是synchronized加鎖還是redis分散式鎖,只需要在共享臨界資源加鎖即可,不涉及共享資源的,就不必要加鎖。

耗時操作,考慮放到非同步執行

耗時操作,考慮用非同步處理,這樣可以降低介面耗時。比如使用者註冊成功後,簡訊郵件通知,是可以非同步處理的。

使用快取

把要查的資料,提前放好到快取裡面,需要時,直接查快取,而避免去查資料庫或者計算的過程

提前初始化到快取

預取思想很容易理解,就是提前把要計算查詢的資料,初始化到快取。如果你在未來某個時間需要用到某個經過複雜計算的資料,才實時去計算的話,可能耗時比較大。這時候,我們可以採取預取思想,提前把將來可能需要的資料計算好,放到快取中,等需要的時候,去快取取就行。這將大幅度提高介面效能。

壓縮傳輸內容

壓縮傳輸內容,傳輸報文變得更小,因此傳輸會更快。

相關文章