Skip to content

บทที่ 5 — OOP พื้นฐาน (Class, Object, Encapsulation)

← บทที่ 4 | สารบัญ | บทที่ 6: Inheritance →

บทนี้คือ หัวใจของ Java — ถ้าเข้าใจ OOP, Spring Boot (ชุดเครื่องมือสำเร็จรูปสำหรับสร้าง Java web app — framework) ทั้งหมดจะเข้าใจง่ายขึ้นมาก

หลังจบบท คุณจะ:

  • เข้าใจ "ทำไม OOP" — ปัญหาก่อน OOP คืออะไร
  • สร้าง class + สร้าง object จาก class
  • ใช้ field, constructor, method
  • เข้าใจ encapsulation (การห่อหุ้ม — ซ่อนข้อมูลไว้ในและเปิดทางเข้าผ่าน method, ใช้ private + getter/setter)
  • รู้จัก this, static, final ใน context ของ class
  • เริ่มแยก class หลายไฟล์ + package

1. ก่อน OOP — ปัญหาคืออะไร

ลองนึกว่าเรามีระบบบัญชีธนาคาร — เก็บข้อมูล user 1000 คน:

java
// แบบไม่ใช้ class — แย่มาก
String[] names = new String[1000];
String[] emails = new String[1000];
int[] balances = new int[1000];
int[] ages = new int[1000];
// ...

// อยากให้คนที่ index 0 ฝากเงิน 1000
balances[0] += 1000;

// อยากย้ายเงินจาก 0 ไป 1
balances[0] -= 500;
balances[1] += 500;

ปัญหา:

  1. ข้อมูลกระจัดกระจาย — ข้อมูลคน 1 คนอยู่หลายที่
  2. error-prone (เกิด error ได้ง่าย) — ลืม sync index, ลืม update balance
  3. logic ฝังในที่ที่ใช้balance += 1000 กระจาย ใครจะเช็คว่าฝากเกิน limit?
  4. ขยายไม่ได้ — เพิ่ม field ใหม่ต้องแก้ทุกที่

2. OOP คือคำตอบ — "Object เก็บ data + behavior"

OOP บอกว่า: รวม data (field) กับ behavior (method) ที่ใช้ data นั้น เข้าด้วยกัน เป็น "object"

⚠️ หมายเหตุ: ตัวอย่างในส่วนนี้ใช้ public field เพื่อให้อ่านง่าย — ยังไม่ใช่ production-grade (ขาด validation และ encapsulation) ดู section 6 สำหรับ version ที่ถูกต้อง

java
// แบบ OOP
class Account {
    String name;
    String email;
    int balance;
    int age;
    
    void deposit(int amount) {
        balance += amount;
    }
    
    void transferTo(Account other, int amount) {
        balance -= amount;
        other.balance += amount;    // ✅ ใน Java, method ของ class เดียวกันเข้าถึง private field
                                    //    ของ instance อื่นของ class เดียวกันได้ (ต่างจากบางภาษา)
    }
}

// ใช้งาน
Account anna = new Account();
anna.name = "Anna";
anna.balance = 1000;

Account ben = new Account();
ben.balance = 500;

anna.transferTo(ben, 200);
// anna.balance = 800, ben.balance = 700

⚠️ ตัวอย่าง transferTo นี้เขียนให้อ่านง่าย ยังไม่ปลอดภัยพอสำหรับงานจริง — ถ้ามีหลายงานทำพร้อมกัน (เช่น 2 คนโอนเงินบัญชีเดียวกันในเวลาเดียวกัน) ยอดเงินอาจเพี้ยนได้ เพราะการลบเงินออกแล้วบวกเข้าอีกบัญชี ไม่ได้ถูกทำเป็น "ก้อนเดียวที่แยกไม่ได้" (เรื่องนี้เรียกว่า concurrency — เรียนในบทหลัง ตอนนี้แค่รู้ว่าตัวอย่างนี้ไว้เรียน ไม่ใช่โค้ดจริง)

ดีกว่ายังไง:

  • ข้อมูลรวม — ทุกอย่างของ anna อยู่ใน object เดียว
  • method ใกล้ datatransferTo รู้จัก balance โดยตรง
  • clear semanticsanna.transferTo(ben, 200) อ่านเข้าใจ
  • ขยายง่าย — เพิ่ม field/method ที่ class

3. Class คืออะไร — เทียบแบบบ้าน ๆ

Class = "แบบ" (blueprint — เหมือนแบบพิมพ์เขียวของบ้าน ระบุว่าบ้านมีห้องอะไร แต่ตัวแบบเองไม่ใช่บ้านจริง) — บอกว่า object ของชนิดนี้มีอะไร และทำอะไรได้

Object = "ของจริง" ที่สร้างตามแบบ

เปรียบเทียบ:

OOP conceptของจริง
Class Carแบบพิมพ์เขียวรถ
Object myCarรถจริง 1 คัน
Field (color, speed)ลักษณะของรถคันนั้น (สีแดง, 100 km/h)
Method (drive, brake)ความสามารถของรถ

จาก class เดียวกัน สร้าง object ได้หลายตัว — แต่ละตัวมี state ของตัวเอง:

java
Car myCar = new Car();
myCar.color = "red";

Car yourCar = new Car();
yourCar.color = "blue";

// 2 ตัวคนละ object — เปลี่ยน myCar ไม่กระทบ yourCar

4. สร้าง class แรก — ตัวอย่าง Dog

java
// ไฟล์: Dog.java
public class Dog {
    // === Fields (state) ===
    String name;
    int age;
    String color;
    
    // === Methods (behavior) ===
    void bark() {
        System.out.println(name + " says: Woof!");
    }
    
    void birthday() {
        age++;
        System.out.println(name + " is now " + age + " years old");
    }
}

ใช้งาน (ในไฟล์อื่น เช่น Main.java):

java
public class Main {
    public static void main(String[] args) {
        // สร้าง object ของ Dog
        Dog buddy = new Dog();
        buddy.name = "Buddy";
        buddy.age = 3;
        buddy.color = "brown";
        
        // เรียก method
        buddy.bark();        // "Buddy says: Woof!"
        buddy.birthday();    // "Buddy is now 4 years old"
        
        // สร้างอีกตัว — คนละ object
        Dog max = new Dog();
        max.name = "Max";
        max.age = 5;
        
        max.bark();          // "Max says: Woof!"
        buddy.bark();        // "Buddy says: Woof!" — buddy ไม่กระทบ
    }
}

4.1 new — สร้าง object

Dog buddy = new Dog();

แยกชิ้น:

  • Dog buddy — ประกาศตัวแปรชื่อ buddy ประเภท Dog
  • = — assignment
  • new Dog() — สร้าง object ใหม่ของ class Dog

ภายในเครื่อง:

  1. Java ขอ memory สำหรับ Dog object ใหม่
  2. ตั้งค่า field default (String = null, int = 0, ...)
  3. คืน "reference" (ที่อยู่ของ object) ให้ตัวแปร buddy

ภาพ:

text
   buddy ──→ [Dog object: name=null, age=0, color=null]

📖 Reference คืออะไร (จากบทที่ 2 มาขยาย):

  • primitive (int, double, ...) — ตัวแปรเก็บค่าจริงตรง ๆ
  • reference (Dog, String, int[], ทุก class) — ตัวแปรเก็บป้ายชี้ไปที่ object ตัวจริงใน heap (ส่วนหน่วยความจำที่ JVM ใช้เก็บ object ทั้งหมด — ตรงข้ามกับ stack ที่เก็บ local variable)

ลูกศร ──→ ในภาพข้างบนคือ reference — ตัวแปร buddy เก็บแค่ "ที่อยู่" ของ Dog object ไม่ใช่ object เอง

สำหรับคนที่เคยเขียน Python/JS มาก่อน — เปรียบเทียบ
  • Python: ทุกตัวแปรเป็น reference (ทุกอย่างเป็น object) — เหมือน Java's reference อย่างเดียว ไม่มี primitive
  • JS: คล้าย Python ในการเก็บ object แต่มี primitive (number, string, ...)
  • Java: มีทั้งคู่ ใน 1 ภาษา → ทำให้ pass-by-value มี 2 พฤติกรรม (Java บทที่ 4 §4)

aliasing (การที่ตัวแปร 2 ตัวชี้ไปที่ object เดียวกัน): 2 ตัวแปรชี้ object เดียวกันได้:

java
Dog a = new Dog();
Dog b = a;          // ⬅ b ชี้ Dog ตัวเดียวกับ a (ไม่ใช่ copy object)
b.name = "Rex";     // ⬅ a.name ก็เป็น "Rex" ด้วย!

4.2 . (dot operator) — เข้าถึง member

java
buddy.name = "Buddy";     // ตั้งค่า field
buddy.bark();             // เรียก method

. แปลว่า "ของ"

  • buddy.name = "name ของ buddy"
  • buddy.bark() = "ให้ buddy bark"

5. Constructor — กำหนดค่าเริ่มต้น

โค้ดด้านบนต้อง set field ทีละตัว — น่าเบื่อ + อาจลืม

Constructor = method พิเศษที่ run อัตโนมัติตอน new (new = keyword สำหรับสร้าง object ใหม่ของ class — เราใช้มาตั้งแต่บทที่ 0 ตอนนี้จะอธิบายลึก)

java
public class Dog {
    String name;
    int age;
    String color;
    
    // Constructor — ชื่อเดียวกับ class, ไม่มี return type
    public Dog(String name, int age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }
    
    void bark() {
        System.out.println(name + " says: Woof!");
    }
}

ใช้งาน:

java
Dog buddy = new Dog("Buddy", 3, "brown");
buddy.bark();   // "Buddy says: Woof!"

5.1 this — ตัว object ตัวเอง

ใน constructor (และ method ทั่วไป) — this หมายถึง "object ที่กำลังถูกเรียก"

java
public Dog(String name, int age, String color) {
    this.name = name;   // this.name = field ของ class
                        // name = parameter
    this.age = age;
    this.color = color;
}

ทำไมต้องใส่ this: เพราะ parameter ชื่อชนกับ field — this. บอก compiler ว่า "ฉันหมายถึง field"

ถ้าตั้งชื่อต่าง (เช่น n, a, c) ไม่ต้องใช้ this:

java
public Dog(String n, int a, String c) {
    name = n;
    age = a;
    color = c;
}

แต่ convention ชอบใช้ชื่อเดียวกัน + this.

5.2 Default constructor

ถ้าไม่เขียน constructor — Java สร้างให้อัตโนมัติ:

java
public Dog() {
    // ว่าง — set field เป็น default
}

📖 access modifier ของ default constructor = เท่ากับ class เสมอ — ถ้า class เป็น public constructor ที่ Java generate ให้ก็จะเป็น public ด้วย ถ้า class ไม่มี modifier (package-private) constructor ที่ generate ให้ก็จะเป็น package-private เช่นกัน

นี่คือเหตุผลที่ new Dog() (ไม่มี argument) ใช้ได้ก่อนที่เราจะเขียน constructor

แต่ทันทีที่เราเขียน constructor ของเราเอง — Java เลิกให้ default

java
public class Dog {
    String name;
    
    public Dog(String name) {
        this.name = name;
    }
}

Dog d = new Dog();         // ❌ error: no default constructor
Dog d2 = new Dog("Max");   // ✅

5.3 Constructor overload — หลายแบบ

java
public class Dog {
    String name;
    int age;
    String color;
    
    // 3 parameters
    public Dog(String name, int age, String color) {
        this.name = name;
        this.age = age;
        this.color = color;
    }
    
    // 2 parameters — color default
    public Dog(String name, int age) {
        this(name, age, "unknown");   // เรียก constructor อื่นด้วย this()
    }
    
    // 1 parameter
    public Dog(String name) {
        this(name, 0, "unknown");
    }
}

Dog d1 = new Dog("Buddy", 3, "brown");
Dog d2 = new Dog("Max", 5);            // color = "unknown"
Dog d3 = new Dog("Rex");               // age = 0, color = "unknown"

this(...) = "เรียก constructor อื่นของ class เดียวกัน" — ต้องเป็น statement แรกใน constructor


6. Encapsulation (การห่อหุ้ม) — ซ่อน data + ควบคุมการเข้าถึง

โค้ดด้านบนมีปัญหา: ใครก็แก้ field ได้ — แม้แต่ใส่ค่าผิด:

java
Dog buddy = new Dog("Buddy", 3, "brown");
buddy.age = -100;        // 😱 อายุติดลบ?
buddy.name = "";         // 😱 ชื่อว่าง?

แก้ด้วย encapsulation — ซ่อน field ด้วย private, ให้เข้าถึงผ่าน method (getter (method สำหรับดึงค่า field) / setter (method สำหรับตั้งค่า field))

java
public class Dog {
    // private = ใช้ได้แค่ใน class นี้
    private String name;
    private int age;
    private String color;
    
    public Dog(String name, int age, String color) {
        setName(name);     // ใช้ setter เพื่อ validate
        setAge(age);
        this.color = color;
    }
    
    // === Getters ===
    public String getName() { return name; }
    public int getAge() { return age; }
    public String getColor() { return color; }
    
    // === Setters with validation ===
    // ⬇ ตัวอย่างนี้ใช้ throw — คือการส่ง error ออกไปเมื่อข้อมูลผิด
    // โปรแกรมจะหยุดที่บรรทัดนั้นทันที รายละเอียดเต็มอยู่บทที่ 8 ตอนนี้อ่านเข้าใจแค่ว่า "ถ้าผิด โค้ดบรรทัดนั้นจะหยุดและแจ้ง error"
    public void setName(String name) {
        // isBlank() = ว่างหรือมีแต่ช่องว่าง (space/tab), isEmpty() = ว่างสนิทเท่านั้น
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name cannot be empty");
            // throw หยุดโปรแกรมตรงนั้นทันทีและส่ง error ออกไป
            // ถ้าไม่มี try/catch จับ โปรแกรมจะ crash พร้อมข้อความ error
            // รายละเอียดเต็มบทที่ 8 — ตอนนี้รู้แค่ว่า throw ใช้ป้องกัน input ผิด
        }
        this.name = name;
    }
    
    public void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
        this.age = age;
    }
    
    public void bark() {
        System.out.println(name + " says: Woof!");
    }
}

ใช้งาน:

java
Dog buddy = new Dog("Buddy", 3, "brown");
buddy.age = -100;        // ❌ compile error: age is private

buddy.setAge(-100);      // ❌ runtime error: IllegalArgumentException
buddy.setAge(4);         // ✅
System.out.println(buddy.getAge());   // 4

Access modifiers ที่ Java มี

📖 package = โฟลเดอร์ที่ใช้จัดกลุ่ม class — รายละเอียดเต็มอยู่ใน section 9

Modifierเข้าถึงจากที่ไหน
publicทุกที่
protectedclass ใน package เดียวกัน + subclass (class ลูก — เรียนบทที่ 6) ในทุก package
(ไม่ใส่)"package-private" — class ใน package เดียวกันเท่านั้น
privateเฉพาะ class นี้

กฎ Encapsulation ที่ดี

  1. field เป็น private เสมอ (ยกเว้น constant public static final)
  2. เปิด getter/setter เฉพาะที่จำเป็น
  3. setter ตรวจ validate ทุกครั้ง
  4. field ที่ไม่ควรเปลี่ยน → ไม่มี setter (เช่น id, createdDate)

7. static — ของ class ไม่ใช่ของ object

ทุก field/method ที่เราเขียนมาเป็นของ object — แต่ละ object มีของตัวเอง

static บอกว่า "นี่เป็นของ class — ทุก object ใช้ตัวเดียวกัน"

7.1 Static field

java
public class Dog {
    private static int totalDogs = 0;   // ของ class — ทุก dog ใช้ตัวเดียว
    
    private String name;
    
    public Dog(String name) {
        this.name = name;
        totalDogs++;        // เพิ่มทุกครั้งที่สร้าง dog ใหม่
    }
    
    public static int getTotalDogs() {
        return totalDogs;
    }
}

new Dog("A");
new Dog("B");
new Dog("C");
System.out.println(Dog.getTotalDogs());   // 3

สังเกต: เรียกผ่าน Dog.getTotalDogs() — ผ่าน class ตรง ๆ ไม่ต้องมี object

7.2 Static method

ใช้กับ utility ที่ไม่ต้องใช้ state ของ object:

java
public class MathUtils {
    public static int square(int n) {
        return n * n;
    }
    
    public static int max(int a, int b) {
        return a > b ? a : b;
    }
}

int s = MathUtils.square(5);         // ไม่ต้อง new MathUtils()
int m = MathUtils.max(10, 20);

7.3 ⚠️ Static method เรียก non-static ไม่ได้

java
public class Dog {
    private String name;       // non-static
    
    public static void doSomething() {
        System.out.println(name);   // ❌ error: name ของ object ไหน?
    }
}

static method ไม่มี "ตัว object ที่เรียก" — แล้วจะใช้ name ของ object ไหน?

7.4 ทำไม main เป็น static

java
public static void main(String[] args) { ... }

ตอน JVM เริ่ม run, ยังไม่มี object ของ class — แต่ต้องเรียก method ได้ → ต้อง static


8. final กับ class

Final field — ไม่เปลี่ยนหลัง assign แล้ว

java
public class User {
    private final String id;    // ตั้งครั้งเดียว
    private String name;
    
    public User(String id, String name) {
        this.id = id;       // ✅ assign ใน constructor
        this.name = name;
    }
    
    public void changeId(String newId) {
        this.id = newId;    // ❌ error: cannot assign final
    }
}

ใช้กับ field ที่ "lock" ตั้งแต่สร้าง — เช่น id, createdAt

📖 กฎเพิ่มเติม: final field ต้องถูกกำหนดค่าครบทุกทางที่ constructor เดินผ่าน (assign ครั้งเดียว แต่ต้องมีแน่ ๆ) — ถ้ามี constructor หลายตัวหรือมี if/else แล้วบางเส้นทางไม่ได้ตั้งค่าให้ compiler จะฟ้อง variable id might not have been initialized

Final method — override ไม่ได้

(เดี๋ยวบทที่ 6 จะเรียน override)

Final class — extend ไม่ได้

ใส่ final หน้า class ทำให้ class นั้น ห้ามมีลูก (สืบทอด/extends ไม่ได้):

java
public final class ImmutablePoint {
    private final int x;
    private final int y;

    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
}
// class อื่นจะเขียน `extends ImmutablePoint` ไม่ได้ — compile error

class สำเร็จรูปของ Java หลายตัวก็เป็น final เช่นนี้ — ตัวอย่างที่รู้จักกันดีคือ String (java.lang.String ถูกประกาศเป็น public final class String) จึงไม่มีใครสืบทอด String ได้

⚠️ อย่าลองสร้างไฟล์ชื่อ String.java ในโปรเจกต์ของตัวเองเพื่อทดลอง — มันจะไปบัง (shadow) java.lang.String ตัวจริง แล้วทำให้เกิด compile error ที่งงมาก ถ้าจะลองให้ตั้งชื่อ class อื่น เช่น ImmutablePoint ข้างบน

Constant — public static final

java
public class Config {
    public static final int MAX_USERS = 100;
    public static final String API_URL = "https://api.example.com";
}

int max = Config.MAX_USERS;

ใช้สำหรับ "ค่าคงที่ที่ทุกที่ใช้ร่วม"


9. แยก class เป็นไฟล์

9.1 หนึ่งไฟล์ = หนึ่ง public class

text
project/
└── src/
    ├── Main.java       (มี class Main)
    ├── Dog.java        (มี class Dog)
    └── User.java       (มี class User)

กฎ: ไฟล์ชื่อ Dog.java → ต้องมี public class Dog ข้างใน

9.2 Package — รวม class เป็นหมวด

ถ้า project ใหญ่ — จัด class เข้าหมวด:

text
project/
└── src/
    ├── com/example/
    │   ├── Main.java
    │   ├── model/
    │   │   ├── Dog.java
    │   │   └── User.java
    │   └── service/
    │       └── UserService.java

ในไฟล์ Dog.java:

java
package com.example.model;

public class Dog { ... }

ใช้ใน Main.java:

java
package com.example;

import com.example.model.Dog;

public class Main {
    public static void main(String[] args) {
        Dog d = new Dog(...);
    }
}

Convention:

  • package = reverse domain ของบริษัท เช่น com.google.maps (เอาชื่อเว็บ google.com มากลับด้านเป็น com.google แล้วตามด้วยชื่อ project)
  • ใช้ตัวเล็กทั้งหมด
  • จัดตาม "หน้าที่" เช่น model, service, repository, controller

9.5 Initializer Block — โค้ดที่ run ตอนสร้าง

📌 หมายเหตุหมายเลข: section นี้ใช้เลข 9.5 เป็น subsection ของ section 9 (แยก class เป็นไฟล์)

นอกจาก constructor — Java มี block พิเศษ สำหรับ init field:

Instance initializer (block ที่รันตอนสร้าง object ทุกครั้ง) { ... }

run ทุกครั้งที่ newก่อน constructor body แต่หลัง field initializer

java
public class Cart {
    private List<String> items;

    {   // instance initializer
        items = new ArrayList<>();
        items.add("default");
    }

    public Cart() {
        // run หลัง initializer
    }
}

ใช้น้อย — ปกติเขียนใน constructor หรือเป็น field initializer (การกำหนดค่าเริ่มต้นตรง ๆ ตอนประกาศ field) ดีกว่า

Static initializer static { ... }

🚧 section นี้ใช้ Map/HashMap (สอนบทที่ 7) และ JNI/JDBC — ข้ามได้สำหรับมือใหม่รอบแรก

run ครั้งเดียว ตอน class ถูก load (ไม่ใช่ตอน new)

java
import java.util.HashMap;   // ต้อง import ทั้งสองบรรทัดนี้
import java.util.Map;

public class Config {
    public static final Map<String, String> SETTINGS;

    static {
        SETTINGS = new HashMap<>();
        // System.getenv อาจ return null ถ้า env variable ไม่ถูกตั้งค่า — ใส่ค่า default เสมอ
        String env = System.getenv("APP_ENV");
        SETTINGS.put("env", env != null ? env : "dev");   // null-check แบบตรงไปตรงมา
        // (Optional.ofNullable เขียนแทนได้ แต่ต้องเพิ่ม import java.util.Optional; ก่อน — เรียนบทที่ 9)
        SETTINGS.put("version", loadVersion());
    }

    private static String loadVersion() { /* ... */ return "1.0"; }
}

ใช้เมื่อ:

  • ต้อง init static final field ที่ต้อง compute (ไม่ใช่ literal)
  • ต้องโหลด JNI library (JNI = Java Native Interface — ส่วนที่ Java เรียกโค้ดภาษา C/C++ โดยตรง): static { System.loadLibrary("..."); }
  • ต้อง register driver: static { Class.forName("...Driver"); } (JDBC เก่า — JDBC 4.0 / Java 6+ ขึ้นไปไม่ต้องแล้ว ยกตัวอย่างนี้เพื่อเข้าใจ static initializer เท่านั้น)

⚠️ เลี่ยงถ้าเลี่ยงได้ — ทำให้ test/debug ยาก, ลำดับโหลดงง ๆ

⚠️ อันตราย: ถ้า static initializer เกิด error ตอนรัน — class ตัวนั้นจะใช้ไม่ได้ไปตลอดใน JVM session ครั้งนั้น (ต่อให้เรียกซ้ำก็พังทุกครั้ง) ทำให้ debug ยากมาก — ถ้าต้องใช้ให้ครอบด้วย try/catch ใน static block หรือ init แบบ lazy แทน

(สำหรับคนที่อยากรู้ชื่อ error: ครั้งแรกจะเป็น ExceptionInInitializerError ครั้งต่อ ๆ ไปเป็น NoClassDefFoundError — ทั้งคู่คือ "ข้อผิดพลาดตอนรัน" ที่จะเรียนละเอียดในบทที่ 8 ตอนนี้ไม่ต้องจำ)


9.6 Nested Class — อ้างอิงขั้นสูง (ข้ามได้จนกว่าจะเรียน Lambda บทที่ 9)

Nested Class — 4 แบบ (เปิดเมื่อเจอ legacy code หรือหลังเรียนบทที่ 9)

🚧 ข้ามได้สำหรับมือใหม่รอบแรก: Nested class 4 แบบ (โดยเฉพาะ anonymous class) เป็น concept ที่ Java ใช้ตอน lambda ยังไม่มี — สมัยใหม่ใช้ lambda แทนเกือบทั้งหมด (บทที่ 9) ตอนนี้ข้ามไปอ่าน Checkpoint ตอนท้ายบทได้เลย กลับมาอ่านเมื่อเจอ legacy code ที่ใช้ pattern นี้

Java อนุญาตให้สร้าง class ภายใน class อื่น — 4 แบบ:

1. Static nested class

java
public class Outer {
    static class Helper {
        void doWork() { ... }
    }
}

Outer.Helper h = new Outer.Helper();   // ไม่ต้องมี Outer object

ใช้เมื่อ helper class ผูกกับ Outer แต่ไม่ต้องการ state ของ Outer

2. Inner class (non-static)

java
public class LinkedList {
    private Node head;

    class Node {           // ผูกกับ instance ของ LinkedList
        Object data;
        Node next;
    }
}

// ต้องสร้างจาก instance ของ outer
LinkedList list = new LinkedList();
LinkedList.Node n = list.new Node();

Inner class เห็น field ของ outer instance — มี implicit reference (การอ้างถึงโดยปริยาย — outer instance ถูก hold ไว้อัตโนมัติ) ใช้อย่างระวัง เพราะ outer object จะถูกอ้างค้างไว้ทำให้ GC เก็บไม่ได้ → เกิด memory leak (ปัญหาที่หน่วยความจำไม่ถูกปล่อยคืนเพราะมีการอ้างอิงค้างอยู่) ได้ใน Java ทุกประเภท (ไม่ใช่แค่ Android) โดยเฉพาะเมื่อส่ง inner object เป็น callback ออกไปนอก outer

💡 ถ้าไม่ต้องการเข้าถึง state ของ outer — ใช้ static nested class แทน inner class เพื่อหลีกเลี่ยงปัญหานี้

⚠️ Spring Boot note: ใน Spring Boot ห้ามใช้ non-static inner class เป็น Spring bean เพราะ Spring ต้องสร้าง outer object ก่อน ซึ่งก่อให้เกิด unexpected dependency และปัญหา serialization

3. Local class — class ใน method

java
public void process() {
    class Validator {
        boolean check(int n) { return n > 0; }
    }
    Validator v = new Validator();
}

ใช้น้อย — lambda มาแทนเกือบหมด

4. Anonymous class — class ไม่มีชื่อ

java
Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("running");
    }
};

ก่อน Java 8 ใช้บ่อยมาก — ตอนนี้ lambda แทน:

java
Runnable r = () -> System.out.println("running");

💡 Rule of thumb: ใช้ static nested class เป็น default (เช่น Map.Entry), หลีกเลี่ยง inner class นอกจากจำเป็น


10. equals + hashCode + toString

ทุก class ที่เราสร้างได้ method พื้นฐานจาก Object มาฟรี — แต่ behavior default มักไม่ดี

10.1 toString — แทน object เป็น string

Default:

java
Dog buddy = new Dog("Buddy", 3, "brown");
System.out.println(buddy);    // Dog@1f32e575 (ขยะ — hash address)

Override:

java
public class Dog {
    // fields...
    
    @Override
    public String toString() {
        return "Dog{name='" + name + "', age=" + age + "}";
    }
}

System.out.println(buddy);    // Dog{name='Buddy', age=3}

@Override = annotation บอก compiler ว่า "ฉันจะ override method ของ parent" — กันสะกดผิด

10.2 equals — เปรียบเทียบเนื้อหา

Default equals เทียบ reference (เหมือน ==):

java
Dog a = new Dog("Buddy", 3, "brown");
Dog b = new Dog("Buddy", 3, "brown");
a.equals(b);    // false (Dog คนละ object)

Override:

📖 instanceof คืออะไร: เช็คว่า object เป็นชนิดนั้นไหม — if (obj instanceof Dog) คืน true ถ้า obj เป็น Dog — รายละเอียดเต็มบทที่ 6.2

java
import java.util.Objects;   // ต้อง import ก่อนใช้ Objects.equals / Objects.hash

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Dog)) return false;    // เช็คว่าเป็น Dog ไหม
    Dog other = (Dog) obj;                      // cast — แปลง type ให้ใช้ field Dog ได้
    return age == other.age
        && Objects.equals(name, other.name)     // null-safe (กัน NPE ถ้า name = null)
        && Objects.equals(color, other.color);
}

💡 Java 16+ shortcut (pattern matching): if (!(obj instanceof Dog other)) return false; รวม instanceof + cast ไว้ในบรรทัดเดียว — เรียนละเอียดบทที่ 6.2 เมื่อเข้าใจ casting แล้ว

📜 equals contract — กฎเหล็ก 5 ข้อ (Effective Java Item 10): สั้น ๆ คือ เทียบตัวเองต้องจริง / A=B แล้ว B=A ต้องจริง / สืบต่อกันได้ / คงเส้นคงวา / ไม่เทียบ null

  1. Reflexive (สะท้อน) — x.equals(x) ต้อง true เสมอ
  2. Symmetric (สมมาตร) — ถ้า a.equals(b) จริง แล้ว b.equals(a) ก็ต้องจริงด้วย รูปแบบทางการ: x.equals(y)y.equals(x) (กัน asymmetric เช่น Date.equals(Timestamp) ใน JDK เก่า)
  3. Transitive (ส่งต่อ) — x.equals(y) + y.equals(z)x.equals(z)
  4. Consistent (สอดคล้อง) — เรียกหลายครั้งคืนค่าเดิมตลอด (ถ้า field ไม่เปลี่ยน)
  5. Non-null (ไม่ใช่ null) — x.equals(null) ต้อง false (ไม่ throw NPE)

🟡 ขั้นกลาง — ข้ามได้รอบแรก: กล่องข้างล่างนี้ถกเรื่องเลือกใช้ instanceof หรือ getClass() ใน equals และพูดถึง record (บทที่ 10) กับหลัก Liskov (บทถัดไป) ซึ่งลึกกว่าที่ต้องรู้ตอนนี้ ถ้าเพิ่งเขียน equals ครั้งแรก ข้ามไปทำ Checkpoint ท้ายบทได้เลย แล้วค่อยกลับมาอ่านทีหลัง

⚠️ instanceof Dog vs getClass() != obj.getClass() — debate คลาสสิค:

  • instanceof → subclass ของ Dog ถือว่า equal ได้ → แต่ "symmetric" อาจพัง: Dog.equals(poodle) = true เพราะ poodle instanceof Dog แต่ถ้า Poodle เพิ่ม field และ override equals → Poodle.equals(dog) = false → ละเมิด symmetric contract
  • getClass() == → strict type match → ปลอดภัย symmetric แต่ทำลายหลัก Liskov (หลักว่า class ลูกแทน class แม่ได้โดยไม่เกิดผลแปลก — เดี๋ยวจะเรียนในบทถัดไป)
  • แนวทาง 2026:
    • ถ้าต้องการ value semantics ที่ถูกต้องสมบูรณ์ → ใช้ record (Java 16+) ที่ compiler gen equals/hashCode ให้ถูกต้องโดยอัตโนมัติ
    • ถ้าต้องใช้ class ปกติ (mutable state) → ใช้ final class เพื่อตัด subclass ทิ้ง แล้วใช้ instanceof ได้ปลอดภัย (แต่ final class แก้ด้วยการห้าม subclass — ไม่ได้ทำ instanceof ถูกต้องในลำดับชั้น)
    • ถ้าต้องการลำดับชั้น (hierarchy) → ใช้ getClass() เพื่อรับประกัน symmetric แต่ยอมรับว่าละเมิด Liskov

10.3 hashCode — สำหรับ HashMap/HashSet

📖 HashMap/HashSet คือที่เก็บข้อมูลแบบค้นหาเร็ว (เรียนบทที่ 7) — มันใช้ hashCode เป็น "เลขช่อง" ในการหา object

ถ้า override equalsต้อง override hashCode ด้วย — กฎของ Java

⚠️ equals/hashCode contract: ถ้า x.equals(y) == truex.hashCode() == y.hashCode() ต้อง เป็นจริง (ตรงข้ามไม่จริง — 2 object hashCode เหมือนกัน อาจไม่ equal ได้) ถ้าผิด contract นี้ — HashMap/HashSet จะ "หา" object ที่ใส่ลงไปแล้วไม่เจอ (bug หายากมาก)

java
@Override
public int hashCode() {
    return Objects.hash(name, age, color);             // สะดวก — boxing ทุก field
}

สำหรับ business code ทั่วไป — ใช้ Objects.hash ก็พอ (อ่านง่ายกว่ามาก)

🔧 Objects.hash performance note — สำหรับ hot path (ขั้นสูง ข้ามได้)

Objects.hash(...) ใช้ varargs → boxing (การห่อค่า primitive เช่น int ให้เป็น object Integer) ทุก primitive + array allocation ทุกครั้ง — ใน hot path (เส้นทางที่ถูกเรียกบ่อยมาก เช่น คีย์ของ HashMap ที่ put/get ล้านครั้ง/วินาที) ให้เขียนเอง:

java
public int hashCode() {
    int h = 1;
    h = 31 * h + (name == null ? 0 : name.hashCode());
    h = 31 * h + age;   // int ใช้ตรง ๆ ได้เลย (Integer.hashCode(age) ก็ได้ — คืนค่าเดิมทุกครั้ง เป็น identity)
                        // ถ้าเป็น long ใช้ Long.hashCode(age), double ใช้ Double.hashCode(d)
    h = 31 * h + (color == null ? 0 : color.hashCode());
    return h;
}

template นี้ใช้ได้กับ int + reference field — ถ้ามี long/double/float ต้องใช้ Long.hashCode / Double.hashCode / Float.hashCode แทน — ดูรายละเอียดเพิ่มเติมบทที่ 16/19

10.4 ใช้ IDE generate

IntelliJ: คลิกขวาในไฟล์ → Generateequals() and hashCode() → เลือก field → done

ไม่ต้องเขียนเอง — IDE ทำให้

แต่ ใน Java 16+ มีของใหม่ที่ดีกว่า: record — auto-gen ทุกอย่างให้ (เดี๋ยวบทที่ 10)

⚠️ record เหมาะกับ immutable data holder เท่านั้น (เช่น DTO, Value Object) — field ของ record เป็น final ทั้งหมด ดังนั้น class ที่มี mutable state เช่น BankAccount (balance เปลี่ยนได้) ไม่เหมาะใช้ record


11. Checkpoint

🛠️ Checkpoint 5.1 — class BankAccount

สร้าง class BankAccount:

  • field: accountNumber (String, final), ownerName (String), balance (double, default 0)
  • constructor: รับ accountNumber + ownerName
  • method: deposit(amount), withdraw(amount) (เช็คเงินพอ), getBalance()
  • field private ทั้งหมด, มี getter

ใน main: สร้าง 2 account, ฝาก/ถอน, print balance

📖 เฉลย

🔴 ระวัง — อย่าใช้ double กับระบบการเงินจริง: ตัวอย่างนี้ใช้ double เพื่อความง่ายในการเรียน แต่ double มี floating-point error เช่น 0.1 + 0.2 != 0.3 ซึ่งอันตรายในระบบการเงิน — ในงานจริงต้องใช้ java.math.BigDecimal เสมอ:

java
import java.math.BigDecimal;
private BigDecimal balance = new BigDecimal("0.00");
java
// BankAccount.java
public class BankAccount {
    private final String accountNumber;
    private String ownerName;
    private double balance;   // ⚠️ ดู warning ด้านบน — ใน production ใช้ BigDecimal
    
    public BankAccount(String accountNumber, String ownerName) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
        this.balance = 0.0;
    }
    
    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        balance += amount;
    }
    
    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        if (amount > balance) {
            throw new IllegalStateException("Insufficient funds");
        }
        balance -= amount;
    }
    
    public String getAccountNumber() { return accountNumber; }
    public String getOwnerName() { return ownerName; }
    public double getBalance() { return balance; }
    
    @Override
    public String toString() {
        return "Account{" + accountNumber + ", " + ownerName + ", balance=" + balance + "}";
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        BankAccount a = new BankAccount("A001", "Anna");
        BankAccount b = new BankAccount("B001", "Ben");
        
        a.deposit(1000);
        b.deposit(500);
        a.withdraw(200);
        
        System.out.println(a);   // Account{A001, Anna, balance=800.0}
        System.out.println(b);   // Account{B001, Ben, balance=500.0}
    }
}

🛠️ Checkpoint 5.2 — เพิ่ม transfer

เพิ่ม method transferTo(BankAccount other, double amount) ที่:

  • ถอนจาก this
  • ฝากใน other
📖 เฉลย
java
public void transferTo(BankAccount other, double amount) {
    this.withdraw(amount);    // ถ้าเงินไม่พอจะ throw ที่นี่
    other.deposit(amount);
}

🛠️ Checkpoint 5.3 — Counter

สร้าง class Counter ที่นับจำนวน object ทั้งหมดที่สร้าง:

  • static field count
  • constructor เพิ่ม count
  • static method getCount()
📖 เฉลย
java
public class Counter {
    private static int count = 0;
    
    public Counter() {
        count++;
    }
    
    public static int getCount() {
        return count;
    }
}

// Main
new Counter();
new Counter();
new Counter();
System.out.println(Counter.getCount());   // 3

🛠️ Checkpoint 5.4 (ท้าทาย) — class Library

สร้างระบบห้องสมุดเล็ก ๆ:

  • class Book (title, author, isAvailable)
  • class Library (มี Book[] หรือ ArrayList<Book> — ใช้ List ได้)
    • method addBook(Book), borrowBook(String title), returnBook(String title), listAvailable()

(ใช้ ArrayList — เรียนละเอียดบทที่ 7 — import java.util.ArrayList; new ArrayList<>();)

📖 เฉลย
java
// ===== ไฟล์ Book.java =====
public class Book {
    private final String title;
    private final String author;
    private boolean isAvailable;
    
    public Book(String title, String author) {
        this.title = title;
        this.author = author;
        this.isAvailable = true;
    }
    
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public boolean isAvailable() { return isAvailable; }
    public void setAvailable(boolean a) { this.isAvailable = a; }
    
    @Override
    public String toString() {
        return "\"" + title + "\" by " + author + (isAvailable ? " [available]" : " [borrowed]");
    }
}

// ===== ไฟล์ Library.java =====
// หมายเหตุ: import ข้างล่างใช้ได้เมื่อ Library และ Book อยู่ใน package เดียวกัน (หรือ default package)
// ถ้าอยู่คนละ package (เช่น com.example.model.Book) ต้องเพิ่ม import com.example.model.Book; ด้วย
import java.util.ArrayList;
import java.util.List;

public class Library {
    private List<Book> books = new ArrayList<>();
    
    public void addBook(Book book) {
        books.add(book);
    }
    
    public void borrowBook(String title) {
        for (Book b : books) {
            if (b.getTitle().equals(title) && b.isAvailable()) {
                b.setAvailable(false);
                System.out.println("Borrowed: " + title);
                return;
            }
        }
        System.out.println("Book not available: " + title);
    }
    
    public void returnBook(String title) {
        for (Book b : books) {
            if (b.getTitle().equals(title) && !b.isAvailable()) {
                b.setAvailable(true);
                System.out.println("Returned: " + title);
                return;
            }
        }
    }
    
    public void listAvailable() {
        for (Book b : books) {
            if (b.isAvailable()) System.out.println(b);
        }
    }
}

// ===== ไฟล์ Main.java =====
public class Main {
    public static void main(String[] args) {
        Library lib = new Library();
        lib.addBook(new Book("1984", "Orwell"));
        lib.addBook(new Book("Brave New World", "Huxley"));
        
        lib.listAvailable();    // 2 books
        lib.borrowBook("1984");
        lib.listAvailable();    // 1 book
        lib.returnBook("1984");
        lib.listAvailable();    // 2 books
    }
}

12. สรุปบท

✅ Class = blueprint, Object = instance จริง — สร้างด้วย new ✅ Field = state, Method = behavior — รวมไว้ในที่เดียว ✅ Constructor = method พิเศษ run ตอนสร้าง — ใช้ this แยก field กับ parameter ✅ Encapsulation: field private + getter/setter + validate ใน setter ✅ Access modifier: public, private, protected, package-private ✅ static = ของ class ไม่ใช่ของ object ✅ final field = ตั้งครั้งเดียว, final class = extend ไม่ได้ ✅ แยก class เป็นไฟล์ + ใช้ package จัดหมวด ✅ Override toString, equals, hashCode (ใช้ IDE generate ได้ — หรือใช้ record ในบทที่ 10)

→ ไปบทที่ 6: Inheritance