Java 继承

1、超类和子类

关键字extends表明正在构造的新类派生于一个已存在的类。这个已存在的类称为超类、基类或父类;新类称为子类、派生类或孩子类。Java 中的类之间只支持单继承,而接口之间支持多继承

在子类的构造器中,我们可以使用super语句调用超类的构造器(该语句必须是子类构造器的第一条语句),来初始化超类的字段。如果子类的构造器没有显式地调用超类的构造器,将自动地调用超类的无参数构造器。如果超类没有无参数地构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,Java 编译器就会报告一个错误。

2、初始化顺序

继承中的初始化顺序,与普通类的初始化顺序相差不大,代码如下:

package package1;

public class Employee {

    static { System.out.println("Employee 静态初始化块"); } // 1

    private int e = -100;

    { // 7
        System.out.print("Employee 非静态初始化块 ");
        foo();
    }

    public Employee() {
        System.out.println("Employee 无参构造器");
    }

    public Employee(double e) {
        this(); // 6
        System.out.println("Employee 有参构造器");
    }

    public void foo() {
        System.out.println("e=" + e);
    }
}
package package2;

import package1.Employee;

public class Manager extends Employee {
    static { System.out.println("Manager 静态初始化块"); } // 2

    private int m = 100;

    { // 8
        System.out.print("Manager 非静态初始化块 ");
        foo();
    }

    public Manager() {
        super(7); // 5
        System.out.println("Manager 无参构造器");
    }

    public Manager(double m) {
        this(); // 4
        System.out.println("Manager 有参构造器");
    }

    @Override
    public void foo() {
        System.out.println("m=" + m);
    }

    public static void main(String[] args) throws ClassNotFoundException {
        Manager manager = new Manager(100.0); // 3
    }
}

运行结果:

Employee 静态初始化块
Manager 静态初始化块
Employee 非静态初始化块 m=0
Employee 无参构造器
Employee 有参构造器
Manager 非静态初始化块 m=100
Manager 无参构造器
Manager 有参构造器

不做过多解释,可以参考一下对象与类中描述的初始化顺序。(注意看代码注释标出的顺序,可以打断点调试看看

3、覆盖方法

覆盖方法(一般加上@Override注解,表示当前方法是覆盖方法):

  • 子类方法的可见性不能低于超类方法
  • 子类的返回值类型可以改为原返回类型的子类型(这时称这两个方法有可协变的返回类型)
  • 子类方法中声明的检查型异常不能比超类方法中声明的异常更通用
  • 超类中的 static 方法不能被子类覆盖,但是可以被子类中相同签名的 static 方法隐藏
  • 参数数量可变的形参换成数组类型构成重写

额外补充:当子类的返回值类型改为原返回类型的子类型时,编译器会在子类中合成一个桥方法,该方法会调用覆盖的方法。

package package1;

public class Employee {
    public Object foo() {
        return new Object();
    }
}
package package2;

import package1.Employee;

public class Manager extends Employee {
    @Override
    public Integer foo() {
        return 7;
    }

    public static void main(String[] args) {

    }
}

我们编译上述代码后,使用javap -v Manager.class命令反编译看看。

public class package2.Manager extends package1.Employee {
  public package2.Manager();
  public java.lang.Integer foo();
  public static void main(java.lang.String[]);
  public java.lang.Object foo();
}

可以看到,Manager类中确实合成了桥方法public java.lang.Object foo(),在该桥方法中会调用覆盖的方法:

public java.lang.Object foo();
    descriptor: ()Ljava/lang/Object;
    flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #13                 // Method foo:()Ljava/lang/Integer;
         4: areturn
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lpackage2/Manager;

4、多态(向上转型)

一个对象变量可以指示多种实际类型的现象称为多态,在运行时能够自动地选择适当的方法,称为动态绑定

在将超类强制类型转换成子类之前,应该使用instanceof进行检查;否则,如果类型不符,将会产生类型转换异常。

Employee e = new Manager(); //多态
if(e instanceof Manager) { // 类型检查
    Manager m = (Manager) e; // 强制类型转换
}

5、抽象类

我们可以使用abstract关键字来声明抽象类抽象方法。(abstract 不能修饰私有方法、静态方法、final 方法/类)

包含抽象方法的类必须被声明为抽象类,但是抽象类可以不包含抽象方法。

6、静态绑定和动态绑定

  • 静态绑定:调用的方法在编译期间就可以确定(private 方法、static 方法、final 方法和构造器是静态绑定的)
  • 动态绑定:在运行时根据对象的实际类型选择调用的方法

示例代码:

class Employee {
    public static String field1 = "Employee Field1";
    public static String method1() { return "Employee Method1"; }

    public String field2 = "Employee Field2";
    public String method2() { return "Employee Method2"; }
}

class Manager extends Employee {
    public static String field1 = "Manager Field1";
    public static String method1() { return "Manager Method1"; }

    public String field2 = "Manager Field2";
    public String method2() { return "Manager Method2"; }
}

public class Main {
    public static void main(String[] args) {
        Employee e = new Manager();
        System.out.println(e.field1 + "\n" + e.field2 +
                "\n" + e.method1() + "\n" + e.method2());
    }
}

运行结果:

Employee Field1
Employee Field2
Employee Method1
Manager Method2

可以发现,字段不论静态与否,都是由编译器解析的,因此不是多态的;静态方法也是由编译器解析的(静态绑定),而普通方法是在运行时确定的(动态绑定)。

为什么 static 方法不能被覆盖?用反证法证明一下。考虑刚才的示例代码:

public class Main {
    public static void main(String[] args) {
        Employee e = new Manager();
        System.out.println(e.field1 + "\n" + e.field2 +
                "\n" + e.method1() + "\n" + e.method2());
    }
}

如果 static 方法能被覆盖,会发生什么?结果应该是这样。

Employee Field1
Employee Field2
Manager Method1
Manager Method2

这样就发现,调用哪个静态方法取决于 e 引用的对象,这就导致静态方法变成了动态绑定的,而实际上静态方法是静态绑定的,所以得出结论静态方法不能被覆盖。

个人理解:如果能被覆盖,那么 static 方法就变成了覆盖方法,而覆盖方法是在运行时动态绑定的,static 方法是在编译时静态绑定的。(over!)


如有错误,欢迎指正。.... .- ...- . .- -. .. -.-. . -.. .- -.-- -.-.--

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值