interface ที่ต้องเอ่ยชื่อ และกฎส่วนลดที่ต่อเพิ่มได้
orderctl คิดยอดรวมและสรุปตามหมวดได้แล้วตั้งแต่บทที่ 4 แต่ยอดที่ได้ยังเป็นยอดเต็มเสมอ
บทนี้เติมส่วนลด และส่วนลดคือที่ที่ทุกภาษาหยิบเครื่องมือ abstraction ออกมาใช้ Java มีเครื่องมือนั้นสองตัว และตัวแรกทำงานกลับด้านกับที่ คอร์ส Go สอนไว้ทั้งบท
บทนี้มีสองชุด · ชุดแรกคือ code จริงของ orderctl ที่อยู่ใน package orderctl; ตามโครง
Maven ของบทที่ 3 · ชุดที่สองคือฉากทดลองที่วางไว้ใน package orderctl.lab;
ฉากทดลองแต่ละอันเป็น file เดียวที่แปลได้เดี่ยวๆ จึงคัดสำเนาย่อของ DiscountRule
มาไว้เป็นชนิดซ้อนข้างใน · แยก package ไว้ต่างหากเพื่อไม่ให้ชื่อชนกับของจริง
รันฉากทดลองด้วยรูปเดียวกับบทที่ 3 คือ mvn -q compile exec:java -Dexec.mainClass=orderctl.lab.<ชื่อ>
ไม่มีการเข้ากันโดยบังเอิญ
หัวข้อที่มีชื่อว่า “ไม่มีการเข้ากันโดยบังเอิญ”ชนิดที่มี method รูปร่างตรงเป๊ะแต่ไม่ได้เขียน
implementsไม่ใช่สมาชิกของ interface นั้น
nominal typingnominal typingการเข้ากันของชนิดตัดสินด้วยชื่อที่ประกาศไว้ ไม่ใช่ด้วยรูปร่าง · Java ต้องเขียน `implements` ออกมาตรงๆ ซึ่งตรงข้ามกับ interface โดยปริยายของ Go คือกฎข้อเดียวที่ทำให้ทั้งบทนี้เป็นอย่างที่มันเป็น ความเข้ากันของชนิดตัดสินจากชื่อที่ประกาศผูกไว้ ไม่ได้ตัดสินจาก method ที่มีอยู่จริง
ฉากข้างล่างคือชนิดที่มี apply(long) รูปร่างตรงทุกตัวอักษร แต่ไม่มีบรรทัดผูกพัน
// ❌ รูปที่ผิด · orderctl/lab/Shape.javastatic final class Foreign { public long apply(long subtotalSatang) { return subtotalSatang - 2_000L; }}
DiscountRule rule = new Foreign();javac คายข้อความนี้ออกมา
error: incompatible types: Foreign cannot be converted to DiscountRule DiscountRule rule = new Foreign(); ^การติด @Override ลงไปเฉยๆ ไม่ได้สร้างความผูกพันนั้นขึ้นมา มันแค่ทำให้ความผิดพลาดย้ายที่
// ❌ รูปที่ผิด · orderctl/lab/Ann.java · ติด @Override ให้ Foreign.applyerror: method does not override or implement a method from a supertype @Override ^บทนี้ยกข้อความจาก javac ตรงๆ ทุกครั้ง เพราะมันชี้ตำแหน่งด้วยลูกศรใต้บรรทัดที่ผิด
mvn -q compile ส่งข้อความเดียวกันผ่าน maven-compiler-plugin ซึ่งเขียนมันใหม่ทั้งบรรทัด
คือตัดลูกศรทิ้ง แล้วเติมชื่อเต็มของชนิดเข้าไปแทน:
[ERROR] .../src/main/java/orderctl/lab/Shape.java:[16,29] incompatible types: orderctl.lab.Shape.Foreign cannot be converted to orderctl.lab.Shape.DiscountRuleเลขในวงเล็บเหลี่ยมคือบรรทัดกับตำแหน่งตัวอักษร · ส่วนคำว่า error: หายไปเพราะ Maven
ย้ายมันไปไว้ที่ป้าย [ERROR] ข้างหน้าแทน
ผลตามมามีข้อเดียว แต่มันเปลี่ยนวิธีทำงานทั้งโปรเจกต์ — ที่ที่ต้องไปแก้คือ file ของผู้สร้างชนิด ไม่ใช่ file ของผู้ใช้ ซึ่งเป็นสิ่งที่คอร์ส Go ไม่เคยต้องทำเลยสักครั้ง
บทที่ 5 ของคอร์ส Go สอนให้เขียน var _ Rule = PercentOff{} ไว้ใน package ของผู้สร้างชนิด
เพราะการสะกดชื่อ method ผิดหนึ่งตัวยังเป็น code ที่ถูกต้อง มันแค่เงียบๆ เลิกเป็น Rule
Java ไม่มีวันลืมข้อนี้ได้ เพราะบรรทัด implements DiscountRule คือด่านนั้นในตัวมันเอง
และเครื่องมือไล่หาผู้ implement ทั้งหมดได้จากชื่อ
ราคาที่ Java จ่ายอยู่คนละที่ และหนักกว่าในโปรเจกต์ที่พึ่ง library — ดูหัวข้อ “ชนิดที่แก้ file ไม่ได้” ข้างล่าง
กฎส่วนลดตัวแรกของ orderctl
หัวข้อที่มีชื่อว่า “กฎส่วนลดตัวแรกของ orderctl”interface method เดียวหนึ่งตัว กับ record สองตัวที่ประกาศความผูกพันไว้ตรงๆ
DiscountRule รับยอดหนึ่งก้อนแล้วคืนยอดใหม่ หน่วยเป็นสตางค์ตามที่บทที่ 2 ตกลงไว้
package orderctl;
@FunctionalInterfacepublic interface DiscountRule {
/** คืนยอดใหม่หลังหักส่วนลด · หน่วยเป็นสตางค์เสมอ */ long apply(long subtotalSatang);
/** เพิ่มทีหลังได้เพราะเป็น default · ผู้ที่ implement ไว้แล้วไม่ต้องแก้อะไรเลย */ default DiscountRule andThen(DiscountRule next) { return subtotalSatang -> next.apply(apply(subtotalSatang)); }
/** กฎที่ไม่ลดอะไรเลย · ใช้เป็นจุดตั้งต้นของการต่อกฎ */ static DiscountRule none() { return subtotalSatang -> subtotalSatang; }}interface ตัวนี้ใช้ของสองอย่างที่คนซึ่งเคยเขียน Java ก่อนรุ่น 8 จะไม่คาดหวัง คือ default
กับ static เพราะก่อนหน้านั้น interface บรรจุเนื้อความอะไรไม่ได้เลย
เส้นเวลาของสิ่งที่ interface ได้มาทีหลังมีสามข้อ
defaultmethod มาที่ Java 8staticmethod ใน interface มาที่ Java 8 เช่นกันprivatemethod ที่ใช้แชร์เนื้อความระหว่าง default มาที่ Java 9 ·DiscountRuleยังไม่ต้องใช้ตัวนี้ เพราะ default ตัวเดียวไม่มีอะไรให้แชร์
ผู้ implement สองตัวแรกเป็น record ซึ่งเขียนความผูกพันไว้ที่ตัวเองตรงๆ
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์package orderctl;
public record PercentOff(int pct) implements DiscountRule {
public PercentOff { if (pct < 0 || pct > 100) { throw new IllegalArgumentException("pct ต้องอยู่ใน 0..100 แต่ได้ " + pct); } }
@Override public long apply(long subtotalSatang) { return subtotalSatang - subtotalSatang * pct / 100; }}
// ─────────── src/main/java/orderctl/FlatOff.java ───────────package orderctl;
public record FlatOff(long satang) implements DiscountRule {
@Override public long apply(long subtotalSatang) { return Math.max(0L, subtotalSatang - satang); }}@Override ตรงนี้ไม่ได้บังคับ และตราบใดที่ apply ยังเป็น abstract มันก็ไม่ได้เพิ่มด่านให้เลย
· ลบบรรทัดนั้นทิ้งแล้วเปลี่ยนชื่อ method ใน interface การแปลก็ยังแดงอยู่ดี ด้วย
error: PercentOff is not abstract and does not override abstract method applyTo(long) in DiscountRule
ที่ที่ @Override ทำงานจริงคือวันที่ apply กลายเป็น default · ตอนนั้นชื่อที่สะกดผิดจะแปลผ่านเงียบสนิท
แล้วผู้เรียกได้พฤติกรรมของ default แทนของ PercentOff · มี @Override อยู่แล้วมันแดงทันที
ด้วย error: method does not override or implement a method from a supertype
ชนิดที่แก้ file ไม่ได้ ยังต่อเข้ามาได้
หัวข้อที่มีชื่อว่า “ชนิดที่แก้ file ไม่ได้ ยังต่อเข้ามาได้”เมื่อชนิดนั้นมาจาก jar ของคนอื่น ทางที่เหลือคือ lambda กับ method reference
นี่คือราคาที่ Java จ่ายให้ nominal typing · ชนิดที่คุณแก้ file ไม่ได้
จะไม่มีวันกลายเป็น DiscountRule ด้วยตัวมันเอง
ทางออกไม่ได้อยู่ที่การแก้ file ต้นทาง แต่อยู่ที่ปลายทางซึ่งเป็นของคุณเอง
package orderctl.lab;
public final class Adapt {
interface DiscountRule { long apply(long subtotalSatang); }
/** ชนิดจาก jar ของคนอื่น · รูปร่างตรงทุกตัว แต่ไม่มีบรรทัด implements */ static final class VendorRule { long compute(long subtotalSatang) { return subtotalSatang - 2_000L; } }
public static void main(String[] args) { VendorRule vendor = new VendorRule(); DiscountRule rule = vendor::compute; System.out.println(rule.apply(20_000L)); }}$ mvn -q compile exec:java -Dexec.mainClass=orderctl.lab.Adapt18000vendor::compute ทำหน้าที่ adapter โดยไม่แตะ file ต้นทางเลย
แต่สังเกตว่านี่ยังเป็น nominal อยู่ทุกประการ ชนิดเป้าหมายมีชื่อ และชื่อนั้นเขียนไว้ที่ปลายทาง
สิ่งที่ทำให้ interface เป็นเป้าหมายของ lambda คือกฎ “มี abstract method ตัวเดียว” เท่านั้น · annotation ตัวนี้แค่เปลี่ยนกฎข้อนั้นให้เป็นด่านตอนแปล
ใส่มันทั้งที่มี abstract สองตัว แล้ว javac ตอบสามบรรทัด:
error: Unexpected @FunctionalInterface annotation @FunctionalInterface ^ Rule is not a functional interface multiple non-overriding abstract methods found in interface RuleJLS §9.6.4.9 อธิบายว่าเพราะ interface บางตัวเป็น functional โดยบังเอิญ จึงไม่จำเป็นและ ไม่พึงประสงค์ที่จะติด annotation นี้ให้ทุกตัว
ประโยคเต็มคือ “Because some interfaces are functional incidentally, it is not necessary or desirable that all declarations of functional interfaces be annotated with @FunctionalInterface.”
java.util.function มี interface สำเร็จอยู่แล้ว แต่มันช่วยไม่ได้ที่นี่ด้วยสองเหตุผล
เหตุผลแรกคือชนิด DR r = f; เมื่อ f เป็น Function<Long, Long> ได้
error: incompatible types: Function<Long,Long> cannot be converted to DR
· สองตัวนี้ไม่ได้มีลายเซ็นเดียวกันด้วยซ้ำ (Long apply(Long) เทียบ long apply(long))
เหตุผลที่สองคือราคา Function<Long, Long> บังคับให้ห่อค่าเป็น object ทุกครั้งที่เรียก
ซึ่งเป็นสิ่งที่บทที่ 4 เพิ่งสอนให้ระวัง · f::apply ผ่านได้ก็เพราะการห่อค่าอัตโนมัตินั้น
เข้ามาเชื่อมให้พอดี
default method เพิ่มความสามารถโดยไม่ต้องแปลใหม่
หัวข้อที่มีชื่อว่า “default method เพิ่มความสามารถโดยไม่ต้องแปลใหม่”
andThenถูกเติมลง interface หลังจากมีผู้ implement ไปแล้ว และ.classเดิมยังใช้ได้
default methoddefault methodmethod ใน interface ที่มีเนื้อความมาด้วย · ทำให้เพิ่มความสามารถลง interface เดิมได้โดยไม่พังผู้ใช้เก่า แก้ปัญหาเดียวที่เจาะจงมาก คือการเพิ่มความสามารถลง interface ที่มีผู้ใช้อยู่แล้ว โดยไม่ต้องไล่แก้ทุกคนที่ implement ไว้
เอกสารของ Oracle เขียนเจตนาของฟีเจอร์ไว้ตรงตัว: “Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.”
คำว่า binary compatibility ในประโยคนั้นแรงกว่าที่อ่านผ่านๆ มันแปลว่าไม่ต้องแปลใหม่ด้วยซ้ำ
การทดลองข้างล่างยืนยันข้อนี้ด้วย checksum ของ .class เดียวกันสามรอบ
ตั้งต้นด้วย DiscountRule รุ่นที่ยังไม่มี andThen คือลบ method ตัวนั้นออกจาก file ข้างบนก่อน
· แล้วเขียนตัวเรียกที่รอใช้ andThen ไว้อีก file หนึ่ง
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์// src/main/java/orderctl/Run.java — ตัวเรียกที่ใช้ andThen ซึ่งยังไม่มีตอนแปล PercentOffpackage orderctl;
public final class Run {
public static void main(String[] args) { DiscountRule member = new PercentOff(10); System.out.println(member.andThen(DiscountRule.none()).apply(10_000L)); }}$ javac --release 25 -Xlint:all -d out orderctl/DiscountRule.java orderctl/PercentOff.java$ md5 -q out/orderctl/PercentOff.class4b83cf0b97e7e81849180455678a8627
# เติม andThen ลง DiscountRule.java แล้วแปล "เฉพาะ interface" ตัวเดียว$ javac --release 25 -Xlint:all -d out orderctl/DiscountRule.java$ md5 -q out/orderctl/PercentOff.class4b83cf0b97e7e81849180455678a8627
$ javac --release 25 -Xlint:all -cp out -d out orderctl/Run.java$ md5 -q out/orderctl/PercentOff.class4b83cf0b97e7e81849180455678a8627$ java -cp out orderctl.Run9000ค่า md5 ตัวนี้ผูกกับตัวอักษรทุกตัวใน PercentOff.java ของคุณ จึงไม่ใช่เลขที่ต้องตรงกับของผม
· สิ่งที่ต้องตรงคือ สามค่าเท่ากันทั้งสามรอบ ซึ่งแปลว่า PercentOff.class ไม่เคยถูกแตะเลย
การทดลองนี้เรียก javac ตรงๆ ไม่ผ่าน Maven เพราะประเด็นของมันคือ .class ที่แปลไว้แล้ว
· Maven จะแปลใหม่ให้เองเมื่อเห็นว่า source เปลี่ยน ซึ่งกลบสิ่งที่เรากำลังวัดพอดี
คอร์ส Go ยกกฎของภาษามาไว้ตั้งแต่บทที่ 5 ว่าชนิดหนึ่งเข้ากับ interface เมื่อมี method ครบ
ผลอีกด้านของกฎเดียวกันคือ การเพิ่ม method ลง interface ที่มีคนใช้แล้ว ทำให้ทุกคนที่ implement ไว้หลุดออกทันที และภาษาไม่มีทางแก้ให้
ท่าที่ชุมชน Go ใช้จึงเป็นการตั้ง interface ตัวใหม่แล้วถามชนิดตอนรันว่ารับได้ไหม
· io.Copy ในกล่องมาตรฐานเขียนแบบนี้ตรงๆ ด้วย src.(WriterTo) แล้วต่อด้วย dst.(ReaderFrom)
Java เติมเป็น default แล้วจบ · ผู้ใช้เก่าไม่ต้องแก้และไม่ต้องแปลใหม่
กฎสามข้อที่มากับ default method
หัวข้อที่มีชื่อว่า “กฎสามข้อที่มากับ default method”สามข้อคือการชนกันที่แดงตอนแปล การบังที่เงียบสนิท และ method ของ
Objectที่ทับไม่ได้
ข้อแรก รับ default ชื่อเดียวกันจากสอง interface แล้วแดงทันที javac ไม่เลือกให้ และไม่มีลำดับความสำคัญให้จำ
// ❌ รูปที่ผิด · ไม่ได้เขียนทับ label() ไว้ที่ตัว Ruleerror: types Labelled and Auditable are incompatible; static final class Rule implements Labelled, Auditable { } ^ class Rule inherits unrelated defaults for label() from types Labelled and Auditableทางแก้คือเขียนทับ แล้วเรียกตัวที่ต้องการด้วยรูป Interface.super
package orderctl.lab;
public final class Both {
interface Labelled { default String label() { return "กฎ"; } }
interface Auditable { default String label() { return "กฎที่ตรวจสอบได้"; } }
static final class Rule implements Labelled, Auditable { @Override public String label() { return Labelled.super.label() + " / " + Auditable.super.label(); } }
public static void main(String[] args) { System.out.println(new Rule().label()); }}$ mvn -q compile exec:java -Dexec.mainClass=orderctl.lab.Bothกฎ / กฎที่ตรวจสอบได้ข้อที่สอง method ของ superclass บังของ interface โดยไม่มีเสียงเตือน ข้อนี้อันตรายกว่าข้อแรก เพราะข้อแรกแดงตอนแปล ส่วนข้อนี้เขียวสนิท
package orderctl.lab;
public final class ClassWins {
interface Labelled { default String label() { return "จาก default ของ interface"; } }
static class Base { public String label() { return "จาก superclass"; } }
static final class Rule extends Base implements Labelled { }
public static void main(String[] args) { System.out.println(new Rule().label()); }}$ mvn -q compile exec:java -Dexec.mainClass=orderctl.lab.ClassWinsจาก superclass-Xlint:all ไม่คายอะไรเลยกับ file นี้ · กฎอยู่ใน JLS §8.4.8 ตรงเงื่อนไข “No concrete method
inherited by C from its direct superclass type has a signature that is a subsignature of the
signature of m as a member of D.”
ข้อที่สาม default ทับ method ของ Object ไม่ได้ ซึ่งตัดสำนวน “ให้ interface แจก
toString ให้ทุกคน” ทิ้งทั้งกอง
// ❌ รูปที่ผิดerror: default method toString in interface Labelled overrides a member of java.lang.Object default String toString() { return "กฎ"; } ^abstract class — เส้นแบ่งอยู่ที่ state กับ constructor
หัวข้อที่มีชื่อว่า “abstract class — เส้นแบ่งอยู่ที่ state กับ constructor”interface ถือ state ของ instance ไม่ได้และมี constructor ไม่ได้ · ส่วนแม่มีได้ตัวเดียว
abstract classabstract classclass ที่สร้าง instance ไม่ได้ มี constructor และ state ร่วมให้ลูก · class ลูกมีแม่ได้ตัวเดียว ซึ่งเป็นเส้นที่แยกมันออกจาก interface กับ interface ไม่ใช่เรื่องของสไตล์ ถ้าคุณมาจาก C# ข้อนี้คือข้อที่ต้องปรับก่อน · ใน Java มันมีเส้นแบ่งที่วัดได้สามข้อ
ข้อแรกคือ state ของ instance JLS §9.3 เขียนว่า “Every field declaration in the body of an interface declaration is implicitly public, static, and final.” เขียนตัวนับลง interface จึงไม่ได้
// ❌ รูปที่ผิดerror: cannot assign a value to static final variable seen seen = seen + 1; ^ข้อที่สองคือ constructor เขียน HasCtor() {} ลง interface แล้วไม่ได้ผิดแค่กฎ
มันไม่อยู่ในไวยากรณ์เลย javac ตอบว่า error: <identifier> expected
ข้อที่สามคือจำนวนแม่ class ลูกมีแม่ได้ตัวเดียว ส่วน interface กี่ตัวก็ได้ · เอกสารของ Oracle เขียนเหตุผลไว้เองว่า “One reason why the Java programming language does not permit you to extend more than one class is to avoid the issues of multiple inheritance of state, which is the ability to inherit fields from multiple classes.”
สองข้อแรกรวมกันได้รูปที่ interface ทำไม่ได้ คือ template method ที่ตรึงลำดับไว้แล้วเจาะช่องให้ลูกเติม
package orderctl.lab;
public final class Capped {
interface DiscountRule { long apply(long subtotalSatang); }
abstract static class CappedRule implements DiscountRule {
private final long capSatang; // state ของ instance — interface ถือไม่ได้
protected CappedRule(long capSatang) { // constructor — interface มีไม่ได้ if (capSatang < 0) { throw new IllegalArgumentException("เพดานส่วนลดติดลบไม่ได้"); } this.capSatang = capSatang; }
/** ช่องเดียวที่ class ลูกเติมได้ */ protected abstract long discountSatang(long subtotalSatang);
/** final = ลูกเปลี่ยนลำดับนี้ไม่ได้ */ @Override public final long apply(long subtotalSatang) { return subtotalSatang - Math.min(discountSatang(subtotalSatang), capSatang); } }
static final class CappedPercentOff extends CappedRule {
private final int pct;
CappedPercentOff(int pct, long capSatang) { super(capSatang); this.pct = pct; }
@Override protected long discountSatang(long subtotalSatang) { return subtotalSatang * pct / 100; } }
public static void main(String[] args) { DiscountRule capped = new CappedPercentOff(50, 4_000L); System.out.println(capped.apply(20_000L));
DiscountRule oneOff = new CappedRule(1_000L) { @Override protected long discountSatang(long subtotalSatang) { return subtotalSatang * 30 / 100; } }; System.out.println(oneOff.apply(20_000L)); }}$ mvn -q compile exec:java -Dexec.mainClass=orderctl.lab.Capped1600019000new CappedRule(4_000L) เปล่าๆ ได้ error: CappedRule is abstract; cannot be instantiated
· แต่รูปที่เห็นในบรรทัดล่างเติมช่องที่เจาะไว้ไปพร้อมกัน จึงสร้างได้ทันทีโดยไม่ต้องตั้งชื่อ class
CappedRule ตรวจค่าใน constructor แต่ไม่เรียก discountSatang ที่นั่น ข้อนี้ตั้งใจ
เหตุผลอยู่ที่ลำดับ · constructor ของแม่ทำงานก่อน field ของลูกถูกตั้งค่า
method ของลูกที่ถูกเรียกตอนนั้นจึงเห็นค่าศูนย์กับ null ล้วน
-Xlint:all มีด่านชื่อ this-escape ให้เรื่องนี้ แต่มันเตือนเฉพาะตอนที่ class ลูกเกิดขึ้นจาก
นอก package ได้จริง คือตัว class เป็น public และ constructor เป็น public หรือ protected
· รูปนั้นจะได้ warning: [this-escape] possible 'this' escape before subclass is fully initialized
แต่รูปใน Capped.java ข้างบนเงียบสนิท เพราะ CappedRule ที่นั่นเป็นชนิดซ้อนที่ไม่ได้ประกาศ
public · ใส่การเรียก discountSatang ลง constructor ของมันแล้ว -Xlint:all ไม่คายอะไรเลย
· ด่านนี้จึงไม่ใช่ตัวที่กันความผิดพลาดข้อนี้ให้คุณ คุณต้องกันเอง
ประกอบแทนสืบทอด
หัวข้อที่มีชื่อว่า “ประกอบแทนสืบทอด”
recordสืบทอดไม่ได้เลย ชนิดของโดเมนนี้จึงเหลือเครื่องมือเดียวคือการห่อ
CappedRule ข้างบนเป็นวัตถุจัดแสดง ไม่ใช่รูปที่คอร์สแนะนำ
เพราะราคาที่จ่ายเพื่อได้ template method คือ CappedPercentOff เลิกเป็น record
ข้อนี้ไม่ใช่ความเห็น มันคือไวยากรณ์ · JLS §8.10 เขียนว่า “A record declaration does not have an extends clause, so it is not possible to explicitly declare a direct superclass type, even Record.” และ javac ไม่ตอบด้วยข้อความเชิงความหมายด้วยซ้ำ
// ❌ รูปที่ผิดerror: '{' expected record CappedPercent(int pct) extends CappedRule { ^ลูกศรชี้ที่ช่องว่างถัดจาก (int pct) ไม่ใช่ที่คำว่า extends เพราะตัวแยกคำเลิกอ่านต่อตั้งแต่ตรงนั้น
Line กับ Order ของ orderctl เป็น record ส่วน Discount เป็น sealed interface
ที่สมาชิกทั้งสามตัวก็เป็น record · ชนิดข้อมูลของโดเมนนี้จึงติดข้อจำกัดเดียวกันทั้งกอง
และเครื่องมือที่เหลือให้ใช้คือ interface กับการประกอบเท่านั้น
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์// src/main/java/orderctl/CountingRule.java — ห่อ DiscountRule ตัวอื่นไว้ข้างใน แทนการสืบทอดpackage orderctl;
import java.util.Objects;
public final class CountingRule implements DiscountRule {
private final DiscountRule inner; private int seen;
public CountingRule(DiscountRule inner) { this.inner = Objects.requireNonNull(inner, "inner"); }
@Override public long apply(long subtotalSatang) { seen++; return inner.apply(subtotalSatang); }
public int seen() { return seen; }}Go ฝัง interface ไว้ในโครงสร้างแล้วได้ method ส่งต่อมาฟรี ส่วน Java ต้องเขียน apply
ออกมาเอง · ที่นี่เท่ากันเพราะ interface มี method เดียว
สิ่งที่ต่างกันจริงคือ virtual dispatch ที่ย้อนกลับเข้าตัวห่อ ซึ่ง Go ไม่มี
· บทที่ 5 ของคอร์ส Go สาธิตด้วย Printer กับ Fancy ที่พิมพ์ <p:base> ไม่ใช่ <p:fancy>
เพราะชนิดข้างในไม่เคยรู้ว่ามีตัวห่ออยู่
แปลว่า Go เขียน template method ไม่ได้เลย ส่วน Java เขียนได้ แล้วต้องรับกฎมาทั้งชุด
· และข้อที่แพงที่สุดคือชนิดที่รับ template method ต้องเลิกเป็น record
ชุดปิดเทียบชุดเปิด
หัวข้อที่มีชื่อว่า “ชุดปิดเทียบชุดเปิด”
Discountเป็นข้อมูลชุดปิดที่ compiler ไล่ให้ครบ ·DiscountRuleเป็นพฤติกรรมชุดเปิดที่ต่อเพิ่มได้
sealed interfacesealed interfaceinterface ที่ระบุรายชื่อ subtype ที่อนุญาต · subtype ต้องปิดตัวเอง (class เขียน `final`, `sealed` หรือ `non-sealed` ส่วน record กับ enum เป็น final อยู่แล้ว) ของบทที่ 2 มาบรรจบกับ interface ของบทนี้ตรงนี้พอดี สองอย่างนี้หน้าตาคล้ายกันแต่ให้คนละอย่าง
Discount คือข้อมูล ชุดปิด · ใครก็เพิ่มกรณีไม่ได้นอกจากเจ้าของ file
และเมื่อเพิ่ม compiler บอกทุกจุดที่ต้องตามไปแก้
DiscountRule คือพฤติกรรม ชุดเปิด · ใครก็ต่อกฎใหม่เข้ามาได้โดยไม่แตะ code เดิม
แต่จะไม่มีวันไล่รายชื่อกฎทั้งหมดได้
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์// src/main/java/orderctl/Rules.java — สะพานจากชุดปิดไปชุดเปิดpackage orderctl;
import java.util.List;
public final class Rules {
private Rules() {}
/** ชุดปิด: เพิ่มกรณีที่สี่ลง Discount แล้ว switch นี้แดงตอนแปลทันที */ public static DiscountRule of(Discount d) { return switch (d) { case Discount.Percent(int pct) -> new PercentOff(pct); case Discount.Fixed(long satang) -> new FlatOff(satang); case Discount.None ignored -> DiscountRule.none(); }; }
/** ชุดเปิด: ต่อกฎกี่ตัวก็ได้ รวมกฎที่คนอื่นเขียนขึ้นทีหลัง */ public static DiscountRule chain(List<? extends DiscountRule> rules) { DiscountRule chained = DiscountRule.none(); for (DiscountRule r : rules) { chained = chained.andThen(r); } return chained; }}เพิ่ม BuyOneGetOne ลง Discount แล้วไม่แตะ Rules.of จะได้
error: the switch expression does not cover all possible input values ทันที
· ความสามารถข้อนี้มาที่ Java 21 พร้อม pattern matching ใน switch
flowchart LR
subgraph closed["ชุดปิด · Discount คือข้อมูล"]
D1["Discount.Percent"]
D2["Discount.Fixed"]
D3["Discount.None"]
end
B["Rules.of<br/>switch ที่ compiler บังคับให้ครบ"]
subgraph open["ชุดเปิด · DiscountRule คือพฤติกรรม"]
R1["PercentOff"]
R2["FlatOff"]
R3["CountingRule"]
R4["กฎที่คนอื่นเขียนขึ้นทีหลัง"]
end
D1 --> B
D2 --> B
D3 --> B
B --> R1
B --> R2
R1 --> C["andThen ต่อเป็นสายเดียว"]
R2 --> C
R3 --> C
R4 --> C
คำบรรยายภาพ: รายชื่อกรณีทางซ้ายปิดตาย เพิ่มกรณีใหม่แล้ว Rules.of แดงทันที · รายชื่อกฎทางขวาเปิดไว้ ใครต่อเพิ่มก็ได้แต่ไล่รายชื่อทั้งหมดไม่ได้ · Rules.of คือสะพานที่แปลงจากฝั่งซ้ายไปฝั่งขวา
ประโยคที่ควรจำมีข้อเดียว — เลือกชุดปิดเมื่อรายการกรณีนิ่งแต่การกระทำจะเพิ่ม · เลือกชุดเปิดเมื่อการกระทำนิ่งแต่รายการกรณีจะเพิ่ม
variance — กำแพงเดียวกันคนละความสูง
หัวข้อที่มีชื่อว่า “variance — กำแพงเดียวกันคนละความสูง”
List<PercentOff>ส่งเข้าที่ที่ต้องการList<DiscountRule>ไม่ได้ แต่ Java แก้ที่ลายเซ็นได้
PercentOff เป็น DiscountRule แต่ List<PercentOff> ไม่ใช่ List<DiscountRule>
· ข้อนี้เหมือนกันทั้งสองภาษา
// ❌ รูปที่ผิด · chain รับ List<DiscountRule> ตรงๆerror: incompatible types: List<PercentOff> cannot be converted to List<DiscountRule> System.out.println(chain(offers, 20_000L)); ^Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full outputGo จบตรงนี้ · บทที่ 5 ของคอร์ส Go ต้องวนคัดลอกทีละตัวเข้ากล่อง interface ใหม่ ซึ่งเป็นราคาที่ Go เลือกจ่ายอย่างเปิดเผย
Java แก้ที่ลายเซ็นได้ด้วย List<? extends DiscountRule> โดยไม่ต้องคัดลอกอะไรเลย
package orderctl.lab;
import java.util.List;
public final class Var {
interface DiscountRule { long apply(long subtotalSatang); }
record PercentOff(int pct) implements DiscountRule { @Override public long apply(long subtotalSatang) { return subtotalSatang - subtotalSatang * pct / 100; } }
static long chain(List<? extends DiscountRule> rules, long subtotalSatang) { long total = subtotalSatang; for (DiscountRule r : rules) { total = r.apply(total); } return total; }
public static void main(String[] args) { List<PercentOff> offers = List.of(new PercentOff(10), new PercentOff(5)); System.out.println(chain(offers, 20_000L)); }}$ mvn -q compile exec:java -Dexec.mainClass=orderctl.lab.Var17100ราคาของทางนี้คือผู้อ่านต้องเจอเครื่องหมาย ? ตั้งแต่บทนี้ ทั้งที่คอร์สยังไม่ได้สอน generics
· กฎที่ใช้ได้พอสำหรับตอนนี้คือ ใส่ ? extends ที่ลายเซ็นเมื่อคุณแค่อ่านจาก collection นั้น
ประกอบร่างแล้วรันจริง
หัวข้อที่มีชื่อว่า “ประกอบร่างแล้วรันจริง”
Mainเรียกทั้งกฎเดี่ยว กฎที่ต่อกัน กฎที่มาจากข้อมูล และกฎที่ห่อกฎอื่นไว้
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์package orderctl;
import java.util.List;
public final class Main {
public static void main(String[] args) { long subtotal = 20_000L;
DiscountRule member = new PercentOff(10); DiscountRule coupon = new FlatOff(3_000L); System.out.println(member.apply(subtotal)); System.out.println(member.andThen(coupon).apply(subtotal));
DiscountRule fromData = Rules.of(new Discount.Percent(25)); System.out.println(fromData.apply(subtotal));
CountingRule counted = new CountingRule(member); DiscountRule all = Rules.chain(List.of(counted, coupon)); System.out.println(all.apply(subtotal)); System.out.println(all.apply(10_000L)); System.out.println(counted.seen());
DiscountRule lambda = s -> s - 1_500L; // ไม่มีชื่อ class แต่ยังต้องมีชนิดเป้าหมายที่ประกาศไว้ System.out.println(lambda.apply(subtotal)); }}$ mvn -q compile exec:java -Dexec.mainClass=orderctl.Main180001500015000150006000218500exec:java ไม่ผูกกับ phase ใด จึงต้องมีคำว่า compile นำหน้าเสมอ — กฎเดียวกับบทที่ 3
บรรทัดที่ควรดูสองครั้งคือเลข 2 · counted ถูกเรียกสองรอบเพราะ all.apply(...) ถูกเรียกสองครั้ง
ซึ่งพิสูจน์ว่า chain เก็บ CountingRule ตัวเดิมไว้จริง ไม่ได้คัดลอกมันไป
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”
DiscountRuleเสร็จแล้ว และคุณเห็นครบทั้งด้านที่ Java ได้เปรียบและด้านที่ Java จ่ายแพงกว่า
คุณออกแบบ interface method เดียว ต่อกฎเข้าด้วยกันด้วย andThen ห่อกฎด้วย CountingRule
และเห็นแล้วว่าทำไมชนิดของโดเมนนี้ถึงเลือกการประกอบแทนการสืบทอด
| เรื่อง | Java จ่ายอะไร | Go จ่ายอะไร |
|---|---|---|
| ผูกชนิดเข้ากับ interface | ต้องแก้ file ของผู้สร้างชนิด ซึ่งบางทีไม่ใช่ file ของคุณ | ไม่ต้องแก้อะไรเลย แต่ต้องเขียนด่าน var _ Rule = ... เอง ไม่งั้นชื่อที่สะกดผิดเงียบสนิท |
| เพิ่มความสามารถลง interface เดิม | เติมเป็น default แล้วจบ ไม่ต้องแปลใหม่ | ผู้ implement เดิมหลุดทันที ต้องตั้ง interface ตัวใหม่แล้วถามชนิดตอนรัน |
| ตรึงลำดับแล้วเจาะช่องให้ลูกเติม | เขียนได้ด้วย abstract class แต่ชนิดนั้นต้องเลิกเป็น record | เขียนไม่ได้เลย เพราะการฝังไม่มี virtual dispatch ย้อนกลับ |
| ส่ง collection ของชนิดลูก | ใส่ ? extends ที่ลายเซ็น | วนคัดลอกทีละตัวเข้ากล่อง interface ใหม่ |
apply ยังมีรูโหว่ที่บทนี้ปิดไม่ได้ — มันไม่มีทางบอกว่า “กฎนี้ใช้กับออเดอร์นี้ไม่ได้”
ทางเดียวที่เหลือคือโยนความผิดพลาดออกไป
บทหน้าปิดท้ายด้วยข้อจำกัดที่ตามมาจากตรงนั้นพอดี — lambda โยน checked exception ไม่ได้ และนั่นทำให้ interface ทั้งกองของบทนี้ต้องออกแบบใหม่ทันทีที่การตรวจความถูกต้องเข้ามา
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- JLS SE25 · Chapter 9. Interfaces (เข้าถึง 2026-08-27) — §9.3 field ใน interface เป็น
public static finalเสมอ · §9.6.4.9 เหตุผลที่ไม่ควรติด@FunctionalInterfaceให้ทุกตัว - JLS SE25 · Chapter 8. Classes (เข้าถึง 2026-08-27) — §8.4.8 ที่มาของกฎ “class บัง interface” · §8.10
recordไม่มีextendsclause ในไวยากรณ์ - The Java Tutorials — Default Methods (เข้าถึง 2026-08-27) — เจตนาของฟีเจอร์และคำว่า binary compatibility · หน้านี้ประทับตัวเองว่าเขียนไว้สำหรับ JDK 8 จึงอ้างเฉพาะเจตนา ไม่อ้างสำนวน code
- The Java Tutorials — Multiple Inheritance of State (เข้าถึง 2026-08-27) — เหตุผลที่ Java ให้มีแม่ได้ตัวเดียว
- JEP 395: Records (JDK 16) · JEP 409: Sealed Classes (JDK 17) · JEP 441: Pattern Matching for switch (JDK 21) — เส้นเวลาของฟีเจอร์ที่บทนี้ใช้ร่วมกัน
- JEP 213: Milling Project Coin (เข้าถึง 2026-08-27) — Status Closed/Delivered · Release 9 ·
privatemethod ใน interface มาถึงพร้อมรุ่นนี้ ส่วนdefaultกับstaticมาก่อนแล้วที่ Java 8 - Go FAQ — Why is there no type inheritance? (เข้าถึง 2026-08-27) — “Interfaces can be added after the fact if a new idea comes along or for testing—without annotating the original types.” และ “Because there are no explicit relationships between types and interfaces, there is no type hierarchy to manage or discuss.”
- บทที่ 5 ของคอร์ส Go บนเว็บนี้ — หัวข้อ “ห่อ Rule ด้วย Rule แทน template method” และกล่อง “ทำไมต้องเขียน
var _ Rule = PercentOff{}ด้วย”
ทุกข้อความ error ในบทนี้ยกมาจาก javac 25.0.4.1 บน macOS arm64 เมื่อ 2026-08-27
· code ทั้งบทแปลผ่านด้วย javac --release 25 -Xlint:all -Werror โดยไม่มี warning และไม่มี
--enable-preview · block ที่ติดป้าย ชิ้นส่วน ต้องแปลรวมกับ file อื่นของโปรเจกต์
ส่วน block ที่เหลือแปลเดี่ยวได้ทันที
เช็กความเข้าใจ — บทที่ 5
ข้อ 1 / 3ชนิด `Foreign` จาก jar ของคนอื่นมี `public long apply(long)` ที่รูปร่างตรงกับ `DiscountRule` ทุกตัวอักษร ข้อใดถูก