Java17 入门基础知识(九)
十五、字符串
在本章中,您将学习:
-
什么是
String对象 -
如何创建
String对象 -
如何使用
String文字 -
如何操纵
Strings -
如何在
switch语句或开关表达式中使用Strings -
如何使用
StringBuilder和StringBuffer对象构造字符串 -
如何创建多行字符串
本章中的所有类都是一个jdojo.string模块的成员,如清单 15-1 中所声明的。
// module-info.java
module jdojo.string {
exports com.jdojo.string;
}
Listing 15-1The Declaration of a jdojo.string Module
什么是字符串?
零个或多个字符的序列称为字符串。在 Java 程序中,字符串由java.lang.String类的对象表示。String类是不可变的。也就是说,String对象的内容在创建后不能修改。String类有两个同伴类,java.lang.StringBuilder和java.lang.StringBuffer。伴随类是可变的。当字符串的内容可以修改时,应该使用它们。
在 Java 9 之前,String类的实现将字符存储在一个char数组中,对字符串中的每个字符使用 2 个字节。大多数String对象只包含拉丁 1 字符,只需要 1 个字节来存储字符串中的一个字符。因此,在大多数情况下,这种String对象的char数组中有一半的空间没有被使用。Java 9 改变了String类的内部实现,使用一个byte数组来存储String对象的内容;它还存储一个编码标志,指示String中的每个字符是 1 字节还是 2 字节。这样做是为了有效利用String对象使用的内存。作为开发人员,在程序中使用字符串不需要了解任何新知识,因为没有为String类更改公共接口。
字符串文字
字符串由一系列用双引号括起来的零个或多个字符组成。所有字符串文字都是String类的对象。字符串文字的示例有
String s1 = ""; // An empty string
String s2 = "Hello"; // String literal consisting of 5 characters
String s3 = "Just a string literal"; // String literal consisting of 21 characters
多个字符串文字可用于组成单个字符串文字:
// Composed of two string literals "Hello" and "Hi". It represents one string literal "HelloHi"
String s4 = "Hello" + "Hi";
使用两个双引号的字符串文字不能分成两行(可以使用本章后面介绍的文本块语法来实现这一点):
// Cannot break a string literal in multiple lines. A compile-time error
String wronStr = "Hello";
如果您想将"Hello"分成两行,您可以使用字符串连接运算符(+)将其断开,如下所示:
String s5 = "He" + "llo";
或者
String s6 = "He" + "llo";
这里显示了另一个多行字符串文字的例子。整个文本代表一个字符串文字:
String s7 = "This is a big string literal" +
" and it will continue in several lines." +
" It is also valid to insert multiple new lines as we did here. " +
"Adding more than one line in between two string literals " +
"is a feature of Java Language syntax, " +
" not of string literal.";
字符串中的转义序列字符
字符串由字符组成。使用所有转义序列字符来构成字符串文字是有效的。例如,要在字符串中包含换行符和回车符,可以使用\n和\r,如下所示:
"\n" // String literal with a line feed
"\r" // String literal with a carriage return
"\n\r" // String literal with a line feed and a carriage return
"First line.\nSecond line." // An embedded line feed
"Tab\tSeparated\twords" // An embedded tab escape character
"Double quote \" is here" // Embedded double quote in string literal
字符串中的 Unicode 转义
一个字符也可以用形式为\uxxxx的 Unicode 转义来表示,其中x是一个十六进制数字(0–9或A–F)。在字符串文字中,字符'A',第一个大写的英文字母,也可以写成'\u0041',例如Apple和\u0041pple在 Java 中的处理是一样的。换行符和回车转义符也可以用 Unicode 转义符分别表示为'\u000A'和'\u000D'。不能使用 Unicode 转义在字符串中嵌入换行符和回车符。换句话说,你不能在一个字符串中用'\u000A'替换'\n',用'\u000D'替换'\r'。为什么?原因是 Unicode 转义在编译过程的最开始就被处理,导致'\u000A'和'\u000D'分别被转换成一个真正的换行符和一个回车符。这违反了字符串不能在两行中继续的规则。例如,在编译的早期阶段,“Hel\u000Alo"被翻译成以下内容,这是一个无效的字符串文字,会生成编译时错误:
"Hello"
Tip
在字符串中使用 Unicode 转义符\u000A和\u000D分别表示换行符和回车符是一个编译时错误。你必须使用\n和\r的转义序列来代替。
什么是 CharSequence?
一个CharSequence是java.lang包中的一个接口。我在第二十一章中讨论接口。现在,您可以将CharSequence视为一个表示可读字符序列的对象。仅举几个例子,String、StringBuffer和StringBuilder就是CharSequence的例子。它们提供只读方法来读取一些属性和它们所表示的字符序列的内容。在String类的 API 文档中,您会看到许多方法的参数被声明为CharSequence。在需要一个CharSequence的地方,你总是可以通过一个String、一个StringBuilder或者一个StringBuffer。
创建字符串对象
String类包含许多可以用来创建String对象的构造器。默认构造器允许您创建一个以空字符串为内容的String对象。例如,以下语句创建一个空的String对象,并将其引用赋给emptyStr变量:
String emptyStr = new String();
String类包含另一个构造器,它将另一个String对象作为参数:
String str1 = new String();
String str2 = new String(str1); // Passing a String as an argument
现在str1表示与str2相同的字符序列。此时,str1和str2都代表一个空字符串。您也可以将字符串文字传递给此构造器:
String str3 = new String("");
String str4 = new String("Have fun!");
执行完这两条语句后,str3将引用一个String对象,它的内容是一个空字符串(零字符序列),而str4将引用一个String对象,它的内容是“Have fun!"”。
字符串的长度
String类包含一个length()方法,该方法返回String对象中的字符数。注意,length()方法返回字符串中的字符数,而不是字符串使用的字节数。方法length()的返回类型是int。清单 15-2 演示了如何计算字符串的长度。空字符串的长度为零。
// StringLength.java
package com.jdojo.string;
public class StringLength {
public static void main (String[] args) {
// Create two string objects
String str1 = new String() ;
String str2 = new String("Hello") ;
// Get the length of str1 and str2
int len1 = str1.length();
int len2 = str2.length();
// Display the length of str1 and str2
System.out.println("Length of \"" + str1 + "\" = " + len1);
System.out.println("Length of \"" + str2 + "\" = " + len2);
}
}
Length of "" = 0
Length of "Hello" = 5
Listing 15-2Knowing the Length of a String
字符串文字是字符串对象
所有字符串文字都是String类的对象。编译器用对一个String对象的引用替换所有的字符串文字。考虑以下语句:
String str1 = "Hello";
当这个语句被编译时,编译器遇到字符串文字"Hello",它创建一个String对象,以"Hello"作为其内容。实际上,字符串文字和String对象是一样的。只要可以使用String对象的引用,就可以使用String文字。String类的所有方法都可以直接和String文字一起使用。例如,要计算String文字的长度,您可以编写
int len1 = "".length(); // len1 is equal to 0
int len2 = "Hello".length(); // len2 is equal to 5
字符串对象是不可变的
对象是不可变的。也就是说,您不能修改String对象的内容。这带来了一个好处,字符串可以被共享,而不用担心它们被修改。例如,如果您需要两个内容相同的String类的对象(相同的字符序列),您可以创建一个String对象,并且您可以在两个地方使用它的引用。有时,Java 中字符串的不变性会被误解,尤其是初学者。考虑下面这段代码:
String str;
str = new String("Just a string");
str = new String("Another string");
这里,str是可以引用任何String对象的引用变量。换句话说,str是可以改变的,是可变的。然而,str所指的String对象总是不可变的。这种情况如图 15-1 和 15-2 所示。
图 15-2
将不同的字符串对象引用赋给字符串变量
图 15-1
字符串引用变量和字符串对象
如果你不希望str在初始化后引用任何其他的String对象,你可以声明它为final,就像这样:
final String str = new String("str cannot refer to other object");
str = new String("Let us try"); // A compile-time error. str is final
Tip
不可变的是内存中的String对象,而不是String类型的引用变量。如果你想让一个引用变量一直引用内存中同一个String对象,你必须声明引用变量final。
比较字符串
您可能想要比较由两个String对象表示的字符序列。String类覆盖了Object类的equals()方法,并提供了自己的实现,它根据内容比较两个字符串是否相等。例如,您可以比较两个字符串是否相等,如下所示:
String str1 = new String("Hello");
String str2 = new String("Hi");
String str3 = new String("Hello");
boolean b1, b2;
b1 = str1.equals(str2); // false will be assigned to b1
b2 = str1.equals(str3); // true will be assigned to b2
还可以将字符串文字与字符串文字或字符串对象进行比较,如下所示:
b1 = str1.equals("Hello"); // true will be assigned to b1
b2 = "Hello".equals(str1); // true will be assigned to b2
b1 = "Hello".equals("Hi"); // false will be assigned to b1
回想一下,==操作符总是比较内存中两个对象的引用。比如str1 == str2和str1 == str3会返回false,因为str1、str2和str3是内存中三个不同String对象的引用。注意new操作符总是返回一个新的对象引用。
有时您希望比较字符串以进行排序。您可能希望根据字符的 Unicode 值或它们在字典中出现的顺序对字符串进行排序。String类中的compareTo()方法和java.text.Collator类中的compare()方法允许您比较字符串以进行排序。
如果您想基于字符的 Unicode 值比较两个字符串,请使用String类的compareTo()方法,其声明如下:
public int compareTo(String anotherString)
它返回一个整数,可以是 0(零)、正整数或负整数。它比较两个字符串的相应字符的 Unicode 值。如果任意两个字符的 Unicode 值不同,该方法将返回这两个字符的 Unicode 值之差。例如,"a".compareTo("b")将返回–1.,'a'的 Unicode 值为 97,'b'的 Unicode 值为 98。它返回差值97 – 98,也就是–1。以下是字符串比较的示例:
"abc".compareTo("abc") will return 0
"abc".compareTo("xyz") will return -23 (value of 'a' – 'x')
"xyz".compareTo("abc") will return 23 (value of 'x' – 'a')
非常重要的一点是,compareTo()方法根据字符的 Unicode 值来比较两个字符串。该比较可能与字典顺序比较不同。这对于英语和其他一些语言来说没有问题,在这些语言中,字符的 Unicode 值与字符的字典顺序相同。在字符的字典顺序可能与其 Unicode 值不同的语言中,不应使用此方法来比较两个字符串。要执行基于语言的字符串比较,应该使用java.text.Collator类的compare()方法。参考本章中的“区分语言的字符串比较”一节,了解如何使用java.text.Collator类。清单 15-3 演示了字符串比较。
// StringComparison.java
package com.jdojo.string;
public class StringComparison {
public static void main(String[] args) {
String apple = new String("Apple");
String orange = new String("Orange");
System.out.println(apple.equals(orange));
System.out.println(apple.equals(apple));
System.out.println(apple == apple);
System.out.println(apple == orange);
System.out.println(apple.compareTo(apple));
System.out.println(apple.compareTo(orange));
}
}
false
true
true
false
0
-14
Listing 15-3Comparing Strings
字符串池
Java 维护了一个包含所有字符串的池,以最小化内存使用并获得更好的性能。它在字符串池中为程序中找到的每个字符串创建一个String对象。当它遇到一个字符串文字时,它在字符串池中寻找具有相同内容的字符串对象。如果它在字符串池中没有找到匹配,它将使用该内容创建一个新的String对象,并将其添加到字符串池中。最后,它用池中新创建的String对象的引用替换字符串。如果它在字符串池中找到一个匹配,它就用在池中找到的String对象的引用替换字符串。让我们用一个例子来讨论这个场景。考虑以下语句:
String str1 = new String("Hello");
当 Java 遇到字符串文字"Hello"时,它会尝试在字符串池中寻找匹配。如果字符串池中没有内容为"Hello"的String对象,则创建一个内容为"Hello"的新的String对象,并将其添加到字符串池中。字符串文字"Hello"将被字符串池中新的String对象的引用所替换。因为使用了new操作符,Java 将在堆上创建另一个 String 对象。因此,在这种情况下将创建两个String对象。考虑以下代码:
String str1 = new String("Hello");
String str2 = new String("Hello");
这段代码会创建多少个String对象?假设执行第一条语句时,"Hello"不在字符串池中。因此,第一条语句将创建两个String对象。当执行第二条语句时,将在字符串池中找到字符串文字"Hello"。这一次,"Hello"将被替换为引用池中已经存在的对象。然而,Java 将创建一个新的String对象,因为您在第二条语句中使用了new操作符。假设"Hello"不在字符串池中,前面两条语句将创建三个String对象。如果这些语句开始执行时"Hello"已经在字符串池中,那么只会创建两个String对象。考虑以下语句:
String str1 = new String("Hello");
String str2 = new String("Hello");
String str3 = "Hello";
String str4 = "Hello";
str1 == str2返回的值会是什么?它将是false,因为new操作符总是在内存中创建一个新对象,并返回这个新对象的引用。
str2 == str3返回的值会是什么?又会是false了。这个需要稍微解释一下。注意new操作符总是创建一个新的对象。因此,str2在内存中有一个对新对象的引用。因为在执行第一条语句时已经遇到了"Hello",所以它存在于字符串池中,str3引用字符串池中内容为"Hello"的String对象。所以str2和str3引用两个不同的对象,str2 == str3返回false。
str3 == str4返回的值会是什么?会是true。注意,在执行第一条语句时,"Hello"已经被添加到字符串池中。第三条语句将把字符串池中一个String对象的引用分配给str3。第四条语句将把字符串池中相同的对象引用分配给str4。换句话说,str3和str4在字符串池中引用同一个String对象。==运算符比较两个参考值;因此,str3 == str4返回true。考虑另一个例子:
String s1 = "Have" + "Fun";
String s2 = "HaveFun";
s1 == s2会回true吗?是的,它会返回true。当在编译时常量表达式中创建一个String对象时,它也被添加到字符串池中。由于"Have" + "Fun"是一个编译时常量表达式,结果字符串"HaveFun"将被添加到字符串池中。因此,s1和s2会引用字符串池中的同一个对象。
所有编译时常量字符串都被添加到字符串池中。考虑以下示例来阐明此规则:
final String constStr = "Constant"; // constStr is a constant
String varStr = "Variable"; // varStr is not a constant
// "Constant is pooled" will be added to the string pool
String s1 = constStr + " is pooled";
// Concatenated string will not be added to the string pool
String s2 = varStr + " is not pooled";
执行这段代码后,"Constant is pooled" == s1将返回true,而"Variable is not pooled" == s2将返回false。
Tip
编译时常量表达式产生的所有字符串和字符串都被添加到字符串池中。
您可以使用其intern()方法将一个String对象添加到字符串池中。如果找到匹配项,intern()方法将从字符串池中返回对象的引用。否则,它向字符串池添加一个新的String对象,并返回新对象的引用。例如,在前面的代码片段中,s2引用了一个String对象,其内容为"Variable is not pooled"。您可以通过编写以下代码将这个String对象添加到字符串池中
// Will add the content of s2 to the string pool and return the reference
// of the string object from the pool
s2 = s2.intern();
现在"Variable is not pooled" == s2将返回true,因为您已经在s2上调用了intern()方法,并且它的内容已经被缓冲。
Tip
String类在内部维护一个字符串池。所有字符串都会自动添加到池中。您可以通过调用String对象上的intern()方法将自己的字符串添加到池中。您不能直接访问该池。除了退出并重新启动应用程序之外,没有办法从池中删除字符串对象。
字符串操作
本节描述了对String对象的一些常用操作。
获取索引处的字符
您可以使用charAt()方法从String对象中获取特定索引处的字符。索引从零开始。表 15-1 显示了字符串"HELLO"中所有字符的索引。
表 15-1
字符串“HELLO”中所有字符的索引
| 索引-> | `0` | `1` | `2` | `3` | `4` | | 字符-> | `H` | `E` | `L` | `L` | `O` |注意第一个字符H的索引是 0(零),第二个字符E是 1,依此类推。最后一个字符 O 的索引是 4,等于字符串"Hello"的长度减 1。
下面的代码片段将打印索引值和字符串"HELLO"中每个索引处的字符:
String str = "HELLO";
// Get the length of string
int len = str.length();
// Loop through all characters and print their indexes
for (int i = 0; i < len; i++) {
System.out.println(str.charAt(i) + " is at index " + i);
}
H is at index 0
E is at index 1
L is at index 2
L is at index 3
O is at index 4
测试字符串是否相等
如果您想比较两个字符串是否相等并忽略它们的大小写,您可以使用equalsIgnoreCase()方法。如果您想执行区分大小写的相等比较,您需要使用equals()方法,如前所述:
String str1 = "Hello";
String str2 = "HELLO";
if (str1.equalsIgnoreCase(str2)) {
System.out.println ("Ignoring case str1 and str2 are equal");
} else {
System.out.println("Ignoring case str1 and str2 are not equal");
}
if (str1.equals(str2)) {
System.out.println("str1 and str2 are equal");
} else {
System.out.println("str1 and str2 are not equal");
}
Ignoring case str1 and str2 are equal
str1 and str2 are not equal
测试字符串是否为空
有时你需要测试一个String对象是否为空。空字符串的长度为零。有三种方法可以检查空字符串:
-
使用
isEmpty()方法。 -
使用
equals()方法。 -
获取
String的长度,并检查它是否为零。
以下代码片段显示了如何使用所有三种方法:
String str1 = "Hello";
String str2 = "";
// Using the isEmpty() method
boolean empty1 = str1.isEmpty(); // Assigns false to empty1
boolean empty2 = str2.isEmpty(); // Assigns true to empty1
// Using the equals() method
boolean empty3 = "".equals(str1); // Assigns false to empty3
boolean empty4 = "".equals(str2); // Assigns true to empty4
// Comparing length of the string with 0
boolean empty5 = str1.length() == 0; // Assigns false to empty5
boolean empty6 = str2.length() == 0; // Assigns true to empty6
这些方法中哪一种最好?第一种方法可能看起来可读性更强,因为方法名称暗示了它的意图。然而,第二种方法是首选,因为它可以优雅地处理与null的比较。如果字符串是null,第一个和第三个方法抛出一个NullPointerException。第二种方法在字符串为null时返回false,例如"".equals(null)返回false。
改变案例
要将字符串的内容转换成小写和大写,可以分别使用toLowerCase()方法和toUpperCase()方法。例如,"Hello".toUpperCase()将返回字符串"HELLO",而"Hello".toLowerCase()将返回字符串"hello"。
回想一下String对象是不可变的。当您在一个String对象上使用toLowerCase()或toUpperCase()方法时,原始对象的内容不会被修改。相反,Java 创建了一个新的String对象,它的内容与原来的String对象相同,只是改变了原来字符的大小写。以下代码片段创建了三个String对象:
String str1 = new String("Hello"); // str1 contains "Hello"
String str2 = str1.toUpperCase(); // str2 contains "HELLO"
String str3 = str1.toLowerCase(); // str3 contains "hello"
搜索字符串
您可以使用indexOf()和lastIndexOf()方法获取一个字符或一个字符串在另一个字符串中的索引,例如:
String str = "Apple";
int index = str.indexOf('p'); // index will have a value of 1
index = str.indexOf("pl"); // index will have a value of 2
index = str.lastIndexOf('p'); // index will have a value of 2
index = str.lastIndexOf("pl"); // index will have a value of 2
index = str.indexOf("k"); // index will have a value of -1
indexOf()方法从字符串的开头开始搜索字符或字符串,并返回第一个匹配的索引。lastIndexOf()方法从末尾开始匹配字符或字符串,并返回第一个匹配的索引。如果在字符串中没有找到字符或字符串,这些方法返回–1。
将值表示为字符串
String类有一个重载的valueOf()静态方法。它可用于获取任何原始数据类型或任何对象的值的字符串表示形式,例如:
String s1 = String.valueOf('C'); // s1 has "C"
String s2 = String.valueOf("10"); // s2 has "10"
String s3 = String.valueOf(true); // s3 has "true"
String s4 = String.valueOf(1969); // s4 has "1969"
获取子字符串
您可以使用substring()方法来获取字符串的一部分。此方法重载如下:
-
字符串子字符串(int startIndex)
-
string substr(int begin index,int endIndex)
第一个版本返回一个字符串,该字符串从索引beginIndex处的字符开始,一直延伸到该字符串的末尾。第二个版本返回一个字符串,从索引beginIndex处的字符开始,延伸到索引endIndex - 1处的字符。如果指定的索引超出了字符串的范围,这两种方法都会抛出一个IndexOutOfBoundsException。以下是使用这些方法的示例:
String s1 = "Hello".substring(1); // s1 has "ello"
String s2 = "Hello".substring(1, 4); // s2 has "ell"
修剪绳子
您可以使用trim()方法删除字符串中所有的前导和尾随空格以及控制字符。事实上,trim()方法从字符串中删除了所有前导和尾随字符,这些字符的 Unicode 值小于\u0020(十进制 32)。例如:
-
" hello ".trim()会回“你好”。 -
"hello ".trim()会回“你好”。 -
"\n \r \t hello\n\n\n\r\r"会回“你好”。
注意,trim()方法只删除了开头和结尾的空白。它不会删除出现在字符串中间的任何空白或控制字符,例如:
-
因为
\n在字符串中,所以" he\nllo ".trim()将返回"he\nllo"。 -
"h ello".trim()将返回"h ello",因为空格在字符串内部。
替换字符串的一部分
String类包含以下方法,允许您通过用不同的字符或字符串替换旧字符串的一部分来创建新字符串:
-
String replace(char oldChar, char newChar) -
String replace(CharSequence target, CharSequence replacement) -
String replaceAll(String regex, String replacement) -
String replaceFirst(String regex, String replacement)
replace(char oldChar, char newChar)方法通过用newChar替换所有出现的oldChar来返回一个新的String对象。这里有一个例子:
// Both 'o's in "tooth" will be replaced by two 'e'. str will contain "teeth"
String str = "tooth".replace('o', 'e');
replace(CharSequence target, CharSequence replacement)方法与CharSequence一起工作。它通过用replacement替换所有出现的target来返回一个新的String对象。这里有一个例子:
// "oo" in "tooth" will be replaced by "ee". str will contain "teeth"
String str = "tooth".replace("oo", "ee");
replaceAll(String regex, String replacement)方法使用regex中的正则表达式来查找匹配。它通过用replacement替换每个匹配返回一个新的String对象。匹配一个数字的正则表达式是\d。我在第十八章中讲述了正则表达式。这里有一个例子:
// Replace all digits with an *. str contains "Born on Sept **, ****"
String str = "Born on Sept 19, 1969".replaceAll("\\d", "*");
replaceFirst(String regex, String replacement)方法的工作原理与replaceAll()方法相同,除了它只使用replacement替换第一个匹配。这里有一个例子:
// Replace the first digit with an *. str contains "Born on Sept *9, 1969"
String str = "Born on Sept 19, 1969".replaceFirst("\\d", "*");
匹配字符串的开头和结尾
startsWith()方法检查字符串是否以指定的参数开始,而endsWith()检查字符串是否以指定的字符串参数结束。两种方法都返回一个boolean值。以下是使用这些方法的示例:
String str = "This is a Java program";
// Test str if it starts with "This"
if (str.startsWith("This")){
System.out.println("String starts with This");
} else {
System.out.println("String does not start with This");
}
// Test str if it ends with "program"
if (str.endsWith("program")) {
System.out.println("String ends with program");
} else {
System.out.println("String does not end with program");
}
String starts with This
String ends with program
拆分和连接字符串
在指定的分隔符周围拆分一个字符串,并使用指定的分隔符将多个字符串连接成一个字符串通常很有用。
使用split()方法将一个字符串拆分成多个字符串。使用分隔符执行拆分。split()方法返回一个String的数组。你将在第十九章中学习数组。但是,在本节中,您将使用它来完成字符串的操作。
Note
split()方法采用一个定义模式的正则表达式作为分隔符。
String str = "AL,FL,NY,CA,GA";
// Split str using a comma as the delimiter
String[] parts = str.split(",");
// Print the the string and its parts
System.out.println(str);
for(String part : parts) {
System.out.println(part);
}
AL,FL,NY,CA,GA
AL
FL
NY
CA
GA
Java 8 在String类中添加了一个静态的join()方法,将多个字符串连接成一个字符串。它超载了:
-
String join(CharSequence delimiter, CharSequence... elements) -
String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)
第一个版本采用一个分隔符和一系列要连接的字符串。第二个参数是 var-args,所以您也可以将一个数组传递给这个方法。
第二个版本采用一个分隔符和一个Iterable,例如一个List或Set。以下代码片段使用第一个版本来连接几个字符串:
// Join some strings using a comma as the delimiter
String str = String.join(",", "AL", "FL", "NY", "CA", "GA");
System.out.println(str);
AL,FL,NY,CA,GA
switch 语句中的字符串
我们在第六章讨论了switch语句。您也可以在switch语句中使用字符串。switch表达式使用了一个String类型。如果switch表达式是null,则抛出NullPointerException。case标签必须是String文字或常量。不能在case标签中使用String变量。下面是一个在switch语句中使用String的例子,它将在标准输出中打印"Turn on":
String status = "on";
switch(status) {
case "on":
System.out.println("Turn on"); // Will execute this
break;
case "off":
System.out.println("Turn off");
break;
default:
System.out.println("Unknown command");
break;
}
字符串的switch语句将switch表达式与case标签进行比较,就好像调用了String类的equals()方法一样。在前面的例子中,将调用status.equals("on")来测试是否应该执行第一个case块。注意,String类的equals()方法执行区分大小写的字符串比较。这意味着使用字符串的switch语句是区分大小写的。
下面的switch语句将在标准输出中打印"Unknown command",因为大写的switch表达式"ON"与小写的第一个case标签"on"不匹配:
String status = "ON";
switch(status) {
case "on":
System.out.println("Turn on");
break;
case "off":
System.out.println("Turn off");
break;
default:
System.out.println("Unknown command"); // Will execute this
break;
}
作为一个良好的编程实践,在执行带有字符串的switch语句之前,您需要做以下两件事:
-
检查
switch语句的switch值是否为null。如果是null,不执行switch语句。 -
如果您想在
switch语句中执行不区分大小写的比较,您需要将switch表达式转换为小写或大写,并相应地在case标签中使用小写或大写。
您可以重写前面的switch语句示例,如清单 15-4 所示,它考虑了两个建议。
// StringInSwitch.java
package com.jdojo.string;
public class StringInSwitch {
public static void main(String[] args) {
operate("on");
operate("off");
operate("ON");
operate("Nothing");
operate("OFF");
operate(null);
}
public static void operate(String status) {
// Check for null
if (status == null) {
System.out.println("status cannot be null.");
return;
}
// Convert to lowercase
switch (status.toLowerCase()) {
case "on":
System.out.println("Turn on");
break;
case "off":
System.out.println("Turn off");
break;
default:
System.out.println("Unknown command");
break;
}
}
}
Turn on
Turn off
Turn on
Unknown command
Turn off
status cannot be null.
Listing 15-4Using Strings in a switch Statement
测试字符串的回文
如果你是一个有经验的程序员,你可以跳过这一节。这对初学者来说是一个简单的练习。
回文是一个单词、一句诗、一个句子或一个数字,向前和向后读起来都一样。例如,“在我看到厄尔巴岛之前我是能干的”和 1991 就是回文的例子。让我们写一个方法,它接受一个字符串作为参数,并测试这个字符串是否是一个回文。如果字符串是回文,该方法将返回true。否则将返回false。您将使用在前面章节中学到的String类的一些方法。下面是对该方法中要执行的步骤的描述。
假设输入字符串的字符数为n。您需要比较索引 0 和(n–1)、1 和(n–2)、2 和(n–3)等处的字符。请注意,如果您继续比较,最后,您将比较索引(n–1)处的字符和索引 0 处的字符,这在开始时已经比较过了。你只需要在中途比较一下角色。如果所有相等的比较都返回true,那么这个字符串就是一个回文。
字符串中的字符数可以是奇数也可以是偶数。在这两种情况下,只比较字符的一半是可行的。字符串的中间根据字符串的长度是奇数还是偶数而变化。例如,字符串"FIRST"的中间是字符R.字符串"SECOND"的中间字符是什么?你可以说它没有中间字,因为它的长度是偶数。为此,有趣的是,如果字符串中的字符数是奇数,您不需要将中间的字符与任何其他字符进行比较。
如果字符串中的字符数是偶数,则需要继续进行字符比较,直到字符串长度的一半;如果字符数是奇数,则需要继续进行字符比较,直到字符串长度的一半减去一个。通过将字符串长度除以 2,可以得到两种情况下要进行的比较次数。注意,字符串的长度是整数;如果您将一个整数除以 2,整数除法将丢弃分数部分,如果有的话,这将处理奇数字符的情况。清单 15-5 包含完整的代码。
// Palindrome.java
package com.jdojo.string;
import java.util.Objects;
public class Palindrome {
public static void main(String[] args) {
String str1 = "hello";
boolean b1 = Palindrome.isPalindrome(str1);
System.out.println(str1 + " is a palindrome: " + b1);
String str2 = "noon";
boolean b2 = Palindrome.isPalindrome(str2);
System.out.println(str2 + " is a palindrome: " + b2);
}
public static boolean isPalindrome(String inputString) {
Objects.requireNonNull(inputString, "String cannot be null.");
// Get the length of string
int len = inputString.length();
// In case of an empty string and one character strings, we do not need to
// do any comparisons. They are always palindromes.
if (len <= 1) {
return true;
}
// Convert the string into uppercase, so we can make the comparisons case insensitive
String newStr = inputString.toUpperCase();
// Initialize the result variable to true
boolean result = true;
// Get the number of comparisons to be done
int counter = len / 2;
// Do the comparison
for (int i = 0; i < counter; i++) {
if (newStr.charAt(i) != newStr.charAt(len - 1 - i)) {
// It is not a palindrome
result = false;
// Exit the loop
break;
}
}
return result;
}
}
hello is a palindrome: false
noon is a palindrome: true
Listing 15-5Testing a String for a Palindrome
StringBuilder 和 StringBuffer
StringBuilder和StringBuffer是String类的伙伴类。与String不同,它们代表一个可变的字符序列。也就是说,您可以更改StringBuilder和StringBuffer的内容,而无需创建新对象。您可能想知道为什么存在两个类来表示同一个东西——一个可变的字符序列。StringBuffer类从一开始就是 Java 库的一部分,而StringBuilder类是在 Java 5 中添加的。两者的区别在于线程安全。StringBuffer是线程安全的,StringBuilder不是线程安全的。大多数时候,您不需要线程安全,在这些情况下使用StringBuffer会有性能损失。这就是后来加上StringBuilder的原因。这两个类有相同的方法,除了StringBuffer中的所有方法都是同步的。本节我们将只讨论StringBuilder。在代码中使用StringBuffer只是改变类名的问题。
Tip
当不需要线程安全时,使用StringBuilder,例如,在方法或构造器中操作局部变量中的字符序列。否则,使用StringBuffer。线程安全和同步将在本系列的第二卷中介绍。
在字符串内容经常变化的情况下,可以使用StringBuilder类的对象,而不是String类。回想一下,由于String类的不变性,使用String对象的字符串操作会产生许多新的String对象,从而降低性能。一个StringBuilder对象可以被认为是一个可修改的字符串。它有许多方法来修改它的内容。StringBuilder类包含四个构造器:
-
StringBuilder() -
StringBuilder(CharSequence seq) -
StringBuilder(int capacity) -
StringBuilder(String str)
无参数构造器创建一个默认容量为 16 的空StringBuilder。
第二个构造器将一个CharSequence对象作为参数。它创建一个StringBuilder对象,其内容与指定的CharSequence相同。
第三个构造器以一个int作为参数;它创建一个空的StringBuilder对象,其初始容量与指定的参数相同。一个StringBuilder的容量是在不分配更多空间的情况下它能容纳的字符数。当需要额外空间时,容量会自动调整。
第四个构造器获取一个String并创建一个与指定的String具有相同内容的StringBuilder。以下是创建StringBuilder对象的一些例子:
// Create an empty StringBuilder with a default initial capacity of 16 characters
StringBuilder sb1 = new StringBuilder();
// Create a StringBuilder from of a string
StringBuilder sb2 = new StringBuilder("Here is the content");
// Create an empty StringBuilder with 200 characters as the initial capacity
StringBuilder sb3 = new StringBuilder(200);
append()方法允许您将文本添加到StringBuilder的末尾。它超载了。它需要多种类型的论证。有关所有重载的append()方法的完整列表,请参考该类的 API 文档。它还有其他方法,例如insert()和delete(),也可以让你修改它的内容。
StringBuilder类有两个属性:length和capacity。在给定的时间点,它们的值可能不相同。它的长度是指其内容的长度,而它的容量是指它在不需要分配新内存的情况下可以容纳的最大字符数。它的长度在任何时候都至多等于它的容量。length()和capacity()方法分别返回它的长度和容量,例如:
StringBuilder sb = new StringBuilder(200); // Capacity:200, length:0
sb.append("Hello"); // Capacity:200, length:5
int len = sb.length(); // len is assigned 5
int capacity = sb.capacity(); // capacity is assigned 200
一个StringBuilder的容量是由运行时控制的,而它的长度是由你放入其中的内容控制的。当其内容被修改时,运行时会调整容量。
您可以通过使用toString()方法将StringBuilder的内容作为String获取:
// Create a String object
String s1 = new String("Hello");
// Create a StringBuilder from of the String object s1
StringBuilder sb = new StringBuilder(s1);
// Append " Java" to the StringBuilder’s content
sb.append(" Java"); // Now, sb contains "Hello Java"
// Get a String from the StringBuilder
String s2 = sb.toString(); // s2 contains "Hello Java"
与String不同,StringBuilder有一个setLength()方法,它把它的新长度作为参数。如果新长度大于现有长度,多余的位置用null字符填充(空字符为\u0000)。如果新长度小于现有长度,其内容将被截断以适应新长度:
// Length is 5
StringBuilder sb = new StringBuilder("Hello");
// Now the length is 7 with last two characters as null character '\u0000'
sb.setLength(7);
// Now the length is 2 and the content is "He"
sb.setLength(2);
StringBuilder类有一个reverse()方法,用相同的字符序列替换它的内容,但是顺序相反。清单 15-6 展示了StringBuilder类的一些方法。
// StringBuilderTest.java
package com.jdojo.string;
public class StringBuilderTest {
public static void main(String[] args) {
// Create an empty StringBuilder
StringBuilder sb = new StringBuilder();
printDetails(sb);
// Append "blessings"
sb.append("blessings");
printDetails(sb);
// Insert "Good " in the beginning
sb.insert(0, "Good ");
printDetails(sb);
// Delete the first o
sb.deleteCharAt(1);
printDetails(sb);
// Append " be with you"
sb.append(" be with you");
printDetails(sb);
// Set the length to 3
sb.setLength(3);
printDetails(sb);
// Reverse the content
sb.reverse();
printDetails(sb);
}
public static void printDetails(StringBuilder sb) {
System.out.println("Content: \"" + sb + "\"");
System.out.println("Length: " + sb.length());
System.out.println("Capacity: " + sb.capacity());
// Print an empty line to separate results
System.out.println();
}
}
Content: ""
Length: 0
Capacity: 16
Content: "blessings"
Length: 9
Capacity: 16
Content: "Good blessings"
Length: 14
Capacity: 16
Content: "God blessings"
Length: 13
Capacity: 16
Content: "God blessings be with you"
Length: 25
Capacity: 34
Content: "God"
Length: 3
Capacity: 34
Content: "doG"
Length: 3
Capacity: 34
Listing 15-6Using a StringBuilder Object
字符串串联运算符(+)
有三种连接字符串的方法:
-
使用
String类的concat(String str)方法 -
使用+字符串串联运算符
-
使用
StringBuilder或StringBuffer
concat()方法将一个String作为参数,这意味着您只能用它来连接字符串。如果要将不同数据类型的值连接成一个字符串,请使用连接运算符,例如:
// Assigns "hi there" to s1
String s1 = "hi ".concat(" there");
// Assign "XY12.56" to s2
String s2 = "X" + "Y" + 12.56;
// Assign "XY12.56" to s3
String s3 = new StringBuilder().append("X").append("Y").append(12.56).toString();
多行字符串
在版本 15 和更高版本中,通过文本块向 Java 添加了多行字符串支持。文本块必须以三个引号和一个新行开始,这是定义跨越多行的字符串的一种更方便的方式。以前在 Java 中,如果你想定义一个包含换行符或多行的字符串,你需要使用“\n”在字符串中显式地提供换行符(新行)。
例如,使用文本块,可以定义如下所示的多行字符串:
String text = """
First line.
Second line.
Third line.
""";
Java 忽略每行前面的空白(空格或制表符)。这使得在保持所有文本缩进的同时定义多行字符串成为可能。
以前,如果没有文本块,您需要以下列方式定义相同的字符串:
String text = "First line.\n" +
"Second line.\n" +
"Third line.\n";
在定义多行字符串时,不要忘记包括三个引号和一个新行。例如,以下语法将不起作用,因为它不包括三个引号后的新行:
String text = """First line.
Second line.
Third line.
""";
您不需要转义文本块中的引号。否则,文本块的字符串定义规则与普通字符串相同。清单 15-7 展示了文本块的作用。
// TextBlocks.java
public class TextBlocks {
public static void main(String[] args) {
String text = """
First line.
"Second line."
Third line.
""";
System.out.print(text);
}
}
First line.
"Second line."
Third line.
Listing 15-7Multiline Strings
这个程序演示了如何定义一个包含引号的多行字符串,并打印出结果。
文本块使得在 Java 程序中定义多行 SQL 查询或 JSON 主体更加容易。
区分语言的字符串比较
String类根据字符的 Unicode 值来比较字符串。有时,您可能希望根据字典顺序来比较字符串。
使用java.text.Collator类的compare()方法执行区分语言(字典顺序)的字符串比较。该方法将两个要比较的字符串作为参数。如果两个字符串相同,则返回0,如果第一个字符串在第二个字符串之后,则返回1,如果第一个字符串在第二个字符串之前,则返回-1。清单 15-8 展示了Collator类的用法。
// CollatorStringComparison.java
package com.jdojo.string;
import java.text.Collator;
import java.util.Locale;
public class CollatorStringComparison {
public static void main(String[] args) {
// Create a Locale object for US
Locale USLocale = new Locale("en", "US");
// Get a Collator instance for US
Collator c = Collator.getInstance(USLocale);
String str1 = "cat";
String str2 = "Dog";
int diff = c.compare(str1, str2);
System.out.print("Comparing using Collator class: ");
print(diff, str1, str2);
System.out.print("Comparing using String class: ");
diff = str1.compareTo(str2);
print(diff, str1, str2);
}
public static void print(int diff, String str1, String str2) {
if (diff > 0) {
System.out.println(str1 + " comes after " + str2);
} else if (diff < 0) {
System.out.println(str1 + " comes before " + str2);
} else {
System.out.println(str1 + " and " + str2 + " are the same.");
}
}
}
Comparing using Collator class: cat comes before Dog
Comparing using String class: cat comes after Dog
Listing 15-8Language-Sensitive String Comparisons
该程序还使用String类显示了相同的两个字符串的比较。请注意,在字典顺序中,单词"cat"位于单词"Dog"之前。Collator类使用它们的字典顺序来比较它们。然而,String类比较了"cat"的第一个字符的 Unicode 值(99)和"Dog"的第一个字符的 Unicode 值(68)。基于这两个值,String类确定"Dog"在"cat"之前。输出确认了比较字符串的两种不同方式。
摘要
在这一章中,你学习了String、StringBuilder和StringBuffer类。一个String表示不可变的字符序列,而StringBuilder和StringBuffer表示可变的字符序列。StringBuilder和StringBuffer工作方式相同,只是后者是线程安全的,而前者不是。
String类提供了几个方法来操作它的内容。每当你从一个String获得一部分内容,一个新的String对象就会被创建。String类根据字符的 Unicode 值比较两个字符串。Java 增加了对文本块的支持,使用三个引号加上一个新行来表示开始,并且文本可以缩进。使用java.text.Collator类按照字典顺序比较字符串。从 Java 7 开始,可以在switch语句中使用字符串。
QUESTIONS AND EXERCISES
-
Java 中的字符串是什么?在创建一个
String对象后,你能改变它的内容吗? -
什么是字符串文字?
-
String级和StringBuilder级有什么区别? -
StringBuffer级和StringBuilder级有什么区别? -
当执行以下代码片段时,编写输出:
String s1 = "Hello"; String s2 = "\"Hello\""; System.out.println("s1 = " + s1); System.out.println("s2 = " + s2); -
当执行以下代码片段时,编写输出:
String s1 = "Who\nknows"; System.out.println("s1 = " + s1); -
当执行以下代码片段时,编写输出:
String s1 = "Having fun with strings"; int len = s1.length(); char c = s1.charAt(4); -
当执行以下代码片段时,编写输出:
String s1 = "Fun"; String s2 = new String("Fun"); System.out.println(s1 == s2); System.out.println(s1.equals(s2)); System.out.println("Fun" == "Fun"); -
当执行以下代码片段时,编写输出:
StringBuilder sb = new StringBuilder(200); sb.append("Hello").append(false); System.out.println("length = " + sb.length()); System.out.println("capacity = " + sb.capacity()); System.out.println(sb.toString()); -
当执行以下代码片段时,编写输出:
```java
String s1 = 10 + 20 + " = what";
String s2 = 10 + String.valueOf(20) + " = what";
System.out.println(s1);
System.out.println(s2);
```
11. 如这里所声明的,完成名为equalsContents()的方法的代码。如果两个参数在删除了开头和结尾的空白并忽略了大小写后具有相同的内容,那么该方法应该返回true。如果两个参数都为空,它应该返回true。否则应该返回false :
```java
public static boolean equalsContents(String s1, String s2) {
/* your code goes here*/
}
```
12. 完成以下代码,以便将年、月和日打印为1969、09和19 :
```java
String date = "1969-09-19";
String year = date./*your code goes here*/;
String month = date./*your code goes here*/;
String day = date./*your code goes here*/;
System.out.println("year = " + year);
System.out.println("month = " + month);
System.out.println("day = " + day);
```
13. 完成下面的代码片段,以便打印预期的输出,显示在该代码片段之后:
```java
String s1 = "noon and spoon";
String s2 = s1./*Your code goes here*/;
System.out.println(s1);
System.out.println(s2);
```
预期的输出如下:
```java
noon and spoon
nun and spun
```
14. 完成下面的代码片段,以便打印预期的输出,显示在该代码片段之后:
```java
String s1 = "noon and spoon";
String s2 = s1./*Your code goes here*/;
System.out.println(s1);
System.out.println(s2);
```
预期的输出如下:
```java
noon and spoon
nn and spn
```
15. 完成一个reverse(String str)方法的代码。它接受一个字符串并返回该字符串的反码。不要使用StringBuilder或StringBuffer类:
```java
public static String reverse(String str) {
/* Your code goes here */
}
```
16. 表达式"abc".compareTo("abc")的值是多少?
十六、日期和时间
在本章中,您将学习:
-
什么是 Java 日期时间 API
-
日期-时间 API 背后的设计原则
-
计时、时区和夏令时(DST)的演变
-
关于日期、时间和日期时间保持的 ISO-8601 标准
-
如何使用日期-时间 API 类表示日期、时间和日期时间,以及如何查询、调整、格式化和解析它们
-
如何使用旧的日期 API
-
如何在旧的和新的日期时间 API 之间进行互操作
日期-时间 API 是在 Java 8 中引入的,从那以后,它在几个接口和类中得到了增强,增加了许多新方法。本章全面介绍了日期-时间 API。API 由以java.time开头的包组成,它们在java.base模块中。本章中的所有示例程序都是一个jdojo.datetime模块的成员,如清单 16-1 中所声明的。
// module-info.java
module jdojo.datetime {
exports com.jdojo.datetime;
}
Listing 16-1The Declaration of a jdojo.datetime Module
日期时间 API
Java 8 引入了一个新的日期时间 API 来处理日期和时间。在本章中,我们将 Java 8 之前的日期和时间相关类称为传统日期时间 API。遗留的日期-时间 API 包括像Date、Calendar、GregorianCalendar等类。它们在java.util和java.sql包里。Date级从 JDK 成立之初就有了;其他的是在 JDK 1.1 中添加的。
为什么我们需要一个新的日期时间 API?简单的答案是,传统日期-时间 API 的设计者在两次尝试中都没有成功。举几个例子,传统日期-时间 API 的一些问题如下:
-
日期总是由两部分组成:日期和时间。如果你只需要一个没有任何时间信息的约会,你别无选择。开发人员过去在 date 对象中将时间设置为午夜,以表示纯日期,这是不正确的,原因有几个。同样的论点对于只存储时间也是有效的。
-
datetime 简单地存储为自 1970 年 1 月 1 日午夜 UTC 以来经过的毫秒数。
-
操纵日期就像你能想到的那样复杂;
Date对象中的year字段被存储为从 1900 开始的偏移量;月份从 0 到 11,而不是从 1 到 12,因为人类习惯于将它们概念化。 -
传统的 datetime 类是可变的,因此不是线程安全的。
第三次是魅力吗?这是第三次尝试提供一个正确的、强大的、可扩展的日期时间 API。然而,Java 日期时间 API 并不是免费的。如果你想充分利用它的潜力,它有一个陡峭的学习曲线。它由大约 80 个类组成。不要担心大量的课程。它们被精心设计和命名。一旦你理解了它的设计背后的思想,你就可以相对容易地理解一个类的名字和你在特定情况下需要使用的方法。作为开发人员,您需要了解大约 15 个类,以便在日常编程中有效地使用 Java 日期时间 API。
设计原则
在开始学习 Java 日期时间 API 的细节之前,您需要理解一些关于日期和时间的基本概念。日期时间 API 基于 ISO-8601 日期时间标准。一个名为 Joda-Time 的 Java datetime 框架启发了这个日期时间 API,它是在 Java 8 中添加的。如果你以前使用过 Joda-Time,你将能够很快学会这个日期-时间 API。您可以在 www.joda.org/joda-time/ 找到 Joda-Time 项目的详细信息。
日期-时间 API 区分了机器和人类使用日期和时间的方式。机器把时间看作是连续的滴答,一个以秒、毫秒等为单位的递增数字。人类使用日历系统按照年、月、日、小时、分钟和秒来处理时间。日期时间 API 有一组独立的类来处理基于机器的时间和基于日历的人类时间。它可以让你将基于机器的时间转换为基于人类的时间,反之亦然。
传统的日期时间 API 已经存在超过 15 年了。在使用现有应用程序时,您可能会遇到旧的 datetime 类。旧的 datetime 类已经过改进,可以与新的类无缝协作。当您编写新代码时,请使用新的日期-时间 API 类。当您接收遗留类的对象作为输入时,将遗留对象转换为新的 datetime 对象,并使用新的日期-时间 API。
Java 日期时间 API 主要由不可变的类组成。因为 API 是可扩展的,所以建议您尽可能创建不可变的类来扩展 API。对 datetime 对象的操作会创建一个新的 datetime 对象。这种模式使得链接方法调用变得容易。
日期-时间 API 中的类不提供公共构造器。它们允许你通过提供名为of()、ofXxx()和from()的静态工厂方法来创建它们的对象。API 使用定义良好的命名约定来命名方法。API 中的每个类都有几个方法。了解方法命名约定可以让您轻松找到适合您的目的的正确方法。我们将在单独的章节中讨论方法命名约定。
一个简单的例子
让我们看一个使用 Java 日期时间 API 处理日期和时间的例子。LocalDate类的一个实例表示没有时间的本地日期;LocalTime类的一个实例表示没有日期的当地时间;LocalDateTime类的一个实例代表一个本地日期和时间;ZonedDateTime类的一个实例用时区表示日期和时间。
A LocalDate和 a LocalTime也被称为分音 、,因为它们不代表时间线上的瞬间;他们也不知道夏令时的变化。一个ZonedDateTime代表给定时区中的一个时间点,可以转换为时间轴上的一个瞬间;它知道夏令时。例如,将凌晨 1:00 的LocalTime增加 4 个小时,将得到另一个凌晨 5:00 的LocalTime,而不管日期和地点。但是,如果您在代表芝加哥/美国时区 2014 年 3 月 9 日凌晨 1:00 的ZonedDateTime上增加 4 个小时,那么在同一时区,它将为您提供 2014 年 3 月 9 日上午 6:00 的时间,因为夏令时的原因,时钟会在当天的凌晨 2:00 向前移动 1 小时。例如,航空应用程序将使用ZonedDateTime类的实例来存储航班的出发时间和到达时间。
在 Date-Time API 中,表示日期、时间和日期时间的类有一个now()方法,分别返回当前日期、时间或日期时间。下面的代码片段创建 datetime 对象,这些对象表示日期、时间以及它们的组合(有和没有时区):
LocalDate dateOnly = LocalDate.now();
LocalTime timeOnly = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime dateTimeWithZone = ZonedDateTime.now();
A LocalDate不区分时区。同一时刻在不同时区会有不同的解读。当时间和时区对日期值的意义不重要时,如出生日期、书籍的出版日期等,使用LocalDate对象存储日期值。
您可以使用静态工厂方法of()指定日期时间对象的组件。下面的代码片段通过指定日期的年、月和日部分来创建一个LocalDate:
// Create a LocalDate representing January 12, 1968
LocalDate myBirthDate = LocalDate.of(1968, JANUARY, 12);
Tip
LocalDate只存储日期值,不存储时间和时区。当您使用静态方法now()获得一个LocalDate时,系统默认时区用于获得日期值。
清单 16-2 展示了如何获取当前日期、时间、日期时间和时区。它还展示了如何从年、月和日构造日期。您可能会得到不同的输出,因为它打印日期和时间的当前值。
// CurrentDateTime.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import static java.time.Month.JANUARY;
public class CurrentDateTime {
public static void main(String[] args) {
// Get current date, time, and datetime
LocalDate dateOnly = LocalDate.now();
LocalTime timeOnly = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime dateTimeWithZone = ZonedDateTime.now();
System.out.println("Current Date: " + dateOnly);
System.out.println("Current Time: " + timeOnly);
System.out.println("Current Date and Time: " + dateTime);
System.out.println("Current Date, Time, and Zone: " + dateTimeWithZone);
// Construct a birth date and time from datetime components
LocalDate myBirthDate = LocalDate.of(1968, JANUARY, 12);
LocalTime myBirthTime = LocalTime.of(7, 30);
System.out.println("My Birth Date: " + myBirthDate);
System.out.println("My Birth Time: " + myBirthTime);
}
}
Current Date: 2021-08-04
Current Time: 08:48:29.402753900
Current Date and Time: 2021-08-04T08:48:29.402753900
Current Date, Time, and Zone: 2021-08-04T08:48:29.403754200-05:00[America/Chicago]
My Birth Date: 1968-01-12
My Birth Time: 07:30
Listing 16-2Obtaining Current Date, Time, and Datetime and Constructing a Date
该程序使用四个类来获取本地日期、时间、日期时间和带时区的日期时间。在传统的日期-时间 API 中,只使用Calendar类就可以得到类似的结果。
日期时间 API 是全面的。它跨越了大约 80 个类和大约 1000 个方法。它允许你使用不同的刻度和不同的日历系统来表示和操作日期和时间。几个地方标准和一个通用标准(ISO-8601)已被用于计时。为了充分利用日期-时间 API,您需要了解计时的历史。接下来的几节将简要介绍使用日历系统和 ISO-8601 日期和时间标准测量时间的不同方法。如果您对这些主题有很好的理解,您可以跳过这些部分,从“探索日期-时间 API”部分继续。
计时的演变
天平是用来测量物理量的,如以米为单位的绳子的长度,以磅为单位的人的体重,以升为单位的水的体积等。在这里,米、磅和升是特定尺度上的计量单位。
我们如何测量时间?时间不是物质的东西。为了测量时间,我们将它与周期性的物理现象联系起来,例如,钟摆的摆动、地球绕轴自转、地球绕太阳公转、与原子中两个能级之间的量子跃迁有关的电磁信号的振荡等。因此,时标是事件的排列,用来定义持续时间。
在古代,由于地球绕轴旋转而产生的日出日落等事件被用作时间刻度;时间刻度的单位是一天。两次连续日出之间的持续时间计为 1 天。
随着人类文明的进步,计时设备被开发出来。其中一些是
-
基于太阳位置的日晷
-
基于钟摆周期性运动的机械钟
-
最后,基于铯-133 原子特性的原子钟
时钟是一种计时装置,由两部分组成:频率标准和计数器。时钟中的频率标准是获得等间隔周期事件的组件,以测量所需时间间隔的长度。计数器(也称为累加器或加法器)计算周期性事件的发生次数。例如,在摆钟中,一个完整的钟摆周期的出现表示 1 秒的时间间隔,齿轮计算秒数,时钟的表面显示时间。即使在古代,也有两部分时钟计时的概念。根据日出和日落的周期性事件,地球的旋转提供了时钟的第一个组成部分;日历提供了时钟的第二个组成部分来计算日、月和年。
基于地球的自转,使用了几种时间尺度,称为世界时(UT)。地球围绕地轴和太阳的运动是不规则的。由于地球运动的不规则性,一天的长度一天比一天长,一年中的天数一年比一年多。太阳日(也称为视太阳日或真太阳日)是通过在当地时间中午连续观察两次太阳经过来测量的时间长度。如果你使用一个完美的时钟每天中午在当地子午线上观察太阳,你会发现,在一年中,太阳在天空中的位置在当地子午线东西方向变化大约 4 度(大约 16 分钟)。这意味着,在一年中的某一天,时钟显示的正午和太阳通过当地子午线的时间之间可能有多达 16 分钟的差异。由于地球自转轴相对于其轨道平面和围绕太阳的椭圆轨道的倾斜,时钟时间和太阳时间之间的差异被称为时间方程。用太阳日测量的时间称为视太阳时。
通过对太阳时进行校正以解释时间方程式而获得的时间被称为世界时零(UT0)或平均太阳时。穿过英国格林威治的本初子午线(零度经度)的午夜,对于 UT0 定义为 00 小时。第二个被定义为平均太阳日的 1/86400。
地球相对于其旋转轴的摆动被称为极移。经极移校正的 UT0 产生另一个时间标度,称为 UT1。地球的旋转速度是不均匀的。UT1 根据地球自转速度的季节变化进行了修正,产生了另一个时间尺度,称为 UT2。
地球不规则的自转速率导致了另一种时间尺度,称为星历表时间(ET)。ET 是基于地球绕太阳一周的周期和其他天体的运动。在 ET 标度上,星历秒被定义为 1900 年 1 月 0 日 12 小时星历时间的热带年的分数 1/31556925.9747。20 世纪 80 年代初,地球动力学时间(TDT)和重心动力学时间(TDB)取代了 ET。
国际原子时(又称为 TAI,法语名称 Temps Atomique International)是一种原子时标度。原子秒,TAI 标度中的时间单位,被定义为对应于铯-133 原子基态的两个超精细能级之间跃迁的 9192631770 个辐射周期的持续时间。1967 年,原子秒的定义变成了国际单位制(SI)秒的定义。国际度量衡局(BIPM)是原子时的官方计时器。有 65 个实验室,230 多个原子钟为 TAI 标度做出贡献。对 TAI 有贡献的每个时钟基于其性能被分配一个加权因子。所有起作用的原子钟的加权平均值给出了 TAI。
为什么我们要用很多原子钟来测量 TAI?一个时钟可能会出现故障并停止计时。甚至原子钟也会受到环境变化的影响。为了避免这样的失败和不准确,几个原子钟被用来跟踪 TAI。
1972 年 1 月 1 日,协调世界时(UTC)被采纳为全世界所有民用的官方时间尺度。UTC 和原子钟的运行速度相同。当 BIPM 在泰表上计算秒时,天文学家继续利用地球自转来测量时间。天文时间与 UTC 进行比较,如果两者相差超过 0.9 秒,就会在 UTC 上加上或减去一个闰秒,以使 UT0 和 UTC 的时间刻度尽可能接近。国际地球自转服务(IERS)决定将闰秒引入 UTC。
在任何时候,UTC 都与 TAI 相差整数秒。UTC 和 TAI 之间的关系如下:
UTC = TAI - (algebraic sum of leap seconds)
截至 2021 年 8 月,UTC 增加了 37 个闰秒。到目前为止,还没有从 UTC 减去闰秒。因此,在 2021 年 8 月,直到引入另一个闰秒,UTC 和 TAI 的关系如下:
UTC = TAI - 37
你可能会想,因为我们一直在给 UTC 加闰秒,所以 UTC 应该在 TAI 之前。这不是真的。将闰秒添加到 UTC 会使该小时在 UTC 刻度上变成 61 秒,而不是 60 秒。TAI 是连续的时间尺度;它一直在滴答作响。当 UTC 完成一小时的第 61 秒时,TAI 已移动到下一小时的第一秒。因此,当添加闰秒时,UTC 落后于 TAI。类似的逻辑,但顺序相反,适用于从 UTC 减去闰秒。如果在未来的任何时候,加上和减去 UTC 的闰秒变得相等,UTC 和 TAI 将读取相同的时间。
UTC 代表地球上本初子午线(经度零度)的时间,它穿过英国的格林威治。UTC 基于 24 小时制,每天从午夜 00 点开始。UTC 也被称为祖鲁时间。ISO-8601 标准使用字母 Z 作为日期指示符的 UTC 例如,15 小时后第 19 分 23 秒的 UTC 被写成 15:19:23Z。
您与 UTC 的合作还没有结束!我们讨论另外两个版本的 UTC:简化的 UTC 和带平滑闰秒的 UTC(UTC-SLS)。
人类习惯于按照 24 小时周期来理解太阳日:每小时由 60 分钟组成,每分钟由 60 秒组成。一个太阳日由 86400 秒组成。在 UTC 标度上,由于闰秒,一个太阳日也可能由 86399 或 86401 秒组成。为了让普通用户更容易理解,大多数计算机系统忽略了 UTC 刻度上的闰秒。忽略闰秒的 UTC 刻度称为简化的 UTC 刻度。
Tip
为了满足大多数用户的期望,新的 Java 日期时间 API 使用简化的 UTC,其中闰秒被忽略,使得所有的日子都有相同的 86400 秒。
当 UTC 加上或减去一个闰秒时,会在一天结束时的时间刻度中产生 1 秒的间隙或重叠。UTC-SLS 是处理 UTC 闰秒的建议标准。UTC-SLS 不是在一天结束时引入闰秒,而是建议通过将时钟的速率改变 0.1%,在一天的最后 1000 秒内进行 1 秒的平滑调整。在 UTC 上添加闰秒的一天,UTC-SLS 将使这一天的最后 1000 秒长 1001 毫秒,从而将 UTC-SLS 时钟的速率从 23:43:21 降低 0.1%到 24:00:00。在 UTC 上添加闰秒的一天,UTC-SLS 会将这一天的最后 1000 秒设为 999 毫秒,从而将 UTC-SLS 时钟的速率从 23:43:19 增加 0.1%到 24:00:00。
最后,有人提议通过取消 UTC 的闰秒来实现统一而单调的民用时间。有些人还提议用闰时来代替 UTC 闰秒!
时区和夏令时
当世界协调时 2021 年 4 月 20 日午夜,印度新德里和美国芝加哥当地时间是几点?印度新德里时间 2021 年 4 月 20 日早上 5 点半,美国芝加哥时间 2021 年 4 月 19 日晚上 7 点。我们如何确定一个地方的当地时间?全世界只有一次不是很好吗?如果是 UTC 午夜,那么世界各地都是午夜。也许这在过去会是一个好主意,因为随着时间的推移,人类的大脑能够通过实践来适应新的想法。某个地区的本地时间设置为一天从午夜 00 点开始。因此,00 小时在新德里和芝加哥都是午夜。
从地理上看,世界可以分为 24 个经度带,每个经度带覆盖从本初子午线开始的 15 度经度范围;每个波段代表一个 1 小时的时区。时区所覆盖的区域将遵守相同的时间。
人类在政治上的分裂多于地理上的分裂。在这个世界上,我们的政治分歧总是压倒地理上的相似性!有时,一条假想的分隔两个国家或州的边界使人们在边界的每一边观察不同的时间。
实际上,时区是根据政治区域划分的:国家和国家内部的区域。每个时区的本地时间都是 UTC 的一个偏移量。时差,即一个时区中 UTC 和本地时间的差异,称为时区时差。本初子午线以东的区域使用正的区域偏移。负区域偏移用于本初子午线以西的区域。时区偏移量以小时和分钟表示,如+5:30 小时、–10:00 小时等。例如,印度使用+5:30 小时的时区偏移量;因此,您可以在 UTC 上加 5 小时 30 分钟,得到印度当地时间。您可能认为时区偏移量的值对于一个时区是固定的。唉,要是我们这些文明先进的人类在守时方面也这么简单就好了!
有些国家有不止一个时区。例如,美国有五个时区:阿拉斯加、太平洋、山地、中部和东部。印度只有一个时区。在美国,当阿拉巴马州莫比尔(中部时区)的当地时间是早上 7:00 时,加利福尼亚州洛杉矶(太平洋时区)的当地时间是早上 5:00。印度的每个地方,因为只有一个时区,所以观察时间相同。
一些时区的时差在一年中会有变化。例如,在美国芝加哥(称为中部时区),夏令时为–5:00,冬令时为–6:00。大多数国家使用固定的时区偏移量。例如,印度使用+5:30 小时固定时区时差。时区的规则由政府决定,这些规则控制着时区偏移量变化的时间以及变化的幅度。这些规则被称为时区规则。
时区偏移量范围在+14:00 小时到–12:00 小时之间。如果一天只有 24 小时,我们如何将+14 作为时区偏移量?+14 到–12 的范围内,一天 26 小时!请注意,一些国家由几个小岛组成,这些小岛位于国际日期变更线的两侧,相隔一天。这给这些国家的岛屿之间的官方交流带来了问题,因为它们只有四个共同的工作日。他们将时区偏移量扩展到 12:00 之后,从而移动了他们国家的国际日期变更线,使整个国家都位于国际日期变更线的一侧。使用+13:00 和+14:00 时区时差的国家有基里巴斯(发音为“kirbas”)、萨摩亚和托克劳。
夏令时用于在春天通过将时钟向前拨(通常是 1 小时)来更好地利用晚上的日光。秋天的时候,时钟会拨回相同的时间。一年中观察夏令时的时间称为夏季;一年的另一部分被称为冬季。并非所有国家都采用夏令时。一个国家的政府决定该国(或一个国家内的某些地方)是否采用夏令时;如果是这样,政府决定日期和时间向前和向后移动时钟。例如,美国采用夏令时的时区在当地时间 2021 年 3 月 11 日凌晨 2:00 将时钟向前拨了 1 小时,因此产生了 1 小时的时差。请注意,在 2021 年 3 月 11 日,当地时间凌晨 2:00 到 3:00 在美国的那些区域不存在。在秋季,当时钟向后移动时,会产生等量的时间重叠。印度和泰国是不采用夏令时的两个国家。DST 每年两次更改 DST 观测位置相对于 UTC 的时区偏移量。
日历系统
人类使用日历来处理时间。日历中使用的时间单位是年、月、日、小时、分钟和秒。从这个意义上说,日历是一个追踪时间的系统,包括过去和未来,对人类社会、政治、法律、宗教和其他目的都有意义。
通常,日历系统不记录一天中的时间;它以日、月、年为单位工作。广义地说,在历法中,一天是基于地球绕轴自转,一个月是基于月球绕地球公转,一年是基于地球绕太阳公转。有时,日历系统是基于周的,而周是基于非天文周期的。
纵观人类历史,已知不同的文明使用不同的日历系统。大多数古代历法系统是基于由太阳运动、月亮运动或者两者产生的天文周期,因此产生了三种类型的历法系统:太阳历、阴历和日月历。
阳历的设计与回归年(也称为太阳年)一致,回归年是春分点之间的平均间隔。当太阳中心与地球赤道在同一平面时,一年出现两次春分。术语“春分”的意思是相等的夜晚;在春分点,白天和黑夜几乎一样长。春分发生在春季的 3 月 21 日左右;秋分发生在 9 月 22 日左右的秋季。阳历是阳历的一个例子,阳历是世界上最常用的民用日历。
阴历是基于月相周期的。它与回归年不一致。在一年中,它会从一个热带年漂移 11-12 天。一个农历大约需要 33 年才能赶上一个回归年,再漂移 33 年。太阴月,也称为合月,是新月之间的时间间隔,等于 29 天,12 小时,44 分钟和 2.8 秒。伊斯兰历是阴历的一个例子。
阴阳历像阴历一样根据月相周期来计算月份。然而,它每 2 年或 3 年插入一个月,以保持自己与回归年一致。佛教、印度教、中国和希伯来历法都是阴阳历的例子。
儒略历
儒略历是一种阳历,由朱利叶斯·凯撒于公元 45 年引入。它被欧洲文明广泛使用,直到+1582 年公历被引入。
普通的一年由 365 天组成。每四年,在 2 月 28 日和 3 月 1 日之间插入一天,被指定为 2 月 29 日,使一年有 366 天,这被称为闰年。公元前 1 年被认为是闰年。儒略历年的平均长度是 365.25 天,接近当时已知的回归年的长度。
一年由 12 个月组成。月份的长度是固定的。表 16-1 列出了儒略历中月份的顺序、名称和天数。
表 16-1
儒略历(和公历)中月份的顺序、名称和天数
|命令
|
月份名称
|
天数
| | --- | --- | --- | | one | 一月 | Thirty-one | | Two | 二月 | 28(闰年 29) | | three | 三月 | Thirty-one | | four | 四月 | Thirty | | five | 五月 | Thirty-one | | six | 六月 | Thirty | | seven | 七月 | Thirty-one | | eight | 八月 | Thirty-one | | nine | 九月 | Thirty | | Ten | 十月 | Thirty-one | | Eleven | 十一月 | Thirty | | Twelve | 十二月 | Thirty-one |
公历
公历是世界上最广泛使用的民用日历。它遵循儒略历一年中的月数和月中的天数的规则。然而,它改变了计算闰年的规则:如果一年能被 4 整除,那么它就是闰年。能被 100 整除的一年不是闰年,除非它也能被 400 整除。
例如,4、8、12、400 和 800 被称为闰年,1、2、3、5、300 和 100 被称为平年。0 年(公元前 1 年)被认为是闰年。有了闰年的新定义,公历一年的平均长度是 365.2425 天,非常接近回归年的长度。公历每 400 年重复一次。如果你把你的纸质日历保存到 2014 年,你的第 n 个曾孙将能够在 2414 年再次使用它!
公历是在 1582 年 10 月 15 日星期五引入的。根据现存的儒略历,公历开始的前一天是 1582 年 10 月 4 日星期四。注意,公历的引入没有影响工作日的循环;但是,它在两个日历之间留下了 10 天的不连续,这被称为转换。转换前的日期是儒略历日期,转换后的日期是公历日期,转换期间的日期不存在。
公历在 1582 年 10 月 15 日之前并不存在。我们如何在公历开始之前给事件指定日期?应用于无效日期的公历被称为预测公历。因此,1582 年 10 月 14 日存在于公历中,与儒略历中的 1582 年 10 月 4 日相同。
为什么公历的第一天是 1582 年 10 月 15 日星期五,而不是 1582 年 10 月 5 日星期五?根据道吉特的说法,在儒略历中,基督教节日复活节的日期是基于 3 月 21 日是春分的假设计算出来的。后来才知道,春分从 3 月 21 日开始一直在漂移;因此,复活节的日期偏离了季节性的春天。为了保持复活节的日期与春天同步,公历的开始日期调整了 10 天,所以 1583 年及以后的春分大约在 3 月 21 日。
Tip
儒略历和公历的主要区别在于确定闰年的规则。阳历中一年的平均长度比儒略历中的更接近于回归年的长度。
日期时间的 ISO-8601 标准
新的日期时间 API 广泛支持 ISO-8601 标准。本节旨在对 ISO-8601 标准中包含的日期时间组件及其文本表示进行简要而有限的概述。ISO-8601 中的日期时间由三部分组成:日期、时间和时区偏移量,它们以下列格式组合:
[date]T[time][zone offset]
日期组件由三个日历字段组成:年、月和日。日期中的两个字段由连字符分隔:
year-month-day
例如,2021-04-30 代表 2021 年 4 月的第 30 天。
有时,人们处理可能不包含完整信息的日期来识别日历中的某一天。例如,12 月 25 日作为圣诞节是有意义的,不需要指定日期中的年份部分。为了在日历中标识特定的圣诞节,我们还必须指定年份。缺少某些部分的日期被称为部分日期。2021,2021-05,- 05-29 等等。都是偏音的例子。ISO-8601 只允许从右端省略日期中的部分。也就是说,它允许省略日或月和日。日期-时间 API 允许三种类型的部分:年、年-月和月-日。
日期和时间部分由“T”字符分隔。时间组件由字段组成:小时、分钟和秒。冒号分隔时间组件中的两个字段。时间以这种格式表示:
hour:minute:second
ISO-8601 使用 24 小时计时系统。小时元素可以在 00 到 24 之间。小时 24 用于表示日历日的结束。minute 元素的值范围从 00 到 59。第二元素的范围可以从 00 到 60。第二元素的值 60 表示正闰秒。例如,15:20:56 表示当地时间午夜后 15 小时 20 分 56 秒。当允许降低精度时,秒或秒和分元素可从时间中省略。例如,15:19 表示 15 小时后的 19 分钟,07 表示从午夜开始的 07 小时。
午夜是一个日历日的开始。用 00:00:00 或 00:00 表示。日历日的开始与前一个日历日的结束相一致。因此,日历日的午夜也可以用 24:00:00 或 24:00 来表示。
如果指定的日期、时间或日期时间不带时区偏移量,则分别被视为本地日期、时间或日期时间。本地日期、时间和日期时间的示例分别是 2021-05-01、13:52:05 和 2021-05-01T13:52:05。
使用时区偏移量,可以表示相对于一天中的 UTC 的时间部分。时区偏移量表示本地时间和 UTC 之间的固定差值。它以加号或减号(+或–)开头,后跟小时和分钟元素,用冒号分隔。区域偏移的一些示例有+05:30、–06:00、+10:00、+5:30 等。字符 Z 用作时区偏移量指示符来表示一天中的 UTC 时间。例如,10:20:40Z 表示一天中上午 10 点 20 分 40 秒的 UTC12:20:40+2:00 表示当地时间下午 12 点 20 分 40 秒,比 UTC 早 2 个小时。10:20:40Z 和 12:20:40+2:00 这两个时间都表示相同的时间点。
Tip
ISO-8601 规定了在时间表示中使用相对于 UTC 分量的固定时区偏移量的标准。回想一下,对于采用夏令时的时区,时区偏移量可能会有所不同。除了 ISO-8601 标准,日期时间 API 还支持可变时区偏移量。
下面是一个完全指定了所有三个部分的日期时间的示例:
2021-05-01T16:30:00-06:00
此日期时间表示 2021 年 5 月 1 日,比 UTC 晚 6 个小时的午夜后 16 小时 30 分钟。
ISO-8601 包括几个其他日期和时间相关概念的标准,如瞬间、持续时间、周期、时间间隔等。日期-时间 API 提供了一些类,这些类的对象直接表示大多数(但不是全部)ISO 日期和时间概念。
日期-时间 API 中所有日期和时间类的toString()方法以 ISO 格式返回日期和时间的文本表示。该 API 包括允许您以非 ISO 格式格式化日期和时间的类。
ISO 标准包括用于指定称为持续时间的时间量的格式。ISO-8601 将持续时间定义为非负量。但是,日期-时间 API 也允许将负数量视为持续时间。表示持续时间的 ISO 格式如下:
PnnYnnMnnDTnnHnnMnnS
在这种格式中,P是持续时间指示符;nn表示一个数字;Y、M、D、H、M、S分别表示年、月、日、时、月、秒;并且T是时间指示器,仅当持续时间涉及小时、分钟和秒时才出现。下面是一些持续时间的文本表示的例子。行内注释描述了持续时间:
P12Y // A duration of 12 years
PT15:30 // A duration of 15 hours and 30 minutes
PT20S // A duration of 20 seconds
P4Y2MT30M // A duration of 4 years 2 months and 30 minutes
Tip
日期-时间 API 提供了Duration和Period类来处理大量的时间。一个Duration代表机器尺度时间线上的时间量。一个Period代表人类尺度时间线上的时间量。
探索日期-时间 API
起初,探索日期-时间 API 是令人生畏的,因为它包含许多具有许多方法的类。学习方法的命名约定将极大地帮助理解 API。日期-时间 API 经过精心设计,以保持类名及其方法的一致性和直观性。以相同前缀开头的方法做类似的工作。例如,类中的of()方法被用作静态工厂方法来创建该类的对象。
日期-时间 API 的所有类、接口和枚举都在java.time包及其四个子包中,如表 16-2 中所列。
表 16-2
日期时间 API 的包和子包
|包裹
|
描述
|
| --- | --- |
| java.time | 包含常用的类。LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Period、Duration和Instant类都在这个包里。这个包中的类是基于 ISO 标准的。 |
| java.time.chrono | 包含支持非 ISO 日历系统的类,例如,回历、泰国佛教日历等。 |
| java.time.format | 包含用于格式化和解析日期和时间的类。 |
| java.time.temporal | 包含用于访问日期和时间组件的类。它还包含类似日期时间调整器的类。 |
| java.time.zone | 包含支持时区和区域规则的类。 |
下面几节解释了日期-时间 API 中方法名使用的前缀及其含义和示例。
ofXxx()方法
日期-时间 API 中的类不提供公共构造器来创建它们的对象。它们允许您通过名为“of”或“ofXxx” (where Xxx is replaced by a description of the parameters)”的静态工厂方法来创建对象。下面的代码片段显示了如何创建LocalDate类的对象:
LocalDate ld1 = LocalDate.of(2021, 5, 2); // 2021-05-02
LocalDate ld2 = LocalDate.of(2021, Month.JULY, 4); // 2021-07-04
LocalDate ld3 = LocalDate.ofEpochDay(2002); // 1975-06-26
LocalDate ld4 = LocalDate.ofYearDay(2014, 40); // 2014-02-09
from()方法
from()方法是一个静态工厂方法,类似于of()方法,用于从指定的参数中派生出一个日期时间对象。与of()方法不同,from()方法需要对指定的参数进行数据转换。
为了理解一个from()方法做什么,可以把它想象成一个deriveFrom()方法。使用from()方法,从指定的参数中派生出一个新的 datetime 对象。下面的代码片段展示了如何从一个LocalDateTime派生出一个LocalDate:
LocalDateTime ldt = LocalDateTime.of(2021, 5, 2, 15, 30); // 2021-05-02T15:30
LocalDate ld = LocalDate.from(ldt); // 2021-05-02
withXxx()方法
日期-时间 API 中的大多数类都是不可变的。他们没有setXxx()方法。如果您想要更改 datetime 对象的一个字段,例如,日期中的年份值,您需要寻找一个带有前缀“with”的方法一个withXxx()方法返回指定字段被改变的对象的副本。
假设您有一个LocalDate对象,并且您想要更改它的年份。你需要使用LocalDate类的withYear(int newYear)方法。下面的代码片段显示了如何从另一个LocalDate获得一个LocalDate,其中的年份发生了变化:
LocalDate ld1 = LocalDate.of(2021, Month.MAY, 2); // 2021-05-02
LocalDate ld2 = ld1.withYear(2014); // 2014-05-02
您可以通过链接withXxx()方法调用来更改多个字段,从而从现有的LocalDate中获得新的LocalDate。下面的代码片段通过更改年份和月份,从现有的LocalDate创建一个新的LocalDate:
LocalDate ld3 = LocalDate.of(2021, 5, 2); // 2021-05-02
LocalDate ld4 = ld3.withYear(2024)
.withMonth(7); // 2024-07-02
getXxx()方法
一个getXxx()方法返回对象的指定元素。例如,LocalDate类中的getYear()方法返回日期中的年份部分。以下代码片段显示了如何从LocalDate对象中获取年、月和日:
LocalDate ld = LocalDate.of(2021, 5, 2);
int year = ld.getYear(); // 2021
Month month = ld.getMonth(); // Month.MAY
int day = ld.getDayOfMonth(); // 2
toXxx()方法
一个toXxx()方法将一个对象转换成一个相关的Xxx类型。例如,LocalDateTime类中的toLocalDate()方法返回一个LocalDate对象,其日期在原始的LocalDateTime对象中。下面是一些使用toXxx()方法的例子:
LocalDate ld = LocalDate.of(2021, 8, 29); // 2021-08-29
// Convert the date to epoch days. The epoch days is the number of days from
// 1970-01-01 to a date. A date before 1970-01-01 returns a negative integer.
long epochDays = ld.toEpochDay(); // 18868
// Convert a LocalDateTime to a LocalTime using the toLocalTime() method
LocalDateTime ldt = LocalDateTime.of(2021, 8, 29, 16, 30);
LocalTime lt = ldt.toLocalTime(); // 16:30
atXxx()方法
atXxx()方法允许您通过提供一些额外的信息,从现有的日期时间对象构建一个新的日期时间对象。对比使用atXxx()方法和withXxx()方法;前者允许您通过提供附加信息来创建新类型的对象,而后者允许您通过更改对象的字段来创建对象的副本。
假设您有日期 2021 年 5 月 2 日。如果您想创建一个新的日期 2021-07-02(月份改为 7),您可以使用一个withXxx()方法。如果您想要创建一个日期时间 2021-05-02T15:30(通过添加时间 15:30),您将使用一个atXxx()方法。下面是一些使用atXxx()方法的例子:
LocalDate ld = LocalDate.of(2021, 5, 2); // 2021-05-02
LocalDateTime ldt1 = ld.atStartOfDay(); // 2021-05-02T00:00
LocalDateTime ldt2 = ld.atTime(15, 30); // 2021-05-02T15:30
atXxx()方法支持构建器模式。以下代码片段显示了如何使用生成器模式来构建本地日期:
// Use a builder pattern to build a date 2021-05-22
LocalDate d1 = Year.of(2021).atMonth(5).atDay(22);
// Use an of() factory method to build a date 2021-05-22
LocalDate d2 = LocalDate.of(2021, 5, 22);
plusXxx()和 minusXxx()方法
一个plusXxx()方法通过添加一个指定的值来返回一个对象的副本。例如,LocalDate类中的plusDays(long days)方法通过添加指定的天数来返回LocalDate对象的副本。
一个minusXxx()方法通过减去一个指定的值返回一个对象的副本。例如,LocalDate类中的minusDays(long days)方法通过减去指定的天数来返回LocalDate对象的副本:
LocalDate ld = LocalDate.of(2021, 5, 2); // 2021-05-02
LocalDate ld1 = ld.plusDays(5); // 2021-05-07
LocalDate ld2 = ld.plusMonths(3); // 2021-08-02
LocalDate ld3 = ld.plusWeeks(3); // 2021-05-23
LocalDate ld4 = ld.minusMonths(7); // 2011-10-02
LocalDate ld5 = ld.minusWeeks(3); // 2021-04-11
multipliedBy()、dividedBy()和 negated()方法
乘法、除法和求反在日期和时间上没有意义。它们适用于表示时间量的日期时间类型,如Duration和Period。持续时间和周期可以加减。日期-时间 API 支持负的持续时间和周期:
Duration d = Duration.ofSeconds(200); // PT3M20S (3 minutes and 20 seconds)
Duration d1 = d.multipliedBy(2); // PT6M40S (6 minutes and 40 seconds)
Duration d2 = d.negated(); // PT-3M-20S (-3 minutes and -20 seconds)
瞬间和持续时间
时间线(或时间轴)是时间流逝的数学表示,即沿着唯一轴的瞬时事件。一个机器尺度的时间线用一个递增的数字来表示时间的流逝,如图 16-1 所示。
图 16-1
代表机器尺度时间流逝的时间线
瞬间是时间线上代表唯一时刻的点。一个历元是时间线上的一个瞬间,用作参考点(或原点)来测量其他瞬间。
Instant类的一个对象代表时间轴上的一个瞬间。它使用时间轴以纳秒的精度表示简化的 UTC。也就是说,时间线上两个连续瞬间之间的时间间隔(或持续时间)是一纳秒。时间轴使用 1970-01-01T00:00:00Z 作为纪元。历元之后的瞬间具有正值;纪元前的瞬间具有负值。该时期的瞬间被赋予零值。
有不同的方法可以创建一个Instant类的实例。使用它的now()方法,您可以使用系统默认时钟获得当前时刻:
// Get the current instant
Instant i1 = Instant.now();
您可以使用来自 epoch 的不同单位的时间量来获得Instant类的实例。下面的代码片段创建了一个Instant对象来表示纪元后的 19 秒,即 1970-01-01T00:00:19Z:
// An instant: 19 seconds from the epoch
Instant i2 = Instant.ofEpochSecond(19);
Duration类的对象表示时间线上两个瞬间之间的时间量。Duration类支持定向持续时间。也就是说,它允许正持续时间和负持续时间。图 16-1 用箭头显示持续时间,表示它们是定向持续时间。
您可以使用ofXxx()静态工厂方法之一创建Duration类的实例:
// A duration of 2 days
Duration d1 = Duration.ofDays(2);
// A duration of 25 minutes
Duration d2 = Duration.ofMinutes(25);
Tip
Instant 类的toString()方法以 ISO-8601 格式yyyy-MM-ddTHH:mm:ss.SSSSSSSSSZ返回Instant的文本表示。Duration类的toString()方法以PTnHnMnS格式返回持续时间的文本表示,其中n是小时数、分钟数或秒数。
你能用瞬间和持续时间做什么?通常,它们用于记录两个事件之间的时间戳和经过时间。可以比较两个瞬间,从而知道一个瞬间发生在另一个瞬间之前还是之后。您可以在一个瞬间上加上(或减去)一个持续时间来获得另一个瞬间。将两个持续时间相加会产生另一个持续时间。日期-时间 API 中的类是Serializable。您可以使用Instant在数据库中存储时间戳。
Instant和Duration类分别存储它们值的秒和纳秒部分。Duration类有getSeconds()和getNano()方法,而Instant类有getEpochSecond()和getNano()方法来获取这两个值。下面是一个获取Instant的秒和纳秒的例子:
// Get the current instant
Instant i1 = Instant.now();
// Get seconds and nanoseconds
long seconds = i1.getEpochSecond();
int nanoSeconds = i1.getNano();
System.out.println("Current Instant: " + i1);
System.out.println("Seconds: " + seconds);
System.out.println("Nanoseconds: " + nanoSeconds);
(You may get a different output.)
Current Instant: 2021-08-22T00:12:42.337685118Z
Seconds: 1629591162
Nanoseconds: 337685118
清单 16-3 展示了一些可以在瞬间和持续时间执行的操作的使用。
// InstantDurationTest.java
package com.jdojo.datetime;
import java.time.Duration;
import java.time.Instant;
public class InstantDurationTest {
public static void main(String[] args) {
Instant i1 = Instant.ofEpochSecond(20);
Instant i2 = Instant.ofEpochSecond(55);
System.out.println("i1:" + i1);
System.out.println("i2:" + i2);
Duration d1 = Duration.ofSeconds(55);
Duration d2 = Duration.ofSeconds(-17);
System.out.println("d1:" + d1);
System.out.println("d2:" + d2);
// Compare instants
System.out.println("i1.isBefore(i2):" + i1.isBefore(i2));
System.out.println("i1.isAfter(i2):" + i1.isAfter(i2));
// Add and subtract durations to instants
Instant i3 = i1.plus(d1);
Instant i4 = i2.minus(d2);
System.out.println("i1.plus(d1):" + i3);
System.out.println("i2.minus(d2):" + i4);
// Add two durations
Duration d3 = d1.plus(d2);
System.out.println("d1.plus(d2):" + d3);
}
}
i1:1970-01-01T00:00:20Z
i2:1970-01-01T00:00:55Z
d1:PT55S
d2:PT-17S
i1.isBefore(i2):true
i1.isAfter(i2):false
i1.plus(d1):1970-01-01T00:01:15Z
i2.minus(d2):1970-01-01T00:01:12Z
d1.plus(d2):PT38S
Listing 16-3Using Instant and Duration Classes
Java 9 中向Duration类添加了几个有用的方法,这些方法可以分为以下三类:
-
方法将一个持续时间除以另一个持续时间
-
获取特定时间单位的持续时间的方法和获取持续时间的特定部分(如天、小时、秒等)的方法。
-
将持续时间截断为特定时间单位的方法
在接下来的章节中,我们将展示使用这些方法的例子。在示例中,我们使用了 23 天 3 小时 45 分 30 秒的持续时间。下面的代码片段将它创建为一个Duration对象,并将它的引用存储在一个名为compTime的变量中:
// Create a duration of 23 days, 3 hours, 45 minutes, and 30 seconds
Duration compTime = Duration.ofDays(23)
.plusHours(3)
.plusMinutes(45)
.plusSeconds(30);
System.out.println("Duration: " + compTime);
Duration: PT555H45M30S
将天数乘以 24 转换为小时数后,如输出所示,该持续时间表示 555 小时 45 分 30 秒。
将一段时间除以另一段时间
此类别中只有一种方法:
long dividedBy(Duration divisor)
dividedBy()方法允许您将一个持续时间除以另一个持续时间。它返回特定的divisor在方法被调用的持续时间内出现的次数。要知道这个持续时间有多少个整周,您可以调用使用 7 天作为持续时间的dividedBy()方法。以下代码片段向您展示了如何计算持续时间中的整天数、周数和小时数:
long wholeDays = compTime.dividedBy(Duration.ofDays(1));
long wholeWeeks = compTime.dividedBy(Duration.ofDays(7));
long wholeHours = compTime.dividedBy(Duration.ofHours(7));
System.out.println("Number of whole days: " + wholeDays);
System.out.println("Number of whole weeks: " + wholeWeeks);
System.out.println("Number of whole hours: " + wholeHours);
Number of whole days: 23
Number of whole weeks: 3
Number of whole hours: 79
转换和检索持续时间部分
在这个类别的Duration类中有几个方法:
-
long toDaysPart() -
long toDays() -
int toHoursPart() -
long toHours() -
int toMillisPart() -
long toMillis() -
int toMinutesPart() -
long toMinutes() -
int toNanosPart() -
long toNanos() -
int toSecondsPart() -
long toSeconds()
Duration类包含两组方法。它们被命名为toXxx()和toXxxPart(),其中Xxx可能是Days、Hours、Minutes、Seconds、Millis、Nanos。
名为toXxx()的方法将持续时间转换为Xxx时间单位,并返回整个部分。名为toXxxPart()的方法将持续时间分解为days:hours:minutes:seconds:millis:nanos部分,并从中返回Xxx部分。在本例中,toDays()会将持续时间转换为天数,并返回整个部分,即 23。toDaysPart()将持续时间分解为23Days:3Hours:45Minutes:30Seconds:0Millis:0Nanos并返回第一部分,即 23。让我们将相同的规则应用于toHours()和toHoursPart()方法。toHours()方法将持续时间转换为小时,并返回整数小时数,即 555。toHoursPart()方法将持续时间分成几部分,并返回小时部分,即 3。以下代码片段向您展示了几个示例:
System.out.println("toDays(): " + compTime.toDays());
System.out.println("toDaysPart(): " + compTime.toDaysPart());
System.out.println("toHours(): " + compTime.toHours());
System.out.println("toHoursPart(): " + compTime.toHoursPart());
System.out.println("toMinutes(): " + compTime.toMinutes());
System.out.println("toMinutesPart(): " + compTime.toMinutesPart());
Duration: PT555H45M30S
toDays(): 23
toDaysPart(): 23
toHours(): 555
toHoursPart(): 3
toMinutes(): 33345
toMinutesPart(): 45
截断持续时间
这个类别的Duration类中只有一个方法:
Duration truncatedTo(TemporalUnit unit)
truncatedTo()方法返回持续时间的副本,其概念性时间单位小于指定的被截断的unit。指定的时间单位必须小于或等于DAYS。指定大于DAYS的时间单位,如WEEKS和YEARS,会引发运行时异常。
Tip
一个truncatedTo(TemporalUnit unit)方法也存在于LocalTime和Instant类中。
以下代码片段向您展示了如何使用此方法:
System.out.println("Truncated to DAYS: " + compTime.truncatedTo(ChronoUnit.DAYS));
System.out.println("Truncated to HOURS: " + compTime.truncatedTo(ChronoUnit.HOURS));
System.out.println("Truncated to MINUTES: " + compTime.truncatedTo(ChronoUnit.MINUTES));
Truncated to DAYS: PT552H
Truncated to HOURS: PT555H
Truncated to MINUTES: PT555H45M
持续时间为23Days:3Hours:45Minutes:30Seconds:0Millis:0Nanos。当您将其截断为DAYS时,所有小于天的部分都被丢弃,它返回 23 天,与输出中显示的 552 小时相同。当您截断到HOURS时,它会丢弃所有小于小时的部分,并返回 555 小时。将其截断为MINUTES会保留最多分钟的部分,并丢弃所有更小的部分,如秒和毫秒。
人类尺度时间
在上一节中,我们讨论了使用Instant和Duration类,它们的实例更适合处理机器时间。人类用年、月、日、时、分、秒等字段来处理时间。回想一下以下用于指定日期和时间的 ISO-8601 格式:
[date]T[time][zone offset]
日期-时间 API 提供了几个类,如表 16-3 中所列,用于表示所有字段及其人类时间的组合。类的“组件”列中的“是”或“否”指示该类的实例是否存储该组件。我们将很快详细讨论所有这些类。
表 16-3
人类尺度的日期和时间类及其组成部分
|类别名
|
日期
|
时间
|
区域偏移
|
区域规则
|
| --- | --- | --- | --- | --- |
| LocalDate | 是 | 不 | 不 | 不 |
| LocalTime | 不 | 是 | 不 | 不 |
| LocalDateTime | 是 | 是 | 不 | 不 |
| OffsetTime | 不 | 是 | 是 | 不 |
| OffsetDateTime | 是 | 是 | 是 | 不 |
| ZonedDateTime | 是 | 是 | 是 | 是 |
| ZoneOffset | 不 | 不 | 是 | 不 |
| ZoneId | 不 | 不 | 是 | 是 |
ZoneOffset 类
ZoneOffset类的一个实例表示相对于 UTC 时区的固定时区偏移量,例如+05:30、–06:00 等。时区不同于 UTC 是一段时间。由于观察到的夏令时,A ZoneOffset不知道时区偏移量的变化。ZoneOffset类声明了三个常量:
-
UTC -
MAX -
MIN
UTC是 UTC 的时区偏移常量。MAX和MIN是支持的最大和最小区域偏移量。
Tip
Z,而不是+00:00或–00:00,被用作 UTC 时区的时区偏移指示符。
ZoneOffset类提供了使用小时、分钟和秒的组合来创建其实例的方法。清单 16-4 展示了如何创建ZoneOffset类的实例。
// ZoneOffsetTest.java
package com.jdojo.datetime;
import java.time.ZoneOffset;
public class ZoneOffsetTest {
public static void main(String[] args) {
// Create zone offset using hour, minute, and second
ZoneOffset zos1 = ZoneOffset.ofHours(-6);
ZoneOffset zos2 = ZoneOffset.ofHoursMinutes(5, 30);
ZoneOffset zos3 = ZoneOffset.ofHoursMinutesSeconds(8, 30, 45);
System.out.println(zos1);
System.out.println(zos2);
System.out.println(zos3);
// Create zone offset using offset ID as a string
ZoneOffset zos4 = ZoneOffset.of("+05:00");
ZoneOffset zos5 = ZoneOffset.of("Z"); // Same as ZoneOffset.UTC
System.out.println(zos4);
System.out.println(zos5);
// Print the values for zone offset constants
System.out.println("ZoneOffset.UTC: " + ZoneOffset.UTC);
System.out.println("ZoneOffset.MIN: " + ZoneOffset.MIN);
System.out.println("ZoneOffset.MAX: " + ZoneOffset.MAX);
}
}
-06:00
+05:30
+08:30:45
+05:00
Z
ZoneOffset.UTC: Z
ZoneOffset.MIN: -18:00
ZoneOffset.MAX: +18:00
Listing 16-4Creating Instances of the ZoneOffset Class
根据 ISO-8601 标准,时区偏移量可能包括小时和分钟或仅包括小时。新的日期时间 API 还允许时区偏移中的秒。您可以使用ZoneOffset类的compareTo()方法将一个区域偏移量与另一个区域偏移量进行比较。区域偏移按降序进行比较,即它们在一天中的时间内出现的顺序,例如,区域偏移+5:30 出现在区域偏移+5:00 之前。ISO-8601 标准支持–12:00 到+14:00 之间的区域偏移。但是,为了避免将来时区偏移延长时出现任何问题,日期-时间 API 支持–18:00 到+18:00 之间的时区偏移。
ZoneId 类
ZoneId类的一个实例表示时区偏移量和为观察到的夏令时更改时区偏移量的规则的组合。并非所有时区都遵循夏令时。为了简化你对ZoneId的理解,你可以这样想:
ZoneId = ZoneOffset + ZoneRules
Tip
A ZoneOffset表示相对于 UTC 时区的固定时区偏移量,而ZoneId表示可变时区偏移量。偏差、一年中时区偏移量更改的时间以及更改量都由时区规则控制。ZoneOffset类继承自ZoneId类。
时区有一个唯一的文本 ID,可以用三种格式指定:
-
在这种格式中,时区 ID 是根据时区偏移量来指定的,它可以是以下格式之一:
+h、+hh、+hh:mm、-hh:mm、+hhmm、-hhmm、+hh:mm:ss、-hh:mm:ss、+hhmmss和-hhmmss,其中h、m和s分别表示小时、分钟和秒的单个数字。Z用于 UTC。区域偏移的一个例子是+06:00。 -
在这种格式中,区域 ID 以
UTC、GMT或UT为前缀,后跟一个区域偏移量,例如UTC+06:00。 -
在这种格式中,通过使用区域来指定区域 ID,例如
America/Chicago。
使用前两种形式的区域 id,创建一个带有固定区域偏移的ZoneId。您可以使用of()工厂方法创建一个ZoneId:
ZoneId usChicago = ZoneId.of("America/Chicago");
ZoneId bdDhaka = ZoneId.of("Asia/Dhaka");
ZoneId fixedZoneId = ZoneId.of("+06:00");
ZoneId类提供对所有已知时区 id 的访问。它的getAvailableZoneIds()静态方法返回一个包含所有可用区域 id 的Set<String>。清单 16-5 显示了如何打印所有区域 id。输出中显示了区域 id 的部分列表。
// PrintAllZoneIds.java
package com.jdojo.datetime;
import java.time.ZoneId;
import java.util.Set;
public class PrintAllZoneIds {
public static void main(String[] args) {
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
for (String zoneId: zoneIds) {
System.out.println(zoneId);
}
}
}
Asia/Aden
Africa/Cairo
Pacific/Honolulu
America/Chicago
Europe/Athens
...
Listing 16-5 Printing All Available Zone IDs
通过一个ZoneId对象,您可以访问由ZoneId表示的时区的区域规则。您可以使用ZoneId类的getRules()方法来获取ZoneRules类的一个实例,以处理夏令时的转换、指定日期时间的时区偏移量、夏令时的数量等规则。通常,您不会在代码中直接使用区域规则。作为一名开发人员,您将使用一个ZoneId来创建一个ZonedDateTime,稍后将对此进行讨论。清单 16-6 中的程序显示了如何查询ZoneRules对象来获取关于ZoneId的时间偏移和时间变化的信息。时间转换列表非常大,在输出中只显示了一部分。
// ZoneRulesTest.java
package com.jdojo.datetime;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.zone.ZoneOffsetTransition;
import java.time.zone.ZoneRules;
import java.util.List;
public class ZoneRulesTest {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
System.out.println("Current Date Time: " + now);
ZoneId fixedZoneId = ZoneId.of("+06:00");
ZoneId bdDhaka = ZoneId.of("Asia/Dhaka");
ZoneId usChicago = ZoneId.of("America/Chicago");
// Print some zone rules for ZoneIds
printDetails(fixedZoneId, now);
printDetails(bdDhaka, now);
printDetails(usChicago, now);
}
public static void printDetails(ZoneId zoneId, LocalDateTime now) {
System.out.println("Zone ID: " + zoneId.getId());
ZoneRules rules = zoneId.getRules();
boolean isFixedOffset = rules.isFixedOffset();
System.out.println("isFixedOffset(): " + isFixedOffset);
ZoneOffset offset = rules.getOffset(now);
System.out.println("Zone offset: " + offset);
List<ZoneOffsetTransition> transitions = rules.getTransitions();
System.out.println(transitions);
}
}
Current Date Time: 2021-08-20T20:10:08.836642261
Zone ID: +06:00
isFixedOffset(): true
Zone offset: +06:00
[]
Zone ID: Asia/Dhaka
isFixedOffset(): false
Zone offset: +06:00
[Transition[Overlap at 1890-01-01T00:00+06:01:40 to +05:53:20], ..., Transition[Overlap at 2010-01-01T00:00+07:00 to +06:00]]
Zone ID: America/Chicago
isFixedOffset(): false
Zone offset: -05:00
[Transition[Overlap at 1883-11-18T12:09:24-05:50:36 to -06:00], ..., Transition[Overlap at 2008-11-02T02:00-05:00 to -06:00]]
Listing 16-6Knowing the Time Change Rules (the ZoneRules) for a ZoneId
一些组织和团体提供了一组时区规则作为数据库,其中包含世界上所有时区的代码和数据。每个提供者都有一个唯一的组 ID。标准规则提供者之一是由 TZDB 组 ID 标识的 TZ 数据库。参见 www.twinsun.com/tz/tz-link.htm 了解更多关于 TZ 数据库的详细信息。
由于时区的规则会随着时间的推移而变化,因此一个组会为不同的区域提供多个版本的规则数据。通常,区域代表时区规则相同的时区。每个组都有自己的版本和区域命名方案。
TZDB 以[area]/[city]格式存储地区名称。一些地区名称的例子有非洲/突尼斯、美洲/芝加哥、亚洲/加尔各答、亚洲/东京、欧洲/伊斯坦布尔、欧洲/伦敦和欧洲/莫斯科。
日期时间 API 使用 TZDB 作为默认时区规则提供程序。如果您使用的是 TZDB 中基于区域的分区 ID,请使用区域名称作为分区 ID。如果 TZDB 以外的组用于区域规则,则区域名称应该以提供商的组 ID 为前缀,形式为“groupregion”。例如,如果您使用的是国际航空运输协会(IATA)的提供商,请为芝加哥地区使用“IATACHI”。关于如何注册您自己的区域规则提供者的更多细节,请参考java.time.zone包中的ZoneRulesProvider类。
有用的与日期时间相关的枚举
在我们讨论表示日期和时间的不同组合的类之前,有必要讨论一些表示日期和时间组件的常量的枚举:
-
Month -
DayOfWeek -
ChronoField -
ChronoUnit
大多数情况下,您会将这些枚举中的常量直接用作方法的参数,或者作为方法的返回值接收它们。一些枚举包含使用常量本身作为输入来计算有用的日期时间值的方法。
代表月份
枚举有 12 个常量来代表一年中的 12 个月。常量名有JANUARY、FEBRUARY、MARCH、APRIL、MAY、JUNE、JULY、AUGUST、SEPTEMBER、OCTOBER、NOVEMBER、DECEMBER。月份按从 1 到 12 的顺序编号,一月是 1,十二月是 12。Month enum 提供了一些有用的方法,比如用of()从 int value中获取Month的实例,用from()从任意 date 对象中获取Month,用getValue()获取Month的int值,等等。
为了获得更好的可读性,如果在日期-时间 API 中可用,请使用枚举常量,而不是整数值。例如,对于七月,在代码中使用Month.JULY,而不是整数 7。有时 API 提供了两个版本的方法:一个采用Month枚举参数,另一个采用int月份值。这种方法的一个例子是LocalDate类中的静态工厂方法of():
-
static LocalDate of(int year, int month, int dayOfMonth) -
static LocalDate of(int year, Month month, int dayOfMonth)
清单 16-7 展示了Month枚举的一些用法。
// MonthTest.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.Month;
public class MonthTest {
public static void main(String[] args) {
// Use Month.JULY as a method argument
LocalDate ld1 = LocalDate.of(2021, Month.JULY, 1);
// Derive a Month from a local date
Month m1 = Month.from(ld1);
// Create a Month from an int value 2
Month m2 = Month.of(2);
// Get the next month from m2
Month m3 = m2.plus(1);
// Get the Month from a local date
Month m4 = ld1.getMonth();
// Convert an enum constant to an int
int m5 = m2.getValue();
System.out.format("%s, %s, %s, %s, %d%n", m1, m2, m3, m4, m5);
}
}
JULY, FEBRUARY, MARCH, JULY, 2
Listing 16-7Using the Month Enum
表示一周中的某一天
枚举有七个常量来代表一周的七天。常量有MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY和SUNDAY。它的getValue()方法返回一个int值,1 代表星期一,2 代表星期二,依此类推,它遵循 ISO-8601 标准。DayOfWeek枚举在java.time包中。下面是一些使用DayOfWeek枚举及其方法的例子:
LocalDate ld = LocalDate.of(2021, 5, 10);
// Extract the day-of-week from a LocalDate
DayOfWeek dw1 = DayOfWeek.from(ld); // THURSDAY
// Get the int value of the day-of-week
int dw11 = dw1.getValue(); // 4
// Use the method of the LocalDate class to get day-of-week
DayOfWeek dw12 = ld.getDayOfWeek(); // THURSDAY
// Obtain a DayOfWeek instance using an int value
DayOfWeek dw2 = DayOfWeek.of(7); // SUNDAY
// Add one day to the day-of-week to get the next day
DayOfWeek dw3 = dw2.plus(1); // MONDAY
// Get the day-of-week two days ago
DayOfWeek dw4 = dw2.minus(2); // FRIDAY
表示日期时间字段
datetime 中的大多数字段都可以表示为数值,例如,年、月、日、小时等。接口的一个实例表示日期时间的一个字段,例如,年、月、分钟等。ChronoField枚举实现了TemporalField接口,并提供了几个常量来表示日期时间字段。ChronoField枚举包含一长串常量。其中一些如下:AMPM_OF_DAY、CLOCK_HOUR_OF_AMPM、CLOCK_HOUR_OF_DAY、DAY_OF_MONTH、DAY_OF_WEEK、DAY_OF_YEAR、ERA、HOUR_OF_AMPM、HOUR_OF_DAY、INSTANT_SECONDS、MINUTE_OF_HOUR、MONTH_OF_YEAR、SECOND_OF_MINUTE、YEAR、YEAR_OF_ERA。
通常,使用TemporalField从日期时间中获取字段的值。所有 datetime 类都有一个为指定的TemporalField返回一个int值的get()方法。如果一个字段的值可能太大而无法存储在一个int中,那么使用伴随的getLong()方法来获取一个long中的值。
并非所有日期时间类都支持所有类型的字段。例如,LocalDate不支持MINUTE_OF_HOUR字段。使用 datetime 类的isSupported()方法来检查它们是否支持特定类型的字段。使用ChronoField的isSupportedBy()方法检查字段是否受日期时间类支持。
Tip
特定于 ISO-8601 日历系统的一些日期时间字段的常量在IsoFields类中声明。例如,IsoFields.DAY_OF_QUARTER表示基于 ISO-8601 的季度日。
以下代码片段演示了如何使用ChronoField从日期时间中提取字段值,以及该日期时间是否支持该字段:
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
...
LocalDateTime now = LocalDateTime.now();
System.out.println("Current Date Time: " + now);
System.out.println("Year: " + now.get(ChronoField.YEAR));
System.out.println("Month: " + now.get(ChronoField.MONTH_OF_YEAR));
System.out.println("Day: " + now.get(ChronoField.DAY_OF_MONTH));
System.out.println("Hour-of-day: " + now.get(ChronoField.HOUR_OF_DAY));
System.out.println("Hour-of-AMPM: " + now.get(ChronoField.HOUR_OF_AMPM));
System.out.println("AMPM-of-day: " + now.get(ChronoField.AMPM_OF_DAY));
LocalDate today = LocalDate.now();
System.out.println("Current Date : " + today);
System.out.println("LocalDate supports year: " + today.isSupported(ChronoField.YEAR));
System.out.println(
"LocalDate supports hour-of-day: " + today.isSupported(ChronoField.HOUR_OF_DAY));
System.out.println("Year is supported by LocalDate: " + ChronoField.YEAR.isSupportedBy(today));
System.out.println(
"Hour-of-day is supported by LocalDate: " + ChronoField.HOUR_OF_DAY.isSupportedBy(today));
Current Date Time: 2021-08-20T20:11:25.739544947
Year: 2021
Month: 8
Day: 20
Hour-of-day: 20
Hour-of-AMPM: 8
AMPM-of-day: 1
Current Date : 2021-08-20
LocalDate supports year: true
LocalDate supports hour-of-day: false
Year is supported by LocalDate: true
Hour-of-day is supported by LocalDate: false
AMPM_OF_DAY字段的值可以是 0 或 1;0 表示上午,1 表示下午。
表示日期时间字段的单位
时间是以年、月、日、小时、分钟、秒、周等单位来计量的。java.time.temporal包中的TemporalUnit接口的一个实例代表一个时间单位。同一个包中的ChronoUnit包含以下常量来表示时间单位:CENTURIES、DAYS、DECADES、ERAS、FOREVER、HALF_DAYS、HOURS、MICROS、MILLENNIA、MILLIS、MINUTES、MONTHS、NANOS、SECONDS、WEEKS和YEARS。
ChronoUnit枚举实现了TemporalUnit接口。因此,enum 中的所有常量都是TemporalUnit的一个实例。
Tip
特定于 ISO-8601 日历系统的一些日期时间单位的常量在IsoFields类中声明。例如,IsoFields.QUARTER_YEARS和IsoFields.WEEK_BASED_YEARS分别代表基于 ISO-8601 的季度年(3 个月)和基于周的年(52 或 53 周)。ISO-8601 标准将 7 天视为一周;一周从星期一开始;一年的第一个日历周包括一年的第一个星期四;一年的第一周可能开始于前一年,而一年的最后一周可能结束于下一年。这可能导致一年中有 53 周。例如,2009 年的第一周开始于 2008 年 12 月 29 日,最后一周开始于 2009 年 12 月 29 日,因此 2009 年是 53 周的一年。
Datetime 类提供了两种方法,minus()和plus()。它们需要一定的时间和时间单位,通过减去和加上指定的时间来返回新的日期时间。便利方法如minusDays()、minusHours()、plusDays()、plusHours()等。也由适用的类提供来加减时间。以下代码片段说明了在这些方法中使用ChronoUnit枚举常量:
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
...
LocalDateTime now = LocalDateTime.now();
// Get the date time 4 days ago
LocalDateTime ldt2 = now.minus(4, ChronoUnit.DAYS);
// Use the minusDays() method to get the same result
LocalDateTime ldt3 = now.minusDays(4);
// Get date and time 4 hours later
LocalDateTime ldt4 = now.plus(4, ChronoUnit.HOURS);
// Use the plusHours() method to get the same result
LocalDateTime ldt5 = now.plusHours(4);
System.out.println("Current Datetime: " + now);
System.out.println("4 days ago: " + ldt2);
System.out.println("4 days ago: " + ldt3);
System.out.println("4 hours after: " + ldt4);
System.out.println("4 hours after: " + ldt5);
Current Datetime: 2021-08-20T20:13:39.453109419
4 days ago: 2021-08-16T20:13:39.453109419
4 days ago: 2021-08-16T20:13:39.453109419
4 hours after: 2021-08-21T00:13:39.453109419
4 hours after: 2021-08-21T00:13:39.453109419
本地日期、时间和日期时间
LocalDate类的一个实例表示没有时间或时区的日期。该类中的几个方法允许您将一个LocalDate转换为其他 datetime 对象,并操纵它的字段(年、月和日)来获得另一个LocalDate。下面的代码片段创建了一些LocalDate对象:
// Get the current local date
LocalDate ldt1 = LocalDate.now();
// Create a local date May 10, 2021
LocalDate ldt2 = LocalDate.of(2021, Month.MAY, 10);
// Create a local date, which is 10 days after the epoch date 1970-01-01
LocalDate ldt3 = LocalDate.ofEpochDay(10); // 1970-01-11
LocalDate类包含两个常量MAX和MIN,分别是最大和最小支持的LocalDate。LocalDate.MAX的值为+99999999-12-31 和LocalDate.MIN is –999999999-01-01。
LocalTime类的一个实例表示没有日期或时区的时间。时间以纳秒的精度表示。它包含MIN、MAX、MIDNIGHT和NOON常量,分别代表时间常量 00:00、23:59:59.99999999、00:00 和 12:00。这个类中的几个方法允许你以不同的方式创建、操作和比较时间。下面的代码片段创建了一些LocalTime对象:
// Get the current local time
LocalTime lt1 = LocalTime.now();
// Create a local time 07:30
LocalTime lt2 = LocalTime.of(7, 30);
// Create a local time 07:30:50
LocalTime lt3 = LocalTime.of(7, 30, 50);
// Create a local time 07:30:50.000005678
LocalTime lt4 = LocalTime.of(7, 30, 50, 5678);
LocalDateTime类的一个实例表示没有时区的日期和时间。它提供了几种创建、操作和比较日期时间的方法。你可以把LocalDateTime想象成LocalDate和LocalTime的组合:
LocalDateTime = LocalDate + LocalTime
以下代码片段以不同的方式创建了一些LocalDateTime对象:
// Get the current local datetime
LocalDateTime ldt1 = LocalDateTime.now();
// A local datetime 2021-05-10T16:14:32
LocalDateTime ldt2 = LocalDateTime.of(2021, Month.MAY, 10, 16, 14, 32);
// Construct a local datetime from a local date and a local time
LocalDate ld1 = LocalDate.of(2021, 5, 10);
LocalTime lt1 = LocalTime.of(16, 18, 41);
LocalDateTime ldt3 = LocalDateTime.of(ld1, lt1); // 2021-05-10T16:18:41
有关方法的完整列表,请参考这些类的在线 API 文档。在浏览在线 API 文档之前,请务必阅读本章中的“浏览日期-时间 API”一节。仅仅在一个类中,你就会发现超过 60 种方法。如果不知道这些方法名背后的模式,查看这些类的 API 文档将会让人不知所措。请记住,在 API 中使用不同的方法可以获得相同的结果。
清单 16-8 展示了一些在本地日期、时间和日期时间上创建和执行操作的方法。
// LocalDateTimeTest.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
public class LocalDateTimeTest {
public static void main(String[] args) {
// Create a local date and time
LocalDate ld = LocalDate.of(2021, Month.MAY, 11);
LocalTime lt = LocalTime.of(8, 52, 23);
System.out.println("ld: " + ld);
System.out.println("ld.isLeapYear(): " + ld.isLeapYear());
System.out.println("lt: " + lt);
// Create a local datetime from the local date and time
LocalDateTime ldt = LocalDateTime.of(ld, lt);
System.out.println("ldt: " + ldt);
// Add 2 months and 25 minutes to the local datetime
LocalDateTime ldt2 = ldt.plusMonths(2).plusMinutes(25) ;
System.out.println("ldt2: " + ldt2);
// Derive the local date and time from the localdatetime
LocalDate ld2 = LocalDate.from(ldt2);
LocalTime lt2 = LocalTime.from(ldt2);
System.out.println("ld2: " + ld2);
System.out.println("lt2: " + lt2);
}
}
ld: 2021-05-11
ld.isLeapYear(): false
lt: 08:52:23
ldt: 2021-05-11T08:52:23
ldt2: 2021-07-11T09:17:23
ld2: 2021-07-11
lt2: 09:17:23
Listing 16-8Using a Local Date, Time, and Datetime
您可以给LocalDate添加年、月和日。如果给 2024-01-31 加一个月会是什么结果?如果日期时间 API 只是将月份添加到月份字段中,结果将是 2024-02-31,这是一个无效的日期。添加月份后,检查结果是否为有效日期。如果不是有效的日期,则该日期将调整为该月的最后一天。在这种情况下,结果将是 2024-02-29:
LocalDate ld1 = LocalDate.of(2024, Month.JANUARY, 31);
LocalDate ld2 = ld1.plusMonths(1);
System.out.println(ld1);
System.out.println(ld2);
2024-01-31
2024-02-29
如果您将日期添加到LocalDate,month和year字段会被调整以保持结果为有效日期:
LocalDate ld1 = LocalDate.of(2024, Month.JANUARY, 31);
LocalDate ld2 = ld1.plusDays(30);
LocalDate ld3 = ld1.plusDays(555);
System.out.println(ld1);
System.out.println(ld2);
System.out.println(ld3);
2024-01-31
2024-03-01
2025-08-08
如何获取特定年份中所有日期都在周日的数据?如何获取未来 5 年中所有落在本月 13 日和星期五的日期?这些类型的计算在 Java 中是可能的,它使用一个顺序循环来生成所有这样的日期并检查每个日期的特定条件,但是 Java 9 通过在LocalDate类中提供一个datesUntil()方法使这样的计算变得非常容易。该方法重载了如下两种变体:
-
Stream<LocalDate> datesUntil(LocalDate endExclusive) -
Stream<LocalDate> datesUntil(LocalDate endExclusive, Period step)
这些方法产生一个有序的LocalDate流。流中的第一个元素是调用该方法的LocalDate。datesUntil(LocalDate endExclusive)方法一次递增流中的日期一天。datesUntil(LocalDate endExclusive, Period step)方法按照指定的step递增日期。指定的结束日期是独占的(不包含)。您可以对返回的流进行一些有用的计算。
Tip
流是在 Java 8 中添加的,是 Java 中一个非常有用的概念,在 JDK 的很多地方都使用,包括 LocalDate。流接口以及支持类和接口位于 java.util.stream 包中。蒸汽代表一系列的对象。它支持可链接的方法,如 map、filter、count 和 reduce,并且延迟执行。我们所说的懒惰是指在使用诸如 count()或 forEach()之类的终端操作之前,它不会做任何事情。forEach()方法可以接受 lambda 表达式或方法引用作为输入,并为流中的每个对象执行一个操作。我们将在 More Java 17 中详细介绍流。
以下代码片段计算 2021 年的周日数。请注意,代码使用 2022 年 1 月 1 日作为最后一个日期,这是唯一的,这将使流返回 2021 年的所有日期:
long sundaysIn2021 = LocalDate.of(2021, 1, 1)
.datesUntil(LocalDate.of(2022, 1, 1))
.filter(ld -> ld.getDayOfWeek() == DayOfWeek.SUNDAY)
.count();
System.out.println("Number of Sundays in 2021: " + sundaysIn2021);
Number of Sundays in 2021: 52
以下代码片段打印 2020 年 1 月 1 日(含)到 2025 年 1 月 1 日(含)之间的所有日期,这些日期都是星期五,并且是该月的第 13 天:
System.out.println("Fridays that fall on 13th of the month between 2020 - 2024: ");
LocalDate.of(2020, 1, 1)
.datesUntil(LocalDate.of(2025, 1, 1))
.filter(ld -> ld.getDayOfMonth() == 13 && ld.getDayOfWeek() == DayOfWeek.FRIDAY)
.forEach(System.out::println);
Fridays that fall on 13th of the month between 2020 – 2024 (inclusive):
2020-03-13
2020-11-13
2021-08-13
2022-05-13
2023-01-13
2023-10-13
2024-09-13
2024-12-13
以下代码片段打印 2021 年每个月的最后一天:
System.out.println("Last Day of months in 2021:");
LocalDate.of(2021, 1, 31)
.datesUntil(LocalDate.of(2022, 1, 1), Period.ofMonths(1))
.map(ld -> ld.format(DateTimeFormatter.ofPattern("EEE MMM dd, yyyy")))
.forEach(System.out::println);
Last Day of months in 2021:
Sun Jan 31, 2021
Sun Feb 28, 2021
Wed Mar 31, 2021
Fri Apr 30, 2021
Mon May 31, 2021
Wed Jun 30, 2021
Sat Jul 31, 2021
Tue Aug 31, 2021
Thu Sep 30, 2021
Sun Oct 31, 2021
Tue Nov 30, 2021
Fri Dec 31, 2021
如何将一个Instant转换成一个LocalDate、LocalTime和LocalDateTime?在 Java 8 中,LocalDateTime类包含一个名为ofInstant ( Instant instant, ZoneId zone)的静态方法,通过提供一个ZoneId,可以将一个 Instant 转换成一个LocalDateTime。然而,在LocalDate和LocalTime类中没有这样的方法。Java 9 通过在这两个类中提供一个ofInstant()方法弥补了这个差距。以下代码片段向您展示了如何使用这两种方法将一个Instant转换为一个LocalDate和一个LocalTime:
/* Without using ofInstant */
// Get an Instant
Instant now = Instant.now();
// Get the system default time zone
ZoneId zone = ZoneId.systemDefault();
// Convert the Instant to a ZonedDateTime
ZonedDateTime zdt = now.atZone(zone);
// Get the LocalDate from the ZonedDateTime
LocalDate ld1 = zdt.toLocalDate();
// Get the LocalTime from the ZonedDateTime
LocalTime lt1 = zdt.toLocalTime();
System.out.println("In Java 8");
System.out.println("Instant: " + now);
System.out.println("Local Date: " + ld1);
System.out.println("Local Time: " + lt1);
/* Using ofInstant */
// Get a LocalDate from the Instant
LocalDate ld2 = LocalDate.ofInstant(now, zone);
// Get the LocalTime from the Instant
LocalTime lt2 = LocalTime.ofInstant(now, zone);
System.out.println("\nIn Java 9");
System.out.println("Instant: " + now);
System.out.println("Local Date: " + ld2);
System.out.println("Local Time: " + lt2);
你如何计算天数、小时数等?在两个日期和时间之间?日期-时间 API 有不同的方法来计算两个日期和时间之间的时间段。我们把对这种计算的讨论推迟到“两个日期和时间之间的周期”一节。
偏移时间和日期时间
OffsetTime和OffsetDateTime类的一个实例分别代表一个时间和一个日期时间,与 UTC 有一个固定的时区偏移量。偏移时间和日期时间不知道时区。ISO-8601 格式的偏移时间和偏移日期时间的示例分别是 10:50:11+5:30 和 2021-05-11T10:50:11+5:30。
Tip
没有OffsetDate类。这是最初设计的一部分。后来不了了之。
本地日期和时间与偏移日期和时间之间的关系可以表示如下:
OffsetTime = LocalTime + ZoneOffset
OffsetDateTime = LocalDateTime + ZoneOffset
使用偏移时间和日期时间类似于使用本地时间和日期时间,只是您必须使用时区偏移。你总是可以从一个OffsetXxx中提取一个LocalXxx。一个OffsetDateTime在时间轴上存储一个瞬间,因此支持OffsetDateTime和Instant之间的转换。
清单 16-9 展示了创建偏移时间和日期时间的例子。当您使用now()方法获取当前偏移时间和日期时间时,系统默认时区用于获取时区偏移值。对于当前时间和日期时间,您将获得不同的输出。
// OffsetDateTimeTest.java
package com.jdojo.datetime;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
public class OffsetDateTimeTest {
public static void main(String[] args) {
// Get the current offset time
OffsetTime ot1 = OffsetTime.now();
System.out.println("Current offset time: " + ot1);
// Create a zone offset +05:30
ZoneOffset offset = ZoneOffset.ofHoursMinutes(5, 30);
// Create an offset time
OffsetTime ot2 = OffsetTime.of(16, 40, 28, 0, offset);
System.out.println("An offset time: " + ot2);
// Get the current offset datetime
OffsetDateTime odt1 = OffsetDateTime.now();
System.out.println("Current offset datetime: " + odt1);
// Create an offset datetime
OffsetDateTime odt2 = OffsetDateTime.of(2021, 5, 11, 18, 10, 30, 0, offset);
System.out.println("An offset datetime: " + odt2);
// Get the local date and time from the offset datetime
LocalDate ld1 = odt1.toLocalDate();
LocalTime lt1 = odt1.toLocalTime();
System.out.println("Current Local Date: " + ld1);
System.out.println("Current Local Time: " + lt1);
// Get the instant from the offset datetime
Instant i1 = odt1.toInstant();
System.out.println("Current Instant: " + i1);
// Create an offset datetime from the instant
ZoneId usChicago = ZoneId.of("America/Chicago");
OffsetDateTime odt3 = OffsetDateTime.ofInstant(i1, usChicago);
System.out.println("Offset datetime from instant: " + odt3);
}
}
Current offset time: 21:06:12.772227562-04:00
An offset time: 16:40:28+05:30
Current offset datetime: 2021-08-20T21:06:12.982737832-04:00
An offset datetime: 2021-05-11T18:10:30+05:30
Current Local Date: 2021-08-20
Current Local Time: 21:06:12.982737832
Current Instant: 2021-08-21T01:06:12.982737832Z
Offset datetime from instant: 2021-08-20T20:06:12.982737832-05:00
Listing 16-9Using Offset Dates, Times, and Datetimes
分区日期时间
ZonedDateTime类的一个实例表示一个带有时区规则的日期时间。时区规则包括时区偏移量及其因夏令时而变化的规则。没有ZonedDate和ZonedTime;它们毫无意义。ZonedDateTime和LocalDateTime之间的关系可以表示如下:
ZonedDateTime = LocalDateTime + ZoneId
下面是一个从LocalDateTime创建ZonedDateTime的例子:
ZoneId usCentral = ZoneId.of("America/Chicago");
LocalDateTime ldt = LocalDateTime.of(2021, Month.MAY, 11, 7, 30);
ZonedDateTime zdt = ZonedDateTime.of(ldt, usCentral);
System.out.println(zdt);
2021-05-11T07:30-05:00[America/Chicago]
并不是所有的LocalDateTime和ZoneId的组合都会产生有效的ZonedDateTime。由于夏令时的变化,某个时区的本地时间线上可能会有间隙或重叠。例如,在美国/芝加哥时区 2013 年 3 月 10 日 02:00,时钟向前拨了一个小时,因此在当地时间线上留下了 1 个小时的间隙;02:00 到 02:59 之间的时间不存在。2013 年 11 月 3 日,在同一个美国/芝加哥时区的 02:00,时钟向后移动一小时,从而在当地时间线上产生了 1 小时的重叠;01:00 到 01:59 之间的时间存在两次。日期-时间 API 有明确定义的规则来处理这种间隙和重叠:
-
如果本地日期时间落在间隙的中间,则时间会向前移动与间隙相同的量。例如,如果您想要为美国/芝加哥时区构建一个 2013 年 3 月 10 日 02:30:00 的时区日期时间,您将得到 2013 年 3 月 10 日 3:30:00。时间往前挪一个小时,等于一个小时的间隙。
-
如果本地日期时间在重叠的中间,则时间有效。在该间隙中,存在两个区域偏移:一个是在向后移动时钟之前存在的较早的偏移,另一个是在向后移动时钟之后存在的较晚的偏移。默认情况下,对于间隙中的时间,使用先前存在的区域偏移。
ZonedDateTime类包含withEarlierOffsetAtOverlap()和withLaterOffsetAtOverlap(),如果时间在重叠范围内,可以让您选择所需的时区偏移量。
以下代码片段演示了ZonedDateTime的结果,时间落在间隙和重叠中:
ZoneId usChicago = ZoneId.of("America/Chicago");
// 2013-03-10T02:30 did not exist in America/Chicago time zone
LocalDateTime ldt = LocalDateTime.of(2013, Month.MARCH, 10, 2, 30);
ZonedDateTime zdt = ZonedDateTime.of(ldt, usChicago);
System.out.println(zdt);
// 2013-10-03T01:30 existed twice in America/Chicago time zone
LocalDateTime ldt2 = LocalDateTime.of(2013, Month.NOVEMBER, 3, 1, 30);
ZonedDateTime zdt2 = ZonedDateTime.of(ldt2, usChicago);
System.out.println(zdt2);
// Try using the two rules for overlaps: one will use the earlier
// offset -05:00 (the default) and another the later offset -06:00
System.out.println(zdt2.withEarlierOffsetAtOverlap());
System.out.println(zdt2.withLaterOffsetAtOverlap());
2013-03-10T03:30-05:00[America/Chicago]
2013-11-03T01:30-05:00[America/Chicago]
2013-11-03T01:30-05:00[America/Chicago]
2013-11-03T01:30-06:00[America/Chicago]
ZonedDateTime类包含一个静态工厂方法ofLocal(LocalDateTime localDateTime, ZoneId zone, ZoneOffset preferredOffset)。如果在指定的zone中当地时间有两个时区偏移量,您可以使用此方法通过指定首选时区偏移量来创建一个ZonedDateTime。如果指定的首选区域偏移无效,则使用重叠的较早区域偏移。下面的代码片段演示了此方法的用法。当我们提供无效的首选偏移–07:00 时,将使用较早的偏移–05:00:
ZoneId usChicago = ZoneId.of("America/Chicago");
ZoneOffset offset5 = ZoneOffset.of("-05:00");
ZoneOffset offset6 = ZoneOffset.of("-06:00");
ZoneOffset offset7 = ZoneOffset.of("-07:00");
// At 2013-10-03T01:30, -05:00 and -06:00 offsets were valid for
// the time zone America/Chicago
LocalDateTime ldt = LocalDateTime.of(2013, Month.NOVEMBER, 3, 1, 30);
ZonedDateTime zdt5 = ZonedDateTime.ofLocal(ldt, usChicago, offset5);
ZonedDateTime zdt6 = ZonedDateTime.ofLocal(ldt, usChicago, offset6);
ZonedDateTime zdt7 = ZonedDateTime.ofLocal(ldt, usChicago, offset7);
System.out.println("With offset " + offset5 + ": " + zdt5);
System.out.println("With offset " + offset6 + ": " + zdt6);
System.out.println("With offset " + offset7 + ": " + zdt7);
With offset -05:00: 2013-11-03T01:30-05:00[America/Chicago]
With offset -06:00: 2013-11-03T01:30-06:00[America/Chicago]
With offset -07:00: 2013-11-03T01:30-05:00[America/Chicago]
ZonedDateTime类包含几个方法,将它转换为本地和偏移日期、时间和日期时间表示,比较它的实例,并通过更改它的一些字段获得它的新实例。清单 16-10 展示了如何使用分区日期时间。您将获得当前日期和时间的不同输出。
// ZonedDateTimeTest.java
package com.jdojo.datetime;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
public class ZonedDateTimeTest {
public static void main(String[] args) {
// Get the current zoned datetime for the system default time zone
ZonedDateTime zdt1 = ZonedDateTime.now();
System.out.println("Current zoned datetime:" + zdt1);
// Create a local datetime
LocalDateTime ldt = LocalDateTime.of(2021, Month.MARCH, 11, 7, 30);
// Create some zoned datetimes
ZoneId usCentralZone = ZoneId.of("America/Chicago");
ZonedDateTime zdt2 = ZonedDateTime.of(ldt, usCentralZone);
System.out.println(zdt2);
// Get zone offset and zone id
ZoneOffset offset = zdt2.getOffset();
ZoneId zone = zdt2.getZone();
System.out.println("Offset:" + offset + ", Zone:" + zone);
// Subtract 10 hours. Zone-offset changes from -05:00 to -06:00
ZonedDateTime zdt3 = zdt2.minusHours(10);
System.out.println(zdt3);
// Create a datetime in Asia/Kolkata time zone
ZoneId indiaKolkataZone = ZoneId.of("Asia/Kolkata");
ZonedDateTime zdt4 = ZonedDateTime.of(ldt, indiaKolkataZone);
System.out.println(zdt4);
// Perform some conversions on zoned date time
LocalDateTime ldt2 = zdt4.toLocalDateTime();
OffsetDateTime odt = zdt4.toOffsetDateTime();
Instant i1 = zdt4.toInstant();
System.out.println("To local datetime: " + ldt2);
System.out.println("To offset datetime: " + odt);
System.out.println("To instant: " + i1);
}
}
Current zoned datetime:2021-08-20T21:14:15.207158017-04:00[America/New_York]
2021-03-11T07:30-06:00[America/Chicago]
Offset:-06:00, Zone:America/Chicago
2021-03-10T21:30-06:00[America/Chicago]
2021-03-11T07:30+05:30[Asia/Kolkata]
To local datetime: 2021-03-11T07:30
To offset datetime: 2021-03-11T07:30+05:30
To instant: 2021-03-11T02:00:00Z
Listing 16-10Using the ZonedDateTime Class
相同的瞬间,不同的时间
有时,您希望将一个时区的日期时间转换为另一个时区的日期时间。这类似于在芝加哥 2021 年 5 月 14 日 16:30 问印度的日期和时间。你可以通过几种方式得到这个。您可以使用ZonedDateTime类的toInstant()方法从第一个分区日期时间中获取瞬间,并使用ofInstant()方法创建第二个分区日期时间。您也可以使用ZonedDateTime类的withZoneSameInstant(ZoneId newZoneId)方法,如清单 16-11 所示,来获得相同的结果。
// DateTimeZoneConversion.java
package com.jdojo.datetime;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class DateTimeZoneConversion {
public static void main(String[] args) {
LocalDateTime ldt = LocalDateTime.of(2021, Month.MAY, 14, 16, 30);
ZoneId usCentral = ZoneId.of("America/Chicago");
ZonedDateTime zdt = ZonedDateTime.of(ldt, usCentral);
System.out.println("In US Central Time Zone:" + zdt);
ZoneId asiaKolkata = ZoneId.of("Asia/Kolkata");
ZonedDateTime zdt2 = zdt.withZoneSameInstant(asiaKolkata);
System.out.println("In Asia/Kolkata Time Zone:" + zdt2);
ZonedDateTime zdt3 = zdt.withZoneSameInstant(ZoneId.of("Z"));
System.out.println("In UTC Time Zone:" + zdt3);
}
}
In US Central Time Zone:2021-05-14T16:30-05:00[America/Chicago]
In Asia/Kolkata Time Zone:2021-05-15T03:00+05:30[Asia/Kolkata]
In UTC Time Zone:2021-05-14T21:30Z
Listing 16-11Converting a Datetime in a Time Zone to Another Time Zone
时钟
Clock类是现实世界时钟的抽象。它提供对某个时区的当前时刻、日期和时间的访问。您可以获得系统默认时区的时钟:
Clock clock = Clock.systemDefaultZone();
您还可以获得特定时区的时钟:
// Get a clock for Asia/Kolkata time zone
ZoneId asiaKolkata = ZoneId.of("Asia/Kolkata");
Clock clock2 = Clock.system(asiaKolkata);
要从时钟中获取当前时刻、日期和时间,可以使用与日期时间相关的类的now(Clock c)方法:
// Get the system default clock
Clock clock = Clock.systemDefaultZone();
// Get the current instant of the clock
Instant instant1 = clock.instant();
// Get the current instant using the clock and the Instant class
Instant instant2 = Instant.now(clock);
// Get the local date using the clock
LocalDate ld = LocalDate.now(clock);
// Get the zoned datetime using the clock
ZonedDateTime zdt = ZonedDateTime.now(clock);
在所有日期、时间和日期时间类中没有参数的now()方法使用默认时区的系统默认时钟。以下两条语句使用相同的时钟:
LocalTime lt1 = LocalTime.now();
LocalTime lt2 = LocalTime.now(Clock.systemDefaultZone());
Clock类的systemUTC()方法返回 UTC 时区的时钟。您也可以获得一个固定的时钟,它总是返回相同的时间。当您希望您的测试用例使用相同的当前时间,并且不依赖于系统时钟的当前时间时,固定时钟在测试中非常有用。您可以使用Clock类的fixed(Instant fixedInstant, ZoneId zone)静态方法来获得一个在指定时区具有固定时刻的时钟。Clock类还可以让你获得一个时钟,它给出的时间与另一个时钟有固定的偏差。
时钟总是知道它的时区。您可以使用Clock类获得系统默认时区,如下所示:
ZoneId defaultZone = Clock.systemDefaultZone().getZone();
Tip
Clock类的默认实现忽略闰秒。您还可以扩展Clock类来实现您自己的时钟。
Clock类包含许多静态工厂方法,这些方法允许您创建一个以指定间隔计时的时钟。这些方法如下:
-
static Clock tick(Clock baseClock, Duration tickDuration) -
static Clock tickMillis(ZoneId zone) -
static Clock tickMinutes(ZoneId zone) -
static Clock tickSeconds(ZoneId zone)
tick()方法允许您以Duration的形式指定 tick 的粒度。此方法返回的时钟使用指定为第一个参数的时钟。返回的时钟使指定的时钟在指定的持续时间内滴答,作为第二个参数。以下代码片段获取系统默认时区的时钟,每 1 毫秒滴答一次:
Clock clock = Clock.tick(Clock.systemDefaultZone(), Duration.ofMillis(1));
其他的tickXxx()方法返回指定时区的最佳可用时钟,该时钟以Xxx间隔计时。例如,tickSeconds()方法返回的时钟每秒滴答一次。
Tip
在 Java 9 中,tickMillis()方法被添加到了Clock类中。
周期
周期是根据日历字段years、months和days定义的时间量。持续时间也是用秒和纳秒来衡量的时间量。支持负句点。
周期和持续时间的区别是什么?持续时间表示精确的纳秒数,而周期表示不精确的时间量。一个时期对于人类就像一个持续时间对于机器一样。
周期的一些例子是 1 天、2 个月、5 天、3 个月和 2 天等。当有人提到两个月的时间时,你不知道这两个月中纳秒的确切数量。2 个月的时间可能意味着不同的天数(因此也就意味着不同的纳秒数),这取决于该时间开始的时间。例如,从 1 月 1 日午夜开始的两个月可能代表 59 天或 60 天,这取决于该年是否是闰年。类似地,一天的时间可能代表 23、24 或 25 小时,这取决于这一天是否遵循夏令时的开始/结束。
Period类的一个实例代表一个句点。使用以下静态工厂方法之一创建一个Period:
-
static Period of(int years, int months, int days) -
static Period ofDays(int days) -
static Period ofMonths(int months) -
static Period ofWeeks(int weeks) -
static Period ofYears(int years)
下面的代码片段创建了Period类的一些实例:
Period p1 = Period.of(2, 3, 5); // 2 years, 3 months, and 5 days
Period p2 = Period.ofDays(25); // 25 days
Period p3 = Period.ofMonths(-3); // -3 months
Period p4 = Period.ofWeeks(3); // 3 weeks (21 days)
System.out.println(p1);
System.out.println(p2);
System.out.println(p3);
System.out.println(p4);
P2Y3M5D
P25D
P-3M
P21D
您可以对周期执行加、减、乘和求反操作。除法运算执行整数除法,例如 7 除以 3 等于 2。以下代码片段显示了一些操作及其对周期的结果:
Period p1 = Period.ofDays(15); // P15D
Period p2 = p1.plusDays(12); // P27D
Period p3 = p1.minusDays(12); // P3D
Period p4 = p1.negated(); // P-15D
Period p5 = p1.multipliedBy(3); // P45D
使用Period类的plus()和minus()方法将一个周期添加到另一个周期,并将一个周期从另一个周期中减去。使用Period类的normalized()方法来规范化年和月。该方法确保month值保持在 0–11 之间。例如,“2 年 15 个月”将被规范化为“3 年 3 个月”:
Period p1 = Period.of(2, 3, 5);
Period p2 = Period.of(1, 15, 28);
System.out.println("p1: " + p1);
System.out.println("p2: " + p2);
System.out.println("p1.plus(p2): " + p1.plus(p2));
System.out.println("p1.plus(p2).normalized(): " + p1.plus(p2).normalized());
System.out.println("p1.minus(p2): " + p1.minus(p2));
p1: P2Y3M5D
p2: P1Y15M28D
p1.plus(p2): P3Y18M33D
p1.plus(p2).normalized(): P4Y6M33D
p1.minus(p2): P1Y-12M-23D
日期-时间 API 处理基于周期和持续时间的计算的方式有很大的不同。包括周期在内的计算行为符合人类的预期。例如,当您将 1 天的时间段添加到ZonedDateTime中时,日期部分会更改为第二天,保持时间不变,而不管一天有多少小时(23、24 或 25 小时)。但是,当您添加一天的持续时间时,它将始终添加 24 小时。让我们通过一个例子来阐明这一点。
2021-03-11T02:00,美国中部时区的时钟向前拨 1 小时,使 2021-03-11 成为 23 小时的一天。假设你给一个人美国中部时区的日期时间 2021-03-10T07:30。如果你问他们一天后的日期时间是什么,他们的答案会是 2021-03-11T07:30。他们的答案很自然,因为对人类来说,在当前日期时间上加一天,第二天也是同样的时间。让我们问一个机器同样的问题。要求机器在 2021-03-10T07:30 上加上 24 小时,认为等于 1 天。该计算机的响应将是 2021-03-11T08:30,因为它将在初始日期时间上增加 24 小时,而已知 02:00 和 03:00 之间的时间不存在。清单 16-12 用一个 Java 程序演示了这个讨论。
// PeriodTest.java
package com.jdojo.datetime;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class PeriodTest {
public static void main(String[] args) {
ZoneId usCentral = ZoneId.of("America/Chicago");
LocalDateTime ldt = LocalDateTime.of(2021, Month.MARCH, 10, 7, 30);
ZonedDateTime zdt1 = ZonedDateTime.of(ldt, usCentral);
Period p1 = Period.ofDays(1);
Duration d1 = Duration.ofHours(24);
// Add a period of 1 day and a duration of 24 hours
ZonedDateTime zdt2 = zdt1.plus(p1);
ZonedDateTime zdt3 = zdt1.plus(d1);
System.out.println("Start Datetime: " + zdt1);
System.out.println("After 1 Day period: " + zdt2);
System.out.println("After 24 Hours duration: " + zdt3);
}
}
Start Datetime: 2021-03-10T07:30-06:00[America/Chicago]
After 1 Day period: 2021-03-11T07:30-05:00[America/Chicago]
After 24 Hours duration: 2021-03-11T08:30-05:00[America/Chicago]
Listing 16-12Difference in Adding a Period and Duration to a Datetime
两个日期和时间之间的时间段
计算两个日期、时间和日期时间之间经过的时间是一个常见的要求。例如,您可能需要计算两个本地日期之间的天数或两个本地日期时间之间的小时数。日期-时间 API 提供了计算两个日期和时间之间经过时间的方法。有两种方法可以获得两个日期和时间之间的时间量:
-
对
ChronoUnit枚举中的一个常量使用between()方法。 -
在一个与日期时间相关的类上使用
until()方法,例如LocalDate、LocalTime、LocalDateTime、ZonedDateTime等。
ChronoUnit枚举有一个between()方法,它接受两个日期时间对象并返回一个long。方法返回从第一个参数到第二个参数所用的时间。如果第二个参数出现在第一个参数之前,它将返回一个负数。返回的数量是两个日期和时间之间的完整单位数。比如你调用HOURS.between(lt1, lt2),其中lt1和lt2分别是 07:00 和 09:30,它会返回 2,而不是 2.5。但是如果调用MINUTES.between(lt1, lt2),会返回 150。
until()方法有两个参数。第一个参数是结束日期或时间。第二个参数是计算经过时间的时间单位。清单 16-13 中的程序展示了如何使用这两种方法来计算两个日期和时间之间的时间。
// TimeBetween.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Month;
import static java.time.temporal.ChronoUnit.DAYS;
import static java.time.temporal.ChronoUnit.HOURS;
import static java.time.temporal.ChronoUnit.MINUTES;
public class TimeBetween {
public static void main(String[] args) {
LocalDate ld1 = LocalDate.of(2022, Month.JANUARY, 7);
LocalDate ld2 = LocalDate.of(2022, Month.MAY, 18);
long days = DAYS.between(ld1, ld2);
LocalTime lt1 = LocalTime.of(7, 0);
LocalTime lt2 = LocalTime.of(9, 30);
long hours = HOURS.between(lt1, lt2);
long minutes = MINUTES.between(lt1, lt2);
System.out.println("Using between (days): " + days);
System.out.println("Using between (hours): " + hours);
System.out.println("Using between (minutes): " + minutes);
// Using the until() method
long days2 = ld1.until(ld2, DAYS);
long hours2 = lt1.until(lt2, HOURS);
long minutes2 = lt1.until(lt2, MINUTES);
System.out.println("Using until (days): " + days2);
System.out.println("Using until (hours): " + hours2);
System.out.println("Using until (minutes): " + minutes2);
}
}
Using between (days): 131
Using between (hours): 2
Using between (minutes): 150
Using until (days): 131
Using until (hours): 2
Using until (minutes): 150
Listing 16-13Computing the Amount of Time Elapsed Between Two Dates and Times
并不总是能够计算出两个日期和时间之间经过的时间。例如,您不能说出LocalDate和LocalDateTime之间的小时数,因为LocalDate不存储时间部分。如果将这样的参数传递给这些方法,将引发运行时异常。规则是指定的结束日期/时间应该可以转换为开始日期/时间。
部分的
部分日期是一种日期、时间或日期时间,它不完全指定时间线上的某个时刻,但对人类仍然有意义。如果有更多的信息,部分可能与时间线上的多个瞬间相匹配。例如,12 月 25 日不是一个可以在时间线上唯一确定的完整日期;然而,当我们谈论圣诞节时,它是有意义的。同样,1 月 1 日作为元旦也是有意义的。
您必须有日期、时间和时区,以便在时间线上唯一地标识某个时刻。如果你有这三条信息中的一些,但不是全部,你就有了一部分。如果不提供更多的信息,就无法从分部中获得Instant。我们已经在前面的章节中讨论了一些部分。
LocalDate、LocalTime、LocalDateTime和OffsetTime是部分音的例子。OffsetDateTime和ZonedDateTime不是偏音;他们有信息来唯一地识别时间线上的一个瞬间。我们将在本节中讨论另外三个部分:
-
Year -
YearMonth -
MonthDay
这些部分的名字很容易描述它们。A Year代表一个年份,比如 2021,2013 等等。A YearMonth代表一年和一个月的有效组合,例如 2021-05、2013-09 等。一个MonthDay代表一个月和一个月中某一天的有效组合,例如 12-15。清单 16-14 显示了你可以在这些部分上执行的一些操作。
// Partials.java
package com.jdojo.datetime;
import java.time.Month;
import java.time.MonthDay;
import java.time.Year;
import java.time.YearMonth;
public class Partials {
public static void main(String[] args) {
// Use Year
Year y1 = Year.of(2021); // 2021
Year y2 = y1.minusYears(1); // 2020
Year y3 = y1.plusYears(1); // 2022
Year y4 = Year.now(); // current year
if (y1.isLeap()) {
System.out.println(y1 + " is a leap year.");
} else {
System.out.println(y1 + " is not a leap year.");
}
// Use YearMonth
YearMonth ym1 = YearMonth.of(2021, Month.MAY); // 2021-05
// Get the number of days in the month
int monthLen = ym1.lengthOfMonth(); // 31
System.out.println("Days in month in " + ym1 + ": " + monthLen);
// Get the number of days in the year
int yearLen = ym1.lengthOfYear(); // 365
System.out.println("Days in year in " + ym1 + ": " + yearLen);
// Use MonthDay
MonthDay md1 = MonthDay.of(Month.DECEMBER, 25);
MonthDay md2 = MonthDay.of(Month.FEBRUARY, 29);
if (md2.isValidYear(2020)) {
System.out.println(md2 + " occurred in 2020");
} else {
System.out.println(md2 + " did not occur in 2020");
}
}
}
2021 is not a leap year.
Days in month in 2021-05: 31
Days in year in 2021-05: 365
--02-29 occurred in 2020
Listing 16-14 Using Year, YearMonth, and MonthDay Partials
最后,清单 16-15 包含一个合并两个部分以得到另一个部分的例子。这是一个完整的程序,从程序运行的那一年开始计算 5 年的圣诞节。您可能会得到不同的输出。
// ChristmasDay.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.Month;
import java.time.MonthDay;
import java.time.Year;
import java.time.format.TextStyle;
import java.util.Locale;
public class ChristmasDay {
public static void main(String[] args) {
MonthDay dec25 = MonthDay.of(Month.DECEMBER, 25);
Year year = Year.now();
// Construct and print Christmas days in next five years
for (int i = 0; i < 5; i++) {
LocalDate ld = year.plusYears(i).atMonthDay(dec25);
int yr = ld.getYear();
String weekDay = ld.getDayOfWeek()
.getDisplayName(TextStyle.FULL, Locale.getDefault());
System.out.format("Christmas in %d is on %s.%n", yr, weekDay);
}
}
}
Christmas in 2021 is on Saturday.
Christmas in 2022 is on Sunday.
Christmas in 2023 is on Monday.
Christmas in 2024 is on Wednesday.
Christmas in 2025 is on Thursday.
Listing 16-15Combining a Year and MonthDay to get a LocalDate
该程序为 12 月 25 日创建了一个MonthDay部分,并一直将一年与它结合起来以得到一个LocalDate。您可以使用如下所示的LocalDate类重写清单 16-15 中的程序。它展示了日期-时间 API 的多功能性,允许您以不同的方式获得相同的结果:
LocalDate ld = LocalDate.of(Year.now().getValue(), Month.DECEMBER, 25);
for (int i = 0; i < 5; i++) {
LocalDate newDate = ld.withYear(ld.getYear() + i);
int yr = newDate.getYear();
String weekDay = newDate.getDayOfWeek()
.getDisplayName(TextStyle.FULL, Locale.getDefault());
System.out.format("Christmas in %d is on %s.%n", yr, weekDay);
}
调整日期
有时您希望调整日期和时间以具有特定的特征,例如,每月的第一个星期一、下一个星期二等。您可以使用TemporalAdjuster接口的实例对日期和时间进行调整。该接口有一个方法adjustInto(),它接受一个Temporal并返回一个Temporal。日期时间 API 提供了几个常用的日期时间调节器。如果他们不适合你的需要,你可以推出自己的调整器。我们讨论两者的例子。
提供了一个TemporalAdjusters类。它由返回不同类型的预定义日期调整器的所有静态方法组成。与日期时间相关的类包含一个with(TemporalAdjuster adjuster)方法。您需要将从TemporalAdjusters类的方法之一返回的对象传递给with()方法。with()方法将通过使用调整器中的逻辑调整其组件来返回原始日期时间对象的副本。以下代码片段计算 2022 年 1 月 1 日之后的第一个星期一:
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
import java.time.temporal.TemporalAdjusters;
...
LocalDate ld1 = LocalDate.of(2022, Month.JANUARY, 1);
LocalDate ld2 = ld1.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
System.out.println(ld1);
System.out.println(ld2);
2022-01-01
2022-01-03
方法名是不言自明的,如表 16-4 所示。
表 16-4
TemporalAdjusters 类中的有用方法
|方法
|
描述
|
| --- | --- |
| next(DayOfWeek dayOfWeek) | 返回一个调整器,该调整器将日期调整为被调整日期之后一周中的第一个指定日期。 |
| nextOrSame(DayOfWeek dayOfWeek) | 返回一个调整器,该调整器将日期调整为被调整日期之后一周中的第一个指定日期。如果要调整的日期已经是一周中的某一天,则返回相同的日期。 |
| previous(DayOfWeek dayOfWeek) | 返回一个调整器,该调整器将日期调整为被调整日期之前一周的第一个指定日期。 |
| previousOrSame(DayOfWeek dayOfWeek) | 返回一个调整器,该调整器将日期调整为被调整日期之前一周的第一个指定日期。如果要调整的日期已经是一周中的某一天,则返回相同的日期。 |
| firstInMonth(DayOfWeek dayOfWeek),``lastInMonth(DayOfWeek dayOfWeek) | 每个都返回一个调整符,该调整符将日期调整为被调整的日期所代表的月份中指定的第一/最后一天。 |
| dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) | 返回一个调整器,该调整器将日期调整为被调整的日期所代表的月份中指定的一周的第ordinal天。它适用于计算日期,如 2022 年 1 月的第三个星期一。 |
| firstDayOfMonth()``lastDayOfMonth() | 每个都返回一个调整符,该调整符将日期调整为被调整日期所代表的月份的第一天/最后一天。 |
| firstDayOfYear()``lastDayOfYear() | 每个都返回一个调整符,该调整符将日期调整为被调整的日期所代表的一年的第一天/最后一天。 |
| firstDayOfNextMonth() | 返回一个调整器,该调整器将日期调整为被调整日期所代表的下个月的第一天。 |
| firstDayOfNextYear() | 返回一个调整器,该调整器将日期调整为被调整日期所代表的下一年的第一天。 |
| ofDateAdjuster(UnaryOperator``<LocalDate> dateBasedAdjuster) | 开发人员编写自己的LocalDate-based调整器的一种方便方法。 |
TemporalAdjusters类提供了一个dayOfWeekInMonth()方法。该方法返回一个日期调整器,它将日期调整到一周中指定的ordinal日,例如,一个月的第一个星期天,一个月的第三个星期五,等等。指定的ordinal值可能在 1 和 5 之间。如果ordinal为 5,并且该月没有第五个指定的dayOfWeek,则从下个月开始返回第一个指定的dayOfWeek。以下代码片段请求日期调整器在 2021 年 6 月的第五个星期天。日期调整器返回 2021 年 7 月的第一个星期日,因为 2021 年 6 月没有第五个星期日:
LocalDate ld1 = LocalDate.of(2021, Month.JUNE, 22);
LocalDate ld2 = ld1.with(TemporalAdjusters.dayOfWeekInMonth(6, DayOfWeek.SUNDAY));
System.out.println(ld1);
System.out.println(ld2);
2021-06-22
2021-07-04
您可以使用日期调整器和其他方法来执行复杂的调整。您可以获得从今天起 3 个月 14 天后的第二个星期五的日期,如下所示:
LocalDate date = LocalDate.now()
.plusMonths(3)
.plusDays(14)
.with(DateAdjusters.dayOfWeekInMonth(2, DayOfWeek.FRIDAY));
您可以使用ofDateAdjuster()方法为LocalDate创建自己的日期调整器。下面的代码片段创建并使用了一个日期调整器。调整器在被调整的日期上增加了 3 个月零 2 天。请注意,我们使用了一个 lambda 表达式来创建调整器,我们在第十一章中简要讨论过:
// Create an adjuster that returns a date after 3 months and 2 days
TemporalAdjuster adjuster =
TemporalAdjusters.ofDateAdjuster((LocalDate date) -> date.plusMonths(3).plusDays(2));
// Use the adjuster
LocalDate today = LocalDate.now();
LocalDate dayAfter3Mon2Day = today.with(adjuster);
System.out.println("Today: " + today);
System.out.println("After 3 months and 2 days: " + dayAfter3Mon2Day);
Today: 2021-08-20
After 3 months and 2 days: 2021-11-22
清单 16-16 演示了如何调整日期。
// AdjustDates.java
package com.jdojo.datetime;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
public class AdjustDates {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
System.out.println("Today: " + today);
// Use a DateAdjuster to adjust today’s date to the next Monday
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
System.out.println("Next Monday: " + nextMonday);
// Use a DateAdjuster to adjust today’s date to the last day of month
LocalDate lastDayOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
System.out.println("Last day of month: " + lastDayOfMonth);
// Create an adjuster that returns a date after 3 months and 2 days
TemporalAdjuster adjuster = TemporalAdjusters.ofDateAdjuster(
(LocalDate date) -> date.plusMonths(3).plusDays(2));
LocalDate dayAfter3Mon2Day = today.with(adjuster);
System.out.println("Date after adding 3 months and 2 days: " + dayAfter3Mon2Day);
}
}
Today: 2021-08-20
Next Monday: 2021-08-23
Last day of month: 2021-08-31
Date after adding 3 months and 2 days: 2021-11-22
Listing 16-16Adjusting Dates and Times
让我们创建一个自定义日期调整器。如果被调整的日期是周末或 13 号星期五,则返回下一个星期一。否则,它返回原始日期。也就是说,调整器将只返回工作日,除了星期五 13。清单 16-17 包含调整器的完整代码。调节器已被定义为类中的常量。使用调整器就像将CustomAdjusters.WEEKDAYS_WITH_NO_FRIDAY_13常量传递给 datetime 类的with()方法一样简单,datetime 类可以提供一个LocalDate:
// CustomAdjusters.java
package com.jdojo.datetime;
import java.time.DayOfWeek;
import static java.time.DayOfWeek.FRIDAY;
import static java.time.DayOfWeek.MONDAY;
import static java.time.DayOfWeek.SATURDAY;
import static java.time.DayOfWeek.SUNDAY;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
public class CustomAdjusters {
public final static TemporalAdjuster WEEKDAYS_WITH_NO_FRIDAY_13
= TemporalAdjusters.ofDateAdjuster(CustomAdjusters::getWeekDayNoFriday13);
// No public constructor as it is a utility class
private CustomAdjusters() {
}
private static LocalDate getWeekDayNoFriday13(LocalDate date) {
// Initialize the new date with the original one
LocalDate newDate = date;
DayOfWeek day = date.getDayOfWeek();
if (day == SATURDAY || day == SUNDAY || (day == FRIDAY && date.getDayOfMonth() == 13)) {
// Return next Monday
newDate = date.with(TemporalAdjusters.next(MONDAY));
}
return newDate;
}
}
Listing 16-17Creating a Custom Date Adjuster
LocalDate ld = LocalDate.of(2013, Month.DECEMBER, 13); // Friday
LocalDate ldAdjusted = ld.with(CustomAdjusters.WEEKDAYS_WITH_NO_FRIDAY_13); // Next Monday
清单 16-18 演示了如何使用定制日期调整器。2021 年 8 月 12 日,星期四。您使用调整器调整 2021 年的 8 月 12 日、13 日和 14 日。2021 年 8 月 12 日,返回时没有任何调整。另外两个日期调整到下周一,也就是 2021 年 8 月 16 日。注意,调整器可以用在任何能够提供LocalDate的 datetime 对象上。程序用它来调整一个ZonedDateTime。
// CustomAdjusterTest.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class CustomAdjusterTest {
public static void main(String[] args) {
LocalDate ld1 = LocalDate.of(2021, Month.AUGUST, 12); // Thursday
LocalDate ld2 = LocalDate.of(2021, Month.AUGUST, 13); // Friday
LocalDate ld3 = LocalDate.of(2021, Month.AUGUST, 14); // Saturday
LocalDate ld1Adjusted = ld1.with(CustomAdjusters.WEEKDAYS_WITH_NO_FRIDAY_13);
System.out.println(ld1 + " adjusted to " + ld1Adjusted);
LocalDate ld2Adjusted = ld2.with(CustomAdjusters.WEEKDAYS_WITH_NO_FRIDAY_13);
System.out.println(ld2 + " adjusted to " + ld2Adjusted);
LocalDate ld3Adjusted = ld3.with(CustomAdjusters.WEEKDAYS_WITH_NO_FRIDAY_13);
System.out.println(ld3 + " adjusted to " + ld3Adjusted);
// Use it to adjust a ZonedDateTime
ZonedDateTime zdt
= ZonedDateTime.of(ld2, LocalTime.of(8, 45), ZoneId.of("America/Chicago"));
ZonedDateTime zdtAdjusted = zdt.with(CustomAdjusters.WEEKDAYS_WITH_NO_FRIDAY_13);
System.out.println(zdt + " adjusted to " + zdtAdjusted);
}
}
2021-08-12 adjusted to 2021-08-12
2021-08-13 adjusted to 2021-08-16
2021-08-14 adjusted to 2021-08-16
2021-08-13T08:45-05:00[America/Chicago] adjusted to 2021-08-16T08:45-05:00[America/Chicago]
Listing 16-18Using the Custom Date Adjuster
查询日期时间对象
所有日期时间类都支持查询。查询是对信息的请求。请注意,您可以使用 datetime 对象的get(TemporalField field)方法获得 datetime 对象的组成部分,例如,来自LocalDate的年份。使用查询来请求不作为组件提供的信息。例如,您可以查询一个LocalDate是否是 13 号星期五。查询的结果可以是任何类型。
TemporalQuery<R>接口的一个实例代表一个查询。所有 datetime 类都包含一个query()方法,该方法将一个TemporalQuery作为参数并返回一个结果。
TemporalQueries是一个包含几个预定义查询作为其静态方法的实用程序类,如表 16-5 所示。如果 datetime 对象没有查询中查找的信息,查询将返回 null。例如,对来自LocalTime对象的LocalDate的查询返回null.年表,这是一个用于在日历系统中识别和操作日期的接口。
表 16-5
TemporalQueries 类中的实用方法
|方法
|
返回类型
|
描述
|
| --- | --- | --- |
| chronology() | TemporalQuery<Chronology> | 获取年表的查询。 |
| localDate() | TemporalQuery<LocalDate> | 获取LocalDate的查询。 |
| localTime() | TemporalQuery<LocalTime> | 获取LocalTime的查询。 |
| offset() | TemporalQuery<ZoneOffset> | 获取ZoneOffset的查询。 |
| precision() | TemporalQuery<TemporalUnit> | 获取支持的最小单位的查询。 |
| zone() | TemporalQuery<ZoneId> | 获取ZoneId的查询。如果ZoneId不可用,它将查询ZoneOffset。如果两者都不可用,则返回 null,例如,LocalDate两者都不可用。 |
| zoneId() | TemporalQuery<ZoneId> | 获取ZoneId的查询。如果ZoneId不可用,则返回 null。 |
清单 16-19 中的程序展示了如何使用预定义的查询。它使用查询从一个LocalDate、一个LocalTime和一个ZonedDateTime中获得精度和LocalDate。该程序使用当前日期,因此您可能会得到不同的输出。
// QueryTest.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalQueries;
import java.time.temporal.TemporalQuery;
import java.time.temporal.TemporalUnit;
public class QueryTest {
public static void main(String[] args) {
// Get references of the precision and local date queries
TemporalQuery<TemporalUnit> precisionQuery = TemporalQueries.precision();
TemporalQuery<LocalDate> localDateQuery = TemporalQueries.localDate();
// Query a LocalDate
LocalDate ld = LocalDate.now();
TemporalUnit precision = ld.query(precisionQuery);
LocalDate queryDate = ld.query(localDateQuery);
System.out.println("Precision of LocalDate: " + precision);
System.out.println("LocalDate of LocalDate: " + queryDate);
// Query a LocalTime
LocalTime lt = LocalTime.now();
precision = lt.query(precisionQuery);
queryDate = lt.query(localDateQuery);
System.out.println("Precision of LocalTime: " + precision);
System.out.println("LocalDate of LocalTime: " + queryDate);
// Query a ZonedDateTime
ZonedDateTime zdt = ZonedDateTime.now();
precision = zdt.query(precisionQuery);
queryDate = zdt.query(localDateQuery);
System.out.println("Precision of ZonedDateTime: " + precision);
System.out.println("LocalDate of ZonedDateTime: " + queryDate);
}
}
Precision of LocalDate: Days
LocalDate of LocalDate: 2021-08-20
Precision of LocalTime: Nanos
LocalDate of LocalTime: null
Precision of ZonedDateTime: Nanos
LocalDate of ZonedDateTime: 2021-08-20
Listing 16-19Querying Datetime Objects
创建和使用自定义查询很容易。有两种方法可以创建自定义查询。
-
创建一个实现
TemporalQuery接口的类,并使用该类的实例作为查询。 -
使用任何方法引用作为查询。该方法应该接受一个
TemporalAccessor并返回一个对象。方法的返回类型定义了查询的结果类型。
清单 16-20 包含了一个Friday13Query类的代码。该类实现了TemporalQuery接口。queryFrom()方法是接口实现的一部分。如果 datetime 对象包含的日期是星期五 13,则该方法返回 true。否则,它返回 false。如果 datetime 对象不包含一个月中的某一天和一周中的某一天的信息,例如一个LocalTime对象,则查询返回 false。该类定义了一个可以用作查询的常量IS_FRIDAY_13。
// Friday13Query.java
package com.jdojo.datetime;
import java.time.DayOfWeek;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQuery;
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.DAY_OF_WEEK;
import static java.time.DayOfWeek.FRIDAY;
public class Friday13Query implements TemporalQuery<Boolean> {
public final static Friday13Query IS_FRIDAY_13 = new Friday13Query();
// Prevent outside code from creating objects of this class
private Friday13Query() {
}
@Override
public Boolean queryFrom(TemporalAccessor temporal) {
if (temporal.isSupported(DAY_OF_MONTH) && temporal.isSupported(DAY_OF_WEEK)) {
int dayOfMonth = temporal.get(DAY_OF_MONTH);
int weekDay = temporal.get(DAY_OF_WEEK);
DayOfWeek dayOfWeek = DayOfWeek.of(weekDay);
if (dayOfMonth == 13 && dayOfWeek == FRIDAY) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
}
Listing 16-20A Class Implementing the TemporalQuery Interface
下面的代码片段将Friday13Query与三个 datetime 对象一起使用。第一个LocalDate发生在星期五 13,正如您在输出中看到的,查询返回 true。
LocalDate ld1 = LocalDate.of(2021, 8, 13);
Boolean isFriday13 = ld1.query(Friday13Query.IS_FRIDAY_13);
System.out.println("Date: " + ld1 + ", isFriday13: " + isFriday13);
LocalDate ld2 = LocalDate.of(2022, 1, 10);
isFriday13 = ld2.query(Friday13Query.IS_FRIDAY_13);
System.out.println("Date: " + ld2 + ", isFriday13: " + isFriday13);
LocalTime lt = LocalTime.of(7, 30, 45);
isFriday13 = lt.query(Friday13Query.IS_FRIDAY_13);
System.out.println("Time: " + lt + ", isFriday13: " + isFriday13);
Date: 2021-08-13, isFriday13: true
Date: 2022-01-10, isFriday13: false
Time: 07:30:45, isFriday13: false
清单 16-21 包含一个CustomQueries类的代码。该类包含一个静态方法isFriday13()。isFriday13()方法的方法引用可以用作查询。
// CustomQueries.java
package com.jdojo.datetime;
import java.time.DayOfWeek;
import static java.time.DayOfWeek.FRIDAY;
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.DAY_OF_WEEK;
import java.time.temporal.TemporalAccessor;
public class CustomQueries {
public static Boolean isFriday13(TemporalAccessor temporal) {
if (temporal.isSupported(DAY_OF_MONTH) && temporal.isSupported(DAY_OF_WEEK)) {
int dayOfMonth = temporal.get(DAY_OF_MONTH);
int weekDay = temporal.get(DAY_OF_WEEK);
DayOfWeek dayOfWeek = DayOfWeek.of(weekDay);
if (dayOfMonth == 13 && dayOfWeek == FRIDAY) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
}
Listing 16-21A CustomQueries Class with a IsFriday13 Method That Can Be Used a Query
下面的代码片段使用CustomQueries类中的isFriday13()方法的方法引用作为查询。该代码使用与上一示例中相同的 datetime 对象,您会得到相同的结果:
LocalDate ld1 = LocalDate.of(2021, 8, 13);
Boolean isFriday13 = ld1.query(CustomQueries::isFriday13);
System.out.println("Date: " + ld1 + ", isFriday13: " + isFriday13);
LocalDate ld2 = LocalDate.of(2022, 1, 10);
isFriday13 = ld2.query(CustomQueries::isFriday13);
System.out.println("Date: " + ld2 + ", isFriday13: " + isFriday13);
LocalTime lt = LocalTime.of(7, 30, 45);
isFriday13 = lt.query(CustomQueries::isFriday13);
System.out.println("Time: " + lt + ", isFriday13: " + isFriday13);
Date: 2021-08-13, isFriday13: true
Date: 2022-01-10, isFriday13: false
Time: 07:30:45, isFriday13: false
日期-时间 API 通常会提供多种选择来执行相同的任务。让我们考虑一个从一个ZonedDateTime获取LocalTime的任务。清单 16-22 中的程序显示了实现这一点的五种方法:
// LocalTimeFromZonedDateTime.java
package com.jdojo.datetime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalQueries;
public class LocalTimeFromZonedDateTime {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
// Use the toLocalTime() method of the ZonedDateTime class (preferred)
LocalTime lt1 = zdt.toLocalTime();
// Use the from() method of the LocalTime class
LocalTime lt2 = LocalTime.from(zdt);
// Use the localTime() query
LocalTime lt3 = zdt.query(TemporalQueries.localTime());
// Use the LocalTime::from method as a query
LocalTime lt4 = zdt.query(LocalTime::from);
// Get all time components and construct a LocalTime
int hours = zdt.getHour();
int minutes = zdt.getMinute();
int seconds = zdt.getSecond();
int nanos = zdt.getNano();
LocalTime lt5 = LocalTime.of(hours, minutes, seconds, nanos);
// Print all LocalTimes
System.out.println("zdt: " + zdt);
System.out.println("lt1: " + lt1);
System.out.println("lt2: " + lt2);
System.out.println("lt3: " + lt3);
System.out.println("lt4: " + lt4);
System.out.println("lt5: " + lt5);
}
}
zdt: 2021-08-04T21:11:42.547440400-05:00[America/Chicago]
lt1: 21:11:42.547440400
lt2: 21:11:42.547440400
lt3: 21:11:42.547440400
lt4: 21:11:42.547440400
lt5: 21:11:42.547440400
Listing 16-22Multiple Ways of Getting the LocalTime from a ZonedDateTime
哪种方法才是正确的方法?大多数情况下,所有方法都会执行相同的逻辑。然而,有些方法比其他方法更具可读性。在这种情况下,应该使用调用ZonedDateTime类的toLocalTime()方法的代码,因为它简单明了,可读性最好。至少,您不应该从ZonedDateTime中提取时间成分来构造LocalTime,如示例中的第五种方法所示。
非 ISO 日历系统
日期类如LocalDate使用的是 ISO 日历系统,也就是公历。日期-时间 API 还允许您使用其他日历,如泰国佛教日历、回历、民国日历和日本日历。非 ISO 日历相关的类在java.time.chrono包中。
每个可用的非 ISO 日历系统都有一个XxxChronology和XxxDate类。XxxChronology类表示Xxx日历系统,而XxxDate类表示Xxx日历系统中的日期。每个XxxChronology类包含一个INSTANCE常量,代表该类的一个单独实例。例如,HijrahChronology和HijrahDate是您将用来处理回历系统的类。下面的代码片段显示了获取泰国佛教日历中当前日期的两种方法。您可能会得到不同的输出:
import java.time.chrono.ThaiBuddhistChronology;
import java.time.chrono.ThaiBuddhistDate;
...
ThaiBuddhistChronology thaiBuddhistChrono = ThaiBuddhistChronology.INSTANCE;
ThaiBuddhistDate now = thaiBuddhistChrono.dateNow();
ThaiBuddhistDate now2 = ThaiBuddhistDate.now();
System.out.println("Current Date in Thai Buddhist: " + now);
System.out.println("Current Date in Thai Buddhist: " + now2);
Current Date in Thai Buddhist: ThaiBuddhist BE 2564-08-20
Current Date in Thai Buddhist: ThaiBuddhist BE 2564-08-20
您也可以将一种日历系统中的日期转换为另一种日历系统。也允许从 ISO 日期转换到非 ISO 日期。将日期从一种日历系统转换到另一种日历系统,只需调用目标日期类的from()静态方法,并将源日期对象作为其参数传递。清单 16-23 展示了如何将 ISO 日期转换成泰国佛教日期,反之亦然。您可能会得到不同的输出。
// InterCalendarDates.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.chrono.ThaiBuddhistDate;
public class InterCalendarDates {
public static void main(String[] args) {
ThaiBuddhistDate thaiBuddhistNow = ThaiBuddhistDate.now();
LocalDate isoNow = LocalDate.now();
System.out.println("Thai Buddhist Current Date: " + thaiBuddhistNow);
System.out.println("ISO Current Date: " + isoNow);
// Convert Thai Buddhist date to ISO date and vice versa
ThaiBuddhistDate thaiBuddhistNow2 = ThaiBuddhistDate.from(isoNow);
LocalDate isoNow2 = LocalDate.from(thaiBuddhistNow);
System.out.println("Thai Buddhist Current Date from ISO: " + thaiBuddhistNow2);
System.out.println("ISO Current Date from Thai Buddhist: " + isoNow2);
}
}
Thai Buddhist Current Date: ThaiBuddhist BE 2564-08-20
ISO Current Date: 2021-08-20
Thai Buddhist Current Date from ISO: ThaiBuddhist BE 2564-08-20
ISO Current Date from Thai Buddhist: 2021-08-20
Listing 16-23Using the Thai Buddhist and ISO Calendars
格式化日期和时间
DateTimeFormatter类的对象允许您格式化和解析日期时间对象。通过格式化,我的意思是以用户定义的文本形式表示日期时间对象,例如,将 2021 年 5 月 24 日的LocalDate表示为“05/24/2021”有时格式化也被称为打印,因为格式化特性还允许您将日期时间对象的文本表示打印(或输出)到Appendable对象,比如StringBuilder。
解析是格式化的逆过程。它允许您从日期时间的文本表示中构造一个日期时间对象。从文本“05/24/2021”创建一个LocalDate对象来表示 2021 年 5 月 24 日,这是解析的一个例子。
存在不同的格式化和解析日期时间的方法。如果学习方法不正确,学习如何格式化日期时间可能会很困难。要记住的最重要的一点是格式化和解析总是由DateTimeFormatter类的对象来执行。区别在于如何创建该对象。DateTimeFormatter类不提供任何公共构造器。您必须间接获取它的对象。一开始,困惑在于如何获得它的对象。使用DateTimeFormatter类的以下两种方法之一来格式化日期、时间或日期时间:
-
String format(TemporalAccessor temporal) -
void formatTo(TemporalAccessor temporal, Appendable appendable)
format()方法接受一个日期、时间或日期时间对象,并根据格式化程序的规则返回该对象的文本表示。formatTo()方法允许您将对象的文本表示写入一个Appendable,例如,一个文件、一个StringBuilder等。
要格式化 datetime 对象,格式化程序需要两条信息:格式模式和区域设置。有时一条或两条信息都是默认的;有时候,你提供了它们。
您可以用几种方式执行格式化。它们都直接或间接地使用一个DateTimeFormatter对象:
-
使用预定义的标准日期时间格式器
-
使用日期时间类的
format()方法 -
使用用户定义的模式
-
使用
DateTimeFormatterBuilder类
使用预定义的格式化程序
预定义的格式化程序在DateTimeFormatter类中被定义为常量。它们在表 16-6 中列出。大多数格式化程序使用 ISO 日期时间格式;一些格式化程序使用稍加修改的 ISO 格式。
表 16-6
预定义的日期时间格式器
|格式程序
|
描述
|
例子
|
| --- | --- | --- |
| BASIC_ISO_DATE | 一个 ISO 日期格式化程序,用于格式化和解析日期,而无需在两个日期部分之间使用分隔符。 | 20140109, 20140109-0600 |
| ISO_DATE,ISO_TIME,ISO_DATE_TIME | 日期、时间和日期时间格式化程序,使用 ISO 分隔符格式化和解析日期、时间和日期时间。 | 2014-01-09, 2014-01-09-06:00,``15:38:32.927, 15:38:32.943-06:00,``2014-01-09T15:20:07.747-06:00, 2014-01-09T15:20:07.825-06:00[America/Chicago] |
| ISO_INSTANT | 一个 instant formatter,用于格式化和解析 UTC 格式的 instant(或表示 instant 的 datetime 对象,如ZonedDateTime)。 | 2014-01-09T21:23:56.870Z |
| ISO_LOCAL_DATE,ISO_LOCAL_TIME,ISO_LOCAL_DATE_TIME | 日期、时间和日期时间格式化程序,用于格式化或分析不带偏移量的日期、时间和日期时间。 | 2014-01-09, 15:30:14.352, 2014-01-09T15:29:11.384 |
| ISO_OFFSET_DATE,ISO_OFFSET_TIME,ISO_OFFSET_DATE_TIME | 日期、时间和日期时间格式化程序,使用 ISO 格式格式化和解析带有偏移量的日期、时间和日期时间。 | 2014-01-09-06:00,``15:34:29.851-06:00,``2014-01-09T15:33:07.07-06:0 |
| ISO_ZONED_DATE_TIME | 日期时间格式化程序,用于格式化和分析带有区域 ID 的日期时间(如果有)。 | 2014-01-09T15:45:49.112-06:00, 2014-01-09T15:45:49.128-06:00[America/Chicago] |
| ISO_ORDINAL_DATE | 一个日期格式化程序,用于格式化和解析带有年份和日期的日期。 | 2014-009 |
| ISO_WEEK_DATE | 一个日期格式化程序,用于格式化和解析基于周的日期。格式为年-周-年-日-周。例如,2014-W02-4 表示 2014 年第二周的第四天。 | 2014-W02-4,``2014-W02-4-06:00 |
| RFC_1123_DATE_TIME | 使用 RFC1123 规范格式化和解析电子邮件日期时间的日期时间格式化程序。 | Thu, 9 Jan 2014 15:50:44 -05:00 |
使用预定义的格式化程序很简单:只需将日期/时间对象传递给format()。下面的代码片段使用ISO_DATE格式化程序来格式化一个LocalDate、OffsetDateTime和ZonedDateTime。当它格式化并打印当前日期时,您可能会得到不同的输出:
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import static java.time.format.DateTimeFormatter.ISO_DATE;
...
// Format dates using the ISO_DATE formatter
String ldStr = ISO_DATE.format(LocalDate.now());
String odtStr = ISO_DATE.format(OffsetDateTime.now());
String zdtStr = ISO_DATE.format(ZonedDateTime.now());
System.out.println("Local Date: " + ldStr);
System.out.println("Offset Datetime: " + odtStr);
System.out.println("Zoned Datetime: " + zdtStr);
Local Date: 2021-08-20
Offset Datetime: 2021-08-20-04:00
Zoned Datetime: 2021-08-20-04:00
请注意预定义格式化程序的名称。正在格式化的 datetime 对象必须包含如其名称所示的组件。例如,ISO_DATE格式化程序期望日期组件的存在,因此,它不应该用于格式化仅时间对象,如LocalTime。类似地,ISO_TIME格式化程序也不应该用来格式化LocalDate:
// A runtime error as a LocalTime does not contain date components
String ltStr = ISO_DATE.format(LocalTime.now());
使用 Datetime 类的 format()方法
您可以使用 datetime 对象的format()方法来格式化它。format()方法接受一个DateTimeFormatter类的对象。下面的代码片段使用了这种方法。使用ISO_DATE格式器:
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import static java.time.format.DateTimeFormatter.ISO_DATE;
...
LocalDate ld = LocalDate.now();
String ldStr = ld.format(ISO_DATE);
System.out.println("Local Date: " + ldStr);
OffsetDateTime odt = OffsetDateTime.now();
String odtStr = odt.format(ISO_DATE);
System.out.println("Offset Datetime: " + odtStr);
ZonedDateTime zdt = ZonedDateTime.now();
String zdtStr = zdt.format(ISO_DATE);
System.out.println("Zoned Datetime: " + zdtStr);
Local Date: 2021-08-20
Offset Datetime: 2021-08-20-04:00
Zoned Datetime: 2021-08-20-04:00
使用用户定义的模式
DateTimeFormatter类中最常用的方法之一是ofPattern()方法,它返回一个具有指定格式模式和区域设置的DateTimeFormatter对象:
-
static DateTimeFormatter ofPattern(String pattern) -
static DateTimeFormatter ofPattern(String pattern, Locale locale)
下面的代码片段获取了两个格式化程序来将日期格式化为“年、月、日”格式。第一个格式化程序以默认语言环境格式化日期时间,第二个格式化程序以德语语言环境格式化日期时间:
// Get a formatter for the default locale
DateTimeFormatter fmt1 = DateTimeFormatter.ofPattern("MMMM dd, yyyy");
// Get a formatter for the German locale
DateTimeFormatter fmt2 = DateTimeFormatter.ofPattern("MMMM dd, yyyy", Locale.GERMAN);
有时您有一个用于模式和地区的DateTimeFormatter对象。您希望使用相同的模式来格式化另一个地区的日期时间。DateTimeFormatter类有一个withLocale()方法,为使用相同模式的指定地区返回一个DateTimeFormatter对象。在前面的代码片段中,您可以用下面的语句替换第二个语句:
// Get a formatter for the German locale using the same pattern as fmt1
DateTimeFormatter fmt2 = fmt1.withLocale(Locale.GERMAN);
Tip
使用DateTimeFormatter类的getLocale()方法来了解它将用来格式化日期时间的区域设置。
日期时间格式是基于一种模式执行的。格式模式是一系列具有特殊含义的字符。例如,模式中的 MMMM 使用月份的完整拼写名称,如一月、二月等。;MMM 使用月份名称的缩写形式,如一月、二月等。;MM 使用两位数的月份号,如 01、02 等。;m 使用一位数或两位数的月份数,如 1、2、10、11 等。
在一个格式模式中,有些字符有特殊的含义,有些是按字面意思使用的。具有特殊含义的字符将被格式化程序解释,它们将被替换为日期时间成分。格式化程序输出出现在模式中的文字字符。所有字母 A–Z 和 A–Z 都被保留为模式字母,尽管并非所有字母都被使用。如果您想在模式中包含一个文字字符串,您需要用单引号将它括起来。要输出单引号,您需要使用两个连续的单引号。
日期时间格式化程序直接输出除[,]和单引号以外的任何非字母字符。但是,建议您用单引号将它们括起来。假设您的本地日期是 2021 年 5 月 29 日。“1997 MMMM dd,yyyy”和“‘1997’MMMM DD,yyyy”两种模式都将输出 1997 年 5 月 29 日 2021;但是,建议使用后者,它在文字 1997 前后使用单引号。
表 16-7 列出了图案中使用的符号及其含义。表中的所有示例都使用“2021-07-29t 07:30:12.789-05:00[美国/芝加哥]”作为输入日期时间。
表 16-7
日期时间格式符号和示例说明
|标志
|
描述
|
例子
| | --- | --- | --- | | | | 图案 | 输出 | | G | 时代 | G | 广告 | | 俄文 | 域年份 | | 回答 | A | | u | 年它可以是正数,也可以是负数。在纪元开始日期之后,它是一个正数。在纪元开始日期之前,它是一个负数。例如,公元 2014 年的年值是 2014 年,而公元前 2014 年的年值是–2014 年。 | u/uuu/uuuu | Two thousand and twenty-one | | 溃疡性龈炎 | Twelve | | 别发牢骚 | 02021 | | y | 纪元年它从纪元开始日期向前或向后计算年份。它总是一个正数。例如,公元 2014 年的年值是 2014 年,公元前 2014 年的年值是 2015 年。在公共纪元中,0 年是公元前 1 年。 | 是/是/是 | Two thousand and twenty-one | | 尤尼克斯 | Twelve | | 是的 | 02021 | | D | 一年中的第几天(1–366) | D | One hundred and fifty | | 男/女 | 一年中的月份 | M | five | | 梅智节拍器 | 05 | | 嗯 | 七月 | | 嗯 | 七月 | | d | 一月中的某一天 | d | 5, 29 | | 截止日期(Deadline Date 的缩写) | 05, 29 | | g | 改良儒略日(在 Java 9 中添加) | g | Fifty-seven thousand seven hundred and ninety-six | | ggg | Fifty-seven thousand seven hundred and ninety-six | | 个性签名 | 057796 | | 问/问 | 一年中的一个季度 | Q | three | | 即时通信软件 | 03 | | 即时通信软件 | Q3 | | QQQQ | 第三季度 | | Y | 基于周的年份 | Y | Two thousand and twenty-one | | 尤尼克斯 | Twelve | | YYYY 年年 | Two thousand and twenty-one | | w | 基于周的一年中的周 | w | Thirty-one | | W | 每月的第几周 | W | five | | E | 星期几 | E | seven | | 电子工程师 | 07 | | 东方马脑脊髓炎 | 太阳 | | 依依社区防屏蔽 | 在星期日 | | F | 一个月中的星期几 | F | one | | a | 一天的上午/下午 | a | 是 | | h | 上午/下午的时钟小时(1–12) | h | seven | | K | 上午/下午的时间(0–11) | K | seven | | k | 上午/下午的时钟小时(1–24) | k | seven | | H | 一天中的小时(0–23) | H | seven | | 殿下 | 07 | | m | 一小时中的分钟 | 毫米 | Thirty | | s | 分钟的秒 | 悬浮物 | Twelve | | S | 几分之一秒 | SSSSSSSSS | 000000789 | | A | 一天中的毫秒 | A | Twenty-seven million and twelve thousand | | n | 毫微秒 | n | Seven hundred and eighty-nine | | 普通 | 一天的纳秒 | 普通 | 27012000000789 | | V | 时区 ID | 卷 | 美国/芝加哥 | | v | 通用非位置区域名称(在 Java 9 中添加) | v | 计算机化 X 线体层照相术 | | vvv | 中央标准时间 | | z | 时区名称 | z | 中央日光时间 | | Z | 区域偏移当区域偏移为零时,它输出+0000 或+00:00,这取决于您是使用 Z、ZZ 还是 ZZZ。 | Z | –0500 | | 锯齿形 | –0500 | | 打鼾声 | –05:00 | | ZZZ | GMT-05:00 | | O | 局部区域偏移 | O | GMT-5 | | X | 区域偏移与符号 Z 不同,它打印区域偏移零的 Z。如果分和秒都是零,例如+09,x 只输出小时;XX 输出小时和分钟,不带冒号,比如+0830;XXX 输出带有冒号的小时和分钟,例如+08:30;XXXX 输出小时、分钟和可选的秒,不带冒号,比如+083045;XXXXX 用冒号输出小时、分钟和可选的秒,例如+08:30:45。 | X | +0530 | | xx | +0530 | | XXX | +05:30 | | 电影站 | +053045 | | 五 x 综合征 | +05:30:45 | | x | 与 X 相同,只是它为区域偏移零打印+00,而不是 z。 | xx | -0500 | | p | 填充下一个它用空格填充后面模式的输出。比如 mm 输出 30,pppmm 输出‘30’,pppmm 输出‘30’。ps 的数量决定输出的宽度。 | pppmm | ' 30' | | | (单引号显示了带有空格的填充。) | | ' | 文本转义单引号内的文本直接输出。要输出单引号,请使用两个连续的单引号。 | 你好 | 你好 | | 你好,MMMM | 你好,七月 | | '' | 单引号 | '''你好' ' ' MMMM | 你好,七月 | | [ ] | 可选部分参考讨论中的例子。 | | | | #, {, } | 这些是留作将来使用的。 | | |
模式字符串中可以有可选的部分。符号[和]分别表示可选部分的开始和结束。只有当所有元素的信息都可用时,才输出包含在可选部分中的模式。否则,跳过可选部分。可选节可以嵌套在另一个可选节中。清单 16-24 展示了如何在模式中使用可选部分。可选部分包含时间信息。设置日期格式时,将跳过可选部分。
// OptionalSectionTest.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.format.DateTimeFormatter;
public class OptionalSectionTest {
public static void main(String[] args) {
// A pattern with an optional section
String pattern = "MM/dd/yyyy[ 'at' HH:mm:ss]";
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern);
LocalDate ld = LocalDate.of(2021, Month.MAY, 30);
LocalTime lt = LocalTime.of(17, 30, 12);
LocalDateTime ldt = LocalDateTime.of(ld,lt);
// Format a date. Optional section will be skipped because a
// date does not have time (HH, mm, and ss) information.
String str1 = fmt.format(ld);
System.out.println(str1);
// Format a datetime. Optional section will be output.
String str2 = fmt.format(ldt);
System.out.println(str2);
}
}
05/30/2021
05/30/2021 at 17:30:12
Listing 16-24Using an Optional Section in a Datetime Formatting Pattern
清单 16-25 展示了如何使用不同的模式来格式化日期和时间。
// FormattingDateTime.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
import java.util.Locale;
public class FormattingDateTime {
public static void main(String[] args) {
LocalDate ld = LocalDate.of(2021, Month.APRIL, 30);
System.out.println("Formatting date: " + ld);
format(ld, "M/d/yyyy");
format(ld, "MM/dd/yyyy");
format(ld, "MMM dd, yyyy");
format(ld, "MMMM dd, yyyy");
format(ld, "EEEE, MMMM dd, yyyy");
format(ld, "'Month' q 'in' QQQ");
format(ld, "[MM-dd-yyyy][' at' HH:mm:ss]");
LocalTime lt = LocalTime.of(16, 30, 5, 78899);
System.out.println("\nFormatting time:" + lt);
format(lt, "HH:mm:ss");
format(lt, "KK:mm:ss a");
format(lt, "[MM-dd-yyyy][' at' HH:mm:ss]");
ZoneId usCentral = ZoneId.of("America/Chicago");
ZonedDateTime zdt = ZonedDateTime.of(ld, lt, usCentral);
System.out.println("\nFormatting zoned datetime:" + zdt);
format(zdt, "MM/dd/yyyy HH:mm:ssXXX");
format(zdt, "MM/dd/yyyy VV");
format(zdt, "[MM-dd-yyyy][' at' HH:mm:ss]");
}
public static void format(Temporal co, String pattern) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern, Locale.US);
String str = fmt.format(co);
System.out.println(pattern + ": " + str);
}
}
Formatting date: 2021-04-30
M/d/yyyy: 4/30/2021
MM/dd/yyyy: 04/30/2021
MMM dd, yyyy: Apr 30, 2021
MMMM dd, yyyy: April 30, 2021
EEEE, MMMM dd, yyyy: Monday, April 30, 2021
'Month' q 'in' QQQ: Month 2 in Q2
[MM-dd-yyyy][' at' HH:mm:ss]: 04-30-2021
Formatting time:16:30:05.000078899
HH:mm:ss: 16:30:05
KK:mm:ss a: 04:30:05 PM
[MM-dd-yyyy][' at' HH:mm:ss]: at 16:30:05
Formatting zoned datetime:2021-04-30T16:30:05.000078899-05:00[America/Chicago]
MM/dd/yyyy HH:mm:ssXXX: 04/30/2021 16:30:05-05:00
MM/dd/yyyy VV: 04/30/2021 America/Chicago
[MM-dd-yyyy][' at' HH:mm:ss]: 04-30-2021 at 16:30:05
Listing 16-25Using Patterns to Format Dates and Times
使用特定于区域设置的格式
DateTimeFormatter类有几个方法返回一个DateTimeFormatter,它带有适合人类阅读的预定义格式模式。使用以下方法获取对此类格式化程序的引用:
-
DateTimeFormatter ofLocalizedDate(FormatStyle dateStyle) -
DateTimeFormatter ofLocalizedDateTime(FormatStyle dateTimeStyle) -
DateTimeFormatter ofLocalizedDateTime(FormatStyle dateStyle, FormatStyle timeStyle) -
DateTimeFormatter ofLocalizedTime(FormatStyle timeStyle)
这些方法接受一个FormatStyle枚举类型的参数,它有四个常量:SHORT、MEDIUM、LONG和FULL。这些常量用于输出不同详细程度的格式化日期和时间。输出中的细节是特定于语言环境的。这些方法使用系统默认区域设置。对于不同的语言环境,使用withLocal()方法获得一个具有指定语言环境的新的DateTimeFormatter。
清单 16-26 展示了如何使用一些预定义的特定于地区的格式。它格式化美国(默认)、德国和印度地区的日期和时间。
// LocalizedFormats.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import static java.time.format.FormatStyle.FULL;
import static java.time.format.FormatStyle.LONG;
import static java.time.format.FormatStyle.MEDIUM;
import static java.time.format.FormatStyle.SHORT;
import java.util.Locale;
public class LocalizedFormats {
public static void main(String[] args) {
LocalDate ld = LocalDate.of(2021, Month.APRIL, 19);
LocalTime lt = LocalTime.of(16, 30, 20);
LocalDateTime ldt = LocalDateTime.of(ld, lt);
DateTimeFormatter fmt = DateTimeFormatter.ofLocalizedDate(SHORT);
System.out.println("Formatter Default Locale: " + fmt.getLocale());
System.out.println("Short Date: " + fmt.format(ld));
fmt = DateTimeFormatter.ofLocalizedDate(MEDIUM);
System.out.println("Medium Date: " + fmt.format(ld));
fmt = DateTimeFormatter.ofLocalizedDate(LONG);
System.out.println("Long Date: " + fmt.format(ld));
fmt = DateTimeFormatter.ofLocalizedDate(FULL);
System.out.println("Full Date: " + fmt.format(ld));
fmt = DateTimeFormatter.ofLocalizedTime(SHORT);
System.out.println("Short Time: " + fmt.format(lt));
fmt = DateTimeFormatter.ofLocalizedDateTime(SHORT);
System.out.println("Short Datetime: " + fmt.format(ldt));
fmt = DateTimeFormatter.ofLocalizedDateTime(MEDIUM);
System.out.println("Medium Datetime: " + fmt.format(ldt));
// Use German locale to format the datetime in medius style
fmt = DateTimeFormatter.ofLocalizedDateTime(MEDIUM)
.withLocale(Locale.GERMAN);
System.out.println("German Medium Datetime: " + fmt.format(ldt));
// Use Indian(English) locale to format datetime in short style
fmt = DateTimeFormatter.ofLocalizedDateTime(SHORT)
.withLocale(new Locale("en", "IN"));
System.out.println("Indian(en) Short Datetime: " + fmt.format(ldt));
// Use Indian(English) locale to format datetime in medium style
fmt = DateTimeFormatter.ofLocalizedDateTime(MEDIUM)
.withLocale(new Locale("en","IN"));
System.out.println("Indian(en) Medium Datetime: " + fmt.format(ldt));
}
}
Formatter Default Locale: en_US
Short Date: 4/19/21
Medium Date: Apr 19, 2021
Long Date: April 19, 2021
Full Date: Thursday, April 19, 2021
Short Time: 4:30 PM
Short Datetime: 4/19/21, 4:30 PM
Medium Datetime: Apr 19, 2021, 4:30:20 PM
German Medium Datetime: 19.04.2021, 16:30:20
Indian(en) Short Datetime: 19/04/21, 4:30 PM
Indian(en) Medium Datetime: 19-Apr-2021, 4:30:20 PM
Listing 16-26Using Predefined Format Patterns
使用 DateTimeFormatterBuilder 类
在内部,所有日期时间格式化程序都是使用DateTimeFormatterBuilder获得的。通常,您不需要使用该类。前面讨论的方法在几乎所有的用例中都是足够的。该类有一个无参数的构造器和许多appendXxx()方法。创建该类的一个实例,并调用这些appendXxx()方法来构建所需的格式化程序。最后,调用toFomatter()方法获得一个DateTimeFormatter对象。下面的代码片段构建了一个DateTimeFormatter对象,将日期格式化为“圣诞节在YEAR在WEEK_DAY”的格式:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import static java.time.format.TextStyle.FULL_STANDALONE;
import static java.time.temporal.ChronoField.DAY_OF_WEEK;
import static java.time.temporal.ChronoField.YEAR;
...
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendLiteral("Christmas in ")
.appendValue(YEAR)
.appendLiteral(" is on ")
.appendText(DAY_OF_WEEK, FULL_STANDALONE)
.toFormatter();
LocalDate ld = LocalDate.of(2020, 12, 25);
String str = ld.format(formatter);
System.out.println(str);
Christmas in 2020 is on Friday
您可以使用一种模式创建相同的格式化程序,这种模式比前面使用DateTimeFormatterBuilder的代码更容易编写和读取:
LocalDate ld = LocalDate.of(2020, 12, 25);
String pattern = "'Christmas in' yyyy 'is on' EEEE";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
String str = ld.format(formatter);
System.out.println(str);
Christmas in 2020 is on Friday
解析日期和时间
解析是从字符串创建日期、时间或日期时间对象的过程。像格式化一样,解析也由一个DateTimeFormatter处理。有关如何获取DateTimeFormatter类的实例的详细信息,请参考上一节“格式化日期和时间”。用于格式化的相同符号也用作解析符号。有两种方法可以将字符串解析为 datetime 对象:
-
使用 datetime 类的
parse()方法 -
使用
DateTimeFormatter类的parse()方法
Tip
如果文本不能被解析,抛出一个DateTimeParseException。这是一个运行时异常。该类包含两个提供错误详细信息的方法。getErrorIndex()方法返回发生错误的文本中的索引。getParsedString()方法返回被解析的文本。在解析日期时间时处理此异常是一种好的做法。
每个 datetime 类都有两个重载版本的静态方法parse()。parse()方法的返回类型与定义的 datetime 类相同。下面是LocalDate类中parse()方法的两个版本:
-
static LocalDate parse(CharSequence text) -
static LocalDate parse(CharSequence text, DateTimeFormatter formatter)
第一个版本的parse()方法采用 ISO 格式的datetime对象的文本表示。例如,对于一个LocalDate,文本应该是 yyyy-mm-dd 格式。第二个版本让您指定一个DateTimeFormatter。下面的代码片段将两个字符串解析成两个LocalDate对象:
// Parse a LocalDate in ISO format
LocalDate ld1 = LocalDate.parse("2022-01-10");
// Parse a LocalDate in MM/dd/yyyy format
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");
LocalDate ld2 = LocalDate.parse("01/10/2022", formatter);
System.out.println("ld1: " + ld1);
System.out.println("ld2: " + ld2);
ld1: 2022-01-10
ld2: 2022-01-10
DateTimeFormatter类包含几个parse()方法,以便于将字符串解析成 datetime 对象。DateTimeFormatter类不知道可以由字符串构成的日期时间对象的类型。因此,它们中的大多数都返回一个TemporalAccessor对象,您可以查询该对象以获得日期时间组件。您可以将TemporalAccessor对象传递给 datetime 类的from()方法来获取特定的 datetime 对象。下面的代码片段展示了如何使用DateTimeFormatter对象构建LocalDate来解析 MM/dd/yyyy 格式的字符串:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
...
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");
TemporalAccessor ta = formatter.parse("01/10/2022");
LocalDate ld = LocalDate.from(ta);
System.out.println(ld);
2022-01-10
另一个版本的parse()方法使用了一个TemporalQuery,它可以用来将字符串直接解析成一个特定的 datetime 对象。下面的代码片段使用了这个版本的parse()方法。第二个参数是LocalDate类的from()方法的方法引用。您可以将下面的代码片段视为前面代码的简写:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy");
LocalDate ld = formatter.parse("01/10/2022", LocalDate::from);
System.out.println(ld);
2022-01-10
DateTimeFormatter类包含一个parseBest()方法。使用这种方法几乎不需要解释。假设您收到一个字符串作为方法的参数。参数可能包含不同的日期和时间信息。在这种情况下,您希望使用最多的信息来解析字符串。考虑以下模式:
yyyy-MM-dd['T'HH:mm:ss[Z]]
这个模式有两个可选部分。具有这种模式的文本可以被完全解析为一个OffsetDateTime,部分解析为一个LocalDateTime和一个LocalDate。您可以为这个模式创建一个解析器,如下所示:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd['T'HH:mm:ss[Z]]");
下面的代码片段将OffsetDateTime、LocalDateTime和LocalDate指定为首选的解析结果类型:
String text = ...
TemporalAccessor ta =
formatter.parseBest(text, OffsetDateTime::from, LocalDateTime::from, LocalDate::from);
该方法将尝试按顺序将文本解析为指定的类型,并返回第一个成功的结果。通常,对parseBest()方法的调用之后是一系列带有instanceof操作符的if-else语句,以检查返回的Object类型。清单 16-27 展示了如何使用parseBest()方法。请注意,第四个文本的格式无效,解析它会引发异常。
// ParseBestTest.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
public class ParseBestTest {
public static void main(String[] args) {
DateTimeFormatter parser
= DateTimeFormatter.ofPattern("yyyy-MM-dd['T'HH:mm:ss[Z]]");
parseStr(parser, "2021-05-31");
parseStr(parser, "2021-05-31T16:30:12");
parseStr(parser, "2021-05-31T16:30:12-0500");
parseStr(parser, "2021-05-31Hello");
}
public static void parseStr(DateTimeFormatter formatter, String text) {
try {
TemporalAccessor ta = formatter.parseBest(text,
OffsetDateTime::from,
LocalDateTime::from,
LocalDate::from);
if (ta instanceof OffsetDateTime) {
OffsetDateTime odt = OffsetDateTime.from(ta);
System.out.println("OffsetDateTime: " + odt);
} else if (ta instanceof LocalDateTime) {
LocalDateTime ldt = LocalDateTime.from(ta);
System.out.println("LocalDateTime: " + ldt);
} else if (ta instanceof LocalDate) {
LocalDate ld = LocalDate.from(ta);
System.out.println("LocalDate: " + ld);
} else {
System.out.println("Parsing returned: " + ta);
}
} catch (DateTimeParseException e) {
System.out.println(e.getMessage());
}
}
}
LocalDate: 2021-05-31
LocalDateTime: 2021-05-31T16:30:12
OffsetDateTime: 2021-05-31T16:30:12-05:00
Text '2021-05-31Hello' could not be parsed, unparsed text found at index 10
Listing 16-27Using the parseBest() Method of the DateTimeFormatter Class
传统日期时间类
我们将 Java 8 之前可用的与日期时间相关的类称为遗留日期时间类。主要的遗留类是Date、Calendar和GregorianCalendar。他们在java.util包里。关于如何将Date和Calendar对象转换为新的日期-时间 API 的日期时间对象,或者反过来,请参考“与遗留日期时间类的互操作性”一节。
日期类
一个Date类的对象代表一个瞬间。一个Date对象存储了从 UTC 时间 1970 年 1 月 1 日午夜开始的毫秒数。
Tip
传统日期时间 API 中的Date类类似于新日期时间 API 中的Instant类。它们分别具有毫秒和纳秒的精度。
从 JDK 1.1 开始,Date类的大多数构造器和方法都被弃用了。Date类的默认构造器用于创建一个带有当前系统日期时间的Date对象。清单 16-28 展示了Date类的用法。您可能会得到不同的输出,因为它打印当前的日期和时间。
// CurrentLegacyDate.java
package com.jdojo.datetime;
import java.util.Date;
public class CurrentLegacyDate {
public static void main (String[] args) {
// Create a new Date object
Date currentDate = new Date();
System.out.println("Current date: " + currentDate);
// Get the milliseconds value of the current date
long millis = currentDate.getTime();
System.out.println("Current datetime in millis: " + millis);
}
}
Current date: Sat Aug 21 20:13:38 EDT 2021
Current datetime in millis: 1629591218981
Listing 16-28Using the Date Class
一个对象以 1900 年为基础工作。当你调用这个对象的setYear()方法将年份设置为 2017 年时,你将需要传递 117(2017–1900 = 117)。它的getYear()方法返回 2017 年的 117。此类中的月份范围从 0 到 11,其中一月是 0,二月是 2 …十二月是 11。
日历类
Calendar是一个抽象类。抽象类不能被实例化。我们在关于继承的第二十章中详细讨论抽象类。GregorianCalendar类是一个具体的类,它继承了Calendar类。
Calendar类声明了一些 final 静态字段来表示日期字段。例如,Calendar.JANUARY可以用来指定日期中的一月。GregorianCalendar类有一个默认的构造器,它创建一个对象来表示当前的日期时间。您还可以创建一个GregorianCalendar对象,使用它的其他构造器来表示特定的日期。它还允许您获取特定时区的当前日期:
// Get the current date in the system default time zone
GregorianCalendar currentDate = new GregorianCalendar();
// Get GregorianCalendar object representing March 26, 2003 06:30:45 AM
GregorianCalendar someDate = new GregorianCalendar(2003, Calendar.MARCH, 26, 6, 30, 45);
// Get Indian time zone, which is GMT+05:30
TimeZone indianTZ = TimeZone.getTimeZone("GMT+05:30");
// Get current date in India
GregorianCalendar indianDate = new GregorianCalendar(indianTZ);
// Get Moscow time zone, which is GMT+03:00
TimeZone moscowTZ = TimeZone.getTimeZone("GMT+03:00");
// Get current date in Moscow
GregorianCalendar moscowDate = new GregorianCalendar(moscowTZ);
Tip
一个Date包含日期时间。一个GregorianCalendar包含一个带时区的日期时间。
日期的月份部分的范围是从 0 到 11。即 1 月为 0,2 月为 1,以此类推。在Calendar类中使用为月份和其他日期字段声明的常量比使用它们的整数值更容易。例如,您应该在程序中使用Calendar.JANUARY常量来表示一月,而不是 0。您可以使用get()方法通过将请求的字段作为参数传递来获取日期时间中的字段值:
// Create a GregorianCalendar object
GregorianCalendar gc = new GregorianCalendar();
// year will contain the current year value
int year = gc.get(Calendar.YEAR);
// month will contain the current month value
int month = gc.get(Calendar.MONTH);
// day will contain day of month of the current date
int day = gc.get(Calendar.DAY_OF_MONTH);
// hour will contain hour value
int hour = gc.get(Calendar.HOUR);
// minute will contain minute value
int minute = gc.get(Calendar.MINUTE);
// second will contain second values
int second = gc.get(Calendar.SECOND);
您可以使用GregorianCalendar类的setLenient()方法将日期解释设置为宽松或不宽松。默认是宽大的。如果日期解释宽松,则日期(如 2003 年 3 月 35 日)将被解释为 2003 年 4 月 5 日。如果日期解释不宽松,这样的日期将导致错误。您还可以使用before()和after()方法来比较两个日期,无论一个日期是在另一个日期之前还是之后。有两种方法,add()和roll(),需要解释一下。下面几节将对它们进行描述。
add()方法
add()方法用于将一个金额添加到日期的特定字段中。添加的量可以是负的或正的。假设您将日期 2003 年 12 月 1 日存储在一个GregorianCalendar对象中。您希望在月字段中添加 5。月字段的值将是 16,这超出了范围(0–11)。在这种情况下,较大的日期字段(这里,year 大于 month)将被调整以适应溢出。在“月份”字段中添加 5 后,日期将是 2004 年 5 月 1 日。以下代码片段阐释了这一概念:
GregorianCalendar gc = new GregorianCalendar(2003, Calendar.DECEMBER, 1);
gc.add(Calendar.MONTH, 5); // Now gc represents May 1, 2004
这种方法也可能导致调整较小的字段。假设您将日期 2003 年 1 月 30 日存储在一个GregorianCalendar对象中。您在月份字段中添加 1。新月份字段不会溢出。但是,产生的日期 2003 年 2 月 30 日不是有效的日期。2003 年 2 月中的日期必须介于 1 和 28 之间。在这种情况下,日期字段会自动调整。它被设置为最接近的有效值,即28。结果日期将是 2003 年 2 月 28 日。
roll()方法
roll()方法的工作原理与add()方法相同,除了当被改变的字段溢出时,它不改变更大的字段。它可能会调整较小的字段以使日期成为有效日期。这是一个重载方法:
-
void roll(int field, int amount) -
void roll(int field, boolean up)
第二个版本将指定的field向上/向下滚动一个时间单位,而第一个版本将指定的field滚动指定的amount。因此,gc.roll(Calendar.MONTH, 1)与gc.roll(Calendar.MONTH, true),相同,gc.roll(Calendar.MONTH, -1)与gc.roll(Calendar.MONTH, false)相同。清单 16-29 展示了GregorianCalendar类的一些方法的使用。您可能会得到不同的输出。
// GregorianDate .java
package com.jdojo.datetime;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class GregorianDate {
public static void main(String[] args) {
GregorianCalendar gc = new GregorianCalendar();
System.out.println("Current Date: " + getStr(gc));
// Add 1 year
gc.add(Calendar.YEAR, 1);
System.out.println("After adding a year: " + getStr(gc));
// Add 15 days
gc.add(Calendar.DATE, 15);
System.out.println("After adding 15 days: " + getStr(gc));
long millis = gc.getTimeInMillis();
Date dt = gc.getTime();
System.out.println("Time in millis: " + millis);
System.out.println("Time as Date: " + dt);
}
public static String getStr(GregorianCalendar gc) {
int day = gc.get(Calendar.DAY_OF_MONTH);
int month = gc.get(Calendar.MONTH);
int year = gc.get(Calendar.YEAR);
int hour = gc.get(Calendar.HOUR);
int minute = gc.get(Calendar.MINUTE);
int second = gc.get(Calendar.SECOND);
String str = day + "/" + (month + 1) + "/" + year + " "
+ hour + ":" + minute + ":" + second;
return str;
}
}
Current Date: 21/8/2021 8:15:15
After adding a year: 21/8/2022 8:15:15
After adding 15 days: 5/9/2022 8:15:15
Time in millis: 1662423315056
Time as Date: Mon Sep 05 20:15:15 EDT 2022
Listing 16-29Using the GregorianCalendar Class
与传统日期时间类的互操作性
当新的日期时间 API 出现时,传统的 datetime 类已经存在了 18 年。作为一名 Java 开发人员,您可能需要维护使用遗留类的应用程序。出于这个原因,还提供了遗留类和新的日期时间 API 之间的互操作性。遗留类中添加了新的方法,可以将其对象转换为新的 datetime 对象,反之亦然。本节将讨论以下遗留类的互操作性:
-
java.util.Date -
java.util.Calendar -
java.util.GregorianCalendar -
java.util.TimeZone -
java.sql.Date -
java.sql.Time -
java.sql.Timestamp -
java.nio.file.attribute.FileTime
表 16-8 包含传统日期时间类及其新日期时间对应类的列表。除了Calendar类,所有遗留类都提供双向转换。toXxx()方法是实例方法。它们返回新的 datetime 类的对象。其他方法是静态方法,它们接受新 datetime 类的对象并返回旧类的对象。例如,java.util.Date类中的from()方法是一个静态方法,它接受一个Instant参数并返回一个java.util.Date。toInstant()方法是一个实例方法,将一个java.util.Date转换成一个Instant。
表 16-8
新日期时间和旧日期时间类之间的转换
|遗留类
|
遗留类中的新方法
|
等效的新日期时间类
|
| --- | --- | --- |
| java.util.Date | from(), toInstant() | Instant |
| Calendar | toInstant() | None |
| GregorianCalendar | from(), toZonedDateTime() | ZonedDateTime |
| TimeZone | getTimeZone(), toZoneId() | ZoneId |
| java.sql.Date | valueOf(), toLocalDate() | LocalDate |
| Time | valueOf(), toLocalTime() | LocalTime |
| Timestamp | from(), toInstant() | Instant |
| valueOf(), toLocalDateTime() | LocalDateTime |
| FileTime | from(), toInstant() | Instant |
清单 16-30 展示了如何将Date转换成Instant,反之亦然。您可能会得到不同的输出。
// DateAndInstant.java
package com.jdojo.datetime;
import java.util.Date;
import java.time.Instant;
public class DateAndInstant {
public static void main(String[] args) {
// Get the current date
Date dt = new Date();
System.out.println("Date: " + dt);
// Convert the Date to an Instant
Instant in = dt.toInstant();
System.out.println("Instant: " + in);
// Convert the Instant back to a Date
Date dt2 = Date.from(in);
System.out.println("Date: " + dt2);
}
}
Date: Sat Aug 21 20:05:05 EDT 2021
Instant: 2021-08-22T00:05:05.841Z
Date: Sat Aug 21 20:05:05 EDT 2021
Listing 16-30Converting a Date to an Instant and Vice Versa
通常,遗留代码使用GregorianCalendar来存储日期、时间和日期时间。您可以将它转换成一个ZonedDateTime,它可以转换成新的日期时间 API 中的任何其他类。Calendar类提供了一个toInstant()方法来将其实例转换成一个Instant。Calendar类是抽象的。通常,您会有一个具体子类的实例,例如GregorianCalendar。因此,将Instant转换为GregorianCalendar是一个两步过程:
-
将
Instant转换为ZonedDateTime。 -
使用
GregorianCalendar类的from()静态方法获得一个GregorianCalendar。
清单 16-31 中的程序展示了如何将一个GregorianCalendar转换成一个ZonedDateTime,反之亦然。该程序还显示了如何获得一个LocalDate、LocalTime等。来自一个GregorianCalendar。您可能会得到不同的输出,因为输出取决于系统的默认时区。
// GregorianCalendarAndNewDateTime.java
package com.jdojo.datetime;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.GregorianCalendar;
import java.util.TimeZone;
public class GregorianCalendarAndNewDateTime {
public static void main(String[] args) {
// Create a GC for the default time zone
GregorianCalendar gc = new GregorianCalendar(2022, 1, 11, 15, 45, 50);
System.out.println("Gregorian Calendar: " + gc.getTime());
// Convert the GC to a LocalDate
LocalDate ld = gc.toZonedDateTime().toLocalDate();
System.out.println("Local Date: " + ld);
// Convert the GC to a LocalTime
LocalTime lt = gc.toZonedDateTime().toLocalTime();
System.out.println("Local Time: " + lt);
// Convert the GC to a LocalDateTime
LocalDateTime ldt = gc.toZonedDateTime().toLocalDateTime();
System.out.println("Local DateTime: " + ldt);
// Convert the GC to an OffsetDate
OffsetDateTime od = gc.toZonedDateTime().toOffsetDateTime();
System.out.println("Offset Date: " + od);
// Convert the GC to an OffsetTime
OffsetTime ot = gc.toZonedDateTime().toOffsetDateTime().toOffsetTime();
System.out.println("Offset Time: " + ot);
// Convert the GC to an ZonedDateTime
ZonedDateTime zdt = gc.toZonedDateTime();
System.out.println("Zoned DateTime: " + zdt);
// Convert the ZonedDateTime to a GC. In GC month starts at 0
// and in new API at 1
ZoneId zoneId = zdt.getZone();
TimeZone timeZone = TimeZone.getTimeZone(zoneId);
System.out.println("Zone ID: " + zoneId);
System.out.println("Time Zone ID: " + timeZone.getID());
GregorianCalendar gc2 = GregorianCalendar.from(zdt);
System.out.println("Gregorian Calendar: " + gc2.getTime());
}
}
Gregorian Calendar: Fri Feb 11 15:45:50 EST 2022
Local Date: 2022-02-11
Local Time: 15:45:50
Local DateTime: 2022-02-11T15:45:50
Offset Date: 2022-02-11T15:45:50-05:00
Offset Time: 15:45:50-05:00
Zoned DateTime: 2022-02-11T15:45:50-05:00[America/New_York]
Zone ID: America/New_York
Time Zone ID: America/New_York
Gregorian Calendar: Fri Feb 11 15:45:50 EST 2022
Listing 16-31Converting
a GregorianCalendar to New Datetime Types and Vice Versa
如何将一个Date转换成一个LocalDate?一个Date代表一个瞬间,所以首先你需要用一个ZoneId把Date转换成一个ZoneDateTime,然后从ZonedDateTime得到一个LocalDate。以下代码片段将由Date表示的当前日期转换为 Java 中的LocalDate:
Date dt = new Date();
LocalDate ld = dt.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate();
System.out.println("Date: " + dt);
System.out.println("LocalDate: " + ld);
Date: Sat Aug 21 20:07:35 EDT 2021
LocalDate: 2021-08-21
这种转换是经常需要的。Java 9 在LocalDate类中添加了一个ofInstant()方法,使得这种转换更加容易。该方法声明如下:
static LocalDate ofInstant(Instant instant, ZoneId zone)
以下代码片段使用 ofInstant()执行相同的转换:
Date dt = new Date();
LocalDate ld = LocalDate.ofInstant(dt.toInstant(), ZoneId.systemDefault());
System.out.println("Date: " + dt);
System.out.println("LocalDate: " + ld);
Date: Sat Aug 21 20:09:26 EDT 2021
LocalDate: 2021-08-21
摘要
通过java.time包,Java 提供了一个全面的日期时间 API 来处理日期、时间和日期时间。默认情况下,大多数类都基于 ISO-8601 标准。主要的类别有
-
Instant -
LocalDate -
LocalTime -
LocalDateTime -
OffsetTime -
OffsetDateTime -
ZonedDateTime
Instant类代表时间轴上的一个瞬间;并且它适合于机器,例如,作为事件的时间戳。LocalDate、LocalTime和LocalDateTime类表示人类可读的日期、时间和不带时区的日期时间。OffsetTime和OffsetDateTime类表示一个时间和日期时间,与 UTC 有一个时区偏移量。ZoneDateTime类表示具有时区规则的时区的日期时间,它将根据时区中夏令时的变化来调整时间。
日期-时间 API 提供了表示机器和人类所用时间的类。Duration类代表机器的时间量,而Period类代表人类感知的时间量。日期时间 API 通过java.time.format.DateTimeFormatter类为格式化和解析日期时间提供了广泛的支持。日期时间 API 通过java.time.chrono包支持非 ISO 日历系统。提供了对回历、日文、民国和泰国佛教日历的内置支持。该 API 是可扩展的,支持构建您自己的日历系统。
EXERCISES
-
您将使用哪个类来存储不包含时间和时区部分的日期?
-
你用什么类来存储一个知道夏令时的日期和时间?
-
a
ZoneId和ZoneOffset有什么区别? -
ZonedDateTime和OffsetDateTime有什么区别? -
编写代码,将代表系统默认时区中当前时间的
Instant转换为LocalDate。 -
编写一个程序,打印从 2001 年到 2099 年的所有年份,其中一年的最后一天(12 月 31 日)是星期一。
-
编写将系统默认时区中的
java.util.Date转换为LocalDate的代码。 -
完成下面的代码片段,以便打印出
"Friday January 12, 1968"。应该将日期 1968-01-12 格式化并打印:LocalDate bday = LocalDate.of(1968, Month.JANUARY, 12); String pattern = /* Your code goes here */; DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern); String formattedBDay = fmt.format(bday); System.out.println(formattedBDay); -
完成以下代码片段,打印 1968 年 1 月 12 日到 1969 年 9 月 19 日之间的天数。它应该打印
616:LocalDate ld1 = LocalDate.of(1968, Month.JANUARY, 12); LocalDate ld2 = LocalDate.of(1969, Month.SEPTEMBER, 19); long daysBetween = /* Your code goes here */; System.out.println(daysBetween); -
完成
printFirstDayOfMonth()方法中的代码。该方法将一个LocalDate作为参数,并打印日期所在月份的第一天。假设这个方法传入的LocalDate是 2017-08-05;它将打印"First day of AUGUST, 2017 is on SATURDAY":
```java
public static void printFirstDayOfMonth(LocalDate ld) {
LocalDate newDate = ld.with(/* Your Code goes here */);
System.out.printf("First day of %s, %d is on %s%n",
ld.getMonth(), ld.getYear(), newDate.getDayOfWeek());
}
```