Java基础——(四)继承

237 阅读13分钟

1. 类、超类和子类

在Java中,通过关键字extends表示继承。extends表明正在构造的新类派生与一个已存在的类,已存在的类称为超类(superclass)、基类(base class)或父类(parent class);新类成为子类(subclass)、派生类(derived class)。 假设我们有两个类——Employee类和Manager类,其中Manager类派生自Employee。

public class Employee {
    private String name;

    private double salary;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }
}

public class Manager extends Employee {
    private double bouns;

    public void setBouns(double bouns) {
        this.bouns = bouns;
    }

    public double getBouns() {
        return this.bouns;
    }
}

子类可以使用父类的公有方法,而父类不能使用子类的扩展方法。以Manager和Employee为例,虽然Manager类没有定义getName方法,但是因为父类Employee定义了,所以Maanger自动继承了父类Employee中的这些方法,同时还自动继承了name、salary这2个域。而对于getBouns方法,因为只有Manager有这个方法,而父类Employee没有这个方法,因此Employee对象不能使用这个方法。 在这里插入图片描述 在通过扩展父类定义子类的时候,仅需要指出子类与超类的不同之处,因此在设计类的时候,应该将通用的方法放在父类中,而将具有特殊用途的方法放在子类中。 然而,父类中的有些方法对子类并不一定适用,比如Maanger类中的getSalary方法应该返回薪水和奖金的综合,因此需要提供一个新的方法来覆盖(override)超类中的这个方法。因为salary在父类定义的访问操作符为private,因此子类Manager不能直接访问到salary,所以需要使用getSalary这个公有方法来访问:

package org.example.test;

public class Manager extends Employee {
    private double bouns;

    public void setBouns(double bouns) {
        this.bouns = bouns;
    }

    public double getBouns() {
        return this.bouns;
    }
    
    @Override
    public double getSalary() {
        return getSalary() + this.bouns;
    }
}

但是,上面的代码是错误的,问题出在调用getSalary的语句上,因为Manager类也有一个getSalary方法,所以这条语句会导致无限次地调用字节,导致整个程序崩溃。 这里我们的本意是调用父类Employee中的getSalary方法,而不是当前类的getSalary方法,因此,可以使用super关键字来解决:

package org.example.test;

public class Manager extends Employee {
    private double bouns;

    public void setBouns(double bouns) {
        this.bouns = bouns;
    }

    public double getBouns() {
        return this.bouns;
    }
    
    @Override
    public double getSalary() {
        return super.getSalary() + this.bouns;
    }
}

super也适用于构造函数中,假设我们的父类Employee有以下构造函数:

public Employee(String name, double salary) {
	this.name = name;
	this.salary = salary;
}

然后子类要在构造函数中,去初始化name,salary和bouns的信息,这个使用可以使用super来调用父类的构造函数对父类的私有域进行初始化:

public Manager(String name, double salary, double bouns) {
        super(name, salary);
        this.bouns = bouns;
    }

1.1 多态

在下面代码中,我们定义一个包含三个雇员的数组,然后将经理和普通雇员都放到数组中,在循环中输出每个人的薪水,此时会看到1和2仅输出基本薪水,而0输出的是基本薪水加奖金,因为0对应的是Manager对象。 在这里插入图片描述 尽管我们这里将所有雇员对象都声明为Employee类型,但是在实例化对象是,因为staff[0]引用的是Manager对象,所以在getSalary时调用的是Manager类中的getSalary方法,也就是说虚拟机直到实际引用的对象类型,并且能够正确地调用相应的方法。 一个对象变量可以指示多种实际类型的现象我们称之为多态(polymorphism)。在java中,对象变量是多态的,一个Employee变量既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类的对象。 在运行时能够自动地选择调用哪个方法的现象成为动态绑定(dynamic binding)。动态绑定有一个非常重要的特性,它无需对现存的代码进行修改,就可以对程序进行扩展。

1.2 继承层次

继承并不仅限于一个层次,例如Manager类可以派生自Person类。由一个公共超类派生出来的所有类的集合被成为继承层次(inheritance hierarchy)。在继承层次中,从某个特定的类到其祖先的路径被成为该类的继承链(inheritance chain)。

1.3 阻止继承:final类和方法

有时候,我们希望阻止人们利用某个类定于子类,不允许被扩展的类被成为final类,如果在定义类的时候,使用final修饰符,就表明这个类是final类。声明格式如下:

final class Executive extends Manager {
}

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

class Employee {
 ...
 public final String getName() {
 	...
 }
}

1.4 强制类型转换

对象引用的转换语法与数值表达式的类型转化类似,仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前即可。如:

Manager boss = (Manager)staff[0];

将一个值存入变量时,编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量,编译器是允许的,但将一个超类的引用赋给一个子类变量,必须进行类型转换,这样才能通过运行时的检查。 对于强制类型转换,有以下要求:

  • 只能在继承层次内进行类型转换
  • 在将超类转换为子类之前,最好使用instanceof进行检查

1.5 抽象类

对于Employe和Student,它们都有一个特性,都是一个Person,因此,我们可以做进一步的抽象,抽象一个Person类,而Employee和Student都是Person的子类。假设我们现在需要对Employee和Student增加一个getDescription方法来返回对这个人的简要描述,此时可以很方便的在Employee类和Student类实现,但是在Person类中应该提供什么内容?这个时候,我们就可以使用abstract关键字,通过abstract关键字,我们就完全不需要实现这个方法。

public abstract String getDescription();

如果一个类包含一个或多个抽象方法,那么这个类本身必须被声明为抽象的。抽象类除了抽象方法之外,还可以包含具体数据和具体方法。

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

抽象方法充当着占位的角色,它们的具体实现在子类,扩展抽象类可以有两种选择,一种是在子类中定义部分抽象方法或抽象方法也不定义,这样子类也必须标记为抽象类;另一种是定义全部的抽象方法,这样子类就不是抽象的。 注意,抽象类不能被实例化。

在这里插入图片描述

1.6 受保护访问

在一般情况下,我们将类中的域标记为private,而方法标记为public,任何声明为private的内容对其他类都是不可见的,包括子类。但有时候,我们希望父类中的某些方法运行被子类访问或子类的方法能访问父类的某个域,这个时候,需要将这些方法或域声明为protected。

2. Object:所有类的超类

Object类是Java中所有类的始祖,在Java中每个类都是由它扩展而来,如果没有明确地指出某个类的超类,那么Object就被认为是这个类的超类。但我们不需要显示地继承它,而言就是不需要这样写:

class Employee extends Object

在java中,只有基本类型不是对象,所有的数组类型,不过是对象数组还是基本类型的数组都扩展自Object类。

2.1 equals方法

Object类中的equals方法用于检测一个对象是否等于另一个对象,在Object类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。但对于多数类来说,这种判断并没有意义,一般来说,应该检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。 以Empolyee类为例,但两个Employee对象的姓名、薪水等都相等时,我们便认为它们是相等的,因此我们复写equals类:

 @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (other == null) {
            return false;
        }
        if (getClass() != other.getClass()) {
            return false;
        }
        Employee otherEmployee = (Employee) other;
        return this.getName().equals(otherEmployee.getName())
                 && salary == otherEmployee.salary;
    }

在子类中定义equals方法时,首先调用超类的equals,如果检测失败,对象就不可能相等,如果超类中的域都相等,就需要比较子类中的实例域,比如下面是Maanger的equals方法:

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

2.2 hashCode方法

散列码(hash code)是由对象导出的一个整型值,散列码是没有规律的,如果x和y是两个不同的对象,x.hashCode()和y.hashCode()基本上不会相等。 由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。 一般来说,如果我们重新定义了equals方法,那么就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。 hashCode方法应该返回一个整型数值(可以是负数),并合理组合实例域的散列码,以便使各个不同的对象产生的散列码更加均匀,比如下面是Employee类的hashCode方法:

@Override
    public int hashCode() {
        return 7 * getName().hashCode()
                + 11 * new Double(salary).hashCode();
    }

上面的写法可能导致空指针异常,我们可以使用null安全的方法Objects.hashCode进行优化,如果参数为null,这个方法返回0:

@Override
    public int hashCode() {
        return 7 * Objects.hash(getName())
                + 11 * new Double(salary).hashCode();
    }

当需要组合多个散列值是,可以调用Objects.hash并提供多个参数,这个方法会对各个参数调用Objects.hashCode并组合这些散列值。

  @Override
    public int hashCode() {
        return Objects.hash(getName(), salary);
    }

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

2.3 toString方法

在Object中还有一个重要的方法,就是toString方法,用于返回表示对象值的字符串。如果没有复写,返回的是“类路径@地址”。 在这里插入图片描述 一般我们的toString会遵循这样的格式:类的名字,随后是一对方括号括起来的域值,下面是Employee类的toString方法:

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

在这里插入图片描述

3. 泛型数组列表

在之前,我们定义数组的时候,一般会给数组一个确定的大小,数组的大小一旦确定,后续我们想要动态更改数组长度会比较困难。在Java中,提供了一个ArrayList的类,它使用起来有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。 ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保持的元素对象类型,需要用一对尖括号将类名括起来加载后面,例如ArrayList。下面声明和构造一个保存Employee对象的数组列表:

ArrayList<Employee> staff = new ArrayList<>();
// 或
ArrayList<Employee> staff = new ArrayList<Employee>();

第一种写法被称为“菱形”写法,因为尖括号<>就像是一个菱形,可以结合new操作符使用菱形语法,编译器会检测新值是声明,如果赋值给一个变量,或传递某个方法,或者从某个方法返回,编译器会检测这个变量、参数或方法的泛型类型,然后将这个类型放在<>中。

方法作用
boolean add(T obj)在数组列表尾端添加一个元素,永远返回true
int size()返回存储在数组列表中当前元素数量
T get(int index)返回数组列表中的第index 个元素
void set(int index, T obj)设置数组列表中的第index个元素为obj
T remove(int index)删除数组列表中你的第index个元素

在这里插入图片描述

4. 对象包装器与自动转箱

有时候,需要将int这样的基本类型转换为对象,所有的基本类型都有一个与之对应的类,例如Integer类对应基本类型int。通常,这些类称为包装器(wrapper),这些对象包装器拥有很鲜明的名字:Integer, Long, Float, Double, Short, Byte, Character, Void和Boolean。对象包装器类还是final,因此不能定义它们的子类。 当我们像定义一个整型数组列表时,不能写成ArrayList,而是要写成ArrayList。此外,在使用整型数组列表添加元素时,可以直接使用基本类型:

list.add(3); 
// java会自动将其变换为:
list.add(Integer.valueOf(3));

这种变换被称为自动装箱(autoboxing)。 相反,当将一个Integer对象赋给一个int值时,将会自动地拆箱:

int n = list.get(i);
// java会自动将其变换为:
int n = list.get(i).intValue();

对于基本类型,我们可以直接使用==来判断两者的值是否相等,但是对于包装类就不一定了,因为包装类是一个对象。以Integer类为例,在Integer类中,会维护一段缓存,缓存值位于-128到127。 在这里插入图片描述 在这段区间内,通过自动装箱得到的Integer对象,它们的值是相等的,如下图所示: 在这里插入图片描述 如果超过-128到127这个区间,那么通过自动装箱时获取的值是不相等的 在这里插入图片描述 如果不是通过自动装箱,而是通过实例化(也就是new)出来的对象,即便处于-128到127这个区间,使用 ==也是不相等的。 在这里插入图片描述

5. 参数数量可变的方法

java提供了可以用可变的参数数量调用的方法,以下列代码为例: 在这里插入图片描述 这里的省略号...是java代码的一部分,它表明这个方法可以接收任意数量的对象。实际上,上面的printWord接收两个参数,一个是Date类型的参数,另一个是String[]数组,也就是说,String...参数与String[]完全一样。

6. 枚举类

public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};

在上面的代码中,这个声明定义的类型是一个类,它有4个实例。在定义枚举的时候,我们已经将它所拥有的实例都定义出来了,因此,在比较两个枚举类型的值时,不需要调用equals方法,直接使用==就可以了。当然,如果我们需要的话,还可以为枚举类型添加一些构造器、方法和域:

public enum Size {
    SMALL("S"),
    MEDIUM("M"),
    LARGE("L"),
    EXTRA_LARGE("XL");
    
    private String abbreviation;
    
    Size(String abbreviation) {
        this.abbreviation = abbreviation;
    }
    
    public String getAbbreviation() {
        return this.abbreviation;
    }
}

所有的枚举类型都是Enum类的子类,它们继承这个类的许多方法,其中最有用的是toString,这个方法能够返回枚举常量名,比如Size.SMALLL.toString()将返回字符串"SMALL"。toString的逆方法是静态方法valueOf,例如

Size s = Enum.valueOf(Size.class, "SMALL");
或者
Size s = Size.valueOf("SMALL");

上面的代码会将s设置为Size.SMALL。 每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组,比如下面的代码会返回Size.SMALL, SIZE.MEDIUM, SIZE.LARGE和SIZE.EXTRA_LARGE的数组。

Size[] values = Size.values();

参考链接

《Java核心技术卷I》(网盘链接:pan.quark.cn/s/06c58d47d…