Java中的接口与Lambda表达式

摘自《Java 核心技术 卷一》第六章

接口

基本定义

接口不是类,而是对类的一组需求描述。

比如使用Arrays.sort对自定义对象数组进行排序,该对象所属的类必须实现了Comparable接口

接口中所有方法自动属于 public,因此在实现类中不必提供关键字 public

接口决不能含有实例域,提供实例域和方法实现应该由相应实现接口的类来完成

Java SE 8 之后,接口中可以提供简单的方法实现,但是依旧不能引用实例域,因为接口没有实例域

简单例子:

// Class Employee
class Employee implements Comparable<Employee> {
  ...
  @Override
  public int compareTo(Employee other) {
    return Double.compare(this.salary, other.salary);
  }
}

// Interface Class Comparable<T> Example
public interface Comparable<T> {
    public int compareTo(T o);
}

实现的 compareTo 也和 equals 一样在继承中可能出现问题

比如有个 Manager 类继承自 Employee 类:

class Manager extends Employee {

  public Manager(String name, int age, double salary) {
    super(name, age, salary);
  }

  @Override
  public int compareTo(Employee other) {
    Manager otherManager = (Manager) other;
    return Double.compare(other.getSalary(), otherManager.getSalary());
  }
}

// Main
public static void main(String[] args) throws Exception {
  Employee staff = new Employee("jerry", 23, 10000);
  Manager manager = new Manager("Mark", 56, 5000000);
  System.out.println(staff.compareTo(manager));  // 正常

  System.out.println(manager.compareTo(staff));  // throw ClassCastException
}

所以这不符合对称规则。所以如果子类之间比较的含义不一样,那就属于不同类对象的非法比较

如果存在一种通用算法,它能够对两个不同的子类对象进行比较,应该在超类中提供 compareTo 方法并设置为 final(这样子类就不能重写该方法)

特性

接口不是类 无法用 new 实例化

可以声明接口变量,接口变量只能引用实现了接口的对象

Comparable x;
x = new Employee(...); // 只能引用实现了接口类的对象

可以用 instanceof 检查特定对象是实现了某接口

if (anObject instanceof Comparable) {...}

接口也可以扩展(像继承一样),接口中没有实例域,但可以包含常量,常量会被自动设置成public static final类型

Java 中每个类只能继承一个超类,但是可以实现多个接口,这样可以赋予类很大的灵活性。

比如希望自己设计的类拥有比较和克隆能力,只需要实现ComparableCloneable接口即可

class Employee implements Cloneable, Comparable {...}

接口和抽象类的区别

既然有抽象类了,为什么还要接口呢?

因为在 Java 中,只能继承一个类,但却可以实现多个接口。

Java 设计者本身不支持多重继承。

静态方法

Java 8 中允许在接口中添加静态方法,可以省去使用伴随类实现静态方法。

默认方法

(Java 8)可以为接口方法提供一个默认实现,必须用default修饰符标记这个方法

public interface Comparable<T>
{
    default int compareTo(T other) {
        return 0;
    }
}

默认方法可以理解为接口可以实现某个方法,而不需要实现类去实现该方法。

为什么要添加”默认方法”这个特性呢?

因为在提出该特性之前,对于已经发布的版本,想要给接口添加新的方法且不影响已有的实现是不可能的(需要修改全部实现该接口的类)。

而默认方法的提出则解决了这个问题,原先实现该接口的类依旧能够正常编译,引进默认方法就是为了解决接口修改与现有的实现类不兼容。

默认方法的冲突问题

假如某个类的超类和需要实现的接口需要实现的两个接口中定义了相同的方法,会如何处理?

Java 中的规则如下:

  • 超类优先:如果超类中提供了一个方法,则接口中同名且有相同参数的默认方法会忽略!(这就是”类优先”)

    // 超类冲突案例
    class Student extends Person implements Named {...}
    
    // 此处生成Student类的对象调用getName方法,由于类优先原则,将调用Person类中的方法
  • 接口冲突:如果一个超接口提供了一个默认方法,另一个接口提供了同名且参数类型相同(无论是不是为默认方法)的方法,该类必须覆盖这个方法来解决冲突!(必须解决此处的二义性二义性)

    // 接口冲突案例
    // Student class
    public class Student implements Person, Named {
      //此处都要覆盖getName方法以解决冲突
      @Override
      public String getName() {
        return "A student's name";
      }
    }
    
    // Person class
    public interface Person {
        // 此处即使不是默认方法,Student类中依旧要解决二义性
      default String getName() {
        return "A person's name";
      }
    }
    
    // Named class
    public interface Named {
    
      default String getName() {
        return getClass().getName() + "_" + hashCode();
      }
    }

这里还会引出一个问题:不要使用默认方法去重新定义 Object 类中的某个方法(例如toStringequals)

因为由于类优先原则,定义这种默认方法肯定会被 Object.toString 和 Object.equals 覆盖!

接口案例

1. Comparator 接口

我们可以为Arrays.sort(obj, compare)传入第二个参数,该参数为一个数组比较器,是实现了Comparator<T>接口的实例。

现在实现一个比较器,以长度为标准判断两个 String 的大小

class lengthComparator implements Comparator<String> {

  @Override
  public int compare(String first, String second) {
    return first.length() - second.length();
  }
}

我们在使用时,依然要创建一个 lengthComparator 类的对象,需要用它来调用 compare 方法

public static void main(String[] args) {
    // 此处还是要实例化lengthComparator类
    Comparator<String> comp = new lengthComparator();
    String[] str = {"aaa", "bbbaaa", "ccsw", "dddssw233"};
    Arrays.sort(str, comp); // Arrays.sort会自动调用实例
    for (String s : str) {
      System.out.print(s + " ");
    }
  }

2. 对象克隆(挺容易出错)

前面已经提过,在 Java 中,建立一个对象的副本,原变量和副本都是同一个对象的引用,任一变量的改变都会影响到另一个变量(原变量改变 =》 副本也发生改变)。

如果想要摆脱这一点,则需要使用clone方法。

Object下的clone方法其实会产生问题的,比如对象 A 中包含对其他对象 S 的引用,那么克隆后的对象 B 中也会存在对相同对象 S 的引用,他们之间还是会共享一些信息。

clone 方法是 Object 类的一个 protected 方法

如果原对象和浅克隆对象共享的子对象是不可变的(如String),那么这种共享是安全的,不过通常遇到包含的子对象都是可变的。

所以在克隆时,对于每一个类,需要判断

  1. 默认的 clone 方法是否满足需求
  2. 是否可以在可变的子对象上调用 clone 来修补默认的 clone 方法
  3. 是否不该使用 clone

重写 clone 时需要:

  1. 实现Cloneable接口
  2. 重新定义 clone 方法,并指定 public

Cloneable 接口是 Java 提供的一组标记接口之一。标记接口不包含任何方法,它唯一作用就是允许在类型查询中使用 instanceof

我们来看一个实现 Cloneable 标记接口的例子:

class Employee implements Comparable<Employee>, Cloneable{

  private String name;
  private int age;
  private double salary;
  private Date hireDay;

  ...

  public Employee clone() throws CloneNotSupportedException {
    return (Employee) super.clone();
  }
}

这里只是将 clone 方法变成 public,我们还要对其中的可变子对象hireDay调用clone方法

public Employee clone() throws CloneNotSupportedException {
  // 先调用Object的clone
  Employee cloned = (Employee) super.clone();
  // 克隆hireDay
  cloned.hireDay = (Date) hireDay.clone();
  return cloned;
}

如果一个对象调用了 clone,但这个对象并没有实现 Cloneable 接口,Object.clone()会抛出一个 CloneNotSupportedException,上面例子的 Date 类已经实现了 Cloneable 接口,故不会抛出异常。

所有数组类型都有一个 public 的 clone 方法,而不是 protected,可以用这个方法建立新数组。

Lambda 表达式

基本例子:

(String first, String second)->
{
  ...
}

lambda 表达式即使没有参数,仍然要提供空括号:

()->{...}

如果可以推导出 lambda 表达式参数类型,则忽略其类型:

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

无需指定 lambda 表达式的返回类型,返回类型会自动由上下文推出

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

如果 lambda 只在某些分支有返回值,其他分支没有返回值,则是不合法的,如下:

(int x) -> {if (x >= 0) return 1;} // error!

函数式接口

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个 lambda 表达式,这种接口称为函数式接口

最好是把 lambda 表达式看做一个函数而不是一个对象

如 Arrays.sort 的第二个参数需要实现一个 Comparator 接口,Comparator 是只有一个方法的接口,所以可以提供一个 lambda 表达式:

Arrays.sort(planets, (first, second) -> first.length() - second.length());

java.util.function包中定义了许多非常通用的函数式接口,比如BiFunction<T,U,R>,它描述了参数类型为 T 和 U 而且返回类型为 R 的函数。可以将字符串比较的 lambda 表达式保存到这个类型的变量中:

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

但是这个有啥用呢?因为毕竟也不能放到Arrays.sort()中去…

我发现该接口有个apply方法,应该用于是运行该 lambda 函数的。

System.out.println(comp.apply("Tom", "Jerry"));  // -2

方法引用

我理解为将现成的方法传递到其他代码中的去运行。

比如表达式System.out::println是一个方法引用,它等价于 lambda 表达式x->System.out.println(x)

比如我想对字符串排序:

String[] employees = {"tom", "jerry", "ddd", "jack", "acow", "zccc"};
Arrays.sort(employees, String::compareToIgnoreCase);
// 输出[acow, ddd, jack, jerry, tom, zccc]

::操作符分隔方法名对象或类名,有以下三种情况:

  1. Object::instanceMethod

  2. Class::staticMethod

    第 1 和第 2 种很好理解,就是等价于提供方法参数的 lambda 表达式

  3. Class::instanceMethod

    第三种情况,第一个参数会成为方法的目标,例如String::compareToIgnoreCase等价于(x, y)->x.compareToIgnoreCase(y)

可以在方法引用中使用this::instanceMethodsuper::instanceMethod

构造器引用

与方法引用类似,只不过方法名为new

假设我把一个字符串列表转换为一个Person数组:

public static <T> ArrayList<T> createArrayList(T... elements) {
    ArrayList<T> list = new ArrayList<T>();
    for (T element : elements) {
      list.add(element);
    }
    return list;
  }

public static void main(String[] args) {
  ArrayList<String> names = Main.createArrayList("tom", "jerry", "jack");
  Stream<Person> stream = names.stream().map(Person::new);  // 对ArrayList中每个元素都调用new
  List<Person> people = stream.collect(Collectors.toList());

  System.out.println(people);  // [tom, jerry, jack]
}

可以用数组类型建立构造器引用,例如int[]::new。它有一个数组长度的参数。x->new int[x]

数组构造器引用对克服构造泛型类型 T 的数组所产生的限制的时候很有用(不太理解??),表达式new T[n]会被改为 new Object[n]。

连接上面那个例子,假设我们需要返回的是一个 Person 的数组,Stream 接口有 toArray 方法,但是它仅仅会返回Object[]

Object[] people = stream.toArray();
Person[] people = stream.toArray();  // 报错,无法将Object[] -> Person[]

这时我们将Person[]::new传入toArray方法:

Person[] people = stream.toArray(Person[]::new);

toArray 方法会调用这个构造器来得到(我理解为生成)一个正确的类型(此处为 Person)的数组;

然后用 stream 重的数据流填充这新生成的数组并最终赋值给people变量。

lambda 中变量的作用域

如果我们希望在一个 lambda 表达式中访问该 lambda 代码块以外的变量,该怎么办呢?

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

上述例子中的text变量即被称为被 lambda 表达式捕获

在 lambda 中引用外部变量中,有以下几种是不合法的:

  1. 在 lambda 中改变变量,比如上面例子中执行text="World"; (理解为在并发的时候会产生许多问题),是不合法的

  2. 引用的变量可能在外部改变,是不合法的

    lambda 表达式中捕获的变量必须是实际上的最终变量(effectively final)

  3. 在 lambda 中声明与一个局部变量同名的参数是不合法的

lambda 表达式中this,是指创建这个 lambda 表达式方法的this参数,可以理解为this在 lambda 中并无任何特殊之处,和出现在包含该 lambda 的方法的其他位置一样。

处理 lambda 表达式

前面总结了两点:

  • 如何生成 lambda 表达式
  • 如何将 lambda 表达式传递到需要一个函数式接口的方法中

接下来要学习如何编写方法处理传递过来的 lambda

下面是一个简单例子:

repeat(10, () -> System.out.println("Hello World!"));

这里的 repeat 需要接受这个 lambda,需要选择一个函数式接口,这里我们选择Runnable接口,该接口定义如下:

public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

这个接口可以看出一般作为无参数或返回值的动作运行。

Java 中常用的函数式接口如下表:

常用的函数式接口

我们定义 repeat 如下:

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

调用action.run()时就会执行传递进来的 lambda 表达式的主体

我们再来一个更复杂一些的例子:

public interface IntConsumer {
  // int类型的参数并返回void
  void accept(int value);
}

private static void repeat(int n, IntConsumer action) {
  for (int i = 0; i < n; ++i) {
    action.accept(i);
  }
}

public static void main(String[] args) {
  repeat(10, (i) -> System.out.println("Countdown: " + (9 - i)));
}

在上面的例子里我们定义了IntConsumer这个函数式接口,该接口有一个抽象方法accept()

下表罗列了基本类型的函数式接口:

基本类型的函数式接口

如果一个你自己设计的接口其中只有一个抽象方法,可以用@FunctionalInterface 注解来标记这个接口,这样做的优点是:

  • 如果你无意中增加了另一个非抽象方法,编译器会产生错误消息
  • javadoc 页会指出该接口是函数式接口

再谈 Comparator

Comparator接口包含很多方便的静态方法来创建比较器,这些方法可以用于 lambda 或方法引用

静态comparing方法取一个键提取器函数,它将类型 T 映射为一个可比较的类型。

Arrays.sort(people, Comparator.comparing(Person::getName));

可以把比较器与thenComparing方法串起来

Arrays.sort(people,
        Comparator.comparing(Person::getLastName)
    .thenComparing(Person::getFirstName));

这种方法有很多变体形式,可以为comparingthenComparing方法提取的键指定一个比较器,比较器完全可以是 lambda 表达式

Arrays.sort(peoples,
        Comparator.comparing(Person::getName, (s, t) -> Integer.compare(s.length(), t.length())).thenComparing(Person::getAge));

comparingthenComparing都有一种变体形式,可以避免 int、long 或 double 值的装箱

Arrays.sort(peoples,
        Comparator.comparingInt(p -> p.getName().length()));

如果键函数可以返回null,要用到nullsFirstnullsLast适配器,这些方法会修改现有的比较器,从而在遇到 null 时不会出现异常

Arrays.sort(peoples,
        Comparator.comparing(Person::getName,
            Comparator.nullsFirst((s, t) -> Integer.compare(s.length(), t.length()))));

nullsFirst方法需要一个比较器,我们可以自己建立,也可以用Comparator.<String>naturalOrder,它可以为任何实现了Comparable的类建立一个比较器

这里Person类没有实现Comparable接口,也可以用 naturalOrder,我的理解是因为比较器比较的是签名的Person::getName,而这个返回的是 String 类型的数据,默认就是实现了Comparable接口,所以可以直接使用

Arrays.sort(peoples,
        Comparator.comparing(Person::getName,
            Comparator.nullsFirst(Comparator.naturalOrder())));

// 也可以默认逆序
 Arrays.sort(peoples,
        Comparator.comparing(Person::getName,
            Comparator.nullsFirst(Comparator.reverseOrder())));  // reverseOrder() == naturalOrder().reversed()

Comparator 比较器接口和 Comparable 排序接口的区别:

链接


文章作者: 玄霄
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 玄霄 !
评论
 上一篇
Ubuntu下通过编译安装Python3 Ubuntu下通过编译安装Python3
本次主要记录通过下载和编译的方式安装Python3 测试版本:Python3.7 1. 安装相关的依赖sudo apt-get update sudo apt-get upgrade sudo apt-get install -y ma
2019-06-25
下一篇 
Mac下配置Aria2来代替迅雷 Mac下配置Aria2来代替迅雷
最近用迅雷下 BT 是越来越不顺心…各种版权限制/资源敏感…会员是一点卵用都没有 好好的正常电影资源(上映 N 久那种)你敏感个啥?活该药丸。 Mac 上的其他下载工具(FDM 等)下载磁力链等也不尽如人意,于是想到了Aria2,今天就来尝
2019-06-16
  目录