Java經典面試題(二)-不古出品

不古Mrbugu發表於2021-11-12

@

目錄

1. 為什麼說 Java 語言“編譯與解釋並存”?

  • 高階程式語言按照程式的執行方式分為編譯型和解釋型兩種。簡單來說,編譯型語言是指編譯器針對特定的作業系統將原始碼一次性翻譯成可被該平臺執行的機器碼;解釋型語言是指直譯器對源程式逐行解釋成特定平臺的機器碼並立即執行。比如,你想閱讀一本英文名著,你可以找一個英文翻譯人員幫助你閱讀, 有兩種選擇方式,你可以先等翻譯人員將全本的英文名著(也就是原始碼)都翻譯成漢語,再去閱讀,也可以讓翻譯人員翻譯一段,你在旁邊閱讀一段,慢慢把書讀完。

  • Java 語言既具有編譯型語言的特徵,也具有解釋型語言的特徵,因為 Java 程式要經過先編譯,後解釋兩個步驟,由 Java 編寫的程式需要先經過編譯步驟,生成位元組碼(*.class 檔案),這種位元組碼必須由 Java 直譯器來解釋執行。因此,我們可以認為 Java 語言編譯與解釋並存。

2.Oracle JDK 和 OpenJDK 的對比?

  • 1、Oracle JDK 比 OpenJDK 更穩定。OpenJDK 和 Oracle JDK 的程式碼幾乎相同,但 Oracle JDK 有更多的類和一些錯誤修復。因此,如果您想開發企業/商業軟體,我建議您選擇 Oracle JDK,因為它經過了徹底的測試和穩定。某些情況下,有些人提到在使用 OpenJDK 可能會遇到了許多應用程式崩潰的問題,但是,只需切換到 Oracle JDK 就可以解決問題;

  • 2、在響應性和 JVM 效能方面,Oracle JDK 與 OpenJDK 相比提供了更好的效能;

  • 3、Oracle JDK 不會為即將釋出的版本提供長期支援,使用者每次都必須通過更新到最新版本獲得支援來獲取最新版本;

  • 3.字元型常量和字串常量的區別?

  • 形式 : 字元常量是單引號引起的一個字元,字串常量是雙引號引起的 0 個或若干個字元

  • 含義 : 字元常量相當於一個整型值( ASCII 值),可以參加表示式運算; 字串常量代表一個地址值(該字串在記憶體中存放位置)

  • 佔記憶體大小 : 字元常量只佔 2 個位元組; 字串常量佔若干個位元組 (注意: char 在 Java 中佔兩個位元組),

4.Java 泛型瞭解麼?什麼是型別擦除?介紹一下常用的萬用字元?

  • Java 泛型(generics)是 JDK 5 中引入的一個新特性, 泛型提供了編譯時型別安全檢測機制,該機制允許程式設計師在編譯時檢測到非法的型別。泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。

  • Java 的泛型是偽泛型,這是因為 Java 在執行期間,所有的泛型資訊都會被擦掉,這也就是通常所說型別擦除 。

List<Integer> list = new ArrayList<>();

list.add(12);
//這裡直接新增會報錯
list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通過反射新增,是可以的
add.invoke(list, "kl");

System.out.println(list);

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

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 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);

常用的萬用字元為: T,E,K,V,?

  • ? 表示不確定的 java 型別
  • T (type) 表示具體的一個 java 型別
  • K V (key value) 分別代表 java 鍵值中的 Key Value
  • E (element) 代表 Element

5.深拷貝與淺拷貝

  • 淺拷貝:對基本資料型別進行值傳遞,對引用資料型別進行引用傳遞般的拷貝,此為淺拷貝。
  • 深拷貝:對基本資料型別進行值傳遞,對引用資料型別,建立一個新的物件,並複製其內容,此為深拷貝。
    深淺拷貝

6.Object 類的常見方法總結

  • Object 類是一個特殊的類,是所有類的父類。它主要提供了以下 11 個方法:
public final native Class<?> getClass()//native方法,用於返回當前執行時物件的Class物件,使用了final關鍵字修飾,故不允許子類重寫。

public native int hashCode() //native方法,用於返回物件的雜湊碼,主要使用在雜湊表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用於比較2個物件的記憶體地址是否相等,String類對該方法進行了重寫使用者比較字串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用於建立並返回當前物件的一份拷貝。一般情況下,對於任何物件 x,表示式 x.clone() != x 為true,x.clone().getClass() == x.getClass() 為true。Object本身沒有實現Cloneable介面,所以不重寫clone方法並且進行呼叫的話會發生CloneNotSupportedException異常。

public String toString()//返回類的名字@例項的雜湊碼的16進位制的字串。建議Object所有的子類都重寫這個方法。

public final native void notify()//native方法,並且不能重寫。喚醒一個在此物件監視器上等待的執行緒(監視器相當於就是鎖的概念)。如果有多個執行緒在等待只會任意喚醒一個。

public final native void notifyAll()//native方法,並且不能重寫。跟notify一樣,唯一的區別就是會喚醒在此物件監視器上等待的所有執行緒,而不是一個執行緒。

public final native void wait(long timeout) throws InterruptedException//native方法,並且不能重寫。暫停執行緒的執行。注意:sleep方法沒有釋放鎖,而wait方法釋放了鎖 。timeout是等待時間。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos引數,這個參數列示額外時間(以毫微秒為單位,範圍是 0-999999)。 所以超時的時間還需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2個wait方法一樣,只不過該方法一直等待,沒有超時時間這個概念

protected void finalize() throws Throwable { }//例項被垃圾回收器回收的時候觸發的操作

7.Java 異常類層次結構圖

在這裡插入圖片描述

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

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

簡單來說:

  • 序列化: 將資料結構或物件轉換成二進位制位元組流的過程
  • 反序列化:將在序列化過程中所生成的二進位制位元組流轉換成資料結構或者物件的過程
    對於 Java 這種物件導向程式語言來說,我們序列化的都是物件(Object)也就是例項化後的類(Class),但是在 C++這種半物件導向的語言中,struct(結構體)定義的是資料結構型別,而 class 對應的是物件型別。

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

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

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

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

10.continue、break、和 return 的區別是什麼?

  • 在迴圈結構中,當迴圈條件不滿足或者迴圈次數達到要求時,迴圈會正常結束。但是,有時候可能需要在迴圈的過程中,當發生了某種條件之後 ,提前終止迴圈,這就需要用到下面幾個關鍵詞:

  • continue :指跳出當前的這一次迴圈,繼續下一次迴圈。
    break :指跳出整個迴圈體,繼續執行迴圈下面的語句。
    return 用於跳出所在方法,結束該方法的執行。return 一般有兩種用法:
    return; :直接使用 return 結束方法執行,用於沒有返回值函式的方法
    return value; :return 一個特定值,用於有返回值函式的方法.

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

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

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

12.final 在 java 中有什麼作用?

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

13.抽象類必須要有抽象方法嗎?

不需要,抽象類不一定非要有抽象方法。

示例程式碼:

abstract class Cat {
    public static void sayHi() {
        System.out.println("hi~");
    }
}

上面程式碼,抽象類並沒有抽象方法但完全可以正常執行。

14.普通類和抽象類有哪些區別?

  • 普通類不能包含抽象方法,抽象類可以包含抽象方法。
  • 抽象類不能直接例項化,普通類可以直接例項化。

15.抽象類能使用 final 修飾嗎?

  • 不能,定義抽象類就是讓其他類繼承的,如果定義為 final 該類就不能被繼承,這樣彼此就會產生矛盾,所以 final 不能修飾抽象類

16.介面和抽象類有什麼區別?

  • 實現:抽象類的子類使用 extends 來繼承;介面必須使用 implements 來實現介面。
  • 建構函式:抽象類可以有建構函式;介面不能有。
  • main 方法:抽象類可以有 main 方法,並且我們能執行它;介面不能有 main 方法。
  • 實現數量:類可以實現很多個介面;但是隻能繼承一個抽象類。
  • 訪問修飾符:介面中的方法預設使用 public 修飾;抽象類中的方法可以是任意訪問修飾符。

17.char型變數中能不能存貯一箇中文漢字?為什麼?

  • char型變數是用來儲存Unicode編碼的字元的,unicode編碼字符集中包含了漢字,所以,char型變數中當然可以儲存漢字啦。不過,如果某個特殊的漢字沒有被包含在unicode編碼字符集中,那麼,這個char型變數中就不能儲存這個特殊漢字。補充說明:unicode編碼佔用兩個位元組,所以,char型別的變數也是佔用兩個位元組。

18.用最有效率的方法算出2乘以8等於幾?

  • 2<< 3,(左移三位)因為將一個數左移n位,就相當於乘以了2的n次方,那麼,一個數乘以8只要將其左移3位即可,而位運算cpu直接支援的,效率最高,所以,2乘以8等於幾的最效率的方法是2<< 3。

19.闡述final、finally、finalize的區別

  • final:修飾符(關鍵字)有三種用法:如果一個類被宣告為final,意味著它不能再派生出新的子類,即不能被繼承,因此它和abstract是反義詞。將變數宣告為final,可以保證它們在使用中不被改變,被宣告為final的變數必須在宣告時給定初值,而在以後的引用中只能讀取不可修改。被宣告為final的方法也同樣只能使用,不能在子類中被重寫。
  • finally:通常放在try…catch…的後面構造總是執行程式碼塊,這就意味著程式無論正常執行還是發生異常,這裡的程式碼只要JVM不關閉都能執行,可以將釋放外部資源的程式碼寫在finally塊中。
  • finalize:Object類中定義的方法,Java中允許使用finalize()方法在垃圾收集器將物件從記憶體中清除出去之前做必要的清理工作。這個方法是由垃圾收集器在銷燬物件時呼叫的,通過重寫finalize()方法可以整理系統資源或者執行其他清理工作。

20.使用final關鍵字修飾一個變數時,是引用不能變,還是引用的物件不能變?

使用final關鍵字修飾一個變數時,是指引用變數不能變,引用變數所指向的物件中的內容還是可以改變的。例如,對於如下語句:

 finalStringBuffer a=new StringBuffer("immutable");

執行如下語句將報告編譯期錯誤:

a=new StringBuffer("");

但是,執行如下語句則可以通過編譯:

a.append(" broken!");

有人在定義方法的引數時,可能想採用如下形式來阻止方法內部修改傳進來的引數物件:

public void method(final  StringBuffer param){
}

實際上,這是辦不到的,在該方法內部仍然可以增加如下程式碼來修改引數物件:

param.append("a");

21.執行緒的sleep()方法和yield()方法有什麼區別?

  • 1、sleep()方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會;
  • 2、執行緒執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
  • 3、 sleep()方法宣告丟擲InterruptedException,而yield()方法沒有宣告任何異常;
  • 4、sleep()方法比yield()方法(跟作業系統CPU排程相關)具有更好的可移植性。

22.當一個執行緒進入一個物件的synchronized方法A之後,其它執行緒是否可進入此物件的synchronized方法B?

  • 不能。其它執行緒只能訪問該物件的非同步方法,同步方法則不能進入。因為非靜態方法上的synchronized修飾符要求執行方法時要獲得物件的鎖,如果已經進入A方法說明物件鎖已經被取走,那麼試圖進入B方法的執行緒就只能在等鎖池(注意不是等待池哦)中等待物件的鎖。

23.請說出與執行緒同步以及執行緒排程相關的方法

  • wait():使一個執行緒處於等待(阻塞)狀態,並且釋放所持有的物件的鎖;
  • sleep():使一個正在執行的執行緒處於睡眠狀態,是一個靜態方法,呼叫此方法要處理InterruptedException異常;
  • notify():喚醒一個處於等待狀態的執行緒,當然在呼叫此方法的時候,並不能確切的喚醒某一個等待狀態的執行緒,而是由JVM確定喚醒哪個執行緒,而且與優先順序無關;
  • notityAll():喚醒所有處於等待狀態的執行緒,該方法並不是將物件的鎖給所有執行緒,而是讓它們競爭,只有獲得鎖的執行緒才能進入就緒狀態;

24.編寫多執行緒程式有幾種實現方式?

  • Java 5以前實現多執行緒有兩種實現方法:一種是繼承Thread類;另一種是實現Runnable介面。兩種方式都要通過重寫run()方法來定義執行緒的行為,推薦使用後者,因為Java中的繼承是單繼承,一個類有一個父類,如果繼承了Thread類就無法再繼承其他類了,顯然使用Runnable介面更為靈活。
  • 補充:Java 5以後建立執行緒還有第三種方式:實現Callable介面,該介面中的call方法可以線上程執行結束時產生一個返回值。

25.synchronized關鍵字的用法?

  • synchronized關鍵字可以將物件或者方法標記為同步,以實現對物件和方法的互斥訪問,可以用synchronized(物件) { … }定義同步程式碼塊,或者在宣告方法時將synchronized作為方法的修飾符。在第60題的例子中已經展示了synchronized關鍵字的用法。

26.舉例說明同步和非同步

  • 如果系統中存在臨界資源(資源數量少於競爭資源的執行緒數量的資源),例如正在寫的資料以後可能被另一個執行緒讀到,或者正在讀的資料可能已經被另一個執行緒寫過了,那麼這些資料就必須進行同步存取(資料庫操作中的排他鎖就是最好的例子)。當應用程式在物件上呼叫了一個需要花費很長時間來執行的方法,並且不希望讓程式等待方法的返回時,就應該使用非同步程式設計,在很多情況下采用非同步途徑往往更有效率。事實上,所謂的同步就是指阻塞式操作,而非同步就是非阻塞式操作。

27.啟動一個執行緒是呼叫run()還是start()方法?

  • 啟動一個執行緒是呼叫start()方法,使執行緒所代表的虛擬處理機處於可執行狀態,這意味著它可以由JVM 排程並執行,這並不意味著執行緒就會立即執行。run()方法是執行緒啟動後要進行回撥(callback)的方法。

28.什麼是執行緒池(thread pool)?

  • 在物件導向程式設計中,建立和銷燬物件是很費時間的,因為建立一個物件要獲取記憶體資源或者其它更多資源。在Java中更是如此,虛擬機器將試圖跟蹤每一個物件,以便能夠在物件銷燬後進行垃圾回收。所以提高服務程式效率的一個手段就是儘可能減少建立和銷燬物件的次數,特別是一些很耗資源的物件建立和銷燬,這就是"池化資源"技術產生的原因。執行緒池顧名思義就是事先建立若干個可執行的執行緒放入一個池(容器)中,需要的時候從池中獲取執行緒不用自行建立,使用完畢不需要銷燬執行緒而是放回池中,從而減少建立和銷燬執行緒物件的開銷。
    Java 5+中的Executor介面定義一個執行執行緒的工具。它的子型別即執行緒池介面是ExecutorService。要配置一個執行緒池是比較複雜的,尤其是對於執行緒池的原理不是很清楚的情況下,因此在工具類Executors面提供了一些靜態工廠方法,生成一些常用的執行緒池,如下所示:

1. newSingleThreadExecutor:建立一個單執行緒的執行緒池。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。

2. newFixedThreadPool:建立固定大小的執行緒池。每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。

3. newCachedThreadPool:建立一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閒(60秒不執行任務)的執行緒,當任務數增加時,此執行緒池又可以智慧的新增新執行緒來處理任務。此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)能夠建立的最大執行緒大小。

4. newScheduledThreadPool:建立一個大小無限的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。

5. newSingleThreadExecutor:建立一個單執行緒的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。

29.執行緒的基本狀態以及狀態之間的關係?

在這裡插入圖片描述

注:其中Running表示執行狀態,Runnable表示就緒狀態(萬事俱備,只欠CPU),Blocked表示阻塞狀態,阻塞狀態又有多種情況,可能是因為呼叫wait()方法進入等待池,也可能是執行同步方法或同步程式碼塊進入等鎖池,或者是呼叫了sleep()方法或join()方法等待休眠或其他執行緒結束,或是因為發生了I/O中斷。

30.簡述synchronized 和java.util.concurrent.locks.Lock的異同?

  • Lock是Java 5以後引入的新的API,和關鍵字synchronized相比主要相同點:Lock 能完成synchronized所實現的所有功能;主要不同點:Lock有比synchronized更精確的執行緒語義和更好的效能,而且不強制性的要求一定要獲得鎖。synchronized會自動釋放鎖,而Lock一定要求程式設計師手工釋放,並且最好在finally 塊中釋放(這是釋放外部資源的最好的地方)。

31.Java中如何實現序列化,有什麼意義?

  • 序列化就是一種用來處理物件流的機制,所謂物件流也就是將物件的內容進行流化。可以對流化後的物件進行讀寫操作,也可將流化後的物件傳輸於網路之間。序列化是為了解決物件流讀寫操作時可能引發的問題(如果不進行序列化可能會存在資料亂序的問題)。
  • 要實現序列化,需要讓一個類實現Serializable介面,該介面是一個標識性介面,標註該類物件是可被序列化的,然後使用一個輸出流來構造一個物件輸出流並通過writeObject(Object)方法就可以將實現物件寫出(即儲存其狀態);如果需要反序列化則可以用一個輸入流建立物件輸入流,然後通過readObject方法從流中讀取物件。序列化除了能夠實現物件的持久化之外,還能夠用於物件的深度克隆。

32.XML文件定義有幾種形式?它們之間有何本質區別?解析XML文件有哪幾種方式?

  • XML文件定義分為DTD和Schema兩種形式,二者都是對XML語法的約束,其本質區別在於Schema本身也是一個XML檔案,可以被XML解析器解析,而且可以為XML承載的資料定義型別,約束能力較之DTD更強大。對XML的解析主要有DOM(文件物件模型,Document Object Model)、SAX(Simple API for XML)和StAX(Java 6中引入的新的解析XML的方式,Streaming API for XML),其中DOM處理大型檔案時其效能下降的非常厲害,這個問題是由DOM樹結構佔用的記憶體較多造成的,而且DOM解析方式必須在解析檔案之前把整個文件裝入記憶體,適合對XML的隨機訪問(典型的用空間換取時間的策略);SAX是事件驅動型的XML解析方式,它順序讀取XML檔案,不需要一次全部裝載整個檔案。當遇到像檔案開頭,文件結束,或者標籤開頭與標籤結束時,它會觸發一個事件,使用者通過事件回撥程式碼來處理XML檔案,適合對XML的順序訪問;顧名思義,StAX把重點放在流上,實際上StAX與其他解析方式的本質區別就在於應用程式能夠把XML作為一個事件流來處理。將XML作為一組事件來處理的想法並不新穎(SAX就是這樣做的),但不同之處在於StAX允許應用程式程式碼把這些事件逐個拉出來,而不用提供在解析器方便時從解析器中接收事件的處理程式。

33.闡述JDBC運算元據庫的步驟

下面的程式碼以連線本機的Oracle資料庫為例,演示JDBC運算元據庫的步驟。

//載入驅動
	Class.forName("oracle.jdbc.driver.OracleDriver");
//建立連線
	Connection con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", "bugu", "bugu");
//建立語句
	PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?");
	ps.setInt(1, 1000);
	ps.setInt(2, 3000);
//執行語句
	ResultSet rs = ps.executeQuery();
//處理結果
	while(rs.next()) {
		System.out.println(rs.getInt("empno") + " - " + rs.getString("ename"));
	}
//關閉資源
	finally {
		if(con != null) {
			try {
				con.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
	}
注:關閉外部資源的順序應該和開啟的順序相反,也就是說先關閉ResultSet、再關閉Statement、在關閉Connection。上面的程式碼只關閉了Connection(連線),雖然通常情況下在關閉連線時,連線上建立的語句和開啟的遊標也會關閉,但不能保證總是如此,因此應該按照剛才說的順序分別關閉。此外,第一步載入驅動在JDBC 4.0中是可以省略的(自動從類路徑中載入驅動),但是我們建議保留。

34.Statement和PreparedStatement有什麼區別?哪個效能更好?

  • 與Statement相比,①PreparedStatement介面代表預編譯的語句,它主要的優勢在於可以減少SQL的編譯錯誤並增加SQL的安全性(減少SQL注射攻擊的可能性);②PreparedStatement中的SQL語句是可以帶引數的,避免了用字串連線拼接SQL語句的麻煩和不安全;③當批量處理SQL或頻繁執行相同的查詢時,PreparedStatement有明顯的效能上的優勢,由於資料庫可以將編譯優化後的SQL語句快取起來,下次執行相同結構的語句時就會很快(不用再次編譯和生成執行計劃)。
為了提供對儲存過程的呼叫,JDBC API中還提供了CallableStatement介面。儲存過程(Stored Procedure)是資料庫中一組為了完成特定功能的SQL語句的集合,經編譯後儲存在資料庫中,使用者通過指定儲存過程的名字並給出引數(如果該儲存過程帶有引數)來執行它。雖然呼叫儲存過程會在網路開銷、安全性、效能上獲得很多好處,但是存在如果底層資料庫發生遷移時就會有很多麻煩,因為每種資料庫的儲存過程在書寫上存在不少的差別。

35.使用JDBC運算元據庫時,如何提升讀取資料的效能?如何提升更新資料的效能?

  • 要提升讀取資料的效能,可以指定通過結果集(ResultSet)物件的setFetchSize()方法指定每次抓取的記錄數(典型的空間換時間策略);要提升更新資料的效能可以使用PreparedStatement語句構建批處理,將若干SQL語句置於一個批處理中執行。

36.在進行資料庫程式設計時,連線池有什麼作用?

  • 由於建立連線和釋放連線都有很大的開銷(尤其是資料庫伺服器不在本地時,每次建立連線都需要進行TCP的三次握手,釋放連線需要進行TCP四次握手,造成的開銷是不可忽視的),為了提升系統訪問資料庫的效能,可以事先建立若干連線置於連線池中,需要時直接從連線池獲取,使用結束時歸還連線池而不必關閉連線,從而避免頻繁建立和釋放連線所造成的開銷,這是典型的用空間換取時間的策略(浪費了空間儲存連線,但節省了建立和釋放連線的時間)。池化技術在Java開發中是很常見的,在使用執行緒時建立執行緒池的道理與此相同。基於Java的開源資料庫連線池主要有:C3P0、Proxool、DBCP、BoneCP、Druid等。

37.JDBC能否處理Blob和Clob?

Blob是指二進位制大物件(Binary Large Object),而Clob是指大字元物件(Character Large Objec),因此其中Blob是為儲存大的二進位制資料而設計的,而Clob是為儲存大的文字資料而設計的。JDBC的PreparedStatement和ResultSet都提供了相應的方法來支援Blob和Clob操作。

38.獲得一個類的類物件有哪些方式?

  • 方法1:型別.class,例如:String.class
  • 方法2:物件.getClass(),例如:"hello".getClass()
  • 方法3:Class.forName(),例如:Class.forName("java.lang.String")

39.如何通過反射建立物件?

  • 方法1:通過類物件呼叫newInstance()方法,例如:String.class.newInstance()
  • 方法2:通過類物件的getConstructor()或getDeclaredConstructor()方法獲得構造器(Constructor)物件並呼叫其newInstance()方法建立物件,例如:String.class.getConstructor(String.class).newInstance("Hello");

40.如何通過反射獲取和設定物件私有欄位的值?

  • 可以通過類物件的getDeclaredField()方法欄位(Field)物件,然後再通過欄位物件的setAccessible(true)將其設定為可以訪問,接下來就可以通過get/set方法來獲取/設定欄位的值了。下面的程式碼實現了一個反射的工具類,其中的兩個靜態方法分別用於獲取和設定私有欄位的值,欄位可以是基本型別也可以是物件型別且支援多級物件操作,例如ReflectionUtil.get(dog, "owner.car.engine.id");可以獲得dog物件的主人的汽車的引擎的ID號。
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;

/**
 * 反射工具類
 * @author 駱昊
 *
 */
public class ReflectionUtil {

	private ReflectionUtil() {
		throw new AssertionError();
	}

	/**
	 * 通過反射取物件指定欄位(屬性)的值
	 * @param target 目標物件
	 * @param fieldName 欄位的名字
	 * @throws 如果取不到物件指定欄位的值則丟擲異常
	 * @return 欄位的值
	 */
	public static Object getValue(Object target, String fieldName) {
		Class<?> clazz = target.getClass();
		String[] fs = fieldName.split("\\.");
		
		try {
			for(int i = 0; i < fs.length - 1; i++) {
				Field f = clazz.getDeclaredField(fs[i]);
				f.setAccessible(true);
				target = f.get(target);
				clazz = target.getClass();
			}
		
			Field f = clazz.getDeclaredField(fs[fs.length - 1]);
			f.setAccessible(true);
			return f.get(target);
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * 通過反射給物件的指定欄位賦值
	 * @param target 目標物件
	 * @param fieldName 欄位的名稱
	 * @param value 值
	 */
	public static void setValue(Object target, String fieldName, Object value) {
		Class<?> clazz = target.getClass();
		String[] fs = fieldName.split("\\.");
		try {
			for(int i = 0; i < fs.length - 1; i++) {
				Field f = clazz.getDeclaredField(fs[i]);
				f.setAccessible(true);
				Object val = f.get(target);
				if(val == null) {
					Constructor<?> c = f.getType().getDeclaredConstructor();
					c.setAccessible(true);
					val = c.newInstance();
					f.set(target, val);
				}
				target = val;
				clazz = target.getClass();
			}
		
			Field f = clazz.getDeclaredField(fs[fs.length - 1]);
			f.setAccessible(true);
			f.set(target, value);
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
}

41.如何通過反射呼叫物件的方法?

請看下面的程式碼:

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
	}
}

42.簡述一下物件導向的"六原則一法則"

  • 單一職責原則:一個類只做它該做的事情。(單一職責原則想表達的就是"高內聚",寫程式碼最終極的原則只有六個字"高內聚、低耦合",就如同葵花寶典或辟邪劍譜的中心思想就八個字"欲練此功必先自宮",所謂的高內聚就是一個程式碼模組只完成一項功能,在物件導向中,如果只讓一個類完成它該做的事,而不涉及與它無關的領域就是踐行了高內聚的原則,這個類就只有單一職責。我們都知道一句話叫"因為專注,所以專業",一個物件如果承擔太多的職責,那麼註定它什麼都做不好。這個世界上任何好的東西都有兩個特徵,一個是功能單一,好的相機絕對不是電視購物裡面賣的那種一個機器有一百多種功能的,它基本上只能照相;另一個是模組化,好的自行車是組裝車,從減震叉、剎車到變速器,所有的部件都是可以拆卸和重新組裝的,好的乒乓球拍也不是成品拍,一定是底板和膠皮可以拆分和自行組裝的,一個好的軟體系統,它裡面的每個功能模組也應該是可以輕易的拿到其他系統中使用的,這樣才能實現軟體複用的目標。)
  • 開閉原則:軟體實體應當對擴充套件開放,對修改關閉。(在理想的狀態下,當我們需要為一個軟體系統增加新功能時,只需要從原來的系統派生出一些新類就可以,不需要修改原來的任何一行程式碼。要做到開閉有兩個要點:①抽象是關鍵,一個系統中如果沒有抽象類或介面系統就沒有擴充套件點;②封裝可變性,將系統中的各種可變因素封裝到一個繼承結構中,如果多個可變因素混雜在一起,系統將變得複雜而換亂,如果不清楚如何封裝可變性,可以參考《設計模式精解》一書中對橋樑模式的講解的章節。)
  • 依賴倒轉原則:面向介面程式設計。(該原則說得直白和具體一些就是宣告方法的引數型別、方法的返回型別、變數的引用型別時,儘可能使用抽象型別而不用具體型別,因為抽象型別可以被它的任何一個子型別所替代,請參考下面的里氏替換原則。)
  • 里氏替換原則:任何時候都可以用子型別替換掉父型別。(關於里氏替換原則的描述,Barbara Liskov女士的描述比這個要複雜得多,但簡單的說就是能用父型別的地方就一定能使用子型別。里氏替換原則可以檢查繼承關係是否合理,如果一個繼承關係違背了里氏替換原則,那麼這個繼承關係一定是錯誤的,需要對程式碼進行重構。例如讓貓繼承狗,或者狗繼承貓,又或者讓正方形繼承長方形都是錯誤的繼承關係,因為你很容易找到違反里氏替換原則的場景。需要注意的是:子類一定是增加父類的能力而不是減少父類的能力,因為子類比父類的能力更多,把能力多的物件當成能力少的物件來用當然沒有任何問題。)
  • 介面隔離原則:介面要小而專,絕不能大而全。(臃腫的介面是對介面的汙染,既然介面表示能力,那麼一個介面只應該描述一種能力,介面也應該是高度內聚的。例如,琴棋書畫就應該分別設計為四個介面,而不應設計成一個介面中的四個方法,因為如果設計成一個介面中的四個方法,那麼這個介面很難用,畢竟琴棋書畫四樣都精通的人還是少數,而如果設計成四個介面,會幾項就實現幾個介面,這樣的話每個介面被複用的可能性是很高的。Java中的介面代表能力、代表約定、代表角色,能否正確的使用介面一定是程式設計水平高低的重要標識。)
  • 合成聚合複用原則:優先使用聚合或合成關係複用程式碼。(通過繼承來複用程式碼是物件導向程式設計中被濫用得最多的東西,因為所有的教科書都無一例外的對繼承進行了鼓吹從而誤導了初學者,類與類之間簡單的說有三種關係,Is-A關係、Has-A關係、Use-A關係,分別代表繼承、關聯和依賴。其中,關聯關係根據其關聯的強度又可以進一步劃分為關聯、聚合和合成,但說白了都是Has-A關係,合成聚合複用原則想表達的是優先考慮Has-A關係而不是Is-A關係複用程式碼,原因嘛可以自己從百度上找到一萬個理由,需要說明的是,即使在Java的API中也有不少濫用繼承的例子,例如Properties類繼承了Hashtable類,Stack類繼承了Vector類,這些繼承明顯就是錯誤的,更好的做法是在Properties類中放置一個Hashtable型別的成員並且將其鍵和值都設定為字串來儲存資料,而Stack類的設計也應該是在Stack類中放一個Vector物件來儲存資料。記住:任何時候都不要繼承工具類,工具是可以擁有並可以使用的,而不是拿來繼承的。)
  • 迪米特法則:迪米特法則又叫最少知識原則,一個物件應當對其他物件有儘可能少的瞭解。(迪米特法則簡單的說就是如何做到"低耦合",門面模式和調停者模式就是對迪米特法則的踐行。對於門面模式可以舉一個簡單的例子,你去一家公司洽談業務,你不需要了解這個公司內部是如何運作的,你甚至可以對這個公司一無所知,去的時候只需要找到公司入口處的前臺美女,告訴她們你要做什麼,她們會找到合適的人跟你接洽,前臺的美女就是公司這個系統的門面。再複雜的系統都可以為使用者提供一個簡單的門面,Java Web開發中作為前端控制器的Servlet或Filter不就是一個門面嗎,瀏覽器對伺服器的運作方式一無所知,但是通過前端控制器就能夠根據你的請求得到相應的服務。調停者模式也可以舉一個簡單的例子來說明,例如一臺計算機,CPU、記憶體、硬碟、顯示卡、音效卡各種裝置需要相互配合才能很好的工作,但是如果這些東西都直接連線到一起,計算機的佈線將異常複雜,在這種情況下,主機板作為一個調停者的身份出現,它將各個裝置連線在一起而不需要每個裝置之間直接交換資料,這樣就減小了系統的耦合度和複雜度,如下圖所示。迪米特法則用通俗的話來將就是不要和陌生人打交道,如果真的需要,找一個自己的朋友,讓他替你和陌生人打交道。)
    在這裡插入圖片描述
    在這裡插入圖片描述

43.簡述一下你瞭解的設計模式。

  • 所謂設計模式,就是一套被反覆使用的程式碼設計經驗的總結(情境中一個問題經過證實的一個解決方案)。使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。設計模式使人們可以更加簡單方便的複用成功的設計和體系結構。將已證實的技術表述成設計模式也會使新系統開發者更加容易理解其設計思路。
    在GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》中給出了三類(建立型[對類的例項化過程的抽象化]、結構型[描述如何將類或物件結合在一起形成更大的結構]、行為型[對在不同的物件之間劃分責任和演算法的抽象化])共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(責任鏈模式)。
    面試被問到關於設計模式的知識時,可以揀最常用的作答,例如:
  • 工廠模式:工廠類可以根據條件生成不同的子類例項,這些子類有一個公共的抽象父類並且實現了相同的方法,但是這些方法針對不同的資料進行了不同的操作(多型方法)。當得到子類的例項後,開發人員可以呼叫基類中的方法而不必考慮到底返回的是哪一個子類的例項。
  • 代理模式:給一個物件提供一個代理物件,並由代理物件控制原物件的引用。實際開發中,按照使用目的的不同,代理可以分為:遠端代理、虛擬代理、保護代理、Cache代理、防火牆代理、同步化代理、智慧引用代理。
    介面卡模式:把一個類的介面變換成客戶端所期待的另一種介面,從而使原本因介面不匹配而無法在一起使用的類能夠一起工作。
  • 模板方法模式:提供一個抽象類,將部分邏輯以具體方法或構造器的形式實現,然後宣告一些抽象方法來迫使子類實現剩餘的邏輯。不同的子類可以以不同的方式實現這些抽象方法(多型實現),從而實現不同的業務邏輯。
    除此之外,還可以講講上面提到的門面模式、橋樑模式、- 單例模式、裝潢模式(Collections工具類和I/O系統中都使用裝潢模式)等,反正基本原則就是揀自己最熟悉的、用得最多的作答,以免言多必失。

44.用Java寫一個單例類

// 餓漢式單例
public class Singleton {
    private Singleton(){}
    private static Singleton instance = new Singleton();
    public static Singleton getInstance(){
        return instance;
    }
}
//懶漢式單例
public class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static synchronized Singleton getInstance(){
        if (instance == null) instance = new Singleton();
        return instance;
    }
}
//實現一個單例有兩點注意事項,①將構造器私有,不允許外界通過構造器建立物件;②通過公開的靜態方法向外界返回類的唯一例項

相關文章