Menu
CoddyTech

Ký tự đại diện (?, extends, super)

Thuộc phần Lập trình hướng đối tượng trong hành trình Java của Coddy. Bài học 57 trên 87.

Tham số kiểu có giới hạn giúp giới hạn kiểu tổng quát vào những kiểu cụ thể, nhưng nếu bạn đang làm việc với các kiểu tổng quát có sẵn mà không biết hoặc không quan tâm đến tham số kiểu chính xác thì sao? Đây là lúc wildcard phát huy tác dụng.

Ký tự đại diện không giới hạn ? đại diện cho một kiểu chưa xác định. Nó hữu ích khi bạn muốn chấp nhận bất kỳ kiểu có tham số nào:

public static void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

printList(new ArrayList<String>());   // Chạy được
printList(new ArrayList<Integer>());  // Chạy được

Wildcard giới hạn trên ? extends Type chấp nhận kiểu được chỉ định hoặc bất kỳ lớp con nào của kiểu đó. Hãy dùng nó khi bạn cần đọc từ một cấu trúc tổng quát:

public static double sumNumbers(List<? extends Number> numbers) {
    double sum = 0;
    for (Number n : numbers) {
        sum += n.doubleValue();
    }
    return sum;
}

sumNumbers(Arrays.asList(1, 2, 3));        // Danh sách Integer
sumNumbers(Arrays.asList(1.5, 2.5));       // Danh sách Double

Ký tự đại diện có giới hạn dưới ? super Type chấp nhận kiểu được chỉ định hoặc bất kỳ lớp cha nào của kiểu đó. Dùng nó khi bạn cần ghi vào một cấu trúc tổng quát:

public static void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}

List<Number> numbers = new ArrayList<>();
addIntegers(numbers);  // Chạy được vì Number là kiểu cha của Integer

Một nguyên tắc hữu ích: dùng extends khi bạn chỉ đọc (bên tạo dữ liệu), dùng super khi bạn chỉ ghi (bên nhận dữ liệu). Nguyên tắc này được gọi là PECS: bên tạo dữ liệu dùng Extends, bên nhận dữ liệu dùng Super.

challenge icon

Thử thách

Dễ

Hãy xây dựng một hệ thống cho ăn ở sở thú để minh họa cả ba loại wildcard! Bạn sẽ tạo một hệ thống phân cấp động vật và thức ăn, rồi viết các phương thức tiện ích sử dụng wildcard không giới hạn, giới hạn trên và giới hạn dưới để xử lý những tình huống cho ăn khác nhau.

Bạn sẽ sắp xếp code trong bốn tệp:

  • Food.java: Tạo một hệ thống phân cấp lớp đơn giản cho thức ăn. Bắt đầu với lớp cơ sở Food, có trường riêng name (String), hàm khởi tạo để khởi tạo trường này và phương thức getName(). Sau đó, tạo hai lớp con trong cùng tệp: Meat kế thừa Food (hàm khởi tạo gọi super(name)) và Vegetable kế thừa Food (theo cách tương tự).
  • Animal.java: Tạo lớp cơ sở Animal có trường riêng species (String), hàm khởi tạo và phương thức getSpecies(). Tạo hai lớp con: Carnivore và Herbivore, mỗi lớp kế thừa Animal và gọi hàm khởi tạo của lớp cha.
  • ZooFeeder.java: Đây là nơi wildcard phát huy tác dụng! Tạo một lớp tiện ích có ba phương thức static:

    printInventory(List<?> items) - Dùng wildcard không giới hạn để in ra danh sách bất kỳ. Duyệt qua danh sách và in từng phần tử bằng toString(), mỗi phần tử trên một dòng.

    calculateTotalFood(List<? extends Food> foods) - Dùng wildcard giới hạn trên để đọc từ một danh sách chứa kiểu Food bất kỳ. Trả về số lượng phần tử dưới dạng int. Đây là ví dụ về việc đọc từ một nguồn cung cấp.

    addMeatToStock(List<? super Meat> stock, String meatName) - Dùng wildcard giới hạn dưới để ghi vào một danh sách có thể chứa Meat. Tạo một đối tượng Meat mới với tên đã cho rồi thêm đối tượng đó vào kho. Đây là ví dụ về việc ghi vào một nơi tiếp nhận dữ liệu.

  • Main.java: Kết hợp các phần để hoàn thiện hệ thống cho ăn ở sở thú! Bạn sẽ nhận hai đầu vào: tên một loại thịt (String) và tên một loại rau (String).

    Trước tiên, tạo một ArrayList<Food> rồi thêm một đối tượng Meat với tên loại thịt và một đối tượng Vegetable với tên loại rau. In ra Food inventory:, sau đó gọi printInventory với danh sách này.

    Tiếp theo, in một dòng trống, rồi in Total food items: [count] bằng cách gọi calculateTotalFood với danh sách thức ăn.

    Cuối cùng, tạo một ArrayList<Food> mới có tên meatStock. In một dòng trống, rồi in Adding to meat stock.... Gọi addMeatToStock với danh sách này và chuỗi "Beef". Sau đó, in Meat stock after adding: rồi gọi printInventory với kho thịt.

Bạn sẽ nhận hai đầu vào theo thứ tự: tên một loại thịt và tên một loại rau.

Ghi đè toString() trong lớp Food để trả về tên thức ăn, nhờ đó việc in ra hoạt động chính xác. Hãy chú ý mỗi kiểu wildcard có một mục đích khác nhau: ? để linh hoạt tối đa khi bạn chỉ cần đọc dữ liệu dưới dạng Object, ? extends khi đọc các kiểu cụ thể và ? super khi ghi vào một collection!

Tự làm thử

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

class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String meatName = scanner.nextLine();
        String vegetableName = scanner.nextLine();
        
        // TODO: Tạo một ArrayList<Food> và thêm các đối tượng Meat và Vegetable
        
        // TODO: In ra "Food inventory:", rồi gọi printInventory
        
        // TODO: In ra một dòng trống, rồi in "Total food items: [count]" bằng calculateTotalFood
        
        // TODO: Tạo một ArrayList<Food> mới tên là meatStock
        
        // TODO: In ra một dòng trống, rồi in "Adding to meat stock..."
        
        // TODO: Gọi addMeatToStock với meatStock và "Beef"
        
        // TODO: In ra "Meat stock after adding:", rồi gọi printInventory với meatStock
    }
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch Java online