面向 Android 开发的 Java 学习手册(四)

原文:Learn Java for Android development

协议:CC BY-NC-SA 4.0

八、探索基本 API:第二部分

标准类库的 java.lang 包提供了许多基本的 API,这些 API 是为了支持语言特性而设计的。在前一章中,你已经遇到了数学、字符串管理和包的 API。在这一章中,我将向你介绍那些与基本类型包装类、线程和系统功能相关的基本库 API。

探索原始类型包装类

java.lang 包包括布尔、字节、字符、双精度、浮点、整数、长、短。这些类被称为原始类型包装类,因为它们的实例将自己包装在原始类型的值周围。

注意原始类型包装类也被称为值类

Java 提供这八个基本类型包装类有两个原因:

  • 集合框架(在第九章中讨论)提供了只能存储对象的列表、集合和映射;它们不能存储原始类型的值。您将基元类型值存储在基元类型包装类实例中,并将该实例存储在集合中。
  • 这些类提供了一个很好的地方来将有用的常量(如 MAX_VALUE 和 MIN_VALUE )和类方法(如 Integer 的 parseInt() 方法和 Character 的 isDigit() 、 isLetter() 和 toUpperCase() 方法)与原语类型关联起来。

在这一节中,我将向您介绍每一个原始类型包装类和一个名为号的 java.lang 类。

布尔值

Boolean 是最小的原始类型包装类。这个类声明了三个常量,包括真和假,它们表示预先创建的布尔对象。它还声明了一对用于初始化一个布尔对象的构造函数:

  • 布尔(布尔值)将布尔对象初始化为值。
  • 布尔(字符串 s) 将的文本转换为真或假值,并将该值存储在布尔对象中。

第二个构造函数将的值与真值进行比较。因为比较不区分大小写,所以这四个字母的任何大写/小写组合(例如 true 、 true 或 tRue )都会导致 TRUE 被存储在对象中。否则,构造函数在对象中存储 false。

注意 Boolean 的构造函数由 Boolean value()补充,返回包装后的布尔值。

Boolean 还声明或覆盖以下方法:

  • int compareTo(布尔 b) 将当前布尔对象与 b 进行比较,以确定它们的相对顺序。当当前对象包含与 b 相同的布尔值时,该方法返回 0;当当前对象包含 true 且 b 包含 false 时,该方法返回正值;当当前对象包含 false 且 b 包含 true 时,该方法返回负值。
  • Boolean equals(Object o)将当前 Boolean 对象与 o 进行比较,当 o 不为 null, o 类型为 Boolean ,且两个对象包含相同的布尔值时,返回 true。
  • 静态布尔 getBoolean(字符串名) 当由名标识的系统属性(本章稍后讨论)存在且等于 true 时,返回 true。
  • int hashCode() 返回一个合适的哈希码,允许布尔对象用于基于哈希的集合(在第九章中讨论)。
  • 静态布尔解析布尔(String s) 解析 s ,当 s 等于【true】【TRUE】【TRUE】或任何其他大写/小写组合时返回 TRUE。否则,此方法返回 false。(解析将一个字符序列分解成有意义的成分,称为记号。)
  • String toString() 当当前布尔实例包含 true 时,返回【true】;否则,该方法返回“false”。
  • 静态字符串 toString(布尔 b) 当 b 包含 true 时,返回“true”;否则,该方法返回“false”。
  • 静态布尔值 Of(boolean b) 当 b 包含真时返回真当 b 包含假时返回假。
  • 静态布尔值 Of(String s) 当 s 等于“真”、“真”、“真”或任何其他大写/小写组合时,返回真。否则,该方法返回假。

注意初入布尔类的人经常认为 getBoolean() 返回一个布尔对象的真/假值。然而, getBoolean() 返回基于布尔值的系统属性的值——我将在本章后面讨论系统属性。如果需要返回一个 Boolean 对象的真/假值,请使用 booleanValue() 方法 来代替。

使用真和假通常比创建布尔对象更好。例如,假设您需要一个方法,当该方法的 double 参数为负时,该方法返回包含 true 的布尔对象,当该参数为零或正时,该方法返回 false。你可以像下面这样声明你的方法 isNegative() 方法 :

Boolean isNegative(double d)
{
   return new Boolean(d < 0);
}

尽管这个方法很简洁,但它不必要地创建了一个布尔对象。当频繁调用该方法时,会创建许多消耗堆空间的布尔对象。当堆空间不足时,垃圾收集器会运行并降低应用的速度,从而影响性能。

下面的例子揭示了一种更好的编码方式 isNegative() :

Boolean isNegative(double d)
{
   return (d < 0) ? Boolean.TRUE : Boolean.FALSE;
}

该方法通过返回预先创建的真或假对象来避免创建布尔对象。

提示你应该努力创建尽可能少的对象。您的应用不仅内存占用更少,而且性能更好,因为垃圾收集器不会像以前那样频繁运行。

性格;角色;字母

Character 是最大的原始类型包装类,包含许多常量、一个构造函数、许多方法和一对嵌套类 ( 子集和 UnicodeBlock )。

注意 字符的复杂性来源于 Java 对 Unicode(en.wikipedia.org/wiki/Unicode)的支持。为了简洁起见,我忽略了大部分字符与 Unicode 相关的复杂性,这超出了本章的范围。

Character 声明了一个单独的 Character(char value) 构造函数,您可以用它将一个 Character 对象初始化为值。这个构造函数由 char charValue() 补充,它返回包装的字符值。

当您开始编写应用时,您可能会编写一些表达式,如 ch>= ’ 0 '&&ch<= ’ 9 '(测试 ch 以查看它是否包含一个数字)和 ch>= ’ A '&&ch<= ’ Z '(测试 ch 以查看它是否包含一个大写字母)。您应该避免这样做,原因有三:

  • 在表达式中引入 bug 太容易了。例如,ch>’ 0 '&&ch<= ’ 9 '引入了一个比较中不包括 ‘0’ 的微妙 bug。
  • 这些表达式不能很好地描述他们正在测试的内容。
  • 表达式偏向于拉丁数字(0–9)和字母(A–Z 和 A–Z)。它们没有考虑在其他语言中有效的数字和字母。例如, ‘\u0beb’ 是一个字符文字,代表泰米尔语中的一个数字。

Character 声明了几个比较和转换类方法来解决这些问题。这些方法包括以下内容:

  • 静态布尔 isDigit(char ch) 当 ch 包含一个数字(通常是 0 到 9,但也包括其他字母中的数字)时,返回 true。
  • 静态布尔 isLetter(char ch) 当 ch 包含一个字母(通常是 A–Z 或 A–Z,但也包括其他字母表中的字母)时返回 true。
  • 当 c h 包含一个字母或数字(通常是 A-Z、A-Z 或 0-9,但也包括其他字母表中的字母或数字)时,静态布尔 isLetterOrDigit(char ch) 返回 true。
  • 静态布尔 is lower case(char ch)当 ch 包含小写字母时返回 true。
  • 静态布尔 isUpperCase(char ch) 当 ch 包含大写字母时返回 true。
  • 静态布尔值 isWhitespace(char ch) 当 ch 包含空白字符(通常是空格、水平制表符、回车符或换行符)时,返回 true。
  • static char to lower case(char ch)返回与 ch 的大写字母对应的小写字母;否则,该方法返回 ch 的值。
  • 静态 char toupper case(char ch)返回 ch 的小写字母的大写等值;否则,该方法返回 ch 的值。

例如, isDigit(ch) 优于 ch>= ’ 0 '&&ch<= ’ 9 ',因为它避免了错误的来源,可读性更好,并且对于非拉丁数字(例如, ‘\u0beb’ )以及拉丁数字返回 true。

浮点和双精度

Float 和 Double 分别在 Float 和 Double 对象中存储浮点和双精度浮点值。这些类声明下列常量:

  • MAX_VALUE 标识可表示为 float 或 double 的最大值。
  • MIN_VALUE 标识可表示为 float 或 double 的最小值。
  • NaN 代表 0.0F/0.0F 为浮动和 0.0/0.0 为双。
  • NEGATIVE_INFINITY 将-infinity 表示为浮点型或双精度型。
  • 正 _ 无穷大 表示+无穷大为浮点型或双精度型。

Float 和 Double 也声明了以下用于初始化其对象的构造函数:

  • Float(浮点值) 将 Float 对象初始化为值。
  • Float(double value) 将 Float 对象初始化为 float 等价于值。
  • Float(String s) 将 s 的文本转换为浮点值,并将该值存储在 Float 对象中。
  • Double(双精度值) 将 Double 对象初始化为值。
  • Double(String s)将 s 的文本转换为双精度浮点值,并将该值存储在 Double 对象中。

Float 的构造函数由 float floatValue() 补充,返回包装后的浮点值。类似地, Double 的构造函数由 double doubleValue() 补充,返回包装后的双精度浮点值。

Float 声明了除了 floatValue() 之外的几个实用方法。这些方法包括以下内容:

  • 静态 int floatToIntBits(浮点值) 将值转换为 32 位整数。
  • 静态布尔 isInfinite(float f) 当 f 的值为+infinity 或–infinity 时返回 true。当当前 Float 对象的值为+infinity 或-infinity 时,相关的布尔 is infinit()方法 返回 true。
  • 静态布尔 isNaN(float f) 当 f 的值为 NaN 时返回 true。当当前 Float 对象的值为 NaN 时,相关的布尔 isNaN() 方法返回 true。
  • 静态浮点 parseFloat(String s) 解析 s ,返回与 s 的浮点值的文本表示等价的浮点值,或者在该表示无效(例如包含字母)时抛出 Java . lang . numberformatexception。

Double 声明了几个实用方法以及 doubleValue() 。这些方法包括以下内容:

  • 静态 long doubleToLongBits(双精度值) 将值转换为长整型。
  • 静态布尔 isInfinite(double d) 当 d 的值为+infinity 或-infinity 时返回 true。当当前 Double 对象的值为+infinity 或-infinity 时,相关的布尔 is infinit()方法返回 true。
  • 静态布尔 isNaN(double d) 当 d 的值为 NaN 时返回 true。当当前 Double 对象的值为 NaN 时,相关的 public boolean isNaN() 方法返回 true。
  • 静态 double parse double(String s)解析 s ,返回与 s 的双精度浮点值的文本表示等效的双精度浮点值,或者在该表示无效时抛出 NumberFormatException 。

float pointbits()和 doubleToIntBits() 方法用于实现 equals() 和 hashCode() 方法,这些方法必须考虑 float 和 double 字段。 floatToIntBits() 和 doubleToIntBits() 允许 equals() 和 hashCode() 正确响应以下情况:

  • 当 f1 和 f2 包含浮点时, equals() 必须返回 true。NaN (或 d1 和 d2 包含双。南)。如果 equals() 以类似于 f1 . float value()= = F2 . float value()(或 D1 . double value()= = D2 . double value())的方式实现,该方法将返回 false,因为 NaN 不等于任何值,包括它本身。
  • 当 f1 包含+0.0 而 f2 包含-0.0 时(或反之亦然),或者 d1 包含+0.0 而 d2 包含-0.0 时(或反之亦然),必须返回 false。如果 equals() 以类似于 f1 . float value()= = F2 . float value()(或 D1 . double value()= = D2 . double value())的方式实现,该方法将返回 true,因为+0.0 = = 0.0 返回 true。

这些要求是基于散列的集合(在第九章中讨论)正常工作所必需的。清单 8-1 展示了它们如何影响 Float 和 Double s equals()方法。

清单 8-1 。演示了 NaN 上下文中的 Float 的 equals() 方法 和+/-0.0 上下文中的 Double 的 equals() 方法

public class FloatDoubleDemo
{
   public static void main(String[] args)
   {
      Float f1 = new Float(Float.NaN);
      System.out.println(f1.floatValue());
      Float f2 = new Float(Float.NaN);
      System.out.println(f2.floatValue());
      System.out.println(f1.equals(f2));
      System.out.println(Float.NaN == Float.NaN);
      System.out.println();
      Double d1 = new Double(+0.0);
      System.out.println(d1.doubleValue());
      Double d2 = new Double(-0.0);
      System.out.println(d2.doubleValue());
      System.out.println(d1.equals(d2));
      System.out.println(+0.0 == -0.0);
   }
}

编译清单 8-1(【FloatDoubleDemo.java】??【javac】)并运行这个应用( java FloatDoubleDemo )。下面的输出证明了 Float 的 equals() 方法正确处理 NaN, Double 的 equals() 方法正确处理+/-0.0:

NaN
NaN
true
false

0.0
-0.0
false
true

提示当你想测试一个 float 或 double 值是否等于+无穷大或无穷大(但不是两者都等于)时,不要使用 isInfinite() 。而是通过=与 NEGATIVE_INFINITY 或 POSITIVE_INFINITY 进行比较。例如, f == Float。负无穷大。

你会发现 parseFloat() 和 parseDouble() 在很多上下文中都很有用。例如,清单 8-2 使用 parseDouble() 将命令行参数解析成 double s。

清单 8-2 。将命令行参数解析为双精度浮点值

public class Calc
{
   public static void main(String[] args)
   {
      if (args.length != 3)
      {
         System.err.println("usage: java Calc value1 op value2");
         System.err.println("op is one of +, -, x, or /");
         return;
      }
      try
      {
         double value1 = Double.parseDouble(args[0]);
         double value2 = Double.parseDouble(args[2]);
         if (args[1].equals("+"))
            System.out.println(value1 + value2);
         else
         if (args[1].equals("-"))
            System.out.println(value1 - value2);
         else
         if (args[1].equals("x"))
            System.out.println(value1 * value2);
         else
         if (args[1].equals("/"))
            System.out.println(value1 / value2);
         else
            System.err.println("invalid operator: " + args[1]);
      }
      catch (NumberFormatException nfe)
      {
         System.err.println("Bad number format: " + nfe.getMessage());
      }
   }
}

指定 java Calc 10E+3 + 66.0 来试用 Calc 应用。这个应用通过输出 10066.0 来响应。如果您指定了 java Calc 10E+3 + A ,您将会看到错误的数字格式:对于输入字符串:【A】作为输出,这是对第二个 parseDouble() 方法调用抛出的 NumberFormatException 对象的响应。

尽管 NumberFormatException 描述了一个未检查的异常,尽管未检查的异常因为代表编码错误而经常不被处理,但是 NumberFormatException 在这个例子中不符合这个模式。这个异常不是由编码错误引起的;它源于有人向应用传递了非法的数字参数,这是无法通过正确的编码来避免的。也许 NumberFormatException 应该被实现为一个检查异常。

整数、长整型、短整型和字节型

Integer 、 Long 、 Short 和 Byte 分别在 Integer 、 Long 、 Short 和 Byte 对象中存储 32 位、64 位、16 位和 8 位的整数值。

每个类声明 MAX_VALUE 和 MIN_VALUE 常量,这些常量标识可以由其关联的原语类型表示的最大值和最小值。这些类还声明了以下用于初始化其对象的构造函数:

  • Integer(int value) 初始化 Integer 对象为值。
  • Integer(String s) 将 s 的文本转换为 32 位整数值,并将该值存储在 Integer 对象中。
  • Long(长值) 将 Long 对象初始化为值。
  • Long(String s) 将 s 的文本转换为 64 位整数值,并将该值存储在 Long 对象中。
  • Short(短值) 将 Short 对象初始化为值。
  • Short(String s) 将 s 的文本转换为 16 位整数值,并将该值存储在 Short 对象中。
  • 字节(字节值 ) 将字节对象初始化为值。
  • Byte(String s) 将 s 的文本转换为 8 位整数值,并将该值存储在 Byte 对象中。

整数的构造函数由 int intValue() , Long 的构造函数由 long longValue() , Short 的构造函数由 short shortValue() 补充, Byte 的构造函数由 Byte value()补充。这些方法返回包装的整数。

这些类声明了各种有用的面向整数的方法。例如, Integer 声明了以下类方法,用于根据特定的表示形式(二进制、十六进制、八进制和十进制)将 32 位整数转换为字符串:

  • 静态字符串 toBinaryString(int i) 返回一个字符串对象,包含 i 的二进制表示。例如,integer . tobinary String(255)返回一个包含 11111111 的字符串对象。
  • 静态字符串 toHexString(int i) 返回一个字符串对象,包含 i 的十六进制表示。例如, Integer.toHexString(255) 返回一个包含 ff 的字符串对象。
  • 静态字符串 toOctalString(int i) 返回一个字符串对象,包含 i 的八进制表示。例如, toOctalString(64) 返回一个包含 100 的字符串对象。
  • 静态字符串 toString(int i) 返回一个字符串对象,包含 i 的十进制表示。例如, toString(255) 返回一个包含 255 的字符串对象。

在二进制字符串前面加上零通常很方便,这样就可以在列中对齐多个二进制字符串。例如,您可能希望创建一个显示以下对齐输出的应用:

11110001
+
00000111
--------
11111000

可惜,【tobinary string()并没有让你完成这个任务。例如,integer . tobinary String(7)返回一个包含 111 而不是 00000111 的字符串对象。清单 8-3 的 toAlignedBinaryString()方法解决了这个疏忽。

清单 8-3 。对齐二进制字符串

public class AlignBinaryString
{
   public static void main(String[] args)
   {
      System.out.println(toAlignedBinaryString(7, 8));
      System.out.println(toAlignedBinaryString(255, 16));
      System.out.println(toAlignedBinaryString(255, 7));
   }

   static String toAlignedBinaryString(int i, int numBits)
   {
      String result = Integer.toBinaryString(i);
      if (result.length() > numBits)
         return null; // cannot fit result into numBits columns
      int numLeadingZeros = numBits - result.length();
      StringBuilder sb = new StringBuilder();
      for (int j = 0; j < numLeadingZeros; j++)
         sb.append('0');
      return sb.toString() + result;
   }
}

toAlignedBinaryString() 方法有两个参数:第一个参数指定要转换成二进制字符串的 32 位整数,第二个参数指定要容纳该字符串的位列数。

在调用 toBinaryString() 返回 i 的不带前导零的等价二进制字符串后, toAlignedBinaryString() 验证该字符串的数字是否能符合 numBits 指定的位列数。如果它们不匹配,这个方法返回 null。

继续, toAlignedBinaryString() 计算前置到结果的前导“0”的数量,然后使用 for 循环创建一串前导零。此方法通过返回结果字符串前面的前导零字符串来结束。

当您运行此应用时,它会生成以下输出:

00000111
0000000011111111
null

编号

每个浮点型、双精度型、整型、长型、短型、字节型除了自己的xValue()方法外,还提供了其他类的xValue()方法。例如, Float 提供了 doubleValue() , intValue() , longValue() , shortValue() , byteValue() 以及 floatValue() 。

这六个方法都是 Number 的成员,它是 Float 、 Double 、 Integer 、 Long 、 Short 和 Byte — Number 的 floatValue() 、 doubleValue() 、 intValue() 和 Number 也是 java.math.BigDecimal 和 java.math.BigInteger 的超类(还有一些并发相关的类;参见第十章)。

Number 的存在是为了简化对一组 Number 子类对象的迭代。例如,可以声明一个 Java . util . list类型的变量,并将其初始化为 Java . util . ArrayList的实例。然后,您可以在集合中存储一组 Number subclass 对象,并通过多态地调用一个子类方法来迭代这个集合。

探索线程

应用通过线程执行,这些线程是应用代码的独立执行路径。当多个线程正在执行时,每个线程的路径可以不同于其他线程的路径。例如,一个线程可能执行 switch 语句的一个案例,而另一个线程可能执行该语句的另一个案例。

注意应用使用线程来提高性能。一些应用可以只使用默认主线程(执行 main() 方法的线程)来执行它们的任务,但是其他应用需要额外的线程来在后台执行时间密集型任务,以便它们保持对用户的响应。

虚拟机为每个线程提供了自己的方法调用堆栈,以防止线程相互干扰。独立的堆栈让线程能够跟踪它们要执行的下一条指令,这些指令可能因线程而异。堆栈还为线程提供自己的方法参数、局部变量和返回值的副本。

Java 通过其线程 API 支持线程。这个 API 由 java.lang 包中的一个接口( Runnable )和四个类(线程、线程组、 ThreadLocal 和 InheritableThreadLocal )组成。在探索了可运行和线程(并且在本次探索中提到了线程组之后,在本节中,我将探索线程同步、线程本地和可继承线程本地。

注意 Java 5 引入了 java.util.concurrent 包作为低级线程 API 的高级替代。(我将在第十章中讨论这个包。)尽管 java.util.concurrent 是处理线程的首选 API,但您也应该对线程有所了解,因为它在简单的线程场景中很有帮助。此外,您可能需要分析其他人依赖于线程的源代码。

可运行线程

Java 提供了 Runnable 接口来标识那些为线程提供代码的对象,线程通过这个接口的唯一的 void run() 方法 来执行——线程不接收任何参数,也不返回值。类实现了 Runnable 来提供这段代码,其中一个类就是线程。

线程为底层操作系统的线程架构提供一致的接口。(操作系统通常负责创建和管理线程。)线程使得将代码与线程相关联以及启动和管理那些线程成为可能。每个线程实例关联一个单独的线程。

线程声明了几个用于初始化线程对象的构造函数。其中一些构造函数采用了 Runnable 参数:你可以提供代码来运行,而不必扩展线程。其他构造函数不采用 Runnable 参数:你必须扩展线程并覆盖它的 run() 方法 来提供运行的代码。

例如, Thread(Runnable runnable) 将一个新的 Thread 对象初始化为指定的 runnable ,其代码将被执行。相反, Thread() 不会将线程初始化为 Runnable 参数。相反,您的 Thread 子类提供了一个调用 Thread() 的构造函数,并且该子类还覆盖了 Thread 的 run() 方法。

在没有显式名称参数的情况下,每个构造函数都会给线程对象分配一个唯一的默认名称(以线程- 开始)。名字使得区分线程成为可能。与选择默认名称的前两个构造函数不同, Thread(String threadName) 允许您指定自己的线程名称。

Thread 也声明了启动和管理线程的方法。表 8-1 描述了许多更有用的方法。

表 8-1 。线程方法

方法描述
静态线程当前线程()返回与调用这个方法的线程相关联的线程对象。
String getName()返回与这个线程对象相关联的名称。
螺纹。状态 getState()状态返回与这个线程对象相关的线程的状态。状态由线程标识。将 enum 状态设置为阻塞(等待获取锁,稍后讨论)、新(已创建但未启动)、可运行(正在执行)、终止(线程已死亡)、 TIMED_WAITING (等待经过指定的时间量)或等待(无限期等待)。
无效中断()在这个线程对象中设置中断状态标志。如果相关线程被阻塞或正在等待,清除该标志,并通过抛出 Java . lang . interrupted exception 类的实例来唤醒线程。
静布尔中断()当与此线程对象相关联的线程有挂起的中断请求时,返回 true。清除中断状态标志。
boolean is live()返回 true 表示这个线程对象的关联线程是活动的而不是死的。一个线程的生命周期从它在 start() 方法中实际启动之前,到它离开 run() 方法之后,在这一点上它就死了。
布尔 isDaemon()当与这个线程对象相关联的线程是一个守护线程时返回 true,这个线程充当一个用户线程(非守护线程)的助手,并且当应用的最后一个非守护线程终止时自动终止,以便应用可以退出。
布尔 is interrupted()当与此线程对象相关的线程有挂起的中断请求时,返回 true。
void join()在这个线程对象上调用这个方法的线程等待与这个对象相关的线程死亡。当这个线程对象的中断()方法被调用时,这个方法抛出中断异常。
无效加入(长毫)在这个线程对象上调用这个方法的线程等待与这个对象相关联的线程死亡,或者直到经过了毫秒毫秒,以先发生的为准。当这个线程对象的 interrupt() 方法被调用时,这个方法抛出 InterruptedException 。
void setdaemon(boolean isdaemon)??]当 isDaemon 为真时,将此线程对象的关联线程标记为守护线程。当线程尚未创建和启动时,该方法抛出 Java . lang . illegalthreadstateexception。
空集名(字符串线程名)将 threadName 的值赋给这个线程对象,作为其关联线程的名称。
静虚空睡眠(长时间)暂停与这个线程对象相关的线程时间毫秒。当这个线程对象的 interrupt() 方法在线程休眠时被调用时,这个方法抛出 InterruptedException 。
虚空开始()创建并启动这个线程对象的关联线程。当线程先前被启动并且正在运行或者已经死亡时,该方法抛出 IllegalThreadStateException。

清单 8-4 通过一个 main() 方法 向您介绍线程 API,该方法演示了 Runnable 、 Thread(Runnable runnable) 、 currentThread() 、 getName() 和 start() 。

清单 8-4 。一对计数线

public class CountingThreads
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         String name = Thread.currentThread().getName();
                         int count = 0;
                         while (true)
                            System.out.println(name + ": " + count++);
                      }
                   };
      Thread thdA = new Thread(r);
      Thread thdB = new Thread(r);
      thdA.start();
      thdB.start();
   }
}

根据清单 8-4 ,执行 main() 的默认主线程首先实例化一个实现 Runnable 的匿名类。然后它创建两个线程对象,将每个对象初始化为 runnable,并调用线程的 start() 方法 来创建并启动两个线程。完成这些任务后,主线程退出 main() 并死亡。

两个启动的线程中的每一个都执行 runnable 的 run() 方法。它调用线程的 currentThread() 方法以获取其关联的线程实例,使用该实例调用线程的 getName() 方法以返回其名称,将 count 初始化为 0,并进入一个无限循环,在该循环中,它输出 name 和 count ,并在每次执行时递增 count

提示要停止一个没有结束的应用,在 Windows 平台上同时按下 Ctrl 和 C 键,或者在非 Windows 平台上执行相同的操作。

当我在 64 位 Windows 7 平台上运行这个应用时,我观察到两个线程在执行过程中交替出现。一次运行的部分输出如下所示:

Thread-0: 0
Thread-0: 1
Thread-1: 0
Thread-0: 2
Thread-1: 1
Thread-0: 3
Thread-1: 2
Thread-0: 4
Thread-1: 3
Thread-0: 5
Thread-1: 4
Thread-0: 6
Thread-1: 5
Thread-0: 7
Thread-1: 6
Thread-1: 7
Thread-1: 8
Thread-1: 9
Thread-1: 10
Thread-1: 11
Thread-1: 12

注意我执行了 Java count threads>output . txt 将输出捕获到 output.txt 中,然后呈现了之前这个文件的部分内容。将输出捕获到文件可能会显著影响输出,否则如果没有捕获输出,将会观察到输出。因为我在本节中展示了捕获的线程输出,所以在您的平台上执行应用时要记住这一点。另外,请注意,您平台的线程架构可能会影响可观察到的结果。我已经在 64 位 Windows 7 平台上测试了本节中的每个示例。

当计算机拥有足够多的处理器和/或处理器内核时,计算机的操作系统会为每个处理器或内核分配一个单独的线程,以便线程同时执行*(同时)。当计算机没有足够的处理器和/或内核时,线程必须等待轮到它使用共享的处理器/内核。*

*操作系统使用一个调度器(en . Wikipedia . org/wiki/Scheduling _(computing来决定一个等待线程何时执行。下表列出了三种不同的调度程序:

注意虽然前面的输出表明第一个线程( Thread-0 )开始执行,但是千万不要假设与 Thread 对象相关联的线程是第一个执行的线程,该对象的 start() 方法被首先调用。尽管这可能适用于某些调度程序,但可能不适用于其他调度程序。

一个多级反馈队列和许多其他线程调度器考虑了优先级(线程相对重要性)的概念。他们经常将抢占式调度(优先级较高的线程抢占——中断并运行,而不是——优先级较低的线程)与循环调度(优先级相等的线程被给予相等的时间片,这些时间片被称为时间片,轮流执行)。

线程通过其 void set priority(int priority)方法支持优先级(将该线程对象的线程优先级设置为优先级,其范围为线程。最小优先级到线程。最大优先级 — 线程。NORMAL_PRIORITY 标识默认优先级)和 int getPriority() 方法(返回当前优先级)。

注意使用 setPriority() 方法会影响应用跨平台的可移植性,因为不同的调度程序可以用不同的方式处理优先级的变化。例如,一个平台的调度程序可能会延迟低优先级线程的执行,直到高优先级线程完成。这种延迟会导致无限期推迟饥饿,因为优先级较低的线程在无限期等待执行时会“饥饿”,这会严重损害应用的性能。另一个平台的调度程序可能不会无限期地延迟较低优先级的线程,从而提高应用的性能。

清单 8-5 重构 清单 8-4 的 main() 方法,给每个线程一个非默认的名字,并在输出 name 和 count 后让每个线程休眠。

清单 8-5 。重温一对计数线程

public class CountingThreads
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         String name = Thread.currentThread().getName();
                         int count = 0;
                         while (true)
                         {
                            System.out.println(name + ": " + count++);
                            try
                            {
                               Thread.sleep(100);
                            }
                            catch (InterruptedException ie)
                            {
                            }
                         }
                      }
                   };
      Thread thdA = new Thread(r);
      thdA.setName("A");
      Thread thdB = new Thread(r);
      thdB.setName("B");
      thdA.start();
      thdB.start();
   }
}

清单 8-5 揭示线程 A 和 B 执行 thread . sleep(100); 休眠 100 毫秒。这种休眠会导致每个线程更频繁地执行,如以下部分输出所示:

A: 0
B: 0
A: 1
B: 1
B: 2
A: 2
B: 3
A: 3
B: 4
A: 4
B: 5
A: 5
B: 6
A: 6
B: 7
A: 7

一个线程偶尔会启动另一个线程来执行冗长的计算、下载大文件或执行其他一些耗时的活动。在完成其他任务后,启动了工作线程的线程准备好处理工作线程的结果,并等待工作线程完成和终止。

可以通过使用 while 循环来等待工作线程死亡,该循环在工作线程的线程对象上重复调用线程的 isAlive() 方法 ,并在该方法返回 true 时休眠一段时间。然而,清单 8-6 展示了一个不太冗长的替代方案: join() 方法 。

清单 8-6 。将默认主线程与后台线程结合

public class JoinDemo
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         System.out.println("Worker thread is simulating " +
                                            "work by sleeping for 5 seconds.");
                         try
                         {
                            Thread.sleep(5000);
                         }
                         catch (InterruptedException ie)
                         {
                         }
                         System.out.println("Worker thread is dying.");
                      }
                   };
      Thread thd = new Thread(r);
      thd.start();
      System.out.println("Default main thread is doing work.");
      try
      {
         Thread.sleep(2000);
      }
      catch (InterruptedException ie)
      {
      }
      System.out.println("Default main thread has finished its work.");
      System.out.println("Default main thread is waiting for worker thread " +
                         "to die.");
      try
      {
         thd.join();
      }
      catch (InterruptedException ie)
      {
      }
      System.out.println("Main thread is dying.");
   }
}

清单 8-6 展示了默认的主线程启动一个工作线程,执行一些工作,然后通过工作线程的 thd 对象调用 join() 来等待工作线程死亡。当您运行此应用时,您将发现类似如下的输出(消息顺序可能会有所不同):

Default main thread is doing work.
Worker thread is simulating work by sleeping for 5 seconds.
Default main thread has finished its work.
Default main thread is waiting for worker thread to die.
Worker thread is dying.
Main thread is dying.

每个线程对象都属于某个线程组对象;线程声明了一个 Thread group getThreadGroup()方法 返回这个对象。您应该忽略线程组,因为它们并不那么有用。如果你需要逻辑分组线程对象,你应该使用数组或者集合。

注意各种线程组方法有缺陷。例如,int enumerate(Thread[]threads)在其 threads 数组参数太小而无法存储其 Thread 对象时,不会在其枚举中包含所有活动线程。虽然您可能认为可以使用来自 int activeCount() 方法的返回值来适当地调整这个数组的大小,但是不能保证这个数组足够大,因为 activeCount() 的返回值随着线程的创建和死亡而波动。

然而,您仍然应该了解线程组,因为它在处理线程执行时抛出的异常方面做出了贡献。清单 8-7 通过呈现一个 run() 方法试图将一个整数除以 0,这导致抛出一个 Java . lang . arithmetic exception 实例,为学习异常处理打下了基础。

清单 8-7 。从 run() 方法中抛出异常

public class ExceptionThread
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         int x = 1 / 0; // Line 10
                      }
                   };
      Thread thd = new Thread(r);
      thd.start();
   }
}

运行这个应用,您将看到一个异常跟踪,它标识了抛出的算术异常 :

Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
                at ExceptionThread$1.run(ExceptionThread.java:10)
                at java.lang.Thread.run(Unknown Source)

当从 run() 方法中抛出异常时,线程终止,并发生以下活动:

  • 虚拟机寻找线程的实例。通过线程的 void setUncaughtExceptionHandler 安装。UncaughtExceptionHandler eh) 方法。当找到这个处理程序时,它将执行传递给实例的 void uncaughtException(Thread t,Throwable e) 方法,其中 t 标识抛出异常的线程的 Thread 对象,而 e 标识抛出的异常或错误——可能是抛出了一个 Java . lang . outofmemory error 实例。如果该方法抛出异常/错误,虚拟机将忽略该异常/错误。
  • 假设 setUncaughtExceptionHandler()没有被调用来安装一个处理程序,虚拟机将控制权传递给关联的 ThreadGroup 对象的 uncaughtException(Thread t,Throwable e) 方法。假设线程组没有被扩展,并且其 uncaughtException() 方法没有被覆盖来处理异常,当父线程组存在时, uncaughtException() 将控制传递给父线程组对象的 uncaughtException() 方法。否则,它检查是否已经安装了默认的未捕获异常处理程序(通过线程的静态 void setDefaultUncaughtExceptionHandler(线程。UncaughtExceptionHandler 处理程序)方法)。如果已经安装了一个默认的未捕获异常处理程序,那么它的 uncaughtException() 方法将使用相同的两个参数来调用。否则, uncaughtException() 检查其 Throwable 参数,以确定它是否是 java.lang.ThreadDeath 的实例。如果是,则不做任何特殊处理。否则,如清单 8-7 的异常消息所示,使用 Throwable 参数的 printStackTrace() 方法,从线程的 getName() 方法返回的包含线程名称和堆栈回溯的消息将被打印到标准错误流中。

清单 8-8 演示了线程的 setUncaughtExceptionHandler()和 setDefaultUncaughtExceptionHandler()方法。

清单 8-8 。演示未捕获的异常处理程序

public class ExceptionThread
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         int x = 1 / 0;
                      }
                   };
      Thread thd = new Thread(r);
      Thread.UncaughtExceptionHandler uceh;
      uceh = new Thread.UncaughtExceptionHandler()
             {
                @Override
                public void uncaughtException(Thread t, Throwable e)
                {
                   System.out.println("Caught throwable " + e + " for thread "
                                      + t);
                }
             };
      thd.setUncaughtExceptionHandler(uceh);
      uceh = new Thread.UncaughtExceptionHandler()
             {
                @Override
                public void uncaughtException(Thread t, Throwable e)
                {
                   System.out.println("Default uncaught exception handler");
                   System.out.println("Caught throwable " + e + " for thread "
                                      + t);
                }
             };
      thd.setDefaultUncaughtExceptionHandler(uceh);
      thd.start();
   }
}

当您运行此应用时,您将观察到以下输出:

Caught throwable java.lang.ArithmeticException: / by zero for thread Thread[Thread-0,5,main]

您也不会看到默认的未捕获异常处理程序的输出,因为默认处理程序没有被调用。要看到那个输出,必须注释掉 thd . setuncaughtexceptionhandler(uceh);。如果还注释掉 thd . setdefaultuncaughtexceptionhandler(uceh);,你会看到清单 8-7 的输出。

注意 线程声明了几个不推荐使用的方法,包括 stop() (停止一个正在执行的线程)。这些方法已被弃用,因为它们不安全。不要使用这些被否决的方法。(我将在本章的后面向你展示如何安全地停止一个线程。)此外,您应该避免使用 static void yield() 方法,该方法旨在将执行从当前线程切换到另一个线程,因为它会影响可移植性并损害应用性能。尽管 yield() 可能会切换到某些平台上的另一个线程(这可以提高性能),但 yield() 可能只会返回到其他平台上的当前线程(这会损害性能,因为 yield() 调用只是浪费了时间)。* *线程同步

在整个执行过程中,每个线程都与其他线程相隔离,因为每个线程都有自己的方法调用堆栈。然而,当线程访问和操作共享数据时,它们仍然会相互干扰。这种干扰会破坏共享数据,这种破坏会导致应用失败。

例如,考虑一个丈夫和妻子共同使用的支票账户。假设夫妻双方同时决定清空这个账户,而不知道对方也在做同样的事情。清单 8-9 展示了这个场景。

清单 8-9 。一个有问题的支票账户

public class CheckingAccount
{
   private int balance;

   public CheckingAccount(int initialBalance)
   {
      balance = initialBalance;
   }

   public boolean withdraw(int amount)
   {
      if (amount <= balance)
      {
         try
         {
            Thread.sleep((int) (Math.random() * 200));
         }
         catch (InterruptedException ie)
         {
         }
         balance -= amount;
         return true;
      }
      return false;
   }

   public static void main(String[] args)
   {
      final CheckingAccount ca = new CheckingAccount(100);
      Runnable r = new Runnable()
                   {
                      public void run()
                      {
                         String name = Thread.currentThread().getName();
                         for (int i = 0; i < 10; i++)
                             System.out.println (name + " withdraws $10: " +
                                                 ca.withdraw(10));
                      }
                   };
      Thread thdHusband = new Thread(r);
      thdHusband.setName("Husband");
      Thread thdWife = new Thread(r);
      thdWife.setName("Wife");
      thdHusband.start();
      thdWife.start();
   }
}

这个应用允许提取比账户中可用金额更多的钱。例如,以下输出显示,当只有 100 美元可用时,提取了 110 美元:

Wife withdraws $10: true
Husband withdraws $10: true
Husband withdraws $10: true
Wife withdraws $10: true
Wife withdraws $10: true
Husband withdraws $10: true
Wife withdraws $10: true
Wife withdraws $10: true
Wife withdraws $10: true
Husband withdraws $10: true
Husband withdraws $10: false
Husband withdraws $10: false
Husband withdraws $10: false
Husband withdraws $10: false
Husband withdraws $10: false
Husband withdraws $10: false
Wife withdraws $10: true
Wife withdraws $10: false
Wife withdraws $10: false
Wife withdraws $10: false

提取的钱比可用于提取的钱多的原因是在丈夫和妻子线程之间存在竞争条件。

注意竞争条件是指多个线程同时或几乎同时更新同一个对象。对象的一部分存储由一个线程写入的值,对象的另一部分存储由另一个线程写入的值。

竞争条件的存在是因为检查取款金额以确保其少于余额中出现的金额并从余额中扣除该金额的操作不是原子(不可分割)操作。(虽然原子是可分的,但 atomic 通常用来指不可分的东西。)

注意thread . sleep()方法调用睡眠时间不定(最长可达 199 毫秒),这样您可以观察到提取的钱比可提取的钱多。如果没有这个方法调用,您可能需要执行应用数百次(或更多次)才能看到这个问题,因为调度程序可能很少会在 amount < = balance 表达式和 balance -= amount 之间暂停线程;表达式语句—代码快速执行。

考虑以下场景:

  • 丈夫线程执行取款()的金额< =余额表达式,返回 true。调度程序暂停丈夫线程,让妻子线程执行。
  • 老婆线程执行取款()的金额< =余额表达式,返回 true。
  • 妻子线程执行撤回。调度程序暂停妻子线程,让丈夫线程执行。
  • 丈夫线程执行撤回。

这个问题可以通过同步对 retract()的访问来解决,这样一次只有一个线程可以在这个方法中执行。通过在方法的返回类型之前向方法头添加保留字 synchronized ,可以在方法级别同步访问,例如,synchronized boolean retract(int amount)。

正如我稍后演示的,您还可以通过指定 synchronized(object){/* synchronized statements /}来同步对语句块的访问,其中 object 是一个任意的对象引用。在执行离开方法/块之前,任何线程都不能进入同步的方法或块;这就是所谓的互斥*。

同步是根据监视器和锁实现的。一个监视器是一个并发结构,用于控制对一个临界区的访问,这是一个必须自动执行的代码区域。它在源代码级别被标识为同步方法或同步块。

是一个令牌,在监视器允许线程在监视器的临界区内执行之前,该线程必须获得该令牌。当线程退出监视器时,令牌被自动释放,以便给另一个线程一个获取令牌并进入监视器的机会。

注意一个已经获得锁的线程在调用线程的 sleep() 方法之一时不会释放这个锁。

进入同步实例方法的线程获取与调用该方法的对象相关联的锁。进入同步类方法的线程获取与该类的 java.lang.Class 对象相关联的锁。最后,进入同步块的线程获得与该块的控制对象相关联的锁。

提示 线程声明一个静态布尔 holdsLock(Object o) 方法,当调用线程持有对象 o 上的监视器锁时,该方法返回 true。你会发现这个方法在断言语句中很方便,比如 assert thread . holds lock(o);。

同步的需求通常是微妙的。例如,清单 8-10 的 ID 工具类声明了一个 getNextID() 方法 ,该方法返回一个唯一的基于 long 的 ID,可能会在生成唯一的文件名时使用。尽管您可能不这么认为,但此方法可能会导致数据损坏并返回重复值。

清单 8-10 。用于返回唯一 id 的工具类

class ID
{
   private static long nextID = 0;
   static long getNextID()
   {
      return nextID++;
   }
}

getNextID() 有两个不同步问题。因为 32 位虚拟机实现需要两步来更新一个 64 位长的整数,所以将 1 加到 nextID 不是原子的:调度程序可能会中断一个只更新了一半 nextID 的线程,这会破坏这个变量的内容。

注意: long 和 double 类型的变量在 32 位虚拟机上的非同步上下文中被写入时会遭到破坏。对于类型为 boolean 、 byte 、 char 、 float 、 int 或 short 的变量,不会出现这个问题;每种类型占用 32 位或更少。

假设多线程调用 getNextID() 。因为 postincrement ( ++ )分两步读取和写入 nextID 字段,所以多个线程可能会检索相同的值。例如,线程 A 执行 ++ ,读取 nextID ,但在被调度器中断之前不递增其值。线程 B 现在执行并读取相同的值。

这两个问题都可以通过同步访问 nextID 来解决,这样只有一个线程可以执行这个方法的代码。所需要做的就是将 synchronized 添加到方法头的方法返回类型之前,例如,static synchronized int get nextid()。

同步也用于线程之间的通信。例如,你可以设计自己的机制来停止一个线程(因为你不能使用线程的不安全 stop() 方法来完成这个任务)。清单 8-11 展示了如何完成这项任务。

清单 8-11 。试图停止线程

public class ThreadStopping
{
   public static void main(String[] args)
   {
      class StoppableThread extends Thread
      {
         private boolean stopped = false;

         @Override
         public void run()
         {
            while(!stopped)
              System.out.println("running");
         }

         void stopThread()
         {
            stopped = true;
         }
      }
      StoppableThread thd = new StoppableThread();
      thd.start();
      try
      {
         Thread.sleep(1000); // sleep for 1 second
      }
      catch (InterruptedException ie)
      {
      }
      thd.stopThread();
   }
}

清单 8-11 引入了一个 main() 方法,它带有一个名为 StoppableThread 的局部类,该局部类子类化 Thread 。 StoppableThread 声明了一个初始化为 false 的 stopped 字段,一个将该字段设置为 true 的 stopThread() 方法,以及一个 run() 方法,该方法的无限循环在每次循环迭代中检查 stopped 以查看其值是否已更改为 true 。

实例化 StoppableThread 后,默认主线程启动与这个线程对象关联的线程。然后它休眠一秒钟,并在死亡前调用 stoppeablethread 的 stop() 方法。当您在单处理器/单核机器上运行这个应用时,您可能会看到应用停止了。当应用在多处理器计算机或具有多个内核的单处理器计算机上运行时,您可能看不到这种停止。出于性能原因,每个处理器或内核可能都有自己的高速缓存(本地化的高速内存),并有自己的停止副本。当一个线程修改这个字段的副本时,另一个线程的停止的副本不会改变。

清单 8-12 重构清单 8-11 以保证应用能在各种机器上正确运行。

清单 8-12 。在多处理器/多核机器上保证停止

public class ThreadStopping
{
   public static void main(String[] args)
   {
      class StoppableThread extends Thread
      {
         private boolean stopped = false;

         @Override
         public void run()
         {
            while(!isStopped())
              System.out.println("running");
         }

         synchronized void stopThread()
         {
            stopped = true;
         }

         private synchronized boolean isStopped()
         {
            return stopped;
         }
      }
      StoppableThread thd = new StoppableThread();
      thd.start();
      try
      {
         Thread.sleep(1000); // sleep for 1 second
      }
      catch (InterruptedException ie)
      {
      }
      thd.stopThread();
   }
}

清单 8-12 的 stopThread() 和 ISS stop()方法 同步支持线程通信(在调用 stopThread() 的默认主线程和在 run() 内部执行的启动线程之间)。当线程进入这些方法之一时,它保证访问 stopped 字段的单个共享副本(不是缓存副本)。

同步是支持互斥或者互斥结合线程通信所必需的。然而,当唯一的目的是在线程之间通信时,存在同步的替代方案。这种选择是保留字易变的,这在清单 8-13 中有演示。

清单 8-13 。线程通信同步的可变替代

public class ThreadStopping
{
   public static void main(String[] args)
   {
      class StoppableThread extends Thread
      {
         private volatile boolean stopped = false;

         @Override
         public void run()
         {
            while(!stopped)
              System.out.println("running");
         }

         void stopThread()
         {
            stopped = true;
         }
      }
      StoppableThread thd = new StoppableThread();
      thd.start();
      try
      {
         Thread.sleep(1000); // sleep for 1 second
      }
      catch (InterruptedException ie)
      {
      }
      thd.stopThread();
   }
}

清单 8-13 声明停止为易变;访问该字段的线程将始终访问单个共享副本(而不是多处理器/多核机器上的缓存副本)。除了生成不太冗长的代码之外, volatile 可能会提供比同步更好的性能。

当一个字段被声明为易变时,它也不能被声明为最终。如果你依赖于波动性的语义(意思),你仍然可以从 final 字段中得到这些。要了解更多信息,请查阅 Brian Goetz 的“Java 理论和实践:修复 Java 内存模型,第二部分”文章(www.ibm.com/developerworks/library/j-jtp03304/)。

注意仅在线程通信上下文中使用 volatile 。此外,您只能在字段声明的上下文中使用该保留字。虽然您可以声明 double 和 long 字段 volatile ,但是您应该避免在 32 位虚拟机上这样做,因为访问一个 double 或 long 变量的值需要两次操作,并且需要通过同步互斥来安全地访问它们的值。

java.lang.Object 的 wait() 、 notify() 和 notifyAll() 方法支持一种线程通信形式,其中一个线程主动等待某个条件(继续执行的先决条件)出现,此时另一个线程通知等待线程它可以继续执行。 wait() 使其调用线程等待一个对象的监视器, notify() 和 notifyAll() 唤醒一个或所有等待监视器的线程。

注意因为 wait() 、 notify() 和 notifyAll() 方法依赖于锁,所以不能从同步方法或同步块的外部调用它们。如果您没有注意到这个警告,您将会遇到一个抛出的 Java . lang . illegalmonitorstateexception 类的实例。此外,一个已经获得锁的线程在调用对象的 wait() 方法之一时释放这个锁。

涉及条件的线程通信的一个经典例子是生产者线程和消费者线程之间的关系。生产者线程产生将由消费者线程消费的数据项。每个产生的数据项都存储在一个共享变量中。

假设线程没有通信,并且以不同的速度运行。生产者可能会生成一个新的数据项,并在消费者检索前一个数据项进行处理之前将其记录在共享变量中。此外,消费者可能会在生成新的数据项之前检索共享变量的内容。

为了克服这些问题,生产者线程必须等待,直到它被通知先前产生的数据项已经被消费,并且消费者线程必须等待,直到它被通知新的数据项已经被产生。清单 8-14 向您展示了如何通过 wait() 和 notify() 来完成这个任务。

清单 8-14 。生产者-消费者关系

public class PC
{
   public static void main(String[] args)
   {
      Shared s = new Shared();
      new Producer(s).start();
      new Consumer(s).start();
   }
}

class Shared
{
   private char c = '\u0000';
   private boolean writeable = true;

   synchronized void setSharedChar(char c)
   {
      while (!writeable)
         try
         {
            wait();
         }
         catch (InterruptedException e) {}
      this.c = c;
      writeable = false;
      notify();
   }

   synchronized char getSharedChar()
   {
      while (writeable)
         try
         {
            wait();
         }
         catch (InterruptedException e) {}
      writeable = true;
      notify();
      return c;
   }
}

class Producer extends Thread
{
   private Shared s;

   Producer(Shared s)
   {
      this.s = s;
   }

   @Override
   public void run()
   {
      for (char ch = 'A'; ch <= 'Z'; ch++)
      {
         synchronized(s)
         {
            s.setSharedChar(ch);
            System.out.println(ch + " produced by producer.");
         }
      }
   }
}
class Consumer extends Thread
{
   private Shared s;

   Consumer(Shared s)
   {
      this.s = s;
   }

   @Override
   public void run()
   {
      char ch;
      do
      {
         synchronized(s)
         {
            ch = s.getSharedChar();
            System.out.println(ch + " consumed by consumer.");
         }
      }
      while (ch != 'Z');
   }
}

这个应用创建了一个共享的对象和两个获取对象引用副本的线程。生产者调用对象的 setSharedChar() 方法 保存 26 个大写字母中的每一个;消费者调用对象的 getSharedChar() 方法 来获取每个字母。

可写实例字段跟踪两个条件:生产者等待消费者消费数据项,消费者等待生产者产生新的数据项。它有助于协调生产者和消费者的执行。下面的场景说明了这种协调,在该场景中,使用者首先执行:

  1. 消费者执行 s.getSharedChar() 来检索一封信。
  2. 在这个同步方法中,消费者调用 wait() ,因为 writeable 包含 true。消费者现在一直等到收到来自生产者的通知。
  3. 生产者最终执行 s . setsharedchar(ch);。
  4. 当生产者进入同步方法时(这是可能的,因为消费者在等待之前释放了 wait() 方法中的锁),生产者发现可写的值为真,并且不调用 wait() 。
  5. 生产者保存角色,将可写设置为假(这将导致生产者等待下一个 setSharedChar() 调用,此时消费者尚未消费角色),并调用 notify() 来唤醒消费者(假设消费者正在等待)。
  6. 生产者退出 setSharedChar(char c) 。
  7. 消费者醒来(并重新获得锁),将可写设置为真(这将导致消费者等待下一个 getSharedChar() 调用,此时生产者还没有产生一个字符),通知生产者唤醒该线程(假设生产者正在等待),并返回共享字符。

尽管同步工作正常,但您可能会观察到输出(在某些平台上)在消费消息之前显示多个生产消息。例如,您可能会看到由 producer 制作的 A。之后是由制片人制作的 B。,其次是 A 所消费的消费者。开始时应用的输出。

这种奇怪的输出顺序是由对 setSharedChar() 的调用及其伴随的 System.out.println() 方法调用不是原子的,以及对 getSharedChar() 的调用及其伴随的 System.out.println() 方法调用不是原子的。通过将这些方法调用对中的每一个包装在同步块中来纠正输出顺序,该同步块在由 s 引用的共享对象上同步。

当您运行这个应用时,它的输出应该总是以相同的交替顺序出现,如下所示(为了简洁起见,只显示了前几行):

A produced by producer.
A consumed by consumer.
B produced by producer.
B consumed by consumer.
C produced by producer.
C consumed by consumer.
D produced by producer.
D consumed by consumer.

注意永远不要在循环之外调用 wait() 。循环测试条件(!可写或可写)在 wait() 调用之前和之后。在调用 wait() 之前测试条件可以确保的活性。如果该测试不存在,并且如果条件成立并且在调用 wait() 之前调用了 notify() ,则等待线程不太可能会被唤醒。调用 wait() 后重新测试条件确保安全。如果重新测试没有发生,并且如果在线程从 wait() 调用中唤醒后条件不成立(当条件不成立时,可能另一个线程偶然调用了 notify() ),线程将继续破坏锁的受保护不变量。

太多的同步可能会有问题。如果不小心的话,您可能会遇到这样的情况:锁被多个线程获取,没有一个线程持有自己的锁,而是持有其他线程所需的锁,没有一个线程能够进入并在以后退出其临界区以释放其持有的锁,因为其他线程持有该临界区的锁。清单 8-15 的非典型例子演示了这个场景,它被称为死锁

清单 8-15 。僵局的病态案例

public class DeadlockDemo
{
   private Object lock1 = new Object();
   private Object lock2 = new Object();

   public void instanceMethod1()
   {
      synchronized(lock1)
      {
         synchronized(lock2)
         {
            System.out.println("first thread in instanceMethod1");
            // critical section guarded first by
            // lock1 and then by lock2
         }
      }
   }

   public void instanceMethod2()
   {
      synchronized(lock2)
      {
         synchronized(lock1)
         {
            System.out.println("second thread in instanceMethod2");
            // critical section guarded first by
            // lock2 and then by lock1
         }
      }
   }

   public static void main(String[] args)
   {
      final DeadlockDemo dld = new DeadlockDemo();
      Runnable r1 = new Runnable()
                    {
                       @Override
                       public void run()
                       {
                          while(true)
                          {
                             dld.instanceMethod1();
                             try
                             {
                                Thread.sleep(50);
                             }
                             catch (InterruptedException ie)
                             {
                             }
                          }
                       }
                    };
      Thread thdA = new Thread(r1);
      Runnable r2 = new Runnable()
                    {
                       @Override
                       public void run()
                       {
                          while(true)
                          {
                             dld.instanceMethod2();
                             try
                             {
                                Thread.sleep(50);
                             }
                             catch (InterruptedException ie)
                             {
                             }
                          }
                        }
                    };
      Thread thdB = new Thread(r2);
      thdA.start();
      thdB.start();
   }
}

清单 8-15 的线程 A 和线程 B 分别在不同的时间调用 instanceMethod1() 和 instanceMethod2() ,。考虑以下执行顺序:

  1. 线程 A 调用 instanceMethod1() ,获取分配给 lock1 引用对象的锁,并进入其外部临界段(但尚未获取分配给 lock2 引用对象的锁)。
  2. 线程 B 调用 instanceMethod2() ,获取分配给 lock2 引用对象的锁,并进入其外部临界段(但尚未获取分配给 lock1 引用对象的锁)。
  3. 线程 A 试图获取与锁 2 相关联的锁。虚拟机强制线程在内部临界区之外等待,因为线程 B 持有该锁。
  4. 线程 B 试图获取与 lock1 关联的锁。虚拟机强制线程在内部临界区之外等待,因为线程 A 持有该锁。
  5. 两个线程都无法继续,因为另一个线程持有所需的锁。您遇到了死锁情况,程序(至少在两个线程的上下文中)冻结了。

尽管前面的例子清楚地标识了死锁状态,但是检测死锁通常并不容易。例如,您的代码可能包含不同类之间的以下循环关系(在几个源文件中):

  • 类 A 的同步方法调用类 B 的同步方法。
  • B 类的同步方法调用 C 类的同步方法。
  • C 类的同步方法调用 A 类的同步方法。

如果线程 A 调用类 A 的 synchronized 方法,而线程 B 调用类 C 的 synchronized 方法,那么当线程 B 试图调用类 A 的 synchronized 方法,而线程 A 仍在该方法内部时,线程 B 将会阻塞。线程 A 将继续执行,直到它调用类 C 的 synchronized 方法,然后阻塞。死锁结果。

注意 Java 语言和虚拟机都没有提供防止死锁的方法,所以这个负担就落在了你的身上。防止死锁发生的最简单方法是避免同步方法或同步块调用另一个同步方法/块。虽然这个建议防止了死锁的发生,但是它是不切实际的,因为您的一个同步方法/块可能需要调用 Java API 中的一个同步方法,并且这个建议是多余的,因为被调用的同步方法/块可能不会调用任何其他同步方法/块,所以不会发生死锁。

有时您会希望将每线程数据(比如用户 ID)与一个线程相关联。虽然您可以使用局部变量来完成这项任务,但是您只能在局部变量存在时才能这样做。您可以使用实例字段将这些数据保存更长时间,但是这样您就必须处理同步问题。幸运的是,Java 提供了 ThreadLocal 作为一个简单(并且非常方便)的选择。

ThreadLocal 类的每个实例描述了一个线程本地变量 ,这个变量为每个访问该变量的线程提供一个单独的存储槽。您可以将线程局部变量视为一个多时隙变量,其中每个线程可以在同一个变量中存储不同的值。每个线程只看到自己的值,不知道其他线程在这个变量中有自己的值。

ThreadLocal 一般被声明为 ThreadLocal < T > ,其中 T 标识存储在变量中的值的类型。该类声明了以下构造函数和方法:

  • ThreadLocal() 创建新的线程局部变量。
  • T get() 返回调用线程存储槽中的值。如果线程调用该方法时条目不存在, get() 调用 initialValue() 。
  • T initialValue() 创建调用线程的存储槽,并在该槽中存储一个初始值(默认值)。初始值默认为 null。你必须子类化 ThreadLocal 并覆盖这个保护的方法来提供一个更合适的初始值。
  • void remove() 删除调用线程的存储槽。如果这个方法后面跟有 get() ,中间没有 set() , get() 调用 initialValue() 。
  • void set(T value) 将调用线程的存储槽的值设置为值。

清单 8-16 展示了如何使用 ThreadLocal 将不同的用户 id 与两个线程关联起来。

清单 8-16 。不同线程的不同用户 id

public class ThreadLocalDemo
{
   private static volatile ThreadLocal<String> userID =
      new ThreadLocal<String>();

   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      @Override
                      public void run()
                      {
                         String name = Thread.currentThread().getName();
                         if (name.equals("A"))
                            userID.set("foxtrot");
                         else
                            userID.set("charlie");
                         System.out.println(name + " " + userID.get());
                      }
                   };
      Thread thdA = new Thread(r);
      thdA.setName("A");
      Thread thdB = new Thread(r);
      thdB.setName("B");
      thdA.start();
      thdB.start();
   }
}

在实例化 ThreadLocal 并将引用分配给名为 userID 的 volatile 类字段(该字段为 volatile ,因为它被不同的线程访问,这可能在多处理器/多核机器上执行),默认主线程创建另外两个线程,在 userID 中存储不同的字符串对象并输出它们的对象。

当您运行此应用时,您将观察到以下输出(可能不是这个顺序):

A foxtrot
B charlie

存储在线程局部变量中的值是不相关的。当一个新线程被创建时,它获得一个包含 initialValue() 值的新存储槽。也许你更愿意将一个值从一个父线程(一个创建另一个线程的线程)传递给一个子线程(被创建的线程)。您可以使用 InheritableThreadLocal 来完成这项任务。

inheritable thread local 是 ThreadLocal 的子类。除了声明一个 InheritableThreadLocal() 构造函数,这个类还声明了下面的 protected 方法:

  • T child value(T parent value)在创建子线程时,根据父线程的值计算子线程的初始值。在子线程启动之前,从父线程调用此方法。该方法返回传递给 parentValue 的参数,并且应该在需要另一个值时被覆盖。

清单 8-17 展示了如何使用 InheritableThreadLocal 将父线程的整数对象传递给子线程。

清单 8-17 。将对象从父线程传递到子线程

public class InheritableThreadLocalDemo
{
   private static volatile InheritableThreadLocal<Integer> intVal =
      new InheritableThreadLocal<Integer>();

   public static void main(String[] args)
   {
      Runnable rP = new Runnable()
                    {
                       @Override
                       public void run()
                       {
                          intVal.set(new Integer(10));
                          Runnable rC = new Runnable()
                                        {
                                           @Override
                                           public void run()
                                           {
                                              Thread thd;
                                              thd = Thread.currentThread();
                                              String name = thd.getName();
                                              System.out.println(name + " " +
                                                                 intVal.get());
                                           }
                                        };
                          Thread thdChild = new Thread(rC);
                          thdChild.setName("Child");
                          thdChild.start();
                       }
                    };
      new Thread(rP).start();
   }
}

在实例化 InheritableThreadLocal 并将其分配给一个名为 intVal 的 volatile 类字段后,默认主线程创建一个父线程,在 intVal 中存储一个包含 10 的 Integer 对象。父线程创建一个子线程,该子线程访问 intVal 并检索其父线程的整数对象。

当您运行此应用时,您将观察到以下输出:

Child 10

探索系统功能

java.lang 包包括四个面向系统的类:系统、运行时、进程和进程构建器。这些类使您可以获得运行应用的系统的信息(如环境变量值)并执行各种系统任务(如执行另一个应用)。为了简洁起见,在本节中,我只向您介绍前三个类。

注意 ProcessBuilder 是运行时的一个方便的替代品,用于创建应用进程并管理它们的属性。要了解关于该类的更多信息,请查看“Java 的 ProcessBuilder 入门:从 Java 程序与 Linux 交互的简单工具类”(singztechmusings . WordPress . com/2011/06/21/Getting-Started-with-javas-process builder-A-sample-Utility-Class-to-Interact-with-Linux-from-Java-Program/)。

系统

系统 是一个在、 out 和 err 类字段中声明的工具类,这些字段分别引用当前的标准输入、标准输出和标准误差流。第一个字段的类型是 Java . io . inputstream,最后两个字段的类型是 java.io.PrintStream 。(我会在第十一章正式介绍这些类。)

System 还声明了提供对当前时间(以毫秒为单位)、系统属性值、环境变量值和其他类型的系统信息的访问的类方法。此外,它声明了支持将一个数组复制到另一个数组、请求垃圾收集等系统任务的类方法。

表 8-2 描述了系统的一些方法。

表 8-2。 系统方法

方法描述
void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)将从零基偏移量 srcPos 开始的 src 数组中由长度指定的元素数量复制到从零基偏移量 destPos 开始的 dest 数组中。当 src 或 dest 为 null 时,该方法抛出 Java . lang . nullpointerexception,当复制导致访问数组边界之外的数据时抛出 Java . lang . indexoutofboundsexception,当 src 数组中的元素无法存储到 dest 中时抛出 Java . lang . arraystoreexception
长电流时间毫秒()返回自 1970 年 1 月 1 日 00:00:00 UTC(协调世界时—参见en.wikipedia.org/wiki/Coordinated_Universal_Time)以来的当前系统时间,单位为毫秒。
void gc()通知虚拟机现在是运行垃圾收集器的好时机。这只是一个提示;不能保证垃圾收集器会运行。
String getEnv(字符串名称)返回由名标识的环境变量的值。
String getProperty(字符串名称)返回由名称标识的系统属性(特定于平台的属性,如版本号)的值,如果不存在该属性,则返回 null。在 Android 环境中有用的系统属性的例子包括 file.separator 、 java.class.path 、 java.home 、 java.io.tmpdir 、 java.library.path 、 line.separator 、 os.arch 、 os.name 、 path.separator 和】
void runFinalization()通知虚拟机现在是执行任何未完成的对象终结的好时机。这只是一个提示;不保证会执行未完成的对象终结。
void seterr(printstream err)设置标准误差装置指向 err 。
void setIn(InputStream in)将标准输入设备设置为指向中的。
无效抵销(打印流输出)将标准输出设备设置为指向 out 。

注意 系统声明安卓不支持的安全管理器 getSecurityManager()和 void setSecurityManager(security manager sm)方法。在 Android 设备上,前一个方法总是返回 null,后一个方法总是抛出一个 Java . lang . security exception 类的实例。关于后一种方法,其文档指出“安全管理器不提供执行不可信代码的安全环境,并且在 Android 上不受支持。不受信任的代码无法安全地隔离在 Android 上的单个虚拟机中。”

清单 8-18 演示了 arraycopy() 、 currentTimeMillis() 和 getProperty() 方法。

清单 8-18 。用系统方法做实验

public class SystemDemo
{
   public static void main(String[] args)
   {
      int[] grades = { 86, 92, 78, 65, 52, 43, 72, 98, 81 };
      int[] gradesBackup = new int[grades.length];
      System.arraycopy(grades, 0, gradesBackup, 0, grades.length);
      for (int i = 0; i < gradesBackup.length; i++)
         System.out.println(gradesBackup[i]);
      System.out.println("Current time: " + System.currentTimeMillis());
      String[] propNames =
      {
         "file.separator",
         "java.class.path",
         "java.home",
         "java.io.tmpdir",
         "java.library.path",
         "line.separator",
         "os.arch",
         "os.name",
         "path.separator",
         "user.dir"
      };
      for (int i = 0; i < propNames.length; i++)
         System.out.println(propNames[i] + ": " +
                            System.getProperty(propNames[i]));
   }
}

清单 8-18 的 main() 方法从演示 arraycopy() 开始。它使用这个方法将一个 grades 数组的内容复制到一个 gradesBackup 数组。

提示array copy()方法是将一个数组复制到另一个数组的最快的便携方法。此外,当您编写一个类,它的方法返回一个对内部数组的引用时,您应该使用 arraycopy() 创建一个数组的副本,然后返回该副本的引用。这样,您可以防止客户端直接操作(并且可能搞砸)内部数组。

main() 接下来调用 currentTimeMillis() 以毫秒值的形式返回当前时间。因为这个值不是人类可读的,你可能想要使用 java.util.Date 类(在第十章中讨论过)。 Date() 构造函数调用 current time millis(),其 toString() 方法将该值转换为可读的日期和时间。

main() 通过在 for 循环中演示 getProperty() 得出结论。这个循环遍历所有的表 8-2 的属性名,输出每个名称和值。

编译清单 8-18:【SystemDemo.java】贾瓦茨 ??。然后执行以下命令行:

java SystemDemo

当我在我的平台上运行这个应用时,它会生成以下输出:

86
92
78
65
52
43
72
98
81
Current time: 1353115138889
file.separator: \
java.class.path: .;C:\Program Files (x86)\QuickTime\QTSystem\QTJava.zip
java.home: C:\Program Files\Java\jre7
java.io.tmpdir: C:\Users\Owner\AppData\Local\Temp\
java.library.path: C:\Windows\system32;C:\Windows\Sun\Java\bin;C:\Windows\system32;C:\Windows;c:\Program Files (x86)\AMD APP\bin\x86_64;c:\Program Files (x86)\AMD APP\bin\x86;C:\Program Files\Common Files\Microsoft Shared\Windows Live;C:\Program Files (x86)\Common Files\Microsoft Shared\Windows Live;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\ATI Technologies\ATI.ACE\Core-Static;C:\Program Files (x86)\Windows Live\Shared;C:\Program Files\java\jdk1.7.0_06\bin;C:\Program Files (x86)\Borland\BCC55\bin;C:\android;C:\android\tools;C:\android\platform-tools;C:\Program Files (x86)\apache-ant-1.8.2\bin;C:\Program Files (x86)\QuickTime\QTSystem\;.
line.separator:

os.arch: amd64
os.name: Windows 7
path.separator: ;
user.dir: C:\prj\dev\ljfad2\ch08\code\SystemDemo

注意 line.separator 存储的是实际的行分隔符字符,而不是其表示形式(如 \r\n ),这也是为什么 line . separator:后面会出现一个空行的原因。

当您调用 System.in.read() 时,输入来源于在中分配给的 InputStream 实例所标识的源。类似地,当您调用 System.out.print() 或 System.err.println() 时,输出将被发送到分别分配给 out 或 err 的 PrintStream 实例所标识的目的地。

提示在 Android 设备上,首先在命令行执行 adb logcat ,可以查看发送到标准输出和标准错误的内容。 adb 是 Android SDK 中包含的工具之一。

Java 在中初始化,在标准输入设备重定向到文件时引用键盘或文件。类似地,Java 初始化 out / err 以在标准输出/错误设备重定向到文件时引用屏幕或文件。您可以通过调用 setIn() 、 setOut() 和 setErr() 来指定输入源、输出目的地和错误目的地——参见清单 8-19 。

清单 8-19 。以编程方式指定标准输入设备源和标准输出/错误设备目标

import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;

public class RedirectIO
{
   public static void main(String[] args) throws IOException
   {
      if (args.length != 3)
      {
         System.err.println("usage: java RedirectIO stdinfile stdoutfile stderrfile");
         return;
      }

      System.setIn(new FileInputStream(args[0]));
      System.setOut(new PrintStream(args[1]));
      System.setErr(new PrintStream(args[2]));

      int ch;
      while ((ch = System.in.read()) != -1)
         System.out.print((char) ch);

      System.err.println("Redirected error output");
   }
}

清单 8-19 展示了一个重定向应用,它让你指定(通过命令行参数)一个文件的名称,从该文件中 System.in.read() 获得其内容,以及 System.out.print() 和 System.err.println() 将它们的内容发送到的文件的名称。然后,它继续将标准输入复制到标准输出,然后演示将内容输出到标准错误。

注意 FileInputStream 提供对存储在由 args[0] 标识的文件中的输入字节序列的访问。类似地, PrintStream 提供对由 args[1] 和 args[2] 标识的文件的访问,这些文件将存储字节的输出和错误序列。

编译清单 8-19:【RedirectIO.java】??【贾瓦茨。然后执行以下命令行:

java RedirectIO RedirectIO.java out.txt err.txt

该命令行不会在屏幕上产生可视输出。而是将 RedirectIO.java 的内容复制到 out.txt 中。它还将重定向的错误输出存储在 err.txt 中。

运行时间和流程

运行时为 Java 应用提供对其运行环境的访问。这个类的实例是通过调用它的运行时 getRuntime() 类方法获得的。

注意运行时类只有一个实例。

运行时声明了几个方法,这些方法也在系统中声明。例如,运行时声明了一个 void gc() 方法。在幕后,系统通过首先获取运行时实例,然后通过该实例调用该方法,来遵从其运行时对应方。比如系统的静态 void gc() 方法执行 Runtime.getRuntime()。GC();。

运行时也声明了没有系统对应的方法。下面的列表描述了其中的一些方法:

  • int available processors()返回虚拟机可用的处理器数量。此方法返回的最小值是 1。
  • long freeMemory() 返回虚拟机提供给应用的可用内存量(以字节为单位)。
  • long maxMemory() 返回虚拟机可以使用的最大内存量(以字节为单位)(或 Long。MAX_VALUE 无限制时)。
  • long total memory()返回虚拟机可用的内存总量(以字节为单位)。该数量可能会随着时间的推移而变化,具体取决于托管虚拟机的环境。

清单 8-20 展示了这些方法。

清单 8-20 。试验运行时方法

public class RuntimeDemo
{
   public static void main(String[] args)
   {
      Runtime rt = Runtime.getRuntime();
      System.out.println("Available processors: " + rt.availableProcessors());
      System.out.println("Free memory: "+ rt.freeMemory());
      System.out.println("Maximum memory: " + rt.maxMemory());
      System.out.println("Total memory: " + rt.totalMemory());
   }
}

编译清单 8-20:【RuntimeDemo.java】贾瓦茨 ??。然后执行以下命令行:

java RuntimeDemo

当我在我的平台上运行这个应用时,我观察到以下结果:

Available processors: 2
Free memory: 123997936
Maximum memory: 1849229312
Total memory: 124649472

一些运行时的方法专用于执行其他应用。例如,进程 exec(字符串程序)在单独的本地进程中执行名为程序的程序。新进程继承了方法调用者的环境,并且返回一个进程对象以允许与新进程通信。发生 I/O 错误时,抛出 IOException 。

提示 ProcessBuilder 是配置流程属性和运行流程的一种方便的替代方法。例如,Process p = new Process builder(" my command “,” myArg ")。start();。

表 8-3 描述了过程的方法。

表 8-3。 处理方法

方法描述
虚空毁灭()终止调用进程并关闭任何关联的流。
int exit value()??返回由这个进程对象(新进程)表示的本机进程的退出值。当本机进程尚未终止时,抛出 IllegalThreadStateException 。
input stream getrststream()返回一个输入流,该输入流连接到由这个进程对象表示的本地进程的标准错误流。该流从这个进程对象表示的进程的错误输出中获取数据。
input stream getinpertstream()返回一个输入流,该输入流连接到由这个进程对象表示的本地进程的标准输出流。该流从这个进程对象所代表的进程的标准输出中获取数据。
输出流 getutputstream()返回一个输出流,该输出流连接到由这个进程对象表示的本地进程的标准输入流。流的输出通过管道进入由这个进程对象表示的进程的标准输入。
int wait for()使调用线程等待与这个进程对象相关联的本地进程终止。返回进程的退出值。按照惯例,0 表示正常终止。当当前线程在等待时被另一个线程中断,这个方法抛出 InterruptedException 。

清单 8-21 演示了 exec(字符串程序)和三个进程的方法。

清单 8-21 。执行另一个应用并显示其标准输出/错误内容

import java.io.InputStream;
import java.io.IOException;

public class Exec
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         System.err.println("usage: java Exec program");
         return;
      }
      try
      {
         Process p = Runtime.getRuntime().exec(args[0]);
         // Obtaining process standard output.
         InputStream is = p.getInputStream();
         int _byte;
         while ((_byte = is.read()) != -1)
            System.out.print((char) _byte);
         // Obtaining process standard error.
         is = p.getErrorStream();
         while ((_byte = is.read()) != -1)
            System.out.print((char) _byte);
         System.out.println("Exit status: " + p.waitFor());
      }
      catch (InterruptedException ie)
      {
         assert false; // should never happen
      }
      catch (IOException ioe)
      {
         System.err.println("I/O error: " + ioe.getMessage());
      }
   }
}

在确认已经指定了一个命令行参数之后,清单 8-21 的 main() 方法试图运行这个参数所标识的应用。 IOException 在找不到应用或者发生其他 I/O 错误时抛出。

假设一切正常,调用 getInputStream() 以获得对输入流的引用,该输入流用于输入新调用的应用写入其标准输出流的字节(如果有的话)。这些字节随后被输出。

接下来, main() 调用 getErrorStream() 以获得对输入流的引用,该输入流用于输入新调用的应用写入其标准错误流的字节(如果有的话)。这些字节随后被输出。

注意为了防止混淆,请记住进程的 getInputStream() 方法用于读取新进程写入其输出流的字节,而进程的 getErrorStream() 方法用于读取新进程写入其错误流的字节。

最后, main() 调用 waitFor() 进行阻塞,直到新进程退出。如果新进程是基于 GUI 的应用,则该方法不会返回,直到您显式终止新进程。对于简单的基于命令行的应用, Exec 应该立即终止。

编译清单 8-21:【Exec.java】??【贾瓦茨。然后执行识别应用的命令行,比如 java 应用启动器:

java Exec java

您应该观察到 java 的用法信息,后面跟有下面一行:

Exit status: 1

注意由于一些本机平台为标准输入和输出流提供有限的缓冲区大小,如果不能及时写入新进程的输入流或读取其输出流,可能会导致新进程阻塞甚至死锁。

练习

以下练习旨在测试您对第八章内容的理解:

  1. 什么是原始类型包装类?
  2. 识别 Java 的原始类型包装类。
  3. Java 为什么要提供原语类型包装类?
  4. 是非判断:字节是最小的原始类型包装类。
  5. 为什么要用 Character 类方法,而不是用 ch>= ’ 0 '&&ch<= ’ 9 '这样的表达式来判断一个字符是不是一个数字,一个字母,等等?
  6. 如何确定 double 变量 d 包含+无穷大还是-无穷大?
  7. 识别作为字节、字符和其他原始类型包装类的超类的类。
  8. 定义线程。
  9. Runnable 接口的用途是什么?
  10. 线程类的用途是什么?
  11. 是非判断:一个线程对象与多个线程相关联。
  12. 定义竞争条件。
  13. 什么是线程同步?
  14. 同步是如何实现的?
  15. 同步是如何工作的?
  16. 是非判断:类型为 long 或 double 的变量在 32 位虚拟机上不是原子的。
  17. 保留字挥发的目的是什么?
  18. 是非判断:对象的 wait() 方法可以从同步方法或块的外部调用。
  19. 定义死锁。
  20. ThreadLocal 类的用途是什么?
  21. InheritableThreadLocal 与 ThreadLocal 有何不同?
  22. 识别本章前面讨论的四个 java.lang 包系统类。
  23. 调用什么系统方法将一个数组复制到另一个数组?
  24. exec(字符串程序)方法完成什么?
  25. 进程的 getInputStream() 方法完成什么?
  26. 创建一个接受两个参数的 MultiPrint 应用:文本和一个表示计数的整数值。这个应用应该打印文本的副本,每行一份。
  27. 修改清单 8-4 的计数线程应用,将两个启动的线程标记为守护线程。运行结果应用时会发生什么?
  28. 修改清单 8-4 的计数线程应用,增加当用户按回车键时停止两个线程计数的逻辑。新的 StopCountingThreads 应用的默认主线程应该在终止前调用 System.in.read() ,并在该方法调用返回后将 true 赋给名为 stopped 的变量。在每次循环迭代开始时,每个计数线程都应该测试这个变量,看它是否包含 true,只有当变量包含 false 时才继续循环。
  29. 创建一个 EVDump 应用,将所有环境变量(不是系统属性)转储到标准输出。

摘要

java.lang 包包括布尔、字节、字符、双精度、浮点、整数、长、短。这些类被称为原始类型包装类,因为它们的实例将自己包装在原始类型的值周围。

Java 提供了这八个原始类型包装类,这样原始类型的值可以存储在集合中,比如列表、集合和映射。此外,这些类提供了一个将有用的常量和类方法与基本类型相关联的好地方。

应用通过线程执行,这些线程是应用代码的独立执行路径。虚拟机为每个线程提供了自己的方法调用堆栈,以防止线程相互干扰。

Java 通过其线程 API 支持线程。这个 API 由 java.lang 包中的一个接口( Runnable )和四个类(线程、线程组、 ThreadLocal 和 InheritableThreadLocal )组成。 ThreadGroup 不如这些其他类型有用。

在整个执行过程中,每个线程都与其他线程相隔离,因为每个线程都有自己的方法调用堆栈。然而,当线程访问和操作共享数据时,它们仍然会相互干扰。这种干扰会破坏共享数据,导致应用失败。

通过使用线程同步可以避免损坏,这样一次只有一个线程可以在临界区内执行,临界区是一个必须以原子方式执行的代码区域。它在源代码级别被标识为同步方法或同步块。

通过在方法返回类型之前向方法头添加保留字 synchronized ,可以在方法级别同步访问。还可以通过指定对象 ) { /同步语句/ } 来同步对语句块的访问。

同步是根据监视器和锁实现的。一个监视器是一个并发结构,用于控制对一个临界区的访问。是一个令牌,在监视器允许线程在监视器的临界区内执行之前,线程必须获取这个令牌。

同步是支持互斥或者互斥结合线程通信所必需的。然而,当唯一的目的是在线程之间通信时,存在同步的替代方案。这个备选项是保留字易变字。

对象的 wait() 、 notify() 和 notifyAll() 方法支持一种线程通信形式,其中一个线程主动等待某个条件(继续执行的先决条件)出现,此时另一个线程通知等待的线程它可以继续执行。 wait() 使其调用线程等待一个对象的监视器, notify() 和 notifyAll() 唤醒一个或所有等待监视器的线程。

太多的同步可能会有问题。如果不小心的话,您可能会遇到这样的情况:锁被多个线程获取,没有一个线程持有自己的锁,而是持有其他线程所需的锁,没有一个线程能够进入并在以后退出其临界区以释放其持有的锁,因为其他线程持有该临界区的锁。这种情况被称为死锁

有时您会希望将每线程数据与一个线程相关联。虽然您可以使用局部变量来完成这项任务,但是您只能在局部变量存在时才能这样做。您可以使用实例字段将这些数据保存更长时间,但是这样您就必须处理同步问题。Java 提供了 ThreadLocal 类作为简单(并且非常方便)的替代。

每个 ThreadLocal 实例描述一个线程本地变量,该变量为每个访问该变量的线程提供一个单独的存储槽。可以把线程局部变量想象成一个多时隙变量,其中每个线程可以在同一个变量中存储不同的值。每个线程只看到自己的值,不知道其他线程在这个变量中有自己的值。

存储在线程局部变量中的值是不相关的。当一个新线程被创建时,它获得一个包含 initialValue() 值的新存储槽。然而,通过使用 InheritableThreadLocal 类,您可以将一个值从一个父线程(一个创建另一个线程的线程)传递给一个子线程(一个被创建的线程)。

java.lang 包包含四个面向系统的类:系统、运行时、进程和进程构建器。这些类使您可以获取运行应用的系统的信息,并执行各种系统任务。

System 是一个工具类,它在、 out 和 err 类字段中声明了,这些字段分别引用当前的标准输入、标准输出和标准误差流。第一个字段属于类型 InputStream ,最后两个字段属于类型 PrintStream 。

System 还声明了提供对当前时间(以毫秒为单位)、系统属性值、环境变量值和其他类型的系统信息的访问的类方法。此外,它声明了支持系统任务的类方法,例如将一个数组复制到另一个数组。

运行时为 Java 应用提供对其运行环境的访问。这个类的实例是通过调用它的运行时 getRuntime() 类方法获得的。然后您可以调用各种环境访问方法,包括在系统中声明的方法。

一些运行时的方法执行其他应用。例如,进程 exec(字符串程序)在单独的本机进程中执行程序。新进程继承了方法调用方的环境;返回一个进程对象,以允许与新进程进行通信。

本章完成了我对 Java 基本 API 的介绍。在第九章中,我开始通过关注集合框架和经典集合 API 来探索 Java 的实用 API。*

九、探索集合框架

应用通常必须管理对象集合。尽管您可以为此使用数组,但它们并不总是一个好的选择。例如,数组有固定的大小,当您需要存储可变数量的对象时,很难确定最佳大小。此外,数组只能由整数索引,这使得它们不适合将任意对象映射到其他对象。

标准类库提供了集合框架和遗留工具 API 来代表应用管理集合。在第九章中,我首先介绍这个框架,然后向你介绍这些遗留 API(以防你在遗留代码中遇到它们)。您会发现,一些遗留的 API 仍然有用。

注意 Java 的并发工具 API 套件(在第十章中讨论)扩展了集合框架。

探索集合框架基础

集合框架是一组类型(主要位于 java.util 包中),提供了一个标准架构来表示和操作集合,集合是存储在为此目的而设计的类实例中的对象组。该框架的架构分为三个部分:

  • 核心接口 :框架提供了核心接口,用于独立于集合的实现来操作集合。
  • 实现类 :框架提供了提供不同核心接口实现的类,以解决性能和其他需求。
  • 工具类 :框架为工具类提供了排序数组、获得同步集合等方法。

核心接口包括 java.lang.Iterable 、集合、列表、集合、分类集合、导航集合、队列、队列、地图、分类地图、导航地图。集合扩展可迭代;列表、集合,以及队列各自扩展集合;分类设置扩展设置;可导航集合扩展分类集合;队列延伸队列; SortedMap 扩展 Map;导航地图扩展分类地图。

图 9-1 展示了核心接口的层次结构(箭头指向父接口)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-1 集合框架基于核心接口的层次结构

框架的实现类包括 ArrayList , LinkedList , TreeSet , HashSet , LinkedHashSet , EnumSet , PriorityQueue , ArrayDeque , TreeMap , HashMap ,linked has 每个具体类的名称都以核心接口名称结尾,标识它所基于的核心接口。

注意额外的实现类是并发工具的一部分。

框架的实现类还包括抽象的 AbstractCollection 、 AbstractList 、 AbstractSequentialList 、 AbstractSet 、 AbstractQueue 和 AbstractMap 类。这些类提供了核心接口的框架实现,以便于创建具体的实现类。

最后,框架提供了两个工具类:数组和集合。

可比与比较

集合实现以某种顺序(排列)存储其元素。这个顺序可能是无序的,也可能是根据某种标准(如字母、数字或时间)排序的。

一个排序的集合实现默认按照它们的自然排序 来存储它的元素。比如 java.lang.String 对象的自然排序是字典序或者字典序(也称字母顺序)。

集合不能依靠 equals() 来指定自然排序,因为这种方法只能确定两个元素是否等价。相反,元素类必须实现 Java . lang . comparable接口及其 int compareTo(T o) 方法。

注意根据可比的基于 Oracle 的 Java 文档,这个接口被认为是集合框架的一部分,尽管它是 java.lang 包的成员。

已排序的集合使用 compareTo() 来确定该方法的元素参数 o 在集合中的自然排序。 compareTo() 将参数 o 与当前元素(调用了 compareTo() 的元素)进行比较,并执行以下操作:

  • 当当前元素应该在或之前时,它返回一个负值。
  • 当当前元素和 o 相同时,返回零值。
  • 当当前元素应该在或之后时,它返回一个正值。

当你需要实现 Comparable 的 compareTo() 方法时,有一些规则你必须遵守。下面列出的这些规则类似于第四章中所示的用于实现 equals() 方法的规则:

  • compareTo() 必须是自反的:对于任何非空的参考值 xx 。compare to(x)必须返回 0。
  • compareTo() 必须对称:对于任何非空参考值 xyx 。compare to(y)= =-y。compare to(x)必须持有。
  • compareTo() 必须是可传递的:对于任何非空的参考值 xyz ,如果 x 。compare to(y)>0 为真,若 y 。compare to(z)>0 为真,则 x 。compare to(z)>0 也必须为真。

另外, compareTo() 应该在将空引用传递给这个方法时抛出 Java . lang . nullpointerexception。但是,您不需要检查 null,因为当这个方法试图访问一个 null 引用的不存在的成员时,它会抛出 NullPointerException 。

注意在 Java 5 及其引入泛型之前, compareTo() 的参数是类型 java.lang.Object 的,在进行比较之前必须转换成适当的类型。当参数的类型与转换不兼容时,转换操作符将抛出一个 Java . lang . classcastexception 实例。

您可能偶尔需要在集合中存储以不同于自然顺序的某种顺序排序的对象。在这种情况下,您需要提供一个比较器来提供这种排序。一个比较器是一个对象,它的类实现了比较器接口。该接口的泛型类型为比较器,提供了以下一对方法:

  • int compare(T o1,T o2) 比较两个参数的顺序。该方法在 o1 等于 o2 时返回 0,在 o1 小于 o2 时返回负值,在 o1 大于 o2 时返回正值。
  • boolean equals(Object o) 当 o “等于”这个比较器时返回 true,因为 o 也是一个比较器,并采用相同的排序。否则,此方法返回 false。

注意 比较器声明等于(),因为这个接口在这个方法的契约上加了一个额外的条件。此外,只有当指定的对象也是一个比较器,并且与该比较器采用相同的排序时,该方法才能返回 true。您不必覆盖 equals() ,但是这样做可以通过允许程序确定两个不同的比较器施加相同的顺序来提高性能。

第六章提供了一个说明实现可比的例子,在本章的后面你会发现另一个例子。此外,在这一章中,我将介绍实现比较器的例子。

可迭代和集合

大部分核心接口都植根于 Iterable 及其集合子接口。它们的泛型分别是 Iterable和 Collection。

Iterable 描述了能够以某种顺序 e 返回其包含对象的任何对象。该接口声明了一个迭代器< T >迭代器()方法,该方法返回一个迭代器实例,用于迭代所有包含对象。

集合表示被称为元素的对象集合。该接口提供了许多集合所基于的集合子接口所共有的方法。表 9-1 描述了这些方法。

表 9-1 。收集方法

方法描述
boolean add(e)将元素 e 添加到此集合中。如果此集合因此被修改,则返回 true 否则,返回 false。(试图将 e 添加到不允许重复且已经包含相同值元素的集合中,会导致 e 未被添加。)当不支持 add() 时,此方法抛出 Java . lang . unsupportedoperationexception,当 e 的类不适合此集合时抛出 ClassCastException ,当 e 的某些属性阻止其添加到此集合时抛出 Java . lang . illegalargumentexception,当 NullPointerException 时抛出 IllegalStateException 表示某个方法在非法或不适当的时间被调用。换句话说,Java/Android 环境或应用并不处于所请求操作的适当状态。当您试图将一个元素添加到一个有界队列(一个具有最大长度的队列)并且队列已满时,经常会抛出这个问题。
布尔 addAll (收藏<?延伸 E > c)将集合 c 的所有元素添加到此集合中。如果此集合因此被修改,则返回 true 否则,返回 false。当这个集合不支持 addAll() 时,这个方法抛出 UnsupportedOperationException,当 c 的一个元素的类不适合这个集合时抛出,当一个元素的某个属性阻止它被添加到这个集合时抛出 IllegalArgumentException,当 c 包含空值时抛出 NullPointerException
虚空清()从该集合中移除所有元素。当该集合不支持 clear() 时,该方法抛出 UnsupportedOperationException。
布尔包含 (对象 o)当此集合包含 o 时返回 true 否则,返回 false。当 o 的类不适合该集合时,该方法抛出 ClassCastException ,当 o 包含空引用且该集合不支持空元素时,该方法抛出 NullPointerException 。
布尔包含所有 (收藏<?> c)当此集合包含由 c 指定的集合中包含的所有元素时,返回 true 否则,返回 false。当 c 的一个元素的类不适合这个集合时,该方法抛出 ClassCastException ,当 c 包含空引用或者当它的一个元素为空并且这个集合不支持空元素时,该方法抛出 NullPointerException 。
布尔等于 (对象 o)将 o 的与该集合进行比较,当 o 的等于该集合时返回 true 否则,返回 false。
int hashCode()返回此集合的哈希代码。相等的集合具有相等的哈希代码。
boolean isEmpty()当此集合不包含任何元素时,返回 true 否则,返回 false。
迭代器< E >迭代器()返回一个迭代器实例,用于遍历集合中包含的所有元素。没有关于元素返回顺序的保证(除非这个集合是某个提供保证的类的实例)。为了方便起见,这个可迭代方法在集合中被重新声明。
布尔删除 (对象 o)从此集合中移除标识为 o 的元素。移除元素时返回 true 否则,返回 false。当此集合不支持 remove() 时,此方法抛出 UnsupportedOperationException,当 o 的类不适合此集合时抛出 ClassCastException ,当 o 包含空引用且此集合不支持空元素时抛出 NullPointerException 。
boolean removeAll (集合<?>【c)从该集合中移除集合 c 中包含的所有元素。当此操作修改此集合时,返回 true 否则,返回 false。当此集合不支持 removeAll() 时,此方法抛出 UnsupportedOperationException,当 c 的某个元素的类不适合此集合时抛出 ClassCastException ,当 c 包含空引用或其某个元素为空且此集合不支持空元素时抛出 NullPointerException 。
布尔零售 (收藏<?> c)保留集合 c 中包含的所有元素。当此操作修改此集合时,返回 true 否则,返回 false。当此集合不支持 retainAll() 时,此方法抛出 UnsupportedOperationException,当 c 的某个元素的类不适合此集合时抛出 ClassCastException ,当 c 包含空引用或其某个元素为空且此集合不支持空元素时抛出 NullPointerException 。
int size()返回此集合中包含的元素数,或者当大于整数时返回 Java . lang . Integer . max _ VALUE。集合中包含的 MAX_VALUE 元素。
对象[]toaarray()返回一个数组,其中包含存储在该集合中的所有元素。如果这个集合保证迭代器返回元素的顺序,那么这个方法会以相同的顺序返回元素。返回的数组是“安全的”,因为该集合不维护对它的引用。(换句话说,即使该集合由数组支持,该方法也会分配一个新数组。)调用者可以安全地修改返回的数组。
T[]to array(T[]a)返回包含此集合中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。如果集合适合指定的数组,它将在数组中返回。否则,将使用指定数组的运行时类型和此集合的大小分配一个新数组。当 null 被传递给 a 和 Java . lang . arraystoreexception 时,该方法抛出 NullPointerException 当 a 的运行时类型不是该集合中每个元素的运行时类型的超类型时。

表 9-1 揭示了各种收集方法的三个例外。首先,一些方法可以抛出 UnsupportedOperationException 类的实例。例如,当您试图将一个对象添加到一个不可变(不可修改)集合中时, add() 抛出 UnsupportedOperationException(将在本章后面讨论)。

其次,集合的一些方法可以抛出类的实例。例如, remove() 抛出 ClassCastException 当您试图从一个基于树的映射中删除一个条目(也称为映射)时,该映射的键是 String s,但却指定了一个非 String 键。

最后,集合的 add() 和 addAll() 方法抛出 IllegalArgumentException 实例,当要添加的元素的某个属性 (attribute)阻止它被添加到这个集合时。例如,第三方集合类的 add() 和 addAll() 方法可能会在检测到负的整数值时抛出这个异常。

注意也许你想知道为什么 remove() 被声明为接受任何对象参数,而不是只接受那些类型是集合类型的对象。换句话说,为什么 remove() 没有声明为布尔 remove(E e) ?另外,为什么 containsAll() 、 removeAll() 和 retainAll() 没有用类型为集合<的参数声明?扩展 E > 以确保集合参数只包含与调用这些方法的集合相同类型的元素。这些问题的答案是需要保持向后兼容性。集合框架是在 Java 5 及其泛型引入之前引入的。为了让版本 5 之前编写的遗留代码继续编译,这四个方法被声明为具有较弱的类型约束。

迭代器和增强的 For 循环语句

通过扩展 Iterable , 集合继承了该接口的 iterator() 方法,这使得迭代集合成为可能。 iterator() 返回一个类的实例,该类实现了 Iterator 接口,其泛型类型表示为 Iterator < E > ,并声明了以下三个方法:

  • 布尔 hasNext() 当这个迭代器实例有更多元素要返回时,返回 true 否则,此方法返回 false。
  • E next() 返回集合中与这个迭代器实例相关联的下一个元素,或者当没有更多的元素要返回时抛出 NoSuchElementException 。
  • void remove() 从与此迭代器实例关联的集合中删除 next() 返回的最后一个元素。每次调用 next() 时,该方法只能被调用一次。当在迭代过程中以除调用 remove() 之外的任何方式修改底层集合时,未指定迭代器实例的行为。当此迭代器不支持此方法时,此方法抛出 UnsupportedOperationException,当 remove() 已被调用而之前没有调用 next() 时,或者当多个 remove() 调用发生而中间没有 next() 调用时,此方法抛出 IllegalStateException 。

以下示例向您展示了如何在调用迭代器()返回一个迭代器实例后迭代一个集合:

Collection<String> col = . . . // This code doesn't compile because of the . . .
// Add elements to col.
Iterator iter = col.iterator();
while (iter.hasNext())
   System.out.println(iter.next());

while 循环反复调用迭代器的 hasNext() 方法来确定迭代是否应该继续,以及(如果应该继续)调用 next() 方法来返回相关集合中的下一个元素。

因为这种习惯用法很常用,所以 Java 5 在 for 循环语句中引入了语法糖,以简化这种习惯用法的迭代。这种糖使该语句看起来像在 Perl 等语言中发现的 foreach 语句,并在下面简化的上一个示例中显示出来:

Collection<String> col = . . . // This code doesn't compile because of the . . .
// Add elements to col.
for (String s: col)
   System.out.println(s);

这个 sugar 隐藏了 col.iterator() ,这个方法调用返回一个迭代器实例,用于迭代 col 的元素。它还隐藏了对这个实例上的迭代器的 hasNext() 和 next() 方法的调用。您将该糖解释为如下内容:“对于列中的每个字符串对象,在循环迭代开始时将该对象分配给 s ”

注意增强的 for 循环语句在隐藏数组索引变量的数组上下文中也很有用。考虑以下示例:

String[]动词= {“跑”、“走”、“跳”};for(字符串动词:verbs) System.out.println(动词);

该示例的内容为“对于动词数组中的每个字符串对象,在循环迭代开始时将该对象分配给动词”,该示例相当于以下示例:

String[]动词= {“跑”、“走”、“跳”};for(int I = 0;我<动词不定式;i++) System.out.println(动词[I]);

增强的 for 循环语句的局限性在于,在需要访问迭代器来从集合中移除元素的情况下,不能使用该语句。此外,在遍历过程中必须替换集合/数组中的元素时,它是不可用的;它不能用于必须并行迭代多个集合或数组的情况。

汽车尾气与无毒〔??〕

认为 Java 应该只支持引用类型的开发人员抱怨过 Java 对基本类型的支持。Java 类型系统二分法的一个明显体现是集合框架:可以在集合中存储对象,但不能存储基于原始类型的值。

虽然您不能在集合中直接存储基于基元类型的值,但是您可以通过首先将该值包装在从基元类型包装类(如 Integer )创建的对象中,然后将该基元类型包装类实例存储在集合中来间接存储该值——参见以下示例:

Collection<Integer> col = . . .; // This code doesn't compile because of the . . .
int x = 27;
col.add(new Integer(x)); // Indirectly store int value 27 via an Integer object.

反过来的情况也是繁琐的。当您想要从列中检索 int 时,您必须调用 Integer 的 intValue() 方法(如果您还记得的话,该方法是从 Integer 的 java.lang.Number 超类继承而来的)。继续这个例子,您可以指定 int y = col.iterator()。下一个()。int value();将存储的 32 位整数赋值给 y 。

为了减轻这种乏味,Java 5 引入了自动装箱和取消装箱,这是一对互补的基于糖的语法语言特性,使原始类型的值看起来更像对象。(这个“戏法”并不完整,因为您不能指定像 27.doubleValue() 这样的表达式。)

自动装箱自动装箱(包装)适当的原始类型包装类的对象中的原始类型值,只要指定了原始类型值但需要引用。例如,您可以将示例的第三行改为 col . add(x);并让编译器将 box x 转换成一个整数对象。

取消装箱自动取消装箱(展开)每当指定了引用但需要原始类型值时,从其包装对象中取消原始类型值。例如,您可以指定 int y = col.iterator()。next();并让编译器在赋值前将返回的整数对象解装箱为 int 值 27。

虽然引入自动装箱和取消装箱是为了简化在集合上下文中使用基元类型值,但是这些语言特性也可以在其他上下文中使用;这种任意使用会导致一个问题,如果不知道幕后发生了什么,这个问题就很难理解。考虑以下示例:

Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2); // Output: true
System.out.println(i1 < i2); // Output: false
System.out.println(i1 > i2); // Output: false
System.out.println(i1 + i2); // Output: 254
i1 = 30000;
i2 = 30000;
System.out.println( i1 == i2 ); // Output: false
System.out.println(i1 < i2); // Output: false
System.out.println(i1 > i2); // Output: false
i2 = 30001;
System.out.println(i1 < i2); // Output: true
System.out.println(i1 + i2); // Output: 60001

除了一个例外,这个例子的输出和预期的一样。例外情况是 i1 == i2 比较,其中 i1 和 i2 都包含 30000。不像 i1 和 i2 都包含 127 的情况那样返回 true, i1 == i2 返回 false。是什么导致了这个问题?

检查生成的代码,你会发现整数 i1 = 127 转换为整数 i1 = Integer . value of(127);和整数 i2 = 127 转换为整数 I2 = Integer . value of(127);。根据 valueOf() 的 Java 文档,这种方法利用缓存来提高性能。

注意 valueOf() 在向集合中添加原始类型值时也会用到。例如, col.add(27) 转换为 col . add(integer . value of(27))。

Integer 在一个小范围的值上维护唯一的 Integer 对象的内部缓存。此范围的下限是-128,上限默认为 127。但是,您可以通过为系统属性 Java . lang . integer . integer cache . high 分配不同的值来更改上限(通过 java.lang.System 类的 String set property(String name,String value) 方法—我在第八章中演示了该方法的 String getProperty(String name)对应项)。

注意Java . lang . Byte、 java.lang.Long 和 java.lang.Short 中的每一个还分别维护唯一的字节、长和短对象的内部缓存。

因为有缓存,每个 Integer.valueOf(127) 调用都返回相同的 Integer 对象引用,这也是为什么 i1 == i2 (比较引用)求值为 true 的原因。因为 30000 位于默认范围之外,所以每个 Integer.valueOf(30000) 调用都返回对新的 Integer 对象的引用,这就是为什么 i1 == i2 计算为 false。

对比 == 和!= ,在比较之前不会对装箱后的值进行拆箱,运算符如 < 、 > 、 + 在执行运算之前会对这些值进行拆箱。结果, i1 < i2 转换为 i1 . int value()<I2 . int value()i1+I2 转换为 i1 . int value()+I2 . int value()。

注意不要假设自动装箱和取消装箱是在 == 和的上下文中使用的!= 运算符。

浏览列表

一个列表是一个有序集合,也称为序列。元素可以通过整数索引存储在特定的位置,也可以从特定的位置访问。其中一些元素可能是重复的或空的(当列表的实现允许空元素时)。列表由列表接口描述,其类属类型为列表< E > 。

List 扩展了集合并重新声明了它继承的方法,部分是为了方便。它还重新声明了迭代器()、 add() 、 remove() 、 equals() 和 hashCode() ,以便在它们的契约上放置额外的条件。例如, List 对 add() 的约定指定它将一个元素追加到列表的末尾,而不是将该元素添加到集合中。

List 也声明了表 9-2 的特定于列表的方法。

表 9-2 。列表特定的方法

方法描述
void 添加 (int index,E e)将元素 e 插入该列表中的位置索引。将当前位于此位置的元素(如果有)和任何后续元素向右移动。当此列表不支持 add() 时,此方法抛出 UnsupportedOperationException,当 e 的类不适合此列表时抛出,当 e 的某些属性阻止其添加到此列表时抛出 IllegalArgumentException,当 e 时抛出 NullPointerException
布尔 addAll (int index,Collection <?延伸 E > c)从位置索引开始,按照 c 迭代器返回的顺序,将所有 c 的元素插入到这个列表中。将当前位于此位置的元素(如果有)和任何后续元素向右移动。当这个列表不支持 addAll() 时,这个方法抛出 UnsupportedOperationException,当 c 的一个元素的类不适合这个列表时抛出,当一个元素的某个属性阻止它被添加到这个列表时抛出 IllegalArgumentException,当 c 时抛出 NullPointerException
E 得到 (int 指数)返回存储在该列表中位置索引处的元素。当 index 小于 0 或者 index 大于等于 size() 时,该方法抛出 IndexOutOfBoundsException。
int indexOf (对象 o)返回列表中第一个出现的元素的索引,如果列表中不包含该元素,则返回-1。当的的类不适合该列表时,该方法抛出 ClassCastException ,当的包含空引用且该列表不支持空元素时,该方法抛出 NullPointerException 。
int lastIndexOf (对象 o)返回元素 o 在列表中最后一次出现的索引,如果列表中不包含该元素,则返回-1。当的的类不适合该列表时,该方法抛出 ClassCastException ,当的包含空引用且该列表不支持空元素时,该方法抛出 NullPointerException 。
列表迭代器< E >列表迭代器()返回列表中元素的列表迭代器。元素的返回顺序与它们在列表中出现的顺序相同。
list iteratorlist iterator(int index)从位于索引的元素开始,返回列表中元素的列表迭代器。元素的返回顺序与它们在列表中出现的顺序相同。当 index 小于 0 或者 index 大于 size() 时,该方法抛出 IndexOutOfBoundsException。
E 去掉 (int index)从这个列表中移除位置索引处的元素,将任何后续元素左移,并返回这个元素。当这个列表不支持 remove() 和 IndexOutOfBoundsException 时,当 index 小于 0 或者 index 大于等于 size() 时,这个方法抛出 UnsupportedOperationException。
E 集 (int index,E e)用元素 e 替换该列表中位置索引处的元素,并返回先前存储在该位置的元素。当此列表不支持 set() 时,此方法抛出 UnsupportedOperationException,当 e 的类不适合此列表时抛出,当 e 的某个属性阻止其添加到此列表时抛出 IllegalArgumentException,当 e 时抛出 NullPointerException
列表< E >子列表 (int fromIndex,int toIndex)返回该列表中从索引(包含)到索引(不包含)的部分的视图(稍后讨论)。(如果 fromIndex 和 toIndex 的相等,则返回的列表为空。)返回的列表是由这个列表支持的,所以返回列表中的非结构性变化也反映在这个列表中,反之亦然。返回的列表支持该列表支持的所有可选列表方法(那些可以抛出 UnsupportedOperationException 的方法)。当 fromIndex 的小于 0, toIndex 大于 size() ,或者 fromIndex 的大于 toIndex 时,该方法抛出 IndexOutOfBoundsException。

表 9-2 引用了列表迭代器接口,它比它的迭代器超级接口更加灵活,因为列表迭代器提供了在任意方向上迭代列表、在迭代过程中修改列表以及获取迭代器在列表中的当前位置的方法。

注意ArrayList 和 LinkedList List 实现类中的 iterator() 和 listIterator() 方法返回的迭代器和 ListIterator 实例是 fail-fast :当一个列表被结构性修改(通过调用实现的 add() 方法添加一个新元素时因此,面对并发修改,迭代器会快速而干净地失败,而不是冒着在未来某个不确定的时间出现任意的、不确定的行为的风险。

ListIterator 声明如下方法:

  • void add(E e) 将 e 插入到被迭代的列表中。这个元素被直接插入到由 next() 返回的下一个元素之前(如果有的话),以及由 previous() 返回的下一个元素之后(如果有的话)。当这个列表迭代器不支持 add() 时,这个方法抛出 UnsupportedOperationException,当 e 的类不适合列表时抛出 ClassCastException ,当 e 的某个属性阻止它添加到列表中时抛出 IllegalArgumentException。
  • 布尔 hasNext() 正向遍历列表时,当该列表迭代器有更多元素时,返回 true。
  • 布尔 hasPrevious() 当这个列表迭代器在反向遍历列表时有更多元素时返回 true。
  • E next() 返回列表中的下一个元素,并移动光标位置。当没有下一个元素时,这个方法抛出 NoSuchElementException 。
  • int nextIndex() 返回对 next() 的后续调用将返回的元素的索引,或者在列表末尾时返回列表的大小。
  • E previous() 返回列表中的前一个元素,并将光标位置向后移动。当没有前一个元素时,这个方法抛出 NoSuchElementException 。
  • int previousIndex() 返回元素的索引,该元素将由对 previous() 的后续调用返回,或者在列表开始时返回-1。
  • void remove() 从列表中删除由 next() 或 previous() 返回的最后一个元素。每次调用 next() 或 previous() 时,只能进行一次调用。此外,只有在最后一次调用 next() 或 previous() 后,还没有调用 add() 时,才能进行。当这个列表迭代器不支持 remove() 和 IllegalStateException 时,当 next() 和 previous() 都没有被调用,或者在最后一次调用 next() 或 previous()之后已经调用了 remove() 或 add() 时,这个方法抛出 UnsupportedOperationException
  • void set(E e) 用元素 e 替换 next() 或 previous() 返回的最后一个元素。只有在最后一次调用 next() 或 previous() 后,既没有调用 remove() 也没有调用 add() 时,才能进行该调用。当这个列表迭代器不支持 set() 时,这个方法抛出 UnsupportedOperationException,当 e 的类不适合列表时抛出 ClassCastException ,当 e 的某个属性阻止它被添加到列表中时抛出 IllegalArgumentException,当

一个 ListIterator 实例没有当前元素的概念。相反,它有一个用于浏览列表的光标的概念。 nextIndex() 和 previousIndex() 方法返回光标位置,该位置总是位于调用 previous() 返回的元素和调用 next() 返回的元素之间。长度为 n 的列表的列表迭代器有 n +1 个可能的光标位置,如下面每个脱字符号( ^ )所示:

                    Element(0)   Element(1)   Element(2)   . . . Element(n-1)
cursor positions:  ^            ^            ^            ^                  ^

注意只要小心,你可以混合呼叫 next() 和 previous() 。请记住,第一次调用 previous() 会返回与最后一次调用 next() 相同的元素。此外,在对 previous() 的一系列调用之后,对 next() 的第一次调用返回与对 previous() 的最后一次调用相同的元素。

表 9-2 对子列表()方法的描述指的是视图的概念,是一个由另一个列表支持的列表。对视图所做的更改会反映在此支持列表中。视图可以覆盖整个列表,或者,正如 subList() 的名字所暗示的,只覆盖列表的一部分。

subList() 方法对于以紧凑的方式在列表上执行范围视图操作非常有用。例如, list.subList(fromIndex,toIndex)。clear();从列表中删除一系列元素,其中第一个元素位于索引的处,最后一个元素位于索引的处。

注意当后备列表发生变化时,视图的含义变得不明确。因此,只要需要在后备列表上执行一系列范围操作,就应该临时使用 subList() 。

数组列表

ArrayList 类提供了一个基于内部数组的列表实现。因此,对列表元素的访问很快。但是,因为必须移动元素以打开插入空间或在删除后关闭空间,所以元素的插入和删除很慢。

ArrayList 提供了三个构造函数:

  • ArrayList() 创建一个空数组列表,初始容量(存储空间)为 10 个元素。一旦达到这个容量,就分配一个更大的数组,将当前数组中的元素复制到更大的数组中,该更大的数组成为新的当前数组。随着附加元素被添加到数组列表中,该过程重复进行。
  • ArrayList(收藏<?extends E > c) 创建一个数组列表,包含 c 的元素,按照它们被 c 的迭代器返回的顺序排列。当 c 包含空引用时,抛出 NullPointerException 。
  • ArrayList(int initial capacity)创建一个空数组列表,初始容量为 initialCapacity 个元素。当 initialCapacity 为负时抛出 IllegalArgumentException 。

清单 9-1 展示了一个数组列表。

清单 9-1 。基于数组的列表演示

import java.util.ArrayList;
import java.util.List;

public class ArrayListDemo
{
   public static void main(String[] args)
   {
      List<String> ls = new ArrayList<String>();
      String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
      for (String weekDay: weekDays)
         ls.add(weekDay);
      dump("ls:", ls);
      ls.set(ls.indexOf("Wed"), "Wednesday");
      dump("ls:", ls);
      ls.remove(ls.lastIndexOf("Fri"));
      dump("ls:", ls);
   }

   static void dump(String title, List<String> ls)
   {
      System.out.print(title + " ");
      for (String s: ls)
         System.out.print(s + " ");
      System.out.println();
   }
}

创建一个数组列表和一个星期名称的数组。然后用这些名称填充这个列表,将列表转储到标准输出,更改其中一个列表条目,再次转储列表,删除一个列表条目,最后一次转储列表。 dump() 方法的增强 for 循环语句在幕后使用了迭代器()、 hasNext() 、 next() 。

当您运行此应用时,它会生成以下输出:

ls: Sun Mon Tue Wed Thu Fri Sat
ls: Sun Mon Tue Wednesday Thu Fri Sat
ls: Sun Mon Tue Wednesday Thu Sat

链接列表

LinkedList 类提供了基于链接节点的列表实现。因为必须遍历链接,所以对列表元素的访问很慢。但是,因为只需要更改节点引用,所以元素的插入和删除很快。

什么是节点?

一个节点是一个固定的值和链接存储器位置序列。与数组不同,在数组中,每个槽存储同一基元类型或引用超类型的单个值,节点可以存储不同类型的多个值。它还可以存储链接(对其他节点的引用)。

考虑下面这个简单的节点类:

类节点

{

字符串名称;//值字段

下一个节点;//链接字段

}

节点描述简单节点,其中每个节点由单个名称值字段和单个下一个链接字段组成。请注意,下一个的与声明它的类类型相同。这种安排让一个节点实例在这个字段中存储对另一个节点实例(即下一个节点)的引用。产生的节点被链接在一起。

下面的代码片段创建了两个节点对象,并将第二个节点对象链接到第一个节点对象。这个片段还演示了如何通过跟踪每个节点对象的下一个字段来遍历这个链表。当遍历代码发现下一个包含空引用时,节点遍历停止,这表示列表结束:

节点优先=新节点();

first.name = “第一个节点”;//您通常会提供 getter 和 setter 方法。

节点 last =新节点();

last.name = “最后一个节点”;

last.next = null

first.next = last

节点 temp = first

while (temp!=空)

{

system . out . println(temp . name);

temp = temp.next

}

代码首先构建两个节点的链表,然后将 first 赋值给局部变量 temp 来遍历列表,而不会丢失对存储在 first 中的第一个节点的引用。当 temp 不为 null 时,循环输出 name 字段。它还通过 temp = temp.next 导航到列表中的下一个节点对象;声明。

如果您将此代码转换为应用并运行该应用,您将发现以下输出:

第一节点

最后一个音符

LinkedList 提供了两个构造函数:

  • 创建一个空的链表。
  • LinkedList(收藏<?extends E > c) 创建一个链表,包含 c 的元素,按照它们被 c 的迭代器返回的顺序排列。当 c 包含空引用时,抛出 NullPointerException 。

清单 9-2 展示了一个链表。

清单 9-2 。链接节点列表的演示

import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

public class LinkedListDemo
{
   public static void main(String[] args)
   {
      List<String> ls = new LinkedList<String>();
      String[] weekDays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
      for (String weekDay: weekDays)
         ls.add(weekDay);
      dump("ls:", ls);
      ls.add(1, "Sunday");
      ls.add(3, "Monday");
      ls.add(5, "Tuesday");
      ls.add(7, "Wednesday");
      ls.add(9, "Thursday");
      ls.add(11, "Friday");
      ls.add(13, "Saturday");
      dump("ls:", ls);
      ListIterator<String> li = ls.listIterator(ls.size());
      while (li.hasPrevious())
         System.out.print(li.previous() + " ");
      System.out.println();
   }

   static void dump(String title, List<String> ls)
   {
      System.out.print(title + " ");
      for (String s: ls)
         System.out.print(s + " ");
      System.out.println();
   }
}

创建一个链表和一个星期名称的数组。然后用这些名字填充这个列表,将列表转储到标准输出,在较短的名字后面插入较长的工作日名字,再次转储列表,并通过使用一个列表迭代器以相反的顺序输出列表,该列表迭代器的光标在列表末尾初始化,并重复调用它的 previous() 方法。

当您运行此应用时,它会生成以下输出:

ls: Sun Mon Tue Wed Thu Fri Sat
ls: Sun Sunday Mon Monday Tue Tuesday Wed Wednesday Thu Thursday Fri Friday Sat Saturday
Saturday Sat Friday Fri Thursday Thu Wednesday Wed Tuesday Tue Monday Mon Sunday Sun

探索集

一个集合是一个不包含重复元素的集合。换句话说,一个集合不包含元素对 e1e2 使得E1。equals(E2)返回 true。此外,一个集合最多可以包含一个空元素。集合由 Set 接口描述,其泛型类型为 Set。

Set 扩展了集合并重新声明了其继承的方法,为了方便起见,也为了给 add() 、 equals() 和 hashCode() 的契约添加规定,以解决它们在 Set 上下文中的行为。另外, Set 的文档声明所有实现类的构造函数必须创建不包含重复元素的集合。

Set 没有引入新的方法。

tree set〔??〕

TreeSet 类提供了一个基于树数据结构的集合实现。因此,元素按排序顺序存储。然而,访问这些元素比使用其他 Set 实现(没有排序)要慢一些,因为必须遍历链接。

查看维基百科的“树(数据结构)”词条([en . Wikipedia . org/wiki/Tree _(数据](http://en.wikipedia.org/wiki/Tree_(data)_ 结构))了解树。

TreeSet 提供了四个构造函数:

  • TreeSet() 创建一个新的空树集合,根据其元素的自然排序进行排序。插入到集合中的所有元素必须实现可比的接口。
  • TreeSet(收藏<?extends E > c) 创建一个新的树集合,包含按照元素的自然排序排序的 c 的元素。插入到新集合中的所有元素必须实现可比接口。当 c 的元素没有实现 Comparable 或者不能相互比较时,这个构造函数抛出 ClassCastException ,当 c 包含空引用时,抛出 NullPointerException 。
  • TreeSet(比较器<?super E > comparator) 创建一个新的空树集合,根据指定的比较器进行排序。将 null 传递给比较器意味着将使用自然排序。
  • TreeSet(sorted setss)创建一个新的树集合,它包含与 ss 相同的元素并使用相同的排序。(我将在本章后面讨论有序集合。)当 ss 包含空引用时,这个构造函数抛出 NullPointerException 。

清单 9-3 展示了一个树集合。

清单 9-3 。一个树集合的演示,其中的字符串元素按照它们的自然顺序排序

import java.util.Set;
import java.util.TreeSet;

public class TreeSetDemo
{
   public static void main(String[] args)
   {
      Set<String> ss = new TreeSet<String>();
      String[] fruits = {"apples", "pears", "grapes", "bananas", "kiwis"};
      for (String fruit: fruits)
         ss.add(fruit);
      dump("ss:", ss);
   }

   static void dump(String title, Set<String> ss)
   {
      System.out.print(title + " ");
      for (String s: ss)
         System.out.print(s + " ");
      System.out.println();
   }
}

创建一个树集合和一个水果名称数组。然后,它用这些名称填充该集合,并将该集合转储到标准输出。因为字符串实现了可比,所以这个应用将水果数组的内容插入到通过 TreeSet() 构造函数创建的树集中是合法的。

当您运行此应用时,它会生成以下输出:

ss: apples bananas grapes kiwis pears

哈希集

HashSet 类提供了一个由散列表数据结构支持的 Set 实现(实现为一个 HashMap 实例,稍后讨论,它提供了一种快速确定元素是否已经存储在该结构中的方法)。尽管这个类没有为它的元素提供排序保证,但是 HashSet 比 TreeSet 要快得多。此外, HashSet 允许空引用存储在其实例中。

注意查看维基百科的“哈希表”条目()了解哈希表。

HashSet 提供四个构造器:

  • HashSet() 创建一个新的空 HashSet,其中后台 HashMap 实例的初始容量为 16,负载系数为 0.75。当我在本章后面讨论散列表时,你会了解到这些项目的含义。
  • HashSet(收藏<?扩展 E > c) 创建一个包含 c 元素的新哈希表。后台散列表的初始容量足以容纳 c 的元素,装载系数为 0.75。当 c 包含空引用时,该构造函数抛出 NullPointerException 。
  • HashSet(int initial capacity)创建一个新的空 HashSet,其中后台 HashMap 实例具有由 initialCapacity 指定的容量和 0.75 的负载系数。当 initialCapacity 的值小于 0 时,该构造函数抛出 IllegalArgumentException 。
  • HashSet(int initialCapacity,float loadFactor) 创建一个新的空 HashSet,其中后台 HashMap 实例具有由 initialCapacity 指定的容量和由 loadFactor 指定的加载因子。当 initialCapacity 小于 0 或 loadFactor 小于或等于 0 时,该构造函数抛出 IllegalArgumentException 。

清单 9-4 展示了一个 hashset。

清单 9-4 。演示一个字符串元素无序的 Hashset

import java.util.HashSet;
import java.util.Set;

public class HashSetDemo
{
   public static void main(String[] args)
   {
      Set<String> ss = new HashSet<String>();
      String[] fruits = {"apples", "pears", "grapes", "bananas", "kiwis",
                         "pears", null};
      for (String fruit: fruits)
         ss.add(fruit);
      dump("ss:", ss);
   }

   static void dump(String title, Set<String> ss)
   {
      System.out.print(title + " ");
      for (String s: ss)
         System.out.print(s + " ");
      System.out.println();
   }
}

HashSetDemo 创建一个 hashset 和一个水果名称数组。然后,它用这些名称填充该集合,并将该集合转储到标准输出。与 TreeSet 不同, HashSet 允许添加 null (不抛出 NullPointerException ),这就是为什么清单 9-4 在 HashSetDemo 的水果数组中包含了 null 。

当您运行此应用时,它会生成如下所示的无序输出:

ss: null grapes bananas kiwis pears apples

假设您想将类的实例添加到一个 hashset 中。与字符串一样,您的类必须覆盖 equals() 和 hashCode();否则,可能会在 hashset 中存储重复的类实例。例如,清单 9-5 给出了一个应用的源代码,该应用的 Planet 类覆盖了 equals() ,但是没有覆盖 hashCode() 。

清单 9-5 。一个自定义的行星类没有覆盖 hashCode()

import java.util.HashSet;
import java.util.Set;

public class CustomClassAndHashSet
{
   public static void main(String[] args)
   {
      Set<Planet> sp = new HashSet<Planet>();
      sp.add(new Planet("Mercury"));
      sp.add(new Planet("Venus"));
      sp.add(new Planet("Earth"));
      sp.add(new Planet("Mars"));
      sp.add(new Planet("Jupiter"));
      sp.add(new Planet("Saturn"));
      sp.add(new Planet("Uranus"));
      sp.add(new Planet("Neptune"));
      sp.add(new Planet("Fomalhaut b"));
      Planet p1 = new Planet("51 Pegasi b");
      sp.add(p1);
      Planet p2 = new Planet("51 Pegasi b");
      sp.add(p2);
      System.out.println(p1.equals(p2));
      System.out.println(sp);
   }
}

class Planet
{
   private String name;

   Planet(String name)
   {
      this.name = name;
   }

   @Override
   public boolean equals(Object o)
   {
      if (!(o instanceof Planet))
         return false;
      Planet p = (Planet) o;
      return p.name.equals(name);
   }

   String getName()
   {
      return name;
   }

   @Override
   public String toString()
   {
      return name;
   }
}

清单 9-5 的星球类声明了一个类型为字符串的名称字段。虽然用单个字符串字段声明行星似乎没有意义,因为我可以重构这个清单来删除行星并使用字符串,但我可能希望在将来为行星引入额外的字段(也许是为了存储行星的质量和其他特征)。

当您运行此应用时,它会生成如下所示的无序输出:

true
[Neptune, Mars, Mercury, Fomalhaut b, Venus, 51 Pegasi b, 51 Pegasi b, Jupiter, Saturn, Earth, Uranus]

这个输出揭示了 hashset 中的两个 51 Pegasi b 元素。尽管从覆盖的 equals() 方法的角度来看,这些元素是相等的(第一个输出行 true 证明了这一点),但是覆盖 equals() 并不足以避免在 hashset 中存储重复的元素:还必须覆盖 hashCode() 。

覆盖清单 9-5 的星球类中的 hashCode() 的最简单方法是让覆盖方法调用 name 字段的 hashCode() 方法并返回其值。(这种技术只适用于单个引用字段的类提供有效的 hashCode() 方法的类。)清单 9-6 展示了这个覆盖的 hashCode() 方法。

清单 9-6 。覆盖 hashCode()的自定义 Planet 类

import java.util.HashSet;
import java.util.Set;

public class CustomClassAndHashSet
{
   public static void main(String[] args)
   {
      Set<Planet> sp = new HashSet<Planet>();
      sp.add(new Planet("Mercury"));
      sp.add(new Planet("Venus"));
      sp.add(new Planet("Earth"));
      sp.add(new Planet("Mars"));
      sp.add(new Planet("Jupiter"));
      sp.add(new Planet("Saturn"));
      sp.add(new Planet("Uranus"));
      sp.add(new Planet("Neptune"));
      sp.add(new Planet("Fomalhaut b"));
      Planet p1 = new Planet("51 Pegasi b");
      sp.add(p1);
      Planet p2 = new Planet("51 Pegasi b");
      sp.add(p2);
      System.out.println(p1.equals(p2));
      System.out.println(sp);
   }
}

class Planet
{
   private String name;

   Planet(String name)
   {
      this.name = name;
   }

   @Override
   public boolean equals(Object o)
   {
      if (!(o instanceof Planet))
         return false;
      Planet p = (Planet) o;
      return p.name.equals(name);
   }

   String getName()
   {
      return name;
   }

   @Override
   public int hashCode()
   {
      return name.hashCode();
   }

   @Override
   public String toString()
   {
      return name;
   }
}

编译 清单 9-6(javac CustomClassAndHashSet.java)并运行应用(Java customclassandhasset)。您将观察到没有重复元素的输出(类似于下图所示):

true
[Saturn, Earth, Uranus, Fomalhaut b, 51 Pegasi b, Venus, Jupiter, Mercury, Mars, Neptune]

注意 LinkedHashSet 是 HashSet 的子类,它使用链表来存储其元素。因此, LinkedHashSet 的迭代器按照元素插入的顺序返回元素。例如,如果清单 9-4 已经指定设置<字符串> ss = new LinkedHashSet <字符串>();,应用的输出应该是 ss:苹果梨葡萄香蕉猕猴桃 null 。另外, LinkedHashSet 比 HashSet 提供更慢的性能,比 TreeSet 提供更快的性能。

枚举集〔??〕

在第六章中,我向你介绍了传统的枚举类型和它们的枚举替换。(一个枚举是通过保留字枚举表示的枚举类型。)下面的例子演示了传统的枚举类型:

static final int SUNDAY = 1;
static final int MONDAY = 2;
static final int TUESDAY = 4;
static final int WEDNESDAY = 8;
static final int THURSDAY = 16;
static final int FRIDAY = 32;
static final int SATURDAY = 64;

虽然 enum 与传统的枚举类型相比有很多优点,但是当将常量组合成一个集合时,传统的枚举类型使用起来不那么笨拙,例如,static final int DAYS _ OFF = SUNDAY | MONDAY;。

DAYS_OFF

注意一个基于 int 的位集不能包含超过 32 个成员,因为 int 的大小是 32 位。类似地,基于长的位集不能包含超过 64 个成员,因为长具有 64 位的大小。

这个位集是通过按位异或运算符( | )对传统枚举类型的整数常量进行按位异或运算而形成的:您也可以使用 + 。每个常数必须是 2 的唯一幂(从 1 开始),因为否则就不可能区分这个位集的成员。

要确定某个常数是否属于位集,请创建一个包含位 AND 运算符( & )的表达式。例如,((DAYS _ OFF&MONDAY)= = MONDAY)与 MONDAY (2)按位 andDAYS _ OFF(3),结果为 2。这个值通过 == 与星期一 (2)进行比较,表达式的结果为真:星期一是 DAYS_OFF 位集的成员。

通过实例化一个适当的 Set 实现类并多次调用 add() 方法来存储集合中的常量,可以用 enum 完成同样的任务。清单 9-7 展示了这个更尴尬的选择。

清单 9-7 。创建相当于 DAYS_OFF 的集合

import java.util.Set;
import java.util.TreeSet;

enum Weekday
{
   SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}

public class DaysOff
{
   public static void main(String[] args)
   {
      Set<Weekday> daysOff = new TreeSet<Weekday>();
      daysOff.add(Weekday.SUNDAY);
      daysOff.add(Weekday.MONDAY);
      System.out.println(daysOff);
   }
}

当您运行此应用时,它会生成以下输出:

[SUNDAY, MONDAY]

注意存储在树集中的是常量的序数,而不是它们的名字,这就是为什么名字看起来是无序的( S 在 M 之前),尽管常量是按照它们序数的排序顺序存储的。

除了比 bitset 更难使用(也更冗长)之外, Set 替代方案需要更多内存来存储每个常量,而且速度也不够快。因为这些问题, EnumSet 被引入。

枚举集类 提供了基于位集的集实现。它的元素是常量,必须来自同一个枚举,该枚举是在创建枚举集时指定的。不允许空元素;任何存储空元素的尝试都会导致抛出 NullPointerException 。

清单 9-8 演示了枚举集。

清单 9-8 。创建相当于 DAYS_OFF 的枚举集

import java.util.EnumSet;
import java.util.Iterator;
import java.util.Set;

enum Weekday
{
   SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
}

public class EnumSetDemo
{
   public static void main(String[] args)
   {
      Set<Weekday> daysOff = EnumSet.of(Weekday.SUNDAY, Weekday.MONDAY);
      Iterator<Weekday> iter = daysOff.iterator();
      while (iter.hasNext())
         System.out.println(iter.next());
   }
}

EnumSetDemo 利用了 EnumSet 的泛型 EnumSet < E 扩展 Enum < E > > 的特性,提供了各种类方法来方便地构造 Enum 集合。例如, < E 扩展 Enum>Enum setof(E E1,E e2) 返回一个由元素 e1 和 e2 组成的 EnumSet 实例。在这个例子中,那些元素是工作日。周日和工作日。周一。

当您运行此应用时,它会生成以下输出:

SUNDAY
MONDAY

注意除了提供()方法的几个重载的, EnumSet 还提供了其他方便创建枚举集的方法。例如, allOf() 返回一个包含所有枚举常量的 EnumSet 实例,其中该方法的唯一参数是一个标识枚举的类文字 (一个表达式,包含一个类名,后面跟一个点,后面跟一个保留字 class ):

设置<工作日>all weekdays = enum Set . allof(Weekday . class);

类似地, range() 返回一个 EnumSet 实例,该实例包含一个枚举元素范围(该范围的限制由该方法的两个参数指定):

for(WeekDay wd:enum set . range(WeekDay。周一,工作日。星期五))system . out . println(wd);

探索有序集合

TreeSet 是排序集合的一个例子,它是一个以升序维护其元素的集合,根据它们的自然排序或根据创建排序集合时提供的比较器进行排序。排序集合由 SortedSet 接口描述。

SortedSet ,其类属类型为 SortedSet < E > ,扩展 Set 。除了两个例外,它从集合继承的方法在排序集合上的行为与在其他集合上的行为相同:

  • 从迭代器()返回的迭代器实例按照元素升序遍历排序后的集合。
  • 由 toArray() 返回的数组包含有序集合的元素。

注意尽管没有得到保证,集合框架中 SortedSet 实现的 toString() 方法(例如 TreeSet )返回一个包含所有有序集合元素的字符串。

分类树集的文档要求一个实现提供我在讨论树集时提出的四个标准构造函数。此外,该接口的实现必须实现表 9-3 中描述的方法。

表 9-3。 排序的具体方法

方法描述
比较器<?超 E >比较器()返回用于对该集合中的元素进行排序的比较器,或者当该集合使用其元素的自然排序时返回 null。
E 首()返回当前在这个集合中的第一个(最低的)元素,或者当这个集合为空时抛出一个 NoSuchElementException 实例。
sort dset耳机(E toElement)将集合中元素严格小于的部分的视图返回给元素。返回的集合由该集合支持,因此返回集合中的更改会反映在该集合中,反之亦然。返回的集合支持该集合支持的所有可选集合操作。当到元素与该集合的比较器不兼容时(或者,当该集合没有比较器时,当到元素不实现可比),该方法抛出 ClassCastException ,当到元素为 null 且该集合不允许空元素时,抛出 NullPointerException ,以及 IllegalArgumentException
E last()返回当前集合中的最后一个(最高的)元素,或者当集合为空时抛出一个 NoSuchElementException 实例。
sorted setsubSet(E from element,E toElement)返回集合中元素范围从元素的到元素的的视图。(当 fromElement 和 toElement 的相等时,返回的集合为空。)返回的集合由该集合支持,因此返回集合中的更改将反映在该集合中,反之亦然。返回的集合支持该集合支持的所有可选集合操作。当 fromElement 的和 Element 的不能使用该集合的比较器相互比较时(或者,当集合没有比较器时,使用自然排序),该方法抛出 ClassCastException ,当 fromElement 或 toElement 的为 null 且该集合不允许 null 元素时,该方法抛出 NullPointerException
排序分类< E >尾集(E fromElement)从元素返回集合中元素大于或等于的部分的视图。返回的集合由该集合支持,因此返回集合中的更改会反映在该集合中,反之亦然。返回的集合支持该集合支持的所有可选集合操作。当 fromElement 的与该集合的比较器不兼容时(或者,当该集合没有比较器时,当 fromElement 的没有实现 Comparable 时),该方法抛出 ClassCastException ,当 fromElement 的为 null 且该集合不允许空元素时,该方法抛出 NullPointerException 以及 IllegalArgumentException

从 headSet() 、 subSet() 和 tailSet() 返回的基于集合的范围视图类似于从 List 的 subList() 方法返回的基于列表的范围视图,除了基于集合的范围视图即使在后台排序集合被修改时仍然有效。因此,基于集合的范围视图可以使用很长一段时间。

注意与端点是后备列表中的元素的基于列表的范围视图不同,基于集合的范围视图的端点是元素空间中的绝对点,允许基于集合的范围视图充当集合的元素空间的一部分的窗口。对基于集合的范围视图所做的任何更改都被写回到后备排序集合,反之亦然。

由耳机()、子集()或 tailSet() 返回的每个范围视图都是半开,因为它不包括其高端点(耳机()和子集())或其低端点( tailSet() )。对于前两种方法,高端点由参数指定给元素;对于最后一种方法,低端点由元素的参数指定。

注意你也可以把返回的范围视图看作是半封闭的,因为它只包括它的一个端点。

清单 9-9 展示了一个基于树集合的有序集合。

清单 9-9 。一组经过排序的水果和蔬菜名称

import java.util.SortedSet;
import java.util.TreeSet;

public class SortedSetDemo
{
   public static void main(String[] args)
   {
      SortedSet<String> sss = new TreeSet<String>();
      String[] fruitAndVeg =
      {
         "apple", "potato", "turnip", "banana", "corn", "carrot", "cherry",
         "pear", "mango", "strawberry", "cucumber", "grape", "banana",
         "kiwi", "radish", "blueberry", "tomato", "onion", "raspberry",
         "lemon", "pepper", "squash", "melon", "zucchini", "peach", "plum",
         "turnip", "onion", "nectarine"
      };
      System.out.println("Array size = " + fruitAndVeg.length);
      for (String fruitVeg: fruitAndVeg)
         sss.add(fruitVeg);
      dump("sss:", sss);
      System.out.println("Sorted set size = " + sss.size());
      System.out.println("First element = " + sss.first());
      System.out.println("Last element = " + sss.last());
      System.out.println("Comparator = " + sss.comparator());
      dump("hs:", sss.headSet("n"));
      dump("ts:", sss.tailSet("n"));
      System.out.println("Count of p-named fruits & vegetables = " +
                         sss.subSet("p", "q").size());
      System.out.println("Incorrect count of c-named fruits & vegetables = " +
                         sss.subSet("carrot", "cucumber").size());
      System.out.println("Correct count of c-named fruits & vegetables = " +
                         sss.subSet("carrot", "cucumber\0").size());
   }

   static void dump(String title, SortedSet<String> sss)
   {
      System.out.print(title + " ");
      for (String s: sss)
         System.out.print(s + " ");
      System.out.println();
   }
}

SortedSetDemo 创建一个有序集合和一个水果和蔬菜名称的数组,然后从这个数组开始填充集合。在转储集合的内容之后,它输出关于集合的信息,包括集合各部分的头部和尾部视图。

当您运行此应用时,它会生成以下输出:

Array size = 29
sss: apple banana blueberry carrot cherry corn cucumber grape kiwi lemon mango melon nectarine onion peach pear pepper plum potato radish raspberry squash strawberry tomato turnip zucchini
Sorted set size = 26
First element = apple
Last element = zucchini
Comparator = null
hs: apple banana blueberry carrot cherry corn cucumber grape kiwi lemon mango melon
ts: nectarine onion peach pear pepper plum potato radish raspberry squash strawberry tomato turnip zucchini
Count of p-named fruits & vegetables = 5
Incorrect count of c-named fruits & vegetables = 3
Correct count of c-named fruits & vegetables = 4

该输出显示,排序后的集合的大小小于数组的大小,因为集合不能包含重复的元素:重复的香蕉、芜菁和洋葱元素没有存储在排序后的集合中。

comparator() 方法 返回 null,因为排序集不是用比较器创建的。相反,有序集依赖于字符串元素的自然排序,以有序的顺序存储它们。

使用参数 “n” 调用 headSet() 和 tailSet() 方法,以分别返回一组元素,这些元素的名称以严格小于 n 的字母和大于或等于 n 的字母开头。

最后,输出告诉您,在向 subSet() 传递上限时必须小心。可以看到, ss.subSet(“胡萝卜”,“黄瓜”)在返回的范围视图中不包括黄瓜,因为黄瓜是 subSet() 的高端点。

要将黄瓜包含在范围视图中,需要形成一个闭合范围闭合区间 l (两个端点都包含在内)。使用字符串对象,您可以通过将 \0 附加到字符串来完成这项任务。比如 ss.subSet(“胡萝卜”,“黄瓜\0”) 包含黄瓜,因为它小于黄瓜\0 。

同样的技术可以应用于任何需要形成开放范围开放区间 (不包括端点)的地方。比如 ss.subSet("胡萝卜\0 ",“黄瓜”)不包含胡萝卜,因为它小于胡萝卜\0 。此外,它不包括高端点黄瓜。

注意当您想要为从您自己的类中创建的元素创建封闭和开放的范围时,您需要提供某种形式的 predecessor() 和 successor() 方法,它们返回一个元素的前任和继任者。

在设计使用有序集合的类时,您需要非常小心。例如,当您计划将该类的实例存储在一个有序集合中时,该类必须实现 Comparable ,在该集合中,这些元素根据它们的自然顺序进行排序。考虑清单 9-10 中的。

清单 9-10 。自定义雇员类没有实现可比性

import java.util.SortedSet;
import java.util.TreeSet;

public class CustomClassAndSortedSet
{
   public static void main(String[] args)
   {
      SortedSet<Employee> sse = new TreeSet<Employee>();
      sse.add(new Employee("Sally Doe"));
      sse.add(new Employee("Bob Doe")); // ClassCastException thrown here
      sse.add(new Employee("John Doe"));
      System.out.println(sse);
   }
}

class Employee
{
   private String name;

   Employee(String name)
   {
      this.name = name;
   }

   @Override
   public String toString()
   {
      return name;
   }
}

当您运行此应用时,它会生成以下输出:

线程“main”Java . lang . classcastexception 中出现异常:Employee 不能转换为 java.lang.Comparable

at java.util.TreeMap.compare(Unknown Source)
 at java.util.TreeMap.put(Unknown Source)
 at java.util.TreeSet.add(Unknown Source)
 at CustomClassAndSortedSet.main(CustomClassAndSortedSet.java:9)

在第二个 add() 方法调用期间抛出了 ClassCastException 实例,因为排序集实现 TreeSet 的实例无法调用第二个 Employee 元素的 compareTo() 方法,因为 Employee 没有实现 Comparable 。

这个问题的解决方案是让类实现可比的 ,这正是清单 9-11 中的所揭示的。

清单 9-11 。使员工要素具有可比性

import java.util.SortedSet;
import java.util.TreeSet;

public class CustomClassAndSortedSet
{
   public static void main(String[] args)
   {
      SortedSet<Employee> sse = new TreeSet<Employee>();
      sse.add(new Employee("Sally Doe"));
      sse.add(new Employee("Bob Doe"));
      Employee e1 = new Employee("John Doe");
      Employee e2 = new Employee("John Doe");
      sse.add(e1);
      sse.add(e2);
      System.out.println(sse);
      System.out.println(e1.equals(e2));
   }
}

class Employee implements Comparable<Employee>
{
   private String name;

   Employee(String name)
   {
      this.name = name;
   }

   @Override
   public int compareTo(Employee e)
   {
      return name.compareTo(e.name);
   }

   @Override
   public String toString()
   {
      return name;
   }
}

清单 9-11 的 main() 方法与清单 9-10 的不同之处在于,它还创建了两个 Employee 对象,初始化为 “John Doe” ,将这些对象添加到排序后的集合中,并通过 equals() 比较这些对象是否相等。此外,清单 9-11 声明雇员实现可比,将 compareTo() 方法引入雇员。

当您运行此应用时,它会生成以下输出:

[Bob Doe, John Doe, Sally Doe]
false

该输出显示只有一个“John Doe”雇员对象存储在排序后的集合中。毕竟,一个集合不能包含重复的元素。然而, false 值(由 equals() 比较得出)也表明排序集合的自然排序与 equals() 不一致,这违反了 SortedSet 的契约 :

排序集维护的排序 (无论是否提供显式比较器)必须与【equals()一致,排序集才能正确实现接口。这是因为 集合 接口是根据【equals()操作定义的,但是排序后的集合使用其compare to()(或 compare() )方法执行所有元素比较,因此从排序后的集合的角度来看,被该方法视为相等的两个元素是相等的。

因为应用工作正常,为什么 SortedSet 的合同有关系?尽管契约似乎与 SortedSet 的 TreeSet 实现无关,但在实现该接口的第三方类的上下文中可能会有关系。

清单 9-12 向您展示了如何纠正这个问题,并让雇员实例与一个有序集合的任何实现一起工作。

清单 9-12 。遵守合同的雇员阶层

import java.util.SortedSet;
import java.util.TreeSet;

public class CustomClassAndSortedSet
{
   public static void main(String[] args)
   {
      SortedSet<Employee> sse = new TreeSet<Employee>();
      sse.add(new Employee("Sally Doe"));
      sse.add(new Employee("Bob Doe"));
      Employee e1 = new Employee("John Doe");
      Employee e2 = new Employee("John Doe");
      sse.add(e1);
      sse.add(e2);
      System.out.println(sse);
      System.out.println(e1.equals(e2));
   }
}

class Employee implements Comparable<Employee>
{
   private String name;

   Employee(String name)
   {
      this.name = name;
   }

   @Override
   public int compareTo(Employee e)
   {
      return name.compareTo(e.name);
   }

   @Override
   public boolean equals(Object o)
   {
      if (!(o instanceof Employee))
         return false;
      Employee e = (Employee) o;
      return e.name.equals(name);
   }

   @Override
   public String toString()
   {
      return name;
   }
}

清单 9-12 通过覆盖 equals() 纠正了 SortedSet 契约违反。运行生成的应用,您会看到第一行输出是【Bob Doe,John Doe,Sally Doe】,第二行是 true :排序后的集合的自然排序现在与 equals() 一致。

注意尽管每当你覆盖 equals() 时覆盖 hashCode() 是很重要的,但我没有覆盖 hashCode() (尽管我覆盖了清单 9-12 的 Employee 类中的 equals() ,以强调基于树的排序集合忽略 hashCode() 。

探索可导航集合

TreeSet 是可导航集合的一个例子,它是一个排序集合,可以以降序和升序迭代,并且可以报告给定搜索目标的最接近匹配。导航集由 NavigableSet 接口描述,其泛型为 navigable set,扩展了 SortedSet ,在表 9-4 中有描述。

表 9-4 。可导航的特定集合方法

方法描述
E 天花板 E 天花板返回这个集合中大于等于 e 的最小元素,如果没有这样的元素,则返回 null。当 e 无法与当前集合中的元素进行比较时,该方法抛出 ClassCastException ,当 e 为 null 且该集合不允许空元素时,该方法抛出 NullPointerException 。
迭代器descending Iterator()以降序返回集合中元素的迭代器。实际上等同于 descendingSet()。迭代器()。
可导航集descending set()返回此集合中包含的元素的逆序视图。降序集合由该集合支持,因此对集合的更改会反映在降序集合中,反之亦然。如果在对集合进行迭代时修改了任何一个集合(除了通过迭代器自己的 remove() 操作),那么迭代的结果是不确定的。
东楼东楼返回这个集合中小于或等于 e 的最大元素,或者当没有这样的元素时返回 null。当 e 无法与当前集合中的元素进行比较时,该方法抛出 ClassCastException ,当 e 为 null 且该集合不允许空元素时,该方法抛出 NullPointerException 。
可导航集< E >耳机 (E toElement,布尔包含)返回这个集合中元素小于(或等于,当包含为真 ) 到元素的部分的视图。返回的集合由该集合支持,因此返回集合中的更改会反映在该集合中,反之亦然。返回的集合支持该集合支持的所有可选集合操作。当 toElement 与该集合的比较器不兼容时(或者,当该集合没有比较器时,当 toElement 没有实现 Comparable 时),该方法抛出 ClassCastException ,当 toElement 为 null 且该集合不允许 null 元素时,抛出 NullPointerException ,以及 IllegalArgumentException
E 高等(E E)??返回该集合中严格大于给定元素的最小元素,如果没有这样的元素,则返回 null。当 e 无法与当前集合中的元素进行比较时,该方法抛出 ClassCastException ,当 e 为 null 且该集合不允许空元素时,该方法抛出 NullPointerException 。
E 低(E 高)返回该集合中严格小于给定元素的最大元素,如果没有这样的元素,则返回 null。当 e 无法与当前集合中的元素进行比较时,该方法抛出 ClassCastException ,当 e 为 null 且该集合不允许空元素时,该方法抛出 NullPointerException 。
和 pollFirst()返回并移除该集合中的第一个(最低的)元素,或者当该集合为空时返回 null。
和 pollLast()返回并移除该集合中的最后一个(最高的)元素,或者当该集合为空时返回 null。
可导航集合< E >子集 (E fromElement,boolean fromInclusive,E toElement,boolean toInclusive)返回该集合中元素范围从 fromElement 到 toElement 的部分的视图。(当从元素和到元素的相等时,返回的集合为空,除非从包含的和包含的都为真。)返回的集合由该集合支持,因此返回集合中的更改将反映在该集合中,反之亦然。返回的集合支持该集合支持的所有可选集合操作。当 from 元素的和 Element 的不能使用该集合的比较器(或者,当集合没有比较器时,使用自然排序)相互比较时,该方法抛出 ClassCastException ,当 from 元素或 to 元素的为 null 且该集合不允许 null 元素时,该方法抛出 NullPointerException
可导航集< E >尾集 (E fromElement,布尔包含)从元素返回该集合中元素大于(或等于,当包含为真 ) 的部分的视图。返回的集合由该集合支持,因此返回集合中的更改会反映在该集合中,反之亦然。返回的集合支持该集合支持的所有可选集合操作。当 fromElement 的与该集合的比较器不兼容时(或者,当该集合没有比较器时,当 fromElement 的没有实现 Comparable 时),该方法抛出 ClassCastException;当 fromElement 的为 null 且该集合不允许 null 元素时,该方法抛出 NullPointerException

清单 9-13 展示了一个基于树集合的可导航集合。

清单 9-13 。导航一组整数

import java.util.Iterator;
import java.util.NavigableSet;
import java.util.TreeSet;

public class NavigableSetDemo
{
   public static void main(String[] args)
   {
      NavigableSet<Integer> ns = new TreeSet<Integer>();
      int[] ints = { 82, -13, 4, 0, 11, -6, 9 };
      for (int i: ints)
         ns.add(i);
      System.out.print("Ascending order: ");
      Iterator iter = ns.iterator();
      while (iter.hasNext())
         System.out.print(iter.next() + " ");
      System.out.println();
      System.out.print("Descending order: ");
      iter = ns.descendingIterator();
      while (iter.hasNext())
         System.out.print(iter.next() + " ");
      System.out.println("\n");
      outputClosestMatches(ns, 4);
      outputClosestMatches(ns.descendingSet(), 12);
   }

   static void outputClosestMatches(NavigableSet<Integer> ns, int i)
   {
      System.out.println("Element < " + i + " is " + ns.lower(i));
      System.out.println("Element <= " + i + " is " + ns.floor(i));
      System.out.println("Element > " + i + " is " + ns.higher(i));
      System.out.println("Element >= " + i +" is " + ns.ceiling(i));
      System.out.println();
   }
}

清单 9-13 创建一组可导航的整数元素。它利用自动装箱来确保 int s 被转换成整数。

当您运行此应用时,它会生成以下输出:

Ascending order: -13 -6 0 4 9 11 82
Descending order: 82 11 9 4 0 -6 -13

Element < 4 is 0
Element <= 4 is 4
Element > 4 is 9
Element >= 4 is 4

Element < 12 is 82
Element <= 12 is 82
Element > 12 is 11
Element >= 12 is 11

以元素开始的前四个输出行属于一个升序集合,其中被匹配的元素( 4 )是该集合的成员。第二个四个元素前缀的行属于降序集合,其中被匹配的元素( 12 )不是成员。

除了让您通过其最接近的匹配方法 ( ceiling() 、 floor() 、 higher() 和 lower() )方便地定位集合元素之外,navigabableset 还让您返回包含特定范围内所有元素的集合视图,如以下示例所示:

  • ns.subSet(-13,true,9,true) :返回从 -13 到 9 的所有元素。
  • ns.tailSet(-6,false) :返回所有大于 -6 的元素。
  • ns.headSet(4,true) :返回所有小于等于 4 的元素。

最后,您可以通过调用 pollFirst() 从集合中返回和移除第一个(最低的)元素,通过调用 pollLast() 返回和移除最后一个(最高的)元素。比如 ns.pollFirst() 移除并返回 -13 ,而 ns.pollLast() 移除并返回 82 。

探索队列

一个队列是一个集合,其中的元素以特定的顺序存储和检索。大多数队列分为以下几类:

  • 先进先出(FIFO )队列:在队列的尾部插入元素,在队列的头部移除元素。
  • 后进先出(LIFO )队列:在队列的一端插入和移除元素,使得最后插入的元素是第一个检索到的元素。这种队列表现为一个堆栈
  • 优先级队列 :根据元素的自然排序或者根据提供给队列实现的比较器来插入元素。

Queue ,其类属类型为 Queue < E > ,扩展了集合,重新声明 add() 来调整其契约(如果有可能在不违反容量限制的情况下立即将指定元素插入该队列),并从集合中继承其他方法。表 9-5 描述了 add() 和其他队列的具体方法。

表 9-5 。特定于队列的方法

方法描述
boolean add(e)如果可能,在不违反容量限制的情况下,立即将元素 e 插入此队列。成功时返回 true 否则,当由于当前没有可用空间而无法添加元素时,抛出 IllegalStateException 。当 e 的类阻止 e 被添加到该队列时,该方法也抛出 ClassCastException ,当 e 包含空引用并且该队列不允许添加空元素时,抛出 NullPointerException ,当 e 的某个属性阻止其被添加到该队列时,抛出 IllegalArgumentException。
E 元素()返回,但不要删除队列头的元素。当这个队列为空时,这个方法抛出 NoSuchElementException 。
布尔报价(E e)如果可能,在不违反容量限制的情况下,立即将元素 e 插入此队列。成功时返回 true 否则,当由于当前没有可用空间而无法添加元素时,返回 false。当 e 的类阻止 e 添加到该队列时,该方法抛出 ClassCastException ,当 e 包含空引用并且该队列不允许添加空元素时,抛出 NullPointerException ,当 e 的某个属性阻止其添加到该队列时,抛出 IllegalArgumentException。
E peek()返回,但不要删除队列头的元素。当此队列为空时,此方法返回 null。
E poll()返回并移除队列头部的元素。当此队列为空时,此方法返回 null。
E remove()返回并移除队列头部的元素。当这个队列为空时,这个方法抛出 NoSuchElementException 。这是 remove() 和 poll() 的唯一区别。

表 9-5 揭示了两组方法:在一组中,一个方法(如 add() )在操作失败时抛出异常;在另一个集合中,一个方法(例如, offer() )在出现故障时返回一个特殊值(false 或 null)。返回特殊值的方法在容量受限的队列实现的上下文中很有用,在这种情况下,失败是经常发生的。

注意在使用容量受限队列时, offer() 方法通常优于 add() ,因为 offer() 不会抛出 IllegalStateException 。

Java 提供了许多队列实现类,其中大多数类都是 java.util.concurrent 包的成员: LinkedBlockingQueue 和 SynchronousQueue 就是例子。相比之下, java.util 包提供了 LinkedList 和 PriorityQueue 作为其队列实现类。

注意许多队列实现类不允许添加空元素。然而,一些类(例如, LinkedList )允许空元素。您应该避免添加 null 元素,因为 null 被 peek() 和 poll() 方法用作特殊的返回值,以指示队列为空。

优先级队列

PriorityQueue 类提供了一个优先级队列的实现,这是一个队列,它根据元素的自然排序或由队列实例化时提供的比较器对其元素进行排序。依赖自然排序时,优先级队列不允许空元素,也不允许插入非可比的对象。

优先级队列头部的元素是指定排序中最小的元素。当多个元素并列为最小元素时,任意选择其中一个元素作为最小元素。类似地,优先级队列尾部的元素是最大的元素,当出现平局时任意选择。

优先级队列是无限的,但是有一个容量来控制用于存储优先级队列元素的内部数组的大小。容量值至少与队列的长度一样大,并且随着元素被添加到优先级队列中而自动增长。

PriorityQueue (其泛型为 PriorityQueue < E > )提供了六个构造函数:

  • PriorityQueue() 创建一个初始容量为 11 个元素的 PriorityQueue 实例,该实例根据元素的自然顺序对其进行排序。
  • PriorityQueue(集合<?扩展 E > c) 创建一个包含 c 元素的优先级队列实例。如果 c 是一个 SortedSet 或 PriorityQueue 实例,那么这个优先级队列将按照相同的顺序进行排序。否则,该优先级队列将根据其元素的自然顺序进行排序。当 c 的元素不能根据优先级队列的顺序相互比较时,该构造函数抛出 ClassCastException ,当 c 或其任何元素包含空引用时,该构造函数抛出 NullPointerException 。
  • priority queue(int initial capacity)用指定的 initialCapacity 创建一个 PriorityQueue 实例,该实例根据元素的自然顺序对其进行排序。当 initialCapacity 小于 1 时,该构造函数抛出 IllegalArgumentException 。
  • priority queue(int initial capacity,比较器<?超级 E >比较器)使用指定的初始容量创建一个优先级队列实例,该实例根据指定的比较器对其元素进行排序。当比较器包含零参考时,使用自然排序。当 initialCapacity 小于 1 时,该构造函数抛出 IllegalArgumentException。
  • 优先级队列(PriorityQueue <?扩展 E > pq) 创建一个包含 pq 元素的 PriorityQueue 实例。该优先级队列将按照与 pq 相同的顺序进行排序。当 pq 的元素不能根据 pq 的排序相互比较时,该构造函数抛出 ClassCastException ,当 pq 或其任何元素包含空引用时,该构造函数抛出 NullPointerException 。
  • PriorityQueue(SortedSet <?扩展 E > ss) 创建一个包含 ss 元素的优先级队列实例。该优先级队列将按照与 ss 相同的顺序进行排序。当 ss 的元素不能根据 ss 的排序相互比较时,该构造函数抛出 ClassCastException ,当 ss 或其任何元素包含空引用时,该构造函数抛出 NullPointerException 。

清单 9-14 展示了一个优先级队列。

清单 9-14 。将随机生成的整数添加到优先级队列中

import java.util.PriorityQueue;
import java.util.Queue;

public class PriorityQueueDemo
{
   public static void main(String[] args)
   {
      Queue<Integer> qi = new PriorityQueue<Integer>();
      for (int i = 0; i < 15; i++)
         qi.add((int) (Math.random() * 100));
      while (!qi.isEmpty())
         System.out.print(qi.poll() + " ");
      System.out.println();
   }
}

创建优先级队列后, PriorityQueueDemo 的主线程向该队列添加 15 个随机生成的整数(范围从 0 到 99)。然后它进入一个 while 循环,重复轮询下一个元素的优先级队列,并输出该元素,直到队列为空。

当您运行这个应用时,它从左到右按数字升序输出一行 15 个整数。例如,我在一次运行中观察到以下输出:

30 43 53 61 61 66 66 67 76 78 80 83 87 90 97

因为当没有更多的元素时, poll() 返回 null,所以我可以将这个循环编码如下:

Integer i;
while ((i = qi.poll()) != null)
   System.out.print(i + " ");

假设您想颠倒上一个示例的输出顺序,使最大的元素出现在左边,最小的元素出现在右边。如清单 9-15 所示,你可以通过将一个比较器传递给适当的优先级队列构造函数来完成这个任务。

清单 9-15 。使用带有优先级队列的比较器

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

public class PriorityQueueDemo
{
   final static int NELEM = 15; // number of elements

   public static void main(String[] args)
   {
      Comparator<Integer> cmp;
      cmp = new Comparator<Integer>()
            {
               @Override
               public int compare(Integer e1, Integer e2)
               {
                  return e2 - e1;
               }
            };
      Queue<Integer> qi = new PriorityQueue<Integer>(NELEM, cmp);
      for (int i = 0; i < NELEM; i++)
         qi.add((int) (Math.random() * 100));
      while (!qi.isEmpty())
         System.out.print(qi.poll() + " ");
      System.out.println();
   }
}

清单 9-15 与清单 9-14 相似,但有一些不同。首先,我声明了一个名为 NELEM 的常量,这样我就可以通过在一个地方指定新值来轻松地改变优先级队列的初始容量和插入到优先级队列中的元素数量。

其次,清单 9-15 声明并实例化了一个匿名类,它实现了比较器。其 compareTo() 方法从元素 e1 中减去元素 e2 ,实现数字降序。编译器通过将 e2 - e1 转换为 E2 . int value()-E1 . int value()来处理 e2 和 e1 的解装箱任务。

最后,清单 9-15 将一个初始容量的 NELEM 元素和实例化的比较器传递给 priority queue(int initial capacity,Comparator <?超 E >比较器)构造器。优先级队列将使用该比较器对这些元素进行排序。

运行这个应用,您将会看到一个由 15 个整数组成的输出行,按照从左到右的降序排列。例如,我观察到这样的输出行:

97 72 70 70 67 64 56 43 36 22 9 5 3 2 1

探索德克

一个队列(发音为 deck)是一个双端队列,其中元素的插入或移除发生在其。Deques 可以用作队列或堆栈。

Deque ,其泛型为 Deque < E > ,扩展了队列,其中继承的 add(E)E 方法在 Deque 的尾部插入 e 。表 9-6 描述了退出的具体方法。

表 9-6 。特定于队列的方法

方法描述
void add first(E E)如果可能,在不违反容量限制的情况下,立即在该队列的开头插入 e 。当使用容量受限的队列时,通常最好使用方法 offerFirst() 。当 e 由于容量限制此时无法添加时,该方法抛出 IllegalStateException ,当 e 的类阻止 e 添加到该 dequee 时抛出 ClassCastException ,当 e 包含空引用且该 dequee 不允许添加空元素时抛出 NullPointerException ,以及 illegalargumen
void add last(E E)如果有可能,在不违反容量限制的情况下,立即在该队列的尾部插入 e 。当使用容量受限的队列时,通常最好使用方法 offerLast() 。当 e 由于容量限制此时无法添加时,该方法抛出 IllegalStateException ,当 e 的类阻止 e 添加到该 dequee 时抛出 ClassCastException ,当 e 包含空引用且该 dequee 不允许添加空元素时抛出 NullPointerException ,以及 illegalargumen
迭代器descending Iterator()以相反的顺序返回这个队列中元素的迭代器。元素将按从最后(尾部)到第一个(头部)的顺序返回。继承的迭代器< E >迭代器()方法从头到尾返回元素。
E 元素()检索但不删除该队列的第一个元素(在头部)。这个方法与 peek() 的不同之处仅在于,当这个 deque 为空时,它抛出 NoSuchElementException 。这个方法相当于 getFirst() 。
E getFirst()检索但不移除此队列的第一个元素。这个方法与 peekFirst() 的不同之处仅在于,当这个队列为空时,它抛出 NoSuchElementException 。
和 get ast()检索但不移除此队列的最后一个元素。这个方法与 peekLast() 的不同之处仅在于,当这个队列为空时,它抛出 NoSuchElementException 。
布尔报价 【鄂 E】如果有可能在不违反容量限制的情况下,立即在该队列的尾部插入 e ,如果成功,则返回 true,如果当前没有可用空间,则返回 false。当使用容量受限的队列时,这种方法通常比 add() 方法更可取,后者只能通过抛出异常来插入元素。当 e 的类阻止 e 添加到该 dequee 时,该方法抛出 ClassCastException ,当 e 包含空引用且该 dequee 不允许添加空元素时,抛出 NullPointerException ,当 e 的某个属性阻止其添加到该 dequee 时,抛出 IllegalArgumentException。这个方法相当于 offerLast() 。
布林竞价(e)在该队列的头部插入 e ,除非会违反容量限制。当使用容量受限的队列时,这种方法通常优于 addFirst() 方法,后者只能通过抛出异常来插入元素。当 e 的类阻止 e 添加到该 dequee 时,该方法抛出 ClassCastException ,当 e 包含空引用且该 dequee 不允许添加空元素时,抛出 NullPointerException ,当 e 的某个属性阻止其添加到该 dequee 时,抛出 IllegalArgumentException。
布林竞价(e)在这个队列的尾部插入 e ,除非它会违反容量限制。当使用容量受限的队列时,这种方法通常优于 addLast() 方法,后者只能通过抛出异常来插入元素。当 e 的类阻止 e 添加到该 dequee 时,该方法抛出 ClassCastException ,当 e 包含空引用且该 dequee 不允许添加空元素时,抛出 NullPointerException ,当 e 的某个属性阻止其添加到该 dequee 时,抛出 IllegalArgumentException。
E peek()检索但不删除该队列的第一个元素(在头部),或者当该队列为空时返回 null。这个方法相当于 peekFirst() 。
和【peekFirst()检索但不删除该队列的第一个元素(在头部),或者当该队列为空时返回 null。
脱颖而出() 脱颖而出检索但不删除该队列的最后一个元素(在尾部),或者当该队列为空时返回 null。
E poll()检索并删除该队列的第一个元素(在头部),或者当该队列为空时返回 null。这个方法相当于 pollFirst() 。
和 pollFirst()检索并删除该队列的第一个元素(在头部),或者当该队列为空时返回 null。
和 pollLast()检索并移除该队列的最后一个元素(在尾部),或者当该队列为空时返回 null。
和【pop()从该队列表示的堆栈中弹出一个元素。换句话说,移除并返回这个队列的第一个元素。这个方法相当于 removeFirst() 。
虚空推 (戊戌)如果可能的话,在不违反容量限制的情况下立即将 e 压入该队列所代表的堆栈(换句话说,在该队列的头部),成功时返回 true,当当前没有可用空间时抛出 IllegalStateException 。当 e 的类阻止 e 添加到该 dequee 时,该方法也抛出 ClassCastException ,当 e 包含空引用并且该 dequee 不允许添加空元素时,抛出 NullPointerException ,当 e 的某个属性阻止其添加到该 dequee 时,抛出 IllegalArgumentException。这个方法相当于 addFirst() 。
E remove()检索并删除此队列的第一个元素(在头部)。这个方法与 poll() 的不同之处仅在于,当这个队列为空时,它抛出 NoSuchElementException 。这个方法相当于的 removeFirst() 。
和【removeFirst()检索并移除此队列的第一个元素。这个方法与 pollFirst() 的不同之处仅在于,当这个队列为空时,它抛出 NoSuchElementException 。
布尔移除首次出现 (对象 o)从该队列中删除第一个出现的 o 。如果 deque 不包含 o ,则不变。当这个队列包含 o 时(或者等价地,当这个队列由于调用而改变时),返回 true。当 o 的类阻止 o 添加到该 dequee 时,该方法抛出 ClassCastException ,当 o 包含空引用且该 dequee 不允许添加空元素时,该方法抛出 NullPointerException 。继承的布尔 remove(Object o) 方法等同于这个方法。
和 removeLast()检索并移除此队列的最后一个元素。这个方法与 pollLast() 的不同之处仅在于,当这个队列为空时,它抛出 NoSuchElementException 。
布尔 removelastcoccurrence(对象 o)从该队列中删除最后一次出现的 o 。如果 deque 不包含 o ,则不变。当这个队列包含 o 时(或者等价地,当这个队列由于调用而改变时),返回 true。当 o 的类阻止 o 添加到该 dequee 时,该方法抛出 ClassCastException ,当 o 包含空引用且该 dequee 不允许添加空元素时,该方法抛出 NullPointerException 。

如表 9-6 所示,队列声明了访问队列两端元素的方法。提供了插入、移除和检查元素的方法。这些方法中的每一个都以两种形式存在:一个在操作失败时抛出异常,另一个返回特殊值(null 或 false,取决于操作)。后一种形式的插入操作是专门为容量受限的队列实现而设计的;在大多数实现中,插入操作不会失败。

图 9-2 展示了一个来自德克的 Java 文档的表格,该表格很好地总结了插入、移除以及检查头部和尾部的方法的两种形式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-2 Deque 声明了 12 种方法,用于插入、移除和检查 deque 头部或尾部的元素

当一个队列被用作队列时,您会观察到 FIFO(先进先出)行为。元素被添加到队列的末尾,并从开头移除。从队列接口继承的方法完全等同于表 9-7 中所示的队列方法。

表 9-7 。队列和等效的队列方法

队列方法等效德克法
添加(e)add last(e)
报价(e)投标
remove()移除第一()
poll()pollFirst()
元素()getFirst()
peek()peekFirst()

最后,deques 也可以用作 LIFO(后进先出)堆栈。当一个队列被用作堆栈时,元素从队列的开始被推入和弹出。因为堆栈的 push(e) 方法等同于 dequee 的 addFirst(e) 方法,所以它的 pop() 方法等同于 dequee 的 removeFirst() 方法,它的 peek() 方法等同于 dequee 的 peekFirst() 方法

array deque〔??〕

ArrayDeque 类提供了 Deque 接口的可调整大小的数组实现。它禁止将空元素添加到 deque 中,并且它的迭代器()方法返回失败快速迭代器。

ArrayDeque 提供了三个构造函数:

  • 创建一个初始容量为 16 个元素的空数组 Deque。
  • ArrayDeque(收藏<?extends E > c) 创建一个数组 deque,包含 c 的元素,按照它们被 c 的迭代器返回的顺序排列。(由 c 的迭代器返回的第一个元素成为 deque 的第一个元素或前面。)当 c 包含空引用时抛出 NullPointerException 。
  • 创建一个空数组队列,其初始容量足以容纳 numElements 个元素。当传递给 numElements 的参数小于或等于零时,不会抛出异常。

清单 9-16 展示了一个数组队列。

清单 9-16 。使用数组队列作为堆栈

import java.util.ArrayDeque;
import java.util.Deque;

public class ArrayDequeDemo
{
   public static void main(String[] args)
   {
      Deque<String> stack = new ArrayDeque<String>();
      String[] weekdays = { "Sunday", "Monday", "Tuesday", "Wednesday",
                            "Thursday", "Friday", "Saturday" };
      for (String weekday: weekdays)
         stack.push(weekday);
      while (stack.peek() != null)
         System.out.println(stack.pop());
   }
}

ArrayDequeDemo 创建一个队列,用作一个堆栈和一个星期名称数组。然后,它将这些名称压入堆栈并弹出,以相反的顺序输出名称。

当您运行此应用时,它会生成以下输出:

Saturday
Friday
Thursday
Wednesday
Tuesday
Monday
Sunday

探索地图

一个映射是一组键/值对(也称为条目)。因为标识一个条目,所以一个映射不能包含重复的键。此外,每个键最多只能映射到一个值。映射由 Map 接口描述,该接口没有父接口,其泛型类型为 Map < K,V>(K 为键的类型; V 是值的类型)。

表 9-8 描述了映射的方法。

表 9-8 。地图方法

方法描述
虚空清()从该地图中移除所有元素,使其为空。当不支持 clear() 时,该方法抛出 UnsupportedOperationException。
布尔包含关键字 (对象关键字)当此映射包含指定的键的条目时,返回 true 否则,返回 false。当键的类型不适合该映射时,该方法抛出 ClassCastException ,当键包含空引用且该映射不允许空键时,该方法抛出 NullPointerException 。
布尔包含值 (对象值)当此映射将一个或多个键映射到值时返回 true。当值的类型不适合此映射时,此方法抛出 ClassCastException ,当值包含空引用且此映射不允许空值时,此方法抛出 NullPointerException 。
设置<地图。条目< K,V>entry set()返回包含在此映射中的条目的集合视图。因为此地图支持视图,所以对地图所做的更改会反映在地图集中,反之亦然。
布尔等于 (对象 o)将 o 与此图比较是否相等。当 o 也是一张地图并且两张地图代表相同的条目时返回 true 否则,返回 false。
V 得到 (对象关键)返回键映射到的值,或者当该映射不包含键的条目时返回 null。如果这个映射允许 null 值,那么 null 的返回值不一定表示这个映射不包含键的条目;也有可能该映射将键显式映射到空引用。 containsKey() 方法可用于区分这两种情况。当键的类型不适合该映射时,该方法抛出 ClassCastException ,当键包含空引用且该映射不允许空键时,该方法抛出 NullPointerException 。
int hashCode()返回此映射的哈希代码。映射的散列码被定义为映射的 entrySet() 视图中条目的散列码之和。
boolean isEmpty()当此映射不包含任何条目时,返回 true 否则,返回 false。
设置< K > keySet()返回包含在此映射中的键的集合视图。因为此地图支持视图,所以对地图所做的更改会反映在地图集中,反之亦然。
V 放 (K 键,V 值)将值与该地图中的键相关联。如果地图先前包含了键的条目,则旧值将被值替换。当没有键的条目时,该方法返回与键相关联的先前值或 null。(如果实现支持空值,空返回值还可以指示映射先前将空引用与关键字关联。)当不支持 put() 时,此方法抛出 UnsupportedOperationException,当键或值的类不适合此映射时抛出 ClassCastException ,当键或值的某个属性阻止其存储在此映射中时抛出 IllegalArgumentException
void putAll (地图<?扩展 K,?延伸 V >米)将地图 m 中的所有条目复制到该地图。这个调用的效果相当于对 map m 中从 key k 到 value v 的每个映射在这个 map 上调用 put(k,v) 一次。当不支持 putAll() 时,此方法抛出 UnsupportedOperationException,当映射 m 中的键或值的类不适合此映射时抛出 ClassCastException ,当映射 m 中的键或值的某个属性阻止其存储在此映射中时抛出 IllegalArgumentException,以及 NullPointerException
V 移除 (对象关键点)当键的条目存在时,将其从该地图中移除。该方法返回该映射先前与键关联的值,或者当该映射不包含键的映射时返回 null。如果这个映射允许 null 值,那么 null 的返回值不一定表示这个映射不包含键的条目;也有可能映射将键显式映射为空。一旦调用返回,该映射将不包含键的条目。当不支持 remove() 时,此方法抛出 UnsupportedOperationException,当键的类不适合此映射时抛出 ClassCastException ,当键包含空引用且此映射不允许空键时抛出 NullPointerException 。
int size()返回此映射中键/值条目的数量。如果地图包含超过的整数。MAX_VALUE 条目,这个方法返回整数。最大值。
收藏< V >值()返回此映射中包含的值的集合视图。因为此地图支持视图,所以对地图所做的更改会反映在集合中,反之亦然。

与列表、集合、队列、地图不扩展集合。但是,可以通过调用 Map 的 keySet() 、 values() 和 entrySet() 方法,将映射视为集合实例,这些方法分别返回键的集合、值的集合和键/值对条目的集合。

注意values()方法返回集合而不是集合,因为多个键可以映射到同一个值,然后 values() 将返回同一个值的多个副本。

由这些方法返回的集合视图(回想一下,集合是集合,因为集合扩展了集合)提供了迭代映射的唯一方法。例如,假设您用三个颜色、红色、绿色和蓝色常量来声明清单 9-17 的颜色枚举。

清单 9-17 。七彩枚举

enum Color
{
   RED(255, 0, 0),
   GREEN(0, 255, 0),
   BLUE(0, 0, 255);

   private int r, g, b;

   private Color(int r, int g, int b)
   {
      this.r = r;
      this.g = g;
      this.b = b;
   }

   @Override
   public String toString()
   {
      return "r = " + r + ", g = " + g + ", b = " + b;
   }
}

以下示例声明了一个由字符串键和颜色值组成的映射,向该映射添加了几个条目,并对键和值进行迭代:

Map<String, Color> colorMap = . . .; // . . . represents the creation of a Map implementation
colorMap.put("red", Color.RED);
colorMap.put("blue", Color.BLUE);
colorMap.put("green", Color.GREEN);
colorMap.put("RED", Color.RED);
for (String colorKey: colorMap.keySet())
   System.out.println(colorKey);
Collection<Color> colorValues = colorMap.values();
for (Iterator<Color> it = colorValues.iterator(); it.hasNext();)
   System.out.println(it.next());

当针对 colorMap 的 hashmap 实现(稍后讨论)运行这段代码时,您应该会看到类似如下的输出:

red
blue
green
RED
r = 255, g = 0, b = 0
r = 0, g = 0, b = 255
r = 0, g = 255, b = 0
r = 255, g = 0, b = 0

前四个输出行标识映射的键;接下来的四个输出行标识地图的值。

entrySet() 方法 返回一个地图的集合。条目对象。这些对象中的每一个都将单个条目描述为一个键/值对,并且是实现映射的类的一个实例。条目接口,其中条目是映射的嵌套接口。表 9-9 描述了地图。条目的方法。

表 9-9。 地图。输入方法

方法描述
布尔等于 (对象 o)将 o 与此条目比较是否相等。当 o 也是一个映射条目并且两个条目具有相同的键和值时,返回 true。
K getKey()返回此项的密钥。当这个条目先前已经从后备映射中移除时,这个方法可选地抛出 IllegalStateException 。
V getValue()返回此项的值。当这个条目先前已经从后备映射中移除时,这个方法可选地抛出 IllegalStateException 。
int hashCode()返回此项的哈希代码。
V 设定值 (V 值)用值替换该条目的值。倒车地图用新的值更新。当不支持 setValue() 时,该方法抛出 UnsupportedOperationException,当 value 的类阻止其存储在后备映射中时抛出 ClassCastException ,当 value 包含空引用且后备映射不允许空时抛出 NullPointerException ,当的某个属性

以下示例向您展示了如何迭代上一示例的映射条目:

for (Map.Entry<String, Color> colorEntry: colorMap.entrySet())
   System.out.println(colorEntry.getKey() + ": " + colorEntry.getValue());

当针对前面提到的 hashmap 实现运行这个示例时,您会看到以下输出:

red: r = 255, g = 0, b = 0
blue: r = 0, g = 0, b = 255
green: r = 0, g = 255, b = 0
RED: r = 255, g = 0, b = 0

树图〔??〕

TreeMap 类提供了一个基于红黑树的 Map 实现。因此,条目按其键的排序顺序存储。然而,访问这些条目比使用其他 Map 实现(没有排序)要慢一些,因为必须遍历链接。

查看维基百科的“红黑树”词条()了解红黑树。

TreeMap 提供了四个构造函数:

  • TreeMap() 创建一个新的、空的树形图,根据其关键字的自然顺序进行排序。所有插入到地图中的键必须实现可比的接口。
  • TreeMap(比较器<?super K > comparator) 创建一个新的、空的树形图,根据指定的比较器进行排序。将 null 传递给比较器意味着将使用自然排序。
  • TreeMap(地图<?扩展 K,?extends V > m) 创建一个包含 m 条目的新树形图,根据其键的自然排序进行排序。插入新地图的所有键必须实现可比接口。当 m 的键没有实现 Comparable 或者不能相互比较时,这个构造函数抛出 ClassCastException ,当 m 包含空引用时,抛出 NullPointerException 。
  • 树形图(SortedMap < K,?extends V > sm) 创建一个新的树形图,包含相同的条目并使用与 sm 相同的排序。(我将在本章后面讨论排序地图。)当 sm 包含空引用时,这个构造函数抛出 NullPointerException 。

清单 9-18 展示了一个树形图。

清单 9-18 。根据基于字符串的键的自然顺序对映射条目进行排序

import java.util.Map;
import java.util.TreeMap;

public class TreeMapDemo
{
   public static void main(String[] args)
   {
      Map<String, Integer> msi = new TreeMap<String, Integer>();
      String[] fruits = {"apples", "pears", "grapes", "bananas", "kiwis"};
      int[] quantities = {10, 15, 8, 17, 30};
      for (int i = 0; i < fruits.length; i++)
         msi.put(fruits[i], quantities[i]);
      for (Map.Entry<String, Integer> entry: msi.entrySet())
         System.out.println(entry.getKey() + ": " + entry.getValue());
   }
}

创建一个树形图和一个水果名称数组。然后用这些名称填充这个映射,并将映射的条目转储到标准输出。

当您运行此应用时,它会生成以下输出:

apples: 10
bananas: 17
grapes: 8
kiwis: 30
pears: 15

哈希映射

HashMap 类提供了一个基于散列表数据结构的 Map 实现。这个实现支持所有的映射操作,并允许空键和空值。它不保证条目的存储顺序。

一个哈希表在一个哈希函数的帮助下将键映射到整数值。Java 以对象的 hashCode() 方法的形式提供这个函数,类覆盖这些方法以提供适当的哈希代码。

一个哈希代码标识哈希表的数组元素之一,它被称为 。对于一些哈希表,桶可以存储与键相关联的值。图 9-3 说明了这种散列表。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-3 。一个简单的哈希表将键映射到存储与这些键相关的值的桶

哈希函数将鲍勃·多伊哈希到 0 ,这标识了第一个桶。这个桶包含账户,这是鲍勃·多伊的雇员类型。散列函数还散列 John Doe 和 Sally Doe 到 1 和 2 (分别),它们的桶包含销售。

完美的散列函数将每个键散列成一个唯一的整数值。然而,这个理想很难实现。实际上,一些键将散列到相同的整数值。这种不唯一的映射被称为碰撞??。

为了解决冲突,大多数哈希表将条目的链表与桶相关联。桶不包含值,而是包含链表中第一个节点的地址,每个节点包含一个冲突条目。参见图 9-4 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 9-4 。复杂的哈希表将键映射到存储链表引用的桶,链表的节点值是从相同的键散列而来的

当在哈希表中存储值时,哈希表使用哈希函数将键哈希到其哈希代码,然后搜索适当的链表以查看是否存在具有匹配键的条目。如果有条目,它的值会用新值更新。否则,将创建一个新节点,用键和值填充,并附加到列表中。

当从哈希表中检索值时,哈希表使用哈希函数将键哈希到其哈希代码,然后搜索适当的链表以查看是否存在具有匹配键的条目。如果有条目,则返回其值。否则,哈希表可能会返回一个特殊值来指示没有条目,或者可能会引发异常。

桶的数量称为哈希表的容量 。存储条目的数量除以桶的数量的比率被称为哈希表的加载因子 。选择正确的负载系数对于平衡性能和内存使用非常重要:

  • 当负载因子接近 1 时,冲突的概率和处理冲突的成本(通过搜索冗长的链表)会增加。
  • 当负载因子接近 0 时,散列表的大小随着存储桶数量的增加而增加,而搜索成本几乎没有提高。
  • 对于许多哈希表来说,0.75 的加载因子接近最佳值。这个值是散列表的散列表实现的默认值。

HashMap 提供了四个构造函数:

  • HashMap() 创建一个新的空 HashMap,初始容量为 16,装载系数为 0.75。
  • HashMap(int initial capacity)创建一个新的空 HashMap,其容量由 initialCapacity 指定,负载系数为 0.75。当 initialCapacity 的值小于 0 时,该构造函数抛出 IllegalArgumentException 。
  • HashMap(int initialCapacity,float loadFactor) 创建一个新的空 HashMap,其容量由 initialCapacity 指定,负载因子由 loadFactor 指定。当 initialCapacity 小于 0 或 loadFactor 小于或等于 0 时,该构造函数抛出 IllegalArgumentException 。
  • HashMap(Map <?扩展 K,?扩展 V > m) 创建一个包含 m 条目的新散列表。当 m 包含空引用时,这个构造函数抛出 NullPointerException 。

清单 9-19 展示了一个散列表。

清单 9-19 。使用 Hashmap 计算命令行参数

import java.util.HashMap;
import java.util.Map;

public class HashMapDemo
{
   public static void main(String[] args)
   {
      Map<String, Integer> argMap = new HashMap<String, Integer>();
      for (String arg: args)
      {
         Integer count = argMap.get(arg);
         argMap.put(arg, (count == null) ? 1 : count + 1);
      }
      System.out.println(argMap);
      System.out.println("Number of distinct arguments = " + argMap.size());
   }
}

HashMapDemo 创建一个由字符串键和整数值组成的 hashmap。每个键都是传递给该应用的命令行参数之一,其值是该参数在命令行中出现的次数。

例如, java HashMapDemo 如果一只土拨鼠能扔木头,它能扔多少木头生成以下输出:

{wood=2, could=2, how=1, if=1, chuck=2, a=2, woodchuck=2, much=1}
Number of distinct arguments = 8

因为 String 类覆盖了 equals() 和 hashCode() ,清单 9-19 可以使用 String 对象作为 hashmap 中的键。当创建一个其实例将用作键的类时,必须确保重写这两个方法。

清单 9-6 向您展示了一个类的覆盖 hashCode() 方法可以调用一个引用字段的 hashCode() 方法并返回它的值,只要该类声明一个引用字段(并且没有原始类型字段)。

更常见的是,类声明多个字段,并且需要一个更好的 hashCode() 方法的实现。实现应该尝试生成最小化冲突的哈希代码。

没有关于如何最好地实现 hashCode() 的规则,各种算法(完成任务的食谱)被创造出来。我最喜欢的算法出现在约书亚·布洛赫(Addison-Wesley,2008;ISBN: 0321356683)。

下面的算法假设存在一个被称为 X 的任意类,该算法与布洛赫的算法 非常相似,但并不相同:

  1. 将 int 变量 hashCode (名称任意)初始化为任意非零整数值,比如 19。这个变量被初始化为一个非零值,以确保它考虑散列码为零的任何初始字段。如果你将 hashCode 初始化为 0,那么最终的散列码将不会受到这些字段的影响,并且你会冒增加冲突的风险。
  2. 对于也在 X 的 equals() 方法中使用的每个字段 f ,计算 f 的哈希码,并将其分配给 int 变量 hc ,如下所示:
    • a.如果 f 为布尔型,计算 hc = f?1 : 0 。
    • b.如果 f 为字节整数、字符、整数或短整型,则计算 hc = (int) f 。整数值是散列码。
    • c.如果 f 为长整型,则计算 HC =(int)(f ^(f>>>32))。该表达式将长整数的最低有效 32 位与其最高有效 32 位进行异或运算。
    • d.如果 f 为浮点类型,则计算 HC = float . float pointbits(f)。此方法考虑了+无穷大、-无穷大和 NaN。
    • e.如果 f 为双精度浮点类型,则计算 long l = double . doubletolongbits(f);HC =(int)(l ^(l>>>32))。
    • f.如果 f 为空引用的引用字段,则计算 hc = 0 。
    • g.如果 f 是一个非空引用的引用字段,并且如果 X 的 equals() 方法通过递归调用 equals() 来比较字段(如清单 9-12 的 Employee 类),则计算 hc = f.hashCode() 。然而,如果 equals() 使用了一个更复杂的比较,创建一个字段的规范(尽可能简单的)表示,并在这个表示上调用 hashCode() 。
    • h.如果 f 是一个数组,通过递归应用该算法并组合 hc 值,将每个元素视为一个单独的字段,如下一步所示。
  3. 将 hc 与 hashCode 组合如下: hashCode = hashCode * 31 + hc 。将 hashCode 乘以 31 使得产生的哈希值取决于字段在类中出现的顺序,这在一个类包含多个相似的字段(例如几个 int s)时提高了哈希值。我选择 31 与 String 类的 hashCode() 方法一致。
  4. 从 hashCode() 返回 hashCode 。

在第四章,清单 4-7 的点类覆盖了 equals() 但没有覆盖 hashCode() 。我后来展示了一小段代码,它必须附加到 Point 的 main() 方法中,以演示不覆盖 hashCode() 的问题。我在这里重申这个问题:

虽然对象 p1 Point(10,20) 在逻辑上是等价的,但是这些对象有不同的 hash 码,导致每个对象引用 hashmap 中不同的条目。如果某个对象没有存储在该条目中(通过 put() ),则 get() 返回 null。

清单 9-20 通过声明一个 hashCode() 方法来修改清单 4-7 的 Point 类。该方法使用上述算法来确保逻辑上等价的点对象散列到相同的条目。

清单 9-20 。覆盖 hashCode()为点对象返回正确的散列码

import java.util.HashMap;
import java.util.Map;

public class Point
{
   private int x, y;

   Point(int x, int y)
   {
      this.x = x;
      this.y = y;
   }

   int getX()
   {
      return x;
   }

   int getY()
   {
      return y;
   }

   @Override
   public boolean equals(Object o)
   {
      if (!(o instanceof Point))
         return false;
      Point p = (Point) o;
      return p.x == x && p.y == y;
   }

   @Override
   public int hashCode()
   {
      int hashCode = 19;
      int hc = x;
      hashCode = hashCode * 31 + hc;
      hc = y;
      hashCode = hashCode * 31 + hc;
      return hashCode;
   }

   public static void main(String[] args)
   {
      Point p1 = new Point(10, 20);
      Point p2 = new Point(20, 30);
      Point p3 = new Point(10, 20);
      // Test reflexivity
      System.out.println(p1.equals(p1)); // Output: true
      // Test symmetry
      System.out.println(p1.equals(p2)); // Output: false
      System.out.println(p2.equals(p1)); // Output: false
      // Test transitivity
      System.out.println(p2.equals(p3)); // Output: false
      System.out.println(p1.equals(p3)); // Output: true
      // Test nullability
      System.out.println(p1.equals(null)); // Output: false
      // Extra test to further prove the instanceof operator's usefulness.
      System.out.println(p1.equals("abc")); // Output: false
      Map<Point, String> map = new HashMap<Point, String>();
      map.put(p1, "first point");
      System.out.println(map.get(p1)); // Output: first point
      System.out.println(map.get(new Point(10, 20))); // Output: null
   }
}

清单 9-20 的 hashCode() 方法有点冗长,因为它将 x 和 y 分别赋给局部变量 hc ,而不是在哈希代码计算中直接使用这些字段。然而,我决定采用这种方法来更好地反映哈希代码算法。

当您运行这个应用时,最感兴趣的是它的最后两行输出。现在,应用在这两行上正确地呈现了第一个点和第一个点以及第一个点,而不是在两行上呈现第一个点和第二个点以及第一个点和空值。

注意 LinkedHashMap 是 HashMap 的子类,它使用链表来存储条目。因此, LinkedHashMap 的迭代器按照条目插入的顺序返回条目。例如,如果清单 9-19 已经指定了 Map < String,Integer>arg Map = new linked hashmap<String,Integer>();,应用对 java 的输出 HashMapDemo 如果一只土拨鼠可以夹住木头土拨鼠可以夹住多少木头 {how=1,much=1,wood=2,could=2,a=2,土拨鼠=2,chuck=2,if=1} 后跟 distinct 参数个数= 8 。

identity yhashmap〔??〕

IdentityHashMap 类提供了一个映射实现,它在比较键和值时使用引用等式( == )而不是对象等式( equals() )。这故意违反了 Map 的通用契约,该契约要求在比较元素时使用 equals() 。

IdentityHashMap 通过系统的 int identity hashCode(Object x)类方法获取哈希代码,而不是通过每个 key 的 hashCode() 方法。 identityHashCode() 为 x 返回与对象的 hashCode() 方法返回相同的哈希代码,无论 x 的类是否覆盖 hashCode() 。空引用的哈希代码为零。

这些特征赋予了 IdentityHashMap 相对于其他 Map 实现的性能优势。此外, IdentityHashMap 支持可变键(用作键的对象,当它们的字段值在映射中改变时,它们的散列码也会改变)。清单 9-21 对比了 IdentityHashMap 和 HashMap 中的可变键。

清单 9-21 。在可变键上下文中对比身份散列表散列表

import java.util.IdentityHashMap;
import java.util.HashMap;
import java.util.Map;

public class IdentityHashMapDemo
{
   public static void main(String[] args)
   {
      Map<Employee, String> map1 = new IdentityHashMap<Employee, String>();
      Map<Employee, String> map2 = new HashMap<Employee, String>();
      Employee e1 = new Employee("John Doe", 28);
      map1.put(e1, "SALES");
      System.out.println(map1);
      Employee e2 = new Employee("Jane Doe", 26);
      map2.put(e2, "MGMT");
      System.out.println(map2);
      System.out.println("map1 contains key e1 = " + map1.containsKey(e1));
      System.out.println("map2 contains key e2 = " + map2.containsKey(e2));
      e1.setAge(29);
      e2.setAge(27);
      System.out.println(map1);
      System.out.println(map2);
      System.out.println("map1 contains key e1 = " + map1.containsKey(e1));
      System.out.println("map2 contains key e2 = " + map2.containsKey(e2));
   }
}

class Employee
{
   private String name;
   private int age;

   Employee(String name, int age)
   {
      this.name = name;
      this.age = age;
   }

   @Override
   public boolean equals(Object o)
   {
      if (!(o instanceof Employee))
         return false;
      Employee e = (Employee) o;
      return e.name.equals(name) && e.age == age;
   }

   @Override
   public int hashCode()
   {
      int hashCode = 19;
      hashCode = hashCode * 31 + name.hashCode();
      hashCode = hashCode * 31 + age;
      return hashCode;
   }

   void setAge(int age)
   {
      this.age = age;
   }

   void setName(String name)
   {
      this.name = name;
   }

   @Override
   public String toString()
   {
      return name + " " + age;
   }
}

清单 9-21 的 main() 方法创建了 IdentityHashMap 和 HashMap 实例,每个实例存储一个由雇员键和字符串值组成的条目。因为 Employee 实例是可变的(因为 setAge() 和 setName()),main()改变它们的年龄,而这些键存储在它们的映射中。这些更改会产生以下输出:

{John Doe 28=SALES}
{Jane Doe 26=MGMT}
map1 contains key e1 = true
map2 contains key e2 = true
{John Doe 29=SALES}
{Jane Doe 27=MGMT}
map1 contains key e1 = true
map2 contains key e2 = false

最后四行显示更改的条目保留在它们的映射中。然而, map2 的 containsKey() 方法报告其 HashMap 实例不再包含其 Employee 键(应该是 Jane Doe 27 ),而 map1 的 containsKey() 方法报告其 IdentityHashMap 实例仍然包含其 Employee 键,现在是

注意 IdentityHashMap 的文档指出“这个类的一个典型用途是保持拓扑的对象图转换,比如序列化或深度复制。”(我在第十一章中讨论了连载。)它还声明“这个类的另一个典型用途是维护代理对象。”还有,stackoverflow 的“Identity HashMap 的用例”主题(stack overflow . com/questions/838528/Use-Cases-for-Identity-HashMap)提到,当键是 java.lang.Class 对象时,使用 IdentityHashMap 比 HashMap 快得多。

枚举数

EnumMap 类提供了一个映射实现,它的键是同一个 enum 的成员。不允许空键;任何存储空键的尝试都会导致抛出 NullPointerException 。因为枚举映射在内部表示为数组,所以枚举映射在性能方面接近数组。

EnumMap 提供了以下构造函数:

  • enum map(Classkey type)用指定的 keyType 创建一个空的枚举映射。当 keyType 包含空引用时,这个构造函数抛出 NullPointerException 。
  • EnumMap(EnumMap < K,?extends V > m) 使用与 m 相同的键类型和 m 的条目创建一个枚举映射。当 m 包含空引用时,这个构造函数抛出 NullPointerException 。
  • EnumMap(地图< K,?扩展 V > m) 创建一个用 m 的条目初始化的枚举映射。如果 m 是一个 EnumMap 实例,这个构造函数的行为就像前面的构造函数一样。否则, m 必须包含至少一个条目,以确定新枚举映射的键类型。当 m 包含空引用时,该构造函数抛出 NullPointerException ,当 m 不是 EnumMap 实例且为空时,该构造函数抛出 IllegalArgumentException。

清单 9-22 展示了枚举图。

清单 9-22 。一张硬币常数的枚举图

import java.util.EnumMap;
import java.util.Map;

enum Coin
{
   PENNY, NICKEL, DIME, QUARTER
}

public class EnumMapDemo
{
   public static void main(String[] args)
   {
      Map<Coin, Integer> map = new EnumMap<Coin, Integer>(Coin.class);
      map.put(Coin.PENNY, 1);
      map.put(Coin.NICKEL, 5);
      map.put(Coin.DIME, 10);
      map.put(Coin.QUARTER, 25);
      System.out.println(map);
      Map<Coin,Integer> mapCopy = new EnumMap<Coin, Integer>(map);
      System.out.println(mapCopy);
   }
}

EnumMapDemo 创建一个硬币键和整数值的映射。然后,它将几个硬币实例插入到该地图中,并输出该地图。最后,它创建该地图的副本并输出该副本。

当您运行此应用时,它会生成以下输出:

{PENNY=1, NICKEL=5, DIME=10, QUARTER=25}
{PENNY=1, NICKEL=5, DIME=10, QUARTER=25}

探索排序地图

TreeMap 是一个排序映射的例子,这是一个按照升序维护条目的映射,按照关键字的自然排序或者按照创建排序映射时提供的比较器排序。排序后的地图由 SortedMap 界面描述。

SortedMap (其通用类型为 SortedMap < K,V > )扩展 Map 。除了两个例外,它从映射继承的方法在排序映射上的行为与在其他映射上的行为相同:

  • 由任何已排序地图的集合视图上的迭代器()方法返回的迭代器实例按顺序遍历集合。
  • 由集合视图的 toArray() 方法返回的数组按顺序包含键、值或条目。

注意尽管没有保证,集合框架中 SortedMap 实现的集合视图的 toString() 方法(例如 TreeMap )返回一个包含所有视图元素的字符串。

SortedMap 的文档要求一个实现必须提供我在讨论树形图时提出的四个标准构造函数。此外,该接口的实现必须实现表 9-10 中描述的方法。

表 9-10。 分类地图的方法

方法描述
比较器<?超级 K >比较器()返回用于对该映射中的键进行排序的比较器,或者当该映射使用其键的自然排序时返回 null。
设置<地图。条目< K,V>entry set()返回此映射中包含的映射的集合视图。集合的迭代器按照键的升序返回这些条目。因为视图由该地图支持,所以对地图所做的更改会反映在地图集中,反之亦然。
K firstKey()返回当前在这个映射中的第一个(最低的)键,或者当这个映射为空时抛出一个 NoSuchElementException 实例。
SortedMap < K,V>head map(K toKey)返回该地图中键严格小于 toKey 的部分的视图。因为此映射支持返回的映射,所以返回的映射中的更改会反映在此映射中,反之亦然。返回的映射支持该映射支持的所有可选映射操作。当 toKey 与该映射的比较器不兼容时(或者,当映射没有比较器时,当 toKey 没有实现 Comparable ),该方法抛出 ClassCastException;当 toKey 为 null 且该映射不允许空键时,该方法抛出 NullPointerException 以及 IllegalArgumentException
设置key Set()返回包含在此映射中的键的集合视图。集合的迭代器按升序返回键。因为地图支持视图,所以对地图所做的更改会反映在地图集中,反之亦然。
【k 负载 Key()返回当前在这个映射中的最后一个(最高的)键,或者当这个映射为空时抛出一个 NoSuchElementException 实例。
SortedMap < K,V>subMap(K from key,K toKey)返回该地图部分的视图,其键的范围从 fromKey 到 toKey 不等。(当 fromKey 和 toKey 相等时,返回的地图为空。)因为这个映射支持返回的映射,所以返回的映射中的变化反映在这个映射中,反之亦然。返回的映射支持该映射支持的所有可选映射操作。当使用此映射的比较器(或者,当映射没有比较器时,使用自然排序)无法将 fromKey 和 toKey 相互比较时,此方法抛出 ClassCastException ,当 fromKey 或 toKey 为 null 并且此映射不允许 null keys 和 illegalarg 时,此方法抛出 NullPointerException
sort dmap<k,V >尺寸图 (K fromKey)从键返回键大于或等于的地图部分的视图。因为此映射支持返回的映射,所以返回的映射中的更改会反映在此映射中,反之亦然。返回的映射支持该映射支持的所有可选映射操作。当 fromKey 与此映射的比较器不兼容时(或者,当映射没有比较器时,当 fromKey 不实现 Comparable 时),此方法抛出 ClassCastException ,当 from key 为 null 且此映射不允许空元素时,抛出 NullPointerException ,以及 IllegalArgumentException
收藏< V >值()返回此映射中包含的值的集合视图。集合的迭代器按照相应键的升序返回值。因为地图支持集合,所以对地图所做的更改会反映在集合中,反之亦然。

清单 9-23 展示了一个基于树形图的排序图。

清单 9-23 。办公用品名称及数量分类图

import java.util.Comparator;
import java.util.SortedMap;
import java.util.TreeMap;

public class SortedMapDemo
{
   public static void main(String[] args)
   {
      SortedMap<String, Integer> smsi = new TreeMap<String, Integer>();
      String[] officeSupplies =
      {
         "pen", "pencil", "legal pad", "CD", "paper"
      };
      int[] quantities =
      {
         20, 30, 5, 10, 20
      };
      for (int i = 0; i < officeSupplies.length; i++)
          smsi.put(officeSupplies[i], quantities[i]);
      System.out.println(smsi);
      System.out.println(smsi.headMap("pencil"));
      System.out.println(smsi.headMap("paper"));
      SortedMap<String, Integer> smsiCopy;
      Comparator<String> cmp;
      cmp = new Comparator<String>()
                {
                   @Override
                   public int compare(String key1, String key2)
                   {
                      return key2.compareTo(key1); // descending order
                   }
                };
      smsiCopy = new TreeMap<String, Integer>(cmp);
      smsiCopy.putAll(smsi);
      System.out.println(smsiCopy);
   }
}

SortedMapDemo 创建一个排序的地图以及办公用品名称和数量的数组。然后从这些数组开始填充地图。在转储出地图的内容和地图各部分的头部视图后,它以降序创建并输出地图的副本。

当您运行此应用时,它会生成以下输出:

{CD=10, legal pad=5, paper=20, pen=20, pencil=30}
{CD=10, legal pad=5, paper=20, pen=20}
{CD=10, legal pad=5}
{pencil=30, pen=20, paper=20, legal pad=5, CD=10}

探索可导航地图

TreeMap 是导航地图的一个例子,这是一个排序的地图,可以以降序和升序迭代,并且可以报告给定搜索目标的最接近匹配。导航地图由 NavigableMap 接口描述,其通用类型为 NavigableMap < K,V>,扩展了 SortedMap ,如表 9-11 所示。

表 9-11。 导航地图专用方法

方法描述
地图。条目< K,V>ceiling 条目 (K 键)返回与大于或等于键的最小键相关联的键-值映射,如果没有这样的键,则返回 null。当键无法与当前映射中的键进行比较时,该方法抛出 ClassCastException ,当键为 null 且该映射不允许空键时,该方法抛出 NullPointerException 。
K ceilingKey (K 键)返回大于或等于键的最小键,没有该键时返回 null。当键无法与当前映射中的键进行比较时,该方法抛出 ClassCastException ,当键为 null 且该映射不允许空键时,该方法抛出 NullPointerException 。
可导航集< K >后代键集()??]返回此映射中包含的键的逆序可导航集视图。集合的迭代器以降序返回键。此贴图支持集合,因此对贴图的更改会反映在集合中,反之亦然。如果在对集合进行迭代时修改了映射(除了通过迭代器自己的 remove() 操作),那么迭代的结果是不确定的。
航海图< K,V >下行地图()??]返回此映射中包含的映射的逆序视图。该贴图支持降序贴图,因此对贴图的更改会反映在降序贴图中,反之亦然。如果在迭代任一映射的集合视图时修改了任一映射(除了通过迭代器自己的 remove() 操作),迭代的结果是未定义的。
地图。条目< K,V>first entry()返回与此映射中最少的键相关联的键-值映射,或者当映射为空时返回 null。
地图。Entry < K,V > floorEntry (K 键)返回与小于或等于键的最大键相关联的键-值映射,或者当没有这样的键时返回 null。当键无法与当前映射中的键进行比较时,该方法抛出 ClassCastException ,当键为 null 且该映射不允许空键时,该方法抛出 NullPointerException 。
K floorKey (K 键)返回小于或等于键的最大键,如果没有该键,则返回 null。当键无法与当前映射中的键进行比较时,该方法抛出 ClassCastException ,当键为 null 且该映射不允许空键时,该方法抛出 NullPointerException 。
navible map<K,V>head map(K toKey,布尔包含)返回此地图中键小于(或等于,当包含为真 ) toKey 的部分的视图。此地图支持返回的地图,因此返回的地图中的更改会反映在此地图中,反之亦然。返回的映射支持该映射支持的所有可选映射操作。当 toKey 与此映射的比较器不兼容时(或者,当映射没有比较器时,当 toMap 不实现 Comparable ),此方法抛出 ClassCastException;当 toKey 为 null 且此映射不允许空键时,此方法抛出 NullPointerException ,以及 IllegalArgumentException
地图。条目< K,V > higherEntry (K 键)返回与严格大于键的最小键相关联的键-值映射,或者当没有这样的键时返回 null。当键无法与当前映射中的键进行比较时,该方法抛出 ClassCastException ,当键为 null 且该映射不允许空键时,该方法抛出 NullPointerException 。
K higherKey (K 键)返回严格大于键的最小键,或者当没有这样的键时返回 null。当键无法与当前映射中的键进行比较时,该方法抛出 ClassCastException ,当键为 null 且该映射不允许空键时,该方法抛出 NullPointerException 。
地图。条目< K,V>last entry()返回与此映射中最大的键相关联的键-值映射,或者当映射为空时返回 null。
地图。条目< K,V >下级 (K 键)返回与严格小于键的最大键相关联的键-值映射,或者当没有这样的键时返回 null。当键无法与当前映射中的键进行比较时,该方法抛出 ClassCastException ,当键为 null 且该映射不允许空键时,该方法抛出 NullPointerException 。
K lowerKey (K 键)返回严格小于键的最大键,或者当没有这样的键时返回 null。当键无法与当前映射中的键进行比较时,该方法抛出 ClassCastException ,当键为 null 且该映射不允许空键时,该方法抛出 NullPointerException 。
可导航集<【k】>【可导航键集()返回此映射中包含的键的基于集合的可导航视图。集合的迭代器按升序返回键。此贴图支持集合,因此对贴图的更改会反映在集合中,反之亦然。如果在对集合进行迭代时修改了映射(除了通过迭代器自己的 remove() 操作),那么迭代的结果是未定义的。
地图。条目< K,V>pollFirstEntry()移除并返回与此映射中最少的键相关联的键-值映射,或者当映射为空时返回 null。
地图。条目< K,V>pollastentry()移除并返回与此映射中最大的键相关联的键-值映射,或者当映射为空时返回 null。
航海图<【k,v】>(k from key,boolean frominclusive,K toKey,boolean toxiv)返回该地图部分的视图,其键的范围从 fromKey 到 toKey 。(当 fromKey 和 toKey 相等时,返回的地图为空,除非 fromInclusive 和 toInclusive 都为真。)此映射支持返回的映射,因此返回的映射中的更改会反映在此映射中,反之亦然。返回的映射支持该映射支持的所有可选映射操作。当 fromKey 和 toKey 无法使用此映射的比较器进行相互比较时(或者,当映射没有比较器时,使用自然排序),此方法抛出 ClassCastException ,当 fromKey 或 toKey 为 null 并且此映射不允许 null 元素,以及 illegalarg
航海图<【k,v】>尺寸图 (K fromKey,boolean 包括)从 Key 返回该地图中键大于(或等于,当包含为真 ) 的部分的视图。此地图支持返回的地图,因此返回的地图中的更改会反映在此地图中,反之亦然。返回的映射支持该映射支持的所有可选映射操作。当 fromKey 与此映射的比较器不兼容时(或者,当映射没有比较器时,当 fromKey 没有实现 Comparable 时),此方法抛出 ClassCastException;当 fromKey 为 null 且此映射不允许空键时,此方法抛出 NullPointerException

表 9-11 的方法描述了表 9-4 中呈现的 NavigableSet 方法的 NavigableMap ,甚至在两个实例中返回 NavigableSet 实例。

清单 9-24 展示了一个基于树形地图的导航地图。

清单 9-24 。在地图上导航(鸟类,在小范围内计数)条目

import java.util.Iterator;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.TreeMap;

public class NavigableMapDemo
{
   public static void main(String[] args)
   {
      NavigableMap<String, Integer> nm = new TreeMap<String, Integer>();
      String[] birds = { "sparrow", "bluejay", "robin" };
      int[] ints = { 83, 12, 19 };
      for (int i = 0; i < birds.length; i++)
         nm.put(birds[i], ints[i]);
      System.out.println("Map = " + nm);
      System.out.print("Ascending order of keys: ");
      NavigableSet<String> ns = nm.navigableKeySet();
      Iterator iter = ns.iterator();
      while (iter.hasNext())
         System.out.print(iter.next() + " ");
      System.out.println();
      System.out.print("Descending order of keys: ");
      ns = nm.descendingKeySet();
      iter = ns.iterator();
      while (iter.hasNext())
         System.out.print(iter.next() + " ");
      System.out.println();
      System.out.println("First entry = " + nm.firstEntry());
      System.out.println("Last entry = "  + nm.lastEntry());
      System.out.println("Entry < ostrich is " + nm.lowerEntry("ostrich"));
      System.out.println("Entry > crow is " + nm.higherEntry("crow"));
      System.out.println("Poll first entry: " + nm.pollFirstEntry());
      System.out.println("Map = " + nm);
      System.out.println("Poll last entry: " + nm.pollLastEntry());
      System.out.println("Map = " + nm);
   }
}

清单 9-24 的 system . out . println(" Map = "+nm);方法调用依靠 TreeMap 的 toString() 方法来获取可导航地图的内容。

运行该应用时,您会看到以下输出:

Map = {bluejay=12, robin=19, sparrow=83}
Ascending order of keys: bluejay robin sparrow
Descending order of keys: sparrow robin bluejay
First entry = bluejay=12
Last entry = sparrow=83
Entry < ostrich is bluejay=12
Entry > crow is robin=19
Poll first entry: bluejay=12
Map = {robin=19, sparrow=83}
Poll last entry: sparrow=83
Map = {robin=19}

探索数组和集合工具 API

没有它的数组和集合工具类,集合框架将是不完整的。每个类都提供各种类方法,这些方法在集合和数组的上下文中实现有用的算法。

以下是数组类的面向数组的实用方法的示例:

  • 静态< T >列表< T > asList (T。。a) 返回一个由数组 a 支持的固定大小的列表。(对返回列表的更改“直写”到数组。)比如 Listbirds = arrays . aslist(" Robin “、” Oriole “、” blue Jay ");将字符串的三元素数组(回想一下,可变参数序列被实现为数组)转换为列表,其引用被分配给 birds 。
  • static int binary search(int[]a,int key) 使用二分搜索法算法在数组 a 中搜索条目键(在下面的列表中解释)。调用此方法之前,必须对数组进行排序;否则,结果是不确定的。此方法返回搜索关键字的索引(如果它包含在数组中);否则返回(-(插入点)- 1)。插入点是将键插入到数组中的点(第一个元素的索引大于键,或者 a.length 如果数组中的所有元素都小于键),并且当且仅当找到键时,保证返回值大于或等于 0。例如,arrays . binary search(new String[]{ " Robin “,” Oriole “,” Bluejay"}," Oriole") 返回 1, “Oriole” 的索引。
  • 静态空填充 (char[] a,char ch) 将 ch 存储在指定字符数组的每个元素中。比如 Arrays.fill(screen[i],’ ');用空格填充 2D 屏幕数组的 i 行。
  • 静态 void 排序 (long[] a) 将长整型数组 a 中的元素按数字升序排序,例如 long lArray = new long[] { 20000L,89L,66L,33L };arrays . sort(lArray);。
  • 静态< T >无效排序 (T[] a,比较器<?超级 T > c) 使用比较器 c 对数组 a 中的元素进行排序。比如当给定比较器<字符串> cmp =新比较器<字符串>(){ @ Override public int compare(String E1,String E2){ return E2 . compare to(E1);} };String[]内行星= { “水星”、“金星”、“地球”、“火星” };, Arrays.sort(内行星,CMP);使用 cmp 帮助将内行星按其元素降序排序:金星、水星、火星、地球就是结果。

在数组中搜索特定元素有两种常见的算法。线性搜索 从索引 0 到被搜索元素的索引或数组的末尾,逐个元素地搜索数组。平均来说,必须搜索一半的元素;更大的数组需要更长的搜索时间。然而,数组不需要排序。

相比之下,二分搜索法 在有序数组 an 项中搜索元素 e 的时间要快得多。它通过递归执行以下步骤来工作:

  1. 将低索引设置为 0。
  2. 将高索引设置为 n - 1。
  3. 如果低索引>高索引,则打印“无法找到” e 。结束。
  4. 将中间指数设置为(低指数+高指数)/ 2。
  5. 如果 e > a 【中间指标】,那么将低指标设置为中间指标+ 1。转到第三部分。
  6. 如果 e < a 【中间指标】,那么将高指标设置为中间指标- 1。转到第三部分。
  7. 打印“找到” e “在索引”中间索引。

该算法类似于在电话簿中最优地查找名字。从打开书到正中间开始。如果名字不在那一页上,就把书翻到前半部分或后半部分的正中间,这取决于名字出现在哪一半。重复,直到找到名称(或找不到)。

对 4,000,000,000 个元素应用线性搜索会导致大约 2,000,000,000 次比较(平均),这需要时间。相比之下,对 4,000,000,000 个元素应用二分搜索法最多可执行 32 次比较。这就是为什么数组包含 binarySearch() 方法,而不包含 linearSearch() 方法。

以下是集合类的面向集合的类方法的示例:

  • 静态< T 扩展对象&可比<?超 T > > T min(收藏<?extends T > c) 根据元素的自然排序返回集合 c 的最小元素。例如,system . out . println(collections . min(arrays . aslist(10,3,18,25)));输出 3 。所有 c 的元素必须实现可比的接口。此外,所有要素必须相互可比。当 c 为空时,该方法抛出 NoSuchElementException 。
  • 静态无效反转(列表<?> l) 反转列表 l 的元素顺序。比如 Listbirds = arrays . aslist(“知更鸟”、“黄鹂”、“蓝鸟”);Collections.reverse(鸟);system . out . println(birds);产生【蓝鸟、黄鹂、知更鸟】作为输出。
  • 静态ListsingletonList(T o)返回一个只包含对象 o 的不可变列表。例如,list . remove all(collections . singletonlist(null));从列表中删除所有空元素。
  • 静态Setsynchronized Set(Sets)返回一个由指定 set s 支持的同步(线程安全)Set,例如,Setss = collections . synchronized Set(new HashSet());。为了保证串行访问,通过返回的集合完成对后台集合( s )的所有访问是非常重要的。
  • 静态< K,V >贴图< K,V >不可修改贴图(贴图<?扩展 K,?extends V > m) 返回 map m 不可修改的视图,例如 Map < String,Integer>MSI = collections . unmodifablemap(new HashMap<String,Integer>());。查询操作对返回的地图“通读”到指定的地图;并且试图修改返回的地图,无论是直接修改还是通过其集合视图修改,都会导致 UnsupportedOperationException。

注意由于性能原因,集合实现是不同步的——不同步的集合比同步的集合有更好的性能。但是,要在多线程上下文中使用集合,您需要获得该集合的同步版本。您可以通过调用诸如 synchronizedSet() 这样的方法来获得该版本。

您可能想知道集合类中各种空类方法的用途。例如,static finalListemptyList()返回一个不可变的空列表,如 Listls = collections . emptyList();。这些方法之所以存在,是因为它们提供了在某些上下文中返回 null(并避免潜在的 NullPointerException s)的有用替代方法。考虑清单 9-25 中的。

清单 9-25 。空无一物的鸟类名单

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

class Birds
{
   private List<String> birds;

   Birds()
   {
      birds = Collections.emptyList();
   }

   Birds(String. . . birdNames)
   {
      birds = new ArrayList<String>();
      for (String birdName: birdNames)
         birds.add(birdName);
   }

   @Override
   public String toString()
   {
      return birds.toString();
   }
}

class EmptyListDemo
{
   public static void main(String[] args)
   {
      Birds birds = new Birds();
      System.out.println(birds);
      birds = new Birds("Swallow", "Robin", "Bluejay", "Oriole");
      System.out.println(birds);
   }
}

清单 9-25 声明了一个 Birds 类,它在一个列表中存储了各种鸟类的名字。这个类提供了两个构造函数:一个无参数构造函数和一个接受可变数量的字符串参数的构造函数,这些参数标识不同的鸟。

noargument 构造函数调用 emptyList() 将其私有的 birds 字段初始化为空的 List 的字符串 — emptyList() 是一个泛型方法,编译器从其上下文中推断其返回类型。

如果你想知道是否需要 emptyList() ,看看 toString() 方法。注意,这个方法计算的是 birds.toString() 。如果您没有将对空列表<字符串> 的引用分配给 birds , birds 将包含 null (创建对象时此实例字段的默认值),并且在尝试评估 birds.toString() 时将抛出一个 NullPointerException 实例。

当您运行这个应用( java EmptyListDemo )时,它会生成以下输出:

[]
[Swallow, Robin, Bluejay, Oriole]

emptyList() 方法实现如下:return(List)EMPTY _ List;。该语句返回分配给集合类中 EMPTY_LIST 类字段的单个列表实例。

你可能想直接使用 EMPTY_LIST ,但是如果你这样做的话,你会遇到一个未检查的警告消息,因为 EMPTY_LIST 被声明为原始类型 List ,混合原始和泛型类型会导致这样的消息。虽然您可以取消警告,但是最好使用 emptyList() 方法。

假设您向 Birds 添加了一个 void setBirds(ListBirds)方法,并向该方法传递一个空列表,如 Birds . setBirds(collections . empty List());。编译器将响应一条错误消息,指出它要求参数的类型为 List < String > ,但参数的类型为 List < Object > 。这样做是因为编译器无法从上下文中找出正确的类型,所以它选择了 List < Object > 。

有一个方法可以解决这个问题,这个方法看起来可能会很奇怪。指定 birds.setBirds(集合。emptyList());,其中形式类型形参列表及其实际类型实参出现在成员访问操作符之后,方法名之前。编译器现在知道正确的类型参数是字符串,并且 emptyList() 将返回列表<字符串> 。

探索传统集合 API

Java 1.2 引入了集合框架。在 Java 包含框架之前,开发人员在集合方面有两种选择:创建自己的框架,或者使用 Java 1.0 引入的向量、枚举、堆栈、字典、哈希表、属性和位集类型。

Vector 是一个描述可增长数组的具体类,很像 ArrayList 。与 ArrayList 实例不同, Vector 实例是同步的。 Vector 已经被通用化,并且也进行了改进以支持集合框架,这使得诸如 ListList = new Vector();合法。

集合框架提供了迭代器来迭代集合的元素。相比之下, Vector 的 elements() 方法通过 Enumeration 的 hasmorelements()和 nextElement() 方法返回一个实现 Enumeration 接口的类的实例,用于枚举(迭代并返回)Vector 实例的元素。

Vector 被具体的 Stack 类子类化,它代表一个 LIFO 数据结构。 Stack 提供了一个 E push(E item) 方法用于将一个对象推送到堆栈上,一个 E pop() 方法用于从堆栈顶部弹出一个项目,以及其他一些方法,比如 boolean empty() 用于确定堆栈是否为空。

栈 就是糟糕的 API 设计的一个很好的例子。通过继承 Vector ,可以调用 Vector 的 void add(int index,E element) 方法在任意位置添加一个元素,并破坏 Stack 实例的完整性。事后看来, Stack 在设计时就应该使用 composition:使用一个 Vector 实例来存储一个 Stack 实例的元素。

Dictionary 是将键映射到值的子类的抽象超类。具体的哈希表类是字典的唯一子类。与 Vector 一样,哈希表实例被同步,哈希表被通用化,哈希表被改进以支持集合框架。

Hashtable 由 Properties 子类化,一个具体的类表示一组持久的属性(基于字符串的键/值对,用于标识应用设置)。 Properties 提供了用于存储属性的 Object set property(String key,String value) 和用于返回属性值的 String getProperty(String key)。

注意应用将属性用于各种目的。例如,如果你的应用有一个图形用户界面,你可以通过一个属性对象将它的主窗口的屏幕位置和大小存储在一个文件中,这样应用可以在下次运行时恢复窗口的位置和大小。

属性 是糟糕的 API 设计的另一个很好的例子。通过从哈希表继承,可以调用哈希表的 V put(K key,V value) 方法来存储一个带有非字符串键和/或非字符串值的条目。事后看来, Properties 应该有杠杆合成:在一个 Hashtable 实例中存储一个 Properties 实例的元素。

注意在第四章中,我讨论了包装类,这就是堆栈和属性应该如何实现。

最后,位集 是一个具体的类,它描述了一组可变长度的位。这个类能够表示任意长度的位集,这与之前描述的基于整数的固定长度位集形成对比,固定长度位集的最大成员数受到限制:基于 int 的位集有 32 个成员,基于 long 的位集有 64 个成员。

BitSet 提供了一对构造函数,用于初始化 BitSet 实例: BitSet() 初始化实例,以初始存储依赖于实现的位数,而 BitSet(int nbits) 初始化实例,以初始存储 nbits 位。 BitSet al so 提供了各种方法,包括以下几种:

  • void 与 (位集 bs) 对这个位集与 bs 进行按位与运算。修改该位集,使得当它和 bs 中相同位置的位为 1 时,该位被设置为 1。
  • void andNot (位集 bs) 将该位集中的所有位设置为 0,其对应的位在 bs 中设置为 1。
  • void clear() 将该位集中的所有位设置为 0。
  • Object clone() 克隆这个位集,产生一个新的位集。克隆的位设置为 1,与此位集完全相同。
  • Boolean get(int bit index)在从零开始的 bitIndex 处,返回该位集的位的值,作为 Boolean true/false 值(true 为 1,false 为 0)。当 bitIndex 小于 0 时,该方法抛出 IndexOutOfBoundsException。
  • int length() 返回该位集的“逻辑大小”,即最高 1 位加 1 的索引,如果该位集不包含 1 位,则返回 0。
  • void or (位集 bs) 用 bs 对该位集进行按位异或运算。修改该位集,使得当该位或 bs 中相同位置的位为 1 或两个位都为 1 时,该位被设置为 1。
  • void set(int bit index,boolean value) 将从零开始的位 bitIndex 设置为值 (true 转换为 1;false 转换为 0)。当 bitIndex 小于 0 时,该方法抛出 IndexOutOfBoundsException。
  • int size() 返回该位集用来表示位值的位数。
  • String toString() 根据为 1 的位的位置返回该位集的字符串表示,例如 {4,5,9,10} 。
  • void xor (位集)用 bs 对该位集进行按位异或运算。修改该位集,使得当该位或 bs 中相同位置的位(但不是两者)为 1 时,该位被设置为 1。

清单 9-26 展示了一个应用,它演示了其中一些方法,并让你更深入地了解按位 AND ( & )、按位异或( | )和按位异或(【^】)运算符是如何工作的。

清单 9-26 。使用可变长度位集

import java.util.BitSet;

public class BitSetDemo
{
   public static void main(String[] args)
   {
      BitSet bs1 = new BitSet();
      bs1.set(4, true);
      bs1.set(5, true);
      bs1.set(9, true);
      bs1.set(10, true);
      BitSet bsTemp = (BitSet) bs1.clone();
      dumpBitset("        ", bs1);
      BitSet bs2 = new BitSet();
      bs2.set(4, true);
      bs2.set(6, true);
      bs2.set(7, true);
      bs2.set(9, true);
      dumpBitset("        ", bs2);
      bs1.and(bs2);
      dumpSeparator(Math.min(bs1.size(), 16));
      dumpBitset("AND (&) ", bs1);
      System.out.println();
      bs1 = bsTemp;
      dumpBitset("        ", bs1);
      dumpBitset("        ", bs2);
      bsTemp = (BitSet) bs1.clone();
      bs1.or(bs2);
      dumpSeparator(Math.min(bs1.size(), 16));
      dumpBitset("OR (|)  ", bs1);
      System.out.println();
      bs1 = bsTemp;
      dumpBitset("        ", bs1);
      dumpBitset("        ", bs2);
      bsTemp = (BitSet) bs1.clone();
      bs1.xor(bs2);
      dumpSeparator(Math.min(bs1.size(), 16));
      dumpBitset("XOR (^) ", bs1);
   }

   static void dumpBitset(String preamble, BitSet bs)
   {
      System.out.print(preamble);
      int size = Math.min(bs.size(), 16);
      for (int i = 0; i < size; i++)
         System.out.print(bs.get(i) ? "1" : "0");
      System.out.print("  size(" + bs.size() + "), length(" + bs.length() + ")");
      System.out.println();
   }

   static void dumpSeparator(int len)
   {
      System.out.print("        ");
      for (int i = 0; i < len; i++)
         System.out.print("-");
      System.out.println();
   }
}

为什么我在 dumpBitset() 中指定了 Math.min(bs.size(),16) ,并传递了一个类似的表达式给 dumpSeparator() ?我想精确地显示 16 位和 16 个破折号(为了美观),并且需要考虑位集的大小小于 16。虽然这不会发生在 Oracle 和 Google 的位集类上,但它可能会发生在其他变体上。

当您运行此应用时,它会生成以下输出:

       0000110001100000  size(64), length(11)
        0000101101000000  size(64), length(10)
        ----------------
AND (&) 0000100001000000  size(64), length(10)

        0000110001100000  size(64), length(11)
        0000101101000000  size(64), length(10)
        ----------------
OR (|)  0000111101100000  size(64), length(11)

        0000110001100000  size(64), length(11)
        0000101101000000  size(64), length(10)
        ----------------
XOR (^) 0000011100100000  size(64), length(11)

注意与向量和哈希表,位集不同步。当在多线程上下文中使用位集时,必须在外部同步对此类的访问。

集合框架已经使得向量、枚举、栈、字典、哈希表过时。这些类型仍然是标准类库的一部分,以支持遗留代码。此外,首选项 API 使得属性在很大程度上过时了。因为位集仍然相关,所以这个类还在继续改进(最近是 Java 7)。

注意当你意识到可变长度位集的用处时,位集得到改进就不足为奇了。由于其紧凑性和其他优点,变长位集通常用于实现操作系统的优先级队列,并有助于内存页面分配。面向 Unix 的文件系统也使用位集来帮助分配索引节点(信息节点)和磁盘扇区。位集在霍夫曼编码 中很有用,这是一种用于实现无损数据压缩的数据压缩算法。

练习

以下练习旨在测试您对第九章内容的理解:

  1. 什么是收藏?
  2. 什么是集合框架?
  3. 集合框架主要由哪些组件组成?
  4. 定义可比。
  5. 什么时候你会让一个类实现可比的接口?
  6. 什么是比较器,它的目的是什么?
  7. 是非判断:集合使用比较器来定义其元素的自然排序。
  8. Iterable 接口描述了什么?
  9. 集合接口代表什么?
  10. 确定集合的 add() 方法会抛出 UnsupportedOperationException 类的实例的情况。
  11. Iterable 的迭代器()方法返回实现迭代器接口的类的实例。这个接口提供了什么方法?
  12. 增强的 for 循环语句的目的是什么?
  13. 增强的 for 循环语句是如何表达的?
  14. 是非判断:增强的 for 循环适用于数组。
  15. 定义自动装箱。
  16. 定义 unboxing。
  17. 什么是列表?
  18. 一个 ListIterator 实例使用什么来导航一个列表?
  19. 什么是视图?
  20. 为什么要使用 subList() 方法?
  21. ArrayList 类提供了什么?
  22. LinkedList 类提供了什么?
  23. 定义节点。
  24. 是非判断: ArrayList 比 LinkedList 提供更快的元素插入和删除。
  25. 什么是集合?
  26. 树集类提供什么?
  27. HashSet 类提供了什么?
  28. 是非判断:为了避免 hashset 中的重复元素,您自己的类必须正确地覆盖 equals() 和 hashCode() 。
  29. HashSet 和 LinkedHashSet 有什么区别?
  30. EnumSet 类提供了什么?
  31. 定义排序集。
  32. 什么是可导航集合?
  33. 是非判断: HashSet 是有序集合的一个例子。
  34. 为什么当你试图添加一个元素到一个有序集合时,这个有序集合的 add() 方法会抛出 ClassCastException ?
  35. 什么是队列?
  36. 是非判断: Queue 的 element() 方法在空队列上被调用时抛出 NoSuchElementException 。
  37. PriorityQueue 类提供什么?
  38. 什么是地图?
  39. TreeMap 类提供什么?
  40. HashMap 类提供了什么?
  41. 哈希表用什么把键映射到整数值?
  42. 继续上一个问题,产生的整数值被称为什么,它们完成什么?
  43. 什么是哈希表的容量?
  44. 什么是哈希表的加载因子?
  45. HashMap 和 LinkedHashMap 有什么区别?
  46. IdentityHashMap 类提供了什么?
  47. EnumMap 类提供了什么?
  48. 定义排序映射。
  49. 什么是可导航地图?
  50. 是非判断: TreeMap 是一个排序地图的例子。
  51. 数组的目的是什么类的静态< T >列表< T > asList(T。。法阵)?
  52. 是非判断:二分搜索法比线性搜索慢。
  53. 您将使用哪个集合方法来返回 hashset 的同步变体?
  54. 识别七种面向遗留集合的类型。
  55. 作为数组列表有用性的一个例子,创建一个 JavaQuiz 应用,展示一个关于 Java 特性的基于多项选择的测验。 JavaQuiz 类的 main() 方法首先用 QuizEntry 数组中的条目填充数组列表(例如,new QuizEntry(" Java 的原始名称是什么?",new String[] { "Oak “,” Duke “,” J ",“以上都不是” },’ A’) )。每个条目由一个问题、四个可能的答案和正确答案的字母(A、B、C 或 D)组成。 main() 然后使用数组列表的 iterator() 方法返回一个 Iterator 实例,这个实例的 hasNext() 和 next() 方法对列表进行迭代。每次迭代输出问题和四个可能的答案,然后提示用户输入正确的选择。用户(通过 System.in.read() )输入 A、B、C 或 D 后, main() 输出一条消息,说明用户是否做出了正确的选择。
  56. 哈希代码生成算法中为什么用(int)(f ^(f>>>32))代替 (int) (f ^ (f > > 32)) ?
  57. 集合提供了静态 int 频率(集合<?> c,对象 o) 方法返回集合 c 元素等于 o 的个数。创建一个 FrequencyDemo 应用,该应用读取其命令行参数,并将除最后一个参数之外的所有参数存储在一个列表中,然后调用 frequency() ,将列表和最后一个命令行参数作为该方法的参数。然后,它输出该方法的返回值(最后一个命令行参数在前面的命令行参数中出现的次数)。例如, java FrequencyDemo 应该输出 null = 0 的出现次数,而 java FrequencyDemo 如果一只土拨鼠会夹木头的话一只土拨鼠能夹多少木头应该输出木头出现次数= 2 。

摘要

集合是一组对象,存储在为此目的而设计的类的实例中。为了使您不必创建自己的集合类,Java 提供了表示和操作集合的集合框架。

集合框架主要由核心接口、实现类以及数组和集合工具类组成。核心接口使得独立于集合的实现来操作集合成为可能。

核心接口包括可迭代、集合、列表、集合、分类集合、导航集合、队列、队列、地图、分类地图、导航地图。集合扩展可迭代;列表、集合,以及队列各自扩展集合;分类设置扩展设置;可导航集合扩展分类集合;队列延伸队列; SortedMap 扩展 Map;导航地图扩展分类地图。

实现类包括 ArrayList , LinkedList , TreeSet , HashSet , LinkedHashSet , EnumSet , PriorityQueue , ArrayDeque , TreeMap , HashMap , LinkedHashMap 每个具体类的名称都以核心接口名称结尾,标识它所基于的核心接口。

没有它的数组和集合工具类,集合框架是不完整的。每个类都提供各种类方法,在数组和集合的上下文中实现有用的算法。例如,数组让您有效地搜索和排序数组,而集合让您获得同步的和不可修改的集合。

在 Java 1.2 引入集合框架之前,开发人员可以创建自己的框架,或者使用 Java 1.0 引入的向量、枚举、堆栈、字典、哈希表、属性和位集类型。

集合框架已经使得向量、枚举、栈、字典、哈希表过时。此外,首选项 API 使得属性在很大程度上过时了。因为位集仍然相关,所以这个类继续被改进。

在第十章的中,我继续探索工具 API,重点是并发工具、日期类、格式化程序类、随机类、扫描程序类等等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值