重学java(2)封装,继承,多态

#说明

java面向对象的主要特点是三个。封装、继承、多态,本内容主要讲解比较重要的知识点。

封装

封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。

具体来说,在java中使用private实现变量或者方法的私有化。

不使用封装

首先我们在不使用封装的情况下创建一个类并初始化一个对象

1
//创建类
2
public class Person {
3
    public String name = "小明";
4
    public int age = 10;
5
}
6
//创建对象
7
Person person = new Person();

此时直接使用person.name就能访问到person的姓名,这也太方便了吧,但是需要知道的是,使用public修饰的变量,在java任意位置的其他类都能访问到,比如,我建立了一个新的类,专门用来该你的名字

1
//老王来了
2
public class Laowang {
3
    void change_name(Person person){
4
        person.name = "小王";
5
        System.out.println("你儿子归我了");
6
    }
7
}
8
//老王偷你儿子
9
Laowang laowang = new Laowang();
10
laowang.change_name(person);

然后你会发现你儿子的名字被改了。这个时候你就该想了,怎么样能让儿子不被偷呢?

使用封装

private就来了,private就是让你把自己的变量或者方法藏起来,更绝的是,你自己都不能直接访问自己的变量,那咋办呢,这个时候set和get方法就来了

1
public class Person {
2
    private String name = "小明";
3
    private int age = 10;
4
5
    public String getName() {
6
        return name;
7
    }
8
9
    public void setName(String name) {
10
        this.name = name;
11
    }
12
13
    public int getAge() {
14
        return age;
15
    }
16
17
    public void setAge(int age) {
18
        this.age = age;
19
    }

然后我们就能通过get和set来访问和修改了,而老王他就改不了你的名字了。但是这个时候你会发现,别人还是可以通过set和get来访问和修改的,比如我们将getName的方法修改一下,再新建一个对象访问

1
public String getName(Person p) {
2
    return p.name;
3
}
4
//
5
Person person = new Person();
6
Person person1 = new Person();
7
System.out.println(person1.getName(person));

这样还是能通过person1访问person的name变量,那set也能改了,也就是说,老王又可以改名字了。那咋办呢,那我们就在get方法里面加个判断吧

1
public String getName(Person p) {
2
    if(this == p){
3
        return p.name;
4
    }
5
    else{
6
        System.out.println("你看个锤子看");
7
    }
8
    return null;
9
}

然后你就发现其他对象就看不了名字了。

总结

封装简单来看就是用private对类的成员变量和函数进行封装,然后再用get和set提供接口,同时可以使用this关键字进行更深层的判断。

继承

继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为

具体实现时,在创建子类的时候,使用extends继承父类

为什么使用继承

这个问题解释起来其实挺简单的,举个简单的例子就是比如狗和猫都有嘴巴,眼睛,都会吃,走,但是我们如果在每个类里面都把这些变量和方法都写一遍,就会使代码产生产生冗余。比较好的方法就是创建一个动物类,包含啦嘴巴和眼睛,都会走,吃,而让猫,狗类继承动物类,再在各自内部实现各自独有的变量和方法。

1
//动物类
2
public class Animal {
3
    protected String eye;
4
    protected String mouse;
5
6
    Animal(){
7
        System.out.println("Animal create");
8
    }
9
    protected void eat(){
10
        System.out.println("animal eat");
11
    }
12
    protected void run(){
13
        System.out.println("animal run");
14
    }
15
}
16
//猫类
17
public class Cat extends Animal {
18
19
    Cat(){
20
        System.out.println("cat create");
21
    }
22
    @Override
23
    protected void run(){
24
        System.out.println("cat run");
25
    }
26
    //添加猫抓老鼠的功能
27
    private void get_moues(){
28
        System.out.println("cat cna get moues");
29
    }
30
}
31
32
//狗类
33
public class Dog extends Animal {
34
35
    Dog(){
36
        System.out.println("dog create");
37
    }
38
    @Override
39
    protected void run(){
40
        System.out.println("dog run");
41
    }
42
		//添加狗看门的功能
43
    private void watch_door(){
44
        System.out.println("dog can watch door");
45
    }
46
}

上面的例子中,猫和狗就不用再定义眼睛和嘴巴了,因为在继承的时候,相当于隐式的将这些变量在自己类中定义了。

构造函数和方法的重写

构造函数

子类是无法继承父类的构造函数的,也就是说在Dog类里面不能定义Animal()这个构造函数,但是在创建子类的时候可以调用。

如果父类的构造器带有参数的,则必须在子类的构造器中显式地通过super关键字调用父类的构造器。

1
//父类添加带参的构造函数
2
Animal(String eye, String mouse){
3
    this.eye = eye;
4
    this.mouse = mouse;
5
}
6
7
//子类需要使用super调用
8
Cat(String eye, String mouse) {
9
  super(eye, mouse);
10
}

如果父类有无参构造器,则在子类的构造器中用super调用父类构造器不是必须的,如果没有使用super关键字,系统会自动调用父类的无参构造器。

子类在使用构造函数创建对象的时候都会先调用父类的构造对象,然后再调用自己的构造函数。

重载和重写

一般将重写,都会跟重载对应起来将。

重写又叫覆盖,发生在继承关系下的子类中。子类可以用自己的方式实现父类的方法,重写不能改变参数列表,也不能缩小方法的访问权限,如果父类方法抛出异常,子类抛出的异常不能比父类的异常“大”,也不能抛出新的异常。

上面的例子中,Animal内部有eat()方法,而Cat内部可以通过重写来改变eat()的内部实现,如果子类重写了父类的方法,那么在调用的时候就不会和构造函数一样再去父类调用一遍了。

需要注意的是,如果父类定义了一个private的方法,那么在子类中重新编写这个方法,都算不上重写,因为private修饰的方法是不能被子类继承的,重新编写这个函数只能算是子类自己重新构造了一个新的函数。

1
//父类的private函数
2
private void eat(){
3
    System.out.println("animal eat");
4
}
5
6
//子类用overide修饰会报错的
7
@Override
8
public void eat(){
9
10
}

而重载和继承没有任何关系(当然,继承之间也存在重载,也就是说,继承可以重载,但是重载不一定继承),它发生在类本身。重载方法的特点是方法名相同而参数列表不同。

1
private void eat(){
2
    System.out.println("animal eat");
3
}
4
private void eat(int a,int b){
5
    System.out.println("animal eat1");
6
}
7
private void eat(int a,int b,double c){
8
		System.out.println("animal eat2");
9
}

只需要记住,被重载的方法必须改变参数列表(参数个数或类型或顺序不一样)

多态

分类

多态,简而言之就是同一个行为具有多个不同表现形式或形态的能力。

在java中,多态主要包括重写式多态和重载柿多态,也就是在继承中已经讲过的两个类别。

  • 重载式多态,也叫编译时多态。也就是说这种多态在编译时已经确定好了。方法名相同而参数列表不同的一组方法就是重载。在调用这种重载的方法时,通过传入不同的参数最后得到不同的结果。
  • 重写式多态,也叫运行时多态。这种多态通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。也就是说,只有程序运行起来,你才知道调用的是哪个子类的方法。

多态的条件

主要考虑运行时的多态,主要包含以下条件

  • 继承。在多态中必须存在有继承关系的子类和父类。
  • 重写。子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
  • 向上转型。在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

继承与重写已经基本介绍过了,下面主要讲向上转型

向上转型

子类引用的对象转换为父类类型称为向上转型。通俗地说就是将子类对象转为父类对象。此处父类对象可以是接口。

1
Animal a = new Cat();

这就是向上转型,将子类创建的对象转化为父类对象,此时a调用的方法是子类Cat中的方法。

需要注意的是

  • 向上转型时,子类单独定义的方法会丢失。比如在Cat类中定义父类没有的方法,当Animal引用指向Cat类实例时是访问不到除了继承外的其他方法。
  • 子类引用不能指向父类对象。Cat c = (Cat)new Animal()这样是不行的。

向上转型可以使代码变得简洁

多态中的注意事项

在向上转型中,往往会涉及到函数的调用,由于父类和子类都定义了相同的函数,不知道应该调用哪一个。看下面这句话:

当父类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,引用变量类型决定可调用的方法。如果子类中没有覆盖该方法,那么会去父类中寻找。

什么意思呢,比如我们有

1
public class Animal {
2
    protected void test(Animal a){
3
        System.out.println("A_a");
4
    }
5
    protected void test(Cat c){
6
        System.out.println("A_c");
7
    }
8
}
1
public class Cat extends Animal {
2
    protected void test(Animal a){
3
        System.out.println("C_a");
4
    }
5
    protected void test(Cat c){
6
        System.out.println("C_c");
7
    }
8
9
}
1
Animal a = new Cat();
2
Animal a1 = new Animal();
3
a.test(a1);

a决定了可以调用的方法,也就是可以使用protected void test(Animal a)和protected void test(Cat c),而Cat决定了调用哪个类中的方法,也就是使用Cat类中的test方法。

继承链中对象方法的调用的优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)

当子类无法在自身找到可调用的方法的时候,会根据上述的继承链依次调用。