Java从零开始系列03:继承

学习目标

  • 类、超类和子类
  • Object:所有类的超类
  • 泛型数组列表
  • 对象包装器与自动装箱
  • 参数数量可变的方法
  • 枚举类
  • 反射
  • 继承的设计技巧

一、类、超类和子类

这里以前文提过的Employee类为例,假设公司中有经理和员工,其待遇上存在一定的差异,但他们之间也存在许多相同的地方,例如,他们都领取薪水,不同的是,经理在完成预期任务后可以获得奖金。这种情况就要使用继承,因为需要为经理定义一个新类Manager,并增加一些新功能,但同时,每个经理也是员工,需要继承员工的方法。

(一)定义子类

可以如下继承Employee类来定义Manager类,这里使用关键字extends来继承:

public class Manager extends Employee
{
	added methods and dields
}

在Java中,所有的继承都是公共继承。

关键字extends表明正在构造的新类派生于一个已存在的类。这个类称为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived classs)或孩子类(child class)。

通过扩展超类定义子类时,只需要指出子类与超类的不同之处。因此在设计类时,应将最一般的方法放在超类中。

(二)覆盖方法

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

public class Manager extends Employee
{
	...
	public double getSalary()
	{
		...
	}
	...
}

只有Employee方法能直接访问Employee类的私有字段,即Manager类的getSalary方法不能直接访问salary字段。因此要使用公共接口,调用超类中的getSalary方法,可以使用特殊的关键字super解决这个问题:

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

(三)子类构造器

提供一个构造器:

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

语句 super(name, salary, year, month, day) 即调用超类中带有n、s、year、month和day参数的构造器。

由于Manager类的构造器不能访问Employee类的私有字段,所以必须通过一个构造器初始化这些字段,可以利用super来构造。使用super调用构造器的语句必须是子类构造器的第一条语句。

Employee类完整代码如下:

import java.time.*;

public class Employee
{
   private String name;
   private double salary;
   private LocalDate hireDay;

   public Employee(String name, double salary, int year, int month, int day)
   {
      this.name = name;
      this.salary = salary;
      hireDay = LocalDate.of(year, month, day);
   }

   public String getName()
   {
      return name;
   }

   public double getSalary()
   {
      return salary;
   }

   public LocalDate getHireDay()
   {
      return hireDay;
   }

   public void raiseSalary(double byPercent)
   {
      double raise = salary * byPercent / 100;
      salary += raise;
   }
}

Manager类的完整代码入下:

public class Manager extends Employee
{
   private double bonus;

   /**
    * @param name the employee's name
    * @param salary the salary
    * @param year the hire year
    * @param month the hire month
    * @param day the hire day
    */
   public Manager(String name, double salary, int year, int month, int day)
   {
      super(name, salary, year, month, day);
      bonus = 0;
   }

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

   public void setBonus(double b)
   {
      bonus = b;
   }
}

可以通过以下程序展示二者在薪水计算上的区别:

/**
 * This program demonstrates inheritance.
 * @version 1.21 2004-02-21
 * @author Cay Horstmann
 */
public class ManagerTest
{
   public static void main(String[] args)
   {
      // construct a Manager object
      var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
      boss.setBonus(5000);

      var staff = new Employee[3];

      // fill the staff array with Manager and Employee objects

      staff[0] = boss;
      staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
      staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);

      // print out information about all Employee objects
      for (Employee e : staff)
         System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
   }
}

(四)继承层次

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

(五)多态

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

“is-a”规则的另一种表述是替换原则(substitution principal)。它指出程序中出现超类对象的任何地方都可以使用子类对象替换。

对象变量是多态的。一个Employee类型的变量既可以一用一个Employee类型的对象,也可以引用Employee类的任何一个子类的对象。例如,可以将子类对象赋给超类变量:

Employee e;
e = new Employee(...);
e = new Manager(...);

不过,不能将超类的引用赋给子类变量。

子类引用的数组可以转换成超类引用的数组,而不需要使用强制类型转换。如:

Manager[] managers = new Manager[10];

将它转换成Employee[]数组是完全合法的:

Employee[] staff = managers;

注:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。如超类方法为public,则子类方法必须为public。

(六)阻止继承:final类和方法

不允许扩展的类被称为final类。假设,希望阻止人们派生Executive类的子类,就可以在声明这个类时使用final修饰符:

public final class Executive extends Manager
{
	...
}

类中某个特定方法也可以声明为final。子类就不能覆盖这个方法。

在早期Java中,有人为了避免动态绑定带来的系统开销而使用final关键字。如果一个方法没有被覆盖而且很短,编译器就能够对它进行优化处理,这个过程称为内联(inlining)。例如,内联调用e.getName() 将被替换为访问字段 e.name。

(七)强制类型转换

继承的强制类型转换遵循以下原则:

  • 只能在继承层次内进行强制类型转换
  • 在将超类强制转换成子类之前,应该使用instanceof进行检查
if (staff[1] instanceof Manager)
{
	boss = (Manager) staff[1];
	...
}

(八)抽象类

如果自下而上在类的继承层次结构中上移,位于上层的类更具有一般性,可能更加抽象。如:

public abstract String getDescription();

为提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。

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

除了抽象方法外,抽象类还可以包含字段的具体方法。抽象方法充当着占位方法的角色,它们在子类中具体实现。扩展抽象类可以有两种选择。一种是在子类中保留抽象类中的部分或所有抽象方法仍未定义,这样就必须将子类也记为抽象类;另一种做法是定义全部方法,这样一来,子类就不是抽象的了。

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

抽象类不能实例化。如果一个类声明为abstract,就不能创建这个类的对象。

可以定义一个抽象类的对象变量,但这个变量只能引用非抽象子类的对象。

(九)受保护访问

最好将类中的字段标记为private,而将方法标记为public。不过有时可能希望限制超类中的某个方法只允许子类访问,或者更少见的,可能希望允许子类方法访问超类的某个字段。为此,需要将这些类方法或字段声明为受保护(protected)。

在Java中,保护字段只能由同一个包中的类访问,从而避免滥用保护机制。

二、Object:所有类的超类

(一)Object类型的变量

可以使用Object类型的变量引用任何类型的对象:

Object obj = new Employee("Harry Hacker", 35000);

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

(二)equals方法

Object类的equals对象用于检测一个对象是否等于另一个对象。

public class Employee
{
	...
	public boolean equals(Object otherObject){
		// a quick test to see if the objects are identical
		if (this == otherObject)    return  true;
        
		// must return false if the explicit parameter is null
		if (otherObject == null)    return false;
        
		// if the classes don't match, they can't be equal
		if (getClass() != otherObject.getClass())
			return false;
        
		// now we know otherObject is a non-null Employee
		Employee ohter = (Employee) otherObject;
		        
		// test whether the fields have identical values
		return name.equals(ohter.name)
			&& salary == other.salary
			&& hireDay.equals(other.hireDay)
	}
}

在子类中定义equals方法时,首先调用超类的equals,如果检测失败,对象就不可能相等,然后再比较子类中的实例字段。

(三)相等测试与继承

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

  • 自反性:对于任何非空引用x,x.equals(x) 应该返回 true
  • 对称性:对于任何引用x和y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 返回 true
  • 传递性
  • 一致性
  • 对于任何非空引用x,x.equals(null) 应该返回 false

有两种情形:

  • 如果子类可以有自己的相等性概念,则对称性需求将强制使用getClass检测
  • 如果由超类决定相等性概念,则可以使用instanceof检测,这样可以在不同子类的对象之间进行相等性比较。

下面给出编写完美equals方法的建议:

  1. 显示参数命名为 otherObject,稍后需要将它强制转换成另一个名为other的变量。
  2. 检测 this 与 otherObject是否相等if (this == otherObject) return true;
  3. 检测 otherObject 是否为null,如果为null,返回false:if (otherObject == null) return false;
  4. 比较 this 与 otherObject 的类。如果 equals 的语义可以在子类中改变,就是用 getClass 检测:if (getClass() != otherObject.getClass)) return false;,如果所有子类都具有相同的相等性语义,可以使用 instanceof 检测:if (!(otherObject instanceof ClassName)) return false;
  5. 将 otherObject 强制转换为相应类型的变量:ClassName other = (ClassName) otherObject;
  6. 现在根据相等性概念比较字段。使用 == 比较基本类型字段,使用 Objects.equals 比较对象字段:return field1 == other.field1 && Objects.equals(field2, other.field2) && ...

对于数组字段,可以使用 Arrays.equals 方法检测是否相等。

(四)hashCode方法

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

每个对象都有一个默认的散列码,其值由对象的存储地址得出。

如果重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义 hashCode 方法,且equals 与 hashCode 的定义必须相容。

(五)toString方法

toString方法会返回表示对象的一个字符串。如 Point 类的 toString 方法将返回下面这样的字符串:

java.awt.Point[x=10,y=20]

下面是 Employee 类中 toString 方法的实现:

public String toString()
{
	return "Employee[name=" + name
		+ ", salary=" + salary
		+ ", hireDay=" + hireDay
		+ "]";
}

实际上还可以写得更好。最好调用 getClass().getName() 获得类名的字符串,而不要将类名硬编码写道 toString 方法中。

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

这样的 toString 方法也可以由子类调用。

三、泛型数组列表

Java允许在运行时确定数组的大小:

int actualSize = ...;
var staff = new Employee[actualSize];

但这样做一旦确定了数组的长度就不能轻易改变了。可以使用ArrayList类。ArrayList是一个有类型参数(type parameter)的泛型类(generic class),生成数组如:ArrayList<Employee>

(一)声明数组列表

声明和构造一个保存Employee对象的数组列表:

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

在Java10中,最好使用var关键字以避免重复写类名:

var staff = new ArrayList<Employee>();

如果没有使用var关键字,可以省去右边的类型参数:

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

这称为“菱形”语法。

使用add方法向数组添加元素:

staff.add(new Employee(...);

如果数组空间已满,数组列表会自动创建一个更大的数组,并将所有较小的数组拷贝到新数组中。

如果已知或可以估计出数组可能存储的元素数量,就可以在填充数组之前调用 ensureCapacity 方法:

staff.ensureCapacity(100);

另外,还可以把初始容量传递给ArrayList构造器:

ArrayList<Employee> staff = new ArrayList<>(100);

size 方法将返回数组列表中包含的实际元素个数:staff.size()

一旦确认数组列表大小,可以调用 trimToSize 方法保存当前元素所需要的存储空间,回收多余空间。

(二)访问数组列表元素

只能使用get和set方法访问数组列表。例如,要设置第i个元素:

staff.set(i, harry);

set方法只能用于替换数组中已经加入的元素。

要获得数组的一个元素:

Employee e = staff.get(i);

四、对象包装器与自动装箱

有时,需要将int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。例如,Integer类对应基本类型int。通常,这些类称为包装器(wrapper)。包装器类是不可变的,是final,不能派生它们的子类。

如定义整型数组列表就可以:

var list = new ArrayList<Integer>();

幸运的是,有一个很有用的特性,从而可以很容易地向ArrayList添加int类型的元素:

list.add(3);

将自动变换为:

list.add(Integer.valueOf(3));

这种变换称为自动装箱(autoboxing).

相反的,将一个Integer对象赋给一个int值时,会自动拆箱。

自动地装箱和拆箱也适用算术表达式。

由于包装器类引用可以为null,所以自动装箱有可能会抛出 NullPointerException 异常;另外,如果在一个条件表达式中混合使用 Integer 和 Double 类型,Integer 值就会自动拆箱,升级为 double,再装箱为Double。

装箱和拆箱都是编译器要做的工作,而不是虚拟机。编译器在生成类的字节码时会插入必要的方法调用。

五、参数数量可变的方法

可以提供参数数量可变的方法,如 printf:

public class PrintStream
{
	public PrintStream printf(String fmt, Object...args)
	{
		return format(fmt, args);
	}
}

这里的 … 是Java代码的一部分,表示方法可以接收任意数量的对象。

六、枚举类

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

实际上,这个声明定义的类型是一个类,它刚好有四个实例,不可能构造新的对象。因此,在比较两个枚举类型的值时,要用 ==

如果需要的话可以为枚举类型增加构造器、方法和字段。构造器只在构造枚举常量时调用。

public enum Size
{
	SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL")
	
	private String abbreviation;

	private Size(String abbreviation)	{ this.abbreviation = abbreviation;}
	private String getAbbreviation()	{ return abbreviation;}
}

枚举器的构造器总是私有的。所有枚举类型都是Enum类的子类。

toString方法可以返回枚举常量名,如Size.SMALL.toString()将返回字符 “SMALL”

toString的逆方法是静态方法valueOf。如

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

将 s 设置为 Size.SMALL。

每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举类型的数组。

Size[] values = Size.values();

ordinal方法返回enum声明中枚举常量的位置,从0开始计数。

七、反射

反射库(reflection library)提供了一个丰富且精巧的工具集,可以用来编写能够动态操纵Java代码的程序。Java可以支持用户界面生成器、对象关系映射器等。

能够分析类能力的程序称为反射(reflection)。反射机制可以用来:

  1. 在运行时分析类的能力
  2. 在运行时检查对象。如,编写一个适用于所有类的toString对象
  3. 实现泛型数组操作代码
  4. 利用Method对象,类似C++的指针

(一)Class类

在程序运行期间,Java运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择要正确执行的方法。

可以用一个特殊的Class类访问这些信息。Object类中的 getClass() 方法将会返回一个Class类型的实例。

Employee e;
...
Class cl = e.getClass();

(二)异常

程序运行错误时,会抛出一个异常,可以通过处理器(handler)捕获异常并进行处理。

异常有两种类型:非检查型(unchecked)异常和检查型(checked)异常。

可以使用throws语句抛出异常,在方法名前加上一个throws子句即可:

public static void doSomethingWithClass(String name)
{
	throws ReflectiveOperationException
	{
		Class cl = Class.forName(name);	// might with throw exception
		do something with cl
	}
}

有关异常会在后续文章进行介绍。
##(三)资源
类通常会有一些关联的数据文件,如图像、声音、文本文件等,这些文件被称为资源(resource)。

有关反射不作详细解释,有兴趣的读者可以自行搜索资料。

八、继承的设计技巧

这里给出一些继承的设计技巧:

  1. 将公共操作和字段放在超类中
  2. 不要使用受保护的字段
  3. 使用继承实现“is-a”关系
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 再覆盖方法时,不要改变预期的行为
  6. 使用多态,而不要使用类型信息
  7. 不要滥用反射

参考资料:

狂神说Java
Java核心技术 卷I(第11版)


上一章:Java从零开始系列02:对象与类
下一章:Java从零开始系列04:接口、lambda表达式与内部类

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值