Невозможно поместить в карту Java тип ключа generi c и тип значения вложенного generi c - PullRequest
2 голосов
/ 22 апреля 2020

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

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

Вот весь код, который компилируется.

public interface Cache {
}

public class ExampleCache implements Cache {
}

public interface Item {
}

public class ExampleItem implements Item {
}

public interface Loader<C extends Cache> {
    void load(C cache);
}

public class ExampleLoader<C extends Cache> implements Loader<ExampleCache> {

    @Override
    public void load(ExampleCache cache) {
    }

}

А вот класс, который не компилируется.

import java.util.HashMap;
import java.util.Map;

public class Registry {

    private final Map<String, Cache> caches = new HashMap<>();
    private final Map<String, Loader<Cache>> loaders = new HashMap<>();

    public Registry() {

        caches.put("cache1", new ExampleCache());

        Loader<ExampleCache> exampleLoader = new ExampleLoader<>();

        // PROBLEM LINE: this line does not compile
        // there is a red underline under 'exampleLoader'
        loaders.put("cache1", exampleLoader);

    }

    public void loadAll() {

        for (Map.Entry<String, Loader<Cache>> entry : loaders.entrySet()) {
            String cacheName = entry.getKey();
            Loader<Cache> loader = entry.getValue();
            Cache cache = caches.get(cacheName);
            loader.load(cache);
        }

    }

}

Ошибка, которую я получаю, приведена ниже.

Error:(19, 31) java: incompatible types: Loader<ExampleCache> cannot be converted to Loader<Cache>

Что я делаю не так? Пожалуйста, объясните свои рассуждения, чтобы я мог глубже понять, что я делаю неправильно. Кроме того, при предложении решения, пожалуйста, убедитесь, что весь код выше компилируется. У меня были случаи, когда, если я исправляю одну область, тогда другая ломается.

Спасибо.

Edit1 : Кто-то предложил изменить объявление карты загрузчика на:

private final Map<String, Loader<? extends Cache>> loaders = new HashMap<>();

Но это нарушает метод loadAll () под ним.

import java.util.HashMap;
import java.util.Map;

public class Registry {

    private final Map<String, Cache> caches = new HashMap<>();
    private final Map<String, Loader<? extends Cache>> loaders = new HashMap<>();

    public Registry() {

        caches.put("cache1", new ExampleCache());

        Loader<ExampleCache> exampleLoader = new ExampleLoader<>();

        loaders.put("cache1", exampleLoader);

    }

    public void loadAll() {

        for (Map.Entry<String, Loader<? extends Cache>> entry : loaders.entrySet()) {

            String cacheName = entry.getKey();
            Loader<? extends Cache> loader = entry.getValue();
            Cache cache = caches.get(cacheName);

            // NEW PROBLEM LINE: this line does not compile
            loader.load(cache);

        }

    }

}

Новая ошибка компиляции:

Error:(26, 25) java: incompatible types: Cache cannot be converted to capture#1 of ? extends Cache

Редактировать 2 : Похоже, решение Энди Тернера сработало! Хотя он скрывает нагрузку внутри нового класса, похоже, что это самый удачный компромисс из всех, что я могу иметь.

import java.util.HashMap;
import java.util.Map;

public class Registry {

    static class CacheLoader<C extends Cache> {

        final C cache;
        final Loader<C> loader;

        CacheLoader(C cache, Loader<C> loader) {
            this.cache = cache;
            this.loader = loader;
        }

        public void load() {
            loader.load(cache);
        }

    }

    private final Map<String, CacheLoader<?>> cacheLoaders = new HashMap<>();

    public Registry() {
        cacheLoaders.put("cache1", new CacheLoader<>(new ExampleCache(), new ExampleLoader<>()));
    }

    public void loadAll() {
        for (CacheLoader<?> cacheLoader : cacheLoaders.values()) {
            cacheLoader.load();
        }
    }

}

Ответы [ 3 ]

3 голосов
/ 22 апреля 2020

Кажется, что две ваши карты:

private final Map<String, Cache> caches = new HashMap<>();
private final Map<String, Loader<Cache>> loaders = new HashMap<>();

по существу параллельны, в том смысле, что Cache и Loader<Cache> для одного и того же ключа предназначены для совместного использования.

Итак, храните их вместе:

private final Map<String, Together<?>> togethers = new HashMap<>();

togethers.put(new Together<>(new ExampleCache(), new ExampleLoader<>()));

, где класс Together выглядит примерно так:

class Together<C extends Cache> {
  private final C cache;
  private final Loader<C> loader;

  // Constructor.

  void load() {
    loader.load(cache);
  }
}

Таким образом, не имеет значения, что это Together<?>, вам не нужно знать этот тип для безопасного вызова метода load().

1 голос
/ 22 апреля 2020

Проблема в том, что Loader<ExampleCache> не является Loader<Cache>.

A Loader<Cache> может принять любой Cache в качестве параметра для его .load() метода, в то время как Loader<ExampleCache> может только accept ExampleCache s.

Из вашего кода я могу заключить, что существует связь между loaders и caches, в частности, должен соблюдаться следующий инвариант:

loaders.get(foo).load(caches.get(foo));

Это должно быть действительным, но вам будет трудно доказать это.

Чтобы решить эту проблему, вам понадобится несколько неконтролируемых приведений, или используйте другой дизайн.

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

    private final Map<String, Cache> caches = new HashMap<>();
    private final Map<String, Loader<? extends Cache>> loaders = new HashMap<>();
    private <C extends Cache> void putIntoRegisty(String name, C cache, Loader<? super C> loader) {
        caches.put(name, cache);
        loaders.put(name, loader);
    }
    

    Только когда-либо изменяйте caches, используя эти (или аналогичные) методы. (Например, вы можете добавить тот, который удаляет кэш и загрузчик).
    Важная часть: поскольку у вас есть только небольшая часть кода, в которой вы изменяете карты, вам будет легче убедить себя и других что инвариант имеет место.

    Теперь до крови loadAll:

    public void loadAll() {
    
        for (Map.Entry<String, Loader<? extends Cache>> entry : loaders.entrySet()) {
            String cacheName = entry.getKey();
            // This is safe, see putIntoRegistry
            @SuppressWarnings("unchecked")
            Loader<Cache> loader = (Loader<Cache>) entry.getValue();
            Cache cache = caches.get(cacheName);
            loader.load(cache);
        }
    }
    
  • Второй вариант - использовать ОДНУ карту для ключа и значения:

    // A Java 14 record would be better better...
    private static class CacheAndLoader<C extends Cache> {
        C cache;
        Loader<C> loader;
        void load() {
            loader.load(cache);
        }           
    }
    private final Map<String, CacheAndLoader<? extends Cache>> cachesAndLoaders = new HashMap<>();
    private <C extends Cache> void putIntoRegisty(String name, C cache, Loader<C> loader) {
        CacheAndLoader<C> cal = new CacheAndLoader<>();
        cal.cache = cache;
        cal.loader = loader;
        cachesAndLoaders.put(name, cal);
    }
    
    
    public void loadAll() {
        for (CacheAndLoader<? extends Cache> cal : cachesAndLoaders.values()) {
            cal.load();
        }
    }
    

    Здесь нам не нужны неконтролируемые приведения, но это за небольшую цену: дополнительный класс данных. Я поместил его в Registry, но ваш стиль кода может потребовать, чтобы он был в собственном файле.

0 голосов
/ 22 апреля 2020

В дополнение к предыдущей модификации, измените также интерфейс и метод переопределения

public interface Loader<C extends Cache> {
    void load(Cache cache);
}

public class ExampleLoader<C extends Cache> implements Loader<ExampleCache> {

    @Override
    public void load(Cache cache) {
    }
}
...