Skip to content

บทที่ 11 — Date/Time, I/O และ Files

← บทที่ 10 | สารบัญ | บทที่ 12 →

📓 โซนอ้างอิง—เปิดตอนต้องใช้ (บท 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
Monthenum เดือน 1-12MAY
DayOfWeekenum วันในสัปดาห์MONDAY
Durationระยะเวลา (วินาที/นาที/ชั่วโมง) — base = วินาทีPT2H30M
Periodระยะเวลา (วัน/เดือน/ปี) — base = วันP1Y2M3D
Clock"นาฬิกา" — แหล่งที่มาของเวลา (ปรับแต่งได้ — สำคัญสำหรับ test)system / fixed / offset
ZoneId / ZoneOffsetidentifier ของ timezone / offset จาก UTCAsia/Bangkok / +07:00
ChronoUnitenum หน่วยเวลา (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 ... SUNDAY

3.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);       // false
java
// 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                              ← สำหรับ ZonedDateTime

Locale — แสดงผลตามภาษา

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); // OK

Built-in formatters ที่ใช้บ่อย

Constantรูปแบบ
ISO_LOCAL_DATE2026-05-18
ISO_LOCAL_DATE_TIME2026-05-18T14:30:25
ISO_OFFSET_DATE_TIME2026-05-18T14:30:25+07:00
ISO_ZONED_DATE_TIME2026-05-18T14:30:25+07:00[Asia/Bangkok]
ISO_INSTANT2026-05-18T07:30:25Z
RFC_1123_DATE_TIMEMon, 18 May 2026 14:30:25 +0700 (HTTP header)
BASIC_ISO_DATE20260518

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 แต่ต่างกัน:

OffsetDateTimeZonedDateTime
เก็บวัน-เวลา + 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());                           // 6

7. ⚠️ 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() (return null ถ้าไม่มี 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=admin
java
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/mydbJDBC (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-io FileAlterationMonitor (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 — ระวังเรื่อง cast long → 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;
        }
    }
}

ทำอะไรบ้าง:

  1. Files.lines() → stream บรรทัด (lazy — ไม่โหลดทั้งไฟล์)
  2. filter ERROR → keep แค่ที่สนใจ
  3. map → ดึงชั่วโมง
  4. groupingBy + counting → นับต่อชั่วโมง
  5. ปิด 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 แล้ว:

  1. สร้าง list ของ User 5 คน
  2. เขียนเป็น JSON ลงไฟล์
  3. อ่านกลับขึ้นมา
  4. filter age > 18
  5. เขียนผลกลับลงไฟล์ใหม่
📖 เฉลย 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


← บทที่ 10 | บทที่ 12 → Concurrency