абстрактные методы в скелетных реализациях интерфейсов

Я перечитывал статью 18 «Эффективная Java» (2-е издание), предпочитаю интерфейсы абстрактным классам. В этом пункте Джош Блох приводит пример скелетной реализации интерфейса Map.Entry<K,V>:

// Skeletal Implementation
public abstract class AbstractMapEntry<K,V>
        implements Map.Entry<K,V> {
    // Primitive operations
    public abstract K getKey();
    public abstract V getValue();

 // ... remainder omitted
}

Из этого примера вытекают два вопроса:

  1. Почему getKey и getValue явно объявлены здесь как абстрактные методы? Они являются частью Map.Entry. интерфейс, поэтому я не вижу причин для избыточного объявления в абстрактном классе.
  2. Зачем использовать идиому оставлять эти примитивные методы, как их называет г-н Блох, абстрактными? Почему бы просто не сделать это:

    // Скелетная реализация публичный абстрактный класс AbstractMapEntry реализует Map.Entry { private K key; частное значение V;

        // Primitive operations
        public K getKey() {return key;}
        public V getValue() {return value;}
    
     // ... remainder omitted
    

    }

Преимущество этого заключается в том, что каждому подклассу не нужно определять свой собственный набор полей, и он по-прежнему может получать доступ к ключу и значению с помощью своих средств доступа. Если подклассу действительно необходимо определить собственное поведение для методов доступа, он может напрямую реализовать интерфейс Map.Entry. Другим недостатком является то, что в методе equals, предоставляемом скелетной реализацией, вызываются абстрактные методы доступа:

// Implements the general contract of Map.Entry.equals
@Override public boolean equals(Object o) {
    if (o == this)
        return true;
    if (! (o instanceof Map.Entry))
        return false;
    Map.Entry<?,?> arg = (Map.Entry) o;
    return equals(getKey(),   arg.getKey()) &&
           equals(getValue(), arg.getValue());
}

Блох предостерегает от вызова переопределяемых методов (пункт 17) из классов, предназначенных для наследования, поскольку это делает суперкласс уязвимым для изменений, сделанных подклассами. Может быть, это вопрос мнения, но я надеялся определить, есть ли в этой истории что-то еще, поскольку Блох на самом деле не останавливается на этом в книге.


person Julie    schedule 25.10.2008    source источник


Ответы (4)


  1. Я бы сказал, что это помогает подчеркнуть, для чего предназначен конкретный класс, вместо того, чтобы просто оставлять это на усмотрение компилятора (или вам нужно сравнить оба, чтобы увидеть, чего не хватает). Своего рода самодокументирующийся код. Но это, конечно, не обязательно, насколько я понимаю, это скорее стильная вещь.
  2. В возврате этих значений есть более важная логика, чем простой геттер и установка. Каждый класс, который я проверил в стандартном JDK (1.5), сделал что-то непростое по крайней мере в одном из методов, поэтому я предполагаю, что он считает такую ​​​​реализацию слишком наивной, и это будет поощрять подклассы использовать ее вместо того, чтобы продумывать проблема самостоятельно.

Что касается проблемы с равными, ничего не изменилось бы, если бы абстрактный класс реализовал их, потому что проблема можна переопределить. В этом случае я бы сказал, что equals пытается быть тщательно реализованным, чтобы предвидеть реализации. Обычно equals в целом не следует реализовывать для возврата true между собой и его подклассом (хотя есть много таких случаев) из-за проблем с ковариацией (суперкласс будет думать, что он равен подклассу, но подкласс не будет думать, что он равен суперклассу) , поэтому этот тип реализации equals сложен, независимо от того, что вы делаете.

person Yishai    schedule 04.06.2009

Блох предостерегает от вызова переопределяемых методов (пункт 17) из классов, предназначенных для наследования, поскольку это делает суперкласс уязвимым для изменений, сделанных подклассами.

Он предупреждает о вызове переопределяемых методов в конструкторе, а не в других методах.

person John    schedule 13.12.2010

Одна из причин, по которой AbstractMapEntry#getKey и getValue являются абстрактными (то есть нереализованными), заключается в том, что Map.Entry является внутренним интерфейсом для Map. Использование вложенных классов/интерфейсов — это то, как Java реализует композицию. Идея композиции состоит в том, что составная часть не является первоклассным понятием. Скорее, составная часть имеет смысл только в том случае, если она содержится в целом. В этом случае составной частью является Map.Entry, а корневым объектом составного объекта является Map. Очевидно, выраженная концепция состоит в том, что Map имеет много Map.Entry.

Следовательно, семантика AbstractMapEntry#getKey и getValue будет существенно зависеть от реализации Map, о которой мы говорим. Обычная старая реализация геттера, как вы написали, будет отлично работать для HashMap. Это не сработает для чего-то вроде ConcurrentHashMap, требующего потокобезопасности. Вполне вероятно, что реализация getKey и getValue в ConcurrentHashMap создает защитные копии. (Рекомендую проверить исходный код самостоятельно).

Еще одна причина не реализовывать getKey и getValue заключается в том, что персонажи, реализующие Map, радикально отличаются от тех, которые никогда не должны были принадлежать (например, Properties), до совершенно разных вселенных от интуитивного понимания Map (например, Provider, TabularDataSupport).

В заключение, не внедрять AbstractMapEntry#getKey и getValue из-за этого золотого правила проектирования API:

Если вы сомневаетесь, не указывайте его (см. здесь)

person Alan    schedule 25.10.2008
comment
Не совсем уверен, как вы видите, я проигнорировал вопросы, которые она отмечает в № 1 и № 2. Очевидное намерение ее вопросов, как я понял, имеет логику если не 1, то 2. Мой ответ защищает сохранение абстракции getKey и getValue. - person Alan; 26.10.2008
comment
@Alan - вы немного пропустили # 1 - если абстрактный класс реализует интерфейс, зачем повторно объявлять методы интерфейса в абстрактном классе, как в случае с getKey и getValue? - person Julie; 26.10.2008
comment
@Alan - похоже на суждение - вы могли бы реализовать средства доступа для удобства расширения некоторых классов (например, HashMap), но за счет других (например, ConcurrentHashMap), которым пришлось бы реализовывать интерфейс с нуля. Я ищу ошибки, которые могут привести к ошибкам. - person Julie; 26.10.2008

  1. я не вижу причин

  2. Позволяет реализации определить, как хранятся ключ и значение.

person objects    schedule 03.06.2009