JavaSE:继承和多态

36 篇文章 0 订阅
36 篇文章 0 订阅

目录

继承

1.为什么要用继承

我们来举一个例子:

就拿猫和狗去举例

单独创建一个猫类
在这里插入图片描述
再单独创建一个狗类
在这里插入图片描述
你会发现其中很多成员和方法是相同的
在这里插入图片描述
面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用

2.继承的概念

继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类
在这里插入图片描述
上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类或超类Dog和Cat可以称为Animal的子类/派生类,继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可

3.继承的语法

如果想要链接父子类,就需要关键字extends

语法格式

修饰符 class 子类 extends 父类 {
// ...
}

这样我们就可以再去单独创建一个Animal类让其他两个类去继承,然后删除Dog类和Cat类当中在Animal出现的成员和方法
在这里插入图片描述
注意:

  1. 子类会将父类中的成员变量或者成员方法继承到子类中了
  2. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了

4.父类与子类之间的访问关系

4.1子类中访问父类的成员变量

4.1.1子类和父类不存在同名对象的情况

在这里插入图片描述

4.1.2子类和父类成员变量名相同的情况

在这里插入图片描述
在子类方法中 或者 通过子类对象访问成员时:

  • 如果访问的成员变量子类中有,优先访问自己的成员变量
  • 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错
  • 如果访问的成员变量与父类中成员变量同名,不管类型成员变量的类型相不相同,优先访问自己的成员变量

成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找

4.2 子类中访问父类的成员方法

4.2.1 子类和父类成员方法名字不同的情况

在这里插入图片描述总结:成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错

4.2.2 子类和父类成员方法名字相同的情况

在这里插入图片描述
总结:

  • 通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到 则访问,否则编译报错
  • 通过子类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错
  • 通过子类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表完全相同(方法名,返回值,形参)则构成重写,子类访问该方法时,优先访问子类中的本方法

5.super关键字

那我们父类和子类出现了相同的成员变量名,如何去在子类中单独访问父类中指定的成员变量呢
Java提供了super关键字,该关键字主要作用:在子类方法中访问父类的成员。

class Base {
    int a;
    int b;
    public void methodA(){
        System.out.println("Base中的methodA()");
    }
    public void methodB(){
        System.out.println("Base中的methodB()");
    }
}
public class TestDemo extends Base{
    int a; // 与父类中成员变量同名且类型相同
    char b; // 与父类中成员变量同名但类型不同
    // 与父类中methodA()构成重载
    public void methodA(int a) {
        System.out.println("Derived中的method()方法");
    }
    // 与基类中methodB()构成重写(即原型一致,重写后序详细介绍)
    public void methodB(){
        System.out.println("Derived中的methodB()方法");
    }
    public void methodC() {
        // 对于同名的成员变量,直接访问时,访问的都是子类的
        a = 100; // 等价于: this.a = 100;
        b = 101; // 等价于: this.b = 101;
        // 注意:this是当前对象的引用
        
        // 访问父类的成员变量时,需要借助super关键字
        // super是获取到子类对象中从基类继承下来的部分
        super.a = 200;
        super.b = 201;
        
        // 父类和子类中构成重载的方法,直接可以通过参数列表区分清访问父类还是子类方法
        methodA(); // 没有传参,访问父类中的methodA()
        methodA(20); // 传递int参数,访问子类中的methodA(int)

        // 如果在子类中要访问重写的基类方法,则需要借助super关键字
        methodB(); // 直接访问,则永远访问到的都是子类中的methodA(),基类的无法访问到
        super.methodB(); // 访问基类的methodB()
    }
}

注意事项:

  1. 只能在非静态方法中使用
  2. 在子类方法中,访问父类的成员变量和方法

6.子类的构造方法

现有父再有子
子类对象构造时,需要先调用父类构造对象,然后再去执行子类构造对象

public class Base {
	public Base(){
		System.out.println("Base()");
	}
}


public class Derived extends Base{
public Derived(){
	// super(); // 注意子类构造方法中默认会调用基类的无参构造方法:super(),
	// 用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中第一条语句,
	// 并且只能出现一次
		System.out.println("Derived()");
	}
}


public class Test {
	public static void main(String[] args) {
		Derived d = new Derived();
	}
}

在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行父类的构造方法,然后执行子类的构造方法,因为:子类对象中成员是有两部分组成的,父类继承下来的以及子类新增加的部分 。父子父子肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整 。

在没有写子类的构造方法时默认是下面这样的:子类构造方法中,里面自动含有super();这个父类构造方法,创建子类对象时先执行子类构造方法中的super();中的父类构造方法,然后再去执行子类构造方法中的其他片段
在这里插入图片描述
但如果默认的super();中的类型和父类构造方法的形参对不上也会出现问题
在这里插入图片描述
需要修改成相对应的形参
在这里插入图片描述
this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造方法中出现在同一个构造方法,两者都需要写在构造方法的第一行会起冲突的
在这里插入图片描述

注意事项:

  1. 若父类定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用父类构造方法
  2. 如果父类构造方法是带有参数的,此时编译器不会再给子类生成默认的构造方法,此时需要用户为子类显式定义(写出)构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
  3. 在子类构造方法中,super(…)调用父类构造时,必须是子类构造函数中第一条语句。
  4. super(…)只能在子类构造方法中出现一次,并且不能和this同时出现

7.super和this

相同点:

  1. 都是Java中的关键字
  2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
  3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在

不同点:

  1. this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
  2. 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
  3. this是非静态成员方法的一个隐藏参数,super不是隐藏的参数
  4. 在构造方法中:this(…)用于调用本类构造方法,super(…)用于调用父类构造方法,两种调用不能同时在构造方法中出现
  5. 构造方法中一定会存在super(…)的调用,用户没有写编译器也会增加,但是this(…)用户不写则没有

8.回看初始化

在这里插入图片描述
在这里插入图片描述
注意事项:

  1. 静态代码块先执行,并且只执行一次,在类加载阶段执行
  2. 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行

通过分析执行结果,得出以下结论:

  1. 父类静态代码块优先于子类静态代码块执行,且是最早执行
  2. 父类实例代码块和父类构造方法紧接着执行
  3. 子类的实例代码块和子类构造方法紧接着再执行
  4. 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行

9.protected关键字

在前面我们介绍过privat default public,下面我们再去介绍一个关键字protected,protected意思是受保护的
下面是对这四个关键字特点的一个总结:
在这里插入图片描述

// 为了掩饰基类中不同访问权限在子类中的可见性,为了简单类B中就不设置成员方法了
// extend01包中
public class B {
	private int a;
	protected int b;
	public int c;
	int d;
}


// extend01包中
// 同一个包中的子类
public class D extends B{
	public void method(){
		// super.a = 10; // 编译报错,父类private成员在相同包子类中不可见
		super.b = 20; // 父类中protected成员在相同包子类中可以直接访问
		super.c = 30; // 父类中public成员在相同包子类中可以直接访问
		super.d = 40; // 父类中默认访问权限修饰的成员在相同包子类中可以直接访问
	}
}


// extend02包中
// 不同包中的子类
public class C extends B {
	public void method(){
		// super.a = 10; // 编译报错,父类中private成员在不同包子类中不可见
		super.b = 20; // 父类中protected修饰的成员在不同包子类中可以直接访问
		super.c = 30; // 父类中public修饰的成员在不同包子类中可以直接访问
		//super.d = 40; // 父类中默认访问权限修饰的成员在不同包子类中不能直接访问
	}
}



// extend02包中
// 不同包中的类
public class TestC {
	public static void main(String[] args) {
		C c = new C();
		c.method();
		// System.out.println(c.a); // 编译报错,父类中private成员在不同包其他类中不可见
		// System.out.println(c.b); // 父类中protected成员在不同包其他类中不能直接访问
		System.out.println(c.c); // 父类中public成员在不同包其他类中可以直接访问
		// System.out.println(c.d); // 父类中默认访问权限修饰的成员在不同包其他类中不能直接访问
		}
}

要注意被protected修饰的成员,在不同包的子类中需要用,直接创建父类的对象去访问是做不到的,只能去super.rotected修饰的成员或者直接写成员变量名去访问
在这里插入图片描述

10.什么时候用什么关键字呢?

什么时候下用哪一种呢

我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.
因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限 例如如果一个方法能用 private, 就尽量不要用 public
另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 还是更希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用).

11.Java的继承种类和方式

在这里插入图片描述
在这里插入图片描述
Java中的继承是不支持多继承的
在这里插入图片描述

12.继承与组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段

就比如说一台汽车

// 轮胎类
class Tire{
	// ...
}

// 发动机类
class Engine{
	// ...
}

// 车载系统类
class VehicleSystem{
	// ...
}

class Car{
	private Tire tire; // 可以复用轮胎中的属性和方法
	private Engine engine; // 可以复用发动机中的属性和方法
	private VehicleSystem vs; // 可以复用车载系统中的属性和方法
	// ...
}

// 奔驰是汽车
class Benz extend Car{
	// 将汽车中包含的:轮胎、发送机、车载系统全部继承下来
}

final

1.final修饰成员变量

final修饰的成员变量必须要初始化给其一个内容,不然会编译不过
在这里插入图片描述
其实可以通过输入值在编译运行后对final修饰的成员变量做最后一次输入
在这里插入图片描述

2.static final修饰成员变量

一个既是 static 又是 final 的字段只占据一段不能改变的存储空间,它必须在定义的时候进行赋值,否则编译器将不予通过
在这里插入图片描述
也无法使用输入或者一些不确定的随机值内容去赋值给static final修饰的成员变量
第一块代码说明final修饰的k值可以给随机值一个不确定的值
第二块说明static final修饰的值不能给一个随机的值
第三块说明static final和final修饰的值给定这种Random创建的确定值也是可以的
在这里插入图片描述

3.final在方法中修饰成员变量

也是需要赋初值的,不然编译无法通过
在这里插入图片描述
在这里插入图片描述

4.final修饰的成员方法

final修饰的成员方法表示该方法无法被重写

多态

1.多态是什么

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

2.多态的实现条件

在java中要实现多态,必须要满足如下几个条件,缺一不可:

  1. 必须在继承体系下

  2. 子类必须要对父类中方法进行重写

  3. 通过父类的引用调用重写的方法

    public class Animal {
    String name;
    int age;
    public Animal(String name, int age){
    this.name = name;
    this.age = age;
    }
    public void eat(){
    System.out.println(name + “吃饭”);
    }
    }

    public class Cat extends Animal{
    public Cat(String name, int age){
    super(name, age);
    }
    @Override
    public void eat(){
    System.out.println(name+“吃鱼~~~”);
    }
    }

    public class Dog extends Animal {
    public Dog(String name, int age){
    super(name, age);
    }
    @Override
    public void eat(){
    System.out.println(name+“吃骨头~~~”);
    }
    }

    ///分割线//

    public class TestAnimal {
    // 编译器在编译代码时,并不知道要调用Dog 还是 Cat 中eat的方法
    // 等程序运行起来后,形参a引用的具体对象确定后,才知道调用那个方法
    // 注意:此处的形参类型必须时父类类型才可以
    public static void eat(Animal a){
    a.eat();
    }
    public static void main(String[] args) {
    //第一种多态方式,传参不同
    Cat cat = new Cat(“喵喵”,2);
    Dog dog = new Dog(“汪汪”, 1);
    eat(cat);
    eat(dog);
    //第二种多态方式,引用对象为父类引用对象接收
    Animal animal1 = new Cat(“喵喵”,2);
    Animal animal2 = new Dog(“汪汪”, 1);
    animal1.eat();
    animal2.eat();

    }
    

    }

    //运行结果:
    //喵喵吃鱼~~~
    //汪汪吃骨头~~~
    //喵喵吃鱼~~~
    //汪汪吃骨头~~~

在上述代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的.
当类的调用者在编写 eat 这个方法的时候, 参数类型为 Animal (父类), 此时在该方法内部并不知道, 也不关注当前的a 引用指向的是哪个类型(哪个子类)的实例. 此时 a这个引用调用 eat方法可能会有多种不同的表现(和 a 引用的实例相关), 这种行为就称为多态.

3.重写

重写(override):也称为覆盖。
重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

3.1方法重写的规则:

  • 子类在重写父类的方法时,一般必须与父类方法原型一致:修饰符 返回值类型 方法名(参数列表) 要完全一致

  • 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
    在这里插入图片描述

  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected

  • 父类被static、private修饰的方法、构造方法都不能被重写 在这里插入图片描述

  • 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.

3.2重写与重载的区别

区别点

重载(override)

重写(override)

参数列表

必须修改

一定不能修改

返回值类型

可以修改

只能互为父子关系或者必须相同

访问限定符

可以修改

一定不能做更严格的限制(可以降低限制)

在这里插入图片描述

静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法,代表是方法重写

4.向上转型

向上转型:实际就是创建一个子类对象,将其当成父类对象来使用

语法格式:

父类类型 对象名 = new 子类类型()


Animal animal = new Cat("喵喵",2);

使用场景:

  1. 直接赋值

  2. 方法传参

  3. 方法返回

    public class TestAnimal {
    // 2. 方法传参:形参为父类型引用,可以接收任意子类的对象
    public static void eatFood(Animal a){
    a.eat();
    }

    // 3. 作返回值:返回任意子类对象
    public static Animal buyAnimal(String var){
    	if("狗" == var){
    		return new Dog("狗狗",1);
    	}else if("猫" == var){
    		return new Cat("猫猫", 1);
    	}else{
    		return null;
    	}
    

    }

    public static void main(String[] args) {
    	Animal cat = new Cat("喵喵",2); // 1. 直接赋值:子类对象赋值给父类对象
    	Dog dog = new Dog("汪汪", 1);
    	
    	eatFood(cat);
    	eatFood(dog);
    	
    	Animal animal = buyAnimal("狗");
    	animal.eat();
    	
    	animal = buyAnimal("猫");
    	animal.eat();
    }
    

    }

向上转型的优点:让代码实现更简单灵活
向上转型的缺陷:不能调用到子类特有的方法

5.向下转型

在这里插入图片描述

public class TestAnimal {
	public static void main(String[] args) {
		Cat cat = new Cat("喵咪",2);
		Dog dog = new Dog("汪汪", 1);

		// 向上转型
		Animal animal = cat;
		animal.eat();
		//打印:喵喵吃鱼~~~

		
		animal = dog;
		animal.eat();
		//汪汪吃骨头~~~


		// 编译失败,编译时编译器将animal当成Animal对象处理
		// 而Animal类中没有bark方法,因此编译失败
		animal.bark();

		// 向下转型
		// 程序可以通过编程,但运行时抛出异常---因为:animal实际指向的是狗
		// 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException
		cat = (Cat)animal;
		cat.mew();
		// animal本来指向的就是狗,因此将animal还原为狗也是安全的
		dog = (Dog)animal;
		dog.bark();
	}
}

向下转型需要向上转型完毕后再去向下转型,向下转型成功后,可以调用子类的对应方法了

向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了instanceof ,如果该表达式为true,则可以安全转换。

public class TestAnimal {
	public static void main(String[] args) {
		Cat cat = new Cat("喵喵",2);
		Dog dog = new Dog("汪汪", 1);


		// 向上转型
		Animal animal = cat;
		animal.eat();
		animal = dog;
		animal.eat();

		if(animal instanceof Cat){
			cat = (Cat)animal;
			cat.mew();
		}

		if(animal instanceof Dog){
			dog = (Dog)animal;
			dog.bark();
		}
	}
}

6.多态的优缺点

使用多态的好处:
1.避免使用大量的 if - else
在这里插入图片描述
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单
在这里插入图片描述
2.可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低

class Triangle extends Shape {
	@Override
	public void draw() {
		System.out.println("△");
	}
}

多态缺陷:
代码的运行效率降低

7. 避免在构造方法中调用重写的方法

一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func

class B {
	public B() {
		// do nothing
		func();//因为创建的是D所以调用的是D类的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();
	}
}


//打印结果
D.func() 0
  • 构造 D 对象的同时, 会调用 B 的构造方法.
  • B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
  • 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.

结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值