Java8: เหตุใดจึงห้ามไม่ให้กำหนดวิธีการเริ่มต้นสำหรับวิธีการจาก java.lang.Object


130

วิธีการเริ่มต้นเป็นเครื่องมือใหม่ที่ดีในกล่องเครื่องมือ Java ของเรา อย่างไรก็ตามฉันพยายามเขียนอินเทอร์เฟซที่กำหนดdefaultเวอร์ชันของtoStringวิธีการ Java บอกฉันว่าสิ่งนี้เป็นสิ่งต้องห้ามเนื่องจากวิธีการที่ประกาศไว้java.lang.Objectอาจไม่ได้รับการdefaultแก้ไข เหตุใดจึงเป็นเช่นนี้

ฉันรู้ว่ามีกฎ "คลาสฐานเสมอชนะ" ดังนั้นโดยค่าเริ่มต้น (pun;) defaultการใช้งานObjectเมธอดใด ๆจะถูกเขียนทับโดยวิธีการObjectต่อไป อย่างไรก็ตามฉันไม่เห็นเหตุผลว่าทำไมจึงไม่ควรมีข้อยกเว้นสำหรับวิธีการจากObjectในข้อมูลจำเพาะ โดยเฉพาะอย่างยิ่งสำหรับtoStringการใช้งานเริ่มต้นอาจมีประโยชน์มาก

แล้วอะไรคือสาเหตุที่นักออกแบบ Java ตัดสินใจไม่อนุญาตให้ใช้defaultวิธีการแทนที่เมธอดจากObject?


1
ฉันรู้สึกดีกับตัวเองมากตอนนี้โหวตให้ 100 ครั้งแล้วก็ได้เหรียญทอง คำถามที่ดี!
ยูจีน

คำตอบ:


186

นี่เป็นอีกหนึ่งปัญหาในการออกแบบภาษาที่ดูเหมือนว่า "เป็นความคิดที่ดี" จนกว่าคุณจะเริ่มขุดคุ้ยและรู้ว่านั่นเป็นความคิดที่ไม่ดี

เมลนี้มีเนื้อหามากมายเกี่ยวกับเรื่องนี้ (และในเรื่องอื่น ๆ ด้วย) มีกองกำลังออกแบบหลายอย่างที่มาบรรจบกันเพื่อนำเราไปสู่การออกแบบปัจจุบัน:

  • ความปรารถนาที่จะรักษารูปแบบการสืบทอดให้เรียบง่าย
  • ความจริงที่ว่าเมื่อคุณดูตัวอย่างที่เห็นได้ชัด (เช่นเปลี่ยนAbstractListเป็นอินเทอร์เฟซ) คุณจะรู้ว่าการสืบทอด equals / hashCode / toString นั้นเชื่อมโยงอย่างมากกับการสืบทอดและสถานะเดียว
  • มันอาจเปิดประตูไปสู่พฤติกรรมที่น่าประหลาดใจ

คุณได้สัมผัสกับเป้าหมาย "ทำให้มันง่าย" แล้ว กฎการสืบทอดและการแก้ปัญหาความขัดแย้งได้รับการออกแบบมาให้ง่ายมาก (คลาสชนะเหนืออินเทอร์เฟซอินเทอร์เฟซที่ได้รับจะชนะเหนืออินเทอร์เฟซที่ได้รับและข้อขัดแย้งอื่น ๆ จะได้รับการแก้ไขโดยคลาสที่ใช้งาน) แน่นอนว่ากฎเหล่านี้สามารถปรับแต่งเพื่อให้มีข้อยกเว้นได้ แต่ ฉันคิดว่าคุณจะพบเมื่อคุณเริ่มดึงสตริงนั้นความซับซ้อนที่เพิ่มขึ้นนั้นไม่ได้เล็กอย่างที่คุณคิด

แน่นอนว่ามีประโยชน์ระดับหนึ่งที่จะปรับความซับซ้อนให้มากขึ้น แต่ในกรณีนี้ไม่มี วิธีการที่เราพูดถึงที่นี่คือเท่ากับ hashCode และ toString วิธีการเหล่านี้ล้วนเกี่ยวกับสถานะออบเจ็กต์อย่างแท้จริงและเป็นคลาสที่เป็นเจ้าของสถานะไม่ใช่อินเทอร์เฟซซึ่งอยู่ในตำแหน่งที่ดีที่สุดในการพิจารณาว่าความเท่าเทียมกันหมายความว่าอย่างไรสำหรับคลาสนั้น (โดยเฉพาะอย่างยิ่งเนื่องจากสัญญาเพื่อความเท่าเทียมกันนั้นค่อนข้างแข็งแกร่งดูประสิทธิผล Java สำหรับผลลัพธ์ที่น่าประหลาดใจ); ผู้เขียนอินเทอร์เฟซถูกลบออกไปไกลเกินไป

ดึงAbstractListตัวอย่างออกมาได้ง่าย คงจะดีไม่น้อยหากเราสามารถกำจัดAbstractListและนำพฤติกรรมดังกล่าวลงในListอินเทอร์เฟซได้ แต่เมื่อคุณก้าวไปไกลกว่าตัวอย่างที่ชัดเจนนี้แล้วจะไม่มีตัวอย่างที่ดีอื่น ๆ อีกมากมายให้พบ ที่รากAbstractListถูกออกแบบมาสำหรับการสืบทอดครั้งเดียว แต่อินเทอร์เฟซต้องได้รับการออกแบบสำหรับการสืบทอดหลาย ๆ

นอกจากนี้ลองจินตนาการว่าคุณกำลังเขียนชั้นเรียนนี้:

class Foo implements com.libraryA.Bar, com.libraryB.Moo { 
    // Implementation of Foo, that does NOT override equals
}

Fooเขียนรูปลักษณ์ที่ supertypes Objectที่เห็นการดำเนินการเท่ากับไม่มีและสรุปว่าจะได้รับความเสมอภาคอ้างอิงทั้งหมดที่เขาต้องทำคือเท่ากับสืบทอดมาจาก จากนั้นในสัปดาห์หน้าผู้ดูแลห้องสมุดสำหรับ Bar "helpfully" จะเพิ่มการequalsใช้งานเริ่มต้น อ๊ะ! ตอนนี้ความหมายของFooอินเทอร์เฟซในโดเมนการบำรุงรักษาอื่น "เป็นประโยชน์" ได้เพิ่มค่าเริ่มต้นสำหรับวิธีการทั่วไป

ค่าเริ่มต้นควรเป็นค่าเริ่มต้น การเพิ่มค่าดีฟอลต์ให้กับอินเทอร์เฟซที่ไม่มี (ที่ใดก็ได้ในลำดับชั้น) ไม่ควรส่งผลกระทบต่อความหมายของคลาสการใช้งานคอนกรีต แต่ถ้าค่าเริ่มต้นสามารถ "ลบล้าง" เมธอด Object ได้นั่นจะไม่เป็นความจริง

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


13
ฉันดีใจที่คุณสละเวลาอธิบายเรื่องนี้และขอขอบคุณทุกปัจจัยที่ได้รับการพิจารณา ผมเห็นว่าเรื่องนี้จะเป็นอันตรายสำหรับhashCodeและแต่ฉันคิดว่ามันจะเป็นประโยชน์อย่างมากสำหรับequals toStringตัวอย่างเช่นบางDisplayableอินเตอร์เฟซที่อาจกำหนดString display()วิธีการและมันจะบันทึกตันสำเร็จรูปเพื่อให้สามารถที่จะกำหนดdefault String toString() { return display(); }ในDisplayableแทนที่จะต้องทุกเดียวDisplayableที่จะใช้toString()หรือขยายDisplayableToStringชั้นฐาน
Brandon

8
@ แบรนดอนคุณถูกต้องที่อนุญาตให้สืบทอด toString () จะไม่เป็นอันตรายในลักษณะเดียวกับที่จะเป็นเท่ากับ () และ hashCode () ในทางกลับกันตอนนี้คุณสมบัติจะผิดปกติมากขึ้น - และคุณยังคงต้องเผชิญกับความซับซ้อนเพิ่มเติมเหมือนกันทั้งหมดเกี่ยวกับกฎการสืบทอดเพื่อประโยชน์ของวิธีการเดียวนี้ ... .
Brian Goetz

5
@gexicide หากtoString()ใช้เฉพาะวิธีการอินเทอร์เฟซคุณสามารถเพิ่มสิ่งที่ต้องการdefault String toStringImpl()ลงในอินเทอร์เฟซและแทนที่toString()ในแต่ละคลาสย่อยเพื่อเรียกการใช้งานอินเทอร์เฟซ - ค่อนข้างน่าเกลียด แต่ใช้งานได้และดีกว่าไม่มีอะไรเลย :) วิธีที่จะทำก็คือการทำให้สิ่งที่ชอบObjects.hash(), และArrays.deepEquals() Arrays.deepToString()+1 สำหรับคำตอบของ @ BrianGoetz!
ซิวฉิงป่อง -Asuka Kenji-

3
พฤติกรรมเริ่มต้นของ toString () ของแลมบ์ดานั้นน่ารังเกียจจริงๆ ฉันรู้ว่าโรงงานแลมบ์ดาถูกออกแบบมาให้เรียบง่ายและรวดเร็ว แต่การคายชื่อคลาสที่ผิดไปจากเดิมนั้นไม่เป็นประโยชน์ การมีการdefault toString()แทนที่ในอินเทอร์เฟซที่ใช้งานได้จะช่วยให้เรา - อย่างน้อยที่สุด - ทำบางสิ่งเช่นพ่นลายเซ็นของฟังก์ชันและคลาสแม่ของตัวดำเนินการ ยิ่งไปกว่านั้นถ้าเราสามารถนำกลยุทธ์ toString แบบวนกลับมาใช้ได้เราสามารถเดินผ่านการปิดเพื่อให้ได้คำอธิบายที่ดีจริงๆของแลมด้าและทำให้เส้นโค้งการเรียนรู้แลมด้าดีขึ้นอย่างมาก
Groostav

การเปลี่ยนแปลง toString ในคลาสคลาสย่อยสมาชิกอินสแตนซ์ใด ๆ อาจมีผลต่อการนำคลาสหรือผู้ใช้คลาสไปใช้ นอกจากนี้การเปลี่ยนแปลงวิธีการเริ่มต้นใด ๆ อาจส่งผลต่อคลาสที่ใช้งานทั้งหมด มีอะไรพิเศษกับ toString คือ hashCode เมื่อพูดถึงคนที่เปลี่ยนแปลงพฤติกรรมของอินเทอร์เฟซ? หากชั้นเรียนขยายชั้นเรียนอื่นก็สามารถเปลี่ยนแปลงได้เช่นกัน หรือถ้าพวกเขากำลังใช้รูปแบบการมอบหมาย ผู้ที่ใช้อินเตอร์เฟส Java 8 จะต้องทำการอัปเกรด อาจมีการแจ้งคำเตือน / ข้อผิดพลาดที่สามารถระงับในคลาสย่อยได้
mmm

30

ห้ามมิให้กำหนดเมธอดเริ่มต้นในอินเทอร์เฟซสำหรับเมธอดในjava.lang.Objectเนื่องจากเมธอดเริ่มต้นจะไม่ "เข้าถึงได้"

วิธีการอินเทอร์เฟซเริ่มต้นสามารถเขียนทับได้ในคลาสที่ใช้อินเทอร์เฟซและการใช้งานคลาสของเมธอดนั้นมีลำดับความสำคัญสูงกว่าการใช้งานอินเทอร์เฟซแม้ว่าเมธอดนั้นจะถูกนำไปใช้ในซูเปอร์คลาส เนื่องจากคลาสทั้งหมดสืบทอดมาจากjava.lang.Objectเมธอดในjava.lang.Objectจะมีความสำคัญเหนือกว่าเมธอดเริ่มต้นในอินเทอร์เฟซและถูกเรียกใช้แทน

Brian Goetz จาก Oracle ให้รายละเอียดเพิ่มเติมเล็กน้อยเกี่ยวกับการตัดสินใจออกแบบในโพสต์รายชื่ออีเมลนี้


3

ฉันไม่เห็นในส่วนหัวของผู้เขียนภาษา Java ดังนั้นเราอาจเดาได้เท่านั้น แต่ฉันเห็นเหตุผลหลายประการและเห็นด้วยกับพวกเขาอย่างยิ่งในปัญหานี้

เหตุผลหลักในการแนะนำวิธีการเริ่มต้นคือเพื่อให้สามารถเพิ่มวิธีการใหม่ให้กับอินเทอร์เฟซได้โดยไม่ทำลายความเข้ากันได้ย้อนหลังของการใช้งานรุ่นเก่า นอกจากนี้ยังอาจใช้เมธอดเริ่มต้นเพื่อจัดเตรียมวิธีการ "อำนวยความสะดวก" โดยไม่จำเป็นต้องกำหนดไว้ในแต่ละคลาสที่นำไปใช้

ไม่มีสิ่งเหล่านี้ใช้กับStringและวิธีการอื่น ๆ ของ Object กล่าวง่ายๆคือวิธีการเริ่มต้นได้รับการออกแบบมาเพื่อให้ลักษณะการทำงานเริ่มต้นโดยที่ไม่มีคำจำกัดความอื่น ไม่จัดให้มีการใช้งานที่จะ "แข่งขัน" กับการใช้งานอื่น ๆ ที่มีอยู่

กฎ "คลาสพื้นฐานมักจะชนะ" ก็มีเหตุผลที่ชัดเจนเช่นกัน ควรที่คลาสจะกำหนดการใช้งานจริงในขณะที่อินเทอร์เฟซกำหนดการใช้งานเริ่มต้นซึ่งค่อนข้างอ่อนกว่า

นอกจากนี้การแนะนำข้อยกเว้นใด ๆ ให้กับกฎทั่วไปทำให้เกิดความซับซ้อนโดยไม่จำเป็นและทำให้เกิดคำถามอื่น ๆ Object คือ (มากหรือน้อย) เป็นคลาสอื่น ๆ ดังนั้นทำไมจึงควรมีพฤติกรรมที่แตกต่างกัน?

ทั้งหมดนี้วิธีแก้ปัญหาที่คุณเสนออาจทำให้เกิดข้อเสียมากกว่าข้อดี


ฉันไม่สังเกตเห็นวรรคสองของคำตอบของ gexicide เมื่อโพสต์ของฉัน มีลิงก์ที่อธิบายปัญหาโดยละเอียด
Marwin

1

การให้เหตุผลนั้นง่ายมากเนื่องจาก Object เป็นคลาสพื้นฐานสำหรับคลาส Java ทั้งหมด ดังนั้นแม้ว่าเราจะกำหนดวิธีการของ Object ให้เป็นวิธีการเริ่มต้นในบางอินเทอร์เฟซ แต่ก็จะไม่มีประโยชน์เพราะเมธอดของ Object จะถูกใช้เสมอ นั่นคือเหตุผลที่เพื่อหลีกเลี่ยงความสับสนเราไม่สามารถมีเมธอดเริ่มต้นที่จะลบล้างเมธอดคลาสอ็อบเจ็กต์ได้


1

ในการให้คำตอบที่อวดดีมากห้ามมิให้กำหนดdefaultวิธีการสำหรับวิธีสาธารณะjava.lang.Objectเท่านั้น มีวิธีพิจารณา 11 วิธีซึ่งแบ่งได้เป็นสามวิธีในการตอบคำถามนี้

  1. หกObjectวิธีการไม่สามารถมีdefaultวิธีการเพราะพวกเขาจะfinalไม่สามารถแทนที่ที่ทั้งหมด: getClass(), notify(), notifyAll(), wait(), และwait(long)wait(long, int)
  2. สามObjectวิธีการไม่สามารถมีdefaultวิธีการด้วยเหตุผลดังกล่าวข้างต้นโดยไบรอันเก๊: equals(Object), และhashCode()toString()
  3. สองObjectวิธีสามารถมีdefaultวิธีการ แต่คุ้มค่าของการเริ่มต้นดังกล่าวเป็นที่น่าสงสัยที่ดีที่สุด: และclone()finalize()

    public class Main {
        public static void main(String... args) {
            new FOO().clone();
            new FOO().finalize();
        }
    
        interface ClonerFinalizer {
            default Object clone() {System.out.println("default clone"); return this;}
            default void finalize() {System.out.println("default finalize");}
        }
    
        static class FOO implements ClonerFinalizer {
            @Override
            public Object clone() {
                return ClonerFinalizer.super.clone();
            }
            @Override
            public void finalize() {
                ClonerFinalizer.super.finalize();
            }
        }
    }

.ประเด็นคืออะไร? คุณยังไม่ได้ตอบส่วนทำไม - "แล้วอะไรคือสาเหตุที่นักออกแบบ Java ตัดสินใจไม่อนุญาตให้ใช้เมธอดเริ่มต้นแทนที่เมธอดจาก Object"
pro_cheats
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.