Java基礎知識面試題

jcjcjcjiangcheng發表於2020-10-02

java有幾種基本型別,分別是什麼?String是基本型別嗎?

java有8種基本資料型別

資料型別長度
boolean1
short2
byte1
char2
int4
float4
double8
long8

String是基本型別嗎

String是一個類,不是基本資料型別

Final、 static 、this、super關鍵字

Final

用於修飾變數、類,方法

  1. 修飾如果是基本資料型別 變數一旦初始化後便不能修改。如果是引用型別的變數,初始化後不能再指向別的物件。
  2. 修飾類的時候,該類不能被繼承,final類中的所有成員方法都會被隱式地指定為final方法。

static

使用場景

  1. 修飾成員變數和成員方法:被 static 修飾的成員屬於類,不屬於單個這個類的某個物件,被類中所有物件共享,可以並且建議通過類名呼叫。被static 宣告的成員變數屬於靜態成員變數,靜態變數 存放在 Java 記憶體區域的方法區。呼叫格式:類名.靜態變數名 類名.靜態方法名()
  2. 修飾靜態程式碼塊:靜態程式碼塊定義在類中方法外, 靜態程式碼塊在非靜態程式碼塊之前執行(靜態程式碼塊—>非靜態程式碼塊—>構造方法)。 該類不管建立多少物件,靜態程式碼塊只執行一次.
  3. 靜態內部類:靜態內部類與非靜態內部類之間存在一個最大的區別: 非靜態內部類在編譯完成之後會隱含地儲存著一個引用,該引用是指向建立它的外圍類,但是靜態內部類卻沒有。沒有這個引用就意味著它的建立是不需要依賴外圍類的建立和它不能使用任何外圍類的非static成員變數和方法。
  4. 靜態導包:格式為:import static這兩個關鍵字連用可以指定匯入某個類中的指定靜態資源,並且不需要使用類名呼叫類中靜態成員,可以直接使用類中靜態成員變數和成員方法。

this

代表本類物件的引用,this 呼叫本類中的其他構造方法時,要放在首行,否則編譯器會報錯。

super

用於訪問父類變數和方法,在構造器中使用 super()呼叫父類中的其他構造方法時,該語句必須處於構造器的首行,否則編譯器會報錯。

final,finalize, finally有什麼區別

final

用於修飾變數、類,方法

  1. 修飾如果是基本資料型別 變數一旦初始化後便不能修改。如果是引用型別的變數,初始化後不能再指向別的物件。
  2. 修飾類的時候,該類不能被繼承,final類中的所有成員方法都會被隱式地指定為final方法。

finalize

finalize()是在java.lang.Object裡定義的,當垃圾收集確定不再有對該物件的引用時,垃圾收集器在物件上呼叫該物件,完成對該物件回收。 該方法已被廢棄。

finally

finally作為異常處理的一部分,它只能用在try/catch語句中,如果程式進入到try/catch語句中,則大部分情況下finally語句塊都會執行。(若在try/catch語句前發生異常,或者在try/catch語句中使用System.exit(0)則不會執行finally語句)。

String、Stringbuffer和StringBuilder的區別

三者都是final類,不可以繼承。

String

是一個字串常量,長度不可變。每次對String修改都會生成新的String物件,然後指標指向新的物件。若要頻繁改變長度,會導致生成多個物件,JVM的會頻繁垃圾回收,影響系統效能。

StringBuilder

執行緒不安全,長度可變。單執行緒操作大量資料,使用StringBuilder。

StringBuffer

執行緒安全 ,長度可變,每次都會對StringBuffer物件本身進行操作,而不是生成新的物件並改變物件引用。多執行緒操作大量資料使用StringBuffer。

Equals方法重寫時為什麼要重寫hashcode方法

hashcode返回物件的記憶體地址經過處理後的結構,由於每個物件的記憶體地址都不一樣,所以雜湊碼也不一樣

String類的hashCode:根據String類包含的字串的內容,根據一種特殊演算法返回雜湊碼,只要字串內容相同,返回的雜湊碼也相同。

首先,hashmap的key的值是不能相同的,如果新建兩個物件,則他們的hashcode是不同的

T a = new T(1);
T b = new T(1);
//並且結果也為false
a.equals(b)

因此,這兩個物件都可以put進去hashmap。

但是hashmap的性質是,當key的值相同的時候,後一面key會覆蓋前面一個key。

但是b物件並沒有覆蓋a,所以要重寫equals函式 令a.equals(b)==true;

public boolean equals(Object obj) {
        if (this == obj)
            return true;
        else if (!(obj instanceof T)) {
            return false;
        } else {
            T other = (T) obj;
            Integer data1 = this.getA();
            Integer data2 = ((T) obj).getA();
            if (data1 == null) {
                if (data2 != null) {
                    return false;
                }
            } else if (!data1.equals(data2)){
                return false;
            }
        }
        return true;
    }
//重寫後a.equals(b)==true。

但是hashmap在儲存a和b物件的時候,它是存的兩份的,在它看來,這兩個的key是不一樣的,因為它們的雜湊碼就是不一樣的,儘管他們的值相同。Map中存了兩個數值一樣的key,這個問題很嚴重。所以要重寫hashcode方法來返回,令相同值的key,返回相同的hashCode。

public int hashCode() {
        boolean PRIME = true;
        byte result = 1;
        Integer a = this.getA();
        int result1 = result * 59 + (a == null ? 43 : a.hashCode());
        return result1;
    }

重寫hashCode後,相同key的值,會覆蓋前一個key,這樣就保證了key的唯一性

所以在重寫equals方法的時候,一定要重寫hashCode方法。

==和equals的區別

  1. ==是操作符,equals是方法

  2. 當時基本資料型別的時候只能使用==,比較的是它們的值

  3. 對於引用型別的變數來說,如String只能使用equals,因為 String 繼承了 Object 類, equals 是 Object 類的通用方法。對於該型別物件的比較,預設情況下,也就是沒有複寫 Object 類的 equals 方法,使用 == 和 equals 比較是一樣效果的,都是比較的是它們在記憶體中的存放地址。

    • 當equals沒有被重寫,預設使用就是 Object 類的方法
    /*
    從原始碼可以看出,裡面使用的就是 == 比較,所以這種情況下比較的就是它們在記憶體中的存放地址。
    */
    public boolean equals(Object obj) {
          return (this == obj);
      }
    
    • 當equals被重寫,如String類裡面的equeal方法
    /*
    從原始碼可以看出, String 類複寫了 equals 方法,當使用 == 比較記憶體的存放地址,當不相等時接下來會比較字串的內容是否相等,所以String類中的equals方法會比較兩者的字串內容是否一樣。
    */
    public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = value.length;
                if (n == anotherString.value.length) {
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }
    

介面和抽象類的區別

抽象類

抽象方法是一種特殊的方法:它只有宣告,而沒有具體的實現。抽象方法的宣告格式為

abstract void sayHolle()

特點

  1. 抽象方法必須用abstract關鍵字進行修飾,如果一個類含有抽象方法,則稱這個類為抽象類。
  2. 抽象類必須在類前用abstract關鍵字修飾。
  3. 因為抽象類中含有無具體實現的方法,所以不能用抽象類建立物件。
  4. 抽象類可以擁有成員變數和普通的成員方法。

與普通類區別

  1. 抽象方法必須為public或者protected(因為如果為private,則不能被子類繼承,子類便無法實現該方法),預設情況下預設為public。
  2. 不能用來建立物件
  3. 如果一個類繼承於一個抽象類,則子類必須實現父類的抽象方法。如果子類沒有實現父類的抽象方法,則必須將子類也定義為為abstract類。

介面

public interface SayHello{}
  1. 介面中可以含有 變數和方法。
  2. 介面中的變數會被隱式地指定為public static final變數,並且只能是public static final變數,用private修飾會報編譯錯誤。
  3. 方法可以用public abstract或者static表示,若沒有修飾符,則方法會被隱式地指定為public abstract,用其他關鍵字,比如private、protected、 final等修飾會報編譯錯誤,並且介面中所有的方法不能有具體的實現(除了static修飾的方法)。

兩者區別

  1. 抽象類可以沒有抽象方法,也可以抽象方法和非抽象方法並存。而在JKD8前,介面中的方法只能是抽象的,JKD8版本後開始提供了介面中的方法的default實現。
  2. 抽象類中的成員變數可以是各種型別的;而介面中的成員變數只能是public static final型別的,必須被初始化,介面只有常量,沒有變數。
  3. 介面中不能含有靜態程式碼塊,但可以含有靜態方法,而抽象類可以有靜態程式碼塊和靜態方法。
  4. 抽象類和類一樣是單繼承,介面可以實現多個父介面。

JDK和JRE的區別

  • JRE 是 Java 的執行環境。面向 Java 程式的使用者,而不是開發者。如果你僅下載並安裝了 JRE,那麼你的系統只能執行 Java 程式。JRE 是執行 Java 程式所必須環境的集合,包含 JVM 標準實現及 Java 核心類庫。它包括 Java 虛擬機器、Java 平臺核心類和支援檔案
  • JDK是 Java 開發工具包,它提供了 Java 的開發環境(提供了編譯器 javac 等工具,用於將 java 檔案編譯為 class 檔案)和執行環境(提 供了 JVM 和 Runtime 輔助包,用於解析 class 檔案使其得到執行)。如果你下載並安裝了 JDK,那麼你不僅可以開發 Java 程式,也同時擁有了執行 Java 程式的平臺。JDK 是整個 Java 的核心,包括了 Java 執行環境JRE,一堆 Java 工具 tools.jar 和 Java 標準類庫 。

深拷貝、淺拷貝

淺拷貝

淺拷貝是建立一個新物件,這個物件有著原始物件屬性值的一份精確拷貝。如果屬性是基本型別,拷貝的就是基本型別的值,如果屬性是引用型別,拷貝的就是記憶體地址 ,所以如果其中一個物件改變了這個地址,就會影響到另一個物件。

深拷貝

深拷貝是將一個物件從記憶體中完整的拷貝一份出來,從堆記憶體中開闢一個新的區域存放新物件,且修改新物件不會影響原物件。

Java中為何要有泛型

泛型的本質是引數化型別,即所操作的資料型別被指定為一個引數。這種型別引數可以用在類、介面和方法的建立中,分別稱為泛型類、泛型介面、泛型方法,目的是為了可以在編譯器防止將錯誤型別的物件放置到容器中

沒使用泛型

public class Gerbil {
    Object gerbilNumber;
    public Gerbil(Object gerbilNumber) {
        this.gerbilNumber = gerbilNumber;
    }

    public Object getGerbilNumber() {
        return gerbilNumber;
    }

    public void setGerbilNumber(Object gerbilNumber) {
        this.gerbilNumber = gerbilNumber;
    }

    public void showType() {
        System.out.println("實際型別為" + gerbilNumber.getClass().getName());
    }
}
public class Test {
    public static void main(String[] args) {
        Gerbil gerbil1 = new Gerbil(new Integer(1));
        gerbil1.showType();
        int i = (int) gerbil1.getGerbilNumber();
        System.out.println("value=" + i);

        Gerbil gerbil2 = new Gerbil("1");
        gerbil2.showType();
        String j = (String) gerbil2.getGerbilNumber();
        System.out.println("value=" + j);
       
    }
}

/**
執行結果:
實際型別為java.lang.Integer
value=1
實際型別為java.lang.String
value=1
**/

在沒有泛型的情況下,通過對型別Object的引用來實現引數的轉換,這種轉換是需要顯式地強制型別轉換,給開發帶來了麻煩。

使用泛型

public class Gerbil<T> {
    T gerbilNumber;
    public Gerbil(T gerbilNumber) {
        this.gerbilNumber = gerbilNumber;
    }

    public T getGerbilNumber() {
        return gerbilNumber;
    }

    public void setGerbilNumber(T gerbilNumber) {
        this.gerbilNumber = gerbilNumber;
    }

    public void showType() {
        System.out.println("實際型別為" + gerbilNumber.getClass().getName());
    }
}

public class Test {
    public static void main(String[] args) {
        Gerbil<Integer> gerbil1 = new Gerbil(1);
        gerbil1.showType();
        int i = gerbil1.getGerbilNumber();
        System.out.println("value=" + i);

        Gerbil<String> gerbil2 = new Gerbil("1");
        gerbil2.showType();
        String j =gerbil2.getGerbilNumber();
        System.out.println("value=" + j);
    }
}

/**
實際型別為java.lang.Integer
value=1
實際型別為java.lang.String
value=1
**/

使用泛型可以消除強制型別轉換。

泛型擦除

public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);//true
    }

程式內部會把ArrayList和ArrayList 擦除成原生的ArrayList,所以輸出true。

泛型的好處

  1. 型別安全:泛型的主要目標是提高 Java 程式的型別安全。通過知道使用泛型定義的變數的型別限制,編譯器可以在一個高得多的程度上驗證型別假設。
  2. 消除強制型別轉換: 泛型的一個附帶好處是,消除原始碼中的許多強制型別轉換。這使得程式碼更加可讀,並且減少了出錯機會。
  3. 潛在的效能收益:泛型為較大的優化帶來可能。在泛型的初始實現中,編譯器將強制型別轉換(沒有泛型的話,要指定這些強制型別轉換)插入生成的位元組碼中。但是更多型別資訊可用於編譯器這一事實,為未來版本的 JVM 的優化帶來可能。由於泛型的實現方式,支援泛型(幾乎)不需要 JVM 或類檔案更改。所有工作都在編譯器中完成,編譯器生成類似於沒有泛型和強制型別轉換時所寫的程式碼,更能確保型別安全。

方法重寫與過載的區別?

重寫:子類繼承了父類原有的方法,但有時子類並不想原封不動的繼承父類中的某個方法,所以在方法名,引數列表,返回型別(除過子類中方法的返回值是父類中方法返回值的子類時)都相同的情況下, 對方法體進行修改或重寫覆蓋,這就是重寫。但要注意子類函式的訪問修飾許可權不能少於父類的

過載

在一個類中,同名的方法如果有不同的引數列表(引數型別不同、引數個數不同甚至是引數順序不同)則視為過載。同時,過載對返回型別沒有要求,可以相同也可以不同,但不能通過返回型別是否相同來判斷過載

Java類初始化順序

public class Parent {
    public static String prent_value = "父類靜態變數";
    public String value = "父類普通變數";
    static {
        System.out.println(prent_value);
        System.out.println("父類靜態塊");
    }

    public Parent() {
        System.out.println(value);
        System.out.println("父類構造方法");
    }

}

public class Children extends Parent {
    public static String children_value = "子類靜態變數";
    public String value = "子類普通變數";
    static {
        System.out.println(children_value);
        System.out.println("子類靜態塊");
    }

    public Children() {
        System.out.println(value);
        System.out.println("子類構造方法");
    }

    public static void main(String[] args) {
        new Children();
    }
}
/**
父類靜態變數
父類靜態塊
子類靜態變數
子類靜態塊
父類普通變數
父類構造方法
子類普通變數
子類構造方法
**/

由輸出結果可以得知,初始化順序為, 父類靜態變數->父類靜態塊->子類靜態變數->子類靜態塊->父類普通變數->父類構造方法->子類普通變數->子類構造方法

java的三大特性 物件導向的特性(封裝、繼承、多型) 的含義

封裝

把事物封裝成一個類,減少耦合,隱藏細節,保持特定的介面與外界聯絡,當介面內部發生變化,不會影響外部呼叫方。

繼承

從一個已知的類派生出一個新的類,新類可以擁有已知的類所擁有的屬性和行為,並且可以通過覆蓋/重寫來增強已知類的能力。

多型

本質就是一個程式存在多個同名的不同方法,主要有三種方式實現

  1. 通過子類對父類的覆蓋。
  2. 通過在一個類中對方法進行過載。
  3. 通過將子類物件作為父類物件使用來實現

多型的實現原理

覆蓋

覆蓋也叫重寫,是子類與父類之間的一種關係。

public class Test {
    class Parent {
        public void say() {
            System.out.println("我是Parent");
        }
    }

    class Son extends Parent {
        @Override
        public void say() {
            System.out.println("覆蓋了父類方法,我是Son");
        }
    }

    public void test() {
        new Son().say();
    }
    public static void main(String[] args) {
        new Test().test();
    }
}

/**
覆蓋了父類方法,我是Son
**/

過載

是指一個類中(包括父類),存在多個同名的不同方法,這些方法的引數個數,順序以及型別不同都可以構成方法的過載,如果兩個方法僅僅是修飾符,返回值,丟擲異常不同,那麼這兩個是相同的方法。

public class Test {
    public void say(int number) {
        System.out.println("序號 "+number);
    }
    //引數個數相同,但是型別不同
    public void say(String name) {
        System.out.println(" 姓名 "+name);
    }
    public void say(int number,String name) {
        System.out.println("序號 "+number+"  姓名 "+name);
    }

    //引數順序不同
    public void say(String name, int number) {
        System.out.println("序號 " + number + "  姓名 " + name);
    }

    //引數個數不同
    public void say(int number, String name, int sex) {
        String temp;
        if (sex == 1) {
            temp = "男";
        } else {
            temp = "女";
        }
        System.out.println("序號 "+number+"  姓名 "+name+" 性別 "+temp);
    }
    public static void main(String[] args) {
        new Test().say(1);
        new Test().say(1, "Mick");
        new Test().say(1,"Mack",1);
    }
}
  • 注意,返回型別不同不能作為函式過載的條件。

子類物件作為父類物件

將不同子類作為父類去看待,可以遮蔽不同子類之間的差異,寫出通用的程式碼。物件的引用型變數具有多型性,因為一個引用變數可以指向不同形式的物件,即子類物件可以作為父類物件使用。主要是向上轉型和向下轉型

向上轉型

子類專為父類,父類可以是介面。

Praent p=new Son();Praent 是父類或者介面,Son是子類

/**常用例子
*/
List<Integer> list = new ArrayList<>();
List<Integer> list1 = new LinkedList<>();
List<Integer> list2 = new ArrayQueue<>(1);

向下轉型

父類物件轉為子類

Son s=(Son)p;

向上轉型的時候可以直接轉,但是必須要強制型別轉換,該父類必須實際指向了一個子類物件才可以強制型別向下轉型。

  • 若Parent p=new Son() 建立父類物件,可以通過強制型別轉換實現向下轉型。但是若是Parent p=new Parent()這種方式建立的父類物件,不可以通過強制型別轉換實現向下轉型為Son物件,執行報錯,因為其本質還是一個Parent物件。

Java的訪問許可權,預設是哪個

privatefriendprotectPublic
同一個類的成員YYYY
同一個包中的成員YYY
不同包中但存在繼承關係的子類成員YY
全域性Y
  • private:只有同一類內部的方法可見,在有就是內部類也可以訪問到。
  • 預設(friendly):包內可見。
  • protected:不同包的時候繼承可見。
  • public:所有類可見。

預設是friend

Java中類修飾符、成員變數修飾符、方法修飾符的種類與類別

類修飾符

  1. public:將一個類宣告為公共類,他可以被任何物件訪問,一個程式的主類必須是公共類
  2. abstract:將一個類宣告為抽象類,沒有實現的方法,需要子類提供方法實現。
  3. 將一個類生命為最終(即非繼承類),表示他不能被其他類繼承。
  4. friendly:預設的修飾符,只有在相同包中的物件才能使用這樣的類。

成員變數修飾符

  1. public:指定該變數為公共,可以被任何物件的方法訪問。
  2. private:指定該變數只能自己的類的方法訪問,其他類的方法均不可以訪問。
  3. protect:指定該變數可以別被同一個包內,自己的類和不同包的子類訪問。在子類中可以覆蓋此變數。
  4. friendly:在同一個包中的類可以訪問,其他包中的類不能訪問。
  5. static:靜態修飾符,指定該變數被所有物件共享,即所有例項都可以使用該變數,即變數屬於這個類
  6. final:最終修飾符,指定該變數的值不能變
  7. transient:過度修飾符,指定該變數是系統保留,暫無特別作用的臨時性變數。
  8. volatile:指定該變數具有可見性和防止指令重排的功能,但不保證原子性

方法修飾符的種類

  1. public:所有類都可以訪問。
  2. private:該方法只能被它自己的類訪問,其他類不能訪問。
  3. protected:該方法可以被它的類和子類訪問。
  4. final:該方法不能過載。
  5. static:指定不需要例項化就可以啟用的一個方法。
  6. synchronize:同步修飾符,防止其他執行緒的訪問,等方法執行結束後才解鎖。
  7. native:本地修飾符。指定此方法的方法體是用其他語言在程式外部編寫的。

什麼是記憶體洩漏,Java中存在記憶體洩漏嗎

  • Java中存在記憶體洩漏。記憶體洩漏就是當長生命週期的物件持有短生命週期物件的引用就很可能發生記憶體洩露,儘管短生命週期物件已經不再需要,但是因為長生命週期物件持有它的引用而導致不能被回收。

具體發生場景

  1. 集合類:如果集合類如果僅僅是區域性變數,不會造成記憶體洩露,因為在方法棧退出後就沒有引用了會被jvm正常回收。但是如果這個集合類是全域性性的變數(比如類中的靜態屬性,全域性性的map等即有靜態引用或final一直指向它),那麼沒有相應的刪除機制,很可能導致集合所佔用的記憶體只增不減,因此提供這樣的刪除機制或者定期清除策略非常必要。
  2. 單例模式:單例物件在被初始化後將在JVM的整個生命週期中存在(以靜態變數的方式),如果單例物件持有外部物件的引用,當外部物件不再使用的時候,但是這個單例持有它的引用,導致這個外部物件將不能被jvm正常回收,導致記憶體洩露。

RuntimeException有哪些,怎麼避免空指標異常

RuntimeException主要有

  1. ArithmeticException:數學計算異常。
  2. NullPointerException:空指標異常。
  3. NegativeArraySizeException:負陣列長度異常。
  4. ArrayOutOfBoundsException:陣列索引越界異常。
  5. ClassNotFoundException:類檔案未找到異常。
  6. ClassCastException:型別強制轉換異常。
  7. SecurityException:違背安全原則異常。

非RuntimeException型別主要有

  1. 其他非RuntimeException型別的常見異常主要有以下幾種。
  2. NoSuchMethodException:方法未找到異常。
  3. IOException:輸入輸出異常。
  4. EOFException:檔案已結束異常。
  5. FileNotFoundException:檔案未找到異常。
  6. NumberFormatException:字串轉換為數字異常。
  7. SQLException:運算元據庫異常

怎麼避免空指標異常

每個變數要初始化,並且每次執行的時候對變數,物件都要做判空處理。

反射了解嗎,應用場景

反射是Java的特徵之一,是一種間接操作目標物件的機制,核心是JVM在執行的時候才動態載入類,並且對於任意一個類,都能夠知道這個類的所有屬性和方法,呼叫方法/訪問屬性,不需要提前在編譯期知道執行的物件是誰,他允許執行中的Java程式獲取類的資訊,並且可以操作類或物件內部屬性。程式中物件的型別一般都是在編譯期就確定下來的,而當我們的程式在執行時,可能需要動態的載入一些類,這些類因為之前用不到,所以沒有載入到jvm,這時,使用Java反射機制可以在執行期動態的建立物件並呼叫其屬性,它是在執行時根據需要才載入。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-Q0hwmtAC-1601623069458)(/Users/jiangcheng/blog/source/_posts/img/反射.png)]

反射的優缺點

優點:使用反射,我們就可以在執行時獲得類的各種內容,進行反編譯,對於Java這種先編譯再執行的語言,能夠讓我們很方便的建立靈活的程式碼,這些程式碼可以在執行時裝配,無需在元件之間進行原始碼的連結,更加容易實現物件導向。

缺點:(1)反射會消耗一定的系統資源,因此,如果不需要動態地建立一個物件,那麼就不需要用反射;

​ (2)反射呼叫方法時可以忽略許可權檢查,因此可能會破壞封裝性而導致安全問題。

應用場景

舉例:比如說我和程式設計師A一起完成一個專案,上司規定了我們的工作(框架),於是乎我們各自為戰。我們要保證自己的程式碼能夠編譯成功再交工給上司,但是,我的程式需要呼叫A要完成的那部分的方法,如果我不利用反射機制,就必須要等A完工了才能編譯測試。顯然,這在工作中是不現實的(不僅僅是時間問題)。有了反射之後,我只需要知道介面就OK了,方法沒有實現也不會影響我的工作。

JDK動態代理原理

JDK動態代理只提供介面的代理,不支援類的代理。核心InvocationHandler介面和Proxy類,InvocationHandler 通過invoke()方法反射來呼叫目標類中的程式碼,動態地將橫切邏輯和業務編織在一起;接著,Proxy利用 InvocationHandler動態建立一個符合某一介面的的例項, 生成目標類的代理物件。

Arrays.sort()的實現

Arrays.sort()底層原始碼

static void sort(int[] a, int left, int right,
                 int[] work, int workBase, int workLen) {
    /**INSERTION_SORT_THRESHOLD常量為286,如果a陣列長度小於INSERTION_SORT_THRESHOLD就使用快速排序,否則用歸併排序。
    **/
    if (right - left < QUICKSORT_THRESHOLD) {
      //進入到sort方法中,
        sort(a, left, right, true);
        return;
    }

    int[] run = new int[MAX_RUN_COUNT + 1];
    int count = 0; run[0] = left;

    // 判斷該陣列是否接近排序完成
    for (int k = left; k < right; run[count] = k) {
        if (a[k] < a[k + 1]) { // ascending
            while (++k <= right && a[k - 1] <= a[k]);
        } else if (a[k] > a[k + 1]) { // descending
            while (++k <= right && a[k - 1] >= a[k]);
            for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {
                int t = a[lo]; a[lo] = a[hi]; a[hi] = t;
            }
        } else { // equal
            for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) {
                if (--m == 0) {
                    sort(a, left, right, true);
                    return;
                }
            }
        }

        /*若count == MAX_RUN_COUNT,則證明陣列並非高度結構化, 則呼叫前文所述私有sort方法進行
         * 快速排序
         */
        if (++count == MAX_RUN_COUNT) {
            sort(a, left, right, true);
            return;
        }
    }

    
    
    if (run[count] == right++) { 
        run[++count] = right;
    } else if (count == 1) { // 陣列已經排序好,直接返回。
        return;
    }

    // 歸併排序
    byte odd = 0;
    for (int n = 1; (n <<= 1) < count; odd ^= 1);

    int[] b;                 
    int ao, bo;              
    int blen = right - left;
    if (work == null || workLen < blen || workBase + blen > work.length) {
        work = new int[blen];
        workBase = 0;
    }
    if (odd == 0) {
        System.arraycopy(a, left, work, workBase, blen);
        b = a;
        bo = 0;
        a = work;
        ao = workBase - left;
    } else {
        b = work;
        ao = 0;
        bo = workBase - left;
    }

    // 合併
    for (int last; count > 1; count = last) {
        for (int k = (last = 0) + 2; k <= count; k += 2) {
            int hi = run[k], mi = run[k - 1];
            for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) {
                if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) {
                    b[i + bo] = a[p++ + ao];
                } else {
                    b[i + bo] = a[q++ + ao];
                }
            }
            run[++last] = hi;
        }
        if ((count & 1) != 0) {
            for (int i = right, lo = run[count - 1]; --i >= lo;
                b[i + bo] = a[i + ao]
            );
            run[++last] = right;
        }
        int[] t = a; a = b; b = t;
        int o = ao; ao = bo; bo = o;
    }
}

Arrays.sort()方法裡面的sort部分原始碼

private static void sort(int[] a, int left, int right, boolean leftmost) {
        int length = right - left + 1;
				//INSERTION_SORT_THRESHOLD為47
        //再次判斷a陣列長度是否大於47,若大於47使用快速排序,小於47使用插入排序
        if (length < INSERTION_SORT_THRESHOLD) {
            if (leftmost) {
                //
                for (int i = left, j = i; i < right; j = ++i) {
                    int ai = a[i + 1];
                    while (ai < a[j]) {
                        a[j + 1] = a[j];
                        if (j-- == left) {
                            break;
                        }
                    }
                    a[j + 1] = ai;
                }
            } else {
                /*
                 * 跳過最長的生序序列
                 */
                do {
                    if (left >= right) {
                        return;
                    }
                } while (a[++left] >= a[left - 1]);

               
				//雙基準快排的開始
        // 通過位運算獲取陣列長度的1/7的近似值
        int seventh = (length >> 3) + (length >> 6) + 1;

        /*
         * 把陣列分為三段,有兩個基準元素
         */
        int e3 = (left + right) >>> 1; // The midpoint
        int e2 = e3 - seventh;
        int e1 = e2 - seventh;
        int e4 = e3 + seventh;
        int e5 = e4 + seventh;

        // Sort these elements using insertion sort
        if (a[e2] < a[e1]) { int t = a[e2]; a[e2] = a[e1]; a[e1] = t; }

        if (a[e3] < a[e2]) { int t = a[e3]; a[e3] = a[e2]; a[e2] = t;
            if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }
        }
        if (a[e4] < a[e3]) { int t = a[e4]; a[e4] = a[e3]; a[e3] = t;
            if (t < a[e2]) { a[e3] = a[e2]; a[e2] = t;
                if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }
            }
        }
        if (a[e5] < a[e4]) { int t = a[e5]; a[e5] = a[e4]; a[e4] = t;
            if (t < a[e3]) { a[e4] = a[e3]; a[e3] = t;
                if (t < a[e2]) { a[e3] = a[e2]; a[e2] = t;
                    if (t < a[e1]) { a[e2] = a[e1]; a[e1] = t; }
                }
            }
        }
        // Pointers
        int less  = left;  // The index of the first element of center part
        int great = right; // The index before the first element of right part

        if (a[e1] != a[e2] && a[e2] != a[e3] && a[e3] != a[e4] && a[e4] != a[e5]) {
            
            int pivot1 = a[e2];
            int pivot2 = a[e4];

            a[e2] = a[left];
            a[e4] = a[right];

            /*
             * Skip elements, which are less or greater than pivot values.
             */
            while (a[++less] < pivot1);
            while (a[--great] > pivot2);

            /*
             * Partitioning:
             *
             *   left part           center part                   right part
             * +--------------------------------------------------------------+
             * |  < pivot1  |  pivot1 <= && <= pivot2  |    ?    |  > pivot2  |
             * +--------------------------------------------------------------+
             *               ^                          ^       ^
             *               |                          |       |
             *              less                        k     great
             *
             * Invariants:
             *
             *              all in (left, less)   < pivot1
             *    pivot1 <= all in [less, k)     <= pivot2
             *              all in (great, right) > pivot2
             *
             * Pointer k is the first index of ?-part.
             */
            outer:
            for (int k = less - 1; ++k <= great; ) {
                int ak = a[k];
                if (ak < pivot1) { // Move a[k] to left part
                    a[k] = a[less];
                    a[less] = ak;
                    ++less;
                } else if (ak > pivot2) { // Move a[k] to right part
                    while (a[great] > pivot2) {
                        if (great-- == k) {
                            break outer;
                        }
                    }
                    if (a[great] < pivot1) { // a[great] <= pivot2
                        a[k] = a[less];
                        a[less] = a[great];
                        ++less;
                    } else { // pivot1 <= a[great] <= pivot2
                        a[k] = a[great];
                    }
                   
                    a[great] = ak;
                    --great;
                }
            }

            a[left]  = a[less  - 1]; a[less  - 1] = pivot1;
            a[right] = a[great + 1]; a[great + 1] = pivot2;

            sort(a, left, less - 2, leftmost);
            sort(a, great + 2, right, false);

            /*
             * If center part is too large (comprises > 4/7 of the array),
             * swap internal pivot values to ends.
             */
            if (less < e1 && e5 < great) {
                /*
                 * Skip elements, which are equal to pivot values.
                 */
                while (a[less] == pivot1) {
                    ++less;
                }

                while (a[great] == pivot2) {
                    --great;
                }

                /*
                 * Partitioning:
                 *
                 *   left part         center part                  right part
                 * +----------------------------------------------------------+
                 * | == pivot1 |  pivot1 < && < pivot2  |    ?    | == pivot2 |
                 * +----------------------------------------------------------+
                 *              ^                        ^       ^
                 *              |                        |       |
                 *             less                      k     great
                 *
                 * Invariants:
                 *
                 *              all in (*,  less) == pivot1
                 *     pivot1 < all in [less,  k)  < pivot2
                 *              all in (great, *) == pivot2
                 *
                 * Pointer k is the first index of ?-part.
                 */
                outer:
                for (int k = less - 1; ++k <= great; ) {
                    int ak = a[k];
                    if (ak == pivot1) { // Move a[k] to left part
                        a[k] = a[less];
                        a[less] = ak;
                        ++less;
                    } else if (ak == pivot2) { // Move a[k] to right part
                        while (a[great] == pivot2) {
                            if (great-- == k) {
                                break outer;
                            }
                        }
                        if (a[great] == pivot1) { // a[great] < pivot2
                            a[k] = a[less];
                            /*
                             * Even though a[great] equals to pivot1, the
                             * assignment a[less] = pivot1 may be incorrect,
                             * if a[great] and pivot1 are floating-point zeros
                             * of different signs. Therefore in float and
                             * double sorting methods we have to use more
                             * accurate assignment a[less] = a[great].
                             */
                            a[less] = pivot1;
                            ++less;
                        } else { // pivot1 < a[great] < pivot2
                            a[k] = a[great];
                        }
                        a[great] = ak;
                        --great;
                    }
                }
            }

            // Sort center part recursively
            sort(a, less, great, false);

        } else { // Partitioning with one pivot
            /*
             * Use the third of the five sorted elements as pivot.
             * This value is inexpensive approximation of the median.
             */
            int pivot = a[e3];

            /*
             * Partitioning degenerates to the traditional 3-way
             * (or "Dutch National Flag") schema:
             *
             *   left part    center part              right part
             * +-------------------------------------------------+
             * |  < pivot  |   == pivot   |     ?    |  > pivot  |
             * +-------------------------------------------------+
             *              ^              ^        ^
             *              |              |        |
             *             less            k      great
             *
             * Invariants:
             *
             *   all in (left, less)   < pivot
             *   all in [less, k)     == pivot
             *   all in (great, right) > pivot
             *
             * Pointer k is the first index of ?-part.
             */
            for (int k = less; k <= great; ++k) {
                if (a[k] == pivot) {
                    continue;
                }
                int ak = a[k];
                if (ak < pivot) { // Move a[k] to left part
                    a[k] = a[less];
                    a[less] = ak;
                    ++less;
                } else { // a[k] > pivot - Move a[k] to right part
                    while (a[great] > pivot) {
                        --great;
                    }
                    if (a[great] < pivot) { // a[great] <= pivot
                        a[k] = a[less];
                        a[less] = a[great];
                        ++less;
                    } else { // a[great] == pivot
                        /*
                         * Even though a[great] equals to pivot, the
                         * assignment a[k] = pivot may be incorrect,
                         * if a[great] and pivot are floating-point
                         * zeros of different signs. Therefore in float
                         * and double sorting methods we have to use
                         * more accurate assignment a[k] = a[great].
                         */
                        a[k] = pivot;
                    }
                    a[great] = ak;
                    --great;
                }
            }
            sort(a, left, less - 1, leftmost);
            sort(a, great + 1, right, false);
        }
    }

Java8 有什麼新特性

  • 介面可以使用預設方法和靜態方法
public interface Animal {
    default void run(){
        System.out.println("run!");
    }
  static void jump(){
    System.out.println("jump!");
  }
}

  • 函式式介面FunctionInterface與lambda表示式

函式式介面FunctionInterface引入了函式式思想,也就是說函式可以作為另一個函式的引數。函式式介面,介面中有且僅有一個抽象方法,但是可以有若干個預設方法,如Runnable,Callable介面就是典型的函式式介面。可以使用@FunctionalInterface註解,宣告一個介面是函式式介面。

@java.lang.FunctionalInterface
public interface FunctionalInterface {
   abstract void handle();
}

lambda表示式,是一段沒有函式名的函式體,可以作為引數直接傳遞給相關的呼叫者。lambda表示式極大的增加了Java語言的表達能力。有以下特點

  1. 可選型別宣告:不需要宣告引數型別,編譯器可以統一識別引數值。
  2. 可選的引數圓括號:一個引數無需定義圓括號,但多個引數需要定義圓括號。
  3. 可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
  4. 可選的返回關鍵字:如果主體只有一個表示式返回值則編譯器會自動返回值,大括號需要指定明表示式返回了一個數值。
public class Test {
    public static void main(String args[]){
        Test tester = new Test();

        // 型別宣告
        MathOperation addition = (int a, int b) -> a + b;

        // 不用型別宣告
        MathOperation subtraction = (a, b) -> a - b;

        // 大括號中的返回語句
        MathOperation multiplication = (int a, int b) -> { return a * b; };

        // 沒有大括號及返回語句
        MathOperation division = (int a, int b) -> a / b;

        System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
        System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
        System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
        System.out.println("10 / 5 = " + tester.operate(10, 5, division));

        // 不用括號
        GreetingService greetService1 = message ->
                System.out.println("Hello " + message);

        // 用括號
        GreetingService greetService2 = (message) ->
                System.out.println("Hello " + message);

        greetService1.sayMessage("Runoob");
        greetService2.sayMessage("Google");
    }

    interface MathOperation {
        int operation(int a, int b);
    }

    interface GreetingService {
        void sayMessage(String message);
    }

    private int operate(int a, int b, MathOperation mathOperation){
        return mathOperation.operation(a, b);
    }
}

  • 方法引用

方法引用是為了進一步簡化lambda表示式,通過類名或者例項名與方法名的組合來直接訪問到類或者例項已經存在的方法或者構造方法。方法引用使用**::來定義,:?*的前半部分表示類名或者例項名,後半部分表示方法名,如果是構造方法就使用NEW來表示。主要有以下幾種形式

  1. 靜態方法引用:ClassName::methodName;
  2. 例項上的例項方法引用:instanceName::methodName;
  3. 超類上的例項方法引用:supper::methodName;
  4. 類的例項方法引用:ClassName:methodName;
  5. 構造方法引用Class:new;
  6. 陣列構造方法引用::TypeName[]::new
public class Test {
    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = User.create(User::new);
            users.add(user);
        }
        users.forEach(User::showUser);
    }

    @FunctionalInterface
    interface Factory<T> {
        T create();
    }
    static class User {
        public void showUser() {
            System.out.println(this.toString());
        }

        public static User create(Factory<User> factory) {
            return factory.create();
        }
    }

}

  • Stream

Java8中有一種新的資料處理方式,那就是流Stream,結合lambda表示式能夠更加簡潔高效的處理資料。Stream使用一種類似於SQL語句從資料庫查詢資料的直觀方式,對資料進行如篩選、排序以及聚合等多種操作。

生成Stream的方法

  1. 從介面Collection中和Arrays
    1. Collection.stream();
    2. Collection.parallelStream(); //相較於序列流,並行流能夠大大提升執行效率
    3. Arrays.stream(T array);
  2. Stream中的靜態方法
    1. Stream.of();
    2. generate(Supplier s);
    3. iterate(T seed, UnaryOperator f);
    4. empty();
  • Optional

為了解決空指標異常,在Java8之前需要使用if-else這樣的語句去防止空指標異常,而在Java8就可以使用Optional來解決。Optional可以理解成一個資料容器,甚至可以封裝null,並且如果值存在呼叫isPresent()方法會返回true。

建立物件的幾種方式

  1. 使用New方法建立物件

    執行這條語句時JVM首先在方法區的常量池中檢視是否有new 後面引數(也就是類名)的符號引用,並檢查是否有類的載入資訊也就是是否被載入解析和初始化過。如果已經載入過了就不在載入,否則執行類的載入全過程。過程如下例子Instance instance=new Instance()

    1. 給例項分配記憶體:此記憶體中存放物件自己的例項變數和從父類繼承過來的例項變數(即使這些從超類繼承過來的例項變數有可能被隱藏也會被分配空間),同時這些例項變數被賦予預設值(零值)
    2. 呼叫建構函式,初始化成員欄位:在Java物件初始化過程中,主要涉及三種執行物件初始化的結構,分別是例項變數初始化**、**例項程式碼塊初始化以及建構函式初始化。
    3. 設定instance物件指向剛分配的記憶體空間: 注意:new操作不是原子操作,2和3的順序可能會調換,導致引用型變數指向一個不為null但是也不完整的物件(可以通過volatile禁止指令重排)。
  2. 使用clone()方法建立物件

    1. clone就是克隆,也就是複製;使用某個物件的clone()方法時(前提是此物件的對應的類中已經實現clone方法),JVM根據被拷貝的物件分配記憶體、建立新的物件,然後會把被clone的物件的值全都拷貝進去;clone()方法是屬於Object類的,clone是在堆記憶體中用二進位制的方式進行拷貝,重新分配給物件一塊記憶體;Object類的clone方法是一個native方法。

    2. Object類的clone()方法是執行緒不安全的。

    3. clone()有淺拷貝和深拷貝兩種模式。淺拷貝是拷貝被拷貝物件的值,若被拷貝物件的屬性有引用型別的,則只拷貝引用的地址;深拷貝是拷貝被拷貝物件的所有值(深層),若有被拷貝物件有引用型別的屬性,則也要拷貝其引用型別的屬性所對應的物件。

      • 淺拷貝

        public class Parent implements Cloneable {
            public String name;
        
            public Child child;
        
            @Override
            protected Object clone() throws CloneNotSupportedException {
                return super.clone();
            }
        
        
            static class Child {
                String name;
            }
        
            public static void main(String[] args) throws CloneNotSupportedException {
                Parent parent1 = new Parent();
                parent1.name = "a";
                parent1.child = new Child();
                parent1.child.name = "a child";
                Parent parent2 = (Parent) parent1.clone();
                System.out.println("parent1 == parent2:"+(parent1 == parent2));
                System.out.println("parent1 hashCode() " + parent1.hashCode());
                System.out.println("parent2 hashCode() " + parent2.hashCode());
        
                System.out.println("parent1 child== parent2 child:" + (parent1.child == parent2.child));
                System.out.println("parent1 child hashCode() " + parent1.child.hashCode());
                System.out.println("parent2 child hashCode() " + parent2.child.hashCode());
                
            }
        }
        
        /**  輸出
        parent1 == parent2:false
        parent1 hashCode() 1639705018
        parent2 hashCode() 1627674070
        parent1 child== parent2 child:true
        parent1 child hashCode() 1360875712
        parent2 child hashCode() 1360875712
        
        從最後child的輸出可以看到,parent1 和 parent2的child實際上指向同一個物件,引用型別是傳遞引用。
        */
        
      • 深拷貝

      public class Parent implements Cloneable {
          public String name;
      
          public Child child;
      
          @Override
          protected Object clone() throws CloneNotSupportedException {
              Parent parent = (Parent) super.clone();
              parent.child = (Child) this.child.clone();
              return parent;
          }
      
      
          static class Child implements Cloneable{
              String name;
      
              @Override
              protected Object clone() throws CloneNotSupportedException {
                  return super.clone();
              }
          }
      
          public static void main(String[] args) throws CloneNotSupportedException {
              Parent parent1 = new Parent();
              parent1.name = "a";
              parent1.child = new Child();
              parent1.child.name = "a child";
              Parent parent2 = (Parent) parent1.clone();
              System.out.println("parent1 == parent2:"+(parent1 == parent2));
              System.out.println("parent1 hashCode() " + parent1.hashCode());
              System.out.println("parent2 hashCode() " + parent2.hashCode());
      
              System.out.println("parent1 child== parent2 child:" + (parent1.child == parent2.child));
              System.out.println("parent1 child hashCode() " + parent1.child.hashCode());
              System.out.println("parent2 child hashCode() " + parent2.child.hashCode());
      
          }
      }
      
      /**
      輸出
      parent1 == parent2:false
      parent1 hashCode() 1639705018
      parent2 hashCode() 1627674070
      parent1 child== parent2 child:false
      parent1 child hashCode() 1360875712
      parent2 child hashCode() 1625635731
      
      可以看到,對child也進行了一次拷貝,實際對於child是淺拷貝,但是對於parent是深拷貝。
      /
      
  3. 使用反射方法建立物件

    1. 獲取類的Class物件例項,獲取方式主要有
      1. Class.forName(“類全路徑”);
      2. 類名.class; 如:Animal.class;
      3. 物件名.getClass();
    2. 通過反射建立類物件的例項物件;獲取Class物件例項後,可以通過Java反射機制建立類物件例項物件;主要有兩種方式:
      1. Class.newInstance():呼叫無參的構造方法,必需確保類中有無引數的可見的建構函式,否則將會丟擲異常;
      2. 呼叫類物件的構造方法。
    3. 強制轉換成使用者所需型別。
    public class User {
        private int age;
        private String name;
        public User(int age, String name) {
            this.age = age;
            this.name = name;
        }
        public User() {
        }
        public void setAge(int age) {
            this.age = age;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "User{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }
        public static void main(String[] args) {
            //正常new 物件
            User user = new User(1, "user1");
            System.out.println(user);
            //使用反射建立物件
            try {
                Class cla = Class.forName("reflect.User");
                Constructor constructor = cla.getConstructor();
                //根據Constructor物件的newInstance方法獲取反射類物件
                Object user2 = constructor.newInstance();
                System.out.println(user2);
                //強制型別轉換
                User user3 = (User) user2;
                user3.setName("user3");
                user3.setAge(3);
                System.out.println(user3);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    

元註解

元註解是用於修飾註解的註解,通常用在註解的定義上,一般用於指定某個註解生命週期以及作用目標等資訊,Java有四種元註解 。

  1. @Target

    說明註解所修飾的物件範圍,關鍵原始碼如下:

    public @interface Target {  
        ElementType[] value();  
    }  
    public enum ElementType {  
      TYPE,FIELD,METHOD,PARAMETED,CONSTRUCTOR,LOCAL_VARIABLE,ANNOCATION_TYPE,PACKAGE,TYPE_PARAMETER,TYPE_USE  
    }  
    /**
    	Type 作用在類/介面
    	FIELD 作用在變數 
    	METHOD 作用在方法
    	PARAMETER 作用在方法引數上
    	CONSTRUCTOR 作用在構造器上
    	LOCAL_VARIABLE 作用在本地區域性變數上
    	ANNOTATION_TYPE 允許作用在註解上
    	PACKAGE 允許作用在包上
    /
    
  2. @Retention(保留策略)

    保留策略定義了該註解被保留的時間長短。關鍵原始碼如下:

    public @interface Retention {  
        RetentionPolicy value();  
    }  
    public enum RetentionPolicy {  
        SOURCE, CLASS, RUNTIME  
    } 
    /**
    	SOURCE:表示在原始檔中有效(即原始檔保留)
    	CLASS:表示在class檔案中有效(即class保留)
    	RUNTIME:表示在執行時有效(即執行時保留)
    /
    
  3. @Documented

    該註解用於描述其它型別的annotation應該被作為被標註的程式成員的公共API,因此可以被javadoc此類的工具文件化。Documented是一個標記註解,沒有成員。關鍵原始碼如下:

    public @interface Documented {
    }
    
  4. @Inherited

    該註解是一個標記註解,@Inherited闡述了某個被標註的型別是被繼承的。如果一個使用了@Inherited修飾的annotation型別被用於一個class,則這個annotation將被用於該class的子類。關鍵原始碼如下

    public @interface Inherited {
    }
    

註解原理

註解本質是一個繼承了Annotation的特殊介面,其具體實現類是Java執行時生成的動態代理類。通過代理物件呼叫自定義註解(介面)的方法,會最終呼叫AnnotationInvocationHandler的invoke方法。該方法會從memberValues這個Map中索引出對應的值。而memberValues的來源是Java常量池。

序列化與反序列化

系列化

物件序列化的最主要的用處就是在傳遞和儲存物件的時候,保證物件的完整性和可傳遞性。序列化是把物件轉換成有序位元組流,以便在網路上傳輸或者儲存在本地檔案中。序列化後的位元組流儲存了Java物件的狀態以及相關的描述資訊。序列化機制的核心作用就是物件狀態的儲存與重建

反序列化

客戶端從檔案中或網路上獲得序列化後的物件位元組流後,根據位元組流中所儲存的物件狀態及描述資訊,通過反序列化重建物件。

為什麼需要序列化與反序列化

當兩個程式進行遠端通訊時,可以相互傳送各種型別的資料,包括文字、圖片、音訊、視訊等, 而這些資料都會以二進位制序列的形式在網路上傳送。傳送方需要把這個Java物件轉換為位元組序列,然後在網路上傳送。接收方需要從位元組序列中恢復出Java物件。

序列化好處

  1. 永久性儲存物件,儲存物件的位元組序列到本地檔案或者資料庫中;
  2. 通過序列化以位元組流的形式使物件在網路中進行傳遞和接收;
  3. 通過序列化在程式間傳遞物件;

Java的3種IO模型區別

3種IO模型

  1. BIO(阻塞IO模型)

    執行緒發起IO請求後,一直阻塞IO,直到緩衝區資料就緒後,再進入下一步操作。針對網路通訊都是一請求一應答的方式,雖然簡化了上層的應用開發,但在效能和可靠性方面存在著巨大瓶頸,如果每個請求都需要新建一個執行緒來專門處理,那麼在高併發的場景下,機器資源很快就會被耗盡。

  2. NIO(IO複用模型)

    執行緒發起io請求後,立即返回(非阻塞io)。同步指的是必須等待IO緩衝區內的資料就緒,而非阻塞指的是,使用者執行緒不原地等待IO緩衝區,可以先做一些其他操作,但是要定時輪詢檢查IO緩衝區資料是否就緒。Java中的NIO 是new IO的意思。其實是NIO加上IO多路複用技術。普通的NIO是執行緒輪詢檢視一個IO緩衝區是否就緒,而Java中的new IO指的是執行緒輪詢地去檢視一堆IO緩衝區中哪些就緒,這是一種IO多路複用的思想。IO多路複用模型中,將檢查IO資料是否就緒的任務,交給系統級別的select或epoll模型,由系統進行監控,減輕使用者執行緒負擔。

  3. AIO(非同步IO模型)

    AIO是真正意義上的非同步非阻塞IO模型。 NIO需要使用者定時輪詢,去檢查IO緩衝區資料是否就緒,佔用應用程式執行緒資源,其實輪詢相當於還是阻塞的,並非真正解放當前執行緒,因為它還是需要去查詢哪些IO就緒。而真正的理想的非同步非阻塞IO應該讓核心系統完成,使用者執行緒只需要告訴核心,當緩衝區就緒後,通知我或者執行我交給你的回撥函式。

Linux5種IO模型的區別

5種IO模型

  1. 阻塞IO模型

    使用者執行緒發出IO請求之後,核心會去檢視資料是否就緒,如果沒有就緒就會等待資料就緒,而使用者執行緒就會處於阻塞狀態,使用者執行緒交出CPU。當資料就緒之後,核心會將資料拷貝到使用者執行緒,並返回結果給使用者執行緒,使用者執行緒才解除block狀態

  2. 非阻塞IO模型

    當使用者執行緒發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。如果結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送read操作。一旦核心中的資料準備好了,並且又再次收到了使用者執行緒的請求,那麼它馬上就將資料拷貝到了使用者執行緒,然後返回。

  3. 多路複用IO模型

    在多路複用IO模型中,會有一個執行緒不斷去輪詢多個socket的狀態,只有當socket真正有讀寫事件時,才真正呼叫實際的IO讀寫操作。因為在多路複用IO模型中,只需要使用一個執行緒就可以管理多個socket,系統不需要建立新的程式或者執行緒,也不必維護這些執行緒和程式,並且只有在真正有socket讀寫事件進行時,才會使用IO資源,所以它大大減少了資源佔用。

  4. 訊號驅動IO模型

    在訊號驅動IO模型中,當使用者執行緒發起一個IO請求操作,會給對應的socket註冊一個訊號函式,然後使用者執行緒會繼續執行,當核心資料就緒時會傳送一個訊號給使用者執行緒,使用者執行緒接收到訊號之後,便在訊號函式中呼叫IO讀寫操作來進行實際的IO請求操作。

  5. 非同步IO模型

    在非同步IO模型中,當使用者執行緒發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從核心的角度,當它受到一個asynchronous read之後,它會立刻返回,說明read請求已經成功發起了,因此不會對使用者執行緒產生任何block。然後,核心會等待資料準備完成,然後將資料拷貝到使用者執行緒,當這一切都完成之後,核心會給使用者執行緒傳送一個訊號,告訴它read操作完成了。也就說使用者執行緒完全不需要實際的整個IO操作是如何進行的,只需要先發起一個請求,當接收核心返回的成功訊號時表示IO操作已經完成,可以直接去使用資料了。

Object方法

Object類是所有類的父類,它有九大方法

  1. clone方法(淺拷貝):保護方法,實現物件的淺複製,只有實現了Cloneable介面才可以呼叫該方法,否則丟擲CloneNotSupportedException異常。
  2. getClass方法:getClass就是返回一個執行例項的Class,通常返回類名。
  3. toString方法:返回該物件的字串表示,如果不重寫,返回類名@雜湊值。
  4. finalize方法:當垃圾回收器確定不存在對該物件的更多引用時,由物件的垃圾回收器呼叫此方法,釋放資源。
  5. equals方法:一般用來比較兩個物件是否相等,如果沒有重寫equals方法,則它的作用與 == 相同。
  6. hashCode方法:該方法用於雜湊查詢,可以減少在查詢中使用equals的次數,重寫了equals方法一般都要重寫hashCode方法。
  7. wait方法:wait()的作用是讓當前執行緒進入等待狀態,同時,wait()也會讓當前執行緒釋放它所持有的鎖。直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,當前執行緒被喚醒(進入“就緒狀態”)。
  8. notify方法:該方法喚醒在該物件上等待的某個執行緒。
  9. notifyAll方法:該方法喚醒在該物件上等待的所有執行緒。

相關文章