Почему 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

Джош Блох и Билл Пью ссылаются на эту проблему в Java Puzzlers IV: Справочная угроза фантома, Атака клона и Месть сдвига .

Джош Блох говорит (6:41), что они пытались обобщить метод get из Map, метод remove и некоторые другие, но «это просто не сработало».

Существует слишком много разумных программ, которые нельзя было бы генерировать, если вы разрешаете только универсальный тип коллекции в качестве типа параметра. Приведенный им пример является пересечением a Listof Numbers и a Listof Longs.

dmeister
источник
6
Почему add () может принимать типизированный параметр, а remove () не может, все еще немного за пределами моего понимания. Джош Блох был бы окончательным справочником для вопросов Коллекций. Это может быть все, что я получу, не пытаясь создать аналогичную реализацию коллекции и убедиться в этом сам. :( Спасибо.
Крис Маццола
2
Крис - прочитайте учебник по Java Generics Tutorial, он объяснит почему.
JeeBee
42
На самом деле, это очень просто! Если add () принимает неправильный объект, это разрушит коллекцию. Он будет содержать вещи, которые он не должен! Это не относится к remove () или содержит ().
Кевин Бурриллион
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()определили так, что его объекты могут быть равны только объектам своего собственного класса, что, конечно, не всегда так. Например, спецификация for List.equals()говорит, что два объекта List равны, если они оба являются списками и имеют одинаковое содержимое, даже если они являются разными реализациями List. Итак, возвращаясь к примеру в этом вопросе, можно иметь Map<ArrayList, Something>и для меня вызов remove()с LinkedListаргументом as, и он должен удалить ключ, представляющий собой список с тем же содержимым. Это было бы невозможно, если бы они remove()были общими и ограничивали тип аргумента.

newacct
источник
1
Но если бы вы определили карту как Map <List, Something> (вместо ArrayList), ее можно было бы удалить с помощью LinkedList. Я думаю, что этот ответ неверен.
Алик Эльзин-килака
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.
Уэстон
11

Потому что, если ваш параметр типа является подстановочным знаком, вы не можете использовать универсальный метод удаления.

Кажется, я вспоминаю, что столкнулся с этим вопросом с помощью метода get (Object) Map. Метод get в этом случае не является универсальным, хотя следует ожидать, что ему будет передан объект того же типа, что и параметр первого типа. Я понял, что если вы передаете вокруг Карт с подстановочным знаком в качестве параметра первого типа, то нет способа извлечь элемент из Карты с помощью этого метода, если этот аргумент был универсальным. Аргументы подстановочных знаков не могут быть действительно удовлетворены, потому что компилятор не может гарантировать, что тип является правильным. Я предполагаю, что причина добавления является общей, что вы должны гарантировать, что тип правильный, прежде чем добавлять его в коллекцию. Однако, при удалении объекта, если тип неправильный, он все равно ничего не будет соответствовать.

Я, наверное, не очень хорошо это объяснил, но мне это кажется достаточно логичным.

Боб Геттис
источник
1
Не могли бы вы подробнее остановиться на этом?
Томас Оуэнс
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метода. Построение предикатов становится очень простым.

Хосам Али
источник
Инвестор? (Наполнитель наполнитель наполнитель)
Мэтт R
3
Список реализован так yourObject.equals(developer), как описано в
Хосам Али
13
Это кажется мне оскорблением
RAY
7
Это злоупотребление, так как ваш объект предиката нарушает контракт equalsметода, а именно симметрию. Метод remove связан только со своей спецификацией, пока ваши объекты удовлетворяют спецификации equals / hashCode, поэтому любая реализация может выполнить сравнение наоборот. Кроме того, ваш объект-предикат не реализует .hashCode()метод (не может реализовать последовательно равные), поэтому вызов remove никогда не будет работать с коллекцией на основе Hash (например, HashSet или HashMap.keys ()). То, что он работает с ArrayList - чистая удача.
Паŭло Эберманн
3
(Я не обсуждаю вопрос универсального типа - об этом ранее уже говорилось - только здесь вы используете equals для предикатов.) Конечно, HashMap и HashSet проверяют хеш-код, а TreeSet / Map использует порядок элементов , Тем не менее, они полностью реализуют Collection.remove, не нарушая свой контракт (если порядок соответствует равным). И разнообразный ArrayList (или AbstractCollection, я думаю) с перевернутым вызовом equals все равно будет правильно реализовывать контракт - это ваша вина, если он не будет работать так, как задумано, поскольку вы нарушаете equalsконтракт.
Paŭlo Ebermann
5

Предположим , один имеет коллекцию Cat, а некоторые ссылки на объекты типов Animal, Cat, SiameseCatи Dog. Спрашивать коллекцию, содержит ли она объект, на который указывает ссылка Catили, SiameseCatкажется разумным. Спросить, содержит ли он объект, на который Animalссылается ссылка, может показаться странным, но это все же вполне разумно. В конце концов, рассматриваемый объект может быть Catи может появиться в коллекции.

Кроме того, даже если объект оказывается чем-то отличным от a Cat, нет проблем с тем, чтобы сказать, появляется ли он в коллекции - просто ответьте «нет, это не так». Коллекция «lookup-style» некоторого типа должна быть способна осмысленно принимать ссылку любого супертипа и определять, существует ли объект в коллекции. Если переданная ссылка на объект имеет несвязанный тип, коллекция не сможет его содержать, поэтому запрос в некотором смысле не имеет смысла (он всегда будет отвечать «нет»). Тем не менее, поскольку нет никакого способа ограничить параметры подтипами или супертипами, наиболее практично просто принять любой тип и ответить «нет» для любых объектов, тип которых не связан с типом коллекции.

Supercat
источник
1
«Если переданная ссылка на объект имеет несвязанный тип, коллекция не сможет его содержать». Неправильно. Он должен содержать только что-то равное ему; и объекты разных классов могут быть равны.
newacct
"может показаться хитрым, но все же вполне разумно" Рассмотрим мир, в котором объект одного типа не всегда можно проверить на равенство с объектом другого типа, потому что параметризован тип, которому он может быть равен (аналогично Comparableпараметризации для типов, с которыми можно сравнивать). Тогда было бы неразумно позволять людям передавать что-то не связанное с этим типом.
newacct
@newacct: Существует принципиальная разница между величиной сравнения и сопоставления равенства: данные объекты Aи Bодного типа, а также Xи Yдругого, так что A> Bи X> Y. Либо A> Yи Y< A, либо X> Bи B< X. Эти отношения могут существовать только в том случае, если для сравнения величин известны оба типа. В отличие от этого, метод сравнения равенства объектов может просто объявить себя неравным по отношению к чему-либо из любого другого типа, без необходимости что-либо знать о другом рассматриваемом типе. Объект типа Catможет не иметь представления, является ли он ...
суперкат
... «больше» или «меньше» объекта типа FordMustang, но ему не составит труда сказать, равен ли он такому объекту (ответ, очевидно, будет «нет»).
суперкат
4

Я всегда думал, что это потому, что у remove () нет причин заботиться о том, какой тип объекта вы ему предоставите. Независимо от этого достаточно легко проверить, является ли этот объект одним из объектов, содержащихся в Collection, поскольку он может вызывать equals () для чего угодно. Необходимо проверить тип в add (), чтобы убедиться, что он содержит только объекты этого типа.

ColinD
источник
0

Это был компромисс. Оба подхода имеют свои преимущества:

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

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

Стефан Фейерхан
источник
-1

Remove не является универсальным методом, поэтому существующий код, использующий неуниверсальную коллекцию, будет по-прежнему компилироваться и иметь то же поведение.

См. Http://www.ibm.com/developerworks/java/library/j-jtp01255.html для получения подробной информации.

Редактировать: комментатор спрашивает, почему метод add является универсальным. [... удалил мое объяснение ...] Второй комментатор ответил на вопрос от firebird84 намного лучше меня.

Джефф С
источник
2
Тогда почему метод add является общим?
Боб Геттис
@ firebird84 remove (Object) игнорирует объекты неправильного типа, но remove (E) может вызвать ошибку компиляции. Это изменило бы поведение.
Ноя
: shrug: - поведение во время выполнения не изменилось; Ошибка компиляции не является поведением во время выполнения . Таким образом изменяется «поведение» метода add.
Джейсон С
-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? В вашем примере это может быть, но это странное ожидание. Я бы предпочел, чтобы вы MyClassпривели здесь аргумент.
SEH
-3

Потому что это сломало бы существующий (до Java5) код. например,

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

Теперь вы можете сказать, что приведенный выше код неверен, но предположим, что o произошел от разнородного набора объектов (т. Е. Он содержал строки, числа, объекты и т. Д.). Вы хотите удалить все совпадения, что было допустимо, потому что при удалении игнорировались бы не строки, потому что они были неравны. Но если вы удалите его (String o), это больше не работает.

Ной
источник
4
Если бы я создал экземпляр List <String>, я бы ожидал, что смогу вызвать только List.remove (someString); Если мне нужно поддерживать обратную совместимость, я бы использовал необработанный List - List <?>, Тогда я могу вызвать list.remove (someObject), нет?
Крис Маззола
5
Если вы замените «удалить» на «добавить», то этот код будет нарушен тем, что фактически было сделано в Java 5.
DJClayworth,