11. 与运行环境交互

空~2022年7月12日
  • java
大约 59 分钟

11. 与运行环境交互

Java 提供了 StringStringBufferStringBuilder 来处理字符串。Java 还提供了 DateCalendar 来处理日期、时间,其中 Date 是一个已经过时的 API,通常推荐使用 Calendar 来处理日期、时间。

正则表达式是一个强大的文本处理工具,通过正则表达式可以对文本内容进行查找、替换、分割等操作。从 JDK 1.4 以后,Java 也增加了对正则表达式的支持,包括新增的 PatternMatcher 两个类,并改写了 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

运行结果:

image-20220803175758216

如果运行 Java 程序时在类名后紧跟一个或多个字符串(多个字符串之间以空格隔开),JVM 就会把这些字符串依次赋给 args 数组元素。

img

如果某参数本身包含了空格,则应该将该参数用双引号 "" 括起来,否则 JVM 会把这个空格当成参数分隔符,而不是当成参数本身。

java ArgsTest "Java Spring"

image-20220803180015126

Scanner

使用 Scanner 类可以很方便地获取用户的键盘输入,Scanner 是一个基于正则表达式的文本扫描器,它可以从文件、输入流、字符串中解析出基本类型值和字符串值。

Scanner 类提供了多个构造器,不同的构造器可以接收文件、输入流、字符串作为数据源,用于从文件、输入流、字符串中解析数据。

Scanner 主要提供了两个方法来扫描输入。

  1. hasNextXxx():是否还有下一个输入项,其中 Xxx 可以是 IntLong 等代表基本数据类型的字符串。如果需要判断是否包含下一个字符串,则可以省略 Xxx
  2. 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 不断从键盘读取键盘输入,每次读到键盘输入后,直接将输入内容打印在控制台。

image-20220803180458463

如果希望改变 Scanner 的分隔符(不使用空白作为分隔符),例如,程序需要每次读取一行,不管这一行中是否包含空格,Scanner 都把它当成一个输入项。在这种需求下,我们可以把 Scanner 的分隔符设置为回车符,不再使用默认的空白作为分隔符。

Scanner 的读取操作可能被阻塞(当前执行顺序流暂停)来等待信息的输入。如果输入源没有结束, Scanner 又读不到更多输入项时(尤其在键盘输入时比较常见),ScannerhasNext()next() 方法都有可能阻塞,hasNext() 方法是否阻塞与和其相关的 next() 方法是否阻塞无关。

Scanner 设置分隔符使用 useDelimiter(String pattern) 方法即可,该方法的参数应该是一个正则表达式。只要把上面程序中粗体字代码行的注释去掉,该程序就会把键盘的每行输入当成一个输入项,不会以空格、Tab 空白等作为分隔符。

Scanner 提供了两个简单的方法来逐行读取。

  1. boolean hasNextLine():返回输入源中是否还有下一行。
  2. 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());
        }
    }
}

image-20220803182945675

提示

因为上面程序涉及文件输入,可能引发文件 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 系统属性的值。

image-20220804165126821

该程序运行结束后还会在当前路径下生成一个 props.txt 文件,该文件中记录了当前平台的所有系统属性。

image-20220804165714939

System 类提供了通知系统进行垃圾回收的 gc() 方法,以及通知系统进行资源清理的 runFinalization() 方法。

System 类还有两个获取系统当前时间的方法:currentTimeMillis()nanoTime(),它们都返回一个 long 型整数。

实际上它们都返回当前时间与 UTC 1970 年 1 月 1 日午夜的时间差,前者以毫秒作为测量单位,后者以纳秒作为测量单位。

这两个方法的返回值的粒度取决于底层操作系统,可能所在的操作系统根本不支持以毫秒、纳秒作为计时单位。

除此之外,System 类的 inouterr 分别代表系统的标准输入(通常是键盘)、标准输出(通常是显示器)和错误输出流,并提供了 setInsetOutsetErr 方法来改变系统的标准输入、标准输出和标准错误输出流。

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 类提供了如下几个常用方法。

  1. boolean equals(Object obj):判断指定对象与该对象是否相等。此处相等的标准是,两个对象是同一个对象,因此该 equals() 方法通常没有太大的实用价值。
  2. protected void finalize():当系统中没有引用变量引用到该对象时,垃圾回收器调用此方法来清理该对象的资源。
  3. Class<?> getClass():返回该对象的运行时类。
  4. int hashCode():返回该对象的 hashCode 值。在默认情况下,Object 类的 hashCode() 方法根据该对象的地址来计算(即与 System.identityHashCode(Object x) 方法的计算结果相同)。但很多类都重写了 Object 类的 hashCode() 方法,不再根据地址来计算其 hashCode() 方法值。
  5. String toString():返回该对象的字符串表示,当我们使用 System.out.println() 方法输出一个对象,或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的 toString() 方法返回该对象的字符串表示。Object 类的 toString() 方法返回 运行时类名@十六进制 hashCode 值 格式的字符串,但很多类都重写了 Object 类的 toString() 方法,用于返回可以表述该对象信息的字符串。

除此之外,Object 类还提供了 wait()notify()notifyAll() 几个方法,通过这几个方法可以控制线程的暂停和运行。

Java 还提供了一个 protected 修饰的 clone() 方法,该方法用于帮助其他对象来实现“自我克隆”,所谓“自我克隆”就是得到一个当前对象的副本,而且二者之间完全隔离。由于 Object 类提供的 clone() 方法使用了 protected 修饰,因此该方法只能被子类重写或调用。

自定义类实现“克隆”的步骤如下。

  1. 自定义类实现 Cloneable 接口。这是一个标记性的接口,实现该接口的对象可以实现“自我克隆”,接口里没有定义任何方法。
  2. 自定义类实现自己的 clone() 方法。
  3. 实现 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 机制只对对象里各实例变量进行“简单复制”,如果实例变量的类型是引用类型,ObjectClone 机制也只是简单地复制这个引用变量,这样原有对象的引用类型的实例变量与克隆对象的引用类型的实例变量依然指向内存中的同一个实例,所以上面程序在 ② 号代码处输出 true。上面程序“克隆”出来的 u1u2 所指向的对象在内存中的存储示意图如图所示。

img

Object 类提供的 clone() 方法不仅能简单地处理“复制”对象的问题,而且这种“自我克隆”机制十分高效。比如 clone 一个包含 100 个元素的 int[] 数组,用系统默认的 clone 方法比静态 copy 方法快近 2 倍。

Object 类的 clone() 方法虽然简单、易用,但它只是一种“浅克隆”——它只克隆该对象的所有 Field 值,不会对引用类型的 Field 值所引用的对象进行克隆。如果开发者需要对对象进行“深克隆”,则需要开发者自己进行“递归”克隆,保证所有引用类型的 Field 值所引用的对象都被复制了。

Objects

Java 7 新增了一个 Objects 工具类,它提供了一些工具方法来操作对象,这些工具方法大多是“空指针”安全的。比如,你不能明确地判断一个引用变量是否为 null,如果贸然地调用该变量的 toString() 方法,则可能引发 NullPointerExcetpion 异常;但如果使用 Objects 类提供的 toString(Object o) 方法,就不会引发空指针异常,当 onull 时,程序将返回一个 "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 提供了 StringStringBuffer 两个类来封装字符串,并提供了一系列方法来操作字符串对象。

String 类是不可变类,即一旦一个 String 对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。

StringBuffer 对象则代表一个字符序列可变的字符串,当一个 StringBuffer 被创建以后,通过 StringBuffer 提供的 append()insert()reverse()setCharAt()setLength() 等方法可以改变这个字符串对象的字符序列。

一旦通过 StringBuffer 生成了最终想要的字符串,就可以调用它的 toString() 方法将其转换为一个 String 对象。

从 JDK 1.5 开始出现的 StringBuilder 类,也代表字符串对象。实际上,StringBuilderStringBuffer 基本相似,两个类的构造器和方法也基本相同。

不同的是,StringBuffer 是线程安全的,而 StringBuilder 则没有实现线程安全功能,所以性能略高。因此在通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑使用 StringBuilder 类。

String 类提供了大量构造器来创建 String 对象,其中如下几个有特殊用途。

  1. String():创建一个包含 0 个字符串序列的 String 对象(并不是返回 null)。
  2. String(byte[] bytes, Charset charset):使用指定的字符集将指定的 byte[] 数组解码成一个新的 String 对象。
  3. String(byte[] bytes, int offset, int length):使用平台的默认字符集将从指定 byte[] 数组的 offset 开始、长度为 length 的子数组解码成一个新的 String 对象。
  4. String(byte[] bytes, int offset, int length, String charsetName):使用指定的字符集将指定的 byte[] 数组从 offset 开始、长度为 length 的子数组解码成一个新的 String 对象。
  5. String(byte[] bytes, String charsetName):使用指定的字符集将指定的 byte[] 数组解码成一个新的 String 对象。
  6. String(char[] value, int offset, int count):将指定的字符数组从 offset 开始、长度为 count 的字符元素连缀成字符串。
  7. String(String original):根据字符串直接量来创建一个 String 对象。也就是说,新创建的 String 对象是该参数字符串的副本。
  8. String(StringBuffer buffer):根据 StringBuffer 对象来创建对应的 String 对象。
  9. 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 是不可变的,所以会额外产生很多临时变量,使用 StringBufferStringBuilder 就可以避免这个问题。

StringBuilder 提供了一系列插入、追加、改变该字符串里包含的字符序列的方法。

StringBuffer 与其用法完全相同,只是 StringBuffer 是线程安全的。

StringBuilderStringBuffer 有两个属性:lengthcapacity,其中 length 属性表示其包含的字符序列的长度。

String 对象的 length 不同的是,StringBuilderStringBufferlength 是可以改变的,可以通过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 里的字符序列,这就是 StringBuilderString 之间最大的区别:StringBuilder 的字符序列是可变的。从程序看到 StringBuilderlength() 方法返回其字符序列的长度,而 capacity() 返回值则比 length() 返回值大。

Math

Java 提供了基本的+、-、*、/、%等基本算术运算的运算符,但对于更复杂的数学运算,例如,三角函数、对数运算、指数运算等则无能为力。

Java 提供了 Math 工具类来完成这些复杂的运算,Math 类是一个工具类,它的构造器被定义成 private 的,因此无法创建 Math 类的对象;Math 类中的所有方法都是类方法,可以直接通过类名来调用它们。

Math 类除了提供了大量静态方法之外,还提供了两个静态 Field:PIE,它们的值分别等于 π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() 方法来获取伪随机数了。

ThreadLocalRandomRandom 都比 Mathrandom() 方法提供了更多的方式来生成各种伪随机数,可以生成浮点类型的伪随机数,也可以生成整数类型的伪随机数,还可以指定生成随机数的范围。

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());
    }
}

image-20220804234505441

如果两个 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

前面在介绍 floatdouble 两种基本浮点类型时已经指出,这两个基本类型的浮点数容易引起精度丢失。

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));
    }
}

image-20220804234925449

不仅是 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));
    }
}

image-20220804235226231

从上面运行结果可以看出 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));
    }
}

image-20220804235504721

处理日期的类

Java 还提供了一系列用于处理日期、时间的类,包括创建日期、时间对象,获取系统当前日期、时间等操作。

Date

此处的 Date 是指 java.util 包下的 Date 类,而不是 java.sql 包下的 Date 类,Date 对象既包含日期,也包含时间。

Date 类提供了 6 个构造器,其中 4 个已经 Deprecated(Java 不再推荐使用,使用不再推荐的构造器时编译器会提出警告信息,并导致程序性能、安全性等方面的问题),剩下的两个构造器如下所示。

  1. Date():生成一个代表当前日期时间的 Date 对象。该构造器在底层调用 System.currentTimeMillis() 获得 long 整数作为日期参数。
  2. Date(long date):根据指定的 long 型整数来生成一个 Date 对象。该构造器的参数表示创建的 Date 对象和 GMT 1970 年 1 月 1 日 00:00:00 之间的时间差,以毫秒作为计时单位。

Date 对象的大部分方法也 Deprecated 了,剩下为数不多的几个方法。

  1. boolean after(Date when):测试该日期是否在指定日期 when 之后。
  2. boolean before(Date when):测试该日期是否在指定日期 when 之前。
  3. int compareTo(Date anotherDate):比较两个日期的大小,后面的时间大于前面的时间时返回-1,否则返回 1。
  4. boolean equals(Object obj):当两个时间表示同一时刻时返回 true
  5. long getTime():返回该时间对应的 long 型整数,即从 GMT 1970-01-0100:00:00 到该 Date 对象之间的时间差,以毫秒作为计时单位。
  6. 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 站点open in new window上,IBM 的开发人员实现了多种日历。在 Internet 上,也有对中国农历的实现。

Calendar 类是一个抽象类,所以不能使用构造器来创建 Calendar 对象。但它提供了几个静态 getInstance() 方法来获取 Calendar 对象,这些方法根据 TimeZoneLocale 类来获取特定的 Calendar,如果不指定 TimeZoneLocale,则使用默认的 TimeZoneLocale 来创建 Calendar

CalendarDate 都是表示日期的工具类,它们直接可以自由转换,如下代码所示。

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 类提供了大量访问、修改日期时间的方法,常用方法如下。

  1. void add(int field, int amount):根据日历的规则,为给定的日历字段添加或减去指定的时间量。
  2. int get(int field):返回指定日历字段的值。
  3. int getActualMaximum(int field):返回指定日历字段可能拥有的最大值。例如月,最大值为 11。
  4. int getActualMinimum(int field):返回指定日历字段可能拥有的最小值。例如月,最小值为 0。
  5. void roll(int field, int amount):与 add()方法类似,区别在于加上 amount 后超过了该字段所能表示的最大范围时,也不会向上一个字段进位。
  6. void set(int field, int value):将给定的日历字段设置为给定值。
  7. void set(int year, int month, int date):设置 Calendar 对象的年、月、日 3 个字段的值。
  8. void set(int year, int month, int date, int hourOfDay, int minute, int second):设置 Calendar 对象的年、月、日、时、分、秒 6 个字段的值。

上面的很多方法都需要一个 int 类型的 field 参数,field 是 Calendar 类的静态 Field,如 Calendar.YEARCalendar.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 类的 YEARMONTHDATE 等静态 Field。

Calendar 类还有如下几个注意点。

addroll 的区别:

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。

如果程序将 ① 处代码注释起来,因为 Calendarset() 方法具有延迟修改的特性,即调用 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 类提供了一些有用的方法用于获取时区的相关信息。

  1. static String[] getAvailableIDs():获取 Java 所支持的所有时区 ID
  2. static TimeZone getDefault():获取运行机器上默认的时区。
  3. String getDisplayName():获取该 TimeZone 对象的时区名称。
  4. String getID():获取该时区的 ID
  5. 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 类里也提供了如下几个特殊的方法。

  1. boolean matches(String regex):判断该字符串是否匹配指定的正则表达式。
  2. String replaceAll(String regex, String replacement):将该字符串中所有匹配 regex 的子串替换成 replacement
  3. String replaceFirst(String regex, String replacement):将该字符串中第一个匹配 regex 的子串替换成 replacement
  4. String[] split(String regex):以 regex 作为分隔符,把该字符串分割成多个子串。

上面这些特殊的方法都依赖于 Java 提供的正则表达式支持,除此之外,Java 还提供了 PatternMatcher 两个类专门用于提供正则表达式支持。

正则表达式是一个用于匹配字符串的模板。可以说,我们定义的任意字符串都可以当成正则表达式使用,例如 "abc",它也是一个正则表达式,只是它只能匹配 "abc" 字符串。

创建正则表达式

正则表达式所支持的合法字符。

img

除此之外,正则表达式中有一些特殊字符,这些特殊字符在正则表达式中有其特殊的用途,比如前面介绍的反斜线 \。如果需要匹配这些特殊字符,就必须首先将这些字符转义,也就是在前面添加一个反斜线 \

正则表达式中的特殊字符如表。

img

将上面多个字符拼起来,就可以创建一个正则表达式。例如:

"\\u0041\\"  // 匹配a\
"\\0101\t"   // 匹配a<制表符>
"\?\["   // 匹配?[

上面的正则表达式依然只能匹配单个字符,这是因为还未在正则表达式中使用“通配符”,“通配符”是可以匹配多个字符的特殊字符。

正则表达式中的“通配符”远远超出了普通通配符的功能,它被称为预定义字符,正则表达式支持如表。

img

提示

上面的 7 个预定义字符其实很容易记忆:

ddigit 的意思,代表数字;

sspace 的意思,代表空白;

wword 的意思,代表单词。

dsw 的大写形式恰好匹配与之相反的字符。

有了上面的预定义字符后,我们就可以创建更强大的正则表达式。例如:

c\wt  // 可以匹配cat、cbt、cct、c0t、c9t等一批字符串
\d\d\d-\d\d\d-\d\d\d\d  // 匹配如000-000-0000形式的电话号码

在一些特殊情况下,例如,若只想匹配 a ~ f 的字母,或者匹配除了 ab 之外的所有小写字母,或者匹配中文字符,上面这些预定义字符就无能为力了,此时就需要使用方括号表达式,方括号表达式有如表。

img

提示

方括号表达式比前面的预定义字符灵活多了,几乎可以匹配任何字符。

例如,若需要匹配所有的中文字符,就可以利用 [\u0041-\u0056] 形式——因为所有中文字符的 Unicode 值是连续的,只要找出所有中文字符中最小、最大的 Unicode 值,就可以利用上面形式来匹配所有的中文字符。

正则表示还支持圆括号表达式,用于将多个表达式组成一个子表达式,圆括号中可以使用或运算符 |。例如,正则表达式 "(public|protected|private)" 用于匹配 Java 的三个访问控制符其中之一。

除此之外,Java 正则表达式还支持如表所示的几个边界匹配符。

img

前面例子中需要建立一个匹配 000-000-0000 形式的电话号码时,我们使用了 \d\d\d-\d\d\d-\d\d\d\d 正则表达式,这看起来比较烦琐。

实际上,正则表达式还提供了数量标识符,正则表达式支持的数量标识符有如下几种模式。

  1. Greedy(贪婪模式):数量表示符默认采用贪婪模式,除非另有表示。贪婪模式的表达式会一直匹配下去,直到无法匹配为止。如果你发现表达式匹配的结果与预期的不符,很有可能是因为——你以为表达式只会匹配前面几个字符,而实际上它是贪婪模式,所以会一直匹配下去。
  2. Reluctant(勉强模式):用问号后缀 ? 表示,它只会匹配最少的字符。也称为最小匹配模式。
  3. Possessive(占有模式):用加号后缀 + 表示,目前只有 Java 支持占有模式,通常比较少用。

三种模式的数量表示符如表所示。

img

关于贪婪模式和勉强模式的对比,看如下代码:

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!"

使用正则表达式

一旦在程序中定义了正则表达式,就可以使用 PatternMatcher 来使用正则表达式。

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 类提供了如下几个常用方法。

  1. find():返回目标字符串中是否包含与 Pattern 匹配的子串。
  2. group():返回上一次与 Pattern 匹配的子串。
  3. start():返回上一次与 Pattern 匹配的子串在目标字符串中的开始位置。
  4. end():返回上一次与 Pattern 匹配的子串在目标字符串中的结束位置加 1。
  5. lookingAt():返回目标字符串前面部分与 Pattern 是否匹配。
  6. matches():返回整个目标字符串与 Pattern 是否匹配。
  7. reset(),将现有的 Matcher 对象应用于一个新的字符序列。

相关信息

PatternMatcher 类的介绍中经常会看到一个 CharSequence 接口,该接口代表一个字符序列,其中 CharBufferStringStringBufferStringBuilder 都是它的实现类。简单地说,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 开头就会返回 truereset() 方法可将现有的 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 与多个邮件地址进行匹配。当程序中的 Matchernull 时,程序调用 matcher() 方法来创建一个 Matcher 对象,一旦 Matcher 对象被创建,程序就调用 Matcherreset() 方法将该 Matcher 应用于新的字符序列。

相关信息

从某个角度来看,Matchermatches()lookingAt()String 类的 equals()startsWith() 有点相似。

区别是 String 类的 equals()startsWith() 都是与字符串进行比较,而 Matchermatches()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 抽象类还有两个子类:

NumberFormatDateFormat,它们分别用以实现数值、日期的格式化。

NumberFormatDateFormat 可以将数值、日期转换成字符串,也可以将字符串转换成数值、日期。

img

NumberFormatDateFormat 都包含了 format()parse() 方法,其中 format() 用于将数值、日期格式化成字符串,parse() 用于将字符串解析成数值、日期。

NumberFormat 也是一个抽象基类,所以无法通过它的构造器来创建 NumberFormat 对象,它提供了如下几个工厂方法来得到 NumberFormat 对象。

  1. getCurrencyInstance():返回默认 Locale 的货币格式器。也可以在调用该方法时传入指定的 Locale,则获取指定 Locale 的货币格式器。
  2. getIntegerInstance():返回默认 Locale 的整数格式器。也可以在调用该方法时传入指定的 Locale,则获取指定 Locale 的整数格式器。
  3. getNumberInstance():返回默认 Locale 的通用数值格式器。也可以在调用该方法时传入指定的 Locale,则获取指定 Locale 的通用数值格式器。
  4. 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 对象。

  1. getDateInstance():返回一个日期格式器,它格式化后的字符串只有日期,没有时间。该方法可以传入多个参数,用于指定日期样式和 Locale 等参数;如果不指定这些参数,则使用默认参数。
  2. getTimeInstance():返回一个时间格式器,它格式化后的字符串只有时间,没有日期。该方法可以传入多个参数,用于指定时间样式和 Locale 等参数;如果不指定这些参数,则使用默认参数。
  3. getDateTimeInstance():返回一个日期、时间格式器,它格式化后的字符串既有日期,也有时间。该方法可以传入多个参数,用于指定日期样式、时间样式和 Locale 等参数;如果不指定这些参数,则使用默认参数。

上面 3 个方法可以指定日期样式、时间样式参数,它们是 DateFormat 的 4 个静态常量:FULLLONGMEDIUMSHORT,通过这 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 日;如果采用严格的日期语法,解析该字符串时将抛出异常。

DateFormatparse() 方法可以把一个字符串解析成 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

DateFormatparse() 方法可以把字符串解析成 Date 对象,但实际上 DataFormatformat() 方法不够灵活——它要求被解析的字符串必须满足特定的格式!

为了更好地格式化日期、解析日期字符串,Java 提供了 SimpleDateFormat 类。

SimpleDateFormatDateFormat 的子类,正如它的名字所暗示的,它是“简单”的日期格式器。SimpleDateFormatDateFormat 更简单,功能更强大。

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 是一个使用日期字段占位符的日期模板。