Java: Khả năng tương tác với Generics

Tổng quan

Một đoạn mã hiện có có thể được sửa đổi để sử dụng Generics mà không cần thực hiện tất cả các thay đổi cùng một lúc. Thiết kế của Generics đảm bảo rằng các thư viện Java mới vẫn có thể được sử dụng để biên dịch mã cũ. Nói cách khác, cùng một đoạn mã sẽ hoạt động với cả phiên bản cũ và phiên bản generic của thư viện. Điều này được gọi là khả năng tương thích nền tảng. Khả năng tương thích này cung cấp cho lập trình viên sự tự do để chuyển từ phiên bản cũ sang phiên bản generic bất cứ khi nào được yêu cầu.

Trong Java, tính generic đảm bảo rằng cùng một tệp lớp được tạo bởi cả phiên bản kế thừa và generic với một số thông tin bổ sung về kiểu. Điều này được gọi là khả năng tương thích nhị phân vì tệp lớp kế thừa có thể được thay thế bằng tệp lớp generic mà không cần biên dịch lại. Hình sau thể hiện sự thích ứng giữa mã kế thừa và mã mới.

Mã kế thừa

Mã kế thừa

Ví dụ sau thể hiện việc sử dụng mã kế thừa với ứng dụng khách kế thừa.

import

java.util.ArrayList;

import

java.util.List;

interface

NumStack

{

public boolean

empty

();

public void

push

(

Object

elt);

public

Object

retrieve

(); }

class

NumArrayStack

implements

NumStack

{

private

List

listObj

;

public

NumArrayStack

() {

listObj

=

new

ArrayList(); }

@Override

public

Object

retrieve

() {

Object value

=

listObj

.remove(

listObj

.size() -

1

);

return

value

; }

@Override

public

String

toString

() {

return

"stack"

+

listObj

.toString(); } }

public class

Client

{

public static void

main

(

String

[] args) {

NumStack stackObj

=

new

NumArrayStack();

for

(

int

ctr =

0

; ctr <

4

; ctr++) {

stackObj

.push(

new

Integer(ctr)); }

assert

stackObj

.toString().equals(

"stack[0, 1, 2, 3]"

);

int

top

= ((

Integer

)

stackObj

.retrieve()).intValue();

System

.out.println(

"Value is : "

+

top

); } }

Trong ví dụ trên dữ liệu mã được thêm vào một ngăn xếp và được truy xuất từ ​​ngăn xếp. Điều này sẽ biên dịch tốt với phiên bản JDK 1.4.

Lưu ý – Rất khó để viết một chương trình hữu ích hoàn toàn độc lập với nền tảng làm việc của nó.  Do đó, khi nền tảng được nâng cấp, đoạn mã trước đó sẽ trở thành mã kế thừa và mã sẽ không hoạt động nữa.

Thư viện Generic với Máy khách kế thừa

Trong mã generic, các lớp được kèm theo một tham số kiểu. Khi một kiểu generic như tập hợp được sử dụng mà không có tham số kiểu, nó được gọi là kiểu thô (raw type). Trong ví dụ trước, NumStack<E> được tham số hóa tương ứng với kiểu thô NumStack và NumArrayStack<E> được tham số hóa tương ứng với kiểu thô NumArrayStack.

Một giá trị của kiểu được tham số hóa có thể được chuyển cho kiểu thô vì kiểu được tham số hóa là một kiểu con của kiểu thô. Java tạo ra một cảnh báo chuyển đổi không được kiểm tra khi một giá trị của kiểu thô được chuyển vào nơi mà kiểu tham số được mong đợi. Trong cùng một ví dụ, một giá trị kiểu NumStack<E> có thể được gán cho một biến kiểu NumStack. Nhưng khi thực hiện ngược lại, một cảnh báo chuyển đổi không được chọn sẽ hiển thị. Hình dưới đây hiển thị thư viện generic với máy khách kế thừa.

Thư viện chung với máy khách kế thừa

Ví dụ sau thể hiện việc sử dụng thư viện generic với ứng dụng khách kế thừa.

import

java.util.*;

interface

NumStack

{

public boolean

empty

();

public void

push

(

Object

elt);

public

Object

retrieve

(); }

class

NumArrayStack

implements

NumStack

{

private

List

listObj

;

public

NumArrayStack

() {

listObj

=

new

ArrayList(); }

public boolean

empty

() {

return

listObj

.size() ==

0

; }

public void

push

(

Object

obj) {

listObj

.add(obj); }

public

Object

retrieve

() {

Object value

=

listObj

.remove(

listObj

.size() -

1

);

return

value

; }

public

String

toString

() {

return

"stack"

+

listObj

.toString(); } }

class

Client

{

public static void

main

(

String

[] args) {

NumStack stackObj

=

new

NumArrayStack();

for

(

int

ctr =

0

; ctr <

4

; ctr++) {

stackObj

.push(

new

Integer(ctr)); }

assert

stackObj

.toString().equals(

"stack[0, 1, 2, 3]"

);

int

top

= ((

Integer

)

stackObj

.retrieve()).intValue();

System

.out.println(

"Value is : "

+

top

); } }

Khi đoạn mã trên được biên dịch, một cảnh báo chuyển đổi không được chọn được hiển thị như sau:

Note – Client.java uses unchecked or unsafe operation

Note – Recompile with -Xlint:unchecked for details.

Nếu đoạn mã được biên dịch bằng cách sử dụng switch như được đề xuất thì thông báo sau sẽ xuất hiện:

Client.java:

21

: warning: [unchecked] unchecked call to add(E) as a member of the raw type java.util.List listObj.add(obj); ^

1

warning

Cảnh báo là do việc sử dụng thêm phương thức generic trong truy xuất phương thức kế thừa. Có thể tắt cảnh báo bằng cách sử dụng switch –source 1.4 như sau:

javac -source 1.4 Client.java

 

Lưu ý – Cảnh báo chuyển đổi không được chọn có nghĩa là không thể cung cấp cùng một đảm bảo an toàn được cấp cho mã generic trong khi biên dịch.

Erasure

Khi bạn chèn một số nguyên vào một danh sách và cố gắng trích xuất một Chuỗi thì điều đó là sai. Nếu bạn trích xuất một phần tử từ danh sách và bằng cách truyền phần tử đó thành String nếu bạn cố gắng coi đó là chuỗi, bạn sẽ nhận được ClassCastException. Lý do là Generics được trình biên dịch Java triển khai dưới dạng chuyển đổi giao diện người dùng được gọi là erasure. Erasure xóa tất cả thông tin kiểu generic. Tất cả thông tin kiểu giữa các dấu ngoặc nhọn sẽ bị loại bỏ, do đó, một kiểu được tham số hóa như List<String> chẳng hạn sẽ được chuyển đổi thành List. Loại erasure duy trì khả năng tương thích với các thư viện và ứng dụng Java được tạo trước khi sử dụng generic. Hình sau thể hiện erasure.

Erasure

Generics trong mã kế thừa

Đôi khi có thể có yêu cầu cập nhật thư viện không phải ngay lập tức mà trong một khoảng thời gian. Trong những trường hợp như vậy, signature của phương thức sẽ thay đổi và sẽ bao gồm các tham số kiểu. Phần thân của phương thức sẽ không thay đổi. Thay đổi này trong signature của phương thức có thể được thực hiện bằng cách thực hiện các thay đổi tối thiểu trong phương thức, hoặc bằng cách tạo stub hoặc bằng cách sử dụng wrapper. Những thay đổi tối thiểu phải được kết hợp là:

  • Thêm tham số kiểu vào khai báo class hoặc interface
  • Thêm tham số kiểu vào class hoặc interface đã được mở rộng hoặc triển khai
  • Thêm tham số kiểu vào signature của phương thức
  • Thêm ép kiểu trong đó kiểu trả về chứa tham số kiểu.

Ví dụ sau thể hiện việc sử dụng generic.

import

java.util.*;

interface

NumStack

<

E

> {

public boolean

empty

();

public void

push

(

E

elt);

public

E

retrieve

(); }

@SuppressWarnings

(

"unchecked"

)

class

NumArrayStack

<

E

>

implements

NumStack

<

E

> {

private

List

listObj

;

public

NumArrayStack

() {

listObj

=

new

ArrayList(); }

public boolean

empty

() {

return

listObj

.size() ==

0

; }

public void

push

(

E

obj) {

listObj

.add(obj); }

public

E

retrieve

() {

Object value

=

listObj

.remove(

listObj

.size() -

1

);

return

(

E

)

value

; }

public

String

toString

() {

return

"stack"

+

listObj

.toString(); } }

class

ClientLegacy

{

public static void

main

(

String

[] args) {

NumStack stackObj

=

new

NumArrayStack();

for

(

int

ctr =

0

; ctr <

4

; ctr++) {

stackObj

.push(

new

Integer(ctr)); }

assert

stackObj

.toString().equals(

"stack[0, 1, 2, 3]"

);

int

top

= ((

Integer

)

stackObj

.retrieve()).intValue();

System

.out.println(

"Value is : "

+

top

);

System

.out.println(

"Stack contains : "

+

stackObj

.toString()); } }

Đoạn mã trên tạo ra một cảnh báo không được kiểm tra.

Sử dụng Generics trong Mã kế thừa

Một thư viện generic nên được tạo khi có quyền truy cập vào mã nguồn. Cập nhật toàn bộ nguồn thư viện cũng như mã máy khách để loại bỏ các cảnh báo tiềm ẩn chưa được kiểm tra.

Ví dụ sau cho thấy việc sử dụng generic trong mã kế thừa.

import

java.util.*;

interface

NumStack

<

E

> {

public void

push

(

E

elt);

public

E

retrieve

(); }

class

NumArrayStack

<

E

>

implements

NumStack

<

E

> {

private

List<

E

>

listObj

;

public

NumArrayStack

() {

listObj

=

new

ArrayList<

E

>(); }

public void

push

(

E

obj) {

listObj

.add(obj); }

public

E

retrieve

() {

E

value

=

listObj

.remove(

listObj

.size() -

1

);

return

value

; }

public

String

toString

() {

return

"stack"

+

listObj

.toString(); } }

Interface và class thực thi sử dụng tham số kiểu. Tham số kiểu <E> thay thế kiểu Object từ signature phương thức push() và get() và thân phương thức. Các tham số kiểu thích hợp được thêm vào mã máy khách.

Tóm tắt kiến thức Generics

♦ Generics trong Java tạo ra một phiên bản đã biên dịch của một lớp generic.

♦ Generics giúp loại bỏ sự mâu thuẫn về kiểu trong thời gian biên dịch hơn là trong thời gian chạy.

♦ Có ba loại ký tự đại diện được sử dụng với Generics gồm “? extends Type”, “? super Type” và “?”.

♦ Các phương thức generic được định nghĩa cho một phương thức cụ thể và có cùng chức năng như tham số kiểu có cho các lớp generic.

♦ Các tham số kiểu được khai báo bên trong phương thức và chữ ký của hàm tạo khi tạo các phương thức và hàm tạo generic.

♦ Có thể gọi một khai báo phương thức generic duy nhất với các đối số thuộc các kiểu khác nhau.

♦ Suy luận kiểu cho phép trình biên dịch Java xác định các đối số kiểu làm cho lời gọi có thể áp dụng được.