目录

本章涵盖了一些注解、接口等特性。

注解

注解(Annotation)是一种元数据形式,提供有关不属于程序本身的程序的数据。 注解对他们注解的代码的操作没有直接的影响。

注解有许多用途,其中包括:

  • 编译器信息 - 编译器可以使用注解来检测错误或取消警告。
  • 编译时和部署时处理 - 软件工具可以处理注解信息以生成代码,XML文件等等。
  • 运行时处理 - 有些注解可在运行时检查。

注解基础

注解的格式

以最简单的形式,注解如下所示:

@Entity

符号字符at(@)向编译器指出后面的是一个注解。 在以下示例中,注解的名称是Override:

@Override
void mySuperMethod() { ... }

注解可以包含被命名或未命名的元素,并且这些元素有值:

@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass() { ... }

@SuppressWarnings(value = "unchecked")
void myMethod() { ... }

如果只有一个名为value的元素,则名称可以省略,如下所示:

@SuppressWarnings("unchecked")
void myMethod() { ... }

如果注解没有元素,那么括号可以省略,如前面的@Override示例所示。

也可以在同一个声明中使用多个注解:

@Author(name = "Jane Doe")
@EBook
class MyClass { ... }

如果注解具有相同的类型,则称为重复注解(repeating annotation):

@Author(name = "Jane Doe")
@Author(name = "John Smith")
class MyClass { ... }

在Java SE 8发行版中支持重复注解。

注解类型可以是Java SE API的java.lang或java.lang.annotation包中定义的类型之一。 在前面的示例中,Override和SuppressWarnings是预定义的Java注解。 也可以定义自己的注解类型。 上例中的 Author 和 Ebook 注解是自定义注解类型。

何处可以使用注解

注解可以应用于声明:类,字段,方法和其他程序元素的声明。 在声明中使用时,每个注解经常按照惯例出现在自己的行上。

从Java SE 8发行版开始,注解也可以应用于类型的使用。 这里有些例子:

  • 类实例创建表达式:
new @Interned MyObject();
  • 类型强制转换(Type cast):
myString = (@NonNull String) str;
  • implements 从句(clause):
class UnmodifiableList<T> implements
        @Readonly List<@Readonly T> { ... }
  • 抛出的异常声明:
void monitorTemperature() throws
        @Critical TemperatureException { ... }

         这种形式的注解被称为类型注解(type annotation)。

声明一个注解类型

代码中许多注解(annotations)代替了注释(comments)。

假设一个软件组织传统上每个类的主体都带有提供重要信息的注释:

public class Generation3List extends Generation2List {

   // Author: John Doe
   // Date: 3/17/2002
   // Current revision: 6
   // Last modified: 4/12/2004
   // By: Jane Doe
   // Reviewers: Alice, Bill, Cindy

   // class code goes here

}

要用注释添加相同的元数据,您必须先定义注解类型。 这样做的语法是:

@interface ClassPreamble {
   String author();
   String date();
   int currentRevision() default 1;
   String lastModified() default "N/A";
   String lastModifiedBy() default "N/A";
   // Note use of array
   String[] reviewers();
}

注解类型定义看起来类似于一个接口定义,其中关键字interface的前面是at符号(@)(@ = AT,即注解类型)。 注解类型是一种接口形式。

前面的注解定义的主体包含注解类型元素声明,这看起来很像方法。 请注意,他们可以定义可选的默认值。

在定义了注解类型之后,可以使用该类型的注解,并填充值,如下所示:

@ClassPreamble (
   author = "John Doe",
   date = "3/17/2002",
   currentRevision = 6,
   lastModified = "4/12/2004",
   lastModifiedBy = "Jane Doe",
   // Note array notation
   reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation3List extends Generation2List {

// class code goes here

}

注:要使@ClassPreamble中的信息出现在Javadoc生成的文档中,您必须使用@Documented注解批注注解@ClassPreamble定义:

// import this to use @Documented
import java.lang.annotation.*;

@Documented
@interface ClassPreamble {

   // Annotation element definitions
   
}

预定义的注解类型

Java SE API中预定义了一组注解类型。 Java编译器使用了一些注解类型,有些也适用于其他注解。

Java语言使用的注释类型

在java.lang中定义的预定义注解类型是@Deprecated,@Override和@SuppressWarnings。

@Deprecated

@Deprecated注解指示标记的元素已弃用,不应再使用。 只要程序使用具有@Deprecated注解的方法,类或字段,编译器就会生成警告。 当一个元素被弃用时,也应该使用Javadoc 的 @deprecated标签进行记录(documented),如以下示例所示。 在Javadoc注释(comments)和注解中使用at号(@)并不是巧合:它们在概念上是相关的。 另请注意,Javadoc标签以小写字母d开头,注解以大写字母D开始。

// Javadoc comment follows
    /**
     * @deprecated
     * explanation of why it was deprecated
     */
    @Deprecated
    static void deprecatedMethod() { }
}

@Override

@Override注解通知编译器该元素是要覆盖在超类中声明的元素。

// mark method as a superclass method
// that has been overridden
@Override 
int overriddenMethod() { }

虽然重写方法时不需要使用此注解,但有助于防止错误。 如果使用@Override标记的方法无法正确覆盖某个超类中的某个方法,则编译器会生成一个错误。

@SuppressWarnings

@SuppressWarnings注解告诉编译器禁止它会产生的特定警告。 在以下示例中,使用废弃的方法,编译器通常会生成警告。 但是,在这种情况下,注解会导致警告被抑制。

// use a deprecated method and tell 
// compiler not to generate a warning
@SuppressWarnings("deprecation")
void useDeprecatedMethod() {
    // deprecation warning
    // - suppressed
    objectOne.deprecatedMethod();
}

每个编译器警告属于一个类别。 Java语言规范列出了两个类别:弃用(deprecation)和未经检查(unchecked)。 在实现泛型出现之前编写的遗留代码接口时,可能会发生未经检查的警告。 要禁止多个类别的警告,请使用以下语法:

@SuppressWarnings({"unchecked", "deprecation"})

@SafeVarargs

@SafeVarargs注解应用于方法或构造函数时,断言代码不会对其可变参数执行潜在的不安全操作。 当使用这种注解类型时,与可变参数使用有关的未经检查的警告被抑制。

@FunctionalInterface

在Java SE 8中引入的@FunctionalInterface注解表明,类型声明旨在成为Java语言规范定义的功能接口。

适用于其他注解的注解

适用于其他注解的注解称为元注解(meta-annotations)。 在java.lang.annotation中定义了几个元注解类型。

@Retention

@Retention注解指定标记的注解的存储方式:

  • RetentionPolicy.SOURCE - 标记的注解仅保留在源代码级别,并被编译器忽略。
  • RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留,但被Java虚拟机(JVM)忽略。
  • RetentionPolicy.RUNTIME - 标记的注解由JVM保留,以便运行时环境可以使用它。

@Documented

@Documented注解表明,无论何时使用指定的注解,这些元素都应该使用Javadoc工具进行记录(documented)。 (默认情况下,Javadoc中不包含注解。)

@Target

@Target注解标记另一个注解来限制可以应用注解的Java元素。 目标注解指定以下元素类型之一作为其值:

  • ElementType.ANNOTATION_TYPE可以应用于注解类型。
  • ElementType.CONSTRUCTOR可以应用于构造函数。
  • ElementType.FIELD可以应用于字段或属性。
  • ElementType.LOCAL_VARIABLE可以应用于局部变量。
  • ElementType.METHOD可以应用于方法级别的注解。
  • ElementType.PACKAGE可以应用于包(package)声明。
  • ElementType.PARAMETER可以应用于方法的参数。
  • ElementType.TYPE可以应用于任何类的元素。

@Inherited

@Inherited注解表示注解类型可以从超类继承。 (默认情况下不是这样。)当用户查询注解类型并且类没有这种类型的注解时,查询该类的超类的注解类型。 这个注解只适用于类声明。

@Repeatable

在Java SE 8中引入的@Repeatable注解表明,标记的注解可以多次应用于相同的声明或类型用途。

类型注解和 Pluggable 类型系统

在Java SE 8发行版之前,注解只能应用于声明。从Java SE 8发行版开始,注解也可以应用于任何类型的用途。这意味着可以在使用类型的任何地方使用注解。使用类型的几个示例是类实例创建表达式(new),强制转换(casts),implenments子句和throws子句。这种形式的注解称为类型注解(type annotations)。

创建类型注解是为了支持Java程序的改进分析,以确保更强大的类型检查。 Java SE 8发行版没有提供类型检查框架,但它允许您编写(或下载)一个类型检查框架,该框架被实现为一个或多个可与Java编译器结合使用的可插入(pluggable)模块。

例如,您要确保程序中的特定变量永远不会分配给null;你想避免触发NullPointerException。你可以写一个自定义的插件(plug-in)来检查这个。然后,您将修改您的代码来注解该特定的变量,指示它永远不会分配给null。变量声明可能如下所示:

@NonNull String str;

当您编译代码(包括命令行中的NonNull模块)时,如果编译器检测到潜在的问题,则会输出警告,允许您修改代码以避免错误。在更正代码以删除所有警告之后,程序运行时将不会发生此特定错误。

您可以使用多个类型检查模块,其中每个模块检查不同类型的错误。通过这种方式,您可以在Java类型系统之上构建,在您想要的时间和地点添加特定的检查。

通过明智地使用类型注解和可插入(pluggable)类型检查器,您可以编写更强大,更不易出错的代码。

在许多情况下,您不必编写自己的类型检查模块。有第三方为你做了工作。例如,您可能想要利用华盛顿大学创建的Checker框架。这个框架包括一个NonNull模块,一个正则表达式模块和一个互斥锁模块。

重复注解

在某些情况下,您希望将相同的注解应用于声明或类型用途。 从Java SE 8发行版开始,重复注解(repeating annotation)使您可以执行此操作。

例如,您正在编写代码以使用计时器服务,该计时器服务使您能够在给定时间或某个时间表上运行方法,类似于UNIX 的 cron服务。 现在你想设置一个计时器在每个月的最后一天和每个星期五下午11点来运行一个方法 doPeriodicCleanup。 要设置计时器运行,请创建@Schedule注解,并将其应用两次到doPeriodicCleanup方法。 第一个用途指定了月份的最后一天,第二个用于指定星期五的下午11点,如以下代码示例所示:

@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }

前面的示例将注解应用于方法。 您可以在任何使用标准注解的地方重复注解。 例如,您有一个处理未授权访问异常的类。 您可以使用一个@Alert注解为管理员和另一个管理员注解该类:

@Alert(role="Manager")
@Alert(role="Administrator")
public class UnauthorizedAccessException extends SecurityException { ... }

出于兼容性原因,重复注释存储在由Java编译器自动生成的容器注释中。 为了让编译器做到这一点,你的代码需要两个声明。

第1步:声明一个可重复的注解类型

注解类型必须用@Repeatable元注解(meta-annotation)标记。 以下示例定义了一个自定义@Schedule可重复注解类型:

import java.lang.annotation.Repeatable;

@Repeatable(Schedules.class)
public @interface Schedule {
  String dayOfMonth() default "first";
  String dayOfWeek() default "Mon";
  int hour() default 12;
}

圆括号中的@Repeatable元注解的值是Java编译器为存储重复注解而生成的容器注解的类型。 在这个例子中,包含的注释注解是Schedules,所以重复的@Schedule注解存储在@Schedules注解中。

Applying the same annotation to a declaration without first declaring it to be repeatable results in a compile-time error.

步骤2:声明包含的注解类型

包含的注解类型必须具有数组类型的值元素。 数组类型的组件类型必须是可重复的注解类型。 包含注解类型的Schedules声明如下:

public @interface Schedules {
    Schedule[] value();
}

检索注解

Reflection API中有几种方法可用于检索注解。返回单个注解的方法(如AnnotatedElement.getAnnotation(Class) )的行为不会改变,因为它们只会在请求类型的注解存在时返回单个注解。如果所请求的类型有多个注解,则可以通过首先获取它们的容器注解来获取它们。通过这种方式,遗留代码继续工作。在Java SE 8中引入了其他方法,它们扫描容器注解以一次返回多个注解,如AnnotatedElement.getAnnotationsByType(Class)。

设计注意事项

设计注解类型时,必须考虑该类型注解的基数。现在可以使用注解零次、一次,或者如果注解的类型被标记为@Repeatable,则可以多次使用。也可以通过使用@Target元注解来限制注解类型的使用位置。例如,您可以创建只能在方法和字段上使用的可重复注解类型。仔细设计注解类型非常重要,以确保使用注解的程序员发现它尽可能的灵活和强大。

接口和继承

接口和继承(Interface and Inheritance)描述接口 - 它们是什么,为什么要写一个接口,以及如何编写一个接口。 本节还介绍了您可以从另一个派生一个类的方法。 也就是说,一个子类如何从超类继承领域和方法。 您将了解到,所有类都派生自 Object 类,以及如何修改子类从超类继承的方法。

接口

Java中的接口

在Java编程语言中,接口是一个引用类型,类似于一个类,它只能包含常量,方法签名,缺省方法,静态方法和嵌套类型。 方法体仅存在于默认方法和静态方法中。 接口不能被实例化 - 它们只能由类实现或由其他接口扩展。

定义一个接口类似于创建一个新的类:

public interface OperateCar {

   // constant declarations, if any

   // method signatures
   
   // An enum with values RIGHT, LEFT
   int turn(Direction direction,
            double radius,
            double startSpeed,
            double endSpeed);
   int changeLanes(Direction direction,
                   double startSpeed,
                   double endSpeed);
   int signalTurn(Direction direction,
                  boolean signalOn);
   int getRadarFront(double distanceToCar,
                     double speedOfCar);
   int getRadarRear(double distanceToCar,
                    double speedOfCar);
         ......
   // more method signatures
}

请注意,方法签名没有大括号,并以分号结尾。

要使用接口,您需要编写一个实现接口的类。 当一个可实例化的类实现一个接口时,它为接口中声明的每个方法提供一个方法体。 例如,

public class OperateBMW760i implements OperateCar {

    // the OperateCar method signatures, with implementation --
    // for example:
    int signalTurn(Direction direction, boolean signalOn) {
       // code to turn BMW's LEFT turn indicator lights on
       // code to turn BMW's LEFT turn indicator lights off
       // code to turn BMW's RIGHT turn indicator lights on
       // code to turn BMW's RIGHT turn indicator lights off
    }

    // other members, as needed -- for example, helper classes not 
    // visible to clients of the interface
}

接口作为API

机器人车的例子显示一个接口被用作工业标准的应用程序编程接口(API)。 API在商业软件产品中也很常见。 通常情况下,一家公司销售一个软件包,其中包含另一家公司想要在自己的软件产品中使用的复杂方法。 一个例子就是出售给制造最终用户图形程序的公司的一套数字图像处理方法。 图像处理公司编写它的类来实现一个接口,并将其公开给客户。 图形公司然后使用界面中定义的签名和返回类型来调用图像处理方法。 虽然图像处理公司的API是公开给客户的,但是API的实现仍然是一个严密的保密措施 - 事实上,它可能会在稍后的日期修改实现,只要继续实现其客户所依赖的原始的接口即可。

定义接口

接口声明由修饰符,关键字 interface,接口名称,父接口(如果有的话)的逗号分隔列表以及接口主体组成。 例如:

public interface GroupedInterface extends Interface1, Interface2, Interface3 {

    // constant declarations
    
    // base of natural logarithms
    double E = 2.718282;
 
    // method signatures
    void doSomething (int i, double x);
    int doSomethingElse(String s);
}

public访问说明符表示该接口可以被任何包中的任何类使用。 如果您没有指定接口是public,那么您的接口只能由与接口相同的包中定义的类访问。

一个接口可以扩展其他接口,就像一个类的子类或扩展另一个类一样。 然而,一个类只能扩展一个其他类,而一个接口可以扩展任意数量的接口。 接口声明包括它所扩展的所有接口的逗号分隔列表。

接口主体

接口主体可以包含抽象方法,默认方法和静态方法。 接口中的抽象方法后面是分号,但没有大括号(抽象方法不包含实现)。 默认的方法是使用default修饰符来定义的,静态的方法使用static关键字来定义。 接口中的所有抽象、default和static方法都是隐式 public,所以可以省略 public 修饰符。

另外,一个接口可以包含常量声明。 在接口中定义的所有常量值都是隐式的public,static和final。 再一次,你可以省略这些修饰符。

实现接口

要声明实现接口的类,可以在类声明中包含一个implements子句。 您的类可以实现多个接口,因此implements关键字后跟由类实现的接口的逗号分隔列表。 按惯例,implements子句跟随extends子句,如果有的话。

使用接口作为类型

当你定义一个新的接口时,你正在定义一个新的引用数据类型。 您可以在任何可以使用任何其他数据类型名称的地方使用接口名称。 如果您定义了一个类型为接口的引用变量,则您分配给它的任何对象都必须是实现该接口的类的实例。

不断发展的(evolving)接口

考虑一下你开发的称为DoIt的接口:

public interface DoIt {
   void doSomething(int i, double x);
   int doSomethingElse(String s);
}

假设在稍后的时间,你想添加第三个方法到DoIt,现在接口变成:

public interface DoIt {

   void doSomething(int i, double x);
   int doSomethingElse(String s);
   boolean didItWork(int i, double x, String s);
   
}

如果你做了这个改变,那么实现旧的DoIt接口的所有类都将因为不再实现旧接口而中断。

尝试预测您的接口的所有用途,并从一开始就完全指定它。 如果你想添加额外的方法到一个接口,你有几个选项。 您可以创建一个扩展DoIt的DoItPlus接口:

public interface DoItPlus extends DoIt {

   boolean didItWork(int i, double x, String s);
   
}

现在,您的代码的用户可以选择继续使用旧接口或升级到新接口。

或者,您可以将您的新方法定义为默认方法。 以下示例定义了一个名为didItWork的默认方法:

public interface DoIt {

   void doSomething(int i, double x);
   int doSomethingElse(String s);
   default boolean didItWork(int i, double x, String s) {
       // Method body 
   }
   
}

请注意,您必须提供默认方法的实施。 你也可以为现有的接口定义新的静态方法。 拥有使用新的默认或静态方法增强接口的类的用户不必修改或重新编译它们以适应追加的方法。

默认方法

使用默认方法,可以将新功能添加到库的接口,并确保与为这些接口的旧版本编写的代码的二进制兼容性。

扩展包含默认方法的接口

扩展包含默认方法的接口时,可以执行以下操作:

  • 根本不提及默认的方法,这可以让你的扩展接口继承默认的方法。
  • 重新声明默认的方法,这使得它是抽象的。
  • 重新定义覆盖它的默认方法。

静态方法

除了默认方法之外,您还可以在接口中定义静态方法。 (静态方法是一个与定义它的类相关联的方法,而不是与任何对象相关联的。类的每个实例共享其静态方法)这使得您可以更轻松地在库中组织helper方法; 您可以在同一个接口中保留特定于接口的静态方法,而不是在单独的类中。

public interface TimeClient {
    // ...
    static public ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    default public ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }    
}

与类中的静态方法类似,您可以指定接口中的方法定义是在方法签名开始处使用static关键字的静态方法。 接口中的所有方法声明(包括静态方法)都是隐式公public,所以可以省略public修饰符。

将默认方法集成到现有的库中

默认方法使您能够为现有接口添加新功能,并确保与为这些接口的旧版本编写的代码的二进制兼容性。 特别是,使用默认方法可以将接受lambda表达式作为参数的方法添加为现有接口。

继承

在Java语言中,类可以从其他类派生,从而继承这些类的字段和方法。

定义:从另一个类派生的类称为子类 - subclass(派生类 - a derived class,扩展类 - extended class或子类 - child class)。 派生子类的类称为超类 - superclass(也是基类 - a base class或父类 - a parent class)。

除了没有超类的Object之外,每个类都有且只有一个直接的超类(单继承 - single inheritance)。 在没有任何其他显式超类的情况下,每个类都隐式地是Object的一个子类。

类可以从派生自类的类派生的类派生,等等,最终派生自最上面的类Object。 这样的一个类被称为从继承链中的所有类继承到Object。

子类从其父类继承所有成员(字段,方法和嵌套类)。 构造函数不是成员,所以它们不会被子类继承,但是可以从子类中调用超类的构造函数。

Java平台类层次结构

在java.lang包中定义的Object类定义并实现了所有类共同的行为。 在Java平台中,许多类直接从Object派生,其他类从某些类派生,等等,形成类的层次结构。

在层次结构的顶部,Object是所有类中最通用的。 靠近层次结构底部的类提供更专业化的行为。

你可以在子类中做什么

子类继承父类的所有public和protected成员,不管子类在什么包内。如果子类与其父类位于同一个包中,它也继承父类的package-private成员。您可以按原样使用继承的成员,替换它们,隐藏它们,或者用新成员补充它们:

  • 继承的字段可以直接使用,就像其他字段一样。
  • 您可以在子类中声明一个与超类中的字段相同的字段,从而隐藏它(不推荐)。
  • 您可以在子类中声明不在超类中的新字段。
  • 继承的方法可以直接使用。
  • 您可以在与超类中签名相同的子类中编写一个新的实例方法,从而覆盖它。
  • 你可以在子类中写一个新的静态方法,它与超类中的签名相同,从而隐藏它。
  • 您可以在子类中声明不在超类中的新方法。
  • 你可以编写一个子类的构造函数来调用超类的构造函数,可以隐式地或使用关键字super。

子类中的私有成员

子类不会继承其父类的私有成员。 但是,如果超类具有访问其私有字段的公共或受保护的方法,则这些也可以由子类使用。

嵌套类可以访问其封闭类的所有私有成员,包括字段和方法。 因此,由子类继承的公共或受保护的嵌套类可间接访问超类的所有私有成员。

Casting Objects

我们已经看到,一个对象是它实例化的类的数据类型。例如:

public MountainBike myBike = new MountainBike();

此时,myBike 是 MountainBike 类型。

MountainBike 是从 Bicycle 和 Object 的后裔。 因此,MountainBike 是 Bicycle,同时也是 Object,可以在需要 Bicycle 或 Object 的地方使用。

反过来并不一定是正确的:一辆 Bicycle 可能是 MountainBike,但不一定。 同样,Object 可能是 Bicycle 或 MountainBike,但不一定。

在继承和实现所允许的对象之中,casting 展示了使用一种类型的对象代替另一种类型的对象。例如:

Object obj = new MountainBike();

obj 既是 Object 也是 MountainBike 那么obj既是一个 Object 也是一个 MountainBike(直到obj被赋予另一个不是MountainBike的对象)。 这被称为 implicit casting。

如果,另一方面,我们写

MountainBike myBike = obj;

这个转换(cast)插入一个运行时检查,obj 被分配一个 MountainBike,以便编译器可以安全地假定 obj 是一个 MountainBike。 如果 obj 在运行时不是 MountainBike,则会抛出异常。

注意:您可以使用 instanceof 运算符对特定对象的类型进行逻辑测试。 这可以避免由于不正确的转换(improper cast)造成的运行时错误。 例如:

if (obj instanceof MountainBike) {
    MountainBike myBike = (MountainBike)obj;
}

在这里,instanceof 运算符验证 obj 是否指向一个 MountainBike,以便我们可以知道将不会抛出运行时异常。

状态的多重继承,实现和类型

类和接口之间的一个显著的区别是类可以有字段,而接口不可以。另外,你可以实例化一个类来创建一个对象,这是你不能用接口实现的。对象是在类中定义的字段保存其状态。Java编程语言不允许扩展多个类的一个原因是为了避免多个状态继承的(multiple inheritance of state)问题,即从多个类继承字段的能力。例如,假使你可以定义一个继承多个类的新类。当通过实例化该类创建对象时,该对象将继承所有类的超类的字段。如果来自不同超类的方法或构造函数实例化相同的字段呢?哪个方法或构造函数优先?因为接口不包含字段,所以不必担心由多重状态继承导致的问题。

实现的多重继承(Multiple inheritance of implementation)是从多个类继承方法定义的能力。这种类型的多重继承会出现问题,如命名冲突和模糊性。当支持这种类型的多重继承的编程语言的编译器遇到包含具有相同名称的方法的超类时,它们有时不能确定访问或调用哪个成员或方法。另外,程序员可以在不知情的情况下通过向超类添加新方法来引入名称冲突。默认方法(Default methods)引入了一种多重继承的实现形式。 一个类可以实现多个接口,它可以包含具有相同名称的默认方法。 Java编译器提供了一些规则来确定特定类所使用的默认方法。

Java编程语言支持类型的多重继承,这是一个类实现多个接口的能力。一个对象可以有多种类型:它自己的类的类型和类所实现的所有接口的类型。意味着如果一个变量被声明为一个接口的类型,那么它的值可以引用任何实现了该接口的类实例化的对象。

与实现的多重继承一样,一个类可以继承它所扩展的接口中定义的方法(默认或静态)的不同实现。在这种情况下,编译器或用户必须决定使用哪一个。

重载和隐藏方法

实例方法

与超类中的实例方法(instance method)具有相同签名(名称,加上其参数的数量和类型)和返回类型的子类中的实例方法将重载(override)超类的方法。

子类重载方法的能力允许类从行为足够“接近(close enough)”的超类继承,然后根据需要修改行为。 重载方法具有相同的名称,参数的数量和类型,以及被重载方法的返回类型。 重载方法也可以返回被重载方法返回的类型的子类型。 这个子类型被称为协变返回类型(a covariant return type)。

当重载一个方法时,你可能想要使用 @Override 注解来指示编译器你打算重载超类中的一个方法。 如果由于某种原因,编译器检测到该方法在其中一个超类中不存在,则会产生一个错误。

静态方法

如果一个子类定义了一个与超类中的静态方法(static method)具有相同签名的静态方法,那么子类中的方法隐藏(hide)超类中的方法。

隐藏静态方法和重载实例方法之间的区别具有重要意义:

  • 被调用的重载实例方法的版本是子类中的版本。
  • 被调用的隐藏静态方法的版本取决于它是从超类还是子类调用。

考虑一个包含两个类的例子。 首先是Animal,它包含一个实例方法和一个静态方法:

public class Animal {
    public static void testClassMethod() {
        System.out.println("The static method in Animal");
    }
    public void testInstanceMethod() {
        System.out.println("The instance method in Animal");
    }
}

第二类是Animal的一个子类,叫做Cat:

public class Cat extends Animal {
    public static void testClassMethod() {
        System.out.println("The static method in Cat");
    }
    public void testInstanceMethod() {
        System.out.println("The instance method in Cat");
    }

    public static void main(String[] args) {
        Cat myCat = new Cat();
        Animal myAnimal = myCat;
        Animal.testClassMethod();
        myAnimal.testInstanceMethod();
    }
}

Cat类覆盖Animal中的实例方法,并隐藏Animal中的静态方法。 此类中的main方法创建一个Cat实例,并用类调用testClassMethod()以及用实例调用testInstanceMethod()。

这个程序的输出如下:

The static method in Animal
The instance method in Cat

如所承诺的,被调用的隐藏静态方法的版本是超类中的一个,被调用的重载实例方法的版本是子类中的版本。

接口方法

接口中的缺省方法(Default methods)和抽象方法(abstract methods)像实例方法(instance methods)一样被继承。但是,当一个类或接口的超类型(supertypes)提供了具有相同签名的多个缺省方法时,Java编译器遵循继承规则来解决命名冲突。 这些规则是由以下两个原则驱动的:

  • 实例方法优于接口默认方法。

考虑以下类和接口:

public class Horse {
    public String identifyMyself() {
        return "I am a horse.";
    }
}
public interface Flyer {
    default public String identifyMyself() {
        return "I am able to fly.";
    }
}
public interface Mythical {
    default public String identifyMyself() {
        return "I am a mythical creature.";
    }
}
public class Pegasus extends Horse implements Flyer, Mythical {
    public static void main(String... args) {
        Pegasus myApp = new Pegasus();
        System.out.println(myApp.identifyMyself());
    }
}

方法 Pegasus.identifyMyself 返回字符串 I am a horse.

  • 已被其他候选者覆盖的方法将被忽略。当超类型共享一个共同的祖先时,就会出现这种情况。
public interface Animal {
    default public String identifyMyself() {
        return "I am an animal.";
    }
}
public interface EggLayer extends Animal {
    default public String identifyMyself() {
        return "I am able to lay eggs.";
    }
}
public interface FireBreather extends Animal { }
public class Dragon implements EggLayer, FireBreather {
    public static void main (String... args) {
        Dragon myApp = new Dragon();
        System.out.println(myApp.identifyMyself());
    }
}

方法 Dragon.identifyMyself 返回字符串 I am able to lay eggs.

如果两个或多个独立定义的默认方法冲突,或者缺省方法与抽象方法冲突,则Java编译器会产生编译器错误。 您必须显式重载超类型方法。

考虑一下现在可以飞行的计算机控制汽车的例子。 您有两个接口(OperateCar和FlyCar),它们为同一方法(startEngine)提供默认实现:

public interface OperateCar {
    // ...
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}
public interface FlyCar {
    // ...
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}

实现OperateCar和FlyCar的类必须重载startEngine方法。你可以用super关键字调用任何默认的实现。

public class FlyingCar implements OperateCar, FlyCar {
    // ...
    public int startEngine(EncryptedKey key) {
        FlyCar.super.startEngine(key);
        OperateCar.super.startEngine(key);
    }
}

super之前的名称(在这个例子中,FlyCar或OperateCar)必须引用一个直接的超接口(a direct superinterface),它定义或继承了被调用的方法的默认值。 这种形式的方法调用并不局限于使用相同的签名来区分包含默认方法的多个实现的接口。 您可以使用super关键字来调用类和接口中的默认方法。

类的继承实例方法可以覆盖抽象接口方法。 考虑以下接口和类:

public interface Mammal {
    String identifyMyself();
}
public class Horse {
    public String identifyMyself() {
        return "I am a horse.";
    }
}
public class Mustang extends Horse implements Mammal {
    public static void main(String... args) {
        Mustang myApp = new Mustang();
        System.out.println(myApp.identifyMyself());
    }
}

方法Mustang.identifyMyself返回字符串 I am a horse。Mustang类继承了Horse类中的identifyMyself方法,该类覆盖了Mammal接口中同名的抽象方法。

注意:接口中的静态方法永远不会被继承。

修饰符

重载方法的访问说明符可以允许比重载的方法更多,但不是更少的访问权限。例如,超类中的受保护实例方法在子类中可以是public,但不能是private。

如果您尝试将超类中的实例方法更改为子类中的静态方法,则会收到编译时错误,反之亦然。

小结

下表总结了在超类中定义具有相同签名的方法时发生的情况。

Defining a Method with the Same Signature as a Superclass’s Method

Superclass Instance Method Superclass Static Method
Subclass Instance Method Overrides Generates a compile-time error
Subclass Static Method Generates a compile-time error Hides

注意:在一个子类中,可以重载从超类继承的方法。这种重载方法既不隐藏也不重写超类实例方法 - 它们是子类独有的新方法。

多态性

字典中的多态性(polymorphism)定义是指生物学中的一个原理,其中生物或物种可以有许多不同的形式或阶段。 这个原理也可以应用于面向对象的编程和像Java语言这样的语言。 类的子类可以定义自己的唯一行为,但是可以共享父类的一些相同的功能。

多态性可以通过对Bicycle类稍作修改来演示。 例如,可以将printDescription方法添加到显示当前存储在实例中的所有数据的类中。

public void printDescription(){
    System.out.println("\nBike is " + "in gear " + this.gear
        + " with a cadence of " + this.cadence +
        " and travelling at a speed of " + this.speed + ". ");
}

为了演示Java语言中的多态特性,使用MountainBike和RoadBike类来扩展Bicycle类。 对于MountainBike,添加一个suspension字段,这是一个String值,指示自行车是否有前减震器Front或者,自行车有一个前后减震器Dual。

这里是更新的类:

public class MountainBike extends Bicycle {
    private String suspension;

    public MountainBike(
               int startCadence,
               int startSpeed,
               int startGear,
               String suspensionType){
        super(startCadence,
              startSpeed,
              startGear);
        this.setSuspension(suspensionType);
    }

    public String getSuspension(){
      return this.suspension;
    }

    public void setSuspension(String suspensionType) {
        this.suspension = suspensionType;
    }

    public void printDescription() {
        super.printDescription();
        System.out.println("The " + "MountainBike has a" +
            getSuspension() + " suspension.");
    }
} 

请注意重载的printDescription方法。 除了之前提供的信息之外,有关suspension的其他数据也包含在输出中。

接下来,创建RoadBike类。 由于公路赛车或竞速赛车有细的轮胎,因此添加一个属性来跟踪轮胎的宽度。 这是RoadBike类:

public class RoadBike extends Bicycle{
    // In millimeters (mm)
    private int tireWidth;

    public RoadBike(int startCadence,
                    int startSpeed,
                    int startGear,
                    int newTireWidth){
        super(startCadence,
              startSpeed,
              startGear);
        this.setTireWidth(newTireWidth);
    }

    public int getTireWidth(){
      return this.tireWidth;
    }

    public void setTireWidth(int newTireWidth){
        this.tireWidth = newTireWidth;
    }

    public void printDescription(){
        super.printDescription();
        System.out.println("The RoadBike" + " has " + getTireWidth() +
            " MM tires.");
    }
}

请注意,printDescription方法再次被重载。 这次显示有关轮胎宽度的信息。

总而言之,有三类:Bicycle,MountainBike和RoadBike。 这两个子类重载printDescription方法并打印唯一信息。

这是一个创建三个Bicycle变量的测试程序。 每个变量被分配到三个bicycle类之一。 然后打印每个变量。

public class TestBikes {
  public static void main(String[] args){
    Bicycle bike01, bike02, bike03;

    bike01 = new Bicycle(20, 10, 1);
    bike02 = new MountainBike(20, 10, 5, "Dual");
    bike03 = new RoadBike(40, 20, 8, 23);

    bike01.printDescription();
    bike02.printDescription();
    bike03.printDescription();
  }
}

以下是测试程序的输出:

Bike is in gear 1 with a cadence of 20 and travelling at a speed of 10. 

Bike is in gear 5 with a cadence of 20 and travelling at a speed of 10. 
The MountainBike has a Dual suspension.

Bike is in gear 8 with a cadence of 40 and travelling at a speed of 20. 
The RoadBike has 23 MM tires.

Java虚拟机(JVM)为每个变量中引用的对象调用适当的方法。 它不调用变量类型定义的方法。 这种行为被称为虚拟方法调用(virtual method invocation),并演示了Java语言中重要的多态特性的一个方面。

隐藏的字段(Hiding Fields)

在类中,与超类中的字段具有相同名称的字段隐藏超类的字段,即使它们的类型不同。 在子类中,超类中的字段不能被其简单名称引用。 相反,该字段必须通过super来访问。 一般来说,我们不建议隐藏字段,因为它使代码难以阅读。

使用关键字super

访问超类成员

如果你的方法重载了它的一个超类的方法,你可以通过使用关键字super来调用被重载的方法。 你也可以使用super来引用一个隐藏的字段(虽然不鼓励隐藏字段)。考虑这个类Superclass:

public class Superclass {

    public void printMethod() {
        System.out.println("Printed in Superclass.");
    }
}

这里是一个名为Subclass的子类,它覆盖了printMethod():

public class Subclass extends Superclass {

    // overrides printMethod in Superclass
    public void printMethod() {
        super.printMethod();
        System.out.println("Printed in Subclass");
    }
    public static void main(String[] args) {
        Subclass s = new Subclass();
        s.printMethod();    
    }
}

在Subclass中,简单名称printMethod()是指在Subclass中声明的一个,它重载了Superclass中的。 因此,要引用从Superclass继承的printMethod(),Subclass必须使用限定名称,如图所示使用super。 编译和执行子类打印以下内容:

Printed in Superclass.
Printed in Subclass

子类构造函数

以下示例说明如何使用super关键字来调用超类的构造函数。 回想一下icycle的例子,MountainBike是自行车的一个子类。 这里是调用超类构造函数的MountainBike(子类)构造函数,然后添加它自己的初始化代码:

public MountainBike(int startHeight, 
                    int startCadence,
                    int startSpeed,
                    int startGear) {
    super(startCadence, startSpeed, startGear);
    seatHeight = startHeight;
} 

调用超类的构造函数必须是子类构造函数的第一行。

调用超类构造函数的语法是

super(); 

super(parameter list);

用super(),超类无参数的构造函数被调用。 用super(parameter list),调用具有匹配参数列表的超类构造函数。

注意:如果构造函数没有显式地调用超类构造函数,那么Java编译器会自动插入对超类的无参构造函数的调用。 如果超类没有没有参数的构造函数,你会得到一个编译时错误。 Object确实有这样的构造函数,所以如果Object是唯一的超类,没有问题。

如果一个子类的构造函数调用它的超类的构造函数,无论是显式的还是隐式的,你可能会认为将有一整个调用的构造函数链,一直回到Object的构造函数。 事实上,情况就是如此。 它被称为构造函数链(constructor chaining),当有长线类下降时(a long line of class descent)你需要注意它。

作为超类的Object

java.lang包中的Object类位于类层次结构树的顶部。 每个类都是Object类的后代,直接或间接的。 每个使用或编写的类都继承Object的实例方法。 您不需要使用这些方法中的任何一种,但是,如果您选择这样做,则可能需要使用特定于您的类的代码来重载它们。 从本节讨论的Object继承的方法是:

  • protected Object clone() throws CloneNotSupportedException
    创建并返回对象的副本。

  • public boolean equals(Object obj)
    指示其他某个对象是否“等于”这一个。

  • protected void finalize() throws Throwable
    当垃圾收集确定没有更多的对象引用时,垃圾回收器调用对象

  • public final Class getClass()
    返回对象的运行时类。

  • public int hashCode()
    返回该对象的哈希码值(hash code value)。

  • public String toString()。 返回对象的字符串表示形式。

Object的notify,notifyAll和wait方法在同步程序中独立运行的线程的活动中扮演着重要角色。 有五种方法:

  • public final void notify()
  • public final void notifyAll()
  • public final void wait()
  • public final void wait(long timeout)
  • public final void wait(long timeout, int nanos)

注意:这些方法中有一些细微的方面,尤其是clone方法。

clone() 方法

如果某个类或其某个超类实现了Cloneable接口,则可以使用clone()方法从现有对象创建一个副本。 要创建一个克隆,你写:

aCloneableObject.clone();

此方法的Object的实现将检查clone()被调用的对象是否实现了Cloneable接口。 如果该对象没有,该方法抛出一个CloneNotSupportedException异常。 目前,您需要知道clone()必须声明为:

protected Object clone() throws CloneNotSupportedException

public Object clone() throws CloneNotSupportedException

如果你要写一个clone()方法来重载Object中的一个。

如果调用clone()的对象实现了Cloneable接口,则Object的clone()方法的实现将创建与原始对象相同类的对象,并将新对象的成员变量初始化为与原始对象具有相同的值对象的相应成员变量。

使类可复制的最简单的方法是将implements Cloneable添加到类的声明中。那么你的对象可以调用clone()方法。

对于某些类,Object的clone()方法的默认行为工作得很好。但是,如果一个对象包含对外部对象的引用,比如说ObjExternal,则可能需要重写clone()以获取正确的行为。否则,一个对象所做的ObjExternal中的更改也将在其克隆中可见。这意味着原始对象和它的克隆不是独立的 - 要解耦它们,必须重写clone()以便克隆对象和ObjExternal。然后原始对象引用ObjExternal,并且克隆引用ObjExternal的一个克隆,以便该对象和它的克隆是真正独立的。

equals() 方法

equals()方法比较两个对象是否相等,如果相等则返回true。 Object类中提供的equals()方法使用恒等操作符(==)来确定两个对象是否相等。 对于基本数据类型,这给出了正确的结果。 然而,对于对象来说,事实并非如此。 Object提供的equals()方法测试对象引用是否相等 - 也就是说,比较的对象是完全相同的对象。

为了测试两个对象在等价意义上是否相等(包含相同的信息),您必须重载equals()方法。 下面是一个重载equals()的Book类的例子:

public class Book {
    ...
    public boolean equals(Object obj) {
        if (obj instanceof Book)
            return ISBN.equals((Book)obj.getISBN()); 
        else
            return false;
    }
}

考虑这个测试Book类的两个实例是否相等的代码:

// Swing Tutorial, 2nd edition
Book firstBook  = new Book("0201914670");
Book secondBook = new Book("0201914670");
if (firstBook.equals(secondBook)) {
    System.out.println("objects are equal");
} else {
    System.out.println("objects are not equal");
}

即使firstBook和secondBook引用两个不同的对象,该程序显示的对象也是相等的。 它们被认为是相等的,因为比较的对象包含相同的ISBN号码。

如果恒等运算符不适合您的类,则应始终重载equals()方法。

注意:如果你重载equals(),你也必须重载hashCode()。

finalize() 方法

Object类提供了一个回调方法finalize(),可以在对象变成垃圾时调用它。 finalize()的对象的实现什么也不做 - 你可以重载finalize()来做清理,比如释放资源。

finalize()方法可以被系统自动调用,但是当它被调用时,或者即使被调用,也是不确定的。 因此,你不应该依靠这种方法来为你做清理工作。 例如,如果在执行 I/O 之后没有在代码中关闭文件描述符,并且您希望finalize()为您关闭它们,则可能会用完文件描述符。

getClass() 方法

你不能重载getClass。

getClass()方法返回一个Class对象,它具有可用于获取有关该类的信息的方法,如名称(getSimpleName()),其超类(getSuperclass())及其实现的接口(getInterfaces())。 例如,以下方法获取并显示对象的类名称:

void printClassName(Object obj) {
    System.out.println("The object's" + " class is " +
        obj.getClass().getSimpleName());
}

java.lang包中的Class类有大量的方法(超过50个)。 例如,您可以测试以查看该类是否是注释(isAnnotation()),接口(isInterface())还是枚举(isEnum())。 您可以看到对象的字段(getFields())或其方法(getMethods()),等等。

hashCode() 方法

hashCode()返回的值是对象的哈希码,它是以十六进制表示的对象的内存地址。

根据定义,如果两个对象相等,则它们的哈希码也必须相等。 如果重载equals()方法,则可以改变两个对象的等同方式,并且Object的hashCode()实现不再有效。 因此,如果重写equals()方法,则还必须重写hashCode()方法。

toString() 方法

你应该总是考虑在你的类中重写toString()方法。

Object的toString()方法返回对象的String表示,这对调试非常有用。 对象的String表示完全取决于对象,这就是为什么您需要在您的类中重写toString()。

您可以使用toString()和System.out.println()来显示对象的文本表示,例如Book的一个实例:

System.out.println(firstBook.toString());

对于正确重载的toString()方法,将会打印一些有用的东西,如下所示:

ISBN: 0201914670; The Swing Tutorial; A Guide to Constructing GUIs, 2nd Edition

编写final类和方法

你可以声明一些或所有类的方法是final。您在方法声明中使用final关键字来指示该方法不能被子类覆盖。 Object类这样做 - 它的一些方法是final的。

你可能希望使一个方法是final的,如果它有一个不应该改变的实现,并且对于这个对象的一致状态是至关重要的。 例如,您可能希望在此ChessAlgorithm类中getFirstPlayer方法是final的:

class ChessAlgorithm {
    enum ChessPlayer { WHITE, BLACK }
    ...
    final ChessPlayer getFirstPlayer() {
        return ChessPlayer.WHITE;
    }
    ...
}

从构造函数调用的方法通常应该被声明为final。 如果构造函数调用非final方法,则子类可能会以令人惊讶的或不希望的结果重新定义该方法。

请注意,您也可以声明整个类是final的。一个声明为final的类不能被子类化。 这是特别有用的,例如,当创建像String类这样的不可变类时。

抽象方法和类

抽象类(abstract class)是一个被声明为抽象的类,它可能包含或不包含抽象方法。 抽象类不能被实例化,但它们可以被子类化。

抽象方法是一个没有实现(没有大括号,后跟分号)的方法,如下所示:

abstract void moveTo(double deltaX, double deltaY);

如果一个类包含抽象方法,那么该类本身_必须声明为抽象的_,如下所示:

public abstract class GraphicObject {
   // declare fields
   // declare nonabstract methods
   abstract void draw();
}

当抽象类是子类时,子类通常为其父类中的所有抽象方法提供实现。 但是,如果没有,那么子类也必须被声明为抽象的。

注意:在接口中未声明为default或static的方法是隐式抽象的,所以抽象修饰符不用于接口方法。 (可以使用,但没有必要)

抽象类与接口比较

抽象类与接口类似。你不能实例化它们,它们可能包含一个声明有或没有实现的混合方法。但是,对于抽象类,您可以声明非static和final的字段,并定义public、protected和private的具体方法。使用接口,所有的字段都是自动public、sprotected和final的,你声明或定义的所有方法(作为默认方法)是public的。另外,只能扩展一个类,不管它是否抽象,而可以实现任意数量的接口。

你应该使用哪个?抽象类或接口?

  • 如果这些语句适用于您的情况,请考虑使用抽象类:

    • 你想在几个密切相关的类中分享代码。
    • 您期望扩展抽象类的类有许多常用的方法或字段,或者需要访问修饰符而不是public(如protected和private)。
    • 您想要声明非static或非final字段。这使您可以定义可以访问和修改它们所属对象状态的方法。
  • 如果以下任何一种语句适用于您的情况,请考虑使用接口:

    • 你期望不相关的类将实现你的接口。例如,Comparable和Cloneable接口是由许多不相关的类实现的。
    • 您想要指定特定数据类型的行为,但不关心谁实现其行为。
    • 你想利用类型的多重继承。

JDK中的抽象类的一个例子是AbstractMap,它是Collections Framework的一部分。它的子类(包括HashMap,TreeMap和ConcurrentHashMap)共享AbstractMap定义的许多方法(包括get,put,isEmpty,containsKey和containsValue)。

JDK中一个实现了几个接口的类的例子是HashMap,它实现了Serializable,Cloneable和Map接口。通过阅读这个接口列表,你可以推断出一个HashMap的实例(不管开发人员或公司是谁实现了这个类)是可以被克隆的,是可序列化的(这意味着它可以被转换成一个字节流;参见Serializable对象),并具有map的功能。另外,Map接口已经增强了很多默认的方法,比如merge和forEach,那些实现了这个接口的老类不需要定义。

请注意,许多软件库都使用抽象类和接口; HashMap类实现了几个接口,并扩展了抽象类AbstractMap。

当一个抽象类实现一个接口

在接口部分,有人指出,实现接口的类必须实现接口的所有方法。 但是,可以定义一个不实现所有接口方法的类,只要这个类声明为抽象的。 例如,

abstract class X implements Y {
  // implements all but one method of Y
}

class XX extends X {
  // implements the remaining method in Y
}

在这种情况下,X类必须是抽象的,因为它没有完全实现Y,但是XX类实际上实现了Y.

类成员

抽象类可能有static字段和static方法。 您可以像使用任何其他类一样将这些静态成员与类引用(例如AbstractClass.staticMethod())一起使用。

参考

  1. Learning the Java Language