กลับไปยังบทความ

Backend

Java OOP: อธิบายหลัก 4 ข้อ Encapsulation, Inheritance, Polymorphism และ Abstraction

เรียนรู้หลัก 4 ข้อของ Java OOP พร้อมตัวอย่างที่รันได้จริง ทั้ง encapsulation และ access modifier, inheritance ด้วย extends และ super, overriding กับ overloading และ abstraction ผ่าน interface

24 กันยายน 25699 นาทีในการอ่าน

java oop four pillars

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 เราก็เปลี่ยนโค้ดข้างในทีหลังได้โดยไม่ทำให้ใครพัง

java
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 ที่ได้เมื่อไม่ใส่อะไรเลย:

ModifierClass เดียวกัน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 ที่เข้าถึงได้ของแม่มา และเพิ่มของตัวเองได้

java
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

java
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; }
}
java
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.00

Loop นี้ไม่รู้และไม่สนว่าในมือมี shape แบบไหน ถ้าจะเพิ่ม Triangle ก็ไม่ต้องแก้ loop เลย กับ inheritance ก็เหมือนกัน Person p = new Student("Kasinphat", "6501234"); p.introduce() จะรันเวอร์ชันของ Student

กฎของ method overriding

Overriding คือการที่ subclass เขียน method ที่สืบทอดมาใหม่ในแบบของตัวเอง จะเป็นการ override ที่ถูกต้องได้ต้อง:

  1. ชื่อและ parameter ตรงกันทุกอย่าง
  2. Return type เหมือนเดิม หรือเป็น subtype ของเดิม (covariant return)
  3. สิทธิ์การเข้าถึง แคบลงกว่าเดิมไม่ได้ method ที่เป็น public จะเปลี่ยนเป็น protected ไม่ได้
  4. Throw checked exception ใหม่หรือกว้างกว่าเดิมไม่ได้
  5. Method ที่เป็น static, final และ private ถูก override ไม่ได้

ใส่ @Override ทุกครั้ง compiler จะช่วยตรวจกฎเหล่านี้ให้

Overriding กับ overloading ต่างกันอย่างไร

OverridingOverloading
เกิดที่ไหน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 แล้ว ส่วนนี้จะพูดถึงการนำไปใช้ออกแบบ

java
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
Polymorphismif/else ยาว ๆ ที่เช็กชนิดของ objectOverriding, interface, dynamic dispatch
Abstractionโค้ดผูกติดกับรายละเอียดการ implementInterface, 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 รับงานลักษณะนี้

พร้อมสร้างสิ่งที่ยิ่งใหญ่หรือยัง?

เล่าโปรเจคของคุณให้เราฟัง — เราจะพาคุณไปถึง

เลือกคุกกี้ที่เราใช้ได้ คุกกี้ที่จำเป็นช่วยให้เว็บไซต์ทำงานได้และเปิดใช้งานเสมอ