主頁 > 後端開發 > JavaSE基礎 (全網最全知識點)

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

2023-02-28 07:00:09 後端開發

背景介紹

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 = https://www.cnblogs.com/buchizicai/p/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 = https://www.cnblogs.com/buchizicai/p/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 = https://www.cnblogs.com/buchizicai/p/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 = https://www.cnblogs.com/buchizicai/p/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));
}

本文來自博客園,作者:不吃紫菜,遵循CC 4.0 BY-SA著作權協議,

轉載請附上原文出處鏈接:https://www.cnblogs.com/buchizicai/p/17097953.html及本宣告;

本文著作權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/545161.html

標籤:Java

上一篇:C語言程式設計

下一篇:Redis高頻面試題總結

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more