โหมดมืด
บทที่ 5 — OOP พื้นฐาน (Class, Object, Encapsulation)
บทนี้คือ หัวใจของ 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 คนอยู่หลายที่
- error-prone (เกิด error ได้ง่าย) — ลืม sync index, ลืม update balance
- logic ฝังในที่ที่ใช้ —
balance += 1000กระจาย ใครจะเช็คว่าฝากเกิน limit? - ขยายไม่ได้ — เพิ่ม 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 ใกล้ data —
transferToรู้จัก balance โดยตรง - clear semantics —
anna.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 ไม่กระทบ yourCar4. สร้าง 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=— assignmentnew Dog()— สร้าง object ใหม่ของ classDog
ภายในเครื่อง:
- Java ขอ memory สำหรับ Dog object ใหม่
- ตั้งค่า field default (String = null, int = 0, ...)
- คืน "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 เดียวกันได้:
javaDog 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 เป็น
publicconstructor ที่ 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()); // 4Access modifiers ที่ Java มี
📖 package = โฟลเดอร์ที่ใช้จัดกลุ่ม class — รายละเอียดเต็มอยู่ใน section 9
| Modifier | เข้าถึงจากที่ไหน |
|---|---|
public | ทุกที่ |
protected | class ใน package เดียวกัน + subclass (class ลูก — เรียนบทที่ 6) ในทุก package |
| (ไม่ใส่) | "package-private" — class ใน package เดียวกันเท่านั้น |
private | เฉพาะ class นี้ |
กฎ Encapsulation ที่ดี
- field เป็น
privateเสมอ (ยกเว้น constantpublic static final) - เปิด getter/setter เฉพาะที่จำเป็น
- setter ตรวจ validate ทุกครั้ง
- 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
📖 กฎเพิ่มเติม:
finalfield ต้องถูกกำหนดค่าครบทุกทางที่ 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 errorclass สำเร็จรูปของ 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 finalfield ที่ต้อง 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
- Reflexive (สะท้อน) —
x.equals(x)ต้อง true เสมอ- Symmetric (สมมาตร) — ถ้า
a.equals(b)จริง แล้วb.equals(a)ก็ต้องจริงด้วย รูปแบบทางการ:x.equals(y)↔y.equals(x)(กัน asymmetric เช่นDate.equals(Timestamp)ใน JDK เก่า)- Transitive (ส่งต่อ) —
x.equals(y)+y.equals(z)→x.equals(z)- Consistent (สอดคล้อง) — เรียกหลายครั้งคืนค่าเดิมตลอด (ถ้า field ไม่เปลี่ยน)
- Non-null (ไม่ใช่ null) —
x.equals(null)ต้อง false (ไม่ throw NPE)🟡 ขั้นกลาง — ข้ามได้รอบแรก: กล่องข้างล่างนี้ถกเรื่องเลือกใช้
instanceofหรือgetClass()ในequalsและพูดถึง record (บทที่ 10) กับหลัก Liskov (บทถัดไป) ซึ่งลึกกว่าที่ต้องรู้ตอนนี้ ถ้าเพิ่งเขียนequalsครั้งแรก ข้ามไปทำ Checkpoint ท้ายบทได้เลย แล้วค่อยกลับมาอ่านทีหลัง⚠️
instanceof DogvsgetClass() != obj.getClass()— debate คลาสสิค:
instanceof→ subclass ของ Dog ถือว่า equal ได้ → แต่ "symmetric" อาจพัง: Dog.equals(poodle) = true เพราะ poodle instanceof Dog แต่ถ้า Poodle เพิ่ม field และ override equals → Poodle.equals(dog) = false → ละเมิด symmetric contractgetClass() ==→ strict type match → ปลอดภัย symmetric แต่ทำลายหลัก Liskov (หลักว่า class ลูกแทน class แม่ได้โดยไม่เกิดผลแปลก — เดี๋ยวจะเรียนในบทถัดไป)- แนวทาง 2026:
- ถ้าต้องการ value semantics ที่ถูกต้องสมบูรณ์ → ใช้
record(Java 16+) ที่ compiler genequals/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/hashCodecontract: ถ้าx.equals(y) == true→x.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: คลิกขวาในไฟล์ → Generate → equals() 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เสมอ:javaimport 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()
- method
(ใช้ 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)