"越是不思考的人,越不愿倾听别人说话。"
作者:Mylvzi
文章主要内容:JavaSE学习之--继承和多态
目录
一.继承(inheritance)
1.为什么要有继承
2.继承的概念
3.继承的语法及代码实现
4.在子类中访问父类成员-->super关键字
1.子类中访问父类成员
11.不存在同名成员变量
1.2存在同名成员变量
1.3 那如何在子类中访问父类的成员呢?通过super关键字!
1,4子类的构造方法
注意事项:
1.super只能放在第一行,放在其他行就报错
2.如果父类的构造方法带参数,则在子类中调用父类的构造方法时也要带参数!(可以快速创建)
3.在子类的构造方法中super和this不能同时出现,且super只能出现一次
1.5super和this的比较
5.再谈初始化
6.protected关键字
1.同一个包不同类中成员的访问
2.不同包不同类中成员的访问
7.Java中的继承关系(三种)
8.final关键字
1.修饰变量会使变量不能被修改(成为常量)
2.修饰类,代表此类无法被继承
3.修饰方法,此方法无法被重写
9.继承与组合
二.多态
1.向上转型
1.向上转型的定义:
2.实现向上转型的三种方式
1.直接赋值
2.方法传参
3.方法返回
3.向上转型的优点:
4.向上转型的缺点:
2.向下转型:
3.动态绑定
4.方法重写
1.重写的规则:
2.重写与重载的区别:
5.多态
1.多态的优点:
1.降低圆圈复杂度
2.可拓展性强
2.多态的缺陷:代码运行效率低
3.多态的注意事项:
我们前面学过类,类是对现实事物的抽象化处理,而通过类实例化的对象则可以用来表示事物。在现实生活中,事物与事物之间有着千丝万缕的联系,最常见的一种就是“父子关系”,儿子会继承父亲的很多特征,而自己又有自己独特的特征;再比如狗和猫都是动物,都具有动物的一些属性,如年龄,性别,毛发颜色等等等等;为了在计算机中表示这种关系,创建了“继承”!
继承(inheritance):就是对事物共性的抽取,从而实现代码复用;
从和实际生活的联系来讲,继承就是将事物的共同属性抽取出来,用一个大类来表示,比如一个父亲有多个孩子,这些孩子都具有某些共性,如年龄,性别,将这些共同属性抽取出来,创建一个新类“Child”;
从代码实现来讲,继承关系的存在实现了代码的复用,子类继承了父类的属性和方法,大大提高效率;
补充:
其实Java中有很多这种“对共性进行抽取从而达到代码复用”的思路,比如前面学过的类变量,就是所有对象的共同属性的抽取
对象的共性-->静态成员变量
类的共性-->继承extends
// 父类
class Animal {
String name;
int age;
public void eat() {
System.out.println(name+"i am eating!!!");
}
public void sleep() {
System.out.println(name+"i am sleeping!!!");
}
}
// 子类
// 通过关键字extends表示此类继承于父类
class Dog extends Animal {
public void bark() {
System.out.println(name+"汪汪叫!!!");
}
}
class Cat extends Animal {
public void mew() {
System.out.println(name+"喵喵叫!!!");
}
}
public class Test {
public static void main(String[] args) {
Dog dog1 = new Dog();
dog1.name = "初一";
dog1.age = 3;
// dog1的成员变量中并没有name和age,但却能引用,则一定继承于父类
System.out.println(dog1.name);
System.out.println(dog1.age);
dog1.eat();
dog1.sleep();
dog1.bark();
}
}
总结:
1. 继承关键字-->extends,格式为:class 子类 extends 父类
2.子类会继承父类的成员和方法,实例化对象之后直接通过对象就能访问
3.子类继承父类的成员和方法之后,还需要有不同于父类的成员或方法(比如Dog类中的bark)
class Base {
int a;
int b;
public void method1() {
System.out.println("hello1");
}
}
class Derived extends Base {
int c;// 类特有的
public void method2() {
a = 30;// 访问父类继承下来的变量
b = 20;// 访问父类继承下来的变量
c = 10;// 访问子类自己的变量
}
class Base {
int a;
int b;
public void method1() {
System.out.println("hello1");
}
}
class Derived extends Base {
int a;// 和父类同名
int c;// 子类特有的
public void method2() {
a = 30;// 究竟是给父类赋值,还是给子类赋值?
b = 20;
c = 10;
}
1.3在子类中访问成员方法
class Base {
int a;
int b;
public void method1() {
System.out.println("hello1");
}
}
class Derived extends Base {
int c;
// 和父类的方法构成了重载
public void method1() {
System.out.println("hello2");
}
// 子类特有的方法
public void method2() {
System.out.println("hello2");
}
}
public class TestDemo1 {
public static void main(String[] args) {
Derived p1 = new Derived();
p1.method1();
p1.method2();
// 输出hello1 hello2
// 证明先执行子类的方法
// 既不属于子类,又不属于父类,编译失败
// p1.method3();
}
}
总结:
1.父类,子类存在同名的成员变量,成员方法,优先访问子类的,子类中没有再去父类中寻找,寻找不到就编译报错
2.子类和父类可构成方法的重载,将参数列表设置为不同,在调用时通过传参调用不同的方法!
由于设计不好或者特殊场景需要,需要在子类中访问父类成员,但如果出现重名编译器会优先访问子类中的,并不会访问父类中的成员变量,那该怎么解决呢?Java中提供了关键字super,该关键字的作用是-->在子类方法中访问父类成员变量
class Base {
int a;
int b;
public void method1() {
System.out.println("hello1");
}
}
class Derived extends Base {
int a;
int b;
int c;
public void method1() {
a = 10;// 等价于this.a
b = 20;// 等价于this.b
// 通过super关键字来访问父类成员
super.a = 30;
super.b = 40;
}
super访问方法:
class Derived extends Base {
int a;
int b;
int c;
public void method1() {
System.out.println("hello2 这是子类方法");
}
public void method2() {
a = 10;// 等价于this.a
b = 20;// 等价于this.b
// 通过super关键字来访问父类成员
super.a = 30;
super.b = 40;
method1();// 输出hello2
super.method1();// 输出hello1
总结:
1.super关键字的作用就是在子类方法中访问父类成员变量和方法!
2.super只能在子类的非静态方法中使用,因为super主要用来访问并实例化父类中的实例变量,而实例变量必须依赖于对象,静态方法不依赖于对象,所以super不能在子类的静态方法中使用! (super可以访问父类的类变量,但是不建议,更建议通过类名)
3.super只是关键字,提升了代码的可读性,并不代表对父类成员的引用!
构造方法是用来初始化成员变量的一种特殊方法,其在对象的创建过程中就会被调用,而且即使你不写构造方法,编译器会默认添加一个不带参数的构造方法,给你的成员变量赋初始值;在子类中,既有从父类继承过来的成员变量,又有自己独有的成员变量,那在构造方法中赋值的顺序有没有要求呢?答案是有的,先上结论:
必须先对父类成员变量进行初始化才能对子类成员变量进行初始化!也就是说在子类的构造方法中会先调用对父类的构造方法,再对子类的成员变量进行初始化。
class Base {
int a;
int b;
// 父类的构造方法
public Base() {
System.out.println("父类的构造方法!!!");
}
}
class Derived extends Base {
int a;
int b;
int c;
public Derived() {
// 会默认有一个不带参数的super()
// 代表父类的初始化
System.out.println("子类的构造方法!!!");
}
}
super()就代表调用父类的构造方法!!!
和this();的含义一样!!!
// 带参数的父类的构造方法
public Base(int a, int b) {
this.a = a;
this.b = b;
System.out.println("这是父类构造方法");
}
// 子类中的构造方法
// 必须先对父类成员变量进行初始化
public Derived(int a, int b, int a1, int b1, int c) {
super(a, b);// 必须放在第一行
// 且必须要传参数
this.a = a1;
this.b = b1;
this.c = c;
System.out.println("这是子类构造方法");
}
注意:如果父类提供的是一个显式的构造方法,则在子类中必须利用super进行传参!!!
如果父类的构造方法不带参数,在子类的构造方法中可以不写,因为系统会自动提供一个不带参数的super();
在构造方法那一节我们学过,对于一个类的构造方法,如果要使用this来调用当前类的其他构造方法,则this只能放在第一行,那如果我既想通过super来初始化父类的成员变量,又想通过this调用当前类的其他构造方法该怎么办?答案是没有办法,如果同时拥有会冲突,只能保证他们不能同时存在!
为什么只能出现一次呢?原因在于继承结构的逻辑链!在继承结构中,子类会先调用父类的构造方法,再执行自身的初始化逻辑,这种执行顺序是线性的,不需要多次进行父类的初始化!
我们学过super和this,发现他们有很多相同点,比如都是通过.操作符来访问变量或方法,那他们有哪些异同点呢?下面来总结一下:
相同点:
1.都是Java中的关键字。
2.都只能在非静态方法中访问非静态成员变量或非静态方法
3.在构造方法中调用时,都必须写在第一行,所以super和this不能同时使用;
不同点:
1.this代表当前对象的引用,用于访问当前对象的成员变量及方法;super是用来访问从父类继承的成员或方法。或者说super是引用对象从父类继承部分的引用
2.在构造方法中,super会有默认的对父类的构造方法(先有父后有子),而this并没有默认的构造方法。
3.this(......) 代表当前对象的构造方法,super(.......)代表父类的构造方法,两者不能同时出现
先回顾一下静态代码块,实例代码块,构造方法之间执行的先后顺序!
class Test {
int a;
// 构造方法
public Test() {
System.out.println("这是构造方法!!!");
}
// 静态块
static {
System.out.println("这是静态代码块!!!");
}
// 实例代码块
{
System.out.println("这是实例代码块!!!");
}
}
public class Test1 {
public static void main(String[] args) {
Test test1 = new Test();
System.out.println("=============");
Test test2= new Test();
}
}
执行结果:
总结:执行顺序-->静态代码块-->实例代码块-->构造方法
且静态代码块只会被执行一次
那如果既有父类的代码块和构造方法,又有子类的代码块和构造方法,执行顺序是怎样的呢?
先看代码:
class Test {
int a;
// 父类的构造方法
public Test() {
System.out.println("这是构造方法!!!");
}
// 父类的静态块
static {
System.out.println("这是静态代码块!!!");
}
// 父类的实例代码块
{
System.out.println("这是实例代码块!!!");
}
}
class DTest extends Test {
// 子类的构造方法
public DTest() {
System.out.println("这是子类的构造方法!!!");
}
// 子类的静态代码块
static {
System.out.println("这是子类的静态代码块!!!");
}
// 子类的代码块
{
System.out.println("这是子类的代码块!!!");
}
}
public class Test1 {
public static void main(String[] args) {
DTest test1 = new DTest();
System.out.println("=============");
DTest test2= new DTest();
}
}
执行结果:
执行顺序:
1.父类,子类的静态代码块
2.父类实例代码块,构造方法
3.子类的实例代码块,构造方法
所有的静态代码块无论实例化多少个对象都只会被执行一次!!!
为了实现封装特性,引入了访问修饰限定符,不同的关键字有不同的权限
protected是“受保护的”,它的权限是可以在同包不同类访问,也可以在不同包但是此包的类是另一个包中类的子类,也就是两个包之间的类存在继承关系!
class B {
private int a;
int b;
protected int c;
public int d;
}
class C extends B {
// 同一个包不同类
public void method() {
super.a = 20;// err,private成员不能跨类访问
super.b = 20;// 默认权限的成员可以跨类访问
super.c = 20;// protected成员可以跨类访问
super.d = 20;// public成员可以跨类访问
}
}
class B {
private int a;
int b;
protected int c;
public int d;
}
// 不同包但存在继承关系之间成员的访问
class C extends B {
public void method2() {
super.a = 20;// err private成员不能跨类访问
super.b = 20;// err 默认权限成员不能跨包访问
super.c = 20;// protected成员在子类中可以访问
super.d = 20;// public成员可以跨包访问
}
}
注意不要忘记引入父类的包:
总结:
1.继承是可以跨包的,如果另一个包中类是另一个包中的类的子类,则存在继承关系;子类会继承父类所有的成员和方法(但是不能全部访问,这取决于父类中成员或方法的访问权限)
2.我们有这么多的权限,那什么时候用什么权限呢?前期学习,大家都会使用一种粗暴的方法,把所有的成员设置为private,所有的方法设置为public,在前期对权限理解不深的时候我们可以这样设置,但使用哪个权限还是要具体问题具体分析,这取决于你对成员或方法的需求!
final关键字可以修饰变量,方法,类
final int b = 20;
b = 30;// err b是常量,无法修改常量的值
final class B {
private int a;
int b;
protected int c;
public int d;
}
// 此时B就无法被继承
class C extends B {
}
平常我们使用的String类就是被final修饰的
继承反映的是类与类之间的关系,同样的,组合 也是一种反应类与类关系的一种思想!
它代表一个类可以由多个类组成,比如一个Person类可以由Leg,Nose,Head类组成,不同于继承的是组合并没有关键字,只需在类中包含其他类就可以实现类的组合,请看下面代码:
class Nose{};
class Head{};
class Leg{};
class Person{
private Nose nose;// 可以复用Nose类中的方法和属性
private Head head;// 可以复用Head类中的方法和属性
private Leg leg;// 可以复用Leg类中的方法和属性
}
class Stu extends Person{
// 继承了人的鼻子,头,腿
};
多态就是通过父类引用不同对象,调用同一个重写的方法时,所表现的行为不同(所产生的结果不同),这种现象就叫做多态!
要理解多态,需要理解几个基本概念:向上转型,重写,向下转型,动态绑定
父类引用引用子类对象就叫做向上转型(子类对象被父类引用)
class Animal{};
class Dog extends Animal{};
class Cat extends Animal{};
public class Test {
public static void main(String[] args) {
// 向上转型
Animal animal1 = new Dog();
Animal animal2 = new Cat();
}
1.直接赋值
2.方法传参
3.方法返回
Animal animal1 = new Dog();
Animal animal2 = new Cat();
将方法形参设置为父类引用,实参是子类对象,通过调用方法实现将父类引用指向子类对象!
class Animal{
public void makeSound() {
System.out.println("动物发出叫声!");
}
};
class Dog extends Animal{
@Override
public void makeSound() {
System.out.println("小狗在汪汪叫!");
}
};
//class Cat extends Animal{};
public class Test {
public static void animalMakeSound(Animal animal) {
animal.makeSound();
}
public static void main(String[] args) {
Animal mydog = new Dog();
animalMakeSound(mydog);
}
在main方法中我们创建了一个Dog对象,并将其向上转型为Animal类型,接着在animalMakeSound方法中调用该对象,让父类引用指向该对象,在方法内部实现了向上转型!
通过上述代码我们可以看出,将形参设置为父类引用最大的好处就是增加了代码的通用性!因为实参可以是任意子类对象
方法的返回类型可以是类 ,通过将方法的返回值设置为父类引用,让返回的对象被父类引用引用!!!
// 父类
class Animal{
public void eat() {
System.out.println("eating");
}
};
class Cat extends Animal{
String name;
int age;
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void eat() {
System.out.println("cat is eating!");
}
}
class Dog extends Animal{
String name;
int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void eat() {
System.out.println("dog is eating!");
}
};
//class Cat extends Animal{};
public class Test {
public static void eat() {
System.out.println("我在吃饭");
}
// 父类引用可以做返回值,还是让父类引用指向返回的对象
public static Animal buyAnimal(String var) {
if("狗".equals(var)) {
return new Dog("小狗",2);
} else if ("猫".equals(var)) {
return new Cat("小米",20);
}else {
return null;
}
}
public static void main(String[] args) {
Animal animal1 =buyAnimal("狗");
Animal animal2 =buyAnimal("猫");
animal1.eat();
animal2.eat();
}
1.实现代码的通用性,实现多态性
通过父类引用子类对象,我们可以创建出更通用的代码,不需要去关注具体子类的类型
2.可以避免很多类型检查和强制类型转换
class Animal {
public void makeSound() {};
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵叫!");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪叫!");
}
}
public class Test1 {
public static void main(String[] args) {
Animal[] animals = new Animal[2];
animals[0] = new Cat();
animals[1] = new Dog();
for (Animal animal: animals) {
// 不适用向上转型
// 需要进行类型检查和强制类型转换
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.makeSound();
} else if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.makeSound();
}
}
// 使用向上转型
// 不需要关注具体的子类对象,代码更通用
for (Animal animal2: animals) {
animal2.makeSound();
}
}
}
对于这个代码,在 animals[0] = new Cat();实际上就发生了向上转型,animals[0]代表一个父类引用,new Cat()代表创建了一个新的Cat对象
无法使用子类特有的方法
如果想通过父类引用去使用子类特有的方法,就要进行“向下转型”,也就是使父类引用发生强制类型转换!
在很多时候子类有其特有的方法,而又不能通过重写来调用,这时候就需要通过向下转型来调用子类特有的方法,向下转型就是将父类引用强制类型转化为子类对象
class Animal{};
class Dog extends Animal{
public void bark() {
System.out.println("汪汪叫!");
}
}
public class testdemo1 {
public static void main(String[] args) {
Animal animal1 = new Dog();
// 向下转型
Dog dog1 = (Dog)animal1;
dog1.bark();// 输出汪汪叫!
}
}
但向下转型是一个很“危险”的操作,因为父类引用是一个较大的范围,你强制转化的类型是一个小范围,会出现意想不到的错误;同时,如果多个类继承于一个父类,要注意向下转型时不要转错类型!如果转换错误,编译器会报错:
所以,为了避免这种错误,需要利用关键字instanceof来检验当前引用所指向的类型
class Animal{};
class Dog extends Animal{};
class Cat extends Animal{};
public class Test {
public static void main(String[] args) {
// 向上转型
Animal animal1 = new Dog();
// 父类引用 引用子类类型
if(animal1 instanceof Dog) {// 利用instanceof运算符进行引用类型检查
// 强制类型转化
Dog dog = (Dog)animal1;
System.out.println("类型检查成功");
}else {
System.out.println("ClassCastException!!!");
}
}
}
动态绑定是面向对象编程非常重要的一个概念,也被叫做运行时绑定或运行多态性。他是指在运行时才确定对象的实际类型,并根据实际类型调用相应的方法,在通俗一点说就是只有在运行时我才知道引用的对象是谁,我该调用的方法是谁,这之前谁都不知道!!!
class Animal {
public void makeSound() {};
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵叫!");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪叫!");
}
}
public class Test1 {
public static void main(String[] args) {
Animal animal = new Dog();// 动态绑定
animal.makeSound();// 实际调用的是Dog的方法 只有在运行时才知道实际对象是Dog
}
}
重写(override):是子类对父类非static,final,private修饰,非构造方法的方法重写;通过重写,可以在子类内部实现属于子类的行为;
1.被重写的父类方法不能被static,final,private修饰,不能是构造方法
2.重写的方法必须要和父类方法保持一致,即返回值,方法名,参数列表都要完全相同
3.子类的重写方法的权限要>=父类被重写方法的权限,比如父类方法的权限是public,那子类重写的方法就不能是protected!
4.子类中重写的方法可以被@Override 注解来显式指定,有了这个注解,可以多一层检验,比如父类的方法名是eat,而在子类中却写成ate,此时编译器就会报错(因为要求子类的方法名要和父类被重写方法的名称一致!)
重载是类方法多态性的体现,重写是子类与父类之间多态性的体现
方法重载是一种静态绑定,即在编译过程中就知道对应的方法(根据用户的传参)
方法重写是一种动态绑定,只有在运行时才能明确调用的方法
再说回多态,以上内容都是为了理解多态而阐述的,所谓多态就是子类调用同一个父类方法时所产生的行为不同的思想,以下是多态的条件:
1.必须存在父类与子类的继承关系
2.必须有方法的重写
3.必须通过父类的引用来调用方法
多态的体现:在程序运行时,引用对象的不同,所产生的结果也不同!
圆圈复杂度是反应一段代码理解难易程度的表示,如果一段代码平铺直叙,那他的圆圈复杂度就很低,此代码易于理解,但如果代码中含有大量的条件语句,循环语句,那么此代码的圆圈复杂度就很高,不易于理解(要判断的东西太多)
class Shape {
public void drawMap() {};
}
class Rect extends Shape {
@Override
public void drawMap() {
System.out.println("矩形");
}
}
class Flower extends Shape {
@Override
public void drawMap() {
System.out.println("❀");
}
}
class Cycle extends Shape {
@Override
public void drawMap() {
System.out.println("⚪");
}
}
public class Test2 {
// 依次打印⚪矩形⚪矩形花
public static void main(String[] args) {
// 1.不使用多态 要判断子类的具体类型
Rect rect = new Rect();
Flower flower = new Flower();
Cycle cycle = new Cycle();
String[] shapes = {"cycle","rect","cycle","rect","flower"};
for (String shape:shapes) {
if(shape.equals("cycle")) {
cycle.drawMap();
} else if (shape.equals("rect")) {
rect.drawMap();
}else {
flower.drawMap();
}
}
// 2.使用多态
Shape[] shapes = {new Cycle(),new Rect(),new Cycle(),new Rect(),new Flower()};
for (Shape shape: shapes) {
shape.drawMap();
}
}
}
比如对于shape类,现在有一个新的子类Triangle,可以直接新创建一个Triangle类,让其包含drawMap方法;如果不使用多态,则需要在打印时多添加一个if-else语句
1.属性不具有多态性,通过父类引用只能访问到父类的属性,而无法访问到子类的属性
2.构造方法没有多态性
先来看如下代码:
class B {
public B() {
// do nothing
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 Test3 {
public static void main(String[] args) {
D d = new D();
}
}
// 执行结果:D.func() 0
实例化d对象时,会先调用父类的构造方法,父类的构造方法中调用了func方法,此时会触发动态绑定,调用子类的func方法,而此时 private int num = 1;这段代码并未被执行,也就是说num还未被初始化,未被初始化则被赋值为0,所以打印:D.func() 0
结论: "用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触 发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.