Java核心技术 继承

本文深入探讨Java中的继承和多态概念,介绍如何使用extends关键字定义子类,覆盖方法的实现,以及如何通过super关键字调用超类方法。同时,文章详细解释了多态的应用场景、动态绑定机制及其实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.类、超类和子类

定义子类

关键字extends表示继承。

public class Manager extends Employee {
    添加方法和域
}

关键字extends表明正在构建的新类派生于一个已存在的类。已存在的类被称为超类、基类或父类。新类称为子类、派生类或孩子类。Java程序员更喜欢用超类和子类。

子类比超类拥有的功能更加丰富,Manager比员工多一份奖金。

在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。

 

覆盖方法

超类中的方法对于子类并不一定适用(Manager的getSalary方法应该返回薪水和奖金的总和)。需要提供新的方法来覆盖(override)超类中的这个方法:

public class Manager extends Employee {
    private double bonus;

    @Override
    public double getSalary() {
        return salary+ bonus;
    }
}

然而这个方法并不能运行。子类不能直接访问超类的私有域

public double getSalary() {
    double baseSalary = getSalary();
    return baseSalary + bonus;
}

仍不能运行,因为子类也有一个getSalary方法(就是正在实现的这个方法),所以这条语句会导致无限次地调用自己,直到整个程序崩溃。

为此,需要使用特定的super解决这个问题:

@Override
public double getSalary() {
    double baseSalary = super.getSalary();
    return baseSalary + bonus;
}

super并不像this一样,super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

在子类中可以增加域、增加方法或覆盖超类的方法,然而不能删除继承的任何域和方法。

 

子类构造器

public Manager(String name, double salary, int year, int month, int day) {
    super(name, salary, year, month, day);
    bonus = 0;
}

这里的super具有不同的含义,语句super(name, salary, year, month, day)是调用超类中含有name,salary,year,day参数的构造器的简写形式。由于子类不能访问超类的私有域,所以必须利用超类构造器对这部分私有域进行初始化。super调用构造器的语句必须是子类构造器的第一条语句

如果子类的构造器没有显式地调用超类构造器,则将自动调用超类默认构造器。如果超类没有不带参数的构造器,并且在子类构造器中又没有显式地调用超类的其他构造器,则Java编译器将报告错误。

 

this的两个用途:

1.引用隐式参数

2.调用该类其他的构造器

super的两个用途:

1.调用超类方法

2.调用超类构造器

两个关键字在调用构造器时候,有一个共同点,调用构造器的语句只能作为另一个构造器的第一条语句出现。构造参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。

 

public static void main(String[] args) {
    Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
    boss.setBonus(5000);

    Employee[] staff = new Employee[3];
    staff[0] = boss;
    staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
    staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
    
    for (Employee e : staff) {
        System.out.println(e.getName() + " " + e.getSalary());
    }
    //Carl Cracker 85000.0
    //Harry Hacker 50000.0
    //Tommy Tester 40000.0
}

只有staff[0]计算了奖金加薪水,e.getSalary()调用能够确定应该执行哪个getSalary方法。尽管e声明为Employee类型,但实际上e既可以引用到Employee类型的对象,也可以引用Manager类型的对象。虚拟机知道e实际引用的对象类型,因此能够正确地调用响应的方法。

 

一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)

在运行时能够自动地选择调用哪个方法的现象被称为动态绑定(dynamic binding)

 

继承层次

继承不仅限于一个层次。Java不支持多继承,有关多多继承的功能Java通过接口实现。

由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)

 

多态

继承关系就是is-a规则,is-a规则的另一种表述法是置换法则。

在Java中,对象变量是多态的。一个Employee对象既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类对象。

Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;

这个例子中,变量staff[0]与boss引用同一个对象。但编译器将staff[0]看成Employee对象。这意味着可以调用boss.setBonus(5000),但不能调用staff[0].setBonus(5000)。

然而,不能将一个超类的引用赋给子类变量。

Manager m = staff[i]; // error

不是所有的雇员都是经理。

多态的条件:

1.要有继承   

2.要有重写      

3.父类引用指向子类对象

 

在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。

Manager[] managers = new Manager[10];
Employee[] staff = managers;
staff[0] = new Employee(...);

编译器接纳这个赋值操作,但staff[0]与manager[0]引用的是同一个对象,似乎我们把一个普通雇员擅自归入经理行列中了。当调用manager[0].setBonus(1000)的时候,将会导致调用一个不存在的域,进而搅乱相邻存储空间的内容。

为了确保不发生这类错误,所有数组都要牢记他们的类型,并负责监督仅将类型兼容的引用存储到数组中。例如new manager[10]创建了一个经理数组,如果试图存储一个Employee类型的引用就会发生ArrayStoreException异常。

 

理解方法调用

x.f(args),隐式参数x声明为C的一个对象。

1.编译器查看对象的声明类型和方法名,编译器会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法(超类的私有方法不可访问)。

2.编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法,这个过程被称为重载解析(overloading resolution)。由于允许参数类型转换(int转为double),如果编译器没有找到与参数类型匹配的方法,或者经过转换后有多个方法与之匹配,就会报告错误。

3.如果是private、static、final方法或者构造器,那个编译器将可以准确地知道应该调用哪个方法,这种调用方式称为静态绑定(static binding)。与之相对应的是,调用的方法依赖于隐式参数的实际类型,并且运行时实现动态绑定

4.当采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。

假设x的实际类型是D,它是C的子类。如果D类定义了方法f,就直接调用它,否则在D的超类在寻找f,依次类推。

虚拟机预先为每个类创建一个方法表(method table),列出所有的签名和实际调用的方法。如果调用super.f(param),编译器将对隐式参数超类的方法表进行搜索。

 

动态绑定有一个重要特性:无需对现存的代码进行修改,就可以对程序进行扩展。

 

阻止继承:final类和方法

不允许扩展的类被称为final类

public final class Executive extends Manager {
    ...
}

类中特定方法也可以被声明为final。子类就不能覆盖这个方法(final类中的所有方法自动的成为final方法,但不包括域)。

将方法或类声明为final主要目的是:确保它们不会在子类中改变语义。

 

强制类型转换

进行类型转换的唯一原因是:在暂时忽略对象的实际类型之后,使用对象的全部功能。

Manager boss1 = (Manager) staff[0];
Manager boss2 = (Manager) staff[1]; // error

第二句转换会产生一个ClassCastException异常。

应该养成一个良好的程序设计习惯,在转换之前先查看是否能够成功地转换:

if (staff[1] instanceof Manager) {
    Manager boss2 = (Manager) staff[1];
}

x如果为null,x instanceof C不会产生异常,只是会返回false。因为null没有引用任何对象,当然也不会引用C类型的对象。

1.只能在继承层内进行类型转换。

2.在将超类转换成子类之前,应该使用instanceof进行检查。


抽象类

如果自上而下在类的继承层次结果中上移,位于上层的类更具有通用性,甚至可能更加抽象。我们只将它作为派生其他类的基类,而不作为想使用的特定实例类。可以使用abstract关键字,这样就不需要实现基类的方法了。

包含一个或多个抽象方法的类本身必须被声明为抽象的。

public abstract class Person {
    public abstract String getDescription();
}

除了抽象方法之外,抽象类还可以包含具体数据和具体方法。

public abstract class Person {
    private String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    public abstract String getDescription();
    public String getName() {
        return name;
    }
}

类即使不含抽象方法,也可以声明为抽象类。

抽象类不能实例化。

new Person("Wu");

是错误的,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象:

Person p = new Student("Wu", "Economics");

 

受保护访问

可以将Employee中的hireDay声明为proteced,而不是私有域,Manager中的方法就可以直接访问它,但Manager类中的方法只能够访问Manager对象中的hireDay域,而不能访问其他Employee对象中的这个域。这种限制有助于避免滥用受保护机制,使得子类只能获得访问受保护域的权利。在实际应用中,要谨慎使用protected属性。

受保护方法更具有实际意义。如果需要限制某个方法的使用,就可以将它声明为protected。这表明子类得到信任,可以正确地使用这个方法,而其他类则不行。这种方法一个最好的示例就是Object中的clone方法。

 

2.Object:所有类的超类

Object类是Java中所有类的始祖,在Java中每个类都是由它扩展而来的。

在Java中,只有基本类型(primitive types)不是对象。

所有数组类型,不过是对象数组还是基本类型的数组都扩展了Object类。

 

equals类

在Object类中,这个方法将判断两个对象是否具有相同的引用。

@Override
public boolean equals(Object otherObject) {
    if (this == otherObject) {
        return true;
    }
    if (otherObject == null) {
        return false;
    }
    if (getClass() != otherObject.getClass()) {
        return false;
    }
    Employee other = (Employee) otherObject;
    return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
}

getClass方法将返回一个对象所属的类。

由于null值调用equals方法会出现NullPointerException异常,所以这里使用Objects.equals方法,如果两个参数都为null,返回true;如果其中一个参数为null,返回false;均不为null,调用a.equals(b)。

 

在子类定义equals方法时,首先调用超类的equals,如果检查失败,对象就不可能相等。

@Override
public boolean equals(Object otherObject) {
    if (!super.equals(otherObject)) {
        return false;
    }
    Manager other = (Manager) otherObject;
    return bonus == other.bonus;
}

 

相等测试与继承

很多人喜欢使用instanceof进行检查类是否匹配。这样不但没有解决otherObject是子类的情况,并还还有可能招致一些麻烦。

java中,instanceof运算符的前一个操作符是一个引用变量,后一个操作数通常是一个类(可以是接口),用于判断前面的对象是否是后面的类,或者其子类、实现类的实例如果是返回true,否则返回false。

Person p = new Student("Wu", "Economics");
System.out.println(p instanceof Object);
System.out.println(p instanceof Student);
// true
// true

Java语言规范要求equals方法具有下面的特性:

1.自反性 x.equals(x) = true

2.对称性

3.传递性

4.一致性,如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。

5.对于任意非空引用x.equals(null)应该返回false。

 

一个完美编写equals方法的建议:

1.显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量

2.检测otherObject与this是否引用同一个对象

if (this == otherObject) {
    return true;
}

3.检测otherObject是否为null

if (otherObject == null) {
    return false;
}

4.比较this与otherObject是否属于同一个类,使用getClass或instanceof检测

●如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检查

●如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同的子类的对象之间进行相等比较。

5.将otherObject转换为相应的类类型变量

ClassName other = (ClassName)otherObject;

6.对所有需要比较的域进行比较

对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。

 

hashCode方法

散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的,如果x和y是两个不同的对象,x.hashCode()与y.hashCode()基本上不会相同。

String类源码的hashCode方法:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
String s = "OK";
StringBuilder sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
String t = new String("OK");
StringBuilder st = new StringBuilder(t);
System.out.println(t.hashCode() + " " + st.hashCode());
// 2524 356573597
// 2524 1735600054

sb和st有着不同的散列码,因为在StringBuilder类中没有定义hashCode方法,它的散列码是由Object类默认hashCode方法导出的对象存储地址。

hashCode方法应该返回一个整数数值(负数也可),并合理地自核实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。

@Override
public int hashCode() {
    return 7 * Objects.hashCode(name)
            + 11 * Double.hashCode(salary)
            + 13 * Objects.hashCode(hireDay);
}

最好使用null安全方法Objects.hashCode,如果参数为null,这个方法返回0。

还有更好的做法,组合多个散列值时,可使用Objects.hash:

public int hashCode() {
    return Objects.hash(name, salary, hireDay);
}

Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。

如果存在数组类型的域,可以使用静态的Arrays.hashCode方法计算一个散列码,这个散列码有数组元素的散列码组成。

 

toString方法

toString方法,返回表示对象值的字符串。

绝大多数toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。

@Override
public String toString() {
    return getClass().getName() + "[name=" + name +
            "salary: " + salary
            + ",hireDay=" + hireDay
            + "]";
}

最好通过getClass().getName()获得类名的字符串,而不要讲类名硬加在toString方法中。

这样的好处是,子类只要调用super.toString方法就可以了

@Override
public String toString () {
    return super.toString()
            + "[bonus=" + bonus +
            "]";
}

随处可见toString方法的主要原因是:只要对象与一个字符串通过操作符 “+”连接起来,Java编译就会自动调用toString方法。

在调用x.toString()的地方可以用 "" + x代替,与toString不同的是,如果x是基础类型,这句话也可以执行。

如果x是任意对象,调用System.out.println(x),println方法就会直接地调用x.toString方法,并打印出得到的字符串。

Object类定义了toString方法,用来打印输出对象所属的类名和散列码。

System.out.println(System.out);
// java.io.PrintStream@14ae5a5

PrintStream类的设计者没有覆盖toString方法。

 

但是素组继承了Object类的toString方法,数组类型将按照旧的格式打印:

int[] numbers = { 2, 3, 5, 7 };
String ss = "" + numbers;
System.out.println(ss);
System.out.println(Arrays.toString(numbers));
// [I@7f31245a
// [2, 3, 5, 7]

[I表明是一个整型数组,修正的方式是调用静态方法Arrays.toString方法,多维数组则需要调用Arrays.deepToString方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值