Java Bài 51 – Generic Tập 1 – Làm Quen Với Generic

Posted by
Rating: 5.0/5. From 7 votes.
Please wait...

Chào mừng các bạn đến với bài học Java số 51, bài học về Generic. Bài học này nằm trong chuỗi bài học lập trình ngôn ngữ Java của Yellow Code Books.

Sau chuỗi bài dài đăng đẳng về lập trình song song, đa nhiệm thông qua Thread và những kiến thức mở rộng liên quan đến nó. Hôm nay chúng ta sẽ đi đến một kiến thức nhẹ ký hơn, nhưng không kém phần quan trọng trong việc lập trình với ngôn ngữ Java. Kiến thức về Generic.

Tuy bài học được chia nhỏ làm nhiều phần, nhưng lượng kiến thức mà chúng mang lại cũng không quá nhiều. Cái chính là những giải nghĩa để giúp các bạn hiểu rõ về Generic này. Vì theo kinh nghiệm của mình đã từng lân la tìm hiểu ở khá nhiều tài liệu khác nhau, thì Generic ở các tài liệu này viết rất khó hiểu, đặc biệt là để giải nghĩa cho câu hỏi tại sao phải dùng Generic, thì ít nơi nào nói rõ. Thôi thì mời các bạn cùng đọc bài viết của mình để cùng so sánh nhé.

Generic Là Gì?

Dĩ nhiên việc đầu tiên khi tìm hiểu đến một kỹ thuật mới mẻ, chúng ta lại tra từ điển. Generic, dịch ra là chung. Thực ra không phải nghĩa của nó là chung chung, là không rõ ràng đâu. Mà nó là một loại, hay một tập hợp chung nào đó. Trái nghĩa với đặc trưng, đặc thù.

Generic trong lập trình cũng vậy, nó được dùng trong ngữ cảnh khi bạn muốn xây dựng các chức năng hay các đối tượng nào đó mà bạn không cần quan tâm đến một kiểu dữ liệu đặc trưng cụ thể, bạn định nghĩa ra một kiểu tập hợp chung cho các kiểu dữ liệu. Cái kiểu tập hợp chung đó được bạn định nghĩa ra ở thời điểm mà bạn lập trình, khi này bạn không bị gò bó quá nhiều vào kiểu dữ liệu đặc trưng nữa, bạn thoải mái sáng tác ra các chức năng hay đối tượng có thể dùng chung cho nhiều kiểu dữ liệu khác nhau. Khi thực thi ứng dụng, bạn mới cần chỉ định cụ thể kiểu dữ liệu đặc trưng cần dùng đến, hoặc hệ thống cũng tự suy ra kiểu dữ liệu đặc trưng cho bạn.

Bởi vì vậy mà Generic còn được gọi là Typed Parameters. Tức là khi này tham số bạn đưa vào cho chức năng hay đối tượng khi thực thi chính là một kiểu dữ liệu đặc trưng nào đó.

Trước khi đi vào tìm hiểu kỹ hơn về Generic, mình xin nhắc lại rằng thực ra đâu đó, bạn cũng từng ít nhiều lần gặp đến Generic này. Vì một lẽ Generic khá phố biến và được sử dụng rất nhiều trong Java, nếu bạn còn nhớ, ở bài thực hành số 5 của bài 49 này cũng có đoạn code mà chúng ta phải dùng đến Generic, Callable<String>, khi còn chưa biết đến nó là gì.

Bài thực hành số 5 của bài 49 đã dùng đến Generic
Bài thực hành số 5 của bài 49 đã dùng đến Generic

Qua đó bạn có thể thấy Generic là cái gì đó cũng khá là gần gũi và dễ dùng đúng không nào. Vâng, việc sử dụng code đã có Generic đôi khi cũng dễ dàng và tường minh như ví dụ trên. Tuy nhiên hãy cùng mình đi tiếp các kiến thức tiếp theo của chuỗi bài này, để hiểu rõ hơn về Generic. Chắc chắn trong sự nghiệp code Java của bạn, bạn sẽ phải cần đủ lượng kiến thức để mà còn giải quyết các lỗi lạ lùng khi sử dụng đến sự kết hợp của nhiều Generic trong code. Cuối cùng bạn cũng phải tự viết ra các phương thức hay đối tượng có sử dụng Generic của riêng bạn nữa chứ.

Tại Sao Lại Cần Generic?

Có thể các ý trên đây đã nói rõ về mục đích cần đến Generic. Nhưng đó là ý đó nói vậy, chứ bạn vẫn chưa thấy được sự cần thiết của kiểu dữ liệu chung này. Vậy hãy cùng mình đi đến ví dụ rất đơn giản như sau.

Giả sử bạn có yêu cầu phải viết một phương thức: tìm số nhỏ nhất từ hai số truyền vào. Rất dễ đúng không nào. Nhanh chóng thôi, bạn đã viết xong phương thức đó có tên minFromTwoNumbers() như sau.

public static int minFromTwoNumbers(int one, int two) {
    if (one < two) {
        return one;
    } else {
        return two;
    }
}

public static void main(String[] args) {
    int one = 7;
    int two = 3;
    System.out.println("Số nhỏ nhất giữa " + one + " và " + two + " là " + minFromTwoNumbers(one, two));
}

// Kết quả
// Số nhỏ nhất giữa 7 và 3 là 3

Nhìn có vẻ mọi thứ ổn thỏa. Tuy nhiên, ngay khi áp dụng minFromTwoNumbers() của bạn vào thực tế, bạn mới nhận ra rằng thực ra phương thức của bạn chỉ giúp tìm số nhỏ nhất giữa hai số nguyên thôi, với hai số kiểu float như sau thì ứng dụng lại báo lỗi không thực thi.

float oneF = 3.5f;
float twoF = 5.7f;
System.out.println("Số nhỏ nhất giữa " + oneF + " và " + twoF + " là " + minFromTwoNumbers(oneF, twoF));

Ôi có gì đâu mà. Bạn đã biết rằng các phương thức trong một lớp có thể khai báo theo kiểu nạp chồng. Do đó bạn hoàn toàn có thể xây dựng các phương thức có cùng tên là minFromTwoNumbers() nhưng khác kiểu tham số truyền vào là xong chứ gì.

public static int minFromTwoNumbers(int one, int two) {
    if (one < two) {
        return one;
    } else {
        return two;
    }
}

public static float minFromTwoNumbers(float one, float two) {
    if (one < two) {
        return one;
    } else {
        return two;
    }
}

public static void main(String[] args) {
    int one = 7;
    int two = 3;
    System.out.println("Số nhỏ nhất giữa " + one + " và " + two + " là " + minFromTwoNumbers(one, two));

    float oneF = 3.5f;
    float twoF = 5.7f;
    System.out.println("Số nhỏ nhất giữa " + oneF + " và " + twoF + " là " + minFromTwoNumbers(oneF, twoF));
}

// Kết quả
// Số nhỏ nhất giữa 7 và 3 là 3
// Số nhỏ nhất giữa 3.5 và 5.7 là 3.5

Cơ mà thấy có gì đó dư thừa code ở đây. Mặc dù ứng dụng chạy tốt, nhưng việc viết lặp lại hai phương thức minFromTwoNumbers() khiến ứng dụng phình ra một cách không đáng có. Những lập trình viên có kinh nghiệm sẽ tránh việc lặp lại này. Vả lại ứng dụng của chúng ta vẫn còn lỗi khi mà nhu cầu thực tế muốn tìm số nhỏ nhất giữa hai số double thì sao. Chẳng lẽ chúng ta phải nạp chồng tất cả các kiểu số? Điều đó làm tăng sự lặp lại. Vả lại việc xây dựng nhiều phương thức nạp chồng như thế này còn có thể gây ra những lỗi tiềm ẩn khác khi chúng ta có nhu cầu chỉnh sửa chúng sau này. Hãy cùng mình giải quyết bài toán này với hai cách như sau, sẽ giúp bạn có cái nhìn rõ ràng ban đầu về Generic.

Cách Giải Quyết Không Dùng Generic

Sau một thời gian tra cứu Google, và cũng tham khảo nhiều ở blog của mình. Bạn nhận ra rằng bạn có thể chỉ cần đến duy nhất một phương thức minFromTwoNumbers() mà thôi. Khi đó đầu vào cho phương thức này phải là một lớp đại diện cho tất cả các kiểu dữ liệu. Uhm… lớp đại diện, có rồi, chính là lớp Object. Bạn tiến hành chỉnh sửa minFromTwoNumbers() như sau.

public static Object minFromTwoNumbers(Object objOne, Object objTwo) {
    // Chuyển 2 object objOne và objTwo về 2 số có thể so sánh được
    double one = ((Number) objOne).doubleValue();
    double two = ((Number) objOne).doubleValue();

    if (one < two) {
        return objOne;
    } else {
        return objTwo;
    }
}

public static void main(String[] args) {
    Integer one = 7;
    Integer two = 3;
    System.out.println("Số nhỏ nhất giữa " + one + " và " + two + " là " + minFromTwoNumbers(one, two));

    Float oneF = 3.5f;
    Float twoF = 5.7f;
    System.out.println("Số nhỏ nhất giữa " + oneF + " và " + twoF + " là " + minFromTwoNumbers(oneF, twoF));

    Double oneD = 3.4567;
    Double twoD = 3.45577;
    System.out.println("Số nhỏ nhất giữa " + oneD + " và " + twoD + " là " + minFromTwoNumbers(oneD, twoD));
}

// Kết quả
// Số nhỏ nhất giữa 7 và 3 là 3
// Số nhỏ nhất giữa 3.5 và 5.7 là 5.7
// Số nhỏ nhất giữa 3.4567 và 3.45577 là 3.45577

Khi này bạn không cần để ý đến kiểu dữ liệu int hay float hay bất kỳ kiểu số nào cho tham số đầu vào của minFromTwoNumbers() nữa. Mà bạn sử dụng luôn hai Object. Có điều để có thể so sánh được hai giá trị kiểu Object này, bạn sẽ phải tìm cách ép kiểu chúng về các số có thể so sánh được. Trong trường hợp này để tránh mất mát dữ liệu, bạn dùng kiểu double cho chắc. Nhưng muốn về được kiểu double nguyên thủy thì bạn lại phải thông qua việc ép kiểu Object về Number, rồi từ Number mới unboxing về lại kiểu nguyên thủy double.

Number là một lớp abstract. Nó là cha của các lớp biểu diễn số quen thuộc như Byte, Short, Integer, Long, Float, Double,…. Rất hữu ích khi cần đến Number để chuyển đổi các giá trị nguyên thủy tương ứng như byte, short, int, long, float, double,… như một ví dụ mà chúng ta vừa thấy trên đây.

Hiểu thêm về Number

Tuy hơi lằng nhằng xíu nhưng đáng giá đúng không nào. Khi này bạn chỉ cần duy nhất một minFromTwoNumbers() cho tất cả các kiểu dữ liệu số. Tuy nhiên do đầu vào là các Object, do đó khi sử dụng ở main(), tốt hơn chúng ta sẽ dùng các lớp wrapper cho từng loại dữ liệu, như Integer, Float hay Double.

Cách tiếp cận đến lớp Object cho ví dụ trên đây để tránh việc ràng buộc vào một kiểu dữ liệu nào đó, người ta gọi là cách tiếp cận theo kiểu kế thừa kiểu dữ liệu. Cách tiếp cận này được sử dụng trước khi Generic được hỗ trợ bởi Java. Tuy cách này cũng khá hiệu quả đấy, nhưng nó tiềm ẩn nhiều rủi ro cho chúng ta. Chúng ta cùng điểm qua một vài rủi ro có thể trông thấy được như sau.

Thứ nhất, bạn cũng dễ dàng thấy rằng do tham số đầu vào của phương thức là một Object, nó sẽ nhận tất cả các kiểu dữ liệu mà bạn truyền vào. Bạn muốn rằng chương trình sẽ kiểm tra hai giá trị số, nhưng trong thực tế nếu hai số đó là kiểu String như sau thì sao.

String oneS = "1";
String twoS = "2";
System.out.println("Số nhỏ nhất giữa " + oneS + " và " + twoS + " là " + minFromTwoNumbers(oneS, twoS));

Với code trên đây trình biên dịch không báo lỗi hay cảnh báo gì cả. Cũng đúng thôi vì nó nghĩ bạn đang truyền đúng kiểu dữ liệu đầu vào, String cũng là một Object mà thôi (tính đa hình). Nhưng khi thực thi chương trình, lỗi sẽ tung ra và ứng dụng của bạn sẽ chết. Như vậy rủi ro đầu tiên này là: rất khó để chương trình của chúng ta kiểm soát đầu vào vì bạn đang sử dụng kiểu Object, là bất kỳ kiểu dữ liệu nào.

Thứ hai, bởi vì đầu vào là một Object, bạn sẽ cần ở đâu đó việc ép kiểu các kiểu dữ liệu này về các kiểu dữ liệu mong muốn, việc ép kiểu sẽ gây ra việc crash ứng dụng nếu chúng ta không cẩn thận. Bạn có thể thêm try catch hoặc câu lệnh kiểm tra điều kiện các giá trị đầu vào, nhưng như vậy cũng khá là tốn công.

Hãy xem cách thức sử dụng Generic như sau.

Cách Giải Quyết Với Generic

Nào bạn hãy cùng so sánh nhé. Chưa cần biết nhiều về Generic nhưng hãy xem trước cách sử dụng chúng thông qua thay đổi code cho minFromTwoNumbers() như sau.

public static <T extends Comparable> T minFromTwoNumbers(T one, T two) {
    if (one.compareTo(two) < 0) {
        return one;
    } else {
        return two;
    }
}

public static void main(String[] args) {
    Integer one = 7;
    Integer two = 3;
    System.out.println("Số nhỏ nhất giữa " + one + " và " + two + " là " + minFromTwoNumbers(one, two));

    Float oneF = 3.5f;
    Float twoF = 5.7f;
    System.out.println("Số nhỏ nhất giữa " + oneF + " và " + twoF + " là " + minFromTwoNumbers(oneF, twoF));

    Double oneD = 3.4567;
    Double twoD = 3.45577;
    System.out.println("Số nhỏ nhất giữa " + oneD + " và " + twoD + " là " + minFromTwoNumbers(oneD, twoD));

    String oneS = "1";
    String twoS = "2";
    System.out.println("Số nhỏ nhất giữa " + oneS + " và " + twoS + " là " + minFromTwoNumbers(oneS, twoS));
}

// Kết quả
// Số nhỏ nhất giữa 7 và 3 là 3
// Số nhỏ nhất giữa 3.5 và 5.7 là 3.5
// Số nhỏ nhất giữa 3.4567 và 3.45577 là 3.45577
// Số nhỏ nhất giữa 1 và 2 là 1

Có thể bạn nhìn chưa quen, nhưng bạn có thể đoán ngay được T đã thay thế cho Object. T là một kiểu dữ liệu chung nào đó mà không phải Object. Bạn thấy rằng khi này T không “mơ hồ” như Object, mà nó là một kiểu dữ liệu nào đó extends từ Comparable. Như vậy T thể hiện một sự cụ thể và rõ ràng hơn so với Object. Với kiểu khai báo “sơ” về T như vậy, hệ thống sẽ biết T là một kiểu Comparable, các kiểu số hay String đều implement từ Comparable cả. Do đó nó giúp ràng buộc đầu vào cho phương thức, chẳng hạn bạn không thể truyền lớp Circle mà ở các bài học trước bạn tự tạo ra được rồi đó, vì Circle không implement Comparable. Điều này giúp tránh xảy ra crash ứng dụng như với cách dùng Object. Hơn nữa với việc hiểu T là kiểu dữ liệu gì, thì khi vào thân hàm, chúng ta không cần đến ép kiểu dài dòng nữa, mà dùng luôn phương thức compareTo() có sẵn ở Comparable, hệ thống tự hiểu và cho phép bạn làm điều này.

Bạn có thấy là khi dùng sang Generic như thế này, bạn hoàn toàn có thể so sánh cả hai chuỗi oneStwoS luôn rồi đấy.

Kết Luận

Bài học hôm nay mở ra cho các bạn một khái niệm ban đầu về Generic. Giúp bạn giải đáp câu hỏi quan trọng: generic là gì và tại sao phải dùng Generic. Về sau khi bạn làm quen với các kiểu dữ liệu Collection, bạn sẽ thấy rõ hơn tầm quan trọng của Generic này.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:
– Đánh giá 5 sao ở mỗi bài viết nếu thấy thích.
– Comment bên dưới mỗi bài viết nếu có thắc mắc.
– Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
– Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.
– Ủng hộ blog theo hướng dẫn ở thanh bên phải để blog ngày càng phát triển hơn.

2 comments

  1. Lâu quá mới thấy đăng bài

    Rating: 5.0/5. From 1 vote.
    Please wait...
  2. Nghe có vẻ sắp tới sẽ co những bài khá thú vị và chuyên sâu về java hóng quá

    Rating: 5.0/5. From 1 vote.
    Please wait...

Leave a Reply