《Java 核心技术 卷Ⅰ》第 6 章 接口、lambda 表达式与内部类
接口
接口示例
lambda 表达式
内部类
接口
接口技术,这种技术主要用来描述类具有什么功能,而并不给出每个功能的具体实现。一个类可以实现(implement)一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象。
接口概念
在 Java 程序设计语言中,接口不是类,是对类的一组需求的描述,这些类要遵从接口描述的统一格式进行定义。
Arrays 类中的 sort 方法承诺可以对对象数组进行排序,但要求满足下列条件,对象所属的类 ** 必须实现了 Comparable 接口。
public interface Comparable
{
int compareTo(Object other)
}
这就是说,任何实现 Comparable 接口的类都需要包含 compareTo 方法,并且这个方法的参数必须是一个 Object 对象,返回一个整型数值;比如调用 x.compareTo(y)时,当 x 小于 y 时,返回一个负数;当 x 等于 y 时,返回 0;否则返回一个正数。
在 Java SE 5 中,Comparable 接口改进为泛型类型。
public interface Comparable<T>
{
int compareTo(T other); // 参数拥有类型 T
}
例如在实现 Comparable<Employee> 接口类中,必须提供 int compareTo(Employee other)方法。
接口中的所有方法自动地属于 public,因此,在接口中声明方法时,不必提供关键字 public。
接口可以包含多个方法
接口中可以定义常量
接口中不能含有实例域
Java SE 8 之前,不能在接口中实现方法
提供实例域和方法实现的任务应该由实现接口的那个类来完成。
在这里可以将接口看成是没有实例域的抽象类。
现在希望用 Arrays 类的 sort 方法对 Employee 对象数组进行排序,Employee 类必须实现 Comparable 接口。
为了让类实现一个接口,通常需要下面两个步骤:
将类声明为实现给定的接口
对接口中的所有方法进行定义
将类声明为实现某个接口,使用关键字 implements:
class Employee implements Comparable
{
…
public int compareTo(Object otherObject)
{
Employee other = (Employee) otherObject;
return Double.compare(salary, other.salary);
}
…
}
这里使用了静态 Double.compare 方法,如果第一个参数小于第二个参数,它会返回一个负值,相等返回 0,否则返回一个正值。
虽然在接口声明中,没有将 compareTo 方法声明为 publuc,这是因为接口中所有方法都自动地是 public,但是,在实现接口时,必须把方法声明为 public,否则编译器将认为这个方法的访问属性是包可见性,即类的默认访问。
可以为泛型 Comparable 接口提供一个类型参数。
class Employee implements Comparable<Employee>
{
…
public int compareTo(Employee other)
{
return Double.compare(salary, other.salary);
}
…
}
为什么不能再 Employee 类直接提供一个 compareTo 方法,而必须实现 Comparable 接口呢?
主要原因是 Java 是一种强类型(strongly type)语言,在调用方法时,编译器将会检查这个方法是否存在。
在 sort 方法一般会用到 compareTo 方法,所以编译器必须确认一定有 compareTo 方法,如果数组元素类实现了 Comparable 接口,就可以确保拥有 compareTo 方法。
接口的特性
接口不是类,尤其不能用 new 实例化接口:
x = new Comparable(…); // Error
尽管不能构造接口的对象,却能声明接口的变量:
Comparable x; // OK
接口变量必须引用实现了接口的类对象:
x = new Employee(…); // OK
也可以使用 instanceof 检查一个对象是否实现了某个特定的接口:
if(x 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。
尽管每个类只能拥有一个超类,但却实现多个接口。
class Employee implements Coneable, Comparable {…}
接口与抽象类
你可能会问:为什么这些功能不能由一个抽象类实现呢?
因为使用抽象类表示通用属性存在这样的问题:每个类只能扩展于一个类,无法实现一个类实现多个接口的需求。
class Employee extends Person implements Comparable {…}
静态方法
在 Java SE 8 中,允许在接口中增加静态方法。虽然说这没有什么不合法的,只是这有违接口作为抽象规范的初衷。
通常的做法是将静态方法放在伴随类中。在标准库中,有成对出现的接口和实用工具类,如 Collection/Collections 或 Path/Paths。
虽然 Java 库都把静态方法放到接口中也是不太可能,但是实现自己接口时,不需要为实用工具方法另外提供一个伴随类。
默认方法
可以为接口方法提供一个默认实现,必须用 default 修饰符标记方法。
public interface Comparable<T>
{
default int compareTo(T other) {return 0;}
}
默认方法的一个重要用法是接口演化(interface evolution)。
以 Collection 接口为例,这个接口作为 Java 的一部分已经很久了,假如很久以前提供了一个类:
public class Bag implements Collection {…}
后来,在 Java SE 8 中,为这个接口增加了一个 stream 方法。如果 stream 方法不是默认方法,那么 Bag 类将不能编译——因为它没有实现这个新方法。
为接口增加一个非默认方法不能保证“源代码兼容”(source compatible)。
解决默认方法冲突
如果一个接口中把方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,会发生什么情况?
解决这种二义性,Java 的规则是:
超类优先,如果超类自己提供了一个具体方法,同名且有相同参数类型的默认方法会被忽略
接口冲突,如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须覆盖这个方法来解决冲突
着重看一下第二个规则,考虑另一个包含 getName 方法的接口:
interface Named
{
default String getName()
{
return getClass().getName() + “_” + hashCode();
}
}
现在有一个类同时实现了这两个接口,这个时候需要程序员来解决这个二义性,在这个实现的方法中提供一个接口的默认 getName 方法。
class Student implements Person, Named
{
public String getName()
{
return Person.super.getName();
}
}
就算 Named 接口并没有 getName 的默认方法,同样需要程序员去解决这个二义性问题。
上面的是两个接口的命名冲突。
现在考虑另一种情况:一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。
class Student extends Person implements Named {…}
这种情况下只会考虑超类的方法,接口所有默认方法会被忽略。
接口示例
接口与回调
回调(callback),可以指出某个特定事件时应该采取的动作。
java.swing 包中有一个 Timer 类,可以使用它在到达给定的时间间隔发送通告。
在构造定时器时,需要设置一个时间间隔,并告知定时器,达到时间间隔时需要做什么。
其中一个问题就是如何告知定时器做什么?在很多语言中,是提供一个函数名,但是,在 Java 标准类库中的类采用的是面向对象方法,它将某个类的对象传递给定时器,然后定时器调用这个对象的方法。
定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了 java.awt.event 包的 ActionListener 接口:
public interface ActionListener
{
void actionPerformed(ActionEvent event);
}
当到达指定时间间隔,定时器就调用 actionPerformed 方法。
使用这个接口的方法:
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println(…);
…
}
}
其中接口方法的 ActionEvent 参数提供了事件的相关信息。
接下来构造类的一个对象,并传递给 Timer 构造器。
ActionListener listener = new TimePrinter()
Timer t = new Timer(10000, listener);
t.start(); // 启动定时器
Comparator 接口
可以对一个字符串数组排序,是因为 String 类实现了 Comparable<String>,而且 String.compareTo 方法可以按字典顺序比较字符串。
现在需要按长度递增的顺序对字符串进行排序,我们肯定不能对 String 进行修改,就算可以修改我们也不能让它用两种不同的方式实现 compareTo 方法。
要处理这种情况,Arrays.sort 方法还有第二个版本,一个数组和一个比较器(comparator)作为参数,比较器实现了 Comparator 接口的类的实例。
public interface Comparator<T>
{
int compare(T first, t second);
}
按字符串长度比较,可以定义一个实现 Comparator<String> 的类:
class LengthComparator implements Comparator<String>
{
public int compare(String first, String second)
{
return first.length() – second.length();
}
}
具体比较时,建立一个实例:
Comparator<String> comp = new LengthComparator();
// comp.compare(words[i], words[j])
Arrays.sort(friends, comp);
对象克隆
Cloneable 接口,指示一个类提供了一个安全的 clone 方法。
Employee original = new Employee(…);
Employee copy = original.clone();
copy.raiseSalary(10); // no changes happen to original
clone 方法是 Object 的一个 protected 方法,代码不能直接调用这个方法(指的是 Object 的这个方法)。
当然,只有 Employee 类可以克隆 Employee 对象,但是默认的克隆操作是浅拷贝,即并没有克隆对象中引用的其他对象。
浅拷贝可能会产生问题么?这取决于具体情况:
原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的
在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下也是安全的
一般来说子对象都是可变的,所以需要定义 clone 方法来建立一个深拷贝,同时克隆所有子对象。
对于每一个类,需要确定:
默认的 clone 是否满足要求
是否可以在可变子对象上调用 clone 来修补默认 clone
是否不该使用 clone
实际上第 3 个选项是默认选项(这句话没有太读懂)。
如果选第 1 个或者第 2 个,类必须:
实现 Cloneable 接口
重新定义 clone 方法,并指定 public 访问修饰符
子类虽然可以访问 Object 受保护的 clone 方法,但是子类只能调用受保护的 clone 方法来克隆它自己的对象。
必须重新定义 clone 为 public,才能允许所有方法克隆对象。
Cloneable 接口是一组标记接口,其他接口一般确保一个类实现一个或一组特定的方法,标记接口不包含任何方法,它的唯一作用就是允许在类型查询中使用 instanceof。
即时 clone 的默认(浅拷贝)实现能够满足要求,还是需要实现 Cloneable 接口,将 clone 重新定义为 public,再调用 super.clone()。
class Employee implements Cloneable
{
// raise visibility level to public, change return type
public Employee clone() throws CloneNotSupportedExcption
{
return (Employee) super.clone();
}
}
与浅拷贝相比,这个 clone 并没有增加任何功能,只是让方法变为公有,要建立深拷贝。
class Employee implements Cloneable
{
…
public Employee clone() throws CloneNotSupportedExcption
{
// Obejct.clone()
Employee cloned = (Employee) super.clone();
//clone mutable fields
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
}
如果一个对象调用 clone,但这个对象类没有实现 Cloneable 接口,Object 的 clone 方法就会抛出一个 CloneNotSupportedExcption,Employee 和 Date 类实现了 Cloneable 接口,所以不会抛出异常,但是编译器并不知道这点,所以声明异常最好还要加上捕获异常。
class Employee implements Cloneable
{
// raise visibility level to public, change return type
public Employee clone() throws CloneNotSupportedExcption
{
try
{
Employee cloned = (Employee) super.clone();
…
}
catch(CloneNotSupportedExcption e) {return null;}
// 因为实现了 Cloneable,所以这并不会发生
}
}
必须当心子类的克隆。
例如,一旦 Employee 类定义了 clone,那么就可以用它来克隆 Manager 对象(因为在 Employee 类中的 clone 已经是 public 了,可以直接使用 Manager.clone())。
但 Employee 的 clone 一定能完成克隆 Manager 对象的工作么?
这取决于 Manager 类的域:
如果是基本类型域,那没有问题
如果是需要深拷贝或者不可克隆域,不能保证子类的实现者一定会修正 clone 方法让它正常工作
出于后者的原因,在 Object 类中的 clone 方法声明 protected。
lambda 表达式
一种表示在将来某个时间点执行的代码块的简洁方法。
使用 lambda 表达式,可以用一种精巧而简洁的方式表示使用回调或变量行为的代码。
为什么引入 lambda 表达式
lambda 表达式是一个可传递的代码块,可以在以后执行一次或多次。
之前的监听器和后面的排序比较例子的共同点是:都是把一个代码块传递到某个对象(定时器或者是 sort 方法),并且这个代码块会在将来某个时间调用。
lambda 表达式的语法
考虑之前的按字符串长度排序例子:
first.length() – second.length()
Java 是一种强类型语言,所以还要指定他们的类型:
(String first, String second)
-> first.length() – second.length()
// 隐式 return 默认返回这个表达式的结果
这就是一个 lambda 表达式,一个代码块以及变量规范。
如果代码要完成的计算不止一条语句,可以像写方法一样,把代码放在 {} 中,并包含显式的 return 语句。
(String first, String second) ->
{
if(first.length() < second.length()) return -1;
else if(first.length() > second.length()) return 1;
else return 0;
}
一些省略形式的表达:
如果没有参数,仍要提供空括号
如果编译器可以推导出参数类型,可以省略类型声明
如果只有一个参数,并且参数类型可以推导,则可以省略小括号
需要注意的地方:
不需要指定返回类型,返回类型总是由上下文推导出(一般在赋值语句里)
如果表达式里只要有一个显式 return,那就要确保每个分支都有一个 return,否则是不合法的
函数式接口
Java 中已经有很多封装代码块的接口,比如 ActionListener 或 Comparator,lambda 表达式与这些接口兼容。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式,这种接口称为函数式接口(functional interface)。
考虑之前的 Arrays.sort 方法,其中第二个参数需要一个 Comparator 实例,函数式接口使用:
Arrays.sort(words,
(first, second) -> first.length() – second.length());
在底层,Arrays.sort 方法会接收实现了 Comparator<Strng> 的某个类的对象,在这个对象上调用 compare 方法会执行这个 lambda 表达式的体。
最好把 lambda 表达式看作一个函数,而不是一个对象,而且要接收 lambda 表达式可以传递到函数式接口。
lambda 表达式可以转换为接口,这让 lambda 表达式很有吸引力,具体的语法很简单:
Timer t = new Timer(10000, event ->
{
System.out.println(…);
…
});
与使用实现了 ActionListener 接口的类相比,这个代码可读性好很多。
实际上,在 Java 中,对 lambda 表达式所能做的也只是能转换为函数式接口,甚至不能把 lambda 表达式赋给类型为 Object 的变量,Object 不是一个函数式接口。
方法引用
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。
比如只要出现一个定时器事件就打印这个事件对象:
Timer t = new Timer(10000, event -> System.out.println(event));
但是如果直接把 println 方法传递给 Timer 构造器就更好了:
Timer t = new Timer(10000, System.out::println);
表达式 System.out::println 就是一个方法引用(method reference),它等价于 lambda 表达式 x – > System.out.println(x)。
考虑一个排序例子:
Arrays.sort(words, String::compareToIgnoreCase);
主要有 3 种情况:
object::instanceMethod
Class::staticMethod
Class::instanceMethod
前面两种等价于提供方法参数的 lambda 表达式,比如 System.out::println 等价于 x -> System.out.println(x),以及 Math::power 等价于(x, y) -> Math.power(x, y)。
对于第 3 种,第 1 个参数会成为方法的目标,例如 String::compareToIgnoreCase 等价于(x, y) -> x.compareToIgnoreCase(y)。
可以在方法引种中使用 this,super 也是合法的,比如 super::instanceMethod,使用 this 作为目标,会调用给定方法的超类版本。
构造器引用
构造器引用与方法引用类似,只不过方法名为 new,例如 Person::new 是 Person 构造器的一个引用,具体选择 Person 多个构造器中的哪一个,这个取决于上下文。
现在有一个字符串列表,你可以把它转换为一个 Person 对象数组,为此要在各个字符串上调用构造器。
ArrayList<String> names = …;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());
stream、map 和 collect 方法会在卷Ⅱ的第 1 章讨论。
现在的重点是 map 方法会为各个列表元素调用 Person(String)构造器,这里编译器从上下文推导出这是在对一个字符串调用构造器。
可以用数组类型建立构造器引用,int[]::new 是一个构造器引用,有一个参数,就是数组的长度,这等价于 x -> new int[x]。
Java 有一个限制:无法构造泛型类型 T 的数组。
数组构造器引用对于克服这个限制很有用。
假设需要一个 Person 对象数组,Stream 接口有一个 toArray 方法可以返回 Object 数组:
Object[] people = stream.toArray();
但是用户想要一个 Person 引用数组,流库利用构造器引用解决了这个问题:
Person[] people = stream.toArray(Person[]::new);
toArray 方法调用构造器获得一个正确类型的数组,然后填充这个数组并返回。
变量作用域
通常可能想在 lambda 表达式中访问外围方法或类中的变量。
public static void repeatMessage(String text, int delay)
{
ActionListener listener = event ->
{
System.out.println(text);
…
};
new Timer(delay, listener).start();
}
具体调用:
repeatMessage(“Hello”, 1000);
lambda 表达式中的变量 text,并不是在这个 lambda 表达式中定义的,但是这其实有问题,因为代码可能会调用返回很久以后才运行,而那时这个参数变量已经不存在了,该如何保留这个变量?
重温一下 lambda 表达式的 3 个部分:
一个代码块
参数
自由变量的值,指非参数并且不在代码中定义的变量
上面的例子中有 1 个自由变量 text。
表示 lambda 表达式的数据结构必须存储自由变量的值,也被叫做自由变量被 lambda 表达式捕获(captured)。
可以把一个 lambda 表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。
关于代码块以及自由变量有一个术语:闭包(closure),Java 中 lambda 表达式就是闭包。
在 lambda 表达式中,只能引用值不会改变的变量,比如下面这种就是不合法的:
public static void countDown(int start, int delat)
{
ActionListener listener = event ->
{
start–; // Error: Can’t mutate captured variable
System.out.println(text);
…
};
new Timer(delay, listener).start();
}
如果在 lambda 表达式中改变变量,并发执行多个操作时就会不安全(具体要见第 14 章并发)。
另外如果在 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: Can’t refer to changing i
…
};
new Timer(1000, listener).start();
}
}
所以简单来说规则就是:lambda 表达式中捕获的变量必须实际上是最终变量(effectively final),即这个变量初始化之后就不再赋新值。
lambda 表达式的体与嵌套块有相同的作用域,所以在 lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
Path first = Path.get(“/usr/bin”);
Comparator<String> comp =
(first, second) -> fisrt.length() – second.length();
// Error: Variable first already defined
当然在 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 表达式中 this 的使用并没有什么特殊之处。
内部类
内部类(inner class)定义在另一个类的内部,其中的方法可以访问包含它们的外部类的域。
内部类主要用于设计具有相互协作关系的类集合。
使用内部类的主要原因:
内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据
内部类可以对同一个包中的其他类隐藏起来
想定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷。
将从以下几部分介绍内部类:
简单的内部类,它将访问外围类的实例域
内部类的特殊语法规则
内部类的内部,探讨如何转换成常规类
讨论局部内部类,它可以访问外围作用域中的局部变量
介绍匿名内部类,说明 Java 在 lambda 表达式之前怎么实现回调的
介绍如何将静态内部类嵌套在辅助类中
内部类访问对象状态
内部类的语法比较复杂。
选择一个简单的例子:
public class TalkingClock
{
private int interval;
private boolean beep;
public TalkingClock(int interval, boolean beep) {…}
public void start() { …}
// an inner class
public class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println(…);
if(beep) Toolkit.getDefaultToolkit().beep();
}
}
}
TimePrinter 类位于 TalkingClock 类内部,并不意味着每个 TalkingClock 对象都有一个 TimePrinter 实例域。
TimePrinter 类没有实例域或者 beep 变量,而是引用了外部类的域里的 beep。
其实内部类的对象总有一个隐式引用,它指向了创建它的外部类对象,这个引用在内部类的定义中不可见。
这个引用是在对象创建内部类对象的时候传入的 this,编译器通过内部类的构造器传入到内部类对象的域中。
// 由编译器插入的语句
ActionListener listener = new TimePrinter(this);
TimePrinter 类可以声明为私有的,这样只有 TalkingClock 方法才能构造 TimePrinter 对象。只有内部类可以是私有的,常规类只可以是包可见和公有可见。
内部类的特殊语法规则
使用外围类引用的语法为 OuterClass.this。
例如之前的 actionPerformed 方法:
public void actionPerformed(ActionEvent event)
{
…
if(TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}
反过来,可以用 `outerObject.new InnerClass(construction parameters)更加明确地编写内部类对象的构造器:
// ActionListener listener = new TimePrinter(this);
ActionListener listener = this.new TimePrinter();
通常来说 this 限定词是多余的,但是可以通过显式命名将外围类引用设置为其他对象,比如当 TimePrinter 是一个公有内部类时,对于任意的语音时钟都可以构造一个 TimePrinter:
TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.ActionListener listener = jabberer.new TimePrinter();
上面的情况是在外围类的作用域之外,所以引用的方法是 OuterClass.InnerClass。
注意:内部类中声明的所有静态域都必须是 final,因为我们希望一个静态域只有一个实例。不过对于每个外部对象,会分别有一个单独的内部类实例,如果这个域不是 final,它可能就不是唯一的。
局部内部类
如果一个类只在一个方法中创建了对象,可以这个方法中定义局部类。
public void start()
{
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event) {…}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval, listener);
t.start();
}
局部类不能用 public 或 private,它的作用域被限定在生命这个局部类的块中。
但是有非常好的隐蔽性,除了 start 方法,没有任何方法知道 TimePrinter 类的存在。
由外部方法访问变量
局部类还有一个优点:他们还能访问局部变量,但是这些局部变量必须是 final,即一旦赋值就不会改变。
下面的例子相比之前进行了一些修改,beep 不再是外部类的一个实例域,而是方法传入的参数变量:
public void start(int interval, final boolean beep)
{
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
…
if(beep) …;
…
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval, listener);
t.start();
}
先说明一下这里的控制流程:
调用 start(int, boolean)
调用局部内部类 TimePrinter 的构造器,初始化 listener
将 listener 引用传递给 Timer 构造器
定时器 t 开始计时
start(int, boolean)方法结束,此时 beep 参数变量不复存在
某个时刻 actionPerformed 方法执行 if(beep) …
为了让 actionPerformed 正常运行,TimePrinter 类在 beep 域释放之前将内部类中要用到的 beep 域用 start 方法的局部变量 beep 进行备份(具体实现方式是编译器给内部类添加了一个 final 域用来保存 beep)。
编译器检测对局部变量的访问,为每一个量建立相应的数据域,并将局部变量拷贝到构造器中,以便将这些数据域初始化为局部变量的副本。
至于 beep 参数前的 final,是因为局部类的方法只能引用定义为 final 的局部变量,从而使得局部变量与局部类中建立的拷贝保持一致。
匿名内部类
假设只创建这个局部类的一个对象,就不必命名了,这种类称为匿名内部类(anonymous inner class)。
public void start(int interval, boolean beep)
{
ActionListener listener = new ActionListener()
{
public void actionPerformed(ActionEvent event) {…}
};
Timer t = new Timer(interval, listener);
t.start();
}
这种语法的含义是:创建一个实现 AcitonListener 接口的类的新对象,需要实现的方法定义在括号内。
通常的语法格式为:
new SuperType(construction parameters)
{
methods and data
}
SuperType 可以是一个接口,也可以是一个类。
由于构造器必须要有一个名字,所以匿名类不能有构造器,取而代之的是:
当 SuperType 是一个超类时,将构造器参数传递给超类构造器
当 SuperType 是一个接口时,不能有任何构造参数(括号 () 还是要保留的)
构造一个类的新对象,和构造一个扩展这个类的匿名内部类的对象的区别:
Person queen = new Person(“Mary”);
Person count = new Person(“Dracula”) {…};
多年来,Java 程序员习惯用匿名内部类实现事件监听器和其他回调,如今最好还是使用 lambda 表达式,比如:
public void start(int interval, boolean beep)
{
Timer t = new Timer(interval, event -> { …});
t.start();
}
可见,用 lambda 表达式写会简洁得多。
双括号初始化
如果想构造一个数组列表,并传递到一个方法:
ArrayList<String> friends = new ArrayList<>();
friends.add(“Harry”);
friends.add(“Tony”);
invite(friends);
如果之后都没有再需要这个数组列表,那么最好使用一个匿名列表解决。
invite(new ArrayList<String>() {{add(“Harry”); add(“Tony”); }};
注意这里的双括号:
外层括号建立了 ArrayList 的一个匿名子类
内层括号则是一个对象构造块(见第 4 章)
静态内部类
有时使用内部类只是为了把一个类隐藏在另一个类的内部,并不需要内部类引用外围类对象,为此可以将内部类声明 static,取消产生的引用。
编写一个方法同时计算出最大最小值:
double min = Double.POSITIV_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for(double v : values)
{
if (min > v) min = v;
if (max < v) max = v;
}
然而必须返回两个数值,可以顶一个包含两个值的类 Pair:
class Pair
{
private double first;
private double second;
public Pair(double f, double s)
{
first = f;
second = s;
}
public double getFirst() { return first;}
public double getSecond() { return second;}
}
minmax 方法可以返回一个 Pair 类型的对象。
class ArrayAlg
{
public static Pair minmax(double[] values)
{
…
return new Pair(min, max);
}
}
然后调用 ArrayAlg.minmax 获得最大最小值:
Pair p = ArrayAlg.minmax(data);
但是 Pair 是一个比较大众化的名字,容易出现名字冲突,解决的方法是将 Pair 定义为 ArrayAlg 的内部公有类,然后用 ArrayAlg.Pair 访问它:
ArrayAlg.Pair p = ArrayAlg.minmax(data);
不过与前面的例子不同,Pair 对象不需要引用任何其他的对象,所以可以把这个内部类声明为 static:
class ArrayAlg
{
public static class Pair {…}
…
}
只有内部类可以声明为 static,静态内部类的对象除了没有对生成它的外围类对象的引用特权外,其他与所有内部类完全一样。
在上面的例子中,必须使用静态内部类,这是因为返回的内部类对象是在静态方法 minmax 中构造的。
如果没有把 Pair 类声明为 static,那么编译器将会给出错误报告:没有可用的隐式 ArrayAlg 类型对象初始化内部类对象。
注释 1:在内部类不需要访问外围类对象时,应该使用静态内部类。
注释 2:与常规内部类不同,静态内部类可以有静态域和方法。
注释 3:声明在接口中的内部类自动成为 static 和 public 类。
代理
代理(proxy),这是一种实现任意接口的对象。
利用代理可以在运行时创建一个实现了一组给定接口的新类。
这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
对于应用程序设计人员来说,遇到的情况很少,所以先跳过,如果后面有必要再开一个专题进行说明。
Java 接口、lambda 表达式与内部类总结
接口概念、特性
接口与抽象类
静态方法
默认方法
解决默认方法冲突
接口示例
lambda 表达式
函数式接口
方法引用
构造器引用
lambda 表达式变量总用域
内部类
局部内部类
匿名内部类
静态内部类
个人静态博客:
气泡的前端日记:https://rheabubbles.github.io