1、面向对象程序设计(OOP)


1.1、面向过程&面向对象

面向过程编程(Procedure Oriented Programming,POP)

  • 步骤清晰简单,第一步做什么,第二步做什么…(线性思维
  • 是以功能(函数)为中心来思考和组织程序,注重功能的实现
  • 面向过程适合处理一些较为简单的问题,(执行者思维),扩展性差,后期维护难度较大

面向对象编程(Object Oriented Programming,OOP)

  • 物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考。最后,才对某个分类下的细节进行面向过程的思考
  • 则注重封装,以对象为中心,强调整体性,代码整体变得更规范;
  • 面向对象适合处理复杂的问题!(设计者思维),代码扩展性强,可维护性高

对于描述复杂的事物,为了从宏观上把握、从整体上合理分析,需要使用面向对象的思路来分析整个系统。但是,具体到微观操作,仍然需要面向过程的思路去处理。

注:面向过程和面向对象两者相辅相成。 面向对象离不开面向过程!

1.2、什么是面向对象?

面向对象编程的本质就是:以类的方式组织代码,以对象的组织(封装)数据

抽象:编程思想!

面向对象三大特征

  • 封装
  • 继承
  • 多态

从认识论角度考虑是先有对象后有类。类,是抽象的,它是对象的抽象。而对象,是具体的某个事物

从代码运行角度考虑是先有类后有对象。类是对象的模板

1.3、类与对象的关系

  • 类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一个具体的事物(类是一个模板)
    • 例:动物、植物、手机、电脑
    • Person类、Phone类、 Car类等,这些类都是用来描述/定义某一类具体的事物应该具备的特点和行为
  • 对象是抽象概念的一个具体实例
    • 张三就是人类的一个具体实例,张三家里的旺财就是狗狗的一个具体实例
    • 能够体现出特点,展现出功能的是具体的实例,而不是一个抽象的概念

类:

静态的属性 属性(Field)

动态的行为 方法(Method)

1.4、属性的定义

属性(property):字段(Field) 成员变量

默认初始化:

  • byte/short/int:0
  • long:0L
  • float:0.0F
  • double:0.0
  • char:0或"\u0000"
  • boolean:false
  • 引用类型:null

语法:修饰符 属性类型 属性名 = 属性值;

public String name = "张三";
public int age = 18;

1.5、创建与初始化对象

  • 使用new关键字创建对象

  • 使用new关键字创建的时候,除了分配内存空间之外,还会给创建好的对象,进行默认的初始化,以及对类中构造器的调用

  • 类中的构造器也称为构造方法,是在进行创建对象的时候必须要调用的。并且构造器有一下两个特点:

    1. 方法名和类的名字相同
    2. 没有返回类型,也不能写void

    构造方法的作用:主要用来初始化对象的值

    注:一旦定义了有参构造,系统不再提供默认无参构造方法,无参就必须显式定义

    // 一个类即使什么也不写,系统也会默认分配一个无参构造方法
    // 显示定义构造器
    public class Person {
        public String name = "张三";
        public int age = 18;
    
        public Person(){
        }
    
        // 实例化对象的时候可以赋初始值	(全参构造)
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public void sayHello() {
            // 谁调用我,this就指向谁
            System.out.println(this.name + "can sayHello!");
        }
    
        public void sing() {
            System.out.println(this.name + "can sing!");
        }
    }
    
    public static void main(String[] args) {
        // 创建第一个对象 构造器赋值
        Person p1 = new Person("小白", 150);
    
        // 创建第二个对象  对象.属性赋值
        Person p2 = new Person();
        p2.name = "小黑";
        p2.age = 25;
    
        // 通过对象.属性 获取值
        System.out.println(p1.name + "\t" + p1.age);
        System.out.println(p2.name + "\t" + p2.age);
    
        // 通过对象.方法名 调用方法
        p1.sayHello();
        p2.sing();
    }
    

    this代表当前类对象的引用(地址),谁调用我,this就指向谁

    6.5、创建对象内存

对象的引用:对象是通过引用来操作的:栈—>堆

2、面向对象三大特征之一封装


2.1、什么是封装?

封装:将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问(隐藏内部细节,保留对外接口)

对象代表什么,就得封装对应的数据,并提供数据对应的行为

2.2、为什么需要封装?

我们程序设计要追求“高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用

记住这句话就够了:属性私有,get/set,方法公开

2.3、封装的使用

封装的步骤:

  1. 修改属性的可见性 设为private
  2. 无参、全参构造方法 给属性赋值
  3. 创建公有的getter/setter方法 用于属性的读写
  4. 在getter/setter方法中加入属性控制语句 对属性值的合法性进行判断
public class User {
    // 标准的JavaBean
    // 1.私有化属性
    private String name;    // 姓名
    private int age;    // 年龄

    // 2.构造方法
    // 无参构造
    public User() {
    }

    // 全参构造
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 3.getter/setter方法
    public String getName() {    // 获得值
        return name;
    }

    public void setName(String name) {    // 给这个数据设置值
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        // 4.属性值合法判断
        if (age < 130 && age > 0) {
            this.age = age;
        } else {
            System.out.println("输入年龄非法");
            this.age = 18;
        }
    }
}

public static void main(String[] args) {
    User u1 = new User("小昭", 25);
    System.out.println(u1.getName() + "\t" + u1.getAge());	// 小昭	25

    User u2 = new User();
    u2.setName("晓琳");
    u2.setAge(225);
    System.out.println(u2.getName() + "\t" + u2.getAge());		// 晓琳	18
}

封装的好处

  1. 提高程序的安全性,保护数据
  2. 隐藏代码的实现细节,保留对外接口(get/set)
  3. 增强系统的可维护性

3、面向对象三大特征之一继承


3.1、什么是继承?

继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模

  • 继承可以让类和类之间产生一种关系,除此之外,类和类之间的关系还有依赖、组合、聚合等
  • 继承关系的两个类,一个为子类(派生类),一个为父类(基类、超类)。子类继承父类,使用extends来表示
  • 子类的父类之间,从意义上讲应该具有"is a"的关系

3.2、为什么要使用继承?

未使用继承前

将重复的代码抽取到父类中

使用继承优化后

3.3、继承的使用

继承是代码复用的一种方式

将子类共有的属性和行为放到父类中

extends的意思是"扩展"。通过这个关键字,可以让一个类和另一个类建立起继承关系

子类是父类的扩展,子类可以在父类的基础上,增加其他的功能,使子类更强大

// 在Java中,所有的类都直接或间接继承Object
// Person 人 父类(基类、超类)
public class Person {
    // 私有化成员变量
    private String name;
    private int age;
    private String gender;

    // 无参/全参构造
    public Person() {
    }

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

    // getter/setter方法
    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;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }
}

// 子类继承了父类,就会拥有父类的全部方法、属性
// 学生 is 人    子类(派生类)
public class Student extends Person{
    private String studentNo;		// 学号

    // 空参构造
    public Student() {
    }

    // 全参构造  父类 + 子类的成员变量
    public Student(String name, int age, String gender, String studentNo) {
        super(name, age, gender);
        this.studentNo = studentNo;
    }

    // 不需要在写 name、age、gender 的getter/setter方法,因为父类里面已经提供了对应的getter/setter方法
    public String getStudentNo() {
        return studentNo;
    }

    public void setStudentNo(String studentNo) {
        this.studentNo = studentNo;
    }
}


public class Teacher extends Person {
    private String major;	// 所教专业

    public Teacher() {
    }

    public Teacher(String name, int age, String gender, String major) {
        super(name, age, gender);
        this.major = major;
    }

    public String getMajor() {
        return major;
    }

    public void setMajor(String major) {
        this.major = major;
    }
}

继承的特点:

  • Java中只支持单继承,不支持多继承,但支持多层继承
  • Java中每一个类都直接或间接继承于Object类

子类不能继承父类那些内容?

  • 子类不能继承父类的构造方法
  • 父类的成员变量无论私有还是非私有,子类都可以继承,只不过私有的成员变量子类不能访问!
  • 父类的成员方法如果能够被添加到虚方法表中则可以被子类继承,反之则不能
    • 添加到虚方法表前提条件
      • 非 private、非 static、非 final修饰的方法
  • 使用final修饰的类不能再被继承

3.4、方法重写(Override)

为什么需要重写?

父类的功能,子类不一定需要,或者不一定满足,此时则需要进行方法重写

方法重写的规则:

  • 前提条件:需要有继承关系,在不同类中,子类重写父类的方法!
  • 方法名必须相同
  • 参数列表必须相同
  • 返回值类型相同或者是其子类
  • 访问修饰符不能严于父类

重写:子类的方法名和父类方法名必须一致;方法体不同

public class Person {
    public void eat(){
        System.out.println("吃大盘鸡");
    }

    public void drink(){
        System.out.println("喝汉斯小木屋");
    }

    private void play(){
        System.out.println("打篮球");
    }
}

class Student extends Person{
    public void lunch(){
        super.eat();        // 吃大盘鸡
        super.drink();      // 喝汉斯小木屋

        this.eat();         // 吃披萨
        this.drink();       // 喝咖啡
    }

    @Override
    public void eat() {
        System.out.println("吃披萨");
    }

    @Override
    public void drink() {
        System.out.println("喝咖啡");
    }
}

public static void main(String[] args) {
    Student s1 = new Student();
    s1.lunch();
}

注:

  • 私有方法不能被重写
  • 子类不能重写父类的静态方法,如果重写则报错!
  • 也就是说只有被添加到虚方法表中的方法才能够被重写

方法重载与方法重写

3.5、super关键字

继承中构造方法的访问特点

  • 子类不能继承父类的构造方法,但是可以通过super调用
  • 子类中所有的构造方法默认先访问父类中的无参构造,再执行自己

为什么要先访问父类中的无参构造呢?

  • 子类在初始化的时候,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据
  • 子类初始化之前,一定要调用父类构造方法先完成父类数据空间的初始化

如何调用父类构造方法?

子类构造方法的第一行语句默认都是:super(),不写也存在,且必须在第一行

super & this

代表的对象不同:

  • this 表示当前方法调用者的地址值,调用本类(包括继承)的属性、方法、本类构造方法
  • super:代表父类对象的引用,调用父类的属性、方法、本类构造方法

前提:

  • this:没有继承也可以使用
  • super:只能在继承条件才可以使用

在这里插入图片描述

4、面向对象三大特征之一多态


4.1、什么是多态

生活中的多态:同一种操作,由于条件不同,产生的结果也不同

程序中的多态:同一个引用类型,使用不同的实例而执行不同操作(父类引用指向子类对象),从而产生多种形态

4.2、多态的使用

多态前提条件:

  1. 有继承关系
  2. 子类重写父类方法
  3. 父类引用指向子类对象 Father f1 = new Son(); (自动类型转换)

注:多态是方法的多态,属性没有多态

父类和子类,有联系,类型装换异常!ClassCastException

4.3、instanceof 关键字

多态的应用

instanceof(强制类型转换)引用类型,判断一个对象是什么类型

应用场景:使用父类作为方法形参实现多态,可以接收所有子类对象,使方法参数的类型更为宽泛

public class Animal {
    private String name;
    private int age;

    public void eat() {
        System.out.println("吃饭");
    }

    public void play() {
        System.out.println("玩耍");
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("猫咪正在吃鱼");
    }

    @Override
    public void play() {
        System.out.println("猫咪正在上树");
    }

    public void catchMouse() {
        System.out.println("猫咪可以抓老鼠");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("狗狗正在啃骨头");
    }

    @Override
    public void play() {
        System.out.println("狗狗在做玩皮球");
    }

    public void lookHome() {
        System.out.println("狗狗可以看门护院");
    }
}

public static void main(String[] args) {
    getAnimal(new Cat());	// 想要那个类型的动物就创建那个动物

    getAnimal(new Dog());
}

public static void getAnimal(Animal animal) {
    animal.eat();
    animal.play();
    if (animal instanceof Cat) {
        Cat c = (Cat) animal;
        c.catchMouse();
    }

    // jdk14 新特性
    // 先判断 animal是否为 Dog类型,如果是,则强转为 Dog类型,转换之后变量名为 d
    if (animal instanceof Dog d) {
        d.lookHome();
    } else {
        System.out.println("没有该类型,无法进行转换");
    }
}

多态

  • 子类转换为父类——向上转型,自动进行类型转换,父类引用仅可调用父类所声明的属性和方法,不可调用子类独有的属性和方法
  • 父类转换为子类——向下转型,通常结合instanceof运算符进行强制类型转换

多态中调用成员的特点:

  • 调用成员变量:编译看左边,运行也看左边
  • 调用成员方法:编译看左边,运行看右边

多态的优缺点

优点:

  • 在多态形式下,右边对象可以实现解耦合,便于扩展和维护
  • 定义方法的时候,使用父类作为参数,可以接收所有子类对象,体现多态的扩展性与便利

缺点:

  • 不能调用子类的特有方法,(可通过instanceof运算符强转为指定类型即可)

4.4、final关键字

  • final修饰的方法:表明该方法是最终方法,不能被重写
  • final修饰的类:表明该类是最终类,不能被继承
  • final修饰的变量:叫做常量,只能被赋值一次
常量

实际开发中,常量一般作为系统的配置信息,方便维护,提高可读性

常量的命名规范:

  • 单个单词:全部大写
  • 多个单词:全部大写,单词之间用下划线隔开

注:final修饰的变量是基本数据类型,那么变量存储的数据值不能发生改变

final修饰的变量如果是引用数据类型,那么变量存储的地址值不能发生改变,内部的属性值是可以改变的

final double PI = 3.14;
// PI = 55;			// 报错

final Student STUDENT = new Student("小昭", 22);
// STUDENT = new Student();		// 报错
STUDENT.setName("小白");
STUDENT.setAge(26);
System.out.println(STUDENT.getName() + "\t" + STUDENT.getAge());

4.5、权限修饰符

权限修饰符:用来控制一个成员能够被访问的范围

可以修饰成员变量,方法,构造方法,内部类

实际开发中,一般只用private和public

  • 成员变量私有
  • 方法公开

特例:如果方法中的代码是抽取其他方法中共性代码,这个方法一般也私有

4.6、代码块

代码块可分为以下三种

  • 局部代码块
    • 提前结束变量的声明周期(已淘汰)
  • 构造代码块
    • 编写在成员位置的代码块
    • 应用场景:可以把构造方法中的重复代码,抽取出来(缺点,不够灵活,每次创建对象都会调用,可以将重复的代码抽取为一个方法,哪里需要哪里调)
    • 执行时机:会优先于创建对象(构造方法)之前执行
  • 静态代码块
    • 编写在成员位置并用static关键字修饰的代码块
    • 应用场景:在类加载的时候,做一些数据初始化的时候使用
    • 执行时机:随着类的加载而加载,并且自动触发、只执行一次

4.7、抽象类

为什么要有抽象类?

在继承时父类有些方法需要被子类重写,但是子类不一定重写,这时就可以使用抽象类来约束子类必须实现父类的抽象方法(专治不服)

抽象方法:将共性的行为(方法)抽取到父类之后,由于每一个子类执行的内容是不一样的,所以,在父类中不能确定具体的方法体。该方法就可以定义为抽象方法

抽象类:如果一个类中存在抽象方法,那么该类就必须声明为抽象类

抽象方法语法格式:public abstract 返回值类型 方法名(参数列表)

抽象类语法格式:public abstract class 类名{}

public abstract class Animal {
    private String name;
    private int age;

    public abstract void eat();

    public void drink() {
        System.out.println(name + "在喝水");
    }
}

public class Dog extends Animal{
    public Dog() {
    }

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

    @Override
    public void eat() {
        System.out.println(getName()+"在啃骨头");
    }
}

抽象类的特点:

  • 抽象类不能被实例化(只能靠子类去实现它;约束!)
  • 抽象类中也可以编写普通方法
  • 抽象类中可以有构造方法(创建子类对象时,给属性赋值)

抽象方法的特点:

  • 抽象方法没有方法体
  • 抽象方法必须在抽象类中,简而言之就是有抽象方法的类一定是抽象类
  • 抽象方法必须在子类中被实现,除非子类也是抽象类

抽象类存在的意义:抽象是将共性抽离出来,产生抽象性概念而非具体

5、接口


5.1、什么是接口?

接口的本质是契约,就像我们社会的法律一样,制定好大家都遵守

OOP的精髓,是对对象的抽象,最能体现这一点就是接口。为什么讨论设计模式,都只针对能力的语言(比如java、c++、c#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象(架构师)

微观概念:接口表示一种能力!

接口的定义:代表了某种能力

方法的定义:能力的具体要求

接口:只有规范!自己无法写方法~专业的约束!约束和实现分离:面向接口编程

定义接口使用interface关键字

接口和类之间是实现关系,通过implements关键字表示

5.2、面向接口编程

程序设计时:

  • 关心实现类有何能力,而不关心实现细节
  • 面向接口的约定而不考虑接口的具体实现

例:

  • 防盗门是一个门 (is a的关系)

  • 防盗门是有一个锁 (has a的关系)那么上锁、开锁就是它的能力

5.3、接口的使用

  1. 定义接口

    public interface UserService {
        // 接口中的所有定义其实都是抽象的  public abstract
        // 定义一些方法,让不同的人实现
        public abstract void add(String name);
        public abstract void del(String name);
    }
    
    
    public interface TimeService {
        void time();
    }
    
  2. 实现接口

    // 类可以实现接口!使用implement关键字实现接口
    // 实现接口的类,就需要重写接口中的所有方法
    // 多继承~利用接口实现伪多继承!  可为类扩充多种能力
    public class UserServiceImpl implements UserService,TimeService{
    
        @Override
        public void add(String name) {
            System.out.println("添加了"+name);
        }
    
        @Override
        public void del(String name) {
            System.out.println("删除了"+name);
        }
    }
    
  3. 使用接口

    public class Test {
        public static void main(String[] args) {
            UserService userService = new UserServiceImpl();
            userService.add("小张");
        }
    }
    

回调原理

5.4、接口的特性

  • 接口不可以被实例化,接口中没有构造方法

  • 接口中的变量都是静态常量(public static final)

  • 接口中的方法都是抽象方法(public abstract)

    • jdk7之前,接口中只能定义抽象方法
    • jdk8新特性:接口中可以定义有方法体的方法(default、static)
    • jdk9新特性:接口中可以定义私有方法(private)
  • 实现接口中的抽象方法是,访问修饰符必须是public

  • 实现类必须实现接口的所有方法,除非此类为抽象类

使用接口的好处:

  • 程序的耦合度降低
  • 更自然的使用多态
  • 设计与实现完全分离
  • 更容易搭建程序框架
  • 更容易更换具体实现

5.5、接口和类之间的关系

  • 类和类的关系
    • 继承关系,只能单继承,不能多继承,但是可以多层继承
  • 类和接口的关系
    • 实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口
  • 接口和接口的关系
    • 继承关系,可以单继承,也可以多继承
jdk8以后新增的方法

允许在接口中定义默认方法,需要使用关键字default修饰

作用:解决接口升级的问题

语法格式:public default 返回值类型 方法名(参数列表){}

注:

  • 默认方法不是抽象方法,所以不强制被重写。但是如果被重写,重写的时候去掉default关键字即可!
  • public可省略,default则不能省略
  • 如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必领对该方法进行重写
public interface Inter {
    public abstract void check();
    public default void update(){
        System.out.println("接口中的默认方法---update");
    }
}

public class InterImpl implements Inter{
    @Override
    public void check() {
        System.out.println("实现类重写了 check 方法");
    }

    @Override
    public void update() {
        System.out.println("重写了接口中的默认方法");
    }
}

允许在接口中定义定义静态方法,需要用static修饰

语法格式:public static 返回值类型 方法名(参数列表){}

注:

  • 静态方法只能通过接口名调用,不能通过实现类名或者对象名调用
  • public可以省略,static不能省略
  • 静态方法是不能被重写的,无法添加到虚方法表中
  • 静态方法中是没有this和super关键字的
public interface Inter {
    public abstract void query();
    public static void delete(){
        System.out.println("接口中的静态方法---delete");
    }
}

public class InterImpl implements Inter {
    @Override
    public void query() {
        System.out.println("实现类重写了 query 方法");
    }
}

public static void main(String[] args) {
    // 调用实现类中的静态方法
    Inter inter = new InterImpl();
    inter.query();

    // 调用接口中的静态方法
    Inter.delete();
}
jdk9以后新增的方法

接口中可以定义私有方法

接口中私有方法的定义格式分为两种

  • 范例一:private 返回值类型 方法名(参数列表){}
  • 范例二:private static 返回值类型 方法名(参数列表){}
public interface InterA {
    public default void method1(){
        System.out.println("method1 方法开始执行了");
        recordLog1();
    }

    // 普通的私有方法,给默认方法服务的
    private void recordLog1(){
        System.out.println("记录程序运行日志,省略若干行代码");
    }

    public static void method2(){
        System.out.println("method2 方法开始执行了");
        recordLog2();
    }

    // 静态的私有方法,给静态方法服务的
    private static void recordLog2(){
        System.out.println("记录程序运行日志,省略若干行代码");
    }
}

接口代表规则,是行为的抽象。想要让哪个类拥有一个行为,就让这个类实现对应的接口就可以了

当一个方法的参数是接口时,可以传递接口所有实现类的对象,这种方式称之为接口多态

5.6、内部类

类的五大成员:属性、方法、构造方法、代码块、内部类

什么是内部类?

内部类就是在一个类的内部在定义一个类,比如,A类中定义了一个B类,那么B类相对A类来说就称为内部类,而A类相对B类来说就是外部类了

何时会用到内部类?

B类表示的事物是A类的一部分,且B单独存在没有任何意义

比如:ArrayList的迭代器、汽车的发动机

内部类的访问特点:

  • 内部类可以直接访问外部类的成员,包括私有
  • 外部类要访问内部类的成员,必须创建对象
public class Car {
    private String carName;
    int carAge;
    String color;

    public void show() {
        System.out.println(carName);

        Engine e = new Engine();
        System.out.println(e.engineName);
    }

    class Engine {
        String engineName;
        int engineAge;

        public void show() {
            System.out.println(engineName);
            System.out.println(carName);
        }
    }
}
内部类的分类
  • 成员内部类
  • 静态内部类
  • 局部内部类
  • 匿名内部类
成员内部类
  • 写在成员位置的,属于外部类的成员(类中方法外)
  • 成员内部类可以被一些修饰符所修饰,比如:private,默认,protected,public,static等
  • 在成员内部类里面,JDK16之前不能定义静态变量,JDK 16开始才可以定义静态变量

如何创建成员内部类的对象?

  • 方式一:外部类编写方法,对外提供内部类对象
  • 方式二:直接创建,格式:外部类名.内部类名 对象名 = new 外部类对象.new 内部类对象
public class Outer {
    public class Inner{
        static int a = 20;
    }

    // 方式一
    // private class Inner{
    //
    // }
    public Inner getInstance(){
        return new Inner();
    }
}

public static void main(String[] args) {
    // 方式二:创建内部类的对象
    Outer.Inner oi = new Outer().new Inner();

    System.out.println(Outer.Inner.a);

    // Outer outer = new Outer();
    // 获取私有内部类的名称
    // Object inner = outer.getInstance();

    // System.out.println(outer.getInstance());
}

外部类成员变量和内部类成员变量重名时,在内部类如何访问?

public class Outer {
    private int a = 30;

    public class Inner{
        private int a = 20;

        public void show(){
            int a = 10;
            System.out.println(a);          // 10
            System.out.println(this.a);      // 20
            System.out.println(Outer.this.a);   // 30
        }
    }
}
静态内部类
  • 静态内部类是一种特殊的成员内部类
  • 静态内部类定义在类中,方法外,用static修饰的叫做静态内部类

静态内部类只能访问外部类中的静态变量和静态方法,如果想要访问非静态的则需要创建外部类对象

public class Outer {
    int x = 25;
    static int y = 66;

    // 静态内部类
    static class Inner {
        public static void show1() {
            Outer o = new Outer();
            System.out.println(o.x);
            System.out.println("静态方法被调用了");
        }

        public void show2() {
            System.out.println("非静态方法被调用了");
        }
    }
}

public static void main(String[] args) {
    // 创建静态内部类对象的格式:外部类名.内部类名 对象名 = new外部类名.内部类名();
    Outer.Inner oi = new Outer.Inner();
    oi.show2();

    // 调用静态内部类方法:外部类名.内部类名.方法名()
    Outer.Inner.show1();
}
局部内部类
  • 将内部类定义在方法里面就叫做局部内部类,类似于方法里面的局部变量
  • 外界是无法直接使用,需要在方法内部创建对象并使用
  • 该类可以直接访问外部类的成员,也可以访问方法内的局部变量
public class Outer {
    int x = 52;

    public void show() {
        int y = 63;
        // 局部内部类
        class Inner {
            String name;

            public void method(){
                System.out.println(x);
                System.out.println(y);
                System.out.println("局部内部类的method方法");
            }
        }

        Inner i = new Inner();
        System.out.println(i.name);
        i.method();
    }
}
匿名内部类

匿名内部类本质上是隐藏了名字的内部类,可以写在成员位置,也可以写在局部位置

语法格式:new 类名或接口名(){

重写方法

};

包含了继承或实现、方法重写、创建对象

new Study() {
    @Override
    public void speakEnglish() {
        System.out.println("重写了讲英语的方法");
    }
};

5.7、枚举类

枚举本质上也是一种类,只不过是这个类的对象是有限的、固定的几个,不能让用户随意创建

枚举的应用场景:订单状态:Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配货)、 Delivered(已发货)、Checked(已确认收货)、Return(退货)、Exchange (换货)、Cancel(取消)

枚举类的实现:在 JDK5.0 之前,需要程序员自定义枚举类型

JDK5.0 之后,Java 支持 enum 关键字来快速定义枚举类型

定义枚举类(JDK5.0 之前)
  • 私有化类的构造器,保证不能在类的外部创建其对象
  • 在类的内部创建枚举类的实例。声明为:public static final ,对外暴露这些常量对象
  • 对象如果有实例变量,应该声明为 private final(建议,不是必须),并在构造器中初始化
public class ColorTest{
    public static void main(String[] args) {
        System.out.println(Color.RED);
    }
}
class Color {
    // 2、声明当前类的实例变量
    private final String colorName;       // 颜色名称
    private final String ColorDesc;       // 颜色描述

    // 1、私有化构造器
    private Color(String colorName, String colorDesc) {
        this.colorName = colorName;
        this.ColorDesc = colorDesc;
    }

    // 3、提供实例变量的 get 方法
    public String getColorName() {
        return colorName;
    }

    public String getColorDesc() {
        return ColorDesc;
    }

    // 4、创建当前类的实例
    public static final Color RED = new Color("红色","喜庆、热烈、幸福、斗志、革命");
    public static final Color GREEN = new Color("绿色","安全、放松、宁静、自然、环保");
    public static final Color BLUE = new Color("蓝色","美丽、文静、理智、洁净");
    public static final Color PINK = new Color("粉色","甜美、温柔、纯真、浪漫");

    @Override
    public String toString() {
        return "Color{" +
            "colorName='" + colorName + '\'' +
            ", ColorDesc='" + ColorDesc + '\'' +
            '}';
    }
}
定义枚举类(JDK5.0 之后)

语法格式:修饰符 enum 枚举类名{

常量对象列表

// …

}

public class PayTest {
    public static void main(String[] args) {
        System.out.println(Pay.ALI_PAY);
        System.out.println(Pay.CREDIT_CARD_PAY);

        System.out.println(Pay.WECHAT_PAY.getClass());
        System.out.println(Pay.WECHAT_PAY.getClass().getSuperclass());
    }
}

enum Pay {
    // 必须在枚举类的开头声明多个对象。对象之间使用,隔开
    WECHAT_PAY("微信支付"),
    ALI_PAY("支付宝支付"),
    BANK_CARD_PAY("银行卡支付"),
    CREDIT_CARD_PAY("信用卡支付");

    // 2、声明当前类的实例变量 使用 private final修饰
    private final String payMethod;

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

    // 3、私有化构造器
    Pay(String payMethod) {
        this.payMethod = payMethod;
    }

    // 4、提供实例变量 get 方法
    public String getPayMethod() {
        return payMethod;
    }
}

enum 方式定义的要求和特点

  • 枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写
  • 定义的实例变量系统会自动添加 public static final 修饰
  • 枚举类默认继承的是 java.lang.Enum 类,因此不能再继承其他的类型

应用场景:开发中,当需要定义一组常量时,建议使用枚举类!

5.8、包装类

何为包装类?

包装类是指将基本数据类型,包装成类 (变成引用数据类型)

在这里插入图片描述

如何将基本数据类型,手动包装为类?拿Integer举例

方式一:创建包装类对象的时候传入参数(不太推荐)

方式二:调用指定包装类中静态方法,包装类.valueOf()

public static void main(String[] args) {
    int num = 20;
    // 将基本数据类型转换为包装类
    Integer it1 = new Integer(20);
    Integer it2 = Integer.valueOf(num);
    System.out.println("it1 = " + it1);
    System.out.println("it2 = " + it2);

    // 将包装类转换为基本数据类型
    int value = it1.intValue();
    System.out.println("value = " + value);
}

从JDK5开始,出现了自动装箱和拆箱,底层还是调用了valueof方法

  • 自动装箱:基本类型的数据和变量可以直接赋值给包装类型的变量
  • 自动拆箱:包装类型的变量可以直接赋值给基本数据类型的变量

有了自动拆装箱,基本数据类型和对应的包装类,可以直接运算,操作起来更加便捷!

public static void main(String[] args) {
    // 需求:将如下字符串转换为数字存放到数组中,并求出该数组中的最大值
    String str = "66,94,34,48,211,985,863";
    String[] numbers = str.split(",");

    int[] arr = new int[numbers.length];
    for (int i = 0; i < numbers.length; i++) {
        // 将字符串类型的整数转成int类型
        int number = Integer.parseInt(numbers[i]);
        arr[i] = number;
    }

    int max = arr[0];
    for (int i = 1; i < arr.length; i++) {
        if (max < arr[i]) {
            max = arr[i];
        }
    }
    System.out.println("max = " + max);
}

注:只能与自己对应的类型之间才能实现自动装箱与拆箱!

如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出java.lang.NumberFormatException异常

包装类API

转换的方法

  • valueof():将基本数据类型转为包装类
  • xxxValue():将包装类转为基本数据类型
  • parsexxx():将指定类型的数值转换成指定类型的数值

数据类型的最大最小值

  • Integer.MAX_VALUE 和 Integer.MIN_VALUE
  • Long.MAX_VALUE 和 Long.MIN_VALUE
  • Double.MAX_VALUE 和 Double.MIN_VALUE

整数转进制

  • Integer.toBinaryString(int i) 得到二进制
  • Integer.toHexString(int i) 得到十六进制
  • Integer.toOctalString(int i) 得到八进制

比较的方法

  • Integer.compare(int x, int y)
  • Double.compare(double d1, double d2)

字符转大小写

  • Character.toUpperCase(‘z’)
  • Character.toLowerCase(‘Z’)

包装类对象的特点

会缓存对象

判断自动装箱范围是否在 -128~127 之间

在:不会创建新的对象,而是从底层数组中直接获取

不在:重新 new 出新的 Integer对象

6、常用类


字符串相关的类

  • String、StringBuffer、StringBuilder

数学相关的类

  • Math类
  • BigInteger与BigDecimal

系统相关类

  • System、Runtime

Java比较器

  • Comparable接口、Comparator接口

JDK8(-) 时间API

  • Date类、SimpleDateFormat类、Calendar类

JDK8(+)新时间API

  • 时间类

    • ZoneId:时区
    • Instant:时间戳/时间线
    • ZonedDateTime:带时区的时间
  • 日期格式化类

    • DateTimeFormatter:用于时间的格式化和解析
  • 日历类

    • LocalDate:年、月、日

    • LocalTime:时、分、秒

    • LocalDateTime:年、月、日、时、分、秒

  • 工具类

    • Period:时间间隔(年,月,日)
    • Duration:时间间隔(时、分、秒,纳秒)
    • ChronoUnit:时间间隔 (所有单位)

6.1、String

Java 程序中所有用双引号包裹的字符串,都是 String 类的对象

  • 字符串在创建之后,其内容(值)创建之后不可更改
  • 字符串虽然不可改变,但是可以被共享(字符串常量池)
String s1 = "Hello World";        // 字面量赋值
String s2 = "Hello World";
String s3 = new String("Hello World");
System.out.println(s1 == s2);       // true
System.out.println(s2 == s3);       // false

字符串常量池的存储位置

  • 字符串常量都存储在字符串常量池(StringTable)中
  • 字符串常量池不允许存放两个相同的字符串常量
  • 字符串常量池,在不同的jdk版本中,存放位置有所不同
    • jdk7之前,字符串常量池存放在方法区
    • jdk7及之后,字符串常量池存放在堆空间

面试题:String str = new String(“hello”);请问在内存中创建了几个对象?

两个,一个是堆空间中new的对象。另一个是在字符串常量池中生成的字面量 hello

String 的连接操作

  • 常量 + 常量:结果仍然存储在字符串常量池中。注:此时的常量可能是字面量,也可能是final修饰的常量
  • 变量 + 变量:都会通过new的方式创建一个新的字符串, 返回堆空间中此字符串对象的地址值
  • 调用字符串的intern():返回的是字符串常量池中字面量的地址
  • concat():不管是常量调用还是变量调用此方法,都返回一个新new创建的对象

"=="和equals()的区别:

  • ==:判断两个字符串在内存中的地址值,即判断是否是同一个字符串对象
  • equals():检查组成字符串的内容是否完全一致

字符串常用方法

方 法说 明
public int length()获取字符串的长度
public boolean equals(Object str)比较内容
public boolean equalsIgnoreCase(String anotherString)比较内容,忽略大小写
public String substring(int beginIndex)从指定位置截,截取到末尾
public String substring(int beginIndex, int endIndex)根据开始和结束索引做截取, 包含头不包含尾
public char chatAt (int index)根据索引找字符
public char[] toCharArray ()将字符串转换为字符数组
public String replace(char oldChar, char newChar)替换指定字符
public String[] split(String regex)根据传入的字符串作为规则进行切割
public boolean contains(CharSequence s)判断给定参数是否在字符串中存在
public boolean startsWith(String prefix)测试此字符串是否以指定的前缀开始
StringBuilder

StringBuilder是字符串的缓冲区,可以将其理解为是一种容器

String是不变类,用String修改字符串会新建一个String对象,如果频繁的修改,将会产生很多的String对象,开销很大,因此Java提供了一个StringBuffer类,这个类在修改字符串方面的效率比String高了很多

public static void main(String[] args) {
    // 创建对象
    StringBuilder sb = new StringBuilder("abc");
    // 添加元素
    sb.append("xyz");
    System.out.println("sb = " + sb);       // abcxyz

    // 插入
    sb.insert(3, "edf");
    System.out.println("sb = " + sb);   // abcdefxyz

    // 获取长度
    System.out.println(sb.length());        // 9

    // 删除
    sb.delete(3, 6);
    System.out.println(sb);         // abcxyz

    // 修改
    sb.replace(0, 2,"ww" );
    System.out.println(sb);     // wwcxyz

    // 反转
    sb.reverse(); 
    System.out.println("sb = " + sb);       // zyxcww
}

常用方法

方 法** 说明 **
public StringBuilder append(String str)添加数据,并返回对象本身
public StringBuilder delete(int start,int end)移除此序列的从start开始到end-1为止的一段字符序列,仍然返回自身对象
public StringBuilder insert(int offset,String str)在指定位置插入字符序列
public StringBuilder replace(int start, int end, String str)将字符串中某段字符替换成另一个字符串
public StringBuilder reverse()将字符序列逆序
public StringBuilder toString()返回此序列中数据的字符串表示形式
public char charAt()获取字符串中某个位置的字符
public int indexOf(int ch)子字符串在字符串中最先出现的位置,如果不存在,返回-1

String类、StringBuffer类和StringBuilder类的区别

  • String:不可变字符序列
  • StringBuffer:可变字符序列,线程安全,效率低
  • StrungBuilder:可变字符序列,线程不安全,效率高

三者共同之处:都是final类,不允许被继承,主要是从性能和安全性上考虑,底层都是使用byte数组存储(jdk9+)

StringBuffer与StringBuilder两者共同之处:可以通过append、insert进行字符串的操作

关于运行速度

StringBuider–>StringBuffer–>String

应用场景:

String:适用于少量的字符串操作的情况

StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况

StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

6.2、System

System是一个工具类,提供了一些与系统相关的方法

System的功能是静态的,都是直接用类名调用即可

在这里插入图片描述

计算机中的时间原点:1970年1月1日00:00:00,我国在东八区,有8个小时时差

public static void main(String[] args) {
    // 方法形参:状态码 0:当前虚拟机正常停止 非0:当前虚拟机异常停止
    // System.exit(0);
    // System.out.println("^-^");

    long ctms = System.currentTimeMillis();
    System.out.println("ctms = " + ctms);   // 从时间原点到现在的毫秒值

    int[] arr1 = {98, 2, 99, 1, 97, 3, 95, 5};
    int[] arr2 = new int[arr1.length];
    // 拷贝数组  将arr1数组中的数据拷贝到arr2中
    // 参数一:数据源,要拷贝的数组从那个数组中来
    // 参数二:从数据源数组中的第几个索引开始拷贝
    // 参数三:数据的目的地,要把数据拷贝到那个数组中
    // 参数四:目的地数组的索引
    // 参数五:拷贝的个数
    System.arraycopy(arr1, 0, arr2, 0, arr1.length);

    System.out.println(Arrays.toString(arr2));
}

注:如果数据源数组和目的地数组都是基本数据类型,那么两者的类型必须保持一致, 否则会报错

如果数据源数组和目的地数组都是引用数据类型,那么子类类型可以赋值给父类类型

6.3、RunTime

Runtime表示当前虚拟机的运行环境

在这里插入图片描述

public static void main(String[] args) throws IOException {
    // 获取Runtime对象
    Runtime r1 = Runtime.getRuntime();

    // exit 停止虚拟机
    // Runtime.getRuntime().exit(0);
    // System.out.println("^_^");

    // 获取CPU的线程数
    System.out.println(r1.availableProcessors());       // 8

    // 可以获取的总内存大小,单位 byte字节
    System.out.println(r1.maxMemory() / 1024 / 1024);     // 4062

    // 已经获取的总内存大小,单位 byte字节
    System.out.println(r1.totalMemory() / 1024 / 1024);           // 254

    // 剩余内存大小
    System.out.println(r1.freeMemory() / 1024 / 1024);      // 252

    // 运行cmd命令
    r1.exec("calc");
}

6.4、Object

Object是Java中的顶级父类,所有的类,都直接或间接的继承了 Object 类 (祖宗类)

Object类中的方法可以被所有子类所访问

在这里插入图片描述

  • 开发中直接输出对象,默认输出对象的地址值其实是毫无意义的,我们更多的时候是希望看到对象的内容数据而不是对象的地址信息

toString存在的意义

  • 父类 toString() 方法存在的意义就是为了被子类重写,以便返回对象的内容信息,而不是地址信息!

equals存在的意义

  • 父类equals方法存在的意义就是为了被子类重写,以便子类自己来定制比较规则

把A对象的属性值完全拷贝给B对象,也叫对象拷贝(对象复制),分为两种

  • 浅克隆(浅拷贝),默认浅克隆
    • 在需要克隆的类中重写clone方法
  • 深克隆(深拷贝),一般使用第三方工具类
Objects

Objects是一个对象工具类,提供了一些操作对象的方法,Objects类与 Object 还是继承关系

在这里插入图片描述

public static void main(String[] args) {
    Student s1 = null;
    Student s2 = new Student("小昭",22);
    // System.out.println(s1.equals(s2));

    // Objects  先判断是否为空,在进行比较
    System.out.println(Objects.equals(s1, s2));     // false

    // 判断是否为null
    System.out.println(Objects.isNull(s2));     // false
    // 判断是否不能null
    System.out.println(Objects.nonNull(s2));        // true
}

6.5、Math

Math类是一个帮助我们用于进行数学计算的工具类

私有化构造方法,所有的方法都是静态的

在这里插入图片描述

public static void main(String[] args) {
    // abs 绝对值
    System.out.println(Math.abs(-6));   // 6
    System.out.println(Math.abs(6));   // 6

    // System.out.println(Math.absExact(Integer.MAX_VALUE + 1));        // 报错,没有与之对应的绝对值 ArithmeticException

    // 进一法:往数轴的正方向进一
    System.out.println(Math.ceil(6.43));        // 7.0
    System.out.println(Math.ceil(-6.43));        // -6.0
    // 去尾法
    System.out.println(Math.floor(6.43));       // 6.0
    System.out.println(Math.floor(-6.43));       // -7.0
    // round 四舍五入
    System.out.println(Math.round(3.49));       // 3
    System.out.println(Math.round(3.5));        // 4
    // max 获取两个值的较大值
    double max = Math.max(3.6, 8.6);
    System.out.println("max = " + max);     // 8.6
    // mix 获取两个值的较小值
    double min = Math.min(3.6, 8.6);
    System.out.println("min = " + min);     // 3.6
    // pow 获取 a 的 b 次幂
    System.out.println(Math.pow(3, 3));     // 27.0
    // random 返回0~1之间的随机数,包含0不包含1
    // 生成4位随机数  最大 - 最小 + 1 + 最小
    for (int i = 0; i < 6; i++) {
        int ran = (int) ((Math.random() * 9999 - 1000 + 1) + 1000);
        System.out.println(ran);
    }
}

6.6、BigInteger

在Java中,整数有四种类型:byte、short、int、long

在底层占用字节个数:byte 1个字节、short 2个字节、int 4个字节、long 8个字节

用于处理任意长度的整数,它可以避免在进行数字计算时由于数据类型限制而导致的问题

public static void main(String[] args) {
    // 获取一个随机的大整数
    for (int i = 0; i < 10; i++) {
        BigInteger bd1 = new BigInteger(3, new Random());
        System.out.println(bd1);        // 0~ 2^3 -1 --> 0~7
    }

    // 获取指定的大整数
    // 字符串中的数字必须是整数,否则报错
    BigInteger bd2 = new BigInteger("9494611331651551313651565");
    System.out.println(bd2);

    // 获取指定进制的大整数
    // 字符串中的数字必须是整数
    // 字符串中的数字必须要和进制匹配,否则都会报错
    BigInteger bd3 = new BigInteger("1000", 2);
    System.out.println(bd3);        // 8

    // 静态方法获取BigInteger的对象,内部有优化
    // 对常用数字 -16~16 多次获取不会创建新的
    BigInteger bd4 = BigInteger.valueOf(16);
    System.out.println(bd4);        // 16

    BigInteger b5 = new BigInteger("24");

    BigInteger res = bd3.add(bd4);
    System.out.println(res);        // 24
    System.out.println(bd4.subtract(bd3));      // 8
    System.out.println(b5 == res);     // false
}
  • 如果BigInteger表示的值没有超出long的范围,可以通过静态方法获取
  • 如果BigInteger表示的值超出long的范围,可以用构造方法获取
  • 对象一旦创建,BigInteger内部记录的值不能发生改变
  • 只要进行计算都会产生一个新的BigInteger对象

6.7、BigDecimal

用于表示较大的小数和解决小数运算中,出现的不精确问题!

在这里插入图片描述

public static void main(String[] args) {
    // System.out.println(0.1 + 0.2);      // 0.30000000000000004

    // 通过传递double类型的小数来创建对象
    // 该种方式有可能是不精确的,所以不推荐使用
    // BigDecimal bd1 = new BigDecimal(0.1);        // 0.1000000000000000055511151231257827021181583404541015625

    // 方式一:通过构造方法获取BigDecimal对象
    BigDecimal bd1 = new BigDecimal("0.1");
    BigDecimal bd2 = new BigDecimal("0.2");
    System.out.println(bd1);
    System.out.println(bd2);
    BigDecimal bd3 = bd1.add(bd2);
    System.out.println(bd3);       // 0.3

    // 方式二:通过静态方法获取BigDecimal对象
    // 对常用数字 0~10 多次获取不会创建新的
    BigDecimal bd5 = BigDecimal.valueOf(10.5);
    System.out.println(bd5);        // 10.5

    // 运算
    BigDecimal b1 = BigDecimal.valueOf(10.0);
    BigDecimal b2 = BigDecimal.valueOf(6.0);
    // 加、减、乘、除
    BigDecimal b3 = b1.add(b2);
    System.out.println(b3);     // 16.0
    System.out.println(b1.subtract(b2));        // 4.0
    System.out.println(b1.multiply(b2));        // 60.0

    // 参数一:除数
    // 参数二:保留几位小数
    // 参数3:舍入模式
    BigDecimal res = b1.divide(b2,2, RoundingMode.HALF_UP);
    System.out.println(res);        // 1.67
}

6.8、JDK8(-) 时间类

Date时间

世界标准时间

  • 格林尼治时间/格林威治时间(Greenwich Mean Time)简称GMT
  • 目前世界标准时间(UTC)已经替换为:原子钟

中国标准时间

  • 世界标准时间+ 8小时

时间单位换算

  • 1秒 = 1000 毫秒
  • 1毫秒 = 1000 微秒
  • 1微妙 = 1000 纳秒

Date类是JDK已经写好的Javabean类,用来描述时间,精确到毫秒

  • 利用空参构造创建的对象,默认表示系统当前时间
  • 利用有参构造创建的对象,表示指定的时间
public static void main(String[] args) {
    // 创建Date对象,默认为当前系统时间
    Date date1 = new Date();
    System.out.println("date1 = " + date1);

    // 创建对象表示指定时间
    Date date2 = new Date(0L);
    System.out.println("date2 = " + date2);

    // setTime 修改时间
    date2.setTime(5000L);
    System.out.println("date2 = " + date2);

    // getTime 获取当前时间毫秒值
    long time = date2.getTime();
    System.out.println("time = " + time);
}
SimpleDateFormat格式化时间

format 格式化:可以把时间变成想要的格式

parse 解析:把字符串表示的时间变成Date对象

public static void main(String[] args) throws ParseException {
    Date date = new Date();
    // 使用空参构造创建 SimpleDateFormat,默认格式
    SimpleDateFormat sdf1 = new SimpleDateFormat();
    // 日期对象 --->  字符串
    String str1 = sdf1.format(date);
    System.out.println(str1);        // 2022/5/10 下午7:58

    // 使用带参构造创建 SimpleDateFormat,指定格式
    SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
    String str2 = sdf2.format(date);
    System.out.println(str2);       // 2022年05月10日 20:00:13

    // 定义字符串表示时间
    String str = "2035-01-01 00:00:00";
    // 创建对象的格式要和字符串的格式保持一致! 否则会报错:ParseException
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    // 字符串 --->  日期对象
    Date newDate = sdf.parse(str);
    System.out.println(newDate);
    System.out.println(newDate.getTime());
}
Calendar日历

表示一个时间的日历对象,通过它可以单独获取、修改时间中的年、月、日、时、分、秒等

创建对象:Calendar是一个抽象类,不能直接创建对象

可以通过getInstance()静态方法,来获取当前时间的日历对象

public static void main(String[] args) {
    // 获取日历对象
    // Calendar是一个抽象类,不能直接 new,可以通过一个静态方法来获取子类对象
    // 底层原理:会根据系统的不同时区来获取不同的日历对象,默认表示当前时间
    // 会把时间中的纪元、年、月、日、时、分、秒、星期等都放到一个数组中
    Calendar calendar = Calendar.getInstance();
    // 修改日历代表的时间
    Date d = new Date(0L);
    calendar.setTime(d);

    // 修改对应值
    calendar.set(Calendar.YEAR, 2023);
    calendar.set(Calendar.MONTH, 11);

    // 在原有基础上增加或者减少
    calendar.add(Calendar.MONTH,6);

    System.out.println(calendar);
    // Java 在 Calendar 类中,把索引对应的数字都定义成了常量
    int year = calendar.get(Calendar.YEAR);
    int month = calendar.get(Calendar.MONTH) + 1;
    int date = calendar.get(Calendar.DAY_OF_MONTH);
    int week = calendar.get(Calendar.DAY_OF_WEEK);
    System.out.println(year + "," + month + "," + date + "," + week);
}

6.9、JDK8(+) 时间类

JDK8之前时间API VS JDK8之后时间API

JDK8 之前JDK8 之后
设计欠妥,使用不方便,很多都被淘汰了设计更合理,功能丰富,使用更方便
都是可变对象,修改后会丢失最开始的时间信息都是不可变对象,修改后会返回新的时间对象,不会丢失最开始的时间
线程不安全线程安全
只能精确到毫秒能精确到毫秒、纳秒
Date时间
ZoneId 时区
public static void main(String[] args) {
    // 1、获取所有时区名称
    Set<String> zoneIds = ZoneId.getAvailableZoneIds();
    System.out.println(zoneIds.size());     // 602个时区
    System.out.println(zoneIds);

    // 2、获取当前系统的默认时区
    ZoneId zoneId = ZoneId.systemDefault();
    System.out.println(zoneId);     // Asia/Shanghai

    // 3、获取指定的时区
    ZoneId zone = ZoneId.of("America/Cuiaba");
    System.out.println(zone);
}
Instant 时间戳/时间线
public static void main(String[] args) {
    // 1、获取当前时间的Instant对象(标准时间)
    Instant now = Instant.now();
    System.out.println(now);

    // 2、根据(秒/毫秒/纳秒)获取Instant对象
    Instant instant1 = Instant.ofEpochMilli(0L);
    System.out.println(instant1);       // 1970-01-01T00:00:00Z

    System.out.println(Instant.ofEpochSecond(6L));      // 1970-01-01T00:00:06Z

    // 3、指定时区
    ZonedDateTime dateTime = Instant.now().atZone(ZoneId.of("Asia/Shanghai"));
    System.out.println(dateTime);

    // 4、isXXX 判断时间系列的方法
    // isBefore:判断调用者的时间是否在参数表示时间的前面
    Instant instant2 = Instant.ofEpochMilli(0L);
    Instant instant3 = Instant.ofEpochMilli(6000L);
    // 判断 instant2 的时间是否在 instant3 前面
    boolean result1 = instant2.isBefore(instant3);
    System.out.println(result1);        // true
    System.out.println(instant2.isAfter(instant3));     // false

    // 5、减少时间系列的方法
    ZonedDateTime instant5 = Instant.now().atZone(ZoneId.of("Asia/Shanghai"));
    // 减少 2个月 1天 6小时 30 分钟  返回新的对象
    ZonedDateTime subDateTime = instant5.minusMonths(2).minusDays(1).minusHours(6L).minusMinutes(30);
    System.out.println(subDateTime);

    // 6、增加时间系列的方法
    // 增加 10 天
    ZonedDateTime addDateTime = subDateTime.plusDays(10);
    System.out.println(addDateTime);

    // 7、修改时间系列的方法
    // 改为 1 月份
    ZonedDateTime modifyDate = addDateTime.withMonth(1);
    System.out.println(modifyDate);
}
ZonedDateTime 带时区的时间
public static void main(String[] args) {
    // 1、获取当前时间对象(带时区)
    ZonedDateTime now = ZonedDateTime.now();
    System.out.println(now);

    // 2、获取指定时间对象(带时区)
    ZonedDateTime time1 = ZonedDateTime.of(LocalDateTime.of(2035, 1, 1,
                    0, 0, 0, 0),
            ZoneId.of("Asia/Shanghai"));
    System.out.println(time1);

    // 3、通过Instant + 时区的方式获取时间对象
    Instant instant = Instant.ofEpochSecond(0L);
    ZoneId zoneId = ZoneId.of("Asia/Shanghai");
    ZonedDateTime time2 = ZonedDateTime.ofInstant(instant, zoneId);
    System.out.println(time2);

    // 4、withXXX 修改时间系列的方法
    // 修改年月日为 2050年10月1日
    ZonedDateTime time3 = time2.withYear(2050).withMonth(10).withDayOfMonth(1);
    System.out.println(time3);
    
    // 5、minusXXX 减少时间系列方法
    ZonedDateTime time4 = time3.minusMonths(3);
    System.out.println(time4);

    // 6、plusXXX 增加时间系列方法
    ZonedDateTime time5 = time3.plusYears(10);
    System.out.println(time5);
}

注:JDK8新增的时间对象都是不可变的,如果修改、增加、减少,原对象是不会发生改变的,而是产生一个新的对象!

DateTimeFormatter 时间的格式化和解析
public static void main(String[] args) {
    // 获取时间对象
    // ZonedDateTime zonedDateTime = Instant.now().atZone(ZoneId.of("Asia/Shanghai"));
    ZonedDateTime zonedDateTime = ZonedDateTime.now();

    // 解析/格式化器
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EE a");
    String dateTime = dtf.format(zonedDateTime);
    System.out.println(dateTime);
}
Calendar 日历
LocalDateTime 年、月、日 时、分、秒
public static void main(String[] args) {
    // 1、获取当前时间的日历对象(年、月、日、时、分、秒)
    LocalDateTime nowDate = LocalDateTime.now();
    System.out.println(nowDate);

    // 2、获取指定时间的日历对象
    LocalDateTime localDateTime = LocalDateTime.of(2025, 1, 30, 12,
            12, 12, 1000);
    System.out.println(localDateTime);

    // 3、获取日历中的每一个字段值
    int year = localDateTime.getYear();
    Month month = localDateTime.getMonth();
    System.out.println("year:" + year);     // 2025
    System.out.println("month:" + month);       // JANUARY
    System.out.println("month:" + month.getValue());      // 1
    System.out.println("month:" + localDateTime.getMonthValue());   // 1

    LocalDateTime localDateTime1 = LocalDateTime.of(2050, 1, 30, 12,
            12, 12, 1000);

    // 4、isXXX 判断时间系列的方法
    System.out.println(localDateTime.isBefore(localDateTime1));     // true
    System.out.println(localDateTime.isAfter(localDateTime1));      // false

    // 5、withXXX 修改时间系列的方法
    LocalDateTime ldt1 = localDateTime.withYear(2060);
    System.out.println(ldt1);

    // 6、minusXXX 减少时间系列的方法
    LocalDateTime ldt2 = localDateTime.minusHours(10);
    System.out.println(ldt2);

    // 7、plusXXX 增加时间系列的方法
    LocalDateTime ldt3 = localDateTime.plusDays(10);
    System.out.println(ldt3);
}
工具类
Period时间间隔(年,月,日)
public static void main(String[] args) {
    // 当前时间(年、月、日)
    LocalDate now = LocalDate.now();
    // 出生年月
    LocalDate birthday = LocalDate.of(2001, 5, 1);

    // 第二个参数减去第一个参数
    Period period = Period.between(birthday, now);
    System.out.println("相差日期间隔对象:" + period);
    System.out.println(period.getYears());		// 21
    System.out.println(period.getMonths());		// 0
    System.out.println(period.getDays());		// 5

    System.out.println(period.toTotalMonths());     // 中间间隔了多少月
}
Duration时间间隔(时、分、秒,纳秒)
public static void main(String[] args) {
    // 秒杀活动开始
    LocalTime secKillStart = LocalTime.of(8, 0, 0, 0);
    // 秒杀活动结束
    LocalTime secKillEnd = LocalTime.of(10, 30, 0, 2000);

    Duration duration = Duration.between(secKillStart, secKillEnd);
    System.out.println("相差时间间隔对象:" + duration);
    System.out.println(duration.toHours());     // 2
    System.out.println(duration.toMinutes());   // 150
    System.out.println(duration.toSeconds());   // 9000
    System.out.println(duration.toMinutes());   // 150
}
ChronoUnit 时间间隔 (所有单位)
public static void main(String[] args) {
    // 当前时间
    LocalDateTime now = LocalDateTime.now();
    // 出生日期
    LocalDateTime birthday = LocalDateTime.of(2000, 10, 16, 12, 20, 20, 1000);

    System.out.println("相差年数:" + ChronoUnit.YEARS.between(birthday, now));
    System.out.println("相差月数:" + ChronoUnit.MONTHS.between(birthday, now));
    System.out.println("相差天数:" + ChronoUnit.DAYS.between(birthday, now));
    System.out.println("相差时数:" + ChronoUnit.HOURS.between(birthday, now));
    System.out.println("相差分数:" + ChronoUnit.MINUTES.between(birthday, now));
    System.out.println("相差秒数:" + ChronoUnit.SECONDS.between(birthday, now));
    System.out.println("相差周数:" + ChronoUnit.WEEKS.between(birthday, now));
}

6.10、比较器

在 Java 中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题

Java 实现对象排序的方式有两种:

  • 自然排序:Comparable接口
  • 定制排序:Comparator接口
自然排序

实现 Comparable 接口的对象列表(和数组)可以通过 Collections.sort 或 Arrays.sort 进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器

实现步骤:具体比较的类要实现Comparable接口,要求重写Comparable接口中的compareTo(Object obj)方法,在该方法中定义比较规则

public class Product implements Comparable {
    /**
     * 当前类需要实现 Comparable 的抽象方法
     * 在此方法中,指明如何判断当前类的大小,比如:按照价格排序
     * 如果返回值为正数,当前对象大
     * 如果返回值为负数,当前对象小
     * 如果返回值为0,一样大
    */
    @Override
    public int compareTo(Object o) {
        if (o == this) {
            return 0;
        }
        if (o instanceof Product p) {
            return Double.compare(this.price, p.price);
        }
        // 手动抛出异常
        throw new RuntimeException("类型不一致");
    }
    // 省略属性、get/set、构造、toString方法
}
public static void main(String[] args) {
    Product[] products = new Product[5];
    products[0] = new Product("红米K40", 2299);
    products[1] = new Product("VivoIQOO", 1798);
    products[2] = new Product("HuaweiMate50Pro", 6999);
    products[3] = new Product("Iphone14ProMax", 8999);
    products[4] = new Product("Oppo Find X6 Pro", 6299);

    Arrays.sort(products);

    // 比较之后进行遍历
    for (int i = 0; i < products.length; i++) {
        System.out.println(products[i]);
    }
}
定制排序

为什么要定制排序?因为有些类我们是无法更改的,且无法去实现Comparable接口,比如:String、Array…,所以需要通过创建一个实现了Comparator接口的实现类去进行定制排序

实现步骤:创建一个实现了Comparator接口的实现类,要求重写Comparator接口中的抽象方法compare(Object o1,Object o2)方法,在该方法中定义比较规则

public static void main(String[] args) {
    Goods[] goods = new Goods[5];
    goods[0] = new Goods("HongK40", 2299);
    goods[1] = new Goods("VivoIQOO", 1798);
    goods[2] = new Goods("HuaweiMate50Pro", 6999);
    goods[3] = new Goods("Iphone14ProMax", 8999);
    goods[4] = new Goods("Oppo Find X6 Pro", 6299);

    // 创建一个实现了 Comparator 接口的实现类的对象
    Comparator comparator = new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            // 定义比较规则
            if (o1 instanceof Goods g1 && o2 instanceof Goods g2) {
                // 按照价格从高到低排序
                return -Double.compare(g1.getPrice(), g2.getPrice());
            }

            throw new RuntimeException("类型不匹配");
        }
    };

    Arrays.sort(goods, comparator);

    // 排序打印输出
    for (Goods good : goods) {
        System.out.println(good.getName() + "," + good.getPrice());
    }
}
Lambda表达式

Lambda表达式是JDK 8开始后的一种新语法形式

public static void main(String[] args) {
    Integer[] arr = {44, 56, 949, 8423, 14, 848};

    // lambda 完整格式
    Arrays.sort(arr, (Integer o1, Integer o2) -> {
        return o1 - o2;
    });

    // lambda 省略写法
    Arrays.sort(arr, (o1, o2) -> o1 - 02);

    System.out.println(Arrays.toString(arr));
}

注:

  • Lambda表达式可以用来简化匿名内部类的书写
  • Lambda表达式只能简化函数式接口的匿名内部类的写法
  • 函数式接口:
    • 有且仅有一个抽象方法的接口叫做函数式接口,接口上方可以加@FunctionalInterface注解

省略规则:

  • 核心:可推导,可省略
  • 参数类型相同可以省略不写
  • 如果只有一个参数,参数类型可以省略,同时()也可以省略
  • 如果Lambda表达式的方法体只有一行,大括号,分号,return可以省略不写,需要同时省略

6.11、正则表达式

正则的作用

  • 校验字符串是否满足一定规则
  • 在一段文本中查找满足要求的内容
public static void main(String[] args) {
    // 验证手机号
    String pattern1 = "1[3-9]\\d{9}";
    System.out.println("18396659666".matches(pattern1));     // true
    System.out.println("08396659666".matches(pattern1));     // false
    System.out.println("16396659666".matches(pattern1));     // true
    System.out.println("163966239666".matches(pattern1));    // false

    // 验证QQ邮箱
    String pattern2 = "^[a-zA-Z0-9._%+-]+@qq.com$";
    System.out.println("464151@qq.com".matches(pattern2));        // true
    System.out.println("464151@163.com".matches(pattern2));       // false
    System.out.println("zhao46@qq.com".matches(pattern2));        // true
}

7、异常机制(Exception)


7.1、什么是异常?

生活中的异常:

正常情况下,小张每天开车去上班,耗时大约30分钟

但是,异常情况迟早要发生!

程序中的异常:

异常是指代码在编译或者执行的过程中可能出现的错误,如:文件找不到、网络连接失败、接收非法参数等

异常处理的必要性:任何程序都可能存在大量的未知问题、错误;如果不对这些问题进行正确处理,则可能导致程序的中断,造成不必要的损失

7.2、异常的分类

**检查时异常(CheckedException):**不是RuntimeException或者其子类的异常,编译阶段就报错,必须处理,否则代码不通过,用于提醒程序员

**运行时异常(RuntimeException):**直接继承自RuntimeException或者其子类,编译阶段不会报错,运行时可能出现的错误

错误Error:系统级别问题、JVM退出等,代码脱离程序员控制的问题

7.3、异常体系结构

  • Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有错误和异常的超类
  • 在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception

在这里插入图片描述

Error:

  • Error对象是由Java虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关
  • Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError(内存溢出)这些异常发生是,Java虚拟机(JVM)一般会选择线程终止!(不能手动处理)

Exception

在Exception分支中有一个重要的子类RuntimeException(运行时异常)

Exception异常层次结构的父类
ArithmeticException算术异常情形,如以零作除数
ArrayIndexOutOfBoundsException数组下标越界
NullPointerException尝试访问 null 对象成员
ClassNotFoundException找不到类等异常,该类为不检查异常,程序中可以选择捕获,也可以不处理
IllegalArgumentException方法接收到非法参数
ClassCastException对象强制类型转换出错
NumberFormatException数字格式转换异常,如把"abc"转换成数字
MissingResourceException丢失资源
StackOverflowError栈溢出异常
ConcurrentModificationException并发修改异常

这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生;

ErrorException的区别:

Error通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程;

Exception通常情况下是可以被程序处理的,并且在程序中应尽可能的去处理这些异常

异常的传递

  • 异常的传递:按照方法的调用链反向传递,如始终没有处理异常,最终会由JVM进行默认异常处理(打印堆栈跟踪信息),并中断程序运行

7.4、异常处理

Java的异常处理是通过5个关键字来实现的:try、catch、 finally、throw、throws

捕获异常

捕获异常:可以让程序继续往下执行,不会中断程序!前提条件:需要捕获到需要处理的异常

try------>执行可能产生异常的代码

catch------>捕获异常,并处理

finally------>无论是否发生异常,代码总能执行

public class Test {
    public static void main(String[] args) {
        int a = 10, b = 0;
        // 假设要捕获多个异常:从小到大
        try {   // try 监控区域  可能出现异常的代码
            System.out.println(a / b);
        } catch (ArithmeticException ex) {    // catch(想要捕获的异常类型!) 捕获异常
            System.out.println("除数不能为0!");
            System.exit(1);
        } catch (Error ex) {
            System.out.println("Error");
            return;
        } catch (Exception ex) {
            System.out.println("Exception");
        } catch (Throwable ex) {
            System.out.println("Throwable");
        } finally {  // 处理善后工作 最终都会执行,除非手动退出JVM
            System.out.println("程序结束!");
        }

        // finally,可以不要finally,假设需要I/O资源关闭,就需要使用finally了
    }
}

注:

  • 排列catch语句的顺序:先子类后父类
  • 发生异常时按顺序逐个匹配
  • 只执行第一个与异常类型匹配的catch语句
  • finally根据需求可写或不写

存在return的try-catch-finally块的执行顺序:

try(产生异常对象) -----> catch(异常类型匹配) -----> finally(执行finally块) -----> return (执行return,退出方法)

声明异常

throws:写在方法定义处,表示声明一个异常,告诉调用者,使用本方法可能会出现那些异常

public static void sub(int a, int b) {
    if (b == 0) {
        throw new ArithmeticException();    // throw 主动抛出异常,一般在方法中使用
    }
    System.out.println(a / b);
}

// 方式一:调用者,处理异常
public static void main(String[] args) {
    try {
        sub(6,0);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 方式二:调用者,继续声明异常
public static void main(String[] args) throws Exception {
    sub(6,0);
}

如果在一个方法体中抛出了异常,如何通知调用者?

  1. 声明异常,多个异常用逗号隔开
  2. 方式一:调用者,处理异常;
  3. 方式二:调用者,继续声明异常;

使用原则:底层代码向上声明或者抛出异常,最上层一定要处理异常,否则程序中断!

抛出异常

throw:写在方法内,结束方法,手动抛出异常对象,交给调用者,方法中下面的代码不再执行

语法:throw 异常对象

public class Test {
    public static void main(String[] args) throws Exception {
        Scanner input = new Scanner(System.in);
        System.out.print("请输入大于100的数字:");
        int num = input.nextInt();
        if (num < 100) {
            throw new Exception("输入的数字必须大于100");	// 手动抛出异常
        } else {
            System.out.println("num值为:" + num);
        }
    }
}

7.5、自定义异常

使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外,用户还可以自定义异常!

自定义异常的意义:就是为了让控制台的报错信息更加的见名知意

使用自定义异常类,大体分为以下几个步骤:

  1. 创建自定义异常类
  2. 编写继承关系
  3. 空参构造,带参构造
public class AgeOutOfBoundsException extends RuntimeException{
    // 运行时:RuntimeException
    // 编译时:Exception
    public AgeOutOfBoundsException() {
    }

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

经验:

  • 处理运行时异常时,采用逻辑去合理的规避同时辅助try-catch处理
  • 在多重catch块后面,可以加一个catch(Exception)来处理可能会被遗漏的异常
  • 对于不确定的代码,也可以加上try-catch,处理潜在的异常
  • 尽量去处理异常,切忌只是简单地调用printStackTrace()去打印输出
  • 尽量添加finally语句块去释放占用的资源

8、集合框架


8.1、为什么使用集合框架?

例:存储一个班学员信息,假定一个班容纳20名学员,我们可以使用数组存储

如何存储每天的新闻信息?每天的新闻总数不确定,太少浪费空间,太多空间不足

如果并不知道程序运行时会需要多少对象,或者需要更复杂方式存储对象——可以使用Java集合框架

集合和数组的区别:

  • 数组声明了它容纳的元素的类型,而集合不声明
  • 数组是静态的,一个数组实例具有固定的大小,一旦创建了就无法改变容量了。而集合是可以动态扩展容量,可以根据需要动态改变大小,集合提供更多的成员方法,能满足更多的需求
  • 数组的存放的类型只能是一种(基本类型/引用类型),集合存放的类型可以不是一种(不加泛型时添加的类型是Object)
  • 数组是Java语言中内置的数据类,是线性排列的,执行效率或者类型检查都是最快的

8.2、Java集合体系结构

Collection单列集合,每个元素(数据)只包含一个值

Map双列集合,每个元素包含两个值(键值对)

Collection集合特点

List系列集合:添加的元素是有序、可重复、有索引

  • 有序:存储和取出的元素顺序一致
  • 可重复:存储的元素可以重复
  • 有索引:可以通过索引操作元素
  • ArrayList、LinekdList

Set系列集合:添加的元素是无序、不重复、无索引

  • HashSet:无序、不重复、无索引
  • LinkedHashSet:有序、不重复、无索引
  • TreeSet:可排序、不重复、无索引

Map接口用于存储任意键值对,提供key到value的映射

  • 键:无序、无索引、唯一(元素不可以重复)
  • 值:无序、无索引、不唯一(元素可以重复)

集合都是泛型的形式,可以在编译阶段约束集合只能操作某种数据类型

注:集合和泛型都只能支持引用数据类型,不支持基本数据类型,所以集合中存储的元素都认为是对象

Collection是单列集合的顶层接口,它的功能是全部单列集合都可以继承使用的

8.3、泛型

何为泛型?
  • Java泛型是JDK1.5中引入的一个新特性,可以在编译阶段约束操作的数据类型,并进行检查
  • 泛型的格式:<数据类型>
  • 集合体系的全部接口和实现类都是支持泛型的使用的

泛型的好处:

  • 统一数据类型
  • 把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为编译阶段类型就能确定下来

注:泛型只能支持引用数据类型

定义泛型类

常见形式有泛型类(类后面)、泛型接口(接口后面)、泛型方法(方法声明上)

定义类时同时定义了泛型的类就是泛型类

语法格式:访问修饰符 class 类名<泛型变量>{ }

此处T可以理解为变量,但是不是用来记录数据的,而是记录类型的,可以写成:T、E、K、V等

// 当编写一个类时,不确定类型,那么该类可以定义为泛型类
public class MyArrayList<T> {
    Object[] obj = new Object[10];
    int size;

    public boolean add(T t) {
        obj[size] = t;
        size++;
        return true;
    }

    public T get(int index) {
        return (T) obj[index];
    }

    @Override
    public String toString() {
        return Arrays.toString(obj);
    }
}

泛型核心思想:把出现泛型变量的地方全部替换成传输的真实数据类型

自定义泛型方法

定义方法时同时定义了泛型的方法就是泛型方法

语法格式:修饰符 <泛型变量> 方法返回值 方法名称(形参列表){}

作用:方法中可以使用泛型接收一切实际类型的参数,方法更具备通用性

public class ArrayUtil {
    private ArrayUtil() {
    }

    // 传递一个任意类型的数组,都能返回它的内容
    public static <T> String printStr(T[] arr) {
        StringJoiner sj = new StringJoiner(",", "[", "]");
        for (int i = 0; i < arr.length; i++) {
            sj.add(arr[i] + "");
        }
        return sj.toString();
    }
}
自定义泛型接口

使用了泛型定义的接口就是泛型接口

语法格式:修饰符 interface 接口名称<泛型变量>{}

作用:泛型接口可以让实现类选择当前功能需要操作的数据类型

public interface Data<T> {
    void add(T e);
    T query();
}
泛型的继承与通配符

泛型不具备继承性,但是数据具备继承性

泛型的通配符:可以限定类型的范围

// ? 表示不确定的类型
// ? extends E:表示可以传递E或者E的子类类型
// ? super E:表示可以传递E或者E的父类类型
public static void method(ArrayList<? super Fu> list){

}

应用场景:

  • 在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口
  • 如果类型不确定,但是能知道是哪个继承体系中的,就可以使用泛型的通配符

8.4、List接口实现类

List接口的实现类:ArrayList(长度可变的数组、存储空间连续) LinkedList链表存储(双向链表存储

ArrayList底层是基于数组实现的,在内存中分配连续的空间。**遍历元素和随机访问(查询快)**元素的效率比较高,**增、删慢;**运行效率快、线程不安全(JDK1.2)

ArrayList

List 常用API

方法名说 明
boolean add(Object o)在列表的末尾顺序添加元素,起始索引位置从0开始
void add(int index,Object o)在指定的索引位置添加元素。索引位置必须介于0和列表中元素个数之间
int size()返回列表中的元素个数
Object get(int index)返回指定索引位置处的元素。取出的元素如果是Object类型,使用前需要进行强制类型转换
boolean contains(Object o)判断列表中是否存在指定元素
boolean remove(Object o)从列表中删除元素,返回是否删除成功
Object remove(int index)从列表中删除指定位置元素,返回被删除的元素,起始索引位置从0开始
public E set(int index, E element)修改指定索引出的元素,返回被修改的元素
public static void main(String[] args) {
    // 1、创建集合
    List<Student> students = new ArrayList<>();

    // 初始化数据
    Student stu1 = new Student("张三", 18);
    Student stu2 = new Student("李四", 20);
    Student stu3 = new Student("王五", 22);
    Student stu4 = new Student("赵六", 25);

    // 2、把学生对象添加到集合中
    students.add(stu1);
    students.add(stu2);
    students.add(stu3);
    students.add(stu4);

    // 3、获取集合长度
    System.out.println("共有" + students.size() + "位同学");

    // 遍历输出
    for (int i = 0; i < students.size(); i++) {
        // 获取每个学生对象
        Student stu = students.get(i);
        System.out.println(stu.getName() + "," + stu.getAge());
    }

    // 4、删除下标为2的同学,从0开始,返回删除的元素
    Student student = students.remove(2);
    System.out.println("del:" + student.toString());

    // 5、判断是否包含指定元素
    // 底层是通过equals方法进行判断是否存在的
    // 所以,如果集合中存储的是自定义对象,要通过contains方法判断是否包含,那么在JavaBean类中,一定要重写equals方法!
    Student stu5 = new Student("张三", 18);
    boolean result = students.contains(stu5);
    System.out.println("result = " + result);

    // 6、清空集合
    students.clear();
    // 7、判断集合是否为空
    System.out.println(students.isEmpty());
}
Collection的遍历方式
  • 迭代器遍历
  • 增强for遍历
  • Lambda表达式遍历
  • 列表迭代器

迭代器在Java中的类是Iterator,迭代器是集合专用的遍历方式

public static void main(String[] args) {
    List<Student> students = new ArrayList<>();

    Student stu1 = new Student("张三", 18);
    Student stu2 = new Student("李四", 20);
    Student stu3 = new Student("王五", 22);

    students.add(stu1);
    students.add(stu2);
    students.add(stu3);

    System.out.println("=====使用迭代器遍历=====");
    // 返回迭代器对象,默认指向当前集合的0索引位置
    Iterator<Student> stuIt = students.iterator();
    // 判断当前位置是否还有元素,有则返回true,反之为false
    while (stuIt.hasNext()) {
        // 获取当前位置的元素,并将迭代器移动到下一个位置
        Student stu = stuIt.next();
        System.out.println(stu.getName() + "\t" + stu.getAge());
        if (stu.getName().equals("李四")){
            // 注:此处不能调用集合中的remove方法
            stuIt.remove();
        }
    }
}

注:迭代器是不依赖索引的

  • 迭代器遍历完毕,指针不会复位
  • 循环中只能用一次next方法
  • 迭代器遍历时,不能用集合的方法进行增加或者删除

增强for:既可以遍历集合也可以遍历数组(JDK1.5)

  • 增强for的底层就是迭代器,为了简化迭代器的代码书写
public static void main(String[] args) {
	// 数据省略...

    System.out.println("=====使用增强for遍历=====");
    for (Student stu : students) {
        System.out.println(stu.getName() + "\t" + stu.getAge());
    }
}

注:修改增强for中的变量,不会改变集合中原本的数据,增强for一般只用于遍历数据!

Lambda表达式遍历:

public static void main(String[] args) {
	// 数据省略...
    
    System.out.println("=====使用Lambda表达式遍历=====");
    students.forEach(new Consumer<Student>() {
        @Override
        public void accept(Student s) {
            System.out.println(s.getName() + "\t" + s.getAge());
        }
    });

    // 简化写法
    students.forEach(s -> System.out.println(s.getName() + "\t" + s.getAge()));
}

列表迭代器

public static void main(String[] args) {
    ArrayList<Integer> numbers = new ArrayList<>();
    numbers.add(55);
    numbers.add(66);
    numbers.add(77);
    numbers.add(88);

    System.out.println("=====普通for=====");
    for (int i = 0; i < numbers.size(); i++) {
        Integer number = numbers.get(i);
        System.out.print(number + "\t");
    }

    System.out.println("=====列表迭代器遍历=====");
    // 获取列表迭代器对象,里面指针也是默认指向0索引
    ListIterator<Integer> listIt = numbers.listIterator();
    while (listIt.hasNext()) {
        Integer number = listIt.next();
        // 在遍历的过程中,可以添加元素
        if (number == 66) {
            listIt.add(99);
        }
        if (number == 88) {
            listIt.remove();
        }
        System.out.print(number + "\t");
    }

    System.out.println("\nLambda表达式");
    numbers.forEach(i -> System.out.print(i + "\t"));
}
五种迭代器应用场景
  • 迭代器遍历:在遍历过程中需要删除元素
  • 列表迭代器:在遍历过程中需要添加元素
  • 增强for或Lambda表达式:只是想获取集合中的每个元素
  • 普通for:遍历的时候需要操作索引
LinkedList

LinkedList采用双向链表存储方式。**插入、删除(增删快)**元素时效率比较高,**查询慢;**运行效率快,线程不安全

LinkedList类的特有方法

方法名说 明
void addFirst(Object o)在列表的首部添加元素
void addLast(Object o)在列表的末尾添加元素
Object getFirst()返回列表中的第一个元素
Object getLast()返回列表中的最后一个元素
Object removeFirst()删除并返回列表中的第一个元素
Object removeLast()删除并返回列表中的最后一个元素
public class TestLinkedList {
    public static void main(String[] args) {
        LinkedList<Student> list = new LinkedList();
        // 初始化数据
        Student stu1 = new Student(1000, "张三", "男", "S1");
        Student stu2 = new Student(1001, "李四", "男", "S2");
        Student stu3 = new Student(1002, "王五", "女", "Y2");
        Student stu4 = new Student(1003, "赵六", "男", "S1");

        list.addFirst(stu4);        // 在第一个位置添加元素
        list.add(stu2);
        list.add(stu3);
        list.addLast(stu1);     // 在最后一个位置添加元素

        System.out.println("集合中第一位同学的姓名是:"+list.getFirst().getName());  // 获取集合中第一个元素
        System.out.println("集合中最后一位同学的姓名是:"+list.getLast().getName()); // 获取集合中最后一个元素

        list.removeFirst();     // 删除集合中第一个元素
        list.removeLast();      // 删除集合中最后一个元素

        System.out.println("共有" + list.size() + "位同学:");
        // 使用增强for循环遍历输出
        for (Student std : list) {
            System.out.println(std.getName() + "\t" + std.getName() + "\t" + std.getGender() + "\t" + std.getGrade());
        }
    }
}

8.5、Set接口实现类

Set接口的实现类:HashSet(内部的数据结构是哈希表) TreeSet(基于红黑树(二叉查找树)实现的

哈希表是一种对于增删改查数据性能都较好的结构

哈希表的组成

  • JDK8之前的,底层使用数组+链表组成
  • JDK8开始后,底层采用数组+链表+红黑树组成

哈希值

  • 根据hashCode方法算出来的int类型的整数
  • 该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
  • 一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值

对象的哈希值特点

  • 同一个对象多次调用hashCode()方法返回的哈希值是相同的
  • 默认情况下,不同对象的哈希值是不同的
  • 但是在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样(哈希碰撞)
HashSet
  • 无序:存取顺序不一致
  • 不重复:可以去除重复
  • 无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素

HashSet是基于HashCode计算元素存放位置, 如果对象的hashCode值不同,则不用判断equals方法,就直接存到HashSet中。当存入元素的哈希码相同时,会调用equals进行确认,如结果为true,则拒绝后者存入。运行效率快、线程不安全

HashSet的使用

public static void main(String[] args) {
    // 创建集合
    HashSet<Student> studentHashSet = new HashSet<>();
    // 1.添加数据
    Student s1 = new Student("小赵", 18);
    Student s2 = new Student("小张", 19);
    Student s3 = new Student("小林", 20);
    Student s4 = new Student("小李", 21);
    studentHashSet.add(s1);
    studentHashSet.add(s2);
    studentHashSet.add(s3);
    studentHashSet.add(s4);

    // 1.添加元素
    // 没有重写hashcode和equals是可以添加的,因为每个对象的哈希值都不一样
    // 但重写了之后,则不可以进行添加,因为有相同元素了(不可重复)
    studentHashSet.add(new Student("小林", 20));
    System.out.println("元素个数:" + studentHashSet.size() + "\n" + studentHashSet);
    // 2.删除元素
    // 如果重写hashcode和equals是可以删除的  否则反之
    studentHashSet.remove(new Student("小李", 21));
    System.out.println("删除之后元素个数:" + studentHashSet.size() + "\n" + studentHashSet);
    // 3.遍历元素
    // 3.1使用增强for循环遍历
    System.out.println("=====使用增强for遍历=====");
    for (Student stu : studentHashSet) {
        System.out.println(stu.getName() + "\t" + stu.getAge());
    }
    // 3.2使用迭代器遍历
    System.out.println("=====使用迭代器遍历=====");
    Iterator<Student> it = studentHashSet.iterator();
    while (it.hasNext()) {
        Student stu = it.next();
        System.out.println(stu.getName() + "\t" + stu.getAge());
    }
    // 3.3使用Lambda表达式遍历
    System.out.println("=====使用Lambda表达式遍历=====");
    studentHashSet.forEach(stu -> System.out.println(stu.getName() + "\t" + stu.getAge()));
    studentHashSet.forEach(System.out::println);

    // 判断
    // 重写hashcode和equals是寻找的是同一个对象  结果为true  否则为false
    System.out.println(studentHashSet.contains(new Student("小林", 20)));     // true
    System.out.println(studentHashSet.contains(new Student("小林", 21)));     // false
    System.out.println(studentHashSet.isEmpty());       // false
}
LinkedHashSet
  • 有序、不重复、无索引

  • 底层基于哈希表,使用双链表记录添加顺序

LinkedHashSet的使用

public static void main(String[] args) {
    // 创建集合对象
    LinkedHashSet<String> strs = new LinkedHashSet<>();

    // 添加元素(数据是有顺序的)
    strs.add("苹果");
    strs.add("香蕉");
    strs.add("葡萄");
    strs.add("橙子");

    // 打印输出
    System.out.println(strs);
}
TreeSet
  • 可排序:按照元素的大小默认升序(从小到大)排序
  • 不重复
  • 无索引
  • TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好

TreeSet的使用

  • 对于数值类型:Integer,Double,默认按照从小到大的顺序进行排序
  • 对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序
  • 对于自定义类型如Student对象,TreeSet无法直接排序,需要自定义规则

方式一:默认排序/自然排序:Javabean类实现Comparable接口指定比较规则

方式二:创建TreeSet对象时候,传递比较器Comparator指定规则

public static void main(String[] args) {
    // 创建集合
    TreeSet<Student> ts = new TreeSet<>();

    Student s1 = new Student("Jack", 18);
    Student s2 = new Student("Lucy", 22);
    Student s3 = new Student("Lili", 25);
    Student s4 = new Student("Torry", 21);

    // 添加数据
    ts.add(s1);
    ts.add(s2);
    ts.add(s3);
    ts.add(s4);

    Iterator<Student> it = ts.iterator();
    while (it.hasNext()) {
        Student stu = it.next();
        System.out.println(stu.getName() + "\t" + stu.getAge());
    }
}

public class Student implements Comparable<Student> {
    private String name;
    private int age;

    @Override
    public int compareTo(Student o) {
        // 负数:认为要添加的元素是小的,存左边
        // 正数:认为要添加的元素是大的,存右边
        // 0:认为要添加的元素已经存在,不再进行存储
        // this:当前要添加的元素
        // o:表示已经在红黑树中存在的元素
        return -this.getAge() - o.getAge();
    }
}
public static void main(String[] args) {
    // 按照长度排序,如果一样长则按照首字母排序
    TreeSet<String> ts = new TreeSet<>((o1, o2) -> {
        // o1:表示当前要添加的元素
        // o2:表示已经在红黑树存在的元素
        // 按照长度排序
        int len = o1.length() - o2.length();
        // 等于0说明长度一样,按照首字母顺序排列
        return len == 0 ? o1.compareTo(o2) : len;
    });
    ts.add("a");
    ts.add("ad");
    ts.add("aoe");
    ts.add("qy");
    ts.add("ioe");
    ts.add("qz");

    for (String str : ts) {
        System.out.print(str + "\t");
    }
}

注:TreeSet集合是一定要排序的,可以将元素按照指定的规则进行排序,否则会报错:ClassCastException

8.6、Map接口实现类

Map接口专门处理键值映射数据的存储,可以根据键实现对值的操作

  • 键不能重复,值可以重复
  • 键和值是一一对应的,每一个键只能找到自己对应的值

Map接口的实现类:HashMap(基于哈希表实现)和TreeMap(基于红黑树(二叉查找树)实现的

Map常用API

Map是双列集合的顶层接口,它的功能是全部双列集合都可以继承使用的

方法名称说明
V put(K key,V value)添加元素
V remove(Object key)根据键删除键值对元素
void clear()移除所有的键值对元素
boolean containsKey(Object key)判断集合是否包含指定的键
boolean containsValue(Object value)判断集合是否包含指定的值
boolean isEmpty()判断集合是否为空
int size()集合的长度,也就是集合中键值对的个数
public static void main(String[] args) {
    // 创建Map集合对象
    HashMap<String,String> map = new HashMap<>();
    // 1、添加元素
    // 如果添加的键不存在,那么直接把键值对添加到map集合中,方法返回null
    // 如果添加的键存在,那么会把原来的键值对对象覆盖(修改),并把覆盖的值进行返回
    map.put("CN","中国");
    String s1 = map.put("RU", "俄罗斯联邦");
    System.out.println(s1);     // null
    map.put("FR","法兰西共和国");
    map.put("US", "美利坚联合众国");

    String s2 = map.put("CN", "中华人民共和国");
    System.out.println(s2);     // 中国

    // 2、根据键删除值 并把删除的值进行返回
    String delEle = map.remove("US");
    System.out.println(delEle);     // 美利坚联合众国

    // 3、判断是否包含指定key
    boolean result = map.containsKey("US");
    System.out.println(result);     // false

    // 4、判断是否包含指定值
    System.out.println(map.containsValue("中华人民共和国"));       // true

    // 5、集合的长度
    System.out.println(map.size());     // 3

    // 6、清空
    map.clear();

    // 7、判断是否为空
    System.out.println(map.isEmpty());      // true

    System.out.println(map);
}
HashMap
  • HashMap底层是哈希表结构的,特点都是由键决定的:无序、不重复、无索引
  • 依赖hashCode方法和equals方法保证键的唯一
  • 如果键存储的是自定义对象,需要重写hashCode和equals方法
  • 如果值存储自定义对象,不需要重写hashCode和equals方法

HashMap的使用

public static void main(String[] args) {
    // 创建集合
    HashMap<Student, String> map = new HashMap<>();
    Student s1 = new Student("小白", 20);
    Student s2 = new Student("夏琳", 25);
    Student s3 = new Student("小昭", 23);
    // 添加元素
    map.put(s1, "河南-郑州");
    map.put(s2, "河南-洛阳");
    map.put(s3, "北京-海淀");

    // 当没有重写hash值和equals时,是可以重复添加的,如果重写了,则不可重复添加,且会把原来的家庭地址改为当前的 ”湖北-武汉“
    Student s4 = new Student("小昭", 23);
    map.put(s4, "湖北-武汉");       

    // 遍历集合
    for (Map.Entry<Student, String> entry : map.entrySet()) {
        System.out.println(entry.getKey() + "\t" + entry.getValue());
    }
}
Map集合的遍历方式
  • 键找值的方式遍历:先获取Map集合全部的键,再根据键遍历找出值
  • 键值对:键值对的方式遍历,把“键值对“看成一个整体
  • Lambda表达式
public static void main(String[] args) {
    // 1、创建集合
    HashMap<String, String> map = new HashMap<>();
    // 2、添加元素
    map.put("CN", "中国");
    map.put("RU", "俄罗斯联邦");
    map.put("FR", "法兰西共和国");
    map.put("US", "美利坚联合众国");

    // 3、通过键找值
    // 3.1、获取所有的key,并把这些键放入一个单列集合中
    Set<String> keys = map.keySet();
    // 3.2、遍历单例集合,得到每一个key
    System.out.println("增强for循环");
    for (String key : keys) {
        // System.out.println(key);
        // 3.3、利用map中的key获取对应的值
        String value = map.get(key);
        System.out.println(key + "\t" + value);
    }

    System.out.println("迭代器遍历");
    Iterator<String> it = keys.iterator();
    while (it.hasNext()) {
        String key = it.next();
        String value = map.get(key);
        System.out.println(key + "\t" + value);
    }

    System.out.println("Lambda表达式遍历");
    map.forEach((k, v) -> System.out.println(k + "\t" + v));

    // 4、map集合第二种遍历方式
    // 通过键值对对象进行遍历
    System.out.println("键值对方式");
    // 4.1、通过一个方法获取所有的键值对对象,返回一个Set集合
    Set<Map.Entry<String, String>> entries = map.entrySet();
    for (Map.Entry<String, String> entry : entries) {
        // 4.2、利用entry调用get方法获取键和值
        String key = entry.getKey();
        String value = entry.getValue();
        System.out.println(key + "\t" + value);
    }
}
LinkedHashMap
  • 由键决定:有序、不重复、无索引
  • 这里的有序指的是保证存储和取出的元素顺序一致
  • 原理:底层数据结构是依然哈希表,只是每个键值对元素又额外的多了一个双链表的机制记录存储的顺序
public static void main(String[] args) {
    LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
    // 存取顺序是一致的
    map.put("a", 97);
    map.put("c", 99);
    map.put("b", 98);
    map.put("b", 100);
    System.out.println(map);
}
TreeMap
  • TreeMap跟TreeSet底层原理一样,都是红黑树结构的
  • 由键决定特性:不重复、无索引、可排序
  • 可排序:只能对键进行排序

TreeMap的使用

public static void main(String[] args) {
    TreeMap<Student, String> tmMap = new TreeMap<>();

    Student s1 = new Student("zhaoliu", 25);
    Student s2 = new Student("xiaolin", 20);
    Student s3 = new Student("xiaobai", 22);
    Student s4 = new Student("zhaohei", 22);
    Student s5 = new Student("zhaohei", 22);

    tmMap.put(s1, "河南-洛阳");
    tmMap.put(s2, "北京-海淀");
    tmMap.put(s3, "上海-杨浦");
    tmMap.put(s4, "河南-郑州");
    tmMap.put(s5, "河南-郑州");

    for (Map.Entry<Student, String> student : tmMap.entrySet()) {
        System.out.println(student);
    }
}

public class Student implements Comparable<Student> {
    private String name;
    private int age;

    // 要求:按照学生年龄的降序排列,年龄一样按照姓名的字母排列,同姓名年龄视为同一个人
    @Override
    public int compareTo(Student o) {
        int i = o.getAge() - this.getAge();
        return i == 0 ? this.getName().compareTo(o.getName()) : i;
    }
}

注:TreeMap集合是一定要排序的,可以默认排序,也可以将键按照指定的规则进行排序,否则会报错:ClassCastException

统计次数

public static void main(String[] args) {
    // 需求:字符串“aababczuabcdacbauzvbbbcdexyz”,请统计字符串中每一个字符出现的次数
    // 新的统计思想:利用map集合进行统计
    // HashMap 和 TreeMap
    // 键:表示要统计的内容
    // 值:要统计的次数
    // 如果需要排序,则使用TreeMap,反之可以使用HashMap
    String str = "aababczuabcdacbauzvbbbcdexyz";

    // 存放数据以及数据出现的次数
    TreeMap<Character, Integer> map = new TreeMap<>();

    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (map.containsKey(c)) {
            // 存在       先把该元素的出现次数+1,再重新放入map集合中
            int count = map.get(c);
            count++;
            map.put(c, count);
        } else {
            // 不存在,直接添加即可
            map.put(c, 1);
        }
    }

    System.out.println(map);
}
可变参数

语法格式:数据类型... 参数名称

可变参数的作用

  • 可变参数用在形参中可以接收多个数据
  • 接收参数非常灵活,方便。可以不接收参数,可以接收1个或者多个参数,也可以接收一个数组
  • 可变参数在方法内部本质上就是一个数组
public static void main(String[] args) {
    int sum = getSum(1, 99, 98, 2, 97, 3);
    System.out.println(sum);
}

// 计算n个数据的和,甚至可以支持不接收参数进行调用
public static int getSum(int... params) {
    int sum = 0;
    for (int i = 0; i < params.length; i++) {
        sum += params[i];
    }
    return sum;
}

注:

  • 一个形参列表中可变参数只能有一个
  • 可变参数必须放在形参列表的最后面
集合工具类Collections

作用:Collections并不属于集合,是用来操作集合的工具类

方法名称说明
public static <T> boolean addAll(Collection<? super T> c, T… elements)给集合对象批量添加元素
public static void shuffle(List<?> list)打乱List集合元素的顺序
public static void main(String[] args) {
    // 创建集合
    ArrayList<String> list = new ArrayList<>();
    // 批量添加元素
    Collections.addAll(list, "苹果", "橘子", "香蕉", "葡萄", "菠萝");
    System.out.println(list);           // [苹果, 橘子, 香蕉, 葡萄, 菠萝]

    // 打乱集合中的元素(洗牌)
    Collections.shuffle(list);
    System.out.println(list);

    ArrayList<Integer> numbers1 = new ArrayList<>();
    Collections.addAll(numbers1, 99, 88, 15, 48, 46, 98, 100);

    // 交互集合中的元素位置
    Collections.swap(numbers1, 2, numbers1.size() - 2);
    System.out.println(numbers1);       // [99, 88, 98, 48, 46, 15, 100]

    // 求集合元素较大值
    Integer max = Collections.max(numbers1);
    System.out.println(max);        // 100
    // 求集合元素较小值
    Integer min = Collections.min(numbers1);
    System.out.println(min);    // 15

    // 元素排序(默认按照升序,可定制排序规则)
    Collections.sort(numbers1, (o1, o2) -> o2.compareTo(o1));
    System.out.println(numbers1);       // [100, 99, 98, 88, 48, 46, 15]

    // 使用指定元素填充集合
    Collections.fill(numbers1, 100);
    System.out.println(numbers1);       // [100, 100, 100, 100, 100, 100, 100]

    // 拷贝集合指定集合中的元素(新集合的元素大小要大于等于源集合的元素,否则会报错)
    List<Integer> numbers2 = Arrays.asList(new Integer[numbers1.size()]);
    System.out.println(numbers2);

    Collections.copy(numbers2, numbers1);
    System.out.println(numbers2);
}

8.7、不可变集合

不可变集合,就是不可被修改的集合

集合的数据项在创建的时候提供,并且在整个生命周期中都不可改变。否则报错

为什么要创建不可变集合?

不想让他人修改集合中的内容,如果某个数据不能被修改,把它防御性地拷贝到不可变集合中是个很好的实践

在List、Set、Map接口中,都存在of方法,可以创建一个不可变的集合

方法名称说明
static <E> List<E> of(E…elements)创建一个具有指定元素的List集合对象
static <E> Set<E> of(E…elements)创建一个具有指定元素的Set集合对象
static <K , V> Map<K,V> of(E…elements)创建一个具有指定元素的Map集合对象

该集合不能添加,不能删除,不能修改,只能查询(只读)

public static void main(String[] args) {
    // 创建不可变的list集合,一旦创建了不可变的集合,则里面的内容不可以被修改、删除、添加,只能查询(只读的)
    List<String> list = List.of("苹果", "香蕉", "葡萄", "火龙果", "甜橙");

    // 如果强行添加/修改/删除,则报错:UnsupportedOperationException
    // list.add("西瓜");
    // list.remove(0);
    // list.set(1,"甘蔗");

    for (String fruit : list) {
        System.out.println(fruit);
    }
}

三种方式的细节:

  • List:直接用
  • Set:元素不能重复
  • Map:元素不能重复、键值对数量最多是10个,超过10个用ofEntries方法

9、Stream流


9.1、什么是Stream流?

在Java 8中,得益于Lambda所带来的函数式编程, 引入了一个全新的Stream流概念

目的:用于简化集合和数组操作的API

Stream流式思想的核心:

  • 先得到集合或者数组的Stream流(就是一根传送带),把元素放上去
  • 然后就用这个Stream流简化的API来方便的操作元素

在这里插入图片描述

操作Stream流步骤:

  • 获取Stream流:创建一条流水线,并把数据放到流水线上准备进行操作
  • 中间方法:流水线上的操作。一次操作完毕之后,还可以继续进行其他操作
  • 终结方法:一个Stream流只能有一个终结方法,是流水线上的最后一个操作

9.2、获取Stream流

获取Stream流的方式

名称说明
default Stream<E> stream()Collection中的默认方法(单列集合)
Stream<T> stream(T[] array)Arrays工具类中的静态方法(数组)
Stream<T> of(T… values)Stream接口的静态方法(一堆零散数据)
无法直接使用Stream流,需要变为单列集合操作(双列集合)
public static void main(String[] args) {
    // 单列集合获取Stream流
    ArrayList<Integer> numbers = new ArrayList<>();
    Collections.addAll(numbers, 59, 87, 61, 84, 100, 985, 211, 863);
    // 筛选出集合中的数值大于100的
    // 获取到一条流水线,并且把数据放到流水线上
    Stream<Integer> stream = numbers.stream();
    // 先使用中间方法过滤掉数值小于100的再使用终结方法打印输出
    stream.filter(n -> n > 100).forEach(n -> System.out.println(n));

    // 集合中的数据还是原来的,没有被修改
    System.out.println(numbers);

    int[] arr = {59, 16, 49, 8, 96, 56, 38, 89};
    // 调用数组工具类的静态方法获取stream流
    Arrays.stream(arr).forEach(n -> System.out.println(n));

    // 调用Stream的静态方法获取stream流
    Stream.of(99, 88, 66, 778, 16, 100).forEach(n -> System.out.println(n));

    HashMap<Integer, String> map = new HashMap<>();
    map.put(1001, "苹果");
    map.put(1002, "香蕉");
    map.put(1003, "葡萄");

    // 双列集合是无法直接获取stream流的,需要先变为单列集合
    // 把map集合中的键值对放到了流水线上
    map.entrySet().stream().forEach(f -> System.out.println(f));
}

9.3、操作Stream流

Stream流的常用API(中间操作方法)

名称说明
Stream<T> filter(Predicate<? super T> predicate)用于对流中的数据进行过滤。
Stream<T> limit(long maxSize)获取前几个元素
Stream<T> skip(long n)跳过前几个元素
Stream<T> distinct()去除流中重复的元素。依赖(hashCode和equals方法)
static <T> Stream<T> concat(Stream a, Stream b)合并a和b两个流为一个流
Stream<R> map(Function<? super T, ? extends R> mapper)转换流中的数据类型

注:

  • 中间方法也称为非终结方法,调用完成后返回新的Stream流可以继续使用,支持链式编程
  • 在Stream流中无法直接修改集合、数组中的数据

Stream流的常见终结操作方法

名称说明
void forEach(Consumer action)对流中的数据进行遍历
long count()统计

注:终结操作方法,调用完成后流就无法继续使用了,原因是不会返回Stream了

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    ArrayList<String> list2 = new ArrayList<>();
    Collections.addAll(list, "张三", "李四", "王五", "赵六", "钱七", "赵六", "孙八", "赵六", "周九", "吴十", "吴一", "吴二");
    Collections.addAll(list2, "苹果", "香蕉", "橘子", "葡萄");

    // 把姓 吴 数据的留下,其他数据过滤掉
    // 注:原来的stream流只能使用一次,建议使用链式编程
    list.stream().filter(name -> name.startsWith("吴")).forEach(name -> System.out.println(name));
    System.out.println("-----------------");

    // 获取前五条数据
    list.stream().limit(5).forEach(name -> System.out.println(name));
    System.out.println("-----------------");

    // 跳过前6条数据,获取从第7条开始的数据
    list.stream().skip(6).forEach(name -> System.out.println(name));
    System.out.println("-----------------");

    // 去除集合中的重复元素
    list.stream().distinct().forEach(name -> System.out.println(name));
    System.out.println("-----------------");

    // 合并二个流为一个大流
    Stream.concat(list.stream(), list2.stream()).forEach(n -> System.out.println(n));

    System.out.println(list.stream().count());      // 12
}

public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    Collections.addAll(list, "1001-苹果", "1002-香蕉", "1003-葡萄", "1004-甜橙");

    // 只获取集合中的编号
    // list.stream().map(new Function<String, Integer>() {
    //     @Override
    //     public Integer apply(String s) {
    //         String[] arr = s.split("-");
    //         String str = arr[0];
    //         int no = Integer.parseInt(str);
    //         return no;
    //     }
    // }).forEach(n -> System.out.println(n));

    list.stream()
        .map(s -> Integer.parseInt(s.split("-")[0]))
        .forEach(n -> System.out.println(n));
}

9.4、收集Stream流

收集Stream流的含义:就是把Stream流操作后的结果数据转回到集合或者数组中去

名称说明
toArray()收集流中的数据,放到数组中
collect(Collector collector)收集流中的数据,放到集合中
public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>();
    Collections.addAll(list, "赵云-男-25", "马超-男-30", "刘备-男-27", "貂蝉-女-22", "周瑜-男-25", "小乔-女-26", "王昭君-女-26", "杨玉环-女-29", "大乔-女-25");

    // 把集合中所有女性收集起来存入list集合当中
    List<String> girlList = list.stream()
        .filter(name -> "女".equals(name.split("-")[1]))
        .collect(Collectors.toList());

    System.out.println(girlList);

    // 把集合中年龄大于25并且为男性的人收集起来存入到set集合当中
    Set<String> boyList = list.stream()
        .filter(age -> Integer.parseInt(age.split("-")[2]) > 25)
        .filter(gender -> "男".equals(gender.split("-")[1]))
        .collect(Collectors.toSet());

    System.out.println(boyList);

    // 把集合中年龄小于等于25岁的女性收集起来并存入到map集合当中
    // 姓名:作为键
    // 年龄:作为值
    // Map<String, Integer> map = list.stream()
    //         .filter(age -> Integer.parseInt(age.split("-")[2]) <= 25)
    //         .filter(gender -> "女".equals(gender.split("-")[1]))
    //         .collect(Collectors.toMap(new Function<String, String>() {
    //             @Override
    //             public String apply(String s) {
    //                 return s.split("-")[0];
    //             }
    //         }, new Function<String, Integer>() {
    //             @Override
    //             public Integer apply(String s) {
    //                 int age = Integer.parseInt(s.split("-")[2]);
    //                 return age;
    //             }
    //         }));

    Map<String, Integer> map = list.stream()
        .filter(age -> Integer.parseInt(age.split("-")[2]) <= 25)
        .filter(gender -> "女".equals(gender.split("-")[1]))
        .collect(Collectors.toMap(name -> name.split("-")[0], age -> Integer.parseInt(age.split("-")[2])));
    System.out.println(map);
}

9.5、方法引用

何为方法引用?

  • 把已经有的方法拿过来用,当做函数式接口中抽象方法的方法体
  • ::方法引用符

规则:

  • 引用处必须是函数式接口
  • 被引用的方法必须已经存在
  • 被引用方法的形参和返回值需要和抽象方法保持一致
  • 被引用方法的功能要满足当前需求
public static void main(String[] args) {
    // 创建一个数组,进行倒序排列
    Integer[] arr = {46, 6789, 916, 99, 116, 97, 391, 95};

    // 匿名内部类
    // Arrays.sort(arr, new Comparator<Integer>() {
    //     @Override
    //     public int compare(Integer o1, Integer o2) {
    //         return o2 - o1;
    //     }
    // });

    // Lambda表达式
    // Arrays.sort(arr, (Integer o1, Integer o2) -> {
    //             return o2 - o1;
    //         }
    // );

    // Lambda表达式简化
    // Arrays.sort(arr, (o1, o2) -> o2 - o1);

    // 方法引用,引用静态方法
    Arrays.sort(arr, Test::desc);

    System.out.println(Arrays.toString(arr));
}

public static Integer desc(int x, int y) {
    return y - x;
}
方法引用的分类
  • 引用静态方法
    • 类名::静态方法
  • 引用成员方法
    • 对象::成员方法
    • 本类:this::方法名,只能在非静态方法中使用
    • 父类:super::方法名,只能在非静态方法中使用
  • 引用构造方法
    • 类名::new
  • 其他调用方式
    • 使用类名引用成员方法
      • 类名::成员方法,如果抽象方法中的第一个参数是A类的,只能引用A类中的方法
    • 使用数组的构造方法
      • 数据类型[]::new
public static void main(String[] args) {
    // 将集合中姓张并且长度为三的人打印输出
    ArrayList<String> list = new ArrayList<>();
    Collections.addAll(list, "张三", "张无忌", "赵敏", "周芷若", "张三丰");

    // list.stream().filter(new Predicate<String>() {
    //     @Override
    //     public boolean test(String s) {
    //         return s.startsWith("张") && s.length() == 3;
    //     }
    // }).forEach(s -> System.out.println(s));

    // 方法引用:引用其他类方法
    list.stream().filter(new StringOpr()::stringJudge).forEach(s -> System.out.println(s));


    // 把集合中的整数,收集到数组当中
    ArrayList<Integer> list2 = new ArrayList<>();
    Collections.addAll(list2, 99, 1, 98, 2, 97, 3, 96);

    // Integer[] arr = list.stream().toArray(new IntFunction<Integer[]>() {
    //     @Override
    //     public Integer[] apply(int value) {
    //         return new Integer[value];
    //     }
    // });

    // 方法引用:引用数组的构造方法
    // 注:数组的类型,需要和流中的数据类型保存一致
    Integer[] arr = list2.stream().toArray(Integer[]::new);
    System.out.println(Arrays.toString(arr));

    public static void main(String[] args) {
        // 创建集合添加Person对象, 对象属性:name,age
        // 只获取姓名并放到数组当中
        ArrayList<Person> list3 = new ArrayList<>();
        Collections.addAll(list3,
                           new Person("赵云", 25),
                           new Person("马超", 30),
                           new Person("王昭君", 22),
                           new Person("貂蝉", 26),
                           new Person("杨玉环", 29));

        // Person ---> String ---> 数组
        String[] arr = list3.stream().map(Person::getName).toArray(String[]::new);
        System.out.println(Arrays.toString(arr));
    }
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐