韓順平Java(持續更新中)


原創上課筆記,轉載請注明出處

第一章 面向對象編程(中級部分) PDF為主

1.1 IDEA

  1. 刪除當前行,ctrl+y
  2. 復制當前行,ctrl+d
  3. 補全代碼,alt+/
  4. 添加或者取消注釋,ctrl+/
  5. 導入該行需要的類,alt+enter
  6. 快速格式化代碼,ctrl+ALT+L
  7. 快速運行程序,alt+r(自己設置)
  8. 生成構造器等,alt+insert
  9. 查看一個類的層級關系,ctrl+H,繼承有用(光標放在類名上)
  10. 快速定位某個方法的位置,ctrl+B(ctrl+鼠標點擊)
  11. 自動分配變量,main方法中,.var,例如new Scanner(System.in).var

查看快捷鍵模板:Live Templates (例如,fori)

1.2 Object類詳解(equals、==、hashCode等)

所有類都實現了Object類,都能使用Object類的方法。

1.2.1 ==運算符

基本類型—>判斷值是否相等

引用類型—>判斷地址是否相等

指向同一個地址,結果為true

1.2.2 equals()方法

1.2.2.1 基本介紹

Object的equals()一目了然,==運算符,用來判斷地址是否相等

而String等類的equals()被重寫了,用來判斷內容是否相等(根據需求,判斷內容相等的標准也是可能會有所改變的)

如何重寫equals方法:

        Person person1 = new Person("jack", 10, '男');
        Person person2 = new Person("jack", 20, '男');
        System.out.println(person1.equals(person2));//假,如果沒有重寫Person類的equals方法,這里的equals方法調用的Object的(即,判斷的是地址)

示例代碼:

    //重寫Object 的 equals方法
    public boolean equals(Object obj) {
        //判斷如果比較的兩個對象是同一個對象,則直接返回true
        if(this == obj) {
            return true;
        }
        //類型判斷
        if(obj instanceof  Person) {//是Person,我們才比較

            //進行 向下轉型, 因為我需要得到obj的 各個屬性
            Person p = (Person)obj;
            return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
        }
        //如果不是Person ,則直接返回false
        return false;

    }
1.2.2.2 課堂練習

第三個輸出:因為Person並沒有重寫equals,所以這里調用的equals方法是Object的,判斷地址是否相同的,而這兩個新的對象肯定不相等,所以返回false

這道題需要注意的是,基本數據類型的==運算符是判斷內容的

1.2.3 hashCode()方法

1.2.4 toString()方法

全類名:包名+類名

        /**Object的toString() 源碼
        (1)getClass().getName() 類的全類名(包名+類名 )
        (2)Integer.toHexString(hashCode()) 將對象的hashCode值轉成16進制字符串
        */
        public String toString() {
            return getClass().getName() + "@" + Integer.toHexString(hashCode());
        }

1.2.5 finalize()方法

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

        Car bmw = new Car("寶馬");
        //這時 car對象就是一個垃圾,垃圾回收器就會回收(銷毀)對象, 在銷毀對象前,會調用該對象的finalize方法
        //,程序員就可以在 finalize中,寫自己的業務邏輯代碼(比如釋放資源:數據庫連接,或者打開文件..)
        //,如果程序員不重寫 finalize,那么就會調用 Object類的 finalize, 即默認處理
        //,如果程序員重寫了finalize, 就可以實現自己的邏輯
        bmw = null;
        System.gc();//主動調用垃圾回收器

        System.out.println("程序退出了....");
    }
}
class Car {
    private String name;
    //屬性, 資源。。
    public Car(String name) {
        this.name = name;
    }
    //重寫finalize
    @Override
    protected void finalize() throws Throwable {
        System.out.println("我們銷毀 汽車" + name );
        System.out.println("釋放了某些資源...");
    }
}

1.3 面向對象的三大特征(封裝、繼承、多態)

面向對象的三大特征:封裝、繼承、多態

1.3.1 封裝

好處:隱藏實現的細節可以對數據進行驗證

實現的步驟:

封裝與構造器:

/有三個屬性的構造器
public Person(String name, int age, double salary) {
     // this.name = name;
     // this.age = age;
     // this.salary = salary;

     //我們可以將 set 方法寫在構造器中,這樣仍然可以驗證
        setName(name);
        setAge(age);
        setSalary(salary);
}

1.3.2 繼承

提升代碼的復用性、便於代碼維護和擴展

  1. 注意:子類構造器內部有個默認隱藏的super()方法,調用父類的構造器。(子類構造器中不顯示super指明父類構造器的話,就是默認調用父類無參構造器)。

  2. 當定義了一個有參構造器且沒有顯式定義無參構造器的話,那么默認的無參構造器就會被有參構造器覆蓋。子類此時必須在所有構造器中用super指明調用的哪個父類構造器。

  3. super在普通方法也能用,調用父類對應的方法

  4. 這個細節一定要注意

    1. 從當前類往上一直追溯到Object類,然后從Object類一直調用構造器方法到當前類

繼承的本質分析(重要)

public class ExtendsTheory {
    public static void main(String[] args) {
        Son son = new Son();//內存的布局
        //?-> 這時請大家注意,要按照查找關系來返回信息(就近原則,自己沒有就找父親,父親沒有就找爺爺)
        //(1) 首先看子類是否有該屬性
        //(2) 如果子類有這個屬性,並且可以訪問,則返回信息
        //(3) 如果子類沒有這個屬性,就看父類有沒有這個屬性(如果父類有該屬性,並且可以訪問,就返回信息..)
        //(4) 如果父類沒有就按照(3)的規則,繼續找上級父類,直到Object...
        System.out.println(son.name);//返回就是大頭兒子
        //System.out.println(son.age);//返回的就是39
        //System.out.println(son.getAge());//返回的就是39
        System.out.println(son.hobby);//返回的就是旅游
    }
}
class GrandPa { //爺類
    String name = "大頭爺爺";
    String hobby = "旅游";
}
class Father extends GrandPa {//父類
    String name = "大頭爸爸";
    private int age = 39;

    public int getAge() {
        return age;
    }
}
class Son extends Father { //子類
    String name = "大頭兒子";
}

注意一點:當打印son.age的時候,此時Son類沒有age這個屬性,於是去Father中尋找,但是這個age是私有的,此時編譯器就會報錯,若要訪問,就要調用getAge()方法。(如果Father類中也沒有age,就去GranPa中尋找)

此外,在現在這個代碼情況下,想要訪問GrandPa中的age屬性(假設爺爺類中有age這個屬性),需要在爺爺中創建一個不重名的get方法,並調用。

課堂練習:

這個題需要提醒一下,this會調用B的構造器,this(“abc")會調用B的有參構造器,而且this調用構造器,會有super(),如果再寫一個super就沖突(這也是為什么super和this不能共存,指訪問構造器)

注意執行順序,往上找super

1.3.3 super關鍵字

super使用細節:

public class B extends A {

    public int n1 = 888;

    //編寫測試方法
    public void test() {
        //super的訪問不限於直接父類,如果爺爺類和本類中有同名的成員,也可以使用super去訪問爺爺類的成員;
        // 如果多個基類(上級類)中都有同名的成員,使用super訪問遵循就近原則。A->B->C

        System.out.println("super.n1=" + super.n1);
        super.cal();
    }

    //訪問父類的屬性 , 但不能訪問父類的private屬性 [案例]super.屬性名
    public void hi() {
        System.out.println(super.n1 + " " + super.n2 + " " + super.n3 );
    }
    public void cal() {
        System.out.println("B類的cal() 方法...");
    }
    public void sum() {
        System.out.println("B類的sum()");
        //希望調用父類-A 的cal方法
        //這時,因為子類B沒有cal方法,因此我可以使用下面三種方式

        //找cal方法時(cal() 和 this.cal()),順序是:
        // (1)先找本類,如果有,則調用
        // (2)如果沒有,則找父類(如果有,並可以調用,則調用)
        // (3)如果父類沒有,則繼續找父類的父類,整個規則,就是一樣的,直到 Object類
        // 提示:如果查找方法的過程中,找到了,但是不能訪問(比如私有方法), 則報錯, cannot access
        //      如果查找方法的過程中,沒有找到,則提示方法不存在
        //cal();
        this.cal(); //等價 cal

        //找cal方法(super.call()) 的順序是直接查找父類,其他的規則一樣
        //super.cal();

        //演示訪問屬性的規則
        //n1 和 this.n1 查找的規則是
        //(1) 先找本類,如果有,則調用
        //(2) 如果沒有,則找父類(如果有,並可以調用,則調用)
        //(3) 如果父類沒有,則繼續找父類的父類,整個規則,就是一樣的,直到 Object類
        // 提示:如果查找屬性的過程中,找到了,但是不能訪問, 則報錯, cannot access
        //      如果查找屬性的過程中,沒有找到,則提示屬性不存在
        System.out.println(n1);
        System.out.println(this.n1);

        //找n1 (super.n1) 的順序是直接查找父類屬性,其他的規則一樣
        System.out.println(super.n1);

    }
    //訪問父類的方法,不能訪問父類的private方法 super.方法名(參數列表);
    public void ok() {
        super.test100();
        super.test200();
        super.test300();
        //super.test400();//不能訪問父類private方法
    }
    //訪問父類的構造器(這點前面用過):super(參數列表);只能放在構造器的第一句,只能出現一句!
    public  B() {
        //super();
        //super("jack", 10);
        super("jack");
    }
}

super和this的比較:

1.3.4 重寫/覆蓋

重寫(override)與重載(overload)的比較:

1.3.5 多態*

1.3.5.1 引出問題

待解決的問題:當對象不同時,需要調用的方法不一樣,如果沒有多態的話,我們可能就會根據不同對象的種類數重載同一個方法很多次,這樣代碼的復用性不高,不利於代碼維護。

1.3.5.2 基本介紹

多態的具體體現:方法的多態和對象的多態

因為編譯類型的對象可以重新指向其他的運行類型,所以運行類型可以改變。運行類型是Java實際執行時的類型,編譯類型是編譯器識別的。

P308 一段代碼的比較,使用了多態前后:animal是dog、cat的父類

    //使用多態機制,可以統一的管理主人喂食的問題
    //animal 編譯類型是Animal,可以指向(接收) Animal子類的對象
    //food 編譯類型是Food ,可以指向(接收) Food子類的對象
    public void feed(Animal animal, Food food) {
        System.out.println("主人 " + name + " 給 " + animal.getName() + " 吃 " + food.getName());
    }

    //主人給小狗 喂食 骨頭
//    public void feed(Dog dog, Bone bone) {
//        System.out.println("主人 " + name + " 給 " + dog.getName() + " 吃 " + bone.getName());
//    }
//    //主人給 小貓喂 黃花魚
//    public void feed(Cat cat, Fish fish) {
//        System.out.println("主人 " + name + " 給 " + cat.getName() + " 吃 " + fish.getName());
//    }

    //如果動物很多,食物很多
    //===> feed 方法很多,不利於管理和維護
    //Pig --> Rice
    //Tiger ---> meat ...
    //...

向上轉型:(轉型向上還是向下,針對的是等號=右邊的類型,相對於它是向上轉父類,還是向下轉子類)

父類的引用指向了子類的對象

        //向上轉型: 父類的引用指向了子類的對象
        //語法:父類類型引用名 = new 子類類型();
        Animal animal = new Cat();
        Object obj = new Cat();//可以嗎? 可以 Object 也是 Cat的父類

        //向上轉型調用方法的規則如下:
        //(1)可以調用父類中的所有成員(需遵守訪問權限)
        //(2)但是不能調用子類的特有的成員
        //(#)因為在編譯階段,能調用哪些成員,是由編譯類型來決定的
        //animal.catchMouse();錯誤
        //(4)最終運行效果看子類(運行類型)的具體實現, 即調用方法時,按照從子類(運行類型)開始查找方法
        //,然后調用,規則我前面我們講的方法調用規則一致。
        animal.eat();//貓吃魚..
        animal.run();//跑
        animal.show();//hello,你好
        animal.sleep();//睡

向下轉型:

        //老師希望,可以調用Cat的 catchMouse方法
        //多態的向下轉型
        //(1)語法:子類類型 引用名 =(子類類型)父類引用;
        //問一個問題? cat 的編譯類型 Cat,運行類型是 Cat
        Cat cat = (Cat) animal;
        cat.catchMouse();//貓抓老鼠
        //(2)要求父類的引用必須指向的是當前目標類型的對象
        Dog dog = (Dog) animal; //可以嗎?

        System.out.println("ok~~");
1.3.5.3 多態的細節
  1. 屬性重寫問題

    屬性的調用是看編譯類型,而方法是看運行類型

    屬性沒有重寫之說!屬性的值看編譯類型

    public class PolyDetail02 {
        public static void main(String[] args) {
            //屬性沒有重寫之說!屬性的值看編譯類型
            Base base = new Sub();//向上轉型
            System.out.println(base.count);// ? 看編譯類型 10
            Sub sub = new Sub();
            System.out.println(sub.count);//?  20
        }
    }
    
    class Base { //父類
        int count = 10;//屬性
    }
    class Sub extends Base {//子類
        int count = 20;//屬性
    }
    
  2. 這里的判斷對象類型是編譯類型還是運行類型運行類型

    public class PolyDetail03 {
        public static void main(String[] args) {
            BB bb = new BB();
            System.out.println(bb instanceof  BB);// true
            System.out.println(bb instanceof  AA);// true
    
            //aa 編譯類型 AA, 運行類型是BB
            //BB是AA子類
            AA aa = new BB();
            System.out.println(aa instanceof AA);// true
            System.out.println(aa instanceof BB);// true, 兩個true說明是判斷的運行類型
    
            Object obj = new Object();
            System.out.println(obj instanceof AA);//false
            String str = "hello";
            //System.out.println(str instanceof AA);
            System.out.println(str instanceof Object);//true
        }
    }
    
    class AA {} //父類
    class BB extends AA {}//子類
    
1.3.5.4 多態課堂練習
  1. 這一題主要注意:屬性看編譯類型,方法看運行類型

1.3.5.5 動態綁定機制*

一個經典案例:第一句打印的a.sum()方法里面getI()是子類的還是父類的?

首先,根據方法的動態綁定機制,a的運行類型是B類,所以先去B類中找有沒有sum方法,沒有去A類中找,找到后發現getI方法,因為a的運行類型是B類,所以又先去B類中找getI方法,由於屬性是沒有動態綁定機制的,所以getI方法中的i就是B類中的i=20。

public class DynamicBinding {
    public static void main(String[] args) {
        //a 的編譯類型 A, 運行類型 B
        A a = new B();//向上轉型
        System.out.println(a.sum());//?40 -> 30
        System.out.println(a.sum1());//?30-> 20
    }
}

class A {//父類
    public int i = 10;
    //動態綁定機制:

    public int sum() {//父類sum()
        return getI() + 10;//20 + 10
    }

    public int sum1() {//父類sum1()
        return i + 10;//10 + 10
    }

    public int getI() {//父類getI
        return i;
    }
}

class B extends A {//子類
    public int i = 20;

//    public int sum() {
//        return i + 20;
//    }

    public int getI() {//子類getI()
        return i;
    }

//    public int sum1() {
//        return i + 10;
//    }
}
1.3.5.6 多態數組
public class PloyArray {
    public static void main(String[] args) {
        //======================================================
        //應用實例:現有一個繼承結構如下:要求創建1個Person對象、
        // 2個Student 對象和2個Teacher對象, 統一放在數組中,並調用每個對象say方法
        Person[] persons = new Person[5];
        persons[0] = new Person("jack", 20);
        persons[1] = new Student("mary", 18, 100);
        persons[2] = new Student("smith", 19, 30.1);
        persons[3] = new Teacher("scott", 30, 20000);
        persons[4] = new Teacher("king", 50, 25000);

        //循環遍歷多態數組,調用say
        for (int i = 0; i < persons.length; i++) {
            //老師提示: person[i] 編譯類型是 Person ,運行類型是是根據實際情況有JVM來判斷
            System.out.println(persons[i].say());//動態綁定機制,數組多態
			
			//===================================================
			//應用實例:如何調用子類特有的方法
            //這里大家聰明. 使用 類型判斷 + 向下轉型.
            if(persons[i]  instanceof  Student) {//判斷person[i] 的運行類型是不是Student
                Student student = (Student)persons[i];//向下轉型
                student.study();
                //小伙伴也可以使用一條語句 ((Student)persons[i]).study();
            } else if(persons[i] instanceof  Teacher) {
                Teacher teacher = (Teacher)persons[i];
                teacher.teach();
            } else if(persons[i] instanceof  Person){
                //System.out.println("你的類型有誤, 請自己檢查...");
            } else {
                System.out.println("你的類型有誤, 請自己檢查...");
            }
        }
    }
}
1.3.5.7 多態參數

第二章 反射專題

2.1 一個需求引出反射(快速入門)

需求:從一個配置文件中讀取指定信息,並用這個信息創建對象且調用方法。

然后在實際操作中會發現,如下圖,當獲取到類的路徑時,並不能通過直接new classfullpath() 來生成對象,因為classfullpath是一個String字符串!

類似於這樣的需求在學習框架時很多,即通過外部文件配置,在不修改源碼情況下,通過修改外部文件配置來控制程序,也符合設計模式的ocp原則(開閉原則:不修改源碼,擴展修改功能) 這一點,在老韓的例子中,只需要把properties配置文件中的methodname更改即可

使用反射解決:通過classfullpath路徑名,來獲取到對應的類(有點反射的意思了)

在Java核心技術第11版中,獲取對象實例的方法變成:

Object o = cla.getConstructor().newInstance();  // Object o = cla.newInstance();

2.2 反射機制

反射原理圖:

  • Java反射機制可以完成:
  • 反射相關的主要類:
  • 反射的優缺點:
    1. 優點:可以動態地創建和使用對象(也是框架底層核心),使用靈活,沒有反射機制的話,框架技術就失去了底層支撐。
    2. 缺點:使用反射基本是解釋執行,對執行速度會有影響。
 // 傳統方案
    static void test1() {
        Cat cat = new Cat();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 90000000; i++) {
            cat.cry();
        }
        long end = System.currentTimeMillis();
        System.out.println("傳統方法調用耗時:"+ (end - start));
    }

    // 反射機制
    static void test2() throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class cls = Class.forName("Cat");
        Object o = cls.getConstructor().newInstance();
        Method method = cls.getMethod("cry");
        long start = System.currentTimeMillis();
        for (int i = 0; i < 90000000; i++) {
            method.invoke(o);
        }
        long end = System.currentTimeMillis();
        System.out.println("反射方法調用耗時:" + (end - start));
    }

輸出:
    傳統方法調用耗時:6
    反射方法調用耗時:501

對反射調用的優化方案——關閉訪問檢查

    // 反射調用優化
    static void test3() throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class cls = Class.forName("Cat");
        Object o = cls.getConstructor().newInstance();
        Method method = cls.getMethod("cry");
        
        method.setAccessible(true);  // 在反射調用方法時,取消安全檢查,進行速度優化
        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 90000000; i++) {
            method.invoke(o);
        }
        long end = System.currentTimeMillis();
        System.out.println("反射優化后調用耗時:" + (end - start));
    }

結果:
    傳統方法調用耗時:5
    反射方法調用耗時:506
    反射優化后調用耗時:292

2.3 Class類特點的梳理

補充:

  1. 在類加載器中進行創建

  2. 在Class類堆中,只會有一份Class對象,這里是通過加鎖,來保證高並發情況下,只有一份Class對象

  3. 通過Class對象可以......

2.4 Class常用方法

2.5 獲取Class對象六種方式

其實是四種最核心的方法:(前四種最重要)

        //1. Class.forName
        String classAllPath = "com.hspedu.Car"; //通過讀取配置文件獲取
        Class<?> cls1 = Class.forName(classAllPath);
        System.out.println(cls1);

        //2. 類名.class , 應用場景: 用於參數傳遞
        Class cls2 = Car.class;
        System.out.println(cls2);

        //3. 對象.getClass(), 應用場景,有對象實例
        Car car = new Car();
        Class cls3 = car.getClass();
        System.out.println(cls3);

        //4. 通過類加載器【4種】來獲取到類的Class對象
        //(1)先得到類加載器 car
        ClassLoader classLoader = car.getClass().getClassLoader();
        //(2)通過類加載器得到Class對象
        Class cls4 = classLoader.loadClass(classAllPath);
        System.out.println(cls4);

        //cls1 , cls2 , cls3 , cls4 其實是同一個對象
        System.out.println(cls1.hashCode());
        System.out.println(cls2.hashCode());
        System.out.println(cls3.hashCode());
        System.out.println(cls4.hashCode());

        //5. 基本數據(int, char,boolean,float,double,byte,long,short) 按如下方式得到Class類對象
        Class<Integer> integerClass = int.class;
        Class<Character> characterClass = char.class;
        Class<Boolean> booleanClass = boolean.class;
        System.out.println(integerClass);//int

        //6. 基本數據類型對應的包裝類,可以通過 .TYPE 得到Class類對象(與5的對象是同一個對象)
        Class<Integer> type1 = Integer.TYPE;
        Class<Character> type2 = Character.TYPE; //其它包裝類BOOLEAN, DOUBLE, LONG,BYTE等待

2.6 哪些類型有Class對象

        Class<String> cls1 = String.class;//外部類
        Class<Serializable> cls2 = Serializable.class;//接口
        Class<Integer[]> cls3 = Integer[].class;//數組
        Class<float[][]> cls4 = float[][].class;//二維數組
        Class<Deprecated> cls5 = Deprecated.class;//注解
        //枚舉
        Class<Thread.State> cls6 = Thread.State.class;
        Class<Long> cls7 = long.class;//基本數據類型
        Class<Void> cls8 = void.class;//void數據類型
        Class<Class> cls9 = Class.class;//

2.7 動態和靜態加載

case 1 的語句是靜態加載,在程序編譯時候就要加載,如果這個類不存在的話,那么就要報錯;相反,

case 2 的語句是動態加載,所以只有當執行case 2 這段代碼時候,才會進行類的加載,動態加載可以理解為延時加載。

2.8 類加載

2.8.1 類加載流程圖

前兩個階段是JVM控制,只有初始化階段是可以由程序員控制。

其中下圖初始化中,是對靜態成員變量的初始化加載,而不是new的階段,那是屬於創建對象了

2.8.2 類加載流程——加載階段

二進制字節流加載到內存中:會將某個類的字節碼二進制數據加載到方法區,同時生成相應的Class類對象(上圖獲取Class對象的三個階段圖中類加載部分)

2.8.3 類加載流程——連接階段(驗證、准備、解析)

驗證:

2021-08-15_114356

准備:

2021-08-15_114433

解析:

2.8.4 類加載流程——初始化

程序員可以操作的階段,與靜態變量有關,與對象沒關系,這是類加載的過程。(靜態變量的加載)

public class ClassLoad03 {
    public static void main(String[] args) throws ClassNotFoundException {
        //老韓分析(加載順序)
        //1. 加載B類,並生成 B的class對象
        //2. 鏈接 num = 0
        //3. 初始化階段
        //    依次自動收集類中的所有靜態變量的賦值動作和靜態代碼塊中的語句,並合並
        /*
                clinit() {
                    System.out.println("B 靜態代碼塊被執行");
                    //num = 300;
                    num = 100;
                }
                合並: num = 100  // 靜態變量按照順序賦值

         */

        //new B();//類加載
        //System.out.println(B.num);//100, 如果直接使用類的靜態屬性,也會導致類的加載

        //看看加載類的時候,是有同步機制控制
        /*
        protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
        {
            //正因為有這個機制,才能保證某個類在內存中, 只有一份Class對象
            synchronized (getClassLoadingLock(name)) {
            //....
            }
            }
         */
        new B();
    }
}

class B {
    static {
        System.out.println("B 靜態代碼塊被執行");
        num = 300;
    }

    static int num = 100;

    public B() {//構造器
        System.out.println("B() 構造器被執行");
    }
}

2.9 獲取類的結構信息

對於提供的API,一般情況下是返回public修飾的字段和方法,若想獲得諸如private、protected修飾的方法或字段,則需要換到類似於getDeclared()*API接口。

2.10 反射暴破

1. 創建實例
/**
 * @author 韓順平
 * @version 1.0
 * 演示通過反射機制創建實例
 */
public class ReflecCreateInstance {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        //1. 先獲取到User類的Class對象
        Class<?> userClass = Class.forName("com.hspedu.reflection.User");
        //2. 通過public的無參構造器創建實例
        Object o = userClass.newInstance();
        System.out.println(o);
        //3. 通過public的有參構造器創建實例
        /*
            constructor 對象就是
            public User(String name) {//public的有參構造器
                this.name = name;
            }
         */
        //3.1 先得到對應構造器(public的有參構造器,String)
        Constructor<?> constructor = userClass.getConstructor(String.class);  //注意,這里傳入構造器形參類型
        //3.2 創建實例,並傳入實參
        Object hsp = constructor.newInstance("hsp");
        System.out.println("hsp=" + hsp);
        //4. 通過非public的有參構造器創建實例
        //4.1 得到private的構造器對象(private有參構造器,int、String)
        Constructor<?> constructor1 = userClass.getDeclaredConstructor(int.class, String.class);
        //4.2 創建實例
		
		/*
		*暴破【暴力破解】 , 使用反射可以訪問private構造器/方法/屬性, 反射面前,都是紙老虎
		*破壞了類的封裝性,相當於留了個后門
		*/
        constructor1.setAccessible(true);
		// 如果沒有第35行的爆破,第37行會報錯:沒有權限...
        Object user2 = constructor1.newInstance(100, "張三豐");
        System.out.println("user2=" + user2);
    }
}
class User { //User類
    private int age = 10;
    private String name = "韓順平教育";

    public User() {//無參 public
    }

    public User(String name) {//public的有參構造器
        this.name = name;
    }

    private User(int age, String name) {//private 有參構造器
        this.age = age;
        this.name = name;
    }

    public String toString() {
        return "User [age=" + age + ", name=" + name + "]";
    }
}

這里的反射爆破可以理解為:將原本用來限制訪問權限的“門”給爆破掉,從而能夠訪問私有的字段方法等,其上圖第四點,通過setAccessible(true),取消安全性檢查,完成爆破。(每一次訪問私有,都要爆破)

2. 操作屬性
        ...
        //1. 得到Student類對應的 Class對象
        Class<?> stuClass = Class.forName("com.hspedu.reflection.Student");
        //2. 創建對象
        Object o = stuClass.newInstance();//o 的運行類型就是Student
        System.out.println(o.getClass());//Student
        //3. 使用反射得到age 屬性對象
        Field age = stuClass.getField("age"); // age是public修飾,可用getField方法
        age.set(o, 88);//通過反射來操作屬性
        System.out.println(o);//
        System.out.println(age.get(o));//返回age屬性的值

        //4. 使用反射操作name 屬性
        Field name = stuClass.getDeclaredField("name");
        //對name 進行暴破, 可以操作private 屬性
        name.setAccessible(true);
        //name.set(o, "老韓");
        // 因為在類加載的時候,static修飾的字段已經存在初始化好了,可以不用通過實例對象來指定
        name.set(null, "老韓~");//修改屬性值,因為name是static屬性,因此 o 也可以寫出null
        System.out.println(o);
        System.out.println(name.get(o)); //獲取屬性值
        System.out.println(name.get(null));//獲取屬性值, 要求name是static
}
class Student {//類
    public int age;
    private static String name;

    public Student() {//構造器
    }

    public String toString() {
        return "Student [age=" + age + ", name=" + name + "]";
    }
}
3. 操作方法
        ...
        //1. 得到Boss類對應的Class對象
        Class<?> bossCls = Class.forName("com.hspedu.reflection.Boss");
        //2. 創建對象(無參構造創建即可)
        Object o = bossCls.newInstance();
        //3. 調用public的hi方法
        //Method hi = bossCls.getMethod("hi", String.class);//OK
        //3.1 得到hi方法對象(注意形參類型)
        Method hi = bossCls.getDeclaredMethod("hi", String.class);//OK,注意是帶形參的方法
        //3.2 調用
        hi.invoke(o, "韓順平教育~");

        //4. 調用private static 方法
        //4.1 得到 say 方法對象(注意形參類型)
        Method say = bossCls.getDeclaredMethod("say", int.class, String.class, char.class);
        //4.2 因為say方法是private, 所以需要暴破,原理和前面講的構造器和屬性一樣
        say.setAccessible(true);
        System.out.println(say.invoke(o, 100, "張三", '男'));
        //4.3 因為say方法是static的,還可以這樣調用 ,可以傳入null
        System.out.println(say.invoke(null, 200, "李四", '女'));

        //5. 在反射中,如果方法有返回值,統一返回Object , 但是他運行類型和方法定義的返回類型一致
        Object reVal = say.invoke(null, 300, "王五", '男');
        System.out.println("reVal 的運行類型=" + reVal.getClass());//String
}
class Boss {//類
    public int age;
    private static String name;

    public Boss() {//構造器
    }

    public Monster m1() {
        return new Monster();
    }

    private static String say(int n, String s, char c) {//靜態方法
        return n + " " + s + " " + c;
    }

    public void hi(String s) {//普通public方法
        System.out.println("hi " + s);
    }
}

第三章 線程(基礎)

2021-08-16_104908

3.1 線程的基本操作

1. 繼承Thread類
/**
 * @author 韓順平
 * @version 1.0
 * 演示通過繼承Thread 類創建線程
 */
public class Thread01 {
	// 主線程main
    public static void main(String[] args) throws InterruptedException {

        //創建Cat對象,可以當做線程使用
        Cat cat = new Cat();

        //老韓讀源碼
        /*  執行步驟:
            (1)
            public synchronized void start() {
                start0();
            }
            (2)
            //start0() 是本地方法(底層方法),是JVM調用, 底層是c/c++實現
            //真正實現多線程的效果, 是start0(), 而不是 run
            private native void start0();

         */

        cat.start();//啟動線程-> 最終會執行cat的run方法(Thread子線程)
        
        //run方法就是一個普通的方法, 沒有真正的啟動一個線程,就會把run方法執行完畢,才向下執行,main線程會被阻塞
        //cat.run();//相當於main線程切換到Thread子線程
        
        //說明: 當main線程啟動一個子線程 Thread-0, 主線程不會阻塞, 會繼續執行。.start()開啟一個子線程
        //這時 主線程和子線程是交替執行..
        
        System.out.println("主線程繼續執行" + Thread.currentThread().getName());//名字main
        for(int i = 0; i < 60; i++) {
            System.out.println("主線程 i=" + i);
            //讓主線程休眠1秒
            Thread.sleep(1000);
        }

    }
}

//老韓說明
//1. 當一個類繼承了 Thread 類, 該類就可以當做線程使用
//2. 我們會重寫 run方法,寫上自己的業務代碼
//3. run Thread 類 實現了 Runnable 接口的run方法
/*
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
 */

class Cat extends Thread {
    int times = 0;
    @Override
    public void run() {//重寫run方法,寫上自己的業務邏輯
        while (true) {
            //該線程每隔1秒。在控制台輸出 “喵喵, 我是小貓咪”
            System.out.println("喵喵, 我是小貓咪" + (++times) + " 線程名=" + Thread.currentThread().getName());
            //讓該線程休眠1秒 ctrl+alt+t
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(times == 80) {
                break;//當times 到80, 退出while, 這時線程也就退出..
            }
        }
    }
}
2021-08-16_155451
2. 實現Runnable接口

Java是單繼承的,若A已經繼承了B,此時就無法再繼承Thread開啟子線程,因此提供了實現Runnable接口的方式

/**
 * @author 韓順平
 * @version 1.0
 * 通過實現接口Runnable 來開發線程
 */
public class Thread02 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        //dog.start(); 這里不能調用start
        //創建了Thread對象,把 dog對象(實現Runnable),放入Thread
        Thread thread = new Thread(dog);
        thread.start();

//        Tiger tiger = new Tiger();//實現了 Runnable
//        ThreadProxy threadProxy = new ThreadProxy(tiger);
//        threadProxy.start();
    }
}

class Animal {
}

class Tiger extends Animal implements Runnable {

    @Override
    public void run() {
        System.out.println("老虎嗷嗷叫....");
    }
}

//線程代理類 , 模擬了一個極簡的Thread類
class ThreadProxy implements Runnable {//你可以把Proxy類當做 ThreadProxy

    private Runnable target = null;//屬性,類型是 Runnable

    @Override
    public void run() {
        if (target != null) {
            target.run();//動態綁定(運行類型Tiger)
        }
    }

    public ThreadProxy(Runnable target) {
        this.target = target;
    }

    public void start() {
        start0();//這個方法時真正實現多線程方法
    }

    public void start0() {
        run();
    }
}


class Dog implements Runnable { //通過實現Runnable接口,開發線程

    int count = 0;

    @Override
    public void run() { //普通方法
        while (true) {
            System.out.println("小狗汪汪叫..hi" + (++count) + Thread.currentThread().getName());

            //休眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (count == 10) {
                break;
            }
        }
    }
}

上面的代碼主要涉及到一個很重要的知識點,就是:實現了Runnable接口的類,可以再用Thread構造器構造一個Thread對象來調用start()方法。

使用Runnable接口實現類實例構建Thread對象時,可以不用線程對象.setName()來給線程取名,直接在new對象的時候,傳入名字即可:Thread thread = new Thread(Runnable實現類實例,name)

3. 多線程執行
public class Thread03 {
    public static void main(String[] args) {

        T1 t1 = new T1();
        T2 t2 = new T2();
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);
        thread1.start();//啟動第1個線程
        thread2.start();//啟動第2個線程
        //...

    }
}

class T1 implements Runnable {

    int count = 0;

    @Override
    public void run() {
        while (true) {
            //每隔1秒輸出 “hello,world”,輸出10次
            System.out.println("hello,world " + (++count));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 60) {
                break;
            }
        }
    }
}

class T2 implements Runnable {

    int count = 0;

    @Override
    public void run() {
        //每隔1秒輸出 “hi”,輸出5次
        while (true) {
            System.out.println("hi " + (++count));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 50) {
                break;
            }
        }
    }
}
4. 線程終止
public class ThreadExit_ {
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        t1.start();

        //如果希望main線程去控制t1 線程的終止, 必須可以修改 loop
        //讓t1 退出run方法,從而終止 t1線程 -> 通知方式

        //讓主線程休眠 10 秒,再通知 t1線程退出
        System.out.println("main線程休眠10s...");
        Thread.sleep(10 * 1000);
		
		// 通知t1線程退出
        t1.setLoop(false);
    }
}

class T extends Thread {
    private int count = 0;
    //設置一個控制變量
    private boolean loop = true;
    @Override
    public void run() {
        while (loop) {  // loop為false,線程結束
            try {
                Thread.sleep(50);// 讓當前線程休眠50ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T 運行中...." + (++count));
        }

    }
    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

3.2. 線程常用方法

1. 中斷、禮讓、插隊

注意事項和細節:

  1. 指start0方法,JVM底層調用方法,實現多線程
  2. interrupt,只是中斷線程,不是結束線程,用於提前中斷休眠的線程
public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        //測試相關的方法
        T t = new T();
        t.setName("老韓");
        t.setPriority(Thread.MIN_PRIORITY);//1
        t.start();//啟動子線程

        //主線程打印5 hi ,然后我就中斷 子線程的休眠
        for(int i = 0; i < 5; i++) {
            Thread.sleep(1000);
            System.out.println("hi " + i);
        }

        System.out.println(t.getName() + " 線程的優先級 =" + t.getPriority());//1

        t.interrupt();//當執行到這里,就會提前中斷 t線程的休眠.(此時t線程正在休眠中,才能中斷休眠)
    }
}

class T extends Thread { //自定義的線程類
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 100; i++) {
                //Thread.currentThread().getName() 獲取當前線程的名稱
                System.out.println(Thread.currentThread().getName() + "  吃包子~~~~" + i);
            }
            try {
                System.out.println(Thread.currentThread().getName() + " 休眠中~~~");
                Thread.sleep(20000);//20秒
            } catch (InterruptedException e) {
                //當該線程執行到一個interrupt 方法時,就會catch 一個 異常, 可以加入自己的業務代碼
                //InterruptedException 是捕獲到一個中斷異常.
                System.out.println(Thread.currentThread().getName() + "被 interrupt了");
            }
        }
    }
}

yield:翻譯 —> 讓出,禮讓的時刻由操作系統底層內核決定,在CPU資源緊張的時候,禮讓的成功率要高一些。

join:加入、插隊,

public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {
        T2 t2 = new T2();
        t2.start();
        for(int i = 1; i <= 20; i++) {
            Thread.sleep(1000);  // 主線程也休眠1秒
            System.out.println("主線程(小弟) 吃了 " + i  + " 包子");
            if(i == 5) {
                System.out.println("主線程(小弟) 讓 子線程(老大) 先吃");
                //join, 線程插隊
                //t2.join();// 這里相當於讓t2 線程先執行完畢(子線程先吃完)
                Thread.yield();//禮讓,不一定成功..(主線程禮讓)
                System.out.println("子線程(老大) 吃完了 主線程(小弟) 接着吃..");
            }
        }
    }
}
class T2 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            try {
                Thread.sleep(1000);//休眠1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子線程(老大) 吃了 " + i +  " 包子");
        }
    }
}
2. 用戶線程和守護線程

守護線程!!!定義重要

public class ThreadMethod03 {
    public static void main(String[] args) throws InterruptedException {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        //如果我們希望當main線程結束后,子線程自動結束
        //,只需將子線程設為守護線程即可
        myDaemonThread.setDaemon(true);  // 這句話關鍵,子線程設置為主線程的守護進程,當主線程結束后,子線程自動結束
        
        myDaemonThread.start();  // 開啟子線程
        for( int i = 1; i <= 10; i++) {//main線程
            System.out.println("寶強在辛苦的工作...");
            Thread.sleep(1000);
        }
    }
}
class MyDaemonThread extends Thread {
    public void run() {
        for (; ; ) {//無限循環
            try {
                Thread.sleep(1000);//休眠1000毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("馬蓉和宋喆快樂聊天,哈哈哈~~~");
        }
    }
}

Daemon:守護

3.3 線程的生命周期(操作系統知識)

通常認為線程的生命周期一共有六個狀態或七個狀態,其中因為在Runnable狀態下有細分為就緒態和運行態,所以細分的話有七個狀態。( 創建態new、可執行態runnable、阻塞態blocked、等待態waiting、超時等待態timedwaiting

從圖中也能明白為什么yield禮讓不一定成功呢,因為只是從運行態切換至就緒態,不一定會立馬獲取到CPU

public class ThreadState_ {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        System.out.println(t.getName() + " 狀態 " + t.getState());
        t.start();

        while (Thread.State.TERMINATED != t.getState()) { // 如果子線程進入結束態,就跳出循環,第一個語句是枚舉
            System.out.println(t.getName() + " 狀態 " + t.getState());
            Thread.sleep(500);  // 方便打印
        }
        System.out.println(t.getName() + " 狀態 " + t.getState());
    }
}

class T extends Thread {
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 5; i++) {
                System.out.println("hi " + i);
                try {
                    Thread.sleep(1000);  // 這里會導致子線程進入timedwaiting態
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            break;
        }
    }
}

運行結果:
    
Thread-0 子線程狀態 NEW
Thread-0 子線程狀態 RUNNABLE
hi 0
Thread-0 子線程狀態 TIMED_WAITING
Thread-0 子線程狀態 TIMED_WAITING
hi 1
Thread-0 子線程狀態 TIMED_WAITING
Thread-0 子線程狀態 TIMED_WAITING
hi 2
Thread-0 子線程狀態 TIMED_WAITING
Thread-0 子線程狀態 TIMED_WAITING
hi 3
Thread-0 子線程狀態 TIMED_WAITING
Thread-0 子線程狀態 TIMED_WAITING
hi 4
Thread-0 子線程狀態 TIMED_WAITING
Thread-0 子線程狀態 TIMED_WAITING
Thread-0 子線程狀態 TERMINATED

3.4 線程同步機制

3.5 互斥鎖

注意事項:

基本介紹中的第5點:(互斥鎖)

在之前售票問題中,為了解決互斥問題,需要讓三個線程對象訪問同一個對象,代碼如下:要求多個線程的鎖對象為同一個

失敗的反例 1和2 :

1
//使用Thread方式
// new SellTicket01().start(); // 1號
// new SellTicket01().start(); // 2號
class SellTicket01 extends Thread {

    private static int ticketNum = 100;//讓多個線程共享 ticketNum

    public void m1() {
        synchronized (this) {// this並不能鎖住m1方法(),因為this是指的各自new的一個Thread對象本身(即1號、2號)
            System.out.println("hello");
        }
    }
    .......

2 重復創建Object,失敗
        ......
        public /*synchronized*/ void sell() { //同步方法, 在同一時刻, 只能有一個線程來執行sell方法
        synchronized (new Object()) {  // 每個線程拿的都是各自新創建的Object對象,並不能鎖到同一個對象
            if (ticketNum <= 0) {
                System.out.println("售票結束...");
                loop = false;
                return;
            }
        ......

成功的例子:方法一,使用Object object = new Object(); 方法二,同一個Class對象實例創建的多個不同的子線程對象,爭奪同一個this對象(本章作業第二個就是這樣)

最穩妥的方法:無論是否是同一個Class對象或不同Class對象,synchronized()里面都用object方法,一般使用synchronized代碼塊,提高效率

/**
 * 使用多線程,模擬三個窗口同時售票100張
 */
public class SellTicket {
    public static void main(String[] args) {
        // 方法一:因為三個窗口對象不同,為了實現三個窗口訪問同一個售票對象並上鎖,需要使用Object指定方法
                //測試
//        SellTicket01 sellTicket01 = new SellTicket01();
//        SellTicket01 sellTicket02 = new SellTicket01();
//        SellTicket01 sellTicket03 = new SellTicket01();
//
//        //這里我們會出現超賣..
//        sellTicket01.start();//啟動售票線程
//        sellTicket02.start();//啟動售票線程
//        sellTicket03.start();//啟動售票線程
        
        // 方法二:當多個線程執行到這里時,就會去爭奪 this對象鎖,是同一個this對象
        SellTicket03 sellTicket03 = new SellTicket03();
        // 子線程執行重寫的run()方法
        new Thread(sellTicket03).start();//第1個線程-窗口
        new Thread(sellTicket03).start();//第2個線程-窗口
        new Thread(sellTicket03).start();//第3個線程-窗口
    }
}
//實現接口方式, 使用synchronized實現線程同步
class SellTicket03 implements Runnable {
    private int ticketNum = 100;//讓多個線程共享 ticketNum
    private boolean loop = true;//控制run方法變量
    
    Object object = new Object();  // 為了讓三個線程(窗口)同時訪問一個對象,即對同一售票行為進行上鎖

    //同步方法(靜態的)的鎖為當前類本身
    //老韓解讀
    //1. public synchronized static void m1() {} 鎖是加在 SellTicket03.class
    //2. 如果在靜態方法中,實現一個同步代碼塊.
    /*
        synchronized (SellTicket03.class) {
            System.out.println("m2");
        }
     */
    public synchronized static void m1() { // 鎖是加在 SellTicket03.class

    }
    
    public static  void m2() { // 注意這里的static方法,syn對象是當前類本身
        synchronized (SellTicket03.class) {  // 這里不能用this,因為是靜態方法,必須用當前類本身
            System.out.println("m2");
        }
    }

    //老韓說明
    //1. public synchronized void sell() {} 就是一個同步方法,但是是一個非靜態方法
    //2. 這時鎖在 this對象
    //3. 也可以在代碼塊上寫 synchronize ,同步代碼塊, 互斥鎖還是在this對象
    public /*synchronized*/ void sell() { //同步方法, 在同一時刻, 只能有一個線程來執行sell方法
        synchronized (/*this*/ object) {  // 注意這里的object
            if (ticketNum <= 0) {
                System.out.println("售票結束...");
                loop = false;
                return;
            }
            //休眠50毫秒, 模擬
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一張票"
                    + " 剩余票數=" + (--ticketNum));//1 - 0 - -1  - -2
        }
    }
    @Override
    public void run() {
        while (loop) { // loop 控制線程運行變量
            sell();//sell方法是一共同步方法
        }
    }
}

3.6 線程死鎖

3.7 釋放鎖

3.8 本章作業

public class ThreadHomeWork01 {
    public static void main(String[] args) {
        A a = new A();
        B b = new B(a);  // 傳遞a進去很關鍵
        b.start();
        a.start();
    }
}

// 創建A線程類
class A extends Thread {
    private boolean loop = true;

    @Override
    public void run() {
        // 輸出1-100的數字
        while (loop) {
            System.out.println((int) (Math.random() * 100 + 1));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

// 創建B線程類,用於從鍵盤中讀取“Q”命令
class B extends Thread {
    private A a;
    private Scanner scanner = new Scanner(System.in);

    public B() {
    }

    public B(A a) { // 直接通過構造器傳入A類對象
        this.a = a;
    }
    
    @Override
    public void run() {
        while (true) { // 循環,方便接收數據
            // 接收到用戶的輸入
            System.out.println("請輸入你的指令(Q)來表示退出:");
            char key = scanner.next().toUpperCase().charAt(0);
            if (key == 'Q') {
                a.setLoop(false);
                System.out.println("B線程退出.");
                break;
            }
        }
    }
}
public class ThreadHomeWork02 {
    public static void main(String[] args) {
        // 同一個實現Runnable接口的實例創建的不同線程對象,爭奪同一個this對象
        Test t = new Test();
        Thread thread1 = new Thread(t);
        thread1.setName("t1");
        Thread thread2 = new Thread(t);
        thread2.setName("t2");
        thread1.start();
        thread2.start();
    }
}

//編程取款的線程
//1.因為這里涉及到多個線程共享資源,所以我們使用實現Runnable方式
//2. 每次取出 1000
class Test implements  Runnable {
    private int money = 10000;
//    Object o = new Object();
    @Override
    public void run() {
        while (true) {
            //解讀
            //1. 這里使用 synchronized 實現了線程同步
            //2. 當多個線程執行到這里時,就會去爭奪 this對象鎖
            //3. 哪個線程爭奪到(獲取)this對象鎖,就執行 synchronized 代碼塊, 執行完后,會釋放this對象鎖
            //4. 爭奪不到this對象鎖,就blocked ,准備繼續爭奪
            //5. this對象鎖是非公平鎖.
            synchronized (/*o*/ this) {
                //判斷余額是否夠
                if (money < 1000) {
                    System.out.println("余額不足");
                    break;
                }
                money -= 1000;
                System.out.println(Thread.currentThread().getName() + " 取出了1000 當前余額=" + money);
            }
            //休眠1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

第四章 集合專題

Java集合底層機制很重要!!!!!!!

集合主要分兩組,分別為單列集合、雙列集合(鍵值對)

Collection:該接口由兩個重要的子接口,List(有序重復)、Set(無序、不允許重復元素),他們的實現子類都是單列集合

Map:實現的子類是雙列集合,存放鍵值對

2021-08-19_102946

4.1 Collection接口和常用方法

4.1.1 Collection接口實現類的特點

注意:Collection是接口不能被實例化,只能通過其實現子類來進行實例化

list.add(10); // 其實是list.add(new Integer(10))

        List list = new ArrayList();
//        add:添加單個元素
        list.add("jack");
        list.add(10);//list.add(new Integer(10))
        list.add(true);
        System.out.println("list=" + list);
//        remove:刪除指定元素
        //list.remove(0);//刪除第一個元素
        list.remove(true);//指定刪除某個元素
        System.out.println("list=" + list);
//        contains:查找元素是否存在
        System.out.println(list.contains("jack"));//T
//        size:獲取元素個數
        System.out.println(list.size());//2
//        isEmpty:判斷是否為空
        System.out.println(list.isEmpty());//F
//        clear:清空
        list.clear();
        System.out.println("list=" + list);
//        addAll:添加多個元素
        ArrayList list2 = new ArrayList();
        list2.add("紅樓夢");
        list2.add("三國演義");
        list.addAll(list2);
        System.out.println("list=" + list);
//        containsAll:查找多個元素是否都存在
        System.out.println(list.containsAll(list2));//T
//        removeAll:刪除多個元素
        list.add("聊齋");
        list.removeAll(list2);
        System.out.println("list=" + list);//[聊齋]
4.1.2 Collection接口遍歷元素方式
  1. 使用Iterator(迭代器)

迭代器的執行原理

        Collection col = new ArrayList();

        col.add(new Book("三國演義", "羅貫中", 10.1));
        col.add(new Book("小李飛刀", "古龍", 5.1));
        col.add(new Book("紅樓夢", "曹雪芹", 34.6));
        //System.out.println("col=" + col);
        //現在老師希望能夠遍歷 col集合
        //1. 先得到 col 對應的 迭代器
        Iterator iterator = col.iterator();
        //2. 使用while循環遍歷
//        while (iterator.hasNext()) {//判斷是否還有數據
//            //返回下一個元素,類型是Object
//            Object obj = iterator.next();  // 編譯類型是Object,但運行時會自動找到對應的類型,運行類型為Book
//            System.out.println("obj=" + obj);
//        }
        //老師教大家一個快捷鍵,快速生成 while => itit
        //顯示所有的快捷鍵的的快捷鍵 ctrl + j
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            System.out.println("obj=" + obj);
        }
        //3. 當退出while循環后 , 這時iterator迭代器,指向最后的元素
        //   iterator.next();//NoSuchElementException
        //4. 如果希望再次遍歷,需要重置我們的迭代器

        iterator = col.iterator();  // 重置迭代器

        System.out.println("===第二次遍歷===");
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            System.out.println("obj=" + obj);
        }
4.1.3 課堂練習
        List list = new ArrayList();
        list.add(new Dog("小黑", 3));
        list.add(new Dog("大黃", 100));
        list.add(new Dog("大壯", 8));

        //先使用for增強
        for (Object dog : list) { // 可以使用Dog dog :list,但得確定集合里面全是Dog對象
            System.out.println("dog=" + dog);
        }

        //使用迭代器
        System.out.println("===使用迭代器來遍歷===");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            Object dog =  iterator.next();
            System.out.println("dog=" + dog);
        }

4.2 List接口方法

4.2.1 List接口和常用方法
2021-08-20_101045
        List list = new ArrayList();
        list.add("張三豐");
        list.add("賈寶玉");
//        void add(int index, Object ele):在index位置插入ele元素
        //在index = 1的位置插入一個對象
        list.add(1, "韓順平");
        System.out.println("list=" + list);
//        boolean addAll(int index, Collection eles):從index位置開始將eles中的所有元素添加進來
        List list2 = new ArrayList();
        list2.add("jack");
        list2.add("tom");
        list.addAll(1, list2);
        System.out.println("list=" + list);
//        Object get(int index):獲取指定index位置的元素
        //說過
//        int indexOf(Object obj):返回obj在集合中首次出現的位置
        System.out.println(list.indexOf("tom"));//2
//        int lastIndexOf(Object obj):返回obj在當前集合中末次出現的位置
        list.add("韓順平");
        System.out.println("list=" + list);
        System.out.println(list.lastIndexOf("韓順平"));
//        Object remove(int index):移除指定index位置的元素,並返回此元素
        list.remove(0);
        System.out.println("list=" + list);
//        Object set(int index, Object ele):設置指定index位置的元素為ele , 相當於是替換,index必須是已經存在的
        list.set(1, "瑪麗");
        System.out.println("list=" + list);
//        List subList(int fromIndex, int toIndex):返回從fromIndex到toIndex位置的子集合
        // 注意返回的子集合 fromIndex <= subList < toIndex 包頭不包尾
        List returnlist = list.subList(0, 2);
        System.out.println("returnlist=" + returnlist);
    }
4.2.2 課堂練習01
               /*
        添加10個以上的元素(比如String "hello" ),在2號位插入一個元素"韓順平教育",
        獲得第5個元素,刪除第6個元素,修改第7個元素,在使用迭代器遍歷集合,
        要求:使用List的實現類ArrayList完成。
         */ 
        List list = new ArrayList();
        for (int i = 0; i < 12; i++) {  // 添加元素
            list.add("hello" + i);
        }
        System.out.println("list:" + list);
        // 在2號位插入一個元素“韓順平教育”
        list.add(1, "韓順平教育");
        System.out.println("在2號位插入一個元素“韓順平教育” list:" + list);
        // 獲得第五個元素
        System.out.println("第五個元素:" + list.get(4));
        // 刪除第六個元素
        list.remove(5);
        System.out.println("刪除第六個元素 list:" + list);
        // 修改第七個元素為文豪
        list.set(6, "文豪");
        System.out.println("修改第七個元素為文豪 list: " + list);
        // 使用迭代器遍歷集合
        Iterator iterator = list.iterator();
        System.out.println("迭代器遍歷集合 list: ");
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.print(" " + next);
        }
4.2.3 List的三種遍歷方法

其中方式二,必須重寫對象類的toString方法!!

4.2.4 課堂練習02
public class ListExercise02 {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Book("紅樓夢", "曹雪芹", 100));
        list.add(new Book("西游記", "吳承恩", 10));
        list.add(new Book("水滸傳", "施耐庵", 19));
        list.add(new Book("三國", "羅貫中", 80));
        System.out.println("排序前:");
        for (Object o : list) {
            System.out.println(o);
        }
        sort(list);
        System.out.println("排序后:");
        for (Object o : list) {  // 使用這個方法,必須重寫toString()
            System.out.println(o);
        }
    }
    
//    //靜態方法
//    //價格要求是從小到大,冒泡排序
//    public static void sort(List list) {
//        int listSize = list.size();
//        for (int i = 0; i < listSize - 1; i++) {
//            for (int j = 0; j < listSize - 1 - i; j++) {
//                //取出對象Book
//                Book book1 = (Book) list.get(j);
//                Book book2 = (Book) list.get(j + 1);
//                if (book1.getPrice() > book2.getPrice()) {//交換
//                    list.set(j, book2);
//                    list.set(j + 1, book1);
//                }
//            }
//        }
//    }

    // 冒泡排序,我自己經常寫的思路
    public static void sort(List list) {
        int listSize = list.size();
        for (int i = 0; i < listSize - 1; i++) {
            for (int j = i; j < listSize; j++) {
                Book book1 = (Book)list.get(i);
                Book book2 = (Book)list.get(j);
                if (book1.getPrice() > book2.getPrice()) {
                    list.set(i, book2);
                    list.set(j, book1);
                }
            }
        }
    }
}

class Book {
    private String name;
    private double price;
    private String author;
    public Book() {}
    public Book (String name, String author, double price) {
        this.name = name;
        this.author = author;
        this.price = price;
    }
    public double getPrice() {
        return price;
    }
    @Override
    public String toString() {  // 使用list.for 增加型for循環,必須重寫toString方法
        return "Book{" +
                "name='" + name + '\'' +
                ", price=" + price +
                ", author='" + author + '\'' +
                '}';
    }
}

4.3 ArrayList底層結構和源碼解析

4.3.1 ArrayList注意事項

2)底層是由數組實現的

3)底層:ArrayList是線程不安全的,源碼沒有用 synchronized


    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
4.3.2 ArrayList底層結構和源碼分析
public class ArrayListSource {
    public static void main(String[] args) {
        //老韓解讀源碼
        //注意,注意,注意,Idea 默認情況下,Debug 顯示的數據是簡化后的,如果希望看到完整的數據
        //需要做設置.
        //使用無參構造器創建ArrayList對象
        ArrayList list = new ArrayList();  // 無參構造器進行debug
//        ArrayList list = new ArrayList(8);  // 指定大小構造器進行debug
        //使用for給list集合添加 1-10數據
        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }
        //使用for給list集合添加 11-15數據
        for (int i = 11; i <= 15; i++) {
            list.add(i);
        }
        list.add(100);
        list.add(200);
        list.add(null);
    }
}

源碼解讀:(從無參構造器開始debug,分析使用無參構造器,即 ArrayList list = new ArrayList() )

第一步:

2021-08-20_154859

2021-08-20_154909

創建了一個空的elementData數組={},Object類型,由於Object是所有類的父類,所以能夠存放很多不同類型的對象。

從這里也能得出結論:ArrayList的底層其實是數組

ArrayList初始化完成后(創建空數組),代碼來到第一個for循環,准備添加數據

第二步:主要是判斷是否需要擴容

2021-08-20_155354

2021-08-20_155547

valueOf 對整數進行裝箱處理,將基本數據類型處理為對應的Integer對象

2021-08-20_155855

modCount++ 記錄集合被修改的次數;此時因為只是創建的空數組,所以size為0,進入另一個add方法

modCount防止多線程操作帶來的異常,源碼注釋的解釋:

大概意思就是說:在使用迭代器遍歷的時候,用來檢查列表中的元素是否發生結構性變化(列表元素數量發生改變)了,保證在多線程環境下,迭代器能夠正常遍歷,主要在多線程環境下需要使用,防止一個線程正在迭代遍歷,另一個線程修改了這個列表的結構。好好認識下這個異常:ConcurrentModificationException。對了,ArrayList是非線程安全的,所以在遍歷非線程安全的集合時(ArrayList和LinkedList),最好使用迭代器

2021-08-20_160001

判斷,當前elementData的大小夠不夠,如果不夠就調用grow()去擴容。如果當前ArrayList長度s小於已分配的elementData數組大小,那么就直接插入新數據,elementData[s] = e,很明顯。

2021-08-20_160345

2021-08-20_160542

grow() 擴容機制,兩種方法。這里的話,執行第二種,即當前size為0,此時初始擴容為10的大小

第三步:添加數據

2021-08-20_161040

擴容成功,可以正常添加數據了,當前ArrayList已有數據+1

2021-08-20_161151

內部add方法執行完畢,來到return語句,返回true,數據添加成功,后面大同小異,第一個for循環debug完畢!

第四步:已分配的內存空間用完了,需要擴容

2021-08-20_161330

無參構造器第一次初始分配大小為10,第一個for循環已經用完,此時來到第二個for循環,需要進行擴容

先是裝箱,裝箱完畢進入add()方法,

2021-08-20_161908

此時,modCount為11,第11次修改,即將進入內add方法

2021-08-20_162041

判斷是否需要擴容,true,進入擴容函數grow()

2021-08-20_162157

原先分配大小為10,顯然得用newLength()方法進行擴容

2021-08-20_162344

擴容多少的計算方法可以簡述為:擴容到oldCapacity的1.5倍。計算方式是:oldCapacity除以2(底層是右移一位實現除法操作),再加上原先的大小,即原先的1.5倍。

2021-08-20_162658

調用Arrays.copyOf()方法,該方法作用:將原先數據復制到一個指定新的大小的數組,原先數據順序和位置不變,多出來的空間全以null。返回給elementData,完成擴容操作。

2021-08-20_163246

從10擴容到15,1.5倍,沒用到的空間,全部是null

另一種:指定大小構造器

2021-08-20_164708

與第一種不同的是:一開始創建了一個指定大小的Object數組。其余后面就跟有大小,進行擴容操作一樣。

4.4 Vector底層結構和源碼解析

元素可以重復,可以添加null

4.4.1 Vector底層結構和ArrayList的比較
4.4.2 Vector源碼分析

自己過一下debug就行:代碼里面的源碼,跟本地不一樣,不同jdk版本,源碼實現不一樣,但大體思路是一樣的

@SuppressWarnings({"all"})
public class Vector_ {
    public static void main(String[] args) {
        //無參構造器 Vector vector = new Vector();
 
        //有參數的構造
        Vector vector = new Vector(8);
        for (int i = 0; i < 10; i++) {
            vector.add(i);
        }
        vector.add(100);
        System.out.println("vector=" + vector);
        //老韓解讀源碼
        //1. new Vector() 底層
        /*
            public Vector() {
                this(10);
            }
         補充:如果是  Vector vector = new Vector(8);
            走的方法:
            public Vector(int initialCapacity) {
                this(initialCapacity, 0);
            }
         2. vector.add(i)
         2.1  //下面這個方法就添加數據到vector集合
            public synchronized boolean add(E e) {
                modCount++;
                ensureCapacityHelper(elementCount + 1);
                elementData[elementCount++] = e;
                return true;
            }
          2.2  //確定是否需要擴容 條件 : minCapacity - elementData.length>0
            private void ensureCapacityHelper(int minCapacity) {
                // overflow-conscious code
                if (minCapacity - elementData.length > 0)
                    grow(minCapacity);
            }
          2.3 //如果 需要的數組大小 不夠用,就擴容 , 擴容的算法
              //newCapacity = oldCapacity + ((capacityIncrement > 0) ?
              //                             capacityIncrement : oldCapacity);
              //就是擴容兩倍.
            private void grow(int minCapacity) {
                // overflow-conscious code
                int oldCapacity = elementData.length;
                int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                                 capacityIncrement : oldCapacity);
                if (newCapacity - minCapacity < 0)
                    newCapacity = minCapacity;
                if (newCapacity - MAX_ARRAY_SIZE > 0)
                    newCapacity = hugeCapacity(minCapacity);
                elementData = Arrays.copyOf(elementData, newCapacity);
            }
         */
    }
}

上面有Vector的有參構造和無參構造,無參初試默認容量為10,注意Vector在有參構造時,能夠指定擴容大小

第一張圖中的newLength()方法中的三元運算符注意一下,如果我們制定了擴容大小的話,在第二張圖里面就能夠清晰得看到每次擴容大小等於指定大小加上原先的大小,若沒有指定大小,則是加上原來的大小,即兩倍。

4.5 LinkedList底層結構和源碼解析

4.5.1 底層結構

2021-08-21_111513

注意點:

  1. 底層維護了一個雙向鏈表,通過觀察LinkedList源碼可以發現有Node結點對象:first和last
  2. LinkedList的增刪操作,因為不是通過數組完成的,所以效率較高。改和查則不一定效率高,畢竟數組可以直接查找,鏈表只能一一遍歷。
4.5.2 源碼解讀

源碼解讀:LinkedList在底層添加元素,是采用尾插法,有一個linkLast()方法

// Debug程序
@SuppressWarnings({"all"})
public class LinkedListUse {
    public static void main(String[] args) {

        LinkedList linkedList = new LinkedList();
        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);
        System.out.println("linkedList=" + linkedList);

        //演示一個刪除結點的
        linkedList.remove(); // 這里默認刪除的是第一個結點
        //linkedList.remove(2);

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

        //修改某個結點對象
        linkedList.set(1, 999);
        System.out.println("linkedList=" + linkedList);

        //得到某個結點對象
        //get(1) 是得到雙向鏈表的第二個對象
        Object o = linkedList.get(1);
        System.out.println(o);//999

        //因為LinkedList 是 實現了List接口, 遍歷方式有三種(迭代器、增強for、普通for)
        System.out.println("===LinkeList遍歷迭代器====");
        Iterator iterator = linkedList.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println("next=" + next);

        }

        System.out.println("===LinkeList遍歷增強for====");
        for (Object o1 : linkedList) {
            System.out.println("o1=" + o1);
        }
        System.out.println("===LinkeList遍歷普通for====");
        for (int i = 0; i < linkedList.size(); i++) {
            System.out.println(linkedList.get(i));
        }
    }
}
 //老韓源碼閱讀.
           1. LinkedList linkedList = new LinkedList();
              public LinkedList() {}
           2. 這時 linkeList 的屬性 first = null  last = null
           3. 執行 添加
               public boolean add(E e) {
                    linkLast(e);
                    return true;
                }
            4.將新的結點,加入到雙向鏈表的最后
             void linkLast(E e) {
                final Node<E> l = last;
                final Node<E> newNode = new Node<>(l, e, null);
                last = newNode;
                if (l == null)
                    first = newNode;
                else
                    l.next = newNode;
                size++;
                modCount++;
            }

// 老韓讀源碼 linkedList.remove(); // 這里默認刪除的是第一個結點

          1. 執行 removeFirst()方法
            public E remove() {
                return removeFirst();  // 很明確了,默認刪除第一個結點
            }
         2. 執行
            public E removeFirst() {
                final Node<E> f = first;
                if (f == null)
                    throw new NoSuchElementException();
                return unlinkFirst(f);
            }
          3. 執行 unlinkFirst, 將 f 指向的雙向鏈表的第一個結點拿掉(源碼還是很好理解的)
            private E unlinkFirst(Node<E> f) {
                // assert f == first && f != null;
                final E element = f.item;
                final Node<E> next = f.next;
                f.item = null;
                f.next = null; // help GC
                first = next;
                if (next == null)
                    last = null;
                else
                    next.prev = null;
                size--;
                modCount++;
                return element;
            }
  1. 底層是雙鏈表
  2. remove()無參方法,默認是刪除第一個元素,即first指向的元素
  3. LinkedList實現了List接口,所以跟其余List實現子類一樣,有三種遍歷循環方法
4.5.3 ArrayList和LinkedList比較

需要注意的是,LinkedList與ArrayList一樣,都是線程不安全的,沒有實現同步,多線程時要慎用。遍歷時,最好使用迭代器進行遍歷,有modCount進行修改記錄,防止發生異常

4.6 Set接口方法

4.6.1 Set接口常用方法
public class SetMethod {
    public static void main(String[] args) {
        //老韓解讀
        //1. 以Set 接口的實現類 HashSet 來講解Set 接口的方法
        //2. set 接口的實現類的對象(Set接口對象), 不能存放重復的元素, 可以添加一個null
        //3. set 接口對象存放數據是  無序  (即添加的順序和取出的順序不一致)
        //4. 注意:取出的順序的順序雖然不是添加的順序,但是取出的順序是固定的.
        Set set = new HashSet();
        set.add("john");
        set.add("lucy");
        set.add("john");//重復
        set.add("jack");
        set.add("hsp");
        set.add("mary");
        set.add(null);//
        set.add(null);//再次添加null
        for(int i = 0; i <10;i ++) {
            System.out.println("set=" + set);  // 結果:set=[null, hsp, mary, john, lucy, jack],取出順序是固定的
        }

        //遍歷
        //方式1: 使用迭代器
        System.out.println("=====使用迭代器====");
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object obj =  iterator.next();
            System.out.println("obj=" + obj);
        }

        set.remove(null);

        //方式2: 增強for(底層是迭代器)
        System.out.println("=====增強for====");
        for (Object o : set) {
            System.out.println("o=" + o);
        }

        //set 接口對象,不能通過索引來獲取(沒有get()方法)

    }
}

注意點:

  1. 不能存放重復元素,但能存放一個null
  2. set存放的數據是無序的,但取出的順序是固定不變的
  3. 只能用迭代器和增強for循環進行遍歷,不能使用普通for,因為Set底層是HashMap,結構復雜(數組+單鏈表)
4.6.2 HashSet
4.6.2.1 HashSet全面說明
        //說明
        //1. 在執行add方法后,會返回一個boolean值
        //2. 如果添加成功,返回 true, 否則返回false
        //3. 可以通過 remove 指定刪除哪個對象
        System.out.println(set.add("john"));//T
        System.out.println(set.add("lucy"));//T
        System.out.println(set.add("john"));//F
        System.out.println(set.add("jack"));//T
        System.out.println(set.add("Rose"));//T


        set.remove("john");
        System.out.println("set=" + set);//3個

        // 重置一下set
        set  = new HashSet();
        System.out.println("set=" + set);//0
        //4 Hashset 不能添加相同的元素/數據?
        set.add("lucy");//添加成功
        set.add("lucy");//加入不了
        set.add(new Dog("tom"));//OK
        set.add(new Dog("tom"));//Ok
        System.out.println("set=" + set);  // set=[Cat{name='tom'}, lucy, Cat{name='tom'}]

        //在加深一下. 非常經典的面試題.
        //看源碼,做分析, 先給小伙伴留一個坑,以后講完源碼,你就了然
        //去看他的源碼,即 add 到底發生了什么?=> 底層機制.
        set.add(new String("hsp"));//ok
        set.add(new String("hsp"));//加入不了.
        System.out.println("set=" + set);  // set=[hsp, Cat{name='tom'}, lucy, Cat{name='tom'}]
4.6.2.2 HashSet底層機制說明

底層是HashMap(數組+單鏈表+紅黑樹)

  1. HashSet擴容機制

equals方法是可以由程序員進行重寫的,也就是是比較對象還是比較內容是可以決定的

一條鏈表的元素個數到達了8個且整個table表的大小達到了64,就會對這條鏈表進行樹化(紅黑樹),如果table表大小沒有達到64,就會對table表進行按兩倍擴容(table數組表)

  1. 源碼解讀(復雜且重要)

    看代碼注釋!

// Debug程序
HashSet hashSet = new HashSet();
        hashSet.add("java");//到此位置,第1次add分析完畢.
        hashSet.add("php");//到此位置,第2次add分析完畢
        hashSet.add("java");
        System.out.println("set=" + hashSet);
老韓對HashSet 的源碼解讀
        1. 執行 HashSet()
            public HashSet() {
                map = new HashMap<>();  // 可以看出來HashSet底層是由HashMap來實現的
            }
        2. 執行 add()
           public boolean add(E e) {//e = "java"
                return map.put(e, PRESENT)==null;//(static) PRESENT = new Object();
           }
         3.執行 put() , 該方法會執行 hash(key) 得到key對應的hash值 算法:h = key.hashCode()) ^ (h >>> 16)
             public V put(K key, V value) {//key = "java" value = PRESENT 共享
                return putVal(hash(key), key, value, false, true);
            }
         4.執行 putVal

注意:

  1. 哈希值的計算,並不是完全等價於hashCode

    key的哈希值拿去hashCode()得出新的hash值,從而找到在table中的索引位置,即:新hash值的計算,並不是直接返回的key.hashCode(),而是將hashCode()值與自身的高16位進行異或運算

    h >>> 16是用來取出h的高16,(>>>是無符號右移)

    原因:

    由於和(length-1)運算,length 絕大多數情況小於2的16次方。所以始終是hashcode 的低16位(甚至更低)參與運算。要是高16位也參與運算,會讓得到的下標更加散列。

    所以這樣高16位是用不到的,如何讓高16也參與運算呢。所以才有hash(Object key)方法。讓他的hashCode()和自己的高16位^運算。所以(h >>> 16)得到他的高16位與hashCode()進行^運算。

  2. 執行部分:最核心的代碼(相當於在講HashMap)P24

    4.執行 putVal
    /**
     Params:
        hash – hash for key (經過hash()方法計算后的新hash值,hashCode與其自身高16位的異或運算)
        key – the key ("java",數據)
        value – the value to put (PRESENT 共享,用來占位)
    */
             final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
                    Node<K,V>[] tab; Node<K,V> p; int n, i; //定義了輔助變量
        
                    //table 就是 HashMap 的一個數組,類型是 Node[]
                    //if 語句表示如果當前table 是null, 或者 大小=0
                    //就是第一次擴容,到16個空間.
                    if ((tab = table) == null || (n = tab.length) == 0)
                        n = (tab = resize()).length;  // resize()方法是擴容方法
        
                    //(1)根據key,得到hash 去計算該key應該存放到table表的哪個索引位置
                    //並把這個位置的對象,賦給 p = tab[...]
                    //(2)判斷p 是否為null
                    //(2.1) 如果p 為null, 表示還沒有存放元素, 就創建一個Node (key="java",value=PRESENT)
                    //(2.2) 就放在該位置 tab[i] = newNode(hash, key, value, null)
                    if ((p = tab[i = (n - 1) & hash]) == null)
                        tab[i] = newNode(hash, key, value, null);
                    else {  // 如果待存放的位置,現在的當前位置已經存放了元素,執行else語句
                        //一個開發技巧提示: 在需要局部變量(輔助變量)時候,再創建
                        Node<K,V> e; K k; //
                        
                        //如果當前索引位置對應的鏈表的第一個元素和准備添加的key的hash值一樣
                        // ==判斷地址是否相同(是否為同一對象),equals判斷內容是否相同(是否兩個對象的屬性相同)
                        //並且滿足 下面兩個條件之一:
                        //(1) 准備加入的key 和 p 指向的Node 結點的 key 是同一個對象 (是否同一對象)
                        //(2)  p 指向的Node 結點的 key 的equals() 和准備加入的key比較后相同 (是否內容相同)
                        //就認為是相同的對象,不能加入(比較的是頭結點)
                        if (p.hash == hash &&  // p指向當前索引位置對應的鏈表的第一個元素
                            ((k = p.key) == key || (key != null && key.equals(k))))
                            e = p;
                        
                        //再判斷 p 是不是一顆紅黑樹,
                        //如果是一顆紅黑樹,就調用 putTreeVal , 來進行添加
                        else if (p instanceof TreeNode)
                            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                        
                        else {//如果table對應索引位置,已經是一個鏈表, 就使用for循環比較
                              // 會有下面幾種情況
                              //(1) 依次和該鏈表的每一個元素比較后,都不相同, 則加入到該鏈表的最后
                              //************************************************************
                              //    注意在把元素添加到鏈表后,立即判斷 該鏈表是否已經達到8個結點(用binCount判斷)
                              //    , 就調用 treeifyBin() 對當前這個鏈表進行樹化(轉成紅黑樹)
                              //    注意,在轉成紅黑樹時,要進行判斷, 還要要求table大小 小於 64,判斷條件:
                              //    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
                              //            resize();//對table擴容
                              //    如果上面條件成立,先table擴容.上面的條件是 treeifyBin()里面的判斷
                              //    只有上面條件不成立時,才進行轉成紅黑樹(需要table大小 大於 64,才樹化)
                              //************************************************************
                              //(2) 依次和該鏈表的每一個元素比較過程中,如果有相同情況,就直接break
    
                            // 之前比較了頭結點,現在比較鏈表其余部分;下面的循環是一個雙指針,向前移動的方式有點特別
                            for (int binCount = 0; ; ++binCount) {//源碼中死循環用for,因為for的指令比while少
                                //binCount記錄當前遍歷位置(0~7,從0開始計數)
                                if ((e = p.next) == null) {// 第一種情況,加入到鏈表最后
                                    p.next = newNode(hash, key, value, null);
                                    if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1st,因為從0計數
                                        treeifyBin(tab, hash);//如果插入后,鏈表長度超過等於8,則要進行樹化
                                    break;
                                }
                                if (e.hash == hash &&
                                    ((k = e.key) == key || (key != null && key.equals(k))))
                                    break;  // 鏈表中有一個元素與待插入元素一樣,不需要進行尾插,break結束循環
                                p = e;
                            }
                        }
                        if (e != null) { // existing mapping for key
                            V oldValue = e.value;
                            if (!onlyIfAbsent || oldValue == null)
                                e.value = value;  // 把新的值覆蓋以前的舊值,map不允許key重復,重復后就替換
                            afterNodeAccess(e);
                            return oldValue;
                        }
                    }
                    ++modCount;
                    //size 就是我們每加入一個結點Node(k,v,h,next), 就會size++
                    // threshold = 0.75 × table大小
                    if (++size > threshold)
                        resize();//達到臨界值,擴容
                    afterNodeInsertion(evict);  // 空方法,由HashMap子類實現
                    return null;
                }
    

    注釋:

    1. 第一次table數組擴容大小為什么是16:在final Node<K,V>[] resize()方法中。table 就是 HashMap 的一個數組,類型是 Node[]

DEFAULT_LOAD_FACTOR = 0.75,負載因子,用於計算table數組的臨界值newThr(帶鏈表的哈希表)作用是一旦到達臨界值后,就擴容,避免大量數據導致阻塞。舉個例子:一個班級有50個座位,為了防止將來有很多人來,當座位用到45個座位時,就加新的座位。

  1. for (int binCount = 0; ; ++binCount) {

如果table對應索引位置,已經是一個鏈表, 就使用for循環比較

HashSet底層簡要總結:

  1. 第一次擴容大小為16(默認值16,static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

  2. 超過臨界值之后,就每次按兩倍擴容:

這里需要額外注意:所謂臨界值大小,並不是指在table表中已經有可插入的位置已經達到臨界值,而是整個table表中所有插入結點的總個數達到了臨界值。關鍵代碼如下:

                //size 就是我們每加入一個結點Node(k,v,h,next), 就會size++
                // threshold = 0.75 × table大小
                if (++size > threshold)
                    resize();//達到臨界值,擴容

重要:也就是說,如果table表當前大小為16,此時表上一條鏈表上有7個元素(含頭結點),另一個鏈表上此時已經添加了5個元素,再添加一個的話,那么table表就要進行擴容,因為size大小已經到達臨界值12了,再添加一個結點,則會觸發擴容機制。:

  1. 樹化條件:

        在Java8中, 如果一條鏈表的元素個數到達 TREEIFY_THRESHOLD(默認是 8 ),
        並且table的大小 >= MIN_TREEIFY_CAPACITY(默認64),就會進行樹化(紅黑樹),
        否則仍然采用數組擴容機制
    
/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;
4.6.2.3 HashSet最佳實踐

如果是同一個對象,那么它們的HashCode一定相同,反之,HashCode相同,卻不一定是同一個對象,這也是為什么在源碼中,需要比較內容。

題目:

         定義一個Employee類,該類包含:private成員屬性name,age 要求:
         創建3個Employee 對象放入 HashSet中
         當 name和age的值相同時,認為是相同員工, 不能添加到HashSet集合中(需要重寫hashCode和equals方法)

源碼:

@SuppressWarnings({"all"})
public class HashSetExercise {
    public static void main(String[] args) {
        /**
         定義一個Employee類,該類包含:private成員屬性name,age 要求:
         創建3個Employee 對象放入 HashSet中
         當 name和age的值相同時,認為是相同員工, 不能添加到HashSet集合中
         */
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("milan", 18));//ok
        hashSet.add(new Employee("smith", 28));//ok
        hashSet.add(new Employee("milan", 18));//需要加入不成功.,不重寫的話,就能夠成功加入,因為對象不同,哈希值不同

        //回答,加入了幾個? 3個
        System.out.println("hashSet=" + hashSet);
    }
}

//創建Employee
class Employee {
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public void setAge(int age) {
        this.age = age;
    }
    
    //如果name 和 age 值相同,則返回相同的hash值
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return age == employee.age &&
                Objects.equals(name, employee.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

為了保證 相同對象不同對象但內容相同 不能新添加至HashSet,結合底層源碼putVal方法里面,需要同時滿足這兩個條件:

哈希值返回一樣,equals方法返回一樣

4.6.3 LinkedHashSet
4.6.3.1 LinkedHashSet全面說明

HashSet的子類,注意與HashSet的區別,底層最大區別就是使用了數組+雙鏈表結構,能夠保證取數據是有順序的。而HashSet是數組+單鏈表,且遍歷打印輸出是無序的,其實是按照哈希值(實際上是哈希table表)遍歷排序打印的

4.6.3.2 LinkedHashSet底層機制說明

Debug代碼:

@SuppressWarnings({"all"})
public class LinkedHashSetSource {
    public static void main(String[] args) {
        //分析一下LinkedHashSet的底層機制
        Set set = new LinkedHashSet();
        set.add(new String("AA"));
        set.add(456);
        set.add(456);
        set.add(new Customer("劉", 1001));
        set.add(123);
        set.add("HSP");

        System.out.println("set=" + set);
    }
}
class Customer {
    private String name;
    private int no;

    public Customer(String name, int no) {
        this.name = name;
        this.no = no;
    }
}

注釋:

        老韓解讀
        1. LinkedHashSet 加入順序和取出元素/數據的順序一致
        2. LinkedHashSet 底層維護的是一個LinkedHashMap(是HashMap的子類)
        3. LinkedHashSet 底層結構 (數組table+雙向鏈表)
        4. 添加第一次時,直接將 數組table 擴容到 16 ,存放的結點類型是 LinkedHashMap$Entry,而不是Node
        5. Table數組是 HashMap$Node[] ,而存放的元素/數據是 LinkedHashMap$Entry類型(其實是數組多態:子類存放至父類類型數組)
                //繼承關系是在內部類完成.
                static class Entry<K,V> extends HashMap.Node<K,V> {
                    Entry<K,V> before, after;
                    Entry(int hash, K key, V value, Node<K,V> next) {
                        super(hash, key, value, next);
                    }
                }
4.6.3.3 LinkedHashSet課堂練習
        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add(new Car("奧拓", 1000));//OK
        linkedHashSet.add(new Car("奧迪", 300000));//OK
        linkedHashSet.add(new Car("法拉利", 10000000));//OK
        linkedHashSet.add(new Car("奧迪", 300000));//ok,因為是不同的car對象
        linkedHashSet.add(new Car("保時捷", 70000000));//OK
        linkedHashSet.add(new Car("奧迪", 300000));//ok因為是不同的car對象,要只有一個奧迪,重寫equals和hash方法

    //必須重寫equals 方法 和 hashCode
    //當 name 和 price 相同時, 就返回相同的 hashCode 值, equals返回t
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return Double.compare(car.price, price) == 0 &&
                Objects.equals(name, car.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, price);
    }

跟HashSet最佳實踐一樣,也是重寫兩個方法

4.6.4 TreeSet

與HashSet最大的不同就是,可以排序(在有參構造器里面重寫compare方法),默認構造器是字符串升序排序

TreeSet的底層其實是TreeMap:鍵值對中的value使用一個靜態對象(Object類)Present進行占位,與前面的HashSet一樣

//老韓解讀
        //1. 當我們使用無參構造器,創建TreeSet時,仍然是無序的
        //2. 老師希望添加的元素,按照字符串大小來排序
        //3. 使用TreeSet 提供的一個構造器,可以傳入一個比較器(匿名內部類)
        //   並指定排序規則

        //4. 簡單看看源碼
        //老韓解讀
        /*
        1. 構造器把傳入的比較器對象,賦給了 TreeSet的底層的 TreeMap的屬性this.comparator
         public TreeMap(Comparator<? super K> comparator) {
                this.comparator = comparator;
            }
         2. 在 調用 treeSet.add("tom"), 在底層會執行到

             if (cpr != null) {//cpr 就是我們的匿名內部類(對象),即:重寫的比較規則
                do {
                    parent = t;
                    //動態綁定到我們的匿名內部類(對象)compare
                    cmp = cpr.compare(key, t.key);
                    if (cmp < 0)
                        t = t.left;
                    else if (cmp > 0)
                        t = t.right;
                    else //如果相等,即返回0,這個Key就沒有加入
                        return t.setValue(value);
                } while (t != null);
            }
         */

//        TreeSet treeSet = new TreeSet();
        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //下面 調用String的 compareTo方法進行字符串大小比較
                //如果老韓要求加入的元素,按照長度大小排序
                //return ((String) o2).compareTo((String) o1);
                return ((String) o1).length() - ((String) o2).length();
            }
        });
        //添加數據.
        treeSet.add("jack");
        treeSet.add("tom");//3
        treeSet.add("sp");
        treeSet.add("a");
        treeSet.add("abc");//3

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

加入不了相同值的元素,也不能加入NULL,因為TreeSet的輸出是需要排序的,NULL無法進行排序

4.7 Map接口(與Collection並列存在)

在上面可知,Set接口是基於Map實現的,而Map存放的是鍵值對,key-value,在Set中value是用常量對象present進行替代的,實際上只用到了key

4.7.1 Map接口全面說明

注釋:

  1. Map的輸入和輸出是無序的,原因跟Set分析一樣,是按照key的哈希值(也就是在table表中的排序輸出的)。
  2. 添加元素用put方法
  3. key不允許重復,當有相同的key時,會進行替換覆蓋
        //老韓解讀Map 接口實現類的特點, 使用實現類HashMap
        //1. Map與Collection並列存在。用於保存具有映射關系的數據:Key-Value(雙列元素)
        //2. Map 中的 key 和  value 可以是任何引用類型的數據,會封裝到HashMap$Node 對象中
        //3. Map 中的 key 不允許重復,原因和HashSet 一樣,前面分析過源碼.
        //4. Map 中的 value 可以重復
        //5. Map 的key 可以為 null, value 也可以為null ,注意 key 為null,
        //   只能有一個,value 為null ,可以多個
        //6. 常用String類作為Map的 key
        //7. key 和 value 之間存在單向一對一關系,即通過指定的 key 總能找到對應的 value
        Map map = new HashMap();
        map.put("no1", "韓順平");//k-v
        map.put("no2", "張無忌");//k-v
        map.put("no1", "張三豐");//當有相同的k , 就等價於替換.
        map.put("no3", "張三豐");//k-v
        map.put(null, null); //k-v
        map.put(null, "abc"); //等價替換
        map.put("no4", null); //k-v
        map.put("no5", null); //k-v
        map.put(1, "趙敏");//k-v
        map.put(new Object(), "金毛獅王");//k-v
        // 通過get 方法,傳入 key ,會返回對應的value
        System.out.println(map.get("no2"));//張無忌
        System.out.println("map=" + map);

Map接口最重要最復雜最核心的特點!!!!!!!

由於Map是按照鍵對值存放,且table結構復雜,不便於遍歷取值。為了方便程序員遍歷Map數據,Map將Node數據的引用(不是復制了一份)存放在了Entry中,所有的數據組成了一個EntrySet 集合(Set類型) ,該集合存放的元素的類型 Entry。這樣,我們可以通過map.values()和map.keySet()獲得key值和value值,更重要的是能夠使用Map.entry的兩個重要方法:getKey()和getValue()。

Entry是Map定義的一個內部接口,在實現Map的子類中,如HashMap,HMap的Node結點是實現了Entry接口的

        Map map = new HashMap();
        map.put("no1", "韓順平");//k-v
        map.put("no2", "張無忌");//k-v
        map.put(new Car(), new Person());//k-v

        //老韓解讀
        //1. k-v 最后是 HashMap$Node node = newNode(hash, key, value, null)
		
        //2. k-v 為了方便程序員的遍歷,還會 創建 EntrySet 集合 ,該集合存放的元素的類型 Entry, 而一個Entry
        //   對象存放的是k,v。即:EntrySet<Entry<K,V>>,實際定義:transient Set<Map.Entry<K,V>> entrySet;
		
        //3. entrySet 中, 定義的類型是 Map.Entry ,但是實際上存放的還是 HashMap$Node
        //   這是因為 static class Node<K,V> implements Map.Entry<K,V>
		//   就是Node類實現了Entry,那么這個類的對象實例就可以賦給接口類Entry
		
        //4. 當把 HashMap$Node 對象 存放到 entrySet 就方便我們的遍歷, 因為 Map.Entry 提供了兩個重要方法
        //   K getKey(); V getValue();

        Set set = map.entrySet();
        System.out.println(set.getClass());// HashMap$EntrySet
        for (Object obj : set) {
            //System.out.println(obj.getClass()); //HashMap$Node
			
            //為了從 HashMap$Node 取出k-v
            //1. 先做一個向下轉型
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "-" + entry.getValue() );
        }
4.7.2 Map接口常用方法
4.7.3 Map六大遍歷方式

注意什么時候需要將Object對象轉換為Map.Entry對象

        Map map = new HashMap();
        map.put("鄧超", "孫儷");
        map.put("王寶強", "馬蓉");
        map.put("宋喆", "馬蓉");
        map.put("劉令博", null);
        map.put(null, "劉亦菲");
        map.put("鹿晗", "關曉彤");

        //第一組: 先取出 所有的Key , 通過Key 取出對應的Value
        Set keyset = map.keySet();  // Entry里面的keySet()
        //(1) 增強for
        System.out.println("-----第一種方式-------");
        for (Object key : keyset) {
            System.out.println(key + "-" + map.get(key));  // 通過map.get(key)取得value值
        }
        //(2) 迭代器
        System.out.println("----第二種方式--------");
        Iterator iterator = keyset.iterator();
        while (iterator.hasNext()) {
            Object key =  iterator.next();
            System.out.println(key + "-" + map.get(key));
        }

        //第二組: 把所有的values取出
        Collection values = map.values();
        //這里可以使用所有的Collections使用的遍歷方法
        //(1) 增強for
        System.out.println("---取出所有的value 增強for----");
        for (Object value : values) {
            System.out.println(value);
        }
        //(2) 迭代器
        System.out.println("---取出所有的value 迭代器----");
        Iterator iterator2 = values.iterator();
        while (iterator2.hasNext()) {
            Object value =  iterator2.next();
            System.out.println(value);

        }

        //第三組: 通過EntrySet 來獲取 k-v (阿里開發規范,使用entrySet的方式遍歷map)
        //重要!!!!!
        Set entrySet = map.entrySet();// EntrySet<Map.Entry<K,V>>

        //(1) 增強for
        System.out.println("----使用EntrySet 的 for增強(第3種)----");
        for (Object entry : entrySet) {
            //將entry 轉成 Map.Entry,向下轉型
            Map.Entry m = (Map.Entry) entry;  // 轉成Map.Entry很關鍵
            System.out.println(m.getKey() + "-" + m.getValue());
        }
        //(2) 迭代器
        System.out.println("----使用EntrySet 的 迭代器(第4種)----");
        Iterator iterator3 = entrySet.iterator();  // 因為entrySet是Set類型
        while (iterator3.hasNext()) {
            Object entry =  iterator3.next();
            //System.out.println(next.getClass());//HashMap$Node -實現-> Map.Entry (getKey,getValue)
            //向下轉型 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue());
        }
4.7.4 Map課堂練習(遍歷)
/**
 * 使用HashMap添加3個員工對象,要求
 * 鍵:員工id
 * 值:員工對象
 *
 * 並遍歷顯示工資>18000的員工(遍歷方式最少兩種)
 * 員工類:姓名、工資、員工id
 */
        // 創建map
        Map map = new HashMap();
        // 添加對象
        map.put(1, new Worker(1, 15000, "張三"));
        map.put(2, new Worker(2, 48000, "李四"));
        map.put(3, new Worker(5, 38000, "王五"));

        // 第一種
        System.out.println("=========第一種方式:keyset========");
        Set set = map.keySet();
        System.out.println("工資大於18000的有:");
        for (Object o : set) {
            Worker worker = (Worker) map.get(o);
            if (worker.getSalary() > 18000) {
                System.out.println(worker.getName() + "--" + worker.getSalary());
            }
        }

        // 第二種:迭代器和增強for
        System.out.println("=========第二種方式:Entryset========");
        Set entry = map.entrySet();
        System.out.println("=========迭代器========");
        // 迭代器
        Iterator iterator = entry.iterator();
        System.out.println("工資大於18000的有:");
        while (iterator.hasNext()) {
            Map.Entry next =  (Map.Entry)iterator.next();
            Worker worker = (Worker)next.getValue();  // value是Worker對象
            if (worker.getSalary() > 18000) {
                System.out.println(worker.getName() + "--" + worker.getSalary());
            }
        }

        System.out.println("=========增強for========");
        // 增強for循環
        System.out.println("工資大於18000的有:");
        for (Object o : entry) {
            Map.Entry entry1 =  (Map.Entry)o;
            Worker worker = (Worker) entry1.getValue();
            if (worker.getSalary() > 18000) {
                System.out.println(worker.getName() + "--" + worker.getSalary());
            }
        }
4.7.5 HashMap小結
4.7.6 HashMap底層機制及源碼剖析

HashSet那節其實已經講得差不多了

4.7.6.1 底層機制
4.7.6.2 源碼分析

debug源碼:

@SuppressWarnings({"all"})
public class HashMapSource1 {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        map.put("java", 10);//ok
        map.put("php", 10);//ok
        map.put("java", 20);//替換value

        System.out.println("map=" + map);//

        /*老韓解讀HashMap的源碼+圖解
        1. 執行構造器 new HashMap()
           初始化加載因子 loadfactor = 0.75
           HashMap$Node[] table = null
        2. 執行put 調用 hash方法,計算 key的 新hash值 (h = key.hashCode()) ^ (h >>> 16)
            public V put(K key, V value) {//K = "java" value = 10
                return putVal(hash(key), key, value, false, true);
            }
         3. 執行 putVal
         final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                Node<K,V>[] tab; Node<K,V> p; int n, i;//輔助變量
                //如果底層的table 數組為null, 或者 length =0 , 就擴容到16
                if ((tab = table) == null || (n = tab.length) == 0)
                    n = (tab = resize()).length;
                //取出hash值對應的table的索引位置的Node, 如果為null, 就直接把加入的k-v
                //, 創建成一個 Node ,加入該位置即可
                if ((p = tab[i = (n - 1) & hash]) == null)
                    tab[i] = newNode(hash, key, value, null);
                else {
                    Node<K,V> e; K k;//輔助變量
                // 如果table的索引位置的key的hash相同和新的key的hash值相同,
                 // 並 滿足(table現有的結點的key和准備添加的key是同一個對象  || equals返回真)
                 // 就認為不能加入新的k-v
                    if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k))))
                        e = p;
                    else if (p instanceof TreeNode)//如果當前的table的已有的Node 是紅黑樹,就按照紅黑樹的方式處理
                        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                    else {
                        //如果找到的結點,后面是鏈表,就循環比較
                        for (int binCount = 0; ; ++binCount) {//死循環
                            if ((e = p.next) == null) {//如果整個鏈表,沒有和他相同,就加到該鏈表的最后
                                p.next = newNode(hash, key, value, null);
                                //加入后,判斷當前鏈表的個數,是否已經到8個,到8個,后
                                //就調用 treeifyBin 方法進行紅黑樹的轉換
                                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                    treeifyBin(tab, hash);
                                break;
                            }
                            if (e.hash == hash && //如果在循環比較過程中,發現有相同,就break,就只是替換value
                                ((k = e.key) == key || (key != null && key.equals(k))))
                                break;
                            p = e;
                        }
                    }
                    if (e != null) { // existing mapping for key
                        V oldValue = e.value;
                        if (!onlyIfAbsent || oldValue == null)
                            e.value = value; //替換,key對應value
                        afterNodeAccess(e);
                        return oldValue;
                    }
                }
                ++modCount;//每增加一個Node ,就size++
                if (++size > threshold[12-24-48])//如size > 臨界值,就擴容
                    resize();
                afterNodeInsertion(evict);
                return null;
            }

              5. 關於樹化(轉成紅黑樹)
              //如果table 為null ,或者大小還沒有到 64,暫時不樹化,而是進行擴容.
              //否則才會真正的樹化 -> 剪枝
              final void treeifyBin(Node<K,V>[] tab, int hash) {
                int n, index; Node<K,V> e;
                if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                    resize();
            }
         */
    }
}
4.7.7 HashTable(與HashMap同級)

HashTable線程安全!HashMap線程不安全!

底層是Entry數組

Debug代碼:

        Hashtable table = new Hashtable();//ok
        table.put("john", 100); //ok
        //table.put(null, 100); //異常 NullPointerException
        //table.put("john", null);//異常 NullPointerException
        table.put("lucy", 100);//ok
        table.put("lic", 100);//ok
        table.put("lic", 88);//替換
        table.put("hello1", 1);
        table.put("hello2", 1);
        table.put("hello3", 1);
        table.put("hello4", 1);
        table.put("hello5", 1);
        table.put("hello6", 1);
        System.out.println(table);

源碼底層解讀:

簡單說明一下Hashtable的底層
1. 底層有數組 Hashtable$Entry[] 初始化大小為 11
2. 臨界值 threshold為8: 11 * 0.75 = 8
3. 擴容: 按照自己的擴容機制來進行即可.
4. 執行 方法 addEntry(hash, key, value, index); 添加K-V 封裝到Entry
5. 當 if (count >= threshold) 滿足時,就進行擴容
5. 按照 int newCapacity = (oldCapacity << 1) + 1; 的大小擴容.(擴容機制)

自己理一下思路:HashTable底層是一個Entry數組,當添加數據時候,進入put方法,首先檢查value是否為NULL,NULL會報錯,結束;遍歷Entry數組,如果當前數據的哈希值與某一數據相同,且equals相同,則進行替換。否則,進入addEntry()方法。

2021-08-23_224740

可以看到默認初始數組大小為11,臨界值比例值為0.75

底層數組類型為Entry

synchronized也解釋了HashTable是線程安全的,圖3源碼展示了不允許value為null,設計的原因。而HashMap在put的時候會調用hash()方法來計算key的hashcode值,可以從hash算法中看出當key==null時返回的值為0

擴容機制解讀:不同於其他集合子類,其具體方法是 原來的兩倍再加1

與HashMap的對比:

HashTable的哈希值計算是直接int hash = key.hashCode();

4.7.8 Properties

IO流章節再細講!

        //老韓解讀
        //1. Properties 繼承  Hashtable
        //2. 可以通過 k-v 存放數據,當然key 和 value 不能為 null
        //增加
        Properties properties = new Properties();
        //properties.put(null, "abc");//拋出 空指針異常
        //properties.put("abc", null); //拋出 空指針異常
        properties.put("john", 100);//k-v
        properties.put("lucy", 100);
        properties.put("lic", 100);
        properties.put("lic", 88);//如果有相同的key , value被替換

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

        //通過k 獲取對應值
        System.out.println(properties.get("lic"));//88

        //刪除
        properties.remove("lic");
        System.out.println("properties=" + properties);

        //修改
        properties.put("john", "約翰");
        System.out.println("properties=" + properties);
4.7.9 TreeMap

默認按照ASCII碼升序排序TreeSet底層是TreeMap,也是這樣排序)

重寫排序規則:有參構造器中重寫Compare方法

        TreeMap treeMap = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //按照傳入的 k(String) 的大小進行排序
                //return ((String) o2).compareTo((String) o1);
                
                //按照K(String) 的長度大小排序
                return ((String) o2).length() - ((String) o1).length();
            }
        });

源碼解讀:

           老韓解讀源碼:
            1. 構造器. 把傳入的實現了 Comparator接口的匿名內部類(對象),傳給給TreeMap的comparator
             public TreeMap(Comparator<? super K> comparator) {
                this.comparator = comparator;
            }
            2. 調用put方法
            2.1 第一次添加, 把k-v 封裝到 Entry對象,放入root(根結點)
            Entry<K,V> t = root;
            if (t == null) {
                compare(key, key); // type (and possibly null) check

                root = new Entry<>(key, value, null);
                size = 1;
                modCount++;
                return null;
            }
            2.2 以后添加
            Comparator<? super K> cpr = comparator;
            if (cpr != null) {
                do { //遍歷所有的key , 給當前key找到適當位置
                    parent = t;
                    cmp = cpr.compare(key, t.key);//動態綁定到我們的匿名內部類的compare
                    if (cmp < 0)
                        t = t.left;
                    else if (cmp > 0)
                        t = t.right;
                    else  //如果遍歷過程中,發現准備添加Key 和當前已有的Key相等,就不添加
                        return t.setValue(value);
                } while (t != null);
            }

需要注意一點的是:無論是在TreeSet,還是在TreeMap,當我們重寫了Compare方法為比較字符串長度,如果有兩個相同字符串長度的值需要添加,第一個添加成功后,第二個是無法添加進去的,而不是像之前的集合子類是進行替換覆蓋的,換言之就是底層不是進行替換的,而是直接返回舊的值。源碼中,當cmp返回為0時,此時兩個對象的比較是在某種規則下是相同的,此時在do while循環中,是執行else語句返回舊值。

例如:按照比較字符串長度進行排序輸出的規則下,第三個鍵值對就無法添加進去,因為已經有tom了

        treeMap.put("jack", "傑克");
        treeMap.put("tom", "湯姆");
        treeMap.put("hsp", "韓順平");//加入不了

4.8 開發中如何選擇集合實現類(記住)

4.9 Collections工具類

演示:

        //創建ArrayList 集合,用於測試.
        List list = new ArrayList();
        list.add("tom");
        list.add("smith");
        list.add("king");
        list.add("milan");
        list.add("tom");

//        reverse(List):反轉 List 中元素的順序
        Collections.reverse(list);
        System.out.println("list=" + list);
//        shuffle(List):對 List 集合元素進行隨機排序
//        for (int i = 0; i < 5; i++) {
//            Collections.shuffle(list);
//            System.out.println("list=" + list);
//        }

//        sort(List):根據元素的自然順序對指定 List 集合元素按升序排序
        Collections.sort(list);
        System.out.println("自然排序后");
        System.out.println("list=" + list);
//        sort(List,Comparator):根據指定的 Comparator 產生的順序對 List 集合元素進行排序
        //我們希望按照 字符串的長度大小排序
        Collections.sort(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //可以加入校驗代碼.
                return ((String) o2).length() - ((String) o1).length();
            }
        });
        System.out.println("字符串長度大小排序=" + list);
        //比如
        Collections.swap(list, 0, 1);
        System.out.println("交換后的情況");
        System.out.println("list=" + list);

        //Object max(Collection):根據元素的自然順序,返回給定集合中的最大元素
        System.out.println("自然順序最大元素=" + Collections.max(list));
        //Object max(Collection,Comparator):根據 Comparator 指定的順序,返回給定集合中的最大元素
        //比如,我們要返回長度最大的元素
        Object maxObject = Collections.max(list, new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return ((String)o1).length() - ((String)o2).length();
            }
        });
        System.out.println("長度最大的元素=" + maxObject);


        //Object min(Collection)
        //Object min(Collection,Comparator)
        //上面的兩個方法,參考max即可

        //int frequency(Collection,Object):返回指定集合中指定元素的出現次數
        System.out.println("tom出現的次數=" + Collections.frequency(list, "tom"));

        //void copy(List dest,List src):將src中的內容復制到dest中

        ArrayList dest = new ArrayList();
        //為了完成一個完整拷貝,我們需要先給dest 賦值,大小和list.size()一樣
        for(int i = 0; i < list.size(); i++) {
            dest.add("");
        }
        //拷貝
        Collections.copy(dest, list);
        System.out.println("dest=" + dest);

        //boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替換 List 對象的所有舊值
        //如果list中,有tom 就替換成 湯姆
        Collections.replaceAll(list, "tom", "湯姆");
        System.out.println("list替換后=" + list);

4.10 本章作業

4.10.1 作業1
public class HomeWork {
    public static void main(String[] args) {
        List list = new ArrayList();

        // 添加新聞對象
        list.add(new News("新冠確診病例超千萬,數百萬印度教信徒赴恆河\"聖浴\"引民眾擔憂"));
        list.add(new News("男子突然想起2個月前釣的魚還在網兜里,撈起一看趕緊放生"));

        // 要進行倒序遍歷,最好使用普通for循環
        for (int i = list.size() - 1; i >= 0; i--) {
            News news = (News) list.get(i);
            if (news.getTitles().length() > 15) {
                System.out.println(news.getTitles().substring(0, 15) + "...");
            } else {
                System.out.println(news.getTitles());
            }
        }
    }
}

class News {
    private String titles;
    private String news;

    public News(String titles) {
        this.titles = titles;
    }

    @Override
    public String toString() {
        return "News{" +
                "titles='" + titles + '\'' + '}';
    }

    public String getTitles() {
        return titles;
    }

    public void setTitles(String titles) {
        this.titles = titles;
    }

    public String getNews() {
        return news;
    }

    public void setNews(String news) {
        this.news = news;
    }
}

/**
男子突然想起2個月前釣的魚還在...
新冠確診病例超千萬,數百萬印度...
*/
4.10.2 作業2
public class Homework02 {
    public static void main(String[] args) {

        ArrayList arrayList = new ArrayList();
        Car car = new Car("寶馬", 400000);
        Car car2 = new Car("賓利",5000000);
        //1.add:添加單個元素
        arrayList.add(car);
        arrayList.add(car2);
        System.out.println(arrayList);
        //* 2.remove:刪除指定元素
        arrayList.remove(car);
        System.out.println(arrayList);
        //* 3.contains:查找元素是否存在
        System.out.println(arrayList.contains(car));//F
        //* 4.size:獲取元素個數
        System.out.println(arrayList.size());//1
        //* 5.isEmpty:判斷是否為空
        System.out.println(arrayList.isEmpty());//F
        //* 6.clear:清空
        //System.out.println(arrayList.clear(););
        //* 7.addAll:添加多個元素
        System.out.println(arrayList);
        arrayList.addAll(arrayList);//2個賓利
        System.out.println(arrayList);
        //* 8.containsAll:查找多個元素是否都存在
        arrayList.containsAll(arrayList);//T
        //* 9.removeAll:刪除多個元素
        //arrayList.removeAll(arrayList); //相當於清空
        //* 使用增強for和 迭代器來遍歷所有的car , 需要重寫 Car 的toString方法

        for (Object o : arrayList) {
            System.out.println(o);//
        }
        System.out.println("===迭代器===");
        Iterator iterator = arrayList.iterator();
        while (iterator.hasNext()) {
            Object next =  iterator.next();
            System.out.println(next);
        }
    }
}
4.10.3 作業3
public class HomeWork03 {
    public static void main(String[] args) {
//         * 按要求完成下列任務
//         * 1)使用HashMap類實例化一個Map類型的對象m,鍵(String)和值(int)分別用於存儲員工的姓名和工資,
        Map m = new HashMap();
//         * 存入數據如下:	jack—650元;tom—1200元;smith——2900元;
        m.put("jack", 650);//int->Integer
        m.put("tom", 1200);//int->Integer
        m.put("smith", 2900);//int->Integer
        System.out.println(m);
//         * 2)將jack的工資更改為2600元
        m.replace("jack", 2600);
        System.out.println(m);
//         * 3)為所有員工工資加薪100元;
        Set set = m.keySet();
        for (Object o : set) {
            m.put(o, (Integer)m.get(o) + 100);
        }
        System.out.println(m);
//         * 4)遍歷集合中所有的員工
        System.out.println("使用entry遍歷:");
        Set set1 = m.entrySet();
        for (Object o : set1) {
            Map.Entry entry01 = (Map.Entry) o;
            System.out.print(entry01.getKey() + "---" + entry01.getValue() + " ");
        }
        System.out.println();
//         * 5)遍歷集合中所有的工資
        Collection values = m.values();
        for (Object value : values) {
            System.out.println(value);
        }
    }
}

/**
{tom=1200, smith=2900, jack=650}
{tom=1200, smith=2900, jack=2600}
{tom=1300, smith=3000, jack=2700}
使用entry遍歷:
tom---1300 smith---3000 jack---2700 
1300
3000
2700
*/
4.10.4 作業4

代碼分析題

對TreeSet去重機制的解讀:

當創建一個新的TreeSet時,實際上是新的TreeMap。而在TreeMap底層,若不傳入一個重寫的比較器對象的話,那么底層會調用添加對象本身實現的比較器,涉及到這塊的源碼如下:

當我們傳入了比較器,代碼就會運行if那段,調用重寫的compare方法進行比較,然后將值返回給cmp,進行下一步處理,如果是0,說明有舊值跟當前插入對象相同,就不進行插入操作。

如果是無參構造器的話!就直接調用插入對象自己的compareTo()方法:如下,當我們插入是String對象時,就調用String對象實現的compareTo()方法

        treeSet.add("jack");
        treeSet.add("tom");
        treeSet.add("sp");
        treeSet.add("a");

例子:下面的操作會報錯!

@SuppressWarnings({"all"})
public class Homework05 {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();
        //分析源碼
        //add 方法,因為 TreeSet() 構造器沒有傳入Comparator接口的匿名內部類
        //所以在底層 Comparable<? super K> k = (Comparable<? super K>) key;
        //即 把 Perosn轉成 Comparable類型
        treeSet.add(new Person());//報錯!ClassCastException. Person對象沒有實現Comparator接口的比較器方法
    }
}
class Person{}
@SuppressWarnings({"all"})
public class Homework05 {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();

        treeSet.add(new Person());  // 成功添加
        treeSet.add(new Person());  // 添加失敗
        treeSet.add(new Person());  // 添加失敗
        System.out.println(treeSet);//因為compareTo()方法均返回0,在TreeMap底層,只能成功添加一個
    }
}

class Person implements Comparable{
    @Override
    public int compareTo(Object o) {
        return 0;
    }
}

4.10.5 作業5

代碼分析題

注解:

remove操作會失敗,因為remove會通過p1的哈希值來確定p1在table表中的位置,然而p1已經發生改變,重新定位不到索引為1的老p1。此時打印出來有兩個元素:p1(1001CC)和p2。進入到add方法,此時1001CC放在索引為3的位置(並不是P1的位置,因為P1原來的AA被修改為CC,但位置並沒有發生改變)。打印元素有:p1、p2、1001CC。再次添加1001AA,定位到索引為1的位置,然而p1發生了改變,重寫的equals和hashCode方法都不能將這兩個元素判定為相等,所以1001AA掛載在p1的位置。最后打印,有4個元素:p1、1001AA、p2、1001CC。

@SuppressWarnings({"all"})
public class Homework06 {
    public static void main(String[] args) {
        HashSet set = new HashSet();//ok
        Person p1 = new Person(1001,"AA");//ok
        Person p2 = new Person(1002,"BB");//ok
        set.add(p1);//ok
        set.add(p2);//ok
        p1.name = "CC";
        set.remove(p1);
        System.out.println(set);//2
        set.add(new Person(1001,"CC"));
        System.out.println(set);//3
        set.add(new Person(1001,"AA"));
        System.out.println(set);//4

    }
}

class Person {
    public String name;
    public int id;

    public Person(int id, String name) {
        this.name = name;
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, id);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", id=" + id +
                '}';
    }
}

/**
[Person{name='BB', id=1002}, Person{name='CC', id=1001}]
[Person{name='BB', id=1002}, Person{name='CC', id=1001}, Person{name='CC', id=1001}]
[Person{name='BB', id=1002}, Person{name='CC', id=1001}, Person{name='CC', id=1001}, Person{name='AA', id=1001}]
*/

第五章 面向對象編程(高級部分) 只記重點,其余看PDF

5.1 類變量和類方法(static,/靜態變量和靜態方法)

5.1.1 類變量/靜態變量

static變量是同一個類所有對象共享的,此外,static類變量,在類加載的時候就已經生成了,即類變量是隨着類的加載而創建,即使是沒有創建對象實例也可以訪問。也隨着類的消亡而銷毀。

訪問類變量和類方法的時候,要遵循訪問權限限制

5.1.2 類方法/靜態方法

  1. 理解:因為靜態變量和靜態方法是隨着類的加載就加載初始化好了,而普通變量和方法此時並沒有進行加載。

課堂作業:

5.2 深入理解main方法

public class Main01 {

    //靜態的變量/屬性
    private static  String name = "韓順平教育";
    //非靜態的變量/屬性
    private int n1 = 10000;

    //靜態方法
    public static  void hi() {
        System.out.println("Main01的 hi方法");
    }
    //非靜態方法
    public void cry() {
        System.out.println("Main01的 cry方法");
    }

    public static void main(String[] args) {
        //可以直接使用 name
        //1. 靜態方法main 可以訪問本類的靜態成員
        System.out.println("name=" + name);
        hi();
        //2. 靜態方法main 不可以訪問本類的非靜態成員
        //System.out.println("n1=" + n1);//錯誤
        //cry();
        //3. 靜態方法main 要訪問本類的非靜態成員,需要先創建對象 , 再調用即可
        Main01 main01 = new Main01();
        System.out.println(main01.n1);//ok
        main01.cry();
    }
}

5.3 代碼塊*

5.3.1 代碼塊的基本介紹

做初始化的操作

代碼塊調用的順序優先於構造器,不管調用哪個構造器創建對象,都會先調用代碼塊的內容

5.3.2 代碼塊的注意事項和細節討論*

類只會加載一次,但可以實例化出多個對象。

public class CodeBlockDetail01 {
    public static void main(String[] args) {
        //類被加載的情況舉例
        //1. 創建對象實例時(new)
        // AA aa = new AA();
        //2. 創建子類對象實例,父類也會被加載, 而且,父類先被加載,子類后被加載
        // AA aa2 = new AA();
        //3. 使用類的靜態成員時(靜態屬性,靜態方法)
        // System.out.println(Cat.n1);

        //static代碼塊,是在類加載時,執行的,而且只會執行一次.
//        DD dd = new DD();
//        DD dd1 = new DD();

        // 普通的代碼塊,在創建對象實例時,會被隱式的調用。
        // 被創建一次,就會調用一次。
        // 如果只是使用類的靜態成員時,普通代碼塊並不會執行
        System.out.println(DD.n1);//8888, 靜態模塊塊一定會執行
    }
}
class DD {
    public static int n1 = 8888;//靜態屬性
    //靜態代碼塊
    static {
        System.out.println("DD 的靜態代碼1被執行...");//
    }
    //普通代碼塊, 在new 對象時,被調用,而且是每創建一個對象,就調用一次
    //可以這樣簡單的,理解 普通代碼塊是構造器的補充
    {
        System.out.println("DD 的普通代碼塊...");
    }
}
class Animal {
    //靜態代碼塊
    static {
        System.out.println("Animal 的靜態代碼1被執行...");//
    }
}
class Cat extends Animal {
    public static  int n1 = 999;//靜態屬性
    //靜態代碼塊
    static {
        System.out.println("Cat 的靜態代碼1被執行...");//
    }
}
class BB {
    //靜態代碼塊
    static {
        System.out.println("BB 的靜態代碼1被執行...");//1
    }
}
class AA extends BB {
    //靜態代碼塊
    static {
        System.out.println("AA 的靜態代碼1被執行...");//2
    }
}

很好記憶,靜態跟類加載有關,所以最先執行。

執行順序:靜態屬性和代碼塊 > 普通屬性和代碼塊 >構造器 這里沒方法的事情

public class CodeBlockDetail02 {
    public static void main(String[] args) {
        A a = new A();// (1) A 靜態代碼塊01 (2) getN1被調用...(3)A 普通代碼塊01 (4)getN2被調用... (5)A() 構造器被調用
    }
}

class A {
    { //普通代碼塊
        System.out.println("A 普通代碼塊01");
    }
    private int n2 = getN2();//普通屬性的初始化

    static { //靜態代碼塊
        System.out.println("A 靜態代碼塊01");
    }

    //靜態屬性的初始化
    private static  int n1 = getN1();

    public static int getN1() {
        System.out.println("getN1被調用...");
        return 100;
    }
    public int getN2() { //普通方法/非靜態方法
        System.out.println("getN2被調用...");
        return 200;
    }
    //無參構造器
    public A() {
        System.out.println("A() 構造器被調用");
    }

}

構造器中隱藏的super和代碼塊結構一定要記住

這里就解釋了為什么普通代碼塊執行順序在構造器前面其實是進入構造器后先執行super和普通代碼塊,完了再執行構造器的內容。

注意構造器中隱式調用的super()方法,這導致父類的初始化操作總是比子類同樣的初始化操作要先執行。

課堂練習:

5.4 單例設計模式(設計模式內容)

  1. 餓漢式

示例代碼:

  1. 餓漢式(還沒使用類的實例,就提前創建好了一個實例)
public class SingleTon01 {

    public static void main(String[] args) {
//        GirlFriend xh = new GirlFriend("小紅");
//        GirlFriend xb = new GirlFriend("小白");

        //通過方法可以獲取對象
        GirlFriend instance = GirlFriend.getInstance();
        System.out.println(instance);

        GirlFriend instance2 = GirlFriend.getInstance();
        System.out.println(instance2);

        System.out.println(instance == instance2);//T
    }
}

//有一個類, GirlFriend
//只能有一個女朋友
class GirlFriend {

    private String name;
    //public static  int n1 = 100;
    
    //為了能夠在靜態方法中,返回 gf對象,需要將其修飾為static
    //對象,通常是重量級的對象, 餓漢式可能造成創建了對象,但是沒有使用.
    private static GirlFriend gf = new GirlFriend("小紅紅");

    //如何保障我們只能創建一個 GirlFriend 對象
    //步驟[單例模式-餓漢式](類的實例還沒使用,就提前創建成功)
    //1. 將構造器私有化(防止在類的外部創建實例)
    //2. 在類的內部直接創建對象(該對象是static)
    //3. 提供一個公共的static方法,返回 gf對象
    private GirlFriend(String name) {
        System.out.println("構造器被調用.");
        this.name = name;
    }
    public static GirlFriend getInstance() {
        return gf;

    }
    @Override
    public String toString() {
        return "GirlFriend{" +
                "name='" + name + '\'' +
                '}';
    }
}
  1. 懶漢式:使用的時候才創建實例
//希望在程序運行過程中,只能創建一個Cat對象
//使用單例模式
class Cat {
    private String name;
    private static Cat cat ; //默認是null

    //步驟
    //1.仍然構造器私有化
    //2.定義一個static靜態屬性對象
    //3.提供一個public的static方法,可以返回一個Cat對象
    //4.懶漢式,只有當用戶使用getInstance時,才返回cat對象, 後面再次調用時,會返回上次創建的cat對象
    //  從而保證了單例
    private Cat(String name) {
        System.out.println("構造器調用...");
        this.name = name;
    }
    public static Cat getInstance() {

        if(cat == null) {//如果還沒有創建cat對象
            cat = new Cat("小可愛");
        }
        return cat;
    }
}

5.5 final關鍵字

  1. 如果我們用final去修飾靜態屬性的時候,若初始化位置在構造器中,此時當類加載時,構造器還沒有被調用,因此這樣的初始化是錯誤的。

    注意觀察TAX_RATE2和3的加載時機,結合上面提到的注意事項,就能明白為什么final修飾的靜態屬性不能放在構造器中初始化

  1. 因為此時整個類已經不能被繼承,既然不能被繼承,那類中的方法自然不能被重寫/覆蓋。

  2. static final修飾的變量如果是基本數據類型或String,就不會進行類加載,而是直接獲取數據值。

結果:10000

如果去掉final,打印結果將會是:

BBB 靜態代碼塊被執行

10000

課堂作業:

5.6 抽象類

當父類的一些方法不能確定時,可以用abstract關鍵字來修飾該方法,這個方法就是抽象方法,用abstract來修飾該類就是抽象類。

5.6.1 引出抽象類

    //思考:這里eat 這里你實現了,其實沒有什么意義
    //即: 父類方法不確定性的問題
    //===> 考慮將該方法設計為抽象(abstract)方法
    //===> 所謂抽象方法就是沒有實現的方法
    //===> 所謂沒有實現就是指,沒有方法體
    //===> 當一個類中存在抽象方法時,需要將該類聲明為abstract類
    //===> 一般來說,抽象類會被繼承,有其子類來實現抽象方法.
//    public void eat() {
//        System.out.println("這是一個動物,但是不知道吃什么..");
//    }
    public abstract void eat()  ;  // 抽象類不能有方法體

5.6.2 抽象類的介紹

5.6.3 抽象類的細節

  1. 注意!只要有抽象方法,這個類就要用abstract聲明!
  2. 只能修飾類和方法!!!!!!
  1. 這一點很重要!因為如果被private、final、static修飾,那么子類是沒有機會去重寫這個抽象方法,與子類實現抽象類是相違背的

5.6.4 抽象類最佳實踐—模板設計模式

P401 講得不錯,設計理念

設計精髓:共同代碼提取,不同代碼抽象

主方法:

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

        AA aa = new AA();
        aa.calculateTime(); //這里還是需要有良好的OOP基礎,對多態的動態綁定機制

        BB bb = new BB();
        bb.calculateTime(); //根據運行類型,calculateTime()方法中job方法會進入到對應的類中——>動態綁定機制
    }
}

模板:動態綁定機制和抽象類結合起來了!

abstract public class Template { //抽象類-模板設計模式

    public abstract void job();//抽象方法

    public void calculateTime() {//實現方法,調用job方法
        //得到開始的時間
        long start = System.currentTimeMillis();
        job(); //動態綁定機制
        //得的結束的時間
        long end = System.currentTimeMillis();
        System.out.println("任務執行時間 " + (end - start));
    }
}

A類:

public class AA extends Template {
    //計算任務
    //1+....+ 800000
    @Override
    public void job() { //實現Template的抽象方法job
        long num = 0;
        for (long i = 1; i <= 800000; i++) {
            num += i;
        }
    }
//    public void job2() {
//        //得到開始的時間
//        long start = System.currentTimeMillis();
//        long num = 0;
//        for (long i = 1; i <= 200000; i++) {
//            num += i;
//        }
//        //得的結束的時間
//        long end = System.currentTimeMillis();
//        System.out.println("AA 執行時間 " + (end - start));
//    }
}

B類:

public class BB extends Template{
    public void job() {//這里也去,重寫了Template的job方法
        long num = 0;
        for (long i = 1; i <= 80000; i++) {
            num *= i;
        }

    }
}

5.7 接口*

5.7.1 接口的基本介紹

接口是對抽象類的極致抽象

加一點:在接口中abstract、public關鍵字都可以省略

允許添加的方法有三種:抽象、默認、靜態

    //寫屬性
    public int n1 = 10;

    //寫方法
    //在接口中,抽象方法,可以省略abstract關鍵字
    public void hi();
    //在jdk8后,可以有默認實現方法,需要使用default關鍵字修飾
    default public void ok() {
        System.out.println("ok ...");
    }
    //在jdk8后, 可以有靜態方法
    public static void cry() {
        System.out.println("cry ....");
    }

5.7.2 接口的注意事項和細節

  1. 接口中,方法是默認public和abstract的,所以可以不用顯示修飾出來,但記住不能加方法體。
  1. Java中類是單繼承的,但是接口是可以實現多個。

  2. 屬性是final static類型int n1 = 10; //等價 public static final int n1 = 10;

5.7.3 課堂練習

對抽象的個人理解:

其實可以把接口理解為抽象且有約束的父類,繼承關系中的特殊例子,抽象體現在其父類(接口中)方法全部為抽象方法,約束體現在方法均被public abstract修飾符修飾、屬性被public static final修飾。

5.7.4 接口和繼承的區別

//繼承
//小結:  當子類繼承了父類,就自動的擁有父類的功能
//      如果子類需要擴展功能,可以通過實現接口的方式擴展.
//      可以理解 實現接口 是 對java 單繼承機制的一種補充.

代碼解耦:接口規范性+動態綁定機制

5.7.5 接口的多態特性

  1. 接口的多態參數:舉個例子,很重要,幫助理解接口多態和繼承多態

    public class InterfacePolyParameter {
        public static void main(String[] args) {
            //接口的多態體現
            //接口類型的變量 if01 可以指向 實現了IF接口類的對象實例
            IF if01 = new Monster();
            if01 = new Car();
    
            //繼承體現的多態
            //父類類型的變量 a 可以指向 繼承AAA的子類的對象實例
            AAA a = new BBB();
            a = new CCC();
        }
    }
    // 接口多態
    interface IF {}
    class Monster implements IF{}
    class Car implements  IF{}
    
    // 繼承多態
    class AAA {}
    class BBB extends AAA {}
    class CCC extends AAA {}
    
  2. 接口的多態數組:動態綁定機制,接口數組能夠根據運行類型自動地調用相關類的方法

public class InterfacePolyArr {
    public static void main(String[] args) {
        //多態數組 -> 接口類型數組
        Usb[] usbs = new Usb[2];
        usbs[0] = new Phone_();
        usbs[1] = new Camera_();
        /*
        給Usb數組中,存放 Phone  和  相機對象,Phone類還有一個特有的方法call(),
        請遍歷Usb數組,如果是Phone對象,除了調用Usb 接口定義的方法外,
        還需要調用Phone 特有方法 call
         */
        for(int i = 0; i < usbs.length; i++) {
            usbs[i].work();//動態綁定..根據當前實現接口的對象調用所屬的work方法
            //和前面一樣,我們仍然需要進行類型的向下轉型
            if(usbs[i] instanceof Phone_) {//判斷他的運行類型是 Phone_
                ((Phone_) usbs[i]).call();
            }
        }

    }
}

interface Usb{
    void work();
}
class Phone_ implements Usb {
    public void call() {
        System.out.println("手機可以打電話...");
    }
    @Override
    public void work() {
        System.out.println("手機工作中...");
    }
}
class Camera_ implements Usb {
    @Override
    public void work() {
        System.out.println("相機工作中...");
    }
}
  1. 接口的多態傳遞現象:其實就是接口繼承接口
public class InterfacePolyPass {
    public static void main(String[] args) {
        //接口類型的變量可以指向,實現了該接口的類的對象實例
        IG ig = new Teacher();
        //如果IG 繼承了 IH 接口,而Teacher 類實現了 IG接口
        //那么,實際上就相當於 Teacher 類也實現了 IH接口.
        //這就是所謂的 接口多態傳遞現象.
        IH ih = new Teacher();
    }
}

interface IH {}
interface IG extends IH {}
class Teacher implements IG {}

5.7.6 課堂練習

x的指向不明確,父類與接口是同優先級的

        //System.out.println(x); //錯誤,原因不明確x
        //可以明確的指定x
        //訪問接口的 x 就使用 A.x
        //訪問父類的 x 就使用 super.x
        System.out.println(A.x + " " + super.x);

5.7.7 類的小結

類的五大成員還差內部類

5.8 內部類

5.8.1 四種內部類

注解:第一點首先根據內部類所在位置進行一個大致判斷,是在字段位置(成員位置)上還是局部位置(如方法內)位置上

匿名內部類很重要!!

5.8.2 局部內部類

示例代碼:

public class LocalInnerClass {//
    public static void main(String[] args) {
        //演示一遍
        Outer02 outer02 = new Outer02();
        outer02.m1();
        System.out.println("outer02的hashcode=" + outer02);
    }
}


class Outer02 {//外部類
    private int n1 = 100;
    private void m2() {
        System.out.println("Outer02 m2()");
    }//私有方法
    public void m1() {//方法
        //1.局部內部類是定義在外部類的局部位置,通常在方法
        //3.不能添加訪問修飾符,但是可以使用final 修飾
        //4.作用域 : 僅僅在定義它的方法或代碼塊中
        
        final class Inner02 {//局部內部類(本質仍然是一個類)
            //2.可以直接訪問外部類的所有成員,包含私有的
            private int n1 = 800;
            public void f1() {
                //5. 局部內部類可以直接訪問外部類的成員,比如下面 外部類n1 和 m2()
                //7. 如果外部類和局部內部類的成員重名時,默認遵循就近原則,如果想訪問外部類的成員,
                //   使用 外部類名.this.成員)去訪問
                //   老韓解讀 Outer02.this 本質就是外部類的對象, 即哪個對象調用了m1, Outer02.this就是哪個對象
                System.out.println("n1=" + n1 + " 外部類的n1=" + Outer02.this.n1);
                System.out.println("Outer02.this hashcode=" + Outer02.this);
                m2();
            }
        }
        
        //6. 外部類在方法中,可以創建Inner02對象,然后調用方法即可
        Inner02 inner02 = new Inner02();
        inner02.f1();
    }
}

注意事項:

  1. 因為局部內部類是在局部位置定義的,可以當做一個局部變量,因此不能用一些訪問限制符進行修飾但能用final修飾

  2. 內部類和外部類的成員變量重名后遵循就近原則,(1)訪問內部類該成員直接用、(2)訪問外部類用 外部類名.this.成員

5.8.3 匿名內部類*

注釋:匿名內部類的位置與局部內部類一樣,但匿名類沒有類名

/**
 * 演示匿名內部類的使用
 */
public class AnonymousInnerClass {
    public static void main(String[] args) {
        Outer04 outer04 = new Outer04();
        outer04.method();
    }
}

class Outer04 { //外部類
    private int n1 = 10;//屬性
    public void method() {//方法
        //基於接口的匿名內部類
        //老韓解讀
        //1. 需求: 想使用IA接口,並創建對象
        //2. 傳統方式,是寫一個類,實現該接口,並創建對象
        //3. 老韓需求是 Tiger/Dog 類只是使用一次,后面再不使用
        //4. 可以使用匿名內部類來簡化開發
        //5. tiger的編譯類型 ? IA
        //6. tiger的運行類型 ? 就是匿名內部類  Outer04$1
        /*
            我們看底層 會分配 類名 Outer04$1
            class Outer04$1 implements IA {
                @Override
                public void cry() {
                    System.out.println("老虎叫喚...");
                }
            }
         */
        //7. jdk底層在創建匿名內部類 Outer04$1,立即馬上就創建了 Outer04$1實例,並且把地址
        //   返回給 tiger
        //8. 匿名內部類使用一次,就不能再使用,但一次實例化后的對象,可以使用多次
        
        IA tiger = new IA() { // 一個匿名類,編譯類型:IA,運行類型:外部類$數字
            @Override
            public void cry() {
                System.out.println("老虎叫喚...");
            }
        };
        
        System.out.println("tiger的運行類型=" + tiger.getClass());  // Outer04$1
        tiger.cry();
        tiger.cry();
        tiger.cry();

//        IA tiger = new Tiger();  // 老方法
//        tiger.cry();

        //演示基於類的匿名內部類
        //分析
        //1. father編譯類型 Father
        //2. father運行類型 Outer04$2
        //3. 底層會創建匿名內部類
        /*
            class Outer04$2 extends Father{
                @Override
                public void test() {
                    System.out.println("匿名內部類重寫了test方法");
                }
            }
         */
        //4. 同時也直接返回了 匿名內部類 Outer04$2的對象
        //5. 注意("jack") 參數列表會傳遞給 構造器
        Father father = new Father("jack"){
            @Override
            public void test() {
                System.out.println("匿名內部類重寫了test方法");
            }
        };
        System.out.println("father對象的運行類型=" + father.getClass());//Outer04$2
        father.test();

        //基於抽象類的匿名內部類
        //需要實現全部的抽象方法
        Animal animal = new Animal(){
            @Override
            void eat() {
                System.out.println("小狗吃骨頭...");
            }
        };
        animal.eat();
    }
}

interface IA {//接口
    public void cry();
}
// 老方法
//class Tiger implements IA {
//
//    @Override
//    public void cry() {
//        System.out.println("老虎叫喚...");
//    }
//}
//class Dog implements  IA{
//    @Override
//    public void cry() {
//        System.out.println("小狗汪汪...");
//    }
//}

class Father {//類
    public Father(String name) {//構造器
        System.out.println("接收到name=" + name);
    }
    public void test() {//方法
    }
}

abstract class Animal { //抽象類
    abstract void eat();
}

注釋:

  1. 匿名類可以分為三種:基於接口的匿名內部類基於類的匿名內部類(可能會傳參數列表進行構造)基於抽象類的匿名內部類

  2. 匿名類的底層:JDK會創建一個類 外部類名稱$數字,數字按照匿名類順序從1開始,下面舉例:

    實現IA接口的匿名類,運行類型為Outer04$1,編譯類型為IA

            // 我們看底層 會分配 類名 Outer04$1
            class Outer04$1 implements IA {
                @Override
                public void cry() {
                    System.out.println("老虎叫喚...");
                }
            }
  1. 匿名內部類創建一次,就不能再使用,但其一次實例化創建出的對象還存在,可以被使用多次。

            // 匿名類只創建一次
    		IA tiger = new IA() { // 一個匿名類,編譯類型:IA,運行類型:外部類$數字
                @Override
                public void cry() {
                    System.out.println("老虎叫喚...");
                }
            };
            
            tiger.cry();
            tiger.cry();
            tiger.cry();
            
    // 三次tiger.cry()輸出都可以
    
  2. 基於類的匿名內部類(可能會傳參數列表進行構造):創建時,不要忘了方法體

            Father father = new Father("jack"){ // 方法體
                @Override
                public void test() {
                    System.out.println("匿名內部類重寫了test方法");
                }
            };
    
    		Father father = new Father("jack"); // 沒有方法體,就是類的實例化
    

注釋:需要明確:匿名內部類既是一個類的定義,同時也是本身也是一個對象,所以才有上圖兩種方法的調用。

public class AnonymousInnerClassDetail {
    public static void main(String[] args) {
        Outer05 outer05 = new Outer05();
        outer05.f1();
        //外部其他類---不能訪問----->匿名內部類
        System.out.println("main outer05 hashcode=" + outer05);
    }
}

class Outer05 {
    private int n1 = 99;
    public void f1() {
        //創建一個基於類的匿名內部類
        //不能添加訪問修飾符,因為它的地位就是一個局部變量
        //作用域 : 僅僅在定義它的方法或代碼塊中
        Person p = new Person(){
            private int n1 = 88;
            @Override
            public void hi() {
                //可以直接訪問外部類的所有成員,包含私有的
                //如果外部類和匿名內部類的成員重名時,匿名內部類訪問的話,
                //默認遵循就近原則,如果想訪問外部類的成員,則可以使用 (外部類名.this.成員)去訪問
                System.out.println("匿名內部類重寫了 hi方法 n1=" + n1 +
                        " 外部內的n1=" + Outer05.this.n1 );
                //Outer05.this 就是調用 f1的 對象
                System.out.println("Outer05.this hashcode=" + Outer05.this);
            }
        };
        p.hi();//動態綁定, 運行類型是 Outer05$1

        //也可以直接調用, 匿名內部類本身也是返回對象
        // class 匿名內部類 extends Person {}
//        new Person(){
//            @Override
//            public void hi() {
//                System.out.println("匿名內部類重寫了 hi方法,哈哈...");
//            }
//            @Override
//            public void ok(String str) {
//                super.ok(str);
//            }
//        }.ok("jack");


    }
}

class Person {//類
    public void hi() {
        System.out.println("Person hi()");
    }
    public void ok(String str) {
        System.out.println("Person ok() " + str);
    }
}
//抽象類/接口...

最佳實踐:

public class InnerClassExercise01 {
    public static void main(String[] args) {
        //當做實參直接傳遞,簡潔高效
        f1(new IL() {  //因為匿名類可以當做一個對象
            @Override
            public void show() {
                System.out.println("這是一副名畫~~...");
            }
        });
        
        //傳統方法
        f1(new Picture());
    }

    //靜態方法,形參是接口類型
    public static void f1(IL il) {
        il.show();
    }
}
//接口
interface IL {
    void show();
}
//類->實現IL => 編程領域 (硬編碼)
class Picture implements IL {
    @Override
    public void show() {
        System.out.println("這是一副名畫XX...");
    }
}
public class InnerClassExercise02 {
    public static void main(String[] args) {
        new CellPhone().alarmClock(new Bell() {//class com.hspedu.innerclass.InnerClassExercise02$1
            @Override
            public void ring() {
                System.out.println("懶豬起床了!!!");
            }
        });

        CellPhone cellPhone = new CellPhone();
        cellPhone.alarmClock(new Bell() {//class com.hspedu.innerclass.InnerClassExercise02$2
            @Override
            public void ring() {
                System.out.println("小伙伴上課了!!!");
            }
        });
    }
}

interface Bell {
    void ring();
}

class CellPhone {
    public void alarmClock (Bell bell) {//形參是Bell接口類型
        System.out.println(bell.getClass());
        bell.ring();//動態綁定
    }
}

5.8.4 成員內部類

public class MemberInnerClass01 {
    public static void main(String[] args) {
        Outer08 outer08 = new Outer08();
        outer08.t1();
        //外部其他類,使用成員內部類的兩種方式
        //老韓解讀
        // 第一種方式
        // outer08.new Inner08(); 相當於把 new Inner08()當做是outer08成員
        // 這就是一個語法,不要特別的糾結.
        Outer08.Inner08 inner08 = outer08.new Inner08();
        inner08.say();
        
        // 第二方式 在外部類中,編寫一個方法,可以返回 Inner08對象
        Outer08.Inner08 inner08Instance = outer08.getInner08Instance();
        inner08Instance.say();
    }
}

class Outer08 { //外部類
    private int n1 = 10;
    public String name = "張三";
    private void hi() {
        System.out.println("hi()方法...");
    }
    //1.注意: 成員內部類,是定義在外部內的成員位置上
    //2.可以添加任意訪問修飾符(public、protected 、默認、private),因為它的地位就是一個成員
    public class Inner08 {//成員內部類
        private double sal = 99.8;
        private int n1 = 66;
        public void say() {
            //可以直接訪問外部類的所有成員,包含私有的
            //如果成員內部類的成員和外部類的成員重名,會遵守就近原則.
            //,可以通過  外部類名.this.屬性 來訪問外部類的成員
            System.out.println("n1 = " + n1 + " name = " + name + " 外部類的n1=" + Outer08.this.n1);
            hi();
        }
    }
    //方法,返回一個Inner08實例
    public Inner08 getInner08Instance(){
        return new Inner08();
    }
    //寫方法
    public void t1() {
        //外部類想要使用成員內部類
        //首先創建成員內部類的對象,然后使用相關的方法
        Inner08 inner08 = new Inner08();
        inner08.say();
        System.out.println(inner08.sal);
    }
}

注釋:

  1. 成員內部類,理解位置,其實就是與屬性字段、方法平級。它的地位就是一個普通成員,所以可以使用訪問修飾符進行修飾。

  2. 外部其他類使用內部成員類的兩種方式,其中新建一個成員內部類語法很怪,Outer08.Inner08 inner08 = outer08.new Inner08();

  3. 重名還是遵循就近原則,可以通過 外部類名.this.屬性 來訪問外部類的成員。

5.8.5 靜態內部類

注釋:其實就是成員內部類用static修飾了

因此,靜態內部類只能訪問外部類所有靜態方法和屬性。

因為靜態內部類只能訪問靜態屬性和方法,因此當靜態內部類發生重名的時候想要訪問外部類的屬性時,直接使用 外部類名.成員 即可。

public class StaticInnerClass01 {
    public static void main(String[] args) {
        Outer10 outer10 = new Outer10();
        outer10.m1();

        //外部其他類 使用靜態內部類
        //方式1
        //因為靜態內部類,是可以通過類名直接訪問(前提是滿足訪問權限)
        Outer10.Inner10 inner10 = new Outer10.Inner10();
        inner10.say();
        //方式2
        //編寫一個方法,可以返回靜態內部類的對象實例.
        Outer10.Inner10 inner101 = outer10.getInner10();
        System.out.println("============");
        inner101.say();

        Outer10.Inner10 inner10_ = Outer10.getInner10_();
        System.out.println("************");
        inner10_.say();
    }
}

class Outer10 { //外部類
    private int n1 = 10;
    private static String name = "張三";
    private static void cry() {}
    //Inner10就是靜態內部類
    //1. 放在外部類的成員位置
    //2. 使用static 修飾
    //3. 可以直接訪問外部類的所有靜態成員,包含私有的,但不能直接訪問非靜態成員
    //4. 可以添加任意訪問修飾符(public、protected 、默認、private),因為它的地位就是一個成員
    //5. 作用域 :同其他的成員,為整個類體
    static class Inner10 {
        private static String name = "韓順平教育";
        public void say() {
            //如果外部類和靜態內部類的成員重名時,靜態內部類訪問的時,
            //默認遵循就近原則,如果想訪問外部類的成員,則可以使用 (外部類名.成員)
            System.out.println(name + " 外部類name= " + Outer10.name);
            cry();
        }
    }

    public void m1() { //外部類---訪問------>靜態內部類 訪問方式:創建對象,再訪問
        Inner10 inner10 = new Inner10();
        inner10.say();
    }

    public Inner10 getInner10() {
        return new Inner10();
    }

    public static Inner10 getInner10_() {
        return new Inner10();
    }
}
2021-08-30_111218

第六章 異常(Exception)

2021-08-30_111502

捕獲異常,避免程序因異常而中斷,保證程序繼續執行下去!

6.1 異常體系圖

2021-08-30_145645

在Exception里面,如果不是在RuntimeException下面,那么就是編譯時異常

6.2 五大運行時異常

6.3 編譯異常

6.4 異常處理

  1. try-catch:
  1. throws:

throws處理機制,其實是二選一,當前方法可以選擇用try-catch-finally進行處理,也可以throws拋出異常,當最后拋出至JVM底層時,JVM打印異常信息然后就退出程序了

6.4.1 try-catch

課堂練習:

這道題要好好看!!!因為finally必須執行,所以return3不會返回,返回return4。

這個題很秒!

最佳實踐:

        //如果用戶輸入的不是一個整數,就提示他反復輸入,直到輸入一個整數為止
        //思路
        //1. 創建Scanner對象
        //2. 使用無限循環,去接收一個輸入
        //3. 然后將該輸入的值,轉成一個int
        //4. 如果在轉換時,拋出異常,說明輸入的內容不是一個可以轉成int的內容
        //5. 如果沒有拋出異常,則break 該循環

public class TryCatchExercise04 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int num = 0;
        String inputStr = "";
        while (true) {
            System.out.println("請輸入一個整數:"); //
            inputStr = scanner.next();
            try {
                num = Integer.parseInt(inputStr); //這里是可能拋出異常
                break;
            } catch (NumberFormatException e) {// 出現異常后
                System.out.println("你輸入的不是一個整數:");
            }
        }
        System.out.println("你輸入的值是=" + num);
    }
}

6.4.2 throws

  1. 對第二點的理解,可以參考下面代碼中 throws 拋出:
public class Throws01 {
    public static void main(String[] args) {
    }
    // 也可以只用一個Exception
    public void f2() throws FileNotFoundException,NullPointerException,ArithmeticException {
        //創建了一個文件流對象
        //老韓解讀:
        //1. 這里的異常是一個FileNotFoundException 編譯異常
        //2. 使用前面講過的 try-catch-finally
        //3. 使用throws ,拋出異常, 讓調用f2方法的調用者(方法)處理
        //4. throws 后面的異常類型可以是方法中產生的異常類型,也可以是它的父類
        //5. throws 關鍵字后也可以是 異常列表, 即可以拋出多個異常
        FileInputStream fis = new FileInputStream("d://aa.txt");
    }
}
//1.對於編譯異常,程序中必須處理,比如 try-catch 或者 throws
//2.對於運行時異常,程序中如果沒有處理,默認就是throws的方式處理

class Father { //父類
    public void method() throws RuntimeException {
    }
}
class Son extends Father {//子類
    //3. 子類重寫父類的方法時,對拋出異常的規定:子類重寫的方法,
    //   所拋出的異常類型要么和父類拋出的異常一致,要么為父類拋出的異常類型的子類型
    //4. 在throws 過程中,如果有方法 try-catch , 就相當於處理異常,就可以不必throws
    @Override
    public void method() throws ArithmeticException {
    }
}

運行異常有默認處理機制(throws),編譯異常必須立馬解決!

    // 編譯處理,必須立馬解決
	public static void f1() throws FileNotFoundException {// 編譯異常,必須處理
        //這里大家思考問題 調用f3() 報錯
        //老韓解讀
        //1. 因為f3() 方法拋出的是一個編譯異常
        //2. 即這時,就要f1() 必須處理這個編譯異常
        //3. 在f1() 中,要么 try-catch-finally ,或者繼續throws 這個編譯異常
        f3(); // 拋出異常
    }
    public static void f3() throws FileNotFoundException {
        FileInputStream fis = new FileInputStream("d://aa.txt");
    }

	// 運行異常,有默認處理機制,可以不用顯示throws處理
    public static void f4() {
        //老韓解讀:
        //1. 在f4()中調用方法f5() 是OK
        //2. 原因是f5() 拋出的是運行異常
        //3. 而java中,並不要求程序員顯示處理,因為有默認處理機制
        f5();
    }
    public static void f5() throws ArithmeticException {

    }

6.5 自定義異常

public class CustomException {
    public static void main(String[] args) /*throws AgeException*/ {
        int age = 180;
        //要求范圍在 18 – 120 之間,否則拋出一個自定義異常
        if(!(age >= 18 && age <= 120)) {
            //這里我們可以通過構造器,設置信息
            throw new AgeException("年齡需要在 18~120之間");
        }
        System.out.println("你的年齡范圍正確.");
    }
}
//自定義一個異常
//老韓解讀
//1. 一般情況下,我們自定義異常是繼承 RuntimeException
//2. 即把自定義異常做成 運行時異常,好處時,我們可以使用默認的處理機制
//3. 即比較方便
class AgeException extends RuntimeException {
    public AgeException(String message) {//構造器
        super(message);
    }
}

6.6 throw和throws對比

輸出了“進入方法A”,然后手動throw一個異常,此時發生已經發生異常,進入finally輸出,主方法捕捉到異常,執行catch語句,打印e.getMessage(),其內容就是throw的內容:”制造異常"。然后就是B方法的執行..................

6.7 課后作業

  1. /*
    編寫應用程序EcmDef.java,接收命令行的兩個參數(整數),計算兩數相除。
    計算兩個數相除,要求使用方法 cal(int n1, int n2)
    對數據格式不正確(NumberFormatException)、缺少命令行參數(ArrayIndexOutOfBoundsException)、除0 進行異常處理(ArithmeticException)。
    */
    public class Homework01 {
        public static void main(String[] args) {
            try {
                //先驗證輸入的參數的個數是否正確 兩個參數
                if(args.length != 2) {
                    throw new ArrayIndexOutOfBoundsException("參數個數不對");
                }
    
                //先把接收到的參數,轉成整數
                int n1 = Integer.parseInt(args[0]);
                int n2 = Integer.parseInt(args[1]);
    
                double res = cal(n1, n2);//該方法可能拋出ArithmeticException
                System.out.println("計算結果是=" + res);
                
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println(e.getMessage());
            } catch (NumberFormatException e) {
                System.out.println("參數格式不正確,需要輸出整數");
            } catch (ArithmeticException e) {
                System.out.println("出現了除0的異常");
            }
        }
        //編寫cal方法,就是兩個數的商
        public static double cal(int n1, int n2) {
            return n1 / n2;
        }
    }
    

func捕獲到異常后,就不執行try代碼塊中的其余剩下部分了,轉而執行catch。因為成功捕獲異常,輸出D的語句會照常執行。

第七章 常用類

小紅旗的筆記有,其余類看PDF

7.1 包裝類

八種基本數據類型!

7.1.1 裝箱與拆箱

public class Integer01 {
    public static void main(String[] args) {
        //演示int <--> Integer 的裝箱和拆箱
        
        //jdk5前是手動裝箱和拆箱
        //手動裝箱 int->Integer
        int n1 = 100;
        Integer integer = new Integer(n1);//第一種方式
        Integer integer1 = Integer.valueOf(n1);//第二種方式
        //手動拆箱
        //Integer -> int
        int i = integer.intValue();

        //jdk5后,就可以自動裝箱和自動拆箱
        int n2 = 200;
        //自動裝箱 int->Integer
        Integer integer2 = n2; //這里是Integer包裝器類等於int類型,自動裝箱,底層使用的是 Integer.valueOf(n2),
        //自動拆箱 Integer->int
        int n3 = integer2; //底層仍然使用的是 intValue()方法
    }
}

7.1.2 課堂練習

三元運算符那里當做一個整體,會向Double精度靠,輸出1.0。而下面的因為是if-else語句獨立分開的,所以輸出1,而不是1.0。

7.1.3 包裝器方法

        //包裝類(Integer)->String
        Integer i = 100;//自動裝箱
        //方式1
        String str1 = i + "";
        //方式2
        String str2 = i.toString();
        //方式3
        String str3 = String.valueOf(i);

        //String -> 包裝類(Integer)
        String str4 = "12345";
        Integer i2 = Integer.parseInt(str4);//使用到自動裝箱
        Integer i3 = new Integer(str4);//構造器

7.1.4 Integer創建機制

在范圍里面(-128~127)是直接返回一個已存在的緩存對象,否則是返回新建的Integer對象。

128那里,均為新建的Integer對象。

7.1.5 Integer面試題

注釋:

==判斷兩個對象是否相等,但在比較基本數據類型時,是比較值是否相等;因此,1和2是new的對象,自然不可能是同一個對象,而6和7是值的比較。

7.2 String類*

7.2.1 基本介紹

不適合大量修改的情況下,適合有多個引用的情況下,如配置文件。

注:實現序列化之后,可以持久化到本地或者進行網絡傳輸,數據序列化就是變成二進制。

注:String底層還是數組,且是一個final類型的數組value,一旦賦值之后,其String的域——value數組指向的地址是不可以改變的(數組是引用類型)。字符串指向的首字符所在的地址。(JDK8是char類型,JDK11是byte類型,但一定是final類型數組)

7.2.2 創建String的兩種方式並剖析

注解:

  1. 方式一:這種方式是直接賦值,首先查看常量池中是否已經存在這樣一個常量,有就直接引用,沒有就創建一個新的final類型常量。
  2. 方式二:s2指向堆中的value數組,而value數組指向常量池中的數據,value數組的指向是無法改變的。但s2可以改變指向。

7.2.3 課堂測試

String的equals方法:比較內容(是否為同一對象,是否字符串挨個字符相同)

注:最后一個為F,記住intern始終指向常量池中的對象就行。

  1. 這個題要搞清楚!!P468

7.2.4 String特性

在常量池中已創建的常量對象是無法改變的,s1只是重新指向了常量池中另一個對象。

補充一下:c並不是直接指向的常量池中的"helloabc",而是堆中的一個對象。(可以自己跑一下,jdk版本不一樣源碼不一樣)

這道題很綜合,面試題!

分析完畢后,也能發現在局部方法中,方法棧有臨時變量性,數組的指向是地址,對其進行修改能夠影響值的真實變化。

7.3 StringBuffer類(線程安全)

7.3.1 基本介紹和結構剖析

String是final類型。

        //老韓解讀
        //1. StringBuffer 的直接父類 是 AbstractStringBuilder
        //2. StringBuffer 實現了 Serializable, 即StringBuffer的對象可以串行化
        //3. 在父類中  AbstractStringBuilder 有屬性 char[] value,不是final(底層還是數組)
        //   該 value 數組存放 字符串內容,引出存放在堆中的,而不是像String一樣存放在常量池
        //4. StringBuffer 是一個 final類,不能被繼承
        //5. 因為StringBuffer 字符內容是存在 char[] value, 所有在變化(增加/刪除)
        //   不用每次都更換地址(即不是每次創建新對象), 所以效率高於 String
        StringBuffer stringBuffer = new StringBuffer("hello");

2)這里說的不用每次更新地址,主要是指String中,str對象指向堆空間中的value的地址不能改變,value則是存放的指向常量池中某個常量的地址,但是如果改變值,那么value指向常量池中的地址就會進行重新指向(因為String是存放final類型的常量值,沒有就新建一個常量值存放在常量池中)。而StringBuffer有一個類似於自動維護緩沖區大小的機制,是一個存放字符串變量的容器(底層是char數組),因此不用每次改變值的時候都需要重新指向。

7.3.2 StringBuffer構造器和與String轉換

  1. 構造器
        //構造器的使用
        //老韓解讀
        //1. 創建一個 大小為 16的 char[] ,用於存放字符內容
        StringBuffer stringBuffer = new StringBuffer();

        //2 通過構造器指定 char[] 大小
        StringBuffer stringBuffer1 = new StringBuffer(100);

        //3. 通過給一個String 創建 StringBuffer, char[] 大小就是 str.length() + 16
        StringBuffer hello = new StringBuffer("hello");
  1. 與String的轉換

    StringBuffer與String的相互轉換:

            //看 String——>StringBuffer
            String str = "hello tom";
            //方式1 使用構造器
            //注意: 返回的才是StringBuffer對象,對str 本身沒有影響
            StringBuffer stringBuffer = new StringBuffer(str);
            //方式2 使用的是append方法
            StringBuffer stringBuffer1 = new StringBuffer();
            stringBuffer1 = stringBuffer1.append(str);
    
            //看看 StringBuffer ->String
            StringBuffer stringBuffer3 = new StringBuffer("韓順平教育");
            //方式1 使用StringBuffer提供的 toString方法
            String s = stringBuffer3.toString();
            //方式2: 使用構造器來搞定
            String s1 = new String(stringBuffer3);
    

7.3.3 StringBuffer常用方法

        StringBuffer s = new StringBuffer("hello");
        //增,append方法是在尾部增加
        s.append(',');// "hello,"
        s.append("張三豐");//"hello,張三豐"
        s.append("趙敏").append(100).append(true).append(10.5);//"hello,張三豐趙敏100true10.5"
        System.out.println(s);//"hello,張三豐趙敏100true10.5"

        //刪
        /*
         * 刪除索引為>=start && <end 處的字符
         * 解讀: 刪除 11~14的字符 [11, 14)
         */
        s.delete(11, 14);
        System.out.println(s);//"hello,張三豐趙敏true10.5"

        //改
        //老韓解讀,使用 周芷若 替換 索引9-11的字符 [9,11)
        s.replace(9, 11, "周芷若");
        System.out.println(s);//"hello,張三豐周芷若true10.5"

        //查找指定的子串在字符串第一次出現的索引,如果找不到返回-1
        int indexOf = s.indexOf("張三豐");
        System.out.println(indexOf);//6

        //插
        //老韓解讀,在索引為9的位置插入 "趙敏",原來索引為9的內容自動后移
        s.insert(9, "趙敏");
        System.out.println(s);//"hello,張三豐趙敏周芷若true10.5"

        //長度
        System.out.println(s.length());//22
        System.out.println(s);

7.3.4 課堂練習

  1. 注釋:

    1. str此時是null對象,追進源碼發現,在append方法底層實現中,對null的處理是處理出一個存放 ‘ n ’、‘ u ’、‘ l ’、‘ l ’ 的數組:
    1. 而在new StringBuffer()構造器方法中,傳入null,因為null.length()非法,會報空指針異常
  2.         String price = "8123564.59";
            StringBuffer sb = new StringBuffer(price);
            //先完成一個最簡單的實現123,564.59
            //找到小數點的索引,然后在該位置的前3位,插入,即可
    //        int i = sb.lastIndexOf(".");
    //        sb = sb.insert(i - 3, ",");
    
            //上面的兩步需要做一個循環處理,才是正確的
            for (int i = sb.lastIndexOf(".") - 3; i > 0; i -= 3) {
                sb = sb.insert(i, ",");
            }
            System.out.println(sb);//8,123,564.59
    

7.4 StringBuilder類

        //老韓解讀
        //1. StringBuilder 繼承 AbstractStringBuilder 類
        //2. 實現了 Serializable ,說明StringBuilder對象是可以串行化(對象可以網絡傳輸,可以保存到文件)
        //3. StringBuilder 是final類, 不能被繼承
        //4. StringBuilder 對象字符序列仍然是存放在其父類 AbstractStringBuilder的 char[] value;
        //   因此,字符序列是堆中
        //5. StringBuilder 的方法,沒有做互斥的處理,即沒有synchronized 關鍵字,因此在單線程的情況下使用
        //   StringBuilder
        StringBuilder stringBuilder = new StringBuilder();

7.5 String、StringBuffer和StringBuilder的比較

2):String復用率高:因為String是final字符串,字符串常量存放在常量池中,不必再新建一個字符串常量,而是直接指向這個已經創建好了的字符串,這既是優點,又具有其缺點,那就是每當改變成為新字符串的時候,就要在常量池新創建字符串常量,會增大開銷。

效率對比:StringBuilder > StringBuffer > String

        long startTime = 0L;
        long endTime = 0L;
		
		// *************************************************************
        StringBuffer buffer = new StringBuffer("");
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 80000; i++) {//StringBuffer 拼接 20000次
            buffer.append(String.valueOf(i));
        }
        endTime = System.currentTimeMillis();
        System.out.println("StringBuffer的執行時間:" + (endTime - startTime));

        // *************************************************************
        StringBuilder builder = new StringBuilder("");
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 80000; i++) {//StringBuilder 拼接 20000次
            builder.append(String.valueOf(i));
        }
        endTime = System.currentTimeMillis();
        System.out.println("StringBuilder的執行時間:" + (endTime - startTime));
		
        // *************************************************************
        String text = "";
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 80000; i++) {//String 拼接 20000
            text = text + i;
        }
        endTime = System.currentTimeMillis();
        System.out.println("String的執行時間:" + (endTime - startTime));
    }

/**
StringBuffer的執行時間:16
StringBuilder的執行時間:22
String的執行時間:3432
*/

總結:

大量修改操作情況下,不用String做開發。

第八章 泛型

8.1 引出泛型

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

        //使用傳統的方法來解決
        ArrayList arrayList = new ArrayList();
        arrayList.add(new Dog("旺財", 10));
        arrayList.add(new Dog("發財", 1));
        arrayList.add(new Dog("小黃", 5));

        //假如我們的程序員,不小心,添加了一只貓
        arrayList.add(new Cat("招財貓", 8));//沒有對數據類型進行約束,能正常進行添加,但是for循環遍歷會出錯

        //遍歷
        for (Object o : arrayList) {
            //向下轉型Object ->Dog,此時誤添加進去的Cat對象會導致發生錯誤
            Dog dog = (Dog) o;
            System.out.println(dog.getName() + "-" + dog.getAge());
        }

    }
}
/*
請編寫程序,在ArrayList 中,添加3個Dog對象
Dog對象含有name 和 age, 並輸出name 和 age (要求使用getXxx())
 */

引入泛型來解決數據類型沒有被約束的問題:

        //使用傳統的方法來解決===> 使用泛型
        //老韓解讀
        //1. 當我們 ArrayList<Dog> 表示存放到 ArrayList 集合中的元素是Dog類型 (細節后面說...)
        //2. 如果編譯器發現添加的類型,不滿足要求,就會報錯
        //3. 在遍歷的時候,可以直接取出 Dog 類型而不是 Object
        //4. public class ArrayList<E> {} E稱為泛型,那么 Dog->E
        ArrayList<Dog> arrayList = new ArrayList<Dog>();
        arrayList.add(new Dog("旺財", 10));
        arrayList.add(new Dog("發財", 1));
        arrayList.add(new Dog("小黃", 5));
        //假如我們的程序員,不小心,添加了一只貓
        //arrayList.add(new Cat("招財貓", 8));//引入泛型后,這里添加非Dog類型的Cat數據,會報錯!!!!
        System.out.println("===使用泛型====");
        for (Dog dog : arrayList) {
            System.out.println(dog.getName() + "-" + dog.getAge());
        }

8.2 泛型說明

泛型就是一種表示數據類型的數據類型,也就是數據類型的參數化

4):E就相當於占位符,等待編譯器傳入數據類型,編譯期間就確定了E的數據類型。

public class Generic03 {
    public static void main(String[] args) {
        //注意,特別強調: E具體的數據類型在定義Person對象的時候指定,即在編譯期間,就確定E是什么類型
        Person<String> person = new Person<String>("韓順平教育");
        person.show(); //String
        /*
            你可以這樣理解,上面的Person類
            class Person {
                String s ;//E表示 s的數據類型, 該數據類型在定義Person對象的時候指定,即在編譯期間,就確定E是什么類型

                public Person(String s) {//E也可以是參數類型
                    this.s = s;
                }

                public String f() {//返回類型使用E
                    return s;
                }
            }
         */
        
        Person<Integer> person2 = new Person<Integer>(100);
        person2.show();//Integer
        /*
            class Person {
                Integer s ;//E表示 s的數據類型, 該數據類型在定義Person對象的時候指定,即在編譯期間,就確定E是什么類型

                public Person(Integer s) {//E也可以是參數類型
                    this.s = s;
                }

                public Integer f() {//返回類型使用E
                    return s;
                }
            }
         */
    }
}

// 泛型的作用是:可以在類聲明時通過一個標識表示類中某個屬性的類型,
// 或者是某個方法的返回值的類型,或者是參數類型
class Person<E> {
    E s ;//E表示 s的數據類型, 該數據類型在定義Person對象的時候指定,即在編譯期間,就確定E是什么類型
    public Person(E s) {//E也可以是參數類型
        this.s = s;
    }
    public E f() {//返回類型使用E
        return s;
    }
    public void show() {
        System.out.println(s.getClass());//顯示s的運行類型
    }
}

8.3 泛型的語法和應用實例

package com.hspedu.generic;

import java.util.*;

/**
 * @author Wenhao Zou
 * @title: Generic01
 * @projectName JavaHan
 * @description: TODO
 * @date 2021/9/1 17:18
 */
public class Generic01 {
    public static void main(String[] args) {
        
        // 1. 使用泛型方式給HashSet,放入三個學生對象
        HashSet<Student> students = new HashSet<>();
        students.add(new Student("jack", 18));
        students.add(new Student("tom", 28));
        students.add(new Student("mary", 19));
        //遍歷
        for (Student student : students) {
            System.out.println(student);
        }
        

        // 2. 使用泛型方式給HashMap 放入3個學生對象
        //K -> String V->Student
        HashMap<String, Student> hm = new HashMap<String, Student>();
        /*
            public class HashMap<K,V>  {}
         */
        hm.put("milan", new Student("milan", 38));
        hm.put("smith", new Student("smith", 48));
        hm.put("hsp", new Student("hsp", 28));
        //迭代器遍歷
        Set<Map.Entry<String, Student>> entries = hm.entrySet();
        Iterator<Map.Entry<String, Student>> iterator = entries.iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Student> next =  iterator.next();
            System.out.println(next);
        }
    }
}
/**
 * 創建  3個學生對象
 * 放入到HashSet中學生對象, 使用.
 * 放入到  HashMap中,要求 Key 是 String name, Value 就是 學生對象
 * 使用兩種方式遍歷
 */
class Student {}

8.4 泛型使用細節

基本數據類型不能作為泛型參數,要使用的話,得用其包裝器類型。

public class GenericDetail {
    public static void main(String[] args) {
        //1.給泛型指向數據類型是,要求是引用類型,不能是基本數據類型
        List<Integer> list = new ArrayList<Integer>(); //OK
        //List<int> list2 = new ArrayList<int>();//錯誤

        //2. 說明
        //因為 E 指定了 A 類型, 構造器傳入了 new A()
        //在給泛型指定具體類型后,可以傳入該類型或者其子類類型
        Pig<A> aPig = new Pig<A>(new A());
        aPig.f();
        Pig<A> aPig2 = new Pig<A>(new B());
        aPig2.f();

        //3. 泛型的使用形式
        ArrayList<Integer> list1 = new ArrayList<Integer>();
        List<Integer> list2 = new ArrayList<Integer>();
        //在實際開發中,我們往往簡寫
        //編譯器會進行類型推斷, 老師推薦使用下面寫法
        ArrayList<Integer> list3 = new ArrayList<>();
        List<Integer> list4 = new ArrayList<>();
        ArrayList<Pig> pigs = new ArrayList<>();

        //4. 如果是這樣寫 泛型默認是 Object
        ArrayList arrayList = new ArrayList();//等價 ArrayList<Object> arrayList = new ArrayList<Object>();

        /*
            public boolean add(Object e) {
                ensureCapacityInternal(size + 1);  // Increments modCount!!
                elementData[size++] = e;
                return true;
            }
         */
        
        Tiger tiger = new Tiger();//泛型默認為Object
        /*
            class Tiger {//類
                Object e;

                public Tiger() {}

                public Tiger(Object e) {
                    this.e = e;
                }
            }
         */

    }
}

class Tiger<E> {//類
    E e;

    public Tiger() {} //默認無參構造器被有參構造器覆蓋,要想使用,必須顯式聲明

    public Tiger(E e) {
        this.e = e;
    }
}

class A {}
class B extends A {}

class Pig<E> {//
    E e;

    public Pig(E e) {
        this.e = e;
    }

    public void f() {
        System.out.println(e.getClass()); //運行類型
    }
}

8.5 泛型課堂練習

public class GenericExercise {
    public static void main(String[] args) {
        ArrayList<Employee> employees = new ArrayList<>();
        employees.add(new Employee("Tom", 20000, new Mydate(2001, 11, 11)));
        employees.add(new Employee("Jack", 12000, new Mydate(2001, 10, 12)));
        employees.add(new Employee("Hsp", 50000, new Mydate(1980, 5, 1)));

        System.out.println("employees: " + employees);
        System.out.println("對員工進行排序:=================");

        employees.sort(new Comparator<Employee>() {
            @Override
            public int compare(Employee o1, Employee o2) {
                if (!(o1 instanceof Employee && o2 instanceof Employee)) {
                    System.out.println("類型不正確!");
                    return 0;
                } else {
                    return o1.getBirthday().compareTo(o2.getBirthday());
                }
            }
        });
        System.out.println("employees: " + employees);
    }
}

class Mydate {
    private int year;
    private int month;
    private int day;

    public Mydate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public int getMonth() {
        return month;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    public int getDay() {
        return day;
    }

    public void setDay(int day) {
        this.day = day;
    }

    @Override
    public String toString() {
        return "Mydate{" +
                "year=" + year +
                ", month=" + month +
                ", day=" + day +
                '}';
    }

    public int compareTo (Mydate o) {
        int yearMinus = this.year - o.getYear();
        if (yearMinus != 0) {
            return yearMinus;
        }

        //如果年相同,就比較month
        int monthMinus = this.month - o.getMonth();
        if (monthMinus != 0) {
            return monthMinus;
        }

        //如果年月相同,比較日
        return this.day - o.getDay();
    }
}

class Employee {
    private String name;
    private int sal;
    private Mydate birthday;

    public Employee(String name, int sal, Mydate birthday) {
        this.name = name;
        this.sal = sal;
        this.birthday = birthday;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getSal() {
        return sal;
    }

    public void setSal(int sal) {
        this.sal = sal;
    }

    public Mydate getBirthday() {
        return birthday;
    }

    public void setBirthday(Mydate birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return "\nEmployee{" +
                "name='" + name + '\'' +
                ", sal=" + sal +
                ", birthday=" + birthday +
                '}';
    }
}

8.6 自定義泛型

8.6.1 基本介紹

注釋:具體案例見下文代碼部分

2)因為在類定義中,數組的類型是用泛型符號占位使用的,而編譯器此時不知道數組的具體數據類型,從而無法知曉應該分配多大的內存空間,因此無法進行初始化(也就是new一個數組)。只能定義一個泛型數組的引用類型。

3)與2)差不多,因為無法指定數據類型,JVM並不能對其進行內存空間的分配以及初始化,因此靜態方法中不能使用泛型。

//老韓解讀
//1. Tiger 后面有泛型,所以我們把 Tiger 就稱為自定義泛型類
//2, T, R, M 泛型的標識符, 一般是單個大寫字母
//3. < >內泛型標識符可以有多個.
//4. 普通成員可以使用泛型 (屬性、方法)
//5. 使用泛型的數組,不能初始化
//6. 靜態方法中不能使用類的泛型
class Tiger<T, R, M> {
    String name;
    R r; //屬性使用到泛型,具體R M T 是什么類型的數據,是在定義Tiger時指定的
    M m;
    T t;
	
    //因為數組在new 不能確定T的類型,因此不知道要使用多大的內存,所以就無法在內存開空間
	//T[] ts = new T[8] //錯誤
	
    T[] ts;//只能定義一個引用

    public Tiger(String name, R r, M m, T t) {//構造器使用泛型
        this.name = name;
        this.r = r;
        this.m = m;
        this.t = t;
    }

    //因為靜態是和類相關的,在類加載時,對象還沒有創建
    //所以,如果靜態方法和靜態屬性使用了泛型,JVM就無法完成初始化
//    static R r2;
//    public static void m1(M m) {
//
//    }

    //方法使用泛型
    public R getR() {
        return r;
    }

    public void setR(R r) {//方法使用到泛型
        this.r = r;
    }

    public M getM() {//返回類型可以使用泛型.
        return m;
    }
    ......
}

8.6.2 案例演示

        //T=Double, R=String, M=Integer
        Tiger<Double,String,Integer> g = new Tiger<>("john");
        g.setT(10.9); //OK
        //g.setT("yy"); //錯誤,類型不對
        System.out.println(g);
        Tiger g2 = new Tiger("john~~");//OK T=Object R=Object M=Object
        g2.setT("yy"); //OK ,因為 T=Object "yy"=String 是Object子類
        System.out.println("g2=" + g2);
public class CustomGeneric_ {
    public static void main(String[] args) {

        //T=Double, R=String, M=Integer
        Tiger<Double,String,Integer> g = new Tiger<>("john");
        g.setT(10.9); //OK
        //g.setT("yy"); //錯誤,類型不對
        System.out.println(g);
        Tiger g2 = new Tiger("john~~");//OK T=Object R=Object M=Object
        g2.setT("yy"); //OK ,因為 T=Object "yy"=String 是Object子類
        System.out.println("g2=" + g2);

    }
}

//老韓解讀
//1. Tiger 后面有泛型,所以我們把 Tiger 就稱為自定義泛型類
//2, T, R, M 泛型的標識符, 一般是單個大寫字母
//3. < >內泛型標識符可以有多個.
//4. 普通成員可以使用泛型 (屬性、方法)
//5. 使用泛型的數組,不能初始化
//6. 靜態方法中不能使用類的泛型
class Tiger<T, R, M> {
    String name;
    R r; //屬性使用到泛型,具體R M T 是什么類型的數據,是在定義Tiger時指定的
    M m;
    T t;
	
    //因為數組在new 不能確定T的類型,因此不知道要使用多大的內存,所以就無法在內存開空間
	//T[] ts = new T[8] //錯誤
	
    T[] ts;//只能定義一個引用

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

    public Tiger(R r, M m, T t) {//構造器使用泛型

        this.r = r;
        this.m = m;
        this.t = t;
    }

    public Tiger(String name, R r, M m, T t) {//構造器使用泛型
        this.name = name;
        this.r = r;
        this.m = m;
        this.t = t;
    }

    //因為靜態是和類相關的,在類加載時,對象還沒有創建
    //所以,如果靜態方法和靜態屬性使用了泛型,JVM就無法完成初始化
//    static R r2;
//    public static void m1(M m) {
//
//    }

    //方法使用泛型

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public R getR() {
        return r;
    }

    public void setR(R r) {//方法使用到泛型
        this.r = r;
    }

    public M getM() {//返回類型可以使用泛型.
        return m;
    }

    public void setM(M m) {
        this.m = m;
    }

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    @Override
    public String toString() {
        return "Tiger{" +
                "name='" + name + '\'' +
                ", r=" + r +
                ", m=" + m +
                ", t=" + t +
                ", ts=" + Arrays.toString(ts) +
                '}';
    }
}

8.6.3 自定義泛型接口

/**
 *  泛型接口使用的說明
 *  1. 接口中,靜態成員也不能使用泛型
 *  2. 泛型接口的類型, 在繼承接口或者實現接口時確定
 *  3. 沒有指定類型,默認為Object
 */

//在繼承接口 指定泛型接口的類型
interface IA extends IUsb<String, Double> {

}
//當我們去實現IA接口時,因為IA在繼承IUsu 接口時,指定了U 為String R為Double
//,在實現IUsu接口的方法時,使用String替換U, 是Double替換R
class AA implements IA {
    @Override
    public Double get(String s) {
        return null;
    }
    @Override
    public void hi(Double aDouble) {

    }
    @Override
    public void run(Double r1, Double r2, String u1, String u2) {
    }
}

//實現接口時,直接指定泛型接口的類型
//給U 指定Integer 給 R 指定了 Float
//所以,當我們實現IUsb方法時,會使用Integer替換U, 使用Float替換R
class BB implements IUsb<Integer, Float> {

    @Override
    public Float get(Integer integer) {
        return null;
    }

    @Override
    public void hi(Float aFloat) {

    }

    @Override
    public void run(Float r1, Float r2, Integer u1, Integer u2) {

    }
}
//沒有指定類型,默認為Object
//建議直接寫成 IUsb<Object,Object>
class CC implements IUsb { //等價 class CC implements IUsb<Object,Object> {
    @Override
    public Object get(Object o) {
        return null;
    }
    @Override
    public void hi(Object o) {
    }
    @Override
    public void run(Object r1, Object r2, Object u1, Object u2) {

    }

}

interface IUsb<U, R> {
    int n = 10;
    //U name; 不能這樣使用,因為接口中所有屬性默認為public static final
    
    //普通方法中,可以使用接口泛型
    R get(U u);

    void hi(R r);

    void run(R r1, R r2, U u1, U u2);

    //在jdk8 中,可以在接口中,使用默認方法, 也是可以使用泛型
    default R method(U u) {
        return null;
    }
}

注意:接口中屬性不能用泛型定義,記住接口中屬性默認為public static final。普通方法中的參數列表里可以使用。

8.6.4 自定義泛型方法

注意與方法使用了泛型進行對比,方法使用了泛型一般是在參數列表位置上使用了泛型占位符。而自定義泛型方法是在修飾符后面有自定義泛型符號。

public class CustomMethodGeneric {
    public static void main(String[] args) {
        Car car = new Car();
        car.fly("寶馬", 100);//當調用方法時,傳入參數,編譯器,就會確定類型
        System.out.println("=======");
        car.fly(300, 100.1);//當調用方法時,傳入參數,編譯器,就會確定類型

        //測試
        //T->String, R-> ArrayList
        Fish<String, ArrayList> fish = new Fish<>();
        fish.hello(new ArrayList(), 11.3f);
    }
}

//泛型方法,可以定義在普通類中, 也可以定義在泛型類中
class Car {//普通類

    public void run() {//普通方法
    }
    //說明 泛型方法
    //1. <T,R> 就是泛型
    //2. 是提供給 fly使用的
    public <T, R> void fly(T t, R r) {//泛型方法
        System.out.println(t.getClass());//String,根據傳入的不同數據類型,會進行變化
        System.out.println(r.getClass());//Integer
    }
}

class Fish<T, R> {//泛型類
    public void run() {//普通方法
    }
    public<U,M> void eat(U u, M m) {//泛型方法

    }
    //說明
    //1. 下面hi方法不是泛型方法
    //2. 是hi方法使用了類聲明的 泛型
    public void hi(T t) {
    }
    //泛型方法,可以使用類聲明的泛型,也可以使用自己聲明泛型
    public<K> void hello(R r, K k) {
        System.out.println(r.getClass());//ArrayList
        System.out.println(k.getClass());//Float
    }
}

課堂練習:

注解:

  1. 注意題中的U是沒有定義的,既不是類名后的泛型符號,也不是泛型方法。
  2. apple.fly(10);// 10 會被自動裝箱 Integer 10, 輸出Integer
  3. apple.fly(new Dog());//Dog
  4. getClass()會打印包名+類名,而getSimpleName()只會打印類名。

8.6.5 泛型繼承和通配符(受限泛型)

注解:

  1. 泛型不具備繼承性!

  2. 泛型上下限的理解:(好好理解,很重要)

    1)<? extends A> 此時泛型只能支持A類或其子類,而父類不能使用,規定了向上支持的上限。

    2)同理,<? super A> 此時泛型支持A類及其父類,A類的子類不能使用,也就是向下使用最低到A類,規定了下限。

public class GenericExtends {
    public static void main(String[] args) {
        
        Object o = new String("xx");

        //泛型沒有繼承性
        //List<Object> list = new ArrayList<String>();

        //舉例說明下面三個方法的使用
        List<Object> list1 = new ArrayList<>();
        List<String> list2 = new ArrayList<>();
        List<AA> list3 = new ArrayList<>();
        List<BB> list4 = new ArrayList<>();
        List<CC> list5 = new ArrayList<>();

        //如果是 List<?> c ,可以接受任意的泛型類型
        printCollection1(list1);
        printCollection1(list2);
        printCollection1(list3);
        printCollection1(list4);
        printCollection1(list5);

        //List<? extends AA> c: 表示 上限,可以接受 AA或者AA子類
//        printCollection2(list1);//×
//        printCollection2(list2);//×
        printCollection2(list3);//√
        printCollection2(list4);//√
        printCollection2(list5);//√

        //List<? super AA> c: 支持AA類以及AA類的父類,不限於直接父類
        printCollection3(list1);//√
        //printCollection3(list2);//×
        printCollection3(list3);//√
        //printCollection3(list4);//×
        //printCollection3(list5);//×
        
    }
    // ? extends AA 表示 上限,可以接受 AA或者AA子類
    public static void printCollection2(List<? extends AA> c) {
        for (Object object : c) {
            System.out.println(object);
        }
    }

    //說明: List<?> 表示 任意的泛型類型都可以接受
    public static void printCollection1(List<?> c) {
        for (Object object : c) { // 通配符,取出時,就是Object
            System.out.println(object);
        }
    }

    // ? super 子類類名AA:支持AA類以及AA類的父類,不限於直接父類,
    //規定了泛型的下限
    public static void printCollection3(List<? super AA> c) {
        for (Object object : c) {
            System.out.println(object);
        }
    }
}

class AA {
}

class BB extends AA {
}

class CC extends BB {
}

8.7 JUnit(單元測試框架)

8.8 本章作業

主要代碼展示,DAO:

/**
 * 定義個泛型類 DAO<T>,在其中定義一個Map 成員變量,Map 的鍵為 String 類型,值為 T 類型。
 *  * 分別創建以下方法:
 *  * (1) public void save(String id,T entity): 保存 T 類型的對象到 Map 成員變量中
 *  * (2) public T get(String id):從 map 中獲取 id 對應的對象
 *  * (3) public void update(String id,T entity):替換 map 中key為id的內容,改為 entity 對象
 *  * (4) public List<T> list():返回 map 中存放的所有 T 對象
 *  * (5) public void delete(String id):刪除指定 id 對象
 */
public class DAO<T> {//泛型類
    private Map<String, T> map = new HashMap<>();

    public T get(String id) {
        return map.get(id);
    }
    public void update(String id,T entity) {
        map.put(id, entity);
    }
    //返回 map 中存放的所有 T 對象
    //遍歷map [k-v],將map的 所有value(T entity),封裝到ArrayList返回即可
    public List<T> list() {
        //創建 Arraylist
        List<T> list = new ArrayList<>();

        //遍歷map
        Set<String> keySet = map.keySet();
        for (String key : keySet) {
            //map.get(key) 返回就是 User對象->ArrayList
            list.add(map.get(key));//也可以直接使用本類的 get(String id)
        }

        return list;
    }
    public void delete(String id) {
        map.remove(id);
    }
    public void save(String id,T entity) {//把entity保存到map
        map.put(id, entity);
    }
}

第九章 IO流專題

9.1 文件

9.1.1 文件的基本介紹

文件就是保存數據的地方。

9.1.2 常用的文件操作

  1. 創建文件

主要需要注意,一定要調用creatNewFile方法在磁盤中創建文件。

    //方式1 new File(String pathname)
    @Test
    public void create01() {
        String filePath = "e:\\news1.txt";
        File file = new File(filePath);

        try {
            file.createNewFile();
            System.out.println("文件創建成功");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    //方式2 new File(File parent,String child) //根據父目錄文件+子路徑構建
    //e:\\news2.txt
    @Test
    public  void create02() {
        File parentFile = new File("e:\\");
        String fileName = "news2.txt";
        //這里的file對象,在java程序中,只是一個對象
        //只有執行了createNewFile 方法,才會真正的,在磁盤創建該文件
        File file = new File(parentFile, fileName);

        try {
            file.createNewFile();
            System.out.println("創建成功~");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //方式3 new File(String parent,String child) //根據父目錄+子路徑構建
    @Test
    public void create03() {
        //String parentPath = "e:\\";
        String parentPath = "e:\\";  // 這里是String,不是File,注意
        String fileName = "news4.txt";
        File file = new File(parentPath, fileName);

        try {
            file.createNewFile();
            System.out.println("創建成功~");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  1. 獲取文件信息
        //先創建文件對象
        File file = new File("e:\\news1.txt");

        //調用相應的方法,得到對應信息
        System.out.println("文件名字=" + file.getName());
        //getName、getAbsolutePath、getParent、length、exists、isFile、isDirectory
        System.out.println("文件絕對路徑=" + file.getAbsolutePath());
        System.out.println("文件父級目錄=" + file.getParent());
        System.out.println("文件大小(字節)=" + file.length());
        System.out.println("文件是否存在=" + file.exists());//T
        System.out.println("是不是一個文件=" + file.isFile());//T
        System.out.println("是不是一個目錄=" + file.isDirectory());//F

/** 結果
文件名字=news1.txt
文件絕對路徑=e:\news1.txt
文件父級目錄=e:\
文件大小(字節)=0
文件是否存在=false
是不是一個文件=false
是不是一個目錄=false
*/
  1. 目錄的操作和文件刪除

        //判斷 d:\\news1.txt 是否存在,如果存在就刪除
        @Test
        public void m1() {
            String filePath = "e:\\news1.txt";
            File file = new File(filePath);
            if (file.exists()) {
                if (file.delete()) {
                    System.out.println(filePath + "刪除成功");
                } else {
                    System.out.println(filePath + "刪除失敗");
                }
            } else {
                System.out.println("該文件不存在...");
            }
        }
    
        //判斷 D:\\demo02 是否存在,存在就刪除,否則提示不存在
        //這里我們需要體會到,在java編程中,目錄也被當做文件
        @Test
        public void m2() {
            String filePath = "D:\\demo02";
            File file = new File(filePath);
            if (file.exists()) {
                if (file.delete()) {
                    System.out.println(filePath + "刪除成功");
                } else {
                    System.out.println(filePath + "刪除失敗");
                }
            } else {
                System.out.println("該目錄不存在...");
            }
        }
    
        //判斷 D:\\demo\\a\\b\\c 目錄是否存在,如果存在就提示已經存在,否則就創建
        @Test
        public void m3() {
            String directoryPath = "D:\\demo\\a\\b\\c";
            File file = new File(directoryPath);
            if (file.exists()) {
                System.out.println(directoryPath + "存在..");
            } else {
                if (file.mkdirs()) { //創建一級目錄使用mkdir() ,創建多級目錄使用mkdirs()
                    System.out.println(directoryPath + "創建成功..");
                } else {
                    System.out.println(directoryPath + "創建失敗...");
                }
            }
        }
    

    刪除:file.delete()

    存在:file.exists()

    創建一級目錄:file.mkdir()

    創建多級目錄:file.mkdirs()

9.2 IO流原理和分類

9.2.1 IO流原理

9.2.2 IO流分類

1)IO流按照操作數據的單位不同可以分為字節流和字符流。字節流適用於二進制文件,比如0101的字節流。字符流(按字符,一個字符有多少個字節,取決於編碼方式)適用於文本文件的讀寫(如漢字,一個漢字由三個字節組成,如果用字節流處理漢字,會出現亂碼)。

2)這四個抽象基類都不能單獨實例化(頂級基類),必須使用其實現子類進行實例化。

9.3 字節流

9.3.1 InputStream 字節輸入流

9.3.1.1 FileInputStream

字節輸入流文件 ——> 程序

下面代碼演示了用FileInputStream進行字節的讀取:進行幾點說明

  1. 方法01是單個字節的讀取,效率比較低,如果對漢字進行讀寫的化,會有亂碼的情況,因為一個漢字是由多個字節組成,而此時是按照單個字節讀取並打印的,會出現亂碼。
  2. 方法02是提高效率的方式,按照一個指定大小的byte數組進行讀取,效率高。
    /**
     * 演示讀取文件...
     * 單個字節的讀取,效率比較低
     * -> 使用 read(byte[] b) 效率高
     */
    @Test
    public void readFile01() {
        String filePath = "e:\\hello.txt";
        int readData = 0;
        FileInputStream fileInputStream = null;
        try {
            //創建 FileInputStream 對象,用於讀取 文件
            fileInputStream = new FileInputStream(filePath);
            //從該輸入流讀取一個字節的數據。 如果沒有輸入可用,此方法將阻止。
            //如果返回-1 , 表示讀取完畢
            while ((readData = fileInputStream.read()) != -1) {
                System.out.print((char)readData);//轉成char顯示
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //關閉文件流,釋放資源.
            try {
                fileInputStream.close();//這個動作一定要執行,釋放資源
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 使用 read(byte[] b) 讀取文件,提高效率
     *  需要明確,如果用字節流讀取中文漢字,會出現亂碼,
     *  因為漢字一個字符不止一個字節組成
     */
    @Test
    public void readFile02() {
        String filePath = "e:\\hello.txt";

        //字節數組
        byte[] buf = new byte[8]; //一次讀取8個字節.每8個字節讀取一次

        int readLen = 0;
        FileInputStream fileInputStream = null;
        try {
            //創建 FileInputStream 對象,用於讀取 文件
            fileInputStream = new FileInputStream(filePath);

            //從該輸入流讀取最多b.length字節的數據到字節數組。 此方法將阻塞,直到某些輸入可用。
            //如果返回-1 , 表示讀取完畢
            //如果讀取正常, 返回實際讀取的字節數
            while ((readLen = fileInputStream.read(buf)) != -1) {
                System.out.print(new String(buf, 0, readLen));//顯示,以實際讀取字節數進行顯示
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //關閉文件流,釋放資源.
            try {
                fileInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
9.3.1.2 BufferedInputStream(包裝流)

BufferedInputStream接收InputStream抽象類實現的子類就行。

9.3.2 OutputStream 字節輸出流

9.3.2.1 FileOutputStream

字節輸出流文件 <—— 程序

  /**
     * 演示使用FileOutputStream 將數據寫到文件中,
     * 如果該文件不存在,則創建該文件
     */
    @Test
    public void writeFile() {
        //創建 FileOutputStream對象
        String filePath = "e:\\a.txt";
        FileOutputStream fileOutputStream = null;
        try {
            //得到 FileOutputStream對象 對象
            //老師說明
            //1. new FileOutputStream(filePath) 創建方式,當寫入內容是,會覆蓋原來的內容
            //2. new FileOutputStream(filePath, true) 創建方式,當寫入內容是,是追加到文件后面
            fileOutputStream = new FileOutputStream(filePath, true);
            
            //寫入一個字節
            fileOutputStream.write('H');
            
            //寫入字符串
            String str = "hsp,world!";
            fileOutputStream.write(str.getBytes());//str.getBytes() 可以把 字符串-> 字節數組
            
            /*
            write(byte[] b, int off, int len) 將 len字節從位於偏移量 off的指定字節數組寫入此文件輸出流
             */
            fileOutputStream.write(str.getBytes(), 0, 3);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
9.3.2.2 BufferedOutputStream(包裝流/處理流)

9.3.3 應用實例

9.3.3.1 文件拷貝

要求:編程完成圖片/音樂的拷貝(涉及到文件字節輸入流和輸出流)

public class FileCopy {
    public static void main(String[] args) {
        //完成 文件拷貝,將 e:\\Koala.jpg 拷貝 c:\\
        //思路分析
        //1. 創建文件的輸入流 , 將文件讀入到程序
        //2. 創建文件的輸出流, 將讀取到的文件數據,寫入到指定的文件.
        String srcFilePath = "e:\\Koala.jpg";
        String destFilePath = "e:\\Koala3.jpg";
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;

        try {
            fileInputStream = new FileInputStream(srcFilePath);
            fileOutputStream = new FileOutputStream(destFilePath);
            //定義一個字節數組,提高讀取效果
            byte[] buf = new byte[1024];
            int readLen = 0;
            while ((readLen = fileInputStream.read(buf)) != -1) {//返回-1時,說明讀取完畢
                //讀取到后,就寫入到文件 通過 fileOutputStream
                //即,是一邊讀,一邊寫
                fileOutputStream.write(buf, 0, readLen);//一定要使用這個方法(因為無法保證最后一次buf能讀滿1024個字節)
            }
            System.out.println("拷貝ok~");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                //關閉輸入流和輸出流,釋放資源
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
9.3.3.2 文件拷貝(包裝流實現)
// 使用字節流,可以實現拷貝二進制文件,當然也可以拷貝文本文件
public class BufferedCopy {
    public static void main(String[] args) {
        // 使用圖片讀取會發生錯誤,得用字節流
        String sourceFile = "d:\\LalaLand.jpg";
        String destFile = "e:\\LalaLand.jpg";

        BufferedInputStream bufferedReader = null;// 改成字節流之后,圖片能夠進行讀取了
        BufferedOutputStream bufferedWriter = null;

        int line;
        //byte[] buff = new byte[1024];//數組讀取法,效率高

        try {
            bufferedReader = new BufferedInputStream(new FileInputStream(sourceFile));
            bufferedWriter = new BufferedOutputStream(new FileOutputStream(destFile));

            while ((line = bufferedReader.read() )!= -1) {//bufferedReader.read(buff);
                bufferedWriter.write((char)line);//bufferedWriter.write(buff, 0, line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {

            try {
                bufferedReader.close();
                bufferedWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

9.4 字符流

9.4.1 Reader 字符輸入流

9.4.1.1 FileReader

使用FileReader從story.txt中讀取內容

/**
     * 單個字符讀取文件
     */
    @Test
    public void readFile01() {
        String filePath = "e:\\story.txt";
        FileReader fileReader = null;
        int data = 0;
        //1. 創建FileReader對象
        try {
            fileReader = new FileReader(filePath);
            //循環讀取 使用read, 單個字符讀取
            while ((data = fileReader.read()) != -1) {
                System.out.print((char) data);  //ASCII碼
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileReader != null) {
                    fileReader.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 字符數組讀取文件
     */
    @Test
    public void readFile02() {
        System.out.println("~~~readFile02 ~~~");
        String filePath = "e:\\story.txt";
        FileReader fileReader = null;

        int readLen = 0;
        char[] buf = new char[8];
        //1. 創建FileReader對象
        try {
            fileReader = new FileReader(filePath);
            //循環讀取 使用read(buf), 返回的是實際讀取到的字符數
            //如果返回-1, 說明到文件結束
            while ((readLen = fileReader.read(buf)) != -1) {
                System.out.print(new String(buf, 0, readLen)); //注意操作,一定得這樣寫
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileReader != null) {
                    fileReader.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
9.4.1.2 BufferedReader 包裝流(處理流)

繼承於Reader

包裝流只進行包裝,真正進行數據讀取的還是節點流。當我們去關閉包裝流的時候,底層實際上是去關閉節點流。

應用案例:

        String filePath = "e:\\a.java";
        //創建bufferedReader
        BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
        //讀取
        String line; //按行讀取, 效率高
        //說明
        //1. bufferedReader.readLine() 是按行讀取文件
        //2. 當返回null 時,表示文件讀取完畢
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
        }

        //關閉流, 這里注意,只需要關閉 BufferedReader ,因為底層會自動的去關閉 節點流
        //FileReader。
        /*
            public void close() throws IOException {
                synchronized (lock) {
                    if (in == null)
                        return;
                    try {
                        in.close();//in 就是我們傳入的 new FileReader(filePath), 關閉了.
                    } finally {
                        in = null;
                        cb = null;
                    }
                }
            }
         */
        bufferedReader.close();//關閉流,節省開銷

注意:BufferedReader流讀取時候,文件讀取完畢,是返回null表示結束,而不是-1。

9.4.2 Writer 字符輸出流

9.4.2.1 FileWriter

最后一句:因為此時文件還在內存中,需要手動刷新或關閉才能寫到磁盤上,關閉操作是會引起強制刷新的,所以也可以。

細節:1.寫入完之后一定要關閉或者刷新。2.是覆蓋寫入還是追加寫入。

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

        String filePath = "e:\\note.txt";
        //創建FileWriter對象
        FileWriter fileWriter = null;
        char[] chars = {'a', 'b', 'c'};
        try {
            fileWriter = new FileWriter(filePath);//默認是覆蓋寫入
//            3) write(int):寫入單個字符
            fileWriter.write('H');
//            4) write(char[]):寫入指定數組
            fileWriter.write(chars);
//            5) write(char[],off,len):寫入指定數組的指定部分
            fileWriter.write("韓順平教育".toCharArray(), 0, 3);
//            6) write(string):寫入整個字符串
            fileWriter.write(" 你好北京~");
            fileWriter.write("風雨之后,定見彩虹");
//            7) write(string,off,len):寫入字符串的指定部分
            fileWriter.write("上海天津", 0, 2);
            //在數據量大的情況下,可以使用循環操作.
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //對應FileWriter , 一定要關閉流,或者flush才能真正的把數據寫入到文件
            //老韓看源碼就知道原因.
            /*
                看看代碼
            private void writeBytes() throws IOException {
				this.bb.flip();
				int var1 = this.bb.limit();
				int var2 = this.bb.position();
				
				assert var2 <= var1;
				
				int var3 = var2 <= var1 ? var1 - var2 : 0;
				if (var3 > 0) {
					if (this.ch != null) {
						assert this.ch.write(this.bb) == var3 : var3;
					} else {
						this.out.write(this.bb.array(), this.bb.arrayOffset() + var2, var3);
					}
				}
				this.bb.clear();
			}
             */
            try {
                //fileWriter.flush();
                //關閉文件流,等價 flush() + 關閉
                fileWriter.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("程序結束...");
    }
}
9.4.2.2 BufferedWriter
String filePath = "e:\\ok.txt";
        //創建BufferedWriter
        //說明:
        //1. new FileWriter(filePath, true) 表示以追加的方式寫入
        //2. new FileWriter(filePath) , 表示以覆蓋的方式寫入
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath));
        bufferedWriter.write("hello, 韓順平教育!");
        bufferedWriter.newLine();//插入一個和系統相關的換行
        bufferedWriter.write("hello2, 韓順平教育!");
        bufferedWriter.newLine();
        bufferedWriter.write("hello3, 韓順平教育!");
        bufferedWriter.newLine();

        //說明:關閉外層流即可 , 傳入的 new FileWriter(filePath) ,會在底層關閉
        bufferedWriter.close();

9.4.3 應用實例

9.4.3.1 文本文件拷貝(BufferedReader和Writer實現)

BufferedReader及Writer是字符流,不能用來讀取二進制文件(如圖片、音樂)

        //老韓說明
        //1. BufferedReader 和 BufferedWriter 是安裝字符操作
        //2. 不要去操作 二進制文件[聲音,視頻,doc, pdf ], 可能造成文件損壞
        //BufferedInputStream
        //BufferedOutputStream
        String srcFilePath = "e:\\a.java";
        String destFilePath = "e:\\a2.java";
//        String srcFilePath = "e:\\0245_韓順平零基礎學Java_引出this.avi";
//        String destFilePath = "e:\\a2韓順平.avi";//要讀二進制文件,得用BufferedInputStream及OutputStream
        BufferedReader br = null;
        BufferedWriter bw = null;
        String line;
        try {
            br = new BufferedReader(new FileReader(srcFilePath));
            bw = new BufferedWriter(new FileWriter(destFilePath));

            //說明: readLine 讀取一行內容,但是沒有換行
            while ((line = br.readLine()) != null) {
                //每讀取一行,就寫入
                bw.write(line);
                //插入一個換行
                bw.newLine();
            }
            System.out.println("拷貝完畢...");

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //關閉流
            try {
                if(br != null) {
                    br.close();//關閉的是FileReader
                }
                if(bw != null) {
                    bw.close();//關閉的是FileWriter
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

9.5 節點流和包裝流

9.5.1 基本介紹與解析

注釋:

  1. 節點流(低級流/底層流)是比較低級的數據流,直接與特定數據源進行數據讀寫,並沒有提供強大的包裝功能,靈活性不高,效率不是很高。

  2. 對包裝流的深入理解:

包裝流是在節點流(底層流)之上的,但不同於節點流局限於同一個數據源,我們可以看到包裝流中(以Buffered為例),無論是writer還是reader都有一個父類的引用屬性Reader或者Writer,這樣就會導致在包裝流中可以實例化任何一個基礎於Writer或者Reader的子類(多態),如上圖的訪問管道流、訪問字符串流、訪問數組等等節點流。相當於用一種包裝流就可以操作所有的節點流,提高了操作的靈活性和性能。這樣的設計模式也叫做修飾器模式

9.5.2 區別與聯系

注釋:

  1. 可以在包裝器類定義一些便捷的方法來處理數據,這是在節點流當中不具備的,包裝器類擴展了節點流類的功能。

9.6 對象處理流(序列化)

9.6.1 序列化和反序列化

序列化:將程序中的數據能夠按照值和其類型一一保存到文件中。

反序列化:從文件中讀取數據,能夠恢復數據的值和類型。

9.6.2 對象處理流的基本介紹

遵循修飾者模式,要使用對象處理流,將其對應實現Input/OutputStream的子類傳進去即可。

舉例:ObjectInputStream因為是從文件向程序進行輸入的,所以是將數據的值和類型進行恢復,所以是反序列化。

9.6.3 ObjectOutputStream(序列化操作)

字節輸出流,從程序輸出到文件,因此是序列化操作。

.writeXX()相關方法

    //序列化
public static void main(String[] args) throws Exception {
        //序列化后,保存的文件格式,不是存文本,而是按照他的格式來保存
        String filePath = "e:\\data.dat";

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));

        //序列化數據到 e:\data.dat
        oos.writeInt(100);// int -> Integer 自動裝箱 (Integer的父類Number是實現了 Serializable 接口的)
        oos.writeBoolean(true);// boolean -> Boolean (實現了 Serializable)
        oos.writeChar('a');// char -> Character (實現了 Serializable)
        oos.writeDouble(9.5);// double -> Double (實現了 Serializable)
        oos.writeUTF("韓順平教育");//String (實現了 Serializable)
        //保存一個dog對象
        oos.writeObject(new Dog("旺財", 10, "日本", "白色"));//Dog須實現序列化(實現Serializable接口),不然要報錯
		
        oos.close();//關閉流
        System.out.println("數據保存完畢(序列化形式)");
    }

9.6.4 ObjectInputStream(反序列化操作)

恢復上一節序列化的操作:

  1. 讀取(反序列化)的順序需要和保存數據(序列化)的順序一致。
  2. .readXX()方法進行反序列化。
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        //指定反序列化的文件
        String filePath = "e:\\data.dat";

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));

        //讀取
        //老師解讀
        //1. 讀取(反序列化)的順序需要和你保存數據(序列化)的順序一致
        //2. 否則會出現異常
        System.out.println(ois.readInt());
        System.out.println(ois.readBoolean());
        System.out.println(ois.readChar());
        System.out.println(ois.readDouble());
        System.out.println(ois.readUTF());

        //dog 的編譯類型是 Object , dog 的運行類型是 Dog
        Object dog = ois.readObject();
        System.out.println("運行類型=" + dog.getClass());//dog
        System.out.println("dog信息=" + dog);//底層 Object -> Dog

        //這里是特別重要的細節:
        //1. 如果我們希望調用Dog的方法, 需要向下轉型
        //2. 需要我們將Dog類的定義,放在到可以引用的位置
        Dog dog2 = (Dog)dog;
        System.out.println(dog2.getName()); //旺財..

        //關閉流, 關閉外層流即可,底層會關閉 FileInputStream 流
        ois.close();
    }

9.6.5 對象處理流使用細節

注解:

1)反序列化順序要和序列化順序一致。

3)private static final long serialVersionUID = 1L; //serialVersionUID 序列化的版本號,可以提高兼容性,當添加新屬性后,不會被認為是新類,而是原先類的升級版

4)transient 表示該屬性不進行序列化保存數據;

5)如果成員屬性有沒有進行序列化的,會報錯,如下面代碼演示中的Master類必須進行序列化。

6)序列化支持繼承,只要父類實現了序列化,子類也默認實現了(此時無需顯示注明實現Serializable接口)。如下舉例:

Dog類實現序列化接口,演示:

//如果需要序列化某個類的對象,實現 Serializable
public class Dog implements Serializable {
    private String name;
    private int age;
    //序列化對象時,默認將里面所有屬性都進行序列化,但除了static或transient修飾的成員
    private static String nation;
    private transient String color;
    
    //序列化對象時,要求里面屬性的類型也需要實現序列化接口
    private Master master = new Master();//Master沒有序列化,所以會報錯

    //serialVersionUID 序列化的版本號,可以提高兼容性,當添加新屬性后,不會被認為是新類,而是原先類的升級版
    private static final long serialVersionUID = 1L;

    public Dog(String name, int age, String nation, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
        this.nation = nation;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", color='" + color + '\'' +
                '}' + nation + " " +master;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

9.7 標准輸入輸出流

public static void main(String[] args) {
        //System 類 的 public final static InputStream in = null;
        // System.in 編譯類型   InputStream
        // System.in 運行類型   BufferedInputStream
        // 表示的是標准輸入(鍵盤)
        System.out.println(System.in.getClass());

        //老韓解讀
        //1. System.out ——> public final static PrintStream out = null;
        //2. 編譯類型 PrintStream
        //3. 運行類型 PrintStream
        //4. 表示標准輸出(顯示器)
        System.out.println(System.out.getClass());

        System.out.println("hello, 韓順平教育~");//向顯示器標准輸出流

        Scanner scanner = new Scanner(System.in);//從鍵盤標准輸入流
        System.out.println("輸入內容");
        String next = scanner.next();
        System.out.println("next=" + next);
    }

9.8 轉換流

9.8.1 基本介紹

引出問題:一個中文亂碼問題。(引出指定讀取文件編碼方式的重要性)

轉換流的核心:可以將字節流轉換為字符流,並且字節流可以指定編碼方式(GBK、UTF-8等等)

InputStreamReader構造器可以指定處理的編碼,同理,OutputstreamWriter也有。

2021-09-03_205044

9.8.2 InputStreamReader 轉換流

/**
 * 演示使用 InputStreamReader 轉換流解決中文亂碼問題
 * 將字節流 FileInputStream 轉成字符流  InputStreamReader, 指定編碼 gbk/utf-8
 */
public class InputStreamReader_ {
    public static void main(String[] args) throws IOException {

        String filePath = "e:\\a.txt";
        //解讀
        //1. 把 FileInputStream 轉成 InputStreamReader
        //2. 指定編碼 gbk
        //InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "gbk");
        //3. 把 InputStreamReader 傳入 BufferedReader
        //BufferedReader br = new BufferedReader(isr);

        //將2 和 3 合在一起
        BufferedReader br = new BufferedReader(new InputStreamReader(
                                                    new FileInputStream(filePath), "gbk"));

        //4. 讀取
        String s = br.readLine();
        System.out.println("讀取內容=" + s);
        //5. 關閉外層流
        br.close();
    }
}

9.8.3 OutputStreamWriter 轉換流

/**
 * 演示 OutputStreamWriter 使用
 * 把FileOutputStream 字節流,轉成字符流 OutputStreamWriter
 * 指定處理的編碼 gbk/utf-8/utf8
 */
public class OutputStreamWriter_ {
    public static void main(String[] args) throws IOException {
        String filePath = "e:\\hsp.txt";
        String charSet = "utf-8";
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath), charSet);
        osw.write("hi, 韓順平教育");
        osw.close();
        System.out.println("按照 " + charSet + " 保存文件成功~");
    }
}

9.9 打印流

打印流只有輸出流,沒有輸入流

  1. PrintStream 字節流

    可以直接打印到文件里面,注意File

    /**
     * 演示PrintStream (字節打印流/輸出流)
     */
    public class PrintStream_ {
        public static void main(String[] args) throws IOException {
    
            PrintStream out = System.out;
            //在默認情況下,PrintStream 輸出數據的位置是 標准輸出,即顯示器
            /*   //print底層代碼其實就是調用write
                 public void print(String s) {
                    if (s == null) {
                        s = "null";
                    }
                    write(s);
                }
             */
            
            out.print("john, hello");
            //因為print底層使用的是write , 所以我們可以直接調用write進行打印/輸出
            out.write("韓順平,你好".getBytes());
            out.close();
    
            //我們可以去修改打印流輸出的位置/設備
            //1. 輸出修改成到 "e:\\f1.txt"
            //2. "hello, 韓順平教育~" 就會輸出到 e:\f1.txt
            //3. public static void setOut(PrintStream out) {
            //        checkIO();
            //        setOut0(out); // native 方法,修改了out
            //   }
            System.setOut(new PrintStream("e:\\f1.txt"));//修改輸出位置
            System.out.println("hello, 韓順平教育~");//輸出到 e:\f1.txt
        }
    }
    
  2. PrintWriter 字符流

/**
 * 演示 PrintWriter 使用方式
 */
public class PrintWriter_ {
    public static void main(String[] args) throws IOException {
        //PrintWriter printWriter = new PrintWriter(System.out);//打印到顯示器
        PrintWriter printWriter = new PrintWriter(new FileWriter("e:\\f2.txt"));//更改打印/輸出的位置
        printWriter.print("hi, 北京你好~~~~");
        printWriter.close();//flush + 關閉流, 才會將數據寫入到文件..
    }
}

9.10 Properties類

9.10.1 引出Properties

有時候需要對程序進行修改,但直接修改程序極為不靈活,所以才會引出properties配置文件的需求。

傳統方法:獲取數據麻煩,尤其是獲取指定數據更為麻煩,需要進行一系列判斷和處理。

public class Properties01 {
    public static void main(String[] args) throws IOException {
        //讀取mysql.properties 文件,並得到ip, user 和 pwd
        BufferedReader br = new BufferedReader(new FileReader("src\\mysql.properties"));
        String line = "";
        while ((line = br.readLine()) != null) { //循環讀取
            String[] split = line.split("=");
            //如果我們要求指定的ip值
            if("ip".equals(split[0])) {
                System.out.println(split[0] + "值是: " + split[1]);
            }
        }
        br.close();
    }
}

9.10.2 Properties基本介紹

注意:Properties類的底層是HashTable,配置文件的存放其實就是鍵值對。

set方法,如果對象中沒有相應的鍵值對,那么就會添加到Properties對象中

9.10.3 應用案例

  1. public class Properties02 {
        public static void main(String[] args) throws IOException {
            //使用Properties 類來讀取mysql.properties 文件
            //1. 創建Properties 對象
            Properties properties = new Properties();
            //2. 加載指定配置文件
            properties.load(new FileReader("src\\mysql.properties"));
            //3. 把k-v顯示控制台
            properties.list(System.out);
            //4. 根據key 獲取對應的值
            String user = properties.getProperty("user");
            String pwd = properties.getProperty("pwd");
            System.out.println("用戶名=" + user);
            System.out.println("密碼是=" + pwd);
        }
    }
    
  2. public class Properties03 {
        public static void main(String[] args) throws IOException {
            //使用Properties 類來創建 配置文件, 修改配置文件內容
    
            Properties properties = new Properties();
            //創建
            /*
                Properties 父類是 Hashtable , 底層就是Hashtable 核心方法
                public synchronized V put(K key, V value) {
                    // Make sure the value is not null
                    if (value == null) {
                        throw new NullPointerException();
                    }
    
                    // Makes sure the key is not already in the hashtable.
                    Entry<?,?> tab[] = table;
                    int hash = key.hashCode();
                    int index = (hash & 0x7FFFFFFF) % tab.length;
                    @SuppressWarnings("unchecked")
                    Entry<K,V> entry = (Entry<K,V>)tab[index];
                    for(; entry != null ; entry = entry.next) {
                        if ((entry.hash == hash) && entry.key.equals(key)) {
                            V old = entry.value;
                            entry.value = value;//如果key 存在,就替換
                            return old;
                        }
                    }
    
                    addEntry(hash, key, value, index);//如果是新k, 就addEntry
                    return null;
                }
    
             */
            // 擁有了三個鍵值對,現在的鍵值對在內存中
            //1.如果該文件沒有key 就是創建
            //2.如果該文件有key ,就是修改
            properties.setProperty("charset", "utf8");//底層就是HashTable
            properties.setProperty("user", "湯姆");//注意保存時,是中文的 unicode碼值
            properties.setProperty("pwd", "888888");
    
            //將k-v 存儲文件中即可,這里的null代表comments(注釋),一般用null即可
            properties.store(new FileOutputStream("src\\mysql2.properties"), null);//從內存中寫到文件中
            System.out.println("保存配置文件成功~");
        }
    }
    

程序執行完畢,查看配置文件時,發現中文變成了unicode碼,可以去查詢,發現就是湯姆。

9.11 本章作業

public class Homework01 {
    public static void main(String[] args) throws IOException {
        /**
         *(1) 在判斷e盤下是否有文件夾mytemp ,如果沒有就創建mytemp
         *(2) 在e:\\mytemp 目錄下, 創建文件 hello.txt
         *(3) 如果hello.txt 已經存在,提示該文件已經存在,就不要再重復創建了
         *(4) 並且在hello.txt 文件中,寫入 hello,world~

         */
        String directoryPath = "e:\\mytemp";
        File file = new File(directoryPath);
        if(!file.exists()) {
            //創建
            if(file.mkdirs()) {
                System.out.println("創建 " + directoryPath + " 創建成功" );
            }else {
                System.out.println("創建 " + directoryPath + " 創建失敗");
            }
        }

        String filePath  = directoryPath + "\\hello.txt";// e:\mytemp\hello.txt
        file = new File(filePath);
        if(!file.exists()) {
            //創建文件
            if(file.createNewFile()) {
                System.out.println(filePath + " 創建成功~");

                //如果文件存在,我們就使用BufferedWriter 字符輸入流寫入內容
                BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file));
                bufferedWriter.write("hello, world~~ 韓順平教育");
                bufferedWriter.close();

            } else {
                System.out.println(filePath + " 創建失敗~");
            }
        } else {
            //如果文件已經存在,給出提示信息
            System.out.println(filePath + " 已經存在,不在重復創建...");
        }
    }
}
public class Homework02 {
    public static void main(String[] args) {
        /**
         * 要求:  使用BufferedReader讀取一個文本文件,為每行加上行號,
         * 再連同內容一並輸出到屏幕上。
         */

        String filePath = "e:\\a.txt";
        BufferedReader br = null;
        String line = "";
        int lineNum = 0;
        try {
            br = new BufferedReader(new FileReader(filePath));
            while ((line = br.readLine()) != null) {//循環讀取
                System.out.println(++lineNum + line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            try {
                if(br != null) {
                    br.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Homework03 {
    public static void main(String[] args) throws IOException {
        /**
         * (1) 要編寫一個dog.properties   name=tom age=5 color=red
         * (2) 編寫Dog 類(name,age,color)  創建一個dog對象,讀取dog.properties 用相應的內容完成屬性初始化, 並輸出
         * (3) 將創建的Dog 對象 ,序列化到 文件 e:\\dog.dat 文件
         */
        String filePath = "src\\dog.properties";
        Properties properties = new Properties();
        properties.load(new FileReader(filePath));
        String name = properties.get("name") + ""; //Object -> String
        int age = Integer.parseInt(properties.get("age") + "");// Object -> int
        String color = properties.get("color") + "";//Object -> String

        Dog dog = new Dog(name, age, color);
        System.out.println("===dog對象信息====");
        System.out.println(dog);

        //將創建的Dog 對象 ,序列化到 文件 dog.dat 文件
        String serFilePath = "e:\\dog.dat";
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(serFilePath));
        oos.writeObject(dog);

        //關閉流
        oos.close();
        System.out.println("dog對象,序列化完成...");
    }

    //在編寫一個方法,反序列化dog
    @Test
    public void m1() throws IOException, ClassNotFoundException {
        String serFilePath = "e:\\dog.dat";
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(serFilePath));
        Dog dog = (Dog)ois.readObject();

        System.out.println("===反序列化后 dog====");
        System.out.println(dog);

        ois.close();

    }
}

class Dog implements  Serializable{
    private String name;
    private int age;
    private String color;

    public Dog(String name, int age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", color='" + color + '\'' +
                '}';
    }
}

第十章 網絡編程

計算機網絡基本知識已經省略

10.1 InetAddress

        //1. 獲取本機的InetAddress 對象
        InetAddress localHost = InetAddress.getLocalHost();
        System.out.println(localHost);//DESKTOP-S4MP84S/192.168.12.1

        //2. 根據指定主機名 獲取 InetAddress對象
        InetAddress host1 = InetAddress.getByName("DESKTOP-S4MP84S");
        System.out.println("host1=" + host1);//DESKTOP-S4MP84S/192.168.12.1

        //3. 根據域名返回 InetAddress對象, 比如 www.baidu.com 對應
        InetAddress host2 = InetAddress.getByName("www.baidu.com");
        System.out.println("host2=" + host2);//www.baidu.com / 110.242.68.4

        //4. 通過 InetAddress 對象,獲取對應的地址
        String hostAddress = host2.getHostAddress();//IP 110.242.68.4
        System.out.println("host2 對應的ip = " + hostAddress);//110.242.68.4

        //5. 通過 InetAddress 對象,獲取對應的主機名/或者的域名
        String hostName = host2.getHostName();
        System.out.println("host2對應的主機名/域名=" + hostName); // www.baidu.com

10.2 Socket

注意:通信結束后,必須手動close,不然可能會導致連接數過多,影響其他連接。

10.3 TCP字節流編程

10.3.1 應用案例1*

服務器端代碼:

// 服務器端代碼
public class SocketTcpServe01 {
    public static void main(String[] args) throws IOException {
        //思路
        //1. 在本機 的9999端口監聽, 等待連接
        //   細節: 要求在本機沒有其它服務在監聽9999
        //   細節:這個 ServerSocket 可以通過 accept() 返回多個Socket[多個客戶端連接服務器的並發]
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("服務端,在9999端口監聽,等待連接..");

        //2. 當沒有客戶端連接9999端口時,程序會阻塞, 等待連接
        //   如果有客戶端連接,則會返回Socket對象,程序繼續,阻塞結束
        Socket socket = serverSocket.accept();//阻塞,accept()方法返回與端口9999建立連接的服務器端socket對象

        System.out.println("服務端 socket =" + socket.getClass());

        //3. 通過socket.getInputStream() 讀取客戶端寫入到數據通道的數據, 顯示
        InputStream inputStream = socket.getInputStream();//從通信通道中讀取數據
        //4. IO讀取
        byte[] buf = new byte[1024];
        int readLen = 0;
        while ((readLen = inputStream.read(buf)) != -1) {
            System.out.println(new String(buf, 0, readLen));//根據讀取到的實際長度,顯示內容.
        }
        //5.關閉流和socket
        inputStream.close();//關閉流
        socket.close();//關閉當前與端口9999建立連接的通信通道
        serverSocket.close();//關閉端口9999
    }
}

客戶端代碼:

// 客戶端,發送 "hello, server" 給服務端
public class SocketTcpClient01 {
    public static void main(String[] args) throws IOException {
        //思路
        //1. 連接服務端 (ip , 端口)
        //解讀: 連接本機的 9999端口, 如果連接成功,返回Socket對象
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        System.out.println("客戶端 socket返回=" + socket.getClass());
        //2. 連接上后,生成Socket, 通過socket.getOutputStream()
        //   得到 和 socket對象關聯的輸出流對象
        OutputStream outputStream = socket.getOutputStream();//獲得建立通信通道的輸出流
        //3. 通過輸出流,寫入數據到 數據通道
        outputStream.write("hello, server".getBytes());//向建立的通信通道中寫入數據
        //4. 關閉流對象和socket, 必須關閉
        outputStream.close();
        socket.close();
        System.out.println("客戶端退出.....");
    }
}

10.3.2 應用案例2*

小結:本題可以在上一個案例的基礎上進行修改,本案例的最主要的關鍵點是設置通道寫入結束標記,如果不設置的話,會導致通道在等待輸出流向其寫入數據。

服務器端:

public class SocketTCP02Server {
    public static void main(String[] args) throws IOException {
        //思路
        //1. 在本機 的9999端口監聽, 等待連接
        //   細節: 要求在本機沒有其它服務在監聽9999
        //   細節:這個 ServerSocket 可以通過 accept() 返回多個Socket[多個客戶端連接服務器的並發]
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("服務端,在9999端口監聽,等待連接..");
        //2. 當沒有客戶端連接9999端口時,程序會 阻塞, 等待連接
        //   如果有客戶端連接,則會返回Socket對象,程序繼續

        Socket socket = serverSocket.accept();

        System.out.println("服務端 socket =" + socket.getClass());
        //
        //3. 通過socket.getInputStream() 讀取客戶端寫入到數據通道的數據, 顯示
        InputStream inputStream = socket.getInputStream();
        //4. IO讀取
        byte[] buf = new byte[1024];
        int readLen = 0;
        while ((readLen = inputStream.read(buf)) != -1) {
            System.out.println(new String(buf, 0, readLen));//根據讀取到的實際長度,顯示內容.
        }
        
        //======================新增=============================
        //5. 獲取socket相關聯的輸出流
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello, client".getBytes());
        //   設置結束標記
        socket.shutdownOutput();//結束輸入到通信通道
		//======================新增=============================
        
        //6.關閉流和socket
        outputStream.close();
        inputStream.close();
        socket.close();
        serverSocket.close();//關閉

    }
}

客戶端:

public class SocketTCP02Client {
    public static void main(String[] args) throws IOException {
        //思路
        //1. 連接服務端 (ip , 端口)
        //解讀: 連接本機的 9999端口, 如果連接成功,返回Socket對象
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        System.out.println("客戶端 socket返回=" + socket.getClass());
        //2. 連接上后,生成Socket, 通過socket.getOutputStream()
        //   得到 和 socket對象關聯的輸出流對象
        OutputStream outputStream = socket.getOutputStream();
        //3. 通過輸出流,寫入數據到 數據通道
        outputStream.write("hello, server".getBytes());
        
        //======================新增=============================
        //   設置結束標記
        socket.shutdownOutput();//輸出已結束

        //4. 獲取和socket關聯的輸入流. 讀取數據(字節),並顯示
        InputStream inputStream = socket.getInputStream();
        byte[] buf = new byte[1024];
        int readLen = 0;
        while ((readLen = inputStream.read(buf)) != -1) {
            System.out.println(new String(buf, 0, readLen));
        }
		//======================新增=============================
        
        //5. 關閉流對象和socket, 必須關閉
        inputStream.close();
        outputStream.close();
        socket.close();
        System.out.println("客戶端退出.....");
    }
}

10.4 TCP字符流編程

在字符流編程中,此時的結束標志換成了newLine()換行符。

服務器端:

public class SocketTcpServe03 {
    public static void main(String[] args) throws IOException {
        //思路
        //1. 在本機 的9999端口監聽, 等待連接
        //   細節: 要求在本機沒有其它服務在監聽9999
        //   細節:這個 ServerSocket 可以通過 accept() 返回多個Socket[多個客戶端連接服務器的並發]
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("服務端,在9999端口監聽,等待連接..");

        //2. 當沒有客戶端連接9999端口時,程序會阻塞, 等待連接
        //   如果有客戶端連接,則會返回Socket對象,程序繼續,阻塞結束
        Socket socket = serverSocket.accept();//阻塞,accept()方法返回與端口9999建立連接的服務器端socket對象

        System.out.println("服務端 socket =" + socket.getClass());

        // 從客戶端讀取數據顯示在服務器端
        //3. 通過socket.getInputStream() 讀取客戶端寫入到數據通道的數據, 顯示
        InputStream inputStream = socket.getInputStream();//從通信通道中讀取數據
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));

        //4. IO讀取
        System.out.println("客戶端數據:" + bufferedReader.readLine());

        // 向客戶端傳數據
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bufferedWriter.write("這里是服務器端傳字符流!");
        bufferedWriter.newLine();
        bufferedWriter.flush();

        //5.關閉流和socket
        bufferedWriter.close();
        bufferedReader.close();
        inputStream.close();//關閉流
        socket.close();//關閉當前與端口9999建立連接的通信通道
        serverSocket.close();//關閉端口9999
    }
}

客戶端:

public class SocketTCP03Client {
    public static void main(String[] args) throws IOException {
        //思路
        //1. 連接服務端 (ip , 端口)
        //解讀: 連接本機的 9999端口, 如果連接成功,返回Socket對象
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        System.out.println("客戶端 socket返回=" + socket.getClass());
        //2. 連接上后,生成Socket, 通過socket.getOutputStream()
        //   得到 和 socket對象關聯的輸出流對象
        OutputStream outputStream = socket.getOutputStream();
        //3. 通過輸出流,寫入數據到 數據通道, 使用字符流
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write("hello, server 字符流");
        bufferedWriter.newLine();//插入一個換行符,表示寫入的內容結束, 注意,要求對方使用readLine()!!!!
        bufferedWriter.flush();// 如果使用的字符流,需要手動刷新,否則數據不會寫入數據通道


        //4. 獲取和socket關聯的輸入流. 讀取數據(字符),並顯示
        InputStream inputStream = socket.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String s = bufferedReader.readLine();
        System.out.println(s);

        //5. 關閉流對象和socket, 必須關閉
        bufferedReader.close();//關閉外層流
        bufferedWriter.close();
        socket.close();
        System.out.println("客戶端退出.....");
    }
}

10.5 網絡上傳文件

一般來說,當我們寫數據完成后的設置結束標記,有兩種:

  1. socket.shutdownOutput();
  2. 兩邊設置readLine()方法

服務器代碼:

public class TCPFileUploadServer {
    public static void main(String[] args) throws Exception {
        // 1. 監聽本機9999端口
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("服務器正在監聽9999號端口..........");
        Socket accept = serverSocket.accept();//阻塞,等待客戶端
        System.out.println("與客戶端連接建立成功!");

        // 2. 設置輸入流,讀取客戶端傳來的文件數據
        BufferedInputStream bufferedInputStream = new BufferedInputStream(accept.getInputStream());
        byte[] bytes = StreamUtil.streamToByteArray(bufferedInputStream);//將讀取的流轉換為byte數組

        // 3. 將得到的文件byte數組,寫入到指定位置
        String destFile = "src\\Lalaland.jpg";
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(destFile));
        bufferedOutputStream.write(bytes);//在指定位置寫入數據
        System.out.println("服務器端:文件寫入完畢!");

        // 4. 向客戶端發出 文件接收成功 的消息
        // 通過socket 獲取到輸出流(字符)
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
        writer.write("服務器端已經收到圖片了!");
        writer.flush();//把內容刷新到數據通道
        accept.shutdownOutput();//設置寫入結束標記

        // 關閉資源
        writer.close();
        bufferedOutputStream.close();
        bufferedInputStream.close();
        accept.close();
        serverSocket.close();
    }
}

客戶端代碼:

public class TCPFileUploadClient {
    public static void main(String[] args) throws Exception {
        // 1. 建立與服務器端的連接
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        System.out.println("與服務器建立成功!");

        // 2. 將文件轉為byte數組
        String filePath = "d:\\LalaLand.jpg";
        BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));// 獲取文件輸入流
        byte[] bytes = StreamUtil.streamToByteArray(bufferedInputStream);//轉為byte數組

        // 3. 向socket建立的通道獲得輸出流,傳送文件byte數組
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(socket.getOutputStream());//獲得輸出流
        bufferedOutputStream.write(bytes);//寫數據
        System.out.println("客戶端:文件傳送完畢!");
        socket.shutdownOutput();//設置寫出數據的結束符

        // 4. 接收服務器端發來 文件接收成功 的消息
        BufferedInputStream bufferedInputStream1 = new BufferedInputStream(socket.getInputStream());// 設置輸入流
        String s = StreamUtil.streamToString(bufferedInputStream1);
        System.out.println(s);

        // 關閉資源
        bufferedOutputStream.close();//關閉輸出流
        bufferedInputStream.close();
        bufferedInputStream1.close();
        socket.close();
    }
}

10.6 netstat

管理員權限下,netstat -anb 可以查看是哪個程序在使用。

10.7 TCP連接的秘密

建立連接時,客戶端的端口是隨機分配的,而服務器端始終是固定。

10.8 UDP編程

10.8.1 UDP原理

UDP原理的注意事項:

  1. UDP建立連接的兩端,沒有明確的客戶端和服務器端,因為接收端也有可能變成發送端,發送端也有可能變成接收端。

  2. 數據的接發送對象不再是Socket和ServerSocket,而是DatagramSocket。

  3. 數據需要進行裝包和拆包。

上圖DatagramPacket構造方法中,可以觀察到:

可以指定主機地址和端口號,進行數據打包。

10.8.2 應用案例

服務端A:

public class UDPReceiverA {
    public static void main(String[] args) throws IOException {
        //1. 創建一個 DatagramSocket 對象,准備在9999接收數據
        DatagramSocket socket = new DatagramSocket(9999);

        //2. 構建一個 DatagramPacket 對象,准備接收數據
        //   在前面講解UDP 協議時,老師說過一個數據包最大 64k
        byte[] buf = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buf, buf.length);//接收數據也要進行封裝打包

        //3. 調用 接收方法, 將通過網絡傳輸的 DatagramPacket 對象
        //   填充到 packet對象
        //老師提示: 當有數據包發送到 本機的9999端口時,就會接收到數據
        //   如果沒有數據包發送到 本機的9999端口, 就會阻塞等待.
        System.out.println("接收端A 等待接收數據..");
        socket.receive(packet);//阻塞等待數據

        //4. 可以把packet 進行拆包,取出數據,並顯示.
        int length = packet.getLength();//實際接收到的數據字節長度
        byte[] data = packet.getData();//接收到數據,拆包
        String s = new String(data, 0, length);//toString,然后打印
        System.out.println(s);


        //==============回復信息給B端================
        //將需要發送的數據,封裝到 DatagramPacket對象
        data = "好的, 明天見".getBytes();
        //說明: 封裝的 DatagramPacket對象 data 內容字節數組 , data.length , 主機(IP) , 端口
        packet =
                new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 9998);//向9998發送數據

        socket.send(packet);//發送

        //5. 關閉資源
        socket.close();
        System.out.println("A端退出...");
    }
}

服務端B:

public class UDPSenderB {
    public static void main(String[] args) throws IOException {
        //1.創建 DatagramSocket 對象,准備在9998端口 接收數據
        //注意在UDP連接中,如果在同一台主機上,各個服務端占據屬於自己的端口,進行接收發送數據的請求
        DatagramSocket socket = new DatagramSocket(9998);

        //2. 將需要發送的數據,封裝到 DatagramPacket對象
        byte[] data = "hello 明天吃火鍋~".getBytes(); //

        //說明: 封裝的 DatagramPacket對象 data 內容字節數組 , data.length , 主機(IP) , 端口
        DatagramPacket packet =
                new DatagramPacket(data, data.length, InetAddress.getByName("192.168.12.1"), 9999);

        socket.send(packet);

        //3.====接收從A端回復的信息=================
        //(1)   構建一個 DatagramPacket 對象,准備接收數據
        byte[] buf = new byte[1024];
        packet = new DatagramPacket(buf, buf.length);//packet已經重新引用了
        //(2)    調用 接收方法, 將通過網絡傳輸的 DatagramPacket 對象
        //   填充到 packet對象
        //老師提示: 當有數據包發送到 本機的9998端口時,就會接收到數據
        //   如果沒有數據包發送到 本機的9998端口, 就會阻塞等待.
        socket.receive(packet);//等待數據發到自己這里9998端口

        //(3)  可以把packet 進行拆包,取出數據,並顯示.
        int length = packet.getLength();//實際接收到的數據字節長度
        data = packet.getData();//接收到數據
        String s = new String(data, 0, length);
        System.out.println(s);

        //關閉資源
        socket.close();
        System.out.println("B端退出");
    }
}

10.9 本章作業

10.9.1 TCP、UDP

客戶端

public class HW01Client {
    public static void main(String[] args) throws IOException {
        // 1. 客戶端向本機端口9999發送數據,需要先建立連接
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        System.out.println("客戶端與服務器連接建立成功!");

        // 2. 獲取用戶輸入
        System.out.println("請輸入發往服務器的內容:");
        Scanner scanner = new Scanner(System.in);
        String question = scanner.next();

        // 3. 使用包裝流進行輸出流的封裝,對服務器發出消息
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bufferedWriter.write(question);
        bufferedWriter.newLine();//結束標志
        bufferedWriter.flush();//刷新,確認寫入

        //4. 接收服務器發來的回復
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        System.out.println(bufferedReader.readLine());

        // 5. 關閉資源
        bufferedReader.close();
        bufferedWriter.close();
        socket.close();
    }
}

服務器端:

public class HW01Server {
    public static void main(String[] args) throws IOException {
        // 1. 服務器端在9999端口監聽
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("等待連接........");
        Socket sockets = serverSocket.accept();//等待客戶端程序
        System.out.println("連接建立成功!");

        // 2. 服務器端獲取客戶端發來的內容
        // 按照字符流進行傳送數據,因此需要使用轉換流InputStreamReader將socket的字節流進行轉換,最終使用包裝流進行操作
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(sockets.getInputStream()));

        // 3. 根據客戶端發來的內容進行相應處理
        // 獲取寫入流對象
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(sockets.getOutputStream()));

        // 進行判斷處理
        String s = bufferedReader.readLine();
        if (s.equals("name")) {
            bufferedWriter.write("我是nova");
        } else if (s.equals("hobby")) {
            bufferedWriter.write("編寫Java程序");
        } else {
            bufferedWriter.write("非法輸入");
        }

        System.out.println("向客戶端發送回復完畢!");

        bufferedWriter.newLine();//設置結束標志
        bufferedWriter.flush();//刷新,確認寫入

        // 4. 關閉資源
        bufferedWriter.close();
        bufferedReader.close();
        sockets.close();
        serverSocket.close();
    }
}
  1. 發送服務端:

public class Homework02SenderB {
    public static void main(String[] args) throws IOException {

        //1.創建 DatagramSocket 對象,准備在9998端口 接收數據
        DatagramSocket socket = new DatagramSocket(9998);

        //2. 將需要發送的數據,封裝到 DatagramPacket對象
        Scanner scanner = new Scanner(System.in);
        System.out.println("請輸入你的問題: ");
        String question = scanner.next();
        byte[] data = question.getBytes(); 

        DatagramPacket packet =
                new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 8888);

        socket.send(packet);

        byte[] buf = new byte[1024];
        packet = new DatagramPacket(buf, buf.length);
        socket.receive(packet);

        int length = packet.getLength();//實際接收到的數據字節長度
        data = packet.getData();//接收到數據
        String s = new String(data, 0, length);
        System.out.println(s);

        //關閉資源
        socket.close();
        System.out.println("B端退出");
    }
}

接收服務端:

public class Homework02ReceiverA {
    public static void main(String[] args) throws IOException {
        DatagramSocket socket = new DatagramSocket(8888);
        byte[] buf = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buf, buf.length);
        System.out.println("接收端 等待接收問題 ");
        socket.receive(packet);

        int length = packet.getLength();//實際接收到的數據字節長度
        byte[] data = packet.getData();//接收到數據
        String s = new String(data, 0, length);

        String answer = "";
        if("四大名著是哪些".equals(s)) {
            answer = "四大名著 <<紅樓夢>> <<三國演示>> <<西游記>> <<水滸傳>>";
        } else {
            answer = "what?";
        }

        //===回復信息給B端
        data = answer.getBytes();
        packet =
                new DatagramPacket(data, data.length, InetAddress.getLocalHost(), 9998);

        socket.send(packet);//發送

        socket.close();
        System.out.println("A端退出...");

    }
}

10.9.2 文件下載

服務器端程序:

public class Homework03Server {
    public static void main(String[] args) throws Exception {
        //1 監聽 9999端口
        ServerSocket serverSocket = new ServerSocket(9999);
        //2.等待客戶端連接
        System.out.println("服務端,在9999端口監聽,等待下載文件");
        Socket socket = serverSocket.accept();//阻塞等待客戶端連接
        //3.讀取 客戶端發送要下載的文件名
        //  這里老師使用了while讀取文件名,時考慮將來客戶端發送的數據較大的情況
        InputStream inputStream = socket.getInputStream();
        byte[] b = new byte[1024];
        int len = 0;
        String downLoadFileName = "";
        while ((len = inputStream.read(b)) != -1) {
            downLoadFileName += new String(b, 0 , len);
        }
        System.out.println("客戶端希望下載文件名=" + downLoadFileName);

        //老師在服務器上有兩個文件, 無名.mp3 高山流水.mp3
        //如果客戶下載的是 高山流水 我們就返回該文件,否則一律返回 無名.mp3

        String resFileName = "";
        if("高山流水".equals(downLoadFileName)) {
            resFileName = "src\\高山流水.mp3";
        } else {
            resFileName = "src\\無名.mp3";
        }

        //4. 創建一個輸入流,讀取文件
        BufferedInputStream bis =
                new BufferedInputStream(new FileInputStream(resFileName));

        //5. 使用工具類StreamUtils ,讀取文件到一個字節數組

        byte[] bytes = StreamUtils.streamToByteArray(bis);
        //6. 得到Socket關聯的輸出流
        BufferedOutputStream bos =
                new BufferedOutputStream(socket.getOutputStream());
        //7. 寫入到數據通道,返回給客戶端
        bos.write(bytes);
        socket.shutdownOutput();//很關鍵.

        //8 關閉相關的資源
        bis.close();
        inputStream.close();
        socket.close();
        serverSocket.close();
        System.out.println("服務端退出...");

    }
}

客戶端程序:

public class Homework03Client {
    public static void main(String[] args) throws Exception {
        //1. 接收用戶輸入,指定下載文件名
        Scanner scanner = new Scanner(System.in);
        System.out.println("請輸入下載文件名");
        String downloadFileName = scanner.next();

        //2. 客戶端連接服務端,准備發送
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        //3. 獲取和Socket關聯的輸出流
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(downloadFileName.getBytes());
        //設置寫入結束的標志
        socket.shutdownOutput();

        //4. 讀取服務端返回的文件(字節數據)
        BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
        byte[] bytes = StreamUtils.streamToByteArray(bis);
        //5. 得到一個輸出流,准備將 bytes 寫入到磁盤文件
        //比如你下載的是 高山流水 => 下載的就是 高山流水.mp3
        //    你下載的是 韓順平 => 下載的就是 無名.mp3  文件名 韓順平.mp3
        String filePath = "e:\\" + downloadFileName + ".mp3";
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
        bos.write(bytes);

        //6. 關閉相關的資源
        bos.close();
        bis.close();
        outputStream.close();
        socket.close();

        System.out.println("客戶端下載完畢,正確退出..");
    }
}

10.10 項目-多用戶通信系統

看老韓PDF


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM