常用 API:字符串、精确金额与日期时间
用促销金额、秒杀倒计时和轮休日历理解常用 API 的边界。
继续上次小节 →Java 进阶线 · 线路目录
Java 进阶线
对象与类型设计3 站
文件、配置与并发4 站
网络、框架与复习3 站
本页导读
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 页给出结论:
| 对比项 | StringBuilder | StringBuffer |
|---|---|---|
| 用法 | 完全一样(方法名、参数都一样) | 完全一样 |
| 线程安全 | 线程不安全 | 线程安全 |
| 使用建议 | 单线程、线程私有的局部变量拼接 | 需要多线程共享同一个容器时使用 |
1.5 案例:返回任意整型数组的内容
需求(第 7 页):设计一个方法,用于返回任意整型数组的内容,要求返回的数组内容格式如 [11, 22, 33]。
分析过程(课件):
- 方法是否需要接收数据?——需要接收整型数组;
- 方法是否需要返回数据?——需要返回拼接后的结果;
- 方法内部:遍历数组的数据,把遍历到的数据都拼接起来,此时使用
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 页的问答正好是本节要点:
BigDecimal的作用是什么?——解决浮点型运算时出现结果失真的问题;- 如何把浮点型转换成
BigDecimal对象?——BigDecimal b1 = BigDecimal.valueOf(0.1); 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);
}
打印结果形如 Tue Jan 04 12:39:17 CST 2022 和 164127157743——这也正是下一节要学 SimpleDateFormat 的原因。
5.2 SimpleDateFormat
SimpleDateFormat 代表简单日期格式化,可以用来把日期对象、时间毫秒值格式化成我们想要的形式,也可以把字符串的时间形式解析成 Date 日期对象。
先看「为什么需要它」:业务系统里给用户看的时间都是有固定格式的,而不是 Date.toString() 的结果。
(表格按课件第 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 | 时间间隔(年、月、日) |
课件的章节页用 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
作业要点:
- 单词:字符串拼接类的单词(
StringBuilder/StringBuffer/StringJoiner)、精确计算浮点数类的单词(BigDecimal)、Java 新时间各个类的单词。 - 简答:
StringBuilder有何作用、与StringBuffer的区别;为什么 Java 时间零点是「1970-01-01 00:00:00」、中国是不是这个时间;LocalDate、ZoneId、ZonedDateTime、Instant、DateTimeFormatter、Period、Duration各自的作用。 - 代码题一(BigDecimal):手机 3999 元、钢化膜 9.9 元、满 4000 打 98 折,买一台手机和两块膜应付多少(精确到分)。注意先把总价算出来再判断门槛、再折扣,最后统一按分舍入。
- 代码题二(BigDecimal):给出让
0.1 + 0.2精确输出0.2(即按精度舍入)的代码。 - 代码题三(Date + SimpleDateFormat):补全素材
seckill项目,实现秒杀倒计时(开始时间为「明天 06:00:00」);提示是构造方法里初始化开始时间,run()里取当前时间减去开始时间得到毫秒值,再换算成时、分、秒。 - 代码题四(Date + Calendar):
"2015-10-20"→ 日期对象 → 日历对象;求出是星期几、这一年的第几天;支持键盘录入,输出形如「2015年-10月-20日 是 星期二,是2015年的第 293 天」。 - 选做(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. 复习要点
String不可变,频繁拼接会产生大量无用对象;StringBuilder是内容可变的容器,适合频繁拼接与修改;少量拼接或只是定义字符串变量时仍用String,不要机械替换。StringBuilder与StringBuffer用法完全一样,区别只在线程安全:StringBuilder线程不安全,StringBuffer线程安全。StringBuilder.append()返回容器本身,可以链式调用;最终结果要用toString()转回String。StringJoiner是 JDK8 新增的可变容器,构造时给定「间隔符 +(可选)开始符号与结束符号」,省掉手写最后一个元素判断。Math是静态工具类:abs、ceil、floor、round、max/min、pow、random([0.0, 1.0))。System.currentTimeMillis()返回从 1970-01-01 00:00:00 起的总毫秒值;起点是 C 语言(Unix)沿用下来的纪元,中国标准时间 = UTC + 8 小时,毫秒值本身全球统一。Runtime是单例类,用Runtime.getRuntime()获取;常用方法有exit、availableProcessors、totalMemory、freeMemory、exec。- 浮点运算会失真,金额用
BigDecimal,优先new BigDecimal("0.1")或BigDecimal.valueOf(0.1);四则运算返回新对象,除法必须交代精度与舍入模式。 - JDK8 之前:
Date表示时间点(getTime/setTime在毫秒值之间转换)、SimpleDateFormat负责格式化与解析(y M d H m s EEE a)、Calendar负责按字段取改日期(月份从 0 开始,且 Java 对象可变)。用固定毫秒数表示「一个月」是不可靠的。 - JDK8 新增
java.time系列都是不可变对象:LocalDate/LocalTime/LocalDateTime处理本地日期时间,ZoneId/ZonedDateTime处理时区,Instant精确到纳秒表示时间线上的时刻,DateTimeFormatter负责格式化与解析,Period求日期差(年/月/日),Duration求时间差(天/时/分/秒/纳秒)。 - 课件特别建议:需要时间戳时用不可变且精确到纳秒的
Instant代替Date。