Core Java 笔记(六)- 接口、lambda

接口

 

接口是对类的一组需求的描述,这些类要遵从接口描述的统一格式进行定义,也可以把接口看作是一组抽象方法的集合

使用接口的目的在于,确保当前的类对外提供特定的方法,从而提高代码的复用性和可读性。

为什么不直接给类添加这个特定的方法?其他类的方法在调用它之前可能会先对参数进行类型转换,如果这个参数所属的类没有实现相应的接口,虚拟机就会抛出异常。

 

一、特性

 

可以声明接口变量,引用实现了该接口的类对象:

Comparable x = new Integer(...);

用 instanceof 可以检查一个对象是否实现了某个特定的接口:

if (anObject instanceof Comparable)  
// ...

一个类不可以有多个超类,但可以实现多个接口,这种特性既能提供多继承的灵活性,又避免了多继承的复杂性和低效性。

与类的继承关系一样,接口也可以被扩展(继承),即设计一条从具有较高通用性的接口到较高专用性的接口的链。

有些接口只定义了常量,实现这个接口的类会自动继承这些常量,可以在方法中直接引用。

 

二、默认方法

 

可以为接口方法提供默认实现,必须用 default 修饰符标记。

设想这样一个场景,在设计图形用户界面时,希望发生鼠标点击事件后得到通知,需要实现一个包含 5 个方法的接口:

public interface MouseListener {
    void mouseClicked(MouseEvent event);
    void mousePressed(MouseEvent event);
    void mouseReleased(MouseEvent event);
    void mouseEntered(MouseEvent event);
    void mouseExited(MouseEvent event);
}

而实际上,往往只关心其中的部分事件类型,因此可以把所有方法声明为 default ,让它们什么都不做,实现这个接口的类不需要多余的代码去覆盖不想用的方法:

public interface MouseListener {
    default void mouseClicked(MouseEvent event) {};
    default void mousePressed(MouseEvent event) {};
    default void mouseReleased(MouseEvent event) {};
    default void mouseEntered(MouseEvent event) {};
    default void mouseExited(MouseEvent event) {};
}

默认方法还可以调用任何接口中的其他方法,例如:

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

在 API 中,很多接口都有相应的伴随类,这个伴随类实现了相应接口的部分或所有方法,如 Collection / AbstractCollection 和 MouseListener / MouseAdapter 。不过自从 Java SE 8 允许在接口中直接实现方法后,这个技术已不再是必要的。

默认方法还有一个重要作用:接口演化,确保向某个接口添加新的方法时,之前使用这个接口的类还能正常编译。

 

解决冲突

如果先在一个接口中定义了一个默认方法,又在超类或另一个接口中定义了同样的方法,则:

  • 涉及超类,类优先

  • 不涉及超类,程序员自行解决二义性

interface Person {
    default String getName() {
        return getClass().getName() + " " + toString();
    }
    // ...
}

interface Named {
    default String getName() {
        return getClass().getName() + "_" + hashCode();
    }
    // ...
}

class Student implements Person, Named {
    public String getName() {
        return Person.super.getName();
    }
    // ...
}

 

三、示例

 

回调(callback)

这是一种常见的程序设计模式,可以指出某个特定事件发生时应该采取的动作。假设我们现在要使用 Timer 类,每隔一段时间执行一些操作,根据面向对象的原则,应该通过传递一个特定的对象来告诉 Timer 实例在到达时间间隔时要执行的具体操作是什么。

基于这种思想,Timer 实例要求传递的对象所属类必须实现 ActionListener 接口,它会在到达时间间隔时调用其中的 actionPerformed 方法:

public interface ActionListener {
    void actionPerformed(ActionEvent event);
}

假设需要执行的具体操作是打印一条信息“I'll keep BBing until you accept me.”,就先定义一个实现 ActionListener 接口的类,然后将执行语句放在 actionPerformed 方法中:

class TimePrinter implements ActionListener {
    public void actionPerformed(ActionEvent event) {
        System.out.println("I'll keep BBing until you accept me.");
    }
}

接下来,将这个类的一个实例传递给 Timer 构造器,最后调用 run 方法启动 Timer 实例:

ActionListener listener = new TimePrinter();
Timer t = new Timer(3000, listener);  // interval : 3000 ms
t.start();

其实使用 lambda 表达式能够大大简化这一过程,下文会介绍这种特性。

 

API - java.swing.Timer
  • start

  • stop

 

Comparator

我们已经知道了对一个对象数组排序的前提是这些对象所属类实现了 Comparable 接口,顺序前后的依据是在 compareTo 方法中确定的,这种做法的局限在于:这个类的用户不能根据自己的要求决定排序的依据。比如,有时可能不想按字典顺序对字符串进行排序,而是按长度递增的顺序。

要处理这种情况,Arrays.sort 还有第二个版本,参数列表中增加了一个比较器(一个实现了 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();
String[] girlfriends = {"Jenifer", "Alice", "Elizabeth"};
Arrays.sort(girlfriends, comp);

 

Cloneable

之前说过,为一个包含对象引用的变量建立副本时,原变量和副本都是同一个对象的引用。如果希望得到一个初始状态相同的新对象,而不对原对象产生干扰,怎么办?使用 clone 方法:

Employee copy = original.clone();  // assume it invokes successfully
copy.raiseSalary(100);  // original unchanged

下图说明了这两种操作的不同:

不过我们不能直接调用 clone 方法,虽然它是在 Object 中声明的,但被设置为 protected,也就是说,假如我们要对一个 Employee 类的变量调用 clone 方法,只能在 Employee 及其子类的方法内部进行。

此外,Object 中的 clone 方法会对逐个域进行拷贝,不管这些域是基本类型还是对象引用,所以很多时候,原对象(original)和克隆的对象(copy)还是会共享一些信息,这样的拷贝过程称为浅拷贝。如果 original 和 copy 共享的子对象属于一个不可变的类,或者这个子对象一直包含不变的常量,那么浅拷贝是安全的,可以被接受。

只是,共享的子对象通常是可变的,必须重新定义 clone 方法来建立一个深拷贝,使得所有的子对象也都被克隆。到了这一步,就需要:

  • 实现 Cloneable 接口

  • 重新定义 clone 方法并设置为 public

Cloneable 属于标记接口(tagging interface),不包含任何方法,唯一的作用就是允许在类型查询中使用 instanceof :

if (obj instanceof Cloneable) 
// ...

换句话说,Cloneable 没有指定 clone 方法,它只是作为一个标记。当一个对象请求克隆,而所属的类却没有实现这个接口,就会产生一个受查异常 CloneNotSupportedException 。即便想沿用默认的浅拷贝,也要实现 Cloneable:

class Employee implements Cloneable {
    public Employee clone() throws CloneNotSupportedException {
        return (Employee) super.clone();  // covariant return
    }
}

要建立深拷贝,则稍微繁琐一些:

class Employee implements Cloneable {
    private String name;
    private double salary;
    private Date hireDay;

    // ...
    public Employee clone() throws CloneNotSupportedException {
        Employee cloned = (Employee) super.clone();
        // clone mutable fields
        cloned.hireDay = (Date) hireDay.clone();
        return cloned;
    }
}

注意,由于此时 clone 方法已经被设置为 public ,可以在其他任何地方调用,进而可能会发生这种情况:某个类从父类那里继承了修改过的 clone 方法,但是没有针对新增加的域重新定义它,浅拷贝又一次出现了。

最后补充一点:所有数组类型都有一个 public 的 clone 方法,可以建立一个新数组,包含原数组所有元素的副本:

int[] numbers = { 4, 6, 2, 7, 1 };
int[] cloned = numbers.clone();
cloned[1] = 0; // numbers[1] is still 6

 

lambda 表达式

 

书中定义:一个可传递的、可在以后执行多次的代码块。直观一点说,lambda 表达式会把它所代表的函数传递给一个方法,让这个函数得到执行,属于函数式编程。

刚开始接触它时,最直接的印象就是简洁明了。在 Java 8 之前,传递一个代码块会略为麻烦,比如上文的例子:要让 Timer 执行一个特定的代码块,需要先定义一个继承 ActionListener 接口的类,在 actionPerformed 方法中包含这个代码块,再构造一个实例,将其作为参数传递给 Timer 。现在有了 lambda 表达式,可以这么做:

Timer t = new Timer(1000, event -> System.out.println("..."));

/*
class TimePrinter implements ActionListener {
    public void actionPerformed(ActionEvent event) {
        System.out.println("...");
    }
}

...

ActionListener listner = new TimePrinter();
Timer t = new Timer(3000, listener);
*/

 

一、语法

 

在上面 Comparator 接口的例子中,我们将 compare 方法定义为:

return first.length() - second.length();

用 lambda 表达式来替换,首先要明确参数的类型(如果编译器结合上下文可以自行推断出来,那么可以忽略),这个例子的 first 和 second 都属于 String 类,写成 lambda 表达式就是:

(String first, String second) -> first.length() - second.length()
// (first, second) -> first.length() - second.length()

当然,这个例子比较特殊,原先方法中就只有一个 return 语句直接返回结果。有时代码要完成的计算无法用一个表达式来完成,那就可以像写方法一样,将多条语句放在 { } 中,并且再加上显式的 return 语句:

(String first, String second) ->
    {
        if (first.length() < second.length()) return -1;
        else if (first.length() > second.length()) return 1;
        else return 0;
     }

不需要参数的情况下,开头应该是空括号:

() -> { for (int i = 0; i <= 10; i++) System.out.print(i + " "); }

lambda 表达式的返回类型总是会由上下文推导得到,不需要指定返回类型。

 

二、函数式接口

 

只包含一个抽象方法的接口称为函数式接口(functional interface),需要一个实现这种接口的类的实例时,就可以提供一个 lambda 表达式,免去了定义类、创建实例等旧有的繁琐步骤。

java.util.function 包中定义了很多函数式接口,比如 BiFunction<T, U, R> 描述了一个参数类型为 T 和 U 并且返回类型为 R 的函数,可以把上文的字符串比较代码传给它:

BiFunction<String, String, Integer> comp
    = (first, second) -> first.length() - second.length();

但这并不能作为 Arrays.sort 的参数,因为类似接口往往有特定的用途,而不是简单地指定参数和返回类型。

常用的函数式接口有:

其中,有一个专门用来传递 lambda 表达式的 Predicate 接口:

public interface Predicate<T> {
    boolean test(T t);
}

ArrayList 类的 removeIf 方法以 Predicate 作为参数,所以可以将代码简写如下:

list.removeIf(e -> e == null);

(是不是看起来很舒服 ^_^ )

 

三、方法引用

 

继续来看 Timer 的例子,我们这回要打印出参数 event :

Timer t = new Timer(1000, event -> System.out.println(event));

这个 lambda 表达式调用了一个已经存在的方法 System.out.println ,并且和接口中待实现的方法接收了同样的参数,在这里,我们发现了一种继续简化的可能性:直接传递已经存在的方法。这就是方法引用(method reference),可以形成一个更加紧凑和易读的 lambda 表达式:

Timer t = new Timer(1000, System.out::println);

再比如,我们想让字符串的排序不考虑大小写,可以传递以下方法表达式:

Arrays.sort(strings, String::compareToIgnoreCase);
// Arrays.sort(strings, (s1, s2) -> s1.compareToIgnoreCase(s2));

我相信你已经发现了 :: 操作符的重要性,它的作用是分隔对象名(或类名)与方法名:

  • object::instanceMethod

  • Class::staticMethod

  • Class::instanceMethod

如果有多个同名的重载方法,编译器会尝试从上下文找到符合接口要求的方法。方法引用的出现是为了更好地支持函数式编程,所以不能独立存在,总是会转换成函数式接口的实例

可以在方法引用中使用 this / super 参数:

class Teacher {
    ArrayList<Paper> papers;

    public void updatePapers() {
        papers.removeIf(this::informal);
    }

    private boolean informal(Paper paper) {
        // ...
    }
}

class Paper {
    // ...  
}

在 lambda 表达式中使用 this 关键字时, 是指创建(而不是接收)这个 lambda 表达式的方法的 this 参数。

构造器引用

构造器引用和方法引用很类似,不同之处在于方法名为 new ,例如:

ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new); // Person(String)

还可以用数组类型建立构造器引用,比如 int[]::new,它接收一个参数作为数组长度,等价于 lambda 表达式:

x -> new int[x]

 

四、闭包

 

闭包(closure)指的是能够读取其他函数内部变量的函数,可以理解为将函数内部和外部连接起来的桥梁。这个概念对于 lambda 表达式的意义在于,有时候 lambda 表达式需要访问外围方法或类中的变量,比如:

public static void repearMessage(String text, int delay) {
    ActionListener listener = event -> {
        System.out.println(text);
        Toolkit.getDefaulToolkit().beep();
    }
    new Timer(delay, listener).start();
}

接着调用:

repeatMessage("Hi", 1000); // 每隔 1 秒打印一次 Hi

在这个例子中,lambda 表达式有一个自由变量 text ,所谓自由变量是指非参数而且没有在当前代码中定义的变量。表示 lambda 表达式的数据结构必须存储自由变量的值,也就是说,这里的字符串 “Hi” 会被捕获

不过,有一条重要的规则:lambda 表达式所捕获的变量必须实际上是最终变量(effectively final),意思就是,这个变量初始化之后不会再为它赋值。

下面是两个错误的示范:

public static void countDown(int start, int delay) {
    ActionListener listener = event -> {
        System.out.println(start);
        start--; // ERROR
    }
    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
        }
        new Timer(1000, listener).start();
    }
}

 

五、进一步应用

 

使用 lambda 表达式的重点是延迟执行(deferred execution),比如以下这些情形:

  • 在一个单独的线程中执行

  • 多次执行

  • 在算法的适当位置执行(例如排序中的比较操作)

  • 发生某种情况时执行(例如点击鼠标)

举个例子,我想定义一个方法,作用是重复一个待定的动作 n 次,我也许会这样调用它:

repeat(10, () -> System.out.println("Today is my birthday!"));

那么我该如何定义 repeat 方法呢?毫无疑问,它的参数必须包括一个函数式接口,在这里我可以使用 Runnable 接口(参考上文的常用函数式接口):

public static void repeat(int n, Runnable action) {
    for (int i = 0; i < n; i++) {
        action.run();
    }
}

好了,现在我又希望告诉这个动作它出现在哪一次迭代中,那么应该使用另一种接口,其中的某个方法会接收一个 int 参数并且返回类型为 void,标准形式如下:

public interface IntConsumer {
    void accept(int value);
}

于是新的 repeat 方法就变成了:

public static void repeat(int n, IntConsumer action) {
    for (int i = 0; i < n; i++) {
        action.accept(i);
    }
}
// repeat(10, i -> System.out.println("Countdown: " + (9-i)));

可以用 Consumer<Integer> 来代替 IntConsumer,但为了提高效率,最好尽量减少自动装箱。下图列出了基本类型 int、long 和 double 的特殊化规范:

 

@FunctionalInterface

这个注解可以用来标注只有一个抽象方法的接口,如果无意中增加了一个非抽象方法,编译器就会报错。

 

六、Comparator 补充

 

Comparator 接口包含很多静态方法来创建比较器,这些方法可以用于 lambda 表达式或方法引用,比如 comparing 方法,它将类型 T 映射为一个可比较的类型(如 String),来看一个具体的例子:

// 对一个 Person 对象数组按名字排序 
Arrays.sort(people, Comparator.comparing(Person::getName));

还可以与 thenComparing 方法串起来:

// 先比较姓,如果姓相同,就比较名字
Arrays.sort(people, Comparator.comparing(Person::getLastName)
    .thenComparing(Person::getFirstName));

这些方法有很多变体形式。可以为 comparing 和 thenComparing 方法提取的键(key)指定一个比较器,例如调用 Person::getName 后再根据结果的长度排序:

Arrays.sort(people, Comparator.comparing(Person::getName,
    (first, second) -> Integer.compare(first.length(), second.length())));

还可以使用 comparing 和 thenComparing 对应于基本类型的变体,避免 int、long 和 double 值的自动装箱:

Arrays.sort(people, Comparator.comparingInt(person -> person.getName().length()));

 

API - java.util.Comparator<T>
  • compare

  • comparing

  • thenComparing

  • comparingXxx

  • thenComparingXxx

  • nullsFirst

  • nullsLast

  • reversed

  • reverseOrder

 

转载于:https://www.cnblogs.com/zzzt20/p/11493235.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值