Java基础之面向对象三大特性:封装、继承和多态


封装

封装指的是:利用抽象数据类型(比如我们常说的类)将 数据对数据的操作 封装在一起,使其构成一个完整的、不可分割的独立实体,这些包装起来的数据就构成了抽象数据类型的属性,而对数据的操作则成为了方法。

在整个封装过程中,数据将会被保存在抽象数据类型的内部,并尽可能地隐藏内部的实现细节,同时提供对这些数据进行操作的接口,通过这些接口使外面的对象可以访问和修改该对象的内部数据。

封装有以下几大好处:

  • 良好的封装能够减少耦合
  • 可以自由修改类内部的结构
  • 可以对成员数据进行精确的控制
  • 隐藏内部信息和具体实现细节

比如,封装 Person 类型:

public class Person {

    /*
     * 对属性进行封装:姓名、性别、年龄
     */
    private String name;
    private String sex;
    private int age;

    /*
     * 提供对外开放的 setter()、getter() 接口
     */
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
    
    public String getSex() {
        return this.sex;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }
}

从上面可以看出,封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。到这里为止,好像也看不出封装的好处,那我们从程序的角度来分析封装带来的好处,如果我们不使用封装,那么该对象就没有 setter() 和 getter(),那么Person 类应该这样写:

public class Person {
    private String name;
    private String sex;
    private int age;
}

程序中就要这样使用它:

Person person = new Person();
person.age = 24;

但是哪天我们要把年龄 age 的类型改为 String 类型呢?只有一处使用了这个类还好,万一有几十甚至上百个地方使用了的话岂不是要改到崩溃?但如果我们使用了封装的话,我们完全只需要稍微修改一下 setAge() 方法即可:

public void setAge(int age) {
	// 使用类型转换即可
    this.age = String.valueOf(age);
}

这样,所有使用 person.setAge(24) 的地方都保持不变。所以,到这里我们可以看出:封装确实可以使我们容易地修改类的内部实现,而无需修改使用了该类的客户代码

我们再来看一下封装的另一个好处:对成员变量的精确控制

假如没有封装,我们进行年龄设置的时候,不小心设置成了这样:

Person person = new Person();
person.age = 300;

很明显,实际的情况下,年龄不应该出现这么大的数字的,如果自己粗心写错并发现了还好,如果没有发现或者别人使用错了那就麻烦了。但使用封装我们就可以避免这种类型的问题,我们可以在 age 的访问接口进行一些控制:

public void setAge(int age) {
	// 控制年龄输入范围
    if (age < 0 || age > 120) {
        System.out.println("年龄设置错误!");
    } else {
        this.age = age;
    }
}

通过对接口数据的控制和过滤来保护成员变量从而提高程序的安全性,这是一种很好的设计思想。到这里为止,大家应该都体会到封装的好处了吧。


继承

继承是建立在封装之上的,有了封装才有继承,继承的目的是:复用代码

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。

继承所描述的是 “is-a” 的关系,如果有两个对象A和B,若可以描述为"A is B",则可以表示A继承B,其中B是被继承者称之为父类或者超类,A是继承者称之为子类或者派生类。

实际上继承者是被继承者的特殊化,它除了拥有被继承者的特性外,还拥有自己独有得特性。在继承关系中,继承者完全可以替换被继承者,反之则不可以,例如:我们可以说猫是动物,但不能说动物是猫就是这个道理,我们把这个称之为"向上转型"

继承有三个重点需要牢记:

  • 子类只能拥有父类非 private 的属性和方法
  • 子类可以拥有自己属性和方法,即子类可以对父类进行扩展
  • 子类可以用自己的方式实现父类的方法
注意:子类只能继承父类的非 private 的属性和方法,父类的 private 属性和方法只能在父类的类中操作,换句话说,private 属性和方法只限定在类本身内部操作,无法被继承。

当然,说到继承,必定少不了这三个东西:构造器、protected关键字、向上转型。

☕️ 构造器

子类除了不能继承父类的 private 属性和方法以外,父类的构造器也是无法继承的。只能够被调用,而不能被继承,想调用父类的构造方法我们需要使用 super() 函数。

在继承体系中,构造器的正确初始化非常重要,我们必须要保证一点:子类的构造器中必须调用父类的构造器来完成父类的初始化。我们看一下下面这个例子:

public class Person {

    private String name;
    private String sex;
    private int age;
    
    public Person() {
        System.out.println("construct Person...");
    }
}

public class Student extends Person {

    private int id;

    public Student() {
    	super();
        System.out.println("construct Student...");
    }

    public static void main(String[] args) {
        Student stu = new Student();
    }
}

结果输出:
construct Person…
construct Student…

这里,由于 Student 构造器调用的是父类的默认构造器,所以可以省略不写,因为编译器会默认给子类调用父类的默认构造器,但这个前提是父类必须给出默认构造器,如果父类没有默认构造器,就需要我们显示的使用 super() 调用父类构造器,否则编译器将报错。

总结而来就是:对于继承而已,子类会默认调用父类的构造器,但是如果没有默认的父类构造器,子类必须要显示的指定父类的构造器,而且必须是在子类构造器中做的第一件事(第一行代码)。

☕️ protected 关键字

我们知道,类的属性和方法有三种访问限制修饰符:public、protected、private,其中子类可以访问父类的非 private 属性和方法,即子类可以访问父类的 public 和 protected 所修饰的属性和方法。但是,在 java 中,protected 所修饰的对象还可以被同一个包下的类所访问

☕️ 向上转型

我们知道继承是is-a的相互关系,比如:猫继承于动物,所以我们可以说猫是动物,或者说猫是动物的一种。像这种把猫看做动物的现象就是向上转型,例子如下:

public class Animal {
	public void run() {
		System.out.println("Animal run...");
	}

	public static void showRun(Animal animal) {
		animal.run();
	}
}

public class Cat extends Animal {
	public static void main(String[] args) {
         Cat cat = new Cat();
         Animal.showRun(cat);      //向上转型
     }
}

上面的例子中,把子类 cat 传给父类的静态方法 showRun(),会将子类转换成父类,在继承关系上面是向上移动的,所以一般称之为向上转型。由于向上转型是从一个叫专用类型向较通用类型转换,所以它总是安全的,唯一发生变化的可能就是属性和方法的丢失。这就是为什么编译器在"未曾明确表示转型"或"未曾指定特殊标记"的情况下,仍然允许向上转型的原因。

☕️ 谨慎继承

首先我们需要明确,继承存在如下缺陷:

  • 父类变,子类就必须变
  • 继承破坏了封装,对于父类而言,它的实现细节对与子类来说都是透明的
  • 继承是一种强耦合关系

所以在纠结什么时候使用继承或者要不要使用继承时,我们可以参考一下《Think in java》中一种方法:问一问自己是否需要从子类向父类进行向上转型。如果必须向上转型,则继承是必要的,但是如果不需要,则应当好好考虑自己是否需要继承。


多态

我们知道,封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而言它的内部细节是隐藏的,暴露给外界的只是它的访问方法。

而继承是为了重用父类代码。两个类若存在IS-A的关系就可以使用继承。同时继承也为实现多态做了铺垫

🍭 多态的概念:

所谓多态就是指程序中定义的引用变量所指向的具体类型和该引用变量所调用的方法在编译时并不确定,只有在程序运行期间才确定。即:一个引用变量倒底会指向哪个类的实例对象,以及该引用变量所调用的方法到底是哪个类中实现的方法,必须在程序运行期间才能决定。

因为在程序运行时才确定具体的类,这样,在不修改源程序代码的情况下,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用变量调用的具体方法随之改变。即:不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

多态的实现需要借助于向上转型这个特性,但是向上转型存在一些缺憾,那就是它必定会导致一些方法和属性的丢失,而导致我们不能够获取它们。所以父类类型的引用可以调用父类中定义的所有属性和方法,对于只存在与子类中的方法和属性它就无法使用了。看下面的例子:

public class Person {

    public void func1() {
        System.out.println("调用 Person 的 func1");
        func2();
    }

    public void func2() {
        System.out.println("调用 Person 的 func2");
    }
}

public class Student extends Person {

    // 重载父类方法,父类不存在这个方法,向上转型后,父类不能调用这个方法
    public void func1(String a) {
        System.out.println("调用 Student 的 func1");
        func2();
    }

    // 重写父类方法,指向子类对象的父类引用调用func2时必定调用该方法
    public void func2() {
        System.out.println("调用 Student 的 func2");
    }
}

public class Test {
	public static void main(String[] args) {
        Person test = new Student(); // 父类引用指向子类对象
        test.func1();
    }
}

结果输出:
调用 Person 的 func1
调用 Student 的 func2

从程序的运行结果中我们发现,test.fun1() 首先是运行父类中的 fun1().然后再运行子类中的 fun2()。

分析:在这个程序中子类重载了父类的方法 func1(),重写了 func2(),由于重载后的 func1(String a) 和 func1() 不是同一个方法,所以父类并没有这个方法,在向上转型后就会丢失该方法,所以执行 test.fun1() 是无法调用 func1(String a) 方法的。而子类重写了 func2() ,那么在调用func2()时就会调用子类的 func2() 方法。

所以,对于多态,我们可以总结如下:

指向子类对象的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,哪怕是重载的方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。

Java实现多态有三个必要条件:继承、重写、向上转型。

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

对于Java而言,它多态的实现机制遵循一个原则:当父类对象引用变量引用子类对象时,由被引用对象的类型而不是引用变量的类型决定了该调用谁的成员方法,但是这个被调用的方法必须是在父类中定义过的,并且被子类覆盖的方法。

在Java中有两种形式可以实现多态:继承和接口

🍭 基于继承实现的多态

基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。

所以基于继承实现的多态可以总结如下:对于引用子类对象的父类引用,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。

如果父类是抽象类,那么子类必须要实现父类中所有的抽象方法,这样该父类所有的子类一定存在统一的对外接口,但其内部的具体实现可以各异。这样我们就可以使用顶层类提供的统一接口来处理该层次的方法。

🍭 基于接口实现的多态

继承是通过重写父类的同一方法的几个不同子类来体现的,那么就可以通过实现接口并覆盖接口中同一方法的几不同的类体现的。

在接口的多态中,指向接口的引用必须是实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。

继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。

如 Map map =new HashMap ; List list=new ArraryList 都是基于接口实现的多态。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章