Java继承

1. 类,超类和子类

假设你在某个公司工作,这个公司中经理的待遇与普通雇员的待遇存在着一些差异。不过, 他们之间也存在着很多相同的地方,例如, 他们都领取薪水。只是普通雇员在完成本职任务之后仅领取薪水, 而经理在完成了预期的业绩之后还能得到奖金。这种情形就需要使用继承。这是因为需要为经理定义一个新类 Manager, 以便增加一些新功能。但可以重用 Employee 类中已经编写的部分代码,并将其中的所有域保留下来。从理论上讲, 在 Manager 与 Employee 之间存在着明显的“ is-a”(是)关系,每个经理都是一名雇员:“ is-a” 关系是继承的一个明显特征

注释: 我们使用员工和经理的传统示例, 不过必须提醒对这个例子要有所保留。在真实世界里, 员工也可能会成为经理, 所以建模时可能希望经理也是员工,而不是一个子类。不过,在例子中, 假设公司里只有两类人:一些人一直是员工,另一些人一直是经理。

1.1 定义子类

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

public class Manager extends Employee{
}

C++ 注释: Java 与 C++ 定义继承类的方式十分相似。Java 用关键字 extends 代替了 C++中的冒号(:)。在 Java 中, 所有的继承都是公有继承, 而没有 C++ 中的私有继承和保护继承

关键字 extends 表明正在构造的新类派生于一个已存在的类。 已存在的类称为超类
( superclass)
基类( base class)父类(parent class); 新类称为子类(subclass、) 派生类( derived class)孩子类(child class)。 超类和子类是 Java 程序员最常用的两个术语,而了解其他语言的程序员可能更加偏爱使用父类和孩子类,这些都是继承时使用的术语。
尽管 Employee 类是一个超类,但并不是因为它优于子类或者拥有比子类更多的功能。实际上恰恰相反,子类比超类拥有的功能更加丰富。例如, 读过 Manager 类的源代码之后就会发现, Manager类比超类 Employee 封装了更多的数据, 拥有更多的功能。

注释: 前缀“ 超” 和“ 子” 来源于计算机科学和数学理论中的集合语言的术语。所有雇员组成的集合包含所有经理组成的集合。可以这样说, 雇员集合是经理集合的超集, 也可以说,经理集合是雇员集合的子集。

在 Manager 类中,增加了一个用于存储奖金信息的域,以及一个用于设置这个域的新方法:
Manager.java 文件

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

Employee.java 文件

class Employee {
    private String name;
    private double salary;
    private LocalDate hireDay;
    private static int nexId;
    private int id = assignId();

    private static int assignId() {
        int r = nexId;
        ++nexId;
        return r;
    }

    public Employee() {
        this.name = "";
        this.salary = 0;
        this.hireDay = LocalDate.now();
    }

    public Employee(double s) {
        this("Employee #" + nexId, s);
        nexId++;
    }

    public Employee(String aName, double aSalary) {
        this.name = aName;
        this.salary = aSalary;
    }

    {
        id = nexId;
        ++nexId;
    }

    public String getName() {
        return this.name;
    }

    public double getSalary() {
        return this.salary;
    }

    public void raiseSalary(double byPercent) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }
}

这里定义的方法和域并没有什么特别之处。 如果有一个 Manager 对象, 就可以使用setBonus方法

Manager boss = . . .;
boss.setBonus(5000);

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

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

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

1.2 覆盖方法 [ 重写 ]

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

public class Manager extends Employee {
    private double bonus;

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }

    public double getSalary() {
        return salary + this.bonus;// won't work
    }
}

应该如何实现这个方法呢? 乍看起来似乎很简单, 只要返回 salary 和 bonus 域的总和就可以了。然而,这个方法并不能运行。这是因为 Manager 类的 getSalary 方法不能够直接地访问超类的私有域。也就是说,尽管每个 Manager 对象都拥有一个名为 salary 的域, 但在Manager 类的getSalary方法中并不能够直接地访问 salary 域。只有 Employee 类的方法才能够访问私有部分。如果 Manager 类的方法一定要访问私有域, 就必须借助于公有的接口, Employee类中的公有方法 getSalary 正是这样一个接口。

public class Manager extends Employee {
    private double bonus;

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }

     public double getSalary() {
        double salary = getSalary();
        return salary + this.bonus;// won't work
    }
}

上面这段代码仍然不能运行。问题出现在调用 getSalary 的语句上,这是因为 Manager类也有一个 getSalary方法(就是正在实现的这个方法,) 所以这条语句将会导致无限次地调用自己,直到整个程序崩溃为止。

这里需要指出:我们希望调用超类 Employee 中的 getSalary 方法, 而不是当前类的这个方法。为此, 可以使用特定的关键字 super 解决这个问题:

super.getSalary();

上述语句调用的是 Employee 类中的 getSalary 方法。下面是 Manager 类中 getSalary方法的正确书写格式:

public class Manager extends Employee {
    private double bonus;

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }

    public double getSalary() {
        double salary = super.getSalary();
        return salary + this.bonus;// won't work
    }
}

注释: 有些人认为 super 与 this 引用是类似的概念, 实际上,这样比较并不太恰当。这是因为 super 不是一个对象的引用, 不能将 super 赋给另一个对象变量, 它只是一个指示编译器调用超类方法的特殊关键字。

正像前面所看到的那样, 在子类中可以增加域、 增加方法或覆盖超类的方法,然而绝对
不能删除继承的任何域和方法。

C++ 注释: 在 Java 中使用关键字 super 调用超类的方法,而在 C++ 中则采用超类名加上::操作符的形式。例如, 在 Manager 类的 getSalary 方法中,应该将super.getSalary 替换为 Employee::getSalary

1.3 子类构造器

在例子的最后,我们来提供一个构造器。

public class Manager extends Employee {
    private double bonus;

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }

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

    public double getSalary() {
        double salary = super.getSalary();
        return salary + this.bonus;// won't work
    }
}

由于 Manager 类的构造器不能访问 Employee 类的私有域, 所以必须利用 Employee类的构造器对这部分私有域进行初始化,我们可以通过 super 实现对超类构造器的调用。使用super 调用构造器的语句必须是子类构造器的第一条语句

如果子类的构造器没有显式地调用超类的构造器, 则将自动地调用超类默认(没有参数)的构造器。 如果超类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用超类的其他构造器’则 Java 编译器将报告错误。

注释: 回忆一下, 关键字 this 有两个用途: 一是引用隐式参数,二是调用该类其他的构造器 , 同样,super 关键字也有两个用途:一是调用超类的方法,二是调用超类的构造器。在调用构造器的时候,这两个关键字的使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造参数既可以传递给本类( this) 的其他构造器,也可以传递给超类(super ) 的构造器。

**C++ 注释:**在 C++ 的构造函数中, 使用初始化列表语法调用超类的构造函数, 而不调用super □ 在 C++ 中, Manager 的构造函数如下所示:

Manager::Manager(String name , double salary, int year, int month, int day) // C++
: Employee(name, salary, year, month , day)
{
bonus = 0;
}

重新定义 Manager 对象的 getSalary 方法之后, 奖金就会自动地添加到经理的薪水中。下面给出一个例子,其功能为创建一个新经理,并设置他的奖金

public class ManagerTest {
    public static void main(String[] args) {
        Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
        boss.setBonus(5000);
        Employee[] staff = new Employee[3];
        staff[0] = boss;
        staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
        staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
        for (Employee e : staff) {
            System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
        }
    }
}


name=Carl Cracker,salary=85000.0
name=Harry Hacker,salary=50000.0
name=Tommy Tester,salary=40000.0

这里的 staff[l] 和 staffs]仅输出了基本薪水,这是因为它们对应的是 Employee 对象,而staff[0] 对应的是 Manager 对象, 它的 getSalary 方法将奖金与基本薪水加在了一起。

需要提到的是,
e.getSalary()

调用能够确定应该执行哪个 getSalary 方法。请注意, 尽管这里将 e 声明为 Employee类型,但实际上 e 既可以引用 Employee 类型的对象,也可以引用 Manager 类型的对象

当 e 引用 Employee 对象时, e.getSalary( ) 调用的是 Employee 类中的 getSalary 方法;当e 引用 Manager 对象时,e.getSalary( ) 调用的是 Manager 类中的 getSalary 方法。虚拟机知道e 实际引用的对象类型,因此能够正确地调用相应的方法。

一个对象变量(例如, 变量 e ) 可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding。)

C++ 注释: 在 Java 中, 不需要将方法声明为虚拟方法。动态绑定是默认的处理方式。如果不希望让一个方法具有虚拟特征, 可以将它标记为 final

ManagerTest.java

public class ManagerTest {
    public static void main(String[] args) {
        Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
        boss.setBonus(5000);
        Employee[] staff = new Employee[3];
        staff[0] = boss;
        staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
        staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
        for (Employee e : staff) {
            System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
        }
    }
}

Employee.java

import java.time.LocalDate;


public class Employee {
    private String name;
    private double salary;
    private LocalDate hireDay;

    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        this.hireDay = LocalDate.of(year, month, day);
    }

    public String getName() {
        return this.name;
    }

    public double getSalary() {
        return this.salary;
    }

    public LocalDate getHireDay() {
        return this.hireDay;
    }

    public void raiseSalary(double byPercent) {
        double raise = this.salary * byPercent / 100;
        this.salary += raise;
    }
}

Manager.java

public class Manager extends Employee {
    private double bonus;

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }

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

    public double getSalary() {
        double salary = super.getSalary();
        return salary + this.bonus;// won't work
    }
}

1.4 继承层次

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

通常, 一个祖先类可以拥有多个子孙继承链。 例如, 可以由 Employee 类派生出子类 Programmer 或 Secretary, 它们与Manager 类没有任何关系(有可能它们彼此之间也没有任何关系)。必要的话,可以将这个过程一直延续下去
在这里插入图片描述
C++ 注释: Java 不支持多继承。有关Java 中多继承功能的实现方式可以通过接口实现。

1.5 多态

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

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

        Employee e;
        e = new Employee(. . .); // Employee object expected
        e - new Manager(. . .); // OK, Manager can be used as well

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

		Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
        boss.setBonus(5000);
        Employee[] staff = new Employee[3];
        staff[0] = boss;

在这个例子中,变量 staff[0] 与 boss 引用同一个对象。但编译器将 staff[0]看成Employee 对象。这意味着, 可以这样调用

boss.setBonus(5000); // OK

但不能这样调用

staff[0].setBonus(5000); // Error

这是因为 staff[0] 声明的类型是 Employee, 而 seffionus 不是 Employee 类的方法

然而,不能将一个超类的引用赋给子类变量。例如,下面的赋值是非法的

Manager m = staff[i]; // Error

原因很清楚:不是所有的雇员都是经理。 如果赋值成功,m 有可能引用了一个不是经理的Employee 对象, 当在后面调用 m.setBonus(…) 时就有可能发生运行时错误

警告: 在 Java 中,子类数组的引用可以转换成超类数组的引用, 而不需要采用强制类型转换。例如, 下面是一个经理数组

Manager[] managers = new Manager[10];

将它转换成 Employee[] 数组完全是合法的:

Employee[] staff = managers; // OK

这样做肯定不会有问题, 请思考一下其中的缘由。 毕竟, 如果 manager[i] 是一个Manager, 也一定是一个 Employee。然而, 实际上,将会发生一些令人惊讶的事情。要切记 managers 和 staff 引用的是同一个数组。现在看一下这条语句:

staff[0] = new Employee("Harry Hacker", . . .);

编译器竟然接纳了这个赋值操作。但在这里, stafflO] 与 manager[0] 引用的是同一个对象, 似乎我们把一个普通雇员擅自归入经理行列中了。这是一种很忌伟发生的情形,当调用 managers[0].setBonus(1000) 的时候, 将会导致调用一个不存在的实例域, 进而搅乱相邻存储空间的内容。

1.6 理解方法调用

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

  1. 编译器査看对象的声明类型和方法名。假设调用 x.f(param,) 且隐式参数 x 声明为 C类的对象。需要注意的是:有可能存在多个名字为 f, 但参数类型不一样的方法。例如,可能存在方法 f(im) 和方法 String。) 编译器将会一一列举所有 C 类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)。
    至此, 编译器已获得所有可能被调用的候选方法。
  2. 接下来,编译器将査看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重栽解析(overloading resolution)。例如,对于调用 x.f(“ Hello” )来说, 编译器将会挑选f(String,) 而不是f(int)。由于允许类型转换( int 可以转换成 double, Manager 可以转换成 Employee, 等等,) 所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法, 或者发现经过类型转换后有多个方法与之匹配, 就会报告一个错误。

至此, 编译器已获得需要调用的方法名字和参数类型。

注释: 前面曾经说过, 方法的名字和参数列表称为方法的签名。例如, f(int) 和f(String)是两个具有相同名字, 不同签名的方法。如果在子类中定义了一个与超类签名相同的方法, 那么子类中的这个方法就覆盖了超类中的这个相同签名的方法。

不过,返回类型不是签名的一部分, 因此,在覆盖方法时, 一定要保证返回类型的兼容性。 允许子类将覆盖方法的返回类型定义为原返回类型的子类型。例如, 假设Employee 类有

public Employee getBuddy() { . . . }

经理不会想找这种普通的员工。为了反映这一点, 在后面的子类 Manager 中,可以按照如下所示的方式覆盖这个方法

public Manager getBuddy() { . . . } // OK to change return type

我们说,这两个 getBuddy 方法具有可协变的返回类型。

  1. 如果是 private 方法、 static 方法、 final 方法或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为静态绑定( static binding )。 与此对应的是,调用的方法依赖于隐式参数的实际类型, 并且在运行时实现动态绑定。在我们列举的示例中, 编译器采用动态绑定的方式生成一条调用 f(String) 的指令。

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

每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表( method table), 其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候, 虚拟机仅查找这个表就行了。在前面的例子中, 虚拟机搜索 D 类的方法表, 以便寻找与调用 f(Sting) 相K配的方法。这个方法既有可能是 D.f(String), 也有可能是X.f(String), 这里的 X 是 D 的超类。这里需要提醒一点,如果调用super.f(param), 编译器将对隐式参数超类的方法表进行搜索

public class ManagerTest {
    public static void main(String[] args) {
        Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
        boss.setBonus(5000);
        Employee[] staff = new Employee[3];
        staff[0] = boss;
        staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
        staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
        for (Employee e : staff) {
            System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
        }
    }
}

调用 e.getSalary() 的详细过程
e 声明为 Employee 类型。Employee 类只有一个名叫 getSalary 的方法, 这个方法没有参数。 因此, 在这里不必担心重载解析的问题。

由于 getSalary 不是 private 方法、 static 方法或 final 方法,所以将采用动态绑定。虚拟机为 Employee 和 Manager 两个类生成方法表。在 Employee 的方法表中, 列出了这个类定义的所有方法:

Employee:
getName()> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)

实际上, 上面列出的方法并不完整, 稍后会看到 Employee 类有一个超类 Object,Employee 类从这个超类中还继承了许多方法,在此,我们略去了 Object 方法。

Manager方法表稍微有些不同。其中有三个方法是继承而来的,一个方法是重新定义的,还有一个方法是新增加的

Manager:
getName() -> Employee.getName()
getSalary() -> Manager.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
setBonus(double) -> Manager.setBonus(double)

在运行时, 调用 e.getSalary() 的解析过程为:

  1. 首先,虚拟机提取 e 的实际类型的方法表。既可能是 Employee、 Manager 的方法表,也可能是 Employee 类的其他子类的方法表
  2. 接下来, 虚拟机搜索定义 getSalary 签名的类。此时,虚拟机已经知道应该调用哪个方法
  3. 最后,虚拟机调用方法

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

警告: 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是, 如果超类方法是 public, 子类方法一定要声明为 public。经常会发生这类错误:在声明子类方法的时候, 遗漏了 public 修饰符。此时,编译器将会把它解释为试图提供更严格的访问权限

1.7 阻止继承:final 类和方法

有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为 final 类。如果在定义类的时候使用了 final 修饰符就表明这个类是 final 类。例如, 假设希望阻止人们定义Executive类的子类,就可以在定义这个类的时候’使用 final 修饰符声明。声明格式如下所示:

public final class Executive extends Manager {
    public Executive(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);
    }
}

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

public class Employee {
    private String name;
    private double salary;
    private LocalDate hireDay;

    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        this.hireDay = LocalDate.of(year, month, day);
    }

    public final String getName() {
        return this.name;
    }

    public double getSalary() {
        return this.salary;
    }

    public LocalDate getHireDay() {
        return this.hireDay;
    }

    public void raiseSalary(double byPercent) {
        double raise = this.salary * byPercent / 100;
        this.salary += raise;
    }
}

注释: 前面曾经说过, 域也可以被声明为 final。 对于 final 域来说,构造对象之后就不允许改变它们的值了。不过, 如果将一个类声明为 final, 只有其中的方法自动地成为final,而不包括域

将方法或类声明为 final 主要目的是: 确保它们不会在子类中改变语义。例如:String类也是 final 类,这意味着不允许任何人定义 String 的子类。换言之,如果有一个 String 的引用, 它引用的一定是一个 String 对象, 而不可能是其他类的对象

为了避免动态绑定带来的系统开销而使用 final 关键字。如果一个方法没有被覆盖并且很短, 编译器就能够对它进行优化处理, 这个过程为称为内联( inlining )。例如,内联调用 e.getName( ) 将被替换为访问 e.name 域。这是一项很有意义的改进, 这是由于 CPU 在处理调用方法的指令时, 使用的分支转移会扰乱预取指令的策略, 所以,这被视为不受欢迎的。然而,如果 getName 在另外一个类中被覆盖, 那么编译器就无法知道覆盖的代码将会做什么操作,因此也就不能对它进行内联处理了。幸运的是, 虚拟机中的即时编译器比传统编译器的处理能力强得多。这种编译器可以准确地知道类之间的继承关系, 并能够检测出类中是否真正地存在覆盖给定的方法。如果方法很简短、 被频繁调用且没有真正地被覆盖, 那么即时编译器就会将这个方法进行内联处理。如果虚拟机加载了另外一个子类,而在这个子类中包含了对内联方法的覆盖, 那么将会发生什么情况呢? 优化器将取消对覆盖方法的内联。这个过程很慢, 但却很少发生。

1.8 强制类型转换

将一个类型强制转换成另外一个类型的过程被称为类型转换。Java 程序设计语言提供了一种专门用于进行类型转换的表示法。例如:

double x = 3.405;
int nx = (int) x ;

将表达式 x 的值转换成整数类型, 舍弃了小数部分

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

Manager boss = (Manager) staff[0]:

进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。例如, 在 managerTest 类中,由于某些项是普通雇员, 所以 staff 数组必须是Employee 对象的数组。我们需要将数组中引用经理的元素复原成 Manager 类, 以便能够访问新增加的所有变量(需要注意, 在前面的示例代码中, 为了避免类型转换, 我们做了一些特别的处理, 即将 boss 变量存入数组之前,先用 Manager 对象对它进行初始化。而为了设置经理的奖金, 必须使用正确的类型。

大家知道,在 Java 中,每个对象变量都属于一个类型。类型描述了这个变量所引用的以及能够引用的对象类型。例如,staff[i]引用一个 Employee 对象(因此它还可以引用Manager对象。

将一个值存人变量时, 编译器将检查是否允许该操作。将一个了-类的引用赋给一个超类变量, 编译器是允许的。但将一个超类的引用赋给一个子类变量, 必须进行类型转换,这样才能够通过运行时的检査。

如果试图在继承链上进行向下的类型转换,并且“ 谎报” 有关对象包含的内容,会发生什么情况呢?

Manager boss = (Manager) staff[1]; // Error

Exception in thread "main" java.lang.ClassCastException: inheritance.Employee cannot be cast to inheritance.Manager at inheritance.ManagerTest.main(ManagerTest.java:21)

运行这个程序时, Java 运行时系统将报告这个错误,并产生一个 ClassCastException异常。 如果没有捕获这个异常,那么程序就会终止。因此,应该养成这样一个良好的程序设计习惯:在进行类型转换之前,先查看一下是否能够成功地转换。这个过程简单地使用 instanceof 操作符就可以实现。 例如:

if (staff[1] instanceof Manager){
    boss = (Manager) staff[1];
}

最后, 如果这个类型转换不可能成功, 编译器就不会进行这个转换。例如,下面这个类型转换:

String c = (String) staff[1];

将会产生编译错误, 这是因为 String 不是 Employee 的子类。
综上所述:

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

注释: 如果 x 为 null , 进行下列测试

x instanceof C

不会产生异常, 只是返回 false。之所以这样处理是因为 null 没有引用任何对象, 当然也不会引用 C 类型的对象。

实际上,通过类型转换调整对象的类型并不是一种好的做法。在我们列举的示例中, 大多数情况并不需要将 Employee 对象转换成 Manager 对象, 两个类的对象都能够正确地调用getSalary 方法,这是因为实现多态性的动态绑定机制能够自动地找到相应的方法

只有在使用 Manager 中特有的方法时才需要进行类型转换, 例如, setBonus 方法如果鉴于某种原因,发现需要通过 Employee 对象调用 setBonus 方法, 那么就应该检查一下超类的设计是否合理。重新设计一下超类,并添加 setBonus法才是正确的选择。请记住,只要没有捕获 ClassCastException 异常,程序就会终止执行。 在一般情况下,应该尽量少用类型转换和 instanceof 运算符。

C++ 注释: Java 使用的类型转换语法来源于 C 语言“ 以往糟糕的日子”, 但处理过程却有些像 C++的 dynamic_cast 操作。例如:

Manager boss = (Manager) staff[1]; // Java

等价于

Manager* boss = dynamic_cast<Manager*>(staff[1]); // C++

它们之间只有一点重要的区别: 当类型转换失败时, Java 不会生成一个 null 对象,
而是抛出一个异常。从这个意义上讲, 有点像 C++ 中的引用 ( reference) 转换。在 C++ 中, 可以在一个操作中完成类型测试和类型转换。

Manager* boss = dynainic_ca.st<Manager*>(staff[1]); // C++
if (boss != NULL) . . .

而在 Java 中, 需要将 instanceof 运算符和类型转换组合起来使用:

if (staff[1] instanceof Manager){
    boss = (Manager) staff[1];
}

1.9 抽象类

如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看, 祖先类更加通用, 人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。例如, 考虑一下对 Employee 类层次的扩展。一名雇员是一个人,一名学生也是一个人。下面将类 Person 和类 Student 添加到类的层次结构中。下是这三个类之间的关系层次图。

为什么要花费精力进行这样高层次的抽象呢? 每个人都有一些诸如姓名这样的属性。学生与雇员都有姓名属性, 因此可以将 getName 方法放置在位于继承关系较高层次的通用超类中
现在, 再增加一个 getDescription 方法,它可以返回对一个人的简短描述。例如:
在这里插入图片描述
在 Employee 类和 Student 类 中 实 现这个方法很容易。 但是在 Person类中应该提供什么内容呢? 除了姓名之外,Person类一无所知。当然, 可以让Person.getDescriptionO 返回一个空字符串。然而,还有一个更好的方法, 就是使用abstract 关键字,这样就完全不需要实现这个方法了。

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

除了抽象方法之外,抽象类还可以包含具体数据和具体方法。例如, Person 类还保存着姓名和一个返回姓名的具体方法。

public abstract class Person {
    public abstract String getDescription();
    
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

提示: 许多程序员认为,在抽象类中不能包含具体方法。建议尽量将通用的域和方法(不管是否是抽象的)放在超类(不管是否是抽象类)中

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

例如, 通过扩展抽象 Person 类, 并实现 getDescription方法来定义 Student 类。由于在例如, 通过扩展抽象 Person 类, 并实现 getDescription方法来定义 Student 类。由于在Student 类中不再含有抽象方法,所以不必将这个类声明为抽象的

类即使不含抽象方法,也可以将类声明为抽象类。抽象类不能被实例化。也就是说,如果将一个类声明为 abstract, 就不能创建这个类的对象。例如,表达式

new Person("Vi nee Vu")//Error

是错误的, 但可以创建一个具体子类的对象。
需要注意, 可以定义一个抽象类的对象变量, 但是它只能引用非抽象子类的对象。 例如:

Person p = new Student("Vinee Vu" , "Economics");

这里的 p 是一个抽象类 Person 的变量,Person 引用了一个非抽象子类 Student 的实例。

C++ 注释: 在 OH•中, 有一种在尾部用 =0 标记的抽象方法, 称为纯虚函数, 例如:

class Person // C++
{
public:
virtual string getDescription() = 0;
}
只要有一个纯虚函数,这个类就是抽象类。在 C++ 中, 没有提供用于表示抽象类的特殊关键字。

下面定义一个扩展抽象类 Person 的具体子类 Student:

public class Student extends Person {
    private String major;

    public Student(String name, String major) {
        super(name);
        this.major = major;
    }

    public String getDescription() {
        return "a student majoring in " + major;
    }
}

在 Student 类中定义了 getDescription 方法。因此,在 Student 类中的全部方法都是非抽象的, 这个类不再是抽象类

定义了抽象超类 Persor 和两个具体子类 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()

这不是调用了一个没有定义的方法吗? 请牢记,由于不能构造抽象类 Person 的对象,所以变量 p 永远不会引用 Person 对象, 而是引用诸如 Employee 或 Student 这样的具体子类对象, 而这些对象中都定义了 getDescription 方法。

思考:

是否可以省略 Person 超类中的抽象方法, 而仅在 Employee 和 Student 子类中定义getDescription方法呢? 如果这样的话,就不能通过变量 p 调用 getDescription方法了。编译器只允许调用在类中声明的方法。 在 Java 程序设计语言中,抽象方法是一个重要的概念。在接口(interface) 中将会看到 更多的抽象方法

PersonTest.java

public class PersonTest {
    public static void main(String[] args) {
        Person[] people = new Person[2];
        people[0] = new Employee("Harry Hacker" , 50000, 1989, 10, 1);
        people[1] = new Student("Maria Morris", "computer science");
        for (Person p : people) {
            System.out.println(p.getName() + p.getDescription());
        }
    }
}

Person.java

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

    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Employee.java

package abstractClasses;

import java.time.LocalDate;


public class Employee extends Person {
    private double salary;
    private LocalDate hireDay;

    public Employee(String name, double salary, int year, int month, int day) {
        super(name);
        this.salary = salary;
        this.hireDay = LocalDate.of(year, month, day);
    }

    public double getSalary() {
        return this.salary;
    }

    public LocalDate getHireDay() {
        return this.hireDay;
    }

    public String getDescription() {
        return String.format("an employee with a salary of $%.2f", salary);
    }

    public void raiseSalary(double byPercent) {
        double raise = this.salary * byPercent / 100;
        this.salary += raise;
    }
}

Student.java

public class Student extends Person {
    private String major;

    public Student(String name, String major) {
        super(name);
        this.major = major;
    }

    public String getDescription() {
        return "a student majoring in " + major;
    }
}

1.10 受保护访问

大家都知道,最好将类中的域标记为 private, 而方法标记为 public。任何声明为 private的内容对其他类都是不可见的。前面已经看到, 这对于子类来说也完全适用,即子类也不能访问超类的私有域。
然而,在有些时候,人们希望超类中的某些方法允许被子类访问, 或允许子类的方法访问超类的某个域。为此, 需要将这些方法或域声明为 protected。例如,如果将超类Employee中的 hireDay 声明为 proteced, 而不是私有的, Manager 中的方法就可以直接地访问它。

不过,Manager 类中的方法只能够访问 Manager 对象中的 hireDay 域, 而不能访问其他Employee 对象中的这个域。这种限制有助于避免滥用受保护机制,使得子类只能获得访问受保护域的权利
在实际应用中,要谨慎使用 protected 属性。假设需要将设计的类提供给其他程序员使用,而在这个类中设置了一些受保护域, 由于其他程序员可以由这个类再派生出新类,并访问其中的受保护域。在这种情况下,如果需要对这个类的实现进行修改,就必须通知所有使用这个类的程序员。这违背了 OOP 提倡的数据封装原则。

受保护的方法更具有实际意义。 如果需要限制某个方法的使用, 就可以将它声明为protected。这表明子类(可能很熟悉祖先类)得到信任,可以正确地使用这个方法,而其他类则不行。这种方法的一个最好的示例就是 Object 类中的 clone 方法

C++ 注释: 事实上,Java 中的受保护部分对所有子类及同一个包中的所有其他类都可见。这与 c++ 中的保护机制稍有不同, Java 中的 protected 概念要比 C++ 中的安全性差。
下面归纳一下 Java 用于控制可见性的 4 个访问修饰符:

  1. 仅对本类可见 private
  2. 对所有类可见 public
  3. 对本包和所有子类可见 protected
  4. 对本包可见—默认(很遗憾) 不需要修饰符

2.Object:所有类的超类

Object 类是 Java 中所有类的始祖, 在 Java 中每个类都是由它扩展而来的。但是并不需要这样写:

public class Employee extends Object

如果没有明确地指出超类,Object 就被认为是这个类的超类。由于在 Java中,每个类都是由 Object 类扩展而来的,所以, 熟悉这个类提供的所有服务十分重要。可以使用 Object 类型的变量引用任何类型的对象:

Object obj = new Employee("Harry Hacker", 35000);

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

Employee e = (Employee) obj ;

在 Java 中,只有基本类型 ( primitive types) 不是对象, 例如,数值、 字符和布尔类型的值都不是对象

所有的数组类塱,不管是对象数组还是基本类型的数组都扩展了 Object 类

Employee[] staff = new Employee[10];
obj = staff; // OK
obj = new int[10]; // OK

C++ 注释: 在 C++ 中没有所有类的根类,不过,每个指针都可以转换成 void 指针*

2.1 equals 方法

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用, 它们一定是相等的。从这点上看,将其作为默认操作也是合乎情理的。然而,对于多数类来说, 这种判断并没有什么意义。例如, 采用这种方式比较两个 PrintStream 对象是否相等就完全没有意义。然而, 经常需要检测两个对象状态的相等性,如果两个对象的状态相等, 就认为这两个对象是相等的

例如, 如果两个雇员对象的姓名、 薪水和雇佣日期都一样, 就认为它们是相等的(在实际的雇员数据库中,比较 ID 更有意义。利用下面这个示例演示 equals 方法的实现机制)

    public boolean equals(Object otherObject) {
        if (this == otherObject) {
            return true;
        }
        if (null == otherObject) {
            return false;
        }
        if (getClass() != otherObject.getClass()) {
            return false;
        }
        Employee other = (Employee) otherObject;
        return name.equals(((Employee) otherObject).name)
                && salary == ((Employee) otherObject).salary
                && hireDay == ((Employee) otherObject).hireDay;
    }

getClass 方法将返回一个对象所属的类,有关这个方法的详细内容稍后进行介绍。在检
测中, 只有在两个对象属于同一个类时, 才有可能相等。

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

    public boolean equals(Object otherObject){
        if (!super.equals(otherObject)){
            return false;
        }else{
            Manager other = (Manager) otherObject;
            return bonus == other.bonus;
        }
    }

2.2 相等测试与继承

如果隐式和显式的参数不属于同一个类, equals 方法将如何处理呢? 这是一个很有争议的问题。 在前面的例子中, 如果发现类不匹配, equals 方法就返冋 false: 但是, 许多程序员却喜欢使用 instanceof 进行检测:

if ( !otherObject instanceof Employee)) return false;

这样做不但没有解决 otherObject 是子类的情况,并且还有可能会招致一些麻烦。这就是建议不要使用这种处理方式的原因所在。Java 语言规范要求 equals 方法具有下面的特性:

  1. 自反性:对于任何非空引用 x, x.equals(x) 应该返回 true
  2. 对称性: 对于任何引用 x 和 y, 当且仅当 y.equals(x) 返回 true , x.equals(y) 也应该返
    回 true
  3. 传递性: 对于任何引用 x、 y 和 z, 如果 x.equals(y) 返 true, y.equals(z) 返回true,x.equals(z) 也应该返回 true
  4. 一致性: 如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果
  5. 对于任意非空引用 x, x.equals(null) 应该返回 false
    这些规则十分合乎情理,从而避免了类库实现者在数据结构中定位一个元素时还要考虑调用 x.equals(y), 还是调用 y.equals(x) 的问题
    然而, 就对称性来说, 当参数不属于同一个类的时候需要仔细地思考一下。请看下面这个调用:
e.equals(m)

这里的 e 是一个 Employee 对象, m 是一个 Manager 对象, 并且两个对象具有相同的姓名、薪水和雇佣日期。 如果在 Employee.equals 中用 instanceof 进行检测, 则返回true,然而这意味着反过来调用:

m.equals(e)

也需要返回 true、 对称性不允许这个方法调用返回 false,或者抛出异常。
这就使得 Manager 类受到了束缚。 这个类的 equals 方法必须能够用自己与任何一
个 Employee 对象进行比较, 而不考虑经理拥有的那部分特有信息! 猛然间会让人感觉
instanceof 测试并不是完美无瑕。

也有人认为不应该利用 getClass 检测, 因为这样不符合置换原则有一个应用AbstractSet 类的 equals 方法的典型例子,它将检测两个集合是否有相同的元素。AbstractSet类有两个具体子类: TreeSet 和 HashSet, 它们分别使用不同的算法实现查找集合元素的操作。无论集合采用何种方式实现,都需要拥有对任意两个集合进行比较的功能
然而, 集合是相当特殊的一个例子, 应该将 AbstractSetequals 声明为 final , 这是因为没有任何一个子类需要重定义集合是否相等的语义(事实上,这个方法并没有被声明为final。这样做, 可以让子类选择更加有效的算法对集合进行是否相等的检测)
下面可以从两个截然不同的情况看一下这个问题:

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

在雇员和经理的例子中, 只要对应的域相等, 就认为两个对象相等。如果两个Manager对象所对应的姓名、 薪水和雇佣日期均相等, 而奖金不相等, 就认为它们是不相同的, 因此,可以使用 getClass 检测
但是,假设使用雇员的 ID 作为相等的检测标准,并且这个相等的概念适用于所有的子类, 就可以使用 instanceof 进行检测, 并应该将 Employee.equals 声明为 final

注 释: 在标准 Java 库中包含150多个 equals 方法的实现,包括使用instanceof 检测、 调用 getClass 检测、 捕获 ClassCastException 或者什么也不做

下面给出编写一个完美的 equals 方法的建议:

  1. 显式参数命名为 otherObject, 稍后需要将它转换成另一个叫做 other 的变量
  2. 检测 this 与 otherObject 是否引用同一个对象:if (this == otherObject) return true;【这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。】
  3. 检测 otherObject 是否为 null, 如 果 为 null, 返 回 false。这项检测是很必要的 if (otherObject = null) return false;
  4. 比较 this 与 otherObject 是否属于同一个类。如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测if (getClass() != otherObject.getCIass()) return false; 如果所有的子类都拥有统一的语义,就使用 instanceof 检测:if (!(otherObject instanceof ClassName)) return false;
  5. 将 otherObject 转换为相应的类类型变量:ClassName other = (ClassName) otherObject
  6. 现在开始对所有需要比较的域进行比较了。使用 =比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配, 就返回 true; 否 则 返 回 false。return field1 == other.field && Objects.equals(field2, other.field2) && ...
    如果在子类中重新定义 equals, 就要在其中包含调用 super.equals(other)
    提示: 对于数组类型的域, 可以使用静态的 Arrays.equals 方法检测相应的数组元素是否相等
    警告: 下面是实现 equals 方法的一种常见的错误。 可以找到其中的问题吗?
    public boolean equals(Employee otherObject) {
        return null != otherObject
                && getClass() == otherObject.getClass()
                && Objects.equals(name, otherObject.name)
                && salary == otherObject.salary
                && Objects.equals(hireDay, otherObject.hireDay);
    }

这个方法声明的显式参数类型是 Employee。其结果并没有覆盖 Object 类的 equals 方
法, 而是定义了一个完全无关的方法。

为了避免发生类型错误, 可以使用 @Override 对覆盖超类的方法进行标记:

@Override public boolean equals(Object other)

如果出现了错误, 并且正在定义一个新方法, 编译器就会给出错误报告。例如, 假设将下面的声明添加到 Employee 类中:

@Override public boolean equals(Employee other)

就会看到一个错误报告, 这是因为这个方法并没有覆盖超类 Object 中的任何方法。

java.util.Arrays 1.2

static Boolean equals(type[]a , type[] b) 5.0
如果两个数组长度相同, 并且在对应的位置上数据元素也均相同, 将返回 true。数组的兀素类型可以是 Objectintlongshortcharbytebooleanfloatdouble

java.util.Objects 7

static boolean equals(Object a, Object b)
如果 a 和 b 都为 null, 返回 true ; 如果只有其中之一为 null, 则返回 false ; 否则返回a.equals(b)

2.3 hashCode 方法

散列码( hash code ) 是由对象导出的一个整型值。散列码是没有规律的。如果 x 和 y是两个不同的对象, x.hashCode( ) 与 y.hashCode( ) 基本上不会相同。下表列出几个通过调用 String 类的 hashCode 方法得到的散列码。
在这里插入图片描述
String 类使用下列算法计算散列码:

    public static void main(String[] args) {
        int hash = 0;
        String str = "Hello";
        for (int i = 0; i < str.length(); i++){
            hash = 31 * hash + str.charAt(i);
        }
        System.out.println(str.hashCode());
        System.out.println(hash);
    }
69609650
69609650

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

        String s = "OK";
        StringBuilder sb = new StringBuilder(s);
        System.out.println(s.hashCode() + " " + sb.hashCode());
        String t = new String("OK");
        StringBuilder tb = new StringBuilder(t);
        System.out.println(t.hashCode() + " " + tb.hashCode());
2524 460141958
2524 1163157884

请注意, 字符串 s 与 t 拥有相同的散列码, 这是因为字符串的散列码是由内容导出的。而字符串缓冲 sb 与 tb却有着不同的散列码, 这是因为在 StringBuffer 类中没有定义hashCode 方法,它的散列码是由Object 类的默认 hashCode 方法导出的对象存储地址。如果重新定义 equals方法,就必须重新定义hashCode 方法, 以便用户可以将对象插人到散列表中

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

例如, 下面是 Employee 类的 hashCode 方法:

    public int hashCode() {
        return 7 * name.hashCode()
                + 11 * new Double(salary).hashCode()
                + 13 * hireDay.hashCode();
    }

不过,还可以做得更好。首先, 最好使用 null 安全的方法 Objects.hashCode。 如果其参数为 null,这个方法会返回 0, 否则返回对参数调用 hashCode 的结果。
另外,使用静态方法 Double.hashCode 来避免创建 Double 对象:

    public int hashCode() {
        return 7 * Objects.hashCode(name)
                + 11 * Double.hashCode(salary)
                + 13 * Objects.hashCode(hireDay);
    }

还有更好的做法,需要组合多个散列值时,可以调用 ObjeCtS.hash 并提供多个参数。这个方法会对各个参数调用 Objects.hashCode, 并组合这些散列值。这样Employee.hashCode 方法可以简单地写为:

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

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

提示: 如果存在数组类型的域, 那么可以使用静态的 Arrays.hashCode 方法计算一个散列码,这个散列码由数组元素的散列码组成

2.3.1 hashCode 常用API 汇总

java.util.Object 1.0

int hashCode( )
返回对象的散列码。散列码可以是任意的整数, 包括正数或负数。两个相等的对象要求返回相等的散列码

java.util.Objects 7

static int hash(Object . .. objects)
返回一个散列码,由提供的所有对象的散列码组合而得到

static int hashCode(Object a )
如果 a 为 null 返回 0, 否则返回 a.hashCode()

java.lang.(lnteger | Long | Short | Byte | Double | Float | Character | Boolean) 1.0

static int hashCode((int11ong|short|byte|double|f1oat|char|boolean) value) 8
返回给定值的散列码。

java.util.Arrays 1.2

static int hashCode(type[] a ) 5.0
计算数组 a 的散列码。组成这个数组的元素类型可以是 object,intlong, short, char, byte, boolean, floatdouble

2.4 toString 方法

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

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

实际上,还可以设计得更好一些。最好通过调用 getClaSS( ).getName( ) 获得类名的字符串,而不要将类名硬加到 toString方法中。

    public String toString() {
        return getClass().getName() +
                "name='" + name + '\'' +
                ", salary=" + salary +
                ", hireDay=" + hireDay +
                '}';
    }

toString方法也可以供子类调用

当然,设计子类的程序员也应该定义自己的 toString 方法,并将子类域的描述添加进去。如果超类使用了 getClass( ).getName( ), 那么子类只要调用 super.toString( )就可以了。例如,下面是 Manager 类中的 toString 方法:

    public String toString() {
        return super.toString() +
                "bonus=" + bonus +
                '}';
    }

在这里插入图片描述

现在,Manager 对象将打印输出如下所示的内容:

package equals;

/**
 * Created by: cxf
 * Description:
 * User: 19696
 * Date: 2021-08-25
 * Time: 13:39
 */
public class EqualsTest {
    public static void main(String[] args) {
        Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
        System.out.println("boss .toStringO:" + boss);
    }
}


boss .toStringO:equals.Managername='Carl Cracker', salary=80000.0, hireDay=1987-12-15}bonus=0.0}

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

        Manager boss = new Manager("Jerry", 15000, 2023, 6, 1);
        String message = "The current position is: " + boss;
        System.out.println(message);
       
The current position is: abstractClasses.Managername='Jerry', salary=15000.0, hireDay=2023-06-01}bonus=0.0}

提示: 在调用 x.toString( ) 的地方可以用 “”+x 替代。这条语句将一个空串与 x 的字符串表示相连接。这里的 x 就是 x.toString( )。 与 toString 不同的是,如果 x 是基本类型,这条语句照样能够执行
如果 x 是任意一个对象, 并调用

System.out.println(x);

println方法就会直接地调用 x.toString( ) 井打印输出得到的字符串

Object 类定义了 toString 方法, 用来打印输出对象所属的类名和散列码。例如, 调用:

System.out.println(System.out)
java.io.PrintStream@74a14482

之所以得到这样的结果是因为 PrintStream 类的设计者没有覆盖 toString方法。

警告: 令人烦恼的是, 数组继承了 object 类的 toString 方法,数组类型将按照旧的格式打印。
列如:

        int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
        String sa = ""+luckyNumbers;
        System.out.println(sa);
[I@1540e19d

生成字符串“ [I@la46e30”(前缀 [I 表明是一个整型数组)。修正的方式是调用静态方法 Arrays.toString。代码:

        int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
        String sa = Arrays.toString(luckyNumbers);
        System.out.println(sa);
[2, 3, 5, 7, 11, 13]

将生成字符串“ [2,3,5,7,11,13]”

要想打印多维数组(即, 数组的数组)则需要调用 Arrays.deepToString方法

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

提示: 强烈建议为自定义的每一个类增加 toString 方法。这样做不仅自己受益, 而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。

代码汇总:
equals/EqualsTest.java

package equals;

/**
 * Created by: cxf
 * Description:
 * User: 19696
 * Date: 2021-08-25
 * Time: 13:39
 */
public class EqualsTest {
    public static void main(String[] args) {
        Employee alicel = new Employee("Alice Adams ", 75000, 1987, 12, 15);
        Employee alice2 = alicel;
        Employee alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15);
        Employee bob = new Employee("Bob Brandson", 50000, 1989, 10, 1);
        System.out.println("alicel == alice2: " + (alicel == alice2));
        System.out.println("alicel == alice3: " + (alicel == alice3));
        System.out.println("alicel.equals(alice3): " + alicel.equals(alice3));
        System.out.println("alicel.equals(bob): " + alicel.equals(bob));
        System.out.println("bob.toString():" + bob);
        Manager carl = new Manager("Carl Cracker", 80000, 1987, 12, 15);
        Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
        boss.setBonus(5000);
        System.out.println("boss .toStringO:" + boss);
        System.out.println("carl .equals(boss): " + carl.equals(boss));
        System.out.println("alicel.hashCode(): " + alicel.hashCode());
        System.out.println("alice3 .hashCode(): " + alice3.hashCode());
        System.out.println("bob.hashCodeO: " + bob.hashCode());
        System.out.println("carl .hashCodeQ : " + carl.hashCode());
    }
}

equals/Employee.java

package equals;

import java.time.LocalDate;
import java.util.Objects;

/**
 * Created by: cxf
 * Description:
 * User: 19696
 * Date: 2021-08-25
 * Time: 13:39
 */
public class Employee {
    private String name;
    private double salary;
    private LocalDate hireDay;

    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        this.hireDay = LocalDate.of(year, month, day);
    }
    public String getName(){
        return name;
    }
    public double getSalary(){
        return salary;
    }
    public LocalDate getHireDay(){
        return hireDay;
    }
    public void raiseSalary(double byPercent){
        double raise = salary * byPercent/100;
        salary += raise;
    }
    public boolean equals(Object otherObject){
        if (this == otherObject){
            return true;
        }
        if (otherObject == null){
            return false;
        }
        if (getClass() != otherObject.getClass()){
            return false;
        }
        Employee other = (Employee) otherObject;
        return Objects.equals(name, ((Employee) otherObject).name)
                && salary == ((Employee) otherObject).salary
                && Objects.equals(hireDay, ((Employee) otherObject).hireDay);
    }
    public int hashCode(){
        return Objects.hash(name, salary, hireDay);
    }

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

equals/Manager.java

package equals;

/**
 * Created by: cxf
 * Description:
 * User: 19696
 * Date: 2021-08-25
 * Time: 13:46
 */
public class Manager extends Employee{
    private double bonus;

    public Manager(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);
        this.bonus = 0;
    }
    public void setBonus(double bonus){
        this.bonus = bonus;
    }
    public boolean equals(Object otherObject){
        if (!super.equals(otherObject)){
            return false;
        }
        Manager other = (Manager) otherObject;
        return bonus == ((Manager) otherObject).bonus;
    }
    public int hashCode(){
        return super.hashCode()+17*Double.hashCode(bonus);
    }

    @Override
    public String toString() {
        return super.toString() +
                "bonus=" + bonus +
                '}';
    }
}

2.4.1 toString 常用API 汇总

java.lang.Object 1.0

Class getClass( )
返回包含对象信息的类对象。稍后会看到 Java 提供了类运行时的描述, 它的内容被封
装在 Class 类中

boolean equals(Object otherObject )
比较两个对象是否相等, 如果两个对象指向同一块存储区域, 方法返回 true ; 否则方法返回false。在自定义的类中, 应该覆盖这个方法

String toString( )
返冋描述该对象值的字符串。在自定义的类中, 应该覆盖这个方法

java.lang.Class 1.0

String getName( )
返回这个类的名字。

Class getSuperclass( )Class 对象的形式返回这个类的超类信息。

3.泛型数组列表

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

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

    int actualSize = 5;
    Employee[] staff = new Employee[actualSize];

当然,这段代码并没有完全解决运行时动态更改数组的问题。一旦确定了数组的大小,改变它就不太容易了。在 Java 中, 解决这个问题最简单的方法是使用 Java 中另外一个被称为ArrayList 的类。它使用起来有点像数组,但在添加或删除元素时, 具有自动调节数组容量的功能,而不需要为此编写任何代码。
ArrayList 是一个采用类型参数( type parameter ) 的泛型类( generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面, 例如ArrayList <Employee>

下面声明和构造一个保存 Employee 对象的数组列表:

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

两边都使用类型参数 Employee, 这有些繁琐。Java SE 7中, 可以省去右边的类型参数:

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

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

注释: Java SE 5.0 以前的版本没有提供泛型类, 而是有一个 ArrayList 类, 其中保存类型为 Object 的元素, 它是“ 自适应大小” 的集合。如果一定要使用老版本的 Java, 则需要将所有的后缀 <. . .> 删掉,, 在 Java SE 5.0 以后的版本中, 没有后缀 <… .> 仍然可以使用ArrayList, 它将被认为是一个删去了类型参數的“ 原始” 类型、

使用 add 方法可以将元素添加到数组列表中。例如,下面展示了如何将雇员对象添加到
数组列表中的方法:

staff.add(new Employee("Harry Hacker", 15000, 2023, 6, 1));
staff.add(new Employee("Tony Tester", 25000, 2023, 6, 1));

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

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

staff.ensureCapacity(1OO);

这个方法调用将分配一个包含 100 个对象的内部数组。然后调用 100 次 add, 而不用重新分配空间。
另外,还可以把初始容量传递给 ArrayList 构造器:

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

警告: 分配数组列表, 如下所示:

new ArrayList<>(1OO) // capacity is 100

它与为新数组分配空间有所不同:

new Employee[100] // size is 100

数组列表的容量与数组的大小有一个非常重要的区别。如果为数组分配 100 个元素的存储空间,数组就有 100 个空位置可以使用。 而容量为 100 个元素的数组列表只是拥有保存 100 个元素的潜力 ( 实际上, 重新分配空间的话,将会超过 100 ), 但是在最初,甚至完成初始化构造之后,数组列表根本就不含有任何元素

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

staff.size()

将返回 staff 数组列表的当前元素数量, 它等价于数组 a 的 a.length

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

一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时, 再调用 trimToSize

C++ 注释: ArrayList 类似于 C++ 的 vector 模板。ArrayList 与 vector 都是泛型类型。 但是 C++ 的 vector 模板为了便于访问元素重栽了 [ ] 运算符。由于 Java 没有运算符重栽,所以必须调用显式的方法。此外,C++ 向量是值拷贝。如果 a 和 b 是两个向量, 賦值操作 a = b 将会构造一个与 b 长度相同的新向量 a, 并将所有的元素由 b 拷贝到 a, 而在Java 中, 这条赋值语句的操作结果是让 a 和 b 引用同一个数组列表

泛型数组 常用API 汇总

java.util.ArrayList<E> 1.2

 ArrayList<E>()
 构造一个空数组列表。
ArrayList<E>(int initialCapacity)
用指定容量构造一个空数组列表。
参数:initalCapacity 数组列表的最初容量
boolean add(E obj)
在数组列表的尾端添加一个元素。 永远返回 true。
参数:obj 添加的元素
int size( )
返回存储在数组列表中的当前元素数量。(这个值将小于或等于数组列表的容量。)
void ensureCapacity(int capacity)
确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素。
参数:capacity 需要的存储容量
void trimToSize( )
将数组列表的存储容量削减到当前尺寸。

3.1 访问数组列表元素

很遗憾, 天下没有免费的午餐。 数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。 其原因是 ArrayList 类并不是 Java 程序设计语言的一部分;它只是一个由某些人编写且被放在标准库中的一个实用类

使用 get 和 set 方法实现访问或改变数组元素的操作,而不使用人们喜爱的 [ ]语法格式
例如,要设置第 i 个元素,可以使用:

staff.set(i , harry):

它等价于对数组 a 的元素赋值(数组的下标从 0 开始):

a[i] = harry;

警告: 只有 i 小于或等于数组列表的大小时, 才能够调用 list.set(,i x。) 例如, 下面这段代码是错误的:

ArrayList<Employee> list = new ArrayList<>(100); // capacity 100,size 0
list.set(0, new Employee("Tony Tester", 25000, 2023, 6, 1)); // no element 0 yet
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

使用 add 方法为数组添加新元素, 而不要使用 set 方法, 它只能替换数组中已经存在的元素内容

使用下列格式获得数组列表的元素:

Employee e = staff.get(i);

等价于:

Employee e = a[i];

注释: 没有泛型类时,原始的 ArrayList 类提供的 get 方法别无选择只能返回 Object, 因此, get 方法的调用者必须对返回值进行类型转换:

Employee e = (Employee) staff.get(i);

原始的 ArrayList 存在一定的危险性。它的 add 和 set 方法允许接受任意类型的对象。
对于下面这个调用

staff.set(i , "Harry Hacker");

编译不会给出任何警告, 只有在检索对象并试图对它进行类型转换时, 才会发现有
问题。如果使用 ArrayList<Employee>, 编译器就会检测到这个错误。

下面这个技巧可以一举两得, 既可以灵活地扩展数组, 又可以方便地访问数组元素。首先,创建一个数组, 并添加所有的元素。

        ArrayList<E> list = new ArrayList<E>();
        while(...){
            E=...;
            list.add(E);
        }

执行完上述操作后,使用 toArray 方法将数组元素拷贝到一个数组中。


        E[] a = new E[list.size()];
        list.toArray(a);

除了在数组列表的尾部追加元素之外,还可以在数组列表的中间插入元素,使用带索引参数的 add 方法

		int n = staff.size()/2;
        staff.add(n, e);

为了插人一个新元素,位于 n之后的所有元素都要向后移动一个位置。如果插人新元素后, 数组列表的大小超过了容量, 数组列表就会被重新分配存储空间。

同样地,可以从数组列表中间删除一个元素

Employee e = staff.remove(n);

位于这个位置之后的所有元素都向前移动一个位置, 并且数组的大小减 1

对数组实施插人和删除元素的操作其效率比较低。对于小型数组来说,这一点不必担心。但如果数组存储的元素数比较多, 又经常需要在中间位置插入、删除元素, 就应该考虑使用链表了

注意下面的变化:

  • 不必指出数组的大小。
  • 使用 add 将任意多的元素尾加到数组中。
  • 使用 size() 替代 length 计算元素的数目。
  • 使用 a.get(i) 替代 a[i] 访问元素。

3.1.1 数组访问 常用API 汇总

java.util.ArrayList<T> 1.2

void set(int index,E obj)
设置数组列表指定位置的元素值, 这个操作将覆盖这个位置的原有内容。
参数: index 位置(必须介于 0 ~ size()-l 之间)
	   obj 新的值
E get(int index)
获得指定位置的元素值。
参数:index 获得的元素位置(必须介于 0 ~ size()-1 之间)
void add(int index,E obj)
向后移动元素,以便插入元素。
参数:index 插入位置(必须介于 0size()-1 之间)
	  obj 新兀素
E remove (int index)
删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回。
参数:index 被删除的元素位置(必须介于 0size()-1 之间)

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

在你自己的代码中, 你可能更愿意使用类型参数来增加安全性。这一目录,你会了解如何与没有使用类型参数的遗留代码交互操作

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

public class EmployeeDB {
    public void update(ArrayList list){}
    public ArrayList find(String query){return null;}
}

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

       EmployeeDB employeeDB = new EmployeeDB();
        ArrayList<Employee> staff = new ArrayList<>();
        employeeDB.update(staff);

也可以将 staff 对象传递给 update 方法

警告: 尽管编译器没有给出任何错误信息或警告, 但是这样调用并不太安全。在update方法中, 添加到数组列表中的元素可能不是 Employee 类型。在对这些元素进行检索时就会出现异常。 听起来似乎很吓人,但思考一下就会发现,这与在 Java 中增加泛型之前是一样的 ,, 虚拟机的完整性绝对没有受到威胁。在这种情形下, 既没有降低安全性,也没有受益于编译时的检查。

相反地,将一个原始 ArrayList 赋给一个类型化 ArrayList 会得到一个警告

ArrayList<Employee> result = employeeDB.find(query); // yields warning

使用类型转换并不能避免出现警告

ArrayList<Employee> result = (ArrayList<Employee>) employeeDB.find(query);// yields another warning

这样, 将会得到另外一个警告信息, 指出类型转换有误。

这就是 Java 中不尽如人意的参数化类型的限制所带来的结果。鉴于兼容性的考虑, 编译器在对类型转换进行检査之后, 如果没有发现违反规则的现象,就将所有的类型化数组列表转换成原始 ArrayList 对象。 在程序运行时,所有的数组列表都是一样的,即没有虚拟机中的类型参数。 因此, 类型转换( ArrayList) 和 ( ArrayList<Employee> ) 将执行相同的运行时检查。

在这种情形下,不必做什么D 只要在与遗留的代码进行交叉操作时,研究一下编泽器的警告性提示,并确保这些警告不会造成太严重的后果就行了。

一旦能确保不会造成严重的后果,可以用@SuppressWamings(“unchecked”) 标注来标记这个变量能够接受类型转换, 如下所示:

ArrayList<Employee> result = (ArrayList<Employee>) employeeDB.find("Herry");

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

有时, 需要将 int 这样的基本类型转换为对象。 所有的基本类型都有一个与之对应的类。例如,Integer 类对应基本类型 int。通常, 这些类称为包装器 ( wrapper ) 这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character 、Void和 Boolean (前6 个类派生于公共的超类 Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时, 对象包装器类还是 final , 因此不能定义它们的子类。

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

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

警告: 由于每个值分别包装在对象中, 所以 ArrayList<lnteger> 的效率远远低于 int[]数组。 因此, 应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要
幸运的是, 有一个很有用的特性, 从而更加便于添加 int 类型的元素到ArrayLisKlntegeP中。下面这个调用

list.add(3);

将自动地变换成

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

这种变换被称为自动装箱(autoboxing)
注释: 大家可能认为自动打包 ( autowrapping) 更加合适, 而“装箱 (boxing) ” 这个词源自于 C#

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

int n = list.get(i);

翻译成

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

甚至在算术表达式中也能够自动地装箱和拆箱。例如,可以将自增操作符应用于一个包装器引用:

Integer n = 3;
n++;

编译器将自动地插人一条对象拆箱的指令, 然后进行自增计算, 最后再将结果装箱

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

        Integer a = 1000;
        Integer b = 1000;
        System.out.println(a == b);
        System.out.println(a.equals(b));
        System.out.println(a instanceof Integer);
        
false
true
true

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

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

关于自动装箱还有几点需要说明。首先, 由于包装器类引用可以为 null, 所以自动装箱有可能会抛出一个 NullPointerException 异常

        Integer n = null;
        System.out.println(n * 2);
 Exception in thread "main" java.lang.NullPointerException       

另外, 如果在一个条件表达式中混合使用 Integer 和 Double 类型, Integer 值就会拆箱,提升为 double, 再装箱为 Double:

Integer n = 1;
Double x = 2.0;
System.out.println(true ? n : x); // Prints 1.0
1.0

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

使用数值对象包装器还有另外一个好处。Java 设计者发现,可以将某些基本方法放置在包装器中, 例如, 将一个数字字符串转换成数值

要想将字符串转换成整型, 可以使用下面这条语句:

int x = Integer.parseInt(s);

这里与 Integer 对象没有任何关系, parselnt 是一个静态方法。但 Integer 类是放置这个方法的一个好地方

API 注释说明了 Integer 类中包含的一些重要方法。其他数值类也实现了相应的方法

警告: 有些人认为包装器类可以用来实现修改数值参数的方法, 然而这是错误的。在由于 Java 方法都是值传递, 所以不可能编写一个下面这样的能够增加整型参数值的Java 方法

    public static void triple_i(int x) {
        x *= 3;
    }

    public static void triple_I(Integer x) {
        x *= 3;
    }

    public static void main(String[] args) {
        int a = 1;
        triple_i(a);
        System.out.println(a);
        a = 1;
        triple_I(a);
        System.out.println(a);
    }
1
1

问题是 Integer 对象是不可变的: 包含在包装器中的内容不会改变: 不能使用这些包装器类创建修改数值参数的方法。

如果想编写一个修改数值参数值的方法, 就需要使用在 org.omg.CORBA 包中定义的持有者( holder) 类型, 包括 IntHolder、BooleanHolder 等。每个持有者类型都包含’一个公有 (!)域值,通过它可以访问存储在其中的值

    public static void triple_IH(IntHolder x) {
        x.value *= 3;
    }

4.1 对象包装其余自动装箱 常用API汇总

java.lang.Integer 1.0

int intValue( )int 的形式返回 Integer 对象的值(在 Number 类中覆盖了 intValue方法)
static String toString(int i)
以一个新 String 对象的形式返回给定数值 i 的十进制表示
static String toString(int i ,int radix )
返回数值 i 的基于给定 radix 参数进制的表示
static int parseInt(String s)
static int parseInt(String s,int radix)
返回字符串 s 表示的整型数值, 给定字符串表示的是十进制的整数(第一种方法)或者是 radix参数进制的整数(第二种方法)
static Integer valueOf(String s)
Static Integer valueOf(String s, int radix)
返回用 s 表示的整型数值进行初始化后的一个新 Integer 对象, 给定字符串表示的是十进制的整数(第一种方法) 或者是 radix 参数进制的整数(第二种方法)

5.参数变量可变的方法

在 Java SE 5.0 以前的版本中, 每个 Java 方法都有固定数量的参数。 然而, 现在的版本提供了可以用可变的参数数量调用的方法 (有时称为 “变参” 方法)前面已经看到过这样的方法:printf 例如, 下面的方法调用:

System.out.printf("%d\n", n);

System.out.printf("%d %s\n", n, "widgets");

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

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

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

实际上, printf 方法接收两个参数, 一个是格式字符串, 另一个是 Object[ ] 数组, 其中保存着所有的参数(如果调用者提供的是整型数组或者其他基本类型的值, "自动装箱"功能将把它们转换成对象 )。 现在将扫描 fmt 字符串, 并将第 i 个格式说明符与 args[i] 的值匹配起来

换句话说 对于 printf 的实现者来说 Object… 参数类型与 Object[ ] 完全一样

编译器需要对 printf 的每次调用进行转换, 以便将参数绑定到数组上, 并在必要的时候进行自动装箱:

System.out.printf("%d %s", new Object[] { new Integer(1), "widgets" } );
1 widgets

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

    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 方法。
注释: 允许将一个数组传递给可变参数方法的最后一个参数。例如:

System.out.printf("%d %s", new Object[] { new Integer(1), "widgets" } );

因此,可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法, 而不会破坏任何已经存在的代码。 例如MessageFormat.format 在 Java SE 5.0 就采用了 这种方式。 甚至可以将 main 方法声明为下列形式:
public static void main(String… args)

6.枚举类

public enum Size{SMALL, MEDIUM, LARGE, EXTRA_LARGE};

实际上, 这个声明定义的类型是一个类, 它刚好有 4 个实例, 在此尽量不要构造新对象。

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

如果需要的话, 可以在枚举类型中添加一些构造器、 方法和域。 当然, 构造器只是在构造枚举常量的时候被调用。 下面是一个示例:
所有的枚举类型都是 Enum 类的子类。它们继承了这个类的许多方法。其中最有用的一 个是 toString, 这个方法能够返回枚举常量名。例如, Size.SMALL.toString( ) 将返回字符串 “SMALL”

System.out.println(Size.SMALL.toString());
SMALL

toString 的逆方法是静态方法 valueOf 例如, 语句:

        Size s = Enum.valueOf(Size.class, "SMALL");
        System.out.println(s);
SMALL

将 s 设置成 Size.SMALL。
每个枚举类型都有一个静态的 values 方法, 它将返回一个包含全部枚举值的数组。 例如, 如下调用

Size[] values = Size.values();

返回包含元素:Size.SMALL, Size.MEDIUM, Size.LARGE, Size.EXTRA_LARGE的数组

ordinal 方法返回 enum 声明中枚举常量的位置, 位置从 0 开始计数。 例如: Size.MEDIUM. ordinal() 返回 1

注释:: 如同 Class 类一样, 鉴于简化的考虑, Enum 类省略了一个类型参数。 例 如, 实 际上, 应该将枚举类型 Size}广展为Enum。 类型参数在 compareTo 方法中使用

枚举类 常用API汇总

java.lang.Enum<E> 5.0

static Enum valueOf(class EnumClass, String name)
返回指定名字、 给定类的枚举常量。
String toString()
返回枚举常量名
int ordinal()
返回枚举常量在 enum 神明中的位置,位置从 0 开始计数
int compareTo(E other)
如果枚举常量出现在 other 之前,返回一个负值
如果 this == other返回 0
否则返回一个正值

枚举常量的出现次序在 enum 声明中给出。

7.反射

反射库(reflection library) 提供了一个非常丰富且精心设计的工具集, 以便编写能够动态操纵 Java 代码的程序。这项功能被大量地应用于 JavaBeans 中, 它是 Java 组件的体系结构。 使用反射, Java 可以支持 Visual Basic 用户习惯 使用的工具。特别是在设计或运行中添加新类时, 能够快速地应用开发工具动态地查询新添加类的能力。

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

  • 在运行时分析类的能力。
  • 在运行时查看对象, 例如, 编写一个 toString 方法供所有类使用。 •实现通用的数组操作代码。
  • 利用 Method 对象, 这个对象很像中的函数指针。
  • 反射是一种功能强大且复杂的机制。 使用它的主要人员是工具构造者, 而不是应用程序员

7.1 class 类

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

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

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

如同用一个 Employee 对象表示一个特定的雇员属性一样, 一个 Class 对象将表示一个特定类的属性。最常用的 Class 方法是getName。这个方法将返回类的名字。例如, 下面这条 语句:

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

如果 e 是一个雇员, 则会打印输出:
Employee Harry Hacker
如果 e 是经理, 则会打印输出:
Manager Harry Hacker

如果类在一个包里, 包的名字也作为类名的一部分

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

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

如果类名保存在字符串中, 并可在运行中改变, 就可以使用这个方法。 当然, 这个方法 只有在className 是类名或接口名时才能够执行。否则, forName 方法将抛出一个 checked exception ( 已检查异常)。无论何时使用这个方法, 都应该提供一个异常处理器(exception handler)。

提示: 在启动时, 包含 main 方法的类被加载。 它会加载所有需要的类。 这些被加栽的类 又要加载它们需要的类, 以此类推。对于一个大型的应用程序来说, 这将会消耗很多时 间, 用户会因此感到不耐烦。 可以使用下面这个技巧给用户一种启动速度比较快的幻觉。 不过, 要确保包含 main 方法的类没有显式地引用其他的类。 首先, 显示一个启动画面; 然后, 通过调用 Class.forName 手工地加载其他的类。

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

      	Class c1 = Random.class;
        Class c2 = int.class;
        Class c3 = Double[].class;

请注意, 一个 Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类。 例如, int 不是类, 但 int.class 是一个 Class 类型的对象。

注释: Class类实际上是一个泛型类。例如,Employee.class的类型是Class<Employee>。 没有说明这个问题的原因是: 它将已经抽象的概念更加复杂化了。在大多数实际问题 中, 可以忽略类型参数, 而使用原始的 Class 类

警告: 鉴于历史原因getName 方法在应用于数组类型的时候会返回一个很奇怪的名字:

        System.out.println(Double[].class.getName());
        System.out.println(Double.class.getName());
        System.out.println(int[].class.getName());
        System.out.println(int.class.getName());
[Ljava.lang.Double;
java.lang.Double
[I
int

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

if (e.getClass() == Employee.getClass()){...}

还有一个很有用的方法 newlnstance( ), 可以用来动态地创建一个类的实例例如:

e.getClass().newInstance();

创建了一个与 e 具有相同类类型的实例。 newlnstance 方法调用默认的构造器 (没有参数的构造器)初始化新创建的对象。 如果这个类没有默认的构造器, 就会抛出一个异常

将 forName 与 newlnstance 配合起来使用, 可以根据存储在字符串中的类名创建一个对象

        String s = "java.util.Random";
        Object m = Class.forName(s).newInstance();
        System.out.println(m);
 java.util.Random@4554617c       

注释: 如果需要以这种方式向希望按名称创建的类的构造器提供参数, 就不要使用上面那条语句, 而必须使用 Constructor 类中的 newlnstance 方法

C++ 注释: newlnstance 方法对应 C++ 中虚拟构造器的习惯用法。 然而,C++ 中的虚拟构造器不是一种语言特性, 需要由专门的库支持。 Class 类与 C++ 中的 type_jnfo 类相似,getClass 方法与 C++ 中的 typeid 运算符等价。 但 Java 中的 Class比 C++ 中的type_info的功能强。C++ 中的 type_jnfo 只能以字符串的形式显示一个类型的名字, 而不能创建那个类型的对象

7.2 捕获异常

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

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

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

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

        try{
            String name = "Zhu";
            Class c1 = Class.forName(name);
            System.out.println("try");
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("catch");
        }
java.lang.ClassNotFoundException: Zhu
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:264)
	at reflection.ReflectionTest.main(ReflectionTest.java:14)
catch

Process finished with exit code 0

如果类名不存在, 则将跳过 try 块中的剩余代码,程序直接进人 catch 子句(这里,利用Throwable 类的 printStackTrace 方法打印出栈的轨迹。Throwable 是 Exception 类的超类)。如果 try块中没有抛出任何异常, 那么会跳过 catch 子句的处理器代码。

对于已检查异常,只需要提供一个异常处理器。 可以很容易地发现会抛出已检査异常的方法。如果调用了一个抛出已检查异常的方法,而又没有提供处理器,编译器就会给出错误报告

7.3.1 捕获异常 常用API汇总

java.lang.Class 1.0

static Class forName(String className)
返回描述类名为 className 的 Class 对象

Object newInstance()
返回这个类的一个新实例。

java.Iang.reflect.Constructor 1.1

Object newInstance(Object[] args)
构造一个这个构造器所属类的新实例
参数:args 这是提供给构造器的参数

java.Iang.Throwable 1.0

void printStackTrace()Throwable 对象和栈的轨迹输出到标准错误流

7.3 利用反射分析类的能力

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

在 java.lang.reflect 包中有三个类 Field、 Method 和 Constructor 分别用于描述类的域、 方法和构造器。 这三个类都有一个叫做 getName 的方法, 用来返回项目的名称。Field 类有一个 getType 方法, 用来返回描述域所属类型的 Class 对象。Method 和 Constructor 类有能够报告参数类型的方法,Method 类还有一个可以报告返回类型的方法。这三个类还有一个叫做 getModifiers 的方法, 它将返回一个整型数值,用不同的位开关描述 public 和 static这样的修饰符使用状况。另外, 还可以利用java.lang.reflect 包中的 Modifier类的静态方法分析getModifiers 返回的整型数值。例如, 可以使用Modifier 类中的 isPublic、isPrivate 或isFinal判断方法或构造器是否是 public、 private 或 final。 我们需要做的全部工作就是调用Modifier类的相应方法,并对返回的整型数值进行分析,另外,还可以利用 Modifier.toString方法将修饰符打印出来。

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

package reflection;

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

/**
 * Created by: cxf
 * Description:
 * User: 19696
 * Date: 2021-08-26
 * Time: 8:54
 */
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 (e.g. java.util.Date): ");
            name = in.next();
        }
        try {
            Class c1 = Class.forName(name);
            Class superc1 = c1.getSuperclass();
            String modifiers = Modifier.toString(c1.getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + " ");
            }
            System.out.print("class" + name);
            if (superc1 != null && superc1 != Object.class) {
                System.out.println("extends " + superc1.getName());
            }
            System.out.print("\n{\n");
            printConstructors(c1);
            System.out.println();
            printMethods(c1);
            System.out.println();
            printFields(c1);
            System.out.println("}");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.exit(0);
    }

    //打印类的所有域
    private static void printFields(Class c1) {
        Field[] fields = c1.getFields();
        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 + " ");
        }
    }

    //打印类的所有方法
    private static void printMethods(Class c1) {
        Method[] methods = c1.getDeclaredMethods();
        for (Method m : methods) {
            Class reType = m.getReturnType();
            String name = m.getName();
            System.out.print(" ");
            //打印 修饰词,返回类型和方法名称
            String modifiers = Modifier.toString(m.getModifiers());
            if (modifiers.length() > 0) {
                System.out.print(modifiers + " ");
            }
            System.out.print(reType.getName() + " " + name + "(");
            //打印 参数类型
            Class[] paramTypes = m.getParameterTypes();
            for (int i = 0; i < paramTypes.length; i++) {
                if (i > 0) {
                    System.out.print(", ");
                }
                System.out.print(paramTypes[i].getName());
            }
            System.out.println(");");
        }
    }

    //打印类的所有构造函器
    private static void printConstructors(Class c1) {
        Constructor[] constructors = c1.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 + "(");
            Class[] paramTypes = c.getParameterTypes();

            //打印 parameter 的类型
            for (int i = 0; i < paramTypes.length; i++) {
                if (i > 0) {
                    System.out.println(", ");
                }
                System.out.print(paramTypes[i].getName());
            }
            System.out.println(");");
        }
    }
}

Enter class name (e.g. java.util.Date): 
java.lang.Double
public final classjava.lang.Doubleextends 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 
}

Process finished with exit code 0

7.3.1 反射分析类的 常用API汇总

java.lang.Class 1.0

Field[] getFields() 1.1
Filed[] getDeclaredFie1ds() 1.1
返回一个包含 Field 对象的数组:getFields 方法将这些对象记录了这个类或其超类的公有域
							   getDeclaredField 方法将这些对象记录了这个类的全部域。如果类中没有域, 或者 Class 对象描述的是基本类型或数组类型, 这些方法将返回一个长度为 0 的数组

Method[] getMethods() 1.1
Method[] getDeclareMethods() 1.1
返回包含 Method 对象的数组:getMethods 将返回所有的公有方法, 包括从超类继承来的公有方法
							getDeclaredMethods 返回这个类或接口的全部方法, 但不包括由超类继承了的方法

Constructor[] getConstructors() 1.1
Constructor[] getDeclaredConstructors() 1.1
返回包含 Constructor 对象的数组: getConstructors包含了 Class 对象所描述的类的所有公有构造
器
								  getDeclaredConstructorsbaohanle所有构造器

java.lang.reflect.Field 1.1
java.lang.reflect.Method 1.1
java.lang.reflect.Constructor 1.1

Class getDeclaringClass( )
返冋一个用于描述类中定义的构造器、 方法或域的 Class 对象

Class[] getExceptionTypes ( ) (ConstructorMethod 类中)
返回一个用于描述方法抛出的异常类型的 Class 对象数组

int getModifiers( )
返回一个用于描述构造器、 方法或域的修饰符的整型数值。使用 Modifier 类中的这个方法可以分析这个返回值

String getName( )
返冋一个用于描述构造器、 方法或域名的字符串

Class[] getParameterTypes ( ) (ConstructorMethod 类中)
返回一个用于描述参数类型的 Class 对象数组

Class getReturnType( ) (Method 类中)
返回一个用于描述返回类型的 Class 对象

java.lang.reflect.Modifier 1.1

static String toString(int modifiers )
返回对应 modifiers 中位设置的修饰符的字符串表7K。
static boolean isAbstract(int modifiers )
static boolean isFinal (int modifiers )
static boolean islnterface(int modifiers )
static boolean isNative(int modifiers )
static boolean isPrivate(int modifiers )
static boolean isProtected(int modifiers )
static boolean isPublic(int modifiers )
static boolean isStatic(int modifiers )
static boolean isStrict(int modifiers )
static boolean isSynchronized(int modifiers )
static boolean isVolati1e(int modifiers )
这些方法将检测方法名中对应的修饰符在 modffiers 值中的位

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

从前面目录中, 已经知道如何查看任意对象的数据域名称和类型:

  • 获得对应的 Class 对象。
  • 通过 Class 对象调用 getDeclaredFields

现在将进一步查看数据域的实际内容。当然, 在编写程序时, 如果知道想要査看的域名和类型,查看指定的域是一件很容易的事情。而利用反射机制可以查看在编译时还不清楚的对象域。

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

    Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
    Class c1 = harry.getClass();
    //展示Employee的类对象
    Field f = c1.getDeclaredField("name");
    //Employee类的名称字段
    Object v = f.get(harry);

实际上,这段代码存在一个问题。由于 name 是一个私有域, 所以 get 方法将会抛出一个IllegalAccessException。只有利用 get 方法才能得到可访问域的值。除非拥有访问权限,否则Java 安全机制只允许査看任意对象有哪些域, 而不允许读取它们的值。

反射机制的默认行为受限于 Java 的访问控制。然而, 如果一个 Java 程序没有受到安全管理器的控制, 就可以覆盖访问控制。 为了达到这个目的, 需要调用 Field、Method或Constructor 对象的 setAccessible 方法。例如:

f.setAtcessible(true); // now OK to call f.get(harry);

setAccessible 方法是 AccessibleObject 类中的一个方法, 它是 Field、 Method 和Constructor类的公共超类。这个特性是为调试、 持久存储和相似机制提供的。本书稍后将利用它编写一个通用的 toString方法

get 方法还有一个需要解决的问题。name 域是一个 String, 因此把它作为 Object 返回没有什么问题。但是, 假定我们想要查看 salary 域。它属于 double 类型,而 Java中数值类型不是对象。要想解决这个问题, 可以使用 Field 类中的 getDouble 方法,也可以调用get方法,此时, 反射机制将会自动地将这个域值打包到相应的对象包装器中,这里将打包成Double

当然,可以获得就可以设置。 调用 f.set(obj,value) 可以将 obj 对象的 f 域设置成新值

ObjectAnalyzerTest.java 显示了如何编写一个可供任意类使用的通用 toString方法。 其中使用getDeclaredFileds 获得所有的数据域, 然后使用 setAccessible 将所有的域设置为可访问的。 对于每个域,获得了名字和值。递归调用toString方法, 将每个值转换成字符串

泛型 toString方法需要解释几个复杂的问题。循环引用将有可能导致无限递归。因此,ObjectAnalyzer 将记录已经被访问过的对象。 另外, 为了能够査看数组内部, 需要采用一种不同的方式

可以使用 toString 方法查看任意对象的内部信息:

        ArrayList<Integer> squares = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            squares.add(i * i);
        }
        System.out.println(new ObjectAnalyzer().toString(squares));

java.util.ArrayList[class java.lang.Object[]{java.lang.Integer[1][][], java.lang.Integer[4][][], java.lang.Integer[9][][], java.lang.Integer[16][][], java.lang.Integer[25][][], null, null, null, null, null}, 5][5][][]

还可以使用通用的 toString 方法实现 ft 己类中的 toString 方法, 如下所示:

    public String toString(){
        return new ObjectAnalyzer().toString(this);
    }

objectAnalyzer/ObjectAnalyzerTest.java

package objectAnalyzer;

import java.util.ArrayList;

/**
 * Created by: cxf
 * Description:
 * User: 19696
 * Date: 2021-08-26
 * Time: 11:38
 */
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));
    }
}

objectAnalyzer/ObjectAnalyzer.java

package objectAnalyzer;

import abstractClasses.Employee;

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

/**
 * Created by: cxf
 * Description:
 * User: 19696
 * Date: 2021-08-26
 * Time: 11:17
 */
public class ObjectAnalyzer {

    /*Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
    Class c1 = harry.getClass();
    //展示Employee的类对象
    Field f = c1.getDeclaredField("name");
    //Employee类的名称字段
    Object v = f.get(harry);*/

    private ArrayList<Object> visited = new ArrayList<>();

    public String toString(Object obj) {
        if (obj == null) {
            return "null";
        }
        if (visited.contains(obj)) {
            return "...";
        }
        visited.add(obj);
        Class c1 = obj.getClass();
        if (c1 == String.class) {
            return (String) obj;
        }
        if (c1.isArray()) {
            String r = c1.getComponentType() + "[]{";
            for (int i = 0; i < Array.getLength(obj); i++) {
                if (i > 0) {
                    r += ", ";
                }
                Object val = Array.get(obj, i);
                if (c1.getComponentType().isPrimitive()) {
                    r += val;
                } else {
                    r += toString(val);
                }
            }
            return r + "}";
        }
        String r = c1.getName();
        //检查此类和所有超类的字段
        do {
            r += "[";
            Field[] fields = c1.getDeclaredFields();
            AccessibleObject.setAccessible(fields, true);
            //获取所有fields域的名称和值
            for (Field f : fields) {
                if (!Modifier.isStatic(f.getModifiers())) {
                    if (!r.endsWith("[")) {
                        r += ", ";
                    }
                    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 += "]";
            c1 = c1.getSuperclass();
        }
        while (c1 != null);
        {
            return r;
        }
    }

    public String toString(){
        return new ObjectAnalyzer().toString(this);
    }
}

7.4.1 反射分析对象 常用API汇总

java.Iang.reflect.AccessibleObject 1.2

void setAccessible(boolean flag)
为反射对象设置可访问标志。flag 为 true 表明屏蔽 Java 语言的访问检查,使得对象的
私有属性也可以被査询和设置

boolean isAccessible( )
返回反射对象的可访问标志的值

static void setAccessible(AccessibleObject[] array,boolean flag)
是一种设置对象数组可访问标志的快捷方法

java.lang.Class 1.1

Field getField(String name )
Field[] getField()
返回指定名称的公有域, 或包含所有域的数组

Field getDeclaredField(String name )
Field[] getDeclaredFields( )
返回类中声明的给定名称的域, 或者包含声明的全部域的数组

java.Iang.reflect.Field 1.1

Object get(Object obj)
返回 obj 对象中用 Field 对象表示的域值

void set(Object obj ,Object newValue )
用一个新值设置 Obj 对象中 Field 对象表示的域

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

java.lang.reflect 包中的 Array 类允许动态地创建数组。例如, 将这个特性应用到 Array类中的 copyOf方法实现中, 应该记得这个方法可以用于扩展已经填满的数组

Employee() a = new Employee[100]:
a = Arrays.copyOf(a, 2*a.length);

如何编写这样一个通用的方法呢? 正好能够将 Employee[ ] 数组转换为 Object[ ] 数组,这让人感觉很有希望。下面进行第一次尝试

    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;
    }

然而, 在实际使用结果数组时会遇到一个问题。这段代码返回的数组类型是对象数组(Object[]) 类型,这是由于使用下面这行代码创建的数组:

 Object[] newArray = new Object[newLength];

一个对象数组不能转换成雇员数组( Employee[ ])。如果这样做, 则在运行时 Java 将会产生 ClassCastException 异常。前面已经看到,Java 数组会记住每个元素的类型,即创建数组时 new 表达式中使用的元素类型。将一个 Employee[ ]临时地转换成Object[]数组,然后再把它转换回来是可以的,但一 从开始就是 Objectt ] 的数组却永远不能转换成Employe ]数组。为了编写这类通用的数组代码, 需要能够创建与原数组类型相同的新数组。为此, 需要java,lang.reflect 包中 Array 类的一些方法。其中最关键的是Array类中的静态方法newlnstance,它能够构造新数组。在调用它时必须提供两个参数,一个是数组的元素类型,一个是数组的长度

Object[] newArray = Array.newInstance(componentType, newLength);

为了能够实际地运行,需要获得新数组的长度和元素类型

可以通过调用 Array.getLength(a) 获得数组的长度, 也可以通过 Array 类的静态getLength方法的返回值得到任意数组的长度。而要获得新数组元素类型,就需要进行以下工作:

  1. 首先获得 a 数组的类对象
  2. 确认它是一个数组
  3. 使用 Class 类(只能定义表示数组的类对象)的 getComponentType 方法确定数组对应的类型

为什么 getLength 是 Array 的方法,而 getComponentType 是 Class 的方法呢? 我们也不清楚。反射方法的分类有时确实显得有点古怪。下面是这段代码:

    public static Object goodCopyOf(Object a, int newLength) {
        Class c1 = a.getClass();
        if (!c1.isArray()) {
            return null;
        }
        Class componentType = c1.getComponentType();
        int length = Array.getLength(a);
        Object newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
        return newArray;
    }

请注意,这个 CopyOf 方法可以用来扩展任意类型的数组, 而不仅是对象数组

int[] a = { 12, 3, 4, 5 };
a = (int[]) goodCopyOf(a, 10);

为了能够实现上述操作,应该将 goodCopyOf 的参数声明为 Object 类型,.而不要声明为对象型数组(Object[])。整型数组类型 int[] 可以被转换成 Object,但不能转换成对象数组

显示了两个扩展数组的方法。请注意, 将 badCopyOf 的返回值进行类型转换将会抛出一个异常

public class CopyOfTest {
    public static Object[] badCopyOf(Object[] a, int newLength) {//not useful
        Object[] newArray = new Object[newLength];
//        Object[] newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
        return newArray;
    }

    public static Object goodCopyOf(Object a, int newLength) {
        Class c1 = a.getClass();
        if (!c1.isArray()) {
            return null;
        }
        Class componentType = c1.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.5.1 反射编写泛型数组代码 常用API汇总

java.lang.reflect.Array 1.1

static Object get(Object array,int index)
static xxx getXxx(Object array,int index)
( xxx 是 booleanbytechardoublefloatintlongshort 之中的一种基本类型)这些方法将返回存储在给定位置上的给定数组的内容

static void set(Object array,int index,Object newValue)
static setXxx(Object array,int index,xxx newValue)
( xxx 是 booleanbytechardoublefloatintlongshort 之中的一种基本类型)这些方法将一个新值存储到给定位置上的给定数组中

static int getLength(Object array)
返回数组的长度

static Object newInstance(Class componentType,int length)
static Object newInstance(Class componentType,int[]lengths)
返回一个具有给定类型、 给定维数的新数组

7.6 调用任意方法

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

为了能够看到方法指针的工作过程, 先回忆一下利用 Field 类的 get 方法查看对象域的过程。与之类似, 在 Method 类中有一个 invoke 方法, 它允许调用包装在当前Method对象中的方法。invoke 方法的签名是:

public Object invoke(Object obj, Object... args)

第一个参数是隐式参数, 其余的对象提供了显式参数(在 Java SE 5.0 以前的版本中,必须传递一个对象数组, 如果没有显式参数就传递一个 null )

对于静态方法,第一个参数可以被忽略, 即可以将它设置为 null

例如, 假设用 m1 代表 Employee 类的 getName 方法,下面这条语句显示了如何调用这个方法:

String n = (String) m1.invoke(harry);

如果返回类型是基本类型, invoke 方法会返回其包装器类型。 例如, 假设 m2 表示Employee 类的 getSalary 方法, 那么返回的对象实际上是一个 Double, 必须相应地完成类型转换。可以使用自动拆箱将它转换为一个 double:

double s = (Double) m2.invoke(harry);

如何得到 Method 对象呢? 当然, 可以通过调用 getDeclareMethods 方法, 然后对返回的 Method 对象数组进行查找, 直到发现想要的方法为止。 也可以通过调用 Class类中的getMethod方法得到想要的方法。它与 getField 方法类似。getField 方法根据表示域名的字符串,返回一个 Field 对象。然而, 有可能存在若干个相同名字的方法,因此要格外小心,以确保能够准确地得到想要的那个方法。有鉴于此,还必须提供想要的方法的参数类型。

getMethod 的签名是:

 public Method getMethod(String name, Class<?>... parameterTypes)

例如, 下面说明了如何获得 Employee 类的 getName 方法和 raiseSalary 方法的方法指针

Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);

到此为止,已经学习了使用 Method 对象的规则。 下面看一下如何将它们组织在一起。MethodTable.java是一个打印诸如 Math.sqrt、 Math.sin 这样的数学函数值表的程序。打印的结果如下所示:

package methods;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Created by: cxf
 * Description:
 * User: 19696
 * Date: 2021-08-26
 * Time: 12:47
 */
public class MethodTableTest {
    private static void printTable(double from, double to, int n, Method f) throws InvocationTargetException, IllegalAccessException {
        ///将该方法作为表头打印出来
        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("X10.4f | %10.4f%n", x, y);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static double square(double x) {
        return x * x;
    }

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        //获取指向Square和sqrt方法的方法指针
        Method square = MethodTableTest.class.getMethod("square", double.class);
        Method sqrt = Math.class.getMethod("sqrt", double.class);
        //打印x值和y值的表格
        printTable(1, 10, 10, square);
        printTable(1, 10, 10, sqrt);
    }
}

public static double methods.MethodTableTest.square(double)
X10.4f |     1.0000
X10.4f |     2.0000
X10.4f |     3.0000
X10.4f |     4.0000
X10.4f |     5.0000
X10.4f |     6.0000
X10.4f |     7.0000
X10.4f |     8.0000
X10.4f |     9.0000
X10.4f |    10.0000
public static double java.lang.Math.sqrt(double)
X10.4f |     1.0000
X10.4f |     2.0000
X10.4f |     3.0000
X10.4f |     4.0000
X10.4f |     5.0000
X10.4f |     6.0000
X10.4f |     7.0000
X10.4f |     8.0000
X10.4f |     9.0000
X10.4f |    10.0000

Process finished with exit code 0

当然,这段打印数学函数表格的代码与具体打印的数学函数无关

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("X10.4f | %10.4f%n", x, y);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }

在这里,f 是一个 Method 类型的对象。由于正在调用的方法是一个静态方法, 所以nvoke 的第一个参数是 null

为了将 Math.sqrt 函数表格化, 需要将 f 设置为:

Math.class.getMethod("sqrt", double.class)

这是 Math 类中的一个方法, 通过参数向它提供了一个函数名 sqrt 和一个 double 类型的参数

8.继承的设计技巧

  1. 将公共操作和域放在超类
    这就是为什么将姓名域放在 Person类中,而没有将它放在 Employee 和 Student 类中的原因
  2. 不要使用受保护的域
    有些程序员认为,将大多数的实例域定义为 protected 是一个不错的主意,只有这样,子类才能够在需要的时候直接访问它们。然而, protected 机制并不能够带来更好的护,其原因主要有两点:
    第一,子类集合是无限制的, 任何一个人都能够由某个类派生一个子类,并编写代码以直接访问 protected 的实例域, 从而破坏了封装性。
    第二, 在 Java 程序设计语言中,在同一个包中的所有类都可以访问 proteced 域,而不管它是否为这个类的子类。

不过,protected 方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用

  1. 使用继承实现“ is-a” 关系
    使用继承很容易达到节省代码的目的,但有时候也被人们滥用了。例如, 假设需要定义一个钟点工类。钟点工的信息包含姓名和雇佣日期,但是没有薪水。他们按小时计薪,并且不会因为拖延时间而获得加薪- 这似乎在诱导人们由 Employee 派生出子类Contractor,然后再增加一个 hourlyWage 域。
public class Contractor extends Employee
{
private double hourlyWage;
}

这并不是一个好主意。因为这样一来, 每个钟点工对象中都包含了薪水和计时工资这两个域。在实现打印支票或税单方法时候, 会带来无尽的麻烦, 并且与不采用继承,会多写很代码。
钟点工与雇员之间不属于“ is-a” 关系。钟点工不是特殊的雇员

  1. 除非所有继承的方法都有意义,否则不要使用继承
    假设想编写一个 Holiday 类3 毫无疑问,每个假日也是一日,并且一日可以用GregorianCalendar 类的实例表示,因此可以使用继承
class Holiday extends GregorianCalendar { . . , }

很遗憾, 在继承的操作中, 假日集不是封闭的。 在 GregorianCalendar 中有一个公有方法add, 可以将假日转换成非假日:

Holiday Christmas;
Christmas.add(Calendar.DAY_OF_MONTH , 12);

因此,继承对于这个例子来说并不太适宜。

需要指出, 如果扩展 LocalDate 就不会出现这个问题。由于这个类是不可变的,所以没有任何方法会把假日变成非假日

  1. 在覆盖方法时,不要改变预期的行为
    置换原则不仅应用于语法, 而且也可以应用于行为,这似乎更加重要。在覆盖一个方法的时候,不应该毫无原由地改变行为的内涵。就这一点而言,编译器不会提供任何帮助,即编译器不会检查重新定义的方法是否有意义。例如,可以重定义 Holiday 类中add方法“修正” 原方法的问题,或什么也不做, 或抛出一个异常, 或继续到下一个假日。

然而这些都违反了置换原则。语句序列

int d1 = x.get(Calendar.DAY_OF_MONTH);
x.add(Calendar.DAY_OF_MONTH , 1);
int d2 = x.get(Calendar.DAY_OF_MONTH);
System.out.println(d2 - d1);

不管 x 属于 GregorianCalendar 类, 还是属于 Holiday 类,执行上述语句后都应该得到预期的行为。当然, 这样可能会引起某些争议。人们可能就预期行为的含义争论不休。例如, 有些人争论说, 置换原则要求 Manager.equals 不处理 bonus 域,因为Employee.equals 没有它。实际上,凭空讨论这些问题毫无意义。关键在于, 在覆盖子类中的方法时,不要偏离最初的设计想法。

  1. 使用多态, 而非类型信息
    无论什么时候,对于下面这种形式的代码
if (x is oftype1)
action(x);
else if (x is oftype2)
action(x);

都应该考虑使用多态性
action, 与 3如0112 表示的是相同的概念吗? 如果是相同的概念,就应该为这个概念定义一个方法, 并将其放置在两个类的超类或接口中,然后, 就可以调用

X.action();

以便使用多态性提供的动态分派机制执行相应的动作。

使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展

  1. 不要过多地使用反射
    反射机制使得人们可以通过在运行时查看域和方法, 让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误, 因此只有在运行时才发现错误并导致异常。现在你已经了解了 Java 支持面向对象编程的基础内容:类、继承和多态。接口和lambda 表达式, 它们对于有效地使用 Java 非常重要
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值