六、掌握高级语言功能:第二部分
在第二章到第四章中,我为学习 Java 语言打下了基础,在第五章中,我在这个基础上引入了一些 Java 更高级的语言特性。在第六章的中,我将继续关注与断言、注释、泛型和枚举相关的高级语言特性。
掌握断言
写源代码不是一件容易的事情。太多的时候,错误被引入到代码中。如果在编译源代码之前没有发现 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)相同的目标;方法调用,但这样做更有效),我选择使用冒泡排序,因为我更喜欢等到第九章进入数组类。
后置条件
一个后置条件是在一个方法成功完成后必须为真的东西。断言语句通常用于通过检查结果是否合法来满足助手方法的后置条件。清单 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 命令行选项允许您基于以下参数之一启用各种粒度的断言(除了没有参数的情况,您必须使用冒号将选项与其参数分开):
- 无参数:断言在除系统类之外的所有类中都启用。
- 包名??。。。:通过指定包名后跟,断言在指定的包及其子包中被启用。。。。
- 。。。:断言在未命名的包中启用,这个包恰好是当前的目录。
- ClassName :通过指定类名在命名类中启用断言。
例如,当通过 Java–ea merge arrays 运行 MergeArrays 应用时,可以启用除系统断言之外的所有断言。此外,您可以通过指定 Java–ea:logging test logger 来启用您可能添加到第五章的日志包中的任何断言。
通过指定–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 。注释一个被剔除的方法 ??
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() 的方法头前加上结果@存根注释来表示。
注意虽然标记接口(在第四章中介绍)似乎已经被标记注释所取代,但事实并非如此,因为标记接口比标记注释更有优势。一个优点是标记接口指定了由标记类实现的类型,这让您可以在编译时发现问题。例如,当一个类没有实现可克隆的接口时,它的实例不能通过对象的 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 的集合框架时(我在第九章中介绍了),这些特性帮助你避免 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 <员工>表示列表中只能存储员工对象。如图所示, <雇员> 指定必须与数组列表重复,如在数组列表<雇员> 中,它是存储雇员 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* > {}
例如,List为泛型类型,其中 List 为接口,类型参数 E 标识列表的元素类型。类似地, Map < K,V>是一个泛型类型,其中 Map 是一个接口,类型参数 K 和 V 标识 Map 的键和值类型。
注意当声明一个泛型类型时,习惯上指定单个大写字母作为类型参数名。此外,这些名称应该有意义。例如, E 表示元素, T 表示类型, K 表示键, V 表示值。如果可能,您应该避免选择在使用它的地方没有意义的类型参数名称。比如 List < E > 表示元素列表,但是 List < S > 是什么意思呢?
参数化类型是泛型类型的实例。每个参数化类型都用类型名替换泛型类型的类型参数。例如,List??(Employee 的 List )和 List??(String 的 List )就是基于 List的参数化类型的例子。同样, Map < String,Employee > 是基于 Map < K,V > 的参数化类型的一个例子。
替换类型形参的类型名称为实际类型实参 。泛型支持五种实际类型参数:
- 具体类型 :将类或接口的名称传递给类型参数。例如,列出<员工>员工;指定列表元素是雇员实例。
- 具体参数化类型 :参数化类型的名称被传递给类型参数。例如,列表<列表<字符串> >名称列表;指定列表元素是字符串列表。
- 数组类型 :将数组传递给类型参数。例如,列表<字符串[] >国家;指定列表元素是由字符串组成的数组,可能是城市名。
- 类型参数 :将一个类型参数传递给类型参数。例如,给定类声明类 X < E > {列表< E >队列;} , X 的类型参数 E 传递给列表的类型参数 E 。
- 通配符??:?被传递给类型参数。比如列表<?>列表;指定列表元素未知。在本章的后面,你会学到通配符。
泛型类型还标识了一个原始类型 ,它是一个没有类型参数的泛型类型。比如列表<员工> 的 raw 类型是列表。Raw 类型是非泛型的,可以保存任何对象。
注意 Java 允许原始类型与泛型混合,以支持在泛型出现之前编写的大量遗留代码。但是,每当编译器在源代码中遇到原始类型时,它都会输出一条警告消息。
声明并使用自己的泛型类型
声明自己的泛型类型并不难。除了指定一个正式的类型参数列表之外,泛型类型还在它的整个实现过程中指定它的类型参数。例如,清单 6-22 声明了一个队列< E > 泛型类型。
清单 6-22 。声明并使用一个队列泛型类型
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 个元素。
注意到队列的 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
类型参数界限
列表的 E 类型参数和映射< K,V>的 K 和 V 类型参数都是无界类型参数 的例子。您可以将任何实际类型参数传递给未绑定的类型参数。
有时有必要限制可以传递给类型参数的实际类型变量的种类。例如,您可能想要声明一个类,其实例只能存储抽象形状类的子类的实例(例如圆形和矩形)。
为了限制实际类型参数,您可以指定一个上限,这是一个可以作为实际类型参数的类型上限的类型。上限是通过保留字扩展后跟类型名来指定的。
例如,ShapesList将 Shape 标识为上界。可以指定 ShapesList <圆形>??、 ShapesList <矩形> ,甚至 ShapesList <形状> ,但不能指定 ShapesList <字符串> ,因为字符串不是形状的子类。
您可以为类型参数分配多个上限,其中第一个上限是一个类或接口,每个附加的上限是一个接口,方法是使用&字符( & )来分隔绑定名称。考虑清单 6-23 。
清单 6-23 。为类型参数指定多个上限 ??
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 中的可比扩展形状&可比就是一个递归类型绑定。递归类型界限很少见,通常与用于指定类型自然排序的可比接口一起出现。
Circle 满足这两个条件:它子类化形状并实现可比性。因此,编译器在遇到 main() 方法的 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 extends Comparable>。虽然 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 >不是 G的子类型。
要理解这个规则,必须刷新一下对子类型多态性的理解(见第四章)。基本上,一个子类型是它的父类型的一个特化类型。例如,圆是一种特殊的形状,而弦是一种特殊的物体。这种多态行为也适用于具有相同类型参数的相关参数化类型(例如, 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 的队列泛型类型后,我提到我会解释为什么我指定了元素= (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。虽然常量值更有意义,但是基于字符串的常量会影响性能,因为您无法使用 == 来有效地比较任何旧字符串(您将在第七章中发现)。与基于字符串的常量相关的其他问题包括将常量的值(“星期四”)而不是常量的名称(星期四)硬编码到源代码中,这使得在以后很难更改常量的值;以及拼错一个硬编码的常量(“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 EnumT 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 ,因为编译器不会编译任何扩展枚举的类。还会抱怨超(名,序数);。
练习
以下练习旨在测试您对第六章内容的理解:
- 什么是断言?
- 什么时候使用断言?
- 是非判断:指定不带参数的 -ea 命令行选项会启用所有断言,包括系统断言。
- 定义注释。
- 什么样的应用元素可以被注释?
- 识别三种编译器支持的注释类型。
- 如何声明注释类型?
- 什么是标记注释?
- 什么是元素?
- 如何给一个元素分配一个默认值?
- 什么是元注释?
- 识别 Java 的四种元注释类型。
- 定义泛型。
- 为什么要使用泛型?
- 泛型类型和参数化类型有什么区别?
- 非静态成员类、局部类和匿名类内部类类别中哪一个不能是泛型?
- 识别五种实际类型参数。
- 是非判断:不能将基本类型的名称(如 double 或 int )指定为实际类型参数。
- 什么是原始类型?
- 编译器何时报告未检查的警告信息,为什么?
- 如何隐藏未检查的警告消息?
- 是非判断:列表的 E 类型参数无界。
- 如何指定一个单一的上限?
- 什么是递归类型界限?
- 为什么需要通配符类型参数?
- 什么是泛型方法?
- 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());
}
}
- 什么是物化?
- 是非判断:类型参数是具体化的。
- 什么是擦除?
- 定义枚举类型。
- 确定当您使用常量基于 int 的枚举类型时可能出现的三个问题。
- 什么是枚举?
- 如何在枚举中使用 switch 语句?
- 用什么方法可以增强一个枚举?
- 抽象枚举类的用途是什么?
- Enum 的 name() 和 toString() 方法有什么区别?
- 是非判断:枚举的泛型类型是枚举< E 扩展枚举< E > > 。
- 声明一个 ToDo 标记注释类型,该类型仅注释类型元素,并且还使用默认的保留策略。
- 重写 StubFinder 应用以使用清单 6-13 的存根注释类型(带有适当的 @Target 和 @Retention 注释)和清单 6-14 的 Deck 类。
- 以类似于清单 6-22 的队列类的方式实现一个栈泛型类型。 Stack 必须声明 push() 、 pop() 和 isEmpty() 方法(它也可以声明一个 isFull() 方法,但该方法在本练习中不是必需的); push() 栈满时必须抛出一个 StackFullException 实例;并且 pop() 必须在栈为空时抛出一个 StackEmptyException 实例。(您必须创建自己的 StackFullException 和 StackEmptyException 助手类,因为标准类库中没有为您提供它们。)声明一个类似的 main() 方法,并在该方法中插入两个断言,这两个断言验证了您关于堆栈在创建后立即为空以及弹出最后一个元素后立即为空的假设。
- 用北、南、东和西成员声明一个罗盘枚举。声明一个 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 语言之旅。在第七章中,我开始通过关注那些与数学、字符串管理和包相关的 API 来强调 Java APIs。
七、探索基本 API:第一部分
标准类库的 java.lang 和 java.math 包提供了许多基本的 API,旨在支持语言特性。您已经遇到了一些这样的 API,比如对象和字符串类以及 Throwable 类层次结构。在这一章中,我将向你介绍那些与数学、字符串管理和包相关的基本库 API。
探索数学应用编程接口
在第二章中,我介绍了 Java 的 + 、 - 、 * 、 / 和 % 运算符,用于对原始类型的值执行基本运算。Java 还提供了用于执行三角学和其他高级数学运算的类,精确地表示货币值,并支持在 RSA 加密(en . Wikipedia . org/wiki/RSA _(algorithm
和其他上下文中使用的超长整数。
数学和严格数学
java.lang.Math 类声明双常数 E 和 PI 表示自然对数底值(2.71828。。。)和圆的周长与其直径的比值(3.14159。。。). E 初始化为 2.718281828459045 并且 PI 初始化为 3.141592653589793 。 Math 还声明了各种类方法来执行各种数学运算。表 7-1 描述了其中的许多方法。
表 7-1。 数学方法
方法 | 描述 |
---|---|
双 abs(双 d) | 返回 d 的绝对值。有四种特殊情况:ABS(-0.0)=+0.0, abs(+infinity) = +infinity ,ABS(-infinity)=+infinity, abs(NaN) = NaN 。 |
浮动 abs(浮动 f) | 返回 f 的绝对值。有四种特殊情况:ABS(-0.0)=+0.0, abs(+infinity) = +infinity ,ABS(-infinity)=+infinity, abs(NaN) = NaN 。 |
int abs(int i) | 返回 i 的绝对值。有一种特殊情况:整数的绝对值。最小值是整数。最小值。 |
长腹肌(长 l) | 返回 l 的绝对值。还有一个特例:龙的绝对值。最小 _ 值为长。最小值。 |
双 acos(双 d) | 返回角度 d 的反余弦值,范围从 0 到 PI。有三种特殊情况: acos(任何东西> 1) = NaN 、 acos(任何东西<??—1)= NaN、 acos(NaN) = NaN 。 |
double asin(double d) | 返回角度 d 的反正弦在-PI/2 到 PI/2 的范围内。有三种特殊情况: asin(任何东西> 1) = NaN 、 asin(任何东西<??—1)= NaN、 asin(NaN) = NaN 。 |
双阿坦(双 d) | 返回角度 d 的反正切在-PI/2 到 PI/2 的范围内。有五种特殊情况:阿坦(+0.0) = +0.0 、阿坦(-0.0)=-0.0、阿坦(+无穷大)= +PI/2 、阿坦(-无穷大)=-PI/2、阿坦(NaN)= NaN |
双天花板(双 d) | 返回不小于 d 且等于整数的最小值(最接近负无穷大)。有六种特殊情况: ceil(+0.0) = +0.0 、ceil(-0.0)=-0.0、ceil(any>-1.0 和<0.0)=-0.0、ceil(+infinity)=+infinity、 ceil(. |
双 cos(双 d) | 返回角度 d 的余弦值(以弧度表示)。有三种特殊情况: cos(+infinity) = NaN 、cos(-infinity)= NaN、 cos(NaN) = NaN 。 |
double exp(double d) | 返回欧拉数 e 的幂 d 。有三种特殊情况: exp(+infinity) = +infinity 、exp(-infinity)=+0.0、 exp(NaN) = NaN 。 |
双楼层(双 d) | 返回不大于 d 且等于整数的最大值(最接近正无穷大)。有五种特殊情况:楼层(+0.0) = +0.0 、楼层(-0.0)=-0.0、楼层(+无穷大)= +无穷大、楼层(-无穷大)=-无穷大、楼层(NaN) = NaN 。 |
双对数(双 d) | 返回 d 的自然对数(底数 e)。有六种特殊情况:log(+0.0)=-infinity,log(-0.0)=-infinity,log(any<0)= NaN,log(+infinity)=+infinity,log(-infinity)= NaN,以及 log |
double log10(double d) | 返回 d 以 10 为底的对数。有六种特殊情况:log10(+0.0)=-无穷大,log10(-0.0)=-无穷大, log10(任何东西< 0) = NaN ,log10(+无穷大)=+无穷大,log10(-无穷大)= NaN |
双最大值(双 d1,双 d2) | 返回 d1 和 d2 中最大的正值(最接近正无穷大)。有四种特殊情况: max(NaN,any)= NaN,max(any,NaN) = NaN , max(+0.0,-0.0) = +0.0 ,max(-0.0,+0.0) = +0.0 。 |
浮动最大值(浮动 f1,浮动 f2) | 返回 f1 和 f2 中最大的正值(最接近正无穷大)。有四种特殊情况: max(NaN,any)= NaN,max(any,NaN) = NaN , max(+0.0,-0.0) = +0.0 ,max(-0.0,+0.0) = +0.0 。 |
int max(int i1,int i2) | 返回 i1 和 i2 中最大的正值(最接近正无穷大)。 |
长最大值(长 l1,长 l2) | 返回 l1 和 l2 的最大正值(最接近正无穷大)。 |
双最小(双 d1,双 d2) | 返回 d1 和 d2 中最负的(最接近负无穷大)。有四种特殊情况: min(NaN,any)= NaN,min(any,NaN) = NaN , min(+0.0,-0.0)=-0.0,min(-0.0,+0.0)=-0.0。 |
浮点最小值(浮点 f1,浮点 f2) | 返回 f1 和 f2 的最大负值(最接近负无穷大)。有四种特殊情况: min(NaN,any)= NaN,min(any,NaN) = NaN , min(+0.0,-0.0)=-0.0,min(-0.0,+0.0)=-0.0。 |
int min(int i1,int i2) | 返回 i1 和 i2 中最负的(最接近负无穷大)。 |
龙敏(长 l1,长 l2) | 返回 l1 和 l2 中最负的(最接近负无穷大)。 |
双随机() | 返回一个介于 0.0(含)和 1.0(不含)之间的伪随机数。 |
长圆形(双 d) | 将 d 的舍入结果返回到长整数。结果相当于 (long) Math.floor(d + 0.5) 。有七种特殊情况: round(+0.0) = +0.0 ,round(-0.0)=+0.0, round(任何东西>长。MAX_VALUE) = Long。MAX_VALUE , round(任何东西<长。MIN_VALUE) = Long。MIN_VALUE , round(+infinity) = Long。MAX_VALUE ,round(-infinity)= Long。最小值和舍入(NaN) = +0.0 。 |
整数舍入(浮点 f) | 将 f 的舍入结果返回到整数。结果相当于 (int) Math.floor(f + 0.5) 。有七种特殊情况: round(+0.0) = +0.0 ,round(-0.0)=+0.0, round(任意>整数。MAX_VALUE) =整数。MAX_VALUE , round(任意值<整数。MIN_VALUE) =整数。MIN_VALUE , round(+infinity) = Integer。MAX_VALUE ,round(-infinity)= Integer。最小值和舍入(NaN) = +0.0 。 |
双符号(双 d) | 返回 d 的符号为 1.0 ( d 小于 0.0)、0.0 ( d 等于 0.0)、1.0 ( d 大于 0.0)。有五种特殊情况: signum(+0.0) = +0.0 、signum(-0.0)=-0.0、 signum(+infinity) = +1.0 、signum(-infinity)=-1.0、 signum(NaN |
浮点符号(浮点 f) | 返回 f 的符号为 1.0 ( f 小于 0.0)、0.0 ( f 等于 0.0)、1.0 ( f 大于 0.0)。有五种特殊情况: signum(+0.0) = +0.0 、signum(-0.0)=-0.0、 signum(+infinity) = +1.0 、signum(-infinity)=-1.0、 signum(NaN |
双 sin(双 d) | 返回角度 d 的正弦值(以弧度表示)。有五种特殊情况: sin(+0.0) = +0.0 、sin(-0.0)=-0.0、sin(+无穷大)= NaN 、sin(-无穷大)= NaN 、 sin(NaN) = NaN 。 |
双 sqrt(双 d) | 返回 d 的平方根。有五种特殊情况: sqrt(+0.0) = +0.0 ,sqrt(-0.0)=-0.0,sqrt(any<0)= NaN,sqrt(+infinity)=+infinity, sqrt(NaN) = NaN 。 |
双谭(双 d) | 返回角度 d 的正切值(以弧度表示)。有五种特殊情况: tan(+0.0) = +0.0 ,tan(-0.0)=-0.0,tan(+无穷大)= NaN ,tan(-无穷大)= NaN , tan(NaN) = NaN 。 |
双角度(双角度) | 通过表达式 angrad * 180 / PI 将角度 angrad 从弧度转换为角度。有五种特殊情况: toDegrees(+0.0) = +0.0 ,to degrees(-0.0)=-0.0,to degrees(+infinity)=+infinity,to degrees(-infinity)=-infinity,to degrees(NaN)= 1 |
双胸(双昂得) | 通过表达式 angdeg / 180 * PI 将角度 angdeg 从度转换为弧度。有五种特殊情况:(toRadians(+0.0)=+0.0,toRadians(-0.0)=-0.0,toRadians(+infinity)=+infinity,toRadians(-infinity)=-infinity,以及 toRadians(NaN)= 1 |
表 7-1 揭示了各种各样有用的面向数学的方法。例如,每个 abs() 方法都返回其参数的绝对值(不考虑符号的数字)。
abs(double) 和 abs(float) 用于安全地比较双精度浮点和浮点值。例如, 0.3 == 0.1 + 0.1 + 0.1 的计算结果为 false,因为 0.1 没有精确的表示形式。但是,您可以将这些表达式与 abs() 和一个公差值进行比较,该公差值表示可接受的误差范围。例如,math . ABS(0.3-(0.1+0.1+0.1))<0.1 返回 true,因为 0.3 和 0.1 + 0.1 + 0.1 之间的绝对差值小于 0.1 容差值。
在前几章中,我演示了其他的数学方法。例如,在第三章中,我演示了数学的 sin() 、 toRadians() 、 cos() 和 random() 方法。
正如 Chapter 6 的 Lotto649 应用所揭示的那样, random() (它返回一个看似随机选择的数字,但实际上是通过可预测的数学计算选择的,因此是伪随机)在模拟中(以及在游戏和任何需要概率元素的地方)是有用的。然而,它的 0.0 到(几乎)1.0 的双精度浮点范围并不实用。为了让 random() 更有用,它的返回值必须转换成更有用的范围,也许是 0 到 49 的整数值,也许是-100 到 100。您会发现下面的 rnd() 方法对于进行这些转换非常有用:
static int rnd(int limit)
{
return (int) (Math.random() * limit);
}
rnd() 通过 limit - 1 整数范围将 random() 的 0.0 到(几乎)1.0 双精度浮点范围转换为 0。例如, rnd(50) 返回一个范围从 0 到 49 的整数。另外, -100 + rnd(201) 通过添加合适的偏移并传递合适的限值值,将 0.0 到(几乎)1.0 转换为 100 到 100。
注意不要指定 (int) Math.random() * limit ,因为这个表达式的值总是 0。该表达式首先转换 random() 的双精度浮点小数值(0.0 到 0.99999。。。)到整数 0,然后将 0 乘以限制,得到 0。
sin() 和 cos() 方法实现正弦和余弦三角函数——参见en.wikipedia.org/wiki/Trigonometric_functions
。这些函数的用途从研究三角形到模拟周期现象(如简谐运动——见en.wikipedia.org/wiki/Simple_harmonic_motion
)。
您可以使用 sin() 和 cos() 来产生和显示正弦和余弦波。清单 7-1 给出了一个应用的源代码。
清单 7-1 。绘制正弦波和余弦波
public class Graph
{
final static int ROWS = 11; // Must be odd
final static int COLS = 23;
public static void main(String[] args)
{
char[][] screen = new char[ROWS][COLS];
double scaleX = COLS / 360.0;
for (int degree = 0; degree < 360; degree++)
{
int row = ROWS / 2 +
(int) Math.round(ROWS / 2 * Math.sin(Math.toRadians(degree)));
int col = (int) (degree * scaleX);
screen[row][col] = 'S';
row = ROWS / 2 +
(int) Math.round(ROWS / 2 * Math.cos(Math.toRadians(degree)));
screen[row][col] = (screen[row][col] == 'S') ? '*' : 'C';
}
for (int row = ROWS - 1; row >= 0; row--)
{
for (int col = 0; col < COLS; col++)
System.out.print(screen[row][col]);
System.out.println();
}
}
}
清单 7-1 引入了一个图类,它首先声明了一对常量:行和列。这些常量指定在其上生成图形的数组的维数。必须给行分配一个奇数;否则,抛出 Java . lang . arrayindexoutofboundsexception 类的一个实例。
提示尽可能使用常量是个好主意。源代码更容易维护,因为您只需要在一个地方更改常量的值,而不必在整个源代码中更改每个相应的值。
Graph 接下来声明它的 main() 方法,它首先创建一个二维屏幕字符数组。这个数组用于模拟一个老式的基于字符的屏幕来查看图形。
main() 接下来计算水平比例值,用于水平缩放每个图形,以便 360 个水平(度)位置适合由 COLS 指定的列数。
继续, main() 进入 for 循环,对于每个正弦和余弦图形,为每个度数值创建(行,列)坐标,并在这些坐标处为屏幕数组分配一个字符。正弦图的字符为 S ,余弦图的字符为 C ,余弦图与正弦图相交时的字符为 * 。
行计算调用 toRadians() 将其度参数转换为弧度,这是 sin() 和 cos() 方法所需要的。然后将从 sin() 或 cos()(1 比 1)返回的值乘以 ROWS / 2 ,将该值缩放至屏幕数组行数的一半。通过 long round(double d) 方法将结果舍入到最近的长整数后,使用强制转换将长整数转换为整数,并将该整数添加到 ROW / 2 以偏移行坐标,使其相对于数组的中间行。列计算更简单,将度数值乘以水平比例因子。
屏幕数组通过一对嵌套的 for 循环转储到标准输出设备。外部 for 循环反转数组输出,使其正面朝上—第 0 行应该最后输出。
编译清单 7-1 ( 贾瓦茨 Graph.java)并运行应用( java Graph )。您将看到以下输出:
表 7-1 还揭示了一些以+无穷大、无穷大、+0.0、0.0 和 NaN(非数字)开头的新奇事物。
Java 的浮点计算能够返回+无穷大、无穷大、+0.0、0.0 和 NaN,因为 Java 很大程度上符合 IEEE 754(【http://en.wikipedia.org/wiki/IEEE_754】),一种浮点计算的标准。以下是产生这些特殊值的情况:
- +infinity 返回试图将一个正数除以 0.0 的结果。比如 system . out . println(1.0/0.0);输出无穷大。
- 尝试将一个负数除以 0.0,返回无穷大。比如 system . out . println(1.0/0.0);输出-无穷大。
- NaN 从尝试将 0.0 除以 0.0、尝试计算负数的平方根以及尝试其他奇怪的运算返回。比如 system . out . println(0.0/0.0);和 system . out . println(math . sqrt(-1.0));各路输出 NaN 。
- +0.0 是试图将正数除以+无穷大的结果。比如 system . out . println(1.0/(1.0/0.0));输出 0.0 (+0.0 不带+号)。
- -0.0 是试图将一个负数除以+无穷大的结果。比如 system . out . println(-1.0/(1.0/0.0));输出 0.0 。
在运算产生+无穷大、-无穷大或 NaN 之后,表达式的其余部分通常等于该特殊值。比如 system . out . println(1.0/0.0 * 20.0);输出无穷大。此外,首先产生+无穷大或-无穷大的表达式可能会转化为 NaN。例如,表达式 1.0 / 0.0 * 0.0 首先得出+无穷大( 1.0 / 0.0 ),然后得出 NaN(+无穷大 * 0.0 )。
另一个好奇心是整数。MAX_VALUE ,整数。最小 _ 值,长。MAX_VALUE 和 Long。最小值。这些项目中的每一个都是一个原始包装类常量,它标识了可以由该类的相关原始类型表示的最大值或最小值。(我在第八章中讨论了原始类型包装类。)
最后,你可能想知道为什么 abs() 、 max() 和 min() 重载方法不包括 byte 和 short 版本,就像在 byte abs(byte b) 和 short abs(short s) 中一样。不需要这些方法,因为字节和短整数的有限范围使它们不适合计算。如果你需要这样的方法,查看清单 7-2 。
清单 7-2 。获取字节整数和短整数的绝对值??
public class AbsByteShort
{
static byte abs(byte b)
{
return (b < 0) ? (byte) -b : b;
}
static short abs(short s)
{
return (s < 0) ? (short) -s : s;
}
public static void main(String[] args)
{
byte b = −2;
System.out.println(abs(b)); // Output: 2
short s = −3;
System.out.println(abs(s)); // Output: 3
}
}
清单 7-2 的(字节)和(短整型)强制转换是必要的,因为 -b 将 b 的值从一个字节转换为一个 int , -s 将 s 的值从一个短整型转换为一个 int 。相比之下, (b < 0) 和 (s < 0) 则不需要这些类型转换,它们会在将 b 和 s 的值与基于 int 的 0 进行比较之前,自动将它们转换为一个 int 。
提示它们在数学中的缺席表明字节和短整型在方法声明中不是很有用。但是,当声明其元素存储小值(如二进制文件的字节值)的数组时,这些类型很有用。如果您声明了一个由 int 或 long 组成的数组来存储这样的值,您最终会浪费堆空间(甚至可能会耗尽内存)。
在搜索 java.lang 包文档时,您可能会遇到一个名为 StrictMath 的类。除了一个更长的名字,这个类看起来和 Math 一样。这些类别之间的差异可以总结如下:
- StrictMath 的方法在所有平台上返回完全相同的结果。相比之下, Math 的一些方法可能会返回因平台不同而略有不同的值。
- 因为 StrictMath 不能利用平台特定的特性,如扩展精度数学协处理器,所以 StrictMath 的实现可能不如 Math 的实现有效。
在很大程度上, Math 的方法调用它们的严格数学的对应物。两个例外是 toDegrees() 和 toRadians() 。尽管这些方法在两个类中有相同的代码体,但是 StrictMath 的实现在方法头中包含了保留字 strictfp :
public static strictfp double toDegrees(double angrad)
public static strictfp double tora dians(double angdeg)
维基百科的“strictfp”词条(en.wikipedia.org/wiki/Strictfp
)提到 strictfp 限制浮点计算以保证可移植性。这个保留字在中间浮点表示和上溢/下溢(生成太大或太小而不适合表示的值)的上下文中实现了可移植性。
如果没有 strictfp ,中间计算就不局限于 Java 支持的 IEEE 754 32 位和 64 位浮点表示。相反,计算可以在支持这种表示的平台上利用更大的表示(可能是 128 位)。
当中间计算的值以 32/64 位表示时会溢出或下溢,而当其值以更多位表示时可能不会溢出/下溢。由于这种差异,可移植性受到了损害。 strictfp 通过要求所有平台使用 32/64 位进行中间计算来实现公平竞争。
当应用于一个方法时, strictfp 确保在该方法中执行的所有浮点计算都是严格符合的。然而, strictfp 可以在类头声明中使用(如在 public strictfp class Fourier transform 中),以确保该类中执行的所有浮点计算都是严格的。
注意 Math 和 StrictMath 被声明为 final 因此它们不能被扩展。此外,它们声明私有的空无参数构造函数,因此它们不能被实例化。最后, Math 和 StrictMath 是工具类的例子,因为它们是作为工具常量和工具(静态)方法的占位符而存在的。
BigDecimal(大十进制)
在第三章的中,我引入了一个 SavingsAccount 类,它有一个余额字段,类型为 int 。该字段记录该账户中的美元数量。或者,它可以表示帐户中包含的便士数。
也许你想知道为什么我没有将余额声明为类型 double 或 float 。这样, balance 可以存储 18.26 这样的值(整数部分 18 美元,小数部分 26 便士)。我没有将 balance 声明为 double 或 float ,原因如下:
- 并非所有可以表示货币数量(美元和美分)的浮点值都可以准确地存储在内存中。例如,0.1(可以用来表示 10 美分)没有精确的存储表示。如果你执行了 double total = 0.1;for(int I = 0;我<50;i++)合计+= 0.1;System.out.println(合计);,你会观察到 5.0999999999999998 而不是正确的 5.1 作为输出。
- 每个浮点计算的结果都需要四舍五入到最接近的分。否则会引入微小的误差,导致最终结果与正确结果不同。虽然 Math 提供了一对 round() 方法,您可能会考虑使用它们来将计算结果四舍五入到最接近的美分,但是这些方法会四舍五入到最接近的整数(美元)。
清单 7-3 的 InvoiceCalc 应用演示了这两个问题。然而,第一个问题并不严重,因为它对不准确性的贡献很小。更严重的问题发生在执行计算后未能舍入到最接近的分。
清单 7-3 。基于浮点的发票计算导致混乱的结果
import java.text.NumberFormat;
public class InvoiceCalc
{
final static double DISCOUNT_PERCENT = 0.1; // 10%
final static double TAX_PERCENT = 0.05; // 5%
public static void main(String[] args)
{
double invoiceSubtotal = 285.36;
double discount = invoiceSubtotal * DISCOUNT_PERCENT;
double subtotalBeforeTax = invoiceSubtotal - discount;
double salesTax = subtotalBeforeTax * TAX_PERCENT;
double invoiceTotal = subtotalBeforeTax + salesTax;
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
System.out.println("Subtotal: " + currencyFormat.format(invoiceSubtotal));
System.out.println("Discount: " + currencyFormat.format(discount));
System.out.println("SubTotal after discount: " +
currencyFormat.format(subtotalBeforeTax));
System.out.println("Sales Tax: " + currencyFormat.format(salesTax));
System.out.println("Total: " + currencyFormat.format(invoiceTotal));
}
}
清单 7-3 执行了几个与发票相关的计算,导致了不正确的最终总数。执行完这些计算后,它会获得一个基于货币的格式化程序,用于将双精度浮点值格式化为带有货币符号(如美元符号 $ )的基于字符串的货币金额。通过调用 java.text.NumberFormat 类的 number format getcurrency instance()方法获得格式化程序。然后,通过将该值作为参数传递给 NumberFormat 的 String format(double value)方法,将该值格式化为货币字符串。
当您运行 InvoiceCalc 时,您将发现以下输出:
Subtotal: $285.36
Discount: $28.54
SubTotal after discount: $256.82
Sales Tax: $12.84
Total: $269.67
此输出显示正确的小计、折扣、折扣后小计和销售税。相比之下,它错误地将 269.67 而不是 269.66 显示为最终总数。即使根据浮点计算,269.67 是正确的值,客户也可能不愿意多付一分钱:
Subtotal: 285.36
Discount: 28.536
SubTotal after discount: 256.824
Sales Tax: 12.8412
Total: 269.6652
这个问题是由于在执行下一次计算之前,没有将每次计算的结果四舍五入到最接近的分位。因此,256.824 中的 0.024 和 12.84 中的 0.0012 构成了最终值,导致 NumberFormat 的 format() 方法 将该值四舍五入为 269.67。
注意切勿使用浮点数或双精度数来表示货币值。
Java 以一个 java.math.BigDecimal 类的形式提供了这两个问题的解决方案。这个不可变的类(一个 BigDecimal 实例不能被修改)表示一个任意精度(位数)的带符号十进制数(比如 23.653),带有一个关联的小数位数(一个指定小数点后位数的整数)。
BigDecimal 声明三个方便常数:一个、十个、零个 。每个常数都是 1、10 和 0 的 BigDecimal 等效值,并且刻度为零。
注意 BigDecimal 声明了几个 ROUND_ 前缀的常量。这些常量在很大程度上已经过时,应该避免使用,还有公共 BigDecimal divide(BigDecimal divisor,int scale,int roundingMode) 和公共 BigDecimal setScale(int newScale,int roundingMode) 方法,它们仍然存在,以便相关的遗留代码继续编译。
BigDecimal 还声明了各种有用的构造函数和方法。在[表 7-2 中描述了其中一些构造函数和方法。
表 7-2。 BigDecimal 构造函数和方法
方法 | 描述 |
---|---|
BigDecimal(int val) | 将 BigDecimal 实例初始化为 val 的数字。将比例设置为 0。 |
BigDecimal(字符串 val) | 将 BigDecimal 实例初始化为 val 的十进制等效值。将小数位数设置为小数点后的位数,如果没有指定小数点,则设置为 0。当 val 为 null 时,该构造函数抛出 Java . lang . nullpointerexception,当 val 的字符串表示无效(例如包含字母)时,抛出 Java . lang . numberformatexception。 |
BigDecimal abs() | 返回一个新的 BigDecimal 实例,包含当前实例值的绝对值。结果比例与当前实例的比例相同。 |
bigdecimal add(bigdecimal eye) | 返回一个新的 BigDecimal 实例,它包含当前值和参数值的和。结果比例是当前比例和参数比例的最大值。当被加数为 null 时,该方法抛出 NullPointerException 。 |
BigDecimal divide(BigDecimal 除数) | 返回一个新的 BigDecimal 实例,该实例包含当前值除以参数值的商。结果标度是当前标度和参数标度的差。当结果需要更多位数时,可能会进行调整。当除数为 null 或 Java . lang . arithmetic exception 当除数表示 0 或结果无法精确表示时,该方法抛出 NullPointerException 。 |
最大小数点(BigDecimal val) | 返回这个或 val 中的,以包含较大值的 BigDecimal 实例为准。当 val 为 null 时,该方法抛出 NullPointerException 。 |
bigdecimal min(bigdecimal val) | 返回这个或 val 中的,无论哪个 BigDecimal 实例包含较小的值。当 val 为 null 时,该方法抛出 NullPointerException 。 |
bigdecimal multiple(bigdecimal 乘) | 返回一个新的 BigDecimal 实例,它包含当前值和参数值的乘积。结果比例是当前比例和参数比例的总和。当被乘数为 null 时,该方法抛出 NullPointerException 。 |
否定的 bigdecimal() | 返回一个新的 BigDecimal 实例,它包含当前值的负值。结果比例与当前比例相同。 |
int precision() | 返回当前 BigDecimal 实例的精度。 |
bigdecimal remainder(bigdecimal 除数) | 返回一个新的 BigDecimal 实例,该实例包含当前值除以参数值的余数。结果比例是当前比例和参数比例的差。当结果需要更多位数时,可能会进行调整。该方法在除数为 null 时抛出 NullPointerException 或者在除数表示 0 时抛出算术异常。 |
int scale() | 返回当前 BigDecimal 实例的小数位数。 |
BigDecimal set scale(int new scale,RoundingMode roundingMode) | 使用指定的小数位数和舍入模式返回新的 BigDecimal 实例。如果新比例大于旧比例,则在未缩放的值上添加额外的零。在这种情况下,不需要舍入。如果新的刻度小于旧的刻度,尾部数字将被删除。如果这些尾随数字不为零,则剩余的未缩放值必须四舍五入。对于此舍入运算,将使用指定的舍入模式。当舍入模式为 null 时,该方法抛出 NullPointerException ,当舍入模式设置为舍入模式时,该方法抛出算术异常。ROUND _ 不必要的,但是根据当前刻度,舍入是必要的。 |
大十进制减法(大十进制减数) | 返回一个新的 BigDecimal 实例,它包含当前值减去参数值。结果比例是当前比例和参数比例的最大值。当减数为 null 时,该方法抛出 NullPointerException 。 |
字符串 toString() | 返回这个 BigDecimal 实例的字符串表示。必要时使用科学符号。 |
表 7-2 是指 java.math.RoundingMode ,是一个包含各种舍入模式常量的 enum。这些常数在表 7-3 中描述。
表 7-3。??【舍入模式常数】??
常数 | 描述 |
---|---|
天花板 | 向正无穷大舍入。 |
向下 | 向零方向舍入。 |
楼层 | 向负无穷大舍入。 |
半 _ 下 | 向“最近的邻居”舍入,除非两个邻居等距,在这种情况下向下舍入。 |
半 _ 偶数 | 向“最近的邻居”舍入,除非两个邻居等距,在这种情况下,向偶数邻居舍入。 |
半开状态 | 向“最近的邻居”舍入,除非两个邻居等距,在这种情况下,向上舍入。(这是学校里普遍教的四舍五入模式。) |
不必要的 | 不需要舍入,因为所请求的操作会产生精确的结果。 |
上升 | 正值向正无穷大舍入,负值向负无穷大舍入。 |
熟悉 BigDecimal 的最好方法是尝试一下。清单 7-4 使用这个类来正确执行清单 7-3 中给出的发票计算。
*清单 7-4 。*基于 BigDecimal 的发票计算 不会导致混淆结果
import java.math.BigDecimal;
import java.math.RoundingMode;
public class InvoiceCalc
{
public static void main(String[] args)
{
BigDecimal invoiceSubtotal = new BigDecimal("285.36");
BigDecimal discountPercent = new BigDecimal("0.10");
BigDecimal discount = invoiceSubtotal.multiply(discountPercent);
discount = discount.setScale(2, RoundingMode.HALF_UP);
BigDecimal subtotalBeforeTax = invoiceSubtotal.subtract(discount);
subtotalBeforeTax = subtotalBeforeTax.setScale(2, RoundingMode.HALF_UP);
BigDecimal salesTaxPercent = new BigDecimal("0.05");
BigDecimal salesTax = subtotalBeforeTax.multiply(salesTaxPercent);
salesTax = salesTax.setScale(2, RoundingMode.HALF_UP);
BigDecimal invoiceTotal = subtotalBeforeTax.add(salesTax);
invoiceTotal = invoiceTotal.setScale(2, RoundingMode.HALF_UP);
System.out.println("Subtotal: " + invoiceSubtotal);
System.out.println("Discount: " + discount);
System.out.println("SubTotal after discount: " + subtotalBeforeTax);
System.out.println("Sales Tax: " + salesTax);
System.out.println("Total: " + invoiceTotal);
}
}
清单 7-4 的 main() 方法首先创建 BigDecimal 对象 invoiceSubtotal 和 discountPercent ,分别初始化为 285.36 和 0.10 。将发票小计乘以折扣百分比并将大小数结果赋给折扣。
此时,贴现包含 28.5360。除了结尾的零,这个值与清单 7–3 中的 invoice subtotal * DISCOUNT _ PERCENT 生成的值相同。应该存储在折扣中的值是 28.54。为了在执行另一个计算之前纠正这个问题, main() 使用这些参数调用 discount 的 setScale() 方法 :
- 2 :小数点后两位
- 圆周率。HALF_UP :传统的舍入方法
设置好比例和适当的取整方式后, main() 从 invoiceSubtotal 中减去 discount ,并将得到的 BigDecimal 实例赋给 subtotalbeforeax。 main() 调用 setScale() 对 subtotalbeforeax 进行适当舍入,然后继续下一次计算。
main() 接下来创建一个名为 salesTaxPercent 的 BigDecimal 对象,该对象被初始化为 0.05 。然后,它将 subtotalbeforeax 乘以 salesTaxPercent ,将结果赋给 salesTax ,并在此 BigDecimal 对象上调用 setScale() 以正确舍入其值。
继续, main() 将 salesTax 与 subtotalbeforeax 相加,将结果保存在 invoiceTotal 中,并通过 setScale() 对结果进行舍入。这些对象中的值通过 System.out.println() 发送到标准输出设备,后者调用它们的 toString() 方法返回 BigDecimal 值的字符串表示。
当您运行这个新版本的 InvoiceCalc 时,您将发现以下输出:
Subtotal: 285.36
Discount: 28.54
SubTotal after discount: 256.82
Sales Tax: 12.84
Total: 269.66
注意 BigDecimal 声明了一个 BigDecimal(double val) 构造函数,您应该尽可能避免使用它。该构造函数将 BigDecimal 实例初始化为存储在 val 中的值,使得当 double 无法准确存储时,该实例可以反映无效的表示。例如, BigDecimal(0.1) 导致 0.1000000000000005511151231257827021181583404541015625 存储在实例中。相比之下, BigDecimal(“0.1”) 恰好存储 0.1 。
BigInteger(大整数)
BigDecimal 将带符号的十进制数存储为 32 位整数刻度的未缩放值。未缩放的值存储在 java.math.BigInteger 类的一个实例中。
BigInteger 是一个不可变的类,表示任意精度的有符号整数。它以二进制补码格式 存储它的值(所有位都翻转—1 到 0 和 0 到 1—并且 1 被加到结果中,以与 Java 的字节整数、短整数、整数和长整数类型使用的二进制补码格式兼容)。
注意查看维基百科的“二进制补码”条目()了解更多关于二进制补码的知识。
BigInteger 声明了三个方便常数:一个、十个、零个 。每个常量都是 1、10 和 0 的大整数。
BigInteger 还声明了各种有用的构造函数和方法。在表 7-4 中描述了其中一些构造函数和方法。
表 7-4。big integer 构造函数和方法
方法 | 描述 |
---|---|
大整数(字节[] val) | 将 BigInteger 实例初始化为存储在 val 数组中的整数,其中 val[0] 存储整数的最高有效(最左边)8 位。当 val 为 null 时,该构造函数抛出 NullPointerException ,当 val.length 等于 0 时,抛出 NumberFormatException 。 |
大整数(字串 val) | 将 BigInteger 实例初始化为与 val 相等的整数。当 val 为 null 时,该构造函数抛出 NullPointerException ,当 val 的字符串表示无效(例如包含字母)时,抛出 NumberFormatException 。 |
大整数 abs() | 返回一个新的 BigInteger 实例,包含当前实例值的绝对值。 |
大整数加(big integer eye) | 返回一个新的 BigInteger 实例,它包含当前值和参数值之和。当被加数为 null 时,该方法抛出 NullPointerException 。 |
大整数除法 | 返回一个新的 BigInteger 实例,它包含当前值除以参数值的商。当除数为 null 时,该方法抛出 NullPointerException ,当除数表示 0 或结果不能精确表示时,抛出算术异常。 |
最大整数(BigInteger val) | 返回这个或者 val ,无论哪个 BigInteger 实例包含较大的值。当 val 为 null 时,该方法抛出 NullPointerException 。 |
【big integer min(big integer val) | 返回这个或者 val ,无论哪个 BigInteger 实例包含较小的值。当 val 为 null 时,该方法抛出 NullPointerException 。 |
【big integer 乘】 | 返回一个新的 BigInteger 实例,它包含当前值和参数值的乘积。当被乘数为 null 时,该方法抛出 NullPointerException 。 |
否定的大整数() | 返回一个新的 BigInteger 实例,它包含当前值的负值。 |
【big integer remainder(big integer 除数) | 返回一个新的 BigInteger 实例,该实例包含当前值除以参数值的余数。该方法在除数为 null 时抛出 NullPointerException ,在除数表示 0 时抛出算术异常。 |
【大大整数减】 | 返回一个新的 BigInteger 实例,它包含当前值减去参数值。当减数为 null 时,该方法抛出 NullPointerException 。 |
字符串 toString() | 返回这个 BigInteger 实例的字符串表示。 |
注意 BigInteger 还声明了几个面向位的方法,比如 big integer and(big integer val)、 BigInteger flipBit(int n) 、big integer shift left(int n)。当您需要执行低级位操作时,这些方法非常有用。
熟悉 BigInteger 的最好方法是尝试一下。清单 7-5 在 factorial() 方法比较上下文中使用了这个类。
清单 7-5 。比较*阶乘()*方法
import java.math.BigInteger;
public class FactComp
{
public static void main(String[] args)
{
System.out.println(factorial(12));
System.out.println();
System.out.println(factorial(20L));
System.out.println();
System.out.println(factorial(170.0));
System.out.println();
System.out.println(factorial(new BigInteger("170")));
System.out.println();
System.out.println(factorial(25.0));
System.out.println();
System.out.println(factorial(new BigInteger("25")));
}
static int factorial(int n)
{
if (n == 0)
return 1;
else
return n * factorial(n - 1);
}
static long factorial(long n)
{
if (n == 0)
return 1;
else
return n * factorial(n - 1);
}
static double factorial(double n)
{
if (n == 1.0)
return 1.0;
else
return n * factorial(n - 1);
}
static BigInteger factorial(BigInteger n)
{
if (n.equals(BigInteger.ZERO))
return BigInteger.ONE;
else
return n.multiply(factorial(n.subtract(BigInteger.ONE)));
}
}
清单 7-5 比较了递归阶乘()方法的四个版本。这种比较揭示了在返回的阶乘值变得没有意义之前可以传递给前三个方法的最大参数,因为数值类型可以精确表示的值的范围受到限制。
第一个版本基于 int ,有一个从 0 到 12 的有用参数范围。传递任何大于 12 的参数都会导致阶乘无法精确地表示为一个 int 。
您可以通过将参数和返回类型更改为 long 来增加 factorial() 的有用范围,但不会增加太多。做了这些改变后,你会发现有用范围的上限是 20。
为了进一步扩大有用的范围,您可以创建一个版本的 factorial() ,它的参数和返回类型是 double 。这是可能的,因为整数可以精确地表示为 double s。然而,可以传递的最大有用参数是 170.0。任何高于该值的值都会导致 factorial() 返回+无穷大。
您可能需要计算更高的阶乘值,也许是在计算涉及组合或排列的统计问题时。准确计算该值的唯一方法是使用基于 BigInteger 的 factorial() 版本。
当您运行前面的应用时,它会生成以下输出:
479001600
2432902008176640000
7.257415615307994E306
7257415615307998967396728211129263114716991681296451376543577798900561843401706157852350749242617459511490991237838520776666022565442753025328900773207510902400430280058295603966612599658257104398558294257568966313439612262571094946806711205568880457193340212661452800000000000000000000000000000000000000000
1.5511210043330986E25
15511210043330985984000000
前三个值表示基于 int 、基于 long 和基于 doublefactorial()方法可以返回的最高阶乘。第四个值代表最高 double 阶乘的 BigInteger 等效值。
请注意, double 方法无法准确表示 170!(!是阶乘的数学符号)。它的精度简直太小了。尽管该方法试图对最小的数字进行舍入,但舍入并不总是有效,因为数字以 7994 而不是 7998 结尾。正如最后两行输出所显示的,舍入只在参数 25.0 之前是准确的。
探索字符串管理
许多计算机语言实现了一个字符串的概念,一个被视为单个单元(而不是单个字符)的字符序列。例如,C 语言将字符串实现为以空字符( ‘\0’ )结尾的字符数组。相比之下,Java 通过 java.lang.String 类实现字符串。
字符串对象是不可变的:你不能修改一个字符串对象的字符串。各种看似修改字符串对象的字符串方法实际上返回一个新的字符串对象,其中包含修改后的字符串内容。因为返回新的 String 对象通常会造成浪费,所以 Java 提供了 java.lang.StringBuffer 和等效的 Java . lang . stringbuilder 类作为解决方法。
在本节中,我将向您介绍 String 和 String buffer/StringBuilder。
线
字符串 将一个字符串表示为一个字符序列。与 C 字符串不同,该序列不以空字符结束。相反,它的长度是单独存储的。与其他引用类型不同,Java 语言通过提供简化字符串处理的语法糖来特别对待字符串类。比如 Java 识别字符串 favLanguage = " Java 作为字符串文字“Java”到字符串变量 favLanguage 的赋值。没有这个糖,你就得指定 String fav language = new String(" Java ");。Java 语言还重载了 + 和 += 操作符来执行字符串连接。
表 7-5 描述了一些字符串的构造函数以及初始化字符串对象和处理字符串的方法。
表 7-5。 字符串构造函数和方法
方法 | 描述 |
---|---|
字符串(char[]数据) | 将这个字符串对象初始化为数据数组中的字符。在初始化这个字符串对象后修改数据对该对象没有影响。 |
字符串(字符串 s) | 将这个字符串对象初始化为的字符串。 |
char charAt(int index) | 返回这个字符串对象的字符串中位于从零开始的索引处的字符。当索引小于 0 或者大于等于字符串长度时,该方法抛出 Java . lang . stringindexoutofboundsexception。 |
String concat(String s) | 返回一个新的字符串对象,包含这个字符串对象的字符串,后跟的参数的字符串。 |
布尔结束(字符串后缀) | 当这个字符串对象的字符串以后缀参数中的字符结束时,当后缀为空(不包含字符)时,或者当后缀包含与这个字符串对象的字符串相同的字符序列时,返回 true。该方法执行区分大小写的比较(例如,A 不等于 A),并在后缀为 null 时抛出 NullPointerException 。 |
布尔等于(Object object) | 当对象的类型为字符串并且此参数的字符串包含与此字符串对象的字符串相同的字符(并且顺序相同)时,返回 true。 |
布尔值等于忽略大小写(字符串 s) | 当 s 和这个字符串对象包含相同的字符时返回 true(忽略大小写)。当字符序列不同时或者当 null 被传递给 s 时,该方法返回 false。 |
int indexOf(int c) | 返回此字符串对象的字符串中由 c 表示的字符的第一个匹配项(从字符串的开头到结尾)的从零开始的索引。当该字符不存在时,返回 1。 |
int indexOf(字符串 s) | 返回的的字符序列在这个字符串对象的字符串中第一次出现的位置(从字符串的开头到结尾)的从零开始的索引。当 s 不存在时返回 1。当 s 为 null 时,该方法抛出 NullPointerException 。 |
String intern() | 在字符串对象的内部表中搜索其字符串等于该字符串对象的字符串的对象。这个字符串对象的字符串在不存在时被添加到表中。返回包含在表中的对象,其字符串等于这个字符串对象的字符串。对于相等的字符串,总是返回相同的字符串对象。 |
int lastIndexOf(int c) | 返回由 c 表示的字符在这个字符串对象的字符串中最后一次出现(从字符串开头到字符串结尾)的从零开始的索引。当该字符不存在时,返回 1。 |
int lastIndexOf(String s) | 返回的的字符序列在这个字符串对象的字符串中最后一次出现的位置(从字符串的开头到结尾)的从零开始的索引。当 s 不存在时返回-1。当 s 为 null 时,该方法抛出 NullPointerException 。 |
int length() | 返回这个字符串对象的字符串中的字符数。 |
字符串替换(char oldChar,char newChar) | 返回一个新的字符串对象,其字符串与这个字符串对象的字符串匹配,除了所有出现的 oldChar 都被替换为 newChar 。 |
String[] split(字符串表达式) | 使用由 expr 指定的正则表达式(字符串的模式用于在字符串中搜索匹配模式的子字符串)将这个字符串对象的字符串拆分成一个由字符串对象组成的数组,作为拆分的基础。当 expr 为 null 时,该方法抛出 NullPointerException ,当 expr 的语法无效时,该方法抛出 Java . util . regex . patternsynctaxexception。 |
布尔型开头(字符串前缀) | 当这个字符串对象的字符串以前缀参数中的字符开始,当前缀为空(不包含字符),或者当前缀包含与这个字符串对象的字符串相同的字符序列时,返回 true。该方法执行区分大小写的比较(例如,A 不等于 A)并在前缀为 null 时抛出 NullPointerException 。 |
String substring(int start) | 返回一个新的字符串对象,其字符串包含这个字符串对象的字符,从位于开始的字符开始。当 start 为负或者大于这个字符串对象的字符串长度时,这个方法抛出 StringIndexOutOfBoundsException。 |
char[]火炬数组() | 返回一个字符数组,包含这个字符串对象的字符串中的字符。 |
字符串 tolpower case() | 返回一个新的字符串对象,其字符串包含这个字符串对象的字符,其中大写字母已被转换为小写字母。这个字符串对象在不包含要转换的大写字母时被返回。 |
String toUpperCase() | 返回一个新的字符串对象,其字符串包含这个字符串对象的字符,其中小写字母已经转换为大写字母。这个字符串对象在不包含要转换的小写字母时被返回。 |
String trim() | 返回一个新的字符串对象,该对象包含这个字符串对象的字符串,当没有前导/尾随空格时,从该字符串或这个字符串对象的开头和结尾删除了空格字符(Unicode 值为 32 或更小的字符)。 |
表 7-5 揭示了几个关于弦的有趣项目。首先,这个类的字符串(字符串 s) 构造函数没有将字符串对象初始化为字符串文字。相反,它的行为类似于 C++复制构造函数,将字符串对象初始化为另一个字符串对象的内容。这种行为表明,字符串文字比它看起来要复杂。
实际上,字符串文字是一个字符串对象。你可以通过执行 System.out.println("abc “)来证明这一点。length());和 system . out . println(” ABC " instance of String);。第一个方法调用输出 3 ,这是“ABC”字符串对象的字符串长度,第二个方法调用输出 true(“ABC”是一个字符串对象)。
注意字符串被存储在一个叫做常量池 的类文件数据结构中。当加载一个类时,为每个字面值创建一个字符串对象,并存储在一个字符串对象的内部表中。
第二个有趣的项目是 intern() 方法, interns (存储一个唯一的副本)一个字符串对象在一个字符串对象的内部表中。 intern() 可以通过引用和 == 或来比较字符串!= 。这些运算符是比较字符串的最快方法,在对大量字符串进行排序时尤其有用。
默认情况下,由文字字符串(“ABC”)和字符串值常量表达式(“a”+“BC”)表示的 String 对象在该表中被拘留,这也是为什么 system . out . println(" ABC " = =“a”+“BC”);输出真值。但是通过 String 构造函数创建的 String 对象是不被 interned 的,这就是为什么 system . out . println(" ABC " = = new String(" ABC “));输出假。相比之下,system . out . println(” ABC " = = new String(" ABC ")。实习生());输出真值。
注意小心使用这种字符串比较技术(它只比较引用),因为当一个被比较的字符串没有被保留时,你很容易引入一个 bug。如有疑问,使用 equals() 或 equalsIgnoreCase() 方法。比如“ABC”各一个。equals(新字符串(“ABC”)和“ABC”。equalsIgnoreCase(new String(" ABC "))返回 true。
表 7-5 还展示了 charAt() 和 length() 方法,这对于迭代字符串的字符很有用。比如 String s = " ABC ";for(int I = 0;I<s . length();i++)system . out . println(s . charat(I));返回每个 s 的 a 、 b 和 c 字符,并在单独的行上输出每个字符。
最后,表 7-5 展示了 split() ,这是我在第六章的 StubFinder 应用中使用的一种方法,用于将一个字符串的逗号分隔值列表拆分成一个由 String 对象组成的数组。此方法使用一个正则表达式来标识字符串拆分所围绕的字符序列。(我会在第十三章讨论正则表达式。)
注意StringIndexOutOfBoundsException 和 ArrayIndexOutOfBoundsException 是共享一个公共 Java . lang . indexoutofboundsexception 超类的兄弟类。
StringBuffer 和 StringBuilder
字符串对象是不可变的:你不能修改一个字符串对象的字符串。看似修改了字符串对象的字符串方法(比如 replace() )实际上返回了一个新的字符串对象,而不是修改后的字符串内容。因为返回新的 String 对象通常会造成浪费,所以 Java 提供了 java.lang.StringBuffer 和 Java . lang . stringbuilder 类作为解决方法。
StringBuffer 和 StringBuilder 除了 StringBuilder 提供比 StringBuffer 更好的性能,但不能在没有显式线程同步的多线程环境中使用(在第八章中讨论)之外,其他都是相同的。
提示在多线程环境中使用 StringBuffer (为了安全),在单线程环境中使用 StringBuilder (为了性能)。
StringBuffer 和 StringBuilder 为高效构建字符串提供了内部字符数组。在创建了一个 string buffer/StringBuilder 对象之后,您可以调用各种方法在数组中添加、删除和插入各种值的字符表示。然后调用 toString() 将数组的内容转换成一个 String 对象并返回这个对象。
表 7-6 描述了一些 StringBuffer 的构造函数和方法,用于初始化 StringBuffer 对象和处理字符串缓冲区。 StringBuilder 的构造函数和方法是相同的,不再讨论。
表 7-6。 StringBuffer 构造函数和方法
方法 | 描述 |
---|---|
串缓冲() | 将这个 StringBuffer 对象初始化为一个空数组,初始容量为 16 个字符。 |
StringBuffer(整数容量) | 将这个 StringBuffer 对象初始化为一个空数组,初始容量为 capacity 个字符。当容量为负时,该构造函数抛出 Java . lang . negative earraysizeexception。 |
串缓冲(串 s) | 将这个 StringBuffer 对象初始化为一个包含的字符的数组。这个对象的初始容量是 16 加上 s 的长度。当 s 为 null 时,该构造函数抛出 NullPointerException 。 |
字串缓冲附录(boolean b) | 当 b 为真时将真追加到该 StringBuffer 对象的数组中,当 b 为假时将假追加到该数组中,并返回该 StringBuffer 对象。 |
串缓冲附录(char ch) | 将 ch 的字符追加到这个 StringBuffer 对象的数组中,并返回这个 StringBuffer 对象。 |
string buffer append(char[]chars) | 将 chars 数组中的字符追加到这个 StringBuffer 对象的数组中,并返回这个 StringBuffer 对象。当 chars 为 null 时,该方法抛出 NullPointerException 。 |
string buffer append(double d) | 将 d 的双精度浮点值的字符串表示追加到这个 StringBuffer 对象的数组中,并返回这个 StringBuffer 对象。 |
string buffer append(float f) | 将 f 的浮点值的字符串表示追加到这个 StringBuffer 对象的数组中,并返回这个 StringBuffer 对象。 |
StringBuffer append(int i) | 将 i 的整数值的字符串表示追加到这个 StringBuffer 对象的数组中,并返回这个 StringBuffer 对象。 |
string buffer append(long l) | 将 l 的长整型值的字符串表示追加到这个 StringBuffer 对象的数组中,并返回这个 StringBuffer 对象。 |
StringBuffer append(对象 obj) | 调用 obj 的 toString() 方法,将返回的字符串的字符追加到这个 StringBuffer 对象的数组中。当 null 传递给 obj 时,将 null 追加到数组中。返回这个 StringBuffer 对象。 |
StringBuffer append(字符串 s) | 将 s 的字符串追加到这个 StringBuffer 对象的数组中。当 null 传递给 s 时,将 null 追加到数组中。返回这个 StringBuffer 对象。 |
int capacity() | 返回这个 StringBuffer 对象的数组的当前容量。 |
char charAt(int index) | 返回这个 StringBuffer 对象数组中位于索引的字符。当索引为负或者大于等于这个 StringBuffer 对象的长度时,这个方法抛出 StringIndexOutOfBoundsException。 |
void ensure capacity(int min) | 确保这个 StringBuffer 对象的容量至少是由 min 指定的容量。如果当前容量小于 min ,则创建一个新的具有更大容量的内部数组。新容量设置为最小和当前容量乘以 2 的较大值,结果加 2。当最小值为负或为零时,不采取任何行动。 |
int length() | 返回存储在这个 StringBuffer 对象的数组中的字符数。 |
字符串缓冲反向() | 返回这个 StringBuffer 对象,并反转其数组内容。 |
见 setharat(int index,char ch) | 用 ch 替换索引处的字符。当索引为负或者大于等于这个 StringBuffer 对象的数组长度时,这个方法抛出 StringIndexOutOfBoundsException。 |
void setLength(int length) | 将这个 StringBuffer 对象的数组长度设置为长度。如果长度参数小于当前长度,数组的内容将被截断。如果长度参数大于或等于当前长度,则足够的空字符( ‘\u0000’ )被追加到数组中。当长度为负时,该方法抛出 StringIndexOutOfBoundsException。 |
String substring(int start) | 返回一个新的 String 对象,包含这个 StringBuffer 对象数组中的所有字符,从位于 start 的字符开始。当 start 小于 0 或者大于等于这个 StringBuffer 对象的数组长度时,这个方法抛出 StringIndexOutOfBoundsException。 |
字符串 toString() | 返回一个新的 String 对象,其字符串等于这个 StringBuffer 对象的数组的内容。 |
一个 StringBuffer 或 StringBuilder 对象的内部数组与容量和长度的概念相关联。容量指的是在数组增长以容纳更多字符之前,数组中可以存储的最大字符数。 Length 指数组中已经存储的字符数。
考虑这样一个场景,您已经编写了将整数值格式化为字符串的代码。作为格式化程序的一部分,您需要在整数前添加特定数量的前导空格。您决定使用以下初始化代码和循环来构建一个带有 3 个前导空格的 space prefix 字符串:
int numLeadingSpaces = 3; // default value
String spacesPrefix = "";
for (int j = 0; j < numLeadingSpaces; j++)
spacesPrefix += "0";
这个循环是低效的,因为每次迭代都创建一个 StringBuilder 对象和一个 String 对象。编译器将该代码片段转换为以下片段:
int numLeadingSpaces = 3; // default value
String spacesPrefix = "";
for (int j = 0; j < numLeadingSpaces; j++)
spacesPrefix = new StringBuffer().append(spacesPrefix).append("0").toString();
对前面的循环进行编码的一种更有效的方法是在进入循环之前创建一个 string buffer/StringBuilder 对象,在循环中调用适当的 append() 方法,并在循环之后调用 toString() 。以下代码片段演示了这种更高效的场景:
int numLeadingSpaces = 3; // default value
StringBuffer sb = new StringBuffer();
for (int j = 0; j < numLeadingSpaces; j++)
sb.append('0');
String spacesPrefix = sb.toString();
注意避免在冗长的循环中使用字符串连接操作符,因为这会导致创建许多不必要的 StringBuilder 和 String 对象。
获取包信息
java.lang.Package 类提供了对包信息的访问(见第五章包的介绍)。这些信息包括关于 Java 包的实现和规范的版本细节、包的名称,以及包是否已经被密封的指示(包中的所有类都被归档在同一个 JAR 文件中)。
表 7-7 描述了包的一些方法。
表 7-7。 包法
方法 | 描述 |
---|---|
String getImplementationTitle() | 返回此包实现的标题,可能为空。标题的格式未指定。 |
字串 getImplementationVendor() | 返回提供此包实现的供应商或组织的名称。该名称可能为空。名称的格式未指定。 |
字符串 getImplementationVersion() | 返回此包实现的版本号,可能为空。此版本字符串必须是由句点分隔的正十进制整数序列,并且可以有前导零。 |
字符串 getName() | 以标准的点符号返回这个包的名称,例如, java.lang 。 |
静态包 get Package(String Package name) | 当找不到标识为 packageName 的包时,返回与标识为 packageName 的包关联的 Package 对象,或者返回 null。当 packageName 为 null 时,该方法抛出 NullPointerException 。 |
静态包[]获取包() | 返回该方法调用方可以访问的所有包对象的数组。 |
字符串获取指定标题() | 返回这个包的规范的标题,可能为空。标题的格式未指定。 |
字符串 get spec vendor() | 返回提供此包实现的规范的供应商或组织的名称。该名称可能为空。名称的格式未指定。 |
字符串 get spec version() | 返回此包实现的规范的版本号,可能为空。此版本字符串必须是由句点分隔的正十进制整数序列,并且可以有前导零。 |
boolean 与(所需字符串)兼容 | 通过将该包的规范版本与所需的版本进行比较,检查该包以确定其是否与指定的版本字符串兼容。当此包的规范版本号大于或等于所需版本号时返回 true(此包兼容);否则,返回 false。当期望的为 null 时,该方法抛出 NullPointerException ,当该包的版本号或期望的版本号不是点格式时,该方法抛出 NumberFormatException 。 |
布尔 isSealed() | 当此包已被密封时返回 true 否则,返回 false。 |
我已经创建了一个 PackageInfo 应用,演示了表 7-7 的包的大部分方法。清单 7-6 展示了这个应用的源代码。
清单 7-6 。获取关于包的信息
public class PackageInfo
{
public static void main(String[] args)
{
if (args.length == 0)
{
System.err.println("usage: java PackageInfo packageName [version]");
return;
}
Package pkg = Package.getPackage(args[0]);
if (pkg == null)
{
System.err.println(args[0] + " not found");
return;
}
System.out.println("Name: " + pkg.getName());
System.out.println("Implementation title: " +
pkg.getImplementationTitle());
System.out.println("Implementation vendor: " +
pkg.getImplementationVendor());
System.out.println("Implementation version: " +
pkg.getImplementationVersion());
System.out.println("Specification title: " +
pkg.getSpecificationTitle());
System.out.println("Specification vendor: " +
pkg.getSpecificationVendor());
System.out.println("Specification version: " +
pkg.getSpecificationVersion());
System.out.println("Sealed: " + pkg.isSealed());
if (args.length > 1)
System.out.println("Compatible with " + args[1] + ": " +
pkg.isCompatibleWith(args[1]));
}
}
编译完清单 7-6(【PackageInfo.java】)之后,在命令行上至少指定一个包名。例如, java PackageInfo java.lang 在 Java 7 下返回以下输出:
Name: java.lang
Implementation title: Java Runtime Environment
Implementation vendor: Oracle Corporation
Implementation version: 1.7.0_06
Specification title: Java Platform API Specification
Specification vendor: Oracle Corporation
Specification version: 1.7
Sealed: false
PackageInfo 还可以让您确定软件包的规范是否与特定的版本号兼容。包与其前身兼容。
比如 Java package info Java . lang 1.6 输出兼容 1.6 的:真,而 Java package info Java . lang 1.8 输出兼容 1.8 的:假。
你也可以在自己的包中使用 PackageInfo ,这些包是你在第五章中学到的。例如,那一章介绍了一个日志 包。
将 PackageInfo.class 复制到包含日志包目录(包含编译后的类文件)的目录下,执行 java PackageInfo 日志。
PackageInfo 通过显示以下输出进行响应:
logging not found
出现此错误消息是因为 getPackage() 在返回描述该包的包对象之前,需要从包中加载至少一个 classfile。
消除前面错误信息的唯一方法是从包中加载一个类。通过将下面的代码片段合并到清单 7–6 中来完成这项任务。
if (args.length == 3)
try
{
Class.forName(args[2]);
}
catch (ClassNotFoundException cnfe)
{
System.err.println("cannot load " + args[2]);
return;
}
这段代码片段,必须在包 pkg = Package.getPackage(args[0])之前;,加载由修改后的 PackageInfo 应用的第三个命令行参数命名的类文件。
通过 java PackageInfo logging 1.5 运行新的 PackageInfo 应用。假设 File.class 存在(您需要在指定这个命令行之前编译这个包),这个命令行将 logging 的 File class 标识为要加载的类:
Name: logging
Implementation title: null
Implementation vendor: null
Implementation version: null
Specification title: null
Specification vendor: null
Specification version: null
Sealed: false
Exception in thread "main" java.lang.NumberFormatException: Empty version string
at java.lang.Package.isCompatibleWith(Unknown Source)
at PackageInfo.main(PackageInfo.java:41)
看到所有这些空值并不奇怪,因为没有包信息被添加到日志包中。另外,抛出 NumberFormatException 与() 兼容,因为 logging 包不包含点格式的规范版本号(为空)。
也许将包信息放入日志包的最简单方法是创建一个日志. jar 文件,类似于第五章中所示的例子。但是首先,您必须创建一个包含包信息的小文本文件。您可以为文件选择任何名称。清单 7-7 揭示了我对 manifest.mf 的选择。
清单 7-7 。包含包裹信息的 manifest.mf
Implementation-Title: Logging Implementation
Implementation-Vendor: Jeff Friesen
Implementation-Version: 1.0a
Specification-Title: Logging Specification
Specification-Vendor: Jeff "JavaJeff" Friesen
Specification-Version: 1.0
Sealed: true
注意确保在最后一行结束时按下回车键(密封:真)。否则,您可能会在输出中看到 密封:错误,因为这个条目不会被 JDK 的 jar 工具 jar 存储在日志包中。
执行下面的命令行来创建一个 JAR 文件,该文件包括日志及其文件,以及其清单,一个名为清单的特殊文件。MF 存储关于 JAR 文件内容的信息,包含清单 7-7 的内容:
jar cfm logging.jar manifest.mf logging
或者,您可以指定以下稍长的命令行之一,相当于前面的命令行:
jar cfm logging.jar manifest.mf logging\*.class
jar cfm logging.jar manifest.mf logging/*.class
任一命令行都创建一个名为 logging.jar 的 JAR 文件(通过 c 【创建】和 f 【文件】选项)。它还将 manifest.mf 的内容(通过 m【MANIFEST】选项)合并到清单中。MF ,它存储在包的/JAR 文件的 META-INF 目录中。
注意要了解关于 JAR 文件清单的更多信息,请阅读 JDK 文档“JAR 文件规范”页面的“JAR 清单”部分(docs . Oracle . com/javase/7/docs/technotes/guides/JAR/JAR . html # JAR _ Manifest
)。
假设 jar 工具没有出现错误信息,执行以下面向 Windows 的命令行(或者适合您平台的命令行)运行 PackageInfo ,从日志包中提取包信息:
java -cp logging.jar;. PackageInfo logging 1.0 logging.File
-cp 命令行选项让您指定类路径,它由 logging.jar 和当前目录(由点(表示)组成。)字符)。未能指定点,java 输出一条错误消息,抱怨无法定位 PackageInfo.class 。
这一次,您应该会看到以下输出:
Name: logging
Implementation title: Logging Implementation
Implementation vendor: Jeff Friesen (IV)
Implementation version: 1.0a
Specification title: Logging Specification
Specification vendor: Jeff Friesen (SV)
Specification version: 1.0
Sealed: true
Compatible with 1.0: true
练习
以下练习旨在测试您对第七章内容的理解:
- Math 声明了哪些常数?
- 为什么是 Math.abs(Integer。 最小值】)等于整数。最小值?
- Math 的 random() 方法完成了什么?为什么表达式 (int) Math.random() * limit 不正确?
- 确定浮点计算过程中可能出现的五个特殊值。
- 数学和严格数学有什么不同?
- strictfp 的目的是什么?
- 什么是 BigDecimal 以及你为什么会使用这个类?
- 哪个舍入模式常数描述了学校通常教授的舍入形式?
- 什么是 BigInteger ?
- 是非判断:字符串文字是一个字符串对象。
- 串的实习生()方法的目的是什么?
- String 和 StringBuffer 有什么区别?
- StringBuffer 和 StringBuilder 有什么不同?
- 包的 isSealed() 方法的目的是什么?
- 是非判断: getPackage() 在返回描述该包的包对象之前,需要从包中加载至少一个 classfile。
- 质数是一个大于 1 的正整数,只能被 1 和它本身整除。创建一个 PrimeNumberTest 应用,该应用确定其唯一的整数参数是否是素数,并输出一个合适的消息。例如, java 素数测试 289 应该输出消息 289 不是素数。检查素性的一个简单方法是从 2 开始循环到整数参数的平方根,并在循环中使用余数运算符来确定参数是否被循环索引整除。例如,因为 6 % 2 产生余数 0 (2 被 6 整除),所以整数 6 不是素数。
- 重写下面低效的循环来使用 StringBuffer 。产生的循环应该最小化对象创建:
```java
String[] imageNames = new String[NUM_IMAGES];
for (int i = 0; i < imageNames.length; i++)
imageNames[i] = new String("image" + i + ".png");
```
- 创建一个接受单个基于整数的命令行参数的 DigitsToWords 应用。这个应用将这个参数转换为一个 int 值(通过 integer . parse int(args[0])),然后将结果传递给一个 String convertDigitsToWords(int integer)类方法,该方法返回一个包含该数字的文本表示的字符串。比如 1 转换为 1, 16 转换为16, 69 转换为六十九, 123 转换为一百二十三, 2938 转换为二千九百三十八当传递给 integer 的值小于 0 或大于 9999 时抛出 Java . lang . illegalargumentexception。使用 StringBuffer 类作为生成文本的存储库。使用示例: java DigitsToWords 2938 。
摘要
标准类库通过其 java.lang 和 java.math 包提供了许多基本的 API。这些 API 包括 Math , StrictMath , BigDecimal , BigInteger , String , StringBuffer , StringBuilder 和 Package 类以及 RoundingMode enum。
数学用高级运算(如三角学)补充基本数学运算( + 、 - 、 * 、 / 、 % )。配套的 StrictMath 类确保所有这些操作在所有平台上产生相同的值。
货币绝不能用浮点和双精度浮点变量来表示,因为不是所有的货币值都能精确表示。相比之下, BigDecimal 类允许您精确地表示和操作这些值。
BigDecimal 依赖于 BigInteger 类来表示其未缩放的值。一个 BigInteger 实例描述了一个任意长度的整数值(受虚拟机内存的限制)。
String 将一个字符串表示为一个字符序列。因为 String 实例是不可变的,Java 提供了 StringBuffer 和 StringBuilder 来更高效地构建字符串。前一个类用于多线程上下文中;后一类(性能更高)是供单线程使用的。
包类提供对包信息的访问。这些信息包括关于 Java 包的实现和规范的版本信息、包的名称以及包是否密封的指示。
在第八章中,我继续通过关注基本类型包装类、线程和系统功能来探索基本 API。