六、掌握高级语言功能:第二部分

在第 2 章到第 4 章中,我为学习 Java 语言打下了基础,在第 5 章中,我在这个基础上引入了一些 Java 更高级的语言特性。在第 6 章的中,我将继续关注与断言、注释、泛型和枚举相关的高级语言特性。

掌握断言

写源代码不是一件容易的事情。太多的时候,错误被引入到代码中。如果在编译源代码之前没有发现 bug,那么它就会变成运行时代码,而运行时代码很可能会意外失败(或者没有失败的迹象,但给出错误的输出)。此时,很难确定失败的原因。

开发人员经常对应用的正确性做出假设,一些开发人员认为,在注释位置指定注释来陈述他们认为什么是真的就足以确定正确性。然而,注释对于防止错误是没有用的,因为编译器会忽略它们。

许多语言通过提供一种叫做断言的语言特性来解决这个问题,这种语言特性让开发人员编写关于应用正确性的假设。当应用运行时,如果断言失败,应用将终止,并显示一条消息,帮助开发人员诊断失败的原因。(您可能认为断言是编译器能够理解的注释。)

注意在他的“Assert Statements Shine Light Into Dark Corners”博客文章(www . drdobbs . com/CPP/Assert-Statements-Shine-Light-Into-Dark/240012746)中,计算机科学家安德鲁·克尼格提到断言是用来检测不变量故障的,其中不变量是你的代码中不应该改变的东西。例如,您可能希望在尝试通过二分搜索法算法搜索列表之前验证数据项列表已排序(不变量)的预期,该算法要求列表已排序。你可以使用断言来了解不变量是否成立。

在这一节中,我将向您介绍 Java 的断言语言特性。在定义了这个术语,向您展示了如何声明断言,并提供了示例之后,我将着眼于使用和避免断言。最后,您将学习如何通过 javac 编译器工具的命令行参数有选择地启用和禁用断言。

声明断言

断言是一个让你通过布尔表达式表达程序正确性假设的语句。如果该表达式的计算结果为 true,则继续执行下一条语句。否则,将引发一个标识失败原因的错误。

断言语句有两种形式,每种形式都以保留字 assert 开始:

assert expression1 ;
assert expression1 : expression2 ;

在该语句的两种形式中,表达式 1 是布尔表达式。在第二种形式中,表达式 2 是任何返回值的表达式。它不能调用返回类型为 void 的方法。

当表达式 1 评估为 false 时,该语句实例化类 java.lang.AssertionError 。第一个语句表单调用该类的 noargument 构造函数,它没有将标识失败细节的消息与 AssertionError 实例相关联。第二种形式调用一个 AssertionError 构造函数,其类型与 expression2 的值的类型相匹配。该值被传递给构造函数,其字符串表示形式被用作错误的详细信息。

当引发错误时,源文件的名称和引发错误的行号作为引发错误的堆栈跟踪的一部分输出到控制台。在许多情况下,这些信息足以确定导致失败的原因,应该使用断言语句的第一种形式。

清单 6-1 展示了断言语句的第一种形式。

清单 6-1 。抛出没有详细消息的断言错误

public class AssertionDemo
{
   public static void main(String[] args)
   {
      int x = 1;
      assert x == 0;
   }
}

当启用断言时(我将在后面讨论这个任务),运行前面的应用会产生以下输出:

Exception in thread "main" java.lang.AssertionError
        at AssertionDemo.main(AssertionDemo.java:6)

在其他情况下,需要更多的信息来帮助诊断失败的原因。例如,假设表达式 1 比较变量 x 和 y ,当 x 的值超过 y 的值时抛出错误。因为这种情况永远不会发生,所以您可能会使用第二种语句形式来输出这些值,以便可以诊断问题。

清单 6-2 展示了断言语句的第二种形式。

清单 6-2 。抛出带有详细消息的断言错误

public class AssertionDemo
{
   public static void main(String[] args)
   {
      int x = 1;
      assert x == 0: x;
   }
}

同样,假设断言是启用的。运行前面的应用会产生以下输出:

Exception in thread "main" java.lang.AssertionError: 1
        at AssertionDemo.main(AssertionDemo.java:6)

x 中的值被附加到第一个输出行的末尾,这有点神秘。为了使这个输出更有意义,您可能希望指定一个表达式,其中也包括变量的名称:assert x = = 0:" x = "+x;例如。

使用断言

在很多情况下应该使用断言。这些情况被组织成内部不变量、控制流不变量和契约设计类别。不变量是你的代码中不应该改变的东西。

内部不变量

一个内部不变量是面向表达式的行为,不期望改变。例如,清单 6-3 通过链式 if-else 语句引入了一个内部不变量,它根据水的温度输出水的状态。

清单 6-3 。发现内部不变量可以变化

public class IIDemo
{
   public static void main(String[] args)
   {
      double temperature = 50.0; // Celsius
      if (temperature < 0.0 )
         System.out.println("water has solidified");
      else
      if (temperature >= 100.0)
         System.out.println("water is boiling into a gas");
      else
      {
         // temperature > 0.0 and temperature < 100.0
         assert(temperature > 0.0 && temperature < 100.0): temperature;
         System.out.println("water is remaining in its liquid state");
      }
   }
}

开发人员可能只指定一个注释来陈述一个假设,即什么表达式导致最终的 else 到达。因为注释可能不足以检测出潜伏的 < 0.0 表达式 bug(水在零度也是固体),所以断言语句是必要的。

内部不变量的另一个例子与没有默认情况的 switch 语句有关。因为开发人员相信所有的路径都被覆盖了,所以避免了默认的情况。然而,这并不总是正确的,如清单 6-4 所示。

清单 6-4 。另一个错误的内部不变量

public class IIDemo
{
   final static int NORTH = 0;
   final static int SOUTH = 1;
   final static int EAST = 2;
   final static int WEST = 3;

   public static void main(String[] args)
   {
      int direction = (int) (Math.random() * 5 );
      switch (direction)
      {
         case NORTH: System.out.println("travelling north"); break;
         case SOUTH: System.out.println("travelling south"); break;
         case EAST : System.out.println("travelling east"); break;
         case WEST : System.out.println("travelling west"); break;
         default   : assert false;
      }
   }
}

清单 6-4 假设 switch 测试的表达式将只计算四个整数常量中的一个。但是, (int) (Math.random() * 5) 也可以返回 4,导致默认情况下执行 assert false;,总是抛出 AssertionError 。(您可能需要运行这个应用几次才能看到断言错误,但是首先您需要学习如何启用断言,这将在本章的后面讨论。)

提示当断言被禁用时,断言为假;不执行,错误未被发现。要一直检测这个 bug,替换 assert false;用投新的 AssertionError(方向);。

控制流不变量

控制流不变量是不期望改变的控制流。例如,清单 6-4 使用一个断言来测试一个假设,即开关的默认情况不会执行。清单 6-5 ,修复了清单 6-4 的 bug,提供了另一个例子。

清单 6-5 。一个错误的控制流不变量

public class CFDemo
{
   final static int NORTH = 0;
   final static int SOUTH = 1;
   final static int EAST = 2;
   final static int WEST = 3;

   public static void main(String[] args)
   {
      int direction = (int) (Math.random() * 4);
      switch (direction)
      {
         case NORTH: System.out.println("travelling north"); break;
         case SOUTH: System.out.println("travelling south"); break;
         case EAST : System.out.println("travelling east"); break;
         case WEST : System.out.println("travelling west");
         default   : assert false;
      }
   }
}

因为原来的 bug 已经修复了,所以永远不会达到默认情况。但是,省略终止 case WEST 的 break 语句会导致执行到达默认 case。这个控制流不变量被打破了。(同样,您可能需要运行这个应用几次才能看到断言错误,但是首先您需要学习如何启用断言,这将在本章后面讨论。)

注意使用断言语句来检测不应该执行的代码时要小心。如果断言语句不能根据詹姆斯·高斯林、比尔·乔伊、盖伊·斯蒂尔和吉拉德·布拉查(Addison-Wesley,2005;ISBN:0321246780;也可在【http://docs.oracle.com/javase/specs/】的 T5T7),编译器会报错。例如,为(;;);断言假;由于无限 for 循环阻止断言语句执行,导致编译器报告错误。

合同设计

契约式设计(【http://en.wikipedia.org/wiki/Design_by_contract】)是一种基于前置条件、后置条件和类不变量来设计软件的方法。断言语句支持非正式的契约式设计风格的开发。

前提条件

一个前提条件是当一个方法被调用时必须为真的东西。断言语句通常用于通过检查参数是否合法来满足助手方法的前提条件。清单 6-6 提供了一个例子。

清单 6-6 。验证前提条件

public class Lotto649
{
   public static void main(String[] args)
   {
      // Lotto 649 requires that six unique numbers be chosen.
      int[] selectedNumbers = new int[6];
      // Assign a unique random number from 1 to 49 (inclusive) to each slot
      // in the selectedNumbers array.
      for (int slot = 0; slot < selectedNumbers.length; slot++)
      {
           int num;
           // Obtain a random number from 1 to 49\. That number becomes the
           // selected number if it has not previously been chosen.
           try_again:
           do
           {
               num = rnd(49) + 1;
               for (int i = 0; i < slot; i++)
                    if (selectedNumbers[i] == num)
                        continue try_again;
               break;
           }
           while (true);
           // Assign selected number to appropriate slot.
           selectedNumbers[slot] = num;
      }
      // Sort all selected numbers into ascending order and then print these
      // numbers.
      sort(selectedNumbers);
      for (int i = 0; i < selectedNumbers.length; i++)
           System.out.print(selectedNumbers[i] + " ");
   }

   static int rnd(int limit)
   {
      // This method returns a random number (actually, a pseudorandom number)
      // ranging from 0 through limit - 1 (inclusive).
      assert limit > 1: "limit = " + limit;
      return (int) (Math.random() * limit);
   }

   static void sort(int[] x)
   {
      // This method sorts the integers in the passed array into ascending
      // order.
      for (int pass = 0; pass < x.length - 1; pass++)
         for (int i = x.length - 1; i > pass; i--)
            if (x[i] < x[pass])
            {
               int temp = x[i];
               x[i] = x[pass];
               x[pass] = temp;
            }
   }
}

清单 6-6 的应用模拟了 Lotto 6/49,这是加拿大的国家彩票游戏之一。 rnd() 助手方法 返回一个在 0 和限制 - 1 之间随机选择的整数。断言语句验证了前提条件,即限制的值必须为 2 或更高。

注意Sort()助手方法通过实现一种叫做冒泡排序的算法(完成某项任务的诀窍)将(排序)选择的数字数组的整数按升序排序。

冒泡排序的工作原理是对数组进行多次遍历。在每次传递期间,各种比较和交换确保下一个最小的元素值“冒泡”到数组的顶部,这将是索引 0 处的元素。

冒泡排序效率不高,但对于排序六元素数组来说绰绰有余。虽然我可以使用位于 java.util 包的 Arrays 类中的一个有效的 sort() 方法(例如,Arrays . sort(selected numbers));完成与清单 6-6 的排序(selectedNumbers)相同的目标;方法调用,但这样做更有效),我选择使用冒泡排序,因为我更喜欢等到第 9 章进入数组类。

后置条件

一个后置条件是在一个方法成功完成后必须为真的东西。断言语句通常用于通过检查结果是否合法来满足助手方法的后置条件。清单 6-7 提供了一个例子。

清单 6-7 。除了前提条件之外,还要验证后置条件

public class MergeArrays
{
   public static void main(String[] args)
   {
      int[] x = { 1, 2, 3, 4, 5 };
      int[] y = { 1, 2, 7, 9 };
      int[] result = merge(x, y);
      for (int i = 0; i < result.length; i++)
         System.out.println(result[i]);
   }

   static int[] merge(int[] a, int[] b)
   {
      if (a == null)
         throw new NullPointerException("a is null");
      if (b == null)
         throw new NullPointerException("b is null");
      int[] result = new int[a.length + b.length];
      // Precondition
      assert result.length == a.length + b.length: "length mismatch";
      for (int i = 0; i < a.length; i++)
         result[i] = a[i];
      for (int i = 0; i < b.length; i++)
         result[a.length + i - 1 ] = b[i];
      // Postcondition
      assert containsAll(result, a, b): "value missing from array";
      return result;
   }

   static boolean containsAll(int[] result, int[] a, int[] b)
   {
      for (int i = 0; i < a.length; i++)
         if (!contains(result, a[i]))
            return false;
      for (int i = 0; i < b.length; i++)
         if (!contains(result, b[i]))
            return false;
      return true;
   }

   static boolean contains(int[] a, int val)
   {
      for (int i = 0; i < a.length; i++)
         if (a[i] == val)
            return true;
      return false;
   }
}

清单 6-7 使用一个断言语句来验证合并后的两个数组中的所有值都出现在合并后的数组中。然而,后置条件并不满足,因为这个清单包含一个 bug。

清单 6-7 也显示了前置条件和后置条件一起使用。唯一的前提条件验证合并后的数组长度等于在合并逻辑之前被合并的数组的长度。

类不变量

一个类不变量是一种内部不变量,它在任何时候都适用于一个类的每个实例,除了当一个实例从一个一致状态转换到另一个一致状态的时候。

例如,假设一个类的实例包含数组,数组的值按升序排序。您可能希望在类中包含一个 isSorted() 方法 ,当数组仍然排序时返回 true,并验证修改数组的每个构造函数和方法都指定了 assert is sorted();在退出之前,满足构造函数或方法退出时数组仍然排序的假设。

避免断言

尽管有很多情况下应该使用断言,但也有一些情况下应该避免使用断言。例如,您不应该使用断言来检查传递给公共方法的参数,原因如下:

  • 检查公共方法的参数是该方法及其调用方之间存在的契约的一部分。如果您使用断言来检查这些参数,并且如果断言被禁用,那么就违反了该契约,因为参数将不会被检查。
  • 断言还防止引发适当的异常。例如,当一个非法参数被传递给一个公共方法时,通常会抛出 Java . lang . illegalargumentexception 或 Java . lang . nullpointerexception。然而, AssertionError 却被抛出。

您还应该避免使用断言来执行应用正常运行所需的工作。这项工作通常是作为断言的布尔表达式的副作用来执行的。当断言被禁用时,工作不会被执行。

例如,假设您有一个 Employee 对象的列表和一些空引用,它们也存储在这个列表中,您想要删除所有的空引用。通过下面的断言语句删除这些引用是不正确的:

assert employees.removeAll(null);

尽管断言语句不会抛出 AssertionError ,因为在雇员列表中至少有一个空引用,但是当断言被禁用时,依赖于该语句执行的应用将会失败。

与其依赖前面的代码来移除空引用,不如使用类似下面的代码:

boolean allNullsRemoved = employees.removeAll(null);
assert allNullsRemoved;

这一次,无论断言是启用还是禁用,所有的空引用都将被删除,并且您仍然可以指定一个断言来验证空引用是否已被删除。

启用和禁用断言

编译器在类文件中记录断言。但是,断言在运行时被禁用,因为它们会影响性能。断言可能会调用一个需要一段时间才能完成的方法,这会影响正在运行的应用的性能。

在测试关于类行为的假设之前,必须启用类文件的断言。在运行 java 应用启动工具时,通过指定 -enableassertions 或 -ea 命令行选项来完成这项任务。

-enableassertions 和 -ea 命令行选项允许您基于以下参数之一启用各种粒度的断言(除了没有参数的情况,您必须使用冒号将选项与其参数分开):

  • 无参数:断言在除系统类之外的所有类中都启用。
  • 包名T2。。。:通过指定包名后跟,断言在指定的包及其子包中被启用。。。。
  • 。。。:断言在未命名的包中启用,这个包恰好是当前的目录。
  • ClassName :通过指定类名在命名类中启用断言。

例如,当通过 Java–ea merge arrays 运行 MergeArrays 应用时,可以启用除系统断言之外的所有断言。此外,您可以通过指定 Java–ea:logging test logger 来启用您可能添加到第 5 章的日志包中的任何断言。

通过指定–disable assessments 或–da 命令行选项,可以禁用断言,也可以以不同的粒度禁用断言。这些选项采用与 -enableassertions 和 -ea 相同的参数。例如,Java-ea–da:lone class main class 启用除了 loneclass 中的断言之外的所有断言。(将 loneclass 和 mainclass 视为您指定的实际类的占位符。)

前面的选项适用于所有的类装入器。除了不带参数时,它们也适用于系统类。这个异常简化了除系统类之外的所有类中断言语句的启用,这通常是所希望的。

要启用系统断言,请指定 -enablesystemassertions 或 -esa ,例如 Java-esa–ea:logging test logger。指定-disable system assessments 或 -dsa 来禁用系统断言。

掌握注释

在开发 Java 应用时,您可能希望用注释(将元数据[描述其他数据的数据]与各种应用元素相关联)。例如,您可能想要标识未完全实现的方法,以便不会忘记实现它们。Java 的注释语言特性让您可以完成这项任务。

在这一节中,我将向您介绍注释。在定义了这个术语并给出了三种编译器支持的注释作为例子之后,我将向您展示如何声明您自己的注释类型并使用这些类型来注释源代码。最后,您会发现如何处理自己的注释来完成有用的任务。

注意 Java 一直支持特别注释机制。例如, java.lang.Cloneable 接口标识了可以通过 java.lang.Object 的 clone() 方法 浅克隆其实例的类;瞬态保留字标记在序列化过程中被忽略的字段,而 @deprecated Javadoc 标记记录不再受支持的方法。相反,注释特性是注释代码的标准。

发现注释

一个注释是一个注释类型的实例,它将元数据与一个应用元素相关联。它在源代码中通过在类型名前面加上 @ 符号来表示。例如, @Readonly 是一个注释, Readonly 是它的类型。

注意你可以使用注释将元数据与构造函数、字段、局部变量、方法、包、参数和类型(注释、类、枚举和接口)关联起来。

编译器支持覆盖、弃用和 SuppressWarnings 注释类型。这些类型位于 java.lang 包中。

@Override 注释对于表达子类方法覆盖超类中的方法,而不是重载该方法是有用的。下面的示例显示了此批注用于作为重写方法的前缀:

@Override
public void draw(int color)
{
   // drawing code
}

@Deprecated 批注用于指示标记的应用元素 deprecated (逐步淘汰)且不应再使用。当不推荐使用的应用元素被非推荐使用的代码访问时,编译器会发出警告。

相比之下, @deprecated javadoc 标签和相关文本会警告您不要使用不推荐使用的项目,并告诉您应该使用什么来代替。下面的例子说明了 @Deprecated 和 @deprecated 可以一起使用:

/**
 * Allocates a <code>Date</code> object and initializes it so that
 * it represents midnight, local time, at the beginning of the day
 * specified by the <code>year</code>, <code>month</code>, and
 * <code>date</code> arguments.
 *
 * @param   year    the year minus 1900.
 * @param   month   the month between 0-11.
 * @param   date    the day of the month between 1-31.
 * @see     java.util.Calendar
 * @deprecated As of JDK version 1.1,
 * replaced by <code>Calendar.set(year + 1900, month, date)</code>
 * or <code>GregorianCalendar(year + 1900, month, date)</code>.
 */
@Deprecated
public Date(int year, int month, int date)
{
    this(year, month, date, 0, 0, 0);
}

这个例子摘录了 Java 的 Date 类中的一个构造函数(位于 java.util 包中)。它的 Javadoc 注释显示, Date(int year,int month,int date) 已经被弃用,取而代之的是在 Calendar 类(也位于 java.util 包中)中使用 set() 方法。我在第十章的中探索日期。)

当编译单元(通常是类或接口)引用不推荐使用的类、方法或字段时,编译器会取消警告。这个特性允许你修改遗留的 API 而不会产生不赞成的警告,在清单 6-8 中有演示。

清单 6-8 。从同一类声明中引用不推荐使用的字段

public class Employee
{
   /**
    * Employee's name
    * @deprecated New version uses firstName and lastName fields.
    */
   @Deprecated
   String name;
   String firstName;
   String lastName;

   public static void main(String[] args)
   {
      Employee emp = new Employee();
      emp.name = "John Doe";
   }
}

清单 6-8 声明了一个雇员类,该类带有一个名字字段,该字段已被弃用。虽然 Employee 的 main() 方法引用了 name ,但是编译器会抑制一个弃用警告,因为弃用和引用发生在同一个类中。

假设您通过引入一个新的 UseEmployee 类并将 Employee 的 main() 方法移到这个类来重构这个清单。清单 6-9 展示了最终的类结构。

清单 6-9 。从另一个类声明中引用不推荐使用的字段

class Employee
{
   /**
    * Employee's name
    * @deprecated New version uses firstName and lastName fields.
    */
   @Deprecated
   String name;
   String firstName;
   String lastName;
}

public class UseEmployee
{
   public static void main(String[] args)
   {
      Employee emp = new Employee();
      emp.name = "John Doe";
   }
}

如果您试图通过 javac 编译器工具编译该源代码,您将会发现以下消息:

Note: UseEmployee.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

您需要将 -Xlint:deprecation 指定为 javac 的命令行参数之一(如在 javac-Xlint:deprecation UseEmployee.java)以发现不推荐的项目和引用该项目的代码:

Employee.java:18: warning: [deprecation] name in Employee has been deprecated
      emp.name = "John Doe";
         ^
1 warning

@SuppressWarnings 注释对于通过“不赞成”或“未检查”参数来抑制不赞成或未检查的警告很有用。(当混合使用泛型和前泛型遗留代码的代码时,会出现未检查的警告。我将在本章后面讨论泛型和未检查的警告。)

例如,当 UseEmployee 类的 main() 方法中的代码访问 Employee 类的 name 字段时,清单 6-10 使用带有 "deprecation" 参数的 @SuppressWarnings 来抑制编译器的反对警告。

清单 6-10 。取消先前的弃用警告

public class UseEmployee
{
   @SuppressWarnings("deprecation")
   public static void main(String[] args)
   {
      Employee emp = new Employee();
      emp.name = "John Doe";
   }
}

注意作为一种风格,你应该总是在嵌套最深的元素上指定 @SuppressWarnings ,在那里它是有效的。例如,如果您想要取消特定方法中的警告,您应该注释该方法而不是它的类。

声明注释类型和注释源代码

在注释源代码之前,您需要可以实例化的注释类型。除了 Override 、 Deprecated 和 SuppressWarnings 之外,Java 还提供了许多注释类型。Java 也允许你声明你自己的类型。

通过指定符号 @ ,紧接着是保留字接口,然后是类型名,最后是主体,来声明一个注释类型。例如,清单 6-11 使用 @interface 声明一个名为 Stub 的注释类型。

清单 6-11 。声明存根注释类型

public @interface Stub
{
}

除了名称之外不提供任何数据的注释类型的实例——它们的主体是空的——被称为标记注释,因为它们出于某种目的标记应用元素。如清单 6-12 所示, @Stub 用于标记空方法(Stub)。

清单 6-12 。注释一个被剔除的方法 T3

public class Deck // Describes a deck of cards.
{
   @Stub
   public void shuffle()
   {
      // This method is empty and will presumably be filled in with appropriate
      // code at some later date.
   }
}

清单 6-12 的 Deck 类声明了一个空的 shuffle() 方法 。这一事实通过实例化存根并在 shuffle() 的方法头前加上结果@存根注释来表示。

注意虽然标记接口(在第 4 章中介绍)似乎已经被标记注释所取代,但事实并非如此,因为标记接口比标记注释更有优势。一个优点是标记接口指定了由标记类实现的类型,这让您可以在编译时发现问题。例如,当一个类没有实现可克隆的接口时,它的实例不能通过对象的 clone() 方法进行浅层克隆。如果 Cloneable 已经被实现为一个标记注释,这个问题直到运行时才会被发现。

虽然标记注释很有用 (@Override 和 @Deprecated 就是很好的例子),但是您通常会希望增强注释类型,以便可以通过它的实例存储元数据。您可以通过向类型中添加元素来完成此任务。

一个元素是一个出现在注释类型主体中的方法头。它不能有参数或 throws 子句,它的返回类型必须是基元类型(如 int )、 java.lang.String 、 java.lang.Class 、枚举、注释类型或前面类型的数组。但是,它可以有默认值。

清单 6-13 向存根添加了三个元素。

清单 6-13 。向存根注释类型添加三个元素

public @interface Stub
{
   int id(); // A semicolon must terminate an element declaration.
   String dueDate();
   String developer() default "unassigned";
}

id() 元素指定了一个标识存根的 32 位整数。 dueDate() 元素指定了一个基于字符串的日期,该日期标识了何时实现方法存根。最后, developer() 指定负责编码方法存根的开发人员的基于字符串的名称。

与 id() 和 dueDate() 不同, developer() 是用默认值、“未赋值”来声明的。当您实例化存根并且没有在该实例中给 developer() 赋值时,就像清单 6-14 的情况一样,这个默认值被赋给 developer() 。

清单 6-14 。初始化一个存根实例的元素

public class Deck
{
   @Stub
   (
      id = 1,
      dueDate = "12/21/2012"
   )
   public void shuffle()
   {
   }
}

清单 6-14 展示了一个 @Stub 注释,它将其 id() 元素初始化为 1 ,将其 dueDate() 元素初始化为 "12/21/2012" 。每个元素名称没有尾随的 () ,两个元素初始化器的逗号分隔列表出现在 ( 和 ) 之间。

假设您决定用单个 String value()element 替换 Stub 的 id() 、 dueDate() 和 developer() 元素,该元素的字符串指定逗号分隔的 id、到期日期和开发人员姓名值。清单 6-15 展示了两种初始化值的方法。

清单 6-15 。初始化每个存根实例的值()元素

public class Deck
{
   @Stub(value = "1,12/21/2012,unassigned")
   public void shuffle()
   {
   }

   @Stub("2,12/21/2012,unassigned")
   public Card[] deal(int ncards)
   {
      return null;
   }
}

清单 6-15 揭示了对值()元素的特殊处理。当它是注释类型的唯一元素时,可以从初始化器中省略 value() 的名称和 = 。我用这个事实指定了清单 6-10 中的@ suppress warnings(" deprecation ")。

在注释类型声明中使用元注释

每个覆盖、弃用和抑制警告注释类型本身用元注释(注释注释类型的注释)进行注释。例如,清单 6-16 向您展示了 SuppressWarnings 注释类型是用两个元注释进行注释的。

清单 6-16 。带注释的 SuppressWarnings 类型声明

@Target(value={TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE})
@Retention(value=SOURCE)
public @interface SuppressWarnings

位于 java.lang.annotation 包中的目标注释类型,标识了注释类型所适用的应用元素的种类。 @Target 表示 @SuppressWarnings 批注可以用来批注类型、字段、方法、参数、构造函数和局部变量。

每个类型、字段、方法、参数、构造器和局部变量都是元素类型枚举的成员,该枚举也位于 java.lang.annotation 包中。(我将在本章后面讨论枚举。)

分配给 Target 的 value() 元素的逗号分隔值列表周围的 { 和 } 字符表示一个数组— value() 的返回类型是 String[] 。尽管这些大括号是必需的(除非数组包含一项),但是在初始化 @Target 时可以省略 value= ,因为 Target 只声明了一个 value() 元素。

位于 java.lang.annotation 包中的 Retention 注释类型,标识了一个注释类型的注释的保留期(也称为生存期)。 @Retention 表示 @SuppressWarnings 注释的生存期仅限于源代码——它们在编译后不存在。

SOURCE 是 RetentionPolicy enum 的成员之一(位于 java.lang.annotation 包中)。其他成员是类和运行时。这三个成员指定了以下保留策略:

  • CLASS :编译器在类文件中记录注释,但是虚拟机不保留它们(为了节省内存空间)。这是默认策略。
  • 运行时:编译器在类文件中记录注释,虚拟机保留它们,以便在运行时可以通过反射 API 读取它们。
  • SOURCE :编译器在使用注释后将其丢弃。

清单 6-11清单 6-13 中显示的存根注释类型有两个问题。首先,缺少一个 @Target 元注释意味着您可以注释任何应用元素 @Stub 。然而,这种注释只有在应用于方法和构造函数时才有意义。查看清单 6-17 。

清单 6-17 。注释不需要的应用元素

@Stub("1,12/21/2012,unassigned")
public class Deck
{
   @Stub("2,12/21/2012,unassigned")
   private Card[] cardsRemaining = new Card[52];

   @Stub("3,12/21/2012,unassigned")
   public Deck()
   {
   }

   @Stub("4,12/21/2012,unassigned")
   public void shuffle()
   {
   }

   @Stub("5,12/21/2012,unassigned")
   public Card[] deal(@Stub("5,12/21/2012,unassigned") int ncards)
   {
      return null;
   }
}

清单 6-17 使用 @Stub 来注释 Deck 类、 cardsRemaining 字段和 ncards 参数,以及注释构造函数和两个方法。前三个应用元素不适合注释,因为它们不是存根。

您可以通过在存根注释类型声明的前面加上 @Target({ElementType。方法,ElementType。CONSTRUCTOR}) 以便存根只适用于方法和构造函数。这样做之后,当您试图编译清单 6-17 中的时, javac 编译器工具将输出以下错误消息:

Deck.java:1: error: annotation type not applicable to this kind of declaration
@Stub("1,12/21/2012,unassigned")
^
Deck.java:4: error: annotation type not applicable to this kind of declaration
   @Stub("2,12/21/2012,unassigned")
   ^
Deck.java:18: error: annotation type not applicable to this kind of declaration
   public Card[] deal(@Stub("5,12/21/2012,unassigned") int ncards)
                      ^
3 errors

第二个问题是默认的类保留策略使得在运行时无法处理@存根注释。您可以通过在存根类型声明前面加上 @Retention(RetentionPolicy)来解决这个问题。运行时)。

清单 6-18 展示了带有期望的@目标和@保留元注释的存根注释类型。

清单 6-18 。一个改版的存根标注类型

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface Stub
{
   String value();
}

注意 Java 还在 java.lang.annotation 包中提供了记录的和继承的元注释类型。由 javadoc 和类似工具记录的@记录的-注释的注释类型的实例,而由@继承的-注释的注释类型的实例是自动继承的。根据继承的 Java 文档,如果“用户查询一个类声明上的注释类型,而该类声明没有该类型的注释,那么该类的超类将自动查询该注释类型。这个过程将被重复,直到找到该类型的注释,或者到达类层次结构的顶部(对象)。如果没有超类具有这种类型的注释,那么查询将指示所讨论的类没有这样的注释。”

处理注释

声明一个注释类型并使用该类型注释源代码是不够的。除非您对这些注释做一些特殊的处理,否则它们将保持休眠状态。完成特定任务的一种方法是编写处理注释的应用。清单 6-19 的 StubFinder 应用就是这么做的。

清单 6-19 。 StubFinder 应用

import java.lang.reflect.Method;

public class StubFinder
{
   public static void main(String[] args) throws Exception
   {
      if (args.length != 1)
      {
         System.err.println("usage: java StubFinder classfile");
         return;
      }
      Method[] methods = Class.forName(args[0]).getMethods();
      for (int i = 0; i < methods.length; i++)
         if (methods[i].isAnnotationPresent(Stub.class))
         {
            Stub stub = methods[i].getAnnotation(Stub.class);
            String[] components = stub.value().split(",");
            System.out.println("Stub ID = " + components[0]);
            System.out.println("Stub Date = " + components[1]);
            System.out.println("Stub Developer = " + components[2]);
            System.out.println();
         }
   }
}

StubFinder 加载一个 classfile,其名称被指定为命令行参数,并输出与每个 public 方法头之前的每个 @Stub 注释相关联的元数据。这些注释是清单 6-18 的存根注释类型的实例。

StubFinder 接下来使用一个名为类的特殊类及其 forName() 类方法 来加载一个类文件。类还提供了一个 getMethods() 方法 ,该方法返回一个 Java . lang . reflect . method 对象的数组,这些对象描述了加载的类的公共方法。

对于每个循环迭代,调用一个方法对象的 isanotationpresent()方法 来确定该方法是否用存根类(称为存根.类)描述的注释进行了注释。

如果 isanotationpresent()返回 true,方法的 getAnnotation() 方法 被调用返回注释存根实例。调用该实例的 value() 方法来检索存储在注释中的字符串。

接下来, String 的 split() 方法 被调用,将字符串的 ID、日期和开发者值的逗号分隔列表拆分成一个由 String 对象组成的数组。然后每个对象连同描述性文本一起输出。(你会在第七章正式被介绍给 split() 。)

类的 forName() 方法 能够抛出各种异常,这些异常必须作为方法头的一部分进行处理或显式声明。为了简单起见,我选择在 main() 方法的头部添加一个 throws Exception 子句。

注意抛出异常有两个问题。首先,处理异常并给出合适的错误消息通常比通过抛出 main() 来“推卸责任”要好。其次,异常是通用的——它隐藏了抛出的异常类型的名称。然而,我发现在一次性工具中指定抛出异常很方便。

在编译完 StubFinder ( 贾瓦克 StubFinder.java)、存根 ( 贾瓦克 Stub.java)、以及清单 6-15 的 Deck 类(贾瓦克 Deck.java)之后,运行 StubFinder ,将 Deck 作为其单一命令行参数( java StubFinder Deck )。您将看到以下输出:

Stub ID = 1
Stub Date = 12/21/2012
Stub Developer = unassigned

Stub ID = 2
Stub Date = 12/21/2012
Stub Developer = unassigned

掌握泛型

Java 5 引入了 generics ,用于声明和使用与类型无关的类和接口的语言特性。当使用 Java 的集合框架时(我在第 9 章中介绍了),这些特性帮助你避免 Java . lang . classcastexceptions

注意虽然泛型的主要用途是集合框架,但是标准类库也包含了与这个框架无关的泛型化(为利用泛型而改造的)类: java.lang.Class 、 java.lang.ThreadLocal 和 Java . lang . ref . weak reference 就是三个例子。

在这一节中,我将向您介绍泛型。首先学习泛型如何在集合框架类的上下文中促进类型安全,然后在泛型类型和泛型方法的上下文中探索泛型。最后,您将了解数组上下文中的泛型。

集合和类型安全的需要

Java 的集合框架使得在各种容器(称为集合)中存储对象并在以后检索这些对象成为可能。例如,您可以将对象存储在列表、集合或映射中。然后,您可以检索单个对象,或者循环访问集合并检索所有对象。

在 Java 5 改革集合框架以利用泛型之前,没有办法阻止集合包含混合类型的对象。编译器在将一个对象添加到集合之前不会检查它的类型是否合适,这种静态类型检查的缺乏导致了 ClassCastException s 。

清单 6-20 展示了生成一个 ClassCastException 是多么容易。

清单 6-20 。缺乏类型安全导致运行时出现 ClassCastException

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

class Employee
{
   private String name;

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

   String getName()
   {
      return name;
   }
}

public class TypeSafety
{
   public static void main(String[] args)
   {
      List employees = new ArrayList();
      employees.add(new Employee("John Doe"));
      employees.add(new Employee("Jane Smith"));
      employees.add("Jack Frost");
      Iterator iter = employees.iterator();
      while (iter.hasNext())
      {
         Employee emp = (Employee) iter.next();
         System.out.println(emp.getName());
      }
   }
}

清单 6-20 的 main() 方法首先实例化 java.util.ArrayList ,然后使用这个列表集合对象的引用将一对 Employee 对象添加到列表中。然后它添加了一个字符串对象,这违反了数组列表 应该只存储雇员对象的隐含契约。

main() next 获取一个 java.util.Iterator 实例用于迭代 Employee s 的列表,只要迭代器的 hasNext() 方法 返回 true,就调用其 next() 方法返回一个存储在数组列表中的对象。

next() 返回的对象必须向下转换为雇员,这样就可以调用雇员对象的 getName() 方法来返回雇员的姓名。该方法返回的字符串然后通过 System.out.println() 输出到标准输出设备。

(Employee) cast 检查由 next() 返回的每个对象的类型,以确保它是一个雇员。虽然这适用于前两个对象,但不适用于第三个对象。将“白色杀机”转换为雇员的尝试导致了 ClassCastException 。

因为假设列表是同质的,所以发生了 ClassCastException 。换句话说,列表只存储单一类型或一系列相关类型的对象。实际上,该列表是异构的,因为它可以存储任何对象。

清单 6-21 的基于泛型的同质列表避免了 ClassCastException 。

清单 6-21 。缺乏类型安全导致编译器错误

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

class Employee
{
   private String name;

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

   String getName()
   {
      return name;
   }
}

public class TypeSafety
{
   public static void main(String[] args)
   {
      List<Employee> employees = new ArrayList<Employee>();
      employees.add(new Employee("John Doe"));
      employees.add(new Employee("Jane Smith"));
      employees.add("Jack Frost");
      Iterator<Employee> iter = employees.iterator();
      while (iter.hasNext())
      {
         Employee emp = iter.next();
         System.out.println(emp.getName());
      }
   }
}

清单 6-21 的重构的 main() 方法说明了泛型的核心特性,即参数化类型(一个类或接口名,后跟一个尖括号分隔的类型列表,标识了在该上下文中什么类型的对象是合法的)。

例如, java.util.List <员工>T1】表示列表中只能存储员工对象。如图所示, <雇员> 指定必须与数组列表重复,如在数组列表<雇员> 中,它是存储雇员 s 的集合实现。

另外,迭代器<雇员> 表示迭代器() 返回一个迭代器,其 next() 方法只返回雇员对象。没有必要将 iter.next() 的返回值转换为 Employee 的值,因为编译器会代表您插入转换。

如果您试图编译这个清单,编译器会在遇到 employees.add("白色杀机")时报告一个错误;。错误信息会告诉你编译器在 Java . util . list接口中找不到 add(java.lang.String) 方法。

与前泛型列表接口中声明的 add(Object) 方法不同,泛型化列表接口的 add() 方法参数反映了接口的参数化类型名称。比如列表<员工> 隐含添加(员工)。

清单 6-20 揭示了导致 class cast exception(employees . add("白色杀机"))的不安全代码;)和触发异常的代码( (Employee) iter.next() )相当接近。但是,在较大的应用中,它们之间的距离往往更远。

您不必在寻找最终导致 ClassCastException 的不安全代码时处理愤怒的客户,您可以依靠编译器通过在编译期间检测到代码时报告错误来为您节省这种挫折和精力。在编译时检测类型安全违规是使用泛型的主要好处。

通用类型

泛型类型是一个类或接口,它通过声明一个形式类型参数列表 (尖括号之间的类型参数名称的逗号分隔列表)来引入一族参数化类型。该语法表示如下:

class *identifier* < *formal_type_parameter_list* > {}
interface *identifier* < *formal_type_parameter_list* > {}

例如,ListT1】为泛型类型,其中 List 为接口,类型参数 E 标识列表的元素类型。类似地, Map < K,V>T7】是一个泛型类型,其中 Map 是一个接口,类型参数 K 和 V 标识 Map 的键和值类型。

注意当声明一个泛型类型时,习惯上指定单个大写字母作为类型参数名。此外,这些名称应该有意义。例如, E 表示元素, T 表示类型, K 表示键, V 表示值。如果可能,您应该避免选择在使用它的地方没有意义的类型参数名称。比如 List < E > 表示元素列表,但是 List < S > 是什么意思呢?

参数化类型是泛型类型的实例。每个参数化类型都用类型名替换泛型类型的类型参数。例如,ListT1(Employee 的 List )和 ListT7(String 的 List )就是基于 ListT13】的参数化类型的例子。同样, Map < String,Employee > 是基于 Map < K,V > 的参数化类型的一个例子。

替换类型形参的类型名称为实际类型实参 。泛型支持五种实际类型参数:

  • 具体类型 :将类或接口的名称传递给类型参数。例如,列出<员工>员工;指定列表元素是雇员实例。
  • 具体参数化类型 :参数化类型的名称被传递给类型参数。例如,列表<列表<字符串> >名称列表;指定列表元素是字符串列表。
  • 数组类型 :将数组传递给类型参数。例如,列表<字符串[] >国家;指定列表元素是由字符串组成的数组,可能是城市名。
  • 类型参数 :将一个类型参数传递给类型参数。例如,给定类声明类 X < E > {列表< E >队列;} , X 的类型参数 E 传递给列表的类型参数 E 。
  • 通配符T3:?被传递给类型参数。比如列表<?>列表;指定列表元素未知。在本章的后面,你会学到通配符。

泛型类型还标识了一个原始类型 ,它是一个没有类型参数的泛型类型。比如列表<员工> 的 raw 类型是列表。Raw 类型是非泛型的,可以保存任何对象。

注意 Java 允许原始类型与泛型混合,以支持在泛型出现之前编写的大量遗留代码。但是,每当编译器在源代码中遇到原始类型时,它都会输出一条警告消息。

声明并使用自己的泛型类型

声明自己的泛型类型并不难。除了指定一个正式的类型参数列表之外,泛型类型还在它的整个实现过程中指定它的类型参数。例如,清单 6-22 声明了一个队列< E > 泛型类型。

清单 6-22 。声明并使用一个队列T3】泛型类型

public class Queue<E>
{
   private E[] elements;
   private int head, tail;

   @SuppressWarnings("unchecked")
   Queue(int size)
   {
      if (size < 2)
         throw new IllegalArgumentException("" + size);
      elements = (E[]) new Object[size];
      head = 0;
      tail = 0;
   }

   void insert(E element) throws QueueFullException
   {
      if (isFull())
         throw new QueueFullException();
      elements[tail] = element;
      tail = (tail + 1) % elements.length;
   }

   E remove() throws QueueEmptyException
   {
      if (isEmpty())
         throw new QueueEmptyException();
      E element = elements[head];
      head = (head + 1) % elements.length;
      return element;
   }

   boolean isEmpty()
   {
      return head == tail;
   }

   boolean isFull()
   {
      return (tail + 1) % elements.length == head;
   }

   public static void main(String[] args)
      throws QueueFullException, QueueEmptyException
   {
      Queue<String> queue = new Queue<String>(6);
      System.out.println("Empty: " + queue.isEmpty());
      System.out.println("Full: " + queue.isFull());
      System.out.println("Adding A");
      queue.insert("A");
      System.out.println("Adding B");
      queue.insert("B");
      System.out.println("Adding C");
      queue.insert("C");
      System.out.println("Adding D");
      queue.insert("D");
      System.out.println("Adding E");
      queue.insert("E");
      System.out.println("Empty: " + queue.isEmpty());
      System.out.println("Full: " + queue.isFull());
      System.out.println("Removing " + queue.remove());
      System.out.println("Empty: " + queue.isEmpty());
      System.out.println("Full: " + queue.isFull());
      System.out.println("Adding F");
      queue.insert("F");
      while (!queue.isEmpty())
         System.out.println("Removing " + queue.remove());
      System.out.println("Empty: " + queue.isEmpty());
      System.out.println("Full: " + queue.isFull());
   }
}

class QueueEmptyException extends Exception
{
}

class QueueFullException extends Exception
{
}

清单 6-22 声明了队列 、 QueueEmptyException 和 QueueFullException 类。后两个类描述了从前一个类的方法中抛出的检查异常。

Queue 实现了一个队列,一个按照先进先出顺序存储元素的数据结构。一个元件在尾部插入,在头部移除。当头部等于尾部时,队列为空,当尾部比头部少一个时,队列为满。因此,一个大小为 n 的队列最多可以存储 n - 1 个元素。

注意到队列T1】的 E 类型参数出现在整个源代码中。例如, E 出现在元素数组声明中来表示数组的元素类型。 E 也被指定为 insert() 的参数类型和 remove() 的返回类型。

E 也出现在 elements =(E[])new Object【size】;。(我稍后会解释为什么我指定了这个表达式,而不是指定更紧凑的元素= new E[size];表情。)

E[] 强制转换导致编译器警告该强制转换未被检查。编译器担心从对象[] 向下转换到 E[] 可能会导致违反类型安全,因为任何类型的对象都可以存储在对象[] 中。

在这个例子中,编译器的担心是不合理的。非 E 对象不可能出现在 E[] 数组中。因为警告在这个上下文中没有意义,所以通过在构造函数前面加上@ suppress warnings(" unchecked ")来取消警告。

注意抑制未检查的警告时要小心。您必须首先证明一个 ClassCastException 不会发生,然后您可以取消警告。

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

Empty: true
Full: false
Adding A
Adding B
Adding C
Adding D
Adding E
Empty: false
Full: true
Removing A
Empty: false
Full: false
Adding F
Removing B
Removing C
Removing D
Removing E
Removing F
Empty: true
Full: false

类型参数界限

列表T1】的 E 类型参数和映射< K,V>T5】的 K 和 V 类型参数都是无界类型参数 的例子。您可以将任何实际类型参数传递给未绑定的类型参数。

有时有必要限制可以传递给类型参数的实际类型变量的种类。例如,您可能想要声明一个类,其实例只能存储抽象形状类的子类的实例(例如圆形和矩形)。

为了限制实际类型参数,您可以指定一个上限,这是一个可以作为实际类型参数的类型上限的类型。上限是通过保留字扩展后跟类型名来指定的。

例如,ShapesList将 Shape 标识为上界。可以指定 ShapesList <圆形>T5、 ShapesList <矩形> ,甚至 ShapesList <形状> ,但不能指定 ShapesList <字符串> ,因为字符串不是形状的子类。

您可以为类型参数分配多个上限,其中第一个上限是一个类或接口,每个附加的上限是一个接口,方法是使用&字符( & )来分隔绑定名称。考虑清单 6-23

清单 6-23 。为类型参数指定多个上限 T3

abstract class Shape
{
}

class Circle extends Shape implements Comparable<Circle>
{
   private double x, y, radius;

   Circle(double x, double y, double radius)
   {
      this.x = x;
      this.y = y;
      this.radius = radius;
   }

   @Override
   public int compareTo(Circle circle)
   {
      if (radius < circle.radius)
         return -1;
      else
      if (radius > circle.radius)
         return 1;
      else
         return 0;
   }

   @Override
   public String toString()
   {
      return "(" + x + ", " + y + ", " + radius + ")";
   }
}

class SortedShapesList<S extends Shape & Comparable<S>>
{
   @SuppressWarnings("unchecked")
   private S[] shapes = (S[]) new Shape[2];
   private int index = 0;

   void add(S shape)
   {
      shapes[index++] = shape;
      if (index < 2)
         return;
      System.out.println("Before sort: " + this);
      sort();
      System.out.println("After sort: " + this);
   }

   private void sort()
   {
      if (index == 1)
         return;
      if (shapes[0].compareTo(shapes[1]) > 0)
      {
         S shape = (S) shapes[0];
         shapes[0] = shapes[1];
         shapes[1] = shape;
      }
   }

   @Override
   public String toString()
   {
      return shapes[0].toString() + " " + shapes[1].toString();
   }
}

public class SortedShapesListDemo
{
   public static void main(String[] args)
   {
      SortedShapesList<Circle> ssl = new SortedShapesList<Circle>();
      ssl.add(new Circle(100, 200, 300));
      ssl.add(new Circle(10, 20, 30));
   }
}

清单 6-23 的 Circle 类扩展了 Shape 并实现了 java.lang.Comparable 接口,用于指定 Circle 对象的自然排序。接口的 compareTo() 方法 通过返回值反映顺序来实现这种排序:

  • 当当前对象应该在以某种方式传递给 compareTo() 的对象之前时,返回负值。
  • 当 current 和 argument 对象相同时,将返回零值。
  • 当当前对象应该在 argument 对象之后时,返回一个正值。

Circle 的覆盖 compareTo() 方法根据半径比较两个 Circle 对象。该方法将半径较小的圆实例排在半径较大的圆实例之前。

sorted shapes listclass 指定 < S 扩展形状&可比< S > > 作为其参数列表。传递给 S 参数的实际类型实参必须是 Shape 的子类,并且还必须实现 Comparable 接口。

注意包含类型参数的类型参数界限被称为递归类型界限 。比如 S 中的可比T8】扩展形状&可比T10】就是一个递归类型绑定。递归类型界限很少见,通常与用于指定类型自然排序的可比接口一起出现。

Circle 满足这两个条件:它子类化形状并实现可比性。因此,编译器在遇到 main() 方法的 sorted shapes listSSL = new sorted shapes list();声明。

上限提供额外的静态类型检查,保证参数化类型遵守其界限。这种保证意味着可以安全地调用上限的方法。比如 sort() 可以调用 Comparable 的 compareTo() 方法。

如果您运行这个应用,您会发现下面的输出,它显示了两个圆形对象按照半径的升序排序:

Before sort: (100.0, 200.0, 300.0) (10.0, 20.0, 30.0)
After sort: (10.0, 20.0, 30.0) (100.0, 200.0, 300.0)

注意类型参数不能有下限。Angelika Langer 在她的“Java 泛型常见问题解答”中解释了这一限制的基本原理,网址为

类型参数范围

类型参数的范围(可见性)是其泛型类型,除非屏蔽了(隐藏)。此范围包括类型参数是其成员的形式类型参数列表。例如, SortedShapesList < S 中的 S 的范围扩展了 Shape&Comparable>是所有的 SortedShapesList 和形式类型参数表。

通过在嵌套类型的形式类型参数列表中声明同名的类型参数,可以屏蔽类型参数。例如,清单 6-24 屏蔽了一个封闭类的 T 类型参数。

清单 6-24 。屏蔽类型变量

class EnclosingClass<T>
{
   static class EnclosedClass<T extends Comparable<T>>
   {
   }
}

EnclosingClass 的 T 类型参数被 EnclosedClass 的 T 类型参数屏蔽,该参数指定了一个上限,在该上限处,只有那些实现了 compatible 接口的类型才能被传递给 EnclosedClass 。从 EnclosedClass 中引用 T 是指传递给 EnclosingClass 的有界的 T 而不是无界的 T 。

如果不需要屏蔽,最好为类型参数选择不同的名称。例如,您可以指定 enclosed class>。虽然 U 不像 T 那样是一个有意义的名字,但这种情况证明了这种选择的合理性。

需要通配符

假设你已经创建了一个字符串 的列表,并且想要输出这个列表。因为您可能会创建一个雇员 的列表和其他类型的列表,所以您希望该方法输出一个任意的列表的对象 。您最终创建了清单 6-25 。

清单 6-25 。试图输出对象的列表

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

public class OutputList
{
   public static void main(String[] args)
   {
      List<String> ls = new ArrayList<String>();
      ls.add("first");
      ls.add("second");
      ls.add("third");
      outputList(ls);
   }

   static void outputList(List<Object> list)
   {
      for (int i = 0; i < list.size(); i++)
         System.out.println(list.get(i));
   }
}

现在你已经完成了你的目标(或者你是这么认为的),你可以通过 OutputList.java 的 javac 编译清单 6-25 。令您惊讶的是,您会收到以下错误消息:

OutputList.java:12: error: method outputList in class OutputList cannot be applied to given types;
      outputList(ls);
      ^
  required: List<Object>
  found: List<String>
  reason: actual argument List<String> cannot be converted to List<Object> by method invocation conversion
1 error

这个错误消息是由于没有意识到泛型类型的基本规则:对于给定的 y 类型的子类型 x,并且给定 G 作为原始类型声明,G < x >不是 GT1】的子类型。

要理解这个规则,必须刷新一下对子类型多态性的理解(见第四章)。基本上,一个子类型是它的父类型的一个特化类型。例如,圆是一种特殊的形状,而弦是一种特殊的物体。这种多态行为也适用于具有相同类型参数的相关参数化类型(例如, List < Object > 是一种专门的 Java . util . collection)。

但是,这种多态行为不适用于多个参数化类型,这些类型的区别仅在于一个类型参数是另一个类型参数的子类型。比如列表<字符串> 就不是列表<对象> 的一种专门化。下面的示例揭示了为什么只有类型参数不同的参数化类型不是多态的:

List<String> ls = new ArrayList<String>();
List<Object> lo = ls;
Lo.add(new Employee());
String s = ls.get(0);

此示例不会编译,因为它违反了类型安全。如果它被编译,一个 ClassCastException 实例将在运行时被抛出,因为在最后一行隐式转换为字符串。

第一行实例化了一个字符串的列表,第二行将其引用向上转换为一个对象的列表。第三行向对象的列表添加一个新的雇员对象。第四行通过 get() 获得 Employee 对象,并试图将其赋给字符串引用变量的 List 。但是,由于隐式转换为字符串—雇员不是字符串,因此抛出 ClassCastException 。

注意虽然您不能将列表<字符串> 向上转换为列表<对象> ,但是您可以将列表<字符串> 向上转换为原始类型列表,以便与遗留代码进行互操作。

前述错误信息揭示了字符串的列表并不是对象的列表。要在不违反类型安全的情况下调用清单 6-25 的 outputList() 方法,只能传递一个清单<对象> 类型的参数,这就限制了该方法的有用性。

然而,泛型提供了一个解决方案:通配符参数(?),代表任意类型。通过将 outputList() 的参数类型从列表<对象> 改为列表<?> ,可以用一个字符串的列表,一个职员的列表调用 outputList() ,以此类推。

通用方法

假设您需要一个方法来将任何类型对象的一个列表复制到另一个列表。尽管您可能会考虑编写一个 void copy List(Listsrc,List < Object > dest) 方法,但是这个方法的用处有限,因为它只能复制元素类型为 Object 的列表。例如,您不能复制一个列表<员工> 。

如果您想要传递其元素为任意类型的源列表和目的列表(但是它们的元素类型一致),您需要指定通配符作为该类型的占位符。例如,您可以考虑编写下面的 copyList() 类方法,它接受任意类型对象的集合作为其参数:

static void copyList(List<?> src, List<?> dest)
{
   for (int i = 0; i < src.size(); i++)
      dest.add(src.get(i));
}

这个方法的参数列表是正确的,但是还有另一个问题:编译器在遇到 dest.add(src.get(i))时会输出以下错误消息;。

CopyList.java:19: error: no suitable method found for add(Object)
         dest.add(src.get(i));
             ^
    method List.add(int,CAP#1) is not applicable
      (actual and formal argument lists differ in length)
    method List.add(CAP#1) is not applicable
      (actual argument Object cannot be converted to CAP#1 by method invocation conversion)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error

这个错误消息假设 copyList() 是名为 CopyList 的类的一部分。虽然看起来不可理解,但该消息基本上意味着 dest.add(src.get(i)) 方法调用违反了类型安全。因为?表示任何类型的对象都可以作为列表的元素类型,目标列表的元素类型可能与源列表的元素类型不兼容。

例如,假设您创建一个字符串的列表作为源列表,创建一个雇员的列表作为目的列表。试图将源列表的元素添加到目标列表中,这违反了类型安全。如果允许这个复制操作,当试图获取目标列表的元素时,将抛出一个 ClassCastException 实例。

You could solve this problem in a limited way as follows:

static void copyList(List<? extends String> src,
                     List<? super String> dest)
{
   for (int i = 0; i < src.size(); i++)
      dest.add(src.get(i));
}

此方法演示了通配符参数功能,在该功能中,您可以提供上限或下限(与类型参数不同)来限制可以作为实际类型参数传递给泛型类型的类型。具体来说,它显示了一个通过扩展的上界,后跟在之后的上界类型?,以及一个经由超的下界,后跟在之后的下界类型?。

你翻译?扩展字符串意味着任何实际类型参数,即字符串或该类型的子类,都可以被传递,你解释?super String 暗示任何实际的类型参数,即字符串或这种类型的超类,都可以被传递。因为字符串不能被子类化,这意味着你只能传递字符串的源列表和字符串或对象的目的列表。

将任意元素类型的列表复制到其他列表的问题可以通过使用泛型方法(具有类型一般化实现的类或实例方法)来解决。泛型方法在语法上表示如下:

< *formal_type_parameter_list* > *return_type identifier* ( *parameter_list* )

formal _ type _ parameter _ list与指定泛型类型时相同:它由具有可选边界的类型参数组成。类型参数可以作为方法的 return_type 出现,类型参数可以出现在 parameter_list 中。编译器从调用方法的上下文中推断实际的类型参数。

您将在集合框架中发现许多泛型方法的例子。比如它的 java.util.Collections 类提供了一个公共静态< T 扩展对象&可比<?超 T > > T min(收藏<?扩展 T > coll) 方法,根据元素的自然排序返回给定集合中的最小元素。

通过在返回类型前面加上 < T > 并将每个通配符替换为 T ,可以很容易地将 copyList() 转换为泛型方法。得到的方法头是void copy List(Listsrc,List < T > dest) ,而清单 6-26 将其源代码作为应用的一部分呈现,该应用将循环的列表复制到循环的另一个列表。

清单 6-26 。声明和使用一个 copyList() 泛型方法

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

class Circle
{
   private double x, y, radius;

   Circle(double x, double y, double radius)
   {
      this.x = x;
      this.y = y;
      this.radius = radius;
   }

   @Override
   public String toString()
   {
      return "(" + x + ", " + y + ", " + radius + ")";
   }
}

public class CopyList
{
   public static void main(String[] args)
   {
      List<String> ls = new ArrayList<String>();
      ls.add("A");
      ls.add("B");
      ls.add("C");
      outputList(ls);
      List<String> lsCopy = new ArrayList<String>();
      copyList(ls, lsCopy);
      outputList(lsCopy);
      List<Circle> lc = new ArrayList<Circle>();
      lc.add(new Circle(10.0, 20.0, 30.0));
      lc.add(new Circle (5.0, 4.0, 16.0));
      outputList(lc);
      List<Circle> lcCopy = new ArrayList<Circle>();
      copyList(lc, lcCopy);
      outputList(lcCopy);
   }

   static <T> void copyList(List<T> src, List<T> dest)
   {
      for (int i = 0; i < src.size(); i++)
         dest.add(src.get(i));
   }

   static void outputList(List<?> list)
   {
      for (int i = 0; i < list.size(); i++)
         System.out.println(list.get(i));
      System.out.println();
   }
}

泛型方法的类型参数是从调用该方法的上下文中推断出来的。比如编译器确定 copyList(ls,ls copy);将串的一个列表复制到串的另一个列表中。同样,它确定 copyList(lc,LC copy);将圆的一个列表复制到圆的另一个列表中。

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

A
B
C

A
B
C

(10.0, 20.0, 30.0)
(5.0, 4.0, 16.0)

(10.0, 20.0, 30.0)
(5.0, 4.0, 16.0)

数组和泛型

在呈现了清单 6-22 的队列T3】泛型类型后,我提到我会解释为什么我指定了元素= (E[])新对象【大小】;而不是更紧凑的 elements = new E[size];表情。由于 Java 的泛型实现,不可能指定包含类型参数(例如, new E[size] 或 new List【50】)或实际类型参数(例如,new Queue【15】)的数组创建表达式。如果您试图这样做,编译器将报告一个通用数组创建错误消息。

在我给出一个例子来说明为什么允许包含类型参数或实际类型实参的数组创建表达式是危险的之前,您需要理解数组上下文中的具体化和协方差,以及擦除,这是泛型实现的核心。

具体化表示抽象,就像它是具体的一样——例如,使一个内存地址可以被其他语言结构直接操作。Java 数组是具体化的,因为它们知道自己的元素类型(元素类型存储在内部),并且可以在运行时强制使用这些类型。试图在数组中存储无效元素会导致虚拟机抛出 Java . lang . arraystoreexception 类的实例。

清单 6-27 教你数组操作如何导致 ArrayStoreException 。

清单 6-27 。一个 ArrayStoreException 是如何产生的

class Point
{
   int x, y;
}

class ColoredPoint extends Point
{
   int color;
}

public class ReificationDemo
{
   public static void main(String[] args)
   {
      ColoredPoint[] cptArray = new ColoredPoint[1];
      Point[] ptArray = cptArray;
      ptArray[0] = new Point();
   }
}

清单 6-27 的 main() 方法首先实例化一个可以存储一个元素的 ColoredPoint 数组。与这种合法的赋值相反(类型是兼容的),指定 colored Point[]cptArray = new Point[1];是非法的(并且不会编译),因为它会在运行时导致 ClassCastException——数组知道该赋值是非法的。

如果不明显,colored Point[]cptArray = new Point[1];是非法的,因为点实例的成员(只有 x 和 y )比 ColoredPoint 实例( x 、 y 和 color )少。试图从 ColoredPoint 数组中的条目访问 Point 实例的不存在的 color 字段将导致内存冲突(因为没有内存分配给 color ),并最终导致虚拟机崩溃。

第二行(Point[]ptArray = cptArray;)是合法的,因为协方差(超类型引用数组是子类型引用数组的超类型)。在这种情况下,点引用的数组是着色点引用数组的超类型。非数组类比是子类型也是超类型。例如, java.lang.Throwable 实例是一种对象实例。

协方差被滥用时是危险的。比如第三行(ptArray[0]= new Point();因为点实例不是 ColoredPoint 实例,所以在运行时导致 ArrayStoreException 。如果没有这个异常,试图访问不存在的成员 color 会使虚拟机崩溃。

与数组不同,泛型类型的类型参数没有具体化。它们在运行时是不可用的,因为它们在源代码编译后就被扔掉了。这种“丢弃类型参数”是擦除的结果,这也涉及到当代码类型不正确时插入对适当类型的强制转换,并用它们的上限替换类型参数(如对象)。

注意编译器执行擦除,让泛型代码与遗留(非泛型)代码互操作。它将通用源代码转换成非通用运行时代码。擦除的一个后果是,除了无限制的通配符类型之外,不能对参数化类型使用 instanceof 操作符。例如,指定列表<员工> le = null 是非法的;if (le instanceof ArrayList <员工> ) {} 。而是必须将表达式的 instanceof 改为 le instanceof ArrayList <?> (无界通配符)或者 le instance of ArrayList(raw 类型,首选使用)。

假设您可以指定一个包含类型参数或实际类型参数的数组创建表达式。为什么这不好?为了回答这个问题,考虑下面的例子,它应该生成一个 ArrayStoreException 而不是一个 ClassCastException ,但是没有这样做:

List<Employee>[] empListArray = new List<Employee>[1];
List<String> strList = new ArrayList<String>();
strList.add("string");
Object[] objArray = empListArray;
objArray[0] = strList;
Employee e = empListArray[0].get(0);

假设第一行是合法的,它创建了一个单元素数组,其中该元素存储了一个雇员的列表。第二行创建一个由字符串组成的列表,第三行在这个列表中存储一个字符串对象。

第四行将 empListArray 分配给 objArrayT5。这种赋值是合法的,因为数组是协变的,擦除将列表<雇员> [] 转换为列表运行时类型和列表子类型对象。

因为擦除,虚拟机遇到 objArray[0] = strList 时不会抛出 ArrayStoreException;。毕竟,您在运行时将一个列表引用分配给了一个列表[] 数组。然而,如果泛型类型被具体化了,这个异常就会被抛出,因为你会将一个 List < String > 引用赋值给一个 List[]数组。

但是,有一个问题。一个列表<字符串> 实例被存储在一个只能容纳列表<雇员> 实例的数组中。当编译器插入的强制转换运算符试图强制转换 empListArray[0]时。get(0) 的返回值( "string" )给雇员,cast 操作符抛出一个 ClassCastException 对象。

掌握菜单

一个枚举类型是一个指定相关常量的命名序列作为其合法值的类型。日历中的月份、货币中的硬币和星期几都是枚举类型的例子。

Java 开发人员传统上使用一组命名的整数常量来表示枚举类型。因为这种表示形式被证明是有问题的,Java 5 引入了 enum 替代方案。

在这一节中,我将向您介绍 enums。在讨论了传统枚举类型的问题之后,我提出了 enum 替代方法。然后,我将向您介绍枚举的起源 Enum 类。

传统枚举类型的问题是

清单 6-28 声明了一个硬币枚举类型,其常量集标识了一种货币中不同种类的硬币。

清单 6-28 。一种枚举型识别硬币

class Coin
{
   final static int PENNY = 0;
   final static int NICKEL = 1;
   final static int DIME = 2;
   final static int QUARTER = 3;
}

清单 6-29 声明了一个工作日枚举类型,其常量标识了一周中的日子。

清单 6-29 。标识工作日的枚举类型

class Weekday
{
   final static int SUNDAY = 0;
   final static int MONDAY = 1;
   final static int TUESDAY = 2;
   final static int WEDNESDAY = 3;
   final static int THURSDAY = 4;
   final static int FRIDAY = 5;
   final static int SATURDAY = 6;
}

清单 6-28 和 6-29 表示枚举类型的方法是有问题的,其中最大的问题是缺乏编译时类型安全性。例如,您可以将一枚硬币传递给一个需要工作日的方法,编译器不会抱怨。

你也可以将硬币与工作日进行比较,如 Coin。镍==工作日。周一,并指定更无意义的表达,如币。一角硬币+工作日。星期五- 1 /硬币。季度。编译器不会抱怨,因为它只看到了 int s

依赖于枚举类型的应用是脆弱的。因为该类型的常量被编译到应用的类文件中,所以更改常量的 int 值需要重新编译相关的应用,否则会有行为不稳定的风险。

枚举类型的另一个问题是 int 常量不能被翻译成有意义的字符串描述。例如,在调试有故障的应用时,数字 4 意味着什么?能够在周四看到而不是 4 会更有帮助。

注意你可以通过使用字符串常量来规避前面的问题。例如,您可以指定 public final 静态字符串 THURSDAY = " THURSDAY。虽然常量值更有意义,但是基于字符串的常量会影响性能,因为您无法使用 == 来有效地比较任何旧字符串(您将在第 7 章中发现)。与基于字符串的常量相关的其他问题包括将常量的值("星期四")而不是常量的名称(星期四)硬编码到源代码中,这使得在以后很难更改常量的值;以及拼错一个硬编码的常量(“THURZDAY”),编译正确但运行时有问题。

枚举替代项

Java 5 引入了枚举作为传统枚举类型的更好替代。一个枚举是一个通过保留字枚举表示的枚举类型。以下示例使用枚举来声明清单 6-28 和 6-29 的枚举类型:

enum Coin { PENNY, NICKEL, DIME, QUARTER }
enum Weekday { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }

尽管它们与 C++和其他语言中基于 int 的枚举类型相似,但这个例子的枚举是类。每个常量都是一个公共静态 final 字段,表示其枚举类的一个实例。

因为常数是最终的,并且因为你不能调用枚举的构造函数来创建更多的常数,你可以使用 == 来有效地比较常数,并且(不像字符串常数比较)安全。比如可以指定 c == Coin。镍。

枚举通过防止您比较不同枚举中的常数来提高编译时类型安全性。比如编译器遇到币会报错。PENNY ==工作日。周日。

编译器也不赞成将错误枚举类型的常数传递给方法。比如不能过工作日。星期五给一个参数类型为硬币的方法。

依赖枚举的应用并不脆弱,因为枚举的常量没有编译到应用的类文件中。同样,enum 提供了一个 toString() 方法,用于返回一个常量值的更有用的描述。

因为枚举非常有用,Java 5 增强了 switch 语句来支持它们。清单 6-30 演示了这个语句打开了前一个例子的硬币枚举中的一个常量。

清单 6-30 。将 Switch 语句与枚举一起使用

public class EnhancedSwitch
{
   enum Coin { PENNY, NICKEL, DIME, QUARTER }

   public static void main(String[] args)
   {
      Coin coin = Coin.NICKEL;
      switch (coin)
      {
         case PENNY  : System.out.println("1 cent"); break;
         case NICKEL : System.out.println("5 cents"); break;
         case DIME   : System.out.println("10 cents"); break;
         case QUARTER: System.out.println("25 cents"); break;
         default     : assert false;
      }
   }
}

清单 6-30 展示了打开一个枚举的常量。这个增强的语句只允许您将常量的名称指定为 case 标签。如果你在名字前面加上 enum,就像中的 case Coin。DIME ,编译器报错。

增强一个枚举

您可以向枚举添加字段、构造函数和方法,甚至可以让枚举实现接口。例如,清单 6-31 向硬币添加了一个字段、一个构造函数和两个方法,以将一个面额值与一个硬币常数相关联(例如 1 代表便士,5 代表镍),并将便士转换成面额。

清单 6-31 。增强硬币枚举

enum Coin
{
   PENNY(1),
   NICKEL(5),
   DIME(10),
   QUARTER(25);

   private final int denomValue;

   Coin(int denomValue)
   {
      this.denomValue = denomValue;
   }

   int denomValue()
   {
      return denomValue;
   }

   int toDenomination(int numPennies)
   {
      return numPennies / denomValue;
   }
}

清单 6-31 的构造函数接受一个命名值,它将该值赋给一个名为 denomValue 的私有空白 final 字段——所有字段都应该声明为 final ,因为常量是不可变的。请注意,这个值在创建过程中被传递给每个常量(例如, PENNY(1) )。

注意当逗号分隔的常量列表后跟除枚举右括号之外的任何内容时,必须用分号终止列表,否则编译器会报告错误。

此外,这个清单的 denomValue() 方法返回 denomValue ,它的 toDenomination() 方法 返回包含在作为参数传递给这个方法的便士数中的硬币数。例如,16 个便士中包含 3 个镍币。

清单 6-32 展示了如何使用增强的硬币枚举。

清单 6-32 。练习增强版硬币枚举

public class Coins
{
   public static void main(String[] args)
   {
      if (args.length == 1)
      {
         int numPennies = Integer.parseInt(args[0]);
         System.out.println(numPennies + " pennies is equivalent to:");
         int numQuarters = Coin.QUARTER.toDenomination(numPennies);
         System.out.println(numQuarters + " " + Coin.QUARTER.toString() +
                            (numQuarters != 1 ? "s," : ","));
         numPennies -= numQuarters * Coin.QUARTER.denomValue();
         int numDimes = Coin.DIME.toDenomination(numPennies);
         System.out.println(numDimes + " " + Coin.DIME.toString() +
                            (numDimes != 1 ? "s, " : ","));
         numPennies -= numDimes * Coin.DIME.denomValue();
         int numNickels = Coin.NICKEL.toDenomination(numPennies);
         System.out.println(numNickels + " " + Coin.NICKEL.toString() +
                            (numNickels != 1 ? "s, " : ", and"));
         numPennies -= numNickels*Coin.NICKEL.denomValue();
         System.out.println(numPennies + " " + Coin.PENNY.toString() +
                            (numPennies != 1 ? "s" : ""));
      }
      System.out.println();
      System.out.println("Denomination values:");
      for (int i = 0; i < Coin.values().length; i++)
         System.out.println(Coin.values()[i].denomValue());
   }
}

清单 6-32 描述了一个应用,它将单独的“便士”命令行参数转换成以 25 分、10 分、5 分和 1 分表示的等价金额。除了调用一个硬币常量的 denomValue() 和 toDenomValue() 方法,应用还调用 toString() 来输出硬币的字符串表示。

另一种叫做 enum 的方法是 values() 。该方法返回一个数组,包含所有在 Coin 枚举中声明的 Coin 常量( value() 的返回类型,在本例中为 Coin[] )。当您需要迭代这些常量时,这个数组非常有用。例如,清单 6-32 调用这个方法来输出每枚硬币的面额。

当您使用 119 作为命令行参数( java Coins 119 )运行这个应用时,它会生成以下输出:

119 pennies is equivalent to:
4 QUARTERs,
1 DIME,
1 NICKEL, and
4 PENNYs

Denomination values:
1
5
10
25

输出显示 toString() 返回一个常量的名称。重写此方法以返回更有意义的值有时很有用。例如,从字符串中提取标记(命名字符序列)的方法可能使用标记枚举来列出标记名,并通过一个覆盖的 toString() 方法来列出值——参见清单 6-33

清单 6-33 。重写 toString() 以返回一个令牌常量的值

public enum Token
{
   IDENTIFIER("ID"),
   INTEGER("INT"),
   LPAREN("("),
   RPAREN(")"),
   COMMA(",");

   private final String tokValue;

   Token(String tokValue)
   {
      this.tokValue = tokValue;
   }

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

   public static void main(String[] args)
   {
      System.out.println("Token values:");
      for (int i = 0; i < Token.values().length; i++)
         System.out.println(Token.values()[i].name() + " = " +
                            Token.values()[i]);
   }
}

清单 6-33 的 main() 方法调用 values() 返回 Token 常量的数组。对于每个常量,它调用常量的 name() 方法返回常量的名称,并隐式调用 toString() 返回常量的值。如果您要运行此应用,您将会看到以下输出:

Token values:
IDENTIFIER = ID
INTEGER = INT
LPAREN = (
RPAREN = )
COMMA = ,

增强枚举的另一种方法是给每个常量分配不同的行为。您可以通过在枚举中引入一个抽象方法并在常量的匿名子类中重写该方法来完成此任务。清单 6-34 的的 TempConversion enum 演示了这种技术。

清单 6-34 。使用匿名子类改变枚举常量的行为

public enum TempConversion
{
   C2F("Celsius to Fahrenheit")
   {
      @Override
      double convert(double value)
      {
         return value * 9.0 / 5.0 + 32.0;
      }
   },

   F2C("Fahrenheit to Celsius")
   {
      @Override
      double convert(double value)
      {
         return (value - 32.0) * 5.0 / 9.0;
      }
   };

   TempConversion(String desc)
   {
      this.desc = desc;
   }

   private String desc;

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

   abstract double convert(double value);

   public static void main(String[] args)
   {
      System.out.println(C2F + " for 100.0 degrees = " + C2F.convert(100.0));
      System.out.println(F2C + " for 98.6 degrees = " + F2C.convert(98.6));
   }
}

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

Celsius to Fahrenheit for 100.0 degrees = 212.0
Fahrenheit to Celsius for 98.6 degrees = 37.0

枚举类

编译器将枚举视为语法糖。当它遇到一个枚举类型声明( enum Coin {} )时,它生成一个类,其名称( Coin )由声明指定,该声明还子类化抽象 Enum 类(在 java.lang 包中),所有基于 java 语言的枚举类型的公共基类。

如果你查看 Enum 的 Java 文档,你会发现它覆盖了对象的 clone() , equals() , finalize() , hashCode() 和 toString() 方法:

  • clone() 被覆盖,以防止常数被克隆,从而永远不会有一个以上的常数副本;否则,常量无法通过 == 进行比较。
  • equals() 被覆盖,通过引用比较常数——相同恒等式( == )的常数必须有相同的内容( equals() ),不同的恒等式暗示不同的内容。
  • finalize() 被覆盖以确保常量不能被最终确定。
  • hashCode() 被覆盖,因为等于()被覆盖。
  • toString() 被覆盖以返回常量的名称。

除了 toString() 之外,所有的覆盖方法都被声明为 final ,这样它们就不能在子类中被覆盖。

Enum 也提供了自己的方法。这些方法包括 final compare to()(EnumimplementsComparable), getDeclaringClass() , name() , ordinal() 方法:

  • compareTo() 将当前常数与作为参数传递的常数进行比较,以查看在枚举中哪个常数在另一个常数之前,并返回一个值来指示它们的顺序。这个方法可以对未排序的常量数组进行排序。
  • getDeclaringClass() 返回当前常量的枚举对应的类对象。比如调用 Coin 时返回 Coin 的类对象。PENNY . getdeclaringclass()forenum Coin { PENNY,NICKEL,DIME,QUARTER} 。另外,调用 TempConversion 时,返回 TempConversion 。C2F.getDeclaringClass() 用于清单 6-34 的 TempConversion 枚举。 compareTo() 方法使用 Class 的 getClass() 方法 和 Enum 的 getDeclaringClass() 方法来确保只比较属于同一个 Enum 的常量。否则,抛出一个 ClassCastException 。
  • name() 返回常量的名称。除非被覆盖以返回更具描述性的内容, toString() 也会返回常量的名称。
  • ordinal() 返回一个从零开始的序数,它是一个整数,标识常量在枚举类型中的位置。 compareTo() 比较序数。

Enum 还提供了 public static<T extends EnumT7】T value of(ClassEnum type,String name) 方法,用于从具有指定名称的指定 Enum 返回 Enum 常量:

  • 枚举类型 标识枚举的类对象,从该对象返回一个常数。
  • 名称 标识要返回的常数的名称。

例如,Coin PENNY = enum . value of(Coin . class," PENNY ");将名为便士的硬币常量分配给便士。

你不会在 Enum 的 Java 文档中发现 values() 方法,因为编译器在生成类时合成了(制造)这个方法。

扩展枚举类

枚举的泛型类型是枚举< E 扩展枚举< E > > 。尽管形式类型参数列表看起来很可怕,但理解起来并不难。但是首先,看一下清单 6-35

清单 6-35 。从类文件的角度看,硬币类

public final class Coin extends Enum<Coin>
{
   public static final Coin PENNY = new Coin("PENNY", 0);
   public static final Coin NICKEL = new Coin("NICKEL", 1);
   public static final Coin DIME = new Coin("DIME", 2);
   public static final Coin QUARTER = new Coin("QUARTER", 3);
   private static final Coin[] $VALUES = { PENNY, NICKEL, DIME, QUARTER };

   public static Coin[] values()
   {
      return Coin.$VALUES.clone();
   }

   public static Coin valueOf(String name)
   {
      return Enum.valueOf(Coin.class, "Coin");
   }

   private Coin(String name, int ordinal)
   {
      super(name, ordinal);
   }
}

在后台,编译器将 enum Coin { PENNY,NICKEL,DIME,QUARTER} 转换成类似于清单 6-35 的类声明。

以下规则向您展示如何在硬币扩展枚举<硬币> 的上下文中解释枚举< E 扩展枚举< E > > :

  • 任何枚举的子类都必须为枚举提供一个实际类型参数。例如,币的表头指定枚举<币> 。
  • 实际的类型参数必须是枚举的子类。比如币就是枚举的子类。
  • Enum 的子类(比如 Coin )必须遵循这样的习惯用法,即它提供自己的名字( Coin )作为实际的类型参数。

第三个规则允许 Enum 声明方法——compare to()、 getDeclaringClass() 和 value of()——其参数和/或返回类型是根据子类( Coin )而不是根据 Enum 指定的。这样做的理由是为了避免必须指定强制转换。比如在 Coin penny = enum . value of(Coin . class," PENNY ")中不需要将 valueOf() 的返回值强制转换为 Coin;。

注意你不能编译清单 6-35 ,因为编译器不会编译任何扩展枚举的类。还会抱怨超(名,序数);。

练习

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

  1. 什么是断言?
  2. 什么时候使用断言?
  3. 是非判断:指定不带参数的 -ea 命令行选项会启用所有断言,包括系统断言。
  4. 定义注释。
  5. 什么样的应用元素可以被注释?
  6. 识别三种编译器支持的注释类型。
  7. 如何声明注释类型?
  8. 什么是标记注释?
  9. 什么是元素?
  10. 如何给一个元素分配一个默认值?
  11. 什么是元注释?
  12. 识别 Java 的四种元注释类型。
  13. 定义泛型。
  14. 为什么要使用泛型?
  15. 泛型类型和参数化类型有什么区别?
  16. 非静态成员类、局部类和匿名类内部类类别中哪一个不能是泛型?
  17. 识别五种实际类型参数。
  18. 是非判断:不能将基本类型的名称(如 double 或 int )指定为实际类型参数。
  19. 什么是原始类型?
  20. 编译器何时报告未检查的警告信息,为什么?
  21. 如何隐藏未检查的警告消息?
  22. 是非判断:列表T1】的 E 类型参数无界。
  23. 如何指定一个单一的上限?
  24. 什么是递归类型界限?
  25. 为什么需要通配符类型参数?
  26. 什么是泛型方法?
  27. In Listing 6-36, which overloaded method does the methodCaller() generic method call?

    清单 6-36 。哪个 someOverloadedMethod() 被调用?

    导入 Java . util . date;

    公共类卡洛莫登格法

    {

    公共静态 void someOverloadedMethod(Object o)

    {

    System.out.println("调用 someOverloadedMethod(Object o)");

    }

    public static void someOverloadedMethod(日期 d)

    {

    System.out.println("调用 someOverloadedMethod(Date d)");

    }

    公共静态< T > void 方法 Caller(T t)

    {

    overloadedmmethod(t);

    }

    public static void main(String[]args)

    {

    method caller(new Date());

    }

    }

  28. 什么是物化?

  29. 是非判断:类型参数是具体化的。
  30. 什么是擦除?
  31. 定义枚举类型。
  32. 确定当您使用常量基于 int 的枚举类型时可能出现的三个问题。
  33. 什么是枚举?
  34. 如何在枚举中使用 switch 语句?
  35. 用什么方法可以增强一个枚举?
  36. 抽象枚举类的用途是什么?
  37. Enum 的 name() 和 toString() 方法有什么区别?
  38. 是非判断:枚举的泛型类型是枚举< E 扩展枚举< E > > 。
  39. 声明一个 ToDo 标记注释类型,该类型仅注释类型元素,并且还使用默认的保留策略。
  40. 重写 StubFinder 应用以使用清单 6-13 的存根注释类型(带有适当的 @Target 和 @Retention 注释)和清单 6-14 的 Deck 类。
  41. 以类似于清单 6-22 的队列类的方式实现一个栈T1】泛型类型。 Stack 必须声明 push() 、 pop() 和 isEmpty() 方法(它也可以声明一个 isFull() 方法,但该方法在本练习中不是必需的); push() 栈满时必须抛出一个 StackFullException 实例;并且 pop() 必须在栈为空时抛出一个 StackEmptyException 实例。(您必须创建自己的 StackFullException 和 StackEmptyException 助手类,因为标准类库中没有为您提供它们。)声明一个类似的 main() 方法,并在该方法中插入两个断言,这两个断言验证了您关于堆栈在创建后立即为空以及弹出最后一个元素后立即为空的假设。
  42. 用北、南、东和西成员声明一个罗盘枚举。声明一个 UseCompass 类,其 main() 方法随机选择这些常量中的一个,然后打开该常量。switch 语句的每种情况都应该输出一条消息,如朝北。

摘要

断言是一种语句,它允许您通过布尔表达式来表达应用正确性的假设。如果该表达式的计算结果为 true,则继续执行下一条语句。否则,将引发一个标识失败原因的错误。

在很多情况下都应该使用断言。这些情况被组织成内部不变量、控制流不变量和契约设计类别。不变量是不变的东西。

尽管有很多情况下应该使用断言,但也有一些情况下应该避免使用断言。例如,您不应该使用断言来检查传递给公共方法的参数。

编译器在类文件中记录断言。但是,断言在运行时被禁用,因为它们会影响性能。在测试关于类行为的假设之前,必须启用类文件的断言。

注释是注释类型的实例,并将元数据与应用元素相关联。在源代码中,它们通过在类型名前面加上 @ 符号来表示。例如, @Readonly 是一个注释, Readonly 是它的类型。

Java 提供了各种各样的注释类型,包括面向编译器的覆盖、弃用和 SuppressWarnings 类型。然而,您也可以通过使用 @interface 语法来声明自己的注释类型。

注释类型可以用元注释进行注释,元注释标识它们可以作为目标的应用元素(例如构造函数、方法或字段)、它们的保留策略以及其他特征。

通过 @Retention 批注为其类型分配了运行时保留策略的批注可以在运行时使用自定义应用进行处理。(Java 5 为此引入了一个 apt 工具,但是从 Java 6 开始,它的功能大部分被集成到了编译器中。)

Java 5 引入了泛型,这是声明和使用与类型无关的类和接口的语言特性。当使用 Java 的集合框架时,这些特性帮助你避免 ClassCastException s。

泛型类型是一个类或接口,它通过声明一个形式类型参数列表来引入一系列参数化类型。替换类型参数的类型名称为实际类型参数。

有五种实际类型参数:具体类型、具体参数化类型、数组类型、类型参数和通配符。此外,泛型类型还标识原始类型,即没有类型参数的泛型类型。

泛型方法是具有类型一般化实现的类或实例方法,例如,void copy List(Listsrc,List < T > dest) 。编译器从调用方法的上下文中推断实际的类型参数。

枚举类型是将相关常数的命名序列指定为合法值的类型。Java 开发人员传统上使用命名整数常量集来表示枚举类型。

因为命名整数常量集被证明是有问题的,Java 5 引入了 enum 替代方法。枚举是通过保留字 enum 在源代码中表示的枚举类型。

您可以向枚举添加字段、构造函数和方法,甚至可以让枚举实现接口。此外,您可以覆盖 toString() 来提供一个更有用的常量值描述,并子类化常量来分配不同的行为。

编译器将枚举视为一个子类枚举的语法糖。这个抽象类覆盖了各种对象方法来提供默认行为(通常是出于安全原因),并为各种目的提供了额外的方法。

这一章基本上完成了 Java 语言之旅。在第 7 章中,我开始通过关注那些与数学、字符串管理和包相关的 API 来强调 Java APIs。