文章出處

一、Java基礎

1、Java面向對象的三個特征與含義 

三大特征是:封裝、繼承和多態

    封裝是指將某事物的屬性和行為包裝到對象中,這個對象只對外公布需要公開的屬性和行為,而這個公布也是可以有選擇性的公布給其它對象。在Java中能使用private、protected、public三種修飾符或不用(即默認defalut)對外部對象訪問該對象的屬性和行為進行限制。

    繼承是子對象可以繼承父對象的屬性和行為,亦即父對象擁有的屬性和行為,其子對象也就擁有了這些屬性和行為。這非常類似大自然中的物種遺傳。

    多態不是很好解釋:更傾向于使用java中的固定用法,即overriding(覆蓋)和overload(過載)。多態則是體現在overriding(覆蓋)上,而overload(過載)則不屬于面向對象中多態的范疇,因為overload(過載)概念在非面向對象中也存在。overriding(覆蓋)是面向對象中的多態,因為overriding(覆蓋)是與繼承緊密聯系,是面向對象所特有的。多態是指父對象中的同一個行為能在其多個子對象中有不同的表現。也就是說子對象可以使用重寫父對象中的行為,使其擁有不同于父對象和其它子對象的表現,這就是overriding(覆蓋)。

2、super 和 this 關鍵字

在子類構造器中使用super()顯示調用父類的構造方法,super()必須寫在子類構造方法的第一行,否則編譯不通過; 

this

屬性:this屬性表示找到本類的屬性,如果本類沒有找到則繼續查找父類;

方法:this方法表示找到本類的方法,如果本類沒有找到則繼續查找父類;

構造:必須放在構造方法的首行,不能與super關鍵字同時出現;

特殊:表示當前對象;

super:

屬性:super屬性直接在子類之中查找父類中的指定屬性,不再查找子類本身屬性;

方法:super方法直接在子類之中查找父類中的指定方法,不再查找子類本身方法;

構造:必須放在構造方法首行,不能與this關鍵字同時出現。

super 和 this 關鍵字:

(1)調用super()必須寫在子類構造方法的第一行,否則編譯不通過。每個子類構造方法的第一條語句,都是隱含地調用super(),如果父類沒有這種形式的構造函數,那么在編譯的時候就會報錯。

(2)super從子類中調用父類的構造方法,this()在同一類內調用其它方法。

(3)super()和this()均需放在構造方法內第一行。

(4)盡管可以用this調用一個構造器,但卻不能調用兩個。

(5)this和super不能同時出現在一個構造函數里面,因為this必然會調用其它的構造函數,其它的構造函數必然也會有super語句的存在,所以在同一個構造函數里面有相同的語句,就失去了語句的意義,編譯器也不會通過。

(6)this()和super()都指的是對象,所以,均不可以在static環境中使用。包括:static變量,static方法,static語句塊。

(7)從本質上講,this是一個指向本對象的指針, 然而super是一個Java關鍵字。

3、訪問權限

(1)訪問權限修飾詞

1)public(公共的):表明該成員變量或方法對所有類或對象都是可見的,所有類或對象都可以直接訪問;

2)protected(受保護的):表明成員變量或方法對該類本身&與它在同一個包中的其它類&在其它包中的該類的子類都可見;

3)default(默認的,不加任何訪問修飾符):表明成員變量或方法只有自己&其位于同一個包內的類可見;

4)private(私有的):表明該成員變量或方法是私有的,只有當前類對其具有訪問權限。

由大到小:public(接口訪問權限)、protected(繼承訪問權限)、包訪問權限(沒有使用任何訪問權限修飾詞)、private(私有無法訪問)。

protected表示就類用戶而言,這是private的,但對于任何繼承于此類的導出類或其他任何位于同一個包內的類來說,卻是可以訪問的(protected也提供了包內訪問權限)。

private和protected一般不用來修飾外部類,而public、abstract或final可以用來修飾外部類(如果用private和protected修飾外部類,會使得該類變得訪問性受限)。

(2)訪問權限注意點:

1)類的訪問權限,只能是包訪問權限(默認無訪問修飾符即可)或者public。若把一個類中的構造器指定為private,則不能訪問該類,若要創建該類的對象,則需要在該類的static成員內部創建,如單例模式。

2)如果沒能為類訪問權限指定一個訪問修飾符,默認得到包訪問權限,則該類的對象可以由包內任何其他類創建,但是包外不可以。

3)訪問權限的控制,也稱為具體實現的隱藏。制定規則(如使用訪問權限,設定成員所遵守的界限),是防止客戶端程序員對類隨心所欲而為。

(3)控制對成員的訪問權限的兩個原因:

使用戶不要碰觸那些不該碰觸的部分,對類內部的操作是必要的,不屬于客戶端程序員所需接口的一部分;

讓類庫設計者可以更改類的內部工作方式,而不會對客戶端程序員產生重大影響;訪問權限控制可以確保不會有任何客戶端程序員依賴于類的底層實現的任何部分。

(4)對某成員的訪問權的唯一途徑:

1)該成員為public;

2)通過不加訪問權限修飾詞并將其他類放置在同一個包內的方式給成員賦予包訪問權;

3)繼承技術,訪問protected成員;

4)提供訪問器和變異器(get/set方法),以讀取和改變數值。

4、抽象類

(1)抽象類不能被實例化,實例化的工作應該交由它的子類來完成,它只需要有一個引用即可。

(2)抽象方法必須由子類來進行重寫。

(3)只要包含一個抽象方法的類,該類必須要定義成抽象類,不管是否還包含有其他方法。

(4)抽象類中可以包含具體的方法,當然也可以不包含抽象方法。

(5)子類中的抽象方法不能與父類的抽象方法同名。

(6)abstract不能與final并列修飾同一個類。(abstract需要子類去實現,而final表示不能被繼承,矛盾。)

(7)abstract 不能與private、static、final或native并列修飾同一個方法。

注意:

A、final修飾的類為終態類,不能被繼承,而抽象類是必須被繼承的才有其意義的,因此,final是不能用來修飾抽象類的。

B、final修飾的方法為終態方法,不能被重寫。而繼承抽象類,必須重寫其方法。

C、抽象方法是僅聲明,并不做實現的方法。

5、值傳遞與引用傳遞

值傳遞:Java中原始數據類型都是值傳遞,傳遞的是值的副本,形參的改變不會影響實際參數的值;

引用傳遞:傳遞的是引用類型數據,包括String,數組,列表,map,類對象等類型,形參與實參指向的是同一內存地址,因此形參改變會影響實參的值。

6、繼承

定義:按照現有類的類型來創建新類 ,無需改變現有類的形式,采用現有類的形式并在其增加新代碼,稱為繼承。通過關鍵字extends實現。

特點:

(1)當創建一個類時,總在繼承。(除非明確指明繼承類,否則都是隱式第繼承根類Object);

(2)為了繼承,一般將所有的數據成員都指定為private,將所有的方法指定為public;

(3)可以將繼承視作是對類的復用;

(4)is-a關系用繼承;

(5)繼承允許對象視為自身的類型或其基類型加以處理;

(6)如果向上轉型,不能調用那些新的方法(如Animal an = new Cat(),an是不能調用Cat中有的而Animal中沒有的方法,會返回一條編譯時出錯消息),所以向上轉型會丟失具體的類型信息。

注意:

(1)構造方法不能被繼承;方法和屬性可以被繼承;

(2)子類的構造方法隱式地調用父類的不帶參數的構造方法;

(3)當父類沒有不帶參數的構造方法時,子類需要使用super來顯示調用父類的構造方法,super指的是對父類的引用;

(4)super關鍵字必須是構造方法中的第一行語句。特例如下:

      當兩個方法形成重寫關系時,可在子類方法中通過super.run() 形式調用父類的run()方法,其中super.run()不必放在第一行語句,因此此時父類對象已經構造完畢,先調用父類的run()方法還是先調用子類的run()方法是根據程序的邏輯決定的。

7、是否可以繼承String類?

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

8、final關鍵字

1)使用范圍:數據、方法和類。

2)final關鍵字:final可以修飾屬性、方法、類。

3)final修飾類:當一個類被final所修飾時,表示該類是一個終態類,即不能被繼承

4)final修飾方法:當一個方法被final所修飾時,表示該方法是一個終態方法,即不能被重寫(Override)。

5)final修飾屬性:當一個屬性被final所修飾時,表示該屬性不能被改寫

(1)final數據:

1)編譯時常量:是使用static和 final修飾的常量,全用大寫字母命名,且字與字之間用下劃線隔開。(不能因為數據是final的就認為在編譯時就知道值,在運行時也可以用某數值來初始化某一常量)

2)final修飾基本數據類型和對象引用:對于基本類型,final修飾的數值是恒定不變;而final修飾對象引用,則引用恒定不變(一旦引用被初始化指向一個對象,就不能改為指向另一個對象),但是對象本身的內容可以修改。

3)空白final:空白final是指被聲明為final但又未給定初值的域,無論什么情況,編譯器都保證空白final在使用被初始化。必須在域的定義處或每個構造器中用表達式對final進行賦值。

4)final參數:final修飾參數后,在方法體中不允許對參數進行更改,只可以讀final參數。主要用于向匿名類傳遞數據。

(2)final方法:

1)使用final修飾方法原因:將方法鎖定以及效率問題。將方法鎖定:防止任何繼承類修改final方法的含義,確保該方法行為保持不變,且不會被覆蓋;效率:早期Java實現中同意編譯器將針對該方法的所有調用轉為內嵌調用。

2)類中所有的private方法都隱式地指定為final的。

(3)final類:

將某個類整體定義為final時,則不能繼承該類,不能有子類。

9、static關鍵字是什么意思?Java中是否可以覆蓋(override)一個private或者是static的方法?

    static表示靜態的意思,可用于修飾成員變量和成員函數,被靜態修飾的成員函數只能訪問靜態成員,不可以訪問非靜態成員。靜態是隨著類的加載而加載的,因此可以直接用類進行訪問。 重寫是子類中的方法和子類繼承的父類中的方法一樣(函數名,參數,參數類型,返回值類型),但是子類中的訪問權限要不低于父類中的訪問權限。重寫的前提是必須要繼承,private修飾不支持繼承,因此被私有的方法不可以被重寫。靜態方法形式上可以被重寫,即子類中可以重寫父類中靜態的方法。但是實際上從內存的角度上靜態方法不可以被重寫。


①static可以修飾內部類,但是不能修飾普通類。靜態內部類的話可以直接調用靜態構造器(不用對象)。

②static修飾方法,static 方法就是沒有 this 的方法。在static方法內部不能調用非靜態方法,反過來是可以的。而且可以在沒有創建任何對象的前提下,僅僅通過類本身來調用 static 方法。這實際上正是 static 方法的主要用途,方便在沒有創建對象的情況下來進行調用(方法/變量)。

最常見的static方法就是main,因為所有對象都是在該方法里面實例化的,而main是程序入口,所以要通過類名來調用。還有就是main中需要經常訪問隨類加載的成員變量。

③static修飾變量,就變成了靜態變量,隨類加載一次,可以被多個對象共享。

④static修飾代碼塊,形成靜態代碼塊,用來優化程序性能,將需要加載一次的代碼設置成隨類加載,靜態代碼塊可以有多個。

Java中static方法不能被覆蓋,因為方法覆蓋是基于運行時動態綁定的,而static方法是編譯時靜態綁定的。還有私有的方法不能被繼承,子類就沒有訪問權限,肯定也是不能別覆蓋的。

10、static方法能否被重寫

     在Java中,子類可繼承父類中的方法,而不需要重新編寫相同的方法。但有時子類并不想原封不動地繼承父類的方法,而是想作一定的修改,這就需要采用方法的重寫(Override)。方法重寫又稱方法覆蓋。 在《Java編程思想》中提及到:“覆蓋”只有在某方法是基類的接口的一部分時才會出現。即,必須能將一個對象向上轉型為它的基本類型并調用相同的方法。那么,我們便可以據此來對static方法能否被重寫的問題進行驗證。

例子:

class StaticSuper{
    public static String staticGet(){
        return "Base staticGet()";
    }
    public String dynamicGet(){
        return "Base dynamicGet()";
    }
}

class StaticSub extends StaticSuper{
    public static String staticGet(){
        return "Derived staticGet()";
    }
    public String dynamicGet(){
        return "Derived dynamicGet()";
    }
}

public class StaticPolyMorphism {
    public static void main(String[] args) {
        StaticSuper sup = new StaticSub();
        System.out.println(sup.staticGet());
        System.out.println(sup.dynamicGet());
    }
}

    在例子中,如果基類StaticSuper中的static方法staticGet()在子類StaticSub中被重寫了,那么sup.staticGet()返回的結果應該是“Derived staticGet()”,實際上結果是如何呢?運行程序后,我們看到輸出是: 

Base staticGet() 
Derived dynamicGet()  

這說明,非靜態方法dynamicGet()的確在子類中被重寫了,而靜態方法staticGet()卻沒有。對于這一點,我們也可以通過在子類方法上添加@Overide注解進行驗證: 

      如圖所示,在子類中的靜態方法staticGet()上添加@Override注解會導致編譯報錯:The method staticGet() of type StaticSub must override or implement a supertype method(StaticSub類的staticGet()方法必須覆蓋或者實現一個父型的方法),而非靜態方法dynamicGet()則無此報錯信息,這也就印證了我們上面的推論。其實,在Java中,如果父類中含有一個靜態方法,且在子類中也含有一個返回類型、方法名、參數列表均與之相同的靜態方法,那么該子類實際上只是將父類中的該同名方法進行了隱藏,而非重寫。換句話說,父類和子類中含有的其實是兩個沒有關系的方法,它們的行為也并不具有多態性。正如同《Java編程思想》中所說:“一旦你了解了多態機制,可能就會認為所有事物都可以多態地發生。然而,只有普通方法的調用可以是多態的。”這也很好地理解了,為什么在Java中,static方法和final方法(private方法屬于final方法)是前期綁定,而其他所有的方法都是后期綁定了。

11、靜態方法和實例方法的區別

(1)在外部調用靜態方法時,可以使用"類名.方法名"的方式,也可以使用"對象名.方法名"的方式。而實例方法只有后面這種方式。也就是說,調用靜態方法可以無需創建對象。

(2)靜態方法在訪問本類的成員時,只允許訪問靜態成員(即靜態成員變量和靜態方法),而不允許訪問實例成員變量和實例方法;實例方法則無此限制。

例子1:調用靜態方法示例

package com.demo;

public class hasStaticMethod {

    //定義一個靜態方法
    public static void callMe(){
        System.out.println("This is a static method.");
    } 
}
package com.demo;

public class invokeStaticMethod {
    
    //下面這個程序使用兩種形式來調用靜態方法。
    public static void main(String args[]){
        hasStaticMethod.callMe();  //不創建對象,直接調用靜態方法      
        hasStaticMethod oa = new hasStaticMethod(); //創建一個對象  
        oa.callMe(); //利用對象來調用靜態方法}
    }
}

程序兩次調用靜態方法,都是允許的,程序的輸出如下:

This is a static method.
This is a static method.

例子2:靜態方法訪問成員變量示例

package com.demo;

public class accessMember {
    
    private static int sa; //定義一個靜態成員變量
    private int ia;  //定義一個實例成員變量
    
    //下面定義一個靜態方法
    static void statMethod(){
        int i = 0;    //正確,可以有自己的局部變量  
        sa = 10;      //正確,靜態方法可以使用靜態變量
        otherStat();  //正確,可以調用靜態方法  
        ia = 20;      //錯誤,不能使用實例變量  
        insMethod();  //錯誤,不能調用實例方法
        
    }
    
    static void otherStat(){}
    
    //下面定義一個實例方法
    void  insMethod(){
        int i = 0;    //正確,可以有自己的局部變量  
        sa = 15;      //正確,可以使用靜態變量  
        ia = 30;      //正確,可以使用實例變量  
        statMethod(); //正確,可以調用靜態方法
    }
}

    本例其實可以概括成一句話:靜態方法只能訪問靜態成員,實例方法可以訪問靜態和實例成員。之所以不允許靜態方法訪問實例成員變量,是因為實例成員變量是屬于某個對象的,而靜態方法在執行時,并不一定存在對象。同樣,因為實例方法可以訪問實例成員變量,如果允許靜態方法調用實例方法,將間接地允許它使用實例成員變量,所以它也不能調用實例方法。基于同樣的道理,靜態方法中也不能使用關鍵字this。

    main()方法是一個典型的靜態方法,它同樣遵循一般靜態方法的規則,所以它可以由系統在創建對象之前就調用。 

12、闡述靜態變量和實例變量的區別

答:靜態變量是被static修飾符修飾的變量,也稱為類變量,它屬于類,不屬于類的任何一個對象,一個類不管創建多少個對象,靜態變量在內存中有且僅有一個拷貝;實例變量必須依存于某一實例,需要先創建對象然后通過對象才能訪問到它。靜態變量可以實現讓多個對象共享內存。

13、是否可以從一個靜態(static)方法內部發出對非靜態(non-static)方法的調用?

答:不可以,靜態方法只能訪問靜態成員,因為非靜態方法的調用要先創建對象,在調用靜態方法時可能對象并沒有被初始化。

14、抽象的(abstract)方法是否可同時是靜態的(static),是否可同時是本地方法(native),是否可同時被synchronized修飾?

答:都不能。抽象方法需要子類重寫,而靜態的方法是無法被重寫的,因此二者是矛盾的。本地方法是由本地代碼(如C代碼)實現的方法,而抽象方法是沒有實現的,也是矛盾的。synchronized和方法的實現細節有關,抽象方法不涉及實現細節,因此也是相互矛盾的。

15、闡述final、finally、finalize的區別

- final:修飾符(關鍵字)有三種用法:如果一個類被聲明為final,意味著它不能再派生出新的子類,即不能被繼承,因此它和abstract是反義詞。將變量聲明為final,可以保證它們在使用中不被改變,被聲明為final的變量必須在聲明時給定初值,而在以后的引用中只能讀取不可修改。被聲明為final的方法也同樣只能使用,不能在子類中被重寫。

- finally:通常放在try…catch…的后面構造總是執行代碼塊,這就意味著程序無論正常執行還是發生異常,這里的代碼只要JVM不關閉都能執行,可以將釋放外部資源的代碼寫在finally塊中。

- finalize:Object類中定義的方法,Java中允許使用finalize()方法在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。這個方法是由垃圾收集器在銷毀對象時調用的,通過重寫finalize()方法可以整理系統資源或者執行其他清理工作。

16、== 與 equals() 方法的區別

(1)基本數據類型與引用數據類型

1)基本數據類型的比較:只能用==;

2)引用數據類型的比較:==是比較棧內存中存放的對象在堆內存地址,equals是比較對象的內容是否相同。

(2)特殊:String作為一個對象

例子一:通過構造函數創建對象時。對象不同,內容相同,"=="返回false,equals返回true。

String s1 = newString("java");
String s2 = new String("java");

System.out.println(s1==s2);            //false
System.out.println(s1.equals(s2));   //true

例子二:同一對象,"=="和equals結果相同

String s1 = new String("java");
String s2 = s1;  //兩個不同的引用變量指向同一個對象

System.out.println(s1==s2);            //true
System.out.println(s1.equals(s2));   //true
String s1 = "java";
String s2 = "java";  //此時String常量池中有java對象,直接返回引用給s2;

System.out.println(s1==s2);  //true

System.out.println(s1.equals(s2));   //true

字面量形式創建對象時:

如果String緩沖池內不存在與其指定值相同的String對象,那么此時虛擬機將為此創建新的String對象,并存放在String緩沖池內。

如果String緩沖池內存在與其指定值相同的String對象,那么此時虛擬機將不為此創建新的String對象,而直接返回已存在的String對象的引用。

(3)String的字面量形式和構造函數創建對象

String s = "aaa"; //采用字面值方式賦值

1)查找StringPool中是否存在“aaa”這個對象,如果不存在,則在String Pool中創建一個“aaa”對象,然后將String Pool中的這個“aaa”對象的地址返回來,賦給引用變量s,這樣s會指向String Pool中的這個“aaa”字符串對象;

2)如果存在,則不創建任何對象,直接將String Pool中的這個“aaa”對象地址返回來,賦給s引用。

String s = new String("aaa");

1)首先在String Pool中查找有沒有"aaa"這個字符串對象,如果有,則不在String Pool中再去創建"aaa"這個對象,直接在堆中創建一個"aaa"字符串對象,然后將堆中的這個"aaa"對象的地址返回來,賦給s引用,導致s指向了堆中創建的這個"aaa"字符串對象;

2)如果沒有,則首先在String Pool中創建一個"aaa"對象,然后再去堆中創建一個"aaa"對象,然后將堆中的這個"aaa"對象的地址返回來,賦給s引用,導致s指向了堆中所創建的這個"aaa"對象。

總結來說:

1)對于==,如果作用于基本數據類型的變量,則直接比較其存儲的 “值”是否相等;如果作用于引用類型的變量,則比較的是所指向的對象的地址。

2)對于equals方法,注意:equals方法不能作用于基本數據類型的變量。如果沒有對equals方法進行重寫,則比較的是引用類型的變量所指向的對象的地址;諸如String、Date等類對equals方法進行了重寫的話,比較的是所指向的對象的內容。

17、初始化及類的加載

(1)加載的含義:通常,加載發生在創建類的第一個對象時,但訪問static域或static方法時,也會發生加載。static的東西只會初始化一次。

(2)加載過程:加載一個類的時候,首先去加載父類的靜態域,然后再加載自身的靜態域,之后去初始化父類的成員變量,后加載父類的構造方法,最后初始化自身的成員變量,后加載自身的構造方法。(先初始化成員變量,后加載構造函數的原因是,構造函數中可能要用到這些成員變量)

父類靜態塊——子類靜態塊——父類塊——父類構造器——子類塊——子類構造器

最終版本:父類靜態域——父類靜態塊——子類靜態域——子類靜態塊——父類成員變量及代碼塊——父類構造器——子類成員變量及代碼塊——子類構造器。

(3)加載次數:加載的動作只會加載一次,該類的靜態域或第一個實體的創建都會引起加載。

(4)變量的初始化:變量的初始化總是在當前類構造器主體執行之前進行的,且static的成員比普通的成員變量先初始化。

指出下面程序的運行結果

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。創建對象時構造器的調用順序是:先初始化靜態成員,然后調用父類構造器,再初始化非靜態成員,最后調用自身構造器。

18、多態

(1)多態只發生在普通方法中,對于域和static方法,不發生多態。子類對象轉化為父類型引用時,對于任何域的訪問都是由編譯器解析。靜態方法是與類相關聯,而不與單個對象相關聯;

(2)在繼承時,若被覆寫的方法不是private,則父類調用方法時,會調用子類的方法,常用的多態性就是當父類引用指向子類對象時。

(3)多態就是指程序中定義的引用變量所指向的具體類型和通過該引用變量發出的方法調用在編程時并不確定,而是在程序運行期間才確定,即一個引用變量到底指向哪個類的實例對象,該引用變量發出的方法調用到底是哪個類中實現的方法,必須在由程序運行期間才能決定。

(4)多態是同一個行為具有多個不同表現形式或形態的能力。

(5)多態就是同一個接口,使用不同的實例而執行不同操作,多態性是對象多種表現形式的體現。

19、基本數據類型與包裝類

所有的包裝類(8個)都位于java.lang包下,分別是Byte,Short,Integer,Long,Float,Double,Character,Boolean。

基本數據類型:byte:8位;short:16位;int:32位;long:64位;float:32位;double:64位;char:16位;boolean:8位。

20、Object類的公有方法

clone()(protected的)、toString()、equals(Object obj)、hashCode()、getClass()、finialize()(protected的)、notify()/notifyAll()、wait()/wait(long timeout)、wait(long timeout,intnaos)

(1)clone方法

保護方法,實現對象的淺復制,只有實現了Cloneable接口才可以調用該方法,否則拋出CloneNotSupportedException異常。

主要是JAVA里除了8種基本類型傳參數是值傳遞,其他的類對象傳參數都是引用傳遞,我們有時候不希望在方法里講參數改變,這是就需要在類中復寫clone方法。

(2)getClass方法

final方法,獲得運行時類型。

(3)toString方法

該方法用得比較多,一般子類都有覆蓋。

(4)finalize方法

該方法用于釋放資源。因為無法確定該方法什么時候被調用,很少使用。

(5)equals方法

該方法是非常重要的一個方法。一般equals和==是不一樣的,但是在Object中兩者是一樣的。子類一般都要重寫這個方法。

(6)hashCode方法

該方法用于哈希查找,可以減少在查找中使用equals的次數,重寫了equals方法一般都要重寫hashCode方法。這個方法在一些具有哈希功能的Collection中用到。

一般必須滿足obj1.equals(obj2)==true,可以推出obj1.hashCode()==obj2.hashCode(),但是hashCode相等不一定就滿足equals。不過為了提高效率,應該盡量使上面兩個條件接近等價。

如果不重寫hashcode(),在HashSet中添加兩個equals的對象,會將兩個對象都加入進去。

(7)wait方法

wait方法就是使當前線程等待該對象的鎖,當前線程必須是該對象的擁有者,也就是具有該對象的鎖。wait()方法一直等待,直到獲得鎖或者被中斷。wait(long timeout)設定一個超時間隔,如果在規定時間內沒有獲得鎖就返回。

調用該方法后當前線程進入睡眠狀態,直到以下事件發生。

(7.1)其他線程調用了該對象的notify方法。

(7.2)其他線程調用了該對象的notifyAll方法。

(7.3)其他線程調用了interrupt中斷該線程。

(7.4)時間間隔到了。

此時該線程就可以被調度了,如果是被中斷的話就拋出一個InterruptedException異常。

(8)notify方法

該方法喚醒在該對象上等待的某個線程。

(9)notifyAll方法

該方法喚醒在該對象上等待的所有線程。

21、Hashcode的作用

Hash是散列的意思,就是把任意長度的輸入,通過散列算法變換成固定長度的輸出,該輸出就是散列值。關于散列值,有以下幾個關鍵結論:

(1)、如果散列表中存在和散列原始輸入K相等的記錄,那么K必定在f(K)的存儲位置上。

(2)、不同關鍵字經過散列算法變換后可能得到同一個散列地址,這種現象稱為碰撞。

(3)、如果兩個Hash值不同(前提是同一Hash算法),那么這兩個Hash值對應的原始輸入必定不同。

HashCode

然后講下什么是HashCode,總結幾個關鍵點:

(1)、HashCode的存在主要是為了查找的快捷性,HashCode是用來在散列存儲結構中確定對象的存儲地址的。

(2)、如果兩個對象equals相等,那么這兩個對象的HashCode一定也相同。

(3)、如果對象的equals方法被重寫,那么對象的HashCode方法也盡量重寫。

(4)、如果兩個對象的HashCode相同,不代表兩個對象就相同,只能說明這兩個對象在散列存儲結構中,存放于同一個位置。

HashCode有什么用

回到最關鍵的問題,HashCode有什么用?不妨舉個例子:

(1)、假設內存中有0 1 2 3 4 5 6 7 8這8個位置,如果我有個字段叫做ID,那么我要把這個字段存放在以上8個位置之一,如果不用HashCode而任意存放,那么當查找時就需要到8個位置中去挨個查找。

(2)、使用HashCode則效率會快很多,把ID的HashCode%8,然后把ID存放在取得余數的那個位置,然后每次查找該類的時候都可以通過ID的HashCode%8求余數直接找到存放的位置了。

(3)、如果ID的 HashCode%8算出來的位置上本身已經有數據了怎么辦?這就取決于算法的實現了,比如ThreadLocal中的做法就是從算出來的位置向后查找第一個為空的位置,放置數據;HashMap的做法就是通過鏈式結構連起來。反正,只要保證放的時候和取的時候的算法一致就行了。

(4)、如果ID的 HashCode%8相等怎么辦(這種對應的是第三點說的鏈式結構的場景)?這時候就需要定義equals了。先通過HashCode%8來判斷類在哪一個位置,再通過equals來在這個位置上尋找需要的類。對比兩個類的時候也差不多,先通過HashCode比較,假如HashCode相等再判斷 equals。如果兩個類的HashCode都不相同,那么這兩個類必定是不同的。

     舉個實際的例子Set。我們知道Set里面的元素是不可以重復的,那么如何做到?Set是根據equals()方法來判斷兩個元素是否相等的。比方說Set里面已經有1000個元素了,那么第1001個元素進來的時候,最多可能調用1000次equals方法,如果equals方法寫得復雜,對比的東西特別多,那么效率會大大降低。使用HashCode就不一樣了,比方說HashSet,底層是基于HashMap實現的,先通過HashCode取一個模,這樣一下子就固定到某個位置了,如果這個位置上沒有元素,那么就可以肯定HashSet中必定沒有和新添加的元素equals的元素,就可以直接存放了,都不需要比較;如果這個位置上有元素了,逐一比較,比較的時候先比較HashCode,HashCode都不同接下去都不用比了,肯定不一樣,HashCode相等,再equals比較,沒有相同的元素就存,有相同的元素就不存。如果原來的Set里面有相同的元素,只要HashCode的生 成方式定義得好(不重復),不管Set里面原來有多少元素,只需要執行一次的equals就可以了。這樣一來,實際調用equals方法的次數大大降低,提高了效率。

22、兩個對象值相同(x.equals(y) == true),但卻可有不同的hashcode,這句話對不對?

答:不對,如果兩個對象x和y滿足x.equals(y) == true,它們的哈希碼(hashcode)應當相同。Java對于eqauls方法和hashCode方法是這樣規定的:

(1) 如果兩個對象相同(equals方法返回true),那么它們的hashCode值一定要相同;

(2) 如果兩個對象的hashCode相同,它們并不一定相同。

當然,你未必要按照要求去做,但是如果你違背了上述原則就會發現在使用容器時,相同的對象可以出現在Set集合中,同時增加新元素的效率會大大下降(對于使用哈希存儲的系統,如果哈希碼頻繁的沖突將會造成存取性能急劇下降)。

23、重寫equals()方法為什么要重寫hashcode()方法?

object對象中的 public boolean equals(Object obj),對于任何非空引用值 x 和 y,當且僅當 x 和 y 引用同一個對象時,此方法才返回 true;
注意:當此方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具有相等的哈希碼。如下:

(1)當obj1.equals(obj2)為true時,obj1.hashCode() == obj2.hashCode()必須為true
(2)當obj1.hashCode() == obj2.hashCode()為false時,obj1.equals(obj2)必須為false

     如果不重寫equals,那么比較的將是對象的引用是否指向同一塊內存地址,重寫之后目的是為了比較兩個對象的value值是否相等。特別指出利用equals比較八大包裝對象(如int,float等)和String類(因為該類已重寫了equals和hashcode方法)對象時,默認比較的是值,在比較其它自定義對象時都是比較的引用地址
hashcode是用于散列數據的快速存取,如利用HashSet/HashMap/Hashtable類來存儲數據時,都是根據存儲對象的hashcode值來進行判斷是否相同的。

     這樣如果我們對一個對象重寫了equals,意思是只要對象的成員變量值都相等那么equals就等于true,但不重寫hashcode,那么我們再new一個新的對象,當原對象.equals(新對象)等于true時,兩者的hashcode卻是不一樣的,由此將產生了理解的不一致,如在存儲散列集合時(如Set類),將會存儲了兩個值一樣的對象,導致混淆,因此,就也需要重寫hashcode()。

24、Override 和 Overload的含義和區別

(1).、Override 特點  

1)、覆蓋的方法的標志必須要和被覆蓋的方法的標志完全匹配,才能達到覆蓋的效果;  

2)、覆蓋的方法的返回值必須和被覆蓋的方法的返回一致;  

3)、覆蓋的方法所拋出的異常必須和被覆蓋方法的所拋出的異常一致,或者是其子類;

4)、方法被定義為final不能被重寫。 

5)、對于繼承來說,如果某一方法在父類中是訪問權限是private,那么就不能在子類對其進行重寫覆蓋,如果定義的話,也只是定義了一個新方法,而不會達到重寫覆蓋的效果。(通常存在于父類和子類之間。)

(2)、Overload 特點  

1)、在使用重載時只能通過不同的參數樣式。例如,不同的參數類型,不同的參數個數,不同的參數順序(當然,同一方法內的幾個參數類型必須不一樣,例如可以是fun(int, float), 但是不能為fun(int, int));  

2)、不能通過訪問權限、返回類型、拋出的異常進行重載;  

3)、方法的異常類型和數目不會對重載造成影響;  

4)、重載事件通常發生在同一個類中,不同方法之間的現象。

其具體實現機制:

overload是重載,重載是一種參數多態機制,即代碼通過參數的類型或個數不同而實現的多態機制。是一種靜態的綁定機制(在編譯時已經知道具體執行的是哪個代碼段)。  

override是覆蓋。覆蓋是一種動態綁定的多態機制。即在父類和子類中同名元素(如成員函數)有不同的實現代碼。執行的是哪個代碼是根據運行時實際情況而定的。


     方法的重載和重寫都是實現多態的方式,區別在于前者實現的是編譯時的多態性,而后者實現的是運行時的多態性。重載發生在一個類中,同名的方法如果有不同的參數列表(參數類型不同、參數個數不同或者二者都不同)則視為重載;重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回類型,比父類被重寫方法更好訪問,不能比父類被重寫方法聲明更多的異常(里氏代換原則)。重載對返回類型沒有特殊的要求。

25、重載(Overload)與覆蓋(Override)

重載(overload):對于類的方法(包括從父類中繼承的方法),方法名相同參數列表不同的方法之間就構成了重載關系。這里有兩個問題需要注意:

(1)什么叫參數列表?參數列表又叫參數簽名,指三樣東西:參數的類型,參數的個數,參數的順序這三者只要有一個不同就叫做參數列表不同。

(2)重載關系只能發生在同一個類中嗎?非也。這時候你要深刻理解繼承,要知道一個子類所擁有的成員除了自己顯式寫出來的以外,還有父類遺傳下來的。所以子類中的某個方法和父類中繼承下來的方法也可以發生重載的關系。例如,父類中有一個方法是 func(){ ... },子類中有一個方法是 func(int i){ ... },就構成了方法的重載。

大家在使用的時候要緊扣定義,看方法之間是否是重載關系,不用管方法的修飾符和返回類型以及拋出的異常,只看方法名和參數列表。而且要記住,構造器也可以重載。

返回值和異常以及訪問修飾符,不能作為重載的條件(因為對于匿名調用,會出現歧義,eg:void a ()和int a() ,如果調用a(),出現歧義)


覆蓋(override):如果在子類中定義一個方法,其名稱、返回類型及參數簽名正好與父類中某個方法的名稱、返回類型及參數簽名相匹配,那么可以說,子類的方法覆蓋了父類的方法。

覆蓋 (override):也叫重寫,就是在當父類中的某些方法不能滿足要求時,子類中改寫父類的方法。當父類中的方法被覆蓋了后,除非用super關鍵字,否則就無法再調用父類中的方法了。

發生覆蓋的條件:

(1)、“三同一不低” 子類和父類的方法名稱參數列表返回類型必須完全相同,而且子類方法的訪問修飾符的權限不能比父類

(2)、子類方法不能拋出比父類方法更多的異常。即子類方法所拋出的異常必須和父類方法所拋出的異常一致,或者是其子類,或者什么也不拋出

(3)、被覆蓋的方法不能是final類型的。因為final修飾的方法是無法覆蓋的。

(4)、被覆蓋的方法不能為private。否則在其子類中只是新定義了一個方法,并沒有對其進行覆蓋。

(5)、被覆蓋的方法不能為static。所以如果父類中的方法為靜態的,而子類中的方法不是靜態的,但是兩個方法除了這一點外其他都滿足覆蓋條件,那么會發生編譯錯誤。反之亦然。即子類實例方法不能覆蓋父類的靜態方法;子類的靜態方法也不能覆蓋父類的實例方法(編譯時報錯),總結為方法不能交叉覆蓋。即使父類和子類中的方法都是靜態的,并且滿足覆蓋條件,但是仍然不會發生覆蓋,因為靜態方法是在編譯的時候把靜態方法和類的引用類型進行匹配。

(6)、父類的抽象方法可以被子類通過兩種途徑覆蓋(即實現和覆蓋)。

(7)、父類的非抽象方法可以被覆蓋為抽象方法。 

方法的覆蓋和重載具有以下相同點:

都要求方法同名

都可以用于抽象方法和非抽象方法之間

方法的覆蓋和重載具有以下不同點:

方法覆蓋要求參數列表(參數簽名)必須一致,而方法重載要求參數列表必須不一致。

方法覆蓋要求返回類型必須一致,方法重載對此沒有要求。

方法覆蓋只能用于子類覆蓋父類的方法,方法重載用于同一個類中的所有方法(包括從父類中繼承而來的方法)

方法覆蓋對方法的訪問權限和拋出的異常有特殊的要求,而方法重載在這方面沒有任何限制。

父類的一個方法只能被子類覆蓋一次,而一個方法可以在所有的類中可以被重載多次。

另外,對于屬性(成員變量)而言,是不能重載的,只能覆蓋。

26、Interface 與 abstract 類的區別

(1)、abstract class 在Java中表示的是一種繼承關系,一個類只能使用一次繼承關系。但是,一個類卻可以實現多個interface。

(2)、在abstract class 中可以有自己的數據成員,也可以有非abstarct的方法,而在interface中,只能夠有靜態的不能被修改的數據成員(也就是必須是static final的,不過在 interface中一般不定義數據成員),所有的方法都是public abstract的。

(3)、抽象類中的變量默認是 friendly 型,其值可以在子類中重新定義,也可以重新賦值。接口中定義的變量默認是public static final 型,且必須給其賦初值,所以實現類中不能重新定義,也不能改變其值。

(4)、abstract class和interface所反映出的設計理念不同。其實abstract class表示的是"is-a"關系,interface表示的是"like-a"關系。

(5)、實現抽象類和接口的類必須實現其中的所有方法。抽象類中可以有非抽象方法。接口中則不能有實現方法。

       abstract class 和 interface 是 Java語言中的兩種定義抽象類的方式,它們之間有很大的相似性。但是對于它們的選擇卻又往往反映出對于問題領域中的概念本質的理解、對于設計意圖的反映是否正確、合理,因為它們表現了概念間的不同的關系。

27、try catch  finally

(1)finally里面的代碼一定會執行的;

(2)當try和catch中有return時,先執行return中的運算結果但是先不返回,然后保存下來計算結果,接著執行finally,最后再返回return的值。

(3)finally中最好不要有return,否則,直接返回,而先前的return中計算后保存的值得不到返回。

28、try 里有return,finally還執行么?

(1)、不管有木有出現異常,finally塊中代碼都會執行;

(2)、當try和catch中有return時,finally仍然會執行;

(3)、在try語句中,try要把返回的結果放置到不同的局部變量當中,執行finaly之后,從中取出返回結果,因此,即使finaly中對變量進行了改變,但是不會影響返回結果,因為使用棧保存返回值,即使在finaly當中進行數值操作,但是影響不到之前保存下來的具體的值,所以return影響不了基本類型的值,這里使用的棧保存返回值。而如果修改list,map,自定義類等引用類型時,在進入了finaly之前保存了引用的地址, 所以在finaly中引用地址指向的內容改變了,影響了返回值。

總結:

    1.影響返回結果的前提是在非 finally 語句塊中有 return 且非基本類型

    2.不影響返回結果的前提是 非 finally 塊中有return 且為基本類型

究其本質 基本類型在棧中存儲,返回的是真實的值,而引用類型返回的是其淺拷貝堆地址,所以才會改變。

    return的若是對象,則先把對象的副本保存起來,也就是說保存的是指向對象的地址。若對原來的對象進行修改,對象的地址仍然不變,return的副本仍然是指向這個對象,所以finally中對對象的修改仍然有作用。而基本數據類型保存的是原原本本的數據,return保存副本后,在finally中修改都是修改原來的數據。副本中的數據還是不變,所以finally中修改對return無影響。

(4)、finally中最好不要包含return,否則程序會提前退出,返回值不是try或catch中保存的返回值。

29、String、StringBuffer 與 StringBuilder的區別

    String 類型和StringBuffer的主要性能區別:String是不可變的對象,因此在每次對String 類型進行改變的時候,都會生成一個新的String 對象,然后將指針指向新的String 對象,所以經常改變內容的字符串最好不要用 String ,因為每次生成對象都會對系統性能產生影響,特別當內存中無引用對象多了以后, JVM 的 GC 就會開始工作,性能就會降低。

    使用 StringBuffer 類時,每次都會對 StringBuffer 對象本身進行操作,而不是生成新的對象并改變對象引用。所以多數情況下推薦使用 StringBuffer ,特別是字符串對象經常改變的情況下。

    StringBuffer對方法加了同步鎖或者對調用的方法加了同步鎖,所以是線程安全的。StringBuilder并沒有對方法進行加同步鎖,所以是非線程安全的。

    StringBuilder與StringBuffer有公共父類AbstractStringBuilder(抽象類)。


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

30、不可變對象

     如果一個對象,在它創建完成之后,不能再改變它的狀態,那么這個對象就是不可變的。不能改變狀態的意思是,不能改變對象內的成員變量,包括基本數據類型的值不能改變,引用類型的變量不能指向其他的對象,引用類型指向的對象的狀態也不能改變。 如何創建不可變類?

(1)將類聲明為final,所以它不能被繼承。

(2)將所有的成員聲明為私有的,這樣就不允許直接訪問這些成員。

(3)對變量不要提供setter方法。

(4)將所有可變的成員聲明為final,這樣只能對它們賦值一次。

(5)通過構造器初始化所有成員,進行深拷貝(deep copy):如果某一個類成員不是原始變量(primitive)或者不可變類,必須通過在成員初始化(in)或者get方法(out)時通過深度clone方法,來確保類的不可變。

(6)在getter方法中,不要直接返回對象本身,而是克隆對象,并返回對象的拷貝。

31、為什么String 要設計成不可變的?

在Java中將String設計成不可變的是綜合考慮到各種因素的結果。如內存,同步,數據結構以及安全等方面的考慮。

(1)字符串常量池的需要。字符串池的實現可以在運行時節約很多heap空間,因為不同的字符串變量都指向池中的同一個字符串。但如果字符串是可變的,那么String interning將不能實現(譯者注:String interning是指對不同的字符串僅僅只保存一個,即不會保存多個相同的字符串。),因為這樣的話,如果變量改變了它的值,那么其它指向這個值的變量的值也會一起改變。

(2)線程安全考慮。 同一個字符串實例可以被多個線程共享。這樣便不用因為線程安全問題而使用同步。字符串自己便是線程安全的。

(3)類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。譬如你想加載java.sql.Connection類,而這個值被改成了myhacked.Connection,那么會對你的數據庫造成不可知的破壞。

(4)支持hash映射和緩存。 因為字符串是不可變的,所以在它創建的時候hashcode就被緩存了,不需要重新計算。這就使得字符串很適合作為Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字符串。

32、內部類可以引用它的包含類(外部類)的成員嗎?有沒有什么限制?

答:一個內部類對象可以訪問創建它的外部類對象的成員,包括私有成員。

33、靜態嵌套類(Static Nested Class)和內部類(Inner Class)的不同?

Static Nested Class是被聲明為靜態(static)的內部類,它可以不依賴于外部類實例被實例化。而通常的內部類需要在外部類實例化后才能實例化。

看下面的代碼哪些地方會產生編譯錯誤?

package com.demo;

public class Outer {

    class Inner {}
     
    public static void foo() { new Inner(); }
 
    public void bar() { new Inner(); }
 
    public static void main(String[] args) {
        new Inner();
    }

}

注意:Java中非靜態內部類對象的創建要依賴其外部類對象,上面的面試題中foo和main方法都是靜態方法,靜態方法中沒有this,也就是說沒有所謂的外部類對象,因此無法創建內部類對象,如果要在靜態方法中創建內部類對象,可以這樣做:

package com.demo;

public class Outer {

    class Inner {}
     
    public static void foo() { new Outer().new Inner(); }
 
    public void bar() { new Inner(); }
 
    public static void main(String[] args) {
        new Outer().new Inner();
    }

}

34、錯誤和異常的區別(Error vs Exception)

java.lang.Error:Throwable 的子類,用于標記嚴重錯誤,表示系統級的錯誤和程序不必處理的異常。合理的應用程序不應該去 try/catch 這種錯誤。是恢復不是不可能但很困難的情況下的一種嚴重問題;比如內存溢出,不可能指望程序能處理這樣的情況; java.lang.Exception:Throwable 的子類,表示需要捕捉或者需要程序進行處理的異常,是一種設計或實現問題;也就是說,它表示如果程序運行正常,從不會發生的情況。并且鼓勵用戶程序去 catch 它。

35、IO 和 NIO的主要區別

(1)面向流與面向緩沖。Java IO和NIO之間第一個最大的區別是,IO是面向流的,NIO是面向緩沖區的。Java IO面向流意味著每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前后移動流中的數據。如果需要前后移動從流中讀取的數據,需要先將它緩存到一個緩沖區。 Java NIO的緩沖導向方法略有不同。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動。這就增加了處理過程中的靈活性。

(2)阻塞與非阻塞IO。Java IO的各種流是阻塞的。這意味著,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。 Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,該線程可以繼續做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。線程通常將非阻塞IO的空閑時間用于在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。

(3)選擇器(Selectors)。Java NIO的選擇器允許一個單獨的線程來監視多個輸入通道,你可以注冊多個通道使用一個選擇器,然后使用一個單獨的線程來“選擇”通道:這些通道里已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。

二、Java集合

36、ArrayList、LinkedList、Vector的區別

     ArrayList,Vector底層是由數組實現,LinkedList底層是由雙向鏈表實現,從底層的實現可以得出它們的性能問題,ArrayList,Vector插入速度相對較慢,查詢速度相對較快,而LinkedList插入速度較快,而查詢速度較慢。再者由于Vevtor使用了線程安全鎖,所以ArrayList的運行效率高于Vector。

37、Map、Set、List、Queue、Stack的特點與用法

Collection            接口的接口   對象的集合

├ List                  子接口      按進入先后有序保存   可重復

│├ LinkedList      接口實現類   鏈表   插入刪除   沒有同步   線程不安全

│├ ArrayList        接口實現類   數組   隨機訪問   沒有同步   線程不安全

│└ Vector            接口實現類   數組     同步    線程安全

│   └ Stack

 

└ Set               子接口     僅接收一次,并做內部排序

  ├ HashSet

  │   └ LinkedHashSet

  └ TreeSet

    對于List ,關心的是順序,它保證維護元素特定的順序(允許有相同元素),使用此接口能夠精確的控制每個元素插入的位置。用戶能夠使用索引(元素在 List 中的位置,類似于數組下標)來訪問 List 中的元素。

    對于 Set ,只關心某元素是否屬于 Set (不允許有相同元素 ),而不關心它的順序。

Map                    接口      鍵值對的集合

├ Hashtable       接口實現類       同步           線程安全

├ HashMap         接口實現類      沒有同步    線程不安全

│├ LinkedHashMap

│└ WeakHashMap

├ TreeMap

└ IdentifyHashMap

     對于 Map ,最大的特點是鍵值映射,且為一一映射,鍵不能重復,值可以,所以是用鍵來索引值。方法 put(Object key, Object value) 添加一個“值” ( 想要得東西 ) 和與“值”相關聯的“鍵” (key) ( 使用它來查找 ) 。方法 get(Object key) 返回與給定“鍵”相關聯的“值”。

    Map 同樣對每個元素保存一份,但這是基于 " 鍵 " 的, Map 也有內置的排序,因而不關心元素添加的順序。如果添加元素的順序對你很重要,應該使用 LinkedHashSet 或者 LinkedHashMap.

    對于效率, Map 由于采用了哈希散列,查找元素時明顯比 ArrayList 快。

更為精煉的總結:

    Collection 是對象集合, Collection 有兩個子接口 List 和 Set。List 可以通過下標 (1,2..) 來取得值,值可以重復。而 Set 只能通過游標來取值,并且值是不能重復的。ArrayList , Vector , LinkedList 是 List 的實現類。ArrayList 是線程不安全的, Vector 是線程安全的,這兩個類底層都是由數組實現的。LinkedList 是線程不安全的,底層是由鏈表實現的。  

    Map 是鍵值對集合。HashTable 和 HashMap 是 Map 的實現類。HashTable 是線程安全的,不能存儲 null 值。HashMap 不是線程安全的,可以存儲 null 值。 

    Stack類:繼承自Vector,實現一個后進先出的棧。提供了幾個基本方法,push、pop、peak、empty、search等。

    Queue接口:提供了幾個基本方法,offer、poll、peek等。已知實現類有LinkedList、PriorityQueue等。

38、HashMap 和 HashTable的區別

    HashMap和Hashtable都實現了Map接口,但決定用哪一個之前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。

    HashMap幾乎可以等價于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受為null的鍵值(key)和值(value),而Hashtable則不行)。

    HashMap是非synchronized,而Hashtable是synchronized,這意味著Hashtable是線程安全的,多個線程可以共享一個Hashtable;而如果沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。

    另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它線程改變了HashMap的結構(增加或者移除元素),將會拋出ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這并不是一個一定發生的行為,要看JVM。這條同樣也是Enumeration和Iterator的區別。

    由于Hashtable是線程安全的也是synchronized,所以在單線程環境下它比HashMap要慢。如果你不需要同步,只需要單一線程,那么使用HashMap性能要好過Hashtable。

    HashMap不能保證隨著時間的推移Map中的元素次序是不變的。

39、HashMap的工作原理

1)存儲:

當程序試圖將多個 key-value 放入 HashMap 中時,以如下代碼片段為例:

HashMap<String , Double> map = new HashMap<String , Double>();
map.put("語文" , 80.0);
map.put("數學" , 89.0);
map.put("英語" , 78.2); 

    HashMap 采用一種所謂的“Hash 算法”來決定每個元素的存儲位置。

    當程序執行 map.put("語文" , 80.0); 時,系統將調用"語文"的 hashCode() 方法得到其 hashCode 值——每個 Java 對象都有 hashCode() 方法,都可通過該方法獲得它的 hashCode 值。得到這個對象的 hashCode 值之后,系統會根據該 hashCode 值來決定該元素的存儲位置。我們可以看 HashMap 類的 put(K key , V value) 方法的源代碼:

public V put(K key, V value) {
    // HashMap允許存放null鍵和null值。
    // 當key為null時,調用putForNullKey方法,將value放置在數組第一個位置。
    if (key == null)
        return putForNullKey(value);
    // 根據key的keyCode重新計算hash值。
    int hash = hash(key.hashCode());
    // 搜索指定hash值在對應table中的索引。
    int i = indexFor(hash, table.length);
    // 如果 i 索引處的 Entry 不為 null,通過循環不斷遍歷 e 元素的下一個元素。
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    // 如果i索引處的Entry為null,表明此處還沒有Entry。
    modCount++;
    // 將key、value添加到i索引處。
    addEntry(hash, key, value, i);
    return null;
}

    從上面的源代碼中可以看出:當我們往HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那么在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。

   上面程序中用到了一個重要的內部接口:Map.Entry,每個 Map.Entry 其實就是一個 key-value 對。從上面程序中可以看出:當系統決定存儲 HashMap 中的 key-value 對時,完全沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算并決定每個 Entry 的存儲位置。我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置之后,value 隨之保存在那里即可。

   上面方法提供了一個根據 hashCode() 返回值來計算 Hash 碼的方法:hash(),這個方法是一個純粹的數學計算,其方法如下:

static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

    對于任意給定的對象,只要它的 hashCode() 返回值相同,那么程序調用 hash(int h) 方法所計算得到的 Hash 碼值總是相同的。接下來程序會調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪個索引處。indexFor(int h, int length) 方法的代碼如下:

static int indexFor(int h, int length) {
    return h & (length-1);
}

    這個方法非常巧妙,它總是通過 h &(table.length -1) 來得到該對象的保存位置——而 HashMap 底層數組的長度總是 2 的 n 次方,這一點可參看后面關于 HashMap 構造器的介紹。

    當 length 總是 2 的倍數時,h & (length-1) 將是一個非常巧妙的設計:假設 h=5,length=16, 那么 h & length - 1 將得到 5;如果 h=6,length=16, 那么 h & length - 1 將得到 6 ……如果 h=15,length=16, 那么 h & length - 1 將得到 15;但是當 h=16 ,length=16 時,那么 h & length - 1 將得到 0 了;當 h=17 ,length=16 時,那么 h & length - 1 將得到 1 了……這樣保證計算得到的索引值總是位于 table 數組的索引之內。

    根據上面 put 方法的源代碼可以看出,當程序試圖將一個 key-value 對放入 HashMap 中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但 key 不會覆蓋。如果這兩個 Entry 的 key 通過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位于 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。

    當向 HashMap 中添加 key-value 對,由其 key 的 hashCode() 返回值決定該 key-value 對(就是 Entry 對象)的存儲位置。當兩個 Entry 對象的 key 的 hashCode() 返回值相同時,將由 key 通過 eqauls() 比較值決定是采用覆蓋行為(返回 true),還是產生 Entry 鏈(返回 false)。

    上面程序中還調用了 addEntry(hash, key, value, i); 代碼,其中 addEntry 是 HashMap 提供的一個包訪問權限的方法,該方法僅用于添加一個 key-value 對。下面是該方法的代碼:

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 獲取指定 bucketIndex 索引處的 Entry 
    Entry<K,V> e = table[bucketIndex];  //// 將新創建的 Entry 放入 bucketIndex 索引處,并讓新的 Entry 指向原來的 Entry
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    // 如果 Map 中的 key-value 對的數量超過了極限
    if (size++ >= threshold)
    // 把 table 對象的長度擴充到原來的2倍。
        resize(2 * table.length);   //
}

    上面方法的代碼很簡單,但其中包含了一個非常優雅的設計:系統總是將新添加的 Entry 對象放入 table 數組的 bucketIndex 索引處——如果 bucketIndex 索引處已經有了一個 Entry 對象,那新添加的 Entry 對象指向原有的 Entry 對象(產生一個 Entry 鏈),如果 bucketIndex 索引處沒有 Entry 對象,也就是上面程序①號代碼的 e 變量是 null,也就是新放入的 Entry 對象指向 null,也就是沒有產生 Entry 鏈。

2)讀取:

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
        e != null;
        e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}

    有了上面存儲時的hash算法作為基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然后通過key的equals方法在對應位置的鏈表中找到需要的元素。

3) 歸納起來簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層采用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。

40、List、Set、Map是否繼承自Collection接口?

答:List、Set 是,Map 不是。Map是鍵值對映射容器,與List和Set有明顯的區別,而Set存儲的零散的元素且不允許有重復元素(數學中的集合也是如此),List是線性結構的容器,適用于按數值索引訪問元素的情形。

41、Collection和Collections的區別?

答:Collection是一個接口,它是Set、List等容器的父接口;Collections是一個工具類,提供了一系列的靜態方法來輔助容器操作,這些方法包括對容器的搜索、排序、線程安全化等等。

42、List、Map、Set三個接口存取元素時,各有什么特點?

答:List以特定索引來存取元素,可以有重復元素。Set不能存放重復元素(用對象的equals()方法來區分元素是否重復)。Map保存鍵值對(key-value pair)映射,映射關系可以是一對一或多對一。Set和Map容器都有基于哈希存儲和排序樹的兩種實現版本,基于哈希存儲的版本理論存取時間復雜度為O(1),而基于排序樹版本的實現在插入或刪除元素時會按照元素或元素的鍵(key)構成排序樹從而達到排序和去重的效果。

43、HashMap、HashTable、LinkedHashMap、TreeMap的區別

   Map主要用于存儲鍵值對,根據鍵得到值,因此不允許鍵重復(重復了覆蓋了),但允許值重復。

   Hashmap 是一個最常用的Map,它根據鍵的HashCode 值存儲數據,根據鍵可以直接獲取它的值,具有很快的訪問速度,遍歷時,取得數據的順序是完全隨機的。HashMap最多只允許一條記錄的鍵為Null,允許多條記錄的值為 Null。HashMap不支持線程的同步,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。

   Hashtable與 HashMap類似,它繼承自Dictionary類,不同的是:它不允許記錄的鍵或者值為空,它支持線程的同步,即任一時刻只有一個線程能寫Hashtable,因此也導致了 Hashtable在寫入時會比較慢。

   LinkedHashMap保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的。也可以在構造時用帶參數,按照應用次數排序。在遍歷的時候會比HashMap慢,不過有種情況例外,當HashMap容量很大,實際數據較少時,遍歷起來可能會比LinkedHashMap慢,因為LinkedHashMap的遍歷速度只和實際數據有關,和容量無關,而HashMap的遍歷速度和他的容量有關。

   TreeMap實現SortMap接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator 遍歷TreeMap時,得到的記錄是排過序的。

   一般情況下,我們用的最多的是HashMap,HashMap里面存入的鍵值對在取出的時候是隨機的,它根據鍵的HashCode值存儲數據,根據鍵可以直接獲取它的值,具有很快的訪問速度。在Map 中插入、刪除和定位元素,HashMap 是最好的選擇。TreeMap取出來的是排序后的鍵值對。但如果您要按自然順序或自定義順序遍歷鍵,那么TreeMap會更好。LinkedHashMap 是HashMap的一個子類,如果需要輸出的順序和輸入的相同,那么用LinkedHashMap可以實現,它還可以按讀取順序來排列,像連接池中可以應用。

三、Spring相關

44、IOC(Inversion of Control)的理解

 (1)、IoC(Inversion of Control)是指容器控制程序對象之間的關系,而不是傳統實現中,由程序代碼直接操控。控制權由應用代碼中轉到了外部容器,控制權的轉移是所謂反轉。 對于Spring而言,就是由Spring來控制對象的生命周期和對象之間的關系;IoC還有另外一個名字——“依賴注入(Dependency Injection)”。從名字上理解,所謂依賴注入,即組件之間的依賴關系由容器在運行期決定,即由容器動態地將某種依賴關系注入到組件之中。  

(2)、在Spring的工作方式中,所有的類都會在spring容器中登記,告訴spring這是個什么東西,你需要什么東西,然后spring會在系統運行到適當的時候,把你要的東西主動給你,同時也把你交給其他需要你的東西。所有的類的創建、銷毀都由 spring來控制,也就是說控制對象生存周期的不再是引用它的對象,而是spring。對于某個具體的對象而言,以前是它控制其他對象,現在是所有對象都被spring控制,所以這叫控制反轉。

(3)、在系統運行中,動態的向某個對象提供它所需要的其他對象。  

(4)、依賴注入的思想是通過反射機制實現的,在實例化一個類時,它通過反射調用類中set方法將事先保存在HashMap中的類屬性注入到類中。 總而言之,在傳統的對象創建方式中,通常由調用者來創建被調用者的實例,而在Spring中創建被調用者的工作由Spring來完成,然后注入調用者,即所謂的依賴注入or控制反轉。 注入方式有兩種:依賴注入和設置注入; IoC的優點:降低了組件之間的耦合,降低了業務對象之間替換的復雜性,使之能夠靈活的管理對象。

IOC容器的技術剖析

    IOC中最基本的技術就是“反射(Reflection)”編程,通俗來講就是根據給出的類名(字符串方式)來動態地生成對象,這種編程方式可以讓對象在生成時才被決定到底是哪一種對象。只是在Spring中要生產的對象都在配置文件中給出定義,目的就是提高靈活性和可維護性。

    目前C#、Java和PHP5等語言均支持反射,其中PHP5的技術書籍中,有時候也被翻譯成“映射”。有關反射的概念和用法,大家應該都很清楚。反射的應用是很廣泛的,很多的成熟的框架,比如像Java中的Hibernate、Spring框架,.Net中NHibernate、Spring.NET框架都是把”反射“做為最基本的技術手段。

    反射技術其實很早就出現了,但一直被忽略,沒有被進一步的利用。當時的反射編程方式相對于正常的對象生成方式要慢至少得10倍。現在的反射技術經過改良優化,已經非常成熟,反射方式生成對象和通常對象生成方式,速度已經相差不大了,大約為1-2倍的差距。

    我們可以把IOC容器的工作模式看做是工廠模式的升華,可以把IOC容器看作是一個工廠,這個工廠里要生產的對象都在配置文件中給出定義,然后利用編程語言提供的反射機制,根據配置文件中給出的類名生成相應的對象。從實現來看,IOC是把以前在工廠方法里寫死的對象生成代碼,改變為由配置文件來定義,也就是把工廠和對象生成這兩者獨立分隔開來,目的就是提高靈活性和可維護性。

45、AOP(Aspect Oriented Programming)的理解

(1)、AOP面向方面編程基于IOC,是對OOP的有益補充;

(2)、AOP利用一種稱為“橫切”的技術,剖解開封裝的對象內部,并將那些影響了 多個類的公共行為封裝到一個可重用模塊,并將其名為“Aspect”,即方面。所謂“方面”,簡單地說,就是將那些與業務無關,卻為業務模塊所共同調用的邏輯或責任封裝起來,比如日志記錄,便于減少系統的重復代碼,降低模塊間的耦合度,并有利于未來的可操作性和可維護性。

(3)、AOP代表的是一個橫向的關 系,將“對象”比作一個空心的圓柱體,其中封裝的是對象的屬性和行為;則面向方面編程的方法,就是將這個圓柱體以切面形式剖開,選擇性的提供業務邏輯。而剖開的切面,也就是所謂的“方面”了。然后它又以巧奪天功的妙手將這些剖開的切面復原,不留痕跡,但完成了效果。

(4)、實現AOP的技術,主要分為兩大類:一是采用動態代理技術,利用截取消息的方式,對該消息進行裝飾,以取代原有對象行為的執行;二是采用靜態織入的方式,引入特定的語法創建“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的代碼。

(5)、Spring實現AOP:JDK動態代理和CGLIB代理。JDK動態代理:其代理對象必須是某個接口的實現,它是通過在運行期間創建一個接口的實現類來完成對目標對象的代理;其核心的兩個類是InvocationHandler和Proxy。 CGLIB代理:實現原理類似于JDK動態代理,只是它在運行期間生成的代理對象是針對目標類擴展的子類。CGLIB是高效的代碼生成包,底層是依靠ASM(開源的java字節碼編輯類庫)操作字節碼實現的,性能比JDK強;需要引入包asm.jar和cglib.jar。 使用AspectJ注入式切面和@AspectJ注解驅動的切面實際上底層也是通過動態代理實現的。

(6)、AOP使用場景:                     

Authentication 權限檢查        

Caching 緩存        

Context passing 內容傳遞        

Error handling 錯誤處理        

Lazy loading 延遲加載        

Debugging  調試      

logging, tracing, profiling and monitoring 日志記錄,跟蹤,優化,校準        

Performance optimization 性能優化,效率檢查        

Persistence  持久化        

Resource pooling 資源池        

Synchronization 同步        

Transactions 事務管理    

另外Filter的實現和struts2的攔截器的實現都是AOP思想的體現。  

46、BeanFactroy 與 ApplicationContext 的區別

(1)BeanFactroy采用的是延遲加載形式來注入Bean的,即只有在使用到某個Bean時(調用getBean()),才對該Bean進行加載實例化,這樣,我們就不能發現一些存在的Spring的配置問題。而ApplicationContext則相反,它是在容器啟動時,一次性創建了所有的Bean。這樣,在容器啟動時,我們就可以發現Spring中存在的配置錯誤。 相對于基本的BeanFactory,ApplicationContext 唯一的不足是占用內存空間。當應用程序配置Bean較多時,程序啟動較慢。

BeanFacotry延遲加載,如果Bean的某一個屬性沒有注入,BeanFacotry加載后,直至第一次使用調用getBean方法才會拋出異常;而ApplicationContext則在初始化自身是檢驗,這樣有利于檢查所依賴屬性是否注入;所以通常情況下我們選擇使用 ApplicationContext。
應用上下文則會在上下文啟動后預載入所有的單實例Bean。通過預載入單實例bean,確保當你需要的時候,你就不用等待,因為它們已經創建好了。

(2)BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但兩者之間的區別是:BeanFactory需要手動注冊,而ApplicationContext則是自動注冊。(Applicationcontext比 beanFactory 加入了一些更好使用的功能。而且 beanFactory 的許多功能需要通過編程實現而 Applicationcontext 可以通過配置實現。比如后處理 bean , Applicationcontext 直接配置在配置文件即可而 beanFactory 這要在代碼中顯示的寫出來才可以被容器識別。 )

(3)beanFactory主要是面對與 spring 框架的基礎設施,面對 spring 自己。而 Applicationcontex 主要面對與 spring 使用的開發者。基本都會使用 Applicationcontex 并非 beanFactory 。

47、Bean的作用域

作用域描述
singleton

在每個Spring IoC容器中一個bean定義對應一個對象實例。

(默認)在spring IOC容器中僅存在一個Bean實例,Bean以單實例的方式存在。

prototype

一個bean定義對應多個對象實例。

每次從容器中調用Bean時,都返回一個新的實例,即每次調用getBean()時,相當于執行new XxxBean()的操作。

request

在一次HTTP請求中,一個bean定義對應一個實例;即每次HTTP請求將會有各自的bean實例,它們依據某個bean定義創建而成。該作用域僅在基于web的Spring ApplicationContext情形下有效。

session

在一個HTTP Session中,一個bean定義對應一個實例。該作用域僅在基于web的Spring ApplicationContext情形下有效。

同一個HTTP session共享一個Bean,不同HTTP session使用不同的Bean,該作用域僅適用于webApplicationContext環境。

globalSession

在一個全局的HTTP Session中,一個bean定義對應一個實例。典型情況下,僅在使用portlet context的時候有效。該作用域僅在基于web的Spring ApplicationContext情形下有效。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

三、JVM

44、GC是什么?為什么要有GC?

答:GC是垃圾收集的意思,內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會導致程序或系統的不穩定甚至崩潰,Java提供的GC功能可以自動監測對象是否超過作用域從而達到自動回收內存的目的,Java語言沒有提供釋放已分配內存的顯示操作方法。Java程序員不用擔心內存管理,因為垃圾收集器會自動進行管理。要請求垃圾收集,可以調用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉顯示的垃圾回收調用。

垃圾回收可以有效的防止內存泄露,有效的使用可以使用的內存。垃圾回收器通常是作為一個單獨的低優先級的線程運行,不可預知的情況下對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收,程序員不能實時的調用垃圾回收器對某個對象或所有對象進行垃圾回收。在Java誕生初期,垃圾回收是Java最大的亮點之一,因為服務器端的編程需要有效的防止內存泄露問題,然而時過境遷,如今Java的垃圾回收機制已經成為被詬病的東西。移動智能終端用戶通常覺得iOS的系統比Android系統有更好的用戶體驗,其中一個深層次的原因就在于Android系統中垃圾回收的不可預知性。

45、介紹JVM中7個區域,并把每個區域中可能造成的內存溢出情況進行說明

程序計數器:看做當前線程所執行的字節碼行號指示器。是線程私有的內存,且唯一一塊不報OutOfMemoryError異常。

Java虛擬機棧:用于描述java方法的內存模型:每個方法被執行時都會同時創建一個棧幀用于存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。每一個方法被調用直至執行完成的過程就對應著一個棧幀在虛擬機中從入棧到出棧的過程。如果線程請求的棧深度大于虛擬機所允許的深度就報StackOverflowError,,如果虛擬機棧可以動態擴展,當擴展時無法申請到足夠的內存會拋出OutOfMemoryError。Java虛擬機棧是線程私有的。

本地方法棧:與虛擬機棧相似,不同的在于它是為虛擬機使用到的Native方法服務的。會拋出StackOverflowError和OutOfMemoryError。是線程私有的。 

Java堆:是所有線程共享的一塊內存,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。如果堆上沒有內存完成實例的分配就會報OutOfMemoryError。

方法區(永久代):用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。當方法區無法滿足內存分配需求時,會拋出OutOfMemoryError。是共享內存。

運行時常量池:用于存放編譯器生成的各種字面量和符號引用,是方法區的一部分。無法申請內存時拋出OutOfMemoryError。

直接內存:不是虛擬機運行時數據的一部分,也不是java虛擬機規范中定義的區域,是計算機直接的內存空間。這部分也被頻繁使用,如JAVA NIO的引入基于通道和緩存區的I/O使用native函數直接分配堆外內存。如果內存不足會報OutOfMemoryError。

46、怎樣判斷一個對象是否需要收集?

GC的兩種判定方法:引用計數與可達性分析法。

引用計數:給對象添加一個引用計數器,每當有一個地方引用該對象時,計數器值加1,當引用失效時,計數器值減1。任何時候計數器都為0的對象就是不可能再被使用的。引用計數缺陷:引用計數無法解決循環引用問題:假設對象A,B都已經被實例化,讓A=B,B=A,除此之外這兩個對象再無任何引用,此時計數器的值就永遠不可能為0,但是引用計數器無法通知gc回收他們。

可達性分析法(GC Roots Traceing): 通過一系列名為“GC Roots”的對象作為起點,從這些節點開始向下搜索,搜索走過的路徑成為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象不可用。 GC Roots對象一般是:虛擬機棧中的引用對象,方法區中類靜態屬性引用的對象,方法區常量引用的對象等。

47、Java中的四種引用

強引用:程序代碼中的普通引用。如Object obj = new Object(),只要強引用存在,垃圾回收器就不會回收。

軟引用:描述一些有用但并非必須的對象。對于軟引用關聯的對象在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。

弱引用(SoftRefence:描述非必須對象,比軟引用弱一些。被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

虛引用(WeakRefence:最弱的引用,不管是否有虛引用存在,完全不會對對象生存時間構成影響,也無法通過虛引用來取得一個對象實例。唯一目的是希望能夠在這個對象被垃圾收集器回收之前收到系統通知。

48、對象創建方法,對象的內存分配,對象的訪問定位

Object obj = new Object(); obj 保存在java棧中的局部變量表里,作為一個引用數據出現。New Object()會在java堆上分配一塊存儲Object類型實例的所有數值的結構化內存,根據類型以及虛擬機實現的對象內存布局不同。這塊內存是不固定的。 對象訪問方式有兩種:句柄和直接指針。

句柄:在java堆中會劃分出一塊內存作為句柄池,reference中存儲的對象是句柄地址。而句柄中包含對象實例數據和類型數據各自的具體地址信息。最大的好處是如果對象地址發生變化不需要改變reference的值,只需要改變句柄中實例數據指針。

直接指針訪問:reference直接存儲對象的地址,最大的好處是速度更快

49、Java中堆和棧的區別

Java把內存劃分成兩種:一種是棧內存,一種是堆內存。

      在函數中定義的一些基本類型的變量和對象的引用變量都在函數的棧內存中分配。當在一段代碼塊定義一個變量時,Java就在棧中為這個變量分配內存空間,當超過變量的作用域后,Java會自動釋放掉為該變量所分配的內存空間,該內存空間可以立即被另作它用。

      堆內存用來存放由 new 創建的對象和數組,在堆中分配的內存,由 Java 虛擬機的自動垃圾回收器來管理。在堆中產生了一個數組或者對象之后,還可以在棧中定義一個特殊的變量,讓棧中的這個變量的取值等于數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量,以后就可以在程序中使用棧中的引用變量來訪問堆中的數組或者對象,引用變量就相當于是為數組或者對象起的一個名稱。引用變量是普通的變量,定義時在棧中分配,引用變量在程序運行到其作用域之外后被釋放。而數組和對象本身在堆中分配,即使程序運行到使用 new 產生數組或者對象的語句所在的代碼塊之外,數組和對象本身占據的內存不會被釋放,數組和對象在沒有引用變量指向它的時候,才變為垃圾,不能在被使用,但仍然占據內存空間不放,在隨后的一個不確定的時間被垃圾回收器收走(釋放掉)。 

具體的說:

      棧與堆都是Java用來在Ram中存放數據的地方。與C++不同,Java自動管理棧和堆,程序員不能直接地設置棧或堆。

      Java堆是一個運行時數據區,類的對象從中分配空間。這些對象通過 new 建立,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器,因為它是在運行時動態分配內存的,Java 的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由于要在運行時動態分配內存,存取速度較慢。 java 中的對象和數組都存放在堆中。

      棧的優勢是,存取速度比堆要快,僅次于寄存器,棧數據可以共享。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。棧中主要存放一些基本類型的變量(,int, short, long, byte, float, double, boolean, char)和對象句柄。

棧有一個很重要的特殊性,就是存在棧中的數據可以共享。 假設我們同時定義:

int a = 3; 
int b = 3;

     編譯器先處理 int a = 3 ;首先它會在棧中創建一個變量為 a 的引用,然后查找棧中是否有 3 這個值,如果沒找到,就將 3 存放進來,然后將 a 指向 3 。接著處理 int b = 3 ;在創建完 b 的引用變量后,因為在棧中已經有 3 這個值,便將 b 直接指向 3 。這樣,就出現了 a 與 b 同時均指向 3 的情況。這時,如果再令 a=4 ;那么編譯器會重新搜索棧中是否有 4 值,如果沒有,則將 4 存放進來,并令 a 指向 4 ;如果已經有了,則直接將 a 指向這個地址。因此 a 值的改變不會影響到 b 的值。要注意這種數據的共享與兩個對象的引用同時指向一個對象的這種共享是不同的,因為這種情況 a 的修改并不會影響到 b,它是由編譯器完成的,它有利于節省空間。而一個對象引用變量修改了這個對象的內部狀態,會影響到另一個對象引用變量。

50、內存溢出和內存泄漏

內存溢出:通俗理解就是內存不夠,程序所需要的內存遠遠超出了你虛擬機分配的內存大小,就叫內存溢出。

內存泄漏:內存泄漏也稱作“存儲滲漏”,用動態存儲分配函數動態開辟的空間,在使用完畢后未釋放,結果導致一直占據該內存單元。直到程序結束。(其實說白了就是該內存空間使用完畢之后未回收)即所謂內存泄漏。

51、內存溢出了怎么辦?

     通過內存映像工具如jhat,jconsole等對dump出來的堆轉存儲快照進行分析,重點是確認內存是出現內存泄露還是內存溢出。 如果是內存泄露進一步使用工具查看泄露的對象到GC Roots的引用鏈。于是就能找到泄露對象是通過怎樣的路徑與GC Roots相關聯并導致垃圾收集器無法自動回收它們。掌握泄露對象的信息,以及GC Roots引用鏈的信息,就可以比較準確定位泄露代碼的位置。 如果不存在**內存泄露,那就需要通過jinfo,Jconsole等工具分析java堆參數與機器物理內存對比是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長,持有狀態過長的情況,嘗試減少程序的運行消耗。

52、Java中有內存泄露嗎?

      有,Java中,造成內存泄露的原因有很多種。典型的例子是長生命周期的對象持有短生命周期對象的引用就很可能發生內存泄露,盡管短生命周期對象已經不再需要,但是因為長生命周期對象持有它的引用而導致不能被回收,這就是java中內存泄露的發生場景,通俗地說,就是程序員可能創建了一個對象,以后一直不再使用這個對象,這個對象卻一直被引用,即這個對象無用但是卻無法被垃圾回收器回收的,這就是java中可能出現內存泄露的情況,例如,緩存系統,我們加載了一個對象放在緩存中(例如放在一個全局map對象中),然后一直不再使用它,這個對象一直被緩存引用,但卻不再被使用。檢查java中的內存泄露,一定要讓程序將各種分支情況都完整執行到程序結束,然后看某個對象是否被使用過,如果沒有,則才能判定這個對象屬于內存泄露。(采用什么工具?) 如果一個外部類的實例對象的方法返回了一個內部類的實例對象,這個內部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由于內部類持有外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成內存泄露。

53、什么時候會發生jvm堆內存溢出?

       簡單的來說 java的堆內存分為兩塊:permantspace(持久代) 和 heap space。 持久帶中主要用于存放靜態類型數據,如 Java Class,,Method 等, 與垃圾收集器要收集的Java對象關系不大。 而heap space分為年輕代和年老代 年輕代的垃圾回收叫 Young GC, 年老代的垃圾回收叫 Full GC。 在年輕代中經歷了N次(可配置)垃圾回收后仍然存活的對象,就會被復制到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象 年老代溢出原因有 循環上萬次的字符串處理、創建上千萬個對象、在一段代碼內申請上百M甚至上G的內存 持久代溢出原因動態加載了大量Java類而導致溢出,以及生產大量的常量。 永久代內存泄露:以一個部署到應用程序服務器的Java web程序來說,當該應用程序被卸載的時候,你的EAR/WAR包中的所有類都將變得無用。只要應用程序服務器還活著,JVM將繼續運行,但是一大堆的類定義將不再使用,理應將它們從永久代(PermGen)中移除。如果不移除的話,我們在永久代(PermGen)區域就會有內存泄漏。

54、OOM你遇到過哪些情況?

java.lang.OutOfMemoryError:Java heap space ------>java堆內存溢出,此種情況最常見,一般由于內存泄露或者堆的大小設置不當引起。

java.lang.OutOfMemoryError:PermGen space ------>java永久代溢出,即方法區溢出了,一般出現于大量Class或者jsp頁面,或者采用cglib等反射機制的情況,因為上述情況會產生大量的Class信息存儲于方法區。

java.lang.StackOverflowError ------> 不會拋OOM error,但也是比較常見的Java內存溢出。JAVA虛擬機棧溢出,一般是由于程序中存在死循環或者深度遞歸調用造成的,棧大小設置太小也會出現此種溢出。可以通過虛擬機參數-Xss來設置棧的大小。

55、GC的三種收集方法:標記清除、標記整理、復制算法的原理與特點,分別用在什么地方?

標記清除:首先標記所有需要回收的對象,在標記完成后統一回收掉所有被標記的對象,它的標記的對象。缺點是效率低,且存在內存碎片。主要用于老生代垃圾回收。 

復制算法:將內存按容量劃分為大小相等的一塊,每次只用其中一塊。當內存用完了,將還存活的對象復制到另一塊內存,然后把已使用過的內存空間一次清理掉。實現簡單,高效。一般用于新生代。一般是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間。HotSpot虛擬機默認比例是8:1,。每次使用Eden和一塊Survivor,當回收時將這兩塊內存中還存活的對象復制到Survivor然后清理掉剛才Eden和Survivor的空間。如果復制過程內存不夠使用則向老年代分配擔保。

標記整理:首先標記所有需要回收的對象,在標記完成后讓所有存活的對象都向一端移動,然后直接清理掉端邊界意外的內存。用于老年代。

分代收集算法:根據對象的生存周期將內存劃分為新生代和老年代,根據年代的特點采用最適當的收集算法。

56、Minor GC 與 Full GC 分別在什么時候發生?

FullGC 一般是發生在老年代的GC,出現一個FullGC經常會伴隨至少一次的Minor GC。速度比MinorGC慢10倍以上。FULL GC發生的情況:

1) 老年代空間不足。老年代空間只有在新生代對象轉入及創建為大對象、大數組時才會出現不足的現象,當執行Full GC后空間仍然不足,則拋出如下錯誤:java.lang.OutOfMemoryError: Java heap space 。措施:為避免以上兩種狀況引起的FullGC,調優時應盡量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。

2) Permanet Generation(方法區或永久代)空間滿。PermanetGeneration中存放的為一些class的信息等,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被占滿,在未配置為采用CMS GC的情況下會執行Full GC。如果經過Full GC仍然回收不了,那么JVM會拋出如下錯誤信息: java.lang.OutOfMemoryError: PermGen space。措施:為避免Perm Gen占滿造成Full GC現象,可采用的方法為增大Perm Gen空間或轉為使用CMS GC。

3) CMS GC時出現promotion failed和concurrent mode failure 對于采用CMS進行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。 promotion failed是在進行Minor GC時,survivor space放不下、對象只能放入老年代,而此時老年代也放不下造成的; concurrent mode failure: CMS在執行垃圾回收時需要一部分的內存空間并且此刻用戶程序也在運行需要預留一部分內存給用戶程序,如果預留的內存無法滿足程序需求就出現一次"Concurrent mod failure",并觸發一次Full GC。 應對措施為:增大survivor space、老年代空間或調低觸發并發GC的比率。

4) 統計得到的Minor GC晉升到舊生代的平均大小大于舊生代的剩余空間。 Hotspot為了避免由于新生代對象晉升到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大于舊生代的剩余空間,那么就直接觸發Full GC。如果小于并且不允許擔保失敗也會發生一次Full GC。

MinorGC 指發生在新生代的垃圾收集動作,非常頻繁,回收速度也快。一般發生在新生代空間不足時。另外一個FullGC經常會伴隨至少一次的Minor GC.。當虛擬機檢測晉升到老年代的平均大小是否小于老年代剩余空間大小,如果小于并且允許擔保失敗,則執行Minor GC。

57、類加載的五個過程:加載、驗證、準備、解析、初始化。

加載: 根據全限定名來獲取定義類的二進制字節流,然后將該字節流所代表的靜態結構轉化為方法區的運行時數據結構,最后在java堆上生成一個代表該類的Class對象,作為方法區這些數據的訪問入口。

驗證:主要時為了確保class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。包含四個階段的驗證過程:

1)文件格式驗證:保證輸入的字節流能夠正確地解析并存儲在方法區之內,格式上符合描述一個java類型信息的要求。

2)元數據驗證:字節碼語義信息的驗證,以保證描述的信息符合java語言規范。驗證點有:這個類是否有父類等。

3)字節碼驗證:主要是進行數據流和控制流分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為.。

4)符號引用驗證:對符號引用轉化為直接引用過程的驗證。

準備:為類變量分配內存并設置變量的初始值,這些內存在方法區進行分配。

解析:將虛擬機常量池中的符號引用轉化為直接引用的過程。解析主要是針對類或接口,字段,類方法。

初始化:執行靜態變量的賦值操作以及靜態代碼塊,完成初識化。初始化過程保證了父類中定義的初始化優先于子類的初始化,但接口不需要執行父類的初始化。

58、雙親委派模型

除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。順序依次是:

Bootstrap ClassLoader::啟動類加載器,加載java_home/lib中的類。

Extension ClassLoader:擴展類加載器,加載java_home/lib/ext目錄下的類庫。

Application ClassLoader::應用程序類加載器,加載用戶類路徑上指定類庫。

      雙親委派模型的工作原理是:如果一個類加載器受到了類加載請求,它首先不會自己去嘗試加載這個類,而把這個請求委派給父類加載器去完成,每一層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父類加載器反饋自己無法完成加載請求時,加載器才嘗試自己加載。這種方式保證了Oject類在各個加載器加載環境中都是同一個類。

四、Java多線程

59、進程和線程之間有什么不同?

一個進程是一個獨立(self contained)的運行環境,它可以被看作一個程序或者一個應用。而線程是在進程中執行的一個任務。Java運行環境是一個包含了不同的類和程序的單一進程。線程可以被稱為輕量級進程。線程需要較少的資源來創建和駐留在進程中,并且可以共享進程中的資源。

60、多線程編程的好處是什么?

在多線程程序中,多個線程被并發的執行以提高程序的效率,CPU不會因為某個線程需要等待資源而進入空閑狀態。多個線程共享堆內存(heap memory),因此創建多個線程去執行一些任務會比創建多個進程更好。舉個例子,Servlets比CGI更好,是因為Servlets支持多線程而CGI不支持。

61、多線程有什么用?

     一個可能在很多人看來很扯淡的一個問題:我會用多線程就好了,還管它有什么用?在我看來,這個回答更扯淡。所謂”知其然知其所以然”,”會用”只是”知其然”,”為什么用”才是”知其所以然”,只有達到”知其然知其所以然”的程度才可以說是把一個知識點運用自如。OK,下面說說我對這個問題的看法:

(1)發揮多核CPU的優勢

    隨著工業的進步,現在的筆記本、臺式機乃至商用的應用服務器至少也都是雙核的,4核、8核甚至16核的也都不少見,如果是單線程的程序,那么在雙核CPU上就浪費了50%,在4核CPU上就浪費了75%。單核CPU上所謂的”多線程”那是假的多線程,同一時間處理器只會處理一段邏輯,只不過線程之間切換得比較快,看著像多個線程”同時”運行罷了。多核CPU上的多線程才是真正的多線程,它能讓你的多段邏輯同時工作,多線程,可以真正發揮出多核CPU的優勢來,達到充分利用CPU的目的。

(2)防止阻塞

     從程序運行效率的角度來看,單核CPU不但不會發揮出多線程的優勢,反而會因為在單核CPU上運行多線程導致線程上下文的切換,而降低程序整體的效率。但是單核CPU我們還是要應用多線程,就是為了防止阻塞。試想,如果單核CPU使用單線程,那么只要這個線程阻塞了,比方說遠程讀取某個數據吧,對端遲遲未返回又沒有設置超時時間,那么你的整個程序在數據返回回來之前就停止運行了。多線程可以防止這個問題,多條線程同時運行,哪怕一條線程的代碼執行讀取數據阻塞,也不會影響其它任務的執行。

(3)便于建模

     這是另外一個沒有這么明顯的優點了。假設有一個大的任務A,單線程編程,那么就要考慮很多,建立整個程序模型比較麻煩。但是如果把這個大的任務A分解成幾個小任務,任務B、任務C、任務D,分別建立程序模型,并通過多線程分別運行這幾個任務,那就簡單很多了。

62、創建多線程的方式

比較常見的一個問題了,一般就是兩種:

(1)繼承Thread類

(2)實現Runnable接口

至于哪個好,不用說肯定是后者好,因為實現接口的方式比繼承類的方式更靈活,也能減少程序之間的耦合度,面向接口編程也是設計模式6大原則的核心

63、start()方法和run()方法的區別

只有調用了start()方法,才會表現出多線程的特性,不同線程的run()方法里面的代碼交替執行。如果只是調用run()方法,那么代碼還是同步執行的,必須等待一個線程的run()方法里面的代碼全部執行完畢之后,另外一個線程才可以執行其run()方法里面的代碼。

start()方法來啟動線程,真正實現了多線程運行。這時無需等待run方法體代碼執行完畢,可以直接繼續執行下面的代碼;通過調用Thread類的start()方法來啟動一個線程, 這時此線程是處于就緒狀態, 并沒有運行。 然后通過此Thread類調用方法run()來完成其運行操作的, 這里方法run()稱為線程體,它包含了要執行的這個線程的內容,run方法運行結束, 此線程終止。然后CPU再調度其它線程。

run()方法當作普通方法的方式調用。程序還是要順序執行,要等待run方法體執行完畢后,才可繼續執行下面的代碼; 程序中只有主線程這一個線程, 其程序執行路徑還是只有一條, 這樣就沒有達到寫線程的目的。

記住:多線程就是分時利用CPU,宏觀上讓所有線程一起執行 ,也叫并發。

64、有哪些不同的線程生命周期?

 關于Java中線程的生命周期,首先看一下下面這張較為經典的圖:

Java線程具有五種基本狀態

新建狀態(New):當線程對象對創建后,即進入了新建狀態,如:Thread t = new MyThread()。

就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處于就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,并不是說執行了t.start()此線程立即就會執行。

運行狀態(Running):當CPU開始調度處于就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處于就緒狀態中。

阻塞狀態(Blocked):處于運行狀態中的線程由于某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分為三種:

1.等待阻塞 -- 運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;

2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態;

3.其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。

65、sleep方法和wait方法有什么區別?

(1)sleep()方法是屬于Thread類中的。而wait()方法,則是屬于Object類中的。

(2)最主要是sleep方法沒有釋放鎖,而wait方法釋放了鎖。

(3)wait,notify和notifyAll只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用。

(4)sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常。

 

 

servlet的生命周期

Servlet運行在Servlet容器中,其生命周期由容器來管理。Servlet的生命周期通過javax.servlet.Servlet接口中的init()、service()和destroy()方法來表示。

Servlet的生命周期包含了下面4個階段:

(1)加載和實例化

     Servlet容器負責加載和實例化Servlet。當Servlet容器啟動時,或者在容器檢測到需要這個Servlet來響應第一個請求時,創建Servlet實例。當Servlet容器啟動后,它必須要知道所需的Servlet類在什么位置,Servlet容器可以從本地文件系統、遠程文件系統或者其他的網絡服務中通過類加載器加載Servlet類,成功加載后,容器創建Servlet的實例。因為容器是通過Java的反射API來創建Servlet實例,調用的是Servlet的默認構造方法(即不帶參數的構造方法),所以我們在編寫Servlet類的時候,不應該提供帶參數的構造方法。

(2)初始化

      在Servlet實例化之后,容器將調用Servlet的init()方法初始化這個對象。初始化的目的是為了讓Servlet對象在處理客戶端請求前完成一些初始化的工作,如建立數據庫的連接,獲取配置信息等。對于每一個Servlet實例,init()方法只被調用一次。在初始化期間,Servlet實例可以使用容器為它準備的ServletConfig對象從Web應用程序的配置信息(在web.xml中配置)中獲取初始化的參數信息。在初始化期間,如果發生錯誤,Servlet實例可以拋出ServletException異常或者UnavailableException異常來通知容器。ServletException異常用于指明一般的初始化失敗,例如沒有找到初始化參數;而UnavailableException異常用于通知容器該Servlet實例不可用。例如,數據庫服務器沒有啟動,數據庫連接無法建立,Servlet就可以拋出UnavailableException異常向容器指出它暫時或永久不可用。

(3)請求處理

      Servlet容器調用Servlet的service()方法對請求進行處理。service()方法為Servlet的核心方法,客戶端的業務邏輯應該在該方法內執行,典型的服務方法的開發流程為:解析客戶端請求-〉執行業務邏輯-〉輸出響應頁面到客戶端。要注意的是,在service()方法調用之前,init()方法必須成功執行。在service()方法中,Servlet實例通過ServletRequest對象得到客戶端的相關信息和請求信息,在對請求進行處理后,調用ServletResponse對象的方法設置響應信息。在service()方法執行期間,如果發生錯誤,Servlet實例可以拋出ServletException異常或者UnavailableException異常。如果UnavailableException異常指示了該實例永久不可用,Servlet容器將調用實例的destroy()方法,釋放該實例。此后對該實例的任何請求,都將收到容器發送的HTTP 404(請求的資源不可用)響應。如果UnavailableException異常指示了該實例暫時不可用,那么在暫時不可用的時間段內,對該實例的任何請求,都將收到容器發送的HTTP 503(服務器暫時忙,不能處理請求)響應。

(4)服務終止

     當容器檢測到一個Servlet實例應該從服務中被移除的時候,容器就會調用實例的destroy()方法,以便讓該實例可以釋放它所使用的資源,保存數據到持久存儲設備中。當需要釋放內存或者容器關閉時,容器就會調用Servlet實例的destroy()方法。在destroy()方法調用之后,容器會釋放這個Servlet實例,該實例隨后會被Java的垃圾收集器所回收。如果再次需要這個Servlet處理請求,Servlet容器會創建一個新的Servlet實例。

      在整個Servlet的生命周期過程中,創建Servlet實例、調用實例的init()和destroy()方法都只進行一次,當初始化完成后,Servlet容器會將該實例保存在內存中,通過調用它的service()方法,為接收到的請求服務。

      總結:web容器加載servlet,生命周期開始。通過調用servlet的init() 方法進行servlet的初始化。通過調用service() 方法實現,根據請求的不同調用不同的do***() 方法。結束服務,web容器調用servlet 的 destory() 方法。

JSP和Servlet有哪些相同點和不同點,他們之間的聯系是什么?

      JSP是Servlet技術的擴展,本質上是Servlet的簡易方式,更強調應用的外表表達。JSP 編譯后是"類servlet"。Servlet和JSP最主要的不同點在于,Servlet的應用邏輯是在Java文件中,并且完全從表示層中的HTML 里分離開來。而JSP的情況是Java和HTML可以組合成一個擴展名為.jsp的文件。JSP側重于視圖,Servlet主要用于控制邏輯。

 

3、Hashcode的作用,與 equals 有什么區別

      同樣用于鑒定2個對象是否相等的,java集合中有 list 和 set 兩類,其中 set不允許元素重復出現,那么這個不允許重復出現的方法,如果用 equals 去比較的話,如果存在1000個元素,你 new 一個新的元素出來,需要去調用1000次 equals 去逐個和他們比較是否是同一個對象,這樣會大大降低效率。hashcode實際上是返回對象的存儲地址,如果這個位置上沒有元素,就把元素直接存儲在上面,如果這個位置上已經存在元素,這個時候才去調用equals方法與新元素進行比較,相同的話就不存了,散列到其他地址上。

4、String、StringBuffer與StringBuilder的區別

String 類型和 StringBuffer 類型的主要性能區別其實在于 String 是不可變的對象。

StringBuffer和StringBuilder底層是 char[]數組實現的。

StringBuffer是線程安全的,而StringBuilder是線程不安全的。


文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()