抽象类

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)

我们继续发现:实现AnimalmakeNoise()方法似乎也没有太大意义,因为不同的动物有不同的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方法里可以安全地调用传入对象petmakeNoise()方法;
  • (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而不影响业务逻辑,这就是前面提到的“高可扩展性”, “高灵活性”;
  • 如果业务逻辑变化了:即PetZoneplayWithPet方法需要调整,我们也只用修改该方法即可实现业务逻辑的变更,这就是“高可维护性”。

例. 考虑更实际的情况:与pet玩耍时,它们可能理会也可能无视你。那么为了模拟这种情况,我们只需在PetZoneplayWithPet方法里引入随机性。

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里:

  • ()圆括号内定义参数
  • {}花括号内定义代码块
  • <>尖括号内定义类型