1. 程式人生 > >Java面試整理 錯題集(上)

Java面試整理 錯題集(上)

2、訪問修飾符public,private,protected,以及不寫(預設)時的區別?
答:

修飾符 當前類 同 包 子 類 其他包
public
protected ×
default × ×
private × × ×

類的成員不寫訪問修飾時預設為default。預設對於同一個包中的其他類相當於公開(public),對於不是同一個包中的其他類相當於私有(private)。受保護(protected)對子類相當於公開,對不是同一包中的沒有父子關係的類相當於私有。Java中,外部類的修飾符只能是public或預設,類的成員(包括內部類)的修飾符可以是以上四種。

5、short s1 = 1; s1 = s1 + 1;有錯嗎?short s1 = 1; s1 += 1;有錯嗎?
答:對於short s1 = 1; s1 = s1 + 1;由於1是int型別,因此s1+1運算結果也是int 型,需要強制轉換型別才能賦值給short型。而short s1 = 1; s1 += 1;可以正確編譯,因為s1+= 1;相當於s1 = (short)(s1 + 1);其中有隱含的強制型別轉換。

下面給出java中四則運算的型別轉換規則:

1、如果兩個運算元中有一個是double型別,另一個就會轉換為double型別;

2、否則,如果有一個運算元是float,另一個就會轉化為float;

3、否則,如果有一個運算元是long,另一個就會轉換為long;

4、否則,兩個運算元都將轉換為int型別。

最近還遇到一個面試題,也是和自動裝箱和拆箱有點關係的,程式碼如下所示:

public class Test03 {

    public static void main(String[] args) {
        Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;

        System.out.println(f1 == f2);
        System.out.println(f3 == f4);
    }
}
如果不明就裡很容易認為兩個輸出要麼都是true要麼都是false。

簡單的說,如果整型字面量的值在-128到127之間,那麼不會new新的Integer物件,而是直接引用常量池中的Integer物件,所以上面的面試題中f1==f2的結果是true,而f3==f4的結果是false。

java中基本型別的包裝類的大部分都實現了常量池技術,這些類是Byte,Short,Integer,Long,Character,Boolean,另外兩種浮點數型別的包裝類則沒有實現。另外Byte,Short,Integer,Long,Character 這5種包裝類預設建立了數值[-128,127]的相應型別的快取資料,但是超出此範圍仍然會去建立新的物件。

8、&和&&的區別?
答:&運算子有兩種用法:(1)按位與;(2)邏輯與。&&運算子是短路與運算。邏輯與跟短路與的差別是非常巨大的,雖然二者都要求運算子左右兩端的布林值都是true整個表示式的值才是true。&&之所以稱為短路運算是因為,如果&&左邊的表示式的值是false,右邊的表示式會被直接短路掉,不會進行運算。很多時候我們可能都需要用&&而不是&,例如在驗證使用者登入時判定使用者名稱不是null而且不是空字串,應當寫為:username != null &&!username.equals(""),二者的順序不能交換,更不能用&運算子,因為第一個條件如果不成立,根本不能進行字串的equals比較,否則會產生NullPointerException異常。注意:邏輯或運算子(|)和短路或運算子(||)的差別也是如此。

           補充:如果你熟悉JavaScript,那你可能更能感受到短路運算的強大,想成為JavaScript的高手就先從玩轉短路運算開始吧。

9、解釋記憶體中的棧(stack)、堆(heap)和方法區(method area)的用法。
答:通常我們定義一個基本資料型別的變數,一個物件的引用,還有就是函式呼叫的現場儲存都使用JVM中的棧空間;而通過new關鍵字和構造器建立的物件則放在堆空間,堆是垃圾收集器管理的主要區域,由於現在的垃圾收集器都採用分代收集演算法,所以堆空間還可以細分為新生代和老生代,再具體一點可以分為Eden、Survivor(又可分為From Survivor和To Survivor)、Tenured;方法區和堆都是各個執行緒共享的記憶體區域,用於儲存已經被JVM載入的類資訊、常量、靜態變數、JIT編譯器編譯後的程式碼等資料;程式中的字面量(literal)如直接書寫的100、"hello"和常量都是放在常量池中,常量池是方法區的一部分,。棧空間操作起來最快但是棧很小,通常大量的物件都是放在堆空間,棧和堆的大小都可以通過JVM的啟動引數來進行調整,棧空間用光了會引發StackOverflowError,而堆和常量池空間不足則會引發OutOfMemoryError。

String str = new String("hello");

上面的語句中變數str放在棧上,用new創建出來的字串物件放在堆上,而"hello"這個字面量是放在方法區的。

補充1:較新版本的Java(從Java 6的某個更新開始)中,由於JIT編譯器的發展和"逃逸分析"技術的逐漸成熟,棧上分配、標量替換等優化技術使得物件一定分配在堆上這件事情已經變得不那麼絕對了。

補充2:執行時常量池相當於Class檔案常量池具有動態性,Java語言並不要求常量一定只有編譯期間才能產生,執行期間也可以將新的常量放入池中,String類的intern()方法就是這樣的。

看看下面程式碼的執行結果是什麼並且比較一下Java 7以前和以後的執行結果是否一致。

String s1 = new StringBuilder("go")
    .append("od").toString();
System.out.println(s1.intern() == s1);
String s2 = new StringBuilder("ja")
    .append("va").toString();
System.out.println(s2.intern() == s2);

10、Math.round(11.5) 等於多少?Math.round(-11.5)等於多少?
答:Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四捨五入的原理是在引數上加0.5然後進行下取整。

11、switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上?
答:在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。從Java 5開始,Java中引入了列舉型別,expr也可以是enum型別,從Java 7開始,expr還可以是字串(String),但是長整型(long)在目前所有的版本中都是不可以的。

switch-case語句完全可以與if-else語句互轉,但通常來說,switch-case語句執行效率要高。

default在當前switch找不到匹配的case時執行。default並不是必須的。

一旦case匹配,就會順序執行後面的程式程式碼,而不管後面的case是否匹配,直到遇見break。

12、用最有效率的方法計算2乘以8?
答: 2 << 3(左移3位相當於乘以2的3次方,右移3位相當於除以2的3次方)。

補充:我們為編寫的類重寫hashCode方法時,可能會看到如下所示的程式碼,其實我們不太理解為什麼要使用這樣的乘法運算來產生雜湊碼(雜湊碼),而且為什麼這個數是個素數,為什麼通常選擇31這個數?前兩個問題的答案你可以自己百度一下,選擇31是因為可以用移位和減法運算來代替乘法,從而得到更好的效能。說到這裡你可能已經想到了:31 * num 等價於(num << 5) - num,左移5位相當於乘以2的5次方再減去自身就相當於乘以31,現在的VM都能自動完成這個優化。

13、陣列有沒有length()方法?String有沒有length()方法?
答:陣列沒有length()方法,有length 的屬性。String 有length()方法。JavaScript中,獲得字串的長度是通過length屬性得到的,這一點容易和Java混淆。

14、在Java中,如何跳出當前的多重巢狀迴圈?
答:在最外層迴圈前加一個標記如A,然後用break A;可以跳出多重迴圈。(Java中支援帶標籤的break和continue語句,作用有點類似於C和C++中的goto語句,但是就像要避免使用goto一樣,應該避免使用帶標籤的break和continue,因為它不會讓你的程式變得更優雅,很多時候甚至有相反的作用,所以這種語法其實不知道更好)

public class BreaklFor {  
    public static void main(String args[]){  
        OK:                    //設定一個標記 使用帶此標記的break語句跳出多重迴圈體  
        for(int i=1;i<100;i++){   //讓i迴圈99次  
            for(int j=1;j<=i;j++){  
                if(i==10){  
                    break OK ;  
                }  
                System.out.print(i + "*" + j + "=" + i*j) ;  
                System.out.print(" ") ;  
            }  
            System.out.println() ;  
        }  
    }  
}  

15、構造器(constructor)是否可被重寫(override)?
答:構造器不能被繼承,因此不能被重寫,但可以被過載。

過載:方法名相同,引數不同(引數型別或者長度)
        過載和修飾符和返回型別無關。
        一是方法的引數列表必須改變,包括引數的型別,引數的個數多少,引數順序。
        二是過載對返回型別,訪問修飾符,異常宣告沒有任何限制,可以作任意的修改。實質上,過載只是建立了一個方法而已,特殊的地方在於方法的名字。

重寫:兩同兩小一大(規則)
      兩同:方法名相同引數型別相同
      兩小:返回值型別(基本資料型別要一致,引用型別可以是其子類)
                 丟擲的異常要小(也可以丟擲父型別的異常的部分異常,或者不丟擲異常)
      一大:訪問控制修飾符大
(1)重寫方法必須和被重寫方法具有相同的引數列表,返回型別必須和被重寫方法的返回型別相同或者是返回型別的子型別。
(2)重寫方法的訪問控制修飾符不能比被重寫方法更嚴格(比如一個在父類中宣告為public的方法重寫成一個protected的方法)。
(3)只有例項方法才能被重寫,超類中的final方法不能被重寫。
(4)重寫方法不能丟擲新的檢查異常,或者是丟擲比被重寫方法宣告的檢查異常更廣泛的檢查異常。

16、兩個物件值相同(x.equals(y) == true),但卻可有不同的hash code,這句話對不對?
答:不對,如果兩個物件x和y滿足x.equals(y) == true,它們的雜湊碼(hash code)應當相同。Java對於eqauls方法和hashCode方法是這樣規定的:(1)如果兩個物件相同(equals方法返回true),那麼它們的hashCode值一定要相同;(2)如果兩個物件的hashCode相同,它們並不一定相同。當然,你未必要按照要求去做,但是如果你違背了上述原則就會發現在使用容器時,相同的物件可以出現在Set集合中,同時增加新元素的效率會大大下降(對於使用雜湊儲存的系統,如果雜湊碼頻繁的衝突將會造成存取效能急劇下降)。

補充:關於equals和hashCode方法,很多Java程式都知道,但很多人也就是僅僅知道而已,在Joshua Bloch的大作《Effective Java》(很多軟體公司,《Effective Java》、《Java程式設計思想》以及《重構:改善既有程式碼質量》是Java程式設計師必看書籍,如果你還沒看過,那就趕緊去亞馬遜買一本吧)中是這樣介紹equals方法的:首先equals方法必須滿足自反性(x.equals(x)必須返回true)、對稱性(x.equals(y)返回true時,y.equals(x)也必須返回true)、傳遞性(x.equals(y)和y.equals(z)都返回true時,x.equals(z)也必須返回true)和一致性(當x和y引用的物件資訊沒有被修改時,多次呼叫x.equals(y)應該得到同樣的返回值),而且對於任何非null值的引用x,x.equals(null)必須返回false。實現高質量的equals方法的訣竅包括:1. 使用==操作符檢查"引數是否為這個物件的引用";2. 使用instanceof操作符檢查"引數是否為正確的型別";3. 對於類中的關鍵屬性,檢查引數傳入物件的屬性是否與之相匹配;4. 編寫完equals方法後,問自己它是否滿足對稱性、傳遞性、一致性;5. 重寫equals時總是要重寫hashCode;6. 不要將equals方法引數中的Object物件替換為其他的型別,在重寫時不要忘掉@Override註解。

17、是否可以繼承String類?
答:String 類是final類,不可以被繼承。

補充:繼承String本身就是一個錯誤的行為,對String型別最好的重用方式是關聯關係(Has-A)和依賴關係(Use-A)而不是繼承關係(Is-A)。

18、當一個物件被當作引數傳遞到一個方法後,此方法可改變這個物件的屬性,並可返回變化後的結果,那麼這裡到底是值傳遞還是引用傳遞?
答:是值傳遞。Java語言的方法呼叫只支援引數的值傳遞。當一個物件例項作為一個引數被傳遞到方法中時,引數的值就是對該物件的引用。物件的屬性可以在被呼叫過程中被改變,但對物件引用的改變是不會影響到呼叫者的。C++和C#中可以通過傳引用或傳輸出引數來改變傳入的引數的值。在C#中可以編寫如下所示的程式碼,但是在Java中卻做不到。

            說明:Java中沒有傳引用實在是非常的不方便,這一點在Java 8中仍然沒有得到改進,正是如此在Java編寫的程式碼中才會出現大量的Wrapper類(將需要通過方法       呼叫修改的引用置於一個Wrapper類中,再將Wrapper物件傳入方法),這樣的做法只會讓程式碼變得臃腫,尤其是讓從C和C++轉型為Java程式設計師的開發者無法容忍。

19、String和StringBuilder、StringBuffer的區別?

答:Java平臺提供了兩種型別的字串:String和StringBuffer/StringBuilder,它們可以儲存和操作字串。其中String是隻讀字串,也就意味著String引用的字串內容是不能被改變的。而StringBuffer/StringBuilder類表示的字串物件可以直接進行修改。StringBuilder是Java 5中引入的,它和StringBuffer的方法完全相同,區別在於它是在單執行緒環境下使用的,因為它的所有方面都沒有被synchronized修飾,因此它的效率也比StringBuffer要高。

面試題1 - 什麼情況下用+運算子進行字串連線比呼叫StringBuffer/StringBuilder物件的append方法連線字串效能更好?

面試題2 - 請說出下面程式的輸出。

class StringEqualTest {

    public static void main(String[] args) {
        String s1 = "Programming";
        String s2 = new String("Programming");
        String s3 = "Program";
        String s4 = "ming";
        String s5 = "Program" + "ming";
        String s6 = s3 + s4;
        System.out.println(s1 == s2);
        System.out.println(s1 == s5);
        System.out.println(s1 == s6);
        System.out.println(s1 == s6.intern());
        System.out.println(s2 == s2.intern());
    }
}

補充:解答上面的面試題需要清除兩點:1. String物件的intern方法會得到字串物件在常量池中對應的版本的引用(如果常量池中有一個字串與String物件的equals結果是true),如果常量池中沒有對應的字串,則該字串將被新增到常量池中,然後返回常量池中字串的引用;2. 字串的+操作其本質是建立了StringBuilder物件進行append操作,然後將拼接後的StringBuilder物件用toString方法處理成String物件,這一點可以用javap -c StringEqualTest.class命令獲得class檔案對應的JVM位元組碼指令就可以看出來。

當呼叫 intern 方法時,如果池已經包含一個等於此 String 物件的字串(該物件由 equals(Object) 方法確定),則返回池中的字串。否則,將此 String 物件新增到池中,並且返回此 String 物件的引用。

它遵循對於任何兩個字串 s 和 t,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true。

所有字面值字串和字串賦值常量表達式都是內部的。

返回:一個字串,內容與此字串相同,但它保證來自字串池中。

22、char 型變數中能不能存貯一箇中文漢字,為什麼?

答:char型別可以儲存一箇中文漢字,因為Java中使用的編碼是Unicode(不選擇任何特定的編碼,直接使用字元在字符集中的編號,這是統一的唯一方法),一個char型別佔2個位元組(16位元),所以放一箇中文是沒問題的。

補充:使用Unicode意味著字元在JVM內部和外部有不同的表現形式,在JVM內部都是Unicode,當這個字元被從JVM內部轉移到外部時(例如存入檔案系統中),需要進行編碼轉換。所以Java中有位元組流和字元流,以及在字元流和位元組流之間進行轉換的轉換流,如InputStreamReader和OutputStreamReader,這兩個類是位元組流和字元流之間的介面卡類,承擔了編碼轉換的任務;對於C程式設計師來說,要完成這樣的編碼轉換恐怕要依賴於union(聯合體/共用體)共享記憶體的特徵來實現了。

24、靜態巢狀類(Static Nested Class)和內部類(Inner Class)的不同?
答:Static Nested Class是被宣告為靜態(static)的內部類,它可以不依賴於外部類例項被例項化。而通常的內部類需要在外部類例項化後才能例項化,其語法看起來挺詭異的。

如果要在靜態方法中建立內部類物件,可以這樣做:

    new Outer().new Inner();

26、抽象的(abstract)方法是否可同時是靜態的(static),是否可同時是本地方法(native),是否可同時被synchronized修飾?
答:都不能。抽象方法需要子類重寫,而靜態的方法是無法被重寫的,因此二者是矛盾的。本地方法是由原生代碼(如C程式碼)實現的方法,而抽象方法是沒有實現的,也是矛盾的。synchronized和方法的實現細節有關,抽象方法不涉及實現細節,因此也是相互矛盾的。

27、闡述靜態變數和例項變數的區別。
答:靜態變數是被static修飾符修飾的變數,也稱為類變數,它屬於類,不屬於類的任何一個物件,一個類不管建立多少個物件,靜態變數在記憶體中有且僅有一個拷貝;例項變數必須依存於某一例項,需要先建立物件然後通過物件才能訪問到它。靜態變數可以實現讓多個物件共享記憶體。

        補充:在Java開發中,上下文類和工具類中通常會有大量的靜態成員。

attention
   <1>static全域性變數與普通的全域性變數有什麼區別?
          全域性變數(外部變數)的說明之前再冠以static 就構成了靜態的全域性變數。全域性變數本身就是靜態儲存方式, 靜態全域性變數當然也是靜態儲存方式.這兩者在儲存方式上並無不同。這兩者的區別雖在於非靜態全域性變數的作用域是整個源程式,當一個源程式由多個原始檔組成時,非靜態的全域性變數在各個原始檔中都是有效的.而靜態全域性變數則限制了其作用域, 即只在定義該變數的原始檔內有效, 在同一源程式的其它原始檔中不能使用它.由於靜態全域性變數的作用域侷限於一個原始檔內,只能為該原始檔內的函式公用,因此可以避免在其它原始檔中引起錯誤.從以上分析可以看出, 把區域性變數改變為靜態變數後是改變了它的儲存方式即改變了它的生存期。把全域性變數改變為靜態變數後是改變了它的作用域, 限制了它的使用範圍.
   <2>static函式與普通函式有什麼區別?
          static函式與普通函式作用域不同.僅在本檔案。只在當前原始檔中使用的函式應該說明為內部函式(static),內部函式應該在當前原始檔中說明和定義.對於可在當前原始檔以外使用的函式,應該在一個頭檔案中說明,要使用這些函式的原始檔要包含這個標頭檔案.
   <3>static區域性變數和普通區域性變數有什麼區別?
          static區域性變數只被初始化一次,下一次依據上一次結果值;

28、是否可以從一個靜態(static)方法內部發出對非靜態(non-static)方法的呼叫?
答:不可以,靜態方法只能訪問靜態成員,因為非靜態方法的呼叫要先建立物件,在呼叫靜態方法時可能物件並沒有被初始化。

靜態方法只能訪問靜態的方法,非靜態的方法既能訪問靜態方法又能訪問非靜態方法。簡言之,非靜態的能訪問非靜態的和靜態的,靜態的只能訪問靜態的。

29、如何實現物件克隆?
答:有兩種方式:
  1). 實現Cloneable介面並重寫Object類中的clone()方法;
  2). 實現Serializable介面,通過物件的序列化和反序列化實現克隆,可以實現真正的深度克隆,

注意:基於序列化和反序列化實現的克隆不僅僅是深度克隆,更重要的是通過泛型限定,可以檢查出要克隆的物件是否支援序列化,這項檢查是編譯器完成的,不是在執行時丟擲異常,這種是方案明顯優於使用Object類的clone方法克隆物件。讓問題在編譯的時候暴露出來總是好過把問題留到執行時。

31、String s = new String("xyz");建立了幾個字串物件?
答:兩個物件,一個是靜態區的"xyz",一個是用new建立在堆上的物件。

32、介面是否可繼承(extends)介面?抽象類是否可實現(implements)介面?抽象類是否可繼承具體類(concrete class)?
答:介面可以繼承介面,而且支援多重繼承。抽象類可以實現(implements)介面,抽象類可繼承具體類也可以繼承抽象類。

34、Anonymous Inner Class(匿名內部類)是否可以繼承其它類?是否可以實現介面?
答:可以繼承其他類或實現其他介面,在Swing程式設計和Android開發中常用此方式來實現事件監聽和回撥。

36、Java 中的final關鍵字有哪些用法?
答:(1)修飾類:表示該類不能被繼承;(2)修飾方法:表示方法不能被重寫;(3)修飾變數:表示變數只能一次賦值以後值不能被修改(常量)。

37、指出下面程式的執行結果。

class A {

    static {
        System.out.print("1");
    }

    public A() {
        System.out.print("2");
    }
}

class B extends A{

    static {
        System.out.print("a");
    }

    public B() {
        System.out.print("b");
    }
}

public class Hello {

    public static void main(String[] args) {
        A ab = new B();
        ab = new B();
    }

}

答:執行結果:1a2b2b。建立物件時構造器的呼叫順序是:先初始化靜態成員,然後呼叫父類構造器,再初始化非靜態成員,最後呼叫自身構造器。

提示:如果不能給出此題的正確答案,說明之前第21題Java類載入機制還沒有完全理解,趕緊再看看吧。

初始化的時機

1)建立類例項的時候,分別有:1、使用new關鍵字建立例項;2、通過反射建立例項;3、通過反序列化方式建立例項。

new Test();
Class.forName(“com.mengdd.Test”);

2)呼叫某個類的類方法(靜態方法)

Test.doSomething();

3)訪問某個類或介面的類變數,或為該類變數賦值。  

int b=Test.a;
Test.a=b;

4)初始化某個類的子類。當初始化子類的時候,該子類的所有父類都會被初始化。

5)直接使用java.exe命令來執行某個主類。

除了上面幾種方式會自動初始化一個類,其他訪問類的方式都稱不會觸發類的初始化,稱為被動引用。

1、子類引用父類的靜態變數,不會導致子類初始化。

2、通過陣列定義引用類,不會觸發此類的初始化

3、引用常量時,不會觸發該類的初始化

用final修飾某個類變數時,它的值在編譯時就已經確定好放入常量池了,所以在訪問該類變數時,等於直接從常量池中獲取,並沒有初始化該類。

初始化的步驟

1、如果該類還沒有載入和連線,則程式先載入該類並連線。

2、如果該類的直接父類沒有載入,則先初始化其直接父類。

3、如果類中有初始化語句,則系統依次執行這些初始化語句。

在第二個步驟中,如果直接父類又有直接父類,則系統會再次重複這三個步驟來初始化這個父類,依次類推,JVM最先初始化的總是java.lang.Object類。當程式主動使用任何一個類時,系統會保證該類以及所有的父類都會被初始化。

38、資料型別之間的轉換:

       - 如何將字串轉換為基本資料型別?
     - 如何將基本資料型別轉換為字串?

答:
- 呼叫基本資料型別對應的包裝類中的方法parseXXX(String)或valueOf(String)即可返回相應基本型別;
- 一種方法是將基本資料型別與空字串("")連線(+)即可獲得其所對應的字串;另一種方法是呼叫String 類中的valueOf()方法返回相應字串

39、如何實現字串的反轉及替換?
答:方法很多,可以自己寫實現也可以使用String或StringBuffer/StringBuilder中的方法。有一道很常見的面試題是用遞迴實現字串反轉,程式碼如下所示:

    public static String reverse(String originStr) {
        if(originStr == null || originStr.length() <= 1) 
            return originStr;
        return reverse(originStr.substring(1)) + originStr.charAt(0);
    }

String本身沒有反轉的函式。但是StringBuffer有。所以用StringBuffer去構造String,然後用StringBuffer的反轉reverse()就可以了

String s="fedcba";
StringBuffer sb=new StringBuffer(s);
sb=sb.reverse();
System.out.println(sb.toString());

40、怎樣將GB2312編碼的字串轉換為ISO-8859-1編碼的字串?
答:程式碼如下所示:

String s1 = "你好";
String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");

43、比較一下Java和JavaSciprt。
答:JavaScript 與Java是兩個公司開發的不同的兩個產品。Java 是原Sun Microsystems公司推出的面向物件的程式設計語言,特別適合於網際網路應用程式開發;而JavaScript是Netscape公司的產品,為了擴充套件Netscape瀏覽器的功能而開發的一種可以嵌入Web頁面中執行的基於物件和事件驅動的解釋性語言。JavaScript的前身是LiveScript;而Java的前身是Oak語言。
下面對兩種語言間的異同作如下比較:
- 基於物件和麵向物件:Java是一種真正的面向物件的語言,即使是開發簡單的程式,必須設計物件;JavaScript是種指令碼語言,它可以用來製作與網路無關的,與使用者互動作用的複雜軟體。它是一種基於物件(Object-Based)和事件驅動(Event-Driven)的程式語言,因而它本身提供了非常豐富的內部物件供設計人員使用。
- 解釋和編譯:Java的原始碼在執行之前,必須經過編譯。JavaScript是一種解釋性程式語言,其原始碼不需經過編譯,由瀏覽器解釋執行。(目前的瀏覽器幾乎都使用了JIT(即時編譯)技術來提升JavaScript的執行效率)
- 強型別變數和型別弱變數:Java採用強型別變數檢查,即所有變數在編譯之前必須作宣告;JavaScript中變數是弱型別的,甚至在使用變數前可以不作宣告,JavaScript的直譯器在執行時檢查推斷其資料型別。
- 程式碼格式不一樣。

補充:上面列出的四點是網上流傳的所謂的標準答案。其實Java和JavaScript最重要的區別是一個是靜態語言,一個是動態語言。目前的程式語言的發展趨勢是函式式語言和動態語言。在Java中類(class)是一等公民,而JavaScript中函式(function)是一等公民,因此JavaScript支援函數語言程式設計,可以使用Lambda函式和閉包(closure),當然Java 8也開始支援函數語言程式設計,提供了對Lambda表示式以及函式式介面的支援。對於這類問題,在面試的時候最好還是用自己的語言回答會更加靠譜,不要背網上所謂的標準答案。

46、try{}裡有一個return語句,那麼緊跟在這個try後的finally{}裡的程式碼會不會被執行,什麼時候被執行,在return前還是後?

答:會執行,在方法返回呼叫者前執行。

47、Java語言如何進行異常處理,關鍵字:throws、throw、try、catch、finally分別如何使用?
答:Java通過面向物件的方法進行異常處理,把各種不同的異常進行分類,並提供了良好的介面。在Java中,每個異常都是一個物件,它是Throwable類或其子類的例項。當一個方法出現異常後便丟擲一個異常物件,該物件中包含有異常資訊,呼叫這個物件的方法可以捕獲到這個異常並可以對其進行處理。Java的異常處理是通過5個關鍵詞來實現的:try、catch、throw、throws和finally。一般情況下是用try來執行一段程式,如果系統會丟擲(throw)一個異常物件,可以通過它的型別來捕獲(catch)它,或通過總是執行程式碼塊(finally)來處理;try用來指定一塊預防所有異常的程式;catch子句緊跟在try塊後面,用來指定你想要捕獲的異常的型別;throw語句用來明確地丟擲一個異常;throws用來宣告一個方法可能丟擲的各種異常(當然宣告異常時允許無病呻吟);finally為確保一段程式碼不管發生什麼異常狀況都要被執行;try語句可以巢狀,每當遇到一個try語句,異常的結構就會被放入異常棧中,直到所有的try語句都完成。如果下一級的try語句沒有對某種異常進行處理,異常棧就會執行出棧操作,直到遇到有處理這種異常的try語句或者最終將異常拋給JVM。

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型別的異常)

54、Collection和Collections的區別?
答:Collection是一個介面,它是Set、List等容器的父介面;Collections是個一個工具類,提供了一系列的靜態方法來輔助容器操作,這些方法包括對容器的搜尋、排序、執行緒安全化等等。

56、TreeMap和TreeSet在排序時如何比較元素?Collections工具類中的sort()方法如何比較元素?
答:TreeSet要求存放的物件所屬的類必須實現Comparable介面,該介面提供了比較元素的compareTo()方法,當插入元素時會回撥該方法比較元素的大小。TreeMap要求存放的鍵值對對映的鍵必須實現Comparable介面從而根據鍵對元素進行排序。Collections工具類的sort方法有兩種過載的形式,第一種要求傳入的待排序容器中存放的物件比較實現Comparable介面以實現元素的比較;第二種不強制性的要求容器中的元素必須可比較,但是要求傳入第二個引數,引數是Comparator介面的子型別(需要重寫compare方法實現元素的比較),相當於一個臨時定義的排序規則,其實就是通過介面注入比較元素大小的演算法,也是對回撥模式的應用(Java中對函數語言程式設計的支援)。

compareTo是Compareable介面的一個方法,主要用於規定建立物件的大小關係,該物件要實現compareable介面, 當a.compareTo(b)>0時,則a>b, 當a.compareTo(b)<0時, a<b.

compare方法是java.util中的Comparator介面的一個方法,compare方法內主要靠定義compareTo規定的物件大小關係來確定物件的大小。

TreeMap 和 TreeSet 是 Java Collection Framework 的兩個重要成員,其中 TreeMap 是 Map 介面的常用實現類,而 TreeSet 是 Set 介面的常用實現類。雖然TreeMap  TreeSet 實現的介面規範不同,但 TreeSet 底層是通過 TreeMap 來實現的(如同HashSet底層是是通過HashMap來實現的一樣),因此二者的實現方式完全一樣。而 TreeMap 的實現就是紅黑樹演算法。

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()方法有什麼區別?
答:
① sleep()方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會;
② 執行緒執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
③ sleep()方法宣告丟擲InterruptedException,而yield()方法沒有宣告任何異常;
④ sleep()方法比yield()方法(跟作業系統CPU排程相關)具有更好的可移植性。

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

提示:關於Java多執行緒和併發程式設計的問題,建議大家看我的另一篇文章《關於Java併發程式設計的總結和思考》

補充: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方法可以線上程執行結束時產生一個返回值

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 塊中釋放(這是釋放外部資源的最好的地方)。

69、Java中有幾種型別的流?
答:位元組流和字元流。位元組流繼承於InputStream、OutputStream,字元流繼承於Reader、Writer。在java.io 包中還有許多其他的流,主要是為了提高效能和使用方便。關於Java的I/O需要注意的有兩點:一是兩種對稱性(輸入和輸出的對稱性,位元組和字元的對稱性);二是兩種設計模式(介面卡模式和裝潢模式)。另外Java中的流不同於C#的是它只有一個維度一個方向。

面試題 - 程式設計實現檔案拷貝。(這個題目在筆試的時候經常出現,下面的程式碼給出了兩種實現方案)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public final class MyUtil {

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

    public static void fileCopy(String source, String target) throws IOException {
        try (InputStream in = new FileInputStream(source)) {
            try (OutputStream out = new FileOutputStream(target)) {
                byte[] buffer = new byte[4096];
                int bytesToRead;
                while((bytesToRead = in.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesToRead);
                }
            }
        }
    }

    public static void fileCopyNIO(String source, String target) throws IOException {
        try (FileInputStream in = new FileInputStream(source)) {
            try (FileOutputStream out = new FileOutputStream(target)) {
                FileChannel inChannel = in.getChannel();
                FileChannel outChannel = out.getChannel();
                ByteBuffer buffer = ByteBuffer.allocate(4096);
                while(inChannel.read(buffer) != -1) {
                    buffer.flip();
                    outChannel.write(buffer);
                    buffer.clear();
                }
            }
        }
    }
}

注意:上面用到Java 7的TWR,使用TWR後可以不用在finally中釋放外部資源 ,從而讓程式碼更加優雅。

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

74、你在專案中哪些地方用到了XML?
答:XML的主要作用有兩個方面:資料交換和資訊配置。在做資料交換時,XML將資料用標籤組裝成起來,然後壓縮打包加密後通過網路傳送給接收者,接收解密與解壓縮後再從XML檔案中還原相關資訊進行處理,XML曾經是異構系統間交換資料的事實標準,但此項功能幾乎已經被JSON(JavaScriptObject Notation)取而代之。當然,目前很多軟體仍然使用XML來儲存配置資訊,我們在很多專案中通常也會將作為配置資訊的硬程式碼寫在XML檔案中,Java的很多框架也是這麼做的,而且這些框架都選擇了dom4j作為處理XML的工具,因為Sun公司的官方API實在不怎麼好用。

補充:現在有很多時髦的軟體(如Sublime)已經開始將配置檔案書寫成JSON格式,我們已經強烈的感受到XML的另一項功能也將逐漸被業界拋棄

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的開源資料庫連線池主要有:C3P0ProxoolDBCPBoneCPDruid等。

補充:在計算機系統中時間和空間是不可調和的矛盾,理解這一點對設計滿足效能要求的演算法是至關重要的。大型網站效能優化的一個關鍵就是使用快取,而快取跟上面講的連線池道理非常類似,也是使用空間換時間的策略。可以將熱點資料置於快取中,當用戶查詢這些資料時可以直接從快取中得到,這無論如何也快過去資料庫中查詢。當然,快取的置換策略等也會對系統性能產生重要影響,對於這個問題的討論已經超出了這裡要闡述的範圍。

79、什麼是DAO模式?
答:DAO(Data Access Object)顧名思義是一個為資料庫或其他持久化機制提供了抽象介面的物件,在不暴露底層持久化方案實現細節的前提下提供了各種資料訪問操作。在實際的開發中,應該將所有對資料來源的訪問操作進行抽象化後封裝在一個公共API中。用程式設計語言來說,就是建立一個介面,介面中定義了此應用程式中將會用到的所有事務方法。在這個應用程式中,當需要和資料來源進行互動的時候則使用這個介面,並且編寫一個單獨的類來實現這個介面,在邏輯上該類對應一個特定的資料儲存。DAO模式實際上包含了兩個模式,一是Data Accessor(資料訪問器),二是Data Object(資料物件),前者要解決如何訪問資料的問題,而後者要解決的是如何用物件封裝資料。82、JDBC能否處理Blob和Clob?
答: Blob是指二進位制大物件(Binary Large Object),而Clob是指大字元物件(Character Large Objec),因此其中Blob是為儲存大的二進位制資料而設計的,而Clob是為儲存大的文字資料而設計的。JDBC的PreparedStatement和ResultSet都提供了相應的方法來支援Blob和Clob操作。

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

說明:上面的正則表示式中使用了懶惰匹配和前瞻,如果不清楚這些內容,推薦讀一下網上很有名的《正則表示式30分鐘入門教程》85、獲得一個類的類物件有哪些方式?
答:
- 方法1:型別.class,例如:String.class
- 方法2:物件.getClass(),例如:"hello".getClass()
- 方法3:Class.forName(),例如:Class.forName("java.lang.String")


90、簡述一下你瞭解的設計模式。
答:所謂設計模式,就是一套被反覆使用的程式碼設計經驗的總結(情境中一個問題經過證實的一個解決方案)。使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。設計模式使人們可以更加簡單方便的複用成功的設計和體系結構。將已證實的技術表述成設計模式也會使新系統開發者更加容易理解其設計思路。
在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(責任鏈模式)。
面試被問到關於設計模式的知識時,可以揀最常用的作答,例如:
- 工廠模式:工廠類可以根據條件生成不同的子類例項,這些子類有一個公共的抽象父類並且實現了相同的方法,但是這些