第四章 接口、lambda 表达式与内部类

本文深入探讨了Java中的接口和代理技术。接口作为类行为规范的抽象,允许类实现多个接口以达到多重继承的效果。接口可以包含常量和抽象方法,Java8以后还支持静态和私有方法。在不能预知实现接口的情况下,代理技术提供了在运行时创建新类的能力,这些类可以实现一组给定接口。代理通过InvocationHandler调用处理器动态定义行为,适用于方法调用跟踪、远程调用等多种场景。
摘要由CSDN通过智能技术生成

到目前为止,我们已经学习了 Java 中面向对象编程的核心概念:类和继承。本章将介绍几种常用的高级技术。

一、接口

1、接口的概念

在 Java 程序设计语言中,接口不是类,而是对希望符合这个接口的类的一组需求。

下面给出一个具体的示例。Arrays 类中的 sort 方法承诺可以对对象数组进行排序,但要求满足下面这个条件:对象所属的类必须实现 Comparable 接口。

下面是 Comparable 接口的代码:

public interface Comparable {
	int comparaTo(Object other);
}

这说明,任何实现 Comparable 接口的类都需要包含 Comparable 方法,这个方法有一个 Object 参数,并且返回一个整数。

接口中的所有方法都自动是 public 方法。因此,在接口中声明方法时,不必提供关键字 public。

上面这个接口只有一个方法,而有些接口可能包含多个方法。接口还可以定义常量,不过,接口绝不会有实例字段。提供字段和方法实现的任务应该由实现接口的那个类来完成。因此,可以将接口看成是没有实例字段的抽象类,但这两个概念还是有区别的。

现在,假设希望使用 Arrays 类的 sort 方法对 Employee 对象数组进行排序,Employee 类就必须实现 Comparable 接口。

为了让类实现一个接口,通常需要完成下面两个步骤:

  1. 将类声明为实现给定的接口。
  2. 对接口中的所有方法提供定义。

要将类声明为实现某个接口,需要使用关键字 implements:

class Employee implements Comoarable

当然,这里的 Employee 类需要提供 compareTo 方法。假设希望根据员工的薪水进行比较。以下是 compareTo 方法的实现:

public int compareTo(Object otherObject) {
	Employee other = (Employee) otherObject;
	return Double.compare(salary, other.salary);
}

在这里,使用了静态 Double.compare 方法。如果第一个参数小于第二个参数,它会返回一个负值,如果二者相等则返回 0;否则返回一个正值。

我们可以做得更好一些,可以为泛型 Comparable 接口提供一个类型参数。

class Employee implements Comparable<Employee> {
	public int compareTo(Employee other) {
		Employee other = (Employee) otherObject;
		return Double.compare(salary, other.salary);
	}
}

现在,我们已经看到,要让一个类使用排序服务必须让它实现 compareTo 方法。这是理所当然的,因为要向 sort 方法提供对象的比较方式。但是为什么不能在 Employee 类中直接提供一个 compareTo 方法,而必须实现 Comparable 接口呢?

主要原因在于 Java 程序设计语言是一种强类型语言。在调用方法的时候,编译器要能检查这个方法确实存在。在 sort 方法中可能会有下面这样的语句:

if (a[i].compareTo(a[j]) > 0) {
	...
}

编译器必须确认 a[i] 一定有一个 compareTo 方法。如果 a 是一个 Comparable 对象的数组,就可以确保拥有 compareTo 方法,因为每个市县 Comparable 接口的类都必须提供这个方法的定义。

java.lang.Comparable< T >

  • int compareTo(T other)
    用这个对象与 other 进行比较。如果这个对象小于 other 则返回一个负整数;如果相等则返回 0;否则返回一个正整数。

java.util.Arrays

  • static void sort(Object[] a)
    对数组 a 中的元素进行排序。要求数组中的元素必须属于实现了 Comparable 接口的类,并且元素之间必须是可比较的。

java.lang.Double

  • static int compare(double x, double y)
    如果 x < y返回一个负整数;如果 x 和 y 相等则返回 0;否则返回一个正整数。

2、接口的属性

接口不是类。具体来说,不能使用 new 运算符实例化一个接口:

x = new Comparable(...) // ERROR

不过,尽管不能构造接口的对象,却能声明接口的变量:

Comparable x; // OK

接口变量必须引用实现了这个接口的类对象:

x = new Employee(..); // OK

接下来,如同使用 instanceof 检查一个对象是否属于某个特定类一样,也可以使用 instanceof 检查一个对象是否实现了某个特定的接口:

if (anObject instanceof Comparable) { ... }

与建立类的继承层次一样,也可以扩展接口。这里允许有多条接口链,从通用性较高的接口扩展到专用性较高的接口。例如,假设有一个名为 Moveable 的接口:

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

然后,可以假设一个名为 Powered 的接口扩展了以上 Moveable 接口:

public interface Powered extends Moveable {
	double milesPerGallon();
}

虽然在接口中不能包含实例字段,但是可以包含常量。例如:

public interface Powered extends Moveable {
	double milesPerGallon();
	double SPEED_LIMIT = 95; // a public static final constant
}

与接口中的方法都自动被设置为 public 一样,接口中的字段总是 public static final。

尽管每个类只能有一个超类,但却可以实现多个接口。这就为定义类的行为提供了极大的灵活性。可以使用逗号将想要实现的各个接口分隔开。

class Employee implements Cloneable, Comparable

3、接口与抽象类

先来解释一下,为什么不将 Comparable 直接设计成一个抽象类呢?如下所示:

abstract class Comparable{
	public abstract int compareTo(Object other);
}

这样一来,Employee 类只需要扩展这个抽象类,并提供 compareTo 方法的实现:

class Employee entends Comparable {
	public int compareTo(Object other) { ... }
}

非常遗憾,使用抽象类表示通用属性存在一个严重的问题。每个类只能扩展一个类。假设 Employee 类已经扩展了另一个类,例如 Person,它就不能再扩展第二个类了。

class Employee extends Person, Comparable // ERROR

但每个类可以实现多个接口,如下所示:

class Employee extends Person implements Comparable // OK

实际上,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。

4、静态和私有方法

在 Java 8中,允许在接口中增加静态方法。理论上讲,没有任何里有认为这是不合法的。只是这有违于将接口作为抽象规范的初衷。

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

在 Java 11 中,Path 接口提供了等价的方法:

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

这样一来,Paths 类就不再是必要的了。

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

在 Java 9 中,接口中的方法可以是 private。private 方法可以是静态方法或实例方法。由于私有方法只能在接口本身的方法中使用,所以它们的用法很有限,只能作为接口中其他方法的辅助方法。

二、代理

利用代理可以在运行时创建实现了一组给定接口的新类。只有在编译时期无法确定需要实现哪个接口时才有必要使用代理。

1、何时使用代理

假设你想构造一个类的对象,这个类实现了一个或多个接口,但是在编译时你可能并不知道这些接口到底是什么。这个问题确实有些难度。要想构造一个具体的类,只需要使用 newInstance 方法或者使用反射找出构造器。但是,不能实例化接口。需要在运行的程序中定义一个新类。

为了解决这个问题,有些程序会生成代码,将这些代码放在一个文件中,调用编译器,然后再加载得到的类文件。很自然地,这样做的速度会比较慢,并且需要将表一起连同程序一起部署。而代理机制则是一种更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类能够实现你指定的接口。具体地,代理类包含以下方法:

  • 指定接口所需要的全部方法。
  • Object 类中的全部方法,例如,toString、equals 等。

不过,不能在运行时为这些方法定义新代码。实际上,必须提供一个调用处理器。调用处理器是实现了 InvocationHandler 接口的类的对象。这个接口只有一个方法:

Object invoke(Object proxy, Method method, Object[] args)

无论何时调用代理对象的方法,调用代理器地 invoke 方法都会被调用,并向其传递 Method 对象和原调用的参数。之后调用处理器必须确定如何处理这个调用。

2、创建代理对象

要想创建一个代理对象,需要使用 Proxy 类的 newProxyInstance 方法。这个方法有三个参数:

  • 一个类加载器。作为 Java 安全模型的一部分,可以对平台和应用类,从因特网下载的类等使用不同的类加载器。
  • 一个 Class 对象数组,每个元素对应需要实现的各个接口。
  • 一个调用处理器。

还有两个需要解决的问题。如何定义处理器?另外,对于得到的代理对象能够做些什么?当然,这两个问题的答案取决于我们想要用代理机制解决什么问题。使用代理可能出于很多目的,例如:

  • 将方法调用路由到远程服务器。
  • 在运行的程序中将用户界面事件与动作关联起来。
  • 为了测试,跟踪方法调用。

在示例程序中,我们要使用代理和调用处理器跟踪方法调用。我们定义了一个 ThraceHandler 包装器类存储包装的对象,其中的 invoke 方法会打印所调用方法的名字和参数,随后用包装的对象作为隐式参数调用这个方法。

class TraceHandler implements InvocationHandler{
	private Object target;
	
	public TraceHandler(Object t) {
		target = t;
	}

	public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
		...
		return m.invoke(target, args);
	}
}

现在,只要在 Proxy 上调用了某个接口的方法,就会打印这个方法的名字和参数,之后再用 value 调用这个方法。

Integer 类实现了 Comparable 接口。代理对象属于在运行时定义的一个类(它有一个名字,如 $Proxy0)。这个类也实现了 Comparable 接口。不过,它的 compareTo 方法调用了代理对象处理器的 invoke 方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一支帆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值