Java 中文官方教程 2022 版(四)

2024-05-24 15:37:01 浏览数 (2)

方法引用

原文:docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

你可以使用 lambda 表达式来创建匿名方法。然而,有时候 lambda 表达式仅仅是调用一个已存在的方法。在这种情况下,通过名称引用现有方法通常更清晰。方法引用使你能够做到这一点;它们是紧凑、易读的 lambda 表达式,用于已经有名称的方法。

再次考虑在 lambda 表达式部分讨论的Person类:

代码语言:javascript复制
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);
    }

    // ...
}

假设你的社交网络应用的成员被包含在一个数组中,并且你想按年龄对数组进行排序。你可以使用以下代码(在示例MethodReferencesTest中找到本节描述的代码片段):

代码语言:javascript复制
Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

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

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

此次调用sort的方法签名如下:

代码语言:javascript复制
static <T> void sort(T[] a, Comparator<? super T> c)

注意Comparator接口是一个函数式接口。因此,你可以使用 lambda 表达式来代替定义并创建一个实现Comparator的类的新实例:

代码语言:javascript复制
Arrays.sort(rosterAsArray,
    (Person a, Person b) -> {
        return a.getBirthday().compareTo(b.getBirthday());
    }
);

然而,比较两个Person实例的出生日期的方法Person.compareByAge已经存在。你可以在 lambda 表达式的主体中调用这个方法:

代码语言:javascript复制
Arrays.sort(rosterAsArray,
    (a, b) -> Person.compareByAge(a, b)
);

因为这个 lambda 表达式调用了一个已存在的方法,你可以使用方法引用代替 lambda 表达式:

代码语言:javascript复制
Arrays.sort(rosterAsArray, Person::compareByAge);

方法引用Person::compareByAge在语义上与 lambda 表达式(a, b) -> Person.compareByAge(a, b)相同。它们各自具有以下特征:

  • 其形式参数列表是从Comparator<Person>.compare复制的,即(Person, Person)
  • 其主体调用方法Person.compareByAge

方法引用的种类

有四种方法引用的种类:

种类

语法

示例

引用静态方法

*ContainingClass*::*staticMethodName*

Person::compareByAge MethodReferencesExamples::appendStrings

引用特定对象的实例方法

*containingObject*::*instanceMethodName*

myComparisonProvider::compareByName myApp::appendStrings2

引用特定类型的任意对象的实例方法

*ContainingType*::*methodName*

String::compareToIgnoreCase String::concat

引用构造函数

*ClassName*::new

HashSet::new

以下示例,MethodReferencesExamples,包含了前三种方法引用的示例:

代码语言:javascript复制
import java.util.function.BiFunction;

public class MethodReferencesExamples {

    public static <T> T mergeThings(T a, T b, BiFunction<T, T, T> merger) {
        return merger.apply(a, b);
    }

    public static String appendStrings(String a, String b) {
        return a   b;
    }

    public String appendStrings2(String a, String b) {
        return a   b;
    }

    public static void main(String[] args) {

        MethodReferencesExamples myApp = new MethodReferencesExamples();

        // Calling the method mergeThings with a lambda expression
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", (a, b) -> a   b));

        // Reference to a static method
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", MethodReferencesExamples::appendStrings));

        // Reference to an instance method of a particular object        
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", myApp::appendStrings2));

        // Reference to an instance method of an arbitrary object of a
        // particular type
        System.out.println(MethodReferencesExamples.
            mergeThings("Hello ", "World!", String::concat));
    }
}

所有的System.out.println()语句都打印相同的内容:Hello World!

BiFunctionjava.util.function包中许多函数接口之一。BiFunction函数接口可以表示接受两个参数并产生结果的 lambda 表达式或方法引用。

静态方法引用

方法引用Person::compareByAgeMethodReferencesExamples::appendStrings是对静态方法的引用。

引用特定对象的实例方法

下面是引用特定对象实例方法的示例:

代码语言:javascript复制
class ComparisonProvider {
    public int compareByName(Person a, Person b) {
        return a.getName().compareTo(b.getName());
    }

    public int compareByAge(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}
ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);

方法引用myComparisonProvider::compareByName调用myComparisonProvider对象的compareByName方法。JRE 推断方法类型参数,本例中为(Person, Person)

类似地,方法引用myApp::appendStrings2将调用myApp对象的appendStrings2方法。JRE 推断方法类型参数,本例中为(String, String)

引用特定类型任意对象的实例方法

下面是一个引用特定类型任意对象的实例方法的示例:

代码语言:javascript复制
String[] stringArray = { "Barbara", "James", "Mary", "John",
    "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

方法引用String::compareToIgnoreCase的等效 lambda 表达式将具有形式参数列表(String a, String b),其中ab是用于更好描述此示例的任意名称。方法引用将调用a.compareToIgnoreCase(b)方法。

类似地,方法引用String::concat将调用a.concat(b)方法。

构造函数引用

你可以通过使用名称new来引用构造函数,与引用静态方法的方式相同。以下方法将元素从一个集合复制到另一个集合:

代码语言:javascript复制
public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
    DEST transferElements(
        SOURCE sourceCollection,
        Supplier<DEST> collectionFactory) {

    DEST result = collectionFactory.get();
    for (T t : sourceCollection) {
        result.add(t);
    }
    return result;
}

函数接口Supplier包含一个名为get的方法,不接受参数并返回一个对象。因此,你可以使用 lambda 表达式调用方法transferElements,如下所示:

代码语言:javascript复制
Set<Person> rosterSetLambda =
    transferElements(roster, () -> { return new HashSet<>(); });

你可以使用构造函数引用来替代 lambda 表达式,如下所示:

代码语言:javascript复制
Set<Person> rosterSet = transferElements(roster, HashSet::new);

Java 编译器推断你想要创建一个包含类型为Person的元素的HashSet集合。或者,你可以按照以下方式指定:

代码语言:javascript复制
Set<Person> rosterSet = transferElements(roster, HashSet<Person>::new);

何时使用嵌套类、局部类、匿名类和 Lambda 表达式

原文:docs.oracle.com/javase/tutorial/java/javaOO/whentouse.html

如在嵌套类一节中所述,嵌套类使您能够逻辑地将仅在一个地方使用的类分组,增加封装的使用,并创建更易读和可维护的代码。局部类、匿名类和 Lambda 表达式也具有这些优点;但是,它们旨在用于更具体的情况:

  • 局部类:如果需要创建一个类的多个实例、访问其构造函数或引入一个新的命名类型(例如,因为您需要稍后调用其他方法),请使用它。
  • 匿名类:如果需要声明字段或额外方法,请使用它。
  • Lambda 表达式:
    • 如果您要封装要传递给其他代码的单个行为单元,请使用它。例如,如果您希望对集合的每个元素执行某个操作,当进程完成时,或者当进程遇到错误时,您将使用 Lambda 表达式。
    • 如果需要一个功能接口的简单实例,并且前述条件均不适用(例如,您不需要构造函数、命名类型、字段或额外方法),请使用它。
  • 嵌套类:如果您的需求类似于局部类,并且希望使类型更广泛可用,且不需要访问局部变量或方法参数时,请使用它。
    • 如果需要访问封闭实例的非公共字段和方法,请使用非静态嵌套类(或内部类)。如果不需要此访问权限,请使用静态嵌套类。

问题和练习:嵌套类

原文:docs.oracle.com/javase/tutorial/java/javaOO/QandE/nested-questions.html

问题

  1. 程序Problem.java无法编译。你需要做什么才能使其编译?为什么?
  2. 使用 Java API 文档中Box类(位于javax.swing包中)的文档来帮助回答以下问题。
    1. Box定义了哪个静态嵌套类?
    2. Box定义了哪个内部类?
    3. Box的内部类的超类是什么?
    4. 从任何类中可以使用Box的哪些嵌套类?
    5. 如何创建BoxFiller类的实例?

练习

获取文件Class1.java。编译并运行Class1。输出是什么?

以下练习涉及修改类DataStructure.java,该类在内部类示例部分讨论。

定义一个名为print(DataStructureIterator iterator)的方法。使用EvenIterator类的实例调用此方法,使其执行与printEven方法相同的功能。

调用方法print(DataStructureIterator iterator),使其打印具有奇数索引值的元素。使用匿名类作为方法的参数,而不是接口DataStructureIterator的实例。

定义一个名为print(java.util.function.Function<Integer, Boolean> iterator)的方法,执行与print(DataStructureIterator iterator)相同的功能。使用 lambda 表达式调用此方法,以打印具有偶数索引值的元素。再次使用 lambda 表达式调用此方法,以打印具有奇数索引值的元素。

定义两个方法,使得以下两个语句打印具有偶数索引值和具有奇数索引值的元素:

代码语言:javascript复制
DataStructure ds = new DataStructure()
// ...
ds.print(DataStructure::isEvenIndex);
ds.print(DataStructure::isOddIndex);

检查你的答案。

枚举类型

原文:docs.oracle.com/javase/tutorial/java/javaOO/enum.html

枚举类型是一种特殊的数据类型,允许变量成为一组预定义的常量之一。变量必须等于为其预定义的值之一。常见示例包括罗盘方向(NORTH、SOUTH、EAST 和 WEST 的值)和一周的天数。

由于它们是常量,枚举类型字段的名称必须是大写字母。

在 Java 编程语言中,您可以使用enum关键字定义枚举类型。例如,您可以指定一个星期几的枚举类型如下:

代码语言:javascript复制
public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY 
}

每当需要表示一组固定常量时,都应该使用枚举类型。这包括自然枚举类型,如我们太阳系中的行星和在编译时知道所有可能值的数据集,例如菜单上的选项、命令行标志等。

这里是一些代码,向您展示如何使用上面定义的Day枚举:

代码语言:javascript复制
public class EnumTest {
    Day day;

    public EnumTest(Day day) {
        this.day = day;
    }

    public void tellItLikeItIs() {
        switch (day) {
            case MONDAY:
                System.out.println("Mondays are bad.");
                break;

            case FRIDAY:
                System.out.println("Fridays are better.");
                break;

            case SATURDAY: case SUNDAY:
                System.out.println("Weekends are best.");
                break;

            default:
                System.out.println("Midweek days are so-so.");
                break;
        }
    }

    public static void main(String[] args) {
        EnumTest firstDay = new EnumTest(Day.MONDAY);
        firstDay.tellItLikeItIs();
        EnumTest thirdDay = new EnumTest(Day.WEDNESDAY);
        thirdDay.tellItLikeItIs();
        EnumTest fifthDay = new EnumTest(Day.FRIDAY);
        fifthDay.tellItLikeItIs();
        EnumTest sixthDay = new EnumTest(Day.SATURDAY);
        sixthDay.tellItLikeItIs();
        EnumTest seventhDay = new EnumTest(Day.SUNDAY);
        seventhDay.tellItLikeItIs();
    }
}

输出为:

代码语言:javascript复制
Mondays are bad.
Midweek days are so-so.
Fridays are better.
Weekends are best.
Weekends are best.

Java 编程语言的枚举类型比其他语言中的对应类型更强大。enum声明定义了一个(称为枚举类型)。枚举类体可以包括方法和其他字段。编译器在创建枚举时会自动添加一些特殊方法。例如,它们具有一个静态values方法,返回一个包含枚举值的数组,按照它们声明的顺序排列。此方法通常与 for-each 结构结合使用,以遍历枚举类型的值。例如,下面Planet类示例中的代码遍历太阳系中的所有行星。

代码语言:javascript复制
for (Planet p : Planet.values()) {
    System.out.printf("Your weight on %s is %f%n",
                      p, p.surfaceWeight(mass));
}

注意: 所有枚举隐式扩展java.lang.Enum。因为一个类只能扩展一个父类(参见声明类),Java 语言不支持状态的多重继承(参见状态、实现和类型的多重继承),因此枚举不能扩展其他任何内容。


在下面的示例中,Planet是一个表示太阳系行星的枚举类型。它们定义了常量质量和半径属性。

每个枚举常量都声明了质量和半径参数的值。这些值在创建常量时传递给构造函数。Java 要求常量在任何字段或方法之前定义。此外,当存在字段和方法时,枚举常量列表必须以分号结尾。


注意: 枚举类型的构造函数必须是包私有或私有访问。它会自动创建在枚举体开头定义的常量。您不能自己调用枚举构造函数。


除了其属性和构造函数外,Planet 还有一些方法,可以让你获取每个行星上物体的表面重力和重量。以下是一个示例程序,它接受你在地球上的体重(以任何单位)并计算并打印出你在所有行星上的体重(以相同单位):

代码语言:javascript复制
public enum Planet {
    MERCURY (3.303e 23, 2.4397e6),
    VENUS   (4.869e 24, 6.0518e6),
    EARTH   (5.976e 24, 6.37814e6),
    MARS    (6.421e 23, 3.3972e6),
    JUPITER (1.9e 27,   7.1492e7),
    SATURN  (5.688e 26, 6.0268e7),
    URANUS  (8.686e 25, 2.5559e7),
    NEPTUNE (1.024e 26, 2.4746e7);

    private final double mass;   // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
    private double mass() { return mass; }
    private double radius() { return radius; }

    // universal gravitational constant  (m3 kg-1 s-2)
    public static final double G = 6.67300E-11;

    double surfaceGravity() {
        return G * mass / (radius * radius);
    }
    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
    public static void main(String[] args) {
        if (args.length != 1) {
            System.err.println("Usage: java Planet <earth_weight>");
            System.exit(-1);
        }
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight/EARTH.surfaceGravity();
        for (Planet p : Planet.values())
           System.out.printf("Your weight on %s is %f%n",
                             p, p.surfaceWeight(mass));
    }
}

如果你在命令行中运行 Planet.class 并带上参数 175,你会得到以下输出:

代码语言:javascript复制
$ java Planet 175
Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
Your weight on EARTH is 175.000000
Your weight on MARS is 66.279007
Your weight on JUPITER is 442.847567
Your weight on SATURN is 186.552719
Your weight on URANUS is 158.397260
Your weight on NEPTUNE is 199.207413

问题和练习:枚举类型

原文:docs.oracle.com/javase/tutorial/java/javaOO/QandE/enum-questions.html

问题

  1. 真或假:Enum类型可以是java.lang.String的子类。

练习

  1. 重写问题和练习:类中的Card类,使其使用枚举类型表示卡牌的等级和花色。
  2. 重写Deck类。

检查你的答案。

课程:注解

原文:docs.oracle.com/javase/tutorial/java/annotations/index.html

注解,一种元数据形式,提供关于程序的数据,这些数据不是程序本身的一部分。注解对其注释的代码的操作没有直接影响。

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

  • 编译器的信息 — 编译器可以使用注解来检测错误或抑制警告。
  • 编译时和部署时处理 — 软件工具可以处理注解信息以生成代码、XML 文件等。
  • 运行时处理 — 一些注解可以在运行时被检查。

本课程解释了注解可以在哪里使用,如何应用注解,在 Java 平台标准版(Java SE API)中有哪些预定义的注解类型可用,如何将类型注解与可插入类型系统结合使用以编写具有更强类型检查的代码,以及如何实现重复注解。

注解基础知识

原文:docs.oracle.com/javase/tutorial/java/annotations/basics.html

注解的格式

在其最简单的形式下,注解看起来像下面这样:

代码语言:javascript复制
@Entity

符号@告诉编译器接下来是一个注解。在下面的例子中,注解的名称是Override

代码语言:javascript复制
@Override
void mySuperMethod() { ... }

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

代码语言:javascript复制
@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass { ... }

代码语言:javascript复制
@SuppressWarnings(value = "unchecked")
void myMethod() { ... }

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

代码语言:javascript复制
@SuppressWarnings("unchecked")
void myMethod() { ... }

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

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

代码语言:javascript复制
@Author(name = "Jane Doe")
@EBook
class MyClass { ... }

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

代码语言:javascript复制
@Author(name = "Jane Doe")
@Author(name = "John Smith")
class MyClass { ... }

从 Java SE 8 发布开始支持重复注解。更多信息,请参见重复注解。

注解类型可以是 Java SE API 的java.langjava.lang.annotation包中定义的类型之一。在前面的示例中,OverrideSuppressWarnings是预定义的 Java 注解。还可以定义自己的注解类型。前面示例中的AuthorEbook注解是自定义注解类型。

注解可以使用的位置

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

从 Java SE 8 发布开始,注解也可以应用于类型的使用。以下是一些示例:

类实例创建表达式:

代码语言:javascript复制
    new @Interned MyObject();

类型转换:

代码语言:javascript复制
    myString = (@NonNull String) str;

implements子句:

代码语言:javascript复制
    class UnmodifiableList<T> implements
        @Readonly List<@Readonly T> { ... }

抛出异常声明:

代码语言:javascript复制
    void monitorTemperature() throws
        @Critical TemperatureException { ... }

这种形式的注解称为类型注解。更多信息,请参见类型注解和可插入类型系统。

声明注解类型

原文:docs.oracle.com/javase/tutorial/java/annotations/declaring.html

许多注解取代了代码中的注释。

假设一个软件组传统上在每个类的主体部分以提供重要信息的注释开头:

代码语言:javascript复制
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

}

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

代码语言:javascript复制
@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,表示注解类型)。注解类型是接口的一种形式,稍后会介绍。目前,您不需要理解接口。

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

在定义注解类型之后,您可以像这样使用该类型的注解,填入值:

代码语言:javascript复制
@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定义:

代码语言:javascript复制
// import this to use @Documented
import java.lang.annotation.*;

@Documented
@interface ClassPreamble {

   // Annotation element definitions

}

预定义的注解类型

原文:docs.oracle.com/javase/tutorial/java/annotations/predefined.html

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

Java 语言使用的注解类型

java.lang中定义的预定义注解类型为@Deprecated@Override@SuppressWarnings

@Deprecated @Deprecated 注解表示标记的元素已被弃用,不应再使用。每当程序使用带有@Deprecated注解的方法、类或字段时,编译器都会生成警告。当元素被弃用时,还应使用 Javadoc 的@deprecated标签进行文档化,如下例所示。在 Javadoc 注释和注解中使用@符号并非巧合:它们在概念上是相关的。另外,请注意,Javadoc 标签以小写d开头,而注解以大写D开头。

代码语言:javascript复制
   // Javadoc comment follows
    /**
     * *@deprecated*
     * *explanation of why it was deprecated*
     */
    @Deprecated
    static void deprecatedMethod() { }
}

@Override @Override 注解告诉编译器,该元素意在覆盖在超类中声明的元素。覆盖方法将在接口和继承中讨论。

代码语言:javascript复制
   // *mark method as a superclass method*
   // *that has been overridden*
   @Override 
   int overriddenMethod() { }

虽然在覆盖方法时不是必须使用此注解,但它有助于防止错误。如果标记为@Override的方法未能正确覆盖其超类中的方法,编译器将生成错误。

@SuppressWarnings @SuppressWarnings 注解告诉编译器抑制其通常生成的特定警告。在下面的示例中,使用了一个已弃用的方法,编译器通常会生成警告。然而,在这种情况下,该注解导致警告被抑制。

代码语言:javascript复制
   // *use a deprecated method and tell* 
   // *compiler not to generate a warning*
   @SuppressWarnings("deprecation")
    void useDeprecatedMethod() {
        // deprecation warning
        // - suppressed
        objectOne.deprecatedMethod();
    }

每个编译器警告都属于一个类别。Java 语言规范列出了两个类别:deprecationunchecked。当与在泛型出现之前编写的旧代码进行接口时,可能会出现unchecked警告。要抑制多个类别的警告,请使用以下语法:

代码语言:javascript复制
@SuppressWarnings({"unchecked", "deprecation"})

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

@FunctionalInterface @FunctionalInterface 注解,引入于 Java SE 8,指示类型声明旨在成为功能接口,如 Java 语言规范所定义。

适用于其他注解的注解

适用于其他注解的注解称为元注解。在java.lang.annotation中定义了几种元注解类型。

@Retention @Retention 注解指定标记的注解如何存储:

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

@Documented @Documented 注解指示每当使用指定的注解时,应使用 Javadoc 工具记录这些元素。(默认情况下,注解不包含在 Javadoc 中。)有关更多信息,请参阅Javadoc 工具页面。

@Target @Target 注解标记另一个注解,限制注解可以应用于哪种 Java 元素。目标注解将以下元素类型之一指定为其值:

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

@Inherited @Inherited 注解指示注解类型可以从超类继承。(默认情况下不是这样。)当用户查询注解类型并且类没有此类型的注解时,将查询类的超类以获取注解类型。此注解仅适用于类声明。

@Repeatable @Repeatable 注解,引入于 Java SE 8,表示标记的注解可以多次应用于同一声明或类型使用。有关更多信息,请参阅重复注解。

类型注解和可插拔类型系统

原文:docs.oracle.com/javase/tutorial/java/annotations/type_annotations.html

在 Java SE 8 发布之前,注解只能应用于声明。从 Java SE 8 发布开始,注解也可以应用于任何类型使用。这意味着注解可以在使用类型的任何地方使用。一些类型使用的示例包括类实例创建表达式(new)、强制转换、implements子句和throws子句。这种形式的注解称为类型注解,注解基础知识提供了几个示例。

类型注解是为了支持改进 Java 程序的分析方式以确保更强的类型检查而创建的。Java SE 8 发布没有提供类型检查框架,但允许您编写(或下载)一个作为一个或多个可插拔模块实现的类型检查框架,这些模块与 Java 编译器一起使用。

例如,您希望确保程序中的特定变量永远不会被赋予 null;您希望避免触发NullPointerException。您可以编写一个自定义插件来检查这一点。然后,您将修改代码以注释该特定变量,指示它永远不会被赋予 null。变量声明可能如下所示:

代码语言:javascript复制
@NonNull String str;

当您在命令行中编译代码时,包括NonNull模块,如果编译器检测到潜在问题,它会打印警告,让您修改代码以避免错误。在您纠正代码以消除所有警告后,当程序运行时,这种特定错误将不会发生。

您可以使用多个类型检查模块,每个模块检查不同类型的错误。通过这种方式,您可以在需要时在 Java 类型系统的基础上构建,添加特定的检查。

通过谨慎使用类型注解和存在可插拔类型检查器,您可以编写更强大且更不容易出错的代码。

在许多情况下,您不必编写自己的类型检查模块。有第三方已经为您完成了这项工作。例如,您可能希望利用华盛顿大学创建的检查器框架。该框架包括一个NonNull模块,以及一个正则表达式模块和一个互斥锁模块。更多信息,请参阅检查器框架。

可重复注解

原文:docs.oracle.com/javase/tutorial/java/annotations/repeating.html

有一些情况下,您希望将相同的注解应用于声明或类型使用。从 Java SE 8 发布开始,可重复注解使您能够做到这一点。

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

代码语言:javascript复制
@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }

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

代码语言:javascript复制
@Alert(role="Manager")
@Alert(role="Administrator")
public class UnauthorizedAccessException extends SecurityException { ... }

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

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

注解类型必须标记为@Repeatable元注解。以下示例定义了一个自定义的@Schedule可重复注解类型:

代码语言:javascript复制
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注解中。

在未声明为可重复之前将相同的注解应用于声明会导致编译时错误。

第 2 步:声明包含注解类型

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

代码语言:javascript复制
public @interface Schedules {
    Schedule[] value();
}

检索注解

反射 API 中有几种可用的方法可用于检索注解。返回单个注解的方法的行为,例如AnnotatedElement.getAnnotation(Class),在只有一个请求类型的注解存在时保持不变。如果存在多个请求类型的注解,可以通过首先获取它们的容器注解来获取它们。通过这种方式,旧代码仍然可以正常工作。在 Java SE 8 中引入了其他方法,通过扫描容器注解一次返回多个注解,例如AnnotatedElement.getAnnotationsByType(Class)。请参阅AnnotatedElement类规范,了解所有可用方法的信息。

设计考虑事项

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

问题和练习:注解

原文:docs.oracle.com/javase/tutorial/java/annotations/QandE/questions.html

问题

以下接口有什么问题?

代码语言:javascript复制
public interface House {
    @Deprecated
    void open();
    void openFrontDoor();
    void openBackDoor();
}

考虑House接口的以下实现,如问题 1 所示。

代码语言:javascript复制
public class MyHouse implements House {
    public void open() {}
    public void openFrontDoor() {}
    public void openBackDoor() {}
}

如果您编译此程序,编译器会产生警告,因为open已被弃用(在接口中)。您可以采取什么措施消除该警告?

以下代码是否会编译出错?为什么?

代码语言:javascript复制
public @interface Meal { ... }

@Meal("breakfast", mainDish="cereal")
@Meal("lunch", mainDish="pizza")
@Meal("dinner", mainDish="salad")
public void evaluateDiet() { ... }

练习

  1. 为增强请求定义一个注解类型,具有idsynopsisengineerdate元素。为工程师指定默认值为unassigned,为日期指定默认值为unknown

检查您的答案。

课程:接口和继承

原文:docs.oracle.com/javase/tutorial/java/IandI/index.html

接口

在上一课中,您看到了实现接口的示例。您可以在这里阅读更多关于接口的信息——它们的作用是什么,为什么您可能想要编写一个,以及如何编写一个。

继承

这一部分描述了如何从一个类派生另一个类。也就是说,子类如何从超类继承字段和方法。您将了解到所有类都是从Object类派生的,以及如何修改子类从超类继承的方法。本节还涵盖类似接口的抽象类

接口

原文:docs.oracle.com/javase/tutorial/java/IandI/createinterface.html

在软件工程中有许多情况下,不同组的程序员需要达成一致的“合同”,明确规定他们的软件如何交互。每个组都应该能够编写他们的代码,而不需要了解其他组的代码是如何编写的。一般来说,接口就是这样的合同。

例如,想象一个未来社会,在这个社会中,由计算机控制的机器人汽车在城市街道上运载乘客,没有人类操作员。汽车制造商编写软件(当然是 Java),操作汽车—停止、启动、加速、左转等等。另一个工业团体,电子导航仪制造商,制造接收 GPS(全球定位系统)位置数据和交通状况无线传输的计算机系统,并利用这些信息驾驶汽车。

汽车制造商必须发布一个行业标准接口,详细说明可以调用哪些方法来使汽车移动(任何制造商的任何汽车)。导航制造商可以编写调用接口中描述的方法来命令汽车的软件。两个工业团体都不需要知道*对方的软件是如何实现的。事实上,每个团体都认为自己的软件是高度专有的,并保留随时修改的权利,只要它继续遵守已发布的接口。

Java 中的接口

在 Java 编程语言中,接口是一种引用类型,类似于类,只能包含常量、方法签名、默认方法、静态方法和嵌套类型。方法体仅存在于默认方法和静态方法中。接口不能被实例化—它们只能被类实现或其他接口扩展。扩展将在本课程的后面讨论。

定义接口类似于创建新类:

代码语言:javascript复制
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
}

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

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

代码语言:javascript复制
public class OperateBMW760i implements OperateCar {

    // the OperateCar method signatures, with implementation --
    // for example:
    public 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
}

在上面的机器人汽车示例中,将实现接口的是汽车制造商。雪佛兰的实现肯定与丰田的实现大不相同,但两家制造商都会遵守相同的接口。作为接口的客户,导航制造商将构建使用汽车位置的 GPS 数据、数字街道地图和交通数据来驾驶汽车的系统。在这样做的过程中,导航系统将调用接口方法:转向、变道、刹车、加速等等。

接口作为 API

机器人汽车示例展示了一个作为行业标准*应用程序编程接口(API)*使用的接口。API 在商业软件产品中也很常见。通常,一家公司销售一个包含复杂方法的软件包,另一家公司希望在自己的软件产品中使用这些方法。一个例子是数字图像处理方法包,这些方法被销售给制作最终用户图形程序的公司。图像处理公司编写其类来实现一个接口,然后将其公开给客户。图形公司然后使用接口中定义的签名和返回类型调用图像处理方法。虽然图像处理公司的 API 是公开的(给其客户),但其 API 的实现被保持为严格保密的秘密—事实上,它可以在以后的某个日期修改实现,只要它继续实现客户依赖的原始接口。

定义一个接口

原文:docs.oracle.com/javase/tutorial/java/IandI/interfaceDef.html

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

代码语言:javascript复制
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关键字定义。接口中的所有抽象、默认和静态方法都隐式地是public的,因此可以省略public修饰符。

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

实现一个接口

原文:docs.oracle.com/javase/tutorial/java/IandI/usinginterface.html

要声明一个实现接口的类,你需要在类声明中包含一个implements子句。你的类可以实现多个接口,因此implements关键字后面跟着一个逗号分隔的接口列表。按照惯例,如果有extends子句,则implements子句跟在其后。

一个示例接口,Relatable

考虑一个定义如何比较对象大小的接口。

代码语言:javascript复制
public interface Relatable {

    // this (object calling isLargerThan)
    // and other must be instances of 
    // the same class returns 1, 0, -1 
    // if this is greater than, 
    // equal to, or less than other
    public int isLargerThan(Relatable other);
}

如果你想要比较相似对象的大小,无论它们是什么,实例化它们的类应该实现Relatable

任何类都可以实现Relatable,只要有一种方法可以比较从该类实例化的对象的相对“大小”。对于字符串,可以是字符数;对于书籍,可以是页数;对于学生,可以是体重;等等。对于平面几何对象,面积是一个不错的选择(参见下面的RectanglePlus类),而对于三维几何对象,体积也可以工作。所有这些类都可以实现isLargerThan()方法。

如果你知道一个类实现了Relatable,那么你就知道可以比较从该类实例化的对象的大小。

实现 Relatable 接口

这里是在创建对象部分中介绍的Rectangle类,重写以实现Relatable

代码语言:javascript复制
public class RectanglePlus 
    implements Relatable {
    public int width = 0;
    public int height = 0;
    public Point origin;

    // four constructors
    public RectanglePlus() {
        origin = new Point(0, 0);
    }
    public RectanglePlus(Point p) {
        origin = p;
    }
    public RectanglePlus(int w, int h) {
        origin = new Point(0, 0);
        width = w;
        height = h;
    }
    public RectanglePlus(Point p, int w, int h) {
        origin = p;
        width = w;
        height = h;
    }

    // a method for moving the rectangle
    public void move(int x, int y) {
        origin.x = x;
        origin.y = y;
    }

    // a method for computing
    // the area of the rectangle
    public int getArea() {
        return width * height;
    }

    // a method required to implement
    // the Relatable interface
    public int isLargerThan(Relatable other) {
        RectanglePlus otherRect 
            = (RectanglePlus)other;
        if (this.getArea() < otherRect.getArea())
            return -1;
        else if (this.getArea() > otherRect.getArea())
            return 1;
        else
            return 0;               
    }
}

因为RectanglePlus实现了Relatable,所以可以比较任意两个RectanglePlus对象的大小。


注意:Relatable接口中定义的isLargerThan方法接受一个Relatable类型的对象。在前面的示例中加粗显示的代码行将other强制转换为RectanglePlus实例。类型转换告诉编译器对象的真实类型。直接在other实例上调用getAreaother.getArea())将无法编译通过,因为编译器不知道other实际上是RectanglePlus的实例。


使用接口作为类型

原文:docs.oracle.com/javase/tutorial/java/IandI/interfaceAsType.html

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

举例来说,这里有一种方法可以找到一对对象中最大的对象,适用于任何从实现了Relatable接口的类实例化的对象:

代码语言:javascript复制
public Object findLargest(Object object1, Object object2) {
   Relatable obj1 = (Relatable)object1;
   Relatable obj2 = (Relatable)object2;
   if ((obj1).isLargerThan(obj2) > 0)
      return object1;
   else 
      return object2;
}

通过将object1强制转换为Relatable类型,它可以调用isLargerThan方法。

如果你坚持在各种类中实现Relatable,那么从任何这些类实例化的对象都可以使用findLargest()方法进行比较——前提是这两个对象属于同一类。同样,它们也可以使用以下方法进行比较:

代码语言:javascript复制
public Object findSmallest(Object object1, Object object2) {
   Relatable obj1 = (Relatable)object1;
   Relatable obj2 = (Relatable)object2;
   if ((obj1).isLargerThan(obj2) < 0)
      return object1;
   else 
      return object2;
}

public boolean isEqual(Object object1, Object object2) {
   Relatable obj1 = (Relatable)object1;
   Relatable obj2 = (Relatable)object2;
   if ( (obj1).isLargerThan(obj2) == 0)
      return true;
   else 
      return false;
}

这些方法适用于任何“可比较”的对象,无论它们的类继承关系如何。当它们实现了Relatable接口时,它们可以是自己类(或超类)类型和Relatable类型。这使它们具有多重继承的一些优势,可以同时具有来自超类和接口的行为。

接口的演变

原文:docs.oracle.com/javase/tutorial/java/IandI/nogrow.html

考虑您开发的名为DoIt的接口:

代码语言:javascript复制
public interface DoIt {
   void doSomething(int i, double x);
   int doSomethingElse(String s);
}

假设以后,您想要向DoIt添加第三个方法,使接口现在变成:

代码语言:javascript复制
public interface DoIt {

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

}

如果您进行此更改,则所有实现旧DoIt接口的类都将中断,因为它们不再实现旧接口。依赖于此接口的程序员将会强烈抗议。

尽量预见接口的所有用途并从一开始完全指定它。如果要向接口添加其他方法,您有几个选项。您可以创建一个扩展DoItDoItPlus接口:

代码语言:javascript复制
public interface DoItPlus extends DoIt {

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

}

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

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

代码语言:javascript复制
public interface DoIt {

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

}

请注意,您必须为默认方法提供实现。您还可以为现有接口定义新的静态方法。实现增强了新默认或静态方法的接口的类的用户无需修改或重新编译它们以适应额外的方法。

默认方法

原文:docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html

接口部分描述了一个涉及计算机控制汽车制造商发布行业标准接口的示例,描述了可以调用哪些方法来操作他们的汽车。如果这些计算机控制汽车制造商为他们的汽车添加新功能,比如飞行,会怎么样?这些制造商需要指定新的方法来使其他公司(如电子导航仪制造商)能够调整他们的软件以适应飞行汽车。这些汽车制造商会在哪里声明这些新的与飞行相关的方法?如果他们将它们添加到原始接口中,那么已经实现这些接口的程序员将不得不重新编写他们的实现。如果将它们添加为静态方法,那么程序员会将它们视为实用方法,而不是必要的核心方法。

默认方法使您能够向库的接口添加新功能,并确保与为旧版本接口编写的代码的二进制兼容性。

考虑下面的接口,TimeClient,如问题和练习的答案:接口中所述:

代码语言:javascript复制
import java.time.*; 

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
}

下面的类,SimpleTimeClient,实现了TimeClient

代码语言:javascript复制
package defaultmethods;

import java.time.*;
import java.lang.*;
import java.util.*;

public class SimpleTimeClient implements TimeClient {

    private LocalDateTime dateAndTime;

    public SimpleTimeClient() {
        dateAndTime = LocalDateTime.now();
    }

    public void setTime(int hour, int minute, int second) {
        LocalDate currentDate = LocalDate.from(dateAndTime);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(currentDate, timeToSet);
    }

    public void setDate(int day, int month, int year) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime currentTime = LocalTime.from(dateAndTime);
        dateAndTime = LocalDateTime.of(dateToSet, currentTime);
    }

    public void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime timeToSet = LocalTime.of(hour, minute, second); 
        dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
    }

    public LocalDateTime getLocalDateTime() {
        return dateAndTime;
    }

    public String toString() {
        return dateAndTime.toString();
    }

    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println(myTimeClient.toString());
    }
}

假设您想要向TimeClient接口添加新功能,比如通过ZonedDateTime对象(类似于LocalDateTime对象,但它存储时区信息)指定时区的能力:

代码语言:javascript复制
public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();                           
    ZonedDateTime getZonedDateTime(String zoneString);
}

TimeClient接口进行这种修改后,您还需要修改SimpleTimeClient类并实现getZonedDateTime方法。但是,与其将getZonedDateTime留空(如前面的例子中),您可以定义一个默认实现。(请记住,抽象方法是声明而没有实现的方法。)

代码语言:javascript复制
package defaultmethods;

import java.time.*;

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();

    static 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 ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }
}

您可以在接口中的方法签名开头使用default关键字来指定一个方法定义是默认方法。接口中的所有方法声明,包括默认方法,都隐式地是public的,因此您可以省略public修饰符。

使用这个接口,你不需要修改SimpleTimeClient类,而这个类(以及任何实现TimeClient接口的类)将已经定义好getZonedDateTime方法。下面的例子,TestSimpleTimeClient,调用了SimpleTimeClient实例的getZonedDateTime方法:

代码语言:javascript复制
package defaultmethods;

import java.time.*;
import java.lang.*;
import java.util.*;

public class TestSimpleTimeClient {
    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println("Current time: "   myTimeClient.toString());
        System.out.println("Time in California: "  
            myTimeClient.getZonedDateTime("Blah blah").toString());
    }
}

扩展包含默认方法的接口

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

  • 完全不提及默认方法,让您扩展的接口继承默认方法。
  • 重新声明默认方法,使其为abstract
  • 重新定义默认方法,覆盖它。

假设您扩展了接口TimeClient如下:

代码语言:javascript复制
public interface AnotherTimeClient extends TimeClient { }

任何实现接口AnotherTimeClient的类都将具有默认方法TimeClient.getZonedDateTime指定的实现。

假设您扩展了接口TimeClient如下:

代码语言:javascript复制
public interface AbstractZoneTimeClient extends TimeClient {
    public ZonedDateTime getZonedDateTime(String zoneString);
}

任何实现接口AbstractZoneTimeClient的类都必须实现方法getZonedDateTime;这个方法是一个abstract方法,就像接口中的所有其他非默认(非静态)方法一样。

假设您扩展了接口TimeClient如下:

代码语言:javascript复制
public interface HandleInvalidTimeZoneClient extends TimeClient {
    default public ZonedDateTime getZonedDateTime(String zoneString) {
        try {
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.of(zoneString)); 
        } catch (DateTimeException e) {
            System.err.println("Invalid zone ID: "   zoneString  
                "; using the default time zone instead.");
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.systemDefault());
        }
    }
}

任何实现接口HandleInvalidTimeZoneClient的类都将使用此接口指定的getZonedDateTime实现,而不是接口TimeClient指定的实现。

静态方法

除了默认方法之外,您还可以在接口中定义静态方法。(静态方法是与定义它的类相关联的方法,而不是与任何对象相关联的方法。类的每个实例共享其静态方法。)这使您更容易在库中组织辅助方法;您可以将特定于接口的静态方法保留在同一接口中,而不是在单独的类中。以下示例定义了一个静态方法,用于检索与时区标识符对应的ZoneId对象;如果没有与给定标识符对应的ZoneId对象,则使用系统默认时区。(因此,您可以简化方法getZonedDateTime):

代码语言:javascript复制
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 表达式作为参数的方法。本节演示了如何通过默认方法和静态方法增强了Comparator接口。

CardDeck类视为问题和练习:类中描述的那样。此示例将CardDeck类重写为接口。Card接口包含两个enum类型(SuitRank)和两个抽象方法(getSuitgetRank):

代码语言:javascript复制
package defaultmethods;

public interface Card extends Comparable<Card> {

    public enum Suit { 
        DIAMONDS (1, "Diamonds"), 
        CLUBS    (2, "Clubs"   ), 
        HEARTS   (3, "Hearts"  ), 
        SPADES   (4, "Spades"  );

        private final int value;
        private final String text;
        Suit(int value, String text) {
            this.value = value;
            this.text = text;
        }
        public int value() {return value;}
        public String text() {return text;}
    }

    public enum Rank { 
        DEUCE  (2 , "Two"  ),
        THREE  (3 , "Three"), 
        FOUR   (4 , "Four" ), 
        FIVE   (5 , "Five" ), 
        SIX    (6 , "Six"  ), 
        SEVEN  (7 , "Seven"),
        EIGHT  (8 , "Eight"), 
        NINE   (9 , "Nine" ), 
        TEN    (10, "Ten"  ), 
        JACK   (11, "Jack" ),
        QUEEN  (12, "Queen"), 
        KING   (13, "King" ),
        ACE    (14, "Ace"  );
        private final int value;
        private final String text;
        Rank(int value, String text) {
            this.value = value;
            this.text = text;
        }
        public int value() {return value;}
        public String text() {return text;}
    }

    public Card.Suit getSuit();
    public Card.Rank getRank();
}

Deck接口包含各种操作牌组中卡片的方法:

代码语言:javascript复制
package defaultmethods; 

import java.util.*;
import java.util.stream.*;
import java.lang.*;

public interface Deck {

    List<Card> getCards();
    Deck deckFactory();
    int size();
    void addCard(Card card);
    void addCards(List<Card> cards);
    void addDeck(Deck deck);
    void shuffle();
    void sort();
    void sort(Comparator<Card> c);
    String deckToString();

    Map<Integer, Deck> deal(int players, int numberOfCards)
        throws IllegalArgumentException;

}

PlayingCard实现了接口Card,而类StandardDeck实现了接口Deck

StandardDeck按如下方式实现了抽象方法Deck.sort

代码语言:javascript复制
public class StandardDeck implements Deck {

    private List<Card> entireDeck;

    // ...

    public void sort() {
        Collections.sort(entireDeck);
    }

    // ...
}

方法Collections.sort对实现接口Comparable的元素类型为List的实例进行排序。成员entireDeck是一个List的实例,其元素类型为扩展了ComparableCard类型。类PlayingCard按如下方式实现了Comparable.compareTo方法:

代码语言:javascript复制
public int hashCode() {
    return ((suit.value()-1)*13) rank.value();
}

public int compareTo(Card o) {
    return this.hashCode() - o.hashCode();
}

方法compareTo导致方法StandardDeck.sort()首先按花色,然后按等级对牌组进行排序。

如果你想先按等级,然后按花色对牌组进行排序怎么办?你需要实现Comparator接口来指定新的排序标准,并使用方法sort(List<T> list, Comparator<? super T> c)(包含Comparator参数的sort方法版本)。你可以在类StandardDeck中定义以下方法:

代码语言:javascript复制
public void sort(Comparator<Card> c) {
    Collections.sort(entireDeck, c);
}  

有了这个方法,你可以指定方法Collections.sort如何对Card类的实例进行排序。一种方法是实现Comparator接口来指定你希望如何对牌进行排序。示例SortByRankThenSuit就是这样做的:

代码语言:javascript复制
package defaultmethods;

import java.util.*;
import java.util.stream.*;
import java.lang.*;

public class SortByRankThenSuit implements Comparator<Card> {
    public int compare(Card firstCard, Card secondCard) {
        int compVal =
            firstCard.getRank().value() - secondCard.getRank().value();
        if (compVal != 0)
            return compVal;
        else
            return firstCard.getSuit().value() - secondCard.getSuit().value(); 
    }
}

以下调用首先按等级,然后按花色对扑克牌组进行排序:

代码语言:javascript复制
StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(new SortByRankThenSuit());

然而,这种方法太啰嗦了;如果你可以只指定排序标准而避免创建多个排序实现,那将更好。假设你是编写Comparator接口的开发人员。你可以向Comparator接口添加哪些默认或静态方法,以使其他开发人员更容易指定排序标准?

首先,假设你想按等级对扑克牌组进行排序,而不考虑花色。你可以如下调用StandardDeck.sort方法:

代码语言:javascript复制
StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
    (firstCard, secondCard) ->
        firstCard.getRank().value() - secondCard.getRank().value()
); 

因为Comparator接口是一个函数式接口,您可以使用 lambda 表达式作为sort方法的参数。在这个例子中,lambda 表达式比较两个整数值。

如果您的开发人员只需调用方法Card.getRank就能创建一个Comparator实例,那将更简单。特别是,如果您的开发人员可以创建一个比较任何可以从getValuehashCode等方法返回数值的对象的Comparator实例,那将很有帮助。Comparator接口已经通过静态方法comparing增强了这种能力:

代码语言:javascript复制
myDeck.sort(Comparator.comparing((card) -> card.getRank()));  

在这个例子中,您可以使用方法引用:

代码语言:javascript复制
myDeck.sort(Comparator.comparing(Card::getRank));  

这种调用更好地演示了如何指定不同的排序标准并避免创建多个排序实现。

Comparator接口已经通过其他版本的静态方法comparing(如comparingDoublecomparingLong)进行了增强,使您能够创建比较其他数据类型的Comparator实例。

假设您的开发人员想要创建一个可以根据多个标准比较对象的Comparator实例。例如,如何先按等级,然后按花色对扑克牌进行排序?与以前一样,您可以使用 lambda 表达式来指定这些排序标准:

代码语言:javascript复制
StandardDeck myDeck = new StandardDeck();
myDeck.shuffle();
myDeck.sort(
    (firstCard, secondCard) -> {
        int compare =
            firstCard.getRank().value() - secondCard.getRank().value();
        if (compare != 0)
            return compare;
        else
            return firstCard.getSuit().value() - secondCard.getSuit().value();
    }      
); 

如果您的开发人员可以从一系列Comparator实例构建一个Comparator实例,那将更简单。Comparator接口已经通过默认方法thenComparing增强了这种能力:

代码语言:javascript复制
myDeck.sort(
    Comparator
        .comparing(Card::getRank)
        .thenComparing(Comparator.comparing(Card::getSuit)));

Comparator接口已经通过其他版本的默认方法thenComparing(如thenComparingDoublethenComparingLong)进行了增强,使您能够构建比较其他数据类型的Comparator实例。

假设您的开发人员想要创建一个Comparator实例,使他们能够以相反的顺序对对象集合进行排序。例如,如何按照牌面从大到小的顺序对扑克牌进行排序,从 A 到 2(而不是从 2 到 A)?与以前一样,您可以指定另一个 lambda 表达式。但是,如果开发人员可以通过调用方法来反转现有的Comparator,那将更简单。Comparator接口已经通过默认方法reversed增强了这种能力:

代码语言:javascript复制
myDeck.sort(
    Comparator.comparing(Card::getRank)
        .reversed()
        .thenComparing(Comparator.comparing(Card::getSuit)));

这个例子演示了如何通过默认方法、静态方法、lambda 表达式和方法引用增强了Comparator接口,以创建更具表现力的库方法,程序员可以通过查看它们的调用方式快速推断出其功能。使用这些构造来增强您的库中的接口。

接口概要

docs.oracle.com/javase/tutorial/java/IandI/summary-interface.html

接口声明可以包含方法签名、默认方法、静态方法和常量定义。唯一有实现的方法是默认方法和静态方法。

实现接口的类必须实现接口中声明的所有方法。

接口名称可以在任何需要类型的地方使用。

问题和练习:接口

原文:docs.oracle.com/javase/tutorial/java/IandI/QandE/interfaces-questions.html

问题

一个实现java.lang.CharSequence接口的类需要实现哪些方法?

以下接口有什么问题?

代码语言:javascript复制
public interface SomethingIsWrong {
    void aMethod(int aValue){
        System.out.println("Hi Mom");
    }
}

修复问题 2 中的接口。

以下接口是否有效?

代码语言:javascript复制
public interface Marker {
}

练习

  1. 编写一个实现java.lang包中CharSequence接口的类。你的实现应该将字符串倒序返回。从本书中选择一句话作为数据。编写一个小的main方法来测试你的类;确保调用所有四个方法。
  2. 假设你已经编写了一个定期通知其客户端当前日期和时间的时间服务器。编写一个接口,服务器可以使用它来强制执行特定的协议。

检查你的答案。

继承

原文:docs.oracle.com/javase/tutorial/java/IandI/subclasses.html

在前面的课程中,您已经多次看到继承的提及。在 Java 语言中,类可以从其他类派生,从而继承那些类的字段和方法。


定义: 从另一个类派生的类称为子类(也称为派生类扩展类子类)。从子类派生的类称为超类(也称为基类父类)。

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

类可以从派生自其他类的类派生,而这些类又从其他类派生,依此类推,最终都是从顶级类Object派生而来。这样的类被称为继承自继承链中一直延伸到Object的所有类。


继承的概念简单而强大:当您想要创建一个新类,并且已经有一个包含您想要的一些代码的类时,您可以从现有类派生您的新类。通过这样做,您可以重用现有类的字段和方法,而无需自己编写(和调试!)它们。

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

Java 平台类层次结构

Object类,定义在java.lang包中,定义并实现了所有类共有的行为,包括您编写的类。在 Java 平台中,许多类直接从Object派生,其他类从其中一些类派生,依此类推,形成一个类的层次结构。

[外链图片转存中…(img-l02ciPtq-1712902080546)]

Java 平台中的所有类都是 Object 的子类

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

继承的示例

这是一个可能实现的Bicycle类的示例代码,该代码在类和对象课程中提供:

代码语言:javascript复制
public class Bicycle {

    // the Bicycle class has three *fields*
    public int cadence;
    public int gear;
    public int speed;

    // the Bicycle class has one *constructor*
    public Bicycle(int startCadence, int startSpeed, int startGear) {
        gear = startGear;
        cadence = startCadence;
        speed = startSpeed;
    }

    // the Bicycle class has four *methods*
    public void setCadence(int newValue) {
        cadence = newValue;
    }

    public void setGear(int newValue) {
        gear = newValue;
    }

    public void applyBrake(int decrement) {
        speed -= decrement;
    }

    public void speedUp(int increment) {
        speed  = increment;
    }

}

一个MountainBike类的类声明,它是Bicycle的子类,可能如下所示:

代码语言:javascript复制
public class MountainBike extends Bicycle {

    // the MountainBike subclass adds one *field*
    public int seatHeight;

    // the MountainBike subclass has one *constructor*
    public MountainBike(int startHeight,
                        int startCadence,
                        int startSpeed,
                        int startGear) {
        super(startCadence, startSpeed, startGear);
        seatHeight = startHeight;
    }   

    // the MountainBike subclass adds one *method*
    public void setHeight(int newValue) {
        seatHeight = newValue;
    }   
}

MountainBike继承了Bicycle的所有字段和方法,并添加了字段seatHeight和一个设置它的方法。除了构造函数外,就好像你完全从头开始编写了一个新的MountainBike类,有四个字段和五个方法。但是,你不必做所有的工作。如果Bicycle类中的方法很复杂并且花费了大量时间来调试,这将特别有价值。

在子类中可以做什么

子类继承其父类的所有publicprotected成员,无论子类位于何种包中。如果子类与其父类在同一包中,它还会继承父类的package-private成员。你可以直接使用继承的成员,替换它们,隐藏它们,或者用新成员补充它们:

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

本课程的以下部分将扩展这些主题。

超类中的私有成员

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

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

对象转型

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

代码语言:javascript复制
public MountainBike myBike = new MountainBike();

那么myBike的类型是MountainBike

MountainBike是从BicycleObject继承而来的。因此,MountainBike是一个Bicycle,也是一个Object,可以在需要BicycleObject对象的任何地方使用。

反之未必成立:Bicycle可能是MountainBike,但不一定。同样,Object可能是BicycleMountainBike,但不一定。

转型展示了在继承和实现允许的对象之间使用一个类型的对象代替另一个类型的对象。例如,如果我们写

代码语言:javascript复制
Object obj = new MountainBike();

那么obj既是一个Object,也是一个MountainBike(直到obj被分配为不是MountainBike的另一个对象为止)。这被称为隐式转换

另一方面,如果我们写

代码语言:javascript复制
MountainBike myBike = obj;

我们会得到一个编译时错误,因为编译器不知道obj是一个MountainBike。然而,我们可以告诉编译器,我们承诺将一个MountainBike分配给obj,通过显式转换

代码语言:javascript复制
MountainBike myBike = (MountainBike)obj;

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


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

代码语言:javascript复制
if (obj instanceof MountainBike) {
    MountainBike myBike = (MountainBike)obj;
}

这里的instanceof运算符验证obj指向一个MountainBike,这样我们可以进行转换,并确保不会抛出运行时异常。


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

原文:docs.oracle.com/javase/tutorial/java/IandI/multipleinheritance.html

类和接口之间的一个重要区别是类可以有字段,而接口不能。此外,您可以实例化一个类来创建一个对象,而接口不能这样做。正如在什么是对象?一节中所解释的,对象将其状态存储在字段中,这些字段在类中定义。Java 编程语言不允许您扩展多个类的一个原因是为了避免状态的多重继承问题,即从多个类继承字段的能力。例如,假设您能够定义一个新类,该类扩展多个类。当您通过实例化该类创建对象时,该对象将从所有超类继承字段。如果不同超类的方法或构造函数实例化相同字段会怎样?哪个方法或构造函数将优先?由于接口不包含字段,您不必担心由于状态的多重继承而导致的问题。

实现的多重继承是从多个类继承方法定义的能力。这种类型的多重继承会引发问题,例如名称冲突和模糊性。当支持这种类型多重继承的编程语言的编译器遇到包含相同名称方法的超类时,有时无法确定要访问或调用哪个成员或方法。此外,程序员可能会通过向超类添加新方法无意中引入名称冲突。默认方法引入了一种实现的多重继承形式。一个类可以实现多个接口,这些接口可以包含具有相同名称的默认方法。Java 编译器提供了一些规则来确定特定类使用哪个默认方法。

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

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

覆盖和隐藏方法

原文:docs.oracle.com/javase/tutorial/java/IandI/override.html

实例方法

子类中具有与超类中实例方法相同签名(名称,以及其参数的数量和类型)和返回类型的实例方法覆盖了超类的方法。

子类覆盖方法的能力允许一个类从一个行为“足够接近”的超类继承,然后根据需要修改行为。覆盖方法具有与其覆盖的方法相同的名称、参数数量和类型以及返回类型。覆盖方法还可以返回被覆盖方法返回类型的子类型。这个子类型被称为协变返回类型

在覆盖方法时,您可能希望使用@Override注解,指示编译器您打算覆盖超类中的方法。如果由于某种原因,编译器检测到该方法在任何一个超类中不存在,则会生成一个错误。有关@Override的更多信息,请参见Annotations

静态方法

如果一个子类定义了一个与超类中静态方法相同签名的静态方法,则子类中的方法隐藏超类中的方法。

隐藏静态方法和覆盖实例方法之间的区别具有重要的影响:

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

考虑一个包含两个类的示例。第一个是Animal,包含一个实例方法和一个静态方法:

代码语言:javascript复制
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");
    }
}

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

代码语言:javascript复制
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()

这个程序的输出如下:

代码语言:javascript复制
The static method in Animal
The instance method in Cat

如约定,调用的隐藏静态方法的版本是在超类中的版本,调用的覆盖实例方法的版本是在子类中的版本。

接口方法

默认方法和抽象方法在接口中像实例方法一样被继承。然而,当一个类或接口的超类型提供了多个具有相同签名的默认方法时,Java 编译器遵循继承规则来解决名称冲突。这些规则受以下两个原则驱动:

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

考虑以下类和接口:

代码语言:javascript复制
public class Horse {
    public String identifyMyself() {
        return "I am a horse.";
    }
}
代码语言:javascript复制
public interface Flyer {
    default public String identifyMyself() {
        return "I am able to fly.";
    }
}
代码语言:javascript复制
public interface Mythical {
    default public String identifyMyself() {
        return "I am a mythical creature.";
    }
}
代码语言:javascript复制
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.

已经被其他候选方法覆盖的方法将被忽略。当超类型共享一个共同的祖先时,就会出现这种情况。

考虑以下接口和类:

代码语言:javascript复制
public interface Animal {
    default public String identifyMyself() {
        return "I am an animal.";
    }
}
代码语言:javascript复制
public interface EggLayer extends Animal {
    default public String identifyMyself() {
        return "I am able to lay eggs.";
    }
}
代码语言:javascript复制
public interface FireBreather extends Animal { }
代码语言:javascript复制
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 编译器会产生编译错误。您必须显式覆盖超类型方法。

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

代码语言:javascript复制
public interface OperateCar {
    // ...
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}
代码语言:javascript复制
public interface FlyCar {
    // ...
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}

实现OperateCarFlyCar的类必须覆盖方法startEngine。您可以使用super关键字调用任何默认实现中的任何一个。

代码语言:javascript复制
public class FlyingCar implements OperateCar, FlyCar {
    // ...
    public int startEngine(EncryptedKey key) {
        FlyCar.super.startEngine(key);
        OperateCar.super.startEngine(key);
    }
}

super之前的名称(在本例中为FlyCarOperateCar)必须引用直接定义或继承了被调用方法的超接口。这种形式的方法调用不仅限于区分包含具有相同签名的默认方法的多个实现接口。您可以使用super关键字在类和接口中调用默认方法。

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

代码语言:javascript复制
public interface Mammal {
    String identifyMyself();
}
代码语言:javascript复制
public class Horse {
    public String identifyMyself() {
        return "I am a horse.";
    }
}
代码语言:javascript复制
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中同名的抽象方法。

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

修饰符

覆盖方法的访问修饰符可以允许更多的访问权限,但不能少于被覆盖方法的访问权限。例如,超类中的受保护实例方法可以在子类中变为公共方法,但不能变为私有方法。

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

总结

以下表格总结了当您定义一个与超类中方法具有相同签名的方法时会发生什么。

定义一个与超类方法具有相同签名的方法

超类实例方法

超类静态方法

子类实例方法

覆盖

生成编译时错误

子类静态方法

生成编译时错误

隐藏


注意:在子类中,您可以重载从超类继承的方法。这样重载的方法既不隐藏也不覆盖超类实例方法——它们是子类独有的新方法。

0 人点赞