【Java SE】面向对象编程(中级)

目录

1.package、import

1.1 package(包)

1.1.1 语法格式

1.1.2 注意事项

1.1.3 包的作用

1.2 import(导入)

1.2.1 语法格式

1.2.2 应用举例

1.2.3 注意事项

2.访问修饰符(背)

2.1 基本介绍

2.2 注意事项

3.面向对象特征之一:封装(encapsulation) 

3.1 什么是封装

3.2 为什么需要封装

3.3 封装的实现步骤

3.3.1 成员变量/属性私有化

3.3.2 成员变量/属性私有化的好处

3.3.3 封装与构造器的结合使用

4. 面向对象特征之二:继承

4.1 生活中的继承

4.2 Java中的继承

4.2.1 角度:从上而下

4.2.2 角度:自下而上

4.3 继承的好处

4.4 继承的语法

4.5 继承的深入讨论/细节问题

4.6 子类对象实例化过程

4.7 练习题

5.super关键字

5.1 基本介绍

5.2 基本语法

5.3 注意事项

6.super、this的对比(非常重要)

7.方法重写/方法覆盖(Override)

7.1 快速入门案例

7.2 方法重写的细节

8.重写与重载的对比

9.面向对象特征之三:多态 

9.1 快速入门案例

9.2 为什么需要多态?

9.3 对象的多态

9.4 方法形参的多态性

9.5 方法返回类型的多态性

9.6 数组的多态性

9.7 多态的向上转型

9.7.1 语法

9.7.2 注意事项

9.8 instanceof

9.9 多态的向下转型

9.9.1 语法

9.9.2 示例代码

9.10 成员变量没有多态性

9.11 动态绑定机制

9.12 虚方法调用

9.13 多态练习题


1.package、import

1.1 package(包)

package,称为包,用于指明定义的类、接口等结构所在的包

1.1.1 语法格式

package 顶层包名.子包名 

假设 Person.java 的存放路径是 \pack1\pack2 ,那么 Person.java 中的顶部第一条语句就是 package pack1.pack2,如下所示

1.1.2 注意事项

  • 一个源文件( .java 文件)只能有一个声明包的 package 语句,且 package 语句作为第一条语句出现。若缺少该语句,则指定为无名包
  • 包名:属于标识符,满足标识符命名的规则和规范即可
  • 同一个包下可以声明多个类、接口,但不能定义同名的类、接口。不同的包下可以定义同名的类、接口

1.1.3 包的作用

包的作用 

1.2 import(导入)

为了使用定义在其它包中的 Java 类,需用 import 语句来显式引入指定包下所需要的类。相当于 import 语句告诉编译器到哪里去寻找这个类

1.2.1 语法格式

import 包名.类名

1.2.2 应用举例

pack1\pack2 下创建 Person.java,在 demo 目录下创建 Cat.java,然后在 Person.java 中创建一个 Cat 对象

Cat.java

package demo;

public class Cat {
    public void eat(){
        System.out.println("cat eat");
    }
}

Person.java 

package pack1.pack2;
import demo.Cat;
public class Person {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.eat();
    }
}

1.2.3 注意事项

(1) import 语句,声明在包的声明(package xx.xx)和类的声明之间

(2)如果使用 .*  ,即导入包下的所有结构。举例:可以使用 java.util.* 的方式,会一次性导入util包下所有的类或接口

(3)建议使用到哪个类,就导入哪个类即可,不建议使用 .* 导入所有结构

(4)如果导入的类或接口是在当前包下的,则可以省略 import 语句

2.访问修饰符(背)

2.1 基本介绍

Java 提供 4访问控制修饰符,用于控制方法、属性(成员变量)的访问权限(访问范围)

 4 种访问控制修饰符:public 、 protected 、 缺省 、 private

(1)public:公开级别,对外公开

(2)protected:对子类和同一个包中的类公开

(3)缺省:没有修饰符号,向同一个包的类公开

(4)private:只有类本身可以访问,子类也不能访问,不对外公开。是最严格的级别

本类内部本包内其他包的子类其他包非子类
public
protected
缺省
private

2.2 注意事项

外部类、接口(interface)只能用 public、缺省 访问控制修饰符

成员变量、成员方法、构造器、成员内部类 4 种访问控制修饰符都可以使用

举例 

 下面例子中,Person 即外部类, Dog 即成员内部类

public class Person{
	public String name;
	public Dog dog;
}

3.面向对象特征之一:封装(encapsulation) 

面向对象的三大特征:封装、继承、多态

3.1 什么是封装

封装就是把客观事物封装成抽象概念的类,并且类可以把自己的数据和方法只向可信的类或者对象开放,向没必要开放的类或者对象隐藏信息

通俗的讲就是把该隐藏的隐藏起来,该暴露的暴露出来,这就是封装的设计思想

3.2 为什么需要封装

  • 我要用洗衣机,只需要按一下开关和洗涤模式即可。有必要了解洗衣机内部的结构吗?

  • 我要开车,需要懂离合、油门、制动等原理吗?

  • 客观世界里每一个事物的内部信息都隐藏在其内部,外界无法直接操作和修改,只能通过指定的方式进行访问和修改

随着系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循 "高内聚、低耦合"

内聚:指一个模块内各个元素彼此结合的紧密程度

耦合:指一个软件结构内不同模块之间互连程度的度量

内聚意味着重用和独立,耦合意味着多米诺效应牵一发动全身

"高内聚,低耦合"  在 Java 中的体现之一:

高内聚 :类的内部数据操作细节自己完成,不允许外部干涉

低耦合 :仅暴露少量的方法给外部使用,尽量方便外部调用

3.3 封装的实现步骤

3.3.1 成员变量/属性私有化

概述:私有化类的成员变量,提供公共的 getset 方法,对外暴露获取和修改属性的功能。在Java 的开发中,一般都会这样做

步骤

(1)将属性作 Private 私有化,这样就不能在其他类直接修改了

(2)提供一个公共的(Public) set 方法,用于对属性合法判断并赋值

public void setXxx(类型 参数名){Xxx表示某个属性
    //加入数据验证的业务逻辑
    属性 = 参数名;
}

(3)提供一个公共的(Public) get 方法,用于获取属性的值

public 数据类型 getXxx(类型 参数名){Xxx表示某个属性
    return Xxx;
}

案例 

创建 Person

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        //增加判断逻辑
        if (name.length()>=2 && name.length()<=6){
            this.name = name;
            return;
        }
        System.out.println("Invalid name!");
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        //增加判断逻辑,用户有可能将age赋值为负值
        if (age <= 0){
            System.out.println("Invalid age!");
            return;
        }
        this.age = age;
    }
}

 验证

public class PersonTest {
    public static void main(String[] args) {
        Person p = new Person();

        /*实例变量用private修饰,跨类是无法直接调用的
           p.name = "小马"; X,错误
           p.age = 20; X,错误
         */

        p.setName("小马");
        p.setAge(20);
        System.out.println("name:"+p.getName()+" age:"+p.getAge());
    }
}

3.3.2 成员变量/属性私有化的好处

  • 让使用者只能通过事先预定的方法访问数据,不能直接访问(即直接对象名.成员变量名),从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问和赋值

  • 便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改

3.3.3 封装与构造器的结合使用

在构造器中结合 SetXxx 方法即可,这样即使用构造器方法初始化对象时也可以判断属性赋值的合法性

案例 

创建 Person

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

    public Person(String name, int age) {
        setName(name);
        setAge(age);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        //增加判断逻辑
        if (name.length()>=2 && name.length()<=6){
            this.name = name;
            return;
        }
        System.out.println("Invalid name!");
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        //增加判断逻辑,用户有可能将age赋值为负值
        if (age <= 0){
            System.out.println("Invalid age!");
            return;
        }
        this.age = age;
    }
}

创建测试类 PersonTest

public class PersonTest {
    public static void main(String[] args) {
        Person p = new Person("小马", 20);
        System.out.println("name:"+p.getName()+" age:"+p.getAge());
    }
}

4. 面向对象特征之二:继承

4.1 生活中的继承

财产继承

绿化:前人栽树,后人乘凉 

样貌继承 

样貌继承之外,还可以样貌进化

补充:继承有延续(下一代延续上一代的基因、财富)、扩展(下一代和上一代又有所不同)的意思  

4.2 Java中的继承

4.2.1 角度:从上而下

通过继承,可以简化 Student 类的定义:  

说明:Student 类继承了父类 Person 的所有属性和方法,并自己增加了一个属性 school . Person 中的属性和方法,Student 都可以使用  

4.2.2 角度:自下而上

多个类中存在相同属性和行为时,可以将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成继承关系。如下图所示  

4.3 继承的好处

  • 继承的出现减少了代码冗余,提高了代码的复用性,有利于更能的扩展

  • 继承的出现让类与类之间产生了 is-a 的关系,为多态的使用提供了前提

  • 父类更通用、更一般,子类更具体

4.4 继承的语法

通过 extends 关键字,可以声明一个类B继承另外一个类A,定义格式如下:

[修饰符] class 类A {
	...
}

[修饰符] class 类B extends 类A {
	...
}

类B:称为子类、派生类(derived class)、SubClass

类A:称为父类、超类、基类(base class)、SuperClass

代码示例 

创建父类 Animal 

//定义动物类,作为父类
public class Animal {
    String name;
    int age;

    //定义动物的吃东西方法
    public void eat() {
        System.out.println(age + "岁的"
                + name + "在吃东西");
    }
}

创建子类 Cat

//定义猫类Cat 继承 动物类Animal
public class Cat extends Animal {
    int count;//记录每只猫抓的老鼠数量

    // 定义一个猫抓老鼠的方法catchMouse
    public void catchMouse() {
        count++;
        System.out.println("抓老鼠,已经抓了"
                + count + "只老鼠");
    }
}

创建测试类 TestCat 

public class TestCat {
    public static void main(String[] args) {
        Cat cat = new Cat();
        
        //对猫从父类继续来的属性赋值
        cat.name = "Tom";
        cat.age = 2;

        //调用该猫继承来的eat()方法
        cat.eat();
        //调用该猫的catchMouse()方法
        cat.catchMouse();
        cat.catchMouse();
        cat.catchMouse();
    }
}
/*输出结果
2岁的Tom在吃东西
抓老鼠,已经抓了1只老鼠
抓老鼠,已经抓了2只老鼠
抓老鼠,已经抓了3只老鼠
*/

4.5 继承的深入讨论/细节问题

(1)子类会继续父类的所有属性和方法,但是如何访问具体还得看这个属性和方法的修饰符是什么。即需要遵守前面 2.访问修饰符 中那个表

验证,debug 查看子类是否继承了父类的全部属性


(2)创建子类对象时,子类必须调用父类的构造器,完成父类的初始化(必须要做的)

  • 父类有无参构造器:当创建子类对象时,不管使用子类的哪一个构造器,默认情况下会调用父类的无参构造器。即子类的任何一个构造器第一行会默认有一个 super()来调用父类的无参构造器,除非你显示定义使用其他的父类构造器,即 super(实参列表)

验证


  • 父类没有无参构造器:必须在子类的构造器中用 super()去指定使用父类的哪个构造器完成对父类的初始化工作,否则,编译不会通过

    • 父类没有无参构造器的情况:类中定义了参构造器,没有显示定义无参构造器,所以系统也不会自动生成无参构造器

验证


(3)super 在使用时,必须放在构造器第一行,且 super 只能在构造器中使用

(4)super() 、 this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器,构造器中要调用其他构造器要么用 super 要么用 this ,两者必须存一

(5)Java 中所有类都是 Object 类的子类,  即 Object 是所有类的父类

用前面的 Base 和 Sub 做一下 debug 验证 

(6) 父类构造器的调用不限于直接父类,会一直往上追溯直到 Object 类 (顶级父类),如下图所示

(7)Java 只支持单继承:一个父类可以有多个子类,但一个子类只能有一个父类

思考:如何让 A "继承" B类和C类?

解决方案:【A继承B,B继承C】

 (8)继承不能滥用,子类和父类最好满足 is-a 的逻辑关系

Person类 Music类
Person is a Music?
Music extends Person//X,不合理
    
Animal类 Cat类
Cat is a Animal?
Cat extends Animal//√,合理

(9)子类不是父类的子集,而是对父类的 "扩展"

4.6 子类对象实例化过程

示例代码

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 = 39;
}
class Son extends Father{
    String name = "儿子";
}

内存原理图

 在调用 son.name 时,要按照查找关系来返回对应的值

(1)首先看子类是否有该属性

(2)如果子类有这个属性,并且可以访问(看访问控制符),则返回信息

(3)如果子类没有这个属性,就看父类有没有这个属性(如果父类有该属性,并且可以访问,则返回信息)

(4)如果父类没有就按照(3)的规则,继续找父类的父类,直到找到 Object

(5)如果经过(1)-(4)都没有找到属性,则报错

4.7 练习题

练习题1

public class test{
    public static void main(String[] args){
        B b = new B();
    }
}

class A{
	A(){
		System.out.println("a");
	}
	A(String name){
		System.out.println("a name");
	}
}
class B extends A{
	B(){
		this("abc");
		System.out.println("b");
	}
	B(String name){
		System.out.println("b name");
	}
}
/*输出结果
a
b name
b
*/

练习题2 

public class test {
    public static void main(String[] args) {
        C c = new C();
    }
}

class A {
    public A() {
        System.out.println("我是A类");
    }
}

class B extends A {
    public B() {
        System.out.println("我是B类的无参构造");
    }

    public B(String name) {
        System.out.println(name + "我是B类的有参构造");
    }
}

class C extends B {
    public C() {
        this("hello");
        System.out.println("我是C类的无参构造");
    }

    public C(String name) {
        super("hahah");
        System.out.println("我是C类的有参构造");
    }
}
/*输出结果
我是A类
hahah我是B类的有参构造
我是C类的有参构造
我是C类的无参构造
*/

5.super关键字

5.1 基本介绍

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

5.2 基本语法

(1)访问父类属性super.属性名

同样要看到属性的修饰符是什么;例如父类中的 Private 属性在子类中用 super.属性名 仍然不能访问

(2)访问父类方法:super.方法名(参数列表)

同样要看方法的修饰符是什么;例如父类中的 Private 方法在子类中用 super.方法名 仍然不能访问

(3)访问父类构造器:super(),super(参数)

super()super(参数) 只能出现在构造器的首行

5.3 注意事项

  • super 的追溯不限于直接父类

  • 如果多个基类(上级类)中都有同名的成员,使用 super 访问遵循''就近原则''

  • 当子类中有和父类中的属性或方法重名时,为了访问父类的属性或方法,必须通过 super 。如果没有重名的,使用 super、this、直接访问 三种方式都是一样的效果 

super、this、直接访问三种方式示例

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

class Father {
   public Father() {
   }
   public void father_method() {
       System.out.println("father method");
   }
}

class Son extends Father {
   public Son() {
   }

   public void test(){
       //当无重名方法或属性时,三种调用都可行
       this.father_method();
       super.father_method();
       father_method();
   }


}
/*输出结果
father method
father method
father method
*/

6.super、this的对比(非常重要)

thissuper
访问属性

访问本类中的属性,如果本类没有该属性则从父类中查找

从父类开始查找属性
调用方法

访问本类中的方法,如果本类没有该方法则从父类中查找

从父类开始查找方法
调用构造器调用本类构造器,必须放在构造器首行调用父类构造器,必须放在子构造器的首行
特殊表示当前对象子类中访问父类对象

super:追溯不限于直接父类

this:调用构造器时不会追溯到其父类,只会调用本类的构造器;调用属性/方法时 this 的追溯同样不限于直接父类

7.方法重写/方法覆盖(Override)

场景问题:父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类的实现不适合于自己当前的类,该怎么办呢?

解决方案:子类可以对从父类中继承来的方法进行改造,我们称为方法的重写(Overwrite)。也称为方法的覆盖(Override)

7.1 快速入门案例

public class Test {
    public static void main(String[] args) {
        Son son = new Son();
        son.cry("小马");
        son.test();
    }
}

class GrandPa{
    public GrandPa(){}
    public void test(){
        System.out.println("GrandPa的test方法");
    }
}

class Father extends GrandPa{
    public Father() {}
    public void cry(String name){
        System.out.println(name+"在哭");
    }
}

class Son extends Father {
    public Son() {}
    //重写父类的cry方法
    public void cry(String name){
        System.out.println(name+"在狗叫");
    }

    //重写爷爷类的test方法
    public void test(){
        System.out.println("重写GrandPa的test方法");
    }
}
/*输出结果
小马在狗叫
重写GrandPa的test方法
*/

7.2 方法重写的细节

    (1)子类方法的形参列表、方法名称,要和父类方法的形参列表、方法名称完全一样

    (2)返回类型:和父类方法一致或者是父类方法返回类型的子类型

    比如父类返回类型是 Object ,子类方法返回类型是String

    public class Test {
        public static void main(String[] args) {
            Son son = new Son();
            String result = son.sayHello("小马");
            System.out.println(result);
        }
    }
    
    class Father{
        public Object sayHello(String name){
            return name+"调用父类的sayHello";
        }
    }
    class Son extends Father{
        //重写父类的sayHello方法
        public String sayHello(String name){
            return name+"调用子类的sayHello";
        }
    }
    /*
    小马调用子类的sayHello
    */
    

    (3)方法访问修饰符:不能缩小父类方法的访问权限

    访问权限从宽松到严格的顺序是:Public、Protected、缺省、Private

    class Father{
        protected Object sayHello(String name){
            return name+"调用父类的sayHello";
        }
    }
    class Son extends Father{
        
        //X,缩小了父类方法的访问权限
        private String sayHello(String name){
            return name+"调用子类的sayHello";
        }
        //√,重写了父类的sayHello方法
        public String sayHello(String name){
            return name+"调用子类的sayHello";
        }
    }

    8.重写与重载的对比

    发生范围方法名形参列表返回类型修饰符
    重载(Overload)本类必须一致类型,个数或者顺序至少有一个不同无要求无要求
    重写(Override)父子类必须一致必须完全一致子类方法返回的类型与父类返回的类型一致,或者是其子类型子类不能缩小父类方法的访问范围

    重载的练习题

    判断下方函数是否与void show(int a,char b,double c){}构成重载
    void show(int x,char y,double z){}//不是
    int show(int a,double c,char b){}//是,参数顺序不同
    void show(int a,double c,char b){}//是,参数顺序不同
    
    boolean show(int c,char b){}//是,参数个数不同
    void show(){}//是,参数个数不同
    double show(int x,char y,double z){}//不是,虽然返回类型不同,但重载对返回类型无要求。参数个数、类型、顺序全都没有改变,不是重载

    9.面向对象特征之三:多态 

    多态的使用前提:① 类的继承关系 ② 方法的重写

    9.1 快速入门案例

    public class Test {
        public static void main(String[] args) {
            //animal 编译类型是Animal,运行类型是Dog
            Animal animal = new Dog();
            animal.cry();//执行到此行时,animal运行类型是Dog,所以cry就是Dog的cry
    
            //animal编译类型仍然是Animal,运行类型编程Cat
            animal = new Cat();
            animal.cry();//执行到此行时,animal运行类型是Cat,所以cry就是Cat的cry
        }
    }
    
    class Animal {
        public void cry() {
            System.out.println("Animal cry");
        }
    }
    class Dog extends Animal {
        public void cry() {
            System.out.println("Dog cry");
        }
    }
    class Cat extends Animal {
        public void cry() {
            System.out.println("Cat cry");
        }
    }
    /*输出结果
    Dog cry
    Cat cry
    */

    9.2 为什么需要多态?

    案例


    引入多态前后 Master 主人类的编写对比

    引入多态前后的对比 

    优势:引入多态后无论有多少种动物,只需要定义一个喂食方法即可,可以"一劳永逸",代码复用性高   


    引入多态前后测试类 Test 的编写对比:

    9.3 对象的多态

    对象的多态性:父类的引用指向子类的对象

    比如 Animal animal = new Dog()Dog 是子类,Animal 是父类

    多态机制可以使得一个对象的编译类型和运行类型不一致

    (1)编译类型、运行类型: = 号左边为编译类型, 右边为运行类型

    (2)编译:使用 javac 指令,.java -> .class

    (3)运行:使用 java 指令, .class ->执行程序

    (4)编译类型可以简单理解为编译器编译时看到的类型,运行类型是真正执行 Java 程序时该数据的类型

    (5)编译类型在定义对象时就确定了,上述中 animal 的编译类型即 Animal,运行类型为 Dog

    9.4 方法形参的多态性

    方法形参的多态性:形参是父类类型,实参是子类对象

    public class Test {
        public static void main(String[] args) {
            Person person = new Person();
    
            Cat cat = new Cat();
            cat.setNickname("雪球");
            person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
            person.feed();
        }
    }
    
    //人类
    class Person{
        private Pet pet;
        public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
            this.pet = pet;
        }
        public void feed(){
            pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
        }
    }
    
    //宠物类
    class Pet {
        private String nickname; //昵称
    
        public String getNickname() {
            return nickname;
        }
    
        public void setNickname(String nickname) {
            this.nickname = nickname;
        }
    
        public void eat(){
            System.out.println(nickname + "吃东西");
        }
    }
    
    class Cat extends Pet {
        //子类重写父类的方法
        public void eat() {
            System.out.println("猫咪" + getNickname() + "吃鱼仔");
        }
    
        //子类扩展的方法(特有的方法)
        public void catchMouse() {
            System.out.println("抓老鼠");
        }
    }

    9.5 方法返回类型的多态性

    方法返回类型的多态性:返回值类型是父类类型,实际返回的是子类对象

    public class Test {
        public static void main(String[] args) {
            PetShop shop = new PetShop();
    
            Pet dog = shop.salPet("Bird");
            dog.setNickname("小白");
            dog.eat();
    
            Pet cat = shop.salPet("Cat");
            cat.setNickname("雪球");
            cat.eat();
        }
    }
    
    //宠物商店类
    class PetShop {
        //返回值类型是父类类型,实际返回的是子类对象
        public Pet salPet(String type){
            switch (type){
                case "Bird":
                    return new Bird();
                case "Cat":
                    return new Cat();
            }
            return null;
        }
    }
    
    //宠物类
    class Pet {
        private String nickname; //昵称
    
        public String getNickname() {
            return nickname;
        }
    
        public void setNickname(String nickname) {
            this.nickname = nickname;
        }
    
        public void eat(){
            System.out.println(nickname + "吃东西");
        }
    }
    //猫类
    class Cat extends Pet {
        //子类重写父类的方法
        public void eat() {
            System.out.println("猫咪" + getNickname() + "吃鱼仔");
        }
    
        //子类扩展的方法(特有的方法)
        public void catchMouse() {
            System.out.println("抓老鼠");
        }
    }
    //鸟类
    class Bird extends Pet {
        //子类重写父类的方法
        public void eat() {
            System.out.println("鸟" + getNickname() + "吃虫子");
        }
    
        //子类扩展的方法(特有的方法)
        public void catchAnt() {
            System.out.println("抓蚂蚁");
        }
    }

    9.6 数组的多态性

    数组的多态性:多态数组即数组的定义类型为父类类型,保存的元素类型可以为子类

    public class Test {
        public static void main(String[] args) {
            Person persons[] = new Person[2];
            persons[0] = new Student();//向上转型,编译类型为Person,运行类型为Student
            persons[1] = new Doctor();//向上转型,编译类型为Person,运行类型为Doctor
    
        }
    }
    class Person{}
    class Student extends Person{}
    class Doctor extends Person{}
    

    9.7 多态的向上转型

    向上转型的本质:父类的引用指向了子类的对象 

    9.7.1 语法

    父类类型 引用名 = new 子类类型()
    Animal animal = new Dog()

    9.7.2 注意事项

    Animal animal = new Dog()

    (1)编译类型看左边,运行类型看右边

    (2)可以调用父类中的所有成员;成员= 属性 + 方法,不过需要遵守修饰符访问权限

    (3)调用方法的规则:当使用 animal.方法名(实参) 时,优先于从子类 Dog 中找方法,找不到再到父类(会一直往上追溯,不限于直接父类)中查找

    public class Test1 {
        public static void main(String[] args) {
            Father f = new Son();
            f.say();
            f.run();
        }
    }
    class Father{
        public void say(){
            System.out.println("father say");
        }
        public void run(){
            System.out.println("father run");
        }
    }
    class Son extends Father{
        public void say(){
            System.out.println("son say");
        }
    }
    /*输出结果
    son say
    father run
     */
    

    (4)调用属性的规则:当使用 animal.属性名 时,即使子类 Dog 也有该属性并且赋予了其他值,也只会从父类 Animal 类中查找该属性。下面是一个例子:

    public class Test1 {
    public static void main(String[] args) {
       Father f = new Son();
       System.out.println(f.name);
    }
    }
    class Father{
    String name = "爸爸";
    }
    class Son extends Father{
    String name = "儿子";
    }
    /*输出结果
    爸爸
    */

     (5)可以调用子类中的方法,但需要注意的是不能调用子类中有但父类中没有的方法

    Animal animal = new Dog()

    原因:编译阶段时,编译器看到的 animal Animal 类型,而如果此时调用的方法在父类 Animal 中没有,则编译不通过

    案例

    原因:在编译阶段,编译器看到的 f 就是 Father 类型,而 Father 类中并没有定义 test() 方法,所以编译不通过

    (6)程序运行阶段时,利用 animal.方法名(参数) 调用方法是先从子类开始查找方法,查找不到就往其父类(会一直往上追溯,不限于直接父类)查找

    案例 

    public class Test {
        public static void main(String[] args) {
            Father f = new Son();
            f.test();//追溯到其父类的父类,即GrandPa的test方法
        }
    }
    
    class GrandPa{
        public void test(){
            System.out.println("GrandPa test");
        }   
    }
    class Father extends GrandPa{}
    class Son extends Father{}

    9.8 instanceof

    在讲多态的向下转型之前,讲一下 instanceof 的使用,这会在后面的向下转型使用到

    instanceof的作用:用于判断对象的运行类型是否为XX类型或XX类型的子类型

    public class Test1{
        public static void main(String[] args) {
    
            //编译类型是Base,运行类型是Sub
            Base base = new Sub();
    
            //运行类型是Sub,是Base的子类型,输出true
            System.out.println(base instanceof Base);
            //运行类型是Sub,输出true
            System.out.println(base instanceof Sub);
    
            Object object = new Object();
            //运行类型是Object,不是Base类型也不是Base的子类型,输出false
            System.out.println(object instanceof Base);
        }
    }
    
    class Base{}//父类
    class Sub extends Base{}//子类

    9.9 多态的向下转型

    向下转型(Downcasting):将一个父类类型的引用变量转换为其子类类型的引用变量

    在某些情况下,当一个对象被向上转型后,它的具体类型信息会丢失,只保留了父类类型的信息。如果需要访问子类中特有的成员,就需要使用向下转型

    9.9.1 语法

    父类类型 引用名 = new 子类类型();//向上转型
    子类类型 引用名 = (子类类型) 父类引用//向下转型
    //示例
    Person person = new Student();//向上转型
    Student student = (Student) person//向下转型

     (1)向下转型后,可以调用子类中所有的成员

     (2)需要注意的是,在进行向下转型之前,一定要确保对象实际上是子类的实例,否则会导致 ClassCastException 异常。因此,在进行向下转型之前,应该使用 instanceof 运算符进行类型检查,以避免出现异常情况。在  9.9.2示例代码 中会演示如何使用 instanceof

    9.9.2 示例代码

    class Animal {
        public void eat() {
            System.out.println("Animal is eating.");
        }
    }
    
    class Dog extends Animal {
        //子类中重写的方法
        @Override
        public void eat() {
            System.out.println("Dog is eating.");
        }
    
        //子类中特有的方法
        public void bark() {
            System.out.println("Dog is barking.");
        }
    }
    
    public class Test1 {
        public static void main(String[] args) {
            Animal animal = new Dog();  //向上转型,animal是父类的引用,指向了子类对象
            //animal.bark();X,无法访问Dog类中特有的bark()方法
    
            // 使用向下转型之前,最好先检查animal是否实际上是子类的实例
            if (animal instanceof Dog) {
                Dog dog = (Dog) animal;  //向下转型
                dog.bark();  // 调用Dog类中的 bark() 方法
            } else {
                System.out.println("animal is not an instance of Dog");
            }
        }
    }

    在上述示例中,首先创建了一个 Dog 类的对象,并将其赋值给一个 Animal 类型的引用变量 animal,这就是向上转型的过程。然后,通过使用 instanceof 检查 animal 是否是 Dog 类的实例,以确保向下转型时的类型安全

    如果 animalDog 类的实例,那么可以将其转型为 Dog 类型,并使用 dog 引用变量调用 Dog 类中特有的方法 bark()。如果 animal 不是 Dog 类的实例,则可以根据实际需求进行相应的处理

    9.10 成员变量没有多态性

    在前面的 9.7.2注意事项 已经提到,由于比较重要,所以这里再重复一下

    在多态机制下,即使子类里定义了与父类完全相同的成员变量,这个成员变量依然不可能覆盖父类中定义的实例变量,调用的仍然是父类中的成员变量

    案例 

    public class Test {
        public static void main(String[] args) {
            Base b = new Sub();//向上转型
            System.out.println(b.a);//1,Base中的a
            System.out.println(((Sub)b).a);//2,向下转型,调用的是Sub中的a
    
            Sub s = new Sub();
            System.out.println(s.a);//2,Sub中的a
            System.out.println(((Base)s).a);//1,向上转型,调用的是Base中的a
        }
    }
    class Base{
        int a = 1;
    }
    class Sub extends Base{
        int a = 2;
    }
    /*输出结果
    1
    2
    2
    1
    */

    9.11 动态绑定机制

    (1)调用对象方法时:该方法会和该对象的运行类型绑定。先去运行类型中找此方法,如果查找不到,则发挥继承机制,去其父类中(一直往上追溯,不限于直接父类)查找方法

    (2)调用对象属性时:没有动态绑定机制,哪里声明,哪里使用

    案例

    public class Test {
        public static void main(String[] args) {
    
            //编译类型是A,运行类型是B
            A a = new B();
            //输出20+10=30,调用方法遵循动态绑定机制,调用的是B中的getI()
            System.out.println(a.sum());
            //输出20+10=30,调用方法遵循动态绑定机制,调用的是B类中的sum1(),而属性没有动态绑定机制,B.sum1()中的i即是B中的i
            System.out.println(a.sum1());
        }
    }
    
    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;
        }
    
        public int sum1() {
            return i + 10;
        }
    }

    9.12 虚方法调用

     Java 虚方法是指:在编译阶段不能确定该方法的调用入口地址,只有在运行阶段才能确定,那么此方法就称为虚方法,即可能被重写的方法

    在多态情况下,如果其子类中重写了父类的方法,则此时父类的方法称为虚方法,父类会根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译阶段编译器是无法确定的,只有在运行阶段才能确定

    案例

    Person 类中定义了 welcome() 方法,各个子类重写了 welcome() Person 中的 welcome() 方法即为虚方法  

    public class Test1 {
        public static void main(String[] args) {
            Chinese chinese = new Chinese();
            Korean korean = new Korean();
            Reception reception = new Reception();
            
            reception.service(chinese);
            reception.service(korean);
        }
    }
    
    //门口接待员
    class Reception{
        public void service(Person person) {//形参为父类对象,实参为子类对象
            person.welcome();//根据传入的子类动态调用各国的欢迎语
        }
    }
    
    
    //人类
    class Person{
        public void welcome(){}
    }
    //中国人
    class Chinese extends Person{
        public void welcome(){
            System.out.println("欢迎光临");
        }
    }
    //韩国人
    class Korean extends Person{
        public void welcome(){
            System.out.println("어서 오세요");
        }
    }
    //泰国人
    class Thai extends Person{
        public void welcome(){
            System.out.println("ยินดีต้อนรั");
        }
    }
    //美国人
    class American extends Person{
        public void welcome(){
            System.out.println("welcome");
        }
    }

    9.13 多态练习题

    练习题1

    public class Test1{
        public static void main(String[] args) {
            double d =13.4;//√
            long l = (long)d;//√
            System.out.println(l);//13
    
            int in = 5;//√
            boolean b =(boolean)in;//X,boolean-> int
    
            Object obj = "Hello";//√,向上转型,编译类型是Object,运行类型是String
            String objStr = (String)obj;//√,向下转型
            System.out.println(objStr);//Hello
    
            Object objPri = new Integer(5);//√,向上转型,编译类型是Object,运行类型是Integer
            String str = (String)objPri;//X,ClassCastException,objPri原本指向的是Integer,不能向下转型为String
            Integer str1 =(Integer)objPri; //√,向下转型
        }
    }

    练习题2 

    public class Test1{
        public static void main(String[] args) {
            Sub sub = new Sub();
            System.out.println(sub.count);//20
            sub.display();//20
            Base base = sub;//向上转型,编译类型是Base,运行类型是Sub(父类的引用指向子类对象)
            System.out.println(base == sub);//true,base和sub指向的都是同一个引用
            System.out.println(base.count);//10,成员变量没有多态性,调用的是Base类中的count
            base.display();//20
        }
    }
    
    class Base{
        int count = 10;
        public void display(){
            System.out.println(this.count);
        }
    }
    class Sub extends Base{
        int count = 20;
        public void display(){
            System.out.println(this.count);
        }
    }
    /*输出结果
    20
    20
    true
    10
    20
     */

    练习题3 

    public class Test1 {
        public static void main(String[] args) {
            
            Base base = new Sub();//向上转型,编译类型为Base,运行类型为Sub
            base.add(1, 2, 3);//调用Sub中的add(int a,int[] arr),输出sub_1
    		
    		Sub s = (Sub)base;//向下转型
    		s.add(1,2,3);//调用Sub中的add(int a,int b,int c),输出sub_2
        }
    }
    
    class Base {
        public void add(int a, int... arr) {
            System.out.println("base");
        }
    }
    
    class Sub extends Base {
    	
        //因为可变参数的本质就是数组,所以Sub中的这个add方法重写了Base中的add方法
        public void add(int a, int[] arr) {
            System.out.println("sub_1");
        }
    
        //与Sub上面这个add方法构造重载
    	public void add(int a, int b, int c) {
    		System.out.println("sub_2");
    	}
    
    }

     练习题4

    多态是运行时行为,证明如下:

    import java.util.Random;
    
    //证明如下:
    class Animal {
        protected void eat() {
            System.out.println("animal eat food");
        }
    }
    
    class Cat extends Animal {
        protected void eat() {
            System.out.println("cat eat fish");
        }
    }
    
    class Dog extends Animal {
        public void eat() {
            System.out.println("Dog eat bone");
        }
    }
    
    class Sheep extends Animal {
        public void eat() {
            System.out.println("Sheep eat grass");
        }
    }
    
    public class Test1 {
        //根据生成的随机数来返回对应的子类
        public static Animal getInstance(int key) {
            switch (key) {
                case 0:
                    return new Cat();
                case 1:
                    return new Dog();
                default:
                    return new Sheep();
            }
    
        }
    
        public static void main(String[] args) {
            int key = new Random().nextInt(3);
            System.out.println(key);
            
            //多态
            Animal animal = getInstance(key);//向上转型
            animal.eat();
        }
    }
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值