面向对象编程-中级 下
8.多态
polymorphic
多态入门
●先看一个问题
√ 问题描述 com.zzw.poly_包 Poly01.java
请编写一个程序, Master 类 中有一个 feed(喂食) 方法, 可以完成 主人给动物喂食的信息.
●先看一个问题
√ 使用传统的方法来解决(private属性)
√ 传统的方法带来的问题是什么? 如何解决?
问题是: 代码的复用性不高, 而且不利于代码维护
解决方案: 引出我要讲解的多态
多态基本介绍
方法或对象具有多种形态[多种状态], 是面向对象的第三大特征, 多态是建立在封装和继承基础之上的.
多态的具体体现
1.方法的多态 PolyMethod.java
重写和重载就体现多态
2.对象的多态(核心, 困难, 重点)
重要的几句话:
(1)一个对象的编译类型和运行类型可以不一致
(2)编译类型在定义对象时, 就确定了, 不能改变 Animal animal
(3)运行类型是可以变化的
(4)编译类型看定义时 =号 的左边, 运行类型看 =号
案例: com.zzw.poly_.objpoly_包 PolyObject.java
Animal animal = new Dog(); [animal 编译类型是Animal, 运行类型是Dog]
animal = new Cat(); [animal 的运行类型变成了 Cat, 编译类型仍然是 Animal]
●多态快速入门案例
使用多态的机制解决主人喂食物的问题, 走代码. Poly01.java
多态的方便性: 假设主人给小猪喂食米饭, 很好实现. 新建米饭, 小猪实体类, 直接调用feed方法, 传入对应的对象就行.
多态注意事项和细节讨论
com.zzw.poly_.detail_包 PolyDatail.java
多态的前提
两个对象(类)存在继承关系
多态的向上转型
1)本质: 父类的引用指向了子类的对象
2)语法: 父类类型 引用名 = new 子类类型();
3)特点: 编译类型看左边, 运行类型看右边
可以调用父类中的所有成员(需遵守访问权限)
不能调用子类中特有成员;
最终运行效果看子类的具体实现!
访问修饰符结合继承理解
不具有继承关系 对象.属性名/方法名
1.同包不同类下, 可以访问public, protected 和 默认
2.不同包下, 可以访问public
具有继承关系 this.属性名/方法名
1.同胞不同类下, 可以访问public, protected 和 默认
2,不同包下, 可以访问public, protected
多态的向下转型
1)语法: 子类类型 引用名 = (子类类型) 父类引用
2)只能强转父类的引用(身份和地位), 不能强转父类的对象(本人)
3)强转, 会损失精度, 即原来能调用的父类的方法不能调用了.
3)要求父类的引用必须指向当前目标类型的对象
这样就有两个引用指向了Cat对象4)当向下转型后, 可以调用子类类型中所有的成员
属性没有重写一说!
属性的值看编译类型 PolyDetail02.java
instanceof 比较操作符
用于判断对象的运行类型是否为XX类型或XX类型的子类型 PolyDetail03.java
课堂练习
com.zzw.poly_.exercise_包 PolyExercise01.java
请说出下面的每条语言, 哪些是正确的, 哪些是错误的, 为什么?
public class PolyExercise01 {
public static void main(String[] args) {
double d = 13.4;
long l = (long)d;
System.out.println(l);
int in = 5;
boolean b = (boolean) in;
Object obj = “Hello”;
String objStr = (String)obj;
System.out.println(objStr);
Object objPri = new Integer(5);
String str = (String)objPri;
Integer str1 = (Integer) objPri;
}
}
public class PolyExercise01 {
public static void main(String[] args) {
double d = 13.4;//ok
long l = (long)d;//ok
System.out.println(l);//ok
int in = 5;//ok
boolean b = (boolean) in;//不对boolean --> int
Object obj = “Hello”;//ok, String -> Object, 向上转型
String objStr = (String)obj;//ok, Object -> String, 向下转型
System.out.println(objStr);//ok
Object objPri = new Integer(5);//ok, Integer -> Object, 向上转型
String str = (String)objPri;//不可以, 把指向Integer的父类引用转成String引用
Integer str1 = (Integer) objPri;//ok, Object -> Integer, 正确的向下转型
}
}
PolyExercise02.java
只要是方法的调用, 就要看对象的运行类型.
public class PolyExercise02 {
public static void main(String[] args) {
Sub s = new Sub();
System.out.println(s.count);
s.display();
Base b = s;
System.out.println(b == s);//true
System.out.println(b.count);
b.display();
}
}
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);
}
}
动态绑定机制
java重要特性: 动态绑定机制 com.zzw.poly_dynamic_包 DynamicBinding.java
public class DynamicBinding {
public static void main(String[] args) {
A a = new B();
System.out.println(a.sum());
System.out.println(a.sum1());
}
}
class A { //父类
public int i = 10;
public int getI() {
return i;
}
public int sum() {
return getI() + 10;//动态绑定机制
}
public int sum1() {
return i + 10;
}
}
class B extends A { //子类
public int i = 20;
public int getI() {
return i;
}
public int sum() {
return i + 20;
}
public int sum1() {
return i + 10;
}
}
进阶: 去掉B类的sum方法, 求a.sum输出什么?
答: a引用的运行类型是B, 所以去B中找sum方法, B中没有, 就去父类A中去找, 找到了, 并且可以调用, 就调用. 调用getI方法, 这时发现子类B中也有getI方法, 这时会怎么办?
java的动态绑定机制
1.当调用对象方法的时候, 该方法会和该对象的内存地址/运行类型绑定
2.当调用对象属性时, 没有动态绑定机制, 哪里声明, 哪里调用. (即属性不会从运行类型开始查找).
进阶: 去掉B类的sum1方法, 求a.sum1输出什么?
答: a引用的运行类型是B, 所以去B中找sum1方法, B中没有, 就去父类A中去找, 找到了, 并且可以调用, 则调用. 由于属性没有动态绑定机制, 哪里声明, 哪里调用, 所以返回20.
难点:
1)调用子类属性时, 如果子类有, 则调用子类的, 如果子类没有, 则去找父类;
2)不能通过调用父类属性来达到调用子类属性的目的, 因为属性没有动态绑定机制, 属性不能被重写. (我自己总结的: 也就是说, 父类不能调用子类的属性).
3)属性按作用域来找, 但是作用域内没有还是按照继承机制
可以通过调用父类方法来达到调用-子类方法的目的, 因为方法被重写了, 方法可以动态绑定;
温故而知新
(1)属性调用是从本类编译类型开始查找
(2)方法调用是查看编译类型中是否有该方法, 如果有, 则从运行类型开始查找; 如果没有, 则报错
调用对象方法时, 首先看对象的编译类型有没有, 编译类型有, 那就从运行类型开始查找(目的是为了查看这个方法有没有被子类重写: 这里要理解重写的概念), 子类的方法必须要满足重写, 才算是有, 否则按照继承关系建立查找关系
多态数组
1)多态数组 com.zzw.poly_.polyarr_包 PloyArray.java, Person.java, Student.java, Teacher.java
数组的定义类型为父类类型, 里面保存的实际元素为子类类型
应用实例: 现有一个继承机构如下: 要求创建1个Person对象, 2个Student对象和2个Teacher对象, 统一放在数组中, 并调用每个对象的say方法.
public class Person {
private String name;
private int age;
public String say() {
return "姓名 " + name + "\t年龄 " + age;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
}
public class Student extends Person {
private double score;
//重写父类的say方法
public String say() {
return super.say() + "\t成绩 " + score;
}
public Student(String name, int age, double score) {
super(name, age);
this.score = score;
}
public double getScore() {
return score;
}
}
public class Teacher extends Person {
private double salary;
//重写父类的say方法
public String say() {
return super.say() + "\t薪水 " + salary;
}
public Teacher(String name, int age, double salary) {
super(name, age);
this.salary = salary;
}
public double getSalary() {
return salary;
}
}
public class PolyArray {
//要求创建1个Person对象, 2个Student对象和2个Teacher对象
// , 统一放在数组中, 并调用每个对象的say方法.
public static void main(String[] args) {
Person[] persons = new Person[5];
persons[0] = new Person("jack", 10);
persons[1] = new Student("tom", 15, 100);
persons[2] = new Student("mary", 23, 85);
persons[3] = new Teacher("king", 40, 20000);
persons[4] = new Teacher("queen", 35, 25000);
//循环遍历多态数组, 调用say方法
for(int i = 0; i < persons.length; i++) {
//提示: person[i] 编译类型是Person, 运行类型是根据实际情况由JVM判断
System.out.println(persons[i].say());
}
}
}
应用实例升级: 如果调用子类特有的方法, 比如Teacher有一个teach方法, Student有一个study方法, 怎么调用?
public class PolyArr {
public static void main(String[] args) {
//现有一个继承结构如下, 要求创建1个Person对象,2个Student对象2个Teacher对象,
//统一放在数组中, 并调用每个对象的say方法.
Person[] persons = new Person[5];
persons[0] = new Person("jack", 10);
persons[1] = new Student("tom", 15, 100);
persons[2] = new Student("mary", 23, 85);
persons[3] = new Teacher("jack", 22, 15000);
persons[4] = new Teacher("scott", 20, 17000);
//循环遍历多态数组, 调用say方法
for (int i = 0; i < persons.length; i++) {
//提示: person[i] 编译类型是 Person, 运行类型是根据实际情况由JVM机来判断
System.out.println(persons[i].say());//动态绑定机制
//不能调用子类的特有的成员
//因为在编译阶段, 能调用哪些成员, 是由编译类型来决定的
if (persons[i] instanceof Student) {//判断person[i] 的运行类型是不是Student
Student student = (Student) persons[i];
student.study();//向下转型
} else if (persons[i] instanceof Teacher) {
Teacher teacher = (Teacher) persons[i];
teacher.teach();
} else if (persons[i] instanceof Person) {
} else {
System.out.println("你的类型有误, 请自己检查...");
}
}
}
}
多态参数
方法定义的形参类型为父类类型, 实参类型允许为子类类型
应用实例1: 前面的主人喂动物
应用实例2: com.zzw.poly_.polyparameter_包
PolyParameter.java
定义员工类Employee, 包含姓名和月工资[private], 以及计算年工资getAnnual的方法. 普通工人和经理继承了员工, 经理类多了奖金bonus属性和管理manage方法, 普通工人类多了work方法, 普通工人和经理类要求分别重写getAnnual方法. 如果没说什么字段,默认private
测试类中添加一个方法showEmpAnnual(Employee e), 实现获取任何员工对象的年工资, 并在main方法中调用该方法 [e.getAnnual()]
测试类中添加一个方法testWork(). 如果是普通员工, 则调用work方法; 如果是经理, 则调用manage方法
public class Employee {
private String name;//姓名
private double salary;//月工资
//计算年工资
public Double getAnnual() {
return 12 * salary;
}
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
}
public class Worker extends Employee {
public Worker(String name, double salary) {
super(name, salary);
}
public void work() {
System.out.println("普通员工 " + getName() + " 工作中...");
}
public Double getAnnual() {//因为普通员工没有其它的收入, 则直接调用父类的方法即可
return super.getAnnual();
}
}
public class Manager extends Employee {
private double bonus;//奖金
public Manager(String name, double salary, double bonus) {
super(name, salary);
this.bonus = bonus;
}
public void manage() {
System.out.println("经理 " + getName() + " 在管理工作中...");
}
//重写获取年薪的方法
public Double getAnnual() {
return super.getAnnual() + bonus;
}
}
public class PolyParameter {
public static void main(String[] args) {
Worker tom = new Worker("tom", 3000);
Manager mary = new Manager("mary", 5000, 200000);
PolyParameter polyParameter = new PolyParameter();
polyParameter.showEmpAnnual(tom);
polyParameter.showEmpAnnual(mary);//动态绑定机制
polyParameter.testWork(tom);
polyParameter.testWork(mary);
}
public void showEmpAnnual(Employee e) {
System.out.println(e.getAnnual());
}
public void testWork(Employee e) {
if (e instanceof Worker) {
Worker worker = (Worker) e;//向下转型
worker.work();
} else if (e instanceof Manager) {
Manager manager = (Manager) e;//向下转型
manager.manage();
} else {
System.out.println("你的类型有误...");
}
}
}
9.Object类详解
equals方法
==和equals的对比
== 是一个比较运算符 com.zzw.object_:
Equals01.java
1.==: 既可以判断基本类型, 又可以判断引用类型 :
2.==: 如果判断基本类型, 判断的是值是否相等. 示例
3.==: 如果判断引用类型, 判断的是地址是否相等, 即判断是不是同一个对象
4.equals: 是Object类中的方法, 只能判断引用类型, 如何看JDK源码
5.Object类中的equals默认判断地址是否相等, 子类往往重写这个方法, 用于判断内容是否相等. 比如String和Integer.
String源码
//分析JDK源码 String类的equals方法
//重写了父类Object的equals方法, 变成了比较两个字符串的值是否相等
public boolean equals(Object anObject) {
if (this == anObject) {//如果传进来的对象和当前对象是同一个对象
return true;
}
if (anObject instanceof String) {//如果入参对象的运行类型是字符串String
String anotherString = (String)anObject;//向下转型(强转)
int n = value.length;//计算当前字符串的长度
if (n == anotherString.value.length) {//如果当前字符串长度和传进来的字符串长度相等
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {//循环n次,依次比较字符串的每个字符
if (v1[i] != v2[i])
return false;
i++;
}//如果字符都相同, 最后返回true
return true;
}
}
//如果比较的不是字符串, 返回false
return false;
}
Object源码
//Object类的equals方法, 默认比较两个对象的值是否相等
//即比较两个对象是否是同一个对象
public boolean equals(Object obj) {
return (this == obj);
}
Integer源码
//从源码可以看到, Integer也重写了Object的equals方法
//变成了判断两个对象的值是否相等
public boolean equals(Object obj) {
if (obj instanceof Integer) {//如果入参对象的运行类型是Integer
return value == ((Integer)obj).intValue();//向下转型(强转), 判断两个对象的值是否相等
}
return false;
}
Integer integer1 = new Integer(1000);
Integer integer2 = new Integer(1000);
// == 判断对象时, 判断的是 是否是同一个对象, 显然这里是两个对象
System.out.println(integer1 == integer2);//返回false
//Integer类重写了父类Object的equals方法, 比较的是两个对象的值是否相等
System.out.println(integer1.equals(integer2));//显然返回true
String str1 = new String("zzw");
String str2 = new String("zzw");
//显然这里是两个对象, 指向的地址不相等
System.out.println(str1 == str2);//返回false
//String重写了equals方法, 比较的是内容是否相等
System.out.println(str1.equals(str2));//这里返回true
图解
名称 | 概念 | 用于基本数据类型 | 用于引用类型 |
---|---|---|---|
== | 比较运算符 | 可以,判断值是否相等 | 可以,判断指向的是否是同一个对象 |
equals | Object类的方法 | 不可以 | 可以,默认判断两个对象是否是同一个对象。 |
如何重写equals方法
应用实例: 判断两个Person对象的内容是否相等, 如果两个Person对象的各个属性值都相等, 返回true, 否则返回false. src/com/zzw/Object_/EqualsExercise01.java
public class EqualsExercise01 {
public static void main(String[] args) {
Person mary = new Person("mary", 23, '男');
Person mary2 = new Person("mary", 23, '男');
//原先比较地址, 返回false
//重写后, 比较对象属性的值, 返回true
System.out.println(mary.equals(mary2));
}
}
class Person {
private String name;
private int age;
private char gender;
public Person(String name, int age, char gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
//重写父类Object的equals方法
public boolean equals(Object obj) {
//如果传进来的和当前的是同一个对象
if (this == obj) {
return true;
}
//如果传进来的对象的运行类型是Person
if (obj instanceof Person) {
//向下转型, 取值
Person person = (Person) obj;
//依次比较属性(本类中有权访问私有属性)
return name.equals(person.name) && age == person.age && gender == person.gender;
}
//如果比较的不是同一个对象, 返回false
return false;
}
}
课堂练习
src/com/zzw/Object_/EqualsExercise02.java
public class EqualsExercise02 {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = “赵志伟”;
Person p2 = new Person();
p2.name = “赵志伟”;
System.out.println(p1 == p2);//false, 两个对象
System.out.println(p1.name.equals(p2.name));//true
System.out.println(p1.equals(p2));//false, equals()默认比较指向的地址
String s1 = new String(“zzw”);
String s2 = new String(“zzw”);
System.out.println(s1.equals(s2));//true, String重写了equals, 比较的是内容
System.out.println(s1 == s2);//false, 并不是一个对象
}
}
class Person {
public String name;
}
src/com/zzw/Object_/EqualsExercise03.java
public class EqualsExercise03 {
public static void main(String[] args) {
int it = 65;
float fl = 65.0f;
System.out.println("65和65.0f是否相等? " + (it == fl));//true, 比较值
char c1 = ‘A’;
char c2 = 12;
System.out.println("65和’A’是否相等? " + (it == c1));//true, 字符的本质是数字
System.out.println(“12和c2是否相等?” + (12 == c2));//true
String s1 = new String(“hello”);
String s2 = new String(“hello”);
System.out.println("s1和s2是否相等? " + (s1 == s2));//false, 因为是两个对象
System.out.println("s1是否equals s2 " + s1.equals(s2));//true, 因为String类的equals比较的是内容
}
}
hashCode方法
小结:
1)提高具有哈希结构的容器的效率!
2)两个引用,如果指向的是同一个对象,则哈希值肯定是一样的!
3)两个引用,如果指向的是不同对象,则哈希值是不一样的
4)哈希值主要根据地址号来计算的,但不能将哈希值等价于地址。
5)案例演示 HashCode_.java
public class HashCode_ {
public static void main(String[] args) {
AA aa = new AA();
AA aa2 = new AA();
AA aa3 = aa;
System.out.println("aa的哈希值=" + aa.hashCode());
System.out.println("aa2的哈希值=" + aa2.hashCode());
System.out.println("aa3的哈希值=" + aa3.hashCode());
}
}
class AA {}
toString方法
√ 基本介绍
默认返回: 全类名(包名 + 类名) + @ + 哈希值的十六进制, 子类往往重写toString方法, 用于返回对象的属性信息
源码
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
√ 重写toString()方法, 打印对象或拼接对象时,都会自动调用该对象的toString形式。
案例演示:Monster【name,job,sal】,ToString.java
√ 当直接输出一个对象时,toString方法会被默认的调用
,比如System.out.println(monster); 就会默认调用 monster.toString()
public class ToString_ {
public static void main(String[] args) {
/**
* Object的toString方法源码
* (1) getClass().getName() 类的全类名(包名+类名)
* (2) Integer.toHexString(hashCode()) 将对象的hashCode值转成16进制
* public String toString() {
* return getClass().getName() + "@" + Integer.toHexString(hashCode());
* }
*/
Monster monster = new Monster("小旋风", "巡山的", 2000);
System.out.println(monster.toString()
+ "\nhashCode=" + Integer.toHexString(monster.hashCode())
+ "\n" + monster.hashCode());
System.out.println("=====当直接输出一个对象时,toString方法会被默认的调用=====");
System.out.println(monster);//等价monster.toString()
}
}
class Monster {
private String name;
private String job;
private double sal;
public Monster(String name, String job, double sal) {
this.name = name;
this.job = job;
this.sal = sal;
}
//重写toString方法, 输出对象的属性
//使用快捷键 alt + insert -> toString键
//重写后, 一般是把对象的属性值输出, 当然程序员也可以自己定制
@Override
public String toString() {
return "Monster{" +
"name='" + name + '\'' +
", job='" + job + '\'' +
", sal=" + sal +
'}';
}
}
finalize方法
1.当对象被回收时,系统自动调用该对象的finalize方法,子类可以重写该方法,做一些释放资源的操作。
2.什么时候被回收: 当某个对象没有任何引用时, 则jvm就认为这个对象是一个垃圾对象, 就会使用垃圾回收机制来销毁该对象. 在销毁对象前, 会先调用finalize方法.
3.垃圾回收机制的调用, 是由系统来决定(即有自己的GC算法), 为了看到这个效果, 也可以通过 System.gc() 主动触发垃圾回收机制, 测试: Car[name]
提示: 我们在实际开发中, 几乎不会使用 finalize, 所以更多就是为了应付面试.
Finalize_.java
//演示 Finalize方法
public class Finalize_ {
public static void main(String[] args) {
Car bmw = new Car("宝马");
//这时, car对象就是一个垃圾, 垃圾回收器就会回收(销毁)对象. 在销毁对象前, 会调用该对象的finalize方法
//, 程序员就可以在 finalize中, 写自己的业务代码(比如释放资源: 数据库连接/关闭文件...)
//, 如果程序员不重写 finalize, 那么就会调用 Object类的 finalize, 即默认处理
//, 如果程序员重写了 finalize, 就可以实现自己的逻辑
bmw = null;
System.gc();//主动调用垃圾回收器
System.out.println("程序退出了");
}
}
class Car {
//属性是资源
private String name;
public Car(String name) {
this.name = name;
}
//重写 finalize
@Override
protected void finalize() throws Throwable {
System.out.println("我们销毁 汽车" + name);
System.out.println("释放了某些资源...");
}
}
10.断点调试
√ 案例1 com.zzw.debug_包 Debug01.java
看一下变量的变化情况. F8 逐行执行代码
public class Debug01 {
public static void main(String[] args) {
//演示逐行执行
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += i;
System.out.println("i=" + i);
System.out.println("sum=" + sum);
}
System.out.println("退出for循环..");
}
}
截图 + 说明
√ 案例2 Debug02.java
看一下数组越界的异常
public class Debug02 {
public static void main(String[] args) {
int[] arr = {1, 10, -1};
for (int i = 0; i <= arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println("退出for循环..");
}
}
√ 案例3 Debug03.java
演示如何追踪源码, 看看java设计者是怎么实现的 (提高编程思想) F7 跳入 IDEA Debug 如何进入源码
小技巧: 将光标放在某个变量上, 可以看到最新的数据.
Debug 时没有Force Step Into 按钮解决方法
public class Debug03 {
public static void main(String[] args) {
int arr[] = {1, -1, 10, -20, 100};
//我们想看Arrays.sort方法底层实现->debug
Arrays.sort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
截图 + 说明
√ 案例4 Debug04.java
演示如何直接执行到下一个断点 F9 Resume Program
小技巧:断点可以在debug过程中,动态地下断点
//演示执行到下一个断点,同时支持动态地下断点
public class Debug04 {
public static void main(String[] args) {
int arr[] = {1, -1, 10, -20, 100};
//我们想看Arrays.sort方法底层实现->debug
Arrays.sort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println("hello100~");
System.out.println("hello200~");
System.out.println("hello300~");
System.out.println("hello400~");
System.out.println("hello500~");
}
}
课后练习
1.使用断点调试的方法,追踪一个对象创建的过程。Person[name, age, 构造器…] 对象创建的流程分析 DebugExercise.java
//debug对象创建的过程,加深对调试的理解
public class DebugExercise {
public static void main(String[] args) {
//创建对象的流程
//(1)加载Person类信息
//(2)初始化 2.1默认初始化 2.2显式初始化 2.3构造器初始化
//(3)返回对象的地址
Person jack = new Person("jack", 20);
System.out.println(jack);
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
先转载类
跳出
Force Step Into Alt+Shift+F7
为什么输出对象默认调用对象的toString方法, 答案在String.valueOf方法里
obj.toString()会动态绑定到运行类型的Person.
进入
跳出
2.使用断点调试,查看动态绑定机制如何工作
Force Step Into Alt+Shift+F7
Step Into F7
取消B类的sum方法注释,重新Debug,观察效果
进入的是子类的sum方法