JavaSE基礎 (全網最全知識點)

不吃紫菜發表於2023-02-27

背景介紹

java執行機理(即使編譯型語言,又是解釋型語言)

image

編譯型語言(如:c語言)

原始碼需要透過預編譯形成可執行檔案,再由系統執行該檔案形成可識別的二進位制檔案

image

image

解釋型語言

邊執行邊轉換。原始碼先翻譯成中間程式碼,直譯器(類似於JVM)再對中間程式碼進行解釋執行,每執行一次都要翻譯一次。

image

image

識別符號規則:

  • 識別符號只能由大小寫字母、數字、下劃線(_)和美元符號($)組成,但是不能以數字開頭

  • 大小寫敏感

  • 不能與Java語言的關鍵字重名

    【例:型別名:final,new,class,static(用於定義變數、方法、類的型別名);跳轉語言:break、throw、for】

  • 不可以是 true 和 false (true、false不是關鍵字)

  • 一般採用駝峰命名法

駝峰命名法:

包名:xxxyyyzzz

類名、介面名:XxxYyyZzz

變數名、方法名:xxxYyyZzz

常量名:XXX_YYY_ZZZ

註釋:

//我是單行註釋

/**
* 我是
* 多行註釋
*/

//TODO 待做標記

二進位制

位元組

一個位也叫一個bit,8個bit稱為1位元組,16個bit稱為一個字,32個bit稱為一個雙字,64個bit稱為一個四字

二進位制轉換

https://www.cnblogs.com/buchizicai/p/15866145.html

計算機的加減法

以8bit(一個位元組為例)

原碼

  • 最高位為符號位
  • 其餘位用於表示二進位制的數字

例如:1:00000001 -1:10000001

反碼

由於原碼計算麻煩,所以有了反碼

  • 正數:反碼是其本身
  • 負數:反碼是負數原碼的基礎上,符號位不變,其餘各位取反

例如:-1:11111110

補碼

由於反碼有+0和-0之分,so有了補碼(java用的就是補碼)

  • 正數:補碼就是其本身
  • 負數:補碼是負數原碼的基礎上,符號位不變,其餘各位取反,再加上1(就是在反碼基礎上加1)

以4bit為例:+0:(1)0000 【1溢位了捨去】,-8:1000

進位制圈⚪

當規定了一個變數的型別,如byte(-128—127),那最大表示的數127再加1,得到的就是-128了。同理,-128在減1,得到的就是127了。

資料型別

整數型別

byte—short—int—long—BigInteger

小數型別

float—double—BigDecimal

字元型(代表一個符號)

  • char 字元型(16個bit,也就是2位元組,它不帶符號!)範圍是0 ~ 65535
  • 使用Unicode表示就是:\u0000 ~ \uffff
  • 字元要用單引號擴起來!比如 char c = '淦';

字元其實本質也是數字,但是這些數字透過編碼表進行對映,代表了不同的字元,比如字元'A'的ASCII碼就是數字65,所以char型別其實可以轉換為上面的整數型別。

Java的char採用Unicode編碼表(不是ASCII編碼!)

Unicode與Ascii區別:Unicode編碼表包含ASCII的所有內容,同時還包括了全世界的語言,ASCII只有1位元組,而Unicode編碼是2位元組,能夠代表65536種文字,足以包含全世界的文字了!(我們編譯出來的位元組碼檔案也是使用Unicode編碼的,所以利用這種特性,其實Java支援中文變數名稱、方法名稱甚至是類名)

資料型別轉換

以下都是自動轉換,非自動轉換就需要強制轉換。如:字串轉整數:Integer.parseInt(String s);

隱式型別轉換

隱式型別轉換支援位元組數小的型別自動轉換為位元組數大的型別,整數型別自動轉換為小數型別,轉換規則如下:【小範圍轉大範圍】

  • byte→short(char)→int→long→float→double

問題:為什麼long比float大,還能轉換為float呢?小數的儲存規則讓float的最大值比long還大,只是可能會丟失某些位上的精度!

int a=100;
long b=a;
System.out.println(b);

//輸出100

顯式型別轉換

也叫強轉換型別,犧牲精度強行進行型別轉換 【大範圍轉小範圍】

int i = 128;
byte b = (byte)i;
System.out.println(b);

//輸出 -128【原因:127+1=-128】

float a=1.01;
int b = a;
System.out.println(b);

//輸出 1

Object a="hello";
String b =(String) a;	//此時必須強轉,因為提供的是Object而要求接收到的是String

資料型別自動提升

在參與運算時(也可以位於表示式中時,自增自減除外),所有的byte型、short型和char的值將被提升到int型:

byte b = 105;
b = b + 1;   //報錯:左邊要求接受byte型,而右邊提供int型
System.out.println(b);

這個特性是由 Java虛擬機器規範 定義的,也是為了提高執行的效率。其他的特性還有:

  • 如果一個運算元是long型,計算結果就是long型
  • 如果一個運算元是float型,計算結果就是float型
  • 如果一個運算元是double型,計算結果就是double型

運算子

加號

  • 拼接作用:字串+數字,結果是字串與數字的拼接(因為此時數字被當作字串看待)

邏輯運算子

  • && 、& 與運算,要求兩邊同時為true才能返回true;

&和&&做邏輯與時的區別

  • &會判斷兩邊的true or false
  • &&當判斷左邊為false時將不再判斷右邊
  • |、|| 或運算,要求兩邊至少要有一個為true才能返回true

|和||做邏輯與時的區別

  • |會判斷兩邊的true or false
  • ||當判斷左邊為true時將不再判斷右邊
  • ! 非運算,一般放在表示式最前面,表示式用括號擴起來,表示對錶達式的結果進行反轉

位運算

注意:返回的是運算後的同型別值,不是boolean!

  • & 按位與(與1得1)
  • | 按位或(與0得0)
  • ^ 按位異或 0 ^ 0 = 0(相異得1,相同得0)
  • ~ 按位非

三目運算子

int a = 7, b = 15;
String str = a > b ? "行" : "不行";  // 判斷條件(只能是boolean,或返回boolean的表示式) ? 滿足的返回值 : 不滿足的返回值 
System.out.println("漢堡做的行不行?"+str);  //漢堡做的行不行?不行

方法

方法的過載

定義:方法名相同,但引數不同(可以是引數個數、型別、返回型別不同,但不可以僅返回型別不同!)

構造方法

構造方法(構造器)沒有返回值,也可以理解為,返回的是當前物件的引用!每一個類都預設自帶一個無參構造方法(如果設定了有參構造,那預設的無參構造就被覆蓋了)

靜態變數和靜態方法

可以理解為:靜態是整個專案的全域性變數,可以被直接呼叫,可以被物件呼叫,但改變的是同一個變數的值

靜態變數和靜態方法是類具有的屬性(後面還會提到靜態類、靜態程式碼塊),也可以理解為是所有物件共享的內容。我們透過使用static關鍵字來宣告一個變數或一個方法為靜態的,一旦被宣告為靜態,那麼透過這個類建立的所有物件,操作的都是同一個目標,也就是說,物件再多,也只有這一個靜態的變數或方法。那麼,一個物件改變了靜態變數的值,那麼其他的物件讀取的就是被改變的值。

類載入機制

類並不是在一開始就全部載入好,而是在需要時才會去載入(提升速度)以下情況會載入類:

  • 訪問類的靜態變數,或者為靜態變數賦值
  • new 建立類的例項(隱式載入)
  • 呼叫類的靜態方法
  • 子類初始化時
  • 其他的情況會在講到反射時介紹

所有被標記為靜態的內容,會在類剛載入的時候就分配,而不是在物件建立的時候分配,所以說靜態內容一定會在第一個物件初始化之前完成載入

public class Student {
    static int a = test();  //直接呼叫靜態方法,只能呼叫靜態方法

    Student(){
        System.out.println("構造類物件");
    }

    static int test(){   //靜態方法剛載入時就有了
        System.out.println("初始化變數a");
        return 1;
    }
}

思考:下面這種情況下,程式能正常執行嗎?如果能,會輸出什麼內容?

public class Student {
    static int a = test();

    static int test(){
        return a;
    }

    public static void main(String[] args) {
        System.out.println(Student.a);
    }
}

//輸出:0

解析:定義和賦值是兩個階段,在定義時會使用預設值(上面講的,類的成員變數會有預設值)定義出來之後,如果發現有賦值語句,再進行賦值,而這時,呼叫了靜態方法,所以說會先去載入靜態方法,靜態方法呼叫時拿到a,而a這時僅僅是剛定義,所以說還是初始值,最後得到0【結論:在定義變數時,會賦予預設值(一般是0),然後再判斷是否有賦值語句,有的話再替換預設值】

程式碼塊和靜態程式碼塊

程式碼塊是在 呼叫該程式碼塊所屬的類物件建立時才被載入(普通成員變數也是如此);

靜態程式碼塊是在 呼叫該程式碼塊所屬的類剛載入時,就被呼叫;

程式碼塊在物件建立時執行,也是屬於類的內容,但是它在構造方法執行之前執行(和成員變數初始值一樣),且每建立一個物件時,只執行一次!(相當於構造之前的準備工作)

public class Student {
    {
        System.out.println("我是程式碼塊");
    }

    Student(){
        System.out.println("我是構造方法");
    }
}

靜態程式碼塊和上面的靜態方法和靜態變數一樣,在類剛載入時就會呼叫;

public class Student {
    static int a;

    static {
        a = 10;
    }
    
    public static void main(String[] args) {
        System.out.println(Student.a);
    }
}

包和訪問控制

包宣告

包的命名:一般包按照個人或是公司域名的規則倒過來寫 頂級域名.一級域名.二級域名 com.java.xxxx

包的匯入

  1. 正常匯入:import math.*

  2. 靜態匯入:

    靜態匯入可以直接匯入某個類的靜態方法或者是靜態變數,匯入後,相當於這個方法或是類在定義在當前類中,可以直接呼叫該方法。

    import static com.test.ui.Student.test;
    
    public class Main {
        public static void main(String[] args) {
            test();
        }
    }
    

    注:靜態匯入不會進行類的初始化/載入!

訪問控制

可作用於方法、變數上。(建立方法變數等預設是default,不用特意寫出來)

image

和檔名稱相同的類,只能是public,並且一個java檔案中只能有一個public class!

// Student.java
public class Student {
    
}
class Test{   //不能新增許可權修飾符!只能是default
	
}

注:類只能的public、default,當類是private時是內部類。public類在一個檔案中有且僅有一個

可變長引數

可變長引數實質就是陣列的一種應用,我們可以指定方法的形參為一個可變長引數,要求實參可以根據情況動態填入0個或多個,而不是固定的數量【由於可變長引數實質是陣列,所以傳入的實參只能是同一資料型別】

public static void main(String[] args) {
     test("AAA", "BBB", "CCC");    //可變長,最後都會被自動封裝成一個陣列
}
    
private static void test(String... test){
     System.out.println(test[0]);    //其實引數就是一個陣列
}

當想要傳入的引數部分是對應的定長引數,部分是不定長引數,需要如下↓

public static void  main(Stirng [] args){
	test(10,"AAA","BBB","CCC");		//10是定長,後面部分是不定長會被封裝到一個陣列裡
}
private static void test(int n, String... test){
    System.out.println(n+test[0]);    //其實可變長引數就是一個陣列
}

封裝、繼承、多型

封裝、繼承和多型是物件導向程式設計的三大特性。

封裝

封裝思想其實就是把實現細節給隱藏了,外部只需知道這個方法是什麼作用,而無需關心如何實現。外界只能呼叫介面or使用該方法,這樣將操作成員變數的許可權與外界隔開。

目的:是為了保證變數的安全性,使用者不必在意具體實現細節,而只是透過外部介面即可訪問類的成員,如果不進行封裝,類中的例項變數可以直接檢視和修改,可能給整個程式碼帶來不好的影響,因此在編寫類時一般將成員變數私有化,外部類需要同getter和setter方法來檢視和設定變數。【小結:成員變數應該私有化(private),使外部只能透過getter、setter方法來檢視和設定變數】

例子:學生小明已經建立成功,正常情況下能隨便改他的名字和年齡嗎?

public class Student {
    private String name;
    private int age;
  
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

擴充:外部現在只能透過呼叫我定義的方法來獲取成員屬性,而我們可以在這個方法中進行一些額外的操作,比如小明可以修改名字,但是名字中不能包含"小"這個字。【再設定變數的時候增加設定條件,如:電話號碼必須11位數字】

public void setName(String name) {
    if(name.contains("小")) return;
    this.name = name;
}

繼承

在定義不同類的時候存在一些相同屬性,為了方便使用可以將這些共同屬性抽象成一個父類,在定義其他子類時可以繼承自該父類,減少程式碼的重複定義,子類可以使用父類中非私有的成員。

例子:現在學生分為兩種,藝術生和體育生,他們都是學生的分支,但是他們都有自己的方法:

public class Student {
    private String name;
    private int age;
  
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

public class SportsStudent extends Student{   //透過extends關鍵字來繼承父類

    public SportsStudent(String name, int age) {
        super(name, age);   //必須先透過super關鍵字(指代父類),實現父類的構造方法!
    }

    public void exercise(){
        System.out.println("我超勇的!");
    }
}

public class ArtStudent extends Student{

    public ArtStudent(String name, int age) {
        super(name, age);
    }

    public void art(){
        System.out.println("隨手畫個畢加索!");
    }
}

繼承特點:

  • 子類具有父類的全部屬性(包括private屬性,私有不能直接使用,但可以透過反射使用),同時子類還能有自己的方法。

  • 繼承只能繼承一個父類,不支援多繼承!

  • 呼叫父類的方法和變數super.way()

  • 子類方法中呼叫變數的優先順序:形參列表中 > 當前類的成員變數 > 父類成員變數

    public void setTest(int test){
        test = 1;
      	this.test = 1;
      	super.test = 1;
    }
    
  • 每一個子類必須定義一個實現父類構造方法的構造方法,也就是需要在構造方法第一行使用super(),如果父類使用的是預設構造方法,那麼子類不用手動指明。

    public class Student {
        private String name;
        private int age;
        
        public Student(){};	//可以省略
    }
    
    public class SportStudent extends Student(){
        SportStudent(){	//如果是無參構造可以省略,若是有參構造則不可以省略,並且super()必須在建構函式第一行執行!
            super();	//無參情況下可以省略
        }
    }
    
    
    
    public class Student {
        private String name;
        private int age;
      
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }
    
    public class ArtStudent extends Student(){
        ArtStudent(String name,String age){
            super(name,age);	//子類有參構造,第一行必須先實現父類的任一建構函式
        }
    }
    

多型

多型是同一個行為具有多個不同表現形式或形態的能力。白話:同樣的方法,由於實現類不同,執行的結果也不同!

方法的重寫

過載:原有方法邏輯不變,支援更多引數實現。(方法名相同,引數個數型別不同)

重寫:子類重寫(覆蓋)父類的方法

//父類中的study
public void study(){
    System.out.println("學習");
}

//子類中的study
@Override  //宣告這個方法是重寫的,但是可以不要,我們現階段不接觸
public void study(){
    System.out.println("給你看點好康的");
}

思考:靜態方法能被重寫嗎?不能!【所以父類和子類重寫的方法不能加static,加了就不是重寫了】

類的型別轉換

類也是支援型別轉換的(僅限於存在親緣關係的類之間進行轉換)比如子類可以直接向上轉型:【小範圍轉大範圍】

Student student = new SportsStudent("lbw", 20);  //父類變數引用子類例項
student.study();     //得到依然是具體實現(父類Student)的結果,而不是當前型別的結果

我們也可以把已經明確是由哪個類實現的父類引用,強制轉換為對應的型別,這叫向下轉型:【大範圍轉小範圍就需要強制轉換,條件是必須是對應的子類,不能是別的子類】

Student student = new SportsStudent("lbw", 20);  //是由SportsStudent進行實現的
//... do something...

SportsStudent ps = (SportsStudent)student;  //讓它變成一個具體的子類。只能轉SportStudent,不能是ArtStudent,因為student是SportStudent轉來的

ps.sport();  //呼叫具體實現類的方法

instanceof關鍵字

A instanceof B:A這個類是不是B這個類的型別,返回Boolean型

那麼我們如果只是得到一個父類引用,但是不知道它到底是哪一個子類的實現怎麼辦?我們可以使用instanceof關鍵字來實現,它能夠進行型別判斷!

private static void test(Student student){
    if (student instanceof SportsStudent){
        SportsStudent sportsStudent = (SportsStudent) student;
        sportsStudent.sport();
    }else if (student instanceof ArtStudent){
        ArtStudent artStudent = (ArtStudent) student;
        artStudent.art();
    }
}

思考:student instanceof Student的結果是什麼?true

(因為Student是SportStudent和ArtStudent的父類,嚴格來講SportStudent和ArtStudent也是Student類)

final關鍵字

可以新增到 類、方法、變數前。一旦新增後,一次賦值後就不可更改,類不可被繼承、方法不可被重寫、變數不可被修改。

抽象類

翻譯:在一個類裡定義一些沒有方法體的方法,必須由子類實現(子類可以繼續寫成抽象類,由最底層的子類實現)

注:類和方法都需要abstract修飾。

特點:輔助繼承關係

public abstract class Student {    //抽象類
    int username;
    public abstract void test();  //抽象方法
}

抽象類由於不是具體的類定義,因此無法直接透過new關鍵字來建立物件!

Student s = new Student(){    //只能直接建立帶實現的匿名內部類!
  public void test(){
    
  }
}

特點:抽象類一般只用作繼承使用!抽象類使得繼承關係之間更加明確:

public void study(){   //現在只能由子類編寫,父類沒有定義,更加明確了多型的定義!同一個方法多種實現!
    System.out.println("給你看點好康的");
}

介面

介面只代表功能,只包含方法的定義,實現介面意味著是實現該功能。

public interface Eat {
	void eat(); 
}
  • 介面只能包含public許可權的抽象方法!我們可以透過宣告default關鍵字來給抽象方法一個預設實現
public interface Eat {
    default void eat(){
        //do something...預設實現一些基本功能
    }
}
  • 介面中定義的變數,預設為public static final
public interface Eat {
    int a = 1;
    void eat();
}

實現介面的類也能透過instanceof關鍵字判斷,也支援向上和向下轉型!

介面和抽象類區別

  1. 抽象類要被子類繼承,介面要被類實現
  2. 介面只能做方法宣告,抽象類中可以作方法宣告,也可以做方法實現
  3. 介面是設計的結果,抽象類是重構的結果
  4. 抽象類和介面都是用來抽象具體物件的,但是介面的抽象級別最高
  5. 抽象類可以有具體的方法和屬性(普通變數),介面只能有抽象方法和不可變常量(公共靜態的常量)
  6. 抽象類主要用來抽象類別,介面主要用來抽象功能

內部類

成員內部類【瞭解】

我們的類中可以在巢狀一個類:

public class Test {
    class Inner{   //類中定義的一個內部類
        
    }
}

成員內部類和成員變數和成員方法一樣,都是屬於物件的,也就是說,必須存在外部物件,才能建立內部類的物件!

public static void main(String[] args) {
    Test test = new Test();
    Test.Inner inner = test.new Inner();   //寫法有那麼一絲怪異,但是沒毛病!
}

靜態內部類【瞭解】

靜態內部類其實就和類中的靜態變數和靜態方法一樣,是屬於類擁有的,我們可以直接透過類名.去訪問:

public class Test {
    static class Inner{

    }
}

public static void main(String[] args) {
    Test.Inner inner = new Test.Inner();   //不用再建立外部類物件了!
}

區域性內部類【瞭解】

對,你沒猜錯,就是和區域性變數一樣噠~

public class Test {
    public void test(){
        class Inner{

        }
        
        Inner inner = new Inner();
    }
}

反正我是沒用過!內部類 -> 累不累 -> 反正我累了!

匿名內部類

白話:在建立介面/類物件的時候重寫方法,或者呼叫父類的方法,實現在建立物件的時候對物件初始化。

(也可以理解在寫匿名內部類的時候,是對介面的實現/對父類的方法重寫)

使用場景:一個介面/類的方法的某個實現方式在程式中只會執行一次

優點:對於使用次數少的方法,無需創造新的類,減少程式碼冗餘

缺點:無法在建立的物件外複用,需要再次使用重寫的方法需要重新在寫一次

一般情況:一般使用一個功能需要,寫介面然後再寫實現類。在建立介面物件才能呼叫裡面的方法。介面 物件名 = new 實現類 ();

使用匿名內部類:可以直接建立介面/類物件然後再建立的時候寫一個只用一次的方法

舉例:

建立物件時呼叫父類方法實現物件初始化:

List<Integer> list = new LinkedList<Integer>(){   //Java9才支援匿名內部類使用鑽石運算子
    {
        this.add(10);
        this.add(2);
        this.add(5);
        this.add(8);
    }
};

介面情況:

//介面
public interface Interface01 {
    void show(String s);
}

//測試類
public test {
    public static void main(String[] args) {
        //寫法一:實現介面的抽象方法
        new Interface01(){
            @Override
            public void show(String s) {
                System.out.println("我是一個" + s);
            }
        }.show("介面");
        
        
        //寫法二:實現介面的抽象方法
        Interface01 test =  new Interface01(){
            @Override
            public void show(String s) {
                System.out.println("我是一個" + s);
            }
        }
        test.show("介面");
    }
}

類的情況:(具體類與抽象類一樣)

//具體類
public class Class01 {
    public void show(String s){
        System.out.println("啦啦啦");
    }
}

//測試類
public static void main(String[] args) {
    //寫法一:重寫具體類的方法
    new Class01(){
        @Override
        public void show(String s) {
            System.out.println("我是一個" + s);
        }
    }.show("具體類");
    
    //寫法二:重寫具體類的方法
    Class01 test = new Class01(){
        @Override
            public void show(String s) {
                System.out.println("我是一個" + s);
            }
    }
    test.show("具體類");
}

lambda表示式

只適用於介面or抽象類中只有一個方法的情況。且只寫引數與方法體

讀作λ表示式,它其實就是我們介面匿名實現的簡化,比如說:

public static void main(String[] args) {
        Eat eat = new Eat() {
            @Override
            public void eat() {
                //DO something...
            }
        };
    }

public static void main(String[] args) {
        Eat eat = (引數) -> {方法體};   //等價於上述內容
    }

lambda表示式(匿名內部類)只能訪問外部的final型別或是隱式final型別的區域性變數(就是值第一次被賦值了後面沒被更改)

為了方便,JDK預設就為我們提供了專門寫函式式的介面,這裡只介紹Consumer

forEach迴圈

//第一種:類似於python
for (Integer i: list) {
            System.out.print(i+" ");
        }

//第二種:物件.foreach
list.forEach(i -> System.out.print(i+" "));

//第三種:用::更改原始碼的accept()方法
list.forEach(System.out::print);
//該程式碼作用:輸出list的all元素

列舉類

在類、介面之外的一種型別

假設現在我們想給小明新增一個狀態(跑步、學習、睡覺),外部可以實時獲取小明的狀態:

public class Student {
    private final String name;
    private final int age;
    private String status;
  
  	//...
  
  	public void setStatus(String status) {
        this.status = status;
    }

    public String getStatus() {
        return status;
    }
}

但是這樣會出現一個問題,如果我們僅僅是儲存字串,似乎外部可以不按照我們規則,傳入一些其他的字串。這顯然是不夠嚴謹的!

有沒有一種辦法,能夠更好地去實現這樣的狀態標記呢?我們希望開發者拿到使用的就是我們定義好的狀態,我們可以使用列舉類!

public enum Status {
    RUNNING, STUDY, SLEEP    //直接寫每個狀態的名字即可,分號可以不打,但是推薦打上
}

使用列舉類也非常方便,我們只需要直接訪問即可

public class Student {
    private final String name;
    private final int age;
    private Status status;
  
 		//...
  
  	public void setStatus(Status status) {   //不再是String,而是我們指定的列舉型別
        this.status = status;
    }

    public Status getStatus() {
        return status;
    }
}

public static void main(String[] args) {
    Student student = new Student("小明", 18);
    student.setStatus(Status.RUNNING);
    System.out.println(student.getStatus());
}

列舉型別使用起來就非常方便了,其實列舉型別的本質就是一個普通的類,但是它繼承自Enum類,我們定義的每一個狀態其實就是一個public static final的Status型別成員變數!

// Compiled from "Status.java"
public final class com.test.Status extends java.lang.Enum<com.test.Status> {
  public static final com.test.Status RUNNING;
  public static final com.test.Status STUDY;
  public static final com.test.Status SLEEP;
  public static com.test.Status[] values();
  public static com.test.Status valueOf(java.lang.String);
  static {};
}

既然列舉型別是普通的類,那麼我們也可以給列舉型別新增獨有的成員方法

public enum Status {
    RUNNING("睡覺"), STUDY("學習"), SLEEP("睡覺");   //無參構造方法被覆蓋,建立列舉需要新增引數(本質就是呼叫的構造方法!)

    private final String name;    //列舉的成員變數
    Status(String name){    //覆蓋原有構造方法(預設private,只能內部使用!)
        this.name = name;
    }
  
  	public String getName() {   //獲取封裝的成員變數
        return name;
    }
}

public static void main(String[] args) {
    Student student = new Student("小明", 18);
    student.setStatus(Status.RUNNING);
    System.out.println(student.getStatus().getName());
}

列舉類還自帶一些繼承下來的實用方法

Status.valueOf("")   //將名稱相同的字串轉換為列舉
Status.values()   //快速獲取所有的列舉

基本型別包裝類

Java並不是純物件導向的語言,雖然Java語言是一個物件導向的語言,但是Java中的基本資料型別卻不是物件導向的。在學習泛型和集合之前,基本型別的包裝類是一定要講解的內容!

我們的基本型別,如果想透過物件的形式去使用他們,Java提供的基本型別包裝類,使得Java能夠更好的體現物件導向的思想,同時也使得基本型別能夠支援物件操作!

image

  • byte -> Byte
  • boolean -> Boolean
  • short -> Short
  • char -> Character
  • int -> Integer
  • long -> Long
  • float -> Float
  • double -> Double

包裝類實際上就行將我們的基本資料型別,封裝成一個類(運用了封裝的思想)

private final int value;   //Integer內部其實本質還是存了一個基本型別的資料,但是我們不能直接操作

public Integer(int value) {
    this.value = value;
}

現在我們操作的就是Integer物件而不是一個int基本型別了!

public static void main(String[] args) {
     Integer i = 1;   //包裝型別可以直接接收對應型別的資料,並變為一個物件!
     System.out.println(i + i);    //包裝型別可以直接被當做一個基本型別進行操作!
}

自動裝箱和拆箱

自動裝箱:在對一個Integer型別的物件賦值時,叫自動裝箱

自動拆箱:對一個Integer型別的物件做運算、賦值給別的變數時,叫拆箱

自動裝箱原理:

Integer i = 1;    //其實這裡只是簡寫了而已
Integer i = Integer.valueOf(1);  //編譯後真正的樣子

Integer.valueOf (x)原理:呼叫valueOf來生成一個Integer物件!

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)   //注意,Java為了最佳化,有一個快取機制,如果是在-128~127之間的數,會直接使用已經快取好的物件,而不是再去建立新的!(面試常考)!!!
       return IntegerCache.cache[i + (-IntegerCache.low)];
  	return new Integer(i);   //返回一個新建立好的物件
}

自動拆箱原理:

public static void main(String[] args) {
    Integer i = Integer.valueOf(1);
    int a = i;    //簡寫
    int a = i.intValue();   //編譯後實際的程式碼
  
  	long c = i.longValue();   //其他型別也有!
}

==是指地址是否相同,equals()是指值是否相同。

當Integer的物件值在-128—127時,快取機制(IntegerCache)使用快取好的物件作為該Integer物件(此時無論多少個Integer物件,使用的都是同一個物件)

public static void main(String[] args) {
    Integer i1 = 28914;
    Integer i2 = 28914;

    System.out.println(i1 == i2);   //實際上判斷是兩個物件是否為同一個物件(記憶體地址是否相同)【當i1,i2在-128—127間,則它們使用的是同一個物件】
    System.out.println(i1.equals(i2));   //這個才是真正的值判斷!
}

思考:下面這種情況結果會是什麼?True

public static void main(String[] args) {
    Integer i1 = 28914;
    Integer i2 = 28914;

    System.out.println(i1+1 == i2+1);	//由於Integer物件經過了自動拆箱,所以等號兩邊都是基本資料int
}

Java異常機制

異常

比如陣列越界異常,空指標異常,算術異常等,他們其實都是異常型別,我們的每一個異常也是一個類,他們都繼承自Exception類!異常型別本質依然類的物件,但是異常型別支援在程式執行出現問題時丟擲(也就是上面出現的紅色報錯)也可以提前宣告,告知使用者需要處理可能會出現的異常!

執行時異常

定義:在編譯階段無法感知程式碼是否會出現問題,只有在執行的時候才知道會不會出錯(正常情況下是不會出錯的),這樣的異常稱為執行時異常。

特點:所有的執行時異常都直接繼承自RuntimeException(RuntimeException也是繼承Exception)

編譯時異常

定義:在編譯階段就需要進行處理的異常(捕獲異常)如果不進行處理,將無法透過編譯!

特點:預設直接繼承自Exception類的異常都是編譯時異常

File file = new File("my.txt");
file.createNewFile();   //要呼叫此方法,首先需要處理異常

錯誤

錯誤比異常更嚴重,異常就是不同尋常,但不一定會導致致命的問題,而錯誤是致命問題,一般出現錯誤可能JVM就無法繼續正常執行了

比如OutOfMemoryError就是記憶體溢位錯誤(記憶體佔用已經超出限制,無法繼續申請記憶體了)

int[] arr = new int[Integer.MAX_VALUE];   //能建立如此之大的陣列嗎?不能!

報錯:

Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
	at com.test.Main.main(Main.java:14)

錯誤都繼承自Error類,一般情況下,程式中只能處理異常,錯誤是很難進行處理的,ErrorExecption都繼承自Throwable類。當程式中出現錯誤或異常時又沒有進行處理時,程式(當前執行緒)將終止執行:

int[] arr = new int[Integer.MAX_VALUE];
System.out.println("lbwnb");  //還能正常列印嗎?

異常處理

程式出現異常時,預設會交給JVM來處理,JVM發現任何異常都會立即終止程式執行,並在控制檯列印棧追蹤資訊。這是就需要編寫程式手動捕獲異常,使程式繼續正常執行。(一旦手動捕獲後,異常就不再交給JVM處理)

使用trycatch語句塊來處理:

int[] arr = new int[5];
try{    //在try塊中執行可能出現異常的程式碼
     arr[5] = 1;    //當程式碼出現異常時,異常會被捕獲,並匹配catch塊中捕獲異常的型別,從而得到異常型別的物件
}catch (ArrayIndexOutOfBoundsException e){   //捕獲的異常型別,並匹配catch塊中捕獲異常的型別,從而得到異常型別的物件
     e.printStackTrace()	//列印棧追蹤資訊,定位異常出現位置及原因
     System.out.println("程式執行出現異常!");  //出現異常時執行
}finally {
  System.out.println("finally:lbwnb");   //無論是否出現異常,都會在最後執行
}

//後面的程式碼會正常執行
System.out.println("lbwnb");

執行結果 :

java.lang.ArrayIndexOutOfBoundsException: 5
	at com.test.Main.main(Main.java:7)    //Main類的第7行出現問題
程式執行出現異常!
finally:lbwnb
lbwnb

finally:

  • try語句塊至少要配合catchfinally中的一個:
try {
    int a = 10;
    a /= 0;
}finally {  //不捕獲異常,程式會終止,但在最後依然會執行下面的內容
    System.out.println("lbwnb"); 
}
  • 思考:trycatchfinally執行順序:try——catch(有異常才執行)——finally
private static int test(int a){
  try{
    return a;
  }catch (Exception e){
    return 0;
  }finally {
    a =  a + 1;
  }
}

執行時異常在編譯時可以不用捕獲,但是編譯時異常必須進行處理

注 :

  1. 可以捕獲到型別不止是Exception的子類,只要是繼承自Throwalbe的類,都能被捕獲,也就是說,Error也能被捕獲,但是不建議這樣做,因為錯誤一般是虛擬機器相關的問題,出現Error應該從問題的根源去解決。
  2. 異常層級圖

image

異常的丟擲

傳入錯誤引數,則需要透過throw關鍵字手動丟擲異常(丟擲異常後,後面的程式碼不再執行),可以方法內try catch處理異常,若不想在方法內處理,則需要同時告知上一級方法執行出現了問題(在方法後加throws Exception),此時上一級就需要try catch來處理異常。

【若最上級main也在方法後加了throws Exception則就是交給了JVM處理該異常】

public static void main(String[] args) {
        try {
            test(1, 0);
        } catch (Exception e) {   //捕獲方法中會出現的異常並處理 or 接收子方法中傳來的異常並處理;
            e.printStackTrace();
            System.out.println("出現異常");
        }
    }

    private static int test(int a, int b) throws Exception {  //宣告並接收下面程式中丟擲的異常型別
        if(b == 0) throw new Exception("0不能做除數!");  //建立異常物件並丟擲異常,給上面接收
        return a/b;  //丟擲異常會終止程式碼執行
    }
  • 非執行時異常必須捕獲處理(不處理的話編譯透過不了)
  • 執行時異常不強求手動捕獲處理(JVM會處理)

注:當異常捕獲出現巢狀時,只會在最內層被捕獲:

public static void main(String[] args){
        try{
            test(1, 0);
        }catch (Exception e){
            System.out.println("外層");
        }
    }

    private static int test(int a, int b){
        try{
            if(b == 0) throw new Exception("0不能做除數!");
        }catch (Exception e){
            System.out.println("內層");
            return 0;
        }
        return a/b;
    }


//結果:內層      

自定義異常

第一步:寫一個類繼承Exception

public class MyException extends Exception {  //直接繼承即可
    
}

public static void main(String[] args) throws MyException {
        throw new MyException();   //直接使用
    }

第二步:使用父類的帶描述的構造方法

public class MyException extends Exception {

    public MyException(String message){
        super(message);
    }
}

public static void main(String[] args) throws MyException {
    throw new MyException("出現了自定義的錯誤");
}

第三步:丟擲自定義異常(可以用自定義異常的父類接收該異常)

try {
  throw new MyException("出現了自定義的錯誤");
} catch (Exception e) {    //捕獲父異常型別
  System.out.println("捕獲到異常");
}

多重異常捕獲

多重異常捕獲,類似於if-else if的結構,父異常型別只能放在最後!:

try {
  //....
} catch (NullPointerException e) {
            
} catch (IndexOutOfBoundsException e){

} catch (RuntimeException e){
            
}

try {
  //....
} catch (RuntimeException e){  //父型別在前,會將子類的也捕獲

} catch (NullPointerException e) {   //永遠都不會被捕獲

} catch (IndexOutOfBoundsException e){   //永遠都不會被捕獲

}

多種異常一併處理用|

try {
     //....
} catch (NullPointerException | IndexOutOfBoundsException e) {  //用|隔開每種型別即可

}

泛型

泛型本質上也是一個語法糖(並不是JVM所支援的語法,編譯後會轉成編譯器支援的語法,比如之前的foreach就是)。

型別擦除:在編譯後會被擦除,變回上面的Object型別呼叫,但是型別轉換由編譯器幫我們完成,而不是我們自己進行轉換(安全)

泛型使用細節:類上使用泛型,那寫在類名後面。方法上使用泛型,那寫在修飾符(static等)後面

舉例:

為了統計學生成績,要求設計一個Score物件,包括課程名稱、課程號、課程成績,但是成績分為兩種,一種是以優秀、良好、合格 來作為結果,還有一種就是 60.0、75.5、92.5 這樣的數字分數,那麼現在該如何去設計這樣的一個Score類呢?現在的問題就是,成績可能是String型別,也可能是Integer型別,如何才能很好的去存可能出現的兩種型別呢?

一般方法:這種方法編譯不會報錯,但執行時會報錯。所以不推薦!!!

//實體類
public class Score {
    String name;
    String id;
    Object score;  //因為Object是所有型別的父類,因此既可以存放Integer也能存放String

  	public Score(String name, String id, Object score) {
        this.name = name;
        this.id = id;
        this.score = score;
    }
}

//主方法
public static void main(String[] args) {
    Score score = new Score("資料結構與演算法基礎", "EP074512", "優秀");  //是String型別的
    Integer number = (Integer) score.score;  //獲取成績需要進行強制型別轉換,雖然並不是一開始的型別,但是編譯不會報錯
}

//結果:執行時出現異常!
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
	at com.test.Main.main(Main.java:14)

範型方法:如果有錯誤編譯就會報錯,不用等執行專案時才發現報錯。

//實體類
public class Score<T> {   //將Score轉變為泛型類<T>
    String name;
    String id;
    T score;  //T為泛型,根據使用者提供的型別自動變成對應型別

    public Score(String name, String id, T score) {   //提供的score型別即為T代表的型別
        this.name = name;
        this.id = id;
        this.score = score;
    }
}

//主方法
public static void main(String[] args) {
    //在呼叫實體類並輸入資料的時候就可以確定T的型別,後面一個磚石運算子不用寫String
    Score<String> score = new Score<String>("資料結構與演算法基礎", "EP074512", "優秀");
    Integer i = score.score;  //編譯不透過,因為成員變數score型別被定為String!
}

原理:主方法在編譯的時候被轉換成如下程式碼(這就是泛型的型別擦除,T被擦除了,變成了Object)

//反編譯後的程式碼
public static void main(String[] args) {
        Score score = new Score("資料結構與演算法基礎", "EP074512", "優秀");
        String i = (String)score.score;   //其實依然會變為強制型別轉換,但是這是由編譯器幫我們完成的
    }

泛型介紹

泛型只比普通的類多了一個型別引數,在使用的時候就需要指定具體的泛型型別。泛型名稱一般取單個大寫字母,如T代表Type的意思。也可以新增數字和其他字元,要設定多個泛型<T,V,M>

T如何被確定

①建立物件並新增資料時new Score<>(xxx,xxx,xxx),系統會根據輸入的資料給泛型賦上相應的型別類。【也就是說只要在使用的時候可以透過傳入的引數確定型別,都可以使用泛型】

②接收new Score的Score score 鑽石運算子內的型別就需要自己根據可能輸入的引數值來判斷了。【若無法判斷可以寫成<?>,但最後需要使用該物件的引數的時候強轉(因為此時引數型別為Object)。不推薦該方法】

泛型的使用範圍:不可以使用在靜態成員變數。(泛型是建立物件後編譯器才能明確泛型型別,而靜態型別是類具有的屬性,編譯器無法透過物件察覺泛型型別)

泛型型別:包括所有基本型別的包裝類(Integer,Double,String),但不能使用基本資料型別(int,double等)

類的泛型方法

這種方法依附於類定義的泛型。原理是在建立該類物件的時候就可以確定T

舉例:

public class Score<T>{
    String name;
    String id;
    T score;
    
public T getScore() {    //若方法的返回值型別為泛型,那麼編譯器會自動進行推斷
  return score;
}

public void setScore(T score) {   //若方法的形式引數為泛型,那麼實參只能是定義時的型別
  this.score = score;
}
}

自定義泛型方法

類的泛型方法需要依附於類,但自定義的泛型方法可以設定為在呼叫該方法傳入引數的時候才確定型別E

靜態方法的自定義泛型方法:

public static <E,ID> void test(E e,ID id){   //在方法定義前宣告泛型
  System.out.println(e+id);
}

成員方法的自定義泛型方法:

public static <E> void test(E e){   //在方法定義前宣告泛型
  System.out.println(e);
}

泛型的界限

上界

定義:

public class Score<T extends Number> { //設定泛型上界,必須是Number的子類

Score<? extends Number> score = new Score<> (xxx,xxxx,xxx); //只能接收Number及其子類的型別

範圍:只有指定型別及指定型別的子類才可以作為其型別引數

(白話:輸入(接收)的引數型別只能是指定型別及其指定型別的子類)

編譯:在編譯時型別擦除會把T型別的Score編譯成指定的最高上界的型別

下界

定義:

下屆只能在接收的時候設定,不能在實體類設定

Score<? super Integer> score = new Score<> (xxx,xxxx,xxx); //只能接收Integer及其父類的new Score型別

範圍:只有指定型別或指定型別的父類才能作為型別引數

(白話:輸入(接收)的引數型別只能是指定型別及其指定型別的父類)

編譯:在編譯時型別擦除會把T型別的Score編譯成Object 型別(雖然設定了下界,但最高上界任然還是Object)

泛型與多型

介面

抽象類與介面類似就不介紹抽象類了

定義:

public interface ScoreInterface<T> {
    T getScore();
    void setScore(T t);
}

介面與實體類

//設定介面泛型與實體類泛型字母相同,建立實體類物件的時候就可以設定介面的型別了
public class Score<T> implements ScoreInterface<T>{   //將Score轉變為泛型類<T>
//也可以在實現介面的時候就指定T    
//public class StringScore implements ScoreInterface<String>{   //在實現時明確型別
    private final String name;
    private final String id;
    private T score;

    public Score(String name, String id, T score) { 
        this.name = name;
        this.id = id;
        this.score = score;
    }

    public T getScore() {
        return score;
    }

    @Override
    public void setScore(T score) {
        this.score = score;
    }
}

介面注入bean後的呼叫

@Resource 
ScoreInterface<String> scoreinterface	//在注入介面bean物件的時候指定型別

思考:

既然繼承後明確了泛型型別,那麼為什麼@Override不會出現錯誤呢,重寫的條件是需要和父類的返回值型別、形式引數一致,而泛型預設的原始型別是Object型別,子類明確後變為Number型別,這顯然不滿足重寫的條件,但是為什麼依然能編譯透過呢?

答案:編譯器在編譯的時候生成了兩個橋接方法用於支援重寫(橋接:呼叫該類的成員方法,並將返回值返回至父類)

舉例:

//父類
class A<T>{
    private T t;
    public T get(){
        return t;
    }
    public void set(T t){
        this.t=t;
    }
}

//子類
class B extends A<Number>{
    private Number n;

    @Override
    public Number get(){   //這並不滿足重寫的要求,因為只能重寫父類同樣返回值和引數的方法,但是這樣卻能夠透過編譯!
        return t;
    }

    @Override
    public void set(Number t){
        this.t=t;
    }
}

原理:編譯時的程式碼。

class B extends A<Number>{
    private Number n;

    @Override
    public Number get(){  
        return t;
    }
    @Override
    public Object get(){
        return this.get();//呼叫返回Number的那個方法
    }

    @Override
    public void set(Number t){
        this.t=t;
    }
    @Override
    public void set(Object t ){
      this.set((Number)t ); //呼叫引數是Number的那個方法
    }
}

I/O流

  1. 所有流用完都需要close(); 也可以使用JDK1.7新增了try-with-resource語法會自動幫我們關閉流【注意,這種語法只支援實現了AutoCloseable介面的類!】
  2. 除檔案流外的五個流只能巢狀檔案流,他們之間無法相互巢狀,所以用流的時候需要判斷應該什麼流巢狀檔案流
  3. 儲存檔案時如果沒有該檔案則會根據定義的路徑自動建立

檔案流

檔案的相對路徑是從與src相同的位置開始算起。如test.txt建立後就是與src檔案同級

注:檔案流不支援reset()方法

檔案位元組流

適用條件:所有檔案位元組流。

(因為每次獲取檔案內容都是透過一個位元組獲取,純文字檔案可能有中文等一字兩位元組的文字,需要同時獲取兩個位元組才能得到一個字元)

輸入流

讀檔案

舉例:

public static void main(String[] args) {
    //test.txt:abcd
    try(FileInputStream inputStream = new FileInputStream("test.txt")) {
        byte[] bytes = new byte[inputStream.available()];   //我們可以提前準備好合適容量的byte陣列來存放
        System.out.println(inputStream.read(bytes));   //一次性讀取全部內容(返回值是讀取的位元組數)
        System.out.println(new String(bytes));   //透過String(byte[])構造方法得到字串
    }catch (IOException e){
        e.printStackTrace();
    }
}

基本操作:

//檢視該檔案剩餘可讀的位元組數量 【只有檔案位元組輸入流有available,字元檔案輸入流沒有】
byte[] bytes = new byte[inputStream.available()];	//用剩下可讀的數量來定義陣列大小

//一次行閱讀bytes陣列大小的位元組,返回讀取的內容。返回讀到的位元組數,沒有可讀的時候會返回-1  【可以透過】
inputStream.read(bytes);
//控制讀取的範圍:第二個引數是從給定陣列的哪個位置開始放入內容,第三個引數是讀取流中的位元組數
inputStream.read(bytes, 1, 2);

//返回讀取到的單個字元
(char)inputStream.read();

//跳過位元組數1,並返回跳過的數量,即1
inputStream.skip(1);


輸出流

寫檔案 (記得要在寫完後outputStream.flush();重新整理,以免寫入失敗

舉例:

public static void main(String[] args) {
  //如果是追加寫檔案則需要呼叫使用這個構造方法
  //try(FileOutputStream outputStream = new FileOutputStream("output.txt", true))
    try(FileOutputStream outputStream = new FileOutputStream("output.txt")) {
        outputStream.write('c');   //同read一樣,可以直接寫入內容
      	outputStream.write("lbwnb".getBytes());   //也可以直接寫入byte[]
      	outputStream.write("lbwnb".getBytes(), 0, 1);  //0是從第幾個位元組開始寫,1是寫的位元組數量
      	outputStream.flush();  //建議在最後執行一次重新整理操作(強制寫入)來保證資料正確寫入到硬碟檔案中
    }catch (IOException e){
        e.printStackTrace();
    }
}

檔案字元流(瞭解)

適用:純文字 (因為只能夠讀取文字)

弊端:無available方法,陣列長度需要自己定。只能按單個字元、陣列指定長度讀取

輸入流

讀檔案

舉例:

public static void main(String[] args) {
    try(FileReader reader = new FileReader("test.txt")){
        char[] str = new char[10];
        reader.read(str);
        System.out.println(str);   //直接讀取到char[]中
    }catch (IOException e){
        e.printStackTrace();
    }
}

基本操作:

//跳過字元,返回跳過字元的數量
reader.skip(1);

//按單個字元讀取,原本是返回字元數,但加了char就是返回讀取到的單個字元
(char) reader.read();
    
//讀取str陣列長度的資料,並存入str陣列中
reader.read(str);

輸出流

寫檔案

舉例:

注意FileWriter裡有write和append方法,都是覆蓋寫入。只是append會返回一個看不懂的地址。 append支援鏈式呼叫:

writer.append("000")
     .append("111")
     .append("222");
public static void main(String[] args) {
    try(FileWriter writer = new FileWriter("output.txt")){
      	writer.getEncoding();   //支援獲取編碼(不同的文字檔案可能會有不同的編碼型別)
       writer.write('牛');
       writer.append('牛');   //其實功能和write一樣
      	writer.flush();   //重新整理
    }catch (IOException e){
        e.printStackTrace();
    }
}

File類

File類專門用於表示一個檔案或資料夾,只不過它只是代表這個檔案,但並不是這個檔案本身。透過File物件,可以更好地管理和操作硬碟上的檔案。

注意:

File file = new File(".idea/aa/bb/ccc");
file.mkdir();	//根據建立物件時的路徑,創造一個資料夾。若路徑中.idea、aa、bb中有一個不存在則不會建立
file.mkdirs();	//(強制建立資料夾)有以上路徑則建立資料夾cc,沒有這就建立全部路徑知道建立出cc資料夾 

基本操作:

public static void main(String[] args) {
    File file = new File("test.txt");   //直接建立檔案物件,可以是相對路徑,也可以是絕對路徑
    System.out.println(file.exists());   //此檔案是否存在
    System.out.println(file.length());   //獲取檔案的大小
    System.out.println(file.isDirectory());   //是否為一個資料夾
    System.out.println(file.canRead());   //是否可讀
    System.out.println(file.canWrite());   //是否可寫
    System.out.println(file.canExecute());   //是否可執行
    
    System.out.println(Arrays.toString(file.list()));   //快速獲取資料夾下的檔名稱列表
	for (File f : file.listFiles()){   //獲取所有file下的子檔案的File物件f
    	System.out.println(f.getAbsolutePath());   //獲取檔案的絕對路徑
}

檔案流練習

複製資料夾下的所有檔案到另一個資料夾

推薦參考答案,因為自己寫用了available,這個在網路傳輸的時候不準

自己寫:

public static void main(String[] args) {
        int i=0;
        //輸入流資料夾
        File file01 = new File(".idea");
        //輸出流資料夾 (注意:這裡是複製一個file01資料夾到該資料夾下,所以路徑最後要是/
        File file02 = new File("test/cc");
        file02.mkdirs();

        //迴圈獲取file01檔案下的每個檔案的物件,然後依次複製到另一個檔案下
        // !!!注意這裡一定要是先迴圈檔案在開流,不能先開流在迴圈檔案。因為反過來檔案資源不會立即關閉浪費資源
       for(File file : file01.listFiles()){
           try(FileInputStream inputStream = new FileInputStream(file);
               FileOutputStream outputStream = new FileOutputStream(file02.getPath()+"/"+file.getName())) {
                byte [] arr = new byte[inputStream.available()];
                inputStream.read(arr);
                outputStream.write(arr);
                outputStream.flush();
               System.out.println("複製了"+i+"個檔案");
               i++;
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
    }

參考答案:

//參考答案:
 public static void main(String[] args) {
        //輸入流資料夾
        File file01 = new File(".idea");
        //輸出流資料夾 (注意:這裡是複製一個file01資料夾到該資料夾下,所以路徑最後要是/
        File file02 = new File("test/cc");
        file02.mkdirs();

        //迴圈獲取file01檔案下的每個檔案的物件,然後依次複製到另一個檔案下
        // !!!注意這裡一定要是先迴圈檔案在開流,不能先開流在迴圈檔案。因為反過來檔案資源不會立即關閉浪費資源
       for(File file : file01.listFiles()){
           try(FileInputStream inputStream = new FileInputStream(file);
               FileOutputStream outputStream = new FileOutputStream(file02.getPath()+"/"+file.getName())) { //複製到的新檔案寫法要注意
                byte [] arr = new byte[20];
                int temp;
                //判斷有沒有複製到最後,到了最後temp=-1,否則temp是等於arr讀到的位元組
                while ((temp=inputStream.read(arr))!=-1){
                    outputStream.write(arr,0,temp);
                }
                //別忘了重新整理
                outputStream.flush();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
    }

轉換流

最佳化的物件是:檔案位元組流

原理【裝飾者模式】:轉換流也是繼承了檔案流的基礎上進行了額外的操作,所以操作I/O檔案的還是檔案流(在檔案位元組流的基礎上套轉換流,從而獲得檔案字元流的便利。)

擁有更多種檔案流的操作,所以操作檔案流時都會巢狀轉換流便於操作

便利:

  • 不用寫入一個字串再去轉byte型別
  • 可以讀取檔案位元組流,按字元的方式讀取
  • 可以適用reset()、mark()【具體看緩衝流】

舉例:

OutputStreamWriter巢狀FileOutputStream

 public static void main(String[] args) {
     //巢狀:
        try(OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("test.txt"))){  //雖然給定的是FileOutputStream,但是現在支援以Writer的方式進行寫入
            writer.write("lbwnb");   //以操作Writer的樣子寫入OutputStream
            writer.append("123");   //這裡的append是追加寫
        }catch (IOException e){
            e.printStackTrace();
        }
    }

InputStreamReader巢狀FileInputStream

public static void main(String[] args) {
    try(InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt"))){  //雖然給定的是FileInputStream,但是現在支援以Reader的方式進行讀取
        System.out.println((char) reader.read());
    }catch (IOException e){
        e.printStackTrace();
    }
}

緩衝流

快取流能夠提高檔案流讀取和寫入的速度 以及重新讀取之前的資料,所以一般操作檔案都會在套完轉換流後再套一層緩衝流。

原理【裝飾者模式】:繼承了檔案流的繼承進行了快取操作,所以操作I/O檔案的還是檔案流

使檔案流在緩衝區操作,提前把 檔案的內容、需要輸出到檔案的內容 放到緩衝流中,便於後續快速獲取。只使用了緩衝流,方法是和檔案流一樣的,只多了reset()、maek()方法

image

緩衝位元組流

輸入流BufferedInputStream的巢狀:

BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("test.txt"))

輸出流BufferedOutputStream的巢狀:

BufferedOutputStream writer = new BufferedOutputStream(new FileOutputStream("output.txt"))

緩衝字元流(瞭解)

輸出流BufferedReader的巢狀:

BufferedReader reader = new BufferedReader(new FileReader("test.txt"))

輸入流BufferedWriter的巢狀:

BufferedWriter writer= new BufferedWriter(new FileWriter("output.txt"))

比檔案字元流多的操作:

//按行讀取,返回讀取的當行內容
reader.readLine();

//讀取多行內容,還可以將多行依次轉換為集合類提到的Stream流
reader
                .lines()
                .limit(2)
                .distinct()
                .sorted()
                .forEach(System.out::println);

//換行
writer.newLine();   

reset()、mark()方法

快取機制的關鍵!可以重新讀取之前讀過的資料

白話:mark就像書籤一樣,在這個BufferedReader對應的buffer裡作個標記,以後再呼叫reset時就可以再回到這個mark過的地方。mark方法有個引數,透過這個整型引數(reallimit),你告訴系統,希望在讀出這麼多個字元之前,這個mark保持有效。讀過這麼多字元之後,系統可以使mark不再有效,而你不能覺得奇怪或怪罪它。這跟buffer有關,如果你需要很長的距離,那麼系統就必須分配很大的buffer來保持你的mark。

但是實操會發現不是這樣的,超過了reallimit 還是可以回到mark的地方,因為mark()後儲存的讀取內容是取readlimit和BufferedInputStream類的緩衝區大小兩者中的最大值,而並非完全由readlimit確定。因此我們需要限制一下緩衝區大小和reallimit一致

緩衝區預設大小:8192

public static void main(String[] args) {
    try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("test.txt"), 1)){  //將緩衝區大小設定為1
        bufferedInputStream.mark(1);   //只保留之後的1個字元
        System.out.println((char) bufferedInputStream.read());
        System.out.println((char) bufferedInputStream.read());   //已經超過了readlimit,繼續讀取會導致mark失效
        bufferedInputStream.reset();   //mark已經失效,無法reset()
        System.out.println((char) bufferedInputStream.read());
        System.out.println((char) bufferedInputStream.read());
    }catch (IOException e) {
        e.printStackTrace();
    }
}

列印流(瞭解)

列印流其實我們從一開始就在使用了,比如System.out就是一個PrintStream,PrintStream也繼承自FilterOutputStream類因此依然是裝飾我們傳入的輸出流,但是它存在自動重新整理機制,例如當向PrintStream流中寫入一個位元組陣列後自動呼叫flush()方法。PrintStream也永遠不會丟擲異常,而是使用內部檢查機制checkError()方法進行錯誤檢查。最方便的是,它能夠格式化任意的型別,將它們以字串的形式寫入到輸出流。

public final static PrintStream out = null;

可以看到System.out也是PrintStream,不過預設是向控制檯列印,我們也可以讓它向檔案中列印:

public static void main(String[] args) {
    try(PrintStream stream = new PrintStream(new FileOutputStream("test.txt"))){
        stream.println("lbwnb");   //其實System.out就是一個PrintStream
    }catch (IOException e){
        e.printStackTrace();
    }
}

我們平時使用的println方法就是PrintStream中的方法,它會直接列印基本資料型別或是呼叫物件的toString()方法得到一個字串,並將字串轉換為字元,放入緩衝區再經過轉換流輸出到給定的輸出流上。

image

因此實際上內部還包含這兩個內容:

/**
 * Track both the text- and character-output streams, so that their buffers
 * can be flushed without flushing the entire stream.
 */
private BufferedWriter textOut;
private OutputStreamWriter charOut;

與此相同的還有一個PrintWriter,不過他們的功能基本一致,PrintWriter的構造方法可以接受一個Writer作為引數,這裡就不再做過多闡述了。

資料流(瞭解)

用於儲存(寫入)資料

注意:寫入的是二進位制資料,並不是寫入的字串,使用DataInputStream可以讀取,一般他們是配合一起使用的。

資料流DataInputStream也是FilterInputStream的子類,同樣採用裝飾者模式,最大的不同是它支援基本資料型別的直接讀取:

public static void main(String[] args) {
    try (DataInputStream dataInputStream = new DataInputStream(new FileInputStream("test.txt"))){
        System.out.println(dataInputStream.readBoolean());   //直接將資料讀取為任意基本資料型別
    }catch (IOException e) {
        e.printStackTrace();
    }
}

用於寫入基本資料型別:

public static void main(String[] args) {
    try (DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream("output.txt"))){
        dataOutputStream.writeBoolean(false);
    }catch (IOException e) {
        e.printStackTrace();
    }
}

物件流

用於儲存(寫入)資料,功能比資料流強大,不僅能儲存基本資料型別,還能儲存物件。【需要儲存的物件實體類一定要實現介面序列化!!!】

ObjectOutputStream不僅支援基本資料型別,透過對物件的序列化操作,以某種格式儲存物件,來支援物件型別的IO,注意:它不是繼承自FilterInputStream的。【在存物件和讀物件的時候,要保證實體類不能有修改,一旦修改,實體類版本號會不同,就無法讀取之前儲存的實體類物件,需要刪除原儲存的內容重新寫入】

public static void main(String[] args) {
    try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("output.txt"));
         ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("output.txt"))){
        People people = new People("lbw");
        outputStream.writeObject(people);
      	outputStream.flush();
        people = (People) inputStream.readObject();
        System.out.println(people.name);
    }catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

static class People implements Serializable{   //必須實現Serializable介面才能被序列化
    String name;

    public People(String name){
        this.name = name;
    }
}

六大流,十六小流總結

情況:

  1. 需要簡化操作的時候,且儲存的內容肉眼和識別沒有加密時【轉換流】:

    InputStreamReader reader = new InputStreamReader(new FileInputStream("test.txt"));
    OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("test.txt"))
    
  2. 需要儲存實體類物件、基本型別資料時【使用物件流(序列化流)】,儲存內容加密了,只能物件流才能讀取:

    ObjectOutputStream writer = new ObjectOutputStream(new FileOutputStream("output.txt");
    ObjectInputStream reader = new ObjectInputStream(new FileInputStream("output.txt"));
    
  3. 需要加快讀取和儲存檔案的時間,儲存內容可識別無加密【緩衝流】:

    BufferedOutputStream writer = new BufferedOutputStream(new FileOutputStream("output.txt"));
    BufferedInputStream reader = new BufferedInputStream(new FileInputStream("test.txt"));
    

Java多執行緒

執行緒基本操作是對執行緒的中斷、加入等操作。執行緒鎖的操作是對設定鎖的程式碼塊的等待、喚醒等操作。

介紹:程式是程式執行的實體,每一個程式都是一個應用程式(比如我們執行QQ、瀏覽器、LOL、網易雲音樂等軟體),都有自己的記憶體空間,CPU一個核心同時只能處理一件事情,當出現多個程式需要同時執行時,CPU一般透過時間片輪轉排程演算法,來實現多個程式的同時執行。

image

程式想要同時執行很麻煩,因為記憶體空間不同導致資料交換十分困難。於是,執行緒橫空出世,一個程式可以有多個執行緒,執行緒是程式執行中一個單一的順序控制流程,現線上程才是程式執行流的最小單元,各個執行緒之間共享程式的記憶體空間(也就是所在程式的記憶體空間),上下文切換速度也高於程式【一個程式可以有多個執行緒】

image

執行緒的基本操作

在沒建立執行緒直接使用Thread裡的方法,操作物件是main方法這個執行緒

執行緒建立和啟動

  1. 建立執行緒構造方法中只有一個run方法,所以可以用lamda表示式
  2. 啟動執行緒有start()和run()方法,前者才是多執行緒啟動,後者是單執行緒啟動
public static void main(String[] args) {
    //建立執行緒
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50; i++) {
            System.out.println("我是一號執行緒:"+i);
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50; i++) {
            System.out.println("我是二號執行緒:"+i);
        }
    });
    //啟動執行緒
    t1.start();
    t2.start();
}

注意:我們發現還有一個run方法,也能執行執行緒裡面定義的內容,但是run是直接在當前執行緒執行,並不是建立一個執行緒執行!

image

執行緒休眠和中斷

獲取當前執行緒的物件:

 Thread t = new Thread(() -> {
     Thread me = Thread.currentThread();	//獲取當前執行緒的物件
     String me = Thread.currentThread().getName();	//獲取當前執行緒的名字
 }
 t.setName("物件名");  	//線上程外設定物件名
 t.start();		//啟動執行緒後,Thread.currentThread().getName()讀到的就是設定的物件名。沒有定義名字就是系統給的名字

休眠:【類方法】

Thread.sleep(xxxx) //單位是毫秒,讓當前程式休眠x秒

中斷:【物件方法】

  • stop() 方法(不建議,因為強制性太強會導致資源釋放不充分)
Thread me = Thread.currentThread();   //獲取當前執行緒物件
me.stop();  //此方法會直接終止此執行緒
  • interrupt() 方法 (如果是要對執行緒中斷用該方法,要對程式碼塊中斷就用後面的wait() )

    原理:線上程內寫獲取中斷訊號的程式碼,執行緒外(如main執行緒)寫該執行緒物件的中斷訊號。main和該執行緒同時執行,直到main執行中斷訊號,該執行緒獲取到就會執行if裡的程式碼(這裡可以return中斷,也可以做別的操作)

public static void main(String[] args) throw InterruptedException{
    Thread t = new Thread(() -> {
        System.out.println("執行緒開始執行!");
        while (true){	//while是細節,如果沒有while,t執行緒執行完了,main執行緒都沒有把訊號釋放出來
            if(Thread.currentThread().isInterrupted()){   //判斷是否存在中斷標誌
                System.out.println("發現中斷訊號,復位,繼續執行...");
                Thread.interrupted();  //復位中斷標記(返回值是當前是否有中斷標記,這裡不用管)
            }
        }
    });
    t.start();
    Thread.sleep(3000);   //休眠3秒
    t.interrupt();   //呼叫t的interrupt方法
}
  • suspend()、resume() (不建議,容易死鎖)

    不推薦使用 suspend() 去掛起執行緒的原因,是因為suspend()在使執行緒暫停的同時,並不會去釋放任何鎖資源。其他執行緒都無法訪問被它佔用的鎖。直到對應的執行緒執行resume()方法後,被掛起的執行緒才能繼續,從而其它被阻塞在這個鎖的執行緒才可以繼續執行。但是,如果resume()操作出現在suspend()之前執行,那麼執行緒將一直處於掛起狀態,同時一直佔用鎖,這就產生了死鎖。

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("執行緒開始執行!");
        Thread.currentThread().suspend();   //暫停此執行緒
        System.out.println("執行緒繼續執行!");
    });
    t.start();
    try {
        Thread.sleep(3000);   //休眠3秒,一定比執行緒t先醒來
        t.resume();   //恢復此執行緒
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

執行緒優先順序、禮讓和加入

執行緒優先順序:

Java程式中的每個執行緒並不是平均分配CPU時間的,為了使得執行緒資源分配更加合理,Java採用的是搶佔式排程方式,優先順序越高的執行緒,優先使用CPU資源!【優先順序越高的執行緒,獲得CPU資源的機率會越大,並不是說一定優先順序越高的執行緒越先執行!】

  • MIN_PRIORITY 最低優先順序
  • MAX_PRIORITY 最高優先順序
  • NOM_PRIORITY 常規優先順序
public static void main(String[] args) {
    Thread t = new Thread(() -> {
        System.out.println("執行緒開始執行!");
    });
    t.start();
    t.setPriority(Thread.MIN_PRIORITY);  //透過使用setPriority方法來設定優先順序
}

執行緒禮讓:(瞭解)【類方法】

執行到禮讓後,會讓別的同優先順序執行緒先執行一小步

Thread.yield();	//執行緒內執行

執行緒加入:【物件方法】

兩個執行緒同時啟動,一個執行緒執行到join會等待另一個執行緒執行完成後再繼續進行。不是將另一個執行緒和當前執行緒合併!

Thread t1 = new Thread(() -> {
    
})
Thread t2 = new Thread(() -> {
	t1.join	//此時t2執行緒停下來,等t1執行完再執行
})
t1.start();
t2.start();                       

執行緒鎖和執行緒同步

在開始講解執行緒同步之前,我們需要先了解一下多執行緒情況下Java的記憶體管理:

image

執行緒之間的共享變數(比如之前懸念中的value變數)儲存在主記憶體(main memory)中,每個執行緒都有一個私有的工作記憶體(本地記憶體),工作記憶體中儲存了該執行緒以讀/寫共享變數的副本。它類似於我們在計算機組成原理中學習的多處理器快取記憶體機制:

image

快取記憶體透過儲存記憶體中資料的副本來提供更加快速的資料訪問,但是如果多個處理器的運算任務都涉及同一塊記憶體區域,就可能導致各自的快取記憶體資料不一致,在寫回主記憶體時就會發生衝突,這就是引入快取記憶體引發的新問題,稱之為:快取一致性

實際上,Java的記憶體模型也是這樣類似設計的,當我們同時去操作一個共享變數時,如果僅僅是讀取還好,但是如果同時寫入內容,就會出現問題!好比說一個銀行,如果我和我的朋友同時在銀行取我賬戶裡面的錢,難道取1000還可能吐2000出來嗎?我們需要一種更加安全的機制來維持秩序,保證資料的安全性

思考:該程式碼執行結果

private static int value = 0;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) value++;
        System.out.println("執行緒1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) value++;
        System.out.println("執行緒2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主執行緒停止1秒,保證兩個執行緒執行完成
    System.out.println(value);
}

結果:不一定是20000,原因就是快取一致性。當兩個執行緒同時讀取value的時候,可能會同時拿到同樣的值,而進行自增操作之後,也是同樣的值,再寫回主記憶體後,本來應該進行2次自增操作,實際上只執行了一次!【此時就需要引入執行緒鎖來解決問題】

執行緒鎖

原理:當一個執行緒進入到同步程式碼塊時,會獲取到當前的鎖,而這時如果其他使用同樣的鎖的同步程式碼塊也想執行內容,就必須等待當前同步程式碼塊的內容執行完畢,在執行完畢後會自動釋放這把鎖,而其他的執行緒才能拿到這把鎖並開始執行同步程式碼塊裡面的內容。(實際上synchronized是一種悲觀鎖,隨時都認為有其他執行緒在對資料進行修改,後面有機會我們還會講到樂觀鎖,如CAS演算法)

synchronized(){}關鍵字建立執行緒鎖。

  1. 它需要在括號中填入一個內容作為鎖,必須是一個物件或是一個類。【被作為鎖的物件或者類不會被影響】

    【別的執行緒和該執行緒操作同一個變數時,鎖必須和該執行緒的鎖一致,否則無用】

  2. {}寫入可能和別的執行緒操作同一變數的程式碼。

  • 作用於程式碼塊【麻煩,在不用等待和喚醒的時候不建議使用(等待和喚醒是必須用於程式碼塊)】
private static int value = 0;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            synchronized (Main.class){	//同一把鎖
                value++;
            }
        }
        System.out.println("執行緒1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) {
            synchronized (Main.class){	//同一把鎖
                value++;
            }
        }
        System.out.println("執行緒2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主執行緒停止1秒,保證兩個執行緒執行完成
    System.out.println(value);
}
  • 作用於方法【便捷,建議使用】

作用於方法,只要不同執行緒呼叫該方法,就是在相同的鎖情況執行,和前面效果一樣

​ 我們發現實際上效果是相同的,只不過這個鎖不用你去給,如果是靜態方法,就是使用的類鎖,而如果是普通成員方法,就是使用的物件鎖。

private static int value = 0;

private static synchronized void add(){
    value++;
}

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) add();
        System.out.println("執行緒1完成");
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 10000; i++) add();
        System.out.println("執行緒2完成");
    });
    t1.start();
    t2.start();
    Thread.sleep(1000);  //主執行緒停止1秒,保證兩個執行緒執行完成
    System.out.println(value);
}

死鎖

介紹:死鎖的概念在作業系統中也有提及,它是指兩個執行緒相互持有對方需要的鎖,但是又遲遲不釋放,導致程式卡住:

image

檢測死鎖的方法:

cmd中輸入jps檢視java程式,ps是電腦程式。jstack是自動找到死鎖,並列印相關執行緒的棧追蹤資訊。

程式的等待與喚醒

wait()notify()以及notifyAll()方法需要配合synchronized關鍵字使用,並且只能在同步程式碼塊(synchronized關鍵字的程式碼塊)中才能使用。

wait()notify()以及notifyAll()作用:使上了同一把鎖的程式可以輪流執行。【白話:一邊程式碼塊執行到一半,等另一邊執行完程式碼塊再繼續執行未執行完的程式碼塊】

舉例:程式碼解釋:物件的wait()方法會暫時使得此執行緒進入等待狀態,同時會釋放當前程式碼塊持有的鎖,這時其他執行緒可以獲取到此物件的鎖,當其他執行緒呼叫物件的notify()方法後,會喚醒剛才變成等待狀態的執行緒(這時並沒有立即釋放鎖)。注意,必須是在持有鎖(同步程式碼塊內部)的情況下使用,否則會丟擲異常!

public static void main(String[] args) throws InterruptedException {
    Object o1 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (o1){
            try {
                System.out.println("開始等待");
                o1.wait();     //進入等待狀態並釋放鎖給別的執行緒
                System.out.println("等待結束!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (o1){
            System.out.println("開始喚醒!");
            o1.notify();     //喚醒處於隨機一個等待狀態的執行緒,但仍然要執行完當前鎖的程式碼塊
//            o1.notifyall();	//喚醒all處於等待的執行緒,但仍然要執行完當前鎖的程式碼塊
          	for (int i = 0; i < 50; i++) {
               	System.out.println(i);   
            }
          	//喚醒後依然需要等待這裡的鎖釋放之前等待的執行緒才能繼續
        }
    });
    t1.start();
    Thread.sleep(1000);
    t2.start();
}

ThreadLocal

用於儲存一個執行緒專有的值【物件方法】

ThreadLocal類,來建立工作記憶體中的變數,它將我們的變數值儲存在內部(只能儲存一個變數),不同的變數訪問到ThreadLocal物件時,都只能獲取到自己執行緒所屬的變數。【每個執行緒的工作記憶體空間不同,所以執行緒之間相互獨立,互不相關】

public static void main(String[] args) throws InterruptedException {
    ThreadLocal<String> local = new ThreadLocal<>();  //注意這是一個泛型類,儲存型別為我們要存放的變數型別
    Thread t1 = new Thread(() -> {
        local.set("lbwnb");   //將變數的值給予ThreadLocal
        System.out.println("執行緒1變數值已設定!");
        try {
            Thread.sleep(2000);    //間隔2秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行緒1讀取變數值:");
        System.out.println(local.get());   //嘗試獲取ThreadLocal中存放的變數
    });
    Thread t2 = new Thread(() -> {
        local.set("yyds");   //將變數的值給予ThreadLocal
        System.out.println("執行緒2變數值已設定!");
    });
    t1.start();
    Thread.sleep(1000);    //間隔1秒
    t2.start();
}

//結果:lbwnb。就算t2也設定了值,但不影響t1的值

擴充:子類執行緒也獲得不了父類執行緒設定的值,但可以透過用InheritableThreadLocal方法來解決這個問題。(在InheritableThreadLocal存放的內容,會自動向子執行緒傳遞)

public static void main(String[] args) {
    ThreadLocal<String> local = new InheritableThreadLocal<>();
    Thread t = new Thread(() -> {
       local.set("lbwnb");
        new Thread(() -> {
            System.out.println(local.get());
        }).start();
    });
    t.start();
}

執行緒小結及練習

  1. 一般都是將鎖設在方法裡,執行緒來呼叫方法。
  2. 儘量不在鎖裡睡眠sleep
private static List<Object> list =new ArrayList<>();

    public static void main(String[] args) {
        Thread chef01 = new Thread(Main::cook);
        chef01.setName("廚師一");
        Thread chef02 = new Thread(Main::cook);
        chef02.setName("廚師二");
        chef01.start();
        chef02.start();

        Thread con01 = new Thread(Main::eat);
        con01.setName("消費者一");
        Thread con02 = new Thread(Main::eat);
        con02.setName("消費者二");
        Thread con03 = new Thread(Main::eat);
        con03.setName("消費者三");
        con01.start();
        con02.start();
        con03.start();
    }

    private static void cook() {
        while (true){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (list){
                list.add(new Object());
                System.out.println(new Date() +Thread.currentThread().getName()+": 新增了新菜!");
                list.notifyAll();
            }
        }
    }

    private static void eat() {
        while (true){
            try {
                synchronized (list){
                    while (list.isEmpty())list.wait();    //當只做了一盤菜,但有多個顧客要時,先搶到的就出菜,沒搶到的就繼續迴圈等待
                    System.out.println(new Date()+Thread.currentThread().getName()+" 拿走了一盤菜!");
                    list.remove(0);
                }
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

定時器

執行的時候會發現,如果不手動結束任務,在執行完任務後不會終止程式。這是因為Timer記憶體維護了一個任務佇列和一個工作執行緒。(詳細的去看原始碼)

public static void main(String[] args) {
    Timer timer = new Timer();    //建立定時器物件
    timer.schedule(new TimerTask() {   //注意這個是一個抽象類,不是介面,無法使用lambda表示式簡化,只能使用匿名內部類
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());    //列印當前執行緒名稱
            timer.cancel(); 	//結束任務
        }
    }, 1000);    //執行一個延時任務
}

守護執行緒

白話:守護執行緒是在別的執行緒結束後會自動結束自己的執行緒,無論執行到哪裡都會立即結束。

守護執行緒裡的子執行緒也會因父類是守護執行緒,其他執行緒都結束後,該父類與子類也會自動結束。

不要把守護程式和守護執行緒相提並論!守護程式在後臺執行執行,不需要和使用者互動,本質和普通程式類似。而守護執行緒就不一樣了,當其他所有的非守護執行緒結束之後,守護執行緒是自動結束,也就是說,Java中所有的執行緒都執行完畢後,守護執行緒自動結束,因此守護執行緒不適合進行IO操作,只適合打打雜

在守護執行緒中產生的新執行緒也是守護的:【仍然會被中斷】

public static void main(String[] args) throws InterruptedException{
    Thread t = new Thread(() -> {
        Thread it = new Thread(() -> {
            while (true){
                try {
                    System.out.println("程式正常執行中...");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        it.start();
    });
    t.setDaemon(true);   //設定為守護執行緒(必須在開始之前,中途是不允許轉換的)
    t.start();
    for (int i = 0; i < 5; i++) {
        Thread.sleep(1000);
    }
}

集合類並行流

集合類中透過並行流可以大大提高運算速度。注意:列印的時候使用並行流中的foreach就不會按照原來的順序列印,所以需要使用並行流中的forEachOrdered列印就可以保持順序的單執行緒列印了。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 4, 5, 2, 9, 3, 6, 0));
    list
            .parallelStream()    //獲得並行流
            .forEachOrdered(System.out::println);
}

Arrays陣列工具類中也有並行方法:

public static void main(String[] args) {
    int[] arr = new int[]{1, 4, 5, 2, 9, 3, 6, 0};
    Arrays.parallelSort(arr);   //使用多執行緒進行並行排序,效率更高
    System.out.println(Arrays.toString(arr));
}

資料結構

線性表

順序表

用陣列實現一個表,對其增加、刪除後資料對自動填補

image

//抽象類
/**
 * 線性表抽象類
 * @param <E> 儲存的元素(Element)型別
 */
public abstract class AbstractList<E> {
    /**
     * 獲取表的長度
     * @return 順序表的長度
     */
    public abstract int size();

    /**
     * 新增一個元素
     * @param e 元素
     * @param index 要新增的位置(索引)
     */
    public abstract void add(E e, int index);

    /**
     * 移除指定位置的元素
     * @param index 位置
     * @return 移除的元素
     */
    public abstract E remove(int index);

    /**
     * 獲取指定位置的元素
     * @param index 位置
     * @return 元素
     */
    public abstract E get(int index);
}


//具體實現類
public class MyList<E> extends AbstractList{
    private int size=0;
    private Object[] arr = new Object[1];


    @Override
    public int size() {
        return size;
    }

    @Override
    public void add(Object o, int index) {
        //判斷插入下標是否有誤
        if (index > size) throw new IllegalArgumentException("非法位置插入");
        //滿了就擴容
        if (size == this.arr.length){
            Object[] arr = new Object[this.arr.length+10];
            for (int i = 0; i < this.arr.length; i++) arr[i]=this.arr[i];
            this.arr=arr;
        }
        //後移
        for (int i = size-1; i >= index; i--) {
            arr[i+1]=arr[i];
        }
        //插入
        arr[index]=o;
        size++;
    }

    @Override
    public Object remove(int index) {
        //判斷刪除下標是否有誤
        if (index >= size) throw new IllegalArgumentException("非法位置輸入");
        //記錄刪除的資料
        E e = (E) arr[index];
        //前移(這裡有個細節,不需要刪除再前移而是直接覆蓋即可)
        for (int i = index; i < size-1; i++) {
            arr[i]=arr[i+1];
        }
        size--;
        return e;
    }

    @Override
    public Object get(int index) {
        //判斷獲取元素的下標是否有誤
        if (index >= size) throw new IllegalArgumentException("非法位置輸入!");
        return arr[index];
    }
}

連結串列

官方:資料分散的儲存在物理空間中,透過一根線儲存著它們之間的邏輯關係,這種儲存結構稱為鏈式儲存結構

白話:就是每一個結點存放一個元素和一個指向下一個結點的引用(C語言裡面是指標,Java中就是物件的引用,代表下一個結點物件)

image

//抽象類和上面一樣

//具體實現類
public class LinkedList<E> extends AbstractList<E>{
    private int size=0;
    private Node<E> head = new Node<E>(null);

    private static class Node<E>{
        private E e;
        private Node<E> next;
        public Node(E e){
            this.e=e;
        }
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public void add(E o, int index) {
        //下標超出
        if (index > size) throw new IllegalArgumentException("非法下標輸入");
        //迴圈找到需要插入的前一個node
        Node<E> node=head,temp;
        for (int i = 0; i < index; i++) {
            node=node.next;
        }
        //暫存index後一個的結點指標
        temp=node.next;
        //建立一個新的結點插入到index上
        node.next=new Node<>(o);
        node.next.next=temp;
        size++;
    }

    @Override
    public E remove(int index) {
        //非法下標輸入
        if (index >= size) throw new IllegalArgumentException("非法下標輸入");
        //迴圈找到需要刪除的結點的前一個結點
        Node<E> node = head,temp;
        for (int i = 0; i < index; i++) {
            node=node.next;
        }
        //暫存刪除的節點
        temp=node.next;
        //改變index前一個結點的next,直接跨過index,與下一個節點關聯
        node.next=node.next.next;
        size--;
        return temp.e;
    }

    @Override
    public E get(int index) {
        //檢查下標合法性
        if (index >= size )throw new IllegalArgumentException("非法下標輸入");
        //迴圈找到index位置上的結點值並return
        Node<E> node=head;
        for (int i = 0; i < index; i++) {
            node=node.next;
        }
        return node.next.e;
    }
}

順序表優缺點:

  • 訪問速度快,隨機訪問效能高
  • 插入和刪除的效率低下,極端情況下需要變更整個表
  • 不易擴充,需要複製並重新建立陣列

連結串列優缺點:

  • 插入和刪除效率高,只需要改變連線點的指向即可
  • 動態擴充容量,無需擔心容量問題
  • 訪問元素需要依次尋找,隨機訪問元素效率低下

JVM呼叫方法的時候就是一個棧操作

先入後出原則。類似於杯子只有一端開口。入棧(壓棧)、出棧

image

程式碼實現:可以看作是單指標法,用size做指標

//抽象類
/**
 * 抽象型別棧,待實現
 * @param <E> 元素型別
 */
public abstract class AbstractStack<E> {

    /**
     * 出棧操作
     * @return 棧頂元素
     */
    public abstract E pop();

    /**
     * 入棧操作
     * @param e 元素
     */
    public abstract void push(E e);
}

//具體實現類
public class ArrayStack<E> extends AbstractStack<E>{
    private int size=0;
    private Object[] arr = new Object[1];


    @Override
    public E pop() {
        return (E) arr[size-1];
    }

    @Override
    public void push(E e) {
        //滿了就擴容
        if (size == this.arr.length){
            Object[] arr = new Object[this.arr.length+10];
            for (int i = 0; i < this.arr.length; i++) arr[i]=this.arr[i];
            this.arr=arr;
        }
        //壓棧
        arr[size++]=e;
    }
}

佇列

和食堂排隊一樣,先進先出

image

程式碼實現:可以看作是雙指標法,head、tail,分別確定入隊和出隊元素的下標

//抽象類
/**
 *
 * @param <E>
 */
public abstract class AbstractQueue<E> {

    /**
     * 進隊操作
     * @param e 元素
     */
    public abstract void offer(E e);

    /**
     * 出隊操作
     * @return 元素
     */
    public abstract E poll();
}

//具體實現類

public class ArrayQueue<E> extends AbstractQueue<E> {   //佇列:編寫程式碼可以理解為一個迴圈圈
    private Object[] arr=new Object[4];
    private int head=0,tail=0;
    
    @Override
    public void offer(E e) {
        //保證下一個不是head就可以繼續插進去
        int next=(tail+1)% arr.length;
        if (next==head) return;
        //插入並迴圈
        arr[tail]=e;
        tail=(tail+1)% arr.length;
    }

    @Override
    public E poll() {
        E e= (E) arr[head];
        head=(head+1)%arr.length;
        return e;
    }
}

連結串列實現棧:壓棧頭插法,出棧頭取法

壓棧就加到連結串列的首節點,連結串列的資料後推一個單位;出棧就將首節點斷出來,頭節點與首節點的下一節點相連。

//把t元素壓入棧(相當於連結串列的頭插法)
    public void push(T t){
        //首結點指向的第一個元素
        Node first=head.next;
        Node newNode = new Node(t, null);
        //首結點指向新結點
        head.next=newNode;
        //新結點指向原來的第一節點first
        newNode.next=first;
        //元素個數加1
        N++;
    }
 
    //把元素彈出棧
    pub
        Node first=head.next;
        if(first==null){
            return null;
        }
        head.next=first.next;
        //元素個數-1
        N--;
        return first.data;
    }

連結串列實現佇列:入隊則尾插法,出隊則頭取法

public void push(int val){
        Node node = new Node(0);
        tail.val = val;
        tail.next = node;
        tail = tail.next;
        size++;
    }

    public int pop(){
        if(size == 0) return -1;
        int a = head.next.val;
        head = head.next;
        size--;
        System.out.println(a);
        return a;
    }


二叉樹

特點:一對多。(順序表、連結串列是一對一)

位於最頂端的結點(沒有父結點)我們稱為根結點,而結點擁有的子節點數量稱為,每向下一級稱為一個層次,樹中出現的最大層次稱為樹的深度(高度)

image

二叉樹

二叉樹每個結點最多有兩棵樹,即左右子樹

image

二叉樹數學性質:

  • 在二叉樹的第i層上最多有2^(i-1) 個節點。
  • 二叉樹中如果深度為k,那麼最多有2^k-1個節點。

二叉樹程式碼實現:

public class TreeNode<E> {
    public E e;   //當前結點資料
    public TreeNode<E> left;   //左子樹
    public TreeNode<E> right;   //右子樹
}

二叉樹的遍歷方式:

  • 前序遍歷:從二叉樹的根結點出發,到達結點時就直接輸出結點資料,按照先向左在向右的方向訪問。ABCDEF

    public static void test(TreeNode root){
            //判斷節點存不存在
            if (root == null ) return;
            //先序遍歷,先輸出根節點
            System.out.print(root.e+"  ");
            test(root.left);
            test(root.right);
        }
    
  • 中序遍歷:從二叉樹的根結點出發,優先輸出左子樹的節點的資料,再輸出當前節點本身,最後才是右子樹。CBDAEF

     public static void test(TreeNode root){
            //判斷節點存不存在
            if (root == null ) return;
            test(root.left);
            //中序遍歷,中間輸出根節點
            System.out.print(root.e+"  ");
            test(root.right);
        }
    
  • 後序遍歷:從二叉樹的根結點出發,優先遍歷其左子樹,再遍歷右子樹,最後在輸出當前節點本身。CDBFEA

     public static void test(TreeNode root){
            //判斷節點存不存在
            if (root == null ) return;
            test(root.left);
            test(root.right);
            //中序遍歷,中間輸出根節點
            System.out.print(root.e+"  ");
        }
    

滿二叉樹與完全二叉樹

滿二叉樹:除最後一層無任何子節點外,每一層上的所有結點都有兩個子結點的二叉樹(白話:所有葉子節點都在同一層)

完全二叉樹:完全二叉樹與滿二叉樹不同的地方在於,它的最下層葉子節點可以不滿,但是最下層的葉子節點必須靠左排布

image

快速查詢

雜湊表

JDK1.8 後才有的。本質就是一個存放連結串列的陣列

由於順序表查詢效率高,但插入刪除效率低。然而連結串列又是插入刪除效率高,查詢效率慢。於是又了折中的雜湊表

image

解釋:陣列中每個元素都是一個頭節點,用於儲存資料。透過hash演算法,可以快速得到元素應該放置在陣列的哪個下標位置。

//假設hash表長度為16,hash演算法為:
private int hash(int hashcode){
  return hashcode % 16;
}

hash碰撞:某兩個數透過某些hash演算法(例如以上的演算法)後得到的hash值相同。

此時先得到hash值得數先進該hash值的陣列下標,後面的數就以連結串列的形式與前一個數連線,以此類推

二叉排序樹

定義:每個節點的左子樹的值小於該節點的值,每個節點的右子樹的值大於該節點的值

image

平衡二叉樹

定義:每個結點的左右兩個子樹的高度差的絕對值不超過1

如何保證二叉排序樹是平衡二叉樹:同時要求每個節點的左右子樹都是平衡二叉樹

image

左左失衡

image

右右失衡

image

左右失衡

image

右左失衡

透過以上四種情況的處理,最終得到維護平衡二叉樹的演算法。

紅黑樹

紅黑樹也是二叉排序樹的一種改進,同平衡二叉樹一樣,紅黑樹也是一種維護平衡的二叉排序樹,但是沒有平衡二叉樹那樣嚴格(平衡二叉樹每次插入新結點時,可能會出現大量的旋轉,而紅黑樹保證不超過三次),紅黑樹降低了對於旋轉的要求,因此效率有一定的提升同時實現起來也更加簡單。但是紅黑樹的效率卻高於平衡二叉樹,紅黑樹也是JDK1.8中使用的資料結構!

image

紅黑樹的特性:
(1)每個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每個葉子節點的兩邊也需要表示(雖然沒有,但是null也需要表示出來)是黑色。
(4)如果一個節點是紅色的,則它的子節點必須是黑色的。
(5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

我們來看看一個節點,是如何插入到紅黑樹中的:

基本的 插入規則和平衡二叉樹一樣,但是在插入後:

  1. 將新插入的節點 X 標記為紅色
  2. 如果 X 是根結點(root),則標記為黑色
  3. 如果 X 的 parent 不是黑色,同時 X 也不是 root:
  • 3.1 如果 X 的 uncle (叔叔) 是紅色

    • 3.1.1 將 parent 和 uncle 標記為黑色

    • 3.1.2 將 grand parent (祖父) 標記為紅色

    • 3.1.3 讓 X 節點的顏色與 X 祖父的顏色相同,然後重複步驟 2、3

  • 3.2 如果 X 的 uncle (叔叔) 是黑色,我們要分四種情況處理

    • 3.2.1 左左 (P 是 G 的左孩子,並且 X 是 P 的左孩子)

    • 3.2.2 左右 (P 是 G 的左孩子,並且 X 是 P 的右孩子)

    • 3.2.3 右右 (P 是 G 的右孩子,並且 X 是 P 的右孩子)

    • 3.2.4 右左 (P 是 G 的右孩子,並且 X 是 P 的左孩子)

      (其實這種情況下處理就和我們的平衡二叉樹一樣了)

插入的動畫演示:

它相比平衡二叉樹,透過不嚴格平衡和改變顏色,就能在一定程度上減少旋轉次數,這樣的話對於整體效能是有一定提升的,只不過我們在插入結點時,就有點麻煩了,我們需要同時考慮變色和旋轉這兩個操作了,但是會比平衡二叉樹更簡單。

那麼什麼時候需要變色,什麼時候需要旋轉呢?我們透過一個簡單例子來看看:

image

首先這棵紅黑樹只有一個根結點,因為根結點必須是黑色,所以說直接變成黑色。現在我們要插入一個新的結點了,所有新插入的結點,預設情況下都是紅色:

image

所以新來的結點7根據規則就直接放到11的左邊就行了,然後注意7的左右兩邊都是NULL,那麼預設都是黑色,這裡就不畫出來了。同樣的,我們往右邊也來一個:

image

現在我們繼續插入一個結點:

image

插入結點4之後,此時違反了紅黑樹的規則3,因為紅色結點的父結點和子結點不能為紅色,此時為了保持以紅黑樹的性質,我們就需要進行顏色變換才可以,那麼怎麼進行顏色變換呢?我們只需要直接將父結點和其兄弟結點同時修改為黑色(為啥兄弟結點也需要變成黑色?因為要滿足性質5)然後將爺爺結點改成紅色即可:

image

當然這裡還需注意一下,因為爺爺結點正常情況會變成紅色,相當於新來了個紅色的,這時還得繼續往上看有沒有破壞紅黑樹的規則才可以,直到沒有為止,比如這裡就破壞了性質一,爺爺結點現在是根結點(不是根結點就不需要管了),必須是黑色,所以說還要給它改成黑色才算結束:

image

接著我們繼續插入結點:

image

此時又來了一個插在4左邊的結點,同樣是連續紅色,我們需要進行變色才可以講解問題,但是我們發現,如果變色的話,那麼從11開始到所有NIL結點經歷的黑色結點數量就不對了:

image

所以說對於這種父結點為紅色,父結點的兄弟結點為黑色(NIL視為黑色)的情況,變色無法解決問題了,那麼我們只能考慮旋轉了,旋轉規則和我們之前講解的平衡二叉樹是一樣的,這實際上是一種LL型失衡:

image

同樣的,如果遇到了LR型失衡,跟前面一樣,先左旋在右旋,然後進行變色即可:

image

而RR型和RL型同理,這裡就不進行演示了,可以看到,紅黑樹實際上也是透過顏色規則在進行旋轉調整的,當然旋轉和變色的操作順序可以交換。所以,在插入時比較關鍵的判斷點如下:

  • 如果整棵樹為NULL,直接作為根結點,變成黑色。
  • 如果父結點是黑色,直接插入就完事。
  • 如果父結點為紅色,且父結點的兄弟結點也是紅色,直接變色即可(但是注意得繼續往上看有沒有破壞之前的結構)
  • 如果父結點為紅色,但父結點的兄弟結點為黑色,需要先根據情況(LL、RR、LR、RL)進行旋轉,然後再變色。

在瞭解這些步驟之後,我們其實已經可以嘗試去編寫一棵紅黑樹出來了,當然程式碼太過複雜,這裡就不演示了。其實紅黑樹難點並不在於如何構建和使用,而是在於,到底是怎麼設計出來的,究竟要多麼豐富的知識儲備才能想到如此精妙的規則。

紅黑樹的發明者:

紅黑樹(Red Black Tree) 是一種自平衡二叉查詢樹,是在計算機科學中用到的一種資料結構,典型的用途是實現關聯陣列

紅黑樹是在1972年由[Rudolf Bayer](https://baike.baidu.com/item/Rudolf Bayer/3014716)發明的,當時被稱為平衡二叉B樹(symmetric binary B-trees)。後來,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改為如今的“紅黑樹”。


集合類

集合類的頂層都是介面,下面的每個類都是實現了上面的介面

image

不可變集合

這個功能在jdk9後才有!!!

不可變集合特點:定義完成後不可以修改、新增、刪除 (和final關鍵字類似)

使用:呼叫靜態方法of方法

List

List<String> list = List.of("柯南","毛利蘭","灰原哀","阿笠博士");

Set

注意:這裡定義的集合中不可有重複值,否則報錯

Set<String> set = Set.of("柯南","毛利蘭","灰原哀","阿笠博士");

Map

注意:鍵是不可重複的(下面的鍵是偵探n號)

當不可變引數的鍵值對在十對以內:

Map<String,String> map = Map.of("偵探一號","工藤新一","偵探二號","毛利小五郎","偵探三號","鈴木園子","偵探四號","服部平次","偵探五號","白馬探");

超過十對鍵值對,但是jdk9時:使用Map.ofEntries(xx); xx是一個陣列,這個方法引數需要接收一個鍵值對陣列

Map<String,String> map = new HashMap<>();
map.put("偵探一號","工藤新一");
map.put("偵探二號","毛利小五郎");
map.put("偵探三號","鈴木園子");
map.put("偵探四號","服部平次");
map.put("偵探五號","白馬探");

//toArray(xx)引數是指定返回的陣列型別,所以傳一個陣列進去該陣列型別就是這個方法返回的型別
Map.ofEntries(map.entrySet().toArray(new Map.Entry[0]));

超過十對鍵值對,且是jdk10時:

Map<String,String> map = new HashMap<>();
map.put("偵探一號","工藤新一");
map.put("偵探二號","毛利小五郎");
map.put("偵探三號","鈴木園子");
map.put("偵探四號","服部平次");
map.put("偵探五號","白馬探");

Map<String,String> newMap = Map.copyOf(map);

迭代器

應用例子可以看看前面foreach內容

Iterable和Iterator介面

每個集合類都有自己的迭代器,透過iterator()方法來獲取:

Iterator<Integer> iterator = list.iterator();   //生成一個新的迭代器
while (iterator.hasNext()){    //判斷是否還有下一個元素
  Integer i = iterator.next();     //獲取下一個元素(獲取一個少一個)
  System.out.println(i);
}

迭代器生成後,預設指向第一個元素,每次呼叫next()方法,都會將指標後移,當指標移動到最後一個元素之後,呼叫hasNext()將會返回false,迭代器是一次性的,用完即止,如果需要再次使用,需要呼叫iterator()方法。

ListIterator<Integer> iterator = list.listIterator();   //List還有一個更好地迭代器實現ListIterator

ListIterator是List中獨有的迭代器,在原有迭代器基礎上新增了一些額外的操作。

Set和Map撇不清的關係

特點:

HashSet:元素不重複、無序

HashMap:key不重複、key無序

LinkedHashSet:在HashSet基礎上,會自動儲存我們(訪問)插入元素的順序(set無法訪問單獨一個元素,因為元素無序)

LinkedHashMap:在HashMap基礎上,會自動儲存我們(訪問)插入元素的順序,對剛(訪問)獲取過的元素會將其位置放到最後

TreeSet:元素不重複,元素從大到小排列(預設)

TreeMap:key不重複,元素順序按照key值從大到小排列(預設)

解釋:

Hash 是用Hash表來存放資料

Tree 是用紅黑樹來存放資料(可以回顧紅黑樹特點)


Stream流

介紹:Java 8 API新增了一個新的抽象稱為流Stream,可以讓你以一種宣告的方式處理資料。Stream 使用一種類似用 SQL 語句從資料庫查詢資料的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。這種風格將要處理的元素集合看作一種流, 流在管道中傳輸, 並且可以在管道的節點上進行處理, 比如篩選, 排序,聚合等。元素流在管道中經過中間操作(intermediate operation)的處理,最後由最終操作(terminal operation)得到前面處理的結果。

stream流思想:像sql語句一樣,對資料一步步的操作得到最終需要的結果。但執行的時候並不是像sql一樣一句句的順序執行,因為stream有指定的執行策略,流會將每次鏈式操作都記錄下來,然後按照內建的鏈式優先順序執行鏈式操作。如下圖一樣,會先執行中間方法 filter --> map --> skip (limit) 等再執行終結方法 count、collect、foreach【一個流中 中間方法可以有多個,但終結方法只能有一個】

image

image
stream流對集合類、工具類的基本操作:

流會將每次鏈式操作都記錄下來,然後按照內建的鏈式優先順序執行鏈式操作

//流對集合類操作
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(3);

list = list
        .stream()
        .distinct()   
        .sorted((a, b) -> b - a)
        .map(e -> {
            System.out.println(">>> "+e);   
            return e+1;
        })
        .limit(2)   
        .collect(Collectors.toList());
//實際上,stream會先記錄每一步操作,而不是直接開始執行內容,當整個鏈式呼叫完成後,才會依次進行!


//流對工具類操作
public static void main(String[] args) {
    Random random = new Random();  //Random是一個隨機數工具類
    random
            .ints(-100, 100)   //生成-100~100之間的,隨機int型數字(本質上是一個IntStream)
            .limit(10)   //只獲取前10個數字(這是一個無限制的流,如果不加以限制,將會無限進行下去!)
            .filter(i -> i < 0)   //只保留小於0的數字
            .sorted()    //預設從小到大排序
            .forEach(System.out::println);   //依次列印
}

將工具類生成一個統計例項實現快速統計:

IntSummaryStatistics 該類只能對靜態資料操作(陣列、random隨機數),不可以是集合類

public static void main(String[] args) {   
    //工具類生成統計例項
    Random random = new Random();  //Random是一個隨機數工具類
    IntSummaryStatistics statistics = random
            .ints(0, 100)
            .limit(100)	//這裡不限制則會無線的生成隨機數
            .summaryStatistics();    //獲取語法統計例項
    System.out.println(statistics.getMax());  //快速獲取最大值
    System.out.println(statistics.getCount());  //獲取數量
    System.out.println(statistics.getAverage());   //獲取平均值
}

獲取流的方法

有三種情況可以獲取流:(下面雙列集合(map)不能直接獲取流,但可以轉成單列集合來獲取)

獲取方式 方法名 說明
單列集合 default Stream stream() Collection中的預設方法
雙列集合 無法直接使用stream流
陣列 public static Stream stream(T[] array) Arrays工具類中的靜態方法
一堆零散資料 public static Stream of(T… values) Stream介面中的靜態方法

單列集合

使用list.stream()

list.stream().forEach(s -> System.out.println(s));

雙列集合

以map為例

  1. 方式一:健值分別獲取流 map.keySet().stream() map.values().stream()

    HashMap<String, Integer> map = new HashMap<>();
    //獲取鍵的流
    map.keySet().stream().forEach(s -> System.out.println(s));
    //獲取值的流
    map.values().stream().forEach(s -> System.out.println(s));
    
  2. 方式二:鍵值對作為整體來獲取流

    HashMap<String, Integer> map = new HashMap<>();
    map.entrySet().stream().forEach(s -> System.out.println(s.getKey() + ":" + s.getValue()));
    

陣列

使用Arrays.stram(arr)

int[] arr = {1, 2, 3};
Arrays.stream(arr).forEach(s -> System.out.println(s));

零散資料

注意這裡雖然是叫零散資料,但必須保證零散的資料是同一型別的

使用Stream.of(T... values) 形參是一個可變引數,可變引數的底層是陣列(所以可以傳一個陣列、列表、集合等)

Stream.of(1,2,3,8,6).forEach(s -> System.out.println(s));

注意:

傳來的陣列必須是包裝類陣列。因為將基本資料型別的陣列到引數中,該陣列會被當成一個資料而不是一組陣列。

 //基本資料型別
int[] arr = {1, 2, 3};
//引用資料型別
String[] arr2 = {"a", "b", "c"};
// 基本資料型別
Stream.of(arr).forEach(s -> System.out.println(s)); // [I@7c3df479 列印的是地址
// 引用資料型別
Stream.of(arr2).forEach(s -> System.out.println(s));// a b c

Stream的中間方法

filter 過濾

符合條件的資料才留下,不符合的就去除

List<String> list = new ArrayList<>();
Collections.addAll(list, "毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");

/*匿名內部類寫法:*/
// filter 過濾:把姓毛利的留下
list.stream().filter(new Predicate<String>() { //這裡的String只是資料的型別
    @Override
    public boolean test(String s) {	//這裡的String只是資料的型別
        // 如果返回值為true,表示當前資料保留,否則捨棄
        return s.startsWith("毛利");
    }
}).forEach(s -> System.out.println(s));	//毛利蘭,毛利小五郎


/* lamdba寫法:*/
list.stream().filter(s -> s.startsWith("張")).forEach(s -> System.out.println(s));//毛利蘭,毛利小五郎

map 轉換

轉換流中的資料型別,也可以修改資料,不改變原來集合list的資料

List<String> list = new ArrayList<>();
Collections.addAll(list, "工藤新一-17", "灰原哀-12", "毛利蘭-16",
                   "白馬探-20", "阿笠博士-55", "毛利小五郎-40", "目暮警官-35", "佐藤-28", "高木-29");
// 目的:只獲取其中的年齡 String -> int

/*匿名內部類寫法:*/
// 第一個引數型別:流中原本的資料型別
// 第二個引數型別:要轉成之後的型別
list.stream().map(new Function<String, Integer>() {
    // apply的形參s:依次表示流中的每一個資料
	// 返回值:表示轉換之後的資料
    @Override
    public Integer apply(String s) {
        String[] split = s.split("-");
        Integer age = Integer.valueOf(split[1]);
        return age;
    }
}).forEach(s -> System.out.print(s + " "));		//17 12 16 20 55 40 35 28 29

/* lamdba寫法:*/
list.stream()
    .map(s -> Integer.valueOf(s.split("-")[1]))	//s代表每個資料,箭頭後面是返回值
    .forEach(s -> System.out.print(s + " "));   // 17 12 16 20 55 40 35 28 29

limit 保留 skip 跳過

limit:只保留前n個資料 skip:跳過前n個資料

注:limit和skip是同優先順序的,所以先寫哪個就執行哪個

List<String> list = new ArrayList<>();
Collections.addAll(list, "毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");

//目的:只得到"基德", "服部平次", "阿笠博士"
list.stream().skip(3).limit(3).forEach(s -> System.out.print(s + " ")); //"基德", "服部平次", "阿笠博士"

list.stream().limit(6).skip(3).forEach(s -> System.out.print(s + " "));	//"基德", "服部平次", "阿笠博士"

distinct 去重

distinct : 元素去重,依賴(hashCode方法和equals方法)

底層利用的是 hashSet 去重的
注:hashSet 儲存自定義物件的時候要重寫hashCode和equals方法

List<String> list1 = new ArrayList<>();
Collections.addAll(list1, "毛利蘭","毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");



list1.stream().distinct().forEach(s -> System.out.print(s + " "));
// "毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀"

concat 合併

使用Stream.concat(list1.stream(), list2.stream()) 合併兩個流

List<String> list1 = new ArrayList<>();
Collections.addAll(list1, "毛利蘭","毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");
List<String> list2 = new ArrayList<>();
Collections.addAll(list2, "赤井秀一", "雪莉");

// concat :合併a和b為一個流,如果兩個流的型別不一致,會合併到它們的父類
Stream.concat(list1.stream(), list2.stream())
    .forEach(s -> System.out.print(s + " "));
// "毛利蘭","毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀","赤井秀一", "雪莉"

Stream的終結方法

一般都是流的最後呼叫的方法

forEach

遍歷每個引數

 List<String> list = new ArrayList<>();
Collections.addAll(list, "毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");

/*匿名內部類寫法:*/
// Consumer的泛型:表示流中資料的型別
// accept() 方法中的形參s:依次表示流中的每一個資料
// 方法體:對每一個資料進行處理
list.stream().forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        // 每次消耗一個資料
        System.out.print(s + " ");
    }
});  //"毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀"


/*lambda寫法:*/
list.stream().forEach(s -> System.out.print(s + " "));
// "毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀"

count

計算元素個數

List<String> list = new ArrayList<>();
Collections.addAll(list, "毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");

long count = list.stream().count(); //count=7

toArray

收集流中的資料,放到陣列中

  • 無參的寫法:返回的是Object陣列【不推薦】

    List<String> list = new ArrayList<>();
    Collections.addAll(list, "毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");
    
    Object[] arr1 = list.stream().toArray();
    // [毛利蘭, 毛利小五郎, 柯南, 基德, 服部平次, 阿笠博士, 灰原哀]
    
  • 有參的寫法:可以指定返回陣列的型別【推薦】

    List<String> list = new ArrayList<>();
    Collections.addAll(list, "毛利蘭", "毛利小五郎", "柯南", "基德", "服部平次", "阿笠博士", "灰原哀");
    
    /*匿名內部類寫法:*/
    // IntFunction的泛型:具體型別的陣列
    // apply的形參value:流中資料的個數,要和陣列的長度保持一致
    // apply的返回值:具體型別的陣列
    
    // toArray 方法引數的作用:負責建立一個指定型別的陣列
    // toArray 方法的底層:會一次得到流裡面的每一個資料,並把資料放到陣列中去
    // tiArray 方法的返回值:是一個裝著流裡面所有資料的陣列
    String[] arr2 = list.stream().toArray(new IntFunction<String[]>() {
        @Override
        public String[] apply(int value) {
            return new String[value];
        }
    });
    System.out.println(Arrays.toString(arr2));
    //[毛利蘭, 毛利小五郎, 柯南, 基德, 服部平次, 阿笠博士, 灰原哀]
    
    /* lambda寫法:*/
    String[] arr3 = list.stream().toArray(value -> new String[value]);
    // [毛利蘭, 毛利小五郎, 柯南, 基德, 服部平次, 阿笠博士, 灰原哀]
    

collect

collect(Collector collector) : 收集流中的資料,放到集合中(List,Set,Map)

List

 List<String> list = new ArrayList<>();
Collections.addAll(list, "柯南-男-15", "灰原哀-女-14", "步美-女-13",
                   "服部平次-男-20", "阿笠博士-男-100", "毛利小五郎-男-40", "目暮警官-男-35", "高木-男-37", "琴酒-男-50");

// 需求:把所有男性放到一個List集合中
List<String> collect1 = list.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toList());
// [柯南-男-15, 服部平次-男-20, 阿笠博士-男-100, 毛利小五郎-男-40, 目暮警官-男-35, 高木-男-37, 琴酒-男-50]

Set

若資料中有重複的值,則最後toSet()的時候會刪除重複值。也是set集合的特點

 List<String> list = new ArrayList<>();
Collections.addAll(list, "柯南-男-15", "灰原哀-女-14", "步美-女-13","服部平次-男-20", "阿笠博士-男-100", "毛利小五郎-男-40", "目暮警官-男-35", "高木-男-37", "琴酒-男-50");

// 需求:把所有男性放到一個Set集合中
Set<String> collect2 = list.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toSet());
System.out.println(collect2);
// [目暮警官-男-35, 琴酒-男-50, 服部平次-男-20, 阿笠博士-男-100, 柯南-男-15, 毛利小五郎-男-40, 高木-男-37]

map

 List<String> list = new ArrayList<>();
Collections.addAll(list, "柯南-男-15", "灰原哀-女-14", "步美-女-13",
                   "服部平次-男-20", "阿笠博士-男-100", "毛利小五郎-男-40", "目暮警官-男-35", "高木-男-37", "琴酒-男-50");

// 需求:把所有的男性收集到Map集合中   鍵:姓名   值:年齡	

/**
*  toMap :
*      引數1:表示鍵的生成規則
*      引數2:表示值的生成規則
*  引數1:
*      Function:
*          泛型1:表示流中每一個資料的型別
*          泛型2:表示Map集合中鍵的資料型別
*      方法apply形參:依次表示流裡面的每一個資料
*          方法體:生成鍵的程式碼
*          返回值:已經生成的鍵
*  引數2:
*      Function:
*          泛型1:表示流中每一個資料的型別
*          泛型2:表示Map集合中值的資料型別
*      方法apply形參:依次表示流裡面的每一個資料
*          方法體:生成值的程式碼
*          返回值:已經生成的值
*
*  注意:這裡Map的鍵不能重複,否則會報錯
*/

/*匿名內部類寫法:*/
Map<String, Integer> map = list.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toMap(new Function<String, String>() {
        @Override
        public String apply(String s) {
            return s.split("-")[0];
        }
    }, new Function<String, Integer>() {
        @Override
        public Integer apply(String s) {
            return Integer.valueOf(s.split("-")[2]);
        }
    }));
System.out.println(map);
// {目暮警官=35, 服部平次=20, 毛利小五郎=40, 高木=37, 阿笠博士=100, 柯南=15, 琴酒=50}


/* lambda寫法*/
Map<String, Integer> map2 = list.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toMap(
        s -> s.split("-")[0],
        s -> Integer.valueOf(s.split("-")[2])));
System.out.println(map2);
// {目暮警官=35, 服部平次=20, 毛利小五郎=40, 高木=37, 阿笠博士=100, 柯南=15, 琴酒=50}
}

練習

  1. 將字串資料轉換成物件List

    //要求:將男生,女生名字都為三個字的封裝到detective類中
    
    List<String> manList = new ArrayList<>();
    List<String> womanList = new ArrayList<>();
    
    Collections.addAll(manList, "柯南-男-15", "服部平次-男-20", "阿笠博士-男-100", "毛利小五郎-男-40", "琴酒-男-50");
    Collections.addAll(womanList,  "灰原哀-女-14", "步美-女-13",  "雪莉-女-50");
    
    Stream<String> manStream = manList.stream().filter(s -> s.split("-")[0].length() > 2);
    Stream<String> womanStream = womanList.stream().filter(s -> s.split("-")[0].length() > 2);
    
    List<Detective> collect = Stream.concat(manStream, womanStream)
        .map(s -> new Detective(s.split("-")[0], Integer.valueOf(s.split("-")[2])))
        .collect(Collectors.toList());
    //[Detective(name=服部平次, age=20), Detective(name=阿笠博士, age=100), Detective(name=毛利小五郎, age=40), Detective(name=灰原哀, age=14)]
    

常用API

靜態的方法,都是直接類名+方法名 的方式呼叫的

Math

public static int abs(int a);                   //獲取引數絕對值
public static double ceil(double a);            //向上取整
public static double floor(double a);           //向下取整
public static int round(float a);               //四捨五入
public static int max(int a,int b);             //獲取兩個int值中的較大值
public static double pow(double a,double b);    //返回a的b次冪(一般是處理大於1的冪)
public static double sqrt(double a);            //返回a的平方根
public static double cbrt(double a);            //返回a的立方根
public static double random();                  //返回值為double的隨機值,範圍[0.0,1.0)

注:abs有bug。int型別的a取值範圍只能是 -2147483648 ~ 2147483647 超過就取不到絕對值。此時可以使用Math.absExact(int a); 這個方法遇到不在範圍內的數會報錯,但這個方法只在jdk15後才有

System

public static void exit(int status);                                        //終止當前執行的Java虛擬機器
public static long currentTimeMillis();                                     //返回當前系統的時間(毫秒為單位)
public static void arraycopy(資料來源陣列,初始索引,目的地陣列,起始索引,複製個數);   //陣列複製
  1. exit( int status ); 狀態碼0表示虛擬機器正常停止,非0表示虛擬機器非正常停止

    底層呼叫的是Runtime的exit方法——停止虛擬機器

  2. 陣列複製注意點:

    • 資料來源和目的地陣列都是基本資料型別,那麼兩者的型別必須保持一致,否則報錯
    • 複製超過陣列長度也會報錯
    • 若資料來源和目的地陣列都是引用資料型別(物件型別等),那麼子類型別可以賦值給父類型別

RunTime

該類的方法不是static,需要建立物件才能使用。但這裡的物件不能手動建立而是需要呼叫getRuntime方法建立,這是餓漢式的設計模式,也保證了該類物件只有一個,不能有多個。(保證虛擬機器在當前電腦中的唯一性)

public static Runtime getRuntime(); //當前系統的執行環境物件
public void exit(int status);       //停止虛擬機器【狀態碼0是正常停止,非0是不正常停止】
public int availableProcessors();   //獲得CPU的執行緒數
public long maxMemory();            //JVM能從系統中獲取總記憶體大小(單位byte)
public long totalMemory();          //JVM已經從系統中獲取總記憶體大小(單位byte)
public long freeMemory();           //JVM剩餘記憶體大小(單位byte)
public Process exec(String command);//執行cmd命令【這裡有bug,執行不了dir。因為本身在的目錄條件受限】	
  1. 呼叫方法:Runtime.getRuntime().exit(0);
  2. Runtime.getRuntime().exec("shutdown -s -t 1200"); 是在1200秒後自動關機
    • shutdown -s:預設在1分鐘後關機
    • shutdown -s -t xx:在xx秒後關機
    • shutdown -a:取消關機操作
    • shutdown -r:關機並重啟

Object

public String toString();           //返回物件的Hash地址
public boolean equals(Object obj);  //比較兩物件地址值是否相等
protected Object clone(int a);      //物件克隆【淺克隆】
  1. 所有類都預設繼承Object,Object的toString方法預設是返回物件的Hash地址。我們平常使用toString方法列印物件or字串是因為toString方法被重寫了,子類可以重寫父類的方法。

  2. 我們平常使用的equals是不一樣的。每種類裡的equals的底層邏輯可能不同【某大廠面試題】

    • String中的equals方法

      String str="abc";
      StringBuilder sb =new StringBuilder("abc");
      
      System.out.println(str.equals(sb)); //false
      
      原因:這裡呼叫的是String的equals,其原始碼是先判斷是不是相同地址,是的話直接返回true。不相同地址再看比較物件是不是字串,若不是直接false,是字串才會繼續比較兩字串的值是否相同。
      
    • StringBuilder中的equals方法

      String str="abc";
      StringBuilder sb =new StringBuilder("abc");
      
      System.out.println(sb.equals(str)); //false
      原因:StringBuilder沒有重寫equals方法,所以是用Object的equals方法,只比較物件地址值是否相同
      
  3. clone的使用

    看原始碼可知Object中的clone方法是protected的

    • 重寫Object中的clone方法(這裡其實在實體類中繼承父類的clone即可 super.clone() )
    • 讓實體類實現Cloneable介面
    • 在業務層建立原物件並呼叫即可

Objects

public static boolean equals(Object a,Object b);    //先做非空判斷,在呼叫重寫的equals方法
public static boolean isNull(Object obj);           //判斷物件是否為null
public static boolean nonNull(Object obj);          //判斷物件是否為非null
  1. equlas這個工具類方法就避免了比較時有null報錯的問題。先判斷非空,如何在呼叫equals方法,如a中重寫了equals方法則優先會使用重寫後的方法

BigInteger

方法都是非靜態的,所以呼叫方法需要建立物件

優點:可以對很大的整數進行運算

能儲存十分大的整數的原理:將很大的數字進行了拆分裝進陣列,第一組只有一個數代表符合位,後面每32位分成一組來計算

構造方法:

public BigInteger(int num, Random rnd) 		//獲取隨機大整數,範圍:[0~ 2的num次方-11
public BigInteger(String val) 				//獲取指定的大整數	【val必須是整數】
public BigInteger(String val, int radix)	//獲取指定進位制的大整	【radix必須和val進位制相同,否則會報錯】
public static BigInteger valueOf(long val)	//靜態方法獲取BigInteger的物件,內部有最佳化
  1. 建立物件的兩種方式

    • 數較小的時候(long型別內)

      原始碼中將 -16~16最佳化了,這個區間的數被提前建立了物件裝到了陣列中,所以多次對這區間內同一個數建立物件,實際是取陣列內的同一個物件

      //靜態方法建立物件
      BigInteger bigInteger = BigInteger.valueOf(123456);
      
    • 數較大的時候(大於long型別)

      //構造方法建立物件
      BigInteger b = new BigInteger("123");	//這裡一個引數的時候只能是字串
      
  2. 建立一個指定進位制的大整數(將一個指定進位制的字串轉成十進位制的大整數)

     BigInteger b2 = new BigInteger("100101011", 2); //這裡b2=299 是一個整數
    

一般方法:

public BigInteger add(BigInteger val) 					//加法
public BigInteger subtract(BigInteger val) 				//減法
public BigInteger multiply(BigInteger val) 				//乘法
public BigInteger divide(BigInteger val) 				//除法,獲取商
public BigInteger[] divideAndRemainder(BigInteger val) 	//除法,獲取商和餘數【陣列 0位為商,1位為餘數】
public boolean equals(Object x) 						//比較是否相同【由於重寫了equls方法,所以這裡是比較值】
public BigInteger pow(int exponent) 					//次冪 
public BigInteger max/min(BigInteger val)	 			//返回較大/小值物件【這裡返回的是較大/小的那個物件的引用】
public int intValue(BigInteger val) 					//轉為int型別整數,超出範圍資料有誤
public long longValue(BigInteger val) 					//轉為long型別整數,超出範圍資料有誤
public double doubleValue(BigInteger val) 					//轉為double型別整數,超出範圍資料有誤
  1. BigInteger物件一旦建立,內部的資料不能發生改變

    每次的運算都會返回一個新的物件

    BigInteger bd9 =BigInteger.valueOf(1);
    BigInteger bd10 =BigInteger.valueOf(2);
    //此時,不會修改參與計算的BigInteger物件中的借,而是產生了一個新的BigInteger物件記錄
    BigInteger result=bd9.add(bd10); //result=3
    
  2. 除法得到商和餘數

    BigInteger bd1 = BigInteger.valueOf(10);
    BigInteger bd2 = BigInteger.valueOf(3);
    BigInteger[] arr = bd1.divideAndRemainder(bd2);
    System.out.println(arr[0]);	//3
    System.out.println(arr[1]);	//1
    

BigDecimal

方法都是非靜態的,所以呼叫方法需要建立物件

優點:可以儲存十分大的浮點數可以精準處理浮點數之間的運算

能儲存十分大的浮點數的原理:將浮點數都轉成字串後每個符號和數字都拆分出來,陣列儲存每個字元對應的ascii碼

構造方法:

//構造方法獲取BigDecimal物件
public BigDecimal(double val) public BigDecimal(string val)
//靜態方法獲取BigDecimal物件
public static BigDecimal valuef(double val)
  1. 建立物件的三種方法:

    • 當數較小的時(double內) 以及 想要精準計算浮點數的時:使用靜態方法【推薦】

      如果我們傳遞的是0~10之間的整數,包含0,包含10,那麼方法會返回已經建立好的物件,不會重新new

      BigDecimal decimal = BigDecimal.valueOf(4869.00);
      
    • 當數較大時:使用字串型的構造方法【推薦】

      BigDecimal decimal1 = new BigDecimal("123.00");
      
    • 當數較大時:使用浮點數型的構造方法【不推薦】

      因為用浮點數建立物件會導致建立的數有誤差。而前面靜態方法不會是因為做了處理轉成了字串

      BigDecimal decimal2 = new BigDecimal(123.00);
      

一般方法:

public BigDecimal add(BigDecimal val)						//加法
public BigDecimal subtract(BigDecimal val) 					//減法
public BigDecimal multiply(BigDecimal val) 					//乘法
public BigDecimal divide(BigDecimal val) 					//除法
public BigDecimal divide(BigDecimal val,精確幾位,舍入模式)	 //除法
  1. BigDecimal物件一旦建立,內部資料不能改變

    BigDecimal bd1 = BigDecimal.valueOf(10.0);
    BigDecimal bd2 = BigDecimal.valueOf(2.0);
    BigDecimal bd3 = bd1.add(bd2); //bd3=12.00
    
  2. 除法,一般都需要指定精確幾位和舍入模式

    //RoundingMode.HALF_UP是指四捨五入 (其他的舍入模式需要去看這個類RoundingMode裡的列舉)
    BigDecimal bd6 = bd1.divide(bd2, 2, RoundingMode.HALF_UP); 	//bd6=3.3  
    

Optional類

Optional類是Java8為了解決null值判斷問題,使用Optional類可以避免顯式的null值判斷(null的防禦性檢查),避免null導致的NPE(NullPointerException)。總而言之,就是對控制的一個判斷,為了避免空指標異常。

工作常用方式:

List<String> list = null;	//外界得到的集合
List<String> newList = Optional.ofNullable(list).orElse(xxxx);	//如果list非空則返回引數list,如果list是空則返回後面引數xxxx

Arrays工具類

Arrays是對陣列操作的工具類。

基本操作:

Arrays.toString(arr);	//將陣列轉為字串輸出

Arrays.sort(arr);	//將陣列正序排列(從小到大),改變本身不返回陣列
Arrays.sort(arr,Collections.reverseOrder()); 	//將陣列逆序排列,改變本身不返回陣列
    
Arrays.equals(arr01,arr02);		//比較兩陣列值是否相同

Arrays.binarySearch(arr,key);	//用二分搜尋尋找arr陣列中是否有key值,有則返回下標

arr02=Arrays.copyOf(arr01,arr01.length);	//從0到length-1複製arr01陣列,返回新的陣列

一般陣列轉集合類:1. 先轉為固定長度的ListArrays.asList(array) 2. 再轉為集合類

Integer[] array = {1, 5, 2, 4, 7, 3, 6};
List<Integer> list = new ArrayList<>(Arrays.asList(array));

Collections工具類

Collections是對集合類操作的工具類

基本操作:

排序操作
1.		reverse(List):反轉List中元素的順序
2.		shuffle(List):對List集合元素進行隨機排序
3.		sort(List):根據元素的自然順序對指定List集合元素按升序排序
4.		sort(List,Comparator):根據指定的Comparator產生的順序對List集合元素進行排序

替換 和 查詢操作
5.		swap(List, int ,int ):將指定List集合中的 i 處元素 和 j 處元素進行交換
6.		Object max(Collection):根據元素的自然順序,返回給定集合中的最大元素
7.      Object max(Collection, Comparator):根據Comparator指定的順序,返回給集合中的最大元素
8.		Object min(Collection):根據元素的自然順序,返回給定集合中的最小元素
9.		Object min(Collection, Comparator):根據Comparator指定的順序,返回給集合中的最大元素
10.		int frequency(Collection,Object):返回指定集合中指定元素的出現次數
11.		void copy(List dest,List src):將src中的內容複製到dest中
        注意複製的目標集合的長度必須大於源集合,否則會丟擲空指標異常
    
12.     boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替換List物件的所有舊值

使用方式:

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    max = Collections.max(list);
    miin = Collections.min(list);
}

時間API

以下都是JDK1.8新增的時間方法 優點:使用更加方便、解決了多執行緒帶來的執行緒安全問題

ZoneId 時區(重點

static Set<string> getAvailableZoneIds() 	//獲取Java中支援的所有時區
static ZoneId systemDefault() 				//獲取系統預設時區
static Zoneld of(string zoneld) 			//獲取一個指定時區
  1. 獲取所有的時區名稱

    Set<String> zoneIds = ZoneId.getAvailableZoneIds(); //zoneIds集合中有六十個時區名,格式:州/城市
    System.out.println(zoneIds.size());//600
    System.out.println(zoneIds);// Asia/Shanghai
    
  2. 獲取當前系統的預設時區

    ZoneId zoneId = ZoneId.systemDefault(); //zoneId= Asia/Shanghai
    
  3. 獲取指定的時區【不知道指定時區的寫法可以透過前面方法獲取所有,然後找需要的時區】

    ZoneId zoneId1 = ZoneId.of("Asia/Pontianak");//zoneId= Asia/Pontianak 
    

Instant 時間戳

static Instant now() 					//獲取當前時間的Instant物件(標準時間)
static Instant ofXxxx(long epochMilli) 	//根據(秒/毫秒/納秒)獲取Instant物件	
ZonedDateTime atZone(ZoneIdzone) 		//指定時區
boolean isxxx(Instant otherInstant) 	//判斷系列的方法
Instant minusXxx(long millisToSubtract) //減少時間系列的方法
Instant plusXxx(long millisToSubtract) 	//增加時間系列的方法
  1. 根據(秒/毫秒/納秒)獲取Instant物件【瞭解】

    Instant instant1 = Instant.ofEpochMilli(0L);
    System.out.println(instant1);//1970-01-01T00:00:00z
    
    Instant instant2 = Instant.ofEpochSecond(1L);
    System.out.println(instant2);//1970-01-01T00:00:01Z
    
    Instant instant3 = Instant.ofEpochSecond(1L, 1000000000L);
    System.out.println(instant3);//1970-01-01T00:00:027
    
  2. 獲取指定時區的時間戳【瞭解】

    ZonedDateTime time = Instant.now().atZone(ZoneId.of("Asia/Shanghai")); //time就是現在上海時區的時間
    
  3. 判斷時間前後【瞭解,主要用後面的LocalDateTime的方法】

    Instant instant4=Instant.ofEpochMilli(0L);
    Instant instant5 =Instant.ofEpochMilli(1000L);
    
    //isBefore:判斷呼叫者代表的時間是否在參數列示時間的前面
    boolean result1=instant4.isBefore(instant5);
    System.out.println(result1);//true
    
    //isAfter:判斷呼叫者代表的時間是否在參數列示時間的後面
    boolean result2 = instant4.isAfter(instant5);
    System.out.println(result2);//false
    

ZonedDateTime

static ZonedDateTime now() 					//獲取當前時間的ZonedDateTime物件
static ZonedDateTime ofXxxx(。。。) 			//獲取指定時間的ZonedDateTime物件
ZonedDateTime withXxx						//(時間) 修改時間系列的方法
ZonedDateTime minusXxx(時間)			 		//減少時間系列的方法
ZonedDateTime plusXxx(時間) 					//增加時間系列的方法

DateTimeFormatter 格式化(重點

static DateTimeFormatter ofPattern		//(格式) 獲取格式物件
String format							//(時間物件) 按照指定方式格式化
  1. 設定時間格式【重點】

    //y:年份 M:月份 d:幾號   H:小時  m:分鐘  s:秒   E:星期幾   a:上午or下午 【-和: 符號可以自由更改】
    DateTimeFormatter dtf1=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EE a");	
    
  2. 格式化時間

     //獲取時間物件
    ZonedDateTime time = Instant.now().atZone(ZoneId.of("Asia/Shanghai"));
    // 解析/格式化器
    DateTimeFormatter dtf1=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm;ss EE a");
    // 格式化
    System.out.println(dtf1.format(time));
    

LocalDate

只能獲取年月日,並且只能對年月日操作 (由於使用較少就不描述了)

LocalTime

只能獲取時分秒毫秒,並且只能對時分秒毫秒操作 (由於使用較少就不描述了)

LocalDateTime(重點

構造方法

//1.建立當前時間物件
LocalDateTime localDateTime = LocalDateTime.now(); //這裡可以放引數指定時區
//2.建立指定時間的物件
LocalDateTime localDateTime = LocalDateTime.of(2022, 10, 06, 18, 00);//指定時間可以具體到納秒

一般方法

LocalDateTime localDateTime = LocalDateTime.now();

localDateTime//今天的日期
localDateTime.getYear()//年
localDateTime.getMonthValue()//月
localDateTime.getDayOfMonth()//日
localDateTime.getHour()//時
localDateTime.getMinute()//分
localDateTime.getSecond()//秒
localDateTime.getNano()//納秒

localDateTime.getDayOfYear()				//當年的第幾天

localDateTime.getDayOfWeek()				//返回英文寫法的星期幾
localDateTime.getDayOfWeek().getValue()		//返回數字星期幾
    
localDateTime.getMonth()					//返回英文寫法的月份
localDateTime.getMonth().getValue()			//返回數字的第幾月
localDateTime.getDayOfMonth()				//返回該時間是當月第幾天
    
LocalDate ld = localDateTime.toLocalDate();	//轉成LocalDate
LocalTime lt = localDateTime.toLocalTime();	//轉成LocalTime

//指定日期格式並設定到當前時間
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EE a");
localDateTime.format(dateTimeFormatter);	

//判斷時間前後
localDateTime.isAfter(time01); 				//返回localDateTime是否在time01後面
localDateTime.isBefore(time01); 			//返回localDateTime是否在time01前面

//在時間物件基礎上修改某個年份/月份/..  withxxx就是修改xxx的資料 【返回一個新的物件(因為時間是不可變物件)】
LocalDateTime time1 = localDateTime.withYear(2000);

//在時間物件基礎上增加一段時間  plusxxx就是增加xxx的資料 【返回一個新的物件(因為時間是不可變物件)】
LocalDateTime time02 = localDateTime.plusDays(100);

//在時間物件基礎上增加一段時間  minxxx就是較少xxx的資料 【返回一個新的物件(因為時間是不可變物件)】
LocalDateTime time03 = localDateTime.minusDays(99);

Period

主要作用是時間作差,這個只能對年月日作差 (由於使用較少就不描述了)

Duration

主要作用對時間作差,這個只能對時分秒毫秒作差(由於使用較少就不描述了)

ChronoUnit 時間作差(重點

// 當前時間
LocalDateTime today = LocalDateTime.now();
System.out.println(today);
// 生日時間
LocalDateTime birthDate = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
System.out.println(birthDate);

System.out.println("相差的年數:" + ChronoUnit.YEARS.between(birthDate, today));
System.out.println("相差的月數:" + ChronoUnit.MONTHS.between(birthDate, today));
System.out.println("相差的週數:" + ChronoUnit.WEEKS.between(birthDate, today));
System.out.println("相差的天數:" + ChronoUnit.DAYS.between(birthDate, today));
System.out.println("相差的時數:" + ChronoUnit.HOURS.between(birthDate, today));
System.out.println("相差的分數:" + ChronoUnit.MINUTES.between(birthDate, today));
System.out.println("相差的秒數:" + ChronoUnit.SECONDS.between(birthDate, today));
System.out.println("相差的毫秒數:" + ChronoUnit.MILLIS.between(birthDate, today));
System.out.println("相差的微秒數:" + ChronoUnit.MICROS.between(birthDate, today));
System.out.println("相差的納秒數:" + ChronoUnit.NANOS.between(birthDate, today));
System.out.println("相差的半天數:" + ChronoUnit.HALF_DAYS.between(birthDate, today));
System.out.println("相差的十年數:" + ChronoUnit.DECADES.between(birthDate, today));
System.out.println("相差的世紀(百年)數:" + ChronoUnit.CENTURIES.between(birthDate, today));
System.out.println("相差的千年數:" + ChronoUnit.MILLENNIA.between(birthDate, today));
System.out.println("相差的紀元數:" + ChronoUnit.ERAS.between(birthDate, today));

包裝類

這裡補充一個知識點:自動裝箱、自動拆箱(jdk5後的新功能)

沒有自動裝/拆箱時,包裝類運算:

Integer i1 = new Integer(1);
Integer i2 = new Integer(2);
int result = i1.intValue() + i2.intValue();
Integer i3 = new Integer(result);
System.out.println(i3);

有自動裝/拆箱時,包裝類運算:

Integer i1 = new Integer(1);
Integer i2 = new Integer(2);
Integer i3 = i1+i2;
System.out.println(i3);

自動裝箱:基本資料型別可以直接被賦值到包裝類上

Integer i = 10;	//基本資料型別被自動裝箱成了包裝類

自動拆箱:

Integer i2 = new Integer(10);
//自動拆箱的動作。包裝類被自動拆箱成了基本資料型別
int i = i2;

java有八大基本包裝類: Byte、Character、Double、Integer、Float、Long、Short、Boolean

因為每個包裝類都相似,所以這裡以Integer為例

Integer

構造方法

//new的構造方法已經過時,在jdk5之後有了自動拆箱和裝箱
public Integer(int value) 							//根據傳遞的整數建立一個Integer物件  
public Integer(String s) 							//根據傳遞的字串建立一個Integer物件
    
//valueOf底層是對-127~128進行了最佳化,這個區間的數被提前建立了物件裝到了陣列中,所以多次對這區間內同一個數建立物件,實際是取陣列內的同一個物件【原因:這個範圍內的數字使用率高,所以不重複建立物件浪費記憶體】
public static Integer valueOf(int i) 				//根據傳遞的整數建立一個Integer物件
public static Integer valueof(String s) 			//根據傳遞的字串建立一個Integer物件
public static Integer valueof(String s, int radix) 	//根據傳遞的字串和進位制建立一個Integer物件

成員方法

public static string tobinarystring(int i) 			//得到二進位制
public static string tooctalstring(int i) 			//得到八進位制
public static string toHexstring(int i) 			//得到十六進位制
public static int parseInt(string s) 				//將字串型別的整數轉成int型別的整數
  1. 將一個int型別數字轉成指定的進位制,返回字串

    String str1 = Integer.toBinaryString(100);	//轉成二進位制字串
    String str2 = Integer.toOctalString(100);	//轉成八進位制字串
    String str3 = Integer.toHexString(100);		//轉成十六進位制字串
    
  2. 字串數字裝成int型別的整數【8種包裝類當中,除了Character都有對應的parseXxx的方法,進行型別轉換】

    細節:引數只能是數字字串,否則報錯

    int i = Integer.parseInt("123");
    

JDK1.8新增方法

集合類的compute

用於替換更新資料

public static void main(String[] args) {
    Map<Integer, String> map = new HashMap<>();
    map.put(1, "A");
    map.put(2, "B");
    
    //不建議
    map.compute(1, (k, v) -> {   //compute會將指定Key(也就是1)的值進行重新計算,若Key不存在,v會返回null
        return v+"M";     //這裡返回原來的value+M
    });
    
    //建議使用
  	map.computeIfPresent(1, (k, v) -> {   //當Key(也就是1)存在時存在則計算並賦予新的值
      return v+"M";     //這裡返回原來的value+M
    });
    System.out.println(map);
    
    //建議使用
    map.computeIfAbsent(0, (k) -> {   //若key(也就是0)不存在則計算並插入新的值
        return "M";     //這裡返回M
    });
}

集合類的merge

用於處理資料

public static void main(String[] args) {
    List<Student> students = Arrays.asList(
            new Student("yoni", "English", 80),
            new Student("yoni", "Chiness", 98),
            new Student("yoni", "Math", 95),
            new Student("taohai.wang", "English", 50),
            new Student("taohai.wang", "Chiness", 72),
            new Student("taohai.wang", "Math", 41),
            new Student("Seely", "English", 88),
            new Student("Seely", "Chiness", 89),
            new Student("Seely", "Math", 92)
    );
    Map<String, Integer> scoreMap = new HashMap<>();
    //merge()是合併,用於key相同但value不同的資料處理,最後一個引數就是資料處理的具體方式(對value的操作)
    students.forEach(student -> scoreMap.merge(student.getName(), student.getScore(), (a,b)->a+b));
    scoreMap.forEach((k, v) -> System.out.println("key:" + k + "總分" + "value:" + v));
}

相關文章