目录
1 继承
1.1为什么需要继承
1.2 继承概念
1.3 继承的语法
1.4 父类成员访问
1.4.1 子类中访问父类的成员变量
1.4.2 子类中访问父类的成员方法
1.5 super关键字
1.6 子类构造方法
1.7 super和this
1.7.1 this
1.7.2 super和this
1.8 再谈初始化
1.9 继承方式
1.10 继承与组合
2 多态
2.1 多态的概念
2.2 多态实现条件
2.3 重写
2.4 向上转移和向下转型
2.4.1 向上转型
2.4.2 向下转型
2.5 多态的优缺点
2.6 避免在构造方法中调用重写的方法
Java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体,但是现实世界错综复杂,事物之间可能会存在一些关联,那在设计程序是就需要考虑。
比如:狗和猫,它们都是一个动物
那我们在对猫和狗的各种动作进行描述时,利用Java,就会设计出这样的代码
发现了吗,这里面有很多重复的地方
那能否将这些共性抽取呢?面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。(共性的抽取,达到代码的复用)
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。
例如:上面的代码,狗和猫都是动物,那么我们就可以将共性的内容进行抽取,然后采用继承的思想来达到共用
图解:
上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类或超类,Dog和Cat可以称为Animal的子类/派生类,继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可
从继承概念中可以看出继承最大的作用就是:实现代码复用
在Java中如果要表示类之间的继承关系,需要借助extends关键字,具体如下:
修饰符 class 子类 extends 父类{
}
实操 :(将上面的代码用继承方法重新设计)
上面的代码:
重新设计的:
然后我们测试一下 :
运行:
注意:1.没有赋值的话,默认放null或0
2.子类会将父类中的成员变量或者成员方法继承到子类中了
3. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
在继承体系中,子类将父类中的方法和字段继承下来了,那在子类中如何访问父类中继承下来的成员呢?
1. 子类和父类不存在同名成员变量
2. 子类和父类成员变量同名
总结:由上诉可知
- 如果访问的成员变量子类中有,优先访问自己的成员变量。
- 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
- 如果访问的成员变量与父类中成员变量同名(与类型无关,同名都先访问自己),则优先访问自己的
1. 成员方法名字不同
总结:成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错。
总结:
- 通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。
- 通过子类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法传递的参数选择合适的方法访问,如果没有则报错;
由于设计不好,或者因场景需要,子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,直接访问是无法做到的,Java提供了super关键字,该关键字主要作用:在子类方法中访问父类的成员。
public class Base {
int a;
int b;
public void methodA(){
System.out.println("Base中的methodA方法");
}
public void methodB() {
System.out.println("Base中的methodB方法");
}
}
public class Derived extends Base{
int a; // 与父类中成员变量同名且类型相同
char b; // 与父类中成员变量同名但类型不同
//与父类中的methodA构成重载
public void methodA(int a){
System.out.println("Derived中的methodA方法");
}
//与父类中的methodB构成重写
public void methodB() {
System.out.println("Derived中的methodB方法");
}
public void methodC(){
// 对于同名的成员变量,直接访问时,访问的都是子类的
a = 10;//相当于 this.a = 10
b = '1';//相当于 this.b = '1'
// 访问父类的成员变量时,需要借助super关键字
// super是获取到子类对象中从基类继承下来的部分
super.a = 3;
super.b = 4;
// 父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法
methodA(); // 没有传参,访问父类中的methodA()
methodA(20); // 传递int参数,访问子类中的methodA(int)
// 如果在子类中要访问重写的基类方法,则需要借助super关键字
methodB(); // 直接访问,则永远访问到的都是子类中的methodA(),基类的无法访问到
super.methodB(); // 访问基类的methodB()
}
}
简单来说就是,想要明确访问父类中的成员,要使用 super 关键字
注意:
- super只能在非静态方法中使用
- super是在子类方法中,访问父类的成员变量和方法。
ps:那super和this的区别是什么?
- this可以访问父类,也可以访问子类
- this优先访问子类(有同名时)
- super只能访问从父类继承下来的成员变量
- 当前类使用了super,那当前类一定是子类
子类对象构造时,需要先调用父类构造方法,然后执行子类的构造方法。
- 当子类继承了父类之后,一定要先帮助父类构造他的构造方法,然后再构造子类自己的构造方法(因为子类对象中成员是有两部分组成的,父类继承下来的以及子类新增加的部分)
- 父类的构造方法只能在子类中调用
代码演示:
初学小贴士:构造方法不需要通过d.来访问,只要实例化对象就行
(当调用完相应的构造方法之后,实例化对象才产生,也就是说你创建对象的时候即使不写构造方法,也会有一个默认的空构造方法)
注意:
- 子类构造方法中默认会调用父类的无参构造方法:super(),用户没有写时,编译器会自动添加,而且super(..)必须是子类构造方法中第一条语句,并且只能出现一次,并且不能和this()同时出现
- 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
代码演示:
父类的构造方法有参数,子类没有,编译失败,如何做看下面吧
在调用构造方法时,super(..)与this()不能同时出现,因为他们俩都只能在第一行
再说super和this之前我先来介绍一下this
那函数体中到底是谁给谁赋值?形参给成员变量?还是成员变量给形参?傻傻分不清楚
看看这样会打印出什么?
这不是我们想要的,这时候就要用this了,
我们可以使用"this.属性"或"this.方法"的方式,调用当前正在运行的对象属性或方法。
如果方法的形参和类的属性同名时,我们必须显式的使用"this.变量"的方式,表明此变量是属性(类中的属性),而非形参。
总结:
- this表示当前对象的引用,不能再引用其他对象(成员方法运行时调用该成员方法的对象)
- this只能使用在非静态方法(又称实例方法,或成员方法)中。谁调用这个实例方法,this就是谁。所以this代表的是:当前对象。
- this不能在静态方法中使用, 因为this代表当前对象,静态方法中不存在当前对象。
ps:静态方法静态方法属于类,可以在实例化之前被类直接调用
2.可以通过this调用其他构造方法来简化代码 (语法this();)
- this();调用当前类当中的其他构造方法(看匹配到那个,就调用那个)
- 只能在当前的构造方法内部来使用
- 只能在第一行
- < 相同点>
1. 都是Java中的关键字
2. 只能在类的非静态方法中使用,用来访问非静态成员的方法和字段
3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
- <不同点>
1. this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
2. 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
3. 在构造方法中:this(...)用于调用本类中的其他构造方法,super(...)用于调用父类构造方法,两种调用不能同时在构造方法中出现
4. 构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,但是this(...)用户不写则没有
1、父类静态代码块优先于子类静态代码块执行,且是最早执行
2、父类实例代码块和父类构造方法紧接着执行
3、子类的实例代码块和子类构造方法紧接着再执行
4、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
Java中只支持以下几种继承方式:
1.单继承 public class A{ }public class B extends A{ }2.多层继承 public class A{ }public class B extends A{ }public class C extends B{
}
3.不同类继承同一个类 public class A{ }public class B extends A{ }public class C extends A{
}
注意:
- Java中不支持多继承。
(public class A{ } public class B{ } public class C extends A,B )- 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了.
- 如果想从语法上进行限制继承, 就可以使用 final 关键字
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。
- 继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物
- 组合表示对象之间是has-a的关系,比如:汽车
ps: 组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,
一般建议:能用组合尽量用组合
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态
同一件事情,发生在不同对象身上,就会产生不同的结果。
在java中要实现多态,必须要满足如下几个条件,缺一不可:
1. 必须在继承体系下
2. 子类必须要对父类中方法进行重写
3. 通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
重写:也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变,名字也要相同。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。
方法重写的规则:
- 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
- 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
- 访问权限不能比父类中被重写的方法的访问权限更低。(private < 默认权限 < protected < public)
- 父类被static、private修饰的方法、构造方法都不能被重写。
- 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.
重写重载区别 :
区别点 | 重写(override) | 重载(override) |
参数列表 | 一定不能修改 | 必须修改 |
返回类型 | 一定不能修改【除非可以构成父子类关系】 | 可以修改 |
访问限定符 | 一定不能做更严格的限制(可以降低限制) | 可以修改 |
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
1. 直接赋值
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
动态绑定 :也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法
在编译的时候确实认为应该调用Animal的eat方法。但是运行的时候发现,子类重写父类的这个
eat方法,所以会直接调用子类
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
我们看下面的代码报错了,因为这样调用的是父类的fly()方法,但是父类中有没有fly()方法,当然报错(上面的可以调用子类中的eat()方法,是因为父类中本来就有eat()方法,但是子类中又重写了eat方法,调用时发生了动态绑定,所以调用了子类的方法)
此时就利用了向下转型,但是不安全
你往下看
报了一个错误: Dog cannot be cast to Bird
Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换,是这样用的,如下:
比如我们想打印【"●", "♦", "●", "♦", "❀"】, 那我们利用多态思想就会写出这样的代码
public class Shape {
public void draw() {
System.out.println("画图形!");
}
}
class Rect extends Shape{
@Override
public void draw() {
System.out.print("♦ ");
}
}
class Cycle extends Shape{
@Override
public void draw() {
System.out.print("● ");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.print("❀ ");
}
}
利用多态 :(动态绑定,向上转型)
public class Test {
public static void drawMap(Shape shape){
shape.draw();
}
public static void main(String[] args) {
Cycle cycle = new Cycle();
Rect rect = new Rect();
Flower flower = new Flower();
Shape[] shapes = {cycle, rect, cycle, rect, flower};
for(int i = 0; i < shapes.length; i++){
drawMap(shapes[i]);
}
}
}
如果没有多态就会这样:(很多if else 语句)
public class Test2 {
public static void main(String[] args) {
Cycle cycle = new Cycle();
Rect rect = new Rect();
Flower flower = new Flower();
String[] shape = {"cycle", "rect", "cycle", "rect", "flower"};
for(int i = 0; i < shape.length; i++){
if (shape[i].equals("cycle")) {
cycle.draw();
} else if (shape[i].equals("rect")) {
rect.draw();
} else if (shape[i].equals("flower")) {
flower.draw();
}
}
}
}
总结
多态优点:
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
2. 可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
多态缺陷:代码的运行效率降低
一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func() 方法. 并且在 B 的构造方法中调用 func()
public class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
运行结果:
是0,可是我们的num明明被初始化赋值成了1 ,因为此时D这个对象还没有构造完成
所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
结论: "用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.
╰(*°▽°*)╯╰(*°▽°*)╯╰(*°▽°*)╯╰(*°▽°*)╯╰(*°▽°*)╯完╰(*°▽°*)╯╰(*°▽°*)╯╰(*°▽°*)╯╰(*°▽°*)╯╰(*°▽°*)╯