第六章 接口、lambda表达式与内部类
6.1 接口
-
接口
: 他不是类,是对希望符合这个接口的类的一组需求 -
接口
中 所有方法 都自动是public方法
。因此,在接口中声明方法时,不必提供关键字public
-
接口
可以定义 常量, 但是 绝不会有 实例字段。👿 👿 👿- 可以将接口其看成没有实例字段的抽象类。但两者还有着一定的区别,如果将接口设计为抽象类,会存在一个严重的问题,即:每个类只能扩展一个类,假设某个类已经扩展了别的类,他就不能再扩展第二个类了。(Java不支持多重继承)
-
一个具体例子:
Arrays类
中 的sort()
方法承诺可以对对象数组进行排序,但必须满足的条件是:对象所属类必须实现Comparable接口
public interface Comparable { int compareTo(Object other); } // java 5 开始 public interface Comparable<T> { int compareTo(T other); }
-
假设希望使用
Arrays类
的sort方法
对Employee对象数组
进行排序,Employee类
就必须实现Comparable接口
// 将类声明为实现给定接口 class Employee implements Comparable { ...... // 对接口中的所有方法提供定义 public int compareTo(Object otherObject) { Employee other = (Employee) otherObject; return Double.compare(salary, other.salary); } } // java 5 开始 class Employee implements Comparable<Employee> { ...... public int compareTo(Employee other) { return Double.compare(salary, other.salary); } }
-
x.compareTo(y)
: x 小于 y,返回负数;x 等于 y,返回0; x 大于 y,返回正数。 -
Comparable接口
的文档建议:compareTo方法
应当与equals方法
兼容,也即是说当x.equals(y)
时,x.compareTo(y)
就应该等于 0。 -
如果比较的两个对象 一个是父类 eg:Employee,一个是子类 eg: Manager,
x.compareTo(y)
不会异常,y.compareTo(x)
会出现异常ClassCastException
。(与equals方法
相似)两种解决方法👿 👿 👿-
不同子类中比较的含义不同时,就应该将属于不同类的对象之间的比较视为违法,在开始
compareTo方法
时,进行如下检查:if(getClass() != other.getClass()) throw new ClassCastException()
-
如果存在一个比较子类对象的通用方法,那么可以在超类中提供一个
compareTo方法
,并将这个方法声明为final
-
-
-
接口不是 类,不能用 new 运算符实例化一个接口,虽然不能构造接口对象 ,但是可以声明接口的引用变量。每个类只能有一个超类,但是可以实现多个接口 -
如同使用
instanceof
检查一个对象是否属于某个特定类一样,也可以使用instanceof
检查一个对象是否实现了某个特定的接口:if(anObject instanceof Comparable){...}
-
接口中的方法 都会被自动设置为
public
,接口中的字段总是public static final
👿 👿 👿 -
以前的做法通常都是将 静态方法 放在
伴随类
中,在标准库中,你会看到成对出现的接口
和实用工具类
,eg:Collection/Collections
或者Path/Paths
。但是,在java8
开始,允许在接口中 增加 静态方法,实现你的接口时,没有必要为实用工具方法另外提供伴随类
-
java9
中接口的方法可以是private
。private方法
可以是静态方法
或实例方法
。他的用法很有限,只能作为接口中其他方法的辅助方法 -
可以为 接口 提供一个 默认实现(默认方法),必须用
default
修饰符标记这样一个方法。默认方法的一个重要用法是 “接口演化”-
冲突问题:在一个接口将一个方法定义为默认方法,又在 超类 或 另外一个接口 中定义同样的方法,会发生 “二义性” 问题,解决规则如下:
超类优先
: 如果超类提供一个具体方法,同名而且有相同类型参数的默认方法会被忽略接口冲突
: 如果一个接口提供一个默认方法,因一个接口提供了一个同名且参数类型相同(不论是否为默认方法)的方法,必须覆盖这个方法来解决冲突- 如果两个接口都没有给共享方法提供默认实现,这里就不存在冲突
-
-
回调
: 是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。 -
Sting.compar()方法
可以按照字典顺序比较字符串,可是当我们需要按照长度去比较字符串时,就需要实现Arrays.sort()
的第二个版本:public interface Comparator<T>{ int comapre(T first, T second); }
按照长度实现可以定义下面这个类
class LengthComparator implements Comparator<String> { public int compare(String first, String second) { return first.length() - second.length(); } } // 具体完成比较时,需要建立一个实例 var comp = new LengthComparator() // 这个比较方法要在比较器对象上调用,而不是在字符串本身上调用 if(comp.compare(words[i], words[j]) > 0) ...
对数组排序时,需要为
Arrays.sort()
传入一个LengthComparator对象
String friends = {"Peter", "Paul","Mary"}; Arrays.sort(friends, new LengthComparator());
-
Cloneable
接口:-
object类
的clone
,他对这个对象一无所知, 所以这能逐个字段进行拷贝。如果是基本类型就没有问题,但是如果这个对象字段中包含对象引用(子对象是不可变不影响),这样一来拷贝下来仍然会出现局部共享。所以出现下面这两个概念:-
“浅拷贝”
: (Object类中clone方法的默认操作):并没有clone 对象中字段引用的其他对象 -
“深拷贝”
: 同时克隆所有子对象 -
对于每一个类需要确定:
-
默认的 Object类 中的clone方法是否满足需求;
-
是否可以在可变的子对象上调用 clone方法 来弥补默认的clone 方法,如果需要:
- 实现
Cloneable
接口 - 重新定义 clone 方法,并指定
public
访问修饰符
- 实现
-
-
-
-
Object类
中的clone方法
声明为protected
, 所以不能在自己写的代码里调用anObject.clone
, 因为你的类不是 该对象的子类,且你可能和定义clone方法的类不在一个包下,所以在重新定义clone方法时,要指定为 public,以便所有方法都可以克隆对象 -
Cloneable接口
没有clone方法
,这个方法是从Object类
继承的。它是Java中少数的 标记接口 ,标记接口 不含任何方法,就是在类检查中允许使用instanceof
-
即使默认
clone方法
实现能够满足要求,但还是需要实现Cloneable接口
, 将 clone 重新定义为public
, 在调用super.clone()
。class Employee implements Cloneable { // public access, change return type public Employee clone() throws CloneNotSupportedException { return (Employee)super.clone(); } ... }
-
“深拷贝” 需要克隆对象中可变的实例字段
class Employee implements Cloneable { ... // 在一个对象上调用 clone 方法,若没有实现Cloneable接口, 会抛出 //CloneNotSupportedException异常 public Employee clone() throws CloneNotSupportedException { // call Object.clone() Employee cloned = (Employee) super.clone(); // clone mutable fields cloned.hireDay = (Date) hireDay.clone(); return cloned; } }
-
所有的数组类型都有一个 **公共的 **
clone方法
,而不是 受保护的。可以用这个方法 建立一个新数组,包含原数组的所有副本。int[] luckNumbers = {2, 3, 5, 7, 11, 13}; int[] cloned = luckNumbers.clone(); cloned[5] = 12 // doesn't change luckNumbers[5]
6.2 lambda 表达式
-
lambda表达式
:是一个可传递的代码块
-
例子
class LengthComparator implements Comparator<String> { public int compare(String first, String second) { return first.length() - second.length(); } } ... Arrays.sort(friends, new LengthComparator());
-
这个例子的特点:将一个 代码块 传递给某个对象。这个代码块会在将来某个时间调用
-
再出现这个与法之前,Java传递一个代码块并不容易,你不能直接传递代码块,因为Java是面向对象语言,你必须构造一个对象,这个对象的类需要有一个方法包含所需要的代码。然后传递一个对象。
-
lambda表达式
是 Java语言 用来 **支持函数式编程 **的。
-
-
lambda表达式
表达的多样形式:-
参数,箭头(->)以及一个表达式,并包含显式的 return 语句
(String first, String second) -> { if(first.length() < second.length()) return -1; else if(first.length() > second.length()) return 1; else return 0; }
-
没有参数也要提供 空括号
() -> {for (int i = 100; i >= 0; i--) System.out.println(i);}
-
如果可以推导出一个Lambda表达式的参数类型,则可以忽略其类型
Comparator<String> comp = (first, second) // same as (String first, String second) -> first.length() - second.length();
-
如果方法只有一个参数,而且这个参数的类型可以推导出,那么可以省略小括号
ActionListener listener = event -> System.out.println("The time is " + Instant.ofEpochMilli(event.getWhen())); // instead of (event) -> ... or (ActionEvent event) -> ...
-
无需定义
lambda表达式
的返回类型,返回类型总是会由上下文推导得出。(String first, String second) -> first.length() < second.length())
-
-
只有一个 抽象方法 的接口,需要这种接口对象时,可以提供 一个
lambda表达式
,这种接口称为函数式接口
。这个表达式会转化为接口,例子如下:// 第二个参数 需要一个 Comparator实例(是只有一个方法的接口),可以提供一个 lambda表达式 Arrays.sort(words, (first, second) -> first.length() - second.length())
-
lambda表达式
可以转化为 接口 。eg:var timer = new Timer(100, event -> { System.out.println("At the tone, the time is" + Instant.ofEpochMilli(event.getwhen())); Toolkit.getDefaultToolkit().beep(); });
-
最好把
lambda表达式
看做是一个函数,而不是一个对象,另外要接受lambda表达式
可以传递到函数式接口 -
实际上,在 Java中,对
lambda表达式
所能做的 也只是 转化为函数式接口
。没有为 Java语言 增加函数类型。👿 👿 👿 -
方法引用
: 例如下面的System.out::println
就是一个方法引用,他指示 编译器 生成一个 函数式接口实例,用给定的方法覆盖实例的抽象方法并调用。 下面例子会生成 ActionListener对象, 它的actionPerformed(ActionEvent event)
方法会调用System.out.println(e)
。// 假设你希望只要出现一个定时器时间就打印这个事件对象 var timer = new Timer(1000, event -> System.out.println(event)); // 直接把 println方法 传递到 Timer构造器 var timer = new Timer(1000, System.out::println); // 假设你想对字符串进行排序,而不考虑字母大小写。可以传递以下方法表达式 Arrays.sort(strings, String::compareToIgnoreCase);
-
方法引用
与lambda表达式
类似,两者都不是对象。在为函数式接口
的变量赋值时会生成一个对象(即方法引用不能独立存在,总是会转化为函数式接口的实例)。注意: 只有当lambda表达式
的体只调用一个方法而不做其他操作时,才能把lambda表达式
重写为方法引用
(阐述了两者的区别)👿 👿 👿 -
方法引用
主要有三种情况,要用::运算符
分割方法名与类名或对象- object::instanceMethod
- 等价于
lambda表达式
eg:System.out::println
等价于x -> System.out.println(x)
- 等价于
- Class::instanceMethod
- 第一个参数会成为方法的隐式参数 eg:
String::compareToIgnoreCase
等价于(x, y) -> x.compareToIgnoreCase(y)
- 第一个参数会成为方法的隐式参数 eg:
- Class::staticMethod
- 所有参数都会传递到静态方法中 eg:
Math.pow
等价于 (x, y) -> Math.pow(x, y)
- 所有参数都会传递到静态方法中 eg:
- object::instanceMethod
-
方法引用可以使用 this指参数,super也是合法的 eg:
this::equals
等同于x -> this.equals(x)
-
构造器引用
: 与方法引用类似,只不用方法名为new
,eg:Person::new
是 person 构造器的一个引用ArrayList<String> names = ...; Stream<Person> steam = names.stream.map(Person::new); List<Person> people = stream.collect(Collectors.toList());
-
可以用 数组类型 建立
构造器引用
。 eg:int::new
是一个构造器引用,他有一个参数:即数组的长度。这个引用等价于 lambda表达式x -> new int[x]
-
变量作用域
: 希望能够在lambda表达式
中访问外围方法或类中的变量public static void repeatMessage(String text, int delay) { ActionListener listener = event -> { System.out,println(text); Toolkit.getDefaultToolkit().beep(); }; new Timer(delay,listener).start(); }
-
text 并不是在这个
lambda表达式
中定义的,lambda表达式
的代码可能会在 reapeatMessage 调用返回很久后才运行,而那时,这个参数变量 已经不存在了,如何保留呢?lambda表达式
有 3 个部分:- 一个代码块
- 参数
- 自由变量的值,这是指非参数而且不在代码中定义的变量 eg: text
- 这个
lambda表达式
的自由变量是 text。表示lambda表达式
的数据结构必须会存储自由变量的值。我们说它被lambda表达式
捕获(captured) eg:可以把lambda表达式
转换为包含一个方法的对象,这个 自由变量 的值就会复制到这个对象的实例变量中。
-
要确保 捕获的值是明确定义的,这里有一个重要限制:在
lambda表达式
中,只能引用 值不会改变 的变量(原因之一:在lambda表达式
中更改变了,在并发执行多个操作会不安全)- 如果这个变量在
lambda表达式
外可能改变,这也是 不合法 的, - 所 捕获 的变量必须是
事实最终变量
:这个变量在初始化之后就不会在为它赋新值
- 如果这个变量在
-
lambda表达式
体与嵌套块 有相同作用域。同样适用命名冲突和遮蔽的有关规则-
在
lambda表达式
中声明一个局部同名的参数 或 局部变量 是 不合法 的。在一个方法中,不能有两个同名局部变量,lambda表达式
也不能有Path first = Path.of("/usr/bin"); // ERROR: Variable first already defined Comparator<String> comp = (first, second) -> first.length() - second.length();
-
-
-
在
lambda表达式
中使用 this关键字,是指创建 这个lambda表达式
的方法的 this参数// 表达式 this.toString() 会调用 Application 对象的 toString 方法 // 而不是 ActionListener实例的方法 Public class Application{ public void init(){ ActionListener listener = event -> { System.out.println(this.toString()); ... } ... } }
-
编写 自定义方法 处理
lambda表达式
-
使用 表达式的 重点: 延迟执行
-
之所以希望以后再执行,有很多原因,如:
- 在一个单独的线程中运行代码
- 多次运行代码
- 在算法的适当位置运行代码,eg:排序中的比较操作
- 发生某种情况时运行代码,eg:点击一个按钮,数据传达
- 只在必要时运行代码
-
例子:假设你想要重复一个动作 n 次,将这个动作和重复次数传递到一个 repeat 方法
repeat(10, () -> System.out.println("Hello,World!"); // 接收 这个 lambda表达式 需要一个函数式接口 public static void repeat(int n, Runnable action){ for(int i = 0; i< n; i++) action.run(); } // 如果我们希望告诉这个动作它出现在 哪一次迭代中,需要选择一个合适的函数式接口 // 这个方法有一个int参数,且返回值为 void public interface IntConsumer{ void accept(int value); } public static void repeat(int n, IntConsumer action){ for(int i = 0; i < n; i++) action.accept(i); } repeat(10, i -> System.out.println("Countdown:" + (9 - i)));
-
-
如果设计你自己的接口,其中只有一个抽象方法,可以用注解
@FunctionalInterface
来标记,如果你无意中增加了另一个抽象方法,编译器会产生一个错误消息
6.3 内部类
-
内部类
: 定义在另一个类中的类 -
为什么需要?
- 内部类可以对同一个包中的其他类隐藏
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有属性
-
Java 中 内部类 与 C++ 中嵌套类有一定区别
- Java 内部类的对象会有一个 隐式索引 ,指向实例化这个对象的外部类对象
- Java 中静态内部类没有这个附加指针,相当于 C++ 中的嵌套类
-
举个例子
public class TalkingClock{ private int interval; private boolean beep; public TalkingClock(int interval, boolean beep){ ... } public void start(){...} // an inner class public class TimerPrinter implememts ActionListener{ ... } } public class TimerPrinter implememts ActionListener{ public void actionPerformed(ActionEvent event){ System.out.println("At the tone, this time is " + Instant.ifEpochMilli(event.getwhen)); if(beep) Toolkit.getDefaultToolkit().beep(); } }
- 一个内部类方法 可以访问自身数据字段, 也可以访问创建它的外围类对象的数据字段
- 内部类对象 总有一个 隐式引用, 指向创建它的外围类对象,它是不可见的,由 编译器 负责自动创建
-
内部类有一个外围类的引用(隐式引用),使用外围类引用的正规语法:
OutClass.this
-
可以这样改写 TimerPrinter类 的 actionPerformed 方法:
public void actionPerformed(ActionEvent event){ ... if(TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep(); }
-
-
编写内部类对象构造器的明确语法:
outerObject.new InnerClass(construction parameters)
-
外围类引用 被设置为 内部类对象 的方法的 this引用, 通常 this限定词 多余
ActionListener listener = this.new TimerPrinter();
-
显示将 外围类引用 设置为其他的对象,因为对于公共内部类,任何 外部类对象 都可以构造 内部类对象
var jabberer = new TalkingClock(1000, true); TalkingClock.TimePrinter listener = jabberer.new TimePrinter();
-
-
外围类作用域之外,引用内部类:
OuterClass.InnerClass
-
内部类声明的 所有静态字段 都必须 是final,并初始化为一个编译时常量
-
Java语法规范限制:内部类不能有 static 方法
-
内部类 是一个 编译器现象,与虚拟机无关。编译器会把内部类转化为 常规类文件, 用 $ 符分隔外部类名与内部类名,而虚拟机对此一无所知 eg:
TimePrinter类
将被转化为成类文件TalkingClock$TimePrinter.class
,内部类会存在 安全风险 👿 👿 👿 -
局部内部类
: 如果内部类只是在外围类中使用 一次,出现这种情况,可以在 **一个方法中局部地 **定义这个类public void start(int interval, boolean beep){ class TimerPrinter implememts ActionListener{ public void actionPerformed(ActionEvent event){ System.out.println("At the tone, this time is " + Instant.ifEpochMilli(event.getwhen)); if(beep) Toolkit.getDefaultToolkit().beep(); } var listener = new TimerPrinter(); var timer = new Timer(interval, listener); timer.start(); }
- 声明 局部内部类 不能有访问说明符
- 作用域: 被限定在声明这个局部内部类的块中
- 优势: 对 外部世界 完全隐藏,甚至 外围类 中的其他代码也不能访问它
- 优点:他们不仅能够访问外部类的字段,还可以访问局部变量(必须是事实最终变量),在编译器底层会为这个局部变量创建相应实例字段,并把局部变量复制到构造器(防止方法结束,局部变量消失)
-
匿名内部类
: 只想创建这个类的一个对象,甚至不想要为类指定名字public void start(int interval, boolean beep){ var listener = new ActionListener{ public void actionPerformed(ActionEvent event){ System.out.println("At the tone, this time is " + Instant.ifEpochMilli(event.getwhen)); if(beep) Toolkit.getDefaultToolkit().beep(); }; var timer = new Timer(interval, listener); timer.start(); }
- 这个语法非常晦涩难懂, 含义:创建一个类的新对象,这个类实现了 ActionListener 接口,需要实现的方法在 actionPerformed 中定义
-
语法:
new SuperType(construction parameters){ inner class methods and data }
-
SuperType
是 接口,如果是这样,内部类就要实现这个接口 eg: ActionListener -
SuperType
是 类,如果是这样,内部类就要扩展这个类 -
匿名内部类不能有构造器,因为构造器名字必须与类名相同,而匿名内部类没有类名 👿 👿 👿
-
construction parameters
构造参数 是要传递给超类构造器的变量,如果一个内部类实现一个接口,那就不会有任何构造参数( 接口是定义行为的,不是定义生产方式的 ),不过仍然要 提供一个小括号new InterfaceType(){ methods and data }
-
例子: 类构造新对象 和 扩展了那个类的匿名内部类对象 之间的差别
// a Person Object var queen = new Person(); // an object of an inner class extending Person var count = new Person("Dracula"){...};
- 如果 构造参数列表 后跟一个开始大括号,就是定义
匿名内部类
- 如果 构造参数列表 后跟一个开始大括号,就是定义
-
尽管不能有构造器,但可以提供一个对象初始化块
var count = new Person("Dracula"){ {Initialization} ... };
-
Java程序员习惯使用匿名内部类实现 事件监听器 和 其他回调,如今,还是使用
lambda表达式
public void start(int interval, boolean beep){ var timer = new Timer(interval, event -> { System.out.println("At the tone, this time is " + Instant.ifEpochMilli(event.getwhen)); if(beep) Toolkit.getDefaultToolkit().beep(); }); timer.start(); }
-
-
静态内部类
: 只是想为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用。将内部类声明为static
,这样就不会生成那个引用(外围对象引用) -
如果 内部类对象是 在静态方法中创建 ,那这个内部类 必须是静态内部类 👿 👿 👿
- 原因: 静态方法不能访问对象,因为静态方法执行时,这个对象可能还没有创建,如果创建普通内部类,他会有外围类对象的引用,但是在实际执行这个静态方法的时候,编译器不知道是否已经创建了这个对象,所以,编译器不知道你产生外围类对象引用是否不合理,所以会报错
-
在接口中声明的内部类自动是 static 和 public
6.4 服务加载器
ServiceLoader类
6.5 代理
代理
: 利用代理可以在 运行时 创建实现了一组给定接口的新类,只有在编译时期无法确定需要实现哪个接口时才有必要使用代理
目前不重要,留着以后看 page273