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

Backend

Class และ Object ใน Java: Constructor, Reference และการเปรียบเทียบด้วย equals

เข้าใจ Java class และ object ตั้งแต่กฎของ constructor, สิ่งที่ new ทำ, reference, == กับ equals, toString, pass by value, overloading ไปจนถึงความต่างของ abstract class กับ interface

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

java classes objects and constructors

ใน Java class คือแม่แบบ (blueprint) ส่วน object คือของจริงที่สร้างออกมาจากแม่แบบนั้น class บอกว่า object แต่ละตัวจะเก็บข้อมูลอะไรและทำอะไรได้บ้าง แต่ตัว class เองยังไม่มีข้อมูลจนกว่าเราจะสร้าง object ด้วย new ถ้าเข้าใจความสัมพันธ์ระหว่าง Java class กับ object ให้ชัด จะตอบคำถามที่มือใหม่สงสัยบ่อยได้เกือบหมด เช่น ทำไม == ถึงได้ false ทั้งที่ object สองตัวดูเหมือนกันทุกอย่าง ทำไม method แก้ค่าใน object ของเราได้แต่เปลี่ยนเป็น object ใหม่ไม่ได้ และทำไมพอ print object แล้วได้ Book@1b6d3586

บทความนี้อธิบายตั้งแต่สมาชิกของ class, กฎของ constructor, สิ่งที่ new ทำจริง ๆ, reference, equals กับ toString, pass by value, overloading ไปจนถึงความต่างระหว่าง abstract class กับ interface โค้ดทั้งหมดใช้ Java 17 ขึ้นไป

Class กับ Object ใน Java ต่างกันอย่างไร

ClassObject (instance)
คืออะไรนิยามของชนิดข้อมูล เขียนไว้ใน source codeค่าที่อยู่ใน memory สร้างขึ้นตอนโปรแกรมรัน
มีกี่ตัวหนึ่งตัวต่อหนึ่งชนิดสร้างได้ไม่จำกัด
สร้างอย่างไรเขียน class Book { ... }new Book(...)
เก็บข้อมูลไหมเก็บได้เฉพาะ field ที่เป็น staticมี instance field เป็นของตัวเองทุกตัว

Object ที่มาจาก class เดียวกันเก็บค่าต่างกันได้โดยสิ้นเชิง และ class เฉย ๆ ก็ไม่ได้รันอะไรด้วยตัวเอง โปรแกรมเริ่มทำงานที่ method public static void main(String[] args) แล้วโค้ดของเราค่อยสร้าง object จากตรงนั้น

สมาชิกของ class: field และ method

Class มีสมาชิกอยู่สองแบบ:

  • Field (หรือเรียกว่า attribute หรือ property) ใช้เก็บข้อมูลของ object
  • Method คือพฤติกรรม หรือสิ่งที่ object ทำได้ object อื่นจะสื่อสารกับมันผ่านการเรียก method
java
public class Book {
    public static final int MAX_STOCK = 999; // constant shared by the whole class

    private final String isbn;  // set once, never changes
    private String title;       // can be updated
    private int stock;

    public Book(String isbn, String title) {
        this(isbn, title, 0);   // delegate to the other constructor
    }

    public Book(String isbn, String title, int stock) {
        this.isbn = isbn;
        this.title = title;
        this.stock = stock;
    }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public int getStock() { return stock; }
}

Field แบ่งได้เป็น ตัวแปร ที่เปลี่ยนค่าได้ กับ ค่าคงที่ ที่ใส่ final ซึ่งกำหนดค่าได้ครั้งเดียว ส่วน field ที่เป็น static จะเป็นของ class ไม่ใช่ของ object ทุก Book จึงใช้ MAX_STOCK ตัวเดียวกัน ขณะที่ field อย่าง title แยกกันอยู่ในแต่ละ object

กฎของ constructor ใน Java

Constructor คือโค้ดที่ทำงานตอนสร้าง object หน้าที่ของมันคือทำให้ object ใหม่อยู่ในสถานะที่ถูกต้องตั้งแต่แรก

  1. ต้อง ชื่อเดียวกับ class ทุกตัวอักษร
  2. ไม่มี return type แม้แต่ void ก็ห้ามใส่ ถ้าเขียน public void Book() จะกลายเป็น method ธรรมดาที่บังเอิญชื่อ Book และจะไม่ถูกเรียกตอนสร้าง object
  3. หนึ่ง class มี constructor ได้หลายตัว ขอแค่ parameter ไม่ซ้ำกัน
  4. ถ้า ไม่เขียน constructor เลย compiler จะเติม default constructor แบบไม่มี parameter ให้ แต่ทันทีที่เราเขียน constructor ของเราเองสักตัว default constructor ตัวนี้จะหายไป
  5. Constructor เรียก constructor อีกตัวได้ด้วย this(...) ซึ่งต้องเป็นคำสั่งแรก วิธีนี้ช่วยไม่ให้ต้องเขียนโค้ดตั้งค่าซ้ำ เหมือน Book(isbn, title) ในตัวอย่างด้านบน

คำสั่ง new ทำอะไรบ้าง

เมื่อเราเขียน new Book("978-0134685991", "Effective Java") สิ่งที่เกิดขึ้นตามลำดับคือ:

  1. จองพื้นที่ memory บน heap สำหรับ field ของ object
  2. ใส่ค่า default ให้ทุก field ตัวเลขเป็น 0, boolean เป็น false, reference เป็น null
  3. เรียก constructor ของ superclass (ทุก class สืบทอดจาก Object เป็นอย่างน้อย)
  4. รัน field initializer เช่น private String label = "ready"; ตามลำดับที่เขียนไว้
  5. รันส่วนที่เหลือของ constructor
  6. คืน reference ที่ชี้ไปยัง object ใหม่

ลองดูลำดับนี้จากตัวอย่างสั้น ๆ:

java
public class Init {
    private int count;               // step 2: starts as 0
    private String label = "ready";  // step 4: initializer

    public Init() {
        System.out.println(count + " " + label); // prints "0 ready"
        count = 10;                              // step 5: constructor body
    }
}

Reference: ตัวแปรเก็บที่อยู่ ไม่ได้เก็บตัว object

ตัวแปรที่มีชนิดเป็น class ไม่ได้เก็บ object ไว้ข้างใน แต่เก็บ reference ที่ชี้ไปยังตำแหน่งของ object ข้อนี้อธิบายว่าทำไม = กับ object ถึงทำงานแบบนี้:

java
Book a = new Book("978-0134685991", "Effective Java");
Book c = a;              // copies the reference, not the object

c.setTitle("EJ 3rd ed.");
System.out.println(a.getTitle()); // "EJ 3rd ed." - a and c are the same object

c = a ไม่ได้ copy หนังสือเล่มใหม่ขึ้นมา เราแค่มีสองชื่อที่ชี้ไปยัง object ตัวเดียวกัน ถ้าต้องการสำเนาที่แยกขาดจากกันจริง ๆ ต้องสร้าง object ใหม่เอง เช่น เขียน copy constructor public Book(Book other) ที่ copy ค่าทีละ field

การเปรียบเทียบ object: == กับ equals()

  • == เปรียบเทียบ reference คือถามว่า "เป็น object ตัวเดียวกันไหม"
  • equals() เปรียบเทียบ ค่า คือถามว่า "สองตัวนี้หมายถึงสิ่งเดียวกันไหม" แต่จะได้ผลแบบนี้ก็ต่อเมื่อ class override มันไว้

equals() ที่สืบทอดมาจาก Object ทำงานเหมือน == ทุกประการ ถ้าอยากให้หนังสือเท่ากันเมื่อ ISBN ตรงกัน ต้อง override เอง และ ต้อง override hashCode() คู่กันทุกครั้ง ไม่อย่างนั้น HashMap กับ HashSet จะมองหนังสือสองเล่มที่ equal กันเป็นคนละ key

java
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Book other)) return false; // pattern matching, Java 16+
    return isbn.equals(other.isbn);
}

@Override
public int hashCode() {
    return Objects.hash(isbn);
}
java
Book a = new Book("978-0134685991", "Effective Java");
Book b = new Book("978-0134685991", "Effective Java");

System.out.println(a == b);      // false: two separate objects
System.out.println(a.equals(b)); // true: same ISBN

String ก็ใช้กฎเดียวกัน "java" == new String("java") ได้ false แต่ .equals() ได้ true ดังนั้นเวลาเทียบ string ให้ใช้ equals() เสมอ

แสดงผลให้อ่านง่ายด้วย toString()

เวลา print object Java จะเรียก toString() ของมัน ค่า default จาก Object คือชื่อ class ตามด้วย @ และ hash code เป็นเลขฐานสิบหก เช่น Book@1b6d3586 ตัวเลขนี้คือ hash code ไม่ใช่ address ใน memory อย่างที่หลายคนเข้าใจ และแทบไม่ช่วยอะไรเวลา debug ให้ override แทน:

java
@Override
public String toString() {
    return "Book{isbn='" + isbn + "', title='" + title + "', stock=" + stock + "}";
}

ถ้า class มีหน้าที่แค่ถือข้อมูล ใช้ record จะได้ constructor, accessor, equals, hashCode และ toString มาให้เลยโดยไม่ต้องเขียนเอง:

java
public record Point(int x, int y) {}
// new Point(1, 2).toString() -> "Point[x=1, y=2]"

Java ส่งค่าแบบ pass by value เสมอ

Java ส่ง argument ทุกตัว แบบ pass by value ถ้าเป็น primitive ค่าที่ส่งไปคือตัวเลขนั้นเลย ถ้าเป็น object ค่าที่ส่งไปคือ reference ดังนั้น method จะได้สำเนาของ reference ที่ยังชี้ไปยัง object ตัวเดิม

java
static void rename(Book book)  { book.setTitle("Renamed"); }        // changes the shared object
static void replace(Book book) { book = new Book("000", "New"); }   // only changes the local copy
static void addOne(int n)      { n++; }                             // only changes the local copy

Book a = new Book("978-0134685991", "Effective Java");
rename(a);   // a.getTitle() is now "Renamed"
replace(a);  // a still points to the original book, title still "Renamed"

int x = 5;
addOne(x);   // x is still 5

สรุปคือ method แก้ไข object ที่เราส่งเข้าไปได้ แต่ทำให้ตัวแปรของเราชี้ไปที่ object ตัวอื่นไม่ได้ คำอธิบายที่ว่า "object ส่งแบบ pass by reference" จึงไม่ถูกต้อง และ replace() ก็แสดงให้เห็นชัดว่าทำไม

Method overloading

Overloading คือการมี method ชื่อเดียวกันแต่ parameter ต่างกัน อยู่ใน class เดียวกัน compiler จะเลือกว่าเรียกตัวไหนตั้งแต่ตอน compile โดยดูจากชนิดของ argument

java
public void restock(int amount) {
    restock(amount, "regular delivery");
}

public void restock(int amount, String reason) {
    if (stock + amount > MAX_STOCK) {
        throw new IllegalArgumentException("stock limit is " + MAX_STOCK);
    }
    stock += amount;
}

Parameter ต้องต่างกันที่จำนวน ชนิด หรือลำดับ ต่างกันแค่ return type ไม่นับ และจะ compile ไม่ผ่าน ตัวอย่างที่เราใช้กันทุกวันคือ System.out.println ซึ่งมี overload แยกสำหรับ int, String, Object และอื่น ๆ อย่าสับสน overloading กับ overriding ซึ่งคือการเขียน method ที่สืบทอดมาใหม่ใน subclass เรื่องนั้นเป็นส่วนหนึ่งของ polymorphism อ่านต่อได้ใน หลัก 4 ข้อของ Java OOP

Abstract class และ interface

บาง type มีไว้ให้ class อื่นมาเติมให้สมบูรณ์เท่านั้น

Abstract class คือ class ที่ใส่ abstract ไว้ และสร้าง object ด้วย new ไม่ได้ ข้างในมี field, constructor และ method ที่เขียนเสร็จแล้วได้ตามปกติ พร้อมกับ abstract method ที่ไม่มี body ซึ่ง subclass ทุกตัวที่ไม่ใช่ abstract ต้อง implement ให้ครบ:

java
public abstract class Media {
    private final String title;

    protected Media(String title) { this.title = title; } // runs via super(title)
    public String getTitle() { return title; }

    public abstract int loanDays(); // no body: subclasses must provide one
}

Interface ไม่ใช่ class แต่เป็นการประกาศว่า type นี้ทำอะไรได้บ้าง method ใน interface เป็น abstract โดย default และตั้งแต่ Java 8 ก็มี method แบบ default และ static ที่มี body ได้ แต่มี instance field หรือ constructor ไม่ได้

java
public interface Borrowable {
    boolean isAvailable();

    default String status() {
        return isAvailable() ? "available" : "on loan";
    }
}

public class Dvd extends Media implements Borrowable {
    private boolean onLoan;

    public Dvd(String title) { super(title); }

    @Override public int loanDays() { return 3; }
    @Override public boolean isAvailable() { return !onLoan; }
}

ถ้า Dvd ลืม implement loanDays() หรือ isAvailable() โค้ดจะ compile ไม่ผ่าน การที่ compiler บังคับตรงนี้คือประโยชน์หลักของทั้งสองแบบ

Abstract classInterface
สร้างด้วย new ได้ไหมไม่ได้ไม่ได้
มี constructorได้ไม่ได้
มี instance fieldได้ไม่ได้ (มีได้แค่ค่าคงที่ static final)
method ที่มี bodyได้ได้เฉพาะ default, static, private
ใช้ได้กี่ตัวต่อ classextends ได้ตัวเดียวimplements ได้หลายตัว

เรื่อง syntax เป็นส่วนที่ง่าย คำถามที่ยากกว่าคือ ควรใช้แบบไหนเมื่อไร ซึ่งเป็นเรื่องการออกแบบ abstraction อธิบายไว้ใน บทความหลัก OOP ของ Java ถ้าคุณเขียน Go ด้วย ลองเทียบกับ interface ของ Go ที่ type ไม่ต้องประกาศเลยว่า implement interface ไหน

คำถามที่พบบ่อย

Class กับ object ใน Java ต่างกันอย่างไร

Class คือนิยามว่า type หนึ่งมี field และ method อะไรบ้าง ส่วน object คือ instance ของ class นั้นที่สร้างตอนรันด้วย new และมีค่า instance field เป็นของตัวเอง class เดียวสร้าง object ได้ไม่จำกัด

Constructor ใน Java return ค่าได้ไหม

ไม่ได้ constructor ไม่มี return type เลย ถ้าใส่เข้าไป แม้แต่ void มันจะกลายเป็น method ธรรมดาและไม่ถูกเรียกตอนสร้าง object

Java เป็น pass by value หรือ pass by reference

Pass by value เสมอ สำหรับ object ค่าที่ถูก copy ไปคือ reference ทำให้ method แก้ไข object ได้ แต่เปลี่ยนตัวแปรของผู้เรียกให้ชี้ไปที่อื่นไม่ได้

ทำไม override equals แล้วต้อง override hashCode ด้วย

Collection ที่ใช้ hash จะดู hash code ก่อน แล้วค่อยเรียก equals เฉพาะใน bucket ที่ตรงกัน ถ้า object สองตัวที่ equal กันคืน hash code คนละค่า HashSet และ HashMap จะมองว่าเป็นคนละรายการ

Abstract class มี constructor ได้ไหม

ได้ เราเรียกมันด้วย new ตรง ๆ ไม่ได้ แต่ subclass จะเรียกผ่าน super(...) เพื่อกำหนดค่า field ที่ abstract class ประกาศไว้

Checklist สำหรับการเขียน class ใน Java

  • ทำ field ให้เป็น private และใส่ final ให้ field ที่ไม่ควรเปลี่ยนหลังสร้าง object
  • ตรวจสอบ input ใน constructor เพื่อไม่ให้มี object ที่อยู่ในสถานะผิดตั้งแต่ต้น
  • ใช้ this(...) เชื่อม constructor เข้าด้วยกัน แทนการ copy โค้ดตั้งค่าซ้ำ
  • เปรียบเทียบค่าด้วย equals() และ override equals กับ hashCode คู่กันเสมอ
  • Override toString() ให้ log และ debugger แสดงข้อมูลที่มีประโยชน์ หรือใช้ record สำหรับ class ที่ถือข้อมูลอย่างเดียว
  • จำไว้ว่า = copy แค่ reference และ method ได้รับสำเนาของ reference

พื้นฐานเหล่านี้ใช้ได้กับทุก Java framework ที่คุณจะเจอต่อไป ถ้าทีมของคุณต้องการคนช่วยสร้างหรือ review ระบบ backend ที่เขียนด้วย Java ทาง Vectorkub รับงานลักษณะนี้อยู่

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

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

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