11. 与运行环境交互
11. 与运行环境交互
Java 提供了 String
、StringBuffer
和 StringBuilder
来处理字符串。Java 还提供了 Date
和 Calendar
来处理日期、时间,其中 Date
是一个已经过时的 API,通常推荐使用 Calendar
来处理日期、时间。
正则表达式是一个强大的文本处理工具,通过正则表达式可以对文本内容进行查找、替换、分割等操作。从 JDK 1.4 以后,Java 也增加了对正则表达式的支持,包括新增的 Pattern
和 Matcher
两个类,并改写了 String
类,让 String
类增加了正则表达式支持,增加了正则表达式功能后的 String
类更加强大。
Java 还提供了非常简单的国际化支持,Java 使用 Locale
对象封装一个国家、语言环境,再使用 ResourceBundle
根据 Locale
加载语言资源包,当 ResourceBundle
加载了指定 Locale
对应的语言资源文件后,ResourceBundle
对象就可调用 getString()
方法来取出指定 key
所对应的消息字符串。
与用户互动
绝大部分程序都需要处理用户动作,包括接收用户的键盘输入、鼠标动作等。
main
回忆 Java 程序的入口——main
方法的方法签名:
public static void main(String[] args){}
public
修饰符:
Java 类由 JVM 调用,为了让 JVM 可以自由调用这个
main
方法,所以使用public
修饰符把这个方法暴露出来。
static
修饰符:
JVM 调用这个主方法时,不会先创建该主类的对象,然后通过对象来调用该主方法。JVM 直接通过该类来调用主方法,因此使用
static
修饰该主方法。
void
返回值:
因为主方法被 JVM 调用,该方法的返回值将返回给 JVM,这没有任何意义,因此
main
方法没有返回值。
上面方法中还包括一个字符串数组形参,根据方法调用的规则:谁调用方法,谁负责为形参赋值。也就是说,main
方法由 JVM 调用,即 args
形参应该由 JVM 负责赋值。
public class ArgsTest {
public static void main(String[] args) {
// 输出args数组的长度
System.out.println(args.length);
// 遍历args数组的每个元素
for (String arg : args) {
System.out.println(arg);
}
}
}
使用 java ArgsTest
命令运行上面程序,看到程序仅仅输出一个 0,这表明 args
数组是一个长度为 0 的空数组——这是合理的。因为计算机是没有思考能力的,它只能忠实地执行用户交给它的任务,既然我们没有给 args
数组设定参数值,那么 JVM 就不知道 args
数组的元素,所以 JVM 将 args
数组设置成一个长度为 0 的数组。
改为如下命令来运行上面程序:
java ArgsTest Java Spring
运行结果:
如果运行 Java 程序时在类名后紧跟一个或多个字符串(多个字符串之间以空格隔开),JVM 就会把这些字符串依次赋给 args
数组元素。
如果某参数本身包含了空格,则应该将该参数用双引号 ""
括起来,否则 JVM 会把这个空格当成参数分隔符,而不是当成参数本身。
java ArgsTest "Java Spring"
Scanner
使用 Scanner
类可以很方便地获取用户的键盘输入,Scanner
是一个基于正则表达式的文本扫描器,它可以从文件、输入流、字符串中解析出基本类型值和字符串值。
Scanner
类提供了多个构造器,不同的构造器可以接收文件、输入流、字符串作为数据源,用于从文件、输入流、字符串中解析数据。
Scanner
主要提供了两个方法来扫描输入。
hasNextXxx()
:是否还有下一个输入项,其中Xxx
可以是Int
、Long
等代表基本数据类型的字符串。如果需要判断是否包含下一个字符串,则可以省略Xxx
。nextXxx()
:获取下一个输入项。Xxx
的含义与前一个方法中的Xxx
相同。
在默认情况下,Scanner
使用空白(包括空格、Tab 空白、回车)作为多个输入项之间的分隔符。
下面程序使用 Scanner
来获得用户的键盘输入。
public class ScannerKeyBoardTest {
public static void main(String[] args) {
// System.in代表标准输入,就是键盘输入
Scanner sc = new Scanner(System.in);
// 增加下面一行将只把回车作为分隔符
// sc.useDelimiter("'\n");
//判断是否还有下一个输入项
while (sc.hasNext()) {
// 输出输入项
System.out.println("键盘输入的内容是:" + sc.next());
}
}
}
运行上面程序,程序通过 Scanner
不断从键盘读取键盘输入,每次读到键盘输入后,直接将输入内容打印在控制台。
如果希望改变 Scanner
的分隔符(不使用空白作为分隔符),例如,程序需要每次读取一行,不管这一行中是否包含空格,Scanner
都把它当成一个输入项。在这种需求下,我们可以把 Scanner
的分隔符设置为回车符,不再使用默认的空白作为分隔符。
Scanner
的读取操作可能被阻塞(当前执行顺序流暂停)来等待信息的输入。如果输入源没有结束, Scanner
又读不到更多输入项时(尤其在键盘输入时比较常见),Scanner
的 hasNext()
和 next()
方法都有可能阻塞,hasNext()
方法是否阻塞与和其相关的 next()
方法是否阻塞无关。
为 Scanner
设置分隔符使用 useDelimiter(String pattern)
方法即可,该方法的参数应该是一个正则表达式。只要把上面程序中粗体字代码行的注释去掉,该程序就会把键盘的每行输入当成一个输入项,不会以空格、Tab 空白等作为分隔符。
Scanner
提供了两个简单的方法来逐行读取。
boolean hasNextLine()
:返回输入源中是否还有下一行。String nextLine()
:返回输入源中下一行的字符串。
Scanner
不仅可以获取字符串输入项,也可以获取任何基本类型的输入项:
public class ScannerLongTest {
public static void main(String[] args) {
// System.in代表标准输入,就是键盘输入
Scanner sc = new Scanner(System.in);
//判断是否还有下一个long整数
while (sc.hasNextLong()) {
// 输出输入项
System.out.println("键盘输入的内容是:" + sc.nextLong());
}
}
}
正如通过 hasNextLong()
和 nextLong()
两个方法,Scanner
可以直接从输入流中获得 long
型整数输入项。与此类似的是,如果需要获取其他基本类型的输入项,则可以使用相应的方法。
提示
上面程序不如 ScannerKeyBoardTest
程序适应性强,因为 ScannerLongTest
程序要求键盘输入必须是整数,否则程序就会退出。
Scanner
不仅能读取用户的键盘输入,还可以读取文件输入。
只要在创建 Scanner
对象时传入一个 File
对象作为参数,就可以让 Scanner
读取该文件的内容。
package test;
import java.io.File;
import java.util.Scanner;
public class ScannerFileTest {
public static void main(String[] args)
throws Exception {
// 将一个File对象作为Scanner的构造器参数,Scanner读取文件内容
Scanner sc = new Scanner(new File("test/src/test/ScannerFileTest.java"));
System.out.println("ScannerFileTest.java文件内容如下:");
// 判断是否还有下一行
while (sc.hasNextLine()) {
// 输出文件中的下一行
System.out.println(sc.nextLine());
}
}
}
提示
因为上面程序涉及文件输入,可能引发文件 IO
相关异常,故主程序声明 throws Exception
表明 main
方法不处理任何异常。
BufferedReader
BufferedReader
是 Java IO
流中的一个字符、包装流,它必须建立在另一个字符流的基础之上。但标准输入:System.in
是字节流,程序需要使用转换流 InputStreamReader
将其包装成字符流。所以程序中用于获取键盘输入的 BufferedReader
对象采用如下代码创建:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
一旦获得了 BufferedReader
对象之后,就可以调用该对象的 readLine()
方法来逐行读取键盘输入;如果没有键盘输入,readLine()
方法将会阻塞来等待键盘输入。
public class KeyboardInTest {
public static void main(String[] args) throws Exception {
// 以System.in字节流为基础,创建一个BufferedReader对象
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = null;
// 逐行读取键盘输入
while ((line = br.readLine()) != null) {
System.out.println("用户键盘输入是:" + line);
}
}
}
使用 BufferedReader
可以逐行读取用户的键盘输入,用户的每次键盘输入都被 BufferedReader
当成 String
对象。与 Scanner
不同的是,BufferedReader
不能读取基本类型输入项,它总是读取 String
对象。
系统相关
Java 提供了 System
类和 Runtime
类来与程序的运行平台进行交互。
System
System
类代表当前 Java 程序的运行平台,程序不能创建 System
类的对象,System
类提供了一些类 Field 和类方法,允许直接通过 System
类来调用这些 Field 和方法。
System
类提供了代表标准输入、标准输出和错误输出的类 Field,并提供了一些静态方法用于访问环境变量、系统属性的方法,还提供了加载文件和动态链接库的方法。下面程序通过 System
类来访问操作的环境变量和系统属性。
public class SystemTest {
public static void main(String[] args) throws Exception {
// 获取系统所有的环境变量
Map<String, String> env = System.getenv();
for (String name : env.keySet()) {
System.out.println(name + " ---> " + env.get(name));
}
// 获取指定环境变量的值
System.out.println(System.getenv("JAVA_HOME"));
// 获取所有的系统属性
Properties props = System.getProperties();
// 将所有的系统属性保存到props.txt文件中
props.store(new FileOutputStream("props.txt"), "System Properties");
// 输出特定的系统属性
System.out.println(System.getProperty("os.name"));
}
}
上面程序通过调用 System
类的 getenv()
、getProperties()
、getProperty()
等方法来访问程序所在平台的环境变量和系统属性,程序运行的结果会输出操作系统所有的环境变量值,并输出 JAVA_HOME
环境变量,以及 os.name
系统属性的值。
该程序运行结束后还会在当前路径下生成一个 props.txt
文件,该文件中记录了当前平台的所有系统属性。
System
类提供了通知系统进行垃圾回收的 gc()
方法,以及通知系统进行资源清理的 runFinalization()
方法。
System
类还有两个获取系统当前时间的方法:currentTimeMillis()
和 nanoTime()
,它们都返回一个 long
型整数。
实际上它们都返回当前时间与 UTC 1970 年 1 月 1 日午夜的时间差,前者以毫秒作为测量单位,后者以纳秒作为测量单位。
这两个方法的返回值的粒度取决于底层操作系统,可能所在的操作系统根本不支持以毫秒、纳秒作为计时单位。
除此之外,System
类的 in
、out
和 err
分别代表系统的标准输入(通常是键盘)、标准输出(通常是显示器)和错误输出流,并提供了 setIn
、setOut
和 setErr
方法来改变系统的标准输入、标准输出和标准错误输出流。
System
类还提供了一个 identityHashCode(Object x)
方法,该方法返回指定对象的精确 hashCode
值,也就是根据该对象的地址计算得到的 hashCode
值。
当某个类的 hashCode()
方法被重写后,该类实例的 hashCode()
方法就不能唯一地标识该对象;但通过 identityHashCode()
方法返回的 hashCode
值,依然是根据该对象的地址计算得到的 hashCode
值。
所以,如果两个对象的 identityHashCode
值相同,则两个对象绝对是同一个对象。
public class IdentityHashCodeTest {
public static void main(String[] args) {
// 下面程序中s1和s2是两个不同的对象
String s1 = new String("Hello");
String s2 = new String("Hello");
// String重写了hashCode()方法——改为根据字符序列计算hashCode值
// 因为s1和s2的字符序列相同,所以它们的hashCode()方法返回值相同
System.out.println(s1.hashCode() + "----" + s2.hashCode());
// s1和s2是不同的字符串对象,所以它们的identityHashCode值不同
System.out.println(System.identityHashCode(s1) + "----" + System.identityHashCode(s2));
String s3 = "Java";
String s4 = "Java";
// s3和s4是相同的字符串对象,所以它们的identityHashCode值相同
System.out.println(System.identityHashCode(s3) + "----" + System.identityHashCode(s4));
}
}
Runtime
Runtime
类代表 Java 程序的运行时环境,每个 Java 程序都有一个与之对应的 Runtime
实例,应用程序通过该对象与其运行时环境相连。应用程序不能创建自己的 Runtime
实例,但可以通过 getRuntime()
方法获取与之关联的 Runtime
对象。
与 System
类似的是,Runtime
类也提供了 gc()
方法和 runFinalization()
方法来通知系统进行垃圾回收、清理系统资源,并提供了 load(String filename)
和 loadLibrary(String libname)
方法来加载文件和动态链接库。
Runtime
类代表 Java 程序的运行时环境,可以访问 JVM 的相关信息,如处理器数量、内存信息等。
public class RuntimeTest {
public static void main(String[] args) {
// 获取Java程序关联的运行时对象
Runtime rt = Runtime.getRuntime();
System.out.println("处理器数量:" + rt.availableProcessors());
System.out.println("空闲内存数:" + rt.freeMemory());
System.out.println("总内存数:" + rt.totalMemory());
System.out.println("可用最大内存数:" + rt.maxMemory());
}
}
除此之外,Runtime
类还有一个功能——它可以直接单独启动一个进程来运行操作系统的命令,如下程序所示。
public class ExecTest {
public static void main(String[] args) throws Exception {
Runtime rt = Runtime.getRuntime();
// 运行记事本程序
rt.exec("notepad.exe");
}
}
常用类
Object
Object
类是所有类、数组、枚举类的父类,也就是说,Java 允许把任何类型的对象赋给 Object
类型的变量。当定义一个类时没有使用 extends
关键字为它显式指定父类,则该类默认继承 Object
父类。
因为所有的 Java 类都是 Object
类的子类,所以任何 Java 对象都可以调用 Object
类的方法。Object
类提供了如下几个常用方法。
boolean equals(Object obj)
:判断指定对象与该对象是否相等。此处相等的标准是,两个对象是同一个对象,因此该equals()
方法通常没有太大的实用价值。protected void finalize()
:当系统中没有引用变量引用到该对象时,垃圾回收器调用此方法来清理该对象的资源。Class<?> getClass()
:返回该对象的运行时类。int hashCode()
:返回该对象的hashCode
值。在默认情况下,Object
类的hashCode()
方法根据该对象的地址来计算(即与System.identityHashCode(Object x)
方法的计算结果相同)。但很多类都重写了Object
类的hashCode()
方法,不再根据地址来计算其hashCode()
方法值。String toString()
:返回该对象的字符串表示,当我们使用System.out.println()
方法输出一个对象,或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的toString()
方法返回该对象的字符串表示。Object
类的toString()
方法返回运行时类名@十六进制 hashCode 值
格式的字符串,但很多类都重写了Object
类的toString()
方法,用于返回可以表述该对象信息的字符串。
除此之外,Object
类还提供了 wait()
、notify()
、notifyAll()
几个方法,通过这几个方法可以控制线程的暂停和运行。
Java 还提供了一个 protected
修饰的 clone()
方法,该方法用于帮助其他对象来实现“自我克隆”,所谓“自我克隆”就是得到一个当前对象的副本,而且二者之间完全隔离。由于 Object
类提供的 clone()
方法使用了 protected
修饰,因此该方法只能被子类重写或调用。
自定义类实现“克隆”的步骤如下。
- 自定义类实现
Cloneable
接口。这是一个标记性的接口,实现该接口的对象可以实现“自我克隆”,接口里没有定义任何方法。 - 自定义类实现自己的
clone()
方法。 - 实现
clone()
方法时通过super.clone();
调用Object
实现的clone()
方法来得到该对象的副本,并返回该副本。
class Address {
String detail;
public Address(String detail) {
this.detail = detail;
}
}
// 实现Cloneable接口
class User implements Cloneable {
int age;
Address address;
public User(int age) {
this.age = age;
address = new Address("广州天河");
}
// 通过调用super.clone()来实现clone()方法
public User clone() throws CloneNotSupportedException {
return (User)super.clone();
}
}
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
User u1 = new User(29);
// clone得到u1对象的副本
User u2 = u1.clone();
// ①判断u1、u2是否相同
System.out.println(u1 == u2);
// ②判断u1、u2的address是否相同
System.out.println(u1.address == u2.address);
}
}
上面程序让 User
类实现了 Cloneable
接口,而且实现了 clone()
方法,因此 User
对象就可实现“自我克隆”——克隆出来的对象只是原有对象的副本。程序在 ① 号代码处判断原有的 User
对象与克隆出来的 User
对象是否相同,程序返回 false
。
Object
类提供的 Clone
机制只对对象里各实例变量进行“简单复制”,如果实例变量的类型是引用类型,Object
的 Clone
机制也只是简单地复制这个引用变量,这样原有对象的引用类型的实例变量与克隆对象的引用类型的实例变量依然指向内存中的同一个实例,所以上面程序在 ② 号代码处输出 true
。上面程序“克隆”出来的 u1
、u2
所指向的对象在内存中的存储示意图如图所示。
Object
类提供的 clone()
方法不仅能简单地处理“复制”对象的问题,而且这种“自我克隆”机制十分高效。比如 clone
一个包含 100 个元素的 int[]
数组,用系统默认的 clone
方法比静态 copy
方法快近 2 倍。
Object
类的 clone()
方法虽然简单、易用,但它只是一种“浅克隆”——它只克隆该对象的所有 Field 值,不会对引用类型的 Field 值所引用的对象进行克隆。如果开发者需要对对象进行“深克隆”,则需要开发者自己进行“递归”克隆,保证所有引用类型的 Field 值所引用的对象都被复制了。
Objects
Java 7 新增了一个 Objects
工具类,它提供了一些工具方法来操作对象,这些工具方法大多是“空指针”安全的。比如,你不能明确地判断一个引用变量是否为 null
,如果贸然地调用该变量的 toString()
方法,则可能引发 NullPointerExcetpion
异常;但如果使用 Objects
类提供的 toString(Object o)
方法,就不会引发空指针异常,当 o
为 null
时,程序将返回一个 "null"
字符串。
相关信息
Java 为工具类的命名习惯是添加一个字母 s
,比如操作数组的工具类是 Arrays
,操作集合的工具类是 Collections
。
public class ObjectsTest {
// 定义一个obj变量,它的默认值是null
static ObjectsTest obj;
public static void main(String[] args) {
// 输出一个null对象的hashCode值,输出0
System.out.println(Objects.hashCode(obj));
// 输出一个null对象的toString,输出null
System.out.println(Objects.toString(obj));
// 要求obj不能为null,如果obj为null则引发异常
System.out.println(Objects.requireNonNull(obj, "obj参数不能是null!"));
}
}
程序还示范了 Objects
提供的 requireNonNull()
方法,当传入的参数不为 null
时,该方法返回参数本身;否则将会引发 NullPointerException
异常。
该方法主要用来对方法形参进行输入校验,例如如下代码:
public Foo(Bar bar) {
// 校验bar参数,如果bar参数为null将引发异常;否则this.bar被赋值为bar参数
this.bar = Objects.requireNonNull(bar);
}
字符串处理类
字符串就是一连串的字符序列,Java 提供了 String
和 StringBuffer
两个类来封装字符串,并提供了一系列方法来操作字符串对象。
String
类是不可变类,即一旦一个 String
对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。
StringBuffer
对象则代表一个字符序列可变的字符串,当一个 StringBuffer
被创建以后,通过 StringBuffer
提供的 append()
、insert()
、reverse()
、setCharAt()
、setLength()
等方法可以改变这个字符串对象的字符序列。
一旦通过 StringBuffer
生成了最终想要的字符串,就可以调用它的 toString()
方法将其转换为一个 String
对象。
从 JDK 1.5 开始出现的 StringBuilder
类,也代表字符串对象。实际上,StringBuilder
和 StringBuffer
基本相似,两个类的构造器和方法也基本相同。
不同的是,StringBuffer
是线程安全的,而 StringBuilder
则没有实现线程安全功能,所以性能略高。因此在通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑使用 StringBuilder
类。
String
类提供了大量构造器来创建 String
对象,其中如下几个有特殊用途。
String()
:创建一个包含 0 个字符串序列的String
对象(并不是返回null
)。String(byte[] bytes, Charset charset)
:使用指定的字符集将指定的byte[]
数组解码成一个新的String
对象。String(byte[] bytes, int offset, int length)
:使用平台的默认字符集将从指定byte[]
数组的offset
开始、长度为length
的子数组解码成一个新的String
对象。String(byte[] bytes, int offset, int length, String charsetName)
:使用指定的字符集将指定的byte[]
数组从offset
开始、长度为length
的子数组解码成一个新的String
对象。String(byte[] bytes, String charsetName)
:使用指定的字符集将指定的byte[]
数组解码成一个新的String
对象。String(char[] value, int offset, int count)
:将指定的字符数组从offset
开始、长度为count
的字符元素连缀成字符串。String(String original)
:根据字符串直接量来创建一个String
对象。也就是说,新创建的String
对象是该参数字符串的副本。String(StringBuffer buffer)
:根据StringBuffer
对象来创建对应的String
对象。String(StringBuilder builder)
:根据StringBuilder
对象来创建对应的String
对象。
String
类也提供了大量方法来操作字符串对象,下面详细介绍这些常用方法。
char charAt(int index)
:获取字符串中指定位置的字符。其中,参数index
指的是字符串的序数,字符串的序数从 0 开始到 length()-1。
String s = new String("abcdefghijklmnopqrstuvwxyz");
System.out.println("s.charAt(5): " + s.charAt(5));
// s.charAt(5): f
int compareTo(String anotherString)
:比较两个字符串的大小。如果两个字符串的字符序列相等,则返回 0;不相等时,从两个字符串第 0 个字符开始比较,返回第一个不相等的字符差。另一种情况,较长字符串的前面部分恰巧是较短的字符串,则返回它们的长度差。
String s1 = new String("abcdefghijklmn");
String s2 = new String("abcdefghij");
String s3 = new String("abcdefghijalmn");
// 返回长度差
System.out.println("s1.compareTo(s2): " + s1.compareTo(s2) );
// 返回'k'-'a'的差
System.out.println("s1.compareTo(s3): " + s1.compareTo(s3) );
// s1.compareTo(s2): 4
// s1.compareTo(s3): 10
String concat(String str)
:将该String
对象与str
连接在一起。与 Java 提供的字符串连接运算符“+”的功能相同。boolean contentEquals(StringBuffer sb)
:将该String
对象与StringBuffer
对象sb
进行比较,当它们包含的字符序列相同时返回true
。static String copyValueOf(char[] data)
:将字符数组连缀成字符串,与String[char[] content]
构造器的功能相同。static String copyValueOf(char[] data, int offset, int count)
:将char
数组的子数组中的元素连缀成字符串,与String(char[] value, int offset, int count)
构造器的功能相同。boolean endsWith(String suffix)
:返回该String
对象是否以suffix
结尾。
String s1 = new String("abcdefghij");
String s2 = new String("ghij");
System.out.println("s1.endsWith(s2): " + s1.endsWith(s2));
// s1.endsWith(s2): true
boolean equals(Object anObject)
:将该字符串与指定对象比较,如果二者包含的字符序列相等,则返回true
;否则返回false
。boolean equalsIgnoreCase(String str)
:与前一个方法基本相似,只是忽略字符的大小写。byte[] getBytes()
:将该String
对象转换成byte
数组。void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
:该方法将字符串中从srcBegin
开始,到srcEnd
结束的字符复制到dst
字符数组中,其中dstBegin
为目标字符数组的起始复制位置。
// s1 = I love java
char[] s1 = {'I',' ','l','o','v','e',' ','j','a','v','a'};
String s2 = new String("ejb");
// s1 = I love ejba
s2.getChars(0,3,s1,7);
System.out.println(s1 );
// I love ejba
int indexOf(int ch)
:找出ch
字符在该字符串中第一次出现的位置。int indexOf(int ch, int fromIndex)
:找出ch
字符在该字符串中从fromIndex
开始后第一次出现的位置。int indexOf(String str)
:找出str
子字符串在该字符串中第一次出现的位置。int indexOf(String str, int fromIndex)
:找出str
子字符串在该字符串中从fromIndex
开始后第一次出现的位置。
String s = new String("write once, run anywhere!");
String ss = new String("run");
System.out.println("s.indexOf('r'): " + s.indexOf('r'));
System.out.println("s.indexOf('r',2): " + s.indexOf('r',2));
System.out.println("s.indexOf(ss): " + s.indexOf(ss));
/*
s.indexOf('r'): 1
s.indexOf('r',2): 12
s.indexOf(ss): 12
*/
int lastIndexOf(int ch)
:找出ch
字符在该字符串中最后一次出现的位置。int lastIndexOf(int ch, int fromIndex)
:找出ch
字符在该字符串中从fromIndex
开始后最后一次出现的位置。int lastIndexOf(String str)
:找出str
子字符串在该字符串中最后一次出现的位置。int lastIndexOf(String str, int fromIndex)
:找出str
子字符串在该字符串中从fromIndex
开始后最后一次出现的位置。int length()
:返回当前字符串长度。String replace(char oldChar, char newChar)
:将字符串中的第一个oldChar
替换成newChar
。boolean startsWith(String prefix)
:该String
对象是否以prefix
开始。boolean startsWith(String prefix, int toffset)
:该String
对象从toffset
位置算起,是否以prefix
开始。
String s = new String("write once, run anywhere!");
String ss = new String("write");
String sss = new String("once");
System.out.println("s.startsWith(ss): " + s.startsWith(ss));
System.out.println("s.startsWith(sss,6): " + s.startsWith(sss,6));
/*
s.startsWith(ss): true
s.startsWith(sss,6): true
*/
String substring(int beginIndex)
:获取从beginIndex
位置开始到结束的子字符串。String substring(int beginIndex, int endIndex)
:获取从beginIndex
位置开始到endIndex
位置的子字符串。char[] toCharArray()
:将该String
对象转换成char
数组。String toLowerCase()
:将字符串转换成小写。String toUpperCase()
:将字符串转换成大写。
String s = new String("java.lang.Class String");
System.out.println("s.toUpperCase(): " + s.toUpperCase());
System.out.println("s.toLowerCase(): " + s.toLowerCase());
/*
s.toUpperCase(): JAVA.LANG.CLASS STRING
s.toLowerCase(): java.lang.class string
*/
static String valueOf(X x)
:一系列用于将基本类型值转换为String
对象的方法。
String
类是不可变的,String
的实例一旦生成就不会再改变了,例如如下代码。
String str1 = "java";
str1 = str1 + "struts";
str1 = str1 + "spring";
上面程序除了使用了 3 个字符串直接量之外,还会额外生成 2 个字符串直接量—— "java"
和 "struts"
连接生成的 "javastruts"
,接着 "javastruts"
与 "spring"
连接生成的 "javastrutsspring"
,程序中的 str1
依次指向 3 个不同的字符串对象。
因为
String
是不可变的,所以会额外产生很多临时变量,使用StringBuffer
或StringBuilder
就可以避免这个问题。
StringBuilder
提供了一系列插入、追加、改变该字符串里包含的字符序列的方法。而
StringBuffer
与其用法完全相同,只是StringBuffer
是线程安全的。
StringBuilder
、StringBuffer
有两个属性:length
和capacity
,其中length
属性表示其包含的字符序列的长度。与
String
对象的length
不同的是,StringBuilder
、StringBuffer
的length
是可以改变的,可以通过length()
、setLength(int len)
方法来访问和修改其字符序列的长度。
capacity
属性表示StringBuilder
的容量,capacity
通常比length
大,程序通常无须关心capacity
属性。
public class StringBuilderTest {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
// 追加字符串 sb = "java"
sb.append("java");
// 插入 sb = "hello java"
sb.insert(0, "hello ");
// 替换 sb = "hello, java"
sb.replace(5, 6, ",");
// 删除 sb = "hellojava"
sb.delete(5, 6);
System.out.println(sb);
// 反转 sb = "avajolleh"
sb.reverse();
System.out.println(sb);
// 输出9
System.out.println(sb.length());
// 输出16
System.out.println(sb.capacity());
// 改变StringBuilder的长度,只保留前面部分 sb = "avajo"
sb.setLength(5);
System.out.println(sb);
}
}
上面程序示范了 StringBuilder
类的追加、插入、替换、删除等操作,这些操作改变了 StringBuilder
里的字符序列,这就是 StringBuilder
与 String
之间最大的区别:StringBuilder
的字符序列是可变的。从程序看到 StringBuilder
的 length()
方法返回其字符序列的长度,而 capacity()
返回值则比 length()
返回值大。
Math
Java 提供了基本的+、-、*、/、%等基本算术运算的运算符,但对于更复杂的数学运算,例如,三角函数、对数运算、指数运算等则无能为力。
Java 提供了 Math
工具类来完成这些复杂的运算,Math
类是一个工具类,它的构造器被定义成 private
的,因此无法创建 Math
类的对象;Math
类中的所有方法都是类方法,可以直接通过类名来调用它们。
Math
类除了提供了大量静态方法之外,还提供了两个静态 Field:PI
和 E
,它们的值分别等于 π
和 e
。
Math
类的所有方法名都明确标识了该方法的作用,可查阅 API 来了解 Math
类各方法的说明。
public class MathTest {
public static void main(String[] args) {
/*---------下面是三角运算---------*/
// 将弧度转换成角度
System.out.println("Math.toDegrees(1.57):" + Math.toDegrees(1.57));
// 将角度转换为弧度
System.out.println("Math.toRadians(90):" + Math.toRadians(90));
// 计算反余弦,返回的角度范围在 0.0 到 pi 之间
System.out.println("Math.acos(1.2):" + Math.acos(1.2));
// 计算反正弦,返回的角度范围在 -pi/2 到 pi/2 之间
System.out.println("Math.asin(0.8):" + Math.asin(0.8));
// 计算反正切,返回的角度范围在 -pi/2 到 pi/2 之间
System.out.println("Math.atan(2.3):" + Math.atan(2.3));
// 计算三角余弦
System.out.println("Math.cos(1.57):" + Math.cos(1.57));
// 计算双曲余弦
System.out.println("Math.cosh(1.2 ):" + Math.cosh(1.2));
// 计算正弦
System.out.println("Math.sin(1.57 ):" + Math.sin(1.57));
// 计算双曲正弦
System.out.println("Math.sinh(1.2 ):" + Math.sinh(1.2));
// 计算三角正切
System.out.println("Math.tan(0.8 ):" + Math.tan(0.8));
// 计算双曲正切
System.out.println("Math.tanh(2.1 ):" + Math.tanh(2.1));
// 将矩形坐标 (x, y) 转换成极坐标 (r, thet))
System.out.println("Math.atan2(0.1, 0.2):" + Math.atan2(0.1, 0.2));
/*---------下面是取整运算---------*/
// 取整,返回小于目标数的最大整数
System.out.println("Math.floor(-1.2 ):" + Math.floor(-1.2));
// 取整,返回大于目标数的最小整数
System.out.println("Math.ceil(1.2):" + Math.ceil(1.2));
// 四舍五入取整
System.out.println("Math.round(2.3 ):" + Math.round(2.3));
/*---------下面是乘方、开方、指数运算---------*/
// 计算平方根
System.out.println("Math.sqrt(2.3 ):" + Math.sqrt(2.3));
// 计算立方根
System.out.println("Math.cbrt(9):" + Math.cbrt(9));
// 返回欧拉数 e 的n次幂
System.out.println("Math.exp(2):" + Math.exp(2));
// 返回 sqrt(x2 +y2),没有中间溢出或下溢
System.out.println("Math.hypot(4 , 4):" + Math.hypot(4, 4));
// 按照 IEEE 754 标准的规定,对两个参数进行余数运算
System.out.println("Math.IEEEremainder(5 , 2):" + Math.IEEEremainder(5, 2));
// 计算乘方
System.out.println("Math.pow(3, 2):" + Math.pow(3, 2));
// 计算自然对数
System.out.println("Math.log(12):" + Math.log(12));
// 计算底数为10的对数
System.out.println("Math.log10(9):" + Math.log10(9));
// 返回参数与1之和的自然对数
System.out.println("Math.log1p(9):" + Math.log1p(9));
/*---------下面是符号相关的运算---------*/
// 计算绝对值
System.out.println("Math.abs(-4.5):" + Math.abs(-4.5));
// 符号赋值,返回带有第二个浮点数符号的第一个浮点参数
System.out.println("Math.copySign(1.2, -1.0):" + Math.copySign(1.2, -1.0));
// 符号函数,如果参数为 0,则返回 0;如果参数大于 0
// 则返回 1.0;如果参数小于 0,则返回 -1.0
System.out.println("Math.signum(2.3):" + Math.signum(2.3));
/*---------下面是大小相关的运算---------*/
// 找出最大值
System.out.println("Math.max(2.3 , 4.5):" + Math.max(2.3, 4.5));
// 计算最小值
System.out.println("Math.min(1.2 , 3.4):" + Math.min(1.2, 3.4));
// 返回第一个参数和第二个参数之间与第一个参数相邻的浮点数
System.out.println("Math.nextAfter(1.2, 1.0):" + Math.nextAfter(1.2, 1.0));
// 返回比目标数略大的浮点数
System.out.println("Math.nextUp(1.2 ):" + Math.nextUp(1.2));
// 返回一个伪随机数,该值大于等于 0.0 且小于 1.0
System.out.println("Math.random():" + Math.random());
}
}
随机数
Random
类专门用于生成一个伪随机数,它有两个构造器:一个构造器使用默认的种子(以当前时间作为种子),另一个构造器需要程序员显式传入一个 long
型整数的种子。
ThreadLocalRandom
类是 Java 7 新增的一个类,它是 Random
的增强版。在并发访问的环境下,使用 ThreadLocalRandom
来代替 Random
可以减少多线程资源竞争,最终保证系统具有较好的性能。
ThreadLocalRandom
类的用法与 Random
类的用法基本相似,它提供了一个静态的 current()
方法来获取 ThreadLocalRandom
对象,获取该对象之后即可调用各种 nextXxx()
方法来获取伪随机数了。
ThreadLocalRandom
与 Random
都比 Math
的 random()
方法提供了更多的方式来生成各种伪随机数,可以生成浮点类型的伪随机数,也可以生成整数类型的伪随机数,还可以指定生成随机数的范围。
public class RandomTest {
public static void main(String[] args) {
Random rand = new Random();
System.out.println("rand.nextBoolean():" + rand.nextBoolean());
byte[] buffer = new byte[16];
rand.nextBytes(buffer);
System.out.println(Arrays.toString(buffer));
// 生成0.0~1.0之间的伪随机double数
System.out.println("rand.nextDouble():" + rand.nextDouble());
// 生成0.0~1.0之间的伪随机float数
System.out.println("rand.nextFloat():" + rand.nextFloat());
// 生成平均值是 0.0,标准差是 1.0的伪高斯数
System.out.println("rand.nextGaussian():" + rand.nextGaussian());
// 生成一个处于int整数取值范围的伪随机整数
System.out.println("rand.nextInt():" + rand.nextInt());
// 生成0~26之间的伪随机整数
System.out.println("rand.nextInt(26):" + rand.nextInt(26));
// 生成一个处于long整数取值范围的伪随机整数
System.out.println("rand.nextLong():" + rand.nextLong());
}
}
Random
使用一个 48 位的种子,如果这个类的两个实例是用同一个种子创建的,对它们以同样的顺序调用方法,则它们会产生相同的数字序列。
当使用默认的种子构造 Random
对象时,它们属于同一个种子。
public class SeedTest {
public static void main(String[] args) {
Random r1 = new Random(50);
System.out.println("第一个种子为50的Random对象");
System.out.println("r1.nextBoolean():\t" + r1.nextBoolean());
System.out.println("r1.nextInt():\t\t" + r1.nextInt());
System.out.println("r1.nextDouble():\t" + r1.nextDouble());
System.out.println("r1.nextGaussian():\t" + r1.nextGaussian());
System.out.println("---------------------------");
Random r2 = new Random(50);
System.out.println("第二个种子为50的Random对象");
System.out.println("r2.nextBoolean():\t" + r2.nextBoolean());
System.out.println("r2.nextInt():\t\t" + r2.nextInt());
System.out.println("r2.nextDouble():\t" + r2.nextDouble());
System.out.println("r2.nextGaussian():\t" + r2.nextGaussian());
System.out.println("---------------------------");
Random r3 = new Random(100);
System.out.println("种子为100的Random对象");
System.out.println("r3.nextBoolean():\t" + r3.nextBoolean());
System.out.println("r3.nextInt():\t\t" + r3.nextInt());
System.out.println("r3.nextDouble():\t" + r3.nextDouble());
System.out.println("r3.nextGaussian():\t" + r3.nextGaussian());
}
}
如果两个 Random
对象的种子相同,而且方法的调用顺序也相同,则它们会产生相同的数字序列。就是说,Random
产生的数字并不是真正随机的,而是一种伪随机。
为了避免两个 Random
对象产生相同的数字序列,通常推荐使用当前时间作为 Random
对象的种子,如下代码所示。
Random rand = new Random(System.currentTimeMillis());
在多线程环境下使用 ThreadLocalRandom
的方式与使用 Random
基本类似,如下程序片段示范了 ThreadLocalRandom
的用法。
ThreadLocalRandom rand = ThreadLocalRandom.current();
// 生成一个4~20之间的伪随机整数
int val1 = rand.nextInt(4 , 20);
// 生成一个2.0~10.0之间的伪随机浮点数
int val2 = rand.nextDouble(2.0, 10.0);
BigDecimal
前面在介绍 float
、double
两种基本浮点类型时已经指出,这两个基本类型的浮点数容易引起精度丢失。
public class DoubleTest {
public static void main(String[] args) {
System.out.println("0.05 + 0.01=" + (0.05 + 0.01));
System.out.println("1.0 - 0.42=" + (1.0 - 0.42));
System.out.println("4.015 * 100=" + (4.015 * 100));
System.out.println("123.3 / 100=" + (123.3 / 100));
}
}
不仅是 Java,很多编程语言也存在这样的问题。
为了能精确表示、计算浮点数,Java 提供了 BigDecimal
类,该类提供了大量的构造器用于创建 BigDecimal
对象,包括把所有的基本数值型变量转换成一个 BigDecimal
对象,也包括利用数字字符串、数字字符数组来创建 BigDecimal
对象。
查看 BigDecimal
类的 BigDecimal(double val)
构造器的详细说明时,可以看到不推荐使用该构造器的说明,主要是因为使用该构造器时有一定的不可预知性。
当程序使用 new BigDecimal(0.1)
来创建一个 BigDecimal
对象时,它的值并不是 0.1,它实际上等于 0.1000000000000000055511151231257827021181583404541015625。
这是因为 0.1 无法准确地表示为 double
浮点数,所以传入 BigDecimal
构造器的值不会正好等于 0.1(虽然表面上等于该值)。
如果使用 BigDecimal(String val)
构造器的结果是可预知的——写入 new BigDecimal("0.1")
将创建一个 BigDecimal
,它正好等于预期的 0.1。
因此通常建议优先使用基于 String
的构造器。
如果必须使用 double
浮点数作为 BigDecimal
构造器的参数时,不要直接将该 double
浮点数作为构造器参数创建 BigDecimal
对象,而是应该通过 BigDecimal.valueOf(double value)
静态方法来创建 BigDecimal
对象。
BigDecimal
类提供了 add()
、subtract()
、multiply()
、divide()
、pow()
等方法对精确浮点数进行常规算术运算。
public class BigDecimalTest {
public static void main(String[] args) {
BigDecimal f1 = new BigDecimal("0.05");
BigDecimal f2 = BigDecimal.valueOf(0.01);
BigDecimal f3 = new BigDecimal(0.05);
System.out.println("使用String作为BigDecimal构造器参数:");
System.out.println("0.05 + 0.01=" + f1.add(f2));
System.out.println("0.05 - 0.01=" + f1.subtract(f2));
System.out.println("0.05 * 0.01=" + f1.multiply(f2));
System.out.println("0.05 / 0.01=" + f1.divide(f2));
System.out.println("使用double作为BigDecimal构造器参数:");
System.out.println("0.05 + 0.01=" + f3.add(f2));
System.out.println("0.05 - 0.01=" + f3.subtract(f2));
System.out.println("0.05 * 0.01=" + f3.multiply(f2));
System.out.println("0.05 / 0.01=" + f3.divide(f2));
}
}
从上面运行结果可以看出 BigDecimal
进行算术运算的效果,而且可以看出创建 BigDecimal
对象时,一定要使用 String
对象作为构造器参数,而不是直接使用 double
数字。
提示
创建 BigDecimal
对象时,不要直接使用 double
浮点数作为参数来调用 BigDecimal
构造器,否则同样会发生精度丢失的问题。
如果程序中要求对 double
浮点数进行加、减、乘、除基本运算,则需要先将 double
类型数值包装成 BigDecimal
对象,调用 BigDecimal
对象的方法执行运算后再将结果转换成 double
型变量。
这是比较烦琐的过程,可以考虑以 BigDecimal
为基础定义一个 Arith
工具类,该工具类代码如下。
public class Arith {
// 默认除法运算精度
private static final int DEF_DIV_SCALE = 10;
// 构造器私有,让这个类不能实例化
private Arith() {
}
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.add(b2).doubleValue();
}
/**
* 提供精确的减法运算
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double sub(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double mul(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}
/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 小数点以后10位的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/
public static double div(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, DEF_DIV_SCALE, BigDecimal.ROUND_HALF_UP).doubleValue();
}
public static void main(String[] args) {
System.out.println("0.05 + 0.01=" + Arith.add(0.05, 0.01));
System.out.println("1.0 - 0.42=" + Arith.sub(1.0, 0.42));
System.out.println("4.015 * 100=" + Arith.mul(4.015, 100));
System.out.println("123.3 / 100=" + Arith.div(123.3, 100));
}
}
处理日期的类
Java 还提供了一系列用于处理日期、时间的类,包括创建日期、时间对象,获取系统当前日期、时间等操作。
Date
此处的 Date
是指 java.util
包下的 Date
类,而不是 java.sql
包下的 Date
类,Date
对象既包含日期,也包含时间。
Date
类提供了 6 个构造器,其中 4 个已经 Deprecated
(Java 不再推荐使用,使用不再推荐的构造器时编译器会提出警告信息,并导致程序性能、安全性等方面的问题),剩下的两个构造器如下所示。
Date()
:生成一个代表当前日期时间的Date
对象。该构造器在底层调用System.currentTimeMillis()
获得long
整数作为日期参数。Date(long date)
:根据指定的long
型整数来生成一个Date
对象。该构造器的参数表示创建的Date
对象和 GMT 1970 年 1 月 1 日 00:00:00 之间的时间差,以毫秒作为计时单位。
Date
对象的大部分方法也 Deprecated
了,剩下为数不多的几个方法。
boolean after(Date when)
:测试该日期是否在指定日期when
之后。boolean before(Date when)
:测试该日期是否在指定日期when
之前。int compareTo(Date anotherDate)
:比较两个日期的大小,后面的时间大于前面的时间时返回-1,否则返回 1。boolean equals(Object obj)
:当两个时间表示同一时刻时返回true
。long getTime()
:返回该时间对应的long
型整数,即从 GMT 1970-01-0100:00:00 到该Date
对象之间的时间差,以毫秒作为计时单位。void setTime(long time)
:设置该Date
对象的时间。
public class DateTest {
public static void main(String[] args) {
Date d1 = new Date();
// 获取当前时间之后100ms的时间
Date d2 = new Date(System.currentTimeMillis() + 100);
System.out.println(d2);
System.out.println(d1.compareTo(d2));
System.out.println(d1.before(d2));
}
}
因为 Date
类的很多方法已经不推荐使用了,所以 Date
类的功能已经被大大削弱了。
例如,对时间进行加减运算,获取指定 Date
对象里年、月、日的所有方法都已被 Deprecated
,如果需要对日期进行这些运算,则应该使用 Calendar
工具类。
Calendar
Calendar
是一个抽象类,它用于表示日历。
历史上有着许多种纪年方法,它们的差异实在太大了,比如说一个人的生日是“七月七日”,那么一种可能是阳(公)历的七月七日,但也可以是阴(农)历的日期。为了统一计时,全世界通常选择最普及、最通用的日历:Gregorian
Calendar
,也就是我们在讲述年份时常用的“公元几几年”。
Calendar
类本身是一个抽象类,它是所有日历类的模板,并提供了一些所有日历通用的方法;但它本身不能直接实例化,程序只能创建 Calendar
子类的实例,Java 本身提供了一个 GregorianCalendar
类,一个代表 GregorianCalendar
的子类,它代表了我们通常所说的公历。
当然,也可以创建自己的 Calendar
子类,然后将它作为 Calendar
对象使用(这就是多态)。在 IBM 的alphaWorks 站点上,IBM 的开发人员实现了多种日历。在 Internet
上,也有对中国农历的实现。
Calendar
类是一个抽象类,所以不能使用构造器来创建 Calendar
对象。但它提供了几个静态 getInstance()
方法来获取 Calendar
对象,这些方法根据 TimeZone
,Locale
类来获取特定的 Calendar
,如果不指定 TimeZone
、Locale
,则使用默认的 TimeZone
、Locale
来创建 Calendar
。
Calendar
与 Date
都是表示日期的工具类,它们直接可以自由转换,如下代码所示。
Calendar calendar = Calendar.getInstance();
// 从Calendar 对象中取出Date 对象
Date date = calendar.getTime();
// 通过Date对象获得对应的Calendar对象
// 因为Calendar/GregorianCalendar没有构造函数可以接收Date对象
// 所以必须先获得一个Calendar实例,然后调用其setTime()方法
Calendar calendar2 = Calendar.getInstance();
calendar2.setTime(date);
Calendar
类提供了大量访问、修改日期时间的方法,常用方法如下。
void add(int field, int amount)
:根据日历的规则,为给定的日历字段添加或减去指定的时间量。int get(int field)
:返回指定日历字段的值。int getActualMaximum(int field)
:返回指定日历字段可能拥有的最大值。例如月,最大值为 11。int getActualMinimum(int field)
:返回指定日历字段可能拥有的最小值。例如月,最小值为 0。void roll(int field, int amount)
:与 add()方法类似,区别在于加上 amount 后超过了该字段所能表示的最大范围时,也不会向上一个字段进位。void set(int field, int value)
:将给定的日历字段设置为给定值。void set(int year, int month, int date)
:设置 Calendar 对象的年、月、日 3 个字段的值。void set(int year, int month, int date, int hourOfDay, int minute, int second)
:设置 Calendar 对象的年、月、日、时、分、秒 6 个字段的值。
上面的很多方法都需要一个 int
类型的 field 参数,field 是 Calendar
类的静态 Field,如 Calendar.YEAR
、Calendar.MONTH
等分别代表了年、月、日、小时、分钟、秒等时间字段。
Calendar.MONTH
字段代表月份,月份的起始值不是 1,而是 0,所以要设置 8 月时,用 7 而不是 8。
import java.util.Calendar;
// 静态导入
import static java.util.Calendar.*;
public class CalendarTest {
public static void main(String[] args) {
Calendar c = Calendar.getInstance();
// 取出年
System.out.println(c.get(YEAR));
// 取出月份
System.out.println(c.get(MONTH));
// 取出日
System.out.println(c.get(DATE));
// 分别设置年、月、日、小时、分钟、秒
c.set(2022, 08, 09, 12, 32, 23);
// 2022-08-09 12:32:23
System.out.println(c.getTime());
// 将Calendar的年前推1年
c.add(YEAR, -1);
// 2022-08-09 12:32:23
System.out.println(c.getTime());
// 将Calendar的月前推8个月
c.roll(MONTH, -8);
// 2022-08-09 12:32:23
System.out.println(c.getTime());
}
}
提示
上面程序使用了静态导入,它导入了 Calendar
类里的所有静态 Field,所以上面程序可以直接使用 Calendar
类的 YEAR
、MONTH
、DATE
等静态 Field。
Calendar
类还有如下几个注意点。
add
与 roll
的区别:
add(int field, int amount)
的功能非常强大,add
主要用于改变 Calendar
的特定字段的值。如果需要增加某字段的值,则让 amount
为正数;如果需要减少某字段的值,则让 amount
为负数即可。
add(int field, int amount)
有如下两条规则。
- 当被修改的字段超出它允许的范围时,会发生进位,即上一级字段也会增大。例如:
Calendar cal1 = Calendar.getInstance();
// 2022-8-9
cal1.set(2022, 7, 9, 0, 0, 0);
// 2022-8-9=> 2023-2-9
cal1.add(MONTH, 6);
- 如果下一级字段也需要改变,那么该字段会修正到变化最小的值。例如:
Calendar cal2 = Calendar.getInstance();
// 2022-8-31
cal2.set(2022, 7, 31, 0, 0, 0);
// 因为进位后月份改为2月,2月没有31日,自动变成28日
// 2022-8-31=> 2023-2-28
cal2.add(MONTH, 6);
roll
的规则与 add
的处理规则不同:当被修改的字段超出它允许的范围时,上一级字段不会增大。
Calendar cal3 = Calendar.getInstance();
// 2022-8-23
cal3.set(2022, 7, 23, 0, 0, 0);
// MONTH字段“进位”,但YEAR字段并不增加
// 2022-8-23=> 2022-2-23
cal3.roll(MONTH, 6);
下一级字段的处理规则与 add
相似:
Calendar cal4 = Calendar.getInstance();
// 2022-8-31
cal4.set(2022, 7, 31, 0, 0 , 0);
// MONTH字段“进位”后变成2,2月没有31日
// YEAR字段不会改变,2022年2月只有28天
// 2022-8-31=> 2022-2-28
cal4.roll(MONTH, 6);
设置 Calendar
的容错性:
当我们调用 Calendar
对象的 set
方法来改变指定时间字段上的值时,有可能传入一个不合法的参数,例如为 MONTH
字段设置 13,这将会导致怎样的后果呢?
public class LenientTest {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
// ①结果是YEAR字段加1,MONTH字段为1(2月)
cal.set(MONTH, 13);
System.out.println(cal.getTime());
// 关闭容错性
cal.setLenient(false);
// ②导致运行时异常
cal.set(MONTH, 13);
System.out.println(cal.getTime());
}
}
① 处代码可以正常运行,因为设置 MONTH
字段的值为 13,将会导致 YEAR
字段加 1;② 处代码将会导致运行时异常,因为设置的 MONTH
字段值超出了 MONTH
字段允许的范围。
Calendar
提供了一个 setLenient()
用于设置它的容错性,Calendar
默认支持较好的容错性,通过 setLenient(false)
可以关闭 Calendar
的容错性,让它进行严格的参数检查。
提示
Calendar
有两种解释日历字段的模式:lenient
模式和 non-lenient
模式。当 Calendar
处于 lenient
模式时,每个时间字段可接受超出它允许范围的值;当 Calendar
处于 non-lenient
模式时,如果为某个时间字段设置的值超出了它允许的取值范围,程序将会抛出异常。
set
方法延迟修改:
set(f, value)
方法将日历字段 f
更改为 value
,此外,它还设置了一个内部成员变量,以指示日历字段 f
已经被更改。
尽管日历字段 f
是立即更改的,但该 Calendar
所代表的时间却不会立即修改,直到下次调用 get()
、getTime()
、getTimeInMillis()
、add()
或 roll()
时才会重新计算日历的时间。
这被称为 set
方法的延迟修改,采用延迟修改的优势是多次调用 set()
不会触发多次不必要的计算(需要计算出一个代表实际时间的 long
型整数)。
public class LazyTest {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
// 2022-8-31
cal.set(2022, 7, 31);
// 将月份设为9,但9月31日不存在
// 如果立即修改,系统将会把cal自动调整到10月1日
cal.set(MONTH, 8);
// ①下面代码输出10月1日
// System.out.println(cal.getTime());
// ②设置DATE字段为5
cal.set(DATE, 5);
// ③
System.out.println(cal.getTime());
}
}
如果程序在 ① 号代码处输出当前 Calendar
里的日期,也会看到输出 2022-10-1,③ 号代码处将输出 2022-10-5。
如果程序将 ① 处代码注释起来,因为 Calendar
的 set()
方法具有延迟修改的特性,即调用 set()
方法后 Calendar
实际上并未计算真实的日期,它只是使用内部成员变量表记录 MONTH
字段被修改为 8,接着程序设置 DATE
字段值为 5,程序内部再次记录 DATE
字段为 5——就是 9 月 5 日,因此看到 ③ 处输出 2022-9-5。
TimeZone
在地理上,地球被划分成 24 个时区,中国北京时间属于东八区,而程序中对时间的默认实现是以格林威治时间为标准的,这样就产生了 8 小时的时间差。为了让程序更加通用,可以使用 TimeZone
设置程序中时间所属的时区,其中 TimeZone
就代表了时区。
TimeZone
是一个抽象类,不能调用其构造器来创建实例,但可以调用它的静态方法:getDefault()
或 getTimeZone()
得到 TimeZone
实例。
其中 getDefault()
方法用于获得运行机器上的默认时区,默认时区可以通过修改操作系统的相关配置来进行调整;getTimeZone()
则根据时区 ID
来获取对应的时区。
TimeZone
类提供了一些有用的方法用于获取时区的相关信息。
static String[] getAvailableIDs()
:获取 Java 所支持的所有时区ID
。static TimeZone getDefault()
:获取运行机器上默认的时区。String getDisplayName()
:获取该TimeZone
对象的时区名称。String getID()
:获取该时区的ID
。static TimeZone getTimeZone(String ID)
:获取指定ID
对应的TimeZone
对象。
public class TimeZoneTest {
public static void main(String[] args) {
// 取得Java所支持的所有时区ID
String[] ids = TimeZone.getAvailableIDs();
System.out.println(Arrays.toString(ids));
TimeZone my = TimeZone.getDefault();
// 获取系统默认时区的ID:Asia/Shanghai
System.out.println(my.getID());
// 获取系统默认时区的名称:中国标准时间
System.out.println(my.getDisplayName());
// 获取指定ID的时区名称:纽芬兰标准时间
System.out.println(TimeZone.getTimeZone("CNT").getDisplayName());
}
}
正则表达式
正则表达式是一个强大的字符串处理工具,可以对字符串进行查找、提取、分割、替换等操作。 String
类里也提供了如下几个特殊的方法。
boolean matches(String regex)
:判断该字符串是否匹配指定的正则表达式。String replaceAll(String regex, String replacement)
:将该字符串中所有匹配regex
的子串替换成replacement
。String replaceFirst(String regex, String replacement)
:将该字符串中第一个匹配regex
的子串替换成replacement
。String[] split(String regex)
:以regex
作为分隔符,把该字符串分割成多个子串。
上面这些特殊的方法都依赖于 Java 提供的正则表达式支持,除此之外,Java 还提供了 Pattern
和 Matcher
两个类专门用于提供正则表达式支持。
正则表达式是一个用于匹配字符串的模板。可以说,我们定义的任意字符串都可以当成正则表达式使用,例如 "abc"
,它也是一个正则表达式,只是它只能匹配 "abc"
字符串。
创建正则表达式
正则表达式所支持的合法字符。
除此之外,正则表达式中有一些特殊字符,这些特殊字符在正则表达式中有其特殊的用途,比如前面介绍的反斜线 \
。如果需要匹配这些特殊字符,就必须首先将这些字符转义,也就是在前面添加一个反斜线 \
。
正则表达式中的特殊字符如表。
将上面多个字符拼起来,就可以创建一个正则表达式。例如:
"\\u0041\\" // 匹配a\
"\\0101\t" // 匹配a<制表符>
"\?\[" // 匹配?[
上面的正则表达式依然只能匹配单个字符,这是因为还未在正则表达式中使用“通配符”,“通配符”是可以匹配多个字符的特殊字符。
正则表达式中的“通配符”远远超出了普通通配符的功能,它被称为预定义字符,正则表达式支持如表。
提示
上面的 7 个预定义字符其实很容易记忆:
d
是 digit
的意思,代表数字;
s
是 space
的意思,代表空白;
w
是 word
的意思,代表单词。
d
、s
、w
的大写形式恰好匹配与之相反的字符。
有了上面的预定义字符后,我们就可以创建更强大的正则表达式。例如:
c\wt // 可以匹配cat、cbt、cct、c0t、c9t等一批字符串
\d\d\d-\d\d\d-\d\d\d\d // 匹配如000-000-0000形式的电话号码
在一些特殊情况下,例如,若只想匹配 a ~ f 的字母,或者匹配除了 ab 之外的所有小写字母,或者匹配中文字符,上面这些预定义字符就无能为力了,此时就需要使用方括号表达式,方括号表达式有如表。
提示
方括号表达式比前面的预定义字符灵活多了,几乎可以匹配任何字符。
例如,若需要匹配所有的中文字符,就可以利用 [\u0041-\u0056]
形式——因为所有中文字符的 Unicode
值是连续的,只要找出所有中文字符中最小、最大的 Unicode
值,就可以利用上面形式来匹配所有的中文字符。
正则表示还支持圆括号表达式,用于将多个表达式组成一个子表达式,圆括号中可以使用或运算符 |
。例如,正则表达式 "(public|protected|private)"
用于匹配 Java 的三个访问控制符其中之一。
除此之外,Java 正则表达式还支持如表所示的几个边界匹配符。
前面例子中需要建立一个匹配 000-000-0000 形式的电话号码时,我们使用了 \d\d\d-\d\d\d-\d\d\d\d
正则表达式,这看起来比较烦琐。
实际上,正则表达式还提供了数量标识符,正则表达式支持的数量标识符有如下几种模式。
Greedy
(贪婪模式):数量表示符默认采用贪婪模式,除非另有表示。贪婪模式的表达式会一直匹配下去,直到无法匹配为止。如果你发现表达式匹配的结果与预期的不符,很有可能是因为——你以为表达式只会匹配前面几个字符,而实际上它是贪婪模式,所以会一直匹配下去。Reluctant
(勉强模式):用问号后缀?
表示,它只会匹配最少的字符。也称为最小匹配模式。Possessive
(占有模式):用加号后缀+
表示,目前只有 Java 支持占有模式,通常比较少用。
三种模式的数量表示符如表所示。
关于贪婪模式和勉强模式的对比,看如下代码:
public class Test {
public static void main(String[] args) {
String str = "hello , java!";
// 贪婪模式的正则表达式
// 输出■ , java!
System.out.println(str.replaceFirst("\\w*", "■"));
// 勉强模式的正则表达式
// 输出■hello , java!
System.out.println(str.replaceFirst("\\w*?", "■"));
}
}
当从 "hello , java!"
字符串中查找匹配 "\\w*"
子串时,因为 "\\w*"
使用了贪婪模式,数量表示符 *
会一直匹配下去,所以该字符串前面的所有单词字符都被它匹配到,直到遇到空格,所以替换后的效果是 "■ , java!"
如果使用勉强模式,数量表示符 *
会尽量匹配最少字符,即匹配 0 个字符,所以替换后的结果是 "■hello , java!"
。
使用正则表达式
一旦在程序中定义了正则表达式,就可以使用 Pattern
和 Matcher
来使用正则表达式。
Pattern
对象是正则表达式编译后在内存中的表示形式,因此,正则表达式字符串必须先被编译为 Pattern
对象,然后再利用该 Pattern
对象创建对应的 Matcher
对象。执行匹配所涉及的状态保留在 Matcher
对象中,多个 Matcher
对象可共享同一个 Pattern
对象。
// 将一个字符串编译成Pattern对象
Pattern p = Pattern.compile("a*b");
// 使用Pattern对象创建Matcher对象
Matcher m = p.matcher("aaaaab");
// 返回true
boolean b = m.matches();
上面定义的 Pattern
对象可以多次重复使用。
如果某个正则表达式仅需一次使用,则可直接使用 Pattern
类的静态 matches
方法,此方法自动把指定字符串编译成匿名的 Pattern
对象,并执行匹配,如下所示。
// 返回true
boolean b = Pattern.matches("a*b", "aaaaab");
采用这种语句每次都需要重新编译新的 Pattern
对象,不能重复利用已编译的 Pattern
对象,所以效率不高。
Pattern
是不可变类,可供多个并发线程安全使用。
Matcher
类提供了如下几个常用方法。
find()
:返回目标字符串中是否包含与Pattern
匹配的子串。group()
:返回上一次与Pattern
匹配的子串。start()
:返回上一次与Pattern
匹配的子串在目标字符串中的开始位置。end()
:返回上一次与Pattern
匹配的子串在目标字符串中的结束位置加 1。lookingAt()
:返回目标字符串前面部分与Pattern
是否匹配。matches()
:返回整个目标字符串与Pattern
是否匹配。reset()
,将现有的Matcher
对象应用于一个新的字符序列。
相关信息
在 Pattern
、Matcher
类的介绍中经常会看到一个 CharSequence
接口,该接口代表一个字符序列,其中 CharBuffer
、String
、StringBuffer
、StringBuilder
都是它的实现类。简单地说,CharSequence
代表一个各种表示形式的字符串。
通过 Matcher
类的 find()
和 group()
方法可以从目标字符串中依次取出特定子串(匹配正则表达式的子串)。
public class FindGroup {
public static void main(String[] args) {
// 创建一个Pattern对象,并用它建立一个Matcher对象
Matcher m = Pattern.compile("\\w+").matcher("Java is very easy!");
while (m.find()) {
System.out.println(m.group());
}
int i = 0;
while (m.find(i)) {
System.out.print(m.group() + "\t");
i++;
}
}
}
/*
Java
is
very
easy
Java ava va a is is s very very ery ry y easy easy asy sy y
*/
find()
方法依次查找字符串中与 Pattern
匹配的子串,一旦找到对应的子串,下次调用 find()
方法时将接着向下查找。
除此之外,find()
方法还可以传入一个 int
类型的参数,带 int
参数的 find()
方法从该 int
索引处向下搜索。
start()
和 end()
方法主要用于确定子串在目标字符串中的位置,如下程序所示。
public class StartEnd {
public static void main(String[] args) {
// 创建一个Pattern对象,并用它建立一个Matcher对象
String regStr = "Java is very easy!";
System.out.println("目标字符串是:" + regStr);
Matcher m = Pattern.compile("\\w+").matcher(regStr);
while (m.find()) {
System.out.println(m.group() + "子串的起始位置:" + m.start() + ",其结束位置:" + m.end());
}
}
}
/*
目标字符串是:Java is very easy!
Java子串的起始位置:0,其结束位置:4
is子串的起始位置:5,其结束位置:7
very子串的起始位置:8,其结束位置:12
easy子串的起始位置:13,其结束位置:17
*/
上面程序使用 find()
、group()
方法逐项取出目标字符串中与指定正则表达式匹配的子串,并使用 start()
、end()
方法返回子串在目标字符串中的位置。
matches()
和 lookingAt()
方法有点相似,只是 matches()
方法要求整个字符串和 Pattern
完全匹配时才返回 true
,而 lookingAt()
只要字符串以 Pattern
开头就会返回 true
。reset()
方法可将现有的 Matcher
对象应用于新的字符序列。
public class MatchesTest {
public static void main(String[] args) {
String[] mails = {"kongyeeku@163.com", "kongyeeku@gmail.com", "ligang@crazyit.org", "wawa@abc.xx"};
String mailRegEx = "\\w{3,20}@\\w+\\.(com|org|cn|net|gov)";
Pattern mailPattern = Pattern.compile(mailRegEx);
Matcher matcher = null;
for (String mail : mails) {
if (matcher == null) {
matcher = mailPattern.matcher(mail);
} else {
matcher.reset(mail);
}
String result = mail + (matcher.matches() ? "是" : "不是") + "一个有效的邮件地址!";
System.out.println(result);
}
}
}
上面程序创建了一个邮件地址的 Pattern
,接着用这个 Pattern
与多个邮件地址进行匹配。当程序中的 Matcher
为 null
时,程序调用 matcher()
方法来创建一个 Matcher
对象,一旦 Matcher
对象被创建,程序就调用 Matcher
的 reset()
方法将该 Matcher
应用于新的字符序列。
相关信息
从某个角度来看,Matcher
的 matches()
、lookingAt()
和 String
类的 equals()
、startsWith()
有点相似。
区别是 String
类的 equals()
和 startsWith()
都是与字符串进行比较,而 Matcher
的 matches()
和 lookingAt()
则是与正则表达式进行匹配。
事实上,String
类里也提供了 matches()
方法,该方法返回该字符串是否匹配指定的正则表达式。
例如:
"kongyeeku@163.com".matches("\\w{3,20}@\\w+\\.(com|org|cn|net|gov)");
// 返回true
除此之外,还可以利用正则表达式对目标字符串进行分割、查找、替换等操作,看如下例子程序。
public class ReplaceTest {
public static void main(String[] args) {
String[] msgs = {"Java has regular expressions in 1.4", "regular expressions now expressing in Java",
"Java represses oracular expressions"};
Pattern p = Pattern.compile("re\\w*");
Matcher matcher = null;
for (int i = 0; i < msgs.length; i++) {
if (matcher == null) {
matcher = p.matcher(msgs[i]);
} else {
matcher.reset(msgs[i]);
}
System.out.println(matcher.replaceAll("哈哈:)"));
}
}
}
String
类中也提供了 replaceAll()
、replaceFirst()
、split()
等方法。
下面的例子程序直接使用 String
类提供的正则表达式功能来进行替换和分割。
public class StringReg {
public static void main(String[] args) {
String[] msgs = {"Java has regular expressions in 1.4", "regular expressions now expressing in Java",
"Java represses oracular expressions"};
for (String msg : msgs) {
System.out.println(msg.replaceFirst("re\\w*", "哈哈:)"));
System.out.println(Arrays.toString(msg.split(" ")));
}
}
}
/*
Java has 哈哈:) expressions in 1.4
[Java, has, regular, expressions, in, 1.4]
哈哈:) expressions now expressing in Java
[regular, expressions, now, expressing, in, Java]
Java 哈哈:) oracular expressions
[Java, represses, oracular, expressions]
*/
格式化
NumberFormat
MessageFormat
是抽象类 Format
的子类,Format
抽象类还有两个子类:
NumberFormat
和 DateFormat
,它们分别用以实现数值、日期的格式化。
NumberFormat
、DateFormat
可以将数值、日期转换成字符串,也可以将字符串转换成数值、日期。
NumberFormat
和 DateFormat
都包含了 format()
和 parse()
方法,其中 format()
用于将数值、日期格式化成字符串,parse()
用于将字符串解析成数值、日期。
NumberFormat
也是一个抽象基类,所以无法通过它的构造器来创建 NumberFormat
对象,它提供了如下几个工厂方法来得到 NumberFormat
对象。
getCurrencyInstance()
:返回默认 Locale 的货币格式器。也可以在调用该方法时传入指定的 Locale,则获取指定 Locale 的货币格式器。getIntegerInstance()
:返回默认 Locale 的整数格式器。也可以在调用该方法时传入指定的 Locale,则获取指定 Locale 的整数格式器。getNumberInstance()
:返回默认 Locale 的通用数值格式器。也可以在调用该方法时传入指定的 Locale,则获取指定 Locale 的通用数值格式器。getPercentInstance()
:返回默认 Locale 的百分数格式器。也可以在调用该方法时传入指定的 Locale,则获取指定 Locale 的百分数格式器。
一旦取得了 NumberFormat
对象后,就可以调用它的 format()
方法来格式化数值,包括整数和浮点数。
public class NumberFormatTest {
public static void main(String[] args) {
// 需要被格式化的数字
double db = 1234000.567;
// 创建四个Locale,分别代表中国、日本、德国、美国
Locale[] locales = {Locale.CHINA, Locale.JAPAN, Locale.GERMAN, Locale.US};
NumberFormat[] nf = new NumberFormat[12];
// 为上面四个Locale创建12个NumberFormat对象
// 每个Locale分别有通用数值格式器、百分数格式器、货币格式器
for (int i = 0; i < locales.length; i++) {
nf[i * 3] = NumberFormat.getNumberInstance(locales[i]);
nf[i * 3 + 1] = NumberFormat.getPercentInstance(locales[i]);
nf[i * 3 + 2] = NumberFormat.getCurrencyInstance(locales[i]);
}
for (int i = 0; i < locales.length; i++) {
switch (i) {
case 0:
System.out.println("-------中国的格式--------");
break;
case 1:
System.out.println("-------日本的格式--------");
break;
case 2:
System.out.println("-------德国的格式--------");
break;
case 3:
System.out.println("-------美国的格式--------");
break;
}
System.out.println("通用数值格式:" + nf[i * 3].format(db));
System.out.println("百分比数值格式:" + nf[i * 3 + 1].format(db));
System.out.println("货币数值格式:" + nf[i * 3 + 2].format(db));
}
}
}
/*
-------中国的格式--------
通用数值格式:1,234,000.567
百分比数值格式:123,400,057%
货币数值格式:¥1,234,000.57
-------日本的格式--------
通用数值格式:1,234,000.567
百分比数值格式:123,400,057%
货币数值格式:¥1,234,001
-------德国的格式--------
通用数值格式:1.234.000,567
百分比数值格式:123.400.057%
货币数值格式:¤ 1.234.000,57
-------美国的格式--------
通用数值格式:1,234,000.567
百分比数值格式:123,400,057%
货币数值格式:$1,234,000.57
*/
NumberFormat
其实有国际化的作用!同样的数值在不同国家的写法是不同的,而 NumberFormat
的作用就是把数值转换成不同国家的本地写法。
DateFormat
与 NumberFormat
相似的是,DateFormat
也是一个抽象类,它也提供了几个工厂方法用于获取 DateFormat
对象。
getDateInstance()
:返回一个日期格式器,它格式化后的字符串只有日期,没有时间。该方法可以传入多个参数,用于指定日期样式和 Locale 等参数;如果不指定这些参数,则使用默认参数。getTimeInstance()
:返回一个时间格式器,它格式化后的字符串只有时间,没有日期。该方法可以传入多个参数,用于指定时间样式和 Locale 等参数;如果不指定这些参数,则使用默认参数。getDateTimeInstance()
:返回一个日期、时间格式器,它格式化后的字符串既有日期,也有时间。该方法可以传入多个参数,用于指定日期样式、时间样式和 Locale 等参数;如果不指定这些参数,则使用默认参数。
上面 3 个方法可以指定日期样式、时间样式参数,它们是 DateFormat
的 4 个静态常量:FULL
、LONG
、MEDIUM
和 SHORT
,通过这 4 个样式参数可以控制生成的格式化字符串。
public class DateFormatTest {
public static void main(String[] args) {
// 需要被格式化的时间
Date dt = new Date();
// 创建两个Locale,分别代表中国、美国
Locale[] locales = {Locale.CHINA, Locale.US};
DateFormat[] df = new DateFormat[16];
// 为上面两个Locale创建16个DateFormat对象
for (int i = 0; i < locales.length; i++) {
df[i * 8] = DateFormat.getDateInstance(SHORT, locales[i]);
df[i * 8 + 1] = DateFormat.getDateInstance(MEDIUM, locales[i]);
df[i * 8 + 2] = DateFormat.getDateInstance(LONG, locales[i]);
df[i * 8 + 3] = DateFormat.getDateInstance(FULL, locales[i]);
df[i * 8 + 4] = DateFormat.getTimeInstance(SHORT, locales[i]);
df[i * 8 + 5] = DateFormat.getTimeInstance(MEDIUM, locales[i]);
df[i * 8 + 6] = DateFormat.getTimeInstance(LONG, locales[i]);
df[i * 8 + 7] = DateFormat.getTimeInstance(FULL, locales[i]);
}
for (int i = 0; i < locales.length; i++) {
switch (i) {
case 0:
System.out.println("-------中国日期格式--------");
break;
case 1:
System.out.println("-------美国日期格式--------");
break;
}
System.out.println("SHORT格式的日期格式:" + df[i * 8].format(dt));
System.out.println("MEDIUM格式的日期格式:" + df[i * 8 + 1].format(dt));
System.out.println("LONG格式的日期格式:" + df[i * 8 + 2].format(dt));
System.out.println("FULL格式的日期格式:" + df[i * 8 + 3].format(dt));
System.out.println("SHORT格式的时间格式:" + df[i * 8 + 4].format(dt));
System.out.println("MEDIUM格式的时间格式:" + df[i * 8 + 5].format(dt));
System.out.println("LONG格式的时间格式:" + df[i * 8 + 6].format(dt));
System.out.println("FULL格式的时间格式:" + df[i * 8 + 7].format(dt));
}
}
}
/*
-------中国日期格式--------
SHORT格式的日期格式:22-8-9
MEDIUM格式的日期格式:2022-8-9
LONG格式的日期格式:2022年8月9日
FULL格式的日期格式:2022年8月9日 星期二
SHORT格式的时间格式:下午7:01
MEDIUM格式的时间格式:19:01:53
LONG格式的时间格式:下午07时01分53秒
FULL格式的时间格式:下午07时01分53秒 CST
-------美国日期格式--------
SHORT格式的日期格式:8/9/22
MEDIUM格式的日期格式:Aug 9, 2022
LONG格式的日期格式:August 9, 2022
FULL格式的日期格式:Tuesday, August 9, 2022
SHORT格式的时间格式:7:01 PM
MEDIUM格式的时间格式:7:01:53 PM
LONG格式的时间格式:7:01:53 PM CST
FULL格式的时间格式:7:01:53 PM CST
*/
相关信息
获得了 DateFormat
之后,还可以调用它的 setLenient(boolean lenient)
方法来设置该格式器是否采用严格语法。
举例来说,如果采用不严格的日期语法(该方法的参数为 true
),对于字符串 "2004-2-31"
将会转换成 2004 年 3 月 2 日;如果采用严格的日期语法,解析该字符串时将抛出异常。
DateFormat
的 parse()
方法可以把一个字符串解析成 Date
对象,但它要求被解析的字符串必须符合日期字符串的要求,否则可能抛出 ParseException
异常。
public static void main(String[] args) throws ParseException {
String str1 = "2007-12-12";
String str2 = "2007年12月10日";
// 下面输出 Wed Dec 12 00:00:00 CST 2007
System.out.println(DateFormat.getDateInstance().parse(str1));
// 下面输出 Mon Dec 10 00:00:00 CST 2007
System.out.println(DateFormat.getDateInstance(LONG).parse(str2));
// 下面抛出 ParseException异常
System.out.println(DateFormat.getDateInstance().parse(str2));
}
SimpleDateFormat
DateFormat
的 parse()
方法可以把字符串解析成 Date
对象,但实际上 DataFormat
的 format()
方法不够灵活——它要求被解析的字符串必须满足特定的格式!
为了更好地格式化日期、解析日期字符串,Java 提供了 SimpleDateFormat
类。
SimpleDateFormat
是 DateFormat
的子类,正如它的名字所暗示的,它是“简单”的日期格式器。SimpleDateFormat
比 DateFormat
更简单,功能更强大。
SimpleDateFormat
可以非常灵活地格式化 Date
,也可以用于解析各种格式的日期字符串。
创建 SimpleDateFormat
对象时需要传入一个 pattern
字符串,这个 pattern
不是正则表达式,而是一个日期模板字符串。
public class SimpleDateFormatTest {
public static void main(String[] args) throws ParseException {
Date d = new Date();
// 创建一个SimpleDateFormat对象
SimpleDateFormat sdf1 = new SimpleDateFormat("G yyyy年中第D天");
// 将d格式化成日期,输出:公元2007年中第354天
String dateStr = sdf1.format(d);
System.out.println(dateStr);
// 一个非常特殊的日期字符串
String str = "07###三月##21";
SimpleDateFormat sdf2 = new SimpleDateFormat("y###MMM##d");
// 将日期字符串解析成日期,输出:Wed Mar 21 00:00:00 CST 2007
System.out.println(sdf2.parse(str));
}
}
使用 SimpleDateFormat
可以将日期格式化成形如 "公元 2007 年中第 354 天"
这样的字符串,也可以把形如 "07###三月##21"
这样的字符串解析成日期,功能非常强大。
SimpleDateFormat
把日期格式化成怎样的字符串,以及能把怎样的字符串解析成 Date
,完全取决于创建该对象时指定的 pattern
参数,pattern
是一个使用日期字段占位符的日期模板。