1. 文章
  2. Java 进阶线
  3. 数据处理与集合

常用 API:字符串、精确金额与日期时间

更新于 10 分钟阅读 第 4 / 15 站 #java#java-advanced#day04

用促销金额、秒杀倒计时和轮休日历理解常用 API 的边界。

我的学习 →

Java 进阶线 · 线路目录
本页导读

day04|java.lang、java.util 包下的常用 API

本日围绕「字符串怎么高效拼、金额怎么算准、日期怎么处理」三类最常见的 API 展开:StringBuilder / StringBuffer / StringJoiner、Math / System / Runtime、BigDecimal,以及 JDK 8 之前的 Date / SimpleDateFormat / Calendar 与 JDK 8 新增的 java.time 系列。学完后应能回答:为什么频繁拼接要用 StringBuilder,为什么金额不能用 double 算,为什么有了 Date 还要学 LocalDateTime。

本日 PPT 目录(第 2 页):StringBuilder、StringBuffer → StringJoiner → Math、System、Runtime → BigDecimal → JDK8 之前传统的日期、时间 → JDK8 开始新增的日期、时间。

两点说明(供复习时核对):

  • 课件第 1 页标题是「常用API(二)」,目录中不含 Object / Objects、包装类与自动装箱拆箱、String 的常用方法、Arrays 等内容,这些不在本日 PPT 范围内(后续课程安排)。
  • 课件里大多数「常见方法」页在抽取文本中只留下 [表格] 占位,没有表体。本文按课件的题目页、配套示例代码 day04-api-code 与 JDK API 还原这些表格,还原处已用「(表格按课件题目与配套代码整理)」标注。

1. StringBuilder 与 StringBuffer

1.1 为什么需要 StringBuilder

StringBuilder 代表可变字符串对象,相当于一个容器,里面装的字符串内容是可以改变的,就是用来操作字符串的。好处是:StringBuilder 比 String 更适合做字符串的修改操作,效率更高,代码也更简洁。

原因在第 6 页的小结里说得很明确:

  • String 是不可变字符串,频繁操作字符串会产生很多无用对象,性能差;
  • StringBuilder 是内容可变的字符串,拼接字符串性能好、代码优雅。

注意(课件原话):如果操作字符串较少,或者不需要操作,以及定义字符串变量时,还是建议用 String。

1.2 性能对比演示

课件用一段「拼接 100 万次」的代码对比了两种写法(第 4 页,同样的实验在第 9 页又出现了一次):

// 写法一:用 String 拼接
public class Test {
    public static void main(String[] args) {
        String s = "";
        for (int i = 0; i < 1000000; i++) {
            s = s + "abc";
        }
        System.out.println(s);
    }
}
// 写法二:用 StringBuilder 拼接
public class Test {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 1000000; i++) {
            sb.append("abc");
        }
        System.out.println(sb);
    }
}

课件对两种写法的评价是:第一种「速度忒慢」,第二种「速度快了」。第 4 页的提问「为啥操作字符串建议使用 StringBuilder,而不用原来学过的 String?」答案就是 1.1 中的不可变性与无用对象问题。

1.3 StringBuilder 的常见方法

(表格按课件题目与配套代码整理)

方法说明
public StringBuilder()创建内容为空的可变字符串容器
public StringBuilder(String str)用指定内容创建
public StringBuilder append(任意类型)追加数据,返回容器本身,可链式调用
public StringBuilder reverse()反转内容
public int length()返回内容的长度
public String toString()把容器内容转回 String

配套代码 d1_stringbuilder/StringBuilderDemo1.java 把上述方法串了一遍:

public class StringBuilderDemo1 {
    public static void main(String[] args) {
        // 1、创建 StringBuilder 对象
        StringBuilder sb = new StringBuilder();      // sb = ""
        StringBuilder sb2 = new StringBuilder("黑马"); // sb2 = "黑马"

        // 2、拼接内容
        sb.append("深圳黑马").append("Java").append(666).append(true);

        // 3、反转内容
        sb.reverse();

        // 4、拿长度
        System.out.println(sb.length());

        // 5、把 StringBuilder 对象转换成 String 对象
        // StringBuilder 是拼接字符串的手段,String 才是开发中的目的
        String result = sb.toString();
        System.out.println(result);
    }
}

1.4 StringBuffer 与 StringBuilder 的区别

第 5 页给出结论:

对比项StringBuilderStringBuffer
用法完全一样(方法名、参数都一样)完全一样
线程安全线程不安全线程安全
使用建议单线程、线程私有的局部变量拼接需要多线程共享同一个容器时使用

1.5 案例:返回任意整型数组的内容

需求(第 7 页):设计一个方法,用于返回任意整型数组的内容,要求返回的数组内容格式如 [11, 22, 33]。

分析过程(课件):

  1. 方法是否需要接收数据?——需要接收整型数组;
  2. 方法是否需要返回数据?——需要返回拼接后的结果;
  3. 方法内部:遍历数组的数据,把遍历到的数据都拼接起来,此时使用 StringBuilder 来完成拼接。
public class StringBuilderTest3 {
    public static void main(String[] args) {
        int[] arr = {11, 44, 33, 66};
        System.out.println(getArrayData(arr));   // [11, 44, 33, 66]
    }

    public static String getArrayData(int[] arr) {
        // 1、判断 arr 是否为 null
        if (arr == null) {
            return null;
        }
        // 2、创建 StringBuilder 对象
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        // 3、遍历数组中的内容
        for (int i = 0; i < arr.length; i++) {
            int data = arr[i];
            sb.append(data).append(i == arr.length - 1 ? "" : ", ");
        }
        sb.append("]");
        return sb.toString();
    }
}

课件第 9 页的写法是用 if (i == arr.length - 1) 分支处理最后一个元素不加分隔符,配套代码则用三元表达式把这两行合成一行,两种写法结果一致。

2. StringJoiner

2.1 为什么还要学 StringJoiner

第 9 页末尾提出的问题是:「既能高效,又能实现更方便的拼接?」——StringBuilder 已经很快,但拼接 [11, 22, 33] 这种带首尾符号、元素之间还要加分隔符的场景,需要自己写 if 判断最后一个元素,代码不够简洁。

StringJoiner(第 10 页):JDK8 开始才有的,跟 StringBuilder 一样,也是用来操作字符串的,也可以看成是一个容器,创建之后里面的内容是可变的。好处是:不仅能提高字符串的操作效率,并且在有些场景下使用它操作字符串,代码会更简洁。

2.2 构造器与常用方法

(表格按课件题目与配套代码整理,两类构造器的区别就是「有没有首尾符号」)

构造器说明
public StringJoiner(CharSequence delimiter)只用间隔符,如 ,
public StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix)间隔符 + 开始符号 + 结束符号
方法说明
public StringJoiner add(CharSequence newElement)添加元素,返回容器本身,可链式调用
public int length()返回内容的字符数
public String toString()返回拼接好的字符串(包含首尾符号)

2.3 案例:用 StringJoiner 重写 1.5

配套代码 d2_stringjoiner/StringJoinerTest1.java:

import java.util.StringJoiner;

public class StringJoinerTest1 {
    public static void main(String[] args) {
        int[] arr = {11, 44, 33, 66};
        System.out.println(getArrayData(arr));   // [11, 44, 33, 66]
    }

    public static String getArrayData(int[] arr) {
        if (arr == null) {
            return null;
        }
        // 参数一:间隔符  参数二:开始符号  参数三:结束符号
        StringJoiner sb = new StringJoiner(", ", "[", "]");
        for (int i = 0; i < arr.length; i++) {
            sb.add(Integer.toString(arr[i]));
        }
        return sb.toString();
    }
}

对比 1.5 的 StringBuilder 版本,这里不再需要判断「是不是最后一个元素」,分隔符与首尾符号都由 StringJoiner 负责。

3. Math、System、Runtime

3.1 Math

Math 代表数学,是一个工具类,里面提供的都是对数据进行操作的一些静态方法。

(表格按课件题目与配套代码整理)

方法说明(含配套代码的运行结果)
public static int abs(int a) / double abs(double a)取绝对值,结果一定是正数。Math.abs(-12) → 12
public static double ceil(double a)向上取整。Math.ceil(4.0000001) → 5.0,Math.ceil(4.0) → 4.0
public static double floor(double a)向下取整。Math.floor(4.999999) → 4.0
public static long round(double a)四舍五入。Math.round(3.4999) → 3,Math.round(3.50001) → 4
public static int max(int a, int b) / min(int a, int b)取较大值 / 较小值
public static double pow(double a, double b)取次方。Math.pow(2, 3) → 8.0
public static double random()取随机数,范围 [0.0, 1.0)(包前不包后)

3.2 System

System 代表程序所在的系统,也是一个工具类。

(表格按课件题目与配套代码整理)

方法说明
public static void exit(int status)终止当前运行的 Java 虚拟机;按照惯例非零状态码表示异常终止,0 表示正常终止
public static long currentTimeMillis()返回从 1970-01-01 00:00:00 走到此刻的总毫秒值(1s = 1000ms)

配套代码 d3_math/SystemTest.java 用两个毫秒值之差做了一次性能统计:

long time = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
    System.out.println(i);
}
long time2 = System.currentTimeMillis();
System.out.println((time2 - time) / 1000.0 + "s");

(补充说明:配套代码中 exit(0) 被注释并标注「禁止使用」;另外测「经过时间」更常用 System.nanoTime(),课件未涉及。)

3.3 时间毫秒值与时区起点

第 15 页专门解释了时间毫秒值:指的是从 1970 年 1 月 1 日 00:00:00 走到此刻的总的毫秒数,应该是很大的(1s = 1000ms)。

为什么选择「1970 年 1 月 1 日 00:00:00」作为时间的起点?课件的说法是:1969 年 8 月,贝尔实验室的程序员肯·汤普逊(Ken Thompson)利用妻儿离开一个月的机会,开始着手创造一个全新的革命性的操作系统,他使用 B 编译语言在老旧的 PDP-7 机器上开发出了 Unix 的一个版本;随后汤普逊和同事丹尼斯·里奇(Dennis Ritchie)改进了 B 语言,开发出了 C 语言,重写了 UNIX。1970 年 1 月 1 日算 C 语言的生日,这个起点就沿用下来了。

作业里的相关追问是「那我们中国也是这个时间么」:起点本身是同一个 UTC 时刻,中国标准时间为世界标准时间 + 8 小时,所以这个瞬间在中国显示为 1970-01-01 08:00:00;但「毫秒值」这个数值是全球统一的,换时区只影响显示(详见 6.3 的时区部分)。

3.4 Runtime

Runtime 代表程序所在的运行环境,并且是一个单例类:对象不通过 new 创建,而是用 getRuntime() 获取。

(表格按课件题目与配套代码整理)

方法说明
public static Runtime getRuntime()返回与当前 Java 应用程序关联的运行时对象
public void exit(int status)终止当前运行的虚拟机
public int availableProcessors()获取虚拟机能够使用的处理器数
public long totalMemory()返回 JVM 中的内存总量(字节数)
public long freeMemory()返回 JVM 中的可用内存量(字节数)
public Process exec(String command)执行命令、启动程序,返回代表该程序的 Process 对象
Runtime jre = Runtime.getRuntime();
System.out.println("处理器数量:" + jre.availableProcessors());
System.out.println("Java虚拟机中的内存总量:" + jre.totalMemory() / 1024.0 / 1024.0 + "MB");
System.out.println("Java虚拟机中的可用内存量:" + jre.freeMemory() / 1024.0 / 1024.0 + "MB");

Process process = jre.exec("QQ");   // 返回的 Process 对象代表启动的 QQ 软件
// ... 需要时调用 process.destroy() 关闭该进程

4. BigDecimal

4.1 浮点型运算失真的问题

BigDecimal 用于解决浮点型运算时出现结果失真的问题。第 18 页的演示代码:

public static void main(String[] args) {
    // 浮点型运算时,直接 + - * / 可能会出现运算结果失真
    System.out.println(0.1 + 0.2);
    System.out.println(1.0 - 0.32);
    System.out.println(1.015 * 100);
    System.out.println(1.301 / 100);
}

浮点型运算失真的实际输出结果

对应输出为 0.30000000000000004、0.6799999999999999、101.49999999999999、0.013009999999999999——所以金额计算不能直接用 double。

4.2 构造器与常用方法

(表格按课件题目与配套代码整理;课件第 19 页单独标出了 BigDecimal(double val) 这个构造器)

构造器说明
public BigDecimal(String val)推荐:按字符串描述构造,能避免二进制浮点的近似细节
public BigDecimal(double val)课件列出的构造器,但传入 double 会带上近似值,一般不用于金额
public static BigDecimal valueOf(double val)更推荐的方式,等价于用 Double.toString 的结果构造
方法说明
public BigDecimal add(BigDecimal augend)加
public BigDecimal subtract(BigDecimal subtrahend)减
public BigDecimal multiply(BigDecimal multiplicand)乘
public BigDecimal divide(BigDecimal divisor)除(结果无法有限表示时会抛 ArithmeticException)
public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode)除并指定保留位数与舍入模式
public double doubleValue()把结果转回基本类型使用

第 20 页的问答正好是本节要点:

  1. BigDecimal 的作用是什么?——解决浮点型运算时出现结果失真的问题;
  2. 如何把浮点型转换成 BigDecimal 对象?——BigDecimal b1 = BigDecimal.valueOf(0.1);
  3. BigDecimal 提供了哪些常见方法?——见上表。
double a = 0.1;
double b = 0.2;

// 包装成 BigDecimal 对象
BigDecimal a1 = new BigDecimal(Double.toString(a));
BigDecimal b1 = new BigDecimal(Double.toString(b));
// 阿里巴巴的规范更推荐用 valueOf,与上面的做法本质一样
BigDecimal a11 = BigDecimal.valueOf(a);
BigDecimal b11 = BigDecimal.valueOf(b);

// 调用方法进行精度运算:add / subtract / multiply / divide
BigDecimal c11 = a11.add(b11);
double result = c11.doubleValue();   // BigDecimal 是手段,结果仍是基本类型
System.out.println(result);         // 0.3

4.3 除法必须交代精度与舍入模式

BigDecimal i = BigDecimal.valueOf(0.1);
BigDecimal j = BigDecimal.valueOf(0.3);
// 参数一:除数  参数二:保留位数  参数三:舍入模式
BigDecimal k = i.divide(j, 2, RoundingMode.HALF_UP);
System.out.println(k);   // 0.33

课件作业中的「元转分再转元」用的是旧写法 divide(money2, 2, BigDecimal.ROUND_DOWN),参数含义相同(保留 2 位、直接舍去多余位数),只是 RoundingMode 常量在 JDK 9 之后更推荐使用。

5. JDK 8 之前:Date、SimpleDateFormat、Calendar

5.1 Date

Date 代表的是日期和时间。

(表格按课件第 22、23 页的题目整理)

构造器说明
public Date()创建表示当前日期时间的对象
public Date(long time)用时间毫秒值创建日期对象
方法说明
public long getTime()获取该日期对象对应的时间毫秒值
public void setTime(long time)把时间毫秒值设置到该日期对象上

第 23 页的两个问答也就是上表内容:日期对象如何创建、如何获取时间毫秒值(Date()、getTime());时间毫秒值怎么转成日期对象(Date(long time)、setTime(long time))。

public static void main(String[] args) {
    // 1、创建当前日期时间对象
    Date d = new Date();
    System.out.println(d);

    // 2、得到当前时间的毫秒值
    long time = d.getTime();
    System.out.println(time);
}

Date 对象直接打印的结果与当前时间毫秒值

打印结果形如 Tue Jan 04 12:39:17 CST 2022 和 164127157743——这也正是下一节要学 SimpleDateFormat 的原因。

5.2 SimpleDateFormat

SimpleDateFormat 代表简单日期格式化,可以用来把日期对象、时间毫秒值格式化成我们想要的形式,也可以把字符串的时间形式解析成 Date 日期对象。

先看「为什么需要它」:业务系统里给用户看的时间都是有固定格式的,而不是 Date.toString() 的结果。

电商订单列表中的下单时间(格式化后的日期时间)

12306 出发日期输入框,录入的是 yyyy-MM-dd 形式的字符串时间

(表格按课件第 27、30 页的题目整理)

构造器说明
public SimpleDateFormat(String pattern)用指定的格式字符串创建格式化对象
方法说明
public final String format(Date d)格式化日期对象
public final String format(Object time)格式化时间毫秒值
public Date parse(String source)解析字符串时间,得到 Date 对象(需处理 ParseException)

时间格式的常见符号(第 28 页):

符号含义符号含义
y年m分
M月s秒
d日EEE星期几
H时a上午 / 下午

同一页给出的格式示例:yyyy-MM-dd HH:mm:ss → 2020-11-11 13:27:06;yyyy年MM月dd日 HH:mm:ss → 2020年11月11日 13:27:06。

格式化与解析的双向流程可以概括为:

Date 对象 / 时间毫秒值  --format-->  2099-11-12 11:11:11
"2022-12-12 12:30:30"  --parse--->  Date 日期对象
// 格式化:日期对象 → 字符串
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String s = sdf.format(new Date());

// 解析:字符串 → 日期对象(格式必须与字符串完全一致)
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy年MM月dd日");
Date date = sdf2.parse("2023年11月11日");

5.3 案例:能不能参加上秒杀活动

需求(第 31 页):

  • 活动方:秒杀开始时间 2023 年 11 月 11 日 0:00:00,结束时间 0:10:00;
  • 小贾下单并付款的时间为:2023 年 11 月 11 日 0:01:18;
  • 小皮下单并付款的时间为:2023 年 11 月 11 日 0:10:51。

秒杀活动的开始时间与结束时间

思路:把三个时间都转成毫秒值再比较,落在活动区间内才算参加上。

public static void main(String[] args) throws ParseException {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
    long start = sdf.parse("2023年11月11日 0:00:00").getTime();
    long end = sdf.parse("2023年11月11日 0:10:00").getTime();

    long jia = sdf.parse("2023年11月11日 0:01:18").getTime();  // 小贾
    long pi = sdf.parse("2023年11月11日 0:10:51").getTime();   // 小皮

    System.out.println(jia >= start && jia <= end);  // true:小贾参加上了
    System.out.println(pi >= start && pi <= end);    // false:小皮超出结束时间
}

5.4 Calendar

第 33、34 页先摆出一个需求:将 2023 年 09 月 10 日增加一个月。如果沿用前面的思路,就会变成:把字符串解析成 Date → 取毫秒值 → 加上「一个月的毫秒值 1000 * 60 * 60 * 24 * 30」→ 再格式化。课件用这个做法反衬出问题:不同月份天数不同,「一个月」并不能用一个固定毫秒数表示(3 月与 4 月、闰年 2 月都会算错)。这就是要学 Calendar 的理由。

第 35、36 页:Calendar 代表的是系统此刻时间对应的日历,通过它可以单独获取、修改时间中的年、月、日、时、分、秒等。

(表格按课件第 36、37 页的题目与题目文档整理)

方法说明
public static Calendar getInstance()获取当前日历对象
public int get(int field)获取某个日历字段的值,如 Calendar.DATE
public void set(int field, int value)设置某个日历字段
public final void set(int year, int month, int date)一次设置年、月、日
public abstract void add(int field, int amount)让某个字段增加 / 减少指定量
public final Date getTime()把日历对象转换成 Date 对象

注意:Calendar 是可变对象,一旦修改后其对象本身表示的时间将产生变化。

Calendar calendar = Calendar.getInstance();
calendar.set(2023, 8, 10);          // 月份从 0 开始:8 表示 9 月
calendar.add(Calendar.MONTH, 1);    // 增加一个月:月份天数由日历自己处理
Date date = calendar.getTime();
System.out.println(new SimpleDateFormat("yyyy年MM月dd日").format(date));  // 2023年10月10日

配套题目还把「月份从 0 开始」这一点用在了求 2 月天数上:把日期设为「用户输入年份的 3 月 1 日」(即 set(year, 2, 1)),再 add(Calendar.DATE, -1) 退一天,get(Calendar.DATE) 得到的就是 2 月的最后一天,等于 28 即为平年,否则为闰年。

6. JDK 8 开始新增的日期、时间

6.1 类总览与学习路线

第 39 页把 java.time 包下的类一次列全:

类表达含义
LocalDate年、月、日
LocalTime时、分、秒
LocalDateTime年、月、日 + 时、分、秒
ZoneId时区
ZonedDateTime带时区的时间
Instant时间戳 / 时间线
DateTimeFormatter用于时间的格式化和解析
Duration时间间隔(时、分、秒,纳秒)
Period时间间隔(年、月、日)

IDEA 中输入 java.time. 时列出的时间类

课件的章节页用 STEP 1 → STEP 4 标注了学习顺序:LocalDate / LocalTime / LocalDateTime → ZoneId / ZonedDateTime → Instant → DateTimeFormatter → Duration / Period。本节也按这个顺序展开。

6.2 LocalDate、LocalTime、LocalDateTime

三者都是不可变对象(withXxx、plusXxx 等方法返回新对象,原对象不变):

  • LocalDate:代表本地日期(年、月、日、星期);
  • LocalTime:代表本地时间(时、分、秒、纳秒);
  • LocalDateTime:代表本地日期、时间(年、月、日、星期、时、分、秒、纳秒),是三个类里最重要的一个。

获取对象的方案(表格按配套代码整理):

方法说明
public static LocalDate now() / LocalTime.now() / LocalDateTime.now()获取当前系统时间对应的对象
public static LocalDate of(int year, int month, int dayOfMonth)获取指定日期的对象
public static LocalTime of(int hour, int minute, int second)获取指定时间的对象
public static LocalDateTime of(int year, int month, int dayOfMonth, int hour, int minute, int second, int nanoOfSecond)获取指定日期时间的对象

LocalDate 常用 API(处理年、月、日、星期):

方法说明
getYear() / getMonthValue() / getDayOfMonth() / getDayOfYear()年 / 月 / 日 / 一年中的第几天
getDayOfWeek().getValue()星期几(1 = 周一 … 7 = 周日)
withYear(int) / withMonth(int) / withDayOfMonth(int) / withDayOfYear(int)直接修改某个信息,返回新对象
plusYears(long) / plusMonths(long) / plusWeeks(long) / plusDays(long)把某个信息加多少
minusYears(long) / minusMonths(long) / minusWeeks(long) / minusDays(long)把某个信息减多少
equals(Object) / isBefore(LocalDate) / isAfter(LocalDate)判断两个日期对象是否相等、在前还是在后

LocalTime 常用 API(处理时、分、秒、纳秒):方法名与上表一一对应,把「年/月/日/周」换成 getHour() / getMinute() / getSecond() / getNano(),withHour/withMinute/withSecond/withNano,plusHours/plusMinutes/plusSeconds/plusNanos,minusHours/minusMinutes/minusSeconds/minusNanos,比较同样是 equals / isBefore / isAfter。

LocalDateTime 常用 API:可以处理年、月、日、星期、时、分、秒、纳秒等全部信息,即上面两张表的方法它基本都有(getYear … getNano、withXxx、plusXxx、minusXxx、equals/isBefore/isAfter)。

LocalDate ld = LocalDate.now();
LocalDate ld2 = ld.withYear(2099);      // 返回新对象,ld 本身不变
LocalDate ld3 = ld.plusMonths(2);       // 加两个月
LocalDate ld8 = LocalDate.of(2099, 12, 12);
System.out.println(ld8.isAfter(ld));    // true

LocalDateTime ldt = LocalDateTime.now();
System.out.println(ldt.getDayOfWeek().getValue());  // 星期几
System.out.println(ldt.getNano());                  // 纳秒

LocalDateTime 与 LocalDate / LocalTime 之间的转换:

方法说明
public LocalDate toLocalDate()取日期部分
public LocalTime toLocalTime()取时间部分
public static LocalDateTime of(LocalDate date, LocalTime time)由日期与时间合成
LocalDate ld = ldt.toLocalDate();              // 合久必分
LocalTime lt = ldt.toLocalTime();
LocalDateTime ldt10 = LocalDateTime.of(ld, lt); // 分久必合

6.3 ZoneId 与 ZonedDateTime

由于世界各个国家与地区的经度不同,各地区的时间也有所不同,因此会划分为不同的时区。ZoneId 代表时区 Id,写法形如 Asia/Shanghai、Asia/Chongqing、America/New_York(一般是「洲名/城市名」或「国家名/城市名」)。中国标准时间 = 世界标准时间(UTC)+ 8 小时。

世界时区划分与时差对照图

ZoneId 的常见方法:

方法说明
public static ZoneId systemDefault()获取系统默认的时区
public static Set<String> getAvailableZoneIds()获取 Java 支持的全部时区 Id
public static ZoneId of(String zoneId)把某个时区 Id 封装成 ZoneId 对象

ZonedDateTime 的常见方法:

方法说明
public static ZonedDateTime now()获取系统默认时区的带时区时间对象
public static ZonedDateTime now(ZoneId zone)获取某个时区的带时区时间对象
其余 getXxx / withXxx / plusXxx / minusXxx功能与 LocalDateTime 一样,只是多带时区信息
ZoneId zoneId = ZoneId.systemDefault();
System.out.println(zoneId.getId());            // Asia/Shanghai

ZoneId an = ZoneId.of("America/New_York");
ZonedDateTime zdt = ZonedDateTime.now(an);

// 很多服务器需要世界标准时间
ZonedDateTime utcNow = ZonedDateTime.now(Clock.systemUTC());

6.4 Instant

通过获取 Instant 的对象可以拿到此刻的时间,该时间由两部分组成:从 1970-01-01 00:00:00 开始走到此刻的总秒数 + 不够 1 秒的纳秒数。

课件给的换算关系:1 秒 = 1000 毫秒;1 毫秒 = 1000 微秒;1 微秒 = 1000 纳秒;1 秒 = 1 000 000 000 纳秒。

方法说明
public static Instant now()获取此刻的 Instant 对象
public long getEpochSecond()从 1970-01-01 00:00:00 走到此刻的总秒数
public int getNano()不够 1 秒的纳秒数

传统 Date 类只能精确到毫秒,并且是可变对象;新增的 Instant 类可以精确到纳秒,并且是不可变对象,课件建议用 Instant 代替 Date。它的作用是:用来记录代码的执行时间,或用于记录用户操作某个事件的时间点。

Instant now = Instant.now();
System.out.println(now.getEpochSecond());   // 例如 1663253222
System.out.println(now.getNano());          // 例如 63411500

6.5 DateTimeFormatter

DateTimeFormatter 是格式化器,用于时间的格式化和解析,作用与 SimpleDateFormat 对应,但是不可变且线程安全。

方法说明
public static DateTimeFormatter ofPattern(String pattern)创建格式化器,格式符号与 SimpleDateFormat 相同
public String format(TemporalAccessor temporal)把日期时间对象格式化成字符串

LocalDateTime 提供的格式化、解析时间的方法:

方法说明
public String format(DateTimeFormatter formatter)用格式化器格式化
public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter)用格式化器把字符串解析成 LocalDateTime 对象
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss EEE a");
LocalDateTime ldt = LocalDateTime.now();

String result = dtf.format(ldt);       // 方式一
String result2 = ldt.format(dtf);      // 方式二,效果相同

// 解析:格式化器的格式必须与待解析字符串一模一样
DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt2 = LocalDateTime.parse("2023-11-11 11:11:11", dtf2);

6.6 Period 与 Duration

Period(一段时期)可以用于计算两个 LocalDate 对象相差的年数、月数、天数:

方法说明
public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive)得到两个日期之间的 Period 对象
getYears() / getMonths() / getDays()相差的年数 / 月数 / 天数
LocalDate start = LocalDate.of(2024, 3, 19);
LocalDate end = LocalDate.of(2024, 10, 13);

Period period = Period.between(start, end);
System.out.println(period.getYears());   // 0
System.out.println(period.getMonths());  // 6
System.out.println(period.getDays());    // 24

Duration(持续时间)可以用于计算两个时间对象相差的天数、小时数、分数、秒数、纳秒数,支持 LocalTime、LocalDateTime、Instant 等时间:

方法说明
public static Duration between(Temporal startInclusive, Temporal endExclusive)得到两个时间之间的 Duration 对象
toDays() / toHours() / toMinutes() / toSeconds() / toMillis() / toNanos()换算成间隔的天数 / 小时 / 分 / 秒 / 毫秒 / 纳秒
LocalDateTime start = LocalDateTime.of(2025, 11, 11, 11, 10, 10);
LocalDateTime end = LocalDateTime.of(2025, 11, 11, 11, 11, 11);

Duration duration = Duration.between(start, end);
System.out.println(duration.toDays());       // 0
System.out.println(duration.toHours());      // 0
System.out.println(duration.toMinutes());    // 1
System.out.println(duration.toSeconds());    // 61
System.out.println(duration.toMillis());     // 61000
System.out.println(duration.toNanos());      // 61000000000

(补充说明:配套代码 Test9.java、Test10.java 还演示了 ChronoUnit.YEARS.between(...) 这类「按单位求间隔」的写法,以及 Duration.toHoursPart() / toMinutesPart() / toSecondsPart() 这类「只取某一段」的写法(高考倒计时案例),课件正文未展开。)

7. 作业与练习

  • 作业文档:day04-常用API/day04作业/作业04/day04作业.md
  • 题目与参考答案目录(可参考):day04-常用API/day04作业/03_题目/day04.md

作业要点:

  1. 单词:字符串拼接类的单词(StringBuilder / StringBuffer / StringJoiner)、精确计算浮点数类的单词(BigDecimal)、Java 新时间各个类的单词。
  2. 简答:StringBuilder 有何作用、与 StringBuffer 的区别;为什么 Java 时间零点是「1970-01-01 00:00:00」、中国是不是这个时间;LocalDate、ZoneId、ZonedDateTime、Instant、DateTimeFormatter、Period、Duration 各自的作用。
  3. 代码题一(BigDecimal):手机 3999 元、钢化膜 9.9 元、满 4000 打 98 折,买一台手机和两块膜应付多少(精确到分)。注意先把总价算出来再判断门槛、再折扣,最后统一按分舍入。
  4. 代码题二(BigDecimal):给出让 0.1 + 0.2 精确输出 0.2(即按精度舍入)的代码。
  5. 代码题三(Date + SimpleDateFormat):补全素材 seckill 项目,实现秒杀倒计时(开始时间为「明天 06:00:00」);提示是构造方法里初始化开始时间,run() 里取当前时间减去开始时间得到毫秒值,再换算成时、分、秒。
  6. 代码题四(Date + Calendar):"2015-10-20" → 日期对象 → 日历对象;求出是星期几、这一年的第几天;支持键盘录入,输出形如「2015年-10月-20日 是 星期二,是2015年的第 293 天」。
  7. 选做(JDK 8 时间):孙工上三天休一天(首次休息日 2020-02-01),输入年、月,用日历方式打印该月休息日(中括号标记),并统计本月休息天数、轮到周末休息的天数。提示用 LocalDate 找月最大天数,再算与首次休息日的间隔天数。

题目文档中还额外出现了几处本日 API 的组合用法:Calendar.set/add/get 求 2 月天数与闰年判断、System.arraycopy 复制旧数组 + Arrays.sort 让新商品价格重新排序、Math.abs/ceil 统计区间内符合条件的整数个数、BigDecimal 的元转分与分转元(multiply 乘 100、divide 保留 2 位)、SimpleDateFormat.parse + System.currentTimeMillis() 计算入职年限。

8. 复习要点

  1. String 不可变,频繁拼接会产生大量无用对象;StringBuilder 是内容可变的容器,适合频繁拼接与修改;少量拼接或只是定义字符串变量时仍用 String,不要机械替换。
  2. StringBuilder 与 StringBuffer 用法完全一样,区别只在线程安全:StringBuilder 线程不安全,StringBuffer 线程安全。
  3. StringBuilder.append() 返回容器本身,可以链式调用;最终结果要用 toString() 转回 String。
  4. StringJoiner 是 JDK8 新增的可变容器,构造时给定「间隔符 +(可选)开始符号与结束符号」,省掉手写最后一个元素判断。
  5. Math 是静态工具类:abs、ceil、floor、round、max/min、pow、random([0.0, 1.0))。
  6. System.currentTimeMillis() 返回从 1970-01-01 00:00:00 起的总毫秒值;起点是 C 语言(Unix)沿用下来的纪元,中国标准时间 = UTC + 8 小时,毫秒值本身全球统一。
  7. Runtime 是单例类,用 Runtime.getRuntime() 获取;常用方法有 exit、availableProcessors、totalMemory、freeMemory、exec。
  8. 浮点运算会失真,金额用 BigDecimal,优先 new BigDecimal("0.1") 或 BigDecimal.valueOf(0.1);四则运算返回新对象,除法必须交代精度与舍入模式。
  9. JDK8 之前:Date 表示时间点(getTime / setTime 在毫秒值之间转换)、SimpleDateFormat 负责格式化与解析(y M d H m s EEE a)、Calendar 负责按字段取改日期(月份从 0 开始,且 Java 对象可变)。用固定毫秒数表示「一个月」是不可靠的。
  10. JDK8 新增 java.time 系列都是不可变对象:LocalDate / LocalTime / LocalDateTime 处理本地日期时间,ZoneId / ZonedDateTime 处理时区,Instant 精确到纳秒表示时间线上的时刻,DateTimeFormatter 负责格式化与解析,Period 求日期差(年/月/日),Duration 求时间差(天/时/分/秒/纳秒)。
  11. 课件特别建议:需要时间戳时用不可变且精确到纳秒的 Instant 代替 Date。