1. 程式人生 > 其它 >《面試補習》- 多執行緒知識梳理

《面試補習》- 多執行緒知識梳理

類和物件

在面向物件中,類和物件是最基本、最重要的組成單元。類實際上是表示一個客觀世界某類群體的一些基本特徵抽象。物件就是表示一個個具體的東西。所以說類是物件的抽象,物件是類的具體

“人類”只是一個抽象的概念,它僅僅是一個概念,是不存在的實體!但是所有具備“人類”這個群體的屬性與方法的物件都叫人!這個物件“人” 是實際存在的實體!每個人都是“人”這個群體的一個物件。

類的屬性

在 Java 中類的成員變數定義了類的屬性。宣告成員變數的語法如下:

[public|protected|private][static][final]<type><variable_name>

各引數的含義如下。

  • public、protected、private:用於表示成員變數的訪問許可權。
  • static:表示該成員變數為類變數,也稱為靜態變數。
  • final:表示將該成員變數宣告為常量,其值無法更改。
  • type:表示變數的型別。
  • variable_name:表示變數名稱。

可以在宣告成員變數的同時對其進行初始化,如果宣告成員變數時沒有對其初始化,則系統會使用預設值初始化成員變數。

初始化的預設值如下:

  • 整數型(byte、short、int 和 long)的基本型別變數的預設值為 0。
  • 單精度浮點型(float)的基本型別變數的預設值為 0.0f。
  • 雙精度浮點型(double)的基本型別變數的預設值為 0.0d。
  • 字元型(char)的基本型別變數的預設值為 “\u0000”。
  • 布林型的基本型別變數的預設值為 false。
  • 陣列引用型別的變數的預設值為 null。如果建立了陣列變數的例項,但沒有顯式地為每個元素賦值,則陣列中的元素初始化值採用陣列資料型別對應的預設值。

成員方法

宣告成員方法可以定義類的行為,行為表示一個物件能夠做的事情或者能夠從一個物件取得的資訊。類的各種功能操作都是用方法來實現的,屬性只不過提供了相應的資料。一個完整的方法通常包括方法名稱、方法主體、方法引數和方法返回值型別。若方法有返回值,則在方法體中用 return 語句指明要返回的值。

形參和實參

關於方法的引數,經常會提到形參與實參,形參是定義方法時引數列表中出現的引數,實參是呼叫方法時為方法傳遞的引數

方法的形參和實參具有以下特點:

  • 形參變數只有在被呼叫時才分配記憶體單元,在呼叫結束時,即刻釋放所分配的記憶體單元。因此,形參只有在方法內部有效,方法呼叫結束返回主調方法後則不能再使用該形參變數。
  • 實參可以是常量、變數、表示式、方法等,無論實參是何種型別的量,在進行方法呼叫時,它們都必須具有確定的值,以便把這些值傳送給形參。因此應預先用賦值、輸入等辦法使實參獲得確定值。
  • 實參和形參在數量、型別和順序上應嚴格一致,否則會發生“型別不匹配” 的錯誤。
  • 方法呼叫中發生的資料傳送是單向的,即只能把實參的值傳送紿形參,而不能把形參的值反向地傳送給實參。因此在方法呼叫過程中,形參的值發生改變,而實參中的值不會變化。
  • 實參變數對形參變數的資料傳遞是“值傳遞”,即只能由實參傳遞給形參,而不能由形參傳遞給實參。程式中執行到呼叫成員方法時,Java 把實參值複製到一個臨時的儲存區(棧)中,形參的任何修改都在棧中進行,當退出該成員方法時,Java 自動清除棧中的內容。

區域性變數

在方法體內可以定義本方法所使用的變數,這種變數是區域性變數。它的生存期與作用域是在本方法內,也就是說,區域性變數只能在本方法內有效或可見,離開本方法則這些變數將被自動釋放

在方法體內定義變數時,變數前不能加修飾符。區域性變數在使用前必須明確賦值,否則編譯時會出錯。另外,在一個方法內部,可以在複合語句(把多個語句用括號{}括起來組成的一個語句稱複合語句)中定義變數,這些變數只在複合語句中有效。

可變引數

在具體實際開發過程中,有時方法中引數的個數是不確定的。為了解決這個問題,在 J2SE 5.0 版本中引入了可變引數的概念。

宣告可變引數的語法格式如下:

methodName({paramList},paramType…paramName)

其中,methodName 表示方法名稱;paramList 表示方法的固定引數列表;paramType 表示可變引數的型別;… 是宣告可變引數的標識;paramName 表示可變引數名稱。

注意:可變引數必須定義在引數列表的最後。

public class StudentTestMethod {
    // 定義輸出考試學生的人數及姓名的方法
    public void print(String...names) {
        int count = names.length;    // 獲取總個數
        System.out.println("本次參加考試的有"+count+"人,名單如下:");
        for(int i = 0;i < names.length;i++) {
            System.out.println(names[i]);
        }
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        StudentTestMethod student = new StudentTestMethod();
        student.print("張強","李成","王勇");    // 傳入3個值
        student.print("馬麗","陳玲");
    }
}

構造方法

構造方法是類的一種特殊方法,用來初始化類的一個新的物件,在建立物件(new 運算子)之後自動呼叫。Java 中的每個類都有一個預設的構造方法,並且可以有一個以上的構造方法。

Java 構造方法有以下特點:

  • 方法名必須與類名相同
  • 可以有 0 個、1 個或多個引數
  • 沒有任何返回值,包括 void
  • 預設返回型別就是物件型別本身
  • 只能與 new 運算子結合使用

值得注意的是,如果為構造方法定義了返回值型別或使用 void 宣告構造方法沒有返回值,編譯時不會出錯,但 Java 會把這個所謂的構造方法當成普通方法來處理。

這時候大家可能會產生疑問,構造方法不是沒有返回值嗎?為什麼不能用 void 宣告呢?

簡單的說,這是 Java 的語法規定。實際上,類的構造方法是有返回值的,當使用 new 關鍵字來呼叫構造方法時,構造方法返回該類的例項,可以把這個類的例項當成構造器的返回值,因此構造器的返回值型別總是當前類,無須定義返回值型別。但必須注意不要在構造方法裡使用 return 來返回當前類的物件,因為構造方法的返回值是隱式的。

注意:構造方法不能被 static、final、synchronized、abstract 和 native(類似於 abstract)修飾。構造方法用於初始化一個新物件,所以用 static 修飾沒有意義。構造方法不能被子類繼承,所以用 final 和 abstract 修飾沒有意義。多個執行緒不會同時建立記憶體地址相同的同一個物件,所以用 synchronized 修飾沒有必要。

在一個類中定義多個具有不同引數的同名方法,這就是方法的過載。

如果在類中沒有定義任何一個構造方法,則 Java 會自動為該類生成一個預設的構造方法。預設的構造方法不包含任何引數,並且方法體為空。

無參構造方法和有參構造方法如下:

public class MyClass {
    private int m;    // 定義私有變數
    MyClass() {
        // 定義無參的構造方法
        m = 0;
    }
    MyClass(int m) {
        // 定義有參的構造方法
        this.m = m;
    }
}

this關鍵字

this.屬性名

大部分時候,普通方法訪問其他方法、成員變數時無須使用 this 字首,但如果方法裡有個區域性變數和成員變數同名,但程式又需要在該方法裡訪問這個被覆蓋的成員變數,則必須使用 this 字首。

// 建立構造方法,為上面的3個屬性賦初始值
public Teacher(String name,double salary,int age) {
    this.name = name;    // 設定教師名稱
    this.salary = salary;    // 設定教師工資
    this.age = age;    // 設定教師年齡
}

this.方法名

this 關鍵字最大的作用就是讓類中一個方法,訪問該類裡的另一個方法或例項變數。

public class Dog {
    // 定義一個jump()方法
    public void jump() {
        System.out.println("正在執行jump方法");
    }
    // 定義一個run()方法,run()方法需要藉助jump()方法
	public void run() {
    // 使用this引用呼叫run()方法的物件
    	this.jump();
    	System.out.println("正在執行run方法");
	}
}

this( )訪問構造方法

public class Student {
    String name;
    // 無參構造方法(沒有引數的構造方法)
    public Student() {
        this("張三");
    }
    // 有參構造方法
    public Student(String name) {
        this.name = name;
    }
    // 輸出name和age
    public void print() {
        System.out.println("姓名:" + name);
    }
    public static void main(String[] args) {
        Student stu = new Student();
        stu.print();
    }
}

注意:

  • this( ) 不能在普通方法中使用,只能寫在構造方法中。
  • 在構造方法中使用時,必須是第一條語句。

static關鍵字

在類中,使用 static 修飾符修飾的屬性(成員變數)稱為靜態變數,也可以稱為類變數,常量稱為靜態常量,方法稱為靜態方法或類方法,它們統稱為靜態成員,歸整個類所有

靜態成員不依賴於類的特定例項,被類的所有例項共享,就是說 static 修飾的方法或者變數不需要依賴於物件來進行訪問,只要這個類被載入,Java 虛擬機器就可以根據類名找到它們。

呼叫靜態成員的語法形式如下:

類名.靜態成員

注意:

  • static 修飾的成員變數和方法,從屬於類。
  • 普通變數和方法從屬於物件。
  • 靜態方法不能呼叫非靜態成員,編譯會報錯。

靜態變數

類的成員變數可以分為以下兩種:

  1. 靜態變數(或稱為類變數),指被 static 修飾的成員變數。
  2. 例項變數,指沒有被 static 修飾的成員變數。

靜態變數與例項變數的區別如下:

1)靜態變數

  • 執行時,Java 虛擬機器只為靜態變數分配一次記憶體,在載入類的過程中完成靜態變數的記憶體分配。
  • 在類的內部,可以在任何方法內直接訪問靜態變數。
  • 在其他類中,可以通過類名訪問該類中的靜態變數。

2)例項變數

  • 每建立一個例項,Java 虛擬機器就會為例項變數分配一次記憶體。
  • 在類的內部,可以在非靜態方法中直接訪問例項變數。
  • 在本類的靜態方法或其他類中則需要通過類的例項物件進行訪問。

靜態變數在類中的作用如下:

  • 靜態變數可以被類的所有例項共享,因此靜態變數可以作為例項之間的共享資料,增加例項之間的互動性。
  • 如果類的所有例項都包含一個相同的常量屬性,則可以把這個屬性定義為靜態常量型別,從而節省記憶體空間。例如,在類中定義一個靜態常量 PI。
public class StaticVar {
    public static String str1 = "Hello";
    public static void main(String[] args) {
        String str2 = "World!";
        // 直接訪問str1
        String accessVar1 = str1+str2;
        System.out.println("第 1 次訪問靜態變數,結果為:"+accessVar1);
        // 通過類名訪問str1
        String accessVar2 = StaticVar.str1+str2;
        System.out.println("第 2 次訪問靜態變數,結果為:"+accessVar2);
        // 通過物件svt1訪問str1
        StaticVar svt1 = new StaticVar();
        svt1.str1 = svt1.str1+str2;
        String accessVar3 = svt1.str1;
        System.out.println("第3次訪向靜態變數,結果為:"+accessVar3);
        // 通過物件svt2訪問str1
        StaticVar svt2 = new StaticVar();
        String accessVar4 = svt2.str1+str2;
        System.out.println("第 4 次訪問靜態變數,結果為:"+accessVar4);
    }
}

執行該程式後的結果如下所示。

第 1 次訪問靜態變數,結果為:HelloWorld!
第 2 次訪問靜態變數,結果為:HelloWorld!
第 3 次訪向靜態變數,結果為:HelloWorld!
第 4 次訪問靜態變數,結果為:HelloWorld!World!

靜態方法

與成員變數類似,成員方法也可以分為以下兩種:

  1. 靜態方法(或稱為類方法),指被 static 修飾的成員方法。
  2. 例項方法,指沒有被 static 修飾的成員方法。

靜態方法與例項方法的區別如下:

  • 靜態方法不需要通過它所屬的類的任何例項就可以被呼叫,因此在靜態方法中不能使用 this 關鍵字,也不能直接訪問所屬類的例項變數和例項方法,但是可以直接訪問所屬類的靜態變數和靜態方法。另外,和 this 關鍵字一樣,super 關鍵字也與類的特定例項相關,所以在靜態方法中也不能使用 super 關鍵字。
  • 在例項方法中可以直接訪問所屬類的靜態變數、靜態方法、例項變數和例項方法。
public class StaticMethod {
    public static int count = 1;    // 定義靜態變數count
    public int method1() {    
        // 例項方法method1
        count++;    // 訪問靜態變數count並賦值
        System.out.println("在靜態方法 method1()中的 count="+count);    // 列印count
        return count;
    }
    public static int method2() {    
        // 靜態方法method2
        count += count;    // 訪問靜態變數count並賦值
        System.out.println("在靜態方法 method2()中的 count="+count);    // 列印count
        return count;
    }
    public static void PrintCount() {    
        // 靜態方法PrintCount
        count += 2;
        System.out.println("在靜態方法 PrintCount()中的 count="+count);    // 列印count
    }
    public static void main(String[] args) {
        StaticMethod sft = new StaticMethod();
        // 通過例項物件呼叫例項方法
        System.out.println("method1() 方法返回值 intro1="+sft.method1());
        // 直接呼叫靜態方法
        System.out.println("method2() 方法返回值 intro1="+method2());
        // 通過類名呼叫靜態方法,列印 count
        StaticMethod.PrintCount();
    }
}

執行該程式後的結果如下所示。

在靜態方法 method1()中的 count=2
method1() 方法返回值 intro1=2
在靜態方法 method2()中的 count=4
method2() 方法返回值 intro1=4
在靜態方法 PrintCount()中的 count=6

靜態程式碼塊

靜態程式碼塊指 Java 類中的 static{ } 程式碼塊,主要用於初始化類,為類的靜態變數賦初始值,提升程式效能。

靜態程式碼塊的特點如下:

  • 靜態程式碼塊類似於一個方法,但它不可以存在於任何方法體中。
  • 靜態程式碼塊可以置於類中的任何地方,類中可以有多個靜態初始化塊。
  • Java 虛擬機器在載入類時執行靜態程式碼塊,所以很多時候會將一些只需要進行一次的初始化操作都放在 static 程式碼塊中進行。
  • 如果類中包含多個靜態程式碼塊,則 Java 虛擬機器將按它們在類中出現的順序依次執行它們,每個靜態程式碼塊只會被執行一次。
  • 靜態程式碼塊與靜態方法一樣,不能直接訪問類的例項變數和例項方法,而需要通過類的例項物件來訪問。
public class StaticCode {
    public static int count = 0;
    {
        count++;
        System.out.println("非靜態程式碼塊 count=" + count);
    }
    static {
        count++;
        System.out.println("靜態程式碼塊1 count=" + count);
    }
    static {
        count++;
        System.out.println("靜態程式碼塊2 count=" + count);
    }
    public static void main(String[] args) {
        System.out.println("*************** StaticCode1 執行 ***************");
        StaticCode sct1 = new StaticCode();
        System.out.println("*************** StaticCode2 執行 ***************");
        StaticCode sct2 = new StaticCode();
    }
}

如上述示例,為了說明靜態程式碼塊只被執行一次,特地添加了非靜態程式碼塊作為對比,並在主方法中建立了兩個類的例項物件。上述示例的執行結果為:

靜態程式碼塊1 count=1
靜態程式碼塊2 count=2
*************** StaticCode1 執行 ***************
非靜態程式碼塊 count=3
*************** StaticCode2 執行 ***************
非靜態程式碼塊 count=4

物件的建立

物件是對類的例項化。物件具有狀態和行為,變數用來表明物件的狀態,方法表明物件所具有的行為。Java 物件的生命週期包括建立、使用和清除。在 Java 語言中建立物件分顯式建立與隱含建立兩種情況。

顯式建立物件

物件的顯式建立方式有 4 種。

使用 new 關鍵字建立物件

這是常用的建立物件的方法,語法格式如下:

類名 物件名 = new 類名();

呼叫 java.lang.Class 或者 java.lang.reflect.Constuctor 類的 newlnstance() 例項方法

在 Java 中,可以使用 java.lang.Class 或者 java.lang.reflect.Constuctor 類的 newlnstance() 例項方法來建立物件,程式碼格式如下:

java.lang.Class Class 類物件名稱 = java.lang.Class.forName(要例項化的類全稱);
類名 物件名 = (類名)Class類物件名稱.newInstance();

呼叫 java.lang.Class 類中的 forName() 方法時,需要將要例項化的類的全稱(比如 com.mxl.package.Student)作為引數傳遞過去,然後再呼叫 java.lang.Class 類物件的 newInstance() 方法建立物件。

呼叫物件的 clone() 方法

該方法不常用,使用該方法建立物件時,要例項化的類必須繼承 java.lang.Cloneable 介面。 呼叫物件的 clone() 方法建立物件的語法格式如下:

類名物件名 = (類名)已建立好的類物件名.clone();

下面建立一個示例演示常用的前三種物件建立方法。示例程式碼如下:

public class Student implements Cloneable {   
    // 實現 Cloneable 介面
    private String Name;    // 學生名字
    private int age;    // 學生年齡
    public Student(String name,int age) {    
        // 構造方法
        this.Name = name;
        this.age = age;
    }
    public Student() {
        this.Name = "name";
        this.age = 0;
    }
    public String toString() {
        return"學生名字:"+Name+",年齡:"+age;
    }
    public static void main(String[] args)throws Exception {
        System.out.println("---------使用 new 關鍵字建立物件---------");
       
        // 使用new關鍵字建立物件
        Student student1 = new Student("小劉",22);
        System.out.println(student1);
        System.out.println("-----------呼叫 java.lang.Class 的 newInstance() 方法建立物件-----------");
       
        // 呼叫 java.lang.Class 的 newInstance() 方法建立物件
        Class c1 = Class.forName("Student");
        Student student2 = (Student)c1.newInstance();
        System.out.println(student2);
        System.out.println("-------------------呼叫物件的 clone() 方法建立物件----------");
        // 呼叫物件的 clone() 方法建立物件
        Student student3 = (Student)student2.clone();
        System.out.println(student3);
    }
}

對上述示例的說明如下:

  • 使用 new 關鍵字或 Class 物件的 newInstance() 方法建立物件時,都會呼叫類的構造方法。
  • 使用 Class 類的 newInstance() 方法建立物件時,會呼叫類的預設構造方法,即無參構造方法。
  • 使用 Object 類的 clone() 方法建立物件時,不會呼叫類的構造方法,它會建立一個複製的物件,這個物件和原來的物件具有不同的記憶體地址,但它們的屬性值相同。
  • 如果類沒有實現 Cloneable 介面,則 clone。方法會丟擲 java.lang.CloneNotSupportedException 異常,所以應該讓類實現 Cloneable 介面。

程式執行結果如下:

---------使用 new 關鍵字建立物件---------
學生名字:小劉,年齡:22
-----------呼叫 java.lang.Class 的 newInstance() 方法建立物件-----------
學生名字:name,年齡:0
-------------------呼叫物件的done()方法建立物件----------
學生名字:name,年齡:0

呼叫 java.io.ObjectlnputStream 物件的 readObject() 方法

隱含建立物件

除了顯式建立物件以外,在 Java 程式中還可以隱含地建立物件,例如下面幾種情況。

1)String strName = "strValue",其中的“strValue”就是一個 String 物件,由 Java 虛擬機器隱含地建立。

2)字串的“+”運算子運算的結果為一個新的 String 物件,示例如下:

String str1 = "Hello";
String str2 = "Java";
String str3 = str1+str2;    // str3引用一個新的String物件

3)當 Java 虛擬機器載入一個類時,會隱含地建立描述這個類的 Class 例項。

提示:類的載入是指把類的 .class 檔案中的二進位制資料讀入記憶體中,把它存放在執行時資料區的方法區內,然後在堆區建立一個 java.lang.Class 物件,用來封裝類在方法區內的資料結構。

總結

無論釆用哪種方式建立物件,Java 虛擬機器在建立一個物件時都包含以下步驟:

  • 給物件分配記憶體。
  • 將物件的例項變數自動初始化為其變數型別的預設值。
  • 初始化物件,給例項變數賦予正確的初始值。

注意:每個物件都是相互獨立的,在記憶體中佔有獨立的記憶體地址,並且每個物件都具有自己的生命週期,當一個物件的生命週期結束時,物件就變成了垃圾,由 Java 虛擬機器自帶的垃圾回收機制處理。

匿名物件

每次 new 都相當於開闢了一個新的物件,並開闢了一個新的實體記憶體空間。如果一個物件只需要使用唯一的一次,就可以使用匿名物件,匿名物件還可以作為實際引數傳遞。

匿名物件就是沒有明確的給出名字的物件,是物件的一種簡寫形式。一般匿名物件只使用一次,而且匿名物件只在堆記憶體中開闢空間,而不存在棧記憶體的引用。

public class Person {
    public String name; // 姓名
    public int age; // 年齡
    // 定義構造方法,為屬性初始化
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 獲取資訊的方法
    public void tell() {
        System.out.println("姓名:" + name + ",年齡:" + age);
    }
    public static void main(String[] args) {
        new Person("張三", 30).tell(); // 匿名物件
    }
}

程式執行結果為:

姓名:張三,年齡:30

在以上程式的主方法中可以發現,直接使用了“new Person("張三",30)”語句,這實際上就是一個匿名物件,與之前宣告的物件不同,此處沒有任何棧記憶體引用它,所以此物件使用一次之後就等待被 GC(垃圾收集機制)回收。

匿名物件在實際開發中基本都是作為其他類例項化物件的引數傳遞的,在Java 應用部分的很多地方都可以發現其用法,匿名物件實際上就是個堆記憶體空間,物件不管是匿名的還是非匿名的,都必須在開闢堆空間之後才可以使用。