20220424 Java核心技术 卷1 基础知识 6
接 口、lambda 表达式与内部类
- 接口(interface)技术主要用来描述类具有什么功能
- lambda 表达式是一种表示可以在将来某个时间点执行的代码块的简洁方法
- 内部类(inner class)技术主要用于设计具有相互协作关系的类集合
- 代理(proxy)是一种实现任意接口的对象
接 口
接口概念
在 Java 程序设计语言中, 接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
java.lang.Comparable
接口定义如下:
public interface Comparable<T> {
public int compareTo(T o);
}
接口中的所有方法自动地属于 public
。 因此, 在接口中声明方法时, 不必提供关键字 public
。在实现接口时, 必须把方法声明为 public
Comparable
接口中还有一个没有明确说明的附加要求: 在调用 x.compareTo(y)
的时候,这个 compareTo
方法必须确实比较两个对象的内容, 并返回比较的结果。 当 x 小于 y 时, 返回一个负数;当 x 等于 y 时, 返回 0 ;否则返回一个正数。
在接口中还可以定义常量,绝不能含有实例域。
提供了静态 Integer.compare
、Double.compare
方法
语 言 标 准 规 定: 对 于 任 意 的 x 和 y, 实 现 必 须 能 够 保 证 sgn(x.compareTo(y)) = -sgn(y.compareTo(x))
。compareTo
与 equals
方 法 一 样, 在 继 承 过 程 中 有 可 能 会 出 现 问 题。
解决方法:如果存在这样一种通用算法,它能够对两个不同的子类对象进行比较,则应该在超类中提供一个 compareTo
方法,并将这个方法声明为 final
, 例如,假设不管薪水的多少都想让经理大于雇员,像 Executive
和 Secretary
这样的 子类又该怎么办呢?如果一定要按照职务排列的话,那就应该在 Employee
类中提供一个 rank
方法,,每个子类覆盖 rank
,并实现一个考虑 rank
值的 compareTo
方法。
接口的特性
可以使用 instanceof
检查一个对象是否实现了某个特定的接口
接口中的域将被自动设为 public static final
有些接口只定义了常量, 而没有定义方法。 例如 javax.swing.SwingConstants
,任何实现 SwingConstants
接口的类都自动地继承了这些常量, 并可以在方法中直接地引用 NORTH
,而不必采用 SwingConstants.NORTH
这样的繁琐书写形式。然而,这样应用接口似乎有点偏离了接口概念的初衷, 最好不要这样使用它
接口与抽象类
有些程序设计语言允许一个类有多个超类, 例如 C++ 。我们将此特性称为 多重继承 (multiple inheritance)。而 Java 的设计者选择了不支持多继承,其主要原因是多继承会让语言本身变得非常复杂(如同 C++ ) ,效率也会降低 (如同 Eiffel ) 。
实际上, 接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。
静态方法
在 Java SE 8 中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的。 只是这有违于将接口作为抽象规范的初衷。
目前为止, 通常的做法都是将静态方法放在伴随类中。在标准库中, 你会看到成对出现的接口和实用工具类, 如 Collection
/ Collections
或 Path
/ Paths
。
从 Java 8 以后,实现你自己的接口时,不再需要为实用工具方法另外提供一个伴随类。
默认方法
可以为接口方法提供一个默认实现。 必须用 default
修饰符标记这样一个方法。
默认方法可以调用任何其他方法。
在 JavaAPI 中,你会看到很多接口都有相应的伴随类,这个伴随类中实现了相应接口的部分或所有方法, 如 Collection
/ AbstractCollection
或 MouseListener
/ MouseAdapter
。在 JavaSE 8 中, 这个技术已经过时。现在可以直接在接口中实现方法。
默认方法的一个重要用法是 接口演化 ( interface evolution ) ,假设新增方法不是一个默认方法。那么之前的实现类将不能编译, 因为它没有实现这个新方法。为接口增加一个非默认方法不能保证 源代码兼容
解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法, 然后又在超类或另一个接口中定义了同样的方法, 会发生什么情况? 规则如下:
- 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的接口默认方法会被忽略
- 接口冲突。 如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型 (不论是否是默认参数)相同的方法, 必须覆盖这个方法来解决冲突
类优先 规则可以确保与 Java SE 7 的兼容性。 如果为一个接口增加默认方法,这对于有这个默认方法之前能正常工作的代码不会有任何影响。
千万不要让一个默认方法重新定义 Object 类中的某个方法。 例如, 不能为 toString
或 equals
定义默认方法, 尽管对于 List
之类的接口这可能很有吸引力, 由于 类优先 规则, 这样的方法绝对无法超越 Object.toString
或 Objects.equals
。
接口示例
接口与回调
回调(callback )是一种常见的程序设计模式。在这种模式中, 可以指出某个特定事件发生时应该采取的动作。
在 java.swing
包中有一个 Timer
类,可以使用它在到达给定的时间间隔时发出通告。 例如,假如程序中有一个时钟, 就可以请求每秒钟获得一个通告, 以便更新时钟的表盘。在构造定时器时,需要设置一个时间间隔, 并告之定时器,当到达时间间隔时需要做些什么操作。如何告之定时器做什么呢? 在很多程序设计语言中,可以提供一个函数名, 定时器周期性地调用它。 但是, 在 Java 标准类库中的类采用的是面向对象方法。它将某个类的对象传递给定时器,然后,定时器调用这个对象的方法。由于对象可以携带一些附加的信息,所以传递一个对象比传递一个函数要灵活得多。当然, 定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了 java.awt.event
包的 ActionListener
接口。下面是这个接口:
public interface ActionListener extends EventListener {
public void actionPerformed(ActionEvent e);
}
Comparator 接口
Arrays.sort
方法还有第二个版本, 有一个数组和一个比较器 ( comparator ) 作为参数, 比较器是实现了 Comparator
接口的类的实例。
java.util.Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
}
compare
方法要在比较器对象上调用, 而 compareTo
是在字符串本身上调用。
对象克隆
clone
方法是 Object
的一个 protected
方法, 这说明你的代码不能直接调用这个方法,必须要重写了 clone
方法后才可以直接调用。
默认的克隆操作是 “浅拷贝”,并没有克隆对象中引用的其他对象。 如果原对象和浅克隆对象共享的子对象是不可变的, 那么这种共享就是安全的。 如果子对象属于一个不可变的类, 如 String
,就是这种情况。或者在对象的生命期中, 子对象一直包含不变的常量, 没有更改器方法会改变它, 也没有方法会生成它的引用,这种情况下同样是安全的。 不过, 通常子对象都是可变的, 必须重新定义 clone
方法来建立一个深拷贝, 同时克隆所有子对象。
Date
是可变的,LocalDate
是不可变的。
对于每一个类,需要确定:
- 默认的
clone
方法是否满足要求 - 是否可以在可变的子对象上调用
clone
来修补默认的clone
方法 - 是否不该使用
clone
实际上第 3 个选项是默认选项。 如果选择第 1 项或第 2 项,类必须:
- 实现
Cloneable
接口; - 重新定义
clone
方法,并指定public
访问修饰符
Cloneable
接口是 Java 提供的一组标记接口 ( tagging interface ) 之一。 标记接口不包含任何方法; 它唯一的作用就是允许
在类型查询中使用 instanceof
,建议你自己的程序中不要使用标记接口。
即使 clone
的默认(浅拷贝)实现能够满足要求, 还是需要实现 Cloneable
接口, 将 clone
重新定义为 public
, 再调用 super.clone()
。
在 Java SE 1.4 之前, clone
方法的返回类型总是 Object
, 而现在可以为你的 clone
方法指定正确的返回类型。这是协变返回类型的一个例子
下面来看创建深拷贝的 clone
方法的一个例子:
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
。
保留 throws
说明符非常适用于 final
类。这样就允许子类在不支持克隆时选择抛出一个 CloneNotSupportedException
。
标准库中只有不到 5% 的类实现了 clone
。
所有数组类型都有一个 public
的 clone
方法, 而不是 protected
。可以用这个方法建立一个新数组, 包含原数组所有元素的副本。
lambda 表达式
lambda 表达式是这些年来 Java 语言最让人激动的变化之一
为什么引入 lambda 表达式
lambda 表达式是一个可传递的代码块, 可以在以后执行一次或多次。
在 Java 中传递一个代码段并不容易, 不能直接传递代码段。Java 是一种面向对象语言, 所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。
在其他语言中,可以直接处理代码块。Java 设计者很长时间以来一直拒绝增加这个特性。毕竟,Java 的强大之处就在于其简单性和一致性。 如果只要一个特性能够让代码稍简洁一些,就把这个特性增加到语言中, 这个语言很快就会变得一团糟,无法管理。
lambda 表达式的语法
Comparator<String> comparator1 = (String first, String second) -> {
return first.length() - second.length();
};
为什么叫 lambda ? 逻辑学家 Alonzo Church 想要形式化地表示能有效计算的数学函数。 他使用了希腊字母 lambda ( λ ) 来标记参数
- 即使 lambda 表达式没有参数, 仍然要提供空括号,就像无参数方法一样
- 如果可以推导出一个 lambda 表达式的参数类型,则可以忽略其类型
- 如果方法只有一 参数, 而且这个参数的类型可以推导得出,可以省略包围参数的小括号
- 无需指定 lambda 表达式的返回类型。lambda 表达式的返回类型总是会由上下文推导得出
Comparator<String> comparator2 = (first, second) -> first.length() - second.length();
函 数 式 接 口
Java 中已经有很多封装代码块的接口,如 ActionListener
或 Comparator
- lambda表达式与这些接口是兼容的
对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达式。这种接口称为函数式接口 (functional interface )。
你可能想知道为什么函数式接口必须有一个抽象方法。不是接口中的所有方法都是抽象的吗? 实际上, 接口完全有可能重新声明 Object 类的方法, 如 toString
或 clone
,这些声明有可能会让方法不再是抽象的。( Java API 中的一些接口会重新声明 Object
方法
来附加 javadoc 注释。Comparator
API 就是这样一个例子。) 更重要的是,在 JavaSE 8 中, 接口可以声明非抽象方法。
为了展示如何转换为函数式接口, 下面考虑 Arrays.sort
方法。它的第二个参数需要一个 Comparator
实例。
在底层, Arrays.sort
方法会接收实现了 Comparator<String>
的某个类的对象。在这个对象上调用 compare 方法会执行这个 lambda 表达式的方法体。
最好把 lambda 表达式看作是一个函数, 而不是一个对象, 另外要接受 lambda 表达式可以传递到函数式接口。
lambda 表达式可以转换为接口, 这一点让 lambda 表达式很有吸引力。
实际上,在 Java 中, 对 lambda 表达式所能做的也只是能转换为函数式接口。在其他支持函数字面量的程序设计语言中,可以声明函数类型(如 (String, String) -> int
)、 声明这些类型的变量,还可以使用变量保存函数表达式。不过,Java 设计者还是决定保持我们熟悉的接口概念, 没有为 Java 语言增加函数类型。
甚至不能把 lambda 表达式赋值给类型为 Object
的变量,Object
不是一个函数式接口。
//Object comparator3 = (first, second) -> first.length() - second.length(); // 编译报错:Cannot resolve method 'length()'
Java API 在 java.util.function
包中定义了很多非常通用的函数式接口。其中一个接口 BiFunction<T, U, R>
描述了参数类型为 T 和 U 而且返回类型为 R 的函数。可以把我们的字符串比较 lambda 表达式保存在这个类型的变量中:
BiFunction<String, String, Integer> biFunction = (first, second) -> first.length() - second.length();
不过, 这对于排序并没有帮助。没有哪个 Arrays.sort
方法想要接收一个 BiFunction
。
类似 Comparator
的接口往往有一个特定的用途, 而不只是提供一个有指定参数和返回类型的方法。Java SE 8 沿袭了这种思路。想要用 lambda 表达式做某些处理,还是要谨记表达式的用途, 为它建立一个特定的函数式接口。
方法引用
有时, 可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。
假设你想对字符串排序, 而不考虑字母的大小写。可以传递以下方法表达式:
Comparator<String> comparator4 = String::compareToIgnoreCase;
从这些例子可以看出, 要用 ::
操作符分隔方法名与对象或类名。主要有 3 种情况:
-
Object::instanceMethod
System.out::println 等价于 x -> System.out.println(x)
-
Class::staticMethod
Math::sqrt 等价于 x -> Math.sqrt(x)
-
Class::instanceMethod
String::compareToIgnoreCase 等价于 (x, y) -> x.compareToIgnoreCase(y)
在前 2 种情况中, 方法引用等价于提供方法参数的 lambda 表达式。 对于第 3 种情况, 第 1 个参数会成为方法的目标。
如果有多个同名的重栽方法, 编译器就会尝试从上下文中找出你指的那一个方法。
类似于 lambda 表达式, 方法引用不能独立存在,总是会转换为函数式接口的实例
可以在方法引用中使用 this
、super
参数。 例如,this::equals
等同于 x -> this.equals(x)
。
构造器引用
构造器引用与方法引用很类似,只不过方法名为 new
。例如,Person::new
是 Person
构造器的一个引用。哪一个构造器呢? 这取决于上下文。
可以用数组类型建立构造器引用。例如, int[]::new
是一个构造器引用,它有一个参数:即数组的长度。 这等价于 lambda 表达式 x-> new int[x]
Java 有一个限制,无法构造泛型类型 T 的数组。数组构造器引用对于克服这个限制很有用。
例如,假设我们需要一个 Person 对象数组。Stream
接口有一个 toArray
方法可以返回 Object
数组:
Object[] people = stream.toArray();
不过,这并不让人满意。用户希望得到一个 Person
引用数组,而不是 Object
引用数组。流库利用构造器引用解决了这个问题。可以把 Person[]::new
传入 toArray
方法:
Person[] people = stream.toArray(Person[]::new):
PersonTest[] personTests1 = new PersonTest[10];
List<PersonTest> list = Arrays.asList(personTests1);
PersonTest[] personTests2 = list.toArray(new PersonTest[0]);
PersonTest[] personTests3 = list.stream().toArray(PersonTest[]::new);
double[] doubles1 = new double[]{1D, 2D, 3D};
List<Double> doubleList = DoubleStream.of(doubles1).boxed().collect(Collectors.toList());
Double[] doubles2 = doubleList.stream().toArray(Double[]::new);
double[] doubles3 = Stream.of(doubles2).mapToDouble(Double::doubleValue).toArray();
变量作用域
public class VarTest {
public static void main(String[] args) throws IOException {
repeatMessage("Hello", 1000);
System.in.read();
}
public static void repeatMessage(String text, int delay) {
ActionListener listener = event -> {
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay, listener).start();
}
}
看 lambda 表达式中的变量 text
。 注意这个变量并不是在这个 lambda 表达式中定义的。实际上,这是 repeatMessage
方法的一个参数变量。
lambda 表达式的代码可能会在 repeatMessage
调用返回很久以后才运行, 而那时这个参数变量已经不存在了。 如何保留 text
变量呢?
lambda 表达式有 3 个部分:
- 一个代码块
- 参数
- 自由变量的值, 这是指非参数而且不在代码中定义的变量
在我们的例子中, 这个 lambda 表达式有 1 个自由变量 text
。 表示 lambda 表达式的数据结构必须存储自由变量的值, 在这里就是字符串 "Hello"。 我们说它被 lambda 表达式捕获(captured)。
关于代码块以及自由变量值有一个术语: 闭包( closure )。 在 Java 中, lambda 表达式就是闭包 。
lambda 表达式可以捕获外围作用域中变量的值。
这里有一条规则:lambda 表达式中捕获的变量必须 实际上是最终变量 ( effectively final ) 。实际上的最终变量是指, 这个变量初始化之后就不会再为它赋新值。
lambda 表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在 lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
在一个 lambda 表达式中使用 this
关键字时, 是指创建这个 lambda 表达式的方法的 this
参数。
处理 lambda 表达式
使用 lambda 表达式的重点是 延迟执行 ( deferred execution ) 。之所以希望以后再执行代码, 这有很多原因, 如:
- 在一个单独的线程中运行代码
- 多次运行代码
- 在算法的适当位置运行代码 (例如, 排序中的比较操作)
- 发生某种情况时执行代码 (如, 点击了一个按钮, 数据到达, 等等)
- 只在必要时才运行代码
常 用 函 数 式 接 口 :
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 | 其他方法 |
---|---|---|---|---|---|
Runnable |
无 | void | run |
作为无参数或返回值的动作运行 | |
Supplier<T> |
无 | T | get |
提供一个 T 类型的值 | |
Consumer<T> |
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 | T | apply |
类型 T 上的一元操作符 | compose , andThen , identity |
BinaryOperator<T> |
T,T | T | apply |
类型 T 上的二元操作符 | andThen , maxBy , minBy |
Predicate<T> |
T | boolean | test |
布尔值函数 | and , or , negate , isEqual |
BiPredicate<T, U> |
T, U | boolean | test |
有两个参数的布尔值函数 | and , or , negate |
下表列出了基本类型 int
、 long
和 double
的 34 个可能的规范。 最好使用这些特殊化规范来减少自动装箱。
基本类型的函数式接口:
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 |
---|---|---|---|
BooleanSupplier |
none | boolean | getAsBoolean |
(R)Supplier |
none | (P) | getAs(R) |
(R)Consumer |
(P) | void | accept |
Obj(R)Consumer<T> |
T, (P) | void | accept |
(R)Function<T> |
(P) | T | apply |
(R)To(S)Function |
(P) | (Q) | applyAs(S) |
To(R)Functioi<T> |
T | (P) | applyAs(R) |
To(R)BiFunction<T,U> |
T, U | (P) | applyAs(R) |
(R)UnaryOperator |
(P) | (P) | applyAs(R) |
(R)BinaryOperator |
(P), (P) | (P) | applyAs(R) |
(R)Pedicate |
(P) | boolean | test |
(P) ,(Q) 为 int
, long
, double
; (R) ,(S) 为 Int
, Long
, Double
最好使用上表中的接口。 例如, 假设要编写一个方法来处理满足某个特定条件的文件。 对此有一个遗留接口 java.io.FileFilter
, 不过最好使用标准的 Predicate<File>
, 只有一种情况下可以不这么做, 那就是你已经有很多有用的方法可以生成 FileFilter
实例。
大多数标准函数式接口都提供了非抽象方法来生成或合并函数。 例如, 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 页里会指出你的接口是一个函数式接口。
并不是必须使用注解根据定义, 任何有一个抽象方法的接口都是函数式接口。不过使用 @FunctionalInterface
注解确实是一个很好的做法。
再谈 Comparator
Comparator
接口包含很多方便的静态方法来创建比较器。 这些方法可以用于 lambda 表达式或方法引用。
静态 comparing
方法取一个 “键提取器” 函数, 它将类型 T 映射为一个可比较的类型 ( 如 String )。 对要比较的对象应用这个函数, 然后对返回的键完成比较。
Arrays.sort(people, Comparator.comparing(Person::getName));
可以把比较器与 thenComparing
方法串起来。
Arrays.sort(people, Comparator.comparing(Person::getLastName).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.comparingInt(p -> p.getName().length()));
如果键函数可以返回 null
, 可 能 就 要 用 到 nullsFirst
和 nullsLast
适配器。 这些静态方法会修改现有的比较器,从而在遇到 null
值时不会抛出异常, 而是将这个值标记为小于或大于正常值。
Arrays.sort(people, Comparator.comparing(Person::getFirstName, Comparator.nullsFirst(Comparator.naturalOrder())));
静态 reverseOrder
方法会提供自然顺序的逆序。要让比较器逆序比较, 可以使用 reversed
实例方法。例如 naturalOrder().reversed()
等同于 reverseOrder()
内部类
内部类( inner class ) 是定义在另一个类中的类。为什么需要使用内部类呢? 其主要原因有以下三点:
- 内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据
- 内部类可以对同一个包中的其他类隐藏起来
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名 (anonymous) 内部类比较便捷
Java 内部类的对象有一个隐式引用, 它引用了实例化该内部对象的外围类对象。通过这个指针, 可以访问外围类对象的全部状态。static
内部类没有这种附加指针,
使用内部类访问对象状态
内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域
内部类的对象总有一个隐式引用, 它指向了创建它的外部类对象,这个引用在内部类的定义中是不可见的
只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性
内部类的特殊语法规则
内部类有一个外围类的引用,使用外围类引用的语法:
OuterClass.this
可以采用下列语法格式更加明确地编写内部对象的构造器:
outerObject.new InnerClass (construction parameters)
在外围类的作用域之外,可以这样引用内部类:
OuterClass.InnerClass
内部类中声明的所有静态域都必须是 final
。原因很简单。我们希望一个静态域只有一个实例, 不过 对于每个外部对象, 会分别有一个单独的内部类实例。如果这个域不是 final
, 它可能就不是唯一的。
内部类不能有 static
方法。
内部类是否有用、必要和安全
内部类是一种编译器现象, 与虚拟机无关。编译器将会把内部类翻译成用 $
(美元符号)分隔外部类名与内部类名的常规类文件, 而
虚拟机则对此一无所知。
在 TalkingClock
类内部的 TimePrinter
类将被翻译成类文件 TalkingClock$TimePrinter.class
局部内部类
局部类不能用 public
或 private
访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。
局部类有一个优势, 即对外部世界可以完全地隐藏起来。
由外部方法访问变量
与其他内部类相比较, 局部类还有一个优点。它们不仅能够访问包含它们的外部类, 还可以访问局部变量。不过, 那些局部变量必须事实上为 final
。
局部内部类会备份使用到的局部变量,编译器编译后,会在类定义中使用 final 实例变量备份使用到的局部变量,可以通过反射验证。
在 JavaSE 8 之前, 必须把从局部类访问的局部变量声明为 final
。
有时,final
限制显得并不太方便。 例如,要统计内部类方法执行的次数,直接使用 int 是不行的,可以使用引用类型的对象来代替,例如 AtomicInteger
或 int[1]
匿名内部类
假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class )。
通常的语法格式为:
new SuperType(construction parameters)
{
inner class methods and data
}
SuperType 可以是接口,也可以是类
由于构造器的名字必须与类名相同, 而匿名类没有类名, 所以, 匿名类不能有构造器。取而代之的是,将构造器参数传递给超类 (superclass) 构造器。尤其是在内部类实现接口的时候, 不能有任何构造参数。
多年来,Java 程序员习惯的做法是用匿名内部类实现事件监听器和其他回调。 如今最好还是使用 lambda 表达式。
双括号初始化 :
package v1ch06;
import java.util.*;
public class TwoInit {
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {
{
add("a");
add("b");
add("c");
}
};
System.out.println(list); // [a, b, c]
System.out.println(list.getClass()); // class v1ch06.TwoInit$1
System.out.println(list.getClass().getSuperclass()); // class java.util.ArrayList
System.out.println(Arrays.toString(list.getClass().getInterfaces())); // []
System.out.println(list.getClass().getEnclosingClass()); // class v1ch06.TwoInit
Map<String, String> map = new HashMap<String, String>(){
{
put("a", "1");
put("b", "2");
put("c", "3");
}
};
System.out.println(map); // {a=1, b=2, c=3}
System.out.println(map.getClass()); // class v1ch06.TwoInit$2
System.out.println(map.getClass().getSuperclass()); // class java.util.HashMap
System.out.println(Arrays.toString(map.getClass().getInterfaces())); // []
System.out.println(map.getClass().getEnclosingClass()); // class v1ch06.TwoInit
}
}
注意这里的双括号。 外层括号建立了 ArrayList 的一个匿名子类。 内层括号则是一个对象构造块。
建立一个与超类大体类似(但不完全相同)的匿名子类通常会很方便。不过, 对于 equals
方法要特别当心,对匿名子类做这个测试时会失败
if (getClass() != other.getClass()) return false;
静态内部类
有时候, 使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为 static
, 以便取消产生的引用。
只有内部类可以声明为 static
。静态内部类的对象除了没有对生成它的外围类对象的引用特权外, 与其他所冇内部类完全一样。
与常规内部类不同,静态内部类可以有静态域和方法。
声明在接口中的内部类自动成为 static
和 public
类。
代理
利用代理可以在运行时创建一个实现了一组给定接口的新类:这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
何时使用代理
代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。尤其是,它具有下列方法:
- 指定接口所需要的全部方法
Object
类中的全部方法, 例如,toString
、equals
等
不能在运行时定义这些方法的新代码。而是要提供一个调用处理器( invocation handler ) 。调用处理器是实现了 InvocationHandler
接口的类对象。在这个接口中只有一个方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
无论何时调用代理对象的方法, 调用处理器的 invoke
方法都会被调用, 并向其传递 Method
对象和原始的调用参数。 调用处理器必须给出处理调用的方式。
创建代理对象
要想创建一个代理对象, 需要使用 Proxy 类的 newProxylnstance 方法。 这个方法有三个参数:
- 一个类加栽器( class loader)。作为 Java 安全模型的一部分, 对于系统类和从因特网上下载下来的类,可以使用不同的类加载器。目前, 用
null
表示使用默认的类加载器 - 一个
Class
对象数组, 每个元素都是需要实现的接口 - 一个调用处理器
代理类的特性
代理类是在程序运行过程中创建的。 然而, 一旦被创建, 就变成了常规类, 与虚拟机中的任何其他类没有什么区别。
所有的代理类都扩展于 Proxy
类。一个代理类只有一个实例域—调用处理器,它定义在 Proxy
的超类中。 为了履行代理对象的职责, 所需要的任何附加数据都必须存储在调用处理器中。
所有的代理类都覆盖了 Object
类中的方法 toString
、 equals
和 hashCode
。如同所有的代理方法一样,这些方法仅仅调用了调用处理器的 invoke
。Object 类中的其他方法(如 clone
和 getClass
) 没有被重新定义。
没有定义代理类的名字,Sun 虚拟机中的 Proxy
类将生成一个以字符串 $Proxy
开头的类名。
对于特定的类加载器和预设的一组接口来说, 只能有一个代理类。 也就是说, 如果使用同一个类加载器和接口数组调用两次 newProxyInstance
方法的话, 那么只能够得到同一个类的两个对象,也可以利用 getProxyClass
方法获得这个类:
Class proxyClass = Proxy.getProxyClass(null, interfaces);
代理类一定是 public
和 final
。 如果代理类实现的所有接口都是 public
, 代理类就不属于某个特定的包;否则, 所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。
可以通过调用 Proxy
类中的 isProxyClass
方法检测一个特定的 Class
对象是否代表一个代理类。