# 第 07 章_面向对象编程 (进阶)
讲师:尚硅谷 - 宋红康(江湖人称:康师傅)
官网:http://www.atguigu.com
# 本章专题与脉络
# 1. 关键字:this
# 1.1 this 是什么?
在 Java 中,this 关键字不算难理解,它的作用和其词义很接近。
- 它在方法(准确的说是实例方法或非 static 的方法)内部使用,表示调用该方法的对象
- 它在构造器内部使用,表示该构造器正在初始化的对象。
this 可以调用的结构:成员变量、方法和构造器
# 1.2 什么时候使用 this
# 1.2.1 实例方法或构造器中使用当前对象的成员
在实例方法或构造器中,如果使用当前类的成员变量或成员方法可以在其前面添加 this,增强程序的可读性。不过,通常我们都习惯省略 this。
但是,当形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加 this 来表明该变量是类的成员变量。即:我们可以用 this 来区分 成员变量
和 局部变量
。比如:
另外,使用 this 访问属性和方法时,如果在本类中未找到,会从父类中查找。这个在继承中会讲到。
举例 1:
class Person{ // 定义 Person 类 | |
private String name ; | |
private int age ; | |
public Person(String name,int age){ | |
this.name = name ; | |
this.age = age ; | |
} | |
public void setName(String name){ | |
this.name = name; | |
} | |
public void setAge(int age){ | |
this.age = age; | |
} | |
public void getInfo(){ | |
System.out.println("姓名:" + name) ; | |
this.speak(); | |
} | |
public void speak(){ | |
System.out.println(“年龄:” + this.age); | |
} | |
} |
举例 2:
public class Rectangle { | |
int length; | |
int width; | |
public int area() { | |
return this.length * this.width; | |
} | |
public int perimeter(){ | |
return 2 * (this.length + this.width); | |
} | |
public void print(char sign) { | |
for (int i = 1; i <= this.width; i++) { | |
for (int j = 1; j <= this.length; j++) { | |
System.out.print(sign); | |
} | |
System.out.println(); | |
} | |
} | |
public String getInfo(){ | |
return "长:" + this.length + ",宽:" + this.width +",面积:" + this.area() +",周长:" + this.perimeter(); | |
} | |
} |
测试类:
public class TestRectangle { | |
public static void main(String[] args) { | |
Rectangle r1 = new Rectangle(); | |
Rectangle r2 = new Rectangle(); | |
System.out.println("r1对象:" + r1.getInfo()); | |
System.out.println("r2对象:" + r2.getInfo()); | |
r1.length = 10; | |
r1.width = 2; | |
System.out.println("r1对象:" + r1.getInfo()); | |
System.out.println("r2对象:" + r2.getInfo()); | |
r1.print('#'); | |
System.out.println("---------------------"); | |
r1.print('&'); | |
System.out.println("---------------------"); | |
r2.print('#'); | |
System.out.println("---------------------"); | |
r2.print('%'); | |
} | |
} |
# 1.2.2 同一个类中构造器互相调用
this 可以作为一个类中构造器相互调用的特殊格式。
this()
:调用本类的无参构造器this(实参列表)
:调用本类的有参构造器
public class Student { | |
private String name; | |
private int age; | |
// 无参构造 | |
public Student() { | |
// this ("",18);// 调用本类有参构造器 | |
} | |
// 有参构造 | |
public Student(String name) { | |
this();// 调用本类无参构造器 | |
this.name = name; | |
} | |
// 有参构造 | |
public Student(String name,int age){ | |
this(name);// 调用本类中有一个 String 参数的构造器 | |
this.age = age; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public int getAge() { | |
return age; | |
} | |
public void setAge(int age) { | |
this.age = age; | |
} | |
public String getInfo(){ | |
return "姓名:" + name +",年龄:" + age; | |
} | |
} |
注意:
- 不能出现递归调用。比如,调用自身构造器。
- 推论:如果一个类中声明了 n 个构造器,则最多有 n - 1 个构造器中使用了 "this (形参列表)"
this()
和this(实参列表)
只能声明在构造器首行。- 推论:在类的一个构造器中,最多只能声明一个
this(参数列表)
- 推论:在类的一个构造器中,最多只能声明一个
# 1.3 练习
** 练习 1:** 添加必要的构造器,综合应用构造器的重载,this 关键字。
练习 2:
(1)按照如下的 UML 类图,创建相应的类,提供必要的结构:
在提款方法 withdraw () 中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。deposit () 方法表示存款。
(2)按照如下的 UML 类图,创建相应的类,提供必要的结构
(3)按照如下的 UML 类图,创建相应的类,提供必要的结构
addCustomer 方法必须依照参数(姓,名)构造一个新的 Customer 对象,然后把它放到 customer 数组中。还必须把 numberOfCustomer 属性的值加 1。
getNumOfCustomers 方法返回 numberofCustomers 属性值。
getCustomer 方法返回与给出的 index 参数相关的客户。
(4)创建 BankTest 类,进行测试。
内存解析图:
# 2. 面向对象特征二:继承 (Inheritance)
# 2.1 继承的概述
# 2.1.1 生活中的继承
- 财产继承:
- 绿化:前人栽树,后人乘凉
“绿水青山,就是金山银山”
样貌:
继承之外,是不是还可以 "
进化
":
继承有延续(下一代延续上一代的基因、财富)、扩展(下一代和上一代又有所不同)的意思。
# 2.1.2 Java 中的继承
角度一:从上而下
为描述和处理个人信息,定义类 Person:
为描述和处理学生信息,定义类 Student:
通过继承,简化 Student 类的定义:
说明:Student 类继承了父类 Person 的所有属性和方法,并增加了一个属性 school。Person 中的属性和方法,Student 都可以使用。
角度二:从下而上
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成 继承关系
。如图所示:
再举例:
# 2.1.3 继承的好处
继承的出现减少了代码冗余,提高了代码的复用性。
继承的出现,更有利于功能的扩展。
继承的出现让类与类之间产生了
is-a
的关系,为多态的使用提供了前提。- 继承描述事物之间的所属关系,这种关系是:
is-a
的关系。可见,父类更通用、更一般,子类更具体。
- 继承描述事物之间的所属关系,这种关系是:
注意:不要仅为了获取其他类中某个功能而去继承!
# 2.2 继承的语法
# 2.2.1 继承中的语法格式
通过 extends
关键字,可以声明一个类 B 继承另外一个类 A,定义格式如下:
[修饰符] class 类A { | |
... | |
} | |
[修饰符] class 类B extends 类A { | |
... | |
} |
# 2.2.2 继承中的基本概念
类 B,称为子类、派生类 (derived class)、SubClass
类 A,称为父类、超类、基类 (base class)、SuperClass
# 2.3 代码举例
1、父类
package com.atguigu.inherited.grammar; | |
/* | |
* 定义动物类 Animal,做为父类 | |
*/ | |
public class Animal { | |
// 定义 name 属性 | |
String name; | |
// 定义 age 属性 | |
int age; | |
// 定义动物的吃东西方法 | |
public void eat() { | |
System.out.println(age + "岁的" | |
+ name + "在吃东西"); | |
} | |
} |
2、子类
package com.atguigu.inherited.grammar; | |
/* | |
* 定义猫类 Cat 继承 动物类 Animal | |
*/ | |
public class Cat extends Animal { | |
int count;// 记录每只猫抓的老鼠数量 | |
// 定义一个猫抓老鼠的方法 catchMouse | |
public void catchMouse() { | |
count++; | |
System.out.println("抓老鼠,已经抓了" | |
+ count + "只老鼠"); | |
} | |
} |
3、测试类
package com.atguigu.inherited.grammar; | |
public class TestCat { | |
public static void main(String[] args) { | |
// 创建一个猫类对象 | |
Cat cat = new Cat(); | |
// 为该猫类对象的 name 属性进行赋值 | |
cat.name = "Tom"; | |
// 为该猫类对象的 age 属性进行赋值 | |
cat.age = 2; | |
// 调用该猫继承来的 eat () 方法 | |
cat.eat(); | |
// 调用该猫的 catchMouse () 方法 | |
cat.catchMouse(); | |
cat.catchMouse(); | |
cat.catchMouse(); | |
} | |
} |
# 2.4 继承性的细节说明
1、子类会继承父类所有的实例变量和实例方法
从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。
- 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存。
- 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会看它的父类甚至父类的父类是否声明了这个方法,遵循
从下往上
找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。
所以继承意味着子类的对象除了看子类的类模板还要看父类的类模板。
2、子类不能直接访问父类中私有的 (private) 的成员变量和方法
子类虽会继承父类私有 (private) 的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承的 get/set 方法进行访问。如图所示:
3、在 Java 中,继承的关键字用的是 “extends”,即子类不是父类的子集,而是对父类的 “扩展”
子类在继承父类以后,还可以定义自己特有的方法,这就可以看做是对父类功能上的扩展。
4、Java 支持多层继承 (继承体系)
class A{} | |
class B extends A{} | |
class C extends B{} |
说明:
子类和父类是一种相对的概念
顶层父类是 Object 类。所有的类默认继承 Object,作为父类。
5、一个父类可以同时拥有多个(直接)子类
一个子类只能拥有一个(直接)父类!
class A{} | |
class B extends A{} | |
class D extends A{} | |
class E extends A{} |
6、Java 只支持单继承,不支持多重继承
注意区分:多层继承 和 多重继承
public class A{} | |
class B extends A{} | |
// 一个类只能有一个父类,不可以有多个直接父类。 | |
class C extends B{} //ok | |
class C extends A,B... //error |
# 2.5 练习
** 练习 1:** 定义一个学生类 Student,它继承自 Person 类
练习 2:
(1) 定义一个 ManKind 类,包括
成员变量 int sex 和 int salary;
方法 void manOrWoman ():根据 sex 的值显示 “man”(sex1) 或者 “woman”(sex0);
方法 void employeed ():根据 salary 的值显示 “no job”(salary==0) 或者 “ job”(salary!=0)。
(2) 定义类 Kids 继承 ManKind,并包括
- 成员变量 int yearsOld;
- 方法 printAge () 打印 yearsOld 的值。
(3) 定义类 KidsTest,在类的 main 方法中实例化 Kids 的对象 someKid,用该对象访问其父类的成员变量及方法。
** 练习 3:** 根据下图实现类。在 CylinderTest 类中创建 Cylinder 类的对象,设置圆柱的底面半径和高,并输出圆柱的体积。
# 3. 方法的重写(override)
注意区分:重载 (overload) 和 重写 (override)
先来回顾一下 4 种权限修饰符:
修饰符 | 本类内部 | 本包内 | 其他包的子类 | 其他包非子类 |
---|---|---|---|---|
private | √ | × | × | × |
缺省 | √ | √ | × | × |
protected | √ | √ | √ | × |
public | √ | √ | √ | √ |
使用频率:public、private 最高
父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于自己当前的类,该怎么办呢?子类可以对从父类中继承来的方法进行改造,我们称为方法的 重写 (override、overwrite)
。也称为方法的 重置
、 覆盖
。
在程序执行时,子类的方法将覆盖父类的方法。
# 3.1 方法重写举例
比如新的手机增加来电显示头像的功能,代码如下:
package com.atguigu.inherited.method; | |
public class Phone { | |
public void sendMessage(){ | |
System.out.println("发短信"); | |
} | |
public void call(){ | |
System.out.println("打电话"); | |
} | |
public void showNum(){ | |
System.out.println("来电显示号码"); | |
} | |
} |
package com.atguigu.inherited.method; | |
//SmartPhone:智能手机 | |
public class SmartPhone extends Phone{ | |
// 重写父类的来电显示功能的方法 | |
@Override | |
public void showNum(){ | |
// 来电显示姓名和图片功能 | |
System.out.println("显示来电姓名"); | |
System.out.println("显示头像"); | |
} | |
// 重写父类的通话功能的方法 | |
@Override | |
public void call() { | |
System.out.println("语音通话 或 视频通话"); | |
} | |
} |
package com.atguigu.inherited.method; | |
public class TestOverride { | |
public static void main(String[] args) { | |
// 创建子类对象 | |
SmartPhone sp = new SmartPhone(); | |
// 调用子类重写的方法 | |
sp.call(); | |
// 调用子类重写的方法 | |
sp.showNum(); | |
} | |
} |
@Override
使用说明:写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。
# 3.2 重写的要求
【复习】方法声明的格式:
权限修饰符
返回值类型
方法名称
( 参数列表
) { 方法体
}
子类重写的方法必须和父类被重写的方法具有相同的
方法名称
、参数列表
。这样才能唯一确定要重写哪一个方法
子类重写的方法的
返回值类型
不大于父类被重写的方法的返回值类型。(例如:Student < Person)。
返回值类型
:
- void:子类必须也是 void
- 基本数据类型:子类必须也是基本数据类型
- 引用数据类型(比如类):子类重写方法的返回值类型不大于父类方法的返回值类型
- 子类重写的方法使用的
权限修饰符
不小于父类被重写的方法的访问权限。(public > protected > 缺省 > private)
如何理解:覆盖嘛,总不能盖不住父类吧
注意:
- 父类的 private 方法不能重写
- 跨包的父类缺省的方法也不能重写
- 子类方法
抛出的异常
不大于父类被重写方法的异常
技巧:如果不想区分这么仔细,都保持一样即可。
此外,子类与父类中同名同参数的方法必须同时声明为非 static 的 (即为重写),或者同时声明为 static 的(不是重写)。因为 static 方法是属于类的,子类无法覆盖父类的方法。
# 3.3 小结:方法的重载与重写
方法的重载: 同
一个类中,方法名相 同
,形参列表 不同
。不看返回值类型。
两同一不同
方法的重写:继承后,子类覆盖父类中的同名、同参数的方法。
(1)同一个类中
package com.atguigu.inherited.method; | |
public class TestOverload { | |
public int max(int a, int b){ | |
return a > b ? a : b; | |
} | |
public double max(double a, double b){ | |
return a > b ? a : b; | |
} | |
public int max(int a, int b,int c){ | |
return max(max(a,b),c); | |
} | |
} |
(2)父子类中
package com.atguigu.inherited.method; | |
public class TestOverloadOverride { | |
public static void main(String[] args) { | |
Son s = new Son(); | |
s.method(1);// 只有一个形式的 method 方法 | |
Daughter d = new Daughter(); | |
d.method(1); | |
d.method(1,2);// 有两个形式的 method 方法 | |
} | |
} | |
class Father{ | |
public void method(int i){ | |
System.out.println("Father.method"); | |
} | |
} | |
class Son extends Father{ | |
public void method(int i){// 重写 | |
System.out.println("Son.method"); | |
} | |
} | |
class Daughter extends Father{ | |
public void method(int i,int j){// 重载 | |
System.out.println("Daughter.method"); | |
} | |
} |
# 3.4 练习
** 练习 1:** 如果现在父类的一个方法定义成 private 访问权限,在子类中将此方法声明为 default 访问权限,那么这样还叫重写吗? (NO)
** 练习 2:** 修改继承内容的练习 2 中定义的类 Kids,在 Kids 中重新定义 employeed () 方法,覆盖父类 ManKind 中定义的 employeed () 方法,输出 “Kids should study and no job.”
# 4. 再谈封装性中的 4 种权限修饰
权限修饰符:public,protected, 缺省,private
修饰符 | 本类 | 本包 | 其他包子类 | 其他包非子类 |
---|---|---|---|---|
private | √ | × | × | × |
缺省 | √ | √(本包子类非子类都可见) | × | × |
protected | √ | √(本包子类非子类都可见) | √(其他包仅限于子类中可见) | × |
public | √ | √ | √ | √ |
外部类:public 和缺省
成员变量、成员方法等:public,protected, 缺省,private
1、外部类要跨包使用必须是 public,否则仅限于本包使用
(1)外部类的权限修饰符如果缺省,本包使用没问题
(2)外部类的权限修饰符如果缺省,跨包使用有问题
2、成员的权限修饰符问题
(1)本包下使用:成员的权限修饰符可以是 public、protected、缺省
(2)跨包下使用:要求严格
(3)跨包使用时,如果类的权限修饰符缺省,成员权限修饰符 > 类的权限修饰符也没有意义
# 5. 关键字:super
# 5.1 super 的理解
在 Java 类中使用 super
来调用父类中的指定操作:
- super 可用于访问父类中的成员变量
- super 可用于调用父类中的成员方法
- super 可用于在子类构造器中调用父类的构造器
注意:
- 尤其当子父类出现同名成员时,可以用 super 表明调用的是父类中的成员
- super 的追溯不仅限于直接父类
- super 和 this 的用法相像,
this
代表本类对象的引用,super
代表父类的内存空间的标识
# 5.2 super 的使用场景
# 5.2.1 子类中调用父类被重写的方法
- 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法;
- 如果子类重写了父类的方法,在子类中需要通过
super.
才能调用父类被重写的方法,否则默认调用的子类重写的方法
举例:
package com.atguigu.inherited.method; | |
public class Phone { | |
public void sendMessage(){ | |
System.out.println("发短信"); | |
} | |
public void call(){ | |
System.out.println("打电话"); | |
} | |
public void showNum(){ | |
System.out.println("来电显示号码"); | |
} | |
} | |
//smartphone:智能手机 | |
public class SmartPhone extends Phone{ | |
// 重写父类的来电显示功能的方法 | |
public void showNum(){ | |
// 来电显示姓名和图片功能 | |
System.out.println("显示来电姓名"); | |
System.out.println("显示头像"); | |
// 保留父类来电显示号码的功能 | |
super.showNum();// 此处必须加 super.,否则就是无限递归,那么就会栈内存溢出 | |
} | |
} |
总结:
方法前面没有 super. 和 this.(相当于省略了
this
)- 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
方法前面有 this.
- 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
方法前面有 super.
- 从当前子类的直接父类找,如果没有,继续往上追溯
# 5.2.2 子类中调用父类中同名的成员变量
- 如果实例变量与局部变量重名,可以在实例变量前面加 this. 进行区别
- 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加
super.
,否则默认访问的是子类自己声明的实例变量 - 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用 this. 实例访问,也可以用 super. 实例变量访问
举例:
class Father{ | |
int a = 10; | |
int b = 11; | |
} | |
class Son extends Father{ | |
int a = 20; | |
public void test(){ | |
// 子类与父类的属性同名,子类对象中就有两个 a | |
System.out.println("子类的a:" + a);//20 先找局部变量找,没有再从本类成员变量找 | |
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找 | |
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找 | |
// 子类与父类的属性不同名,是同一个 b | |
System.out.println("b = " + b);//11 先找局部变量找,没有再从本类成员变量找,没有再从父类找 | |
System.out.println("b = " + this.b);//11 先从本类成员变量找,没有再从父类找 | |
System.out.println("b = " + super.b);//11 直接从父类局部变量找 | |
} | |
public void method(int a, int b){ | |
// 子类与父类的属性同名,子类对象中就有两个成员变量 a,此时方法中还有一个局部变量 a | |
System.out.println("局部变量的a:" + a);//30 先找局部变量 | |
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找 | |
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找 | |
System.out.println("b = " + b);//13 先找局部变量 | |
System.out.println("b = " + this.b);//11 先从本类成员变量找 | |
System.out.println("b = " + super.b);//11 直接从父类局部变量找 | |
} | |
} | |
class Test{ | |
public static void main(String[] args){ | |
Son son = new Son(); | |
son.test(); | |
son.method(30,13); | |
} | |
} |
总结:起点不同(就近原则)
变量前面没有
super.
和this.
- 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的
局部变量
, - 如果不是局部变量,先从当前执行代码的
本类去找成员变量
- 如果从当前执行代码的本类中没有找到,会往上找
父类声明的成员变量
(权限修饰符允许在子类中访问的)
- 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的
变量前面有 this.
- 通过 this 找成员变量时,先从当前执行代码的本类去找成员变量
- 如果从当前执行代码的本类中没有找到,会往上找 == 父类声明的成员变量(== 权限修饰符允许在子类中访问的)
变量前面 super.
- 通过 super 找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
- 如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)
特别说明:应该避免子类声明和父类重名的成员变量
在阿里的开发规范等文档中都做出明确说明:
# 5.2.3 子类构造器中调用父类构造器
① 子类继承父类时,不会继承父类的构造器。只能通过 super(形参列表)
的方式调用父类指定的构造器。
② 规定: super(形参列表)
,必须声明在构造器的首行。
③ 我们前面讲过,在构造器的首行可以使用 "this (形参列表)",调用本类中重载的构造器,
结合②,结论:
在构造器的首行, this(形参列表)
和 super(形参列表)
只能二选一。
④ 如果在子类构造器的首行既没有显示调用 this(形参列表)
,也没有显式调用 super(形参列表)
,子类此构造器默认调用 super()
,即调用父类中空参的构造器。
⑤ 由③和④得到结论:
子类的任何一个构造器中,
- 要么会调用本类中重载的构造器;
- 要么会调用父类的构造器;
⑥ 由⑤得到:一个类中声明有 n 个构造器,最多有 n-1 个构造器中使用了 this(形参列表)
,则剩下的那个一定使用 super(形参列表)
。
开发中常见错误:
如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则
编译出错
。
情景举例 1:
class A{ | |
} | |
class B extends A{ | |
} | |
class Test{ | |
public static void main(String[] args){ | |
B b = new B(); | |
//A 类和 B 类都是默认有一个无参构造,B 类的默认无参构造中还会默认调用 A 类的默认无参构造 | |
// 但是因为都是默认的,没有打印语句,看不出来 | |
} | |
} |
情景举例 2:
class A{ | |
A(){ | |
System.out.println("A类无参构造器"); | |
} | |
} | |
class B extends A{ | |
} | |
class Test{ | |
public static void main(String[] args){ | |
B b = new B(); | |
//A 类显示声明一个无参构造, | |
//B 类默认有一个无参构造, | |
//B 类的默认无参构造中会默认调用 A 类的无参构造 | |
// 可以看到会输出 “A 类无参构造器 " | |
} | |
} |
情景举例 3:
class A{ | |
A(){ | |
System.out.println("A类无参构造器"); | |
} | |
} | |
class B extends A{ | |
B(){ | |
System.out.println("B类无参构造器"); | |
} | |
} | |
class Test{ | |
public static void main(String[] args){ | |
B b = new B(); | |
//A 类显示声明一个无参构造, | |
//B 类显示声明一个无参构造, | |
//B 类的无参构造中虽然没有写 super (),但是仍然会默认调用 A 类的无参构造 | |
// 可以看到会输出 “A 类无参构造器 "和"B 类无参构造器 ") | |
} | |
} |
情景举例 4:
class A{ | |
A(){ | |
System.out.println("A类无参构造器"); | |
} | |
} | |
class B extends A{ | |
B(){ | |
super(); | |
System.out.println("B类无参构造器"); | |
} | |
} | |
class Test{ | |
public static void main(String[] args){ | |
B b = new B(); | |
//A 类显示声明一个无参构造, | |
//B 类显示声明一个无参构造, | |
//B 类的无参构造中明确写了 super (),表示调用 A 类的无参构造 | |
// 可以看到会输出 “A 类无参构造器 "和"B 类无参构造器 ") | |
} | |
} |
情景举例 5:
class A{ | |
A(int a){ | |
System.out.println("A类有参构造器"); | |
} | |
} | |
class B extends A{ | |
B(){ | |
System.out.println("B类无参构造器"); | |
} | |
} | |
class Test05{ | |
public static void main(String[] args){ | |
B b = new B(); | |
//A 类显示声明一个有参构造,没有写无参构造,那么 A 类就没有无参构造了 | |
//B 类显示声明一个无参构造, | |
//B 类的无参构造没有写 super (...),表示默认调用 A 类的无参构造 | |
// 编译报错,因为 A 类没有无参构造 | |
} | |
} |
情景举例 6:
class A{ | |
A(int a){ | |
System.out.println("A类有参构造器"); | |
} | |
} | |
class B extends A{ | |
B(){ | |
super(); | |
System.out.println("B类无参构造器"); | |
} | |
} | |
class Test06{ | |
public static void main(String[] args){ | |
B b = new B(); | |
//A 类显示声明一个有参构造,没有写无参构造,那么 A 类就没有无参构造了 | |
//B 类显示声明一个无参构造, | |
//B 类的无参构造明确写 super (),表示调用 A 类的无参构造 | |
// 编译报错,因为 A 类没有无参构造 | |
} | |
} |
情景举例 7:
class A{ | |
A(int a){ | |
System.out.println("A类有参构造器"); | |
} | |
} | |
class B extends A{ | |
B(int a){ | |
super(a); | |
System.out.println("B类有参构造器"); | |
} | |
} | |
class Test07{ | |
public static void main(String[] args){ | |
B b = new B(10); | |
//A 类显示声明一个有参构造,没有写无参构造,那么 A 类就没有无参构造了 | |
//B 类显示声明一个有参构造, | |
//B 类的有参构造明确写 super (a),表示调用 A 类的有参构造 | |
// 会打印 “A 类有参构造器 "和"B 类有参构造器 " | |
} | |
} |
情景举例 8:
class A{ | |
A(){ | |
System.out.println("A类无参构造器"); | |
} | |
A(int a){ | |
System.out.println("A类有参构造器"); | |
} | |
} | |
class B extends A{ | |
B(){ | |
super();// 可以省略,调用父类的无参构造 | |
System.out.println("B类无参构造器"); | |
} | |
B(int a){ | |
super(a);// 调用父类有参构造 | |
System.out.println("B类有参构造器"); | |
} | |
} | |
class Test8{ | |
public static void main(String[] args){ | |
B b1 = new B(); | |
B b2 = new B(10); | |
} | |
} |
# 5.3 小结:this 与 super
1、this 和 super 的意义
this:当前对象
- 在构造器和非静态代码块中,表示正在 new 的对象
- 在实例方法中,表示调用当前方法的对象
super:引用父类声明的成员
2、this 和 super 的使用格式
- this
- this. 成员变量:表示当前对象的某个成员变量,而不是局部变量
- this. 成员方法:表示当前对象的某个成员方法,完全可以省略 this.
- this () 或 this (实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
- super
- super. 成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
- super. 成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
- super () 或 super (实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错
# 5.4 练习
** 练习 1:** 修改方法重写的练习 2 中定义的类 Kids 中 employeed () 方法,在该方法中调用父类 ManKind 的 employeed () 方法,然后再输出 “but Kids should study and no job.”
** 练习 2:** 修改继承中的练习 3 中定义的 Cylinder 类,在 Cylinder 类中覆盖 findArea () 方法,计算圆柱的表面积。考虑:findVolume 方法怎样做相应的修改?
在 CylinderTest 类中创建 Cylinder 类的对象,设置圆柱的底面半径和高,并输出圆柱的表面积和体积。
附加题:在 CylinderTest 类中创建一个 Circle 类的对象,设置圆的半径,计算输出圆的面积。体会父类和子类成员的分别调用。
练习 3:
1、写一个名为 Account 的类模拟账户。该类的属性和方法如下图所示。该类包括的属性:账号 id,余额 balance,年利率 annualInterestRate;包含的方法:访问器方法(getter 和 setter 方法),返回月利率的方法 getMonthlyInterest (),取款方法 withdraw (),存款方法 deposit ()。
写一个用户程序测试 Account 类。在用户程序中,创建一个账号为 1122、余额为 20000、年利率 4.5% 的 Account 对象。使用 withdraw 方法提款 30000 元,并打印余额。
再使用 withdraw 方法提款 2500 元,使用 deposit 方法存款 3000 元,然后打印余额和月利率。
提示:在提款方法 withdraw 中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。
运行结果如图所示:
2、创建 Account 类的一个子类 CheckAccount 代表可透支的账户,该账户中定义一个属性 overdraft 代表可透支限额。在 CheckAccount 类中重写 withdraw 方法,其算法如下:
如果(取款金额<账户余额),
可直接取款
如果(取款金额>账户余额),
计算需要透支的额度
判断可透支额overdraft是否足够支付本次透支需要,如果可以
将账户余额修改为0,冲减可透支金额
如果不可以
提示用户超过可透支额的限额
要求:写一个用户程序测试 CheckAccount 类。在用户程序中,创建一个账号为 1122、余额为 20000、年利率 4.5%,可透支限额为 5000 元的 CheckAccount 对象。
使用 withdraw 方法提款 5000 元,并打印账户余额和可透支额。
再使用 withdraw 方法提款 18000 元,并打印账户余额和可透支额。
再使用 withdraw 方法提款 3000 元,并打印账户余额和可透支额。
提示:
(1)子类 CheckAccount 的构造方法需要将从父类继承的 3 个属性和子类自己的属性全部初始化。
(2)父类 Account 的属性 balance 被设置为 private,但在子类 CheckAccount 的 withdraw 方法中需要修改它的值,因此应修改父类的 balance 属性,定义其为 protected。
运行结果如下图所示:
# 6. 子类对象实例化全过程
Dog dog = new Dog("小花","小红"); |
举例:
class Creature { | |
public Creature() { | |
System.out.println("Creature无参数的构造器"); | |
} | |
} | |
class Animal extends Creature { | |
public Animal(String name) { | |
System.out.println("Animal带一个参数的构造器,该动物的name为" + name); | |
} | |
public Animal(String name, int age) { | |
this(name); | |
System.out.println("Animal带两个参数的构造器,其age为" + age); | |
} | |
} | |
public class Dog extends Animal { | |
public Dog() { | |
super("汪汪队阿奇", 3); | |
System.out.println("Dog无参数的构造器"); | |
} | |
public static void main(String[] args) { | |
new Dog(); | |
} | |
} |
# 7. 面向对象特征三:多态性(Polymorphism)
一千个读者眼中有一千个哈姆雷特。
# 7.1 多态的形式和体现
# 7.1.1 对象的多态性
多态性,是面向对象中最重要的概念,在 Java 中的体现:对象的多态性:父类的引用指向子类的对象。
格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)
父类类型 变量名 = 子类对象; |
举例:
Person p = new Student(); | |
Object o = new Person();//Object 类型的变量 o,指向 Person 类型的对象 | |
o = new Student(); //Object 类型的变量 o,指向 Student 类型的对象 |
对象的多态:在 Java 中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向 (引用) 多种不同类型的对象
# 7.1.2 多态的理解
Java 引用变量有两个类型:
编译时类型:由 声明
该变量时使用的类型决定;
运行时类型:由 实际赋给该变量的对象
决定;
简称:编译时,看左边;运行时,看右边。
- 若
编译时类型
和运行时类型
不一致,就出现了对象的多态性 - 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
“看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
使用前提:① 类的继承关系 ② 方法的重写
# 7.1.3 举例
package com.atguigu.polymorphism.grammar; | |
public class Pet { | |
private String nickname; // 昵称 | |
public String getNickname() { | |
return nickname; | |
} | |
public void setNickname(String nickname) { | |
this.nickname = nickname; | |
} | |
public void eat(){ | |
System.out.println(nickname + "吃东西"); | |
} | |
} |
package com.atguigu.polymorphism.grammar; | |
public class Cat extends Pet { | |
// 子类重写父类的方法 | |
@Override | |
public void eat() { | |
System.out.println("猫咪" + getNickname() + "吃鱼仔"); | |
} | |
// 子类扩展的方法 | |
public void catchMouse() { | |
System.out.println("抓老鼠"); | |
} | |
} |
package com.atguigu.polymorphism.grammar; | |
public class Dog extends Pet { | |
// 子类重写父类的方法 | |
@Override | |
public void eat() { | |
System.out.println("狗子" + getNickname() + "啃骨头"); | |
} | |
// 子类扩展的方法 | |
public void watchHouse() { | |
System.out.println("看家"); | |
} | |
} |
1、方法内局部变量的赋值体现多态
package com.atguigu.polymorphism.grammar; | |
public class TestPet { | |
public static void main(String[] args) { | |
// 多态引用 | |
Pet pet = new Dog(); | |
pet.setNickname("小白"); | |
// 多态的表现形式 | |
/* | |
编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法; | |
运行时,看 “子类”,如果子类重写了方法,一定是执行子类重写的方法体; | |
*/ | |
pet.eat();// 运行时执行子类 Dog 重写的方法 | |
// pet.watchHouse ();// 不能调用 Dog 子类扩展的方法 | |
pet = new Cat(); | |
pet.setNickname("雪球"); | |
pet.eat();// 运行时执行子类 Cat 重写的方法 | |
} | |
} |
2、方法的形参声明体现多态
package com.atguigu.polymorphism.grammar; | |
public class Person{ | |
private Pet pet; | |
public void adopt(Pet pet) {// 形参是父类类型,实参是子类对象 | |
this.pet = pet; | |
} | |
public void feed(){ | |
pet.eat();//pet 实际引用的对象类型不同,执行的 eat 方法也不同 | |
} | |
} |
package com.atguigu.polymorphism.grammar; | |
public class TestPerson { | |
public static void main(String[] args) { | |
Person person = new Person(); | |
Dog dog = new Dog(); | |
dog.setNickname("小白"); | |
person.adopt(dog);// 实参是 dog 子类对象,形参是父类 Pet 类型 | |
person.feed(); | |
Cat cat = new Cat(); | |
cat.setNickname("雪球"); | |
person.adopt(cat);// 实参是 cat 子类对象,形参是父类 Pet 类型 | |
person.feed(); | |
} | |
} |
3、方法返回值类型体现多态
package com.atguigu.polymorphism.grammar; | |
public class PetShop { | |
// 返回值类型是父类类型,实际返回的是子类对象 | |
public Pet sale(String type){ | |
switch (type){ | |
case "Dog": | |
return new Dog(); | |
case "Cat": | |
return new Cat(); | |
} | |
return null; | |
} | |
} |
package com.atguigu.polymorphism.grammar; | |
public class TestPetShop { | |
public static void main(String[] args) { | |
PetShop shop = new PetShop(); | |
Pet dog = shop.sale("Dog"); | |
dog.setNickname("小白"); | |
dog.eat(); | |
Pet cat = shop.sale("Cat"); | |
cat.setNickname("雪球"); | |
cat.eat(); | |
} | |
} |
# 7.2 为什么需要?
开发中,有时我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。
案例:
(1)声明一个 Dog 类,包含 public void eat()
方法,输出 “狗啃骨头”
(2)声明一个 Cat 类,包含 public void eat()
方法,输出 “猫吃鱼仔”
(3)声明一个 Person 类,功能如下:
- 包含宠物属性
- 包含领养宠物方法
public void adopt(宠物类型Pet)
- 包含喂宠物吃东西的方法
public void feed()
,实现为调用宠物对象.eat()
方法
public class Dog { | |
public void eat(){ | |
System.out.println("狗啃骨头"); | |
} | |
} |
public class Cat { | |
public void eat(){ | |
System.out.println("猫吃鱼仔"); | |
} | |
} |
public class Person { | |
private Dog dog; | |
//adopt:领养 | |
public void adopt(Dog dog){ | |
this.dog = dog; | |
} | |
//feed:喂食 | |
public void feed(){ | |
if(dog != null){ | |
dog.eat(); | |
} | |
} | |
/* | |
问题: | |
1、从养狗切换到养猫怎么办? | |
修改代码把 Dog 修改为养猫? | |
2、或者有的人养狗,有的人养猫怎么办? | |
3、要是还有更多其他宠物类型怎么办? | |
如果 Java 不支持多态,那么上面的问题将会非常麻烦,代码维护起来很难,扩展性很差。 | |
*/ | |
} |
# 7.3 多态的好处和弊端
好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
减少代码冗余,不需要定义多个重载的方法。
举例:
class Account{ public void withdraw(){} // 取钱}
class CheckAccount extends Account{ // 信用卡// 重写方法
public void withdraw(){}}
class SavingAccount extends Account{ // 储蓄卡// 重写方法
public void withdraw(){}}
class Customer{ Account account; public void setAccount(Account account){ // 多态的好处:形参只需声明为父类类型,稍后可以传各种各样的子类实例对象进来 this.account = account;}
public Account getAccount(){ return account;}
}
class CustomerTest{ public static void main(String[] args){ Customer cust = new Customer(); cust.setAccount(new CheckAccount()); cust.getAccount().withdraw(); // 调用的是信用卡里的方法}
}
弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中自定义的属性和方法。
其实也不算弊端,挺合理的。
解决方法:向下(强制)转型
Student m = new Student(); | |
m.school = "pku"; // 合法,Student 类有 school 成员变量 | |
Person e = new Student(); | |
e.school = "pku"; // 非法,Person 类没有 school 成员变量 | |
// 属性是在编译时确定的,编译时 e 为 Person 类型,没有 school 成员变量,因而编译错误。 |
开发中:
使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。
【开闭原则 OCP】
- 对扩展开放,对修改关闭
- 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能
# 7.4 虚方法调用 (Virtual Method Invocation)
在 Java 中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
Person e = new Student(); | |
e.getInfo(); // 调用 Student 类的 getInfo () 方法 |
- 子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法。
- 父类根据赋给它的不同子类对象,动态调用属于子类的该方法。
- 这样的方法调用在编译期是无法确定的。
举例:
前提:Person 类中定义了 welcome () 方法,各个子类重写了 welcome ()。
执行:多态的情况下,调用对象的 welcome () 方法,实际执行的是子类重写的方法。
拓展:
静态链接(或早起绑定)
:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。那么调用这样的方法,就称为非虚方法调用。比如调用静态方法、私有方法、final 方法、父类构造器、本类重载构造器等。
动态链接(或晚期绑定)
:如果被调用的方法在编译期无法被确定,只能在运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。调用这样的方法,就称为虚方法调用。比如调用重写的方法(针对父类)、实现的方法(针对接口)。
# 7.5 成员变量没有多态性
若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。
对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量。
public class TestVariable { | |
public static void main(String[] args) { | |
Base b = new Sub(); | |
System.out.println(b.a); // 1 | |
System.out.println(((Sub) b).a); // ? = 2 | |
Sub s = new Sub(); | |
System.out.println(s.a); // 2 | |
System.out.println(((Base) s).a); // ? = 1 | |
} | |
} | |
class Base { | |
int a = 1; | |
} | |
class Sub extends Base { | |
int a = 2; | |
} |
# 7.6 向上转型与向下转型
首先,一个对象在 new 的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型永远不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。
# 7.6.1 为什么要类型转换
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在 编译期间
,就会出现类型转换的现象。
但是,使用父类变量接收了子类对象之后,我们就 不能调用
子类拥有,而父类没有的方法了。这也是多态给我们带来的一点 "小麻烦"。
所以,想要调用子类特有的方法,必须做类型转换,使得 编译通过
。
向上转型:当左边的变量的类型(父类) > 右边对象 / 变量的类型(子类),我们就称为向上转型
- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
- 此时,一定是安全的,而且也是自动完成的
向下转型:当左边的变量的类型(子类)< 右边对象 / 变量的编译时类型(父类),我们就称为向下转型
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 不是所有通过编译的向下转型都是正确的,可能会发生
ClassCastException
,为了安全,可以通过isInstanceof
关键字进行判断
# 7.6.2 如何向上或向下转型
向上转型:自动完成
向下转型:(子类类型)父类变量
在向下转型之前,使用关键字
instanceof
进行判断,避免出现ClassCastException
异常。
public class ClassCastTest { | |
public static void main(String[] args) { | |
// 没有类型转换 | |
Dog dog = new Dog();//dog 的编译时类型和运行时类型都是 Dog | |
// 向上转型(自动) | |
Pet pet = new Dog();//pet 的编译时类型是 Pet,运行时类型是 Dog | |
pet.setNickname("小白"); // 可以调用父类 Pet 有声明的方法 setNickname | |
pet.eat();// 可以调用父类 Pet 有声明的方法 eat,但实际执行的是子类重写的 eat 方法体 | |
// pet.watchHouse ();// 不能调用父类没有的方法 watchHouse | |
// 向下转型(强制):解决多态的局限性 | |
Dog d = (Dog) pet; | |
System.out.println("d.nickname = " + d.getNickname()); | |
d.eat();// 可以调用 eat 方法 | |
d.watchHouse();// 可以调用子类扩展的方法 watchHouse | |
// 向下转型可能会出现 ClassCastException 异常 | |
Cat c = (Cat) pet;// 编译通过,因为从语法检查来说,pet 的编译时类型是 Pet,Cat 是 Pet 的子类,所以向下转型语法正确 | |
// 这句代码运行报错 ClassCastException,因为 pet 变量的运行时类型是 Dog,Dog 和 Cat 之间是没有继承关系的 | |
} | |
} |
# 7.6.3 instanceof 关键字
为了避免 ClassCastException
的发生,Java 提供了 instanceof
关键字,给引用变量做类型的校验。如下代码格式:
// 检验对象 a 是否是数据类型 A 的对象,返回值为 boolean 型 | |
对象a instanceof 数据类型A |
- 说明:
- 只要用
instanceof
判断返回 true 的,那么强转为该类型就一定是安全的,不会报ClassCastException
异常。 - 如果对象 a 属于类 A 的子类 B,
a instanceof A
值也为 true。 - 要求对象 a 所属的类与类 A 必须是子类和父类的关系,否则编译错误。
- 只要用
代码:
package com.atguigu.polymorphism.grammar; | |
public class TestInstanceof { | |
public static void main(String[] args) { | |
Pet[] pets = new Pet[2]; | |
pets[0] = new Dog();// 多态引用 | |
pets[0].setNickname("小白"); | |
pets[1] = new Cat();// 多态引用 | |
pets[1].setNickname("雪球"); | |
for (int i = 0; i < pets.length; i++) { | |
pets[i].eat(); | |
if(pets[i] instanceof Dog){ | |
Dog dog = (Dog) pets[i]; | |
dog.watchHouse(); | |
}else if(pets[i] instanceof Cat){ | |
Cat cat = (Cat) pets[i]; | |
cat.catchMouse(); | |
} | |
} | |
} | |
} |
# 7.7 练习
练习 1:定义三个类,父类 GeometricObject 代表几何形状,子类 Circle 代表圆形,MyRectangle 代表矩形。定义一个测试类 GeometricTest,编写 equalsArea 方法测试两个对象的面积是否相等(注意方法的参数类型,利用动态绑定技术),编写 displayGeometricObject 方法显示对象的面积(注意方法的参数类型,利用动态绑定技术)。
练习 2:
建立 InstanceTest 类,在类中定义方法 method (Person e);
在 method 中:
(1) 根据 e 的类型调用相应类的 getInfo () 方法。
(2) 根据 e 的类型执行:
如果 e 为 Person 类的对象,输出:
“a person”;
如果 e 为 Student 类的对象,输出:
“a student”
“a person ”
如果 e 为 Graduate 类的对象,输出:
“a graduated student”
“a student”
“a person”
class Person { | |
protected String name = "person"; | |
protected int age = 50; | |
public String getInfo() { | |
return "Name: " + name + "\n" + "age: " + age; | |
} | |
} | |
class Student extends Person { | |
protected String school = "pku"; | |
@Override | |
public String getInfo() { | |
return "Name: " + name + "\nage: " + age | |
+ "\nschool: " + school; | |
} | |
} | |
class Graduate extends Student { | |
public String major = "IT"; | |
@Override | |
public String getInfo() { | |
return "Name: " + name + "\nage: " + age | |
+ "\nschool: " + school + "\nmajor:" + major; | |
} | |
} | |
public class InstanceTest { | |
public static void main(String[] args) { | |
InstanceTest test = new InstanceTest(); | |
test.method(new Graduate()); | |
} | |
public void method(Person e) { | |
System.out.println(e.getInfo()); | |
if (e instanceof Graduate) { | |
System.out.println("a graduate student"); | |
} | |
if (e instanceof Student) { | |
System.out.println("a student"); | |
} | |
if (e instanceof Person) { | |
System.out.println("a person"); | |
} | |
} | |
} |
练习 3:笔试 & 面试
题目 1:继承成员变量和继承方法的区别
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); | |
} | |
} | |
public class FieldMethodTest { | |
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); | |
System.out.println(b.count); | |
b.display(); | |
} | |
} |
题目 2:
// 考查多态的笔试题目: | |
public class InterviewTest1 { | |
public static void main(String[] args) { | |
Base base = new Sub(); | |
base.add(1, 2, 3); | |
// Sub s = (Sub)base; | |
// s.add(1,2,3); | |
} | |
} | |
class Base { | |
public void add(int a, int... arr) { | |
System.out.println("base"); | |
} | |
} | |
class Sub extends Base { | |
public void add(int a, int[] arr) { | |
System.out.println("sub_1"); | |
} | |
// public void add(int a, int b, int c) { | |
// System.out.println("sub_2"); | |
// } | |
} |
题目 3:
//getXxx () 和 setXxx () 声明在哪个类中,内部操作的属性就是哪个类里的。 | |
public class InterviewTest2 { | |
public static void main(String[] args) { | |
Father f = new Father(); | |
Son s = new Son(); | |
System.out.println(f.getInfo());//atguigu | |
System.out.println(s.getInfo());// 尚硅谷 | |
s.test();// 尚硅谷 atguigu | |
System.out.println("-----------------"); | |
s.setInfo("大硅谷"); | |
System.out.println(f.getInfo());//atguigu | |
System.out.println(s.getInfo());// 大硅谷 | |
s.test();// 大硅谷 atguigu | |
} | |
} | |
class Father { | |
private String info = "atguigu"; | |
public void setInfo(String info) { | |
this.info = info; | |
} | |
public String getInfo() { | |
return info; | |
} | |
} | |
class Son extends Father { | |
private String info = "尚硅谷"; | |
public void setInfo(String info) { | |
this.info = info; | |
} | |
public String getInfo() { | |
return info; | |
} | |
public void test() { | |
System.out.println(this.getInfo()); | |
System.out.println(super.getInfo()); | |
} | |
} |
题目 4:多态是编译时行为还是运行时行为?
// 证明如下: | |
class Animal { | |
protected void eat() { | |
System.out.println("animal eat food"); | |
} | |
} | |
class Cat extends Animal { | |
protected void eat() { | |
System.out.println("cat eat fish"); | |
} | |
} | |
class Dog extends Animal { | |
public void eat() { | |
System.out.println("Dog eat bone"); | |
} | |
} | |
class Sheep extends Animal { | |
public void eat() { | |
System.out.println("Sheep eat grass"); | |
} | |
} | |
public class InterviewTest { | |
public static Animal getInstance(int key) { | |
switch (key) { | |
case 0: | |
return new Cat (); | |
case 1: | |
return new Dog (); | |
default: | |
return new Sheep (); | |
} | |
} | |
public static void main(String[] args) { | |
int key = new Random().nextInt(3); | |
System.out.println(key); | |
Animal animal = getInstance(key); | |
animal.eat(); | |
} | |
} |
# 8. Object 类的使用
# 8.1 如何理解根父类
类 java.lang.Object
是类层次结构的根类,即所有其它类的父类。每个类都使用 Object
作为超类。
Object 类型的变量与除 Object 以外的任意引用数据类型的对象都存在多态引用
method(Object obj){…} // 可以接收任何类作为其参数
Person o = new Person();
method(o);
所有对象(包括数组)都实现这个类的方法。
如果一个类没有特别指定父类,那么默认则继承自 Object 类。例如:
public class Person {
...
}
// 等价于:
public class Person extends Object {
...
}
# 8.2 Object 类的方法
根据 JDK 源代码及 Object 类的 API 文档,Object 类当中包含的方法有 11 个。这里我们主要关注其中的 6 个:
# 1、(重点)equals()
= =:
基本类型比较值:只要两个变量的值相等,即为 true。
int a=5;
if(a==6){…}
引用类型比较引用 (是否指向同一个对象):只有指向同一个对象时,== 才返回 true。
Person p1=new Person();
Person p2=new Person();
if (p1==p2){…}
- 用 “==” 进行比较时,符号两边的
数据类型必须兼容
(可自动转换的基本数据类型除外),否则编译出错
- 用 “==” 进行比较时,符号两边的
**equals():** 所有类都继承了 Object,也就获得了 equals () 方法。还可以重写。
只能比较引用类型,Object 类源码中 equals () 的作用与 “==” 相同:比较是否指向同一个对象。
格式:obj1.equals (obj2)
特例:当用 equals () 方法进行比较时,对类 File、String、Date 及包装类(Wrapper Class)来说,是比较类型及内容而不考虑引用的是否是同一个对象;
- 原因:在这些类中重写了 Object 类的 equals () 方法。
当自定义使用 equals () 时,可以重写。用于比较两个对象的 “内容” 是否都相等
重写 equals () 方法的原则
对称性
:如果 x.equals (y) 返回是 “true”,那么 y.equals (x) 也应该返回是 “true”。自反性
:x.equals (x) 必须返回是 “true”。传递性
:如果 x.equals (y) 返回是 “true”,而且 y.equals (z) 返回是 “true”,那么 z.equals (x) 也应该返回是 “true”。一致性
:如果 x.equals (y) 返回是 “true”,只要 x 和 y 内容一直不变,不管你重复 x.equals (y) 多少次,返回都是 “true”。任何情况下,
x.equals(null)
,永远返回是 “false”;x.equals(和x不同类型的对象)
永远返回是 “false”。
重写举例:
class User{ | |
private String host; | |
private String username; | |
private String password; | |
public User(String host, String username, String password) { | |
super(); | |
this.host = host; | |
this.username = username; | |
this.password = password; | |
} | |
public User() { | |
super(); | |
} | |
public String getHost() { | |
return host; | |
} | |
public void setHost(String host) { | |
this.host = host; | |
} | |
public String getUsername() { | |
return username; | |
} | |
public void setUsername(String username) { | |
this.username = username; | |
} | |
public String getPassword() { | |
return password; | |
} | |
public void setPassword(String password) { | |
this.password = password; | |
} | |
@Override | |
public String toString() { | |
return "User [host=" + host + ", username=" + username + ", password=" + password + "]"; | |
} | |
@Override | |
public boolean equals(Object obj) { | |
if (this == obj) // 自反性 | |
return true; | |
if (obj == null) //x.equals (null),永远返回是 “false” | |
return false; | |
if (getClass() != obj.getClass()) //x.equals (和 x 不同类型的对象) 永远返回是 “false” | |
return false; | |
User other = (User) obj; | |
if (host == null) { | |
if (other.host != null) | |
return false; | |
} else if (!host.equals(other.host)) | |
return false; | |
if (password == null) { | |
if (other.password != null) | |
return false; | |
} else if (!password.equals(other.password)) | |
return false; | |
if (username == null) { | |
if (other.username != null) | |
return false; | |
} else if (!username.equals(other.username)) | |
return false; | |
return true; | |
} | |
} |
** 面试题:** 和 equals 的区别
从我面试的反馈,85% 的求职者 “理直气壮” 的回答错误…
==
:既可以比较基本类型也可以比较引用类型。对于基本数据类型就是比较值,对于引用数据类型就是比较内存地址;equals
:只能比较引用数据类型,判断内存地址是否相同;
它是属于
java.lang.Object
类里面的方法,如果该方法没有被重写过默认也是==
,即比较内存地址;因此,通常情况下,重写 equals 方法,会比较类中的相应属性是否都相等。不然就没有重写的必要了。
我们可以看到String 等类的
equals
方法是被重写过的,因此是比较值的,而且 String 类在日常开发中用的比较多,久而久之,形成了 equals 是比较值的错误观点。
具体要看自定义类里有没有重写 Object 的 equals 方法来判断。
练习 0:
public class Account { | |
private int balance; | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) { | |
return true; | |
} | |
if (!(o instanceof Account)) { | |
return false; | |
} | |
Account account = (Account) o; | |
return getBalance() == account.getBalance(); | |
} | |
public Account() { | |
} | |
public Account(int balance) { | |
this.balance = balance; | |
} | |
public int getBalance() { | |
return balance; | |
} | |
public void setBalance(int balance) { | |
this.balance = balance; | |
} | |
} |
public class Customer { | |
private String name; | |
private int age; | |
private Account account; | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) { | |
return true; | |
} | |
if (!(o instanceof Customer)) { | |
return false; | |
} | |
Customer customer = (Customer) o; | |
return getAge() == customer.getAge() && Objects.equals(getName(), customer.getName()) && Objects.equals(getAccount(), customer.getAccount()); | |
} | |
public Customer() { | |
} | |
public Customer(String name, int age, Account account) { | |
this.name = name; | |
this.age = age; | |
this.account = account; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public int getAge() { | |
return age; | |
} | |
public void setAge(int age) { | |
this.age = age; | |
} | |
public Account getAccount() { | |
return account; | |
} | |
public void setAccount(Account account) { | |
this.account = account; | |
} | |
} |
public class CustomerTest { | |
public static void main(String[] args) { | |
/* 判断两个 Customer 对象是否 equals (), | |
* 除了 Customer 类需要重写 equals () 方法外, | |
* 其内部的类类型的属性所在的类(Account 类)也需要重写 equals () 方法, | |
* 否则两个 Customer 对象的 equals () 方法返回 false */ | |
Customer c1 = new Customer("Tom", 21, new Account(1000)); | |
Customer c2 = new Customer("Tom", 21, new Account(1000)); | |
System.out.println("c1和c2是否相等:" + c1.equals(c2)); | |
} | |
} |
练习 1:
int it = 65; | |
float fl = 65.0f; | |
System.out.println(“65和65.0f是否相等?” + (it == fl)); // | |
char ch1 = 'A'; char ch2 = 12; | |
System.out.println("65和'A'是否相等?" + (it == ch1));// | |
System.out.println("12和ch2是否相等?" + (12 == ch2));// | |
String str1 = new String("hello"); | |
String str2 = new String("hello"); | |
System.out.println("str1和str2是否相等?"+ (str1 == str2));// | |
System.out.println("str1是否equals str2?"+(str1.equals(str2)));// | |
System.out.println(“hello” == new java.util.Date()); // |
练习 2:
编写 Order 类,有 int 型的 orderId,String 型的 orderName,相应的 getter () 和 setter () 方法,两个参数的构造器,重写父类的 equals () 方法:public boolean equals (Object obj),并判断测试类中创建的两个对象是否相等。
练习 3:
请根据以下代码自行定义能满足需要的 MyDate 类,在 MyDate 类中覆盖 equals 方法,使其判断当两个 MyDate 类型对象的年月日都相同时,结果为 true,否则为 false。 public boolean equals (Object o)
public class EqualsTest { | |
public static void main(String[] args) { | |
MyDate m1 = new MyDate(14, 3, 1976); | |
MyDate m2 = new MyDate(14, 3, 1976); | |
if (m1 == m2) { | |
System.out.println("m1==m2"); | |
} else { | |
System.out.println("m1!=m2"); // m1 != m2 | |
} | |
if (m1.equals(m2)) { | |
System.out.println("m1 is equal to m2");// m1 is equal to m2 | |
} else { | |
System.out.println("m1 is not equal to m2"); | |
} | |
} | |
} |
# 2、(重点)toString()
方法签名:public String toString ()
① 默认情况下,toString ()返回的是 对象的运行时类型 @ 对象的hashCode值的十六进制形式
② 在进行 String 与其它类型数据的连接操作时,自动调用 toString () 方法
Date now=new Date(); | |
System.out.println(“now=”+now); // 相当于 | |
System.out.println(“now=”+now.toString()); |
③ 如果我们直接 sout (对象),默认会自动调用这个对象的 toString ()
因为 Java 的引用数据类型的变量中存储的实际上是对象的内存地址,但是 Java 对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM 帮你调用了对象的 toString ()。
④ 可以根据需要在用户自定义类型中重写 toString () 方法
如String 类重写了 toString () 方法,返回字符串的值。
s1="hello"; | |
System.out.println(s1);// 相当于 System.out.println (s1.toString ()); |
例如自定义的 Person 类:
public class Person { | |
private String name; | |
private int age; | |
@Override | |
public String toString() { | |
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; | |
} | |
} |
练习:定义两个类,父类 GeometricObject 代表几何形状,子类 Circle 代表圆形。
# 3、clone()
创建并返回当前对象的副本。
//Object 类的 clone () 的使用 | |
public class CloneTest { | |
public static void main(String[] args) { | |
Animal a1 = new Animal("花花"); | |
try { | |
Animal a2 = (Animal) a1.clone(); | |
System.out.println("原始对象:" + a1); | |
a2.setName("毛毛"); | |
System.out.println("clone之后的对象:" + a2); | |
} catch (CloneNotSupportedException e) { | |
e.printStackTrace(); | |
} | |
} | |
} | |
class Animal implements Cloneable{ | |
private String name; | |
public Animal() { | |
super(); | |
} | |
public Animal(String name) { | |
super(); | |
this.name = name; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
@Override | |
public String toString() { | |
return "Animal [name=" + name + "]"; | |
} | |
@Override | |
protected Object clone() throws CloneNotSupportedException { | |
// TODO Auto-generated method stub | |
return super.clone(); | |
} | |
} |
# 4、finalize()
从 jdk9 后就不建议使用了,但允许用
- 当对象被回收时,系统自动调用该对象的 finalize () 方法。(不是垃圾回收器调用的,是本类对象调用的)
- 永远不要主动调用某个对象的 finalize 方法,应该交给垃圾回收机制调用。
- 什么时候被回收:当某个对象没有任何引用时,JVM 就认为这个对象是垃圾对象,就会在之后不确定的时间使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize () 方法。
- 子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连接资源。
- 如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象。
- 在 JDK 9 中此方法已经被
标记为过时
的。
public class FinalizeTest { | |
public static void main(String[] args) { | |
Person p = new Person("Peter", 12); | |
System.out.println(p); | |
p = null;// 此时对象实体就是垃圾对象,等待被回收。但时间不确定。 | |
System.gc();// 强制性释放空间 | |
} | |
} | |
class Person{ | |
private String name; | |
private int age; | |
public Person(String name, int age) { | |
super(); | |
this.name = name; | |
this.age = age; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public int getAge() { | |
return age; | |
} | |
public void setAge(int age) { | |
this.age = age; | |
} | |
// 子类重写此方法,可在释放对象前进行某些操作 | |
@Override | |
protected void finalize() throws Throwable { | |
System.out.println("对象被释放--->" + this); | |
} | |
@Override | |
public String toString() { | |
return "Person [name=" + name + ", age=" + age + "]"; | |
} | |
} |
# 5、getClass()
public final Class<?> getClass():获取对象的运行时类型
因为 Java 有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用 getClass () 方法
public static void main(String[] args) { | |
Object obj = new Person(); | |
System.out.println(obj.getClass());// 运行时类型 | |
} |
结果:
class com.atguigu.java.Person |
# 6、hashCode()
public int hashCode():返回每个对象的 hash 值。(后续在集合框架章节重点讲解)
public static void main(String[] args) { | |
System.out.println("AA".hashCode());//2080 | |
System.out.println("BB".hashCode());//2112 | |
} |
# 8.3 native 关键字的理解
使用 native 关键字说明这个方法是原生函数,也就是这个方法是用 C/C++
等非 Java 语言实现的,并且 被编译成了DLL
,由 Java 去调用。
本地方法是有方法体的,用 c 语言编写。由于本地方法的方法体源码没有对我们开源,所以我们看不到方法体
在 Java 中定义一个 native 方法时,并不提供实现体。
1. 为什么要用 native 方法
Java 使用起来非常方便,然而有些层次的任务用 java 实现起来不容易,或者我们对程序的效率很在意时,例如:Java 需要与一些底层操作系统或某些硬件交换信息时的情况。native 方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解 Java 应用之外的繁琐的细节。
2. native 声明的方法,对于调用者,可以当做和其他 Java 方法一样使用
native method 的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM 将控制调用本地方法的所有细节。