35 KiB
1. Inheritance继承 and Polymorphism多态
Suppose you will define classes to model circles, rectangles, and triangles. These classes have many common features. What is the best way to design these classes so to avoid redundancy? The answer is to use inheritance.
1.1. 如何降低描述冗余
如何降低代码的冗余?例如在C语言中,通过什么方式降低代码的冗余。
在C语言中,降低代码冗余(增加代码复用性),一般使用函数来实现;但是函数是一个高纬度的抽象,和我们日常生活联系并不紧密。那么我们考虑日常生活中的例子。
通常我们描述一猫,只用说明猫是:界:动物界;门:脊椎动亚门;纲:哺乳纲;目:食肉目;科:猫科;属:猫属;种:猫种。
上述的图表示物种分类的一部分。这样描述一个物种的分类有很多好处:
- 清楚表明了祖先与后代的关系(继承树);
- 后代具备祖先的所有特性;
- 明确不同种之间的关系。
如果大家都清楚上述的规则,当我们讨论猫种的时候,就默认猫种具备祖先的所有特性,而不必再说明猫种是脊椎动物,胎生等这些它祖先所具备的特性。这种特性在Java种的类种也存在,这就是继承。
1.2. Java中的继承
例如要在平面上描述两个类,圆和矩形,那么这两个类好像有一些基本的特性是一致的。例如,填充颜色color,是否填充 filled,对象创建时间 dateCreated等。另外,需要对上述属性设置或者读取的方法:getColor(),setColor(),isFilled(),setFilled()等。
如果每个类都需要描述所有的这些特性好像有点浪费,如何做到降低代码冗余?我们使用到继承,如下图表述,GeometrocObject是父类,包括所有子类共有的特性(数据和方法);Circle和Rectangle继承于GeometrocObject。这样在编写Circle和Rectangle的时候就只需要对其特性进行描述就可以了;这两个类的共性在其父类中以及体现出来了,子类完全继承父类的所有特性(数据和方法)。
代码实现如下:
1.2.1. 父类
SimpleGeometricObject
public class SimpleGeometricObject {
private String color = "white";
private boolean filled;
private java.util.Date dateCreated;
/** Construct a default geometric object */
public SimpleGeometricObject() {
dateCreated = new java.util.Date();
}
/**
* Construct a geometric object with the specified color and filled value
*/
public SimpleGeometricObject(String color, boolean filled) {
dateCreated = new java.util.Date();
this.color = color;
this.filled = filled;
}
/** Return color */
public String getColor() {
return color;
}
/** Set a new color */
public void setColor(String color) {
this.color = color;
}
/**
* Return filled. Since filled is boolean, its get method is named isFilled
*/
public boolean isFilled() {
return filled;
}
/** Set a new filled */
public void setFilled(boolean filled) {
this.filled = filled;
}
/** Get dateCreated */
public java.util.Date getDateCreated() {
return dateCreated;
}
/** Return a string representation of this object */
public String toString() {
return "created on " + dateCreated + "\ncolor: " + color + " and filled: " + filled;
}
}
可以看到父类对共有的变量和方法进行了定义,虽然父类可以实例化,但是好像实例化父类并没有什么实际的意义。
注意:这个类最好不能被实例化,因为没有意义。下一章将借用这个例子,并使其不能被实例化。
1.2.2. 子类Circle
CircleFromSimpleGeometricObject
public class CircleFromSimpleGeometricObject extends SimpleGeometricObject {
private double radius;
public CircleFromSimpleGeometricObject() {
}
public CircleFromSimpleGeometricObject(double radius) {
this.radius = radius;
}
public CircleFromSimpleGeometricObject(double radius, String color, boolean filled) {
this.radius = radius;
setColor(color);
setFilled(filled);
}
/** Return radius */
public double getRadius() {
return radius;
}
/** Set a new radius */
public void setRadius(double radius) {
this.radius = radius;
}
/** Return area */
public double getArea() {
return radius * radius * Math.PI;
}
/** Return diameter */
public double getDiameter() {
return 2 * radius;
}
/** Return perimeter */
public double getPerimeter() {
return 2 * radius * Math.PI;
}
/* Print the circle info */
public void printCircle() {
System.out.println("The circle is created " + getDateCreated() + " and the radius is " + radius);
}
}
- 子类Circle使用
extend SimpleGeometricObject
表明继承于SimpleGeometricObject
那么子类拥有父类的所有特性(数据和方法); - 但是请注意,虽然子类拥有父类的所有特性,不意味子类就可以无限制的访问这些特性,例如
color
,filled
,dateCreated
这些是父类的私有变量,在子类中并不能访问,而要通过对应的getter和setter函数访问(这些函数是公开的); CircleFromSimpleGeometricObject
增加了自己的特性radius
并设置成私有,以及对应的getter和setter函数;- 子类中增加了自己的行为:
getArea()
、getPerimeter
和printCircle()
; - 当然,子类型的构造函数也不一样了,这是另外一个话题,后面会讲解。
1.2.3. 子类
RectangleFromSimpleGeometricObject
public class RectangleFromSimpleGeometricObject extends SimpleGeometricObject {
private double width;
private double height;
public RectangleFromSimpleGeometricObject() {
}
public RectangleFromSimpleGeometricObject(double width, double height) {
this.width = width;
this.height = height;
}
public RectangleFromSimpleGeometricObject(double width, double height, String color, boolean filled) {
this.width = width;
this.height = height;
setColor(color);
setFilled(filled);
}
/** Return width */
public double getWidth() {
return width;
}
/** Set a new width */
public void setWidth(double width) {
this.width = width;
}
/** Return height */
public double getHeight() {
return height;
}
/** Set a new height */
public void setHeight(double height) {
this.height = height;
}
/** Return area */
public double getArea() {
return width * height;
}
/** Return perimeter */
public double getPerimeter() {
return 2 * (width + height);
}
}
RectangleFromSimpleGeometricObject也使用同样的方式继承于SimpleGeometricObject,只是新增的变量和方法不太一样。
1.2.4. 测试主类
TestCircleRectangle
public class TestCircleRectangle {
public static void main(String[] args) {
CircleFromSimpleGeometricObject circle = new CircleFromSimpleGeometricObject(1);
System.out.println("A circle " + circle.toString());
System.out.println("The color is " + circle.getColor());
System.out.println("The radius is " + circle.getRadius());
System.out.println("The area is " + circle.getArea());
System.out.println("The diameter is " + circle.getDiameter());
RectangleFromSimpleGeometricObject rectangle = new RectangleFromSimpleGeometricObject(2, 4);
System.out.println("\nA rectangle " + rectangle.toString());
System.out.println("The area is " + rectangle.getArea());
System.out.println("The perimeter is " + rectangle.getPerimeter());
}
}
通过测试程序,我们发现子类的对象引用变量上可以调用toString()
,getColor()
等方法(这些方法是在父类中定义的),说明子类的确继承了父类的所有特性。
思考一个问题:getArea()
函数和getPerimeter()
函数既然在两个子类中存在,就说明是这两个子类的共性;那为什么不把这两个函数写在父类当中?对了其实是因为他们的算法不同,虽然函数签名是一样的。下一章将要学习如何把这个方法放到父类中去。
另外一个是toString()
这个函数非常特别,后面会讲到。
2. 子类的构造函数
2.1. 构造函数也被继承了吗?
No. They are not inherited. They are invoked explicitly or implicitly. Explicitly using the super keyword.
A constructor is used to construct an instance of a class. Unlike properties and methods, a superclass's constructors are not inherited in the subclass. They can only be invoked from the subclasses' constructors, using the keyword super. If the keyword super is not explicitly used, the superclass's no-arg constructor is automatically invoked.
构造函数并不会被子类型所继承,子类型通过显式或者隐式的方式调用父类的构造函数;显式调用使用super关键字。如果没有显式调用,那么子类型将隐式调用父类型不带参数的构造函数。
为了便于子类型调用父类的构造函数,建议在定义类型的时候,要么不写构造函数,否则建议写一个不带参数的构造函数。
A constructor may invoke an overloaded constructor or its superclass’s constructor. If none of them is invoked explicitly, the compiler puts super() as the first statement in the . For example
父类如果只定义了带参数的构造函数,那么Java不会分配不带参数的构造函数(没有不带参数的构造函数);这时如果子类没有显式调用父类带参数的构造函数,将导致构造函数找不到的问题。
例如:
class Person {
int age;
public Person(int a) {
age = a;
}
}
class Man extends Person {
}
上诉例子中定义的 Man 类将出现编译错误,因为 Person 缺少不带参数的构造函数,Person 又没有显式的调用Person带参数的构造函数。修改 Man 类如下就可以了:
class Man extends Person {
public Man() {
super(10);
}
}
2.2. super关键字
The keyword super refers to the superclass of the class in which super appears. This keyword can be used in three ways:
- To call a superclass constructor
- To call a superclass method
- To access superclass fields
super关键字调用父类构造方法还可以理解,为什么还需要super关键字去调用父类的函数和字段?CircleFromSimpleGeometricObject
不是可以直接调用父类SimpleGeometricObject
的方法吗?例如:getColor()
等方法?
这是因为子类可以覆盖(override)父类的方法,后面的覆盖父类方法的小节会讲解。
2.3. 构造函数链
当使用new关键字构建一个对象的时候,如果这个对象所属的类有父类(通常情况下都有),那么是否和一步一步构建的?我们看看下面的例子。
public class Faculty extends Employee {
public static void main(String[] args) {
new Faculty();
}
public Faculty() {
System.out.println("(4) Faculty's no-arg constructor is invoked");
}
}
class Employee extends Person {
public Employee() {
this("(2) Invoke Employee’s overloaded constructor");
System.out.println("(3) Employee's no-arg constructor is invoked");
}
public Employee(String s) {
System.out.println(s);
}
}
class Person {
public Person() {
System.out.println("(1) Person's no-arg constructor is invoked");
}
}
main函数开起来比较奇怪,为什么在Faculty内中,又使用new 方法构造自己所在的类?其实大家不用太关心main函数在那里。首先因为java虚拟机要访问到这个静态函数作为程序的入口;其次,对于 main 函数,只要被构建的类对于main函数来说是可以访问就行了。上述所有类型都定义在一个文件间当中,这样如同是一个包;因此main函数可以访问该文件中的所有类;进而,也可以访问 Faculty 类,构建 Faculty 类的对象当然也是可以的。其实把main函数放在这个文件的任何类中都是可以的,只不过我们习惯放在 Faculty 这个类中(这个文档的文件名也是和这个类一致的)。
我们接下来仔细分析一下发生了什么,运行的结果是:
(1) Person's no-arg constructor is invoked
(2) Invoke Employees overloaded constructor
(3) Employee's no-arg constructor is invoked
(4) Faculty's no-arg constructor is invoked
也就是说:Person的构造函数被最先执行完成(注意不是最先被调用),然后是 Employee 的构造函数被执行完成,最后是 Faculty 的构造函数被执行完成。
sequenceDiagram
activate Faculty()
Faculty()->>Employee(): 隐式调用super()
deactivate Faculty()
activate Employee()
Employee()->>Person(): 隐式调用 Person()
deactivate Employee()
activate Person()
Note over Person():1
Person()->>Employee(): Person()调用返回
activate Employee()
deactivate Person()
Note over Employee():2、3
deactivate Employee()
Employee()->>Faculty():Employee()调用返回
activate Faculty()
Note over Faculty():4
deactivate Faculty()
可以看到,这是典型的函数调用栈的方式:最先进入的函数最后退出。其实Person类还隐式调用了Object(),会在Object小节讲解。
3. Overriding 覆盖父类的方法
在很多时候,子类型可能需要改变父类型的一些行为(函数),这时子类型可以定义一个与父类型一样的函数,但是实现不一样,这样父类型的实现就被隐藏了。
3.1. 继承父类型的函数
先来看看子类型继承父类型,拥有父类型的全部能力的情况:
class Fruit {
public String toString() {
return "This is fruit.";
}
}
class Apple extends Fruit {
}
public class Test {
public static void main(String[] args) {
Apple apple = new Apple();
System.out.println(apple.toString()); // 输出:This is fruit.
}
}
Apple中没有toString()函数,但是其父类型后,因此Apple也得到了这样的能力。其实是间接调用父类型的toString()函数。
3.2. 覆盖父类型的函数
如果在Apple中也定义一个toString()函数会有什么样的结果?
class Fruit {
public String toString() {
return "This is fruit.";
}
}
class Apple extends Fruit {
public String toString() {
return "This is an apple.";
}
}
public class Test {
public static void main(String[] args) {
Apple apple = new Apple();
System.out.println(apple.toString()); // 输出:This is an apple.
}
}
Apple类型中的 toString() 函数覆盖了父类型的同名函数,这样的方式叫做 override。在Apple类型的对象上面调用toString()会出现Apple类自己toString()函数的行为,而不会调用Fruit类型的toString()。换句话说,子类型如果定义了和父类型一样的函数,父类型的这个同名函数将不可见。
3.3. 使用super关键字调用父类型被隐藏的函数
子类如果覆盖了父类的某个函数,但是在子类中(只能在直接子类中)任然有办法调用父类被隐藏的函数。
class Fruit {
public String toString() {
return "This is fruit.";
}
}
class Apple extends Fruit {
public String toString() {
return super.toString() + "\nThis is an apple.";
}
public void printFruitToString() {
System.out.println(super.toString());
}
}
public class Test {
public static void main(String[] args) {
Apple apple = new Apple();
System.out.println(apple.toString());
apple.printFruitToString();
}
}
结果是:
This is fruit.
This is an apple.
This is fruit.
1、2行输出是System.out.println(apple.toString());
的结果。可以看到第1行是Apple类中toString()
函数使用super.toString()
调用了Fruit类中toString
的结果。第3行使用Apple的函数printFruitToString()
,本质也是使用了super.toString()
。
注意:super关键字只能在直接子类中使用,表示其直接父类。
3.4. Overriding vs. Overloading
An instance method can be overridden only if it is accessible. Thus a private method cannot be overridden, because it is not accessible outside its own class. If a method defined in a subclass is private in its superclass, the two methods are completely unrelated.
(了解)实例函数在子类中可以访问的情况下才可以被覆盖。如果父类中有一个私有函数,子类中定义了一个函数(与父类函数名,参数都一样),那么这两个函数没有任何关系。
Like an instance method, a static method can be inherited. However, a static method cannot be overridden. If a static method defined in the superclass is redefined in a subclass, the method defined in the superclass is hidden.
(了解)和实例函数一样,静态函数可以被子类继承;但是静态函数不能被覆盖;如果子类型重新定义了父类型的静态函数,父类型的静态函数将被一直隐藏。
覆盖(Overriding):只在父类型函数和子类型函数一样的情况下出现;
重载(Overloading):函数签名不一样
上图中左边是覆盖,右边是重载。
4. Object类
如果在定义的时候不使用关键字(extends)指定父类,那么默认其父类是Object。也就是说,所有的引用类型都是从Object直接或者是间接继承(扩展)而来的。
class Fruit{
}
// 与下面是一样的
class Fruit extends Object{
}
其实Object是所有引用类型最终的“根”;每个引用类型都是Object直接或者间接的子类。
4.1. toString方法
这样以来,所有引用类型都具备Object所具备的所有能力,最常用的就是toString()方法。
toString()方法是在Object中定义的方法,会打印一个该对象的描述。表述的大约格式是:类名@对象地址
class Fruit{
}
public class Test {
public static void main(String[] args) {
Fruit fruit = new Fruit();
System.out.println(fruit.toString());
}
}
试试上面的例子。
5. Polymorphism多态
Polymorphism means that a variable of a supertype can refer to a subtype object.
多态是指直接或者间接的父类型引用变量可以用来引用子类型的对象。很难理解,有什么用?
A class defines a type. A type defined by a subclass is called a subtype, and a type defined by its superclass is called a supertype. Therefore, you can say that Circle is a subtype of GeometricObject and GeometricObject is a supertype for Circle.
上面是说父类型和子类型的关系。例如借用本章开头的分类图:猫种可以说成是属于猫属、猫科、食肉动物目。。。就是这意思,这有什么用?
5.1. 简单的例子
classDiagram
Fruit <|-- Apple
Fruit <|-- Orange
Fruit : +toString()
class Apple{
+toString()
}
class Orange{
+toString()
+printInfo()
}
Apple <|-- GiantApple
有三个类:Fruit是父类,Apple和Orange都从Fruit扩展而来,且都覆盖了其toString()方法。
class Fruit {
public String toString() {
return "This is fruit.";
}
}
class Apple extends Fruit {
public String toString() {
return "This is an apple.";
}
}
class Orange extends Fruit {
public String toString() {
return "This is an orange.";
}
public void printInfo() {
System.out.println("My name is Orange.");
}
}
public class Test {
public static void main(String[] args) {
Fruit fruit = new Apple();
System.out.println(fruit.toString()); // 打印:This is an apple.
fruit = new Orange();
System.out.println(fruit.toString()); // 打印:This is an orange.
}
}
注意分析主函数中的 Fruit fruit = new Apple();
这一行代码。变量类型是Fruit,而实际的对象是Apple(注意:Apple是Fruit的子类)。这样类型都不一样,可以赋值吗?这样做是完全合法的,多态的一个重要用法是用父类的引用变量类引用(指向)子类的对象。
这样理解可能会好一些:
- 变量类型声明只是调用能力声明;
- 子类型对象的能力一定不小于父类型对象的能力;
上面例子中真实的对象是Apple,能力声明是Fruit。因为Apple的能力不小于Fruit,因此我用Fruit类型声明的变量去调用Apple的对象不存在任何问题(Apple的能力大于Fruit,Fruit的能力只是Apple能力的一个子集)。但是因为能力声明的限制(引用变量的类型),只能调用Fruit这个类型所具备的能力(例如,Orange类型有一个printInfo()
函数,就无法在 Fruit的引用变量上调用)。
但是为什么输入结果不是This is fruit
?这时因为子类型覆盖了父类型的toString()
方法;真正的行为表现形式还要看对象的确切类型是什么。例子中,对象是 Apple 或者是 Orange;toString()的行为也只能是Apple或者Orange对应这个函数的真是行为,而不是他父类的行为(父类的这个行为被覆盖了)。关于这个问题,在下面的动态函数绑定中还有解释。
用父类型的引用变量引用子类型的对象,并调用相应的函数体现不同的行为,这就是多态。多态应用中,子类型一般需要覆盖父类型的方法。
5.2. 参数中使用多态
我们先来看看一个例子:
public class Test {
public static void main(String[] args) {
Apple apple = new Apple();
Orange orange = new Orange();
printInfo(apple);
printInfo(orange);
}
static void printInfo(Fruit fruit) {
System.out.println(fruit.toString()); // 这里引用变量的类型其实是父类型
}
}
同样,函数的参数也可以使用父类型,这样的调用和上面变量类型使用父类型其实是一样的。
5.3. 动态函数绑定
子类型覆盖了父类型的函数,哪么当通过引用变量调用一个对象某个函数的时候,不是编译的时候决定的(静态),而而是在运行的时候,引用变量引用的真实对象所决定的(对象决定行为,不是引用变量类型),这样的方式叫做动态函数绑定。
public class DynamicBindingDemo {
public static void main(String[] args) {
m(new GraduateStudent());
m(new Student());
m(new Person());
m(new Object());
}
public static void m(Object x) {
System.out.println(x.toString());
}
}
class GraduateStudent extends Student {
}
class Student extends Person {
public String toString() {
return "Student";
}
}
class Person extends Object {
public String toString() {
return "Person";
}
}
上述例子执行的结果是:
Student
Student
Person
java.lang.Object@7e774085
虽然m(Object x)
的引用变量类型是 Object,在其中调用 toString() 方法(toString()方法是在Object中定义的,因此每个类型都具备该方法,当然可以覆盖);但是其行为是右具体的对象确定的。
书上的解释:
When the method m(Object x) is executed, the argument x’s toString method is invoked. x may be an instance of GraduateStudent, Student, Person, or Object. Classes GraduateStudent, Student, Person, and Object have their own implementation of the toString method. Which implementation is used will be determined dynamically by the Java Virtual Machine at runtime. This capability is known as dynamic binding.
Dynamic binding works as follows: Suppose an object o is an instance of classes C1, C2, ..., Cn-1, and Cn, where C1 is a subclass of C2, C2 is a subclass of C3, ..., and Cn-1 is a subclass of Cn. That is, Cn is the most general class, and C1 is the most specific class. In Java, Cn is the Object class. If o invokes a method p, the JVM searches the implementation for the method p in C1, C2, ..., Cn-1 and Cn, in this order, until it is found. Once an implementation is found, the search stops and the first-found implementation is invoked.
上面文字使用下面的例子来解释:Apple这个例子中,我们再次扩展Apple,为一个GiantApple类:
sclass GiantApple extends Apple {
}
public class Test {
public static void main(String[] args) {
Fruit fruit = new GiantApple();
System.out.println(fruit.toString()); // 打印:This is an apple.
}
}
上述代码的输出任然是This is an apple.
。真实对象是GiantApple
,而GiantApple
没有toString()方法,因此Java会顺着继承的关系一层一层向上去找父类的toString()的实现;当找到的第一个个toString()的实现就调用。GiantApple
没有toString()实现,因此在其父类Apple
中寻找,找到了,就执行。
6. 引用类型的类型转换
我们把上述的法则增加一条:
- 变量类型声明只是调用能力声明;
- 子类型对象的能力一定不小于父类型对象的能力;
- 真正的行为表现要看其具体对象的类型,而不是看引用变量的类型。
- 一个对象内部的可见性(可以调用什么属性和方法)由其引用变量的类型所确定。
这样我们多态的规则就完备了。凡是以后关于多态的问题都可以由以上三条规则解释。
6.1. 子类型转换成父类型
更准确的说应该是父类型的引用变量引用子类型的对象,就像前面的:
Fruit fruit = new Apple();
这样总是成功的,这里没有显式(explicit )的进行类型转换,而叫做隐式(implicit )类型转换。其隐含的语义是:Apple也是水果。只不过我们通过fruit引用变量只能调用类型Fruit所能调用的变量和函数(能力声明)。
更进一步来说,下面的语法也是完全正确的:
Object object = new Apple();
因为 Object 类型是所有引用类型的共同祖先(根),因此其语义是Apple也是对象。当然通过object引用变量所能调用的变量或者函数相对Apple这个类型来说可能就更少了。
下面的也是合法的:
Apple apple = new GiantApple();
Object objedt = apple;
对比一下,我们多态的三条规则,是不是可以理解?
6.2. 父类型转换成子类型
6.2.1. 非强制转换
这里有两种情况:首先是子类型的引用变量引用父类型的对象:
Orange orange = new Fruit(); // 非法
或者是这样:
Fruit fruit = new Fruit();
Orange orange = fruit; // 非法
这样的操作是非法的,因为:
- 真实对象是Fruit;
- 引用变量类型是 Orange;
- Orange是Fruit的子类型,其能力大于(准确的说是不小于)Fruit;
- 用较大的能力声明去定义一个较小能力的对象,而对其进行调用,可能造成能力不匹配的问题。例如,Orange 有一个
printInfo()
的方法,而Fruit没有,当用orange.pringInfo()
进行调用的时候,因为真实对象不具备这个能力,所以导致出错。
因此:父类型赋值给子类型是非法的!其隐含的语义是 Fruit 是 Orange,这显然是不正确的。
6.2.2. 强制转换
真实对象是子类型,而引用变量是父类型,这种情况是合法的。但是否可以转换回去?例如:
Object object = new Orange(); // 合法
Orange orange = object; // 非法
这为什么变成非法的了?答案是因为动态绑定的特性,编译器不知道引用变量的object的真实类型(因为object变量可以引用所有的引用类型对象)。那如果非要这么做应该怎么办?答案是强制类型转换,或者叫做显式(explicit )类型转换:
Object object = new Orange(); // 合法
Orange orange = (Orange)object; // 合法,强制类型转换
orange.printInfo(); // 正确,真实对象是Orange,有这个能力
但是你能保证object引用的真是一个Orange对象吗?
Object object = new Apple(); // 合法
Orange orange = (Orange)object; // 合法,强制类型转换
orange.printInfo(); // 语法正确,运行抛出异常
因为真实对象是Apple,不具备printInfo()的能力。那么这是就需要用到 instanceof 操作符了。
6.3. instanceof 操作
instanceof 操作符是用来判断一个引用变量所引用对象的类型,其语义是:该对象是属于XXX类吗?
注意:只能操作引用类型,对原始类型(Primitive)操作是语法错误。
Object object = new Apple();
if (object instanceof GiantApple) System.out.println("GiantApple"); // 不输出
if (object instanceof Apple) System.out.println("Apple"); // 输出
if (object instanceof Object) System.out.println("Object"); // 输出
非常有用的一个操作符号,有了这个,先判断成功后再进行强制类型转换就一定不会出错了。我们来看看一个更复杂的例子:
public class Test {
public static void main(String[] args) {
Fruit[] fruits = {new Apple(), new Orange(), new Apple(), new Orange(), new GiantApple()}; // 水果的数组
whatFruit(fruits); // 调用统计函数
}
// 统计水果的种类
static void whatFruit(Fruit[] fruits) {
int countApple = 0;
int countOrange = 0;
for (Fruit fruit : fruits) { // 增强for循环
if (fruit instanceof Apple) { // 判断是Apple?
countApple++;
}
if (fruit instanceof Orange) { // 判断是 Orange?
countOrange++;
Orange orange = (Orange) fruit; // 类型强制转换,一定成功
orange.printInfo(); // 调用 Orange 专属的能力
}
}
System.out.printf("We have %d apples.\n", countApple);
System.out.printf("We have %d oranges.\n", countOrange);
}
}
有了上面的例子,是不是清楚多了。
注意:强制类型转换只能用作同一个类属的继承关系中,其语义是属于某个类;例如:Apple属于Fruit,Orange属于Fruit,Apple属于Object。但是不同类属将不能进行强制类型转换,例如:Orange和Apple,这两个类没有属于的这种语义,属于不同的分支(人和猴子属于不同的分支,说人属于猴子,或者猴子属于人都是不正确的)。如下图:
classDiagram
Object <|-- Fruit
Fruit <|-- Apple
Fruit <|-- Orange
Fruit : +toString()
class Apple{
+toString()
}
class Orange{
+toString()
+printInfo()
}
Apple <|-- GiantApple
7. ArrayList类
ArrayList 是比Array更好的一个“数组”。相比传统的数组,ArrayList是动态的,且提供更多的操作。
import java.util.ArrayList;
public class TestArrayList {
public static void main(String[] args) {
// Create a list to store cities
ArrayList<String> cityList = new ArrayList();
// Add some cities in the list
cityList.add("London");
// cityList now contains [London]
cityList.add("Denver");
// cityList now contains [London, Denver]
cityList.add("Paris");
// cityList now contains [London, Denver, Paris]
cityList.add("Miami");
// cityList now contains [London, Denver, Paris, Miami]
cityList.add("Seoul");
// contains [London, Denver, Paris, Miami, Seoul]
cityList.add("Tokyo");
// contains [London, Denver, Paris, Miami, Seoul, Tokyo]
System.out.println("List size? " + cityList.size());
System.out.println("Is Miami in the list? " + cityList.contains("Miami"));
System.out.println("The location of Denver in the list? " + cityList.indexOf("Denver"));
System.out.println("Is the list empty? " + cityList.isEmpty()); // Print false
// Insert a new city at index 2
cityList.add(2, "Xian");
// contains [London, Denver, Xian, Paris, Miami, Seoul, Tokyo]
// Remove a city from the list
cityList.remove("Miami");
// contains [London, Denver, Xian, Paris, Seoul, Tokyo]
// Remove a city at index 1
cityList.remove(1);
// contains [London, Xian, Paris, Seoul, Tokyo]
// Display the contents in the list
System.out.println(cityList.toString());
// Display the contents in the list in reverse order
for (int i = cityList.size() - 1; i >= 0; i--)
System.out.print(cityList.get(i) + " ");
System.out.println();
// Create a list to store two circles
java.util.ArrayList<CircleFromSimpleGeometricObject> list = new java.util.ArrayList<>();
// Add two circles
list.add(new CircleFromSimpleGeometricObject(2));
list.add(new CircleFromSimpleGeometricObject(3));
// Display the area of the first circle in the list
System.out.println("The area of the circle? " + list.get(0).getArea());
}
}
掌握书上这个例子应该是足够了。注意ArrayList的声明方式,这是泛型申明。关于泛型,我们只需要知道如何使用就可以了,不用关心太多。
ArrayList<String> cityList = new ArrayList();
尖括号中的就是泛型,表达的语义是ArrayList中转载的数据是什么类型。当然,如果是ArrayList<Object>
那么可以装任何的引用类型。
注意:泛型中尖括号中的类型必须是引用类型,不能是原始类型。
需要了解ArrayList和Array间的相互转换:
public static void main(String[] args) {
// Creating an ArrayList from an array of objects:
String[] array = { "red", "green", "blue" };
ArrayList<String> list = new ArrayList<>(Arrays.asList(array));
// Creating an array of objects from an ArrayList:
String[] array1 = new String[list.size()];
list.toArray(array1);
}
8. protected 访问修饰
现在我们可以完整访问修饰符了。有了继承后,有了一个新的修饰符叫做 protected,比 public更严格,比 缺省 更宽松;其含义是允许其子类型拥有完全的访问,而不关心是否在同一个包当中。记住下面这张表格就可以了。
下图是一个例子:
8.1. 不能降低父类成员的访问级别
A subclass may override a protected method in its superclass and change its visibility to public. However, a subclass cannot weaken the accessibility of a method defined in the superclass. For example, if a method is defined as public in the superclass, it must be defined as public in the subclass.
子类可以覆盖其超类中的受保护方法,并将其可见性更改为公共。然而,子类不能削弱在超类中定义的方法的可访问性。例如,如果一个方法在超类中定义为public,那么它在子类中必须定义为public。
如何理解?我们规则第2条:子类型的能力永远不小于父类型的能力。类型的能力是其成员可见性觉得的,可见性降低了,其能力降低。因此不能弱化可访问性(可见性)。
访问修饰的从严格到宽松的排序:private<default<protected<public
8.2. final 修饰
The final class cannot be extended;
The final variable is a constant;
The final method cannot be overridden by its subclasses.
注意:
The modifiers are used on classes and class members (data and methods), except that the final modifier can also be used on local variables in a method. A final local variable is a constant inside a method.
修饰符用于类和类成员(数据和方法),但final修饰符也可以用于方法中的局部变量。final用于一个局部变量,这个变量就是方法内部的常量。
9. 本章重点
除了特别标注,都需要掌握。