Day1—面向对象基础

Java的面向对象和JavaScript中的思想是一样的,但在个别语法上有点小区别,而且Java本身就是面向对象的语言,我们平时就是在一个个类文件中书写各种方法

使用IDE创建class文件的过程就等于我们在JavaScript中使用class关键字,IDE软件会自动生成class模版

类和对象

概念上和JavaScript中没区别,区别在于Java中类的声明格式为修饰符 class 类名,比如public class Student{},而对象的声明则没区别,格式为new 构造方法()

构造方法(函数)

Java中的构造函数和JavaScript不同,Java中构造函数的书写格式为public 类名(参数列表){},构造函数一定没有返回值类型

构造函数也是可以重载的

在不定义的情况下是有默认构造函数的,一旦自定义了构造函数则默认的构造函数失效,而不是对默认构造函数的重载,因此一旦自定义了构造函数,一定要手写无参的构造函数

成员变量/实例变量

对象抽象出来的属性,比如学生都有nameage等属性,这些属性在构造对象实例的时候一同构造形成对象自己的属性,这些属性一旦构造就是对象私有的,想要使用格式必须为对象.属性,而不能用类名来使用

类变量/静态变量

是所有对象共有的属性,使用格式为类名.属性,在程序开始的时候就初始化,程序结束才会消失,因此就算没有构造对象,静态变量仍然存在,书写格式为static 数据类型 属性名

成员方法/实例方法

概念和成员变量一样,书写格式为修饰符 返回值类型 方法名(参数列表){}

静态方法

和静态变量的概念相同,书写格式为修饰符 static 返回值类型 方法名(参数列表){},在我们平时书写的主函数就是静态方法public static void main(String[] args),静态方法只能调用静态的属性和静态的方法

作用域

按照作用域分变量分全局变量和局部变量,全局变量有成员变量和静态变量,他们的作用域是整个类,声明也是在所有方法之外,而局部变量的作用域是所在代码块{}中

全局变量会自动生成默认值,而局部变量不会自动生成默认值,如果不赋值则无法使用

全局变量之间不能重名,但全局变量和局部变量之间可以重名

局部变量在不嵌套的不同代码块中可以重名,如果两个代码块是包含关系则局部变量不能重名,比如

for(int i = 0 ; i < 5 ; i++){
    if(true){
        int i = 0; // 报错,不能重名
    }
}

this

Java中的this只用来做一件事,在方法中区分全局变量和局部变量,当一个方法需要参数时有可能局部变量会与全局变量重名而导致无法给全局变量赋值,此时可以使用this来区分全局变量

this指向当前对象

Day2—面向对象三大特征

封装

概念

尽可能隐藏对象的内部实现细节,控制对象的修改及访问的权限,将对象的属性和对应的行为封装

程序表达

在程序上表达起来就是使用private修饰符将属性和方法变成类私有的,让实例对象无法直接访问,也就是无法使用对象.属性或者对象.方法()来访问属性和方法

但使用private修饰符之后就会导致无法获取或者修改属性,因此我们必须为每个属性提供公共访问方法public void set属性名(){}public void get属性名(){},IDE一般提供快捷的生成settergetter的方法,以IDEA为例,在书写完私有属性后,在右键菜单选中—>Generate,选择Getter and Setter,选中所有属性然后点击OK就会自动生成

这种方法的好处是,我们可以在公共访问方法里书写逻辑语句来筛选合法数据

JavaBean设计

是一种被设计出来的规范,分为数据承载Bean和业务逻辑Bean

数据承载Bean规范是

  • 私有化属性
  • 对外提供公共的setter、getter⽅法
  • 提供有参⽆参构造

可以看出JavaBean很好地符合了封装的规范,因此JavaBean就是一种封装方法

至于更深的JavaBean概念等到后面再来理解,一开始不要给予太多压力

继承

继承的概念

继承就是子类可以通过继承来使用父类的方法和属性

作用是减少代码的冗余,提⾼代码的复⽤性

Java中所有的类都直接或间接继承了Object类

程序格式

子类 extends 父类,此时子类就可以直接使用父类的属性和方法了

属性和方法的继承

成员属性无论是否为私有的都可以被继承,即被存储在子类的存储空间中,但能不能被直接访问还是要看访问修饰符

成员方法中,在虚方法表中的方法一定能被继承,除此之外还有一些不在虚方法表上的方法也可以被继承,在下文中会提及

虚方法表

可以理解为除父类的私有方法、static修饰的方法、final修饰的方法、构造方法之外的方法都在虚方法表上

在继承时不需要将父类的方法都复制一份,而是使用虚方法表指向父类的方法,当调用时根据虚方法表来调用父类的方法

如果发生重写,重写的方法会覆盖虚方法表上原来的指向,这样在调用方法时就会调用重写后的方法

访问修饰符

访问修饰符用于控制类、变量、方法和构造方法的直接访问权限

我个人将测试分为子类在继承时直接访问父类中被修饰的属性、父类构造对象后使用对象调用父类中被修饰的属性、子类构造对象后使用对象调用从父类继承来的被修饰的属性

  1. 当我们子类在继承时以super.属性的方式来访问继承到的父类中被修饰的属性时,
修饰符 同包的子类 不同包的子类
private
默认(包访问) 可以访问
protected 可以访问 可以访问
public 可以访问 可以访问

从上表中可以看到,private修饰符修饰的属性无法被访问,但实际上如果使用从父类继承来的公开的setget方法却能够为private属性设置值以及读取值,代表属性实际上还是被继承并且存在于子类的空间中

  1. 当我们使用父类构造对象后,使用对象.属性的格式访问父类中被修饰属性和方法时
修饰符 本类内 同包(不同类包括子类内) 不同包(不同类包括子类内)
private 可以访问
默认(包访问) 可以访问 可以访问
protected 可以访问 可以访问
public 可以访问 可以访问 可以访问

还是那句话,不能直接访问不代表不存在,可以使用公共方法来间接访问,即getset方法

  1. 当我们使用子类在测试类中构造对象后,使用对象.属性的格式访问子类继承自父类中被修饰属性和方法时
修饰符 父、子、测试类同包 仅父、测试同包 仅父、子同包 仅子、测试同包 三者均不同包
private
默认(包访问) 可以访问
protected 可以访问 可以访问
public 可以访问 可以访问 可以访问 可以访问 可以访问

可以看出子类构造出来的对象中继承来的属性的权限是综合了父类和子类一起考虑的

其中protected较为独特,这是因为protected本质是默认包访问的一种延伸,是为了解决不同包的子类无法在继承时直接访问默认权限属性或方法的问题,因此protected属性和方法可以在不同包内被继承但是却无法被父类构造的对象访问,而使用子类构造出来的对象也只能在和父类同包的情况下才能使用继承来的protected修饰的属性

当然同理也可以知道,表中不可以直接访问的属性不代表不存在,事实上完全可以使用公开方法来间接地访问属性

方法重写

子类的成员属性和成员方法的调用采用就近原则,即子类有就用子类的,子类没有就用父类继承的,父类没有就用父类的父类继承的

如果子类方法和父类方法重名,因为就近原则会优先使用子类自己的方法,因此这种子类和父类方法名相同、参数列表相同、返回值类型相同的情况就叫做方法重写,在虚方法表上的方法可以被重写

方法重写要求子类的修饰符权限要比父类的宽泛或者一样

方法重写的返回值类型可以不同,但要求子类返回值类型必须是父类返回值类型的派生类

重写的方法必须加上@Override注解,这个注解标记的方法,就说明这个方法必须是重写父类的方法,否则编译阶段报错,可以提高代码的可读性和安全性

super关键字

super的作用体现在当子类的属性或者方法和父类重名时,通过super来代指从父类继承来的属性和方法,和this作区分,而当没有重名属性和方法时,thissuper指向的是同一个,也就是都指向从父类继承来的

子类的构造方法

子类的构造方法中第一行永远会默认调用父类的构造方法,即super(),他指代了父类的构造方法,因为必须先构造父类后,子类才能使用父类的属性

有时候我们在使用有参构造函数来初始化子类对象时需要将父类的属性一起初始化,此时就可以手动在子类构造函数第一行书写一个super(参数)来初始化父类属性

如果书写多个super(),则只有第一次生效,不会构造多个父类

多态

概念

父类引用指向子类对象,从而产生多种形态。

目的是为了让多种类型的对象可以统一管理

格式

  • 必须要有继承关系
  • 必须要有方法重写
  • 必须要有父类的引用指向子类的对象

最后一条的意思是,在使用时一定要写成父类 对象名 = new 子类()的格式,这也被称为向上转型

语法细节

  • 编译看左边:在书写程序的时候只能使用父类中定义过的并且在子类中被重写的方法,无法使用子类自有的方法
  • 运行看右边:在程序运行时,访问的方法是子类中重写的方法

注意以上口诀只针对方法,当调用属性时则运行时也看左边,也就是使用的属性还是父类所有的

事实上,在编译时因为在表面上这个对象是一个父类的变量,因此会在父类中查找你所调用的属性和方法

不过在实际运行时,它本质上还是子类对象,但是因为属性是直接复制父类的,因此他去优先查找父类属性时能直接查找到父类的属性,于是就使用父类的属性

但是他调用方法时所查找的虚方法表是子类的虚方法表,上面的方法已经是被子类中重写的方法所覆盖了,因此他调用方法时会根据索引去调用子类的方法

向上转型向下转型

父类 对象名1 = new 子类()就叫做向上转型

子类 对象名2 = (子类)对象名1就叫做向下转型,由父类数据类型转为子类数据类型,这样做的原因是,多态的情况下无法使用子类自有的属性和方法,因此将子类变回原来的类型就可以使用自己的方法了

需要注意的是,向下转型之前一定要先向上转型,且向下转型无法转到别的派生类中,只能转到其原本的子类类型,否则会报错

多态的作用

现在如果我们有一个Animal的抽象类里面定义了吃和睡的抽象方法,然后又有三个个实体类DogBirdFish,他们继承并重写了两个抽象方法,现在有一个饲养员Keeper,他的方法是给这三个动物喂食,那么我们如何书写这个方法呢,难道我们要写三个不同参数的方法重载吗,事实上使用多态直接一次就搞定了

写成这样public void feed(Animal animal){animal.eat();},我们使用父类声明变量,然后调用feed方法时传入三个类声明的对象,则参数就形成了多态,此时我们调用对象方法就是三个对象各自的方法了(运行看右边)

instanceof

因为有时候我们无法确定当前对象应该往哪个子类转型,因此可以用instanceof关键字来比较

对象名 instanceof 类名,当确实属于该类型时会返回true

他的工作原理是测试它左边的对象是否是它右边的类的实例,这里的实例包括直接子类的实例和间接子类的实例,甚至包括接口

jdk14新特性:可以在判断时直接强转,比如对象名 instanceof 类名 变量名,这句代码的意思是,判断这个对象是不是这个类型的实例,如果是则直接强转,如果不是则返回false

Day3—面向对象其他关键字

static 补充

之前说过static关键字可以用来修饰属性和方法生成静态属性和静态方法,静态属性和静态方法都是存储在静态区的

静态属性的补充

静态属性被继承后只能使用子类的类名访问

静态方法补充

静态方法中不能使用this和super关键字

静态方法可以被继承但无法被重写

被继承后使用子类的类名调用

静态代码块

除此之外还可以定义静态代码块static{ }

作用是可以给静态变量初始化,具有和静态变量一样的特性

静态的东西都只会在类初始化的时候执行一次,之后都再次不会执行

final关键字

意味是最终的

final修饰的类

final形容的类无法被继承

String、Math等都是final修饰的

final修饰的成员变量

final修饰的成员变量必须初始化,初始化时机可以在定义时直接初始化,也可以在构造函数中初始化,一旦初始化就不能再被改变,即变成常量

可以被继承

final形容的局部变量

final形容的局部变量不能被修改地址,因此这就意味着基本数据类型无法修改内容,但引用数据类型可以在不修改地址的情况下修改堆空间存储的内容

final修饰静态变量

final修饰静态变量时表示静态常量,final限制更改,static限制唯一共有

静态常量初始化时机在定义时直接初始化,也可以用静态代码块初始化

静态常量名全大写,两个单词用下划线连接,比如USER_NAME

可以被继承

final 修饰的方法

final 修饰的方法可以被继承但无法被覆盖重写

静态方法不需要final修饰,因为没必要,static已经不允许重写了

abstract

作用

在实际中有一些父类完全不需要实例化,比如Animal,在实际中构造的更多的是比他具体的子类对象,比如DogCat,为了限制这种对象的创建,需要用到abstract

或者很多方法在父类中完全不需要实现,因为子类一定会重写,这类方法就可以被写成抽象方法

语法特点

abstract修饰的类叫做抽象类,只能被继承无法被实例化

abstract修饰的方法叫做抽象方法,没有方法体,只有声明不需要具体实现,作用是在多人开发时可以统一方法格式,以及可以形成多态

一个类中存在抽象方法,则这个类必须是抽象类

抽象方法必须被子类重写,除非子类也是抽象类

抽象方法不能使用static修饰

Day 4—接口

概念和格式

接口相当于特殊的抽象类,但他不是抽象类,定义方式、组成部分与抽象类类似。使用interface关键字定义接口

抽象类是专门继承的,而接口是专门用来实现的,接口是一种定义的标准,如果想实现接口则必须符合接口的标准,即必须重写接口所有抽象方法(JDK1.8之后接口里可以书写普通方法)

修饰符 interface 接口名

语法

  • 所有属性都是公开静态常量,隐式使用public static final修饰,初始赋值,无法更改。
  • 所有方法都是公开抽象方法,隐式使用public abstract修饰,没有方法体。
  • 没有构造方法、动态代码块、静态代码块。不能被实例化
  • 类引用接口后必须重写所有接口方法,如果想不全部重写则要求该类为抽象类
  • 接口有多态,格式以及要求和普通类一样
  • 接口可以继承接口,且可以多继承
  • 接口可以多实现,即一个类可以实现多个接口,弥补单继承缺陷

作用

可以看出接口和抽象类有非常多的相似之处,那为什么还需要接口的,因为java中继承是单继承,子类只能继承一个父类,这就产生了一个问题

现在如果我们有一个Animal的抽象类里面定义了吃eat()和睡sleep()的抽象方法,然后又有三个个实体类DogBirdFish,他们继承并重写了两个抽象方法,现在有一个饲养员Keeper,他的方法是给这三个动物喂食public void feed(Animal animal){animal.eat();},我们这里使用了多态的写法,这样就不需要写三遍不同参数类型的方法重载了

此时如果我们想给饲养员添加一个喂水的方法怎么办呢

  • 难道在Animal类中添加一个抽象方法drink()吗,可这样Fish也要重写这个方法,而Fish并不需要这个方法
  • 难道在DogBird中各自写一个方法吗,可这样我们就用不了多态,在Keeper中就需要写多个方法重载
  • 难道我们要细分喝水的和不喝水的Animal吗,这样以后能不能飞、能不能游泳、能不能看家等都要分一遍吗

这个时候接口的作用就出来了,我们定义一个接口DrinkAble,里面定义一个drink的抽象方法,然后让DogBird实现,此时我们Keeper中的方法就可以写成public void feedWater(DrinkAble animal){animal.drink();},看,又形成了多态

因此我们可以知道接口的作用

  • 给同一个父类的不同子类添加差异化能力
  • 给多个不同父类的子类添加同样的能力
  • 实现多态

参考文献:搞了这么多年终于知道接口和抽象类的应用场景了

同时在结构上,继承代表的是该子类本质是什么,而接口的实现代表的是子类能做什么

jdk8之后的接口内默认方法

jdk8之后增加了新特性,可以在接口内书写默认方法,默认方法可以有方法体,使用default修饰

public default 返回值 方法名( 参数 ){ }

默认方法要求

  • 默认方法不是抽象方法,不强制重写,但是如果重写需要去掉default关键字
  • public可以省略,但是default不能省略
  • 如果实现类实现了多个接口,多个接口内有重名方法,则实现类必须重写该方法

jdk8之后的接口内静态方法

格式和普通类内的静态方法一样使用static关键字

public static 返回值 方法名( 参数 ){ }

注意事项

  • 只能使用接口名调用,不能使用实现类名调用或者实现类对象调用
  • public可以省略,但是static不能省略

jdk9之后的接口内私有方法

接口内私有的方法一样使用private修饰

私有方法是为了将接口内默认方法或者静态方法中共同的代码抽取出来进一步封装,他不应该被接口以外感知到,因此将其变为私有

格式为private 返回值 方法名( 参数 ){ },和private static 返回值 方法名( 参数 ){ }

前者是为默认方法服务的,后者是为静态方法服务的

Day5—内部类和泛型

内部类应该是外部类的一部分,而且内部类单独出现没有意义

内部类可以访问外部类的私有属性和方法

外部类想使用内部类的属性和方法必须先创建内部类对象

成员内部类

成员内部类的书写位置在外部类里,和类的成员方法、成员变量同级,可以被修饰符修饰,访问权限和外部类被修饰的成员属性一样,如果外部类之外想要获取一个私有的成员内部类,可以使用一个公共方法来返回一个内部类的对象,外部可以使用Object类型接受

成员内部类可以使用外部类的成员变量和方法,包括静态和普通的

成员内部类里只能定义普通成员变量和方法,不能定义静态变量和方法,(JDK16以上可以定义

成员内部类的创建

因为成员内部类的等级与外部类的成员方法处于同级,因此成员内部类的定义格式就变成了外部类类名.内部类的类名 变量名 = new 外部类的类名().new 内部类的类名(),这本质上和必须先创建外部类对象才能使用外部类的属性是一样的

当然也可以不使用链式编写,可以先构造出外部类的对象,然后用外部类的对象构造内部类外部类类名.内部类的类名 变量名 = 外部类对象.new 内部类的类名()

如果成员内部类和外部类属性重名怎么办

此时属性的调用采取就近原则,当我想要使用外部类的属性时,可以书写成以下格式

外部类名.this.属性名

因为内部类有一个隐藏的指向外部类的外部类名.this

静态内部类

本质来说就是成员内部类加上了static修饰

静态内部类中可以定义普通成员变量和成员方法,也可以定义静态的成员变量和方法

静态内部类只能访问到外部类的静态变量和方法

创建静态内部类的对象的方法外部类类名.内部类 变量名 = new 外部类.内部类()

静态内部类没有默认的外部类名.this

局部内部类

书写在外部类的成员方法内的类叫做局部内部类,和局部变量是同级,不能被修饰符修饰

在局部内部类中不能定义静态变量

局部内部类可以访问外部类的普通成员,也可以访问静态成员

局部内部类可以访问所在成员方法里的局部变量,但要求局部变量必须为常量

外部无法直接使用局部内部类,需要在方法内创建对象

局部内部类的对象构造方式和普通类一样,但要求构造的位置必须在所在成员方法里

匿名类

之前的三种只需要了解语法能看懂代码即可,但匿名类使用的地方非常多,因此需要特别注意

概念

要么定义在类的方法中,要么定义在方法的参数中的没有名字的类叫做匿名内部类

作用

匿名类实际上最大的作用就是作为参数来传递的,只不过java中不允许函数作为参数,因此使用一个子类来重写父类/接口的方法然后创建子类对象并作为参数传递,匿名类就是将以上步骤合并在了一起,省去了单独创建一个有名字的子类来继承父类/接口的过程,其中子类重写的方法有且仅有一个

格式

匿名内部类经常和抽象父类以及接口共用,这里使用接口实现举例,父类继承是一样的,书写方式有三种

在使用前先定义一个接口/抽象父类来作为被继承或实现的类,其中定义一个方法

public interface Bjer {
    int bj(int t1, int t2);
}

然后定义一个类,这个类中有一个方法是接受两个参数和一个接口对象,在方法中合适的时机调用对象中重写的方法

public class Test04 {
    public int test(int a, int b, Bjer bjer) {
        return bjer.bj(a, b);
    }
}
  1. 第一种匿名内部类的书写方式是接口名 变量名 = new 接口名(){ 接口方法实现},因为我们不能使用接口来构造对象,因此这里实际上是存在一个接口的实现类,创建的是这个实现类的对象,只不过这个实现类没有名字,因此他就是匿名类,这样我们就将创建一个类实现接口、重写接口的方法和构造实现类的对象这三步合成了一步,然后就可以将这个匿名类构造的对象作为参数传递了

            Test04 t = new Test04()
            Bjer aa = new Bjer(){
                @Override
                public int bj(int a, int b) {
                    return b-a;
                }
            };
            System.out.println(t.test(10, 9, aa));
    

    我们可以发现这种写法类似于我们JavaScript中先定义一个匿名函数,然后将这个匿名函数作为回调函数一样

  2. 第二种写法,将变量声明也省去了,直接在参数列表里书写匿名类,步骤和方法一一样

            Test04 t = new Test04()
            int result = t.test(10, 9, new Bjer() {
                @Override
                public int bj(int a, int b) {
                    return a - b;
                }
            });
            System.out.println(result);
    

    我们可以发现这种写法就和我们JavaScript中最常用的回调函数的书写方式一样了

  3. 第三种写法,和JavaScript中箭头函数一样,将声明去掉,保留参数列表,然后将方法体和参数列表用箭头连接,只不过java里用->

    Test04 t = new Test04()
    t.test(10, 9, (int a, int b) -> a - b);
    

    和JavaScript一样,如果方法体只有一句代码可以省略大括号

lambda表达式

上述中匿名内部类的第三种写法就是lambda表达式

lambda表达式只能简化函数式接口的匿名内部类,也就是接口有且仅有一个抽象方法,才能使用lambda简化

  • 是接口
  • 有且仅有一个抽象方法

如果不确定可以在接口上加上@FunctionalInterface注解来判断是否符合,如果不报错则说明符合标准

lambda表达式可以进一步省略

  • 方法体只有一行时可以不写大括号,省略return,省略分号
  • 参数类型可以不写
  • 参数只有一个小括号可以不写

泛型

在实际使用中我想让一个方法的参数传递什么类型都可以就需要使用到泛型,我们可以看到java的ArrayList<>就支持泛型,他可以根据创建的时候提供的数据类型来变成相应的集合

泛型标识

泛型标识可以是任何字符,他只是用来区分普通数据类型和泛型,只要保证相应的泛型对得上就可以,java中有约定俗成的泛型标识

  • T :代表一般的任何类
  • E :代表 Element 元素的意思,或者 Exception 异常的意思
  • K :代表 Key 的意思
  • V :代表 Value 的意思,通常与 K 一起配合使用
  • S :代表 Subtype 的意思,文章后面部分会讲解示意。

泛型类

书写格式为

class 类名称<泛型标识>{
    public 泛型标识 变量名;
    public int 方法名(泛型标识 参数名){
        return 0;
    }
    public 泛型标识 方法名(int a){
    }
}

可以看到在类名后加上尖括号<>,然后在内部加上泛型标识就形成了泛型类,此时类中的成员就可以使用泛型标识来表示成员的数据类型待定

泛型类中有三处可以转化为泛型

  1. 可以将非静态方法的参数变成泛型
  2. 可以将非静态方法的返回值变成泛型
  3. 可以将非静态变量变成泛型

泛型类中待定的成员数据类型在构建对象的时候被声明,因此这也是为什么静态成员无法使用泛型的原因

泛型接口

和泛型类差不多

public interface 接口名<泛型标识> {
    泛型标识 方法名(泛型标识 参数名){
    }
}

泛型接口中成员变量因为默认是静态的因此无法转成泛型,但是方法的参数和返回值可以使用

泛型接口的数据类型在被实现的时候确定,也可以实现类延续泛型,在创建对象的时候再确定

泛型方法

泛型方法的书写格式为

public <泛型标识> 返回类型 方法名(泛型标识 变量名) {
    ...
}

泛型方法可以书写在泛型类中,并且可以混用泛型类的泛型标识

需要注意,在泛型类中写的方法不叫做泛型方法,泛型方法必须在修饰符后面有<泛型标识>

泛型方法中可以使用泛型的地方有返回值和参数

泛型方法的数据类型在被调用的时候确定,且编译器可以根据传入的参数来自动判断类型

通配符

? extends E,限定泛型只能是E或E的子类,不能是无关类,写入受限,读取统一变成E

? super E,限定泛型只能是E或E的父类,不能是无关类,写入不受限,读取受限

?,表示<? extends Object>

  • 通配符可以用在方法参数中或者返回值来限定传入的参数类型的泛型范围
  • 通配符不是在定义类的时候使用的,因为他并非确定的类型,和普通泛型不同

参考文献1

参考文献2

参考文献3

范例

将匿名类的案例完整的书写一遍

首先定义一个泛型接口,接口中定义了一个抽象方法,方法的参数转成跟随泛型接口

public interface Bjer<T> {
    int bj(T t1, T t2);
}

然后定义一个类,类里面有泛型方法,根据调用时传入的参数来确定数据类型,并且方法在适当的时候会调用传入的对象的方法实现

public class Test04 {
    public <T> int bj(T a, T b, Bjer<T> bjer) {
        return bjer.bj(a, b);
    }
}

然后定义一个学生类,作为数据的承载者

public class Student {
    private int id;
    private int age;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

    public Student() {
    }

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

最后是主函数

public class Test06 {
    public static void main(String[] args) {
        Test04 studentTest04 = new Test04();
        Student student1 = new Student(20, 18, "001");
        Student student2 = new Student(222, 100, "002");
        int result01 = studentTest04.bj(student1, student2, (a, b) -> a.getAge() - b.getAge());
        System.out.println(result01);
    }
}

此时调用Test04中的泛型方法时,泛型方法会自动根据传入的参数来确定三个参数的数据类型为Student,然后方法调用匿名类的方法同时将前两个参数传给他,此时泛型接口的数据类型已经在泛型方法那一步就被确认了