继承
// 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)
方法重写的注意事项:
- 普通方法可以重写, static 修饰的静态方法不能重写
- 重写中子类的方法的访问权限不能低于父类的方法访问权限(也就是说如果父类方法是用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 方法, 就会编译报错, 提示无法构成重写.
重写和重载的区别:
方法重写 | 方法重载 | |
---|---|---|
方法名 | 相同 | 相同 |
参数列表 | 相同 | 不同 |
返回值 | 相同 | 不做要求 |
范围 | 继承 | 同一个类 |
限制 | 被重写的方法不能拥有比父类更严格的访问控制权限 | 没有访问控制权限要求 |
发生多态要满足两个条件:(这个多态叫做运行时多态)
- 父类需要引用子类对象(即向上转型)
- 通过父类的引用调用子类和父类同名的覆盖方法
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.
使用多态的好处是什么?
-
类调用者对类的使用成本进一步降低.
封装 是让类的调用者不需要知道类的实现细节.
多态 能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低. -
能够降低代码的 “圈复杂度”(一段代码中的分支和循环语句越多,圈复杂度越高), 避免使用大量的 if - else
-
可扩展能力更强,使用多态的方式代码改动成本也比较低.