Java基础—面向对象(三)

面向对象(三)

一、多态

1.多态定义
多态就是事物存在的多种体现形态。多态允许同一父类不同的子类对象对同一方法产生不同的行为方式。例如,中国人和俄罗斯人都是人的子类,都具备吃饭这个功能;但是当中国人和俄罗斯人分别调用吃饭这个方法的时候,其执行变现出不同的行为方式,即俄罗斯人是用叉子吃,中国人是用筷子吃。

2.多态的体现
多态是指在执行期间(而非编译期间)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。示例代码一步一步解释多态的形成过程。

定义一个Man类,代表人,定义俄罗斯人Russian和中国人Chinese的类,并继承Man,复写Man中的eat()方法,并创建对象调用该方法。

示例代码:

package com.heisejiuhuche;

public class TestPolymorphism {
    public static void main(String[] args) {
        Russian r = new Russian();
        r.eat();
        //Russian r1 = new Russian();
        //r1.eat();
        //Russian r2 = new Russian();
        //r2.eat();

        Chinese c = new Chinese();
        c.eat();
    }
}

abstract class Man {
    /*
     * 是人,都要吃饭,是强制性的功能,子类必须要复写
     */
    abstract void eat();
}

class Russian extends Man {
    public void eat() {
        System.out.println("Fork...");
    }

    public void drinkVodka() {
        System.out.println("Feels good...");
    }
}

class Chinese extends Man {
    public void eat() {
        System.out.println("Chopsticks...");
    }

    public void speak() {
        System.out.println("Chinese...");
    }
}

在主函数中,如果要创建很多个Russian对象,或者Chinese对象,就要写很多句对象.方法的语句,那么将该语句提取成方法。

示例代码:

package com.heisejiuhuche;

public class TestPolymorphism {
    public static void main(String[] args) {
        Russian r = new Russian();
        func(r);

        Chinese c = new Chinese();
        func(c);
    }

    //将 对象.方法 的代码提取成一个方法
    public static void func(Chinese c) {
        c.eat();
    }

    public static void func(Russian r) {
        r.eat();
    }
}

但是这么一来,如果以后还要加入印度人Indian,他们用手吃饭。那么在主函数中,就要加入新的eat()方法:

public static void eat(Indian i);

如果以后还要创建很多国家人的对象,这样的方式就不合理。由于无论俄罗斯人,中国人还是印度人,都是人的子类,那么语句:

Man m = new Chinese();

是没有问题的,因为中国人是人。所以,修改之后的代码,就是多态的体现。

示例代码:

public class TestPolymorphism {
    public static void main(String[] args) {
        Man r = new Russian();
        Man c = new Chinese();
        func(r);
        func(c);
    }

    //中国人,俄罗斯人,印度人都是人,那么这个方法只需要接收一个人作为参数即可
    public static void func(Man m) {
        m.eat();
    }
}

主函数中func()方法在执行的时候,判断接收到的人对象的具体类型是中国人还是俄罗斯人(动态绑定),最后调用相应对象的eat()方法,体现多态。

小扩展:
Man r = new Russian();这行代码,是向上转型,将子类Russian向上转型为Man类型,也称为类型提升。

3.多态的前提
要实现多态,必须有:
1)继承
必须有类与类之间的继承关系,有继承,才有第二前提复写;

2)复写
必须有子类对父类方法的复写,有复写,多态才有可以调用的方法,存在才有意义;

3)父类引用指向子类对象(向上转型)
必须有向上转型,子类都继承于父类,多态在程序执行期间,通过判断调用方法的对象的实际类型,体现出具体方法内容,才能达到让代码具备可扩展性的目的

4.多态的利弊
1)利:
多态提高了程序的扩展性;

2)弊:
只能使用父类的引用访问父类中的成员(详细解释见代码)

示例代码:

public class TestPolymorphism {
    public static void main(String[] args) {
        Man r = new Russian();
        Man c = new Chinese();
        func(r);
        func(c);
    }

    //父类的引用只能调用父类中的成员和方法
    public static void func(Man m) {
        m.eat();
        //这句话是错误的,编译无法通过,因为Man类中没有该方法
        m.speak();
    }
}

如果想要调用Chinese对象特有的speak()方法,应该强制将父类引用向下转型成子类类型。

示例代码:

public class TestPolymorphism {
    public static void main(String[] args) {
        Man m = new Chinese();
        //通过向下转型,调用Chinese对象的speak方法
        Chinese c = (Chinese)m;
        c.speak();
    }

    //父类的引用只能调用父类中的成员和方法
    public static void func(Man m) {
        m.eat();
    }
}

注意:
Man m = new Chinese();
Chinese c = (Chinese)m;
这是合法的;但是如果写成:
Man m = new Man();
Chinese c = (Chinese)m;
这是非法的,m指向的是人类型,不能说人就是中国人。转型的原则可以理解成,自始至终是子类的对象在做向上或者向下转型。

小扩展:
instanceof运算符
m instanceof Chinese判断m是否属于Chinese类型,是,返回true,否,返回false

5.多态使用的注意事项
1)多态状态下成员方法的特点:
以代码Parent p = new SubClass();SubClassParent子类为例

1> 在编译时期,编译能否通过取决于引用型变量所属的类中是否有要调用的方法;Parent类中有要调用的方法,编译通过,否则编译失败;
2> 在运行时期,程序运行是否出错取决于实际对象所属的类中是否有要调用的方法;SubClass类中有要调用的方法,运行成功,否则运行失败;
3> 总结为:成员方法在多态调用时,编译是否通过看左边,运行是否出错看右边

示例代码:

package com.heisejiuhuche;

public class TestPolymorphism {
    public static void main(String[] args) {
        Parent p = new SubClass();
        p.method1();
        p.method2();
        //p的引用所在的Parent类没有method3方法,编译失败
        //p.method3();
    }

}

class Parent {
    int num = 5;

    public void method1() {
        System.out.println("Parent method1...");
    }

    public void method2() {
        System.out.println("Parent method2...");
    }
}

class SubClass extends Parent {
    int num = 10;

    public void method1() {
        System.out.println("SubClass method1...");
    }

    public void method3() {
        System.out.println("SubClass method3...");
    }
}

上述程序,注释掉错误代码,运行之后的结果为:

SubClass method1...
Parent method2...

运行时,SubClass类中有method1method2方法(运行看右边),运行成功。

2)多态状态下成员变量的特点(面试用):

示例代码:

package com.heisejiuhuche;

public class TestPolymorphism {
    public static void main(String[] args) {
        Parent p = new SubClass();
        System.out.println(p.num);
        SubClass s = new SubClass();
        System.out.println(s.num);
    }

}

class Parent {
    int num = 5;
}

class SubClass extends Parent {
    int num = 10;
}

代码运行结果:

5
10

上述代码,父类调用的是父类中的重名成员变量,子类调用的是子类中的重名成员变量。所以,多态状态下调用重名成员变量,无论编译和运行,都参考引用型变量所属的类(父类)。

3)多态状态下静态成员方法的特点(面试用):

示例代码:

package com.heisejiuhuche;

public class TestPolymorphism {
    public static void main(String[] args) {
        Parent p = new SubClass();
        p.method1();
    }

}

class Parent {

    public static void method1() {
        System.out.println("Parent method1...");
    }

}

class SubClass extends Parent {

    public static void method1() {
        System.out.println("SubClass method1...");
    }
}

上述代码运行结果为:

Parent method1...

静态成员方法所属于类,是类成员,当类加载的时候,静态方法method1就已经被绑定在其所属的Parent类上(静态绑定)。虽然父类的引用p指向子类SubClass的对象,但是当用p调用静态方法method1的时候,找的还是Parent类中的静态方法method1。总结为,多态状态下调用静态成员方法,无论编译和运行,都参考引用型变量所属的类(父类)。

6.多态的应用
1)电脑运行示例
定义一个电脑类,电脑的运行基于主板,要在主板上实现多种功能,比如声卡,显卡,网卡等,用多态实现。

无多态示例代码:

class Computer {
    public static void main(String[] args) {
        MotherBoard mb = new MotherBoard();
        //启动主板
        mb.run();
        //让主板使用网卡功能
        mb.useEnthernetController();
    }
}

class MotherBoard {
    /*
     * 主板基本功能
     */
    public void run() {
        System.out.println("Motherboard running...");
    }

    public void useEnthernetController(EnthernetController e) {
        e.turnUp();
        e.turnDown();
    }
}

/*
 * 声明网卡类
 */
class EnthernetController {

    public void turnUp() {
        System.out.println("EnthernetController turned up...");
    }

    public void turnDown() {
        System.out.println("EnthernetController turned down...");       
    }
}

如果以后继续添加功能,如声卡功能,显卡功能等,上述代码在增加新功能类的同时,还要修改主板代码,添加相应方法,才能调用相应功能。因此,没有使用多态的情况下,代码没有可复用性,代码扩展能力、可维护性低下。

使用多态代码示例:

package com.heisejiuhuche;

/**
 * 用多态的方式实现一台电脑,使主板具有功能扩展性,使代码具备复用性和可扩展性
 * @author jeremy
 *
 */

public class Computer {
    public static void main(String[] args) {
        MotherBoard mb = new MotherBoard();
        mb.run();
        //第一次使用基本功能,没有扩展功能即PCI对象为null
        mb.usePCI(null);
        //第二次使用上网功能呢,使用PCI对象EnthernetController
        mb.usePCI(new EnthernetController());
        //第三次使用声音功能呢,使用PCI对象AudioDevice
        mb.usePCI(new AudioDevice());
    }
}

class MotherBoard {
    /*
     * 主板基本功能
     */
    public void run() {
        System.out.println("Motherboard running...");
    }

    /*
     * 主板使用PCI插槽功能
     */
    public void usePCI(PCI p) {
        /*
         * 如果PCI对象p不为空,则调用turnUp(),turnDown()功能
         */
        if(p != null) {
            p.turnUp();
            p.turnDown();
        }
    }
}

/*
 * 声明PCI接口,以扩展主板功能
 */
interface PCI {
    public abstract void turnUp();
    public abstract void turnDown();
}

/*
 * 声明网卡,实现PCI接口,扩展主板的上网功能
 */
class EnthernetController implements PCI {

    @Override
    public void turnUp() {
        System.out.println("EnthernetController turned up...");
    }

    @Override
    public void turnDown() {
        System.out.println("EnthernetController turned down...");       
    }

}

/*
 * 声明声卡,实现PCI接口,扩展主板的听音乐功能
 */
class AudioDevice implements PCI {

    @Override
    public void turnUp() {
        System.out.println("AudioDevice turned up...");     
    }

    @Override
    public void turnDown() {
        System.out.println("AudioDevice turned down...");               
    }
}

多态的使用使程序可扩展性大大提升。今后添加的功能,只需增添新的功能类并实现PCI接口,然后在主函数中,调用主板对象的usePCI功能,并将新的PCI功能作为参数传递即可。

2)Lady有宠物
定义动物类,定义Lady类;不同的Lady有不同的宠物;当不同的Lady逗她们各自的宠物时,宠物表现出不同的行为方式,用多态实现。

示例代码:

//定义Animal类
class Animal {
    private String name;

    Animal(String name) {
        this.name = name;
    }

    public void enjoy() {
        System.out.println("scream.......");
    }
}

//定义Cat类,继承Animal,复写enjoy()方法
class Cat extends Animal {
    private String eyesColor;

    Cat(String n, String c) {
        super(n);
        eyesColor = c;
    }

    public void enjoy() {
        System.out.println("cat scream........");
    }   
}

//定义Dog类,继承Animal,复写enjoy()方法
class Dog extends Animal {
    private String furColor;

    Dog(Stirng n, Stirng c) {
        super(n);
        furColor = c;
    }

    public void enjoy() {
        System.out.println("dog scream..........");
    }
}

class Lady {
    private String name;
    private Animal pet;

    // 父类Animal的引用指向子类pet对象
    Lady(Strin name, Animal pet) {
        this.name = name;
        this.pet = pet;
    }

    //调用pet的enjoy方法,实现多态
    public void myPetEnjoy() {
        pet.enjoy();
    }
}


public class Test {
    public static void main(String[] args) {
        //初始化Cat,Dog对象
        Cat c = new Cat("catname", "blue");
        Dog d = new Dog("dogname", "black");

        //初始化Lady对象,构造两个Lady,并把Cat,Dog对象传给她们
        Lady l1 = new Lady("l1", c);
        Lady l2 = new Lady("l2", d);

        //调用Lady的myPetEnjoy()方法,继而调用pet的enjoy()方法,实现多态
        l1.myPetEnjoy();
        l2.myPetEnjoy();
    }
}

输出结果:

cat scream..........
dog scream..........

二、Object类

1.Object类的定义
Object类是所有类的直接或间接父类,该类中定义的是所有类都具备的功能。

2.Object类中的方法
1)equals()方法

示例代码:

class Cat {

}

class TestObject {
    public static void main(String[] args) {
        Cat c1 = new Cat();
        Cat c2 = new Cat();
        Cat c3 = c1;
        System.out.println(c1.equals(c2));
        System.out.println(c1 == c2);
        System.out.println(c1.equals(c3));
        System.out.println(c1 == c3);
    }
}

程序运行结果为:

false
false
true
true

equals==比较的是两个对象的内存地址值,只要内存地址相同,返回true,否则返回false。实际开发中,比较对象意义不大。如果要建立比较对象中特定成员变量值的方法,只需复写Object类中的equals()方法即可。

示例代码:

package com.heisejiuhuche;

public class TestEquals {
    public static void main(String[] args) {
        Cat c1 = new Cat(4);
        Cat c2 = new Cat(4);
        System.out.println("c1 equals c2: " + '\t' + c1.equals(c2));
        System.out.println("c1 == c2: " + '\t' + (c1 == c2));

    }
}

class Cat {
    int num;

    Cat(int num) {
        this.num = num;
    }

    /*
     * 复写equals方法
     */
    public boolean equals(Object obj) {
        /* 判断,如果参数obj不是Cat类型,直接返回false */
        if(!(obj instanceof Cat)) {
            return false;
        }
        /* 如果参数是Cat类型,向下转型成Cat类型,再做比较 */
        Cat c = (Cat)obj;
        /* 比较本来的num和参数c的num值,相等返回true,不等返回false */
        return this.num == c.num;
    }
}

程序输出结果:

true

2)toString()方法
ObjecttoString()方法返回调用该方法的对象所属的类名+@+该对象的哈希值

toString()方法:getClass().getName() + '@' + Integer.toHexString(hashCode())

该返回值对用户来说没有什么意义,因此,要自定义对象的输出,只需复写Object类当中的toString()方法。

示例代码:

package com.heisejiuhuche;

public class TestToString {
    public static void main(String[] args) {
        HeiMa hm1 = new HeiMa(6);
        System.out.println(hm1.toString());

        HeiMa hm2 = new HeiMa(7);
        System.out.println(hm2.toString());
    }
}

class HeiMa {
    int num;

    HeiMa(int num) {
        this.num = num;
    }

    /* 复写Object中的toString()方法 */
    public String toString() {
        return this.getClass().getName() + ":" + num;
    }
}

程序输出结果为:

com.heisejiuhuche.HeiMa:6
com.heisejiuhuche.HeiMa:7

三、扩展知识

1.对象池
1)对象池概述
Java利用对象池用来储存包装类对象和String对象。JVM启动的时候,会实例化9个对象池,用于储存8中基本数据类型对应包装类的对象以及String对象

2)对象池的优点
对象池技术能够有效减少对象生成和初始化时的内存消耗,提高系统运行效率

3)对象池基本原理
以创建String对象为例,当直接用引号包含字符串时(String s = "abc";),虚拟机回到String的对象池中寻找是否有等于"abc"这个值的对象,有就直接拿来用,没有就创建一个,并将"abc"在内存中的值,赋给这个对象引用

示例代码:

String s1 = "abc";
String s2 = "abc";

由于两个对象都在对象池中,s1s2指向的是同一String对象,内存地址相同,因此:

System.out.println(s1 == s2);

输出结果为:

true

当使用new的方式创建String对象时(String s = new String("abc");),虚拟机将在堆内存中开辟一片内存空间,储存该String对象,即该对象在对象池外被创建。

示例代码:

String s1 = "abc";
String s2 = new String("abc");

由于s2不在对象池中,是堆内存中一个新的对象,内存地址不同,因此:

System.out.println(s1 == s2);

输出结果为:

false

2.String类的equals()方法和”==“运算符
String类的equals()方法在运用的时候和==操作符有些许不同。

示例代码:

String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
System.out.println(s1.equals(s2)); //返回true,地址相同(都在对象池),值相同
System.out.println(s1 == s2); //返回true,地址相同,值相同
System.out.println(s1.equals(s3)); //返回true,地址不同,但是值相同
System.out.println(s1 == s2); //返回false,值相同,但是地址不同

总结,String类的equals()方法,比较的是两个String的值,只要值相同,返回true;而==运算符比较的是地址和值,只有两者都相同,才返回true