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


@

目錄

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;
    }
}
//實現一個單例有兩點注意事項,①將構造器私有,不允許外界通過構造器創建對象;②通過公開的靜態方法向外界返回類的唯一實例


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM