跳转至

第6章 接口、lambda表达式与内部类

接口

如果实现了某个接口,就能使用这个接口的方法和变量。

接口的属性

  • 接口中的方法都是 public,但定义时不需要特别说明。
  • 基于上一点,如果父类实现了某个接口,则子类也相当于实现这个接口。如果子类希望改变接口方法的行为,可以选择覆盖这些方法。
  • 接口不能定义变量,但可以定义常量。某个类只要实现了接口,就自动获得接口中的常量。在类中调用接口的常量不需要指定接口名字。
  • 接口的常量都是 public static final,同样不需要在定义时特别说明。
  • 接口可以扩展成更高级的接口。

默认方法

接口可以定义默认方法,例如:

public interface Collection {
    int size();
    default boolean empty() {return size() == 0;}
}

默认方法可以不实现。

默认方法的作用:提供兼容性。假设 Bag 类实现了 Collection 接口,Collection 接口新增了一个方法,可以将这个方法设置为默认方法,这样原有的代码可以正常运行。

解决默认方法冲突

超类与接口冲突,超类优先。接口与接口冲突,必须覆盖解决冲突。

  1. 超类优先规则:如果子类继承了超类的方法,同时实现了接口,接口中有方法(无论这个方法是否为默认方法)与超类中的方法冲突,则超类的方法优先。例如:
    interface myInterface {
        default void method() {
            System.out.println("Interface - method");
        }
    }
    
    class baseClass {
        public void method() {
            System.out.println("baseClass - method");
        }
    }
    
    class childClass extends baseClass implements myInterface{}
    
    public class Main {
        public static void main(String[] args) {
            var a = new childClass();
            a.method(); // 会输出 baseClass - method
        }
    }
    
  2. 接口冲突:如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法(无论这个方法是否为默认方法),必须覆盖这个方法来解决冲突。

标记接口

某些接口不包含任何方法,唯一的作用是允许在类型查询的语句中使用 instanceof。例如 Cloneable 接口,实现这个接口不需要实现任何方法,但如果没有作出标记,请求克隆时就会生成一个检查型异常。

Object 类的 clone 方法是 protected,因此,即使默认的浅拷贝能够满足要求,也要将 clone 定义为 public,再调用 super.clone()

lambda 表达式

lambda 表达式定义了一个代码块,类似于匿名函数(但 java 中没有函数类型 ),方便在以后使用。

函数式接口

有些接口只有一个抽象方法,这种接口称为函数式接口。

可以将 lambda 表达式作为某个实现了函数式接口的对象传入参数中,这样可以避免创建对象等繁琐的流程。

在传入 lambda 表达式时,参数的类型以及返回类型都会自动推导,例如:

  1. 将字符串数组按字符串长度排序,Arrays.sort() 的第二个参数接收一个实现了 Comparable<T> 接口的对象。
    var planets = new String[] { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" };
    Arrays.sort(planets, (x, y) -> x.length() - y.length());
    
  2. 创建定时任务,Timer() 的第二个参数接收一个实现了 ActionListener 接口的对象。
    var timer = new Timer(1000, event -> System.out.println("The time is " + new Date()));
    
  3. ArrayList 中的 removeIf() 方法,能够根据条件删除元素,其参数接收一个实现了 Predicate<T> 接口的对象。
    list.removeIf(e -> e == null);
    
  4. Supplier<T> 接口提供了懒计算功能。
    // 只有在 null 时才执行第二个参数
    LocalDate hireDay = Objects.requireNonNullOrElseGet(day, () -> new LocalDate(1970, 1, 1));
    

方法引用

lambda 表达式可以进一步简化成一个方法引用,有以下三种用法:

  1. object::instanceMethod,例如在创建定时任务的参数中使用 System.out::println
    var timer = new Timer(1000, System.out::println);
    
    System.out::println 有 10 个重载,系统会自动匹配最接近的重载,在这个例子中,最接近的是 println(Object event) 的方法,相当于:
    var timer = new Timer(1000, event -> System.out.println(event));
    
  2. Class::instanceMethod,例如在对字符串数组排序时,想忽略字母的大小写:
    var planets = new String[] { "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" };
    Arrays.sort(planets, String::compareToIgnoreCase);
    
    这种情况下,第 1 个参数会成为方法的隐式参数,相当于:
    Arrays.sort(planets, (x, y) -> x.compareToIgnoreCase(y));
    
  3. Class::staticMethod,例如 Math::pow,这种情况下所有的参数都会传递到静态方法当中,相当于 (x, y) -> Math.pow(x, y)
Info

注意,只有当 lambda 表达式的结构时只调用一个方法而不做其他操作时,才能把 lambda 表达式重写为方法引用。例如:

s -> s.length() == 0
这里除了一个方法调用之外还有一个比较,所以不能使用方法引用。

变量作用域

  • lambda 表达式可以自动捕获外围作用域的变量(不需要手动添加捕获)。
  • lambda 表达式只能引用不会改变的变量,被捕获的变量既不能在内部改变,也不能在外部改变,否则报错。
  • lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
  • lambda 表达式中使用 this 关键字时,是指创建这个 lambda 表达式的方法的 this 参数。例如:
public class Application {
    public void init()
    {
        ActionListener liStener = event -> {
            System.out.println(this.toString());
            ......
        };
        ......
    }
}

表达式 this .toString() 会调用 Application 对象的 toString 方法,而不是 ActionListener 实例的方法。


最后更新: 2023-12-10
创建日期: 2023-12-03