第六章 接口、lambda 表达式与内部类。
接 口
接口概念
Arrays类中的 sort 方法承诺可以对对象数组进行排序,但要求满 足下列前提:对象所属的类必须实现了 Comparable 接口。 下面是 Comparable 接口的代码:
public interface Comparable { int compareTo(Object other); }
这就是说, 任何实现 Comparable 接口的类都需要包含 compareTo方法,并且这个方法的参 数必须是一个 Object 对象,返回一个整型数值。
有人认为, 将 Arrays 类中的 sort 方法定义为接收一个 Comparable[ ] 数组就可以在 使用元素类型没有实现 Comparable 接口的数组作为参数调用 sort 方法时, 由编译器给出 错误报告。但事实并非如此。在这种情况下, sort 方法可以接收一个 Object[ ] 数组, 并 对其进行笨拙的类型转换:
// Approach used in the standard library not recommended if (((Comparable) a[i]).compareTo(a[j]) > 0) { // rearrange a[i] and a[j] }
如果 a[i] 不属于实现了 Comparable 接口的类, 那么虚拟机就会抛出一个异常。 程序清单 6-1 给出了对一个 Employee 类(程序清单 6-2 ) 实例数组进行排序的完整代码, 用于对一个员工数组排序。
//程序清单 6-1 interfaces/EmployeeSortTest.java package interfaces; import java.util.*; /** * This program demonstrates the use of the Comparable interface. * @version 1.30 2004-02-27 * @author Cay Horstmann */ public class EmployeeSortTest { public static void main(String[] args) { Employee[] staff = new Employee[3]; staff[0] = new Employee("Harry Hacker", 35000); staff[1] = new Employee("Carl Cracker", 75000); staff[2] = new Employee("Tony Tester", 38000); Arrays.sort(staff); // print out information about all Employee objects for (Employee e : staff) System.out.println("name=" + e.getName() + ",salary=" + e.getSalary()); } }
//程序清单 6-2 interfaces/Employee.java package interfaces; public class Employee implements Comparable<Employee> { private String name; private double salary; public Employee(String n, double s) { name = n; salary = s; } public String getName() { return name; } public double getSalary() { return salary; } public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } /** * Compares employees by salary * @param other another Employee object * @return a negative value if this employee has a lower salary than * otherObject, 0 if the salaries are the same, a positive value otherwise */ public int compareTo(Employee other) { return Double.compare(salary, other.salary); } }
接口的特性
接口不是类,尤其不能使用 new运算符实例化一个接口:
x = new Comparable(. . .); // ERROR
然而, 尽管不能构造接口的对象,却能声明接口的变量:
Comparable x; // OK
接口变量必须弓I用实现了接口的类对象:
x = new Employee(. . .); // OK provided Employee implements Comparable
接下来, 如同使用 instanceof检查一个对象是否属于某个特定类一样, 也可以使用 instance 检查一个对象是否实现了某个特定的接口:
if (anObject instanceof Comparable) { . . . }
与可以建立类的继承关系一样,接口也可以被扩展。
可以将接口方法标记为 public, 将域标记为 public static final。有些程序员出于习 惯或提高清晰度的考虑, 愿意这样做。
接口与抽象类
为什么 Java 程 序设计语言还要不辞辛苦地引入接口概念? 为什么不将 Comparable 直接设计成如下所示的 抽象类。
abstract class Comparable // why not? { public abstract int compareTo(Object other); }
然后,Employee 类再直接扩展这个抽象类, 并提供 compareTo方法的实现:
class Employee extends Comparable // why not? { public int compareTo(Object other) { . . . } }
非常遗憾, 使用抽象类表示通用属性存在这样一个问题: 每个类只能扩展于一个类。假 设 Employee类已经扩展于一个类, 例如 Person, 它就不能再像下面这样扩展第二个类了:
class Employee extends Person, Comparable // Error
但每个类可以像下面这样实现多个接口:
class Employee extends Person implements Comparable // OK
静态方法
目前为止, 通常的做法都是将静态方法放在伴随类中。在标准库中, 你会看到成对出现 的接口和实用工具类, 如 Collection/Collections 或 Path/Paths。
下面来看 Paths 类, 其中只包含两个工厂方法。可以由一个字符串序列构造一个文件或 目录的路径, 如 Paths.getfjdk1.8.0", "jre", "bin")。在 Java SE 8 中, 可以为 Path 接口增加以 下方法:
public interface Path { public static Path get(String first, String... more) { return Fi1eSystems.getDefault().getPath(first, more); } . . . }
这样一来, Paths 类就不再是必要的了。
默认方法
可以为接口方法提供一个默认实现。必须用 default 修饰符标记这样一个方法。
public interface Comparable<T> { default int compareTo(T other) { return 0; } // By default, all elements are the same }
当然, 这并没有太大用处, 因为 Comparable 的每一个实际实现都要覆盖这个方法。不过 有些情况下, 默认方法可能很有用。
解决默认方法冲突
Java 的相应规则要简单得多。规则如下:
1 ) 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会 被忽略。
2 ) 接口冲突。 如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且 参数类型(不论是否是默认参数)相同的方法, 必须覆盖这个方法来解决冲突。
当然,如果两个接口都没有为共享方法提供默认实现, 那么就与 Java SE 8之前的 情况一样,这里不存在冲突。 实现类可以有两个选择:实现这个方法,或者干脆不实现。 如果是后一种情况,这个类本身就是抽象的。
接口示例
接口与回调
回调(callback) 是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发 生时应该采取的动作。
程序清单 6-3 给出了定时器和监听器的操作行为。在定时器启动以后, 程序将弹出一个 消息对话框, 并等待用户点击 Ok 按钮来终止程序的执行。在程序等待用户操作的同时, 每 隔 10 秒显示一次当前的时间。
运行这个程序时要有一些耐心。程序启动后, 将会立即显示一个包含“ Quit program?” 字样的对话框, 10 秒钟之后, 第 1 条定时器消息才会显示出来。
需要注意, 这个程序除了导入javax.swing.* 和 java.util.* 外, 还通过类名导入了javax. swing.Timer。 这就消除了javax.swing.Timer 与 java.util.Timer 之间产生的二义性。 这里的 java. util.Timer 是一个与本例无关的类, 它主要用于调度后台任务。
//程序清单 6-3 timer/TimerTest.java package timer; /** @version 1.00 2000-04-13 @author Cay Horstmann */ import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import javax.swing.Timer; // to resolve conflict with java.util.Timer public class TimerTest { public static void main(String[] args) { ActionListener listener = new TimePrinter(); // construct a timer that calls the listener // once every 10 seconds Timer t = new Timer(10000, listener); t.start(); JOptionPane.showMessageDialog(null, "Quit program?"); System.exit(0); } } class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { Date now = new Date(); System.out.println("At the tone, the time is " + now); Toolkit.getDefaultToolkit().beep(); } }
Comparator 接口
现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。 肯定不能让 String类用两种不同的方式实现 compareTo方法— —更何况,String类也不应由我们来修改 。
要处理这种情况,ArrayS.Sort 方法还有第二个版本, 有一个数组和一个比较器 (comparator )作为参数, 比较器是实现了 Comparator 接口的类的实例。
public interface Comparators { int compare(T first, T second); }
要按长度比较字符串,可以如下定义一个实现 Comparator<String> 的类:
class LengthComparator implements Comparator<String> { public int compare(String first, String second) { return first.lengthO - second.lengthO; } }
具体完成比较时,需要建立一个实例:
Comparator<String> comp = new LengthComparator() ; if (conp.compare(words[i], words[j]) > 0) ...
将这个调用与 words[i].compareTo(words[j]) 做比较。这个 compare方法要在比较器对象 上调用, 而不是在字符串本身上调用。
对象克隆
看 Object 类如何实现clone。它对于这个对象一无所知, 所以只能逐个域地进行拷贝。 如果 对象中的所有数据域都是数值或其他基本类型,拷贝这些域没有任何问题、 但是如果对象包 含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对 象仍然会共享一些信息。
Cloneable 接口是 Java 提供的一组标记接口 (tagging interface) 之一。(有些程序员 称之为记号接口 ( maHcer interface))。应该记得,Comparable 等接口的通常用途是确保一个类实现一个或一组特定的方法。标记接口不包含任何方法; 它唯一的作用就是允许 在类型查询中使用 instanceof:
if (obj instanceof Cloneable) . . .
建议你自己的程序中不要使用标记接口。
程序清单 64中的程序克隆了 Employee 类(程序清单 6-5 ) 的一个实例,然后调用两个 更改器方法。raiseSalary 方法会改变 salary 域的值, 而 setHireDay 方法改变 hireDay 域的状 态。这两个更改器方法都不会影响原来的对象, 因为 done 定义为建立一个深拷贝。
//程序清单 6-4 clone/CloneTest.java package clone; /** * This program demonstrates cloning. * @version 1.10 2002-07-01 * @author Cay Horstmann */ public class CloneTest { public static void main(String[] args) { try { Employee original = new Employee("John Q. Public", 50000); original.setHireDay(2000, 1, 1); Employee copy = original.clone(); copy.raiseSalary(10); copy.setHireDay(2002, 12, 31); System.out.println("original=" + original); System.out.println("copy=" + copy); } catch (CloneNotSupportedException e) { e.printStackTrace(); } } }
//程序清单 6-5 clone/Employee.java package clone; import java.util.Date; import java.util.GregorianCalendar; public class Employee implements Cloneable { private String name; private double salary; private Date hireDay; public Employee(String n, double s) { name = n; salary = s; hireDay = new Date(); } public Employee clone() throws CloneNotSupportedException { // call Object.clone() Employee cloned = (Employee) super.clone(); // clone mutable fields cloned.hireDay = (Date) hireDay.clone(); return cloned; } /** * Set the hire day to a given date. * @param year the year of the hire day * @param month the month of the hire day * @param day the day of the hire day */ public void setHireDay(int year, int month, int day) { Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime(); // Example of instance field mutation hireDay.setTime(newHireDay.getTime()); } public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } public String toString() { return "Employee[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]"; } }
lambda 表达式
为什么引入 lambda 表达式
lambda 表达式是一个可传递的代码块, 可以在以后执行一次或多次。具体介绍语法(以 及解释这个让人好奇的名字)之前,下面先退一步,观察一下我们在 Java 中的哪些地方用过 这种代码块。
将这个工作放在一个 ActionListener 的 actionPerformed方法中:
class Worker implements ActionListener { public void actionPerformed(ActionEvent event) { // do some work } }
想要反复执行这个代码时,可以构造 Worker 类的一个实例。然后把这个实例提交到一 个 Timer 对象。这里的重点是 actionPerformed方法包含希望以后执行的代码。
或者可以考虑如何用一个定制比较器完成排序。如果想按长度而不是默认的字典顺序对 字符串排序,可以向 sort 方法传人一个 Comparator 对象:
class LengthComparator implements Comparator<String> { public int compare(String first, String second) { return first.lengthQ - second.lengthO; } } . . . Arrays.sort(strings, new LengthComparatorO);
compare 方法不是立即调用。 实际上, 在数组完成排序之前, sort 方法会一直调用 compare 方法, 只要元素的顺序不正确就会重新排列元素。将比较元素所需的代码段放在 sort 方法中, 这个代码将与其余的排序逻辑集成(你可能并不打算重新实现其余的这部分 逻辑) 。
lambda 表达式的语法
再来考虑上一节讨论的排序例子。我们传人代码来检查一个字符串是否比另一个字符串 短。这里要计算:
first.lengthO - second.length()
first 和 second 是什么? 它们都是字符串。Java 是一种强类型语言,所以我们还要指定它 们的类型:
(String first, String second) -> first.lengthO - second.length()
这 就 是 你 看 到 的 第 一 个 表 达 式。lambda 表达式就是一个代码块, 以及必须传人 代码的变量规范。
如果一个 lambda 表达式只在某些分支返回一个值, 而在另外一些分支不返回值, 这是不合法的。例如,(int x)-> { if(x>= 0) return 1;} 就不合法。
程序清单 6-6中的程序显示了如何在一个比较器和一个动作监听器中使用 lambda 表达式。
//程序清单 6-6 lambda/LambdaTest.java package lambda; import java.util.*; import javax.swing.*; import javax.swing.Timer; /** * This program demonstrates the use of lambda expressions. * @version 1.0 2015-05-12 * @author Cay Horstmann */ public class LambdaTest { public static void main(String[] args) { var planets = new String[] { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" }; System.out.println(Arrays.toString(planets)); System.out.println("Sorted in dictionary order:"); Arrays.sort(planets); System.out.println(Arrays.toString(planets)); System.out.println("Sorted by length:"); Arrays.sort(planets, (first, second) -> first.length() - second.length()); System.out.println(Arrays.toString(planets)); var timer = new Timer(1000, event -> System.out.println("The time is " + new Date())); timer.start(); // keep program running until user selects "OK" JOptionPane.showMessageDialog(null, "Quit program?"); System.exit(0); } }
函数式接口
对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达 式。这种接口称为函数式接口 (functional interface)。
为了展示如何转换为函数式接口,下面考虑 Arrays.sort 方法。它的第二个参数需要一个 Comparator 实例, Comparator 就是只有一个方法的接口, 所以可以提供一个 lambda 表达式:
Arrays.sort(words, (first, second) -> first.lengthO - second.lengthO);
在底层, Arrays.sort 方法会接收实现了 Comparator<String> 的某个类的对象。 在这个对象上调用 compare方法会执行这个 lambda 表达式的体。这些对象和类的管理完全取决于具 体实现, 与使用传统的内联类相比,这样可能要高效得多。最好把 lambda 表达式看作是一 个函数,而不是一个对象, 另外要接受 lambda 表达式可以传递到函数式接口。
方法引用
假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:
Timer t = new Timer(1000, event -> System.out.println(event));
但是,如果直接把 println 方法传递到 Timer 构造器就更好了。具体做法如下:
Timer t = new Timer(1000, Systei.out::println);
表达式 System.out::println 是一个方法引用(method reference), 它等价于 lambda 表达式 x 一> System.out.println(x)。
再来看一个例子, 假设你想对字符串排序, 而不考虑字母的大小写。可以传递以下方法 表达式:
Arrays.sort(strings,String::conpareToIgnoreCase)
从这些例子可以看出,要用::操作符分隔方法名与对象或类名。主要有 3 种情况:
•object::instanceMethod
•Class::staticMethod
•Class/ .instanceMethod
构造器引用
假设我们需要一个 Person 对象数组。Stream 接口有一个 toArray方法可 以返回 Object 数组:
Object[] people = stream.toArrayO; 不过,这并不让人满意。用户希望得到一个 Person 引用数组,而不是 Object 引用数组。 流库利用构造器引用解决了这个问题。可以把 Person[]::new?人 toArray方法:
Person[] people = stream.toArray(PersonD::new):
toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。
变量作用域
通常, 你可能希望能够在 lambda 表达式中访问外围方法或类中的变量。考虑下面这个 例子:
public static void repeatMessage(String text, int delay) { ActionListener listener = event -> { System.out.println(text); Toolkit.getDefaultToolkitO.beep(): }; new Timer(delay, listener).start(); }
来看这样一个调用:
repeatMessage("Hello", 1000); // Prints Hello every 1 ,000 milliseconds
现在来看 lambda 表达式中的变量 text。注意这个变量并不是在这个 lambda 表达式中定 义的。实际上,这是 repeatMessage方法的一个参数变量。
lambda 表达式有 3 个部分:
1 ) 一个代码块;
2 ) 参数;
3 ) 自由变量的值, 这是指非参数而且不在代码中定义的变量。
处理 lambda 表达式
使用 lambda 表达式的重点是延迟执行 deferredexecution ) 毕竟, 如果想耍立即执行代 码,完全可以直接执行, 而无需把它包装在一个lambda 表达式中。之所以希望以后再执行 代码, 这有很多原因, 如:
•在一个单独的线程中运行代码;
•多次运行代码;
•在算法的适当位置运行代码(例如, 排序中的比较操作);
•发生某种情况时执行代码(如, 点击了一个按钮, 数据到达, 等等);
•只在必要时才运行代码。
下面来看一个简单的例子。假设你想要重复一个动作 n 次。 将这个动作和重复次数传递 到一个 repeat方法:
repeat(10, 0 -> System.out.println("Hello, World!"));
要接受这个 lambda 表达式, 需要选择(偶尔可能需要提供)一个函数式接口。
再谈 Comparator
浙公网安备 33010602011771号