2.3 Java类库

Java类库也被简称为Java API,这是随Java SE平台分发的大量预置类。下面是Java类库包含的一些重要主题:

  • 常用数据结构的定义和实现
  • 控制台I/O
  • 文件I/O
  • 数学运算
  • 联网
  • 正则表达式
  • XML的创建和处理
  • 数据库访问
  • GUI工具包
  • 反射

这里无法全面介绍Java类库,而只提供一些API示例,让你大致知道去哪里寻找所需的类。介绍具体的类之前,我们先来看看Java类库的主要组织结构,这包括如下主题:

  • Java类库的组织结构
  • 包概述
  • java.lang包中的重要类
  • 集合API,具体地说是java.util.ArrayListjava.util.HashMap

2.3.1 Java类库的组织结构

Java类库中的所有类都放在包中。最重要的包的名称都以如下两项内容打头:

  • java
  • javax

这主要是历史原因导致的。声誉良好的现代Java SE实现都实现了这两个包中的类,但还有其他的公有类。一些杂项类放在以org打头的包中,如org.w3corg.xml,但本书不会介绍这些类。

厂商可在库中添加自己的类,在Oracle的实现中,这些类位于名称以com.sunsuncom.oracle打头的包中。然而,建议你使用附加库而不是这些包中的类。

2.3.2 包概述

为了让你对类的分组情况有大致了解,这里简要地介绍一下Java类库中最重要的包。这只是个引子,旨在让你熟悉Java类库的结构,因此并没有列出所有的包。

描述
java.lang 这个包中的类是最重要的,其中包括StringStringBuilder类、基本类型包装类、线程化类以及所有类的祖先——Object
java.lang.reflect 提供了反射API。反射让你能够动态地检查类,以获悉方法和变量的名称、调用方法以及读写属性
java.uil 最重要的包之一,包含实现集合、日期和时间、国际化等的类
java.util.concurrent 包含用于并发编程的类
java.io java.net java.nio 包含与操作系统、文件和网络I/O相关的类,还包含字符集编码/解码类
java.math 提供了BigDecimal类。相比于基本类型floatdouble以及BigInteger类,这个类要精确得多,它能存储的整数值比基本类型intlong大得多
java.xml 包含XML处理类
java.sql javax.sql 包含用于访问JDBC数据库系统的类
java.awt 抽象窗口工具包(Abstract Window Toolkit)——最早的Java GUI工具包,位于操作系统原生GUI和JVM之间
javax.swing 包含Swing GUI工具包类,这些类是建立在AWT工具包的基础之上的。一个不同于AWT的重要之处是,其所有GUI控件都是使用Java代码实现的
javafx JavaFX GUI工具包类,这是一款非常时髦的3D加速图形产品

2.3.3 java.lang包中的重要类

对JVM平台来说,java.lang包中的类至关重要,因此这个包中的很多类在本书后面经常会被提及。本节旨在提供一些背景信息,而不是要取代Java API文档。本节将介绍下面这些类:

  • Object类(java.lang.Object);
  • String类(java.lang.String);
  • 基本数据类型包装类(java.lang包中的Integer、Long、Short、Char、Float和Double);
  • 异常和错误(java.lang.Exceptionjava.lang.Error)。

下面是这里要讨论的类组成的类层次结构图:

2.3 Java类库 - 图1

本书后面还将讨论Java类库中众多其他的类。

  • Object类(java.lang.Object

java.lang包中的Object类是其他所有类的基类;在JVM中,只有它没有父类。在Java语言中,没有显式地继承其他类的类都隐式地继承java.lang.Object类。

  • Object类的重要方法

下表列出了java.lang.Object类中最常用的方法。

方法名返回类型描述 toString()String返回对象的文本描述 equals(Object object)boolean指出传入的对象是否与当前对象相等,它使用多种规则来判断相等性,这将在后面讨论 hashCode()int计算当前对象的散列值,将在后面介绍集合API时讨论

Object(以及每个JVM对象)提供的重要方法之一是toString(),它返回当前对象实例的文本描述。Oracle提供的默认实现是返回全限定类名以及十六进制格式的hashCode()结果,但建议在自定义类中重写这个方法,以提供更易于理解的描述。

集合API大量地使用了方法equals()hashCode(),后面将更详细地介绍它们的用法。

有关java.lang.Object类的完整方法列表,请参阅API文档。

  • String类(java.lang.String

java.lang.String类表示JVM中的字符串。这是一种不可修改的对象,这意味着修改String对象时不会影响原始对象,而是生成一个包含修改后内容的新字符串。字符串在内部都是使用UTF-16编码存储的。

这个类将在下一章更详细地介绍。本书介绍的有些语言有自己的字符串类,这些类包含额外的便利方法和独特的功能。通常,JVM语言会在幕后透明地在这两种字符串类型之间进行转换。

  • 基本类型包装器类(java.lang包中的Integer、Long、Short、Char、Float、Double类)

并非所有的Java API都能够使用JVM的内置基本数据类型。如果需要的是基本类型包装类,而你传递的是基本数据类型变量,编译器将自动创建指定包装类的实例。反之亦然,即在需要的是基本数据类型变量,而你传递的是包装类对象时,编译器将把包装类对象的值赋给基本数据类型变量。这个过程被称为自动装箱(autoboxing)。

2.3 Java类库 - 图2 并非所有的JVM语言都支持自动装箱,但大多数流行的JVM语言都支持,包括本书介绍的所有语言。

与String类一样,这些类也都是不可修改的。调用任何修改值的方法时,都将创建并返回一个包含新值的实例。

前一章说过,有些JVM语言遵循“一切皆对象”的面向对象编程规则,不支持基本数据类型,因此使用包装类来处理基本类型值。

  • 自动装箱示例

下面的代码将一个基本类型int值赋给一个java.lang.Integer引用变量:

  1. int primitiveInt = 42;
  2. Integer wrappedInteger = primitiveInt;

这将创建一个包装了int值42的Integer对象,与显式地创建一个Integer实例等效:

  1. Integer wrappedInteger = new Integer(42);

下面的代码将两个Integer实例传递给一个将两个int值作为参数的API,结果符合预期:

  1. System.out.println("Hello world".substring(new Integer(0),
  2. new Integer(5)));

这将打印Hello。

  • 异常和错误(java.lang.Exceptionjava.lang.Error

JVM开发人员必须知道JVM是如何管理运行阶段错误的。鉴于所有的语言都有自己的运行阶段错误处理机制,这里不介绍错误是如何处理的,而只说说发生运行阶段错误时的情况。

在方法中发生运行阶段错误时,将创建并引发一个异常或Error对象。在Java语言中,这是使用关键字throw实现的。下面是一个引发通用异常Exception的示例:

  1. throw new Exception("Oops!");

Java有很多继承ExceptionError类的内置类。创建自定义异常类时,应考虑可重用哪个既有的异常类。例如,如果方法要求传入的引用不能为空,应在传入的参数为空时引发异常java.lang.NullPointerException;所有Java API在用户将空引用传递给不支持它的方法时都这样做。

2.3 Java类库 - 图3 注意:如果你仔细研究本节开头的类图,将发现ExceptionError都继承了Throwable类。Throwable是可引发的对象,但通常使用ExceptionError的子类,因为它们使用起来更方便。

ExceptionError类的不同之处如下。

  • 在程序很可能能够妥善地处理错误并继续运行时引发异常。
  • 发现根本没有想到的问题时引发错误。很多错误都是由JVM本身引发的。
    异常或错误(以下简称异常)被引发时,JVM将查看引发异常的方法。如果它包含能够处理错误的错误处理程序,就把控制权交给错误处理程序。如果这个方法不能处理任何错误(或当前错误),就检查方法调用方是否包含错误处理程序。这个过程将不断重复,直到找到能够处理当前错误且不引发新错误的方法,或者进入第一个方法调用。在第二种情况下,JVM实例将崩溃并生成类似于下面的栈跟踪:
  1. Exception in thread "main" java.lang.Exception: Oops
  2. at ExceptionDemo.method3(ExceptionDemo.java:37)
  3. at ExceptionDemo.method2(ExceptionDemo.java:33)
  4. at ExceptionDemo.method1(ExceptionDemo.java:29)
  5. at ExceptionDemo.main(ExceptionDemo.java:25)

很多语言都在生成的Java字节码中包含源代码文件名和行号,这样可以在栈跟踪中包含源代码行号,让栈跟踪更容易理解。

Java有严格的异常引发规则,因此并非每个类都能够引发所有的异常;在这方面,很多其他的JVM语言都灵活得多。有关Java的异常引发规则以及java.lang.RuntimeException类在Java语言中扮演的重要角色,将在下一章讨论。

2.3.4 集合API——java.util.ArrayListjava.util.HashMap

java.util包包含各种各样的数据结构,这里只介绍其中的两个,但后面会时不时地提及其他的。有些JVM语言使用这些类的变种,以提供额外的功能,但大多数JVM语言都使用这里讨论的类,以最大限度地与Java和JVM平台兼容。

很多语言都支持泛型(generics),以限制对象可使用的对象类型。在集合类中,通过使用泛型限制的是可在集合类中存储的对象类型。鉴于很多语言都有不同的泛型表示规则,这个主题将在讨论各种语言时阐述,这里就按下不表了。在本书介绍的语言中,Clojure当前根本就不支持泛型。

需要指出的是,集合类只能用于处理对象。对于基本数据类型值,将把它们自动装箱为对象,反之亦然;这也适用于支持基本类型值的其他JVM语言。自动装箱在前面介绍基本类型值时讨论过。

本节将介绍如下两个集合类:

  • java.util.ArrayList(一个列表类,在内部是使用数组实现的);
  • java.util.HashMap(包含键-值组合的容器)。

2.3 Java类库 - 图4 在Python程序员看来,这两个类分别相当于列表和字典;而在Ruby程序员看来,它们分别相当于Ruby中的ArrayHash对象。

  • ArrayListjava.util.ArrayList

这个类非常简单,使用起来也很方便。顾名思义,它实现了可存储其他对象的链表结构。

虽然JVM平台和大多数JVM语言都提供了内置的数组支持,但ArrayList对象使用起来更容易。相比于常规数组,ArrayList有如下两个优势。

  • 在已经填满且需要更多空间时,ArrayList对象会自动加长,而数组必须手动进行管理。
  • 数组只提供了一个属性(用于获取数组的长度),而ArrayList类提供了大量便利的方法。

2.3 Java类库 - 图5 虽然在JVM中,数组没有内置方法,但Java提供的java.util.Arrays类有很多方法,让数组使用起来更容易。有些JVM语言甚至给数组添加了额外的方法。

下面来看一些方法和代码示例。

(1) ArrayList类中常用的方法

下表列出了ArrayList对象提供的一些最重要的方法。请注意,使用泛型时,ArrayList对象处理的不是对象,而是指定的类型。

方法名返回类型描述 add(Object o)boolean将指定对象添加到内部列表(通常是使用数组实现的)中 add(int index, Object o)-将指定对象添加到列表的指定位置 addAll(Collection c)boolean将指定集合中的所有项添加到当前列表中,其中Collection是一个接口,集合API中的很多类都实现了它 clear()-清除所有的内容 contains(Object o)Object指出指定的对象是否包含在列表中 get(int index)Object获取指定索引处的对象 set (int index, Object o)Object将指定索引处的对象替换为传入的对象 size()int返回列表包含的元素个数

(2) ArrayList使用示例

下面是一个简单的Java语言示例,演示了ArrayList的一些方法:

  1. ArrayList list1 = new ArrayList();
  2. list1.add("this is a test");
  3. list1.add(0, "Hello");
  4. ArrayList list2 = new ArrayList();
  5. list2.addAll(list1);
  6. list1.clear();
  7. System.out.println(list1);
  8. System.out.println(list2);
  9. System.out.println(list2.contains("this is a test"));

输出如下:

  1. []
  2. [Hello, this is a test]
  3. true

第一行输出[]表明list1为空,而第二行输出表明list2包含两个字符串,依次为Hellothis is a test。最后,向控制台打印了单词true,因为this is a test包含在ArrayList对象list2中。

  • HashMap(java.util.HashMap

HashMap存储键-值组合。在JVM中,这种数据结构被称为映射。在映射中插入对象时,需要同时指定键对象和值对象;有了键对象,就可获取与之相关联的值。这种数据结构不保留键的插入顺序。

从技术上说,HashMap计算键对象的散列值,并以能够快速查找的方式存储它们,再存储与键相关联的值。下面先来看一些常用方法和代码示例,再更详细地介绍这个类的工作原理。

(1) HashMap类中常用的方法

HashMap类提供了很多方法,下表列出了其中最常用的,但要正而八经地使用HashMap类,务必参阅完整的API文档。

方法名返回类型描述 put (Object key, Object value)Object添加指定的键-值对。如果指定的键已经存在,则使用指定的值替换原来与之相关联的值。如果添加了指定的键,就返回null,否则返回原来的值 putAll (Map map)-添加指定映射中所有的键-值对,同样,对于已存在的键,替换与之相关联的值 putIfAbsent (Object key, Object value)Object仅当指定的键不存在时,才添加指定的键-值对。如果指定的键已存在,就什么都不做。在添加了指定的键-值对时返回null;如果指定的键已存在,就返回原来与之相关联的值 remove (Object key)Object如果指定的键已存在,就删除相应的键-值对,否则什么都不做 containsKey (Object key)boolean指出指定的键当前是否包含在映射中 get (Object key)Object返回与指定的键相关联的值;如果没有找到指定的键,就返回null getOrDefault (Object key, Object defaultValue)Object如果找到指定的键,就返回与之相关联的值,否则返回defaultValue clear()-清空集合,即删除所有的键-值对 size()int返回映射当前存储的键-值对个数

(2) HashMap使用示例

下面的Java代码演示了HashMap的一些基本用法:

  1. HashMap map = new HashMap();
  2. map.put("key1", "value1");
  3. map.put("key1", "value2");
  4. map.putIfAbsent("key1", "value3");
  5. System.out.println(map.get("key1"));
  6. System.out.println(map.containsKey("value2"));
  7. System.out.println(map.size());

输出如下:

  1. value2
  2. false
  3. 1

第一行输出为value2,因为第二次调用map.put时将原来的value1替换成了value2,而方法调用map.putIfAbsent什么都没做。第二行输出为false,因为方法map.containsKey只查找键。最后一行输出为1,因为只存储了一个键-值对。

  • 让自定义类的对象能够存储在集合API中

前面说过,基类java.lang.Object包含如下两个重要的方法:

  • hashCode()
  • equals(Object other)
    所有集合API都大量地使用了这两个方法。为提高性能,同时确保集合API能够像预期的那样工作,在要存储到集合对象中的类中,必须重写这两个方法以提供良好的实现。创建自定义类时,Java程序员必须自己编写这两个方法的实现;但本书介绍的其他语言都通常会在你定义类时自动为这两个方法生成实现。

2.3 Java类库 - 图6 如果你不喜欢JVM语言替你生成的方法hashCode()equals()的实现,在大多数情况下都可手动重写这些方法,以便提供自己的实现。

鉴于需要提供这两个方法的实现的大多是Java程序员,有关这两个方法需要遵循的规则将在第3章介绍。然而,这里将简要地介绍这些方法,让你知道众多的集合类是如何使用散列机制的。

(1) hashCode()简介

顾名思义,这个方法在需要获得当前对象的散列值时被调用。

方法hashCode()必须返回一个随对象内容变化而变化的整数值。另外,在对象类似的情况下,它应尽可能返回不同的值。

2.3 Java类库 - 图7 返回不能标识对象的值不算错,但这将给大多数集合类的性能带来负面影响。这一点将在后面更详细地讨论。

(2) equals()简介

方法equals()在传入的对象与当前对象相等时返回true,否则返回false。下面是一个简单的示例:

  1. Integer i = 25;
  2. Object o = new Object();
  3. System.out.println(i.equals(o));

上述代码将在控制台中打印false

方法equals()必须检查两个对象,并指出它们是否类似。这个方法必须遵循很多规则,这也将在第3章讨论,因为通常只有Java开发人员才需要自己编写这个方法。

2.3 Java类库 - 图8 如果类提供的方法equals()的实现没有遵守所有的规则(也叫约定,contract),将无法保证集合类能够正确地工作。

(3) 散列机制

为说明方法hashCode()equals()为何如此重要,我们来看一个示例。

请看下面的Java代码,它创建一个HashMap实例并在其中添加一个键-值对:

  1. map = HashMap();
  2. map.put("key1", "value1");

将对传入的键对象(这里是String实例key1)调用方法hashCode()。这将生成一个可用于散列的数字(在这个示例中,为123)。在内部,HashMap实例以能够快速查找的方式存储键的散列值,并将键和值都与之关联起来。

2.3 Java类库 - 图9

现在添加一个新的键-值对key2value2。假设对对象key2调用方法hashCode()返回的是234,由于这个散列值未被占用,因此将添加它,并将指定的键对象和值对象与之关联起来。

2.3 Java类库 - 图10

接下来,添加新的键-值对key3value3,但发生了意外:对String实例key3调用方法hashCode()时,返回的也是234,因此这个散列值同时指向键-值对key2/value2key3/value3。这被称为冲突(collision)。

2.3 Java类库 - 图11

程序要求获取与键对象key3相关联的值对象:

  1. Object o = map.get("key3");

HashMap对象将对传入的键"key3"调用方法hashCode(),这也将返回234。然而,HashMap对象发现这个散列值与两个键-值对相关联,因此对String对象"key2""key3"都调用方法equals(),以确定哪个键与String对象"key3"匹配。在这里,匹配的是"key3",因此返回对象"value3"

从这个示例可知,方法hashCode()equals()非常重要。添加键-值对时,发生的冲突越少,找到键的速度越快。如果方法equals()的实现有问题,键查找过程也将失败。