Java基础知识之接口、lambda表达式与内部类
本文内容参考《Java核心技术 卷Ⅰ》
接口
接口的概念
接口不是类,而是对希望符合这个接口的类的一组需求。
接口中所有方法都自动是public
方法,并且所有方法都绝对不会实现。接口中不会有实例字段,但可以包含常量,并且自动被设置为public static final
。
为了让类实现一个接口,需要完成下面两个步骤:
- 将类声明为实现给定的接口(使用
implements
关键字)。 - 对接口中的所有方法提供定义。
假设希望使用Arrays
类的sort
方法对Employee
对象数组进行排序,Employee
类就必须实现Comparable
接口,假设希望根据员工工资进行比较
1 | class Employee implements Comparable<Employee> { |
接口的属性
不能用new
实例化一个接口
1 | x = new Comparable(...); // ERROR |
但可以声明接口的变量,且必须引用实现了这个接口的类
1 | Comparable x = new Employee(); |
可以利用instanceof检查一个对象是否实现了某个特定的接口
1 | if (anObject instanceof Comparable) ... |
默认方法
可以为接口方法提供一个默认实现,这必须使用default
修饰符标记,比如Iterator
接口中的remove
方法
1 | public interface Iterator<E> { |
默认方法可以调用其他方法,例如Collection接口可以定义一个便利的方法
1 | public interface Collection { |
默认方法的一个重要用法是“接口演化”。假如一个类A
实现了接口B
。但是在后来的版本中,接口B
添加了新方法,如果新方法不是默认方法,那么A
类将不能编译,因为它没实现这个方法。所以,为接口增加一个非默认方法不能保证“源代码兼容”。
解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,规则如下
-
超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
-
接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须使用覆盖这个方法来解决冲突。
考虑两个包含
getName
方法的接口1
2
3
4
5
6
7interface Person {
default String getName() { return ""; }
}
interface Named {
default String getName() { return getClass().getName() + "_" + hashCode(); }
}如果有一个类同时实现这两个接口,Java编译器会报错。只需要在
Student
类中提供一个getName
方法即可,也可以选择两个冲突方法中的一个1
2
3class Student implments Person, Named {
public String getName() { return Person.super.getName(); }
}
lambda表达式
lambda表达式的语法
java中的一种lambda
表达形式:参数,箭头(->)以及一个表达式
1 | String[] strings = new String[100]; |
即使lambda表达式没有参数,也要提供一个括号
1 | () -> { |
如果方法只有一个参数且这个参数类型能被推导出来,可以省略括号
1 | ActionListener listener = event -> System.out.println("xxx"); |
变量作用域
lambda
表达式可以捕获外围作用域中变量的值,但捕获的变量必须是事实最终变量。也就是说这个变量在初始化后就不会再为它赋新的值。
1 | public static void repeat(String text, int count) { |
在上面的代码中,i
的值是会变的,所以不能被捕获,而text
总是指向同一个String
对象,所以捕获它是合法的。
在lambda
表达式中使用this
关键字时,是指创建这个lambda
表达式的方法的this
参数
1 | public class Application { |
表达式中的this.toString()
会调用Application
对象的toString
方法,而不是ActionListener
实例的方法。
处理lambda表达式
使用lambda
表达式的重点是延迟执行。有很多原因,如:
- 在一个单线程中运行代码。
- 多次运行代码。
- 在算法的适当位置运行代码。
- 发生某种情况时执行代码。
- 只在必要时才运行代码。
假如想要重复一个动作n
次,将这个动作和重复次数传递到另一个repeat
方法
1 | repeat(10, () -> System.out.println("Hello World!")); |
要接受这个lambda
表达式,需要选择一个函数式接口,我们在这里可以用Runnable
接口
1 | public static void repeat(int n, Runnable action) { |
我们希望告诉这个动作出现在哪一次迭代中,我们需要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个int
参数而且返回类型为void
1 | public interface IntConsumer { |
对刚刚的repeat
方法改进
1 | public static void repeat(int n, IntConsumer action) { |
可以如下调用
1 | repeat(10, i -> System.out.println("Countdown: " + (9 - i))); |
内部类
内部类是定义在另一个类中的类。使用内部类的主要原因:
- 内部类可以对同一个包中其他类隐藏。
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。
局部内部类
可以在一个方法中局部地定义这个类
1 | public void start() { |
声明局部类时不能有访问说明符(public
、private
等)。局部类的作用域被限定在声明这个局部类的块中。
匿名内部类
使用局部内部类时,假如只想创建这个类的一个对象,甚至不需要为类指定名字。这样的一个类被称为匿名内部类。
1 | public void start(int interval, boolean beep) { |
它的含义是:创建一个类的新对象,这个类实现了ActionListener
接口,需要实现的方法actionPerformed
在括号{}
内定义。一般语法如下
1 | new SuperType(construction parameters) { |
其中,SuperType
可以是接口,也可以是一个类。
由于匿名内部类没有类名,所以它不能有构造器。但匿名内部类可以提供一个对象初始化块
1 | Person count = new Person("Dracula") { |
静态内部类
如果使用内部类只是为了把一个类隐藏在另一个类的内部,并不需要内部类有外围类对象的一个引用,可以将内部类声明为static
。
一个典型的例子,考虑一个任务:计算数组中的最小值和最大值。
1 | double min = Double.POSITIVE_INFINITY; |
然而这个方法必须返回两个数,为此可以定义一个包含两个值的类Pair
1 | class ArrayAlg { |
在Pair
对象中不需要任何其他对象的引用。
在接口声明中的内部类自动是static
和public
。