在上一章我们学会了如何创建类和实例。这一章我们来深入探讨OOD(面对对象编程)的重要思想:封装、继承、接口,帮助我们更好地构建类,并理解类与类之间的关系。
封装 Encapsulation
封装意味着将数据的细节隐藏起来,仅公开有限的接口接口给用户。我们通过两种方式实现,第一种就是将类中的数据变为private,另一种便是提供get和set方法给外界,限制用户操作类数据的方式。
比如我们有一个Game类,其中有一个score数据:
public class Game {
public int score;
}
这样定义有很大的缺点:外界可以直接操作score数值,可以将其设置为无限大或无限小,甚至胡乱操作。我们就可以将其数据变为private,并创建get和set给外界来操作此数据:
public class Game {
private int score;
public int getScore() {
return score;
}
public void setScore(int score) {
if(score < 0 || score > 100) {
System.out.println("Score Error");
return;
}
this.score = score;
}
}
我们通过创建getScore和setScore来限制用户操作score的方式,在setScore中,我们会检查数据的值是否符合我们的设定,如果违规(小于0或大于100),那么此操作就无法完成。通过封装,我们限制用户使用类的方式,只提供外界一个黑匣子,以保证数据的安全。
继承 Inheritance
在面对对象编程中,在我们已经创建了一个类后,而又想再创建一个与之相似的类,比如添加新的方法,或者修改原来的方法。我们不必从头开始,可以从原来的类派生出新的类,我们把原来的类称为父类或基类,而派生出来的类称为子类,子类则会继承父类的数据和方法。
让我们看一个简单的例子,首先我们定义一个Animal类:
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void greet() {
System.out.println("Hello, I am " + this.name);
}
}
现在,我们想创建一个 Dog 类,比如:
public class Dog {
private String name;
public Dog(String name) {
this.name = name;
}
public void greet() {
System.out.println("WangWang..., I am " + this.name);
}
}
可以看到,Dog类和Animal类几乎是一样的,只是 greet 方法略有不同,我们完全没必要创建一个新的类,可以直接创建子类(child class)来继承父类Animal:
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
public void greet() {
System.out.println("WangWang..., I am " + this.name);
}
public void run() {
System.out.println("I am running!");
}
}
Dog类是从 Animal 类继承而来的,Dog类自动获得了 Animal 类的数据和方法,这些数据和方法必须是子类能够访问到的范围,被public和protected修饰的数据和方法就能被子类访问到。在Dog的构建函数中,我们调用了super(name),这就意味着Dog调用了Animal的Animal(String name)函数,将自己的name数据初始化为参数name的数值。
子类还可以对父类继承来的方法进行修改,Dog就对父类中的greet进行了重写(Override),并且还新增了一个方法 run。我们调用父类和子类对比一下:
public class Main {
public static void main(String[] args) {
Animal animal = new Animal("animal");
animal.greet(); // Hello, I am animal
Dog dog = new Dog("dog");
dog.greet(); // WangWang..., I am dog
dog.run(); // I am running!
}
}
多态 Polymorphism
多态的概念其实不难理解,它是指针对不同类型的参数进行相同的操作,根据对象(或类)类型的不同而表现出不同的行为。比如在我们刚学的继承中,子类可以拿到父类的数据和方法,也可以重写父类的方法,还可以新增自己特定的方法。有了继承,就能实现多态,便可以为不同的数据类型的实现提供统一的接口。比如我们再创建一个Animal的子类Cat:
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
public void greet() {
System.out.println("MiaoMiao..., I am " + this.name);
}
}
现在我们创建两个都是Animal类型的变量cat和dog,并将它们具体实例化成Cat和Dog类,那么当我们调用cat和dog的 greet 方法,它们则会自动调用实际类型的 greet 方法,做出不同的响应:
public class Main {
public static void main(String[] args) {
Animal cat = new Cat("cat");
cat.greet(); // MiaoMiao..., I am cat
Animal dog = new Dog("dog");
dog.greet(); // WangWang..., I am dog
}
}
抽象 Abstraction
数据抽象的意思是隐藏细节,只暴露最重要的信息给用户。抽象可以通过 abstract class 或 interface(接口)实现。abstract 关键字是一个用于类和方法的修饰符,我们无法创建 abstract class 类型的实例,这种抽象类只能被继承。而abstract method只能定义在abstract class中,这种方法没有具体执行内容。一个抽象类既可以有抽象方法也可以有正常的方法:
public abstract class Person {
public abstract void greet();
public void sleep() {
System.out.println("Zzz");
}
}
上面的例子中,我们创建了一个抽象类Person,如果我们运行以下的代码则会报错,因为我们无法给抽象类创建实例:
Person person = new Person(); // 会报错
要使用抽象类,我们必须创建一个类继承它,比如我们创建一个Teenager继承Person:
public class Teenager extends Person {
public void greet() {
System.out.println("I am a teenager.");
}
}
要注意的是,如果一个类继承了抽象类,那么此类必须要实现抽象类中定义的抽象方法,不然会报错,所以Teenager就必须要实现greet方法,下面是Teenager的调用方式:
public class Main {
public static void main(String[] args) {
Teenager teenager = new Teenager();
teenager.greet(); // I am a teenager.
teenager.sleep(); // Zzz
}
}
接口 Interface
另一个实现数据抽象的方式就是使用接口(interface)。一个接口就是完全的抽象类,其中只含有抽象方法,这些方法中是没有任何逻辑代码的。类的主要作用便是定义一些特定的方法,具体逻辑让正常的类实现:
interface Student {
public void goToSchool();
public void takeExam();
}
如果要使用接口的方法,那么具体的类必须实现(implements)其接口。只要被实现,具体的类必须将接口方法的具体逻辑全部实现:
public class Person implements Student {
public void goToSchool() {
System.out.println("I'm going to school.");
}
public void takeExam() {
System.out.println("I'm taking an exam.");
}
}
一个类可以实现多个接口,我们可以再定义一个Employee类,并将其加到Person implements后,不同接口之间用逗号隔开:
public interface Employee {
public void goToWork();
public void getSalary();
}
public class Person implements Student, Employee {
public void goToSchool() {
System.out.println("I'm going to school");
}
public void takeExam() {
System.out.println("I'm taking an exam");
}
public void goToWork() {
System.out.println("I'm going to work.");
}
public void getSalary() {
System.out.println("I just got the salary.");
}
}
这样我们就能从Person类中调用接口中全部的方法了:
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.goToSchool();
person.takeExam();
person.goToWork();
person.getSalary();
}
}
我们可以把接口理解为一个特定的属性,而这个属性有相对应的一些方法,通过实现(implements)接口,具体的类必须实现其属性相对应的特定方法,帮助实现数据的抽象化。我们要注意继承和接口所代表的逻辑关系,继承之间的关系是 is-a,也就意味着子类是父类的一个子集。实现接口的类和接口之间的关系是 has-a,意味着具体类拥有接口的一些功能。
实践练习
请创建一个Game类,其中包含score数据,请使用封装的概念来设计此类,并在其中创建一个displayInfo方法。然后再创建两个子类VideoGame,PhoneGame继承Game类,并分别重写displayInfo方法,然后调用这两个类的实例来查看区别。(display的逻辑可以很简单,打印出游戏的类型和分数即可)