多态性
原文:
docs.oracle.com/javase/tutorial/java/IandI/polymorphism.html
多态性的词典定义指的是生物学中的一个原则,即一个生物体或物种可以具有许多不同的形式或阶段。这个原则也可以应用于面向对象编程和像 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
方法。除了之前提供的信息外,输出还包括有关悬架的额外数据。
接下来,创建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
变量的测试程序。每个变量分配给三个自行车类中的一个。然后打印每个变量。
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)调用与每个变量引用的对象相对应的方法。它不调用变量类型定义的方法。这种行为称为虚拟方法调用,展示了 Java 语言中重要的多态性特性的一个方面。
隐藏字段
原文:
docs.oracle.com/javase/tutorial/java/IandI/hidevariables.html
在一个类中,如果一个字段与超类中的字段同名,即使它们的类型不同,该字段也会隐藏超类的字段。在子类中,无法通过简单名称引用超类中的字段。相反,必须通过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
如所示。编译和执行Subclass
将打印以下内容:
Printed in Superclass. Printed in Subclass
子类构造函数
以下示例说明了如何使用super
关键字调用超类的构造函数。回想一下Bicycle
示例中MountainBike
是Bicycle
的子类。这是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
的构造函数。事实上,情况确实如此。这被称为构造函数链,当存在长串的类继承时,您需要注意这一点。
作为超类的对象
原文:
docs.oracle.com/javase/tutorial/java/IandI/objectclass.html
Object
类,位于java.lang
包中,位于类层次结构树的顶部。每个类都是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()
为对象返回一个哈希码值。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 { String ISBN; public String getISBN() { return ISBN; } 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
引用了两个不同的对象,这个程序会显示 objects are equal
。它们被认为是相等的,因为比较的对象包含相同的 ISBN 号码。
如果身份运算符对于你的类不合适,你应该始终重写 equals()
方法。
**注意:**如果你重写了 equals()
,你必须同时重写 hashCode()
。
finalize()
方法
Object
类提供了一个回调方法 finalize()
,当对象变为垃圾时可能会被调用。Object
的 finalize()
实现什么也不做—你可以重写 finalize()
来进行清理,比如释放资源。
finalize()
方法可能会被系统自动调用,但是何时调用,甚至是否调用都是不确定的。因此,不要依赖这个方法来清理资源。例如,如果在执行 I/O 操作后没有关闭文件描述符,并且期望 finalize()
为您关闭它们,那么可能会耗尽文件描述符。相反,使用 try
-with 资源语句来自动关闭应用程序的资源。参见 The try-with-resources Statement 和 Finalization and Weak, Soft, and Phantom References 中的 Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide。
getClass() 方法
你不能重写 getClass
。
getClass()
方法返回一个Class
对象,该对象有一些方法可以获取关于类的信息,比如它的名称(getSimpleName()
)、它的父类(getSuperclass()
)和它实现的接口(getInterfaces()
)。例如,下面的方法获取并显示对象的类名:
void printClassName(Object obj) { System.out.println("The object's" + " class is " + obj.getClass().getSimpleName()); }
Class
类,位于java.lang
包中,有大量的方法(超过 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
关键字表示该方法不能被子类重写。Object
类就是这样做的——它的一些方法是final
的。
如果一个方法有一个不应该被更改的实现,并且对对象的一致状态至关重要,你可能希望将其设置为最终方法。例如,你可能想要将ChessAlgorithm
类中的getFirstPlayer
方法设置为最终方法:
class ChessAlgorithm { enum ChessPlayer { WHITE, BLACK } ... final ChessPlayer getFirstPlayer() { return ChessPlayer.WHITE; } ... }
从构造函数中调用的方法通常应该声明为最终方法。如果构造函数调用一个非最终方法,子类可能重新定义该方法,导致意想不到或不希望的结果。
请注意,你也可以声明整个类为最终类。声明为最终类的类不能被子类化。例如,当创建像String
类这样的不可变类时,这是非常有用的。
抽象方法和类
抽象类 是一个声明为 abstract
的类—它可能包含或不包含抽象方法。抽象类不能被实例化,但可以被子类化。
抽象方法 是一种声明但没有实现的方法(没有大括号,后面跟着一个分号),如下所示:
abstract void moveTo(double deltaX, double deltaY);
如果一个类包含抽象方法,那么这个类本身必须声明为 abstract
,如下所示:
public abstract class GraphicObject { // declare fields // declare nonabstract methods abstract void draw(); }
当一个抽象类被子类化时,子类通常为其父类中的所有抽象方法提供实现。但是,如果没有提供实现,则子类也必须声明为 abstract
。
注意: 接口 中的方法(参见 接口 部分)如果没有声明为默认或静态,则隐式是抽象的,因此不需要使用 abstract
修饰符。 (可以使用,但是不必要。)
抽象类与接口的比较
抽象类类似于接口。你不能实例化它们,它们可能包含一些声明有或没有实现的方法。然而,使用抽象类,你可以声明非静态和非最终的字段,并定义公共、受保护和私有的具体方法。使用接口,所有字段都自动是公共的、静态的和最终的,你声明或定义的所有方法(作为默认方法)都是公共的。此外,你只能扩展一个类,无论它是否是抽象的,而你可以实现任意数量的接口。
你应该使用抽象类还是接口?
- 如果你的情况符合以下任何一种情况,请考虑使用抽象类:
- 你想在几个密切相关的类之间共享代码。
- 你期望扩展你的抽象类的类有许多共同的方法或字段,或者需要除了 public 之外的访问修饰符(比如 protected 和 private)。
- 你想声明非静态或非最终字段。这使你能够定义可以访问和修改它们所属对象状态的方法。
- 如果你的情况符合以下任何一种情况,请考虑使用接口:
- 你期望不相关的类会实现你的接口。例如,
Comparable
和Cloneable
这些接口被许多不相关的类实现。 - 你想指定特定数据类型的行为,但不关心谁实现它的行为。
- 你想利用类型的多重继承。
JDK 中抽象类的一个示例是AbstractMap
,它是集合框架的一部分。它的子类(包括 HashMap
、TreeMap
和 ConcurrentHashMap
)共享许多方法(包括 get
、put
、isEmpty
、containsKey
和 containsValue
),这些方法是由 AbstractMap
定义的。
JDK 中实现多个接口的一个类的示例是HashMap
,它实现了接口 Serializable
、Cloneable
和 Map
。通过阅读这些接口列表,你可以推断出 HashMap
的实例(无论是哪个开发者或公司实现的类)可以被克隆,是可序列化的(这意味着它可以被转换为字节流;参见可序列化对象部分),并且具有映射功能。此外,Map
接口已经通过许多默认方法(如 merge
和 forEach
)进行了增强,而旧类实现了该接口的类不必定义这些方法。
请注意,许多软件库同时使用抽象类和接口;HashMap
类实现了多个接口,并且还扩展了抽象类 AbstractMap
。
抽象类示例
在面向对象的绘图应用程序中,你可以绘制圆、矩形、线条、贝塞尔曲线和许多其他图形对象。这些对象都具有某些状态(例如:位置、方向、线条颜色、填充颜色)和行为(例如:moveTo、rotate、resize、draw)是共同的。其中一些状态和行为对所有图形对象都是相同的(例如:位置、填充颜色和 moveTo)。其他需要不同实现(例如,resize 或 draw)。所有 GraphicObject
必须能够自行绘制或调整大小;它们只是在如何执行这些操作上有所不同。这是一个抽象超类的完美情况。你可以利用相似之处,并声明所有图形对象都继承自相同的抽象父对象(例如 GraphicObject
),如下图所示。
类 Rectangle、Line、Bezier 和 Circle 继承自 GraphicObject
首先,声明一个抽象类 GraphicObject
,以提供所有子类完全共享的成员变量和方法,例如当前位置和 moveTo
方法。GraphicObject
还声明了抽象方法,例如 draw
或 resize
,这些方法需要所有子类实现,但必须以不同的方式实现。GraphicObject
类可能如下所示:
abstract class GraphicObject { int x, y; ... void moveTo(int newX, int newY) { ... } abstract void draw(); abstract void resize(); }
每个非抽象子类 GraphicObject
,如 Circle
和 Rectangle
,必须提供 draw
和 resize
方法的实现:
class Circle extends GraphicObject { void draw() { ... } void resize() { ... } } class Rectangle extends GraphicObject { void draw() { ... } void resize() { ... } }
Java 中文官方教程 2022 版(五)(2)https://developer.aliyun.com/article/1486295