接口、lambda
表达式与内部类
6.1接口
6.1.1接口的概念
与
equals
方法一样,Comparable
接口在继承中有可能会出现问题。例如,Manager
扩展了Employee
,而后者实现的是Comparable<Employee>
,而不是Comparable<Manager>
。如果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
。
这种情况与equals
方法一样,补救方式也一样。有两种不同的情况:
- 如果不同子类中的比较有不同的含义,就应该将属于不同类的对象之间的比较视为非法。每个
compareTo
方法都应该在开始时进行以下检测:if (getClass() != other.getClass()) throw new ClassCastException();
。- 如果存在一个能够比较子类对象的通用算法,那么可以在超类中提供一个
compareTo
方法,并将这个方法声明为final
。例如,如果要按照职务排列,就应该在Employee
类中提供一个rank
方法。让每个子类覆盖rank
,并实现一个考虑rank
值的compareTo
方法。
6.1.2接口的属性
接口中的所有方法都自动是
public
方法。接口可以定义常量,并且总是public static final
的,但是绝不会有实例字段,不过可以提供简单方法,当然,这些方法不能引用实例字段。
6.1.5默认方法
可以为接口方法提供一个默认实现。必须用
default
修饰符标记这样一个方法,这样子类就不用必须实现。其中一个重要的用法是接口演化。
6.1.6解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,在java中的规则如下:
- 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
- 接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
interface Person {
default String getName() { return ""; }
}
interface Named {
default String getName() { return getClass().getName() + "_" + hashCode(); }
}
// 类会继承Person和Named接口提供的两个不一致的getName方法。并不是从中选择一个。Java编译器会报告一个错误,
// 让程序员来解决这个二义性问题。只需要在Student类中提供一个getName方法即可。在这个方法中,可以选择两个
// 冲突方法中的一个:
class Student implements Person, Named {
public String getName() {
return Person.super.getName();
}
}
6.1.7接口与回调
回调是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。
public class TimerTest {
public static void main(String[] args) {
var listener = new TimePrinter();
var timer = new Timer(1000, listener);
timer.start();
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
Toolkit.getDefaultToolkit().beep();
}
}
6.1.9对象克隆
Cloneable
接口指示一个类提供了一个安全的clone
方法。需要注意的是,clone
是Object
的一个protected
方法,这说明你的代码不能直接调用这个方法。例如,只有Employee
类可以克隆Employee
对象。这个限制是有原因的。想想看Object
类如何实现clone
。它对于这个对象一无所知,所以只能逐个字段地进行拷贝。如果对象中的所有数据字段都是数值或其他基本类型,拷贝这些字段没有任何问题。但是如果对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。
默认情况下,克隆操作是浅拷贝,并没有克隆对象中引用的其他对象。浅拷贝会有什么影响吗?这要看具体情况。如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如果子对象属于一个不可变的类,如String
,就是这种情况。或者在对象的生命周期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的。
不过,通常子对象都是可变的,必须重新定义clone
方法来建立一个深拷贝,同时克隆所有子对象。
对于每一个类,需要确定:
- 默认的
clone
方法是否满足要求。- 是否可以在可变的子对象上调用
clone
来修补默认的clone
方法。- 是否不该使用
clone
。实际上第3个选项是默认选项。如果选择第1或第2项,类必须:
- 实现
Cloneable
接口。- 重新定义
clone
方法,并指定public
访问修饰符。
Object
类中的clone
方法声明为protected
,所以你的代码不能直接调用anObject.clone()
。由于受保护访问的规则比较微妙,子类只能调用受保护的clone
方法来克隆它自己的对象。必须重新定义clone
为public
才能允许所有方法克隆对象。
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
Employee original = new Employee("John Q. Public", 50000);
original.setHireDay(2000, 1, 1);
Employee copy = original.clone();
copy.raiseSalary(10);
copy.setHireDay(2002, 12, 31);
System.out.println("original = " + original);
System.out.println("copy = " + copy);
// false
System.out.println(original == copy);
}
}
class Employee implements Cloneable {
private String name;
private double salary;
private Date hireDay;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
hireDay = new Date();
}
@Override
public Employee clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
public void setHireDay(int year, int month, int day) {
Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime();
hireDay.setTime(newHireDay.getTime());
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
}
class Manager extends Employee {
private double bonus;
public Manager(String name, double salary) {
super(name, salary);
}
public double getBonus() {
return bonus;
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
}
必须当心子类的克隆。例如,一旦为
Employee
类定义了clone
方法,任何人都可以用它来克隆Manager
对象。Employee
克隆方法能完成工作吗?这取决于Manager
类的字段。在这里是没有问题的,因为bonus
字段是基本类型。但是Manager
可能会有需要深拷贝或不可克隆的字段。不能保证子类的实现者一定会修正clone
方法让它正常工作。出于这个原因,在Object
类中,clone
方法声明为protected
。不过,如果希望类用户调用clone
,就不能这样做。
要不要在自己的类中实现clone
呢?如果客户需要建立深拷贝,可能就需要实现这个方法,所以依情况而定。
6.2Lambda表达式
Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。
6.2.3函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口。
Arrays.sort(words, (first, second) -> first.length() - second.length());
在底层,
Arrays.sort
方法会接受实现了Comparator<String>
的某个类的对象。在这个对象上调用compare
方法会执行这个lambda表达式的体。这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能高效得多。最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口。
Lambda表达式可以转换为接口,这一点让lambda表达式很有吸引力:
var timer = new Timer(1000, event -> {
System.out.println("At the tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
Toolkit.getDefaultToolkit().beep();
});
实际上,在java中,对lambda表达式所能做的也只是转换为函数式接口。因此,不能把lambda表达式赋给类型为
Object
的变量,Object
不是函数式接口。
6.2.4方法引用
有时,lambda表达式只涉及一个方法调用:
var timer = new Timer(1000, event -> System.out.println(event));
// 等价于
var timer = new Timer(1000, System.out::println);
表达式
System.out::println
是一个方法引用,它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。在这个例子中,会生成一个ActionListener
,它的actionPerformed(ActionEvent e)
方法要调用System.out.println(e)
。
类似于lambda表达式,方法引用也不是一个对象。不过,为一个类型为函数式接口的变量赋值时会产生一个对象。
要用::
运算符分隔方法名与对象或类名主要有3种情况:
object::instanceMethod
Class::instanceMethod
Class::staticMethod
在第1种情况下,方法引用等价于向方法传递参数的lambda表达式。对于
System.out::println
,对象是System.out
,所以方法表达式等价于x -> System.out.println(x)
。
对于第2种情况,第1个参数会成为方法的隐式参数。例如,String::compareToIgnoreCase
等同于(x, y) -> x.compareToIgnoreCase(y)
。
在第3种情况下,所有参数都传递到静态方法:Math::pow
等价于(x, y) -> Math.pow(x, y)
。
方法引用 | 等价的lambda表达式 | 说明 |
---|---|---|
separator::equals | x -> separator.equals(x) | 这是包含一个对象和一个实例方法的方法表达式。Lambda参数作为这个方法的显式参数传入 |
String::trim | x -> x.trim() | 这是包含一个类和一个实例方法的方法表达式。Lambda表达式会成为隐式参数 |
String::concat | (x, y) -> x.concat(y) | 同样,这里有一个实例方法,不过这次由一个显式参数。与前面一样,第一个lambda参数会成为隐式参数,其余的参数会传递到方法 |
Integer::valueOf | x -> Integer::valueOf(x) | 这是包含一个静态方法的方法表达式。Lambda参数会传递到这个静态方法 |
Integer::sum | (x, y) -> Integer::sum(x, y) | 这是另一个静态方法,不过这一次有两个参数。两个lambda参数都传递到这个静态方法。Integer.sum 方法专门创建为作为一个方法引用。对于lambda表达式,可以只写作(x, y) -> x + y |
Integer::new | x -> new Integer(x) | 这是一个构造器引用。Lambda参数传递到这个构造器 |
Integer[]::new | n -> new Integer[n] | 这是一个数组构造器引用。Lambda参数是数组长度 |
注意,只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。
包含对象的方法引用与等价的lambda表达式还有一个细微的差别。考虑一个方法引用,如
separator::equals
。如果separator
为null
,构造separator::equals
时就会立即抛出一个NullPointerException
异常。Lambda表达式x -> separator.equals(x)
只在调用时才会抛出NullPointerException
。
可以在方法引用中使用this
参数。例如,this::equals
等同于x -> this.equals(x)
。使用super
也是合法的。使用super
作为目标,会调用给定方法的超类版本:
class Greeter {
public void greet(ActionEvent event) {
System.out.println("Hello, the time is " + Instant.ofEpochMilli(event.getWhen()));
}
}
class RepeatedGreeter extends Greeter {
public void greet(ActionEvent event) {
// 等价于event -> super.greet(event)
var timer = new Timer(1000, super::greet);
timer.start();
}
}
6.2.5构造器引用
构造器引用与方法引用很类似,只不过方法名为
new
,并且使用哪一个构造器取决于上下文。
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());
// 如果有多个Person构造器,编译器会选择有一个String参数的构造器,以为它从上下文
// 推导出这是在调用带一个字符串的构造器
可以用数组类型建立构造器引用。例如,
int[]::new
是一个构造器引用,它有一个参数,即数组的长度。这等价于lambda表达式x -> new int[x]
。
Java有一个限制,无法构造泛型类型为T的数组。数组构造器引用对于克服这个限制很有用。表达式new T[n]
会产生错误,因为这会改为new Object[n]
。对于开发类库的人来说,这是一个问题。因此,更好的解决方式是:
Person[] people = stream.toArray(Person[]::new);
// toArray方法调用这个构造器来得到一个有正确类型的数组,
// 然后填充并返回这个数组
6.2.6变量作用域
能够在lambda表达式中访问外围方法或类中的变量。具体的实现细节在于,可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。不过需要注意的是,在lambda表达式中,只能引用值不会改变的变量(事实最终变量,即这个变量初始化之后就不会再为它赋新值)。
这个限制是有原因的。如果在lambda表达式中更改变量,并发执行多个动作时就会不安全。因此,下面代码都是不合法的:
public static void countDown(int start, int delay) {
ActionListener listener = event -> {
// Error: Can't mutate captured variable
// Variable used in lambda expression should be final or effectively final
start--;
// 可以访问,但是不能修改。
System.out.println(start);
};
new Timer(delay, listener).start();
}
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表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的:
Path first = Path.of("/usr/bin");
Comparator<String> comp = (first, second) -> first.length - second.lenght;
// Error: variable first already defined
在一个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.2.7处理lambda表达式
使用lambda表达式的重点是延迟执行。毕竟,如果想要立即执行代码,完全可以直接执行,而无须把它包装在一个lambda表达式中。之所以希望以后再执行代码,这有很多原因:
- 在一个单独的线程中运行代码。
- 多次运行代码。
- 在算法的适当位置运行代码。
- 发生某种情况时执行代码
- 只在必要时才运行代码。
常用的函数式接口:
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 | 描述 | 其他方法 |
---|---|---|---|---|---|
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 | booean | test | 布尔值函数 | and 、or 、negate 、isEqual |
BiPredicate<T, U> | T, U | boolean | test | 有两个参数的布尔值函数 | and 、or 、negate |
基本类型的函数式接口(注:
p
、q
是int
、long
、double
;P
、Q
是Int
、Long
、Double
):
函数式接口 | 参数类型 | 返回类型 | 抽象方法名 |
---|---|---|---|
BooleanSupplier | 无 | boolean | getAsBoolean |
PSupplier (例如:IntSupplier ) | 无 | p | getAsP |
PConsumer | p | void | accept |
ObjPConsumer<T> | T, p | void | accept |
PFunction<T> | p | T | apply |
PToQFunction | p | q | applyAsQ |
ToPFunction<T> | T | p | applyAsP |
ToPBiFunction<T, U> | T, U | p | applyAsP |
PUnaryOperator | p | p | applyAsP |
PBinaryOperator | p, p | p | applyAsP |
PPredicate | p | boolean | test |
大多数标准函数式接口都提供了非抽象方法来生成或合并函数。例如,
Predicate.isEqual(a)
等同于a::equals
,不过如果a
为null
也能正常工作。已经提供了默认方法and
、or
和negate
来合并谓词。例如,Predicate.isEqual(a).or(Predicate.isEqual(b))
就等同于x -> a.equals(x) || b.equals(x)
。
6.3内部类
内部类是定义在另一个类中的类,之所以使用内部类主要有两个原因:
- 内部类可以对同一个包中的其他类隐藏。
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。
6.3.1使用内部类访问对象状态
public class InnerClassTest {
public static void main(String[] args) {
var clock = new TalkingClock(1000, true);
clock.start();
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TalkingClock {
private int interval;
private boolean beep;
public TalkingClock(int interval, boolean beep) {
this.interval = interval;
this.beep = beep;
}
public void start() {
var listener = new TimePrinter();
var timer = new Timer(interval, listener);
timer.start();
}
// 位于TalkingClock类内部,并不意味着每个TalkingClock都有一个TimePrinter实例字段
public class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
// beep指示TalkingClock对象中创建这个TimePrinter的字段
if (beep) {
Toolkit.getDefaultToolkit().beep();
}
}
}
}
可以看到,一个内部类方法可以访问自身的数据字段,也可以访问创建它的外围类对象的数据字段。为此,内部类的对象总有一个隐式引用,指向创建它的外部类对象。这个引用在内部类的定义中是不可见的。不过,为了说明这个概念,将外围类对象的引用称为
outer
:
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
if (outer.beep) {
Toolkit.getDefaultToolkit().beep();
}
}
外围类的引用在构造器中设置。编译器会修改所有的内部类构造器,添加一个对应外围类引用的参数:
// Automatically generated code
public TimePrinter(TalkingClock clock) {
outer = clock;
}
在
start
方法中构造一个TimePrinter
对象后,编译器就会将当前语音时钟的this
引用传递给这个构造器:var listener = new TimePrinter(this);
。
6.3.2内部类的特殊语法规则
事实上,使用外围类引用的正规语法还要复杂一些:
OuterClass.this
,表示外围类引用:
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
if (TalkingClock.this.beep) {
Toolkit.getDefaultToolkit().beep();
}
}
反过来,可以采用
outerObject.new InnerClass(construction parameters)
这样的语法更加明确地编写内部类对象的实例化:ActionListener listener = this.new TimePrinter();
。
在这里,最新构造的TimePrinter
对象的外围类引用被设置为创建内部类对象的方法的this
引用。这是一种最常见的情况。通常,this.
限定词是多余的。不过,也可以通过显式地命名将外围类引用设置为其他的对象。例如,由于TimePrinter
是一个公共内部类,对于任意的语音时钟都可以构造一个TimePrinter
:
var jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();
6.3.3内部类是否有用、必要和安全
内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换为常规的类文件,用
$
分隔外部类名与内部类名,而虚拟机则对此一无所知:
public class innerClass.TalkingClock$TimePrinter implements java.awt.event.ActionListener {
final innerClass.TalkingClock this$0;
public innerClass.TalkingClock$TimePrinter(innerClass.TalkingClock);
public void actionPerformed(java.awt.event.ActionEvent);
}
// 可以清楚地看到,编译器生成了一个额外的实例字段this$0,对应外围类的引用。另外,
// 还可以看到构造器的TalkingClock参数。
而之所以内部类具有那些额外的访问权限,是因为编译器在外围类添加了一些额外的静态方法,例如:
class TalkingClock {
private int interval;
private boolean beep;
public TalkingClock(int, boolean);
static boolean access$0(TalkingClock); // 内部类只访问外围类的beep,因此只生成对应的静态方法
public void start();
}
// 因此if (beep)等价于if (TalkingClock.access$0(outer))
但是这样做,存在着一些安全风险。熟悉类文件结构的黑客可以使用十六进制编辑器轻松地创建一个类文件,其中利用虚拟机指令调用那个静态方法。由于隐秘方法需要拥有包可见性,所以攻击代码需要与被攻击类放在同一个包中。
6.3.4局部内部类
如果一个内部类只在创建这个类型对象的方法中使用了一次,那么就可以在一个方法中局部地定义这个类:
public void start() {
class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
if (beep) {
Toolkit.getDefaultToolkit().beep();
}
}
}
var listener = new TimePrinter();
var timer = new Timer(interval, listener);
timer.start();
}
声明局部类时不能有访问说明符。局部类的作用域被限定在声明这个局部类的块中。局部类有一个很大的优势,即对外部世界完全隐藏,甚至外部类的其他代码也不能访问它。
6.3.5由外部方法访问变量
与其他内部类相比较,局部类还有一个优点。它们不仅能够访问外部类的字段,还可以访问局部变量。不过,那些局部变量必须是事实最终变量,即一旦赋值就绝不会改变。
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 " + Instant.ofEpochMilli(event.getWhen()));
if (beep) {
Toolkit.getDefaultToolkit().beep();
}
}
}
var listener = new TimePrinter();
var timer = new Timer(interval, listener);
timer.start();
}
// TalkingClock不再需要存储实例变量beep和interval,TimePrinter只是引用start方法中的beep和interval参数变量。
// 之所以能这样,在于编译器:
class TalkingClock$1TimePrinter {
TalkingClock$1TimePrinter(TalkingClock, boolean, int);
public void actionPerformed(java.awt.event.ActionEvent);
final boolean val$beep;
final int val$interval;
final TalkingClock this$0;
}
// 请注意构造器参数和实例变量。当创建一个对象的时候,相应的值会传递给构造器,并存储在实例变量中。
// 编译器检测对局部变量的访问,为每一个变量建立相应的实例字段,并将局部变量复制到构造器,从而能
// 初始化这些实例字段。
6.3.6匿名内部类
使用局部内部类时,通常还可以再进一步。假如只想创建这个类的一个对象,甚至不需要为类指定名字。这样一个类被称为匿名内部类。
public void start(int interval, boolean beep) {
var listener = new ActionListener(){
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
if (beep) {
Toolkit.getDefaultToolkit().beep();
}
}
};
var timer = new Timer(interval, listener);
timer.start();
}
// 相应的语法含义是:创建一个类的新对象,这个类实现了ActionListener接口,需要实现的方法
// actionPerformed在括号{}内定义。
由于构造器的名字必须与类名相同,而匿名内部类没有类名,所以,匿名内部类不能有构造器。实际上,构造参数要传递给超类构造器。具体地,只要内部类实现一个接口,就不能有任何构造参数。不过,仍然要提供一组小括号。
生成日志或调试消息时,通常希望包含当前类的类名:
System.err.println("Something awful happened in " + getClass());
不过,这对于静态方法不奏效。毕竟,调用
getClass
时调用的是this.getClass()
,而静态方法没有this
。所以应该使用以下表达式:
new Object(){}.getClass().getEnclosingClass() // gets class or static method
在这里,
new Object(){}
会建立Object
的匿名子类的一个匿名对象,getEnclosingClass
则得到其外围类,也就是包含这个静态方法的类。
6.3.7静态内部类
有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用。为此,可以将内部类声明为
static
,这样就不会生成那个引用。
public class StaticInnerClassTest {
public static void main(String[] args) {
var values = new double[4];
for (int i = 0; i < values.length; i++) {
values[i] = 100 * Math.random();
}
ArrayAlg.Pair p = ArrayAlg.minmax(values);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());
}
}
class ArrayAlg {
/**
* 由于Pair是一个十分大众化的名字,为了防止命名冲突,解决办法是将Pair定义为一个公共的静态内部类
*/
public static class Pair {
private double first;
private double second;
public Pair(double first, double second) {
this.first = first;
this.second = second;
}
public double getFirst() {
return first;
}
public double getSecond() {
return second;
}
}
public static Pair minmax(double[] values) {
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v : values) {
if (min > v) {
min = v;
}
if (max < v) {
max = v;
}
}
// 示例中必须使用静态内部类,因为内部类对象是在静态方法中构造的。
// 如果没有将Pair声明为static,那么编译器将会报错,指出没有
// 可用的隐式ArrayAlg类型对象来初始化内部类对象
return new Pair(min, max);
}
}
只要内部类不需要访问外围类对象,就应该使用静态内部类。与常规内部类不同,静态内部类可以有静态字段和方法。在接口中声明的内部类自动是
static
和public
。
6.5代理
利用代理可以在运行时创建实现了一组给定接口的新类。只有在编译时期无法确定需要实现哪个接口时才有必要使用代理。
6.5.1何时使用代理
假设想构造一个类的对象,这个类实现了一个或多个接口,但是在编译时可能并不知道这些接口到底是什么。要想构造一个具体的类,只需要使用
newInstance
方法或者使用反射找出构造器。但是,不能实例化接口。需要在运行的程序中定义一个新类。
为了解决这个问题,有些程序会生成代码,将这些代码放在一个文件中,调用编译器,然后再加载得到的类文件。很自然地,这样做的速度会比较慢,并且需要将编译器连同程序一起部署。而代理机制则是一种更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。具体地,代理类包含以下方法:
- 指定接口所需要的全部方法。
Object
类中的全部方法。例如,toString
、equals
等。不过,不能在运行时为这些方法定义新代码。实际上,必须提供一个调用处理器。调用处理器是实现了
InvocationHandler
接口的类的对象。这个接口只有一个方法:
Object invoke(Object proxy, Method method, Object[] args)
无论何时调用代理对象的方法,调用处理器的
invoke
方法都会被调用,并向其传递Method
对象和原调用的参数。之后调用处理器必须确定如何处理这个调用。
6.5.2创建代理对象
要想创建一个代理对象,需要使用
Proxy
类的newProxyInstance
方法。这个方法有三个参数:
- 一个类加载器。作为java安全模型的一部分,可以对平台和应用类,从因特网下载的类等使用不同的类加载器。
- 一个
Class
对象数组,每个元素对应需要实现的各个接口。- 一个调用处理器。
使用代理可能出于很多目的,例如:
- 将方法调用路由到远程服务器。
- 在运行的程序中将用户界面事件与动作关联起来。
- 为了调试、跟踪方法调用。
public interface IUserDao {
void save();
}
public class UserDao implements IUserDao {
@Override
public void save() {
System.out.println("----已经保存数据!----");
}
}
public class ProxyFactory {
// 维护一个目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
// 给目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
(proxy, method, args) -> {
System.out.println("开始事务2");
System.out.println("目标对象所属类: " + target.getClass());
System.out.println("目标对象方法: " + method.getName());
System.out.println("目标对象方法参数: " + Arrays.toString(args));
// 执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务2");
return returnValue;
});
}
}
public class App {
public static void main(String[] args) {
// 目标对象
IUserDao target = new UserDao();
// 原始类型UserDao
System.out.println(target.getClass());
// 给目标对象创建代理对象
IUserDao proxy = (IUserDao) new ProxyFactory(target).getProxyInstance();
// class $Proxy0 内存中动态生成的代理对象
System.out.println(proxy.getClass());
// 执行代理对象方法
proxy.save();
}
}
public class ProxyTest {
public static void main(String[] args) {
var elements = new Object[1000];
// Fill elements with proxies for the integers 1 ... 1000
for (int i = 0; i < elements.length; i++) {
Integer value = i + 1;
var handler = new TraceHandler(value);
// Create a proxy object
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{Comparable.class}, handler);
elements[i] = proxy;
}
// Construct a random integer
Integer key = 288;
// Search for the key
int result = Arrays.binarySearch(elements, key);
// print match if found
if (result >= 0) {
System.out.println(elements[result]);
}
}
}
/**
* An invocation handler that prints out the method name and parameters,
* then invokes the original method.
*/
class TraceHandler implements InvocationHandler {
private Object target;
/**
* Constructs a TraceHandler
* @param target the implicit parameter of the method call
*/
public TraceHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
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);
}
}
6.5.3代理类的特性
需要记住,代理类是在程序运行过程中动态创建的。然而,一旦被创建,它们就变成了常规类,与虚拟机中的任何其他类没有什么区别。
所有的代理类都扩展Proxy
类。一个代理类只有一个实例字段——即调用处理器,它在Proxy
超类中定义。完成代理对象任务所需要的任何额外数据都必须存储在调用处理器中。
所有的代理类都要覆盖Object
类的toString
、equals
和hashCode
方法。如同所有代理方法一样,这些方法只是在调用处理器上调用invoke
。Object
类中的其他方法(如clone
和getClass
)没有重新定义。
对于一个特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次newProxyInstance
方法,将得到同一个类的两个对象。也可以利用getProxyClass
获得这个类:
Class proxyClass = Proxy.getProxyClass(null, interfaces);
代理类总是
public
和final
。如果代理类实现的所有接口都是public
,这个代理类就不属于任何特定的包;否则,所有非公共的接口都必须属于同一个包,同时,代理类也属于这个包。
可以通过调用Proxy
类的isProxyClass
方法检测一个特定的Class
对象是否表示一个代理类。