Java 中文官方教程 2022 版(四)(3)https://developer.aliyun.com/article/1486288
假设您的开发人员想要创建一个可以根据多个标准比较对象的Comparator
实例。例如,如何先按等级,然后按花色对扑克牌进行排序?与以前一样,您可以使用 lambda 表达式来指定这些排序标准:
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
增强了这种能力:
myDeck.sort( Comparator .comparing(Card::getRank) .thenComparing(Comparator.comparing(Card::getSuit)));
Comparator
接口已经通过其他版本的默认方法thenComparing
(如thenComparingDouble
和thenComparingLong
)进行了增强,使您能够构建比较其他数据类型的Comparator
实例。
假设您的开发人员想要创建一个Comparator
实例,使他们能够以相反的顺序对对象集合进行排序。例如,如何按照牌面从大到小的顺序对扑克牌进行排序,从 A 到 2(而不是从 2 到 A)?与以前一样,您可以指定另一个 lambda 表达式。但是,如果开发人员可以通过调用方法来反转现有的Comparator
,那将更简单。Comparator
接口已经通过默认方法reversed
增强了这种能力:
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
接口的类需要实现哪些方法? - 以下接口有什么问题?
public interface SomethingIsWrong { void aMethod(int aValue){ System.out.println("Hi Mom"); } }
- 修复问题 2 中的接口。
- 以下接口是否有效?
public interface Marker { }
练习
- 编写一个实现
java.lang
包中CharSequence
接口的类。你的实现应该将字符串倒序返回。从本书中选择一句话作为数据。编写一个小的main
方法来测试你的类;确保调用所有四个方法。 - 假设你已经编写了一个定期通知其客户端当前日期和时间的时间服务器。编写一个接口,服务器可以使用它来强制执行特定的协议。
检查你的答案。
继承
原文:
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
类的示例代码,该代码在类和对象课程中提供:
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
的子类,可能如下所示:
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
类中的方法很复杂并且花费了大量时间来调试,这将特别有价值。
在子类中可以做什么
子类继承其父类的所有public和protected成员,无论子类位于何种包中。如果子类与其父类在同一包中,它还会继承父类的package-private成员。你可以直接使用继承的成员,替换它们,隐藏它们,或者用新成员补充它们:
- 继承的字段可以直接使用,就像任何其他字段一样。
- 你可以在子类中声明一个与超类中相同名称的字段,从而隐藏它(不建议)。
- 你可以在子类中声明超类中没有的新字段。
- 继承的方法可以直接使用。
- 你可以在子类中编写一个新的实例方法,其签名与超类中的方法相同,从而覆盖它。
- 你可以在子类中编写一个新的静态方法,其签名与超类中的方法相同,从而隐藏它。
- 你可以在子类中声明超类中没有的新方法。
- 你可以编写一个子类构造函数,隐式地或使用关键字
super
调用超类的构造函数。
本课程的以下部分将扩展这些主题。
超类中的私有成员
子类不继承其父类的private
成员。但是,如果超类有用于访问其私有字段的公共或受保护方法,子类也可以使用这些方法。
嵌套类可以访问其封闭类的所有私有成员—包括字段和方法。因此,一个由子类继承的公共或受保护的嵌套类间接访问了超类的所有私有成员。
对象转型
我们已经看到,一个对象的数据类型是它实例化的类的数据类型。例如,如果我们写
public MountainBike myBike = new MountainBike();
那么myBike
的类型是MountainBike
。
MountainBike
是从Bicycle
和Object
继承而来的。因此,MountainBike
是一个Bicycle
,也是一个Object
,可以在需要Bicycle
或Object
对象的任何地方使用。
反之未必成立:Bicycle
可能是MountainBike
,但不一定。同样,Object
可能是Bicycle
或MountainBike
,但不一定。
转型展示了在继承和实现允许的对象之间使用一个类型的对象代替另一个类型的对象。例如,如果我们写
Object obj = new MountainBike();
那么obj
既是一个Object
,也是一个MountainBike
(直到obj
被分配为不是MountainBike
的另一个对象为止)。这被称为隐式转换。
另一方面,如果我们写
MountainBike myBike = obj;
我们会得到一个编译时错误,因为编译器不知道obj
是一个MountainBike
。然而,我们可以告诉编译器,我们承诺将一个MountainBike
分配给obj
,通过显式转换:
MountainBike myBike = (MountainBike)obj;
这个转换插入了一个运行时检查,以确保obj
被分配为MountainBike
,这样编译器可以安全地假定obj
是一个MountainBike
。如果在运行时obj
不是MountainBike
,则会抛出异常。
注意: 您可以使用instanceof
运算符对特定对象的类型进行逻辑测试。这可以避免由于不正确的转换而导致运行时错误。例如:
if (obj instanceof MountainBike) { MountainBike myBike = (MountainBike)obj; }
这里的instanceof
运算符验证obj
指向一个MountainBike
,这样我们可以进行转换,并确保不会抛出运行时异常。
状态、实现和类型的多重继承
原文:
docs.oracle.com/javase/tutorial/java/IandI/multipleinheritance.html
类和接口之间的一个重要区别是类可以有字段,而接口不能。此外,您可以实例化一个类来创建一个对象,而接口不能这样做。正如在什么是对象?一节中所解释的,对象将其状态存储在字段中,这些字段在类中定义。Java 编程语言不允许您扩展多个类的一个原因是为了避免状态的多重继承问题,即从多个类继承字段的能力。例如,假设您能够定义一个新类,该类扩展多个类。当您通过实例化该类创建对象时,该对象将从所有超类继承字段。如果不同超类的方法或构造函数实例化相同字段会怎样?哪个方法或构造函数将优先?由于接口不包含字段,您不必担心由于状态的多重继承而导致的问题。
实现的多重继承是从多个类继承方法定义的能力。这种类型的多重继承会引发问题,例如名称冲突和模糊性。当支持这种类型多重继承的编程语言的编译器遇到包含相同名称方法的超类时,有时无法确定要访问或调用哪个成员或方法。此外,程序员可能会通过向超类添加新方法无意中引入名称冲突。默认方法引入了一种实现的多重继承形式。一个类可以实现多个接口,这些接口可以包含具有相同名称的默认方法。Java 编译器提供了一些规则来确定特定类使用哪个默认方法。
Java 编程语言支持类型的多重继承,即一个类可以实现多个接口的能力。一个对象可以有多种类型:它自己类的类型以及类实现的所有接口的类型。这意味着如果一个变量声明为接口的类型,那么它的值可以引用任何实例化自实现该接口的任何类的对象。这在将接口用作类型一节中讨论。
与多重继承实现一样,一个类可以继承在其扩展的接口中定义的方法的不同实现(作为默认或静态)。在这种情况下,编译器或用户必须决定使用哪一个。
覆盖和隐藏方法
实例方法
子类中具有与超类中实例方法相同签名(名称,以及其参数的数量和类型)和返回类型的实例方法覆盖了超类的方法。
子类覆盖方法的能力允许一个类从一个行为“足够接近”的超类继承,然后根据需要修改行为。覆盖方法具有与其覆盖的方法相同的名称、参数数量和类型以及返回类型。覆盖方法还可以返回被覆盖方法返回类型的子类型。这个子类型被称为协变返回类型。
在覆盖方法时,您可能希望使用@Override
注解,指示编译器您打算覆盖超类中的方法。如果由于某种原因,编译器检测到该方法在任何一个超类中不存在,则会生成一个错误。有关@Override
的更多信息,请参见Annotations
。
静态方法
如果一个子类定义了一个与超类中静态方法相同签名的静态方法,则子类中的方法隐藏超类中的方法。
隐藏静态方法和覆盖实例方法之间的区别具有重要的影响:
- 调用的覆盖实例方法的版本是在子类中的版本。
- 调用的隐藏静态方法的版本取决于它是从超类还是从子类调用的。
考虑一个包含两个类的示例。第一个是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"); } }
第二个类,Cat
,是Animal
的一个子类:
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
如约定,调用的隐藏静态方法的版本是在超类中的版本,调用的覆盖实例方法的版本是在子类中的版本。
接口方法
默认方法和抽象方法在接口中像实例方法一样被继承。然而,当一个类或接口的超类型提供了多个具有相同签名的默认方法时,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
)必须引用直接定义或继承了被调用方法的超接口。这种形式的方法调用不仅限于区分包含具有相同签名的默认方法的多个实现接口。您可以使用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
中同名的抽象方法。
注意:接口中的静态方法不会被继承。
修饰符
覆盖方法的访问修饰符可以允许更多的访问权限,但不能少于被覆盖方法的访问权限。例如,超类中的受保护实例方法可以在子类中变为公共方法,但不能变为私有方法。
如果您尝试将超类中的实例方法更改为子类中的静态方法,或者反之,则会收到编译时错误。
总结
以下表格总结了当您定义一个与超类中方法具有相同签名的方法时会发生什么。
定义一个与超类方法具有相同签名的方法
超类实例方法 | 超类静态方法 | |
子类实例方法 | 覆盖 | 生成编译时错误 |
子类静态方法 | 生成编译时错误 | 隐藏 |
注意:在子类中,您可以重载从超类继承的方法。这样重载的方法既不隐藏也不覆盖超类实例方法——它们是子类独有的新方法。