接口

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

  • 接口可以定义常量, 但是绝不能有实例字段
  • Java 8之前, 接口中的方法都是抽象方法
  • 定义接口的方法不必指定为public, 因为接口方法自动为public
  • 接口中的字段都是public static final的, 也不需要手动指定
  • 但是实现接口时, 必须明确写public, 否则编译器会默认认为这个方法的访问属性是包可访问, 就会报错
1
2
3
4
5
6
7
8
class E implements Comparable<E> {
public int compareTo(E oth) {
return Double.compare(salary, oth.salary);
// 如果x < y 返回 负数
// 如果x = y 返回 0
// 如果x > y 返回正数
}
}
  • Comparable接口文档建议compareTo方法与equals方法兼容, 即x.equals(y)时, x.compareTo(y) == 0
  • 大部分都是兼容的, 除了BigDecimal
    • x = new BigDecimal("1.0")y = new BigDecimal("1.00");因为精度不同, 所以x.compareTo(y) == 0但是x.equals(y) == false;
    • 理论上应该不返回0, 但是不知道谁大谁小
  • 语言标准规定 x.compareTo(y) == -y.compareTo(x), 同样如果前者抛出异常, 后者也必须抛出异常
  • 如果M继承自E, E实现了Comparable<E>, 而没有实现Comparable<M>
  • 如果要在M中重写, 就要做好准备比较ME, 不能简单的将E转换为M
  • 比如xM, yE, 调用y.compareTo(x)不会抛出异常, 调用x.compareTo(y)就会抛出一个ClassCastException
  • 可以在每个compareTo之前都进行检测: if (getClass != oth.getClass()) throw new ClassCastException

接口属性

  • 接口不是类, 不能使用new Comparable()
  • 但是可以使用接口变量, 必须引用一个实现了这个接口的对象Comparable x = new E()
  • 可以使用instanceof检查某个对象是否实现了一个接口
  • 可以使用extends扩展接口, 一个类只能有一个超类, 但是可以实现多个接口
  • 记录和枚举类不能扩展其他类, 因为他们隐式扩展了RecordEnum类, 但是他们可以实现接口
  • 接口也可以是密封的sealed, 直接子类型, 必须声明在permits中, 或者在一个文件中

默认方法

  • 可以为接口提供一个default方法, 表示方法的默认实现

    1
    2
    3
    4
    5
    public interface Comparable<T> {
    default int compareTo(T oth) {
    return 0;
    }
    }
  • 大部分情况没什么用, 因为每个实现的接口都会覆盖这个方法

  • 不过有时候也能有用, 比如Iterator接口, 声明了一个remove()

    1
    2
    3
    4
    5
    public interface Iterator<E> {
    default void remove() {
    throw new UnsupportOperationException("remove");
    }
    }
  • 默认方法也可以调用其他方法

  • 默认方法可以实现接口演化, 实现代码兼容. 比如以前有一个类class B implements Collection, 后来Java版本更新, Collection接口中添加了新的方法, 如果不用default修饰新的方法就会导致原来的类B无法编译

  • 如果在一个接口中定义了一个方法, 在超类或者另一个接口中定义了同样的方法, Java有自己的规则:

    1. 超类优先: 如果超类定义了一个具体方法, 同名且有相同参数类型的默认方法会被忽略
    2. 接口冲突: 如果一个接口提供了一个默认方法, 另一个接口提供了一个同名并且参数类型相同的方法, 不论是否为默认方法, 就需要覆盖这个方法来解决冲突
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      interface Person {
      default String getName() {return "";}
      }
      interface Name {
      default String getName() {return getClass().getName() + "_" + hashCode();}
      }
      // 如果有一个类同时实现了这两个接口
      class Student implements Person, Name {}
      // 编译器会报错, 需要在Student类中提供一个getName(), 可以选择两个冲突方法中的一个
      class Student implements Person, Name {
      public String getName() {
      return Person.super.getName();
      }
      }
      // 就算如果Name类中没有定义默认的getName()方法, 编译器还是会报错, 要求程序员解决二义性问题
      // 如果两个类都没有提供默认的getName()方法, 就不会有冲突
      • 另一种情况是类扩展了超类, 同时实现了一个接口, 超类和接口继承了相同的方法
      1
      2
      3
      4
      class Student extends Person implements Name {}
      // 这种情况下只会考虑超类的方法, 接口所有的默认方法都会被忽略
      // 类优先的规则可以确保和Java 7的兼容性
      // 如果为一个接口添加默认方法, 对于有默认方法之前的版本代码不会有影响
  • 绝对不能为Object某个方法定义默认方法, 比如toString(), equals()
  • 因为类优先的规则, 这样的方法绝对无法超越Object.toString()Objects.equals()

Comparator接口

  • 如果调用Arrays.sort()对字符串进行排序的话, 会按照字典序

  • 现在如果想要按照字符串的长度进行排序, 就不能修改String.compareTo()

  • 可以使用Arrays.sort()方法的第二个版本, 接受一个数组和一个比较器作为参数, 比较器是实现了Comparator接口的类的实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public interface Comparator<T> {
    int compare(T first, T second);
    }
    // 比如需要按照长度比较字符串, 可以以如下方法实现
    class LengthComparator implements Comparator<String> {
    public int compare(String first, String second) {
    return first.length() - second.length();
    }
    }
    // 完成比较需要建立一个实例
    var comp = new LengthComparator();
    if (comp.compare(words[i], words[j]) > 0) {}
    // 如果需要对一个数组进行排序, 可以调用 Arrays.sort()
    String[] f = {};
    Arrays.sort(f, new LengthComparator);
  • 静态comparing方法接受一个键提取器函数, 将类型T映射为一个可比较的类型, 比如String

    • 要比较的对象引用这个函数, 然后对返回的键完成比较, 比如假设有一个Person数组, 可以按照名字进行排序
    • Arrays.sort(people, Comparator.comparing(Person::getName));
    • 可以把比较器和thenComparing()串起来, 处理比较相同的结果
    • Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
    • 可以给comparingthenComparing提取的键指定一个比较器, 完成对人名长度的排序
    • Arrays.sort(people, Comparator.comparing(Person::getName, (s, t) -> Integer.compare(s.length(), t.length())));
    • Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));
  • 如果键函数可能返回null, 就需要用到nullsFirstnullsLast适配器, 可以修改比较器, 遇到null的时候不会抛出异常, 而是标记当前值小于或大于正常值

    • Comparator.comparing(Person::getMiddleName(), Comparator.nullsFirst(...));
    • nullsFirst方法需要一个比较器, 就是两个字符串的比较器
    • naturalOrder方法可以为任何实现了Comparable的类建立一个比较器
    • Arrays.sort(people, comparing(Person::getMiddleName, nullsFirst(naturalOrder()));
    • 静态reverseOrder方法可以提供逆序, 等同于naturalOrder().reversed()

Cloneable接口

  • cloneObjectprotected方法, 不能直接调用这个方法, 子类只能调用受保护的clone()来克隆他自己的对象, 如果其中包含了一些其他对象, 就没有办法clone
  • 默认的克隆操作是一个浅拷贝, 没有克隆对象中引用的其他对象
  • 如果原对象和浅克隆对象共享的子对象是不可变的, 那么这种共享就是安全的, 比如String
    • 或者在对象生命周期中, 子对象一直保持不变,没有更改器方法改变它, 也没有方法生成他的引用, 这种情况下就是安全的
  • 但是大多数情况下, 子对象都是可变的, 必须重新定义clone方法, 需要确定
    1. 默认的clone方法能满足要求
    2. 可以在可变子对象上调用clone弥补默认的clone
    3. 不能使用clone
    • 如果指定第一项或者第二项, 需要实现Cloneable接口, 重新定义clone方法, 同时指定public
  • Cloneable接口是Java中少数的标记接口, 记号接口
  • 用途是确保一个类实现一个特定的方法或一组方法, 标记接口不包含任何方法, 唯一的作用就是允许在类型查询中使用instabceof
  • 自己写代码不要使用标记接口
  • 即使默认的clone()可以满足要求, 还是需要实现Clonebale接口, 将clone重新定义为public, 调用super.clone()
    1
    2
    3
    4
    5
    6
    class E implements Cloneable {
    public E clone() throws CloneNotSupportException {
    E cloned = (E) super.clone();
    cloned.Day = (Date) Day.clone();
    }
    }
  • 需要注意子类的克隆, 一旦为E定义了clone(), 别人就可以使用他克隆子类M
  • 所以最好避免使用clone(), 使用另一个方法达到同样的目的

lambda表达式

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

  • 以上面的排序为例, first.length() - secode.length() 其中first, second都是字符串
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // lambda表达式写法
    (String first, String second) -> {
    if (first.length() < second.length()) return -1;
    if (first.length() > second.length()) return 1;
    else return 0;
    }
    // 即使lambda表达式没有参数, 仍然需要提供空括号
    () -> {
    for (int i = 100; i >= 0; i --) {
    System.out.println(i);
    }
    }
    // 如果可以推导出lambda表达式的类型, 就可以忽略类型
    Comparator<String> comp = (first, second) -> {
    first.length() - second.length();
    }
    // 如果只有一个参数, 并且参数类型还可以推导出来, 那么还可以省略小括号
    ActionListener listener = event -> {
    System.out.println("1111" + Instant.ofEpochMilli(event.getWhen()));
    }
    // 不需要指定lambda表达式的返回类型, 因为返回类型一定会根据上下文推导得到
    // 可以使用var指示一个推导的类型, 不常用, 一般为了关联注解
    (@NonNull var first, @NonNull var second) -> first.length() - second.length();
  • 如果一个lambda表达式只有部分分支有返回值, 是不合法的

函数式接口

  • 对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个lambda表达式, 称为函数式接口
    1
    2
    3
    4
    5
    6
    7
    // 比如Arrays.sort()第二个参数需要一个Comparator实例
    // 只有一个抽象方法的接口, 就可以改成lambda表达式
    Arrays.sort(words, (first, second) -> first.length() - second.length());
    // 可以把lambda表达式看作一个函数, 而不是一个对象, 同时lambda表达式可以传递到函数式接口
    // 不能把lambda表达式赋值给类型为Object的变量, Object不是一个函数式接口
    // ArrayList通过lambda表达式删除一个数组列表中所有的null
    list.removeIf(e -> e == null);

方法引用

1
2
3
4
5
6
7
8
9
10
var timer = new Timer(1000, event -> System.out.println(event));
// 方法引用可以直接将println传入timer中
var timer = new Timer(1000, System.out::println);
// 这里System.out::println是一个方法引用
// 指示编译器生成一个函数式接口实例, 覆盖这个接口的抽象方法来调用给定的方法
Runnable task = System.out::println;
// 这里的Runnable函数式接口有一个无参数的抽象方法 void run()
// 这里调用task.run() 就会自动选择无参数的println() 打印一个空行
// 如果想要对字符串进行排序, 忽略大小写
Arrays.sort(words, String::compareToIgnoreCase);
  • 使用::操作符分割方法名和对象或者类名, 有三种情况:
    1. object::instanceMethod
      • 方法引用等价于一个lambda表达式, 参数传递到方法, 对于System.out::println, 对象是System.out, 方法等价于x -> System.out.println(x)
    2. Class::instanceMethod
      • 第一个参数会成为隐式参数, 比如String::compareToIgnoreCase等同于(x, y) -> x.compareToIgnoreCase(y)
    3. Class::staticMethod
      • 所有参数都传递到静态方法, Math::pow等价于(x, y) -> Math.pow(x, y)
  • 只有当lambda表达式的体只调用一个方法而不做其他操作的时候, 才能将其转换为方法引用
  • s -> s.length() == 0里面只有一个方法调用, 但是还有一个比较, 所以不能使用方法引用
  • 方法引用不会独立存在, 总是会转换为函数式接口的实例
  • 如果要删除数组中所有为空的值, 可以使用 list.removeIf(Objects::isNull)
  • 包含对象的方法引用与等价的lambda表达式的区别, 比如separator::equals, 如果separatornull, 构造separator::equals时就会立即抛出NullPointerException异常, lambda表达式x -> separator.equals(x)只会在调用时才会抛出NPE
  • 可以在方法引用中使用this参数, 比如this::equals 等同于x -> this.equals(x)
  • 同样使用super也是可以的

构造器引用

和方法引用类似, 只不过方法名为new, 比如Person::newPerson的构造器引用, 使用哪一个构造器取决于上下文

1
2
3
4
5
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.toList();
// 这里将字符串列表转换为一个Person数组
// map为各个列表元素调用Person(String)构造器
  • int[]::new有一个数组的长度作为参数, 是一个构造器引用
  • Java中无法构造泛型类型T的数组, 数组构造器可以克服这个限制
1
2
3
Object[] people = stream.toArray();
// toArray()可以返回一个Object类型数组, 但是如果希望得到一个Person数组, 就需要构造器引用
Person[] people = stream.toArray(Person[]::new);

变量作用域

  • 一个lambda表达式有三个部分
    1. 一个代码块
    2. 参数
    3. 自由变量: 指非参数, 并且不在代码中定义的变量
  • lambda会将自由变量的值复制到lambda表达式的数据结构实例对象中
  • lambda表达式中只能引用不会改变的值, 比如引用int就是不合法的
  • 如果在lambda表达式中更改变量, 并发执行多个动作的时候就不安全
  • 就算lambda表达式内部没有修改变量, 但是这个变量也有可能在外部改变, 这也是不合法的
  • lambda表达式捕获的变量必须是事实最终变量, 指的是这个变量初始化以后不会赋新值, 比如String
  • lambda表达式中不能声明和局部变量同名的参数或变量
  • lambda表达式中使用this参数的时候, 是指创建这个lambda表达式的方法的this参数
    1
    2
    3
    4
    5
    6
    7
    8
    public class A {
    public void init() {
    B b = event -> {
    System.out.println(this.toString());
    }
    }
    }
    // 这里的this.toString()调用的是A对象的toString(), 而不是B实例方法

处理lambda表达式

lambda表达式的重点是延迟执行

  • 延迟执行的原因
    1. 单线程
    2. 多次运行代码
    3. 在算法的适当位置运行, 比如排序的比较操作
    4. 发生某种情况的时候运行, 比如点击按钮, 数据到达
    5. 只有必要时才运行代码
1
repeat(10, () -> System.out.println("1"));
  • 如果设计自己的函数式接口, 里面只有一个抽象方法, 可以使用@FunctionalInterface注解来标记接口
    • 这样如果添加了一个新的抽象方法, 可以检查出来并报错
    • 同时javadoc会标记这个接口是一个函数式接口
    • 不是必须使用这个注解