# 第六章 接口、lambda表达式与内部类
## 6.1 接口
### 6.1.1 概念
接口不是类,而是对希望符合这个接口的类的一组需求\
Arrays类中的sort方法承诺可以对对象数组进行排序,但是对象所属的类必须实现Comparable接口。\
接口代码如下
```
public interface Comparable
{
int compareTo(Object other);
}
```
接口中所有方法自动都是public 不必手动声明\
接口不会有实例字段,也不会有实例(可粗略划分为没有实例字段的抽象类)\
为了让类实现接口,需要下面两个步骤
1. 将类声明为实现给定接口(implements)
2. 对接口中的所有方法提供定义
```
class Employee implements Comparable<Employee>
public int compareTo(Employee other)
{
return Double.compare(salary,other.salary);//静态的Double.compare方法,<则返回负数,>则返回正数,否则返回0
}
```
因为Manager扩展了Employee,而Employee实现的是Comparable```<Employee>```,而不是Comparable```<Manager>```。如果Manager要实现compareTo,就必须做好准备比较Employee和Manager
如果不同子类中的比较有不同含义,就不应该比较不同类,compareTo方法应该进行检测
```
if(getClass()!=other.getClass()) throw new ClassCastException();
```
如果存在一个比较子类的通用算法,可以在超类设置一个compareTo方法,并设置为final。
### EmployeeSortTest.java
```
package javaSX.interfaces;
import java.util.*;
public class EmployeeSortTest {
public static void main(String[] args) {
var staff = new Employee[3];
staff[0] = new Employee("harry", 10000);
staff[1] = new Employee("harr", 2000);
staff[2] = new Employee("har", 3000);
Arrays.sort(staff);
for (Employee e : staff) {
System.out.println("name=" + e.getName() + ", salary=" + e.getSalary());
}
}
}
```
### Employee.java
```
package javaSX.interfaces;
public class Employee implements Comparable<Employee> {
private double salary;
private String name;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public double getSalary() {
return this.salary;
}
public void raiseSalary(double percent) {
double raise = this.salary * percent / 100;
this.salary += raise;
}
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
}
}
```
### 6.1.2 接口的属性
接口虽然不能实例化,但能够声明接口的变量,接口变量必须引用实现了这个接口的类的对象
```
x=new Comparable(...);//error
Comparable x;//Ok
x=new Employee(...)//Employee implements Comparable
```
接口可以extends接口,也可以包含常量\
接口中的方法都是public 接口的字段都是public static final\
类实现多个接口中间用逗号隔开
### 6.1.3 接口与抽象类
抽象类与接口的最大不同在于,抽象类是类,类不能够多重继承
### 6.1.5 默认方法
默认方法即在接口的方法加关键字default,默认方法可以调用其他方法
```
public interface Collection
{
int size();
default boolean isEmpty(){
return size()==0;
}
}
```
默认方法的重要用法在于“接口演化”,以Collection接口为例,Bag类实现它
```
public class Bag implements Collection
```
后来又为这个接口添加了一个stream方法,假设该方法不是默认方法,那么Bag类将不能通过编译,因为它没有实现新方法。若该新方法是默认方法,则在一个Bag实例上调用stream方法将调用Collection.stream方法
### 6.1.6 解决默认方法冲突
先在一个接口声明一个默认方法,又在超类或另一个接口定义同样的方法。冲突规则如下:
1. 超类优先 如果超类提供了一个具体方法,同名且具有相同类型参数的默认方法会被忽略。
2. 接口冲突,程序员需要二选一,即使一个是默认方法一个不是,如果两个方法都非默认(没有具体实现),则不存在冲突。类可以实现方法,也可以不实现(抽象类)
```
interface Person
{
default String getName()
{
return "";
}
}
interface Named
{
default String getName()
{
return getClass().getName()+"_"+hashCode();
}
}
class Student implements Person,Named
{
public String getName()
{
return Person.super.getName();
}
}
```
### 6.1.7 接口与回调
回调指触发某个事件应该采取的动作
### TimerTest.java
```
package javaSX.timer;
import java.awt.*;
import java.awt.event.*;
import java.time.*;
import javax.swing.*;
public class TimerTest {
public static void main(String[] args) {
var listener = new TimerPrinter();
var timer = new Timer(1000, listener);
timer.start();
JOptionPane.showMessageDialog(null, "Quit Program?");
System.exit(0);
}
}
class TimerPrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("Tone time is:" + Instant.ofEpochMilli(event.getWhen()));
Toolkit.getDefaultToolkit().beep();
}
}
```
### 6.1.8 Comparator接口
现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。\
Arrays.sort还有第二个版本,用一个数组和一个比较器作为参数,比较器是实现了Comparator接口的类的实例
```
public interface Comparator<T>
{
int compare(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();
String test={"Peter","Mary","Bob"};
Arrays.sort(test,comp);
```
数组变为{"Bob","Mary","Peter"}
### 6.1.9 对象克隆
```
var ori =new Employee("Mary",6000);
Employee copy=ori;
copy.raiseSalary(1000);//ori 一样会提高,因为原变量和副本都是同一个对象的引用
```
使用clone可以进行浅拷贝,像salary这种数值或基本类型字段,拷贝没有问题。但如果对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用。这样一来,原对象和克隆对象依然会共享一些信息。
```
Employee copy=ori.clone();
copy.raiseSalary(1000)
```
Cloneable接口没有指定clone方法,只作为一个标记接口(标记接口不包含任何方法,只允许在类型查询中使用instanceof)。对象对于克隆很固执,如果一个对象请求克隆,但是没有实现这个接口,就会生成一个检查型异常。下面为深拷贝
```
class Employee implements Cloneable
{
public Employee clone() throws CloneNotSupportException
{
Employee cloned=(Employee)super.clone();
cloned.hireDay=(Date)hireDay.clone();
return cloned
}
}
```
如果在一个对象上调用clone,但这个对象的类并没有实现Cloneable接口,Object类的clone方法就会抛出CloneNotSupportException
所有数组类型都有一个公共的clone方法,而非受保护的,可用来建立不会对原始数组造成影响的副本
```
int[] num={1,2,3,4};
int[] copy=num.clone();
copy[2]=12;//不会改变num数组
```
### CloneTest.java
```
package javaSX.clone;
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
var original = new Employee("John", 6000);
original.setHireDay(2000, 1, 2);
Employee copy = original.clone();
copy.raiseSalary(10);
copy.setHireDay(2002, 12, 11);
System.out.println("original=" + original);
System.out.println("copy=" + copy);
}
}
```
### Employee.java
```
package javaSX.clone;
import java.util.Date;
import java.util.GregorianCalendar;
public class Employee implements Cloneable {
private double salary;
private String name;
private Date hireDay;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
hireDay = new Date();
}
public Employee clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
public String getName() {
return name;
}
public double getSalary() {
return this.salary;
}
public void raiseSalary(double percent) {
double raise = this.salary * percent / 100;
this.salary += raise;
}
public void setHireDay(int year, int month, int day) {
Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime();
hireDay.setTime(newHireDay.getTime());
}
public String toString() {
return "Employee[name=" + name + ", salary=" + salary + ", hireDay=" + hireDay + "]";
}
}
```
## 6.2 lambda表达式
### 6.2.1 为什么引入lambda
lambda是一个可传递的代码块
### 6.2.2 lambda语法
为实现比较字符串长短而不是首字母
```
(String first,String second)
-> first.length() - second.length();
```
也可以加入{}
```
(String first,String second)->
{
if(first.length()==second.length()) return 1;
else return 0;
}
```
### 6.2.3 函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式,这种接口称为函数式接口。
下面展示如何转换为函数式接口,考虑Arrays.sort()方法。他的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口。所以可以提供一个lambda表达式。
```
Arrays.sort(words,
(String first,String second) -> first.length()-second.length());
```
Arrays.sort方法会接收实现了```Comparator<String>```的某个类的对象。在这个对象上,调用compare方法会执行这个lambda方法表达式的体。最好把lambda表达式看做一个函数,而不是对象。
lambda表达式可以转换为接口
```
var timer=new Timer(1000, event ->//event参数省略了类型(编译器根据上下文推导) 并且因为只有一个参数所以省略了括号
{
System.out.println("At the tone, the time is"
+Instant.ofEpochMilli(event.getWhen()));
Toolkit.getDefaultToolkit().beep();
});
```
### 6.2.4 方法引用
有时,lambda涉及一个方法。假如你希望只要出现一个定时器时间就打印这个事件对象。
```
var timer=new Timer(1000, event->System.out.println(event));
```
也可以直接把println方法传递到Timer构造器
```
var Timer=new Timer(1000, System.out::println);
```
表达式System.out::println是一个方法引用,他指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。在这个例子中,会生成一个ActionListener,它的actionPerformed(ActionEvent e)方法要调用System.out.println(e);
要想使用::运算符分割方法名与对象或类名,主要分3种情况
1. object::instanceMethod
方法引用等价于向方法传递参数的lambda表达式,对于System.out::println,对象是System.out,所以方法表达式等价于x->System.out.println(x)
2. Class::instanceMethod
第一个参数会成为方法的隐式参数。例如String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y)
3. Class::staticMethod
所有参数都传递到静态方法,Math::pow等价于(x,y)->Math.pow(x,y)
只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。
```
s->s.length==0//这里是一个方法调用,但还有一个比较,不能使用方法引用
```
可以在方法引用中使用this参数,例如this::equals等同于x->this.equals(x),使用super也是合法的。
### 6.2.5 构造器方法引用
```
Person[] people=stream.toArray(Person[]::new);
```
toArray方法调用这个构造器来得到一个有正确类型的数组,然后填充并返回这个数组
### 6.2.6 变量作用域
若希望能够在lambda表达式中访问外围方法或类中的变量,lambda会捕获外围变量,但只能捕获值不会改变的变量,变量在初始化后就不会赋新值,
## 6.3 内部类
内部类是定义在一个类中的类,内部类可以对本包的其他类隐藏,内部类方法可以访问定义这个类的作用域中的数据,包括私有数据。
```
public class TalkingClock
{
private int interval;
private boolean beep;
public TalkingClock(int interval, boolean bepp){...}
public void start()
public class TimerPrinter implementsActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("Time is"+Instant.ofEpochMilli(event.getWhen()));
if (beep) Tookit.getDefaultToolkit().beep();
}
}
}
```
每个TalkingClock不意味着都有一个Timeprinter实例字段,TimePrinter对象是由TalikingClock类的方法构造的。内部类声明的所有静态字段必须是final
### 6.3.4 局部内部类
可以在一个方法中局部定义一个类。声明局部类不能有访问说明符(public...)局部类只能在声明的方法里使用。
### 6.3.5 由外部方法访问变量
与其他内部类相比,局部内部类不仅能访问外部类字段,还可以访问局部变量(一经赋值不能改变的)。
### 6.3.6 匿名内部类
假如只想创建匿名内部类的一个对象,可以使用匿名内部类
### 6.3.7 静态内部类
当使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用,可以将内部类声明为static