Общее преобразование системы событий Java в ограниченный подстановочный знак

Я создавал свою собственную систему событий и пытался сделать ее пригодной для использования вот так.

EventSystem.get(EventClientStart.class)
    .setCallback((event) -> {
        System.out.println("Client Started");
    });

EventSystem.get(EventClientStart.class)
    .invoke(new EventClientStart());

Но возникла проблема с проверками типа, что я пропустил? T расширяет событие, я думаю, оно должно работать, и это действительно так (только если я приведу его к (EventController), но тогда я получу предупреждение «Непроверенное приведение»)

EventSystem.java

package im.silkproject.event;

import im.silkproject.event.internal.EventController;

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

public final class EventSystem
{
    private static final Map<Class<? extends Event>, EventController<? extends Event>> map = new HashMap<>();

    private EventSystem() { }

    public static <T extends Event> EventController<T> get(Class<T> event)
    {
        return map.computeIfAbsent(event, k -> new EventController<>());
    }
}

Событие.java

package im.silkproject.event;

public class Event
{
    private boolean cancelled;

    public void cancel()
    {
        cancelled = true;
    }

    public boolean isCancelled()
    {
        return cancelled;
    }
}

EventCallback.java

package im.silkproject.event.internal;

@FunctionalInterface
public interface EventCallback<T>
{
    void __call(T event);
}

EventController.java

package im.silkproject.event.internal;

import java.util.concurrent.CopyOnWriteArrayList;

public class EventController<T>
{
    private final CopyOnWriteArrayList<EventCallback<T>> callbacks = new CopyOnWriteArrayList<>();

    public void invoke(T event)
    {
        for (EventCallback<T> callback : callbacks)
        {
            callback.__call(event);
        }
    }

    public int length()
    {
        return callbacks.size();
    }

    public boolean setCallback(EventCallback<T> event)
    {
        return callbacks.addIfAbsent(event);
    }

    public boolean unsetCallback(EventCallback<T> event)
    {
        return callbacks.remove(event);
    }
}

Это проблема ассоциации ключ-значение, которая не является частью языка. Невозможно сообщить компилятору, что универсальный тип определенного ключа совместим с универсальным типом конкретного значения, хранящегося в карте для этого ключа. Вы можете объявить только общий универсальный тип для всех ключей и общий универсальный тип для всех значений. Похоже на: вопросы и ответы

Holger 05.04.2024 15:03
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
1
64
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я думаю, это связано с Covariance на Java. Вы можете читать только с List или Map, если используете Upper Bounded Wildcards. Более подробную информацию вы можете найти в этом ответе здесь

На самом деле, я не понимаю, как это можно использовать в моем случае.

qhouz 05.04.2024 07:13
Ответ принят как подходящий

Несмотря на то, что в вашем методе вы заявили, что T является подтипом Event и что тип возвращаемого значения — EventController<T>, вы возвращаете значение Maps, которое вместо этого соответствует EventController<? extends Event>>.

В какой-то момент T будет относиться к очень конкретному подтипу Event, тогда как ? extends Event может быть любым неизвестным подтипом События и не обязательно соответствовать T. В этом сценарии компилятор не может определить во время компиляции, какой именно тип возвращается для ? extends Event. Итак, поскольку возвращать ? extends Event вместо T небезопасно, вы получаете ошибку компилятора.

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

public static <T extends Event> EventController<? extends Event> get(Class<T> event) {
     return map.computeIfAbsent(event, k -> new EventController<>());
}

Вот также очень интересная статья из Java Tutorials, в которой представлены подстановочные знаки и объясняется ваш конкретный случай на примере. Я прикреплю сюда выдержку и выделю ту часть, которая относится к вашему случаю:

public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
        s.draw(this);
   }
}

Правила типов гласят, что drawAll() можно вызывать только в списках, содержащих ровно Shape: например, его нельзя вызывать из List<Circle>. Это прискорбно, поскольку все, что делает метод, — это считывает фигуры из списка, поэтому его с таким же успехом можно вызвать по List<Circle>. Чего мы действительно хотим, так это того, чтобы метод принимал список любой формы:

public void drawAll(List<? extends Shape> shapes) {
    ...
}

Здесь есть небольшое, но очень важное отличие: мы заменили тип List<Shape> на List<? extends Shape>. Теперь drawAll() будет принимать списки любого подкласса Shape, поэтому теперь мы можем вызвать его в List<Circle>, если захотим.

List<? extends Shape> — пример ограниченного подстановочного знака. ? обозначает неизвестный тип, как и подстановочные знаки, которые мы видели ранее. Однако в данном случае мы знаем, что этот неизвестный тип на самом деле является подтипом Shape. (Примечание: это может быть сам Shape или какой-то подкласс; он не обязательно расширяет Shape буквально.) Мы говорим, что Shape — это верхняя граница подстановочного знака.

Как обычно, за гибкость использования подстановочных знаков приходится платить. Эта цена заключается в том, что теперь запрещено записывать фигуры в тело метода. Например, запрещено:

public void addRectangle(List<? extends Shape> shapes) {
    // Compile-time error!
    shapes.add(0, new Rectangle());
}

Вы сможете понять, почему приведенный выше код запрещен. Тип второго параметра shapes.add()? extends Shape — неизвестный подтип Shape. Поскольку мы не знаем, что это за тип, мы не знаем, является ли он супертипом Rectangle; это может быть, а может и не быть таким супертипом, поэтому передавать Rectangle туда небезопасно.

Другие вопросы по теме