天天看點

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

作者:湯圓說Java

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 。

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

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

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

JRE

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

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

JRE是Java的運作環境,并不是一個開發環境,是以沒有包含任何開發工具,如編譯器和調試器等。

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

JDK

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

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

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

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

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

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

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

JRE = JVM + Java 核心類庫

JDK = JRE + Java工具 + 編譯器 + 調試器

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

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 來存儲
簡單類型 boolean byte char short Int long float double
二進制位數 1 8 16 16 32 64 32 64
包裝類 Boolean Byte Character Short Integer Long Float Double

在Java規範中,沒有明确指出boolean的大小。在《Java虛拟機規範》給出了單個boolean占4個位元組,和boolean數組1個位元組的定義,具體 還要看虛拟機實作是否按照規範來,是以boolean占用1個位元組或者4個位元組都是有可能的。

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

由于計算機中儲存的小數其實是十進制的小數的近似值,并不是準确值,是以,千萬不要在代碼中使用浮點數來表示金額等重要的名額。

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

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

  • 值傳遞是對基本型變量而言的,傳遞的是該變量的一個副本,改變副本不影響原變量。
  • 引用傳遞一般是對于對象型變量而言的,傳遞的是該對象位址的一個副本,并不是原對象本身,兩者指向同一片記憶體空間。是以對引用對象進行操作會同時改變原對象。

java中不存在引用傳遞,隻有值傳遞。即不存在變量a指向變量b,變量b指向對象的這種情況。

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

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

為了讓基本類型也具有對象的特征,就出現了包裝類型。相當于将基本類型包裝起來,使得它具有了對象的性質,并且為其添加了屬性和方法,豐富了基本類型的操作。

自動裝箱和拆箱

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

原始類型 包裝類型
boolean Boolean
byte Byte
char Character
float Float
int Integer
long Long
short Short
double Double

裝箱:将基礎類型轉化為包裝類型。

拆箱:将包裝類型轉化為基礎類型。

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

  • 指派操作(裝箱或拆箱)
  • 進行加減乘除混合運算 (拆箱)
  • 進行>,<,==比較運算(拆箱)
  • 調用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包括RuntimeException和Error類,其他所有異常稱為檢查(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線程。

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

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

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

可以看到,在這個業務流程裡邊第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中的工具類BeanUtils和PropertyUtils進行對象複制。

同步和異步的差別?

同步:發出一個調用時,在沒有得到結果之前,該調用就不傳回。

異步:在調用發出後,被調用者傳回結果之後會通知調用者,或通過回調函數處理這個調用。

阻塞和非阻塞的差別?

阻塞和非阻塞關注的是線程的狀态。

阻塞調用是指調用結果傳回之前,目前線程會被挂起。調用線程隻有在得到結果之後才會恢複運作。

非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞目前線程。

舉個例子,了解下同步、阻塞、異步、非阻塞的差別:

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

Java8的新特性有哪些?

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

序列化和反序列化

  • 序列化:把對象轉換為位元組序列的過程稱為對象的序列化.
  • 反序列化:把位元組序列恢複為對象的過程稱為對象的反序列化.

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

當我們隻在本地 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: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 -> 進入攔截器 -> 執行控制器。可以看到過濾器和攔截器的執行時機也是不同的,過濾器會先執行,然後才會執行攔截器,最後才會進入真正的要調用的方法。

對接第三方接口要考慮什麼?

嗯,需要考慮以下幾點:

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

後端接口性能優化有哪些方法?

有以下這些方法:

1、優化索引。給where條件的關鍵字段,或者order by後面的排序字段,加索引。

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

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

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

5、降低鎖粒度。在并發場景下,多個線程同時修改資料,造成資料不一緻的情況。這種情況下,一般會加鎖解決。但如果鎖加得不好,導緻鎖的粒度太粗,也會非常影響接口性能。

6、加緩存。如果表資料量非常大的話,直接從資料庫查詢資料,性能會非常差。可以使用Redis和memcached提升查詢性能,進而提高接口性能。

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分布式鎖,隻需要在共享臨界資源加鎖即可,不涉及共享資源的,就不必要加鎖。

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

耗時操作,考慮用異步處理,這樣可以降低接口耗時。比如使用者注冊成功後,短信郵件通知,是可以異步處理的。

使用緩存

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

提前初始化到緩存

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

壓縮傳輸内容

壓縮傳輸内容,傳輸封包變得更小,是以傳輸會更快。

繼續閱讀