继承
4.1. 类、超类和子类
-
关键字extends
- 表明正在构造的新类派生于一个已存在的类。已存在的类成为超类(superclass),基类(base class)或父类(parent class);新类成为子类(subclass),派生类(derived class)或孩子类(child class)。
超类并不因为位于子类之上,拥有比子类更多的功能。实际上,子类比超类拥有的功能更加丰富。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。
当超类中的有些方法不适用于子类时,可以覆盖(override)掉超类中的这个方法。
子类的方法不能够直接地访问超类的私有域。如果一定要访问私有域,需要借助公有的接口(访问器方法)。
-
关键字super
-
调用超类的方法:
super.getSalary();
-
调用超类的构造器
使用super调用构造器的语句必须是子类构造器的第一条语句。public Manager(String n,double s,int year) { super(n,s,y); bonus=0; }
如果子类的构造器没有显式地调用超类的构造器,将自动地调用超类默认的构造器(无参构造器)。
继承层次
- 由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。
多态
- 一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。
- 在Java中,一个Employee变量既可以引用一个Employee对象,也可以引用Employee类的任何一个子类的对象。
-
动态绑定
- 对象变量在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。
调用对象方法的过程:
- 编译器查看对象的声明类型和方法名。获得所有可能被调用的候选方法。
- 编译器查看调用方法时提供的参数类型。这个过程被称为重载解析(overloading resolution)。
- 如果是private方法、final方法、static方法或构造器,那么编译器可以准确地知道该调用哪个方法。这种调用方式成为静态绑定(static binding)。
- 当采用动态绑定方法调用方法时,虚拟机一定调用与所引用对象的实际类型最合适的类的方法。
每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table)。其中列出了所有方法的签名和实际调用的方法。
在覆盖一个方法时,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法一定要声明为public。
阻止继承:final类和方法
- 不允许扩展的类被称为final类。
final class Excutive extends Manager
- 类中的特定方法也可以被声明为final。子类就不能覆盖这个方法(final类中所有的方法自动成为final方法,但不包括域)。
强制类型转换
- 将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换。
- 强制类型转换:用一对圆括号将目标类名括起来,放在需要转换的对象引用前
Manager boss = (Manager)staff[0];
- 只能在继承层次内进行类型转换
- 在将超类转换为子类之前,应该使用instanceof进行检查
if (staff[1] instanceof Manager)
抽象类
- 使用abstract关键字,则在该类中不需要实现这个方法
public abstract String getDescription();
- 包含一个或多个抽象方法的类必须被声明为抽象类
- 抽象方法充当着占位的角色,它们的具体实现在子类中
- 类即使不含抽象方法,也可以将类声明为抽象类
- 抽象类不能被实例化。但可以定义一个抽象类的对象变量,它只能引用抽象子类的对象。
Person p=new Student("Xiaoming");
-
受保护访问
- 将域或方法声明为protected:超类中的域或方法可以被子类访问
Java控制可见性的4个访问修饰符:
- 仅对本类可见:private
- 对本包可见:default(默认,不需要修饰符)
- 对本包及所有子类可见:protected
- 对所有类可见:public
4.2. Object类:所有类的超类
- Object类是所有类的超类,但不需要特别指出。如果没有明确指出超类,Object类就被认为是这个类的超类。
- 可以使用Object类的变量引用任何类型的对象
- 在Java中,只有基本数据类型不是对象。所有的数组类型,不管是对象数组还是基本类型的数组,都扩展于Object类。
equals方法
- 用于检测一个对象是否等于另外一个对象(判断是否具有相同的引用)
- 为了防备变量为null的情况,使用Object.equals方法。if (Object.equals(name,other.name))
- 如果两个参数都为null,返回true;如果其中一个为null,返回false;如果都不为null,调用name.equals(other.name)。
相等测试与继承
Java要求equals方法需要具有的特性:
- 自反性:x.equals(x)应该返回true
- 对称性:y.equals(x)与x.equals(y)的结果应该相同
- 传递性:如果x.equals(y)为true,y.equals(z)为true,那么x.equals(z)也为true
- 一致性:如果x、y引用的对象没有变化,反复调用x.equals(y)应该返回同样的结果
- 对于任意非空引用x,x.equals(null)应该返回false
编写完美的equals方法:
- 显式参数命名为otherObject,稍后转换成other变量
- 检测this与otherObject是否引用同一个对象
if(this==otherObject)
- 检测otherObject是否为null,如果为null,返回false
if(otherObject==null)
- 比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,使用getClass检测:
if(getClass()==otherObject.getClass())
。如果所有子类有统一的语义,使用instanceof检测:if(otherObject instanceof ClassName)
- 将otherObject转换为相应的类类型变量
ClassName other=(ClassName)otherObject
- 比较所有的域。使用==比较基本类型域,使用equals比较对象域。
hashCode方法
- 散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。
toString方法
- 返回表示对象值的字符串
- 数组使用toString方法会按旧的格式打印。最好使用静态方法Arrays.toString。
4.3. 泛型数组列表
ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,如ArrayList<Employee>
。
/* ArrayList 用法 */
//在Java7中,可以省去右边的类型参数
ArrayList<Employee> staff = new ArrayList<Employee>();
//添加对象
staff.add(new Employee("Harry"));
//在数组列表中间插入元素
staff.add(i,harry);
//删除一个元素
staff.remove(i);
//返回数组列表中实际元素数目
staff.size();
//确认数组列表大小不再变化时,可以将存储区域的大小调整为当前元素数量所需的存储空间数目
staff.trimToSize();
/* 访问数组列表元素 */
//设置(替换!)第i个元素
staff.set(i,harry);
//获得数组列表的元素
Employee e=staff.get(i);
//使用for each遍历
for(Employee e:staff)
do something
4.4. 对象包装器与自动装箱
包装器
- 有时候需要将基本类型转换为对象使用。所有的基本类型都有一个与之对应的类。如,Integer类对应基本类型int。这些类称为包装器(wrapper)。
- 对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。
包装器的使用:
ArrayList<Integer> list=new ArrayList<>();
- 自动装箱(autoboxing):
list.add(3);
将自动变换成list.add(Integer.valueOf(3));
- 自动拆箱:
int n=list.get(i);
- 自动装箱和拆箱是编译器认可的,而不是虚拟机。
使用包装器的好处:
- 可以将一些基本方法放置在包装器中,如将一个数字字符串转换成数值。
int x=Integer.parseInt(s);
4.5. 参数数量可变的方法
System.out.printf("%d",n);
与System.out.printf("%d %S",n,"widgets");
虽然参数数量不同,但是调用了同一个方法。
printf方法是这样定义的:public PrintStream printf(String format,Object... args) ....
其中的省略号是Java代码的一部分,表明这个方法可以接收任意数量的对象。
编译器需要对printf的每次调用进行转换,以便将参数绑定到数组上,并在必要的时候进行自动装箱。System.out.printf("%d %s",new Object[]{new Integer(n),"widgets"});
4.6. 枚举类
/* 枚举类的使用 */
public class EnumTest {
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
//每个枚举类型都有一个静态的values方法,返回一个包含全部枚举值的数组
System.out.println("Enter a size:"+Arrays.toString(Size.values()));
String input = in.next().toUpperCase();
//将size设置为input对应的枚举实例
Size size=Enum.valueOf(Size.class, input);
//toString方法返回枚举常量名
System.out.println(size.toString());
//ordinal方法返回enum声明中枚举常量的位置
System.out.println("index="+ size.ordinal());
System.out.println("size=" + size);
System.out.println("abbreviation=" + size.getAbbreviation());
// 比较两个枚举类型的值,不需要调用equals,直接使用“==”
if (size==Size.EXTRA_LARGE) {
System.out.println("You should pay attention to your weight");
}
}
}
enum Size{ //声明枚举类
//类有5个实例
SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
private String abbreviation;
private Size(String abbreviation){ this.abbreviation = abbreviation; }
public String getAbbreviation(){ return abbreviation; }
}
4.7. 继承设计的技巧
- 将公共操作和域放在超类
- 不要使用受保护的域。首先,子类集合是无限制的,任何人都能够由某个类派生一个子类,并编写代码直接访问protected的作用域,从而破坏了封装性。其次,同一个包内的所有类都可以访问protected域,而不管它是否为这个类的子类。不过,protected对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。
- 使用继承实现“is-a”关系。
- 除非所有继承的方法都有意义,否则不要使用继承。
- 使用多态,而非类型信息。