在上一章我们学会了如何创建类和实例。这一章我们来深入探讨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的逻辑可以很简单,打印出游戏的类型和分数即可)