Java入门part6--继承和多态

继承


// Animal.java
public class Animal {
	public String name;
	public Animal(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println(this.name + "is eating" + food);
	}
}
// Cat.java
class Cat {
	public String name;
	public Cat(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println(this.name + "is eating" + food);
	}
	public void jump() {
		System.out.println(this.name + "is jumping");
	}
}
// Bird.java
class Bird {
	public String name;
	public Bird(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println(this.name + "is eating" + food);
	}
	public void fly() {
		System.out.println(this.name + "is flying");
	}
}

观察可见:

  • Animal,Cat,Bird三个类都有相同的方法eat(),且实现的功能一样
  • 三个类都具有同样的属性name
  • 从逻辑上讲,Cat,Bird都是Animal的一种(他们之间是is-a关系)

所以我们可以让Cat,Bird继承Animal类,实现代码复用本质上来讲继承就是为了代码的复用

继承的语法规则

extends

用关键字extends来实现继承,

class 子类/派生类 extends 父类/基类/超类{

}

(像Cat,Bird这种类就叫做子类/派生类,而Animal这种被继承的类叫做父类/基类/超类)

注意:

  • 使用 extends 指定父类.
  • Java 是单继承,也就是说一个子类只能继承一个父类 (而C++/Python等语言支持多继承).
  • 子类会继承父类的所有 public 的字段和方法.
  • 对于父类的 private 的字段和方法, 子类中是无法访问的.
  • 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用.

super

super()//调用父类的构造方法 必须放在第一行   因为要构造子类要先构造父类
super.func();//调用父类的方法func()
super.data;  //调用父类的数据成员data

父类只能访问自己的成员 或者是方法
但子类可以通过关键字super访问父类的成员和方法
所以我们可以将Animal,Cat,Bird的代码优化如下:

// Animal.java
public class Animal {
	public String name;
	public Animal(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println(this.name + "is eating" + food);
	}
}
// Cat.java
class Cat extends Animal{
	public Cat(String name) {
		super(name);
	}
	public void jump() {
		System.out.println(this.name + "is jumping");
	}
}
// Bird.java
class Bird extends Animal{
	public Bird(String name) {
		super(name);
	}
	public void fly() {
		System.out.println(this.name + "is flying");
	}
}

此时只需将Cat,Bird类中Animal有的字段和方法删除即可
注意:
在子类的构造方法中一定要先用super()构造父类,构造完后再构造子类自己

但是此时如果想要实现封装,将父类的name属性变为private,那么编译就会出错,因为子类无法访问private修饰的方法和字段,此时应该如何实现封装呢?

protected关键字

使用protected关键字就很好的解决了这个问题:

  • 对于类的调用者来说,并不能访问protected修饰的字段和方法
  • 但对于子类来说,protected所修饰的字段和方法是可以访问的

此处拓展几个权限修饰关键字的修饰范围:(default是什么关键字都不加)

范围 private default protected public
同一包中同一类
同一包中不同类 ×
不同包中的子类 × ×
不同包中非子类 × × ×

注意:
final所修饰的类不可被继承

注意区分继承和组合

  • 继承是一种is-a关系
  • 组合是一种has-a关系,是一种包含关系

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.比如:

class Student{
...
}
class Teacher{
...
}
class School{
	public Student[] students;
	public Teacher[] teacher;
	...
}

多态


向上转型

即将子类的值赋值给父类 /父类引用子类对象,比如:

Cat cat = new Cat("大不妞");

可以写成

Animal cat = new Cat("大不妞");

此时 cat 是父类 (Animal) 的引用, 指向子类 (Cat) 的实例. 这种写法称为 向上转型

向上转型发生的时机 :

  • 直接赋值
  • 方法传参
  • 方法返回

上面列举的是直接赋值,方法传参和方法返回见下:

方法传参的形式()

public class Test {
	public static void main(String[] args) {
		Cat cat = new Cat("大不妞");
		feed(cat);
	}
	public static void feed(Animal animal) {
		animal.eat(" fish");
	}
}

方法返回

public class Test {
	public static void main(String[] args) {
		Animal animal = findMyAnimal();
	}
	public static Animal findMyAnimal() {
		Cat cat = new Cat("大不妞");
		return cat;
	}
}

动态绑定

将前面示例的代码稍作改动,让子类Cat和Animal有一个同名但实现功能不同的方法eat();

// Animal.java
public class Animal {
	public String name;
	public Animal(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println("我是一只小动物");
		System.out.println(this.name + "is eating" + food);
	}
}
// Cat.java
class Cat extends Animal{
	public Cat(String name) {
		super(name);
	}
	public void eat(String food) {
		System.out.println("我是一只小猫咪");
		System.out.println(this.name + "is eating" + food);
	}
}

class Demo0223 {
    public static void main(String[] args) {
        Animal animal1 = new Animal("大不妞");
        animal1.eat(" fish");

        Animal animal2 = new Cat("大不妞");
        animal2.eat(" fish");
    }
}

执行结果:
在这里插入图片描述
此时, 我们发现:

  • animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, 而animal2 指向的是Cat 类型的实例.
  • animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而animal2.eat() 实际调用了子类的方法.

由此可得:
在 Java 中, 子类和父类拥有同名方法,此时调用该方法时究竟执行的是子类的方法还是父类的方法 , 要看究竟这个引用指向的是子类对象还是父类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定

反汇编发现程序编译时确实调用的是父类的方法 但是运行时却调用的子类的方法 这就是运行时绑定(也叫动态绑定),这就是所谓的 编译看左,运行看右

方法重写(override)

像是上述代码当中的eat():
子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 方法覆写/重写/覆盖(Override)

方法重写的注意事项:

  1. 普通方法可以重写, static 修饰的静态方法不能重写
  2. 重写中子类的方法的访问权限不能低于父类的方法访问权限(也就是说如果父类方法是用protected修饰,那么子类方法肯定不能是public修饰)

对于重写的方法可以显示的给一个注解@override

class Cat extends Animal{
	public Cat(String name) {
		super(name);
	}
	@override
	public void eat(String food) {
		System.out.println("我是一只小猫咪");
		System.out.println(this.name + "is eating" + food);
	}
}

这样做的好处在于这个注解能帮我们进行一些合法性校验.
例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.

重写和重载的区别:

方法重写 方法重载
方法名 相同 相同
参数列表 相同 不同
返回值 相同 不做要求
范围 继承 同一个类
限制 被重写的方法不能拥有比父类更严格的访问控制权限 没有访问控制权限要求

发生多态要满足两个条件:(这个多态叫做运行时多态)

  1. 父类需要引用子类对象(即向上转型)
  2. 通过父类的引用调用子类和父类同名的覆盖方法

class对象存储位置在方法区
反射: 获取class对象(用三种方法会发现class对象地址一样==》class对象只有一个)

向下转型

向下转型是将子类对象转给父类,一般不太常见,下面将介绍他的作用

还是刚刚这段代码

// Animal.java
public class Animal {
	public String name;
	public Animal(String name) {
		this.name = name;
	}
	public void eat(String food) {
		System.out.println("我是一只小动物");
		System.out.println(this.name + "is eating" + food);
	}
}
// Cat.java
class Cat extends Animal{
	public Cat(String name) {
		super(name);
	}
	@override
	public void eat(String food) {
		System.out.println("我是一只小猫咪");
		System.out.println(this.name + "is eating" + food);
	}
	public void jump() {
		System.out.println(this.name + "is jumping");
	}
}

让猫咪吃东西

        Animal animal = new Cat("大不妞");
        animal.eat(" fish");
//执行结果
//大不妞 is eating fish

如果我们想让猫咪跑起来

animal.jump();

此时编译出错,找不到jump();方法
因为编译看左,运行看右,编译时期编译器先在Animal类中看有没有jump方法,没有所以直接编译出现错误
那如果想要让猫咪跑起来就只能

		Animal animal = new Cat("大不妞");
        Cat cat = (Cat)animal;
        animal.jump();

这种就是向下转型,但是向下转型存在风险,比如:

		Animal animal = new Bird("啾啾");
        Cat cat = (Cat)animal;
        animal.jump();
//此时执行会抛出类型转换异常 java.lang.ClassCastException

因为本质上animal是一个Bird类型的,和Cat直接没有关系,所以就会出现类型转换异常
==》要发生向下转型最好先判断是否是一个实例

instanseof

instanseof可以判定一个引用是否是某个类的实例

if(Animal instanseof Cat){
    Cat cat=(Cat)animal;
    cat.jump();
}

构造方法内是否可以发生运行时绑定?

答案是可以,例子见下

class A {
    public A() {
        func();
    }
	public void func() {
    	System.out.println("A.func()");
    }   
}

class B extends A {
    private int num = 1;
    @Override
    public void func() {
        System.out.println("B.func() " + num);
    }
}
public class Test {
    public static void main(String[] args) {
        B b = new B(); 
    }
}
// 执行结果
B.func() 0

为什么执行出来的num会是0?
构造子类对象前要先构造父类
所以构造 B 对象的同时, 会调用 A 的构造方法.
A 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 B 中的 func
此时 B 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.

使用多态的好处是什么?

  1. 类调用者对类的使用成本进一步降低.
    封装 是让类的调用者不需要知道类的实现细节.
    多态 能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
    因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.

  2. 能够降低代码的 “圈复杂度”(一段代码中的分支和循环语句越多,圈复杂度越高), 避免使用大量的 if - else

  3. 可扩展能力更强,使用多态的方式代码改动成本也比较低.

猜你喜欢

转载自blog.csdn.net/qq_43360037/article/details/104394859