CodeGym /Các khóa học /JAVA 25 SELF /Tạo và xử lý sự kiện tự định nghĩa

Tạo và xử lý sự kiện tự định nghĩa

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Khi bạn cần sự kiện tự định nghĩa

Các sự kiện chuẩn của Java như nhấn nút, di chuyển chuột hoặc thay đổi văn bản chỉ là phần nổi của tảng băng. Trong các ứng dụng thực tế xuất hiện vô số tình huống không nằm gọn trong những khuôn mẫu đó. Ví dụ, chương trình có thể tải dữ liệu từ Internet và cần thông báo cho các phần khác của ứng dụng khi việc tải hoàn tất. Trong game, người chơi có thể nhận được một thành tựu mới, và nên thông báo đồng thời cho nhiều thành phần, chẳng hạn giao diện và hệ thống ghi log. Trong ứng dụng doanh nghiệp, việc thay đổi trạng thái đơn hàng phải kích hoạt thông báo cho kế toán, kho và người dùng cùng lúc.

Trong tất cả các trường hợp như vậy, việc tạo các loại sự kiện và listener riêng là hữu ích. Chúng cho phép mô tả những kịch bản tương tác đặc thù giữa các thành phần và giúp mã linh hoạt, có cấu trúc hơn.

Cấu trúc của sự kiện tùy chỉnh

Để tạo sự kiện của riêng bạn trong Java, thường triển khai ba phần:

  1. Lớp sự kiện — thường kế thừa java.util.EventObject. Lưu trữ thông tin về sự kiện (nguồn phát là ai, dữ liệu nào gắn với sự kiện).
  2. Giao diện listener — ví dụ MyEventListener, mở rộng java.util.EventListener và định nghĩa các phương thức xử lý sự kiện.
  3. Cơ chế đăng ký/hủy đăng ký — các phương thức trong nguồn sự kiện cho add...Listener/remove...Listener và gọi listener khi sự kiện xảy ra (fire...).

Hãy xem từng bước qua ví dụ một ứng dụng tải dữ liệu từ tệp hoặc mạng, và chúng ta cần thông báo cho các phần khác khi tải xong.

Lớp sự kiện

Tạo một lớp sự kiện chứa thông tin về lần tải đã hoàn tất.

import java.util.EventObject;

// Lớp sự kiện: kế thừa từ EventObject
public class DataLoadedEvent extends EventObject {
    private final String data; // Thông tin bổ sung về sự kiện

    public DataLoadedEvent(Object source, String data) {
        super(source); // source — đối tượng đã kích hoạt sự kiện
        this.data = data;
    }

    public String getData() {
        return data;
    }
}

Ở đây source là đối tượng nguồn phát sự kiện (ví dụ, bộ tải dữ liệu), còn data là chuỗi với dữ liệu đã tải (có thể là đường dẫn tệp, JSON, kết quả, v.v.).

Giao diện listener

Xác định giao diện listener. Thông thường nó mở rộng EventListener (giao diện đánh dấu để phân loại).

import java.util.EventListener;

// Giao diện listener cho sự kiện của chúng ta
public interface DataLoadedListener extends EventListener {
    void dataLoaded(DataLoadedEvent event);
}

Phương thức dataLoaded sẽ được gọi khi sự kiện xảy ra.

Nguồn phát sự kiện

Cần một thực thể lưu danh sách listener, cho phép đăng ký/gỡ bỏ và thông báo khi sự kiện xảy ra.

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

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

    // Đăng ký listener
    public void addDataLoadedListener(DataLoadedListener listener) {
        listeners.add(listener);
    }

    // Gỡ bỏ listener
    public void removeDataLoadedListener(DataLoadedListener listener) {
        listeners.remove(listener);
    }

    // Phương thức kích hoạt sự kiện (ví dụ, sau khi tải dữ liệu)
    private void fireDataLoaded(String data) {
        DataLoadedEvent event = new DataLoadedEvent(this, data);
        // Thông báo cho tất cả listener
        for (DataLoadedListener listener : listeners) {
            listener.dataLoaded(event);
        }
    }

    // Ví dụ về phương thức "tải" dữ liệu
    public void loadData() {
        // Giả lập tải (ví dụ, từ tệp hoặc mạng)
        String loadedData = "Đây là dữ liệu đã được tải!";
        System.out.println("Dữ liệu đã được tải: " + loadedData);

        // Thông báo cho tất cả listener
        fireDataLoaded(loadedData);
    }
}

Trong thực tế, phương thức loadData() có thể bất đồng bộ, đọc tệp, gọi server, v.v., nhưng với ví dụ này chúng ta chỉ mô phỏng việc tải.

2. Sử dụng sự kiện tùy chỉnh

Giả sử chúng ta có một thành phần muốn biết khi việc tải dữ liệu hoàn tất.

public class DataLoadedHandler implements DataLoadedListener {
    @Override
    public void dataLoaded(DataLoadedEvent event) {
        System.out.println("Trình xử lý đã nhận sự kiện: " + event.getData());
    }
}

Kết nối tất cả trong lớp chính của ứng dụng:

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

        // Đăng ký listener
        loader.addDataLoadedListener(handler);

        // Bắt đầu tải dữ liệu
        loader.loadData();
    }
}

Điều gì sẽ xảy ra?

  1. DataLoader tải dữ liệu (mô phỏng).
  2. Sau khi tải xong, gọi fireDataLoaded, tạo đối tượng sự kiện và thông báo cho tất cả listener.
  3. Trình xử lý của chúng ta (DataLoadedHandler) nhận sự kiện và in ra thông báo.

Ví dụ đầu ra:

Dữ liệu đã được tải: Đây là dữ liệu đã được tải!
Trình xử lý đã nhận sự kiện: Đây là dữ liệu đã được tải!

Sử dụng lớp ẩn danh và biểu thức lambda

Để không phải tạo quá nhiều lớp riêng cho mỗi listener, thường dùng lớp ẩn danh hoặc biểu thức lambda (từ Java 8):

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

        // Listener bằng biểu thức lambda
        loader.addDataLoadedListener(event ->
            System.out.println("Trình xử lý lambda: " + event.getData())
        );

        loader.loadData();
    }
}

Đăng ký và gỡ bỏ listener

Có thể thêm và xóa listener. Điều này quan trọng để quản lý bộ nhớ và ngăn rò rỉ (đặc biệt nếu listener là đối tượng “nặng” hoặc không còn cần thiết).

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

// Sau này, nếu không còn cần handler:
loader.removeDataLoadedListener(handler);

Nếu không gỡ listener, còn nguồn phát sự kiện sống lâu, listener sẽ ở lại trong danh sách và không bị bộ gom rác thu dọn — có thể dẫn đến rò rỉ bộ nhớ.

3. Thực hành: ví dụ nhỏ — bộ đếm lượt nhấn

Hãy làm một ví dụ nhỏ có thể phát triển trong khuôn khổ ứng dụng học tập. Giả sử chúng ta có một lớp bộ đếm tăng giá trị của nó, và mỗi lần tăng sẽ thông báo cho listener về giá trị mới.

Lớp sự kiện

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;
    }
}

Giao diện listener

import java.util.EventListener;

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

Lớp bộ đếm

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);
        }
    }
}

Sử dụng

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

        // Đăng ký listener qua lambda
        counter.addCounterChangedListener(event ->
            System.out.println("Bộ đếm đã thay đổi: " + event.getNewValue())
        );

        counter.increment(); // Bộ đếm đã thay đổi: 1
        counter.increment(); // Bộ đếm đã thay đổi: 2
    }
}

4. Các lỗi thường gặp khi tạo và xử lý sự kiện tự định nghĩa

Lỗi số 1: quên gọi phương thức thông báo listener. Sự kiện được tạo ra, nhưng phương thức đáng lẽ phải thông báo cho listener (fireDataLoaded, fireCounterChanged) lại không được gọi. Kết quả là listener “im lặng”.

Lỗi số 2: ngoại lệ trong handler của listener. Nếu một listener ném ngoại lệ, các listener còn lại có thể không nhận được thông báo. Thực hành tốt — bao bọc lời gọi listener trong trycatch để một listener “xấu” không ảnh hưởng đến những listener khác.

Lỗi số 3: không gỡ listener. Nếu listener không còn cần thiết nhưng không bị gỡ, nó vẫn tiếp tục nhận sự kiện và không bị giải phóng khỏi bộ nhớ. Điều này có thể dẫn đến rò rỉ bộ nhớ — đặc biệt khi nguồn phát sống lâu và có nhiều listener.

Lỗi số 4: sửa đổi danh sách listener trong khi duyệt. Nếu trong handler có ai đó thêm hoặc xóa listener, điều này có thể dẫn đến ConcurrentModificationException. Cách an toàn — trước hết sao chép danh sách sang một mảng riêng, sau đó lặp trên bản sao.

Lỗi số 5: thao tác lâu trong handler. Nếu handler làm việc gì đó lâu (tải tệp, chờ mạng), giao diện có thể “đơ”. Hãy chuyển các tác vụ nặng sang luồng riêng hoặc sử dụng cơ chế bất đồng bộ.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION