当前位置:首页 > 服务端 > CORE JAVA 第六章 接口、lambda表达式和内部类

CORE JAVA 第六章 接口、lambda表达式和内部类

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

​ 接口(interface)技术主要用来描述类具有什么功能,而并不给出每个功能的具体实现。一个类可以实现(implement)一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象。

​ lambda表达式是一种表示可以在将来某个时间点执行的代码块的简洁方法。使用lambda表达式,可以用一种精巧而简洁的方式表示使用回调或变量行为的代码。

​ 内部类(inner class)定义在另外一个类的内部,其中的方法可以访问包含它们的外部类的域。内部类技术主要用于设计具有相互协作关系的类集合。

​ 代理(proxy),是一种实现任意接口的对象。

6.1 接口

6.1.1 接口概念

​ 接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。

​ “如果类遵从某个特定接口,那么就履行这项服务”。

​ 例如:

Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了Comparable接口。

​ 下面是Comparable接口的代码:

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

这就是说,任何实现Comparable接口的类都需要包含compareTo方法,并且这个方法的参数必须是一个Object对象,返回一个整型数值。

注释:在Java SE 5.0中,Comparable接口已经改进为泛型类型。

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

​ 接口可以包含一个或多个方法,还可以定义常量。

​ 接口绝不能含有实例域。在Java SE 8之前,也不能在接口中实现方法。提供实例域和方法实现的任务应该由实现接口的那个类来完成。

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

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

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

class Employee implements Comparable

以下是compareTo方法的实现:

public int compareTo(Object otherObject)
{
    Employee other = (Employee) otherObject;
    return Double.compare(salary, other.salary);
}
// 静态Double.compare方法:如果第一个参数小于第二个参数,它会返回一个负值;二者相等,返回0;否则返回一个正值、

​ 在实现接口时,必须把方法声明为public。

​ 为泛型Comparable接口提供一个类型参数。

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

这样就不用对Object参数进行类型转换了。

提示:Comparable接口中的compareTo方法将返回一个整型数值。如果两个对象不相等,则返回一个正值或者一个负值。在对两个整数域进行比较时,这点非常有用。例如,假设每个雇员都有一个唯一整数id,并希望根据ID对雇员进行重新排序,那么就可以返回id - other.id。如果第一个ID小于另一个ID,则返回一个负值;如果两个ID相等,则返回0;否则返回一个正值。但有一点需要注意:整数的范围不能过大,以避免造成减法运算的溢出。如果能够确信ID为非负整数,或者它们的绝对值不会超过(Inetger.MAX_VALUE-1)/2,就不会出现问题。否则,调用静态Integer.compare方法。

​ 当然,这里的减法技巧不适用于浮点值。可以使用Double.compare方法。

​ 要让一个类使用排序服务,必须让它实现compareTo方法。但是为什么不能在Employee类直接提供一个compareTo方法,而必须实现Comparable接口呢?

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

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

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

注释:语言标准规定,对于任意的x和y,实现必须能够保证sgn(x.compareTo(y)) = -sgn(y.compareTo(x))。这里的sgn是一个数值的符号。如果n是负值,sgn(n) = -1;如果n是0,sgn(n) = 0;如果n是正值,sgn(n) = 1。

​ 与equals方法一样,在继承过程中有可能会出现问题。

​ 这是因为Manager扩展了Employee,而Employee实现的是Comparable ,而不是Comparable 。如果Manager覆盖了compareTo方法,就必须要有经理与雇员进行比较的思想准备(即改变原方法的预期行为的具体实现方法),绝不能仅仅将雇员转换成经理(像下面这样)。

class Manager extends Employee
{
    public int compareTo(Employee other)
    {
        Manager otherManager = (Manager) other;	// NO!
    }
}

这样的话不符合反对称的规则。如果x是一个Employee对象,y是一个Manager对象,调用x.compareTo(y)不会抛出异常,它只是将x和y都作为雇员进行比较。但是反过来,y.compareTo(x)将会抛出一个ClassCastException。

​ 如果子类之间的比较含义不一样,那就属于不同类对象的非法比较。每个compareTo方法都应该在开始时进行下列检测:

if (getClass() != other.getClass())	throw new ClassCastException();

​ 如果存在通用算法,能够对两个不同的子类对象进行比较,则应该在超类中提供一个compareTo方法,并将这个方法声明为final。

6.1.2 接口的特性

  • 接口不是类,尤其不能使用new运算符实例化一个接口(不能构造接口的对象):
x = new Comparable();//ERROR
  • 能声明接口的变量:
Comparable x;	//OK

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

x = new Employee();//OK provided Employee implements Comparable
  • 可以使用instanceof检测一个对象是否实现了某个特定的接口:
if (anObject instanceof Comparable){……}
  • 与可以建立类的继承关系一样,接口也可以被扩展。这里允许存在多条从具有较高通用性的接口到较高专用性的接口的链。例如,有一个称为Moveable的接口:
public interface Moveable
{
    void move(double x, double y);
}

然后,可以以它为基础扩展一个叫做Powered的接口:

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。

  • 有些接口只定义了常量,而没有定义方法。
  • 尽管每个类只能够拥有一个超类,但却可以实现多个接口。

6.1.3 接口与抽象类

​ 使用抽象类表示通用属性存在这样一个问题:每个类只能扩展于一个类。但每个类可以实现多个接口。

6.1.4 静态方法

​ 在Java SE 8中,允许在接口中增加静态方法。

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

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

6.1.5 默认方法

​ 可以为接口方法提供一个默认实现。必须使用default修饰符标记这样一个方法。

public interface Comparable<T>
{
    default int compareTo(T other)	{return 0;}
}

当然,这并没有太大用处,因为Comparable的每一个实际实现都要覆盖这个方法。不过有些情况下,默认方法可能很有用。比如在实现一个有很多方法的接口时,你只需要关心其中的1、2个方法,可以把其余的方法设置成默认方法,什么也不做(在Java SE 8中,可以把所有方法声明为默认方法)。这样,实现这个接口只需要为关心的方法进行覆盖。

​ 默认方法可以调用任何其他方法。

​ 默认方法的一个重要用法是“接口演化”(interface evolution)。假设很久之前你提供了这样一个类:

public class Bag implements Collection

后来,在Java SE 8中,又为Collection接口增加了一个stream方法。

​ 假设stream方法不是一个默认方法。那么Bag类将不能编译,因为它没有实现这个新方法。为接口增加一个非默认方法不能保证“源代码兼容”。

​ 不过,假设不重新编译这个类,而只是使用原先的一个包含这个类的JAR文件。这个类仍能正常加载,尽管没有这个新方法。程序仍然可以正常构造Bag实例,不会有意外发生。不过,如果程序在一个Bag实例上调用stream方法,就会出现一个AbstractMethodError。

​ 将方法实现为一个默认方法就可以解决这两个问题。Bag类可以重新编译。另外如果没有重新编译而直接加载这个类,并在一个Bag实例上调用stream方法,将调用Collection.stream方法。

6.1.6 解决默认方法冲突

​ 如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口定义了同样的方法,会发生什么情况?规则如下:

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

即如果至少有一个接口提供了一个实现,编译器就会报告错误,而程序员就必须解决这个二义性。

​ 如果两个接口都没有为共享方法提供默认实现,那么这里不存在冲突。

警告:千万不要让一个默认方法重新定义Object类中的某个方法。例如,不能为toString或equals定义默认方法。由于类优先规则,这样的方法绝对无法超越Object.toString或Objects.equals。

6.2 接口实例

6.2.1 接口与回调

​ 回调(callback)是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。例如,可以指出在按下鼠标或选择某个菜单项时应该采取什么行动。

​ 在java.swing包中有一个Timer类,可以使用它在到达给定的时间间隔发出通告(定时器)。

​ 在构造定时器时,需要设置一个时间间隔,并告知定时器,当到达时间间隔时需要做些什么操作。

​ 如何告知定时器做什么呢?在Java中,将某个类的对象传递给定时器,然后定时器调用这个对象的方法。由于对象可以携带一些附加的信息,所以传递一个对象比传递一个函数要灵活的多。

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

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

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

​ 假设希望每隔10秒钟打印一条信息“At the tone,the time is……”,然后响一声,就应该定义一个实现ActionListener接口的类,然后将需要执行的语句放在actionPerformed方法中。

class TimePrinter implements ActionListener
{
    public void actionPerformed(ActionEvent event)
    {
        System.out.println("At the tone, the time is" + New Date());
        Toolkit.getDefaultToolkit().beep();
    }
}

actionPerformed方法的ActionEvent参数提供了事件的相关信息,例如,产生这个事件的源对象。在这个程序中,事件的详细信息并不重要,因此,可以放心地忽略这个参数。

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

// Timer构造器签名:
Timer(int interval, ActionListener listener)
ActionListener listener = new TimePrinter();
Timer t = new Timer(10000, listener);	//第二个参数是监听器对象

​ 最后,启动定时器:

t.start();	//启动定时器,一旦启动成功,定时器将调用监听器的actionPerformed。

每隔10秒钟打印一条信息“At the tone,the time is……”,然后响一声。

6.2.2 Comparator接口

​ 之前,我们已经了解了如何对一个对象数组排序,前提是这些对象是实现了Comparable接口的类的实例。例如可以对一个字符串数组排序,因为String类实现了Comparable ,而且String.compareTo方法可以按字典顺序比较字符串。

​ 现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。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();
    }
}

具体完成比较时,需要建立一个实例:

Comparator<String> comp = new LengthComparator();
if (comp.compare(words[i], words[j]) > 0) ……

这个compare方法要在比较器对象上调用,而不是在字符串本身上(words[i].compareTo(words[j]))调用。

注释:我们需要通过建立LengthComparator对象的一个实例来调用compare方法。

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

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

6.2.3 对象克隆

​ Cloneable接口指示一个类提供了一个安全的clone方法。

​ 一个包含对象引用的变量建立副本时,原变量和副本都是同一个对象的引用。这说明,任何一个变量改变都会影响另一个变量。

Employee original = new Employee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(10);	// also changed original

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

Employee copy = original.clone();
copy.raiseSalary(10);	//  original unchanged
//Exception in thread "main" java.lang.Error: Unresolved compilation problems: 
//	Type mismatch: cannot convert from Object to Employee
//	The method clone() from the type Object is not visible

​ 不过并没有这么简单。clone方法是Object的一个protected方法,这说明你的代码不能直接调用这个方法。只有Employee类(在Employee类体里)可以克隆Employee对象。这个限制是有原因的。想想看Object类如何实现clone。它对于这个对象一无所知,所以只能逐个域的进行拷贝。如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域没有任何问题。但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。

​ 默认的克隆操作是浅拷贝,并没有克隆对象中引用的其他对象。如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的,例如子对象属于一个不可变的类,如String。或者在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的。

​ 不过通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝,同时克隆出所有子对象。

​ 对于每一个类,需要确定:

  1. 默认的clone方法是否满足要求;
  2. 是否可以在可变的子对象上调用clone来修补默认的clone方法(深拷贝)。
  3. 是否不该使用clone。

​ 第三个选项是默认选项。如果选择第1项或第2项,类必须:

  1. 实现Cloneable接口。
  2. 重新定义clone方法,并指定public修饰符。

注释:clone方法是Object的一个protected方法,这说明你的代码不能直接调用anObject.clone()。子类只能调用受保护的clone方法来克隆它自己的对象。必须重新定义clone为public才能允许所有方法克隆对象。

​ 在这里,Cloneable接口的出现与接口的正常使用并没有关系。具体来说,它没有指定clone方法,这个方法是从Object类继承的。这个接口只是作为一个标记,指示类设计者了解克隆过程。如果一个对象请求克隆,但没有实现这个接口,就会生成一个受查异常。

注释:Cloneable接口是Java提供的一组标记接口之一。标记接口不包含任何方法;它唯一的作用就是允许在类型查询中使用instanceof。

​ 即使clone的默认(浅拷贝)实现能够满足要求,还是需要实现Cloneable接口,将clone重新定义为public,再调用super.clone()。

class Employee implements Cloneable
{
    // raise visibility level to public
    public Employee clone() throws CloneNotSupportedException
    {
        return (Employee) super.clone();
    }
}

注释:在Java SE 1.4之前,clone方法的返回类型总是Object,而现在可以为你的clone方法指定正确的返回类型。这是协变返回类型的一个例子。

协变返回类型:

​ 在面向对象程序设计中,协变返回类型指的是子类中的成员函数的返回值类型不必严格等同于父类中被重写的成员函数的返回值类型,而可以是更 "狭窄" 的类型。

​ Java 5.0添加了对协变返回类型的支持,即子类覆盖(即重写)基类方法时,返回的类型可以是基类方法返回类型的子类。协变返回类型允许返回更为具体的类型。

​ 与Object.clone提供的浅拷贝相比,前面看到的clone方法并没有为它增加任何功能。这是只是让这个方法是公有的。要建立深拷贝,需要克隆对象中可变的实例域。

class Employee implements Cloneable
{
    ……
    public Employee clone() throws CloneNotSupportedException
    {
        // call Object.clone()
        Employee cloned = (Employee) super.clone();
        
        // clone mutable fields
        cloned.hireDay = (Date) hireDay.clone();
        
        return cloned;
    }
}

​ 如果在一个对象上调用clone,但这个对象的类并没有实现Cloneable接口,Object类的clone方法就会抛出一个CloneNotSupportedException。

​ 必须当心子类的克隆。子类可能会有需要深拷贝或不可克隆的域。不能保证子类的实现者一定会修正clone方法让它正常工作。出于这个原因,在Object类中clone方法声明为protected。

​ 所有的数组类型都有一个public的clone方法,而不是protected。建立一个新数组,包含原数组的所有副本。

int[] nums = {2, 3, 5, 7, 11, 13};
int[] cloned = nums.clone();
cloned[5] = 12;	// doesn't change nums[5]

6.3 lambda表达式

​ lambda表达式,采用一种简洁的语法定义代码块。

6.3.1 为什么引入lambda表达式

​ lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

​ 首先回忆一下我们在哪些地方传递过代码块:

  1. ActionListener的actionPerformed方法包含希望以后执行的代码。将实例提供到一个Timer对象。
  2. 用一个定制比较器完成排序,按照长度对字符串排序,可以向sort方法传入一个Comparator对象。Comparator中的compare方法不是立即调用。实际上,在数组完成排序之前,sort方法会一直调用compare方法。

这两个例子都是将一个代码块传递到某个对象(一个定时器,或者一个sort方法)。这个代码块会在将来某个时间调用。

​ 到目前为止,不能直接传递代码段。Java是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。

6.3.2 lambda表达式的语法

​ 考虑上一节讨论的排序的例子。传入代码检测一个字符串是否比另一个字符串短。这里要计算:

first.length() - second.length()

​ first和second都是字符串。Java是一种强类型语言,所以我们还要指定它们的类型:

(String first, String second)
	-> first.length() - second.length();

​ 以上就是lambda表达式。lambda表达式就是一个代码块,以及必须传入代码的变量规范。

​ 以上就是一种lambda表达式形式:参数,箭头(->)以及一个表达式。

​ 如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在{}中,并包含显示的return语句。

(String first, String second) ->
	{
    	if (first.length() < second.length())	return -1;
    	else if (first.length() > second.length())	return 1;
    	else return 0;
	}

​ 若lambda表达式没有参数,仍然要提供空括号,像无参数方法一样:

() -> { for (int i = 100; i >= 0; i--)	System.out.println(i);}

​ 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。

Comparator<String> comp 
    = (first, second)
    	-> first.length() - second.length();

在这里,编译器可以推导出first和second必然是字符串,因为这个lambda表达式将赋值给一个字符串比较器。

​ 如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号。

Actionlistener listener = event ->
    System.out.println("The time is " + new Date());

​ 无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。

注释:如果一个lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,是不合法的。

​ 下面显示如何在一个比较器和一个动作监听器中使用lambda表达式。

//planets为数组名
//Arrays.sort(planets, new LengthComparator());
Arrays.sort(planets,(first, second) -> first.length() - second.length());

//Timer t = new Timer(10000, listener);
Timer t = new Timer(10000, event -> System.out.println("The time is " + new Date()));

个人理解:

lambda表达式的作用就是代替方法中的函数式接口的对象参数,其中对象的某个方法会在将来执行一次或多次。

lambda表达式的内容就是方法中的内容。

​ 虽然使用 Lambda 表达式可以对某些接口进行简单的实现,但并不是所有的接口都可以使用 Lambda 表达式来实现。Lambda 规定接口中只能有一个需要被实现的方法,不是规定接口中只能有一个方法

​ 可以把lambda表达式赋值给接口变量。因为lambda表达式可以转换为接口,下文会提到。

6.3.3 函数式接口

​ Java中有很多封装代码块的接口,如ActionListener或Comparator。lambda表达式与这些接口是兼容的。

​ 对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口

lambda表达式转换为函数式接口

​ 展示如何转换为函数式接口,考虑Arrays.sort方法,它的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:

Arrays.sort(words,
           (first, second) -> first.length() - second.length());

在底层,Arrays.sort方法会接收实现了Comparator 的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。

​ 最好把lambda表达式看作是一个函数,而不是一个对象。另外要接受lambda表达式可以传递到函数式接口。

​ lambda表达式可以转换为接口:

Timer t = new Timer(1000, event ->
                    {
                        System.out.println("At the tone, the time is " + new Date());
                        Toolkit.getDefaultToolkit().beep();
                    });

​ 实际上,在Java中,对lambda表达式所能做的也只是能转换为函数式接口。

lambda表达式和函数式接口的关系

​ Java API在java.util.function包中定义了很多非常通用的函数式接口。其中一个接口BiFunction<T,U,R>描述了参数类型为T和U而且返回类型为R的函数。可以把我们的字符串比较lambda表达式保存在这个类型的变量中:

BiFunction<String, String, Integer> comp 
    = (first, second) -> first.length() - second.length();

不过,这对于排序并没有帮助。没有哪个Arrays.sort方法想要接受一个BiFunction。类似Comparator的接口往往有一个特定的用途,而不只是提供一个有指定参数和返回类型的方法。

​ 想要用lambda表达式做某些处理,还是要谨记表达式的用途,为它建立一个特定的函数式接口

Predicate接口

​ java.util.function包中有一个尤其有用的接口Predicate:

public interface Predicate<T>
{
    boolean test(T t);
    
    // additional default and static methods
}

​ ArrayList类有一个removeIf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式。例如,下面语句将从一个数组列表删除所有的null值:

list.removeIf(e -> e == null);

6.3.4 方法引用

​ 有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。例如,假设你希望只要出现一个定时器事件就打印这个事件对象。为此可以调用:

Time t = new Timer(1000, event -> System.out.println(event));

​ 但是,如果直接把println方法传递到Timer构造器就更好了。具体做法如下:

Timer t = new Timer(1000, System.out::println);

​ 表达式 System.out::println是一个方法引用(method reference),它等价于lambda表达式x -> System.out.println(x)

​ 再来看一个例子,假设你想对字符串排序,而不考虑字母的大小写。

Arrays.sort(strings, String::compareToIgnoreCase)

方法引用的三种情况

​ 要用::操作符分隔方法名与对象或类名。主要有三种情况:

  1. object::instanceMethod
  2. Class::staticMethod
  3. Class::instanceMethod

​ 在前两种情况中,方法引用等价于提供方法参数的lambda表达式。 System.out::println等价于x -> System.out.println(x);类似地,Math::pow等价于(x,y) -> Math.pow(x,y)

​ 对于第三种情况,第1个参数会成为方法的目标。例如,String::compareToIgnoreCase等同于(x,y) -> x.compareToIgnoreCase(y)

注释:如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的那一个方法。选择哪一个方法取决于方法引用转换为哪个函数式接口的方法参数。

​ 类似于lambda表达式,方法引用不能独立存在,总是会转换成函数式接口的实例

方法引用与this参数

​ 可以在方法引用中使用this参数。例如,this::equlas等价于x -> this.equals(x)

​ 使用super也是合法的。super::instanceMethod使用this作为目标,会调用给定方法的超类版本。

个人理解

​ 方法引用是lambda表达式一种简写的方式。

6.3.5 构造器引用

​ 构造器引用与方法引用很类似,只不过方法名为new。例如Person::new是Person构造器的一个引用。具体是哪一个构造器,取决于上下文。

数组类型构造器引用

​ 可以用数组类型建立构造器引用。例如,int[]::new是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x -> new int[x]

泛型类型数组与数组构造器引用 ?

​ Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。

​ 表达式new T[n]会产生错误,因为这会改成new Object[n]。这是一个问题。例如,我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:

Object[] people = stream.toArray();

​ 不过,这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了这个问题。可以把Person[]::new传入toArray方法:

Person[] people = stream.toArray(Person[]::new);

​ toArray方法调用这个构造器来得到一个正确类型的数组,然后填充这个数组并返回。

6.3.6 变量作用域

​ 通常,你可能希望能够在lambda表达式中访问外围方法或类中的变量。例如:

public static void repeatMessage(String text, int delay)
{
    ActionListener listener = event ->
    {
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    };
    new Timer(delay, listener).start();
}

​ 来看这样一个调用:

repeatMessage("Hello", 1000);

​ 现在来看lambda表达式中的变量text。注意这个变量并不是在这个lambda表达式中定义的,而是repeatMessage方法的一个参数变量。

​ 这里好像会有问题:lambda表达式的代码可能会在repeatMessage调用返回很久以后才运行,而那时这个参数变量已经不存在了。如何保留text变量呢?

​ 要了解到底会发生什么,下面来巩固我们对lambda表达式的理解。lambda表达式有3个部分:

  1. 一个代码块
  2. 参数
  3. 自由变量的值,这是指非参数而且不在代码中定义的变量

​ 在我们的例子中,这个lambda表达式有一个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串"Hello"。我们说它被lambda表达式捕获。(下面来看具体的实现细节。例如,可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。)

注释:关于代码块以及自由变量值有一个术语:闭包(closure)。在Java中,lambda表达式就是闭包。

​ 可以看到,lambda表达式可以捕获外围作用域中变量的值。在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在lambda表达式中,只能引用值不会改变的变量。例如,下面的做法是不合法的:

public static void countDown(int start, int delay)
{
    ActionListener listener = event ->
    {
        start--;	//Error: Can't mutate captured varible
        System,out.pirntln(start);
    };
    new Timer(delay, listener).start();
}

​ 之所以有这个限制是有原因的。如果在lambda表达式中改变变量,并发执行多个动作时就会不安全。

​ 另外如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。例如:

public static void repeat(String text, int count)
{
    for(int i = 1; i <= count; i++)
    {
        ActionListener listener = event ->
        {
            System.out.println(i + ":" + text);
            	//Error: Cannot refer to changing i
        };
        new Timer(1000, listener).start();
    }
}

​ 这里有一条规则:lambda表达式中捕获的变量必须实际上是最终变量(effectively final)。实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。

​ lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。

​ lambda表达式中也不能有同名的局部变量。

​ 在lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如:

public class Application
{
    public void init()
    {
        ActionListener listener = event ->
        {
            System.out.println(this.toString());
            ……
        };
        ……
    }
}

​ 表达式this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。lambda表达式的作用域嵌套在init方法中,与出现在这个方法中的其他位置一样,lambda表达式中this的含义并没有变化。

6.3.7 处理lambda表达式

​ 目前为止,已经了解了如何生成lambda表达式,以及如何把lambda表达式传递到需要一个函数式接口的方法。下面来看如何编写方法处理lambda表达式。

​ 使用lambda表达式的重点是延迟执行。之所以希望以后再执行代码,有很多原因,如:

  • 在一个单独的线程中运行代码;
  • 多次运行代码;
  • 在算法的适当位置运行代码(排序中的比较操作);
  • 发生某种情况时执行代码(点击了一个按钮,数据 到达,等等);
  • 只在必要时才运行代码。

​ 下面来看一个简单的例子。假设你想要重复一个动作n次。现在将这个动作和重复次数传递到一个repeat方法:

repeat(10, () -> System.out.println("Hello world"));

​ 要接受这个lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口。表6-1列出了Java API中提供的最重要的函数式接口。在这里,我们可以使用Runnable接口:

public static void repeat(int n, Runnable action)
{
    for(int i = 0; i < n; i++)
        action.run();
}

​ 调用action.run()时会执行这个lambda表达式的主体。

​ 现在让这个例子更复杂一些。我们希望告诉这个动作它出现在哪一次迭代中。为此,需要选择一个合适的函数式接口。其中要包含一个方法,方法有一个int参数而且返回类型为void。处理int值的标准接口如下:

public interface IntConsumer
{
    void accept(int value);
}

​ 下面给出repeat方法的改进版本:

public static void repeat(int n, IntConsumer action)
{
    for(int i = 0; i < n; i++)
        action.accept(i);
}

​ 可以如下调用它:

repeat(10, i -> System.out.println("Countdown: " + (9 - i)));

​ 最好使用特殊化规范来减少自动装箱。出于这个原因,使用了IntConsumer而不是Consumer

表6-1 常用函数式接口:

函数式接口 参数类型 返回类型 抽象方法名 描述 其他方法
Runnable void run 作为无参数或返回值的动作运行
Supplier T get 提供一个T类型的值
Consumer T void accept 处理一个T类型的值 andThen
BiConsumer<T,U> T,U void accept 处理T和U类型的值 andThen
Function<T,R> T R apply 有一个T类型参数的函数 compose,andThen,identity
BiFunction<T,U,R> T,U R apply 有T和U类型参数的函数 andThen
UnaryOperator T T apply 类型T的一元操作符 compose,andThen,identity
BinaryOperator T,T T apply 类型T的二元操作符 andThen,maxBy,minBy
Predicate T boolean test 布尔值函数 and,or,negate,isEqual
BiPredicate<T,U> T,U boolean test 有两个参数的布尔值函数 and,or,negate

表6-2 基本类型的函数式接口 略

提示:最好使用表6-1或6-2中的接口。

注释

​ ?

​ 大多数标准函数式接口都提供了非抽象方法来生成或合并函数。例如,Predicate.isEqual(a)等同于a::equals,如果a为null也能正常工作。已经提供了默认方法and、or和negate来合并谓词。例如,Predicate.isEqual(a).or(Predicate.isEqual(b))就等同于x -> a.equals(x) || b.equals(x)

注释:如果设计你自己的接口,其中只有一个抽象方法,可以用@FunctionalInterface注解来标记这个接口。这样做有两个优点。如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。另外Javadoc页里会指出你的接口是一个函数式接口。

​ 并不是必须使用注解。根据定义,任何有一个抽象方法的接口都是函数式接口。

6.3.8 再谈Comparator

​ Comparator接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda表达式或方法引用。

​ 静态comparing方法取一个“键提取器”函数,它将类型T映射为一个可比较的类型(如String)。对要比较的对象应用这个函数,然后对返回的键完成比较。例如,假设有一个Person对象数组,可以如下按名字对这些对象排序:

Arrays.sort(people,Comparator.comparing(Person::getName));
// comparing 方法接收一个 函数式接口 ,通过一个 lambda 表达式传入

​ 可以把比较器与thenComparing方法串起来。例如,

Arrays.sort(people,
           Comparator.comparing(Person::getName)
           .thenComparing(Person::getFirstName));

​ 如果两个人的姓相同,就会使用第二个比较器。

​ 这些方法有很多变体形式。可以为comparing和thenComparing方法提取的键指定一个比较器。例如,可以根据人名长度完成排序:

Arrays.sort(people, Comparator.comparing(Person::getName,
                                        (s,t) -> Integer.compare(s.length(), t.length())));

​ 另外,comparing和thenComparing方法都有变体形式,可以避免int、long或double值的装箱。要完成前一个操作,还有一种更容易的做法:

Arrays.sort(people, Comparator.comparaingInt(p -> p.getName().length()));

​ 如果键函数可以返回null,可能就要用到nullsFirst和nullsLast适配器。这些静态方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或大于正常值。例如,假设一个人没有中名时getMiddleName会返回一个null,就可以使用Comparator.comparing(Person::getMiddleName(),Comparator.nullsFirst(...))

​ nullsFirst方法需要一个比较器,在这里就是比较两个字符串的比较器。naturalOrder方法可以为任何实现了Comparable的类建立一个比较器。在这里,Comparator.<String>naturalOrder()正是我们需要的。下面是一个完整的调用,可以按可能为null的中名进行排序。这里使用了一个静态导入java.util.Comparator.*,以便理解这个表达式。注意naturalOrder的类型可以推导得出。

Arrays.sort(people, comparing(Person::getMiddleName, nullsFirst(naturalOrder())));

​ 静态reverseOrder方法会提供自然顺序的逆序。要让比较器逆序比较,可以使用reversed实例方法。例如naturalOrder().reversed()等同于reverseOrder()

6.4 内部类

​ 内部类是定义在另一个类中的类。

​ 为什么需要使用内部类:

  • 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
  • 内部类可以对同一个包中的其他类隐藏起来。
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。

​ 我们将这个比较复杂的内容分几部分介绍:

  • 6.4.1节,给出一个简单的内部类,访问外围类的实例域。
  • 6.4.2节,给出内部类的特殊语法规则。
  • 6.4.3节,探讨如何将内部类的内部转换成常规类。
  • 6.4.4节,讨论局部内部类,它可以访问外围作用域中的局部变量。
  • 6.4.5节,介绍匿名内部类,说明在Java有lambda表达式之前用于实现回调的基本方法。
  • 6.4.6节,介绍如何将静态内部类嵌套在辅助类中。

6.4.1 使用内部类访问对象状态

​ 下面进一步分析TimerTest示例,并抽象出一个TalkingClock类。构造一个语音时钟需要提供两个参数:发布通告的间隔和开关铃声的标志。

public class TalkingClock
{
    private int interval;
    private boolean beep;
    
    public TalkingClock(int interval, boolean beep){……}
    public void start(){……}
    
    public class TimePrinter implements ActionListener
        // an inner class
    {
        ……
    }
}

​ 需要注意,这里的TimePrinter类位于TalkingClock类内部。这并不意味着每个TalkingClock都有一个TimePrinter实例域。如前所示,TimePrinter对象是由TalkingClock类的方法构造

​ 下面是TimePrinter类的详细内容。需要注意一点,actionPerformed方法在发出铃声之前检查了beep标志。

 public class TimePrinter implements ActionListener
        // an inner class
    {
        public void actionPerformed(ActionEvent event)
        {
            System.out.println("At the tone, the time is " + new Date());
            if (beep)	Toolkit.getDefaultToolkit().beep();
        }
    }

​ TimePrinter类没有实例域或名为beep的变量,取而代之的是beep引用了创建TimePrinter的TalkingClock对象的域。从传统意义上讲,一个方法可以引用调用这个方法的对象数据域。内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域

内部类的对象总是有一个隐式引用,它指向了创建它的外部类对象。这个引用在内部类的定义中是不可见的。然而,为了说明这个概念,我们将外围类对象的引用称为outer。于是actionPerformed方法将等价于下列形式:

	public void actionPerformed(ActionEvent event)
        {
            System.out.println("At the tone, the time is " + new Date());
            if (outer.beep)	Toolkit.getDefaultToolkit().beep();
        }

外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器,添加一个外围类引用的参数。因为TimePrinter类没有定义构造器,所以编译器为这个类生成了一个默认的构造器:

public TimePrinter(TalkingClock clock)	// automatically generated code
{
    outer = clock;
}

​ outer不是Java的关键字。我们只是用它说明内部类中的机制。

​ 当在start方法中创建了TimePrinter对象后,编译器就会将this引用传递给当前的语音时钟的构造器:

ActionListener listener = new TimePrinter(this);	// parameter automatically added

​ 如果有一个TimePrinter类是一个常规类,它就需要通过TalkingClock类的公有方法访问beep标志。而使用内部类可以给予改进,即不必提供仅用于访问其他类的访问器。

注释:TimerPrinter类声明为私有的。这样一来,只有TalkingClock的方法才能够构造TimePrinter对象。只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。

6.4.2 内部类的特殊语法规则

​ 上一节中已经讲述了内部类有一个外围类的引用outer。事实上,使用外围类引用的正规语法还要复杂一些。

​ 表达式OuterClass.this表示外围类引用。例如:

		public void actionPerformed(ActionEvent event)
        {
            ……
            if (TalkingClock.this.beep)	Toolkit.getDefaultToolkit().beep();
        }

​ 反过来,可以采用下列语法格式更加明确地编写内部对象的构造器

outerObject.new InnerClass(construction parameters)

​ 例如,

ActionListener listener = this.new TimePrinter();

​ 在这里,最新构造的TimePrinter对象的外围类引用被设置为创建内部类对象的方法中的this引用。通常,this限定词是多余的。不过,可以通过显式地命名将外围类引用设置为其他的对象。例如,如果TimePrinter是一个公有内部类,对于任意的语音时钟都可以构造一个TimePrinter:

TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

​ 需要注意,在外围类的作用域之外,可以这样引用内部类:

OuterClass.InnerClass

注释:内部类中声明的所有静态域都必须是final。内部类不能有static方法(也可以有静态方法,但是只能访问外围类的静态域和方法)。

6.4.3 内部类是否有用、必要和安全

​ 内部类是一种编译器现象,与虚拟机无关。编译器会把内部类翻译成用$分隔外部类名与内部类名的常规类文件,而虚拟机则对此一无所知。

​ 通过对TalkingClock.TimePrinter进行反射,可以看到编译器为了引用外围类,生成了一个附加的实例域。另外,还可以看到构造器的TalkingClock参数。

​ 由于内部类拥有访问特权,可以访问外围类的私有数据,所以与常规类比较起来功能更加强大。

​ 既然内部类可以被翻译成名字很古怪的常规类(而虚拟机对此一点也不了解),内部类如何管理那些额外的访问特权呢?

​ 利用反射查看TalkingClock类,可以看到编译器会在外围类添加静态方法,它将返回作为参数的beep域。而内部类会调用这个方法。

​ 这样做会有安全风险,任何人都可以通过调用添加的静态方法读取到私有域beep。

​ 总而言之,如果内部类访问了私有数据域,就有可能通过附加在外围类所在包中的其他类访问它们。但程序员不可能无意之中就获得对类的访问权限,而必须刻意地构建或修改类文件才有可能达到这个目的。

6.4.4 局部内部类

​ 在之前TalkingClock示例的代码中,TimePrinter这个类名字只在start方法中创建这个类型的对象时使用了一次。

​ 当遇到这类情况时,可以在一个方法中定义局部类

public void start()
{
    class TimePrinter implements ActionListener
    {
        ……
    }
    
    ActionListener listener = new TimePrinter();
    Timer t = new Timer(interval, listener);
    t.start();
}

​ 局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。

​ 局部类有一个优势,即对外部世界可以完全地隐藏起来。即使TalkingClock类中的其他代码也不能访问它。除start方法之外,没有任何方法知道TimePrinter类的存在。

6.4.5 由外部方法访问变量

​ 与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类,还可以访问局部变量。不过,那些局部变量必须事实上为final。它们一旦赋值就绝不会改变。

​ 下面的示例将TalkingClock构造器的参数interval和beep移至start方法中。

public void start(int interval, boolean beep)
{
    class TimePrinter implements ActionListener
    {
        public void actionPerformed(ActionEvent event)
        {
            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();
}

​ 请注意,TalkingClock类不再需要存储实例变量beep了,它只是引用start方法中的beep参数变量。

​ 程序行if (beep) ……毕竟在start方法内部,为什么还要研究能不能访问beep变量的值呢?

​ 为了能够更清楚的看到内部的问题,考查一下控制流程:

  1. 调用start方法。
  2. 调用内部类TimePrinter的构造器,以便初始化对象变量listener。
  3. 将listener引用传递给Timer构造器,定时器开始计时,start方法结束。此时,start方法的beep参数变量不复存在。
  4. 然后,actionPerformed方法执行if (beep)……。

​ 为了能让actionPerformed方法工作,TimePrinter类在beep域释放之前将beep域用start方法的局部变量进行备份。当创建一个对象时,编译器必须检测对局部变量的访问,为每一个变量建立相应的数据域,并将局部变量拷贝到构造器中,以便将这些数据域初始化为局部变量的副本。

​ 从程序员的角度看,局部变量的访问非常容易。它减少了需要显示编写的实例域,从而使得内部类更加简单。

局部类与final

​ 局部类的方法只可以引用定义为final的局部变量。鉴于此情况,在列举的示例中。将beep参数声明为final,对它进行初始化后不能够再进行修改。因此,就使得局部变量与在局部类内建立的拷贝保持一致。

注释:在Java SE 8之前,必须把从局部类访问的局部变量声明为final。例如,start方法原本就应当这样声明,从而使内部类能够访问beep参数:

public void start(int interval, final boolean beep)

​ 有时,final限制显得不太方便。例如,想更新在一个封闭作用域内的计数器。int counter = 0; counter++;

​ 由于知道counter需要更新,所以不能将counter声明为final。由于Integer对象是不可变的,也不能用Integer代替它。补救的方法是使用一个长度为1的数组:

int[] counter = new int[1]; counter[0]++;

​ 在内部类被首次提出时,原型编译器对内部类中修改的局部变量自动地进行转换。不过,后来这种做法被废弃。毕竟,这里存在一个危险。同时在多个线程中执行内部类中的代码时,这种并发更新会导致竞态条件(14章)。

6.4.6 匿名内部类

​ 将局部内部类的使用再深入一步。假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类

public void start(int interval, boolean beep)
{
    ActionListener listener = new ActionListener()
    {
        public void actionPerformed(ActionEvent event)
        {
            System,out.println("At the tone, the time is " + new Date());
            if (beep) Toolkit.getDefaultToolkit().beep();
        }
    };
    
    Timer t = new Timer(interval, listener);
    t.start();
}

​ 它的含义是:创建一个实现ActionListener接口的类的新对象,需要实现的方法actionPerformed定义在括号{}内。

​ 通常的语法格式是:

new superType(construction parameters)
{
    //inner class methods and data
}

​ 其中,superType可以是ActionListener这样的接口,于是内部类就要实现这个接口。superType也可以是一个类,于是内部类就要扩展它。

​ 由于匿名类没有类名,所以匿名类不能有构造器。取而代之的是,将构造器参数传递给超类构造器。尤其是在内部类实现接口的时候,不能有任何构造参数。

​ 多年来,Java程序员习惯的做法是用匿名内部类实现事件监听器和其他回调。如今最好还是使用lambda表达式。例如:

public void start(int interval, boolean beep)
{

    Timer t = new Timer(interval, event ->
                        {
                            System,out.println("At the tone, the time is " + new Date());
            				if (beep) Toolkit.getDefaultToolkit().beep();
                        });
    t.start();
}

注释:双括号初始化:

​ 利用内部类语法。假设构造一个数组列表,并将它传递到一个方法:

ArrayList<String> friends = new ArrayList<>();
friends.add("Harry");
friends.add("Tony");
invite(friends);

​ 如果不再需要这个数组列表,最好让它作为一个匿名列表。为匿名列表添加元素:

invite(new ArrayList<String>() {{ add("Harry"); add("Tony"); }});

​ 注意这里的双括号。外层括号建立了ArrayList的一个匿名子类。内层括号则是一个对象构造块。

警告:建立一个与超类大体类似的匿名子类通常会很方便。不过,对于equals方法要特别当心。第五章中我们曾建议equals方法最后使用以下测试:

if (getClass() != other.getClass()) return false;

​ 但是对匿名子类做这个测试时会失效。

提示:生成日志或调试消息时,通常希望包含当前类的类名,如

System.err.println("Something awful happened in " + getClass());

​ 不过,这对于静态方法不奏效。调用getClass的时候调用的是this.getClass,而静态方法没有this。

​ 应该使用:

new Object(){}.getClass().getEnclosingClass();	// get class of static method

​ 在这里,new Object(){}会建立Object的一个匿名子类的一个匿名对象,getEnclosingClass则得到其外围类,也就是包含这个静态方法的类。

6.4.7 静态内部类

​ 有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以把内部类声明为static,以便取消产生的引用。

​ 只有内部类能声明为static。静态内部类的对象除了没有对生成它的外围类对象的引用特权外,与其它所有内部类完全一样。

​ 若内部类对象是在静态方法中构造的,则必须使用静态内部类。如果没有将内部类声明为static,那么编译器将会给出错误报告:没有可用的隐式外部类类型对象初始化内部类对象。

注释:与常规内部类不同,静态内部类可以有静态域和方法。

注释:声明在接口中的内部类自动称为static和public类。

6.5 代理

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

6.5.1 何时使用代理

​ 假设有一个表示接口的Class对象(有可能只包含一个接口),它的确切类型在编译时无法知道。要想构造一个实现这些接口的类,就需要使用newInstance方法或反射找出这个类的构造器。但是,不能实例化一个接口,需要在程序处于运行状态时定义一个新类。

​ 为了解决这个问题,有些程序将会生成代码;将这些代码放置在一个文件中;调用编译器;然后再加载结果类文件。这样做速度比较慢,而且需要将编译器与程序放在一起。

​ 代理机制是一种更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。尤其是,它具有下列方法:

  • 指定接口所需要的全部方法。
  • Object类中的全部方法。

​ 然而,不能在运行时定义这些方法的新代码。而是要提供一个调用处理器(invocation handler)。调用处理器是实现了InvocationHandler接口的类对象。在这个接口中只有一个方法:

​```Object invoke(Object proxy, Method method, Object[] args)```
// 定义了代理对象调用方法时希望执行的动作

​ 无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始的调用参数。调用处理器必须给出处理调用的方式。

6.5.2 创建代理对象

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

  • 一个类加载器。目前用null表示使用默认的类加载器。
  • 一个Class对象数组,每个元素都是需要实现的接口。
  • 一个调用处理器。

​ 还有两个需要解决的问题。如何定义一个处理器?能够用结果代理对象做些什么?这两个问题的答案取决于打算使用代理机制解决什么问题。使用代理可能出于很多原因:

  • 路由对远程服务器的方法调用。
  • 在程序运行期间,将用户接口事件与动作关联起来。
  • 为调试,跟踪方法调用。

​ 在示例中,使用代理和调用处理器跟踪方法调用,并且定义了一个TraceHandler包装器类implements InvocationHandler接口,存储包装的对象。其中的invoke方法打印出被调用方法的名字和参数,随后用包装好的对象作为隐式参数调用这个方法。

​ 下面说明如何构造用于跟踪方法调用的代理对象:

Object value = ……;
//	construct wrapper
InvocationHandler handler = new TraceHandler(value);
// construct proxy for one or more interfaces
Class[] interfaces = new Class[] {Comparable.class};
Object proxy = Proxy.newProxyInstance(null, interfaces, handler);

​ 现在,无论何时用proxy调用某个方法,这个方法的名字和参数就会打印出来,之后再用value调用它。

​ 示例见书。

个人理解

​ 创建了代理类对象后,每对代理类对象调用他要实现的接口中的方法或Object类中的某些方法时,这些方法都会调用调用处理器中的invoke方法。之后包装好的对象再会调用接口中的方法或Object类中的某个方法。

6.5.3 代理类的特性

​ 代理类是在程序运行过程中创建的。然而,一旦被创建,就变成了常规类,与虚拟机中的任何其他类没有什么区别。

​ 所有的代理类都扩展于Proxy类。一个代理类只有一个实例域——调用处理器,它定义在Proxy的超类中。为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中。

​ 所有的代理类都覆盖了Object类中的方法toString、equals和hashCode。如同所有的代理法一样,这些方法仅仅调用了调用处理器的invoke。Object类中的其他方法(如clone和getClass)没有被重新定义。

​ 没有定义代理类的名字,Sun虚拟机中的Proxy类将生成一个以字符串$Proxy开头的类名。

​ 对于特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次newProxyInstance方法的话,那么只能够得到同一个类的两个对象。可以利用getProxyClass方法获得这个类:

Class proxyClass = Proxy.getProxyClass(null, interfaces);

​ 代理类一定是public和final。

​ 可以通过调用Proxy类中的isProxyClass方法检测一个特定的Class对象是否代表一个代理类。

作者:c1utchfan
来源链接:https://www.cnblogs.com/c1utchfan/p/13437745.html

版权声明:
1、Java侠(https://www.javaxia.com)以学习交流为目的,由作者投稿、网友推荐和小编整理收藏优秀的IT技术及相关内容,包括但不限于文字、图片、音频、视频、软件、程序等,其均来自互联网,本站不享有版权,版权归原作者所有。

2、本站提供的内容仅用于个人学习、研究或欣赏,以及其他非商业性或非盈利性用途,但同时应遵守著作权法及其他相关法律的规定,不得侵犯相关权利人及本网站的合法权利。
3、本网站内容原作者如不愿意在本网站刊登内容,请及时通知本站(javaclubcn@163.com),我们将第一时间核实后及时予以删除。





本文链接:https://www.javaxia.com/server/124964.html

分享给朋友:

“CORE JAVA 第六章 接口、lambda表达式和内部类” 的相关文章