█ 六. 接口, 内部类, lambda

一. 接口

1. 基本概念

Interface

用来描述一个类具备某些特定的功能, 但不提供具体的实现: 定义了一些方法名, 但没有方法体

接口中定义的方法, 都是public方法, 无须使用 public 关键字来修饰.

 

2. 静态方法

3. 默认方法

4. 接口回调

 

二. 函数式接口

1. 什么是函数式接口

在 Java 中, 不能直接传递一个代码块, 必须先构造一个对象, 这个对象的类中含有一个包含所需代码的方法.

为了实现这种"传递代码块"的需求, java 8 定义了一种特殊的接口, 接口中有且只有一个抽象方法,

当一个方法以函数式接口作为形参时, 在调用这个方法时就可以以 lambda 表达式作为实参, 这个 lambda 表达式的实参将会成为这个函数式接口中那唯一一个抽象方法的方法体实现.

java 8 以前的函数式接口:

2. Java 8 的函数式接口

java 8 中提供了一个 java.util.function 包, 里面有各种形式的函数式接口, 方便开发人员使用.

根据提供的方法类型, 可以分成以下几类, 并根据参数和返回值的具体类型进行了扩展

2.1. Consumer

消费型接口, 需要传入参数, 没有返回值

接口类型接口方法描述
Consumer<T>void accept(T t);接受 T 类型参数, 无返回值
IntConsumervoid accept(int value);接受 int 类型参数, 无返回值
LongConsumervoid accept(long value);接受 long 参数, 无返回值
DoubleConsumervoid accept(double value);接受 double 参数, 无返回值
BiConsumer<T,U>void accept(T t, U u);接受 TU 两种类型参数, 无返回值
ObjIntConsumer<T>void accept(T t, int value);接受 Tint 参数, 无返回值
ObjLongConsumer<T>void accept(T t, long value);接受 Tlong参数, 无返回值
ObjDoubleConsumer<T>void accept(T t, double value);接受 Tdouble参数, 无返回值

2.2. Supplier

供给型接口, 不需传入参数, 提供一个指定类型返回值

接口名接口方法描述
Supplier<T>T get();获取一个 T 类型返回值
IntSupplierint getAsInt();获取一个 int 类型返回值
LongSupplierlong getAsLong();获取一个 long 类型返回值
DoubleSupplierdouble getAsDouble();获取一个 double 类型返回值
BooleanSupplierboolean getAsBoolean();获取一个 boolean 类型返回值

2.3. Predicate

断言型接口, 根据传入参数, 返回一个 boolean

接口名接口方法描述
Predicate<T>boolean test(T t);根据 T 类型参数, 得到 boolean 结果
IntPredicateboolean test(int value);根据 int 类型参数, 得到 boolean 结果
LongPredicateboolean test(long value);根据 long 类型参数, 得到 boolean 结果
DoublePredicateboolean test(double value);根据 double 类型参数, 得到 boolean 结果
BiPredicate<T,U>boolean test(T t, U u);根据 TU 两个参数, 得到 boolean 结果

2.4. Function

函数型接口, 根据传入参数, 返回指定类型结果,

注意 Function 接口 参数类型返回值类型不同, 如需 参数类型返回值类型 相同的函数, 请使用 Operator 接口

1) 一元函数, 单参数

接口名接口方法描述
Function<T,R>R apply(T t);根据 T 类型参数, 得到 R 类型结果
IntFunction<R>R apply(int value);根据 int 类型参数, 得到 R 类型结果
LongFunction<R>R apply(long value);根据 long 类型参数, 得到 R 类型结果
DoubleFunction<R>R apply(double value);根据 double 类型参数, 得到 R 类型结果
ToIntFunction<T>int applyAsInt(T value);根据 T 类型参数, 得到 int 类型结果
LongToIntFunctionint applyAsInt(long value);根据 long 类型参数, 得到 int 类型结果
DoubleToIntFunctionint applyAsInt(double value);根据 double 类型参数, 得到 int 类型结果
ToLongFunction<T>long applyAsLong(T value);根据 T 类型参数, 得到 long 类型结果
IntToLongFunctionlong applyAsLong(int value);根据 int 类型参数, 得到 long 类型结果
DoubleToLongFunctionlong applyAsLong(double value);根据 double 类型参数, 得到 long 类型结果
ToDoubleFunction<T>double applyAsDouble(T value);根据 T 类型参数, 得到 double 类型结果
IntToDoubleFunctiondouble applyAsDouble(int value);根据 int 类型参数, 得到 double 类型结果
LongToDoubleFunctiondouble applyAsDouble(long value);根据 long 类型参数, 得到 double 类型结果

2) 二元函数, 双参数

接口名接口方法描述
BiFunction<T,U,R>R apply(T t, U u);根据 TU 参数, 获取 R 类型结果
ToIntBiFunction<T,U>int applyAsInt(T t, U u);根据 TU 参数, 获取 int 类型结果
ToLongBiFunction<T,U>long applyAsLong(T t, U u);根据 TU 参数, 获取 long 类型结果
ToDoubleBiFunction<T,U>double applyAsDouble(T t, U u);根据 TU 参数, 获取 double 类型结果

2.5. Operator

算子型接口, 效果类似于运算符. 根据参数得到结果,

与 Function 接口相似, 区别在于 Operator 的 参数类型返回值类型 要保持一致

接口名接口方法描述
UnaryOperator<T>T apply(T t);继承Function<T,T>, 根据单参数得到结果
IntUnaryOperatorint applyAsInt(int operand);根据 int 类型参数, 得到 int 类型结果
LongUnaryOperatorlong applyAsLong(long operand);根据 long 类型参数, 得到 long 类型结果
DoubleUnaryOperatordouble applyAsDouble(double operand);根据 double 类型参数, 得到 double 类型结果
BinaryOperator<T>T apply(T t1, T t2);继承 BiFunction<T,T,T>, 根据双参数得到结果
IntBinaryOperatorint applyAsInt(int left, int right);根据两个 int 类型参数, 得到 int 类型结果
LongBinaryOperatorlong applyAsLong(long left, long right);根据两个 long 类型参数, 得到 long 类型结果
DoubleBinaryOperatordouble applyAsDouble(double left, double right);根据两个 double 类型参数, 得到 double 类型结果

3. 使用函数式接口

3.1. 开发函数式接口

函数式接口的关键就是: 有且只有一个抽象方法, 只要满足这个要求, 就是一个函数式接口.

java8 中提供了 单参数单返回值的 Function<T,R> 和 双参数单返回值的 BiFunction<T,U,R>,

下面展示的则是经过扩展的 三参数单返回值的函数式接口:

代码中的 @FunctionalInterface 表明这个接口是一个函数式接口, 这个注解不是必须的, 但建议加上.

使用 @FunctionalInterface 的作用有两个:

  1. 检查接口中的抽象接口, 确保有且只有一个抽象方法, 否则会提示编译错误

  2. 在生成的javadoc中指示这个接口为函数式接口

    1536819191835

3.2. 以函数式接口为方法形参

定义一些方法, 这些方法使用函数式接口作为形参, 这样的方法才能使用 lambda 表达式或方法引用作为实参进行传递.

接受函数式接口参数后, 在方法体内调用该函数式接口中定义的抽象方法, 日后使用 lambda 表达式或方法引用时, 就会被执行相应的方法体.

后续案例都会使用这种形式的方法来接收 lambda 表达式.

3.3. 以接口对象为方法实参

定义好函数式接口和接收函数式接口参数的方法后, 就可以在后面的工作中调用 3.2. 中定义的方法, 传入 lambda 表达式作为实参.

三. 内部类

1. 内部类

内部类 inner class 是定义在另一个类中的类, 使用内部类可能有以下原因:

  1. 内部类方法可以访问该类定义所在的作用与中的数据, 包括私有的数据
  2. 内部类可以对同一个包中的其他类隐藏起来, 对其他类不可见
  3. 当想要定义一个回调函数且不想编写大量格式代码时, 使用匿名内部类比较方便

而且 Java中的 内部类对象持有一个隐式的引用 Outer.this, 它引用了实例化该内部对象的外围类对象, 通过这个指针, 可以访问到外围类对象的全部状态, 而 static 修饰的静态内部类没有这样的指针

start() 方法内打断点, 观察对象情况:

由此可见, 内部类对象中都自动含有一个创建它的外围类对象的引用. 且不论内部类对象是否访问了外围类对象的数据域, 都会含有这个引用 ( 可以将内部类 run() 方法中访问 brandmodel 的语句注释后观察)

1536907526293

 

内部类对象对外围类对象的引用, 不是由开发者自己定义的, 因此不需要在内部类的源码中进行定义.

这个引用是在构造器中设置的, 编译器修改了所有内部类的结构, 添加了一个外围类引用的参数 this$0 .

上述例子中的内部类经编译器修改后相当于这样的, 添加了一个 this$0 实例域并修改了构造器:

1.2. 访问控制

1) 内部类访问外围类

再看上面的例子, 如果 Engine 类是常规类而不是内部类, 那么要想在 run() 方法访问 Car 的私有域, 就需要在 Car 类中提供私有域的 Getter 方法.

而如果 EngineCar 的内部类, 则可以直接访问 Car 的私有域, 不需要通过 Getter 方法

2) 外围类访问内部类

常规类的访问控制只能是 package(默认)public但对于内部类, 可以为 privateprotected.

对于 private 内部类中的 private 实例域/方法, private 并未限制外围类去访问, 不知道具体什么原理

编译器为外围类的实例域添加了访问方法, 编译后的内部类通过这些访问方法访问到外围类的私有域.

外部类对象可以访问内部类中 private 的实例域和方法

2. 特殊语法规则

2.1. 获取外围类对象

前面已经讲到, 内部类对象中含有一个外围类对象的引用, 并用 outer 进行描述. 但实际上, 如果要在内部中获取外围类对象, 需要采用以下的形式, 并返回制定类型的对象

外部类名.this

2.2. 创建内部类对象

2.3.

静态域必须为 final, 即 static final 类常量, 声明的同时赋值, 不可改变

不能有 static 方法/ 或静态方法只能访问外围类的静态域

3. 其他内部类

 

2. 局部内部类

在方法之中定义的内部类, 称为局部内部类.

局部内部类不能使用 public, protected, private 等访问控制符修饰, 因为局部内部类的作用域被限制在这个方法的方法体中, 对外界安全隐藏, 因此也不能以局部内部类作为返回值类型返回.

可以将局部内部类继承某一个类, 并以超类类型作为返回值, 从而将该局部内部类的对象带到外面.

局部内部内不仅可以访问包含它的外围类对象, 还可以访问方法中的局部变量, 但要求方法中的局部变量为final

3. 匿名内部类

将局部内部类更深入一步, 如果只需要这个类的一个对象, 则不需要为类命名了, 这种类被称为匿名内部类

由于构造器的名字与类名相同, 而匿名内部类没有类名, 自然也就没有构造函数了. 取而代之的是可以将构造参数传递给超类的构造器,

超类 对象名 = new 超类(构造参数){...}

如果是实现接口的匿名内部类, 则不需传入任何的构造参数, 但仍需保留空的小括号

接口 对象名 = new 接口(){...}

对比一下创建 普通类 对象, 以及创建一个 扩展超类的匿名内部类 的对象, 可以发现创建匿名内部类时, 在构造方法后面带有一个大括号方法体, 这个方法体就是匿名内部类的实现.

注意匿名内部类对外都是以超类类型存在, 因此, 匿名内部类对象能访问的方法都必须是超类中定义的方法.

如果在匿名内部类中新增一个公有方法定义, 则匿名内部类之外对此方法一无所知, 因而也无法调用, 该公有方法没有意义. 但仍可以定义私有方法, 在匿名内部类之中进行调用.

3.2. 双括号初始化

如果需要构造一个数组或者列表, 并将其作为参数传递, 此后就不再需要这个数组列表, 那么可以考虑将其作为一个匿名列表.

那作为一个匿名列表, 要如何为它添加元素呢? 这种情况下可以采用匿名内部类, 通过 "双括号初始化" 的方法为其添加元素. 例子如下

注意第二种方式的双层大括号, 其中外层大括号是匿名内部类的类级大括号, 内层括号则是初始化块, 初始化块将在构造内部类的对象时执行, 通过这种方式就可以将元素添加到匿名列表之中.

通过传统方式创建的列表对象, 以及双括号初始化创建的列表对象, 还有一个区别在于对象的真实类型, 传统方式列表对象的类型为 ArrayList, 但双括号初始化得到的对象是 ArrayList 的一个子类, 如果需要对多个列表对象调用 equals 方法进行比较, 则会因为类型不同而得到 false 的结果

3.3. 在静态方法中输出当前类

在生成日志或者调试信息时, 可能会希望在日志中记录当前类的类型, 为此可能会调用 getClass() 方法

但是 getClass() 方法是个实例方法, 在静态方法中不能使用, 为此, 也可以通过匿名内部类来获取当前类的类型.

在上面的代码中, new Object(){} 会创建 Object 的一个匿名子类对象, 而 getEnclosingClass() 方法则会得到其外围类, 即包含这个静态方法的类.

4. 静态内部类

在前面学习到的内部类中, 如果是在内部类是在非静态方法中创建的, 那么该内部类对象就会持有一个当前外围类对象的引用, 以便内部类访问外围类对象的数据域. 但有时候可能可能只是想将一个类隐藏在另一个类里面, 并不需要内部类引用外围类, 为此, 可以将内部类声明为 static, 作为静态内部类从而消除内部类对外围类的引用.

请看下面的例子:

1537100940532

从上述例子中可以看到:

4.1. 静态域和静态方法

非静态内部类 ( 包括普通内部类, 局部内部类, 匿名内部类 ) 不能有 static 修饰的静态方法, 且静态域必须为 final.

静态内部类中可以像普通类一样声明静态方法, 且静态域不要求 final

在接口 interface 中定义的内部类, 都自动成为 staticpublic 的, 其中可以包含静态域和静态方法

5. 捕获局部变量

前面说到, 局部内部类 和 匿名内部类 可以访问定义它们的那个方法中的局部变量, 这是怎么实现的呢?

编译器会检测内部类对局部变量的访问, 将每一个需要的局部变量捕获, 在内部类中建立相应的数据域并修改内部类的构造器, 在需要构造内部类对象的时候, 会将局部变量拷贝到构造器中,

这样处理过后, 内部类对象中就有了局部变量的一个备份, 即使原来的方法栈已经消失, 内部类对象也能够正常运行.

又因为局部类对象中的局部变量是原始数据的一个拷贝, 如果在原来的方法中或局部类中对自己的那一份数据进行了修改, 则会导致两份数据不一致, 可能到带来奇怪的问题. 为了解决这个数据一致性的问题, java 要求被内部类捕获的局部变量是事实上 final 的, 即一经声明后就不再修改, 这样才能保证局部变量的两份拷贝保持一致.

对于基本类型,

对于引用类型

类型外部对象引用访问外部类持有静态方法持有静态域
普通只能非静态方法创建, 有外部对象引用静态域, 实例域必须 final
局部非静态方法创建的有, 静态方法创建的没有静态域, 实例域必须 final
匿名非静态方法创建的有, 静态方法创建的没有静态域, 实例域必须 final
静态非静态/静态方法创建, 没有外部引用静态域可以可以, 不需 final

 

四. Lambda 与方法引用

1. 概述

2. Lambda 表达式

2.1. 完整形式

(参数1类型 参数1, 参数2类型 参数2) -> { 含 return 语句的方法体 }

完整形式可以使用语句块

2.2. 单行形式

(参数1类型 参数1, 参数2类型 参数2) -> 不含 return 与分号的单行语句

如果方法体中只有一行语句, 则可以采用单行形式. 且该行语句的结果会作为返回值返回

单行形式中省略分号, 大括号, return 关键字

2.3. 类型推断

( 参数1, 参数2 ) -> { 含 return 语句的方法体 }

( 参数1, 参数2 ) -> 不含 return 与分号的单行语句

如果根据上下文可以确定参数类型, 则可以在 lambda 表达式的参数列表中省略参数类型

如 lambda 表达式作为一个已知参数类型的方法的实参时, 因为该方法需要的参数以及返回值都是确定类型的, 那么作为实参的 lambda 应该满足该方法的需求, 使用与方法需求一直的参数类型. 此时即可省略 lambda 表达式的参数类型声明

2.4. 类型可推单参数

参数 -> { 含 return 语句的方法体 }

参数 -> 不含 return 与分号的单行语句

如果参数只有一个, 且这个参数的类型可以根据上下文推断, 则可以省略参数列表的小括号

2.5. 无参数

() -> { 含 return 语句的方法体 }

() -> 不含 return 与分号的单行语句

如果 lambda 不需要提供参数, 则需要提供空的小括号, 就像普通的无参方法一样.

1) lambda 表达式

3. 方法引用

如果已经存在现成的方法可以满足需求, 则可以传递 方法引用, 指定一个方法作为实参, 传递给函数式接口.

注意, 要作为实参的方法, 其参数列表与返回值类型要符合目标接口的抽象方法中的要求.如果作为实参的方法有多个重载方法, 那么编译器会根据目标接口的抽象方法, 确定要使用哪个重载方法.

3.1. 类名::静态方法

与普通的方法调用一样, 由类名来调用静态方法, 但符号为双冒号 ::

使用这种方式时, 需要先定义一个静态方法, 且满足: 参数列表与返回值 同 抽象方法中的一样

类名::静态方法

3.2. 对象::实例方法

与普通的方法调用一样, 由对象来调用实例方法, 但符号为双冒号 ::, 可以使用 this, super 关键字

使用这种方式时, 需要先定义一个静态方法, 且满足: 参数列表与返回值 同 抽象方法中的一样

使用 类名::静态方法对象::实例方法 时, 等价于参数列表相同的 lambda 表达式

对象::实例方法

3.3. 类名::实例方法

通过 类名::实例方法 , 此时这个实例方法将由抽象方法中定义的第一个参数来调用.

抽象方法的第一个参数, 将会成为实参方法的调用方, 抽象方法中的其余参数作为实参方法的参数,

因此作为实参的方法, 比函数式接口中的抽象方法少一个参数,

又因为实参方法要由抽象方法第一个参数来调用, 则这个类型是抽象方法中第一个参数的类型, 实参方法是第一个参数所属类中定义的方法, 即

第一参数类型::实例方法

3.4. 类名::new

构造器引用与方法引用类似, 但方法名为 new, 可以将 类名::new 作为实参传递给函数式接口.

因为构造器往往会有多个重载方法, 编译器会根据函数式接口的需求, 选择合适的构造器. 而由于参数类型不同, 可能需要将构造方法传递给不同的函数式接口, 比如:

完整例子如下:

附: 具有多个构造方法的类

程序输出结果:

4. 闭包与变量的作用域

3.1. 闭包与外部变量

有时候我们可能会希望能够在 lambda 中访问外部方法的局部变量, 又或者是类中声明的静态域实例域, 如:

在方法体中出现的 var 和 sVar, 并不属于这个目标接口, 我们也未在 lambda 中声明过任何与之相关的变量来接受他们的值, lambda 表达式到底是怎么处理这种外部变量的呢?

我们知道, lambda 在底层将会被转化为目标接口的一个对象, 由这个对象来调用 lambda 的方法体. 既然 lambda 能够处理外部变量, 就说明这个底层对象中的某个部分, 应该存储了这些外部变量的内容.

到这里, 我们可以发现 lambda 表达式有三个部分组成:

对上述代码 debug 执行, 可以看到 lambda 的对象 consumer 中有以下内容:

所谓的自由变量, 就是通过对象的实例域存储相应的引用来实现

1536815275167

如果我们的 lambda 表达式不使用任何外部变量, 则 consumer 对象不会包含任何实例域 field, 如下所示

1536812385268

 

3.2. 对外部变量的要求

在内部类和 lambda 中使用外部变量时, 需要满足以下要求:

 

3.3. lambda 与 内部类的区别

1) 外部对象

lambda 表达式中, 按需捕获外部类对象的引用, 而匿名内部类, 则始终都会捕获外部类对象的引用.

lambda 表达式的对象, 捕获了外部的ClosePackTest@849 对象, 其中就包含了自由变量 var

1536808032884

2) 局部其他变量

内部类方式, 不论是否使用了外部类对象的实例域, 内部类对象都会捕获了外部对象 this$0

1536807879856

五. 代理

代理, proxy, 可以在运行时创建一个实现了一组指定接口的新类, 并创建代理对象. 应用程序开发中很少用到这种情况, 但代理技术带来的灵活性对系统程序设计人员来说非常重要.