Java核心技术 第五章 继承

类、超类、子类:

子类方法不能直接访问超类的私有域。

thissuper并非引用,不能将其赋给另一个对象变量。

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关系,所以经理可以继承于雇员。

动态绑定:

对象调用对象方法的过程:

  1. 编译器查看对象的声明类型和方法名。
  2. 接下来,编译器将查看调用方法时提供的参数类型。
  3. 如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确的知道应该调用哪个方法,我们将这种调用方式成为静态绑定。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
  4. 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。x类型为DD中无对应方法,则在超类中查找对应方法。

在覆盖一个方法时,子类方法不能低于超类方法的可见性。(否则编译不能通过)

阻止继承: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个访问修饰符:

  1. 仅对本类可见--private
  2. 对所有类可见--public
  3. 对本包和所有子类可见--protected
  4. 对本包可见--默认,不需要修饰符

protected:与基类不在同一个包中的子类,只能访问自身从基类继承而来的受保护成员,而不能访问基类实例本身的受保护成员 。

Object:所有类的超类:

Object类是所有类的始祖,在Java中每个类都是由它扩展而来的。

equals方法:

Object类中的equals方法判断两个对象是否有相同的引用。对大多数类来说,并没有意义,所以为了比较两个类的状态是否相同,需要重写equals方法。

Java语言规范要求equals具有下面特性:

  1. 自反性:非空引用x,x.equals(x)应返回true
  2. 对称性
  3. 传递性
  4. 一致性:反复调用,结果不变
  5. 对于任意非空引用x,x.equals(null)应该返回false。

注意:

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

编写equals方法建议:

  1. 显示参数命名为otherObject,稍候需要将它转换为另一个叫other的变量。
  2. 检测this与otherObject是否引用同一个对象:if(this == otherObject) return true ;
  3. 检测otherObject是否为null,如果为null,返回false
  4. 比较this与otherobject是否属于同一个类。如果equals的语义在每个子类中有所改变,就用getClass检测,if(fetClass() != otherObject.getClass()) return false ;否则使用instanceof检测,if(!(otherObject instanceof ClassName)) return false ;
  5. 将otherObject转换为相应的类型变量:ClassName other = (ClassName)otherObject ;
  6. 现在开始对所有需要比较的域进行比较了,使用==比较基本类型,使用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)

 

反射: 

反射机制可以用来:

  1. 在运行时分析类的能力。
  2. 在运行中查看对象,例如,编写一个toString方法供所有类使用。
  3. 实现通用的数组操作代码
  4. 利用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对象,而最好使用接口和内部类。

 

 继承设计的技巧:

  1. 将公共操作和域放在超类
  2. 不要使用受保护的域
  3. 使用继承实现“is-a”关系
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 在覆盖方法时,不要改变预期行为
  6. 使用多态,而非类型信息
  7. 不要过多的使用反射

 

 

 

 

原文地址:https://www.cnblogs.com/chwy/p/5654016.html