抽象类
1. 抽象类的简单理解和实现
简单地讲,抽象类就是不允许实例化的类。
1.1 抽象类 (abstract class)
上一节Animal
的例子中,我们创建一个Animal
的实体是没什么意义的。这样我们可以给Animal
类添加abstract
修饰符,阻止其实例化。
package animal;
public abstract class Animal {
public int age = 0;
public void makeNoise() {
System.out.println("The animal is making noise!");
}
}
这样就再也不能生成Animal
的对象了。
1.2 抽象方法 (abstract method)
我们继续发现:实现Animal
的makeNoise()
方法似乎也没有太大意义,因为不同的动物有不同的makeNoise()
方法,所以我们可以进一步给makeNoise()
添加abstract
修饰符:
package animal;
public abstract class Animal {
public int age = 0;
public abstract void makeNoise(); // 这是一个抽象方法
}
注意: 这里的abstract
类型的makeNoise()
方法没有方法体:因为方法也是抽象的,所以没有具体实现的细节。Animal
类只负责声明“Animal需要makeNoise()
”这么个方法,但是具体实现那是具体动物的事情了,abstract
方法就是负责这么一种声明。
1.3 使用抽象类
我们新建一个Dog类,继承Animal类,此时就会发现,Dog类不光继承了age属性,还必须实现makeNoise()方法,不然就会报错。
package animal;
public class Dog extends Animal{
@Override
public void makeNoise() { // 必须实现继承来的abstract方法
System.out.println("Dog says: Woof Woof");
}
}
在AnimalExample.java里使用Dog类:
import animal.Dog;
public class AnimalExample {
public static void main(String[] args) {
Dog myDog = new Dog();
// 引用Dog继承来的属性age
System.out.println("Dog's age = " +myDog.age);
// 调用Dog自己实现的makeNoise()方法
myDog.makeNoise();
}
}
运行结果
Dog's age = 10
Dog says: Woof Woof
2. 为什么需要继承和抽象类?
当然阻止创建抽象类的对象并不是设计抽象类的目的,教科书上大多数都说继承是为了“实现代码的复用,可以在一个类的基础上扩展新的功能”,但是这可能也不是继承的最重要的目的。
继承是利用合理的设计模式开发高可扩展性、高可维护性、和高灵活性系统的基本手段之一。
不能理解上面的话也没关系,我们通过下面的小例子初步感知继承和抽象类的用途。
基于上面的抽象Animal类和具体的Dog类,我们再将以前的Cat类简化成如下形式,只包含一个实现的makeNoise()方法。
package animal;
public class Cat extends Animal {
@Override
public void makeNoise() {
System.out.println("Cat says: Meow Meow");
}
}
我们用如下结构的类图来表示这种继承关系:
Fig. 1. 继承关系的UML类图
注.
到目前为止,我们有一个抽象类Animal,以及一个继承Animal的Cat类,一个继承Animal的Dog类。
假设我们有一个商业逻辑:有个宠物店(PetZone
类),提供一个playWithPet
的服务(一个方法)。客人playWithPet
,动物们就会发出声音回应(makeNoise
)。那么我们可以写这么一个PetZone类,并提供这个服务。
import animal.Animal;
public class PetZone {
public static void playWithPet(Animal pet) {
pet.makeNoise();
}
}
注.
- (1)
playWithPet
不光可以针对Cat
,也可以针对Dog
,所以它的参数是Animal,而不是具体哪种动物; - (2) 虽然我们不知道具体是那种动物,但是只要它是动物(继承了
Animal
类),那么Animal
里定义的abstract
方法makeNoise()
的声明,保证了继承Animal
类的具体类(Dog
,Cat
或者其他类)一定有并且实现了这个方法,所以我们在playWithPet
方法里可以安全地调用传入对象pet
的makeNoise()
方法; - (3) 而
pet
类型不同,它的makeNoise()
行为就不同,比如:如果pet是Cat类型会Meow Meow叫,而pet是Dog类型会Woof Woof叫.
模拟使用PetZone的业务
InheritanceExample.java
import animal.Animal;
import animal.Cat;
import animal.Dog;
public class InheritanceExample {
public static void main(String[] args) {
// 这里我们将petCat的类型定义为Animal,而非Cat;但是new Cat()实际生成了一只Cat
Animal petCat = new Cat();
Animal petDog = new Dog();
// 调用PetZone的playWithPet方法,与上面两只pets玩耍
PetZone.playWithPet(petCat);
PetZone.playWithPet(petDog);
}
}
运行结果
Cat says: Meow Meow
Dog says: Woof Woof
- (4) 如果宠物店有了新类型的动物,比如鸟,我们只用创建一个
Bird
类,继承Animal类并且实现makeNoise()方法;而PetZone
类不需要修改,就能兼容新的宠物类型。
package animal;
public class Bird extends Animal{
@Override
public void makeNoise() {
System.out.println("Bird says: Jeo Jeo");
}
}
然后在使用PetZone
的服务时添加使用这个新类型的Animal:
import animal.Animal;
import animal.Bird;
import animal.Cat;
import animal.Dog;
public class InheritanceExample {
public static void main(String[] args) {
Animal petCat = new Cat();
Animal petDog = new Dog();
// 新添加的Animal pet
Animal petBird = new Bird();
PetZone.playWithPet(petCat);
PetZone.playWithPet(petDog);
// 直接调用PetZone的playWithPet方法
PetZone.playWithPet(petBird);
}
}
运行结果
Cat says: Meow Meow
Dog says: Woof Woof
Bird says: Jeo Jeo
Tips
- 虽然添加了新的动物
Bird
,但是无需修改实现业务逻辑的PetZone类!可以通过这种方式添加任意种类的Animal
而不影响业务逻辑,这就是前面提到的“高可扩展性”, “高灵活性”; - 如果业务逻辑变化了:即
PetZone
的playWithPet
方法需要调整,我们也只用修改该方法即可实现业务逻辑的变更,这就是“高可维护性”。
例.
考虑更实际的情况:与pet玩耍时,它们可能理会也可能无视你。那么为了模拟这种情况,我们只需在PetZone
的playWithPet
方法里引入随机性。
import java.util.Random;
import animal.Animal;
public class PetZone {
public static void playWithPet(Animal pet) {
Random rand = new Random();
double prob = rand.nextDouble();
if(prob > 0.5) {
pet.makeNoise();
} else {
System.out.printf("The pet %s walked away.\n", pet.getClass().getSimpleName().toLowerCase());
}
}
}
修改好后,再次运行InheritanceExample.java
,我们随机地得到如下结果:
Cat says: Meow Meow The pet dog walked away.
Bird says: Jeo Jeo
注.
- 这里
pet.getClass().getSimpleName()
利用Java反射机制,获取pet所属的类的名字;string.toLowerCase()
是将string
里的字符全部转换成小写。不理解就算了。
Just for fun.
上述playWithPet
方法还可以通过Java泛型Generics实现,虽然在这个例子中毫无必要且多此一举:
public static <T extends Animal> void playWithPet(T pet) {
pet.makeNoise();
}
如果把这个方法和前面的playWithPet
方法放在一起,Java会报错说这两个方法是同一个方法。
这里比较有意义的是我们又遇见一种新的括号<>
。这里做一个总结。
Java里:
()
圆括号内定义参数;{}
花括号内定义代码块;<>
尖括号内定义类型。