面向对象三大特性:封装、继承(涉及super关键字)和多态;java 动态绑定机制

 目录

封装

基本定义

封装的实现步骤

封装和构造器结合

继承

基本介绍

继承的基本语法

继承的使用细节

继承本质分析

super 关键字

基本介绍

基本语法

super 的使用细节

多态

基本介绍

多态的具体体现

多态的细节和注意事项

java 动态绑定机制

多态的应用

多态数组

多态参数

封装

基本定义

封装(encapsulation)就是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作,才能对数据进行操作。如电视机是一个封装好的整体,人们在使用时不需要关注其内部的运行细节,只需要通过遥控器上的按钮就可以轻松的实现对电视机的操作。

封装有两个好处:

  • 可以隐藏实现的细节
  • 可以对数据进行验证,保证数据的安全性和合理性

封装的实现步骤

  1. 将属性进行私有化,(用 private 修饰属性,让外部不能直接访问和修改属性)
  2. 提供一个公共的(public)set 方法,用于对属性判断并赋值

public void setXxx(类型 参数名) {  // Xxx 表示某个属性

        // 加入数据验证的业务逻辑...

        属性 = 参数名;

}

     3. 提供一个公共的(publiget 方法,用于获取属性的值

public 数据类型 getXxx() {  // Xxx 表示某个属性

        // 权限判断...

        return xxx;

}

封装和构造器结合

为了防止外部直接通过构造器跳过 set 方法修改数据,可以在构造器中调用 set 方法。

class Person {
    private int age;

    public Person(int age){
        // 这种修改数据的方式没有经过数据验证,存放风险
        // this.age = age;

        // 应该调用 set 方法进行修改,这样可以对数据进行验证
        this.setAge(age);
    }

    // 修改数据
    public void setAge(int age){
        if (age >= 1 && age <= 120){
            this.age = age;
        } else {
            System.out.println("输入的年龄有误!");
        }
    }

    // 获取数据
    public int getAge(){
        return this.age;
    }
}

继承

基本介绍

继承可以解决代码复用的问题。当多个类存在相同的属性和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法。所有的子类不需要重新定义这些属性和方法,只需要通过 关键字 extends 来声明继承父类即可。继承示意图如下:

继承的基本语法

class 子类 extends 父类 {

}

说明:

  • 子类会自动拥有父类定义的属性和方法
  • 父类又叫超类、基类
  • 子类又叫派生类

继承的使用细节

  • 子类继承了父类所有的属性和方法(包括私有的属性和方法,虽然不能直接访问,但实际上也继承了私有的属性和方法,可以通过 idea 中的 debug 进行查看),子类可以直接访问父类的非私有属性和方法,但是不能直接访问父类的私有属性和方法,要通过父类提供的公共的方法进行访问
// 父类
public class Base {
    public int n1 = 100;
    protected int n2 = 200;
    int n3 = 300;
    private int n4 = 400;

    public Base() {
        System.out.println("Base()....");
    }
    // 父类提供的一个公共的访问私有属性 n4 的方法
    public int getN4(){
        return n4;
    }
    public void test100() {
        System.out.println("test100()....");
    }
    protected void test200() {
        System.out.println("test200()....");
    }
    void test300() {
        System.out.println("test300()....");
    }
    private void test400() {
        System.out.println("test400()....");
    }
    // 父类提供的一个公共的访问私有方法 test400 的方法
    public void callTest400(){
        test400();;
    }
}


public class Sub extends Base{
    public Sub() {
        System.out.println("Sub()....");
    }

    public void sayOk(){
        // 子类可以直接访问父类的非私有属性和方法,但是不能直接访问父类的私有属性和方法
        System.out.println(n1);
        System.out.println(n2);
        System.out.println(n3);
        // System.out.println(n4); // 不可以访问,提示:The field Base.n4 is not visible
        //要通过父类提供的公共的方法间接访问 n4
        System.out.println(getN4());

        test100();
        test200();
        test300();
        // test400(); // 不可以调用,提示:The method test400() from the type Base is not visible
        //要通过父类提供的公共的方法间接访问 test400()
        callTest400();
    }
}
  • 子类必须调用父类的构造器(默认情况下在子类的构造器中已经隐式调用了 super() 语句),完成父类的初始化
  • 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,就必须在子类的每个构造器中用 super 去显式指定使用父类的具体的某一个构造器,完成父类的初始化工作,否则,编译会报错
  • 如果希望指定去调用父类的某个构造器,则可以利用 super 显式调用:super(参数列表),参数列表和要调用的父类构造器的参数列表一致
  • super() 和 this() 都只能在构造器中使用,且都只能放在构造器的第一行,所以这两个方法不能共存在一个构造器中
  • java 所有类都是 Object 类的子类
  • 父类构造器的调用不限于直接父类,而是一直往上追溯到 Object 类(顶级父类)
  • 子类最多只能继承一个父类(指直接继承),即 java 中是单继承机制
  • 不能滥用继承,子类和父类之间必须满足 is - a 的逻辑关系,如猫是一个动物,满足要求可以用继承

继承本质分析

如以下代码:

public class Test {
    public static void main(String[] args){
        Son son = new Son();
    }
}

class GrandPa {
    String name = "爷爷";
    String hobby = "下象棋";
}
class Father extends GrandPa {
    String name = "爸爸";
    int age = 40;
}
class Son extends Father {
    String name = "儿子";
}

针对上面的代码,内存分析示意图如下:

创建好子类对象后,要访问该对象的某个属性时,会先查找子类本身有没有该属性,如果没有就去查找父类,如果父类没有就去查找父类的父类,直到找到该属性或找到 Object 类。

但假设把 Father 类的 age 属性改成私有的,然后在 GrandPa 类里添加一个默认访问级别的属性 age,此时如果要去访问 Son 类对象的 age 属性:son.age,编译期会报错。

原因:由上面的内存示意图可知,在 son 指向的堆内存中,拥有 Father 和 GrandPa 父类的所有属性(不管是私有的还是其他的),当编译器查找 age 这个属性时,Son 类中没有 -> 往上查找,找到 Father 类,找到了 age 这个属性,查找结束,不会再继续向上在 GrandPa 类中查找。所以即使 Father 类中的 age 属性是私有的不能被 son 访问,但是也是查找到了,而因为私有属性不能直接访问,所以就会报错。

public class Test {
    public static void main(String[] args){
        Son son = new Son();
        System.out.println(son.age); // 错误
    }
}

class GrandPa {
    String name = "爷爷";
    String hobby = "下象棋";
    int age = 70;
}
class Father extends GrandPa {
    String name = "爸爸";
    private int age = 40;
}
class Son extends Father {
    String name = "儿子";
}

super 关键字

基本介绍

super 代表父类的引用,用于访问父类的属性、方法和构造器。

基本语法

  • 访问父类的属性,但不能访问父类的 private 属性:super.属性名
  • 访问父类的方法,但不能访问父类的 private 方法:super.方法名(参数列表)
  • 访问父类的构造器,只能在父类的构造器中使用,并且必须放在第一行:super(参数列表);参数列表和要访问的父类构造器的参数列表一致

super 的使用细节

  • 调用父类构造器的好处:分工明确,父类属性由父类初始化,子类属性由子类初始化
  • 当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过 super ,如果没有重名,使用 super、this 和直接访问时一样的效果。
public class Test {
    public static void main(String[] args){
        Zi zi = new Zi();
        zi.sayhi();
    }
}
class Fu {
    public void hi(){
        System.out.println("Fu 类中的 hi()...");
    }
}

class Zi extends Fu {
    public void hi(){
        System.out.println("Zi 类中的 hi()...");
    }

    public void sayhi(){
        /**
         * hi();/this.hi(); 语句执行过程:
         * 1、先查找本类中有没有 hi() 方法,如果有并且可以访问,则直接执行本类的 hi() 方法
         * 2、如果本类中没有 hi() 方法,去查找父类中有没有 hi() 方法,
         *      如果有并且能够访问,则执行父类中的 hi() 方法,
         *      如果有但不能访问(父类中的 hi() 方法是私有的),则报错,进行相应的提示
         * 3、如果父类中没有 hi() 方法,继续往上查找父类的父类,判断情况同 2
         * 4、如果查找到 Object 类还是没有找到,则报错提示没有该方法
         */
        hi(); // 与 this.hi() 等价

        /**
         * super.hi();  语句执行过程:
         * 1、直接跳过本类,从父类开始查找 hi() 方法,
         *      如果有并且能够访问,则执行父类中的 hi() 方法,
         *      如果有但不能访问(父类中的 hi() 方法是私有的),则报错,进行相应的提示
         * 2、如果父类中没有 hi() 方法,继续往上查找父类的父类,判断情况同 1
         * 3、如果查找到 Object 类还是没有找到,则报错提示没有该方法
         */
        super.hi();

        // 访问属性的情况同上述的访问方法过程一致,在此不再进行说明
    }
}
  • super 和 this 的比较
No.区别点thissuper
1访问属性访问本类中的属性,如果本类没有此属性,则从父类中继续查找从父类开始查找属性,依次往上查找,直到找到或到 Object
2调用方法访问本类中的方法,如果本类没有此方法,则从父类中继续查找从父类开始查找方法,依次往上查找,直到找到或到 Object
3调用构造器调用本类的构造器,必须放在构造器中的第一行调用父类的构造器,必须放在子类构造器中的第一行
4特殊表示当前对象在子类中访问父类对象

多态

基本介绍

方法或对象具有多种形态,是面向对象的第三大特征,多态是建立在封装和继承基础之上的。

多态的具体体现

(1)方法的多态:方法重载和方法重写就体现了多态

(2)对象的多态

  • 一个对象的编译类型和运行类型可以不一致

Animal animal = new Dog();

说明:animal 的编译类型是 Animal,运行类型是 Dog

这一句代码可以描述为:父类的对象引用指向了子类的对象,因为 animal 是 Animal 这个父类对象的引用,但它不代表真正的对象,真正的对象是堆内存中的 Dog 这个子类对象。 

在创建对象时,对象名可以称为对象的引用,或说指向这个对象,但这个名字并不代表真正的对象,真正的对象时堆内存中的那个框框(参考内存示意图)。

  • 编译类型在定义对象的时候就确定了,不能改变

Animal animal = new Dog();

说明:此时animal 的编译类型就是 Animal,不能改变

  • 运行类型是可以改变的,可以通过 getClass() 来查看运行类型

Animal animal = new Dog();

System.out.println("animal 的运行类型为:" + animal.getClass());

animal = new Cat();

System.out.println("animal 的运行类型为:" + animal.getClass());

说明:animal 的运行类型刚开始是 Dog,但是后面可以进行更改,如上述运行类型更改为Cat类型,但此时编译类型仍然是 Animal

  • 编译类型看定义时 = 的左边,运行类型看 = 的右边

Animal animal = new Dog();

说明:animal 的编译类型看等号的左边,是 Animal,运行类型看等号的右边,即 new 关键字的右边,是 Dog

public class Test {
    public static void main(String[] args) {
        // animal 的编译类型是 Animal,运行类型是 Dog
        Animal animal = new Dog();

        // 一个父类的对象引用,可以指向一个子类对象,运行时是以运行类型为主的
        // 运行类型真正看的是堆内存里的对象
        // 因为 animal 的运行类型是 Dog,所以执行的是 Dog 类里的 cry()
        animal.cry(); // 输出:dog...

        // animal 的编译类型还是 Animal,不能改变,但是现在运行类型变成了 Cat
        animal = new Cat();
        // 因为此时 animal 的运行类型是 Cat,所以执行的是 Cat 类里的 cry()
        animal.cry(); // 输出:cat...
    }
}

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

class Dog extends Animal {
    public void cry() {
        System.out.println("dog...");
    }
}

class Cat extends Animal {
    public void cry(){
        System.out.println("cat...");
    }
}

多态的细节和注意事项

  • 多态的前提是:两个对象(类)存在继承关系
  • 多态的向上转型:
    • 本质:父类的引用指向了子类的对象
    • 语法:父类类型 引用名 = new 子类类型();
    • 特点:编译看左边,运行看右边。可以调用父类中的所有成员(须遵守访问权限),但不能调用子类中的特有成员。最终运行效果要看子类的具体实现。
public class Test {
    public static void main(String[] args) {
        Animal animal = new Cat();

        /**
         * 可以调用父类中的所有成员(须遵守访问权限),但不能调用子类中的特有成员。最终运行效果要看子类的具体实现
         * 可以调用父类里的所有成员(须遵守访问权限)
         * 但是不能调用 Cat 类里特有的方法,因为能不能调用是由编译器决定的,
         * 而 animal 的编译类型是 Animal,所以识别不了 Cat 子类里的特有方法
         * 即因为在编译阶段,能调用哪些成员(属性和方法)是由编译类型决定的
         */
        // animal.catchMouse(); // 报错

        /**
         * 最终的运行效果看子类(运行类型)的具体实现,
         * 即调用方法时,先从子类查找是否有该方法,有则调用,
         * 否则查找父类,规则同之前的方法调用规则一致
         */
        // Cat 类里有 eat 方法,直接调用
        animal.eat(); // 猫吃鱼
        // Cat 类里没有 run 方法,查找其父类 Animal,有并且可以访问,调用
        animal.run(); // 动物跑步
        // Cat 类里没有 show 方法,查找其父类 Animal,有并且可以访问,调用
        animal.show(); // Animal show
    }
}

class Animal {
    String name = "动物";
    int age = 10;

    public void sleep(){
        System.out.println("动物睡觉");
    }

    public void run(){
        System.out.println("动物跑步");
    }

    public void eat(){
        System.out.println("动物吃东西");
    }

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


class Cat extends Animal {
    public void eat(){ // 重写 Animal 里的 eat 方法
        System.out.println("猫吃鱼");
    }

    public void catchMouse(){ // Cat 类特有的方法
        System.out.println("猫抓老鼠");
    }
}
  • 多态的向下转型
    • 语法:子类类型 引用名 = (子类类型)父类引用;
    • 只能强转父类的引用,不能强转父类的对象
    • 要求父类的引用必须指向的是当前目标类型的对象,即父类引用必须指向要强转的子类对象
    • 向下转型后,可以调用子类类型中的所有成员
public class Test {
    public static void main(String[] args) {
        Animal animal = new Cat();

        // 语法:子类类型 引用名 = (子类类型)父类引用;
        // 此时 cat 的编译类型和运行类型都是 Cat
        // animal 此时实际指向的是 Cat 对象,可以向下转型为 Cat
        Cat cat = (Cat) animal;
        // 向下转型后,可以调用子类类型中的所有成员
        cat.catchMouse(); // 猫抓老鼠

        // 只能强转父类的引用,不能强转父类的对象
        // 错误,a 指向的是 Animal 对象,不能强转为 Cat
        // Animal a = new Animal();
        // Cat c = (Cat) a;

        // 要求父类的引用必须指向的是当前目标类型的对象,即父类引用必须指向要强转的子类对象
        // animal 这个父类引用此时实际指向的是 Cat 对象,可以向下转型为 Cat
        Cat cat1 = (Cat) animal;
    }
}
  • 属性没有重写的说法,属性的值看编译类型
public class Test {
    public static void main(String[] args) {
        // 属性的值直接看编译类型
        Base base = new Sub();
        // base 的编译类型是 Base,所以输出的是 Base 里的 num 属性
        System.out.println(base.num); // 10

        Sub sub = new Sub();
        // sub 的编译类型是 Sub,所以输出的是 Sub 里的 num 属性
        System.out.println(sub.num); // 999
    }
}

class Base {
    int num = 10;
}

class Sub extends Base {
    int num = 999;
}
  • instanceof 比较操作符,用于判断对象的运行类型是否为 XX 类型或 XX 类型的子类
public class Test {
    public static void main(String[] args) {
        // bb 的编译类型和运行类型都是 BB
        BB bb = new BB();
        // bb 是 BB 类型,返回 true
        System.out.println(bb instanceof BB); // true
        // bb 是 AA 类型的子类,返回 true
        System.out.println(bb instanceof AA); // true

        // aa 的编译类型是 AA ,运行类型是 BB
        AA aa = new BB();
        // instanceof 看的是运行类型,所以 aa 是 BB 类型,返回 true
        System.out.println(aa instanceof BB); // true
        // instanceof 看的是运行类型,所以 aa 是 AA 类型的子类,返回 true
        System.out.println(aa instanceof AA); // true
    }
}

class AA {}

class BB extends AA {}

java 动态绑定机制

  • 当调用对象的方法时,该方法会和对象的内存地址/运行类型绑定
  • 当调用对象属性时,没有动态绑定机制,属性在哪里声明就在哪里使用
  • 上述两句话个人认为也可以这么理解:方法的调用看的是运行类型,属性的调用看的是编译类型

public class Test {
    public static void main(String[] args) {
        // a 编译类型为 A ,运行类型为 B
        A a = new B();

        /**
         * 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
         * 当调用对象属性时,没有动态绑定机制,属性在哪里声明就在哪里使用
         * 上述两句话个人认为也可以这么理解:方法的调用看的是运行类型,属性的调用看的是编译类型
         * a.sum() 执行过程:
         *  1、调用 sum() ,sum 方法和 a 的内存地址/运行类型绑定,此时内存地址/运行类型为 B 类
         *  2、B 类里没有 sum() ,执行继承机制,去父类 A 里找,找到并执行
         *  3、执行父类 A 里的 sum() ,在 return 语句中,又调用了 getI()
         *  4、调用 getI() ,getI 方法和 a 的内存地址/运行类型绑定,此时内存地址/运行类型为 B 类
         *  5、B 类里有 getI() ,所以执行的是 B 类里的 getI 方法
         *  6、因为对象属性没有动态绑定机制,所以直接看属性所在类的属性,如果子类没有该属性,再去查找父类,即仍然遵循就近原则
         *  7、所以 B 类里的 getI 方法返回 B 类里的 i 属性,为20
         *  8、所以父类 A 里的 sum() 方法返回20 + 20 = 30
         *  9、所以最终输出 30
         */
        System.out.println(a.sum()); // 30
        
        /**
         * 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
         * 当调用对象属性时,没有动态绑定机制,属性在哪里声明就在哪里使用
         * 上述两句话个人认为也可以这么理解:方法的调用看的是运行类型,属性的调用看的是编译类型
         * a.sum1() 执行过程:
         *  1、调用 sum1() ,sum1 方法和 a 的内存地址/运行类型绑定,此时内存地址/运行类型为 B 类
         *  2、B 类里没有 sum1() ,执行继承机制,去父类 A 里找,找到并执行
         *  3、执行父类 A 里的 sum1() ,在 return 语句中,返回 i + 10
         *  4、因为对象属性没有动态绑定机制,所以直接看属性所在类的属性,如果子类没有该属性,再去查找父类,即仍然遵循就近原则
         *  5、所以父类 A 里的 sum1() 方法返回 10 + 10 = 20
         *  6、所以最终输出 20
         */
        System.out.println(a.sum1()); // 20
    }
}

class A {
    public int i = 10;

    public int sum(){
        return getI() + 10;
    }

    public int sum1(){
        return i + 10;
    }

    public int getI(){
        return i;
    }
}

class B extends A {
    public int i = 20;

    public int getI(){
        return i;
    }
}

多态的应用

多态数组

数组的定义类型为父类类型,里面保存的数组元素类型为子类类型。

应用实例:创建一个 Person 对象,2个 Student 对象和2个 Teacher 对象,统一放在数组中,并调用每个对象的 say 方法

应用实例升级:调用子类特有的方法,比如 Teacher 有一个 teach 方法,Student 有一个 study 方法

public class Test {
    public static void main(String[] args) {
        Person[] persons = new Person[5];
        
        // 向上转型
        persons[0] = new Person("person", 100);
        persons[1] = new Student("stu1", 18, 98);
        persons[2] = new Student("stu2", 18, 60);
        persons[3] = new Teacher("tea1", 30, 8000);
        persons[4] = new Teacher("tea2", 45, 10000);

        for(int i = 0; i < persons.length; i++){
            // 应用实例
            // persons[i] 的编译类型都是 Person ,运行类型看具体的情况
            System.out.println(persons[i].say());

            // 应用实例升级
            if(persons[i] instanceof Student){
                Student s = (Student)persons[i]; // 向下转型
                s.study();
            }

            if(persons[i] instanceof Teacher){
                Teacher t = (Teacher)persons[i]; // 向下转型
                t.teach();
            }
            System.out.println("--------------------------------");
        }
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void setName(String name){
        this.name = name;
    }

    public String getName(){
        return name;
    }

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

    public int getAge(){
        return age;
    }

    public String say(){
        return "name= " + name + "\tage= " + age;
    }
}

class Student extends Person {
    private double score;

    public Student(String name, int age, double score){
        super(name, age);
        this.score = score;
    }

    public void setScore(double score){
        this.score = score;
    }

    public double getScore(){
        return score;
    }

    @Override
    public String say(){
        return "学生: " +  super.say() + "\tscore= " + score;
    }

    public void study(){
        System.out.println("学生" + getName() + "在学java...");
    }
}


class Teacher extends Person {
    private double salary;

    public Teacher(String name, int age, double salary){
        super(name, age);
        this.salary = salary;
    }

    public void setSalary(double salary){
        this.salary = salary;
    }

    public double getSalary(){
        return salary;
    }

    @Override
    public String say(){
        return "老师: " +  super.say() + "\tsalary= " + salary;
    }

    public void teach(){
        System.out.println("老师" + getName() + "在教java...");
    }
}

多态参数

方法定义的形参类型为父类类型,实参类型允许为子类类型

public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        // 实参为子类类型 Student 、Teacher
        // Student 、Teacher 类和多态数组中的定义一致
        test.showInfo(new Student("stu", 10, 90));
        test.showInfo(new Teacher("tea", 40, 9000));
    }

    // 形参为父类类型 Person 
    public void showInfo(Person person){
        System.out.println(person.say());
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值