เพื่อที่จะเข้าใจว่าการผูกแบบคงที่และแบบไดนามิกทำงานอย่างไร? หรือวิธีระบุโดยคอมไพเลอร์และ JVM?
ลองมาตัวอย่างด้านล่างที่Mammal
เป็นระดับผู้ปกครองซึ่งมีวิธีการspeak()
และHuman
ระดับขยายMammal
, แทนที่speak()
วิธีการและจากนั้นอีกครั้ง overloads speak(String language)
มันด้วย
public class OverridingInternalExample {
private static class Mammal {
public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); }
}
private static class Human extends Mammal {
@Override
public void speak() { System.out.println("Hello"); }
public void speak(String language) {
if (language.equals("Hindi")) System.out.println("Namaste");
else System.out.println("Hello");
}
@Override
public String toString() { return "Human Class"; }
}
public static void main(String[] args) {
Mammal anyMammal = new Mammal();
anyMammal.speak();
Mammal humanMammal = new Human();
humanMammal.speak();
Human human = new Human();
human.speak();
human.speak("Hindi");
}
}
เมื่อเรารวบรวมโค้ดด้านบนและลองดูที่ bytecode โดยใช้javap -verbose OverridingInternalExample
เราจะเห็นว่าคอมไพเลอร์สร้างตารางคงที่ซึ่งจะกำหนดรหัสจำนวนเต็มให้กับการเรียกใช้เมธอดทุกครั้งและรหัสไบต์สำหรับโปรแกรมที่ฉันได้แยกและรวมไว้ในโปรแกรมเอง ( ดูความคิดเห็นด้านล่างทุกวิธีการโทร)
โดยดูที่โค้ดข้างต้นเราจะเห็นว่า bytecodes ของhumanMammal.speak()
, human.speak()
และhuman.speak("Hindi")
มีความแตกต่างกันโดยสิ้นเชิง ( invokevirtual #4
, invokevirtual #7
, invokevirtual #9
) เพราะคอมไพเลอร์จะสามารถแยกความแตกต่างระหว่างพวกเขาขึ้นอยู่กับรายการอาร์กิวเมนต์และการอ้างอิงระดับ เพราะทั้งหมดนี้ได้รับการแก้ไขที่รวบรวมเวลาแบบคงที่นั่นคือเหตุผลที่วิธีการมากไปเป็นที่รู้จักกันคงความแตกต่างหรือคงผูกพัน
แต่ bytecode สำหรับanyMammal.speak()
และhumanMammal.speak()
เหมือนกัน ( invokevirtual #4
) เนื่องจากตามคอมไพเลอร์ทั้งสองวิธีถูกเรียกโดยMammal
อ้างอิง
ดังนั้นคำถามเกิดขึ้นหากการเรียกทั้งสองวิธีมีรหัสไบต์เดียวกันแล้ว JVM จะรู้ได้อย่างไรว่าจะเรียกวิธีใด
คำตอบซ่อนอยู่ใน bytecode เองและเป็นinvokevirtual
ชุดคำสั่ง JVM ใช้invokevirtual
คำสั่งเพื่อเรียกใช้ Java ที่เทียบเท่ากับวิธีการเสมือน C ++ ใน C ++ หากเราต้องการแทนที่เมธอดหนึ่งในคลาสอื่นเราจำเป็นต้องประกาศว่าเป็นเสมือน แต่ใน Java เมธอดทั้งหมดจะเป็นเสมือนตามค่าเริ่มต้นเนื่องจากเราสามารถแทนที่ทุกเมธอดในคลาสลูกได้ (ยกเว้นเมธอดส่วนตัวขั้นสุดท้ายและสแตติก)
ใน Java ตัวแปรอ้างอิงทุกตัวมีตัวชี้ที่ซ่อนอยู่สองตัว
- ตัวชี้ไปยังตารางซึ่งเก็บวิธีการของวัตถุไว้อีกครั้งและตัวชี้ไปยังวัตถุคลาส เช่น [speak (), speak (String) Class object]
- ตัวชี้ไปยังหน่วยความจำที่จัดสรรบนฮีปสำหรับข้อมูลของวัตถุนั้นเช่นค่าของตัวแปรอินสแตนซ์
ดังนั้นการอ้างอิงอ็อบเจ็กต์ทั้งหมดจะถือการอ้างอิงไปยังตารางโดยอ้อมซึ่งเก็บการอ้างอิงเมธอดทั้งหมดของอ็อบเจ็กต์นั้น Java ได้ยืมแนวคิดนี้มาจาก C ++ และตารางนี้เรียกว่าตารางเสมือน (vtable)
vtable คือโครงสร้างที่เหมือนกับอาร์เรย์ซึ่งเก็บชื่อเมธอดเสมือนและการอ้างอิงดัชนีอาร์เรย์ JVM สร้างเพียงหนึ่ง vtable ต่อคลาสเมื่อโหลดคลาสลงในหน่วยความจำ
ดังนั้นเมื่อใดก็ตามที่ JVM พบกับinvokevirtual
ชุดคำสั่งมันจะตรวจสอบ vtable ของคลาสนั้นสำหรับการอ้างอิงเมธอดและเรียกใช้เมธอดเฉพาะซึ่งในกรณีของเราคือเมธอดจากอ็อบเจ็กต์ไม่ใช่การอ้างอิง
เพราะทั้งหมดนี้ได้รับการแก้ไขที่รันไทม์เท่านั้นและที่รันไทม์ JVM ได้รับรู้วิธีการที่จะก่อให้เกิดนั่นคือเหตุผลที่วิธีการเอาชนะเป็นที่รู้จักกันแบบไดนามิก Polymorphismหรือเพียงแค่ความแตกต่างหรือแบบไดนามิกผูกพัน
คุณสามารถอ่านได้รายละเอียดเพิ่มเติมในบทความของฉันอย่างไร JVM จับวิธีมากไปและแทนที่ภายใน