文章目錄
- 一、java面試題集合框架
- 1. 請簡要介紹 Java 集合框架的體系結構
- 2. ArrayList 和 LinkedList 的區別是什么
- 3. HashMap 的工作原理是什么,它在 JDK 7 和 JDK 8 中有哪些不同
- 4. 如何解決 HashMap 的線程安全問題
- 5. TreeSet 是如何保證元素有序的
- 二、java面試題多線程與并發
- 1. 什么是線程和進程,它們有什么區別
- 2. 創建線程有哪幾種方式,各有什么優缺點
- 3. 什么是線程安全,如何保證線程安全
- 4. 什么是死鎖,如何避免死鎖
- 三、java面試題異常處理
- 1. Java 中的異常體系是怎樣的
- 2. try-catch-finally 語句的執行順序是怎樣的
- 3. throws 和 throw 的區別是什么
- 4. 自定義異常有什么作用,如何自定義異常
- 四、java面試題IO 流
- 1. 請介紹 Java 中的 IO 流體系
- 2. 字節流和字符流有什么區別
- 3. 什么是緩沖流,使用緩沖流有什么好處
- 4. 如何實現文件的復制
- 5. 什么是 NIO,它與傳統 IO 有什么區別
java面試題帶答案2025最新整理12萬字:https://pan.quark.cn/s/6e941688e902
一、java面試題集合框架
1. 請簡要介紹 Java 集合框架的體系結構
Java 集合框架主要分為兩大分支:Collection 和 Map。
- Collection 接口:它是單列集合的根接口,下面又有三個主要的子接口。
- List 接口:有序、可重復的集合。常見實現類有 ArrayList(基于動態數組實現,查詢快,增刪慢)、LinkedList(基于雙向鏈表實現,增刪快,查詢慢)和 Vector(線程安全的動態數組,性能相對較低)。
- Set 接口:無序、不可重復的集合。HashSet 基于哈希表實現,不保證元素順序;LinkedHashSet 繼承自 HashSet,使用鏈表維護元素插入順序;TreeSet 基于紅黑樹實現,可對元素進行自然排序或根據指定的比較器排序。
- Queue 接口:隊列,遵循先進先出(FIFO)原則。PriorityQueue 是優先隊列,根據元素的自然順序或指定比較器進行排序;LinkedList 也實現了 Queue 接口。
- Map 接口:雙列集合,存儲鍵值對。HashMap 基于哈希表實現,允許鍵和值為 null,不保證元素順序;LinkedHashMap 繼承自 HashMap,使用鏈表維護元素插入順序;TreeMap 基于紅黑樹實現,根據鍵的自然順序或指定比較器對鍵進行排序;Hashtable 是線程安全的,不允許鍵和值為 null。
2. ArrayList 和 LinkedList 的區別是什么
- 數據結構:ArrayList 基于動態數組實現,數組在內存中是連續存儲的;LinkedList 基于雙向鏈表實現,每個節點包含數據、指向前一個節點的引用和指向后一個節點的引用。
- 性能特點:
- 查詢性能:ArrayList 支持隨機訪問,通過索引可以快速定位元素,時間復雜度為
O(1)
;LinkedList 不支持隨機訪問,需要從頭或尾開始遍歷鏈表,時間復雜度為
O(n)
。 - 增刪性能:在列表末尾添加元素時,兩者性能相近;但在中間或開頭插入或刪除元素時,ArrayList 需要移動大量元素,時間復雜度為
O(n)
,而 LinkedList 只需要修改節點的引用,時間復雜度為
O(1)
。 - 內存占用:ArrayList 的主要內存開銷在于數組的預分配空間,可能會有一定的空間浪費;LinkedList 每個節點需要額外的引用空間,整體內存開銷相對較大。
3. HashMap 的工作原理是什么,它在 JDK 7 和 JDK 8 中有哪些不同
- 工作原理
HashMap 基于哈希表實現,通過鍵的 hashCode() 方法計算哈希值,再根據哈希值確定元素在數組中的存儲位置。當發生哈希沖突(不同鍵的哈希值相同)時,使用鏈表或紅黑樹來存儲沖突的元素。 - JDK 7 和 JDK 8 的區別
- 數據結構:JDK 7 采用數組 + 鏈表的結構;JDK 8 采用數組 + 鏈表 + 紅黑樹的結構。當鏈表長度超過 8 且數組長度大于 64 時,鏈表會轉換為紅黑樹,以提高查找效率。
插入方式:JDK 7 使用頭插法,即新元素插入到鏈表頭部;JDK 8 使用尾插法,新元素插入到鏈表尾部,避免了多線程環境下的鏈表成環問題。 - 哈希算法:JDK 8 對哈希算法進行了優化,減少了哈希沖突的概率。
- 擴容機制:JDK 7 在擴容時需要重新計算每個元素的哈希值和位置;JDK 8 在擴容時,根據元素的哈希值與原數組長度的與運算結果,將元素分為低位鏈表和高位鏈表,避免了重新計算哈希值。
4. 如何解決 HashMap 的線程安全問題
使用 Hashtable:Hashtable 是線程安全的,它的方法都使用 synchronized 關鍵字修飾,但性能較低,因為所有操作都進行同步,會影響并發性能。
使用 Collections.synchronizedMap():可以將一個普通的 HashMap 轉換為線程安全的 Map,例如:
java
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class SynchronizedMapExample {
public static void main(String[] args) {
Map<String, String> hashMap = new HashMap<>();
Map<String, String> synchronizedMap = Collections.synchronizedMap(hashMap);
}
}
這種方式也是通過同步機制保證線程安全,性能也有一定的損耗。
使用 ConcurrentHashMap:ConcurrentHashMap 是專門為并發場景設計的線程安全的 Map。在 JDK 7 中,它采用分段鎖機制,將整個 Map 分為多個段,每個段都有自己的鎖,不同段的操作可以并發進行;在 JDK 8 中,采用 CAS(Compare - And - Swap)和 synchronized 來保證并發操作的安全性,性能更高。
5. TreeSet 是如何保證元素有序的
TreeSet 基于紅黑樹實現,它可以保證元素按照自然順序或指定的比較器順序進行排序。
- 自然排序:如果元素的類實現了 java.lang.Comparable 接口,TreeSet 會根據元素的 compareTo() 方法進行排序。例如:
java
import java.util.TreeSet;
class Person implements Comparable {
private int age;
public Person(int age) {this.age = age;
}@Override
public int compareTo(Person other) {return Integer.compare(this.age, other.age);
}@Override
public String toString() {return "Person{age=" + age + "}";
}
}
public class TreeSetNaturalOrderExample {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet<>();
treeSet.add(new Person(20));
treeSet.add(new Person(10));
treeSet.add(new Person(30));
System.out.println(treeSet);
}
}
- 定制排序:可以在創建 TreeSet 時傳入一個 Comparator 對象,TreeSet 會根據 Comparator 的 compare() 方法進行排序。例如:
java
import java.util.Comparator;
import java.util.TreeSet;
class Student {
private int score;
public Student(int score) {this.score = score;
}@Override
public String toString() {return "Student{score=" + score + "}";
}
}
public class TreeSetCustomOrderExample {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet<>(Comparator.comparingInt(s -> s.score));
treeSet.add(new Student(80));
treeSet.add(new Student(90));
treeSet.add(new Student(70));
System.out.println(treeSet);
}
}
二、java面試題多線程與并發
1. 什么是線程和進程,它們有什么區別
- 進程:進程是程序在操作系統中的一次執行過程,是系統進行資源分配和調度的基本單位。每個進程都有自己獨立的內存空間、系統資源和執行上下文。例如,打開一個瀏覽器就是啟動了一個進程。
- 線程:線程是進程中的一個執行單元,是 CPU 調度和分派的基本單位。一個進程可以包含多個線程,這些線程共享進程的內存空間和系統資源,但每個線程有自己獨立的棧和程序計數器。例如,在瀏覽器進程中,可能有負責渲染頁面的線程、處理網絡請求的線程等。
區別: - 資源占用:進程擁有自己獨立的資源,而線程共享所在進程的資源,因此創建和銷毀進程的開銷比線程大。
- 并發程度:進程之間的并發度較低,因為進程之間的通信和同步需要通過操作系統提供的機制(如管道、消息隊列等);線程之間的并發度較高,因為它們共享內存,通信和同步相對簡單。
- 調度:進程的調度由操作系統內核完成,開銷較大;線程的調度可以由操作系統內核或用戶線程庫完成,開銷相對較小。
2. 創建線程有哪幾種方式,各有什么優缺點
- 繼承 Thread 類
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println(“Thread is running”);
}
}
public class ThreadInheritanceExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- 優點:實現簡單,直接繼承 Thread 類,重寫 run() 方法即可。
- 缺點:Java 是單繼承的,繼承了 Thread 類后就不能再繼承其他類,會限制類的擴展性。
- 實現 Runnable 接口
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(“Runnable is running”);
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
- 優點:避免了單繼承的局限性,一個類可以在實現 Runnable 接口的同時繼承其他類;可以將相同的 Runnable 實例傳遞給多個線程,實現資源共享。
- 缺點:代碼相對復雜,需要創建 Thread 對象并將 Runnable 實例傳遞給它。
- 實現 Callable 接口并結合 FutureTask
java
import java.util.concurrent.*;
class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
return 1 + 2;
}
}
public class CallableExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable callable = new MyCallable();
FutureTask futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
Integer result = futureTask.get();
System.out.println("Result: " + result);
}
}
- 優點:Callable 的 call() 方法可以有返回值,并且可以拋出異常;可以通過 FutureTask 獲取線程執行的結果。
- 缺點:代碼復雜度較高,需要處理 FutureTask 和異常。
3. 什么是線程安全,如何保證線程安全
-
線程安全的定義
當多個線程訪問某個對象時,不管運行時環境采用何種調度方式或者這些線程將如何交替執行,并且在主調代碼中不需要額外的同步或協調,這個對象都能表現出正確的行為,那么就稱這個對象是線程安全的。 -
保證線程安全的方法
-
使用 synchronized 關鍵字:可以修飾方法或代碼塊,確保同一時刻只有一個線程可以訪問被修飾的方法或代碼塊。例如:
java
class Counter {
private int count = 0;public synchronized void increment() {
count++;
}public synchronized int getCount() {
return count;
}
} -
使用 Lock 接口及其實現類:如 ReentrantLock,可以更靈活地控制鎖的獲取和釋放。例如:
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class CounterWithLock {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {lock.lock();try {count++;} finally {lock.unlock();}
}public int getCount() {lock.lock();try {return count;} finally {lock.unlock();}
}
}
- 使用原子類:如 AtomicInteger、AtomicLong 等,這些類使用 CAS 操作保證操作的原子性。例如:
java
import java.util.concurrent.atomic.AtomicInteger;
class CounterWithAtomic {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {count.incrementAndGet();
}public int getCount() {return count.get();
}
}
4. 什么是死鎖,如何避免死鎖
- 死鎖的定義
死鎖是指兩個或多個線程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。例如,線程 A 持有資源 1 并請求資源 2,而線程 B 持有資源 2 并請求資源 1,兩個線程就會陷入死鎖狀態。 - 避免死鎖的方法
- 破壞互斥條件:一般來說,互斥條件是資源本身的特性,很難被破壞。
- 破壞占有并等待條件:可以采用一次性分配所有資源的方法,即線程在執行前一次性請求它所需要的所有資源。
- 破壞不剝奪條件:允許線程在持有資源的情況下,當請求其他資源失敗時,釋放已持有的資源。
- 破壞循環等待條件:對資源進行排序,線程按照資源的順序依次請求資源,避免形成循環等待。例如,規定線程必須先請求資源 1,再請求資源 2。
-
請介紹 volatile 關鍵字的作用
volatile 關鍵字主要有兩個作用:
保證可見性:當一個變量被聲明為 volatile 時,它會保證對該變量的寫操作會立即刷新到主內存中,而讀操作會直接從主內存中讀取。這樣可以保證不同線程對該變量的修改能夠及時被其他線程看到。例如:
java
class VolatileExample {
private volatile boolean flag = false;public void setFlag() {
flag = true;
}public boolean getFlag() {
return flag;
}
}
-
禁止指令重排序:在 Java 中,編譯器和處理器為了提高性能,可能會對指令進行重排序。volatile 關鍵字可以禁止指令重排序,保證變量的讀寫操作按照代碼的順序執行。例如,在單例模式的雙重檢查鎖定中,使用 volatile 關鍵字可以避免因指令重排序導致的問題:
java
public class Singleton {
private static volatile Singleton instance;private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
三、java面試題異常處理
1. Java 中的異常體系是怎樣的
Java 中的異常體系以 Throwable 類為根類,它有兩個主要的子類:
Error:表示系統級的錯誤,通常是由系統內部錯誤或資源耗盡等嚴重問題引起的,程序無法處理這些錯誤。例如,OutOfMemoryError 表示內存不足,StackOverflowError 表示棧溢出。
Exception:表示程序可以捕獲和處理的異常,它又分為受檢查異常(Checked Exception)和非受檢查異常(Unchecked Exception)。
受檢查異常:必須在方法簽名中聲明或者在方法內部進行捕獲處理。例如,IOException、SQLException 等。
非受檢查異常:也稱為運行時異常(RuntimeException 及其子類),不需要在方法簽名中聲明,也可以不進行捕獲處理。例如,NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException 等。
2. try-catch-finally 語句的執行順序是怎樣的
首先執行 try 塊中的代碼。
如果 try 塊中沒有拋出異常,則執行完 try 塊后,接著執行 finally 塊中的代碼。
如果 try 塊中拋出了異常,會依次檢查 catch 塊,找到匹配的異常類型進行處理。如果找到匹配的 catch 塊,執行該 catch 塊中的代碼,然后執行 finally 塊;如果沒有找到匹配的 catch 塊,會在執行 finally 塊后將異常拋給上層調用者。
無論 try 塊中是否拋出異常,finally 塊中的代碼都會執行(除非在 try 或 catch 塊中調用了 System.exit() 方法)。例如:
java
public class TryCatchFinallyExample {
public static void main(String[] args) {
try {
int result = 1 / 0;
System.out.println(“This line will not be executed”);
} catch (ArithmeticException e) {
System.out.println("Caught ArithmeticException: " + e.getMessage());
} finally {
System.out.println(“Finally block is executed”);
}
}
}
3. throws 和 throw 的區別是什么
throws:用于方法簽名中,用于聲明該方法可能會拋出的異常類型。它表示該方法本身不處理這些異常,而是將異常拋給調用者處理。例如:
java
public class ThrowsExample {
public static void readFile() throws java.io.IOException {
java.io.FileInputStream fis = new java.io.FileInputStream(“test.txt”);
}
}
throw:用于方法內部,用于手動拋出一個異常對象。它表示在程序執行過程中,當滿足某個條件時,主動拋出一個異常。例如:
java
public class ThrowExample {
public static void checkAge(int age) {
if (age < 0) {
throw new IllegalArgumentException(“Age cannot be negative”);
}
}
}
4. 自定義異常有什么作用,如何自定義異常
自定義異常的作用
可以根據業務需求,創建特定類型的異常,使異常信息更加明確和有針對性,方便程序的調試和維護。
可以將業務邏輯中的錯誤情況進行封裝,提高代碼的可讀性和可維護性。
自定義異常的步驟
定義一個類,繼承自 Exception(受檢查異常)或 RuntimeException(非受檢查異常)。
提供構造方法,通常包含一個無參構造方法和一個帶字符串參數的構造方法,用于傳遞異常信息。例如:
java
class MyCustomException extends Exception {
public MyCustomException() {
super();
}
public MyCustomException(String message) {super(message);
}
}
public class CustomExceptionExample {
public static void main(String[] args) {
try {
throw new MyCustomException(“This is a custom exception”);
} catch (MyCustomException e) {
System.out.println(e.getMessage());
}
}
}
四、java面試題IO 流
1. 請介紹 Java 中的 IO 流體系
Java 中的 IO 流體系主要分為輸入流和輸出流,又可以根據處理的數據類型分為字節流和字符流。
字節流:以字節為單位進行數據讀寫,主要用于處理二進制數據。
輸入流:InputStream 是所有字節輸入流的抽象基類,常見的實現類有 FileInputStream(用于從文件中讀取數據)、BufferedInputStream(帶緩沖功能的輸入流)等。
輸出流:OutputStream 是所有字節輸出流的抽象基類,常見的實現類有 FileOutputStream(用于向文件中寫入數據)、BufferedOutputStream(帶緩沖功能的輸出流)等。
字符流:以字符為單位進行數據讀寫,主要用于處理文本數據。
輸入流:Reader 是所有字符輸入流的抽象基類,常見的實現類有 FileReader(用于從文件中讀取字符數據)、BufferedReader(帶緩沖功能的字符輸入流)等。
輸出流:Writer 是所有字符輸出流的抽象基類,常見的實現類有 FileWriter(用于向文件中寫入字符數據)、BufferedWriter(帶緩沖功能的字符輸出流)等。
2. 字節流和字符流有什么區別
處理的數據類型:字節流處理的是二進制數據,適用于處理圖像、音頻、視頻等文件;字符流處理的是文本數據,它會根據字符編碼將字節數據轉換為字符進行讀寫。
使用場景:如果處理的是文本文件,使用字符流更方便,因為它可以直接處理字符;如果處理的是二進制文件,必須使用字節流。
緩沖區:字節流通常沒有緩沖區,每次讀寫操作都會直接與數據源或目標進行交互;字符流通常帶有緩沖區,這樣可以提高讀寫效率。
3. 什么是緩沖流,使用緩沖流有什么好處
緩沖流是對基本流的包裝,它在內存中設置了一個緩沖區,用于暫存數據。常見的緩沖流有 BufferedInputStream、BufferedOutputStream、BufferedReader 和 BufferedWriter。
使用緩沖流的好處主要有:
提高讀寫效率:由于緩沖流在內存中設置了緩沖區,每次讀寫操作會先將數據讀寫到緩沖區中,當緩沖區滿或達到一定條件時,再一次性將緩沖區中的數據寫入到目標數據源或從數據源讀取數據到緩沖區。這樣可以減少與磁盤或網絡的交互次數,提高讀寫效率。
簡化操作:緩沖流提供了一些方便的方法,如 BufferedReader 的 readLine() 方法可以一次讀取一行文本,簡化了文本處理的操作。
4. 如何實現文件的復制
可以使用字節流或字符流來實現文件的復制,以下是使用字節流的示例:
java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopyExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream(“source.txt”);
FileOutputStream fos = new FileOutputStream(“destination.txt”)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
System.out.println(“File copied successfully”);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在這個示例中,使用 FileInputStream 從源文件中讀取數據,使用 FileOutputStream 將數據寫入到目標文件中。通過一個緩沖區 buffer 來提高讀寫效率。使用 try-with-resources 語句可以自動關閉流,避免資源泄漏。
5. 什么是 NIO,它與傳統 IO 有什么區別
NIO 的定義
NIO(New IO)是 Java 1.4 引入的新的 IO 庫,它提供了一種非阻塞的、基于通道(Channel)和緩沖區(Buffer)的 IO 操作方式。
與傳統 IO 的區別
阻塞與非阻塞:傳統 IO 是阻塞式的,當進行讀寫操作時,線程會一直阻塞直到操作完成;NIO 是非阻塞式的,線程可以在等待 IO 操作完成的同時去執行其他任務。
面向流與面向緩沖區:傳統 IO 是面向流的,數據是單向流動的,只能順序讀寫;NIO 是面向緩沖區的,數據先被讀取到緩沖區中,然后可以在緩沖區中進行隨機訪問和操作。
通道與流:傳統 IO 使用流進行數據讀寫;NIO 使用通道(Channel),通道可以雙向讀寫,并且可以與緩沖區進行交互。
選擇器:NIO 引入了選擇器(Selector),可以實現單線程管理多個通道,提高了并發性能。例如,一個線程可以通過選擇器同時監聽多個通道的讀寫事件。