2019年Java面試題基礎系列228道
第一篇更新1~20題的答案解析
第二篇更新21~50題答案解析
本次更新Java 面試題(一)的51~95題答案
51、類 ExampleA 繼承 Exception,類 ExampleB 繼承ExampleA。
有如下程式碼片斷:
try {
throw new ExampleB("b")
}
catch(ExampleA e){
System.out.println("ExampleA");
}
catch(Exception e){
System.out.println("Exception");
}複製程式碼
**請問執行此段程式碼的輸出是什麼?
答:
輸出:ExampleA。(根據里氏代換原則[能使用父型別的地方一定能使用子型別],抓取 ExampleA 型別異常的 catch 塊能夠抓住 try 塊中丟擲的 ExampleB 型別的異常)
面試題 - 說出下面程式碼的執行結果。(此題的出處是《Java 程式設計思想》一書)
class Annoyance extends Exception {
}
class Sneeze extends Annoyance {
}
class Human {
public static void main(String[] args)
throws Exception {
try {
try {
throw new Sneeze();
}
catch ( Annoyance a ) {
System.out.println("Caught Annoyance");
throw a;
}
}
catch ( Sneeze s ) {
System.out.println("Caught Sneeze");
return ;
}
finally {
System.out.println("Hello World!");
}
}
}複製程式碼
52、List、Set、Map 是否繼承自 Collection 介面?
List、Set 是 ,Map 不是。Map 是鍵值對對映容器,與 List 和 Set 有明顯的區別,而 Set 儲存的零散的元素且不允許有重複元素(數學中的集合也是如此),List是線性結構的容器,適用於按數值索引訪問元素的情形。
53、闡述 ArrayList、Vector、LinkedList 的儲存效能和特性。
ArrayList 和 Vector 都是使用陣列方式儲存資料,此陣列元素數大於實際儲存的資料以便增加和插入元素,它們都允許直接按序號索引元素,但是插入元素要涉及陣列元素移動等記憶體操作,所以索引資料快而插入資料慢,Vector 中的方法由於新增了 synchronized 修飾,因此 Vector 是執行緒安全的容器,但效能上較ArrayList 差,因此已經是 Java 中的遺留容器。LinkedList 使用雙向連結串列實現儲存(將記憶體中零散的記憶體單元通過附加的引用關聯起來,形成一個可以按序號索引的線性結構,這種鏈式儲存方式與陣列的連續儲存方式相比,記憶體的利用率更高),按序號索引資料需要進行前向或後向遍歷,但是插入資料時只需要記錄本項的前後項即可,所以插入速度較快。Vector 屬於遺留容器(Java 早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遺留容器),已經不推薦使用,但是由於 ArrayList 和 LinkedListed 都是非執行緒安全的,如果遇到多個執行緒操作同一個容器的場景,則可以通過工具類Collections 中的 synchronizedList 方法將其轉換成執行緒安全的容器後再使用(這是對裝潢模式的應用,將已有物件傳入另一個類的構造器中建立新的物件來增強實現)。
補充:遺留容器中的 Properties 類和 Stack 類在設計上有嚴重的問題,Properties是一個鍵和值都是字串的特殊的鍵值對對映,在設計上應該是關聯一個Hashtable 並將其兩個泛型引數設定為 String 型別,但是 Java API 中的Properties 直接繼承了 Hashtable,這很明顯是對繼承的濫用。這裡複用程式碼的方式應該是 Has-A 關係而不是 Is-A 關係,另一方面容器都屬於工具類,繼承工具類本身就是一個錯誤的做法,使用工具類最好的方式是 Has-A 關係(關聯)或Use-A 關係(依賴)。同理,Stack 類繼承 Vector 也是不正確的。Sun 公司的工程師們也會犯這種低階錯誤,讓人唏噓不已。
54、Collection 和 Collections 的區別?
Collection 是一個介面,它是 Set、List 等容器的父介面;Collections 是個一個工具類,提供了一系列的靜態方法來輔助容器操作,這些方法包括對容器的搜尋、排序、執行緒安全化等等。
55、List、Map、Set 三個介面存取元素時,各有什麼特點?
List 以特定索引來存取元素,可以有重複元素。Set 不能存放重複元素(用物件的equals()方法來區分元素是否重複)。Map 儲存鍵值對(key-value pair)對映,對映關係可以是一對一或多對一。Set 和 Map 容器都有基於雜湊儲存和排序樹的兩種實現版本,基於雜湊儲存的版本理論存取時間複雜度為 O(1),而基於排序樹版本的實現在插入或刪除元素時會按照元素或元素的鍵(key)構成排序樹從而達到排序和去重的效果。
56、TreeMap 和 TreeSet 在排序時如何比較元素?Collections 工具類中的 sort()方法如何比較元素?
TreeSet 要求存放的物件所屬的類必須實現 Comparable 介面,該介面提供了比較元素的 compareTo()方法,當插入元素時會回撥該方法比較元素的大小。TreeMap 要求存放的鍵值對對映的鍵必須實現 Comparable 介面從而根據鍵對元素進 行排 序。Collections 工具類的 sort 方法有兩種過載的形式,第一種要求傳入的待排序容器中存放的物件比較實現 Comparable 介面以實現元素的比較;第二種不強制性的要求容器中的元素必須可比較,但是要求傳入第二個引數,引數是Comparator 介面的子型別(需要重寫 compare 方法實現元素的比較),相當於一個臨時定義的排序規則,其實就是通過介面注入比較元素大小的演算法,也是對回撥模式的應用(Java 中對函數語言程式設計的支援)。
57、Thread 類的 sleep()方法和物件的 wait()方法都可以讓執行緒暫停執行,它們有什麼區別?
sleep()方法(休眠)是執行緒類(Thread)的靜態方法,呼叫此方法會讓當前執行緒暫停執行指定的時間,將執行機會(CPU)讓給其他執行緒,但是物件的鎖依然保持,因此休眠時間結束後會自動恢復(執行緒回到就緒狀態,請參考第 66 題中的執行緒狀態轉換圖)。wait()是 Object 類的方法,呼叫物件的 wait()方法導致當前執行緒放棄物件的鎖(執行緒暫停執行),進入物件的等待池(wait pool),只有呼叫物件的 notify()方法(或 notifyAll()方法)時才能喚醒等待池中的執行緒進入等鎖池(lock pool),如果執行緒重新獲得物件的鎖就可以進入就緒狀態。
補充:可能不少人對什麼是程式,什麼是執行緒還比較模糊,對於為什麼需要多執行緒程式設計也不是特別理解。簡單的說:程式是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,是作業系統進行資源分配和排程的一個獨立單位;執行緒是程式的一個實體,是 CPU 排程和分派的基本單位,是比程式更小的能獨立執行的基本單位。執行緒的劃分尺度小於程式,這使得多執行緒程式的併發性高;程式在執行時通常擁有獨立的記憶體單元,而執行緒之間可以共享記憶體。使用多執行緒的程式設計通常能夠帶來更好的效能和使用者體驗,但是多執行緒的程式對於其他程式是不友好的,因為它可能佔用了更多的 CPU 資源。當然,也不是執行緒越多,程式的效能就越好,因為執行緒之間的排程和切換也會浪費 CPU 時間。時下很時髦的 Node.js就採用了單執行緒非同步 I/O 的工作模式。
58、執行緒的 sleep()方法和 yield()方法有什麼區別?
(1) sleep()方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會;
(2) 執行緒執行 sleep()方法後轉入阻塞(blocked)狀態,而執行 yield()方法後轉入就緒(ready)狀態;
(3)sleep()方法宣告丟擲 InterruptedException,而 yield()方法沒有宣告任何異常;
(4)sleep()方法比 yield()方法(跟作業系統 CPU 排程相關)具有更好的可移植性。
59、當一個執行緒進入一個物件的 synchronized 方法 A 之後,其它執行緒是否可進入此物件的 synchronized 方法 B?
不能。其它執行緒只能訪問該物件的非同步方法,同步方法則不能進入。因為非靜態方法上的 synchronized 修飾符要求執行方法時要獲得物件的鎖,如果已經進入A 方法說明物件鎖已經被取走,那麼試圖進入 B 方法的執行緒就只能在等鎖池(注意不是等待池哦)中等待物件的鎖。
60、請說出與執行緒同步以及執行緒排程相關的方法。
(1) wait():使一個執行緒處於等待(阻塞)狀態,並且釋放所持有的物件的鎖;
(2)sleep():使一個正在執行的執行緒處於睡眠狀態,是一個靜態方法,呼叫此方法要處理 InterruptedException 異常;
(3)notify():喚醒一個處於等待狀態的執行緒,當然在呼叫此方法的時候,並不能確切的喚醒某一個等待狀態的執行緒,而是由 JVM 確定喚醒哪個執行緒,而且與優先順序無關;
(4)notityAll():喚醒所有處於等待狀態的執行緒,該方法並不是將物件的鎖給所有執行緒,而是讓它們競爭,只有獲得鎖的執行緒才能進入就緒狀態;
補充:Java 5 通過 Lock 介面提供了顯式的鎖機制(explicit lock),增強了靈活性以及對執行緒的協調。Lock 介面中定義了加鎖(lock())和解鎖(unlock())的方法,同時還提供了 newCondition()方法來產生用於執行緒之間通訊的 Condition 物件;此外,Java 5 還提供了訊號量機制(semaphore),訊號量可以用來限制對某個共享資源進行訪問的執行緒的數量。在對資源進行訪問之前,執行緒必須得到訊號量的許可(呼叫 Semaphore 物件的 acquire()方法);在完成對資源的訪問後,執行緒必須向訊號量歸還許可(呼叫 Semaphore 物件的 release()方法)。
61、編寫多執行緒程式有幾種實現方式?
Java 5 以前實現多執行緒有兩種實現方法:一種是繼承 Thread 類;另一種是實現Runnable 介面。兩種方式都要通過重寫 run()方法來定義執行緒的行為,推薦使用後者,因為 Java 中的繼承是單繼承,一個類有一個父類,如果繼承了 Thread 類就無法再繼承其他類了,顯然使用 Runnable 介面更為靈活。
補充:Java 5 以後建立執行緒還有第三種方式:實現 Callable 介面,該介面中的 call方法可以線上程執行結束時產生一個返回值。
62、synchronized 關鍵字的用法?
synchronized 關鍵字可以將物件或者方法標記為同步,以實現對物件和方法的互斥訪問,可以用 synchronized(物件) { … }定義同步程式碼塊,或者在宣告方法時將 synchronized 作為方法的修飾符。在第 60 題的例子中已經展示了synchronized 關鍵字的用法。
63、舉例說明同步和非同步。
如果系統中存在臨界資源(資源數量少於競爭資源的執行緒數量的資源),例如正在寫的資料以後可能被另一個執行緒讀到,或者正在讀的資料可能已經被另一個執行緒寫過了,那麼這些資料就必須進行同步存取(資料庫操作中的排他鎖就是最好的例子)。當應用程式在物件上呼叫了一個需要花費很長時間來執行的方法,並且不希望讓程式等待方法的返回時,就應該使用非同步程式設計,在很多情況下采用非同步途徑往往更有效率。事實上,所謂的同步就是指阻塞式操作,而非同步就是非阻塞式操作。
64、啟動一個執行緒是呼叫 run()還是 start()方法?
啟動一個執行緒是呼叫 start()方法,使執行緒所代表的虛擬處理機處於可執行狀態,這意味著它可以由 JVM 排程並執行,這並不意味著執行緒就會立即執行。run()方法是執行緒啟動後要進行回撥(callback)的方法。
65、什麼是執行緒池(thread pool)?
在物件導向程式設計中,建立和銷燬物件是很費時間的,因為建立一個物件要獲取記憶體資源或者其它更多資源。在 Java 中更是如此,虛擬機器將試圖跟蹤每一個物件,以便能夠在物件銷燬後進行垃圾回收。所以提高服務程式效率的一個手段就是儘可能減少建立和銷燬物件的次數,特別是一些很耗資源的物件建立和銷燬,這就是”池化資源”技術產生的原因。執行緒池顧名思義就是事先建立若干個可執行的執行緒放入一個池(容器)中,需要的時候從池中獲取執行緒不用自行建立,使用完畢不需要銷燬執行緒而是放回池中,從而減少建立和銷燬執行緒物件的開銷。Java 5+中的 Executor 介面定義一個執行執行緒的工具。它的子型別即執行緒池介面是 ExecutorService。要配置一個執行緒池是比較複雜的,尤其是對於執行緒池的原理不是很清楚的情況下,因此在工具類 Executors 面提供了一些靜態工廠方法,生成一些常用的執行緒池,如下所示:
(1)newSingleThreadExecutor:建立一個單執行緒的執行緒池。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。
(2)newFixedThreadPool:建立固定大小的執行緒池。每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。
(3) newCachedThreadPool:建立一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閒(60 秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說 JVM)能夠建立的最大執行緒大小。
(4)newScheduledThreadPool:建立一個大小無限的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。
(5)newSingleThreadExecutor:建立一個單執行緒的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。
第 60 題的例子中演示了通過 Executors 工具類建立執行緒池並使用執行緒池執行執行緒的程式碼。如果希望在伺服器上使用執行緒池,強烈建議使用 newFixedThreadPool方法來建立執行緒池,這樣能獲得更好的效能。
66、執行緒的基本狀態以及狀態之間的關係?
說明:其中 Running 表示執行狀態,Runnable 表示就緒狀態(萬事俱備,只欠CPU),Blocked 表示阻塞狀態,阻塞狀態又有多種情況,可能是因為呼叫 wait()方法進入等待池,也可能是執行同步方法或同步程式碼塊進入等鎖池,或者是呼叫了 sleep()方法或 join()方法等待休眠或其他執行緒結束,或是因為發生了 I/O 中斷。
67、簡述 synchronized 和 java.util.concurrent.locks.Lock的異同?
Lock 是 Java 5 以後引入的新的 API,和關鍵字 synchronized 相比主要相同點:Lock 能完成 synchronized 所實現的所有功能;主要不同點:Lock 有比synchronized 更精確的執行緒語義和更好的效能,而且不強制性的要求一定要獲得鎖。synchronized 會自動釋放鎖,而 Lock 一定要求程式設計師手工釋放,並且最好在 finally 塊中釋放(這是釋放外部資源的最好的地方)。
68、Java 中如何實現序列化,有什麼意義?
序列化就是一種用來處理物件流的機制,所謂物件流也就是將物件的內容進行流化。可以對流化後的物件進行讀寫操作,也可將流化後的物件傳輸於網路之間。序列化是為了解決物件流讀寫操作時可能引發的問題(如果不進行序列化可能會存在資料亂序的問題)。要實現序列化,需要讓一個類實現 Serializable 介面,該介面是一個標識性介面,標註該類物件是可被序列化的,然後使用一個輸出流來構造一個物件輸出流並通過 writeObject(Object)方法就可以將實現物件寫出(即儲存其狀態);如果需要反序列化則可以用一個輸入流建立物件輸入流,然後通過 readObject 方法從流中讀取物件。序列化除了能夠實現物件的持久化之外,還能夠用於物件的深度克隆(可以參考第 29 題)。
69、Java 中有幾種型別的流?
位元組流和字元流。位元組流繼承於 InputStream、OutputStream,字元流繼承於Reader、Writer。在 java.io 包中還有許多其他的流,主要是為了提高效能和使用方便。關於 Java 的 I/O 需要注意的有兩點:一是兩種對稱性(輸入和輸出的對稱性,位元組和字元的對稱性);二是兩種設計模式(介面卡模式和裝潢模式)。另外 Java 中的流不同於 C#的是它只有一個維度一個方向。
70、寫一個方法,輸入一個檔名和一個字串,統計這個字串在這個檔案中出現的次數。
程式碼如下:
import java.io.BufferedReader;
import java.io.FileReader;
public final class MyUtil {
// 工具類中的方法都是靜態方式訪問的因此將構造器私有不允許建立物件
(絕對好習慣)
private MyUtil() {
throw new AssertionError();
}
/**
* 統計給定檔案中給定字串的出現次數
*
* @param filename 檔名
* @param word 字串
* @return 字串在檔案中出現的次數
*/
public static int countWordInFile(String filename, String word) {
int counter = 0;
try (FileReader fr = new FileReader(filename)) {
try (BufferedReader br = new BufferedReader(fr)) {
String line = null;
while ((line = br.readLine()) != null) {
int index = -1;
while (line.length() >= word.length() && (index =
line.indexOf(word)) >= 0) {
counter++;
line = line.substring(index + word.length());
}
}
}
}
catch (Exception ex) {
ex.printStackTrace();
}
return counter;
}
}複製程式碼
71、如何用 Java 程式碼列出一個目錄下所有的檔案?
如果只要求列出當前資料夾下的檔案,程式碼如下所示:
import java.io.File;
class Test12 {
public static void main(String[] args) {
File f = new File("/Users/Hao/Downloads");
for (File temp : f.listFiles()) {
if(temp.isFile()) {
System.out.println(temp.getName());
}
}
}
}複製程式碼
如果需要對資料夾繼續展開,程式碼如下所示:
import java.io.File;
class Test12 {
public static void main(String[] args) {
showDirectory(new File("/Users/Hao/Downloads"));
}
public static void showDirectory(File f) {
_walkDirectory(f, 0);
}
private static void _walkDirectory(File f, int level) {
if(f.isDirectory()) {
for (File temp : f.listFiles()) {
_walkDirectory(temp, level + 1);
}
} else {
for (int i = 0; i < level - 1; i++) {
System.out.print("t");
}
System.out.println(f.getName());
}
}
}複製程式碼
在 Java 7 中可以使用 NIO.2 的 API 來做同樣的事情,程式碼如下所示:
class ShowFileTest {
public static void main(String[] args) throws IOException {
Path initPath = Paths.get("/Users/Hao/Downloads");
Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes
attrs)
throws IOException {
System.out.println(file.getFileName().toString());
return FileVisitResult.CONTINUE;
}
}
);
}
}複製程式碼
72、用 Java 的套接字程式設計實現一個多執行緒的回顯(echo)伺服器。
73、XML 文件定義有幾種形式?它們之間有何本質區別?解析XML 文件有哪幾種方式?
XML 文件定義分為 DTD 和 Schema 兩種形式,二者都是對 XML 語法的約束,其本質區別在於 Schema 本身也是一個 XML 檔案,可以被 XML 解析器解析,而且可以為 XML 承載的資料定義型別,約束能力較之 DTD 更強大。對 XML 的解析主要有 DOM(文件物件模型,Document Object Model)、SAX(Simple API forXML)和 StAX(Java 6 中引入的新的解析 XML 的方式,Streaming API for XML),其中 DOM 處理大型檔案時其效能下降的非常厲害,這個問題是由 DOM 樹結構佔用的記憶體較多造成的,而且 DOM 解析方式必須在解析檔案之前把整個文件裝入記憶體,適合對 XML 的隨機訪問(典型的用空間換取時間的策略);SAX 是事件驅動型的 XML 解析方式,它順序讀取 XML 檔案,不需要一次全部裝載整個檔案。當遇到像檔案開頭,文件結束,或者標籤開頭與標籤結束時,它會觸發一個事件,使用者通過事件回撥程式碼來處理 XML 檔案,適合對 XML 的順序訪問;顧名思義,StAX 把重點放在流上,實際上 StAX 與其他解析方式的本質區別就在於應用程式能夠把 XML 作為一個事件流來處理。將 XML 作為一組事件來處理的想法並不新穎( SAX 就是這樣做的),但不同之處在於 StAX 允許應用程式程式碼把這些事件逐個拉出來,而不用提供在解析器方便時從解析器中接收事件的處理程式。
74、你在專案中哪些地方用到了 XML?
XML 的主要作用有兩個方面:資料交換和資訊配置。在做資料交換時,XML 將資料用標籤組裝成起來,然後壓縮打包加密後通過網路傳送給接收者,接收解密與解壓縮後再從 XML 檔案中還原相關資訊進行處理,XML 曾經是異構系統間交換資料的事實標準,但此項功能幾乎已經被被JSON(JavaScript Object Notation)取而代之。當然,目前很多軟體仍然使用 XML 來儲存配置資訊,我們在很多專案中通常也會將作為配置資訊的硬程式碼寫在 XML 檔案中,Java 的很多框架也是這麼做的,而且這些框架都選擇了 dom4j 作為處理 XML 的工具,因為 Sun 公司的官方API 實在不怎麼好用。
補充:現在有很多時髦的軟體(如 Sublime)已經開始將配置檔案書寫成 JSON格式,我們已經強烈的感受到 XML 的另一項功能也將逐漸被業界拋棄。
75、闡述 JDBC 運算元據庫的步驟。
下面的程式碼以連線本機的 Oracle 資料庫為例,演示 JDBC 運算元據庫的步驟。
(1) 載入驅動。
Class.forName("oracle.jdbc.driver.OracleDriver");複製程式碼
(2) 建立連線。
Connection con =
DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl",
"scott", "tiger");複製程式碼
(3) 建立語句。
PreparedStatement ps = con.prepareStatement("select * from emp
where sal between ? and ?");
ps.setint(1, 1000);
ps.setint(2, 3000);複製程式碼
(4)執行語句。
ResultSet rs = ps.executeQuery();複製程式碼
(5)處理結果。
while(rs.next()) {
System.out.println(rs.getint("empno") + " - " +
rs.getString("ename"));
}複製程式碼
(6) 關閉資源。
finally {
if(con != null) {
try {
con.close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
}複製程式碼
提示:關閉外部資源的順序應該和開啟的順序相反,也就是說先關閉 ResultSet、再關閉 Statement、在關閉 Connection。上面的程式碼只關閉了 Connection(連線),雖然通常情況下在關閉連線時,連線上建立的語句和開啟的遊標也會關閉,但不能保證總是如此,因此應該按照剛才說的順序分別關閉。此外,第一步載入驅動在 JDBC 4.0 中是可以省略的(自動從類路徑中載入驅動),但是我們建議保留。
76、Statement 和 PreparedStatement 有什麼區別?哪個效能更好?
與 Statement 相比,①PreparedStatement 介面代表預編譯的語句,它主要的優勢在於可以減少 SQL 的編譯錯誤並增加 SQL 的安全性(減少 SQL 注射攻擊的可能性);②PreparedStatement 中的 SQL 語句是可以帶引數的,避免了用字串連線拼接 SQL 語句的麻煩和不安全;③當批量處理 SQL 或頻繁執行相同的查詢時,PreparedStatement 有明顯的效能上的優勢,由於資料庫可以將編譯優化後的SQL 語句快取起來,下次執行相同結構的語句時就會很快(不用再次編譯和生成執行計劃)。
補充:為了提供對儲存過程的呼叫,JDBC API 中還提供了 CallableStatement 介面。儲存過程(Stored Procedure)是資料庫中一組為了完成特定功能的 SQL 語句的集合,經編譯後儲存在資料庫中,使用者通過指定儲存過程的名字並給出引數(如果該儲存過程帶有引數)來執行它。雖然呼叫儲存過程會在網路開銷、安全性、效能上獲得很多好處,但是存在如果底層資料庫發生遷移時就會有很多麻煩,因為每種資料庫的儲存過程在書寫上存在不少的差別。
77、使用 JDBC 運算元據庫時,如何提升讀取資料的效能?如何提升更新資料的效能?
要提升讀取資料的效能,可以指定通過結果集(ResultSet)物件的 setFetchSize()方法指定每次抓取的記錄數(典型的空間換時間策略);要提升更新資料的效能可以使用 PreparedStatement 語句構建批處理,將若干 SQL 語句置於一個批處理中執行。
78、在進行資料庫程式設計時,連線池有什麼作用?
由於建立連線和釋放連線都有很大的開銷(尤其是資料庫伺服器不在本地時,每次建立連線都需要進行 TCP 的三次握手,釋放連線需要進行 TCP 四次握手,造成的開銷是不可忽視的),為了提升系統訪問資料庫的效能,可以事先建立若干連線置於連線池中,需要時直接從連線池獲取,使用結束時歸還連線池而不必關閉連線,從而避免頻繁建立和釋放連線所造成的開銷,這是典型的用空間換取時間的策略(浪費了空間儲存連線,但節省了建立和釋放連線的時間)。池化技術在Java 開發中是很常見的,在使用執行緒時建立執行緒池的道理與此相同。基於 Java 的開源資料庫連線池主要有:C3P0、Proxool、DBCP、BoneCP、Druid 等。
補充:在計算機系統中時間和空間是不可調和的矛盾,理解這一點對設計滿足效能要求的演算法是至關重要的。大型網站效能優化的一個關鍵就是使用快取,而快取跟上面講的連線池道理非常類似,也是使用空間換時間的策略。可以將熱點資料置於快取中,當使用者查詢這些資料時可以直接從快取中得到,這無論如何也快過去資料庫中查詢。當然,快取的置換策略等也會對系統效能產生重要影響,對於這個問題的討論已經超出了這裡要闡述的範圍。
79、什麼是 DAO 模式?
DAO(Data Access Object)顧名思義是一個為資料庫或其他持久化機制提供了抽象介面的物件,在不暴露底層持久化方案實現細節的前提下提供了各種資料訪問操作。在實際的開發中,應該將所有對資料來源的訪問操作進行抽象化後封裝在一個公共 API 中。用程式設計語言來說,就是建立一個介面,介面中定義了此應用程式中將會用到的所有事務方法。在這個應用程式中,當需要和資料來源進行互動的時候則使用這個介面,並且編寫一個單獨的類來實現這個介面,在邏輯上該類對應一個特定的資料儲存。DAO 模式實際上包含了兩個模式,一是 DataAccessor(資料訪問器),二是 Data Object(資料物件),前者要解決如何訪問資料的問題,而後者要解決的是如何用物件封裝資料。
80、事務的 ACID 是指什麼?
(1)原子性(Atomic):事務中各項操作,要麼全做要麼全不做,任何一項操作的失敗都會導致整個事務的失敗;
(2)一致性(Consistent):事務結束後系統狀態是一致的;
(3)隔離性(Isolated):併發執行的事務彼此無法看到對方的中間狀態;
(4)永續性(Durable):事務完成後所做的改動都會被持久化,即使發生災難性的失敗。通過日誌和同步備份可以在故障發生後重建資料。
補充:關於事務,在面試中被問到的概率是很高的,可以問的問題也是很多的。首先需要知道的是,只有存在併發資料訪問時才需要事務。當多個事務訪問同一資料時,可能會存在 5 類問題,包括 3 類資料讀取問題(髒讀、不可重複讀和幻讀)和 2 類資料更新問題(第 1 類丟失更新和第 2 類丟失更新)。
髒讀(Dirty Read):A 事務讀取 B 事務尚未提交的資料並在此基礎上操作,而 B事務執行回滾,那麼 A 讀取到的資料就是髒資料。
不可重複讀(Unrepeatable Read):事務 A 重新讀取前面讀取過的資料,發現該資料已經被另一個已提交的事務 B 修改過了。
幻讀(Phantom Read):事務 A 重新執行一個查詢,返回一系列符合查詢條件的行,發現其中插入了被事務 B 提交的行。
第 1 類丟失更新:事務 A 撤銷時,把已經提交的事務 B 的更新資料覆蓋了。
第 2 類丟失更新:事務 A 覆蓋事務 B 已經提交的資料,造成事務 B 所做的操作丟失。
資料併發訪問所產生的問題,在有些場景下可能是允許的,但是有些場景下可能就是致命的,資料庫通常會通過鎖機制來解決資料併發訪問問題,按鎖定物件不同可以分為表級鎖和行級鎖;按併發事務鎖定關係可以分為共享鎖和獨佔鎖,具體的內容大家可以自行查閱資料進行了解。直接使用鎖是非常麻煩的,為此資料庫為使用者提供了自動鎖機制,只要使用者指定會話的事務隔離級別,資料庫就會通過分析 SQL 語句然後為事務訪問的資源加上合適的鎖,此外,資料庫還會維護這些鎖通過各種手段提高系統的效能,這些對使用者來說都是透明的(就是說你不用理解,事實上我確實也不知道)。ANSI/ISOSQL 92 標準定義了 4 個等級的事務隔離級別,如下表所示:
需要說明的是,事務隔離級別和資料訪問的併發性是對立的,事務隔離級別越高併發性就越差。所以要根據具體的應用來確定合適的事務隔離級別,這個地方沒有萬能的原則。
81、JDBC 中如何進行事務處理?
Connection 提供了事務處理的方法,通過呼叫 setAutoCommit(false)可以設定手動提交事務;當事務完成後用 commit()顯式提交事務;如果在事務處理過程中發生異常則通過 rollback()進行事務回滾。除此之外,從 JDBC 3.0 中還引入了Savepoint(儲存點)的概念,允許通過程式碼設定儲存點並讓事務回滾到指定的儲存點。
82、JDBC 能否處理 Blob 和 Clob?
Blob 是指二進位制大物件(Binary Large Object),而 Clob 是指大字元物件(Character Large Objec),因此其中 Blob 是為儲存大的二進位制資料而設計的,而 Clob 是為儲存大的文字資料而設計的。JDBC 的 PreparedStatement 和ResultSet 都提供了相應的方法來支援 Blob 和 Clob 操作。
83、簡述正規表示式及其用途。
在編寫處理字串的程式時,經常會有查詢符合某些複雜規則的字串的需要。正規表示式就是用於描述這些規則的工具。換句話說,正規表示式就是記錄文字規則的程式碼。
說明:計算機誕生初期處理的資訊幾乎都是數值,但是時過境遷,今天我們使用計算機處理的資訊更多的時候不是數值而是字串,正規表示式就是在進行字串匹配和處理的時候最為強大的工具,絕大多數語言都提供了對正規表示式的支援。
84、Java 中是如何支援正規表示式操作的?
Java 中的 String 類提供了支援正規表示式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java 中可以用 Pattern 類表示正規表示式物件,它提供了豐富的 API 進行各種正規表示式操作。
面試題: - 如果要從字串中擷取第一個英文左括號之前的字串,例如:北京市(朝陽區)(西城區)(海淀區),擷取結果為:北京市,那麼正規表示式怎麼寫?
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class RegExpTest {
public static void main(String[] args) {
String str = "北京市(朝陽區)(西城區)(海淀區)";
Pattern p = Pattern.compile(".*?(?=\()");
Matcher m = p.matcher(str);
if(m.find()) {
System.out.println(m.group());
}
}
}複製程式碼
85、獲得一個類的類物件有哪些方式?
(1)方法 1:型別.class,例如:String.class
(2)方法 2:物件.getClass(),例如:”hello”.getClass()
(3)方法 3:Class.forName(),例如:Class.forName(“java.lang.String”)
86、如何通過反射建立物件?
方法 1:通過類物件呼叫 newInstance()方法,例如:String.class.newInstance()
方法 2:通過類物件的 getConstructor()或 getDeclaredConstructor()方法獲得構造器(Constructor)物件並呼叫其 newInstance()方法建立物件,例如:String.class.getConstructor(String.class).newInstance(“Hello”);
87、如何通過反射獲取和設定物件私有欄位的值?
可以通過類物件的 getDeclaredField()方法欄位(Field)物件,然後再通過欄位物件的 setAccessible(true)將其設定為可以訪問,接下來就可以通過 get/set 方法來獲取/設定欄位的值了。下面的程式碼實現了一個反射的工具類,其中的兩個靜態方法分別用於獲取和設定私有欄位的值,欄位可以是基本型別也可以是物件型別且支援多級物件操作。
88、如何通過反射呼叫物件的方法?
請看下面的程式碼:
import java.lang.reflect.Method;
class MethodInvokeTest {
public static void main(String[] args) throws Exception {
String str = "hello";
Method m = str.getClass().getMethod("toUpperCase");
System.out.println(m.invoke(str));
// HELLO
}
}複製程式碼
89、簡述一下物件導向的”六原則一法則”。
(1)單一職責原則:一個類只做它該做的事情。(單一職責原則想表達的就是”高內聚”,寫程式碼最終極的原則只有六個字”高內聚、低耦合”,就如同葵花寶典或辟邪劍譜的中心思想就八個字”欲練此功必先自宮”,所謂的高內聚就是一個程式碼模組只完成一項功能,在物件導向中,如果只讓一個類完成它該做的事,而不涉及與它無關的領域就是踐行了高內聚的原則,這個類就只有單一職責。我們都知道一句話叫”因為專注,所以專業”,一個物件如果承擔太多的職責,那麼註定它什麼都做不好。這個世界上任何好的東西都有兩個特徵,一個是功能單一,好的相機絕對不是電視購物裡面賣的那種一個機器有一百多種功能的,它基本上只能照相;另一個是模組化,好的自行車是組裝車,從減震叉、剎車到變速器,所有的部件都是可以拆卸和重新組裝的,好的乒乓球拍也不是成品拍,一定是底板和膠皮可以拆分和自行組裝的,一個好的軟體系統,它裡面的每個功能模組也應該是可以輕易的拿到其他系統中使用的,這樣才能實現軟體複用的目標。)
(2)開閉原則:軟體實體應當對擴充套件開放,對修改關閉。(在理想的狀態下,當我們需要為一個軟體系統增加新功能時,只需要從原來的系統派生出一些新類就可以,不需要修改原來的任何一行程式碼。要做到開閉有兩個要點:①抽象是關鍵,一個系統中如果沒有抽象類或介面系統就沒有擴充套件點;②封裝可變性,將系統中的各種可變因素封裝到一個繼承結構中,如果多個可變因素混雜在一起,系統將變得複雜而換亂,如果不清楚如何封裝可變性,可以參考《設計模式精解》一書中對橋樑模式的講解的章節。)
(3)依賴倒轉原則:面向介面程式設計。(該原則說得直白和具體一些就是宣告方法的引數型別、方法的返回型別、變數的引用型別時,儘可能使用抽象型別而不用具體型別,因為抽象型別可以被它的任何一個子型別所替代,請參考下面的里氏替換原則。)
(4)里氏替換原則:任何時候都可以用子型別替換掉父型別。(關於里氏替換原則的描述,Barbara Liskov 女士的描述比這個要複雜得多,但簡單的說就是能用父型別的地方就一定能使用子型別。里氏替換原則可以檢查繼承關係是否合理,如果一個繼承關係違背了里氏替換原則,那麼這個繼承關係一定是錯誤的,需要對程式碼進行重構。例如讓貓繼承狗,或者狗繼承貓,又或者讓正方形繼承長方形都是錯誤的繼承關係,因為你很容易找到違反里氏替換原則的場景。需要注意的是:子類一定是增加父類的能力而不是減少父類的能力,因為子類比父類的能力更多,把能力多的物件當成能力少的物件來用當然沒有任何問題。)
(5)介面隔離原則:介面要小而專,絕不能大而全。(臃腫的介面是對介面的汙染,既然介面表示能力,那麼一個介面只應該描述一種能力,介面也應該是高度內聚的。例如,琴棋書畫就應該分別設計為四個介面,而不應設計成一個介面中的四個方法,因為如果設計成一個介面中的四個方法,那麼這個介面很難用,畢竟琴棋書畫四樣都精通的人還是少數,而如果設計成四個介面,會幾項就實現幾個介面,這樣的話每個介面被複用的可能性是很高的。Java 中的介面代表能力、代表約定、代表角色,能否正確的使用介面一定是程式設計水平高低的重要標識。)
(6)合成聚合複用原則:優先使用聚合或合成關係複用程式碼。(通過繼承來複用程式碼是物件導向程式設計中被濫用得最多的東西,因為所有的教科書都無一例外的對繼承進行了鼓吹從而誤導了初學者,類與類之間簡單的說有三種關係,Is-A 關係、Has-A 關係、Use-A 關係,分別代表繼承、關聯和依賴。其中,關聯關係根據其關聯的強度又可以進一步劃分為關聯、聚合和合成,但說白了都是Has-A 關係,合成聚合複用原則想表達的是優先考慮 Has-A 關係而不是 Is-A 關係複用程式碼,原因嘛可以自己從百度上找到一萬個理由,需要說明的是,即使在Java 的 API 中也有不少濫用繼承的例子,例如 Properties 類繼承了 Hashtable類,Stack 類繼承了 Vector 類,這些繼承明顯就是錯誤的,更好的做法是在Properties 類中放置一個 Hashtable 型別的成員並且將其鍵和值都設定為字串來儲存資料,而 Stack 類的設計也應該是在 Stack 類中放一個 Vector 物件來儲存資料。記住:任何時候都不要繼承工具類,工具是可以擁有並可以使用的,而不是拿來繼承的。)
(7)迪米特法則:迪米特法則又叫最少知識原則,一個物件應當對其他物件有儘可能少的瞭解。(迪米特法則簡單的說就是如何做到”低耦合”,門面模式和調停者模式就是對迪米特法則的踐行。對於門面模式可以舉一個簡單的例子,你去一家公司洽談業務,你不需要了解這個公司內部是如何運作的,你甚至可以對這個公司一無所知,去的時候只需要找到公司入口處的前臺美女,告訴她們你要做什麼,她們會找到合適的人跟你接洽,前臺的美女就是公司這個系統的門面。再複雜的系統都可以為使用者提供一個簡單的門面,Java Web 開發中作為前端控制器的 Servlet 或 Filter 不就是一個門面嗎,瀏覽器對伺服器的運作方式一無所知,但是通過前端控制器就能夠根據你的請求得到相應的服務。調停者模式也可以舉一個簡單的例子來說明,例如一臺計算機,CPU、記憶體、硬碟、顯示卡、音效卡各種裝置需要相互配合才能很好的工作,但是如果這些東西都直接連線到一起,計算機的佈線將異常複雜,在這種情況下,主機板作為一個調停者的身份出現,它將各個裝置連線在一起而不需要每個裝置之間直接交換資料,這樣就減小了系統的耦合度和複雜度,如下圖所示。迪米特法則用通俗的話來將就是不要和陌生人打交道,如果真的需要,找一個自己的朋友,讓他替你和陌生人打交道。)
90、簡述一下你瞭解的設計模式。
所謂設計模式,就是一套被反覆使用的程式碼設計經驗的總結(情境中一個問題經過證實的一個解決方案)。使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。設計模式使人們可以更加簡單方便的複用成功的設計和體系結構。將已證實的技術表述成設計模式也會使新系統開發者更加容易理解其設計思路。
在 GoF 的《Design Patterns: Elements of Reusable Object-OrientedSoftware》中給出了三類(建立型[對類的例項化過程的抽象化]、結構型[描述如何將類或物件結合在一起形成更大的結構]、行為型[對在不同的物件之間劃分責任和演算法的抽象化])共 23 種設計模式,包括:Abstract Factory(抽象工廠模式),Builder(建造者模式),Factory Method(工廠方法模式),Prototype(原始模型模式),Singleton(單例模式);Facade(門面模式),Adapter(介面卡模式),Bridge(橋樑模式),Composite(合成模式),Decorator(裝飾模式),Flyweight(享元模式),Proxy(代理模式);Command(命令模式),Interpreter(直譯器模式),Visitor(訪問者模式),Iterator(迭代子模式),Mediator(調停者模式),Memento(備忘錄模式),Observer(觀察者模式),State(狀態 模式 ),Strategy(策略 模式 ),Template Method(模板方法模式),Chain Of Responsibility(責任鏈模式)。
面試被問到關於設計模式的知識時,可以揀最常用的作答,例如:
(1)工廠模式:工廠類可以根據條件生成不同的子類例項,這些子類有一個公共的抽象父類並且實現了相同的方法,但是這些方法針對不同的資料進行了不同的操作(多型方法)。當得到子類的例項後,開發人員可以呼叫基類中的方法而不必考慮到底返回的是哪一個子類的例項。
(2)代理模式:給一個物件提供一個代理物件,並由代理物件控制原物件的引用。實際開發中,按照使用目的的不同,代理可以分為:遠端代理、虛擬代理、保護代理、Cache 代理、防火牆代理、同步化代理、智慧引用代理。
(3)介面卡模式:把一個類的介面變換成客戶端所期待的另一種介面,從而使原本因介面不匹配而無法在一起使用的類能夠一起工作。
(4)模板方法模式:提供一個抽象類,將部分邏輯以具體方法或構造器的形式實現,然後宣告一些抽象方法來迫使子類實現剩餘的邏輯。不同的子類可以以不同的方式實現這些抽象方法(多型實現),從而實現不同的業務邏輯。除此之外,還可以講講上面提到的門面模式、橋樑模式、單例模式、裝潢模式(Collections 工具類和 I/O 系統中都使用裝潢模式)等,反正基本原則就是揀自己最熟悉的、用得最多的作答,以免言多必失。
91、用 Java 寫一個單例類。
(1)餓漢式單例
public class Singleton {
private Singleton(){
}
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}複製程式碼
(2)懶漢式單例
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance(){
if (instance == null) instance = new Singleton();
return instance;
}
}複製程式碼
注意:實現一個單例有兩點注意事項,①將構造器私有,不允許外界通過構造器建立物件;②通過公開的靜態方法向外界返回類的唯一例項。這裡有一個問題可以思考:Spring 的 IoC 容器可以為普通的類建立單例,它是怎麼做到的呢?
92、什麼是 UML?
UML 是統一建模語言(Unified Modeling Language)的縮寫,它發表於 1997年,綜合了當時已經存在的物件導向的建模語言、方法和過程,是一個支援模型化和軟體系統開發的圖形化語言,為軟體開發的所有階段提供模型化和視覺化支援。使用 UML 可以幫助溝通與交流,輔助應用設計和文件的生成,還能夠闡釋系統的結構和行為。
93、UML 中有哪些常用的圖?
UML 定義了多種圖形化的符號來描述軟體系統部分或全部的靜態結構和動態結構,包括:用例圖(use case diagram)、類圖(class diagram)、時序圖(sequencediagram)、協作圖(collaboration diagram)、狀態圖(statechart diagram)、活動圖(activity diagram)、構件圖(component diagram)、部署圖(deploymentdiagram)等。在這些圖形化符號中,有三種圖最為重要,分別是:用例圖(用來捕獲需求,描述系統的功能,通過該圖可以迅速的瞭解系統的功能模組及其關係)、類圖(描述類以及類與類之間的關係,通過該圖可以快速瞭解系統)、時序圖(描述執行特定任務時物件之間的互動關係以及執行順序,通過該圖可以瞭解物件能接收的訊息也就是說物件能夠向外界提供的服務)。用例圖:
類圖:
時序圖:
94、用 Java 寫一個氣泡排序。
氣泡排序幾乎是個程式設計師都寫得出來,但是面試的時候如何寫一個逼格高的氣泡排序卻不是每個人都能做到,下面提供一個參考程式碼:
import java.util.Comparator;
/**
* 排序器介面(策略模式: 將演算法封裝到具有共同介面的獨立的類中使得它們可
以相互替換)
* @author 駱昊
*
*/
public interface Sorter {
/**
* 排序
* @param list 待排序的陣列
*/
public <T extends Comparable<T>> void sort(T[] list);
/**
* 排序
* @param list 待排序的陣列
* @param comp 比較兩個物件的比較器
*/
public <T> void sort(T[] list, Comparator<T> comp);
}
import java.util.Comparator;
/**
* 氣泡排序
*
* @author 駱昊
*
*/
public class BubbleSorter implements Sorter {
@Override
public <T extends Comparable<T>> void sort(T[] list) {
Boolean swapped = true;
for (int i = 1, len = list.length; i < len && swapped; ++i) {
swapped = false;
for (int j = 0; j < len - i; ++j) {
if (list[j].compareTo(list[j + 1]) > 0) {
T temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
swapped = true;
}
}
}
}
@Override
public <T> void sort(T[] list, Comparator<T> comp) {
Boolean swapped = true;
for (int i = 1, len = list.length; i < len && swapped; ++i) {
swapped = false;
for (int j = 0; j < len - i; ++j) {
if (comp.compare(list[j], list[j + 1]) > 0) {
T temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
swapped = true;
}
}
}
}
}複製程式碼
95、用 Java 寫一個折半查詢。
折半查詢,也稱二分查詢、二分搜尋,是一種在有序陣列中查詢某一特定元素的搜尋演算法。搜素過程從陣列的中間元素開始,如果中間元素正好是要查詢的元素,則搜素過程結束;如果某一特定元素大於或者小於中間元素,則在陣列大於或小於中間元素的那一半中查詢,而且跟開始一樣從中間元素開始比較。如果在某一步驟陣列已經為空,則表示找不到指定的元素。這種搜尋演算法每一次比較都使搜尋範圍縮小一半,其時間複雜度是 O(logN)。
import java.util.Comparator;
public class MyUtil {
public static <T extends Comparable<T>> int binarySearch(T[] x, T
key) {
return binarySearch(x, 0, x.length- 1, key);
}
// 使用迴圈實現的二分查詢
public static <T> int binarySearch(T[] x, T key, Comparator<T> comp)
{
int low = 0;
int high = x.length - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
int cmp = comp.compare(x[mid], key);
if (cmp < 0) {
low= mid + 1;
} else if (cmp > 0) {
high= mid - 1;
} else {
return mid;
}
}
return -1;
}
// 使用遞迴實現的二分查詢
private static<T extends Comparable<T>> int binarySearch(T[] x, int
low, int high, T key) {
if(low <= high) {
int mid = low + ((high -low) >> 1);
if(key.compareTo(x[mid])== 0) {
return mid;
} else if(key.compareTo(x[mid])< 0) {
return binarySearch(x,low, mid - 1, key);
} else {
return binarySearch(x,mid + 1, high, key);
}
}
return -1;
}
}複製程式碼
說明:上面的程式碼中給出了折半查詢的兩個版本,一個用遞迴實現,一個用迴圈實現。需要注意的是計算中間位置時不應該使用(high+ low) / 2 的方式,因為加法運算可能導致整數越界,這裡應該使用以下三種方式之一:low + (high - low)/ 2 或 low + (high – low) >> 1 或(low + high) >>> 1(>>>是邏輯右移,是不帶符號位的右移)
最後
歡迎大家關注我的公種浩【程式設計師追風】,整理了1000道2019年多家公司java面試題400多頁pdf文件,文章都會在裡面更新,整理的資料也會放在裡面。喜歡文章記得關注我點個贊喲,感謝支援!