类、超类、子类:
子类方法不能直接访问超类的私有域。
this和super并非引用,不能将其赋给另一个对象变量。
super在构造器中的应用:
public Manager(String n, double s, int year, int month, int day ) {
super(n, s, year, month, day) ;
bonus = 0 ;
}
使用super调用构造器的语句必须是子类构造器的第一条语句。
如果子类构造器没有显示的调用超类的构造器,则将自动的调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没有显示的调用超类的其他构造器,则Java编译器会报错。
一个对象变量可以指示多种实际类型的现象被称为多态。在运行时能够自动的选择调用哪个方法的现象称为动态绑定。
多态:
每个经理都是雇员,是is-a关系,所以经理可以继承于雇员。
动态绑定:
对象调用对象方法的过程:
- 编译器查看对象的声明类型和方法名。
- 接下来,编译器将查看调用方法时提供的参数类型。
- 如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确的知道应该调用哪个方法,我们将这种调用方式成为静态绑定。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
- 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。x类型为D,D中无对应方法,则在超类中查找对应方法。
在覆盖一个方法时,子类方法不能低于超类方法的可见性。(否则编译不能通过)
阻止继承:final类和方法
如果在定义类的时候使用了final修饰符就表明这个类是final类,不允许被继承。
类中的方法被声明为final,则该方法在子类中不能被覆盖。
将类或方法声明为final的主要目的是:确保他们不会在子类中改变语义。
强制类型转换:
将一个子类的引用赋给一个超类变量,编译器是允许的。但是将一个超类的引用赋给一个子类变量,必须进行类型转换。
Manger boss = (manager) staff[1] ;
会在运行时发生错误,抛出ClassCastException异常。
在进行类型转换前,应先查看是否能够成功转换。
if(staff[1] instanceofr Manager) {
boss = (manager) staff[1] ;
}
如果类型转换不可能成功,编译器就不会进行这个转换。例:
Date c = (Date) staff[1] ; //将会产生编译错误, 因为Date不是Employee的子类
综上所述:
- 只能在继承层次内进行类型转换
- 在将超类转换成子类前,应该使用instanceof进行检查。
抽象类:
abstract class Person {
Private String name ;
public Person(String n) {
name = n ;
}
public abstract String getDescription() ;
public String getName() {
return name ;
}
}
//Employee 和 Student 继承Person
Person[] people = new Person[2] ;
people[0] = new Employee(...) ;
people[1] = new Student(...) ;
for(Person p : people) {
System.out.println(p.getName() + "," + p.getDescription) ;
}
p.getDescription()合法,变量p永远不会引用Person对象,而是引用Employee或Student子类中的getDescription()方法。
受保护访问:
4个访问修饰符:
- 仅对本类可见--private
- 对所有类可见--public
- 对本包和所有子类可见--protected
- 对本包可见--默认,不需要修饰符
protected:与基类不在同一个包中的子类,只能访问自身从基类继承而来的受保护成员,而不能访问基类实例本身的受保护成员 。
Object:所有类的超类:
Object类是所有类的始祖,在Java中每个类都是由它扩展而来的。
equals方法:
Object类中的equals方法判断两个对象是否有相同的引用。对大多数类来说,并没有意义,所以为了比较两个类的状态是否相同,需要重写equals方法。
Java语言规范要求equals具有下面特性:
- 自反性:非空引用x,x.equals(x)应返回true
- 对称性
- 传递性
- 一致性:反复调用,结果不变
- 对于任意非空引用x,x.equals(null)应该返回false。
注意:
- 如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。
- 如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。
编写equals方法建议:
- 显示参数命名为otherObject,稍候需要将它转换为另一个叫other的变量。
- 检测this与otherObject是否引用同一个对象:if(this == otherObject) return true ;
- 检测otherObject是否为null,如果为null,返回false
- 比较this与otherobject是否属于同一个类。如果equals的语义在每个子类中有所改变,就用getClass检测,if(fetClass() != otherObject.getClass()) return false ;否则使用instanceof检测,if(!(otherObject instanceof ClassName)) return false ;
- 将otherObject转换为相应的类型变量:ClassName other = (ClassName)otherObject ;
- 现在开始对所有需要比较的域进行比较了,使用==比较基本类型,使用equals比较对象域。
hashCode方法:
散列码是由对象导出的一个整形值。
字符串的散列码是由内容导出的,因此内容相同的字符串,散列码相同。String类重写了hashCode方法。
equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。
如果存在数组类型的域,那么可以使用静态的Arrays.hashCode方法计算一个散列码,这个散列码由数组元素的散列码组成。
toString方法:
设计子类的程序员也应该定义自己的toString方法,并将子类域的描述添加进去。如果超类使用了getClass().getName(),那么子类只要调用super.toString()就可以了。
只要对象与一个字符串通过操作符“+”连接起来,Java编译器就会自动的调用toString方法,以便获得这个对象的字符串描述。
int[] luckynumbers = {1,3,5,7,9} ;
String s = Arrays.toString(luckyNumbers) ;
结果:[1,3,5,7,9]
java.lang.Class
String getName() //返回这个类的名字
Class getSuperclass() //以Class对象的形式返回这个类的超类信息
泛型数组列表:
ArrayList<Employee> staff = new ArrayList<Employee>() ;
Java7中可以省略后面的<Employee>,ArrayList<Employee> staff = new ArrayList<>() ;
staff.add(new Employee("Harry Hacker", ...)) ;
staff.ensureCapacity(100) ;
new ArrayList(100) //capacity is 100
staff.size() //返回数组列表中实际包含的元素数目,等价于数组a的a.length
void trimToSize() //将数组列表的存储容量削减到当前尺寸
访问数组列表元素:
staff.set(i, harry) ; //只有当i位置存在元素时才能使用set方法
Employee e = staff.get(i) ;
Employeee = staff.remove(n) ; //位于这个位置之后的所有元素都向前移动一个位置,并且数组的大小减一。i必须在0~size() -1 之间
对象包装器与自动装箱:
基本类型对应的包装器类:Integer,Long,Double,Float,Short,Byte,Character,Void, Boolean。(前六个派生于公共的超类Number)
对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时对象包装器类还是final,因此不能定义它们的子类。
<>中的类型参数不允许是基本类型,因此不允许写成:ArrayList<int>,可以写成Arraylist <Integer> list = new ArrayList<>() ;
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++ ;
java.lang.Integer 1.0
static int parseInt(String s)
static int parseInt(String s, int radix)
java.text.NumberFormat 1.1
Number parse(String s)
参数数量可变的方法:
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)
反射:
反射机制可以用来:
- 在运行时分析类的能力。
- 在运行中查看对象,例如,编写一个toString方法供所有类使用。
- 实现通用的数组操作代码
- 利用Method对象,这个对象很像C++中的函数指针
Class类:
Employee e ;
Class cl = e.getClass() ;
e.getClass().getName() ;
还可以调用静态方法forName获得类名对应的Class对象。
String className = "java.util.Date" ;
Class cl = Class.forName(className) ;
className必须是类名或接口名,否则在运行时会抛出已检查异常。使用该方法时,应提供一个异常处理器。
获得Class对象的第三种方法:
如果T是任意的Java类型,T.calss将代表匹配的类对象。
Class cl1= Date.class ;
Class cl2 = int.class ;
Class cl3 = Double[].class ;
一个Class对象实际上表示一个类型,而这个类型未必一定是一种类。int不是类,但int.class是一个Class类型的对象。
虚拟机为每个类型管理一个Class对象,因此,可以利用==运算符实现两个类对象比较的操作。例:
if (e.getClass() == Employee.class) ...
e.getClass.newInstance() ; //创建一个与e相同类型的实例。newInstance方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认构造器,就会抛出一个异常。
String s = "java.util.Date" ;
Object m = Class.forName(s).newInstance() ;
如果需要以这种方式向希望按名称创建的类的构造器提供参数,就不要使用上面那条语句,而必须使用Constructor类中的newInstance方法。
利用反射分析类的能力:
在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。这三个类都有一个叫getName的方法,用于返回项目的名称。Field类有一个getType方法,用来返回描述域所属类型的Class对象。Method和Constructor类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。这三个类还有一个叫getModifiers的方法,它将返回一个整数数值,用不同的位开关描述public和static这样的修饰符使用情况。另外,可以使用Modifier类中的isPublic、isPrivate或isFinal判断方法或构造器是否是public、private或final。另外可以利用Modifier.toSting方法将修饰符打印出来。
Class类中的getFileds、getMethods和getConstructors方法将分别返回类提供的public域、方法和构造器,其中包括超类的公有成员。Class类的getDeclareFields、getDeclareMethods和getDeclareConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。
Constructors[] constructors = cl.getDeclaredConstructors() ;
Method[] methods = cl.getDeclaredMethods() ;
Field[] fields = cl.getDeclaredFields() ;
在运行时使用反射分析对象:
在编写程序使,如果知道想要查看的域名和类型,查看指定的域是一件很容易的事情。而利用反射机制可以查看在编译时还不清楚的对象域。
查看对象域的关键方法是Field类中的get方法。如果f是一个Field类型的对象,obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的当前值。
Employee harry = new Employee("Hary Hacker", 35000, 10, 1, 1989) ;
Class cl = harry.getClass() ;
Field f = cl.getDeclaredField("name") ;
Object v = f.get(harry) ;
由于name是私有域,所以get方法会抛出一个IllegalAccessException。
反射机制的默认行为受限于Java的访问控制。然而,如果一个Java程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用Field、Method和Constructor对象的setAccess方法。例:
f.setAccessible(true) ; //now OK to call f.get(harry)
setAccessible方法是AccessibleObject类中的一个方法,它是Field、Method和Constructor类的公共超类。这个特性是为调试、持久存储和相似机制提供的。
get方法还有一个需要解决的问题。name域是一个String,可以作为Object返回。若要返回double类型的域,需要使用Field类中的getDouble方法。阈值将自动打包为Double。
调用f.set(obj, value)可以将obj对象的f域设置成新值。
使用反射编写泛型数组代码:
Employee[] a = new Employee[100] ;
a = Arrays.copyOf(a, 2*a.length) ;
copyOf方法的实现:
第一种方法:
public static Object[] badCopyOf(Object[] a, int newLength]) {
Object[] newArray = new Object[newLength] ;
System.arrayCopy(a, 0, newArray, 0, Math.min(a.length, newLength)) ;
return newArray ;
}
由于方法内新建的是Object[]数组,最终将不能转变为Employee[]数组,所以该方法不可行。
改进版:
public static Object goodCopyOf(Object a, int newLength) {
Class cl = a.getClass() ;
if (!cl.isArray()) return null ;
Class componentType = cl.getComponentType() ;
int length = Araay.getLength(a) ;
Object newArray = Array.newInstance(componentType, newLength) ;
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength)) ;
}
int[] a = {1,2,3,4,5} ;
a = (int[]) goodCopyOf(a, 10) ;
为了能够实现上述操作,应该将goodCopyOf的参数声明为Object类型,而不要声明为对象数组(Object[])。整形数组类型int[]可以被转换成Object,但不能转换成对象数组。
调用任意方法:
Method类中有一个invoke方法,它允许调用包装在当前Method对象中的方法。invoke方法的签名是:
Object invoke(Object obj, Object... args)
第一个参数是隐式参数,其余的对象提供了显式参数。
对于静态方法,第一个参数可以被忽略,即可以将它设置为null。
假设m1代表Employee类的getName方法,下面这条语句显示了如何调用这个方法:
String n = (String) m1.invoke(harry) ;
如果返回的是基本类型,invoke方法会返回其包装器类型。
假设m2表示Employee类的getSalary方法,那么返回的对象实际上是一个Double,必须相应地完成类型转换。可以使用自动拆箱将它转换为一个double:
double s = (Double)m2.invoke(harry) ;
可以通过调用getDeclaredMethod方法,然后对返回的Method对象数组进行查找,知道发现想要的方法为止。也可以通过调用Class类中的getMethod方法得到想要的方法。由于存在相同名字的方法,因此,必须提供想要的方法的参数类型。getMethod的签名是:
Method getMethod(String name, Class... parameterTypes)
例:
Method m1 = Employee.class.getMethod("getName") ;
Method m2 = Employee.class.getMethod("raiseSalary", double.class) ;
如果在调用方法的时候提供了一个错误的参数,那么invoke方法将会抛出一个异常,另外invoke的参数和返回值必须是Object类型的。有鉴于此,建议在必要的时候才使用Method对象,而最好使用接口和内部类。
继承设计的技巧:
- 将公共操作和域放在超类
- 不要使用受保护的域
- 使用继承实现“is-a”关系
- 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期行为
- 使用多态,而非类型信息
- 不要过多的使用反射