目录
1.继承
1.1为什么需要继承
在面向对象的时候,对象之间会存在一些关联,我们在设计程序的时候需要考虑。继承就是专门用来进行共性抽取,实现代码的简化和复用
继承允许程序员在原有类(父类/基类)特性的基础上进行扩展,增加新功能从而产生新的类(子类/派生类)
1.2继承的语法
修饰符 class 子类 extends 父类 {
}
子类会将父类中的成员变量和成员方法继承到子类中
1.3 父类成员的访问
子类成员将父类的成员变量和成员方法继承下来后,子类中能否直接访问父类继承下来的成员呢
1.3.1 子类中访问父类的成员变量
子类和父类不存在同名的成员变量
class Base {
int a;
}
class Derived extends Base {
int c;
public void meth0d() {//JAVA所有的除定义或声明语句之外的
//任何语句都必须在方法内部
// (而方法又必须在某个类内部,不存在游离于类外部的方法或成员)
a = 10;//访问从父类中继承下来的成员变量a
c = 30;//访问子类自己的c
}
}
子类和父类存在同名的成员变量
此时如果访问的成员变量子类有同名的,则优先访问子类的成员变量。
成员变量的访问遵循就近原则,自己有则优先自己的,如果没有再向父类中查找
1.3.2 子类中访问父类的成员方法
同上,通过子类对象访问父类和子类不同方法名时,优先在子类中查找,找到就访问子类自己的。没在子类中找到再去父类中寻找
通过派生类创建的对象访问父类与子类同名的方法时,如果子类和父类的同名方法参数列表不同(意思是两个同名方法构成了重载),则会根据调用方式时传递的参数选择合适的方法。
当子类和父类中存在相同的成员时,如何在子类中访问父类呢
1.4 super关键字
子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,直接访问会变成访问子类的成员。Java提供了super关键字,通过该关键字,就能在子类中访问父类的成员
注意:super只能在非静态方法中使用。只能在子类的方法中,去访问父类的成员变量和方法。super的其他用法在后文介绍
1.5 子类的构造方法
子类对象构造时,需要先调用父类构造方法,然后再执行子类自己的构造方法。
子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分。在构造对象的时候,会先调用父类的构造方法,将从父类继承下来的那部分成员变量初始化,然后再调用子类自己的构造方法,将子类自己新增加的成员变量初始化。
注意
- 若父类显示定义无参或者默认的构造方法,在子类构造方法第一行默认隐藏super()调用,即调用基类的构造方法
- 如果父类构造方法是带有参数的,此时需要用户自己在子类显式定义构造方法,并且在子类构造方法中选择合适的父类构造方法调用,否则会报错
- 在子类构造方法中,super(…)只能在子类构造方法的第一条语句(注释不算做语句,第一条不一定非要是第一行)
- super(…)只能在子类构造方法中出现一次,并且不能和this同时出现
1.6 super和this
上文提到了suoer和this,这两个都可以在成员方法中访问成员变量 和 调用其他成员方法,都可以作为构造方法的第一条语句,那么他们有什么异同呢
相同点
- 只能在类中的非静态方法中使用,用来访问非静态成员方法和成员变量
- 在构造方法中调用时,必须是构造方法中的第一条语句,并且不可以同时出现
不同点
this是对当前对象的引用,当前对象就是调用实例方法的对象(静态方法不可以被调用的)。super是子类对象中从父类继承下来那一部分成员的引用
在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
在构造方法中:this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造方法中出现
构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加(只不过有时候不可见),但是this(…)用户不写则没有
1.7 执行顺序
在继承关系上,静态代码块,实例代码块和构造方法的执行顺序又是哪样的呢
结论
- 父类静态代码块优先于子类的静态代码块执行,且是最高执行
- 父类实例代码块和父类构造方法紧接着执行
- 子类的实例代码块和子类构造方法紧接着再执行
- 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
class Father {
String name;
public Father(String name) {//父类的带一个参数的构造方法
this.name = name;
System.out.println("父类的构造方法");
}
{
System.out.println("父类实例代码块");
}
static {
System.out.println("父类静态代码块");
}
}
class Son extends Father {
public Son(String name) {//父类构造方法带参数
//需要在子类显式定义构造方法,并且在子类构造方法中
// 选择那个带一个参数的父类构造方法调用
super(name);
}
{
System.out.println("子类的实例代码块");
}
static {
System.out.println("子类的静态代码块");
}
}
public class Test {
public static void main(String[] args) {
Son son1 = new Son("张三");
Son son2 = new Son("李四");
}
1.8 继承方式
Java是不支持多继承的,在真实项目中类会有很多,类之间的关系也会更加复杂,即便如此一般也不希望出现三层以上的继承关系。Java中也有解决多继承问题的方法,在下一篇接口中会介绍
1.9 final关键字
final关键字可以用来修饰变量、方法以及类
- 修饰变量或字段,即变为常量
- 修饰类:表示此类无法被继承
- 修饰方法:表示该方法无法被重写
1.10 继承与组合
组合也是表示类与类之间关系的方式,并没有特殊语法,仅仅是将一个类的实例化对象作为另一个类的成员变量
继承表示 is-a 的关系:狗是动物
组合表示 has-a 的关系:汽车有轮胎
class Tyre {
}
class Car {
private Tyre tyre;//组合
}
2 多态
2.1 多态的概念
同一个行为,当不同的对象去完成时会产生不同的状态
2.2 多态的实现条件
- 必须在继承体系下,向上转型
- 子类必须对父类中的方法进行重写
- 通过父类对象的引用调用重写的方法
完成以上三步,就会发生动态绑定,动态绑定是多态的基础
多态体现:在代码运行时,当传递不同类创建的对象时,会调用对应类中的方法
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(this.name+"吃饭");
}
}
class Dog extends Animal{
public Dog(String name) {
super(name);//子类对象构造时,需要先调用父类构造方法
}
@Override
public void eat() {//子类必须对父类中的方法进行重写
System.out.println(this.name+"吃狗粮");
}
}
public class Test {
public static void eat(Animal a) {
a.eat();//通过父类Animal对象的引用a
//调用子类Dog中重写的方法
}
原则上父类只能调用自己的方法,之所以表现出来是子类的方法,是因为发生了动态绑定
2.3 重写
- 重写是子类对父类中非static,非private,非final,非构造方法等实现过程的重新编定
- 子类重写父类方法时,返回值类型 方法名(参数列表)必需与父类完全一致
- 访问权限必须大于等于父类中被重写的方法的访问权限。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected重写的方法
- 可以使用 @Override 注解来显式指定. 有了这个注解编译器能帮我们进行一些合法性校验. 例如方法名写错了编译器会报错
重写与重载的区别
方法重载是一个类的多态性,方法重写则是子类与父类的一种多态性
两种绑定
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用哪个方法。典型代表函数重载。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法
2.4向上转型和向下转型
2.4.1向上转型
实际就是创建一个子类对象,将其当成父类对象来使用。让父类对象的引用指向子类对象
实现方式:
- 直接赋值
- 方法传参
- 方法返回
public class Test {
//3.作返回值
public static Animal buyAnimal(String var) {//这里返回类型是Animal
if("狗".equals(var)) {
return new Dog("狗狗");//返回一个子类对象,用父类接收
}else {
return null;
}
}
public static void eatFood(Animal a) {//2.方法传参:用父类对象的引用去指向子类的对象
a.eat();
}
Animal dog = new Dog("小七");//1.直接赋值
}
优缺点:
可以让代码更加接单灵活,但是向上转型后就不能调用子类特有的方法了
2.4.2向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法
此时:将父类引用再还原为子类对象即可,即向下转换。使用强制类型转换实现
public static void main(String[] args) {
Dog dog = new Dog("小七");
Animal animal = dog;//向上转型
dog = (Dog)animal;//向上转型后向下转型
dog.bark();//向下转型后就可以通过dog去调用
//Dog类中特有的方法了
}
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。
这里将animal强转为Cat的时候不会报错,但是运行的时候会报错,无法将原来向上转型指向Dog的animal强转成Cat类,所以说向下转型非常不安全
Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。
if(animal instanceof Cat) {
cat = (Cat)animal;
cat.miaomiao();
}
加入instanceof之后,运行时就不会报错,强转不会执行,当然miaomiao方法也不会被调用
2.5 多态的优缺点
优点
- 可以降低代码的圈复杂度,避免使用大量的if-else
- 可扩展能力更强
拓展:什么是圈复杂度
圈复杂度(Cyclomatic Complexity)是一种代码复杂度的衡量标准,由 Thomas McCabe 于 1976年定义。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。圈复杂度大说明程序代码的判断逻辑复杂,可能质量低且难于测试和维护。程序的可能错误和高的圈复杂度有着很大关系。圈复杂度越低越好
缺陷
- 成员变量没有多态性,当父类和子类都由同名属性的时候,通过父类引用,只能引用父类自己的成员属性
- 构造方法没有多态性
2.6避免在构造方法中调用重写的方法
class A {
public A() {
fun();
}
public void fun() {
System.out.println("A.fun");
}
}
class B extends A {
private int num = 1;
public void fun() {
System.out.println(this.num+"Bfun");
}
}
public class Test {
public static void main(String[] args) {
B b = new B();//运行结果为0Bfun
}
}
这里的运行结果为什么是0Bfun:
- 我们在new一个B类对象的时候,因为B是A的子类,所以需要先调用A类的构造方法才行
- 但是在A类的构造方法中我们调用了fun函数,这个fun方法并不会用A类自己写的,那是因为此时发生了动态绑定,调用了B类中的fun函数
- 在B类的fun函数包含this.num,这里num为零的原因是B类还为进行初始化(因为这些是发生在父类的构造方法,子类压根还没开始构造),num的默认值为0,所以输出0Bfun
综上:避免在构造方法中调用的方法
除了:Java构造方法之间通过 this 关键字进行调用