CodeGym /Cursos /JAVA 25 SELF /Criação e tratamento de eventos personalizados

Criação e tratamento de eventos personalizados

JAVA 25 SELF
Nível 50 , Lição 2
Disponível

1. Quando são necessários eventos personalizados

Os eventos padrão do Java, como clique de botão, movimento do mouse ou mudança de texto — são apenas a ponta do iceberg. Em aplicativos reais surgem muitas situações que não se encaixam nesses moldes. Por exemplo, um programa pode carregar dados da internet e é preciso notificar outras partes do aplicativo quando o carregamento é concluído. Em um jogo, o jogador pode obter uma nova conquista, e isso deve ser informado a vários componentes ao mesmo tempo, por exemplo, à interface e ao sistema de logging. Em um aplicativo de negócios, a alteração do estado de um pedido deve acionar notificações para a contabilidade, o estoque e o usuário simultaneamente.

Em todos esses casos, é útil criar tipos próprios de eventos e ouvintes. Eles permitem descrever cenários exclusivos de interação entre componentes e tornar o código mais flexível e estruturado.

Estrutura de um evento personalizado

Para criar seu próprio evento em Java, geralmente implementamos três partes:

  1. Classe de evento — normalmente, uma subclasse de java.util.EventObject. Armazena informações sobre o evento (quem é a origem, quais dados estão associados ao evento).
  2. Interface do ouvinte — por exemplo, MyEventListener, que estende java.util.EventListener e define os métodos de tratamento do evento.
  3. Mecanismo de inscrição/cancelamento — métodos na fonte do evento para add...Listener/remove...Listener e chamada dos ouvintes quando o evento acontece (fire...).

Vamos considerar isso passo a passo no exemplo de um aplicativo em que dados são carregados de um arquivo ou da rede, e precisamos notificar outros quando o carregamento for concluído.

Classe de evento

Vamos criar uma classe de evento que conterá informações sobre a conclusão do carregamento.

import java.util.EventObject;

// Classe de evento: herdamos de EventObject
public class DataLoadedEvent extends EventObject {
    private final String data; // Informações adicionais sobre o evento

    public DataLoadedEvent(Object source, String data) {
        super(source); // source — objeto que disparou o evento
        this.data = data;
    }

    public String getData() {
        return data;
    }
}

Aqui, source é o objeto-fonte do evento (por exemplo, o carregador de dados), e data é uma string com os dados carregados (pode ser um caminho de arquivo, JSON, resultado etc.).

Interface do ouvinte

Vamos definir a interface do ouvinte. Normalmente, ela estende EventListener (uma interface de marcação para tipagem).

import java.util.EventListener;

// Interface do ouvinte do nosso evento
public interface DataLoadedListener extends EventListener {
    void dataLoaded(DataLoadedEvent event);
}

O método dataLoaded será chamado quando o evento ocorrer.

Fonte do evento

Precisamos de uma entidade que armazene a lista de ouvintes, permita registrá-los/removê-los e notifique quando o evento acontecer.

import java.util.ArrayList;
import java.util.List;

public class DataLoader {
    private final List<DataLoadedListener> listeners = new ArrayList<>();

    // Registro do ouvinte
    public void addDataLoadedListener(DataLoadedListener listener) {
        listeners.add(listener);
    }

    // Remoção do ouvinte
    public void removeDataLoadedListener(DataLoadedListener listener) {
        listeners.remove(listener);
    }

    // Método que inicia o evento (por exemplo, após o carregamento dos dados)
    private void fireDataLoaded(String data) {
        DataLoadedEvent event = new DataLoadedEvent(this, data);
        // Notificamos todos os ouvintes
        for (DataLoadedListener listener : listeners) {
            listener.dataLoaded(event);
        }
    }

    // Exemplo de método que "carrega" dados
    public void loadData() {
        // Simulamos o carregamento (por exemplo, de um arquivo ou da rede)
        String loadedData = "Estes são os dados carregados!";
        System.out.println("Dados carregados: " + loadedData);

        // Informamos a todos os ouvintes
        fireDataLoaded(loadedData);
    }
}

Na prática, o método loadData() pode ser assíncrono, ler um arquivo, acessar um servidor etc., mas, para o exemplo, apenas simulamos o carregamento.

2. Usando um evento personalizado

Suponha que temos um componente que quer saber quando o carregamento dos dados termina.

public class DataLoadedHandler implements DataLoadedListener {
    @Override
    public void dataLoaded(DataLoadedEvent event) {
        System.out.println("O manipulador recebeu o evento: " + event.getData());
    }
}

Vamos juntar tudo na classe principal do aplicativo:

public class Main {
    public static void main(String[] args) {
        DataLoader loader = new DataLoader();
        DataLoadedHandler handler = new DataLoadedHandler();

        // Registramos o ouvinte
        loader.addDataLoadedListener(handler);

        // Iniciamos o carregamento de dados
        loader.loadData();
    }
}

O que vai acontecer?

  1. DataLoader carrega os dados (simulação).
  2. Após o carregamento, chama fireDataLoaded, que cria o objeto de evento e notifica todos os ouvintes.
  3. Nosso manipulador (DataLoadedHandler) recebe o evento e imprime a mensagem.

Exemplo de saída:

Dados carregados: Estes são os dados carregados!
O manipulador recebeu o evento: Estes são os dados carregados!

Uso de classes anônimas e expressões lambda

Para não criar classes separadas para cada ouvinte, costuma-se usar classes anônimas ou expressões lambda (a partir do Java 8):

public class Main {
    public static void main(String[] args) {
        DataLoader loader = new DataLoader();

        // Ouvinte via expressão lambda
        loader.addDataLoadedListener(event ->
            System.out.println("Manipulador lambda: " + event.getData())
        );

        loader.loadData();
    }
}

Registro e remoção de ouvintes

Os ouvintes podem ser adicionados e removidos. Isso é importante para o gerenciamento de memória e para evitar vazamentos (especialmente se o ouvinte for um objeto “pesado” ou não for mais necessário).

DataLoadedHandler handler = new DataLoadedHandler();
loader.addDataLoadedListener(handler);

// Depois, se o manipulador não for mais necessário:
loader.removeDataLoadedListener(handler);

Se você não remover o ouvinte, e a própria fonte do evento viver por muito tempo, o ouvinte permanecerá na lista e não será removido pelo garbage collector — isso pode levar a vazamentos de memória.

3. Prática: mini-exemplo — contador de cliques

Vamos fazer um pequeno exemplo que pode ser desenvolvido no contexto de um aplicativo de estudos. Suponha uma classe de contador que incrementa seu valor e, a cada incremento, notifica os ouvintes sobre o novo valor.

Classe de evento

import java.util.EventObject;

public class CounterChangedEvent extends EventObject {
    private final int newValue;

    public CounterChangedEvent(Object source, int newValue) {
        super(source);
        this.newValue = newValue;
    }

    public int getNewValue() {
        return newValue;
    }
}

Interface do ouvinte

import java.util.EventListener;

public interface CounterChangedListener extends EventListener {
    void counterChanged(CounterChangedEvent event);
}

Classe do contador

import java.util.ArrayList;
import java.util.List;

public class Counter {
    private int value = 0;
    private final List<CounterChangedListener> listeners = new ArrayList<>();

    public void addCounterChangedListener(CounterChangedListener listener) {
        listeners.add(listener);
    }

    public void removeCounterChangedListener(CounterChangedListener listener) {
        listeners.remove(listener);
    }

    public void increment() {
        value++;
        fireCounterChanged();
    }

    private void fireCounterChanged() {
        CounterChangedEvent event = new CounterChangedEvent(this, value);
        for (CounterChangedListener listener : listeners) {
            listener.counterChanged(event);
        }
    }
}

Uso

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();

        // Registramos o ouvinte via lambda
        counter.addCounterChangedListener(event ->
            System.out.println("O contador mudou: " + event.getNewValue())
        );

        counter.increment(); // O contador mudou: 1
        counter.increment(); // O contador mudou: 2
    }
}

4. Erros comuns ao criar e tratar eventos personalizados

Erro nº 1: esqueceu de chamar o método de notificação dos ouvintes. O evento é criado, mas o método que deve notificar os ouvintes (fireDataLoaded, fireCounterChanged) não é chamado. Como resultado, os ouvintes ficam em silêncio.

Erro nº 2: exceções nos manipuladores dos ouvintes. Se um dos ouvintes lançar uma exceção, os demais podem não receber notificações. Uma boa prática é envolver as chamadas aos ouvintes em trycatch, para que um ouvinte “problemático” não atrapalhe os outros.

Erro nº 3: não remover os ouvintes. Se um ouvinte não for mais necessário, mas não for removido, ele continuará recebendo eventos e não será removido da memória. Isso pode levar a vazamentos de memória — especialmente quando a fonte vive por muito tempo e há muitos ouvintes.

Erro nº 4: modificar a lista de ouvintes durante a iteração. Se, no manipulador do evento, alguém adicionar ou remover um ouvinte, isso pode levar a ConcurrentModificationException. Uma abordagem segura é primeiro copiar a lista para um array separado e então iterar sobre a cópia.

Erro nº 5: operações longas nos manipuladores. Se o manipulador fizer algo demorado (carregamento de arquivo, espera de rede), a interface pode “travar”. Mova as tarefas pesadas para outra thread ou use mecanismos assíncronos.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION