目录
利用继承(inheritance),人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域,并可以在此基础上添加一些新的方法和域来满足新的需求。
另外,反射(reflection)是指在程序运行期间发现更多类及其属性的能力。由于主要是开发软件工具的人员,而不是编写应用程序的人员对这项功能感兴趣,所以可以按需学习。
类、超类和子类
若两个对象之间存在普遍的共同点,同时存在一些差异,这个时候就可以使用继承。
定义子类
关键字extends表示继承。
extends关键字表示正在构造的新类派生于一个已存在的类。
已存在的类称为:超类(superclass)、基类(base class)或父类(parent class)
新类称为:子类(student class)、派生类(derived class)或孩子类(child class)
往往子类比超类拥有的功能更加丰富。
在通过扩展超类定义子类的时候,仅需要指出两者的不同之处即可。因此在设计类时应该讲通用的方法放在超类中,而将具有特殊用途的方法放在子类中。
覆盖方法
当超类中的有些方法对子类不适用时,需要通过在子类中重新定义方法代码覆盖超类中的原方法。
在子类中,方法不能直接访问超类的私有域,只有超类的方法才能访问其私有域。若一定需要访问超类的私有域,就必须借助于超类的公有接口,通过super关键字调用。
class Employee
{
private double salary;
...
public double getSalary()
{
return salary;
}
...
}
public class Manager extends Employee
{
...
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
...
}
以上代码实现了对超类方法的覆盖,并访问了超类的私有域。
在覆盖方法是,子类方法不能低于超类方法的可见性。即超类为public,子类就必须为public。
子类构造器
由于子类不能直接访问超类的私有域,所以必须通过利用超类的构造器,来实现超类私有域的初始化。
public Manager(String name, double salary, int year, int month, int day)
{
super(name, salary, year, month, day);
bonus = 0;
}
若子类中没有显式得调用超类的构造器,系统将自动调用超类的默认无参数构造器;若既没有显式得调用,超类中也没有无参数构造器,则编译器将会报错。
继承层次
继承并不局限于一个层次。由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。
在一个继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)
Java不支持多继承。即一个类最多只能继承于一个类
多态
在Java程序设计中,对象变量是多态的。一个Employee对象既可以引用Employee类对象,也可以引用Employee类的任意子类对象。
public static void main(String[] args)
{
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee(...);
staff[2] = new Rmployee(...);
for(Employee e : staff)
{
System.out.println(e.getSalary());
}
}
e.getSalary();对于staff[1]和staff[2]来说将调用Employee类中的getSalary方法,对于staff[0],将调用Manger类的getSalary方法。即当e引用的是哪个类的对象,就执行哪个类中的方法。虚拟机根据其引用的对象类型,正确的调用相应的方法。这种一个对象可以只是多种实际类型的现象称为多态(polymorphism)。在运行时能够自动的选择调用那个方法的现象称为动态绑定(dynamic binding)。
虽然staff[0]引用的是boss对象,但是,编译器会将staff看作是Employee对象,这意味着bos.setBounus(...)合法,但staff[0].setBounus(...)不能通过编译,因为setBounus方法不是Employee类中的方法。
上述例子中,可以将子类对象引用赋值给超类对象变量,但是不能将超类对象引用赋值给子类对象变量。可以理解为只允许向上转型,不允许向下转型。
虽然子类数组的引用可以赋值给超类数组引用,而不需要强制转换,但因为两者引用同一数组,为了避免声明一个超类想象从而加入了子类队列这种错误,需要牢记创建数组时的元素类型,并确保只将类型兼容的引用存储到数组中。实例如下:
Manager[] managers = new Manager[10];
Employee[] staff = managers; //OK
staff[0] = new Employee(...);
//可以执行,但这将把一个employee对象引入存储进一个Manager数组对象引用
//当staff[0].setBounus(...)时,将导致调用一个不存在的实例域,进而搅乱相邻存储空间的内容
阻止继承:final 类和方法
有时候,人们希望一些类不被扩展,即不能声明其子类。
不允许扩展的类称为final类。使用final修饰符定义。
类中的方法也可以声明为final。如果这样做,这个方法就不能被覆盖。(final类的所有方法都默认是final方法,但域不会。)
在早起的Java中,程序员为了逼民啊动态绑定带来的系统开销而使用final关键字。如果一个方法没有被覆盖并且很短,编译器就能够对他进行优化处理,这个过程被称为内联(inlining)。
强制类型转换
在向下转型时,需要使用(。。。)的形式,如Manager boss = (Manager)staff[0];
在进行类型转换之前,最好检查一下是否可以转型
if(staff[1] instanceof Manager)
{
boss = (Manager)staff[1];
...
}
注意:
- 类型转换只能在继承层次内进行
- 在将超类转换成子类之前,应该使用instanceof进行检查。
一般情况下,应该尽量少用类型转换和instanceof运算符。
抽象类
对于一些通用的对象属性或行为,可以放置在位于继承层次中较高层次的通用超类中。
使用abstract关键字,可以定义无代码实体的方法,如public abstract String getDescribtion();为提高程序清晰度,包含一个或多个抽象方法的类必须背声明为抽象的。当然,没有抽象方法的类也可以定义为抽象的,只是抽象类不能被实例化。
在抽象类中可以包含抽象方法,也可以有具体数据和具体方法。
抽象方法起着占位的作用,其具体实现有子类定义
抽象类不能实例化,但可以定义对象对量,只是其引用只能是该抽象类的非抽象子类对象。
注意:编译器只允许调用在类中声明的方法。实例如下:
abstract class Father
{
public abstract void test();
//若此方法在此省略,即不声明该方法,则frame类中将报错,因为father为Father对象,而Father类中不存在test方法。
...
}
class child1
{
public void test()
{
...
}
}
class child2
{
public void test()
{
...
}
}
class frame
{
public static void main(String[] args)
{
Father[] f = new Father[2] //实例化Father数组,而不是实例化Father对象
f[0] = new child1();
f[1] = new child2();
for(Father father : f)
{
System.out.println(father.test());
}
}
}
受保护访问
在前文中,我们知道,子类不能直接访问超类的私有域/私有方法,若希望某些域/方法被子类访问,可以使用protected修饰符。
但是该修饰符需要谨慎使用,因为其安全性较差。
private——仅对本类可见
protected——对本包和子类可见
public——所有类可见
默认——对本包可见
Object:所有类的超类
Object是Java所有类的始祖,在Java中每个类都是由它扩展而来。
可以使用Object类型的变量引用任何类型的对象,如:Object obj = new Employee(...);
以下介绍几个Object类中常用的方法:
equals方法
Object类中的equals方法用于检测一个对象是否等于另一个对象。将判断两个对象的引用是否相同
Object.equals(obj1,obj2);
相等测试与继承
编写一个完美equals方法的建议:
1、显式参数命名为otherObject,
2、检测this域otherObject是否引用同一个对象:
if (this == otherObject) return false;
3、检测otherObject是否为null。
if (otherObject == null) return false;
4、检测otherObject是否属于同一个类。若equals的语义在每个子类中有所改变,就使用getVlass检测;
if (getClass() == otherObject.getClass()) return false;
如果所有子类有统一的语义,就使用instanceof检测:
if (!(otherObject instanceof ClassName)) return false;
5、将otherObject转换为相应的类类型变量
ClassName other = (ClassName) otherObject;
6、现在开始对所需要比较的域进行比较了。使用==比较基本类型域,使用equals比较对象域。
return field1 == other.field2 && Object.equals(field2,other.field2) && ...;
注意:可以使用@override对覆盖超类的方法进行标记,如果出现错误或正在定义新的方法,就会收到错误报告。
hashCode方法
散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。且两个对象的散列码一般是不同的。
String s = “OK”; String b = "OK";这两者的散列码是相同的,因为字符串对象的散列码是由内容导出的。
toString方法
建议为自己建立的每一个增加toString方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。
数组的toString:Arrays.toString(type[] a);
多维数组toString:Arrays.deepToString();
范型数组列表
ArrayList<Integer> test = new ArrayList<Integer>(2);
//test.ensureCapacity(2);
//可以显式指定大小,也可以通过ensureCapacity指定
test.add(1);
test.add(3);
test.add(11);
System.out.print(test.size()); // print 3
一旦确认数组列表的大小不会再发生改变,可以调用trimToSize()方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。
访问数组列表元素
void set(int index, E obj)
E get(int index)
void add(int index, E obj)
E remoce(int index)
对象包装器与自动装箱
假设想定义一个整型数组列表,而尖括号中不能的类型参数不允许时基本类型。这时候就需要用到对象包装器
有时,需要讲int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。通常,这些类称为包装器(wrapper)。
对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Byte、Short、Character、Void和Boolean。
对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器还是final的,因此不能定义他们的恶子类。
因为每个值包装在对象中,所以整型数组列表比int型数组的效率低很多。因此,整型数组列表适合用来构造小型集合,因为此时程序员的操作方便更为重要,而大型集合,就使用int数组。
list.add(3)将自动的变换成:list.add(Integer.valueOf(3));。这种变换称为自动装箱(autoboxing)
int a = list.get(i);将被转换为:int a = list.get(i).intValue();。这种变换是自动拆箱
装箱和拆箱是编译器认可的,而不是虚拟机。
参数数量可变的方法
参数数量可变的方法(“变参方法”)
public static double max(double... values)
{
double largest = Double.NEGATIVE_INFINIRY;
for(double v : values)
if(v > largest)
largest = v;
return largest;
}
允许将数组作为最后一个传递给可变参数数量方法的参数。
枚举类
枚举类型:public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};
这个声明定义的是一个类,刚好有四个实例。
因此,在比较两个枚举类型时,永远不需要调用equals方法,直接使用“==”就可以了。
常用方法:
Size.SMALL.toString();
Size s = Enum.valueOf(Size.class, "SMALL");
Size[] values = Size.values();
Size.MEDIUM.ordinal(); //return 1;
packages mums;
import java util.*;
/**
*This program demonstrates enumerated types.
*@version 1.0 2004-05-24
*@author Cay Horstmann
*/
public class EnumTest
{
public static void main(String[] args)
{
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("abbrebiation=" + size.getAbbrevition());
if(size = Size.EXTRA_LARGE)
System.out.println("Good job---you paid attention to the _.");
}
}
enum Size
{
SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
private Size(String abbreviation) { this.abbreviation = abbreviation; }
public String getAbbreviation() { return abbreviation; }
private String abbreviation;
}
反射
Class类
捕获异常
利用反射分析类能力
在运行时使用反射分析对象
使用反射编写范型数组代码
调用任意方法
继承的设计技巧
1、将公共操作和域放在超类
2、不要使用受保护的域(protected修饰符)
不过,protected方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。
3、使用继承实现“is-a”关系
4、除非所有继承的方法都有意义,否则不要使用继承
5、在覆盖方法时,不傲改变预期的行为
6、使用多态,而非类型信息
7、不要过多地使用反射。