Java 8 的時間 API 用起來比之前的 Date、Calendar 不知道爽幾倍。但新 API 的時區轉換常常爆炸,因為邏輯和以前差很多。LinkedHashMap 是保持插入順序的 Map,常被小看但超實用。一次整理出來。

Java 8 時間 API 基礎

主要類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.time.*;

// LocalDateTime - 純日期時間,沒有時區
LocalDateTime now = LocalDateTime.now();
System.out.println(now); // 2026-02-20T14:30:45.123456

// LocalDate - 只有日期
LocalDate today = LocalDate.now();
System.out.println(today); // 2026-02-20

// LocalTime - 只有時間
LocalTime time = LocalTime.now();
System.out.println(time); // 14:30:45.123456

// Instant - UTC 時間戳,從 1970-01-01T00:00:00Z 起算
Instant instant = Instant.now();
System.out.println(instant); // 2026-02-20T06:30:45.123456Z

// ZoneId - 時區識別碼
ZoneId taipeiZone = ZoneId.of("Asia/Taipei");

// ZonedDateTime - 帶時區的日期時間
ZonedDateTime taipeiTime = ZonedDateTime.now(taipeiZone);
System.out.println(taipeiTime); // 2026-02-20T14:30:45.123456+08:00[Asia/Taipei]

時間轉換

最常見的坑:UTC 時間轉台灣時間。

1
2
3
4
5
6
7
8
9
10
11
12
// 假設 API 回傳的是 UTC 時間字串
String utcTimeStr = "2026-02-20T06:30:00Z"; // UTC 時間

// 方法 1:用 Instant 再轉 ZonedDateTime
Instant instant = Instant.parse(utcTimeStr);
ZonedDateTime taipeiTime = instant.atZone(ZoneId.of("Asia/Taipei"));
System.out.println(taipeiTime); // 2026-02-20T14:30:00+08:00[Asia/Taipei]

// 方法 2:用 LocalDateTime + ZoneOffset(更複雜但有時更清楚)
LocalDateTime utcLocal = LocalDateTime.parse(utcTimeStr.replace("Z", ""));
Instant inst = utcLocal.toInstant(ZoneOffset.UTC);
ZonedDateTime taipeiTime = inst.atZone(ZoneId.of("Asia/Taipei"));

關鍵的坑

UTC 時間轉本地時間的常見錯誤:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 爛做法 1:直接加 8 小時(早期做法)
long utcMillis = System.currentTimeMillis();
long taipeiMillis = utcMillis + 8 * 60 * 60 * 1000; // 這是錯的!

// 爛做法 2:假設 LocalDateTime.parse 就是 UTC
String utcStr = "2026-02-20T06:30:00";
LocalDateTime ldt = LocalDateTime.parse(utcStr);
// 錯誤:ldt 現在是 2026-02-20T06:30:00(沒有時區資訊),
// 系統會當成本地時間,而不是 UTC

// 正確做法
Instant inst = LocalDateTime.parse(utcStr).toInstant(ZoneOffset.UTC);
ZonedDateTime taipeiTime = inst.atZone(ZoneId.of("Asia/Taipei"));

格式化和解析

1
2
3
4
5
6
7
8
9
10
11
12
13
// 格式化
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(formatter);
System.out.println(formatted); // 2026-02-20 14:30:45

// 解析
LocalDateTime parsed = LocalDateTime.parse("2026-02-20 14:30:45", formatter);

// 帶時區的格式化
ZonedDateTime taipei = ZonedDateTime.now(ZoneId.of("Asia/Taipei"));
String tzFormatted = taipei.format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
System.out.println(tzFormatted); // 2026-02-20T14:30:45.123456+08:00[Asia/Taipei]

和舊 Date 轉換

有些古老 library 還在用 java.util.Date:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Instant -> Date
Instant instant = Instant.now();
Date date = Date.from(instant);

// Date -> Instant
Date date = new Date();
Instant instant = date.toInstant();

// LocalDateTime -> Date(需要時區)
LocalDateTime ldt = LocalDateTime.now();
Date date = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());

// Date -> LocalDateTime(需要時區)
Date date = new Date();
LocalDateTime ldt = date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();

LinkedHashMap 筆記

HashMap 不保證順序,但有時候需要保持插入順序。LinkedHashMap 就是這種場景。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 普通 HashMap:順序不確定
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("Alice", 30);
hashMap.put("Bob", 25);
hashMap.put("Cathy", 28);
// 遍歷時順序不確定

// LinkedHashMap:保持插入順序
Map<String, Integer> linkedMap = new LinkedHashMap<>();
linkedMap.put("Alice", 30);
linkedMap.put("Bob", 25);
linkedMap.put("Cathy", 28);
// 遍歷時就是 Alice -> Bob -> Cathy

linkedMap.forEach((k, v) -> System.out.println(k + ": " + v));
// 輸出:
// Alice: 30
// Bob: 25
// Cathy: 28

設定初始容量避免擴容

跟 HashMap 一樣,要預先設定容量避免頻繁 resize:

1
2
3
4
5
6
7
8
9
10
// 不好:會自動擴容
Map<String, String> map = new LinkedHashMap<>();

// 更好:預先指定容量
Map<String, String> map = new LinkedHashMap<>(16);

// 最好:根據預期大小計算
// LinkedHashMap 也遵循 0.75 的 load factor
// 如果預期 100 個元素,容量應該是 100 / 0.75 ≈ 134
Map<String, String> map = new LinkedHashMap<>(134);

LinkedHashMap 的內部結構

1
2
3
4
5
6
7
8
HashMap:
[bucket]─────> entry1 -> entry2 -> entry3

LinkedHashMap(保持插入順序):
[bucket]─────> entry1 -> entry2 -> entry3

同時維護一條雙向鏈結串列:
entry1 <-> entry2 <-> entry3

跟 HashMap 相比,多了一條雙向鏈結串列來記錄插入順序。查詢還是 O(1),但記憶體稍多。

LRU Cache 實現(特殊用法)

LinkedHashMap 有個特殊的 accessOrder 模式,記錄訪問順序而不是插入順序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 記錄訪問順序(LRU)
Map<String, String> lruCache = new LinkedHashMap<String, String>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 10; // 最多保持 10 個最近訪問的項
}
};

lruCache.put("key1", "value1");
lruCache.put("key2", "value2");
lruCache.get("key1"); // 訪問了 key1,它會移到最後
lruCache.put("key3", "value3");

// key1 因為被訪問過,優先級提高,不會被淘汰
// 如果再加超過 10 個,最舊的會被移除

accessOrder 參數:

  • false - 記錄插入順序(預設)
  • true - 記錄訪問順序(get、put 都會更新順序)

實戰組合:時間戳序列化

常見場景:API 回傳時間,要保持原始順序,還要轉成台灣時間。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
List<String> timeStrings = Arrays.asList(
"2026-02-20T06:00:00Z",
"2026-02-20T08:00:00Z",
"2026-02-20T10:00:00Z"
);

// 用 LinkedHashMap 保持順序,轉成台灣時間
Map<String, ZonedDateTime> timeMap = new LinkedHashMap<>();

for (String utcStr : timeStrings) {
Instant instant = Instant.parse(utcStr);
ZonedDateTime taipeiTime = instant.atZone(ZoneId.of("Asia/Taipei"));
timeMap.put(utcStr, taipeiTime);
}

// 遍歷時保持原始順序
timeMap.forEach((utcStr, taipeiTime) -> {
System.out.println(utcStr + " -> " + taipeiTime);
});

// 輸出(順序保證):
// 2026-02-20T06:00:00Z -> 2026-02-20T14:00:00+08:00[Asia/Taipei]
// 2026-02-20T08:00:00Z -> 2026-02-20T16:00:00+08:00[Asia/Taipei]
// 2026-02-20T10:00:00Z -> 2026-02-20T18:00:00+08:00[Asia/Taipei]

實務建議

時間 API:

  1. 儘量用 Instant + ZoneId 的組合處理時區轉換
  2. 永遠記得 LocalDateTime.parse() 不包含時區資訊
  3. 和舊 Date 轉換時透過 Instant 作中介
  4. 格式化用 DateTimeFormatter,別自己組字串

LinkedHashMap:

  1. 需要保持順序就用 LinkedHashMap,別用 HashMap
  2. 預先設定容量避免擴容
  3. 簡單 LRU 緩存用 accessOrder = true
  4. 需要複雜 LRU 邏輯就用 Guava 的 CacheBuilder

大多數時候用 Java 8 的時間 API 和 LinkedHashMap,就能搞定九成的場景。剩下一成是奇怪的需求,那就另當別論了。