Чому колекції Java не видаляють загальні методи?


144

Чому Collection.remove (Object o) не є загальним?

Здається, Collection<E>могло бboolean remove(E o);

Потім, коли ви випадково намагаєтесь видалити (наприклад) Set<String>замість кожної окремої рядки з а Collection<String>, пізніше це буде помилка часу компіляції, а не проблема налагодження.


13
Це дійсно може вкусити вас, коли ви поєднаєте його з автобоксингом. Якщо ви спробуєте щось видалити зі списку, і ви передасте його цілим числом замість int, який він викликає методом видалення (об'єкта).
ScArcher2

2
Схожий питання про карту: stackoverflow.com/questions/857420 / ...
AlikElzin-kilaka

Відповіді:


73

Джош Блох та Білл П'ю посилаються на цю проблему в Java Puzzlers IV: The Phantom Reference Menace, Attack of the Clone та Revenge of the Shift .

Джош Блох каже (6:41), що вони намагалися узагальнити метод отримання Map, видалити метод та деякі інші, але "це просто не вийшло".

Занадто багато розумних програм, які неможливо було б узагальнити, якщо дозволити лише загальний тип колекції як тип параметра. Приклад , наведений їм є перетин Listз Numberс і Listна Longс.


6
Чому add () може приймати введений параметр, але delete () не може все ще трохи перевищувати моє розуміння. Josh Bloch стане остаточним посиланням на питання колекцій. Це може бути все, що я отримую, не намагаючись зробити подібну реалізацію колекції і переконатися в цьому. :( Дякую.
Кріс Маццола

2
Кріс - прочитайте PDF Підручник з Java Generics, він пояснить, чому.
JeeBee

42
Насправді це дуже просто! Якщо add () взяв неправильний об'єкт, це порушить колекцію. Він містив би речі, яких цього не передбачається! Це не стосується видалення (), або містить ().
Кевін Бурліон

12
До речі, це основне правило - використання параметрів типу для запобігання фактичного збитку лише колекції - дотримується абсолютно послідовно у всій бібліотеці.
Кевін Бурліон

3
@KevinBourrillion: Я працював з дженериками протягом багатьох років (як на Java, так і на C #), не розуміючи, що правило "фактичного пошкодження" навіть існує ... але тепер, коли я це бачив, це прямо сказано, це має 100% ідеальний сенс. Дякую за рибу !!! Крім того, я відчуваю, що я змушений повернутися назад і подивитися на мої реалізації, щоб побачити, чи можна, і тому деякі методи можуть бути відродженими. Зітхнути.
corlettk

74

remove()( Mapяк і в Collection) не є загальним, тому що ви повинні мати можливість передавати будь-який тип об'єкта remove(). Видалений об’єкт не повинен бути того ж типу, що і об'єкт, до якого ви передаєте remove(); потрібно лише, щоб вони були рівними. З специфікації remove(), remove(o)видаляє об'єкт eтакий, який (o==null ? e==null : o.equals(e))є true. Зауважте, що нічого не потрібно oі eбути того ж типу. Це випливає з того факту, що equals()метод приймає в якості Objectпараметра не такий же тип, як об’єкт.

Хоча, можливо, правдою є те, що багато класів equals()визначають так, що його об'єкти можуть бути рівними лише об'єктам власного класу, але це, звичайно, не завжди. Наприклад, специфікація List.equals()говорить про те, що два об'єкти List рівні, якщо вони обидва Списки і мають однаковий вміст, навіть якщо вони різні реалізації List. Так що, повертаючись до прикладу в цьому питанні, можна мати Map<ArrayList, Something>і для мене , щоб зателефонувати remove()з в LinkedListякості аргументу, і він повинен видалити ключ , який представляє собою список з тим же вмістом. Це було б неможливо, якби remove()були загальними та обмежили його тип аргументу.


1
Але якби Ви визначили Map як Map <Список, Щось> (замість ArrayList), це можна було б видалити за допомогою LinkedList. Я вважаю, що ця відповідь неправильна.
AlikElzin-kilaka

3
Відповідь видається правильною, але неповною. Це лише піддається питанню, чому, чорт, не вони також не генералізували equals()метод? Я міг би бачити більше переваг для безпеки безпеки замість цього "лібертарного" підходу. Я думаю, що більшість випадків із теперішньою реалізацією стосуються помилок, які потрапляють до нашого коду, а не для радості цієї гнучкості, яку remove()приносить метод.
kellogs

2
@kellogs: Що б ви мали на увазі під "генеризацією equals()методу"?
newacct

5
@MattBall: "де T - клас декларування", але в Java немає такого синтаксису. Tповинен бути оголошений як параметр типу в класі, і Objectне має жодних параметрів типу. Немає можливості мати тип, який посилається на "клас декларування".
newacct

3
Я думаю , що Kellogs каже , що якщо рівність було загальний інтерфейс Equality<T>з equals(T other). Тоді ви могли б мати remove(Equality<T> o)і oє лише деяким об’єктом, який можна порівняти з іншим T.
weston

11

Оскільки, якщо ваш параметр типу є загальним символом, ви не можете використовувати загальний метод видалення.

Здається, я згадую, що я стикався з цим питанням методом get (Object) Map. Метод get в цьому випадку не є загальним, хоча слід обґрунтовано очікувати, що він буде переданий об'єктом того ж типу, що і параметр першого типу. Я зрозумів, що якщо ви проходите навколо Карт із підстановкою як перший параметр типу, тоді ви не зможете вилучити елемент із Мапи за допомогою цього методу, якщо цей аргумент був загальним. Аргументи wildcard насправді не можуть бути задоволені, оскільки компілятор не може гарантувати правильність типу. Я припускаю, що причина додавання є загальною в тому, що ви повинні гарантувати правильність типу перед тим, як додати його до колекції. Однак при видаленні об’єкта, якщо тип неправильний, він все одно не відповідатиме.

Я, мабуть, не дуже добре пояснив це, але мені це здається досить логічним.


1
Ви могли б трохи детальніше розібратися в цьому?
Томас Оуенс

6

Окрім інших відповідей, є ще одна причина, чому метод повинен приймати an 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методу. Побудова предикатів стає дуже простим.


Інвестор? (Філлер наповнювача наповнювача)
Matt R

3
Список реалізований так yourObject.equals(developer), як це зафіксовано в API колекцій: java.sun.com/javase/6/docs/api/java/util/…
Aly

13
Мені це здається зловживанням
RAY

7
Це зловживання, оскільки ваш предикатний об’єкт розриває контракт equalsметоду, а саме симетрію. Метод видалення пов'язаний лише з його специфікацією до тих пір, поки ваші об'єкти виконують специфікацію 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, немає жодних проблем сказати, чи з’являється він у колекції - просто відповідайте «ні, це не так». Колекція "типу пошуку" якогось типу повинна мати можливість осмислено приймати посилання на будь-який супертип і визначати, чи існує об'єкт у колекції. Якщо посилання на об'єкт, що передається, має непов'язаний тип, колекція не може його містити, тому запит у певному сенсі не має сенсу (він завжди відповідає "ні"). Тим не менш, оскільки немає жодного способу обмежити параметри підтипами або супертипами, найрезультативніше просто прийняти будь-який тип і відповісти "ні" на будь-які об'єкти, тип яких не пов'язаний з типом колекції.


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 () не має підстав піклуватися про те, який тип об'єкта ви йому надаєте. Досить просто, незалежно, перевірити, чи є цей об'єкт одним із тих, що містить колекція, оскільки він може називати рівним () будь-що. Необхідно перевірити тип на add (), щоб переконатися, що він містить лише об'єкти цього типу.


0

Це був компроміс. Обидва підходи мають свою перевагу:

  • remove(Object o)
    • є більш гнучким. Наприклад, це дозволяє повторити список номерів та видалити їх із списку довгих.
    • код, який використовує цю гнучкість, може бути легше узагальнений
  • remove(E e) приносить більшу безпеку типу тим, що більшість програм хоче робити, виявляючи непомітні помилки під час компіляції, як помилково намагаючись видалити ціле число зі списку шортів.

Зрозуміла зворотна сумісність завжди була головною метою при розробці API Java, тому було вибрано видалення (Object o), оскільки це спростило генерування існуючого коду. Якби сумісність із зворотним рівнем НЕ була проблемою, я припускаю, що дизайнери обрали б видалити (E e).


-1

Видалити не є загальним методом, так що існуючий код, що використовує колекцію, яка не є загальною, все одно збиратиметься та матиме таку саму поведінку.

Докладні відомості див. У розділі http://www.ibm.com/developerworks/java/library/j-jtp01255.html .

Редагувати: коментатор запитує, чому метод додавання є загальним. [... видалив моє пояснення ...] Другий коментатор відповів на питання від firebird84 набагато краще, ніж я.


2
Тоді чому метод додавання є загальним?
Боб Геттіс

@ firebird84 remove (Object) ігнорує об'єкти неправильного типу, але delete (E) спричинить помилку компіляції. Це змінило б поведінку.
ной

: знизати плечима: - поведінка часу виконання не змінюється; помилка компіляції - це не поведінка часу виконання . "Поведінка" методу додавання змінюється таким чином.
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буде рівним an A? У вашому прикладі це може бути, але це дивне очікування. Я б швидше побачив, як ви MyClassподаєте тут аргумент.
seh

-3

Тому що він би порушив існуючий (до 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), ні?
Кріс Маццола

5
Якщо ви заміните "видалити" на "додати", цей код настільки ж порушений тим, що було зроблено на Java 5.
DJClayworth
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.