Java核心技术1-接口

  |  

摘要: 本文是《Java核心技术 10th》中接口的要点总结

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


本文是《Java核心技术1》第10版 Chap6 中关于【接口】的要点总结。

接口(interface)技术主要用来描述类具有什么功能,而并不给出每个功能的具体实现。一个类可以实现(implement)一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象。


接口的概念

接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。

具体的例子:Arrays 类中的 sort 方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了 Comparable 接口。代码如下:

1
2
3
public interface Comparable {
int compareTo(Object other);
}

注意接口中的所有方法自动地属于 public,因此接口声明中不用加 public。

上面代码的意思是说:任何实现 Comparable 接口的类都需要包含 compareTo 方法,并且这个方法的参数必须是一个 Object 对象,返回一个整型数值。

上面的 Comparable 还有泛型版本,代码如下:

1
2
3
public interface Comparable<T> {
int compareTo(T other);
}

例如在实现 Comparable 接口的类中,必须提供 int compareTo(Employee other) 方法。

语言标准对 Comparable 接口还有附加要求:

  • 在调用 x.compareTo(y) 的时候,这个 compareTo 方法必须确实比较两个对象的内容,并返回比较的结果。当 x 小于 y 时,返回一个负数;当 x 等于 y 时,返回 0;否则返回一个正数。
  • 于任意的 x 和 y,实现必须能够保证 sgn(x.compareTo(y)) = -sgn (y.compareTo(x))

接口中可以有多个方法,还可以定义常量,但是不能含有实例域。Java8 以后可以在接口中实现方法,这些方法也不能引用实例域。

提供实例域和方法实现的任务应该由实现接口的那个类来完成。因此,可以将接口看成是没有实例域的抽象类。(但还是有区别的)

实现一个接口

实现一个接口通常有两步:

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

注意虽然接口声明中不用写 public,因为自动的是 public,但是实现接口时,必须声明 public。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Employee implements Comparable<Employee> {
private String name;
private double salary;

public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}

public String getName() {
return name;
}

public double getSalary() {
return salary;
}

public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}

/**
* Compares employees by salary
* @param other another Employee object
* @return a nagative value if this employee has a lower salary than
* otherObject, 0 if the salaries are the same, a positive value otherwise
*/
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
}
}

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Arrays;

/**
* this progream demonstrates the use of the comparable interface
* @version 1.0 2022-05-21
*/
public class EmployeeSortTest {
public static void main(String[] args) {
Employee[] staff = new Employee[3];

staff[0] = new Employee("Harry Hacker", 35000);
staff[1] = new Employee("Carl Cracker", 75000);
staff[2] = new Employee("Tony Tester", 38000);

Arrays.sort(staff);

for(Employee e: staff) {
System.out.println("name=" + e.getName() + ", salary=" + e.getSalary());
}
}
}

为什么不能在 Employee 类直接提供一个 compareTo 方法,而必须实现 Comparable 接口呢?

Java 程序设计语言是一种强类型(stronglytyped)语言。在调用方法的时候,编译器将会检查这个方法是否存在。在 sort 方法中可能存在下面这样的语句:

1
2
3
4
if(a[i].compareTo(a[j]) > 0) {
// rearrange a[i] and a[j]
...
}

为此,编译器必须确认 a[i] 一定有 compareTo 方法。如果 a 是一个 Comparable 对象的数组,就可以确保拥有 compareTo 方法,因为每个实现 Comparable 接口的类都必须提供这个方法的定义。

接口的实现在继承过程中可能出现的反对称性问题

考虑继承了 Employee 的 Manager。由于 Employee 实现的是 Comparable 而不是 Comparable,如果 Manager 覆盖了 compareTo,则必须承认 Manager 对象可以与 Employee 对象进行比较这件事,而不是将 Employee 转换为 Manager。

1
2
3
4
5
class Manager extends Employee {
public int compareTo(Employee other) {
Manager otherManager = (Manager) other;
}
}

上面的代码就是错误的将 Employee 转换为 Manager 的做法,违反了反对称性

也就是 e.compareTo(m) 是将 e 和 m 都作为 Employee 进行比较,不会抛出异常;但 m.compareTo(e) 会抛出 ClassCastException 异常。

上面这个反对称性的问题与文章 Java核心技术1-Object类 中关于 equals 的反对称性的情况一样,解决方法也一样,分两种情况

  • 子类之间的比较含义不一样,则每个 compareTo 方法都应该在开始时进行下面的检测
    1
    2
    3
    4
    5
    6
    7
    class Manager extends Employee {
    public int compareTo(Employee other) {
    if(getClass() != other.getClass()) {
    throw new ClassCastException();
    }
    }
    }
  • 如果有通用方法对两个不同的子类对象进行比较,则应该在超类中提供 compareTo 方法,并声明为 final

例如排序时,我们想对比职位,那么就应该在 Employee 中提供 rank 方法,然后子类覆盖 rank 方法即可,final 的 compareTo 方法中调用 rank。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Employee implements Comparable<Employee> {
private String name;
private double salary;

public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}

public String getName() {
return name;
}

public double getSalary() {
return salary;
}

public final int compareTo(Employee other) {
if(rank() < other.rank()) {
return -1;
} else if(rank() > other.rank()) {
return 1;
} else {
return Double.compare(salary, other.salary);
}
}

public int rank() {
return 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Manager extends Employee {
private double bonus;

public Manager(String name, double salary) {
super(name, salary);
bonus = 20;
}

public double getSalary() {
double baseSalary = super.getSalary();
return baseSalary + bonus;
}

public int rank() {
return 1;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Arrays;

public class EmployeeSortTest {
public static void main(String[] args) {
Employee[] staff = new Employee[5];

staff[0] = new Employee("Harry Hacker", 35000);
staff[1] = new Employee("Carl Cracker", 75000);
staff[2] = new Manager("Manager1", 30000);
staff[3] = new Employee("Tony Tester", 38000);
staff[4] = new Manager("Manager2", 40000);

Arrays.sort(staff);

for(Employee e: staff) {
System.out.println("name=" + e.getName() + ", salary=" + e.getSalary());
}
}
}

接口的特性

  • 不能构造接口的对象,但是可以声明接口的变量,之后该变量必须引用实现了接口的类的对象。
1
2
Comparable x;
x = new Employee(...);
  • 判断对象是否实现了某个特定的接口:
1
2
3
if(x instanceof Comparable) {
...
}
  • 接口可被扩展,与类的继承关系类似。
1
2
3
4
5
6
7
public interface Moveable {
void move(double x, double y);
}

public interface Powered extends Moveable {
double milePerGallon();
}
  • 接口中不能包含实例域,可以包含常量
1
2
3
4
5

public interface Powered extends Moveable {
double milePerGallon();
double SPEED_LIMIT = 95; // 这是一个 public static final 常量
}
  • 一个类只能有一个超类,但是可以实现多个接口

例如 Java 有一个内置接口 Cloneable。我们自定义的类可以即实现 Comparable 接口,又实现 Cloneable 接口。

1
2
3
class Employee implements Cloneable, Comparable {
...
}

接口与抽象类

为什么不将 Comparable 设计成接口而不是抽象类,代码如下:

1
2
3
4
5
6
7
abstract class Comparable {
public abstract int compareTo(Object other);
}

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

最大的问题是 Java 是不支持多重继承的,接口可以提供多重继承的大多数好处,同时避免多重继承的复杂型和低效性。

C++ 的多重继承与 Java 的接口

C++ 具有多重继承特性,随之带来了一些诸如虚基类、控制规则和横向指针类型转换等复杂特性。


接口中的静态方法

Java8 以后,可以在接口中增加静态方法,虽然合法,但是与接口作为抽象规范的初衷。

实践中的做法是将静态方法放到伴随类中。例如标准库中,会看到成对出现的接口和实用工具类,例如 Collection/CollectionsPath/Paths

但是 Java8 以后,这种伴随类过时了,因为可以咋接口中实现方法了。以 Path 接口为例,Paths 是它的伴随类,但是现在可以直接将静态方法在 Path 接口中实现了,Paths 其实已经不必要了。

1
2
3
4
5
6
public interface Path {
public static Path get(String first, String... more) {
return FileSystem.getDefault().getPath(first, more);
}
...
}

接口方法的默认实现

1
2
3
4
5
public interface Comparable<T> {
default int compareTo(T other) {
return 0;
}
}

一般情况下,这种默认实现没用,因为每一个实现该接口的类都要覆盖这个方法。

默认方法比较有用的几种情况:

(1) 但是有的时候希望在实现接口的类中没用覆盖某方法时,可以有默认的动作,此时默认实现就有用了。例如接口中的方法很多的时候,如果每个方法都有默认实现,则在实现接口的类中只实现关心的方法即可。

(2) 默认方法可以调用任何其他方法,例如 Collection 接口:

1
2
3
4
5
6
public interface Collection {
int size();
default boolean isEmpty() {
return size() == 0;
}
}

这样在实现 Collection 的时候,就不用实现 isEmpty 方法了。

(3) 接口演化:要给接口增加方法的时候的兼容问题。

例如 Collection 接口有一个类:

1
public class Bag implements Collection

此时给 Collection 加一个 stream 方法,如果没有默认实现,则已经使用的 Bag 不能编译,因为它没有实现 stream 方法,此时如果给 stream 默认实现,皆可以解决。


默认方法的冲突

先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法。则会出现问题。

在 Scala 和 C++ 中,解决这种二义性的规则很复杂,Java 中相对简单,如下

(1) 超类优先:如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
(2) 接口冲突:如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。

第一个规则的例子:Person 是一个类,Named 是一个接口:

1
2
3
class Student extends Person implements Named {
...
}

此时只会考虑超类方法,接口的所有默认方法会被忽略,也就是 Student 从 Person 继承了 getName 方法,而 Named 接口是否给 getName 默认实现不影响。

有了这种类优先的规则,如果一个接口增加默认方法,则此前正常工作的方法没有影响。

不要让接口中的默认方法重新定义 Object 类中的某个方法。例如,不能为 toString 或 equals 定义默认方法,尽管对于 List 之类的接口这可能很有吸引力。由于“类优先”规则,这样的方法绝对无法超越 Object.toString 或 Objects.equals。

第二个规则的例子,Person 和 Named 是两个接口:

1
2
3
4
5
6
7
8
9
10
11
interface Named {
default String getName() {
return getClass().getName() + "_" + hashCode();
}
}

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

Student 会继承 Person 和 Named 接口提供的两个不一样的 getName 方法,那么 Student 中必须覆盖这个方法,可以选择这两个冲突方法中的一个,也可以自己实现。


接口的一些示例

接口与回调

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

在 Java 中动作是通过对象传递出去的,该对象会持有相应的方法和附加信息。

例如,在 A 类的 func1 方法中,当发生某个事件时,调用 B 对象中的 func2 方法,这个 func2 方法可以抽象为一个接口,B 类需要实现这个接口即可。

  • callback/InterfaceCallback.java
1
2
3
4
5
package callback;

public interface InterfaceCallback {
void callback(String info);
}
  • callback/PrintInfoProcessor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package callback;

public class PrintInfoProcessor {
private InterfaceCallback[] callbacks;

public PrintInfoProcessor(InterfaceCallback[] callbacks) {
this.callbacks = callbacks;
}

public void printInfo(String str) {
if(str.length() % 2 == 0) {
System.out.println("进入 str 长度为偶数的回调函数");
callbacks[0].callback(str);
} else {
System.out.println("进入 str 长度为奇数的回调函数");
callbacks[1].callback(str);
}
}
}
  • callback/InterfaceCallbackTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package callback;

public class InterfaceCallbackTest {
public static void main(String[] args) {
InterfaceCallback[] callbacks = new InterfaceCallback[2];
callbacks[0] = new Callback1();
callbacks[1] = new Callback2();

PrintInfoProcessor processor = new PrintInfoProcessor(callbacks);

processor.printInfo("abcd");
processor.printInfo("abcde");
processor.printInfo("abcdef");
processor.printInfo("abcdefg");
}
}

class Callback1 implements InterfaceCallback {
public void callback(String info) {
System.out.println("callback1: " + info);
}
}

class Callback2 implements InterfaceCallback {
public void callback(String info) {
System.out.println("callback2: " + info);
}
}

Comparator 接口: 自定义比较方法

要对一个对象数组排序,需要对象实现了 Comparable 接口。例如可以对 String[] 排序,因为 String 实现了 Comparable,其中 String.compareTo 方法是按字典顺序比较字符串。

如果我们想自己定义比较方法,比如按照长度来比较。那么就要自己实现 compareTo 方法,但是 String 类不应该由我们来修改,因此不能直接改 String.compareTo 方法。此时 Arrays.sort 方法还有第二个版本:以数组和比较器为参数,这里比较器是实现了 Comparator 接口的类的实例。

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

按照长度比较字符串的比较器实现如下

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

完成具体的对比:

1
2
3
4
5
6
// compare 不是静态方法,因此还是需要一个实例
Comparator<String> comp = new LengthComparator();

if(comp.compare(words[i], words[j])) {
...
}

完成排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package sort;

import java.util.Arrays;
import java.util.Comparator;

public class SortTest {
public static void main(String[] args) {
String[] words = {"Peter", "abc", "Paul", "Mary"};
show(words);
Arrays.sort(words, new LengthComparator());
show(words);
}

private static void show(String[] words) {
for(String word: words) {
System.out.println(word);
}
System.out.println("----");
}
}

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

对象克隆: Cloneable 接口

Cloneable 接口指示一个类提供了一个安全的 clone 方法。

copy 和 clone

假设有了一个包含对象引用的变量:

1
Employee original = new Employee("John Public", 50000);

此时如果对该变量建立副本:

1
Employee copy = original;

则副本 copy 和原变量 original 都是同一个对象的引用,如下图:

如果希望 copy 是一个新对象,它的初始状态与 original 相同,但是之后它们各自会有自己不同的状态,这种情况下就可以使用 clone 方法。

1
Employee copy = original.clone();

深拷贝和浅拷贝

Object 在实现 clone 时,由于对实际对象一无所知,只能逐个域地进行拷贝。如果对象中的所有数据域都是基本类型,则拷贝没有任何问题。但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。这种拷贝称为浅拷贝,如下图。

浅拷贝

如果原对象和浅克隆对象共享的子对象是不可变的(例如 String)那么浅拷贝的这种共享就是安全的。

通常子对象是可变的,此时必须重新定义 clone 方法来建立一个深拷贝。

在实践中需要考虑 3 中处理方式:

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

其中第 3 个是默认情况,如果选择第 1, 2 种处理方式,则需要 2 步:

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

实现 Cloneable 接口的例子,深拷贝

  • clone/CloneTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package clone;

public class CloneTest {
public static void main(String[] args) {
try {
Employee original = new Employee("John", 50000);
original.setHireDay(2022, 1, 1);
Employee copy = original.clone();
copy.raiseSalary(10);
copy.setHireDay(2022, 5, 20);
System.out.println("original=" + original);
System.out.println("copy=" + copy);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
  • clone/Employee.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package clone;

import java.util.Date;
import java.util.GregorianCalendar;

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

public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
hireDay = new Date();
}

public Employee clone() throws CloneNotSupportedException {
// call Object.clone()
Employee cloned = (Employee) super.clone();

// clone mutable fields
cloned.hireDay = (Date) hireDay.clone();

return cloned;
}

public void setHireDay(int year, int month, int day) {
Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime();

// Example of instance field mutation
hireDay.setTime(newHireDay.getTime());
}

public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}

public String toString() {
return "Employee[name=" + name + ", salary=" + salary + ", hireDay=" + hireDay + "]";
}
}

Object 的 clone 方法

clone 方法是 Object 的一个 protected 方法,在子类内部是可以调用 Object.clone 方法的,而外部是不能通过 obj.clone 调用继承来的 clone 方法的。

如果 Object.clone 默认的浅拷贝满足要求,则也还是要实现 Cloneable 接口,重新定义 clone 为 public,然后调用 super.clone。

标记接口

Cloneable 接口并没有指定 clone 方法,这个方法是从 Object 类继承的。因此 Cloneable 接口只是作为一个标记,称为标记接口。

标记接口(tagging interface)不含任何方法,它唯一作用是允许在类型查询中使用 instanceof。

1
if(obj instanceof Cloneable)

CloneNotSupportedException 异常

如果在一个对象上调用 clone,但这个对象的类并没有实现 Cloneable 接口,Object 类的 clone 方法就会抛出一个 CloneNotSupportedException。

数组类型的 clone

数组类型有一个 public 的 clone。这个 clone 建立新数组,包含原数组的所有元素的副本。


Share