Как сделать специализацию Карты неизменяемой?

В настоящее время я обновляю свои знания Java с помощью примера кода среднего размера. У меня есть структура данных Map<String, String>, и я обычно инициализирую ее с помощью new LinkedHashMap<>(), чтобы сохранить порядок вставки. Я очень часто использую это в своем коде и хочу избавиться от повторения объявлений. В C++ я бы назвал карту псевдонимом, но в Java, насколько мне известно, псевдонима нет.

Поэтому мне пришла в голову идея создать подкласс общего типа следующим образом:

public class Attributes extends LinkedHashMap<String, String> {

    public Attributes() {
        super();
    }

    public Attributes(Map<? extends String, ? extends String> map) {
        super(map);
    }

}

Пока это выглядит хорошо, но теперь я хочу создать неизменяемую копию этого, потому что атрибуты должны быть частью неизменяемых/неизменяемых структур данных. До того, как я использовал это:

Map<String, String> unmodifiableAttributes = Collections.unmodifiableMap(
        new LinkedHashMap<>(attributes)
);

Это не работает для производного класса, я пробовал это:

Attributes unmodifiableAttributes = Collections.unmodifiableMap(
        new Attributes(attributes)
);

Компилятор отклоняет его с помощью Incompatible types.

Есть ли простой способ получить неизменяемую (или неизменяемую) копию такого подкласса? Или моя мысль совершенно неверна? Я не хочу писать полнофункциональный декоратор, всего несколько строк кода.

ОБНОВЛЕНИЕ

Пока что похоже, что нет хорошего решения для того, что я хочу сделать. Я просмотрел исходный код класса Java Collections, и там есть внутренние классы для немодифицируемых карт и подобных коллекций. Они используются для переноса входной коллекции и возвращаются соответствующими статическими методами. Можно было бы переопределить это, но я думаю, что это было бы слишком много накладных расходов.

У нас было больше обсуждения нарушения LSP вместо исходного вопроса, что тоже действительно интересно.


person Andi    schedule 31.08.2019    source источник
comment
До того, как я использовал это, в чем была проблема с этим?   -  person Andy Turner    schedule 31.08.2019
comment
Map‹String, String› ничего не говорит о значении данных. Это может быть карта для (фамилия, имя), (автор, название) или подобное. В первую очередь хотелось бы, чтобы код лучше самодокументировался. И, во-вторых, меньше символов для ввода.   -  person Andi    schedule 31.08.2019


Ответы (2)


Вы не можете сделать подкласс LinkedHashMap немодифицируемым, потому что это нарушит взаимозаменяемость Лискова: LinkedHashMap задокументировано как изменяемый, поэтому все подклассы также должны быть.

У вас есть дополнительная проблема, заключающаяся в том, что на самом деле довольно много работы, чтобы сделать карту немодифицируемой: у вас есть не только очевидные методы, такие как put и remove, у вас также есть такие вещи, как clear, putAll, putIfAbsent, computeIfAbsent, computeIfPresent. И тогда вам придется беспокоиться о методах, возвращающих представления: entrySet, keySet, values все должны возвращать неизменяемые представления. Я уверен, что пропустил несколько методов, которые также нуждаются в переопределении, но моя точка зрения по-прежнему заключается в том, что сделать изменяемую карту немодифицируемой нетривиально.

Однако у вас могут быть немодифицируемые реализации Map. Самый простой способ сделать это — расширить AbstractMap и делегируйте фактическому LinkedHashMap:

public class Attributes extends AbstractMap<String, String> {
    private final LinkedHashMap<String, String> delegate;

    public Attributes() {
        this(Collections.emptyMap());
    }

    public Attributes(Map<? extends String, ? extends String> map) {
        this.delegate = new LinkedHashMap<>(map);
    }

    // Override the methods you need to, and that are required by AbstractMap.
    // Details of methods to override in AbstractMap are given in Javadoc.
}

Но я также хотел бы задаться вопросом, действительно ли ваш класс Attributes должен реализовывать что-то столь же общее, как интерфейс Map - если вам нужна эта универсальность, вы можете просто использовать Map напрямую.

person Andy Turner    schedule 31.08.2019
comment
Нарушение LSP — действительно интересный момент. Но я не уверен, что это настоящая причина? В случае использования Map‹String, String› этот экземпляр также имеет метод put() и другие методы модификации. Наивный пользователь может попытаться изменить, но получит исключение. Для меня это выглядит также как LSP. - person Andi; 31.08.2019
comment
@Andi, это не нарушение LSP: метод задокументирован как необязательный: он помещает значение на карту или генерирует исключение. Таким образом, генерация исключения является вполне приемлемой вещью для подкласса. Плохо то, что интерфейс карты не дает вам никаких средств узнать, не вызывая метод. В основном это означает, что вы никогда не должны вызывать методы мутации на карте, которые вы точно не знаете, для поддержки этих методов. - person Andy Turner; 31.08.2019
comment
Я понимаю вашу точку зрения, да. Но насколько я понимаю, необязательный — это просто текст в документации, а не в определении интерфейса. Я не уверен в этом, эта реализация кажется мне немного расплывчатой. - person Andi; 31.08.2019
comment
@ Анди, да, необязательно - это просто текст. Тот факт, что он говорит Throws: UnsupportedOperationException - если операция размещения не поддерживается этой картой, делает ее частью определения интерфейса. - person Andy Turner; 31.08.2019

Collections.unmodifiableMap возвращает Map<K,V> поэтому вам придется использовать его следующим образом:

Map<String, String> unmodifiableAttributes = Collections.unmodifiableMap(
            new Attributes(attributes)
);

И вы не можете привести возвращаемый объект к Attributes, например:

Attributes unmodifiableAttributes = (Attributes) Collections.unmodifiableMap(
            new Attributes(attributes)
);

потому что Collections.unmodifiableMap возвращает экземпляр private static UnmodifiableMap, поэтому вы получите ClassCastException. И Attributes не является подтипом UnmodifiableMap.

Также я думаю, что в вашем случае было бы проще использовать LinkedHashMap напрямую, а не создавать из него производный класс, так как, как я вижу, функционал не отличается от исходного. А затем используйте объект, возвращенный из Collections.unmodifiableMap, как Map.

person Michał Krzywański    schedule 31.08.2019
comment
Да, это то, что я узнал до сих пор. Я не собираюсь создавать подклассы, я просто хочу иметь более короткий способ определения переменных и параметров метода. Как упоминалось в моем вопросе, в C++ ключевое слово using очень помогает сделать исходный код более читабельным без добавления дополнительного кода. Но кажется, в Java нет такого простого способа? - person Andi; 31.08.2019