20220424 Java核心技术 卷1 基础知识 4-5

对 象 与 类

面向对象程序设计概述

面向对象的程序是由对象组成的, 每个对象包含对用户公开的特定功能部分和隐藏的实现部分。

传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。一旦确定了这些过程, 就要开始考虑存储数据的方式。这就是 Pascal 语言的设计者 Niklaus Wirth 将其著作命名为《算法 + 数据结构 = 程序》 ( Algorithms + Data Structures = Programs, Prentice Hall, 1975 ) 的原因。

需要注意的是,在 Wirth 命名的书名中, 算法是第一位的, 数据结构是第二位的,这就明确地表述了程序员的 X作方式。首先要确定如何操作数据, 然后再决定如何组织数据, 以便于数据操作。 而 OOP 却调换了这个次序, 将数据放在第一位,然后再考虑操作数据的算法。

对于一些规模较小的问题, 将其分解为过程的开发方式比较理想。而面向对象更加适用于解决规模较大的问题。

类( class )是构造对象的模板或蓝图

由类构造 (construct)对象的过程称为创建类的实例 ( instance )

封装 ( encapsulation , 有时称为 数据隐藏 )是与对象有关的一个重要概念。 从形式上看,封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。对象中的数据称为 实例域( instance field ), 操纵数据的过程称为 方法( method )。 对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前 状态 ( state )。无论何时, 只要向对象发送一个消息,它的状态就有可能发生改变。

实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。 程序仅通过对象的方法与对象数据进行交互。

封装给对象赋予了“ 黑盒 ” 特征, 这是提高重用性和可靠性的关键。 这意味着一个类可以全面地改变存储数据的方式,只要仍旧使用同样的方法操作数据, 其他对象就不会知道或介意所发生的变化。

OOP 的另一个原则会让用户自定义 Java 类变得轻而易举, 这就是: 可以通过扩展一个类来建立另外一个新的类。事实上, 在 Java 中, 所有的类都源自于一个“ 神通广大的超类”,它就是 Object

在扩展一个已有的类时, 这个扩展后的新类具有所扩展的类的全部属性和方法。在新类中, 只需提供适用于这个新类的新方法和数据域就可以了。通过扩展一个类来建立另外一个类的过程称为 继承(inheritance) 。

对 象

对象的三个主要特性:

  • 对象的行为(behavior) :可以对对象施加哪些操作,或可以对对象施加哪些方法?
  • 对象的状态 (state) :当施加那些方法时,对象如何响应?
  • 对象标识(identity ) :如何辨别具有相同行为与状态的不同对象?

同一个类的所有对象实例, 由于支持相同的 行为 而具有家族式的相似性。对象的行为是用可调用的方法定义的。

此外,每个对象都保存着描述当前特征的信息。这就是对象的 状态 。对象的状态可能会随着时间而发生改变,但这种改变不会是自发的。 对象状态的改变必须通过调用方法实现(如果不经过方法调用就可以改变对象状态, 只能说明封装性遭到了破坏)。

对象的状态并不能完全描述一个对象。每个对象都有一个唯一的 标识( identity ) 。例如,在一个订单处理系统中, 任何两个订单都存在着不同之处,即使所订购的货物完全相同也是如此。需要注意,作为一个类的实例, 每个对象的标识永远是不同的, 状态常常也存在着差异 。

对象的这些关键特性在彼此之间相互影响着。 例如, 对象的状态影响它的行为(如果一个订单“ 已送货” 或“ 已付款”, 就应该拒绝调用具有增删订单中条目的方法。反过来, 如果订单是“ 空的”,即还没有加人预订的物品,这个订单就不应该进人“ 已送货” 状态) 。

识 别 类

传统的过程化程序设计, 必须从顶部的 main 函数开始编写程序。在面向对象程序设计时没有所谓的“ 顶部”。对于学习OOP 的初学者来说常常会感觉无从下手。答案是:首先从设计类开始,然后再往每个类中添加方法。

识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。

当然, 所谓“ 找名词与动词” 原则只是一种经验, 在创建类的时候, 哪些名词和动词是重要的完全取决于个人的开发经验。

类之间的关系

在类之间, 最常见的关系有

  • 依赖 ( uses-a
  • 聚合( has-a
  • 继承( is-a

依赖(dependence ) ,即 “ uses-a ” 关系, 是一种最明显的、 最常见的关系。 如果一个类的方法操纵另一个
类的对象,我们就说一个类依赖于另一个类。

应该尽可能地将相互依赖的类减至最少。用软件工程的术语来说, 就是让类之间的耦合度最小。

聚合(aggregation ) ,即 “ has-a ” 关系, 是一种具体且易于理解的关系。聚合关系意味着类 A 的对象包含类 B 的对象。

表达类关系的 UML ( Unified Modeling Language, 统一建模语言 )符号 :

img

使用预定义类

并不是所有的类都具有面向对象特征。 例如,Math 类。

对象与对象变量

要想使用对象,就必须首先构造对象, 并指定其初始状态。然后,对对象应用方法。

在 Java 程序设计语言中, 使用 构造器(constructor) 构造新实例。 构造器是一种特殊的方法, 用来构造并初始化对象。

Java 的日期类库有些混乱, 已经重新设计了两次

一定要认识到: 一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。

在 Java 中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new 操作符的返回值也是一个引用。

局部变量不会自动地初始化为 null,而必须通过调用 new 或将它们设置为 null 进行初始化。

String s1, s2 = null;
// System.out.println(s1.toString());  // 编译时报错:Variable 's1' might not have been initialized
System.out.println(s2.toString());  // 运行时异常:Exception in thread "main" java.lang.NullPointerException

Java 类库中的 LocalDate

类库设计者决定将保存时间与给时间点命名分开。所以标准 Java 类库分别包含了两个类:一个是用来表示时间点的 Date 类;另一个是用来表示大家熟悉的日历表示法的 LocalDate 类。Java SE 8 引入了另外一些类来处理日期和时间的不同方面

不要使用构造器来构造 LocalDate 类的对象。实际上,应当使用静态工厂方法 (factory method) 代表你调用构造器。

类库设计者意识到应当单独提供类来处理日历, 不过在此之前这些方法已经是 Date 类的一部分了。Java 1.1 中引入较早的一组日历类时,Date 方法被标为废弃不用。 虽然仍然可以在程序中使用这些方法, 不过如果这样做, 编译时会出现警告。 最好还是不要使用这些废弃不用的方法, 因为将来的某个类库版本很有可能将它们完全删除。

LocalDate now = LocalDate.now();
System.out.println(LocalDate.of(2021, 10, 26)); // 2021-10-26


Date date = new Date(2021,10,26);   // 注意:2021-11-26
System.out.println(date);   // Sat Nov 26 00:00:00 CST 3921

更改器方法与访问器方法

Java 库的一个较早版本曾经有另一个类来处理日历, 名为 GregorianCalendar

LocalDate.plusDays 方法不同,GregorianCalendar.add 方法是一个 更改器方法 ( mutator method )

只访问对象而不修改对象的方法有时称为 访问器方法(accessor method),例如,LocalDate.getYearGregorianCalendar.get 就是访问器方法

显示当前月的日历:

LocalDate localDate = LocalDate.now();
int month = localDate.getMonthValue();
int today = localDate.getDayOfMonth();

localDate = localDate.minusDays(today - 1);   // Set to start of month
DayOfWeek weekday = localDate.getDayOfWeek();
int value = weekday.getValue();     // 1 = Monday, ... 7 = Sunday

// 打印头信息
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
// 打印第一周的空格
for (int i = 1; i < value; i++) {
    System.out.print("    ");
}
while (localDate.getMonthValue() == month) {
    System.out.printf("%3d", localDate.getDayOfMonth());
    if (localDate.getDayOfMonth() == today) {
        System.out.print("*");
    } else {
        System.out.print(" ");
    }
    localDate = localDate.plusDays(1);
    if (localDate.getDayOfWeek() == DayOfWeek.MONDAY) {
        System.out.println();
    }
}
if (localDate.getDayOfWeek() != DayOfWeek.MONDAY) {
    System.out.println();
}

用户自定义类

隐式参数与显式参数

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

raiseSalary 方法有两个参数。 第一个参数称为 隐式 (implicit ) 参数, 是出现在方法名前的 Employee 类对象。 第二个参数位于方法名后面括号中的数值, 这是一个 显式 (explicit ) 参数 (有些人把隐式参数称为方法调用的目标或接收者。)

在每一个方法中, 关键字 this 表示隐式参数。

封装的优点

警告: 注意不要编写返回引用可变对象的访问器方法。 例如,返回一个 Date 类对象。

如果需要返回一个可变对象的引用, 应该首先对它进行克隆(clone)。 对象 clone 是指存放在另一个位置上的对象副本。

基于类的访问权限

方法可以访问所调用对象的私有数据。

public boolean equals(Employee other) {
    return name.equals(other.name);
}

调用方式:

if (harry.equals(boss)) {
    // ...
}

final 实例域

可以将实例域定义为 final。 构建对象时必须初始化这样的域。也就是说, 必须确保在每一个构造器执行之后, 这个域的值被设置, 并且在后面的操作中, 不能够再对它进行修改。

final 修饰符大都应用于基本 (primitive) 类型域,或不可变(immutable) 类的域(如果类中的每个方法都不会改变其对象, 这种类就是不可变的类。

对于可变的类, 使用 final 修饰符可能会对读者造成混乱。例如,

private final StringBuilder evaluations;  

final 关键字只是表示存储在 evaluations 变量中的对象引用不会再指示其他 StringBuilder 对象。 不过这个对象的内容仍然可以更改。

静态域与静态方法

main 方法都被标记为 static 修饰符

静态域

如果将域定义为 static , 每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。

静态域属于类,不属于任何独立的对象。

静态域又被称为类域。

静态常量

Math 类中定义了一个静态常量:

public static final double PI = 3.14159265358979323846;

在程序中,可以采用 Math.PI 的形式获得这个常量

另一个多次使用的静态常量是 System.out

public final static PrintStream out = null;

这里赋值为 null 也是已赋值状态。

如果查看一下 System 类, 就会发现有一个 setOut 方法, 它可以将 System.out 设置为不同的流。 读者可能会感到奇怪, 为什么这个方法可以修改 final 变量的值。 原因在于, setOut 方法是一个本地方法, 而不是用 Java 语言实现的。 本地方法可以绕过 Java 语言的存取控制机制。 这是一种特殊的方法, 在自己编写程序时, 不应该这样处理。

静态方法

静态方法在运算时,没有隐式的参数(this)。

静态方法不能实例域,可以访问静态域

可以通过类名或对象调用静态方法。不过,通过对象调用静态方法很容易造成混淆, 其原因是静态方法计算的结果与对象毫无关系。 我们建议使用类名, 而不是对象来调用静态方法。

在下面两种下使用静态方法:

  • 方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如: Math.pow )
  • 方法只需要访问类的静态域(例如: Math.toRadians )

工厂方法

静态方法还有另外一种常见的用途。类似 LocalDateNumberFormat 的类使用静态工厂方法(factory method) 来构造对象。

为什么 NumberFormat 类不利用构造器完成这些操作呢? 这主要有两个原因:

  • 无法命名构造器。构造器的名字必须与类名相同。但是, 这里希望将得到的货币实例和百分比实例采用不用的名字
  • 当使用构造器时,无法改变所构造的对象类型。而 Factory 方法将返回一个 DecimalFormat 类对象, 这是 NumberFormat 的子类

main 方法

main 方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的 main 方法将执行并创建程序所需要的对象。

每一个类可以有一个 main 方法。这是一个常用于对类进行单元测试的技巧。

方法参数

按值调用 (call by value) 表示方法接收的是调用者提供的值。按引用调用 ( call by reference) 表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值, 而不能修改传递值调用所对应的变量值。

Java 程序设计语言总是采用按值调用。 方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。

方法参数共有两种类型:

  • 基本数据类型(数字、布尔值)
  • 对象引用

Java 对两种方法参数类型的处理都是相同的,都是传递参数值拷贝,基本数据类型的拷贝在方法中被改变不会影响到入参,对象引用的拷贝实际上是指向相同对象的引用,所以如果将引用的拷贝重新指向其他对象,不影响入参引用,如果是使用拷贝引用修改指向对象的状态,因为指向的对象是同一个,所以感觉是入参引用被改变了。

对 象 构 造

重载

如果多个方法(比如, StringBuilder 构造器方法)有相同的名字、 不同的参数,便产生了 重载

编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好。(这个过程被称为 重载解析(overloading resolution)。)

Java 允许重载任何方法, 而不只是构造器方法。因此,要完整地描述一个方法,需要指出 方法名 以及 参数类型 。这叫做 方法的签名(signature)。 例如, String 类有 4 个称为 indexOf 的公有方法。

indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)

返回类型不是方法签名的一部分。 也就是说, 不能有两个名字相同、 参数类型也相同却返回不同类型值的方法。

默认域初始化

如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值: 数值为 0、布尔值为 false、 对象引用为 null。但是,这并不是一种良好的编程习惯。

这是域与局部变量的主要不同点。 必须明确地初始化方法中的局部变量。 但是,如果没有初始化类中的域, 将会被自动初始化为默认值(0、 false 或 null )。

无参数的构造器

请记住, 仅当类没有提供任何构造器的时候, 系统才会提供一个默认的构造器。

显式域初始化

可以在类定义中, 直接将一个值赋给任何域。

public class Parent {
    private int i = 1;
    // ...
}

在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用。

初始值不一定是常量值, 可以调用方法对域进行初始化。

初始化块

三种初始化数据域的方法:

  • 在构造器中设置值
  • 在声明中赋值
  • 初始化块

初始化块分为非静态和静态两种,都在构造器之前执行。

public class Parent {
    private int i1 = 11;
    private static int i2 = 21;

    {
        i1 = 12;
    }

    static {
        i2 = 22;
    }
}

初始化块可以定义在域声明前,初始化结果取决于定义的顺序,例如上面的结果为:i1 = 12、i2 = 22,下面的结果为:i1 = 11、i2 = 21

public class Parent {
    
    {
        i1 = 12;
    }

    static {
        i2 = 22;
    }
    
    private int i1 = 11;
    private static int i2 = 21;

}

初始化块定义在域声明前,只能赋值,不能使用,否则会报编译错误

{
    // System.out.println(i1);     // 编译错误:Illegal forward reference
    i1 = 12;
}

private int i1 = 11;

对象析构与 finalize 方法

由于 Java 有自动的垃圾回收器,不需要人工回收内存, 所以 Java 不支持析构器。

可以为任何一个类添加 finalize 方法。finalize 方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用 finalize 方法回收任何短缺的资源, 这是因为很难知道这个方法什么时候才能够调用。

有个名为 System.runFinalizersOnExit(true) 的方法能够确保 finalizer 方法在 Java 关闭前被调用。不过, 这个方法并不安全,也不鼓励大家使用。有一种代替的方法是使用方法 Runtime.addShutdownHook 添加“ 关闭钩” (shutdown hook)

Java 允许使用包( package ) 将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。

使用包的主要原因是确保类名的唯一性。为了保证包名的绝对唯一性, Sun 公司建议将公司的因特网域名(这显然是独一无二的)以逆序的形式作为包名,并且对于不同的项目使用不同的子包。

从编译器的角度来看, 嵌套的包之间没有任何关系。 例如,java.util 包与 java.util.jar 包毫无关系。每一个都拥有独立的类集合。

类的导入

可以使用 import 语句导入一个特定的类或者整个包。

需要注意的是, 只能使用星号(*) 导入一个包, 而不能使用 import java.*import java.*.* 导入以 java 为前缀的所有包。

如果一个类中需要使用两个 Date 类, 又该怎么办呢? 答案是,在每个类名的前面加上完整的包名。

在包中定位类是编译器 ( compiler ) 的工作。类文件中的字节码肯定使用完整的包名来引用其他类。

静态导入

import 语句不仅可以导入类,还增加了导入静态方法和静态域的功能。

import static java.lang.System.out;

out.println();

将类放入包中

如果没有在源文件中放置 package 语句, 这个源文件中的类就被放置在一个默认包 ( defaulf package ) 中。默认包是一个没有名字的包。

包作用域

标记为 public 的部分可以被任意的类使用;标记为 private 的部分只能被定义它们的类使用。 如果没有指定 public 或 private, 这个部分(类、方法或变量)可以被同一个包中的所有方法访问。

类 路 径

类存储在文件系统的子目录中。类的路径必须与包名匹配。

JAR 文件使用 ZIP 格式组织文件和子目录。可以使用所有 ZIP 实用程序查看内部的 rt.jar 以及其他的 JAR 文件。

由于运行时库文件( rt.jar 和在 jre/lib 与 jre/lib/ext 目录下的一些其他的 JAR 文件)会被自动地搜索, 所以不必将它们显式地列在类路径中。

设置类路径

采用 -classpath (或 -cp ) 选项指定类路径:

Linux:java -classpath /home/user/dassdir:.:/home/user/archives/archive.jar HyProg
Windows:java -classpath c:\classdir;.;c:\archives\archive.jar MyProg

文档注释

JDK 包含一个很有用的工具,叫做 javadoc , 它可以由源文件生成一个 HTML 文档。

注释的插入

javadoc 实用程序(utility) 从下面几个特性中抽取信息:

  • 公有类与接口
  • 公有的和受保护的构造器及方法
  • 公有的和受保护的域

应该为上面几部分编写注释 、 注释应该放置在所描述特性的前面。 注释以 /** 开始, 并以 */ 结束。

每个 /** . . . */ 文档注释在标记之后紧跟着 自由格式文本( free-form text )。标记由 @ 开始, 如 @author@param

自由格式文本的第一句应该是一个概要性的句子。javadoc 实用程序自动地将这些句子抽取出来形成概要页。

在自由格式文本中,可以使用 HTML 修饰符, 例如,用于强调的 <em> 、 用于着重强调的 <strong> 以及包含图像的 <img> 等。不过, 一定不要使用 <hl><hr> ,因为它们会与文档的格式产生冲突。若要键入等宽代码, 需使用 {@code ... } 而不是 <code>

如果文档中有到其他文件的链接, 例如, 图像文件(用户界面的组件的图表或图像等), 就应该将这些文件放到子目录 doc-files 中。javadoc 实用程序将从源目录拷贝这些目录及其中的文件到文档目录中。 在链接中需要使用 doc-files 目录, 例如:<img src="doc-files/uml_png" alt="UML diagram" >

类注释

类注释必须放在 import 语句之后,类定义之前。

没有必要在每一行的开始用星号 * ,然而, 大部分 IDE 提供了自动添加星号 * , 并且当注释行改变时, 自动重新排列这些星号的功能。

方法注释

每一个方法注释必须放在所描述的方法之前。除了通用标记之外, 还可以使用下面的标记:

  • @param 变量描述
  • @return 描述
  • @throws 类描述

域注释

只需要对公有域(通常指的是静态常量)建立文档。

通用注释

下面的标记可以用在类文档的注释中:

标记 描述
@author 作者
@version 版本

下面的标记可以用于所有的文档注释中:

标记 描述 示例
@since 始于 @since version 1.7.1
@deprecated 不再使用 @deprecated Use <code> setVisible(true) </code> instead
@see 引用,用于类和方法上,这个标记将在“ see also ” 部分增加一个超级链接。 @see com.horstraann.corejava.Employee#raiseSalary(double)
@see <a href="http://www.horstmann.com/corejava.html">The Core Java home page</a>
@see "Core Java 2 volume 2"
@link 引用,可用于注释中的任何位置 {@link com.horstraann.corejava.Employee#raiseSalary(double)}

包与概述注释

可以直接将类、 方法和变量的注释放置在 Java 源文件中, 只要用 /** . . . */ 文档注释界定就可以了。但是, 要想产生包注释,就需要在每一个包目录中添加一个单独的文件。可以有如下两个选择:

  • 提供一个以 package.html 命名的 HTML 文件。在标记 <body> 中的所有
    文本都会被抽取出来
  • 提供一个以 package-info.java 命名的 Java 文件。这个文件必须包含一个初始的以 /** 和 */ 界定的 Javadoc 注释, 跟随在一个包语句之后。它不应该包含更多的代码或注释

还可以为所有的源文件提供一个概述性的注释。这个注释将被放置在一个名为 overview.html 的文件中,这个文件位于包含所有源文件的父目录中。标记 <body> 之间的所有文本将被抽取出来。当用户从导航栏中选择 “Overview” 时,就会显示出这些注释内容。

注释的抽取

javadoc -d docDirectory nameOfPackage

有关其他的选项, 请查阅 javadoc 实用程序的联机文档,http://docs.oracle.com/javase/8/docs/guides/javadoc

类设计技巧

  1. 一定要保证数据私有

  2. 一定要对数据初始化

  3. 不要在类中使用过多的基本类型

    用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。例如, 用一个称为 Address 的新的类替换一个 Customer 类中以下的实例域:

    private String street;
    private String city;
    private String state;
    private int zip;
    
  4. 不是所有的域都需要独立的域访问器和域更改器

  5. 将职责过多的类进行分解

  6. 类名和方法名要能够体现它们的职责

    命名类名的良好习惯是采用一个名词(Order )、 前面有形容词修饰的名词( RushOrder )或动名词(有 “-ing” 后缀)修饰名词(例如, BillingAddress )。

  7. 优先使用不可变的类

    LocalDate 类以及 java.time 包中的其他类是 不可变的——没有方法能修改对象的状态

    更改对象的问题在于, 如果多个线程试图同时更新一个对象, 就会发生并发更改。其结果是不可预料的。 如果类是不可变的,就可以安全地在多个线程间共享其对象

继 承

类、超类和子类

定义子类

关键字 extends 表示继承

已存在的类称为 超类 ( superclass )、 基类( base class ) 或 父类( parent class); 新类称为 子类(subclass、) 派生类 ( derived class ) 或 孩子类( child class )。

超类 这个名称可能带来误解,实际上,子类比超类拥有的功能更加丰富。

前缀 来源于计算机科学和数学理论中的集合语言的术语。

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

关键字 super 指示调用父类方法

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

子类构造器

我们可以通过 super 实现对超类构造器的调用。使用 super 调用构造器的语句必须是子类构造器的第一条语句。

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

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

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

继承层次

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

Java 不支持多继承。

多 态

有一个用来判断是否应该设计为继承关系的简单规则, 这就是 is-a 规则, 它表明子类的每个对象也是超类的对象。

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

在 Java 程序设计语言中, 对象变量是 多态 的。

不能将一个超类的引用赋给子类变量。

警告: 在 Java 中, 子类数组的引用可以转换成超类数组的引用, 而不需要采用强制类型转换。

Child[] childArr = new Child[5];
Parent[] parentArr = childArr;
parentArr[0] = new Parent();    // 运行时异常:Exception in thread "main" java.lang.ArrayStoreException
childArr[0].childSay();

所有数组都要牢记创建它们的元素类型, 并负责监督仅将类型兼容的引用存储到数组中。

理解方法调用

假设要调用 x.f(args) ,隐式参数 x 声明为类 C 的一个对象。下面是调用过程的详细描述:

  1. 编译器査看对象的声明类型和方法名,编译器将会一一列举所有 C 类中名为 f 的方法和其超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)

  2. 编译器将査看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配, 就选择这个方法。这个过程被称为 重载解析(overloading resolution)

    方法的名字和参数列表称为方法的签名。

    如果在子类中定义了一个与超类签名相同的方法, 那么子类中的这个方法就覆盖了超类中的这个相同签名的方法。

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

  3. 如果是 private 方法、 static 方法、 final 方法或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称为 静态绑定( static binding )

  4. 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法

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

动态绑定有一个非常重要的特性: 无需对现存的代码进行修改,就可以对程序进行扩展。

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

阻止继承:final 类和方法

不允许扩展的类被称为 final 类。如果在定义类的时候使用了 final 修饰符就表明这个类是 final 类。

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

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

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

在早期的 Java 中,有些程序员为了避免动态绑定带来的系统开销而使用 final 关键字。如果一个方法没有被覆盖并且很短, 编译器就能够对它进行优化处理, 这个过程为称为 内联 ( inlining )。 例如,内联调用 e.getName() 将被替换为访问 e.name 域。

幸运的是, 虚拟机中的即时编译器比传统编译器的处理能力强得多。这种编译器可以准确地知道类之间的继承关系, 并能够检测出类中是否真正地存在覆盖给定的方法。

强制类型转换

// 将表达式 x 的值转换成整数类型, 舍弃了小数部分。
double x = 3.405;
int nx = (int) x;

Employee[] staff = new Employee[10];
Manager boss = (Manager) staff[0];

进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后, 使用对象的全部功能。

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

只能在继承层次内进行类型转换。

在将超类转换成子类之前,应该使用 instanceof 进行检查。否则可能出现 ClassCastException 异常,只要
没有捕获 ClassCastException 异常,程序就会终止执行。

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

x instanceof C

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

在一般情况下,应该尽量少用类型转换和 instanceof 运算符。

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

抽象类

如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看, 祖先类更加通用, 人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。

方法上使用 abstract 关键字,这样就完全不需要实现这个方法了

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

类即使不含抽象方法,也可以将类声明为抽象类。

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

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

受保护访问

大家都知道, 最好将类中的域标记为 private , 而方法标记为 public。任何声明为 private 的内容对其他类都是不可见的。前面已经看到, 这对于子类来说也完全适用, 即子类也不能访问超类的私有域

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

在实际应用中,要谨慎使用 protected 属性。假设需要将设计的类提供给其他程序员使用,而在这个类中设置了一些受保护域, 由于其他程序员可以由这个类再派生出新类,并访问其中的受保护域。在这种情况下,如果需要对这个类的实现进行修改, 就必须通知所有使用这个类的程序员。这违背了 OOP 提倡的数据封装原则。

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

事实上,Java 中的受保护部分对所有子类及同一个包中的所有其他类都可见。

Java 用于控制可见性的 4 个访问修饰符:

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

Object: 所有类的超类

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

public class Employee extends Object

如果没有明确地指出超类,Object 就被认为是这个类的超类。

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

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

equals 方法

Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。

public boolean equals(Object obj) {
    return (this == obj);
}

工具方法 Objects.equals

public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

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

public boolean equals(Object otherObject) {
    if (!super.equals(otherObject)) return false;
    Manager other = (Manager) otherObject;
    // super.equals checked that this and other belong to the same class
    return bonus == other.bonus;
}

相等测试与继承

Employee 类的 equals 方法:

public boolean equals(Object otherObject) {
    // a quick test to see if the objects are identical
    if (this == otherObject) return true;

    // must return false if the explicit parameter is null
    if (otherObject == null) return false;

    // if the classes don't match, they can't be equal
    if (getClass() != otherObject.getClass()) return false;

    // now we know otherObject is a non-null Employee
    Employee other = (Employee) otherObject;

    // test whether the fields have identical values
    return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
}

Manager 继承自 Employee 类,Managerequals 方法:

public boolean equals(Object otherObject) {
    if (!super.equals(otherObject)) return false;
    Manager other = (Manager) otherObject;
    // super.equals checked that this and other belong to the same class
    return bonus == other.bonus;
}

在前面的例子中, 如果发现类不匹配, equals 方法就返冋 false : 但是, 许多程序员却喜欢使用 instanceof 进行检测:

if (!(otherObject instanceof Employee)) return false;  

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

  • 自反性 : 对于任何非空引用 x , x.equals(x) 应该返回 true
  • 对称性 : 对于任何引用 x 和 y , 当且仅当 y.equals(x) 返回 true , x.equals(y) 也应该返回 true
  • 传递性 : 对于任何引用 x、 y 和 z , 如果 x.equals(y) 返回 truey.equals(z) 返回 true , x.equals(z) 也应该返回 true
  • 一致性 : 如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果
  • 对于任意非空引用 x , x.equals(null) 应该返回 false

instanceof 检测对象类型,不能保证 对称性

某些书的作者认为不应该利用 getClass 检测, 因为这样不符合 置换原则

下面可以从两个截然不同的情况看一下这个问题:

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

在雇员和经理的例子中, 只要对应的域相等, 就认为两个对象相等。如果两个 Manager 对象所对应的姓名、 薪水和雇佣日期均相等, 而奖金不相等, 就认为它们是不相同的, 因此,可以使用 getClass 检测。

但是, 假设使用雇员的 ID 作为相等的检测标准, 并且这个相等的概念适用于所有的子类, 就可以使用 instanceof 进行检测, 并应该将 Employee.equals 声明为 final

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

  1. 显式参数命名为 otherObject , 稍后需要将它转换成另一个叫做 other 的变量

  2. 检测 thisotherObject 是否引用同一个对象:

    if (this = otherObject) return true;
    

    这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多

  3. 检测 otherObject 是否为 null , 如 果 为 null , 返 回 false 。这项检测是很必要的

    if (otherObject = null) return false;
    
  4. 比较 thisotherObject 是否属于同一个类。如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测:

    if (getClass() != otherObject.getClass()) return false;
    

    如果所有的子类都拥有统一的语义,就使用 instanceof 检测:

    if (!(otherObject instanceof ClassName)) return false;
    
  5. otherObject 转换为相应的类类型变量:

    ClassName other = (ClassName) otherObject
    
  6. 现在开始对所有需要比较的域进行比较了。使用 == 比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配, 就返回 true ; 否则返回 false

    return field1 == other.field1 && Objects.equa1s(fie1d2, other.field2) && ... ;
    
  7. 如果在子类中重新定义 equals , 就要在其中包含调用 super.equals(other)

对于数组类型的域, 可以使用静态的 Arrays.equals 方法检测相应的数组元素是否相等。

下面是实现 equals 方法的一种常见的错误:

public class Employee {
    public boolean equals(Employee other) {
		// ...
    }
}

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

为了避免发生类型错误, 可以使用 @Override 对覆盖超类的方法进行标记,如果出现了错误, 并且正在定义一个新方法, 编译器就会给出错误报告。

hashCode 方法

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

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

Object 类中的 hashCode 方法:

public native int hashCode();

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

使用 null 安全的方法 Objects.hashCode

public static int hashCode(Object o) {
    return o != null ? o.hashCode() : 0;
}

public static int hash(Object... values) {
    return Arrays.hashCode(values);
}

需要组合多个散列值时, 可以调用 Objects.hash 并提供多个参数:

public static int hash(Object... values) {
    return Arrays.hashCode(values);
}

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

Arrays.hashCode

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

使用静态方法 Double.hashCode 来避免创建 Double 对象,其他基本类型的包装类也提供了相同静态方法:

public static int hashCode(double value) {
    long bits = doubleToLongBits(value);
    return (int)(bits ^ (bits >>> 32));
}

toString 方法

Object 类中的 toString 方法

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

绝大多数(但不是全部)的 toString 方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。

只要对象与一个字符串通过操作符 + 连接起来,Java 编译就会自动地调用 toString 方法,以便获得这个对象的字符串描述。在调用 x.toString() 的地方可以用 ""+x 替代。 这条语句将一个空串与 x 的字符串表示相连接。这里的 x 就是 x.toString() 。与 toString 不同的是, 如果 x 是基本类型, 这条语句照样能够执行。

数组继承了 Object 类的 toString 方法,[I@la46e30 ,前缀 [I 表明是一个整型数组,修正的方式是调用静态方法 Arrays.toString 。 要想打印多维数组(即, 数组的数组)则需要调用 Arrays.deepToString 方法。

Objects 类提供的工具方法:

public static String toString(Object o) {
    return String.valueOf(o);
}

public static String toString(Object o, String nullDefault) {
    return (o != null) ? o.toString() : nullDefault;
}

String.valueOf

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

泛型数组列表 ArrayList

在 Java 中,]允许在运行时确定数组的大小。

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

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

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

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

两边都使用类型参数 Employee, 这有些繁琐。 Java SE 7中, 可以省去右边的类型参数,这被称为 菱形 语法

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

在 Java 的老版本中, 程序员使用 Vector 类实现动态数组。 不过, ArrayList 类更加有效, 没有任何理由一定要使用 Vector

方法名称 方法声明 描述
ensureCapacity void ensureCapacity(int minCapacity) 确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素
trimToSize void trimToSize() 将数组列表的存储容量削减到当前尺寸

访问数组列表元素

下面这段代码是错误的:

ArrayList<String> list = new ArrayList(100); // capacity 100,size 0
list.set(0, "x"); // no element 0 yet
// java.lang.IndexOutOfBoundsException: Index: 0, Size: 0

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

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

public void test2() {
    @SuppressWarnings("unchecked")
    ArrayList<String> list = (ArrayList<String>) arr();
}

private ArrayList arr() {
    return new ArrayList();
}

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

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

一旦能确保不会造成严重的后果, 可以用 @SuppressWarnings("unchecked") 标注来标记这个变量能够接受类型转换

对象包装器与自动装箱

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

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

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

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

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

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

注意 == 运算符

自动装箱规范要求 booleanbytechar <= 127,介于 -128 ~ 127 之间的 shortint 被包装到固定的对象中。因此,在此范围内的基本数据类型对象 == 成立。比较包装器对象时,应该使用 equals 方法。

由于包装器类引用可以为 null , 所以自动装箱有可能会抛出一个 NullPointerException 异常

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

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

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

参数数量可变的方法

省略号 ... 表明这个方法可以接收任意数量的对象。

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

允许将一个数组传递给可变参数方法的最后一个参数。 因此,可以将已经存在且最后一个参数是数组的方法重新定义为可变参数的方法,而不会破坏任何已经存在的代码。

@Test
public void test4() {
    System.out.println(max(1, 2, 3));
    System.out.println(max(new double[]{1, 2, 3}));
}

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

枚举类

enum Size {
    SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

    private Size(String abbreviation) {
        this.abbreviation = abbreviation;
    }

    public String getAbbreviation() {
        return abbreviation;
    }

    private String abbreviation;
}

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

如果需要的话, 可以在枚举类型中添加一些构造器、 方法和域。 当然, 构造器只是在构造枚举常量的时候被调用。

所有的枚举类型都是 Enum 类的子类。

toStringname 方法能够返回枚举常量名。 例如, Size.SMALL.toString() 将返回字符串 SMALLtoString 的逆方法是静态方法 valueOf

Size s = Enum.valueOf(Size,class, "SMALL");
Size s2 = Size.valueOf("SMALL");

每个枚举类型都有一个静态的 values 方法, 它将返回一个包含全部枚举值的数组。

ordinal 方 法 返 回 enum 声 明 中 枚 举 常 量 的 位 置, 位 置 从 0 开始计数。

反射

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

能够分析类能力的程序称为反射(reflective )。

Class 类

在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。然而, 可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class , 这个名字很容易让人混淆。Object 类中的 getClass() 方法将会返回一个 Class 类型的实例。

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

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

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

如果 T 是任意的 Java 类型(或 void 关键字),T.class 将代表匹配的类对象。例如:

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

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

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

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

  • Double[].class.getName() 返回 [Ljava.lang.Double;
  • int[].class.getName() 返回 [I
System.out.println(boolean[].class.getName());  // [Z
System.out.println(char[].class.getName());     // [C
System.out.println(byte[].class.getName());     // [B
System.out.println(short[].class.getName());    // [S
System.out.println(int[].class.getName());      // [I
System.out.println(long[].class.getName());     // [J
System.out.println(float[].class.getName());    // [F
System.out.println(double[].class.getName());   // [D

System.out.println(Boolean[].class.getName());      // [Ljava.lang.Boolean;
System.out.println(Character[].class.getName());    // [Ljava.lang.Character;
System.out.println(Byte[].class.getName());         // [Ljava.lang.Byte;
System.out.println(Short[].class.getName());        // [Ljava.lang.Short;
System.out.println(Integer[].class.getName());      // [Ljava.lang.Integer;
System.out.println(Long[].class.getName());         // [Ljava.lang.Long;
System.out.println(Float[].class.getName());        // [Ljava.lang.Float;
System.out.println(Double[].class.getName());       // [Ljava.lang.Double;

System.out.println(Void[].class.getName());         // [Ljava.lang.Void;
System.out.println(String[].class.getName());       // [Ljava.lang.String;

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

newInstance() 可以用来动态地创建一个类的实例,newInstance 方法调用默认的构造器 (没有参数的构造器)初始化新创建的对象。 如果这个类没有默认的构造器, 就会抛出一个异常

如果希望向类的构造器提供参数, 必须使用 Constructor 类中的 newInstance 方法。

捕获异常

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

异常有两种类型: 未检查异常已检查异常 。 对于已检查异常, 编译器将会检查是否提供了处理器。

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

利用反射分析类的能力

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

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

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

查看对象域的关键方法是 Field 类中的 get 方法。 如果 f 是一个 Field 类型的对象(例如,通过 getDeclaredFields 得到的对象) ,obj 是某个包含 f 域的类的对象,f.get(obj) 将返回一个对象,其值为 obj 域的当前值。

如果 f 是私有域, get 方法将会抛出一个 IllegalAccessException 。除非拥有访问权限,否则 Java 安全机制只允许査看任意对象有哪些域, 而不允许读取它们的值。

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

f.setAtcessible(true);

setAccessible 方法是 AccessibleObject 类中的一个方法, 它是 FieldMethodConstructor 类的公共超类。

AccessibleObject 类中还有批量设置权限的静态方法 setAccessible

Field[] fields = cl.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);

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

java.lang.reflect 包中的 Array 类允许动态地创建数组。

将一个 Employee[] 临时地转换成 Object[] 数组, 然后再把它转换回来是可以的,但一个从开始就是 Object[] 的数组却永远不能转换成 Employe[] 数组。

以下方法可以用来扩展任意类型的数组, 而不仅是对象数组:

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

调用任意方法

反射机制允许你调用任意方法

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

public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException

第一个参数是隐式参数, 其余的对象提供了显式参数(在 Java SE 5.0 以前的版本中, 必须传递一个对象数组, 如果没有显式参数就传递一个 null )。对于静态方法,第一个参数可以被忽略, 即可以将它设置为 null

如果返回类型是基本类型, invoke 方法会返回其包装器类型。

invoke 的参数和返回值必须是 Object 类型的。这就意味着必须进行多次的类型转换。

使用反射获得方法指针的代码要比仅仅直接调用方法明显慢一些

有鉴于此, 建议仅在必要的时候才使用 Method 对象,而最好使用接口以及 Java SE 8中的 lambda 表达式。 特别要重申: 建议 Java 开发者不要使用 Method 对象的回调功能。使用接口进行回调会使得代码的执行速度更快, 更易于维护。

getMethodsgetDeclaredMethods 区别:

  • getMethods 返回父类和自身的 public 方法,不返回 private 方法
  • getDeclaredMethods 返回自身的 publicprivate 方法

继承的设计技巧

  1. 将公共操作和域放在超类
  2. 不要使用受保护的域
  3. 使用继承实现 “is-a” 关系
  4. 除非所有继承的方法都有意义, 否则不要使用继承
  5. 在覆盖方法时, 不要改变预期的行为
  6. 使用多态,而非类型信息
  7. 不要过多地使用反射
posted @ 2022-04-24 21:14  流星<。)#)))≦  阅读(13)  评论(0编辑  收藏  举报