12. 继承(第十六天)
12.1. 复习变量的访问
- 类加载机制中,是这样的:在程序执行之前,凡是需要加载的类全部加载到JVM当中。
- 先完成加载才会执行main方法。
- 私有的是可以在本类中访问的。在其它类中必须使用set和get方法。
- 程序再怎么变化,万变不离其宗,有一个固定的规律:
- 所有的实例相关的都是先创建对象,通过“引用.”来访问。
- 所有的静态相关的都是直接采用“类名.”来访问。
- 大结论:
只要负责调用的方法a和被调用的方法b在同一个类当中:this. 可以省略、类名. 可以省略
12.2. 继承(extends)
12.2.1. 继承作用
- 什么是继承?
基本作用:子类继承父类,代码可以得到复用。(这个不是重要的作用,是基本作用。)
主要(重要)作用:因为有了继承关系,才有了后期的方法覆盖和多态机制。 - 继承的相关特性(六点):
- B类继承A类,则称A类为超类(superclass)、父类、基类,
B类则称为子类(subclass)、派生类、扩展类。
class A{}
class B extends A{}
我们平时聊天说的比较多的是:父类和子类
superclass 父类
subclass 子类
- java 中的继承只支持单继承,不支持多继承
class B extends A,C{ } 这是错误的。
- java 中不支持多继承,但有的时候会产生间接继承的效果。
class C extends B,class B extends A,
也就是说,C 直接继承 B其实 C 还间接继承 A。
- java 中规定,子类继承父类,除构造方法不能继承之外,剩下都可以继承。
父类中private修饰的不能在子类中直接访问。可以通过间接的手段来访问。
- java 中的类没有显示的继承任何类,则默认继承 Object类,Object类是 java 语言提供的
根类(老祖宗类),也就是说,一个对象与生俱来就有 Object类型中所有的特征。
java这么庞大的一个继承结构,最顶点是:Object
- 继承也存在一些缺点: 耦合度非常高,父类修改,子类受牵连。
12.3. day16天作业(详解)
💡day 17天
12.4. 继承的补充
- 什么情况下可以使用继承
凡是能采用“is a”描述的,都可以继承。
Cat is a Animal:猫是一个动物
Dog is a Animal:狗是一个动物
CreditAccount is a Account:信用卡账户是一个银行账户
- 假设以后的开发中有一个A类,有一个B类,A类和B类确实也有重复的代码,那么他们两个之间就
可以继承吗?
- 不一定,还是要看一看它们之间是否能够使用“is a”来描述。
- 任何一个类,没有显示继承任何类,默认继承Object,那么Object类当中有哪些方法呢?
- 以后慢慢的大家一定要适应看JDK的源代码(多看看牛人写的程序自己才会变成牛人。)
- JDK源代码在什么位置? (根据自己的安装路径对应查找)
C:\Program Files\Java\jdk-13.0.2\lib\src.zip
- 你现在能看懂以下代码了吗?
System.out.println("Hello World!");
- System.out 中,out后面没有小括号,说明out是变量名。
- 另外System是一个类名,直接使用类名System.out,说明out是一个静态变量。
- System.out 返回一个对象,然后采用“对象.”的方式访问println()方法。
12.4.1 object类
- 子类继承父类
实质上,子类继承父类之后,是将父类继承过来的方法归为自己所有。实际上调用的也不是父类的
方法,是他子类自己的方法(因为已经继承过来了就属于自己的。)。 - 默认继承object类
- object类中有哪些方法?
public class Object {
// 注意:当源码当中一个方法以“;”结尾,并且修饰符列表中有“native”关键
字
// 表示底层调用C++写的dll程序(dll动态链接库文件)
private static native void registerNatives();
// 静态代码块
static {
// 调用registerNatives()方法。
registerNatives();
}
- equals方法你应该能看懂。
// public是公开的
// boolean 是方法的返回值类型
// equals 是一个方法名:相等
// (Object obj) 形参
public boolean equals(Object obj) {
//方法体
return (this == obj);
}
public String toString() {
return getClass().getName() + "@" +
Integer.toHexString(hashCode());
}
- tostring方法
- System.out.println(引用);
当直接输出一个“引用”的时候,println()方法会先自动调用“引用.toString()”,然
后输出toString()方法的执行结果。
- public static void main(String[] args){
- 分析这个代码可以执行吗?
- ExtendsTest05.toString();
- 分析:不能执行,因为toString方法是实例方法不能通过类名的方式访问
// 实际上是内存地址经过“哈希算法”得出的十六进制结果。
System.out.println(retValue); // ExtendsTest05@2f92e0f4
// 2f92e0f4 可以“等同”看做对象在堆内存当中的内存地址
public String toString() {
return getClass().getName() + "@" +
Integer.toHexString(hashCode());
}
12.5. Editplus字体颜色说明
- editPlus中蓝色是关键字
- 黑色是标识符
- 在 editplus中的红色字体,表示这个类是SUN的JDK写好的一个类。
13. 方法覆盖和多态
13.1. 方法覆盖
- 什么时候考虑方法的覆盖
父类中的方法无法满足子类的业务需求,子类有必要对继承过来的方法进行覆盖。
方法覆盖又叫:方法重写、override
- 什么条件满足的时候构成方法覆盖?
第一:有继承关系的两个类
第二:具有相同方法名、返回值类型、形式参数列表
第三:访问权限不能更低。
第四:抛出异常不能更多。
//protected表示受保护的。没有public开放。
// 错误:正在尝试分配更低的访问权限; 以前为public
/*
protected void move(){
System.out.println("鸟儿在飞翔!!!");
}
*/
//错误:被覆盖的方法未抛出Exception
/*
public void move() throws Exception{
System.out.println("鸟儿在飞翔!!!");
}
*/
- 分析:这个sing()和父类中的sing(int i)有没有构成方法覆盖呢?
class Animal{
public void move(){
System.out.println("动物在移动!");
}
public void sing(int i){
System.out.println("Animal sing....
");
}
}
class Bird extends Animal{
public void sing(){
// 没有,原因是,这两个方法根本就是两个完全不同的方法。
// 可以说这两个方法构成了方法重载吗?可以。
System.out.println("Bird sing.....
");
}
}
- 关于Object类中toString()方法的覆盖?
toString()方法存在的作用就是:将java对象转换成字符串形式。
- 大多数的java类toString()方法都是需要覆盖的。因为Object类中提供的toString()方法输出的是一个java对象的内存地址。
- 至于toString()方法具体怎么进行覆盖?
格式可以自己定义,或者听需求的。(听项目要求的。)
- 重要结论:
- 当子类对父类继承过来的方法进行“方法覆盖”之后,子类对象调用该方法的时候,一定执行覆盖之后的方法。
- 一定要注意:方法覆盖/重写的时候,建议将父类的方法复制粘贴,这样比较保险。
- 这里还有几个注意事项:(这几个注意事项,当学习了多态语法之后自然就明白了!)
注意1:方法覆盖只是针对于方法,和属性无关。
注意2:私有方法无法覆盖。
注意3:构造方法不能被继承,所以构造方法也不能被覆盖。
注意4:方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。
13.2. 关于Object类中的toString()方法
- toString()方法的作用是什么?
作用:将“java对象”转换成“字符串的形式” - Object类中toString()方法的默认实现是什么?
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
toString: 方法名的意思是转换成String
含义:调用一个java对象的toString()方法就可以将该java对象转换成字符串的表示形式。
- 那么toString()方法给的默认实现够用吗?
- 不满意,希望输出:xxxx年xx月xx日
重写MyDate的toString()方法之前的结果
//System.out.println(t1.toString()); //MyDate@28a418fc
// 重写MyDate的toString()方法之后的结果
System.out.println(t1.toString());
System.out.println(t1);
MyDate t2 = new MyDate(2008, 8, 8);
System.out.println(t2); //2008年8月8日
13.3. 方法重载和方法覆盖有区别
方法重载发生在同一个类当中。
方法覆盖是发生在具有继承关系的父子类之间。
方法重载是一个类中,方法名相同,参数列表不同。
方法覆盖是具有继承关系的父子类,并且重写之后的方法必须和之前的方法一致: 方法名一致、参数列表一致、返回值类型一致。
13.4. 多态的基础语法
- 向上转型和向下转型的概念:
- 向上转型:子--->父 (upcasting)
又被称为自动类型转换:Animal a = new Cat(); - 向下转型:父--->子 (downcasting)
又被称为强制类型转换:Cat c = (Cat)a; 需要添加强制类型转换符。
什么时候需要向下转型?
需要调用或者执行子类对象中特有的方法,必须进行向下转型,才可以调用。
- 向下转型有风险吗?
容易出现ClassCastException(类型转换异常)
- 怎么避免这个风险?
instanceof运算符,可以在程序运行阶段动态的判断某个引用指向的对象是否为某一种类型。养成好习惯,向下转型之前一定要使用instanceof运算符进行判断。
不管是向上转型还是向下转型,首先他们之间必须有继承关系,这样编译器就不会报错。
- 注意事项:
- java中允许向上转型,也允许向下转型。
- 无论是向上转型,还是向下转型,两种类型之间必须有继承关系,没有继承关系编译器报
错。
- 以后在工作过程中,和别人聊天的时候,要专业一些,说向上转型和向下转型,不要说
自动类型转换,也不要说强制类型转换,因为自动类型转换和强制类型转换是使用在基
本数据类型方面的,在引用类型转换这里只有向上和向下转型。
- 什么是多态
多种形态,多种状态,编译和运行有两个不同的状态。
- 编译期叫做静态绑定:绑定父类的方法。
- 运行期叫做动态绑定:动态绑定子类型对象的方法。
Animal a = new Cat();
// 编译的时候编译器发现a的类型是Animal,所以编译器会去Animal类中找
move()方法
// 找到了,绑定,编译通过。但是运行的时候和底层堆内存当中的实际对象有关
// 真正执行的时候会自动调用“堆内存中真实对象”的相关方法。
a.move();
- 多态的典型代码:父类型的引用指向子类型的对象。(java中允许这样写代码!!!
- Test01.java 完整代码
public class Test01{
public static void main(String[] args){
Animal a1 = new Animal();
a1.move(); //动物在移动!!!
Cat c1 = new Cat();
c1.move(); //cat走猫步!
Bird b1 = new Bird();
b1.move(); //鸟儿在飞翔!!!
// 代码可以这样写吗?
/*
1、Animal和Cat之间有继承关系吗?有的。
2、Animal是父类,Cat是子类。
3、Cat is a Animal,这句话能不能说通?能。
4、经过测试得知java中支持这样的一个语法:
父类型的引用允许指向子类型的对象。
Animal a2 = new Cat();
a2就是父类型的引用。
new Cat()是一个子类型的对象。
允许a2这个父类型引用指向子类型的对象。
*/
Animal a2 = new Cat();
Animal a3 = new Bird();
/*
分析:a2.move();
java程序分为编译阶段和运行阶段。
先来分析编译阶段:对于编译器来说,编译器只知道a2的类型是Animal,所以编译器在检
查语法的时候,会去Animal.class字节码文件中找move()方法,找到了,绑定上move()方
法,编译通过,静态绑定成功。(编译阶段属于静态绑定。)
再来分析运行阶段:运行阶段的时候,实际上在堆内存中创建的java对象是Cat对象,所以
move的时候,真正参与move的对象是一只猫,所以运行阶段会动态执行Cat对象的move()方
法。这个过程属于运行阶段绑定。(运行阶段绑定属于动态绑定。)
*/
a2.move(); //cat走猫步!
- 分析这个程序能否编译和运行呢?
- 分析程序一定要分析编译阶段的静态绑定和运行阶段的动态绑定。
- 只有编译通过的代码才可能运行。没有编译,根本轮不到运行。
Animal a5 = new Cat(); // 底层对象是一只猫。
a5.catchMouse();
// 错误: 找不到符号
// why??? 因为编译器只知道a5的类型是Animal,去Animal.class文件中找catchMouse()方法
// 结果没有找到,所以静态绑定失败,编译报错。无法运行。(语法不合法。)
- 假设代码写到了这里,我非要调用catchMouse()方法怎么办?
- 这个时候就必须使用“向下转型”了。(强制类型转换)
// 以下这行代码为啥没报错????
// 因为a5是Animal类型,转成Cat,Animal和Cat之间存在继承关系。所以没报
错。
Cat x = (Cat)a5;
x.catchMouse(); //猫正在抓老鼠!!!!
- // 向下转型有风险吗?
Animal a6 = new Bird();
这一行代码表面上a6是一个Animal,运行的时候实际上是一只鸟儿。
/*
分析以下程序,编译报错还是运行报错???
编译器检测到a6这个引用是Animal类型,
而Animal和Cat之间存在继承关系,所以可以向下转型。
编译没毛病。
运行阶段,堆内存实际创建的对象是:Bird对象。
在实际运行过程中,拿着Bird对象转换成Cat对象
就不行了。因为Bird和Cat之间没有继承关系。
运行是出现异常,这个异常和空指针异常一样非常重要,也非常经典:
java.lang.ClassCastException:类型转换异常。
java.lang.NullPointerException:空指针异常。这个也非常重要。
*/
//Cat y = (Cat)a6;
//y.catchMouse();
- 什么时候必须进行向下转型?
调用子类对象上特有的方法时。
- java中“ . ''的用法
- java中只有“类名”或者“引用”才能去“点”
类名.
引用.
万变不离其宗,只要你想“点”,“点”前面要么是一个类名,要么是一个引用。
- 属性重写问题
- 当父类和子类 属性重名时,向上转型看编译期,和运行期无关,即编译期谁在调用,就是谁的属性值
public class Test {
int count = 10 ;
public static void main(String[] args) {
Test test = new Test01();
System.out.println(test.count); // 值为10
}
}
class Test01 extends Test{
int count = 20 ;
}
13.5. 运算符( instanceof )
- 怎么避免ClassCastException异常的发生???
- 新的内容,运算符:instanceof (运行阶段动态判断)
- 第一:instanceof可以在运行阶段动态判断引用指向的对象的类型。
- 第二:instanceof的语法:
(引用 instanceof 类型) - 第三:instanceof运算符的运算结果只能是:true/false
- 第四:c是一个引用,c变量保存了内存地址指向了堆中的对象。
假设(c instanceof Cat)为true表示:
c引用指向的堆内存中的java对象是一个Cat。
假设(c instanceof Cat)为false表示:
c引用指向的堆内存中的java对象不是一个Cat。 - 程序员要养成一个好习惯:任何时候,任何地点,对类型进行向下转型时,一定要使用instanceof 运算符进行判断。(java规范中要求的。)
- 这样可以很好的避免:ClassCastException
System.out.println(a6 instanceof Cat); //false
if(a6 instanceof Cat){ // 如果a6是一只Cat
Cat y = (Cat)a6; // 再进行强制类型转换
y.catchMouse();
}
}
- 为什么使用 instanceof 运算符? 我们为什么还要进行instanceof的判断呢?
原因是:你以后可能肉眼看不到。
public class AnimalTest{
// test方法是程序员B负责编写。
// 这个test()方法的参数是一个Animal
public void test(Animal a){ // 实例方法
// 你写的这个方法别人会去调用。
// 别人调用的时候可能给你test()方法传过来一个Bird
// 当然也可能传过来一个Cat
// 对于我来说,我不知道你调用的时候给我传过来一个啥。
if(a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse();
}else if(a instanceof Bird){
Bird b = (Bird)a;sing();
}
}
}
- 养成好的素养 按照以下格式 编写程序
public class Test02{
public static void main(String[] args){
Animal x = new Bird();
Animal y = new Cat();
if(x instanceof Bird){
Bird b = (Bird)x;
b.sing();
} else if(x instanceof Cat){
Cat c = (Cat)x;
c.catchMouse();
}
if(y instanceof Bird){
Bird b = (Bird)y;
b.sing();
} else if(y instanceof Cat){
Cat c = (Cat)y;
c.catchMouse();
}
}
13.6. 多态在开发中的作用
- 多态在开发中有什么作用?
- 多态在开发中的作用是:降低程序的耦合度,提高程序的扩展力。
public class Master{
public void feed(Dog d){}
public void feed(Cat c){}
}
以上的代码中表示:Master和Dog以及Cat的关系很紧密(耦合度高)。导致扩展力很差。
public class Master{
public void feed(Pet pet){
pet.eat();
}
}
以上的代表中表示:Master和Dog以及Cat的关系就脱离了,Master关注的是Pet类。
这样Master和Dog以及Cat的耦合度就降低了,提高了软件的扩展性。
- 什么是软件扩展性?
- 假设电脑中的内存条部件坏了,我们可以买一个新的插上,直接使用。这个电脑的设计就考
虑了“扩展性”。内存条的扩展性很好。 - 面向父类型编程,面向更加抽象进行编程,不建议面向具体编程。因为面向具体编程会让
软件的扩展力很差
- 方法覆盖需要和多态机制联合起来使用才有意义
Animal a = new Cat();
a.move();
要的是什么效果?
编译的时候move()方法是Animal的。
运行的时候自动调用到子类重写move()方法上。
- 假设没有多态机制,只有方法覆盖机制,你觉得有意义吗?
- 没有多态机制的话,方法覆盖可有可无。
- 没有多态机制,方法覆盖也可以没有,如果父类的方法无法满足
- 子类业务需求的时候,子类完全可以定义一个全新的方法。
- 方法覆盖和多态不能分开
2、静态方法存在方法覆盖吗?
- 多态自然就和对象有关系了。
- 而静态方法的执行不需要对象。
- 所以,一般情况下,我们会说静态方法“不存在”方法覆盖。不探讨静态方法的覆盖。
public class OverrideTest05{
public static void main(String[] args){
// 静态方法可以使用“引用.
”来调用吗?可以
// 虽然使用“引用.
”来调用,但是和对象无关。
Animal a = new Cat(); //多态
// 静态方法和对象无关。
// 虽然使用“引用.
”来调用。但是实际运行的时候还是:Animal.doSome()
a.doSome();
Animal.doSome();
Cat.doSome();
}
}
class Animal{
// 父类的静态方法
public static void doSome(){
System.out.println("Animal的doSome方法执行!");
}
}
class Cat extends Animal{
// 尝试在子类当中对父类的静态方法进行重写
public static void doSome(){
System.out.println("Cat的doSome方法执行!");
}
}
- 面向对象的三大特征:封装、继承、多态
- 真的是一环扣一环。
- 有了封装,有了这种整体的概念之后。
- 对象和对象之间产生了继承。
- 有了继承之后,才有了方法的覆盖和多态。
- 软件ocp开发原则
- 这里提到了一个软件开发原则:
- 七大原则最基本的原则:OCP(对扩展开放,对修改关闭)
- 目的是:降低程序耦合度,提高程序扩展力。
- 面向抽象编程,不建议面向具体编程。
- 解释之前遗留的问题:
- 私有方法无法覆盖。
public class OverrideTest06{
// 私有方法
private void doSome(){
System.out.println("OverrideTest06's private method doSome
execute!");
}
// 入口
public static void main(String[] args){
// 多态
OverrideTest06 ot = new T();
ot.doSome(); //OverrideTest06's private method doSome execute!
}
}
/*
// 在外部类中无法访问私有的。
class MyMain{
public static void main(String[] args){
OverrideTest06 ot = new T();
//错误: doSome() 在 OverrideTest06 中是 private 访问控制
//ot.doSome();
}
}
*/
// 子类
class T extends OverrideTest06{
// 尝试重写父类中的doSome()方法
// 访问权限不能更低,可以更高。
public void doSome(){
System.out.println("T's public doSome method execute!");
}
}
- 方法覆盖只是针对于“实例方法”,“静态方法覆盖”没有意义。(这是因为方法覆盖通常和多态联合 起来)
- 总结两句话: 私有不能覆盖; 静态不谈覆盖。
- 在方法覆盖中,关于方法的返回值类型。
什么条件满足之后,会构成方法的覆盖呢?
发生具有继承关系的两个类之间。
父类中的方法和子类重写之后的方法:具有相同的方法名、相同的形式参数列表、相同的返
回值类型。 - 返回值类型
- 学习了多态机制之后:
- “相同的返回值类型”可以修改一下吗?
- 对于返回值类型是基本数据类型来说,必须一致。
- 对于返回值类型是引用数据类型来说,重写之后返回值类型可以变的更小(但意义不大,实际开发中没人这样写).