4.1 接口

4.1.1 接口的概念

接口被用来统一类的共通行为,当不同的类需要进行信息共享时,是不需要特别去创建类间的关系。举例来说,一个人(Human)及一只鹦鹉(Parrot)都会吹口哨(whistle),然而Human及Parrot不应该为Whistler的子类,最好的做法是令他们为Animal的子类,而他们可以使用Whistler的接口进行沟通。

以上引用维基百科的描述,个人理解接口就是为类去赋能的。由于Java是单继承,而使用接口可以模拟多继承,如果希望某个类具有某个能力,只需要实现对应的接口。

实现接口需要以下两步:

  1. 将类声明为实现给定的接口;
  2. 实现接口中定义的所有方法。

4.1.2 接口与抽象类

虽然接口不能包含实例字段,但是可以包含常量。在接口中,方法默认是public类型,而常量则默认为public static final类型,故在声明方法或常量时,都无需特别的书写以上修饰符。

接口之间也可以存在继承关系,由抽象度高的接口向专业性高的接口进行衍生。一个类只能有一个直接父类,但是一个类却可以实现多个接口,这意味着接口的出现可以大大丰富类的能力,同时也是为什么不能用抽象类代替接口的主要原因。

接口不是类,故不能使用new去实例化一个接口,但是可以声明接口变量,其中接口变量只能引用实现了该接口的类对象。

  1. Comparable x; //ok
  2. x = new Employee(...);
  3. //可以使用instance方法判断一个对象是否实现了某个接口
  4. if (anObject instanceof Comparable) {
  5. ...
  6. }

4.1.3 默认方法

可以为接口方法提供一个默认实现,此时该方法应该使用default修饰。虽然绝大多数情况下不会用到默认方法,因为实现接口的类都会实现接口定义的方法,但在某些情况下默认方法也是很有用的。

例如Iterator接口的实现如下:

public interface Iterator<E>{
    boolean hasNext();
    E next();
    default void remove(){
        throw new UnsupportedOperationException("remove");
    }
}

类实现该接口必须实现hasNext()next(),但是并非所有迭代器都需要实现remove()。如果没有实现却被调用,就会抛出异常。

此外,默认方法中能够调用接口中定义的其他方法。例如Collection接口中可以定义一个便利方法:

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

这样,实现了Collection接口的类就不必再去实现isEmpty()了。

默认方法还有一个很重要的用法是”接口演化”,比如自定义了一个类Bag实现了Collection接口,过了很久以后,该接口添加了一个非默认方法,此时Bag类将无法被编译,因为没有实现这个接口新增的方法。此时就可以将该新增的方法声明为默认方法,这样既能保证实现了它的类能够正常编译,也能重写默认方法实现覆盖。

4.1.4 解决默认方法冲突

问题一:如果接口A提供了一个默认方法func(),并且类L实现了A接口,同时L的父类也定义了同名同参数的方法func(),那么类L的对象调用该方法时调用的是谁的?
:忽略接口提供的默认方法,只考虑父类方法。

问题二:接口A和接口B都提供了默认方法func(),且类C实现了这两个接口,则类C对象调用func()时调用谁的?
Java不允许这种二义性出现,类C必须实现该方法。

4.1.5 接口与回调

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

个人对回调的理解如下
一个类A不具有做某件事的能力,但是它知道B有,于是通过调用B类的方法b()做成了这件事,如果A希望B做完这件事后将结果通知给它,此时B又会调用Acallback()方法,来将结果告知给A,这就叫做回调
4. 接口、lambda表达式与内部类 - 图1
回调只是一种思想,而非某个编程语言所独有,由于Java是纯面向对象的语言,故不存纯函数这一说,也就没有所谓的回调函数,而在其他的编程语言中则有这一概念。在Java中回调的实现是通过将对象作为参数传递给某个方法来实现的,其中该对象实现了某个接口。

一定程度上可以验证个人之前的思考,即某个对象实现了某个接口,说明该对象获得了做某件事的能力(接口给类赋能),那么别的类就可以通过调用该对象来做某件事。

举例

public class TimerTest {

    public static void main(String[] args) {
        ActionListener listener = new TimePrinter();

        Timer timer = new Timer(1000, listener);
        timer.start();

        //当用户点击ok时,程序停止执行
        JOptionPane.showMessageDialog(null, "Quit program?");
        System.exit(0);
    }
}

class TimePrinter implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent event){
        System.out.println("At the tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
    }
}

目标:每一秒打印出当前的时间
代码分析TimerJava中一个定时器类,它本身并没有打印当前时间的能力,所有自定义了实现了该功能的TimePrint类,那么要做的就是通过Timer去调用TimePrint类中的actionPerformed()。当执行第7行代码时,就开始每隔1s调用actionPerformed()。至于为什么会这么执行,是因为Timer类有一个构造器public Timer(int delay, ActionListener listener),当listener不为null时,就会为当前的Timer对象timer加一个监听器,调用start()方法后做了什么呢?官方文档的描述如下:

causing it to start sending action events to its listeners. 使它开始向其侦听器发送动作事件。

这里我理解就是将每秒的监听动作作为event参数传给了监听器的actionPerformed()并去调用该方法。此时控制台每隔1s就会打印出当前的时间信息,那么什么时候停止呢?第10行定义了一个面板,当用户点击面板上的ok时,就会结束主程序,这里我认为会回调timerstop(),这才能体现回调思想。

4.1.6 Comparator接口

前面学过如果要使用Arrays.sort()对自定义类的对象进行排序,则该类就需要实现Comparable接口。但是这会存在一个问题,即如果对一个类定义了不止一种的排序方式,该怎么办呢?我们定义的类总不能再去实现一次Comparable接口。好在Arrays.sort还有另一种版本Arrays.sort(T[] a, Comparator<? super T> c)

分析第二种版本,可以看出传入了两个参数,一个是数组,另一个是一个比较器。其中该比较器是实现了Comparator接口的类的实例。Comparator接口的定义如下:

public interface Comparator<T> {
    int compare(T first, T second);
}

String默认是按照字典序进行排序的,因为在String类实现了Comparable接口。但是假如我们需要使其按照长度进行排序,就需要去实现一个类实现Comparator接口,代码如下:

public class ComparatorTest {
    public static void main(String[] args) {
        String[] name = {"abc", "abcdef", "bf"};
        Arrays.sort(name, new LengthComparator());

        for (String s : name) {
            System.out.println(s);
        }
    }
}

class LengthComparator implements Comparator<String> {
    public int compare(String first, String second) {
        return first.length() - second.length();
    }
}

4.1.7 对象克隆

之前学习过将对象变量赋值给另一个对象变量,此时两个变量指向同一个对象,即只要对象内容改变,那么这两个变量的引用对象都将发生变化。

克隆则是希望两个对象的初始状态一致,但是后续各自的变化不会影响到对方。

Object类中定义了clone(),被protected进行修饰。Object类的clone()实现方法为:若是基本数据类型,则拷贝一份;若是引用数据类型,则拷贝其引用。这也叫做浅拷贝。事实上,希望达到的效果是,即使为引用数据类型,针对里面的内容也是独立的两份,所以要对里面的引用类型再做clone(),这也叫做深拷贝

为了让一个类具有clone()功能,需要让其实现Cloneable接口,并重写Object类的clone(),并将其声明为public。之所以声明为public是为了在其他类中也能对当前类进行克隆操作。

Cloneable接口是Java中提供的少数标记接口之一(空接口),顾名思义其只起到标记作用。可以通过instanceof来判断一个类是否实现了该接口,如果没有实现该接口并试图调用clone(),会抛CloneNotSupportedException异常。

4.2 lambda表达式

4.2.1 为什么引入lambda表达式

初步理解:简化了匿名内部类的写法。

4.2.2 lambda表达式的语法

  • 参数列表:实际上就是接口中抽象方法的形参
  • 箭头
  • lambda体:实现接口的抽象方法体
语法格式:
1. 无参数,无返回值
() -> System.out.println("Hello world");  # 当无参数时,参数列表部分需要一对空括号

2. 一个参数,但无返回值
str -> System.out.println("Hello world"); 
# 一个参数时,小括号可以省略;此外,无需声明参数的类型,因为编译器会进行类型推断

3. lambda体为多行,需要用大括号将体括起来,单行则不需要
(first, second) -> 
{
    if (first.length() < second.length()) return -1;
    else if (fist.length() > second.length()) return 1;
    else return 0;
}

4. 当lambda体存在return且只有一行时时,可以省略return

4.2.3 函数式接口

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

当自定义函数式接口后,可以为其加上@FunctionalInterface注解,方便进行校验。

4.2.4 内置的常用函数式接口

java.util.function包下定义了很多通用的函数式接口,其中最为重要的有如下四种:

函数式接口 参数类型 返回类型 抽象方法名 描述
Consumer T void accept 处理一个T类型的值
Supplier void T get 提供一个T类型的值
Function T R apply 有一个T类型参数的函数
Predicate T boolean test 布尔值函数

举例:过滤出一个列表中包含字母a的所有字符串
思路:定义一个方法filterString(List<String> list, Predicate<String> pre),其中第二个参数为函数式接口,故可以传入一个lambda表达式作为其实例。当第12行执行时,实际上就是执行了lambda表达式的主体。

class Solution {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("abc", "bc", "ac");
        List<String> list1 = filterString(list, s -> s.contains("a"));
        System.out.println(list1);  # [abc, ac]
    }

    public static List<String> filterString(List<String> list, Predicate<String> pre) {
        ArrayList<String> filterList = new ArrayList<>();

        for(String s : list) {
            if(pre.test(s)){
                filterList.add(s);
            }
        }
        return filterList;
    }
}

4.2.5 方法引用(一)

有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作,你就无需再自己去实现,而是使用方法引用

比如有如下的Person类:

public class Person {

    // ...

    LocalDate birthday;

    public int getAge() {
        // ...
    }

    public LocalDate getBirthday() {
        return birthday;
    }   

    public static int compareByAge(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
    }

    // ...
}

假设有一群人存在于一个Person数组中,目标是对该数组中人的生日进行排序,则可用如下代码实现:

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

class PersonAgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person a, person b) {
        return a.getBirthday().compareTo(b.getBirthday);
    }
}

Arrays.sort(rosterAsArray, new PersonAgeComparator());

由于Comparator是一个函数式接口,故可以用lambda表达式简化上述代码:

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

class PersonAgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person a, person b) {
        return a.getBirthday().compareTo(b.getBirthday);
    }
}

Arrays.sort(rosterAsArray, (a, b) -> a.getBirthday().compareTo(b.getBirthday());

然而注意到,在自定义的比较器类中重写的compare方法其实和Person类中定义的compareByAge方法做了一样的事,所以大可不必重新定义一个这样的方法而是直接引用已有的compareByAge()。于是代码可以重写如下:

Arrays.sort(rosterAsArray, (a, b) -> Person.compareByAge(a, b))

进一步就可以写成方法引用的写法:

Arrays.sort(rosterAsArray, Person::compareByAge)

可以看出,方法引用是**lambda**表达式的另一种表示方式,其中方法引用写法并未显式的指明参数,其形参来源于其赋予接口抽象方法的形参,而方法体则是调用了引用的方法。

再举几个例子:

  1. 你希望出现一个定时器事件就打印该事件对象:

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

  使用方法引用:

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

4.2.6 方法引用(二)

有四种不同类型的方法引用:

方法 语法 实例
引用一个静态方法 Class::staticMethod Peron::compareByAge
针对特定对象引用实例方法 object::instanceMethod myApp::appendString2
针对一特定类型引用其任意对象的实例方法 Class::instanceMethod String::concat
引用一个构造器 ClassName::new HashSet::new

其中,前两种等价于提供方法参数的lambda表达式;对于第三种情况,第一个参数会成为方法的目标。举例说明:

1. 引用静态方法
Math::pow == (x, y) -> Math.pow(x, y);

2. 针对特定对象引用实例方法
System.out::println == x -> System.out.println(x);

3. 针对一特定类型引用其任意对象的实例方法
String::compareToIgnoreCase == (x, y) -> x.compareToIgnoreCase(y);

第四种类型为构造器引用,它和方法引用的形式很像,但是方法均为new,当某个类含有多个构造器时,它会自动根据上下文选择合适的构造器。

import java.util.function.*;

public class Stu {
    private Integer id;
    private String name;
    private Integer age;

    public Stu() {
    }

    public Stu(Integer id) {
        this.id = id;
    }

    public Stu(Integer id, Integer age) {
        this.id = id;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Stu{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public static void main(String[] args) {
        //调用无参构造器
        Supplier<Stu> supplierStu = () -> new Stu();
        System.out.println(supplierStu.get());
        Supplier<Stu> supplierStu1 = Stu::new;
        System.out.println(supplierStu1.get());

        //调用一个参数的构造器
        Function<Integer, Stu> functionStu = id -> new Stu(id);
        System.out.println(functionStu.apply(100));
        Function<Integer, Stu> functionStu1 = Stu::new;
        System.out.println(functionStu1.apply(200));

        //调用两个参数的构造器
        BiFunction<Integer,Integer,Stu> biFunctionStu = (id, age) -> new Stu(id, age);
        System.out.println(biFunctionStu.apply(100, 18));
        BiFunction<Integer, Integer, Stu> biFunctionStu1 = Stu::new;
        System.out.println(biFunctionStu1.apply(200, 20));
    }
}

上述代码中,Stu类有三种不同的构造器,分别为无参,单参和双参。测试程序中,根据构造器引用赋给不同的接口对象,自动选择了最为合适的构造器。

除了方法引用与构造器引用外,还有数组引用。语法格式为:Type[]::new

//举例
public void test(){
    //创建指定长度的字符串数组
    Function<Integer,String[]> f1 = (x) -> new String[x];
    System.out.println(f1.apply(10).length); //10
    Function<Integer,String[]> f2 = String[]::new;//数组引用 可以直接代替
    System.out.println(f2.apply(20).length); //20
}

4.2.7 变量作用域

lambda表达式可以捕获外围作用域的变量,但是只能引用值不会改变的变量

4.3 内部类

在一个类内部定义的类称为嵌套类,嵌套类分为两种,一种为非静态嵌套类(non-static nested class),也称为内部类;另一种为静态嵌套类(static nested class)。

内部类可以进一步分为局部内部类匿名内部类

4.3.1 内部类简单实例和原理小述

定义一个类TalkingClock,它能够规律性的beep,由于本例想要每隔一段时间去调用某段代码,故使用了java.swing.Timer来实现。TalkingClock类定义如下:

class TalkingClock {

    private int interval;
    private boolean beep;

    //构造器
    public TalkingClock(int interval, boolean beep) {
        this.interval = interval;
        this.beep = beep;
    }

    //start the clock
    public void start() {
        ActionListener listener = new TimePrinter();
        Timer timer = new Timer(1000, new TimePrinter());
        timer.start();

    }

    private class TimePrinter implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
}

代码分析
TimePrinter类是内部类,其外部类为TalkingClock,在外部类的start()中,创建了TimePrint类的实例。说明要创建TimePrinter类的实例,需要由TalkingClock的对象调用start()来创建。

此时注意25行,在内部类中调用了beep,但是内部类并没有名为beep的实例域或变量,说明其访问的是调用它的外部类的beep值。之所以能做到访问外部类对象的属性,是因为内部类的对象有一个隐式引用,指向了创建它的外部类的对象。

截屏2021-05-26 下午3.02.38.png
所以代码的25行等价于:if(outer.beep),这个引用的实现来自于内部类的构造器,编译器会为内部类自动生成一个默认构造器,在本例中构造器如下:

public TimePrint(TalkingClock clock) {
    outer = clock;
}

所以在第14行去实例化TimePrinter时,编译器会将this作为参数传递给构造器。即
ActionListener listener = new TimePrinter(this);

综上所述,内部类是可以访问外部类对象的所有成员的。

4.3.2 更标准的语法

上节中的outer只是为了说明内部类的机制,java中并非有此关键字,本节将展示更标准的语法。

  1. 表达式OuterClass.this表示外部类的引用;
  2. 创建内部类对象:OuterClass.InnerClass innerObject = outerObject.new InnerClass();

4.3.3 局部内部类

局部类是被定义在一个block中的类。所谓block,就是在一对括号内的0条或若干条语句。通常来说,可以看到局部内部类定义在方法体中。

在4.3.1节的代码中,TimePrinter这个类名只有在start()中创建它的对象时出现了一次,所以可以将其定义在一个方法中:

public void start() {
    class TimePrinter implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }

    ActionListener listener = new TimePrinter();
    Timer timer = new Timer(1000, new TimePrinter());
    timer.start();
}

局部内部类不能用publicprivate进行修饰,它的作用域被限定在所在的block中。

局部类的优势在于可以对外界完全隐藏,及时外部类的代码也无法访问它,只有start知道它的存在。另一个优势在于它不仅可以访问包含它的外部类,还可以访问局部变量,但是局部变量必须由final修饰。理由看下例:

public void start(int interval, boolean beep) {
    class TimePrinter implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the time is " + new Date());
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }

    ActionListener listener = new TimePrinter();
    Timer timer = new Timer(1000, new TimePrinter());
    timer.start();
}

代码分析
当调用start()时,首先实例化TimePrinter,然后实例化一个Timer,最后调用start()启动计时器。此时start()结束,它的参数变量beep也随之消失,但是之后在执行actionPerform()时,需要调用beep

那么问题出现,既然beep消失,它怎么能访问到beep呢?是因为局部类将局部变量复制了一份作为自己的成员变量保存了起来。其次,为了保证复制的局部变量与成员变量保持一致,所以必须用final进行修饰,否则如果类中存在修改成员变量的代码,执行时就不能保证二者一致了。

4.3.4 匿名内部类(Anomymous Classes)

匿名内部类可以使代码更简洁,它允许你同时声明和实例化一个类。它与局部类很类似,不同之处在于匿名类没有类名,当局部类只需要使用一次时,可以使用匿名内部类。

匿名内部类的语法组成如下:

new 实现接口()
{
    //匿名内部类类体部分
}

new 父类构造器(实参列表)
{
  //匿名内部类类体部分
}

4.3.5 静态内部类

如果用static来修饰一个内部类,那么就是静态内部类。这个内部类属于外部类本身,但是不属于外部类的任何对象。因此使用static修饰的内部类称为静态内部类。

静态内部类有如下规则:

  • 静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。
  • 外部类可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象访问其实例成员。