读后笔记 -- Java核心技术(第11版 卷I ) Chapter6 接口、lambda 表达式与内部类
6.1 接口
6.1.1. 接口
不是类,而是对类的一组需求描述,这些类要遵从接口描述的同一格式定义。
接口与类的区别: 接口中默认的方法是 public abstract,所以不写的都是这个类型;
不能用于实例化对象; 没有构造方法; 所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法; 不能包含成员变量,除了 static 和 final 变量; 不是被类继承了,而是要被类实现; 支持多继承; ** 提供 实例字段 和 方法实现 的任务,由实现接口的那个类 来完成。
类实现一个接口,需要两步:
- 1)将类声明为实现给定的接口;
- 2)对接口中的所有方法进行定义;
public interface Comparable<T> { // since Java 5,提供泛型类型 int compareTo(T other); // 接口中的所有方法都是 public } public class Employee implements Comparable<Employee> { // 接口实现,Comparable 的 CompareTo()接受任意的 Object,上面接口有泛型类型支持的 Comparable 版本,可直接传递 Employee 类型,不用进行类型转换 @Override public int compareTo(Employee other) { // 接口方法的实现 return Double.compare(salary, other.salary); // 浮动数比较 Double.compare(),整数比较 Integer.compare() } ... }
知识点: Arrays.sort(Object[] a); // 由于历史原因,API 里面的 sort 接受的是 Object[] 数组,然后再强制转换为 Comparable。理想情况应该是 public static void sort(Comparable[] a)
如果要对数组 a 中的元素排序,要求:数组中的元素必须属于实现了 Comparable 接口的类,并且元素间可比。
6.1.2. 接口的属性
// 1)可声明接口的变量,但必须引用实现了接口的类对象; Comparable x; // OK,声明接口变量 x = new Employee(...); // OK,Employee类 实现了 Comparable 接口(上面6.1.1的例子中),所以 x 可以引用 Employee 的类对象
// 另一个实际使用的例子,如:
List<String> list = new ArrayList(); // 可声明 List 接口的变量 list,但必须引用实现了该接口的类 ArrayList 的对象(通过new)
list.add("Amy"); // 2)可使用 instanceof 检查一个对象是否实现了特定的接口(类似于类); if (anObject instanceof Comparable) {...} // 3) 接口可以通过 extends 再扩展;
public interface Moveable { ...; }
public interface Powered extends Moveable { ...; } // 4)接口不能包含 实例字段 或 静态方法,但可包含 常量; public interface Powered extends Moveable { double milesPerGallon(); // 方法自动标记为 public (一般不写) double SPEED_LIMIT = 95; // 自动标记为 public static final constant (一般不写) }
// 5)一个类:仅有一个超类,但可实现 多个接口 class Manager extends Employee implements Cloneable, Comparable { ...; }
6.1.3. 接口与抽象类
接口中包含的几种方法:
- 1) abstract => 在实现类中被覆盖
- 2-3) default, static (from java 8)
- 4-5) private, private static (from Java 9) => 目的:用于抽取这个接口中的公共代码
Q: 为什么不使用抽象类,而采用接口?
A: 每个类只能扩展一个类(如抽象类),但是可以引用 多个接口。如果将 Comparable 定义为抽象类,那么 Manager 就没办法同时扩展 Employee 类和 Comparable 类 ----- 4.1 Java 抽象类 ----- public abstract class Employee { ... } // 详细描述参看 5.1.9 https://www.cnblogs.com/bruce-he/p/15738254.html ----- 4.2 Java 抽象方法 ----- public abstract class Employee { ... public abstract double computePay(); ... }
如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。 abstract 关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。 声明抽象方法会造成以下两个结果: 1)如果一个类包含抽象方法,那么该类必须是抽象类; 2)任何子类必须重写父类的抽象方法,或者声明自身为抽象类。最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。
更多详情参考:https://www.runoob.com/java/java-abstraction.html
6.1.4. 静态方法、私有方法
Java 8,接口有 静态 static 方法;通常将静态方法放在伴随类中,如 Collection/Collections、Path/Paths;
Java 9,接口有 私有 private 方法(可以是 静态方法 或 实例方法),只能在接口本身的方法中使用,故作为接口中其他方法的辅助方法;
public interface Path { // Since Java 11
public static Path of(URI uri) { ... } public static Path of(String first, String... more) { return FileSystems.getDefualt().getPath(first, more); } ... }
6.1.5. 默认方法 default
默认方法 : 接口可以有实现方法,而且不需要实现类去实现其方法。 Q:为什么要有这个特性? A:之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8 之前的集合框架没有 foreach 方法,通常能想到的解决办法是在 JDK 里给相关的接口添加新的方法及实现。
然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。
白话解释:新的 SDK 接口中有增加一些方法,因为类要扩展接口,必须实现这些接口的方法,这就导致之前的类代码要重新写。一个办法就是,让接口中的新增方法变成 default 方法,且已经实现,那么以前类的代码就不用重新修改了。
更多参考:https://www.runoob.com/java/java8-default-methods.html
// A default method can call an abstract method: public interface Collection { int size(); // an abstract method default bolean isEmpty() { return size() == 0; } // call abstract method size() ... }
【例如 Collection 接口,后来 有人写了一个 Bag 方法实现 Colleciton 接口。 然后,在 JDK 8 中 Collection 接口增加了一个 Stream 方法,如果将其设置为 default 方法,就可以解决 Bag 方法冲突的问题 】
6.1.6. 解决默认方法冲突
冲突场景:如果先在一个接口中将一个方法定义为默认方法, 然后又在超类或另一个接口中定义了同样的方法
解决冲突的规则:
- 1)超类优先(超类+接口)。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略;
- 2)接口冲突(>=2 接口)。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法, 必须 覆盖这个方法 或 指定某接口的方法 来解决冲突;
// 1. 场景:实现两个接口的 默认方法冲突。 两种解决方法: public interface Vehicle { default void print() { System.out.println("我是一辆车!"); } } public interface FourWheeler { default void print(){ System.out.println("我是一辆四轮车!"); } } // 解决方案一:创建自己的默认方法,来覆盖重写接口的默认方法: public class Car implements Vehicle, FourWheeler { default void print(){ System.out.println("我是一辆四轮汽车!"); } } // 解决方案二: 使用 super 来调用指定接口的默认方法 public class Car implements Vehicle, FourWheeler { public void print(){ Vehicle.super.print(); } }
// 1. 接口冲突:两个接口都定义相同的方法,类 Student 要同时实现 Person 和 Named 接口 // 场景 1.1: 如果有一个是默认方法,或两个都是默认方法。 // 解决办法:实现类必须做出决定,具体调用哪个接口的方法,“Interface.super.method();” interface Person { default String getName() { return "John Q. Public"; } } interface Named { default String getName() { return getClass().getName() + "_" + hashCode(); } } class Student implements Person, Named { public String getName() { return Person.super.getName(); } // 指定调用的具体接口的 默认方法 ... } // 场景 1.2:如果 两个方法都是抽象方法, // 另外一个解决办法:实现 方法 或 将类声明为 abstract
// 2. 类优先: Student 类继承 Person 类 和 Named 接口,两者提供一样的 getName 方法 ----- 超类 Person.java ------ public class Person { private String name; public Person(String name) { this.name = name; } public String getName() { return name; } } ----- 接口 Named.java ------ public interface Named { default String getName() { return getClass().getName() + "_" + hashCode(); } } ----- 子类 Student.java ------ public class Student extends Person implements Named { public Student(String name) { super(name); } } ----- 测试类 ------ Student harry = new Student("Harry Porter"); System.out.println(harry.getName()); // 输出: "Harry Porter", 超类 Person 有 getName(),接口 Named 有 getName(),当方法冲突时,使用类 Person 的方法
6.1.7. 接口与回调
回调 Callbacks:常见的程序设计模式,可以指定某个特定事件发生时应该采取的动作。如 Timer 类
public class TimerTest { public static void main(String[] args) { ActionListener listener = new TimerPrinter(); // 定义一个接口的变量 listener,必须引用实现了该接口的类的对象; // construct a timer that calls the listener Timer t = new Timer(10000, listener); t.start(); JOptionPane.showMessageDialog(null, "Quit program?"); System.exit(0); } } class TimerPrinter implements ActionListener { @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + new Date()); Toolkit.getDefaultToolkit().beep(); } }
6.1.8. Comparator 接口
// 使用 Comparator 接口的 Compare 方法,可以自定义排序。有别于 Comparable 接口的 Comparable 排序
public class ComparatorTest { public static void main(String[] args) { String[] friends = {"Peter", "Paul", "May", "Albert"}; Arrays.sort(friends); System.out.println(Arrays.toString(friends)); // 按照 字典顺序 排序,[Albert, May, Paul, Peter] Arrays.sort(friends, new LengthComparator()); System.out.println(Arrays.toString(friends)); // 按照 字符串长度 排序,[May, Paul, Peter, Albert] } } class LengthComparator implements Comparator<String> { @Override public int compare(String o1, String o2) { return o1.length() - o2.length(); } }
6.1.9. 对象克隆
Employee original = new Employee("John Public", 50000); // 1) make a copy, original also changed Employee copy = original; copy.raiseSalary(10); // original also changed // 2) clone, then original is not changed Employee copy = original.clone(); copy.raiseSalary(10); // original is not changed
对于每一个类,需要确定的一个思路是:
- 1)默认的 clone 方法是否满足要求;
- 2)是否可以在可变的子对象上调用 clone 来修补默认的 clone 方法;
- 3)是否不该使用 clone ---- (默认,一般不使用);
如果选择 1)或 2),则该类必须:
- 1)实现 Cloneable 接口;
- 2)重新定义 clone 方法,并指定为 public;
!!! Object.clone 仅提供 浅copy,其特点是: 1. 如果 原对象 和 浅clone 共享的子对象是不可变的(如 String 类);或者 子对象 一直包含不变的常量,没有更改器改变它,也没有方法会生成它的引用,那么这种共享是安全的; 2. 如果 子对象 是可变的,那么 任意改变一个,两者都会改变,这就不适用于 浅copy 了; => 需要定义一个 deep copy 另外,Object.clone 方法是 protected,子类只能调用该方法来 clone 自己的对象,必须将 clone() 重新定义为 public,才能允许所有的方法来克隆对象。 深copy (即使 浅copy 满足要求,下面的第一步还是需要实现): 1). 浅 copy: a) 实现 Cloneable 接口;b) 重新定义 clone 方法为 public; c)调用 super.clone(); 2). + clone 对象中可变的实例字段;
注意,Cloneable 接口是标记接口,其内部没有任何方法,唯一用途就是允许使用 instanceof 进行类型查询。
详细的描述:https://www.cnblogs.com/bruce-he/p/17271648.html
// 实现 Cloneable 接口 class Employee implements Cloneable { ... public Employee clone() throws CloneNotSupportedException { // 1) 重新定义 Clone(); 2)方法声明为 public // 3)调用 super.clone()方法,其调用 Object.clone(),并复制所有字段。Object.clone() 检查方法是否可以 clone,并是否实现了 Cloneable 接口,否则 throw 一个异常 Employee cloned = (Employee) super.clone(); // 将返回的 Object 对象转回成实际类型对象 // 4) clone 对象中可变的实例字段 (Employee 类提供了一个可设置的 setHireDay 方法),并 加入到 cloned 对象 cloned.hireDay = (Date) hireDay.clone(); return cloned; } }
所有的数组类型 都有一个 public 的 clone 方法,所以可以直接调用,不需要重新定义
int[] luckyNumbers = {2, 3, 5, 7, 11, 13}; int[] cloned = luckyNumbers.clone(); cloned[5] = 12; // doesn't change luckyNumbers[5] // [2, 3, 5, 7, 11, 13] System.out.println(Arrays.toString(luckyNumbers)); // [2, 3, 5, 7, 11, 12] System.out.println(Arrays.toString(cloned));
6.2 lambda 表达式
6.2.2. lambda 表达式
// 案例:字符串按长度排序的几种实现方式 // 方式1). 一般情况下,频繁调用 first.length() - second.length(); // 方式2). 通过实现 Comparator 接口 class LengthComparator implements Comparator<String> { public int compare(String first, String second) { return first.length() - second.length(); } } Arrays.sort(strings, new LengthComparator()); // 调用 // 方式3). Lambda 表达式: (String first, String second) -> first.length() - second.length();
表达式,3个部分构成:(参数)-> { 表达式 };
// 1)如果表达式不止一行,需要 {} + return (String first, String second) -> { if (first.length() < second.length()) return -1; else if (first.length() > second.length()) return 1; else return 0; } 2)无参数时,依然要有 空括号; () -> { for (int i = 100; i >= 0; i--) System.out.println(i); } 3)如可以推导出一个 lambda 表达式的参数类型,则可忽略其类型; Comparator<String> comp = (first, second) // same as (String first, String second) -> first.length() - second.length(); 4)如方法仅一个参数,且该参数的类型可推导出,那么 可以省略 (); ActionListener listener = event -> // Instead of (event) -> ... or (ActionEvent event) -> ... System.out.println("The time is " + new Date()); 5)无需指定 lambda 表达式的返回类型,总会根据上下文推导出来; (String first, String second) -> first.length() - second.length() 6)如只在某些分支返回值,另外的不返回,则不合法; (int x) -> { if (x >=0 ) return 1; } // illegal,仅在 x>0 时返回值
// 案例: 对数组的排序 Arrays.sort()
var planets = new String[] {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"};
// 1). 默认的按字典排序 Arrays.sort(planets); // 2). 按长度排序,lambda 表达式 Arrays.sort(planets, (first, second) -> first.length() - second.length());
6.2.3. 函数式接口
函数式接口(Functional Interface):有且仅有一个抽象方法,需要这种接口的对象时,就可以提供一个 lambda 表达式。
Functional interface = Interface with a single abstract method.
Lambda expression can be used whenever a functional interface value is expected.
Conversion to a functional interface is the only thing that you can do with a lambda expression.
函数式接口可以被隐式转换为 lambda 表达式。
Lambda 表达式的初衷:取一个 Lambda 表达式,1)把它保存到一个接口类型的变量中;2)传递到一个需要接口类型值的方法;
1)函数式接口:
JDK 1.8 之前已有的函数式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
JDK 1.8 新增加的函数接口:
- java.util.function
// 场景1:函数式接口 Comparator 的应用 -- 排序
String[] planets = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"}; Arrays.sort(planets, (first, second) -> first.length() - second.length()); => 解析: 此处的 Arrays.sort() 方法调用的是 Arrays 类的 public static <T> void sort(T[] a, Comparator<? super T> c) {...} 其第二个参数是一个 Comparator 的实例,Comparator 是只有一个抽象方法的接口,所以可以提供一个 lambda 表达式。具体解释参照下面的(ActionListener、Comparator 接口为何是 函数式接口 的解释)
// 场景2:函数式接口 ActionListener 的应用 -- 事件发生器
lambda 表达式可以转换为接口,其解释为:下面例子的 Timer() 类的第二个参数是 ActionListener 接口,故可以将 lambda 表达式作为一个接口传入。
另外, lammbda 表达式代码相比实现了 ActionListener 接口的类比起来更易读:
// 方式一:通过实现 ActionListener 接口来实现 class TimePrinter implements ActionListener { @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + Instant.ofEpochMilli(e.getWhen())); Toolkit.getDefaultToolkit().beep(); } } // 调用 Timer t = new Timer(1000, listener); t.start();
// 方式二:通过 lambda 表达式实现(相比方式一,更易读) var t = new Timer(1000, event -> { // Timer(int delay, ActionListener listener),其第二个参数也是 ActionListener,最终还是将 lambda 表达式传入 System.out.println("The timer is " +Instant.ofEpochMilli(e.getWhen())); Toolkit.getDefaultToolKit().beep(); }); t.start();
// ActionListener、Comparator 接口为何是 函数式接口 的解释:
1)ActionListener: 查看源码,它是仅一个 public 方法且没有方法实现,在接口中默认是抽象方法 public interface ActionListener extends EventListener { public void actionPerformed(ActionEvent e); } 2)Comparator 接口: public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); default Comparator<T> reversed() { ... } ... public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() { ... } ... } // 查看 Comparator 的源码,其包含的方法如上: a)boolean equals() 源自 Object.euqals(Object),是对其的重写,已经有方法实现; b)其他 default、static 等方法都有具体实现; c)所以,推断出仅有 compare() 方式是没有方法实现的 抽象方法;
综合场景1、2:在 Java 中,对 lambda 表达式所能做的也只是转换为 函数式接口。
其他较常用的函数式接口包含:java.util.function.Predicate, java.util.function.Supplier
public class Java8Tester { public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6); System.out.println("Print out all data:"); eval(list, n -> true); System.out.println("Print out all data > 3:"); eval(list, n -> n>3); } public static void eval(List<Integer> list, Predicate<Integer> predicate) { // Interface Predicate<T> has an abstract method boolen test(T t) for (Integer n : list) { if (predicate.test(n)) System.out.print(n + " "); } System.out.println(); } }
6.2.4 方法引用 Method References
// 应用场景:只要出现一个定时器事件,就打印这个事件对象
// 实现方式1: 函数式接口 var timer = new Timer(1000, event -> System.out.println(event)); // 实现方式2(Better solution): 方法引用 var timer = new Timer(1000, System.out::println); // 解析:指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。此例中,会生成一个 ActionListener,它的 actionPerformed(ActionEvent e) 方法要调用 System.out.println(e)。
方法引用的 3 种情况
|
|
object::instanceMethod |
等价于 向方法传递参数的 lambda 表达式 如 System.out::println,对象是 System.out,等价于:x -> System.out.println(x) |
Class::instanceMethod |
第一个参数会成为方法的隐式参数 如 String::compareToIgnoreCase,等价于: (x, y) -> x.compareToIgnoreCase(y) |
Class::staticMethod |
所有参数都传递到静态方法 如 Math::pow,等价于: (x,y) -> Math.pow(x, y) |
注意:只有当 lambda 表达式的体只调用一个方法而不做其他操作时,才能把 lambda 表达式重写为方法引用。
如, s -> s.length == 0; // 这里有一个方法调用,还有一个比较,所以 不能使用方法引用。
// 关于方法引用的其他注释 1. 类似于 lambda 表达式,方法引用不是一个对象。不过,为一个类型为函数式接口的变量赋值时会生成一个对象;
2. 类似于 lambda 表达式,方法引用不能独立存在,总是会转换为 函数式接口 的实例; 3. 包含对象的方法引用 与 等价的 lambda 表达式还有一个细微的差别。如 方法引用 separator::equals,如果 seperator 为 null,那么: 1)构造 separator::equals 时立即抛出 NullPointerException; 2)lambda 表达式 x -> separator.equals(x) 只在调用时才会抛出 NullPointerException;
还可以在方法引用中使用 this、super
this::equals // 等同于 x -> this.equals(x);
super::instanceMethod // 调用给定方法的超类版本
具体案例参考:https://www.w3cschool.cn/java/java-instance-method-reference.html
6.2.5 构造器引用
1. 构造器引用:Class::new
// 应用场景1: 将 字符串列表 转换为一个 对象列表
// 处理过程: 字符串 -> 流 -> 对象列表
ArrayList<String> names = ...; Stream<Person> stream = names.stream().map(Person::new); // map()会接受一个lambda 表达式,会为每个列表元素调用 Person(String) 构造器。Person::new, same as "s -> new Person(s)" List<Person> people = stream.collect(Collectors.toList());
2. 可以用数组元素建立构造器引用。 如 int[]::new // 它有一个参数,即数组的长度,等价于 lambda 表达式 "x -> new int[x]"
3. 无法构造泛型类型 T 的数组,因为构造任意类型的数组是不合法的。使用数组构造器就可以解决。
// 应用场景2:将 流 转换为 数组 Person[] people = stream.toArray(Person[]::new); // toArray() 调用这个构造器来得到一个有正确类型的数组,然后填充并返回这个数组
案例 参考: https://www.runoob.com/java/java8-method-references.html
6.2.6. 变量作用域 Variable Scope
lambda 表达式有 3 个部分:
- 1. A block of code 一个代码块;
- 2. Parameters 参数;
- 3. Value for the free variables 自由变量的值(不是在代码中定义的变量); // 只能引用 值不会改变的变量
lambda 表达式的几个规则:
- 1)变量必须是最终变量(即初始化后,不会再赋予新值)。这是因为 Java 实现了闭包,可以捕获外围的变量值,使得外围方法在退出时,变量依然存在,但约束是该变量不可变;
- 2)表达式的体与嵌套块有相同的作用域。所以,方法中不能有同名的局部变量,声明与一个局部变量同名的参数或局部变量是不合法的;
- 3)lambda 表达式不能有同名的局部变量(因为归属一个方法);
- 4)表达式中使用的 this,是调用 创建这个表达式的 方法的 this 参数;
- 5)不能改变 捕获的值;
// 1) lambda 表达式中的 i 根据 for 循环不断赋予新值 => error public static void repeat(String text, int count) { for (int i = 1; i <= count; i++) { ActionListener listener = event -> { System.out.println(i + ": " + text); } // Error: 1) Cannot refer to changing i new Timer(1000, listener).start(); } } // 2)变量冲突:variable first already defined Path first = Paths.get("/usr/bin"); Comparator<String> comp = (first, second) -> first.length() - second.length(); // 4) this public class Application() { public void init() { ActionListener listener = event -> { System.out.println(this.toString()); // this.toString() 会调用 Application 对象的 toString() ... } } } // 5) 不能改变捕获的值 public static void countDown(int start, int delay) { ActionListener listener = event -> { start--; // ERROR: 5) can't mutate captured variable System.out.println(start); }; new Timer(delay, listener).start(); }
6.2.7. 处理 lambda 表达式
使用 lambda 表达式的重点是 延迟执行(deferred execution)。适用的场景包括:
- 在一个单独的线程中运行代码;
- 多次运行代码;
- 在算法的适当位置运行代码(如,排序中的比较操作);
- 发生某种情况时执行代码(如,点击一个按钮,数据到达等);
- 只在必要时才运行;
// Example 1: Repeat an action n times: // step1: 解析成如下方法: repeat(10, () -> System.out.println("Hello, World!")); // step2: 实现: Pick a functional interface for the second parameter: public static void repeat(int n, Runnable action) { for (int i = 0; i < n: i++) { action.run(); } // 调用 action.run() 时,会执行 lambda 表达式的主体 }
// Example2: 输出动作出现在哪一次的迭代中 // step1: 解析,需要一个合适的函数式接口,其中的方法有一个 int 参数且返回类型为 void,故选择接口 public interface IntConsumer { void accept(int value); } // step2: 改进 repeat 方法。 To pass the count to the action, pick a functional interface form java.utilfunction package: public static void repeat(int n, IntConsumer action) { for (int i = 0; i < n; i++) { action.accept(i); } } // Called like this: repeat(10, i -> System.out.println("Countdown: " + (9-i)));
6.2.8 再谈 Comparator 接口
// the static method comparing makes a comparator from a key extractor function: Arrays.sort(people, Compartor.comparing(Person::getName)); // 按 getName 的名字排序 // if the key is a primitive type, use comparingInt or comparingDouble to avoid boxing: Arrays.sort(words, Comparator.comparingInt(String::length)); // 按单次长度比较,比单纯使用 comparing 少一次装箱过程 // the default method then Comparing chains comparators: Arrays.sort(staff, Comparator.comparing(Employee::getName) .thenComparing(Person::getSalary));
6.3 内部类
内部类:定义在另一个类中的类。主要原因:
- 1)内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据;
- 2)内部类可以对同一个包中的其他类隐藏;
- 3)当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous) 内部类比较便捷(Before lambda expressions, inner classes were commonly used for callbacks);
- !!! Java 8 中,内部类的很多用例已经被 Lambda 表达式所取代,不过偶尔还会有内部类用于其他一些用途,如限定类的作用域和信息隐藏
6.3.1 使用内部类访问对象状态:
内部类既可以访问自身的数据字段,也可以访问 创建它的外围类对象的数据字段
类与各sample的对应:内部类(TalkingClock) -> 局部内部类(TalkingClock2)-> 外部方法访问变量(TalkingClock3)-> 匿名内部类(TalkingClock4)-> Lambda 表达式(InnerClassLambda ))
// 内部类的用例
public class InnerClass { public static void main(String[] args) { TalkingClock clock = new TalkingClock(2000, true); clock.start(); JOptionPane.showMessageDialog(null, "Quit program?"); // keep program running until user selects "OK" System.exit(0); } } class TalkingClock { private int interval; private boolean beep; public TalkingClock(int interval, boolean beep) { this.interval = interval; this.beep = beep; } public void start() { var listener = new TimePrinter(); var t = new Timer(interval, listener); t.start(); } public class TimePrinter implements ActionListener { // 内部类,定义在 TalkingClock 内部的类 @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + new Date()); // beep 引用了创建 TimePrinter 类 的外部类 TalkingClock 对象的字段,简单理解即为 outer.beep,此 outer 为虚构语法,正确的是 TalkingClock.this,详见 6.3.2 节第1点说明 if (beep) Toolkit.getDefaultToolkit().beep(); } } }
// 外围类的引用在构造器中设置。编译器会修改所有的内部类的构造器, 添加一个对应外围类引用的参数。因为 TimePrinter 类没有定义构造器,所以编译器为这个类生成了一个无参数构造器,其代码如下所示: public TimePrinter(TalkingClock clock) { // automatically generated code outer = clock; }
6.3.2 内部类的特殊语法规则
1. 使用外围类引用的正规语法是: OuterClass.thispublic void actionPerformed(ActionEvent e) { ... if (TalkingClock.this.beep) { Toolkit.getDefaultToolkit().beep(); } } 2. 采用下列语法更明确地编写内部对象的构造器: outerObject.new InnerClass(construction parameters) ActionListener listener = this.new TimePrinter(); // 在这里,最新构造的 TimePrinter 对象的外围类引用被设置为创建内部类对象的方法的 this 引用 3. 也可以通过显式地命名将外围类引用设置为其他的对象。由于 TimePrinter 是一个公共内部类,在外围类的作用域之外,可以通过 OuterClass.InnerClass 引用内部类: TalkingClock jabberer = new TalkingClock(1000, true); TalkingClock.TimePrinter listener = jabberer.new TimePrinter(); // 语法规则: OuterClass.InnerClass object = outerClassObject.new InnerClass(); 4. 其他规范: 1)内部类中声明的所有静态字段必须是 final,并初始化为一个编译时常量; 2)内部类的 static 方法只能访问外围类的静态字段和方法;
6.3.4 局部内部类(Local Inner Classes)
局部类不能用 public 或 private 访问说明符进行声明。作用域被限定在声明这个局部类的块中。
局部类优势:对外界完全隐藏。 比内部类更隐蔽。即使 TalkingClock 类中的其他代码也不能访问它。除 start 方法之外, 没有任何方法知道 TimePrinter 类的存在。
// 6.3.1 内部类的用例中,TimePrinter 类仅在 start 方法中创建这个类型的对象时使用了一次,那么可以 在一个方法中定义局部类。 class TalkingClock2 { private int interval; private boolean beep; public TalkingClock2(int interval, boolean beep) { this.interval = interval; this.beep = beep; } public void start() {
// 局部内部类,无 public or private,对比于 6.3.1 的内部类,将 类的定义放置在类 TalkingClock 的一个方法中
class TimePrinter implements ActionListener { @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + new Date()); if (beep) { Toolkit.getDefaultToolkit().beep(); } } } ActionListener listener = new TimePrinter(); Timer t = new Timer(interval, listener); t.start(); } }
6.3.5 由外部方法访问变量
局部类的优点:
- 1)可以访问包含它们的外部类的字段;
- 2)可以访问 定义为 final 的 局部变量;
class TalkingClock3 { // 相对于 TalkingClock2 (6.3.4),更是将变量 interval, beep 作为方法 start() 的参数,少了定义及初始化的过程。外部方法就可以直接访问这两个变量
public void start(int interval, final boolean beep) {
class TimePrinter implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep(); // 内部类 TimePrinter 可以访问上面定义为 final 的变量 beep
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval, listener);
t.start();
}
}
// 调用:
TalkingClock3 clock3 = new TalkingClock3();
clock3.start(1000, true);
6.3.6 匿名内部类(Anonymous Inner Classes)
class TalkingClock4 { public void start(int interval, boolean beep) { // new ActionListener() {...}; => {} 里面定义的就是 匿名内部类。
// 相比较 TalkingClock3(局部内部类),TimePrinter 只是实现 ActionListener 接口,且对外部隐藏。那么,无须定义该类,转而直接实现 ActionListener 接口 var listener = new ActionListener() { // 定义 listener 接口变量,new ActionListener() {...} 匿名内部类,实现了 ActionListener 接口,故可以赋值 @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + new Date()); if (beep) Toolkit.getDefaultToolkit().beep(); } }; var t = new Timer(interval, listener); t.start(); } }
使用内部类时,如果只想创建这个类的一个对象,甚至不需要为类指定名字,该类被称为 匿名内部类(annoymous inner class)。
1. Java 中可以实现一个类中包含另外一个类,且不需要提供任何的类名直接实例化; 2. 匿名类是不能有名字的类,它们不能被引用,只能在创建时用 new 语句来声明它们; 3. 匿名类不能有构造器,实际是将构造器参数传递给超类构造器。具体地说,只要内部类实现一个接口,就不能有任何构造参数; 4. 匿名类通常的语法格式: // SuperType:1)可以是接口(如 ActionListener),内部类就需要实现这个接口(例子参照上面的 TalkingClock4);
2)可以是类,内部类就需要扩展这个类; new SuperType(construction parameters) { inner class methods and data } 匿名类的格式: class outerClass { object1 = new Type(parameterList) { // 定义一个匿名类 // 匿名类代码 }; }
5. 在内部类实现接口的时候,不能有任何构造参数,格式如: new InterfaceType() { methods and data }
6. 对比 构造类对象 与 构造匿名内部类的差异: Person queen = new Person("Mary"); // 构造一个类的对象 Person count = new Person("Dracula") {...}; // 构造了一个扩展了类的匿名内部类的对象,如果构造参数列表() 后跟一个 {},那么就是定义 匿名内部类
7. 一般常用 匿名内部类实现事件监听和其他回调,如今更多使用 lambda 表达式;
// 匿名内部类 => 现在更多实用 lambda 表达式 class TalkingClock5 { public void start(int interval, boolean beep) { Timer t = new Timer(interval, event -> { // Lambda 表达式 System.out.println("At the tone, the time is " + new Date()); if (beep) { Toolkit.getDefaultToolkit().beep(); } }); t.start(); }
6.3.7 静态内部类
1) static inner class = inner class without reference to creating object 没有外部对象引用的内部类 2) useful for a private class that doesn't need to know the creating object 静态内部类的使用规则:只要内部类不需要访问外围类对象
静态内部类的其他特性: 1)与常规内部类不同,静态内部类可以有 静态字段 和 方法; 2)声明在接口中的内部类自动是 static 和 public; // Example:返回一个数组的最大、最小值,1)传统方式:使用2个方法分别计数最大、最小,数组遍历 2次;
2)better:只遍历一次数组,同时计数出最大、最小。那么返回值必须是两个数,正好使用类 Pair class ArrayAlg { public static class Pair { public double first; // 特性1:静态内部类的字段(静态字段,Java 8 以后不用特别指定为 final) public double second;
public Pair(double first, double second) {
this.first = first;
this.second = second;
}
public double getFirst() { return first; } // 特性2: 静态内部类的方法(静态方法)
... } ... public static Pair minmax(double[] values) { // 定义方法提供给 ArrayAlg 调用 ... return new Pair(min, max); // no creating object } } // Called as: ArrayAlg.Pair p = ArrayAlg.minmax(data); // 由于 Pair 这个词很大众化,所以需要将它定义为 ArrayAlg 的公共内部类,那么就可以通过 ArrayArg.Pair 来调用了
// 因为 ArrayAlg 直接调用 minmax() 而没有创建对象,就必须将 minmax() 声明为 static
// minmax() 方法中,使用 return new Pair() 而未创建对象,那么 Pair 类也必须声明为 static
6.4 服务加载器(Skipped)
6.5 代理(Skipped)
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· DeepSeek “源神”启动!「GitHub 热点速览」
· 上周热点回顾(2.17-2.23)