ch 6 接口、lambda表达式与内部类
@author:posper
@version:v 1.0
@date:2021/7/2
本文档根据 《Java 核心卷1》ch 6 与 b 站/千锋 lambda 表达式 整理。
本文档为第二遍看《Java 核心卷1》ch 6 的时候所整理。第一遍看的时候画的思维导图。
ch 6 接口、lambda表达式与内部类
6.1 接口
接口的概念
-
接口(interface)用来描述类应该做什么,而不指定他们具体如何去做(具体实现需在实现该接口的子类中)
-
一个类可以实现一个或多个接口
- Java 中不支持多继承,但是可以通过实现多个接口达到类似多继承的效果
-
类实现接口的步骤:
-
将类声明为实现给定的接口;
使用 implements 关键字
-
对接口中的所有抽象方法提供定义
-
-
一点注意:
-
在接口声明中,方法可以不加修饰符,方法自动默认为 public。
-
但是,在实现接口时,必须把方法显示声明为 public ;否则,编译器报错。
- 因为实现类中方法的访问权限一定要大于接口中方法声明的权限,不显示加 public 就默认为包访问权限了,所以报错。
-
接口的属性
- 接口不是类
- 不能使用 new 运算符实例化接口;
- 但是,可以声明一个接口的变量,然后使其引用实现了这个接口的类的对象。
- 接口中可以使用 this 关键字,代表当前接口变量。
- 接口可以扩展(extends)接口
- 接口中不能包含实例字段,但是可以包含常量
- 接口中的方法都自动设置为 public abstract,接口中的字段总是 public static final
接口与抽象类的区别
-
Java 不支持多继承。
-
为什么 Java 不支持多继承?
- 多继承会让程序语言变得非常复杂(如 C++);
- 多继承会是效率降低(如 Eiffel)
-
接口的优点:接口可以提供多继承的大多数好处,同时能避免多继承的复杂性和低效性。
-
为什么有了抽象类还要使用接口 ?
- 因为一个类只能扩展一个父类,但是可以实现多个接口。
静态和私有方法
-
Java 8 中,允许在接口中增加静态方法
- 可以在接口中实现这些静态方法;
- 虽然在 Java8 之后合法,但是这样有违接口作为抽象规范的初衷;
- 通常做法是,标准库中成对出现接口以及对应的工具类(如 Collection/Collections,Path/Paths),并将静态方法放在伴随类中,而不是接口中(当然,也能放在接口中,只是说通常做法而已)
-
Java 9 中,接口的方法可以是 private
- private 方法可以是静态方法或者实例方法;
- 但是只能在接口本身中使用,一般作为当前接口中其他方法的辅助方法。
默认方法
-
default 修饰符
-
接口中默认方法的作用:”接口演化“
- 接口中的默认方法可以不在实现类中具体实现,也可以覆盖之;
- “接口演化”:接口更新时,将添加的新方法(如,Java 8 中在 Collection 接口中添加的 stream() 方法设置为 default 方法),则旧版本中接口的实现类不用强制重写接口中新加入的方法。
解决默认方法冲突
-
1、超类优先
-
如果一个类继承一个超类且扩展了一个接口,则优先使用超类的具体方法,而不是接口中默认方法;
-
一个类继承的超类和实现的接口中,如果有同名方法,主要有下面 4 种情况:
超类 接口 使用情况 具体方法 抽象方法 调用的是超类中的具体方法。此时,不用在具体类中重写接口中的抽象方法 具体方法 默认方法 超类优先。使用的仍然是超类中的具体方法。 抽象方法 默认方法 必须在实现子类的重写方法,然后调用的是子类中的重写方法。 抽象方法 抽象方法 必须在实现子类的重写方法,然后调用的是子类中的重写方法。 -
总结:此时超类优先,一句话反正就是看父类的。
- 如果父类中是具体方法,则不管接口中方法如何,直接调用父类的具体方法;
- 如果父类中是抽象方法,则不管接口中方法如何,则必须在实现类中重写抽象类中的抽象方法,然后调用的是实现类的重写方法
-
-
2、接口冲突
- 两个接口具有同名且参数列表均相同的默认方法,则一个实现了这两个接口的类需要覆盖这个默认方法,以解决同名默认方法冲突
接口1 接口2 使用情况 默认方法 抽象方法 必须在实现子类的重写方法,然后调用的是子类中的重写方法。 默认方法 默认方法 必须在实现子类的重写方法,然后调用的是子类中的重写方法。 抽象方法 默认方法 必须在实现子类的重写方法,然后调用的是子类中的重写方法。 抽象方法 抽象方法 必须在实现子类的重写方法,然后调用的是子类中的重写方法。 - 总结:此时,无论这两个接口中的方法是什么类型的,都必须在实现类中重写,最终调用的是实现类中的重写方法。
- 如果至少有一个接口中的同名方法是抽象的,则实现类不是继承另一个接口的默认方法,而是必须要重写该方法;
- 如果两个接口中的方法都是默认方法,此时会产生方法冲突,也要在实现类中重写方法。
- 在实现类中,重写方法时可以选择使用接口中的默认方法,语法形式为:接口名.super.方法名
接口与回调
- 回调(callback)可以指定某个特定时间发生时应该采取的动作
Comparator 接口
- 利用 Comparator 实现自定义排序的步骤:
- 1、定义一个比较器的类
- 比较器是实现了 Comparator 接口的类的实例
- 比较器的类是实现了 Comparator 接口的类
- 2、重写 compare 方法
- 3、调用 Arrays.sort(arr, cmp)
- 1、定义一个比较器的类
- 自定义类型实现排序的两种方式:
- 方法1:自定义类型实现 Comparable 接口,然后重写 compareTo 方法;
- 方法2:实现一个比较器 Comparator,然后将其传递给 Arrays.sort(arr, cmp)
- 实现比较器的三种方式:
- 1、方法1:创建一个 Comparator 的实现类,重写 compare 方法;
- 2、方法2:匿名内部类;
- 3、方法3:lambda 表示式
- 实现比较器的三种方式:
对象克隆
-
拷贝与克隆:
- 拷贝只是复制对象变量中的内容。
- 对于基本类型来说,拷贝和克隆无本质区别。
- 但是,对于引用类型来说,拷贝只是复制一份和原有变量相同的引用,在内存中这两个变量仍然引用同一个对象,并没有创建新对象;而克隆则是,在内存中重新生成一个和原有对象状态信息完全一致的新对象。
public class Employee { private String name; private double salary; private Date hireDay; .... // 省略一堆方法 } Employee original = new Employee("John Public", 50000); Employee copy = original; // 拷贝 copy.raiseSalary(lO); // 两个变量引用同一个对象,一变则全变
-
浅拷贝
-
适用于只包含基本类型字段或者不可变类字段的类
- 直接调用 Object 类中的 clone 方法即可
-
只拷贝引用,对于可变类字段,原对象和克隆对象仍会共享一些信息
-
默认的克隆操作是”浅拷贝“
// 浅拷贝 Employee copy = original.clone(); // 调用的是 Object 类中的 clone() 方法
-
-
深拷贝
-
包含可变类字段时,必须使用深拷贝克隆所有子对象
-
深拷贝步骤
- 1、待克隆的类型 implements Cloneable 接口;
- 2、重写 Cloneable 接口的 clone 方法;
- 1)先浅拷贝(即,调用 Object.clone 方法);
- 2)再分别克隆可变类引用字段。
-
// 深拷贝
public class Employee implements Cloneable { // 1、实现 Cloneable 接口
private String name;
private double salary;
private Date hireDay;
@Override
protected Employee clone() throws CloneNotSupportedException { //2、重写clone 方法,并将返回类型改为 Employee
// 1)、浅拷贝:调用 Object 中的 clone 方法
Employee cloned = (Employee) super.clone();
// 2)、克隆可变字段
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
}
6.2 lambda 表达式
为什么引入 lambda 表达式?
- lambda 表达式作用:简化函数式接口的实现
- 接口的实现方法(3种)
- 定义一个接口实现类
- 匿名内部类
- lambda 表达式(最简洁,但只能实现函数式接口)
函数式接口
- 只有一个抽象方法的接口,称为函数式接口;
- 可以将 lambda 表达式传递给一个函数式接口;
- 使用 @FunctionalInterface 注解,可以验证一个接口是否是函数式接口。
lambda 表达式语法
-
语法:(参数),箭头( -> ) 以及一个 {语句块}
(参数) -> { 方法体 }
// lambda 表达式:实现一个多参数,有返回值的函数式接口 SingleReturnMutipleParameter lambda6 = (int x, int y) -> { System.out.println("\n6、实现一个多参数,有返回值的函数式接口..."); System.out.println(x + " + " + y + " = " + (x + y)); return x + y; };
- 参数类型必须与其要实现的函数式接口中的唯一抽象方法的参数列表保持一致;
- 无须指定 lambda表达式的返回类型:
- 如果函数式接口中的抽象方法中有返回值,则在 lambda 表达式中也要带上返回值;
- 返回类型一定要与其将实现的函数式接口中的抽象方法的返回类型保持一致。
-
如果代码不止一个表达式,则可以将代码放在 {} 中
-
lambda 表达式没有参数时,需要提供一个空括号
-
可以推导出 lambda 表达式参数类型时,可以忽略其类型。但如果省略,必须全部省略。
-
无须指定 lambda 表达式返回类型
lambda 表达式语法进阶
参数类型的精简
-
参数类型
- lambda表达式中的参数的类型可以省略不写。(因为函数式接口中的抽象方法已经定义了参数类型)
- 注意:如果省略,必须全部省略;不能出现有的省略,有的不省略的情况。
-
参数的小括号
- 如果 lambda表达式中的参数有且只有一个,则可以省略小括号。(无参,以及参数大于 1都不可省小括号)
- 注意:
- 只有当只有一个参数时,才能省略小括号。多了少了都不能省。
- 省略小括号后,必须省略参数类型。
// 1、实现一个单参数,无返回值的函数式接口 NoneReturnSingleParameter lambda2 = x -> { // 省略了参数类型和小括号 System.out.println("1、实现一个单参数,无返回值的函数式接口"); }
方法体部分的精简
-
方法体大括号的精简
- 当方法体中,有且只有一条语句时,花括号可以省略;
// 方法体精简:省略花括号(只有一条语句) NoneReturnSingleParameter lambda2 = x -> System.out.println("1、实现一个单参数,无返回值的函数式接口");
-
return 的精简
- 如果一个方法中唯一的一条语句是一个返回语句, 此时在省略掉大括号的同时, 也必须省略掉 return。
// return 的精简: SingleReturnMutipleParameter lambda6 = (x, y) -> x + y;
方法引用
- 使用“方法引用”的原因:
- lambda表达式是为了简化接口的实现的。
- 如果 lambda表达式中出现了较为复杂的逻辑代码,此时可读性会降低,应选择使用方法引用。
- 方法引用: 引用一个已经存在的方法, 使其替代 lambda 表达式完成接口的实现。
- :: 运算符 分割方法名与对象名或类名
- 方法引用的三种情况:
- 静态方法的引用
- 语法:类名::静态方法
- 注意:
- 在引用的方法后面, 不要添加小括号;
- 引用的这个方法, 参数(数量、类型) 和 返回值, 必须要跟接口中定义的一致。
- 实例方法的引用
- 语法:对象名:实例方法
- 注意:
- 在引用的方法后面, 不要添加小括号;
- 引用的这个方法, 参数(数量、类型) 和 返回值, 必须要跟接口中定义的一致。
- 构造器引用
- 语法:类名::new
- 注意:
- 可以通过接口中的方法的参数, 区分引用不同的构造方法。
- 这个部分多与 《Java卷II》中流库相关
- 静态方法的引用
变量作用域
-
lambda 表达式包含 3 个部分
- 1、一个代码块;
- 2、参数;
- 3、自由变量的值:这里指非参数,而且不在方法体中定义的变量。
-
Java中,lambda 表达式就是闭包
-
lambda 表达式中捕获的变量(即,自由变量)必须是事实最终变量
- 1)捕获的变量值不能在 lambda 表达式代码块中改变
- 2)捕获的变量值不能在 lambda 表达式外部改变
-
lambda 表达式的体与嵌套块有相同的作用域
- lambda 表达式不能有与其所在方法同名的局部变量
int x = 123;
String text = "abc";
NoneReturnSingleParameter lambda = (x) -> { // error, x 不能重定义
System.out.println("x = " + x); // error
System.out.println("text + " + text); // text 为自由变量
};
lambda.test(4);
6.3 内部类
-
定义:定义在另一个类中的类,被称为内部类
-
使用内部类的两个原因:
- 1、内部类可以对同一个包中的其他类隐藏;
- 2、内部类方法可以访问定义这个类的作用域中的数据,包括 private 字段。
-
编译器将内部类转换为 OuterCalss$InnerClass.class 文件,与虚拟机无关。
实例内部类
-
在外部类中定义,不用 static 修饰;
-
类似实例字段;
-
依托于外部对象
public class OuterClass { // 实例内部类 class InnerClass { // 默认包可见,也可以改为 public 或 private(此时只能在 OuterClass 中使用) ... } } OuterClass outer = new OuterClass(); // 创建外部类对象 // 内部类对象依托于外部类对象引用 OuterClass.InnerClass inner = outer.new InnerClass();
静态内部类
- 在外部类中定义,用 static 修饰;
- 类似静态字段;
- 依托于外部类,不依托于外部对象
public class OuterClass {
// 静态内部类
static class InnerStaticClass {
...
}
...
}
// 2、创建静态内部类实例
OuterClass.InnerStaticClass innerStaticClass = new OuterClass.InnerStaticClass(); // 不用依赖外部类对象
局部内部类
-
在外部类中的方法中定义,不能有访问说明符(即,public 或 private);
- 局部内部类只能被 abstract or final 修饰
-
类似局部变量;
-
在方法结束时,局部内部类不能再使用
- 可以访问局部变量;
- 访问的局部变量必须是事实最终变量,即它们一旦赋值就不会再改变。
public class OuterClass {
// 使用局部内部类....
public void testInnerLocalClass(int x) {
class InnerLocalClass { // 定义局部内部类
public void show() {
x++; // error。事实最终变量赋值后不能修改
System.out.println("x = " + x); // 局部内部类中可以访问局部变量
}
}
// 创建局部内部类
InnerLocalClass innerLocalClass = new InnerLocalClass();
innerLocalClass.show();
}
}
OuterClass outer = new OuterClass(18); // 创建外部类对象
// 3、创建局部内部类
outer.testInnerLocalClass(123);
匿名内部类
- 没有名字的类,只能使用一次;
- 这个常用。用匿名内部类作为接口参数的一次性实现类来实现接口的抽象方法
- 匿名内部类是定义一个接口的实现类的简写形式,但是可读性相对较差,且只能使用一次(因为类没有名字)
- 语法:new ImplA() { 抽象方法的实现 … }
- 等价于 class Obj implements ImplA { // 抽象方法实现 … } ,然后再将 Obj 的对象传给 ImplA
// 接口
interface ImplSum {
int sum(int x, int y);
}
public class AnonymousInnerClassTest {
public static void mySum(ImplSum implSum, int x, int y) {
implSum.sum(x, y);
}
public static void main(String[] args) {
// 1、匿名内部类
mySum(new ImplSum() {
@Override
public int sum(int x, int y) {
return x + y;
}
}, 10, 20);
// 2、匿名内部类的等价形式
mySum(new Obj(), 15, 20);
}
}
// 匿名内部类的等价形式
class Obj implements ImplSum {
@Override
public int sum(int x, int y) {
return x + y;
}
}
内部类之前对于简洁地实现回调非常重要,但如今 lambda 表达式在这方面可以做的更好。
6.4 服务加载器
- 第一遍暂时跳了…
- 第 2 遍还是跳了…
6.5 代理
- 第一遍暂时跳了…
- 第 2 遍(2021/7/2)还是跳了…