继承与多态
一. 子类
1. is a 关系
-
子类与父类的关系:
- 子类 is a (或is kind of) 父类对象
- 子类中的任何一个成员也是父类中一个成员
-
使用了继承这一特性,可以支持软件的复用性,保证代码可以在类之间共享
-
经理和员工之间的关系
-
显然,经理是一名员工,他具有员工的一般特性
-
此外,经理还有员工所没有的额外特性
-
经理类:
-
public class Manager { // 具有特殊性的类 private String name; private String sex; private String age; // 以上是与员工共有的属性 private String department; // 所管理的部门 特有属性 private Employee[] subordinates; // 所管下级 特有属性 }
-
员工类:
-
public class Employee { // 具有一般性的类 private String name; private String sex; private String age; }
-
-
可以看出,经理类与员工类存在重复部分,适合于员工的很多属性和方法可以不经修改就被经理类使用
-
员工类与经理之间存在 is a 关系,Manager is a Employee
-
-
-
使用 is a 关系需要注意,一些对象之间并不是 一般 与 特殊 的关系,例如
- 汽车包括了车身和发动机,但不能说他们存在 is a 关系
- 它们只能是整体与部分的关系,一般为 has a 关系
- 公司没了经理,员工还能干活,而汽车没了发动机,怎么开
2. extends关键字
-
Java提供了派生机制,允许用以前已定义的类来定义一个新类
- 新类称为子类,原来的类称为父类(超/基类)
- 两个类中共同的内容放在父类,特殊的类放在子类
-
使用extends关键字表示派生
-
public class A extends B { }
- 表明 A类 派生于 B类,A为子类,B为父类
- 如果一个类的定义中,没有出现 extends 关键字,则表明这个类派生与Object类
- Java中预定义以及程序员自定义的类都直接或间接派生于Object
-
-
类的划分也需要看实际的应用而定
- 例如大学生可以分为全日制大学生和非全日制大学生两类
- 也可以分为 本科生 以及 研究生 两类
- 还可以按照院系父类,如何将大集合划分为小集合,要看具体应用的标准去定
-
现在使用继承的方式来重新定义经理类和员工类
-
员工类
-
public class Employee { // 父类 private String name; private String sex; private String age; }
-
-
经理类
-
public class Manager extends Employee { // 子类 private String department; // 只生成员工类没有的属性 private Employee[] subordinates; }
-
-
经理类中有员工类的所有的变量和方法,都继承于父类中的定义
-
子类只定义额外的属性,或者进行必要的修改
-
派生机制改善了程序的可维护性,增加了可靠性,对父类所做的修改延伸到子类中
-
3. Object类
-
Object类是Java程序中所有类的直接或者间接父类,Object类包含了所有Java类的公共属性
-
主要构造方法如下:
-
获取当前对象所属的类信息,返回Class对象
-
public final native Class<?> getClass();
-
-
按字符串对象返回当前对象本身的有关信息
-
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
-
-
比较两个对象是否是同一个对象
-
public boolean equals(Object obj) { return (this == obj); }
-
-
-
-
关于对象相等的判断,在Java中有两种方式
- 一种是 运算符 ==
- 一种是 使用 equals()方法
- 两种方式都是判断内存地址
- 两个对象具有相同的类型和相同的属性值,则为相等。同一个对象的一定相等,相等的对象不一定同一
-
相等判断实例
-
实体类
public class BankAccount { private String name; private int account; private float balance; public BankAccount() { } public BankAccount(String name, int account, float balance) { this.name = name; this.account = account; this.balance = balance; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAccount() { return account; } public void setAccount(int account) { this.account = account; } public float getBalance() { return balance; } public void setBalance(float balance) { this.balance = balance; } }
-
测试类
public class Test { public static void main(String[] args) { BankAccount a = new BankAccount("张三", 123456, 100.00f); BankAccount b = new BankAccount("张三", 123456, 100.00f); if (a.equals(b)){ System.out.println("equals_Yes"); }else { System.out.println("equals_No"); } if (a == b){ System.out.println("==_Yes"); }else { System.out.println("==_No"); } } }
-
控制台
equals_No ==_No
-
分别输出No,由于a和b是两个独立的对象,所以他们的内存地址不是一样的
-
如果b不是使用new操作符来创建,而是通过赋值的方式,则输出的是Yes
-
-
要判断两个对象之间的值是否相同,需要在类声明中对Object类的equals()方法进行重写
-
idea快捷键可直接生成
-
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BankAccount that = (BankAccount) o; return account == that.account && Float.compare(that.balance, balance) == 0 && Objects.equals(name, that.name); }
-
-
4. 单重继承
-
在类的继承机制中,实现了单重继承机制。抛弃了多重继承机制
-
多重继承机制
- 指多个类共同派生一个子类,即一个子类可以有多个父类
- 容易造成子类实例的混乱
-
单继承机制
- 并没有减弱Java在继承方面的能力
- Java提供了接口这个概念,多重继承的能力通过接口来实现
- 总而言之,单继承就是如果一个类有父类,则其父类只能有一个
-
-
子类不能继承构造方法
- 一个子类可以从父类以及祖先类中继承所有可继承的方法和成员变量
- 只有两个方式可以得到构造方法
- 自己手动编写的
- 在用户没有编写的时候,系统默认提供的
-
子类可以直接使用父类中定义的公有和默认的属性和方法
-
子类不能直接访问父类中私有属性和方法,可以通过父类中定义的公有以及保护方法访问私有数据成员,可以通过get方法
-
public class B { public int a = 10; private int b = 20; protected int c = 30; int d = 40; public int getB() { return b; } } class A extends B { public void tryVariables() { System.out.println(a); // 允许直接使用 System.out.println(b); // 不允许直接使用,b是私用的 System.out.println(getB()); // 不允许直接使用,通过公用方法访问,get System.out.println(c); // 受保护的,直接使用 System.out.println(d); // 默认的,直接使用 } }
-
-
5.对象转型
-
使用对象的父类类型的一个变量指向该对象
-
可以将子类的对象赋给父类的变量
-
A a = new B(); // 子类B的实例赋值给父类变量a
- 这称为对象转型,使用变量a,可以只知道 父类A 的内容,而隐藏 子类B 中的特殊内容
- 编译器晓得 变量a 是一个B子类,而不是A
-
-
-
类的变量既可以指向本类实例,又可以指向其子类的实例,对象的多态性
-
instanceof 运算符
-
在程序中,有时候需要判断一个引用到底指向哪个实例,可以使用此运算符
-
假设,员工类型的引用指向一个对象没打不分不清该对象是员工类型还是承包商类型,借助instanceof可以判断它的真正类型(承包商和经理类继承于员工类)
-
public void methods(Employee e) { if (e instanceof Manager) { System.out.println("经理类"); }else if (e instanceof Contractors) { System.out.println("承包商类"); }else { System.out.println("都不是"); } }
-
-
如果 instanceof 运算符 已判定父类的引用指向的是子类实例,就可以转换该引用,恢复对象的全部功能
-
如下:使用父类 Employee 的引用指向 Manager 的实例,然后访问相关属性
-
Employee m = new Manager(); // 使用父类引用指向子类实例
-
这样是访问不到特有属性的,编译器知道当使用员工类型的引用来访问特有属性时,编译器在员工类中找不到那个成员,就会报错
-
将其转型
-
Manager m1 = (Manager)m; // 将员工类型的,转换为Manager类型
-
-
-
-
-
-
进行对象转型有以下规则
-
沿类层次向 上 转型是合法的,例如将 经理引用类型 转型为 员工引用,这种子 转 父的情况下不需要转型运算符
-
向下转型,只能是祖先类转型为后代类,其他类之间是非法的
-
为什么要向下转型呢,是不是觉得很鸡肋,我直接new子类实例不好吗
- 现在有一个 电子产品的接口类,然后我搞一个戴尔的类实现这个电子产品接口
- 再分别定义一个 鼠标类,键盘类 都实现这个电子产品接口
- 然后搞个购物车类,用一个集合存放这些商品,使用泛型
- 泛型里面穿个电子产品的接口,为什么不传鼠标呢,键盘呢,那样就太low了,需要建多少个集合来存放商品,于是我直接电子产品,执行到这的时候会传相应的商品进去,比如传了个戴尔笔记本,他在传进去的过程中,会经历一次向上转型,然而如果这个时候我想去试用一下这个笔记本的功能,也就是特有的属性方法,显然是没办法的。
- 于是Java语言有个向下转型的特性,即强转回子类
- 需要用到子类实例的时候,就从那个父类集合里拿出来向下转型就可以了,一样可以使用子类实例对象
-
二. 方法覆盖与多态
1. 方法覆盖
-
父类中原有的方法可能无法满足新要求,因此需要在修改父类中已有的方法,这就是方法覆盖(重写/隐藏)
-
子类中定义方法名,返回类型,参数列表与父类中的方法使用完全一致(具有相同的方法签名),此时就是覆盖了父类中的方法
-
逻辑上来看就是子类中的成员方法隐藏了父类中的同名方法
-
2. 方法覆盖的规则
-
子类重写父类方法时,与父类使用的是相同的方法名和参数列表,执行的功能可以与父类不完全一样,也可以完全不一样,满足了灵活性
-
覆盖的同名方法中,子类不能比父类方法的访问权限更严格,若父中方法为 public,则子类不能是private的方法
-
如果参数列表不同,则为重载,重载的方法只属于同一个类
-
super关键字
-
如果子类已经重写了父类的方法,但在子类中还想使用父类中被隐藏的方法,super 可以解决这个烦恼
-
public class SuperClass { void showMy() { System.out.println("Im 钢铁侠"); System.out.println("蜘蛛侠"); } } class SubClass extends SuperClass { void showMy() { System.out.println("美国队长"); super.showMy(); // 调用父类方法,输出父类中的信息 System.out.println("黑豹"); } } class SuperTest{ public static void main(String[] args) { SubClass son = new SubClass(); // 子类实例 son.showMy(); // 调用子类方法 } } 控制台: 美国队长 Im 钢铁侠 蜘蛛侠 黑豹
-
-
-
使用super需要注意两个问题
- super调用父类方法,将执行父类方法中的所有操作,可能包括原本不希望进行的操作
- 由继承机制可以知道,super.method( ) 语句调用的这个方法不一定是在父类中加以描述的,也有可能是父类从它的祖先类继承而来的
- 有可能需要按继承层次关系依次向上才能找到
-
应用覆盖时要注意两个规则
- 覆盖方法的允许范围不能小于原来的方法
- 覆盖方法所抛出的异常不能比原方法多
3. 调用父类的构造方法
-
Java要求父类的对象要在子类运行前完全初始化
-
子类不能从父类继承构造方法
-
但是super关键字可以用在构造方法中,可以为子类调用父类的构造方法
-
如果子类构造方法的定义中没有明确调用父类的构造方法,则系统在执行子类的构造方法的时候会自动调用父类的默认无参构造
-
在子类构造方法中调用了父类构造,则调用语句必须!!出现在子类构造方法的第一行
-
public class SuperClass { String name; public SuperClass() { } public SuperClass(String name) { this.name = name; } } class SubClass extends SuperClass { String temp; public SubClass(String n, String t) { super(n); // 调用父类构造方法 放在第一行 this.temp = t; } }
-
-
调用super( ) 时参数无限制,其参数列表与父类中某个构造的参数列表相符即可
4. 多态
-
有了多态能够允许同一条方法调用指令在不同的上下文做不同的事
-
如之前 经理 与 员工 的例子
- 经理类 与 员工类 之间具有 is a 关系
-
经理类得到了父类员工类的所有可继承属性,包括数据成员和方法成员
-
比如说现在两个类中都有 getTemp( ) 方法,实际上子类覆盖了父类中的方法
-
假定声明了两个变量
-
Employee e = new Employee(); Manager m = new Manager();
- e.getTemp()与 m.getTemp() 执行的是不同的代码
- e是员工类中的方法,m执行的经理类中的方法
-
-
如果这样创建实例,使用多态
-
Employee e = new Manager();
- 如此一来,e将调用的是哪个方法呢
- 可以有经理类的,也可以有员工类的
- 首先检查父类中是否有该方法,如果有,再去调用子类的同名方法
-
-
-
多态是面向对象语言的一个重要特性
- 重载一个方法名可以看做是多态的,父子间直接或间接重写的方法名要由对象在运行时决定要调哪个方法,也是多态
- Java规定,多态情况下要执行的是与对象真正类型(运行时类型)相关的方法,而不是引用类型(编译时类型)
- 变量的静态类型出现在声明的类型中,例如 变量 e 的静态类型是 Employee
- 静态类型也是引用类型,是在代码编译时确定下来的
- 运行过程中某一时刻变量指向 的对象的类型,那个类型就是动态类型,此刻才是它真正的类型
-
变量的动态类型会随着运行进程而改变,比如 e变量的动态类型是 Manager
-
动态绑定
-
对方法采用动态绑定
-
调整稍后可能被覆盖的方法的这种处理方式称为动态绑定或者后绑定
-
动态绑定一定要到运行时才能确定要执行的方法代码
-
动态绑定只是针对对象的方法,对属性没用,因为属性不能重写
-
-
静态绑定
-
对属性采用静态绑定
-
在编译期就发现程序中的错误,而不是在运行期,提高效率
-
如果一个方法不可被继承或者继承后不可被覆盖通常就是静态绑定
-
在编译过程中能确定调用方法的处理方式,为静态绑定
-
在程序执行前方法已经被绑定(也就是说在编译过程中就已经知道这个方法到底是哪个类中的方法)
-
Java 当中的方法只有final,static,private和构造方法是静态绑定
-
private不能被继承,没办法通过它子类的对象来调
-
final 虽然可以被继承但是不能被 重写覆盖
-
构造方法也不能被继承,虽然可以通过super调用
-
static方法可以被子类继承,但是不能被子类重写
-
-
-
多态实例
-
public class SuperClass { public void method() { System.out.println("钢铁侠"); } } class SubClass extends SuperClass { public void method() { System.out.println("蜘蛛侠"); } } class SuperTest{ public static void main(String[] args) { SuperClass sup = new SuperClass(); // 静态类型与动态类型一致 SubClass sub = new SubClass(); // 静态类型与动态类型一致 SuperClass ssc = new SubClass(); // 静态类型与动态类型不一致 sup.method(); // 执行父类的方法 输出 钢铁侠 sub.method(); // 执行子类的方法 输出 蜘蛛侠 ssc.method(); // 执行子类的方法 输出 蜘蛛侠 } }
- ssc 声明的类型是SuperClass(静态类型),单它指向的是SubClass(动态类型)的实例,所以调用的方法是子类的
-
三. 终极类与抽象类
1. final关键字
- final 表示终极,既可以修饰类,也可以修饰类中的成员变量和方法
- 如果一个类被定义了 final,则它不能有子类,方法被定义了,不能被覆盖,变量被定义了,不能被修改
- 与之对应的关键字是 abstract,它可以用于类 或者 方法,表示抽象,使用abstract 修饰的方法其方法体为空,修饰的类必须被子类继承
2. 终极类
-
有时候一些类是不能被继承的,比如 Java.lang下的String类
- 为了保证如果一个方法中有一个指向String类的引用,则他就是一个真正的String类型了,而不是一个已被修改的String的子类
-
某个类的结构和功能已经很完整,不需要生成它的子类,也可以使用 Final 进行修饰
-
声明格式是在 class 前面加上final进行修饰即可
-
final public class SuperClass
-
public final class SuperClass
-
3. 终极方法
-
被标记的方法将不能被覆盖
- 可以确保被调用的时候是最原始的方法,而不是被子类中修改的方法
- 把方法标记为final 有时候也被用于优化,提高编译运行效率
-
定义格式,在返回值类型前面,添加即可
-
public final void method() { System.out.println("钢铁侠"); }
-
4. 终极变量
-
被 final 标记的变量,实际上就变成了一个常量,企图修改终极变量的值都会引发编译错误
- 不可被重新赋值
-
当程序中需要使用一些特殊用途的常量时,可以将其定义在一个类中,其他类引入该类直接使用这些常量
-
保证了常量使用的统一,为程序的维护提供了方便
-
public class CrowdConstant { public static final String ATTR_NAME_EXCEPTION = "exception"; public static final String MESSAGE_LOGIN_FAILED = "抱歉!账号或者密码错误 请重新登陆"; public static final String MESSAGE_LOGIN_ACCT_ALREADY_IN_USE = "抱歉该账号已经被使用"; public static final String MESSAGE_ACCOUNT_NOT_LOGIN = "请登陆之后再访问"; public static final String MESSAGE_SIRING_INVALIDATE = "字符串不合法!请不要传入空字符串"; public static final String ATTR_NAME_LOGIN_ADMIN = "loginAdmin"; public static final String MESSAGE_SYSTEM_ERROR_LOGIN_NOT_UNIQUE = "系统错误,登陆账号不唯一"; }
-
-
将一个引用类型的变量修饰为 final
-
此变量无法指向其他对象,但所值对象中的属性值还是可以被改变的
-
public class SuperClass { int num = 1234; } class SubClass { public static void main(String[] args) { final SuperClass sc = new SuperClass(); sc.num = 888; // 可以修改,修改的是sc指向的内存中的值 sc = new SuperClass(); // 编译报错,不可指向其他对象 } }
-
5. 抽象类
-
有时需要创建某个类代表一些基本行为,为其规范定义一些方法,却无法或者不适宜在该类具体实现,希望在其子类中根据实际情况去实现这些方法
-
定义了方法却没有定义具体实现的类为抽象类
- 通过 abstract 把一个类定义成抽象类
- 每一个未具体实现的方法也应该标识上 abstract,为抽象方法
-
抽象类需要注意的
-
抽象类是不能创建对象,必须生成抽象类的非抽象的子类,才可创建实例
-
一个抽象类中可以包含非抽象方法和成员变量
-
包含抽象方法的一定是抽象类,抽象类中的方法不一定全是抽象方法
-
若一个抽象类除了抽象方法啥都没有,使用接口更合适
-
6. 抽象类与final的特点
四. 接口
1.接口是什么
-
接口是体现抽象类功能的另一种方式,可以看成一个只有抽象方法的抽象类
-
规定一个类的基本形式,方法名,参数列表,返回值类型,但不规定方法体
-
Java不支持多重继承,但是允许Java一个类实现多个接口,实现了多重继承的能力,并且结构更加清晰
2.接口的定义
-
接口本身也具有数据成员变量和方法,但数据成员变量一定要赋初值,且值不能再更改
-
接口的定义中,允许省略定义 数据成员的 final 关键字 方法的public以及abstract关键字
-
接口的定义格式:
-
public interface SuperClass // 接口修饰符 interface关键字 接口名
-
3.接口的实现
-
在实现该接口的任何对象中,都能够调用此接口中的方法
-
一个类同时可以实现多个接口
-
实现接口需要用到 implements 关键字
-
实现了接口的类,必须将该接口中的所有方法,全部实现
-
格式如下:
-
class SubClass implements SuperClass { @Override public void put() { System.out.println(num); // 1234 已赋值的变量无法修改 } }
-
-
接口的好处
- 由于接口中的方法都是抽象方法,具体的实现在具体的类中完成
- 因此即使不同的接口有同名的方法,类的实例也不会混淆
-
与抽象类一样,不能直接由接口创建对象,必须通过实现接口的类来创建
- 使用接口名作为一个引用类型(编译时类型),该引用指向任何实现了该接口的实例(运行时类型)
- 使用时根据 动态绑定原则,视该变量所指向的具体实例来操作