java核心技术卷1 第六章:接口、lambda表达式与内部类

接口

接口不是类,而是描述了符合这个接口的类应该做什么,描述了一组抽象的需求,而没有指定怎么做

  • 接口中的所有方法自动是public,接口中声明方法不需要加public(java规范,减少不必要的冗余声明,即使一些程序员为了清晰习惯加上public)

    实现接口时,需要加上public,不然默认将权限设为了defalut(包内可访问),这是缩小了可见性,编译器会报错

  • java8之前,接口中只能有抽象方法

  • 类实现接口关键字:implements。可以同时实现多个接口,不同接口用逗号隔开

Arrarys.sort(),可以对对象数组排序,只要对象实现了Comparable接口的compareTo方法。其中返回负值代表当前对象小于other,返回0代表相等,大于0代表大于,正值和负值的具体数值并不重要,所以这很灵活,比如比较int ID大小,可以直接return ID1-ID2

补充:如果担心整数相减溢出,可以调用Integer.compare(x,y)进行比较

Comparable文档建议,compareTo和equals方法相容,也就是conpareTo相等equals也应该相等,大部分JAVA API库遵循了这个建议,少部分没有,比如BigDecimal

补充为什么不能在对象中直接实现一个compareTo方法,而一定要实现comparable接口?

答:这样可以向编译器

保证,该类is-a comparable的类,一定实现了compareTo方法。这和java语言的性质有关,java是强类型,编译器要确定这个方法确实是存在的。


接口的属性

  • 接口不是类,不能实例化出一个对象。(new出一个实例存在堆中)
  • 可以定义一个接口类型的引用,指向一个实现了接口的类
  • 可以用instanceof判断一个对象是否实现了接口(is-a关系,对象实现了接口,属于接口的一个实例,属于is-a关系)
  • 接口也可以进行继承,从通用到专有

  • 接口中可以包含常量,字段自动成为public static final。(方法自动是public)

  • 接口可以是密封sealed,可以permits一些类来实现接口,或是定义实现类在同一个文件中

  • 类只能extends扩展一个类,但是可以实现多个接口。

    类可以实现多个接口,获得了c++多继承的大多数有点,同时避免了多继承带来的复杂性和低效性

  • java8中允许接口中定义static方法,这合法,但是似乎有些破坏了接口作为抽象规范的初衷,因此,通常的做法是再提供一个伴随类,这个类提供了接口内的static方法,来允许用户通过这个伴随类来调用静态方法,而不是通过接口调用。比如Path接口和Paths伴随类,伴随类加一个s后缀,称为使用工具类

  • 但是java之后的版本,如Java11,又在接口中提供了这些static方法的定义,伴随类似乎又可以不需要了

  • java9之后,接口中可以定义private方法,可以在接口内部使用,但用途有限,只是作为其他方法的辅助方法

默认实现

接口中的方法可以提供一个默认实现,加上default关键字前缀并增加实现即可。

这里的default直接写出来,就代表是接口中的默认实现方法,而其他地方的默认访问权限(类或方法前)的default不要写出来,写出来非法

  • 默认实现中可以调用其他抽象方法来实现自己的逻辑,当通过对象调用时,其中的方法调用的就是对应对象实现的方法

接口默认实现的另一个意义:接口演化。是的旧版本的源代码保持兼容,仍然可以顺利编译

比如一个接口增加了一个方法,如果不提供默认实现,原本实现这个接口的类还没有增加新方法的实现,就会编译报错,而如果增加了新的默认实现,那么实现类就继承了这个实现(作为实现类对对应方法的实现),而可以顺利通过编译

默认方法冲突

当一个类同时继承了父类和接口内的同名方法

解决:

  1. 父类的方法优先。调用子类的对应方法,调用的是父类的,接口内的方法被忽略

当一个类同时继承了两个接口的同名方法

解决:

  1. 必须重写实现这个接口(无论这两个接口是一个提供了默认实现还是两个都提供了)

  2. 如果接口内有实现的默认方法,可以用InterfaceName.super.Method()来调用对应的默认方法作为自己的实现。注意关键词

  3. 如果两个方法都没有默认实现,那么可以选择实现这个方法,也可以直接把这个类标记为abstract,选择不实现

类优先原则

当一个类同时继承了父类的和一个接口的同名方法,如果父类提供了实现,类优先下,相当于实现类用父类实现了接口。不过要注意父类的同名方法的访问权限要大于接口的。

补充:extends写在implement前面


接口与回调

回调(callback):常见的程序设计模式。指的是先指定一个特定事件发生时的动作(注册),但检测到事件发生时就执行这个动作。

比如AactionListener接口,actionPerformer中指定动作,每当特定事件发生就产生回调,执行这个动作

comparator接口

可以实现comparable接口来对对象sort排序,但是一些对象如String内部已经实现了comparable接口,按照字典序来比较大小,如果像按照其他方法来比较大小,如何实现?

Arrays.sort还有第二个版本,接收一个对象数组和一个实现了comparator接口的实例,第二个参数作为一个比较器。

  • 实现其中的compare方法,传入一个T表示比较对象的类型,通过返回值正负比较大小

对象克隆(不太常见,先简单了解)

当直接将不同对象赋值,是简单的改变了指针指向,指向了同一个内存区域,相互影响,如果想要两个独立的对象,就需要调用clone方法

clone

是Object的一个protected方法,只能在一个类的内部方法调用,在外部不能通过对象.clone()直接调用。

Object实现的clone方法:逐个拷贝当前对象的所有字段(因为它对对象的字段性质一无所知),是一种浅拷贝。对于基本数值类型,char,boolean可以正常拷贝,但是对于引用类型就是简单的指向了同一个地方,是一种不安全的共享。需要是的两个对象的对象引用指向不同的内存空间,进行安全的共享

重写clone,进行深拷贝

原本的Object中定义的protected方法,不能在外部通过object.clone()调用进行一个对象的拷贝,所以要重写此方法的访问权限为public

  1. 实现Cloneable接口
  2. 实现clone方法,改变访问权限为public

Cloneable接口:java中少数的标记接口,内不含任何方法(clone方法是从Object中继承的),其是用来确保一个类实现了特定的方法,唯一的作用是用来instanceof方法一个类是否是一个接口/父类的实例。如果不implement这个接口而进行clone,就会抛出一个异常:CloneNotSupportedException,当一个类试图重写clone而没有implement Cloneable接口就会抛出这个异常。

java1.4前,clone方法返回一个Object,而我们自己可以返回任意的类类型,这是协变的体现:重写的方法返回值只要是兼容的类型就可以(is-a)

重写了clone方法后,注意子类的调用

当父类重写了clone,变成了public clone,子类也可以调用clone,如果父类中调用了super.clone() (Object实现的clone方法),那么子类直接调用会简单的复制自己的字段,可能不能正确深拷贝自己的所有字段。

Clone方法很少出现,标准库中只有5%不到实现了clone方法

数组类型实现了public的clone方法,复制所有的元素副本,返回一个新数组


lambda表达式

lambda表达式:可以用来传递代码块,这个代码块在之后的时间内调用一次或多次。可以用lambda表达式来代替实现了函数式接口的对象

原始的java代码块传递方式:用一个实例对象实现接口的方法,然后把对象传递给接口引用。

lambda表达式形式

() -> {};

  • ():内部放置方法参数,即使方法没有参数,也要加上()括号,
  • 如果可以推断出一个方法的参数类型,就可以忽略参数类型,比如一个lambda表达式代替一个确定的接口的确定方法,则可以推断出这个方法参数类型必然是那个接口内方法对于的参数类型
  • 如果只有一个参数,且参数类型省略,那么可以不加()括号。经过试验,如果不省略类型,一个参数还是要加()
  • 无需指定lambda表达式的返回值类型,返回值类型可以自动推断,直接return即可
  • lambda表达式内的所有分支都要有一个return语句,不允许一些分支内有return,其他一些判断分支内无return

其他性质:

  • 如果函数体只有一条语句,{}可以省略
  • 如果只有一条语句就是return语句,那么return也必须省略

函数式接口

只有一个抽象方法的接口。

可以有多个实例方法,其他字段等,但是只能有一个抽象方法

如上一节所说的comparator比较器,就是一个只有一个抽象方法的函数式接口,可以传入一个lambda表达式来代替这个实现类对象。

也就是说,lambda表达式可以转换为接口

实际上,lambda表达式能做的也只有转换成函数式接口

补充:java中没有类似c++中的函数指针类型的函数类型

java.util.function包中定义了许多的函数式接口,可用来传递lambda接口,注册接口的行为,通过函数式接口对象调用。


方法引用

当lambda表达式只涉及一个方法调用,可重写为一个方法引用

方法引用:只是编译器生成一个函数式接口的实例,来调用给的实现方法

实际上的实现,不论是lambda表达式还是方法引用,都会为函数式接口生成一个对象,这个对象包含了接口方法的实现

共三种形式的方法引用

1:object::instanceMethod

用一个实际的实例对象加上::和方法。这个形式下的方法引用可以调用类中定义的非static方法,直接把lambda表达式的参数全部传给对应方法

2:Class::instanceMethod

用类名加上::方法名,这个形式下,类中定义的方法的第一个参数必须是该类类型的对象,后面是若干个参数

当调用时,比如

Interface inferface =(implObj,a,b)->{...}

此时,相等于调用了implObj.Method(a,b);

也就是说,用类名::非static方法名,还是要通过一个实例对象来调用。接口内方法的第一个参数需要是一个对象,然后通过Class::instanceMethod调用时,相当于调用第一个对象的类内的对于方法,这个方法的参数个数为n-1.就是没有接口方法中的对象参数剩下的参数构成的方法。

第一个参数称为隐式参数,其他参数传递到方法

3:Class::staticMethod

直接引用类内的静态方法,可以使用类名调用,直接把lambda中的参数全部传给了对应的静态方法

注意:只有lambda表达式只有一个方法调用时可以用方法引用,可以用方法引用代替lambda表达式,如果还有其他动作,比如方法引用之后还有其他操作,就不能使用方法调用

当Class::instanceMethod对应多个同名的重载方法是,会根据上下文和参数,选择出一个最佳的方法来进行接口中方法的实现

lambda表达式和方法引用都不会独立存在,都会在需要函数式接口对象的时候使用,转为一个函数式接口的对象

一些java API包含了专门用作方法引用的方法:提供了一些方法的实现,传递给需要函数式接口的地方,实现接口方法逻辑

lambda表达式和方法引用的一个细微差别:发现错误(报错)时间点:

object::method,如果object为null,立刻报错,而lambda表达式x->{method}会等待到运行时才会发现null错误

可以在方法引用中使用this和super

this::,和super::,均表示调用指定对象(当前类,父类)的方法来实现接口方法

构造器引用

当一个函数式接口的方法返回值是一个类的对象,就可以用构造器引用,直接返回一个该构造器构造出的对象。

构造器引用:需要在指定位置可以访问到构造函数(访问权限满足)

Class::new

会根据方法自动找到找到正确的构造函数。

如一个Interface Test{Dog getDog();}

可以用 Test test=Dog::new;

这样可以直接获得构造器构造出的实例对象作为返回值。

变量作用域

lambda表达式构成

  • 参数
  • 函数体
  • 自由变量(不是在lambda参数或函数体中定义的,外部嵌套块中的变量)、

lambda表达式可以使用外部变量,称为捕获。

和这个有关的实现细节:lambda表达式转换为一个有一个方法的对象,对象的实例字段保存了捕获的自由变量,从而可以在方法中使用。

一些限制:

  • 只能捕获事实最终变量(effective),也就是初始化后值不再改变的值
  • lambda中不可以修改捕获的自由变量。若可以修改,因为lambda的延迟执行,可能多个地方同时执行修改,这会带来并发问题,所以Java中不允许lambda对自由变量修改

lambda表达式和外部的嵌套块有相同的作用域,所以要注意命名冲突,比如参数名和外部的变量重名就是一种重定义。

lambda中可以使用this,就是对应的那个外部类的对象

思考:

因为相同的作用域,所以可以直接使用外部的变量和this,但是有一些限制:只能使用(捕获)事实最终变量,不能对自由变量修改。

处理lambda表达式

lambda的特点就是延迟执行,只先注册以下方法,然后在某个特定的时间点,特定的事件发生后再调用。

java API中提供了很多的函数式表达式,用于存储不同参数,不同返回值类型的方法,用于之后创建接口对象调用方法。

其中的一些接口,

  • Runable,无参数和返回值,只执行一个动作
  • Supplier:无参数,返回(提供)一个类型值
  • Consumer:消耗(接收)一个类型值,无返回值
  • Predicate:接收一个类型的参数进行test,返回true/false

还有一些特殊化的接口,而不像上面的通用泛型接口,特殊化的比通用接口更高效(第8章解释)

大多数标准函数式接口还提供了非抽象方法(接口中的其他方法只能是static,实例接口无法定义,因为无法实例化对象,实例方法也就无法调用)

设计函数式接口:使用注解@FunctionalInterface,若错误定义了函数式接口,编译器会给出警告。

不是必须,但是推荐

内部类

定义在一个类中的类

  • 可以访问其嵌套类的所有成员,包括private字段,带来了灵活性。(内部类会有一个隐式的引用,指向外部类的对象,通过这个引用访问到外部类的全部字段)
  • static 修饰的内部类无外部类的引用,无法访问嵌套类的字段,只是个作用域更小的类

声明在一个类的内部,并不意味着每个外部类对象都有一个内部类的实例,内部类的实例需要在外部类的方法中进行构造

一些性质

  • 内部类可以声明为private和protected,就像外部类的其他成员一样,声明为private就代表只有这个外部类内可以访问这个类。而其他的普通类的权限之呢个是public或者默认
  • 外部创建一个内部类(如果访问权限允许),外部类.new InnerClass
  • 外部类引用:OutClass.this:表示外部类。如果有同名,可以用此方法引用到外部类的成员
  • 内部类声明的所有static字段都必须是final的
  • 内部类不能有static方法。(直观的想,可以有只能访问外部类static成员的static内部类,但是java设计者没有允许,并且没有其他更多解释)

编译后,内部类转为一个常规的.class文件,名字为OutClass$InnerClass.class,$隔开外部类与内部类名

局部内部类

将内部类声明在一个方法内部,这样进一步减小了可访问区域,只有这个方法可以,其他的类方法都不知道这个类的存在

一些性质

  • 不可以声明访问权限,private,public等。这是作为内部类成员时可以访问的,也是作为普通类的访问权限可以设置的,但是在方法内不可以设置,就只能省略,在这个方法中使用
  • 局部内部类可以访问这个方法的变量,类似lambda,有相同的作用域,且可访问的变量必须是effective final.事实最终变量

匿名内部类

(暂时理解为一个匿名类,不清楚为什么叫内部类)

只需要一个类的对象,而不需要多次创建这个类的对象,就不需要声明类的名字

语法:

new SuperType(constructor parameter){
    inner class methods and data
}

其中,由于没有类名,所以没有构造方法(构造方法名和类名同),可以用一个初始化块{}来初始化参数

一些性质

  • 如果是扩展接口,那么就是返回一个实现了这个接口方法的对象
  • 如果是扩展类,那么就是返回一个扩展了这个类的子类

可以用匿名内部类实现事件监听器(接口)和其他回调,比先定义一个类实现方法然后传递对象简洁,但是现在用lambda表达式更简单

静态内部类

static修饰的内部类,无法访问到外部类的成员。

只有内部类可以声明为static:具有类成员的性质。其他的普通类不可以,只能是public和省略的包可见,而private,protected,static都是只有内部类可以声明

一些性质:

  • 与常规内部类不同,static内部类可以有static字段和static方法(常规内部类static字段必须是final,不能有static方法)
  • 接口中声明的内部类自动是static和public(public:基本性质,static:接口无实例,非static类就无法访问了,需要是static然后用接口名访问)# 接口

接口不是类,而是描述了符合这个接口的类应该做什么,描述了一组抽象的需求,而没有指定怎么做

  • 接口中的所有方法自动是public,接口中声明方法不需要加public(java规范,减少不必要的冗余声明,即使一些程序员为了清晰习惯加上public)

    实现接口时,需要加上public,不然默认将权限设为了defalut(包内可访问),这是缩小了可见性,编译器会报错

  • java8之前,接口中只能有抽象方法

  • 类实现接口关键字:implements。可以同时实现多个接口,不同接口用逗号隔开

Arrarys.sort(),可以对对象数组排序,只要对象实现了Comparable接口的compareTo方法。其中返回负值代表当前对象小于other,返回0代表相等,大于0代表大于,正值和负值的具体数值并不重要,所以这很灵活,比如比较int ID大小,可以直接return ID1-ID2

补充:如果担心整数相减溢出,可以调用Integer.compare(x,y)进行比较

Comparable文档建议,compareTo和equals方法相容,也就是conpareTo相等equals也应该相等,大部分JAVA API库遵循了这个建议,少部分没有,比如BigDecimal

补充为什么不能在对象中直接实现一个compareTo方法,而一定要实现comparable接口?

答:这样可以向编译器

保证,该类is-a comparable的类,一定实现了compareTo方法。这和java语言的性质有关,java是强类型,编译器要确定这个方法确实是存在的。


接口的属性

  • 接口不是类,不能实例化出一个对象。(new出一个实例存在堆中)
  • 可以定义一个接口类型的引用,指向一个实现了接口的类
  • 可以用instanceof判断一个对象是否实现了接口(is-a关系,对象实现了接口,属于接口的一个实例,属于is-a关系)
  • 接口也可以进行继承,从通用到专有

![image-20240318194804017](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240318194804017.png)

  • 接口中可以包含常量,字段自动成为public static final。(方法自动是public)

  • 接口可以是密封sealed,可以permits一些类来实现接口,或是定义实现类在同一个文件中

  • 类只能extends扩展一个类,但是可以实现多个接口。

    类可以实现多个接口,获得了c++多继承的大多数有点,同时避免了多继承带来的复杂性和低效性

  • java8中允许接口中定义static方法,这合法,但是似乎有些破坏了接口作为抽象规范的初衷,因此,通常的做法是再提供一个伴随类,这个类提供了接口内的static方法,来允许用户通过这个伴随类来调用静态方法,而不是通过接口调用。比如Path接口和Paths伴随类,伴随类加一个s后缀,称为使用工具类

  • 但是java之后的版本,如Java11,又在接口中提供了这些static方法的定义,伴随类似乎又可以不需要了

  • java9之后,接口中可以定义private方法,可以在接口内部使用,但用途有限,只是作为其他方法的辅助方法

默认实现

接口中的方法可以提供一个默认实现,加上default关键字前缀并增加实现即可。

这里的default直接写出来,就代表是接口中的默认实现方法,而其他地方的默认访问权限(类或方法前)的default不要写出来,写出来非法

  • 默认实现中可以调用其他抽象方法来实现自己的逻辑,当通过对象调用时,其中的方法调用的就是对应对象实现的方法
  • ![image-20240318195932853](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240318195932853.png)

接口默认实现的另一个意义:接口演化。是的旧版本的源代码保持兼容,仍然可以顺利编译

比如一个接口增加了一个方法,如果不提供默认实现,原本实现这个接口的类还没有增加新方法的实现,就会编译报错,而如果增加了新的默认实现,那么实现类就继承了这个实现(作为实现类对对应方法的实现),而可以顺利通过编译

默认方法冲突

当一个类同时继承了父类和接口内的同名方法

解决:

  1. 父类的方法优先。调用子类的对应方法,调用的是父类的,接口内的方法被忽略

当一个类同时继承了两个接口的同名方法

解决:

  1. 必须重写实现这个接口(无论这两个接口是一个提供了默认实现还是两个都提供了)
  2. 如果接口内有实现的默认方法,可以用InterfaceName.super.Method()来调用对应的默认方法作为自己的实现。注意关键词
  3. ![image-20240318200635720](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240318200635720.png)
  4. 如果两个方法都没有默认实现,那么可以选择实现这个方法,也可以直接把这个类标记为abstract,选择不实现

类优先原则

当一个类同时继承了父类的和一个接口的同名方法,如果父类提供了实现,类优先下,相当于实现类用父类实现了接口。不过要注意父类的同名方法的访问权限要大于接口的。

补充:extends写在implement前面


接口与回调

回调(callback):常见的程序设计模式。指的是先指定一个特定事件发生时的动作(注册),但检测到事件发生时就执行这个动作。

比如AactionListener接口,actionPerformer中指定动作,每当特定事件发生就产生回调,执行这个动作

comparator接口

可以实现comparable接口来对对象sort排序,但是一些对象如String内部已经实现了comparable接口,按照字典序来比较大小,如果像按照其他方法来比较大小,如何实现?

Arrays.sort还有第二个版本,接收一个对象数组和一个实现了comparator接口的实例,第二个参数作为一个比较器。

  • 实现其中的compare方法,传入一个T表示比较对象的类型,通过返回值正负比较大小

![image-20240318202228592](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240318202228592.png)

对象克隆(不太常见,先简单了解)

当直接将不同对象赋值,是简单的改变了指针指向,指向了同一个内存区域,相互影响,如果想要两个独立的对象,就需要调用clone方法

clone

是Object的一个protected方法,只能在一个类的内部方法调用,在外部不能通过对象.clone()直接调用。

Object实现的clone方法:逐个拷贝当前对象的所有字段(因为它对对象的字段性质一无所知),是一种浅拷贝。对于基本数值类型,char,boolean可以正常拷贝,但是对于引用类型就是简单的指向了同一个地方,是一种不安全的共享。需要是的两个对象的对象引用指向不同的内存空间,进行安全的共享

重写clone,进行深拷贝

原本的Object中定义的protected方法,不能在外部通过object.clone()调用进行一个对象的拷贝,所以要重写此方法的访问权限为public

  1. 实现Cloneable接口
  2. 实现clone方法,改变访问权限为public

Cloneable接口:java中少数的标记接口,内不含任何方法(clone方法是从Object中继承的),其是用来确保一个类实现了特定的方法,唯一的作用是用来instanceof方法一个类是否是一个接口/父类的实例。如果不implement这个接口而进行clone,就会抛出一个异常:CloneNotSupportedException,当一个类试图重写clone而没有implement Cloneable接口就会抛出这个异常。

java1.4前,clone方法返回一个Object,而我们自己可以返回任意的类类型,这是协变的体现:重写的方法返回值只要是兼容的类型就可以(is-a)

重写了clone方法后,注意子类的调用

当父类重写了clone,变成了public clone,子类也可以调用clone,如果父类中调用了super.clone() (Object实现的clone方法),那么子类直接调用会简单的复制自己的字段,可能不能正确深拷贝自己的所有字段。

Clone方法很少出现,标准库中只有5%不到实现了clone方法

数组类型实现了public的clone方法,复制所有的元素副本,返回一个新数组


lambda表达式

lambda表达式:可以用来传递代码块,这个代码块在之后的时间内调用一次或多次。可以用lambda表达式来代替实现了函数式接口的对象

原始的java代码块传递方式:用一个实例对象实现接口的方法,然后把对象传递给接口引用。

lambda表达式形式

() -> {};

  • ():内部放置方法参数,即使方法没有参数,也要加上()括号,
  • 如果可以推断出一个方法的参数类型,就可以忽略参数类型,比如一个lambda表达式代替一个确定的接口的确定方法,则可以推断出这个方法参数类型必然是那个接口内方法对于的参数类型
  • 如果只有一个参数,且参数类型省略,那么可以不加()括号。经过试验,如果不省略类型,一个参数还是要加()
  • 无需指定lambda表达式的返回值类型,返回值类型可以自动推断,直接return即可
  • lambda表达式内的所有分支都要有一个return语句,不允许一些分支内有return,其他一些判断分支内无return

其他性质:

  • 如果函数体只有一条语句,{}可以省略
  • 如果只有一条语句就是return语句,那么return也必须省略

函数式接口

只有一个抽象方法的接口。

可以有多个实例方法,其他字段等,但是只能有一个抽象方法

如上一节所说的comparator比较器,就是一个只有一个抽象方法的函数式接口,可以传入一个lambda表达式来代替这个实现类对象。

也就是说,lambda表达式可以转换为接口

实际上,lambda表达式能做的也只有转换成函数式接口

补充:java中没有类似c++中的函数指针类型的函数类型

java.util.function包中定义了许多的函数式接口,可用来传递lambda接口,注册接口的行为,通过函数式接口对象调用。

![image-20240318205926150](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240318205926150.png)

![image-20240318205946180](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240318205946180.png)


方法引用

当lambda表达式只涉及一个方法调用,可重写为一个方法引用

方法引用:只是编译器生成一个函数式接口的实例,来调用给的实现方法

![image-20240318210311621](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240318210311621.png)

实际上的实现,不论是lambda表达式还是方法引用,都会为函数式接口生成一个对象,这个对象包含了接口方法的实现

共三种形式的方法引用

1:object::instanceMethod

用一个实际的实例对象加上::和方法。这个形式下的方法引用可以调用类中定义的非static方法,直接把lambda表达式的参数全部传给对应方法

2:Class::instanceMethod

用类名加上::方法名,这个形式下,类中定义的方法的第一个参数必须是该类类型的对象,后面是若干个参数

当调用时,比如

Interface inferface =(implObj,a,b)->{...}

此时,相等于调用了implObj.Method(a,b);

也就是说,用类名::非static方法名,还是要通过一个实例对象来调用。接口内方法的第一个参数需要是一个对象,然后通过Class::instanceMethod调用时,相当于调用第一个对象的类内的对于方法,这个方法的参数个数为n-1.就是没有接口方法中的对象参数剩下的参数构成的方法。

第一个参数称为隐式参数,其他参数传递到方法

3:Class::staticMethod

直接引用类内的静态方法,可以使用类名调用,直接把lambda中的参数全部传给了对应的静态方法

注意:只有lambda表达式只有一个方法调用时可以用方法引用,可以用方法引用代替lambda表达式,如果还有其他动作,比如方法引用之后还有其他操作,就不能使用方法调用

![image-20240318212318122](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240318212318122.png)

当Class::instanceMethod对应多个同名的重载方法是,会根据上下文和参数,选择出一个最佳的方法来进行接口中方法的实现

lambda表达式和方法引用都不会独立存在,都会在需要函数式接口对象的时候使用,转为一个函数式接口的对象

一些java API包含了专门用作方法引用的方法:提供了一些方法的实现,传递给需要函数式接口的地方,实现接口方法逻辑

![image-20240318212641339](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240318212641339.png)

lambda表达式和方法引用的一个细微差别:发现错误(报错)时间点:

object::method,如果object为null,立刻报错,而lambda表达式x->{method}会等待到运行时才会发现null错误

可以在方法引用中使用this和super

this::,和super::,均表示调用指定对象(当前类,父类)的方法来实现接口方法

构造器引用

当一个函数式接口的方法返回值是一个类的对象,就可以用构造器引用,直接返回一个该构造器构造出的对象。

构造器引用:需要在指定位置可以访问到构造函数(访问权限满足)

Class::new

会根据方法自动找到找到正确的构造函数。

如一个Interface Test{Dog getDog();}

可以用 Test test=Dog::new;

这样可以直接获得构造器构造出的实例对象作为返回值。

变量作用域

lambda表达式构成

  • 参数
  • 函数体
  • 自由变量(不是在lambda参数或函数体中定义的,外部嵌套块中的变量)、

lambda表达式可以使用外部变量,称为捕获。

和这个有关的实现细节:lambda表达式转换为一个有一个方法的对象,对象的实例字段保存了捕获的自由变量,从而可以在方法中使用。

一些限制:

  • 只能捕获事实最终变量(effective),也就是初始化后值不再改变的值
  • lambda中不可以修改捕获的自由变量。若可以修改,因为lambda的延迟执行,可能多个地方同时执行修改,这会带来并发问题,所以Java中不允许lambda对自由变量修改

lambda表达式和外部的嵌套块有相同的作用域,所以要注意命名冲突,比如参数名和外部的变量重名就是一种重定义。

lambda中可以使用this,就是对应的那个外部类的对象

思考:

因为相同的作用域,所以可以直接使用外部的变量和this,但是有一些限制:只能使用(捕获)事实最终变量,不能对自由变量修改。

处理lambda表达式

lambda的特点就是延迟执行,只先注册以下方法,然后在某个特定的时间点,特定的事件发生后再调用。

java API中提供了很多的函数式表达式,用于存储不同参数,不同返回值类型的方法,用于之后创建接口对象调用方法。

![image-20240319140343766](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240319140343766.png)

其中的一些接口,

  • Runable,无参数和返回值,只执行一个动作
  • Supplier:无参数,返回(提供)一个类型值
  • Consumer:消耗(接收)一个类型值,无返回值
  • Predicate:接收一个类型的参数进行test,返回true/false

还有一些特殊化的接口,而不像上面的通用泛型接口,特殊化的比通用接口更高效(第8章解释)

![image-20240319140702463](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240319140702463.png)

大多数标准函数式接口还提供了非抽象方法(接口中的其他方法只能是static,实例接口无法定义,因为无法实例化对象,实例方法也就无法调用)

![image-20240319140825411](C:\Users\cps\Desktop\学习\java\java学习博客\java核心技术卷1 12版\images\image-20240319140825411.png)

设计函数式接口:使用注解@FunctionalInterface,若错误定义了函数式接口,编译器会给出警告。

不是必须,但是推荐

内部类

定义在一个类中的类

  • 可以访问其嵌套类的所有成员,包括private字段,带来了灵活性。(内部类会有一个隐式的引用,指向外部类的对象,通过这个引用访问到外部类的全部字段)
  • static 修饰的内部类无外部类的引用,无法访问嵌套类的字段,只是个作用域更小的类

声明在一个类的内部,并不意味着每个外部类对象都有一个内部类的实例,内部类的实例需要在外部类的方法中进行构造

一些性质

  • 内部类可以声明为private和protected,就像外部类的其他成员一样,声明为private就代表只有这个外部类内可以访问这个类。而其他的普通类的权限之呢个是public或者默认
  • 外部创建一个内部类(如果访问权限允许),外部类.new InnerClass
  • 外部类引用:OutClass.this:表示外部类。如果有同名,可以用此方法引用到外部类的成员
  • 内部类声明的所有static字段都必须是final的
  • 内部类不能有static方法。(直观的想,可以有只能访问外部类static成员的static内部类,但是java设计者没有允许,并且没有其他更多解释)

编译后,内部类转为一个常规的.class文件,名字为OutClass$InnerClass.class,$隔开外部类与内部类名

局部内部类

将内部类声明在一个方法内部,这样进一步减小了可访问区域,只有这个方法可以,其他的类方法都不知道这个类的存在

一些性质

  • 不可以声明访问权限,private,public等。这是作为内部类成员时可以访问的,也是作为普通类的访问权限可以设置的,但是在方法内不可以设置,就只能省略,在这个方法中使用
  • 局部内部类可以访问这个方法的变量,类似lambda,有相同的作用域,且可访问的变量必须是effective final.事实最终变量

匿名内部类

(暂时理解为一个匿名类,不清楚为什么叫内部类)

只需要一个类的对象,而不需要多次创建这个类的对象,就不需要声明类的名字

语法:

new SuperType(constructor parameter){
    inner class methods and data
}

其中,由于没有类名,所以没有构造方法(构造方法名和类名同),可以用一个初始化块{}来初始化参数

一些性质

  • 如果是扩展接口,那么就是返回一个实现了这个接口方法的对象
  • 如果是扩展类,那么就是返回一个扩展了这个类的子类

可以用匿名内部类实现事件监听器(接口)和其他回调,比先定义一个类实现方法然后传递对象简洁,但是现在用lambda表达式更简单

静态内部类

static修饰的内部类,无法访问到外部类的成员。

只有内部类可以声明为static:具有类成员的性质。其他的普通类不可以,只能是public和省略的包可见,而private,protected,static都是只有内部类可以声明

一些性质:

  • 与常规内部类不同,static内部类可以有static字段和static方法(常规内部类static字段必须是final,不能有static方法)
  • 接口中声明的内部类自动是static和public(public:基本性质,static:接口无实例,非static类就无法访问了,需要是static然后用接口名访问)
posted @ 2024-03-19 15:07  shueykkk  阅读(3)  评论(0编辑  收藏  举报