1. 程式人生 > 其它 >C語言小題庫裡有趣的事情

C語言小題庫裡有趣的事情

一、java基礎

1、資料型別

  • 基本資料型別
    int :4位元組
    float :4位元組
    double :8位元組
    char :2位元組
    long :8位元組
    byte :1位元組
    boolen:被編譯成 int 型別來使用,佔 4 個 byte
    short:2位元組
  • 引用資料型別
    字串
    陣列

    介面
    lambda

注意事項:

  1. 字串是引用資料型別;
  2. 浮點數可能只是一個近似值,並非精確,所以兩個相同的浮點數使用==可能為false;
  3. 浮點數預設為double型別,如果使用float需要加字尾F;整數預設為int型別,如果使用long需要加字尾L;
  4. 基本資料型別都有包裝型別,基本型別與包裝型別之間的賦值使用自動裝箱和拆箱完成。

2、資料型別轉換

隱式型別轉換(小範圍轉大範圍)

  • char/byte/short 型別在計算時會自動轉換為int型別
  • +=,-=,*=,/=運算子可以執行隱式型別轉換
  • String + int = String
	short s1 = 1;
	// s1 = s1 +1; 報錯,因為=右邊已經是int型別了
	s1+=1; //隱式型別轉換
	s1 = (short) (s1 + 1); //強制型別轉換,編譯通過

強制型別轉換(大範圍轉小範圍)

  • 小範圍型別=(小範圍型別)原本型別

3、緩衝池

編譯器自動裝箱過程中,會優先從緩衝池中尋找物件,如果緩衝池中沒有再建立新的物件。

基本資料型別對應的緩衝池:

  1. boolean : true、false
  2. byte :所有值緩衝池都有
  3. short :-128 ~ +127
  4. int :-128 ~ +127
  5. char: \u0000 ~ \u007F

注意事項:Integer.valueOf(123)函式也會先在緩衝池中尋找

Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true

4、String型別

特點:
1.被宣告為final,因此不可繼承
2.內部使用char陣列儲存,該陣列被宣告為final,意味著 value 陣列初始化之後就不能再引用其它陣列,因此可以保證 String 不可變。

不可變的好處:
1.可以快取hash值,因為 String 的 hash 值經常被使用,例如 String 用做 HashMap 的 key。不可變的特性可以使得 hash 值也不可變,因此只需要進行一次計算。
2.String Pool 的需要,如果一個String物件被建立過了,就會從字串常量池中取得。
3.安全性,String 經常作為引數,String 不可變性可以保證引數不可變。
4.執行緒安全,可以在多個執行緒中安全使用。

String string1 = "hello"
String string2 = "hello"
System.out.print(string1==string2);//輸出為true,即為同一個引用物件

String string1 = new String("hello");
String string2 = new String("hello");
System.out.print(string1==string2);//輸出為false,不同物件

String, StringBuffer and StringBuilder的區別
1.String不可變,StringBuffer and StringBuilder可變
2.String和StringBuffer執行緒安全,StringBuilder執行緒不安全

String常用方法:

boolean equals( String str );//字串內容一樣則返回true,和常量比較時推薦 “abc”.equals(string); “ == ”比較的是地址
public int length( String str );//返回長度
public String concat(String str1,String str2);//返回字串拼接,原字串不變
public char charAt( int index );//返回字串某個字元
public int indexOf( String str);//返回某個字串在本字串中出現的位置,沒有則返回-1
public String substring(int index);//返回擷取引數到結尾的字串
public String substring(int begin, int end );//返回[ begin, end )之間的字串

//與轉換有關的方法:
public char[ ] toCharArray( );//轉化為字元陣列
public byte[ ] getBytes( );//獲取底層位元組陣列
public String replace( charSequence str1, charSequence str2 );//字串的替換

//分割字串的方法:
public String[ ] split( String regex );//返回分割後的字串陣列,注意:引數其實是一個正則表示式,如果按“.”分割,則需在引數中寫(“\\.”)

5、陣列

5.1 初始化

靜態初始化:

int[] array2 = new int [ ]{"ha","lou"};
int[] array3 = {"ha","lou"}; //(不能拆成兩個步驟)

動態初始化: 初始化時,存資料型別的預設值

int[] array1 = new array [5]; 

注意: 直接列印陣列記憶體名,會得到對應的記憶體地址雜湊值

5.2 陣列作為引數傳遞

  • 陣列作為返回值,返回的是陣列地址。
  • 陣列作為方法引數,傳入的也是陣列地址。事實上,java中所有引數傳遞的都是值傳遞
public static int[ ] arrayName ( int [ ] array1 ) { return array1 ;}

5.3 多維陣列

以二維陣列為例:
建立一個二維陣列:

int[][] array = new int[3][4];

本質是建立了一個大小為3的一維陣列,該陣列的每一個元素都是一個大小為4的一維陣列

line = array.length;//返回二維陣列行數
colum = array[0].length;//返回二維陣列的列數

6、java記憶體劃分

1.棧(stack):存放方法中的引數,即區域性變數,超出作用域則消失;方法的執行一定在棧中;
2.堆(heap):凡是new出來的東西都存放在堆當中;堆記憶體中的東西都有都有預設值:其中布林型別預設是false,字元型別預設是' \u0000 ',int型別預設為0
3.方法區(method area):儲存.class相關資訊,包含方法的資訊。
4.本地方法棧(native method stack):與作業系統有關。
5.暫存器(pc register):與CPU有關。

7、程式在記憶體中的存放位置及關係

示例1:簡單的陣列物件訪問

public class Main{
	public static void main(String[] args){
		int[] array = new int[3]; //動態初始化
		System.out.println(array); //地址值
		System.out.println(array[0]); //0
		System.out.println(array[1]); //0
		System.out.println(array[2]); //0
		
		System.out.println("=========================")
		array[1]=10;
		array[2]=20;
		System.out.println(array); //地址值
		System.out.println(array[0]); //0
		System.out.println(array[1]); //10
		System.out.println(array[2]); //20A
	}
}

示例2:陣列物件引用

public class Main{
	public static void main(String[] args){
		int[] arrayA = new int[3]; //動態初始化
		arrayA[1]=10;
		arrayA[2]=20;
		int[] arrayB = arrayA;
		System.out.println(arrayB); //地址值
		System.out.println(arrayB[0]); //0
		System.out.println(arrayB[1]); //10
		System.out.println(arrayB[2]); //20

		arrayB[1] = 100;
		arrayB[2] = 200;
		System.out.println(arrayB[0]); //0
		System.out.println(arrayB[1]); //10
		System.out.println(arrayB[2]); //20
	}
}

示例3:建立物件

public class Phone{
	String brand;
	double price;
	public void call(String who){
		System.out.println("call "+who);
	}
	public void sendMessage(){
		System.out.println("傳送短息 ");
	}
}

public class Main{
	public static void main(String[] args){
		Phone one = new Phone();
		System.out.println(one.brand);
		System.out.println(one.price);
		one.brand = "蘋果";
		one.price = 12000;
		System.out.println(one.brand);
		System.out.println(one.price);
		one.call("喬布斯");
		one.message();
	}
}


例4:物件作為引數傳遞

public class Main{
	public static void main(String[] args){
		Phone one = new Phone();
		one.brand = "蘋果";
		one.price = 12000;

		method(one);
	}
	public static void method(Phone phone){
		System.out.println(one.brand);
		System.out.println(one.price);
	}
}

8、java的三大特性

8.1 封裝

含義: 利用抽象資料型別將資料和操作封裝在一起,使其構成一個不可分割的獨立實體。儘可能地隱藏內部的細節,只保留一些對外介面使之與外部發生聯絡。

優點:

  1. 減少耦合:各部分能夠獨立開發
  2. 減輕維護負擔
  3. 有效調節效能
  4. 提高可重用性
  5. 降低了構建大型系統的風險:即使整個系統不可用,但是這些獨立的模組卻有可能是可用的

8.2 繼承

含義: 子類得到父類的一些方法和屬性。

注意:

  1. 繼承應該遵循里氏替換原則,子類物件必須能夠替換掉所有父類物件。
  2. java為單繼承,即子類只能繼承一個父類
  3. 父類引用指向子類物件稱為向上轉型

訪問許可權:
Java 中有三個訪問許可權修飾符: private、protected 以及 public,如果不加訪問修飾符,表示預設default。許可權大小:public>protect>default>private
public:公共許可權,任何類或方法呼叫該類都可以使用
protect:保護許可權,只有該類和該類的子類可以訪問
default(預設不寫):預設許可權,同一個包裡的類可以訪問
private:私有許可權,只能該類訪問

繼承中構造方法的訪問特點:

  • 子類構造方法中預設有一個super()呼叫,所以一定是先呼叫父類的構造,再呼叫子類的構造。注:如果父類沒有無參構造方法會報錯。
  • 子類必須呼叫父類的構造方法,不寫則預設新增super();寫了,則用指定的super()呼叫
  • 在本類的構造方法中,訪問本類的另一個構造方法,this()也必須是構造方法的第一個語句,且唯一,且不能和super()同時使用

繼承中成員變數的訪問特點:
在父子類的繼承關係中,如果成員變數重名,則建立子類物件時,訪問方法有兩種方式——

  1. 直接通過子類物件訪問成員變數:等號左邊是誰,就優先使用誰,沒有則向上找
  2. 間接通過成員方法訪問訪問成員變數:該方法屬於誰就優先使用誰,沒有則向上找

8.3 多型

多型分為編譯時多型執行時多型

  • 編譯時多型主要指方法的過載
  • 執行時多型:父類引用指向的子類物件的具體型別在執行期間才確定

多型的使用

父類名稱 物件名 = new 父類名稱();
介面名稱 物件名 = new 實現類名稱();

訪問成員變數的兩種方式:
1、 直接通過物件名稱訪問成員變數,等號左邊是誰,優先使用誰,沒有則向上查詢
2、通過成員方法訪問成員變數,看方法屬於誰,優先使用誰,沒有則向上查詢

成員方法的訪問規則:
1、 看new的誰,就優先使用誰,沒有則向上查詢(因為new的物件方法同名方法會進行覆蓋)

物件的上下轉型

instanceof關鍵字
通過instanceof關鍵字,可以知道父類引用的物件本來是什麼子類
格式: 物件 instanceof 類名 ,將會得到一個boolean型別值結果,判斷前面的物件能不能當後面型別的例項。

9、重寫和過載

9.1 重寫(Override)

存在於繼承體系中,指子類實現了一個與父類在方法宣告上完全相同的一個方法,以用@override檢測覆蓋是否成功,重寫有以下限制:

  1. 必須保證父子類之間的方法名稱相同,引數列表相同
  2. 子類方法的許可權必須【大於等於】父類方法的許可權修飾符,私有方法和靜態方法無法覆蓋重寫
  3. 子類方法的返回型別必須是父類方法返回型別或為其子型別

9.2 過載(Overload)

存在於同一個類中,指一個方法與已經存在的方法名稱上相同,但是引數型別、個數、順序至少有一個不同【方法名相同,引數列表不同】
注意: 與返回值沒有關係,返回值不同,其它都相同不算是過載。

10、區域性變數和成員變數區別

  1. 定義位置不一樣:成員變數在類當中,區域性變數在方法內
  2. 作用範圍不一樣:成員變數在整個類中通用,區域性變數只能在方法內使用
  3. 預設值不一樣:成員變數有預設值,區域性變數沒有必須建立時宣告
  4. 記憶體位置不一樣:成員變數在堆中,區域性變數在棧中
  5. 生命週期不一樣:成員變數隨著物件的建立而誕生,隨著物件被收回而消失,區域性變數隨著方法進棧而誕生,隨著方法出棧而消失。

11、常用類

11.1 Scanner類

import java.util.Scanner;	//導包

scanner sc = new Scanner(System.in);		//建立Scanner類
sc.next();		//鍵入字串
sc.nextInt();	
sc.nextDouble();		//鍵入數字

11.2 Random類

import java.util.Random;		//導包

Random r = new Random( );		//建立
int num = r.nextInt( );		//使用
int num = r.nextInt(int n );		//產生[0, n)的隨機數

11.3 Array陣列類

import java.util.Array;

public static String toString( 陣列 );//將陣列裝化為字串
public static void sort( 陣列 );//將陣列預設升序排序,如果是自定義類需要有(Comparable或comparactor介面支援)

11.4 ArrayList集合類

和陣列的區別: arraylist集合的長度是可變的,而陣列長度是不可變的

ArrayList<String>  list = new ArrayList<>();		//建立一個list物件
list.add(E e );		//新增元素
// 常用方法:
public Boolean add(E e);  //備註:對於Arraylist物件而言,add()一定成功,返回值可有可無
public E get(int index);
public E remove(int index);
public int size( );

注意事項:

  1. 對於ArrayList集合來說,直接列印得到的不是地址值,而是內容,為空時輸出[ ]
  2. 對於ArrayList來說,有一個代表泛型(泛型:裝在集合裡的元素全是統一的該型別,泛型只能是引用型別,不能是基本型別)
  3. 如是希望在ArrayList中儲存基本資料型別,則必須使用基本型別對應的“包裝類”
    byte/byte 、short/Short、 int/Integer、 long/Long 、 double/Double、
    char/Character、 boolean/Boolean
  4. ArrayList只適合用來遍歷查詢,而刪除操作效率低

12、抽象類和抽象方法

抽象方法:加上abstract關鍵字,不需要{ }實現任何功能。
抽象類:在class之前加上abstract關鍵字。抽象方法必須在抽象類中,抽象類中可以有具體方法。

注意事項:

  1. 不能直接new抽象類
  2. 必須由一個子類來繼承抽象父類
  3. 子類必須覆蓋重寫(實現)抽象類中的所有的抽象方法,然後建立子類物件進行使用;如果沒有實現所有的抽象方法,則子類仍是一個抽象類
  4. 覆蓋重寫(實現):去掉抽象方法的abstract關鍵字,補上方法體

13、介面

定義: 介面就是多個類的公共規範;介面是一種引用資料型別,最重要的就是其中的抽象方法。注:關鍵字變成interface之後,其生成的位元組碼檔案仍然是.class

public interface 介面名{
	//介面內容
}

注意事項:

  • 介面中的抽象方法,修飾符必須是兩個固定的關鍵字——public abstract,也可省略
  • 介面中包含的內容有:1.常量(預設都是static 、 final修飾) 2.抽象方法 3.預設方法 4.靜態方法 5.私有方法(JDK9以後)

介面的使用:
1、 介面不能直接使用,必須定義一個實現類來實現該介面;

public class 實現類名稱 implement 介面名{
		……
}

2、 介面的實現類必須覆蓋重寫(實現)介面中的所有抽象方法,除非實現類是抽象類;
3、 建立實現類的物件,進行使用。

介面中的預設方法(jdk8以後)
1、介面中使用預設方法,可以解決介面升級問題;
2、介面中的預設方法可以通過實現類物件直接呼叫,也可以被實現類進行覆蓋重寫。

public default 返回值型別 方法名( ){
	//方法體
}

介面中的私有方法(jdk9以後)
用途: 介面中定義一個方法,內部解決兩個預設方法之間程式碼複用的問題,API客戶無法看到也就不能被實現類呼叫。

  • 普通私有方法:解決多個預設方法之間程式碼重複問題
private 返回值型別 方法名稱(){
			//方法體
}
  • 靜態私有方法:解決多個靜態方法之間程式碼重複問題
private static 返回值型別 方法名稱(){
			//方法體
}

介面中的常量——推薦完全大寫+下劃線_命名
必須使用public static final 進行修飾,從效果上來看,其實就是介面的常量。即使省略了關鍵字,仍然是常量,必須手動賦值,不可更改。

使用介面時注意:
1、 介面是沒有靜態程式碼塊或者構造方法的;
2、 一個類的直接父類是唯一的,但是一個類可以同時實現多個介面;

public class DemoInterface implements MyInterface1, Myinterface2{
		//覆蓋所有的抽象方法
}

3、 實現類所實現的所有介面中,存在重複的抽象方法,那麼只需覆蓋重寫一次即可;
4、 實現類若沒有完全覆蓋所有的抽象方法,那麼該實現類必須是一個抽象類;
5、 如果實現類所實現的多個介面當中,存在重複預設方法,則實現類一定要對重複的預設方法進行覆蓋重寫,而且帶著default關鍵字不能省;
6、 直接父類中的方法和所實現介面的預設方法產生衝突,優先使用父類中的方法(繼承優先於實現

14、關鍵字

14.1 static 關鍵字

  • 一旦使用了static關鍵字,那麼這樣的內容將不再屬於物件自己,而是屬於類的。凡是本類的物件,都共享同一份。
  • 無論是靜態成員變數還是成員方法都推薦使用類名來呼叫。(即使用了物件名,編譯器也會把它翻譯過來)
  • 對於本類中的靜態方法,可以忽略類名
  • 靜態不能訪問非靜態(因為【先】有靜態內容,【後】有非靜態內容)
  • 靜態方法不能用this關鍵字(this表示當前物件)
  • 靜態方法存放在方法區的靜態區

靜態程式碼塊
當第一次用到本類時,靜態程式碼塊執行唯一一次;總是優先於構造方法

public class 類名稱{
	static{
		//靜態程式碼塊內容
	}
}

靜態內部類:
非靜態內部類依賴於外部類的例項,而靜態內部類不需要。靜態內部類不能訪問外部類的非靜態的變數和方法。

// 靜態導包,在使用靜態變數和方法時不用再指明 ClassName,從而簡化程式碼
import static com.xxx.ClassName.* 


public class OuterClass {
    class InnerClass {
    }

    static class StaticInnerClass {
    }

    public static void main(String[] args) {
        // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        StaticInnerClass staticInnerClass = new StaticInnerClass();
    }
}

14.2 final 關鍵字

四種用法:
1、 修飾一個類 ———— 不能有任何子類(太監類 )
2、 修飾一個方法 ———— 這個方法不能被子類覆蓋重寫
3、 修飾一個成員變數 ———— 只能一次賦值,內容不可更改;初始化和賦值可以分階段進行
4、 修飾一個區域性變數 ———— 變數不可變,由於成員變數具有預設值,所以final之後必須手動賦值;對於final成員變數,可以直接賦值或構造方法賦值(選其一)

注意: 對於方法、類來說,abstract 和 final關鍵字矛盾,不能同時出現
對於基本型別來說,指內容不變,對引用型別來說,指地址不變(但是內容可變)

15、內部類

含義: 一個事物的內部包含另一個類,包括 成員內部類區域性內部類(匿名內部類)

成員內部類的使用:
1.間接方法:在外部類中例項化一個內部類,通過外部類的方法呼叫
2.直接方法:外部類名稱.內部類名稱 物件名 = new 外部類名稱( ).new 內部類名稱( )通過該方法之間建立一個內部類物件
注意: 如果內部類和外部類中的變數重名,則this.變數名表示內部類中的變數,外部類.this.變數名表示外部類中的變數。

區域性內部類的使用:
1.只用當前所屬方法內部可以使用它
2.在當前方法中例項化該類,然後使用該類,外部通過當前方法使用該類的例項方法。
注意: 如果希望訪問所在方法的區域性變數,那麼這個區域性變數必須是【有效final的】。(原因:內部類和區域性變數的生命週期不一樣)Java8+以後,只要區域性變數不變,則final關鍵字可以省略。

匿名內部類的使用:
如果介面的實現類(或父類的子類)只需要使用唯一的一次,那麼就可以省略該類的定義,使用匿名內部類。

介面名稱 物件名 = new 介面名稱( ){
	@overide
	//覆蓋重寫所有的抽象方法
}

16、Object類的通用方法

16.1 概覽

public final native Class<?> getClass()

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException

protected void finalize() throws Throwable {}

16.2 equals()

等價關係:
1.自反性 x.equals(x)
2.對稱性 x.equals(y) == y.equals(x)
3.傳遞性 if (x.equals(y) && y.equals(z)) x.equals(z)
4.一致性:多次呼叫結果不變 x.equals(y) == x.equals(y)

equals() 與 ==比較:
1.對於基本型別,==判斷兩個值是否相等,基本型別沒有equals()方法
2.對於引用型別,== 判斷兩個變數是否引用同一個物件,而equals() 判斷引用的物件是否等價

16.3 hashCode()

說明:
1.hashCode() 返回雜湊值,而equals()是用來判斷兩個物件是否等價。等價的兩個物件雜湊值一定相同,但是雜湊值相同的兩個物件不一定等價。
2.在覆蓋 equals() 方法時應當總是覆蓋 hashCode() 方法,保證等價的兩個物件雜湊值也相等。HashSet等集合新增不重複物件時,判斷的就是hashCode值。

16.4 toString()

說明:
1.預設返回 ToStringExample@4554617c 這種形式,其中 @ 後面的數值為雜湊碼的無符號十六進位制表示
2.方便物件內容列印。

16.5 clone()

說明:
1.clone()objectprotected 方法,一個類不顯式去重寫 clone(),則其它類就不能直接去呼叫。
2.重寫clone()方法時需要實現Cloneable介面(規定),否則會丟擲CloneNotSupportedException異常

拷貝:

  • 淺拷貝:拷貝物件和原始物件的引用型別引用同一個物件
  • 深拷貝:拷貝物件和原始物件的引用型別引用不同物件

clone()的替代方案:
使用 clone() 方法來拷貝一個物件即複雜又有風險,它會丟擲異常,並且還需要型別轉換,以使用拷貝建構函式或者拷貝工廠來拷貝一個物件。

public class CloneConstructorExample {
    private int[] arr;

    public CloneConstructorExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public CloneConstructorExample(CloneConstructorExample original) {
        arr = new int[original.arr.length];
        for (int i = 0; i < original.arr.length; i++) {
            arr[i] = original.arr[i];
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

    public int get(int index) {
        return arr[index];
    }
}
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1); //使用建構函式進行深拷貝
e1.set(2, 222);
System.out.println(e2.get(2)); // 結果是2

二、集合

1、容器

說明: 就是可以容納其他Java物件的物件。優點是:

  • 降低程式設計難度
  • 提高程式效能
  • 提高API間的互操作性
  • 降低學習難度
  • 降低設計和實現相關API的難度
  • 增加程式的重用性

注意: Java容器裡只能放物件,對於基本型別(int, long, float, double等),需要將其包裝成物件型別後(Integer, Long, Float, Double等)才能放到容器裡。

容器主要包括 CollectionMap 兩種,Collection 儲存著物件的集合,而 Map 儲存著鍵值對(兩個物件)的對映表。

容器介面繼承關係:

2、Collection

和陣列的區別:
1.陣列長度固定,而集合長度是可變的;
2.陣列中儲存的是同一類元素,可以儲存基本資料型別的值;集合儲存的都是物件,物件的型別可以不一致。

2.1 List介面

特點:
1.有序的集合
2.允許有重複的元素
3.有索引,可以使用普通的for迴圈遍歷

2.1.1 ArrayList(執行緒不安全)

說明: ArrayList底層是一個數組,使用自動擴容的機制避免超出容量。陣列擴容通過一個公開的方法ensureCapacity(int minCapacity)來實現,也可以根據需求手動增加容量。

常用函式:

add()
addAll()
set()
get()
remove()
trimToSize() //將底層陣列的容量調整為當前列表儲存的實際元素的大小的功能
indexOf(), lastIndexOf()

Fail-Fast機制: 記錄modCount引數,在面對併發的修改時,迭代器很快就會完全失敗,而不是冒著在將來某個不確定時間發生任意不確定行為的風險。

2.1.2 LinkedList(執行緒不安全)

說明: LinkedList同時實現了List介面和Qeque介面,也就是說它既可以看作一個順序容器,又可以看作一個佇列(Queue),同時又可以看作一個棧(Stack)。注:關於棧或佇列,現在的首選是ArrayDeque

特點: 底層是雙向連結串列,增刪快,查詢慢

執行緒不安全,如果想讓其安全,可以使用Collections.synchronizedList()方法將其轉換為一個SynchronizedList物件。

2.1.3 Vector(執行緒安全)

說明: 集合中的操作都是同步方法,執行緒安全,但是效率低。

SynchronizedList物件和Vector物件的一些區別:
1.SynchronizedList有很好的擴充套件和相容功能。他可以將所有的List的子類轉成執行緒安全的類。
2.使用SynchronizedList的時候,進行遍歷時要手動進行同步處理。
3.SynchronizedList是同步程式碼塊可以指定鎖定的物件,Vector是同步方法

2.2 迭代器

說明: 對集合進行遍歷,import java.util.Iterator

常用的兩個方法:

boolean hasNext( ) 	//如果仍有元素,則返回true
E next( )			//返回迭代的下一個元素

迭代器的使用:
1、使用集合中的方法iterator( )獲取迭代器介面的實現類,使用Iterator介面接收(多型)
2、使用Iterator中的boolean hasNext( )方法判斷是否還有下一個元素
3、使用Iterator中的E next( )方法取出集合中的下一個元素

增強for迴圈: 底層使用迭代器,簡化了迭代器的使用

for(String str:collection){
	System.out.println(str);
}

2.3 Queue&Stack

概述: Java裡有Stack的類(Vector繼承類),卻沒有Queue的類(它是個介面名字)。當需要使用棧時,Java已不推薦使用Stack,而是推薦使用更高效的ArrayDeque;使用佇列時也首選ArrayDeque(次選是LinkedList)。

2.3.1 Queue介面

Queue介面繼承自Collection介面,除了最基本的Collection的方法之外,它還支援額外的insertion, extraction和inspection操作。共6個方法,一組是丟擲異常的實現;另外一組是返回值的實現(沒有則返回null)。

2.3.2 Deque介面

Deque是"double ended queue", 表示雙向的佇列,繼承自 Queue介面,除了支援Queue的方法之外,還可以對佇列的頭和尾都進行操作。

由於Deque既可以當佇列,也可以當棧使用,對應的方法分別為:

2.3.3 ArrayDeque類

Deque介面的實現類,底層通過迴圈陣列實現,任何一點都可能被看作起點或者終點。執行緒不安全,另外不允許放入null元素。

2.3.4 PriorityQueue類

Queue介面的實現類,是一種優先佇列,作用是每次取出的元素都是佇列中最小的元素(建構函式中需要傳入比較器Comparator進行比較)。底層是由陣列表示的小頂堆實現的。

PriorityQueue的peek()element()操作是常數時間,add(), offer(), 無引數的remove()以及poll()方法的時間複雜度都是log(N)。

2.4 Set介面

特點:
1.元素不重複
2.沒有索引
3.無序,即放入和取出的順序可能不同

2.4.1 HashSet

是對HashMap進行了簡單封裝實現的,也就是說HashSet裡面有一個HashMap(介面卡模式),具體內容參考HashMap

2.4.2 LinkedHashSet

是對LinkedHashMap進行了簡單封裝實現的,也就是說LinkedHashSet裡面有一個LinkedHashMap(介面卡模式),具體內容參考LinkedHashMap

2.4.3 TreeSet

是對TreeMap進行了簡單封裝實現的,也就是說TreeSet裡面有一個TreeMap(介面卡模式),具體內容參考TreeMap

3、Map

3.1 HashMap

特點:
1.執行緒不安全
2.不保證元素順序
3.採用拉鍊法解決雜湊衝突

底層結構:

注意: 將物件放入到HashMap或HashSet中時,需要重寫hashCode()和equals()方法。

在java8以後,HashMap底層改由陣列+連結串列+紅黑樹組成,當衝突元素大於8個時,會將衝突連結串列轉化為紅黑樹,這時在這些位置查詢的時間複雜度降為O(logN)。

3.2 LinkedHashMap

特點: HashMap的基礎上,採用雙向連結串列(doubly-linked list)的形式將所有entry物件連線起來,為保證元素的迭代順序跟插入順序相同。


LinkedHashMap經典用法:
可以輕鬆實現一個採用了FIFO替換策略的快取。具體說來,LinkedHashMap有一個子類方法protected boolean removeEldestEntry(Map.Entry<K,V> eldest),該方法的作用是告訴Map是否要刪除“最老”的Entry,所謂最老就是當前Map中最早插入的Entry,如果該方法返回true,最老的那個元素就會被刪除。在每次插入新元素的之後LinkedHashMap會自動詢問removeEldestEntry()是否要刪除最老的元素。這樣只需要在子類中過載該方法,當元素個數超過一定數量時讓removeEldestEntry()返回true,就能夠實現一個固定大小的FIFO策略的快取。示例程式碼如下:

/** 一個固定大小的FIFO替換策略的快取 */
class FIFOCache<K, V> extends LinkedHashMap<K, V>{
    private final int cacheSize;
    public FIFOCache(int cacheSize){
        this.cacheSize = cacheSize;
    }

    // 當Entry個數超過cacheSize時,刪除最老的Entry
    @Override
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
       return size() > cacheSize;
    }
}

3.3 TreeMap

特點:
1.TreeMap底層通過紅黑樹(Red-Black tree)實現,也就意味著containsKey(), get(), put(), remove()都有著log(n)的時間複雜度;
2.TreeMap實現了SortedMap介面,也就是說會按照key的大小順序對Map中的元素進行排序,也可以傳入比較器。

3.3 WeakHashMap*

特點: WeakHashMap 裡的entry可能會被GC自動刪除,即使程式設計師沒有呼叫remove()或者clear()方法。更直觀的說,當使用 WeakHashMap 時,即使沒有顯示的新增或刪除任何元素,也可能發生如下情況:

1.呼叫兩次size()方法返回不同的值;
2.兩次呼叫isEmpty()方法,第一次返回false,第二次返回true;
3.兩次呼叫containsKey()方法,第一次返回true,第二次返回false,儘管兩次使用的是同一個key;
4.兩次呼叫get()方法,第一次返回一個value,第二次返回null,儘管兩次使用的是同一個物件。

適用場景: 適用於需要快取的場景。在快取場景下,由於記憶體是有限的,不能快取所有物件;物件快取命中可以提高系統效率,但快取MISS也不會造成錯誤,因為可以通過計算重新得到。

造成該特點的原因: Java中記憶體是通過GC自動管理的,GC會在程式執行過程中自動判斷哪些物件是可以被回收的,並在合適的時機進行記憶體釋放。GC判斷某個物件是否可被回收的依據是,是否有有效的引用指向該物件,這裡的有效引用並不包括弱引用。而WeakHashMap中的物件就是弱引用,有可能被CG自動回收了,所以會發生這些奇怪情況。

Weak HashSet獲取方式: java中沒有對應的WeakHashSet物件,可以通過Collections.newSetFromMap(Map<E,Boolean> map)將Map類包裝為Set類。

// 將WeakHashMap包裝成一個Set
Set<Object> weakHashSet = Collections.newSetFromMap(
        new WeakHashMap<Object, Boolean>());

3.4 Map集合的遍歷

第一種方式: 通過鍵找值的方式
1.使用Map集合中的keyset()方法,把Map集合中的key取出來放在一個set集合中
2.遍歷set集合,獲取每一個key值(使用迭代器/增強for)
3.通過Map中的方法get (key)找到對應的value

第二種方式: 使用Map的內部類Entry物件遍歷
1.使用Map集合中的方法entrySet(),把Map集合中的多個Entry物件取出來,儲存到一個set集合中
2.遍歷set集合,獲取每一個Entry物件
3.使用Entry物件中的getKey()getValue()方法獲取鍵值

Map<String, Integer> map = new HashMap<String, Integer>();
map.put("黃曉明", 33);
map.put("胡歌", 28);

Set<Map.Entry<String, Integer>> entries = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entries.iterator();
while (iterator.hasNext()) {
	Map.Entry<String, Integer> entry = iterator.next();
	System.out.println("key:"+entry.getKey()+" value:"+entry.getValue());
	}

三、泛型機制

1、泛型概述

泛型是一種未知的資料型別,可以當成一種變數,用來接收資料型別,建立物件的時候,就會確定泛型的資料型別。

泛型的意義:
1.適用於多種資料型別執行相同的程式碼【程式碼複用】;
2.型別在使用時指定,不需要強制型別轉換(型別安全,編譯器會檢查型別)【型別約束】;

//它將提供型別的約束,提供編譯前的檢查
// list中只能放String, 不能放其它型別的元素
List<String> list = new ArrayList<String>();

2、泛型的使用

2.1 泛型類

一個簡單的泛型類:

class Point<T>{ // 此處可以隨便寫識別符號號,T是type的簡稱
    private T var; // var的型別由T指定,即:由外部指定

    public T getVar() {
        return var;
    }

    public void setVar(T var) {
        this.var = var;
    }

    public static void main(String args[]){
        Point<String> p = new Point<String>() ;     // 裡面的var型別為String型別  
        p.setVar("it") ;                            // 設定字串  
        System.out.println(p.getVar().length()) ;   // 取得字串的長度  
    }
}

多元泛性類:

class Notepad<K,V>{ // 此處可以隨便寫識別符號號,T是type的簡稱
    private K key;
    private V value;

    public K getKey() {
        return key;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }

    public static void main(String args[]){
        Notepad<String,Integer> notepad = new Notepad<>();
        notepad.setKey("小米");
        notepad.setValue(1200);

        System.out.print("姓名;" + notepad.getKey()) ;      // 取得資訊
        System.out.print(",年齡;" + notepad.getValue()) ;       // 取得資訊
    }
}

2.2 泛型介面

//定義一個泛型介面
interface Info<T>{
    public T getVar() ;
}
//實現介面
class InfoImpl<T> implements Info<T>{
    private T var;

    @Override
    public T getVar() {
        return this.var;
    }
    public void setVar(T var) {
        this.var = var;
    }
    public static void main(String[] args) {
        InfoImpl<String> info = new InfoImpl<>();
        info.setVar("hello");
        System.out.println(info.getVar());
    }
}

2.3 泛型方法

建立泛型方法:

呼叫泛型方法語法格式:

2.4 泛型上下限

當代碼中存在隱式型別轉換時:

class A{}
class B extends A {}

// 如下兩個方法不會報錯
public static void funA(A a) {
    // ...          
}
public static void funB(B b) {
    funA(b);
    // ...             
}

// 如下funD方法會報錯
public static void funC(List<A> listA) {
    // ...          
}
public static void funD(List<B> listB) {
	//報錯,因為存在隱式型別轉換
    funC(listB); // Unresolved compilation problem: The method doPrint(List<A>) in the type test is not applicable for the arguments (List<B>)
    // ...             
}

解決方法:加入了型別引數的上下邊界機制<? extends A>,編譯器知道型別引數的範圍,如果傳入的例項型別B是在這個範圍內的話允許轉換,這時只要一次型別轉換就可以了,執行時會把物件當做A的例項看待。

public static void funC(List<? extends A> listA) {
    // ...          
}
public static void funD(List<B> listB) {
    funC(listB); // 編譯器檢查,在允許範圍內
    // ...             
}

2.5 泛型萬用字元

<?>代表任意的資料型別
適用場景: 定義一個方法遍歷ArrayList集合,但是不確定ArrayList集合使用什麼資料型別,則可以使用<?>來接收資料型別(即,同時接收多種型別)。ps:不能建立物件使用,只能作為方法的引數

萬用字元的使用(以上述)

interface Info<T>{
    public T getVar() ;
}

class InfoImpl<T> implements Info<T>{
    private T var;
    @Override
    public T getVar() {
        return this.var;
    }
    public void setVar(T var) {
        this.var = var;
    }

    public static void main(String[] args){
        InfoImpl<?>[] list = new InfoImpl<?>[2];
        InfoImpl<String> info1 = new InfoImpl<>();
        info1.setVar("String");        
        InfoImpl<Integer> info2 = new InfoImpl<>();
        info2.setVar(222);
        
        list[0] = info1;
        list[1] = info2;
        
        trvalList(list);

    }
    public static void trvalList(InfoImpl<?>[] list){
        for(int i =0;i<list.length;i++){
            System.out.println(list[i].getVar());
        }
    }
}

泛型陣列:

//宣告一組泛型陣列
List<String>[] list11 = new ArrayList<String>[10]; //編譯錯誤,非法建立 
List<String>[] list12 = new ArrayList<?>[10]; //編譯錯誤,需要強轉型別 
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是會有警告 
List<?>[] list14 = new ArrayList<String>[10]; //編譯錯誤,非法建立 
List<?>[] list15 = new ArrayList<?>[10]; //OK 
List<String>[] list6 = new ArrayList[10]; //OK,但是會有警告

合理的建立泛型陣列:

public ArrayWithTypeToken(Class<T> type, int size) {
    array = (T[]) Array.newInstance(type, size);
}

3、泛型使用小結

<?> 無限制萬用字元
<? extends E> extends 關鍵字聲明瞭型別的上界,表示引數化的型別可能是所指定的型別,或者是此型別的子類
<? super E> super 關鍵字聲明瞭型別的下界,表示引數化的型別可能是指定的型別,或者是此型別的父類
& 可以用來多個限制 <T extends Staff & Passenger>

// 使用原則《Effictive Java》
// 為了獲得最大限度的靈活性,要在表示 生產者或者消費者 的輸入引數上使用萬用字元,使用的規則就是:生產者有上限、消費者有下限
1. 如果引數化型別表示一個 T 的生產者,使用 < ? extends T>;
2. 如果它表示一個 T 的消費者,就使用 < ? super T>;
3. 如果既是生產又是消費,那使用萬用字元就沒什麼意義了,因為你需要的是精確的引數型別。

4、深入理解泛型

4.1 偽泛型

說明: java中的泛型是一種“偽泛型”,由於泛型是java1.5引入,為了相容之前的版本所以採用該策略。

含義: Java在語法上支援泛型,但是在編譯階段會進行所謂的“型別擦除”(Type Erasure),將所有的泛型表示(尖括號中的內容)都替換為具體的型別(其對應的原生態型別),就像完全沒有泛型一樣。

型別擦除的原則:
1.消除型別引數宣告,即刪除<>及其包圍的部分;
2.根據型別引數的上下界推斷並替換所有的型別引數為原生態型別:無限制萬用字元或沒有上下界限定則替換為Object;如果存在上下界限定則根據子類替換原則取型別引數的最左邊限定型別;
3.為了保證型別安全,必要時插入強制型別轉換程式碼;
4.自動產生“橋接方法”以保證擦除型別後的程式碼仍然具有泛型的“多型性”。

型別擦除的過程:

無限制型別擦除:

有限制類型擦除:
<T extends Number><? extends Number>的型別引數被替換為Number<? super Number>被替換為Object

擦除方法定義中的型別引數:

4.2 如何理解泛型的編譯期檢查

既然說型別變數會在編譯的時候擦除掉,那為什麼我們往 ArrayList 建立的物件中新增整數會報錯呢?不是說泛型變數String會在編譯的時候變為Object型別嗎?為什麼不能存別的型別呢?既然型別擦除了,如何保證我們只能使用泛型變數限定的型別呢?

說明: Java編譯器是通過先檢查程式碼中泛型的型別,然後在進行型別擦除,再進行編譯。

例子:
下面的程式中,使用add方法新增一個整型,會直接報錯,說明這就是在編譯之前的檢查。因為如果是在編譯之後檢查,型別擦除後,原始型別為Object,是應該允許任意引用型別新增的。可實際上卻不是這樣的,這恰恰說明了關於泛型變數的使用,是會在編譯之前檢查的。

public static  void main(String[] args) {  

    ArrayList<String> list = new ArrayList<String>();  
    list.add("123");  
    list.add(123);//編譯錯誤  
}

4.3 橋接方法

型別擦除會造成多型的衝突,而JVM解決方法就是橋接方法

例子——
有這樣一個泛型類:

class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T value) {  
        this.value = value;  
    }  
}

建立一個子類:

class DateInter extends Pair<Date> {  
    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  
    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
}

在這個子類中,我們設定父類的泛型型別為Pair<Date>,那麼父類裡面的兩個方法的引數都為Date型別。然而經過泛型擦除後,父類的方法引數變為了Object型別,這樣,子類方法對父類方法根本就不會是重寫,而是過載。為了解決這樣的衝突,JVM採用橋接方法來解決。

我們用javap -c className的方式反編譯下DateInter子類的位元組碼,結果如下:

class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  
  com.tao.test.DateInter();  
    Code:  
       0: aload_0  
       1: invokespecial #8                  // Method com/tao/test/Pair."<init>":()V  
       4: return  

  public void setValue(java.util.Date);  //我們重寫的setValue方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: invokespecial #16                 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V  
       5: return  

  public java.util.Date getValue();    //我們重寫的getValue方法  
    Code:  
       0: aload_0  
       1: invokespecial #23                 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;  
       4: checkcast     #26                 // class java/util/Date  
       7: areturn  

  public java.lang.Object getValue();     //編譯時由編譯器生成的橋方法  
    Code:  
       0: aload_0  
       1: invokevirtual #28                 // Method getValue:()Ljava/util/Date 去呼叫我們重寫的getValue方法;  
       4: areturn  

  public void setValue(java.lang.Object);   //編譯時由編譯器生成的橋方法  
    Code:  
       0: aload_0  
       1: aload_1  
       2: checkcast     #26                 // class java/util/Date  
       5: invokevirtual #30                 // Method setValue:(Ljava/util/Date; 去呼叫我們重寫的setValue方法)V  
       8: return  
}

即自動建立Object型別的橋方法,用來覆蓋父類的方法,橋方法中呼叫子類重寫的方法。

4.4 一些關於泛型的問答

:如何理解基本型別不能作為泛型型別?
:因為當型別擦除後,物件原始型別變為Object,但是Object型別不能儲存int值,只能引用Integer的值。

:如何理解泛型型別不能例項化?
:這本質上是由於型別擦除決定的。在 Java 編譯期沒法確定泛型引數化型別,也就找不到對應的類位元組碼檔案,所以自然就不行了。可以通過反射實現:

T test = new T(); // 錯誤!
//可以通過反射例項化泛型型別
static <T> T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
    T obj = clazz.newInstance();
    return obj;
}

:如何理解泛型類中的靜態方法和靜態變數?
:泛型類中的靜態方法和靜態變數不可以使用泛型類所宣告的泛型型別引數。因為泛型類中的泛型引數的例項化是在定義物件的時候指定的,而靜態變數和靜態方法不需要使用物件來呼叫。物件都沒有建立,如何確定這個泛型引數是何種型別,所以當然是錯誤的。

//錯誤
public class Test2<T> {    
    public static T one;   //編譯錯誤    
    public static  T show(T one){ //編譯錯誤    
        return null;    
    }    
}

public class Test2<T> {    
	//因為這是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的 T,而不是泛型類中的T
    public static <T> T show(T one){ //這是正確的    
        return null;    
    }    
}

:如何理解異常中使用泛型?
:1.不能丟擲也不能捕獲泛型類的物件;2.不能再catch子句中使用泛型變數;3.但是在異常宣告中可以使用型別變數,例如:

public static<T extends Throwable> void doWork(T t) throws T {
    try{
        ...
    } catch(Throwable realCause) {
        t.initCause(realCause);
        throw t; 
    }
}

:既然型別被擦除了,那麼如何獲取泛型的引數型別呢?
:可以通過反射(java.lang.reflect.Type)獲取泛型。

public class GenericType<T> {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public static void main(String[] args) {
        GenericType<String> genericType = new GenericType<String>() {};
        Type superclass = genericType.getClass().getGenericSuperclass();
        //getActualTypeArguments 返回確切的泛型引數, 如Map<String, Integer>返回[String, Integer]
        Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; 
        System.out.println(type);//class java.lang.String
    }
}

四、註解機制

1、註解基礎

註解是在JDK1.5之後引入的新特性,用於對程式碼進行說明,可以對包、類、介面、欄位、方法引數、區域性變數等進行註解。作用有:

  • 生成文件,通過程式碼裡標識的元資料生成javadoc文件
  • 編譯檢查,通過程式碼裡標識的元資料讓編譯器在編譯期間進行檢查驗證
  • 編譯時動態處理,編譯時通過程式碼裡標識的元資料動態處理,例如動態生成程式碼
  • 執行時動態處理,執行時通過程式碼裡標識的元資料動態處理。

2、Java自帶的標準註解

  • @Override 標明重寫某個方法
  • @Deprecated 標明某個類或方法過時
  • @SuppressWarnings 標明要忽略的警告

3、java元註解

元註解是用於定義註解的註解,包括@Retention@Target@Inherited@Documented

  • @Retention 標明註解被保留的階段,包括原始碼階段(RetentionPolicy.SOURCE)、編譯階段【預設】(RetentionPolicy.CLASS)、執行時階段(RetentionPolicy.RUNTIME
  • @Target 標明註解的物件,包括方法、類、屬性等,取值範圍定義在ElementType列舉中:
	public enum ElementType {
	    TYPE, // 類、介面、列舉類
	    FIELD, // 成員變數(包括:列舉常量)
	    METHOD, // 成員方法
	    PARAMETER, // 方法引數
	    CONSTRUCTOR, // 構造方法
	    LOCAL_VARIABLE, // 區域性變數
	    ANNOTATION_TYPE, // 註解類
	    PACKAGE, // 可用於修飾:包
	    TYPE_PARAMETER, // 型別引數,JDK 1.8 新增
	    TYPE_USE // 使用型別的任何地方,JDK 1.8 新增
	}
  • @Inherited 被它修飾的Annotation將具有繼承性,如果某個類使用了被@Inherited修飾的Annotation,則其子類將自動具有該註解
  • @Documented 描述在使用 javadoc 工具為類生成幫助文件時是否要保留其註解資訊
  • @Repeatable 重複註解(jdk8),允許在同一申明型別(類,屬性,或方法)的多次使用同一個註解。
//重複註解的例子:
@Repeatable(Authorities.class)
public @interface Authority {
     String role();
}

public @interface Authorities {
    Authority[] value();
}

public class RepeatAnnotationUseNewVersion {
    @Authority(role="Admin")
    @Authority(role="Manager")
    public void doSomeThing(){ }
}
  • @Native 註解修飾成員變數,則表示這個變數可以被原生代碼引用,常常被程式碼生成工具使用。

4、自定義註解

定義自己的註解:

package com.pdai.java.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethodAnnotation {
    public String title() default "";
    public String description() default "";
}

使用註解:

import java.io.FileNotFoundException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class TestMethodAnnotation {

    @Override
    @MyMethodAnnotation(title = "toStringMethod", description = "override toString method")
    public String toString() {
        return "Override toString method";
    }

    @Deprecated
    @MyMethodAnnotation(title = "old static method", description = "deprecated old static method")
    public static void oldMethod() {
        System.out.println("old method, don't use it.");
    }

    @SuppressWarnings({"unchecked", "deprecation"})
    @MyMethodAnnotation(title = "test method", description = "suppress warning static method")
    public static void genericsTest() throws FileNotFoundException {
        List l = new ArrayList();
        l.add("abc");
        oldMethod();
    }
}

5、深入理解註解

註解支援繼承嗎?

註解是不支援繼承的。不能使用關鍵字extends來繼承某個@interface,但註解在編譯後,編譯器會自動繼承java.lang.annotation.Annotation介面。另外,一個類使用@Inherited修飾的註解,該類的子類自動具有該註解。

註解@interface 是一個繼承了Annotation介面的介面,裡面每一個屬性,其實就是介面的一個抽象方法。
(註解是框架的靈魂)

五、異常機制

1、異常的層次結構

  • Throwable
    Throwable 是 Java 語言中所有錯誤與異常的超類。
    Throwable 包含兩個子類:Error(錯誤)和 Exception(異常),它們通常用於指示發生了異常情況。
    Throwable 包含了其執行緒建立時執行緒執行堆疊的快照,它提供了 printStackTrace() 等介面用於獲取堆疊跟蹤資料等資訊。

  • Error(錯誤)
    一般表示程式碼執行時 JVM 出現問題,此類錯誤發生時,JVM 將終止執行緒。

  • Exception(異常)
    程式本身可以捕獲並且可以處理的異常。Exception 這種異常又分為兩類:執行時異常和編譯時異常。
    執行時異常: 編譯器不會檢查它,一般是由程式邏輯錯誤引起的
    非執行時異常: 從程式語法角度講是必須進行處理的異常,如果不處理,程式就不能編譯通過。如IOException、SQLException等。

2、異常的宣告(throws)

若方法中存在檢查異常,如果不對其捕獲,那必須在方法頭中顯式宣告該異常,以便於告知方法呼叫者此方法有異常,需要進行處理。

public static void method() throws IOException, FileNotFoundException{
    //something statements
}

注意: 若是父類的方法沒有宣告異常,則子類繼承方法後,也不能宣告異常。

3、異常的丟擲(throw)

如果程式碼可能會引發某種錯誤,可以建立一個合適的異常類例項並丟擲它,這就是丟擲異常。

public static double method(int value) {
    if(value == 0) {
        throw new ArithmeticException("引數不能為0"); //丟擲一個執行時異常
    }
    return 5.0 / value;
}

說明: 大部分情況下都不需要手動丟擲異常,因為Java的大部分方法要麼已經處理異常,要麼已宣告異常,所以一般都是捕獲異常或者再往上拋。丟擲異常適用的場合在於,異常型別可能有多種,可以用統一的異常型別向外暴露,不需暴露太多內部異常細節。例如:

private static void readFile(String filePath) throws MyException {    
    try {
        // code
    } catch (IOException e) {
        MyException ex = new MyException("read file failed.");
        ex.initCause(e);
        throw ex;
    }
}

4、自定義異常

習慣上,定義一個異常類應包含兩個建構函式,一個無參建構函式和一個帶有詳細描述資訊的建構函式(Throwable 的 toString 方法會列印這些詳細資訊,除錯時很有用), 比如上面用到的自定義MyException:

public class MyException extends Exception {
    public MyException(){ }
    public MyException(String msg){
        super(msg);
    }
    // ...
}

5、異常的捕獲

5.1 try-catch

在一個 try-catch 語句塊中可以捕獲多個異常型別,並對不同型別的異常做出不同的處理,也可以捕獲多種型別異常,用 | 隔開

private static void readFile(String filePath) {
    try {
        // code
    } catch (FileNotFoundException | UnknownHostException e) {
        // handle FileNotFoundException or UnknownHostException
    } catch (IOException e){
        // handle IOException
    }
}

try-catch-finally

try {                        
    //執行程式程式碼,可能會出現異常                 
} catch(Exception e) {   
    //捕獲異常並處理   
} finally {
    //必執行的程式碼
}

執行順序:

5.2 try-finally

try塊中引起異常,異常程式碼之後的語句不再執行,直接執行finally語句。 try塊沒有引發異常,則執行完try塊就執行finally語句。

5.3 try-with-resource

java1.7引入的語法糖,可以自動回收資源。

private  static void tryWithResourceTest(){
    try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
        // code
    } catch (IOException e){
        // handle exception
    }
}

其中Scanner類:

public final class Scanner implements Iterator<String>, Closeable {
  // ...
}
public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

6、使用異常的一些總結

  • 優先捕獲最具體的異常
  • 不要捕獲 Throwable 類
  • 不要忽略異常,合理的做法是至少要記錄異常的資訊
  • 包裝異常時不要拋棄原始的異常
  • 不要記錄並丟擲異常
  • 不要使用異常控制程式的流程:異常非常耗時
  • 不要在finally塊中使用return

7、深入理解異常

一個簡單的異常:

public static void simpleTryCatch() {
   try {
       testNPE();
   } catch (Exception e) {
       e.printStackTrace();
   }
}

使用javap來分析這段程式碼:

//javap -c Main
 public static void simpleTryCatch();
    Code:
       0: invokestatic  #3                  // Method testNPE:()V
       3: goto          11
       6: astore_0
       7: aload_0
       8: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
      11: return
    Exception table:
       from    to  target type
           0     3     6   Class java/lang/Exception

當一個異常發生時:

  1. JVM會在當前出現異常的方法中,查詢異常表,是否有合適的處理者來處理
  2. 如果當前方法異常表不為空,並且異常符合處理者的from和to節點,並且type也匹配,則JVM呼叫位於target的呼叫者來處理。
  3. 如果上一條未找到合理的處理者,則繼續查詢異常表中的剩餘條目
  4. 如果當前方法的異常表無法處理,則向上查詢(彈棧處理)剛剛呼叫該方法的呼叫處,並重覆上面的操作。
  5. 如果所有的棧幀被彈出,仍然沒有處理,則拋給當前的Thread,Thread則會終止。
  6. 如果當前Thread為最後一個非守護執行緒,且未處理異常,則會導致JVM終止執行。

六、反射機制

1、概述

反射就是把java類中的各種成分對映成一個個的Java物件。

例如:一個類有:成員變數、方法、構造方法、包等等資訊,利用反射技術可以對一個類進行解剖,把個個組成部分對映成一個個物件。

2、Class類

1.Class類也是一個實實在在的類,存在於JDK的java.lang包中。
2.每個java類執行時都在JVM裡表現為一個Class物件,可通過類名.class、型別.getClass()、Class.forName("類名")等方法獲取class物件
3.陣列同樣也被對映為class 物件的一個類,所有具有相同元素型別和維數的陣列都共享該 Class 物件。
4.基本型別boolean,byte,char,short,int,long,float,double和關鍵字void同樣表現為 class 物件。
5.Class類只存私有建構函式,因此對應Class物件只能有JVM建立和載入
6.Class類的物件作用是執行時提供或獲得某個物件的型別資訊

3、java類載入機制

3.1 類的生命週期

類載入的過程包括了載入驗證準備解析初始化五個階段。
其中,載入驗證準備初始化發生的順序確定,通常是交叉混合進行的。解析發生的順序不一定,這是為了支援java語言的執行時繫結。

3.2 類的載入

載入是類載入過程的第一個階段,在載入階段,虛擬機器需要完成以下三件事情:
1.通過全限定類名獲取二進位制位元組流
2.將位元組流代表的靜態儲存結構轉化為方法區的執行時資料結構
3.在java堆中建立一個java.lang.Class物件,作為訪問方法區這些資料的入口。

3.3 連線

  • 驗證:確保被載入的類的正確性
    這一階段是為了保證位元組流檔案中的資訊符合虛擬機器的要求。主要有檔案格式驗證元資料驗證位元組碼驗證符號引用驗證ps: 驗證階段是非常重要的,但不是必須的,在某些情況下那麼可以考慮採用-Xverifynone引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間

  • 準備:為類的靜態變數分配記憶體,並將其初始化為預設值

  • 解析:把類中的符號引用轉換為直接引用
    解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。

3.4 初始化

初始化主要針對類變數。Java中類變數初始化有兩種方式

  • 宣告時指定類變數的初始值
  • 使用靜態程式碼塊初始化類變數

JVM初始化的步驟:

  • 假如這個類還沒有被載入和連線,則程式先載入並連線該類
  • 假如該類的直接父類還沒有被初始化,則先初始化其直接父類
  • 假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化時機(只有出現以下情況時才會導致類的初始化)

  • 通過new建立類的例項
  • 訪問某個類或者介面的靜態變數,或者對該靜態變數賦值
  • 呼叫靜態方法
  • 反射
  • 初始化某個類的子類,其父類也會被初始化
  • Java虛擬機器啟動時被標明為啟動類的類

3.5 解除安裝

在以下情況JVM將結束生命週期:

  • 執行了System.exit()方法
  • 程式正常執行結束
  • 程式在執行過程中遇到了異常或錯誤而異常終止
  • 由於作業系統出現錯誤而導致Java虛擬機器程序終止

4、類載入器

4.1 類載入器的層次

注意: 這裡父類載入器並不是通過繼承關係來實現的,而是採用組合實現的。

從開發人員角度類載入器大致劃分為以下三類:

  • 啟動類載入器: Bootstrap ClassLoader,負責載入存放在JDK\jre\lib下或被-Xbootclasspath引數指定的路徑中的類庫。啟動類載入器是無法被Java程式直接引用的。
  • 擴充套件類載入器: Extension ClassLoader,負責載入JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。
  • 應用程式類載入器: Application ClassLoader,負責載入使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器,預設的類載入器。
public class ClassLoaderTest {
     public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}

結果如下:
sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null //BootstrapLoader(引導類載入器)是用C語言實現的,找不到一個確定的返回父Loader的方式

4.2 類的載入方式

類載入有三種方式:
1、命令列啟動應用時候由JVM初始化載入
2、通過Class.forName()方法動態載入
3、通過ClassLoader.loadClass()方法動態載入

//類載入的例子
public class loaderTest { 
        public static void main(String[] args) throws ClassNotFoundException { 
                ClassLoader loader = HelloWorld.class.getClassLoader(); 
                System.out.println(loader); 
                //使用ClassLoader.loadClass()來載入類,不會執行初始化塊 
                loader.loadClass("Test2"); 
                //使用Class.forName()來載入類,預設會執行初始化塊 
//                Class.forName("Test2"); 
                //使用Class.forName()來載入類,並指定ClassLoader,初始化時不執行靜態塊 
//                Class.forName("Test2", false, loader); 
        } 
}

public class Test2 { 
        static { 
                System.out.println("靜態初始化塊執行了!"); 
        } 
}

Class.forName()ClassLoader.loadClass()區別:
1.Class.forName(): 將類的.class檔案載入到jvm中之外,還會對類進行解釋,執行類中的static塊; 2.ClassLoader.loadClass(): 只幹一件事情,就是將.class檔案載入到jvm中,不會執行static中的內容,只有在newInstance()才會去執行static塊;
3.Class.forName(name, initialize, loader)帶參函式也可控制是否載入static塊。並且只有呼叫了newInstance()方法採用呼叫建構函式,建立類的物件。

4.3 JVM的類載入機制

java採用雙親委派機制進行類的載入。過程如下:

  1. 當AppClassLoader載入一個class時,它首先不會自己去嘗試載入這個類,而是把類載入請求委派給父類載入器ExtClassLoader去完成。
  2. 當ExtClassLoader載入一個class時,它首先也不會自己去嘗試載入這個類,而是把類載入請求委派給BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader載入失敗(例如在$JAVA_HOME/jre/lib裡未查詢到該class),會使用ExtClassLoader來嘗試載入;
  4. 若ExtClassLoader也載入失敗,則會使用AppClassLoader來載入,如果AppClassLoader也載入失敗,則會報出異常ClassNotFoundException。

雙親委派機制的優點: 防止記憶體中出現多份同樣的位元組碼;保證Java程式安全穩定執行

4.4 自定義類載入器

通常情況下,我們都是直接使用系統類載入器,為保證安全性,這些位元組碼經過了加密處理,這時系統類載入器就無法對其進行載入,這樣則需要自定義類載入器來實現。只需要重寫 findClass 方法即可。一般不要重寫loadClass方法,因為這樣容易破壞雙親委託模式。

雙親委託機制的類載入器程式碼:

public Class<?> loadClass(String name)throws ClassNotFoundException {
            return loadClass(name, false);
    }
    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
            // 首先判斷該型別是否已經被載入
            Class c = findLoadedClass(name);
            if (c == null) {
                //如果沒有被載入,就委託給父類載入或者委派給啟動類載入器載入
                try {
                    if (parent != null) {
                         //如果存在父類載入器,就委派給父類載入器載入
                        c = parent.loadClass(name, false);
                    } else {
                    //如果不存在父類載入器,就檢查是否是由啟動類載入器載入的類,通過呼叫本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父類載入器和啟動類載入器都不能完成載入任務,才呼叫自身的載入功能
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

重寫findClass方法:

import java.io.*;

public class MyClassLoader extends ClassLoader {

    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

	//此處可以有解密檔案的邏輯
    private byte[] loadClassData(String className) { 
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.pdai.jvm.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

七、SPI機制

1、概述

SPI(Service Provider Interface),是JDK內建的一種 服務提供發現機制,可以用來啟用框架擴充套件和替換元件。主要思想是將裝配的控制權移到程式之外,在模組化設計中這個機制尤其重要,其核心思想就是解耦

具體操作: 當服務的提供者提供了一種介面的實現之後,需要在classpath下的META-INF/services/目錄裡建立一個以服務介面命名的檔案,這個檔案裡的內容就是這個介面的具體的實現類。當其他的程式需要這個服務的時候,就可以通過查詢這個jar包(一般都是以jar包做依賴)的META-INF/services/中的配置檔案,配置檔案中有介面的具體實現類名,可以根據這個類名進行載入例項化,就可以使用該服務了。JDK中查詢服務的實現的工具類是:java.util.ServiceLoader

2、SPI機制的簡單例項

//定義一個介面
public interface Search {
    public List<String> searchDoc(String keyword);   
}

//檔案搜尋實現
public class FileSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("檔案搜尋 "+keyword);
        return null;
    }
}

//資料庫搜尋實現
public class DatabaseSearch implements Search{
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("資料搜尋 "+keyword);
        return null;
    }
}

接下來可以在resources下新建META-INF/services/目錄,然後新建介面全限定名的檔案:com.myproject.Search,裡面加上我們需要用到的實現類:

com.myproject.Search

測試:

public class TestCase {
    public static void main(String[] args) {
        ServiceLoader<Search> s = ServiceLoader.load(Search.class);
        Iterator<Search> iterator = s.iterator();
        while (iterator.hasNext()) {
           Search search =  iterator.next();
           search.searchDoc("hello world");
        }
    }
}

這就是spi的思想,介面的實現由provider實現,provider只用在提交的jar包裡的META-INF/services下根據平臺定義的介面新建檔案,並新增進相應的實現類內容就好。

3、深入理解SPI

SPI機制通常使用流程:
1.定義標準,就是定義介面,比如介面java.sql.Driver
2.廠商或者框架開發者開發具體的實現:在META-INF/services目錄下定義一個名字為介面全限定名的檔案,比如java.sql.Driver檔案,檔案內容是具體的實現名字,比如me.cxis.sql.MyDriver
3.程式設計師使用,引用具體廠商的jar包來實現我們的功能:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
//獲取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍歷
while(driversIterator.hasNext()) {
    driversIterator.next();
    //可以做具體的業務邏輯
}

SPI和API的區別:
1.SPI-“介面”位於“呼叫方”所在的“包”中

-概念上更依賴呼叫方。
-組織上位於呼叫方所在的包中。
-實現位於獨立的包中。
-常見的例子是:外掛模式的外掛。

2.API - “介面”位於“實現方”所在的“包”中

-概念上更接近實現方。
-組織上位於實現方所在的包中。
-實現和介面在一個包中。

4、SPI機制的缺陷

  1. 不能按需載入,需要遍歷所有的實現,並例項化,然後在迴圈中才能找到我們需要的實現。如果不想用某些實現類,或者某些類例項化很耗時,它也被載入並例項化了,這就造成了浪費。
  2. 獲取某個實現類的方式不夠靈活,只能通過 Iterator 形式獲取,不能根據某個引數來獲取對應的實現類。
  3. 多個併發多執行緒使用 ServiceLoader 類的例項是不安全的