浅谈Java中的继承

示例中定义了一个基类(父类、超类)Employee,然后class Manager extends Employee

1
2
3
4
5
6
7
8
9
10
11
12
class Employee {
private static int nextId = 1;
private String name;
private double salary;
private int id;
//......
//..省略getter和setter以及构造方法...
}

class Manager extends Employee {
double bonus; //奖金
}

类、超类和子类

super关键字

这里我们希望调用超类中的getSalary()方法,而不是自调用,需要使用特定的关键字super:

1
2
3
4
5
6
7
8
9
10
//使用超类中带有n,s,year,month,day参数的构造器
public Manager(String n, double s, int year, int month, int day) {
super(n,s,year,month,day);//必须写在第一句
bonus = 0;
}
//普通方法
public double getSalary() {
double baseSalary = super.getSalary();//调用超类中的getSalary()方法
return baseSalary + bonus;
}

注意:super不是对象的引用,和this不同,它只是一个指示编译器调用超类方法的特殊关键字

  • 不能删除继承的任何域和方法
  • 如果父类没有不带参数的构造器,子类又没有显示调用其他超类带构造器,系统会报错。

多态

一个对象变量可以指示多种实际类型的现象被称为多态(Polymorphism).在运行时能够自动地选择调用哪个方法的现象称为动态绑定。
一个判断是否应该设计为继承关系的规则:”is-a”规则。

  • 动态绑定和静态绑定
  1. 动态绑定是指在类的继承关系中,编译器在调用函数时可以根据声明类型、方法名、参数去动态调用与之完全匹配的方法。优先调用当前声明的子类的方法
    一个类中,方法名字和参数列表称为方法的签名,而返回类型不是方法的签名。子类中可以覆盖超类中相同签名的方法,因此在覆盖方法是们一定要保证返回类型的兼容性。
    1
    2
    3
    4
    //假设在超类Employee中有:
    public Employee getBuddy() {...}
    //子类Manager要覆盖这个方法:
    public Manager getBuddy() {...}//It's ok to change the return type
  2. 如果是private、static、final方法或着构造器,那么编译器可以准确地知道调用什么方法,这种调用方式称为静态绑定

注意:再覆盖方法时,子类方法不能低于超类方法的可见性(public>private)

final关键字:阻止继承

在定义类的时候使用final修饰符阻止人们定义该类的子类:

1
final class Executive extends Manager {...}

类中的特定方法也可以被声明为final,这样做子类就不能再覆盖这个方法:

1
2
3
4
5
6
7
class Employee {
...
public final String getName() {
return name;
}
...
}

域也可以被声明为final。对于fianl域,构造对象之后就不允许改变它们的值了。不过如果将一个类声明为final,只有其中的方法自动地成为final,而不包括域。

强制类型转换

将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋值给一个子类变量,必须进行类型转换。为避免ClassCastException异常,应该养成一个良好的程序设计习惯:在进行类型转换之前,先查一下能否成功地转换,使用instanceof运算符:

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

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

抽象类

使用abstract关键字的方法,不需要实现:

1
2
public abstract String getDescription();
//no implementation required

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

1
2
3
4
5
6
7
8
9
10
11
12
abstract class Person {
private String name;
public Person(String n) {
name = n;
}
//抽象方法的具体实现在子类中
public abstract String getDescription();

public String getName() {
return name;
}
}

抽象类的子类如果没有实现所有的抽象方法,则必须将子类也标为抽象类,因为这种情况下子类中还是有抽象方法。
抽象类不能被实例化,但可以创建一个具体子类的对象。

1
2
3
4
5
6
7
8
9
10
11
new Person("Vincent");//error

Person p = new Student("Vincent","Economics");//ok!

Person[] people = new Person[2];
people[0] = new Student(...);
people[1] = new Employee(...);
for (Person p:people) {
System.out.println(p.getName()+","+p.getDescription());
}
//如果我们省略超类中的getDescription()方法,而仅在子类中定义该方法,那我们就不能通过变量p调用getDescription方法了。

Object:所有类的超类

Object类是Java中所有类的超类,即Java中每个类都是由它拓展而来。只有基本类型(primitive types)不是对象,例如数值、字符、布尔类型。所有的数组类,不管是对象数组还是基本类型的数组都拓展自Object类。
Object类有一些共同的方法:

1. equals方法

Object类中的equals方法用于检测两个对象是否相等。Object类中的这个方法返回两个对象是否具有相同的引用,在很多情况下这是没有意义的。所以对于自定义类我们会重写equals方法。
相等测试与继承
Java语言规范要求equals方法具有:自反性,对称性,传递性,一致性,对于非空x有x.equals(null) = false.
书中用了getClass()方法来检测,许多程序元也喜欢用instanceof进行检测,关于instanceof的效果参考langya2007
但是用instanceof检测违背了对称性。例如 e.equals(m)返回true,而m.equals(e)返回false(这里m(manager)是e(employee)的子类)。

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

2. hashCode方法

散列码是由对象导出的一个整型值,散列码是没有规律的,不同对象的散列码基本不同。hashCode方法定义在Object类中,每个对象都有一个默认的散列码,其值为对象的存储地址。
字符串String的散列码是由内容导出的,that is内容相同的字符串散列码相同。但是,StringBuffer类没有定义hashCode方法,它的散列码是由Object类的默认hashCode方法到处的对象存储地址。

1
2
3
4
5
6
//String使用下列方法计算散列码:
int hash = 0;
for (int i = 0; i < length(); i++) {
hash = 31 * hash + charAt(i);
}
//所以可以理解为什么相同内容的String的散列码相同

如果重新定义equals方法,就必须重新定义hashCode方法,以便用户将对象插入到散列表中

1
2
3
4
5
6
7
8
9
10
//使用null安全的方法Objects.hashCode()方法获取散列码
public int hashCode() {
return 7 * Objects.hashCode(name) +
+ 11 * new Double(salary).hashCode() +
+ 13 * Objects.hashCode(hireDay);
}
//需要组合多个散列值时,可以调用Objects.hash()并提供多个参数
public int hashCode() {
return Objects.hash(name, salary, hireDay);
}

Equals和hashCode定义必须一直,如果x.equals(y)返回true,那么两个对象的散列码必须相同,例如如果定义Employee.equals比较雇员ID,那么hashCode方法就需要散列ID,而不是雇员的姓名或存储地址。
另外,数组类型可以使用Arrays.hashCode()方法计算一个散列码。

3. toString方法

返回对象值的字符串,且绝大多数toString方法都是返回:类型名[域值]
只要对象与一个字符串通过操作符+连接起来,Java编译器就会自动地调用 toString()
Object类定义了toString方法,打印输出对象所属的类名和散列码:

1
2
3
System.out.println(System.out);
//输出:java.io.PrintStream@2f6684
//PrintStream类的设计者没有覆盖toString方法

! 数组继承了object类的toString方法,所以在打印数组时我们不用.toString(),而是使用静态方法Arrays.toString(...)或者Arrays.deepToString(...)(多维数组)。

泛型数组列表

Example: ArrayList<>

对象包装器与自动装箱

Integer,Long,Float,Double,Short,Byte,Character,Void,Boolean(前6个类派生自公共的超类Number)。对象包装器类是不可变的,一旦够着了包装器就不允许更改包装在其中的值,且包装器类都是final,不能定义子类。
自动装箱拆箱是非常普遍的~
如何编写一个修改数值参数值的方法:需要使用在org.omg.CORBA中定义的holder类型。

1
2
3
4
5
6
7
8
9
10
11
12
public static void triple(IntHolder x) {
x.value = 3 * x.value;
}

//以下是错误示范:
public static void triple(int x) { //won't work
x = 3 * x;
}

public static void triple(Integer x) { //won't work
x = 3 * x;
}

参数数量可变的方法

我们看printf的方法定义:

1
2
3
4
5
public class PrintStream {
public PrintStream printf(String fmt,Object... args) {
return format(fmt,args);
}
}

...表明这个方法可以接受任意数量的对象(除fmt参数外)

枚举类

ClassName.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum ClassName {
STRING("String"),INTEGER("Integer"),BYTE("Byte"),
BIGDECIMAL("BigDecimal"),TIME("time"),STRING_LIKE("String-like"),
LONG("Long"),INTEGER_IN("Integer-in"),DATE_BETWEEN("Date-between"),
JOIN("Join"),SUBQUERY("SubQuery");

private String lowercase;
ClassName(String lowercase) {
this.lowercase = lowercase;
}
public String getLowercase() {
return lowercase;
}
}

EnumTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EnumTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Enter a classname:");
String input = scanner.next().toUpperCase();
ClassName size = Enum.valueOf(ClassName.class, input);
System.out.println("enter classname is:" + size.getLowercase());
}
}

/**输出:
*Enter a classname:
*string
*enter classname is:String
*/

在比较两个枚举类型的值时,不需要用equals,而直接使用”==“。所有枚举类型都是Enum类的子类,他们继承了Enum的许多方法,例如toString(), Enum.valueOf()

1
2
3
4
ClassName.STRING.toString();//返回字符串"STRING"
ClassName cn = Enum.valueOf(ClassName.class, "STRING");
ClassName[] values = ClassName.values();//返回包含全部枚举值的数组
int index = ClassName.STRING.ordinal();//返回枚举常量STRING的位置0

反射

反射库提供了一个非常丰富且精心设计的工具集,以便编写能够移动操作Java代码的程序。
能够分析类能力的程序称为反射(reflective),反射机制功能:

  • 在运行中分析类的能力
  • 在运行中查看对象,例如编写一个toString方法供所有类使用
  • 实现通用的数组操作代码
  • 利用Method对象,这个对象很像C++中的函数指针

Class类

在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。
通过专门的Java类Class可以访问这些信息

  1. Object类中的getClass()方法返回一个Class类型的实例

    1
    2
    3
    Employee e;
    ...
    Class c1 = e.getClass();

    一个Class对象表示一个特定类的属性。

  2. 最常用的Class方法是getName()

    1
    2
    3
    4
    5
    6
    7
    System.out.println(e.getClass().getName);
    //输出: Employee

    //如果类在一个包里,包的名字也作为类名的一部分
    Date d = new Date();
    Class c2 = d.getClass();
    String name = c2.getName();//name is set to "java.util.Date"
  3. 调用静态方法forName()获得类名对应的Class

    1
    2
    String className = "java.util.Date";
    Class c1 = Class.forName(className);//注意检查异常
  4. 获取Class对象最简单的方法就是在类名后面加.class

getName()方法应用在数组类型时会返回比较奇怪的名字,历史原因
虚拟机为每个类型管理一个Class对象。因此可以利用”==”比较两个类对象:

1
2
3
if (e.getClass() == Employee.class) {
...
}
  1. 快速创建一个类的实例:
    1
    Object ee = e.getClass().newInstance();

捕获异常

在可能抛出已检查异常的一个或多个方法调用代码放在try块中,然后再catch子句中提供处理器代码。

继承设计的技巧

  1. 将公共操作和域放在超类
  2. 不要使用受保护的域
    protected机制并不能带来很好的保护,一个是因为子类集合是无限的,二是一个包中的所有类都可以访问protected域。
  3. 使用继承实现”is-a”关系
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 在覆盖方法时,不要改变预期行为
  6. 使用多态,而非类型信息。
  7. 不要过多的使用反射