Iterator cho phép lấy các phần tử trong collection từ đầu đến hết mà không cần quan tâm tới chỉ số và thứ tự của chúng.. Interface RandomAccess cho biết ta có thể truy cập đến một phần
Trang 1Java Collections Framework
Java Collections Framework 1
1 Giới thiệu chung 1
2 List 2
3 Các interface Iterable, Iterator và Enumerator 9
4 Set 11
5 Map 14
1 Giới thiệu chung
Array là cách nhanh và dễ nhất để lưu trữ các loại dữ liệu dạng mảng Tuy nhiên, array có một nhược điểm là kích thước cố định Dĩ nhiên là ta có thể "thay đổi kích thước" của array bằng cách tạo ra một array mới rồi sao chép dữ liệu qua đó, nhưng việc này sẽ tốn thời gian và công sức
Để khắc phục nhược điểm trên của array, Java đã xây dựng một dạng dữ liệu gọi chung là collection, được đặt trong package java.util
Các đối tượng collection có khả năng lưu trữ dữ liệu dạng mảng như array, ngoài ra nó còn cho phép thêm và xóa các phần tử một cách tùy ý Dĩ nhiên là khi đó collection sẽ ko thể chạy nhanh và dễ dùng như array
Trong Java, collection được cài đặt trong Collections Framework Collections Framework bao gồm hai phần chính là collection và map Việc mô tả các chức năng chung của các collection được quy định trong interface Collection, Collection lại được chia thành hai loại chính là List và Set, ngoài ra còn vài interface phụ khác Còn các map được đại diện bởi interface Map
Dưới đây là sơ đồ quan hệ của Java Collections Framework (chỉ có một số interface và class chính)
Trang 2Hình 1: Collections Framework
2 List
List là một bộ phận của Java Collections, list được đại diện bởi interface List Interface
List được kế thừa từ interface Collection Dưới List là lớp AbstractList, dưới nữa
là các lớp Vector, AbstractSequentalList, ArrayList, LinkedList, Stack,
Ở đây xin giới thiệu về Vector, các class khác cũng có cách dùng và tính năng tương tự
HashSet
AbstractCollection AbstractSet
WeakHashMap
TreeSet
ArrayList
AbstractList
List
Vector AbstractSequentalList
AbstractMap
HashMap
TreeMap
HashTable
Dictionary
Properties
Trang 32.1 Lớp Vector
2.1.1 Giới thiệu
Vector là một collection dạng List, được tạo ra bằng cách extend class AbstractList,
AbstractList lại được extend từ AbstractCollection Ngoài ra Vector còn implement từ các interface Serializable, Cloneable, Iterable, List, RandomAccess
và dĩ nhiên không thể thiếu interface Collection
Class AbstractCollection là một lớp ảo, trong đó khai báo các thuộc tính và phương thức cơ bản cho các đối tượng dạng collection
Class AbstractList là một lớp ảo, trong đó khai báo các thuộc tính và phương thức cơ bản cho các đối tượng dạng list (bao gồm mảng, danh sách liên kết,…) để phân biệt với các đối tượng dạng set (tập hợp)
Interface Serializable cho biết đối tượng Vector có khả năng biến đổi thành một dãy byte dữ liệu (VD: để ghi vào file) và phục hồi từ dãy byte dữ liệu (VD:
để đọc từ file) Chi tiết về object serialization sẽ được trình bày trong một phần riêng
Interface Cloneable cho biết đối tượng Vector có thể tạo ra bản sao của nó thông qua phương thức Object clone()
Interface Iterable cho biết đối tượng Vector có thể tạo ra đối tượng Iterator Iterator cho phép lấy các phần tử trong collection từ đầu đến hết mà không cần quan tâm tới chỉ số và thứ tự của chúng
Interface List cho biết Vector lưu các phần tử một cách có thứ tự (sequence) và cho phép tìm kiếm các phần tử List cũng nói rằng Vector cho phép lưu những phần tử trùng nhau List dùng để phân biệt với Set (tập hợp)
Interface RandomAccess cho biết ta có thể truy cập đến một phần tử bất kỳ trong
Vector thông qua chỉ số (index), và việc truy cập này không tốn thời gian xử lý (chính xác hơn là thời gian xử lý không đáng kể và như nhau với mọi vị trí) Điều này cho thấy Vector không phải là danh sách liên kết mà là sự phát triển của array Trong phần sau, các constructor của Vector sẽ cho ta thấy rõ điều này
Interface Collection cho biết Vector có khả năng chứa đựng dữ liệu dạng mảng
và cho phép thêm/xóa các phần tử
Trang 4Hình 2 Vị trí của Vector trong Java Collections Framework
Ngoài Vector, ta còn có thể dùng ArrayList Nhìn chung ArrayList tương tự như
Vector, ngoại trừ việc nó không có tính năng đồng bộ (synchronize - bảo đảm tính nhất quán dữ liệu khi chạy trong ứng dụng đa tiểu trình)
2.1.2 Các constructor
Trước khi đi vào các constructor, ta cần điểm qua các thuộc tính quan trọng của Vector,
đó là initialCapacity, capacityIncrement và size
initialCapacity: là sức chứa khởi đầu của Vector Ví dụ nếu
initialCapacity = 10 thì Vector được tạo ra sẽ có khả năng chứa 10 phần tử Nếu trong quá trình hoạt động, số phần tử thêm vào vượt quá sức chứa (capacity) thì Vector sẽ tự gia tăng sức chứa
capacityIncrement: là độ gia tăng sức chứa của Vector khi số phần tử thêm vào vượt quá sức chứa hiện tại Quy ước: Nếu độ tăng nhỏ hơn 1 có nghĩa là sức chứa sẽ tăng gấp đôi mỗi khi cần thiết
size: là số phần tử hiện có trong Vector Do đó, size không thể vượt quá sức chứa (capacity)
Iterable
Collection
List
AbstractCollection
AbstractList
Vector
Stack
Serializable Cloneable RandomAccess
Trang 5Bảng dưới đây là các constructor của Vector.
Vector()
Đây là constructor default Constructor này sẽ tạo
ra một đối tượng Vector với sức chứa mặc định là
10, độ tăng sức chứa là 0 (nghĩa là tăng gấp đôi khi cần tăng)
Vector(int initialCapacity)
Constructor này tạo ra một Vector với sức chứa khởi đầu là initialCapacity, độ tăng sức chứa là
0 (nghĩa là tăng gấp đôi khi cần tăng)
Vector(int initialCapacity,
int capacityIncrement)
Constructor này tạo ra một Vector với sức chứa khởi đầu là initialCapacity, độ tăng sức chứa là
capacityIncrement (nếu capacityIncrement nhỏ hơn 1 thì Vector sẽ là tăng gấp đôi sức chứa khi cần tăng)
Vector(Collection c) Constructor này tạo ra một Vector với các phần tử
có sẵn trong Collection c
Bảng 1 Các constructor của lớp Vector
2.1.3 Các phương thức chính
Bảng dưới đây là các phương thức thường dùng của Vector Chi tiết xin xem thêm trong Java API Documents
Object get(int index) Trả về object ở vị trí index (bắt đầu từ 0)
void set(int index, Object e) Thay thế về object ở vị trí index bởi e
void add(Object e) Chèn phần tử e vào cuối Vector
void add(int index, Object e) Chèn phần tử e vào vị trí index
Object lastElement() Trả về phần từ cuối cùng của Vector
Object remove(int index) Xóa và trả về phần tử tại vị trí index
int size()
Trả về số phần tử đang có trong Vector
Lưu ý: cần phân biệt số phần tử (size) với sức chứa (capacity)
boolean remove(Object e) Xóa handle quản lý đối tượng e trong Vector
Nếu trong Vector có nhiều handle cùng quản lý
e thì chỉ có handle đầu tiên bị xóa Nếu trong Vector không có handle nào quản lý e thì sẽ trả
Trang 6về false, khi đó Vector sẽ không bị thay đổi gì.
Enumeration elements() Trả về một đối tượng Enumeration chứa các
phần tử trong Vector
Iterator iterator() Trả về một đối tượng Iterator chứa các phần tử
trong Vector
Bảng 2 Các phương thức thường dùng của lớp Vector
2.2 Vector hỗn tạp
Như đã nói trên, do Object là lớp "cha" của mọi lớp khác trong Java, nên nếu ta tạo ra một mảng các Object (thực chất là mảng các handle kiểu Object) thì mảng đó sẽ có khả năng chứa mọi loại đối tượng (thông qua up-cast)
Trong collection, các phần tử của chúng cũng đều là các handle kiểu Object Nên collection (cụ thể là Vector) cũng có khả năng chứa mọi loại đối tượng
Khi một mảng (hoặc collection) chứa nhiều đối tượng không cùng loại (nghĩa là không cùng lớp) ta nói đó là mảng (hoặc collection) hỗn tạp Khi đó phải thật cẩn thận khi sử dụng để tránh lỗi down-cast
Cách xửlý và kiểm tra với mảng hỗn tạp và collection hỗn tạp là tương tự nhau: upcast khi thêm phần tử mới và down-cast khi dùng phần tử, kèm theo đó là kiểm tra tính hợp lệ của down-cast
2.2.1 Ví dụ về Vector hỗn tạp
Trong ví dụ này, ta tạo ra Vector V chứa phân số (lớp PhanSo) và học sinh (lớp HocSinh) PhanSo và HocSinh là hai lớp độc lập (không extend nhau)
Vector V=new Vector();
HocSinh hs=new HocSinh();
hs.Nhap();
V.add(new PhanSo()); // Up-cast PhanSo-Object
((PhanSo)V.lastElement()).Nhap(); // Down-cast Object-PhanSo
V.add(new HocSinh()); // Up-cast HocSinh-Object
((HocSinh)V.lastElement()).Nhap(); // Down-cast Object-HocSinh
Như vậy, Vector V sẽ chứa 3 handle kiểu Object, trong đó có 2 handle quản lý HocSinh
và 1 handle quản lý PhanSo
2.2.2 Lỗi down-cast
Đoạn code này được thực thi ngay sau đoạn code tạo Vector ở trên Nó sẽ gây ra lỗi ép kiểu khi down-cast
/* !!! Đoạn code gây lỗi down-cast !!! */
for(int i=0;i<V.size();i++)
((HocSinh)V.get(i)).Xuat();
Trang 7Ban đầu, ta thêm vào V hai đối tượng HocSinh và một PhanSo Đối tượng PhanSo là do handle thứ 1 (tính từ 0) quản lý
Trong đoạn code lỗi, ta ép kiểu tất cả các handle của V từ Object về HocSinh và gọi phương thức xuất Do đó, handle thứ 1 là handle quản lý PhanSo cũng sẽ bị ép về
HocSinh, đây là nguyên nhân gây lỗi down-cast
Lưu ý: đôi khi trong Vector hỗn tạp, ta có thể gọi thực thi các phương thức của từng phần tử một cách "vô tư" mà không hề bị lỗi down-cast Xem ví dụ sau:
/* !!! Dùng Vector hỗn tạp mà ko bị lỗi down-cast !!! */
for(int i=0;i<V.size();i++)
System.out.println(V.get(i).toString());
Lý do: phương thức toString() đã có từ lớp Object nên việc gọi tới nó mà không cần
ép kiểu là hoàn toàn hợp lệ
2.2.3 Từ khóa instanceof
Để tránh lỗi down-cast khi dùng mảng hỗn tạp, ta cần kiểm tra kiểu của đối tượng do handle quản lý (bằng từ khóa instanceof) rồi mới down-cast
for(int i=0;i<V.size();i++)
{
if (V.get(i) instanceof HocSinh) // Kiểm tra kiểu của V[i]
((HocSinh)V.get(i)).Xuat(); // Down-cast Object-HocSinh
else
if (V.get(i) instanceof PhanSo) // Kiểm tra kiểu của V[i]
((PhanSo)V.get(i)).Xuat(); // Down-cast Object-PhanSo
System.out.println();
}
2.3 Vector định kiểu
2.3.1 Bao bọc
Như đã nêu trên, Vector có khả năng lưu trữ mọi loại đối tượng Trong hoàn cảnh cụ thể,
để tránh rắc rồi về xác định kiểu của các handle trong Vector, ta có thể tạo ra một
"MyVector" của riêng mình với kiểu phần tử cố định và duy nhất Việc này có thể được thực hiện bằng cách tạo ra một đối tượng mới bao bọc đối tượng Vector
Ví dụ, để tạo ra một đối tượng chỉ cho phép lưu trữ các HocSinh, ta có thể làm như sau:
// Lớp MangHocSinh tương tự như Vector nhưng chỉ cho phép lưu trữ và xử // lý cho các đối tượng kiểu HocSinh
public class MangHocSinh
{
// MangHocSinh bao bọc một đối tượng Vector
private Vector lop;
public MangHocSinh()
{
lop=new Vector();
}
Trang 8/* Cài đặt lại các phương thức lấy dữ liệu */
void ThemHocSinh(HocSinh hs)
{
// Up-cast HocSinh-Object
lop.add(hs);
}
HocSinh LayHocSinh(int i)
{
// Down-cast Object-HocSinh: luôn luôn OK
return (HocSinh)lop.elementAt(i);
}
HocSinh HocSinhCuoiCung()
{
// Down-cast Object-HocSinh: luôn luôn OK
return (HocSinh)lop.lastElement();
}
public int SoHocSinh()
{
return lop.size();
}
public void XoaHocSinh(int i)
{
lop.removeElementAt(i);
}
}
Khi đó, MangHocSinh sẽ được dùng để lưu trữ HocSinh một cách dễ dàng và an toàn
// Tạo mảng học sinh
MangHocSinh mhs=new MangHocSinh();
// Nhập mảng học sinh
for(int i=0;i<3;i++)
{
// Không thể add gì khác ngoài HocSinh vào "Vector" này
mhs.ThemHocSinh(new HocSinh());
// Không cần lo lỗi down-cast nữa
mhs.HocSinhCuoiCung().Nhap();
}
// Xuất mảng học sinh
for(int i=0;i<mhs.SoHocSinh();i++)
{
// Không cần lo lỗi down-cast nữa
mhs.LayHocSinh(i).Xuat();
System.out.println();
}
2.3.2 Generics
Khi làm việc với các Collection, để xử lý trên dữ liệu mà kiểu là chưa biết trước, ta có thể dùng lớp Object và gọi ép kiểu (down-cast, up-cast) khi cần Việc này khiến chương trình sẽ rối rắm và dễ xảy ra lỗi (ClassCastException) Để khắc phục vấn đề này, từ JDK 5, Java bổ sung một tính năng mới gọi là Generics
Với Generics ta không cần phải ép kiểu và kiểm tra kiểu nữa
Ví dụ:
Trang 9// Khai báo một Vector chứa các HocSinh
Vector<HocSinh>v=new Vector();
// Chèn vào một HocSinh
v.insert(new HocSinh());
// Chèn vào một PhanSo không cho phép
// v.insert(new PhanSo()); // Dòng lệnh này sẽ bị lỗi
// Nhập dữ liệu cho học sinh vừa chèn (không cần ép kiểu)
v.getLastElement().Nhap();
2.4 Stack
Lớp này kế thừa trực tiếp từ lớp Vector, dùng khi cần truy cập các phần tử theo dạng LIFO ("last in first out" hay "vào sau ra trước")
Các phương thức chính:
Object pop() Lấy phần tử ở đỉnh ra khỏi stack
void push(Object o) Thêm một phần tử vào stack
Object peek() Xem giá trị của phần tử đỉnh của stack mà
không lấy nó ra khỏi stack
Bảng 3 Các phương thức chính của lớp Stack
3 Các interface Iterable, Iterator và Enumerator
3.1 Giới thiệu
Iterable không phải là một bộ phận của Collections Framework, nhưng việc sử dụng nó thường gắn liền với các đối tượng collection
Iterable quy định khả năng duyệt qua các phần tử của collection một cách tuần tự mà không quan tâm tới chỉ số lẫn vị trí của chúng bằng Iterator hoặc Enumeration Điều này giúp việc truy cập trở nên nhanh chóng (đối với các collection dạng danh sách liên kết, so với truy cập qua chỉ số) và tự nhiên hơn
Nhìn chung, Enumeration và Iterator là tương tự nhau Ban đầu, JDK chỉ có
Enumeration Iterator được bổ sung vào sau này và là sự cải tiến của Enumeration:
Enumeration chỉ cho phép lấy phần tử và xử lý, còn Iterator cho phép xóa phần tử, ngoài ra các phương thức của Iterator được đặt lại tên cho dễ nhớ hơn
Do là interface, Enumeration và Iterator không thể được tạo ra tùy ý thông qua constructor mà bắt buộc phải được tạo ra từ một collection nào đó (như Vector chẳn hạn) Để tạo ra Enumeration, ta dùng phương thức Enumeration elements(), còn để tạo ra Iterator, ta dùng phương thức Iterator iterator() (như đã để cập trong phần trước)
Ngoài ra, kể từ JDK5, Iterable cho phép truy cập dữ liệu của collection bằng vòng lặp
"for-each" (xem trong phần sau)
Trang 103.2 Các phương thức
Enumeration
boolean hasMoreElements()
Cho biết Enumeration này còn phần tử chưa xét tới hay không Trả về true nếu còn
Object nextElement()
Trả về đối tượng kế tiếp đối tượng đang xét Nếu gọi lần đầu thì sẽ trả về phần tử đầu tiên của Enumeration
Iterator
boolean hasNext() Cho biết Iterator này còn phần tử chưa xét
tới hay không Trả về true nếu còn
Object next()
Trả về đối tượng kế tiếp đối tượng đang xét Nếu gọi lần đầu thì sẽ trả về phần tử đầu tiên của Iterator
void remove()
Xóa phần tử đang xét của Iterator Phương thức này chỉ được gọi tối đa một lần ứng với một lời gọi next
Bảng 4 Các phương thức thường dùng của Enumerator và Iterator
3.3 Enumeration vs Vector
Dưới đây ta sẽ xem một ví dụ để thấy rõ sự khác biệt khi dùng Enumeration và
Iterator so với Vector
Xuất ra các phần tử của Vector V - cách thông thường (dùng chỉ số):
// Xuất ra các phần tử của Vector V
for (int i=0; i<V.size(); i++)
System.out.println(V.get(i));
Xuất ra các phần tử của Vector V - dùng Enumeration:
// Xuất ra các phần tử của Vector V – dùng Enumeration
// Cách 1 - for
for (Enumeration E = V.elements(); E.hasMoreElements();)
System.out.println(E.nextElement());
// Cách 2 - while
Enumeration E = V.elements();
while (E.hasMoreElements())
System.out.println(E.nextElement());
Xuất ra các phần tử của Vector V - dùng Iterator:
// Xuất ra các phần tử của Vector V – dùng Enumeration