JAVA SE學習筆記(七):終章:Java程序設計基礎筆記(全10萬字)

Java程序設計入門

​                                              copyright © 2020 by 宇智波Akali

目錄

文章目錄


第1章 Java基礎語法

1、數據類型​​

1.1 八種基本數據類型

byte,short,int,long,float,double,char,booleanbyte, short, int, long, float, double, char, boolean

在Java中的關鍵字、保留字、標識符等使用方法和C++相似,但是變量:必須初始化

byte類型的取值範圍也是[-128, 127]。

long類型和float類型在定義時,值後面要加l或L, f或F。

long a = 3l; float b = 1.22F;

boolean類型不用0或非0的數值來表示,僅由True/FalseTrue/False表示,這裏和其他語言不同。

boolean b1 = true;  boolean b2 = false;

換行輸出語句:

System.out.println(str);

輸入語句示例 e. g. 1-01

//讀入單個數據
Scenner scenner = new Scenner(System.in);
int input = scenner.nextInt(); //String input = scenner.next();
scenner.close();
//一次讀入多個數據
Scenner scenner = new Scenner(System.in);
String str = scenner.nextLine();	//一次讀入一行字符串
String strs[] = str.split(" "); 	//將上面的字符串按照空格劃分爲各個單位存在數組中
int a = Integer.parseInt(strs[0]);//通過基本類型的包裝類將字符串轉爲基本數據類型
double b = Double.parseDouble(strs[1]);

1.2引用類型:String

String str = "Hello world!";

​ 引用類型都可以用null作爲值,也就是說可以在初始化的時候賦值爲null,所以,String可以使用null作爲值,此外,String與其他類型還有一個不同點:其值是不變的,這和引用類型在內存中的存儲機制有關,後面會有涉及。

int i0 =1;
int i1 = 1;
//以上這種情況會在內存中存儲2個1的值

String s0 = "Hello";
String s1 = "Hello";
//因爲String是引用類型,所以只存在一個"Hello",變量名去引用"Hello".

String可以用加法進行拼接。

1.3基本數據類型轉換

  • char, byte, short三者同級,不可互相轉換且<int <long <float < double。

  • 多種數據類型同時計算時統一成最大容量的類型進行計算

  • 而多個相同類型變量運算時,先要轉換爲相對應的數據類型的默認類型(比如:兩個byte類型的變量相加,會把兩個byte類型轉換爲默認的int類型之後再計算,得到的結果是int類型)這種情況適用於變量類型的容量小於默認變量類型

且:當把任何基本類型的值和字符串值進行連接運算時(+),基本數據類型自動轉換爲字符串類型。

1.3.1強制類型轉換

e. g. int k = 7;
       byte a = (byte) k;
//通常字符串不能直接轉換爲基本類型,但可以通過基本類型對應的包裝類來實現:
String a = "43";
int i = Integer.parselnt(a);

boolean類型不可以轉換爲其他的數據類型

2. 運算符

2.1賦值運算符

支持連續賦值。

思考1

short s = 3;
s=s+2;  ①
s+=2;//①和②有什麼區別?

①會報錯,因爲在運算s+2時會轉換爲int型,s != int型變量,這裏應該手動強制類型轉換:
s = (short) s + 2;
②可以正常運行,使用擴展賦值運算符時,會自動將變量類型強制轉換爲當前類型的變量類型。

思考2

int i = 1;
i *= 0.1;
System.out.println(i);
// 輸出 0,因爲i = i * 0.1會強制轉換爲int型,0.1變成0

注意:

如果對負數取模,可以把模數負號忽略不記,如:5%-2=1。
但被模數是負數則不可忽略。此外,取模運算的結果不一定總是整數
int型之間做除法時,只保留整數部分而捨棄小數部分。
“+”除字符串相加功能外,還能把非字符串轉換成字符串.例如:
System.out.println("5+5="+5+5); //打印結果是?5+5=55
以下二者的區別:
System.out.println(‘*’ + ‘\t’ +‘*’); 輸出:93,爲Ascall碼,42+9+42=93 System.out.println(“*” + ‘\t’ +‘*’);輸出:* *

2.2 邏輯運算符:與&、或|、非!、異或^

異或運算:一個true,一個false則爲true 同爲true或同爲false則爲false。

單&時,爲邏輯與,左邊無論真假,右邊都進行運算。
雙&時,爲短路與,如果左邊爲真,右邊參與運算,如果左邊爲假,那麼右邊不參與運算。
“|”和“||”的區別同理,||表示:當左邊爲真,右邊不參與運算。

2.3其他運算符

  • 位運算符:轉換爲2進制,各個位置進行運算。
  • 三目運算符:(條件表達式)?表達式1:表達式2; 條件表達式爲true則運算後的結果爲表達式1否則爲表達式2。

2.4 運算符優先級

3. 程序流程控制

3.1 順序結構

按照順序執行

3.2 分支結構

(1)	if(條件){
		語句;
	}else if(條件){
		語句;
	}else{
		語句;
	}
(2) int i = 1;
switch(i){
	case 1:
		語句;
		break;
	case 2:
		語句;
		break;
	default:
		語句;
		break;
	}
//switch(表達式)中表達式的返回值必須是下述幾種類型之一:byte, short, char, int, 枚舉,String;
//case子句中的值必須是常量,且其值應該不同,語句塊中沒有break程序會執行到switch結尾

3.3 循環結構

  • for 循環
  • while 循環
  • do-while 循環
  • 循環嵌套

3.4 特殊流程控制語句

  • break:結束當前所處的整個循環。
  • continue:跳出當前循環,直接進去下一循環。
  • return:並非專門用於結束循環,執行到return語句時,不管出於幾層循環內,將結束整個方法。

4. 數組

數組爲引用類型,分配空間時要new,傳值時傳遞的是引用。

4.1 一維數組

聲明方式:

//僅聲明還不能使用,還要初始化,new分配空間。 
type name[]; 
 type[] name1;

數組聲明以後在內存中沒有分配具體的存儲空間,也沒有設定數組的長度。

初始化:

  • 動態初始化:數組聲明且爲數組元素分配空間與賦值的操作分開進行。

    int[] arr = new int[3];		//初始化時(聲明時也可以用new)要傳入數組長度;如果沒有給數組元素賦值,則數組元素會默認初始化,數字類型默認爲0,對象類型默認爲null。
    arr[0] = 3;
    arr[1] = 9;
    arr[2] = 8;
    
  • 靜態初始化:在定義數組的同時就爲數組元素分配空間並賦值。

    int a[] = new int[]{ 3, 9, 8};
    int[] a = {3,9,8};
    

4.2 多維數組

聲明方式:·

int n[][];

初始化:

  • 動態初始化

    int a[][];
    a = new int[2][3];
    //或者聲明和動態初始化同時進行
    int n[][] = new int[2][3];//只聲明,沒有new分配空間和長度是無法使用的,一維數組也一樣
    
  • 靜態初始化

    int a[][] = new int [][]{
        {1,2},
        {2,2}
    };
    
  • 讓第二維長短不一

    //如果需要初始化第二維長度不一樣的二維數組,則可以使用如下的格式:
    int n[][];
    n = new int[2][]; //只初始化第一維的長度
    //分別初始化後續的元素
    n[0] = new int[4];
    n[1] = new int[3];
    //得到
    /*{  
    	{1,2,3,4},
    	{2,3,4}
    }*/
    

5. 源文件佈局

5.1 佈局格式

[<包聲明>]
	[<引用聲明>]
	<類聲明>

5.2 關鍵字-package

  1. package語句作爲Java源文件的第一條語句,指明該文件中定義的類所在的包。若缺省該語句,則指定爲無名包。
  2. 包對應於文件系統的目錄,package語句中,用 “.” 來指明包(目錄)的層次。
  3. 包通常用小寫單詞,類名首字母通常大寫.

5.3 關鍵字-import

**作用:**爲了使用定義在不同包中的Java類,需用import語句來引入指定包層次下所需要的類或全部類(.*),但是包內的子包中的類不會被引用。

語法格式:import 包名[.子包名…]. <類名 |*>

示例:

import  p1.Test;   //import p1.*;表示引入p1包中的所有類
public class TestPackage{
    public static void main(String args[]){
        Test t = new Test();          //Test類在p1包中定義
        t.display();
    }
}

import語句出現在package語句之後、類定義之前,一個源文件中可包含多個import語句。


第2章 Java 面向對象

1. 什麼是面向對象

1.1 面向對象與面向過程

面向過程,強調的是功能行爲;面向對象,將功能封裝進對象,強調具備了功能的對象。

1.2 面向對象的三大特性

  • 封裝性
  • 繼承性
  • 多態性

2. 類和對象

2.1類和對象的關係

類 = 汽車設計圖 對象 = 實實在在的汽車

面向對象程序設計的重點是類的設計
定義類其實是定義類中的成員(成員變量和成員方法)

2.2 Java類及類的成員

Java的一個類class一般包含:

  1. 屬性:即成員變量
  2. 行爲:即成員方法(函數)

2.3 類的語法格式

修飾符 class 類名{
	屬性聲明;
    方法聲明;
}

注意:方法中不能再定義方法

e. g. 2-01

public class Person {
    //類的成員變量可以先聲明,不用初始化,類成員變量是有默認值
    public String name;			//姓名,String的默認值是null
    private int age;			//年齡,int的默認值是0
    public static String sex = "男";		//靜態類變量-後面詳解

    /**
     *行爲,方法,也叫函數
     *方法的名稱如果是多個單詞,首個的單詞的首字母小寫,其他的單詞首字母大寫,這樣就像一個駝峯一樣,所	   *以叫駝峯命名法
     */
    public void showName(){	
        System.out.println("姓名:" + name);
        getAge();				//同一個類中,所有的方法可以直接互相調用,不用new去實例化對象
    }

    //有返回值的函數
    public int getAge(){		
        return age;
    }
}

2.4 類的實例化:對象的創建和使用

  • 使用new + 類構造器創建一個新的對象
  • 使用對象名.對象成員的方式訪問對象成員(包括對象的屬性和方法)

以e. g. 2-01中創建的Person類爲例:

e. g. 2-02

public class People {
    public static void main(String[] args){
        //實例化Person類,也就是創建Person對象
        Person person = new Person();
        //聲明一個Person類型的變量,變量名person,實例化Person類並且給person賦值,賦值的值就是當前的實例
        person.name = "張三";					//給person對象的name屬性賦值
        person.showName();						//對象的方法的調用
        int i = person.getAge();
        System.out.println(i);
        person.age = 11;						//給person對象的age屬性賦值
        int a = person.getAge();
        System.out.println(a);
    }
}
//注:如果創建了一個類的多個對象,對於類中定義的屬性,每個對象都擁有各自的一套副本,且互不干擾.

2.5 關於對象

2.51 匿名對象

  • 概念: 不定義對象的句柄,而直接調用這個對象的方法,這樣的對象叫做匿名對象。
  • 使用情況: 如果對一個對象只需要進行一次方法調用,那麼就可以使用匿名對象。 經常將匿名對象作爲實參傳遞給一個方法調用。

e. g. 2-03

new Person().shout(); 	//沒有給對象命名(定義句柄),直接調用對象中的方法。

2.5.2 在同一個類中的訪問機制

​ 類中的方法可以直接訪問類中的成員變量。(例外:static方法訪問非static的成員變量,編譯不通過)

2.5.3 在不同類中的訪問機制

​ 先創建要訪問類的對象,再用對象訪問類中定義的成員。

3. 類的成員之一 :屬性

3.1 語法格式

修飾符 類型 屬性名 = 初值;

說明: 修飾符private:該屬性只能由該類的方法訪問,不能通過 對象.屬性 的方式調用。
修飾符public:該屬性可以被該類以外的方法訪問。
類型:任何基本類型,如int、boolean或任何類。

3.2 變量分類

3.2.1 成員變量

  • 實例變量(不以static修飾):

    ​ 就是在類實例化成對象之後才能被使用的變量,如Person中的name(成員變量有默認的init值)。
    ​ 實例變量存在於對象所在的堆內存中。

  • 類變量(以static修飾):

    ​ 這樣的變量是靜態的,類不需要實例化成對象就可以通過 類名.屬性 調用.如Person中的sex。

3.2.2 局部變量

  • 形參(方法簽名中定義的變量)
  • 方法局部變量(在方法內定義的變量)
  • 代碼塊局部變量(在代碼塊中定義的變量)

注意:

a.局部變量存在於棧內存中,作用的範圍結束,變量空間會自動釋放.
b.局部變量沒有默認初始化值,每次必須顯式初始化.
c.局部變量聲明時不指定權限修飾符,所以方法內的變量聲明不用修飾符

4. 類的成員之二:方法

4.1 語法格式

修飾符 返回值類型 方法名(參數類型 形參1,參數類型 形參2...){
    程序代碼
    return 返回值;

說明: 修飾符:public, private, protected等。
返回類型爲void,則不用return.關於形參,實參,返回值等不再記錄,和C++完全一致

4.2 方法的注意事項

  1. 方法是類或對象行爲特徵的抽象,也稱爲函數
  2. Java裏的方法不能獨立存在,所有的方法必須定義在類裏
  3. 方法只有被調用纔會被執行
  4. 方法中只能調用方法,不可以在方法內部定義方法
    並且在同一個類中,方法之間可以直接互相調用,所以在方法中調用另一個方法時不需new

4.3 方法的重載

4.3.1 概念

​ 在同一個類中,允許存在一個以上的同名方法,只要它們的參數個數或者參數類型不同即可,與形參名無關。

4.3.2 重載代碼示例

e. g. 2-04

public int add(int x, int y){
    return x + y;
}

public double add(int x, double y){
    return x + y;
}

public int add(int x, int y, int z){
    return x + y + z;
}
//在主類Text中定義重載函數,在main()中,進行對象實例化,以及方法的調用
Text examp = new Text();
examp.add(1, 2);
examp.add(1, 0.5);
examp.add(1,2,3);

4.3.3 方法重載的特點

  1. 與返回值類型無關,只看參數列表,且參數列表必須不同。
  2. 參數個數或參數類型,參數相同只是順序不同也可以構成重載,調用時,根據方法參數列表的不同來區別。

4.4 方法之可變個數的形參

4.4.1 定義方法

  • a.使用數組作爲形參

傳參使用時需要在main函數中new一個數組作爲實參傳入,數組長度可爲0+0 \to+ \infty

public void printInfo(String[] args){		//這裏的String可以根據需要使用基本數據類型
    for(int i = 0; i < args.length; i++){
        System.out.println(args[i]);
    }
}
  • b.使用Java自帶的可變形參表示法

(使用方法與使用數組的形式相同)

e. g. 2-05:

//方法的參數部分有可變形參,需要放在形參聲明的最後
public void printInfo1(int d, String... args){
    for(int i = 0; i < args.length; i++){
        System.out.println(args[i]);
    }
}
//使用:(在此之前要在main中實例化一個方法所在的類,命名爲p3)
p3.printInfo1(2"lisi", "23", "男");
//或者像使用數組一樣
String[] ss2 = new String[]{"北京市xxx","13133334444","152109874523666541"};
p3.printInfo1(ss2);
//不同的是,當參數爲空時,使用數組的方法要傳入null或空數組,用b方法形參可以空着

4.5 JVM的內存模型圖

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-v31iMMFJ-1587194076057)(Java Record.assets/20200208235224440.png)]

4.6 方法的參數傳遞

  1. Java裏方法的參數傳遞方式只有一種:值傳遞

    即將實際參數值的副本(複製品)傳入方法內,而參數本身不受影響。

  2. 參數傳遞的兩種情況:

    • a.如果方法的形參是基本數據類型,那麼實參(實際的數據)向形參傳遞參數時,就是直接傳遞值,把實參的值複製給形參。
    • b.如果方法的形參是對象,那麼實參(實際的對象),向形參傳遞參數的時,也是把值給形參,這個值是實參在棧內存中的值,也就是引用對象在堆內存中的地址。
    • 基本數據類型都是保存在棧內存中,引用對象在棧內存中保存的是引用對象的地址,那麼方法的參數傳遞是傳遞值(是變量在棧內存的當中的值)
  3. 參數傳遞的內存示意圖:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-sG7r4a8U-1587194076061)(Java Record.assets/20200208235637959.jpg)]

5.類的成員之三:構造器

5.1 語法格式

修飾符  類名 (參數列表) {		//修飾符與類的修飾符一致
    初始化語句;					//可以使用初始化語句和參數列表將類的屬性默認初始化或顯式初始化
}

5.2 構造器的特徵

  1. 它具有與類相同的名稱
  2. 它不聲明返回值類型。(與聲明爲void不同)
  3. 不能被static、final、synchronized、abstract、native修飾,不能有return語句返回值

5.3 構造器的作用

​ 創建對象,以及給對象進行初始化。

​ 比如:Order o = new Order();

Person p = new Person(Peter,15);

​ new對象 實際就是調用類的構造器(C++中的構造方法、構造函數在Java中稱構造器)。

5.4 構造器的分類

​ 根據參數不同構造器可以分爲如下兩類:

  • 隱式無參構造器(系統默認提供)
  • 顯式定義一個或多個構造器(無參或有參)

注意:

a. Java語言中,每個類都至少有一個構造器
b. 默認構造器的修飾符與所屬類的修飾符一致
c. 一旦顯式定義了構造器,則系統不再提供默認構造器
d. 一個類可以創建多個重載的構造器
e. 父類的構造器不可被子類繼承

5.5 構造器重載

構造器的重載使得對象的創建更加靈活,方便創建各種不同的對象。

e. g. 2-06 :

public class Person{
    //構造器重載,參數列表必須不同
    public Person(String name, int age, Date d) {this(name,age);}
    public Person(String name, int age) {}
    public Person(String name, Date d) {}
    public Person(){}
}

6. 類的成員之四:初始化塊(代碼塊)

01. 作用: 對Java對象進行初始化。

**02. 程序的執行順序:**聲明成員變量的默認值 -> 靜態代碼塊 -> 顯式初始化、多個初始化塊依次被執行(同級別下按先後順序執行) -> 構造器再對成員進行賦值操作

**03. 靜態代碼塊:**一個類中初始化塊若有修飾符,則只能被static修飾,稱爲靜態代碼塊(static block ),當類被載入時,類屬性的聲明和靜態代碼塊先後順序被執行,且只被執行一次。

04. static塊通常用於初始化static (類)屬性

示例:e. g. 2-07:

class Person {
    public static int total;
    static {
        total = 100;//爲total賦初值 
    }
    …… //其它屬性或方法聲明
}

05. 非靜態代碼塊:沒有static修飾的代碼塊

​ a. 可以有輸出語句。
​ b. 可以對類的屬性聲明進行初始化操作。
​ c. 可以調用靜態和非靜態的變量或方法。
​ d. 若有多個非靜態的代碼塊,那麼按照從上到下的順序依次執行。
​ e. 每次創建對象的時候,都會執行一次,且先於構造器執行。

06. 靜態代碼塊:用static 修飾的代碼塊

​ a. 可以有輸出語句。
​ b. 可以對類的屬性聲明進行初始化操作。
​ c. 不可以對非靜態的屬性初始化。即:不可以調用非靜態的屬性和方法。
​ d. 若有多個靜態的代碼塊,那麼按照從上到下的順序依次執行。
​ e. 靜態代碼塊的執行要先於非靜態代碼塊。
​ f. 靜態代碼塊只執行一次。

7. 類的成員之五: 內部類

概念: 在Java中,允許一個類的定義位於另一個類的內部,前者稱爲內部類,後者稱爲外部類。

作用: 解決Java不能多重繼承的問題(接口也可以),有時只有一個方法想要繼承,專門寫一個接口顯得有點繁瑣,可以在類中寫一個內部類,用內部類繼承該類的方法即可。

a. 內部類可以使用外部類的私有數據,因爲它是外部類的成員,同一個類的成員之間可相互訪問。而外部類要訪問內部類中的成員需要 內部類.成員 或者 內部類對象.成員

b. 內部類可以聲明爲static,但此時就不能再使用外層類的非static的成員變量。非static的內部類中的成員不能聲明爲static的,只有在外部類或static的內部類中才可聲明static成員

c. 內部類可以聲明爲abstract類型或final類型 ,因此可以被其它的內部類繼承或者終止繼承。

代碼示例: e. g. 2-08

class A {
    private int s;
    public class B{			//定義內部類
        public void mb() {
            s = 100;     
            System.out.println("在內部類B中s=" + s);
        }  }
    public void ma() {		//定義在外部類中訪問內部類的方法
        B i = new B();
        i.mb();
    }  
}

public class Test {	
    public static void main(String args[]){
        A o = new A();			
        o.ma();				//創建外部類對象,通過外部類中的方法訪問內部類
    }   
} 

第3章 面向對象的特徵

1. 面向對象的特徵之一: 封裝和隱藏

​ 使用者對類內部定義的屬性(對象的成員變量)的直接操作會導致數據的錯誤、混亂或安全性問題,因此需要對類進行封裝和隱藏。

  1. Java中通過將數據聲明爲私有的(private),再提供公共的(public)方法get_X()和set_X()等方法,實現對該屬性的操作,以實現下述目的:

a. 隱藏一個類中不需要對外提供的實現細節;
b. 使用者只能通過事先定製好的方法來訪問數據,可以方便地加入控制邏輯,限制對屬性的不合理操作;
c. 便於修改,增強代碼的可維護性;

  1. 四種訪問權限修飾符

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-TNtAsqOE-1587194076062)(Java Record.assets/image-20200319191721641.png)]

  1. 注意:

a. 對於class的權限修飾只可以用public和default(缺省)。
b. public類可以在任意地方被訪問。
c. default類只可以被同一個包內部的類訪問。
d. 在同一個Java文件中可以寫多個class,但是隻有一個可以定義爲public,其他修飾符空着(default)

2. 面向對象的特徵之二: 繼 承

​ 多個類中存在相同屬性和行爲時,將這些內容抽取到單獨一個類中,那麼子類無需再定義這些屬性和行爲,只要繼承父類即可,比如 Person類中的屬性和行爲在 Student、Teacher等類中也有,後者只需要繼承前者即可擁有Person的所有屬性、行爲。

2.1 定義方法

//定義父類
public class Person {
    public String name;
    public int age;
    public String getInfo() {...}
}
//使用extends關鍵字定義繼承的子類,意即對父類的"擴展".
public class Student extends Person{
    public String school;
}
//Student類繼承了父類Person的所有屬性和方法,並增加了一個屬性school.
//Person中的屬性和方法,Student都可以利用.
//如果子類和父類不在同一個包下,子類只能使用父類中public和protected修飾的成員

繼承的出現提高了代碼的複用性。
繼承的出現讓類與類之間產生了關係,提供了多態的前提。
不要僅爲了獲取其他類中某個功能而去繼承,繼承的類之間應該有邏輯性
子類不能直接訪問父類中私有的(private)的成員變量和方法,應通過setter、getter方法訪問

注意: 一個子類只能有一個父類,一個父類可以派生出多個子類,Java支持多層繼承,但不支持一個子類多個父類這樣的多重繼承

2.2 方法的重寫(override)

(1)概念:在子類中可以根據需要對從父類中繼承來的方法進行改造,子類的方法將覆蓋父類的方法,稱重寫

(2)示例:

e. g. 2-07:

public class Person {
    public String name;
    public int age;
    public String getInfo() {
        return "Name: "+ name + "\n" +"age: "+ age;
    }
}
public class Student extends Person {
    public String school;
    public String getInfo() {       //重寫方法
        return  "Name: "+ name + "\nage: "+ age 
            + "\nschool: "+ school;
    }
}

(3)方法重寫時的注意:

a. 重寫方法必須和被重寫方法具有相同的方法名稱、參數列表和返回值類型。
b. 重寫方法不能使用比被重寫方法更嚴格的訪問權限。
c. 重寫和被重寫的方法須同時爲static的,或同時爲非static的。
d. 子類方法拋出的異常不能大於父類被重寫方法的異常

在eclipse中可以使用快捷鍵 Alt+/ 選擇override進行快速重寫。

3. 面向對象的特徵之三:多態性

3.1 Java中多態性的兩種體現

  1. 方法的重載(overload)和重寫(overwrite)

    **a. 重載:**本類中允許同名方法存在,體現相同的方法實現不同的邏輯。

    b. 重寫: 子類對父類方法的覆蓋,子類可以使用和父類相同的方法名,覆蓋掉父類的邏輯。

  2. **對象的多態性:**子類的對象可以替代父類的對象使用,可以直接應用在抽象類和接口上。

  3. Java引用變量有兩個類型:

    ​ 編譯時類型和運行時類型。編譯時類型由聲明該變量時使用的類型決定,運行時類型由實際賦給該變量的對象決定。若編譯時類型和運行時類型不一致,就出現多態(Polymorphism)。

3.2 對象的多態性

  1. 一個變量只能有一種確定的數據類型,但一個引用類型變量可能指向(引用)多種不同類型的對象,例如:

    Person p = new Person();
    Person e = new Student(); // Person類型的變量e,指向Student類型的對象
    
  2. 子類可看做是特殊的父類,所以父類類型的引用可以指向子類的對象:向上轉型(up-casting)。即子類的對象可以給父類類型的變量作爲引用。

  3. 一個引用類型變量如果聲明爲父類的類型,但實際引用的是子類對象,那麼該變量就不能再訪問子類中添加的屬性和方法。例如:

    Student m = new Student();
    m.school = “BNU”; 	//合法,Student類有school成員變量
    Person e = new Student(); 
    e.school = “BNU”;	//非法,Person類沒有school成員變量
    //屬性是在編譯時確定的,編譯時e爲Person類型,沒有school成員變量,因而編譯錯誤。
    
  4. 虛擬方法調用

//正常的方法調用
Person p = new Person();
p.getInfo();
Student s = new Student();
s.getInfo();
//虛擬方法調用(多態情況下)
Person e = new Student();
e.getInfo();	//調用Student類的getInfo()方法
//編譯時類型和運行時類型,編譯時e爲Person類型,而方法的調用是在運行時確定的,所以調用的是Student類的getInfo()方法。
//Java的方法是運行在棧內存中的,在運行方法時會動態進棧和出棧--動態綁定.

多態小結:

前提

​ a. 需要存在繼承或者實現關係
​ b. 要有覆蓋操作

成員方法

​ 編譯時:要查看引用變量所屬的類中是否有所調用的方法.
​ 運行時:調用實際對象所屬的類中的重寫方法.
​ 成員方法的多態性,也就是多態綁定,必須在有方法的重寫前提下才能進行.

成員變量

​ 不具備多態性,只看引用變量所屬的類.


第4章 高級類特性01

1. 關鍵字-this

作用:

	01.	它在方法內部使用,即表示這個方法所屬對象的引用;
	2.	它在構造器內部使用,表示該構造器正在初始化的對象;
            		3.	this表示當前對象,可以調用類的屬性、方法和構造器;

使用時機: 當在方法內需要用到調用該方法的對象時,就用this。

使用this調用屬性方法代碼示例e. g. 4-01:

/**
 * a.當形參與成員變量重名時,如果在方法內部需要使用成員變量,
 * 必須添加this來表明該變量時類成員
 * b.在任意方法內,如果使用當前類的成員變量或成員方法可以在其前面添加this,
 * 增強程序的閱讀性
 */
class Person{		// 定義Person類
    private String name ;						
    private int age ;						
    public Person(String name,int age){	
        this.name = name ;   	   	//這裏必須用this指針表明name爲類的屬性,而非形參
        this.age = age ;  }
    public void getInfo(){	
        System.out.println("姓名:" + name) ;
        this.speak();
    }
    public void speak(){
        System.out.println(“年齡:” + this.age);	
    }
}

class Person{					// 定義Person類
    private String name ;		
    private int age ;			
    public Person(){	 		// 無參構造
        System.out.println("新對象實例化") ;
    }
    public Person(String name){
        this();      			// 調用本類中的無參構造方法,且放在構造方法的首行
        this.name = name ;	
    }
    public Person(String name,int age){	
        this(name) ;  			// 調用有一個參數的構造方法,且放在構造方法的首行
        this.age = age;
    }
    public String getInfo(){	
        return "姓名:" + name + ",年齡:" + age ;
    }  
}
/**
			 * c.this可以作爲一個類中,構造器相互調用的特殊格式
			 * d.使用this()必須放在構造器的首行
			 * e.使用this調用本類中其他的構造器,保證至少有一個構造器是不用this的。
			 *(實際上就是不能出現構造器自己調用自己)
			 */

2. 關鍵字-super

作用:

  1. ​ super可用於訪問父類中定義的屬性,方法,以及父類的構造函數。
  2. ​ a. 尤其當子父類出現同名成員時,可以用super進行區。
    ​ b. super的追溯不僅限於直接父類,還可以調用子類之上的所有父類。
    ​ c. super和this的用法相像,this代表本類對象的引用,super代表父類的內存空間的標識。

使用方法: 使用 super.+父類成員 進行調用。

注意:

a.子類中所有的構造器默認都會訪問父類中空參數的構造器

b.當父類中沒有空參數的構造器時,子類的構造器必須通過this(參數列表)或者super(參數列表)語句指定調用本類或者父類中相應的構造器,且必須放在構造器的第一行

c.如果子類構造器中既未顯式調用父類或本類的構造器,且父類中又沒有無參的構造器,則編譯出錯

代碼示例: e. g. 4-02:

//定義父類
public class Person {

private String name;
private int age;
private Date birthDate;	

public Person(String name, int age, Date d) {
        this.name = name;
        this.age = age;
        this.birthDate = d;
    }
public Person(String name, int age) {
        this(name, age, null);
	}
public Person(String name, Date d) {
        this(name, 30, d);
 	}
public Person(String name) {
        this(name, 30);
	}

}

//調用父類
public class Student extends Person {
private String school;

public Student(String name, int age, String s) {
          super(name, age);
          school = s;
    }
public Student(String name, String s) {
          super(name);
          school = s;
	}
public Student(String s) { // 編譯出錯: no super(),系統將調用父類無參數的構造方法。
          school = s;
	}
}

3. this和super的區別

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-BOopuUbD-1587194076064)(Java Record.assets/20200213201203822.png)]

4. JavaBean

概念: JavaBean 是一種Java語言寫成的可重用組件。所謂 JavaBean,是指符合如下標準的Java類:

​ a. 類是公共的;
​ b. 有一個無參的公共的構造器;
​ c. 有屬性,屬性一般是私有的,且有對應的get、set方法;

用戶可以認爲JavaBean提供了一種隨時隨地的複製和粘貼的功能,而不用關心任何改變。

對於eclipse,在創建了類和其中的屬性後,可以使用 右鍵 -> Source -> Generate Getters and Setters.

5. instanceof操作符

作用:x instanceof A 檢驗x是否爲類A的對象,返回值爲boolean型。

要求x所屬的類與類A必須是子類和父類的關係,否則編譯錯誤。

如果x屬於類A的子類B,x instanceof A 值也爲true。

6. Object類

概念: Object類是所有Java類的根父類(基類)。在多層繼承中處於最高層的父類一定是Object類。

如果在類的聲明中未使用extends關鍵字指明其父類,則默認父類爲Object類 :

示例: e. g. 4-03:

public class Person {
    ...
}
//等價於:
public class Person extends Object {
    ...
}

**作用:**可以用於接收作爲參數的類。

示例:e. g. 4-04:

method(Object obj){}//可以接收任何類作爲其參數
Person o=new Person();  
method(o);

Object類中的方法:

​ Object是所有類的父類,子類可以調用父類的方法,所以Object中的方法在其他所有類中都可以使用 對象.方法()調用。

方法列表:

num 方法 類型 描述
a public Object() 構造 構造方法
b public boolean equals(Object obj) 普通 對象比較
c public int hashCode() 普通 取得Hash碼
d public String toString() 普通 對象打印時調用

7. 對象類型轉換(Casting)

對Java對象的強制類型轉換稱爲造型

a. 從子類到父類的類型轉換可以自動進行。
b. 從父類到子類的類型轉換必須通過造型(強制類型轉換)實現。
c. 無繼承關係的引用類型間的轉換是非法的。
d. String類是Object類的子類,所以也滿足上面的法則。

對象類型轉換代碼示例:e. g. 4-05:

public class Test{
    public void method(Person e){	 //設Person類中沒有getschool()						方法
        // System.out.pritnln(e.getschool());   //非法,編譯時錯誤,因爲e在形參傳入時是Person類的對象,沒有.getschool()方法,應該進行判斷和轉換

        if(e  instanceof  Student){
            Student me = (Student)e;	//將e強制轉換爲Student類型
            System.out.pritnln(me.getschool());
        }	    
    }
    public static  void main(Stirng args[]){
        Test t = new Test();
        Student m = new Student();
        t.method(m);
    }
}

8. ==操作符與equals方法

8.1 ==操作符

  1. 基本類型比較值:只要兩個變量的值相等,即爲true。
  2. 引用類型比較引用(是否指向同一個對象):只有指向同一個對象時(在內存中的地址),==才返回true。
  3. 用“==”進行比較時,符號兩邊的數據類型必須兼容(可自動轉換的基本數據類型除外),否則編譯出錯。

8.2 equals()方法

  1. 所有類都繼承了Object,也就獲得了equals()方法,還可以對方法進行重寫。
  2. 只能比較引用類型,其作用與“==”相同,比較是否指向同一個對象(地址)。
  3. 調用格式:obj1.equals(obj2);
  4. 特例:當用equals()方法進行比較時,對類File、String、Date及包裝類來說,因爲在這些類中重寫了Object類的equals()方法,所以比較的是類型及內容,而不是引用的是否是同一個對象。

9. String對象的創建

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ogDjgu9I-1587194076066)(Java Record.assets/20200213202631107.png)]

10. toString()方法

**作用:**toString()方法在Object類中定義,其返回值是String類型,返回類名和它的引用地址。

打印對象時,默認輸出對象的內存地址,即等價於輸出 對象.toString()

11. 包裝類(Wrapper)

11.1 概念及作用

​ 包裝類(封裝類)是針對八種基本定義相應的引用類型。基本數據類的包裝類主要用於基本數據類型與字符串之間的轉換。

​ 基本數據類型的實例化轉爲對應的包裝類後,就有了類的特點,就可以調用類中的方法。

11.2 基本數據類型的包裝類

基本數據類型 包裝類
boolean Boolean
byte Byte
short Short
int Integer
long Long
char Character
float Float
double Double

11.3 基本數據類型包裝成包裝類(裝箱)

//通過包裝類的構造器實現:
int i = 500;   Integer t = new Integer(i);
//還可以通過字符串參數構造包裝類對象:
Float f = new Float("4.56");

11.4 獲得包裝類對象中包裝的基本類型變量(拆箱)

//調用包裝類的.xxxValue()方法:
boolean b = bObj.booleanValue();

JDK 1.5之後,支持自動裝箱,自動拆箱。但類型必須匹配。

11.5 使用包裝類進行字符串與基本數據類型之間的轉換

a. 字符串轉換成基本數據類型

//通過包裝類的構造器實現:
int i = new Integer(12);
//通過包裝類的parseXxx(String s)靜態方法:
Float f = Float.parseFloat(12.1);

b. 基本數據類型轉換成字符串

//調用字符串重載的valueOf()方法:
String fstr = String.valueOf(2.34f);
//更直接的方式:
String intStr = 5 + “”;

第5章 高級類特性02

1. 關鍵字-static

1.1 概念及作用

​ 在Java類中,可用static修飾屬性、方法、代碼塊、內部類。

​ 被修飾後的成員具備以下特點

​ a. 隨着類的加載而加載;
​ b.優先於對象存在;
​ c.修飾的成員,被所有對象所共享;
​ d.訪問權限允許時,可不創建對象,直接被類調用。

1.2 類變量

類變量,即被static修飾的類屬性。

a. 類變量不用實例化,直接 類名.屬性名 就可以使用,是類的一部分,被所有類的實例化對象共享,也稱爲靜態變量。

b. 類變量的使用要小心,因爲只要有一處改動說有對象都會得到變化。

示例代碼:e. g. 5-01:

class Person {
    private int id;
    public static int total = 0;	//使用static修飾
    public Person() {				//構造方法,每new一個對象就使total加一併傳給id
        total++;
        id = total;
    }
}

1.3 類方法

類方法,即被static修飾的類方法。

a. 沒有對象的實例時,可以用**類名.方法名()**的形式訪問由static標記的類方法.

b. 在static方法內部只能訪問類的static屬性,不能訪問類的非static屬性。

c. 因爲不需要實例就可以訪問static方法,因此static方法內部不能有this和super。

d. 重載的方法需要同時爲static修飾或者非static修飾。

示例代碼:e. g. 5-02:

class Person {
    private int id;
    private static int total = 0;
    public static int getTotalPerson() { 
        id++;	//非法
        return total;
    }
    public Person() {
        total++;
        id = total;
    }
}

2. 關鍵字-final

作用:

a. 在Java中聲明類、屬性和方法時,可使用關鍵字final來修飾,表示“最終”。

b. final標記的類不能被繼承。提高安全性,提高程序的可讀性。

c. final標記的方法不能被子類重寫

d. final標記的變量(成員變量或局部變量)即稱爲常量。名稱大寫,且只能被賦值一次。

e. final標記的成員變量必須在聲明的同時或在每個構造方法中或代碼塊中顯式賦值,然後才能使用。

f. static final:全局常量

示例代碼:e. g. 5-03:

public final class Test{
    public static int totalNumber = 5 ;
    public final int ID;
    public Test(){
        ID = ++totalNumber;  //可在構造方法中給final變量賦值
    }
    public static void main(String[] args) {
        Test t = new Test();
        System.out.println(t.ID);		
        final int I = 10;
        final int J;
        J = 20;
        J = 30;  //非法
    }
}

3. 抽象類和抽象方法

定義: Java允許類設計者指定:超類聲明一個方法但不提供實現,該方法的實現由子類提供。這樣的方法稱爲抽象方法。有一個或更多抽象方法的類稱爲抽象類。

聲明: 用abstract關鍵字來修飾一個類時,將該類聲明爲抽象類,用abstract來修飾一個方法時,該方法爲抽象方法:abstract int abstractMethod( int a );

注意:

a. 含有抽象方法的類必須被聲明爲抽象類。

b. 抽象類不能被實例化。抽象類是用來作爲父類被繼承的,抽象類的子類必須重寫父類的抽象方法,並提供方法體。若沒有重寫全部的抽象方法,則仍爲抽象類。

c. 不能用abstract修飾屬性、私有方法、構造器、靜態方法、final的方法。

代碼示例: e. g. 5-04:

		//Vehicle是一個抽象類,有兩個抽象方法。
public abstract class Vehicle{
    int num;
    public abstract double calcFuelEfficiency();	//計算燃料效率的抽象方法
    public abstract double calcTripDistance();	//計算行駛距離的抽象方法
}
public class Truck extends Vehicle{
    public double calcFuelEfficiency(){
        //寫出計算卡車的燃料效率的具體方法   
    }
    public double calcTripDistance(){
        //寫出計算卡車行駛距離的具體方法   
    }
}

public class RiverBarge extends Vehicle{
    public double calcFuelEfficiency(){
        //寫出計算駁船的燃料效率的具體方法  
    }
    public double calcTripDistance(){
        //寫出計算駁船行駛距離的具體方法
    }
}
//注意:抽象類不能實例化,即new Vihicle()是非法的.抽象類可以有構造方法,只是不能直接創建抽象類的實例對象而已。

4. 設計模式

4.1 什麼是設計模式

概念: 設計模式是在大量的實踐中總結和理論化之後優選的代碼結構、編程風格、以及解決問題的思考方式。

4.2. 單例設計模式

概念: 採取一定的方法保證在整個的軟件系統中,對某個類只能存在一個對象實例,並且該類只提供一個取得其對象實例的方法。

目的: 實例化對象的創建需要消耗大量的時間和資源時,或者沒有必要頻繁的new新對象,這種情況就適合用單例設計模式。

4.2.1 餓漢式單例設計

先new好一個對象,有需要時都用這個對象。

示例代碼:e. g. 5-05:

public class Single{
    //private的構造器,使得不能在類的外部使用new創建該類的對象
    private Single() {}
    //私有的,靜態的Single類型的變量,只能在類的內部訪問,並且類創建是就創建該變量
    private static Single onlyone = new Single();
    //getSingle()爲static,不用創建對象即可訪問
    public static Single getSingle() {
        //通過getSingle()方法返回對象,返回上一行創建的類
        return onlyone;			
    }
}

public class TestSingle{
    public static void main(String args[]) {		
        Single  s1 = Single.getSingle();      //訪問靜態方法
        Single  s2 = Single.getSingle();
        if (s1==s2){
            System.out.println("s1 is equals to s2!");
        }
    }
}

4.2.2 懶漢式單例設計

最開始對象爲null,直到被調用才new一個對象,並且之後都用這個對象。

示例代碼:e. g. 5-06:

public class Singleton{
    //1.將構造器私有化,保證在此類的外部,不能調用本類的構造器。
    private Singleton(){}
    //2.創建Singleton類型的static對象
    private static Singleton instance = null;
    //3.設置公共的方法來訪問類的實例
    public static Singleton getInstance(){
        //3.1如果類的實例未創建,那些先要創建,然後返回給調用者:本類。因此,需要static 修飾。
        if(instance == null){
            instance = new Singleton();
        }
        //3.2 若有了類的實例,直接返回給調用者。
        return instance;
    }
}

4.3 模板方法設計模式(Template-method)

概念: 抽象類體現的就是一種模板模式的設計,抽象類作爲多個子類的通用模板,在抽象類中給出的抽象方法相當於文章的各章節標題(模式),子類在抽象類的基礎上進行擴展、改造,但子類總體上會保留抽象類的行爲方式。

目的: 當功能內部一部分實現是確定,一部分實現是不確定的。這時可以把不確定的部分暴露出去,讓子類去實現。

代碼示例:e. g. 5-07:

//程序計時
abstract class Template{
    public final void getTime(){
        long start = System.currentTimeMillis();
        code();									
        long end = System.currentTimeMillis();
        System.out.println("執行時間是:"+(end - start));
    }
    public abstract void code();//將code();程序留白,在子類中根據需要再編寫
}
class SubTemplate extends Template{
    public void code(){
        for(int i = 0;i<10000;i++){
            System.out.println(i);
        } 
    } 
}

4.4 工廠方法(Factory-method)

​ FactoryMethod模式是設計模式中應用最爲廣泛的模式,在面向對象的編程中,對象的創建工作非常簡單,對象的創建時機卻很重要。通過工廠把new對象隔離,通過產品的接口可以接受不同實際產品的實現類,實例的類名發生改變並不影響其他人對類的調用。

5. 接 口(interface)

5.1 概念

​ 接口(interface)是抽象方法和常量值的定義的集合,從本質上講,接口是一種特殊的抽象類,這種抽象類中只包含常量public static final)和方法的定義,而沒有變量和方法的實現。

5.2 作用01

​ 有時必須從幾個類中派生出一個子類,繼承它們所有的屬性和方法。但是,Java不支持多重繼承,只支持多層繼承。有了接口,可以讓子類實現多個接口,就可以得到多重繼承的效果。接口的主要用途就是被實現類實現。(面向接口編程)

5.3 接口的定義與實現

  1. 用interface來定義,接口中的所有成員變量都默認是由public static final修飾的。接口中的所有方法都默認是由public abstract修飾的。
  2. 接口沒有構造器,接口採用多層繼承機制。
  3. 一個類可以實現多個接口,接口也可以繼承其它接口
  4. 與繼承關係類似,接口與實現類之間存在多態性,即接口可以接收一個類的對象,即將對象轉爲接口類型,對象中只包含接口中定義的方法。

定義接口 代碼示例:e. g. 5-08:

//定義接口
public interface InterfaceA{
    int ID = 1;
    void start();
    public void run();
    void stop();
}
//或者按照標準寫法
public interface InterfaceA{
    public static final int ID = 1;
    public abstract void start();
    public abstract void run();
    public abstract void stop();
}

實現接口 代碼示例:e. g. 5-09:

//在類中實現接口
class SubClass implements InterfaceA{
    //實現接口中的抽象方法
}
//實現接口的類中必須提供接口中所有方法的具體實現內容,方可實例化。否則,仍爲抽象類。

含有接口和繼承的類的定義語法:先寫extends,後寫implements

< modifier> class < name> [extends < superclass>]
    [implements < interface> [,< interface>]* ] {
    < declarations>*
}

example: 用Java描述一個會唱歌做飯的老師:

如果使用繼承,則需要多層繼承:

​ 顯然這很不方便,如果單個功能需要修改則在子類中都需要修改。並且上述幾個類之間功能獨立,並沒有明顯的聯繫,使用繼承太過牽強。

​ 這時候可以定義Teacher類繼承Person類,定義Sing、Cooking接口,並讓Teacher類實現這些接口。

代碼示例:e. g. 5-10:

interface Sing { public void sing();}
interface Cooking {public void fry();}
abstract class People{
    String name;
    int age;
    public int eat(){}
} 
class Teacher extends People implements Sing,Cooking{
		public void run() {……}
		public double swim()  {……}
		public int eat() {……}
}

5.4 作用02

​ 當抽象父類中需要新增抽象方法時,子類中必須實現該新增方法,否則子類就應改爲抽象類,同理,子類的子類…都需要作出改變,這樣代價大,不方便。這時可以將新增的抽象方法寫爲一個接口,根據需要在對應的子類中實現接口即可。

代碼示例:e. g. 5-11:

interface Runner { public void run();}
interface Swimmer {public double swim();}
class Creator{public int eat(){}} 
class Man extends Creator implements Runner ,Swimmer{
    public void run() {……}
    public double swim()  {……}
    public int eat() {……}
}

注意: 如果實現接口的類中沒有實現接口中的全部方法,必須將此類定義爲抽象類 。

5.5 接口的多態以及接口的應用

接口只是一個規範,所以裏面的方法都是空的。
假如我開了一個寵物糧店,聲明所有寵物都可以來我這裏買糧食,這就相當於一個接口,

public interface PetRestaurant {
 	public void buy();
}

當一隻狗看到了,知道自己是寵物,所以它去實現這個接口

public class DogPet implements PetRestaurant {
 	@Override
 	public void buy() {
  		System.out.println("我是狗,我要買狗糧");
 	}
}

當一隻貓看到了,知道自己也是寵物,所以也去實現這個接口

public class CatPet implements PetRestaurant {
 	@Override
    public void buy() {
  		System.out.println("我是貓,我要買貓糧");
    }
}

當狗和貓來我的店之前,我是不知道他們到底是什麼,但是當他們來到我的店,我就知道一個要貓糧食,一個要狗糧食。因爲他們都實現了 我這個接口,都可以買。下面這個類相當於一個接待顧客的類,即店小二,他接待所有實現了我這個寵物店接口的動物,傳進來一個PetRestaurant 類型的寵物,注意,這個PetRestaurant 是接口

public class test {
 	public void buy(PetRestaurant pet){
  		pet.buy();
 	}
}

好了,這個時候我這個老闆出現了,我可以給他們賣糧食了,相當於測試類

public class Tests {
 	public static void main(String[] args) {
  		PetRestaurant dog = new DogPet();  //實例化一個狗,相當於把狗顧客實例化
  		PetRestaurant cat = new CatPet();//實例化一個貓,相當於把貓顧客實例化

 	test t = new test();  //實例化一個店小二
  	t.buy(cat);  //把貓交給店小二
  	t.buy(dog); //把狗交給店小二
 	}
}
//這樣運行的結果是
//我是貓,我要買貓糧
//我是狗,我要買狗娘

你知道嗎,整個過程我這個店主其實根本不知道來的到底是貓是狗還是其他什麼,我只要有一個店小二,把這些來的不知什麼動物都全部交給店小二,店小二就知道怎麼去賣了,因爲這些狗啊貓啊都實現了我這個寵物店的接口,而店小二就負責接待所有實現了我這個接口的動物。這就有一個好處,假如明天來了一頭小豬,只要它實現了我這個接口,我只管交給店小二處理就OK了,我這個店小二根本不需要變化,我這個店主也只需要實例化一下這個動物就OK
你想,假如沒有接口,會怎麼辦,來一個貓,我要去創造一個貓,還要實例化,來一隻狗,我要創建一隻狗,同樣要實例化,還要配備專門的店小二去接待,就會相當麻煩。

5.6 接口的繼承

接口也可以繼承另一個接口,使用extends關鍵字。

interface MyInterface{
		public static final String s=“MyInterface”;
		public void absM1();
}
interface SubInterface extends MyInterface{
		public void absM2();
}
public class SubAdapter implements SubInterface{
		public void absM1(){System.out.println(“absM1”);}
		public void absM2(){System.out.println(“absM2”);}
}
//實現類SubAdapter必須給出接口SubInterface以及父接口MyInterface中所有方法的實現。

抽象類和接口:

​ 抽象類是對一類事物的高度抽象,其中既有屬性也有抽象,方法;接口是對方法的抽象,不能在接口中定義常規屬性,即一系列動作的抽象。所以,當需要對一類事物的抽象時,就用抽象類,以形成父類;當需要對一系列動作進行抽象時,就使用接口,在需要這些動作的類中實現這些接口。

面向對象總結

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-iN1qvH52-1587194076067)(Java Record.assets/image-20200322220926716.png)]


第六章 異 常

1. 異常的分類

對於遇到的錯誤,一般有兩種解決方法:

  1. 遇到錯誤就終止程序的運行。

  2. 由程序員在編寫程序時,就考慮到錯誤的檢測、錯誤消息的提示,以及錯誤的處理。

Java程序運行過程中所發生的異常事件可分爲兩類:

Error: JVM系統內部錯誤、資源耗盡等嚴重情況.

Exception: 其它因編程錯誤或偶然的外在因素導致的一般性問題,例如:空指針訪問、試圖讀取不存在的文件、網絡連接中斷等。

程序員通常只能處理Exception,而對Error無能爲力。

java異常類層次

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-3vHw0nca-1587194076068)(Java Record.assets/image-20200322222022155.png)]

常見異常

  1. RuntimeException
    • 錯誤的類型轉換
    • 數組下標越界
    • 空指針訪問
  2. IOExeption
    • 從一個不存在的文件中讀取數據
    • 越過文件結尾繼續讀取EOFException
    • 連接一個不存在的URL

2. 異常處理機制

​ 在編寫程序時,經常要在可能出現錯誤的地方加上檢測的代碼,如進行x/y運算時,要檢測分母爲0,數據爲空,輸入的不是數據而是字符等。過多的分支會導致程序的代碼加長,可讀性差。因此採用異常機制:將異常處理的程序代碼集中在一起,與正常的程序代碼分開,使得程序簡潔,並易於維護。

  • Java提供的是異常處理的抓拋模型

  • Java程序的執行過程中如出現異常,會自動生成一個異常類對象,該異常對象將被提交給Java運行時系統,這個過程稱爲拋出(throw)異常。

  • 如果一個方法內拋出異常,該異常會被拋到調用方法中。如果異常沒有在調用方法中處理,它繼續被拋給這個調用方法的調用者。這個過程將一直繼續下去,直到異常被處理。這一過程稱爲捕獲(catch)異常。

  • 如果一個異常回到main()方法,並且main()也不處理,則程序運行終止。

異常處理格式:

//異常處理是通過try-catch-finally語句實現的。 
try{
    ......	//可能產生異常的代碼
}
catch( ExceptionName1 e1 )
{
    ......	//當產生ExceptionName1型異常時的處置措施,也可使用所有異常的父類Exception
}
catch( ExceptionName2 e2 )
{
    ...... 	//當產生ExceptionName2型異常時的處置措施
        //與其它對象一樣,可以訪問一個異常對象的成員變量或調用它的方法。
        //getMessage() 方法,用來得到有關異常事件的信息
        //printStackTrace()用來跟蹤異常事件發生時執行堆棧的內容。

}  
[ finally{
    ......	//無條件執行的語句
        //不論在try、catch代碼塊中是否發生了異常事件,finally塊中的語句都會被執行。
        //finally語句是可選的

}  ]

還可進行拋出異常聲明重寫方法聲明拋出異常人工拋出異常創建用戶自定義異常類

聲明拋出異常是Java中處理異常的第二種方式

如果一個方法(中的語句執行時)可能生成某種異常,但是並不能確定如何處理這種異常,則此方法應顯式地聲明拋出異常,表明該方法將不對這些異常進行處理,而由該方法的調用者負責處理。

在方法聲明中用 throws 子句可以聲明拋出異常的列表,throws後面的異常類型可以是方法中產生的異常類型,也可以是它的父類。

這裏只關注異常的捕獲,其他處理方式不詳細瞭解。

3. 異常處理代碼示例(捕獲)

數組越界

e. g. 6-01:

public class Test01{
	public static void main(String[] args){
		String friends[] = {"Lisa","billy","Nessy"};
      	try{
		    for(int i=0;i<5;i++) {
           	System.out.println(friends[i]);
           }
      	}catch(ArrayIndexOutOfBoundsException e){//異常處理
           System.out.println("index err");
      	}
      	System.out.println("\nthis is the end");
  }
}

運行結果:

Lisa billy Nessy
index err this is the end


第七章 Java集合

1.集合概述

01. Java集合類存放於 java.util 包中,是一個用來存放對象的容器。
a.集合只能存放對象。比如你存一個 int 型數據 1放入集合中,其實它是自動轉換成 Integer 類後存入的,Java中每一種基本類型都有對應的引用類型。
b.集合存放的是多個對象的引用,對象本身還是放在堆內存中。
c.集合可以存放不同類型,不限數量的數據類型。

02 .Java 集合可分爲 Set、List 和 Map 三種大體系
a. Set:無序、不可重複的集合
b. List:有序,可重複的集合
c. Map:具有映射關係的集合

2. Set

2.1. HashSet

01 . HashSet 是 Set 接口的典型實現,我們大多數時候說的set集合指的都是HashSet,按 Hash 算法來存儲集合中的元素,因此具有很好的存取和查找性能

02 .HashSet 具有以下特點:
a.不能保證元素的排列順序
b.不可重複
HashSet 不是線程安全的
集合元素可以使 null

遍歷方法:

03 .使用 Iterator 接口遍歷集合元素
a. Iterator 接口主要用於遍歷 Collection 集合中的元素,Iterator 對象也被稱爲迭代器

04 .使用 for each 循環遍歷集合元素

代碼案例:

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set
public class Test_HashSet {
	public static void main(String[] args) {
		Set set = new HashSet();
		set.add(1);						//向Hash集合中添加元素
		set.add("a");
		
		System.out.println(set);
		
		set.remove(1);					//移除元素
		
		System.out.println(set);
		
		System.out.println(set.contains(1)); //判斷元素是否存在
		
		set.clear();		//清空集合
		
		System.out.println(set);
		
		set.add("a");
		set.add("b");
		set.add("c");
		set.add("d");
		System.out.println(set);
		//使用迭代器接口遍歷集合
		Iterator it = set.iterator();
		
		while(it.hasNext()){
			System.out.println(it.next());
		}
		
		//for each迭代集合,推薦使用
		for(Object obj : set){//意思是把set的每個值取出來,賦值給obj,直到循環set的所有值
			System.out.println(obj);
		}
		
		System.out.println(set.size());	//獲取集合元素個數
		
		//如果想讓集合只能存同樣類型的對象,使用泛型
		Set<String> set1 = new HashSet<String>();//比如指定String爲集合的泛型,那麼這個集合不能存String之外的數據
		set.add("abc");
		System.out.println(set);
		
		//Set set = new HashSet();  等價於 Set<Object> set = new HashSet<Object>();
	}
	
}

2.2. TreeSet

01 . TreeSet 是 SortedSet 接口的實現類,TreeSet 可以確保集合元素處於排序狀態。TreeSet 支持兩種排序方法:自然排序和定製排序。默認情況下,TreeSet 採用自然排序。

02.定製排序:如果需要實現定製排序,則需要在創建 TreeSet 集合對象時,提供一個 Comparator 接口的實現類對象。由該 Comparator 對象負責集合元素的排序邏輯。

代碼示例:

import java.util.Comparator;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
public class Test_TreeSet {
	public static void main(String[] args) {
//		Set<Integer> set = new TreeSet<Integer>();	//TreeSet必須放入相同類型的對象,或直接用泛型限定
//		TreeSet自然排序
//		set.add(5);
//		set.add(2);
//		set.add(4);
//		set.add(3);
//		System.out.println(set);
//		set.remove(5);
//		set.contains(3);
//		set.clear();
//		
//		//使用迭代器遍歷集合
//		Iterator<Integer> it = set.iterator();
//		while(it.hasNext()){
//			System.out.println(it.next());
//		}
//		//for each迭代集合,推薦
//		for(Integer i : set){
//			System.out.println(i);
//		}
		
		Person p1 = new Person("張三",23);
		Person p2 = new Person("lisi",20);
		Person p3 = new Person("wangwu",16);
		Person p4 = new Person("Lucy",29);
		
		Set<Person> set = new TreeSet<Person>(new Person()); 
        //這裏傳入構造,因爲在TreeSet源碼裏構造方法能傳一個Comparator比較器的參數,按傳參的比較器的定義來排序
		set.add(p1);
		set.add(p2);
		set.add(p3);
		set.add(p4);
		
		for(Person p : set){
			System.out.println(p.name+"  "+ p.age);
		}
		
	}
}
//定製排序
class Person implements Comparator<Person>{	//把Person對象存到TreeSet中並且按照年齡排序
	int age;
	String name;
	
	public Person(){
		
	}
	public Person(String name, int age){
		this.name = name;
		this.age = age;
	}
	
	@Override
	public int compare(Person o1, Person o2) {//重寫Comparator中的排序,按年齡正序排列
		if(o1.age > o2.age){
			return 1;
		}else if(o1.age < o2.age){
			return -1;
		}else{
			return 0;
		}
	}
}

3. List與ArrayList

01 . List 代表一個元素有序、且可重複的集合,集合中的每個元素都有其對應的順序索引
List 允許使用重複元素,可以通過索引來訪問指定位置的集合元素。
List 默認按元素的添加順序設置元素的索引。

代碼示例:

import java.util.ArrayList;
import java.util.List;

public class Test_ArrayList {
	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();
		list.add("b");	//下標爲0
		list.add("c");	//下標爲1
		list.add("d");	//下標爲2
		list.add("a");	//下標爲3
		list.add("d");	//允許重複
		System.out.println(list);
		System.out.println(list.get(2));
		
		list.add(1, "f");	//在指定索引下標位置插入
		System.out.println(list);
		
		//在指定下標插入集合,集合數據類型必須與與其一致
		List<String> list02 = new ArrayList<String>();
		list02.add("123");
		list02.add("456");
		
		list.addAll(2, list02);	//插入集合
		System.out.println(list);
		
		System.out.println(list.indexOf("d"));	//查找元素第一次出現的下標,沒有則輸出-1
		System.out.println(list.lastIndexOf("d"));	//查找元素最後一次出現的下標
		
		list.remove(2);	//根據索引移除元素
		list.set(3, "ff");	//根據指定的索引修改值
		System.out.println(list);
		
		System.out.println(list.subList(2, 5));	//list.subList(a, b),截取a到b-1的元素
		System.out.println(list.size());
		
	}
}

4. Map

01 . Map 用於保存具有映射關係的數據,因此 Map 集合裏保存着兩組值,一組值用於保存 Map 裏的 Key,另外一組用於保存 Map 裏的 Value。

02 . Map 中的 key 和 value 都可以是任何引用類型的數據,Map中的Key不允許重複。

03 . Map常用的兩種爲HashMap和TreeMap。

代碼示例:

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

public class Test_HashMap {
	public static void main(String[] args) {
		Map<String, Integer> map = new HashMap<String, Integer>();
		map.put("a", 1);	//使用.put();方法添加元素
		map.put("b", 4);
		map.put("c", 4);
		map.put("d", 8);
		System.out.println(map);
		
		System.out.println(map.get("b"));	//根據key取值
		map.remove("c");
		System.out.println(map);			//根據key移除元素
		System.out.println(map.size());		//映射的個數
		System.out.println(map.containsKey("f"));	//判斷當前的集合是否包含key
		System.out.println(map.containsValue(4));	//判斷當前的集合是否包含value
		
//		map.clear();								//清空集合
		
		//遍歷map
		map.keySet();				//可以獲取所有的key集合
		map.values();				//可以獲取所有的values集合
		System.out.println(map.keySet());
		System.out.println(map.values());
	
		//1.可以通過map.keySet()來遍歷
		Set<String> keys = map.keySet();
		
		for(String key : keys){
			System.out.println("key:"+ key + ", value:"+ map.get(key));
		}
		
		//2.通過map.entrySet();遍歷
		Set<Entry<String, Integer>> entrys = map.entrySet();
		for(Entry<String, Integer> en : entrys){
			System.out.println("key:" + en.getKey() +", value:" +en.getValue());
		}
		
		//TreeMap可以按自然排序(字典排序),也可自定義排序(未舉例)
		Map<Integer, String> tree_map = new TreeMap();
		tree_map.put(3, "a");
		tree_map.put(1, "b");
		tree_map.put(4, "c");
		tree_map.put(2, "d");
		System.out.println(tree_map);
	}
}

5. 操作集合的工具類:Collections

Collections 是一個操作 Set、List 和 Map 等集合的工具類

排序操作:

​ reverse(List):反轉 List 中元素的順序
​ shuffle(List):對 List 集合元素進行隨機排序
​ sort(List):根據元素的自然順序對指定 List 集合元素按升序排序
​ sort(List,Comparator):根據指定的 Comparator 產生的順序對 List 集合元素進行排序
​ swap(List,int, int):將指定 list 集合中的 i 處元素和 j 處元素進行交換

查找和替換:

Object max(Collection):根據元素的自然順序,返回給定集合中的最大元素

Object max(Collection,Comparator):根據 Comparator 指定的順序,返回給定集合中的最大元素

Object min(Collection)

Object min(Collection,Comparator)

int frequency(Collection,Object):返回指定集合中指定元素的出現次數

boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替換 List 對象的所有舊值

因爲Collections類是匿名內部類,可以直接調用,不用(不能)實例化,例如

在已有list的情況下,直接使用如下指令,可翻轉list

Collections.reverse(list);


第八章 泛 型

1. 爲什麼需要泛型(Generic)

​ 泛型,解決數據類型的安全性問題,其主要原理是在類、接口、方法聲明時通過一個標識表示其中某個屬性的類型或者是某個方法的返回值及參數類型。這樣在聲明或實例化時只要指定好需要的具體的類型即可。

​ Java泛型可以保證如果程序在編譯時沒有發出警告,運行時就不會產生ClassCastException異常。同時,代碼更加簡潔、健壯。Java中的泛型,只在編譯階段有效,並且在對象進入和離開方法的邊界處添加類型檢查和類型轉換的方法。也就是說,泛型信息不會進入到運行時階段。

​ 簡單說,泛型就是一個類型限定的方法,可以在類、方法、接口中使用泛型,以便限定參數類型,同時,在未知參數類型時,泛型可以根據接收的參數,自動確認返回類型。

2. 泛型類

指定類的數據類型,集合也同理。
//定義泛型類,在類名後加上<泛型名>,這裏的泛型名可以隨意設置,只是個代號,一般大寫T,意爲Type
class A<T>{
	private T key;
	
	public void setKey(T key){
		this.key = key;
	}
	
	public T getKey(){
		return this.key;
	}
}

對象實例化時不指定泛型,默認爲:Object類並且泛型不同的引用不能相互賦值

3. 泛型接口

在聲明類的時候,需將泛型的聲明也一起加到類中

//定義一個泛型接口
interface Generator<T> {
T next();
}
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

在實現類實現泛型接口時,如已將泛型類型傳入實參類型,則所有使用泛型的地方都要替換成傳入的實參類型

class FruitGenerator implements Generator<String> {
@Override
public String next() {
// TODO Auto-generated method stub
return null;
	}
}

4. 泛型方法

方法也可以被泛型化,不管此時定義在其中的類是不是泛型化的。在泛型方法中可以定義泛型參數,此時,參數的類型就是傳入數據的類型。

public class DAO {
    public <E>  void show(E e){
        System.out.println(e.toString());
    }
    public <T> T show1(T t){
        return t;
    }
}

泛型方法與可變參數

/**
 * 泛型方法與可變參數
 * @param args
 */
public <T> void printMsg( T... args){
    for(T t : args){
       System.out.println("泛型測試 ,t is " + t);
    }
}

靜態方法無法訪問類上定義的泛型,即使靜態方法要使用泛型類中已經聲明過的泛型也不可以

如果靜態方法操作的引用數據類型不確定的時候,必須要將泛型定義在方法上

5. 通配符

/**
 * 不確定集合中的元素具體的數據類型
 * 使用?表示所有類型
 * @param list
 */
public void test(List<?> list){
System.out.println(list);
}

有限制的通配符:

舉例:

<? extends Person> 限制區間: (無窮小 , Person]

只允許泛型爲Person及Person子類的引用調用

<? super Person> 限制區間: [Person , 無窮大)

只允許泛型爲Person及Person父類的引用調用

**<? extends Comparable> **

只允許泛型爲實 現Comparable接口的實現類的引用調用


第九章 枚舉和註解

1. 枚舉類

枚舉類概述:

在某些情況下,一個類的對象是有限而且固定的。例如季節類,只能有 4 個對象。如果手動實現枚舉類,代碼量很大,如下:

枚舉類和普通類的區別

使用 enum 定義的枚舉類默認繼承了 java.lang.Enum

•枚舉類的構造器只能使用 private 訪問控制符

•枚舉類的所有實例必須在枚舉類中顯式列出(, 分隔 ; 結尾). 列出的實例系統會自動添加 public static final 修飾

所有的枚舉類都提供了一個 values 方法, 該方法可以很方便地遍歷所有的枚舉值

JDK 1.5 中可以在 switch 表達式中使用枚舉類的對象作爲表達式, case 子句可以直接使用枚舉值的名字, 無需添加枚舉類作爲限定

•若枚舉只有一個成員, 則可以作爲一種單子模式的實現方式

枚舉類對象的屬性不應允許被改動, 所以應該使用 private final 修飾

•枚舉類使用 private final 修飾的屬性應該在構造器中爲其賦值

•若枚舉類顯式的定義了帶參數的構造器, 則在列出枚舉值時也必須對應的傳入參數

枚舉代碼示例:

public class Test {
	public static void main(String[] args) {
		//Season.SPRING,這段執行就是獲取一個Season的對象
		Season spring = Season.SPRING;
		spring.showInfo();
		
		Season summer = Season.SUMMER;
		summer.showInfo();
		
		Season spring1 = Season.SPRING;
		//每次執行Season.SPRING獲得是相同的對象,枚舉類中的每個枚舉都是單例模式的
		System.out.println(spring.equals(spring1));
		spring1.test();
		
	}
}
//枚舉也可以實現接口
enum Season implements ITest{
	SPRING("春天","春暖花開"),//此處相當於在調用有參的私有構造private Season(String name,String desc)
	SUMMER("夏天","炎炎夏日"),
	AUTUMN("秋天","秋高氣爽"),
	WINTER("冬天","寒風凜冽");
	
	private final String name;
	private final String desc;
	
	private Season(String name,String desc){
		this.name = name;
		this.desc = desc;
	}
	
	public void showInfo(){
		System.out.println(this.name + ": " + this.desc);
	}

	@Override
	public void test() {
		System.out.println("這是實現的ITest接口的test方法");
	}
}
//接口
interface ITest{
	void test();
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9vYUbHnI-1587194076069)(Java Record.assets/image-20200410182710977.png)]

2. 註解

Annotation(註解) 概述: 註解其實就是代碼裏的特殊標記, 這些標記可以在編譯, 類加載, 運行時被讀取, 並執行相應的處理. 通過使用 Annotation, 程序員可以在不改變原有邏輯的情況下, 在源文件中嵌入一些補充信息.

三個基本註解:

•@Override: 限定重寫父類方法, 該註釋只能用於方法

•@Deprecated: 用於表示某個程序元素(類, 方法等)已過時

•@SuppressWarnings: 抑制編譯器警告.

**自定義註解:**使用 @interface 關鍵字自定義註解類(瞭解即可)。


第十章 IO 流

1. 文件流

1.1 File 類

File類:是文件和目錄路徑名的抽象表示形式。File 能新建、刪除、重命名文件和目錄,但 File 不能訪問文件內容本身。如果需要訪問文件內容本身,則需要使用輸入/輸出流。

File類的常用方法

在這裏插入圖片描述

File f = new File("D:/test/abc/testFile.txt");	 	//以文件或者目錄路徑爲參數創建File類

1.2 Java IO 原理

Java程序中,對於數據的輸入/輸出操作以”流(stream)” 的方式進行。

**輸入input:**讀取外部數據(磁盤、光盤等存儲設備的數據)到程序(內存)中。

**輸出output:**將程序(內存)數據輸出到磁盤、光盤等存儲設備中。

流的分類:

根據數據單位分爲:字節流(8 bit),字符流(16 bit)

根據數據流向分爲:輸入流、輸出流

流類。

1.3 文件字節流

對流的處理常出現錯誤,通常try-catch或者throws

1.3.1 文件字節輸入流

/**
	 * 文件字節輸入流FileInputStream
	 *  在讀取文件時,必須保證該文件已存在,否則出異常
	 */
public static void testFileInputStream(){
    try {
        FileInputStream in = new FileInputStream("D:/test/abc/tt1.txt");

        byte[] b = new byte[1024];//設置一個byte數組接收讀取的文件的內容

        int len = 0;//設置一個讀取數據的長度

        //in.read方法有一個返回值,返回值是讀取的數據的長度,如果讀取到最後一個數據,還會向後讀一個,這個時候返回值是-1.
        //也就意味着當in.read的返回值是-1的時候整個文件就讀取完畢了
        
        while((len = in.read(b)) != -1){
            System.out.println(new String(b,0,len));
            //new String(b,0,len),參數1是緩衝數據的數組,參數2是從數組的那個位置開始轉化字符串,參數3是總共轉化幾個字節
        }

        in.close();//注意,流在使用完畢之後一段要關閉
    } catch (Exception e) {
        e.printStackTrace();
    }
}

1.3.2 文件字節輸出流

/**文件字節輸出流FileOutputStream
    * 在寫入一個文件時,如果目錄下有同名文件將被覆蓋
    */
    public static void testFileOutputStream(){
    try {
        FileOutputStream out = new FileOutputStream("D:/test/abc/tt4.txt");//指定向tt4輸出數據(沒有該文件則自動創建。)
        String str = "knsasjadkajsdkjsa";
        out.write(str.getBytes());//把數據寫到內存
        out.flush();//把內存中的數據刷寫到硬盤
        out.close();//關閉流

    } catch (Exception e) {
        e.printStackTrace();
    }
}

1.3.3 使用文件字節流拷貝文件

/**
	 * 複製文件到指定位置
	 * @param inPath 源文件路徑
	 * @param outPanth 複製到的文件夾位置
	 */
public static void copyFile(String inPath, String outPanth){
    try {
        FileInputStream in = new FileInputStream(inPath);//讀取的源文件

        FileOutputStream out = new FileOutputStream(outPanth);//複製到哪裏

        byte[] b = new byte[100];

        int len = 0;

        while((len = in.read(b)) != -1){
            out.write(b, 0, len);//參數1是寫的緩衝數組,參數2是從數組的那個位置開始,參數3是獲取的數組的總長度
        }

        out.flush();//把寫到內存的數據刷到硬盤
        out.close();
        in.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
}

1.4 文件字符流

文件字符流的方法調用和文件字節流只有很少的差別。

1.4.1 文件字符輸入流

/**
	 * 文件字符輸入流FileReader
	 *  在讀取文件時,必須保證該文件已存在,否則出異常
	 * @param inPath
	 */
public static void testFileReader(String inPath){
    try {
        FileReader fr = new FileReader(inPath);//創建文件字符輸入流的對象

        char[] c = new char[10];//使用char數組臨時存數據,是和字節流的唯一區別

        int len = 0;//定義一個輸入流的讀取長度

        while((len = fr.read(c)) != -1){
            System.out.println(new String(c, 0, len));
        }

        fr.close();//關閉流

    } catch (Exception e) {
        e.printStackTrace();
    }
}

1.4.2 文件字符輸出流

/**
	 * 文件字符輸出流FileWriter
	 * 在寫入一個文件時,如果目錄下有同名文件將被覆蓋
	 * @param text 輸出的內容
	 * @param outPath 輸出的文件
	 */
public static void testFileWriter(String text,String outPath){
    try {
        FileWriter fw = new FileWriter(outPath);
        fw.write(text);//寫到內存中
        fw.flush();//把內存的數據刷到硬盤
        fw.close();//關閉流

    } catch (Exception e) {
        e.printStackTrace();
    }
}

1.4.3 使用文件字符流拷貝文件

/**
	 * 字符流完成拷貝文件,字符流只適合操作內容是字符文件
	 * @param inPaht
	 * @param outPath
	 */
public static void copyFile(String inPaht, String outPath){
    try {
        FileReader fr = new FileReader(inPaht);
        FileWriter fw = new FileWriter(outPath);

        char[] c = new char[100];

        int len = 0;

        while((len = fr.read(c)) != -1){//讀取數據
            fw.write(c,0,len);//寫數據到內存
        }

        fw.flush();

        fw.close();
        fr.close();


    } catch (Exception e) {
        e.printStackTrace();
    }
}
}

1.5 文件字節流和文件字符流的區別

​ 字符流是由Java虛擬機將字節轉化爲2個字節的Unicode字符爲單位的字符而成的,所以它對多國語言支持性比較好!如果是音頻文件、圖片、歌曲等文件,字節流更加合適。如果是關係到中文(文本)的,用字符流好點.字節流可用於任何類型的對象,包括二進制對象,而字符流只能處理字符或者字符串。 字節流提供了處理任何類型的IO操作的功能,但它不能直接處理Unicode字符,而字符流就可以。

2. 處理流

2.1 處理流之一:緩衝流

​ 爲了提高數據讀寫的速度,Java API 提供了帶緩衝功能的流類,緩衝流是把數據緩衝到內存中,對讀寫的數據提供了緩衝的功能,提高了讀寫的效率,速率大約是不使用緩衝的75000倍。緩衝流對應文件字節流文件字符流兩種數據操作單位,分爲:

BufferedInputStream BufferedOutputStream 緩衝字節輸入、輸出流

BufferedReader BufferedWriter 緩衝字符輸入、輸出流

2.1.1 緩衝字節輸入流

/**
	 * 緩衝字節輸入流
	 * BufferedInputStream
	 * @throws Exception 
	 */
public static void testBufferedInputStream() throws Exception{
    //文件字節輸入流對象
    FileInputStream in = new 				FileInputStream("D:\\testdemo\\demo\\src\\day13\\tt.txt");

    //把文件字節輸入流放到緩衝字節輸入流對象
    BufferedInputStream br = new BufferedInputStream(in);

    byte[] b = new byte[10];

    int len = 0;

    while((len = br.read(b)) != -1){
        System.out.println(new String(b,0,len));
    }

    //關閉流的時候,本着一個最晚開的最早關,依次關
    br.close();
    in.close();
}

2.1.2 緩衝字節輸出流

/**
	 * 緩衝字節輸出流
	 * BufferedOutputStream
	 */
public static void testBufferedOutputStream() throws Exception{
    //創建字節輸出流對象
    FileOutputStream out = new FileOutputStream("D:\\testdemo\\demo\\src\\day13\\tt1.txt");

    //把字節輸出流對象放到緩衝字節輸出流中
    BufferedOutputStream bo = new BufferedOutputStream(out);

    String s = "hello world";

    bo.write(s.getBytes());//寫到內存中

    bo.flush();//刷到硬盤上

    //關閉流的時候,本着一個最晚開的最早關,依次關
    bo.close();
    out.close();

}

2.1.3 使用緩衝字節流拷貝文件

/**
	 * 緩衝流實現文件的複製
	 */
public static void copyFile() throws Exception{
    //緩衝輸入流
    BufferedInputStream br = new BufferedInputStream(new FileInputStream("D:\\testdemo\\demo\\src\\day13\\tt1.txt"));

    //緩衝輸出流
    BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream("D:\\testdemo\\demo\\src\\day13\\tt2.txt"));

    byte[] b = new byte[1024];

    int len = 0;//設置一個沒出讀取到的數據的長度,直到br.read方法執行到最後(比如說文件中只有hello world,執行到最後一個就讀取d的後面,這個時候返回值就是-1)

    while((len = br.read(b)) != -1){
        bo.write(b, 0, len);//寫到內存
    }

    bo.flush();//刷到硬盤

    bo.close();
    br.close();
}

2.1.4 緩衝字符輸入流

略,和字符流只有char數組和類名不同。

2.1.5 緩衝字符輸出流

略,和字符流只有char數組和類名不同。

2.1.6 使用緩衝字符流拷貝文件

略,和字符流只有char數組和類名不同。

2.2 處理流之二: 轉換流

​ 字符流方式的輸入輸出操作是建立在字符集的基礎上的。
常見字符集

  • ASCII
  • ANSI(GB2321/BIG5/ISO-8859-2/…)
  • Unicode(UTF-8/UTF16BE/UTF16LE/…)
  • Java字符基於Unicode字符集

​ 所有的文件都是有編碼格式,對於我們來說,TXT 和java文件一般來講有三種編碼,ISO8859-1,西歐編碼,是純粹英文編碼,不適應漢字,GBK 和 UTF-8,這兩編碼是適用於中文和英文,我們一般使用UTF-8包含較多漢字,所以加載速度較慢,一般網頁選擇GBK編碼。

轉換流提供了在字節流和字符流之間的轉換,字節流中的數據都是字符時,轉成字符流操作更高效。

2.2.1 轉換字節輸入流爲字符輸入流

/**
	 * 轉換字節輸入流爲字符輸入流
	 * 注意,在轉換字符流的時候,設置的字符集編碼要與讀取的文件的數據的編碼一致
	 * 不然就會出現亂碼
	 * InputStreamReader
	 */
public static void testInputStreamReader() throws Exception{
    FileInputStream fs = new FileInputStream("D:\\testdemo\\demo\\src\\day13\\tt5.txt");

    //把字節流轉換爲字符流
    InputStreamReader in = new InputStreamReader(fs,"GBK");//參數1是字節流,參數2是編碼
    //InputStreamReader in = new InputStreamReader(fs,"UTF-8");//參數1是字節流,參數2是編碼,不統一則會亂碼。

    char[] c = new char[100];
    int len = 0;

    while((len = in.read(c)) != -1){
        System.out.println(new String(c,0,len));
    }

    in.close();
    fs.close();
}

2.2.2 轉換字節輸出流爲字符輸出流

/**
	 * 轉換字節輸出流爲字符輸出流
	 * 注意,在轉換字符流的時候,設置的字符集編碼要與讀取的文件的數據的編碼一致
	 * 不然就會出現亂碼
	 * OutputStreamWriter
	 */
public static void testOutputStreamWriter() throws Exception{
    FileOutputStream out = new FileOutputStream("D:\\testdemo\\demo\\src\\day13\\tt6.txt");

    //OutputStreamWriter os = new OutputStreamWriter(out, "UTF-8");
    OutputStreamWriter os = new OutputStreamWriter(out, "GBK");

    os.write("你好你好");
    os.flush();

    os.close();
    out.close();
}

2.3 處理流之三:標準輸入輸出流

​ System.in 和 System.out 分別代表了系統標準的輸入和輸出設備。在數據量很大時,使用標準輸入流的效率比Scanner要高。

/**
	 * 標準的輸入流
	 * @throws Exception
	 */
public static void testSystemIn() throws Exception{
    //創建一個接收鍵盤輸入數據的輸入流
    InputStreamReader is = new InputStreamReader(System.in);

    //把輸入流放到緩衝流裏
    BufferedReader br = new BufferedReader(is);

    String str = "";//定義一個臨時接收數據的字符串
	//每次讀取一行並打印
    while((str = br.readLine()) != null){
        System.out.println(str);
    }

    br.close();
    is.close();
}
/**
	 * 把控制檯輸入的內容寫到指定的TXT文件中,當接收到字符串over,就結束程序的運行
	 */
public static void write2TXT() throws Exception{
    //創建一個接收鍵盤輸入數據的輸入流
    InputStreamReader is = new InputStreamReader(System.in);

    //把輸入流放到緩衝流裏
    BufferedReader br = new BufferedReader(is);

    BufferedWriter out = new BufferedWriter(new FileWriter("D:\\testdemo\\demo\\src\\day13\\tt7.txt"));

    String line = "";
	//接收到over則停止寫入
    while((line = br.readLine()) != null){
        if(line.equals("over")){
            break;
        }
        //讀取的每一行都寫到指定的TXT文件
        out.write(line);
    }

    out.flush();
    out.close();
    br.close();
    is.close();
}

2.4 處理流之四:打印流

2.5 處理流之五:數據流

數據流,專門用來做基本數據類型的讀寫的流類。

數據輸出流:

/**
	 * 數據輸出流
	 * 用數據輸出流寫到文件的中的基本數據類型的數據,是亂碼的,不能直接辨認出來,需要數據輸入流來讀取
	 * DataOutputStream
	 */
public static void testDataOutputStream() throws Exception{
    DataOutputStream out = new DataOutputStream(new FileOutputStream("D:/testdemo/demo/src/day13/tt8.txt"));

    //		out.writeBoolean(true);
    out.writeDouble(1.35d);
    //		out.writeInt(100);

    out.flush();
    out.close();

}

數據輸入流:

/**
	 * 數據輸入流
	 * 用數據輸出流寫到文件的中的基本數據類型的數據,是亂碼的,不能直接辨認出來,需要數據輸入流來讀取
	 * 用數據輸入流讀取數據輸出流寫到文件中的數據時,要保證使用和當時寫的數據類型一致的類型來讀取
	 * 也就是說,如果寫的時候是writeDouble,讀的時候就得是readDouble
	 * DataInputStream
	 */
public static void testDataInputStream() throws Exception{
    DataInputStream in = new DataInputStream(new FileInputStream("D:/testdemo/demo/src/day13/tt8.txt"));

    System.out.println(in.readDouble());

    in.close();
}

2.6 處理流之六:對象流(對象的序列化與反序列化)

​ 用於存儲和讀取對象的處理流。它的強大之處就是可以把Java中的對象寫入到數據源中,也能把對象從數據源中還原回來。(用途:對象存儲在硬盤(對象的持久化),以及對象的網絡傳輸,因爲兩者都基於二進制,所以,需要將對象進行序列化,讀取時需要反序列化)。

序列化(Serialize):用ObjectOutputStream類將一個Java對象寫入IO流中。序列化的好處在於可將任何實現了Serializable接口的對象轉化爲字節數據,使其在保存和傳輸時可被還原。

​ 如果需要讓某個對象支持序列化機制,則必須讓其類是可序列化的,爲了讓某個類是可序列化的,該類必須實現如下兩個接口之一:

  • Serializable
  • Externalizable

凡是實現Serializable接口的類都有一個表示序列化版本標識符的靜態變量:
private static final long serialVersionUID;
serialVersionUID用來表明類的不同版本間的兼容性
如果類沒有顯示定義這個靜態變量,它的值是Java運行時環境根據類的內部細節自動生成的。若類的源代碼作了修改,serialVersionUID 可能發生變化。故建議,顯示聲明
顯示定義serialVersionUID的用途

  • 希望類的不同版本對序列化兼容,因此需確保類的不同版本具有相同的serialVersionUID
  • 不希望類的不同版本對序列化兼容,因此需確保類的不同版本具有不同的serialVersionUID

使用方法: 創建一個 ObjectOutputStream,調用 ObjectOutputStream 對象的 writeObject(對象) 方法輸出可序列化對象。注意寫出一次,操作flush()。

反序列化(Deserialize):用ObjectInputStream類從IO流中恢復該Java對象。不能序列化static和transient修飾的對象。

使用方法:創建一個 ObjectInputStream,調用 readObject() 方法讀取流中的對象。

代碼示例:

先要創建可序列化的對象所屬類

/**
 * 可以序列化與反序列化的對象
 * 實現Serializable接口
 */
public class Person implements Serializable{

	/**
	 * 一個表示序列化版本標識符的靜態變量
	 * 用來表明類的不同版本間的兼容性
	 */
	private static final long serialVersionUID = 1L;
	
	public String name;
	public int age;

}

注意:對象的序列化與反序列化使用的類要嚴格一致,包名,類名,類機構等等所有都要一致

對象的序列化:

/**
	 * 對象的序列化
	 */
public static void testSerialize() throws Exception{
    //定義對象的輸出流,把對象的序列化之後的流放到指定的文件中
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:/testdemo/demo/src/day13/tt9.txt"));

    //將類實例化爲對象
    Person p = new Person();
    p.name = "zhangsan";
    p.age = 11;

    //寫入內存
    out.writeObject(p);
    out.flush();//刷寫數據到硬盤

    out.close();
}

對象的反序列化:

/**
	 * 對象的反序列化
	 */
public static void testDeserialize() throws Exception{
    //創建對象輸入流對象,從指定的文件中把對象序列化後的流讀取出來
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:/testdemo/demo/src/day13/tt9.txt"));

    Object obj = in.readObject();

    Person p = (Person)obj;

    System.out.println(p.name);
    System.out.println(p.age);

    in.close();

}

2.7 文件的隨機讀寫操作

RandomAccessFile :支持 “隨機訪問” 的方式,程序可以直接跳到文件的任意 地方來讀、寫文件。

RandomAccessFile 對象包含一個記錄指針,用以標示當前讀寫處的位置。

RandomAccessFile 類對象可以自由移動記錄指針:
long getFilePointer():獲取文件記錄指針的當前位置。
void seek(long pos):將文件記錄指針定位到 pos 位置。

創建 RandomAccessFile 類實例需要指定一個 mode 參數,該參數指定 RandomAccessFile 的訪問模式:

  • r: 以只讀方式打開
  • rw:打開以便讀取和寫入
  • rwd:打開以便讀取和寫入;同步文件內容的更新
  • rws:打開以便讀取和寫入;同步文件內容和元數據的更新

代碼示例:

/**
	 * 隨機讀文件
	 */
public static void testRandomAccessFileRead() throws Exception{
    //RandomAccessFile的構造有兩個參數,參數1是讀寫的文件的路徑
    //參數2是指定 RandomAccessFile 的訪問模式
    //r: 以只讀方式打開
    //rw:打開以便讀取和寫入
    //rwd:打開以便讀取和寫入;同步文件內容的更新
    //rws:打開以便讀取和寫入;同步文件內容和元數據的更新
    //最常用是r和rw

    RandomAccessFile ra = new RandomAccessFile("D:/testdemo/demo/src/day13/tt10.txt", "r");

    //ra.seek(0);//設置讀取文件內容的起始點
    ra.seek(8);//通過設置讀取文件內容的起始點,來達到從文件的任意位置讀取

    byte[] b = new byte[1024];

    int len = 0;

    while((len = ra.read(b)) != -1){
        System.out.println(new String(b, 0, len));
    }

    ra.close();
}
/**
	 * 隨機寫
	 */
public static void testRandomAccessFileWrite() throws Exception{
    RandomAccessFile ra = new RandomAccessFile("D:/testdemo/demo/src/day13/tt10.txt", "rw");

    //ra.seek(0);//設置寫的起始點,0代表從開頭寫
    //注意:如果是在文件開頭或者中間的某個位置開始寫的話,就會用寫的內容覆蓋掉等長度的原內容
    ra.seek(ra.length());//設置寫的起始點,ra.length()代表從文件的最後結尾寫,也就是文件的追加

    ra.write("你好".getBytes());

    ra.close();
}

第十一章 反射

​ 反射機制,就是通過一個抽象的類名能夠在自己記憶(加載類的內存)中找到相匹配的類的具體信息。

​ 有沒有一個統一的方式來描述這些類呢?有沒有這樣一個類,可以對隨意的類進行高度的抽象,形成一個可以描述所有類的類?有,就是Class。

​ Reflection(反射)是被視爲動態語言的關鍵,反射機制允許程序在執行期藉助於Reflection API取得任何類的內部信息,並能直接操作任意對象的內部屬性及方法。

Java反射機制提供的功能

  1. 在運行時判斷任意一個對象所屬的類
  2. 在運行時構造任意一個類的對象
  3. 在運行時判斷任意一個類所具有的成員變量和方法
  4. 在運行時調用任意一個對象的成員變量和方法
  5. 生成動態代理

Java反射機制研究及應用:

反射相關的主要API:

java.lang.Class:代表一個類

java.lang.reflect.Method:代表類的方法

java.lang.reflect.Field:代表類的成員變量

java.lang.reflect.Constructor:代表類的構造方法

1. Class類

​ 在Object類中定義了以下的方法,此方法將被所有子類繼承:
public final Class getClass();

​ 以上的方法返回值的類型是一個Class類,此類是Java反射的源頭, 實際上所謂反射從程序的運行結果來看也很好理解,即:可以通過對象反射求出類的名稱.

​ 反射可以得到的信息:某個類的屬性、方法和構造器、某個類到底實現了哪些接口。對於每個類而言,JRE 都爲其保留一個不變的 Class 類型的對象。一個 Class 對象包含了特定某個類的有關信息。
注意:

  1. Class本身也是一個類,Class 對象只能由系統建立對象
  2. 一個類在 JVM 中只會有一個Class實例
  3. 一個Class對象對應的是一個加載到JVM中的一個.class文件
  4. 每個類的實例都會記得自己是由哪個 Class 實例所生成
  5. 通過Class可以完整地得到一個類中的完整結構

Class類的常用方法

方法名
static Class forName(String name) 根據類的全類名(包名+類名)獲取Class對象
Object newInstance() 創建目標類對象
getName() 獲取全類名
Class getSuperclass() 獲取所有的父類的Class對象
Class [] getInterfaces() 獲取所有實現的接口
ClassLoader getClassLoader() 獲取類 的類加載器
Class getSuperclass() 獲取父類的Class對象
Constructor[] getConstructors() 獲取所有的構造器
Field[] getDeclaredFields() 獲取所有的屬性
Method getMethod(String name,Class paramTypes) 獲取對應的方法

代碼示例:

//創建Person類
public class Person {
	public String name;
	int age;
}
//實例化Class類對象的方法
public class Test {
	public static void main(String[] args) {
		Person p = new Person();
		Class clazz = p.getClass();//clazz對象中就包含對象p所屬的Person類的所有的信息
		
		//方法1:若已知具體的類,通過類的.class屬性獲取,該方法最爲安全可靠,程序性能最高
		Class c0 = Person.class;//通過類名.class創建指定類的Class實例
        
		//方法2:已知某個類的實例,調用該實例的getClass()方法獲取Class對象
		Class c1 = p.getClass();//通過一個類的實例對象的getClass()方法,獲取對應實例對象的類的Class實例
		
        //方法3:已知一個類的全類名,且該類在類路徑下,可通過Class類的靜態方法forName()獲取,可能拋出ClassNotFoundException
		try {
			//通過Class的靜態方法forName(String className)來獲取一個類的Class實例
			//forName(String className)方法中的參數是你要獲取的Class實例的類的全路徑(包名.類名)
			Class c2 = Class.forName("day14.Person");//這個是獲取Class實例的常用方式
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
        
        //方法4:
        ClassLoader cl = this.getClass().getClassLoader();
      	Class c3 = cl.loadClass("類的全類名");

		
	}
}

2. 通過反射調用類的完整結構

使用反射可以取得:

格式說明: public Class<?>[] 爲 返回值類型, getInterface()爲調用方法e. g. clazz.getInterface().

  1. 實現的全部接口
    public Class<?>[] getInterfaces()
    確定此對象所表示的類或接口實現的接口。

  2. 所繼承的父類
    public Class<? Super T> getSuperclass()
    返回表示此 Class 所表示的實體(類、接口、基本類型)的父類的 Class。

  3. 全部的構造器

    使用方法見下面第三節

    ​ public Constructor[] getConstructors()
    返回此 Class 對象所表示的類的所有public構造方法,返回的是一個Constructor類數組,其中還有以下方法。
    ​ public Constructor[] getDeclaredConstructors()
    返回此 Class 對象表示的類聲明的所有構造方法(包含private的構造方法)。
    Constructor類中

    取得修飾符: public int getModifiers(); 返回1爲public,2爲private

    取得方法名稱: public String getName();

    取得參數的類型:public Class<?>[] getParameterTypes();

  4. 全部的方法

    使用方法見下面第四節

    ​ public Method[] getDeclaredMethods()
    返回此Class對象所表示的類或接口的全部方法
    ​ public Method[] getMethods()
    返回此Class對象所表示的類或接口的public的方法
    Method類中

    public Class<?> getReturnType()取得全部的返回值

    public Class<?>[] getParameterTypes()取得全部的參數

    public int getModifiers()取得修飾符

  5. 全部的Field

    使用方法見下面第五節

    ​ public Field[] getFields()
    返回此Class對象所表示的類或接口的public的Field。
    ​ public Field[] getDeclaredFields()
    返回此Class對象所表示的類或接口的全部Field
    Field方法中

    public int getModifiers() 以整數形式返回此Field的修飾符

    public Class<?> getType() 得到Field的屬性類型

    public String getName() 返回Field的名稱。

  6. 類所在的包
    使用方法見下面第五節
    Package getPackage()

3. 通過反射創建一個對象

//如果用反射的構造方法來創建對象
try {
    Class clazz = Class.forName("day14.Student");//通過包名.類名的字符串,調用Class.forName方法獲取指定類的Class實例
    Object obj = clazz.newInstance();//相當於調用Student類的無參公有的構造方法
    Student stu = (Student)obj;

    Constructor c = clazz.getConstructor(String.class);//指定獲取有一個參數並且爲String類型的公有的構造方法
    Student stu1 = (Student)c.newInstance("第一中學");//newInstance實例化對象,相當於調用public Student(String school)
    System.out.println(stu1.school);

    //通過反射機制,可以強制的調用私有的構造方法
    Constructor c = clazz.getDeclaredConstructor(String.class,int.class);//指定獲取有兩個參數(String,int)的構造方法

    c.setAccessible(true);//解除私有的封裝,下面就可以對這個私有方法強制調用				
    Student stu = (Student)c.newInstance("zhangsan",12);

} catch (Exception e) {
    e.printStackTrace();
}

4. 通過反射獲取類的方法


try {
    	Class clazz = Class.forName("day14.Student");//通過包名.類名的字符串,調用Class.forName方法獲取指定類的Class實例
		
//    Method[] ms = clazz.getMethods();//獲取到類的所有公有的方法
    Method[] ms = clazz.getDeclaredMethods();//獲取類所有方法,包含公有和私有
    for(Method m : ms){
        System.out.println("方法名:" + m.getName());
        System.out.println("返回值類型:" + m.getReturnType());
        System.out.println("修飾符:" + m.getModifiers());

        Class[] pcs = m.getParameterTypes();//獲取方法的參數類型,是一個數組,方法有幾個參數,數據就有幾個元素
        if(pcs != null && pcs.length > 0){
            for(Class pc : pcs){
                System.out.println("參數類型:" + pc.getName());
            }
        }

        System.out.println("==============================================");

    }
} catch (Exception e) {
    e.printStackTrace();
}

5. 通過反射獲取類的屬性和所屬包


try {
    	Class clazz = Class.forName("day14.Student");//通過包名.類名的字符串,調用Class.forName方法獲取指定類的Class實例
//		Field[] fs = clazz.getFields();//獲取類的公有的屬性,包含父類的公有屬性
			
		Field[] fs = clazz.getDeclaredFields();//獲取本類的(不包括父類的屬性)所有的屬性,包括私有
			
		for(Field f : fs){
			System.out.println("修飾符:" + f.getModifiers());
			System.out.println("屬性的類型:" + f.getType());
			System.out.println("屬性的名稱:" + f.getName());
		}

		Package p = clazz.getPackage();//獲取類所在的包
		System.out.println(p.getName());

} catch (Exception e) {
    e.printStackTrace();
}

6. 通過反射調用指定方法

try {
    Class clazz = Class.forName("day14.Student");//通過包名.類名的字符串,調用Class.forName方法獲取指定類的Class實例
    /**
	* 注意:下面不論是反射調用setInfo還是test方法
	* 都調用的obj對象的方法,obj對象時間上就是student對象
	*/
    //要調用方法,先要有對象
    Constructor con = clazz.getConstructor();//獲取無參構造
    Object obj = con.newInstance();//使用無參構造創建對象

    Method m = clazz.getMethod("setInfo", String.class,String.class);//得到名稱叫setInfo,參數是String,String的方法
    m.invoke(obj, "zhangsan","第一中學");//參數1是需要實例化的對象,後面的參數是調用當前的方法實際參數

    //如果想要調用一個私有方法
    Method m1 = clazz.getDeclaredMethod("test", String.class);//獲取方法名爲test,參數爲1個String類型的方法

    m1.setAccessible(true);//解除私有的封裝,下面可以強制調用私有的方法

    m1.invoke(obj, "李四");

    //調用一個重載方法
    Method m2 = clazz.getMethod("setInfo", int.class);//setInfo的重載方法
    m2.invoke(obj, 1);

    //有返回值的方法
    Method m3 = clazz.getMethod("getSchool");//這是獲取方法名爲getSchool並且沒有參數的方法
    String school = (String)m3.invoke(obj);//調用有返回值的但是沒有參數的方法
    System.out.println(school);

} catch (Exception e) {
    e.printStackTrace();
}

7. 反射調用指定屬性

try {
    Class clazz = Class.forName("day14.Student");//通過包名.類名的字符串,調用Class.forName方法獲取指定類的Class實例
    //反射創建一個對象
    Constructor con = clazz.getConstructor();
    Student stu = (Student)con.newInstance();

    Field f = clazz.getField("school");//獲取名稱爲school的屬性

    f.set(stu, "第三中學");//對stu對象的school屬性設置值"第三中學"
    String school = (String)f.get(stu);//獲取stu對象的school屬性的值
    System.out.println(school);

    //如果是私有的屬性
    Field f1 = clazz.getDeclaredField("privateField");

    f1.setAccessible(true);//解除私有的封裝,下面就可以強制的調用這個屬性

    f1.set(stu, "測試私有屬性");
    System.out.println(f1.get(stu));

} catch (Exception e) {
    e.printStackTrace();
}

8. Java的動態代理

​ 假如一個java項目,其中有100 java類,每個java類有10個方法,這總共1000個方法現在有這樣一個需求,需要在每個java方法上加上2行相同的代碼,在方法執行前輸出這個方法開始執行,在方法執行後輸出這個方法已經完成,手動完成顯然不可能,Proxy :專門完成代理的操作類,是所有動態代理類的父類。通過此類爲一個或多個接口動態地生成實現類。

代碼示例

//接口
public interface ITestDemo {
	void test1();
	void test2();
}
//實現接口的類
public class TestDemoImpl implements ITestDemo {

	@Override
	public void test1() {
		System.out.println("執行test1()方法");
	}

	@Override
	public void test2() {
		System.out.println("執行test2()方法");
	}

}
//動態代理類
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 動態代理類
 *
 */
public class ProxyDemo implements InvocationHandler{

	Object obj;//被代理的對象
	
	public ProxyDemo(Object obj){
		this.obj = obj;
	}
	
	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		
		System.out.println(method.getName() + " 方法開始執行");
		
		Object result = method.invoke(this.obj, args);//執行的是指定代理對象的指定的方法
		
		System.out.println(method.getName() + " 方法執行完畢");
		return result;
	}

}
//主類
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Test {
	public static void main(String[] args) {
		ITestDemo test = new TestDemoImpl();
		/**
		 * 注意:如果一個對象想要通過Proxy.newProxyInstance方法被代理,
		 * 那麼這個對象的類一定要有相應的接口
		 * 就像本類中的ITestDemo接口和實現類TestDemoImpl
		 */
		test.test1();
		test.test2();
		System.out.println("======================");
		/**
		 * 需求:
		 * 在執行test1和test2方法時需要加入一些東西
		 * 在執行方法前打印test1或test2開始執行
		 * 在執行方法後打印test1或test2執行完畢
		 * 打印的方法名要和當時調用方法保存一致
		 */
		
		InvocationHandler handler = new ProxyDemo(test);
		/**
		 * Proxy.newProxyInstance(ClassLoader, interfaces, h)
		 * 參數1是代理對象的類加載器
		 * 參數2是被代理的對象的接口
		 * 參數3是代理對象
		 * 
		 * 返回的值就成功被代理後對象,返回的是Object類型,需要根據當時的情況去轉換類型
		 */
		ITestDemo t = (ITestDemo)Proxy.newProxyInstance(handler.getClass().getClassLoader(), test.getClass().getInterfaces(), handler);
		
		t.test1();
		System.out.println("-----------------------");
		t.test2();
		
	}
}
  • 注意:如果一個對象想要通過Proxy.newProxyInstance方法被代理,
  • 那麼這個對象的類一定要有相應的接口
  • 就像本類中的ITestDemo接口和實現類TestDemoImpl

動態代理步驟

1.創建一個實現接口InvocationHandler的類,它必須實現invoke方法,以完成代理的具體操作。

2.創建被代理的類以及接口

3.通過Proxy的靜態方法

newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 創建一個Subject接口代理

4.通過 Subject代理調用RealSubject實現類的方法


第十二章 線程

1. 基本概念:程序 - 進程 - 線程

程序(program) :是爲完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的代碼,靜態對象。

進程(process): 是程序的一次執行過程,或是正在運行的一個程序。動態過程:有它自身的產生、存在和消亡的過程。

線程(thread):進程可進一步細化爲線程,是一個程序內部的一條執行路徑。

CPU 核心數: 有幾個核心就代表同一時刻能夠處理多少個任務(進程),所以現實中的電腦在不同的進程之間不停快速切換,人類無法察覺。

主頻 就是CPU在不同進程之間切換的頻率。

可以把進程理解爲一條河流,而開啓了線程就相當於河流有了支流。

2. 多線程的創建和啓動

何時需要多線程:

  • 程序需要同時執行兩個或多個任務。

  • 程序需要實現一些需要等待的任務時,如用戶輸入、文件讀寫操作、網絡操作、搜索等。

  • 需要一些後臺運行的程序時。

Java語言的JVM允許程序運行多個線程,它通過java.lang.Thread類來實現。

Thread類的特性:

  1. 每個線程都是通過某個特定Thread對象的run()方法來完成操作的,經常把run()方法的主體稱爲線程體,想要在開啓的多線程中運行的代碼邏輯就寫在run()方法中。
  2. 通過該Thread對象的start()方法來調用啓動這個線程,本質上就是開始運行run()方法

2.1 創建線程的兩種方式

2.1.1. 繼承Thread類

  1. 定義子類繼承Thread類。

  2. 子類中重寫Thread類中的run方法。

  3. 創建Thread子類對象,即創建了線程對象。

  4. 調用線程對象start方法:啓動線程,調用run方法。

代碼示例:

/**
 * 定義子類繼承Thread類
 * 繼承Thread的方式實現多線程
 */
public class TestThread extends Thread{
	@Override
    //重寫run()方法
	public void run() {
		System.out.println("多線程運行的代碼");
		for(int i = 0; i < 5; i++){
			System.out.println("這是多線程的邏輯代碼:" + i);
		}
	}
}
public class Test {
	public static void main(String[] args) {
        
		Thread t0 = new TestThread();//類的多態,用父類接收子類,繼承Thread類的線程
		t0.start();//啓動線程,開始運行run方法中的代碼,後續還可以根據需要開啓更多線程
//		new Test().test();
		System.out.println("---------------1");
		System.out.println("---------------2");
		System.out.println("---------------3");
		
		/**
		 * 多次運行這個main方法之後
		 * 我們發現main方法中打印的3行與開啓線程運行run方法中的打印語句是混合起來
		 * 而且main方法中的打印與run方法中打印語句順序是不固定的
		 * 爲什麼呢?
		 * main執行t0.start()方法開啓多線程之後,就相當於在main方法之外開啓一個支流
		 * 這個個時候t0.start()的之後的main方法的其他代碼的運行就與run方法運行無關了
		 * 以當前代碼爲例
		 * t0.start()的之後的main方法的其他代碼與run方法的代碼並行運行
		 * 就像兩條河流一樣,各走各的
		 * 那麼控制檯輸出的結果就是兩條並行程序的運行結果總和,這個結果需要拆開成兩部分看
		 * 就可以看到,各自是保持自己輸出順序
		 * 這個就是多線程的異步,這個異步相對於執行t0.start()的主程序來說的
		 * 簡單來說開啓了線程之後run方法中運行的代碼主程序中t0.start()之後的程序是並行執行的,沒先後關係,這個叫異步
		 */
	}
	
	public void test(){
		System.out.println("main線程運行的代碼");
		for(int i = 0; i < 5; i++){
			System.out.println("這是main邏輯代碼:" + i);
		}
	}
}

2.1.2 實現Runnable接口

1)定義子類,實現Runnable接口。

2)子類中重寫Runnable接口中的run方法。

3)通過Thread類含參構造器創建線程對象。

4)將Runnable接口的子類對象作爲實際參數傳遞給

Thread類的構造方法中。

5)調用Thread類的start方法:開啓線程,調用

Runnable子類接口的run方法。

代碼示例:

/**
 * 通過實現Runnable接口方式實現多線程
 */
public class TestRunnable implements Runnable{

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + ":Runnable多線程運行的代碼"); //Thread.currentThread().getName()爲獲取線程名稱的方法,配合t4中傳入參數使用
		for(int i = 0; i < 5; i++){
			count++;
			System.out.println(Thread.currentThread().getName() + ":這是Runnable多線程的邏輯代碼" );
		}
	}

}
public class Test {
	public static void main(String[] args) {

		Thread t3 = new Thread(new TestRunnable());
		t3.start();
		
		Thread t4 = new Thread(new TestRunnable(), "t-1");//加入線程名稱參數,在TestRunnable類中
		t4.start();
		Thread t5 = new Thread(new TestRunnable(), "t-2");
		t5.start();
//		new Test().test();
		System.out.println("---------------1");
		System.out.println("---------------2");
		System.out.println("---------------3");

	}
	
	public void test(){
		System.out.println("多線程運行的代碼");
		for(int i = 0; i < 5; i++){
			System.out.println("這是多線程的邏輯代碼:" + i);
		}
	}
}

2.2.繼承方式和實現方式的聯繫與區別

區別:

繼承Thread: 線程代碼存放Thread子類run方法中。重寫run方法

**實現Runnable:**線程代碼存在接口的子類的run方法。實現run方法

實現Runnable接口方式開啓多線程的好處

1)避免了單繼承的侷限性,如果使用繼承Thread方式,因爲Java類不能多重繼承,所以繼承了Thread就不能再繼承其他類了。

2)多個線程可以共享同一個接口實現類的對象,非常適合多個相同線程來處理同一份資源。

所以一般使用實現接口方式來實現多線程

示例代碼:

/**
 * 通過實現Runnable接口方式實現多線程
 */
public class TestRunnable implements Runnable{

	int count = 0;
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + ":Runnable多線程運行的代碼"); //Thread.currentThread().getName()爲獲取線程名稱的方法,配合t4中傳入參數使用
		for(int i = 0; i < 5; i++){
			count++;
			System.out.println(Thread.currentThread().getName() + ":這是Runnable多線程的邏輯代碼:" + count);
		}
	}
}

public class Test {
	public static void main(String[] args) {
        //將new創建的TestRunnable對象接收,然後向t4,t5傳入同一個run,共享資源
        //運行後count會自加10次,從而實現多線程操作同一份資源。
		Runnable run = new TestRunnable();
        
		Thread t4 = new Thread(run, "t-1");
		t4.start();
		Thread t5 = new Thread(run, "t-2");
		t5.start();
//		new Test().test();
		System.out.println("---------------1");
		System.out.println("---------------2");
		System.out.println("---------------3");
	}
	
	public void test(){
		System.out.println("多線程運行的代碼");
		for(int i = 0; i < 5; i++){
			System.out.println("這是多線程的邏輯代碼:" + i);
		}
	}
}

2.3 使用多線程的優點

  • 多線程程序的優點:
  1. 提高應用程序的響應。對圖形化界面更有意義,可增強用戶體驗。

  2. 提高計算機系統CPU的利用率

  3. 改善程序結構。將既長又複雜的進程分爲多個線程,獨立運行,利於理解和修改

如果在一個很長的方法中,各段代碼互不相干,就可以使用多線程處理。

2.4 Thread類的有關方法

void start(): 啓動線程,並執行對象的run()方法
run(): 線程在被調度時執行的操作
String getName(): 返回線程的名稱,如果在創建線程的時候沒有指定名稱,系統會給出默認名稱,通過getName()獲取線程名稱
void setName(String name): 設置該線程名稱
static currentThread(): 返回當前線程

/**

  • 線程的優先級,就是哪個線程有較大個概率被執行
  • 優先級是用數組1-10表示,數字越大優先級越高,如果沒有設置默認優先級是5
    */

getPriority() : 返回線程優先值
setPriority(int newPriority) : 改變線程的優先級

static void yield(): 線程讓步,暫停當前正在執行的線程,把執行機會讓給優先級相同或更高的線程,若隊列中沒有同優先級的線程,忽略此方法

join() :當某個程序執行流中調用其他線程的 join() 方法時,調用線程將被阻塞,直到 join() 方法加入的 join 線程執行完才繼續執行後續代碼。低優先級的線程也可以獲得執行。

static void sleep(long millis)(指定時間:毫秒)令當前活動線程在指定時間段內放棄對CPU控制,使其他線程有機會被執行,時間到後重排隊。拋出InterruptedException異常

stop(): 強制線程生命期結束

boolean **isAlive():**返回boolean,判斷線程是否還活着

示例代碼:

public class Test1 {
	public static void main(String[] args) {
		TestRun run0 = new TestRun();
		TestRun run1 = new TestRun();
		
		Thread t0 = new Thread(run0);
		
		Thread t1 = new Thread(run1);
		
		t0.setName("線程t-0");//設置線程的名稱
		t1.setName("線程t-1");//設置線程的名稱
		
//		t0.setPriority(1);//設置線程的優先級
//		t1.setPriority(10);//設置線程的優先級
		
		t0.start();
		t1.start();
//		System.out.println(t0.getName());//如果在創建線程的時候沒有指定名稱,系統會給出默認名稱,通過getName()獲取線程名稱
//		System.out.println(t1.getName());
		
		/**
		 * 線程的優先級,就是哪個線程有較大個概率被執行
		 * 優先級是用數組1-10表示,數字越大優先級越高,如果沒有設置默認優先級是5
		 */
		
//		System.out.println("t0的優先級:" + t0.getPriority());//獲取線程的優先級
		
		System.out.println("---------------1");
		System.out.println("---------------2");
		
		System.out.println(t1.isAlive());//判斷當前的線程是否存活
		
		t1.stop();//強制線程生命期結束,強制停止此線程
		
		try {
			t0.join();//相當於在這塊把t0的run的代碼插入到這個位置執行
			/**
			 * 專業的說法
			 * 就是阻塞當前的main方法,先不執行System.out.println("---------------3");代碼
			 * 先執行join進來的線程的代碼
			 * join的線程執行完畢之後繼續執行之前main方法阻塞的代碼System.out.println("---------------3");
			 */
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		
		System.out.println("---------------3");
		
		System.out.println(t1.isAlive());
	}
}

class TestRun implements Runnable{
	int count = 0;
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + ":Runnable多線程運行的代碼");
		for(int i = 0; i < 5; i++){
//			try {
//				Thread.sleep(1000);//當前線程睡眠1000毫秒
//				//相當於當前的這個循環每隔1000毫秒執行一次循環
//			} catch (InterruptedException e) {
//				e.printStackTrace();
//			}
			
//			if(i % 2 == 0){
//				Thread.yield();//線程讓步
//			}
			
			count++;
			System.out.println(Thread.currentThread().getName() + ":這是Runnable多線程的邏輯代碼:" + count);
		}
	}
	
}

3. 線程的生命週期

JDK中用Thread.State枚舉表示了線程的幾種狀態

要想實現多線程,必須在主線程中創建新的線程對象。Java語言使用Thread類及其子類的對象來表示線程,在它的一個完整的生命週期中通常要經歷如下的五種狀態

Ø新建: 當一個Thread類或其子類的對象被聲明並創建時,新生的線程對象處於新建狀態

Ø**就緒:**處於新建狀態的線程被start()後,將進入線程隊列等待CPU時間片,此時它已具備了運行的條件

Ø**運行:**當就緒的線程被調度並獲得處理器資源時,便進入運行狀態, run()方法定義了線程的操作和功能

Ø**阻塞:**在某種特殊情況下,被人爲掛起或執行輸入輸出操作時,讓出 CPU 並臨時中止自己的執行,進入阻塞狀態

Ø**死亡:**線程完成了它的全部工作或線程被提前強制性地中止

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Mbg2CPpe-1587194076072)(Java Record.assets/image-20200418140401840.png)]

4. 線程的同步與鎖死

​ 多個線程執行的不確定性引起執行結果的不穩定

​ 多個線程對賬本的共享,會造成操作的不完整性,會破壞數據,即使加入了判斷語句,但是多個線程核能同時滿足條件,從而越過條件(當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。導致共享數據的錯誤。),解決方法:直接在方法上加上同步鎖,限制對多條操作共享數據的語句,只能同時讓一個線程完整執行完,在執行過程中,其他線程不可以參與執行。

Synchronized的使用方法

Java對於多線程的安全問題提供了專業的解決方式:
同步機制

  1. synchronized還可以放在方法聲明中,表示整個方法
    爲同步方法。
    例如:
    public synchronized void show (String name){
    ….
    }

  2. synchronized (對象){
    // 需要被同步的代碼;
    }

    如果針對對象要加同步鎖,那就加在方法上,如果針對某段代碼要加同步鎖,那就直接在代碼塊上加同步鎖

public class Test2 {
	public static void main(String[] args) {
		//定義賬戶對象
		Acount a = new Acount();
		Acount a1 = new Acount();
		
		//多線程對象
		User u_weixin = new User(a, 2000);
		User u_zhifubao = new User(a, 2000);
		
		Thread weixin = new Thread(u_weixin,"微信");
		Thread zhifubao = new Thread(u_zhifubao,"支付寶");
		
		weixin.start();
		zhifubao.start();
	}
}

class Acount{
	public static int money = 3000;//全局變量,所有的操作共享這個變量
	
	/**
	 * 提款,判斷賬戶錢夠不夠
	 * 多線程調用這個方法,就有問題,線程共享資源時,一個線程在執行這個方法沒有完畢時,另一個線程又開始執行這個方法
	 * 解決思路:顯然一個線程整體執行完這個方法,另一個線程再執行
	 * 通過synchronized同步鎖來完成
	 * 可以直接在方法上加上synchronized關鍵字
	 * 在普通方法上加同步鎖synchronized,鎖的是整個對象,不是某一個方法
	 * 不同的對象就是不同的鎖,普通方法加synchronized,線程使用不同的此方法的對象,還有共享資源的問題
	 *  靜態的方法加synchronized,對於所有的對象都是使用同一個一個鎖
	 * 同一個對象的不同方法加同步鎖,鎖的當前方法對應的對象,當前的對象的所有加了同步鎖的方法是共用一個同步鎖
	 * @param m
	 */
	public synchronized void drawing(int m){
		String name = Thread.currentThread().getName();
		
		if(money < m){
			System.out.println(name + "操作,賬戶金額不足:" + money);
		}else{
			System.out.println(name + "操作,賬戶原有金額:" + money);
			System.out.println(name + "操作,取款金額:" + m);
			
			System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
			money = money - m;
			System.out.println(name + "操作,取款後的餘額:" + money);
		}
		
	}
}
class User implements Runnable{
	Acount acount;
	int money;
	public User(Acount acount,int money){
		this.acount = acount;
		this.money = money;
	}
	@Override
	public void run() {
		acount.drawing(money);
	}
	
}

多種加同步鎖方式例程

public class Test2 {
	public static void main(String[] args) {
		//定義賬戶對象
		Acount a = new Acount();
		Acount a1 = new Acount();
		
		//多線程對象
		User u_weixin = new User(a, 2000);
		User u_zhifubao = new User(a, 2000);
		
		Thread weixin = new Thread(u_weixin,"微信");
		Thread zhifubao = new Thread(u_zhifubao,"支付寶");
		
		weixin.start();
		zhifubao.start();
	}
}

class Acount{
	public static int money = 3000;//全局變量,所有的操作共享這個變量
	
	/**
	 * 提款,判斷賬戶錢夠不夠
	 * 多線程調用這個方法,就有問題,線程共享資源時,一個線程在執行這個方法沒有完畢時,另一個線程又開始執行這個方法
	 * 解決思路:顯然一個線程整體執行完這個方法,另一個線程再執行
	 * 通過synchronized同步鎖來完成
	 * 可以直接在方法上加上synchronized關鍵字
	 * 在普通方法上加同步鎖synchronized,鎖的是整個對象,不是某一個方法
	 * 不同的對象就是不同的鎖,普通方法加synchronized,線程使用不同的此方法的對象,還有共享資源的問題
	 * 
	 * 普通方法加同步鎖,鎖的當前方法對應的對象,當前的對象的所有加了同步鎖的方法是共用一個同步鎖
	 * @param m
	 */
	public synchronized void drawing(int m){
		String name = Thread.currentThread().getName();
		
		if(money < m){
			System.out.println(name + "操作,賬戶金額不足:" + money);
		}else{
			System.out.println(name + "操作,賬戶原有金額:" + money);
			System.out.println(name + "操作,取款金額:" + m);
			
			System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
			money = money - m;
			System.out.println(name + "操作,取款後的餘額:" + money);
		}
		
	}
	
	public synchronized void drawing1(int m){
		String name = Thread.currentThread().getName();
		
		if(money < m){
			System.out.println(name + "操作,賬戶金額不足:" + money);
		}else{
			System.out.println(name + "操作,賬戶原有金額:" + money);
			System.out.println(name + "操作,取款金額:" + m);
			
			System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
			money = money - m;
			System.out.println(name + "操作,取款後的餘額:" + money);
		}
		
	}
	
	/**
	 * 靜態的方法加synchronized,對於所有的對象都是使用同一個一個鎖
	 * @param m
	 */
	public static synchronized void drawing2(int m){
		String name = Thread.currentThread().getName();
		
		if(money < m){
			System.out.println(name + "操作,賬戶金額不足:" + money);
		}else{
			System.out.println(name + "操作,賬戶原有金額:" + money);
			System.out.println(name + "操作,取款金額:" + m);
			
			System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
			money = money - m;
			System.out.println(name + "操作,取款後的餘額:" + money);
		}
		
	}
	
	/**
	 * 對代碼塊加入同步鎖
	 * 代碼塊synchronized(this),所有當前的對象的synchronized(this)同步的的代碼都是使用同一個鎖
	 * @param m
	 */
	public void drawing3(int m){
		synchronized(this){//表示當前的對象的代碼塊被加了synchronized同步鎖
			//用this鎖代碼塊是代表當前的對象,如果在其他方法中也有synchronized(this)的代碼塊使用的都是同一個同步鎖
			String name = Thread.currentThread().getName();
			
			if(money < m){
				System.out.println(name + "操作,賬戶金額不足:" + money);
			}else{
				System.out.println(name + "操作,賬戶原有金額:" + money);
				System.out.println(name + "操作,取款金額:" + m);
				
				System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
				money = money - m;
				System.out.println(name + "操作,取款後的餘額:" + money);
			}
		}
	}
	
	public void drawing4(int m){
		synchronized(this){//表示當前的對象的代碼塊被加了synchronized同步鎖
			//用this鎖代碼塊是代表當前的對象,如果在其他方法中也有synchronized(this)的代碼塊使用的都是同一個同步鎖
			String name = Thread.currentThread().getName();
			
			if(money < m){
				System.out.println(name + "操作,賬戶金額不足:" + money);
			}else{
				System.out.println(name + "操作,賬戶原有金額:" + money);
				System.out.println(name + "操作,取款金額:" + m);
				
				System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
				money = money - m;
				System.out.println(name + "操作,取款後的餘額:" + money);
			}
		}
	}
	
	/**
	 * synchronized修飾代碼塊,想要根據不同的對象有不同的鎖
	 * synchronized(a),這個小括號中傳入不同的對象就是不同的鎖
	 * @param m
	 */
	public void drawing5(int m,Acount a){
		synchronized(a){//表示通過方法的參數傳遞進來的對象的代碼塊被加了synchronized同步鎖
			//不同的對象就有不同的同步鎖
			String name = Thread.currentThread().getName();
			
			//線程通信:如果是微信操作的,先不執行,等支付寶操作,支付寶操作完,微信再繼續操作
			if(name.equals("微信")){
				try {
					a.wait();//當前的線程進入等待的阻塞狀態
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			
			if(money < m){
				System.out.println(name + "操作,賬戶金額不足:" + money);
			}else{
				System.out.println(name + "操作,賬戶原有金額:" + money);
				System.out.println(name + "操作,取款金額:" + m);
				
				System.out.println(name + "操作,取款操作:原金額" + money + " - " + "取款金額" + m);
				money = money - m;
				System.out.println(name + "操作,取款後的餘額:" + money);
			}
			
			if(name.equals("支付寶")){
				try {
					a.notify();//喚醒當前優先級最高的線程,進入就緒狀態
//					a.notifyAll();//喚醒當前所有的線程,進入就緒狀態
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}
} 

class User implements Runnable{
	Acount acount;
	int money;
	public User(Acount acount,int money){
		this.acount = acount;
		this.money = money;
	}
	@Override
	public void run() {
//		acount.drawing(money);
//		if(Thread.currentThread().getName().equals("微信")){
////			acount.drawing(money);
//			acount.drawing3(money);
//		}else{
////			acount.drawing1(money);
//			acount.drawing4(money);
//		}
//		acount.drawing2(money);//調用類的靜態方法
		
//		acount.drawing3(money);
		
		acount.drawing5(money, acount);
	}
	
}

5. 線程死鎖問題介紹

死鎖

​ 不同的線程分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖。

解決方法:

Ø專門的算法、原則,比如加鎖順序一致

Ø儘量減少同步資源的定義,儘量避免鎖未釋放的場景

6. 線程通信

wait() notify() notifyAll(),這三個方法只能用於有同步鎖的方法或代碼塊

Ø wait():令當前線程掛起並放棄CPU、同步資源,使別的線程可訪問並修改共享資源,而當前線程排隊等候再次對資源的訪問

Ønotify():喚醒正在排隊等待同步資源的線程中優先級最高者結束等待

ØnotifyAll ():喚醒正在排隊等待資源的所有線程結束等待.

Java.lang.Object提供的這三個方法只有在synchronized方法或synchronized代碼塊中才能使用,否則會報java.lang.IllegalMonitorStateException異常

wait() 方法:在當前線程中調用方法: 對象名.wait()

使當前線程進入等待(某對象)狀態 ,直到另一線程對該對象發出 notify (或notifyAll) 爲止。

調用方法的必要條件:當前線程必須具有對該對象的監控權(加鎖)

調用此方法後,當前線程將釋放對象監控權 ,然後進入等待

在當前線程被notify後,要重新獲得監控權,然後從斷點處繼續代碼的執行。

notify()/notifyAll():

在當前線程中調用方法: 對象名.notify()

功能:喚醒等待該對象監控權的一個線程。

調用方法的必要條件:當前線程必須具有對該對象的監控權(加鎖)

7. 例程:生產者與消費者問題

​ 生產者(Productor)將產品交給店員(Clerk),而消費者(Customer)從店員處取走產品,店員一次只能持有固定數量的產品(比如:20),如果生產者試圖生產更多的產品,店員會叫生產者停一下,如果店中有空位放產品了再通知生產者繼續生產;如果店中沒有產品了,店員會告訴消費者等一下,如果店中有產品了再通知消費者來取走產品。

這裏可能出現兩個問題:

  1. 生產者比消費者快時,消費者會漏掉一些數據沒有取到。
  2. 消費者比生產者快時,消費者會取相同的數據。

代碼示例:

public class Test {
	public static void main(String[] args) {
		Clerk c = new Clerk();
		//消費時不生產,生產時不消費
		
		//生產者
		new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (c) {
					while(true){//無限循環代表無限的生產次數
						if(c.productNum == 0){//產品數爲0,開始生產
							System.out.println("產品數爲0,開始生產");
							while(c.productNum < 4){
								c.productNum++;//增加產品
								System.out.println("庫存:" + c.productNum);
							}
							System.out.println("產品數爲" + c.productNum + ",結束生產");
							
							c.notify();//喚醒消費者線程
						}else{
							try {
								c.wait();//生產者線程等待
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
						}
					}
				}
			}
		}, "生產者").start();
		
		//消費者
		new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (c) {
					while(true){//無限循環代表無限的消費次數
						if(c.productNum == 4){//產品數爲4,開始消費
							System.out.println("產品數爲4,開始消費");
							while(c.productNum > 0){
								c.productNum--;//消費產品
								System.out.println("庫存:" + c.productNum);
							}
							System.out.println("產品數爲" + c.productNum + ",結束消費");
							
							c.notify();//喚醒生產者線程
						}else{
							try {
								c.wait();//消費者線程等待
							} catch (InterruptedException e) {
								e.printStackTrace();
							}
						}
					}
				}
			}
		}, "消費者").start();
		
	}
}


class Clerk{
	public static int productNum = 0;
}

​ 2020.4.18
版權說明:該文檔由作者根據Java教程進行整理,其中涉及圖片代碼僅供學習,侵刪。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章