《Java核心技术》第五章 继承 学习笔记

第5章 继承

本章将学习面向对象程序设计的另外一个基本概念:继承(inheritance)。

利用继承,人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。这是Java程序设计中的一项核心技术。

另外,本章还阐述了反射(reflection)的概念。反射是指在程序运行期间发现更多的类及其属性的能力。这是一个功能强大的特性,使用起来也比较复杂。

5.1 类、超类和子类

5.1.1 定义子类

下面是由继承Employee类来定义Manager类的格式,关键字extends表示继承。

public class Manager extends Employee {
    
    
    // 添加方法和域
}

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

尽管Employee类是一个超类,但并不是因为它优于子类或者拥有比子类更多的功能。实际上恰恰相反,子类比超类拥有的功能更加丰富。

public class Manager extends Employee {
    
    
    private double bonus;
    
    public void setBonus(double bonus)
    {
    
    
        this.bonus = bonus;
    }
}

当然,由于setBonus方法不是在Employee类中定义的,所以属于Employee类的对象不能使用它。

然而,尽管在Manager类中没有显式地定义getName和getHireDay等方法,但属于Manager类的对象却可以使用它们,这是因为Manager类自动地继承了超类Employee中的这些方法。

同样,从超类中还继承了name、salary和hireDay这3个域。这样一来,每个Manager类对象就包含了4个域:name、salary、hireDay和bonus。

在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中,这种将通用的功能放到超类的做法,在面向对象程序设计中十分普遍。

5.1.2 覆盖方法

然而,超类中的有些方法对子类Manager并不一定适用。具体来说,Manager类中的getSalary方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override)超类中的这个方法:

super是一个指示编译器调用超类方法的特殊关键字。

@Override
public double getSalary() {
    
    
    return super.getSalary() + bonus;
}

5.1.3 子类构造器

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

这里的super是“调用超类Employee中含有n、s、year、month和day参数的构造器”的简写形式。

5.1.4 继承层次

继承并不仅限于一个层次。例如,可以由Manager类派生Executive类。由一个公共超类派生出来的所有类的集合被称为继承层次。
在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链。
在这里插入图片描述

5.1.5 多态

有一个用来判断是否应该设计为继承关系的简单规则,这就是“is-a”规则,它表明子类的每个对象也是超类的对象。例如,每个经理都是雇员,因此,将Manager类设计为Employee类的子类是显而易见的,反之不然,并不是每一名雇员都是经理。

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

例如,可以将一个子类的对象赋给超类变量。

Employee employee;
employee = new Manager();
employee = new Employee();

在Java程序设计语言中,对象变量是多态的。一个Employee变量既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类的对象(例如,Manager、Executive、Secretary等)。

5.1.6 理解方法调用

弄清楚如何在对象上应用方法调用非常重要。下面假设要调用x.f(args),隐式参数x声明为类C的一个对象。下面是调用过程的详细描述:

1)编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String)。编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法 至此,编译器已获得所有可能被调用的候选方法。

2)接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析(overloading resolution)。例如,对于调用x.f(“Hello”)来说,编译器将会挑选f(String),而不是f(int)。由于允许类型转换(int可以转换成double, Manager可以转换成Employee,等等),所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。至此,编译器已获得需要调用的方法名字和参数类型。

3)如果是private方法、static方法、final方法(有关final修饰符的含义将在下一节讲述)或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。

动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类Executive,并且变量e有可能引用这个类的对象,我们不需要对包含调用e.getSalary()的代码进行重新编译。如果e恰好引用一个Executive类的对象,就会自动地调用Executive.getSalary()方法。

4)当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型是D,它是C类的子类。如果D类定义了方法f(String),就直接调用它;否则,将在D类的超类中寻找f(String),以此类推。

每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。

5.1.7 阻止继承:final类和方法

有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为final类。如果在定义类的时候使用了final修饰符就表明这个类是final类。

类中的特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法)。

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

public final class Main extends Manager
{
    
    
...   
}

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

5.1.8 强制类型转换

有时候也可能需要将某个类的对象引用转换成另外一个类的对象引用。对象引用的转换语法与数值表达式的类型转换类似,仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。

Employee employee = new Employee();
Manager manager = (Manager)employee;

5.1.9 抽象类

如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看,祖先类更加通用,人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。例如,考虑一下对Employee类层次的扩展。一名雇员是一个人,一名学生也是一个人。下面将类Person和类Student添加到类的层次结构中。图5-2是这三个类之间的关系层次图。
在这里插入图片描述
为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。
除了抽象方法之外,抽象类还可以包含具体数据和具体方法。

**抽象方法充当着占位的角色,它们的具体实现在子类中。**扩展抽象类可以有两种选择。一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。

抽象类不能被实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。

可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。
Person.java

public abstract class Person {
    
    
    private String name;
    public abstract String getDescription();
    public Person(String name)
    {
    
    
        this.name = name;
    }
    public String getName()
    {
    
    
        return this.name;
    }
}

Student.java

public abstract class Person {
    
    
    private String name;
    public abstract String getDescription();
    public Person(String name)
    {
    
    
        this.name = name;
    }
    public String getName()
    {
    
    
        return this.name;
    }
}

PersonTest.java

public class Main {
    
    
    public static void main(String[] args) {
    
    
        Person[] people = new Person[2];
        people[0] = new Employee("f", 1, 1, 1, 1);
        people[1] = new Student("ff", "jj");
    }
}

5.1.10 受保护访问

有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为protected。例如,如果将超类Employee中的hireDay声明为proteced,而不是私有的,Manager中的方法就可以直接地访问它。

5.2 Object:所有类的超类

Object类是Java中所有类的始祖,在Java中每个类都是由它扩展而来的。
如果没有明确地指出超类,Object就被认为是这个类的超类。

5.2.1 equals方法

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

public class Employee extends Person
{
    
    
    @Override
    public boolean equals(Object o) {
    
    
        // 判断指针是否相等
        if (this == o) return true;
        // 判断类是否相等
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        // 判断类属性是否相同
        return id == employee.id &&
                Double.compare(employee.salary, salary) == 0 &&
                name.equals(employee.name) &&
                Objects.equals(hireDay, employee.hireDay);
    }

getClass方法将返回一个对象所属的类。

5.2.3 hashCode方法

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

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

如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。

hashCode方法应该返回一个整型数值(也可以是负数),并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。

@Override
public int hashCode() {
    
    
    return Objects.hash(id, name, salary, hireDay);
}

Equals与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode( )就必须与y.hashCode( )具有相同的值。例如,如果用定义的Employee.equals比较雇员的ID,那么hashCode方法就需要散列ID,而不是雇员的姓名或存储地址。

5.2.4 toString方法

在Object中还有一个重要的方法,就是toString方法,它用于返回表示对象值的字符串。
绝大多数(但不是全部)的toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。

@Override
public String toString() {
    
    
    return "Employee{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", salary=" + salary +
            ", hireDay=" + hireDay +
            '}';
}

最好通过调用getClass( ).getName( )获得类名的字符串,而不要将类名硬加到toString方法中。

toString方法是一种非常有用的调试工具。在标准类库中,许多类都定义了toString方法,以便用户能够获得一些有关对象状态的必要信息。

5.3 泛型数组列表 ArrayList

在许多程序设计语言中,特别是在C++语言中,必须在编译时就确定整个数组的大小。程序员对此十分反感,因为这样做将迫使程序员做出一些不情愿的折中。例如,在一个部门中有多少雇员?肯定不会超过100人。一旦出现一个拥有150名雇员的大型部门呢?愿意为那些仅有10名雇员的部门浪费90名雇员占据的存储空间吗?

在Java中,情况就好多了。它允许在运行时确定数组的大小。

在Java中,解决这个问题最简单的方法是使用Java中另外一个被称为ArrayList的类。它使用起来有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。

ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,例如,ArrayList

// 下面声明和构造一个保存Employee对象的数组列表:
ArrayList<Employee> staff = new ArrayList<>();
// 使用add方法可以将元素添加到数组列表中。
staff.add(new Employee("harry",1,1,1,1));
staff.add(new Employee("poot",1,1,1,1));
// 如果已经清楚或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法:
staff.ensureCapacity(100);
// size方法将返回数组列表中包含的实际元素数目。
int size = staff.size();
// 一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。
staff.trimToSize();

new ArrayList<>();省略了后面的Employee类型,这被称为**“菱形”语法**,因为空尖括号<>就像是一个菱形。可以结合new操作符使用菱形语法。编译器会检查新值是什么。如果赋值给一个变量,或传递到某个方法,或者从某个方法返回,编译器会检查这个变量、参数或方法的泛型类型,然后将这个类型放在<>中。

如果调用add且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。

5.3.1 访问数组列表元素

使用get和set方法实现访问或改变数组元素的操作,而不使用人们喜爱的[ ]语法格式。
使用add方法为数组添加新元素,而不要使用set方法,它只能替换数组中已经存在的元素内容。

// 替换0号元素
staff.set(0, new Employee("ff",3,4,5,1));
// 获取到1号元素
Employee e = staff.get(1);
// 还可以在数组列表的中间插入元素,使用带索引参数的add方法。
// 位于n之后的所有元素都要向后移动一个位置。如果插入新元素后,数组列表的大小超过了容量,数组列表就会被重新分配存储空间
staff.add(1, new Employee("ff",1,1,1,1));
// 可以从数组列表中间删除一个元素
// 位于这个位置之后的所有元素都向前移动一个位置,并且数组的大小减1
staff.remove(n);
// 删除指定的元素
Employee e = staff.get(1);
staff.remove(e);
// 遍历数组
for (Employee employee : staff) {
    
    
    System.out.println(employee);
}

5.4 对象包装器与自动装箱

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

对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。

假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型,也就是说,不允许写成ArrayList。这里就用到了Integer对象包装器类。我们可以声明一个Integer对象的数组列表。

ArrayList<Integer> list = new ArrayList<>();

幸运的是,有一个很有用的特性,从而更加便于添加int类型的元素到ArrayList中。
下面这个调用

list.add(3);

将自动地变换成

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

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

当将一个Integer对象赋给一个int值时,将会自动地拆箱。也就是说,编译器将下列语句:

int n = list.get(i);

将自动地变换成

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

大多数情况下,容易有一种假象,即基本类型与它们的对象包装器是一样的,只是它们的相等性不同。大家知道,==运算符也可以应用于对象包装器对象,只不过检测的是对象是否指向同一个存储区域,因此,下面的比较通常不会成立:

Integer a = 1000;
Integer b = 1000;
if (a == b) 

然而,Java实现却有可能(may)让它成立。如果将经常出现的值包装到同一个对象中,这种比较就有可能成立。这种不确定的结果并不是我们所希望的。解决这个问题的办法是在两个包装器对象比较时调用equals方法。

注释:自动装箱规范要求boolean、byte、char≤127,介于-128~127之间的short和int被包装到固定的对象中。例如,如果在前面的例子中将a和b初始化为100,对它们进行比较的结果一定成立。

5.5 参数数量可变的方法

现在的版本提供了可以用可变的参数数量调用的方法(有时称为“变参”方法)。
例如,下面的方法调用:

int n = 10;
System.out.printf("%d", n);
System.out.printf("fff");
System.out.printf("%d %s", n, "widgets");

在上面语句中,尽管一个调用包含两个参数,一个调用包含三个参数,一个调用包含一个参数,但它们调用的都是同一个方法。
printf语句定义如下:

public PrintStream printf(String format, Object ... args) {
    
    
    return format(format, args);
}

这里的省略号…是Java代码的一部分,它表明这个方法可以接收任意数量的对象(除fmt参数之外)。

实际上,printf方法接收两个参数,一个是格式字符串,另一个是Object[ ]数组,其中保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值,自动装箱功能将把它们转换成对象)。

用户自己也可以定义可变参数的方法,并将参数指定为任意类型,甚至是基本类型。下面是一个简单的示例:其功能为计算若干个数值的最大值。

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

5.6 枚举类

在比较两个枚举类型的值时,永远不需要调用equals,而直接使用“= =”就可以了。

public class EnumTest {
    
    
    public static void main(String[] args) {
    
    
    	// 将s设置成Size.SMALL。
        Size size = Enum.valueOf(Size.class, "SMALL");
        String abbr = size.getAbbr();
        // ordinal方法返回enum声明中枚举常量的位置,位置从0开始计数。
        int ordinal = size.ordinal();
        // 每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组。
        Size[] sizes = Size.values();
        for (Size s: sizes) {
    
    
            System.out.println(s);
        }
    }
}

enum Size
{
    
    
    SMALL("S"),MEDIUM("M"),LARGE("L");
    // 构造函数 abbr为缩写
    private Size(String abbr) {
    
    this.abbr = abbr;}
    public String getAbbr() {
    
     return abbr;}
    private String abbr;
}

5.7 反射

反射库(reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。这项功能被大量地应用于JavaBeans中,它是Java组件的体系结构。

特别是在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力。

能够分析类能力的程序称为反射(reflective)。反射机制的功能极其强大,在下面可以看到,反射机制可以用来:

  • 在运行时分析类的能力。
  • 在运行时查看对象,例如,编写一个toString方法供所有类使用。
  • 实现通用的数组操作代码。
  • 利用Method对象,这个对象很像C++中的函数指针。

反射是一种功能强大且复杂的机制。使用它的主要人员是工具构造者,而不是应用程序员。

5.7.1 Class类

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

然而,可以通过专门的Java类访问这些信息。保存这些信息的类被称为Class,这个名字很容易让人混淆。Object类中的getClass( )方法将会返回一个Class类型的实例。

public static void main(String[] args) {
    
    
    Employee employee = new Employee();
    // 获取对应的类 com.company.Employee
    Class cl = employee.getClass();
    String name = cl.getName();
    // 根据name获取class 有可能找不到对应的类,需要抛出异常
    try {
    
    
        Class empCl = Class.forName("com.company.Employee");
    } catch (ClassNotFoundException exception) {
    
    
        exception.printStackTrace();
    }
}

5.7.2 捕获异常

当程序运行过程中发生错误时,就会“抛出异常”。抛出异常比终止程序要灵活得多,这是因为可以提供一个**“捕获”异常的处理器**(handler)对异常情况进行处理。

如果没有提供处理器,程序就会终止,并在控制台上打印出一条信息,其中给出了异常的类型。可能在前面已经看到过一些异常报告,例如,偶然使用了null引用或者数组越界等。

异常有两种类型:未检查异常和已检查异常。对于已检查异常,编译器将会检查是否提供了处理器。然而,有很多常见的异常,例如,访问null引用,都属于未检查异常。编译器不会查看是否为这些错误提供了处理器。毕竟,应该精心地编写代码来避免这些错误的发生,而不要将精力花在编写异常处理器上。

并不是所有的错误都是可以避免的。如果竭尽全力还是发生了异常,编译器就要求提供一个处理器。Class.forName方法就是一个抛出已检查异常的例子。在第7章中,将会看到几种异常处理的策略。现在,只介绍一下如何实现最简单的处理器。将可能抛出已检查异常的一个或多个方法调用代码放在try块中,然后在catch子句中提供处理器代码。

try {
    Class empCl = Class.forName("com.company.Employee");
} catch (ClassNotFoundException exception) {
    exception.printStackTrace();
}

如果类名不存在,则将跳过try块中的剩余代码,程序直接进入catch子句(这里,利用Throwable类的printStackTrace方法打印出栈的轨迹。Throwable是Exception类的超类)。如果try块中没有抛出任何异常,那么会跳过catch子句的处理器代码。对于已检查异常,只需要提供一个异常处理器。可以很容易地发现会抛出已检查异常的方法。如果调用了一个抛出已检查异常的方法,而又没有提供处理器,编译器就会给出错误报告。

5.7.3 利用反射分析类的能力

下面简要地介绍一下反射机制最重要的内容——检查类的结构。

在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。这三个类都有一个叫做getName的方法,用来返回项目的名称。Field类有一个getType方法,用来返回描述域所属类型的Class对象。Method和Constructor类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。这三个类还有一个叫做getModifiers的方法,它将返回一个整型数值,用不同的位开关描述public和static这样的修饰符使用状况。另外,还可以利用java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整型数值。

Class类中的getFields、getMethods和getConstructors方法将分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。Class类的getDeclareFields、getDeclareMethods和getDeclaredConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。

下面程序显示了如何打印一个类的全部信息的方法。
这个程序将提醒用户输入类名,然后输出类中所有的方法和构造器的签名,以及全部域名。
假如输入

java.lang.Double

程序将会输出:

public final class java.lang.Double extends java.lang.Number
{
    
    
   public java.lang.Double(double);
   public java.lang.Double(java.lang.String);

   public boolean equals(java.lang.Object);
   public static java.lang.String toString(double);
   public java.lang.String toString();
   public int hashCode();
   public static int hashCode(double);
   public static double min(double, double);
   public static double max(double, double);
   public static native long doubleToRawLongBits(double);
   public static long doubleToLongBits(double);
   public static native double longBitsToDouble(long);
   public volatile int compareTo(java.lang.Object);
   public int compareTo(java.lang.Double);
   public byte byteValue();
   public short shortValue();
   public int intValue();
   public long longValue();
   public float floatValue();
   public double doubleValue();
   public static java.lang.Double valueOf(java.lang.String);
   public static java.lang.Double valueOf(double);
   public static java.lang.String toHexString(double);
   public static int compare(double, double);
   public static boolean isNaN(double);
   public boolean isNaN();
   public static boolean isFinite(double);
   public static boolean isInfinite(double);
   public boolean isInfinite();
   public static double sum(double, double);
   public static double parseDouble(java.lang.String);

   public static final double POSITIVE_INFINITY 
   public static final double NEGATIVE_INFINITY 
   public static final double NaN 
   public static final double MAX_VALUE 
   public static final double MIN_NORMAL 
   public static final double MIN_VALUE 
   public static final int MAX_EXPONENT 
   public static final int MIN_EXPONENT 
   public static final int SIZE 
   public static final int BYTES 
   public static final java.lang.Class TYPE 
   private final double value 
   private static final long serialVersionUID 
}

代码如下

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class ReflectionTest {
    
    
    public static void main(String[] args) {
    
    
        String name;
        if (args.length > 0)
            name = args[0];
        else {
    
    
            Scanner in = new Scanner(System.in);
            System.out.println("enter class name: ");
            name = in.next();
        }
        try {
    
    
            Class cl = Class.forName(name);             // 根据name获取类
            Class superCl = cl.getSuperclass();         // 获取超类
            String modifier = Modifier.toString(cl.getModifiers()); // 获取修饰符
            if (modifier.length() > 0) {
    
    
                System.out.print(modifier + " ");
            }
            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);
    }
    
    // 获取构造函数
    public static void printConstructors(Class cl){
    
    
        Constructor[] constructors = cl.getDeclaredConstructors();
        for (Constructor constructor : constructors)
        {
    
    
            String name = constructor.getName();
            System.out.print("   ");
            String modifiers = Modifier.toString(constructor.getModifiers());
            if (modifiers.length() > 0)
                System.out.print(modifiers + " ");
            System.out.print(name + "(");
            Class[] paramTypes = constructor.getParameterTypes(); // 获取参数类型
            for (int i = 0; i < paramTypes.length; i++) {
    
    
                if (i > 0)
                    System.out.print(", ");
                System.out.print(paramTypes[i].getName());
            }
            System.out.println(");");
        }
    }

    // 获取类的方法
    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("   ");
            String modifier = Modifier.toString(m.getModifiers());
            if (modifier.length() > 0)
                System.out.print(modifier + " ");
            System.out.print(retType.getName() + " " + name + "(");
            Class[] paraTypes = m.getParameterTypes();
            for (int i = 0; i <paraTypes.length; i++) {
    
    
                if (i > 0)
                    System.out.print(", ");
                System.out.print(paraTypes[i].getName());
            }
            System.out.println(");");
        }
    }
    
    // 获取类的字段
    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 + " ");
        }
    }
}

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

查看对象域的关键方法是Field类中的get方法。如果f是一个Field类型的对象(例如,通过getDeclaredFields得到的对象), obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的当前值。这样说起来显得有点抽象,这里看一看下面这个示例的运行。

/code

public static void main(String[] args) {
    
    
        Employee employee = new Employee("fff",1,1,1,1);
        Class cl = employee.getClass();
        try {
    
    
            // 获取字段
            Field f = cl.getDeclaredField("salary");
            // 对private类型变量设置访问访问权限
            f.setAccessible(true);
            // 获取该对象当前该字段的值
            Object v = f.get(employee);
        } catch (NoSuchFieldException exception) {
    
    
            exception.printStackTrace();
        } catch (IllegalAccessException exception) {
    
    
            exception.printStackTrace();
        }
    }

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

5.7.6 调用任意方法

在C和C++中,可以从函数指针执行任意函数。从表面上看,Java没有提供方法指针,即将一个方法的存储地址传给另外一个方法,以便第二个方法能够随后调用它。事实上,Java的设计者曾说过:方法指针是很危险的,并且常常会带来隐患。他们认为Java提供的接口(interface)是一种更好的解决方案。然而,反射机制允许你调用任意方法。

在Method类中有一个invoke方法,它允许调用包装在当前Method对象中的方法。invoke方法的签名是:

Object invoke(Object obj, Object … args)

第一个参数是隐式参数,其余的对象提供了显式参数。对于静态方法,第一个参数可以被忽略,即可以将它设置为null。

public static void main(String[] args) {
    
    
        Employee employee = new Employee("fff",1,1,1,1);
        Class cl = employee.getClass();
        try {
    
    
            // Method m = cl.getDeclaredMethod("getSalary");
            Method m = cl.getMethod("getSalary");
            Double n = (Double) m.invoke(employee);
        } catch (IllegalAccessException exception) {
    
    
            exception.printStackTrace();
        } catch (NoSuchMethodException exception) {
    
    
            exception.printStackTrace();
        } catch (InvocationTargetException exception){
    
    
            exception.printStackTrace();
        }
    }

5.8 继承的设计技巧

  1. 将公共操作和域放在超类
  2. 不要使用受保护的域(子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性)
  3. 使用继承实现“is-a”关系
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 在覆盖方法时,不要改变预期的行为
  6. 使用多态,而非类型信息
  7. 不要过多地使用反射(反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。)

猜你喜欢

转载自blog.csdn.net/qq_17677907/article/details/112184536