การดำเนินการของโปรแกรม Java ต้องใช้สองขั้นตอน: การคอมไพล์และการดำเนินการ (การตีความ) ในเวลาเดียวกัน Java เป็นภาษาโปรแกรมเชิงวัตถุ เมื่อคลาสย่อยและคลาสพาเรนต์มีเมธอดเดียวกัน และคลาสย่อยแทนที่เมธอดของคลาสพาเรนต์ เมื่อโปรแกรมเรียกใช้เมธอดขณะรันไทม์ ควรเรียกเมธอดของคลาสพาเรนต์หรือเมธอดที่ถูกแทนที่ของคลาสย่อย ควรเป็นคำถามเมื่อเราเรียนรู้ปัญหา Java ครั้งแรก อันดับแรกเราจะพิจารณาว่าจะเรียกวิธีการใดหรือการดำเนินการของตัวแปรที่เรียกว่าการรวม
มีวิธีการเชื่อมโยงสองวิธีใน Java วิธีแรกคือการผูกแบบคงที่หรือที่เรียกว่าการรวมล่วงหน้า อีกประการหนึ่งคือการผูกแบบไดนามิกหรือที่เรียกว่าการรวมล่าช้า
การเปรียบเทียบความแตกต่าง
1. การเชื่อมโยงแบบคงที่จะเกิดขึ้นในเวลาคอมไพล์ และการเชื่อมโยงแบบไดนามิกจะเกิดขึ้นที่รันไทม์
2. ใช้ตัวแปรหรือวิธีการแก้ไขด้วยส่วนตัว คงที่ หรือสุดท้าย และใช้การเชื่อมโยงแบบคงที่ วิธีการเสมือน (วิธีการที่สามารถแทนที่โดยคลาสย่อย) จะถูกผูกไว้แบบไดนามิกตามวัตถุรันไทม์
3. การเชื่อมโยงแบบคงที่เสร็จสมบูรณ์โดยใช้ข้อมูลคลาส ในขณะที่การเชื่อมโยงแบบไดนามิกจะต้องเสร็จสิ้นโดยใช้ข้อมูลอ็อบเจ็กต์
4. วิธีการโอเวอร์โหลดเสร็จสมบูรณ์โดยใช้การเชื่อมโยงแบบคงที่ ในขณะที่วิธีการแทนที่เสร็จสมบูรณ์โดยใช้การเชื่อมโยงแบบไดนามิก
ตัวอย่างวิธีการโอเวอร์โหลด
นี่คือตัวอย่างของวิธีการโอเวอร์โหลด
คัดลอกรหัสรหัสดังต่อไปนี้:
TestMain คลาสสาธารณะ {
โมฆะคงที่สาธารณะ main (String [] args) {
สตริง str = สตริงใหม่ ();
ผู้โทร ผู้โทร = ผู้โทรใหม่();
ผู้โทร.โทร(str);
-
ผู้โทรคลาสคงที่ {
การเรียกโมฆะสาธารณะ (Object obj) {
System.out.println("อินสแตนซ์ของวัตถุใน Caller");
-
การโทรเป็นโมฆะสาธารณะ (String str) {
System.out.println("อินสแตนซ์สตริงในผู้โทร");
-
-
-
ผลการดำเนินการก็คือ
คัดลอกรหัสรหัสดังต่อไปนี้:
22:19 $javaTestMain
อินสแตนซ์ String ใน Caller
ในโค้ดข้างต้น มีการใช้งานเมธอดการโทรที่โอเวอร์โหลดสองครั้ง อย่างหนึ่งได้รับวัตถุประเภท Object เป็นพารามิเตอร์ และอีกวิธีหนึ่งได้รับวัตถุประเภท String เป็นพารามิเตอร์ str เป็นวัตถุ String และวิธีการเรียกทั้งหมดที่ได้รับพารามิเตอร์ประเภท String จะถูกเรียก การเชื่อมโยงที่นี่คือการเชื่อมโยงแบบคงที่ตามประเภทพารามิเตอร์ ณ เวลารวบรวม
ตรวจสอบ
เพียงแค่ดูรูปลักษณ์ภายนอกก็ไม่สามารถพิสูจน์ได้ว่ามีการผูกแบบคงที่ คุณสามารถตรวจสอบได้โดยใช้ javap เพื่อคอมไพล์
คัดลอกรหัสรหัสดังต่อไปนี้:
22:19 $ javap -c TestMain
เรียบเรียงจาก "TestMain.java"
TestMain คลาสสาธารณะ {
TestMain สาธารณะ ();
รหัส:
0: aload_0
1: เรียกใช้พิเศษ #1 // วิธีการ java/lang/Object"<init>":()V
4: กลับ
โมฆะคงสาธารณะ main(java.lang.String[]);
รหัส:
0: ใหม่ #2 // คลาส java/lang/String
3: ซ้ำ
4: เรียกใช้พิเศษ #3 // วิธีการ java/lang/String"<init>":()V
7: astore_1
8: ใหม่ #4 // คลาส TestMain$Caller
11: ซ้ำ
12: เรียกใช้พิเศษ #5 // วิธี TestMain$Caller"<init>":()V
15: astore_2
16: aload_2
17: aload_1
18: เรียกใช้เสมือน #6 // วิธี TestMain$Caller.call:(Ljava/lang/String;)V
21: กลับมา
-
ฉันเห็นบรรทัดที่ 18 นี้: invokevirtual #6 // Method TestMain$Caller.call:(Ljava/lang/String;)V is really static bound, ซึ่งยืนยันว่าเมธอดผู้เรียกที่ได้รับอ็อบเจ็กต์ String เป็นพารามิเตอร์ถูกเรียก
ตัวอย่างของการแทนที่วิธีการ
คัดลอกรหัสรหัสดังต่อไปนี้:
TestMain คลาสสาธารณะ {
โมฆะคงที่สาธารณะ main (String [] args) {
สตริง str = สตริงใหม่ ();
ผู้โทรเข้า = SubCaller ใหม่ ();
ผู้โทร.โทร(str);
-
ผู้โทรคลาสคงที่ {
การโทรเป็นโมฆะสาธารณะ (String str) {
System.out.println("อินสแตนซ์สตริงใน Caller");
-
-
SubCaller คลาสคงที่ขยายผู้โทร {
@แทนที่
การโทรเป็นโมฆะสาธารณะ (String str) {
System.out.println("อินสแตนซ์สตริงใน SubCaller");
-
-
-
ผลการดำเนินการก็คือ
คัดลอกรหัสรหัสดังต่อไปนี้:
22:27 $javaTestMain
อินสแตนซ์ String ใน SubCaller
ในโค้ดข้างต้น มีการนำวิธีการโทรไปใช้ใน Caller เราประกาศตัวแปร callerSub ประเภท Caller แต่ตัวแปรนี้ชี้ไปที่วัตถุ SubCaller จากผลลัพธ์จะเห็นได้ว่าเรียกการใช้เมธอดการโทรของ SubCaller แทนการใช้เมธอดการโทรของ Caller เหตุผลสำหรับผลลัพธ์นี้คือ การเชื่อมโยงแบบไดนามิกเกิดขึ้นเมื่อรันไทม์ และในระหว่างกระบวนการเชื่อมโยง จำเป็นต้องกำหนดเวอร์ชันของการนำเมธอดการโทรไปใช้ที่จะเรียกใช้
ตรวจสอบ
การเชื่อมโยงแบบไดนามิกไม่สามารถตรวจสอบได้โดยตรงโดยใช้ javap และหากพิสูจน์ได้ว่าไม่ได้ดำเนินการการเชื่อมโยงแบบคงที่ นั่นหมายความว่ามีการดำเนินการการเชื่อมโยงแบบไดนามิก
คัดลอกรหัสรหัสดังต่อไปนี้:
22:27 $ javap -c TestMain
เรียบเรียงจาก "TestMain.java"
TestMain คลาสสาธารณะ {
TestMain สาธารณะ ();
รหัส:
0: aload_0
1: เรียกใช้พิเศษ #1 // วิธีการ java/lang/Object"<init>":()V
4: กลับ
โมฆะคงสาธารณะ main(java.lang.String[]);
รหัส:
0: ใหม่ #2 // คลาส java/lang/String
3: ซ้ำ
4: เรียกใช้พิเศษ #3 // วิธีการ java/lang/String"<init>":()V
7: astore_1
8: ใหม่ #4 // คลาส TestMain$SubCaller
11: ซ้ำ
12: เรียกใช้พิเศษ #5 // วิธี TestMain$SubCaller"<init>":()V
15: astore_2
16: aload_2
17: aload_1
18: เรียกใช้เสมือน #6 // วิธี TestMain$Caller.call:(Ljava/lang/String;)V
21: กลับมา
-
ตามผลลัพธ์ข้างต้น 18: involvevirtual #6 // Method TestMain$Caller.call:(Ljava/lang/String;)V นี่คือ TestMain$Caller.call แทนที่จะเป็น TestMain$SubCaller.call เนื่องจากไม่สามารถกำหนดรูทีนย่อยการเรียกได้ ณ เวลาคอมไพล์ คลาสยังคงเป็นการนำคลาสพาเรนต์ไปใช้ ดังนั้นจึงสามารถจัดการได้โดยการผูกแบบไดนามิกที่รันไทม์เท่านั้น
เมื่อการโหลดซ้ำตรงกับการเขียนใหม่
ตัวอย่างต่อไปนี้ค่อนข้างผิดปกติ วิธีการโทรมีมากเกินไปสองครั้งในคลาส Caller สิ่งที่ซับซ้อนกว่าคือ SubCaller รวม Caller และแทนที่ทั้งสองวิธี ที่จริงแล้ว สถานการณ์นี้เป็นสถานการณ์ที่ซับซ้อนของสองสถานการณ์ข้างต้น
รหัสต่อไปนี้จะทำการผูกแบบคงที่ก่อนเพื่อกำหนดวิธีการเรียกที่มีพารามิเตอร์เป็นวัตถุ String จากนั้นจึงทำการผูกแบบไดนามิกที่รันไทม์เพื่อพิจารณาว่าจะดำเนินการใช้งานการเรียกของคลาสย่อยหรือคลาสพาเรนต์
คัดลอกรหัสรหัสดังต่อไปนี้:
TestMain คลาสสาธารณะ {
โมฆะคงที่สาธารณะ main (String [] args) {
สตริง str = สตริงใหม่ ();
ผู้โทร callerSub = SubCaller ใหม่ ();
callerSub.call(str);
-
ผู้โทรคลาสคงที่ {
การเรียกโมฆะสาธารณะ (Object obj) {
System.out.println("อินสแตนซ์ของวัตถุใน Caller");
-
การโทรเป็นโมฆะสาธารณะ (String str) {
System.out.println("อินสแตนซ์สตริงในผู้โทร");
-
-
SubCaller คลาสคงที่ขยายผู้โทร {
@แทนที่
การเรียกโมฆะสาธารณะ (Object obj) {
System.out.println("อินสแตนซ์วัตถุใน SubCaller");
-
@แทนที่
การโทรเป็นโมฆะสาธารณะ (String str) {
System.out.println("อินสแตนซ์สตริงใน SubCaller");
-
-
-
ผลการดำเนินการก็คือ
คัดลอกรหัสรหัสดังต่อไปนี้:
22:30 $javaTestMain
อินสแตนซ์ String ใน SubCaller
ตรวจสอบ
เนื่องจากได้มีการแนะนำไปแล้วข้างต้น ฉันจะโพสต์ผลลัพธ์การแยกคอมไพล์ที่นี่เท่านั้น
คัดลอกรหัสรหัสดังต่อไปนี้:
22:30 $ javap -c TestMain
เรียบเรียงจาก "TestMain.java"
TestMain คลาสสาธารณะ {
TestMain สาธารณะ ();
รหัส:
0: aload_0
1: เรียกใช้พิเศษ #1 // วิธีการ java/lang/Object"<init>":()V
4: กลับ
โมฆะคงสาธารณะ main(java.lang.String[]);
รหัส:
0: ใหม่ #2 // คลาส java/lang/String
3: ซ้ำ
4: เรียกใช้พิเศษ #3 // วิธีการ java/lang/String"<init>":()V
7: astore_1
8: ใหม่ #4 // คลาส TestMain$SubCaller
11: ซ้ำ
12: เรียกใช้พิเศษ #5 // วิธี TestMain$SubCaller"<init>":()V
15: astore_2
16: aload_2
17: aload_1
18: เรียกใช้เสมือน #6 // วิธี TestMain$Caller.call:(Ljava/lang/String;)V
21: กลับมา
-
คำถามที่อยากรู้อยากเห็น
ไม่สามารถใช้การเชื่อมโยงแบบไดนามิกได้หรือไม่
ในความเป็นจริง ตามทฤษฎีแล้ว การผูกมัดของวิธีการบางอย่างสามารถทำได้โดยการผูกแบบคงที่เช่นกัน ตัวอย่างเช่น:
คัดลอกรหัสรหัสดังต่อไปนี้:
โมฆะคงที่สาธารณะ main (String [] args) {
สตริง str = สตริงใหม่ ();
ผู้โทรเข้าสุดท้าย callerSub = SubCaller ใหม่ ();
callerSub.call(str);
-
ตัวอย่างเช่น ที่นี่ callerSub เก็บอ็อบเจ็กต์ของ subCaller และตัวแปร callerSub ถือเป็นที่สิ้นสุด และวิธีการเรียกจะถูกดำเนินการทันที ในทางทฤษฎี คอมไพเลอร์สามารถรู้ได้ว่าวิธีการเรียกของ SubCaller ควรถูกเรียกโดยการวิเคราะห์โค้ดที่เพียงพอ
แต่ทำไมถึงไม่มีการผูกมัดแบบคงที่?
สมมติว่า Caller ของเราสืบทอดมาจากคลาส BaseCaller ของเฟรมเวิร์กบางตัว ซึ่งใช้วิธีการโทร และ BaseCaller สืบทอดมาจาก SuperCaller วิธีการโทรยังถูกนำมาใช้ใน SuperCaller
สมมติว่า BaseCaller และ SuperCaller ในเฟรมเวิร์ก 1.0
คัดลอกรหัสรหัสดังต่อไปนี้:
SuperCaller คลาสคงที่ {
การเรียกโมฆะสาธารณะ (Object obj) {
System.out.println("อินสแตนซ์วัตถุใน SuperCaller");
-
-
BaseCaller คลาสคงที่ขยาย SuperCaller {
การเรียกโมฆะสาธารณะ (Object obj) {
System.out.println("อินสแตนซ์ของวัตถุใน BaseCaller");
-
-
เราดำเนินการนี้โดยใช้กรอบงาน 1.0 ผู้โทรสืบทอดมาจาก BaseCaller และเรียกใช้เมธอด super.call
คัดลอกรหัสรหัสดังต่อไปนี้:
TestMain คลาสสาธารณะ {
โมฆะคงที่สาธารณะ main (String [] args) {
วัตถุ obj = วัตถุใหม่ ();
SuperCaller callerSub = SubCaller ใหม่ ();
callerSub.call(obj);
-
Caller คลาสคงที่ขยาย BaseCaller {
การเรียกโมฆะสาธารณะ (Object obj) {
System.out.println("อินสแตนซ์ของวัตถุใน Caller");
ซุปเปอร์.โทร(obj);
-
การโทรเป็นโมฆะสาธารณะ (String str) {
System.out.println("อินสแตนซ์สตริงในผู้โทร");
-
-
SubCaller คลาสคงที่ขยายผู้โทร {
@แทนที่
การเรียกโมฆะสาธารณะ (Object obj) {
System.out.println("อินสแตนซ์วัตถุใน SubCaller");
-
@แทนที่
การโทรเป็นโมฆะสาธารณะ (String str) {
System.out.println("อินสแตนซ์สตริงใน SubCaller");
-
-
-
จากนั้นเรารวบรวมไฟล์คลาสตามเวอร์ชัน 1.0 ของเฟรมเวิร์กนี้ สมมติว่าการรวมแบบคงที่สามารถระบุได้ว่า super.call ของผู้เรียกข้างต้นถูกนำไปใช้เป็น BaseCaller.call
จากนั้นเราถือว่า BaseCaller ไม่ได้เขียนวิธีการโทรของ SuperCaller ใหม่ในเวอร์ชัน 1.1 ของเฟรมเวิร์กนี้ จากนั้นสมมติฐานข้างต้นที่ว่าการใช้งานการโทรที่สามารถผูกไว้แบบคงที่จะทำให้เกิดปัญหาในเวอร์ชัน 1.1 เนื่องจาก super.call ควรใช้ SuperCall ในเวอร์ชัน 1.1. การใช้เมธอดการโทร แทนที่จะคิดว่าการใช้เมธอดการโทรของ BaseCaller ถูกกำหนดโดยการเชื่อมโยงแบบคงที่
ดังนั้นบางสิ่งที่สามารถผูกมัดแบบคงที่ได้จริง ๆ จะถูกผูกไว้แบบไดนามิกโดยคำนึงถึงความปลอดภัยและความสม่ำเสมอ
ได้รับแรงบันดาลใจในการเพิ่มประสิทธิภาพหรือไม่?
เนื่องจากการผูกแบบไดนามิกจำเป็นต้องกำหนดเวอร์ชันของการนำเมธอดหรือตัวแปรไปใช้ที่รันไทม์ จึงใช้เวลานานกว่าการผูกแบบคงที่
ดังนั้น โดยไม่ส่งผลกระทบต่อการออกแบบโดยรวม เราสามารถพิจารณาแก้ไขวิธีการหรือตัวแปรด้วยไพรเวต สแตติก หรือขั้นสุดท้ายได้