โหมดมืด
บทที่ 11 — Date/Time, I/O และ Files
📓 โซนอ้างอิง—เปิดตอนต้องใช้ (บท 11-19) บทนี้เป็นบทแรกของ "โซนที่ 2" (บท 11-19) — เป็นหัวข้อที่ใช้ตอนทำงานจริงมากกว่าตอนหัดเขียน ถ้าเพิ่งเริ่มเรียน Java ข้ามทั้งบทไปได้ กลับมาเปิดตอนต้องจัดการวันเวลา/อ่านเขียนไฟล์จริง ๆ ส่วนต้นบท (วันเวลา, อ่าน-เขียนไฟล์) เข้าถึงง่าย ส่วนท้าย (NIO, JSON/CSV) ลึกขึ้น — เลือกอ่านเฉพาะที่ต้องใช้ได้
บทนี้รวมหัวข้อที่ทุกโปรเจกต์ Java ต้องใช้ — เวลา กับ ไฟล์
ศัพท์ที่จะเจอบ่อยในบทนี้: I/O (Input/Output = การรับเข้า-ส่งออกข้อมูล เช่น อ่าน/เขียนไฟล์), NIO.2 (New I/O รุ่น 2 = ชุด API ไฟล์รุ่นใหม่ของ Java), JSON (รูปแบบข้อความเก็บข้อมูลแบบ key-value ที่นิยมบนเว็บ), CSV (Comma-Separated Values = ไฟล์ตารางคั่นด้วยจุลภาค), timezone (เขตเวลา), DST (Daylight Saving Time = การปรับนาฬิกาตามฤดูกาลในบางประเทศ), UTC (เวลามาตรฐานกลางของโลก), epoch (จุดเริ่มนับเวลา = 1 ม.ค. 1970)
หลังจบบท คุณจะ:
- ใช้ Date/Time API ของ Java สมัยใหม่ (java.time) ไม่ใช้ Date/Calendar เก่า
- อ่าน/เขียน text file + binary file
- เข้าใจ stream (InputStream/OutputStream/Reader/Writer)
- ใช้ NIO.2 (
Path,Files) แทนFileเก่า - แปลง JSON/CSV ↔ object
1. ทำไม Date/Time เก่ามันแย่
ก่อน Java 8 เรามี java.util.Date กับ Calendar:
java
Date date = new Date(); // วันนี้
Calendar c = Calendar.getInstance();
c.set(2026, 4, 18); // เดือนเริ่มที่ 0 → 4 = May (?!)
c.add(Calendar.DAY_OF_MONTH, 7); // บวก 7 วัน
Date result = c.getTime();ปัญหา:
- เดือน 0-indexed → bug บานเบอะ
- Date mutable → method แก้ค่าตัวเอง → bug ใน multi-thread
- timezone งง →
Dateไม่มี timezone แต่toString()แสดง - ออกแบบมาตั้งแต่ Java 1.0 — ทีมไม่เคยปรับ
Java 8 ปี 2014 มาแก้ด้วย java.time package ซึ่งกำหนดไว้ใน JSR-310 (ข้อกำหนดมาตรฐาน Java สำหรับ API นี้) แนวคิดของ java.time ได้แรงบันดาลใจมาจาก Joda-Time (library วันเวลายอดนิยมในยุคก่อน Java 8)
2. คลาสหลักใน java.time
| Class | ใช้ทำอะไร | ตัวอย่าง |
|---|---|---|
LocalDate | วันเดือนปี (ไม่มีเวลา ไม่มี timezone) | 2026-05-18 |
LocalTime | เวลา (ไม่มีวัน) | 14:30:00 |
LocalDateTime | วัน + เวลา (ไม่มี timezone) | 2026-05-18T14:30 |
ZonedDateTime | วัน + เวลา + timezone (DST aware) | 2026-05-18T14:30+07:00[Asia/Bangkok] |
OffsetDateTime | วัน + เวลา + offset จาก UTC (ไม่มี DST) | 2026-05-18T14:30+07:00 |
Instant | เวลา ณ จุดหนึ่งบน timeline (UTC, ไม่มี calendar) | 1779062400 (≈ 2026-05-18T00:00Z, นับวินาทีจาก epoch) |
Year | ปีอย่างเดียว | 2026 |
YearMonth | เดือน-ปี (ไม่มีวัน) — เช่น "บัตรเครดิตหมดอายุเดือนไหน" | 2026-05 |
MonthDay | วัน-เดือน (ไม่มีปี) — เช่น "วันเกิดทุก ๆ ปี" | --05-18 |
Month | enum เดือน 1-12 | MAY |
DayOfWeek | enum วันในสัปดาห์ | MONDAY |
Duration | ระยะเวลา (วินาที/นาที/ชั่วโมง) — base = วินาที | PT2H30M |
Period | ระยะเวลา (วัน/เดือน/ปี) — base = วัน | P1Y2M3D |
Clock | "นาฬิกา" — แหล่งที่มาของเวลา (ปรับแต่งได้ — สำคัญสำหรับ test) | system / fixed / offset |
ZoneId / ZoneOffset | identifier ของ timezone / offset จาก UTC | Asia/Bangkok / +07:00 |
ChronoUnit | enum หน่วยเวลา (DAYS, HOURS, NANOS, …) | สำหรับ between() |
ทุกตัว immutable + thread-safe — method ทุกตัว return ค่าใหม่
3. ใช้งานพื้นฐาน
Java สมัยใหม่ใช้ java.time (ตั้งแต่ Java 8) จัดการวันเวลา — ต่างจาก Date/Calendar เก่าที่ใช้ยาก คลาสหลักคือ LocalDate (วันที่), LocalTime (เวลา), LocalDateTime (ทั้งคู่) ข้อดีคือ immutable และเดือนเริ่มนับจาก 1 (ไม่ใช่ 0 แบบเก่า):
java
import java.time.*;
import java.time.format.DateTimeFormatter;
// สร้าง
LocalDate today = LocalDate.now(); // 2026-05-18
LocalDate birthday = LocalDate.of(1995, 3, 15); // (year, month, day) - month เริ่ม 1!
LocalDate parsed = LocalDate.parse("2026-05-18");
LocalTime now = LocalTime.now(); // 14:30:25.123
LocalTime lunch = LocalTime.of(12, 0); // 12:00
LocalDateTime dt = LocalDateTime.now(); // 2026-05-18T14:30
LocalDateTime meeting = LocalDateTime.of(2026, 5, 18, 10, 0);
// คำนวณ
LocalDate nextWeek = today.plusDays(7); // ไม่แก้ today!
LocalDate nextMonth = today.plusMonths(1);
LocalDate yesterday = today.minusDays(1);
// เปรียบเทียบ
boolean isAfter = today.isAfter(birthday);
boolean isBefore = today.isBefore(LocalDate.of(2030, 1, 1));
// อ่านค่า
int year = today.getYear();
Month month = today.getMonth(); // enum: JANUARY ... DECEMBER
int dayOfMonth = today.getDayOfMonth();
DayOfWeek dow = today.getDayOfWeek(); // MONDAY ... SUNDAY3.1 YearMonth / MonthDay / Year — เมื่อ "ครบส่วน" ไม่ใช่สิ่งที่ต้องการ
หลายครั้งเราไม่อยากเก็บวันเต็ม:
java
// YearMonth — เดือน-ปี (วันบัตรเครดิตหมดอายุ, salary period, รายงานรายเดือน)
YearMonth expiry = YearMonth.of(2027, 12); // "12/27" บนบัตร
YearMonth thisMonth = YearMonth.now(); // 2026-05
int daysInMonth = expiry.lengthOfMonth(); // 31
boolean isLeap = YearMonth.of(2024, 2).isLeapYear(); // true
boolean leap = YearMonth.of(2024, 2).isValidDay(29); // true (29 ก.พ. 2024 มี)
YearMonth nextMonth = thisMonth.plusMonths(1); // 2026-06
LocalDate firstDay = thisMonth.atDay(1); // 2026-05-01
LocalDate lastDay = thisMonth.atEndOfMonth(); // 2026-05-31
// loop ทุกเดือนใน 1 ปี
YearMonth start = YearMonth.of(2026, 1);
for (int i = 0; i < 12; i++) {
YearMonth m = start.plusMonths(i);
System.out.println(m + " มี " + m.lengthOfMonth() + " วัน");
}
// parse จากรูปแบบ "MM/yy" บนบัตร
YearMonth fromCard = YearMonth.parse("12/27",
DateTimeFormatter.ofPattern("MM/yy"));java
// MonthDay — วัน-เดือน (วันเกิด, วันหยุดประจำปี ที่ไม่สนปี)
MonthDay songkran = MonthDay.of(4, 13);
MonthDay todayMd = MonthDay.from(LocalDate.now());
boolean isSongkranToday = todayMd.equals(songkran); // true เฉพาะถ้ารันวันที่ 13 เม.ย.
LocalDate songkran2026 = songkran.atYear(2026); // 2026-04-13
// validation: วันที่ valid ไหมในปีไหน
boolean leapDay = MonthDay.of(2, 29).isValidYear(2024); // true
boolean notLeap = MonthDay.of(2, 29).isValidYear(2025); // falsejava
// Year — ปีอย่างเดียว
Year y = Year.of(2026);
boolean leapYear = y.isLeap(); // false
int days = y.length(); // 365
YearMonth ym = y.atMonth(5); // 2026-05
LocalDate ld = y.atMonthDay(MonthDay.of(12, 25)); // 2026-12-25💡 ใช้ตัวที่ "พอดี" กับโจทย์ — ถ้าเก็บวันหมดอายุบัตร อย่าใช้ LocalDate (ต้องเดาวันที่ 1 หรือวันสุดท้าย); ใช้ YearMonth ตรง ๆ
3.2 Clock — abstract source ของเวลา (สำคัญสำหรับการ test)
ปัญหาของ LocalDate.now(): ทดสอบยาก — ค่าจะเปลี่ยนทุกครั้งที่รัน
แก้: ทุก factory method (now()) รับ Clock ได้ → inject clock เข้าไป
java
// production — ใช้นาฬิกาจริง
Clock systemClock = Clock.systemDefaultZone();
Clock systemUtc = Clock.systemUTC();
LocalDate today = LocalDate.now(systemClock); // 2026-05-25
Instant nowI = Instant.now(systemUtc);
// test — clock หยุดที่จุดหนึ่ง
Clock fixed = Clock.fixed(Instant.parse("2026-05-25T10:00:00Z"), ZoneOffset.UTC);
LocalDate testDay = LocalDate.now(fixed); // 2026-05-25 เสมอ
// shift clock (test การคำนวณข้าม timezone)
Clock plusOneHour = Clock.offset(systemClock, Duration.ofHours(1));
// tick clock — round down เป็นนาที (ลด flakiness ของ test)
Clock tickMinute = Clock.tickMinutes(ZoneOffset.UTC);ในโปรเจกต์จริง เราส่ง Clock เข้า constructor แทนที่จะสร้างเองข้างใน เพราะตอน test จะได้ใส่ Clock จำลองเวลาแทน — เทคนิคนี้เรียกว่า dependency injection (จะเจออีกมากในบท Spring Boot):
java
public class InvoiceService {
private final Clock clock;
public InvoiceService(Clock clock) { this.clock = clock; }
public Invoice create() {
return new Invoice(LocalDate.now(clock)); // ไม่เรียก now() ตรง ๆ
}
}
// production: new InvoiceService(Clock.systemDefaultZone())
// test: new InvoiceService(Clock.fixed(...))💡 กฎ: production code ไม่ควรเรียก
LocalDate.now()/Instant.now()ตรง ๆ — ให้รับClockมา inject เพื่อ test ได้
3.3 ChronoUnit + TemporalAdjusters — คำนวณวันที่ระดับสูง
สำหรับคำนวณวันเวลาขั้นสูง java.time มีเครื่องมือทรงพลัง — ChronoUnit หาความต่างเป็นหน่วยที่ต้องการ (วัน/เดือน/ปี) และ TemporalAdjusters หาวันแบบมีเงื่อนไข (เช่น "วันศุกร์ถัดไป", "วันสุดท้ายของเดือน"):
ChronoUnit — หน่วยเวลา enum
java
import java.time.temporal.ChronoUnit;
LocalDate a = LocalDate.of(2026, 1, 1);
LocalDate b = LocalDate.of(2026, 5, 25);
long days = ChronoUnit.DAYS.between(a, b); // 144
long months = ChronoUnit.MONTHS.between(a, b); // 4
long weeks = ChronoUnit.WEEKS.between(a, b); // 20
// บวก/ลบหน่วยทั่วไป
LocalDateTime dt = LocalDateTime.now();
LocalDateTime later = dt.plus(15, ChronoUnit.MINUTES);
LocalDateTime earlier = dt.minus(2, ChronoUnit.HOURS);
// truncate — ตัดส่วนเล็กกว่าทิ้ง
LocalDateTime hourStart = dt.truncatedTo(ChronoUnit.HOURS); // ตัดนาที/วินาทีออก
Instant minute = Instant.now().truncatedTo(ChronoUnit.MINUTES);TemporalAdjusters — "ปรับ" วันที่ตามกฎ
java
import static java.time.temporal.TemporalAdjusters.*;
import java.time.DayOfWeek;
LocalDate today = LocalDate.of(2026, 5, 25); // จันทร์
LocalDate firstOfMonth = today.with(firstDayOfMonth()); // 2026-05-01
LocalDate lastOfMonth = today.with(lastDayOfMonth()); // 2026-05-31
LocalDate firstOfNextMonth = today.with(firstDayOfNextMonth()); // 2026-06-01
LocalDate firstOfYear = today.with(firstDayOfYear()); // 2026-01-01
LocalDate nextFriday = today.with(next(DayOfWeek.FRIDAY)); // 2026-05-29
LocalDate prevSunday = today.with(previous(DayOfWeek.SUNDAY));// 2026-05-24
LocalDate firstMonOfMonth = today.with(firstInMonth(DayOfWeek.MONDAY));
LocalDate lastFriOfMonth = today.with(lastInMonth(DayOfWeek.FRIDAY));
LocalDate secondTueOfMonth = today.with(dayOfWeekInMonth(2, DayOfWeek.TUESDAY));
// ทำเอง: ถ้าวันหยุด เลื่อนเป็นวันถัดไป
TemporalAdjuster nextBusinessDay = TemporalAdjusters.ofDateAdjuster(d -> {
DayOfWeek dow = d.getDayOfWeek();
if (dow == DayOfWeek.SATURDAY) return d.plusDays(2);
if (dow == DayOfWeek.SUNDAY) return d.plusDays(1);
return d;
});
LocalDate dueDate = LocalDate.of(2026, 5, 30).with(nextBusinessDay); // เสาร์ → จันทร์4. Format + Parse
java
LocalDate date = LocalDate.of(2026, 5, 18);
// Format → String
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy");
String text = date.format(fmt); // "18/05/2026"
// Built-in formatters
String iso = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // "2026-05-18"
// Parse → LocalDate
LocalDate parsed = LocalDate.parse("18/05/2026", fmt);
// Pattern ที่ใช้บ่อย:
// yyyy = 4-digit year MM = month (01-12) dd = day (01-31)
// HH = 24-hour mm = minute ss = second
// hh = 12-hour a = AM/PM
// EEEE = full weekday MMM = short month
// SSS = millisecond X / XX / XXX = offset (-08, -0800, -08:00) ← สำหรับ OffsetDateTime
// VV = zone id z = zone name ← สำหรับ ZonedDateTimeLocale — แสดงผลตามภาษา
Locale (ตัวระบุภาษา/ประเทศสำหรับการแสดงผล เช่น ไทย/อังกฤษ) มาจาก java.util.Locale — ถ้า import แบบ wildcard import java.util.*; หรือ import java.util.Locale; ก็ใช้ได้เลย
java
import java.util.Locale;
import java.time.format.FormatStyle; // จำเป็นสำหรับ FormatStyle.FULL / SHORT / MEDIUM / LONG
LocalDate d = LocalDate.of(2026, 5, 18);
DateTimeFormatter th = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.of("th")); // Locale.of (Java 19+) — `new Locale("th")` deprecated แล้ว (deprecated = เลิกแนะนำให้ใช้แล้ว อาจถูกลบในอนาคต)
DateTimeFormatter en = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.ENGLISH);
String thStr = d.format(th); // "18 พฤษภาคม 2026"
String enStr = d.format(en); // "18 May 2026"
// localized full
DateTimeFormatter full = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
.withLocale(Locale.ENGLISH);
String s = d.format(full); // "Monday, May 18, 2026"DateTimeFormatterBuilder — เมื่อ pattern string ไม่พอ
java
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
// ตัวอย่าง: case-insensitive + optional second + default ค่า
DateTimeFormatter custom = new DateTimeFormatterBuilder()
.parseCaseInsensitive() // "may" = "MAY"
.appendPattern("yyyy-MM-dd HH:mm")
.optionalStart().appendPattern(":ss").optionalEnd() // วินาทีจะมีหรือไม่ก็ได้
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0) // ถ้าไม่มี → 0
.toFormatter();
LocalDateTime a = LocalDateTime.parse("2026-05-18 14:30", custom); // OK
LocalDateTime b = LocalDateTime.parse("2026-05-18 14:30:25", custom); // OKBuilt-in formatters ที่ใช้บ่อย
| Constant | รูปแบบ |
|---|---|
ISO_LOCAL_DATE | 2026-05-18 |
ISO_LOCAL_DATE_TIME | 2026-05-18T14:30:25 |
ISO_OFFSET_DATE_TIME | 2026-05-18T14:30:25+07:00 |
ISO_ZONED_DATE_TIME | 2026-05-18T14:30:25+07:00[Asia/Bangkok] |
ISO_INSTANT | 2026-05-18T07:30:25Z |
RFC_1123_DATE_TIME | Mon, 18 May 2026 14:30:25 +0700 (HTTP header) |
BASIC_ISO_DATE | 20260518 |
5. Timezone — เมื่อต้องคุยกับโลก
LocalDateTime ไม่มี timezone — แค่ "ตัวเลข"ZonedDateTime มี timezone
java
ZoneId bangkok = ZoneId.of("Asia/Bangkok");
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
ZonedDateTime now = ZonedDateTime.now(bangkok);
ZonedDateTime sameInstantInTokyo = now.withZoneSameInstant(tokyo);
// เวลาในไทย 14:00 → ในญี่ปุ่นคือ 16:00 (เวลาเดียวกัน คนละ timezone)
// แปลงไป UTC (เก็บ database ควรใช้ UTC เสมอ!)
Instant utc = now.toInstant();💡 กฎทอง: เก็บใน database = UTC (
Instantหรือtimestamp with time zone)
แปลง timezone ตอนแสดงผลเท่านั้น
OffsetDateTime vs ZonedDateTime — ตัวไหนตอนไหน?
ทั้งคู่มี offset จาก UTC แต่ต่างกัน:
OffsetDateTime | ZonedDateTime | |
|---|---|---|
| เก็บ | วัน-เวลา + offset (+07:00) | วัน-เวลา + zone id (Asia/Bangkok) |
| รู้จัก DST? | ❌ ไม่รู้ | ✅ รู้ — ปรับ offset ตามฤดู |
| ขนาด | เล็กกว่า | ใหญ่กว่า |
| ใช้ตอนไหน | snapshot ของจุดเวลา (เก็บใน DB, ส่งใน API) | คำนวณข้ามวัน/ข้ามเดือนใน timezone ที่มี DST |
java
// OffsetDateTime — fix offset
OffsetDateTime odt = OffsetDateTime.now(); // 2026-05-25T14:00+07:00
OffsetDateTime fromIso = OffsetDateTime.parse("2026-05-25T14:00+07:00");
// ZonedDateTime — DST aware
ZonedDateTime ny = ZonedDateTime.of(
LocalDateTime.of(2026, 3, 8, 1, 30), ZoneId.of("America/New_York"));
ZonedDateTime threeHoursLater = ny.plusHours(3);
// ที่ NY 2026-03-08 02:00 → กระโดดเป็น 03:00 เพราะ DST → offset เปลี่ยนจาก -05:00 → -04:00
// ผลลัพธ์: 2026-03-08T05:30-04:00 (01:30 + 3 ชม. ข้ามช่วง 02:00-03:00 ที่หายไปเพราะ DST)💡 หลักการทั่วไป (rule of thumb): API response / DB column =
OffsetDateTimeหรือInstant; ปฏิทินผู้ใช้ในประเทศที่มี DST =ZonedDateTime
6. Duration + Period — ระยะเวลา
java.time แยก "ระยะเวลา" เป็นสองชนิดตามความละเอียด — Duration สำหรับเวลาแบบชั่วโมง/นาที/วินาที และ Period สำหรับวัน/เดือน/ปี เลือกใช้ให้ถูกตามว่าวัดอะไร:
java
// Duration = เวลา (ชม./นาที/วินาที)
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 30);
Duration workDay = Duration.between(start, end); // PT8H30M
long minutes = workDay.toMinutes(); // 510
Duration timeout = Duration.ofSeconds(30);
Duration retry = Duration.ofMillis(500);
// Period = วัน/เดือน/ปี
LocalDate from = LocalDate.of(2020, 1, 1);
LocalDate to = LocalDate.of(2026, 5, 18);
Period age = Period.between(from, to); // P6Y4M17D
System.out.println(age.getYears()); // 67. ⚠️ Pitfall — Date เก่ายังอยู่
หลายที่ยังใช้ java.util.Date (เช่น JDBC, library เก่า)
แปลง:
java
// Date → Instant (modern)
Date legacy = new Date();
Instant modern = legacy.toInstant();
// Instant → Date
Date back = Date.from(modern);
// LocalDateTime ↔ Date (ต้องใส่ timezone)
LocalDateTime ldt = LocalDateTime.now();
Date d = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());⚠️
ZoneId.systemDefault()= "เอา timezone ของเครื่องที่รัน" — สะดวกแต่ ระวัง: ค่าขึ้นอยู่กับ timezone ของ JVM ซึ่งอาจต่างกันระหว่าง dev (ไทย +07:00) กับ server (UTC) → bug หาไม่เจอ ถ้าต้องการ predictable behavior ให้ระบุ zone ชัดเจน:ZoneId.of("Asia/Bangkok")หรือZoneOffset.UTCหรือตั้ง-Duser.timezone=UTCให้ทุก environment
8. ไฟล์ใน Java — ภาพรวม
มี 2 API:
- Old (
java.io.File) — ตั้งแต่ Java 1.0, design เก่า - NIO.2 (
java.nio.file.Path+Files) — Java 7+, แนะนำใช้
เราจะใช้ NIO.2
9. Path — แทน String "C:/users/jirap/..."
java
import java.nio.file.Files;
import java.nio.file.Path;
Path p = Path.of("data", "users.json"); // relative
Path abs = Path.of("/var/log/app.log"); // absolute (Linux)
Path winAbs = Path.of("C:", "Users", "jirap", "doc.txt"); // Windows
// ข้อมูลของ path
Path name = p.getFileName(); // users.json
Path parent = p.getParent(); // data
Path absolute = p.toAbsolutePath(); // /home/.../data/users.json
boolean exists = Files.exists(p);💡 Path = แค่ string ที่ฉลาด. ยังไม่มีไฟล์จริงก็สร้าง Path ได้ — ใช้ Files ถึงจะแตะ disk
10. อ่าน/เขียน text file
อ่านทั้งไฟล์ (เล็ก)
java
import java.nio.file.Files;
import java.nio.file.Path;
Path path = Path.of("data/hello.txt");
// อ่านเป็น String เดียว
String content = Files.readString(path);
// อ่านเป็น list ของบรรทัด
List<String> lines = Files.readAllLines(path);⚠️ ไฟล์ใหญ่อย่าใช้ — โหลดทั้งไฟล์เข้า memory!
อ่านทีละบรรทัด (ใหญ่ — stream)
java
try (Stream<String> stream = Files.lines(path)) {
stream.filter(line -> line.contains("ERROR"))
.forEach(System.out::println);
}
// try-with-resources → ปิด stream อัตโนมัติเขียนไฟล์
java
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardOpenOption;
Files.writeString(path, "Hello\nWorld\n");
// ค่า default = overwrite + create
Files.write(path, List.of("line 1", "line 2"), StandardCharsets.UTF_8); // ระบุ charset ชัดเจน
// append:
Files.writeString(path, "more\n", StandardOpenOption.APPEND);11. ⚠️ try-with-resources — บังคับใช้กับ I/O
ไฟล์เปิดแล้วต้อง ปิด เสมอ ไม่งั้น file descriptor รั่ว (leak) — เปิดสะสมไปเรื่อย ๆ จนเปิดไฟล์ใหม่ไม่ได้ (OS มี limit จำนวน open file ต่อ process) และบน Windows ไฟล์อาจถูก lock ลบ/แก้ไม่ได้:
java
// ❌ leak resource ถ้า exception
BufferedReader reader = Files.newBufferedReader(path);
String line = reader.readLine();
reader.close(); // ถ้า readLine() throw — บรรทัดนี้ไม่ถูกเรียก
// ✅ try-with-resources — ปิดให้แม้ exception
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line = reader.readLine();
}Class ที่ implement AutoCloseable ใช้ try-with-resources ได้
12. Binary file — InputStream / OutputStream
text file = readable text, แต่ image/zip/pdf = binary (byte ดิบ)
java
// อ่าน
byte[] data = Files.readAllBytes(Path.of("photo.jpg"));
// เขียน
Files.write(Path.of("copy.jpg"), data);
// Stream binary (ไฟล์ใหญ่)
try (InputStream in = Files.newInputStream(Path.of("big.zip"));
OutputStream out = Files.newOutputStream(Path.of("backup.zip"))) {
byte[] buffer = new byte[8192];
int n;
while ((n = in.read(buffer)) != -1) {
out.write(buffer, 0, n);
}
}Stream Hierarchy
Reader = "InputStream + decoder"
Writer = "OutputStream + encoder"
เกือบทุกครั้ง → ใช้ Reader/Writer สำหรับ text, ใช้ InputStream/OutputStream สำหรับ binary
13. Files API — ทำอย่างอื่นกับไฟล์
นอกจากอ่าน/เขียนเนื้อหา คลาส Files (จาก NIO.2) ยังจัดการไฟล์/โฟลเดอร์ได้ครบ — สร้าง, ลบ, ก๊อป, ย้าย, เช็คว่ามีอยู่ และเดินสำรวจไดเรกทอรี:
java
// สร้าง / ลบ
Files.createFile(path);
Files.createDirectory(path);
Files.createDirectories(Path.of("a/b/c")); // mkdir -p
Files.delete(path);
Files.deleteIfExists(path);
// copy / move
Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING);
Files.move(src, dest);
// ข้อมูล
long size = Files.size(path); // bytes
boolean isDir = Files.isDirectory(path);
boolean isRegular = Files.isRegularFile(path);
FileTime modified = Files.getLastModifiedTime(path);
// list ไฟล์ใน directory
try (Stream<Path> entries = Files.list(Path.of("/tmp"))) {
entries.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// walk recursively
try (Stream<Path> walk = Files.walk(Path.of("src"))) {
walk.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}13.1 Scanner — อ่าน input แบบง่าย ๆ
Scanner เหมาะกับงาน "อ่าน token ทีละชิ้น":
java
import java.util.Scanner;
// อ่าน console
try (Scanner sc = new Scanner(System.in)) {
System.out.print("ชื่อ: ");
String name = sc.nextLine();
System.out.print("อายุ: ");
int age = sc.nextInt();
System.out.println("สวัสดี " + name + " อายุ " + age);
}
// อ่านไฟล์
try (Scanner sc = new Scanner(Path.of("data.txt"))) {
while (sc.hasNextLine()) {
System.out.println(sc.nextLine());
}
}
// แยก token ด้วย delimiter
try (Scanner sc = new Scanner("a,b,c,d").useDelimiter(",")) {
while (sc.hasNext()) System.out.println(sc.next()); // a / b / c / d
}⚠️ Scanner มี edge case:
nextInt()ไม่กิน newline → ต่อด้วยnextLine()แล้วจะได้ string ว่าง
สำหรับ console interactive ใหม่ ๆ ลองSystem.console().readLine()(returnnullถ้าไม่มี terminal)
13.2 Properties — config file แบบ key=value
properties
# app.properties
app.name=My App
app.version=1
db.url=jdbc:postgresql://localhost/mydb
db.user=adminjava
import java.util.Properties;
Properties props = new Properties();
try (var in = Files.newInputStream(Path.of("app.properties"))) {
props.load(in);
}
String name = props.getProperty("app.name");
String dbUrl = props.getProperty("db.url", "jdbc:h2:mem:test"); // default
int version = Integer.parseInt(props.getProperty("app.version"));
// เขียนกลับ
props.setProperty("app.lastRun", Instant.now().toString());
try (var out = Files.newOutputStream(Path.of("app.properties"))) {
props.store(out, "Updated by app"); // มี header comment
}สำหรับ config ใหญ่ ๆ ใน Java สมัยใหม่ → ใช้ YAML (รูปแบบไฟล์ config ที่อ่านง่าย มักใช้นามสกุล
.yml)/JSON + Jackson หรือ Spring@ConfigurationProperties(annotation ของ Spring สำหรับโหลด config เป็น object — ดูรายละเอียดในบท Spring Boot)📝 ตัวอย่าง
app.propertiesด้านบนมีdb.url=jdbc:postgresql://localhost/mydb— JDBC (Java Database Connectivity) คือ API มาตรฐานของ Java สำหรับเชื่อมต่อฐานข้อมูลjdbc:postgresql://...คือรูปแบบ URL บอกว่าใช้ PostgreSQL รายละเอียด JDBC เต็ม ๆ ดูในบทที่ 17
13.3 WatchService — เฝ้าดูไฟล์เปลี่ยนแปลง
java
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
Path dir = Path.of("/var/log/myapp");
try (WatchService ws = FileSystems.getDefault().newWatchService()) {
dir.register(ws, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
while (true) {
WatchKey key = ws.take(); // blocking — รอ event
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path name = (Path) event.context(); // ชื่อไฟล์ (relative ต่อ dir)
System.out.println(kind + ": " + name);
}
if (!key.reset()) break; // directory หายไป → ออก
}
}ใช้ทำ: hot-reload config, monitor folder รับไฟล์ใหม่ (เช่น batch ETL)
⚠️ WatchService ใช้ kernel API ของ OS — แต่ละ OS ละเอียดต่างกัน บางครั้ง event ซ้ำหรือมาช้า (โดยเฉพาะบน macOS) — งาน production-grade ลอง
commons-ioFileAlterationMonitor(Apache Commons IO = library ภายนอก ต้องเพิ่ม dependency เอง ดูวิธีในบทที่ 14 — ยังเป็น blocking polling เหมือนกัน แต่จัดการ edge case ให้มากกว่า)
13.4 FileChannel + MappedByteBuffer — ไฟล์ใหญ่มาก ๆ
📖 ศัพท์ที่ใช้ในหัวข้อนี้:
- memory-map = ผูกไฟล์เข้ากับ memory address โดยตรง (OS จัดการโหลดเองเป็นชิ้น ๆ)
- heap = พื้นที่หน่วยความจำที่ JVM ใช้เก็บ object
- GC (Garbage Collector) = ระบบเก็บขยะของ JVM ที่คืน memory อัตโนมัติ
- zero-copy = ส่งข้อมูลโดยไม่ต้องก๊อปผ่าน buffer กลาง
NIO low-level: FileChannel แทน stream → เร็วกว่า, รองรับ memory-map
java
import java.nio.*;
import java.nio.channels.FileChannel;
// memory-mapped: map ไฟล์เข้า memory แทนอ่านทีละบิต
try (FileChannel ch = FileChannel.open(Path.of("huge.bin"),
StandardOpenOption.READ)) {
long size = ch.size();
MappedByteBuffer buf = ch.map(FileChannel.MapMode.READ_ONLY, 0, size);
// อ่านเหมือน array — OS จัดการ paging (การสลับข้อมูลระหว่าง RAM กับ disk) ให้
byte first = buf.get(0);
byte last = buf.get((int) size - 1);
}จุดเด่น:
- อ่านไฟล์ GB ๆ โดยไม่โหลดทั้งไฟล์เข้า heap (OS จะ swap page เอง)
- เร็วมากเพราะข้าม buffer copy (zero-copy ในบาง path)
- เหมาะกับ index file, database file, large CSV
จุดระวัง:
- จำกัด ~2 GB ต่อ 1 map call — ไฟล์ใหญ่กว่านั้นต้องแบ่ง map เป็นช่วง ๆ (เรียก
ch.map(...)หลายครั้ง);buf.get((int) size - 1)ก็ overflow ถ้าsize > Integer.MAX_VALUE— ระวังเรื่อง castlong → int MappedByteBufferปล่อย memory ตอน GC เท่านั้น — บน Windows อาจ lock ไฟล์ทิ้งไว้ByteBufferมีposition/limit— ใช้ผิดอ่านข้อมูลผิด- ไม่ใช่ thread-safe — ห้ามแชร์ระหว่าง thread
14. JSON — Jackson (เกือบ standard ของโลก Java)
📖 pom.xml คืออะไร?
pom.xmlคือไฟล์ของ Maven (ระบบ build tool) ใช้ระบุ library ภายนอกที่โปรเจกต์ต้องการดึงมาใช้ ถ้าใช้ IntelliJ ให้เปิดไฟล์pom.xmlในโปรเจกต์แล้ววาง<dependency>ลงใน<dependencies>— IntelliJ จะดาวน์โหลดให้อัตโนมัติ รายละเอียด Maven เต็ม ๆ ดูในบทที่ 14
Java ไม่มี JSON ในตัว แต่ Jackson เป็น library ที่แทบทุกโปรเจกต์ใช้ (Spring Boot ก็ใช้) — แปลง object↔JSON ด้วย ObjectMapper (writeValueAsString, readValue) เพิ่ม module jsr310 เพื่อรองรับ LocalDate:
pom.xml:
xml
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.0</version> <!-- ตรวจ version ล่าสุดที่ Maven Central ก่อนใช้งานจริง — ใช้ version เดียวกันทั้งโปรเจกต์ -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId> <!-- รองรับ LocalDate -->
<version>2.19.0</version> <!-- ต้องตรงกับ jackson-databind ด้านบน -->
</dependency>java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
record User(String name, int age, LocalDate birthday) {}
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// ↑ จำเป็น — ไม่งั้น LocalDate จะ serialize เป็น array [2000,1,1] แทน string "2000-01-01"
// object → JSON
User u = new User("Anna", 25, LocalDate.of(2000, 1, 1));
String json = mapper.writeValueAsString(u);
// {"name":"Anna","age":25,"birthday":"2000-01-01"}
// JSON → object
User parsed = mapper.readValue(json, User.class);
// อ่านจากไฟล์
User fromFile = mapper.readValue(Path.of("user.json").toFile(), User.class);
// เขียนลงไฟล์
mapper.writeValue(Path.of("out.json").toFile(), u);
// pretty print
String pretty = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(u);15. CSV — สั้น ๆ ด้วย OpenCSV
สำหรับไฟล์ CSV ไม่ควรแยกด้วย split(",") เอง (พังเมื่อข้อมูลมี comma หรือ quote) — ใช้ library อย่าง OpenCSV ที่จัดการ edge case ให้ครบ:
xml
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.9</version>
</dependency>java
import com.opencsv.CSVReader;
try (CSVReader reader = new CSVReader(Files.newBufferedReader(Path.of("data.csv")))) {
String[] line;
while ((line = reader.readNext()) != null) {
System.out.println(line[0] + " | " + line[1]);
}
}สำหรับ Excel ใช้ Apache POI
16. ตัวอย่างใหญ่ — Log Analyzer
อ่าน log file นับ ERROR ต่อชั่วโมง:
java
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.*;
public class LogAnalyzer {
private static final DateTimeFormatter FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws Exception {
Path log = Path.of("app.log");
Map<Integer, Long> errorsByHour;
try (Stream<String> lines = Files.lines(log)) {
errorsByHour = lines
.filter(line -> line.contains("ERROR"))
.map(LogAnalyzer::extractHour)
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(h -> h, Collectors.counting()));
}
errorsByHour.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> System.out.printf("%02d:00 %d errors%n", e.getKey(), e.getValue()));
}
static Integer extractHour(String line) {
// log format: "2026-05-18 14:30:25 [ERROR] ..."
try {
LocalDateTime t = LocalDateTime.parse(line.substring(0, 19), FMT);
return t.getHour();
} catch (Exception e) {
return null;
}
}
}ทำอะไรบ้าง:
Files.lines()→ stream บรรทัด (lazy — ไม่โหลดทั้งไฟล์)filterERROR → keep แค่ที่สนใจmap→ ดึงชั่วโมงgroupingBy + counting→ นับต่อชั่วโมง- ปิด stream อัตโนมัติด้วย try-with-resources
17. Checkpoint
🛠️ Checkpoint 11.1 — Date arithmetic
เขียนโปรแกรมรับวันเกิด แล้วบอกว่าคุณอายุกี่ปี กี่เดือน กี่วัน + วันเกิดครั้งต่อไปอีกกี่วัน
📖 เฉลย 11.1
java
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.Scanner;
public class DateArithmetic {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("วันเกิด (yyyy-MM-dd): ");
LocalDate birthday = LocalDate.parse(sc.nextLine());
LocalDate today = LocalDate.now();
Period age = Period.between(birthday, today);
System.out.printf("อายุ: %d ปี %d เดือน %d วัน%n",
age.getYears(), age.getMonths(), age.getDays());
MonthDay md = MonthDay.from(birthday);
LocalDate nextBirthday = md.atYear(today.getYear());
if (!nextBirthday.isAfter(today)) {
nextBirthday = md.atYear(today.getYear() + 1);
}
long daysLeft = ChronoUnit.DAYS.between(today, nextBirthday);
System.out.println("วันเกิดครั้งต่อไปอีก " + daysLeft + " วัน");
}
}🛠️ Checkpoint 11.2 — Word counter
อ่านไฟล์ .txt แล้วนับว่าแต่ละคำมีกี่ครั้ง — เรียงมากไปน้อย แสดง top 10
(Hint: Files.lines() + String.split("\\s+") + Collectors.groupingBy + counting)
📖 เฉลย 11.2
java
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;
public class WordCounter {
public static void main(String[] args) throws Exception {
Path path = Path.of("input.txt");
try (Stream<String> lines = Files.lines(path)) {
lines.flatMap(line -> Arrays.stream(line.split("\\s+")))
.filter(w -> !w.isBlank())
.map(String::toLowerCase)
.collect(Collectors.groupingBy(w -> w, Collectors.counting()))
.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(10)
.forEach(e -> System.out.printf("%-20s %d%n", e.getKey(), e.getValue()));
}
}
}🛠️ Checkpoint 11.3 — JSON CRUD
สร้าง User record แล้ว:
- สร้าง list ของ User 5 คน
- เขียนเป็น JSON ลงไฟล์
- อ่านกลับขึ้นมา
- filter age > 18
- เขียนผลกลับลงไฟล์ใหม่
📖 เฉลย 11.3
java
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.nio.file.Path;
import java.util.List;
record User(String name, int age) {}
public class JsonCrud {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 1. สร้าง list
List<User> users = List.of(
new User("Alice", 25), new User("Bob", 17),
new User("Charlie", 30), new User("Diana", 15),
new User("Eve", 22)
);
// 2. เขียนลงไฟล์
Path file = Path.of("users.json");
mapper.writerWithDefaultPrettyPrinter().writeValue(file.toFile(), users);
// 3. อ่านกลับ
CollectionType listType = mapper.getTypeFactory()
.constructCollectionType(List.class, User.class);
List<User> loaded = mapper.readValue(file.toFile(), listType);
// 4-5. filter แล้วเขียนไฟล์ใหม่
List<User> adults = loaded.stream().filter(u -> u.age() > 18).toList();
mapper.writerWithDefaultPrettyPrinter().writeValue(Path.of("adults.json").toFile(), adults);
System.out.println("ผู้ใหญ่: " + adults);
}
}18. สรุปบท
Date/Time
✅ ใช้ java.time (LocalDate/Time/DateTime/Zoned/Offset/Instant) — ลืม Date/Calendar
✅ ทุก class immutable + thread-safe — method return ค่าใหม่เสมอ
✅ ใช้ "ตัวที่พอดี" — YearMonth สำหรับเดือน-ปี, MonthDay สำหรับวันเกิด, Year สำหรับปี
✅ เก็บ database = UTC — Instant หรือ OffsetDateTime; แปลง timezone ตอนแสดง
✅ ZonedDateTime = DST aware, OffsetDateTime = fix offset (ตัวเลือกที่ถูกของแต่ละบริบทต่างกัน)
✅ Clock = inject เข้า service เพื่อ test ได้ — อย่าเรียก now() ตรง ๆ ใน production code
✅ ChronoUnit สำหรับคำนวณช่วงเวลา, TemporalAdjusters สำหรับ "วันถัดไปที่..."
✅ DateTimeFormatterBuilder สำหรับ pattern ที่ pattern string ทำไม่ได้
I/O + Files
✅ ใช้ Path + Files แทน File เก่า
✅ Files.readString/writeString ไฟล์เล็ก, Files.lines() + stream ไฟล์ใหญ่
✅ try-with-resources เสมอเมื่อเปิด stream/file
✅ Reader/Writer = text, InputStream/OutputStream = binary
✅ Scanner = อ่าน input ทีละ token, Properties = config key=value
✅ WatchService = เฝ้าโฟลเดอร์, FileChannel + MappedByteBuffer = ไฟล์ขนาด GB
✅ Jackson แปลง JSON ↔ object — registerModule(JavaTimeModule) สำหรับ java.time