JAVA学习笔记——面向对象编程:继承1

目录

  • 类、超类和子类
    • 超类和子类
    • 覆盖方法
    • 子类构造器
    • 多态
    • 方法的调用过程
    • 阻止继承:final 类和方法
    • 强制类型转换
    • 抽象类
    • 受保护访问
  • Object 类:所有类的超类
    • equals 方法
    • hashCode 方法
    • toString 方法

类、超类和子类

超类和子类

关键字 extends 表明正在构造的新类派生于一个已存在的类。已存在的类称为超类 (superclass)基类 (base class)父类 (parent class);新类称为子类 (subclass)派生类 (derived class)孩子类 (child class),Java 中一般称为“超类”和“子类”。

子类继承于超类,拥有与超类相同的方法和实例域,并且还有一些超类不具有的新特性,这种现象称为 “继承”。一般来说,子类比超类具有更丰富的特性,“继承” 是一个把类从一般到特殊的过程。Java 中子类只允许继承自一个超类,即每个子类有且仅有一个超类。

技巧:在定义的过程中,仅需在子类中定义与超类不同的部分,与超类相同的部分不需要重复定义。

// Employee 类
class Employee
{
   private String name;
   private double salary;
   private LocalDate hireDay;

   public Employee(String n, double s, int year, int month, int day)
   {
      name = n;
      salary = s;
      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;
      return;
   }
}
// Manager 类
public class Manager extends Employee
{
	private double bonus;
	public void setBonos(double bonus)
	{
		this.bonus = bonus;
	}
}

由一个公共超类派生出来的所有类的集合被称为继承层次 (inheritance hierarchy),如图 1 所示。在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链 (inheritance chain)。通常,一个祖先类可以拥有多个子孙继承链。

JAVA学习笔记——面向对象编程:继承1_第1张图片

图1 Employee 继承层次

覆盖方法

当超类的方法在子类中并不适用时,可以在子类中重新定义该方法,从而实现 “方法覆盖” (override)。见下例:

public class Manager extends Employee
{
	public double getSalary()
	{
		return salary + bonus;	// won't work
	}
}

这个方法并不能运行。这是因为 Manager 类的 getSalary 方法不能够直接地访问超类的私有域。也就是说,尽管每个 Manager 对象都拥有一个名为 salary 的域,但在 Manager 类的 getSalary 方法中并不能够直接地访问 salary 域。只有 Employee 类的方法才能够访问私有部分。如果 Manager 类的方法一定要访问私有域,就必须借助于公有的接口, Employee 类中的公有方法 getSalary 正是这样一个接口。

public class Manager extends Employee
{
	public double getSalary()
	{
		double baseSalary = getSalary();  // still won't work
		return baseSalary + bonus;
	}
}

上面这段代码仍然不能运行。问题出现在调用 getSalary 的语句上,这是因为 Manager 类也有一个 getSalary 方法(就是正在实现的这个方法)所以这条语句将会导致无限次地调用自己,直到整个程序崩溃为止。

public class Manager extends Employee
{
	public double getSalary()
	{
		double baseSalary = super.getSalary();
		return baseSalary + bonus;
	}
}

实际上,我们希望调用 Employee 类中的 getSalary 方法,因此,适用 super 关键字,可以指定调用父类中的方法。在“覆盖方法”时,需访问权限和调用的是哪个方法。

super 是提示编译器调用超类方法的特殊关键字,并非对象的引用,不能赋给另一个对象变量
this 是对特定对象的引用,可以赋给另一个对象变量,二者并不相同

子类构造器

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

此处的 super 表示调用超类中的构造器 Employee(String n, double s, int year, int month, int day)。由于 Manager 类的构造器不能访问 Employee 类的私有域, 所以必须利用 Employee 类的构造器对这部分私有域进行初始化,我们可以通过 super 实现对超类构造器的调用。调用构造器的语句只能作为另一个构造器的第一条语句出现。

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

多态

在 Java 程序设计语言中,对象变量是多态的。 一个 Employee 变量既可以引用一个 Employee 类对象, 也可以引用一个 Employee 类的任何一个子类的对象(例如,ManagerExecutiveSecretary 等)。

当超类变量引用 Employee 对象时,调用 getSalary 方法,则编译器调用 Employee 中的 getSalary 方法;当超类变量引用 Manager 对象时,调用 getSalary 方法,则编译器调用 Manager 中的 getSalary 方法。这样的现象称为“多态”。

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

      Employee[] 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());
   }
}
/**
  name=Carl Cracker,salary=85000.0
  name=Harry Hacker,salary=50000.0
  name=Tommy Tester,salary=40000.0
 */

注意:在上例中,bossstaff[0] 引用的是同一个 Manager 对象,但在定义 staff 时,定义为 Employee 类型,因此不能这样调用:staff[0].setBonus(5000)

此处须区分与“多态”的区别

  • 多态实现的前提在于,超类和子类中都具有该方法,由于子类相比于超类,具有“特殊性”,因此进行了方法覆盖。
  • 而上述错误中,setBonus 是子类特有的方法,而超类却不具有,因此超类变量不能调用子类特有的方法。

在使用“多态”时,应当时刻关注变量类型和对象类型。

方法的调用过程

假设要调用 x.f(args) 方法,x 是声明为类 C 的一个对象,方法的调用过程如下:

  1. 编译器将会一一列举所有 C 类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法。
  2. 编译器根据调用的参数,进行匹配,确定调用的是哪一个 f 方法。若成功匹配,则进行下一步;若没有匹配成功,则会报错。(假设成功匹配到方法 f(String)
  3. 如果是 private 方法、static 方法、final 方法或者构造器, 那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定 (static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
  4. 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。假设 x 的实际类型是 D,它是 C 类的子类。如果 D 类定义了方法 f(String) 就直接调用它;否则,将在 D 类的超类中寻找 f(String) 以此类推。

动态绑定有一个非常重要的特性: 无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类 Executive, 并且变量 e 有可能引用这个类的对象,我们不需要对包含调用 e.getSalary() 的代码进行重新编译。如果 e 恰好引用一个 Executive 类的对象,就会自动地调用 Executive.getSalary() 方法。

阻止继承:final 类和方法

final 类

不允许扩展的类被称为 final 类。如果在定义类的时候使用了 final 修饰符就表明这个类是 final 类。final 类不允许成为任何其他类的超类。

public final class Executive extends Manager
{
	...
}

final 方法

类中的特定方法也可以被声明为 final。如果这样做,子类就不能覆盖这个方法( final 类中的所有方法自动地成为 final 方法;但域不会,仍保持原本状态,用 final 修饰的域,构造对象之后就不允许改变它们的值了)。

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

强制类型转换

将一个值存人变量时,编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换,否则将无法通过编译器检查。

if (staff[1] instanceof Manager)
{
	boss = (Manager) staff[1]:
	...
}

如果这个类型转换不可能成功,编译器就不会进行这个转换。

  • 只能在继承层次内进行类型转换。
  • 在将超类转换成子类之前,应该使用 instanceof 操作符进行检查。

在一般情况下,应该尽量少用类型转换和 instanceof 运算符。

抽象类

在类的继承层次中,越上层的超类,具有的功能越少,“通用性”越强;越下层的子类,具有的功能越多,“通用性”越差。因此,一般最上层的超类可以定义为抽象类

PersonEmployeeStudent 三者的关系中,Person 作为另外两个的超类。而在实际编程中,很少会用到 Person 对象,由于其太过抽象,功能太少,但它仍具有 EmployeeStudent 类的公共特性,例如姓名,年龄,性别等,因此可以考虑将 Person 定义为一个抽象类。

// 抽象类定义
public abstract class Person
{
   public abstract String getDescription();
   private String name;

   public Person(String name)
   {
      this.name = name;
   }

   public String getName()
   {
      return name;
   }
}
  • 抽象类是指在普通类的结构里面增加抽象方法的组成部分。不含抽象方法的类也可以定义为抽象类。
  • 对于抽象方法,使用 abstract 修饰,不需要定义其具体的实现过程,具体的实现在子类继承时完成,抽象方法在抽象类中起到一个占位符的作用。
  • 具有抽象方法的类,都必须定义为抽象类,且抽象类不能实例化为某一个具体的对象。
/**
 * This program demonstrates abstract classes.
 * @version 1.01 2004-02-21
 * @author Cay Horstmann
 */
public class PersonTest
{
   public static void main(String[] args)
   {
      Person[] people = new Person[2];

      // fill the people array with Student and Employee objects
      people[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
      people[1] = new Student("Maria Morris", "computer science");

      // print out names and descriptions of all Person objects
      for (Person p : people)
         System.out.println(p.getName() + ", " + p.getDescription());
   }
}

上例中,子类 EmployeeStudent 类已将 getDescription 方法扩展。在循环中,定义了 Person 变量 p,由于 Person 为抽象类,因此 p 实际引用的是 EmployeeStudent 对象,而不可能是 Person 对象。

在抽象类 Person 中,若没有定义抽象方法 public abstract String getDescription();,则上述程序会报错。原因在于:getDescription 方法只定义在子类中的话,pPerson 变量,超类无法调用子类方法。

更多抽象类介绍

受保护访问

任何声明为 private 的内容对其他类都是不可见的,这对于子类来说也完全适用,即子类也不能访问超类的私有域。

然而,在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为 protected

受保护的方法更具有实际意义。如果需要限制某个方法的使用, 就可以将它声明为 protected。这表明子类(可能很熟悉祖先类)得到信任,可以正确地使用这个方法,而其他类则不行。

修饰符总结

  • private:仅对本类可见
  • public:对所有类可见
  • protected:对本包和所有子类可见
  • 无修饰符(默认):对本包可见

Object 类:所有类的超类

Object 类是 Java 中所有类的始祖,在 Java 中每个类都是由它扩展而来的。如果没有明确地指出超类,Object 就被认为是这个类的超类。下面介绍关于 Object 类的一些基本内容。

equals 方法

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。

然而,对于多数类来说,这种判断并没有什么意义。例如,采用这种方式比较两个 PrintStream 对象是否相等就完全没有意义。经常需要检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。对于自定义类,我们可以覆盖 equals 方法。

public class Employee
{
	...
	// 注意,超类中的 equals 方法的签名是 equals(Object o)
	// 如果自定义时,定义 equals(Employee e)
	// 则定义的是另一个重载方法,而不是对超类的 equals 进行覆盖
	// 可以用 @Override 对超类的方法进行标记,便于检查是否正确覆盖
	// @Override public boolean equals(Employee other)
	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 other = (Employee) otherObject;
		// test whether the fields have identical values
		return name.equals(other.name) && salary = other.salary && hireDay.equals(other.hireDay);
	}
}

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

public class Manager extends Employee
{
	...
	public boolean equals(Object otherObject)
	{
		if (!super.equals(otherObject)) return false;
		// super.equals checked that this and otherObject belong to the same class
		Manager other = (Manager) otherObject;
		return bonus == other.bonus;
	}
}

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

  1. 自反性:对于任何非空引用 x,x.equals(x) 应该返回 true
  2. 对称性:对于任何引用 x 和 y,当且仅当 y.equals(x) 返回 truex.equals(y) 也应该返回 true
  3. 传递性:对于任何引用 x、y 和 z,如果 x.equals(y) 返回 truey.equals(z) 返回 true,则 x.equals(z) 也应该返回 true
  4. 一致性:如果 x 和 y 引用的对象没有发生变化,反复调用 x.equaIs(y) 应该返回同样的结果。
  5. 对于任意非空引用 x,x.equals(null) 应该返回 false

一些重新定义 equals 方法的建议

  1. 显式参数命名为 otherObject,稍后需要将它转换成另一个叫做 other 的变量。
  2. 检测 thisotherObject 是否引用同一个对象。
  3. 检测 otherObject 是否为 null,如果为 null,返回 false。这项检测是很必要的。
  4. 比较 thisotherObject 是否属于同一个类。如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测。
  5. otherObject 转换为相应的类类型变量。
  6. 对所有需要比较的域进行比较。

hashCode 方法

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

public class Employee
{
	// method 1
	public int hashCode()
	{
		return 7 * name.hashCode() 
			+ 11 * new Double(salary).hashCode() 
			+ 13 * hireDay.hashCode();
	}
	...
}

// method 2
public int hashCode()
{
	return 7 * Objects.hashCode(name)
		+ 11 * Double.hashCode(salary)   // 此处使用静态方法,避免新构造一个 Double 对象
		+ 13 * Objects.hashCode(hireDay);
}

// method 3
public int hashCodeO
{
	// Objects 是 JDK7 中引入的工具类
	// Objects.hash 方法可以自动调用参数的 hashCode 方法
	// 并将各个 hashCode 进行组合,得到最终的 hashCode
	return Objects.hash(name, salary, hireDay);
}

如果重新定义 equals 方法,就必须重新定义 hashCode 方法,以便用户可以将对象插人到散列表中。Equals 与 hashCode 的定义必须一致:如果 x.equals(y) 返回 true,那么 x.hashCode() 就必须与 y.hashCode() 具有相同的值。例如,如果用定义的 Employee.equals 比较雇员的 ID,那么 hashCode 方法就需要散列 ID,而不是雇员的姓名或存储地址。

toString 方法

Object 中还有一个重要的方法,就是 toString方法,它用于返回表示对象值的字符串。toString 方法是一种非常有用的调试工具。在标准类库中,许多类都定义了 toString 方法,以便用户能够获得一些有关对象状态的必要信息。绝大多数(但不是全部)的 toString 方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。

当自定义子类时也应该定义子类的 toString 方法,并将子类域的描述添加进去。如果超类使用了 getClass().getName(),那么子类只要调用 super.toString() 就可以了。

只要对象与一个字符串通过操作符 “+” 连接起来,Java 编译就会自动地调用 toString 方法,以便获得这个对象的字符串描述。Object 类定义了 toString 方法,用来打印输出对象所属的类名和散列码。

建议:为自定义的每一个类增加 toString 方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。


参考资料

  1. 《Java核心技术 卷1 基础知识》
  2. 《Java抽象类 详解》

你可能感兴趣的:(JAVA学习笔记,java,编程语言,继承,抽象类,object)