阅读Java核心技术Ⅰ的笔记(Java基础、第六章、接口、lambda表达式、内部类与动态代理浅析)

命运的无常不会使我疯狂,如山顶松柏屹立在狂风暴雨中央——岛屿心情《游》

第六章

  • 一个类可以实现( implement)—个或多个接口,并在需要接口的地方, 随时使用实现了相应接口的对象。
  • 使用 lambda 表达式,可以用一种精巧而简洁的方式表示使用回调或变量行为的代码。
  • 内部类( inner class) 机制。理论上讲,内部类有些复杂, 内部类定义在另外一个类的内部, 其中的方法可以访问包含它们的外部类的域。内部类技术主要用于设计具有相互协作关系的类集合
  • 在本章的最后还将介绍代理(proxy), 这是一种实现任意接口的对象。代理是一种非常专业的构造工具,它可以用来构建系统级的工具。

6.1 接口

6.1.1 接口概念

想要使用Arrays.sort()就必须要对排序类实现Comparable这个接口。

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

任何实现 Comparable 接口的类都需要包含 compareTo 方法,并且这个方法的参数必须是一个 Object 对象,返回一个整型数值。
在 JavaSE 5.0 中,Comparable 接口已经改进为泛型类型。

public interface Comparable<T> {
int compareTo(T other) ; // parameter has type T
}

引入泛型的好处在于实现这个方法的时候不用再强制转换类型了。
接口中的所有方法自动地属于 public。 因此,在接口中声明方法时,不必提供关键字public。
为了让类实现一个接口, 通常需要下面两个步骤:

  1. 将类声明为实现给定的接口
  2. 对接口中的所有方法进行定义
    要将类声明为实现某个接口, 需要使用关键字 implements:

例如:

class Employee implement Comparable

比较两个浮点数的大小,例如x,y不能使用x-y,应该使用Double.compare(x,y),因为如果x和y非常接近的时候,x-y可能会四舍五入等于0。
为什么想实现排序服务不能给类直接提供一个 compareTo 方法,而必须实现 Comparable 接口呢?因为,于 Java 程序设计语言是一种强类型 ( strongly typed) 语言。在调用方法的时候, 编译器将会检查这个方法是否存在。如果a是一个Comparable对象的数组,就可以确保一定有compareTo方法。因为每个实现 Comparable 接口的类都必须提供这个方法的定义。通过接口检查。
有人认为, 将 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 接口的类, 那么虚拟机就会抛出一个异常。所以必须要实现Comparable接口才能实现排序。
当发生父类与子类进行比较的情况的时候,会发生和实现equals相同的情况,如果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 运算符实例化一个接口。然而, 尽管不能构造接口的对象,却能声明接口的变量,接口变量必须弓I用实现了接口的类对象。可以使用instance 检查一个对象是否实现了某个特定的接口。
虽然在接口中不能包含实例域或静态方法,但却可以包含常量。例如:

public interface Powered extends Moveable
{
double milesPerCallonO;
double SPEED_LIHIT = 95; // a public static final constant
}

与接口中的方法都自动地被设置为 public—样,接口中的域将被自动设为 public static final。
Java 语言规范却建议不要书写这些多余的关键字,所以定义接口方法或者变量的时候不必再写修饰符

6.1.3 接口与抽象类

为什么 Java 程序设计语言还要不辞辛苦地引入接口概念?
每个类只能扩展于一个类。但是每个类可以实现多个接口,而且多继承会让语言本身变得非常复杂(如同 C++,) 效率也会降低(如同 Eiffel)。实际上,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。

6.1.4 静态方法

在 Java SE 8 中,允许在接口中增加静态方法。目前为止, 通常的做法都是将静态方法放在伴随类中。
来看 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 类就不再是必要的了。实际上再Java11中确实提供了等价的方法:

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

再Java9中,接口的方法可以是private。private可以是静态方法或者实例方法,由于private修饰之后只能在接口本身的方法中调用,所以用法很有限,只能作为接口中其他方法的辅助方法。

6.1.5 默认方法

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

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

有些情况下, 默认方法很有用。
默认方法的一个重要用法是“ 接口演化” (interface evolution。) 以 Collection 接口为例,这个接口作为 Java 的一部分已经有很多年了。假设很久以前你提供了这样一个类:public class Bag implements Collection
后来, 在 JavaSE 8 中, 又为这个接口增加了一个 stream 方法。假设 stream 方法不是一个默认方法。那么 Bag 类将不能编译, 因为它没有实现这个新方法。为接口增加一个非默认方法不能保证“源代码兼容”。将方法实现为一个默认方法就可以解决这个问题。

6.1.6 解决默认方法冲突

总得来说就是,一个类继承的超类和实现的接口中有相同的方法,或者实现的多个接口里面有相同的方法。这个时候就会发生方法冲突,有下面几个规则:

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

看第二个规则,考虑另一个包含 getName 方法的接口:

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

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

如果有一个类同时实现了这两个接口会怎么样呢?类会继承 Person 和 Named 接口提供的两个不一致的 getName 方法。并不是从中选择一个,Java 编译器会报告一个错误,让程序员来解决这个二义性。只需要Student 类中提供一个 getName 方法。在这个方法中,可以选择两个冲突方法中的一个,如下所示:

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

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

6.1.7 接口与回调

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

6.1.8 Comparator接口

Comparator接口和Comparable实现的功能差不多,都是为排序而生,但是Comparator接口适用面更广,例如想按照String字符串的长度进行排序,肯定不能让String类用两种不同的方式实现compareTo方法——更何况,String类也不应由我们来修改。
怎么使用Comparator接口呢?使用Arrays.sort(arr,Comparator),第二个参数使用Comparator比较器作为参数。

6.1.9 对象克隆

默认的clone方法是Object类的protected修饰的,当调用clone方法时,如果对象的字段都是数值或者基本类型的,那没有任何问题,但是当对象的数值是其他对象的引用的时候,会拷贝出相同子对象的另外一个引用,也就是原对象与拷贝的对象的引用会指向同一个对象。这不是我们想要的。这种复制叫做浅拷贝
浅拷贝

所以想要原对象与拷贝出来的对象的两个引用指向两个对象就需要重写clone方法。例如下:

public class ImplementMyInterface implements Cloneable{
    private InterEmployee employee;

    public InterEmployee getEmployee() throws CloneNotSupportedException {
        return employee.clone();
    }
    public void setEmployee(InterEmployee employee) {
        this.employee = employee;
    }
    public ImplementMyInterface() {
    }

    //默认提供的clone是浅克隆,就是如果一个类中的域是其他类的类型,那么克隆出来的两个对象会同时指向这个对象的引用所以还是要重写clone方法
    public ImplementMyInterface clone() throws CloneNotSupportedException{
        ImplementMyInterface implementMyInterface= (ImplementMyInterface) super.clone();
        implementMyInterface.employee=employee.clone();
        return implementMyInterface;
    }
    @Override
    public String toString() {
        return "ImplementMyInterface{" +
                "employee=" + employee +
                '}';
    }
}

ImplementMyInterface 类中含有InterEmployee类的变量。InterEmployee如下。

public class InterEmployee implements Cloneable{
    private String name;
    private int age;
    private double salary;
    public InterEmployee() {
    }
    public InterEmployee(String name, int age, double salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }
    @Override
    public String toString() {
        return "InterEmployee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                '}';
    }
    public InterEmployee clone() throws CloneNotSupportedException {
        return (InterEmployee) super.clone();
    }
}

这个时候就可以实现ImplementMyInterface的克隆,为什么呢,且看代码当中,两个类都实现了Cloneable接口,在ImplementMyInterface中先克隆出自己,这个时候原对象与克隆出的对象的变量的值为InterEmployee对象的同一个引用(可以理解为指针指向同一个地址)。再使用

implementMyInterface.employee=employee.clone();

就可以实现两个变量赋值为两个不同的对象引用了,这个时候不再是浅拷贝了。那InterEmployee怎么就能拷贝了呢,因为InterEmployee对象中全部都是基本类型,也实现了Cloneable接口,重写Clone方法很简单调用

super.clone();

就可以了。
补充一下:

Object o=new Object();
o.clone();//error

不能使用o.clone方法,有两个原因,可以查看Object的类的clone方法如下:

protected native Object clone() throws CloneNotSupportedException;

首先,是protected修饰,不在同一个包下,对这个方法是没有访问权限的,其次是由native修饰,native修饰就是说java使用了一个并不是java语言书写的方法,他的运行速度比我们写的要快的多因为更贴近系统。所以对一个对象实现克隆推荐使用默认的clone方法。
所有的数组都有一个公共的clone方法,而不是受保护的。所以可以直接使用array.clone()。

6.2 lambda表达式

  • lambda 表达式是一个可传递的代码块, 可以在以后执行一次或多次。
  • 即使 lambda 表达式没有参数, 仍然要提供空括号,就像无参数方法一样。
  • 如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型。
Comparator<String> comp
= (first, second) // Same as (String first, String second) -> first.lengthO - second.lengthO;
  • 无需指定 lambda 表达式的返回类型。lambda 表达式的返回类型总是会由上下文推导得出。
  • 如果一个 lambda 表达式只在某些分支返回一个值, 而在另外一些分支不返回值,这是不合法的。例如,(int x)-> { if(x >= 0) return 1; } 就不合法。

6.2.3 函数式接口

对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达式。这种接口称为函数式接口 ( functional interface )。把 lambda 表达式看作是一个函数,而不是一个对象, 另外要接受 lambda 表达式可以传递到函数式接口。
不能把丨ambda 表达式赋值给类型为 Object 的变量,Object 不是一个函数式接口。
Java API 在java.util.fimction 包中定义了很多非常通用的函数式接口。例如:java.util.function 包中有一个尤其有用的接口 Predicate:

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

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

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

6.2.4 方法引用

举个例子:

Timer t = new Timer(1000, event -> System.out.println(event)):
Timer t = new Timer(1000, Systei.out::println);//二者等价

主要有3种情况:

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod
    在前 2 种情况中,方法引用等价于提供方法参数的 lambda 表达式。前面已经提到,System.out::println 等价于 x -> System.out.println(x。) 类似地,Math::pow 等价于(x,y) ->Math.pow(x, y)。
    对于第 3 种情况, 第 1 个参数会成为方法的目标。例如String::compareToIgnoreCase 等同于 (x, y) -> x.compareToIgnoreCase(y) 。
    super 也是合法的。super::instanceMethod。

6.2.5 构造器引用

构造器引用与方法引用很类似,只不过方法名为 new。

ArrayList<String> names = . . .;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.col1ect(Col1ectors.toList());

Java 有一个限制,无法构造泛型类型 T 的数组。数组构造器引用对于克服这个限制很有用。
例如,假设我们需要一个 Person 对象数组。Stream 接口有一个 toArray 方法可以返回 Object 数组:

Object[] people = stream.toArray();

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

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

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

6.2.6 变量作用域

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

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

lambda 表达式的代码可能会在repeatMessage 调用返回很久以后才运行,而那时这个参数变量已经不存在了。 如何保留 text变量呢?
要了解到底会发生什么,下面来巩固我们对 lambda 表达式的理解 lambda 表达式有 3个部分:

  1. 一个代码块
  2. 参数
  3. 自由变量的值, 这是指非参数而且不在代码中定义的变量
    在我们的例子中, 这个 lambda 表达式有 1 个自由变量 text。表示 lambda 表达式的数据结构必须存储自由变量的值,在这里就是字符串 “Hello”。我们说它被 lambda 表达式捕获。关于代码块以及自由变量值有一个术语: 闭包(closure)。
    lambda 表达式可以捕获外围作用域中变量的值。 在 Java 中,要确保所捕获
    的值是明确定义的,这里有一个重要的限制。在 lambda 表达式中, 只能引用值不会改变的变量。例如, 下面的做法是不合法的:
public static void countDown(int start, int delay) {
	ActionListener listener = event ->
	 {
		start ; // Error: Can't mutate captured variable
		System.out.println(start);
	 };
	new Timer(delay, listener),start(); 
}

另外如果在 lambda 表达式中引用变量, 而这个变量可能在外部改变,这也是不合法的。lambda 表达式中捕获的变量必须实际上是最终变量 ( effectivelyfinal。)实际上的最终变量是指, 这个变量初始化之后就不会再为它赋新值。在这里,text 总是指示同一个 String 对象,所以捕获这个变量是合法的。
在方法中,不能有两个同名的局部变量, 因此, lambda 表达式中同样也不能有同名的局部变量。
在一个 lambda 表达式中使用 this 关键字时, 是指创建这个 lambda 表达式的方法的 this参数。

public class ApplicationO
{
	public void init() 
	{
		ActionListener listener * event -> 
		{
			System.out.print n(this.toStringO); 
			...
		}
	    ...
	}
}

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

6.2.7 处理lambda表达式

使用 lambda 表达式的重点是延迟执行。毕竟, 如果想耍立即执行代
码,完全可以直接执行, 而无需把它包装在一个Lambda 表达式中。之所以希望以后再执行代码, 这有很多原因, 如:

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

常用函数式接口。
常用函数式接口
表 6-2 列出了基本类型 int、 long 和 double 的 34 个可能的规范。 最好使用这些特殊化规范来减少自动装箱。
基本类型的函数式接口
最好使用表 6-1 或表 6-2 中的接口。 例如, 假设要编写一个方法来处理满足某个特定条件的文件。 对此有一个遗留接口 java.io.FileFilter, 不过最好使用标准的Predicate , 只有一种情况下可以不这么做, 那就是你已经有很多有用的方法可以生成 FileFilter 实例。大多数标准函数式接口都提供了非抽象方法来生成或合并函数。 例如, Predicate.isEqual(a) 等同于 a::equals, 不过如果 a 为 null 也能正常工作。已经提供了默认方法 and、or 和 negate 来合并谓词。 例如Predicate.isEqual⑻.or(Predicate.isEqual(b)) 就等同于 x -> a.equals(x) || b.equals(x)。
如果设计你自己的接口,其中只有一个抽象方法,可以用 @FunctionalInterface 注解来标记这个接口。这样做有两个优点。 如果你无意中增加了另一个非抽象方法, 编译器会产生一个错误消息。 另外 javadoc 页里会指出你的接口是一个函数式接口。

6.2.8 再谈Comparator

Comparator 接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda表达式或方法引用。
可以把比较器与 thenComparing 方法串起来。如下:

Arrays.sort(people ,
Comparator.comparing(Person::getlastName) 
.thenConipari ng(Pe rson::getFi rstName));

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

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

如果键函数可以返回 null, 可 能 就 要 用 到 nullsFirst 和 nullsLast 适配器。例如, 假设一个人没有中名时 getMiddleName 会返回一个 null, 就可以使用Comparator.comparing(Person::getMiddleName(),Comparator.nullsFirst(… )。nullsFirst 方法需要一个比较器,在这里就是比较两个字符串的比较器。naturalOrder 方法可以为任何实现了 Comparable 的类建立一个比较器。下面是一个完整的调用, 可以按可能为 null 的中名进行排序。这里使用了一个静态导人 java.util.C0mparator.*,以便理解这个表达式。注意 naturalOrder 的类型可以推导得出。

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

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

6.3 内部类

内部类( inner class) 是定义在另一个类中的类。为什么需要使用内部类呢?

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

内部类的对象有一个隐式引用, 它引用了实例化该内部对象的外围类对象。通过这个指针, 可以访问外围类对象的全部状态。在 Java 中,static 内部类没有这种附加指针。

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

内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。 内部类的对象总有一个隐式引用, 它指向了创建它的外部类对象。
内部类对象拥有一个对外围类对象的引用
这个引用在内部类的定义中是不可见的。外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器, 添加一个外围类引用的参数。
例如TimePrinter是TalkingClock的内部类:

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

outer 不是 Java 的关键字。我们只是用它说明内部类中的机制。
只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。

6.3.2 内部类的特殊语法规则

可以通过显式地命名将外围类引用设置为其他的对象。例如, 如果 TimePrinter 是一个公有内部类,对于任意的语音时钟都可以构造一个 TimePrinter:

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

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

OuterClass.InnerClass

内部类中声明的所有静态域都必须是 final。原因很简单。我们希望一个静态域只有一个实例, 不过对于每个外部对象, 会分别有一个单独的内部类实例。如果这个域不是 final , 它可能就不是唯一的。
内部类不能有 static 方法。Java 语言规范对这个限制没有做任何解释。也可以允许有静态方法,但只能访问外围类的静态域和方法。显然,Java 设计者认为相对于这种复杂性来说, 它带来的好处有些得不偿失。

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

内部类的语法很复杂(可以看到,稍后介绍的匿名内部类更加复杂 )。它与访问控制和安全性等其他的语言特性的没有明显的关联。
javap 程序运行 ReflectionTest 程序:TimePrinter是TalkingClock的一个内部类,这时会看到下面的输出结果:

public class TalkingClockSTimePrinter
{
	public TalkingGockJTimePrinter(TalkingCtock);
	public void actionPerformed(java.awt.event.ActionEvent);
	final TalkingClock this$(); 
}

可以清楚地看到, 编译器为了引用外围类, 生成了一个附加的实例域 this$0 (名字this$0 是由编译器合成的,在自己编写的代码中不能够引用它)。另外,还可以看到构造器的TalkingClock 参数.
内部类如何管理那些额外的访问特权呢?利用ReflectTest 程序査看一下 TalkingClock 类:

class TalkingClock
{
private int interval;
private boolean beep;
publ ic TalkingClock(int, boolean);
static boolean access$O(TalkingClock);
public void start(); 
}

请注意编译器在外围类添加静态方法 accessSO。它将返回作为参数传递给它的对象域beep。(方法名可能稍有不同,如 access$000, 这取决于你的编译器。)

6.3.4 局部内部类

内部类只在一个方法种创建这个对象使用一次的时候可以使用局部内部类。
例如:

public void start0 {
class TimePrinter implements ActionListener
{
	public void actionPerforaed(ActionEvent event) {
			System.out.println("At the tone, the tine is " + new Date());
			if (beep) Toolkit.getDefaultToolkit().beep(); 
		}
	 }
	ActionListener listener = new TimePrinter();
	Timer t = new Timer(interva1, listener); t.start(); 
}

局部类不能用 public 或 private 访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。局部类有一个优势, 即对外部世界可以完全地隐藏起来。

6.3.5 由外部方法访问变量

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

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

访问外部变量的控制流程:

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

为了能够让 actionPerformed方法工作,TimePrinter 类在 beep 域释放之前将 beep 域用start 方法的局部变量进行备份。
在 JavaSE 8 之前, 必须把从局部类访问的局部变量声明为 final。
例如, start 方法原本就应当这样声明, 从而使内部类能够访问 beep 参数:

public void start(int interval , final boolean beep);

6.3.6 匿名内部类

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

public void start(int interval, boolean beep) {
		ActionListener listener = new ActionListenerO
		{
			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(); 
}

通常的语法格式为:

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

由于构造器的名字必须与类名相同, 而匿名类没有类名,所以,匿名类不能有构造器。取而代之的是,将构造器参数传递给超类( superclass) 构造器。尤其是在内部类实现接口的时候, 不能有任何构造参数。还要像提供一组括号。
多年来,Java 程序员习惯的做法是用匿名内部类实现事件监听器和其他回调。如今最好还是使用 lambda 表达式。
下面的技巧称为“ 双括号初始化” (double brace initialization), 这里利用了内部类语法。

Object[] objects = new ArrayList<String>() {
            {
                add("I");
                add("Love");
                add("you");
            }
        }.toArray();

我们曾建议 equals 方法最好使用以下测试:

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

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

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

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

不过,这对于静态方法不奏效。毕竟, 调用 getClass 时调用的是 this.getClass(), 而静态方法没有 this。所以应该使用以下表达式:

new Object(){}.getCIass().getEndosingClass() // gets class of static method

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

6.3.7 静态内部类

有时候, 使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为 static, 以便取消产生的引用。只有内部类可以声明为 static。静态内部类的对象除了没有对生成它的外围类对象的引用特权外, 与其他所冇内部类完全一样。

  • 在内部类不需要访问外围类对象的时候, 应该使用静态内部类。 有些程序员用嵌套类 (nested class) 表示静态内部类。
  • 与常规内部类不同,静态内部类可以有静态域和方法。
  • 声明在接口中的内部类自动成为 static 和 public 类。

6.4 代理

这里就贴个例子吧。

package com.myProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Random;

/**
 * 100个数字里面查找一个数字
 * 使用代理类来跟踪调用的方法
 */
public class ProxyTest {
    public static void main(String[] args) {
        Object []objects=new Object[1000];
        for(int i=0;i<objects.length;i++){
            Integer integer=new Integer(i+1);
            TraceHandler handler=new TraceHandler(integer);
            Object proxy=Proxy.newProxyInstance(
                    ClassLoader.getSystemClassLoader(),
                    new Class[]{Comparable.class},handler
            );
            objects[i]=proxy;
        }
        Integer key=new Random().nextInt(objects.length)+1;
        int result= Arrays.binarySearch(objects,key);
        if (result>=0) System.out.println(objects[result]);
    }
}

class TraceHandler implements InvocationHandler{
    private Object target;

    public TraceHandler(Object object) {
        this.target = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //输出target的值
        System.out.print(target);
        //输出调用方法的名称
        System.out.print("."+method.getName()+"(");
        if (args!=null){
            for (int i=0;i<args.length;i++){
                System.out.print(args[i]);
                if (i<args.length-1) System.out.print(",");
            }
        }
        System.out.println(")");
        return method.invoke(target,args);
    }
}

克隆和代理是库设计者和工具构造者感兴趣的高级技术, 对应用程序员来说,它们并不十分重要。(我不这么认为)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值