ทำไม Java Collections จึงไม่ลบเมธอดทั่วไป?


144

ทำไมไม่ Collection.remove (Object o)ทั่วไป?

ดูเหมือนว่า Collection<E>จะมีboolean remove(E o);

จากนั้นเมื่อคุณพยายามลบ (ตัวอย่าง) โดยไม่ได้ตั้งใจSet<String>แทนที่จะใช้สตริงแต่ละอันจาก a Collection<String>มันจะเป็นข้อผิดพลาดเวลาในการคอมไพล์แทนที่จะเป็นปัญหาการดีบักในภายหลัง


13
สิ่งนี้สามารถกัดคุณได้เมื่อคุณรวมเข้ากับการล็อคอัตโนมัติ ถ้าคุณพยายามที่จะลบบางสิ่งออกจากรายการและคุณผ่านมันเป็นจำนวนเต็มแทนที่จะเป็น int มันเรียกเมธอด remove (Object)
ScArcher2

2
คำถามที่คล้ายกันเกี่ยวกับแผนที่: stackoverflow.com/questions/857420/…
AlikElzin-kilaka

คำตอบ:


73

Josh Bloch และ Bill Pugh อ้างถึงปัญหานี้ในJava Puzzlers IV: The Phantom Reference Menace, Attack of the Clone และ Revenge of The Shiftผีอ้างอิงคุกคามการโจมตีของโคลนและการแก้แค้นของกดปุ่ม Shift

Josh Bloch กล่าวว่า (6:41) ว่าพวกเขาพยายามสร้างวิธีการรับแผนที่ลบวิธีการและอื่น ๆ แต่ "มันใช้งานไม่ได้"

มีโปรแกรมที่สมเหตุสมผลมากเกินไปที่ไม่สามารถสร้างได้หากคุณอนุญาตประเภททั่วไปของการรวบรวมเป็นประเภทพารามิเตอร์ ตัวอย่างที่ได้รับจากเขาเป็นจุดตัดของListของNumbers และ ListของLongs


6
ทำไมเพิ่ม () สามารถรับพารามิเตอร์ที่พิมพ์ได้ แต่การลบ () ไม่สามารถเกินความเข้าใจของฉันได้เล็กน้อย Josh Bloch จะเป็นข้อมูลอ้างอิงที่ชัดเจนสำหรับคำถาม Collections มันอาจเป็นสิ่งที่ฉันได้รับโดยไม่ต้องพยายามใช้คอลเลกชันที่คล้ายกันและดูด้วยตัวเอง :( ขอบคุณ.
Chris Mazzola

2
Chris - อ่าน Java Generics Tutorial PDF มันจะอธิบายว่าทำไม
JeeBee

42
จริงๆแล้วมันง่ายมาก! หาก add () นำวัตถุที่ไม่ถูกต้องมันจะทำลายคอลเลกชัน มันจะมีสิ่งที่ไม่ควรทำ! ไม่ได้เป็นกรณีสำหรับการลบ () หรือมี ()
Kevin Bourrillion

12
อนึ่งกฎพื้นฐาน - การใช้พารามิเตอร์ประเภทเพื่อป้องกันความเสียหายที่เกิดขึ้นกับคอลเลกชันเท่านั้น - มีการติดตามอย่างสม่ำเสมอในไลบรารีทั้งหมด
Kevin Bourrillion

3
@KevinBourrillion: ฉันทำงานกับ generics มาหลายปีแล้ว (ทั้ง Java และ C #) โดยที่ไม่เคยรู้เลยว่ากฎ "ความเสียหายที่แท้จริง" ยังคงมีอยู่ ... แต่ตอนนี้ฉันได้เห็นมันแล้ว ขอบคุณสำหรับปลา !!! ยกเว้นตอนนี้ฉันรู้สึกว่าถูกบังคับให้กลับไปดูการใช้งานของฉันเพื่อดูว่าวิธีการบางอย่างสามารถทำได้หรือไม่ดังนั้นจึงควรลดความน่าเชื่อถือ ถอนหายใจ
corlettk

74

remove()(ในMapเช่นเดียวกับในCollection) ไม่ได้ทั่วไปเพราะคุณควรจะสามารถที่จะผ่านในรูปแบบใด ๆ remove()ของวัตถุ วัตถุที่เอาออกไม่จำเป็นต้องเป็นชนิดเดียวกับวัตถุที่คุณผ่านไปremove(); มันต้องการเพียงแค่ว่าพวกเขาจะเท่ากัน จากคุณสมบัติของremove(), remove(o)เอาวัตถุeดังกล่าวว่าเป็น(o==null ? e==null : o.equals(e)) trueโปรดทราบว่าไม่มีอะไรที่ต้องใช้oและeเป็นประเภทเดียวกัน นี้ตามมาจากความจริงที่ว่าequals()วิธีการที่ใช้ในการObjectพารามิเตอร์ as ไม่ใช่แค่ชนิดเดียวกับวัตถุ

แม้ว่ามันอาจจะเป็นความจริงโดยทั่วไปที่คลาสจำนวนมากได้equals()กำหนดไว้เพื่อให้อ็อบเจ็กต์สามารถเท่ากับอ็อบเจ็กต์ของคลาสของตัวเองเท่านั้นซึ่งแน่นอนว่าไม่เสมอไป ยกตัวอย่างเช่นข้อกำหนดสำหรับบอกว่าวัตถุทั้งสองรายการมีค่าเท่ากันถ้าหากพวกเขาทั้งสองรายการและมีเนื้อหาเดียวกันแม้ว่าพวกเขาจะใช้งานที่แตกต่างกันของList.equals() Listดังนั้นกลับมาที่ตัวอย่างในคำถามนี้มันเป็นไปได้ที่จะมีMap<ArrayList, Something>และสำหรับฉันที่จะโทรremove()ด้วยLinkedListเป็นอาร์กิวเมนต์และควรลบที่สำคัญซึ่งเป็นรายการที่มีเนื้อหาเดียวกัน สิ่งนี้จะเป็นไปไม่ได้หากremove()เป็นประเภททั่วไปและ จำกัด ประเภทอาร์กิวเมนต์


1
แต่ถ้าคุณต้องกำหนด Map เป็น Map <List, Something> (แทนที่จะเป็น ArrayList) มันจะเป็นไปได้ที่จะลบโดยใช้ LinkedList ฉันคิดว่าคำตอบนี้ไม่ถูกต้อง
AlikElzin-kilaka

3
คำตอบดูเหมือนจะถูกต้อง แต่ไม่สมบูรณ์ เพียงยืมตัวเองเพื่อถามว่าทำไมห่าก็ทำไม่ได้ทำให้equals()วิธีการทั่วไปเช่นกัน? ฉันเห็นประโยชน์เพิ่มเติมในการพิมพ์ความปลอดภัยแทนวิธี "เสรีนิยม" นี้ ฉันคิดว่ากรณีส่วนใหญ่ที่มีการใช้งานในปัจจุบันสำหรับข้อผิดพลาดในรหัสของเรามากกว่าเกี่ยวกับความสนุกสนานนี้ความยืดหยุ่นที่remove()วิธีนำ
kellogs

2
@kellogs: คุณจะหมายความว่า " equals()วิธีทั่วไป" หรือไม่
newacct

5
@MattBall: "โดยที่ T คือคลาสที่ประกาศ" แต่ไม่มีไวยากรณ์ดังกล่าวใน Java Tต้องประกาศเป็นพารามิเตอร์ type ในคลาสและObjectไม่มีพารามิเตอร์ประเภทใด ๆ ไม่มีวิธีใดที่จะมีประเภทที่อ้างถึง "คลาสที่ประกาศ"
newacct

3
ผมคิดว่า Kellogs ไม่ว่าจะเป็นสิ่งที่ถ้าความเสมอภาคเป็นอินเตอร์เฟซทั่วไปด้วยEquality<T> equals(T other)จากนั้นคุณสามารถมีremove(Equality<T> o)และoเป็นเพียงวัตถุบางอย่างที่สามารถเปรียบเทียบกับวัตถุอื่นTได้
weston

11

เนื่องจากหากพารามิเตอร์ประเภทของคุณเป็นไวด์การ์ดคุณจะไม่สามารถใช้วิธีลบทั่วไปได้

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

ฉันอาจไม่ได้อธิบายได้ดี แต่ดูเหมือนว่าสมเหตุสมผลสำหรับฉัน


1
คุณช่วยอธิบายเรื่องนี้หน่อยได้ไหม?
Thomas Owens

6

นอกเหนือจากคำตอบอื่น ๆ แล้วยังมีอีกเหตุผลที่วิธีการที่ควรยอมรับObjectซึ่งเป็นเพรดิเคต พิจารณาตัวอย่างต่อไปนี้:

class Person {
    public String name;
    // override equals()
}
class Employee extends Person {
    public String company;
    // override equals()
}
class Developer extends Employee {
    public int yearsOfExperience;
    // override equals()
}

class Test {
    public static void main(String[] args) {
        Collection<? extends Person> people = new ArrayList<Employee>();
        // ...

        // to remove the first employee with a specific name:
        people.remove(new Person(someName1));

        // to remove the first developer that matches some criteria:
        people.remove(new Developer(someName2, someCompany, 10));

        // to remove the first employee who is either
        // a developer or an employee of someCompany:
        people.remove(new Object() {
            public boolean equals(Object employee) {
                return employee instanceof Developer
                    || ((Employee) employee).company.equals(someCompany);
        }});
    }
}

ประเด็นก็คือวัตถุที่ถูกส่งผ่านไปยังremoveวิธีการมีหน้าที่รับผิดชอบในการกำหนดequalsวิธีการ การสร้างภาคแสดงเป็นวิธีที่ง่ายมาก


นักลงทุน? (ฟิลเลอร์ฟิลเลอร์ฟิลเลอร์)
แมตต์อาร์

3
รายการดังกล่าวได้รับการนำมาใช้ตามyourObject.equals(developer)ที่ระบุไว้ใน Collections API: java.sun.com/javase/6/docs/api/java/util/…
Hosam Aly

13
ดูเหมือนจะเป็นการละเมิดต่อฉัน
RAY

7
มันเป็นการละเมิดเนื่องจากวัตถุของคุณแบ่งสัญญาของequalsวิธีการคือสมมาตร วิธีการลบถูกผูกไว้กับสเปคของมันตราบเท่าที่วัตถุของคุณทำตามข้อมูลจำเพาะเท่ากับ / hashCode ดังนั้นการใช้งานใด ๆ ก็สามารถทำการเปรียบเทียบได้อย่างอิสระ นอกจากนี้วัตถุเพรดิเคตของคุณไม่ได้ใช้.hashCode()วิธีการ (ไม่สามารถใช้งานได้อย่างสม่ำเสมอเพื่อให้เท่ากับ) ดังนั้นการลบการโทรจะไม่ทำงานบนคอลเลกชันตาม Hash (เช่น HashSet หรือ HashMap.keys ()) การทำงานกับ ArrayList นั้นเป็นโชคดี
Paŭlo Ebermann

3
(ฉันไม่ได้พูดถึงคำถามประเภททั่วไป - นี่เป็นคำตอบก่อนหน้า - เฉพาะการใช้งานของคุณเท่านั้นสำหรับเพรดิเคตที่นี่) แน่นอน HashMap และ HashSet กำลังตรวจสอบรหัสแฮชและ TreeSet / Map ใช้การเรียงลำดับขององค์ประกอบ . แต่ถึงกระนั้นพวกเขาดำเนินการอย่างเต็มที่Collection.removeโดยไม่ทำลายสัญญา (ถ้าการสั่งซื้อสอดคล้องกับเท่ากับ) และ ArrayList ที่หลากหลาย (หรือ AbstractCollection ฉันคิดว่า) ด้วยการโทรที่เท่าเทียมกันจะยังคงใช้สัญญาได้อย่างถูกต้อง - มันเป็นความผิดของคุณถ้ามันไม่ทำงานตามที่ตั้งใจเพราะคุณทำผิดequalsสัญญา
Paŭlo Ebermann

5

สมมติหนึ่งที่มีคอลเลกชันของCatและบางส่วนอ้างอิงวัตถุประเภทAnimal, Cat, และSiameseCat Dogการถามคอลเลกชันว่ามีวัตถุที่อ้างถึงโดยCatหรือSiameseCatการอ้างอิงดูเหมือนว่าเหมาะสม การถามว่ามันมีวัตถุที่อ้างถึงโดยAnimalอ้างอิงอาจดูเหมือนหลบ แต่มันก็ยังสมเหตุสมผล วัตถุที่เป็นปัญหาอาจจะเป็นCatและอาจปรากฏในคอลเล็กชัน

นอกจากนี้แม้ว่าวัตถุจะเป็นอย่างอื่นนอกเหนือจาก a Catก็ไม่มีปัญหาที่จะบอกได้ว่ามันปรากฏอยู่ในคอลเลกชันหรือไม่ - เพียงแค่ตอบว่า "ไม่เลย" คอลเลกชัน "ลักษณะการค้นหา" ของบางประเภทควรจะสามารถยอมรับการอ้างอิงของ supertype ใด ๆ ได้อย่างมีความหมายและตัดสินว่าวัตถุนั้นมีอยู่ภายในคอลเลกชันหรือไม่ หากการอ้างอิงวัตถุที่ผ่านเป็นประเภทที่ไม่เกี่ยวข้องไม่มีวิธีที่จะเก็บอาจมีดังนั้นแบบสอบถามในความรู้สึกที่ไม่มีความหมายบางอย่าง (มันมักจะตอบว่า "ไม่") อย่างไรก็ตามเนื่องจากไม่มีวิธีใดที่จะ จำกัด พารามิเตอร์ให้เป็นประเภทย่อยหรือ supertypes จึงเป็นไปได้มากที่สุดที่จะยอมรับประเภทและคำตอบ "ไม่" สำหรับวัตถุใด ๆ ที่ประเภทนั้นไม่เกี่ยวข้องกับการรวบรวม


1
"ถ้าการอ้างอิงวัตถุที่ส่งผ่านเป็นประเภทที่ไม่เกี่ยวข้องไม่มีวิธีใดที่คอลเลกชันอาจมีได้" ผิด มันจะต้องมีบางสิ่งที่เท่ากัน และวัตถุของคลาสที่แตกต่างกันสามารถเท่ากัน
newacct

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

@newacct: มีความแตกต่างพื้นฐานระหว่างการเปรียบเทียบขนาดและการเปรียบเทียบความเท่าเทียมกันคือวัตถุที่กำหนดAและBประเภทหนึ่งและXและYของผู้อื่นเช่นว่าA> Bและ>X Yอย่างใดอย่างหนึ่งA> YและY< AหรือX> Bและ<B Xความสัมพันธ์เหล่านั้นสามารถเกิดขึ้นได้ก็ต่อเมื่อการเปรียบเทียบขนาดทราบเกี่ยวกับทั้งสองประเภท ในทางตรงกันข้ามวิธีการเปรียบเทียบความเท่าเทียมกันของวัตถุสามารถประกาศตัวเองไม่เท่ากันกับสิ่งอื่นใดโดยไม่ต้องรู้อะไรเลยเกี่ยวกับประเภทอื่น ๆ ที่เป็นปัญหา ประเภทของวัตถุCatอาจมีความคิดไม่ว่าจะเป็นไม่ ...
SuperCat

... "มากกว่า" หรือ "น้อยกว่า" วัตถุประเภทหนึ่งFordMustangแต่มันไม่ควรยากที่จะบอกว่ามันเท่ากับวัตถุดังกล่าว (คำตอบที่เห็นได้ชัดว่าเป็น "ไม่")
supercat

4

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


0

มันเป็นการประนีประนอม ทั้งสองวิธีมีข้อได้เปรียบ:

  • remove(Object o)
    • มีความยืดหยุ่นมากขึ้น ตัวอย่างเช่นมันอนุญาตให้วนซ้ำผ่านรายการตัวเลขและลบออกจากรายการ longs
    • รหัสที่ใช้ความยืดหยุ่นนี้สามารถสร้างได้ง่ายขึ้น
  • remove(E e) ทำให้มีความปลอดภัยมากขึ้นในสิ่งที่โปรแกรมส่วนใหญ่ต้องการทำโดยตรวจหาข้อบกพร่องที่ละเอียด ณ เวลารวบรวมเช่นพยายามลบจำนวนเต็มออกจากรายการกางเกงขาสั้นโดยไม่ตั้งใจ

ความเข้ากันได้ย้อนหลังเป็นเป้าหมายสำคัญเสมอเมื่อพัฒนา Java API ดังนั้นจึงเลือกลบ (Object o) เนื่องจากทำให้การสร้างโค้ดที่มีอยู่ง่ายขึ้น หากความเข้ากันได้ย้อนหลังไม่เป็นปัญหาฉันเดาว่านักออกแบบจะเลือกลบ (E e)


-1

การลบไม่ใช่วิธีทั่วไปเพื่อให้รหัสที่มีอยู่ซึ่งใช้คอลเลกชันที่ไม่ใช่ทั่วไปจะยังคงรวบรวมและยังคงมีพฤติกรรมเดียวกัน

ดูhttp://www.ibm.com/developerworks/java/library/j-jtp01255.htmlเพื่อดูรายละเอียด

แก้ไข: ผู้วิจารณ์ถามว่าทำไมวิธีการเพิ่มจึงเป็นเรื่องทั่วไป [... ลบคำอธิบายของฉัน ... ] ผู้วิจารณ์คนที่สองตอบคำถามจาก firebird84 ดีกว่าฉันมาก


2
แล้วทำไมวิธีการเพิ่มแบบทั่วไปจึงเป็นเช่นนั้น?
Bob Gettys

@ firebird84 remove (Object) เพิกเฉยวัตถุประเภทที่ไม่ถูกต้อง แต่การลบ (E) จะทำให้เกิดข้อผิดพลาดในการคอมไพล์ ที่จะเปลี่ยนพฤติกรรม
โนอาห์

: shrug: - พฤติกรรมของรันไทม์จะไม่เปลี่ยนแปลง ข้อผิดพลาดในการคอมไพล์ไม่ใช่พฤติกรรมรันไทม์ การเปลี่ยนแปลง "พฤติกรรม" ของวิธีการเพิ่มด้วยวิธีนี้
304 Jason S

-2

อีกสาเหตุหนึ่งก็เพราะอินเตอร์เฟซ นี่คือตัวอย่างที่จะแสดง:

public interface A {}

public interface B {}

public class MyClass implements A, B {}

public static void main(String[] args) {
   Collection<A> collection = new ArrayList<>();
   MyClass item = new MyClass();
   collection.add(item);  // works fine
   B b = item; // valid
   collection.remove(b); /* It works because the remove method accepts an Object. If it was generic, this would not work */
}

คุณกำลังแสดงบางสิ่งบางอย่างที่คุณสามารถหลีกหนีได้เพราะremove()ไม่แปรปรวน แม้ว่าคำถามคือว่าควรได้รับอนุญาต ArrayList#remove()ทำงานโดยคำนึงถึงคุณค่าความเสมอภาคไม่ใช่การอ้างอิงความเท่าเทียมกัน ทำไมคุณถึงคาดหวังว่า a Bจะเท่ากับ a A? ในตัวอย่างของคุณอาจเป็นได้ แต่เป็นความคาดหวังที่แปลก ฉันอยากเห็นคุณให้การMyClassโต้แย้งที่นี่
seh

-3

เพราะมันจะทำลายรหัส (pre-Java5) ที่มีอยู่ เช่น,

Set stringSet = new HashSet();
// do some stuff...
Object o = "foobar";
stringSet.remove(o);

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


4
หากฉันยกตัวอย่างรายการ <String> ฉันคาดว่าจะสามารถเรียกใช้ List.remove (someString) เท่านั้น หากฉันต้องการรองรับความเข้ากันได้แบบย้อนหลังฉันจะใช้รายชื่อดิบ - รายชื่อ <?> จากนั้นฉันสามารถเรียก list.remove (someObject) ได้ไหม?
Chris Mazzola

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