摘自 《Java 核心技术(卷 Ⅰ)》

《Java 核心技术(卷 Ⅰ)》
《Java 核心技术(卷 Ⅰ)》

继承、泛型与反射

1. 类、超类和子类

1.1 定义子类

关键字 extends 表示继承

public class Manager extends Employee {
    ....
}

在 Java 中,所有的继承都是公有继承,而没有 C++ 中的私有继承和保护继承。

关键字 extends 表明正在构造的新类派生于一个已存在的类。已存在的类称为超类(superclass)基类(base class)父类(parent class),新类称为子类(subclass)派生类(derived class)孩子类(child class)

子类比超类封装了更多的数据,拥有更多的功能

1.2 覆盖方法

可以在子类中提供一个新的方法来覆盖(override)超类中的同名方法

public double getSalary() {
    double baseSalary = super.getSalary();
    return baseSalary + bonus;
}

super 不是一个对象的引用,不能将 super 赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字

1.3 子类构造器

public Manager(String name, double salary, int year, int month, int day) {
    super(name, salary, year, month, day);
    bonus = 0;
}

如果子类的构造器没有显式地调用超类的构造器, 则将自动地调用超类默认(没有参数) 的构造器

如果超类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用超类的其他构造器,则 Java 编译器将报错

1.4 继承层次

继承并不仅限于一个层次,由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。在继承层次中,从某个特定类到其祖先的路径被称为该类的继承链(inheritance chain)

一个祖先类可以拥有多个子孙继承链。另外,Java 不支持多继承

1.5 多态

在 Java 中,is-a 规则表明子类的每个对象也是超类的对象。它的另一种表述法是置换法则,即程序中出现超类对象的任何地方都可以用子类对象置换

1.6 理解方法调用

1.7 阻止继承:final 类和方法

有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为 final 类

public final class Executive extends Manager {
    ...
}

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

final 类中的所有方法自动成为 final 方法

public class Employee {
    public final String getName() {
        return name;
    }
}

将方法或类声明为 final 的主要目的是:确保它们不会在子类中改变语义

1.8 强制类型转换

对象引用的转换语法与数值表达式的类型转换类似:

Manager boss = (Manager) staff[0];

需要注意的是:

  • 只能在继承层次内进1行类型转换
  • 将超类转换成子类之前,应该使用instanceof进行检查

1.9 抽象类

使用 abstract 关键字修饰类中的抽象方法

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

为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的

public abstract class Person {
    ...
    public abstract String getDescription();
}

除了抽象方法之外,抽象类还可以包含具体数据和具体方法

  • 抽象方法充当着占位的角色,它们的具体实现在子类中
  • 类即使不包含抽象方法,也可以将类声明为抽象类
  • 抽象类不能被实例化

1.10 控制可见性的访问修饰符

在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为 protected

下面归纳一下 Java 用于控制可见性4 个访问修饰符

  • 仅对本类可见private
  • 所有类可见public
  • 本包所有子类可见protected
  • 本包可见默认,不需要修饰符

2. Object:所有类的超类

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

在 Java 中,只有基本类型(primitive types)不是对象,例如数值、字符和布尔类型的值。而所有的数组类型扩展了 Object 类

可以使用 Object 类型的变量引用任何类型的对象:

Object obj = new Employee("Abel Su", 35000);

当然,Object 类型的变量只能用于作为各种值的通用持有者。要想对其中的内容进行具体的操作,还需要清楚对象的原始数据类型,并进行相应的类型转换

Employee e = (Employee) obj;

2.1 equals 方法

Object 类中的equals方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用

在子类中定义equals方法时,首先调用超类的equals。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。

2.2 相等测试与继承

// java.util.Arrays
// 如果两个数组长度相同,并且在对应的位置上数据元素也均相同,则返回 true
static boolean equals(type[] a, type[] b)
// java.util.Objects
// 如果 a 和 b 都为 null,返回 true;如果只有其中之一为 null,则返回 false;否则返回 a.equals(b)
static boolean equals(Object a, Object b)

2.3 hashCode 方法

散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的,如果 x 和 y 是两个不同的对象,那么x.hashCode()y.hashCode()基本上不会相同。

字符串的散列码是由内容导出的

由于 hashCode 方法定义在 Object 类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址

2.4 toString 方法

在 Object 中还有一个重要的方法,就是toString方法,它用于返回表示对象值的字符串。例如 Point 类的toString方法将返回下面的字符串:

java.awt.Point[x=10,y=20]

绝大多数(但不是全部)的 toString 方法都遵循这样的格式:类的名字,随后是一对方括号括起来的赋值。最好通过getClass().getName()获得类名的字符串。

随处可见 toString 方法的主要原因是:只要对象与一个字符串通过操作符+连接起来,Java 编译器就会自动的调用 toString 方法,以便获得这个对象的字符串描述

3. 泛型数组列表

ArrayList 是一个采用类型参数(type parameter)泛型类(generic class)

ArrayList<Employee> staff = new ArrayList<Employee>();

Java SE 7 中,可以省去右边的类型参数

ArrayList<Employee> staff = new ArrayList<>();

使用 add 方法可以将元素添加到数组列表中:

staff.add(new Employee("Abel Su", ...));
staff.add(new Employee("Harry Potter", ...));

数组列表管理着对象引用的一个内部数组。最终,数组的全部空间有可能被用尽。如果调用 add 且内部数组已经满了,数组列表就将自动的创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中

如果已经清楚或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法

staff.ensureCapacity(100);

这个方法调用将分配一个包含 100 个对象的内部数组。然后调用 100 次 add, 而不用重新分配空间

另外,还可以把初始容量传递给 ArrayList 构造器

ArrayList<Employee> staff = new ArrayList<>(100);

size方法将返回数组列表中包含的实际元素数目

一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize方法,该方法会将存储区域的大小调整为当前元素数量所需要的存储空间数目,垃圾回收器(GC)将回收多余的存储空间

3.1 访问数组列表元素

使用getset方法实现访问或改变数组元素的操作,而不能使用类似数组中的[]语法格式。

void set(int index, E obj)
E get(int index)
void add(int index, E obj)
E remove(int index) // 删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回。

3.2 类型化与原始数组列表的兼容性

假设有下面这个遗留下来的类:

public class EmployeeDB {
    public void update(ArrayList list) {
        ...
    }
    public ArrayList find(String query) {
        ...
    }
}

可以将一个类型化的数组列表传递给 update 方法,而并不需要进行任何类型转换。

4. 对象包装器与自动装箱

有时,需要将 int 这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类,这些类称为包装器(wrapper)。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character、Void 和 Boolean(前 6 个类派生于公共的超类 Number)。

  • 对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值
  • 对象包装器是 final 的,因此不能定义它们的子类
ArrayList<Integer> list = new ArrayList<>();

由于每个值分别包装在对象中,所以 ArrayList 的效率远远低于 int[] 数组。因此,应该用它构造小型集合,此时程序员操作的方便性要比执行效率更加重要。

有一个很有用的特性,从而便于添加 int 类型的元素到 ArrayList,调用list.add(3)将自动变换成:

list.add(Integer.valueOf(3));

这种变换被称为自动装箱(autoboxing)

相反的,当将一个 Integer 对象赋给一个 int 值时,将会自动拆箱,编译器会将int n = list.get(i)翻译成:

int n = list.get(i).intValue();

装箱和拆箱编译器认可的,而不是虚拟机。编译器在生成类的字节码时, 插人必要的方法调用。虚拟机只是执行这些字节码。

5. 参数数量可变的方法

用户也可以自己定义可变参数的方法,并将参数指定为任意类型, 甚至是基本类型:

public static double max(double... values) {
    double largest = Double.NEGATIVE_INFINITY;
    for (double v : values) {
        if (v > largest) {
            largest = v;
        }
    }
    return largest;
}

然后就可以调用该方法:

double m = max(3.1, 40.4, -5);

编译器会将new double[]{3.1, 40.4, -5}传递给max方法。

6. 枚举类

public enum Size {
    SMALL,
    MEDIUM,
    LARGE,
    EXTRA_LARGE
};
...
String s_small = Size.SMALL.toString();
Size s = Enum.valueOf(Size.class, "SMALL");
Size[] values = Size.values();
int pos = Size.MEDIUM.ordinal(); // 返回枚举常量的位置

7. 反射

能够分析类能力的程序称为反射(reflective)。反射机制的功能十分强大,可以用来:

  • 在运行时分析类的能力
  • 在运行时查看对象
  • 实现通用的数组操作代码
  • 利用 Method 对象,这个对象类似于 C++ 中的函数指针

7.1 Class 类

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

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

Employee e;
...
Class cl = e.getClass();

最常用的 Class 方法getName(),这个方法将返回类的名字

System.out.println(e.getClass().getName() + " " + e.getName());

-------
Employee Harry Hacker

如果类在一个包中,包的名字也会作为类名的一部分,如java.util.Random

还可以调用静态方法forName(className)获得类名对应的 Class 对象

String className = "java.util.Random";
Class cl = Class.forName(className);

获得 Class 类对象的第三种方法非常简单:如果 T 是任意的 Java 类型(或 void 关键字)T.class将代表匹配的类的对象

Class cl1 = Random.class; // if you import java.util.*;
Class cl2 = int.class;
Class cl3 = Double[].class;

虚拟机为每个类型管理一个 Class 对象。因此,可以利用==运算符实现两个类对象比较的操作

if (e.getClass() == Employee.class) ...

另一个很有用的方法newInstance()可以用来动态的创建一个类的实例

String s = "java.util.Random";
Object m = Class.forName(s).newInstance();

7.2 捕获异常

try {
    String name = ...; // get class name
    Class cl = Class.forName(name); // might throw exception
    do something with cl
} catch (Exception e) {
    e.printStackTrace();
}

7.3 利用反射分析类的能力

反射机制最重要的内容——检查类的结构。

package reflection;

import java.util.*;
import java.lang.reflect.*;

/**
 * This program uses reflection to print all features of a class.
 * @version 1.1 2004-02-21
 * @author Cay Horstmann
 */
public class ReflectionTest
{
   public static void main(String[] args)
   {
      // read class name from command line args or user input
      String name;
      if (args.length > 0) name = args[0];
      else
      {
         Scanner in = new Scanner(System.in);
         System.out.println("Enter class name (e.g. java.util.Date): ");
         name = in.next();
      }

      try
      {
         // print class name and superclass name (if != Object)
         Class cl = Class.forName(name);
         Class supercl = cl.getSuperclass();
         String modifiers = Modifier.toString(cl.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");
         System.out.print("class " + name);
         if (supercl != null && supercl != Object.class) System.out.print(" extends "
               + supercl.getName());

         System.out.print("\n{\n");
         printConstructors(cl);
         System.out.println();
         printMethods(cl);
         System.out.println();
         printFields(cl);
         System.out.println("}");
      }
      catch (ClassNotFoundException e)
      {
         e.printStackTrace();
      }
      System.exit(0);
   }

   /**
    * Prints all constructors of a class
    * @param cl a class
    */
   public static void printConstructors(Class cl)
   {
      Constructor[] constructors = cl.getDeclaredConstructors();

      for (Constructor c : constructors)
      {
         String name = c.getName();
         System.out.print("   ");
         String modifiers = Modifier.toString(c.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");         
         System.out.print(name + "(");

         // print parameter types
         Class[] paramTypes = c.getParameterTypes();
         for (int j = 0; j < paramTypes.length; j++)
         {
            if (j > 0) System.out.print(", ");
            System.out.print(paramTypes[j].getName());
         }
         System.out.println(");");
      }
   }

   /**
    * Prints all methods of a class
    * @param cl a class
    */
   public static void printMethods(Class cl)
   {
      Method[] methods = cl.getDeclaredMethods();

      for (Method m : methods)
      {
         Class retType = m.getReturnType();
         String name = m.getName();

         System.out.print("   ");
         // print modifiers, return type and method name
         String modifiers = Modifier.toString(m.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");         
         System.out.print(retType.getName() + " " + name + "(");

         // print parameter types
         Class[] paramTypes = m.getParameterTypes();
         for (int j = 0; j < paramTypes.length; j++)
         {
            if (j > 0) System.out.print(", ");
            System.out.print(paramTypes[j].getName());
         }
         System.out.println(");");
      }
   }

   /**
    * Prints all fields of a class
    * @param cl a class
    */
   public static void printFields(Class cl)
   {
      Field[] fields = cl.getDeclaredFields();

      for (Field f : fields)
      {
         Class type = f.getType();
         String name = f.getName();
         System.out.print("   ");
         String modifiers = Modifier.toString(f.getModifiers());
         if (modifiers.length() > 0) System.out.print(modifiers + " ");         
         System.out.println(type.getName() + " " + name + ";");
      }
   }
}

7.4 在运行时使用反射分析对象

ObjectAnalyzer.java

package objectAnalyzer;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;

public class ObjectAnalyzer
{
   private ArrayList<Object> visited = new ArrayList<>();

   /**
    * Converts an object to a string representation that lists all fields.
    * @param obj an object
    * @return a string with the object's class name and all field names and
    * values
    */
   public String toString(Object obj)
   {
      if (obj == null) return "null";
      if (visited.contains(obj)) return "...";
      visited.add(obj);
      Class cl = obj.getClass();
      if (cl == String.class) return (String) obj;
      if (cl.isArray())
      {
         String r = cl.getComponentType() + "[]{";
         for (int i = 0; i < Array.getLength(obj); i++)
         {
            if (i > 0) r += ",";
            Object val = Array.get(obj, i);
            if (cl.getComponentType().isPrimitive()) r += val;
            else r += toString(val);
         }
         return r + "}";
      }

      String r = cl.getName();
      // inspect the fields of this class and all superclasses
      do
      {
         r += "[";
         Field[] fields = cl.getDeclaredFields();
         AccessibleObject.setAccessible(fields, true);
         // get the names and values of all fields
         for (Field f : fields)
         {
            if (!Modifier.isStatic(f.getModifiers()))
            {
               if (!r.endsWith("[")) r += ",";
               r += f.getName() + "=";
               try
               {
                  Class t = f.getType();
                  Object val = f.get(obj);
                  if (t.isPrimitive()) r += val;
                  else r += toString(val);
               }
               catch (Exception e)
               {
                  e.printStackTrace();
               }
            }
         }
         r += "]";
         cl = cl.getSuperclass();
      }
      while (cl != null);

      return r;
   }
}

ObjectAnalyzerTest.java

package objectAnalyzer;

import java.util.ArrayList;

/**
 * This program uses reflection to spy on objects.
 * @version 1.12 2012-01-26
 * @author Cay Horstmann
 */
public class ObjectAnalyzerTest
{
   public static void main(String[] args)
   {
      ArrayList<Integer> squares = new ArrayList<>();
      for (int i = 1; i <= 5; i++)
         squares.add(i * i);
      System.out.println(new ObjectAnalyzer().toString(squares));
   }
}

7.5 使用反射编写泛型数组代码

package arrays;

import java.lang.reflect.*;
import java.util.*;

/**
 * This program demonstrates the use of reflection for manipulating arrays.
 * @version 1.2 2012-05-04
 * @author Cay Horstmann
 */
public class CopyOfTest
{
   public static void main(String[] args)
   {
      int[] a = { 1, 2, 3 };
      a = (int[]) goodCopyOf(a, 10);
      System.out.println(Arrays.toString(a));

      String[] b = { "Tom", "Dick", "Harry" };
      b = (String[]) goodCopyOf(b, 10);
      System.out.println(Arrays.toString(b));

      System.out.println("The following call will generate an exception.");
      b = (String[]) badCopyOf(b, 10);
   }

   /**
    * This method attempts to grow an array by allocating a new array and copying all elements.
    * @param a the array to grow
    * @param newLength the new length
    * @return a larger array that contains all elements of a. However, the returned array has 
    * type Object[], not the same type as a
    */
   public static Object[] badCopyOf(Object[] a, int newLength) // not useful
   {
      Object[] newArray = new Object[newLength];
      System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
      return newArray;
   }

   /**
    * This method grows an array by allocating a new array of the same type and
    * copying all elements.
    * @param a the array to grow. This can be an object array or a primitive
    * type array
    * @return a larger array that contains all elements of a.
    */
   public static Object goodCopyOf(Object a, int newLength) 
   {
      Class cl = a.getClass();
      if (!cl.isArray()) return null;
      Class componentType = cl.getComponentType();
      int length = Array.getLength(a);
      Object newArray = Array.newInstance(componentType, newLength);
      System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
      return newArray;
   }
}

7.6 调用任意方法

java.lang.reflect.Method

public Object invoke(Object implicitParameter, Object[] explicitParameters)
// 调用这个对象所描述的方法,传递给定参数,并返回方法的返回值
// 对于静态方法,把 null 作为隐式参数传递

MethodTableTest.java

package methods;

import java.lang.reflect.*;

/**
 * This program shows how to invoke methods through reflection.
 * @version 1.2 2012-05-04
 * @author Cay Horstmann
 */
public class MethodTableTest
{
   public static void main(String[] args) throws Exception
   {
      // get method pointers to the square and sqrt methods
      Method square = MethodTableTest.class.getMethod("square", double.class);
      Method sqrt = Math.class.getMethod("sqrt", double.class);

      // print tables of x- and y-values

      printTable(1, 10, 10, square);
      printTable(1, 10, 10, sqrt);
   }

   /**
    * Returns the square of a number
    * @param x a number
    * @return x squared
    */
   public static double square(double x)
   {
      return x * x;
   }

   /**
    * Prints a table with x- and y-values for a method
    * @param from the lower bound for the x-values
    * @param to the upper bound for the x-values
    * @param n the number of rows in the table
    * @param f a method with a double parameter and double return value
    */
   public static void printTable(double from, double to, int n, Method f)
   {
      // print out the method as table header
      System.out.println(f);

      double dx = (to - from) / (n - 1);

      for (double x = from; x <= to; x += dx)
      {
         try
         {
            double y = (Double) f.invoke(null, x);
            System.out.printf("%10.4f | %10.4f%n", x, y);
         }
         catch (Exception e)
         {
            e.printStackTrace();
         }
      }
   }
}

8. 继承的技巧

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