ใน 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 ต่างกันอย่างไร
| Class | Object (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
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 ใหม่อยู่ในสถานะที่ถูกต้องตั้งแต่แรก
- ต้อง ชื่อเดียวกับ class ทุกตัวอักษร
- ไม่มี return type แม้แต่
voidก็ห้ามใส่ ถ้าเขียนpublic void Book()จะกลายเป็น method ธรรมดาที่บังเอิญชื่อBookและจะไม่ถูกเรียกตอนสร้าง object - หนึ่ง class มี constructor ได้หลายตัว ขอแค่ parameter ไม่ซ้ำกัน
- ถ้า ไม่เขียน constructor เลย compiler จะเติม default constructor แบบไม่มี parameter ให้ แต่ทันทีที่เราเขียน constructor ของเราเองสักตัว default constructor ตัวนี้จะหายไป
- Constructor เรียก constructor อีกตัวได้ด้วย
this(...)ซึ่งต้องเป็นคำสั่งแรก วิธีนี้ช่วยไม่ให้ต้องเขียนโค้ดตั้งค่าซ้ำ เหมือนBook(isbn, title)ในตัวอย่างด้านบน
คำสั่ง new ทำอะไรบ้าง
เมื่อเราเขียน new Book("978-0134685991", "Effective Java") สิ่งที่เกิดขึ้นตามลำดับคือ:
- จองพื้นที่ memory บน heap สำหรับ field ของ object
- ใส่ค่า default ให้ทุก field ตัวเลขเป็น
0,booleanเป็นfalse, reference เป็นnull - เรียก constructor ของ superclass (ทุก class สืบทอดจาก
Objectเป็นอย่างน้อย) - รัน field initializer เช่น
private String label = "ready";ตามลำดับที่เขียนไว้ - รันส่วนที่เหลือของ constructor
- คืน reference ที่ชี้ไปยัง object ใหม่
ลองดูลำดับนี้จากตัวอย่างสั้น ๆ:
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 ถึงทำงานแบบนี้:
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 objectc = 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
@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);
}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 ISBNString ก็ใช้กฎเดียวกัน "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 แทน:
@Override
public String toString() {
return "Book{isbn='" + isbn + "', title='" + title + "', stock=" + stock + "}";
}ถ้า class มีหน้าที่แค่ถือข้อมูล ใช้ record จะได้ constructor, accessor, equals, hashCode และ toString มาให้เลยโดยไม่ต้องเขียนเอง:
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 ตัวเดิม
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
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 ให้ครบ:
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 ไม่ได้
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 class | Interface | |
|---|---|---|
สร้างด้วย new ได้ไหม | ไม่ได้ | ไม่ได้ |
| มี constructor | ได้ | ไม่ได้ |
| มี instance field | ได้ | ไม่ได้ (มีได้แค่ค่าคงที่ static final) |
| method ที่มี body | ได้ | ได้เฉพาะ default, static, private |
| ใช้ได้กี่ตัวต่อ class | extends ได้ตัวเดียว | 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()และ overrideequalsกับhashCodeคู่กันเสมอ - Override
toString()ให้ log และ debugger แสดงข้อมูลที่มีประโยชน์ หรือใช้ record สำหรับ class ที่ถือข้อมูลอย่างเดียว - จำไว้ว่า
=copy แค่ reference และ method ได้รับสำเนาของ reference
พื้นฐานเหล่านี้ใช้ได้กับทุก Java framework ที่คุณจะเจอต่อไป ถ้าทีมของคุณต้องการคนช่วยสร้างหรือ review ระบบ backend ที่เขียนด้วย Java ทาง Vectorkub รับงานลักษณะนี้อยู่
