Java9 秘籍(二)
四、数字和日期
数字在许多应用中扮演着重要的角色。因此,了解如何在您试图执行的工作环境中正确使用它们是很有帮助的。本章帮助您了解如何使用数字执行一些最基本的操作,并且还提供了关于执行高级任务(如使用货币)的见解。使用货币有很多方法,本章将重点介绍其中的几种。
日期也很重要,因为它们在应用中有多种用途。在 Java 8 中,引入了名为 java.time 的新日期时间包。日期-时间 API 使用 ISO-8601 中定义的日历作为默认日历。因此,日历是基于公历系统的,在本章中,您将学习如何处理日期、时间和时区数据。日期-时间 API 遵循几个设计原则,因为它清晰、流畅、不可变和可扩展。这个 API 使用了一种清晰的语言,简洁且定义明确。它也非常流畅,因此处理日期-时间数据的代码易于阅读和理解。日期-时间 API 中的大多数类都是不可变的,因此为了改变日期-时间对象,您必须创建原始对象的修改副本。因此,date-time 类中的许多方法都有相应的名称,例如 of()和 with(),这样您就知道您是在创建一个副本,而不是修改原始文件。最后,新的日期-时间 API 可以在许多情况下进行扩展,这使得它在许多环境中都很有用。
日期-时间 API 由一组丰富的类组成,提供了在以前的 API 中很难实现的解决方案。尽管有许多不同的类,但大多数都包含一组相似的方法,因此相同的原则可以应用于所有的日期和时间单位。表 4-1 列出了大多数日期-时间类中常见的一组方法。
表 4-1。日期时间 API 的常用方法
|方法
|
描述
| | --- | --- | | 在 | 将一个对象与另一个对象合并。 | | 格式 | 将指定的格式应用于临时对象,生成一个字符串。 | | 从 | 将输入参数转换为目标类的实例。 | | 得到 | 返回目标对象的部分状态。 | | 存在 | 查询目标对象。 | | 负的 | 返回减去指定时间后的目标对象的修改副本。 | | 关于 | 使用指定的验证输入参数创建一个实例。 | | 从语法上分析 | 分析输入字符串以产生目标类的实例。 | | 加 | 返回添加了指定时间量的目标对象的修改副本。 | | 到 | 将对象转换为不同的类型。 | | 随着 | 返回指定元素已更改的目标对象的修改副本(相当于 setter)。 |
如前所述,日期-时间 API 是流畅的;因此,它的每个类都位于一个明确标记的包中。表 4-2 列出了组成日期时间 API 的包,以及在每个包中可以找到的类的简要描述。
表 4-2。日期时间 API 包
|包裹
|
描述
| | --- | --- | | java.time | API 的核心类。这些类用于处理基于 ISO-8601 标准的日期时间数据。这些类是不可变的和线程安全的。 | | java.time.chrono | 使用除 ISO-8601 以外的日历系统的 API。 | | java.time.format | 用于格式化日期时间数据的类。 | | java.time.temporal | 允许在日期-时间类之间插值的扩展 API。 | | java.time.zone | 支持时区数据的类。 |
本章简要概述了一些常用的日期时间功能。如果您将执行与日期和时间相关的重要工作,那么除了本章之外,您还应该阅读在线提供的日期-时间 API 文档。
4-1.将浮点和双精度值舍入为整数
问题
您需要能够将应用中的浮点数或双精度数舍入为整数值。
解决办法
使用 java.lang.Math round()方法之一将数字舍入为所需的格式。Math 类有两种方法可用于对浮点数或双精度值进行舍入。下面的代码演示了如何使用这些方法:
public static int roundFloatToInt(float myFloat){
return Math.round(myFloat);
}
public static long roundDoubleToLong(double myDouble){
return Math.round(myDouble);
}
第一个方法 roundFloatToInt()接受一个浮点数,并使用 java.lang.Math 类将该数四舍五入为整数。第二个方法 roundDoubleToLong()接受一个 Double 值,并使用 java.lang.Math 类将该 Double 值四舍五入为 Long 值。
它是如何工作的
java.lang.Math 类包含大量帮助方法,使我们在处理数字时更加轻松。round()方法也不例外,因为它们可以很容易地用于舍入浮点或双精度值。java.lang.Math round()方法的一个版本接受浮点数作为参数。它会将浮点数舍入到最接近的 int 值,并向上舍入。如果参数不是数字(NaN),则返回零。当正无穷大或负无穷大的参数传递给 round()时,结果等于 Integer 的值。MAX_VALUE 或整数。将分别返回 MIN_VALUE。java.lang.Math round()方法的第二个版本接受双精度值。double 值被舍入到最接近的 long 值,并向上舍入。就像另一轮()一样,如果参数是 NaN,将返回一个零。类似地,当正无穷大或负无穷大的参数传递给 round()时,结果等于 Long 的值。MAX_VALUE 或 Long。将分别返回 MIN_VALUE。
注意
NaN、POSITIVE_INFINITY 和 NEGATIVE_INFINITY 是在 Float 和 Double 类中定义的常量值。NaN(非数字)是一个未定义或不可表示的值。例如,NaN 值可以通过将 0.0f 除以 0.0f 来生成。由 POSITIVE_INFINITY 和 NEGATIVE_INFINITY 表示的值是指由生成特定类型(浮点型或双精度型)的极大值或负值的运算生成的值,这些值无法正常表示。例如,1.0/0.0 或–1.0/0.0 会产生这样的值。
4-2.格式化双精度和长十进制值
问题
您需要能够在应用中格式化双精度和长数字。
解决办法
使用 DecimalFormat 类将值格式化并舍入到应用要求的精度。在下面的方法中,接受双精度值并打印格式化的字符串值:
public static void formatDouble(double myDouble){
NumberFormat numberFormatter = new DecimalFormat("##.000");
String result = numberFormatter.format(myDouble);
System.out.println(result);
}
例如,如果传递给 formatDouble()方法的 double 值是 345.9372,则结果如下:
345.937
同样,如果将值. 7697 传递给该方法,结果如下:
.770
每个结果都使用指定的模式进行格式化,然后进行相应的舍入。
它是如何工作的
DecimalFormat 类可以与 NumberFormat 类一起使用,对 double 或 long 值进行舍入和/或格式化。NumberFormat 是一个抽象类,它提供了格式化和解析数字的接口。这个类提供了为每个地区格式化和解析数字的能力,并获得货币、百分比、整数和数字的格式。NumberFormat 类本身非常有用,因为它包含可用于获取格式化数字的工厂方法。事实上,要获得一个格式化的字符串,几乎不需要做什么工作。例如,下面的代码演示了对 NumberFormat 类调用一些工厂方法:
// Obtains an instance of NumberFormat class
NumberFormat format = NumberFormat.getInstance();
// Format a double value for the current locale
String result = format.format(83.404);
System.out.println(result);
// Format a double value for an Italian locale
result = format.getInstance(Locale.ITALIAN).format(83.404);
System.out.println(result);
// Parse a String into a Number
try {
Number num = format.parse("75.736");
System.out.println(num);
} catch (java.text.ParseException ex){
System.out.println(ex);
}
若要使用模式进行格式化,DecimalFormat 类可以与 NumberFormat 一起使用。在这个配方的解决方案中,您看到了通过向其构造函数传递一个模式来创建一个新的 DecimalFormat 实例将返回一个 NumberFormat 类型。这是因为 DecimalFormat 扩展了 NumberFormat 类。因为 NumberFormat 类是抽象的,所以 DecimalFormat 包含了 NumberFormat 的所有功能,以及处理模式的附加功能。因此,就像您在前面的演示中看到的那样,它可以用于处理不同地区的不同格式。这在处理双精度或长格式时提供了极大的灵活性。
如前所述,DecimalFormat 类可以在其构造函数中采用基于字符串的模式。还可以使用 applyPattern()方法在事后将模式应用于 Format 对象。每个模式都包含前缀、数字部分和后缀,这允许您将特定的十进制值格式化为所需的精度,并根据需要包含前导数字和逗号。用于构建模式的符号显示在表 4-3 中。每个模式还包含一个积极和消极的子模式。这两个子模式由分号(;)并且负子模式是可选的。如果不存在负子模式,则使用局部减号。例如,一个完整的模式示例应该是###,# # 0.00;(###,##0.00).
表 4-3。十进制格式模式字符
|性格;角色;字母
|
描述
| | --- | --- | | # | 数字;如果没有数字,则为空白 | | Zero | 数字;如果没有数字,则为零 | | 。 | 小数 | | - | 减号或负号 | | , | 逗号或分组分隔符 | | E | 科学符号分隔符 | | ; | 正负子模式分隔符 |
DecimalFormat 类提供了足够的灵活性,几乎可以在任何情况下格式化 double 和 long 值。
4-3.比较 int 值
问题
你需要比较两个或更多的 int 值。
解决方案 1
使用比较运算符比较整数值。在下面的示例中,三个 int 值相互比较,演示了各种比较运算符:
int int1 = 1;
int int2 = 10;
int int3 = -5;
System.out.println(int1 == int2); // Result: false
System.out.println(int3 == int1); // Result: false
System.out.println(int1 == int1); // Result: true
System.out.println(int1 > int3); // Result: true
System.out.println(int2 < int3); // Result: false
如您所见,比较运算符将生成一个布尔结果。
解决方案 2
使用 Integer.compare(int,int)方法对两个 int 值进行数值比较。下面几行可以比较第一个解决方案中声明的相同 int 值:
System.out.println("Compare method -> int3 and int1: " + Integer.compare(int3, int1));
// Result -1
System.out.println("Compare method -> int2 and int1: " + Integer.compare(int2, int1));
// Result 1
它是如何工作的
也许最常用的数值比较是针对两个或更多的 int 值。Java 语言使得使用比较运算符来比较一个 int 变得非常容易(见表 4-4 )。
表 4-4。比较运算符
|操作员
|
功能
| | --- | --- | | == | 等于 | | != | 不等于 | | > | 大于 | | < | 不到 | | >= | 大于或等于 | | <= | 小于或等于 |
这个方法的第二个解决方案演示了在 Java 7 语言中添加的 integer compare()方法。这个静态方法接受两个 int 值并对它们进行比较,如果第一个 int 值大于第二个,则返回 1,如果两个 int 值相等,则返回 0,如果第一个 int 值小于第二个,则返回-1。若要使用 Integer.compare()方法,请传递两个 int 值,如以下代码所示:
Integer.compare(int3, int1));
Integer.compare(int2, int1));
就像你在学校的数学课一样,这些比较运算符将确定第一个整数是等于、大于还是小于第二个整数。这些比较运算符简单易用,最常见于 if 语句的上下文中。
4-4.比较浮点数
问题
您需要在应用中比较两个或多个浮点值。
解决方案 1
使用 float 对象的 compareTo()方法来执行一个 Float 与另一个 Float 的比较。以下示例显示了 compareTo()方法的实际应用:
Float float1 = new Float("9.675");
Float float2 = new Float("7.3826");
Float float3 = new Float("23467.373");
System.out.println(float1.compareTo(float3)); // Result: -1
System.out.println(float2.compareTo(float3)); // Result: -1
System.out.println(float1.compareTo(float1)); // Result: 0
System.out.println(float3.compareTo(float2)); // Result: 1
调用 compareTo()方法的结果是一个整数值。负结果表示第一个浮点值小于与之比较的浮点值。零表示两个浮点值相等。最后,正的结果表明第一个浮点值大于与之比较的浮点值。
解决方案 2
使用 Float 类 compare()方法来执行比较。下面的示例演示了 Float.compare(Float,float)方法的用法。
System.out.println(Float.compare(float1, float3)); // Result: -1
System.out.println(Float.compare(float2, float3)); // Result: -1
System.out.println(Float.compare(float1, float1)); // Result: 0
System.out.println(Float.compare(float3, float2)); // Result: 1
它是如何工作的
比较两个 float 对象最有用的方法是使用 compareTo()方法。该方法将对给定的浮点对象执行数值比较。结果将是一个整数值,指示第一个 float 在数值上是大于、等于还是小于与之比较的 float。如果浮点值是 NaN,则认为它等于其他 NaN 值或大于所有其他浮点值。此外,浮点值 0.0f 大于浮点值-0.0f。
使用 compareTo()的替代方法是 compare()方法,它也是 Float 类的原生方法。compare()方法是在 Java 1.4 中引入的,它是一个静态方法,以与 compareTo()相同的方式比较两个浮点值。它只是让代码读起来有点不同。compare()方法的格式如下:
Float.compare(primitiveFloat1, primitiveFloat2)
所示的 compare()方法实际上将使用 compareTo()进行以下调用:
new Float(float1).compareTo(new Float(float2));
最后,使用 compareTo()或 compare()将返回相同的结果。
4-5.计算货币价值
问题
您正在开发一个需要使用货币值的应用,但您不确定使用哪种数据类型来存储和计算货币值。
解决方案 1
使用 BigDecimal 数据类型对货币值执行计算。使用 number format . getcurrency instance()helper 方法设置计算结果的格式。在下面的代码中,使用属于 BigDecimal 类的一些方法计算了三个货币值。然后,计算结果被转换为双精度值,并使用 NumberFormat 类进行格式化。首先,看看这些值是如何计算的:
BigDecimal currencyOne = new BigDecimal("25.65");
BigDecimal currencyTwo = new BigDecimal("187.32");
BigDecimal currencyThree = new BigDecimal("4.86");
BigDecimal result = null;
String printFormat = null;
// Add all three values
result = currencyOne.add(currencyTwo).add(currencyThree);
// Convert to double and send to formatDollars(), returning a String
printFormat = formatDollars(result.doubleValue());
System.out.println(printFormat);
// Subtract the first currency value from the second
result = currencyTwo.subtract(currencyOne);
printFormat = formatDollars(result.doubleValue());
System.out.println(printFormat);
接下来,让我们看看代码中使用的 formatDollars()方法。此方法接受双精度值,并使用基于美国地区的 NumberFormat 类对其执行格式化。然后,它返回一个表示货币的字符串值:
public static String formatDollars(double value){
NumberFormat dollarFormat = NumberFormat.getCurrencyInstance(Locale.US);
return dollarFormat.format(value);
}
正如您所看到的,NumberFormat 类允许根据指定的地区对货币进行格式化。如果您正在使用一个处理货币并具有国际范围的应用,这将非常方便。
$217.83
$161.67
解决方案 2
利用 Java Money API,这是 JSR 354 的重点,来执行货币计算。
注意
Java Money API 是在 jcp.org/en/jsr/deta… 的 JSR 354 下开发的。它最初是为了在 Java 9 中完成和包含。然而,JSR 完成得相当早,并且不包含对 Java 9 代码库的依赖。因此,Java Money API 也可以用于旧版本的 Java,比如 Java 8,它可以在 javamoney.github.io/[的 Github 上获得。](javamoney.github.io/)
下面的示例演示如何使用执行货币计算和格式设置。Java Money API。
MonetaryAmount amount1 = Money.of(25.65, Monetary.getCurrency("USD"));
MonetaryAmount amount2 = Money.of(187.32, Monetary.getCurrency("USD"));
MonetaryAmount amount3 = Money.of(4.86,Monetary.getCurrency("USD"));
MonetaryAmount result = null;
result = amount1.add(amount2).add(amount3);
MonetaryAmountFormat printFormat = MonetaryFormats.getAmountFormat(
AmountFormatQuery.of(Locale.US));
System.out.println("Sum of all: " + printFormat.format(result));
result = amount2.subtract(amount1);
System.out.println("Subtract amount1 from amount 2: " + printFormat.format(result));
它是如何工作的
许多人在处理货币时试图使用不同的数字格式。虽然可以使用任何类型的数字对象来处理货币,但是在 Java 5 语言中添加了 BigDecimal 类,以帮助满足处理货币值的需求。我们将首先解释如何利用 BigDecimal 进行货币计算,因为这是一个经典的过程,然后我们将看一看 Java Money API。
BigDecimal 类最有用的特性可能是它提供了对舍入的控制。这就是为什么这样的类对于处理货币值如此有用。BigDecimal 类为舍入值提供了一个简单的 API,也使转换成 double 值变得容易,正如这个配方的解决方案所展示的那样。
注意
使用 BigDecimal 处理货币值是一种很好的做法。但是,这可能会牺牲一些性能。根据应用和性能要求,如果性能成为问题,可能值得使用 Math.round()来实现基本的舍入。
若要使用 BigDecimal 类提供特定的舍入,应使用 MathContext 对象或 RoundingMode 枚举值。在这两种情况下,都可以通过使用货币格式化解决方案(如解决方案示例中演示的解决方案)来省略这种精度。BigDecimal 对象内置了数学实现,因此执行这样的操作很容易。表 4-5 中描述了您可以使用的算术运算。
表 4-5。大十进制算术方法
|方法
|
描述
| | --- | --- | | 添加() | 将一个 BigDecimal 对象值与另一个相加。 | | 减去() | 从另一个 BigDecimal 对象值中减去一个 BigDecimal 对象值。 | | 乘法() | 将一个 BigDecimal 对象的值乘以另一个。 | | abs() | 返回给定 BigDecimal 对象值的绝对值。 | | 功率 | 返回 BigDecimal 的 n 次幂;这个功率的计算精度是无限的。 |
执行完所需的计算后,对 BigInteger 对象调用 doubleValue()方法进行转换并获得双精度值。然后,您可以使用货币结果的 NumberFormat 类来格式化 double。
Java Money API 最初名为 JSR 354,旨在使 Java 语言中的货币操作更容易。该 API 为该语言带来了真正重大的变化,因为它最终允许人们以标准的方式对待货币,而不是以各种方式使用 BigDecimal。使用 Java Money API 的回报可能是巨大的,因为它可以使代码更容易阅读和理解,并提供货币结果,而不是必须强制转换为货币值的结果。
在解决方案 2 中,相同的货币值用于演示一些计算练习。API 中货币的标准类型是货币金额。在该解决方案中,您可以看到有三个 MonetaryAmount 对象,每个对象都使用 USD 货币表示不同的美元和美分值。为了获取存储在 MonetaryAmount 对象中的值,Money 实现类用于解析提供给它的值,然后返回指定货币类型的 MonetaryAmount 类型。Money 类使用 BigDecimal 存储数字值。
MonetaryAmount 接口提供了许多方法,可用于对存储值执行操作、与其他金额、精度等进行比较。具体来说,在解决方案中,您可以看到 add()方法接受另一个 MonetaryAmount,它用于将传入的值添加到原始 MonetaryAmount 中。另一个这样的方法是 subtract(),它从原始值中减去传递的值。
该解决方案还提供了有关格式化货币值的信息。MonetaryFormats 工厂可用于获取特定于所需地区的格式。然后,可以将生成的 MonetaryAmountFormat 模式应用于 MonetaryAmount,以相应地更改值的表示。
4-6.随机生成值
问题
您正在开发的应用需要使用随机生成的数字。
解决方案 1
使用 java.util.Random 类来帮助生成随机数。Random 类的开发目的是为一些 Java 数字数据类型生成随机数。这段代码演示了如何使用 Random 来生成这样的数字:
// Create a new instance of the Random class
Random random = new Random();
// Generates a random Integer
int myInt = random.nextInt();
// Generates a random Double value
double myDouble = random.nextDouble();
// Generates a random float
float myFloat = random.nextFloat();
// Generates a random Gaussian double
// mean 0.0 and standard deviation 1.0
// from this random number generator's sequence.
double gausDouble = random.nextGaussian();
// Generates a random Long
long myLong = random.nextLong();
// Generates a random boolean
boolean myBoolean = random.nextBoolean();
解决方案 2
利用 Math.random()方法。这将产生一个大于 0.0 但小于 1.0 的双精度值。下面的代码演示了此方法的用法:
double rand = Math.random();
它是如何工作的
java.util.Random 类使用 48 位种子来生成一系列伪随机值。正如您在这个配方的解决方案的例子中看到的,Random 类可以根据给定的种子生成许多不同类型的随机数值。默认情况下,种子是根据计算机处于活动状态的毫秒数的计算结果生成的。但是,可以使用 Random setSeed()方法手动设置种子。如果两个随机对象具有相同的种子,它们将产生相同的结果。
应该注意,在有些情况下,Random 类可能不是生成随机值的最佳选择。例如,如果您尝试使用 java.util.Random 的线程安全实例,那么在处理许多线程时,您可能会遇到性能问题。在这种情况下,您可以考虑使用 ThreadLocalRandom 类。要查看有关 ThreadLocalRandom 的更多信息,请参见位于docs . Oracle . com/javase/9/docs/API/Java/util/concurrent/threadlocalrrandom . html的文档。
类似地,如果您需要使用加密安全的随机对象,请考虑使用 secure Random。关于这个类的文档可以在docs . Oracle . com/javase/9/docs/API/Java/security/securerandom . html找到。
当您需要生成指定类型的随机值时,java.util.Random 类非常方便。它不仅易于使用,而且还为返回类型提供了广泛的选项。另一种简单的技术是使用 Math.random()方法,它产生一个介于 0.0 到 1.0 之间的 double 值,如解决方案 2 所示。这两种技术都提供了生成随机值的好方法。但是,如果需要生成特定类型的随机数,java.util.Random 是最佳选择。
4-7.获取不带时间的当前日期
问题
您正在开发一个应用,希望获得当前日期(不包括时间)并显示在表单上。
解决办法
利用日期-时间 API 获取当前日期。LocalDate 类表示年-月-日格式的 ISO 日历。下面几行代码捕获并显示当前日期:
LocalDate date = LocalDate.now();
System.out.println("Current Date:" + date);
它是如何工作的
日期-时间 API 使得获取当前日期变得容易,而不需要包括其他信息,比如时间。为此,导入 java.time.LocalTime 类并调用其 now()方法。LocalTime 类不能被实例化,因为它是不可变的和线程安全的。对 now()方法的调用返回另一个 LocalDate 对象,包含年-月-日格式的当前日期。
now()方法的另一个版本接受 java.time.Clock 对象作为参数,并根据该时钟返回日期。例如,下面几行代码演示了如何获取表示系统时间的时钟:
Clock clock = Clock.systemUTC();
LocalDate date = LocalDate.now(clock);
在以前的版本中,有其他方法可以获得当前日期,但通常时间是和日期一起出现的,然后必须进行格式化以删除不需要的时间数字。新的 java.time.LocalDate 类使得处理与时间无关的日期成为可能。
4-8.根据给定的日期条件获取日期对象
问题
您希望获得一个日期对象,给定一个年-月-日规格。
解决办法
为要获取对象的年、月和日调用 LocalDate.of()方法。例如,假设您想要获取 2000 年 11 月的指定日期的日期对象。您可以将该日期条件传递给 LocalDate.of()方法,如以下代码行所示:
LocalDate date = LocalDate.of(2000, Month.NOVEMBER, 11);
System.out.println("Date from specified date: " + date);
结果如下:
Date from specified date: 2000-11-11
它是如何工作的
LocalDate.of()方法接受三个值作为参数。这些参数代表年、月和日。year 参数始终被视为 int 值。month 参数可以表示为一个 int 值,它对应于一个表示月份的枚举。Month 枚举将返回每个月的 int 值,一月返回 1,十二月返回 12。因此,月。十一月返回 11。Month 对象也可以作为第二个参数传递,而不是作为 int 值。最后,通过将一个 int 值作为第三个参数传递给 of()方法来指定一个月中的第几天。
注意
有关月份枚举的更多信息,请参见位于download.java.net/jdk9/docs/api/java/time/Month.html的在线文档。
4-9.获取年-月-日日期组合
问题
您想要获取指定日期的年、年、月或月。
解决方案 1
要获取指定日期的年月,请使用 java.time.YearMonth 类。该类用于表示特定年份的月份。在以下代码行中,YearMonth 对象用于获取当前日期和另一个指定日期的年和月。
YearMonth yearMo = YearMonth.now();
System.out.println("Current Year and month:" + yearMo);
YearMonth specifiedDate = YearMonth.of(2000, Month.NOVEMBER);
System.out.println("Specified Year-Month: " + specifiedDate);
结果如下:
Current Year and month:2014-12
Specified Year-Month: 2000-11
解决方案 2
要获得当前日期或指定日期的月-日,只需使用 java.time.MonthDay 类。下面几行代码演示了如何获取月-日组合。
MonthDay monthDay = MonthDay.now();
System.out.println("Current month and day: " + monthDay);
MonthDay specifiedDate = MonthDay.of(Month.NOVEMBER, 11);
System.out.println("Specified Month-Day: " + specifiedDate);
结果如下:
Current month and day: --12-14
Specified Month-Day: --11-11
注意,默认情况下,MonthDay 不会返回非常有用的格式。有关格式化的更多帮助,请参见配方 4-17。
它是如何工作的
日期-时间 API 包括一些类,这些类使得获取应用需要的日期信息变得容易。其中两个是 YearMonth 和 MonthDay 类。YearMonth 类用于获取年月格式的日期。它包含了一些可以用来获得年月组合的方法。如解决方案中所示,您可以调用 now()方法来获取当前的年-月组合。与 LocalDate 类类似,YearMonth 也包含一个 of()方法,该方法接受 int 格式的年份和一个表示一年中月份的数字。在该解决方案中,Month 枚举用于获取月份值。
与 YearMonth 类类似,MonthDay 以月-日格式获取日期。它还包含一些不同的方法来获得月-日组合。解决方案 2 演示了两种这样的技术:通过调用 now()方法获得当前的月-日组合,并使用 of()方法获得指定日期的月-日组合。of()方法接受一个整数值作为第一个参数,第二个参数接受一个整数值作为第几天。
4-10.基于当前时间获取和计算时间
问题
您希望获得当前时间,以便可以用它来标记给定的记录。您还想基于该时间执行计算。
解决办法
使用 LocalTime 类来获取和显示当前时间,该类是新的日期时间 API 的一部分。在以下代码行中,演示了 LocalTime 类。
LocalTime time = LocalTime.now();
System.out.println("Current Time: " + time);
一旦获得了时间,就可以针对 LocalTime 实例调用方法来获得所需的结果。在以下代码行中,有一些使用 LocalTime 方法的示例:
// atDate(LocalDate): obtain the local date and time
LocalDateTime ldt = time.atDate(LocalDate.of(2011,Month.NOVEMBER,11));
System.out.println("Local Date Time object: " + ldt);
// of(int hours, int min): obtain a specific time
LocalTime pastTime = LocalTime.of(1, 10);
// compareTo(LocalTime): compare two times. Positive
// return value returned if greater
System.out.println("Comparing times: " + time.compareTo(pastTime));
// getHour(): return hour in int value (24-hour format)
int hour = time.getHour();
System.out.println("Hour: " + hour);
// isAfter(LocalTime): return Boolean comparison
System.out.println("Is local time after pastTime? " + time.isAfter(pastTime));
// minusHours(int): Subtract Hours from LocalTime
LocalTime minusHrs = time.minusHours(5);
System.out.println("Time minus 5 hours: " + minusHrs);
// plusMinutes(int): Add minutes to LocalTime
LocalTime plusMins = time.plusMinutes(30);
System.out.println("Time plus 30 mins: " + plusMins);
结果如下:
Current Time: 22:21:08.419
Local Date Time object: 2011-11-11T22:21:08.419
Comparing times: 1
Hour: 22
Is local time after pastTime? true
Time minus 5 hours: 17:21:08.419
Time plus 30 mins: 22:51:08.419
它是如何工作的
有时需要获得当前系统时间。LocalTime 类可用于通过调用其 now()方法来获取当前时间。与 LocalDate 类类似,可以调用 LocalTime.now()方法来返回等于当前时间的 LocalTime 对象。LocalTime 类还包含几个可以用来操作时间的方法。解决方案中包含的示例提供了可用方法的简要概述。
让我们看一些例子,为如何调用 LocalTime 方法提供一些上下文。若要获取设置为特定时间的 LocalTime 对象,请调用 LocalTime.of(int,int)方法,传递表示小时和分钟的 int 参数。
// of(int hours, int min): obtain a specific time
LocalTime pastTime = LocalTime.of(1, 10);
atDate(LocalDate)实例方法用于将 LocalDate 对象应用于 LocalTime 实例,返回 LocalDateTime 对象(更多信息,请参见配方 4-11)。
LocalDateTime ldt = time.atDate(LocalDate.of(2011,Month.NOVEMBER,11));
有几种方法可以用来获得时间的部分。例如,getHour()、getMinute()、getNano()和 getSecond()方法可用于返回 LocalTime 对象的那些指定部分。
int hour = time.getHour();
int min = time.getMinute();
int nano = time.getNano();
int sec = time.getSecond();
也有几种比较方法可供使用。例如,compareTo(LocalTime)方法可用于将一个 LocalTime 对象与另一个进行比较。isAfter(LocalTime)可用于确定时间是否在另一个之后,isBefore(LocalTime)用于指定相反的时间。如果需要计算,有几种方法可用,包括:
-
减号(长时间总量,时间单位)
-
减去(临时金额)
-
小时数(长)
-
分钟(长)
-
小写(长)
-
毫秒(长型)
-
加(long amountToAdd,temporalunit unit)
-
加号(临时金额)
-
plusHours(长)
-
多分钟(长)
-
纳秒(长)
-
plusSeconds(长)
要查看 LocalTime 类中包含的所有方法,请参见位于docs . Oracle . com/javase/9/docs/API/Java/time/local time . html的在线文档。
4-11.获取并一起使用日期和时间
问题
在您的应用中,您不仅希望显示当前日期,还希望显示当前时间。
解决方案 1
利用 LocalDateTime 类来捕获和显示当前日期和时间,该类是新的日期时间 API 的一部分。LocalDateTime 类包含一个名为 now()的方法,该方法可用于同时获取当前日期和时间。下面几行代码演示了如何做到这一点:
LocalDateTime ldt = LocalDateTime.now();
System.out.println("Local Date and Time: " + ldt);
结果 LocalDateTime 对象包含日期和时间,但不包含时区信息。LocalDateTime 类还包含其他方法,这些方法提供了处理日期时间数据的选项。例如,若要返回具有指定日期和时间的 LocalDateTime 对象,请将 int 类型的参数传递给 LocalDateTime.of()方法,如下所示:
// Obtain the LocalDateTime object of the date 11/11/2000 at 12:00
LocalDateTime ldt2 = LocalDateTime.of(2000, Month.NOVEMBER, 11, 12, 00);
以下示例演示了 LocalDateTime 对象中可用的一些方法:
// Obtain the month from LocalDateTime object
Month month = ldt.getMonth();
int monthValue = ldt.getMonthValue();
System.out.println("Month: " + month);
System.out.println("Month Value: " + monthValue);
// Obtain day of Month, Week, and Year
int day = ldt.getDayOfMonth();
DayOfWeek dayWeek = ldt.getDayOfWeek();
int dayOfYr = ldt.getDayOfYear();
System.out.println("Day: " + day);
System.out.println("Day Of Week: " + dayWeek);
System.out.println("Day of Year: " + dayOfYr);
// Obtain year
int year = ldt.getYear();
System.out.println("Date: " + monthValue + "/" + day + "/" + year);
int hour = ldt.getHour();
int minute = ldt.getMinute();
int second = ldt.getSecond();
System.out.println("Current Time: " + hour + ":" + minute + ":" + second);
// Calculation of Months, etc.
LocalDateTime currMinusMonths = ldt.minusMonths(12);
LocalDateTime currMinusHours = ldt.minusHours(10);
LocalDateTime currPlusDays = ldt.plusDays(30);
System.out.println("Current Date and Time Minus 12 Months: " + currMinusMonths);
System.out.println("Current Date and Time MInus 10 Hours: " + currMinusHours);
System.out.println("Current Date and Time Plus 30 Days:" + currPlusDays);
结果如下:
Day: 28
Day Of Week: SATURDAY
Day of Year: 332
Date: 11/28/2015
Current Time: 10:23:8
Current Date and Time Minus 12 Months: 2014-11-28T10:23:08.399
Current Date and Time MInus 10 Hours: 2015-11-28T00:23:08.399
Current Date and Time Plus 30 Days:2015-12-28T10:23:08.399
解决方案 2
如果只需要获得当前日期而不需要进入日历细节,请使用 java.util.date 类来生成一个新的 Date 对象。这样做将生成一个等于当前系统日期的新日期对象。在下面的代码中,您可以看到创建一个新的 Date 对象并获取当前日期是多么容易:
Date date = new Date();
System.out.println("Using java.util.Date(): " + date);
System.out.println("Getting time from java.util.Date(): " + date.getTime());
结果将是一个 Date 对象,它包含从运行代码的系统中获取的当前日期和时间,包括时区信息,如下面的清单所示。时间是自 1970 年 1 月 1 日 00:00:00 GMT 以来的毫秒数。
Using java.util.Date(): Sat Nov 28 10:23:08 CST 2015
Getting time from java.util.Date(): 1448727788454
解决方案 3
如果需要更精确的日历,可以使用 java.util.Calendar 类。虽然使用 Calendar 类会使您的代码更长,但结果比使用 java.util.Date 更精确。以下代码演示了使用该类获取当前日期的一小部分功能:
Calendar gCal = Calendar.getInstance();
// Month is based upon a zero index, January is equal to 0,
// so we need to add one to the month for it to be in
// a standard format
int month = gCal.get(Calendar.MONTH) + 1;int day = gCal.get(Calendar.DATE);
int yr = gCal.get(Calendar.YEAR);
String dateStr = month + "/" + day + "/" + yr;
System.out.println(dateStr);
int dayOfWeek = gCal.get(Calendar.DAY_OF_WEEK);
// Print out the integer value for the day of the week
System.out.println(dayOfWeek);
int hour = gCal.get(Calendar.HOUR);
int min = gCal.get(Calendar.MINUTE);
int sec = gCal.get(Calendar.SECOND);
// Print out the time
System.out.println(hour + ":" + min + ":" + sec);
// Create new DateFormatSymbols instance to obtain the String
// value for dates
DateFormatSymbols symbols = new DateFormatSymbols();
String[] days = symbols.getWeekdays();
System.out.println(days[dayOfWeek]);
// Get crazy with the date!
int dayOfYear = gCal.get(Calendar.DAY_OF_YEAR);
System.out.println(dayOfYear);
// Print the number of days left in the year
System.out.println("Days left in " + yr + ": " + (365-dayOfYear));
int week = gCal.get(Calendar.WEEK_OF_YEAR);
// Print the week of the year
System.out.println(week);
如这段代码所示,使用 Calendar 类时,可以获得关于当前日期的更详细的信息。运行代码的结果将如下所示:
11/28/2015
7
10:28:26
Saturday
332
Days left in 2015: 33
48
注意
尽管 java.util.Calendar 为获取精确的日期/时间信息提供了一种健壮的技术,但是从 java 8 开始,首选的解决方案是利用 Java 日期-时间 API。
它是如何工作的
许多应用需要使用当前日历日期。通常还需要获得当前时间。有不同的方法可以做到这一点,这个食谱的解决方案展示了其中的三种。Date-Time API 包括一个 LocalDateTime 类,它使您能够通过调用其 now()方法来捕获当前日期和时间。调用 LocalDateTime.of()时,可以通过指定相应的 int 和 Month 类型参数来获取指定的日期和时间。还有许多方法可以通过 LocalDateTime 实例使用,例如 getHours()、getMinutes()、getNanos()和 getSeconds(),这些方法允许对日期和时间进行更细粒度的控制。LocalDateTime 的实例还包含用于执行计算、转换、比较等操作的方法。为了简洁起见,这里没有列出所有的方法,但是提供了进一步的信息;请参考位于docs . Oracle . com/javase/9/docs/API/Java/time/local datetime . html的在线文档。这个菜谱的解决方案 1 演示了 LocalDateTime 的使用,展示了如何执行计算并获取日期和时间的一部分以供将来使用。
默认情况下,java.util.Date 类可以不带任何参数进行实例化,以返回当前日期和时间。Date 类也可以通过 getTime()方法返回一天中的当前时间。如解决方案中所述,getTime()方法返回自 1970 年 1 月 1 日 00:00:00 GMT 以来的毫秒数,由正在使用的 Date 对象表示。关于将当前日期和时间分解成更细粒度的时间间隔,还有其他几种方法可以针对 Date 对象调用。例如,Date 类有方法 getHours()、getMinutes()、getSeconds()、getMonth()、getDay()、getTimezoneOffset()和 getYear()。但是,不建议使用除 getTime()以外的任何方法,因为 java.time.LocalDateTime 和 java.util.Calendar get()方法都不赞成使用这些方法。当一个方法或类被弃用时,这意味着它不应该再被使用,因为它可能会在 Java 语言的未来版本中被删除。但是,Date 类中包含的一些方法还没有被标记为不推荐使用,所以 Date 类很可能会包含在 Java 的未来版本中。保持不变的方法包括 after()、before()、compareTo()、setTime()和 equals()等比较方法。这个菜谱的解决方案 2 演示了如何实例化一个 Date 对象并打印出当前的日期和时间。
如前所述,Date 类有许多方法已经过时,不应再使用。在这个方法的解决方案 3 中,java.util.Calendar 类被演示为获取大部分信息的后继类。Calendar 类是在 JDK 1.1 中引入的,当时许多 Date 方法都被弃用了。从解决方案 3 中可以看出,Calendar 类包含了 Date 类中包含的所有相同的功能,只是 Calendar 类更加灵活。Calendar 类实际上是一个包含方法的类,这些方法用于在特定时间和日期之间进行转换,并以各种方式操作日历。解决方案 3 中演示的 Calendar 就是这样一个类,它扩展了 Calendar 类,因此提供了这种功能。Calendar 类在 Java 8 中获得了一些新方法。表 4-6 中列出了 java.util.Calendar 中的新方法。
表 4-6。Java 8 中 java.util.Calendar 的新方法
|方法名
|
描述
| | --- | --- | | getAvailableCalendarTypes() | 返回包含所有支持的日历类型的不可修改的集合。 | | getCalendarType() | 返回此日历的日历类型。 | | t 常量() | 转化为瞬间。 |
对于某些应用,Date 类可以很好地工作。例如,在处理时间戳时,Date 类会很有用。但是,如果应用需要日期和时间的详细操作,那么建议使用 LocalDateTime 或 Calendar 类,这两个类都包括 Date 类的所有功能以及更多特性。这个配方的所有解决方案在技术上都是合理的;选择最适合您的应用需求的一个。
4-12.获取机器时间戳
问题
您需要从系统获得一个基于机器的时间戳。
解决办法
利用一个 Instant 类,它表示基于机器时间的时间线上一纳秒的开始。在下面的示例中,使用了一个 Instant 来获取系统时间戳。在其他场景中也会用到该瞬间,例如在基于该瞬间计算不同日期时。
public static void instants(){
Instant timestamp = Instant.now();
System.out.println("The current timestamp: " + timestamp);
//Now minus three days
Instant minusThree = timestamp.minus(3, ChronoUnit.DAYS);
System.out.println("Now minus three days:" + minusThree);
ZonedDateTime atZone = timestamp.atZone(ZoneId.of("GMT"));
System.out.println(atZone);
Instant yesterday = Instant.now().minus(24, ChronoUnit.HOURS);
System.out.println("Yesterday: " + yesterday);
}
结果如下:
The current timestamp: 2015-11-28T16:21:42.197Z
Now minus three days:2015-11-25T16:21:42.197Z
2015-11-28T16:21:42.197Z[GMT]
Yesterday: 2015-11-27T16:21:42.273Z
它是如何工作的
Date-Time API 引入了一个名为 Instant 的新类,它表示基于机器的时间中时间轴上一纳秒的开始。基于机器时间,瞬间的值从纪元(1970 年 1 月 1 日 00:00:00Z)开始计数。纪元前的任何值都是负的,纪元后的值都是正的。Instant 类非常适合于获取机器时间戳,因为它包含所有相关的日期和时间信息,精确到纳秒。
Instant 类是静态且不可变的,因此要获得当前时间戳,可以调用 now()方法。这样做将返回当前瞬间的副本。Instant 还包括转换和计算方法,每个方法都返回 Instant 或其他类型的副本。在这个解决方案中,now()方法返回当前的时间戳,然后是几个示例,展示如何执行计算和获取即时信息。
Instant 是 Java 8 中一个重要的新特性,因为它使得处理当前时间和日期数据变得更加容易。其他日期和时间类,如 LocalDateTime,也很有用。然而,瞬间是最准确的时间戳,因为它是基于纳秒精度的。
4-13.基于时区转换日期和时间
问题
您正在开发的应用有可能在全世界得到应用。在应用的某些区域,需要显示静态日期和时间,而不是系统日期和时间。在这种情况下,需要对这些静态日期和时间进行转换,以适应应用用户当前所在的特定时区。
解决办法
日期-时间 API 通过时区和偏移类提供了处理时区数据的适当工具。在下面的场景中,假设应用正在处理租赁车辆的预订。你可以在一个时区租车,然后在另一个时区还车。下面几行代码演示了如何在这种情况下打印出个人的预订。以下名为 scheduleReport 的方法接受表示签入和签出日期/时间的 LocalDateTime 对象,以及每个对象的 ZoneIds。航空公司可以使用这种方法打印特定航班的时区信息。
public static void scheduleReport(LocalDateTime checkOut, ZoneId checkOutZone,
LocalDateTime checkIn, ZoneId checkInZone){
ZonedDateTime beginTrip = ZonedDateTime.of(checkOut, checkOutZone);
System.out.println("Trip Begins: " + beginTrip);
// Get the rules of the check out time zone
ZoneRules checkOutZoneRules = checkOutZone.getRules();
System.out.println("Checkout Time Zone Rules: " + checkOutZoneRules);
//If the trip took 4 days
ZonedDateTime beginPlus = beginTrip.plusDays(4);
System.out.println("Four Days Later: " + beginPlus);
// End of trip in starting time zone
ZonedDateTime endTripOriginalZone = ZonedDateTime.of(checkIn, checkOutZone);
ZonedDateTime endTrip = ZonedDateTime.of(checkIn, checkInZone);
int diff = endTripOriginalZone.compareTo(endTrip);
String diffStr = (diff >= 0) ? "NO":"YES";
System.out.println("End trip date/time in original zone: " + endTripOriginalZone);
System.out.println("End trip date/time in check-in zone: " + endTrip );
System.out.println("Original Zone Time is less than new zone time? " +
diffStr );
ZoneId checkOutZoneId = beginTrip.getZone();
ZoneOffset checkOutOffset = beginTrip.getOffset();
ZoneId checkInZoneId = endTrip.getZone();
ZoneOffset checkInOffset = endTrip.getOffset();
System.out.println("Check out zone and offset: " + checkOutZoneId + checkOutOffset);
System.out.println("Check in zone and offset: " + checkInZoneId + checkInOffset);
}
结果如下:
Trip Begins: 2015-12-13T13:00-05:00[US/Eastern]
Checkout Time Zone Rules: ZoneRules[currentStandardOffset=-05:00]
Four Days Later: 2015-12-17T13:00-05:00[US/Eastern]
End trip date/time in original zone: 2015-12-18T10:00-05:00[US/Eastern]
End trip date/time in check-in zone: 2015-12-18T10:00-07:00[US/Mountain]
Original Zone Time is less than new zone time? YES
Check out zone and offset: US/Eastern-05:00
Check in zone and offset: US/Mountain-07:00
它是如何工作的
时区给开发人员增加了另一个挑战,Java 日期时间 API 为使用时区提供了一个简单的方面。日期-时间 API 包括一个 java.time.zone 包,其中包含许多有助于处理时区数据的类。这些类为时区规则、数据以及本地时间线中的间隙和重叠提供支持,这些间隙和重叠通常是夏令时转换的结果。表 4-7 中列出了组成区域包的类别。
表 4-7。时区类别
|类别名
|
描述
| | --- | --- | | ZoneId | 指定区域标识符并用于转换。 | | ZoneOffset(区域偏移) | 指定格林威治/UTC 时间的时区偏移量。 | | ZonedDateTime | 一个 date-time 对象,它还处理时区数据,该数据具有格林威治/UTC 时间的时区偏移量。 | | 区域规则 | 定义特定时区的时区偏移量如何变化的规则。 | | ZoneRulesProvider | 向特定系统提供时区规则。 | | 区域偏移转换 | 由局部时间线中的不连续性引起的两个偏移之间的转换。 | | ZoneOffsetTransitionRule | 表达如何创建过渡的规则。 |
从最基本的时区类 ZoneId 开始,每个时区都包含一个特定的时区标识符。此标识符对于将特定时区分配给日期时间非常有用。在该解决方案中,ZoneId 用于计算两个时区之间的任何差异。ZoneId 标识了基于特定偏移量的固定或基于地理区域的转换应使用的规则。有关 ZoneId 的更多详细信息,请参见位于docs . Oracle . com/javase/9/docs/API/Java/time/zoned datetime . html的文档。
ZonedDateTime 是一个不可变的类,用于同时处理日期-时间和时区数据。这个类表示一个包含 ZoneId 的对象,非常类似于 LocalDateTime。它可以用来表示日期的所有方面,包括年、月、日、小时、分钟、秒、毫微秒和时区。该类包含一组用于执行计算、转换等的方法。为了简洁起见,这里没有列出 ZonedDateTime 中包含的方法,但是您可以在位于docs . Oracle . com/javase/9/docs/API/Java/time/zoned datetime . html的文档中了解它们。
ZoneOffset 指定格林威治/UTC 时间的时区偏移量。您可以通过调用 ZonedDateTime.getOffset()方法来查找特定时区的偏移量。ZoneOffset 类包含一些方法,这些方法可以很容易地将偏移量分解为不同的时间单位。例如,getTotalSeconds()方法返回小时、分钟和秒字段的总和,作为可以添加到时间中的单个偏移量。有关更多信息,请参考位于docs . Oracle . com/javase/9/docs/API/Java/time/zone offset . html的在线文档。
可以定义许多规则来确定单个时区的时区偏移量如何变化。ZoneRules 类用于为区域定义这些规则。例如,可以调用 ZoneRules 来指定或确定夏令时是否是一个因素。还可以将 Instant 或 LocalDateTime 传递给 getOffset()和 getTransition()等 ZoneRules 方法,以返回 ZoneOffset 或 ZoneOffsetTransition。有关 ZoneRules 的更多信息,请参考位于docs . Oracle . com/javase/9/docs/API/Java/time/zone/zone rules . html的在线文档。
另一个经常使用的时区类是 ZoneOffsetTransition。这个类模拟了由于夏令时的变化而导致的春季和秋季偏移量之间的转换。它用于确定转场之间是否有间隙,获取转场的持续时间,等等。有关 ZoneOffsetTransition 的更多信息,请参见位于docs . Oracle . com/javase/9/docs/API/Java/time/zone/ZoneOffsetTransition . html的在线文档。
ZoneRulesProvider、ZoneOffsetTransitionRule 和其他类通常不像其他类那样经常用于处理日期和时区。这些类对于管理时区规则和转换的配置非常有用。
注意
java.time.zone 包中的类非常重要,因为每个类都可以调用许多方法。这个食谱提供了入门指南,只有时区使用的基本知识。有关更多详细信息,请参见联机文档。
4-14.比较两个日期
问题
您想要确定一个日期是否大于另一个日期。
解决办法
利用日期-时间 API 类中的 compareTo()方法之一。在下面的解决方案中,比较了两个 LocalDate 对象,并显示了相应的消息。
public static void compareDates(LocalDate ldt1,
LocalDate ldt2) {
int comparison = ldt1.compareTo(ldt2);
if (comparison > 0) {
System.out.println(ldt1 + " is larger than " + ldt2);
} else if (comparison < 0) {
System.out.println(ldt1 + " is smaller than " + ldt2);
} else {
System.out.println(ldt1 + " is equal to " + ldt2);
}
}
同样,在进行日期比较时,也有一些方便的方法。具体来说,isAfter()、isBefore()和 isEqual()方法可以像 compareTo()一样用于比较,如下面的清单所示。
public static void compareDates2(LocalDate ldt1, LocalDate ldt2){
if(ldt1.isAfter(ldt2)){
System.out.println(ldt1 + " is after " + ldt2);
} else if (ldt1.isBefore(ldt2)){
System.out.println(ldt1 + " is before " + ldt2);
} else if (ldt1.isEqual(ldt2)){
System.out.println(ldt1 + " is equal to " + ldt2);
}
}
它是如何工作的
许多日期-时间 API 类都包含一个方法,用于比较两个不同的日期-时间对象。在本示例的解决方案中,LocalDate.compareTo()方法用于确定一个 LocalDate 对象是否大于另一个。如果第一个 LocalDate 大于第二个 LocalDate,则 compareTo()方法返回一个负 int 值,如果两者相等,则返回零,如果第二个 local date 大于第一个 local date,则返回一个正数。
每个包含 compareTo()的日期时间类都有相同的结果。也就是说,返回一个 int 值,指示第一个对象是大于、小于还是等于第二个对象。下面列出了包含 compareTo()方法的每个类:
-
持续时间
-
局部日期
-
LocalDateTime
-
LocalTime(本地时间)
-
瞬间
-
蒙特达伊
-
OffsetDateTime
-
偏移时间
-
年
-
年月
-
ZoneOffset(区域偏移)
如第二个清单所示,isAfter()、isBefore()和 isEqual()方法也可以用于比较。这些方法返回一个布尔值来指示比较结果。虽然这些方法的结果可以像 compareTo()一样用于执行日期比较,但是它们可以使代码更容易阅读。
4-15.寻找日期和时间之间的间隔
问题
您需要确定两个日期或时间之间过去了多少小时、几天、几周、几个月或几年。
解决方案 1
利用日期-时间 API 来确定两个日期之间的差异。具体来说,使用 Period 类来确定两个日期之间的时间段(以天为单位)。下面的示例演示如何获取两个日期之间的天数、月数和年数的间隔。
注意
此示例显示日、月和年的差异,但不显示两个日期之间的累计日或月。要确定两个日期之间的总累积天数、月数和年数,请继续阅读解决方案#2 和#3。
LocalDate anniversary = LocalDate.of(2000, Month.NOVEMBER, 11);
LocalDate today = LocalDate.now();
Period period = Period.between(anniversary, today);
System.out.println("Number of Days Difference: " + period.getDays());
System.out.println("Number of Months Difference: " + period.getMonths());
System.out.println("Number of Years Difference: " + period.getYears());
结果如下:
Number of Days Difference: 16
Number of Months Difference: 1
Number of Years Difference: 13
解决方案 2
使用 java.util.concurrent.TimeUnit 枚举在给定日期之间执行计算。使用此枚举,可以获得天、小时、微秒、毫秒、分钟、纳秒和秒的整数值。这样做将允许您执行必要的计算。
// Obtain two instances of the Calendar class
Calendar cal1 = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();
// Set the date to 01/01/2010:12:00
cal2.set(2010,0,1,12,0);
Date date1 = cal2.getTime();
System.out.println(date1);
long mill = Math.abs(cal1.getTimeInMillis() - date1.getTime());
// Convert to hours
long hours = TimeUnit.MILLISECONDS.toHours(mill);
// Convert to days
Long days = TimeUnit.HOURS.toDays(hours);
String diff = String.format("%d hour(s) %d min(s)", hours,
TimeUnit.MILLISECONDS.toMinutes(mill) - TimeUnit.HOURS.toMinutes(hours));
System.out.println(diff);
diff = String.format("%d days", days);
System.out.println(diff);
// Divide the number of days by seven for the weeks
int weeks = days.intValue()/7;
diff = String.format("%d weeks", weeks);
System.out.println(diff);
这段代码的输出将被格式化,以显示指示当前日期和所创建的 date 对象之间的差异的文本字符串。
解决方案 3
若要确定以天、月、年或其他时间单位表示的总累积差异,请使用 ChronoUnit 类。下面的代码演示了如何利用 ChronoUnit 类来确定两个日期之间的天数和年数。
LocalDate anniversary = LocalDate.of(2000, Month.NOVEMBER, 11);
LocalDate today = LocalDate.now();
long yearsBetween = ChronoUnit.YEARS.between(anniversary, today);
System.out.println("Years between dates: " + yearsBetween);
long daysBetween = ChronoUnit.DAYS.between(anniversary, today);
System.out.println("Days between dates:" + daysBetween);
结果如下:
Years between dates: 13
Days between dates:4794
它是如何工作的
与大多数编程技术一样,用 Java 执行日期计算有多种方法。Java 8 中引入的日期-时间 API 包括一些用于确定时间间隔的新技术。Period 类用于确定指定对象的两个单位之间的差异周期。若要获取两个日期时间对象之间的时间段,请调用 Period.between()方法,传递两个要获取时间段的日期时间对象。周期有许多方法可以用来将间隔分解成不同的单位。例如,可以使用 getDays()方法获得两个日期-时间对象的周期天数。同样,可以调用 getMonths()和 getYears()方法来返回周期中的月数或年数。
日期-时间 API 还包括一个 ChronoUnit Enum,可用于 ISO 以外的日历系统,提供基于单位的访问来操作日期和时间。枚举中的每个单位值都包含许多用于执行操作的方法。其中一个方法是 between(),它只返回两个给定日期时间对象之间的指定时间单位。在解决方案中,它用于使用 ChronoUnit 返回年和日。YEARS.between()和计时单位。DAYS.between(),分别为。
最有用的技术之一是根据给定日期的时间(以毫秒为单位)执行计算。这提供了最精确的计算,因为它以非常小的时间间隔工作:毫秒。通过对 Calendar 对象调用 getTimeInMillis()方法,可以从该对象中获取以毫秒为单位的当前时间。同样,Date 对象将通过调用 getTime()方法返回以毫秒表示的值。正如您在这个食谱的解答中看到的,执行的第一个数学运算是给定日期之间的差值(以毫秒为单位)。获得该值,然后取其绝对值,将提供执行日期计算所需的基础。为了获得数字的绝对值,请使用 java.lang.Math 类中包含的 abs()方法,如以下代码行所示:
long mill = Math.abs(cal1\. getTimeInMillis() - date1.getTime());
绝对值将以长格式返回。可以使用 TimeUnit 枚举来获得不同的日期转换。它包含许多表示不同时间间隔的静态枚举常量值,类似于 Calendar 对象的那些值。这些值显示在这里。
注意
一个枚举类型是其字段由一组固定的常数值组成的类型。Java 语言在 1.5 版中欢迎枚举类型。
-
天
-
小时
-
微秒
-
毫秒
-
分钟
-
纳秒
-
秒
这些值本身就说明了它们所代表的转换间隔。通过对这些枚举调用转换方法,可以转换表示两个日期之间持续时间的长值。正如您在这个配方的解决方案中看到的,首先使用 enum 建立时间单位,然后对该时间单位进行转换调用。以下面的转换为例。第一,time unit 的时间单位。毫秒是成立的。其次,对其调用 toHours()方法,并将由 mill 字段表示的 long 值作为参数传递:
TimeUnit.MILLISECONDS.toHours(mill)
这段代码可以用英文翻译如下:“field mill 的内容用毫秒表示;将这些内容转换成小时。”该调用的结果将是将工厂字段中的值转换为小时。通过堆叠对 TimeUnit 的调用,可以进行更精确的转换。例如,以下代码将 mill 字段的内容转换为小时,然后转换为天:
TimeUnit.HOURS.toDays(TimeUnit.MILLISECONDS.toHours(mill))
同样,英文翻译可以读作,“场磨的内容以毫秒表示。将这些内容转换成小时。接下来,将这些小时转换成天数。”
TimeUnit 可以使时间间隔转换非常精确。将时间单位转换的精度与数学相结合,将允许您将两个日期的差异转换为任何时间间隔。
4-16.从指定的字符串中获取日期时间
问题
你想把一个字符串解析成一个日期时间对象。
解决办法
利用时态日期时间类的 parse()方法来解析使用预定义或自定义格式的字符串。下面几行代码演示了如何使用 parse()方法的变体将字符串解析为 date 或 date-time 对象。
// Parse a String to form a Date-Time object
LocalDate ld = LocalDate.parse("2014-12-28");
LocalDateTime ldt = LocalDateTime.parse("2014-12-28T08:44:00");
System.out.println("Parsed Date: " + ld);
System.out.println("Parsed Date-Time: " + ldt);
// Using a different Parser
LocalDate ld2 = LocalDate.parse("2014-12-28", DateTimeFormatter.ISO_DATE);
System.out.println("Different Parser: " + ld2);
// Custom Parser
String input = "12/28/2013";
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");
LocalDate ld3 = LocalDate.parse(input, formatter);
System.out.println("Custom Parsed Date: " + ld3);
} catch (DateTimeParseException ex){
System.out.println("Not parsable: " + ex);
}
结果如下:
Parsed Date: 2014-12-28
Parsed Date-Time: 2014-12-28T08:44
Different Parser: 2014-12-28
Custom Parsed Date: 2014-12-28
它是如何工作的
Date-Time API 的时态类包括一个 parse()方法,该方法可用于使用指定的格式解析给定的输入字符串。默认情况下,parse()方法将根据目标对象的默认 DateTimeFormatter 进行格式化。例如,要解析字符串“2014-01-01”,可以调用默认的 LocalDate.parse()方法。
LocalDate date = LocalDate.parse("2014-01-01");
但是,可以将另一个 DateTimeFormatter 指定为 parse()方法的第二个参数。DateTimeFormatter 是用于格式化和打印日期和时间的最终类。它包含许多内置的格式化程序,可以指定这些格式化程序将字符串强制转换为日期-时间对象。例如,要基于不带偏移量的标准 ISO_DATE 格式进行解析,请调用 DateTimeFormatter。ISO_DATE,如该配方的解决方案中所示。有关 DateTimeFormatter 的更多信息,请参见位于docs . Oracle . com/javase/9/docs/API/Java/time/format/datetime formatter . html的在线文档。
通常,需要将文本字符串解析成日期-时间对象。许多核心日期-时间类都内置了 parse()方法,这使得这些任务变得很容易。
4-17.格式化日期以供显示
问题
您的应用需要使用特定的格式显示日期。您希望一次性定义该格式,并将其应用于所有需要显示的日期。
解决方案 1
利用 DateTimeFormatter 类(日期-时间 API 的一部分)根据您想要使用的模式来格式化日期和时间。DateTimeFormatter 类包含一个 ofPattern()方法,该方法接受一个字符串模式参数来指定所需的模式。每个时态日期时间类都包含一个 format()方法,该方法接受 DateTimeFormatter 并返回目标日期时间对象的基于字符串的格式。在以下代码行中,演示了 DateTimeFormatter:
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("MMMM dd yyyy");
LocalDateTime now = LocalDateTime.now();
String output = now.format(dateFormatter);
System.out.println(output);
DateTimeFormatter dateFormatter2 = DateTimeFormatter.ofPattern("MM/dd/YY HH:mm:ss");
String output2 = now.format(dateFormatter2);
System.out.println(output2);
DateTimeFormatter dateFormatter3 = DateTimeFormatter.ofPattern("hh 'o''clock' a, zzzz");
ZonedDateTime zdt = ZonedDateTime.now();
String output3 = zdt.format(dateFormatter3);
System.out.println(output3);
结果如下:
December 28 2013
12/28/13 10:44:06
10 o'clock AM, Central Standard Time
解决方案 2
使用 java.util.Calendar 类获取所需的日期,然后使用 java.text.SimpleDateFormat 类格式化该日期。下面的示例演示 SimpleDateFormat 类的用法:
// Create new calendar
Calendar cal = Calendar.getInstance();
// Create instance of SimpleDateFormat class using pattern
SimpleDateFormat dateFormatter1 = new SimpleDateFormat("MMMMM dd yyyy");
String result = null;
result = dateFormatter1.format(cal.getTime());
System.out.println(result);
dateFormatter1.applyPattern("MM/dd/YY hh:mm:ss");
result = dateFormatter1.format(cal.getTime());
System.out.println(result);
dateFormatter1.applyPattern("hh 'o''clock' a, zzzz");
result = dateFormatter1.format(cal.getTime());
System.out.println(result);
运行此示例将产生以下结果:
June 22 2011
06/22/11 06:24:41
06 o'clock AM, Central Daylight Time
从结果中可以看出,DateTimeFormatter 和 SimpleDateFormat 类使得将日期转换成任何格式都很容易。
它是如何工作的
对于任何程序来说,日期格式都是一个常见的问题。人们喜欢在不同的情况下以特定的格式看到他们的约会。Java 语言包含几个方便的实用程序,用于正确格式化日期时间数据。具体来说,较新的 API 包括 DateTimeFormatter 类,Java SE 的早期版本包括 SimpleDateFormat 类,它们中的每一个都可以方便地执行格式化过程。
DateTimeFormatter 类是最后一个类,主要用于打印和格式化日期时间对象。若要获取可应用于对象的 DateTimeFormatter,请调用 DateTimeFormatter.ofPattern()方法,传递表示所需输出的基于字符串的模式。表 4-8 列出了可在基于字符串的模式中使用的不同模式字符。然后,通过调用对象的 format()方法并将 DateTimeFormatter 作为参数传递,可以将生成的 DateTimeFormatter 应用于任何时态日期时间对象。结果将是根据指定模板模式格式化的日期时间对象。
表 4-8。模式字符
|性格;角色;字母
|
描述
| | --- | --- | | G | 时代 | | y | 年 | | Y | 周年 | | M | 一年中的月份 | | w | 一年中的周 | | W | 月中的周 | | D | 一年中的每一天 | | d | 一个月中的第几天 | | F | 一个月中的星期几 | | E | 星期几的名称 | | u | 一周中的天数 | | a | 上午/下午 | | H | 一天中的小时(0–23) | | k | 一天中的小时数(1–24) | | K | 上午/下午的小时数(0–11) | | h | 上午/下午的小时数(1–12) | | m | 小时中的分钟 | | s | 分钟秒 | | S | 毫秒 | | z | 通用时区 | | Z | RFC 822 时区 | | X | ISO 8601 时区 |
SimpleDateFormat 类是在 Java 的早期版本中创建的,因此您不必为给定的日期执行手动翻译。
注意
不同的地区使用不同的日期格式,SimpleDateFormat 类简化了特定于地区的格式。
若要使用类,必须通过将基于字符串的模式作为参数传递给构造函数或者不向构造函数传递任何参数来实例化实例。基于字符串的模式提供了一个应该应用于给定日期的模板,然后返回一个以给定模式样式表示日期的字符串。一个模式由许多不同的字符串在一起组成。表 4-8 显示了可以在一个模式中使用的不同字符。
任何模式字符都可以放在一个字符串中,然后传递给 SimpleDateFormat 类。如果该类在没有传递模式的情况下被实例化,则可以稍后使用该类的 applyPattern()方法来应用该模式。当您想要更改一个实例化的 SimpleDateFormat 对象的模式时,applyPattern()方法也很方便,如这个配方的解决方案所示。以下代码摘录演示了模式的应用:
SimpleDateFormat dateFormatter1 = new SimpleDateFormat("MMMMM dd yyyy");
dateFormatter1.applyPattern("MM/dd/YY hh:mm:ss");
一旦将模式应用于 SimpleDateFormat 对象,就可以将表示时间的长值传递给 SimpleDateFormat 对象的 Format()方法。format()方法将返回使用所应用的模式格式化的给定日期\时间。然后,可以根据应用的需要使用基于字符串的结果。
4-18.编写可读的数字文本
问题
您的应用中的一些数值很长,您想让它看起来更容易判断一个数字有多大。
解决办法
在较大的数字中用下划线代替逗号或小数,以使它们更具可读性。下面的代码显示了一些通过使用下划线代替逗号来提高数值可读性的示例:
int million = 1_000_000;
int billion = 1_000_000_000;
float ten_pct = 1_0f;
double exp = 1_234_56.78_9e2;
注意
小数点值将自动默认为双精度值,除非使用尾随的“f”来表示该值是浮点数。
它是如何工作的
有时处理大量数据会变得很麻烦,很难读懂。自从 Java 7 发布以来,为了使代码更容易阅读,下划线现在可以和数字一起使用。下划线可以出现在数字文本中数字之间的任何位置。这允许使用下划线代替逗号或空格来分隔数字,使它们更容易阅读。
注意
下划线不能放在数字的开头或结尾,小数点或浮点文字附近,后缀 F 或 L 之前,或者应该是数字串的位置。
4-19.声明二进制文本
问题
您正在开发一个需要声明二进制数的应用。
解决办法
利用二进制文字使你的代码可读。下面的代码段演示了二进制文本的用法:
int bin1 = 0b1100;
short bin2 = 0B010101;
short bin3 = (short) 0b1001100110011001;
System.out.println(bin1);
System.out.println(bin2);
System.out.println(bin3);
这将导致以下输出:
12
21
-26215
它是如何工作的
随着 Java 7 的发布,二进制文字成为了 Java 语言的一部分。byte、short、int 和 long 类型可以用二进制数字系统来表示。这个特性有助于在代码中更容易识别二进制数。为了使用二进制格式,只需在数字前面加上 0b 或 0B。
摘要
数字和日期在大多数应用中起着不可或缺的作用。Java 语言提供了大量的类,可以用来处理不同种类的数字,并对它们进行格式化以适应大多数情况。本章回顾了一些可用于舍入和格式化数字以及生成随机值的技术。Java 8 的发布引入了一个日期和时间包,为获取和处理日期带来了一个令人耳目一新、易于使用的 API。这一章讲述了新的日期和时间包的基础知识,更多内容可以在网上找到:docs.oracle.com/javase/tutorial/datetime/。
五、面向对象的 Java
自从应用开发的第一天以来,编程语言已经发生了很大的变化。过去,过程语言是最先进的;事实上,今天仍有成千上万的 COBOL 和其他程序应用在使用。随着时间的推移,编码变得更加高效,重用、封装、抽象和其他面向对象的特性成为应用开发的关键。随着语言的发展,它们开始融入在程序中使用对象的思想。早在 20 世纪 70 年代,Lisp 语言就引入了一些面向对象的技术,但是真正的面向对象编程直到 20 世纪 90 年代才大获成功。
面向对象的程序由许多不同的代码组成,它们一起协同工作。面向对象的哲学不是编写包含一长串语句和命令的程序,而是将功能分解成独立的有组织的对象。每个对象都包含与其相关的功能,当这些对象被组合在一起时,它们可以被用来开发复杂的解决方案。随着人们注意到面向对象等同于生产力,诸如使用方法封装功能和重用另一个类的功能等编程技术开始流行起来。
在这一章中,我们将触及 Java 语言的一些关键的面向对象的特性。从涵盖访问修饰符的基本方法,到处理内部类的高级方法,本章包含的方法将帮助你理解 Java 的面向对象方法。
5-1.控制对类成员的访问
问题
你想创建一个不能从任何其他类访问的类的成员。
解决办法
创建私有实例成员,而不是将它们提供给其他类(公共的或受保护的)。例如,假设您正在创建一个应用,它将用于管理一项运动的一组运动员。您创建了一个名为 Player 的类,它将用于表示团队中的一名球员。您不希望从任何其他类访问该类的字段。下面的代码演示了一些实例成员的声明,使它们只能从定义它们的类中访问。
private String firstName = null;
private String lastName = null;
private String position = null;
private int status = -1;
它是如何工作的
若要将类成员指定为私有,请使用 private 关键字作为其声明或签名的前缀。private 访问修饰符用于隐藏类的成员,这样外部类就不能访问它们。任何被标记为私有的类成员将只对同一类的其他成员可用。任何外部类都不能访问被指定为私有的字段或方法,使用代码完成的集成开发环境(ide)也不能看到它们。
正如在这个方法的解决方案中提到的,在声明一个类的成员时,有三种不同的访问修饰符可以使用。这些修饰符是公共的、受保护的和私有的。声明为公共的成员可用于任何其他类。那些被声明为受保护的类可用于同一个包中的任何其他类。最好只将那些需要从另一个类直接访问的类成员声明为 public 或 protected。使用 private access 修饰符隐藏类的成员有助于实施更好的面向对象。
5-2.使私有字段可供其他类访问
问题
您希望创建私有实例成员,以便外部类不能直接访问它们。但是,您也希望以受控的方式访问这些私有成员。
解决办法
通过设置 getters 和 setters 来访问私有字段,从而封装私有字段。下面的代码演示了私有字段的声明,后面是可用于从外部类获取或设置该字段值的访问器(getter)和赋值器(setter)方法:
private String firstName = null;
/**
* @return the firstName
*/
public String getFirstName() {
return firstName;
}
/**
* @param firstName the firstName to set
*/
public void setFirstName(String firstName) {
this.firstName = firstName;
}
外部类可以使用 getFirstName()方法来获取 FirstName 字段的值。同样,外部类可以使用 setFirstName(String firstName)方法来设置 FirstName 字段的值。
它是如何工作的
通常,当字段在类中被标记为私有时,它们仍然需要被外部类访问,以便设置或检索它们的值。为什么不直接处理这些字段,然后将它们公开呢?直接处理其他类的字段并不是好的编程实践,因为通过使用访问器(getters)和赋值器(setters),可以以受控的方式授予访问权限。通过不直接针对另一个类的成员进行编码,您还可以帮助分离代码,这有助于确保如果一个对象发生更改,依赖于它的其他对象不会受到负面影响。正如您在这个菜谱的解决方案的例子中所看到的,隐藏字段并使用公共方法来访问这些字段是相当容易的。简单地创建两个方法;一个用于获取私有字段的值,即“getter”或访问器方法。另一个用于设置私有字段的值,即“setter”或 mutator 方法。在这个配方的解决方案中,getter 用于返回私有字段中包含的未更改的值。类似地,setter 用于设置私有字段的值,方法是接受与私有字段具有相同数据类型的参数,然后将私有字段的值设置为该参数的值。
使用 getters 或 setters 访问字段的类不知道方法背后的任何细节。例如,如果需要,getter 或 setter 方法可以包含更多的功能。此外,可以更改这些方法的细节,而无需更改访问它们的任何代码。
注意
使用 getters 和 setters 并不能完全分离代码。事实上,许多人认为使用 getters 和 setters 不是一个好的编程实践。使用访问器方法的对象仍然需要知道它们正在处理的实例字段的类型。也就是说,getters 和 setters 是提供对对象私有实例字段的外部访问的标准技术。要以更面向对象的方式使用访问器方法,请在接口中声明它们,并针对接口而不是对象本身进行编码。有关接口的更多信息,请参考配方 5-6。
5-3.创建具有单个实例的类
问题
您希望创建一个在整个应用中只能有一个实例的类,这样所有应用用户都可以与该类的同一个实例进行交互。
解决方案 1
使用单例模式创建类。实现 Singleton 模式的类只允许该类的一个实例,并提供对该实例的单点访问。假设您想要创建一个统计类,用于计算一项有组织的运动中每个队和运动员的统计数据。在应用中拥有这个类的多个实例是没有意义的,所以您希望将 Statistics 类创建为一个 Singleton,以防止生成多个实例。下列类别代表单一模式:
package org.java9recipes.chapter5.recipe5_03;
import java.util.ArrayList;
import java.util.List;
import java.io.Serializable;
public class Statistics implements Serializable {
// Definition for the class instance
private static volatile Statistics instance = new Statistics();
private List teams = new ArrayList();
/**
* Constructor has been made private so that outside classes do not have
* access to instantiate more instances of Statistics.
*/
private Statistics(){
}
/**
* Accessor for the statistics class. Only allows for one instance of the
* class to be created.
* @return
*/
public static Statistics getInstance(){
return instance;
}
/**
* @return the teams
*/
public List getTeams() {
return teams;
}
/**
* @param teams the teams to set
*/
public void setTeams(List teams) {
this.teams = teams;
}
protected Object readResolve(){
return instance;
}
}
如果另一个类试图创建该类的一个实例,它将使用 getInstance()访问器方法来获取 Singleton 实例。值得注意的是,解决方案代码演示了急切实例化,这意味着实例将在加载单例时被实例化。对于惰性实例化,它将在第一次请求时被实例化,您必须注意同步 getInstance()方法以使它是线程安全的。下面的代码演示了一个惰性实例化的示例:
public static Statistics getInstance(){
synchronized(Statistics.class){
if (instance == null){
instance = new Statistics();
}
}
return instance;
}
解决方案 2
首先,创建一个枚举,并在其中声明一个名为 INSTANCE 的元素。接下来,在枚举中声明其他字段,这些字段可用于存储应用所需的值。以下枚举表示将提供与解决方案 1 相同功能的单例:
import java.util.ArrayList;
import java.util.List;
public enum StatisticsSingleton {
INSTANCE;
private List teams = new ArrayList();
/**
* @return the teams
*/
public List getTeams() {
return teams;
}
/**
* @param teams the teams to set
*/
public void setTeams(List teams) {
this.teams = teams;
}
}
注意
recipe5_03 包中有一个测试类,您可以使用它来处理 enum Singleton 解决方案。
它是如何工作的
Singleton 模式用于创建不能被任何其他类实例化的类。当您只想将某个类的一个实例用于整个应用时,这很有用。可以通过以下三个步骤将单例模式应用于一个类。首先,将类的构造函数设为私有,这样外部类就不能实例化它。接下来,定义一个私有静态 volatile 字段,它将表示该类的一个实例。volatile 关键字保证每个线程使用相同的实例。创建类的实例,并将其分配给字段。在该配方的解决方案中,类名为 Statistics,字段定义如下:
private static volatile Statistics instance = new Statistics();
最后,实现一个名为 getInstance()的访问器方法,该方法只返回实例字段。下面的代码演示了这样一种访问器方法:
public static Statistics getInstance(){
return instance;
}
要使用另一个类中的 Singleton,调用 Singleton 的 getInstance()方法。这将返回类的一个实例。下面的代码显示了另一个类的示例,该类获取了该配方的解决方案 1 中定义的 Statistics Singleton 的一个实例。
Statistics statistics = Statistics.getInstance();
List teams = statistics.getTeams();
任何调用该类的 getInstance()方法的类都将获得相同的实例。因此,对于整个应用中对 getInstance()的每次调用,Singleton 中包含的字段都具有相同的值。
如果单例被序列化然后反序列化会发生什么?这种情况可能会导致在反序列化时返回对象的另一个实例。为了防止此问题发生,请确保实现 readResolve()方法,如解决方案 1 中所示。当对象被反序列化时调用此方法,简单地返回实例可以确保不会生成另一个实例。
解决方案 2 展示了一种创建单例的不同方法,即使用 Java enum 而不是类。使用这种方法是有益的,因为 enum 提供序列化,禁止多重实例化,并允许您更简洁地处理代码。为了实现枚举单例,创建一个枚举并声明一个实例元素。这是一个静态常数,它将把枚举的实例返回给引用它的类。然后,您可以将元素添加到应用中的其他类可以用来存储值的枚举中。
与任何编程解决方案一样,有不止一种方法来做事情。有些人认为解决方案 1 中展示的标准单例模式不是最理想的解决方案。其他人出于不同的原因不喜欢 enum 解决方案。这两种方法都可以,尽管你可能会发现在某些情况下一种比另一种更有效。
5-4.生成类的实例
问题
在您的一个应用中,您想提供动态生成对象实例的能力。对象的每个实例都应该可以使用,对象创建者不需要知道对象创建的细节。
解决办法
利用工厂方法模式实例化类的实例,同时从对象创建者那里抽象出创建过程。创建工厂将使类的新实例能够在调用时返回。下面的类表示一个简单的工厂,它在每次调用其 createPlayer(String)方法时返回 Player 子类的一个新实例。返回的 Player 子类取决于传递给 createPlayer 方法的字符串值。
public class PlayerFactory {
public static Player createPlayer(String playerType){
Player returnType;
switch(playerType){
case "GOALIE":
returnType = new Goalie();
break;
case "LEFT":
returnType = new LeftWing();
break;
case "RIGHT":
returnType = new RightWing();
break;
case "CENTER":
returnType = new Center();
break;
case "DEFENSE":
returnType = new Defense();
break;
default:
returnType = new AllPlayer();
}
return returnType;
}
}
如果一个类想要使用该工厂,它只需调用静态 createPlayer 方法,传递一个表示 Player 新实例的字符串值。下面的代码代表了一个 Player 子类;其他的可能非常相似:
public class Goalie extends Player implements PlayerType {
private int totalSaves;
public Goalie(){
this.setPosition("GOALIE");
}
/**
* @return the totalSaves
*/
public int getTotalSaves() {
return totalSaves;
}
/**
* @param totalSaves the totalSaves to set
*/
public void setTotalSaves(int totalSaves) {
this.totalSaves = totalSaves;
}
}
其他每个球员子类都非常类似于守门员类。需要注意的最重要的代码是工厂方法 createPlayer,它可用于创建 Player 类的新实例。
注意
为了进一步说明这个例子,您可以限制可以访问的方法。通过返回 PlayerType 类型的对象,并且只在该接口中声明可访问的方法,可以做到这一点。
它是如何工作的
工厂用于生成对象。它们通常用于从对象的创建者那里抽象出对象的实际创建。当创建者不需要知道生成新对象的实际实现细节时,这非常方便。当需要对对象的创建进行受控访问时,工厂模式也很有用。为了实现工厂,创建一个包含至少一个用于返回新创建的对象的方法的类。
在这个配方的解决方案中,PlayerFactory 类包含一个名为 createPlayer(String)的方法,该方法返回一个新创建的 Player 对象。这个方法在幕后不做任何特别的事情;它只是根据传递给该方法的字符串值实例化一个新的播放器实例。另一个可以访问 PlayerFactory 类的对象可以使用 createPlayer 返回新的 Player 对象,而无需知道该对象是如何创建的。虽然在 createPlayer 方法的情况下这并没有隐藏太多,但是 PlayerFactory 抽象了正在实例化的类的细节,因此开发人员只需担心如何获得新的 Player 对象。
工厂模式是控制如何创建对象的一种有效方式,它使创建某种类型的对象变得更加容易。想象一下,如果一个对象的构造函数接受的不仅仅是几个参数;创建不仅仅需要几个参数的新对象会变得很麻烦。生成一个工厂来创建这些对象,这样您就不必对每个实例化的所有参数进行硬编码,这样可以提高您的工作效率!
5-5.创建可重用对象
问题
您希望生成一个对象,用于表示应用中的某些内容。此外,您希望能够重用该对象来表示多个实例。例如,假设您正在创建一个应用,用于为不同的运动队生成统计数据和联盟信息。在这种情况下,您想要创建一个可以用来表示团队的对象。
解决办法
创建一个 JavaBean,它可以用来表示您想要创建的对象。JavaBean 对象提供了将对象字段声明为私有的能力,并且它们还允许读取和更新属性,以便可以在应用中传递和使用对象。这个菜谱演示了一个名为 Team 的 JavaBean 的创建。团队对象包含几个不同的字段,这些字段可以包含信息:
public class Team implements TeamType {
private List<Player> players;
private String name = null;
private String city = null;
/**
* @return the players
*/
public List<Player> getPlayers() {
return players;
}
/**
* @param players the players to set
*/
public void setPlayers(List<Player> players) {
this.players = players;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the city
*/
public String getCity() {
return city;
}
/**
* @param city the city to set
*/
public void setCity(String city) {
this.city = city;
}
}
正如您所看到的,这个解决方案中的对象包含三个字段,每个字段都被声明为 private。然而,每个字段都有两个访问器方法――getter 和 setter――允许字段被间接访问。
它是如何工作的
JavaBean 是一个用来保存信息的对象,这样就可以在应用中传递和使用信息。JavaBean 最重要的方面之一是它的字段被声明为私有的。这禁止其他类直接访问这些字段。相反,每个字段应该由定义的方法封装,以便其他类可以访问它们。这些方法必须遵循以下命名约定:
-
用于访问字段数据的方法应该使用前缀 get,后跟字段名来命名。
-
用于设置字段数据的方法应该使用前缀 set 命名,后跟字段名。
例如,在这个菜谱的解决方案中,Team 对象包含一个包含玩家姓名的字段。为了访问该字段,应该声明一个名为 getPlayers 的方法。该方法应该返回包含在玩家字段中的数据。同样,要填充 players 字段,应该声明一个名为 setPlayers 的方法。该方法应该接受与 players 字段类型相同的参数,并且应该将 players 字段的值设置为等于该参数。这可以在下面的代码中看到:
public List<Player> getPlayers() {
return players;
}
void setPlayers(List<Player> players) {
this.players = players;
}
JavaBeans 可用于填充数据列表、写入数据库记录或用于无数其他功能。使用 JavaBeans 使得代码更容易阅读和维护。它还有助于增加未来代码增强的可能性,因为只需要很少的代码实现。使用 JavaBeans 的另一个好处是大多数主流 ide 会自动完成字段的封装。
5-6.为类定义接口
问题
您希望创建一组方法签名和字段,这些方法签名和字段可以用作公共模板来公开类实现的方法和字段。
解决办法
生成一个 Java 接口来声明一个类必须实现的每个字段和方法。这样的接口可以由一个类实现,并用来表示一个对象类型。以下代码是一个接口,用于声明 Team 对象必须实现的方法:
public interface TeamType {
void setPlayers(List<Player> players);
void setName(String name);
void setCity(String city);
String getFullName();
}
接口中的所有方法都是隐式抽象的。也就是说,只提供了方法签名。还可以在接口中包含静态最终字段声明。
它是如何工作的
Java 接口是一种用于定义结构的构造,无论是类必须实现的字段还是方法。在大多数情况下,接口不包括任何方法实现;相反,它们只包含方法签名。接口可以包含隐式静态和最终变量。
注意
从 Java SE 8 开始,接口可以包含方法实现。这种方法被称为*默认方法。*更多详情见制作方法 5-7。
在这个配方的解决方案中,接口不包括任何常量字段声明。但是,它包括四个方法签名。所有方法签名都没有指定访问修饰符,因为接口中的所有声明都是隐式公共的。接口用于公开一组功能;因此,接口中公开的所有方法都必须是隐式公共的。任何实现接口的类都必须为接口中声明的任何方法签名提供实现,除了默认方法和抽象类(更多细节见方法 5-7 和 5-13),在这种情况下,接口可以为它的一个子类留下实现。
虽然 Java 语言不允许多重继承,但是一个 Java 类可以实现多个接口,从而允许受控形式的多重继承。抽象类也可以实现接口。下面的代码演示了一个实现接口的类:Team 对象声明实现了 TeamType 接口。
public class Team implements TeamType {
private List<Player> players;
private String name;
private String city;
/**
* @return the players
*/
public List<Player> getPlayers() {
return players;
}
/**
* @param players the players to set
*/
public void setPlayers(List<Player> players) {
this.players = players;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the city
*/
public String getCity() {
return city;
}
/**
* @param city the city to set
*/
public void setCity(String city) {
this.city = city;
}
public String getFullName() {
return this.name + " - " + this.city;
}
}
接口可以用来声明对象的类型。任何声明为具有接口类型的对象都必须遵循接口中声明的所有实现,除非存在默认实现。例如,下面的字段声明定义了一个包含所有在 TeamType 接口中声明的属性的对象:
TeamType team;
接口也可以扩展其他接口(因此多重继承提供了相同类型的理论)。但是,由于接口中没有方法实现,因此在 Java 类中实现多个接口比在 C++中扩展多个类要安全得多。
接口是 Java 语言最重要的结构之一。它们提供了用户和类实现之间的接口。尽管不使用接口也可以创建完整的应用,但它们有助于促进面向对象,并对其他类隐藏方法实现。
5-7.在不破坏现有代码的情况下修改接口
问题
您已经有了一个实现接口的实用程序类,并且实用程序库中的许多不同的类都实现了该接口。假设您想向实用程序类添加一个新方法,并通过它的接口使它可供其他类使用。但是,如果更改接口,可能会破坏一些已经实现该接口的现有类。
解决办法
将新方法及其实现作为默认方法添加到实用程序类接口中。通过这样做,实现该接口的每个类将自动获得对新方法的使用,并且不会被强制实现它,因为存在默认实现。下面的类接口包含一个默认方法,任何实现该接口的类都可以使用该方法。
public interface TeamType {
List<Player> getPlayers();
void setPlayers(List<Player> players);
void setName(String name);
void setCity(String city);
String getFullName();
default void listPlayers() {
getPlayers().stream().forEach((player) -> {
System.out.println(player.getFirstName() + " " + player.getLastName());
});
}
}
接口 TeamType 包含一个名为 listPlayers()的默认方法。这个方法不需要由任何实现 TeamType 的类来实现,因为接口中包含了一个默认的实现。
它是如何工作的
在以前的 Java 版本中,接口只能包含方法签名和常量变量。不可能在接口中定义方法实现。这在大多数情况下工作良好,因为接口是一种旨在加强类型安全和抽象实现细节的构造。但是,在某些情况下,允许接口包含默认方法实现是有益的。例如,如果有许多类实现了一个现有的接口,那么如果该接口被更改,许多代码可能会被破坏。这将造成向后兼容不可能的情况。在这种情况下,将一个默认的方法实现放在一个接口中是有意义的,而不是强制所有的类实现一个放在接口中的新方法。这就是为什么缺省方法变得必不可少,并且包含在 Java 8 版本中的原因。
要在接口中创建默认方法(也称为“defender 方法”),请在方法签名中使用关键字 default,并包含一个方法实现。一个接口可以包含零个或多个默认方法。在这个配方的解决方案中,listPlayers()方法是 TeamType 接口中的默认方法,任何实现 TeamType 的类都将自动继承默认实现。理论上,任何实现 TeamType 的类都不会受到 listPlayers()默认方法的影响。这使人们能够在不破坏向后兼容性的情况下更改接口,这具有很大的价值。
注意
从 Java 9 开始,可以在接口中创建私有方法。私有方法只能由同一接口中的默认方法使用。因此,如果您有一些在两个或更多默认方法中重复的代码,那么可重复的代码可以封装在私有方法中。
5-8.用不同的值构造同一类的实例
问题
您的应用需要能够构造同一对象的实例,但是每个对象实例需要包含不同的值,从而创建同一对象的不同类型。
解决办法
利用构建器模式,通过一步一步的过程构建同一对象的不同类型。例如,假设您有兴趣为一个体育联盟创建不同的团队。每个团队必须包含相同的属性,但是这些属性的值因团队而异。因此,您创建了许多相同类型的对象,但是每个对象都是唯一的。下面的代码演示了 builder 模式,该模式可用于创建所需的团队。
首先,您需要定义每个团队需要包含的一组属性。为此,应该创建一个 Java 接口,包含需要应用于每个团队对象的不同属性。以下是这种界面的一个示例:
public interface TeamType {
public void setPlayers(List<Player> players);
public void setName(String name);
public void setCity(String city);
public String getFullName();
}
接下来,定义一个类来代表一个团队。这个类需要实现刚刚创建的 TeamType 接口,这样它将遵循构建团队所需的格式:
public class Team implements TeamType {
private List<Player> players;
private String name = null;
private String city = null;
private int wins = 0;
private int losses = 0;
private int ties = 0;
/**
* @return the players
*/
public List<Player> getPlayers() {
return players;
}
/**
* @param players the players to set
*/
public void setPlayers(List<Player> players) {
this.players = players;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the city
*/
public String getCity() {
return city;
}
/**
* @param city the city to set
*/
public void setCity(String city) {
this.city = city;
}
public String getFullName(){
return this.name + " – " + this.city;
}
}
现在已经定义了团队类,需要创建一个构建器。构建器对象的目的是允许逐步创建团队对象。为了抽象构建对象的细节,应该创建一个构建器类接口。该接口应该定义将用于构建对象的任何方法,以及将返回完全构建的对象的方法。在这种情况下,该接口将定义构建新团队对象所需的每个方法,然后构建器实现将实现该接口。
public interface TeamBuilder {
public void buildPlayerList();
public void buildNewTeam(String teamName);
public void designateTeamCity(String city);
public Team getTeam();
}
下面的代码演示了生成器类的实现。虽然下面的代码不会创建一个定制的播放器列表,但是它包含了实现构建器模式所需的所有特性。创建一个更加定制的球员名单的细节可以在以后解决,可能是通过允许用户通过键盘输入来创建球员。此外,TeamBuilder 接口可以用于实现不同运动的团队。下面的类命名为 HockeyTeamBuilder,但是实现 TeamBuilder 的类似类可以命名为 FootballTeamBuilder,依此类推。
public class HockeyTeamBuilder implements TeamBuilder {
private Team team;
public HockeyTeamBuilder(){
this.team = new Team();
}
@Override
public void buildPlayerList() {
List players = new ArrayList();
for(int x = 0; x <= 10; x++){
players.add(PlayerFactory.getPlayer());
}
team.setPlayers(players);
}
@Override
public void buildNewTeam(String teamName) {
team.setName(teamName);
}
@Override
public void designateTeamCity(String city){
team.setCity(city);
}
public Team getTeam(){
return this.team;
}
}
最后,通过调用在其接口中定义的方法来使用构建器创建团队。下面的代码演示了如何使用这个生成器来创建一个团队。您可以在这个菜谱的源代码中使用花名册类来测试这个代码:
public Team createTeam(String teamName, String city){
TeamBuilder builder = new HockeyTeamBuilder();
builder.buildNewTeam(teamName);
builder.designateTeamCity(city);
builder.buildPlayerList();
return builder.getTeam();
}
尽管构建器模式的演示相对较短,但它演示了如何隐藏对象的实现细节,从而使对象更容易构建。您不需要知道构建器中的方法实际上做什么;你只需要呼唤他们。
它是如何工作的
构建器模式提供了一种以过程方式生成对象的新实例的方法。它抽象了对象创建的细节,因此创建者不需要做任何特定的工作来生成新的实例。通过将工作分解成一系列步骤,构建器模式允许对象以不同的方式实现其构建器方法。因为对象创建者只能访问构建器方法,所以创建不同的对象类型要容易得多。
有几个类和接口是使用构建器模式所必需的。首先,您需要定义一个类及其不同的属性。正如这个配方的解决方案所展示的,这个类可能遵循 JavaBean 模式(更多细节见配方 5-5)。通过创建 JavaBean,您将能够使用它的 setters 和 getters 来填充对象。接下来,您应该创建一个接口,用于访问您创建的对象的 setters。每个 setter 方法都应该在接口中定义,然后对象本身应该实现该接口。正如在解决方案中所看到的,Team 对象包含以下 setterss,并且每个 setter 都是在 TeamType 接口中定义的:
public void setPlayers(List<Player> players);
public void setName(String name);
public void setCity(String city);
现实生活中,一个团队大概会包含更多的属性。例如,您可能想要设置一个吉祥物和一个主体育场的名称和地址。这个例子中的代码可以被认为是缩写的,因为它演示了一个通用的“团队对象”的创建,而不是向您展示创建一个真实团队的所有代码。因为 Team 类实现了这些在 TeamType 接口中定义的 setters,所以可以调用接口方法来与 Team 类的实际方法进行交互。
在对对象及其接口进行编码之后,需要创建实际的构建器。生成器由一个接口及其实现类组成。首先,您必须定义在构建对象时希望其他类调用的方法。例如,在这个配方的解决方案中,在名为 TeamBuilder 的构建器接口中定义了 buildNewTeam()、designateTeamCity()和 buildPlayerList()方法。当一个类以后想要构建这些对象之一时,它只需要调用这些定义的方法就可以了。接下来,定义一个构建器类实现。实现类将实现在构建器接口中定义的方法,对对象创建者隐藏这些实现的所有细节。在这个配方的解决方案中,构建器类 HockeyTeamBuilder 实现了 TeamBuilder 接口。当一个类想要创建一个新的团队对象时,它只是实例化一个新的构建器类。
TeamBuilder builder = new HockeyTeamBuilder();
为了填充新创建的类对象,在其上调用构建器方法。
builder.buildNewTeam(teamName);
builder.designateTeamCity(city);
builder.buildPlayerList();
使用这种技术为对象提供了一步一步的创建。构建该对象的实现细节对对象创建者是隐藏的。对于不同的构建器实现来说,使用相同的 TeamBuilder 接口来构建不同类型的团队对象是非常容易的。例如,可以编写一个构建器实现来为足球生成团队对象,而另一个实现可以被定义来为棒球生成团队对象。每个团队对象的实现都是不同的。然而,它们都可以实现相同的接口——team builder——并且创建者可以简单地调用构建器方法而不用关心细节。
5-9.通过接口与类交互
问题
您已经创建了一个实现接口或类类型的类。您希望通过调用接口中声明的方法来与该类的方法进行交互,而不是直接使用该类。
解决办法
将同一类型的字段声明为接口。然后,可以将实现接口的类分配给已声明的字段,并调用接口中声明的方法来执行工作。在下面的示例中,一个字段被声明为 TeamType 类型。使用配方 5-8 中的相同类,你可以看到类 Team 实现了 TeamType 接口。以下示例中创建的字段包含对新团队对象的引用。
因为 Team 类实现了 TeamType 接口,所以可以使用该接口中公开的方法:
TeamType team = new Team();
team.setName("Juneau Royals");
team.setCity("Chicago");
System.out.println(team.getFullName());
结果输出:
Juneau Royals – Chicago
它是如何工作的
接口的用处有很多。接口的两个最重要的用例是一致性和抽象。接口定义了一个模型,任何实现接口的类都必须符合这个模型。因此,如果在接口中定义了一个常量,它将自动在类中使用。如果在接口中定义了一个方法,那么这个类必须实现这个方法,除非已经定义了一个默认的实现(见方法 5-7)。接口提供了一种很好的方式让类符合标准。
接口对任何不需要看到的类隐藏不必要的信息。接口中定义的任何方法都是公共的,任何类都可以访问。正如这个配方的解决方案中所演示的,创建了一个对象,并将其声明为接口的类型。示例中的接口 TeamType 只包含团队对象中可用的一小部分方法。因此,对于任何处理已声明为 TeamType 的对象的类来说,唯一可访问的方法是那些在接口中定义的方法。使用此接口类型的类不能访问任何其他方法或常数,也不需要访问。接口是隐藏不需要被其他类使用的逻辑的好方法。另一个很大的副作用是:实现接口的类可以被改变和重新编译,而不会影响使用该接口的代码。然而,如果一个接口被改变,可能会对实现它的任何类产生影响。因此,如果 getFullName()方法实现发生变化,任何针对 TeamType 接口编码的类都不会受到影响,因为接口没有变化。实现将在幕后改变,任何处理接口的类都将开始使用新的实现,而不需要知道。
注意
在某些情况下,现有类的变更会导致代码中断。在使用库时,这种情况更为常见。例如,假设一个类实现了一个用新方法签名更新的接口。实现该接口的所有类现在都必须更新,以包括新方法的实现,为了保持向后兼容性,这在库类中有时是不可能的。这是 Java 8 中包含默认方法的主要原因;更多详情见配方 5-7。
最后,接口有助于提高安全性。它们隐藏了在接口中声明的方法的实现细节,以免任何类使用该接口调用该方法。如前一段所述,如果一个类对 TeamType 接口调用 getFullName()方法,只要结果按预期返回,它就不需要知道该方法的实现细节。
旧的 Enterprise JavaBean (EJB)模型使用接口与执行数据库工作的方法进行交互。这个模型很好地隐藏了其他类不需要的细节和逻辑。其他框架使用类似的模型,通过 Java 接口公开功能。接口的使用已经被证明是一种聪明的软件编码方式,因为它提高了可重用性、灵活性和安全性。
5-10.使类可克隆
问题
您希望一个类能够被另一个类克隆或复制。
解决办法
在要克隆的类中实现可克隆接口;然后调用该对象克隆方法来复制它。下面的代码演示了如何使 Team 类可克隆:
public class Team implements TeamType, Cloneable, Serializable {
private String name;
private String city;
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the city
*/
public String getCity() {
return city;
}
/**
* @param city the city to set
*/
public void setCity(String city) {
this.city = city;
}
public String getFullName() {
return this.name + " - " + this.city;
}
/**
* Overrides Object's clone method to create a deep copy
*
* @return
*/
@Override
public Team clone() {
Team obj = null;
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
obj = (Team) ois.readObject();
ois.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException cnfe) {
cnfe.printStackTrace();
}
return obj;
}
/**
* Overrides Object's clone method to create a shallow copy
*
* @return
*/
public Team shallowCopyClone() {
try {
return (Team) super.clone();
} catch (CloneNotSupportedException ex) {
return null;
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Team) {
Team other = (Team) obj;
return other.getName().equals(this.getName())
&& other.getCity().equals(this.getCity());
} else {
return false;
}
}
}
要制作团队对象的深层副本,需要针对该对象调用 clone()方法。要制作对象的浅层副本,必须调用 shallowCopyClone()方法。以下代码演示了这些技术:
Team team1 = new Team();
Team team2 = new Team();
team1.setCity("Boston");
team1.setName("Bandits");
team2.setCity("Chicago");
team2.setName("Wildcats");
Team team3 = team1;
Team team4 = team2.clone();
Team team5 = team1.shallowCopyClone();
System.out.println("Team 3:");
System.out.println(team3.getCity());
System.out.println(team3.getName());
System.out.println("Team 4:");
System.out.println(team4.getCity());
System.out.println(team4.getName());
// Teams move to different cities
team1.setCity("St. Louis");
team2.setCity("Orlando");
System.out.println("Team 3:");
System.out.println(team3.getCity());
System.out.println(team3.getName());
System.out.println("Team 4:");
System.out.println(team4.getCity());
System.out.println(team4.getName());
System.out.println("Team 5:");
System.out.println(team5.getCity());
System.out.println(team5.getName());
if (team1 == team3){
System.out.println("team1 and team3 are equal");
} else {
System.out.println("team1 and team3 are NOT equal");
}
if (team1 == team5){
System.out.println("team1 and team5 are equal");
} else {
System.out.println("team1 and team5 are NOT equal");
}
这段代码演示了如何克隆一个对象。结果输出如下。
Team 3:
Boston
Bandits
Team 4:
Chicago
Wildcats
Team 3:
St. Louis
Bandits
Team 4:
Chicago
Wildcats
Team 5:
Boston
Bandits
team1 and team3 are equal
team1 and team5 are NOT equal
它是如何工作的
有两种不同的策略可用于复制对象:浅层副本和深层副本。可以制作一个浅拷贝,它将拷贝该对象,而不拷贝它的任何内容或数据。相反,所有变量都通过引用传递到复制的对象中。创建对象的浅层副本后,原始对象及其副本中的对象引用相同的数据和内存。因此,修改原始对象的内容也会修改复制的对象。默认情况下,对对象调用 super.clone()方法会执行浅层复制。这个菜谱的解决方案中的 shallowCopyClone()方法演示了这种技术。
可以进行的第二种复制称为深度复制,它复制包含所有内容的对象。因此,每个对象都引用内存中不同的空间,修改一个对象不会影响另一个。在这个配方的解决方案中,展示了深层拷贝和浅层拷贝之间的区别。首先,创建团队 1 和团队 2。接下来,用一些值填充它们。然后,team3 对象被设置为与 team1 对象相等,而 team4 对象是 team2 对象的克隆。当 team1 对象中的值发生变化时,它们在 team3 对象中也会发生变化,因为这两个对象的内容指向内存中的同一个空间。这是一个对象浅层拷贝的例子。当 team2 对象中的值发生变化时,它们在 team4 对象中保持不变,因为每个对象都有自己的变量,这些变量引用内存中的不同空间。这是深层拷贝的一个例子。
为了制作对象的精确副本(深层副本),您必须序列化对象,以便可以将其写入磁盘。基本对象类实现了 clone()方法。默认情况下,对象类的 clone()方法是受保护的。为了使一个对象可克隆,它必须实现可克隆接口并覆盖默认的 clone()方法。您可以通过一系列步骤序列化对象来制作对象的深层副本,例如将对象写入输出流,然后通过输入流读回它。这个菜谱的解决方案的 clone()方法中显示的步骤就是这样做的。该对象被写入 ByteArrayOutputStream,然后使用 ByteArrayInputStream 读取。一旦发生这种情况,对象就被序列化,这就创建了深层副本。此配方的解决方案中的 clone()方法已被覆盖,因此它创建了一个深层副本。
一旦遵循了这些步骤,并且对象实现了 Cloneable 并覆盖了默认的 object clone()方法,就可以克隆对象了。为了制作对象的深层副本,只需调用该对象的被覆盖的 clone()方法,如解决方案中所示。如果只是从 clone()方法返回 Object,那么就需要进行类型转换,如下所示:
Team team4 = (Team) team2.clone();
克隆对象并不十分困难,但是很好地理解对象副本之间的差异是很重要的。
5-11.比较对象
问题
您的应用需要能够比较两个或多个对象,以查看它们是否相同。
解决方案 1
若要确定两个对象引用是否指向同一个对象,请使用==和!=运算符。下面的解决方案演示了两个对象引用的比较,以确定它们是否引用同一个对象。
// Compare if two objects contain the same values
Team team1 = new Team();
Team team2 = new Team();
team1.setName("Jokers");
team1.setCity("Crazyville");
team2.setName("Jokers");
team2.setCity("Crazyville");
if (team1 == team2){
System.out.println("These object references refer to the same object.");
} else {
System.out.println("These object references do NOT refer to the same object.");
}
// Compare two objects to see if they refer to the same object
Team team3 = team1;
Team team4 = team1;
if (team3 == team4){
System.out.println("These object references refer to the same object.");
} else {
System.out.println("These object references do NOT refer to the same object.");
}
运行代码的结果:
These object references do NOT refer to the same object.
These object references refer to the same object.
解决方案 2
若要确定两个对象是否包含相同的值,请使用 equals()方法。被比较的对象必须实现 equals()和 hashCode(),这样这个解决方案才能正常工作。下面是覆盖这两个方法的 Team 类的代码:
public class Team implements TeamType, Cloneable {
private List<Player> players;
private String name;
private String city;
// Used by the hashCode method for performance reasons
private volatile int cachedHashCode = 0;
/**
* @return the players
*/
public List<Player> getPlayers() {
return players;
}
/**
* @param players the players to set
*/
public void setPlayers(List<Player> players) {
this.players = players;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the city
*/
public String getCity() {
return city;
}
/**
* @param city the city to set
*/
public void setCity(String city) {
this.city = city;
}
public String getFullName() {
return this.name + " - " + this.city;
}
/**
* Overrides Object's clone method
*
* @return
*/
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException ex) {
return null;
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Team) {
Team other = (Team) obj;
return other.getName().equals(this.getName())
&& other.getCity().equals(this.getCity())
&& other.getPlayers().equals(this.getPlayers());
} else {
return false;
}
}
@Override
public int hashCode() {
int hashCode = cachedHashCode;
if (hashCode == 0) {
String concatStrings = name + city;
if (players.size() > 0) {
for (Player player : players) {
concatStrings = concatStrings
+ player.getFirstName()
+ player.getLastName()
+ player.getPosition()
+ String.valueOf(player.getStatus());
}
}
hashCode = concatStrings.hashCode();
}
return hashCode;
}
}
下面的解决方案演示了包含相同值的两个对象的比较。
// Compare if two objects contain the same values
Team team1 = new Team();
Team team2 = new Team();
// Build Player List
Player newPlayer = new Player("Josh", "Juneau");
playerList.add(0, newPlayer);
newPlayer = new Player("Jonathan", "Gennick");
playerList.add(1, newPlayer);
newPlayer = new Player("Joe", "Blow");
playerList.add(1, newPlayer);
newPlayer = new Player("John", "Smith");
playerList.add(1, newPlayer);
newPlayer = new Player("Paul", "Bunyan");
playerList.add(1, newPlayer);
team1.setName("Jokers");
team1.setCity("Crazyville");
team1.setPlayers(playerList);
team2.setName("Jokers");
team2.setCity("Crazyville");
team2.setPlayers(playerList);
if (team1.equals(team2)){
System.out.println("These object references contain the same values.");
} else {
System.out.println("These object references do NOT contain the same values.");
}
运行这段代码的结果是:
These object references do NOT refer to the same object.
These object references contain the same values.
These object references refer to the same object.
它是如何工作的
比较运算符(==)可用于确定两个对象是否相等。这种相等不属于对象值,而是属于对象引用。通常应用更关心对象的值;在这种情况下,equals()方法是首选,因为它比较的是对象中包含的值,而不是对象引用。
比较运算符查看对象引用,并确定它是否指向与要比较的对象引用相同的对象。如果两个对象相等,将返回布尔值 true 结果;否则,将返回布尔假结果。在解决方案 1 中,team1 对象引用和 team2 对象引用之间的第一次比较返回 false 值,因为这两个对象在内存中是分开的,即使它们包含相同的值。在解决方案 1 中,team3 对象引用和 team4 对象引用之间的第二次比较返回 true 值,因为这两个引用都引用了 team1 对象。
equals()方法可用于测试两个对象是否包含相同的值。为了使用 equals()方法进行比较,被比较的对象应该覆盖 object 类 equals()和 hashCode()方法。equals()方法应该实现与包含在对象中的值进行比较,从而产生真实的比较结果。以下代码是一个被覆盖的 equals()方法的示例,该方法已被放入 Team 对象中:
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Team) {
Team other = (Team) obj;
return other.getName().equals(this.getName())
&& other.getCity().equals(this.getCity())
&& other.getPlayers().equals(this.getPlayers());
} else {
return false;
}
}
正如您所看到的,被覆盖的 equals()方法首先检查作为参数传递的对象是否引用了与它进行比较的对象相同的对象。如果是,则返回真结果。如果两个对象没有引用内存中的同一个对象,equals()方法会检查这两个字段是否相等。在这种情况下,任何两个在 name 和 city 字段中包含相同值的团队对象都将被视为相等。一旦 equals()方法被覆盖,就可以执行这两个对象的比较,如该配方的解决方案 2 所示。
hashCode()方法返回一个 int 值,该值必须始终返回同一个整数。有很多方法可以计算对象的 hashCode。在网上搜索一下这个话题,你会发现各种各样的技巧。实现 hashCode()方法的一个最基本的方法是将所有对象的变量连接成字符串格式,然后返回结果字符串的 hashCode()。缓存 hashCode 的值供以后使用是一个好主意,因为初始计算可能需要一些时间。解决方案 2 中的 hashCode()方法演示了这种策略。
考虑到有多种方法可以比较 Java 对象,比较 Java 对象可能会变得令人困惑。如果要对对象标识执行比较,请使用比较(==)运算符。但是,如果您想要比较对象中的值,或者对象的状态,那么 equals()方法是一个不错的选择。
5-12.扩展类的功能
问题
你的一个应用包含了一个类,你想用它作为另一个类的基础。您希望您的新类包含该基类的相同功能,但还包含其他功能。
解决办法
通过使用 extends 关键字后跟要扩展的类的名称来扩展基类的功能。下面的示例显示了两个类。第一个类名为 HockeyStick,表示一个曲棍球棒对象。它将由名为 WoodenStick 的第二个类扩展。通过这样做,WoodenStick 类将继承 HockeyStick 中包含的所有属性和功能,私有变量和具有默认访问级别的变量除外。WoodenStick 类成为 HockeyStick 的子类。首先,让我们看一下 HockeyStick 类,它包含标准曲棍球棒的基本属性:
public class HockeyStick {
private int length;
private boolean curved;
private String material;
public HockeyStick(int length, boolean curved, String material){
this.length = length;
this.curved = curved;
this.material = material;
}
/**
* @return the length
*/
public int getLength() {
return length;
}
/**
* @param length the length to set
*/
public void setLength(int length) {
this.length = length;
}
/**
* @return the curved
*/
public boolean isCurved() {
return curved;
}
/**
* @param curved the curved to set
*/
public void setCurved(boolean curved) {
this.curved = curved;
}
/**
* @return the material
*/
public String getMaterial() {
return material;
}
/**
* @param material the material to set
*/
public void setMaterial(String material) {
this.material = material;
}
}
接下来,看看 HockeyStick 的子类:一个名为 WoodenStick 的类。
public class WoodenStick extends HockeyStick {
private static final String material = "WOOD";
private int lie;
private int flex;
public WoodenStick(int length, boolean isCurved){
super(length, isCurved, material);
}
public WoodenStick(int length, boolean isCurved, int lie, int flex){
super(length, isCurved, material);
this.lie = lie;
this.flex = flex;
}
/**
* @return the lie
*/
public int getLie() {
return lie;
}
/**
* @param lie the lie to set
*/
public void setLie(int lie) {
this.lie = lie;
}
/**
* @return the flex
*/
public int getFlex() {
return flex;
}
/**
* @param flex the flex to set
*/
public void setFlex(int flex) {
this.flex = flex;
}
}
注意
在这个例子中,我们假设可能有不止一种类型的 HockeyStick。在这种情况下,我们扩展 HockeyStick 来创建一个 WoodenStick,但是我们也可以扩展 HockeyStick 来创建其他类型的 HockeyStick,比如 AluminumStick 或 GraphiteStick。
它是如何工作的
对象继承是任何面向对象语言的基本技术。从基类继承增加了价值,因为它允许代码在多个地方重用。这有助于使代码管理更加容易。如果在基类中进行了更改,它将自动在子类中继承。另一方面,如果您的应用中分散着重复的功能,那么一个微小的更改就意味着您必须在许多地方更改代码。对象继承也使得为一个或多个子类指定一个基类变得容易,这样每个类可以包含相似的字段和功能。
Java 语言只允许一个类扩展另一个类。这在概念上不同于包含多重继承的其他语言,如 C++。虽然有些人认为单个类继承是语言的障碍,但它是为了增加语言的安全性和易用性而设计的。当一个子类包含多个超类时,混乱就会接踵而至。
5-13.为要扩展的类定义模板
问题
您希望定义一个模板,用于生成包含类似功能的对象。
解决办法
定义一个抽象类,其中包含可以在其他类中使用的字段和功能。抽象类还可以包含未实现的方法,称为抽象方法,需要由抽象类的子类实现。下面的示例演示了抽象类的概念。示例中的抽象类表示一个团队日程,它包括一些基本的字段声明和每个团队的日程都需要使用的功能。然后由 TeamSchedule 类扩展 Schedule 类,它将用于为每个团队实现特定的功能。首先,让我们看看抽象的 Schedule 类:
public abstract class Schedule {
public String scheduleYear;
public String teamName;
public List<Team> teams;
public Map<Team, LocalDate> gameMap;
public Schedule(){}
public Schedule(String teamName){
this.teamName = teamName;
}
abstract void calculateDaysPlayed(int month);
}
接下来,TeamSchedule 扩展了抽象类的功能。
public class TeamSchedule extends Schedule {
public TeamSchedule(String teamName) {
super(teamName);
}
@Override
void calculateDaysPlayed(int month) {
int totalGamesPlayedInMonth = 0;
for (Map.Entry<Team, LocalDate> entry : gameMap.entrySet()) {
if (entry.getKey().equals(teamName)
&& entry.getValue().getMonth().equals(month)) {
totalGamesPlayedInMonth++;
}
}
System.out.println("Games played in specified month: " + totalGamesPlayedInMonth);
}
}
如您所见,TeamSchedule 类可以使用抽象 Schedule 类中包含的所有字段和方法。它还实现了包含在 Schedule 类中的抽象方法。
它是如何工作的
抽象类就是这样标记的,它们包含可以在子类中使用的字段声明和方法。它们与常规类的不同之处在于它们可以包含抽象方法,抽象方法是没有实现的方法声明。这个配方的解决方案包含一个名为 calculateDaysPlayed()的抽象方法。抽象类可能包含也可能不包含抽象方法。它们可以包含字段和完全实现的方法。抽象类不能被实例化;其他类只能扩展它们。当一个类扩展一个抽象类时,它获得该抽象类的所有字段和功能。然而,在抽象类中声明的任何抽象方法都必须由子类实现。
您可能想知道为什么抽象类不仅仅包含方法的实现,这样它的所有子类都可以使用它。如果你思考一下这个概念,它就非常有意义。一种类型的对象执行的任务可能与另一种不同。使用抽象方法会强制扩展抽象类的类实现它,但它允许自定义实现方式的能力。
5-14.增加类封装
问题
您的一个类需要使用另一个类的功能。但是,没有其他类需要使用相同的功能。您希望生成一个只能由需要它的类使用的实现,同时将代码放在一个逻辑位置,而不是创建一个包含这一附加功能的单独的类。
解决办法
在需要其功能的类中创建一个内部类。
import java.util.ArrayList;
import java.util.List;
/**
* Inner class example. This example demonstrates how a team object could be
* built using an inner class object.
*
* @author juneau
*/
public class TeamInner {
private Player player;
private List<Player> playerList;
private int size = 4;
/**
* Inner class representing a Player object
*/
class Player {
private String firstName = null;
private String lastName = null;
private String position = null;
private int status = -1;
public Player() {
}
public Player(String position, int status) {
this.position = position;
this.status = status;
}
protected String playerStatus() {
String returnValue = null;
switch (getStatus()) {
case 0:
returnValue = "ACTIVE";
break;
case 1:
returnValue = "INACTIVE";
break;
case 2:
returnValue = "INJURY";
break;
default:
returnValue = "ON_BENCH";
break;
}
return returnValue;
}
public String playerString() {
return getFirstName() + " " + getLastName() + " - " + getPosition();
}
/**
* @return the firstName
*/
public String getFirstName() {
return firstName;
}
/**
* @param firstName the firstName to set
*/
public void setFirstName(String firstName) {
this.firstName = firstName;
}
/**
* @return the lastName
*/
public String getLastName() {
return lastName;
}
/**
* @param lastName the lastName to set
*/
public void setLastName(String lastName) {
this.lastName = lastName;
}
/**
* @return the position
*/
public String getPosition() {
return position;
}
/**
* @param position the position to set
*/
public void setPosition(String position) {
this.position = position;
}
/**
* @return the status
*/
public int getStatus() {
return status;
}
/**
* @param status the status to set
*/
public void setStatus(int status) {
this.status = status;
}
@Override
public String toString(){
return this.firstName + " " + this.lastName + " - "+
this.position + ": " + this.playerStatus();
}
}
/**
* Inner class that constructs the Player objects and adds them to an array
* that was declared in the outer class;
*/
public TeamInner() {
final int ACTIVE = 0;
// In reality, this would probably read records from a database using
// a loop...but for this example we will manually enter the player data.
playerList = new ArrayList();
playerList.add(constructPlayer("Josh", "Juneau", "Right Wing", ACTIVE));
playerList.add(constructPlayer("Joe", "Blow", "Left Wing", ACTIVE));
playerList.add(constructPlayer("John", "Smith", "Center", ACTIVE));
playerList.add(constructPlayer("Bob","Coder", "Defense", ACTIVE));
playerList.add(constructPlayer("Jonathan", "Gennick", "Goalie", ACTIVE));
}
public Player constructPlayer(String first, String last, String position, int status){
Player player = new Player();
player.firstName = first;
player.lastName = last;
player.position = position;
player.status = status;
return player;
}
public List<Player> getPlayerList() {
return this.playerList;
}
public static void main(String[] args) {
TeamInner inner = new TeamInner();
System.out.println("Team Roster");
System.out.println("===========");
for(Player player:inner.getPlayerList()){
System.out.println(player.playerString());
}
}
}
运行这段代码的结果是一个团队成员的列表。
Team Roster
===========
Josh Juneau - Right Wing
Joe Blow - Left Wing
John Smith - Center
Bob Coder - Defense
Jonathan Gennick - Goalie
它是如何工作的
有时将功能封装在单个类中很重要。其他时候,为只在另一个类中使用的功能包含一个单独的类是没有意义的。假设您正在开发一个 GUI,您需要使用一个类来支持一个按钮的功能。如果按钮类中没有可重用的代码,那么创建一个单独的类并公开该功能供其他类使用是没有意义的。相反,将该类封装在需要该功能的类中是有意义的。这一理念是内部类(也称为嵌套类)的一个用例。
内部类是包含在另一个类中的类。内部类可以像任何其他类一样被公开、私有或保护。它可以包含与普通类相同的功能;唯一的区别是内部类包含在封闭类中,也称为外部类。这个配方的解决方案演示了这种技术。TeamInner 类包含一个名为 Player 的内部类。Player 类是一个表示 Player 对象的 JavaBean 类。如您所见,Player 对象能够从其包含的类继承功能,包括其私有字段。这是因为内部类包含对外部类的隐式引用。它也可以由包含它的 TeamInner 类访问,如 constructPlayer()方法中所示:
public Player constructPlayer(String first, String last, String position, int status){
Player player = new Player();
player.firstName = first;
player.lastName = last;
player.position = position;
player.status = status;
return player;
}
外部类可以根据需要多次实例化内部类。在这个例子中,constructPlayer()方法可以被调用任意次,实例化内部类的一个新实例。但是,当实例化外部类时,不会实例化内部类的任何实例。类似地,当外部类不再使用时,所有内部类实例也被销毁。
内部类可以通过引用外部类和它想要调用的方法来引用外部类方法。下面一行代码演示了这样一个引用,它使用了这个配方的解决方案中表示的相同对象。假设玩家类需要从外部类获得玩家列表;您应该编写类似下面的内容:
TeamInner.this.getPlayerList();
虽然不经常使用,但外部类以外的类可以通过使用以下语法获得对公共内部类的访问:
TeamInner outerClass = new TeamInner();
outerClass.player = outerClass.new Player();
静态内部类有点不同,因为它们不能直接引用其封闭类的任何实例变量或方法。下面是一个静态内部类的例子。
public class StaticInnerExample {
static String hello = "Hello";
public static void sayHello(){
System.out.println(hello);
}
static class InnerExample {
String goodBye = "Good Bye";
public void sayGoodBye(){
System.out.println(this.goodBye);
}
}
public static void main (String[] args){
StaticInnerExample.sayHello();
StaticInnerExample.InnerExample inner =
new StaticInnerExample.InnerExample();
inner.sayGoodBye();
}
}
内部类有助于提供逻辑封装。此外,它们允许私有字段的继承,这在使用标准类时是不可能的。
摘要
Java 是一种面向对象的语言。为了利用这种语言的能力,人们必须学会如何精通面向对象。本章讲述了诸如类创建和访问修饰符之类的基础知识。它还涵盖了封装、接口和配方,以帮助开发人员利用面向对象的强大功能。