接口、lambda表达式与内部类

6 接口、lambda表达式与内部类

接口是用来描述类应该做什么(行为),而不指定应该如何做(具体行为)。

lamda 表达式,是一种很简洁的方法,用来创建可以在将来某个时间点执行的代码块。通过使用它,可以用一种精巧而简洁的方式来表示使用回调或可变行为的代码

内部类,定义在另外一个类内部,它们的使用方法可以访问包含它们的外部类字段。内部类在设计具有相互协作关系的类集合时很有用。

6.1 接口

特点:

  • 接口中所有方法都自动是 public 的。因此,在接口中声明方法时,无需提供关键字 public
  • 接口中可以定义常量,接口中的字段总是 public static final。
  • 接口中绝没有实例字段,在java8以前,接口中绝不会实现方法

可以将接口看做是没有实例字段的 抽象类。因为实例字段和方法实现的任务应该由实现接口的那个类来完成。

接口的属性

接口不是类,故而

  • a 不能使用 new 运算符实例化一个接口
  • b 可以声明接口的变量,不过该接口变量必须引用实现了这个接口的类对象
  • c 可以扩展接口,如同建立类的继承层次一样。允许有多条接口链,从通用性较高的接口扩展到专用性较高的接口。

b 示例代码

Comparable x;
x = new Employee(...)// Employee类实现了Comparable 接口

c 示例代码(假设有一个叫 Moveable的接口)

public interface Moveable {
    void move(double x, double y);
}

//假设一个名叫 Power 的接口扩展了 Moveable 接口
public interface Power extends Moveable {
    double milesPerGallon();
}

//虽然接口中不能包含实例字段,但可包含常量
public interface Power extends Moveable {
    double milesPerGallon();
    double SPEED_LIMIT = 95;// a public static final constant
}

注意:

  • 与接口中方法都被自动设置为 public 一样,接口中的字段总是 public static final。
  • 可以将接口的方法标记为 public ,将字段标记为 public static final. 但是Java 语言规范建议不要提供多余的关键字。

虽然每个类只能有一个超类,但可以实现多个接口。为定义类的行为提供了极大的灵活性。

例如,java 中有一个非常重要的内置接口,名叫 cloneable.
如果每个类实现了这个 cloneable 接口,Object 类中的 clone 方法就可以创建你的类对象的一个副本。
如果希望自己设计的类具有克隆和比较的能力,只要实现这个接口即可。用逗号将想要实现的接口分隔开。

class Employee implements Cloneable, Comparable

instanceof 作用:检查一个对象是否属于某个特定类,检查一个对象是否实现了某个特定的接口。 示例如下:

if (anObject instanceof Comparable) { ... }
抽象类与接口

Java 为何要引入接口概念,不将 Comparable 直接设计成一个抽象类呢?

  • 使用抽象类表示通用属性存在一个严重的问题。每个类只能扩展一个类。
  • 有些程序设计语言(尤其 C++)允许一个类有多个超类-------多重继承特性。而 Java 的设计者选择了不支持多重继承,主要原因是多重继承会让语言变得非常复杂,或者效率会降低
  • 另外,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。
静态和私有方法

Java8 中,允许在接口中增加静态方法。这并非不合法,只是有违将接口作为抽象规范的初衷

通常做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和实用工具类,如 Collection/Collections 或 Path/Paths.

可以通过一个 URI 或者字符串序列构成一个文件或者目录的路径,如 Paths.get(“jdk-11”, “conf”, “security”)。在java 11 中,Path 接口提供了等价的方法:

public interface Path {
    public static Path of(URI uri) {...}
    public static Path of(String first, String... more){...}
    ...
}

因此,Path 类就不再是必要的了。

类似地,实现你自己的接口时,不需要再为实用工具方法另外提供一个伴随类。

在 Java9 中,接口中的方法可以是 private。private 方法可以是静态方法或实例方法。

由于私有方法只能在接口本身的方法中使用,因此它们的用法很有限,只能作为接口中其他方法的辅助方法。

默认方法

作用总结:

  • 限制调用某种方法,若调用某方法即报错。
  • 可以调用其他方法
  • 接口演化

可以将接口方法提供一个默认实现。使用 default 修饰符标记

public interface Comparable<T> {
	default int compareTo(T other) {return 0;}
	// by default, all elements are the same
}

这样用处不大,因为 Comparable 的每一个具体实现都会覆盖这个方法。不过有些情况下,默认方法可能很有用。例如:Iterator 接口,用于访问一个数据结构中的元素。这个接口声明了一个 remove 方法,如下:

public interface Iterator<E> {
	boolean hasNext();
	E next();
	default void remove() {
		throw new UnsuppportedOperationException("remove");
	}
}

如果你要实现一个迭代器,就需要提供 hasNext 和 next 方法。这些方法没有默认实现-----他们依赖于你要遍历访问的数据结构。
不过,如果你的迭代器是只读的,就不用操心实现 remove 方法

默认方法可以调用其他方法。 例如,Collection 接口定义一个便利方法:

public interface Collection {
	int size();// an abstract method
	default boolean isEmpty() {return size() == 0}
}
解决默认方法冲突

如果先在一个接口中将一个方法定义为默认方法,然后又在一个超类或者另一个接口中定义同样的方法。会引发冲突。

java 解决的规则:

  • 超类优先 。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
  • 接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否默认参数)相同的方法,必须覆盖这个方法来解决冲突。

下面看第二个规则。考虑两个包含 getName 方法的接口:

interface Person {
	default String getName() { return "";}
}

interface Named {
	default String getName() {
		return getClass().getName() + "_" + hasCode();
	}
}

此时,如果一个类同时实现了这两个接口会怎么样?

class Student implements Person, Named { … }

类会继承 Person 和 Named 接口提供的两个不一致的 getName 方法。并不是从中选择一个, Java 编译器会报告一个错误,让程序员来解决二义性问题。只需要在 Student 类中提供一个 getName() 方法即可。在这个方法中,可以选择两个冲突方法中的一个,如下:

class Student implements Person, Named {
	public String getName() { return Person.super.getName();}
	...
}

现在假设 Named 接口没有为 getName 提供默认实现:

interface Named {
	String getName();
}

Student 类会从 Person 接口继承默认方法吗?这好像挺合理,不过,Java 设计者更强调一致性。两个接口如何冲突并不重要。如果至少有一个接口提供了一个实现,编译器就会报告错误,程序员就必须解决这个二义性。

注释:
当然,如果两个接口都没有为共享方法提供默认实现,那么就与 Java 8 之前的情况一样,这里不存在冲突。
实现类可以有两个选择:实现这个方法,或者干脆不实现。 如果是后一种情况,这个类本身就是抽象的。

前面讨论了两个接口命名冲突问题,现在来考虑另一种情况:一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。例如,假设 Person 是一个类, Student 定义为:

class Student extends Person implements Named {...}

在这种情况下,只会考虑超类方法,接口的所有默认方法都会被忽略。这其实是 “类优先” 规则。

**“类优先” 规则可以确保与 Java 7 的兼容性。**如果为一个接口增加默认方法,这对于有这些默认方法之前能正常工作的代码不会有任何影响。

接口与回调

回调(callback)是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。

比如在 java.swing 包中有一个 Timer 类,如果希望经过一定时间间隔就得到通知, Timer 类就很有用。例如,假如程序中有一个时钟,可以请求每秒通知一次,以便更新时钟的表盘。

构造定时器时,需设置一个时间间隔,并告诉定时器经过这个时间间隔需要做什么。

如何告诉定时器做什么呢?在很多程序设计语言中,可以提供一个函数名,定时器要定期地调用这个函数。但是,Java 标准类库中的类采用的是面向对象方法。 先向定时器传入某个类的对象,然后,定时器调用这个对象的方法。由于对象可以携带一些附加的信息,因此传递一个对象比传递一个函数要灵活的多。

当然,定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了 java.awt.event 包的 ActionListener 接口。如下

public interface ActionListener {
	void actionPerformed(ActionEvent event);
}

当到达指定的时间间隔时,定时器就调用 actionPerformed 方法。

假设希望每隔 1 秒钟打印一条消息“At the tone, the time is …”, 然后响一声,那么可以定义一个实现 ActionListener 接口的类,然后将想要执行的语句放在 actionPerformed 方法中。

class Timeer implement ActionListener {
	public void actionPerformed(ActionEvent event) {
		 System.out.println("At the tone, the time is ..." 
		   + Instant.ofEpochMilli(event.getWhen()));
		 Toolkit.getDefaultToolkit().beep();
	}
}

注意:

actionPerformed 方法的 ActionEvent 参数。这个参数提供了事件的相关信息,例如,发生这个事件的时间。
event.getWhen() 调用会返回这个事件的时间,表示为“纪元”(197.年1月1日)以来的毫秒数。
如果把它传入静态方法 Instant.ofEpochMilli,可以得到一个更可读的描述。

接下来,构造这个类的对象,并将它传递给 Timer 构造器

var listener = new TimePrinter();
Timer t = new Timer(1000, listener);

Timer 构造器的第一个参数是一个时间间隔(单位是毫秒),即经过多长时间通知一次。这里是每隔1秒钟通知一次。第二个参数是监听器对象。
最后启动定时器:

t.start();

每隔一秒就会显示下面的消息,然后响一声铃

At the tone, the time is 2017-12-16T05:01:49.550Z
Comparator 接口

针对 希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序的情况。 可以使用 Arrays.sort 方法的第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了 Comparator 接口的类的实例。

public interface Comparator<T> {
	int compare(T first, T second);
}

要按长度进行比较字符串,可以如下定义一个实现 Comparator 的类:

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) ...

将这个调用与 words[i].compareTo(words[j]) 进行比较。

  • 这个 compare 方法要在比较器对象上调用,而不是字符串本身调用。

要对一个数组排序,需要为 Arrays.sort 方法传入一个 LengthComparator 对象:

String[] friends = {"Peter", "Paul", "Mary"};
Arrays.sort(friends, new LengthComparator());
对象克隆

要了解克隆的具体含义,得先回忆为一个包含对象引用的变量建立副本时会发生什么。

  • 原变量和副本都是同一个对象的引用。说明任何一个变量改变都会影响另一个变量。
var original = new Emplyee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(10);// 也会改变 original

如果希望 copy 是一个新对象,它的初始状态与 original 相同,但是之后他们各自会有自己不同的状态,这种情况则要使用 clone 方法。

Employee copy = original.clone();
copy.raiseSalary(10);// 不会改变 original

不过并没那么简单。 clone 方法是 Object 的一个 protected 方法,这说明你的代码不能直接调用这个方法。只有 Employee 类可以克隆 Employ 对象。
这个限制是有原因的:

想想看 Object 类如何实现 clone 。它对这个对象一无所知,所以只能逐个字段地进行拷贝。如果对象中的所有数据字段都是数值或其他基本类型,拷贝这些字段没有问题。但是如果对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。

默认的克隆操作是“浅拷贝”,并没有克隆对象中引用的其他对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值