Java OOP ตั้งอยู่บนหลัก 4 ข้อ ได้แก่ encapsulation, inheritance, polymorphism และ abstraction โดย encapsulation ปกป้องข้อมูลภายใน object, inheritance ให้ class หนึ่งนำโค้ดของอีก class มาใช้ต่อได้, polymorphism ทำให้การเรียก method เดียวกันทำงานต่างกันไปตาม object ที่อยู่เบื้องหลังจริง ส่วน abstraction ทำให้โค้ดพึ่งพาแค่ว่าอะไรบางอย่าง ทำอะไรได้ โดยไม่ต้องรู้ว่า ทำอย่างไร แต่ละข้อแก้ปัญหาคนละแบบ และถ้าใช้มากเกินไปก็สร้างปัญหาใหม่ได้เหมือนกัน
บทความนี้ถือว่าคุณเขียน class, constructor และ method เป็นแล้ว ถ้ายังไม่คล่อง แนะนำให้อ่าน class, object และ constructor ใน Java ก่อน ตัวอย่างทั้งหมด compile ได้บน Java 17 ขึ้นไป
Encapsulation ใน Java
Encapsulation คือการให้ object เก็บ field ไว้เป็น private แล้วเปิดให้ภายนอกใช้ได้เฉพาะการกระทำที่สมเหตุสมผล โค้ดส่วนอื่นจะเข้ามาแก้ค่าจนทำให้ object อยู่ในสถานะผิดไม่ได้ ต้องผ่าน method ที่ class ควบคุมไว้เท่านั้น
แนวคิดที่ใกล้กันคือ information hiding ผู้เรียกไม่จำเป็นต้องรู้ว่าข้างใน class ทำงานอย่างไร ถ้าเขาใช้แค่ public method เราก็เปลี่ยนโค้ดข้างในทีหลังได้โดยไม่ทำให้ใครพัง
public class BankAccount {
private final String owner;
private long balance; // in the smallest currency unit, e.g. satang
public BankAccount(String owner) {
this.owner = Objects.requireNonNull(owner);
}
public String getOwner() { return owner; }
public long getBalance() { return balance; }
public void deposit(long amount) {
if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
balance += amount;
}
public void withdraw(long amount) {
if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
if (amount > balance) throw new IllegalStateException("insufficient funds");
balance -= amount;
}
}สังเกตว่าไม่มี setBalance() ยอดเงินเปลี่ยนได้ผ่าน deposit กับ withdraw เท่านั้น และทั้งสอง method ก็ตรวจกฎไว้ครบ
Getter และ setter
Method ที่อ่านค่า field เรียกว่า getter (accessor) ส่วน method ที่เปลี่ยนค่าเรียกว่า setter (mutator) ทั้งสองเป็นวิธีมาตรฐานในการเปิดข้อมูลที่ถูก encapsulate ไว้ แต่การมี getter/setter ไม่ได้แปลว่า encapsulate แล้ว field ที่เป็น private แต่มีทั้ง getter และ setter แบบ public ก็แทบไม่ต่างจาก public field หลักที่ควรใช้คือ:
- เพิ่ม getter เฉพาะเมื่อโค้ดภายนอกต้องใช้ค่านั้นจริง ๆ
- เพิ่ม setter เฉพาะเมื่อการเปลี่ยนค่าตรง ๆ เป็นการกระทำที่ถูกต้อง และตรวจสอบ input ข้างในเสมอ
- ตั้งชื่อ method ตามการกระทำจริง (
withdraw,rename,cancel) ดีกว่า setter ทั่ว ๆ ไป - ถ้า field ไม่ควรเปลี่ยน ให้ใส่
finalแล้วไม่ต้องมี setter เลย
ตาราง access modifier
Java มีคำสั่งกำหนดสิทธิ์การเข้าถึง 3 คำ บวกระดับที่ 4 ที่ได้เมื่อไม่ใส่อะไรเลย:
| Modifier | Class เดียวกัน | Package เดียวกัน | Subclass ต่าง package | ที่ไหนก็ได้ | UML |
|---|---|---|---|---|---|
public | ได้ | ได้ | ได้ | ได้ | + |
protected | ได้ | ได้ | ได้ | ไม่ได้ | # |
| (ไม่ใส่) package-private | ได้ | ได้ | ไม่ได้ | ไม่ได้ | ~ |
private | ได้ | ไม่ได้ | ไม่ได้ | ไม่ได้ | - |
จุดที่มือใหม่มักพลาดคือ protected ให้สิทธิ์ ทุก class ใน package เดียวกัน ด้วย ไม่ได้จำกัดแค่ subclass แนะนำให้เริ่มจาก private เสมอ แล้วค่อยขยายสิทธิ์เมื่อมีเหตุผลจริง
Inheritance: extends และ super
Inheritance ให้ subclass (class ลูก) ต่อยอดจาก superclass (class แม่) ด้วย extends subclass จะได้ field และ method ที่เข้าถึงได้ของแม่มา และเพิ่มของตัวเองได้
public class Person {
protected final String name;
public Person(String name) {
this.name = name;
}
public String introduce() {
return "Hi, I'm " + name;
}
}
public class Student extends Person {
private final String studentId;
public Student(String name, String studentId) {
super(name); // call the Person constructor
this.studentId = studentId;
}
@Override
public String introduce() {
return super.introduce() + ", student " + studentId;
}
}กฎที่ควรรู้:
- Class หนึ่ง
extendsได้ แค่ class เดียว Java ไม่มี multiple inheritance ของ class แต่ implement interface ได้หลายตัว - Constructor ไม่ถูกสืบทอด constructor ของ subclass ต้องเรียก constructor ของแม่ด้วย
super(...)และ argument ต้องตรงกับ constructor ตัวใดตัวหนึ่งของแม่ - ถ้าไม่เขียน
super(...)compiler จะแทรกsuper()แบบไม่มี argument ให้เอง ถ้าแม่ไม่มี constructor แบบไม่มี parameter ก็จะ compile error ซึ่งเป็นจุดที่คนเจอบ่อย - ก่อน Java 25
super(...)ต้องเป็นคำสั่งแรกของ constructor ตั้งแต่ Java 25 วางคำสั่งอย่างการตรวจ argument ไว้ก่อนได้ ตราบใดที่ไม่อ่านค่าจาก object ที่กำลังสร้าง super.method()ใช้เรียก method เวอร์ชันของแม่ที่ถูก override ไป เหมือนที่introduce()ด้านบนทำ- Member ที่เป็น
privateใช้ใน subclass ไม่ได้ ถึงแม้มันจะอยู่ใน object จริง ๆ ก็ตาม - Class ที่เป็น
finalถูก extend ไม่ได้ และ method ที่เป็นfinalถูก override ไม่ได้
ใช้ inheritance เฉพาะเมื่อ subclass เป็น ชนิดหนึ่งของแม่จริง ๆ (is-a) Student เป็น Person แต่ Car ไม่ได้เป็น Engine ถ้าความสัมพันธ์คือ "มี" (has-a) ให้ใส่ field ของ type นั้นแทน วิธีนี้เรียกว่า composition และช่วยให้ลำดับชั้นของ class ไม่ลึกเกินไป
Polymorphism: เรียกแบบเดียว ได้หลายพฤติกรรม
Polymorphism แปลว่า "หลายรูปแบบ" ตัวแปรที่เป็น supertype เก็บ subtype ตัวไหนก็ได้ และเมื่อเรียก method Java จะรันเวอร์ชันของ object ตัวจริงตอน runtime กลไกนี้เรียกว่า dynamic dispatch
public interface Shape {
double area();
}
public record Circle(double radius) implements Shape {
@Override
public double area() { return Math.PI * radius * radius; }
}
public record Rectangle(double width, double height) implements Shape {
@Override
public double area() { return width * height; }
}List<Shape> shapes = List.of(new Circle(1), new Rectangle(2, 3));
for (Shape s : shapes) {
System.out.printf("%s -> %.2f%n", s, s.area());
}
// Circle[radius=1.0] -> 3.14
// Rectangle[width=2.0, height=3.0] -> 6.00Loop นี้ไม่รู้และไม่สนว่าในมือมี shape แบบไหน ถ้าจะเพิ่ม Triangle ก็ไม่ต้องแก้ loop เลย กับ inheritance ก็เหมือนกัน Person p = new Student("Kasinphat", "6501234"); p.introduce() จะรันเวอร์ชันของ Student
กฎของ method overriding
Overriding คือการที่ subclass เขียน method ที่สืบทอดมาใหม่ในแบบของตัวเอง จะเป็นการ override ที่ถูกต้องได้ต้อง:
- ชื่อและ parameter ตรงกันทุกอย่าง
- Return type เหมือนเดิม หรือเป็น subtype ของเดิม (covariant return)
- สิทธิ์การเข้าถึง แคบลงกว่าเดิมไม่ได้ method ที่เป็น
publicจะเปลี่ยนเป็นprotectedไม่ได้ - Throw checked exception ใหม่หรือกว้างกว่าเดิมไม่ได้
- Method ที่เป็น
static,finalและprivateถูก override ไม่ได้
ใส่ @Override ทุกครั้ง compiler จะช่วยตรวจกฎเหล่านี้ให้
Overriding กับ overloading ต่างกันอย่างไร
| Overriding | Overloading | |
|---|---|---|
| เกิดที่ไหน | Subclass เขียน method ของแม่ใหม่ | Class เดียวกัน ชื่อเดียวกัน |
| Parameter | ต้องเหมือนกันทุกอย่าง | ต้องต่างกัน |
| ตัดสินใจตอน | Runtime (ดูชนิดของ object จริง) | Compile time (ดูชนิดของ argument ที่ประกาศ) |
| เป้าหมาย | เปลี่ยนพฤติกรรมตาม subtype | มีหลายรูปแบบให้เรียกใช้สะดวก |
Bug คลาสสิกเกิดจากการสับสนสองอย่างนี้ ถ้าเขียน public boolean equals(Book other) จะกลายเป็นการ overload equals แทนที่จะ override equals(Object) ทำให้ collection ไม่เคยเรียกมันเลย ถ้าใส่ @Override ไว้ compiler จะฟ้องทันที
Abstraction: พึ่งพาสิ่งที่ทำได้ ไม่ใช่วิธีทำ
Abstraction คือการเปิดเผยเฉพาะการกระทำที่จำเป็น และซ่อนรายละเอียดการ implement ไว้ ใน Java เราใช้ interface และ abstract class ในการแสดง abstraction เรื่อง syntax ของทั้งสองอยู่ใน บทความ class และ object แล้ว ส่วนนี้จะพูดถึงการนำไปใช้ออกแบบ
public record Order(String id, long total) {}
public record PaymentResult(boolean success, String reference) {}
public interface PaymentGateway {
PaymentResult charge(String orderId, long amount);
}
public class CheckoutService {
private final PaymentGateway gateway;
public CheckoutService(PaymentGateway gateway) {
this.gateway = gateway;
}
public PaymentResult checkout(Order order) {
return gateway.charge(order.id(), order.total());
}
}CheckoutService ไม่รู้ว่า gateway ข้างหลังเป็นระบบบัตรเครดิต PromptPay หรือตัวปลอมที่ใช้ใน test เราสลับ implementation ได้โดยไม่ต้องแตะ logic ของ checkout เลย นี่คือแนวคิดเดียวกับ ports and adapters ใน hexagonal architecture และที่จริงคุณก็ใช้มันอยู่ทุกวันตอนเขียน List<String> names = new ArrayList<>(); แล้วเขียนโค้ดโดยอิงกับ List
จะเลือกแบบไหนเมื่อไร:
- Interface เป็นตัวเลือกแรกเมื่อต้องการนิยามความสามารถ class หนึ่ง implement ได้หลายตัว และ class ที่ไม่เกี่ยวข้องกันก็ใช้ interface ร่วมกันได้
- Abstract class เหมาะเมื่อ subclass มี state หรือโค้ดที่ใช้ร่วมกันจริง เช่น field ร่วม constructor หรือ template method ที่เรียก abstract step
- ไม่ต้องใช้ทั้งคู่ เมื่อมี implementation เดียวและไม่น่าจะมีเพิ่ม interface ที่ไม่มีใคร implement เพิ่มแค่ทำให้โค้ดอ้อมขึ้นโดยไม่ได้อะไร
หลัก 4 ข้อของ Java OOP ทำงานร่วมกันอย่างไร
| หลัก | ปัญหาที่แก้ | เครื่องมือหลักใน Java |
|---|---|---|
| Encapsulation | ข้อมูลใน object ถูกแก้แบบไม่มีการควบคุม | field แบบ private และ method ที่ตรวจสอบค่า |
| Inheritance | โค้ดซ้ำกันระหว่าง type ที่เกี่ยวข้องกัน | extends, super |
| Polymorphism | if/else ยาว ๆ ที่เช็กชนิดของ object | Overriding, interface, dynamic dispatch |
| Abstraction | โค้ดผูกติดกับรายละเอียดการ implement | Interface, abstract class |
ในตัวอย่าง checkout ตัว gateway ซ่อน HTTP client และ API key ไว้ข้างใน (encapsulation) CheckoutService มองเห็นแค่ PaymentGateway (abstraction) gateway แต่ละตัวมี charge ในแบบของตัวเอง (polymorphism) และถ้าหลาย gateway ใช้ logic การ retry ร่วมกัน ก็เก็บไว้ใน abstract base class ได้ (inheritance)
คำถามที่พบบ่อย
หลัก OOP 4 ข้อใน Java มีอะไรบ้าง
Encapsulation, inheritance, polymorphism และ abstraction บางหลักสูตรสอนแค่ 3 ข้อโดยนับ abstraction รวมกับ encapsulation แต่เอกสาร Java ส่วนใหญ่แยกเป็น 4 ข้อ
Encapsulation กับ abstraction ต่างกันอย่างไร
Encapsulation คือการปกป้องข้อมูลภายใน class ด้วยการควบคุมสิทธิ์การเข้าถึง ส่วน abstraction คือการกำหนดว่า type หนึ่งเปิดการกระทำอะไรให้ส่วนอื่นของระบบใช้ encapsulation ซ่อน field ส่วน abstraction ซ่อนว่ากำลังคุยกับ implementation ตัวไหนอยู่
Java รองรับ multiple inheritance ไหม
ไม่รองรับสำหรับ class หนึ่ง class extend ได้แค่ class เดียว แต่ implement interface ได้ไม่จำกัด และ interface ก็มี method แบบ default ได้
Member ที่เป็น protected ใช้จาก package เดียวกันได้ไหม
ได้ protected รวมสิทธิ์ระดับ package ไว้ด้วย ทุก class ใน package เดียวกันจึงใช้ได้ รวมถึง subclass ที่อยู่ต่าง package
Overloading นับเป็น polymorphism ไหม
ตำราบางเล่มเรียกว่า compile-time polymorphism แต่เวลาคนเขียน Java พูดถึง polymorphism ทั่วไป มักหมายถึง overriding และ dynamic dispatch ตอน runtime
นำ Java OOP ไปใช้จริง
- เริ่ม field ทุกตัวเป็น
privateแล้วค่อยเพิ่ม getter และ setter เมื่อจำเป็น - แทน setter ทั่วไปด้วย method ที่ตั้งชื่อตามการกระทำจริงและตรวจกฎไว้ข้างใน
- ใช้
extendsเฉพาะความสัมพันธ์แบบ "เป็น" จริง ๆ นอกนั้นใช้ composition - ใส่
@Overrideให้ทุก method ที่ override - รับ interface ใน constructor และ parameter เพื่อให้สลับ implementation และเขียน test ได้ง่าย
- รักษาลำดับชั้นให้ตื้น ถ้าต้องไล่ดูถึง 3 ชั้นกว่าจะเข้าใจ class เดียว ควรออกแบบใหม่
หลักเดียวกันนี้ใช้ได้ตอนสร้าง UI ด้วย เช่น component และ event listener ใน Swing ถ้าต้องการคนช่วยออกแบบหรือ review codebase แบบ object-oriented ทีม Vectorkub รับงานลักษณะนี้
