面向对象(三)
一、多态
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();
,SubClass
为Parent
子类为例
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
类中有method1
,method2
方法(运行看右边),运行成功。
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()方法Object
的toString()
方法返回调用该方法的对象所属的类名+@+该对象的哈希值
。
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";
由于两个对象都在对象池中,s1
和s2
指向的是同一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
。