Giới Thiệu Lập Trình Generic Trong Java: Khái Niệm Và Ứng Dụng

Giới Thiệu Chung Về Generic

Lập trình generic, hay còn gọi là các kiểu tham số hóa (parameterized types), là một tính năng quan trọng được giới thiệu trong Java Development Kit (JDK) 5.0, mang lại sự cải tiến đáng kể về an toàn kiểu dữ liệu cho ngôn ngữ Java. Khái niệm này cho phép chúng ta viết các lớp, giao diện và phương thức có thể hoạt động với nhiều loại đối tượng khác nhau mà không cần chỉ định rõ ràng kiểu dữ liệu cụ thể tại thời điểm biên dịch.

Mục đích chính của lập trình generic là thúc đẩy khả năng tái sử dụng mã (code reusability) và đảm bảo an toàn kiểu (type safety). Nhờ generic, trình biên dịch có thể phát hiện lỗi liên quan đến kiểu dữ liệu ngay tại thời điểm biên dịch thay vì chạy (runtime), giúp giảm thiểu lỗi và làm cho mã nguồn dễ đọc, dễ bảo trì hơn.

Lớp Generic

Một lớp generic là một lớp được định nghĩa với một hoặc nhiều biến kiểu (type variables). Các biến kiểu này thường được đặt trong dấu ngoặc nhọn <> ngay sau tên lớp. Ví dụ, lớp ArrayList trong Java là một lớp generic, cho phép bạn chỉ định kiểu của các phần tử mà nó sẽ chứa, chẳng hạn ArrayList<String>.

Cú pháp định nghĩa một lớp generic như sau:

class MyGenericClass<T, U> {
    // ...
}

Trong ví dụ này, T và U là các biến kiểu. Chúng được sử dụng để chỉ định kiểu trả về của phương thức, kiểu của các trường (field) hoặc biến cục bộ trong lớp. Để minh họa, chúng ta hãy xem xét một lớp DataPair đơn giản có thể lưu trữ hai giá trị cùng kiểu:

public class DataPair<T> {
    private T value1;
    private T value2;

    public DataPair() {
        this.value1 = null;
        this.value2 = null;
    }

    public DataPair(T item1, T item2) {
        this.value1 = item1;
        this.value2 = item2;
    }

    public T getValue1() { return value1; }
    public T getValue2() { return value2; }

    public void setValue1(T newValue) { this.value1 = newValue; }
    public void setValue2(T newValue) { this.value2 = newValue; }
}

Để sử dụng lớp DataPair, bạn cần cung cấp một kiểu dữ liệu cụ thể khi khởi tạo:

// Khởi tạo một cặp chứa hai chuỗi
DataPair<String> stringPair = new DataPair<>("Xin", "Chào");

// Khởi tạo một cặp chứa hai số nguyên
DataPair<Integer> intPair = new DataPair<>(10, 20);

Những Hạn Chế Của Lớp Generic

Mặc dù mạnh mẽ, các lớp generic cũng có một số hạn chế cần lưu ý:

  1. Không thể sử dụng kiểu nguyên thủy (primitive types) làm tham số kiểu (ví dụ: Pair<int> là không hợp lệ, phải dùng Pair<Integer>).
  2. Thông tin kiểu tham số bị xóa tại thời điểm chạy (type erasure), nghĩa là tại runtime, Pair<String> và Pair<Integer> đều trở thành Pair thô (raw type). Do đó, truy vấn kiểu tại runtime chỉ áp dụng cho kiểu thô.
  3. Không thể tạo các mảng của kiểu tham số hóa trực tiếp (ví dụ: new T[10] là không hợp lệ).
  4. Không thể ném hoặc bắt các thể hiện của lớp generic (ví dụ: class MyException<T> extends Exception là không hợp lệ).
  5. Biến kiểu không hợp lệ trong ngữ cảnh tĩnh của lớp generic.

Phương Thức Generic

Ngoài các lớp generic, bạn có thể định nghĩa các phương thức generic. Một phương thức generic có biến kiểu riêng, cho phép nó hoạt động với các kiểu dữ liệu khác nhau độc lập với lớp chứa nó (lớp này có thể là generic hoặc không generic). Biến kiểu của phương thức được khai báo trước kiểu trả về của phương thức.

public class ArrayOperations {
    public static <T> void printArray(T[] elements) {
        for (T element : elements) {
            System.out.println(element);
        }
    }

    public static <T> T getFirstElement(T[] elements) {
        if (elements != null && elements.length > 0) {
            return elements[0];
        }
        return null;
    }
}

Giao Diện Generic

Tương tự như lớp, giao diện cũng có thể được định nghĩa dưới dạng generic để cung cấp khả năng trừu tượng hóa cho các kiểu dữ liệu khác nhau. Khi một lớp triển khai giao diện generic, nó phải cung cấp một kiểu dữ liệu cụ thể cho biến kiểu hoặc bản thân lớp đó cũng phải là generic.

public interface StackService<T> {
    T push(T element); // Đẩy một phần tử vào stack
    T pop();          // Lấy và xóa phần tử trên cùng của stack
    T peek();         // Lấy phần tử trên cùng của stack mà không xóa
    boolean isEmpty(); // Kiểm tra stack có rỗng không
    int size();       // Trả về số lượng phần tử trong stack
}

Giới Hạn Kiểu (Type Bounds) và Ký Tự Đại Diện (Wildcards)

Trong lập trình generic, đôi khi bạn cần giới hạn các kiểu dữ liệu có thể được sử dụng làm tham số kiểu. Điều này được thực hiện bằng cách sử dụng các giới hạn kiểu.

  • Giới Hạn Trên (Upper Bounds) với extends:

    Sử dụng <T extends Type> để chỉ ra rằng T phải là Type hoặc một lớp con của Type. Điều này hữu ích khi bạn cần truy cập các phương thức của Type.

    public class ArrayOperations {
        // Phương thức generic để tìm giá trị nhỏ nhất và lớn nhất trong một mảng các phần tử so sánh được
        public static <T extends Comparable<T>> DataPair<T> getMinMax(T[] data) {
            if (data == null || data.length == 0) {
                return null;
            }
            T minValue = data[0];
            T maxValue = data[0];
    
            for (int i = 1; i < data.length; i++) {
                if (minValue.compareTo(data[i]) > 0) {
                    minValue = data[i];
                }
                if (maxValue.compareTo(data[i]) < 0) {
                    maxValue = data[i];
                }
            }
            return new DataPair<>(minValue, maxValue);
        }
    }
    
  • Ký Tự Đại Diện (Wildcards) ?:

    Ký tự ? là một wildcard biểu thị một kiểu chưa xác định. Nó có thể được sử dụng với các giới hạn trên hoặc dưới.

    • ? extends Type (Upper Bounded Wildcard):

      Cho phép một tham số kiểu có thể là Type hoặc bất kỳ lớp con nào của Type. Thường được sử dụng khi bạn chỉ muốn đọc dữ liệu từ một tập hợp.

      // Phương thức này có thể nhận một DataPair của Employee hoặc bất kỳ lớp con nào của Employee (ví dụ: Manager)
      public static void displayTeamMembers(DataPair<? extends Employee> employeePair) {
          Employee firstMember = employeePair.getValue1();
          Employee secondMember = employeePair.getValue2();
          System.out.println(firstMember.getName() + " và " + secondMember.getName() + " là đồng nghiệp.");
      }
      
    • ? super Type (Lower Bounded Wildcard):

      Cho phép một tham số kiểu có thể là Type hoặc bất kỳ lớp cha nào của Type. Thường được sử dụng khi bạn muốn ghi dữ liệu vào một tập hợp.

      // Phương thức này có thể nhận một DataPair của Manager hoặc bất kỳ lớp cha nào của Manager (ví dụ: Employee)
      public static void findBonusExtremes(Manager[] managers, DataPair<? super Manager> resultContainer) {
          if (managers.length == 0) return;
          Manager minBonusManager = managers[0];
          Manager maxBonusManager = managers[0];
          for (int i = 1; i < managers.length; i++) {
              if (minBonusManager.getBonus() > managers[i].getBonus()) minBonusManager = managers[i];
              if (maxBonusManager.getBonus() < managers[i].getBonus()) maxBonusManager = managers[i];
          }
          resultContainer.setValue1(minBonusManager); // Ghi vào container
          resultContainer.setValue2(maxBonusManager); // Ghi vào container
      }
      
    • ? (Unbounded Wildcard):

      Chỉ ra rằng tham số kiểu có thể là bất kỳ kiểu nào. Hữu ích khi bạn không quan tâm đến kiểu cụ thể hoặc chỉ muốn thực hiện các thao tác hoạt động với Object.

      public class UtilityMethods {
          public static boolean containsNullElements(DataPair<?> pair) {
              return pair.getValue1() == null || pair.getValue2() == null;
          }
      }
      

Ví Dụ Thực Hành Với Code

Để củng cố các khái niệm trên, chúng ta sẽ xem xét một số ví dụ minh họa và sau đó xây dựng một cấu trúc dữ liệu Stack generic.

Ví Dụ 1: Lớp Generic cho Cặp Dữ Liệu

Sử dụng lớp DataPair<T> đã định nghĩa ở trên, chúng ta tạo một phương thức để tìm chuỗi nhỏ nhất và lớn nhất trong một mảng chuỗi.

import java.time.LocalDate;

// Lớp DataPair đã được định nghĩa ở trên

class ArrayOperations {
    /**
     * Tìm chuỗi nhỏ nhất và lớn nhất trong một mảng chuỗi.
     * @param values Mảng các chuỗi.
     * @return Một đối tượng DataPair chứa giá trị min và max, hoặc null nếu mảng rỗng.
     */
    public static DataPair<String> findStringExtremes(String[] values) {
        if (values == null || values.length == 0) {
            return null;
        }
        String minimumVal = values[0];
        String maximumVal = values[0];

        for (int i = 1; i < values.length; i++) {
            String current = values[i];
            if (minimumVal.compareTo(current) > 0) {
                minimumVal = current;
            }
            if (maximumVal.compareTo(current) < 0) {
                maximumVal = current;
            }
        }
        return new DataPair<>(minimumVal, maximumVal);
    }

    /**
     * Tìm giá trị nhỏ nhất và lớn nhất trong một mảng các đối tượng so sánh được.
     * @param data Mảng các đối tượng kiểu T.
     * @return Một DataPair với các giá trị min và max, hoặc null nếu mảng rỗng.
     */
    public static <T extends Comparable<T>> DataPair<T> getMinMax(T[] data) {
        if (data == null || data.length == 0) {
            return null;
        }
        T minValue = data[0];
        T maxValue = data[0];

        for (int i = 1; i < data.length; i++) {
            if (minValue.compareTo(data[i]) > 0) {
                minValue = data[i];
            }
            if (maxValue.compareTo(data[i]) < 0) {
                maxValue = data[i];
            }
        }
        return new DataPair<>(minValue, maxValue);
    }
}

public class PairDemo1 {
    public static void main(String[] args) {
        String[] words = { "Apple", "Banana", "Cherry", "Date", "Fig" };
        DataPair<String> extremes = ArrayOperations.findStringExtremes(words);
        System.out.println("Min: " + extremes.getValue1());
        System.out.println("Max: " + extremes.getValue2());

        LocalDate[] importantDates = {
            LocalDate.of(1990, 5, 15),
            LocalDate.of(2000, 1, 1),
            LocalDate.of(1985, 11, 22),
            LocalDate.of(2010, 3, 10)
        };
        DataPair<LocalDate> dateExtremes = ArrayOperations.getMinMax(importantDates);
        System.out.println("Ngày sớm nhất: " + dateExtremes.getValue1());
        System.out.println("Ngày muộn nhất: " + dateExtremes.getValue2());
    }
}

Kết quả chạy của chương trình trên sẽ tương tự như sau:

Min: Apple
Max: Fig
Ngày sớm nhất: 1985-11-22
Ngày muộn nhất: 2010-03-10

Ví Dụ 2: Ký Tự Đại Diện Với Kế Thừa

Chúng ta sẽ tạo các lớp Employee và Manager (kế thừa từ Employee) để minh họa cách sử dụng ký tự đại diện với giới hạn kiểu trong ngữ cảnh kế thừa.

import java.time.LocalDate;

public class Employee {
    private String name;
    private double salary;
    private LocalDate hireDate;

    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        this.hireDate = LocalDate.of(year, month, day);
    }

    public String getName() { return name; }
    public double getSalary() { return salary; }
    public LocalDate getHireDate() { return hireDate; }

    public void raiseSalary(double byPercent) {
        double raiseAmount = salary * byPercent / 100;
        salary += raiseAmount;
    }

    @Override
    public String toString() {
        return "Employee[Name=" + name + ", Salary=" + salary + "]";
    }
}

public class Manager extends Employee {
    private double bonus;

    public Manager(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);
        this.bonus = 0;
    }

    @Override
    public double getSalary() {
        double baseSalary = super.getSalary();
        return baseSalary + bonus;
    }

    public void setBonus(double b) { this.bonus = b; }
    public double getBonus() { return bonus; }
}

public class UtilityMethods {
    public static boolean containsNullElements(DataPair<?> pair) {
        return pair.getValue1() == null || pair.getValue2() == null;
    }

    public static void exchangeValues(DataPair<?> pair) {
        swapInternal(pair);
    }

    private static <T> void swapInternal(DataPair<T> pair) {
        T temp = pair.getValue1();
        pair.setValue1(pair.getValue2());
        pair.setValue2(temp);
    }
}

public class PersonelPairDemo {
    public static void main(String[] args) {
        Manager ceo = new Manager("Alice CEO", 800000, 2005, 1, 15);
        Manager cfo = new Manager("Bob CFO", 600000, 2005, 1, 15);
        
        DataPair<Manager> managementPair = new DataPair<>(ceo, cfo);
        displayTeamMembers(managementPair); // Sử dụng ? extends Employee

        ceo.setBonus(150000);
        cfo.setBonus(75000);
        Manager[] managersArray = { ceo, cfo };

        DataPair<Employee> bonusResult = new DataPair<>(); // Pair có thể chứa Employee hoặc Manager
        findBonusExtremes(managersArray, bonusResult); // Sử dụng ? super Manager
        System.out.println("Quản lý ít tiền thưởng nhất: " + bonusResult.getValue1().getName() + 
                           ", nhiều tiền thưởng nhất: " + bonusResult.getValue2().getName());
        
        // Hoán đổi vị trí
        findMinMaxBonus(managersArray, bonusResult); // Sử dụng ? super Manager với hoán đổi
        System.out.println("Sau hoán đổi: Quản lý đầu tiên: " + bonusResult.getValue1().getName() + 
                           ", quản lý thứ hai: " + bonusResult.getValue2().getName());
    }

    public static void displayTeamMembers(DataPair<? extends Employee> employeePair) {
        Employee firstMember = employeePair.getValue1();
        Employee secondMember = employeePair.getValue2();
        System.out.println(firstMember.getName() + " và " + secondMember.getName() + " là đồng nghiệp.");
    }

    public static void findBonusExtremes(Manager[] managers, DataPair<? super Manager> resultContainer) {
        if (managers.length == 0) return;
        Manager minBonusManager = managers[0];
        Manager maxBonusManager = managers[0];
        for (int i = 1; i < managers.length; i++) {
            if (minBonusManager.getBonus() > managers[i].getBonus()) minBonusManager = managers[i];
            if (maxBonusManager.getBonus() < managers[i].getBonus()) maxBonusManager = managers[i];
        }
        resultContainer.setValue1(minBonusManager);
        resultContainer.setValue2(maxBonusManager);
    }

    public static void findMinMaxBonus(Manager[] managers, DataPair<? super Manager> resultContainer) {
        findBonusExtremes(managers, resultContainer);
        UtilityMethods.exchangeValues(resultContainer); // Hoán đổi giá trị trong cặp
    }
}

Kết quả chạy của chương trình trên:

Alice CEO và Bob CFO là đồng nghiệp.
Quản lý ít tiền thưởng nhất: Bob CFO, nhiều tiền thưởng nhất: Alice CEO
Sau hoán đổi: Quản lý đầu tiên: Alice CEO, quản lý thứ hai: Bob CFO

Xây Dựng Một Stack Generic

Bây giờ, chúng ta sẽ áp dụng các kiến thức về generic để xây dựng một triển khai Stack sử dụng ArrayList, có khả năng chứa bất kỳ kiểu dữ liệu đối tượng nào một cách an toàn.

1. Giao Diện StackService

public interface StackService<T> {
    T push(T element);   // Đẩy phần tử vào stack, trả về phần tử đã đẩy (hoặc null nếu element là null)
    T pop();             // Lấy và xóa phần tử trên cùng, trả về null nếu stack rỗng
    T peek();            // Lấy phần tử trên cùng mà không xóa, trả về null nếu stack rỗng
    boolean isEmpty();   // Kiểm tra stack có rỗng không
    int size();          // Trả về số lượng phần tử
}

2. Triển Khai DynamicArrayStack

import java.util.ArrayList;

public class DynamicArrayStack<T> implements StackService<T> {
    private ArrayList<T> internalStorage; // Sử dụng ArrayList generic để lưu trữ

    public DynamicArrayStack() {
        internalStorage = new ArrayList<>();
    }

    @Override
    public T push(T element) {
        if (element == null) {
            return null; // Không đẩy phần tử null, trả về null
        }
        internalStorage.add(element);
        return element;
    }

    @Override
    public T pop() {
        if (isEmpty()) {
            return null; // Trả về null nếu stack rỗng
        }
        return internalStorage.remove(internalStorage.size() - 1);
    }

    @Override
    public T peek() {
        if (isEmpty()) {
            return null; // Trả về null nếu stack rỗng
        }
        return internalStorage.get(internalStorage.size() - 1);
    }

    @Override
    public boolean isEmpty() {
        return internalStorage.isEmpty(); // Phương thức kiểm tra rỗng của ArrayList
    }

    @Override
    public int size() {
        return internalStorage.size();
    }

    @Override
    public String toString() {
        return internalStorage.toString();
    }
}

3. Lớp Vehicle

public class Vehicle {
    private int vehicleId;
    private String modelName;

    public Vehicle(int id, String name) {
        this.vehicleId = id;
        this.modelName = name;
    }

    public int getVehicleId() { return vehicleId; }
    public void setVehicleId(int vehicleId) { this.vehicleId = vehicleId; }
    public String getModelName() { return modelName; }
    public void setModelName(String modelName) { this.modelName = modelName; }

    @Override
    public String toString() {
        return "Vehicle [ID=" + vehicleId + ", Model=" + modelName + "]";
    }
}

4. Ứng Dụng Chính (StackDemo)

import java.util.InputMismatchException;
import java.util.Scanner;

public class StackDemo {
    public static void main(String[] args) {
        Scanner inputScanner = new Scanner(System.in);
        while (true) {
            System.out.println("Chọn kiểu dữ liệu (Integer, Double, Vehicle) hoặc 'quit' để thoát:");
            String choice = inputScanner.nextLine().trim();

            if (choice.equalsIgnoreCase("quit")) {
                break;
            }

            try {
                processStackOperations(inputScanner, choice);
            } catch (InputMismatchException e) {
                System.err.println("Lỗi nhập liệu: Vui lòng nhập đúng định dạng.");
                inputScanner.nextLine(); // Xóa bỏ đầu vào không hợp lệ
            } catch (Exception e) {
                System.err.println("Đã xảy ra lỗi: " + e.getMessage());
            }
        }
        inputScanner.close();
    }

    private static void processStackOperations(Scanner scanner, String typeChoice) {
        System.out.println(typeChoice + " Test");
        System.out.print("Nhập số lượng phần tử để push (m): ");
        int pushCount = scanner.nextInt();
        System.out.print("Nhập số lượng phần tử để pop (n): ");
        int popCount = scanner.nextInt();
        scanner.nextLine(); // Đọc ký tự xuống dòng còn lại sau nextInt()

        switch (typeChoice) {
            case "Integer":
                DynamicArrayStack<Integer> intStack = new DynamicArrayStack<>();
                for (int i = 0; i < pushCount; i++) {
                    System.out.print("Nhập số nguyên " + (i + 1) + ": ");
                    Integer val = scanner.nextInt();
                    scanner.nextLine();
                    System.out.println("push:" + intStack.push(val));
                }
                for (int i = 0; i < popCount; i++) {
                    System.out.println("pop:" + intStack.pop());
                }
                System.out.println(intStack.toString());
                int sumInt = 0;
                int initialIntStackSize = intStack.size();
                for (int i = 0; i < initialIntStackSize; i++) {
                    Integer popped = intStack.pop();
                    if (popped != null) {
                        sumInt += popped;
                    }
                }
                System.out.println("sum=" + sumInt);
                break;
            case "Double":
                DynamicArrayStack<Double> doubleStack = new DynamicArrayStack<>();
                for (int i = 0; i < pushCount; i++) {
                    System.out.print("Nhập số thực " + (i + 1) + ": ");
                    Double val = scanner.nextDouble();
                    scanner.nextLine();
                    System.out.println("push:" + doubleStack.push(val));
                }
                for (int i = 0; i < popCount; i++) {
                    System.out.println("pop:" + doubleStack.pop());
                }
                System.out.println(doubleStack.toString());
                double sumDouble = 0;
                int initialDoubleStackSize = doubleStack.size();
                for (int i = 0; i < initialDoubleStackSize; i++) {
                    Double popped = doubleStack.pop();
                    if (popped != null) {
                        sumDouble += popped;
                    }
                }
                System.out.println("sum=" + String.format("%.1f", sumDouble));
                break;
            case "Vehicle":
                DynamicArrayStack<Vehicle> vehicleStack = new DynamicArrayStack<>();
                for (int i = 0; i < pushCount; i++) {
                    System.out.print("Nhập ID phương tiện " + (i + 1) + ": ");
                    int id = scanner.nextInt();
                    scanner.nextLine();
                    System.out.print("Nhập tên mẫu phương tiện " + (i + 1) + ": ");
                    String model = scanner.nextLine();
                    Vehicle vehicle = new Vehicle(id, model);
                    System.out.println("push:" + vehicleStack.push(vehicle));
                }
                for (int i = 0; i < popCount; i++) {
                    System.out.println("pop:" + vehicleStack.pop());
                }
                System.out.println(vehicleStack.toString());
                if (!vehicleStack.isEmpty()) {
                    int initialVehicleStackSize = vehicleStack.size();
                    for (int i = 0; i < initialVehicleStackSize; i++) {
                        Vehicle poppedVehicle = vehicleStack.pop();
                        if (poppedVehicle != null) {
                            System.out.println(poppedVehicle.getModelName());
                        }
                    }
                }
                break;
            default:
                System.out.println("Lựa chọn không hợp lệ.");
        }
        System.out.println("Hoàn thành kiểm tra ngăn xếp.");
    }
}

Dưới đây là một ví dụ về đầu vào và đầu ra của chương trình StackDemo:

Đầu vào mẫu:

Integer
5
2
1 2 3 4 5
Double
5
3
1.1 2.0 4.9 5.7 7.2
Vehicle
3
2
1 Ford
2 Cherry
3 BYD
quit

Đầu ra mẫu:

Chọn kiểu dữ liệu (Integer, Double, Vehicle) hoặc 'quit' để thoát:
Integer Test
Nhập số lượng phần tử để push (m): Nhập số lượng phần tử để pop (n): Nhập số nguyên 1: push:1
Nhập số nguyên 2: push:2
Nhập số nguyên 3: push:3
Nhập số nguyên 4: push:4
Nhập số nguyên 5: push:5
pop:5
pop:4
[1, 2, 3]
sum=6
Hoàn thành kiểm tra ngăn xếp.
Chọn kiểu dữ liệu (Integer, Double, Vehicle) hoặc 'quit' để thoát:
Double Test
Nhập số lượng phần tử để push (m): Nhập số lượng phần tử để pop (n): Nhập số thực 1: push:1.1
Nhập số thực 2: push:2.0
Nhập số thực 3: push:4.9
Nhập số thực 4: push:5.7
Nhập số thực 5: push:7.2
pop:7.2
pop:5.7
pop:4.9
[1.1, 2.0]
sum=3.1
Hoàn thành kiểm tra ngăn xếp.
Chọn kiểu dữ liệu (Integer, Double, Vehicle) hoặc 'quit' để thoát:
Vehicle Test
Nhập số lượng phần tử để push (m): Nhập số lượng phần tử để pop (n): Nhập ID phương tiện 1: Nhập tên mẫu phương tiện 1: push:Vehicle [ID=1, Model=Ford]
Nhập ID phương tiện 2: Nhập tên mẫu phương tiện 2: push:Vehicle [ID=2, Model=Cherry]
Nhập ID phương tiện 3: Nhập tên mẫu phương tiện 3: push:Vehicle [ID=3, Model=BYD]
pop:Vehicle [ID=3, Model=BYD]
pop:Vehicle [ID=2, Model=Cherry]
[Vehicle [ID=1, Model=Ford]]
Ford
Hoàn thành kiểm tra ngăn xếp.
Chọn kiểu dữ liệu (Integer, Double, Vehicle) hoặc 'quit' để thoát:

Thẻ: Java Generics TypeSafety GenericClass GenericMethod

Đăng vào ngày 8 tháng 11 lúc 17:40