Java中的接口

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

接口的概念

在Java中,接口不是类,而是对希望符合这个接口的类的一组需求。我们经常听到服务提供商这样说:“如果你的类符合某个特定接口,我就会履行这项服务。”Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下面这个条件:对象所属的类必须实现Comparable接口。

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

  • 将类声明为实现给定的接口。
  • 对接口中的所有方法提供定义。

接口的属性

  1. 不能使用new运算符实例化一个接口。
  2. 接口变量必须引用实现了这个接口的类对象。
  3. 如同使用instanceof检查一个对象是否属于某个特定类一样,也可以使用instanceof检查一个对象是否实现了某个特定的接口。
  4. 允许有多条接口链,从通用性较高的接口扩展到专用性较高的接口。
  5. 接口中不能包含实例字段,但是可以包含常量。
  6. 与接口中的方法都自动被设置为public一样,接口中的字段总是public static final

接口与抽象类

使用抽象类表示通用属性存在一个严重的问题。每个类只能扩展一个类。有些程序设计语言允许一个类有多个超类,这个特性称为多重继承(multiple inheritance)。Java的设计者选择了不支持多重继承,其主要原因是多重继承会让语言变得非常复杂,或者效率会降低。接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。

静态和私有方法

在Java 8中,允许在接口中增加静态方法。通常的做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和使用工具类,如Collection/Collections等。

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

默认方法

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

在有些情况下,默认方法可能会很有用,例如,Iterator接口,用于访问一个数据结构中的元素。这个接口声明了一个remove方法:

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; }
}

默认方法的一个重要用法是“接口演化”(interface evolution)。以Collection接口为例,这个接口作为Java的一部分已经有很多年了。假设很久以前你提供了这样一个类:

public class Bag implements Collection

后来,在Java 8中,又为这个接口增加了一个stream方法。
假设stream方法不是一个默认方法,那么Bag将不能编译,因为它没有实现这个新方法。为接口增加一个非默认方法不能保证“源代码兼容”(source compatible)。
不过,假设不重新编译这个类,而只是使用原先的一个包含这个类的JAR文件。这个类仍能正常加载,尽管没有这个新方法。程序仍然可以正常构造Bag实例,不会有意外发生。不过,如果程序在一个Bag实例上调用stream方法,就会出现一个AbstractMethodError。

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

解决默认方法冲突

如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样方法,会发生什么情况?

Java的规则如下:

  1. 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
  2. 接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须覆盖这个方法来解决冲突。
  3. 一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。在这种情况下,只会考虑超类方法,接口的所有默认方法都会被忽略。这正是“类优先”规则。

“类优先”规则可以确保与Java 7的兼容性。如果为一个接口增加默认方法,这对于有这些默认方法之前能正常工作的代码不会有任何影响。

Comparator接口

当对一个对象数组进行排序,前提是这些对象是实现了Comparable接口的类的实例。例如,可以对一个字符串数组排序,因为String类实现了Comparable<String>,而且String.compareTo方法可以按字典顺序比较字符串。

现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。肯定不能让String类用两种不同的方式实现compareTo方法——更何况,String类也不应由我们来修改。

要处理这种情况,Arrays.sort方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了Comparator接口的类的实例。

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

要按长度比较字符串,可以如下定义一个实现Comparator<String>的类:

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) ...

将这个调用与words[i].compareTo(words[j])进行比较。这个compare方法要在比较器对象上调用,而不是在字符串本身上调用。

要对一个数组排序,需要为Arrays.sort方法传入一个LengthComparator对象:

String[] friends = {"peter","paul","mary"};
Arrays.sort(friends, new LengthComparator());

对象克隆

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

要了解克隆的具体含义,先来回忆为一个包含对象引用的变量建立副本时会发生什么。原变量和副变量都是同一个对象的引用。这说明,任何一个变量改变都会影响另一个变量。

var original = new Employee("john",50000);
Employee copy = original;
copy.raiseSalary(10);

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

Employee copy = original.clone();
copy.raiseSalary(10);

不过并没有这么简单。clone方法是Object的一个protected方法,这说明你的代码不能直接调用这个方法。只有Employee类可以克隆Employee对象。这个限制是有原因的。想想看Object类如何实现clone。它对于这个对象一无所知,所以只能逐个字段地进行拷贝。如果对象中的所有数据字段都是数值或其他基本类型,拷贝这些字段没有任何问题。但是如果对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用,这样一来,原对象和克隆对象仍然会共享一些信息。如图所示。
在这里插入图片描述
可以看到,默认的克隆操作是“浅拷贝 ”,并没有克隆对象中引用的其他对象。
浅拷贝会有什么影响吗?这要看具体情况。如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如果子对象属于一个不可变的类,如String,就是这种情况。或者在对象生命周期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的。

不过,通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝 (deep copy),同时克隆所有子对象。

对于每一个类,需要确定:

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

实际上第3个选项是默认选项。如果选择第1项或第2项,类必须:

  1. 实现Cloneable接口;
  2. 重新定义clone方法,并指定public访问修饰符。

在这里,Cloneable接口的出现与接口的正常使用并没有关系。具体来说,它没有指定clone方法,这个方法是从Object类继承的。这个接口只是作为一个标记,指示类设计者了解克隆过程。对象对于克隆很“偏执”,如果一个对象请求克隆,但是没有实现这个接口,就会生成一个检查型异常。

Cloneable接口是Java提供的少数标记接口(tagging interface)之一。Comparable等接口的通常用途是确保一个类实现一个或一组特定的方法。标记接口不包含任何方法;它唯一的作用就是允许在类型查询中使用instanceof:
if (obj instanceof Cloneable) …
建议你自己的程序中不要使用标记接口。

即使clone的默认(浅拷贝)实现能够满足要求,还是需要实现Cloneable接口,将clone重新定义为public,再调用super.clone()。

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

与Object.clone提供的浅拷贝相比,前面看到的clone方法并没有为它增加任何功能。这里只是让这个方法是公共的。要建立深拷贝,还需要做更多工作,克隆对象中可变的实例字段。

class Employee implements Cloneable {
	public Employee clone() throws CloneNotSupportedException {
		// call Object.clone()
		Employee cloned = (Employee) super.clone();
		// clone mutalbe fields
		cloned.hireDay = (Date) hireDay.clone();
		return cloned;
	}
}

如果在一个对象上调用clone,但这个对象的类并没有实现Cloneable接口,Object类的clone方法就会抛出一个CloneNotSupportedException。当然,Employee和Date类实现了Cloneable接口,所以不会抛出这个异常。不过,编译器并不了解这一点,因此,我们声明了这个异常。

捕获这个异常是不是更好一些?
public Employee clone() {
try {
Employee cloned = (Employee) super.clone();
} catch(CloneNotSupportedException e) {
return null;
}
}
这适用于final类。否则,最好还是保留throws说明符。这样就允许子类在不支持克隆时选择抛出一个CloneNotSupportedException。

public class CloneTest {

    public static void main(String[] args) throws CloneNotSupportedException {
        Employee original = new Employee("john", 50000);
        original.setHireDay(2000,1,1);
        Employee copy = original.clone();
        copy.raiseSalary(10);
        copy.setHireDay(2002,12,31);
        System.out.println("original=" + original);
        System.out.println("copy=" + copy);
    }

}

package section5_5;

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;
        this.hireDay = new Date();
    }

    public Employee clone() throws CloneNotSupportedException {
        Employee cloned = (Employee) super.clone();
        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();
        hireDay.setTime(newHireDay.getTime());
    }

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

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", salary=" + salary +
                ", hireDay=" + hireDay +
                '}';
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值