Java 进阶总结 - Zhangyao719/java-study GitHub Wiki

一、static 关键字

特点

  1. 静态成员属于类的成员,不属于实例对象的成员(非静态的成员属于对象成员);
  2. 静态成员会随着类的加载而加载;
  3. 静态成员优先于非静态成员存在在内存中且在内存中只有一份;
  4. 静态成员可以被类和类的所有实例对象共享。
public class Student {
    // 静态变量
    static int total;
    // 实例变量
    String name;

    // 静态方法
    public static void fn1() {
        System.out.println("类和对象都能调用静态方法");
        System.out.println("只能访问静态成员(变量和方法)");
        System.out.println("不能使用 this");
    }

    public void fn2() {
        System.out.println("只有对象能调用");
        System.out.println("可以访问静态变量:" + total);
        System.out.println("可以调用静态方法");
        fn1();
    }
}

注意事项

  1. 类方法中可以直接访问类的成员,不可以直接访问实例成员;
  2. 实例方法中既可以直接访问类的成员,也可以直接访问实例成员;
  3. 实例方法中可以出现 this 关键字(代表当前对象),类方法中不能出现 this 关键字。

二、代码块

静态代码块

public class Test {
    public static ArrayList<String> users = new ArrayList();

    // 静态代码块
    static {
        System.out.println("类加载时执行");
        users.add("张三");
        users.add("李四");
    }

    public static void main(String[] args) {
        System.out.println(users); // ["张三", "李四"]
    }
}
  1. 类加载时自动执行,由于类只会加载一次,所以静态代码块也只会执行一次;
  2. 完成类的初始化,对类变量进行初始化赋值。

实例代码块

public class Test {
    // 实例代码块
    {
        System.out.println("创建对象时先执行");
    }

    // 构造器
    public Test() {
        System.out.println("构造器后执行");
    }

    public static void main(String[] args) {
        new Test()
    }
}
  1. 每次创建对象时,执行实例代码块,并在构造器之前执行;
  2. 和构造器一样,都用来完成对象的初始化。

三、static 与单例设计模式

确保一个类只有一个对象

写法

  1. 把类的构造器私有(不允许被直接拿来实例化);
  2. 定义一个类变量记住类的一个对象;
  3. 定义一个类方法,返回对象;
public class SingleA {
    // 2. 静态变量保存实例对象
    private static SingleA a = new SingleA();
    
    // 1. 私有化构造器
    private SingleA() {}

    // 3. 封装一个静态方法暴露实例对象(防止被外部篡改)
    public static SingleA getSingleInstance() {
        return a;
    }
}
public class SingleB {
    private static SingleB b;

    private SingleB() {}

    public static SingleB getSingleInstance() {
        // 在调用方法时才开始创建单例
        if (b == null) {
            b = new SingleB();
        }
        return b;
    }
}

四、继承

特点

  1. 子类能继承父类的非私有成员(变量和方法);
  2. 子类创建的对象包含子类和父类的成员,但是能否访问取决于成员的访问权限。
public class Father {
    public int i;
    private int j;

    public void printI() {
        System.out.println("i = " + i);
    }

    private void printJ() {
        System.out.println("j = " + j);
    }

    public int getJ() {
        return j 
    }
}
public class Son extends Father {
    public int k;

    public void printK() {
        System.out.println("k = " + k);
    }
}
public class Test {
    public static void main(String[] args) {
        Son s1 = new Son();
        System.out.println(s1.i); // success,父类实例变量
        System.out.println(s1.j); // error,私有变量无法访问
        System.out.println(s1.k); // success,子类实例变量

        s1.printI(); // success
        s1.printJ(); // error
        System.out.println("使用 get 获取父类私有变量:" + s1.getJ()); // success
    }
}

权限修饰符

权限修饰符 访问范围
private 只能本类
缺省 本类、同一个包中的类
protected 本类,同一个包中的类,子类
public 任意位置

单继承

  1. Java 是单继承,一个类只能继承一个直接父类;
  2. Java 不支持多继承,但支持多层继承;
// Java 不允许一次多继承
class A extends B, C {}

// Java 支持多层继承
class D {}
class E extends D {}
class F extends E {}
  1. Java 中所有的类都继承于Object

方法重写

特点

子类可以重写一个方法名和参数列表一样,但是内部处理逻辑不同的方法来覆盖父类中的同名方法,以满足自己的特定需求,这就是方法重写。

注意事项

  1. 使用 @Override注解,它可以制定 Java 编译器,检查我们方法重写的格式是否正确,代码可读性也会更好;
  2. 访问权限必须大于或等于父类方法的权限(public > protected > 缺省);
  3. 返回值类型必须与父类的方法返回值类型一致,或者范围更小;
  4. 私有方法、静态方法不能重写,否则报错。

super 访问父类成员

访问同名成员,默认依据就近原则,也可以使用关键字指定要访问的成员。

  1. this访问自己的成员;
  2. super访问父类的成员;
class Father {
    String name = "父类名称";
}

class Son extends Father {
    String name= "子类名称";

    public void showName() {
        String name = "局部名称";

        System.out.println(name); // 就近原则 "局部名称"
        System.out.println(this.name); // "子类名称"
        System.out.println(super.name); // "父类名称"
    }
}
class Father {
    @Override
    public void run() {
        System.out.println("子类方法");
    }

    public void go() {
        run(); // 执行子类的方法
        super.run(); // 执行父类的方法
    }
}

class Son extends Father {
    public void run() {
        System.out.println("父类方法");
    }
}

子类构造器

特点

子类的全部构造器(有参或无参)都会先调用父类的无参构造器,再执行自己。

public class Animal {
    public Animal() {
        System.out.println("父类无参构造器");
    }

    public Animal(String name) {
        System.out.println("父类有参构造器");
    }
}

public class Tiger extends Animal {
    String name;
    public Tiger() {
        System.out.println("子类无参构造器");
    }

    public Tiger(String name) {
        System.out.println("子类有参构造器");
        this.name = name;
    }
}

// 都会先执行父类的无参构造器
Tiger tiger1 = new Tiger();
Tiger tiger2 = new Tiger("Tom");

注意事项

  1. 默认情况下,子类全部构造器第一行代码都是 super(),它会调用父类的无参构造器;
  2. 如果父类没有无参构造器(只写了有参构造器),则必须手动在子类构造器的第一行加上 super(...),指定父类的有参构造器。

调用 super() 初始化

子类构造器通过 super()调用父类构造器,把对象中包含父类的部分数据先初始化赋值;

再把对象中包含子类的部分数据也初始化赋值。

public class Animal {
    private String kind;

    public Animal() {
    }

    public Animal(String kind) {
        this.kind = kind;	
    }

    public String getKind() {
        return kind;
    }
}

public class Tiger extends Animal {
    private String skill;

    public Tiger() {
    }

    public Tiger(String kind, String skill) {
        // 调用 super 初始化父类的私有变量
        super(kind);
        this.skill = skill;
    }

    public String getSkill() {
        return skill;
    }

    public void setSkill(String skill) {
        this.skill = skill;
    }
}


public class Test {
    public static void main(String[] args) {
        Tiger tiger = new Tiger("老虎", "捕食");
        System.out.println(tiger.getSkill());
        System.out.println(tiger.getKind());
    }
}

this() 调用兄弟构造器

  1. 使用 this(),调用兄弟构造器;
  2. this()super()不能同时出现,因为兄弟构造器内部也会执行 super(),会导致父类构造器执行两次;
  3. this()super()必须在第一行;
public class Animal {
    private String kind;
    private String habit;
    private String desc;

    public Animal() {
    }

    public Animal(String kind, String habit) {
        // 使用 this(),调用兄弟构造器
        this(kind, habit, "动物大世界");
    }
    
    public Animal(String kind, String habit, String desc) {
        this.kind = kind;
        this.desc = desc;
        this.habit = habit;
    }
}

五、多态

定义

  1. 一定有继承或实现关系;
  2. 存在父类引用子类对象;
  3. 存在方法重写。
public class Animal {
    String name = "动物";
    public void bark() {
        System.out.println("动物的叫声");
    }
}

// 1. Dog 继承自 Animal
public class Dog extends Animal {
    String name = "狗";

    // 2. 存在方法重写
    @Override
    public void bark() {
        System.out.println("汪汪汪");
    }

    public void guard() {	
        System.out.println("看门");
    }
}

// 3. 存在父类引用子类对象
Animal dog = new Dog(); // 编译看左边,执行看右边
dog.bark();
// 5. 不能调用子类独有的功能
dog.guard(); // error,不能调用 Dog 独有的功能,因为 Animal 没有 Dog 独有的 guard() 方法

// 4. Java 中的属性(成员变量)没有多态
System.out.println(dog.name); // 依旧是 "动物的叫声"

注意事项

  1. 多态是对象和行为的多态,Java 中的属性(成员变量)没有多态;
  2. 不能调用子类独有的功能;

类型转换

  1. 只要有继承或实现关系的两个类就可以强转;
  2. 编译时不会报错,但可能出现强制类型转换异常;
Animal dog = new Dog();

dog.guard(); // error,Animal 没有 guard()

Dog dogCopy = (Dog) dog;
dogCopy.guard(); // success,Animal 强制转换成了 Dog

Cat catCopy = (Cat) dog; // success,没有报错,但其实把 dog 转成 cat 是不对的
catCopy.guard(); // 报错 cat 没有 guard()

instanceof 与类型校验

建议类型收窄时,使用 instanceof进行校验

if (dog instanceof Dog) {
    Dog dogCopy = (Dog) dog;
    dogCopy.guard();
}

六、final

可以修饰类、方法、变量

特点

  1. 修饰类:被 final修饰的类成为最终类,不能再被继承了;
  2. 修饰方法:被 final修饰的方法成为最终方法,不能被重写了;
  3. 修饰变量:被 final修饰的变量只能被赋值一次。
public class Test {
    public static final String USER_NAME = "张三";
    // 或者
    public static final int USER_AGE;
    static {
        USER_AGE = 18;
    }
}

七、抽象类

特点

  1. 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类;
  2. 类该有的成员(成员变量、方法、构造器)抽象类都可以有;
  3. 抽象类不能创建对象,仅作为一中特殊的父类,用来给子类继承并实现;
  4. 一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
public abstract class A {
    // 只有签名,没有具体实现
    public abstract void go()
}

public class B extends A {
    // 重写抽象类中定义的抽象方法
    @Override
    public void go() {
        System.out.println("重写抽象类的抽象方法");
    }
}

B b = new B();
b.go();

八、abstract 与模板方法设计模式

  1. 解决方法中存在的重复代码的问题;
  2. 建议使用 final保护模板方法,避免被重写导致失效;
public abstract class People {
    public final void write() {
        System.out.println("《标题》");

        // 替换中间自定义的内容
        writeContent();

        System.out.println("结尾");
    }

    public abstract void writeContent();
}

public class Student extends People {
    @Override
    public void writeContent() {
        System.out.println("我爱上课");
    }
}

public class Teacher extends People {
    @Override
    public void writeContent() {
        System.out.println("我爱教课");
    }
}

People s = new Student()
s.write()

People t = new Teacher()
t.write()

九、接口

特点

  1. 接口不能创建对象;
  2. 接口用来被类实现implements,实现接口的类被称为实现类
  3. 接口与接口是多继承,一个接口可以同时继承多个接口;
  4. 实现类实现多个接口,必须重写完全部接口定义的全部抽象方法,否则这个类必须是抽象类;
public interface A {
    // 定义常量,可以省略 public static final
    String USER_NAME = "张三";

    // 定义抽象方法,可以省略 public abstract
    void run();
}

public interface B {
    void eat();
}

// 实现类
public class BImpl implements A, B {
    @Override
    public void run() {
        
    }

    @Override
    public void eat() {
        
    }
}

BI s1 = new BImpl();
A s2 = new BImpl();
B s3 = new BImpl();
interface A {
    void a()
}

interface B {
    void b()
}

interface C extends A, B {
    void c()
}

JDK 8+ 新增的三种接口方法

  1. default修饰默认方法(普通方法);
  2. private修饰私有方法,可以通过接口内部的普通方法调用;
  3. static修饰静态方法,只能用接口本身调用;
public interface A {
    // 1. default 默认方法
    default void go() {
        run();
        System.out.println("普通的实例方法");
    }

    // 2. private 私有方法
    // 可以通过接口内部的普通方法调用
    private void run() {
        System.out.println("调用接口内部私有方法");
    }

    static void hello() {
        System.out.println("只能用接口本身调用");
    }
}

// 通过实现类调用普通方法
public class AImpl implements A {}


AImpl instance = new AImpl();
instance.go();

// 直接调用接口执行接口的静态方法
A.hello();

注意事项

  1. 一个接口继承多个接口,如果多个接口中存在方法签名冲突(比如返回值类型不一样),则此时不支持多继承;
  2. 一个类实现多个接口,如果多个接口中存在方法签名冲突,则此时不支持多实现;
  3. 一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会优先使用父类的;
  4. 一个类实现了多个接口,多个接口中存在同名默认方法,可以不冲突,这个类重写该方法即可。

十、内部类

类的五大成分:成员变量、方法、构造器、内部类、代码块。

如果一个类定义在另一个类的内部,这个类就是内部类。

形式1:成员内部类(了解)

  1. 可以直接访问外部类的实例成员、静态成员;
  2. 可以使用外部类名.this 获取当前外部类对象;
public class People {
    private String name = "张三";

    // 成员内部类
    public class User {
        private String name = "李四";

        public void show() {
            String name = "王五";
            System.out.println(name); // 王五
            System.out.println(this.name); // 李四
            System.out.println(People.this.name); // 张三
        }
    }
}

// 创建成员内部类实例对象
People.User user = new People().new User();

形式2:静态内部类(了解)

  1. 可以直接访问外部类的静态成员,不能访问外部类的实例成员
public class People {
    private String name = "张三";
    private static double height;

    // 静态内部类
    public static class User {
        public void show() {
            // 可以访问外部类的静态成员
            System.out.println(height); // success

            // 不可以访问外部类的实例成员
            System.out.println(name); // error
        }
    }
}

// 创建静态内部类实例对象
People.User user = new People.User();

形式3:局部内部类(了解)

  1. 定义在方法、代码块、构造器等执行体中;
public class Test {
    public static void main(String[] args) {
        class A {
            // 鸡肋语法,没啥卵用
        }
    }
}

形式4:匿名内部类 ★★★

  1. 特殊的局部内部类,匿名指不需要为这个类申明名字;
  2. 本质是一个子类,并会立即创建出一个子类对象
  3. 用于更方便的创建一个子类对象;
public class Test {
    public static void main(String[] args) {
        // 创建匿名内部类对象
        // 匿名内部类的名称:“当前类名$编号”
        Animal dog = new Animal() {
            @Override
            public void eat() {
                System.out.println("小狗吃骨头");
            }
        };
        dog.eat();
    }
}

abstract class Animal {
    public abstract void eat();
}

使用场景

作为一个对象参数传输给方法使用

public class Test {
    public static void main(String[] args) {
        // 直接实现 Swimming 接口,并立即创建一个实例对象
        Swimming s1 = new Swimming() {
            @Override
            public void swim() {
                System.out.println("蛙泳");
            }
        };
        go(s1);

        go(new Swimming() {
            @Override
            public void swim() {
                System.out.println("自由泳");
            }
        });
    }

    public static void go(Swimming s) {
        s.swim();
    }
}

interface Swimming {
    void swim();
}

十一、枚举

特点

  1. 枚举类的第一行只能罗列一些名称,这些名称都是常量,并且每个常量记住的都是枚举类的一个对象;
  2. 枚举类的构造器是私有的,所以枚举类对外不能创建对象;
  3. 枚举类是最终类,不可以被继承;
  4. 枚举中,从第二行开始,可以定义类的其他各种成员;
  5. 编译器为枚举类新增了几个方法,并且枚举类都是继承:java.lang.Enum 类的,从 enum 类也会继承到一些方法。
public enum A {
    X, Y, Z

    // 也可以定义其他成员
    private int age;
    public void setAge(int age) {
        this.age = age;
    }
    public int getAge() {
        return age;
    }
}

public class Test {
    public static void main(String[] args) {
        A x = A.X;
        A y = A.Y;
        A z = A.Z;
        System.out.println(x + " " + y + " " + z);


        // 1. values 方法用来获取枚举类的全部对象,返回一个数组
        A[] list = A.values();
        for (int i = 0; i < list.length; i++) {
            A item = list[i];
            System.out.println(item);
        }

        // 2. valueOf 返回具有指定名称的指定枚举类型的枚举常量
        A z2 = A.valueOf("Z");
        System.out.println(z2 == z); // true

        // 3. ordinal 返回此枚举常量的序数索引
        System.out.println(x.ordinal()); // 0
        System.out.println(y.ordinal()); // 1
    }
}

使用场景

用作信息标志和信息分类

public enum ActionType {
    DOWN, UP, HALF_UP, DELETE_LEFT;
}

public class Test {
    public static void main(String[] args) {
        handleData(3.9, ActionType.DOWN);
    }

    public static double handleData(double data, ActionType type) {
        switch(type) {
            case DOWN:
                data = Math.floor(data);
                break;
            case UP:
                data = Math.ceil(data);
                break;
            case HALF_UP:
                data = Math.round(data);
                break;
            case DELETE_LEFT:
                data = (int) data;
                break;
        }
        return data;
    }
}

十二、泛型

泛型变量建议用大写字母,比如,E、T、K、V 等。

泛型类

修饰符 class 类名<类型变量,类型变量,......>

泛型接口

修饰符 interface 接口名<类型变量,类型变量,......>

泛型方法、通配符、上下限

修饰符 <类型变量,类型变量,......> 返回值类型 方法名(形参列表) {}

public static <T> T printArray(T[] a) {}
  • 通配符 ? 表示任意类型;
  • 上下限 ? extends Car 表示上限必须是 Car 或 Car 的子类;
  • 上下限 ? super Car 表示下限必须是 Car 或 Car 的父类;
public static void go(ArrayList<? extends Car> car) {}

泛型的擦除问题

  1. 泛型工作在编译阶段,一旦程序编译成 class 文件,class 文件中就不存在泛型了,这就是泛型擦除;
  2. 泛型不能直接支持基本数据类型,只能支持对象类型(引用数据类型);

十三、Lambda 表达式

定义

只能简化函数式接口匿名内部类代码。

(被重写方法的形参列表) -> {

被重写的方法;

}

函数式接口

  • 有且仅有一个抽象方法的接口;
  • 在大部分的函数式接口上,会有一个 @FunctionalInterface的注解,有该注解的接口必定是函数式接口。

案例

public class Test {
    public static void main(String[] args) {
        Animal cat = new Animal() {
            @Override
            public void eat() {
                System.out.println("小猫吃鱼");
            }
        };

        // 简化函数式接口
        Animal dog = () -> {
            System.out.println("小狗吃肉");
        };
        dog.eat();
    }
}

@FunctionalInterface
interface Animal {
    //    有且仅有一个抽象方法
    void eat();
}

简化规则

  1. 参数类型可以不写;
  2. 如果只有一个参数,参数类型可以省略,同时()也可以省略;
  3. 如果 Lambda 表达式中的方法体代码只有一行代码,可以省略大括号不写,同时要省略分号。此时,如果这行代码是 return 语句,也必须去掉 return 不写。
// 简化前:
Arrays.sort(list, new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return Double.compare(o1.getHeight(), o2.getHeight());
    }
});

// 简化后:
Arrays.sort(list, (o1, o2) -> Double.compare(o1.getHeight(), o2.getHeight()));

十四、方法引用

静态方法的引用

如果某个 Lambda 表达式里只是调用一个静态方法,并且前后参数的形式一致(传递的参数列表和接受的参数列表),就可以使用静态方法引用。

类名::静态方法

public class Student {
    public static int compareByHeight(Student o1, Student o2) {
        return Double.compare(o1.getHeight(), o2.getHeight());
    }    
}

public class Test {
    public static void main(String[] args) {
        Student[] list = new Student[2];
        list[0] = new Student("张三", 18, '男', 1.72);
        list[1] = new Student("李四", 19, '男', 1.82);

        // 调用静态方法
        Arrays.sort(list, (o1, o2) -> Student.compareByHeight(o1, o2));
        
        // 简写:
        // 1. 调用的是 compareByHeight 静态方法;
        // 2. 前面传递的参数列表和后面接受的参数列表一致;
        Arrays.sort(list, Student::compareByHeight);
    }
}

实例方法的引用

与静态方法的引用类似

public class Test {
    public static void main(String[] args) {
        Test t = new Test();
        Arrays.sort(list, t::compareByHeight);
    }

    public int compareByHeight(Student o1, Student o2) {
        return Double.compare(o1.getHeight(), o2.getHeight());
    }
}

特定类型方法的引用

如果某个Lambda表达式里只是调用一个实例方法,并且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参的,则此时就可以使用特定类型的方法引用。

public class Test {
    public static void main(String[] args) {
        String[] names = {"dlei", "Angela", "baby", "caocao", "coach", "曹操", "deby", "eason", "andy"};

        // 排序的正常写法:
        Arrays.sort(names, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                // 忽略大小写,按首字母排序
                return o1.compareToIgnoreCase(o2);
            }
        });

        // 简化:
        // 1. 调用的是 o1 实例的方法
        // 2. o1 是函数体方法的主调
        // 3. o2 是实例方法的入参
        Arrays.sort(names, (o1, o2) -> o1.compareToIgnoreCase(o2));

        // 终极简化:
        Arrays.sort(names, String::compareToIgnoreCase);
    }
}

构造器的引用

如果 Lambda 表达式里只是在创建对象,并且前后参数情况一致,就可以使用构造器引用。

// Car 实体类
public class Car {
    private String name;
    public Car() {}
    public Car(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

// 创造 Car 的接口
@FunctionalInterface
interface CreateCar {
    Car create(String name);
}

public Test {
    public static void main(String[] args) {
        // 1. 正常写法:
        CreateCar creator = new CreateCar() {
            @Override
            public Car create(String name) {
                return new Car(name);
            }
        };
        // 2. 简写
        CreateCar creator1 = name -> new Car(name);
        // 3. 终极简写:
        CreateCar creator2 = Car::new;
        Car han = creator2.create("比亚迪 汉L");
        System.out.println(han.getName());
    }
}

十五、正则表达式

书写规则

字符类(只匹配单个字符)
[abc] 只能是a,b,或c
[^abc] 除了a,b,c之外的任何字符
[a-zA-Z] a到z A到z,包括(范围)
[a-d[m-p]] a到d,或m到p
[a-z&&[def]] d,e,或f(交集)
[a-z&&[^bc]] a到z,除了b和c(等同于[ad-z])
[a-z&&[^m-p]]: a到z,除了m到p(等同于[a-1q-z])
预定义字符(只匹配单个字符)
. 任何字符
\d 一个数字: [0-9]
\D 非数字:[^0-9]
\s 一个空白字符
\S 非空白字符: [^\s]
\w [a-zA-Z 0-9]
\W [^\w]一个非单词字符
数量词
x? X,一次或0次
x* X,零次或多次
x+ X,一次或多次
x {n} X,正好n次
x {n, } X,至少n次
x {n, m} X,至少n但不超过m次
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Test {
    public static void main(String[] args) {
        // 1. 定义爬取规则对象,定义要爬取的格式
        Pattern pattern = Pattern.compile("[A-Z]");

        // 2. 通过匹配规则对象与内容建立联系,得到一个匹配器对象
        Matcher matcher = pattern.matcher("123Zahahaz" + "ZZZ");

        // 3. 使用匹配器对象爬取内容
        while (matcher.find()) {
            String res1 = matcher.group();
            String res2 = matcher.group(1); // 只要爬取出来的邮箱中的第一组括号内容
            System.out.println(res1);
            System.out.println(res2);
        }
    }
}

分组爬取

使用小括号(),创建分组。

import java.util.regex.Pattern;
import java.util.regex.Matcher;
 
public class RegexExample {
    public static void main(String[] args) {
        // 创建Pattern对象,使用 () 创建分组
        Pattern pattern = Pattern.compile("(\\w+)@(\\w+\\.\\w+)");

        // 创建Matcher对象
        String input = "My email is [email protected]";
        Matcher matcher = pattern.matcher(input);

        // 查找匹配项并获取分组内容
        if (matcher.find()) {
            String localPart = matcher.group(1); // 获取本地部分(local part)
            String domain = matcher.group(2); // 获取域名部分(domain)
            System.out.println("Local Part: " + localPart);
            System.out.println("Domain: " + domain);
        } else {
            System.out.println("No match found.");
        }
    }
}

贪婪与惰性

紧跟在任何量词 *、 +、? 或 {} 的后面,将会使量词变为非贪婪(匹配尽量少的字符),和缺省使用的贪婪模式(匹配尽可能多的字符)正好相反。

public class RegexExample {
    public static void main(String[] args) {
        // 在 + 后添加 ? 开启惰性模式
        Pattern pattern = Pattern.compile("欢迎(.+?)光临");

        String input = "欢迎张三光临" + "欢迎李四光临" + "欢迎王五光临";
        Matcher matcher = pattern.matcher(input);

        // 查找匹配项并获取分组内容
        while (matcher.find()) {
            String user = matcher.group(1);
            System.out.println("用户:" + user);
        }
    }
}

搜索替换与内容分割

结合 String 提供的以下方法实现

方法名 说明
public String replaceAll(String regex, String newStr) 按照正则表达式匹配的内容进行替换
public String[] split(String regex) 按照正则表达式匹配的内容进行分割字符串,返回一个字符串数组
public class Test {
    public static void main(String[] args) {
        String str = "古力娜扎asd12迪丽热巴gdf241马尔扎哈88986ui卡尔扎巴";

        // 替换
        String res = str.replaceAll("\\w+", "-");
        System.out.println(res);
        System.out.println("\n-----------------\n");

        // 分割
        String[] names = str.split("\\w+");
        for (int i = 0; i < names.length; i++) {
            System.out.println(names[i]);
        }
    }
}

十六、异常与捕获

常见异常

示例 说明 异常
int[] arr = {1};
arr[2];
数组索引越界异常 ArrayIndexOutofBoundsException
String name = null;
name.length;
空指针异常 NullPointerException
10 / 0; 数学操作异常 ArithmeticException
Object o = "哈哈";
Integer i = (Integer) o;
类型转换异常 ClassCastException
String s = "23a";
int i = Integer.valueOf(s);
数字转换异常 NumberFormatException

Error:代表系统级别错误;

Exception:异常,代表程序出现问题,通常会用 Exception 以及子类来封装程序出现的问题。

  • 运行时异常:RuntimeException 及其子类,编译阶段不会出现错误提醒,运行时出现的异常(比如数组索引越界异常);
  • 编译时异常:编译阶段就会出现错误提醒。(比如日期解析异常);

捕获异常

  1. throw new Exception("xxxx")来抛出异常;
  2. try...catch() {...}来捕获异常;
public class Test {
    public static void main(String[] args) {
        try {
            int res = divide(10, 0);
            System.out.println("计算结果: " + res);
        } catch (RuntimeException e) {
            // 2. catch 捕获异常并打印异常信息
            System.out.println("捕获到异常");
            e.printStackTrace();
        }
        System.out.println("程序运行结束");

    }

    public static int divide(int a, int b) {
        if (b == 0) {
            // 1. 抛出数学计算异常
            throw new ArithmeticException("Divide by zero");
        } else {
            return a / b;
        }
    }
}

自定义异常

运行时异常

  1. 定义一个异常类继承 RuntimeException;
  2. 重写构造器;
  3. 通过 throw new 异常类(xxx)来创建异常对象并抛出。
// 自定义运行时异常
public class CustomRuntimeException extends RuntimeException {
    public CustomRuntimeException() {}
    public CustomRuntimeException(String message) {
        super(message);
    }

}

public class Test {
    public static void main(String[] args) {
        try {
            setAge(200);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void setAge(int age) {
        if (age < 1 || age > 100) {
            throw new CustomRuntimeException("非法年龄");
        }
        System.out.println("年龄合法");
    }
}

编译时异常

  1. 定义一个异常类继承 Exception;
  2. 重写构造器;
  3. 通过 throw new 异常类(xxx)来创建异常对象并抛出;
  4. 在方法签名上添加throws 异常类,抛出异常(否则代码会报错);
// 自定义运行时异常
public class CustomRuntimeException extends RuntimeException {
    public CustomRuntimeException() {}
    public CustomRuntimeException(String message) {
        super(message);
    }
}

public class Test {
    public static void main(String[] args) {
        try {
            setName(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 方法签名添加 throws CustomCompileException 抛出错误
    public static void setName(String name) throws CustomCompileException {
        if (name == null) {
            throw new CustomCompileException("非法姓名");
        }
        System.out.println("名字合法");
    }
}

建议使用运行时异常,避免频繁和非必要的报错。

异常的处理

方式一:捕获异常,记录并响应合适信息给用户;

方式二:捕获异常,尝试重新修复;

十七、集合进阶

集合的关系

画板

Collection

Collection 是单例集合的祖宗,全部单例集合都会继承它规定的方法(功能)。

方法名 说明
public boolean add(E e) 把给定的对象添加到当前集合中
public void clear() 清空集合中所有的元素
public boolean remove(E e) 把给定的对象在当前集合中删除
public boolean contains(Object obj) 判断当前集合中是否包含给定的对象
public boolean isEmpty() 判断当前集合是否为空
public int size() 返回集合中元素的个数
public Object[] toArray() 把集合中的元素,存储到数组中
boolean addAll(Collection<? extends E> c) 将指定集合中的所有元素添加到此集合中
Collection<string> list1 = new ArrayList<>();
list1.add("ab");
list1.add("cd");
list1.add("ef");

// 将字符串集合转成字符串数组
String[] arrays = list1.toArray(String[]::new)

// 把 list2 的数据添加进 list1
Collection<string> list2 = new ArrayList<>();
list1.addAll(list2);

Collection 的遍历方式

迭代器遍历

迭代器是用来遍历集合的专用方式(数组没有迭代器)

public class Test {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("a");
        list.add("b");
        list.add("c");

        // 获取集合的迭代器对象
        Iterator<String> iterator = list.iterator();

//        next() 方法返回迭代中的下一个元素
//        System.out.println(iterator.next()); // a
//        System.out.println(iterator.next()); // b
//        System.out.println(iterator.next()); // c
//        System.out.println(iterator.next()); // 溢出 error NuSuchElementException

//        使用循环迭代
        while(iterator.hasNext()) {
            String ele = iterator.next();
            System.out.println(ele);
        }
    }
}

增强 for 遍历

用来遍历集合或者数组

for (类型 变量 : 集合)

// 遍历 Collection
Collection<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
for (String s : list) {
    System.out.println(s);
}

// 遍历数组
int[] arr = {1, 2, 3, 4};
for (int i : arr) {
    System.out.println("i = " + i);
}

forEach 遍历

使用 Collection 的 forEach方法来完成

default void forEach(Consumer<? super T> action) 
Collection<String> list = new ArrayList<String>();
list.add("a");
list.add("b");

list.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});

// 简写
list.forEach(s -> System.out.println(s));

// 方法引用简写
list.forEach(System.out::println);

集合的并发修改异常

使用迭代器遍历集合时,又同时在删除集合中的数据,程序就会出现并发修改异常的错误。

注意:

  1. **Iterator 遍历、增强 for 遍历、forEach 遍历中使用集合自带的 ****remove()**方法都会出现并发修改异常错误;
  2. **如果需要在遍历时删除数据,请使用 Iterator 自带的 ****remove**方法进行删除。
public class Test {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Java入门");
        list.add("硫磺枸杞");
        list.add("黑枸杞");
        list.add("Java 进阶");

        // 1. 迭代器的并发修改异常
       Iterator<String> iterator = list.iterator();
       while (iterator.hasNext()) {
           String ele = iterator.next();
           if (ele.contains("枸杞")) {
               list.remove(ele); // error
           }
       }

        // 2. 增强 for 的并发修改异常
       for (String s : list) {
           if (s.contains("枸杞")) {
               list.remove(s); // error
           }
       }

        // 3. forEach 的并发修改异常
        list.forEach(s -> {
            if (s.contains("枸杞")) {
                list.remove(s); // error
            }
        });
    }
}
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
   String ele = iterator.next();
   if (ele.contains("枸杞")) {
       // 只能使用 Iterator 自带的 remove() 进行删除
       iterator.remove();
   }
}

如果非要用 for 循环,可以倒叙删除,或者使用 i-- 的方式进行删除。

List 集合

ArrayList 和 LinkedList 的区别

底层采用的数据结构不用,应用场景不同

ArrayList 集合

特点

  • 基于数组实现;
  • 查询速度快

特指根据索引寻址速度快,查询数据通过地址值和索引定位,查询任意数据耗时相同。

  • 删除效率低

可能需要把后面很多的数据进行迁移

  • 添加效率低

可鞥需要把后面很多数据后移,再添加元素,或者也可能需要进行数组的扩容。

底层原理

  1. 利用无参构造器创建的集合,会在底层创建一个默认长度为 0 的数组;
  2. 添加第一个元素时,底层会创建一个新的长度为 10 的数组;
  3. 存满时,会扩容 1.5 倍;
  4. 如果一次添加多个元素,1.5 倍还放不下,则新创建数组的长度以实际长度为准;
// 开发中建议使用多态创建 ArrayList 集合
List<String> list = new ArrayList<String>();

LinkedList 集合

特点

  • 基于双链表实现;
  • 查询慢,增删相对较快,尤其是首尾节点;

链表首尾操作的特有方法

方法名 说明
public void addFirst(E e) 在该列表开头插入指定的元素
public void addLast(Ee) 将指定的元素追加到此列表的末尾
public E getFirst() 返回此列表中的第一个元素
public E getLast() 返回此列表中的最后一个元素
public E removeFirst() 从此列表中删除并返回第一个元素
public E removeLast() 从此列表中删除并返回最后一个元素
LinkedList<String> queue1 = new LinkedList<String>();
// 按队列添加元素(先进)
queue1.addLast("第一个人");
queue1.addLast("第二个人");
queue1.addLast("第三个人");

// 按队列移除元素(先出)
queue1.removeFirst();
queue1.removeFirst();
System.out.println("queue = " + queue1);
LinkedList<String> queue2 = new LinkedList<String>();
// 想象一个右端封口,左端开口的栈: 
// 按栈添加元素(进栈)
queue2.push("第一颗子弹"); // 就是 addFirst()
queue2.push("第二颗子弹");
queue2.push("第三颗子弹");
System.out.println("queue2 = " + queue2); // [第三颗子弹, 第二颗子弹, 第一颗子弹]

// 按栈移除元素(出栈)
queue2.pop(); // 就是 removeFirst()
System.out.println("queue2 = " + queue2); // [第二颗子弹, 第一颗子弹]

Set 集合

Set 系列集合特点:

  1. 无序,添加数据的顺序和获取出的数据顺序不一致;
  2. 不重复;
  3. 无索引

Set 要用到的常用方法,基本上都是 Collection 提供的,自己几乎没有额外新增一些功能。

不同 Set 之间的区别

  • HashSet:无序、不重复、无索引;
  • LinkedHashSet:有序、不重复、无索引;
  • TreeSet:默认按元素大小升序排序、不重复、无索引;

Hash 值

定义

  • 是一个 int 类型的随机数值,Java 中每一个对象都有一个哈希值;
  • Java 中所有的对象,都可以调用 Object 类提供的 hashCode()方法,返回该对象自己的哈希值;

对象哈希值的特点

  • 同一个对象多次调用 hashCode()方法返回的哈希值是相同的;
  • 不同对象,它们的哈希值一般不同,但也有可能会相同(哈希碰撞);

HashSet

Set<String> set = new HashSet<>();

底层原理

  • 基于哈希表实现

HashSet 的去重机制

  1. 先看元素的哈希值是否一样;
  2. 再看元素的内容是否一样。

所以,HashSet 集合默认不能对内容一样的两个不同对象去重。

解决:必须重写对象的 hasCode()equals()方法。

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

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

    // 只要两个对象的内容一样,返回的哈希值就是一样的
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    // 比较内容
    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }
}


public class HashSetDemo {
    public static void main(String[] args) {
        Set<Student> list = new HashSet<>();
        Student s1 = new Student("张三", 18);
        Student s2 = new Student("李四", 19);
        Student s3 = new Student("李四", 19);
        list.add(s1);
        list.add(s2);
        list.add(s3);

        // 并没有去重 [{name=张三, age=18}, {name=李四, age=19}, {name=李四, age=19}]
        System.out.println(list);

        // 因为 s2 和 s3 的 hashcode 不一样,HashSet 先比较 hashCode,再比较内容
        System.out.println(s2.hashCode());
        System.out.println(s3.hashCode());

        // 解决:重写 Student 的 hashCode() 和 equals()
    }
}

LinkedHashSet

底层原理

  • 基于哈希表实现;
  • 每个元素都额外的多了一个双链表机制记录它前后元素的位置;

TreeSet

底层原理

  • 基于红黑树实现的排序
  • 对于数值类型,Integer、Double,默认按照数值本身的大小进行升序排序;
  • 对于字符串类型,默认按照首字符的编号生序排序;

自定义排序规则

  • TreeSet 集合存储自定义类型的对象时,必须指定排序规则,支持如下两种方式来指定:
  • 方式一:让自定义的类实现 Comparable接口,重写 compareTo()方法来指定规则;
  • 方式二:通过调用 TreeSet 集合有参构造器,可以设置 Comparator对象(比较器对象,用于指定比较规则)public TreeSet (Comparator<? super E> comparator)
public class Student implements Comparable<Student> {
    private String name;
    private int age;

    public Student() {}
    public Student(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;
    }
    public void setAge(int age) {
        this.age = age;
    }

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

    @Override
    public int compareTo(Student o) {
        // return this.age - o.age; // 升序
         return o.age - this.age; // 降序
    }
}

public class TreeSetDemo {
    public static void main(String[] args) {
        // 自定义排序方式一:类实现 Comparable 接口,重写 compareTo 方法
        // Set<Student> list = new TreeSet<>();

        // 自定义排序方式二:传递 Comparator 对象
//        Set<Student> list = new TreeSet<>(new Comparator<Student>() {
//            @Override
//            public int compare(Student o1, Student o2) {
//                return Double.compare(o2.getAge(), o1.getAge());
//            }
//        });
        Set<Student> list = new TreeSet<>((o1, o2) -> Double.compare(o2.getAge(), o1.getAge()));

        list.add(new Student("张三", 18));
        list.add(new Student("李四", 19));
        list.add(new Student("王五", 20));

        System.out.println("list = " + list);
    }
}

使用场景总结

Set 集合 使用场景
ArrayList ★★★ 记住元素的添加顺序,需要存储重复的元素,又要频繁的根据索引查询数据
LinkedList 希望记住元素的添加顺序,且增删首尾数据的情况较多
HashSet ★★★ 不在意元素顺序,也没有重复元素需要存储,只希望增删改查都快
LinkedHashSet 记住元素的添加顺序,也没有重复元素需要存储,且希望增删改查都快
TreeSet 对元素进行排序,也没有重复元素需要存储且希望增删改查都快?
⚠️ **GitHub.com Fallback** ⚠️