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:
- 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).
- Interface do ouvinte — por exemplo, MyEventListener, que estende java.util.EventListener e define os métodos de tratamento do evento.
- 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?
- DataLoader carrega os dados (simulação).
- Após o carregamento, chama fireDataLoaded, que cria o objeto de evento e notifica todos os ouvintes.
- 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 try–catch, 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.
GO TO FULL VERSION