一、概述

本文简单描述Java的三大特性,继承、封装、多态,使用例子说明它们的原理。

二、封装

封装就是把类的属性隐藏起来,不允许外面直接访问类的内部属性,而是提供可以访问的方法供外界调用。举例子,就像我们笔记本,把内部的实现细节,CPU,主板那些包装好,只提供键盘、USB接口、耳机口给我们用。

比如我们现在新建一个Person类。

1
public class Person {
2
    public String name;
3
    public String sex;
4
5
    public Person(String name, String sex) {
6
        this.name = name;
7
        this.sex = sex;
8
    }
9
10
    @Override
11
    public String toString() {
12
        return "Person{" +
13
                "name='" + name + '\'' +
14
                ", sex='" + sex + '\'' +
15
                '}';
16
    }
17
}

现在,我们编写一段测试代码,new一个Person类,然后发现可以直接通过 “实例.属性” 给属性重新赋值,而且属性值被篡改后已经失去了原来的意义。

1
public static void main(String[] args) {
2
    Person person = new Person("小明","男");
3
    System.out.println(person.toString());
4
    person.sex = "嘿嘿嘿"; // 性别被随意的修改了,显然不符合我们的期望
5
    System.out.println(person.toString());
6
}

输出结果如下:

1
Person{name='小明', sex='男'}
2
Person{name='小明', sex='嘿嘿嘿'}

为了防止这种情况的出现,封装的概念就很重要了,我们把属性私有化,只对外提供一些方法来操作这些属性。

比如上面这个例子,我们通过setter/getter来对属性进行赋值,用户想要修改实例的属性值,那么需要通过我们的setter方法来实现,这个时候,我们就可以在setter中做一些卡控,不允许用户设置除了“男”和“女”之外的属性值。

我们重新修改一下Person类。

1
public class Person {
2
    private String name; // 将public修改为private
3
    private String sex;
4
5
    // 无参构造方法
6
    public Person(){
7
8
    }
9
10
    public Person(String name, String sex) {
11
        this.name = name;
12
        this.sex = sex;
13
    }
14
15
    public String getName() {
16
        return name;
17
    }
18
19
    public void setName(String name) {
20
        this.name = name;
21
    }
22
23
    public String getSex() {
24
        return sex;
25
    }
26
27
    public void setSex(String sex) throws Exception {
28
        try{
29
            if (!(sex.equals("男")||sex.equals("女"))) // 做一些卡控
30
                throw new Exception("性别只允许输入“男”或“女”");
31
            this.sex = sex;
32
        }catch (Exception e){
33
            throw e;
34
        }
35
    }
36
37
    @Override
38
    public String toString() {
39
        return "Person{" +
40
                "name='" + name + '\'' +
41
                ", sex='" + sex + '\'' +
42
                '}';
43
    }
44
}

我们在setSex()方法中做了一些卡控,只允许输入“男”或“女”,其他的会抛出异常。这就体现了封装的特性,外部无需关系我们内部的实现细节,而且我们可以把控自己的属性,不会被随意篡改。

运行下面这段代码,就会抛出异常[Exception in thread “main” java.lang.Exception: 性别只允许输入“男”或“女”]了。

1
public static void main(String[] args) throws Exception {
2
    Person person = new Person("小明","男");
3
    System.out.println(person.toString());
4
    // person.sex = "嘿嘿嘿"; // 性别被随意的修改了,显然不符合我们的期望
5
    person.setSex("嘿嘿嘿"); // 上面的person.sex = "嘿嘿嘿";已经不能被访问了
6
    System.out.println(person.toString());
7
}

三、继承

我们定义一个学生类,一共有三个属性,名字、性别、电话号码,新建如下:

1
public class Student {
2
    private String name;
3
    private String sex;
4
    private String phoneNum; // 电话号码
5
    // 省略setter/getter
6
}

我们会发现,学生类和Person类有很多相似的地方,其中name,sex属性是一致的。我们同样也需要对学生性别做一些卡控,这时候我们可以选择和Person类一样,也修改一下setSex()。但是这也太麻烦了,如果系统庞大的时候,要修改很多地方,也要写很多重复代码。

这时候我们就可以用到继承了,继承就是子类继承于父类,这时候子类就拥有了父类的所有属性和方法(虽然不一定能使用,但是理论上全都拥有了)。

我们修改Student类继承于Person。

1
public class Student extends Person{
2
    private String phoneNum; // 电话号码
3
    // 省略setter getter
4
}

这时候,我们新建一个学生实例,赋值性别,发现执行的正是父类的方法,也意味着我们不需要重新写一遍卡控代码了。

1
public static void main(String[] args) throws Exception {
2
    Student student = new Student();
3
    student.setSex("嘿嘿嘿");
4
}

运行这段代码,同样会抛出异常[Exception in thread “main” java.lang.Exception: 性别只允许输入“男”或“女”]。

其中,子类也是不能够访问父类的private字段的,但是如果改成protected,子类是可以访问的。也就是说private的范围是当前类,protected的范围是继承树。

四、多态

多态是同一行为,对象不同,就具有不同的表现形态。比如我们现在定义一个动物类,有发出声音的方法。有猫和狗都继承于动物类,但是同样是发出声音,猫和狗是不一样的。

1
public class Animal {
2
    public void voice(){
3
        System.out.println("动物发出声音");
4
    }
5
}
6
7
public class Cat extends Animal{
8
    @Override
9
    public void voice() {
10
        System.out.println("猫发出声音,喵喵喵");
11
    }
12
}
13
14
public class Dog extends Animal{
15
    @Override
16
    public void voice() {
17
        System.out.println("狗发出声音,汪汪汪");
18
    }
19
}

这时候,我们编写一些测试代码,如下:

1
public static void main(String[] args) throws Exception {
2
    Animal animal = new Dog();
3
    animal.voice();
4
}

这里,我们声明的引用类型是Animal类,但是实际类型是Dog。这时候调用animal.voice();应该是直接打印父类的voice()还是子类Dog的voice()呢?

我们执行程序,发现是调用的子类的voice()。因此,Java实例方法的调用实际上是调用的运行时的实际类型的方法,而不是我们声明的方法。

这样看起来还不是那么明显,如果我们写一个方法,传入的参数是Animal类,内部执行voice();方法。

1
public static void ShowVoice(Animal animal){
2
    animal.voice();
3
}

这时候就体现多态的作用了,这个方法可以根据不同的具体子类实例,来调用不同的方法,也就是同一个发出叫声的方法,但是有多种不同的形态实现。

编写测试代码如下:

1
public static void main(String[] args) throws Exception {
2
    Animal dog = new Dog();
3
    ShowVoice(dog);
4
    Animal cat = new Cat();
5
    ShowVoice(cat);
6
}

执行结果:

1
狗发出声音,汪汪汪
2
猫发出声音,喵喵喵

五、总结

封装:禁止外部直接修改属性值,而是通过我们对外提供的方法来操作属性,增加安全性。

继承:以父类为模板,继承父类的所有属性和方法,可以做一些拓展,减少冗余代码,易于维护。

多态:针对声明类的同一个行为,不同的实现类有不同的行为结果。