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

JavaGuide發表於2022-06-21

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

前兩篇:

異常

Java 異常類層次結構圖概覽

Exception 和 Error 有什麼區別?

在 Java 中,所有的異常都有一個共同的祖先 java.lang 包中的 Throwable 類。Throwable 類有兩個重要的子類:

  • Exception :程式本身可以處理的異常,可以通過 catch 來進行捕獲。Exception 又可以分為 Checked Exception (受檢查異常,必須處理) 和 Unchecked Exception (不受檢查異常,可以不處理)。
  • ErrorError 屬於程式無法處理的錯誤 ,我們沒辦法通過 catch 來進行捕獲不建議通過catch捕獲 。例如 Java 虛擬機器執行錯誤(Virtual MachineError)、虛擬機器記憶體不夠錯誤(OutOfMemoryError)、類定義錯誤(NoClassDefFoundError)等 。這些異常發生時,Java 虛擬機器(JVM)一般會選擇執行緒終止。

Checked Exception 和 Unchecked Exception 有什麼區別?

Checked Exception 即 受檢查異常 ,Java 程式碼在編譯過程中,如果受檢查異常沒有被 catch或者throws 關鍵字處理的話,就沒辦法通過編譯。

比如下面這段 IO 操作的程式碼:

除了RuntimeException及其子類以外,其他的Exception類及其子類都屬於受檢查異常 。常見的受檢查異常有: IO 相關的異常、ClassNotFoundExceptionSQLException...。

Unchecked Exception不受檢查異常 ,Java 程式碼在編譯過程中 ,我們即使不處理不受檢查異常也可以正常通過編譯。

RuntimeException 及其子類都統稱為非受檢查異常,常見的有(建議記下來,日常開發中會經常用到):

  • NullPointerException(空指標錯誤)
  • IllegalArgumentException(引數錯誤比如方法入參型別錯誤)
  • NumberFormatException(字串轉換為數字格式錯誤,IllegalArgumentException的子類)
  • ArrayIndexOutOfBoundsException(陣列越界錯誤)
  • ClassCastException(型別轉換錯誤)
  • ArithmeticException(算術錯誤)
  • SecurityException (安全錯誤比如許可權不夠)
  • UnsupportedOperationException(不支援的操作錯誤比如重複建立同一使用者)
  • ......

Throwable 類常用方法有哪些?

  • String getMessage(): 返回異常發生時的簡要描述
  • String toString(): 返回異常發生時的詳細資訊
  • String getLocalizedMessage(): 返回異常物件的本地化資訊。使用 Throwable 的子類覆蓋這個方法,可以生成本地化資訊。如果子類沒有覆蓋該方法,則該方法返回的資訊與 getMessage()返回的結果相同
  • void printStackTrace(): 在控制檯上列印 Throwable 物件封裝的異常資訊

try-catch-finally 如何使用?

  • try塊 : 用於捕獲異常。其後可接零個或多個 catch 塊,如果沒有 catch 塊,則必須跟一個 finally 塊。
  • *catch塊 : 用於處理 try 捕獲到的異常。
  • finally 塊 : 無論是否捕獲或處理異常,finally 塊裡的語句都會被執行。當在 try 塊或 catch 塊中遇到 return 語句時,finally 語句塊將在方法返回之前被執行。

程式碼示例:

try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
} catch (Exception e) {
    System.out.println("Catch Exception -> " + e.getMessage());
} finally {
    System.out.println("Finally");
}

輸出:

Try to do something
Catch Exception -> RuntimeException
Finally

注意:不要在 finally 語句塊中使用 return! 當 try 語句和 finally 語句中都有 return 語句時,try 語句塊中的 return 語句會被忽略。這是因為 try 語句中的 return 返回值會先被暫存在一個本地變數中,當執行到 finally 語句中的 return 之後,這個本地變數的值就變為了 finally 語句中的 return 返回值。

jvm 官方文件中有明確提到:

If the try clause executes a _return_, the compiled code does the following:

  1. Saves the return value (if any) in a local variable.
  2. Executes a jsr to the code for the finally clause.
  3. Upon return from the finally clause, returns the value saved in the local variable.

程式碼示例:

public static void main(String[] args) {
    System.out.println(f(2));
}

public static int f(int value) {
    try {
        return value * value;
    } finally {
        if (value == 2) {
            return 0;
        }
    }
}

輸出:

0

finally 中的程式碼一定會執行嗎?

不一定的!在某些情況下,finally 中的程式碼不會被執行。

就比如說 finally 之前虛擬機器被終止執行的話,finally 中的程式碼就不會被執行。

try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
} catch (Exception e) {
    System.out.println("Catch Exception -> " + e.getMessage());
    // 終止當前正在執行的Java虛擬機器
    System.exit(1);
} finally {
    System.out.println("Finally");
}

輸出:

Try to do something
Catch Exception -> RuntimeException

另外,在以下 2 種特殊情況下,finally 塊的程式碼也不會被執行:

  1. 程式所在的執行緒死亡。
  2. 關閉 CPU。

相關 issue: https://github.com/Snailclimb...

?? 進階一下:從位元組碼角度分析try catch finally這個語法糖背後的實現原理。

如何使用 try-with-resources 代替try-catch-finally

  1. 適用範圍(資源的定義): 任何實現 java.lang.AutoCloseable或者 java.io.Closeable 的物件
  2. 關閉資源和 finally 塊的執行順序:try-with-resources 語句中,任何 catch 或 finally 塊在宣告的資源關閉後執行

《Effective Java》中明確指出:

面對必須要關閉的資源,我們總是應該優先使用 try-with-resources 而不是try-finally。隨之產生的程式碼更簡短,更清晰,產生的異常對我們也更有用。try-with-resources語句讓我們更容易編寫必須要關閉的資源的程式碼,若採用try-finally則幾乎做不到這點。

Java 中類似於InputStreamOutputStreamScannerPrintWriter等的資源都需要我們呼叫close()方法來手動關閉,一般情況下我們都是通過try-catch-finally語句來實現這個需求,如下:

//讀取文字檔案的內容
Scanner scanner = null;
try {
    scanner = new Scanner(new File("D://read.txt"));
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (scanner != null) {
        scanner.close();
    }
}

使用 Java 7 之後的 try-with-resources 語句改造上面的程式碼:

try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

當然多個資源需要關閉的時候,使用 try-with-resources 實現起來也非常簡單,如果你還是用try-catch-finally可能會帶來很多問題。

通過使用分號分隔,可以在try-with-resources塊中宣告多個資源。

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
     BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
    int b;
    while ((b = bin.read()) != -1) {
        bout.write(b);
    }
}
catch (IOException e) {
    e.printStackTrace();
}

異常使用有哪些需要注意的地方?

  • 不要把異常定義為靜態變數,因為這樣會導致異常棧資訊錯亂。每次手動丟擲異常,我們都需要手動 new 一個異常物件丟擲。
  • 丟擲的異常資訊一定要有意義。
  • 建議丟擲更加具體的異常比如字串轉換為數字格式錯誤的時候應該丟擲NumberFormatException而不是其父類IllegalArgumentException
  • 使用日誌列印異常之後就不要再丟擲異常了(兩者不要同時存在一段程式碼邏輯中)。
  • ......

泛型

什麼是泛型?有什麼作用?

Java 泛型(Generics) 是 JDK 5 中引入的一個新特性。使用泛型引數,可以增強程式碼的可讀性以及穩定性。

編譯器可以對泛型引數進行檢測,並且通過泛型引數可以指定傳入的物件型別。比如 ArrayList<Persion> persons = new ArrayList<Persion>() 這行程式碼就指明瞭該 ArrayList 物件只能傳入 Persion 物件,如果傳入其他型別的物件就會報錯。

ArrayList<E> extends AbstractList<E>

並且,原生 List 返回型別是 Object ,需要手動轉換型別才能使用,使用泛型後編譯器自動轉換。

泛型的使用方式有哪幾種?

泛型一般有三種使用方式:泛型類泛型介面泛型方法

1.泛型類

//此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的引數常用於表示泛型
//在例項化泛型類時,必須指定T的具體型別
public class Generic<T>{

    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}

如何例項化泛型類:

Generic<Integer> genericInteger = new Generic<Integer>(123456);

2.泛型介面

public interface Generator<T> {
    public T method();
}

實現泛型介面,不指定型別:

class GeneratorImpl<T> implements Generator<T>{
    @Override
    public T method() {
        return null;
    }
}

實現泛型介面,指定型別:

class GeneratorImpl<T> implements Generator<String>{
    @Override
    public String method() {
        return "hello";
    }
}

3.泛型方法

   public static < E > void printArray( E[] inputArray )
   {
         for ( E element : inputArray ){
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }

使用:

// 建立不同型別陣列: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray  );
printArray( stringArray  );
注意: public static < E > void printArray( E[] inputArray ) 一般被稱為靜態泛型方法;在 java 中泛型只是一個佔位符,必須在傳遞型別後才能使用。類在例項化時才能真正的傳遞型別引數,由於靜態方法的載入先於類的例項化,也就是說類中的泛型還沒有傳遞真正的型別引數,靜態的方法的載入就已經完成了,所以靜態泛型方法是沒有辦法使用類上宣告的泛型的。只能使用自己宣告的 <E>

專案中哪裡用到了泛型?

  • 自定義介面通用返回結果 CommonResult<T> 通過引數 T 可根據具體的返回型別動態指定結果的資料型別
  • 定義 Excel 處理類 ExcelUtil<T> 用於動態指定 Excel 匯出的資料型別
  • 構建集合工具類(參考 Collections 中的 sort, binarySearch 方法)。
  • ......

反射

何為反射?

如果說大家研究過框架的底層原理或者我們們自己寫過框架的話,一定對反射這個概念不陌生。

反射之所以被稱為框架的靈魂,主要是因為它賦予了我們在執行時分析類以及執行類中方法的能力。通過反射你可以獲取任意一個類的所有屬性和方法,你還可以呼叫這些方法和屬性。

反射機制優缺點

  • 優點 : 可以讓我們們的程式碼更加靈活、為各種框架提供開箱即用的功能提供了便利
  • 缺點 :讓我們在執行時有了分析操作類的能力,這同樣也增加了安全問題。比如可以無視泛型引數的安全檢查(泛型引數的安全檢查發生在編譯時)。另外,反射的效能也要稍差點,不過,對於框架來說實際是影響不大的。Java Reflection: Why is it so slow?

反射的應用場景

像我們們平時大部分時候都是在寫業務程式碼,很少會接觸到直接使用反射機制的場景。

但是,這並不代表反射沒有用。相反,正是因為反射,你才能這麼輕鬆地使用各種框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射機制。

這些框架中也大量使用了動態代理,而動態代理的實現也依賴反射。

比如下面是通過 JDK 實現動態代理的示例程式碼,其中就使用了反射類 Method 來呼叫指定的方法。

public class DebugInvocationHandler implements InvocationHandler {
    /**
     * 代理類中的真實物件
     */
    private final Object target;

    public DebugInvocationHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("after method " + method.getName());
        return result;
    }
}

另外,像 Java 中的一大利器 註解 的實現也用到了反射。

為什麼你使用 Spring 的時候 ,一個@Component註解就宣告瞭一個類為 Spring Bean 呢?為什麼你通過一個 @Value註解就讀取到配置檔案中的值呢?究竟是怎麼起作用的呢?

這些都是因為你可以基於反射分析類,然後獲取到類/屬性/方法/方法的引數上的註解。你獲取到註解之後,就可以做進一步的處理。

註解

Annotation (註解) 是 Java5 開始引入的新特性,可以看作是一種特殊的註釋,主要用於修飾類、方法或者變數。

註解本質是一個繼承了Annotation 的特殊介面:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

public interface Override extends Annotation{

}

註解只有被解析之後才會生效,常見的解析方法有兩種:

  • 編譯期直接掃描 :編譯器在編譯 Java 程式碼的時候掃描對應的註解並處理,比如某個方法使用@Override 註解,編譯器在編譯的時候就會檢測當前的方法是否重寫了父類對應的方法。
  • 執行期通過反射處理 :像框架中自帶的註解(比如 Spring 框架的 @Value@Component)都是通過反射來進行處理的。

JDK 提供了很多內建的註解(比如 @Override@Deprecated),同時,我們還可以自定義註解。

I/O

什麼是序列化?什麼是反序列化?

如果我們需要持久化 Java 物件比如將 Java 物件儲存在檔案中,或者在網路傳輸 Java 物件,這些場景都需要用到序列化。

簡單來說:

  • 序列化: 將資料結構或物件轉換成二進位制位元組流的過程
  • 反序列化:將在序列化過程中所生成的二進位制位元組流轉換成資料結構或者物件的過程

對於 Java 這種物件導向程式語言來說,我們序列化的都是物件(Object)也就是例項化後的類(Class),但是在 C++這種半物件導向的語言中,struct(結構體)定義的是資料結構型別,而 class 對應的是物件型別。

維基百科是如是介紹序列化的:

序列化(serialization)在電腦科學的資料處理中,是指將資料結構或物件狀態轉換成可取用格式(例如存成檔案,存於緩衝,或經由網路中傳送),以留待後續在相同或另一臺計算機環境中,能恢復原先狀態的過程。依照序列化格式重新獲取位元組的結果時,可以利用它來產生與原始物件相同語義的副本。對於許多物件,像是使用大量引用的複雜物件,這種序列化重建的過程並不容易。物件導向中的物件序列化,並不概括之前原始物件所關係的函式。這種過程也稱為物件編組(marshalling)。從一系列位元組提取資料結構的反向操作,是反序列化(也稱為解編組、deserialization、unmarshalling)。

綜上:序列化的主要目的是通過網路傳輸物件或者說是將物件儲存到檔案系統、資料庫、記憶體中。

<p style="text-align:right;font-size:13px;color:gray">https://www.corejavaguru.com/...</p>

Java 序列化中如果有些欄位不想進行序列化,怎麼辦?

對於不想進行序列化的變數,使用 transient 關鍵字修飾。

transient 關鍵字的作用是:阻止例項中那些用此關鍵字修飾的的變數序列化;當物件被反序列化時,被 transient 修飾的變數值不會被持久化和恢復。

關於 transient 還有幾點注意:

  • transient 只能修飾變數,不能修飾類和方法。
  • transient 修飾的變數,在反序列化後變數值將會被置成型別的預設值。例如,如果是修飾 int 型別,那麼反序列後結果就是 0
  • static 變數因為不屬於任何物件(Object),所以無論有沒有 transient 關鍵字修飾,均不會被序列化。

獲取用鍵盤輸入常用的兩種方法

方法 1:通過 Scanner

Scanner input = new Scanner(System.in);
String s  = input.nextLine();
input.close();

方法 2:通過 BufferedReader

BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();

Java 中 IO 流分為幾種?

  • 按照流的流向分,可以分為輸入流和輸出流;
  • 按照操作單元劃分,可以劃分為位元組流和字元流;
  • 按照流的角色劃分為節點流和處理流。

Java IO 流共涉及 40 多個類,這些類看上去很雜亂,但實際上很有規則,而且彼此之間存在非常緊密的聯絡, Java IO 流的 40 多個類都是從如下 4 個抽象類基類中派生出來的。

  • InputStream/Reader: 所有的輸入流的基類,前者是位元組輸入流,後者是字元輸入流。
  • OutputStream/Writer: 所有輸出流的基類,前者是位元組輸出流,後者是字元輸出流。

按操作方式分類結構圖:

IO-操作方式分類

按操作物件分類結構圖:

IO-操作物件分類

既然有了位元組流,為什麼還要有字元流?

問題本質想問:不管是檔案讀寫還是網路傳送接收,資訊的最小儲存單元都是位元組,那為什麼 I/O 流操作要分為位元組流操作和字元流操作呢?

回答:字元流是由 Java 虛擬機器將位元組轉換得到的,問題就出在這個過程還算是非常耗時,並且,如果我們不知道編碼型別就很容易出現亂碼問題。所以, I/O 流就乾脆提供了一個直接操作字元的介面,方便我們平時對字元進行流操作。如果音訊檔案、圖片等媒體檔案用位元組流比較好,如果涉及到字元的話使用字元流比較好。

後記

專注 Java 原創乾貨分享,大三開源 JavaGuide (「Java學習+面試指南」一份涵蓋大部分 Java 程式設計師所需要掌握的核心知識。準備 Java 面試,首選 JavaGuide!),目前已經 120k+ Star。

原創不易,歡迎點贊分享,歡迎關注我在思否的賬號,我會持續分享原創乾貨!加油,衝!

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

相關文章