Java 接口、lambda表达式与内部类

1.接口

接口用来描述类应该做什么,而不指定它们具体应该如何做。一个类可以实现一个或多个接口。

1.1概念

接口不是类,而是对希望符合这个接口的类的一组需求。

//Arrays类中的sort方法承诺可以对对象数组进行排序,但要求对象所属的类必须实现Comparable接口
public interface Comparable<T>{
    int compareTo(T other);
}
//在调用x.compareTo(y)时,x<y返回负数;x==y返回0;x>y返回正数。
复制代码

接口中的所有方法都自动是public方法,在接口中声明方法时,不必提供关键字public

接口可以包含多个方法,可以定义常量。但绝不会有实例字段。

让类实现一个接口,通常需要完成两个步骤:

  1. 将类声明为实现给定的接口。
  2. 对接口中的所有方法提供定义。
public class Employee implements Comparable {
    @Override
    public int compareTo(Object otherObj) {
        Employee other = (Employee) otherObj;
        return Double.compare(salary,other.salary);
    }
}

//为泛型Comparable接口提供一个类型参数
public class Employee implements Comparable<Employee> {
    @Override
    public int compareTo(Employee other) {
        return Double.compare(salary,other.salary);
    }
}
复制代码

1.2接口的属性

//接口不是类,具体来说,不能使用`new`运算符实例化一个接口:
x = new Comparable(...); //error
//不过,尽管不能构造接口的对象,却能声明接口的变量:
Comparable x; 
//接口变量必须引用实现了这个接口的类对象
x = new Employee(...); 
//使用instanceof检查一个对象是否实现了某个特定的接口
System.out.println(x instanceof Comparable); //true
复制代码

与建立类的继承层次一样,也可以扩展接口。

//定义一个名为Moveable的接口:
public interface Moveable{
    void move(double x, double y);
}
//假设一个名为Powered的接口扩展了以上Moveable接口:
public interface Powered extends Moveable{
    double milesPerGallon();
    double SPEED_LIMIT = 95;  //常量 public static final
}
//与接口中的方法都自动被设置为public一样,接口中的字段总是public static final
复制代码

1.3接口和抽象类

abstract class Comparable{
    public abstract int compareTo(Object other);
}

class Employee extends Comparable{
    public int compareTo(Object other){
        ...
    }
}
//使用抽象类可以提供CompareTo方法的实现,但是,每个类只能扩展一个类
复制代码

1.4静态和私有方法

Java 8中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的,只是这有违于将接口作为抽象规范的初衷。

目前为止,通常的做法都是将静态方法放在伴随类中。在标准库中,有成对出现的接口和工具类,如Collection/Collection或Path/Paths。

//由一个URI或者字符串序列构造一个文件或目录的路径
//使用Paths工具类中public static Path get(String first, String... more){}
Path path = Paths.get("jdk-12", "conf", "sec"); //class
System.out.println(path); //jdk-12\conf\sec

//Path接口中的static Path of(String first, String... more){}
Path of = Path.of("jdk-12", "conf", "sec"); //interface
System.out.println(of); //jdk-12\conf\sec
复制代码

这样一来,Paths类就不再是必要的,类似的,实现自己的接口时,没有理由再为实用工具方法提供一个伴随类。

Java 9中,接口总的方法可以是privateprivate方法可以是静态方法或实例方法。由于私有方法只能在接口本身的方法中使用,所以它们的用法很有限,只能作为接口中其他方法的辅助方法。

1.5默认方法

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

public interface Comparable<T>{
    default int compareTo(T other){
        return 0;
    }
}
//不过,这种并无太大用处,每个具体实现都会覆盖这个方法。而在某些情况下,默认方法可能很有用。
//Iterator接口用于访问一个数据结构中的元素。这个接口声明了一个remove方法,如下:
public interface Iterator<E>{
    boolean hasNext();
    E next();
    //如果要遍历访问的数据结构是只读的,就不用管remove方法,默认抛出异常
    default void remove(){
        throw new UnsupportedOperationException("remove");
    }
}

//默认方法可以调用其他方法
public interface Collection {
    int size();

    default boolean isEmpty() {
        return size() == 0;
    }
}
复制代码

默认方法的一个重要用法是"接口演化"。以Collection接口为例,这个接口作为Java的一部分已经有很多年了。假设很久以前提供一个类实现了该接口:

public class Bag implements Collection
复制代码

Java 8中,又为这个接口增加了一个stream方法。假设该方法不是一个默认方法,那么Bag类将不能编译,因为它没有实现这个新方法。为接口增加一个非默认方法不能保证"源代码兼容"。不过,假设不重新编译这个类,而只是使用原先的一个包含这个类的JAR文件,这个类还是可以正常加载。但是,如果程序在一个Bag实例上调用Stream方法,就会出现一个AbstractMethodError

将方法实现为一个默认方法就可以解决这两个问题。Bag类又能正常编译了。如果没有重新编译而直接加载这个类,并在一个Bag实例上调用Stream方法,将调用Collection.stream方法即默认方法。

1.6.解决默认方法冲突

如果在一个接口中讲一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,会发生什么情况?对于解决这些二义性,Java有相应规则:

  1. 超类优先,如果超类提供了一个具体方法,同名且有相同参数类型的默认方法会被忽略。
  2. 接口冲突,如果一个接口提供一个默认方法,另一个接口提供一个同名且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
//默认方法冲突问题
interface Person {
    //提供默认实现
    default String getName() {
        return "";
    }
}

interface Named {
    //不论是否提供默认实现,都会出现冲突。
    //如果至少有一个接口提供了一个实现,编译器就会报错。
    String getName();
//    default String getName() {
//        return getClass().getName() + "_" + hashCode();
//    }
}

class Student implements Person, Named {
    //提供一个getName方法来解决冲突,可以选择冲突方法中的一个
    @Override
    public String getName() {
        return Person.super.getName();
    }
}


//如果扩展超类同时实现一个接口,并从超类和接口继承相同的方法。在这种情况下,只会考虑超类方法,接口中的所有默认方法都会被忽略。
class Student extends Person implements Named{...}
复制代码

1.7接口与回调

回调是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。

public class TimerTest {
    public static void main(String[] args) {
        TimePrinter timePrinter = new TimePrinter();
        //Timer(int delay, ActionListener listener)
        //构造一个定时器,每经过1秒通知timePrinter监听器一次
        Timer timer = new Timer(1000, timePrinter);
        //启动定时器。一旦启动,定时器将调用监听器的actionPerformed
        timer.start();
        //停止定时器。一旦停止,定时器将不再调用监听器的actionPerformed
//        timer.stop();

        //static void showMessageDialog(Component parentComponent, Object message)
        //显示一条提示信息和OK按钮的对话框。这个对话框位于parentComponent组件中央。为null显示屏幕中央
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }
}

class TimePrinter implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent actionEvent) {
        System.out.println("at the tone,the time is" + Instant.ofEpochMilli(actionEvent.getWhen()));
        //Toolkit.getDefaultToolkit()获得默认的工具箱,beep()发出一声铃响
        Toolkit.getDefaultToolkit().beep();
    }
}
复制代码

1.8 Comparator 接口

因为String类实现了Comparable<String>接口,而且String类中重写的compareTo方法按字典顺序比较字符串。但如果希望按长度递增的顺序对字符串进行排序,而不是按字段顺序进行排序,这时并不能修改String类。为了处理这种情况,Arrays.sort方法有第二个版本,有一个数组和一个比较器(Comparator)作为参数,比较器是实现了Comparator接口的类的实例。

public interface Comparator<T> {
    int compare(T var1, T var2);
}

//定义一个实现Comparator<String>接口的类,并重写compare方法
class LengthComparator implements Comparator<String> {
    @Override
    public int compare(String s, String t1) {
        return s.length() - t1.length();
    }
}

String[] words = {"shinobu", "asd", "gxsb"};
Arrays.sort(words,new LengthComparator());
for(String s : words) 
    System.out.print(s); //asd gxsb shinobu
复制代码

2.lambda表达式

lambda表达式用来创建可以在将来某个时间点执行的代码块。通过使用lambda表达式,可以用一种精巧而简洁的方式表示使用回调或可变行为的代码

2.1 概念

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

class LengthComparator implements Comparator<String> {
    @Override
    public int compare(String s, String t1) {
        return s.length() - t1.length();
    }
}
String[] words = {"shinobu", "asd", "gxsb"};
Arrays.sort(words,new LengthComparator());
//在这里compare方法不是立即调用。实际上,在数组完成排序之前,sort方法会一直调用compare方法,只要元素的顺序不正确就会重新排列元素。
//将一个代码块传递到某个对象,这个代码快会在将来某个事件调用。
复制代码

2.2 语法

//传入代码来检查一个字符串是否比另一个字符串短
first.length() - second.length()
//需要指定first和second的类型
(String first,String second)
  -> first.length()- second.length()
  
 //如果代码要完成的计算无法放在一个表达式中,就可以想写方法一样,把这些代码放在{}中,并包含显示的return语句 
(String first, String second) -> {
    if (first.length() < second.length()) return -1;
    else if (first.length() > second.length()) return 1;
    else return 0
}

//即使lambda表达式没有参数,仍要提供空括号,就像无参数方法一样
() -> {
    for (int i = 0; i < 100; i++) {
        System.out.println(i);
    }
}

//如果可以推导出一个lambda表达式的参数类型,则可以忽略且类型。
Comparator<String> comp = (first, second) -> first.length();

//如果方法只有一个参数,而且这个参数的类型可以推导的出,还可以省略小括号
ActionListener listener = event -> System.out.println("the time is" +
        Instant.ofEpochMilli(event.getWhen()));

//无需指定lambda表达式的返回类型。lambda的返回类型总是会由上下文推导得出
(String first,String second) -> first.length() - second.length();

//!!!但如果一个lambda表达式只在某些分支返回一个值,而另外一些分支不返回值,这是不合法的。
(int x) -> {
    if (x >= 0) return 1;
}
复制代码
String[] planets = {"Shinobu", "Ads", "Gxsb", "Mars", "Inori"};
System.out.println(Arrays.toString(planets));
System.out.println("Sorted in dictionary order:");
Arrays.sort(planets);
System.out.println(Arrays.toString(planets));
System.out.println("Sorted by length:");
//比较器使用lambda表达式
Arrays.sort(planets, (first, sencod) -> first.length() - sencod.length());
System.out.println(Arrays.toString(planets));

//动作监听器使用lambda表达式
Timer timer = new Timer(1000, event -> System.out.println("the time is" + new Date()));
timer.start();
复制代码

2.3 函数式接口

Java中有很多封装代码块的接口,lambda表达式与这些接口是兼容的。

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

//Arrays.sort方法第二参数需要一个Comparator实例,Compartor就是只有一个方法的接口,所以可以提供一个lambda表达式
Arrays.sort(words, (first,second)->first.length()-second.length());
//在底层,Arrays.sort方法会接收实现了Compartor<String>的某个类的对象。在这个对象上调用Compare方法会执行这个lambda表达式的体。
//这些对象和类的管理完全取决与具体实现,与传统的内联类相比,这样可能要高效的多。
//最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口.

//lambda表达式可以转换为接口,这一点让lambda表达式很有吸引力:
Timer timer = new Timer(1000, event -> {
    System.out.println("at the tone,the time is "
            + Instant.ofEpochMilli(event.getWhen()));
    Toolkit.getDefaultToolkit().beep();
});

//原来的做法:
class TimePrinter implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent actionEvent) {
        System.out.println("at the tone,the time is" + Instant.ofEpochMilli(actionEvent.getWhen()));
        Toolkit.getDefaultToolkit().beep();
    }
}
Timer timer = new Timer(1000, new TimePrinter());
复制代码

不能把lambda表达式赋给类型为Object的变量,Object不是函数式接口

想要使用lambda表达式做某些处理,还要谨记表达式的用途,为它建立一个特定的函数式接口。java.util.function包中有一个尤其有用的接口Predicate:

public interface Predicate<T>{
    boolean test(T t);
    //additional default and static methods
}
复制代码

ArrayList类有一个removeIf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式。例如,下面的语句将从一个数组列表删除所有null值:

list.removeIf(e -> e == null);
复制代码

另一个有用的函数式接口是Supplier:

public interface Supplier<T>{
    T get();
}
复制代码

供应者(Supplier)没有参数,调用时会生成一个T类型的值。供应者用于实现懒计算。某些方法只有在需要值是才调用供应者。

2.4 方法引用

有时,lambda表达式涉及一个方法。例如,

//假设希望只要出现一个定时器时间就打印这个事件对象。
Timer timer = new Timer(1000, event -> System.out.println(event));
//但是,如果把println方法传递到Timer构造器就更好了:
Timer timer = new Timer(1000, System.out::println);

//表达式System.out::println是一个方法引用,它指示编译器生成一个函数式接口的实例,
//覆盖这个接口的抽象方法来调用给定的方法。在这个例子中,会生成一个ActionListener,
//它的actionPerformed(ActionEvent e)方法要调用System.out.println(e)。
复制代码

再来看一个例子,假设想要对字符串进行排序,而不考虑字母的大小写。可以传递一下表达式:

Arrays.sort(planets, String::compareToIgnoreCase);
复制代码

2.5 构造器引用

3.内部类

内部类定义在另外一个类的内部,它们的方法可以访问包含它们它们的外部类的字段。内部类在设计具有相互写作关系的类集合时很有用。

  • 内部类可以对同一个包中的其他类隐藏;
  • 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。

实际上,内部类的对象总有一个隐式对象,指向创建它的外部类对象。这个引用在内部类的定义中是不可见的,

3.1 局部内部类

当内部类只使用一次时,可以在一个方法中局部定义一个类即局部类。声明局部类时不能有访问说明符。局部类的作用域被限定在声明这个局部类的块中。局部类对外部世界完全隐藏。

3.2 匿名内部类

使用局部内部类时,通常可以再进一步。假如只想创建一个类的一个对象,不需要为类指定名字。这样一个类被称为匿名内部类。

//创建一个类的新对象,这个类实现了ActionListener接口,需要实现的方法actionPerformed
//在括号{}内定义
var listen = new ActionListener(){
    public void actionPerformed(ActionEvent event){
        ...
    }
};
var timer = new Timer(interval, listener);
timer.start();
//一般的,语法如下:
//new SuperType(construction parameter){
//    inner class methods and data
//}
//其中SuperType可以是接口,如ActionListener,如果是这样,内部类就要实现这个接口。
//SuperType也可以是一个类,如果是这样,内部类就要扩展这个类。
复制代码
//使用lambda表达式写匿名内部类更加简洁
var timer = new Timer(interval, event -> {
    ...
});
timer.start();
复制代码

3.3 静态内部类

有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用。为此,可以把内部类声明为static,这样就不会生成那个引用。

猜你喜欢

转载自juejin.im/post/7019577780274888711