Day09-面向对象-多态

Day09-面向对象-多态

学习目标

  • 对象数组
  • 能够说出final关键字的使用
  • 能够说出什么是多态
  • 能够说出多态的特点以及作用
  • 能够说出引用数类型转换方式作用以及注意事项
  • 能够使用instanceof关键字判断一个对象是否属于某一个类

1对象数组

数组是用来存储一组数据的容器,一组基本数据类型的数据可以用数组装,那么一组对象也可以使用数组来装。

即数组的元素可以是基本数据类型,也可以是引用数据类型。当元素是引用数据类型是,我们称为对象数组。

注意:对象数组,首先要创建数组对象本身,即确定数组的长度,然后再创建每一个元素对象,如果不创建,数组的元素的默认值就是null,所以很容易出现空指针异常NullPointerException。

1.1 对象数组的声明和使用

案例:

(1)定义矩形类,包含长、宽属性,area()求面积方法,perimeter()求周长方法,String getInfo()返回圆对象的详细信息的方法

(2)在测试类中创建长度为5的Rectangle[]数组,用来装3个矩形对象,并给3个矩形对象的长分别赋值为10,20,30,宽分别赋值为5,15,25,遍历输出

package com.atguigu.test08.array;

public class Rectangle {
    double length;
    double width;

    double area(){//面积
        return length * width;
    }

    double perimeter(){//周长
        return 2 * (length + width);
    }

    String getInfo(){
        return "长:" + length +
                ",宽:" + width +
                ",面积:" + area() +
                ",周长:" + perimeter();
    }
}

package com.atguigu.test08.array;

public class ObjectArrayTest {
    public static void main(String[] args) {
        //声明并创建一个长度为3的矩形对象数组
        Rectangle[] array = new Rectangle[3];

        //创建3个矩形对象,并为对象的实例变量赋值,
        //3个矩形对象的长分别是10,20,30
        //3个矩形对象的宽分别是5,15,25
        //调用矩形对象的getInfo()返回对象信息后输出
        for (int i = 0; i < array.length; i++) {
            //创建矩形对象
            array[i] = new Rectangle();

            //为矩形对象的成员变量赋值
            array[i].length = (i+1) * 10;
            array[i].width = (2*i+1) * 5;

            //获取并输出对象对象的信息
            System.out.println(array[i].getInfo());
        }
    }
}

1.2 对象数组的内存图分析

对象数组中数组元素存储的是元素对象的地址。

在这里插入图片描述

2. 多态

多态是继封装、继承之后,面向对象的第三大特性。

生活中,比如一条叫旺财的狗,我们可以说它是一条狗,也可以说它是一个动物;一个叫张三的男性,我们可以称它为男人,也可以称它为人,同时还能称呼他是一个动物。

多态: 就是指同一事物,具有多个不同表现形式。

2.1 前提

  1. 继承或者实现【二选一】
  2. 方法的重写【意义体现:不重写,无意义】
  3. 父类引用指向子类对象【格式体现】
class Test {
    public static void main(String[] args) {
        // 父类的引用 stu 指向了一个 new Student() 子类对象
        Person stu = new Student();
        stu.sleep();  // 子类重写了父类的方法,执行的是子类自己的方法
    }
}

class Person {
    void sleep() {
        System.out.println("人正在睡觉");
    }
}

class Student extends Person {
    @Override
    void sleep() {
        System.out.println("学生正在教室里睡觉");
    }
}

2.2 多态的意义

查看一下代码,思考这段代码是否可以进行优化:

class Test {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        func(dog);
        func(cat);
    }
    private static void func(Dog dog) {
        dog.eat();
    }
    private static void func(Cat cat) {
        cat.eat();
    }
}
class Dog {
    public void eat() {
        System.out.println("小狗正在啃骨头");
    }
}
class Cat {
    public void eat() {
        System.out.println("小猫正在吃鱼");
    }
}

上述代码中,根据不同的参数类型,我们定义了不同的func方法,这个方法出现了重载。但是,我们从func方法的内部实现上来看,这两个方法都是调用对象的eat方法。

其实,在上述代码里,我们可以考虑给Dog类和Cat类定义一个父类,通过传递父类参数的形式,来简化代码。

实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。

class Test {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        func(dog);
        func(cat);
    }
    private static void func(Animal animal) { // 参数只需要指定 Animal 父类即可
        animal.eat();
    }
}
class Animal{  // 定义一个 Animal 类,让Dog和Cat都继承自它
    public void eat() {
        System.out.println("动物正在吃东西");
    }
}
class Dog extends Animal {
    public void eat() {
        System.out.println("小狗正在啃骨头");
    }
}
class Cat extends Animal {
    public void eat() {
        System.out.println("小猫正在吃鱼");
    }
}

上述代码的简化,并不只是简单的替换,同时还是一种扩展。当我们再出现新类型(例如Pig类)的对象时,我们只需要让Pig类继承自Animal类,就可以直接把这个Pig类型的对象传给func方法作为参数使用。这样就大大的提高了代码的灵活性,便于代码后期的扩展。

实际上,多态在我们程序中是大量存在的,因为有了多态的存在,才使得Java语言变得更加的灵活以扩展,实现了低耦合高内聚的编码思想。

2.3 多态的特点

父类引用指向子类对象的这种多态形式,在调用方法和使用属性时,有一些细节需要我们注意。

2.3.1 成员方法的特点

观察一下代码,思考代码的执行结果。

package com.atguigu.java;

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.eat();  // 会调用 Dog 类的 eat 方法
//        animal.watchDog();    编译会报错
        ((Dog) animal).watchDog();
    }
}

class Animal {  // 定义一个 Animal 类,让Dog和Cat都继承自它
    public void eat() {
        System.out.println("动物正在吃东西");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("小狗正在啃骨头");
    }
    public void watchDog() {
        System.out.println("小狗正在看家");
    }
}

对于调用方法,记住一点编译看左边,运行看右边。

也就是说,在编译时要看等号左边声明的类型里是否有这个方法,如果有这个方法就编译通过,反之不通过。而在运行时,是看等号右边到底创建的是一个什么类型的对象,会直接调用这个对象具体的方法。

2.3.2 成员变量的特点

直接通过对象名称访问成员变量,等号左边是谁,优先用谁

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog();
        System.out.println(animal.type);  // 动物
    }
}

class Animal {  // 定义一个 Animal 类,让Dog和Cat都继承自它
    String type = "动物";
    public void eat() {
        System.out.println("动物正在吃东西");
    }
}

class Dog extends Animal {
    String type = "狗";
    @Override
    public void eat() {
        System.out.println("小狗正在啃骨头");
    }
}

2.4 引用数据类型转换

多态的转型分为向上转型与向下转型两种:

  • 向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。

    使用格式:

    父类类型  变量名 = new 子类类型();
    如:Animal a = new Cat();
    
  • 向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。

    使用格式:

    子类类型 变量名 = (子类类型) 父类变量名;:Cat c =(Cat) a; 
    
2.4.1 类型转换的意义

调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。

public class Test {
    public static void main(String[] args) {
        Animal dog = new Dog();
//        dog.watchDoor();  编译报错,调用方法时,编译时看左边的类型,是Animal,Animal类里没有 watchDoor方法
        ((Dog) dog).watchDoor();
    }
}
class Animal {
    String type = "动物";
}
class Dog extends Animal {
    public void watchDoor() {
        System.out.println("狗正在看门");
    }
}
2.4.2 转型的异常

转型的过程中,一不小心就会遇到这样的问题,请看如下代码:

public class Test {
    public static void main(String[] args) {
        // 向上转型  
        Animal a = new Cat();  
        a.eat();               // 调用的是 Cat 的 eat

        // 向下转型  
        Dog d = (Dog)a;       
        d.watchHouse();        // 调用的是 Dog 的 watchHouse 【运行报错】
    }  
}

这段代码可以通过编译,但是运行时,却报出了 ClassCastException ,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,格式如下:

变量名 instanceof 数据类型 
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false

所以,转换前,我们最好先做一个判断,代码如下:

public class Test {
    public static void main(String[] args) {
        // 向上转型  
        Animal a = new Cat();  
        a.eat();               // 调用的是 Cat 的 eat

        // 向下转型  
        if (a instanceof Cat){
            Cat c = (Cat)a;       
            c.catchMouse();        // 调用的是 Cat 的 catchMouse
        } else if (a instanceof Dog){
            Dog d = (Dog)a;       
            d.watchHouse();       // 调用的是 Dog 的 watchHouse
        }
    }  
}

3. final关键字的使用

学习了继承后,我们知道,子类可以在父类的基础上改写父类内容,比如,方法重写。那么我们能不能随意的继承API中提供的类,改写其内容呢?显然这是不合适的。为了避免这种随意改写的情况,Java提供了final 关键字,用于修饰不可改变内容。(最终的)

final: 不可改变。可以用于修饰类、方法和变量。

  • 类:被修饰的类,不能被继承。
  • 方法:被修饰的方法,不能被重写。
  • 变量:被修饰的变量,不能被重新赋值。

3.1 修饰类

final class Person {}

// 报错,被 final 修饰的类不能被继承
// class Student extends Person {}

查询API发现像 public final class Stringpublic final class Mathpublic final class Scanner 等,很多我们学习过的类,都是被final修饰的,目的就是仅供我们使用,而不让我们所以改变其内容。

3.2 修饰方法

被final修饰的方法继承后不允许重写,否则会报错。

class Person {
    final void demo(){
        System.out.println("我是Person类里的demo方法");
    }
}

class Student extends Person {
    // void demo(){}  // 报错,被final修饰的方法不允许被重写
}

3.3 修饰局部变量

  • 如果局部变量时基本数据类型,被final修饰后,只能赋值一次,再次赋值会报错。

    public class FinalDemo1 {
        public static void main(String[] args) {
            // 声明变量,使用final修饰
            final int a;
            // 第一次赋值 
            a = 10;
            // 第二次赋值
            a = 20; // 报错,不可重新赋值
            // 声明变量,直接赋值,使用final修饰
            final int b = 10;
            // 第二次赋值
            b = 20; // 报错,不可重新赋值
        }
    }
    
  • 如果局部变量是引用数据类型,在被final修饰后,只能指向一次某个对象,不允许再修改指向。但是不影响对象内部的成员变量值的修改。

    // 被 final 修饰的变量只能指向一次内存地址
    final Person p = new Person("zhangsan", 18);
    // p = new Person("Jack", 20);   不能够再次指向其他位置,否则会报错
    p.setName("李四");  // 但是可以修改这个对象的属性
    

3.4 修饰成员变量

  • 显式初始化,即在定义时就指定变量的值。

    class Test {
        public static void main(String[] args) {
            Person p = new Person();
            // p.ID = 1;  报错,ID被final修饰,不允许修改
        }
    }
    
    class Person {
        final int ID = 10;
    }
    
  • 构造方法初始化。

    class Test {
        public static void main(String[] args) {
            Person p = new Person("张三",8);
            // p.ID = 3;  报错,被final修饰的ID不允许再修改
        }
    }
    class Person {
        private String name;
        final int ID;
        Person(String name,int ID) {
            this.name = name;
            this.ID = ID;
        }
    }
    

被 final 修饰的常量通常都使用全大写,例如: Math.PI,Math.E

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值