Skip to main content
Jkyo Chen Blog

第五章 继承

继承(is-a) #

#

superclass超类 -> subclass子类 base class基类 -> derived class派生类 parent class夫类 -> child class孩子类

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

继承层次 #

阻止进程:final和方法 #

强制类型转换 #

if(staff[1] instanceof Manager) {
	boss = (Manager) staff[1];
}
  1. 只能在继承层次进行类型转换。
  2. 在将超类转换成子类之前,应该使用instanceof进行检查。
    • 一般情况下少用类型转换和instanceof运算符

抽象类 #

Person p = new Student("Vince Vu", "Economics");
p.getDescription();
//p时一个抽象类Person的变量,Person引用了一个非抽象子类Student的实例。
//由于不能构造抽象类Person的对象,所以变量p永远不会引用Person对象。而是引用诸如Employee或Student这样的具体子类对象,而这些对象中都定义了getDescription()

访问修饰符 #

  1. 仅对本类可见 - private
  2. 对所有类可见 - public
  3. 对本包和所有子类可见 - protected(谨慎使用) - 例子:Object类中的clone方法。
  4. 对本包可见 - 默认(很遗憾),不需要修饰符

object #

equals #

class Employee {
	public boolean equals(Object otherObject) {
		//a quick test to see if the objects are identical
		if(this == otherObject) return false;

		//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;) return false;
		//getClass方法将返回一个对象所属的类。

		//now we know otherObject is a non-null Employee
		Employee other = (Employee)otherObject;

		//test whether the fields have identical values
		return name.equals(name, other.name)
			&& salary == other.salary
			&& hireDay.equals(hireDay, other.hireDay);
		//防备name或者hireDay可能为null的情况 - Objects.equals(a, b);
	}
}

//如果超类中的域相等,就需要比较子类中的实例域。
class Manager extends Employee {
	public boolean equals(Object otherObject) {

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

相等测试与继承 #

//下面实现equals方法的一种常见的错误
public class Employee {
	public boolean equals(Employee other) {
		return Object.equals(name, other.name)
			&& salary == other.salary
			&& Object.equals(hireDay, other.hireDay)
	}
	...
}
//这个方法声明的显示参数类型是Employee。其结果并没有覆盖Object类的equals方法,而是定义了一个完全无关的方法。
//为了避免发生类型错误,可以使用@Override对覆盖超类的方法进行标记:
@Override public boolean equals (Object other)
//如果出现了错误,并且正在定义一个新方法,编译器就会给出错误报告。例如,假设将下面的声明添加到Employee类中:
@Override public boolean equals(Employee other)
//就会看到一个错误报告,这是因为这个方法并没有覆盖超类Object中的任何方法。
API java.util.Arrays 1.2
static Boolean equals(type[] a, type[] b)
//如果两个数组长度相同,并且在对应的位置上位置上数据元素也均相同,将返回true。数组的元素类型可以是Object, int, long, short, char, byte, boolean, float或double.

API java.util.Objects 7
static boolean equals(Object a, Object b)
//如果a和b都为null,返回true;如果只有其中之一为null,则返回false;否则返回a.equals(b).

hashCode方法 #

String类使用下列算法计算散列码:
int hash = 0;
for(int i = 0; i < length(); i++)
	hash = 31 * hash + charAt(i);
String s = "OK";
StringBilder sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
String t = new String("OK");
StringBuilder tb = new StringBuilder(t);
System.out.println(t.hashCode() + " " + tb.hashCode());
//字符串s和t拥有相同的散列码,这是因为字符串的散列码是由内容导出的。
//而字符串缓冲sb与tb却有着不同的散列码,这是因为在StringBuffer类中没有定义hashCode方法,它的散列码是由Object类的默认hashCode方法导出的对象存储地址。
API java.lang.Object 1.0
  int hashCode()
  //返回对象的散列码。散列码可以是任意的整数,包括正数或负数。两个相等的对象要求返回相等的散列码。

  java.lang.Object 7
  int hash(object... objects)
  //返回一个散列码,由提供的所有对象的散列码组合而得到。
  static int hashCode(Object a)
  //如果a为null返回0,否则返回a.hashCode().

  java.util.Arrays 1.2
  static int hashCode(type[] a) 5.0
  //计算数组a的散列码。组合这个数组的元素类型可以是object, int, long, short, char, byte, boolean, float或double.

toString方法 #

public String toString() {
  //return "Employee[name=" + name
  //	+ ",salary=" + salary
  //  	+ ",hireDay=" + hireDay
  // 	+ "]";
  return getClass().getName()
    + "[name=" + name
    + ",salary=" + salary
    + ",hireDay=" + hireDay
    + "]";
  //getClass().getName()获得类名的字符串。
}
Class Manager extends Employee {
  ...
    public String toString() {
      return super.toString()
      	+ "[bonus=" + bonus
        + "]";
    }
}

泛型数组列表 #

访问数组列表的元素 #

//可以使用”for each“循环遍历数组列表
for(Employee e : staff)
  do something with e
//和for循环效果一样。

java.util.ArrayList 1.2
    void set(int index, T obj)
    //设置数组列表指定位置的元素值,这个操作将覆盖这个位置的原有内容。(参数:index 位置(必须介于0~size()-1 之间) obj  新的值)
    T get(int index)
    //获得指定位置的元素值。(参数index 获得的元素位置(必须介于0~size()-1 之间))
    void add(int index, T obj)
    //向后移动元素,以便插入元素。(参数:index 插入位置(必须介于0~size()-1 之间) obj 新元素)
    T remove(int index)
    //删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回。(参数:index  被删除的元素位置(必须介于0~size()-1 之间))

类型化与原始数组列表的兼容性 #

对象包装器与自动装箱 #

//Java SE 5.0的另一个改进便于添加或者获得数组元素
list.add(3);//list.add(Integer.valueOf(3))//自动装箱(自动打包)
//当将一个Integer.对象赋值给一个int值时,将会自动拆箱
int n = list.get(i);//int n = list.get(i).intValue();

Integer n = 3;
n++;
//编译器将自动地插入一条对象拆箱的指令,然后进行自增计算,最后再将结果装箱。

Integer a = 1000;
Integer b = 1000;
if(a == b)...
//检测的是对象是否指向同一个存储区域,Java实现有可能让它成立。如果将经常出现的值包装到同一个对象中,这种比较就有可能成立。解决这个问题的办法是在两个包装器对象比较时调用equals方法。
//包装器类不可以用来实现修改数值参数。
//Java是值传递。
public static void triple(int x) {//won't work
  x = 3 * x;
}
public static void triple(Integer x) {//won't work
  ...
}
//Integer对象是不可变的:包含在包装器中的内容不会改变。

//编写一个修改数值参数值的方法,就需要使用在org.omg.CORBA包中定义的持有者(holder)类型,包括IntHolder,BooleanHolder等。每个持有者类型都包含一个公有(!)域值,通过它可以访问存储在其中的值。
public static void tripe(IntHolder x) {
  x.value = 3 * x.value;
}
API java.lang.Integer 1.0
  int intValue()
  //以int的形式返回Integer对象的值(在Number类中覆盖了intValue方法)
  static String toString(int i)
  //以一个新String toString(int i, int radix)
  static int parseInt(String s)
  //返回字符串s表示的整型数值,给定字符串表示的是十进制的整数
  static int parseInt(String s, int radix)
  //返回字符串s表示的整型数值,是radix参数进制的整数
  static Integer valueOf(String s)
  //返回用s表示的整型数值进行初始化后的一个新Integer对象,给定字符串表示的是十进制的整数
  static Integer valueOf(String s, int radix)

  java.text.NumberFormat 1.1
  Number parse(String s)
  //返回数字值,假设给定的String表示类一个数值

参数数量可变的方法 #

System.out.printf("%d", n);
System.out.printf("%d %s", n, "widgets");

//调用的都是同一个方法
public class PrintStream {
	public PrintStream printf(String fmt, Object... args) {
		return format(fmt, args);
	}
}
//这里的省略号... 是Java代码的一部分,它表明这个方法可以接受任意数量的对象(除fmt参数之外)
//prinf方法接受两个参数,一个是格式化字符串,另一个是Object[]数组,其中保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值,自动装箱功能将把它们转换成对象)。
//现在将扫描fmt字符串,并将第i个格式说明符与args[i]的值匹配起来。
//对于printf的实现者来说,Object... 参数类型与Object[]完全一样。
//编译器需要对printf的每次调用进行转换,以便将参数绑定到数组上,并在必要的时候进行自动装箱:
System.out.printf("%d %s", new Object[] { new Integer(n), "widgets"});

public static double max(double... values) {
	double largest = Double.MIN_VALUE;
	for(double v : values) if(v > largest) largest = v;
	return largest;
}
double m = max(3.1, 40.4, -5);
//编译器将new double[]{3.1, 40.4, -5}传递给max方法。

//允许将一个数组传递给可变参数方法的最后一个参数。
System.out.printf("%d %s", new Object[] { new Integer(1), "widgets"});

//因此将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法,而不会破坏任何已经存在的代码。
//MessageFormat.format在Java SE 5.0就采用了这种方式。甚至可以将main方法声明下列形式:
public static void main(String... args)

枚举类 #

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;
	private Size(String abbreviation) { this.abbreviation = abbreviation;}
	public String getAbbreviation() { return abbreviation;}
}
package enums;
import java.util.*;

public class EnumTest {
	Scanner in = new Scanner(System.in);
	System.out.print("Enter a size: (SMALL, MEDIUM, LARGE, EXTRA_LARGE)");
	String input = in.next().toUpperCase();
	Size size = Enum.valueOf(Size.class, input);
	System.out.println("size=" + size);
	System.out.println("abbreviation=" + size.getAbbreviation());
	if(size == Size.EXTRA_LARGE)
		System.out.print("Good job--you paid attention to the _.")
}
enum Size {
	SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
	private String abbreviation;
	private Size(String abbreviation) { this.abbreviation = abbreviation;}
	public String getAbbreviation() { return abbreviation;}
}
API java.lang.Enum 5.0
	static Enum valueOf(Class enumClass, String name)
	//返回指定名字,给定类的枚举常量
	String toString()
	//返回枚举常量名。
	int ordinal()
	//返回枚举常量在enum声明中的位置,位置从0开始计数。
	int compareTo(E other)
	//如果枚举类型出现在other之前,则返回一个负值;如果this == other,则返回0;否则,返回正值。枚举常量的出现次序在enum声明中给出。

反射 #

复杂

Class类 #

继承设计的技巧 #

  1. 将公共操作和域放在超类。

  2. 不要使用受保护的域。(protected机制不能带来更好的保护)

    • 子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实力域,从而破坏封装性。
    • 在Java程序设计语言中,在同一个包中的所有类都可以访问protected域,而不管它是否是为这个类的子类。
    • 不过protected方法对于指示那些不提供一般用途而在子类中重新定于的方法很有用。
  3. 使用继承实现"is-a"关系。(确认是否为is-a的关系)

  4. 除非所有继承的方法都有意义,否则不要使用继承。

  5. 在覆盖方法时,不要改变预期的行为。

    • 置换原则不仅应用于语法,而且也可以应用于行为。覆盖方法的时候,不应该毫无原由地改变行为的内涵。
    • 覆盖方法不要偏离最初的设计想法。
  6. 使用多态,而非类型信息。

    if(x is of type1)
    	action1(x);
    else if(x if of type2)
    	action2(x);
    //考虑多态性。
    //如果action1和action2是相同的概念,就应该为这个概念定义一个方法,并将其放置在两个类的超类或接口中,然后就可以调用x.action();以便使用多态性提供的动态分派机制执行相应的动作。
    • 使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。
  7. 不要过多的使用反射。

    • 反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但通常不适于编写应用程序。
    • 反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。