บทนำ: ปัญหาของ Mutable Objects
Mutable = สามารถเปลี่ยนแปลงได้
java// ❌ ปัญหา: Object ที่ mutable
public class Point {
public double x;
public double y;
}
// สร้าง point
Point p1 = new Point();
p1.x = 10;
p1.y = 20;
// ส่งให้ function อื่น
printPoint(p1);
// ❌ ใครก็เปลี่ยนได้!
p1.x = 999;
p1.y = -999;
// ❌ ปัญหา: Function คาดหวังค่าเดิม แต่เปลี่ยนไปแล้ว
ปัญหา:
- ❌ Object ถูกเปลี่ยนโดยไม่ตั้งใจ
- ❌ ไม่ปลอดภัยใน multi-threading
- ❌ Bug ยากต่อการ debug
- ❌ ไม่สามารถ cache ได้
Immutable Object คืออะไร?
Immutable Object = Object ที่ไม่สามารถเปลี่ยนแปลงได้ หลังจากสร้าง
textCharacteristics:
1. ไม่มี setter
2. Attributes เป็น private final
3. Constructor initialize เมื่อสร้าง
4. ไม่สามารถเปลี่ยนค่าได้ หลังจากนั้น
ยาวไปอยากเลิกอ่าน
- บทนำ: ปัญหาของ Mutable Objects
- Immutable Object คืออะไร?
- Immutable Object Design Pattern
- เกณฑ์ที่ต้องปฏิบัติ
- ตัวอย่างที่ 1: String เป็น Immutable
- ตัวอย่างที่ 2: Immutable Point Class
- ตัวอย่างที่ 3: Immutable Date Class
- ตัวอย่างที่ 4: Immutable Person ด้วย Collections
- Mutable vs Immutable
- ตารางเปรียบเทียบ
- Safe Class Design Principles
- 1. Principle of Least Privilege
- 2. Encapsulation
- 3. Defense Against Mutation
- ตัวอย่างที่ 5: Design Safe Money Class
- ตัวอย่างที่ 6: Immutable Address ด้วย Complex Logic
- Benefits ของ Immutable Objects
- Immutable ใน Java
- Java Built-in Immutable Classes
- ตัวอย่างการใช้
- Best Practices: Design Safe Classes
- Checklist
- ตัวอย่างสุดท้าย: Complete Immutable Person Class
- สรุป: Immutable Object & Safe Design
Immutable Object Design Pattern
เกณฑ์ที่ต้องปฏิบัติ
text1. ✓ ทุก attributes เป็น private final
2. ✓ ไม่มี setter
3. ✓ Constructor initialize ทั้งหมด
4. ✓ Mutable attributes ต้อง deep copy
5. ✓ Getter ต้อง safe (ไม่ return reference เลย)
ตัวอย่างที่ 1: String เป็น Immutable
java// String ใน Java เป็น immutable
String name = "John";
// ✓ ดูเหมือน change ค่า
String newName = name.toUpperCase();
// ❓ แต่จริงๆ สร้าง object ใหม่
System.out.println(name); // "John" (ไม่เปลี่ยน)
System.out.println(newName); // "JOHN" (object ใหม่)
// ✓ ไม่สามารถ change ได้
// name[0] = 'j'; // ERROR! ไม่มี method
// ✓ String reference เดียวกัน → value เดียวกัน
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true (same object)
// ✓ ปลอดภัยใน multi-threading
ตัวอย่างที่ 2: Immutable Point Class
java// ✓ Immutable Point
public class ImmutablePoint {
// ✓ private final
private final double x;
private final double y;
// Constructor (เดียวทาง initialize)
public ImmutablePoint(double x, double y) {
this.x = x;
this.y = y;
}
// ✓ Getter (ไม่มี setter)
public double getX() {
return this.x;
}
public double getY() {
return this.y;
}
// ✓ ไม่สามารถ change ได้
// ถ้าต้องการค่าใหม่ → สร้าง object ใหม่
public ImmutablePoint move(double dx, double dy) {
return new ImmutablePoint(this.x + dx, this.y + dy);
}
public void displayInfo() {
System.out.printf("Point: (%.2f, %.2f)\n", x, y);
}
}
// ใช้งาน
public class Main {
public static void main(String[] args) {
ImmutablePoint p1 = new ImmutablePoint(10, 20);
p1.displayInfo(); // Point: (10.00, 20.00)
// ✓ ต้องการจุดใหม่ → สร้าง object ใหม่
ImmutablePoint p2 = p1.move(5, 10);
p2.displayInfo(); // Point: (15.00, 30.00)
// ✓ p1 ไม่เปลี่ยน
p1.displayInfo(); // Point: (10.00, 20.00)
// ❌ ไม่มี setter
// p1.setX(100); // ERROR!
}
}
ตัวอย่างที่ 3: Immutable Date Class
javaimport java.time.LocalDate;
// ✓ Immutable Date
public class ImmutableDate {
private final int day;
private final int month;
private final int year;
public ImmutableDate(int day, int month, int year) {
if (day < 1 || day > 31) {
throw new IllegalArgumentException("Invalid day");
}
if (month < 1 || month > 12) {
throw new IllegalArgumentException("Invalid month");
}
this.day = day;
this.month = month;
this.year = year;
}
public int getDay() {
return this.day;
}
public int getMonth() {
return this.month;
}
public int getYear() {
return this.year;
}
// ✓ Methods ที่ return new object
public ImmutableDate addDays(int days) {
// Simplified (ไม่คำนวณแบบ real date)
int newDay = this.day + days;
return new ImmutableDate(newDay, this.month, this.year);
}
public void displayInfo() {
System.out.printf("%d/%d/%d\n", day, month, year);
}
}
// ใช้งาน
public class Main {
public static void main(String[] args) {
ImmutableDate d1 = new ImmutableDate(25, 10, 2024);
d1.displayInfo(); // 25/10/2024
// ✓ Add days → new object
ImmutableDate d2 = d1.addDays(10);
d2.displayInfo(); // 35/10/2024 (simplified)
// ✓ d1 ไม่เปลี่ยน
d1.displayInfo(); // 25/10/2024
}
}
ตัวอย่างที่ 4: Immutable Person ด้วย Collections
javaimport java.util.ArrayList;
import java.util.Collections;
import java.util.List;
// ✓ Immutable Person
public class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies; // reference type
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
// ✓ Deep copy + unmodifiable
this.hobbies = Collections.unmodifiableList(
new ArrayList<>(hobbies)
);
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
// ✓ Return unmodifiable list
public List<String> getHobbies() {
return this.hobbies;
}
public void displayInfo() {
System.out.printf("Name: %s | Age: %d | Hobbies: %s\n",
name, age, hobbies);
}
}
// ใช้งาน
public class Main {
public static void main(String[] args) {
List<String> hobbyList = new ArrayList<>();
hobbyList.add("Reading");
hobbyList.add("Gaming");
ImmutablePerson person = new ImmutablePerson("John", 25, hobbyList);
person.displayInfo();
// ✓ ปรับเปลี่ยน original list
hobbyList.add("Swimming");
// ✓ Person ไม่เปลี่ยน (deep copy)
person.displayInfo();
// ❌ ไม่สามารถ add hobby
try {
person.getHobbies().add("Cooking"); // ERROR!
} catch (Exception e) {
System.out.println("Error: Cannot modify hobbies");
}
}
}
Output:
textName: John | Age: 25 | Hobbies: [Reading, Gaming]
Name: John | Age: 25 | Hobbies: [Reading, Gaming]
Error: Cannot modify hobbies
Mutable vs Immutable
ตารางเปรียบเทียบ
text┌──────────────┬──────────────────────┬──────────────────────┐
│ │ Mutable │ Immutable │
├──────────────┼──────────────────────┼──────────────────────┤
│ Setter │ มี │ ไม่มี │
│ Attributes │ public / private │ private final │
│ Change │ setX(), setY() │ return new object │
│ Safety │ ต่ำ │ สูง │
│ Threading │ ต้อง synchronize │ thread-safe │
│ Cache │ ไม่ได้ │ ได้ │
│ Example │ StringBuilder │ String, Integer │
└──────────────┴──────────────────────┴──────────────────────┘
Safe Class Design Principles
1. Principle of Least Privilege
java// ✓ ดี: เปิดเผยน้อยที่สุด
public class Account {
private double balance; // private
public double getBalance() { // public getter
return this.balance;
}
public void withdraw(double amt) { // business method
if (amt > this.balance) {
throw new Exception("Insufficient");
}
this.balance -= amt;
}
// ไม่มี setBalance()
}
// ❌ ไม่ดี: เปิดเผยมากเกินไป
public class Account {
public double balance; // public - ใครก็เปลี่ยนได้
public void setBalance(double amt) { // direct setter
this.balance = amt; // ไม่มี validation
}
}
2. Encapsulation
java// ✓ ดี: Encapsulation
public class Rectangle {
private double width;
private double height;
public void setWidth(double w) {
if (w <= 0) {
throw new IllegalArgumentException("Width must be positive");
}
this.width = w; // ✓ validation ทำงาน
}
}
// ❌ ไม่ดี: Direct access
public class Rectangle {
public double width;
// width = -5; // ❌ Invalid state!
}
3. Defense Against Mutation
java// ✓ ดี: Prevent mutation
public class Config {
private final Map<String, String> settings;
public Config(Map<String, String> data) {
// ✓ Deep copy
this.settings = Collections.unmodifiableMap(
new HashMap<>(data)
);
}
public String getSetting(String key) {
return this.settings.get(key);
}
// ไม่มี setSetting()
}
// ❌ ไม่ดี: Vulnerable
public class Config {
private Map<String, String> settings;
public Map<String, String> getSettings() {
return this.settings; // ❌ ใครก็เปลี่ยนได้
}
}
ตัวอย่างที่ 5: Design Safe Money Class
java// ✓ Immutable Money
public class Money {
private final double amount;
private final String currency;
public Money(double amount, String currency) {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
if (currency == null || currency.isEmpty()) {
throw new IllegalArgumentException("Currency required");
}
this.amount = amount;
this.currency = currency;
}
public double getAmount() {
return this.amount;
}
public String getCurrency() {
return this.currency;
}
// ✓ ไม่สามารถ change ได้
// ถ้าต้องการรวมเงิน → return new Money
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Different currencies");
}
return new Money(this.amount + other.amount, this.currency);
}
// ✓ ไม่สามารถ change ได้
// ถ้าต้องการหักเงิน → return new Money
public Money subtract(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Different currencies");
}
double result = this.amount - other.amount;
if (result < 0) {
throw new IllegalArgumentException("Result would be negative");
}
return new Money(result, this.currency);
}
@Override
public String toString() {
return String.format("%.2f %s", amount, currency);
}
}
// ใช้งาน
public class Main {
public static void main(String[] args) {
Money m1 = new Money(1000, "THB");
Money m2 = new Money(500, "THB");
System.out.println("m1 = " + m1); // 1000.00 THB
System.out.println("m2 = " + m2); // 500.00 THB
// ✓ Add
Money m3 = m1.add(m2);
System.out.println("m1 + m2 = " + m3); // 1500.00 THB
// ✓ m1, m2 ไม่เปลี่ยน
System.out.println("m1 = " + m1); // 1000.00 THB
System.out.println("m2 = " + m2); // 500.00 THB
// ❌ ไม่มี setAmount()
// m1.setAmount(9999); // ERROR!
}
}
Output:
textm1 = 1000.00 THB
m2 = 500.00 THB
m1 + m2 = 1500.00 THB
m1 = 1000.00 THB
m2 = 500.00 THB
ตัวอย่างที่ 6: Immutable Address ด้วย Complex Logic
java// ✓ Immutable Address
public class ImmutableAddress {
private final String street;
private final String city;
private final String country;
private final String zipCode;
public ImmutableAddress(String street, String city, String country, String zip) {
if (street == null || street.isEmpty()) {
throw new IllegalArgumentException("Street required");
}
if (city == null || city.isEmpty()) {
throw new IllegalArgumentException("City required");
}
if (country == null || country.isEmpty()) {
throw new IllegalArgumentException("Country required");
}
if (!zip.matches("\\d{5}")) {
throw new IllegalArgumentException("Invalid zip code");
}
this.street = street;
this.city = city;
this.country = country;
this.zipCode = zip;
}
public String getStreet() {
return this.street;
}
public String getCity() {
return this.city;
}
public String getCountry() {
return this.country;
}
public String getZipCode() {
return this.zipCode;
}
// ✓ Factory method → return new object
public ImmutableAddress updateCity(String newCity) {
return new ImmutableAddress(
this.street,
newCity,
this.country,
this.zipCode
);
}
@Override
public String toString() {
return String.format("%s, %s, %s %s",
street, city, country, zipCode);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ImmutableAddress)) {
return false;
}
ImmutableAddress other = (ImmutableAddress) obj;
return this.street.equals(other.street) &&
this.city.equals(other.city) &&
this.country.equals(other.country) &&
this.zipCode.equals(other.zipCode);
}
}
// ใช้งาน
public class Main {
public static void main(String[] args) {
ImmutableAddress addr1 =
new ImmutableAddress("123 Main St", "Bangkok", "Thailand", "10110");
System.out.println("Address 1: " + addr1);
// ✓ Change city → new object
ImmutableAddress addr2 = addr1.updateCity("Chiang Mai");
System.out.println("Address 2: " + addr2);
// ✓ Original unchanged
System.out.println("Address 1: " + addr1);
// ✓ Can be used as HashMap key (because immutable)
System.out.println("Same? " + addr1.equals(addr2)); // false
}
}
Output:
textAddress 1: 123 Main St, Bangkok, Thailand 10110
Address 2: 123 Main St, Chiang Mai, Thailand 10110
Address 1: 123 Main St, Bangkok, Thailand 10110
Same? false
Benefits ของ Immutable Objects
text┌────────────────────────────────────────────┐
│ Benefits ของ Immutable Objects │
├────────────────────────────────────────────┤
│ │
│ 1. Thread Safety │
│ └─ ไม่ต้อง synchronize │
│ │
│ 2. Simpler Code │
│ └─ ไม่ต้อง worry mutation │
│ │
│ 3. Can Use As HashMap Key │
│ └─ hashCode ไม่เปลี่ยน │
│ │
│ 4. Caching │
│ └─ Value ไม่เปลี่ยน → cache ได้ │
│ │
│ 5. Easier Testing │
│ └─ Predictable behavior │
│ │
│ 6. String Interning │
│ └─ Memory optimization │
│ │
└────────────────────────────────────────────┘
Immutable ใน Java
Java Built-in Immutable Classes
text- String
- Integer, Long, Double, Boolean (wrapper classes)
- BigInteger, BigDecimal
- LocalDate, LocalTime, LocalDateTime
- Collections.unmodifiableList/Map/Set
ตัวอย่างการใช้
java// String immutable
String s = "Hello";
s = s.toUpperCase(); // ✓ ไม่ change ต้องเดิม, return new String
// Integer immutable
Integer i = 10;
Integer j = i + 5; // ✓ return new Integer
// LocalDate immutable
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1); // ✓ return new LocalDate
Best Practices: Design Safe Classes
Checklist
text┌─────────────────────────────────────────┐
│ Safe Class Design Checklist │
├─────────────────────────────────────────┤
│ │
│ ✓ All fields private final │
│ ✓ No setters │
│ ✓ Constructor validates input │
│ ✓ Defensive copy for mutable fields │
│ ✓ Getters return safe copies │
│ ✓ Methods return new objects (not this) │
│ ✓ No public/static mutable collections │
│ ✓ Override equals() and hashCode() │
│ ✓ Prevent extension (final class) │
│ ✓ Document immutability │
│ │
└─────────────────────────────────────────┘
ตัวอย่างสุดท้าย: Complete Immutable Person Class
javaimport java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
// ✓ Fully Immutable Person
public final class ImmutablePerson { // final = ไม่ extend
private final String name;
private final int age;
private final LocalDate birthDate;
private final List<String> certifications;
public ImmutablePerson(String name, int age, LocalDate birthDate,
List<String> certifications) {
// Validation
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name required");
}
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age");
}
if (birthDate == null) {
throw new IllegalArgumentException("Birth date required");
}
this.name = name;
this.age = age;
this.birthDate = birthDate;
// ✓ Deep copy + unmodifiable
this.certifications = Collections.unmodifiableList(
new ArrayList<>(certifications)
);
}
// Getters only
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public LocalDate getBirthDate() {
return this.birthDate;
}
public List<String> getCertifications() {
return this.certifications;
}
// ✓ Factory methods (return new objects)
public ImmutablePerson withName(String newName) {
return new ImmutablePerson(newName, this.age,
this.birthDate, this.certifications);
}
public ImmutablePerson withCertification(String cert) {
List<String> newCerts = new ArrayList<>(this.certifications);
newCerts.add(cert);
return new ImmutablePerson(this.name, this.age,
this.birthDate, newCerts);
}
@Override
public String toString() {
return String.format("Person{name='%s', age=%d, birth=%s, certs=%s}",
name, age, birthDate, certifications);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ImmutablePerson)) {
return false;
}
ImmutablePerson other = (ImmutablePerson) obj;
return this.name.equals(other.name) &&
this.age == other.age &&
this.birthDate.equals(other.birthDate) &&
this.certifications.equals(other.certifications);
}
@Override
public int hashCode() {
return name.hashCode() * 31 + age;
}
}
// ใช้งาน
public class Main {
public static void main(String[] args) {
LocalDate dob = LocalDate.of(1990, 5, 15);
List<String> certs = new ArrayList<>();
certs.add("Java");
certs.add("OOP");
ImmutablePerson person1 =
new ImmutablePerson("John", 34, dob, certs);
System.out.println("Person 1: " + person1);
// ✓ Add certification → new object
ImmutablePerson person2 = person1.withCertification("Design Pattern");
System.out.println("Person 2: " + person2);
// ✓ Original unchanged
System.out.println("Person 1: " + person1);
// ✓ Can use as HashMap key
System.out.println("Can cache: yes (immutable)");
}
}
Output:
textPerson 1: Person{name='John', age=34, birth=1990-05-15, certs=[Java, OOP]}
Person 2: Person{name='John', age=34, birth=1990-05-15, certs=[Java, OOP, Design Pattern]}
Person 1: Person{name='John', age=34, birth=1990-05-15, certs=[Java, OOP]}
Can cache: yes (immutable)
สรุป: Immutable Object & Safe Design
text┌──────────────────────────────────────────────┐
│ Immutable Object & Safe Class Design │
├──────────────────────────────────────────────┤
│ │
│ IMMUTABLE OBJECT │
│ ├─ ไม่สามารถเปลี่ยนแปลงได้ │
│ ├─ ทุก attributes private final │
│ ├─ ไม่มี setter │
│ └─ Methods return new objects │
│ │
│ DESIGN PRINCIPLES │
│ ├─ Principle of Least Privilege │
│ ├─ Encapsulation │
│ ├─ Defense Against Mutation │
│ ├─ Fail-Fast (throw exception early) │
│ └─ Stateless Design │
│ │
│ SAFETY TECHNIQUES │
│ ├─ Input validation ใน constructor │
│ ├─ Deep copy สำหรับ reference types │
│ ├─ Return unmodifiable collections │
│ ├─ Override equals() + hashCode() │
│ └─ Final class (prevent extension) │
│ │
│ BENEFITS │
│ ├─ Thread-safe (no synchronization) │
│ ├─ Simpler code │
│ ├─ Can use as HashMap key │
│ ├─ Easier testing │
│ └─ Memory optimization (caching) │
│ │
│ EXAMPLES │
│ ├─ String │
│ ├─ Integer, Long, Double │
│ ├─ LocalDate, LocalTime │
│ └─ Custom: Money, Address, etc │
│ │
└──────────────────────────────────────────────┘
