面向对象——多态

多态

多态定义

多态:可以理解为事物存在的多种体现形态。
例如:

人:男人,女人
动物:猫,狗

猫这个对象对应的类型是猫类型:猫 x = new 猫();,同时猫也是动物中的一种,也可以把猫称为动物:动物 x = new 猫();。动物是猫和狗等具体事物中抽取出来的父类型。
本文从以下几个方面介绍多态:

  1. 多态的体现——父类的引用指向了自己的子类对象。即父类的引用也可以接收自己的子类对象
  2. 多态的前提——必须是类与类之间有关系,要么继承,要么实现。通常还有一个前提:存在覆盖
  3. 多态的好处——多态的出现大大的提高了程序的扩展性
  4. 多态的弊端——提高了扩展性,但是只能使用父类的引用访问父类中的成员,不能预先使用子类,因为那时子类还没存在
  5. 多态的应用
  6. 多态的出现在代码中的特点(多态使用的注意事项)

以动物:猫,狗,猪为例说之。

abstract class Animal {
    public abstract void eat();
}
class Cat extends Animal {
    public void eat() {
        System.out.println("吃鱼");
    }

    public void catchMouse() {
        System.out.println("抓老鼠");
    }
}
class Dog extends Animal {
    public void eat() {
        System.out.println("吃骨头");
    }
    public void kanJia() {
        System.out.println("看家");
    }
}
class Pig extends Animal {
    public void eat() {
        System.out.println("饲料");
    }
    public void gongDi() {
        System.out.println("拱地");
    }
}

那么以下代码:

Animal a = new Cat(); // 类型提升,向上转型
a.eat();

如果想要调用猫的特有方法时,如何操作?——强制将父类的引用转成子类类型,向下转型,即:

Cat c = (Cat)a;
c.catchMouse();

千万不要出现这样的操作,就是将父类对象转成子类类型,即:

Animal a = new Animal();
Cat c = (Cat)a;

我们能转换的是父类引用指向了自己的子类对象时,该引用可以被提升,也可以被强制转换。多态自始至终都是子类对象在做着变化。

instanceof关键字的使用

当我们使用向下转型时,确实是可以带来好处,即可以使用子类的特有功能。但是也会带来弊端——必须得面对具体的子类型。这即是说向下转型有风险,容易发生ClassCastException,只要转换类型和对象类型不匹配就会发生,想要安全,必须要进行判断,判断一个对象是否匹配某一个类型,这时我们就需要使用一个新的关键字——instanceof了,instanceof的用法为:对象 instanceof 类型。我们举例说明:

class DuoTaiDemo2 {
    public static void main(String[] args) {
        function(new Dog());    
        function(new Cat());
    }

    public static void function(Animal a) {
        a.eat();
        if(a instanceof Cat) {
            Cat c = (Cat)a;
            c.catchMouse();
        } else if(a instanceof Dog) {
            Dog d = (Dog)a;
            d.kanJia();
        }   
    }
}

多态特点

  • 在多态中,成员变量的特点:当子父类中出现同名变量时,多态调用时,只看调用该成员变量的引用所属的类中的成员变量。简单说:无论编译还是运行,都看等号(=)的左边(引用型变量所属的类)
    明白了在多态中成员变量的特点之后,试着看以下代码的运行结果:

    class Fu
    {
        int num = 5;
        void show()
        {
            System.out.println("num = " + this.num); // 打印num = 5
        }
    }
    
    class Zi extends Fu
    {
        int num = 6;
    }
    
    class DuoTaiTest2 
    {
        public static void main(String[] args) 
        {
            Fu f = new Zi();
            f.show();
        }
    }

    很显然打印的是num = 5,为什么会这样呢?一图以蔽之:
    这里写图片描述

  • 在多态中成员函数(非静态)的特点:出现一模一样函数时,多态调用,在编译时期,参阅引用型变量所属的类中是否有调用的方法,如果有,编译通过,如果没有编译失败。在运行时期,参阅对象所属的类中是否有调用的方法。简单总结就是:成员函数(非静态)在多态调用时,编译看左边,运行看右边
    在一些专业书上也有这样的表述:成员方法动态绑定到当前对象上
  • 在多态中,静态成员函数(或者静态成员变量)的特点:出现一模一样函数时,多态调用,无论编译和运行,都参考左边(引用型变量所属的类)。其实我们要知道,真正调用静态方法是不需要对象的,直接类名调用。因为静态方法绑定到类上,所以这种情况更多用于面试中。

多态应用

例1,基础班学生:学习,睡觉;高级班学生:学习,睡觉。可以将这两类事物进行抽取。

abstract class Student {
    public abstract void study();
    public void sleep() {
        System.out.println("躺着睡");
    }
}

// 工具类
class DoStudent {
    public void doSomething(Student stu) {
        stu.study();
        stu.sleep();
    }
}

class BaseStudent extends Student {
    public void study() {
        System.out.println("base study");
    }
    public void sleep() {
        System.out.println("坐着睡");
    }
}
class AdvStudent extends Student {
    public void study() {
        System.out.println("adv study");
    }
}
class DuoTaiDemo {
    public static void main(String[] args) {
        DoStudent ds = new DoStudent();
        ds.doSomething(new BaseStudent());
        ds.doSomething(new AdvStudent());
    }
}

例2,需求:电脑运行示例,电脑运行基于主板。

// 接口定义规则
interface PCI {
    public void open();
    public void close();
}
class MainBoard {
    public void run() {
        System.out.println("mainboard run");
    }
    public void usePCI(PCI p) { // PCI p = new NetCard(); // 接口型引用指向自己的子类对象
        if(p != null) {
            p.open();
            p.close();
        }
    }
}
class NetCard implements PCI {
    public void open() {
        System.out.println("netcard open");
    }
    public void close() {
        System.out.println("netcard close");
    }
}
class SoundCard implements PCI {
    public void open() {
        System.out.println("soundcard open");
    }
    public void close() {
        System.out.println("soundcard close");
    }
}
class DuoTaiDemo {
    public static void main(String[] args) {
        MainBoard mb = new MainBoard();
        mb.run();
        mb.usePCI(null);
        mb.usePCI(new NetCard());
        mb.usePCI(new SoundCard());
    }
}

示意图:
这里写图片描述
例3,数据库的操作。数据是:用户信息。

  1. 连接数据库(JDBC Hibernate)
  2. 操作数据库(CRUD)——C creat R read U update D delete
  3. 关闭数据库连接
interface UserInfoDao {
    public void add(User user);
    public void delete(User user);
}
class UserInfoByJDBC implements UserInfoDao {
    public void add(User user) {
        1、JDBC连接数据库
        2、使用sql添加语句添加数据
        3、关闭连接
    }
    public void delete(User user) {
        1、JDBC连接数据库
        2、使用sql删除语句删除数据
        3、关闭连接
    }
}
class UserInfoByHibernate implements UserInfoDao {
    public void add(User user) {
        1、Hibernate连接数据库
        2、使用sql添加语句添加数据
        3、关闭连接
    }
    public void delete(User user) {
        1、Hibernate连接数据库
        2、使用sql删除语句删除数据
        3、关闭连接
    }
}

class DBOperate {
    public static void main(String[] args) {
        UserInfoDao ui = new UserInfoByHibernate();
        ui.add(user);
        ui.delete(user);
    }
}

示意图如下:
这里写图片描述

Object类

Object是所有对象的直接或者间接父类,传说中的上帝。该类中定义的肯定是所有对象都具备的功能。
Object类中已经提供了对对象是否相同的比较方法,如果自定义类中也有比较相同的功能,没有必要重新定义。只要沿袭父类中的功能,建立自己特有比较内容即可,这就是覆盖。
例,假如有一个Person类,其代码为:

class Person extends Object
{
    private int age;
    private String name;

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

现在我们的需求是:定义一个方法,判断两个Person对象是否是同一个,判断的依据是根据姓名和年龄,如果姓名和年龄都相同,视为同一个人。
我的分析:不用再自定义方法判断对象是否相同了,因为在Object父类中,已经定义了这样的方法,直接使用就可以了,但是判断的内容是根据Person的特点定义的,那就需要保留父类的功能声明,定义子类功能的特有内容,使用覆盖。所以要在Person类中加入如下equals方法:

public boolean equals(Object obj) // Object obj = p2;--->Object obj = new Person();
{
    // 提高点效率。如果两个引用指向了同一个对象,就不用再转换并比较内容了,直接判断地址就哦了。
    if (this == obj)
        return true;

    // obj.age是错误的,因为Object中没有age属性,
    // 想要使用子类对象的特有属性或行为,必须对其进行向下转型,并且需要进行类型判断
    if (!(obj instanceof Person))
    {
        // return false; 
        throw new ClassCastException("类型错误");
    }
    Person p = (Person)obj;
    // 如果判断姓名字符串是否相同,不要用==,字符串本身是一个对象,所以要使用String类的equals方法
    return this.name.equals(p.name) && this.age == p.age;
}

有时候,我们还需要重写Object类的toString()方法,建立Person对象特有的字符串表现形式。查询API帮助文档,我们可以发现:Object类的toString方法返回一个字符串,该字符串由类名(对象是该类的一个实例)、at标记符“@”和此对象哈希码的无符号十六进制表示组成。换句话说,该方法返回一个字符串,它的值等于:getClass().getName() + '@' + Integer.toHexString(hashCode())。如果我们不在Person类中重写该方法,运行以下程序:

class ObjectDemo 
{
    public static void main(String[] args) 
    {
        Person p1 = new Person("lisi", 21);
        Person p2 = new Person("mazi", 25);

        System.out.println(p1); // Person@139a55,打印对象时,默认调用toString()方法
        System.out.println(p1.toString());
    }
}

则会输出Person@139a55,而且在打印对象时,默认调用了toString()方法

  • 顺便说一下,如果要我们自己来弄,输出Person@139a55这样的东西,又该怎么做呢?
    这时,我们就要接触一点点反射的知识了,关于反射的知识以后会另开一篇专讲。A.classB.class这些class文件都有名称,这些文件内都有构造函数,一般方法,java中用Class来描述这些class文件,可通过getName()获取名称。所以以下代码:

    Person p1 = new Person("lisi", 21);
    Class c = p1.getClass();
    System.out.println(c.getName()); // Person

    会输出Person
    现在我们就可以自己来编写了,代码如下:

    Person p1 = new Person("lisi", 21);
    Class c = p1.getClass();
    System.out.println(c.getName()+"@@"+Integer.toHexString(p1.hashCode())); 

    这时就会输出Person@@139a55这样的东西了。

说完toString()方法,我们须在Person类中重写该方法,所以应在Person类中添加如下toString()方法:

/**
建立Person对象特有的字符串表现形式,只要覆盖toString方法即可
*/
public String toString()
{
    return "Person[name = " + this.name +", age = " + this.age + "]";
}

这样,测试类的代码可这样写为:

class ObjectDemo 
{
    public static void main(String[] args) 
    {
        Person p1 = new Person("lisi", 21);
        Person p2 = new Person("mazi", 25);

        System.out.println(p1.equals(p2)); // 判断的是对象的内容,用equals。
        System.out.println(p1 == p2); // 判断的是对象的地址
    }
}

内部类

将一个类定义在另一个类的里面,对里面那个类就称为内部类(内置类,嵌套类)。例如,A类要直接访问B类中的成员时,可以将A类直接定义到B类中,作为B类的内部类存在。
内部类的访问规则:

  • 内部类可以直接访问外部类中的成员,包括私有。之所以可以直接访问外部类中的成员,是因为内部类中持有了一个外部类中的引用,格式:外部类名.this。用一个例子来验证:

    class Outer
    {
        int num = 2;
    
        class Inner
        {
            int num = 3;
    
            void show()
            {
                int num = 4;
                System.out.println("show..." + Outer.this.num);
            }
        }
    
        public void method()
        {
            new Inner().show();
        }
    }
    class InnerClassDemo2 
    {
        public static void main(String[] args) 
        {
            new Outer().method();
        }
    }
  • 外部类要想访问内部类,只能创建内部类的对象来访问。

访问格式:

  • 非静态,非私有的内部类访问方式
    当内部类定义在外部类的成员位置上,而且非私有,可以在外部其他类中,可以直接建立内部类对象。格式为:外部类名.内部类名 变量名 = 外部类对象.内部类对象;,例,Outer.Inner in = new Outer().new Inner();示例代码如下:

    class Outer
    {
        private int num = 4;
        class Inner // 内部类,相当于外部类中的一个成员,它就可以被成员修饰符所修饰,public private static
        {
            void show()
            {
                System.out.println(num);
            }
        }
    }
    class InnerClassDemo
    {
        public static void main(String[] args) 
        {
            Outer.Inner in = new Outer().new Inner();
            in.show();
        }
    }

    而且在非静态内部类中只允许定义静态的常量,存于常量池中,不能定义其他静态成员。所以,以下代码编译是没有任何问题的:

    class Outer
    {
        private int num = 4;
        class Inner // 内部类,相当于外部类中的一个成员,它就可以被成员修饰符所修饰,public private static
        {
            static final int count = 5; // 在非静态内部类中只允许定义静态的常量,存于常量池中,不能定义其他静态成员
            void show()
            {
                System.out.println(num);
            }
        }
    }
    class InnerClassDemo
    {
        public static void main(String[] args) 
        {
            Outer.Inner in = new Outer().new Inner();
            in.show();
        }
    }
  • 静态,非私有的内部类访问方式,访问非静态成员
    当内部类在成员位置上时,就可以被成员修饰符修饰,比如,private将内部类在外部类中进行封装,static内部类就具备static的特性。当内部类被static修饰后,只能直接访问外部类中的static成员,出现了访问局限
    在外部其他类中,如何直接访问静态内部类非静态成员呢?答案是格式为new Outer.Inner().function();。示例代码如下:

    class Outer
    {
        private static int num = 4;
        class Inner // 内部类,相当于外部类中的一个成员,它就可以被成员修饰符所修饰,public private static
        {
            void show()
            {
                System.out.println(num);
            }
        }
    
        static class Inner2 // 静态内部类,相当于一个外部类
        {
            void show2()
            {
                System.out.println("show2..." + num);
            }
        }
    
    }
    class InnerClassDemo
    {
        public static void main(String[] args) 
        {
            Outer.Inner2 in = new Outer.Inner2();
            in.show2();
        }
    }
  • 静态,非私有的内部类访问方式,访问静态成员
    在外部其他类中,如何直接访问静态内部类静态成员呢?答案是格式为Outer.Inner.function();。示例代码如下:

    class Outer
    {
        private static int num = 4;
        class Inner // 内部类,相当于外部类中的一个成员,它就可以被成员修饰符所修饰,public private static
        {
            void show()
            {
                System.out.println(num);
            }
        }
    
        static class Inner2 // 静态内部类,相当于一个外部类
        {
            void show2()
            {
                System.out.println("show2..." + num);
            }
    
            static void show3()
            {
                System.out.println("show3..." + num);
            }
        }
    
    }
    class InnerClassDemo
    {
        public static void main(String[] args) 
        {
            Outer.Inner2.show3();
        }
    }

注意:当内部类中定义了静态成员,该内部类必须是static的。当外部类中的静态方法访问内部类时,内部类也必须是static的。

class Outer {
    private static int x = 3;
    static class Inner { // 静态内部类
        static void function() { // 当内部类中定义了静态成员,该内部类必须是static的
            System.out.println("inner::::"+x); // 当内部类被static修饰后,只能直接访问外部类中的static成员
        }
    }
    static class Inner2 {
        void show() {
            System.out.println("inner2 show");
        }
    }
    public static void method() {
        new Inner2().show(); // 当外部类中的静态方法访问内部类时,内部类也必须是static的
    }
}

内部类定义在局部时

  1. 不可以被成员修饰符修饰,因为privatestatic不能修饰局部成员。
  2. 可以直接访问外部类中的成员,因为还持有外部类中的引用。但是不可以访问它所在的局部中的变量,只能访问被final修饰的局部变量,主要原因是生命周期不同。注意:java8没这个区别了,但是被final修饰的变量是一个常量,只能被赋值一次,所以一经存在就不得更改。
    例,以下是java8的运行环境。

    class Outer {
        int x = 3;
        void method(int a) {
            // a++; // 从内部类引用的本地变量必须是最终变量或实际上的最终变量
            int y = 4;
            class Inner {
                void function() {
                    System.out.println(a);
                }
            }
            new Inner().function();
        }
    }
    class InnerClassDemo {
        public static void main(String[] args) {
            Outer out = new Outer();
            out.method(7);
            out.method(8);
        }
    }

为了能说明局部内部类只能访问被final修饰的局部变量,而且其主要原因是生命周期不同这一点,我们举例验证。

class Outer
{
    private int num = 4;
    Object obj;
    public void method()
    {
        /*final*/ int x = 5;
        class Inner extends Object // Inner本身继承Object
        {
            // 覆盖Object类中的toString()方法
            public String toString()
            {
                System.out.println("x = " + 5);
                System.out.println("show..." + num);
                return "Inner...abc";
            }
        }
        // 创建内部类的对象
        Inner in = new Inner();
        // 将内部类对象的地址赋值给obj
        obj = in;
    }

    public void function()
    {
        // 打印obj指向的对象的字符串表现形式
        System.out.println(obj.toString());
    }
}


class InnerClassDemo2 
{
    public static void main(String[] args) 
    {
        new Outer().method();
    }
}

以上例子在JVM内存中的示意图大概是这样的:
这里写图片描述

匿名内部类

  1. 匿名内部类其实就是内部类的简写格式。
  2. 定义匿名内部类的前提:内部类必须是继承一个类或者实现接口。
  3. 匿名内部类的格式:new 父类或者接口() {定义子类的内容}
  4. 匿名内部类其实就是一个匿名子类对象,而且这个对象有点胖。可以理解为带内容的对象。
  5. 匿名内部类中定义的方法最后不要超过3个。
    例,

    abstract class AbsDemo {
        abstract void show();
    }
    
    class Outer {
        int x = 3;
        public void function() {
            AbsDemo d = new AbsDemo() {
                int num = 9;
                void show() {
                    System.out.println("num==="+num);
                }
                void abc() {
                    System.out.println("haha");
                }
            };
            d.show();
            // d.abc(); // 编译失败,因为父类中没有这个方法
        }
    }

练习一:补全代码,通过匿名内部类。

interface Inter {
    void method();
}
class Test {
    // 补足代码。通过匿名内部类

}
class InnerClassTest {
    public static void main(String[] args) {
        Test.function().method();
    }
}

解:

interface Inter {
    void method();
}
class Test {
    // 补足代码。通过匿名内部类
    static Inter function() {
        return new Inter() {
            public void method() {
                System.out.println("Inter method");
            }
        };
    }
}
class InnerClassTest {
    public static void main(String[] args) {
        // Test.function():Test类中有一个静态的方法function
        // .method():function这个方法运算后的结果是一个对象,而且是一个Inter类型的对象,
        // 因为只有是Inter类型的对象,才可以调用method()
        Test.function().method();
    }
}

面试时可能遇到的一个小问题(有关匿名内部类的),如果没有一个类继承或一个接口实现,还能使用匿名内部类吗?答案是可以的。

class InnerTest {

    public static void main(String[] args) {
        new Object() { // new Object() {}是Object类的子类对象
            public void function() {
                System.out.println("hello");
            }
        }.function();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李阿昀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值