Download as pdf or txt
Download as pdf or txt
You are on page 1of 366

GIÁO TRÌNH

LẬP TRÌNH JAVA CƠ BẢN

HÀ NỘI - 2017
MỤC LỤC
Trang
LỜI NÓI ĐẦU ......................................................................................... 3
Chương 1. GIỚI THIỆU VỀ JAVA ....................................................... 5
1.1. Lịch sử ra đời và phát triển .......................................................... 5
1.2. Các đặc điểm ................................................................................ 8
1.3. Nền tảng Java ............................................................................. 10
1.4. Các mô trường hỗ trợ lập trình Java .......................................... 15
Tổng kết chương ............................................................................... 18
Câu hỏi ôn tập ................................................................................... 19
Chương 2. NGÔN NGỮ LẬP TRÌNH JAVA ...................................... 20
2.1. Kiến trúc chương trình Java....................................................... 20
2.2. Các thành phần của chương trình .............................................. 21
2.3. Xây dựng chương trình đầu tiên. ............................................... 22
2.4. Từ khóa, định danh, biến, hằng ................................................. 35
2.5. Các kiểu dữ liệu cơ bản ............................................................. 39
2.6. Toán tử trong Java ..................................................................... 42
2.7. Các cấu trúc điều khiển.............................................................. 51
Tổng kết chương ............................................................................... 60
Bài tập ............................................................................................... 61
Câu hỏi ôn tập ................................................................................... 62
Chương 3. LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG TRONG JAVA ......... 64
3.1. Giới thiệu ................................................................................... 64
3.2. Khái niệm lập trình hướng đối tượng ........................................ 64
3.3. Khái niệm đối tượng .................................................................. 65
3.4. Lớp ............................................................................................. 66
3.5. Lập trình với các đối tượng........................................................ 76
3.6. Các vấn đề khác về lớp .............................................................. 83
3.7. Lớp lồng (Nested Class) ............................................................ 97
3.8. Lớp cục bộ và lớp ẩn danh....................................................... 100
Tổng kết chương ............................................................................. 107
Bài tập ............................................................................................. 108
Câu hỏi ôn tập ................................................................................. 112
Chương 4. KẾ THỪA, ĐA HÌNH VÀ TẠO GÓI .............................. 117
4.1. Giới thiệu ................................................................................. 117
4.2. Giao diện .................................................................................. 117
4.3. Thừa kế .................................................................................... 127
4.4. Đa hình (Polymorphism) ......................................................... 149
4.5. Gói (Package) .......................................................................... 154
Tổng kết chương ............................................................................. 163
Bài tập ............................................................................................. 164
Câu hỏi ôn tập ................................................................................. 167
Chương 5. MẢNG VÀ XÂU KÝ TỰ ................................................. 171
5.1. Giới thiệu về mảng................................................................... 171
5.2. Mảng một chiều ....................................................................... 171
5.3. Mảng nhiều chiều..................................................................... 176
5.4. Lớp Arrays ............................................................................... 179
5.5. Xâu ký tự.................................................................................. 180
Tổng kết chương ............................................................................. 187
Bài tập ............................................................................................. 188
Câu hỏi ôn tập ................................................................................. 189
Chương 6. CÁC LỚP BAO VÀ CÁC LỚP TIỆN ÍCH ...................... 192
6.1. Các lớp bao (Wrapper Classes) ............................................... 192
6.2. Lớp Date .................................................................................. 194
6.3. Lớp GregorianCalendar ........................................................... 198
6.4. Các lớp làm việc với biểu thức chính quy ............................... 202
6.5. Lớp StringTokenizer ................................................................ 205
6.6. Lớp Random ............................................................................ 207
6.7. Lớp Math ................................................................................. 208
Tổng kết chương ............................................................................. 212
Bài tập ............................................................................................. 213
Câu hỏi ôn tập ................................................................................. 213
Chương 7. CÁC CẤU TRÚC DỮ LIỆU ............................................ 215
7.1. Giới thiệu ................................................................................. 215
7.2. Danh sách liên kết .................................................................... 215
7.3. Ngăn xếp .................................................................................. 221
7.4. Hàng đợi................................................................................... 224
7.5. Cây ........................................................................................... 227
7.6. Java Collection Framework ..................................................... 233
7.7. Lập trình tổng quát................................................................... 246
Tổng kết chương ............................................................................. 254
Bài tập ............................................................................................. 255
Câu hỏi ôn tập ................................................................................. 256
Chương 8. XỬ LÝ NGOẠI LỆ .......................................................... 258
8.1. Định nghĩa ngoại lệ .................................................................. 258
8.2. Mô hình xử lý lỗi truyền thống ................................................ 258
8.3. Tổ chức các lớp ngoại lệ trong Java ........................................ 259
8.4. Quá trình lan truyền ngoại lệ .................................................. 264
8.5. Xử lý ngoại lệ........................................................................... 265
8.6. Ngoại lệ do người dùng định nghĩa ......................................... 270
Tổng kết chương ............................................................................. 272
Bài tập ............................................................................................. 272
Câu hỏi ôn tập ................................................................................. 273
Chương 9. LẬP TRÌNH NHẬP XUẤT DỮ LIỆU ............................. 274
9.1. Luồng trong Java ..................................................................... 274
9.2. Luồng byte ............................................................................... 275
9.3. Luồng ký tự .............................................................................. 278
9.4. Nhập xuất với file .................................................................... 282
9.5. Nhập xuất có vùng đệm ........................................................... 288
9.6. Nhập xuất dữ liệu nhị phân ...................................................... 291
9.7. Luồng đối tượng ...................................................................... 294
9.8. Lớp File .................................................................................... 296
9.9. Lớp Scanner ............................................................................. 298
Tổng kết chương ............................................................................. 301
Bài tập ............................................................................................. 302
Câu hỏi ôn tập ................................................................................. 302
Chương 10. LẬP TRÌNH GIAO DIỆN ĐỒ HỌA .............................. 305
10.1. Giới thiệu ............................................................................... 305
10.2. Xây dựng giao diện GUI với thư viện AWT ......................... 305
10.3. Lập trình xử lý sự kiện ........................................................... 313
10.4. Các thành phần giao diện (Components)............................... 323
10.5. Bộ quản lý trình bày (Layout Manager) ................................ 342
10.6. Java Swing ............................................................................. 350
Tổng kết chương ............................................................................. 357
Bài tập ............................................................................................. 357
Câu hỏi ôn tập ................................................................................. 360
TÀI LIỆU THAM KHẢO ................................................................... 361
CHỮ VIẾT TẮT

API Application Programming Interface (Giao diện lập trình


ứng dụng)
CORBA Common Object Request Broker Architecture (Kiến trúc
môi giới các đối tượng)
IDE Integrated Development Environment (Môi trường phát
triển tích hợp)
J2SE Java 2 Platform Standard Edition (Phiên bản chuẩn của
nền tảng Java 2)
JAXB Java Architecture for XML Binding (Thư viện chuyển
đổi đối tượng sang dạng XML)
JDBC Java Database Connectivity (Giao diện lập trình ứng
dụng để kết nối CSDL)
JDK Java Development Kit (Bộ công cụ phát triển phần mềm
của Java)
JIT Just-In-Time (Trình biên dịch theo thời gian)
JNI Java Native Interface (Giao diện lập trình bậc thấp của
Java)
JRE Java Runtime Environment (Môi trường thực thi theo
thời gian)
JVM Java Virtual Machine (Máy ảo Java)
RMI Remote Method Invocation (Triệu gọi phương thức từ
xa)
OOP Object Oriented Programming (Lập trình hướng đối
tượng)
SDK Software Development Kit (Bộ công cụ phát triển phần
mềm)
3

LỜI NÓI ĐẦU


Công nghệ Java với ngôn ngữ lập trình Java và cách tiếp cận máy ảo ra
đời là một cuộc cách mạng trong kỹ nghệ phần mềm. Với những thay đổi mang
tính bước ngoặt, ngày nay Java đã trở thành một trong những ngôn ngữ lập trình
phổ biến nhất thế giới, thậm chí có thể nói là phổ biến nhất. Có thể thấy được
rằng, Java đang được sử dụng một cách phổ biến để xây dựng một dải rất rộng
các loại ứng dụng, từ những ứng dụng nhỏ chạy trên thẻ thông minh, trên điện
thoại di động cho đến những ứng dụng lớn và rất lớn chạy trên máy để bàn và
trên Internet, đây là một đặc điểm mà không một ngôn ngữ lập trình nào khác có
được. Hiện nay Java được một công ty lớn trên thế giới là Oracle quản lý, phát
triển và có một cộng đồng đông đảo trên toàn cầu liên tục cập nhật, bổ sung các
thư viện. Xuất phát từ những ưu điểm trên, giáo trình này được biên soạn với
mục đích cung cấp những kiến thức cơ bản và nâng cao cùng với những thông
tin mới nhất cho người học về Java.
Nội dung giáo trình gồm 10 chương:
Chương 1. GIỚI THIỆU VỀ JAVA: Trình bày về lịch sử ra đời và phát
triển, đặc điểm của Java, cách triển khai Java để xây dựng các chương trình.
Chương 2. NGÔN NGỮ LẬP TRÌNH JAVA: Trình bày quy trình xây
dựng chương trình Java, các thành phần của chương trình, các từ khóa, cách đặt
tên định danh, biến, hằng, các kiểu dữ liệu cơ bản, các toán tử, các cấu trúc điều
khiển của ngôn ngữ lập trình Java.
Chương 3. LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG TRONG JAVA:
Trình bày khái niệm lập trình định hướng đối tượng, làm rõ cách định nghĩa và
làm việc với lớp, cách tạo đối tượng lớp, cách tạo chương trình gồm các đối
tượng với ngôn ngữ lập trình Java.
Chương 4. KẾ THỪA, ĐA HÌNH VÀ TẠO GÓI: Trình bày các nội
dung nâng cao về lập trình hướng đối tượng như kế thừa, đa hình, giao diện, tạo
gói cùng cách vận dụng để việc lập trình được hiệu quả hơn.
Chương 5. MẢNG VÀ XÂU KÝ TỰ: Trình bày về các kiểu dữ liệu hay
được dùng trong lập trình là mảng và xâu ký tự cùng việc cài đặt trong Java.
Chương 6. CÁC LỚP BAO VÀ CÁC LỚP TIỆN ÍCH: Trình bày các
kiểu dữ liệu tiện ích, hay được dùng trong các chương trình do Java cung cấp.
Chương 7. CÁC CẤU TRÚC DỮ LIỆU: Trình bày việc cài đặt các kiểu
dữ liệu trừu tượng phổ biến như ngăn xếp, hàng đợi, danh sách liên kết, cây,
cùng việc lập trình tổng quát trong Java.
4

Chương 8. XỬ LÝ NGOẠI LỆ: Trình bày mô hình xử lý lỗi kiểu mới


(xử lý ngoại lệ), giúp việc xử lý lỗi được hiệu quả và dễ dàng hơn.
Chương 9. LẬP TRÌNH NHẬP XUẤT DỮ LIỆU: Trình bày khả năng
hỗ trợ lập trình nhập xuất dữ liệu đa dạng của Java, mô hình luồng, nhập xuất
với dữ liệu có các định dạng khác nhau.
Chương 10. LẬP TRÌNH GIAO DIỆN ĐỒ HỌA: Trình bày quy trình
xây dựng một ứng dụng có giao diện đồ họa hoàn chỉnh cùng khả năng tương
tác hiệu quả với người dùng.
Giáo trình được định hướng sử dụng để đào tạo học phần Ngôn ngữ lập
trình 1 (Java) cho sinh viên các ngành Công nghệ thông tin, Kỹ thuật phần mềm,
Khoa học máy tính, Hệ thống thông tin, Truyền thông và mạng máy tính. Ngoài
ra, giáo trình cũng có thể dùng làm tài liệu tham khảo trong đào tạo các học
phần về lập trình Java nâng cao, lập trình hướng đối tượng, cấu trúc dữ liệu và
giải thuật. Để học được cuốn giáo trình này, người học cần có những kiến thức
cơ bản về lập trình máy tính, có một số kỹ năng về lập trình với ngôn ngữ C. Để
phát triển khả năng lập trình chuyên nghiệp cho người học, trong giáo trình có
một số tên kiểu dữ liệu, một số dòng chú thích trong các chương trình ví dụ cùng
một số thuật ngữ được viết bằng tiếng Anh. Do Java cùng với những framework
liên quan là những công nghệ đồ sộ, nên mặc dù đã rất cố gắng, các tác giả cũng
khó tránh khỏi những sai sót khi biên soạn. Rất mong nhận được các ý kiến
đóng góp từ bạn đọc và các chuyên gia.

Các tác giả


5

Chương 1
GIỚI THIỆU VỀ JAVA
1.1. LỊCH SỬ RA ĐỜI VÀ PHÁT TRIỂN
Ngôn ngữ lập trình Java được khởi đầu bởi James Gosling và các đồng
nghiệp ở Sun Microsystems năm 1991. Ban đầu ngôn ngữ này được gọi là Oak
(có nghĩa là cây sồi, do bên ngoài văn phòng của Gosling có trồng nhiều loại cây
này), sau đó được đổi tên thành Green và cuối cùng là Java. Tên gọi Java lấy
cảm hứng từ loại cà phê Java có xuất xứ từ đảo Java của Indonesia, điều này giải
thích vì sao biểu tượng của Java là cốc cà phê bốc khói. Java được thiết kế với
mục đích để lập trình với các thiết bị điện tử dân dụng của Sun, có phong cách
cú pháp khá giống với C++. Hiện nay công ty Oracle (mua lại Sun
Microsystems từ 2009) đang giữ bản quyền và phát triển Java thường xuyên.
Java được tạo ra với tiêu chí "Viết (code) một lần, thực thi khắp nơi"
("Write Once, Run Anywhere" (WORA)). Chương trình phần mềm viết bằng
Java có thể chạy trên mọi nền tảng (platform) khác nhau thông qua một môi
trường thực thi (máy ảo) với điều kiện có môi trường thực thi thích hợp nền tảng
đó. Môi trường thực thi của Oracle hiện hỗ trợ Solaris, Linux, Mac OS,
Windows. Ngoài ra, một số công ty, tổ chức cũng như cá nhân khác cũng phát
triển môi trường thực thi Java cho những hệ điều hành khác như BEA, IBM, HP,
FreeBSD, các hệ điều hành di động, hệ điều hành nhúng,.... Trong đó đáng nói
đến nhất là IBM Java Platform hỗ trợ Windows, Linux, AIX&z/OS.
Công nghệ Java được công bố vào năm 1995, rồi trở nên nổi tiếng khi
Netscape tuyên bố tại hội thảo SunWorld năm 1995 là trình duyệt Navigator của
họ sẽ hỗ trợ Java. Về sau Java được được hỗ trợ trên hầu hết các trình duyệt như
Internet Explorer (Microsoft), Firefox (Mozilla), Safari (Apple),...
Những chi tiết về ngôn ngữ, máy ảo và API (Application Programming
Interface – Giao diện lập trình ứng dụng) của Java (Java Development Kit -
JDK) được giữ bởi Cộng đồng Java (do Oracle quản lý). Sau khi Oracle mua lại
công ty Sun Microsystems năm 2009-2010, Oracle đã mô tả họ là “người quản
lý công nghệ Java với cam kết bồi dưỡng không ngừng một cộng đồng tham gia
và minh bạch”.
Các phiên bản Java:
- JDK bản Alpha và Beta (1995): Các phiên bản Alpha và Beta được
giới thiệu với giao diện lập trình ứng dụng (API) chưa hoàn thiện. Cùng với các
phiên bản này là trình duyệt hỗ trợ có tên là WebRunner.
6

- JDK 1.0 (23/1/1996): Tên mã là Oak, bản phát hành chính thức đầu
tiên. Bản chạy ổn định là JDK 1.0.2.
- JDK 1.1 (19/2/1997), bản phát hành này bổ sung thêm:
+ Sự thay đổi mô hình sự kiện trong thư viện AWT.
+ Lớp nội hay lớp trong (inner class).
+ JavaBeans (các lớp được viết theo những quy tắc nhất định, những lớp
này có mục đích liên kết các đối tượng với nhau để tiện lợi trong truyền dữ liệu).
+ JDBC (Java Database Connectivity – Kết nối CSDL) — Một chuẩn
không phụ thuộc nền tảng hỗ trợ tương tác giữa các ứng dụng Java với các hệ
quản trị CSDL.
+ RMI (Remote Method Invocation – Giao diện triệu gọi phương thức từ
xa).
+ Reflection – Khả năng quan sát được cấu trúc bên trong của chương
trình khi chạy.
- J2SE 1.2 (8/12/1998): Phiên bản có tên mã là Playground. Đây là sự bắt
đầu của nền tảng Java 2, mang ý nghĩa thế hệ thứ hai. Phiên bản đầu tiên của
Java 2 được đánh số 1.2. Với Java 2, SUN Microsystems phát hành Java dưới
dạng gói J2SE (Java 2 Platform Standard Edition — Phiên bản chuẩn của nền
tảng Java 2).
Java 2 hoặc Java 2.0 là một sự phát triển và hoàn thiện tiếp theo của ngôn
ngữ lập trình và nền tảng Java trước (từ đó trở đi được quy ước là Java 1.0).
Hiện nay, Java 2.0 liên tục được phát triển và bổ sung thêm nhiều khả năng mới,
đặc biệt để cạnh tranh sau khi nền tảng.Net ra đời.
Những sự hoàn thiện chủ yếu trong Java 2 so với Java 1.0 là:
+ Thư viện Swing.
+ Các Collection.
+ Java 2D.
+ Hỗ trợ công nghệ kéo thả (drag-and-drop).
+ Hỗ trợ một cách đầy đủ Unicode.
+ Hỗ trợ làm việc với các file âm thanh có định dạng khác nhau.
+ Hỗ trợ một cách đầy đủ công nghệ CORBA.
+ Bổ sung trình biên dịch JIT (Just-in-Time) làm nâng cao hiệu suất chạy
chương trình.
+ Hoàn thiện các công cụ JDK.
7

- J2SE 1.3 (8/5/2000), tên mã là Kestrel, các bổ sung chính:


+ Bổ sung máy ảo HotSpot.
+ RMI được thay đổi để tương thích hơn với CORBA.
+ Giao diện JNDI (Java Naming and Directory Interface) được bổ sung
vào các thư viện chính.
+ Kiến trúc JPDA (Java Platform Debugger Architecture).
+ Thư viện âm thanh JavaSound
- J2SE 1.4 (6/2/2002): Có tên mã Merlin. Đây là bản phát hành đầu tiên
được phát triển bởi Java Community Process (cộng đồng Java).
Những thay đổi chính:
+ Thêm từ khóa assert vào ngôn ngữ lập trình.
+ Thư viện biểu thức chính quy (regular expression).
+ Thư viện lan truyền ngoại lệ (exception chaining).
+ Thư viện hỗ trợ Internet Protocol version 6 (IPv6).
+ Thư viện non-blocking IO (còn được gọi là New Input/Output, NIO) hỗ
trợ nhập xuất tốc độ cao.
+ Thư viện ghi nhật ký (logging API).
+ Thư viện đọc/ghi các file ảnh định dạng JPEG và PNG.
+ Thư viện xử lý XML và XSLT.
+ Thư viện bảo mật (JCE, JSSE, JAAS).
+ Thư viện Java Web Start.
+ Thư viện Preferences API (trong package java.util.prefs).
Quá trình hỗ trợ và cập nhật bảo mật cho Java 1.4 kết thúc vào 10/2008.
- J2SE 5.0 (30/9/2004), có tên mã Tiger, trong phiên bản này có rất nhiều
sự bổ sung:
+ Kiểu liệt kê (enum).
+ Annotation — khả năng thêm vào mã nguồn chương trình những siêu
dữ liệu mà không ảnh hưởng đến sự hoạt động nhưng cho phép nhận được thêm
nhiều thông tin về chương trình và quá trình thực thi.
+ Công nghệ lập trình tổng quát (generics).
+ Phương thức có số tham số biến đổi.
+ Autoboxing/Unboxing — khả năng biến đổi tự động giữa các kiểu dữ
liệu cơ sở và các lớp bao tương ứng (wrapper class) (ví dụ giữa int — Integer).
+ Cho phép import các biến static.
+ Vòng lặp duyệt các phần tử của collection (iterator, foreach).
8

- Java SE 6 (11/12/2006): Có tên mã Mustang. Bắt đầu từ phiên bản này,


Sun đổi tên J2SE thành Java SE.

Những thay đổi chính:


+ Ngừng hỗ trợ những phiên bản cũ Win9x của hệ điều hành Windows.
+ Hỗ trợ ngôn ngữ Script.
+ Phát triển hỗ trợ Web Service với JAX-WS.
+ Hỗ trợ JDBC 4.0.
+ Java Compiler API - các API cho phép điều khiển trình biên dịch Java
Compiler.
+ Nâng cấp JAXB lên phiên bản 2.0.
+ Nhiều thành phần giao diện được hoàn thiện.
+ Tối ưu hóa hoạt động của máy ảo JVM .
- Java SE 7 (7/7/2011): Có tên mã Dolphin, đây là phiên bản chứa đựng
một sự cập nhật đồ sộ.
Những khả năng mới trong Java 7:
+ Máy ảo hỗ trợ các ngôn ngữ lập trình động.
+ Lệnh switch chấp nhận tham số kiểu xâu ký tự.
+ Quản lý tài nguyên tự động trong khối lệnh try.
+ Hỗ trợ lập trình song song.
+ Thư viện lập trình nhập/xuất mới: java.nio.file và java.nio.file.attribute.
+ Thư viện các thuật toán mã hóa Elliptic.
- Java SE 8 (18/3/2014). Các bổ sung chính:
+ Hỗ trợ lambda expressions (hàm không có tên).
+ Cho phép nhúng mã JavaScript trong chương trình.
+ Hỗ trợ số học số nguyên không dấu (Unsigned Integer Arithmetic).
+ Thư viện các API để làm việc với ngày tháng, thời gian.
+ Các thư viện liên kết tĩnh JNI.
- Java SE 9: Tại hội thảo JavaOne 2011, hãng Oracle đã thảo luận về các
tính năng mà họ hy vọng có được khi phát hành Java 9 dự kiến vào năm 2016,
đó là việc hỗ trợ tốt hơn bộ nhớ động lên đến vài gigabyte, tích hợp mã bậc thấp,
và máy ảo tự động bật tắt. Tuy nhiên, vào đầu năm 2016, lịch phát hành Java 9
được thay đổi dời sang tháng Ba năm 2017.
- Java SE 10: Dự kiến phát hành vào năm 2018.
1.2. CÁC ĐẶC ĐIỂM
9

- Java là một ngôn ngữ đơn giản: Java tương tự như C++ nhưng đã bỏ bớt
các đặc tính phức tạp của C++ như: quản lý bộ nhớ, con trỏ, nạp chồng toán tử,
các từ khóa include, struct, union.

- Tính khả chuyển (portable): Chương trình viết trên Java sau khi biên
dịch chứa mã byte (bytecodes) không phụ thuộc vào hệ thống máy sử dụng.
Chương trình viết trên máy này hoàn toàn có thể chạy trên các loại máy khác mà
không cần phải biên dịch lại. Đây là đặc điểm mang tính cách mạng trong kỹ
nghệ phần mềm. Hình 1.1 minh họa cho đặc điểm này.

Hình 1.1. Tính khả chuyển của Java


- Java là ngôn ngữ hướng đối tượng: Cũng giống như C++, Java là một
ngôn ngữ hướng đối tượng. Tuy nhiên tính chất hướng đối tượng trong Java là
hướng đối tượng hoàn toàn, nghĩa là không thể viết một chương trình hướng thủ
tục ở trong Java, tất cả các hàm trong chương trình đều phải là các phương thức
của các lớp, thậm chí hàm chính (hàm main) của một chương trình viết bằng
Java cũng phải đặt bên trong một lớp. Hướng đối tượng trong Java không có tính
đa kế thừa (multi inheritance) lớp như trong C++, thay vào đó Java đưa ra khái
niệm interface để hỗ trợ khả năng đa kế thừa.
- Hỗ trợ xây dựng ứng dụng phân tán: Java hỗ trợ các công nghệ lập trình
RMI, CORBA, JavaBean. Các công nghệ này cho phép sử dụng lại các lớp đã
tạo ra, triệu gọi các phương thức (method) hoặc các đối tượng từ một máy ở xa.
- Hỗ trợ lập trình đa luồng (multithread): Đặc tính này cho phép tạo nhiều
tiến trình, tiểu trình có thể chạy song song cùng một thời điểm và có thể tương
tác với nhau.
- Java là ngôn ngữ có tính an toàn: Chương trình viết trên Java trước khi
thực thi được kiểm tra an toàn với nhiều mức:
+ Mức 1: Mức ngôn ngữ, nhờ tính bao gói dữ liệu của lập trình hướng đối
tượng, có thể ngăn chặn truy cập trực tiếp bộ nhớ và phải truy cập gián tiếp có
10

kiểm soát thông qua các phương thức.


+ Mức 2: Mức trình biên dịch, kiểm tra an toàn cho mã nguồn trước khi
biên dịch.
+ Mức 3: Mức trình thông dịch, mã bytecode được kiểm tra an toàn trước
khi thực thi.
+ Mức 4: Mức class, các class trước khi nạp được kiểm tra an toàn.
- Quá trình biên dịch và thông dịch: Khác với phần lớn các ngôn ngữ
khác, một chương trình viết trên Java để có thể thực thi, phải trải qua quá trình
cả biên dịch và thông dịch. File chương trình nguồn có đuôi *.java đầu tiên được
biên dịch thành file bytecode có đuôi *.class và sau đó khi thực thi, nó sẽ được
trình thông dịch (máy ảo) thông dịch thành mã máy để thực hiện. File *.class có
thể chạy được ở bất kỳ platform nào (Viết một lần, chạy khắp nơi - Write Once
Run Anywhere).
- Giải phóng bộ nhớ tự động (Garbage Collection): Trong Java, hiện
tượng rò rỉ bộ nhớ hầu như không xảy ra do bộ nhớ được quản lí bởi Java
Virtual Machine (JVM) có khả năng tự động “dọn dẹp rác”. Người lập trình
không phải quan tâm đến việc phải giải phóng bộ nhớ như C, C++. Tuy nhiên
khi sử dụng những tài nguyên mạng, nhập xuất file, CSDL (nằm ngoài kiểm soát
của JVM) mà người lập trình không đóng các luồng (streams) thì thất thoát bộ
nhớ vẫn có thể xảy ra.
1.3. NỀN TẢNG JAVA
Java không chỉ là một ngôn ngữ lập trình, mà là tập hợp một loạt các công
nghệ cho phép xây dựng các loại ứng dụng khác nhau. Công nghệ Java phát
triển mạnh mẽ nhờ vào Sun Microsystem (hiện nay là Oracle và cộng đồng
Java) cung cấp nhiều công cụ, thư viện lập trình phong phú hỗ trợ cho việc phát
triển nhiều loại ứng dụng khác nhau. Một công nghệ Java bao gồm:
- Ngôn ngữ lập trình;
- Môi trường phát triển;
- Môi trường thực thi và triển khai.
Hiện nay có các công nghệ chính như sau:
- Java SE — Java Standard Edition, đây là nội dung chính của Java, chứa
các trình dịch, các giao diện lập trình ứng dụng (API), môi trường thực thi (Java
Runtime Environment – JRE). Java SE thích hợp để xây dựng các ứng dụng
chạy các máy tính cá nhân.
- Java EE — Java Enterprise Edition, là công nghệ giúp xây dựng các ứng
11

dụng lớn mức độ doanh nghiệp và chạy trên môi trường mạng.
- Java ME — Java Micro Edition, là công nghệ giúp xây dựng các ứng
dụng nhỏ chạy trên các thiết bị có năng lực tính toán thấp như điện thoại di
động, máy tính bỏ túi hay các thiết bị nhúng.
- JavaFX — Là công nghệ cần cho việc xây dựng các ứng dụng Internet
có giao diện đồ họa phức tạp (Rich Internet Application).
- Java Card — Là công nghệ giúp xây dựng các ứng dụng chạy trên thẻ
thông minh (smart card).
- Một công nghệ nữa không thể không nói đến, công nghệ này đã làm
Java trở thành ngôn ngữ lập trình phổ biến bậc nhất hiện nay. Đó là công nghệ
hỗ trợ xây dựng các ứng dụng trên nền hệ điều hành Android. Để lập trình trên
Android, thông thường sử dụng ngôn ngữ lập trình Java. Tuy nhiên Android chỉ
hỗ trợ việc biên dịch ngôn ngữ Java mà không cung cấp các thư viện lớp, ngoài
ra máy ảo trên Java cũng là Dalvik VM chứ không phải máy ảo JVM.
Các công nghệ trên hỗ trợ xây dựng các loại ứng dụng khác nhau, tuy
nhiên những ứng dụng này có điểm chung là đều làm việc trên nền tảng Java.
Nền tảng Java chứa các chương trình bảo đảm xây dựng và thực thi các ứng
dụng, đó là trình biên dịch và môi trường thực thi Java Runtime Environment
(JRE). Trình biên dịch có trách nhiệm dịch mã nguồn ra mã bytecode, trình biên
dịch được cung cấp như một phần của JDK. JRE chứa máy ảo và trình biên dịch
tại lúc chạy just-in-time (JIT) có nhiệm vụ chuyển mã bytecode sang mã máy để
thực thi. Ngoài hai chương trình này, nền tảng Java còn chứa các thư viện mở
rộng. JDK là bộ phần mềm bắt buộc phải có để xây dựng ứng dụng Java, còn
JRE là bộ phần mềm bắt buộc phải có để máy tính có thể thực thi được ứng
dụng Java.
Java Development Kit (JDK - Bộ công cụ cho người phát triển ứng dụng
bằng ngôn ngữ lập trình Java) là một tập hợp những công cụ phần mềm được
phát triển bởi Sun Microsystems trước đây và hiện nay là hãng Oracle dành cho
các nhà phát triển phần mềm, dùng để viết những ứng dụng Java. Kể từ khi Java
ra đời, JDK là bộ phát triển phần mềm (Software Development Kit – SDK)
thông dụng nhất cho Java. Ngày 17 tháng 11 năm 2006, hãng Sun tuyên bố JDK
sẽ được phát hành dưới giấy phép GNU General Public License (GPL), nghĩa là
JDK đã trở thành một phần mềm miễn phí.
JDK chứa những công cụ phần mềm chính sau:
- appletviewer – công cụ cho phép chạy và gỡ rối các applet và không cần
12

trình duyệt;
- apt – công cụ xử lý annotation (annotation-processing tool);
- extcheck – trình tiện ích để phát hiện các xung đột trong file JAR;
- idlj – trình biên dịch từ ngôn ngữ IDL (Interactive Data Language) sang
Java;
- jabswitch – công cụ Java Access Bridge, giúp nhận được các thông tin
về các công nghệ hỗ trợ trên các hệ thống Microsoft Windows;
- java – trình thông dịch các ứng dụng Java. Công cụ này thông dịch các
file *.class nhận được từ trình biên dịch javac. Công cụ này được sử dụng cả
trong phát triển và triển khai. Trình thông dịch trước đây nằm trong JRE, và hiện
nay java trở thành trình thông dịch được cung cấp kèm JDK;
- javac – trình biên dịch, có nhiệm vụ biên dịch các file mã nguồn sang
file bytecode (file *.class);
- javadoc – công cụ sinh tài liệu tự động từ mã nguồn;
- jar – công cụ đóng gói, giúp đóng gói tất cả các file của một ứng dụng
thành file JAR. Công cụ này cũng giúp quản lý các file JAR có trên máy;
- javafxpackager – công cụ giúp đóng gói và tạo chữ ký cho các ứng dụng
JavaFX;
- jarsigner – công cụ tạo chữ ký và xác minh file JAR;
- javah – công cụ sinh các file C header (*.h) và các file cài đặt tương
ứng, được sử dụng cho các phương thức native;
- javap – công cụ disassembler (dịch ngược) các file *.class;
- javaws – công cụ Java Web Start (chạy ứng dụng Java trên Internet qua
trình duyệt) cho các ứng dụng JNLP (Java Network Launching Protocol);
- JConsole – công cụ giúp giám sát máy ảo và các ứng dụng Java;
- jdb – trình gỡ rối (debugger);
- jhat – công cụ phân tích bộ nhớ động dùng cho Java (đang thử nghiệm);
- jinfo – trình tiện ích giúp thu thập thông tin cấu hình về một tiến trình
Java đang chạy hoặc đã bị dừng một cách không mong muốn (đang thử
nghiệm);
- jmap – trình tiện ích giúp lập bản đồ bộ nhớ của một ứng dụng Java
(đang thử nghiệm);
- jmc – Java Mission Control, công cụ giúp thu thập thông tin về một sự
cố;
- jrunscript – Java command-line script shell, công cụ chạy các script lệnh
13

Java;
- jstack – trình tiện ích giúp thu thập thông tin stack của các thread (đang
thử nghiệm);
- jstat – công cụ giám sát và thống kê hoạt động của máy ảo (đang thử
nghiệm);
- jstatd – trình jstat chạy ở chế độ nền (daemon) (đang thử nghiệm);
- keytool – công cụ để thao tác với keystore (kho lưu trữ các chứng chỉ
bảo mật);
- pack200 – công cụ nén file JAR;
- policytool – công cụ tạo và quản lý các chính sách, cho phép xác định
chính sách trong lúc chạy ứng dụng, quy định các quyền có thể cho mã nguồn
nhận được từ các nguồn khác nhau;
- VisualVM – công cụ giúp cung cấp các thông tin chi tiết về các ứng dụng
đang chạy trên máy ảo;
- wsimport – công cụ giúp sinh các thành phần JAX-WS khả chuyển để
triệu gọi một web service;
- xjc – công cụ là một phần của JAXB API (Java API for XML Binding),
cho phép sinh các lớp Java từ một lược đồ XML.
Mặc dù JDK và JRE được cung cấp riêng rẽ, nhưng trong JDK cũng có
một JRE được gọi là JRE riêng. Vì vậy khi đã cài đặt JDK thì có thể xây dựng
và triển khai các ứng dụng Java mà không cần cài đặt thêm JRE. Còn nếu như
chỉ cần cho máy tính có thể thực thi các ứng dụng Java thì trên máy chỉ cần cài
đặt JRE. Cả JDK và JRE có thể được tải về từ trang web của Oracle
(oracle.com) một cách miễn phí.
14

Hình 1.2. Vai trò của JVM


JRE bao gồm thành phần chính là máy ảo Java (Java Virtual Machine –
JVM) và một số API. Hình 1.2 mô tả vai trò của JVM.
JVM có nhiệm vụ thông dịch mã bytecode sang mã máy, JVM bảo đảm
cho một ứng dụng Java sau khi biên dịch có thể chạy được trên các nền tảng (hệ
điều hành, phần cứng) khác nhau mà không cần viết hoặc biên dịch lại. Nói một
cách khác, JVM giúp trung hòa sự khác biệt giữa các nền tảng. Tuy nhiên cần
lưu ý là với mỗi nền tảng lại cần một JVM tương ứng cho nền tảng đó chứ
không có một JVM dành cho tất cả các nền tảng. Kiến trúc tổng thể về công
nghệ Java được thể hiện trên Hình 1.3.
Những ý tưởng về JDK, JRE, JVM là những ý tưởng mang tính cách
mạng, sau này môi trường lập trình Visual Studio.NET cũng thừa kế những ý
tưởng này để xây dựng một môi trường lập trình mới cạnh tranh với Java. Cụ
thể, có thể thấy sự tương đồng giữa JDK và.NET Framework, giữa JRE với
CLR (Common Language Runtime)
15

Hình 1.3. Kiến trúc tổng thể của công nghệ Java
1.4. CÁC MÔI TRƯỜNG HỖ TRỢ LẬP TRÌNH JAVA
Java là ngôn ngữ phổ biến trên thế giới, để xây dựng ứng dụng trên Java
hiện nay có rất nhiều môi trường hỗ trợ. Để xây dựng một ứng dụng Java, cách
đơn giản nhất là dùng một trình soạn thảo văn bản tùy ý để soạn thảo mã nguồn
và dùng JDK để biên dịch và thực thi. Tuy nhiên với cách này thì người phát
triển phải làm việc trong chế độ dòng lệnh, và các hoạt động soạn thảo, gỡ rối
rất khó khăn.
Để các hoạt động soạn thảo, chạy và gỡ rối được đơn giản hơn, nhiều môi
trường phát triển tích hợp (Integrated Development Environment – IDE) đã
được phát triển.
Những IDE phổ biến nhất cho lập trình Java:
- NetBeans IDE — IDE miễn phí và mạnh mẽ, cho phép xây dựng tất cả
các loại ứng dụng Java (Java ME, Java SE, Java EE, Java Web, Java FX,...).
Đây là IDE được Sun Microsystems khuyến nghị dùng. Trong NetBeans, ngoài
ứng dụng Java còn có thể xây dựng các ứng dụng trên các ngôn ngữ khác (C,
C++, PHP, Fortran,...).
- Eclipse IDE — Một trong các IDE phổ biến cho Java của công ty IBM,
cũng miễn phí và cho phép xây dựng tất cả các ứng dụng Java. Trên IDE này
cũng có thể xây dựng ứng dụng trên các ngôn ngữ khác (C, C++, Ruby,
Fortran,...).
16

- IntelliJ IDEA — IDE của công ty JetBrains, có phiên bản miễn phí
(Community Edition) và phiên bản thương mại (Ultimate Edition).
- JDeveloper — IDE miễn phí của Oracle.
- BlueJ — IDE được sử dụng trong giảng dạy và xây dựng các ứng dụng
nhỏ và vừa.
Trong khuôn khổ tài liệu này, chúng ta sẽ tập trung vào nội dung ngôn
ngữ lập trình trong công nghệ Java chứ không đi sâu vào các nội dung môi
trường phát triển và triển khai.
Để phát triển và triển khai, thực thi các ví dụ, trong tài liệu sử dụng JDK
1.7 và NetBeans IDE 8.0. Các phiên bản JDK và NetBeans mới hơn đều hỗ trợ
các ví dụ của tài liệu.
JDK được tải về từ trang web oracle.com của Oracle (Hình 1.4),
NetBeans được tải từ trang web netbeans.org (Hình 1.5) hoặc cũng từ trang của
Oracle (Hình 1.4). Lưu ý là cần chọn đúng phiên bản cho hệ điều hành đang cài
trên máy và cần cài đặt JDK trước NetBeans.
Hệ điều hành được sử dụng trong giáo trình là Windows 7.0, các thư mục
chứa JDK và NetBeans khi cài đặt được chọn ngầm định: C:\Program Files\Java
và C:\Program Files\NetBeans 8.0.
Khi cài đặt NetBeans, hệ điều hành sẽ tạo ra Shortcut trên màn hình để
người sử dụng có thể mở.
17

Hình 1.4. Trang web để tải về JDK

Hình 1.5. Trang web để tải về NetBeans IDE


Sau khi cài đặt, cấu trúc thư mục JDK được thể hiện trên Hình 1.6. Trong
các thư mục con, thư mục “bin” chứa các trình biên dịch, thông dịch và các
chương trình tiện ích khác (Hình 1.7), thư mục “jre” chứa môi trường thực thi
(JRE được cung cấp kèm theo JDK).

Hình 1.6. Cấu trúc thư mục Java


18

Hình 1.7. Các chương trình chính trong JDK


Nếu chỉ dùng JDK để xây dựng và thực thi ứng dụng thì người lập trình
phải sử dụng các chương trình javac và java ở chế độ dòng lệnh. Còn nếu sử
dụng IDE thì IDE sẽ giúp gọi javac và java một cách tự động.
Ngoài IDE NetBeans, chúng ta cũng có thể sử dụng IDE Eclipse, quy trình sử
dụng hai IDE này để lập trình Java rất giống nhau.

TỔNG KẾT CHƯƠNG


Chương 1 đã trình bày một cách tổng quan về các nội dung:
- Lịch sử ra đời của công nghệ Java, ngôn ngữ lập trình Java, các phiên
bản.
- Các đặc điểm của Java: Đơn giản, khả chuyển, hướng đối tượng, hỗ trợ
lập trình phân tán và đa luồng, an toàn, biên dịch và thông dịch, tự động giải
phóng bộ nhớ.
- Một công nghệ Java bao gồm: Ngôn ngữ lập trình, môi trường phát
triển, môi trường thực thi.
- Các công nghệ Java phổ biến: Java SE, Java EE, Java ME,... Các công
nghệ đều làm việc trên nền tảng Java.
- Nền tảng Java trình biên dịch và môi trường thực thi Java Runtime
Environment (JRE).
19

- Để xây dựng ứng dụng Java cần có JDK, còn để thực thi cần có JRE.
- Cách cài đặt Java cùng thông tin môi trường lập trình để làm cơ sở cho
những nội dung tiếp theo.
Câu hỏi ôn tập
1. Lịch sử ra đời của Java, các đặc điểm, nội dung của công nghệ Java.
2. Các công nghệ Java cụ thể và những loại ứng dụng được hỗ trợ.
3. Khái niệm và phân biệt JDK, JRE, JVM.
4. Khái niệm biên dịch và thông dịch.
5. Các chương trình biên dịch và thông dịch trong JDK.
6. Cách cài đặt JDK và IDE cho lập trình Java.
20

Chương 2
NGÔN NGỮ LẬP TRÌNH JAVA
2.1. KIẾN TRÚC CHƯƠNG TRÌNH JAVA
Giống như những ngôn ngữ lập trình khác, file mã nguồn chương trình
Java cũng có phần mở rộng đặc trưng, ở đây phần mở rộng là.java. Ở trong
Chương 1, chúng ta đã biết rằng một chương trình Java khi chạy sẽ phải cần đến
nền tảng Java. Nền tảng Java bao gồm JVM và các API cùng một số chương
trình khác. Với đặc điểm này, kiến trúc một chương trình Java có thể được miêu
tả như trên Hình 2.1.

Hình 2.1. Kiến trúc chương tình Java


Máy ảo JVM cùng các chương trình trong JDK đã được giới thiệu trong
Chương 1. Để bắt đầu việc xây dựng một chương trình Java, cần biết có những
API nào được cung cấp cho người lập trình. Trong Java, các API được cung cấp
dưới dạng các lớp (class) và được đóng gói trong các gói (package). Khi cài đặt
JDK, người lập trình có thể sử dụng những gói chuẩn sau để xây dựng chương
trình:
- java.lang – chứa những nội dung cơ bản nhất của ngôn ngữ Java.
- java.applet – dùng để xây dựng các applet.
- java.awt – dùng để lập trình giao diện đồ họa với thư viện AWT.
- java.io – dùng để lập trình nhập/xuất.
- java.util – chứa các thư viện tiện ích.
- java.net – dùng cho lập trình mạng.
- java.awt.event – dùng cho lập trình xử lý sự kiện trong ứng dụng GUI.
- java.rmi – dùng cho lập trình triệu gọi từ xa RMI.
- java.security – các thư viện hỗ trợ lập trình bảo mật.
- java.sql – dùng cho lập trình CSDL.
Các gói này đóng vai trò là những thư viện chứa những đoạn mã đã được
xây dựng trước mà người lập trình có thể sử dụng lại. Java cung cấp từ khóa
import cho người lập trình để sử dụng một gói, từ khóa import có vai trò tương
21

tự từ khóa include trong C/C++.


Trong các gói trên, gói java.lang luôn được tự động kết nối với mọi
chương trình Java, người lập trình không cần thông báo import.
2.2. CÁC THÀNH PHẦN CỦA CHƯƠNG TRÌNH
Phần đầu của một file chương trình Java xác định thông tin về các thư
viện sẽ được sử dụng. Trong lập trình Java, chương trình được chia thành các
lớp hoặc các gói riêng biệt, mỗi lớp thông thường được chứa trong một file riêng
rẽ, các lớp có thể được gom vào các gói. Những lớp và gói được sử dụng trong
chương trình được chỉ dẫn với sự trợ giúp của câu lệnh nhập “import”. Mỗi
chương trình có thể có nhiều hơn một câu lệnh nhập. Dưới đây là một ví dụ về
câu lệnh nhập:
import java. awt.*;
Câu lệnh này nhập gói “awt”, ở đây java là tên của thư mục chứa gói
“awt”, ký hiêu “*” chỉ tất cả các lớp thuộc gói này. Với câu lệnh này, người lập
trình có thể gọi sử dụng tất cả các lớp trong gói “awt”. Nếu chỉ muốn sử dụng
một lớp cụ thể, thì cần chỉ rõ tên đầy đủ của lớp trong phát biểu import, ví dụ
câu lệnh sau sẽ chỉ nhập lớp Button trong gói java.awt vào chương trình:
import java.awt.Button;
Trong chương trình, tất cả các mã, bao gồm các biến, và các khai báo
được thực hiện trong phạm vi các lớp. Bởi vậy, sau các câu lệnh nhập thường là
các khai báo lớp. Một lớp thông thường gồm có các thuộc tính (các biến) và các
phương thức (là các hàm) xử lý các thuộc tính của lớp để nhận được kết quả nào
đó. Một chương trình đơn giản có thể chỉ có một vài lớp. Mỗi câu lệnh, mỗi khai
báo đều được kết thúc bởi dấu chấm phảy “;”. Chương trình còn có thể bao gồm
các ghi chú. Khi dịch, chương trình dịch sẽ tự loại bỏ các ghi chú này. Ví dụ,
dưới đây là khai báo một lớp:
class Class_Name{
int num1, num2; /* Đây là dòng ghi chú*/
Show(){ //Khai báo phương thức (hàm)

statements; // Kết thúc bởi dấu chấm phảy


}
....
}
Chương trình Java được tạo từ những đơn vị được gọi là “token”. Token
22

là đơn vị riêng lẻ, nhỏ nhất, có ý nghĩa đối với trình biên dịch của một chương
trình Java.
Các “token” được chia thành năm loại:
- Định danh (identifiers): Dùng để thể hiện tên biến, hằng, tên phương
thức, hoặc tên các lớp. Chương trình biên dịch sẽ xác định các tên này là duy
nhất trong chương trình.
- Từ khoá/từ dự phòng (Keyword/Reserve Words): Là những định danh
đã được Java xác định trước. Người lập trình không được phép sử dụng chúng
như một định danh. Ví dụ class, import là những từ khoá.
- Ký tự phân cách (separator): Thông báo cho trình biên dịch việc phân
nhóm các phần tử của chương trình. Ví dụ các ký tự phân cách: {} ;,
- Nguyên dạng (literals): Là các giá trị không đổi trong chương trình.
Nguyên dạng có thể là các số, chuỗi, các ký tự hoặc các giá trị Boolean. Ví dụ
21, ‘A’, 31.2, “This is a sentence” là những nguyên dạng.
- Các toán tử: Cho phép lập và tính toán các biểu thức được hình thành
bởi dữ liệu và các đối tượng. Java có một tập lớn các toán tử, các toán tử sẽ
được giới thiệu chi tiết ở chương này.
2.3. XÂY DỰNG CHƯƠNG TRÌNH ĐẦU TIÊN
Chúng ta hãy bắt đầu từ chương trình Java với một ứng dụng đơn giản và
kinh điển. Chương trình trong Ví dụ 2.1 cho phép hiển thị một thông điệp “Hello
World” ra màn hình.
Ví dụ 2.1. Chương trình in ra màn hình dòng chữ “Hello World”:
// This is a simple program called “First.java”
class First {
public static void main(String args[]) {
System.out.println(“Hello World”);
}
}
Chương trình sẽ được xây dựng theo hai cách: cách thứ nhất là dùng trình
soạn thảo văn bản bất kỳ kết hợp với JDK, cách thứ hai là dùng NetBeans.
Cách thứ nhất:
Trước hết cần thêm thư mục chứa các chương trình javac, java vào cuối
biến Path của hệ điều hành để hệ điều hành biết nơi chứa các chương trình này.
Lưu ý khi nhập, tên đúng phải chứa cặp nháy kép chỗ "Program Files" và kết
thúc bởi ;. Nghĩa là phải thêm vào Path xâu ký tự sau:
23

C:\"Program Files"\Java\jdk1.7.0_51\bin;

Hình 2.2. Truy cập để thiết lập các tham số của hệ thống

Hình 2.3. Truy cập các biến môi trường của hệ thống
Để thay đổi biến Path, cần vào Control Panel, chọn mục System, nhấn
chuột vào dòng chữ “Advanced system settings” ở cột bên trái của cửa sổ hiện
ra (Hình 2.2).
24

Tiếp theo, trên cửa sổ hiện ra, nhấn chuột vào nút “Environment
Variables” trong tab Advanced (Hình 2.3).
Trong cửa sổ hiện ra, chọn dòng chứa biến Path và nhấn nút Edit (Hình
2.4).

Hình 2.4. Truy cập vào biến Path của hệ thống


Tiếp theo, trên cửa sổ hiện ra, thêm vào cuối biến Path xâu ký tự
“C:\"Program Files"\Java\jdk1.7.0_51\bin;” và nhấn nút OK (Hình 2.5). Lưu ý
là chỉ thêm vào cuối biến Path, chứ không được thay đổi phần đã có của Path
(Để di chuyển đến cuối xâu ký tự chứa biến Path, có thể dùng phím End trên
bàn phím).

Hình 2.5. Thêm đường dẫn chứa java, javac vào biến Path
Như vậy, chúng ta đã khai báo với hệ điều hành Windows đường dẫn đến
các chương trình javac, java của JDK, nghĩa là chúng ta đã có thể gõ lệnh javac,
java tại chương trình thực thi dòng lệnh cmd.exe của Windows. Với các phiên
bản khác của hệ điều hành Windows, cách làm cũng tương tự, mấu chốt là phải
25

khai báo với hệ điều hành đường dẫn đến các chương trình javac, java của JDK.
Tiếp theo, để tạo chương trình, chúng ta tạo thư mục C:\Java để chứa file mã
nguồn.
Dùng Notepad (hoặc một trình soạn thảo văn bản bất kỳ) để soạn thảo
chương trình trong Ví dụ 2.1, và lưu trữ vào file “First.java” trên đĩa cứng tại
thư mục C:\Java. Tên file mã nguồn đóng vai trò rất quan trọng trong Java.
Chương trình biên dịch Java chỉ chấp nhận file với phần mở rộng “.java”.
Trong Java các mã cần phải gom thành các lớp, tên lớp chính (một file có thể
có nhiều lớp) và phần cơ sở của tên file phải trùng nhau (có thể khác nhau
chữ hoa, chữ thường).
Để biên dịch mã nguồn, ta sử dụng trình biên dịch javac. Trước hết cần
mở chương trình thực thi dòng lệnh cmd.exe bằng cách truy cập qua nút lệnh
Start của Windows (Hình 2.6).

Hình 2.6. Mở chương trình thực thi dòng lệnh cmd.exe của Windows
Tại dấu nhắc dòng lệnh, dùng lệnh nhảy cd để chọn thư mục làm việc là
C:\Java, sau đó gõ lệnh java với tham số là tên đầy đủ của file mã nguồn cần
biên dịch (Hình 2.7).
26

Hình 2.7. Biên dịch chương trình Java


Nếu chương trình không có lỗi thì trình dịch java sẽ tạo ra file First.class
chứa các mã “bytecodes”. Những mã này chưa thể thực thi được, để chương
trình thực thi được ta cần dùng trình thông dịch java, lưu ý là không gõ
đuôi.class. Kết quả sẽ hiển thị trên màn hình như trên Hình 2.8.

Hình 2.8. Thực thi chương trình Java


Với những file chương trình có đuôi.class hoặc sau khi đóng gói thành file
có đuôi.jar, nếu không có giao diện đồ họa thì khi chạy phải thông qua chương
trình java ở chế độ dòng lệnh. Với những chương trình có giao diện đồ họa, sau
khi đóng gói thành file.jar thì file này có thể chạy được ngay trên giao diện
Explorer như những file.exe bình thường.
Có thể thấy rằng, cách xây dựng thuần túy bằng JDK khá phức tạp.
Những công việc thiết lập biến môi trường, nhập dòng lệnh hoặc sửa lỗi khi
chương trình có lỗi chiếm khá nhiều thời gian. Tuy nhiên cách này giúp hiểu rõ
hơn về quá trình xây dựng và thực thi chương trình Java và những người bắt đầu
Java cũng cần phải biết.
Cách thứ hai: Dùng NetBeans IDE.
27

Khởi động NetBeans, mở menu File, chọn menu con New Project. Trên
cửa sổ New Project, trong mục Categories chọn loại ứng dụng là Java, trong
mục Projects chọn Java Application (Hình 2.9). Trong NetBeans, mỗi chương
trình được coi là một project. Ghi nhớ là trong những ví dụ sau chúng ta cũng sẽ
chọn loại ứng dụng này.

Hình 2.9. Tạo chương trình Java bằng NetBeans

Hình 2.10. Đặt tên chương trình và chọn nơi lưu trữ
28

Sau khi chọn xong, nhấn nút Next, nhập tên ứng dụng là First và thư mục
chứa ứng dụng là C:\Java\First. Chọn xong nhấn nút Finish (Hình 2.10).

Hình 2.11. Cấu trúc thư mục một project trong NetBeans

Hình 2.12. Thêm file mã nguồn vào project


Trên Hình 2.11 là cấu trúc chương trình vừa tạo, hiện tại project chứa hai
thư mục “Source Packages” và “Libraries”. Thư mục “Source Packages” được
29

quy ước là thư mục chứa mã nguồn, hiện tại đang rỗng. Thư mục “Libraries”
chứa các thư viện API của JDK mà người lập trình có thể gọi ra sử dụng. Trong
một project có thể tạo, sửa, xóa các thư mục mới.
Để tạo file mã nguồn chương trình trong Ví dụ 2.1, nhấn nút phải chuột
vào thư mục con “Default Package” của thư mục “Source Packages”, tại menu
ngữ cảnh chọn New và chọn Java class (Hình 2.12).
Trên cửa sổ mở ra, nhập Class Name là First, sau đó nhất nút Finish để
kết thúc quá trình tạo khung lớp (Hình 2.13).

Hình 2.13. Đặt tên lớp (tên file mã nguồn)


Trên Hình 2.14 là khung của lớp mới được tạo ra, chúng ta có thể viết
thêm mã nguồn vào để nhận được mã nguồn chương trình Ví dụ 2.1, từ khóa
public trên dòng “public class First” chúng ta nên giữa nguyên để thông báo cho
NetBeans biết rằng First là lớp chính của file First.java.
30

Hình 2.14. Khung mã nguồn được NetBeans tạo ra


Sau khi nhập mã nguồn chương trình Ví dụ 2.1, có thể nhất nút Run
Project hoặc phím tắt F6 hoặc chọn menu con Run Project trong menu Run để
chạy chương trình (Hình 2.15).

Hình 2.15. Viết code và chạy chương trình trong NetBeans


Kết quả chương trình được thể hiện trên cửa sổ con Output của NetBeans
(Hình 2.16).
31

Hình 2.16. Xem kết quả chạy chương trình qua cửa sổ Output
Có thể thấy rằng, quá trình soạn thảo mã nguồn và chạy chương trình
trong NetBeans rất thuận tiện. Trong các ví dụ tiếp theo, công việc tạo các file
mã nguồn sẽ được thực hiện theo trình tự như trên.
2.3.1. Phân tích chương trình đầu tiên
Để hiểu kỹ hơn về chương trình Ví dụ 2.1, chúng ta bắt đầu ở dòng đầu
tiên:
// This is a simple program called “First.java”
Ký hiệu “// ” dùng để thuyết minh dòng lệnh (thông báo ghi chú). Trình
biên dịch sẽ bỏ qua dòng thuyết minh này. Java còn hỗ trợ ghi chú nhiều dòng.
Loại ghi chú này có thể bắt đầu với /* và kết thúc với */. Kiểu ghi chú trong
Java giống như trong C++, ví dụ một số kiểu ghi chú:
/*This is a comment that
extends to two lines*/
/ *This is
a multi line
comment */
Dòng kế tiếp trong chương trình khai báo lớp có tên First. Một lớp được
khai báo bắt đầu bằng từ khóa class, kế đến là tên lớp (và cũng chính là tên phần
cơ sở của file chứa).
Tên lớp nói chung nên bắt đầu bằng chữ in hoa. Từ khoá class khai báo
32

định nghĩa lớp, First là định danh cho tên của lớp. Thân lớp được bao trong cặp
ngoặc móc đóng và mở ({ và }). Nội dung chi tiết về lớp được trình bày trong
Chương 3.
Trong lớp First có phương thức duy nhất:
public static void main(String args[ ])
Phương thức main luôn là phương thức chính và duy nhất của mọi ứng
dụng Java, từ đây chương trình bắt đầu việc thực thi của mình. Chúng ta sẽ tìm
hiểu từng từ trong dòng lệnh này.
Từ khoá public là một chỉ thị xác định phạm vi truy xuất của lớp. Nó cho
biết thành viên của lớp có thể được truy xuất từ bất cứ đâu trong chương trình
(project). Trong trường hợp này, phương thức main được khai báo public, nghĩa
là JVM có thể truy xuất phương thức này.
Từ khoá static cho phép main được gọi tới mà không cần tạo ra một thể
hiện (instance) của lớp. Trong trường hợp này, phương thức main được phép tồn
tại trên bộ nhớ, thậm chí nếu không có một thể hiện của lớp đó được tạo ra. Điều
này rất quan trọng vì JVM trước tiên gọi main để thực thi chương trình.
Vì lý do này phương thức main cần phải là tĩnh (static). Nó không phụ
thuộc vào các thể hiện của lớp được tạo ra.
Từ khoá void thông báo cho máy tính biết rằng phương thức sẽ không trả
lại bất cứ giá trị nào khi thực thi chương trình.
Phương thức main sẽ thực hiện một số tác vụ nào đó, nó là điểm mốc mà
từ đó tất cả các ứng dụng Java được khởi động.
String args[] là tham số dùng trong phương thức main. Các biến số trong
dấu ngoặc đơn nhận từng thông tin được chuyển vào main. Những biến này là
các tham số của phương thức. Thậm chí ngay khi không có một thông tin nào
được chuyển vào main, phương thức vẫn được thực hiện với các dữ liệu rỗng –
không có gì trong dấu ngoặc đơn.
Biến args[] là một mảng kiểu xâu ký tự kiểu String. Các tham số truyền
cho chương trình từ dòng lệnh được lưu vào mảng này.
Mã nằm giữa dấu ngoặc móc của main được gọi là thân phương thức. Các
câu lệnh được thực thi trong main cần được chỉ rõ trong khối này. Thân phương
thức main hiện chỉ chứa đúng một dòng lệnh:
System.out.println(“Hello World”);
Dòng lệnh này hiển thị xâu ký tự “Hello World” ra màn hình. Phương
thức println() đưa dữ liệu ra một cổng xuất (output). Phương thức này của đối
33

tượng System.out. Ở đây System là một lớp đã định trước, lớp này cho phép truy
nhập vào hệ thống và out là đối tượng luồng xuất chuẩn, ngầm định là luồng
xuất ra màn hình.
2.3.2. Truyền tham số cho chương trình qua dòng lệnh
Chúng ta tìm hiểu rõ thêm về khả năng truyền tham số vào chương trình
từ dòng lệnh qua mảng args[].Với khả năng này có thể tạo chương trình Java
giống như một lệnh của hệ điều hành và được gọi thông qua chương trình cmd.
Chương trình tại Ví dụ 2.2 cho thấy ba tham số xâu ký tự của dòng lệnh
được tiếp nhận như thế nào trong phương thức main.
Ví dụ 2.2. Chương trình in ra các tham số dòng lệnh:
class Pass{
public static void main(String args []){
System.out.println(“This is what the main method
received”);
System.out.println(args [0]);
System.out.println(args [1]);
System.out.println(args [2]);}}
Chúng ta lưu trữ chương trình trong thư mục “C:\Java” và biên dịch. Chạy
chương trình bằng lệnh java và nhập vào ba xâu ký tự “We”, “Change”, “Lives”
ngăn cách nhau bởi dấu cách trên cùng một dòng. Hình 2.17 thể hiện các tham
số được truyền tại dòng lệnh và được xử lý như thế nào.

Hình 2.17. Truyền tham số qua dòng lệnh cho phương thức main
Khi gặp một dấu cách (space), có thể hiểu một xâu ký tự được kết thúc.
Nhưng thông thường một xâu ký tự được kết thúc khi gặp dấu nháy kép. Hình
2.18 thể hiện điều này.
34

Hình 2.18. Truyền xâu ký tự có chứa dấu cách qua dòng lệnh
Với khả năng này hoàn toàn có thể tạo chương trình Java tạo, copy, xóa
file,... giống như những lệnh tương ứng của hệ điều hành.
Nếu xây dựng chương trình trong NetBeans IDE, cũng có thể truyền tham
số từ bên ngoài vào chương trình. Cách truyền như sau:
- Mở menu Run.
- Chọn menu con Set Project Configuration, chọn tiếp menu con
Customize.
- Trên cửa sổ mở ra, tại hộp văn bản Arguments ta nhập các tham số
muốn truyền, các tham số cách nhau bởi dấu cách (Hình 2.19). Ngoài việc nhập
tham số dòng lệnh, cửa sổ này còn cho phép nhập các tham số khác của chương
trình.

Hình 2.19. Nhập tham số dòng lệnh trong NetBeans


Sau khi nhập tham số xong, có thể chạy chương trình một cách bình
thường bằng NetBeans.
35

2.4. TỪ KHÓA, ĐỊNH DANH, BIẾN, HẰNG


2.4.1. Từ khóa
Java cũng giống như một ngôn ngữ lập trình bất kỳ, cung cấp một tập hợp
các từ khóa, gồm các nhóm:
- Từ khóa cho các kiểu dữ liệu cơ bản: byte, short, int, long, float, double,
char, boolean.
- Từ khóa cho câu lệnh lặp: do, while, for, break, continue.
- Từ khóa cho câu lệnh rẽ nhánh: if, else, switch, case, default, break.
- Từ khóa đặc tả đặc tính một method: private, public, protected, final,
static, abstract, synchronized.
- Hằng (literal): true, false, null.
- Từ khóa liên quan đến phương thức: return, void.
- Từ khoá liên quan đến package: package, import.
- Từ khóa cho việc xử lý lỗi: try, catch, finally, throw, throws.
- Từ khóa liên quan đến lập trình hướng đối tượng: new, extends,
implements, class, instanceof, this, super.
2.4.2. Định danh, biến, hằng
Định danh là dùng biểu diễn tên của biến, của phương thức, của lớp. Khi
khai báo định danh cần lưu ý các điểm:
- Mỗi định danh được bắt đầu bằng một chữ cái, một ký tự gạch dưới (_)
hay dấu đôla ($). Các ký tự tiếp theo có thể là chữ cái, chữ số, dấu $ hoặc một
ký tự được gạch dưới.
- Mỗi định danh chỉ được chứa hai ký tự đặc biệt, tức là chỉ được chứa
một ký tự gạch dưới và một ký tự dấu $. Ngoài ra không được phép sử dụng bất
kỳ ký tự đặc biệt nào khác.
- Các định danh không được sử dụng dấu cách “ ” (space).
- Java phân biệt chữ hoa và chữ thường giống như C, C++.
Ví dụ các định danh: Hello, _prime, var8, tvLang
2.4.3. Biến
Biến là vùng nhớ dùng để lưu trữ các giá trị trong chương trình mà có thể
thay đổi được. Mỗi biến gắn liền với một kiểu dữ liệu và một định danh duy
nhất gọi là tên biến.
Tên biến được đặt theo các quy tắc sau:
- Tên biến phải tuân thủ những ràng buộc như đối với định danh
- Tên biến thông thường là một chuỗi các ký tự (Unicode), ký số.
36

- Tên biến phải bắt đầu bằng một chữ cái, một dấu gạch dưới hay dấu
dollar ($).
- Tên biến không được trùng với các từ khóa (xem lại các từ khóa trong
java).
- Tên biến không có khoảng trắng ở giữa tên.
Ví dụ tên biến đúng: a, _a, A, _b, _B, $d, hoTen, _giaTri, sinhVien1,
sinhVien2; tên biến sai: 5a, hoc sinh, 1gia tri, if, try.
Trong Java, biến có thể được khai báo ở bất kỳ nơi đâu trong chương
trình. Việc khai báo một biến bao gồm ba thành phần: kiểu dữ liệu của biến, tên
của nó và giá trị ban đầu được gán cho biến (không bắt buộc). Để khai báo nhiều
biến, có thể sử dụng dấu phẩy để phân cách các biến. Khi khai báo biến, luôn
nhớ rằng Java phân biệt chữ thường và chữ in hoa (case-sensitive).
Cú pháp:
Datatype identifier [=value] [, identifier[=value]… ];
Ví dụ, để khai báo một biến nguyên (int) có tên là counter dùng để lưu giá
trị ban đầu là 1, ta có thể thực hiện phát biểu sau đây:
int counter = 1;
Có 3 loại biến:
- Biến địa phương (Local variables).
- Biến thể hiện (Instance variables).
- Biến của lớp/biến tĩnh (Class/Static variables).
Biến địa phương:
- Biến địa phương được khai báo trong phương thức, hoặc là khối lệnh.
- Biến địa phương được tạo khi các phương thức, hoặc là khối lệnh được
gọi và biến sẽ bị hủy khi thoát khỏi phương thức, hoặc khối lệnh.
- Không thể sử dụng các bộ điều chỉnh truy cập (Access Modifiers) cho
các biến địa phương.
- Biến địa phương chỉ có tầm vực hoạt động (tồn tại) trong phương thức,
hàm dựng hoặc các khối lệnh.
- Biến địa phương được lưu trữ ngầm ở tầng stack trong bộ nhớ máy tính.
- Không có giá trị khởi tạo mặc định cho các biến địa phương nên các
biến địa phương phải được gán giá trị trước khi dùng.
Ví dụ biến a trong đoạn mã sau là biến địa phương:
class Variable1{
public static void main(String args[]){
37

int a = 0;
}
}
Biến thể hiện (thuộc tính):
- Biến thể hiện được cài khai báo bên trong lớp nhưng bên ngoài bất kỳ
phương thức hay khối lệnh nào.
- Một đối tượng được lưu trữ trên vùng heap của bộ nhớ máy tính.
- Biến thể hiện được khởi tạo khi một đối tượng được khởi tạo với từ khóa
new và bị hủy khi một đối tượng bị hủy.
- Một biến thể hiện lưu trữ giá trị và giá trị đó có thể được tham chiếu bởi
nhiều phương thức hay khối lệnh.
- Biến thể hiện được khai báo bên trong lớp trước khi hoặc sau khi được
dùng.
- Bộ điều chỉnh truy cập (Access modifiers) có thể được dùng khi khai
báo biến thể hiện.
- Biến thể hiện khả dụng đối với tất cả các phương thức hoặc khối lệnh
trong lớp.
- Biến thể hiện có giá trị mặc định. Nếu là kiểu số thì giá trị mặc định là 0,
kiểu Boolean là false, và đối tượng là null. Giá trị có thể khởi tạo khi khai báo
hoặc ta thường khởi tạo trong phương thức khởi tạo.

- Biến thể hiện có thể được truy cập trực tiếp bằng cách gọi chúng bên
trong lớp.
Trong ví dụ sau, biến a là biến thể hiện hay a là thuộc tính của lớp
Variable2:
class Variable2{
int a;
public static void main(String args[]){

}

}
Biến của lớp/biến tĩnh:
- Biến của lớp còn được gọi là biến tĩnh, chúng được khai báo với từ khóa
static trong lớp, nhưng bên ngoài các phương thức, hàm dựng hoặc các khối
38

lệnh.
- Chỉ có một bản sao (copy) của mỗi biến tĩnh trong một lớp, bất kể bao
nhiêu đối tượng được tạo ra từ nó.
- Biến tĩnh ít được sử dụng, nó thường được sử dụng để khai báo hằng số
(constant). Hằng số là các biến được khai báo với bổ từ final. Biến hằng số
không bao giờ thay đổi giá trị của nó khác với giá trị khởi tạo ban đầu.
- Biến tĩnh được lưu trữ trong vùng bộ nhớ tĩnh.
- Biến tĩnh được tạo ra khi chương trình bắt đầu và bị hủy khi chương
trình kết thúc.
- Cách sử dụng tương tự như các biến thể hiện. Tuy nhiên, các biến tĩnh
thường được khai báo là public để ta có thể dùng chúng ở những lớp khác.
- Giá trị mặc định là tương tự như các biến thể hiện. Đối với các số giá trị
mặc định là 0, đối với kiểu boolean thì là false, và với đối tượng là null. Giá trị
có thể được chỉ định khi khai báo hoặc trong các hàm dựng. Ngoài ra các giá trị
có thể được chỉ định trong khối khởi tạo tĩnh (sẽ được giới thiệu sau).
- Biến tĩnh có thể được truy cập bằng cách gọi với tên lớp. Cú pháp:
Tên_lớp.Tên_biến_tĩnh.
- Khi khai báo biến tĩnh là public static final, thì nên đặt tên biến (hằng
số) là chữ viết hoa. Ví dụ biến TEN là biến (tĩnh) của lớp Variable3:
class Variable3{
public static final int TEN = 10;
public static void main(String args[]){..}..}
Các loại biến thể hiện và biến của lớp được trình bày chi tiết trong
Chương 3.
2.4.4. Hằng
- Hằng là một giá trị bất biến trong chương trình.
- Tên hằng được đặt theo qui ước giống như tên biến. Tuy nhiên hằng nên
được đặt tên bằng cách có các ký tự viết hoa.
- Hằng số nguyên: Trường hợp giá trị hằng ở dạng long ta thêm vào cuối
giá trị biến chữ “l” hay “L”. (ví dụ: 1L, 5L, 3L)
- Hằng số thực: Trường hợp giá trị hằng có kiểu float ta thêm tiếp vĩ ngữ
“f” hay “F”, còn kiểu số double thì ta thêm tiếp vĩ ngữ “d” hay “D”.
- Hằng Boolean: Java có hai hằng kiểu Boolean là true, false.
- Hằng ký tự: Là một ký tự đơn nằm giữa nằm giữa hai dấu ngoặc đơn. Ví
dụ: ‘a’: hằng ký tự a.
39

- Hằng xâu ký tự: Là tập hợp các ký tự được đặt giữa hai dấu nháy kép “”.
Một hằng xâu ký tự không có ký tự nào là một hằng xâu rỗng. Ví dụ hằng xâu
ký tự: “Hello Wolrd”.
Cú pháp khai báo hằng:
final Kiểu_dữ_liệu Tên_hằng = giá trị cần gán;
Ví dụ:
final int Y2K = 2000;
2.5. CÁC KIỂU DỮ LIỆU CƠ BẢN
Java cung cấp một vài kiểu dữ liệu. Chúng được hỗ trợ trên tất cả các nền
tảng. Ví dụ, dữ liệu loại int (số nguyên) của Java được thể hiện bằng 4 bytes
trong bộ nhớ của tất cả các loại máy có hỗ trợ chạy chương trình Java. Bởi vậy
các chương trình Java không cần phải thay đổi khi chạy trên các nền khác nhau.
Trong Java kiểu dữ liệu được chia thành hai loại:
- Các kiểu dữ liệu cơ sở (primitive)
- Các kiểu dữ liệu tham chiếu (reference)
2.5.1. Các kiểu dữ liệu cơ sở
Các kiểu dữ liệu cơ sở được phân chia ra kiểu dữ liệu luận lý (boolean),
kiểu ký tự (char) và kiểu số (Hình 2.18). Trong kiểu số có kiểu số nguyên gồm
các kiểu byte, short, int, long và kiểu số thực gồm các kiểu float, double.
- Kiểu luận lý hay còn gọi là kiểu logic (boolean): Kiểu này chỉ chứa hai
giá trị true và false.
Giá trị mặc định của biến kiểu luận lý: false . Biến kiểu boolean không
thể được chuyển thành kiểu nguyên và ngược lại.

Hình 2.18. Các kiểu dữ liệu cơ sở


- Kiểu char: Kiểu ký tự theo chuẩn Unicode. Kiểu này biểu diễn các ký tự
40

trong bảng mã Unicode, gồm 216 = 65536 ký tự khác nhau, có giá trị từ '\u0000'
đến '\uFFFF'.
Trong kiểu char một số hằng ký tự đặc biệt đã được định nghĩa trước giúp
cho việc điều khiển nhập xuất (Bảng 2.1).
- Kiểu số nguyên:
Bốn kiểu số nguyên khác nhau là: byte, short, int, long.
Biến kiểu byte có kích thước 1 byte, kiểu short có kích thước 2 byte, kiểu
int có kích thước 4 byte, kiểu long có kích thước 8 byte.
Kiểu mặc định của các biến số nguyên là kiểu int. Ví dụ:
int x = 0;
long y=100;
int a=1,b,c;
Bảng 2.1. Các ký tự đặc biệt
Ký tự Ý nghĩa
\b Xóa lùi (BackSpace)
\t Tab
\n Xuống hàng
\r Dấu về đầu dòng
\” Nháy kép
\’ Nháy đơn
\\ Số ngược
\f Đẩy trang
\uXXXX Ký tự Unicode có mã là
XXXX trong hệ cơ số 16.
Không thể chuyển biến kiểu int sang kiểu boolean và ngược lại như trong
ngôn ngữ C/C++. Ví dụ về việc không thể chuyển đổi kiểu số nguyên và kiểu
boolean:
boolean b = false;
if (b == 0){
System.out.println("Xin chao");
}
Lúc biên dịch đoạn chương trình trên, Java sẽ báo lỗi vì không được so
sánh biến kiểu boolean với giá trị kiểu int.
- Kiểu số thực: Gồm 2 kiểu float và double.
Kiểu float có kích thước 4 byte và giá trị mặc định là 0.0f.
41

Kiểu double có kích thước 8 byte và giá trị mặc định là 0.0d.
Khai báo và khởi tạo giá trị cho các biến kiểu dấu chấm động. Ví dụ:
float x = 100.0/7;
double y = 1.56E6;
Do có nhiều kiểu dữ liệu, nên khi gặp phải sự không tương thích kiểu dữ
liệu thì phải chuyển đổi kiểu dữ liệu cho biến hoặc biểu thức. Trong một biểu
thức nếu có nhiều dữ liệu có các kiểu khác nhau thì trước khi thực hiện việc tính
toán, tất cả các dữ liệu được chuyển kiểu tự động thành dữ liệu có kiểu rộng
nhất. Độ rộng của các kiểu dữ liệu được quy định như sau (Hình 2.19).

Hình 2.19. Độ rộng kiểu dữ liệu số


Ví dụ:
Kết quả biểu thức: 1+0.5+1.5E6 sẽ là một giá trị có kiểu double.
Nếu trong biểu thức có xâu ký tự thì tất cả các dữ liệu được tự động
chuyển thành xâu ký tự trước khi thực hiện tính toán.
Ví dụ:
Biểu thức “abc”+1+2.5+’a’ sẽ cho kết quả là “abc12.5a”
Kích thước của các kiểu dữ liệu được mô tả trong Bảng 2.2.
Bảng 2.2. Kích thước bộ nhớ của các kiểu dữ liệu cơ sở
Kiểu dữ Độ dài theo Phạm vi
liệu số bit
byte 8 -128 đến 127
char 16 ‘\u0000’ to ’u\ffff ’
boolean 1 “true” hoặc “false”
short 16 -32768 đến 32767
int 32 -2,147,483,648 đến +2,147,483,648
(-232…232 – 1)
42

Kiểu dữ Độ dài theo Phạm vi


liệu số bit
long 64 -9,223,372,036’854,775,808
đến +9,223,372,036’854,775,808
(-264…264 – 1)
float 32 -3.40292347E+38 đến +3.40292347E+38
double 64 -1,79769313486231570E+308
đến +1,79769313486231570E+308
Ngoài việc ép kiểu tự động, trong Java còn có thể ép kiểu tường minh.
Cú pháp ép kiểu:
<tên biến> = (kiểu_dữ_liệu) <tên_biến>;
Ví dụ:
float fNum = 2.2;
int iCount = (int) fNum; //iCount sẽ bằng 2.
Có 2 loại ép kiểu tường minh:
- Ép kiểu rộng (widening conversion): Từ kiểu nhỏ sang kiểu lớn (không
mất mát thông tin). Ví dụ ép kiểu biến int sang biến float.
- Ép kiểu hẹp (narrow conversion): Từ kiểu lớn sang kiểu nhỏ (có khả
năng mất mát thông tin). Ví dụ ép kiểu biến float sang biến int thì sẽ bị mất phần
lẻ.
2.5.2. Kiểu dữ liệu tham chiếu (reference)
Trong Java có ba kiểu dữ liệu tham chiếu (Bảng 2.3), các kiểu dữ liệu này
sẽ được trình bày chi tiết hơn trong những phần sau.
Bảng 2.3. Các kiểu dữ liệu tham chiếu
Kiểu dữ liệu Mô tả
Mảng (Array) Tập hợp các dữ liệu cùng loại.
Lớp (Class) Tập hợp các biến và các phương thức.
Giao diện Là một kiểu dữ liệu trừu tượng được tạo ra để bổ sung
(Interface) cho khả năng đa kế thừa trong Java.
2.6. TOÁN TỬ TRONG JAVA
Các toán tử cho phép kết hợp các biến, các đối tượng dữ liệu với nhau
thành các biểu thức. Java cung cấp nhiều dạng toán tử, gồm các loại sau:
- Toán tử số học.
- Toán tử thao tác trên dạng bit.
- Toán tử quan hệ.
43

- Toán tử luận lý.


- Toán tử điều kiện.
- Toán tử gán.
2.6.1. Toán tử số học
Các toán hạng của các toán tử số học phải ở dạng số. Một số toán tử số
học áp dụng được cho các toán hạng ký tự và xâu ký tự. Các toán tử số học được
liệt kê trong Bảng 2.4.
Bảng 2.4. Các toán tử số học
Toán Mô tả
tử
+ Toán tử cộng. Trả về giá trị tổng của hai toán hạng.
Ví dụ 5+3 trả về kết quả là 8.
- Toán tử trừ (gồm một ngôi và hai ngôi).
Toán tử trừ hai ngôi trả về hiệu giữa hai toán hạng, còn toán tử
trừ một ngôi trả về giá trị khác dấu của toán hạng. Ví dụ 5-3 có
kết quả là 2 và –10 trả về giá trị âm của 10.
* Toán tử nhân.
Trả về giá trị là tích hai toán hạng. Ví dụ 5*3 có kết quả là 15.
/ Toán tử chia.
Trả về giá trị là thương của phép chia.
Ví dụ 6/3 kết quả là 2.
Lưu ý: Nếu hai toán hạng là số nguyên thì toán tử trả về phần
thương nguyên.
% Phép lấy modulo (chia lấy phần dư).
Giá trị trả về là phần dư của phép toán chia.
Ví dụ 10%3 giá trị trả về là 1.
++ Toán tử tăng lên 1.
Tăng giá trị của biến lên 1. Ví dụ a++ tương đương với a= a+1.
-- Toán tử giảm xuống 1.
Giảm giá trị của biến 1 đơn vị. Ví dụ a-- tương đương với a=a-1.
+= Toán tử cộng và gán giá trị.
Cộng các giá trị của toán hạng bên trái vào toán hạng bên phải và
gán giá trị trả về vào toán hạng bên trái.
Ví dụ c+=a tương đương c=c+a.
-= Toán tử trừ và gán giá trị.
44

Toán Mô tả
tử
Trừ các giá trị của toán hạng bên trái vào toán hạng bên phải và
gán giá trị trả về vào toán hạng bên trái.
Ví dụ c-= a tương đương với c=c-a.
*= Toán tử nhân và gán.
Nhân các giá trị của toán hạng bên trái với toán hạng bên phải và
gán giá trị trả về vào toán hạng bên trái.
Ví dụ c *= a tương đương với c=c*a.
/= Toán tử chia và gán.
Chia giá trị của toán hạng bên trái cho toán hạng bên phải và gán
giá trị trả về vào toán hạng bên trái.
Ví dụ c /= a tương đương với c=c/a.
%= Toán tử chia lấy số dư và gán.
Chia giá trị của toán hạng bên trái cho toán hạng bên phải và gán
giá trị số dư vào toán hạng bên trái.
Ví dụ c%=a tương đương với c=c%a.
Ví dụ 2.3. Sử dụng các toán tử số học:
class ArithmeticOp {
public static void main(String args[]){
int p=5,q=12,r=20,s;
s=p+q;
System.out.println(“p+q is”+s);
s=p%q;
System.out.println(“p%q is”+s);
s*=r;
System.out.println(“s*=r is”+s);
System.out.println(“Value of p before operation is”+p);
p++;
System.out.println(“Value of p after operation is”+p);
double x=25.75,y=14.25,z;
z=x-y;
System.out.println(“x-y is” +z);
z-=2.50;
System.out.println(“z-=2.50 is “+z);
45

System.out.println(“Value of z before operation is”+z);


z--;
System.out.println(“Value of z after operation is”+z);
z=x/y;
System.out.println(“x/y is” +z);}}
Kết quả chạy chương trình:
p+q is 17
p%q is 5
s*=r is 100
Value of p before operation is 5
Value of p after operation is 6
x-y is 11.5
z-=2.50 is 9.0
Value of z before operation is 9.0
Value of z after operation is 8.0
x/y is 1.8070175438596492
2.6.2. Toán tử thao tác trên dạng bit
Các toán tử dạng bit cho phép thao tác với các dữ liệu nguyên thuỷ ở dạng
biểu diễn nhị phân (dạng các bit) của các dữ liệu đó. Các phép toán này được
thực hiện nhanh, ưu tiên, được hỗ trợ trực tiếp bởi vi xử lý, và được dùng để
điều khiển các giá trị dùng cho so sánh và tính toán. Toán tử bit thực hiện phép
tính dựa trên cơ sở đại số Bool. Các toán tử bit được liệt kê trong Bảng 2.5.
Bảng 2.5. Các toán tử trên dạng bit
Toán Mô tả
tử
~ Phủ định (NOT).
Trả về giá trị phủ định của một số. Thông thường ~x = -x-1. Ví
dụ a=10 thì ~a=-11.
& Toán tử AND.
Trả về giá trị là 1 nếu các toán hạng là 1 và 0 trong các trường
hợp khác. Ví dụ nếu a=5 (0101) và b=3 (0011) thì a&b trả về
giá trị 0001 (bằng 1 trong hệ thập phân).
| Toán tử OR.
Trả về giá trị là 1 nếu một trong các toán hạng là 1 và 0 trong
các trường hợp khác. Ví dụ nếu a=5 (0101) và b=3 (0011) thì
46

Toán Mô tả
tử
a|b trả về giá trị 0111 (bằng 7 trong hệ thập phân).
^ Exclusive OR (XOR).
Trả về giá trị là 1 nếu chỉ một trong các toán hạng là 1 và trả về
0 trong các trường hợp khác. Ví dụ nếu a=5 (0101) và b=3
(0011) thì a^b trả về giá trị 0110 (bằng 6 trong hệ thập phân).
>> Dịch sang phải.
Chuyển toàn bộ các bít của một số sang phải một số vị trí, giữ
nguyên dấu của số âm. Toán hạng bên trái là số bị dịch còn số
bên phải chỉ số vị trí mà các bít cần dịch.
Ví dụ x=37 tức là 00100101 vậy x>>2 sẽ là 00001001 (là 9).
Phép toán dịch phải tương đương với phép chia cho lũy thừa 2:
x>>3 tương đương với x/8 (x/23). Tuy nhiên phép toán dịch
được thực thi nhanh hơn so với phép chia.
<< Dịch sang trái.
Chuyển toàn bộ các bít của một số sang trái một số vị trí, giữ
nguyên dấu cuả số âm. Toán hạng bên trái là số bị dịch còn số
bên phải chỉ số vị trí mà các bít cần dịch.
Ví dụ x=37 tức là 00100101 vậy x<<2 sẽ là 0010010100 (là
148).
Phép toán dịch trái tương đương với phép nhân cho lũy thừa 2:
x<<3 tương đương với x*8 (x*23). Tuy nhiên phép toán dịch
trái cũng được thực thi nhanh hơn so với phép nhân.
Ví dụ 2.4 thể hiện sự tính toán với các toán tử bit đối với các số nguyên.
Ví dụ 2.4. Chương trình tính toán với các toán tử bit:
public class BitOp {
public static void main(String[] args) {
int a = 10, c=5, d=3, res;
res=~a;
System.out.println("~a is " +res);
res = c&d;
System.out.println("c&d is " +res);
res = c|d;
System.out.println("c|d is " +res);
47

res = c^d;
System.out.println("c^d is " +res);
res = 37>>2;
System.out.println("37>>2 is " +res);
res = 37<<2;
System.out.println("37<<2 is " +res);
}
}
Kết quả nhận được khi chạy:
~a is -11
c&d is 1
c|d is 7
c^d is 6
37>>2 is 9
37<<2 is 148
2.6.3. Toán tử quan hệ
Các toán tử quan hệ kiểm tra mối quan hệ giữa hai toán hạng. Kết quả của
một biểu thức có dùng các toán tử quan hệ là những giá trị boolean (logic
“Đúng” (true) hoặc “Sai” (false). Các toán tử quan hệ được sử dụng trong các
cấu trúc điều khiển. Bảng 2.6 mô tả các toán tử quan hệ.
Bảng 2.6. Các toán tử quan hệ
Toán Mô tả
tử
== So sánh bằng.
Toán tử này kiểm tra sự tương đương của hai toán hạng.
Ví dụ (a==b) trả về giá tri true nếu giá trị của a và b như
nhau.
!= So sánh khác.
Kiểm tra sự khác nhau của hai toán hạng.
Ví dụ (a!=b) trả về giá trị true nếu a khác b.
> Lớn hơn.
Kiểm tra giá trị của toán hạng bên phải lớn hơn toán hạng bên
trái hay không.
Ví du (a>b) trả về giá trị true nếu a lớn hơn b, ngược lại (nhỏ
hơn hoặc bằng), trả về false.
48

Toán Mô tả
tử
< Nhỏ hơn.
Kiểm tra giá trị của toán hạng bên phải có nhỏ hơn toán hạng
bên trái hay không.
Ví dụ (a<b) trả về giá trị true nếu a nhỏ hơn b, ngược lại (lớn
hơn hoặc bằng) trả về false.
>= Lớn hơn hoặc bằng.
Kiểm tra giá trị của toán hạng bên phải có lớn hơn hoặc bằng
toán hạng bên trái hay không.
Ví dụ (a>=b) trả về giá trị true nếu a lớn hơn hoặc bằng b,
ngược lại (nhỏ hơn) trả về false.
<= Nhỏ hơn hoặc bằng.
Kiểm tra giá trị của toán hạng bên phải có nhỏ hơn hoặc bằng
toán hạng bên trái hay không.
Ví dụ (a<=b) trả về giá trị true nếu a nhỏ hơn hoặc bằng b,
ngược lại (lớn hơn) trả về false.
Ví dụ 2.5. Minh họa toán tử quan hệ:
class RelationalOp {
public static void main (String args[]){
float a= 10.0F;
double b=10.0;
if (a= = b)
System.out.println(“a and b are equal”);
else
System.out.println(“a and b are not equal”);
}
}
Kết quả chương trình sẽ hiển thị:
a and b are not equal
Trong chương trình trên cả a và b là những số có dấu phẩy động, có giá trị
giống nhau (bằng 10). Tuy vậy chúng không phải là cùng một kiểu, a là kiểu
float còn b là kiểu double nên chúng không bằng nhau. Bởi vậy khi kiểm tra giá
trị của các toán hạng, cần lưu ý là kiểu dữ liệu cũng cần phải được tính đến.
49

2.6.4. Toán tử logic


Các toán tử logic làm việc với các toán hạng kiểu luận lý (boolean). Các
toán tử kiểu này được chỉ ra trong Bảng 2.7.
Bảng 2.7. Các toán tử logic
Toán Mô tả
tử
&& Và (AND)
Trả về một giá trị “Đúng” (true) nếu chỉ khi cả hai toán hạng
có giá trị true.
Ví dụ: Nếu a=4, b=6 thì (a>2) && (b<10) trả về giá trị true.
|| Hoặc (OR).
Trả về giá trị true nếu một toán hạng là true hoặc cả hai đều là
true. Trả về false chỉ khi cả hai toán hạng là false.
Ví dụ: Nếu a=4, b=6 thì (a>12) || (b<10) trả về giá trị true.
^ XOR
Trả về giá trị true nếu chỉ một trong các giá trị là true, các
trường hợp còn lại cho giá trị false.
! Toán tử một ngôi NOT. Chuyển giá trị từ true sang false và
ngược lại.
2.6.5. Toán tử điều kiện
Toán tử điều kiện là một loại toán tử đặc biệt vì nó gồm ba thành phần
cấu thành biểu thức điều kiện. Đây là toán tử ba ngôi duy nhất.
Cú pháp:
biểu_thức_1?biểu_thức_2: biểu_thức_3;
Ở đây:
- biểu_thức_1 là biểu thức điều kiện luận lý (Boolean), trả về giá trị true
hoặc false.
- biểu_thức_2 là giá trị trả về cho phép toán nếu biểu_thức_1 có giá trị
true.
- biểu_thức_3 là giá trị trả về cho phép toán nếu biểu_thức_1 có giá trị
false.
Ví dụ với lệnh gán x = (3>5)?5:8 thì x nhận giá trị 8.
Biểu thức của toán tử này hoàn toàn có thể chuyển về mệnh lệnh if thông
thường. Cách chuyển như sau:
Câu lệnh:
50

res = biểu_thức_1?biểu_thức_2: biểu_thức_3;


Tương đương với:
if (biểu_thức_1)
res = biểu_thức_2;
else
res = biểu_thức_3;
2.6.6. Toán tử gán
Toán tử gán (=) dùng để gán một giá trị vào một biến. Phép toán này trả
về giá trị được gán, vì vậy có thể gán giá trị cho nhiều biến cùng một lúc.
Ví dụ đoạn lệnh sau gán giá trị cho biến num và minh họa khả năng gán
liên tiếp giá trị cho các biến trên một dòng lệnh đơn.
int num = 20;
int p,q,r,s;
p=q=r=s=num;
Dòng lệnh cuối cùng được thực hiện từ phải qua trái. Đầu tiên giá trị ở
biến num được gán cho s, phép gán trả về giá trị num (s), sau đó giá trị của s
được gán cho r và cứ tiếp như vậy đối với p, q. Sau phép gán, các 4 biến p, q, r,
s đều có giá trị là 20.
2.6.7. Độ ưu tiên của các toán tử
Các biểu thức nói chung gồm nhiều toán tử. Độ ưu tiên quyết định thứ tự
thực hiện các toán tử trên các biểu thức. Bảng 2.8 liệt kê độ ưu tiên các toán tử
trong Java. Trong một biểu thức có nhiều toán tử, toán tử có độ ưu tiên lớn hơn
sẽ được thực hiện trước toán tử có độ ưu tiên nhỏ hơn. Nếu các toán tử có độ ưu
tiên bằng nhau thì thứ tự thực hiện tùy ý.
Trong một biểu thức có thể thay đổi thứ tự thực hiện của toán tử. Để thay
đổi, có thể sử dụng dấu ngoặc đơn (). Từng phần của biểu thức được giới hạn
trong ngoặc đơn được thực hiện trước tiên. Nếu sử dùng nhiều ngoặc đơn lồng
nhau thì toán tử nằm trong ngoặc đơn phía trong sẽ thực thi trước, sau đó đến
các vòng phía ngoài. Nhưng trong phạm vi một ngoặc đơn thì quy tắc thứ tự ưu
tiên vẫn giữ nguyên tác dụng.
Bảng 2.8. Độ ưu tiên toán tử
Độ ưu tiên Toán tử
1 Các toán tử đơn như +, -, ++, --
2 Các toán tử số học và các toán tử dịch như *, /, +, -, <<, >>
3 Các toán tử quan hệ như >, <, >=, <=, = =, !=
51

Độ ưu tiên Toán tử
4 Các toán tử logic và bit như &&, ||, &, |, ^
5 Các toán tử gán như =, *=, /=, +=, -=
2.7. CÁC CẤU TRÚC ĐIỀU KHIỂN
Tất cả các ngôn ngữ lập trình đều cung cấp các cơ chế cho phép cài đặt
(hiện thực hóa - implementation) một thuật toán bất kỳ. Trong thuật toán, có thể
có các cấu trúc rẽ nhánh, cấu trúc lặp. Những cấu trúc này được gọi chung là các
cấu trúc điều khiển. Java cung cấp các cấu trúc điều khiển khá đơn giản và hoạt
động giống như các cấu trúc tương ứng trong C/C++. Các cấu trúc điều khiển
gồm có:
- Các cấu trúc rẽ nhánh, bao gồm:
+ Cấu trúc điều kiện (if – else)
+ Cấu trúc lựa chọn switch - case
- Các cấu trúc lặp, bao gồm:
+ Vòng lặp while
+ Vòng lặp do - while
+ Vòng lặp for
- Các lệnh nhảy: break, continue
2.7.1. Cấu trúc điều kiện
Cấu trúc rẽ nhánh được hiện thực bởi câu lệnh if và if – else. Câu lệnh cho
phép kiểm tra kết quả của một biểu thức điều kiện và thực thi một thao tác phù
hợp trên cơ sở kết quả đó.
Cú pháp câu lệnh if:
if (conditon)
statement;
Ở đây condition là biểu thức Boolean, nếu biểu thức này trả về kết quả
true thì chương trình sẽ thực thi câu lệnh statement, ngược lại (trả về false)
chương trình sẽ bỏ qua câu lệnh statement và thực hiện câu lệnh tiếp theo.
Ví dụ, đoạn mã sau đây sẽ in ra màn hình xâu ký tự “x is 100” chỉ khi
biến x chứa giá trị 100:
if (x == 100)
System.out.println("x is 100");
Nếu muốn một khối nhiều lệnh được thực hiện trong trường hợp condition
là true, thì có thể sử dụng một cặp ngoặc nhọn { }:
52

if (x == 100) {
System.out.println ("x is ");
System.out.println( x);
}
Chúng ta cũng có thể chỉ định điều gì sẽ xảy ra nếu biểu thức điều kiện
không được thoả mãn bằng cách sử dụng từ khoá else đi kèm với if.
Cú pháp câu lệnh if-else:
if (conditon){
action 1 statements;
}
else{
action 2 statements;
}
Ở đây:
- condition: Biểu thức Boolean, biểu thức này trả về giá trị true hoặc
false;
- action 1: Các dòng lệnh được thực thi khi condition trả về là true;
- else: Từ khóa xác định các câu lệnh được thực hiện nếu condition trả về
giá trị false;
- action 2: Các câu lệnh được thực thi khi condition trả về giá trị false.
Ví dụ 2.6. Kiểm tra tính chẵn lẻ của một số và hiển thị thông báo phù
hợp. Một số là chẵn nếu số dư trong phép chia số đó cho 2 bằng 0:
class CheckNumber {
public static void main(String args[] {
int num =10;
if (num %2 = = 0)
System.out.println (num+ “is an even number”);
else
System.out.println (num +”is an odd number”);
}
}
Ở đoạn chương trình trên num được gán giá trị nguyên là 10. Trong câu
lệnh if-else điều kiện num %2 trả về giá trị 0 và điều kiện thực hiện là true. Do
đó thông báo “10 is an even number” được in ra. Trong chương trình chỉ có một
câu lệnh được viết trong đoạn if và else, bởi vậy không cần phải được đưa vào
53

dấu ngoặc móc.


Các câu lệnh if – else có thể lồng nhau. Chương trình trong Ví dụ 2.7
kiểm tra xem một điểm thi thang điểm 100 (testscore) thuộc mức xếp hạng nào
(A, B, C, D). Ở đây hạng A tương ứng với điểm thi từ 90 trở lên, hạng B tương
ứng với điểm từ 80 đến 89, hạng C tương ứng với điểm từ 70 đến 79, hạng D
tương ứng với điểm từ 60 đến 69, hạng F tương ứng với điểm nhỏ hơn 60. Để
xác định hạng, đầu tiên chương trình kiểm tra điểm có thuộc hạng A hay không,
nếu không thì kiểm tra với hạng B, sau đó với hạng C, D, F.
Ví dụ 2.7. Câu lệnh if-else lồng nhau:
class IfElseDemo {
public static void main(String[] args) {
int testscore = 76;
char grade;
if (testscore >= 90) {
grade = 'A';
} else if (testscore >= 80) {
grade = 'B';
} else if (testscore >= 70) {
grade = 'C';
} else if (testscore >= 60) {
grade = 'D';
} else {
grade = 'F';
}
System.out.println("Grade = " + grade);
}
}
Kết quả chạy chương trình:
Grade = C
2.7.2. Cấu trúc lựa chọn switch-case
Câu lệnh if – else trong trường hợp chuẩn cho phép chương trình có thể rẽ
theo hai nhánh. Nếu chúng ta muốn chương trình có thể rẽ theo nhiều nhánh (lựa
chọn) thì có thể sử dụng cấu lệnh switch - case. Câu lệnh này được sử dụng kèm
theo một biểu thức có thể trả về nhiều kết quả.
Cú pháp:
54

swich (expression) {
case value1: action 1 statement; break;
case value2: action 2 statement; break;
……………:
case valueN: actionN statement; break;
default: default - action }
Ở đây:
- expession: Biến chứa một giá trị xác định.
- value1,value 2,…, valueN: Các giá trị hằng số phù hợp với giá trị trên
biến mà expression có thể nhận.
- action1, action2,…, actionN: Các khối lệnh được thực thi khi expression
có giá trị tương ứng value1,value 2,…, valueN.
- break: Từ khoá được sử dụng để bỏ qua tất cả các câu lệnh sau đó và
giành quyền điều khiển cho cấu trúc bên ngoài switch. Từ khóa này không nhất
thiết phải sử dụng. Tuy nhiên nên sử dụng để chương trình không phải chạy tiếp
những trường hợp case sau (là những trường hợp không thể xảy ra).
- default: Từ khóa tuỳ chọn được sử dụng để chỉ rõ các câu lệnh nào được
thực hiện khi expression không nhận giá trị value1,value 2,…, valueN.
- default - action: Các câu lệnh được thực hiện khi expression không nhận
giá trị value1, value 2,..., valueN.
Câu lệnh switch có thể làm việc với biến và các giá trị case có kiểu: byte,
short, char, int, kiểu liệt kê, kiểu lớp String, các kiểu lớp bao Character, Byte,
Short, Integer.
Chương trình sau chuyển giá trị biến day là số thứ tự một ngày trong tuần
sang tên ngày dưới dạng xâu ký tự. Biến day phải có giá trị nằm trong khoảng từ
0 đến 6, chương trình sẽ thông báo lỗi nếu day nằm ngoài phạm vi này. Ở đây 0
tương ứng với Sunday (Chủ Nhật), 1 tương ứng với Monday (Thứ Hai)...
Ví dụ 2.8. Chương trình chuyển đổi số thứ tự sang tên ngày trong tuần:
class SwitchDemo {
public static void main(String agrs[]) {
int day =4;
switch(day) {
case 0: system.out.println(“Sunday”);
break;
case 1: System.out.println(“Monday”);
55

break;
case 2: System.out.println(“Tuesday”); break;
case 3: System.out.println(“Wednesday”);
break;
case 4: System.out.println(“Thursday”);
break;
case 5:System.out.println(“Friday”); break;
case 6:System.out.println(“Saturday”);
break;
default:System.out.println(“Invalid day of week”);
}
}
}
Kết quả chạy chương trình: Thursday
Nếu ta đổi day có giá trị 6 thì kết quả sẽ là Saturday.
2.7.3. Các cấu trúc lặp
Trong lập trình máy tính, vòng lặp được sử dụng khi cần phải thực hiện
một khối lệnh lặp đi lặp lại nhiều lần. Java cũng như C/C++, cung cấp các loại
vòng lặp sau với cú pháp cũng giống như trong C/C++:
- Vòng lặp for;
- Vòng lặp while;
- Vòng lặp do…while.
Cú pháp vòng lặp for:
for (initialization; Boolean_expression; update)
statement;
Luồng điều khiển trong một vòng lặp for gồm các bước như sau:
- Đoạn lệnh initialization (khởi tạo) được thực thi đầu tiên, và chỉ một lần.
Đoạn lệnh này có mục đích khai báo và khởi tạo (một hoặc nhiều) biến điều
khiển vòng lặp. Đoạn lệnh này có thể rỗng, tuy nhiên dấu chấm phẩy ‘;’ là bắt
buộc phải có;
- Sau đó, Boolean_expression (điều kiện lặp) được kiểm tra. Nếu nó là
true, khối lệnh statement được thực thi. Nếu nó là false, statement không được
thực thi và luồng điều khiển nhảy tới lệnh tiếp theo sau vòng lặp for;
- Sau khi thân vòng lặp thực thi, luồng điều khiển nhảy trở lại đoạn lệnh
update. Đoạn lệnh này cho phép cập nhật các biến điều khiển vòng lặp. Đoạn
56

lệnh này có thể để trống, miễn là có dấu chấm phảy xuất hiện sau
Boolean_expression;
- Boolean_expression lại được kiểm tra lần nữa. Nếu là true, vòng lặp lại
thực thi và tiến trình như trên lặp lại. Nếu Boolean_expression là false, vòng lặp
kết thúc.

Ví dụ 2.9. Chương trình in ra các số từ 10 đến 19 sử dụng vòng lặp for:


public class Test {
public static void main(String args[]) {
for (int x = 10; x < 20; x = x+1) {
System.out.print("Gia tri cua x: " + x );
System.out.print("\n");// Xuống dòng
}
}
}
Kết quả in ra màn hình:
Gia tri cua x: 10
Gia tri cua x: 11
Gia tri cua x: 12
Gia tri cua x: 13
Gia tri cua x: 14
Gia tri cua x: 15
Gia tri cua x: 16
Gia tri cua x: 17
Gia tri cua x: 18
Gia tri cua x: 19
Cú pháp vòng lặp while:
while(Boolean_ expression)
statement;
Ở đây:
- Boolean_ expression: Biểu thức Boolean, trả về giá trị true hoặc false.
Khối lệnh statement được thực hiện lặp khi mà Boolean_ expression còn trả về
giá trị true;
57

- statement: Khối lệnh (thân vòng lặp) được thực hiện nếu Boolean_
expression nhận giá trị true.
Ví dụ 2.10 thể hiện việc dùng vòng lặp while để tính giai thừa của số 5.
Giai thừa của 5 được tính như tích 5*4*3*2*1. Để tính giai thừa, chương trình
sử dụng một biến fact để chứa giá trị, ban đầu fact được khởi tạo giá trị 1, sau đó
sử dụng vòng lặp để nhân fact với biến a trong mỗi bước lặp.
Ban đầu a bằng 5 và sau mỗi bước lặp a giảm đi 1, vòng lặp kết thúc khi a
bằng 0. Với vòng lặp thế này thì fact sẽ có giá trị sau khi kết thúc vòng lặp là
5*4*3*2*1.
Ví dụ 2.10. Sử dụng vòng lặp while:
class WhileDemo {
public static void main(String args[]) {
int a = 5, fact = 1;
while (a>= 1) {
fact *=a;
a--;
}
System.out.println(“The Factorial of 5 is “+fact);
}
}
Ở ví dụ trên, vòng lặp được thực thi khi điều kiện (a>=1) là true. Biến a
được khai báo bên ngoài vòng lặp và được gán giá trị là 5. Sau mỗi lần lặp, giá
tri của a giảm đi 1. Sau năm lần lặp, giá trị của a bằng 0, điều kiện lặp (a>=1)
trở thành false và vòng lặp kết thúc. Kết quả được hiển thị là “The factorial of 5
is 120”.
Cú pháp vòng lặp do..while:
do
statement;
while (Boolean_ expression);
Vòng lặp do…while là tương tự như vòng lặp while, ngoại trừ khối lệnh
statement luôn được thực thi ít nhất một lần, không phụ thuộc vào Boolean_
expression có giá trị true hay false.
Ví dụ 2.11. In các số từ 10 đến 19 với vòng lặp do while:
public class Test {
public static void main(String args[]){
58

//Khai báo và gán giá trị ban đầu bằng 10 cho biến chạy
int x = 10;
do{
System.out.print("Gia tri cua x: " + x );
x++;
//Xuống dòng
System.out.print("\n");
}while( x < 20 );
}
}
Kết quả khi chạy:
Gia tri cua x: 10
Gia tri cua x: 11
Gia tri cua x: 12
Gia tri cua x: 13
Gia tri cua x: 14
Gia tri cua x: 15
Gia tri cua x: 16
Gia tri cua x: 17
Gia tri cua x: 18
Gia tri cua x: 19
2.7.4. Các lệnh nhảy break và continue
Câu lệnh break được sử dụng để dừng toàn bộ vòng lặp, break được sử
dụng bên trong bất kỳ vòng lặp nào hoặc trong câu lệnh switch.
Câu lệnh break sẽ dừng sự thực thi của vòng lặp trong cùng chứa nó và
thực thi dòng lệnh tiếp theo sau vòng lặp đó.
Cú pháp của một lệnh break là một lệnh đơn bên trong vòng lặp:
break;
Ví dụ 2.12. Chương trình với lệnh break. Chương trình gồm một vòng lặp
for với biến chạy i từ 1 đến 9. Khi thực hiện lặp, nếu điều kiện 10i=30 thì vòng
lặp bị dừng:
public class Test {
public static void main(String args[]) {
for(int i=1;i<10;i++ ) {
if (10*i == 30) {
59

break;
}
System.out.print( i );
System.out.print("\n");
}
}
}
Kết quả chạy chương trình như sau:
10
20

Câu lệnh continue có thể được sử dụng trong bất kỳ vòng lặp nào, có tác
dụng làm cho vòng lặp ngay lập tức tiếp tục lần lặp tiếp theo.
Trong một vòng lặp for, từ khóa continue làm luồng điểu khiển ngay lập
tức nhảy tới đoạn lệnh update.
Trong vòng lặp while hoặc do…while, luồng điều khiển ngay lập tức nhảy
tới Boolean_expression.
Cú pháp của continue là một lệnh đơn bên trông bất kỳ vòng lặp nào:
continue;
Ví dụ 2.13 thể hiện chương trình với lệnh continue. Chương trình gồm
một vòng lặp for với biến chạy x từ 1 đến 9. Mỗi bước lặp chương trình in ra giá
trị biến chạy. Khi thực hiện lặp, nếu điều kiện 10x=30 thì không in ra giá trị
biến chạy và vòng lặp chuyển sang bước tiếp theo.
Ví dụ 2.13. Chương trình với lệnh continue:
public class Test {
public static void main(String args[]) {
int [] numbers = {10, 20, 30, 40, 50};
for(int x =1;i<=5;i++) {
if( 10*x == 30 ) {
continue;//Bỏ qua không in
}
System.out.print( x );
//Xuống dòng
System.out.print("\n");
}
60

}
}
Kết quả chạy chương trình như sau:
10
20
40
50

Nhãn (label):
Không giống như C/C++, Java không hỗ trợ lệnh goto để nhảy đến một vị
trí nào đó của chương trình. Thay vào đó, Java kết hợp nhãn (label) với từ khóa
break hoặc continue để thay thế cho lệnh goto.
Cú pháp có thể như sau:
label:
for (…) {
for (…) {
if (<biểu thức điều kiện>)
//Thoát khỏi vòng lặp
break label;
else
continue label;
}
}
Ở đây, lệnh label: xác định vị trí của nhãn và xem như tên của vòng lặp
ngoài. Nếu <biểu thức điều kiện> đúng thì lệnh break label sẽ thực hiện việc
nhảy ra khỏi vòng lặp có nhãn là label, ngược lại sẽ tiếp tục vòng lặp có nhãn
label (khác với break và continue thông thường chỉ thoát khỏi hay tiếp tục vòng
lặp trong cùng chứa nó).
Mặc dù Java cung cấp khả năng kết hợp break và continue với nhãn, tuy
nhiên nên tránh sử dụng khả năng này. Việc sử dụng sẽ làm rối cấu trúc chương
trình.
TỔNG KẾT CHƯƠNG
Chương 2 đã giới thiệu chi tiết về các nội dung sau:
- Khi viết chương trình Java, có thể gọi các API được cung cấp sẵn. Các
API được chứa trong các lớp, các lớp được đóng gói trong các package. Để sử
61

dụng lớp hoặc gói, cần dùng từ khóa import.


- Chương trình Java được tổ chức thành các lớp. Một lớp thông thường
gồm có các thuộc tính và các phương thức xử lý các thuộc tính của lớp để nhận
được kết quả nào đó. Các lớp được lưu trữ trong các file có phần mở rộng
là.java, tên lớp trùng với phần cơ sở của tên file.
- Chương trình Java được tạo từ những đơn vị được gọi là “token”. Token
là đơn vị riêng lẻ, nhỏ nhất, có ý nghĩa đối với trình biên dịch của một chương
trình Java. Các “token” được chia thành năm loại: Định danh, Từ khoá/từ dự
phòng, Ký tự phân cách, Nguyên dạng, Các toán tử.
- Chương trình Java có thể được xây dựng theo hai cách: dùng trình soạn
thảo văn bản bất kỳ kết hợp với JDK, hoặc dùng IDE (NetBeans).
- Chương trình Java được biên dịch bởi chương tình javac và được thực
thi bởi chương trình java.
- Chương trình Java được thực thi bắt đầu từ phương thức main của một
lớp nào đó, phương thức main có tham số được truyền từ dòng lệnh.
- Java cung cấp một tập các từ khóa, quy tắc định danh, biến, hằng.
- Có 3 loại biến: Biến địa phương (Local variables), Biến thể hiện
(Instance variables), Biến của lớp/biến tĩnh (Class/Static variables).
- Trong Java kiểu dữ liệu được chia thành hai loại: Các kiểu dữ liệu cơ sở
(primitive) và các kiểu dữ liệu tham chiếu (reference).
- Các kiểu dữ liệu cơ sở được phân chia ra kiểu dữ liệu luận lý (boolean),
kiểu ký tự (char) và kiểu số. Trong kiểu số có kiểu số nguyên gồm các kiểu byte,
short, int, long và kiểu số thực gồm các kiểu float, double.
- Trong một biểu thức nếu có nhiều dữ liệu có các kiểu khác nhau thì
trước khi thực hiện việc tính toán, tất cả các dữ liệu được chuyển kiểu tự động
thành dữ liệu có kiểu rộng nhất.
- Các kiểu dữ liệu tham chiếu gồm có: Mảng (Array), Lớp (Class), Giao
diện (Interface).
- Java cung cấp các loại toán tử: Toán tử số học, Toán tử dạng bit, Toán tử
quan hệ, Toán tử luận lý, Toán tử điều kiện, Toán tử gán.
- Để cài đặt cấu trúc điều kiện, cần dùng if – else.
- Để cài đặt cấu trúc lựa chọn, có thể dùng switch – case.
- Để cài đặt cấu trúc lặp, có thể dùng while, do – while, for
- Để nhảy giữa các vị trí trong chương trình, có thể dùng break, continue.
BÀI TẬP
62

1. Viết chương trình để xuất dòng chữ sau ra màn hình:


“Welcome to the world of Java”
2. Giải phương tình bậc hai với ba hệ số a, b, c cho trước
3. Viết một chương trình hiển thị tổng các bội số của 7 nằm giữa 1 và 100.
4. Viết chương trình chuyển đổi một số tự nhiên cho trước ở hệ cơ số 10
thành số ở hệ cơ số b bất kì (1< b≤ 36).
5. Viết chương trình để cộng bảy số hạng của dãy sau:
1!+2!+3!…
6. In ra màn hình 10 số Fibonaci đầu tiên.
7. In ra màn hình các số nguyên tố từ 2 đến 100.
8. Viết chương trình phân tích một số nguyên cho trước thành các thừa số
nguyên tố. Ví dụ: Số 28 được phân tích thành 2*2*7.
9. Tính đại lượng sau với n cho trước:

2  4  ..  2(n  1)  2n

10. Tìm USCLN của hai số a, b cho trước.


11. Một số được gọi là số thuận nghịch độc nếu ta đọc từ trái sang phải
hay từ phải sang trái số đó ta vẫn nhận được một số giống nhau. Hãy liệt kê tất
cả các số thuận nghịch độc có sáu chữ số (Ví dụ số: 558855).
12. Viết chương trình liệt kê tất cả các hoán vị của 1, 2..., n với n cho
trước.
Câu hỏi ôn tập
1. Trong Java, kiểu dữ liệu dạng byte nằm trong giới hạn
từ……….đến…………
2. Hãy chỉ ra các định danh hợp lệ:
a. Tel_num
b. Emp1
c. 8678
d. batch.no
3. Đầu ra của chương trình sau là gì?
class me{
public static void main(String srgs[ ]){
int sales=820;
int profit=200;
System.out.println((sale +profit)/10*5);
}
63

}
4. Trong Java, chương trình bắt đầu chạy từ phương thức nào của lớp?
5. Phương thức System.out.println() dùng để làm gì?
6. Kiểu dữ liệu của từng hằng sau là gì?
a. 10 b. '2' c. 10.7 d. "12"
7. Tìm các lỗi có thể trong chương trình sau:
public static void main(String[] args){
/* This is the main method*/
System.out.println("Hello")
}
8. Nếu ban đầu x bằng 10, thì sau khi thực hiện các phép toán sau, x sẽ có
giá trị bao nhiêu:
a. ++x b. x++
9. Nếu x=10 và sum=0, thì giá trị của từng biểu thức sau là bao nhiêu:
a. sum=++x b. sum=x++
10. Nếu x=10 và y=5, thì giá trị của từng biểu thức sau là bao nhiêu:
a. ++x+y b. --x+y c. --x+(++y) d. --x+y--
11. Vòng lặp nào trong Java không kiểm tra điều kiện trước khi lặp?
a. for b. while c. do while
12. Thay thế vòng lặp while thành for cho đoạn chương trình sau mà
không làm thay đổi kết quả:
x=1;
while(x<10){
System.out.println(x+"\t");
++x;
}
13. Đầu ra của đoạn chương trình sau là gì?
x=10;
while(x>5){
System.out.println(x);
x--;
}
64

Chương 3
LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG TRONG JAVA
3.1. GIỚI THIỆU
Trong các chương trước, chúng ta đã biết rằng Java là ngôn ngữ lập trình
hướng đối tượng, chương trình Java được tạo thành từ các lớp hoặc các gói riêng
biệt. Trong chương trình, tất cả các mã, bao gồm các khai báo biến và các khai
báo hàm đều được thực hiện trong phạm vi các lớp. Ngoài các lớp được cung
cấp sẵn, Java còn cho phép lập trình viên xây dựng các lớp của riêng mình.
Chương này sẽ cung cấp cho chúng ta những khái niệm cơ bản trong lập trình
hướng đối tượng như lớp, đối tượng, phương thức, thuộc tính và những khả
năng của Java giúp cài đặt và sử dụng các khái niệm này.
3.2. KHÁI NIỆM LẬP TRÌNH HƯỚNG ĐỐI TƯỢNG
Lập trình hướng đối tượng (Object Oriented Programming – OOP) là kỹ
thuật lập trình hỗ trợ công nghệ đối tượng, nói một cách đơn giản là kỹ thuật cho
phép định nghĩa các đối tượng trong mã nguồn chương trình. Trong lập trình
hướng đối tượng, một chương trình được xem như một tập hợp các đối tượng,
các đối tượng này tương tác với nhau để thực hiện một chức năng nào đó của
chương trình. Mỗi đối tượng chứa các trạng thái cùng với các hành vi liên quan.
Các đối tượng trong chương trình thường được xây dựng dựa trên các đối tượng
thực ta thấy trong cuộc sống hàng ngày. Thế giới thực là thế giới các đối tượng
và chúng ta có thể tìm thấy xung quanh nhiều ví dụ về các đối tượng thực như
con mèo, cái bàn, cái quạt,... Lập trình hướng đối tượng có 4 tính chất chính sau:
- Tính trừu tượng (abstraction): Đây là khả năng cho phép định nghĩa
những kiểu dữ liệu trừu tượng (lớp, giao diện) trong chương trình. Lớp (class)
thực chất chính là kết quả của sự trừu tượng hóa (suy luận dựa trên dữ liệu quan
sát được) một đối tượng cụ thể, hay lớp là bản chất của một đối tượng cụ thể.
Các thuộc tính của lớp nhận được từ trừu tượng hóa dữ liệu của đối tượng, còn
hành vi (phương thức) của lớp nhận được từ sự trừu tượng hóa hành vi của đối
tượng.
- Tính đóng gói (encapsulation): Đây là khả năng cho phép chuyển một
lớp trong suy nghĩ của lập trình viên (trong thế giới thực) thành một kiểu dữ liệu
lớp trong chương trình. Ngoài ra, khi chuyển đổi còn cho phép lập trình viên có
thể thiết lập những phạm vi có thể truy cập vào các thuộc tính và phương thức
của lớp (cho phép hay không cho phép truy cập từ bên ngoài lớp). Khi không
cho phép truy cập từ bên ngoài lớp, nghĩa là lập trình viên đã che giấu thông tin
65

(information hiding) của lớp, giúp đảm bảo sự toàn vẹn cho đối tượng của lớp.
- Tính đa hình (polymorphism): Nghĩa là nhiều hình thái, thể hiện qua
việc một hành vi của đối tượng được hiện thực bởi các hành động khác nhau tùy
thuộc vào hoàn cảnh. Ví dụ, đối tượng người có hành vi “Chào nhau”, nhưng
nếu đối tượng người là người Việt thì hành vi này thực thi bởi hành động nói
câu “Xin chào”, còn nếu đối tượng người là người Anh thì hành động được thực
thi sẽ là nói câu “Hello”. Cái hay của tính đa hình là việc xác định hành động
cần thực thi diễn ra một cách tự động bởi trình biên dịch, và người lập trình
không cần phải chỉ định rõ ràng. Tính đa hình giúp cho việc lập trình trở nên
tổng quát hơn, đơn giản hơn. Tính đa hình được trình bày chi tiết trong Chương
4.
- Tính kế thừa (inheritance): Đây là khả năng cho phép xây dựng một lớp
mới trên cơ sở tái sử dụng một lớp đã có. Khi tái sử dụng, có thể bổ sung thêm
những thuộc tính hoặc phương thức mới, hoặc sửa lại (ghi đè) những cái đã có.
Tính chất này giúp việc lập trình trở nên nhanh chóng hơn. Tính kế thừa cũng
được trình bày chi tiết trong Chương 4.
3.3. KHÁI NIỆM ĐỐI TƯỢNG
Đối tượng là khái niệm trung tâm của OOP. Đối tượng trong OOP được
xây dựng dựa trên đối tượng trong thế giới thực. Đối tượng trong thế giới thực
có hai đặc tính chung:
- Trạng thái: là tổ hợp những giá trị cụ thể của các thuộc tính của đối
tượng;
- Hành vi: Là những hành vi mà đối tượng có thể thực hiện được.
Ví dụ, một đối tượng con mèo nào đó có ba thuộc tính (tên, màu, giống)
với trạng thái (tên: Tom; màu: Trắng; giống: Đực) và các hành vi (kêu, bắt
chuột, vẫy đuôi). Một đối tượng chiếc quạt có trạng thái (số hiện thời: 1, tốc độ
cánh quạt hiện thời: 100) và các hành vi (đổi số, tắt quạt). Xác định trạng thái và
hành vi cho các đối tượng thực là một cách tốt để bắt đầu suy nghĩ theo hướng
lập trình hướng đối tượng.
Các đối tượng trong OOP về mặt khái niệm giống như các đối tượng thực:
cũng bao gồm trạng thái và hành vi liên quan. Một đối tượng lưu trữ trạng thái
của nó trong các trường (các biến) và bộc lộ hành vi của nó thông qua các
phương thức (các hàm). Các phương thức hoạt động dựa trên trạng thái nội tại
của đối tượng và đóng vai trò là cơ chế căn bản cho việc giao tiếp giữa đối
tượng với đối tượng.
66

Xét ví dụ một chiếc quạt, đối tượng quạt có thể có thuộc tính “số hiện
thời” với ba trạng thái có thể 0, 1, 2, 3 và các phương thức “tắt”, “đổi số”.
Phương thức tắt quạt chỉ thực hiện được khi trạng thái “số hiện thời” khác
0 (trạng thái cánh quạt đang quay), sau khi thực hiện phương thức này, “số hiện
thời” trở về 0 (trạng thái cánh quạt dừng quay). Phương thức “đổi số” cho phép
chuyển “số hiện thời” hiện tại sang một trạng thái khác cho trước.
Trong OOP có thể có những đối tượng không có thuộc tính hoặc không có
phương thức.
3.4. LỚP
3.4.1. Khái niệm
Lớp là khái niệm luôn đi kèm với đối tượng, trong khi đối tượng là một
cái gì đó cụ thể thì lớp là khái niệm trừu tượng, phản ánh tập hợp các đối tượng
có cùng bản chất (cùng thuộc tính, cùng hành vi).
Lớp trong OOP cũng giống nhưng lớp trong thế giới thực. Trong thế giới
thực, chúng ta có thể thấy có lớp người, lớp động vật, lớp mèo, lớp quạt (là tập
hợp các quạt có cùng nhà sản xuất và model).
Trong OOP, một lớp (class) là một bản thiết kế hay một khuôn mẫu để
tạo các đối tượng, còn đối tượng là một thể hiện cụ thể của lớp. Lớp và đối
tượng có mối quan hệ như sau:
- Lớp và đối tượng đều có các thuộc tính và hành vi. Tuy nhiên trong đối
tượng thì các thuộc tính có giá trị cụ thể, còn trong lớp thì các thuộc tính chỉ là
các biến.
- Lớp là kiểu dữ liệu, còn đối tượng là biến của kiểu dữ liệu đó.
- Lớp là sự trừu tượng hóa hay tổng quát hóa của đối tượng, còn đối tượng
là một thể hiện cụ thể của lớp.
Trong OOP, có thể hiểu đơn giản lớp là một cấu trúc đặc biệt chứa các
biến dữ liệu và các hàm xử lý các biến dữ liệu đó. Lớp trong OOP là sự phát
triển của khái niệm cấu trúc (ví dụ như kiểu struct trong ngôn ngữ C).
3.4.2. Khai báo các lớp
Java cho phép lập trình viên xây dựng các lớp của riêng mình. Để khai
báo và định nghĩa lớp, chúng ta dùng từ khóa class. Cú pháp có dạng như sau:
class TenLop {
//Các khai báo trường, constructor (phương thức khởi tạo);
//và các phương thức khác;
}
67

Trong khai báo lớp, thân của lớp là phần nằm giữa hai dấu ngoặc nhọn.
Thân của lớp có chứa khai báo các constructor, các trường, các phương thức.
Constructor được dùng để khởi tạo tự động giá trị ban đầu cho đối tượng mới.
Các khai báo trường xác định các thuộc tính (biến) của các đối tượng
thuộc lớp này, các phương thức để cài đặt hành vi của các đối tượng của lớp.
Khi khai báo các trường, có thể gán giá trị ngầm định cho chúng. Các phương
thức được khai báo và định nghĩa như những hàm bình thường.
Khi khai báo lớp, có thể bổ sung thêm các bộ điều chỉnh (modifier) như
public, protected hoặc private ở trước từ khóa class, các bộ điều chỉnh này xác
định phạm vi truy cập của lớp trong các gói (package) (Trong Java, các lớp được
đóng gói vào trong các gói). Các bộ điều chỉnh của lớp sẽ được trình bày chi tiết
trong mục 3.5.3.
Nếu chúng ta sử dụng IDE, ví dụ như NetBeans để lập trình project Java,
thì việc tạo và khai báo một lớp rất đơn giản. Cách tạo mới một lớp đã được
trình bày trong mục 2.3 với các hình minh họa từ 2.9 đến 2.14.
Một project Java có thể có nhiều lớp, các lớp được chứa trong các file có
phần mở rộng.java. Trong một file có thể định nghĩa nhiều lớp, nhưng chỉ có
một lớp có bộ điều chỉnh public đi kèm, lớp này được coi là lớp chính của file
và có tên trùng với phần cơ sở của tên file.
Ví dụ 3.1. Khai báo lớp Quat (quạt) gồm hai thuộc tính so (số), tocDo (tốc
độ) và các phương thức datSo (đặt số), datTocDo (đặt tốc độ), tatQuat (tắt quạt),
inTrangThai (in ra màn hình trạng thái của quạt):
class Quat {
int so = 0;
int tocDo = 0;
void datSo(int soMoi) {
so = soMoi;
}
void datTocDo(int tocDoMoi) {
tocDo = tocDoMoi;
}
void tatQuat() {
so = 0; tocDo = 0;
}
void inTrangThai() {
68

System.out.println("so:" + so + " toc do canh quat:" +


tocDo);
}
}
Có thể thấy ở ví dụ trên, các phương thức của lớp Quat có nhiệm vụ tác
động lên các thuộc tính của lớp.
3.4.3. Khai báo các thuộc tính
Các thuộc tính, hay các trường của lớp chính là các biến (biến đối tượng
hay còn được gọi là biến thể hiện), do đó, các thuộc tính được khai báo giống
như khai báo biến. Trong Ví dụ 3.1, lớp Quat có hai thuộc tính so và tocDo.
Khi khai báo các thuộc tính, có thể chỉ định bộ điều chỉnh phạm vi truy
cập public, protected, private hoặc để trống (default modifier) như trong Ví dụ
3.1.
Cú pháp khai báo thuộc tính trong một lớp:
bộ_điều_chỉnh_truy_cập Kiểu_dữ_liệu tên_thuộc_tính;
Ví dụ, trong lớp Quat, có thể dùng các dòng lệnh sau để định nghĩa các
thuộc tính:
public int so;
public int tocDo;
Các khai báo thuộc tính được cấu thành bởi ba thành phần:
- Bộ điều chỉnh phạm vi truy cập (public, protected, private);
- Kiểu của thuộc tính;
- Tên của thuộc tính.
Các bộ điều chỉnh truy cập
Bộ điều chỉnh truy cập nằm ở vị trí trái nhất trong khai báo thuộc tính,
cho phép điều khiển khả năng truy cập đến thuộc tính đó. Hiện tại, ta chỉ xem
xét các bộ điều chỉnh public, private, để trống (default modifier):
- Bộ điều chỉnh public cho phép thuộc tính có thể được truy cập từ tất cả
các lớp.
- Bộ điều chỉnh private chỉ định thuộc tính chỉ được truy cập ở bên trong
thân lớp.
- Bộ điều chỉnh để trống (default modifier) cho phép thuộc tính có thể
được truy cập từ tất cả các lớp trong cùng một gói với lớp của thuộc tính. Phạm
vi của default modifier hẹp hơn public, protected nhưng rộng hơn private.
Theo tinh thần đóng gói trong OOP, để bảo đảm tính đúng đắn của dữ
69

liệu, các thuộc tính thường được định nghĩa đi kèm với bộ điều chỉnh truy cập
private. Nghĩa là các thuộc tính nên định nghĩa chỉ có thể truy cập ở bên trong
lớp (không cho phép truy cập từ bên ngoài lớp một cách trực tiếp). Việc truy cập
đến các thuộc tính từ bên ngoài lớp sẽ được thực hiện một cách gián tiếp qua các
phương thức có bộ điều chỉnh public, protected hoặc để trống (default modifier).
Thông thường, để làm việc với các thuộc tính private, cần một cặp
phương thức: một để lấy (đọc) giá trị thuộc tính và một để thiết lập giá trị cho
thuộc tính. Thông qua các phương thức, chúng ta có thể đảm bảo được tính đúng
đắn của dữ liệu các thuộc tính.
Ví dụ, trong đoạn mã sau với lớp Quat, thuộc tính so có thể được trả về
bằng phương thức laySo và được thiết lập bằng phương thức datSo. Trong thực
tế, thuộc tính so chỉ có thể nhận giá trị từ 0 đến 3, thông qua việc thiết lập gián
tiếp bằng phương thức datSo, chúng ta có thể đảm bảo được điều này:
public class Quat {
private int so;
private int tocDo;
public int laySo() {
return so;
}
public void datSo(int soMoi) {
if ((soMoi < 0) || (soMoi>3))
so = 0;
else
so = soMoi;
}

}
Các kiểu
Tất cả các thuộc tính phải thuộc một kiểu dữ liệu nào đó. Có thể sử dụng
các kiểu dữ liệu cơ bản như int, float, boolean,…, hoặc có thể dùng các kiểu
tham chiếu như Integer, Float, Object,....
Tên thuộc tính
Tên các thuộc tính được đặt theo quy tắc định danh.
Tên lớp và phương thức cũng được đặt theo quy tắc định danh như thuộc
tính, tuy nhiên tên lớp nên có chữ cái đầu là chữ hoa, và từ đầu tiên trong tên
70

phương thức nên là một động từ.


3.4.4. Định nghĩa phương thức
Phương thức của lớp là những hàm, dùng thể tác động lên các thuộc tính
của lớp hoặc các tham số truyền vào.Ví dụ định nghĩa một phương thức:
public double tinhDienTichHinhThang(double dayLon, int dayNho,
double chieuCao) { tính toán được thực hiện ở đây }
Các phần cần có của một định nghĩa phương thức: Kiểu trả về của phương
thức, tên phương thức cùng một cặp ngoặc tròn (), và thân của phương thức đặt
giữa hai dấu ngoặc nhọn {}. Tổng quát hơn, một định nghĩa phương thức có sáu
thành phần, theo thứ tự từ trái sang phải:
1. Các bộ điều chỉnh (modifier): Ví dụ như public, private, và các bộ điều
chỉnh khác sẽ được trình bày sau. Ý nghĩa của public và private khi kết hợp với
phương thức cũng giống như kết hợp với thuộc tính. Nếu phương thức có nhiều
bộ điều chỉnh thì các bộ điều chỉnh được viết liên tiếp nhau, ví dụ public static;
2. Kiểu dữ liệu trả về (Type): Là kiểu dữ liệu của giá trị trả về của phương
thức khi phương thức thực thi xong, hoặc void nếu phương thức không trả về giá
trị;
3. Tên phương thức (method_name): Được đặt tên theo quy tắc định danh;
4. Các tham số được liệt kê trong cặp dấu ngoặc tròn “( )”: Một danh sách
các tham số đầu vào được phân cách bởi dấu phảy, mỗi tham số đều phải có kiểu
dữ liệu đi kèm phía trước. Nếu không có tham số, vẫn phải chỉ định cặp dấu
ngoặc tròn trống;
5. Một danh sách ngoại lệ có thể xảy ra khi phương thức thực thi. Các
ngoại lệ được đề cập đến trong Chương 8;
6. Thân của phương thức nằm trong cặp dấu ngoặc nhọn { }: Là đoạn mã
bao gồm khai báo các biến cục bộ. Đoạn mã chứa các chỉ thị lệnh để thực hiện
một nhiệm vụ nào đó.
Cú pháp tổng quát:
modifiers Type method_name(Type1 parameter1,..) Exception1,…{
statements;
}
Ví dụ:
public int getPerimeter(int a, int b, int c) IOException{
return a+b+c;
}
71

Khi định nghĩa phương thức, mặc dù tên của phương thức có thể là bất kỳ
định danh nào, nhưng để lập trình chuyên nghiệp, chúng ta nên quy ước giới hạn
việc đặt tên các phương thức. Thông thường, trong trường hợp tên phương thức
cấu thành bởi một từ, thì từ đó nên là một động từ và viết bằng chữ thường; còn
trong trường hợp tên cấu thành từ nhiều từ, thì từ bắt đầu nên là một động từ viết
bằng chữ thường theo sau là các tính từ, hoặc danh từ,…chữ cái đầu của từ thứ
hai và các từ phía sau nên là chữ hoa.
Ví dụ việc đặt tên phương thức:
chay(); // (chạy)
soSanh(); // (so sánh)
layMauNetVe(); // (lấy màu nét vẽ)
datSo(); // (đặt số)
Thông thường, một phương thức có một tên duy nhất trong một lớp. Tuy
nhiên, một phương thức cũng có thể có cùng tên với các phương thức khác nhờ
khả năng nạp chồng phương thức của Java.
3.4.5. Nạp chồng phương thức
Ngôn ngữ lập trình Java cho phép nạp chồng phương thức, nghĩa là cho
phép định nghĩa các phương thức trùng tên nhau. Java có thể phân biệt các
phương thức thông qua tên phương thức và danh sách các kiểu dữ liệu của tham
số. Nghĩa là các phương thức trong cùng một lớp có thể có cùng tên nếu chúng
có danh sách các kiểu dữ liệu tham số khác nhau.
Giả sử rằng chúng ta muốn xây dựng một lớp có thể dùng vẽ các đối
tượng hình học như: điểm (xác định bằng tọa độ của đối tượng điểm và độ lớn
của điểm này trên màn hình), hình chữ nhật (xác định bằng tọa độ góc trái trên,
chiều dài và chiều rộng), hình tròn (xác định bằng tọa độ tâm và bán kính).
Thông thường, để vẽ mỗi đối tượng, chúng ta định nghĩa một phương thức
tương ứng, ví dụ như veDiem(), veHinhChuNhat(), veHinhTron(). Tuy nhiên,
cách làm này khiến mã của lớp khá cồng kềnh và khó nhớ. Với khả năng nạp
chồng phương thức, có thể dùng cùng tên cho tất cả các phương thức vẽ, nhưng
truyền vào một danh sách tham số có kiểu khác nhau cho mỗi phương thức. Do
đó, lớp vẽ các đối tượng hình học có thể khai báo bốn phương thức có tên là
veHinh(), mỗi phương thức có một danh sách tham số có kiểu riêng như trong
đoạn chương trình sau:
public class LopVeHinh {
//phương thức vẽ điểm
72

public void veHinh(double x, double y, int doLonCuaDiem) {... }


//phương thức vẽ hình chữ nhật
public void veHinh(double x, double y, double chieuDai, double
chieuRong) {... }
//phương thức vẽ hình tròn
public void veHinh(double x, double y, double banKinh) {...
}
}
Các phương thức nạp chồng được phân biệt bằng số lượng và kiểu các
tham số được truyền cho phương thức. Trong đoạn chương trình trên, phương
thức vẽ điểm veHinh(double x, double y, int doLonCuaDiem) và vẽ hình tròn
veHinh(double x, double y, double banKinh) là các phương thức khác nhau bởi
vì chúng có danh sách các kiểu tham số khác nhau.
Khi nạp chồng, không thể khai báo nhiều hơn một phương thức với cùng
tên và cùng danh sách các kiểu dữ liệu tham số, bởi vì trình dịch không thể phân
biệt được chúng.
Trình dịch không xem xét kiểu trả về khi phân biệt các phương thức, bởi
vậy không thể khai báo hai phương thức với cùng tên và danh sách kiểu dữ liệu
tham số nhưng khác kiểu trả về.
Khi dịch chương trình, trình dịch xác định kiểu dữ liệu của các giá trị cụ
thể của tham số truyền cho phương thức để chọn phương thức nạp chồng cần
thực thi. Tuy nhiên cũng có trường hợp nhập nhằng khi xác định phương thức
thực thi, nên cần phải cẩn thận khi định nghĩa phương thức nạp chồng và giá trị
tham số truyền cho phương thức.
Chú ý: Nên hạn chế định nghĩa nạp chồng các phương thức, vì chúng có
thể làm chương trình khó đọc.
Một số ví dụ về nạp chồng phương thức:
- Nạp chồng phương thức có số lượng tham số khác nhau:
class Calculation{
static void sum(int a,int b){
System.out.println(a+b);
}
static void sum(int a,int b,int c){
System.out.println(a+b+c);
}
73

public static void main(String args[]){


sum(10,10,10);
sum(20,20);
}
}

Kết quả chạy:


30
40
- Nạp chồng với các kiểu dữ liệu khác hẳn nhau đôi một:
class Calculation2{
static void sum(int a,int b){System.out.println(a+b);}
static void sum(double a,double b){System.out.println(a+b);}
public static void main(String args[]){
sum(10.5,10.5);
sum(20,20);
}
}
Kết quả chạy:
21.0
40
- Lỗi nạp chồng khi hai phương thức chỉ khác nhau về kiểu dữ liệu trả về:
class Calculation3{
static int sum(int a,int b){System.out.println(a+b);}
static double sum(int a,int b){System.out.println(a+b);}
public static void main(String args[]){
int result=obj.sum(20,20); //Compile Time Error
}
}
Kết quả chạy: Lỗi biên dịch
- Có thể nạp chồng cả phương thức main():
class Overloading1{
public static void main(int a){
System.out.println(a);
}
74

public static void main(String args[]){


System.out.println("main() method invoked");
main(10);
}
}

Kết quả chạy:


main() method invoked
10
- Nạp chồng khi có nhiều phương thức đáp ứng kiểu dữ liệu của giá trị
tham số truyền vào. Trong trường hợp này, kiểu dữ liệu có độ lớn nhỏ nhất được
lựa chọn. Ví dụ:
class OverloadingCalculation2{
static void sum(int a,int b){
System.out.println("int arg method invoked");
}
static void sum(long a,long b){
System.out.println("long arg method invoked");
}
public static void main(String args[]){
sum(20,20);/phương thức sum(int, int) được gọi
}
}
Kết quả chạy:
int arg method invoked
- Lỗi nhập nhằng khi truyền giá trị cho tham số:
class OverloadingCalculation3{
static void sum(int a,long b){
System.out.println("a method invoked");
}
static void sum(long a,int b){
System.out.println("b method invoked");
}
public static void main(String args[]){
75

sum(20,20);//nhập nhằng,
//trình dịch không xác định được phương thức thực thi
}
}
Kết quả chạy: Lỗi biên dịch
3.4.6. Phương thức khởi tạo - Constructor
Constructor hay phương thức khởi tạo là một phương thức đặc biệt của
lớp, có vai trò khởi tạo giá trị ban đầu (của các thuộc tính) cho đối tượng của
lớp. Các constructor cũng được định nghĩa giống như các phương thức thông
thường, chỉ có điểm khác là chúng có tên trùng với tên lớp và không có kiểu trả
về.
Ví dụ, trong đoạn mã sau, lớp Quat có một constructor có hai tham số
Quat(int so1, int tocDo1):
public class Quat {
private int so;
private int tocDo;
public Quat(int so1, int tocDo1) {
so = so1;
tocDo = tocDo1;
}
public int laySo() {
return so;
}
public void datSo(int soMoi) {
if ((soMoi < 0) || (soMoi>3))
so = 0;
else
so = soMoi;
}

}
Trong đoạn mã trên, constructor Quat(int so1, int tocDo1) có nhiệm vụ
khởi tạo giá trị ban đầu cho hai thuộc tính so, tocDo bởi hai tham số truyền vào.
Constructor được gọi thông qua toán tử new khi tạo đối tượng. Ví dụ, để khai
báo và tạo một đối tượng quat1 của lớp Quat và gán giá trị ban đầu (3,220) cho
76

các thuộc tính (so, tocDo), có thể sử dụng constructor trên như sau:
Quat quat1 = new Quat(3, 220);
Dòng lệnh trên sẽ thực hiện các hoạt động sau:
- Khai báo biến tham chiếu quat1;
- Tạo vùng bộ nhớ với kích thước đủ lớn để chứa đối tượng lớp Quat;
- Kích hoạt constructor có hai tham số kiểu (int, int) để khởi tạo giá trị
cho đối tượng vừa tạo ra;
- Trỏ tham chiếu quat1 đến vùng bộ nhớ chứa đối tượng đã tạo ra;
Constructor, giống như các phương thức khác, cũng có thể được nạp
chồng. Ví dụ, ngoài constructor có hai tham số ở trên, trong lớp Quat có thể
định nghĩa các constructor khác, bao gồm constructor không tham số:
public Quat() { so = 0; tocDo = 0;}
Constructor này có thể được kích hoạt bởi toán tử new như sau:
Quat quat2 = new Quat();
Khi định nghĩa một lớp, có thể không cần phải cung cấp bất kỳ
constructor nào cho lớp của mình, nhưng phải cẩn thận khi làm điều này. Trình
dịch tự động cung cấp một constructor mặc định không tham số cho bất kỳ lớp
nào không có constructor. Constructor của lớp sẽ tự động gọi đến constructor
không tham số của lớp cơ sở nếu không có lời gọi đến một constructor cụ thể
của lớp cơ sở. Trong trường hợp này, trình dịch sẽ báo lỗi nếu lớp cơ sở không
có constructor không tham số. Nếu lớp đang được định nghĩa không có lớp cơ
sở rõ ràng, thì nó luôn có một lớp cơ sở ẩn Object, và lớp này có một constructor
không tham số. Nghĩa là, khi định nghĩa một lớp không có constructor và không
thừa kế từ một lớp nào thì trình dịch sẽ tự động cung cấp một constructor không
tham số cho lớp đó và constructor này sẽ gọi đến constructor không tham số của
lớp Object. Một đặc điểm của Java là tất cả các lớp trong Java đều thừa kế tự
động từ lớp Object. Các nội dung về thừa kế, về constructor lớp cơ sở sẽ được
trình bày trong Chương 4.
Khi định nghĩa constructor, hoàn toàn có thể kết hợp với các bộ điều
chỉnh truy cập. Tuy nhiên chỉ nên sử dụng bộ điều chỉnh public. Chú ý là: nếu
một lớp không thể gọi constructor của lớp LopX, thì nó không thể trực tiếp tạo
ra các đối tượng thuộc lớp LopX.
Các nội dung về sử dụng constructor để tạo đối tượng được trình bày chi
tiết trong mục 3.5.
3.5. LẬP TRÌNH VỚI CÁC ĐỐI TƯỢNG
77

3.5.1. Chương trình với các đối tượng


Ở phần trên, chúng ta đã nắm bắt được khái niệm lớp cũng như việc xây
dựng lớp. Trong mục này, chúng ta sẽ làm quen các vấn đề tạo và sử dụng đối
tượng của lớp.
Một chương trình Java thông thường tạo ra nhiều đối tượng, các đối tượng
này tương tác với nhau thông qua việc gọi các phương thức. Thông qua các
tương tác giữa các đối tượng, một chương trình có thể thực hiện nhiều chức
năng khác nhau theo mong muốn của người lập trình. Khi một đối tượng hoàn
thành công việc của nó (theo lý do mà nó đã được tạo ra), các tài nguyên của nó
(bộ nhớ mà đối tượng sử dụng) được thu hồi để sử dụng cho mục đích khác.
Trong Ví dụ 3.2 là chương trình đơn giản tạo ra ba đối tượng, một đối
tượng của lớp Diem (Điểm) và hai đối tượng của lớp HinhChuNhat (Hình chữ
nhật). Sau đó chương trình in ra thông tin của các đối tượng.
Ví dụ 3.2. Chương trình với các đối tượng:
class Diem {// Lớp Điểm
public int x = 0; //Hoành độ
public int y = 0; //Tung độ
public Diem(int x1, int y1) {//constructor
x = x1;y = y1;
}
}
class HinhChuNhat {// Lớp Hình chữ nhật
public int chieuDai = 0;// Chiều dài, gán giá trị ngầm định là 0
public int chieuRong = 0; // Chiều rộng, gán giá trị ngầm định là 0
public Diem diemTraiTren;// Tọa độ góc trái phía trên của hình
// bốn constructor
public HinhChuNhat() {
diemTraiTren = new Diem(0, 0);
}
public HinhChuNhat(Diem d) {
diemTraiTren = d;
}
public HinhChuNhat(int dai, int rong) {
diemTraiTren = new Diem(0, 0);
chieuDai = dai; chieuRong = rong;
78

}
public HinhChuNhat(Diem d, int dai, int rong) {
diemTraiTren = d;
chieuDai = dai; chieuRong = rong;
}
// phương thức di chuyển hình chữ nhật
public void diChuyen(int x, int y) {
diemTraiTren.x = x; diemTraiTren.y = y;
}
// phương thức tính chu vi hình chữ nhật
public int tinhChuVi() {
return 2 * (chieuDai + chieuRong);
}
}
// Chương trình chính
public class TaoDoiTuong {
public static void main(String[] args) {
//Khai báo và tạo một đối tượng điểm
//và hai đối tượng hình chữ nhật.
Diem diem1 = new Diem(23, 94);
HinhChuNhat chuNhat1 =
new HinhChuNhat(diem1, 75, 230);
HinhChuNhat chuNhat2 =
new HinhChuNhat(150, 10);

//hiển thị chiều dài, chiều rộng và chu vi của chuNhat1


System.out.println("Chieu dai cua chuNhat1: " +
chuNhat1.chieuDai);
System.out.println("Chieu rong cua chuNhat1: " +
chuNhat1.chieuRong);
System.out.println("Chu vi cua chuNhat1: " +
chuNhat1.tinhChuVi());

//đặt vị trí của chuNhat2


chuNhat2.diemTraiTren = diem1;
79

//hiển thị vị trí của chuNhat2


System.out.println("Hoanh do cua diem trai tren cua
chuNhat2: " + chuNhat2.diemTraiTren.x);
System.out.println("Tung do cua diem trai tren cua
chuNhat2: " + chuNhat2.diemTraiTren.y);
//di chuyển chuNhat2 và hiển thị vị trí mới
chuNhat2.diChuyen(70, 50);
System.out.println("Hoanh do cua diem trai tren cua
chuNhat2: " + chuNhat2.diemTraiTren.x);
System.out.println("Tung do cua diem trai tren cua
chuNhat2: " + chuNhat2.diemTraiTren.y);
}
}
Chương trình này tạo, điều chỉnh, và hiển thị thông tin của nhiều đối
tượng khác nhau.
Kết quả chạy như sau:
Chieu dai cua chuNhat1: 75
Chieu rong cua chuNhat1: 230
Chu vi cua chuNhat1: 610
Hoanh do cua diem trai tren cua chuNhat2: 23
Tung do cua diem trai tren cua chuNhat2: 94
Hoanh do cua diem trai tren cua chuNhat2: 70
Tung do cua diem trai tren cua chuNhat2: 50
Các mục 3.5.2, 3.5.3 sẽ dùng ví dụ trên để mô tả vòng đời của một đối
tượng trong một chương trình. Từ đó, chúng ta sẽ học được cách tạo và sử dụng
các đối tượng trong các chương trình của mình. Chúng ta cũng sẽ tìm hiểu cơ
chế tự động dọn dẹp bộ nhớ sau khi một đối tượng kết thúc vòng đời của mình
trong mục 3.5.4.
3.5.2. Tạo đối tượng
Như chúng ta đã biết, một lớp là một khuôn mẫu để tạo ra các đối tượng.
Mỗi câu lệnh dưới đây (trong chương trình Ví dụ 3.2) tạo ra một đối tượng và
gán nó cho một biến tham chiếu:
Diem diem1 = new Diem(12, 34);
HinhChuNhat chunhat1 = new HinhChuNhat(diem1, 75, 230);
HinhChuNhat chunhat2 = new HinhChuNhat(150, 10);
80

Dòng đầu tiên tạo ra một đối tượng của lớp Diem, và các dòng thứ hai và
ba mỗi dòng ra một đối tượng thuộc lớp HinhChuNhat.
Mỗi một câu lệnh có ba phần:
- Khai báo: Phần chương trình có chữ đậm là tất cả các khai báo biến bao
gồm tên biến và kiểu dữ liệu của biến. Các biến ở đây là các biến tham chiếu;
- Tạo đối tượng: Từ khóa new là một toán tử trong Java để tạo đối tượng.
Toán tử này sẽ cấp một vùng nhớ đủ lớn để chứa đối tượng của lớp;
- Khởi tạo: Constructor được gọi sau toán tử new để khởi tạo giá trị cho
đối tượng mới.
Cú pháp tổng quát:
ClassName obj = new ClassName(..);
Hoặc:
ClassName obj;
obj = new ClassName(..);
Chúng ta sẽ xem xét chi tiết từng phần trên.

- Khai báo một biến để tham chiếu tới một đối tượng:
Trong các phần trước, chúng ta đã biết cú pháp để khai báo một biến như
sau:
KieuBien tenBien;
Câu lệnh này yêu cầu trình dịch tạo biến tenBien để tham chiếu tới dữ liệu
có kiểu là KieuBien. Với một biến kiểu cơ bản, khai báo này cũng cấp phát bộ
nhớ cho biến. Với biến kiểu tham chiếu, cũng có thể khai báo trên một dòng
riêng. Ví dụ:
Diem diem1;
Tuy nhiên, với biến tham chiếu, khi khai báo như thế, giá trị của nó sẽ
không được xác định cho đến khi một đối tượng thực sự được tạo ra và gán cho
nó. Việc khai báo một biến tham chiếu sẽ không tạo ra một đối tượng. Cần phải
gán một đối tượng cho biến tham chiếu trước khi sử dụng nó, nếu không trình
dịch sẽ báo lỗi. Để tạo đối tượng, cần dùng toán tử new như được mô tả dưới
đây.
- Tạo đối tượng với toán tử new:
Toán tử new tạo đối tượng một lớp bằng cách phân bổ bộ nhớ cho một đối
tượng mới và trả về một tham chiếu tới phần bộ nhớ đó. Toán tử new cũng gọi
constructor đối tượng. Sau toán tử new là một lời gọi tới một constructor.
81

Toán tử new trả về tham chiếu tới đối tượng nó tạo ra. Tham chiếu này
thường được gán cho một biến có kiểu thích hợp, như:
diem1 = new Diem(12, 34);
Tham chiếu do toán tử new trả về không nhất thiết phải gán cho một biến.
Nó có thể cũng được dùng trực tiếp trong biểu thức. Ví dụ:
int chieuDai = new HinhChuNhat().chieuDai;
- Khởi tạo một đối tượng:
Lớp Diem chứa một constructor, có thể nhận ra constructor bởi khai báo
của nó dùng tên giống với tên lớp và nó không có kiểu trả về. Constructor này
có hai tham số kiểu số nguyên, và được khai báo là (int x1, int y1). Câu lệnh sau
cung cấp các giá trị 12 và 34 cho các tham số:
Diem diem1 = new Diem(12, 34);
Biến diem1 bây giờ trỏ tới một đối tượng Diem có hoành độ là 12 và tung
độ là 34.
Lớp HinhChuNhat có bốn constructor, mỗi constructor giúp khởi tạo giá
trị ban đầu cho điểm góc trái phía trên hình chữ nhật, chiều rộng và chiều cao
của hình chữ nhật, sử dụng các kiểu cơ bản và tham chiếu.
Trong chương trình Ví dụ 3.2, khi trình dịch Java gặp dòng lệnh dưới đây,
nó biết cần gọi constructor trong lớp HinhChuNhat có một tham số có kiểu
Diem và sau đó là hai tham số kiểu số nguyên:
HinhChuNhat chuNhat1 = new HinhChuNhat(diem1, 75, 230);
Constructor này của lớp HinhChuNhat khởi tạo đối tượng hình chữ nhật
có diemTraiTren là diem1, chiều dài hình là 75 và chiều rộng là 230. Sau dòng
lệnh này, sẽ có hai tham chiếu (diem1 và thuộc tính diemTraiTren trong đối
tượng chuNhat1) cùng trỏ đến một đối tượng Diem có tọa độ (12,34). Tuy nhiên
điều này là bình thường, trong Java một đối tượng có thể có nhiều tham chiếu
tới nó.
Cũng trong chương trình Ví dụ 3.2, dòng lệnh sau gọi constructor
HinhChuNhat có hai tham số kiểu số nguyên để khởi tạo đối tượng chuNhat2 có
chieuDai là 150, chieuRong là 10 và tọa độ góc trái phía trên là (0,0):
HinhChuNhat chuNhat2 = new HinhChuNhat(150, 10);
3.5.3. Sử dụng đối tượng
Sau khi tạo một đối tượng, có thể chúng ta muốn dùng nó cho việc gì đó.
Có thể chúng ta cần lấy giá trị hoặc thay đổi giá trị của một thuộc tính nào nó,
hoặc gọi một phương thức để thực hiện một hành động.
82

Để truy cập vào thuộc tính của đối tượng qua tham chiếu đối tượng, có thể
sử dụng toán tử chấm (.), tiếp đó là tên thuộc tính, cú pháp như sau:
thamChieuDoiTuong.tenThuocTinh;
Ví dụ, chương trình Ví dụ 3.2 đã dùng cách truy cập
chuNhat2.diemTraiTren.x và chuNhat2.diemTraiTren.y để lấy giá trị tọa độ của
điểm góc trái bên trên của hình chữ nhật chuNhat2.
Để truy cập một thuộc tính của một đối tượng, cũng có thể dùng một biểu
thức nào đó để trả về tham chiếu đối tượng. Có thể dùng giá trị trả về từ toán tử
new để truy cập các thuộc tính của một đối tượng mới:
int chieuDai = new HinhChuNhat().chieuDai;
Câu lệnh này tạo ra một đối tượng HinhChuNhat mới và ngay lập tức lấy
ra giá trị chieuDai. Về bản chất, câu lệnh này trả về giá trị chieuDai mặc định
của một HinhChuNhat. Sau khi câu lệnh này thực hiện, chương trình không còn
có tham chiếu tới đối tượng HinhChuNhat được tạo ra, bởi vì chương trình chưa
lưu trữ tham chiếu này. Đối tượng này không có tham chiếu, và tài nguyên của
nó được giải phóng để tái sử dụng.
Để gọi các phương thức của đối tượng, cách thực hiện cũng hoàn toàn
tương tự với việc truy cập thuộc tính.
Khi gọi, ta chỉ cần viết tên của phương thức vào sau tham chiếu của đối
tượng với toán tử chấm (.) ở giữa. Trong lời gọi phương thức, cần cung cấp các
giá trị tham số cho phương thức, các giá trị này nằm trong dấu ngoặc tròn. Nếu
phương thức không có tham số nào, thì sử dụng cặp dấu ngoặc tròn rỗng “()”.
Cú pháp như sau:
thamChieuDoiTuong.tenPhuongThuc(danhSachThamSo);
hoặc:
thamChieuDoiTuong.tenPhuongThuc();
Trong Ví dụ 3.2, chương trình đã gọi hai phương thức lớp HinhChuNhat:
tinhChuVi() để tính chu vi hình chữ nhật và diChuyen() để thay đổi điểm trái
trên của hình chữ nhật.
Khi dùng tham chiếu đối tượng để truy cập đến các thuộc tính và phương
thức, cần chú ý đến phạm vi được phép truy cập đến các thành viên đó. Ví dụ,
chúng ta không thể truy cập đến các thành viên private của đối tượng ở bên
ngoài phạm vi của lớp đối tượng đó.
3.5.4. Bộ gom dữ liệu rác
Các đối tượng trong chương trình Java được tạo bằng toán tử new và bộ
83

nhớ để chứa các đối tượng được cấp phát một cách động. Thông thường, khi cấp
phát bộ nhớ động, cần theo dõi tất cả các đối tượng đã tạo ra và hủy bỏ chúng
khi chúng không còn được cần đến nữa. Quản lý bộ nhớ là một công việc khá
nhàm chán nhưng lại hay dẫn đến lỗi. Khác với nhiều ngôn ngữ lập trình khác,
Java cho phép lập trình viên tạo ra số lượng đối tượng nhiều như mong muốn
(tất nhiên phải trong giới hạn bộ nhớ hệ điều hành xử lý được) và lập trình viên
không cần phải lo về việc hủy bỏ chúng. Môi trường JRE của Java tự động hủy
bỏ các đối tượng khi chúng không còn được dùng đến nữa. Việc hủy bỏ các đối
tượng không cần đến nữa được thực hiện bởi bộ gom dữ liệu rác (Garbage
Collection).
Một đối tượng thích hợp cho việc gom dữ liệu rác, tức là được coi là rác
khi không còn tham chiếu nào tới đối tượng đó. Các tham chiếu được lưu giữ
trong một biến thường bị mất khi chương trình chuyển việc thực thi ra ngoài
phạm vi của nó. Hoặc, có thể hủy bỏ một biến tham chiếu đối tượng bằng cách
cho biến đó nhận giá trị đặc biệt null. Trong một chương trình có thể có nhiều
tham chiếu tới cùng một đối tượng. Nếu một đối tượng không có tham chiếu nào
tới nó nữa thì đối tượng đó sẽ bị hủy bởi bộ gom dữ liệu rác và bộ nhớ chứa đối
tượng được trả về cho hệ điều hành. Bộ gom dữ liệu rác thực hiện công việc này
tự động vào thời điểm thích hợp do nó xác định.
Ví dụ một số trường hợp đối tượng bị bộ dọn rác thu hồi:
1) Hủy tham chiếu bằng cách gán giá trị null:
Diem e=new Diem();
e=null;
2) Gán tham chiếu cho đối tượng khác:
Diem e1=new Diem();
Diem e2=new Diem ();
e1=e2; //Đối tượng thứ nhất được e1 tham chiếu đến
//sẽ bị bộ dọn rác hủy bỏ
3) Khi tạo đối tượng không có tham chiếu:
new Diem();
3.6. CÁC VẤN ĐỀ KHÁC VỀ LỚP
3.6.1. Trả về giá trị cho phương thức
Một phương thức kết thúc việc thực thi (trả điều khiển về chương trình đã
gọi nó) khi gặp phải một trong các trường hợp sau:
- Đã hoàn thành tất cả các lệnh trong thân phương thức;
84

- Gặp câu lệnh return;


- Ném ra một ngoại lệ (được trình bày trong Chương 8).
Khi phương thức kết thúc việc thực thi, nó có thể trả về giá trị nào đó.
Kiểu dữ liệu giá trị trả về của phương thức được chỉ rõ trong khai báo phương
thức. Trong thân của phương thức, để kết thúc việc thực thi và trả về giá trị
chúng ta dùng lệnh return.
Một phương thức được khai báo void không trả về giá trị. Phương thức
này không cần chứa lệnh return, nhưng cũng có thể có. Trong trường hợp có
lệnh return thì lệnh này có thể được dùng để thoát khỏi phương thức. Cú pháp
được sử dụng đơn giản như sau:
return;
Nếu chúng ta thử trả về một giá trị từ một phương thức được khai báo
void, thì trình dịch sẽ báo lỗi.
Một phương thức được khai báo kiểu trả về khác void phải chứa một lệnh
return với một giá trị trả về tương ứng, có dạng như sau:
return giaTriTraVe;
Kiểu dữ liệu của giá trị trả về phải khớp với kiểu trả về được khai báo của
phương thức; chúng ta không thể trả về một giá trị nguyên từ một phương thức
được khai báo để trả về char.

Ví dụ, phương thức tinhChuVi() trong lớp HinhChuNhat đã được trình


bày trong Ví dụ 3.2 trả về một số nguyên:
public int tinhChuVi() {
return 2 * (chieuDai + chieuRong);
}
Phương thức này trả về số nguyên là giá trị của biểu thức 2 * (chieuDai +
chieuRong).
Phương thức tinhChuVi() trả về một kiểu cơ bản. Một phương thức cũng
có thể trả về một kiểu tham chiếu. Ví dụ, phương thức có các tham số là hai đối
tượng Quat và trả về đối tượng có tốc độ cánh quạt hiện thời lớn hơn:
public Quat timQuatNhanh(Quat quat1, Quat quat2) {
if (quat1.tocDo > quat2.tocDo)
return quat1;
else
return quat2;
85

}
3.6.2. Tham chiếu this
Java cung cấp một từ khóa đặc biệt this là tham chiếu đến chính đối tượng
mà chúng ta đang làm việc với các thành viên (phương thức, thuộc tính) của nó.
Tham chiếu this được dùng khi chúng ta định nghĩa một lớp, có thể hiểu một
cách đơn giản hơn là this chính là tham chiếu đến lớp chúng ta đang định nghĩa.
Thông qua this, khi định nghĩa lớp, chúng ta có thể tham chiếu tới bất kỳ thành
viên nào của lớp.
Sử dụng this truy cập thuộc tính
Chúng ta có thể sử dụng this truy cập vào mọi thuộc tính của lớp trong
các phương thức của lớp. Tuy nhiên, thông thường ta sử dụng từ khóa this khi
thuộc tính trùng tên với tham số của phương thức hay constructor.
Ví dụ, chúng ta có lớp Diem được viết như sau:
public class Diem {
public int x = 0;
public int y = 0;
//constructor
public Diem(int x1, int y1) {
x = x1;
y = y1;
}}
Nhưng lớp cũng có thể được viết như sau:
public class Diem {
public int x = 0;
public int y = 0;
//constructor
public Diem(int x, int y) {
this.x = x;
this.y = y;
}
}
Trong trường hợp này, các tham số của constructor trùng tên với thuộc
tính cần khởi tạo, và chúng ta không thể gán x=x vì trình dịch không phân biệt
được đâu là thuộc tính cần gán và đâu là tham số truyền vào. Để giải quyết vấn
đề, tham chiếu this được sử dụng để chỉ rõ thuộc tính cần gán là this.x.
86

Sử dụng this với một constructor


Trong một constructor, chúng ta cũng có thể sử dụng từ khóa this để gọi
một constructor khác trong cùng một lớp. Việc làm này được gọi là gọi
constructor tường minh. Dưới đây là lớp HinhChuNhat, với một cài đặt khác so
với cài đặt ở trên (Ví dụ 3.3).
Ví dụ 3.3. Sử dụng this để gọi constructor:
public class HinhChuNhat {
private int x, y;
private int chieuDai, chieuRong;
public HinhChuNhat() {
this(0, 0, 1, 1);
}
public HinhChuNhat(int chieuDai, int chieuRong) {
this(0, 0, chieuDai, chieuRong);
}
public HinhChuNhat(int x, int y, int chieuDai, int chieuRong) {
this.x = x; this.y = y;
this.chieuDai = chieuDai;
this.chieuRong = chieuRong;
}
//các phương thức khác…
}
Trong Ví dụ 3.3, lớp HinhChuNhat có ba constructor. Mỗi constructor
khởi tạo một số hoặc tất cả các biến thành viên của hình chữ nhật. Các
constructor cung cấp giá trị ban đầu cho các thuộc tính thành viên. Ở đây,
constructor không tham số tạo ra một đối tượng HinhChuNhat kích thước 1×1
tại tọa độ (0,0). Constructor hai tham số gọi constructor bốn tham số, truyền
chieuDai và chieuRong nhưng luôn sử dụng tọa độ (0,0). Như đã đề cập ở trên,
trình dịch xác định constructor được gọi dựa vào số lượng và kiểu tham số.
Chú ý: Nếu có lời gọi constructor trong một constructor, thì lời gọi đó
phải là dòng lệnh đầu tiên trong thân của constructor.
3.6.3. Kiểm soát truy cập tới các thành viên của lớp
Ở trên chúng ta đã làm quen với các bộ điều chỉnh truy cập. Trong mục
này, chúng ta sẽ làm rõ hơn việc sử dụng chúng. Có hai cấp độ điều khiển truy
cập:
87

- Mức đỉnh: gồm public và default modifier (package-private, không chỉ


định bộ điều chỉnh nào cả). Các bộ điều chỉnh mức đỉnh được áp dụng cho khai
báo lớp.
- Mức thành viên: gồm public, private, protected, và default modifier
(package-private). Các bộ điều chỉnh mức thành viên được áp dụng cho khai báo
các thành viên của lớp.
Một lớp có thể được khai báo với bộ điều chỉnh public, trong trường hợp
này tất cả các lớp ở mọi nơi có thể thấy (sử dụng, truy cập) được lớp này. Nếu
một lớp không có bộ điều chỉnh (mặc định, hay package-private), thì lớp đó chỉ
có thể thấy được trong gói (package) chứa nó (một gói là một nhóm các lớp liên
quan – chúng ta sẽ tìm hiểu về gói ở chương sau).
Ở mức thành viên, chúng ta cũng có thể sử dụng bộ điều chỉnh public
hoặc không có bộ điều chỉnh (default modifier, package–private) giống như mức
đỉnh với cùng ý nghĩa. Đối với các thành viên lớp, có thêm hai bộ điều chỉnh
truy cập nữa là private và protected. Bộ điều chỉnh private xác định thành viên
chỉ có thể được truy cập trong chính lớp chứa nó. Bộ điều chỉnh protected xác
định thành viên chỉ có thể được truy cập trong gói của nó (giống với package-
private) và bởi các lớp dẫn xuất của lớp chứa thành viên đó (có thể trong một
gói khác). Bộ điều chỉnh protected sẽ được trình bày trong chương sau. Về
phạm vi truy cập, có thể nói rộng nhất là public, sau đó đến protected, default
modifier và cuối cùng là private.
Bảng 3.1 dưới đây minh họa truy cập tới các thành viên mỗi bộ điều chỉnh
cho phép.
Bảng 3.1. Các cấp độ truy cập của các bộ điều chỉnh
Bộ điều Truy cập Truy cập Truy cập Truy cập
chỉnh trong cùng trong cùng trong lớp trong các
một lớp một gói con phạm vi
khác
public Có Có Có Có
protected Có Có Có Không
Để trống Có Có Không Không
(default
modifier)
private Có Không Không Không
Trong Bảng 3.1, các cột thể hiện các thông tin sau:
88

- Cột thứ nhất chứa các bộ điều chỉnh truy cập.


- Cột dữ liệu thứ hai chỉ ra các thành viên của một lớp luôn có thể truy
cập tới các thành viên khác trong lớp đó, không phụ thuộc vào bộ điều chỉnh.
- Cột thứ ba chỉ ra rằng liệu các lớp trong cùng một gói với một lớp nào
đó (không tính đến mối quan hệ thừa kế) có thể truy cập tới các thành viên của
lớp đó không.
- Cột thứ tư chỉ ra liệu các lớp dẫn xuất của một lớp được khai báo bên
ngoài gói của lớp đó có thể truy cập tới thành viên của lớp đó hay không.
- Cột thứ năm chỉ ra liệu các chương trình khác có thể sử dụng các thành
viên của lớp hay không.
Các mức độ truy cập tác động đến người lập trình theo hai hướng:
- Thứ nhất, khi dùng các lớp từ một nguồn khác, ví dụ như các lớp trong
nền tảng Java, các mức độ truy cập xác định các thành viên của các lớp đó mà
các lớp của người lập trình có thể dùng.
- Thứ hai, khi viết một lớp, người lập trình cần quyết định mức độ truy
cập mỗi thành viên trong lớp của mình nên có.
Chúng ta xem xét phạm vi tác động của các bộ điều chỉnh truy cập qua Ví
dụ 3.4. Project trong ví dụ có hai gói là package1 và package2, với:
- package1 có hai lớp ClassA và ClassB;
- package2 có hai lớp ClassC và ClassD;
- lớp ClassC là lớp con của ClassA.
Các dòng chú thích đi kèm với các dòng lệnh miêu tả phạm vi truy cập.

Ví dụ 3.4. Phạm vi tác động của các bộ điều chỉnh truy cập:
File ClassA.java:
package package1;
public class ClassA {
public void publicMethod(){
//Phương thức public
protectedMethod(); //OK - truy cập được
defaultModMethod(); //OK
privateMethod(); //OK
}
protected void protectedMethod(){
89

//Phương thức protected


}
void defaultModMethod(){
//Phương thức với default modifier
}
private void privateMethod(){
//Phương thức private
}
}
File ClassB.java:
package package1;
public class ClassB {
void methodB(){
ClassA objA = new ClassA();// Tạo đối tượng lớp ClassA
objA.publicMethod(); //OK - Truy cập được
objA.protectedMethod(); //OK;
objA.defaultModMethod(); //OK
objA.privateMethod(); //Lỗi - Không truy cập được
}
}
File ClassC.java:
package package2;
import package1.*; // Sử dụng các lớp của package1
public class ClassC extends ClassA{

void methodC(){
publicMethod(); //OK - Truy cập được
protectedMethod(); //OK;
defaultModMethod(); //Lỗi - Không truy cập được
privateMethod(); //Lỗi - Không truy cập được
ClassA objA = new ClassA();
objA.protectedMethod(); // Lỗi - Không truy cập được
}
}
File ClassD.java:
90

package package2;
import package1.*;
public class ClassD {
void methodD(){
ClassA objA = new ClassA();
objA.publicMethod(); //OK - Truy cập được
objA.protectedMethod(); //Lỗi - Không truy cập được
objA.defaultModMethod(); //Lỗi - Không truy cập được
objA.privateMethod(); //Lỗi - Không truy cập được
}
}
Trong ví dụ trên, package2 sử dụng các lớp của package1. Có thể thấy
thành viên public của ClassA có thể được truy cập ở khắp mọi nơi; thành viên
protected có thể truy cập được ở trong lớp chứa nó, trong các lớp cùng gói và
trong các lớp con của lớp chứa nó; thành viên default modifier có thể truy cập
được ở trong lớp chứa nó và trong các lớp cùng gói; thành viên private chỉ có
thể truy cập được ở trong lớp chứa nó.
Lời khuyên chọn mức độ truy cập:
- Nếu các lập trình viên khác sử dụng lớp của chúng ta, và chúng ta muốn
đảm bảo các lỗi sử dụng nhầm không xảy ra thì nên sử dụng mức độ truy cập
giới hạn hẹp nhất có thể đối với từng thành viên.
- Nên sử dụng private với các thuộc tính và cung cấp các phương thức
public (set/get) để làm việc với các thuộc tính.
Ví dụ 3.5 trình bày cách sử dụng bộ điều chỉnh truy cập để đảm bảo tính
đúng đắn của dữ liệu.

Ví dụ 3.5. Đảm bảo tính đúng đắn của dữ liệu bằng bộ điều chỉnh truy
cập:
class WrongDate{
int day; //Ngày
int month; // Tháng
int year; // Năm
}

class MyDate{
91

private int day; // Ngày


private int month; // Tháng
private int year; // Năm
public void setDay(int d){
//Phương thức thiết lập giá trị cho ngày
if (d>31||d<1) //Nếu thiết lập giá trị không chuẩn
//thì gán giá trị ngầm định chuẩn
day = 15;
else
day = d;
}
public int getDay(){
// Phương thức trả về giá trị ngày
return day;
}
public void setMonth(int m) {
if (m>12||m<1)
month = 6;
else
month = m;
}
public int getMonth() {
return month;
}
public void setYear(int y) {
if (y==0) year = 1;
else year = y; }
public int getYear() {
return year;
}
}
public class Test {// Chương trình chính
public static void main(String[] args) {
WrongDate d1 = new WrongDate();
d1.day = 100;// Lỗi về tính đúng đắn của dữ liệu
92

MyDate d2 = new MyDate();


d2.setDay(100);// Thử gán giá trị không chuẩn
System.out.println(d2.getDay());
}
}
Kết quả chạy chương trình:
15
Trong ví dụ trên có hai lớp đều lưu trữ dữ liệu thời gian ngày, tháng, năm.
Lớp WrongDate dùng bộ điều chỉnh truy cập default modifier cho các thuộc tính
và có thể dẫn đến dữ liệu thời gian bị thiết lập giá trị không chuẩn (trong thực tế
không có ngày 100). Lớp MyDate dùng bộ điều chỉnh private cho các thuộc tính
và cung cấp các cặp phương thức set/get để làm việc với các thuộc tính. Thông
qua các cặp phương thức này có thể kiểm soát được tính đúng đắn của dữ liệu.
Kết quả là mặc dù chương trình đã cố gắng gán giá trị không hợp lệ cho trường
ngày (day), nhưng dữ liệu này vẫn không bị làm hỏng.
3.6.4. Các thành viên được sử dụng ở mức lớp
Trong mục này chúng ta sẽ tìm hiểu việc sử dụng từ khóa static để tạo các
thuộc tính (các biến) và các phương thức ở mức lớp.
Các biến lớp
Thông thường, khi tạo các đối tượng từ một lớp, thì các đối tượng có vùng
bộ nhớ lưu trữ riêng biệt, và các thuộc tính của các đối tượng cũng phân biệt với
nhau. Đôi khi, chúng ta muốn có các biến dùng chung cho tất cả các đối tượng.
Điều này được thực hiện bằng bộ điều chỉnh static. Các thuộc tính có bộ điều
chỉnh static trong khai báo lớp được gọi là các thuộc tính static hay các biến lớp.
Các thuộc tính này được coi là gắn với các lớp hơn là với bất kỳ đối tượng nào.
Mọi đối tượng của lớp đều có chung biến lớp. Biến lớp này nằm ở một vị trí bộ
nhớ cố định. Đối tượng nào cũng có thể thay đổi giá trị của biến lớp, nhưng các
biến lớp cũng có thể được chỉnh sửa mà không phải tạo ra một đối tượng lớp.
Ví dụ, giả sử chúng ta muốn tạo một số đối tượng Quat và gán cho mỗi
đối tượng một số theo trình tự, bắt đầu là 1 cho đối tượng đầu tiên. Chỉ số này là
duy nhất với mỗi đối tượng và do đó là một thuộc tính thông thường. Bên cạnh
đó, chúng ta cũng cần một thuộc tính lưu trữ số lượng đối tượng Quat đã được
tạo ra để xác định được chỉ số gán cho đối tượng tiếp theo. Thuộc tính này
không liên quan đến bất kỳ đối tượng cá thể nào, mà liên quan đến lớp. Để thực
hiện điều này, chúng ta cần một biến lớp soLuongQuat như trong ví dụ sau:
93

Ví dụ 3.6. Dùng thuộc tính static để lưu trữ số lượng đối tượng lớp:
class Quat {
private int so;
private int tocDo;
// Chỉ số của đối tượng:
private int chiSo;
//Biến lớp lưu trữ số lượng đối tượng quạt đã được tạo ra:
static int soLuongQuat = 0;
public Quat(int so, int tocDo){
this.so = so;
this.tocDo = tocDo;
// tăng soLuongQuat và gán cho chiSo
chiSo = ++soLuongQuat;
}
// phương thức mới trả về biến thực thể chiSo
public int layChiSo() { return chiSo;
}
...
}
public class ViduStatic{
public static void main(String[] args) {
//Làm việc với Quat.soLuongQuat
}
}
Để truy cập đến các biến lớp, thông thường chúng ta dùng tên lớp, điều
này giúp thể hiện rõ thuộc tính là các biến lớp. Tuy nhiên cũng có thể tham
chiếu tới các thuộc tính static bằng một tham chiếu đối tượng. Ví dụ
quat1.soLuongQuat, với quat1 là một đối tượng Quat. Nhưng điều này không
được khuyến khích vì không thể hiện rõ các biến lớp.
Các phương thức lớp:
Ngôn ngữ lập trình Java hỗ trợ các phương thức static cũng như các thuộc
tính static. Các phương thức static có bộ điều chỉnh static trong phần khai báo,
có thể được gọi bằng tên lớp mà không cần thông qua một đối tượng của lớp.
Lời gọi một phương thức lớp (static) như sau:
TenLop.tenPhuongThuc(các tham số);
94

Chú ý: Chúng ta cũng có thể tham chiếu tới các phương thức static với
một tham chiếu đối tượng, ví dụ: tenDoiTuong.tenPhuongThuc(các tham số),
nhưng điều này không được khuyến khích vì không thể hiện rõ các phương thức
lớp.
Thông thường, các phương thức lớp dùng để truy cập các thuộc tính
static.
Ví dụ, có thể bổ sung một phương thức static cho lớp Quat để truy cập
thuộc tính static soLuongQuat:
public static int laySoLuongQuat() {
return soLuongQuat;
}
Các phương thức static (phương thức lớp) và các phương thức thông
thường (phương thức đối tượng) cũng có một số giới hạn khi sử dụng:
- Các phương thức thông thường có thể truy cập trực tiếp các thuộc tính
thông thường và các phương thức thông thường.
- Các phương thức thông thường có thể truy cập trực tiếp các biến lớp và
các phương thức lớp.
- Các phương thức lớp có thể truy cập trực tiếp các biến lớp và phương
thức lớp.
- Các phương thức lớp không thể truy cập trực tiếp các thuộc tính và
phương thức thông thường – nếu cần, phải sử dụng một tham chiếu đối tượng.
Các phương thức lớp cũng không thể sử dụng từ khóa this vì không có đối tượng
cho this tham chiếu tới.
Các hằng số:
Bộ điều chỉnh static, kết hợp với bộ điều chỉnh final, cũng được dùng để
định nghĩa các hằng số. Bộ điều chỉnh final chỉ ra rằng giá trị của thuộc tính này
là không thể thay đổi.
Ví dụ, khai báo biến sau định nghĩa một hằng tên PI, giá trị của nó xấp xỉ
số π (tỷ số giữa chu vi và đường kính của một hình tròn):
static final double PI = 3.141592653589793;
Các hằng số được định nghĩa theo cách này không thể gán lại giá trị.
Thông thường, tên của các giá trị hằng được viết in hoa, nếu tên được cấu thành
từ hơn một từ, các từ được phân cách bằng dấu gạch dưới “_”.
3.6.5. Khởi tạo các thuộc tính
Chúng ta có thể cung cấp giá trị khởi tạo cho một thuộc tính trong khai
95

báo, ví dụ:
public class LopX {
// khởi tạo giá trị a bằng 1
public static int a = 1;
// khởi tạo giá trị là true
private boolean b = true;
}
Cách khởi tạo này thực hiện khi giá trị khởi tạo đã có và việc gán có thể
đặt ở trên một dòng. Tuy nhiên, cách này có những hạn chế vì tính đơn giản của
nó. Nếu việc khởi tạo yêu cầu một logic nào đó (ví dụ, kiểm tra tính hợp lệ hay
một vòng lặp for để gán giá trị cho một mảng), thì một phép gán đơn giản không
thực hiện được, khi đó các thuộc tính có thể được khởi tạo trong các constructor.
Với các biến lớp, ngôn ngữ lập trình Java có cung cấp các khối khởi tạo static.
Chú ý: Không nhất thiết phải khai báo các thuộc tính tại vị trí bắt đầu của
định nghĩa lớp, dù rằng điều này là cách phổ biến. Chỉ cần khai báo và khởi tạo
chúng trước khi chúng được sử dụng.
Các khối khởi tạo static:
Một khối khởi tạo static là một khối chương trình đặt trong dấu ngoặc
nhọn {}, và đặt sau từ khóa static, có dạng như sau:
static {
// chương trình để khởi tạo
}
Một lớp có thể có số lượng khối khởi tạo static tùy ý, và chúng có thể
xuất hiện bất kỳ chỗ nào trong thân lớp. Các khối static được gọi theo trình tự
chúng xuất hiện trong chương trình nguồn.
Một lựa chọn khác cho khởi tạo các thuộc tính static là có thể viết một
phương thức private static, ví dụ:
class LopX {
public static KieuBien tenBien = khoiTaoBienLop();
private static KieuBien khoiTaoBienLop() {// code khởi tạo}
}
Lợi thế của các phương thức private static là chúng có thể được sử dụng
lại sau nếu cần khởi tạo lại biến lớp.
Khởi tạo các thuộc tính đối tượng:
Thông thường, các thuộc tính đối tượng (non-static) được khởi tạo trong
96

một constructor. Ngoài constructor, có hai lựa chọn khác dùng để khởi tạo các
thuộc tính đối tượng: khối khởi tạo và phương thức final.
Các khối khởi tạo cho các thuộc tính đối tượng giống như các khối khởi
tạo static, nhưng không cần từ khóa static:
{
// code khởi tạo
}
Trình dịch Java sao chép các khối khởi tạo (static và non-static) vào mọi
constructor. Do đó, cách này có thể được dùng để chia sẻ một đoạn mã chung
giữa nhiều constructor.
Ví dụ 3.7. Các khối khởi tạo static và thông thường (non-static):
public class Test {
static{
System.out.println("Static");
}
{
System.out.println("Non-static block");
}
public static void main(String[] args) {
Test t = new Test();
Test t2 = new Test();
}
}
Kết quả chạy chương trình:
Static
Non-static block
Non-static block
Chương trình trong Ví dụ 3.7 tạo ra hai đối tượng lớp Test bằng
constructor ngầm định. Constructor này chứa các khối khởi tạo static và non-
static. Các khối khởi tạo static là chung cho mọi đối tượng, vì vậy khi chạy
chương trình, mặc dù có hai đối tượng nhưng chỉ có một khối khởi tạo static
được thực hiện (in ra dòng chữ Static).
Phương thức final là loại phương thức không cho phép nạp chồng trong
lớp dẫn xuất khi thừa kế. Loại phương thức này có thể được dùng để khởi tạo
một thuộc tính đối tượng, ví dụ:
97

class LopX {
private KieuBien tenBien = khoiTaoBienThucThe();
protected final KieuBien khoiTaoBienThucThe() {
// code khởi tạo
}
}
Cách khởi tạo này đặc biệt hữu ích trong trường hợp các lớp dẫn xuất
muốn sử dụng lại phương thức khởi tạo khoiTaoBienThucThe. Nếu phương thức
khởi tạo là non-final, nó có thể bị ghi đè trong lớp dẫn xuất, và khi khởi tạo một
đối tượng thuộc lớp dẫn xuất bằng phương thức khoiTaoBienThucThe trong lớp
dẫn xuất thì lời gọi này có thể cho các kết quả không như mong muốn. Chi tiết
sẽ được trình bày trong Chương 4.
3.7. LỚP LỒNG (NESTED CLASS)
Ngôn ngữ lập trình Java cho phép định nghĩa một lớp trong một lớp khác.
Một lớp như vậy được gọi là lớp lồng và có dạng như sau:
class LopNgoai {
...
class LopLong {
...
}
}
Các lớp lồng được chia làm hai nhóm: nhóm tĩnh (static) và không tĩnh
(non-static). Các lớp lồng được khai báo static gọi là các lớp lồng static (static
nested class), các lớp lồng non-static được gọi là các lớp trong (inner class):
class LopNgoai {
...
static class LopLongStatic {
...
}
class LopTrong {
...
}
}
Một lớp lồng là một thành viên của lớp chứa nó. Lớp chứa được gọi là lớp
ngoài hoặc lớp đỉnh. Các lớp lồng non-static (các lớp trong) được truy cập tới
98

các thành viên khác của lớp chứa, cả khi các thành viên này được khai báo
private. Các lớp lồng static không được truy cập tới các thành viên khác của lớp
chứa.
Là một thành viên của lớp nên một lớp lồng cũng có thể được khai báo là
private, public, protected, hoặc để trống (package private hay default modifier,
chỉ có thể được truy cập được ở trong gói). (Nhớ rằng các lớp bên ngoài chỉ
được khai báo public hoặc default modifier).
Các lý do sử dụng các lớp lồng bao gồm:
- Sử dụng lớp lồng là cách logic để nhóm các lớp chỉ được dùng tại một vị
trí. Nếu một lớp chỉ hữu ích đối với một lớp khác, thì việc lồng nó vào lớp đó và
giữ hai lớp cùng với nhau là hợp lý. Việc lồng “Các lớp trợ giúp” như vậy làm
cho gói (package) của chúng được tổ chức hợp lý hơn.
- Làm tăng tính đóng gói. Ví dụ với hai lớp A và B, trong đó B cần truy
cập các thành viên của A được khai báo private, bằng cách dấu lớp B trong lớp
A, thì B có thể truy cập chúng. Ngoài ra, B có thể ẩn với phạm vi bên ngoài lớp
A.
- Làm chương trình dễ đọc và dễ bảo trì hơn. Việc lồng các lớp nhỏ trong
các lớp mức đỉnh (lớp không nằm trong lớp khác) đặt đoạn mã gần với nơi được
sử dụng hơn.
Các lớp lồng static:
Như với các biến và phương thức lớp (biến và phương thức static), một
lớp lồng static liên kết với lớp chứa nó. Và giống như các phương thức lớp, một
lớp lồng static không thể tham chiếu trực tiếp tới các thuộc tính và phương thức
đối tượng (non-static) được định nghĩa trong lớp chứa nó, lớp lồng static chỉ có
thể truy cập tới các thành viên này thông qua một tham chiếu đối tượng.
Chú ý: Một lớp lồng static tương tác với các thành viên non-static của lớp
ngoài (và các lớp khác) giống như bất kỳ lớp ở mức đỉnh nào khác. Trên thực
thế, một lớp lồng static về mặt hành vi là một lớp mức đỉnh được lồng trong một
lớp ở mức đỉnh khác để thuận lợi cho việc đóng gói.
Các lớp lồng static được truy cập sử dụng tên của lớp chứa:
LopNgoai.LopLongStatic
Ví dụ, để tạo một đối tượng của một lớp lồng static, có thể sử dụng cú
pháp sau:
LopNgoai.LopLongStatic doiTuong = new LopNgoai.LopLongStatic();
Các lớp trong (non-static):
99

Như các thuộc tính và phương thức đối tượng, một lớp trong liên kết với
một đối tượng của lớp chứa nó và được truy cập trực tiếp tới các phương thức và
các thuộc tính của đối tượng đó. Cũng bởi vì một lớp trong liên kết với một đối
tượng, bản thân nó không thể có các thành viên static.
Các đối tượng của một lớp trong tồn tại trong một đối tượng của lớp ngoài
và được truy cập trực tiếp tới các phương thức và thuộc tính của đối tượng chứa.
Để tạo một đối tượng thuộc lớp trong (đối tượng trong), đầu tiên phải tạo một
đối tượng thuộc lớp ngoài (đối tượng ngoài). Sau đó, tạo đối tượng trong trong
đối tượng ngoài với cú pháp sau:
LopNgoai.LopTrong doiTuongTrong =
doiTuongNgoai.new LopTrong();
Có hai loại lớp trong đặc biệt: lớp cục bộ (method local inner class) và lớp
ẩn danh (anonymous inner class). Hình 3.1 thể hiện sự phân loại lớp lồng.

Hình 3.1. Sự phân loại lớp lồng


Ví dụ 3.8. Dùng lớp trong để truy cập thành viên private của lớp ngoài:
class Outer_Demo {
//thuộc tính private của lớp ngoài
private int num= 175;
//lớp trong
public class Inner_Demo{
public int getNum(){
System.out.println("Đây là phương thức getnum của
lớp trong");
return num;
}
}
}
100

public class My_class{


public static void main(String args[]){
//Tạo đối tượng lớp ngoài
Outer_Demo outer = new Outer_Demo();
//Tạo đối tượng lớp trong
Outer_Demo.Inner_Demo inner =
outer.new Inner_Demo();
System.out.println(inner.getNum());
}
}
Kết quả chạy chương trình:
Đây là phương thức getnum của lớp trong
175
3.8. LỚP CỤC BỘ VÀ LỚP ẨN DANH
3.8.1. Lớp cục bộ
Lớp cục bộ là lớp được khai báo trong thân một phương thức.
Trong ví dụ 3.9, chương trình kiểm tra tính hợp lệ hai số điện thoại. Số
điện thoại hợp lệ là một xâu ký tự phải chứa 10 chữ số trong tập hợp [0,1,.., 9].
Chương trình định nghĩa lớp cục bộ SoDienThoai trong phương thức
kiemTraSoDienThoai.
Ví dụ 3.9. Sử dụng lớp cục bộ:
public class ViDuLopCucBo {
//Định nghĩa tập hợp các chữ số của số điện thoại
static String bieuThucChinhQuy = "[^0-9]";
public static void kiemTraSoDienThoai(String soDienThoai1,
String soDienThoai2) {
final int doDaiSoDienThoai = 10;
// Hợp lệ trong phiên bản JDK 8 và mới hơn:
// int doDaiSoDienThoai = 10;
class SoDienThoai {
// Số điện thoại sau khi được định dạng lại
String soDienThoaiDuocDinhDang = null;
SoDienThoai(String soDienThoai){
String soHienThoi =
soDienThoai.replaceAll(bieuThucChinhQuy,
101

"");
if (soHienThoi.length() == doDaiSoDienThoai)
soDienThoaiDuocDinhDang =
soHienThoi;
else
soDienThoaiDuocDinhDang = null;
}
public String laySoDienThoai() {
return soDienThoaiDuocDinhDang;
}
}
SoDienThoai sdt1 = new SoDienThoai (soDienThoai1);
SoDienThoai sdt2 = new SoDienThoai (soDienThoai2);
if (sdt1.laySoDienThoai() == null)
System.out.println("So thu nhat khong hop le");
else
System.out.println("So thu nhat la " +
sdt1.laySoDienThoai());
if (sdt2.laySoDienThoai() == null)
System.out.println("So thu hai khong hop le");
else
System.out.println("So thu hai la " +
sdt2.laySoDienThoai());
}
public static void main(String[] args) {
//Thử chạy chương trình với 2 số điện thoại
kiemTraSoDienThoai("091-222-3333", "444-5555");
}
}
Ví dụ này xác nhận một số điện thoại bằng cách đầu tiên loại bỏ tất cả các
ký tự trong số điện thoại trừ các ký tự từ 0 tới 9. Sau đó, nó kiểm tra liệu số điện
thoại có bao gồm chính xác 10 chữ số. Ví dụ này in ra các thông tin sau:
So thu nhat la 0912223333
So thu hai khong hop le
Một lớp cục bộ được truy cập các thành viên của lớp chứa. Trong ví dụ
102

trước, constructor SoDienThoai truy cập thành viên bieuThucChinhQuy của


ViDuLopCucBo.
Ngoài ra, một lớp cục bộ được truy cập các biến cục bộ. Tuy nhiên, một
lớp cục bộ chỉ có thể truy cập các biến cục bộ được khai báo final. Khi một lớp
cục bộ truy cập một biến cục bộ hoặc một tham số trong khối chứa, lớp này thu
nạp biến hoặc tham số đó. Ví dụ, constructor SoDienThoai có thể truy cập biến
cục bộ doDaiSoDienThoai bởi vì nó được khai báo final; doDaiSoDienThoai là
một biến được thu nạp.
Tuy nhiên, bắt đầu trong Java SE8, một lớp cục bộ có thể truy cập các
biến cục bộ và các tham số của khối chứa là final hoặc “effectively final”. Một
biến hoặc tham số mà giá trị không bao giờ bị thay đổi sau khi nó khởi tạo gọi là
“effectively final”. Ví dụ, giả sử rằng biến doDaiSoDienThoai không được khai
báo final, và chúng ta bổ sung lệnh gán được in đậm trong constructor
SoDienThoai:
SoDienThoai (String soDienThoai) {
// Sẽ bị lỗi nếu án giá trị cho một thuộc tính “effectively final”
doDaiSoDienThoai = 7;
String soHienThoi = soDienThoai.replaceAll(
bieuThucChinhQuy, "");
if (soHienThoi.length() == doDaiSoDienThoai)
soDienThoaiDuocDinhDang = soHienThoi;
else
soDienThoaiDuocDinhDang = null;
}
Do lệnh gán này, mà biến doDaiSoDienThoai không còn là “effectively
final”. Kết quả là trình biên dịch Java đưa ra lỗi do các biến cục bộ được tham
chiếu từ một lớp trong phải là final hoặc “effectively final” (lớp trong
SoDienThoai cố gắng truy cập biến doDaiSoDienThoai):
if (soHienThoi.length() == doDaiSoDienThoai)
Bắt đầu trong JavaSE8, nếu khai báo lớp cục bộ trong một phương thức,
thì nó có thể truy cập các tham số của phương thức. Ví dụ, có thể định nghĩa
phương thức sau trong lớp cục bộ SoDienThoai:
public void inCacSoDienThoaiBanDau() {
System.out.println("Cac so ban dau la " + soDienThoai1 +
" va " + soDienThoai2);
103

}
Phương thức inCacSoDienThoaiBanDau truy cập các tham số
soDienThoai1 và soDienThoai2 của phương thức kiemTraSoDienThoai.
Các lớp cục bộ tương tự như các lớp trong bởi vì chúng không thể định
nghĩa hay khai báo các thành viên static. Các lớp cục bộ trong các phương thức
static, ví dụ như lớp SoDienThoai, được định nghĩa trong phương thức static
kiemTraSoDienThoai, chỉ có thể tham chiếu tới các thành viên static trong lớp
chứa. Ví dụ, nếu không định nghĩa biến thành viên bieuThucChinhQuy là static,
thì trình dịch Java đưa ra một lỗi “non-static variable bieuThucChinhQuy
cannot be referenced from a static context.” (biến non-static bieuThucChinhQuy
không thể được tham chiếu trong ngữ cảnh static). Các lớp cục bộ là non-static
bởi vì chúng được truy cập các thành viên non-static của lớp chứa.
Không thể khai báo một giao diện (interface) trong một khối; các giao
diện về bản chất là static. Ví dụ, phần chương trình dưới đây biên dịch lỗi bởi vì
giao diện ChaoAiDo được định nghĩa trong thân của phương thức
chaoBangTiengAnh:
public void chaoBangTiengAnh() {
interface ChaoAiDo{
public void chao();
}
class ChaoAiDoBangTiengAnh implements ChaoAiDo {
public void chao() {
System.out.println("Chao ");
}
}
ChaoAiDo chao1 = new ChaoAiDoBangTiengAnh();
chao1.chao();
}
Khái niệm giao diện được trình bày chi tiết trong Chương 4.
Không thể khai báo bộ điều chỉnh static hoặc khai báo các giao diện thành
viên trong một lớp cục bộ. Phần chương trình dưới đây không biên dịch được vì
phương thức noiTamBiet() được khai báo static. Trình biên dịch đưa ra một lỗi
“modifier 'static' is only allowed in constant variable declaration” (bộ điều
chỉnh static chỉ được phép trong khai báo biến hằng) khi dịch phương thức sau:
public void noiTamBietBangTiengAnh() {
104

class TamBietTiengAnh {
public static void noiTamBiet() {
System.out.println("Bye bye");}}
TamBietTiengAnh.noiTamBiet();}
Một lớp cục bộ có thể có các thành viên static, với điều kiện chúng là các
biến hằng (Một biến hằng là biến có kiểu cơ bản hoặc kiểu String được khai báo
final và được khởi tạo bằng một biểu thức hằng). Một biểu thức hằng thường là
một biểu thức xâu ký tự hoặc một biểu thức số học. Phần chương trình sau
không có lỗi bởi vì thành viên loiTamBiet là một biến hằng:
public void noiTamBietBangTiengAnh() {
class TamBietTiengAnh {
public static final String loiTamBiet = "Bye bye";
public void noiTamBiet() {
System.out.println(loiTamBiet);
}
}
TamBietTiengAnh tbta = new TamBietTiengAnh();
tbta.noiTamBiet();
}
3.8.2. Lớp ẩn danh (anonymous class)
Các lớp ẩn danh cho phép có thể làm chương trình ngắn gọn hơn. Chúng
cho phép khai báo và khởi tạo một lớp cùng lúc. Chúng giống như các lớp cục
bộ, chỉ có điểm khác là chúng không có tên. Dùng lớp ẩn danh khi chỉ cần dùng
lớp đúng một lần.
Trong khi các lớp cục bộ là các khai báo lớp, thì các lớp ẩn danh là các
biểu thức, nghĩa là lớp này được định nghĩa thông qua một biểu thức. Ví dụ lớp
LopAnDanhChaoTheGioi dưới đây sử dụng các lớp ẩn danh trong các câu lệnh
khởi tạo các biến cục bộ chaoTiengPhap và chaoTiengTayBanNha, nhưng dùng
một lớp cục bộ để khởi tạo biến chaoTiengAnh:
Ví dụ 3.10. Sử dụng lớp ẩn danh:
public class LopAnDanhChaoTheGioi {
interface ChaoTheGioi {
public void chao();
public void chaoAiDo(String aiDo);
}
105

public void noiChao() {


class ChaoTiengAnh implements ChaoTheGioi {
String ten = "world";
public void chao() {
chaoAiDo("world");}
public void chaoAiDo(String tenAiDo) {
ten = tenAiDo;
System.out.println("Hello " + ten);
}
}
ChaoTheGioi chaoTiengAnh = new ChaoTiengAnh();
ChaoTheGioi chaoTiengPhap = new ChaoTheGioi () {
String ten = "tout le monde";
public void chao() {
chaoAiDo("tout le monde");
}
public void chaoAiDo(String tenAiDo) {
ten = tenAiDo;
System.out.println("Salut " + ten);
}
};
ChaoTheGioi chaoTiengTayBanNha = new ChaoTheGioi ()
{
String ten = "mundo";
public void chao() {
chaoAiDo("mundo");
}
public void chaoAiDo(String tenAiDo) {
ten = tenAiDo;
System.out.println("Hola, " + ten);
}
};
chaoTiengAnh.chao();
chaoTiengPhap.chaoAiDo("Fred");
chaoTiengTayBanNha.chao();
106

}
public static void main(String[] args) {
LopAnDanhChaoTheGioi ungDung =
new LopAnDanhChaoTheGioi ();
ungDung.noiChao();
}}
Kết quả chạy chương trình:
Hello world
Salut Fred
Hola, mundo
Như đã nói ở trên, lớp ẩn danh là một biểu thức. Cú pháp một biểu thức
lớp ẩn danh có dạng lời gọi một constructor, chỉ khác ở chỗ có kèm theo một
khối định nghĩa lớp. Chúng ta xem việc khởi tạo đối tượng chaoTiengPhap:
ChaoTheGioi chaoTiengPhap = new ChaoTheGioi () {
String ten = "tout le monde";
public void chao() {
chaoAiDo("tout le monde");
}
public void chaoAiDo(String tenAiDo) {
ten = tenAiDo;
System.out.println("Salut " + ten);
}
};
Biểu thức lớp ẩn danh bao gồm:
- Toán tử new.
- Tên của giao diện sẽ được cài đặt hoặc một lớp sẽ được mở rộng. Trong
ví dụ này, lớp ẩn danh sẽ cài đặt giao diện ChaoTheGioi. (Khái niệm giao diện
được trình bày trong Chương 4).
- Các dấu ngoặc tròn chứa các tham số của một constructor, giống như
một biểu thức khởi tạo một đối tượng lớp thông thường. Chú ý: Khi cài đặt một
giao diện (không có constructor), cần dùng một cặp dấu ngoặc tròn rỗng, như
trong Ví dụ 3.10.
- Thân của khai báo lớp. Thân có thể có các khai báo phương thức nhưng
không thể có các câu lệnh.
Vì định nghĩa lớp ẩn danh là một biểu thức, nên nó phải là một phần của
107

một câu lệnh. Trong Ví dụ 3.10, biểu thức lớp ẩn danh là một phần của câu lệnh
khởi tạo đối tượng chaoTiengPhap. (Điều này lý giải tại sao có một dấu chấm
phẩy sau dấu đóng ngoặc nhọn.)
Giống như các lớp cục bộ, các lớp ẩn danh có thể thu nạp các biến và có
cùng khả năng truy cập tới các biến cục bộ trong phạm vi chứa như các lớp cục
bộ:
- Một lớp ẩn danh được truy cập tới các thành viên của lớp chứa.
- Một lớp ẩn danh không thể truy cập tới các biến cục bộ (các tham số của
hàm hoặc các biến cục bộ trong hàm) không được khai báo final hoặc
“effectively final” trong phạm vi chứa nó. Một biến hoặc tham số mà giá trị
không bao giờ bị thay đổi sau khi khởi tạo được gọi là “effectively final”.
- Giống một lớp lồng, một khai báo nào đó (ví dụ khai báo một biến)
trong một lớp ẩn danh che phủ các khai báo có cùng tên trong phạm vi chứa.
Lớp ẩn danh cũng có các giới hạn như lớp cục bộ đối với các thành viên
của nó:
- Không thể khai báo bộ điều chỉnh static và các giao diện thành viên
trong một lớp ẩn danh.
- Một lớp ẩn danh có thể có các thành viên static, với điều kiện chúng là
các biến hằng.
Chú ý rằng có thể có các khai báo sau trong các lớp ẩn danh:
- Các trường thuộc tính
- Các cài đặt phương thức ở kiểu cơ sở và các phương thức mới.
- Các bộ khởi tạo đối tượng
- Các lớp cục bộ
Tuy nhiên, không thể khai báo các constructor trong một lớp ẩn danh.
TỔNG KẾT CHƯƠNG
Chương 3 đã giới thiệu những nội dung chính sau:
- Java là ngôn ngữ lập trình hướng đối tượng, chương trình Java được tạo
thành từ các lớp hoặc các gói riêng biệt. Trong chương trình, tất cả các mã, bao
gồm các biến, và các khai báo đều được thực hiện trong phạm vi các lớp. Ngoài
các lớp được cung cấp sẵn, Java còn cho phép lập trình viên xây dựng các lớp
của riêng mình.
- Lập trình hướng đối tượng là kỹ thuật cho phép định nghĩa các đối tượng
trong mã nguồn chương trình.
- Trong lập trình hướng đối tượng, một chương trình được xem như một
108

tập hợp các đối tượng, các đối tượng này tương tác với nhau để thực hiện các
chức năng nào đó của chương trình.
- Đối tượng chứa các trạng thái cùng với các hành vi liên quan. Các đối
tượng trong chương trình thường được xây dựng dựa trên các đối tượng thế giới
thực. Một đối tượng lưu trữ trạng thái của nó trong các trường (thuộc tính) và
bộc lộ hành vi của nó thông qua các phương thức.
- Lập trình hướng đối tượng có bốn tính chất: trừu tượng hóa, đóng gói,
đa hình và kế thừa.
- Trong OOP, một lớp là một kiểu dữ liệu trừu tượng, chứa đựng các
thuộc tính và các phương thức làm việc với các thuộc tính. Lớp là khuôn mẫu để
tạo các đối tượng, còn đối tượng là một thể hiện cụ thể của lớp.
- Lớp được khai báo và định nghĩa bởi từ khóa class. Khi định nghĩa lớp,
cần định nghĩa các thành viên (thuộc tính, phương thức) của lớp. Khi định nghĩa
lớp, cũng cần xác định phạm vi truy cập của lớp cũng như của các thành viên
của lớp. Phạm vi truy cập của lớp được chỉ định thông qua các bộ điều chỉnh
truy cập. Khi khai báo thuộc tính, có thể khởi tạo giá trị ngầm định cho thuộc
tính.
- Trong một lớp có thể có các phương thức trùng tên nhau, nhưng có danh
sách các kiểu dữ liệu của các tham số khác nhau. Các phương thức này được gọi
là các phương thức nạp chồng.
- Constructor là phương thức đặc biệt, dùng để khởi tạo giá trị ban đầu
cho đối tượng lớp khi khai báo. Đối tượng được khai báo giống như khai báo
biến, tuy nhiên để đối tượng được cấp bộ nhớ lưu trữ thì phải sử dụng toán tử
new. Bộ nhớ lưu trữ đối tượng được thu hồi một cách tự động bởi bộ dọn rác.
- Java cung cấp từ khóa đặc biệt this, là tham chiếu đến chính lớp đang
được định nghĩa. Thông qua this, có thể truy cập vào các thuộc tính và phương
thức của lớp.
- Java cung cấp từ khóa static cho phép định nghĩa các thành viên mức
lớp, các thành viên này là chung cho tất cả các đối tượng của lớp và thường
được truy cập thông qua tên lớp.
- Java cho phép định nghĩa một lớp ở bên trong lớp khác. Lớp được định
nghĩa bên trong lớp khác được gọi là lớp lồng. Lớp lồng có thể phân loại ra lớp
lồng static, lớp trong, lớp trong cục bộ và lớp ẩn danh. Lớp lồng làm tăng tính
đóng gói của chương trình, giúp các đoạn mã được đặt ở nơi cần sử dụng.
BÀI TẬP
109

1. Xây dựng chương trình gồm lớp NhanVien (nhân viên), và lớp
KiemTra chứa phương thức main để kiểm tra lớp NhanVien. Lớp NhanVien
gồm:
- Các thuộc tính maNhanVien (mã nhân viên), hoTen (họ tên), luong (tiền
lương hàng tháng);
- Constructor có tham số;
- Các phương thức lấy thuộc tính và đặt giá trị cho các thuộc tính (không
có phương thức đặt giá trị cho thuộc tính maNhanVien), phương thức tính lương
theo năm, phương thức tăng lương theo một tỷ lệ nào đó.
2. Xây dựng chương trình gồm lớp HoaDon (hoá đơn chỉ có một sản
phẩm) và lớp KiemTra để kiểm tra lớp HoaDon. Lớp HoaDon gồm:
- Các thuộc tính maHoaDon (mã hoá đơn), moTa (mô tả sản phẩm),
soLuong (số lượng sản phẩm), donGia (đơn giá);
- Constructor có tham số;
- Phương thức lấy và đặt giá trị cho các thuộc tính (không có phương thức
đặt giá trị cho thuộc tính maHoaDon), phương thức tính tổng tiền.
3. Xây dựng chương trình gồm lớp TaiKhoan (tài khoản ngân hàng), và
lớp KiemTra để kiểm tra lớp TaiKhoan. Lớp TaiKhoan gồm:
- Các thuộc tính soTaiKhoan (số tài khoản), hoTen (họ tên), soTien (số
tiền trong tài khoản);
- Constructor có tham số;
- Phương thức lấy và đặt giá trị cho các thuộc tính (không có phương thức
đặt giá trị cho thuộc tính soTaiKhoan), phương thức nạp tiền, rút tiền, và chuyển
tiền.
4. Xây dựng chương trình gồm lớp ThoiGian (thời gian), và lớp KiemTra
chứa phương thức main để kiểm tra lớp ThoiGian. Lớp ThoiGian gồm:
- Các thuộc tính gio (giờ), phut (phút), giay (giây);
- Constructor có tham số;
- Các phương thức lấy và đặt giá trị cho các thuộc tính, phương thức tăng
thời gian lên 1 giây, phương thức giảm thời gian 1 giây.
5. Xây dựng chương trình gồm lớp Diem (điểm), lớp TamGiac (tam giác),
và lớp KiemTra chứa phương thức main để kiểm tra các lớp.
- Lớp Diem gồm:
+ Hai thuộc tính x, y là tọa độ điểm;
+ Các constructor không có tham số và có tham số;
110

+ Các phương thức lấy giá trị các thuộc tính x, y, các phương thức đặt giá
trị cho x, y, và cả x và y, phương thức tính khoảng cách tới một điểm có đối số
truyền vào là hoành độ và tung độ của điểm đó, phương thức tính khoảng cách
tới một điểm có đối số truyền vào là một đối tượng Diem, và phương thức
toString() in ra giá trị các thuộc tính của đối tượng Diem.
- Lớp TamGiac gồm:
+ Ba thuộc tính kiểu Diem;
+ Constructor có các tham số truyền vào là các hoành độ và tung độ của
ba điểm, constructor có các tham số truyền vào là ba đối tượng kiểu Diem.
+ Phương thức kiểm tra ba điểm có tạo thành tam giác không, phương
thức tính diện tích tam giác, phương thức toString() in ra toạ độ ba điểm của tam
giác (chú ý sử dụng phương thức toString() của lớp Diem).
6. Xây dựng chương trình gồm lớp Diem (điểm), lớp HinhTron (hình
tròn), và lớp KiemTra để kiểm tra các lớp. Lớp Diem gồm các thành phần như
bài tập 5. Lớp HinhTron gồm:
- Thuộc tính tam (tâm hình tròn) có kiểu Diem và thuộc tính banKinh (bán
kính hình tròn);
- Constructor không tham số và có tham số;
- Phương thức lấy giá trị các thuộc tính banKinh, tam, các phương thức
lấy toạ độ x, y của tâm, phương thức đặt giá trị cho thuộc tính banKinh, tam, và
các phương thực đặt giá trị cho các toạ độ x, y của tâm, phương thức tính diện
tích, phương thức tính chu vi, và phương thức tính khoảng cách tới một hình
tròn khác (biết rằng khoảng cách giữa hai hình tròn là khoảng cách giữa hai tâm
của hình tròn), phương thức toString() để in ra bán kính và các toạ độ tâm của
hình tròn (chú ý sử dụng phương thức toString() của lớp Diem).
7. Xây dựng chương trình gồm lớp TacGia (tác giả), lớp Sach (Sach), và
lớp KiemTra chứa phương thức main để kiểm tra các lớp.
- Lớp TacGia gồm:
+ Các thuộc tính maTacGia (mã tác giả), ten (tên), email,
thongTinTacGia (thông tin tác giả);
+ Constructor có tham số;
+ Phương thức lấy và đặt giá trị cho các thuộc tính (không có phương
thức đặt giá trị cho thuộc tính maTacGia ), và phương thức toString() trả về xâu
ký tự chứa giá trị của các thuộc tính.
- Lớp Sach gồm:
111

+ Các thuộc tính maSach (mã sách), ten (tên sách), tacGia (tác giả, kiểu
TacGia), gia (giá), soTrang (số trang);
+ Constructor có tham số;
+ Các phương thức lấy và đặt giá trị cho các thuộc tính, phương thức
toString() trả về xâu ký tự chứa thông tin về sách cùng với thông tin của tác giả
của cuốn sách (chú ý sử dụng phương thức toString() của lớp TacGia).
8. Xây dựng chương trình gồm lớp KhachHang (khách hàng), HoaDon
(hoá đơn), và lớp KiemTra chứa phương thức main để kiểm tra các lớp.
- Lớp KhachHang gồm:
+ Các thuộc tính maKhachHang (mã khách hàng), hoTen (họ tên),
giamGia (phân trăm giá được giảm);
+ Constructor có tham số;
+ Phương thức lấy và đặt giá trị cho các thuộc tính (không có phương
thức đặt giá trị cho thuộc tính maKhachHang).
- Lớp HoaDon gồm:
+ Các thuộc tính maHoaDon (mã hoá đơn), khachHang (khách hàng có
kiểu KhachHang), tienTra (tiền trả);
+ Constructor có tham số;
+ Phương thức lấy giá trị các thuộc tính maHoaDon, khanhHang, tienTra,
hoTen (họ tên khách hàng), phương thức đặt giá trị cho thuộc tính tienTra và
phương thức tính tiền trả sau khi giảm giá.
9. Xây dựng chương trình gồm lớp KhachHang (khách hàng), lớp
TaiKhoan (tài khoản), và lớp KiemTra chứa phương thức main để kiểm tra các
lớp.
- Lớp KhachHang gồm:
+ Các thuộc tính maKhachHang (mã khách hàng), tenKhachHang (tên
khách hàng), diaChi (địa chỉ);
+ Constructor có tham số;
+ Các phương thức lấy và đặt giá trị cho các thuộc tính (không có phương
thức đặt giá trị cho thuộc tính maKhachHang)
- Lớp TaiKhoan gồm:
+ Các thuộc tính soTaiKhoan (số tài khoản), khachHang (khách hàng, có
kiểu KhachHang), soTien (số tiền trong tài khoản);
+ Constructor khởi tạo có tham số;
+ Các phương thức lấy giá trị các thuộc tính soTaiKhoan, khachHang,
112

soTien, hoTen (họ tên khách hàng);


+ Phương thức nạp tiền, phương thức rút tiền.
Câu hỏi ôn tập
1. Có lớp nào trong Java không có constructor không?
2. Nếu trong lớp đã có định nghĩa constructor có tham số và không có
constructor không tham số, thì trình dịch có tự động thêm vào constructor ngầm
định không?
3. Ý nghĩa và cách sử dụng toán tử new?
4. Phát biểu nào là đúng?
(a) Thành viên public của lớp có thể được truy cập trong tất cả các gói.
(b) Thành viên private của lớp không thể truy cập được bởi các phương
thức của cùng lớp đó.
5. Kết quả chạy chương trình sau là gì:
class Area{
void find(int l, int b){
System.out.println("Area is"+(l*b));
}
void find(int l, int b,int h){
System.out.println("Area is"+(l*b*h));
}
public static void main (String[] args){
Area ar = new Area();
ar.find(8,5);
ar.find(4,6,2);
}
}
6. Hãy xác định kết quả chạy chương trình sau:
class Area{
void find(long l,long b){
System.out.println("Area is"+(l*b));
}
void find(int l, int b,int h){
System.out.println("Area is"+(l*b*h));
}
public static void main (String[] args){
113

Area ar = new Area();


ar.find(8,5);
ar.find(2,4,6);
}
}
7. Hãy xác định kết quả chương trình sau:
class Test{
Test(){
this(10);
}
Test(int x){
System.out.println("x="+x);
}}
public class MyProject{
public static void main(String args[]){
Test t = new Test();
}
}
8. Kết quả chạy chương trình sau là gì:
public class Test{
static int i = 1;
public static void main(String args[]){
System.out.println(i+”, “);
m(i);
System.out.println(i);
}
public void m(int i){
i += 2;
}
}
(a) 1, 3 (b) 3, 1 (c) 1, 1 (d) 1, 0 (e) Lỗi biên dịch
9. Kết quả chạy chương trình sau là gì:
class Course{
public String courseName;
public int courseID;
114

}
public class CourseProvide{
public static void main(String[]args){
Course course1 = new Course();
course1.courseID = 0001;
course1.courseName = "0001";
Course course2 = new Course();
course2 = course1;
System.out.println("course1.courseID = " +
course1.courseID +", course2.courseID = " +
course2.courseID);
}
}

10. Cho lớp sau:


public class LopX{
public static String a = "abc";
public int b = 1;
}
a) Cho biết lớp có những biến lớp nào?
b) Cho biết lớp có những thuộc tính đối tượng nào?
c) Đoạn chương trình sau lỗi ở đâu:
public static void main(String[] args){
LopX lopX;
lopX.a = "123";
lopX.b = 1000;
}
d) Cho biết kết quả được in ra của đoạn chương trình sau:
LopX u = new LopX();
LopX v = new LopX();
u.a = "cde";
v.a = "xyz";
u.b = 10;
v.b = 20;
System.out.println("u.a = " + u.a);
115

System.out.println("v.a = " + v.a);


System.out.println("u.b = " + u.b);
System.out.println("v.b = " + v.b);
System.out.println("LopX.a = " + LopX.a);
11.a) Một chương trình giải phóng bộ nhớ của một đối tượng như thế
nào?
b) Cho đoạn chương trình sau (LopX được định nghĩa trong Câu 10):
LopX a, b, c;
a = new LopX();
a.b = 1;
b = new LopX(); b.b = 2;
c = new LopX(); c.b = 3;
b=null;
Đoạn chương trình trên đã tạo ra những tham chiếu nào cho các đối
tượng? Sau khi thực thi, đối tượng nào có thể được bộ dọn rác thu hồi bộ nhớ?
12. Kết quả thực hiện của chương trình sau là gì:
public class ClassB{
class ClassA {
public ClassA(int x) {
this.x = x;
}
int x;
}
public ClassB(int x, int x2, int y) {
x = x2;
this.y = y;
}
private ClassB(int x, int y) {
this.x = x;
this.y = y;
}
private int x;
private int y;
public static void main(String[]args) {
ClassB b = new ClassB(20, 10);
116

ClassB.ClassA a = new ClassB(10, 10).new ClassA(10);


System.out.println(a.x + " " + b.x);
}
}
117

Chương 4
KẾ THỪA, ĐA HÌNH VÀ TẠO GÓI
4.1. GIỚI THIỆU
Chương 3 đã giới thiệu những khái niệm cơ bản của lập trình hướng đối
tượng (OOP) trong Java. Trong chương này chúng ta sẽ nghiên cứu những khả
năng nâng cao của OOP trong Java như giao diện, cách tạo một lớp trên cơ sở sử
dụng lại hoặc chỉnh sửa những thuộc tính và phương thức của một lớp đã có,
tính đa hình của lời gọi phương thức. Chính nhờ những khả năng này mà OOP
giúp việc lập trình được dễ dàng hơn và hiệu suất hơn. Ngoài ra, chương này
cũng giới thiệu về khả năng của Java cho phép tạo các gói (package) để nhóm
các kiểu (giao diện và lớp) có liên quan, giúp tìm kiếm và sử dụng các kiểu dễ
dàng hơn, tránh các xung đột đặt tên và để kiểm soát truy cập.
4.2. GIAO DIỆN
4.2.1. Vai trò của giao diện
Trong quá trình lập trình, có thể có tình huống khi các lập trình viên khác
nhau cần có một “hợp đồng” về cách các chương trình của họ tương tác với
nhau. Mỗi lập trình viên có thể viết chương trình của mình mà không cần hiểu
chương trình của người khác được viết thế nào, nhưng vẫn đảm bảo các chương
trình giao tiếp được với nhau. Các giao diện là những hợp đồng đem lại khả
năng như vậy.
Ví dụ, chúng ta hình dung một xã hội tương lai ở đó các xe được điều
khiển bởi máy tính mà không cần con người. Các hãng sản xuất xe viết chương
trình (bằng Java) vận hành xe dừng, chạy, tăng tốc, rẽ trái,…Một nhóm khác,
các nhà sản xuất thiết bị dẫn đường điện tử, làm các hệ thống máy tính nhận dữ
liệu vị trí từ hệ thống định vị toàn cầu (GPS – Global Positioning System) và các
điều kiện giao thông. Sau đó, các thông tin này được dùng để điều khiển xe.
Các nhà sản xuất xe phải công bố một giao diện chuẩn về các phương
thức được gọi để làm xe di chuyển (bất kỳ xe nào, từ bất kỳ nhà sản xuất nào).
Các nhà sản xuất phương tiện dẫn đường sau đó có thể viết phần mềm để gọi các
phương thức được mô tả trong giao diện này để ra lệnh cho xe. Không nhóm nào
cần biết phần mềm của nhóm khác được cài đặt thế nào. Trong thực thế, mỗi
nhóm có quyền sửa phần mềm thuộc quyền sở hữu của mình bất kỳ lúc nào,
miễn là nó vẫn tuân thủ giao diện được công bố.
Trong ngôn ngữ lập trình Java, một giao diện là một kiểu tham chiếu,
tương tự như một lớp, có thể chứa các hằng số, các ký hiệu phương thức, các
118

kiểu lồng. Các phương thức trong giao diện chỉ có tiêu đề mà không có thân.
Tuy nhiên, từ Java 8 trở đi, giao diện có thể có các phương thức mặc định,
các phương thức static là những phương thức có thể có thân.
Không thể tạo các đối tượng từ các giao diện, giao diện chỉ có thể được
cài đặt bởi các lớp hoặc mở rộng bởi các giao diện khác.
Định nghĩa một giao diện tương tự như định nghĩa một lớp, nhưng dùng
từ khóa interface thay cho từ khóa class.
Ví dụ định nghĩa giao diện:
public interface VanHanhOTo {
// Các khai báo hằng nếu có
// các ký hiệu phương thức
//Rẽ
int re(Huong huong, double banKinh, double vanTocBatDau,
double vanTocKetThuc);
//Đổi làn
int doiLan(Huong huong, double vanTocBatDau,
double vanTocKetThuc);
//Phát tín hiệu rẽ
int tinHieuRe (Huong huong, boolean tinHieuBat);
//Đọc tín hiệu ra đa phía trước
int layRadarPhiaTruoc(double khoangCachToiOTo,
double vanTocOTo);
//Đọc tín hiệu ra đa phía sau
int layRadarPhiaSau(double khoangCachToiOTo,
double vanTocOTo);
......
// các ký hiệu phương thức khác

}
Chú ý rằng các ký hiệu phương thức kết thúc bằng dấu chấm phảy và
không đi kèm với cặp dấu ngoặc nhọn {}.
Giao diện cũng được lưu trong các file có phần mở rộng .java và được
biên dịch thành file .class như đối với lớp.
Thông thường, giao diện được sử dụng cho việc cài đặt các lớp. Khi một
lớp thực thể (lớp có thể tạo ra các đối tượng) cài đặt một giao diện, nó cung cấp
119

thân phương thức cho mỗi phương thức được khai báo trong giao diện đó. Việc
cài đặt được thực hiện bằng từ khóa implements.
Ví dụ:
public class VanHanhJaguarProject7 implements VanHanhOTo {
// Các ký hiệu phương thức của VanHanhOTo
// có cài đặt đi cùng, ví dụ:
int tinHieuRe (Huong huong, boolean tinHieuBat) {
// code để bật, tắt đèn báo rẽ trái, rẽ phải của
//Jaguar Project 7
}
// Cài đặt các phương thức khác.
}
Khi một lớp cài đặt một giao diện, thì chúng ta có thể biết được lớp đó có
các thành viên nào. Với ví dụ trên, thân của lớp VanHanhJaguarProject7 có thể
rất dài, rất phức tạp, nhưng qua dấu hiệu cài đặt giao diện VanHanhOTo, ta có
thể kết luận được lớp này có các phương thức re(), doiLan(), tinHieuRe(),
layRadarPhiaTruoc(), layRadarPhiaSau(). Giao diện có thể nói có vai trò là
khuôn đúc ra các lớp có các hành vi (có tên) giống nhau, nhưng cách thực hiện
các hành vi có thể khác nhau. Nói một cách khác, giao diện là sự tổng quát của
lớp, có mức trừu tượng hóa cao hơn lớp.
4.2.2. Khai báo giao diện
Một khai báo giao diện bao gồm bộ điều chỉnh truy cập, từ khóa interface,
tên giao diện, có thể có từ khóa extends và theo sau là danh sách các giao diện
cha (nếu có) được phân tách bằng dấu phảy, và cuối cùng là thân của giao diện.
Cú pháp:
modifier interface Interface_Name [extends Interface1,...]{
//Khai báo các ký hiệu phương thức
//Khai báo các thuộc tính
}
Ví dụ:
public interface GiaoDien extends GiaoDien1, GiaoDien2, GiaoDien3{
// các khai báo hằng, ví dụ:
// double pi = 3.1416;
// các ký hiệu phương thức
void lamDieuGiDo(int i, double x);
120

int lamDieuGiDoKhac(String s);



}
Trong ví dụ trên, bộ điều chỉnh truy cập public chỉ ra giao diện có thể
được dùng trong bất kỳ gói nào. Nếu không chỉ định giao diện là public, thì giao
diện chỉ có thể truy cập được ở trong gói của giao diện. Ý nghĩa của các bộ điều
chỉnh truy cập đối với giao diện cũng giống như đối với lớp.
Tên giao diện có thể bất kỳ, chỉ cần tuân thủ quy tắc định danh. Một giao
diện có thể khai báo mở rộng (thừa kế) các giao diện khác bằng từ khóa extends,
giống như một lớp con thừa kế một lớp khác. Tuy nhiên, trong khi một lớp chỉ
có thể thừa kế một lớp khác, một giao diện có thể thừa kế một số lượng giao
diện bất kỳ. Khai báo mở rộng một giao diện bao gồm danh sách tất cả các giao
diện cha được phân tách bởi dấu phảy. Phần mở rộng giao diện được trình bày
chi tiết trong mục 4.2.5.
Thân giao diện có thể bao gồm các phương thức trừu tượng (abstract
method). Từ Java 8 trở đi, thân giao diện có thể có thêm các phương thức mặc
định (default) và các phương thức tĩnh (static). Một phương thức trừu tượng
trong một giao diện được khai báo kết thúc bằng dấu chấm phảy và không có
thân (không có dấu ngoặc nhọn - một phương thức trừu tượng không có phần cài
đặt). Các phương thức mặc định được khai báo với bộ điều chỉnh truy cập mặc
định cùng từ khóa default, các phương thức tĩnh cùng từ khóa static. Tất cả các
phương thức trừu tượng, mặc định và tĩnh trong một giao diện được ngầm hiểu
là public, bởi vậy chúng ta có thể không cần chỉ định rõ bộ điều chỉnh truy cập
public.
Ngoài ra, một giao diện có thể bao gồm các khai báo thuộc tính hằng. Tất
cả các thuộc tính hằng được định nghĩa trong một giao diện được ngầm hiểu là
public, static, và final. Một lần nữa, chúng ta cũng có thể không chỉ định rõ
những bộ điều chỉnh truy cập này khi khai báo các thuộc tính.
4.2.3. Cài đặt giao diện
Giao diện thông thường được dùng cho việc tạo ra các lớp cụ thể bằng
cách cài đặt. Để khai báo một lớp cài đặt một giao diện, chúng ta dùng từ khóa
implements đi kèm giao diện trong khai báo lớp. Một lớp có thể cài đặt nhiều
hơn một giao diện, bởi vậy theo sau từ khóa implements có thể là một danh sách
các giao diện mà lớp cài đặt, được phân cách bằng dấu phảy. Theo quy ước, từ
khóa implements đứng sau từ khóa extends (nếu có).
121

Cú pháp :
modifier class Class_Name [extends Class1, ...] implements Interface1,
…{
//Định nghĩa các phương thức của giao diện;}
Chúng ta xem xét ví dụ với giao diện Comparable. Giao diện Comparable
có sẵn trong Java dùng để mô tả cách so sánh các đối tượng với nhau. Giao diện
có dạng như sau:
public interface Comparable {
// đối tượng gọi phương thức compareTo(other)
//và đối tượng other được hiểu là các đối tượng của
//cùng một lớp
// phương thức ngầm quy ước trả về 1, 0, hoặc -1 khi đối tượng gọi
// lớn hơn, bằng, hoặc nhỏ hơn other
public int compareTo(Object other);
}
Nếu muốn định nghĩa một lớp mà các đối tượng của lớp đó có thể so sánh
với nhau, thì chúng ta có thể khai báo lớp cài đặt giao diện Comparable và định
nghĩa lại phương thức compareTo() trong lớp đó.
Một lớp có thể cài đặt Comparable nếu có cách nào đó để so sánh các đối
tượng được tạo ra từ lớp. Đối với các xâu ký tự, có thể bởi thứ tự từ điển; đối
với quyển sách, có thể bởi số trang; đối với con người, có thể bởi chiều cao, cân
nặng,… Đối với các đối tượng hình học phẳng, diện tích có thể là một lựa chọn
tốt (xem lớp HinhChuNhat bên dưới), trong khi thể tích sẽ dùng cho các đối
tượng hình học ba chiều. Tất cả các lớp có thể cài đặt phương thức
compareTo().
Tất nhiên chúng ta có thể định nghĩa một lớp với cách so sánh riêng của
lớp đó, nhưng giao diện Comparable có thể cung cấp một chuẩn cho các lớp có
các đối tượng có thể so sánh được với nhau và chúng ta nên sử dụng giao diện
này. Nếu biết một lớp cài đặt Comparable, thì chúng ta cũng biết ngay rằng các
đối tượng của lớp đó có thể so sánh được với nhau bằng phương thức
compareTo().
Dưới đây là lớp HinhChuNhat, giống lớp HinhChuNhat được trình bày
trong Chương 3, nhưng có cài đặt giao diện Comparable.
Ví dụ 4.1. Định nghĩa lớp HinhChuNhat cài đặt giao diện Comparable:
class Diem {
122

public int x = 0;
public int y = 0;
//constructor
public Diem(int x, int y) {this.x = x;this.y = y;}
}
class HinhChuNhat implements Comparable {
public int chieuDai = 0;
public int chieuRong = 0;
public Diem diemTraiTren;
// Constructor ngầm định
public HinhChuNhat() {
diemTraiTren = new Diem(0, 0);
}
// Constructor một tham số
public HinhChuNhat(Diem d) {
diemTraiTren = d;
}
// Constructor hai tham số
public HinhChuNhat(int dai, int rong) {
diemTraiTren = new Diem(0, 0);
chieuDai = dai;
chieuRong = rong;
}
// Constructor ba tham số
public HinhChuNhat(Diem d, int dai, int rong) {
diemTraiTren = d;
chieuDai = dai;
chieuRong = rong;
}
// phương thức di chuyển hình chữ nhật
public void diChuyen(int x1, int y1) {
diemTraiTren.x = x1; diemTraiTren.y = y1;
}
// phương thức tính chu vi
public int tinhChuVi() {
123

return 2* (chieuDai + chieuRong);


}
// phương thức tính diện tích
public int tinhDienTich() {
return chieuDai * chieuRong;
}
// phương thức cài đặt giao diện Comparable
public int compareTo(Object other) {
HinhChuNhat chuNhat = (HinhChuNhat) other;
if (this.tinhDienTich() < chuNhat.tinhDienTich()) return -1;
else
if (this.tinhDienTich() > chuNhat.tinhDienTich())
return 1;
else
return 0;
}
}
public class GiaodienSosanh {
public static void main(String[] args) {
HinhChuNhat h1 = new HinhChuNhat(10, 20);
HinhChuNhat h2 = new HinhChuNhat(20, 30);
System.out.println("Kết quả so sánh hai hình chữ nhật: "+
h1.compareTo(h2));
}
}
Kết quả chạy chương trình:
Kết quả so sánh hai hình chữ nhật: -1
Bởi vì HinhChuNhat cài đặt Comparable, nên hai đối tượng h1, h2 kiểu
HinhChuNhat có thể được so sánh (qua diện tích). Kết quả cho thấy h1 (có diện
tích 200) nhỏ hơn h2 (có diện tích 600).
Chú ý: Phương thức compareTo(), như định nghĩa trong giao diện
Comparable, có một tham số kiểu Object. Dòng chương trình viết đậm trong ví
dụ 4.1 chuyển kiểu other thành một đối tượng HinhChuNhat. Việc chuyển kiểu
cho trình dịch biết đối tượng thực sự là đối tượng của lớp nào. Việc gọi phương
thức tinhDienTich() trực tiếp trên đối tượng other (other.tinhDienTich()) sẽ gây
124

lỗi vì trình biên dịch không hiểu other thực sự là một đối tượng của lớp
HinhChuNhat.
4.2.4. Sử dụng giao diện như một kiểu
Giao diện cũng là một kiểu dữ liệu tham chiếu giống như lớp, vì vậy có
thể dùng các tên giao diện để khai báo các biến tham chiếu đối tượng. Nếu khai
báo một biến tham chiếu kiểu giao diện, thì đối tượng mà biến đó được gán phải
là một đối tượng của lớp cài đặt giao diện này.
Cách sử dụng giao diện như một kiểu thường có dạng như trong đoạn mã
sau:
interface GiaoDienNaoDo{

}
class LopNaoDo impelements GiaoDienNaoDo{

}

GiaoDienNaoDo doiTuong = new LopNaoDo();
Thông thường, chúng ta dùng giao diện như một kiểu để khai báo tham
chiếu đối tượng khi chúng ta muốn gán tham chiếu đó cho các đối tượng của các
lớp khác nhau cài đặt giao diện đó.
Ví dụ 4.2 thể hiện chương trình tính tổng chu vi của các hình phẳng.
Trong chương trình có 3 đối tượng hình chữ nhật, hình vuông, hình tam giác. Để
tính tổng chu vi các hình, chúng ta thực hiện các bước sau:
- Tạo một giao diện có khai báo phương thức tính chu vi;
- Định nghĩa các lớp hình học cài đặt giao diện này, khi cài đặt cần định
nghĩa lại phương thức tính chu vi của giao diện cho thích hợp với lớp hình học
đó;
- Khai báo tham chiếu giao diện;
- Gán tham chiếu đối tượng đến các đối tượng các lớp hình học và dùng
tham chiếu giao diện để tính chu vi của đối tượng hình học;
- Tính tổng chu vi và đưa ra kết quả.
Ví dụ 4.2. Chương trình tính tổng chu vi của các hình phẳng.
interface Hinh{
double tinhChuVi();
}
125

class HinhChuNhat implements Hinh{


public double dai;
public double rong;
//Cung cấp thân cho phương thức giao diện
public double tinhChuVi(){
return 2*(dai+rong);
}
}
class HinhVuong implements Hinh{
public double canh;
public double tinhChuVi(){return 4*canh;}}
class HinhTamGiac implements Hinh{
public double a, b, c;
public double tinhChuVi(){
return a+b+c;
}
}
public class KieuGiaoDien{
public static void main(String[] args){
Hinh h;
HinhChuNhat hcn = new HinhChuNhat();
hcn.dai = 5; hcn.rong = 3;
HinhVuong hv = new HinhVuong();
hv.canh = 2;
HinhTamGiac htg = new HinhTamGiac();
htg.a = 3;
htg.b = 4;
htg.c = 5;
double tongChuvi = 0;
h = hcn;
tongChuvi = tongChuvi + h.tinhChuVi();
h = hv;
tongChuvi = tongChuvi + h.tinhChuVi();
h = htg;
tongChuvi = tongChuvi + h.tinhChuVi();
126

System.out.println("Tổng chu vi là: " + tongChuvi);


}
}
Kết quả chạy chương trình:
Tổng chu vi là: 36.0
Trong ví dụ trên, tham chiếu giao diện có thể được tham chiếu đến các đối
tượng các các lớp cài đặt. Có thể để ý thấy là đoạn code tính chu vi đối với các
hình khác nhau là như nhau (đều là h.tinhChuvi()), đây là nền tảng của việc lập
trình tổng quát sẽ được đề cập đến ở những phần sau.
4.2.5. Mở rộng các giao diện
Chúng ta xem xét giao diện ThucHien (thực hiện) có hai phương thức:
public interface ThucHien{
void thucHienCongViec1(int i, double x);
int thucHienCongViec2(String s);
}
Giả sử rằng, sau đó, chúng ta muốn thêm phương thức thứ ba vào
ThucHien và giao diện trở thành:
public interface ThucHien {
void thucHienCongViec1(int i, double x);
int thucHienCongViec2(String s);
boolean thucHienCongViec3(int i, double x, String s);
}
Nếu ta thay đổi kiểu này, thì tất cả các lớp cài đặt giao diện ThucHien cũ
sẽ hỏng bởi vì chúng không còn cài đặt giao diện cũ và các lập trình viên dựa
vào giao diện này sẽ “cực lực phản đối”.
Giải pháp là cố gắng dự báo tất cả trường hợp sử dụng của giao diện và
mô tả đầy đủ ngay từ đầu. Nhưng nếu phải bổ sung thêm các phương thức cho
một giao diện, thì vẫn có một số lựa chọn.
Thứ nhất, có thể tạo một giao diện ThucHienMoi (thực hiện mới) mở rộng
ThucHien bằng từ khóa extends, cách làm này còn gọi là thừa kế giao diện:
public interface ThucHienMoi extends ThucHien{
boolean thucHienCongViec3(int i, double x, String s);
//các phương thức khác
}
Bây giờ, những lập trình viên có thể chọn tiếp tục sử dụng giao diện cũ,
127

hoặc nâng cấp chương trình để sử dụng giao diện mới.


Cách thứ hai, từ Java 8 trở đi, có thể định nghĩa các phương thức mới của
giao diện như là các phương thức mặc định (default method). Ví dụ sau định
nghĩa một phương thức mặc định tên là thucHienCongViec3:
public interface ThucHien {
void thucHienCongViec1(int i, double x);
int thucHienCongViec2(String s);
default boolean thucHienCongViec3(int i, double x, String s) {
// Thân phương thức}
// Các phương thức mặc định và static mới}
Chú ý: Phải cài đặt các phương thức mặc định. Ngoài các phương thức
mặc định, cũng có thể định nghĩa các phương thức static mới cho các giao diện
đang tồn tại. Những lập trình viên có các lớp cài đặt giao diện (có thêm các
phương thức mặc định và static mới) sẽ không phải chỉnh sửa hay biên dịch lại
chúng để dung nạp các phương thức bổ sung.
4.3. THỪA KẾ
4.3.1. Cú pháp thừa kế
Ở mục trên, chúng ta đã làm quen với việc mở rộng (thừa kế) giao diện. Ở
mục này chúng ta sẽ xem xét việc mở rộng lớp bằng thừa kế.
Thừa kế cung cấp một cơ chế mạnh và tự nhiên để tổ chức chương trình.
Thừa kế trong lập trình dựa trên tính chất thừa kế trong thế giới thực. Xét ví dụ
các loại quạt: quạt cây, quạt phun sương, quạt sạc, tất cả đều có các đặc tính của
quạt nói chung (đường kính cánh quạt, số hiện thời, vận tốc cánh quạt hiện thời).
Tất nhiên, mỗi loại quạt còn có các đặc tính khác làm chúng khác với các loại
quạt khác: quạt cây có chiều cao tối đa, chiều cao tối thiểu, chiều cao hiện thời
(đơn vị cm), quạt có đang quay xung quanh hay không; quạt phu sương có dung
tích bình nước (lít), lượng nước còn trong bình (phần trăm), có đang phun sương
hay không; quạt sạc có loại ắc quy, lượng điện còn (phần trăm); quạt có đang
sạc hay không. Chúng ta có thể kết luận rằng, các loại quạt cụ thể nêu trên đều
thừa kế những đặc tính của quạt nói chung. Giả sử, cần xây dựng các lớp quạt
nêu trên trong một chương trình, thì chúng ta có thể sử dụng khả năng thừa kế
mà Java cung cấp để đơn giản hóa việc viết code.
128

Quat
so : int
tocDo : int

tatQuat()
doiSo(so : int)

QuatCay
QuatSac
dangQuayQuanh : boolean
loaiAcquy : string
chieuCaoToiDa : float
acquyCon : float
chieuCaoToiThieu : float
QuatPhunSuong dangSac : boolean
chieuCaoHienThoi : float
dungTichBinhNuoc : float
sac()
chinhDoCao(doCao : float) nuocCon : float
tatSac()
quayQuanh() dangPhunSuong : boolean
tatQuayQuanh()
phunSuong()
tatPhunSuong()

Hình 4.1. Cây phả hệ các lớp quạt


Khả năng thừa kế của lập trình hướng đối tượng cho phép các lớp thừa kế
trạng thái và hành vi dùng chung từ một lớp khác mà không cần phải định nghĩa
lại.
Lớp thừa kế được gọi là lớp dẫn xuất, hoặc lớp con; lớp được thừa kế
được gọi là lớp cơ sở hoặc lớp cha. Trong ví dụ này, Quat trở thành lớp cơ sở
của các lớp QuatCay, QuatPhunSuong, và QuatSac (Hình 4.1). Trong Java, mỗi
lớp chỉ được phép có một lớp cơ sở trực tiếp, và mỗi lớp cơ sở có thể có số
lượng lớp dẫn xuất không giới hạn.
Cú pháp để tạo một lớp dẫn xuất rất đơn giản. Tại vị trí bắt đầu khai báo
lớp, sử dụng từ khóa extends, theo sau là tên của lớp để kế thừa và danh sách các
giao diện để cài đặt (nếu có). Cú pháp:
modifier class Derived_Class extends Base_Class [implements
Interface1,…]{
//Định nghĩa các thành viên của Derived Class
}
Ví dụ:
class QuatCay extends Quat {
// các trường và phương thức mới định nghĩa một chiếc quạt cây
}
Với định nghĩa này, lớp QuatCay sẽ có tất cả các thuộc tính và phương
thức của lớp Quat, và cho phép code của nó tập trung vào các đặc tính của riêng
nó. Điều này giúp cho code của các lớp dẫn xuất dễ đọc hơn. Tuy nhiên, cần
phải chú ý ghi lại trạng thái và hành vi mỗi lớp cơ sở định nghĩa một cách đúng
đắn, vì code này sẽ không xuất hiện trong mã nguồn của các lớp dẫn xuất.
129

Với tính chất thừa kế, một đối tượng của lớp dẫn xuất cũng được coi là
đối tượng của các lớp cơ sở và chúng ta hoàn toàn có thể gán tham chiếu của lớp
cơ sở vào đối tượng của lớp dẫn xuất. Ví dụ:
class LopCoSo{
int i;
void inThongtin(){}
}
class LopDanXuat extends LopCoSo{
int n;
void inThongtinLopDanXuat(){}
}
public class ViduKethua{
public static void main(String[] args){
LopCoSo lcs = new LopDanXuat();
}}
4.3.2. Ghi đè phương thức
Khi định nghĩa lớp dẫn xuất thừa kế lớp cơ sở, ngoài việc định nghĩa thêm
các phương thức mới thì trong lớp dẫn xuất chúng ta hoàn toàn có thể định nghĩa
lại các phương thức của lớp cơ sở để phù hợp hơn với lớp dẫn xuất. Việc định
nghĩa lại các phương thức của lớp cơ sở trong lớp dẫn xuất được gọi là ghi đè
(overriding). Chúng ta xét các trường hợp phương thức thông thường (non-
static), phương thức static và phương thức giao diện.
Với phương thức thông thường: Một phương thức thông thường trong một
lớp dẫn xuất có cùng ký hiệu (tên phương thức, số lượng và kiểu của các tham
số) và cùng kiểu trả về với phương thức thông thường của lớp cơ sở sẽ ghi đè
phương thức này của lớp cơ sở.
Khả năng ghi đè phương thức cho phép một lớp dẫn xuất có thể sửa
phương thức một cách phù hợp hơn với lớp dẫn xuất. Phương thức ghi đè có
cùng tên, số lượng và kiểu tham số, và kiểu trả về như phương thức nó ghi đè.
Tuy nhiên, một phương thức ghi đè cũng có thể trả về một kiểu con (kiểu dẫn
xuất) của kiểu trả về bởi phương thức bị ghi đè, ví dụ:
Ví dụ 4.3. Ghi đè phương thức thông thường:
class Base{}
class Derived extends Base{}
class LopCoSo{
130

int i;
void inThongtin(){
System.out.print("Đây là phương thức của lớp cơ sở");
}
public Base traveDoituong(){
return new Base();
}
}
class LopDanXuat extends LopCoSo{
int n;
@Override
void inThongtin(){
System.out.print("Đây là phương thức của lớp dẫn xuất");
}
@Override
public Derived traveDoituong(){return new Derived();}}
Trong ví dụ 4.3, trong lớp dẫn xuất có sự ghi đè hai phương thức của lớp
cơ sở, trong đó phương thức inThongtin() có ký hiệu hoàn toàn trùng với ký hiệu
của phương thức trong lớp cơ sở, còn traveDoituong() có kiểu trả về là kiểu con
của kiểu trả về của phương thức lớp cơ cở.
Khi ghi đè một phương thức, chúng ta có thể muốn dùng chú thích
@Override chỉ dẫn cho trình dịch rằng ta có ý định ghi đè một phương thức
trong lớp cơ sở. Nếu vì lý do nào đó, trình dịch phát hiện phương thức này
không tồn tại trong một trong những lớp cơ sở, thì nó sẽ đưa ra thông báo lỗi.
Với phương thức static, nếu một lớp con định nghĩa một phương thức
static với cùng ký hiệu như phương thức static trong lớp cha, thì phương thức
này trong lớp con che dấu (che phủ) phương thức trong lớp cha, nghĩa là trong
lớp con vẫn có phương thức của lớp cha nhưng bị che đi bởi phương thức của
lớp con.
Phân biệt giữa che phủ phương thức static và ghi đè một phương thức
thông thường có những ngụ ý quan trọng:
- Khi dùng một biến tham chiếu để gọi một phương thức thông thường thì
phương thức được gọi sẽ là phương thức của kiểu đối tượng mà biến tham chiếu
trỏ đến (Nếu kiểu của biến tham chiếu là lớp cha, nhưng kiểu đối tượng mà biến
đó trỏ đến là lớp con thì phương thức được gọi vẫn là phương thức của lớp con);
131

- Phiên bản của phương thức static bị che phủ được gọi tùy thuộc vào nó
được gọi từ lớp cha hay lớp con (nếu gọi thông qua tên lớp) hoặc kiểu của biến
tham chiếu là lớp cha hay lớp con (nếu gọi thông qua một biến tham chiếu).
Ví dụ 4.4. Ghi đè và che phủ phương thức:
// Lớp thứ nhất là DongVat, chứa một phương thức thông thường
//và một phương thức static
class DongVat {
//Phương thức static
public static void kiemTraPhuongThucStatic() {
System.out.println("Phuong thuc static trong lop DongVat");
}
//Phương thức thông thường (non-static)
public void kiemTraPhuongThucNonStatic() {
System.out.println("Phuong thuc thong thuong trong lop
DongVat");
}
}
//Lớp thứ hai Meo là một lớp con của DongVat:
public class Meo extends DongVat {
//Che phủ phương thức static lớp cha
public static void kiemTraPhuongThucStatic() {
System.out.println("Phuong thuc static trong lop Meo");
}
//Ghi đè phương thức non-static lớp cha
public void kiemTraPhuongThucNonStatic() {
System.out.println("Phuong thuc thong thuong trong lop
Meo");
}
public static void main(String[] args) {
Meo meo = new Meo();
DongVat dongVat = meo;
//Phương thức lớp DongVat sẽ được gọi
dongVat.kiemTraPhuongThucStatic();
//Phương thức lớp Meo sẽ được gọi
meo.kiemTraPhuongThucStatic();
132

//Phương thức lớp Meo sẽ được gọi


dongVat.kiemTraPhuongThucNonStatic();
}
}
Lớp Meo ghi đè phương thức thông thường trong DongVat và che phủ
phương thức static trong DongVat. Kết quả chạy chương trình này như sau:
Phuong thuc static trong lop DongVat
Phuong thuc static trong lop Meo
Phuong thuc thong thuong trong lop Meo
Như đã đề cập, phiên bản của phương thức static bị che dấu được gọi là
phương thức trong lớp cha khi dùng tham chiếu lớp cha để dọi, và phiên bản của
phương thức thông thường được ghi đè được gọi là phương thức trong lớp con.
Với phương thức giao diện, khi dùng lớp cài đặt giao diện, thì các phương
thức mặc định (default) và các phương thức trừu tượng (abstract) trong các giao
diện được ghi đè trong lớp cài đặt giống như các phương thức thông thường.
Tuy nhiên, khi khai báo một lớp thừa kế từ các kiểu cơ sở (một lớp với nhiều
giao diện), nếu các kiểu cơ sở cung cấp nhiều phương thức mặc định với cùng
ký hiệu, thì trình dịch Java tuân theo các luật thừa kế để giải quyết xung đột tên.
Các luật này được điều khiển bởi hai nguyên lý sau:
Nguyên lý 1: Các phương thức thông thường được ưu tiên hơn các
phương thức mặc định của giao diện.
Ví dụ với các lớp và các giao diện sau:
//Lớp ngựa
public class Ngua {
public String dinhDanh() {// định danh
return "Toi la ngua.";
}
}
//Giao diện vật bay
public interface VatBay {
default public String dinhDanh() {
return "Toi co the bay.";
}
}
//Giao diện vật tưởng tượng
133

public interface VatTuongTuong {


default public String dinhDanh () {
return "Toi la mot sinh vat tuong tuong.";
}
}
//Lớp ngựa bay thừa kế từ lớp Ngua và các giao diện
public class NguaBay extends Ngua implements VatBay, VatTuongTuong
{
public static void main(String[] args) {
NguaBay nguaBay = new NguaBay ();
//Gọi phương thức dinhDanh.
//Phương thức nào sẽ được gọi?
System.out.println(nguaBay.dinhDanh ());
}
}
Ở đây, trình dịch sẽ ưu tiên phương thức thông thường của lớp Ngua, và
phương thức nguaBay.dinhDanh() trả về xâu ký tự “Toi la ngua.”.
Nguyên lý 2: Các phương thức, bị ghi đè bởi các phương thức khác, bị bỏ
qua khi thừa kế. Tình huống này có thể xảy ra khi các kiểu cơ sở cùng thừa kế từ
một kiểu (cùng một tổ tiên).
Xem các giao diện và các lớp sau:
//Giao diện động vật
public interface DongVat {
default public String dinhDanh() {
return "Toi la mot dong vat.";
}
}
//Giao diện vật đẻ trứng
public interface VatDeTrung extends DongVat {
default public String dinhDanh() {return "Toi co the de trung.";}
}
//Giao diện vật thổi ra lửa
public interface VatThoiRaLua extends DongVat { }
//Lớp rồng
public class Rong implements VatDeTrung, VatThoiRaLua {
134

public static void main (String[] args) {


Rong rong = new Rong();
System.out.println(rong.dinhDanh());
}
}
Kết quả: Phương thức rong.dinhDanh() trả về xâu ký tự “Toi co the de
trung.”
Trong trường hợp này, phương thức dinhDanh() của giao diện DongVat
đã bị ghi đè bởi phương thức dinhDanh() của giao diện VatDeTrung, nên khi
khai báo lớp Rong cài đặt các giao diện VatDeTrung, VatThoiRaLua thì trong
lớp Rong, phương thức dinhDanh() sẽ là phương thức của giao diện
VatDeTrung.
Nếu trong các giao diện cơ sở có hai hoặc nhiều hơn các phương thức
mặc định được định nghĩa độc lập xung đột nhau (trùng ký hiệu), hoặc một
phương thức mặc định xung đột với một phương thức trừu tượng, và trong kiểu
dẫn xuất, các phương thức đó không được ghi đè thì trình dịch Java sẽ thông báo
lỗi. Cần phải ghi đè các phương thức bị xung đột đó của các kiểu cơ sở trong
kiểu dẫn xuất một cách tường minh.
Xem ví dụ về những chiếc xe được điều khiển bởi máy tính và có thể bay.
Chúng ta có hai giao diện (VanHanhOTo và BayOTo) cung cấp các cài đặt mặc
định cho cùng phương thức (khoiDongDongCo()):
//Xe ô tô tự vận hành
public interface VanHanhOTo {
default public int khoiDongDongCo (int key) {
// Cài đặt
}
}
//Xe ô tô bay
public interface BayOTo {
default public int khoiDongDongCo (int key) {
// Cài đặt
}
}
Một lớp cài đặt cả VanHanhOTo và BayOTo phải ghi đè phương thức
khoiDongDongCo(). Tuy nhiên trong kiểu dẫn xuất vẫn có thể gọi bất kỳ cài đặt
135

mặc định nào của các kiểu cơ sở với từ khóa super (các phương thức chỉ bị che
phủ).
public class OToBay implements VanHanhOTo, BayOTo {
public int khoiDongDongCo(int key) {
// Gọi phương thức của giao diện BayOTo
BayOTo.super.khoiDongDongCo(key);
// Gọi phương thức của giao diện VanHanhOTo
VanHanhOTo.super.khoiDongDongCo(key);
}
}
Tên đứng trước super (trong ví dụ này, BayOTo hoặc VanHanhOTo) phải
tham chiếu tới một giao diện cơ sở. Giao diện này định nghĩa hoặc thừa kế
phương thức được gọi dưới dạng một phương thức mặc định. Cách gọi phương
thức như vậy không bị giới hạn trong việc phân biệt nhiều giao diện cơ sở cùng
chứa các phương thức mặc định có cùng ký hiệu. Có thể dùng từ khóa super để
gọi một phương thức mặc định trong cả các lớp cơ sở.
Khi kế thừa đồng thời lớp và giao diện, thì các phương thức thông thường
được kế thừa từ lớp cơ sở có thể ghi đè các phương thức giao diện trừu tượng
của các giao diện cơ sở.
Xem ví dụ các giao diện và các lớp sau:
//Giao diện vật có vú
public interface VatCoVu {
String dinhDanh();
}
//Lớp ngựa
public class Ngua {
public String dinhDanh() {
return "Toi la ngua.";
}
}
//Lớp ngựa thảo nguyên
public class NguaThaoNguyen extends Ngua implements VatCoVu {
public static void main(String[] args) {
NguaThaoNguyen nguaThaoNguyen
= new NguaThaoNguyen ();
136

System.out.println(nguaThaoNguyen.dinhDanh ());
}
}
Phương thức NguaThaoNguyen.dinhDanh() trả về chuỗi “Toi la ngua”.
Lớp NguaThaoNguyen thừa kế phương thức dinhDanh() từ lớp Ngua sẽ ghi đè
phương thức trừu tượng có cùng tên trong giao diện VatCoVu.
Chú ý: Các phương thức static trong các giao diện không bao giờ được
thừa kế.
Khi ghi đè phương thức, các bộ điều chỉnh truy cập ở phương thức kiểu
cơ sở và ở phương thức kiểu dẫn xuất có thể khác nhau, nhưng bộ điều chỉnh
truy cập cho phương thức ghi đè phải có phạm vi không hẹp hơn bộ điều chỉnh ở
phương thức bị ghi đè. Ví dụ một phương thức protected trong lớp cha có thể trở
thành public, nhưng không thể là private ở trong lớp con.
Không thể thay đổi tính chất static khi ghi đè. Trình dịch sẽ báo lỗi nếu
chúng ta thay đổi một phương thức thông thường trong lớp cha thành một
phương thức static trong lớp con, và ngược lại.
Chú ý: Trong một lớp con, có thể nạp chồng các phương thức kế thừa từ
lớp cha. Các phương thức bị nạp chồng như vậy không che dấu và cũng không
ghi đè các phương thức thực thể của lớp cha – chúng là các phương thức mới,
duy nhất đối với lớp con.
Trong OOP Java, cũng có thể khai báo thuộc tính kiểu dẫn xuất trùng tên
với thuộc tính kiểu cơ sở. Trong trường hợp này, thuộc tính kiểu dẫn xuất chỉ
che phủ thuộc tính kiểu cơ sở chứ không ghi đè. Thuộc tính kiểu cơ sở vẫn có
thể truy cập được bằng từ khóa super. Ví dụ:
class Base{
int i;
void nhap(int i){
this.i = i;
}
}
class Derived extends Base{
int i;
void inThongtin(){
System.out.println("i trong Base là: " + super.i);
System.out.println("i trong Derived là: " + i);
137

}
}
public class ViduGhide {
public static void main(String[] args) {
Derived d = new Derived();
d.i = 10; d.nhap(20);
d.inThongtin();
}
}
Kết quả chạy chương trình:
i trong Base là: 20
i trong Derived là: 10
4.3.3. Sử dụng từ khóa super
Thông qua cơ chế thừa kế, trong đối tượng lớp dẫn xuất luôn có một phần
là đối tượng lớp cơ sở, từ khóa super là tham chiếu đến phần đối tượng đó. Từ
khóa super chỉ được sử dụng khi định nghĩa các thành viên của lớp dẫn xuất (chỉ
sử dụng bên trong lớp dẫn xuất), có thể hiểu đơn giản super là tham chiếu đến
lớp cơ sở của lớp dẫn xuất đang định nghĩa. Từ khóa super có sự tương đồng với
từ khóa this, this là tham chiếu đến chính lớp đang định nghĩa, còn super là tham
chiếu đến lớp cơ sở của lớp đang định nghĩa, cả hai từ khóa này chỉ được sử
dụng khi định nghĩa một lớp.
Từ khóa super được sử dụng trong các trường hợp: để truy cập vào các
thuộc tính hoặc để gọi các phương thức và constructor của lớp cơ sở. Việc dùng
super để truy cập vào thuộc tính và gọi phương thức lớp cơ sở là giải pháp khi
thuộc tính và phương thức bị che phủ hoặc ghi đè bởi các thành viên tương ứng
trong lớp dẫn xuất. Xét ví dụ dùng super truy cập phương thức lớp cơ sở bị ghi
đè sau:
//Lớp cha
public class LopCha {
public void phuongThucIn () {
System.out.println("In trong LopCha.");
}
}
//Lớp con LopCon ghi đè phuongThucIn():
public class LopCon extends LopCha {
138

public void phuongThucIn() {


//Muốn gọi phuonThuIn trong lớp cha phải dùng super
super.phuongThucIn();
//Code của phương thức lớp con
System.out.println("In trong LopCon.");
}
public static void main(String[] args) {
LopCon lopCon = new LopCon();
lopCon.phuongThucIn();
}
}
Trong LopCon, tên đơn giản phuongThucIn() tham chiếu tới một phương
thức được khai báo trong LopCon, ghi đè phương thức này trong LopCha. Bởi
vậy, để tham chiếu tới phuongThucIn() thừa kế từ LopCha, LopCon phải dùng
super. Chương trình in ra màn hình thông tin sau:
In trong LopCha.
In trong LopCon.
Từ khóa super cũng thường được dùng để gọi các constructor của lớp cơ
sở với mục đích khởi tạo phần đối tượng lớp cơ sở trong đối tượng lớp dẫn xuất.
Giả sử có lớp QuatCay là lớp dẫn xuất của Quat như thể hiện trong cây phả hệ
Hình 4.1, ví dụ sau thể hiện constructor của lớp QuatCay đầu tiên gọi
constructor của lớp Quat và sau đó khởi tạo các thành viên của riêng QuatCay.
Ví dụ 4.5. Dùng super để gọi constructor của lớp cơ sở:
//file Quat.java
public class Quat{
public int so; //số
public int tocDo; // tốc độ quay
public Quat(int so, int tocDo){
this.so = so; this.tocDo = tocDo;
}
}
//file QuatCay.java
public class QuatCay extends Quat{
public QuatCay(int so,
int tocDo,
139

boolean dangQuayQuanh, //có đang quay quanh?


float chieuCaoToiDa, //chiều cao tối đa
float chieuCaoToiThieu, //chiều cao tối thiểu
float chieuCaoHienThoi) ////chiều cao hiện thời
{
// Khởi tạo các thuộc tính so, tocDo kế thừa từ lớp Quat:
super(so, tocDo);
this.dangQuayQuanh = dangQuayQuanh;
this.chieuCaoToiDa = chieuCaoToiDa;
this.chieuCaoToiThieu = chieuCaoToiThieu;
this.chieuCaoHienThoi = chieuCaoHienThoi;
}
//Các phương thức khác
}
Cần lưu ý là trong constructor của lớp dẫn xuất, lời gọi constructor của
lớp cơ sở phải là dòng lệnh đầu tiên.
Cú pháp gọi một constructor của lớp cơ sở là:
super();
hoặc:
super(danh sách tham số);
Với super(), constructor không tham số của lớp cơ sở được gọi. Với
super(danh sách tham số), constructor của lớp cơ sở với danh sách tham số phù
hợp được gọi.
Chú ý: Nếu một constructor lớp dẫn xuất không gọi constructor của lớp
cơ sở một cách tường minh, thì trình dịch Java sẽ tự động chèn một lời gọi tới
constructor không tham số của lớp cơ sở. Khi một constructor của lớp dẫn xuất
gọi một constructor của lớp cơ sở một cách tường minh hay không tường minh,
thì sẽ có một chuỗi các constructor được gọi và constructor của lớp Object sẽ
được gọi cuối cùng.
4.3.4. Lớp Object
Lớp Object, trong gói java.lang, ở đỉnh của cây các lớp theo thức bậc thừa
kế. Mọi lớp đều là hậu duệ, trực tiếp hoặc gián tiếp của lớp Object. Tất cả các
lớp, có sẵn hay do lập trình viên xây dựng, đều thừa kế các phương thức thông
thường (non-static) của lớp Object. Nói chung, chúng ta không cần dùng bất kỳ
phương thức nào của Object, nhưng nếu muốn dùng thì nên ghi đè chúng bằng
140

phương thức trong lớp của mình. Bảng 4.1 mô tả một số phương thức của lớp
Object.
Bảng 4.1. Một số phương thức của lớp Object
STT Phương thức và mô tả
protected Object clone();
1
Tạo và trả về một bản sao của đối tượng gọi phương thức này.
public boolean equals(Object obj);
2 Chỉ ra liệu đối tượng obj có “bằng” đối tượng gọi phương thức
equals hay không.
protected void finalize();
3 Được bộ dọn rác gọi, khi bộ dọn rác xác định được không còn
tham chiếu nào tới đối tượng.
public final Class getClass();
4
Trả về lớp của một đối tượng.
public int hashCode();
5
Trả về giá trị mã băm của đối tượng.
public String toString();
6
Trả về biểu diễn chuỗi của đối tượng.
Một số lưu ý khi sử dụng các phương thức lớp Object:
- Phương thức clone(): Một đối tượng gọi được phương thức này chỉ khi
lớp của đối tượng đó hoặc một trong những lớp cơ sở của nó cài đặt giao diện
Cloneable. Nếu lớp của đối tượng và các lớp cơ sở không cài đặt Cloneable, thì
lời gọi phương thức này sẽ ném ra ngoại lệ CloneNotSupportedException. Việc
xử lý ngoại lệ sẽ được trình bày trong Chương 8.
Nếu đối tượng gọi clone() có lớp cài đặt giao diện Cloneable, thì lời gọi
phương thức clone() của Object tạo ra một đối tượng cùng kiểu lớp với đối
tượng gốc và khởi tạo các thuộc tính của đối tượng mới này có cùng giá trị với
các thuộc tính tương ứng của đối tượng gốc.
Để đối tượng có thể gọi clone(), thì đơn giản chỉ cần khai báo lớp của đối
tượng cài đặt Cloneable như sau:
class MyClass implements Cloneable{..}
Đối với một số lớp, hành vi mặc định của phương thức clone() của Object
làm việc đúng theo kỳ vọng. Tuy nhiên, nếu một lớp có thuộc tính có kiểu tham
chiếu, thì phương thức clone() của Object sẽ trả về bản sao không độc lập với
đối tượng gốc (vì các thuộc tính tham chiếu tương ứng trong đối tượng gốc và
141

bản sao đều trỏ đến một đối tượng nào đó), khi đó có thể cần ghi đè clone() để
hành vi này hoạt động đúng. Trong phương thức ghi đè, phải nhân bản cả đối
tượng được thuộc tính tham chiếu trỏ đến. Ví dụ:
+ Trường hợp không ghi đè clone():
public class Student implements Cloneable{
int id;
StringBuilder name;
Student(int id,StringBuilder name){
this.id=id; this.name=name;
}
public static void main(String args[]) throws
CloneNotSupportedException{
StringBuilder st = new StringBuilder("Nguyen Van A");
Student s1=new Student(101,st);
Student s2=(Student)s1.clone(); //s2 là bản sao của s1
st.append(" Ha Noi"); //Thay đổi s1
System.out.println(s1.id+" "+s1.name);
System.out.println(s2.id+" "+s2.name);
}
}
Kết quả chạy chương trình:
101 Nguyen Van A Ha Noi
101 Nguyen Van A Ha Noi
Nghĩa là, khi s1 thay đổi thì s2 cũng thay đổi theo, s2 không độc lập với
s1 vì trường st là một trường tham chiếu.
+ Trường hợp ghi đè clone():
public class Student implements Cloneable{
int id;
StringBuilder name;
Student(int id,StringBuilder name){
this.id=id; this.name=name;
}
public Object clone()throws CloneNotSupportedException{
Student st = new Student(id, name);
StringBuilder _name = new StringBuilder(name);
142

st.id = id; st.name = _name; return st;


}
public static void main(String args[]) throws
CloneNotSupportedException{
StringBuilder st = new StringBuilder("Nguyen Van A");
Student s1=new Student(101,st);
Student s2=(Student)s1.clone(); st.append(" Ha Noi");
System.out.println(s1.id+" "+s1.name);
System.out.println(s2.id+" "+s2.name);
}
}
Kết quả chạy chương trình:
101 Nguyen Van A Ha Noi
101 Nguyen Van A
Hai đối tượng s1, s2 bây giờ đã thực sự là hai đối tượng độc lập với nhau.
Các chương trình ở trên đã sử dụng lớp StringBuilder, lớp này khá giống với lớp
String chỉ khác là đối tượng lớp StringBuilder có thể thay đổi được chính nó
(xem Chương 5).
- Phương thức equals():
Phương thức equals() mang ý nghĩa so sánh hai tham chiếu đối tượng và
trả về true nếu chúng bằng nhau. Phương thức này dùng toán tử so sánh
bằng/đồng nhất (==) để xác định liệu hai đối tượng có bằng nhau không. Đối với
các kiểu dữ liệu cơ bản, toán tử này cho kết quả đúng. Tuy nhiên, đối với các
tham chiếu đối tượng, toán tử này không cho kết quả đúng. Phương thức equal()
do Object trả về kết quả đúng chỉ khi hai tham chiếu cùng trỏ đến một đối
tượng.
Để kiểm tra hai tham chiếu đối tượng có bằng nhau không theo nghĩa
chứa thông tin giống nhau, cần phải ghi đè phương thức equals(). Ta xét ví dụ so
sánh hai tham chiếu đối tượng hình chữ nhật lớp HinhChuNhat. Hai hình chữ
nhật bằng nhau nếu chiều dài và chiều rộng bằng nhau tương ứng. Dưới đây là
một ví dụ của lớp HinhChuNhat ghi đè equals():
public class HinhChuNhat{
public float chieuDai;
public float chieuRong;
HinhChuNhat(d,r){
143

chieuDai = d; chieuRong = r;
}
public boolean equals(Object obj) {
if (obj instanceof HinhChuNhat) {
HinhChuNhat chuNhat = (HinhChuNhat) obj;
return (chieuDai == chuNhat.chieuDai &&
chieuRong == chuNhat.chieuRong);
}
else
return false;
}
}
Với lớp HinhChuNhat như trên thì đoạn code kiểm tra hai tham chiếu
HinhChuNhat có bằng nhau hay không có thể có dạng:
HinhChuNhat chuNhat1 = new HinhChuNhat(10, 20);
HinhChuNhat chuNhat2 = new HinhChuNhat(10, 20);
if (chuNhat1.equals(chuNhat2)) {
System.out.println("các đối tượng bằng nhau");
} else {
System.out.println("các đối tượng không bằng nhau");
}
Kết quả chạy sẽ là:
các đối tượng bằng nhau
Đoạn chương trình này hiển thị “các đối tượng bằng nhau” dù rằng
chuNhat1 và chuNhat2 tham chiếu tới hai đối tượng khác nhau. Chúng được
xem là bằng nhau vì các đối tượng đem so sánh có cùng chiều dài (chieuDai) và
chiều rộng (chieuRong).
Chú ý: Nếu ghi đè equals(),thì cũng nên ghi đè hashCode().
- Phương thức finalize():
Lớp Object cung cấp phương thức finalize(), có thể được một đối tượng
gọi khi đối tượng này đã hết mục đích sử dụng (trở thành dữ liệu bỏ đi). Cài đặt
finalize() của lớp Object không làm gì cả, và chúng ta có thể ghi đè finalize() để
thực hiện công việc dọn dẹp, ví dụ như giải phóng các tài nguyên mà đối tượng
sử dụng.
Phương thức finalize() có thể được hệ thống gọi tự động, nhưng khi nào
144

nó được gọi, hay thậm chí liệu nó có được gọi hay không là không rõ. Do đó,
không nên dựa vào phương thức này để dọn dẹp các tài nguyên đối tượng sử
dụng. Ví dụ, nếu chúng ta không đóng các file trong chương trình sau khi thực
hiện các thao tác nhập xuất và chúng ta hy vọng rằng finalize() sẽ đóng chúng,
thì có thể gặp phải tình huống các file không được đóng.
- Phương thức getClass():
Đây là phương thức không thể ghi đè. Phương thức getClass() trả về một
đối tượng lớp Class, lớp này có các phương thức trả về thông tin lớp đối tượng
gọi như tên lớp (getSimpleName()), lớp cha (getSuperclass()), hay các giao diện
nó cài đặt (getInterfaces()),... Ví dụ, phương thức sau lấy và hiển thị tên lớp của
đối tượng tham số:
void inTenLop(Object doiTuong) {
System.out.println("Lop cua doi tuong la " +
doiTuong.getClass().getSimpleName());
}
- Phương thức hashCode():
Giá trị do phương thức hashCode() trả về là mã băm của đối tượng, đây là
địa chỉ bộ nhớ của đối tượng dưới dạng hệ cơ số 16.
Thông thường theo quy ước, nếu hai đối tượng bằng nhau, thì mã băm
cũng phải giống nhau. Do đó, khi ghi đè phương thức equals() để thay đổi phép
so sánh bằng giữa hai đối tượng thì cài đặt gốc của hashCode() sẽ không thích
hợp nữa. Vì vậy, nếu ghi đè phương thức equals(), thì cũng phải ghi đè phương
thức hashCode().
- Phương thức toString():
Nên ghi đè phương thức toString() trong các lớp của mình. Phương thức
toString() của Object trả về biểu diễn dạng xâu ký tự String của đối tượng, rất
hữu ích để gỡ rối. Biểu diễn String cho một đối tượng phụ thuộc hoàn toàn vào
đối tượng, đây là lý do nên ghi đè toString() trong các lớp của mình.
Chúng ta có thể dùng toString() cùng với System.out.println() để hiển thị
biểu diễn dạng văn bản của một đối tượng, ví dụ như một đối tượng của lớp
HinhChuNhat:
System.out.println(chuNhat1.toString());
Với lớp HinhChuNhat, thì phương thức toString() có thể được ghi đè như
sau:
public String toString(){
145

return “Chieu dai: “


+ chieuDai + ”; chieu rong: “ + chieuRong;
}
4.3.5. Từ khóa final
Lập trình hướng đối tượng cho phép ghi đè phương thức khi thừa kế. Tuy
nhiên, đôi khi có những phương thức của lớp cha mà người lập trình không
muốn nó bị thay đổi trong lớp con, vì phương thức đó chỉ dành riêng (thích hợp)
cho lớp cha. Trong trường hợp này có thể dùng từ khóa final, chúng ta dùng từ
khóa final trong một khai báo phương thức để chỉ ra rằng phương thức này
không thể bị ghi đè trong các lớp con. Bản thân lớp Object cũng có một số
phương thức là final.
Chúng ta có thể muốn tạo ra một phương thức final nếu cài đặt của nó
không nên bị thay đổi và nó có ý nghĩa quyết định đối với trạng thái nhất quán
của đối tượng. Ví dụ, chúng ta có thể muốn tạo phương thức
traVeNguoiDiTruoc() trong lớp ChoiCoVua là final (trong cờ vua, người đi
trước luôn là người cầm quân trắng):
class ChoiCoVua {
// TRANG, DEN tương ứng với người cầm quân trắng và người
//cầm quân đen
enum NguoiChoi { TRANG, DEN }
final NguoiChoi traVeNguoiDiTruoc() {
return NguoiChoi.TRANG;
}
...
}
Trong một lớp, các phương thức, mà được các constructor gọi, nói chung
nên được khai báo là final. Nếu một constructor gọi một phương thức non-final,
một lớp con có thể định nghĩa lại phương thức đó và cho các kết quả bất ngờ
hoặc không như mong muốn.
Xem xét ví dụ sau:
class LopX {
private int a;
LopX() {
a = khoiTaoBienThucThe();
}
146

public int khoiTaoBienThucThe () {


return 1;
}
public int layA() {
return a;
}
}
class LopY extends LopX{
private int b = 2;
LopY() {
super();
}
public int khoiTaoBienThucThe () {
return b;
}
}
class KiemTra() {
public static void main(String[] args) {
LopY y = new LopY();
System.out.println("y.layA() = " + y.layA());
}
}
Khi ta khởi tạo một đối tượng của lớp LopY, thì phương thức
khoiTaoBienThucThe() trong lớp dẫn xuất sẽ được gọi để gán giá trị cho biến a
và điều này có thể cho kết quả không như mong muốn. Như ví dụ trên kết quả in
ra là y.layA() = 0. Lý do là phương thức khoiTaoBienThucThe() trong lớp dẫn
xuất được gọi nhưng biến đối tượng b vẫn chưa được khởi tạo.
Chúng ta cũng có thể khai báo cả một lớp là final. Nếu một lớp được khai
báo là final, thì lớp đó không thể có lớp con (không cho phép kế thừa). Điều này
rất hữu ích, ví dụ, khi tạo ra một lớp không thể chỉnh sửa được như lớp String.
4.3.6. Lớp trừu tượng và phương thức trừu tượng
Một lớp trừu tượng là lớp được khai báo abstract, lớp trừu tượng có thể
có các phương thức trừu tượng (abstract) và các phương thức thông thường
(không trừu tượng).Các lớp abstract không thể tạo ra các đối tượng, nhưng
chúng có thể có lớp con.
147

Một phương thức trừu tượng là phương thức được khai báo không có
phần cài đặt (không cần các dấu ngoặc nhọn và kết thúc bằng dấu chấm phảy) có
dạng như sau:
abstract void diChuyen(double deltaX, double deltaY);
Nếu một lớp bao gồm các phương thức trừu tượng, thì lớp này phải được
khai báo là abstract, như dưới đây:
public abstract class Hinh {
// khai báo các trường, và các phương thức không trừu tượng
abstract void veHinh();
}
Khi một lớp con kế thừa một lớp trừu tượng, lớp con thường cung cấp các
cài đặt cho tất cả các phương thức trừu tượng trong lớp cha của nó. Tuy nhiên,
nếu không cài đặt hết các phương thức trừu tượng, thì lớp con cũng phải được
khai báo abstract.
Chú ý: Các phương thức trong một giao diện không được khai báo là
default hay static ngầm hiểu là abstract, bởi vậy bộ điều chỉnh abstract không
cần dùng đối với các phương thức của giao diện.
Các lớp trừu tượng tương tự như các giao diện. Chúng ta không thể tạo ra
các đối tượng từ các lớp này, và chúng có thể chứa tổ hợp các phương thức được
khai báo có hoặc không có cài đặt. Tuy nhiên, với các lớp trừu tượng, có thể
khai báo các trường không là static và final, và định nghĩa các phương thức
private, protected, và public. Với các giao diện, tất cả các trường tự động là
public, static, và final, và tất cả các phương thức được khai báo hay định nghĩa
(như các phương thức mặc định) là public. Ngoài ra, có thể mở rộng (thừa kế)
chỉ một lớp, dù lớp đó là lớp trừu tượng hay không, trong khi có thể cài đặt số
lượng tùy ý các giao diện.
Một câu hỏi đặt ra là nên dùng lớp trừu tượng hay giao diện trong chương
trình.
- Nên sử dụng các lớp trừu tượng nếu ở một trong các tình huống sau:
+ Chúng ta muốn chia sẻ một đoạn code trong số một số lớp có quan hệ
gần gũi. Khi đó có thể biến đoạn code thành lớp trừu tượng.
+ Chúng ta hy vọng rằng các lớp mở rộng lớp trừu tượng của mình có
nhiều phương thức hoặc trường chung, hoặc các thành viên này yêu cầu các bộ
điều chỉnh truy cập khác public (như protected và private).
148

+ Chúng ta muốn khai báo các trường không phải là static hoặc final.
Điều này cho phép định nghĩa các phương thức có thể truy cập và chỉnh sửa
trạng thái của đối tượng chứa các phương thức này.
- Nên dùng các giao diện nếu ở một trong các tính huống sau:
+ Chúng ta hy vọng các lớp không liên quan sẽ cài đặt giao diện của
mình. Ví dụ giao diện Comparable được cài đặt bởi nhiều lớp không liên quan.
+ Chúng ta muốn đặc tả hành vi của một kiểu dữ liệu cụ thể, nhưng không
quan tâm về cách cài đặt hành vi này.
+ Chúng ta muốn sử dụng đa thừa kế.
Ví dụ với lớp trừu tượng: Trong một ứng dụng hướng đối tượng vẽ các
đối tượng đồ họa, có thể vẽ đường tròn, hình chữ nhật, đoạn thẳng, và nhiều đối
tượng đồ họa khác. Tất cả những đối tượng này có các trạng thái (ví dụ: vị trí,
hướng, màu đường, màu nền) và các hành vi chung (ví dụ như: di chuyển, quay,
thay đổi kích thước, vẽ). Một trong số trạng thái và hành vi này giống nhau đối
với tất cả các đối tượng đồ họa (ví dụ: vị trí, màu nền, và phương thức di
chuyển). Các hành vi khác (ví dụ thay đổi kích thước, hoặc vẽ) yêu cầu các cài
đặt khác. Tất cả hình đều phải có thể vẽ được hoặc thay đổi kích thước; chúng
chỉ khác ở cách thực hiện. Đây là một tình huống thích hợp cho một lớp cha trừu
tượng. Có thể tận dụng những điểm tương đồng và khai báo tất cả các đối tượng
đồ họa thừa kế một đối tượng cha trừu tượng như sơ đồ cây phả hệ ở Hình 4.2.
Hinh

HinhChuNhat HinhTron DoanThang

Hình 4.2. Cây phả hệ các lớp đối tượng hình họa
Đầu tiên, chúng ta khai báo một lớp trừu tượng Hinh, để cung cấp các
biến và phương thức thành viên mà tất cả các lớp con chia sẻ, ví dụ như vị trí
hiện thời và phương thức diChuyen(). Lớp Hinh cũng khai báo các phương thức
trừu tượng cho các phương thức (ví dụ như veDoiTuong() hay
thayDoiKichThuoc()) cần được tất cả các lớp con cài đặt nhưng phải được cài
đặt theo các cách khác nhau.
Lớp Hinh có thể có dạng như sau:
abstract class Hinh {
149

int x, y;
void diChuyen(int newX, int newY) {
...
}
abstract void veHinh();
abstract void thayDoiKichThuoc();
}
Mỗi lớp con không trừu tượng của Hinh, như HinhTron và HinhChuNhat,
phải cung cấp các cài đặt cho các phương thức veHinh() và
thayDoiKichThuoc():
class HinhTron extends Hinh {
void veHinh() {...}
void thayDoiKichThuoc() {...
}
}
class HinhChuNhat extends Hinh {
void veHinh() {...}
void thayDoiKichThuoc() {...}
}
Lớp trừu tượng có thể cài đặt một giao diện. Khi một lớp cài đặt một giao
diện, thông thường phải cài đặt tất cả các phương thức của giao diện. Tuy nhiên,
ta có thể định nghĩa một lớp không cài đặt tất cả các phương thức của giao diện,
với điều kiện lớp này được khai báo là trừu tượng. Ví dụ:
abstract class B implements A {
// cài đặt tất cả các phương thức của A, trừ ra một số phương thức
}
class C extends B {
// cài đặt phương thức còn lại trong A
}
Trong trường hợp này, lớp B phải là trừu tượng bởi vì nó không hoàn toàn
cài đặt giao diện A, nhưng lớp C trên thực tế đã cài đặt toàn bộ giao diện A.
Một lớp trừu tượng có thể có các trường và phương thức static. Có thể
dùng các thành viên tĩnh (static) thông qua tham chiếu lớp (ví dụ
LopTruuTuong.phuongThucTinh()).
4.4. ĐA HÌNH (POLYMORPHISM)
150

Thuật ngữ đa hình liên quan tới một nguyên lý trong sinh học, trong đó
một sinh vật hay một loài có thể có nhiều dạng hay giai đoạn phát triển khác
nhau. Trong ngôn ngữ lập trình hướng đối tượng, đa hình cho phép một thực thể
(ví dụ, biến, hàm, hoặc đối tượng) có thể có nhiều dạng khác nhau.
Trong Java có thể chia ra ba loại đa hình. Loại đa hình thứ nhất cho phép
một biến tham chiếu có thể tham chiếu tới bất kỳ đối tượng nào có kiểu là kiểu
con (kiểu dẫn xuất) của kiểu biến tham chiếu đó. Giả sử chúng ta có lớp Quat và
các lớp con QuatCay, QuatPhunSuong. Đoạn chương trình dưới đây minh họa
một biến v (có kiểu Quat) có thể tham chiếu tới các đối tượng của các lớp dẫn
xuất. Ở các thời điểm khác nhau, biến v tham chiếu tới các đối tượng có các kiểu
khác nhau và khác với kiểu của biến v:
class QuatCay extends Quat{..}
..
class QuatPhunSuong extends Quat{..}

Quat v;
Quat quat = new Quat();
QuatCay quatCay = new QuatCay() ;
QuatPhunSuong quatPhunSuong = new QuatPhunSuong();
v = quat; // Gán một cách bình thường
v = quatCay; //Tham chiếu tới đối tượng của lớp con – được chấp nhận
v = quatPhunSuong; //Tham chiếu tới đối tượng lớp con

Loại đa hình thứ hai cho phép sử dụng một tên phương thức cho nhiều
phương thức trong cùng một lớp miễn là danh sách kiểu tham số của các phương
thức đó khác nhau. Loại đa hình này chính là sự nạp chồng và đã được đề cập
trong Chương 3.
Loại đa hình thứ ba thể hiện qua việc gọi phương thức bằng một biến
tham chiếu. Khi đó, phương thức được gọi sẽ được xác định một cách động. Giả
sử ta có một biến tham chiếu refA có kiểu lớp A, biến này được gán cho đối
tượng objB của lớp B (lớp B là lớp con cháu của lớp A, Ví dụ 4.6), kiểu như sau:
A refA;
B objB = new B();
refA = objB;
refA.method();
151

Với cách gọi phương thức method() như trên thì đầu tiên Java sẽ tìm
phương thức method() trong lớp B (là lớp của đối tượng được tham chiếu đến),
nếu lớp B có phương thức method() thì phương thức này sẽ được gọi. Nếu lớp B
không có, thì Java sẽ tìm phương thức trong các lớp cơ sở của lớp B, nếu tìm
thấy trong một lớp cơ sở nào đó thì phương thức method() của lớp cơ sở đó được
gọi. Nếu trong tất cả các lớp cơ sở của lớp B (cả lớp Object) đều không có
phương thức method() thì Java sẽ báo lỗi. Với kiểu đa hình này thì phương thức
được gọi có vẻ như được nối kết động với biến tham chiếu gọi phương thức đó.
Cùng một hành vi refA.method(), nhưng có thể có các cách hiện thực khác nhau,
tùy thuộc vào đối tượng mà refA tham chiếu đến, nghĩa là một hành vi có thể
tương ứng với nhiều hành động. Đây chính là loại đa hình mà chúng ta muốn nói
đến trong lập trình hướng đối tượng. Để minh họa, chúng ta xét Ví dụ 4.6.
Ví dụ 4.6. Đa hình với lập trình hướng đối tượng:
package vidudahinh;
class A{
public void method(){
System.out.println("Phương thức lớp A");
}
public void methodA(){
System.out.println("Phương thức methodA lớp A");
}
public void methodB(){
System.out.println("Phương thức methodB lớp A");
}
}
class B extends A{
public void method(){
System.out.println("Phương thức lớp B");
}
public void methodB(){
System.out.println("Phương thức methodB lớp B");
}
}
class C extends B{
public void method(){
152

System.out.println("Phương thức lớp C");}}


public class Polymorphism {
public static void main(String[] args) {
A refA; refA = new A();
refA.method();
refA = new B();
refA.method();
refA = new C();
refA.method();
refA.methodB();
refA.methodA();
}
}
Kết quả chạy chương trình:
Phương thức lớp A
Phương thức lớp B
Phương thức lớp C
Phương thức methodB lớp B
Phương thức methodA lớp A
Trong Ví dụ 4.6 có ba lớp A, B, C và lớp chương trình Polymorphism.
Lớp C kết thừa từ lớp B, lớp B kế thừa lớp A. Trong cả ba lớp đều có phương
thức method() với thân khác nhau.
Trong chương trình có khai báo tham chiếu refA của lớp A, tham chiếu
này lần lượt được trỏ đến các đối tượng của lớp A, B và C. Khi dùng tham chiếu
refA để gọi phương thức method() thì phương thức được gọi không phải luôn là
phương thức lớp A (là lớp của refA) mà là phương thức của lớp của đối tượng
mà refA tham chiếu đến. Ngoài ra, khi dùng refA gọi phương thức methodB() khi
refA đang trỏ đến đối tượng lớp C, thì Java đầu tiên sẽ tìm methodB() trong lớp
C, trong lớp C không có methodB(), Java tiếp tục tìm trong lớp B là lớp cơ sở
của C và tìm thấy methodB() rồi thực hiện. Tương tự khi dùng refA gọi
methodA().
Tính chất đa hình giúp chúng ta có thể viết chương trình một cách tổng
quát. Xét ví dụ tính lương phải trả hàng tháng tại một cơ quan, giả sử trong cơ
quan có các nhóm người lao động là cán bộ cơ hữu, nhân viên hợp đồng và lao
động thời vụ. Lương cán bộ được tính bằng hệ số lương nhân với lương cơ bản,
153

lương nhân viên hợp đồng được thỏa thuận hàng tháng, lương lao động thời vụ
được tính bằng số ngày làm việc trong tháng nhân với tiền công một ngày.
Thông thường, để tính lương phải trả hàng tháng ta phải tính lương từng
người một cách riêng biệt rồi cộng toàn bộ với nhau. Với khả năng đa hình có
thể làm cách khác như trong Ví dụ 4.7.
Ví dụ 4.7. Tổng quát hóa chương trình với đa hình:
//Định nghĩa lớp Nguoi (người) là một lớp cơ sở trừu tượng
//với phương thức trừu tượng tính lương
abstract class Nguoi{
abstract double tinhLuong();
}
//Khai báo các lớp con CanBoCoHuu (cán bộ cơ hữu),
//NhanVienHopDong (nhân viên hợp đồng),
//LaoDongThoiVu (lao động thời vụ)
//kế thừa từ Nguoi với phương thức tinhLuong tương ứng
class CanBoCoHuu extends Nguoi{
//Hệ số lương
double hesoLuong;
//Lương cơ bản
double luongCoBan = 1000000;
//Tính lương
public double tinhLuong(){
return hesoLuong*luongCoBan;
}
}
class NhanVienHopDong extends Nguoi{
double luongThoaThuan;
public double tinhLuong(){
return luongThoaThuan;
}
}
class LaoDongThoiVu extends Nguoi{
int ngayCong;//Ngày công
double luongNgay;//Lương một ngày
public double tinhLuong(){
154

return ngayCong*luongNgay;
}
}
public class Polymorphism {
public static void main(String[] args) {
CanBoCoHuu c1 = new CanBoCoHuu();
c1.hesoLuong = 2;
CanBoCoHuu c2 = new CanBoCoHuu();
c2.hesoLuong = 3;
NhanVienHopDong n = new NhanVienHopDong();
n.luongThoaThuan = 500000;
LaoDongThoiVu l = new LaoDongThoiVu();
l.ngayCong = 20; l.luongNgay = 10000;
Nguoi[] nhansu = {c1, c2, n, l};
double tongLuong = 0;
for (int i =0; i<nhansu.length;i++)
tongLuong = tongLuong + nhansu[i].tinhLuong();
System.out.println("Tổng lương phải trả một tháng là: "+
tongLuong);
}
}
Kết quả chạy chương trình:
Tổng lương phải trả một tháng là: 5700000.0
Chương trình trên tính lương phải trả hàng tháng cho cơ quan có 02 cán
bộ cơ hữu, 01 nhân viên hợp đồng và 01 lao động thời vụ. Có thể thấy rằng, với
khả năng đa hình, chương trình tính lương rất gọn chỉ gồm một vòng lặp, việc
tính lương đã được tổng quát hóa và thuật toán tính lương không phụ thuộc vào
loại nhân viên cũng như số lượng trong từng loại.
4.5. GÓI (PACKAGE)
4.5.1. Tạo và sử dụng các gói
Gói (package) trong Java giúp tìm kiếm, sử dụng và kiểm soát truy cập
các kiểu dễ dàng hơn, tránh các xung đột đặt tên.
Định nghĩa: Một gói (package) là nhóm các kiểu liên quan, hỗ trợ quản lý
không gian tên và bảo vệ truy cập. Trong đó, các kiểu có thể là các lớp, các giao
diện, các kiểu liệt kê, và các kiểu chú thích.
155

Các kiểu là một phần của nền tảng Java. Các kiểu được nhóm vào các
nhóm khác nhau theo chức năng: các lớp cơ bản trong gói java.lang, các lớp đọc
và ghi trong gói java.io,… Chúng ta cũng có thể đặt các kiểu của mình trong các
gói.
Giả sử cần viết một nhóm các lớp biểu diễn các đối tượng đồ họa như
hình tròn, hình chữ nhật, đoạn thẳng và một giao diện Keo, giao diện Keo có ý
nghĩa là nếu một lớp mà cài đặt nó thì đối tượng của lớp đó có thể kéo thả được
bằng chuột trên cửa sổ chương trình. Khung các lớp và giao diện như sau:
//file Keo.java
public interface Keo {
...
}
//file Hinh.java
public abstract class Hinh {
...
}
//file HinhTron.java
public class HinhTron extends Hinh implements Keo {
...
}
//file HinhChuNhat.java
public class HinhChuNhat extends Hinh implements Keo {
...
}
//file Diem.java
public class Diem extends Hinh implements Keo {
...
}
//file DoanThang.java
public class DoanThang extends Hinh implements Keo {
...
}
Chúng ta có thể thấy rằng, nên gom những lớp và giao diện này trong một
gói vì một số lý do sau:
- Các kiểu này có liên quan với nhau.
156

- Chúng ta biết tìm các kiểu hỗ trợ các tính năng về đồ họa ở đâu.
- Tên của các kiểu lớp và giao diện không xung đột với tên kiểu trong các
gói khác bởi vì gói này tạo ra một không gian tên mới.
- Chúng ta có thể cho phép các kiểu trong cùng gói được truy cập không
giới hạn tới các kiểu khác nhưng vẫn hạn chế truy cập đối với các kiểu ở ngoài
gói này.
4.5.2. Tạo gói
Để tạo một gói, cần đặt tên cho gói (các quy ước đặt tên được thảo luận ở
phần tiếp theo) và viết một lệnh package cùng với tên của gói đó tại đầu của mỗi
file nguồn các kiểu (lớp, giao diện, kiểu liệt kê, annotation) mà chúng ta muốn
cho vào gói này. Cú pháp:
package tên_gói;
Ví dụ:
package hinhhoa;
Câu lệnh package phải là dòng đầu tiên trong file mã nguồn. Chỉ có thể có
một câu lệnh gói trong mỗi file nguồn, và nó áp dụng cho tất cả các kiểu trong
file.
Ở đây cần chú ý là nếu viết nhiều kiểu trong một file nguồn, thì chỉ có
một kiểu là public, và kiểu đó phải có cùng tên với phần cơ sở của file nguồn. Ví
dụ, chúng ta có thể định nghĩa lớp public HinhTron trong file HinhTron.java,
định nghĩa giao diện public Keo trong file Keo.java, định nghĩa kiểu liệt kê
(enum) public Ngay trong file Ngay.java,…
Chúng ta có thể cho các kiểu không là public (non-public) vào cùng một
file như kiểu public. Tuy nhiên, điều này không được khuyến khích, trừ khi các
kiểu non-public mang vai trò phụ trợ cho kiểu public trong file đó. Khi có các
kiểu non-public trong cùng file với kiểu public, thì chỉ có kiểu public có thể
được truy cập từ ngoài gói này. Tất cả các kiểu non-public ở mức đỉnh (không là
các kiểu lồng) sẽ là package private, nghĩa là các kiểu này chỉ có thể truy cập
được bởi các lớp trong cùng gói.
Nếu chúng ta muốn cho các lớp và giao diện đồ họa liệt kê trong phần
trước vào một gói gọi là hinhhoa, thì sẽ cần sáu file nguồn có khung như sau:
//file Keo.java
package hinhhoa;
public interface Keo {...}
//file Hinh.java
157

package hinhhoa;
public abstract class Hinh {
...
}
//file HinhTron.java
package hinhhoa;
public class HinhTron extends Hinh implements Keo {...}
//file HinhChuNhat.java
package hinhhoa;
public class HinhChuNhat extends Hinh implements Keo {
...
}
//file Diem.java
package hinhhoa;
public class Diem extends Hinh implements Keo {
...
}
//file DoanThang.java
package hinhhoa;
public class DoanThang extends Hinh implements Keo {
...
}
Để ý rằng, dòng đầu tiên trong các file trên là khai báo gói, nghĩa là tất cả
các kiểu khai báo trên được gom vào trong gói có tên là hinhhoa.
Nếu không dùng lệnh package, thì kiểu được khai báo sẽ thuộc một gói
không tên. Thông thường, một gói không tên chỉ dành cho các chương trình nhỏ
hoặc tạm thời hoặc khi vừa bắt đầu quá trình phát triển. Còn đối với một chương
trình chuyên nghiệp thì các kiểu thường nằm trong các gói có tên.
4.5.3. Đặt tên cho gói
Với các lập trình viên trên khắp thế giới, viết các lớp và các giao diện
bằng ngôn ngữ lập trình Java, nhiều lập trình viên sẽ có khả năng dùng cùng một
tên cho các kiểu khác nhau. Ví dụ: ta định nghĩa lớp Rectangle trong gói
graphics khi đã có lớp Rectangle trong gói java.awt. Trình dịch vẫn cho phép cả
hai lớp có cùng tên nếu chúng ở các gói khác nhau. Tên đầy đủ của mỗi lớp bao
gồm cả tên gói. Tên đầy đủ của lớp Rectangle trong gói graphics là
158

graphics.Rectangle, và tên đầy đủ của lớp Rectangle trong gói java.awt là


java.awt.Rectangle.
Chương trình làm việc tốt trừ khi trong chương trình có các kiểu có tên
đầy đủ giống nhau (tên gói trùng nhau). Để tránh vấn đề này, cần đặt tên gói
theo các quy ước.
Các quy ước đặt tên gói:
- Tên của gói được viết bằng chữ thường để tránh xung đột với tên của
các lớp và các giao diện;
- Các công ty dùng các tên miền Internet đảo ngược để bắt đầu các tên gói
của họ, ví dụ, com.vidu.tengoi cho gói có tên là tengoi được một lập trình viên
tạo ra tại vidu.com;
Những xung đột tên xảy ra trong một cơ quan cần được xử lý bằng quy
ước trong cơ quan, có thể theo vùng hoặc tên dự án sau tên cơ quan (ví dụ,
com.vidu.tenduan.tengoi).
Các gói trong ngôn ngữ Java bắt đầu với java. hoặc javax.
Trong một số trường hợp, tên miền Internet có thể không là tên gói hợp lệ.
Điều này có thể xảy ra nếu tên miền chứa một dấu nối hoặc một ký tự đặc biệt
nào đó, hoặc tên gói bắt đầu bằng một chữ số hoặc một ký tự không hợp lệ theo
quy định đặt tên trong Java, hoặc nếu tên gói bao gồm một từ khóa đã có ví dụ
“float”. Trong tình huống này, quy ước đặt ra là bổ sung thêm một dấu gạch
dưới. Bảng 4.2 ví dụ việc chuẩn hóa tên các gói.
Bảng 4.2. Ví dụ chuẩn hóa các tên gói
Tên miền Tên gói
chi-nhanh-x.vidu.com com.vidu.chinh_nhanh_x
vidu.float float_.vidu
012ten.vidu.com com.vidu._012ten
4.5.4. Sử dụng các thành viên của gói
Các kiểu trong một gói được gọi là các thành viên của gói. Khi muốn sử
dụng một thành viên gói có bộ điều chỉnh truy cập public ở bên ngoài gói, cần
phải làm một trong những điều dưới đây:
- Tham chiếu tới thành viên này bằng tên đầy đủ;
- Chỉ định sử dụng (bằng từ khóa import) thành viên này;
- Chỉ định sử dụng (bằng từ khóa import) toàn bộ gói của thành viên.
Chúng ta xem xét cụ thể từng cách sử dụng.
- Tham chiếu tới một thành viên gói thông qua tên đầy đủ:
159

Có thể dùng tên đơn giản của thành viên gói nếu chương trình đang được
viết nằm trong cùng một gói với thành viên đó hoặc nếu thành viên đó đã được
chỉ định sử dụng (import) và không trùng tên với thành viên nào trong gói.
Tuy nhiên, nếu chúng ta muốn dùng một thành viên từ một gói khác và
gói đó chưa được chỉ định (import), thì phải dùng tên đầy đủ của thành viên, bao
gồm tên gói.
Ví dụ, với lớp HinhChuNhat được khai báo trong gói hinhhoa ở trên thì
tên đầy đủ là hinhhoa.HinhChuNhat và chúng ta có thể dùng tên này để tạo một
đối tượng theo cú pháp sau:
hinhhoa.HinhChuNhat hinhChuNhat =
new hinhhoa.HinhChuNhat();
Các tên đầy đủ có thể chấp nhận được khi không được sử dụng thường
xuyên. Nhưng nếu một tên đầy đủ được dùng đi dùng lại nhiều lần, thì cách viết
tên kiểu này trở nên nhàm chán và chương trình khó đọc. Để khắc phục, có thể
dùng từ khóa import chỉ định một thành viên nào đó hoặc gói chứa nó, và sau đó
có thể sử dụng thành viên một cách đơn giản.
- Chỉ định dùng một thành viên của gói:
Để chỉ định sử dụng một thành viên cụ thể trong file chương trình hiện
thời, cần viết lệnh import tại vị trí bắt đầu của file (hoặc sau lệnh package nếu
có lệnh này) và trước bất kỳ định nghĩa kiểu nào. Cú pháp:
import ten_goi.ten_lop;
Ví dụ cú pháp import để chỉ định dùng lớp HinhChuNhat từ gói hinhhoa
đã được tạo ra ở trên:
import hinhhoa.HinhChuNhat;
Bây giờ chúng ta có thể tham chiếu tới lớp HinhChuNhat bằng tên đơn
giản.
HinhChuNhat hinhChuNhat = new HinhChuNhat();
Cách sử dụng thành viên này nên được dùng nếu chúng ta chỉ dùng một
số thành viên của gói. Còn nếu như muốn sử dụng nhiều kiểu từ một gói, thì nên
chỉ định sử dụng cả gói.
- Chỉ định sử dụng cả gói:
Để chỉ định sử dụng tất cả các kiểu chứa trong một gói cụ thể, dùng lệnh
import với dấu *. Ví dụ, cú pháp yêu cầu sử dụng tất cả các thành viên của gói
hinhhoa:
import hinhhoa.*;
160

Sau lệnh này, chúng ta có thể tham chiếu tới bất kỳ lớp nào hay giao diện
nào trong gói hinhhoa bằng tên đơn giản. Ví dụ:
HinhTron hinhTron = new HinhTron();
HinhChuNhat hinhChuNhat = new HinhChuNhat();
Có một dạng ít phổ biến khác của import cho phép chỉ định sử dụng các
lớp lồng public của một lớp chứa. Ví dụ, nếu lớp hinhhoa.HinhChuNhat chứa
các lớp lồng, ví dụ HinhChuNhat.HinhVuong, thì có thể chỉ định sử dụng
HinhChuNhat và các lớp lồng bằng cách dùng hai câu lệnh dưới đây. Câu lệnh
import thứ hai không chỉ định sử dụng HinhChuNhat.
import hinhhoa.HinhChuNhat;
import hinhhoa.HinhChuNhat.*;
Để thuận tiện, trình dịch Java tự động chỉ định sử dụng (import) hai gói
cho mỗi file nguồn là gói java.lang và gói hiện tại (gói chứa file hiện tại).
Các gói và lớp được định nghĩa có tính thứ bậc, nghĩa là các gói có thể
chứa gói và lớp khác dựa vào tên gọi, nhưng điều này không luôn luôn đúng. Ví
dụ, trong Java có gói java.awt và các lớp java.awt.color, java.awt.font, và nhiều
lớp khác bắt đầu với java.awt. Tuy nhiên, các lớp java.awt.color, java.awt.font,
và các gói java.awt.* khác không có trong gói java.awt. Tiền tố java.awt (Java
Abstract Window Toolkit) được dùng cho một số gói và lớp liên quan, chứ
không phải thể hiện các gói và lớp đó nằm trong gói java.awt.
Lệnh import java.awt.* sẽ chỉ định sử dụng tất cả các kiểu trong gói
java.awt, nhưng không chỉ định java.awt.color, java.awt.font, hoặc gói nào đó
có dạng java.awt.tengoi. Nếu chúng ta muốn dùng các lớp và các kiểu khác
trong java.awt.tengoi cũng như trong java.awt, cần phải chỉ định cả hai gói cùng
với tất cả các file của chúng như sau:
import java.awt.*;
import java.awt.tengoi.*;
Gói được tạo ra với mục đích tránh xung đột về tên gọi, nhưng vẫn có thể
có trường hợp nhập nhằng về tên. Nếu một thành viên trong một gói chia sẻ tên
với một thành viên trong một gói khác và cả hai gói đều được chỉ định (import)
vào chương trình, thì cần phải tham chiếu tới mỗi thành viên bằng tên đầy đủ
của chúng. Ví dụ, trong gói hinhhoa đã định nghĩa lớp HinhChuNhat và một gói
tengoi nào đó cũng có lớp HinhChuNhat. Nếu cả hai gói được import vào
chương trình, thì câu lệnh sau là nhập nhằng vì trình dịch không biết lớp
HinhChuNhat là lớp từ gói nào:
161

HinhChuNhat hinhChuNhat;
Trong tình huống này, cần phải dùng tên đầy đủ để chỉ ra chính xác lớp
HinhChuNhat mà chúng ta muốn dùng. Ví dụ:
hinhhoa.HinhChuNhat hinhChuNhat;
Lệnh import có thể đi kèm với từ khóa static. Có những tình huống cần
truy cập thường xuyên tới các trường static final (các hằng số) và các phương
thức static từ một hoặc hai lớp. Việc truy cập đến các thành viên static này
thông qua tiền tố là tên lớp của chúng có thể làm rối chương trình.
Để khắc phục, có thể sử dụng các lệnh import static. Lệnh này cho phép
chỉ định các hằng số và các phương thức static để có thể sử dụng mà không phải
dùng đến tiền tố tên lớp để gọi.
Ví dụ, lớp java.lang.Math định nghĩa hằng số PI và nhiều phương thức
static, bao gồm các phương thức tính toán sin(x), cos(x), tang(x), căn bậc hai,
giá trị lớn nhất, giá trị nhỏ nhất, mũ,… Cách bình thường để dùng những đối
tượng này từ một lớp khác là dùng tiền tố tên lớp để gọi, kiểu như sau:
double r = Math.cos(Math.PI * goc);
Khi dùng lệnh import static để chỉ định sử dụng các thành viên static của
java.lang.Math, thì không cần dùng tiền tố tên lớp khi gọi. Các thành viên static
của Math có thể được chỉ định đơn lẻ:
import static java.lang.Math.PI;
hoặc theo nhóm: import static java.lang.Math.*;
Khi chúng được chỉ định, các thành viên static có thể được dùng không
cần tên đầy đủ, ví dụ: double r = cos(PI * goc);
Rõ ràng, chúng ta có thể viết các lớp của riêng mình chứa các hằng số và
các phương thức static mà chúng ta thường dùng, và sau đó dùng lệnh import
static. Ví dụ:
import static goihangvaphuongthucstatic.CacHangSo.*;
Tuy nhiên cũng nên dùng import static một cách hạn chế. Việc dùng
import static quá nhiều có thể làm chương trình khó đọc và bảo trì, bởi vì người
đọc không biết một đối tượng static thuộc về một lớp cụ thể nào. Khi được sử
dụng hợp lý, import static làm chương trình dễ đọc hơn do loại bỏ được việc lặp
lại của tên lớp.
Trong Java, lớp hay giao diện được lưu trữ trong các file, còn gói là các
thư mục.
4.5.5. Một số gói cơ bản của Java
162

Nền tảng Java hỗ trợ rất nhiều lớp để các lập trình viên có thể sử dụng
phát triển ứng dụng của mình. Các lớp này được nhóm lại trong các gói. Các gói
cơ bản được miêu tả trong Bảng 4.3.
Bảng 4.3. Các gói cơ bản của Java
Tên gói Mô tả
java.lang Cung cấp các lớp cơ bản cho việc xây dựng chương
trình trình Java. Gói này bao gồm các lớp như: Object,
Class, Boolean, Character, Integer, Long, Float,
Double, Math, String, StringBuffer, StringBuilder,...
java.awt Bao gồm tất cả các lớp để tạo các giao diện người dùng
và để tô vẽ đồ họa, hình ảnh. Gói này bao gồm các lớp
như: lớp Button, CheckBox, Graphics, Image,…
java.io Cung cấp dữ liệu vào và ra cho hệ thống thông qua các
luồng dữ liệu, sự tuần tự hóa và hệ thống file. Gói này
bao gồm các lớp như: BufferedInputStream,
BufferedOutputStream, BuferedReader,
BufferedWriter,…
java.util Bao gồm bộ khung các tập hợp, các lớp tập hợp, mô
hình sự kiện, các kiểu về ngày và thời gian, và các lớp
tiện ích hỗn hợp. Gói này bao gồm các giao diện như:
EventListener, Observer,… và các lớp như: ArrayList,
Arrays, Calendar, Random,…
java.net Cung cấp các lớp cài đặt ứng dụng mạng. Gói này bao
gồm các lớp như: ServerSocket, Socket,
SocketAddress,…
java.awt.event Cung cấp các giao diện và các lớp xử lý các kiểu sự
kiện khác sau gây ra bởi các thành phần AWT. Gói này
bao gồm các giao diện như: ActionListener,
ComponentListener, KeyListener, MouseListener,… và
các lớp như: ActionEvent, ComponentEvent, KeyEvent,
MouseEvent,…
java.rmi Cung cấp gói RMI. RMI là viết tắt của Remote Method
Invocation (gọi phương thức từ xa). RMI là cơ chế cho
phép một đối tượng trên một máy ảo Java gọi các
phương thức của một đối tượng trong một máy ảo Java
163

Tên gói Mô tả
khác. Bất kỳ đối tượng nào được gọi theo cách này phải
cài đặt giao diện Remote trong gói này.
java.security Cung cấp các lớp và các giao diện cho chương trình
khung an ninh. Gói này bao gồm các lớp như
AccessController, KeyPair (giữ khóa công khai và khóa
bí mật), KeyPairGenerator,…
java.sql Cung cấp API để truy cập và xử lý dữ liệu lưu trữ trong
một nguồn dữ liệu nào đó (thường là một cơ sở dữ liệu
quan hệ) sử dụng ngôn ngữ lập trình Java. Gói này bao
gồm các giao diện như Connection, SQLData,
SQLInput, SQLOutput,.. và các lớp như
DriverManager, SQLPermission,…
TỔNG KẾT CHƯƠNG
Chương 4 đã đề cập một cách chi tiết đến các nội dung:
- Giao diện là khuôn đúc ra các lớp có các hành vi (có tên) giống nhau,
nhưng cách thực hiện các hành vi có thể khác nhau. Giao diện là sự tổng quát
của lớp, có mức trừu tượng hóa cao hơn lớp. Thông thường, các phương thức
trong giao diện là những phương thức không có thân, thân các phương thức giao
diện được cung cấp trong các lớp cài đặt giao diện.
- Khả năng thừa kế của lập trình hướng đối tượng cho phép các lớp thừa
kế trạng thái (các thuộc tính) và hành vi (phương thức) từ một lớp khác mà
không cần phải định nghĩa lại. Lớp thừa kế được gọi là lớp dẫn xuất, hoặc lớp
con; lớp được thừa kế được gọi là lớp cơ sở hoặc lớp cha. Khả năng thừa kế cho
phép tái sử dụng code, giúp việc viết code được nhanh hơn, cho phép mở rộng
và chỉnh sửa những kiểu dữ liệu đã có.
- Khi định nghĩa lớp dẫn xuất qua kế thừa, có thể định nghĩa lại các
phương thức của lớp cơ sở để phù hợp hơn với lớp dẫn xuất. Việc định nghĩa lại
các phương thức của lớp cơ sở trong lớp dẫn xuất được gọi là ghi đè
(overriding).
- Thông qua kế thừa, các lớp sẽ tạo nên một cây phả hệ, ở nút gốc của cây
phả hệ là lớp Object. Tất cả các lớp đều là lớp dẫn xuất trực tiếp hoặc gián tiếp
của lớp Object. Trong tất cả các lớp đều có các phương thức của lớp Object. Tuy
nhiên khi sử dụng, các phương thức của lớp Object nên được ghi đè.
- Lớp trừu tượng là lớp có phương thức trừu tượng, phương thức trừu
164

tượng là phương thức không có thân giống như phương thức giao diện. Phương
thức trừu tượng được cung cấp thân trong các lớp dẫn xuất khi kế thừa. Lớp trừu
tượng có mức trừu tượng hóa cao hơn lớp thông thường nhưng thấp hơn giao
diện.
- Đa hình trong lập trình hướng đối tượng là sự thể hiện việc một hành vi
(lời gọi một phương thức) có thể được thực hiện theo các cách khác nhau. Đa
hình được hiện thực thông qua sự nối kết động giữa tham chiếu gọi và phương
thức được gọi. Đa hình giúp việc lập trình được tổng quát hơn.
- Việc gom nhóm các kiểu liên quan vào các gói cho phép quản lý các
kiểu được tốt hơn, tránh được sự xung đột trong cách đặt tên kiểu.
BÀI TẬP
1. Xây dựng chương trình gồm lớp HinhTron (hình tròn) và lớp HinhTru
(hình trụ) thừa kế lớp HinhTron, và lớp KiemTra chứa phương thức main() để
kiểm tra các lớp.
- Lớp HinhTron gồm:
+ Thuộc tính banKinh (bán kính hình tròn);
+ Các constructor không tham số và có tham số;
+ Phương thức lấy và đặt giá trị thuộc tính banKinh, phương thức tính
diện tích, và phương thức toString() trả về một xâu ký tự chứa giá trị của thuộc
tính banKinh.
- Lớp HinhTru gồm:
+ Thuộc tính chieuCao (chiều cao hình trụ);
+ Các constructor không tham số, và có tham số;
+ Phương thức lấy và đặt giá trị cho thuộc tính chieuCao, phương thức
tính thể tích của hình trụ, và phương thức toString() trả về một xâu ký tự chứa
giá trị của các thuộc tính (chú ý sử dụng phương thức toString() của lớp
HinhTron).
2. Xây dựng chương trình gồm lớp Nguoi (người), lớp SinhVien (sinh
viên), lớp GiaoVien (giáo viên) thừa kế lớp Nguoi, và lớp KiemTra chứa phương
thức main() để kiểm tra các lớp.
- Lớp Nguoi gồm:
+ Các thuộc tính maSo (mã số), hoTen (họ tên), ngaySinh (ngày sinh),
diaChi (địa chỉ);
+ Các constructor không tham số và có tham số;
+ Các phương thức lấy và đặt giá trị cho các thuộc tính (không có phương
165

thức đặt giá trị cho thuộc tính maSo), và phương thức toString() trả về xâu ký tự
chứa giá trị của các thuộc tính.
- Lớp SinhVien gồm:
+ Các thuộc tính nganh (ngành), khoa (khoa), namNhapHoc (năm nhập
học), namTotNghiep (năm tốt nghiệp);
+ Các constructor không tham số và có tham số;
+ Các phương thức lấy và đặt giá trị cho các thuộc tính, phương thức
toString() trả về xâu ký tự chứa giá trị của các thuộc tính.
- Lớp GiaoVien gồm:
+ Các thuộc tính boMon (bộ môn), khoa (khoa), hocHam (học hàm),
hocVi (học vị), luong (lương);
+ Constructor khởi tạo có tham số;
+ Các phương lấy và đặt giá trị cho các thuộc tính, phương thức toString()
trả về xâu ký tự chứa giá trị của các thuộc tính.
3. Xây dựng chương trình gồm lớp KhanhHang (khách hàng), lớp
KhachHangTX (khách hàng thường xuyên) thừa kế lớp KhachHang, lớp
KiemTra chứa phương thức main() để kiểm tra các lớp.
- Lớp KhachHang gồm:
+ Các thuộc tính maKhachHang (mã khách hàng), hoTen (họ tên), diaChi
(địa chỉ), tienTra (tiền khách hàng phải trả)
+ Constructor có tham số
+ Các phương thức lấy và đặt giá trị cho các thuộc tính (không có phương
thức đặt giá trị cho thuộc tính maKhachHang), phương thức toString() trả về xâu
ký tự chứa các giá trị của các thuộc tính.
- Lớp KhachHangTX gồm:
+ Thuộc tính giamGia (phần trăm giảm giá)
+ Constructor có tham số
+ Các phương thức lấy và đặt giá trị cho thuộc tính giamGia, phương thức
tính tiền theo thuộc tính giamGia, phương thức toString() trả về xâu chứa các
giá trị của các thuộc tính (chú ý sử dụng phương thức toString() của lớp
KhachHang).
4. Xây dựng chương trình gồm lớp Diem (điểm), lớp DiemDiChuyen
(điểm có thể di chuyển), lớp KiemTra chứa phương thức main() để kiểm tra các
lớp.
- Lớp Diem gồm:
166

+ Các thuộc tính x, y là các toạ độ điểm;


+ Constructor không tham số và có tham số;
+ Các phương thức lấy và đặt giá trị cho từng thuộc tính, và tất cả các
thuộc tính, phương thức toString() trả về xâu ký tự chứa giá trị của các thuộc
tính.
- Lớp DiemDiChuyen gồm:
+ Các thuộc tính vx (vận tốc theo phương ngang), vy (vận tốc theo phương
dọc);
+ Constructor không tham số và có tham số;
+ Các phương thức lấy và đặt giá trị cho từng thuộc tính, và tất cả các
thuộc tính, phương thức di chuyển với tham số truyền vào là khoảng thời gian di
chuyển, phương thức toString() trả về xâu bao gồm giá trị của các thuộc tính
(chú ý sử dụng phương thức toString() của lớp Diem).
5. Xây dựng chương trình gồm giao diện GiaoDienDiChuyen chứa các
phương thức di chuyển của các đối tượng hình họa, lớp DiemDiChuyen (điểm có
thể di chuyển), lớp HinhTronDiChuyen (hình tròn có thể di chuyển), và lớp
KiemTra chứa phương thức main() để kiểm tra các lớp.
- Giao diện GiaoDienDiChuyen bao gồm:
+ Các phương thức diLen(), diXuong(), sangTrai(), sangPhai() lần lượt
cho cách hành vi di chuyển đi lên, đi xuống, sang trái, sang phải.
- Lớp DiemDiChuyen bao gồm:
+ Các biến thuộc tính x, y, vx, vy lần lượt là hoành độ, tung độ, vận tốc
theo phương ngang, vận tốc theo phương dọc;
+ Constructor có tham số;
+ Phương thức toString() và các phương thức cài đặt các phương thức của
GiaoDienDiChuyen.
- Lớp HinhTronDiChuyen bao gồm:
+ Các biến thuộc tính r, tam lần lượt là bán kính, và tâm của hình tròn có
kiểu DiemDiChuyen;
+ Constructor có tham số;
+ Phương thức toString() và các phương thức cài đặt các phương thức của
GiaoDienDiChuyen.
6. Xây dựng chương trình gồm:
- Giao diện DoiTuongHinhHoc (đối tượng hình học) gồm các phương
thức tính diện tích và tính chu vi;
167

- Giao diện ThayDoiKichThuoc (thay đổi kích thước) có phương thức thay
đổi kích thước với tham số truyền vào là phần trăm thay đổi (phần trăm thay đổi
có thể là âm, bằng không, hoặc dương, khi đó kích thước của đối tượng hình học
sẽ giảm, không thay đổi, hoặc tăng);
- Lớp HinhTron (hình tròn) cài đặt giao diện DoiTuongHinhHoc gồm
thuộc tính banKinh (bán kính), constructor khởi tạo có tham số, phương thức
tính diện tích, phương thức tính chu vi;
- Lớp HinhTronThayDoi (hình tròn có thể thay đổi kích thước) thừa kế
lớp HinhTron và cài đặt giao diện ThayDoiKichThuoc chứa constructor có tham
số, và phương thức thay đổi kích thước.
- Lớp KiemTra chứa phương thức main() để kiểm tra các lớp.

Câu hỏi ôn tập


1. Cho giao diện sau:
public interface GiaoDien {
void phuongThuc1(int i);
default void phuongThuc2(int i) {
//thân phương thức
}
default void phuongThuc3(int i) {
//thân phương thức
}
void phuongThuc4(int i);
}
Một lớp cài đặt GiaoDien có thể cài đặt những phương thức nào?
2. Giao diện sau lỗi ở đâu?
public interface GiaoDien {
void phuongThuc1(int i){
System.out.println("i = " + i);
}
}
Cách sửa lỗi có thể như thế nào?
3. Giao diện sau có bị lỗi không?
public interface GiaoDien{
168

}
4. Cho hai lớp sau:
public class LopX {
public void phuongThuc1(int i) {
}
public void phuongThuc2(int i) {
}
public static void phuongThuc3(int i) {
}
public static void phuongThuc4(int i) {
}
}

public class LopY extends LopX {


public static void phuongThuc1(int i) {
}
public void phuongThuc2(int i) {
}
public void phuongThuc3(int i) {
}
public static void phuongThuc4(int i) {
}
}
a) Phương thức nào ghi đè một phương thức trong lớp cha?
b) Phương thức nào che phủ một phương thức trong lớp cha?
c) Các phương thức khác làm gì?
5. Các điểm khác giữa giao diện và lớp trừu tượng là gì?
6. Cho định nghĩa các lớp:
abstract public class DongVat {
abstract public void chao();
}
public class Meo extends DongVat {
@Override
public void chao() {
169

System.out.println("meo chao");
}
}
public class Cho extends DongVat {
@Override
public void chao() {
System.out.println("cho chao");
}
public void chao(Cho cho) {
System.out.println("cho chao cho");
}
}
public class ChoTo extends Cho {
@Override
public void chao() {System.out.println("cho to chao");}
@Override
public void chao(Cho cho) {
System.out.println("cho to chao cho");
}
}
Hãy cho biết kết quả in ra màn hình hoặc lỗi (nếu có) của chương trình
kiểm tra sau:
public class KiemTraDongVat {
public static void main(String[] args) {
// Sử dụng các lớp con
Meo meo1 = new Meo();
meo1.chao();
Cho cho1 = new Cho();
cho1.chao();
ChoTo choTo1 = new ChoTo();
choTo1.chao();
// Sử dụng đa hình
DongVat dongVat1 = new Meo();
dongVat1.chao();
DongVat dongVat2 = new Cho();
170

dongVat2.chao();
DongVat dongVat3 = new ChoTo();
dongVat3.chao();
DongVat dongVat4 = new DongVat();
// chuyển kiểu
Cho cho2 = (Cho)dongVat2;
ChoTo choTo2 = (ChoTo)dongVat3;
Cho cho3 = (Cho)dongVat3;
Meo meo2 = (Meo)dongVat2;
cho2.chao(cho3);
cho3.chao(cho2);
cho2.chao(choTo2);
choTo2.chao(cho2);
choTo2.chao(choTo1);
}
}
171

Chương 5
MẢNG VÀ XÂU KÝ TỰ
5.1. GIỚI THIỆU VỀ MẢNG
Với những cách khai báo biến đã làm quen thì mỗi lần khai báo chúng ta
chỉ khai báo được một vài biến. Đối với những chương trình cần xử lý một số
lượng dữ liệu lớn, cần khai báo rất nhiều biến để lưu trữ dữ liệu thì với cách khai
báo mỗi lần một vài biến sẽ không thích hợp. Nghĩa là cần phải có cơ chế cho
phép khai báo một lần một số lượng lớn các biến. Trong các ngôn ngữ lập trình
nói chung và Java nói riêng, mảng là cơ chế cho phép thực hiện việc khai báo
như vậy.
Về khái niệm, mảng là tập hợp nhiều phần tử có cùng tên, cùng kiểu dữ
liệu và mỗi phần tử trong mảng được truy xuất thông qua chỉ số của nó trong
mảng.
Trong Java có hai loại mảng: mảng một chiều và mảng nhiều chiều.
Trước hết chúng ta sẽ làm quen với mảng một chiều.
5.2. MẢNG MỘT CHIỀU
Để sử dụng mảng, đầu tiên cũng cần phải khai báo mảng. Có hai dạng cú
pháp khai báo mảng:
- <kiểu dữ liệu> <tên mảng>[];
- <kiểu dữ liệu>[] <tên mảng>;
Ví dụ:
int arrInt[];
int[] arrInt;
int[] arrInt1, arrInt2, arrInt3;
Mảng là một kiểu dữ liệu tham chiếu, khi khai báo với cú pháp như trên
mới chỉ có biến tham chiếu được tạo ra và chưa có phần tử mảng nào tồn tại.
Nghĩa là, để có thể sử dụng được mảng, cần tạo vùng bộ nhớ để lưu trữ các phần
tử của mảng và trỏ (gán) biến tham chiếu đến vùng bộ nhớ đó. Công việc này
còn được gọi là khởi tạo, các thông số cần chỉ ra khi khởi tạo gồm số phần tử và
giá trị của các phần tử.
Việc khởi tạo có thể được thực hiện ngay khi khai báo hoặc thực hiện sau.
Ví dụ khởi tạo khi khai báo:
int arrInt[] = {1, 2, 3};
char arrChar[] = {‘a’, ‘b’, ‘c’};
String arrString[] = {“ABC”, “EFG”, ‘”DFD”};
172

Với cách khởi tạo trên mảng arrInt sẽ có 3 phần tử kiểu số nguyên, có giá
trị tương ứng là 1, 2, 3. Mảng arrChar có 3 phần tử kiểu ký tự, có giá trị tương
ứng là ‘a’, ‘b’, ‘c’. Mảng arrString có 3 phần tử kiểu xâu ký tự, có giá trị tương
ứng là “ABC”, “EFG”, “DFD”.
Việc khởi tạo cũng có thể được tiến hành sau khi khai báo. Số lượng phần
tử hay lượng bộ nhớ dùng để lưu trữ mảng được xác định bằng từ khóa new. Cú
pháp:
<Tên mảng> = new <Kiểu dữ liệu>[Số phần tử];
Ví dụ: Khởi tạo các mảng 3 phần tử với các phần tử có giá trị ngầm định.
arrInt = new int[3];
arrChar = new char[3];
arrString = new String[3];
Có thể cấp phát bộ nhớ ngay khi khai báo mảng. Cú pháp:
<Kiểu dữ liệu> <Tên mảng>[]=new <Kiểu dữ liệu>[Số phần tử];
Ví dụ khai báo mảng mang kiểu dữ liệu int có 100 phần tử:
int mang[]=new int[100];
Với cách khởi tạo này, các phần tử của mảng sẽ có giá trị ngầm định
tương ứng với kiểu dữ liệu mảng. Số phần tử của mảng sau khi khởi tạo là
không thay đổi được, muốn thay đổi phải cấp phát lại bộ nhớ.
Các phần tử mảng được truy cập qua chỉ số, chỉ số mảng trong Java bắt
đầu từ 0. Phần tử đầu tiên có chỉ số là 0, phần tử thứ n có chỉ số là n-1. Các phần
tử của mảng được truy xuất thông qua chỉ số của nó được đặt giữa cặp dấu
ngoặc vuông [].
Ví dụ:
int arrInt[] = {1, 2, 3};
int x = arrInt[0]; // x sẽ có giá trị là 1.
int y = arrInt[1]; // y sẽ có giá trị là 2.
int z = arrInt[2]; // z sẽ có giá trị là 3.
Để duyệt qua các phần tử của mảng, thông thường phải sử dụng vòng lặp.
Ví dụ 5.1. Chương trình nhập và xuất giá trị các phần tử của một mảng
các số nguyên:
class ArrayDemo{
public static void main(String args[]){
int [] arrInt = new int[10];
173

int i;
for(i = 0; i < 10; i = i+1) arrInt[i] = i;
for(i = 0; i < 10; i = i+1)
System.out.println("This is arrInt[" + i +"]: " +
arrInt[i]);
}
}
Chương trình tại Ví dụ 5.1 khai báo và khởi tạo một mảng kiểu số nguyên
gồm 10 phần tử, sau đó gán cho các phần tử các giá trị tương ứng từ 0 đến 9.
Sau khi gán giá trị xong, chương trình in các phần tử của mảng ra màn hình, mỗi
phần tử trên một dòng kèm theo chú thích. Kết quả nhận được trên màn hình:
This is arrInt[0]: 0
This is arrInt[1]: 1
This is arrInt[2]: 2
This is arrInt[3]: 3
This is arrInt[4]: 4
This is arrInt[5]: 5
This is arrInt[6]: 6
This is arrInt[7]: 7
This is arrInt[8]: 8
This is arrInt[9]: 9
Ví dụ 5.2. Chương trình tìm phần tử có giá trị nhỏ nhất (Min) và lớn nhất
(Max) trong một mảng:
class MinMax{
public static void main(String args[]){
int nums[] = { 99, -10, 100123, 18, -978, 5623, 463, -9, 287,
49 };
int min, max;
min = max = nums[0];
for(int i=1; i < 10; i++){
if(nums[i] < min) min = nums[i];
if(nums[i] > max) max = nums[i];
}
System.out.println("Min and max: " + min + " " + max);
}
174

}
Kết quả của chương trình như sau:
Min and max: -978 100123
Mảng trong Java là một đối tượng, có các thuộc tính và các phương thức.
Một thuộc tính khá cần thiết của đối tượng mảng là length, thuộc tính này lưu
trữ số phần tử của mảng. Đây là thuộc tính chỉ đọc, với chương trình Ví dụ 5.2
thì nums.length = 10.
Trong những chương trình lớn, lập trình viên đôi khi không nhớ được số
phần tử của một mảng đã được cấp phát và do đó có thể truy cập đến chỉ số
mảng nằm ngoài khoảng cho phép (chỉ số lớn hơn hoặc bằng số phần tử) dẫn
đến các lỗi ngoại lệ. Để tránh việc đó, có thể sử dụng thuộc tính length. Ví dụ,
trong chương trình có mảng a, khi khởi tạo có 10 phần tử, vòng lặp duyệt mảng
có thể có dạng như sau:
for(i = 0; i<10;i++){
//làm việc gì đó với a[i];
}
Với cách duyệt thế này, giả sử khi lập trình viên khởi tạo lại mảng, cấp
cho mảng số phần tử ít hơn 10 mà quên thay đổi điều kiện lặp i<10 của vòng lặp
trên thì chương trình sẽ chạy có lỗi. Để tránh tình huống này, vòng lặp nên có
dạng như sau:
for(i = 0; i<a. length;i++){
//làm việc gì đó với a[i];
}
Với cú pháp vòng lặp như trên thì chương trình sẽ không gặp lỗi truy xuất
phần tử mảng ngoài khoảng cho phép.
Vòng lặp foreach:
Ngoài cách dùng các vòng lặp thông thường, các phiên bản Java từ JDK
1.5 còn đưa vào một loại vòng lặp – vòng lặp for duyệt qua từng phần tử mảng
mà không cần sử dụng đến chỉ số phần tử.
Cú pháp:
for (declaration: expression){
//Statements
}
Ở đây:
- declaration: Là sự khai báo một biến với kiểu tương thích với các phần
175

tử của mảng cần truy cập (expression). Khi vòng lặp hoạt động, biến này sẽ lần
lượt nhận giá trị là các phần tử mảng expression.
- expression: Đề cập đến mảng cần duyệt, expression có thể là một biến
mảng hoặc lời gọi một phương thức trả về một mảng.
Ví dụ 5.3. Chương trình hiển thị ra màn hình tất cả các phần tử của mảng
myList:
public class TestArray {
public static void main(String[] args) {
double[] myList = {1.9, 2.9, 3.4, 3.5};
// In ra màn hình tất cả các phần tử mảng
for (double element: myList) {
System.out.println(element);
}
}
}

Kết quả nhận được trên màn hình như sau:


1.9
2.9
3.4
3.5
Các biến mảng cũng giống như biến của các kiểu dữ liệu đơn giản, có thể
được truyền vào làm tham số của các phương thức. Đoạn mã sau thể hiện việc
sử dụng phương thức để in ra các phần tử của mảng tham số:
public static void printArray(int[] array) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
Phương thức này có thể được gọi cùng với việc truyền một mảng cụ thể
làm tham số. Ví dụ, dòng mã sau gọi phương thức printArray để hiển thị các số
3, 1, 2, 6, 4, 2:
printArray(new int[]{3, 1, 2, 6, 4, 2});
Ngoài việc dùng biến mảng làm tham số, các phương thức cũng có thể trả
về giá trị là một mảng. Ví dụ phương thức sau trả về mảng đảo ngược thứ tự
176

phần tử của mảng tham số:


public static int[] reverse(int[] list) {
int[] result = new int[list.length];
for (int i = 0, j = result.length - 1; i < list.length; i++, j--) {
result[j] = list[i];
}
return result;
}
5.3. MẢNG NHIỀU CHIỀU
Khi khai báo mảng một chiều, nếu các phần tử của mảng một chiều cũng
là mảng (các phần tử có kiểu mảng) thì có nghĩa là chúng ta đã khai báo một
mảng nhiều chiều. Cú pháp khai báo một mảng n-chiều (n = 2,3,4,..) như sau:
<Kiểu dữ liệu>[][]...[] <Tên mảng>;
hoặc:
<Kiểu dữ liệu> <Tên mảng> [][]..[]; // (gồm n cặp ngoặc vuông [])
Cú pháp khai báo và cấp phát bộ nhớ cho mảng n chiều:
<Kiểu dữ liệu> <Tên mảng>[][]...[] = new <Kiểu dữ liệu>[Số phần tử
chiều 1][Số phần tử chiều 2].....[Số phần tử chiều n]
Có thể hiểu một cách đơn giản về mảng nhiều chiều như sau:
Mảng hai chiều là mảng một chiều mà các phần tử là các mảng một chiều;
Mảng ba chiều là mảng một chiều mà các phần tử là các mảng hai chiều;
...
Mảng n-chiều là mảng một chiều mà các phần tử là các mảng (n-1)-chiều.
Trong số các mảng nhiều chiều, mảng hai chiều là loại hay được sử dụng.
Mảng hai chiều có thể được hình dung như một bảng các phần tử được tổ chức
thành các hàng và cột.
Ví dụ, để lưu nhiệt độ của ba ngày khác nhau và mỗi ngày lưu 5 giá trị
nhiệt độ đo được, có thể định nghĩa một mảng hai chiều với kích thước 3 hàng
và 5 cột như sau:
double [ ] [ ] temperatures = new doule[3][5];
Lưu ý rằng ở cả hai phía trái và phải của câu lệnh trên phải sử dụng hai
cặp dấu ngoặc vuông “[ ][ ]”. Khai báo ở phía bên trái cho biết đây không phải
là mảng một chiều gồm các giá trị double mà là một bảng các giá trị double. Ở
phía bên phải, khi xây dựng mảng cần phải chỉ chiều (kích thước) của bảng.
Thông thường, số hàng được khai báo trước và sau đó là số cột. Lệnh khai báo
177

trên sẽ tạo ra một biến tham chiếu mảng temperatures trỏ đến một bảng các
phần tử như sau:

Để truy cập đến các phần tử cần sử dụng hai chỉ số, chỉ số thứ nhất xác
định chỉ số hàng chứa phần tử, còn chỉ số thứ hai xác định chỉ số cột chứa phần
tử cần truy cập. Với mảng n-chiều cần n chỉ số để truy cập vào các phần tử. Các
chỉ số cũng bắt đầu từ 0 giống như đối với mảng một chiều.
Ví dụ để lưu trữ nhiệt độ đo được lần thứ tư của ngày thứ nhất là 98.3 và
nhiệt độ lần đo thứ nhất của ngày thứ ba là 99.4 có thể viết mã như sau:
temperatures [ 0 ] [ 3 ] = 98.3;
temperatures [ 2 ] [ 0 ] = 99.4;
Sau khi thực thi các lệnh trên thì các giá trị được lưu trữ trong mảng lúc
này như sau:

Để thuận tiện trong quá trình truy xuất một phần tử của mảng theo từng
bước, cần bắt đầu suy nghĩ từ tên của mảng. Ví dụ, nếu muốn truy xuất đến phần
tử đầu tiên của hàng thứ ba trong mảng, có thể suy nghĩ tuần tự theo các bước
sau:
temperatures: toàn bộ bảng
temperatures [ 2 ]: toàn bộ hàng thứ ba
temperatures [ 2 ] [ 0 ]: phần tử đầu tiên của hàng thứ ba
Giống như mảng một chiều, mảng hai chiều cũng có thể được khởi tạo giá
trị các phần tử khi khai báo bằng cách liệt kê. Ví dụ mảng hai chiều b gồm 2
hàng và 2 cột có thể được khởi tạo như sau:
int b[][] = { { 1, 2 }, { 3, 4 } };
178

Các giá trị trên cùng một hàng được quy ước nhóm trong cặp ngoặc nhọn.
Với cách này, thì các giá trị 1 và 2 được gán cho b[0][0] và b[0][1] một cách
tương ứng; 3 và 4 được gán cho b[1][0] và b[1][1].
Khi gặp cách khởi tạo này, trình biên dịch sẽ tính số cặp ngoặc nhọn lồng
bên trong để xác định số hàng của mảng. Đối với mỗi cặp ngoặc nhọn lồng bên
trong, trình biên dịch tính số các phần tử bên trong cặp ngoặc đó để xác định số
cột của mảng. Với cách khởi tạo này thì số phần tử trên mỗi hàng có thể khác
nhau và mảng nhận được có tên là mảng gồ ghề hay mảng lởm chởm (jagged
array).
Ví dụ tạo mảng gồ ghề b có hai hàng, hàng thứ nhất có hai phần tử 1 và 2,
hàng thứ hai có ba phần tử 3, 4, 5:
int b[][] = { { 1, 2 }, { 3, 4, 5 } };
Quá trình khai báo và khởi tạo mảng hai chiều cũng có thể tách rời nhau
với sự trợ giúp của từ khóa new. Ví dụ tạo mảng hai chiều b có ba hàng và bốn
cột:
int b[][];
b = new int [ 3 ][ 4 ];
Mảng hai chiều gồ ghề cũng có thể được khai báo và khởi tạo một cách
riêng rẽ. Ví dụ tạo mảng hai chiều b có hai hàng, hàng thứ nhất có 5 phần tử,
hàng thứ hai có 3 phần tử:
int b[][];
b = new int[ 2 ][ ]; // tạo 2 hàng
b[ 0 ] = new int[ 5 ]; // tạo 5 cột trên hàng 0
b[ 1 ] = new int[ 3 ]; // tạo 3 cột trên hàng 1
Ví dụ 5.4. Chương trình duyệt và in các phần tử của mảng hai chiều bằng
vòng lặp for:
public class InitArray {
public static void main( String args[] ) {
int array1[][] = { { 1, 2, 3 }, { 4, 5, 6 } };
int array2[][] = { { 1, 2 }, { 3 }, { 4, 5, 6 } };
System.out.println( "Values in array1 by row are" );
outputArray( array1 ); // displays array1 by row
System.out.println( "\nValues in array2 by row are" );
outputArray( array2 ); // displays array2 by row
} // end main
179

public static void outputArray( int array[][] ){


// loop through array's rows
for ( int row = 0; row < array.length; row++ ) {
// loop through columns of current row
for (int column = 0; column < array[ row ].length;
column++ )
System.out.printf( "%d ", array[ row ][ column ] );
System.out.println(); // start new line of output
} // end outer for
} // end method outputArray
} // end class InitArray
Trong phương thức main của lớp InitArray có hai mảng hai chiều được
khai báo. Mảng array1 có hàng thứ nhất gồm các phần tử 1, 2, 3; hàng thứ hai
gồm các phần tử 4, 5, 6. Mảng array2 là mảng gồ ghề có ba hàng: hàng thứ nhất
có các phần tử 1, 2; hàng thứ hai có phần tử 3; hàng thứ ba có các phần tử 4, 5,
6.
Chương trình dùng phương thức outputArray để in các phần tử của các
mảng array1 và array2 ra màn hình. Phương thức outputArray có tham số là
mảng hai chiều kiểu số nguyên int array[][]. Phương thức này dùng vòng lặp
for để in các phần tử ra màn hình theo từng dòng (hàng). Trong điều kiện của
vòng lặp for ở ngoài, biểu thức array.length trả về số hàng của mảng array.
Trong vòng lặp for bên trong, biểu thức array[row].length trả về số cột (số phần
tử) trên hàng row của mảng array. Chúng ta có thể sử dụng thuộc tính length với
từng hàng của mảng hai chiều vì các hàng cũng là các mảng. Với cách sử dụng
các điều kiện thế này, các vòng lặp sẽ duyệt một cách chính xác qua tất cả các
phần tử mà không phụ thuộc vào số hàng và số cột trên từng hàng của mảng.
Kết quả chạy chương trình như sau:
Values in array1 by row are
123
456
Values in array2 by row are
12
3
456
5.4. LỚP Arrays
180

Để cung cấp thêm những khả năng khi làm việc với mảng, trong Java còn
có lớp java.util.Arrays. Lớp này chứa các phương thức static để sắp xếp, tìm
kiếm, so sánh mảng và nạp giá trị cho các phần tử mảng. Các phương thức đều
được nạp chồng để làm việc với tất cả các kiểu dữ liệu đơn giản. Các phương
thức được mô tả ngắn gọn trong Bảng 5.1.
Bảng 5.1. Một số phương thức của lớp Arrays
STT Phương thức và mô tả

public static int binarySearch(Object[] a, Object key);


Tìm kiếm phần tử key trong mảng a có kiểu Object (có các
phương thức nạp chồng với các kiểu byte, int, double,...) bằng
thuật toán tìm kiếm nhị phân. Mảng a phải được sắp xếp trước
1
khi gọi phương thức. Phương thức trả về chỉ số của phần tử có
giá trị bằng key nếu có phần tử như thế hoặc trả về (-
(insertion_point + 1)) với insertion_point là vị trí trong mảng mà
giá trị key có thể chèn vào mà không làm thay đổi thứ tự mảng.
public static boolean equals(long[] a, long[] a2);
Trả về giá trị true nếu hai mảng a và a2 bằng nhau và trả về false
trong trường hợp ngược lại. Hai mảng bằng nhau nếu có số phần
2
tử bằng nhau và các phần tử tại các vị trí như nhau cũng bằng
nhau. Phương thức cũng được nạp chồng cho các mảng có các
kiểu dữ liệu đơn giản khác (byte, short, int...)
public static void fill(int[] a, int val);
3 Gán cho tất cả các phần tử của mảng a giá trị bằng val, phương
thức được nạp chồng cho các kiểu dữ liệu đơn giản khác.
public static void sort(Object[] a);
4 Sắp xếp mảng a theo thứ tự tăng dần, phương thức được nạp
chồng cho các kiểu dữ liệu đơn giản khác.
5.5. XÂU KÝ TỰ
5.5.1. Lớp String
Các ngôn ngữ lập trình luôn phải cung cấp cơ chế làm việc với các xâu ký
tự. Trong những ngôn ngữ lập trình khác (C/C++ chẳng hạn), một xâu ký tự
được xem như một mảng các ký tự, nhưng trong Java thì khác. Java cung cấp
một lớp String để làm việc với đối tượng xâu ký tự cùng các thao tác trên đối
181

tượng dữ liệu này.


Lớp String cho phép tạo ra những đối tượng xâu ký tự. Nhưng những đối
tượng này không thể thay đổi được - giá trị của chúng một khi đã được khởi tạo
thì không thể thay đổi.
Ví dụ việc khai báo biến xâu ký tự msg gán giá trị "Hello":
String msg = "Hello";
Sau khi khai báo, trong bộ nhớ sẽ có một vùng nhớ được cấp phát để chứa
đối tượng xâu ký tự "Hello" và biến msg được trỏ (tham chiếu) đến vùng nhớ đó.
//Giả sử cần thay đổi biến msg với giá trị mới là "Hello world"
msg += " world";
Khi đó vùng nhớ chứa đối tượng "Hello" không bị thay đổi, mà thay vào
đó một vùng nhớ mới được cấp phát để lưu đối tượng "Hello world" và msg
được trỏ đến vùng nhớ mới này.
Đối tượng "Hello" trong ví dụ trên còn được gọi là một hằng xâu (string
literal)
Trong Java, string literal được lưu liên tiếp trong bộ nhớ. Nếu khởi tạo hai
đối tượng lớp String với cùng một giá trị mà không dùng từ khóa new thì sẽ chỉ
có một đối tượng được tạo ra trong bộ nhớ. Ví dụ:
Tạo xâu s1 và gán cho giá trị "hello":
String s1="hello"; //s1 trỏ tới “vùng nhớ 1” có giá trị là “hello”
Tạo xâu s2 và gán cho giá trị “hello”:
String s2="hello"; //s2 cũng sẽ trỏ tới “vùng nhớ 1” (có giá trị là
//“hello”)
Ở đây, s1 và s2 cùng trỏ tới một đối tượng lớp String trong bộ nhớ. Như
đã trình bày ở trên, đối tượng lớp String là không thay đổi được, bởi vậy, nếu
thay đổi giá trị của s1 thì một đối tượng lớp String mới sẽ được tạo còn đối
tượng lớp String ban đầu mà s2 trỏ tới vẫn giữ nguyên (là “hello”).
Giống như kiểu mảng và những kiểu đối tượng khác, có thể tạo đối tượng
xâu ký tự bằng từ khóa new. Khi sử dụng new, một phương thức tạo
(constructor) được gọi. Lớp String từ Java SE 6 cung cấp 15 constructor cho
phép khởi tạo xâu từ những nguồn khác nhau. Chương trình dưới đây minh họa
việc tạo xâu từ mảng các ký tự:
public class StringDemo{
public static void main(String args[]){
char[] helloArray = { 'h', 'e', 'l', 'l', 'o', '.'};
182

String helloString = new String(helloArray);


System.out.println( helloString);
}
}
Kết quả thể hiện trên màn hình khi chạy:
hello.
Trong Bảng 5.2 là mô tả một số phương thức của lớp String.
Bảng 5.2. Một số phương thức của lớp String
STT Tên phương thức và mô tả
1 char charAt(int index);
Trả về ký tự có chỉ số là index. Chỉ số các ký tự trong xâu cũng
giống như chỉ số các phần tử mảng, được bắt đầu từ 0.
2 int compareTo(String anotherString);
So sánh xâu với xâu anotherString theo thứ tự từ điển. Trả về 0
nếu hai xâu trùng nhau.
3 int compareToIgnoreCase(String str);
Giống như compareTo(String anotherString) nhưng không tính
đến khác biệt ký tự hoa – thường khi so sánh.
4 String concat(String str);
Nối xâu str vào cuối xâu ký tự gọi phương thức.
5 static String copyValueOf(char[] data);
Trả về xâu ký tự biểu diễn các ký tự của mảng data.
6 boolean endsWith(String suffix);
Trả về true nếu xâu ký tự gọi phương thức kết thúc bởi xâu
suffix.
7 byte[] getBytes();
Biến xâu ký tự thành chuỗi byte sử dụng bảng mã ngầm định của
hệ thống.
8 byte[] getBytes(Charset charset);
Biến xâu ký tự thành chuỗi byte sử dụng bảng mã được truyền
vào phương thức.
9 int hashCode();
Trả về mã băm của xâu.
10 int indexOf(int ch);
Trả về chỉ số của ký tự đầu tiên trong xâu bằng với ký tự tham
183

STT Tên phương thức và mô tả


số. Nếu không có ký tự như vậy, phương thức trả về -1.
11 int indexOf(String str);
Tìm kiếm xâu str và trả về vị trí tìm thấy đầu tiên. Nếu không
tìm thấy, trả về -1.
12 boolean isEmpty();
Trả về true nếu xâu rỗng (length() trả về 0).Ngược lại trả về
false.

13 int lastIndexOf(int ch);


Tìm kiếm ký tự ch trong xâu và trả về vị trí bắt gặp cuối cùng.
Nếu không tìm thấy, trả về -1.
14 int lastIndexOf(String str);
Tìm kiếm xâu str và trả về vị trí tìm thấy cuối cùng (vị trí bên
phải nhất). Nếu không tìm thấy, trả về -1.
15 int length();
Trả về chiều dài (số lượng ký tự) của xâu.
16 boolean matches(String regex);
Kiểm tra xem xâu có dạng biểu thức chính quy regex hay không.
Trả về true nếu có, ngược lại trả về false.
17 String replace(char oldChar, char newChar);
Trả về xâu ký tự nhận được bằng cách thay thế tất cả các ký tự
oldChar trong xâu bằng ký tự newChar.
18 boolean startsWith(String prefix);
Kiểm tra xem xâu ký tự có bắt đầu bởi xâu prefix hay không.
Nếu có trả về true, ngược lại trả về false.
19 String substring(int beginIndex);
Trả về xâu ký tự con bắt đầu từ vị trí beginIndex.
20 String substring(int beginIndex, int endIndex);
Trả về xâu ký tự con bắt đầu từ vị trí beginIndex và kết thúc ở vị
trí endIndex.
21 char[]toCharArray();
Biến xâu thành một mảng ký tự.
22 String toLowerCase();
Trả về xâu ký tự nhận được bằng cách biến tất cả các ký tự thành
184

STT Tên phương thức và mô tả


dạng ký tự thường tương ứng sử dụng quy tắc ngầm định.
23 String toUpperCase();
Trả về xâu ký tự nhận được bằng cách biến tất cả các ký tự thành
dạng ký tự thường tương ứng sử dụng quy tắc ngầm định.
24 String trim();
Trả về xâu ký tự nhận được bằng cách loại bỏ tất cả các ký tự
trắng (space) ở đầu và cuối xâu.
25 static String valueOf(boolean b);
Biến đổi biến boolean b thành xâu và trả về kết quả.
26 static String valueOf(char c);
Biến đổi biến ký tự c thành xâu và trả về kết quả.
27 static String valueOf(char[] data);
Biến đổi mảng ký tự data thành xâu và trả về kết quả.
28 static String valueOf(double d);
Biến đổi số thực d kiểu double thành xâu và trả về kết quả.
29 static String valueOf(float f);
Biến đổi số thực f kiểu float thành xâu và trả về kết quả.
30 static String valueOf(int i);
Biến đổi số nguyên i kiểu int thành xâu và trả về kết quả.
31 static String valueOf(long l);
Biến đổi số nguyên l kiểu long thành xâu và trả về kết quả.
Để minh họa các phương thức, chúng ta sẽ xem xét một số ví dụ.
Ví dụ 5.5. Hiển thị số lượng ký tự của xâu. Để xác định số lượng ký tự
của xâu, lớp String cung cấp phương thức length():
public class StringDemo {
public static void main(String args[]) {
String palindrome = "Dot saw I was Tod";
int len = palindrome.length();
System.out.println( "String Length is: " + len );
}
}
Kết quả khi chạy sẽ là:
String Length is: 17
Ví dụ 5.6. Kiểm tra xem một xâu ký tự có đuôi là một xâu ký tự khác hay
185

không:
public class Test{
public static void main(String args[]){
String Str = new String("This is really not immutable!!");
boolean retVal = Str.endsWith( "immutable!!" ); //Kiểm tra
xem Str //có kết thúc bởi xâu "immutable!!" hay không
System.out.println("Returned Value = " + retVal );
retVal = Str.endsWith( "immu" ); //Kiểm tra xem Str có
//kết thúc bởi xâu "immu" hay không
System.out.println("Returned Value = " + retVal );}}
Kết quả nhận được:
Returned Value = true
Returned Value = false
5.5.2. Các lớp StringBuffer và StringBuilder
Lớp String khi làm việc với xâu ký tự, có một hạn chế là các phương thức
không thể thay đổi được chính đối tượng xâu ký tự gọi chúng. Để tránh những
hạn chế này, Java cung cấp các lớp StringBuffer và StringBuilder. Các phương
thức của hai lớp này có thể thay đổi được chính đối tượng gọi mình. Hai lớp này
có tác dụng giống nhau, sự khác biệt lớn nhất là các phương thức của lớp
StringBuilder không được đồng bộ khi lập trình đa luồng, nhưng chính do vậy
mà có tốc độ thực hiện nhanh hơn so với StringBuffer. Trong những bài toán
không cần đồng bộ luồng nên sử dụng lớp StringBuilder, trong trường hợp
ngược lại cần sử dụng lớp StringBuffer. Trong Bảng 5.3 là một số phương thức
quan trọng của lớp StringBuffer.
Bảng 5.3. Một số phương thức của lớp StringBuffer
STT Phương thức và mô tả
1 public StringBuffer append(String s);
Cập nhật lại xâu ký tự gọi phương thức bằng cách chèn thêm vào
cuối xâu một xâu ký tự s. Phương thức được nạp chồng với tham
số là các kiểu dữ liệu đơn giản khác (boolean, char, int, long,...).
2 public StringBuffer reverse();
Đảo ngược lại thứ tự các ký tự trong xâu gọi phương thức.
3 public delete(int start, int end);
186

STT Phương thức và mô tả


Xóa xâu ký tự con bắt đầu từ chỉ số start đến chỉ số end.
4 public insert(int offset, int i);
Chèn vào xâu ký tự số nguyên i tại vị trí offset. Phương thức
được nạp chồng với tham số có các kiểu dữ liệu đơn giản khác.
5 replace(int start, int end, String str);
Thay thế xâu ký tự con bắt đầu từ chỉ số start và kết thúc ở chỉ
số end bằng xâu ký tự str.
6 void setCharAt(int index, char ch);
Thiết lập giá trị cho ký tự có chỉ số index giá trị ch.

Ví dụ 5.7. Thay đổi xâu ký tự bằng cách chèn thêm vào cuối một xâu
bằng phương thức append():
public class Test{
public static void main(String args[]){
StringBuffer sBuffer = new StringBuffer(" test");
sBuffer.append(" String Buffer");
System.out.println(sBuffer);
}
}
Kết quả nhận được khi chạy:
test String Buffer
Ví dụ 5.8. Đảo ngược xâu ký tự với phương thức reverse():
public class Test {
public static void main(String args[]) {
StringBuffer buffer = new StringBuffer("Game Plan");
buffer.reverse(); System.out.println(buffer);
}
}
Kết quả nhận được trên màn hình:
nalP emaG
Ví dụ 5.9. Xóa ký tự trong xâu:
public class Test {
187

public static void main(String args[]) {


StringBuffer sb = new StringBuffer("abcdefghijk");
sb.delete(3,7); //Xóa các ký tự từ vị trí 3 đến vị trí 7.
System.out.println(sb);
}
}
Kết quả nhận được trên màn hình:
abchijk
Ví dụ 5.10. Chèn xâu ký tự:
public class Test {
public static void main(String args[]) {
StringBuffer sb = new StringBuffer("abcdefghijk");
sb.insert(3,"123"); System.out.println(sb);
}}
Kết quả nhận được trên màn hình:
abc123defghijk
Ví dụ 5.11. Thay thế xâu ký tự:
public class Test {
public static void main(String args[]) {
StringBuffer sb = new StringBuffer("abcdefghijk");
sb.replace(3, 8, "ZARA");
System.out.println(sb);
}
}
Kết quả nhận được trên màn hình:
abcZARAijk
TỔNG KẾT CHƯƠNG
Nội dung Chương 5 đã đề cập đến các nội dung chính sau:
- Mảng là tập hợp nhiều phần tử có cùng tên, cùng kiểu dữ liệu và mỗi
phần tử trong mảng được truy xuất thông qua chỉ số của nó trong mảng. Trong
Java có hai loại mảng: mảng một chiều và mảng nhiều chiều.
- Mảng là một kiểu dữ liệu tham chiếu, khi khai báo thông thường mới chỉ
có biến tham chiếu được tạo ra và chưa có phần tử mảng nào tồn tại. Để có thể
sử dụng được mảng, cần tạo vùng bộ nhớ để lưu trữ các phần tử của mảng và trỏ
(gán) biến tham chiếu đến vùng bộ nhớ đó. Công việc này được gọi là khởi tạo,
188

các thông số cần chỉ ra khi khởi tạo gồm số phần tử và giá trị của các phần tử.
Việc khởi tạo có thể được thực hiện ngay khi khai báo hoặc thực hiện sau bằng
từ khóa new.
- Để duyệt qua các phần tử của mảng, thông thường phải sử dụng vòng
lặp.
- Mảng nhiều chiều là mảng một chiều có các phần tử cũng là mảng.
- Mảng hai chiều có thể được hình dung như một bảng các phần tử được
tổ chức thành các hàng và cột.
- Với mảng n-chiều cần n chỉ số để truy cập vào các phần tử. Các chỉ số
bắt đầu từ 0.
- Java cung cấp lớp Arrays chứa các phương thức static để sắp xếp, tìm
kiếm, so sánh mảng và nạp giá trị cho các phần tử mảng.
- Java cung cấp lớp String để làm việc với đối tượng xâu ký tự cùng các
thao tác trên đối tượng dữ liệu này.
- Trong Java, các hằng xâu (string literal) được lưu liên tiếp trong bộ nhớ.
Nếu khởi tạo hai đối tượng lớp String với cùng một giá trị mà không dùng từ
khóa new thì sẽ chỉ có một đối tượng được tạo ra trong bộ nhớ.
- Giống như kiểu mảng và những kiểu đối tượng khác, có thể tạo đối
tượng xâu ký tự bằng từ khóa new.
- Lớp String khi làm việc với xâu ký tự, có một hạn chế là các phương
thức không thể thay đổi được chính đối tượng xâu ký tự gọi chúng. Để tránh
những hạn chế này, Java cung cấp các lớp StringBuffer và StringBuilder. Các
phương thức của hai lớp này có thể thay đổi được chính đối tượng gọi mình.
BÀI TẬP
1. Cho trước một mảng gồm 10 phần tử. Viết chương trình tính trung bình
cộng của các phần tử của mảng.
2. Cho trước một mảng gồm 10 phần tử. Viết chương trình liệt kê tất cả
các phần tử của mảng có giá trị âm.
3. Cho trước một mảng gồm 10 phần tử. Viết chương trình tính tích tất cả
các phần tử của mảng có giá trị dương.
4. Cho trước dãy số thực a0, a1... an-1. Hãy liệt kê các phần tử xuất hiện
trong dãy đúng một lần.
5. Cho trước dãy số thực a0, a1..., an-1. Hãy xác định chỉ số của phần tử
đầu tiên trong dãy bằng giá trị x cho trước. Nếu không có phần tử như vậy, in ra
-1.
189

6. Cho trước dãy số thực a0, a1..., an-1. Hãy liệt kê các phần tử xuất hiện
trong dãy đúng 2 lần.
7. Cho trước dãy số thực a0, a1..., an-1. In ra màn hình số lần xuất hiện của
các phần tử.
8. Cho trước dãy số thực a0, a1..., an-1. Sắp xếp dãy theo thứ tự tăng dần và
in ra màn hình.
9. Cho trước ma trận A có n dòng, m cột, các phần tử là những số nguyên
lớn hơn 0 và nhỏ hơn 100. Viết chương trình thực hiện các chức năng sau:
- Tìm phần tử lớn nhất của ma trận cùng chỉ số của số đó.
- Tìm và in ra các phần tử là số nguyên tố của ma trận.
- Sắp xếp tất cả các cột của ma trận theo thứ tự tăng dần và in kết quả ra
màn hình.
10. Cho trước một xâu ký tự. Đếm số từ của xâu ký tự đó. Ví dụ xâu "
Trường học " có 2 từ.
11. Viết chương trình đổi xâu ký tự cho trước sang chữ in hoa rồi in kết
quả ra màn hình. Ví dụ: Từ xâu “abcdAbcD” sẽ in xâu “ABCDABCD”.
12. Viết chương trình thực hiện chuẩn hoá một xâu ký tự cho trước (loại
bỏ các dấu cách thừa, chuyển ký tự đầu mỗi từ thành chữ hoa, các ký tự khác
thành chữ thường).
13. Viết chương trình tìm từ dài nhất trong xâu ký tự cho trước. Từ đó
xuất hiện ở vị trí nào? (Chú ý: nếu có nhiều từ có độ dài giống nhau thì chọn từ
đầu tiên tìm thấy).
14. Cho trước một xâu họ tên theo cấu trúc: họ...đệm...tên; viết chương
trình chuyển xâu đó sang biểu diễn theo cấu trúc tên…họ…đệm.
Câu hỏi ôn tập
1. Kết quả chạy chương trình sau là gì?
public class ArraysInJava {
public static void main(String[] args) {
int[] i = new int[0];
System.out.println(i[0]);
}
}
2. Kết quả chạy chương trình sau là gì?
public class ArraysInJava {
static void methodOne(int[] a) {
190

int[] b = new int[5];


a = b;
System.out.print(a.length);
System.out.print(b.length);
}
public static void main(String[] args) {
int[] a = new int[10]; methodOne(a);
System.out.print(a.length);
}
}
3. Chương trình sau có lỗi không?
public class ArraysInJava {
public static void main(String[] args) {
int[] a = {1}; int[] b[] = {{1}};
int[][] c[] = {{{1}}};
int[][] d [][] = {{{{1}}}};
}}
4. Kết quả chạy chương trình là gì?
public class ArraysInJava {
public static void main(String[] args) {
String[][][][] colors = {
{
{
{"RED", "GREEN", "BLUE"},
{"GREEN", "RED", "BLUE"}
},
{ {"ORANGE", "GREEN", "WHITE"},
{"BLACK", "INDIGO", "BLUE"}
}
},
{
{ {"SKY BLUE", "ALMOND", "AQUA"},
{"APPLE GREEN", "PINK", "BLUE GREEN"}
},
{ {"VIOLET", "BRASS", "GREY"},
191

{"BROWN", "INDIGO", "CHERRY"}


}
}
};
System.out.println(colors[1][0][1][0]);
System.out.println(colors[0][1][0][1]);
System.out.println(colors[0][0][0][2]);
System.out.println(colors[1][1][1][2]);
System.out.println(colors[0][0][0][0]);
System.out.println(colors[1][1][1][1]);
}
}
5. String có phải là từ khóa của Java không?
6. String là kiểu dữ liệu cơ sở hay kiểu tham chiếu?
7. Có bao nhiêu cách để tạo đối tượng lớp String?
8. Có bao nhiêu đối tượng được tạo ra trong bộ nhớ khi thực hiện đoạn
chương trình sau:
String s1 = "abc"; String s2 = "abc";
9. Làm thế nào để tạo đối tượng xâu ký tự có khả năng thay đổi được bản
thân?
10. Có bao nhiêu đối tượng được tạo ra trong bộ nhớ khi thực hiện đoạn
chương trình sau:
String s1 = new String("abc");
String s2 = "abc";
11. Có bao nhiêu đối tượng được tạo ra trong bộ nhớ khi thực hiện đoạn
chương trình sau:
String s1 = new String("abc");
String s2 = new String("abc");
12. Giá trị của xâu ký tự str sau khi thực thi đoạn mã sau là gì?
String str = "Dr. Decaffeinated";
StringBuffer strBuf = new StringBuffer(str.substring(6, 10) );
strBuf.setCharAt(1, 'o');
strBuf.append( 'e' );
str = strBuf.toString( );
192

Chương 6
CÁC LỚP BAO VÀ CÁC LỚP TIỆN ÍCH
6.1. CÁC LỚP BAO (WRAPPER CLASSES)
Thông thường, khi làm việc với các số chúng ta sử dụng các kiểu dữ liệu
đơn giản như byte, int, long, double,... Ví dụ:
int i = 5000;
float gpa = 13.65;
byte mask = 0xaf;
Những kiểu dữ liệu này không phải kiểu dữ liệu đối tượng (class object),
và trong lập trình hướng đối tượng, sự có mặt của những kiểu dữ liệu này trong
một chương trình làm cho chương trình đó trở thành không hướng đối tượng
hoàn toàn. Để có thể lập trình hướng đối tượng hoàn toàn, Java đưa ra những
kiểu dữ liệu lớp tương ứng với các kiểu dữ liệu đơn giản đó. Những lớp đó được
gọi là những lớp bao (wrapper class), các kiểu dữ liệu đơn giản và các lớp bao
tương ứng được trình bày trong Bảng 6.1.
Bảng 6.1. Các kiểu dữ liệu đơn giản và các lớp bao tương ứng
Kiểu đơn giản Lớp bao
byte Byte

short Short

int Integer

long Long

float Float

double Double

char Character

boolean Boolean
Tất cả các lớp bao đều là các lớp con của lớp trừu tượng Number. Các lớp
trên Hình 6.1 đều nằm trong gói java.lang, khi sử dụng không cần phải chỉ định
import.
Các biến dữ liệu đơn giản và các đối tượng lớp bao tương ứng có thể được
chuyển hóa một cách tự động. Quá trình biến đổi (bao bọc) một biến của kiểu dữ
liệu đơn giản được gọi là boxing (đóng hộp). Khi sử dụng các đối tượng lớp bao
193

như những biến kiểu dữ liệu đơn giản, trình biên dịch đầu tiên sẽ phải “lấy dữ
liệu” từ trong đối tượng bao ra, quá trình này được gọi là unboxing (mở hộp).

Hình 6.1. Cây phả hệ các lớp bao


Ví dụ 6.1. Boxing và unboxing:
public class Test{
public static void main(String args[]){
// Đóng hộp số nguyên 5 vào đối tượng kiểu Integer
Integer x = 5;
// Lấy dữ liệu số nguyên từ trong đối tượng x kiểu Integer
// và thực hiện phép toán cộng hai số nguyên,
//kết quả trả về lại đóng hộp vào đối tượng x
x = x + 10;
System.out.println(x);
}
}
Kết quả hiển thị trên màn hình:
15
Trong Java, quá trình boxing được trình biên dịch thực hiện một cách tự
động, vì vậy boxing còn được gọi là Autoboxing.
Ngoài các lớp bao dữ liệu số, trong Java còn có lớp bao Character cho
kiểu dữ liệu ký tự char. Các quá trình autoboxing và unboxing cũng được thực
hiện đối với lớp này.
Các lớp bao ngoài việc đối tượng hóa các kiểu dữ liệu đơn giản, còn có
những phương thức và những thuộc tính hữu ích.Trong Bảng 6.2 là một số
phương thức của lớp Character.
194

Bảng 6.2. Một số phương thức của lớp Character


STT Phương thức và mô tả
1 static boolean isDigit(char ch);
Trả về true nếu ch là chữ số, ngược lại trả về false.
2 static boolean isLetter(char ch);
Trả về true nếu ch là chữ cái, ngược lại trả về false.
3 static boolean isLowerCase(char ch);
Trả về true nếu ch là ký tự thường, ngược lại trả về false.
4 static boolean isUpperCase(char ch);
Trả về true nếu ch là ký tự hoa, ngược lại trả về false.
5 static char toLowerCase(char ch);
Biến ký tự ch thành ký tự thường và trả về kết quả.
6 static char toUpperCase(char ch);
Biến ký tự ch thành ký tự hoa và trả về kết quả.
6.2. LỚP Date
Lớp Date đóng gói dữ liệu về ngày, tháng, năm, thời gian. Lớp Date là
một trong những lớp tiện ích, các lớp tiện ích được đóng gói trong gói java.util.
Lớp có hai constructor, một constructor tạo đối tượng với thời điểm hiện tại,
constructor này không có tham số. Cú pháp:
Date( );
Constructor thứ hai có một tham số là số giây đã trôi qua từ thời điểm 00
giờ 00 phút 00 giây ngày 01/01/1070. Cú pháp:
Date(long millisec);
Constructor này tạo đối tượng lưu trữ thời điểm cách 00:00:00 ngày
01/01/1970 đúng một số giây là millisec.
Khi đã tạo đối tượng, có thể sử dụng một số phương thức như miêu tả
trong Bảng 6.3 để làm việc với thời gian.
Bảng 6.3. Một số phương thức của lớp Date
STT Phương thức và mô tả
boolean after(Date date);
1 Trả về true nếu đối tượng gọi phương thức muộn hơn đối tượng
tham số, ngược lại trả về false.
2 boolean before(Date date);
195

Trả về true nếu đối tượng gọi phương thức sớm hơn đối tượng
tham số, ngược lại trả về false.
Object clone( );
3
Nhân bản đối tượng gọi phương thức.
int compareTo(Date date);
So sánh đối tượng gọi phương thức và đối tượng tham số. Trả về
4 0 nếu hai đối tượng bằng nhau. Trả về giá trị âm nếu đối tượng
gọi sớm hơn đối tượng tham số. Trả về giá trị dương nếu đối
tượng gọi muộn hơn đối tượng tham số.
long getTime( );
5 Trả về số giây đã trôi qua từ 00:00:00 ngày 01/01/1970 tính đến
thời điểm hiện tại.
void setTime(long time);
6 Thiết lập thời điểm cũ bởi thời điểm cách 00:00:00 ngày
01/01/1970 một số giây là time.
Lớp Date hỗ trợ rất nhiều khả năng để làm việc với các thời điểm. Ví dụ
sau thể hiện việc hiển thị thông tin về thời điểm hiện tại ra màn hình bằng
phương thức toString().
Ví dụ 6.2. Hiển thị thời điểm hiện tại ra màn hình:
import java.util.Date;
public class DateDemo {
public static void main(String args[]) {
Date date = new Date();
System.out.println(date.toString());
}
}
Kết quả trên màn hình như sau:
Tue May 31 14:44:32 ICT 2016
Thông tin về thời điểm hiện tại trong ví dụ trên được hiển thị ra màn hình
với định dạng ngầm định. Để điều khiển việc hiển thị, có thể sử dụng lớp
SimpleDateFormat trong Ví dụ 6.3.
Ví dụ 6.3. Hiển thị thời điểm với lớp SimpleDateFormat:
import java.util.*; import java.text.*;
public class DateDemo {
public static void main(String args[]) {
196

Date dNow = new Date( );


SimpleDateFormat ft =
new SimpleDateFormat ("E yyyy.MM.dd 'at' hh:mm:ss a zzz");
System.out.println("Current Date: " + ft.format(dNow));}}
Kết quả nhận được trên màn hình:
Current Date: Th 3 31.05.2016 at 02:47:08 CH ICT
Cách sử dụng lớp SimpleDateFormat rất đơn giản, lớp dùng các ký tự
ASCII để biểu thị cho các thông tin về thời gian. Bảng 6.4 thể hiện các biểu thị
có thể.
Bảng 6.4. Các định dạng của lớp SimpleDateFormat
Ký tự Mô tả Ví dụ
G Biểu thị thời kỳ AD
y Năm dưới dạng 4 chữ số 2001
M Tháng trong năm July or 07
d Ngày trong tháng 10
h Giờ trong dạng A.M./P.M. (1~12) 12
H Giờ trong ngày (0~23) 22
m Phút trong giờ 30
s Giây trong phút 55
S Milli giây 234
E Ngày trong tuần Tuesday
D Ngày trong năm 360
F Ngày của tuần trong tháng 2 (second Wed. in July)
w Tuần trong năm 40
W Tuần trong tháng 1
a Định dạng A.M./P.M. PM
k Giờ trong ngày (1~24) 24
K Giờ trong dạng A.M./P.M. (0~11) 10
197

Ký tự Mô tả Ví dụ
z Múi giờ Eastern Standard Time
' Chuyển sang dạng văn bản Ký hiệu ngăn cách
" Trích dẫn đơn `
Lớp SimpleDateFormat còn có một số phương thức hữu ích, ví dụ như
phương thức parse() biến đổi một xâu ký tự thành đối tượng thời gian. Phương
thức này cần cho nhu cầu nhập dữ liệu thời gian từ bàn phím.
Ví dụ 6.4. Biến đổi xâu ký tự nhập từ tham số dòng lệnh thành đối tượng
thời gian:
import java.util.*;
import java.text.*;
public class DateDemo {
public static void main(String args[]) {
//Xác định định dạng thời gian là YYYY-MM-DD
SimpleDateFormat ft =
new SimpleDateFormat ("yyyy-MM-dd");
//Kiểm tra xem có tham số dòng lệnh không,
//nếu có thì lấy đó làm giá trị thời gian nhập vào.
//Nếu không có thì lấy xâu "1818-11-11" làm
//đối tượng thời gian nhập vào.
String input = args.length == 0 ? "1818-11-11": args[0];
System.out.print(input + " Parses as ");
Date t;
try {
//Biến đổi xâu input thành đối tượng Date
t = ft.parse(input);
System.out.println(t); //Đưa ra kết quả
}
catch (ParseException e) {
System.out.println("Unparseable using " + ft);
}
}
}
Biên dịch và dùng chương trình cmd để chạy trong hai trường hợp: không
198

có tham số dòng lệnh và có tham số dòng lệnh. Kết quả nhận được:
java DateDemo
1818-11-11 Parses as Wed Nov 11 00:00:00 GMT 1818
java DateDemo 2007-12-01
2007-12-01 Parses as Sat Dec 01 00:00:00 GMT 2007

6.3. LỚP GregorianCalendar


Lớp GregorianCalendar là một lớp con cụ thể của lớp Calendar, dùng để
làm việc với lịch Gregory (là lịch thông thường hiện nay).Để trả về thời điểm,
locale (vùng chính trị, địa lý, văn hóa) và múi giờ hiện tại, có thể sử dụng
phương thức getInstance().
Các Bảng 6.5, 6.6, 6.7 liệt kê một số constructor, phương thức, trường
(thuộc tính) của lớp GregorianCalendar.
Bảng 6.5. Một số constructor của lớp GregorianCalendar.
STT Constructor
1 GregorianCalendar();
Tạo đối tượng lịch GregorianCalendar với thời điểm hiện tại và
locale, múi giờ ngầm định.
2 GregorianCalendar(int year, int month, int date);
Tạo đối tượng GregorianCalendar với ngày, tháng, năm cụ thể và
locale, múi giờ ngầm định.
3 GregorianCalendar(int year, int month, int date, int hour, int
minute);
Tạo đối tượng GregorianCalendar với ngày, tháng, năm, giờ,
phút cụ thể và locale, múi giờ ngầm định.
4 GregorianCalendar(int year, int month, int date, int hour, int
minute, int second);
Tạo đối tượng GregorianCalendar với ngày, tháng, năm, giờ,
phút, giây cụ thể và locale, múi giờ ngầm định.
5 GregorianCalendar(Locale aLocale);
Tạo đối tượng GregorianCalendar với thời điểm hiện tại, múi giờ
ngầm định và locale cụ thể.
199

STT Constructor
6 GregorianCalendar(TimeZone zone);
Tạo đối tượng GregorianCalendar với thời điểm hiện tại, locale
ngầm định và múi giờ cụ thể.
7 GregorianCalendar(TimeZone zone, Locale aLocale);
Tạo đối tượng GregorianCalendar với thời điểm hiện tại, locale
cụ thể và múi giờ cụ thể.
Bảng 6.6. Một số phương thức của lớp GregorianCalendar
STT Phương thức
1 void add(int field, int amount);
Thêm một lượng thời gian (có thể âm) amount vào trường thời
gian field, việc thêm dựa trên quy tắc lịch.
2 int get(int field);
Trả về giá trị của trường thời gian field.
3 int getActualMaximum(int field);
Trả về giá trị lớn nhất có thể của trường thời gian field tại thời
điểm hiện tại.
4 int getActualMinimum(int field);
Trả về giá trị lớn nhất có thể của trường thời gian field tại thời
điểm hiện tại.
5 Date getTime();
Trả về thời điểm hiện tại.
6 long getTimeInMillis();
Trả về thời điểm hiện tại tính bằng số milli giây.
7 TimeZone getTimeZone();
Trả về múi giờ hiện tại.
8 boolean isLeapYear(int year);
Kiểm tra xem năm year có phải năm nhuận hay không. Nếu năm
nhuận, trả về true, ngược lại trả về false.
9 void set(int field, int value);
200

STT Phương thức


Thiết lập giá trị cho trường thời gian field.
10 void set(int year, int month, int date);
Thiết lập lại giá trị cho ngày, tháng, năm.
11 void set(int year, int month, int date, int hour, int minute);
Thiết lập lại giá trị cho ngày, tháng, năm, giờ, phút.
12 void set(int year, int month, int date, int hour, int minute, int
second);
Thiết lập lại giá trị cho ngày, tháng, năm, giờ, phút, giây.
13 void setTime(Date date);
Thiết lập lại giá trị cho thời điểm hiện tại
14 void setTimeInMillis(long millis);
Thiết lập lại giá trị cho thời điểm hiện tại bằng số giây đã trôi
qua tính từ 0:0:0 ngày 1/1/1970.
15 void setTimeZone(TimeZone value);
Thiết lập lại múi giờ.
16 String toString();
Trả về xâu ký tự biểu diễn đối tượng lịch.
Bảng 6.7. Các trường thời gian của lớp GregorianCalendar
Tên trường Mô tả Giá trị ngầm định
ERA Kỷ nguyên AD (Công nguyên)
YEAR Năm 1970
MONTH Tháng JANUARY
DAY_OF_MONTH Ngày trong 1
tháng
Ngày trong tuần Ngày đầu tiên của
DAY_OF_WEEK
tuần
WEEK_OF_MONTH Tuần của tháng 0
DAY_OF_WEEK_IN_MONTH Ngày của tuần 1
trong tháng
AM_PM AM hay PM AM
201

Tên trường Mô tả Giá trị ngầm định


HOUR, Giờ, giờ trong
HOUR_OF_DAY, ngày, phút,
0
MINUTE, giây, milli giây
SECOND, MILLISECOND
Ví dụ 6.6. Hiển thị thông tin về lịch hiện tại:
import java.util.*;
public class GregorianCalendarDemo {
public static void main(String args[]) {
String months[] = {
"Jan", "Feb", "Mar", "Apr",
"May", "Jun", "Jul", "Aug",
"Sep", "Oct", "Nov", "Dec"};
int year;
// Tạo đối tượng lịch Gregory và khởi tạo
// với thời điểm hiện tại và locale, múi giờ ngầm định.
GregorianCalendar gcalendar = new GregorianCalendar();
// Hiển thị thời điểm hiện tại và thông tin kèm theo.
System.out.print("Date: ");
System.out.print(months[gcalendar.get(Calendar.MONTH)]);
System.out.print(" " + gcalendar.get(Calendar.DATE) + " ");
System.out.println(year = gcalendar.get(Calendar.YEAR));
System.out.print("Time: ");
System.out.print(gcalendar.get(Calendar.HOUR) + ":");
System.out.print(gcalendar.get(Calendar.MINUTE) + ":");
System.out.println(gcalendar.get(Calendar.SECOND));
// Kiểm tra năm hiện tại có nhuận hay không
if(gcalendar.isLeapYear(year)) {
//Năm nhuận
System.out.println("The current year is a leap year");
}
else {
System.out.println("The current year is not a leap year");
}
}
202

}
Kết quả nhận được trên màn hình:
Date: May 31 2016
Time: 3:14:6
The current year is a leap year
6.4. CÁC LỚP LÀM VIỆC VỚI BIỂU THỨC CHÍNH QUY
Java cung cấp gói java.util.regex để làm việc với biểu thức chính quy.
Một biểu thức chính quy (regular expression) về bản chất là một mẫu để
mô tả một tập hợp các xâu ký tự chia sẻ chung mẫu này (có khuôn mẫu này). Ví
dụ, đây là một tập hợp các xâu ký tự có một số điều chung:
- “a string”;
- “a longer string”;
- “a much longer string”.
Điểm chung ở đây là tất cả các xâu ký tự này đều bắt đầu bằng "a" và kết
thúc bằng "string".
Để làm việc với biểu thức chính quy, trong gói java.util.regex có ba lớp
cốt lõi:
- Pattern: Đối tượng lớp này dùng để mô tả một mẫu xâu ký tự.
- Matcher: Để kiểm tra một xâu ký tự xem nó có khớp với mẫu không.
- PatternSyntaxException: Để báo rằng một xâu không thể khớp được với
mẫu đã định nghĩa.
Trước khi tìm hiểu các lớp, cần hiểu được một số cú pháp mẫu biểu thức
chính quy. Một mẫu (pattern) biểu thức chính quy mô tả một khuôn xâu ký tự,
thông thường khuôn này được sử dụng để xem một xâu ký tự nào đó có thể ăn
khớp hay không (xâu ký tự có khuôn dạng đó hay không). Mẫu được tạo nên từ
các cấu kiện. Bảng 6.8 liệt kê một số trong các cấu kiện mẫu phổ biến nhất mà
có thể sử dụng trong các xâu ký tự mẫu.
Bảng 6.8. Một số cấu kiện của mẫu biểu thức chính quy
Cấu Xâu ký tự được coi là ăn khớp
kiện

. Bất kỳ ký tự nào.
Ví dụ: ký tự ‘a’ được coi là khớp với mẫu ‘.’

? Ký tự không có mặt lần nào hoặc chỉ có mặt đúng một lần.
203

Cấu Xâu ký tự được coi là ăn khớp


kiện

Ví dụ: X? nghĩa là ký tự X chỉ có mặt đúng một lần hoặc không


có mặt trong xâu.

* Ký tự không có mặt hoặc có mặt nhiều hơn một lần.


Ví dụ: X* nghĩa là X không có mặt hoặc có mặt nhiều hơn một
lần trong xâu.

+ Ký tự có mặt một lần hoặc nhiều hơn.


Ví dụ X+ nghĩa là X có mặt một lần hoặc nhiều hơn trong xâu.

[] Một dải các ký tự hay chữ số.


Ví dụ: [a-z], [0-9].

[^..] Không phải những ký tự tiếp sau.


Ví dụ: [^X] mang ý nghĩa trong xâu không có ký tự X.

\d Bất kỳ số nào (tương đương với [0-9]).

\D Bất kỳ cái gì không là số (tương đương với [^0-9]).

\s Bất kỳ khoảng trống nào (tương đương với [ \n\t\f\r]).

\S Không có bất kỳ khoảng trống nào (tương đương với [^ \n\t\f\r]).

\w Bất kỳ từ nào (tương đương với [a-zA-Z_0-9]).

\W Không có bất kỳ từ nào (tương đương với [^\w]).


Các cấu kiện ?, *, + còn được gọi là các lượng tử ((quantifiers), vì chúng
xác định số lần ký tự có mặt trong xâu. Các cấu kiện như \d là các lớp ký
tự được định nghĩa trước. Bất kỳ ký tự nào mà không có ý nghĩa đặc biệt trong
một mẫu sẽ là một trực kiện và chỉ khớp với chính nó.
Để so khớp một xâu ký tự với một mẫu biểu thức chính quy, có thể sử
dụng các lớp Pattern và Matcher.
Ví dụ 6.7. Chương trình so khớp xâu ký tự với mẫu biểu thức chính quy:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegexMatches{
204

public static void main( String args[] ){


Pattern pattern = Pattern.compile("a.*string");
Matcher matcher = pattern.matcher("a string");
boolean didMatch = matcher.matches();
System.out.println(didMatch);
int patternStartIndex = matcher.start();
System.out.println(patternStartIndex);
int patternEndIndex = matcher.end();
System.out.println(patternEndIndex);
}
}
Trong ví dụ, trước tiên chúng ta tạo ra một Pattern. Chúng ta làm điều đó
bằng cách gọi compile(), một phương thức tĩnh của Pattern, với một xâu ký tự
biểu diễn mẫu mà chúng ta muốn so khớp.
Trong ví dụ này, mẫu biểu thức chính quy có nghĩa: Một khuôn các xâu
ký tự bắt đầu là 'a', theo sau là không hay nhiều ký tự khác và kết thúc bằng xâu
“string”.
Sau khi tạo mẫu, chúng ta gọi phương thức matcher() của đối
tượng pattern. Lời gọi này tạo ra một đối tượng lớp Matcher, đối tượng này có
nhiệm vụ so khớp một xâu ký tự nào đó với mẫu pattern bằng phương thức
matches(). Phương thức này trả về kết quả so khớp cùng với các thông tin khác.
Một số phương thức của lớp Matcher:
- matches() cho biết rằng xâu tham số có khớp đúng với mẫu hay không.
- start() cho biết giá trị chỉ số trong xâu tham số matches() mà tại đó bắt
đầu khớp đúng với mẫu.
- end() cho biết giá trị chỉ số trong xâu tham số matches() mà tại đó kết
thúc khớp đúng với mẫu, cộng với một.
Kết quả chạy chương trình:
true
0
7
Nếu trong xâu ký tự cần so khớp có nhiều ký tự hơn trong mẫu biểu thức
chính quy thì nên sử dụng phương thức lookingAt() thay cho matches(). Trong
trường hợp này, nếu sử dụng matches() thì sẽ nhận được kết quả sai (false), bởi
vì có nhiều ký tự trong xâu cần so khớp không ăn khớp với mẫu, còn phương
205

thức lookingAt() cho phép tìm kiếm xâu con khớp với mẫu.
Ví dụ với xâu ký tự:
“Here is a string with more than just the pattern”
và mẫu:
“a.*string”
thì matches() trả về false còn lookingAt() trả về true.
6.5. LỚP StringTokenizer
Lớp StringTokenizer được sử dụng để tách một xâu ký tự ra các xâu con
(được gọi là token). Các token ngăn cách với nhau bởi các ký tự ngăn cách.
Để sử dụng, đầu tiên phải tạo đối tượng lớp bằng một trong những
constructor được miêu tả trong Bảng 6.9.
Sau khi gọi constructor để tạo đối tượng, có thể sử dụng một số phương
thức của lớp StringTokenizer (Bảng 6.10) để làm việc với từng token.

Bảng 6.9. Các constructor của StringTokenizer


STT Constructor
1 StringTokenizer(String str);
Tạo đối tượng chứa các token nhận được từ xâu ký tự str, ký tự
ngăn cách trong trường hợp này là dấu cách.
2 StringTokenizer(String str, String delim);
Tạo đối tượng chứa các token nhận được từ xâu ký tự str, các ký tự
ngăn cách là các ký tự của xâu delim.
3 StringTokenizer(String str, String delim, boolean returnDelims);
Tạo đối tượng chứa các token nhận được từ xâu ký tự str, các ký tự
ngăn cách là các ký tự của xâu delim.
Tham số returnDelims xác định liệu các ký tự delim có được coi là
token hay không.
Nếu returnDelims là true thì các ký tự ngăn cách nếu có trong str
thì cũng được coi là các token.
Nếu returnDelims là false thì các ký tự ngăn cách bị bỏ qua.

Bảng 6.10. Một số phương thức của lớp StringTokenizer


206

STT Phương thức


1 int countTokens();
Trả về số lượng token trong đối tượng gọi.
2 boolean hasMoreElements();
Trả về giá trị giống như phương thức hasMoreTokens.
3 boolean hasMoreTokens();
Kiểm tra xem trong đối tượng còn có token nữa hay không. Nếu
còn trả về true, ngược lại trả về false.
4 String nextToken();
Trả về token tiếp theo trong đối tượng.

Một số ví dụ với lớp StringTokenizer:


Ví dụ 6.8. Tách một xâu ký tự ra các xâu con ngăn cách với nhau bởi các
dấu cách:
import java.util.*;
public class StringTokenizerDemo {
public static void main(String[] args) {
// Tạo đối tượng string tokenizer
StringTokenizer st = new StringTokenizer("Come to learn");
// Tính số lượng token
System.out.println("Total tokens: " + st.countTokens());
// Kiểm tra xem còn token hay không và đưa ra màn hình
while (st.hasMoreTokens()){
System.out.println("Next token: " + st.nextToken());
}
}
}
Kết quả nhận được khi chạy chương trình:
Total tokens: 3
Next token: Come
Next token: to
Next token: learn
207

Ví dụ 6.9. Tách một xâu ký tự ra các xâu con ngăn cách với nhau bởi các
ký tự nhất định:
import java.util.*;
public class StringTokenizerDemo {
public static void main(String[] args) {
// Tạo đối tượng string tokenizer
StringTokenizer st = new StringTokenizer("Come to learn","oea ");
// Tính số lượng token
System.out.println("Total tokens: " + st.countTokens());
// Kiểm tra xem còn token hay không và đưa ra màn hình
while (st.hasMoreTokens()){
System.out.println("Next token: " + st.nextToken());
}
}
}
Kết quả nhận được khi chạy chương trình:
Total tokens: 5
Next token: C
Next token: m
Next token: t
Next token: l
Next token: rn
6.6. LỚP Random
Lớp Random dùng để sinh các số ngẫu nhiên.
Để sử dụng, phải tạo đối tượng bằng một trong các constructor được miêu
tả trong Bảng 6.11.
Bảng 6.11. Các constructor của lớp Random
STT Constructor
1 Random();
Tạo đối tượng sinh số ngẫu nhiên.
2 Random(long seed);
Tạo đối tượng sinh số ngẫu nhiên sử dụng hạt gieo đơn giản.
Trong Bảng 6.12 là một số phương thức của lớp Random.
Bảng 6.12. Một số phương thức của lớp Random
208

STT Phương thức


1 protected int next(int bits);
Trả về một số ngẫu nhiên mà ở dạng biểu diễn nhị phân có số bit
là bits.
2 void nextBytes(byte[] bytes);
Phương thức sinh các byte có giá trị ngẫu nhiên và trả giá trị về
trong mảng bytes tham số của phương thức.
3 double nextDouble();
Trả về số thực kiểu double ngẫu nhiên nằm giữa 0.0 và 1.0.
4 float nextFloat();
Trả về số thực kiểu float ngẫu nhiên nằm giữa 0.0 và 1.0.
5 int nextInt();
Trả về số nguyên ngẫu nhiên kiểu int.
6 int nextInt(int n);
Trả về số nguyên ngẫu nhiên kiểu int nằm giữa 0 và n.
7 long nextLong();
Trả về số nguyên ngẫu nhiên kiểu long.
Ví dụ 6.10. Sinh một số thực ngẫu nhiên kiểu float:
import java.util.*;
public class RandomDemo {
public static void main( String args[] ){
// Tạo đối tượng sinh số ngẫu nhiên
Random randomno = new Random();
// Sinh ngẫu nhiên một số thực kiểu float
System.out.println("Float value: " +
randomno.nextFloat());
}
}
Kết quả nhận được khi chạy chương trình:
Float value: 0.5909014
6.7. LỚP Math
Lớp Math chứa các thuộc tính hằng số toán học và các phương thức hiện
209

thực các hàm toán học cơ bản như hàm mũ, logarit, các hàm lượng giác,....
Các thuộc tính của lớp Math:
- static double E: Hằng số cơ số của logarit tự nhiên, xấp xỉ 2.718.
- static double PI: Hằng số Pi, xấp xỉ 3.14.
Trong Bảng 6.13 là một số phương thức của lớp Math.
Bảng 6.13. Một số phương thức của lớp Math
STT Phương thức và mô tả
1 static double abs(double a);
Phương thức trả về giá trị tuyệt đối của tham số a kiểu double.
2 static float abs(float a);
Phương thức trả về giá trị tuyệt đối của tham số a kiểu float.
3 static int abs(int a);
Phương thức trả về giá trị tuyệt đối của tham số a kiểu int.
4 static long abs(long a);
Phương thức trả về giá trị tuyệt đối của tham số a kiểu long.
5 static double acos(double a);
Phương thức cài đặt hàm arccos(a), giá trị trả về nằm trong đoạn
[0.0, pi].
6 static double asin(double a);
Phương thức cài đặt hàm arcsin(a), giá trị trả về nằm trong đoạn [-
pi/2, pi/2].
7 static double atan(double a);
Phương thức cài đặt hàm arctang(a), giá trị trả về nằm trong
khoảng (-pi/2, pi/2).
9 static double cbrt(double a);
Phương thức trả về giá trị căn bậc ba của a.
10 static double ceil(double a);
Phương thức trả về giá trị nhỏ nhất lớn hơn hoặc bằng a. Kết quả
thực tế luôn là một số nguyên. Ví dụ ceil(2.3) sẽ trả về 3.
13 static double cos(double a);
210

STT Phương thức và mô tả


Phương thức trả về giá trị của hàm lượng giác cos(a).
14 static double cosh(double x);
Phương thức trả về giá trị của hàm hyperbolic cosine.
15 static double exp(double a);
Phương thức trả về giá trị ea, với e là cơ số của logarit tự nhiên.
17 static double floor(double a);
Phương thức trả về giá trị lớn nhất nhỏ hơn hoặc bằng a. Kết quả
thực tế luôn là một số nguyên. Ví dụ floor(3.4) trả về 3.
22 static double log(double a);
Phương thức trả về logarit tự nhiên của a.
23 static double log10(double a);
Phương thức trả về logarit thập phân của a.
25 static double max(double a, double b);
Phương thức trả về giá trị lớn nhất trong hai giá trị a và b kiểu
double.
26 static float max(float a, float b);
Phương thức trả về giá trị lớn nhất trong hai giá trị a và b kiểu
float.
27 static int max(int a, int b);
Phương thức trả về giá trị lớn nhất trong hai giá trị a và b kiểu int.
28 static long max(long a, long b);
Phương thức trả về giá trị lớn nhất trong hai giá trị a và b kiểu
long.
29 static double min(double a, double b);
Phương thức trả về giá trị nhỏ nhất trong hai giá trị a và b kiểu
double.
30 static float min(float a, float b);
Phương thức trả về giá trị nhỏ nhất trong hai giá trị a và b kiểu
float.
211

STT Phương thức và mô tả


31 static int min(int a, int b);
Phương thức trả về giá trị nhỏ nhất trong hai giá trị a và b kiểu int.
32 static long min(long a, long b);
Phương thức trả về giá trị nhỏ nhất trong hai giá trị a và b kiểu
long.
33 static double pow(double a, double b);
Phương thức trả về giá trị ab.
34 static double random();
Phương thức trả về giá trị ngẫu nhiên trong khoảng [0,1).
35 static long round(double a);
Phương thức trả về số nguyên kiểu long gần nhất a.
36 static int round(float a);
Phương thức trả về số nguyên kiểu int gần nhất a.
37 static double sin(double a);
Phương thức trả về giá trị sin(a).
38 static double sinh(double x);
Phương thức trả về giá trị sin hyperbolic của a.
39 static double sqrt(double a);
Phương thức trả về giá tị căn bậc hai của a.
40 static double tan(double a);
Phương thức trả về giá tị hàm lượng giác tang(a).
Ví dụ 6.11. Sử dụng phương thức Math.sin():
import java.lang.*;
public class MathDemo {
public static void main(String[] args) {
// Gán giá trị cho 2 số thực
double x = 45;
double y = -180;
// Biến đổi thành radian
x = Math.toRadians(x);
212

y = Math.toRadians(y);
// In ra màn hình giá trị sin của 2 số này
System.out.println("Math.sin(" + x + ")=" + Math.sin(x));
System.out.println("Math.sin(" + y + ")=" + Math.sin(y));
}
}
Kết quả trên màn hình:
Math.sin(0.7853981633974483)=0.7071067811865475
Math.sin(-3.141592653589793)=-1.2246467991473532E-16
TỔNG KẾT CHƯƠNG
Chương 6 giới thiệu các nội dung sau:
- Java cung cấp các lớp bao tương ứng với từng kiểu dữ liệu cơ sở. Các
lớp này được sử dụng thay thế cho các kiểu dữ liệu cơ sở với mục đích giúp
chương trình trở thành hướng đối tượng hoàn toàn.
- Quá trình biến đổi (bao bọc) một biến của kiểu dữ liệu đơn giản thành
đối tượng lớp bao tương ứng được gọi là boxing (đóng hộp).
- Khi sử dụng các đối tượng lớp bao như những biến kiểu dữ liệu đơn
giản, trình biên dịch đầu tiên sẽ phải “lấy dữ liệu” từ trong đối tượng bao ra, quá
trình này được gọi là unboxing (mở hộp). Trong Java, quá trình boxing được
trình biên dịch thực hiện một cách tự động, vì vậy boxing còn được gọi là
Autoboxing.
- Java cung cấp gói java.util chứa các lớp tiện ích.
- Lớp tiện ích Date dùng để làm việc với kiểu dữ liệu ngày, tháng, năm,
thời gian. Để điều khiển việc hiển thị đối tượng lớp Date, có thể sử dụng lớp
SimpleDateFormat.
- Lớp tiện ích GregorianCalendar cung cấp các phương thức và thuộc
tính để làm việc với lịch.
- Java cung cấp gói java.util.regex để làm việc với biểu thức chính quy,
trong gói có ba lớp cốt lõi:
+ Pattern: Đối tượng lớp này dùng để mô tả một mẫu xâu ký tự.
+ Matcher: Để kiểm tra một xâu ký tự xem nó có khớp với mẫu không.
+ PatternSyntaxException: Để báo rằng một xâu không thể khớp được với
mẫu đã định nghĩa.
- Lớp StringTokenizer được sử dụng để tách một xâu ký tự ra các xâu con
(được gọi là token). Các token ngăn cách với nhau bởi các ký tự ngăn cách.
213

- Lớp Random dùng để sinh các số ngẫu nhiên.


- Lớp Math chứa các thuộc tính là các hằng số toán học và các phương
thức hiện thực các hàm toán học cơ bản như hàm mũ, logarit, các hàm lượng
giác,...
BÀI TẬP
1. Lập chương trình biến đổi những xâu ký tự biểu diễn một số thành dữ
liệu số bằng cách sử dụng các phương thức parseXXX() của các lớp bao. Ví dụ:
Biến đổi xâu “1234.5” sẽ nhận được số 1234.5.
2. Cho trước một thời điểm (dữ liệu Date), lập chương trình tìm thời điểm
của ngày Thứ Năm gần nhất trước thời điểm đã cho.
3. Cho trước một năm, liệt kê số ngày tất cả các tháng trong năm đó.
4. Cho trước một tháng trong năm hiện tại, liệt kê tất cả các ngày Thứ Hai
trong tháng đó.
5. Tìm tất cả các thời điểm của ngày Thứ Sáu ngày 13 trong năm hiện tại.
6. Viết chương trình liệt kê thông tin lịch của thời điểm hiện tại.
7. Cho trước một xâu ký tự, viết chương trình kiểm tra xem xâu đó có
biểu diễn họ tên một người không (Họ tên luôn có dạng Họ Tên_đệm Tên).
8. Viết chương trình đảo một xâu biểu diễn họ tên người từ thứ tự “Họ
Tên_đệm Tên” sang “Tên Tên_đệm Họ”.
9. Viết chương trình tính diện tích hình tròn đơn vị bằng phương pháp
Monte Carlo (sinh ngẫu nhiên 10000 điểm và đếm xem có bao nhiêu điểm nằm
trong đường tròn).
10. Viết chương trình tính hàm ex bằng công thức khai triển Taylor với độ
chính xác được cho trước.
Câu hỏi ôn tập
1. Tại sao cần có các lớp bao?
2. Các lớp Date, GregorianCalendar dùng để làm gì, chứa đựng những
thông tin gì?
3. Khái niệm biểu thức chính quy, được dùng để làm gì?
4. Có thể tách các từ được ngăn cách bởi dấu ‘;’ từ một xâu ký tự bằng
lớp StringTokennizer được không?
5. Giá trị của x trong các lệnh sau là bao nhiêu?
a. x = Math.abs( 7.5 );
b. x = Math.floor( 7.5 );
c. x = Math.abs( 0.0 );
214

d. x = Math.ceil( 0.0 );
e. x = Math.abs( -6.4 );
f. x = Math.ceil( -6.4 );
g. x = Math.ceil(-Math.abs( -8 + Math.floor( -5.5 ) ) );
215

Chương 7
CÁC CẤU TRÚC DỮ LIỆU
7.1. GIỚI THIỆU
Trong tin học, khái niệm giải thuật và khái niệm cấu trúc dữ liệu là hai
khái niệm cơ sở. Thuật ngữ giải thuật (algorithm) thể hiện một giải pháp cụ thể,
thực hiện theo thứ tự từng bước để tìm ra một lời giải cho một bài toán nào đó.
Khái niệm cấu trúc dữ liệu (data structure) là cách tổ chức và lưu trữ dữ
liệu trong máy tính điện tử sao cho dữ liệu có thể được sử dụng một cách hiệu
quả.
Trong thiết kế chương trình, việc chọn cấu trúc dữ liệu là vấn đề quan
trọng. Những kinh nghiệm con người đã đúc kết trong việc xây dựng các hệ
thống lớn cho thấy việc viết mã cũng như hiệu năng của chương trình phụ thuộc
rất nhiều vào các cấu trúc dữ liệu được sử dụng trong chương trình.
Thông thường, một cấu trúc dữ liệu được chọn cẩn thận sẽ cho phép thực
hiện giải thuật hiệu quả hơn. Việc chọn cấu trúc dữ liệu cụ thể thường bắt đầu từ
chọn một cấu trúc dữ liệu trừu tượng. Một cấu trúc dữ liệu được thiết kế tốt đem
lại nhiều thao tác hữu ích hơn đồng thời giảm tài nguyên sử dụng cho một giải
thuật. Các cấu trúc dữ liệu được triển khai bằng cách sử dụng các kiểu dữ liệu,
các tham chiếu và các phép toán được cung cấp bởi một ngôn ngữ lập trình.
Như vậy, việc hình dung và tổ chức các dữ liệu theo các cấu trúc khác
nhau sẽ ảnh hưởng đến cách thức xử lý, nghĩa là, thay đổi cấu trúc dữ liệu sẽ dẫn
đến thay đổi giải thuật.
Chương này sẽ giới thiệu các cấu trúc dữ liệu trừu tượng cơ bản như ngăn
xếp, hàng đợi, danh sách liên kết và một số kiểu dữ liệu trừu tượng khác cùng
với cách cài đặt các cấu trúc dữ liệu này trên Java.
7.2. DANH SÁCH LIÊN KẾT
Trong thực tế, danh sách là một kiểu cấu trúc rất phổ biến, ví dụ danh
sách sinh viên của một khóa học, danh sách bệnh nhân chờ khám bệnh tại phòng
khám,…, các danh sách này đều có đặc điểm chung là có số lượng phần tử hữu
hạn, có thứ tự và khi cần có thể thay đổi số lượng phần tử của danh sách. Khi
cần xây dựng các chương trình làm việc với các các danh sách đối tượng dữ liệu,
đầu tiên cần phải tìm cách lưu trữ chúng.
Trước đây, khi cần lưu trữ các phần tử dữ liệu, chúng ta đã sử dụng mảng.
Mảng là kiểu dữ liệu danh sách đặc biệt hiệu quả ở tốc độ truy cập. Tuy nhiên,
khi cần giải quyết các bài toán liên quan đến danh sách có các phép toán bổ sung
216

hay loại bỏ một phần tử được thực hiện thường xuyên thì kiểu dữ liệu mảng
không hiệu quả do phải thực hiện việc dịch chuyển một số phần tử lùi xuống (để
chèn bổ sung một phần tử mới) hoặc tiến lên (để lấp chỗ phần tử bị xóa bỏ).
Ngoài ra, khi gặp các bài toán cần phải thay đổi số chiều của mảng, hoặc thay
đổi kích thước của mảng tại thời điểm đang thực thi chương trình thì việc sử
dụng cấu trúc mảng sẽ gặp khó khăn trong việc xử lý yêu cầu. Vì vậy, với các
trường hợp này, kiểu cấu trúc dữ liệu được đề xuất là danh sách liên kết (Linked
list).
Trong danh sách liên kết, mỗi phần tử của danh sách được gọi là nút. Nút
gồm hai thành phần: phần chứa “thông tin” (dữ liệu) ứng với phần tử của danh
sách, phần “liên kết” chứa địa chỉ của nút tiếp theo của nút đó. Nút cuối cùng
không có nút tiếp theo nữa nên phần “liên kết” chứa giá trị mang tính quy ước
để đánh dấu nút kết thúc danh sách, giá trị này được đặt là null. Danh sách liên
kết được mô tả bởi một đối tượng head (hoặc first) chỉ đến phần tử đầu tiên và
một đối tượng tail (hoặc last) chỉ đến phần tử cuối cùng của danh sách (Hình
7.1).

Hình 7.1. Danh sách liên kết


Để triển khai hay cài đặt cấu trúc danh sách liên kết trong chương trình,
đầu tiên cần cái đặt cấu trúc nút danh sách, sau đó mới có thể cài đặt cấu trúc
danh sách. Ví dụ 7.1 minh họa cách thức cài đặt cấu trúc nút, cấu trúc danh sách
liên kết chứa các phần tử với kiểu dữ liệu số nguyên cùng các thao tác cơ bản.
Với danh sách các phần tử có các kiểu dữ liệu khác, cách cài đặt hoàn toàn
tương tự.
Ví dụ 7.1. Cài đặt danh sách liên kết:
//File Node.java
//Cấu trúc dữ liệu nút danh sách
public class Node {
public int data; //Phần thông tin
public Node nextNode; // Phần liên kết
public Node(int value, Node node){
data = value; nextNode = node;
}
217

public Node(int value){data = value; nextNode = null;}


}

//File DanhSachLienKet.java
public class DanhSachLienKet {
private Node first; //Tham chiếu đến phần tử đầu tiên
private Node last; //Tham chiếu đến phần tử cuối cùng
public DanhSachLienKet(){ first = last = null;}
//Phương thức kiểm tra danh sách có rỗng không
public boolean isEmpty(){ return (first == null);}
//Thêm phần tử vào vị trí đầu danh sách
public void insertAtFront(int insertItem){
if (isEmpty())
first = last = new Node(insertItem);
else
first = new Node(insertItem,first );
}
//Thêm phần tử vào vị trí cuối danh sách
public void insertAtBack( int insertItem ){
if (isEmpty()) first = last = new Node(insertItem );
else
last = last.nextNode= new Node(insertItem);
}
//Xóa phần tử ở đầu danh sách và trả về 0 nếu xóa được,
//hoặc trả về -1 nếu danh sách rỗng
public int removeFromFront(){
int result = -1;
if (!isEmpty()){
result = 0;
if ( first == last )
first = last = null;
else
first = first.nextNode;
}
return result;
218

}
//Xóa phần tử ở cuối danh sách và trả về 0 nếu xóa được,
//hoặc trả về -1 nếu danh sách rỗng
public int removeFromBack(){ int result = -1;
if ( ! isEmpty() ){result = 0;
if ( first == last ) first = last = null;
else{
Node temp = first;
while ( temp.nextNode != last ) temp = emp.nextNode;
last = temp; last.nextNode =null;
}
}
return result;
}//In giá trị các phần tử danh sách ra màn hình
public void print(){
Node temp = first;
while (temp != null){
System.out.print(temp.data + " ");temp = temp.nextNode;
}
System.out.println("\n");
}
}
Để làm việc với danh sách liên kết, Java cũng cung cấp lớp LinkedList.
Các phương thức chính của lớp LinkedList được mô tả trong Bảng 7.1.
Bảng 7.1. Một số phương thức của lớp LinkedList
STT Phương thức và mô tả

1 void add(int index, Object element);


Chèn đối tượng element tại vị trí index trong danh sách.

2 boolean add(Object o);


Thêm phần tử o vào cuối danh sách.

3 boolean addAll(Collection c);


Thêm tất cả phần tử trong tập hợp collection vào cuối danh sách,
219

STT Phương thức và mô tả

theo thứ tự của chúng trong collection.

4 boolean addAll(int index, Collection c);


Chèn tất cả phần tử trong tập hợp collection vào danh sách, bắt
đầu từ vị trí index.

5 void addFirst(Object o);


Thêm đối tượng o vào đầu danh sách.

6 void addLast(Object o);


Thêm đối tượng o vào cuối danh sách.

7 void clear();
Xóa bỏ tất cả phần tử của danh sách và thu hồi bộ nhỡ đã cấp.

8 Object clone();
Tạo một bản sao của danh sách.

9 boolean contains(Object o);


Trả về true nếu danh sách chứa phần tử o.

10 Object get(int index);


Trả về phần tử tại vị trí index trong danh sách.

11 Object getFirst();
Trả về phần tử đầu tiên của danh sách.

12 Object getLast();
Trả về phần tử cuối của danh sách.

13 int indexOf(Object o);


Trả về số thứ tự (bắt đầu từ 0 – số thứ tự phần tử đầu tiên) của
phần tử o trong danh sách tại lần xuất hiện đầu tiên, hoặc -1 nếu
danh sách không chứa phần tử này.

14 int lastIndexOf(Object o);


220

STT Phương thức và mô tả

Trả về số thứ tự (bắt đầu từ 0 – số thứ tự phần tử đầu tiên) của


phần tử o trong danh sách tại lần xuất hiện cuối cùng, hoặc -1 nếu
danh sách không chứa phần tử này.

15 ListIterator listIterator(int index);


Trả về một list-iterator (kiểu như tham chiếu) đến phần tử có số
thứ tự index.

16 Object remove(int index);


Xóa bỏ khỏi danh sách phần tử tại vị trí index, trả về giá trị phần
tử đó.

17 boolean remove(Object o);


Xóa bỏ khỏi danh sách phần tử o tại vị trí xuất hiện đầu tiên.

18 Object removeFirst();
Xóa bỏ khỏi danh sách và trả về phần tử đầu tiên.

19 Object removeLast();
Xóa bỏ khỏi danh sách và trả về phần tử cuối cùng.

20 Object set(int index, Object element);


Thay thế phần tử tại vị trí index bởi phần tử element.

21 int size();
Trả về số phần tử trong danh sách.

22 Object[] toArray();
Trả về một mảng chứa tất cả phần tử của danh sách theo đúng thứ
tự trong danh sách.
Chương trình trong Ví dụ 7.2 minh họa cách sử dụng lớp LinkedList.
Ví dụ 7.2. Sử dụng lớp LinkedList trong Java:
package vn.edu.mta.fit.tutorial.chapter7;
import java.util.*;
public class LinkedListDemo {
221

public static void main(String args[]) {


LinkedList testDSLK = new LinkedList();
// Thêm phần tử vào linked list
testDSLK.add("B");
testDSLK.add("D");
testDSLK.add("E");
testDSLK.addFirst("A");
testDSLK.addLast("Z");
testDSLK.add(2, "C");
System.out.println("Hiển thị LinkedList: " + testDSLK);
// xóa phần tử khỏi linked list
testDSLK.remove("E");
testDSLK.remove(2);
testDSLK.removeFirst();
testDSLK.removeLast();
System.out.println("Hiển thị LinkedList sau xóa: " +
testDSLK);
// Thay đổi giá trị dữ liệu của một Node
Object val = testDSLK.get(1);
testDSLK.set(1, (String) val + "a thay doi");
System.out.println("LinkedList sau khi thay doi: " +
testDSLK);
}
}
Kết quả chạy chương trình:
Hiển thị LinkedList: [A, B, C, D, E, Z]
Hiển thị LinkedList sau xóa: [B, D]
LinkedList sau khi thay doi: [B, Da thay doi]
7.3. NGĂN XẾP
Ngăn xếp (stack) là một kiểu cấu trúc dữ liệu trừu tượng hoạt động theo
nguyên lý “vào sau ra trước” (Last In First Out - LIFO) hay “vào trước ra sau”
(First In Last Out – FILO). Đây là một kiểu danh sách đặc biệt mà phép bổ sung
(push) và phép loại bỏ (pop) luôn thực hiện ở một đầu (đỉnh) của danh sách.
222

Hình 7.2. Ngăn xếp với các thao tác đặc trưng
Có thể hình dung ngăn xếp như một chồng đĩa xếp lên nhau, để đưa thêm
một đĩa vào thì phải đặt nó lên trên đỉnh của chồng đĩa, và khi lấy ra cũng chỉ
lấy được đĩa ở phía trên cùng. Đĩa được đưa vào đầu tiên sẽ là chiếc đĩa lấy ra
cuối cùng khỏi ngăn xếp (Hình 7.2).
Để cài đặt ngăn xếp có thể sử dụng danh sách liên kết để lưu trữ dữ liệu.
Chương trình trong Ví dụ 7.3 minh họa cách cài đặt và sử dụng cấu trúc ngăn
xếp với kiểu dữ liệu số nguyên của từng phần tử dựa trên cơ sở cấu trúc dữ liệu
danh sách liên kết DanhSachLienKet trong Ví dụ 7.3.
Ví dụ 7.3. Cài đặt cấu trúc dữ liệu ngăn xếp:
public class NganXep{
private DanhSachLienKet stackList;
public NganXep() {
stackList = new DanhSachLienKet();
}
public void push(int value){
stackList.insertAtFront( value );
}
public int pop() {
return stackList.removeFromFront();
}
public boolean isEmpty(){
return stackList.isEmpty();
}
public void print(){
stackList.print();
}
public static void main(String[] args){
223

NganXep stack = new NganXep ();


stack.push(2); stack.push(0);
stack.push(1); stack.push(6);
stack.print();
stack.pop();
stack.pop();
stack.print();
}
}
Kết quả chạy chương trình:
6102

02
Để làm việc với ngăn xếp, Java cũng cung cấp lớp Stack với các phương
thức cơ bản được mô tả trong Bảng 7.2.
Bảng 7.2. Một số phương thức của lớp Stack
STT Phương thức và mô tả
boolean empty();
1 Kiểm tra stack có rỗng hay không. Trả về true nếu stack rỗng, trả
về false trong trường hợp ngược lại.
Object peek( );
2
Trả về phần tử ở đỉnh stack nhưng không xóa phần tử khỏi stack.
Object pop( );
3
Trả về phần tử ở đỉnh stack và xóa phần tử khỏi stack.
Object push(Object element);
4 Đưa phần tử element vào stack, phương thức trả về chính phần tử
được thêm vào.
Chương trình trong Ví dụ 7.4 minh họa cách sử dụng lớp Stack trong Java
để lưu trữ và làm việc với các phần tử kiểu số nguyên. Lớp Stack là một lớp
tổng quát, có thể cụ thể hóa để chứa các kiểu dữ liệu bất kỳ. Về lớp tổng quát, có
thể xem mục 7.6.
Ví dụ 7.4. Sử dụng lớp Stack:
package vn.edu.mta.fit.tutorial.chapter7;
224

import java.util.Stack;
public class TestStack{
public static void main(String[] agrs){
//Tạo stack các số nguyên
Stack <Integer> s = new Stack<Integer>();
//Chèn vào stack các số từ 0 đến 9
for (int i=0; i<10; i++)
s.push(i);
while(!s.isEmpty()){
System.out.print(s.peek() + " ");
s.pop();
}
}
}
Kết quả chạy chương trình:
9876543210
7.4. HÀNG ĐỢI
Hàng đợi (Queue) là một cấu trúc dữ liệu dùng để chứa các đối tượng làm
việc theo cơ chế “vào trước ra trước” (First In First Out – FIFO) hay “Last In
Last Out” (LILO). Nghĩa là đối tượng được đưa vào hàng đợi trước thì sẽ được
lấy ra khỏi hàng đợi trước đối tượng được đưa vào sau. Hàng đợi có thể được
hình dung như một hàng người đang xếp hàng để đợi được phục vụ, người đến
đầu tiên sẽ được phục vụ đầu tiên, sau đó mới đến lượt người đến thứ hai, thứ
ba,..., người đến sau cùng sẽ phải đứng xếp hàng ở cuối hàng và được phục vụ
cuối cùng.

Hình 7.3. Hàng đợi với các thao tác đặc trưng
Trong hàng đợi, thao tác thêm vào và lấy một đối tượng ra khỏi hàng đợi
được gọi lần lượt là “Enqueue” và “Dequeue”. Việc thêm một đối tượng luôn
225

diễn ra ở cuối hàng đợi và việc lấy một đối tượng ra khỏi hàng đợi luôn thực
hiện với phần tử ở vị trí đầu hàng đợi (Hình 7.3).
Cấu trúc dữ liệu kiểu hàng đợi có nhiều ứng dụng trong lập trình như
dùng để khử đệ quy, lưu vết các quá trình tìm kiếm theo chiều rộng và quay
lui, vét cạn, tổ chức quản lý và phân phối tiến trình trong các hệ điều hành, tổ
chức bộ đệm bàn phím…
Cũng giống như đối với ngăn xếp, để cài đặt hàng đợi, chúng ta có thể sử
dụng danh sách liên kết (Ví dụ 7.5).
Ví dụ 7.5. Cài đặt hàng đợi:
public class HangDoi{
private DanhSachLienKet queueList;
public HangDoi(){ queueList = new DanhSachLienKet();}
public void enqueue(int value){
queueList.insertAtBack( value );
}
public int dequeue(){
return queueList.removeFromFront();
}
public boolean isEmpty() {
return queueList.isEmpty();
}
public void print() {
queueList.print();
}
}
Tương tự như Stack, Java cũng cung cấp giao diện Queue dùng để đặc
trưng cho hàng đợi, với các phương thức cơ bản sau:
- add(), offer(): thêm phần tử vào cuối hàng đợi (Enqueue)
- remove(), poll(): lấy phần tử từ đầu hàng đợi (Dequeue)
- peek(), element(): trả về phần tử đầu hàng đợi mà không loại bỏ nó ra
khỏi hàng đợi
- isEmpty(): Kiểm tra hàng đợi có rỗng ko
Ví dụ sau minh họa cách thức sử dụng giao diện Queue trong Java. Một
số lớp hỗ trợ sẽ được giải thích chi tiết hơn trong mục 7.6 trình bày về
Collections Framework của chương này.
226

Ví dụ 7.6. Sử dụng giao diện Queue trong Java:


package vn.edu.mta.fit.tutorial.chapter7;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
public class QueueTest {
public static void main(String[] args) {
//Tạo đối tượng hàng đợi dựa trên danh sách liên kết
Queue<Integer> qe = new LinkedList<>();
for (int i = 1; i < 10; i++) {
qe.add(i); //Thêm một phần tử vào Queue.
}
System.out.println("First Out:");
while (!qe.isEmpty()) {
System.out.println("Poll: " + qe.poll());
}
//Kiểm tra Queue có rỗng không?
System.out.println("Queue is empty: " + qe.isEmpty());
//Thêm lại các giá trị vào Queue,
System.out.println("Thêm lại giá trị vào Queue");
for (int i = 1; i < 10; i++) {
qe.offer(i);
}
//Duyệt giá trị trong Queue, phải dùng
//lớp Iterator hỗ trợ
System.out.println("\n In các giá trị trong Queue");
Iterator it= qe.iterator();
while(it.hasNext()){
int iteratorValue = (Integer)it.next();
System.out.println("Queue Next Value: " + iteratorValue);
//Sử dụng lệnh PEEK trả về phần tử đầu tiên của Queue
System.out.println("Peek: " + qe.peek());
}
}
}
227

Kết quả chạy chương trình:


First Out:
Poll: 1
Poll: 2
Poll: 3
Poll: 4
Poll: 5
Poll: 6
Poll: 7
Poll: 8
Poll: 9
Queue is empty: true
Thêm lại giá trị vào Queue

In các giá trị trong Queue


Queue Next Value: 1
Peek: 1
Queue Next Value: 2
Peek: 1
Queue Next Value: 3
Peek: 1
Queue Next Value: 4
Peek: 1
Queue Next Value: 5
Peek: 1
Queue Next Value: 6
Peek: 1
Queue Next Value: 7
Peek: 1
Queue Next Value: 8
Peek: 1
Queue Next Value: 9
Peek: 1
7.5. CÂY
7.5.1. Một số khái niệm
228

Cây là một cấu trúc phi tuyến, được thiết lập trên một tập hợp hữu hạn các
phần tử được gọi là nút, trong đó có một phần tử đặc biệt gọi là nút gốc (root) và
các nút được liên kết bởi một quan hệ phân cấp gọi là quan hệ cha con.
Cây có thể định nghĩa một cách đệ quy như sau:
- Một nút là một cây. Nút đó cũng là nút gốc của cây ấy;
- Nếu T1, T2, …Tk là các cây với n1, n2,…. nk lần lượt là các gốc; n là một
nút và n có quan hệ cha con với các nút n1, n2,…. nk thì một cây mới T sẽ được
tạo lập với n là nút gốc của nó. Các cây T1, T2, …Tk được gọi là các cây con của
T. Hình 7.4 thể hiện ví dụ một cây.
Quy ước, cây mà không có nút nào được gọi là cây rỗng.
Nút gốc

Nút trong

Nút lá

Hình 7.4. Cấu trúc cây


Số các con của một nút được gọi là cấp (degree) của nút đó. Nút có cấp
bằng không được gọi là nút lá (leaf node), nút không phải nút lá được gọi là nút
trong hoặc nút nhánh (branch node). Cấp cao nhất của nút trên một cây được gọi
là cấp của cây đó.
Gốc của cây có mức (level) bằng một. Nếu nút cha có mức là i thì nút con
có mức là i+1.
Chiều cao (height) hay chiều sâu (depth) của một cây là số mức lớn nhất
của nút có trên cây đó.
Nếu n1, n2..., nk là dãy các nút mà ni là cha của ni+1 với 1 ≤ i < k thì dãy đó
được gọi là một đường đi (path) từ n1 đến nk. Độ dài của đường đi được tính
bằng số nút của đường đi đó trừ đi một.
Nếu thứ tự các cây con của một nút được yêu cầu ghi nhận thì cây đang
xét là cây có thứ tự (ordered tree), ngược lại gọi là cây không có thứ tự
(unordered tree).
7.5.2. Cây nhị phân và cây nhị phân tìm kiếm
Cây nhị phân là một dạng quan trọng của cấu trúc cây. Đặc điểm của cây
nhị phân là mọi nút trên cây chỉ có tối đa hai con. Hai cây con của mỗi nút được
229

gọi là cây con trái (left subtree) và cây con phải (right subtree).
Trong thực tế, cấu trúc cây nhị phân được sử dụng khá rộng rãi, ví dụ như
để thể hiện kết quả thi đấu tennis của giải Roland Garros, hay thể hiện biểu thức
số học với các toán tử là nút, còn các toán hạng là các cây con bên trái, bên phải
(Ví dụ Hình 7.5),..

Hình 7.5. Cây nhị phân biểu diễn biểu thức 5 + 2*3 – 4
Trong thực tế có nhiều chương trình yêu cầu phải xử lý lần lượt các nút
trên cây nhị phân theo một thứ tự nào đó, sao cho mỗi nút chỉ được duyệt một
lần. Để duyệt cây nhị phân có thể có các cách: duyệt theo thứ tự trước (preorder
traversal), duyệt theo thứ tự giữa (inorder traversal) và duyệt theo thứ tự sau
(postorder traversal). Cả ba cách duyệt đều là những cách đệ quy.
Các đoạn giả mã mô tả thuật toán duyệt cây như sau:
- Cách 1 - Duyệt theo thứ tự trước (preorder traversal):
+ Bước 1: Thăm gốc;
+ Bước 2: Duyệt cây con trái theo thứ tự trước;
+ Bước 3: Duyệt cây con phải theo thư tự trước.
- Cách 2 - Duyệt theo thứ tự giữa (inorder traversal):
+ Bước 1: Duyệt cây con trái theo thứ tự giữa;
+ Bước 2: Thăm gốc;
+ Bước 3: Duyệt cây con phải theo thứ tự giữa.
- Cách 3 - Duyệt theo thứ tự sau (postorder traversal):
+ Bước 1: Duyệt cây con trái theo thứ sau;
+ Bước 2: Duyệt cây con phải theo thứ tự sau;
+ Bước 3: Thăm gốc;
230

Ví dụ với cây nhị phân ở hình vẽ trên, dãy các nút được thăm trong các
phép duyệt là:
- Duyệt theo thứ tự trước: A B D G H E C F I G
- Duyệt theo thứ giữa: G D H B E A F I C G
- Duyệt theo thứ tự sau: G H D E B I F G C A

Hình 7.6. Ví dụ về cây nhị phân tìm kiếm


Cây nhị phân tìm kiếm là một trường hợp đặc biệt của cây nhị phân mà
giá trị của các nút thuộc cây con bên trái nhỏ hơn giá trị của nút cha, và giá trị
của các nút thuộc cây con bên phải lớn hơn giá trị của nút cha. Trong thực tế,
khi xét đến cây nhị phân, chủ yếu người ta xét cây nhị phân tìm kiếm (Hình 7.6).
Nhờ ràng buộc về nút trên cây nhị phân tìm kiếm, việc tìm kiếm một phần
tử trong cây trở nên có định hướng. Hơn nữa, nhờ cấu trúc cây mà việc tìm kiếm
trở nên nhanh đáng kể. Thao tác duyệt cây trên cây nhị phân tìm kiếm hoàn toàn
giống như trên cây nhị phân. Chỉ có một đặc điểm là khi duyệt theo thứ tự giữa,
các nút duyệt qua sẽ tạo nên một dãy các nút theo thứ tự tăng dần. Dễ dàng thấy
rằng, số lần so sánh tối đa phải thực hiện để tìm phần tử X là h, với h là chiều
cao của cây. Thao tác tìm kiếm trên cây nhị phân tìm kiếm có n nút có độ phức
tạp thuật toán cỡ O(log2n).
231

Để cài đặt cây nhị phân tìm kiếm, đầu tiên cần cài đặt cấu trúc nút của
cây. Một cây nhị phân tìm kiếm được đặc trưng chỉ bởi một nút gốc. Các cách
duyệt cây theo thứ tự trước, giữa và sau được cài đặt dưới dạng phương thức đệ
quy. Ví dụ 7.7 minh họa việc cài đặt và sử dụng cây nhị phân tìm kiếm.
Ví dụ 7.7. Cài đặt cây nhị phân tìm kiếm:
//File TreeNode.java
public class TreeNode{
int data;// Dữ liệu tại nút
TreeNode leftNode, rightNode; //Cây con trái và cây con phải
public TreeNode( int nodeData ){
data = nodeData; leftNode = rightNode = null;
}//Chèn giá trị mới vào
public void insert( int value ){
if ( value < data ){
if (leftNode == null) leftNode = new TreeNode(value);
else leftNode.insert(value);
}
else if ( value > data ) {
if (rightNode == null)
rightNode = new TreeNode(value);
else rightNode.insert( value );
}
}
}
//File Tree.java
public class Tree {
private TreeNode root;//Gốc của cây
public Tree(){ root = null;}
//Chèn giá trị mới vào cây
public void insertNode( int insertValue ){
if (root == null)
root = new TreeNode(insertValue);
else
root.insert(insertValue);
}
232

//Duyệt thứ tự trước


public void preorderTraversal(){
preorder( root );
}
//Duyệt thứ tự giữa
public void inorderTraversal(){
inorder( root );
}
//Duyệt thứ tự sau
public void postorderTraversal(){
postorder( root );
}
private void preorder(TreeNode node){
if ( node == null )
return;
System.out.print( node.data + " " );
preorder(node.leftNode);
preorder(node.rightNode);
}
private void inorder(TreeNode node){
if (node == null) return;
inorder( node.leftNode );
System.out.print( node.data + " " );
inorder( node.rightNode );
}
private void postorder(TreeNode node){
if ( node == null )
return;
postorder( node.leftNode );
postorder( node.rightNode );
System.out.print( node.data + " " );
}
}
//File TreeTest.java
public class TreeTest{
233

public static void main(String[] args){


//Tạo cây
Tree tree = new Tree();
int value;
//Chèn giá trị vào cây
for ( int i = 1; i <= 10; i++ ){
value = ( int ) ( Math.random() * 100 );
tree.insertNode( value );
}
System.out.println ( "\n\nPreorder traversal" );
tree.preorderTraversal();
System.out.println ( "\n\nInorder traversal" );
tree.inorderTraversal();
System.out.println ( "\n\nPostorder traversal" );
tree.postorderTraversal();
}
}
Kết quả chạy chương trình:
Preorder traversal
34 13 32 23 21 91 42 63 72 97

Inorder traversal
13 21 23 32 34 42 63 72 91 97

Postorder traversal
21 23 32 13 72 63 42 97 91 34
7.6. JAVA COLLECTION FRAMEWORK
7.6.1. Nền tảng các tập hợp
Trong toán học, tập hợp có thể hiểu tổng quát là một sự tụ tập của một số
hữu hạn hay vô hạn của các đối tượng nào đó (còn gọi là các phần tử của tập
hợp). Tập hợp là một khái niệm nền tảng và quan trọng của toán học hiện đại.
Trong tin học, các chương trình thường xuyên phải làm việc với tập hợp, ví dụ
như việc lưu trữ thông tin các nhân viên trong một công ty, tập hợp các sinh viên
của một cơ sở đào tạo, tập hợp các ảnh chụp X.Quang của một bệnh nhân,...
Java cũng như các ngôn ngữ lập trình khác hỗ trợ cấu trúc dữ liệu mảng
234

như một tập hợp cơ bản nhất, điểm mạnh của cấu trúc mảng là khả năng truy
xuất nhanh đến các thành phần của mảng thông qua chỉ số dựa trên tính chất các
phần tử của mảng có cùng kiểu dữ liệu. Tuy nhiên cấu trúc mảng không phù hợp
với các bài toán có nhiều thao tác thêm/xóa phần tử do phải dồn dịch các vị trí
trong mảng. Sử dụng mảng cũng gặp khó khăn khi giải quyết các yêu cầu mở
rộng trong khi đang thực thi chương trình do mảng có kích cỡ và số chiều cố
định.
Để khắc phục các giới hạn của mảng, từ phiên bản Java 1.0 đã đưa vào
lớp java.util.Vector là một lớp lưu trữ danh sách động của các đối tượng, lớp
java.util.Hashtable là lớp lưu trữ các cặp khóa/giá trị (key/value). Tới phiên bản
Java 1.2 tiếp tục giới thiệu các cách tiếp cận các tập hợp, được gọi là nền tảng
các tập hợp (Java Collections Framework - JCK). Khái niệm Collection trong
Java cung cấp cho lập trình viên một tập hợp các lớp và giao diện để giúp cho
việc xử lý tập hợp các đối tượng được dễ dàng hơn. Collection hoạt động gần
giống với mảng ngoại trừ việc nó có thể thay đổi kích thước và có nhiều các
phương thức xử lý đa dạng hơn mảng.
Một collection là một đối tượng có khả năng chứa một nhóm đối tượng
khác. Mỗi một đối tượng trong một collection được gọi là một phần tử. Các kiểu
collection khác nhau sẽ quản lý các phần tử theo các cách riêng. Các collection
trong Java chỉ làm việc với các đối tượng. Tất cả các lớp collection đều được
khai báo dạng tổng quát (generic).
Với việc sử dụng nền tảng các tập hợp, là một kiến trúc hợp nhất để biểu
diễn và thao tác trên các collection, sẽ giúp cho việc xử lý các collection độc lập
với biểu diễn chi tiết bên trong của chúng. Ngoài ra, sử dụng các nền tảng tập
hợp giúp giảm thời gian phát triển ứng dụng, tăng cường hiệu năng chương
trình, dễ dàng mở rộng với các collection mới, đồng thời khuyến khích việc tái
sử dụng các đoạn mã chương trình.
Kiến trúc của các nền tảng tập hợp bao gồm 3 thành phần sau: các giao
diện (interface), các lớp thi hành (implementation classes) và một số tiện ích
khác dưới dạng phương thức tĩnh (algorithms):
- Giao diện (Interfaces): Là các giao diện thể hiện tính chất của các kiểu
collection khác nhau như List, Set, Map… Việc sử dụng các giao diện để định
nghĩa các cấu trúc dữ liệu thay vì các lớp có ưu điểm là chúng ta có thể cài đặt
các lớp thực thi khác nhau cho cùng một kiểu giao diện. Với tiếp cận này, lập
trình viên có nhiều phương án linh hoạt tiếp cận để giải quyết bài toán.
235

- Các lớp thực hiện (Implementations): Là các lớp collection có sẵn và cài
đặt các collection interfaces. Thay vì phải viết các lớp cho riêng mình, người
dùng có thể sử dụng các lớp về collection được Java cung cấp sẵn. Hầu hết các
lớp trong Java collection đều nằm trong gói java.util. Java cũng có một tập hợp
của các lớp về collection trong gói java.util.concurrent. Nội dung của chương
này sẽ không đề cập đến collection trong gói java.util.concurrent.
- Algorithms: Là các phương thức tĩnh để xử lý trên collection, ví dụ như
sắp xếp danh sách, tìm phần tử lớn nhất...
7.6.2. Các giao diện trong nền tảng các tập hợp
Trong nền tảng các tâp hợp, các giao diện được chia thành hai nhóm là
nhóm Collection và nhóm Map.
a. Nhóm Collection
Nhóm Collection có ba giao diện con là Queue, List và Set. Điểm khác
nhau cơ bản giữa các giao diện này là cách thức lưu trữ dữ liệu của từng giao
diện (Hình 7.7).

Hình 7.7. Các giao diện của nhóm Collection


- Giao diện Collection: Là giao diện gốc của nhóm giao diện Collection.
Tất cả các giao diện con đều được thừa kế từ giao diện Collection. Vì kiểu
Collection được định nghĩa dạng giao diện tổng quát do đó kiểu Collection có
thể được sử dụng như là kiểu các tham số của phương thức trong các lớp thực
thi.
Collection chỉ làm việc với các phần tử là đối tượng, vì vậy các kiểu dữ
liệu cơ bản như int, double, boolean, char…không thể đưa trực tiếp vào
Collection mà phải thông qua các lớp bao Int, Double, Boolean, Char…
Java không có lớp thực thi cho giao diện Collection, chính vì vậy khi sử
dụng, lập trình viên phải dùng một trong số các giao diện con kế thừa từ giao
diện Collection. Giao diện Collection chỉ định nghĩa các phương thức mà các
lớp kế thừa cần phải có. Tuy nhiên, nếu một lớp thi hành nào đó không hỗ trợ
236

phương thức đã được định nghĩa ở giao diện Collection, nó có thể gây ra ngoại
lệ UnsupportedOperationException.
- Giao diện Queue: Hàng đợi cũng là một tập hợp tuần tự, tất cả các phần
tử được thêm vào cuối của hàng đợi và chỉ có thể truy cập lấy phần tử đầu tiên
của hàng đợi. Giao diện Queue cho phép lưu trữ các phần tử có giá trị lặp lại,
tuy nhiên không thể lưu trữ các phần tử là null.
- Giao diện List: Giao diện List kế thừa từ Collection, nó cung cấp thêm
các phương thức để xử lý collection kiểu danh sách với các phần tử được xếp
theo chỉ số. Giao diện List là một danh sách tuần tự các đối tượng, nơi mà các
đối tượng giống nhau có thể xuất hiện một hoặc nhiều lần, hoặc đối tượng
null…. Giao diện List cho phép thêm một phần tử vào bất kỳ một vị trí nào trong
danh sách, thay đổi một phần tử nào tại một vị trí nào đó trong danh sách, hoặc
xóa một phần tử tại một vị trí bất kỳ trong danh sách.
- Giao diện Set: Giao diện Set kế thừa từ Collection, hỗ trợ các thao tác xử
lý trên collection kiểu tập hợp. Giao diện Set yêu cầu các phần tử phải không
được trùng lặp về giá trị. Tùy theo lớp thi hành, giao diện Set hỗ trợ chứa phần
tử null hay không, nếu có hỗ trợ thì chỉ chứa nhiều nhất một phần tử null. Các
phần tử trong giao diện Set không có tính tuần tự, vì vậy không thể xác định
“phần tử thứ k của giao diện Set”. Giao diện Set không có thêm phương thức
riêng ngoài các phương thức kế thừa từ giao diện Collection.
- Giao diện SortedSet: Là giao diện kế thừa từ giao diện Set, hỗ trợ các
phương thức trên tập hợp các phần tử có thể so sánh được. Các đối tượng đưa
vào trong một SortedSet phải thuộc về lớp cài đặt giao diện Comparable hoặc
lớp cài đặt SortedSet phải nhận một Comparator trên kiểu của đối tượng đó.
Trong nền tảng các tập hợp, mỗi một kiểu giao diện sẽ sử dụng các cấu
trúc dữ liệu khác nhau để lưu các phần tử. Trong các giao diện, một số hỗ trợ
tính thứ tự của các phần tử, một số lại không hỗ trợ. Vì vậy, để duyệt các phần
tử của một collection, có thể sử dụng một trong ba cách: sử dụng đối tượng
Iterator; sử dụng vòng lặp for-each; và sử dụng phương thức forEach().
Giao diện Collection mở rộng từ giao diện Iterable (có thể lặp được)
trong gói java.lang.Iterable, do đó nó thừa kế phương thức public Iterator<E>
iterator(), bộ lặp Iterator chứa các phần tử của Collection, cho phép truy cập
đến các phần tử của Collection lần lượt theo thứ tự hoặc truy cập theo kiểu ngẫu
nhiên (Xem các ví dụ mục 7.6.5).
b. Nhóm Map
237

Giao diện thuộc nhóm Map đặc trưng bởi việc lưu trữ dữ liệu qua các cặp
key/value. Giao diện Map có hai giao diện con là SortedMap và
java.util.concurrent.ConcurrentMap. Giao diện ConcurrentMap được đưa vào
từ phiên bản Java 1.5, trong khuôn khổ giáo trình, chúng ta không xem xét giao
diện này.

Hình 7.8. Các giao diện của nhóm Map


Giao diện SortedMap là giao diện thừa kế từ giao diện Map, bổ sung thêm
khả năng sắp xếp các cặp key/value theo thứ tự tăng dần của giá trị key (Hình
7.8). Giống như SortedSet, các đối tượng khoá đưa vào trong SortedMap phải có
lớp cài đặt giao tiếp Comparable hoặc lớp cài đặt SortedMap phải nhận một
Comparator trên đối tượng khoá.
7.6.3. Lớp thực hiện
Các lớp thực hiện (implementation classes) là các lớp được Java thiết kế
sẵn nhằm hỗ trợ lập trình viên phát triển ứng dụng, các lớp này chủ yếu được
đóng gói trong gói java.util. Các lớp thực hiện này thể hiện các cấu trúc dữ liệu
cụ thể như mảng động, danh sách liên kết, cây đỏ đen, bảng băm… Trên Hình
7.9 là một số lớp thực hiện của nhóm Collection.
Giao diện Queue có hai lớp thực hiện, là lớp danh sách liên kết
(LinkedList) và lớp hàng đợi có ưu tiên (PriorityQueue). Đối với lớp danh sách
liên kết dùng để thi hành Queue, cách thức làm việc của lớp được giới thiệu
trong mục 7.4 trong chương này. Riêng với lớp PriorityQueue lưu trữ các phần
tử theo trật tự tự nhiên của các phần tử (nếu các phần tử này là kiểu
Comparable), hoặc theo một Comparator (bộ so sánh) truyền cho
PriorityQueue.
238

Iterable Interface

Class
Collection

List Queue Set

ArrayList PriorityQueue ArrayList

LinkedList ArrayList
Deque

Vector SortedSet

ArrayDeque
Stack TreeSet

Hình 7.9. Các lớp thực hiện của nhóm giao diện Collection
Một số lớp thực hiện thường sử dụng của nhóm Collection:
- Lớp ArrayList: Mảng động, nếu các phần tử thêm vào vượt quá kích
thước mảng, mảng sẽ tự động tăng kích thước. Nếu xóa phần tử thì mảng cũng
thay đổi kích thước.
- Lớp LinkedList: Danh sách liên kết hai chiều. Hỗ trợ thao tác trên đầu và
cuối danh sách.
- Lớp HashSet: Bảng băm.
- Lớp LinkedHashSet: Bảng băm kết hợp với danh sách liên kết nhằm
đảm bảo thứ tự các phần tử.
- Lớp TreeSet: Cây đỏ đen (red-black tree).
239

Hình 7.10. Các lớp thực hiện của nhóm giao diện Map
Với nhóm Map (Hình 7.10), một số lớp thực hiện thường sử dụng:
- HashMap: Bảng băm
- LinkedHashMap: Bảng băm kết hợp với danh sách liên kết nhằm đảm
bảo thứ tự khóa của các phần tử.
- TreeMap: Cây đỏ đen.
7.6.4. Các thuật toán
Các thuật toán được cài đặt như những phương thức tĩnh của lớp
Collections, có mục đích thực hiện những nhiệm vụ nhất định trên các phần tử
của tập hợp. Ví dụ một số phương tĩnh:
- static Object max(Collection c): Trả về phần tử lớn nhất trong tập hợp c;
- static Object min(Collection c): Trả về phần tử nhỏ nhất trong tập hợp c;
- static int binarySearch(List list, Object key): Trả về số thứ tự của phần
tử bằng key trong danh sách list, nếu không có phần tử bằng key thì trả về -1;
- static void sort(List list): Sắp xếp các phần tử trong danh sách list theo
thứ tự tăng dần;
- static void shuffle(List list): Hoán vị ngẫu nhiên các phần tử của danh
sách list;
-…
Ví dụ 7.8. Chương trình minh họa cách sử dụng phương thức tĩnh để trộn
danh sách các số:
package vn.edu.mta.fit.tutorial.chapter7;
import java.util.*;
public class ShuffleTest {
public static void main(String[] args) {
List numbers = new ArrayList(5);
for (int i = 1; i <= 5; i++)
numbers.add(new Integer(i));
System.out.println("Before shuffling:" + numbers + "\n");
//Trộn các số một cách ngẫu nhiên
Collections.shuffle(numbers);
System.out.println("After shuffling:" + numbers + "\n");
}
}
Kết quả chạy chương trình:
240

Before shuffling: [1, 2, 3, 4, 5]


After shuffling: [2, 1, 4, 3, 5]
7.6.5. Một số ví dụ
Ví dụ 7.9. Sử dụng giao diện Collection và bộ lặp (iterator) để duyệt các
phần tử của giao diện Collection:
package vn.edu.mta.fit.tutorial.chapter7;
import java.util.Collection;
import java.util.Iterator;
import java.util.Vector;
public class CollectionAndIterator{
public static void main(String[] args) {
// Tạo một đối tượng Collection, các phần tử kiểu String.
Collection<String> test = new Vector<String>();
//Chèn các xâu ký tự vào collection test
test.add("A");
test.add("B");
test.add("C");
test.add("D");
// In ra số phần tử của tập hợp.
System.out.println("Kích thước:" + test.size());
// Lấy ra bộ lặp Iterator để truy cập các phần tử của tập hợp
// Rõ ràng bộ lặp này chỉ chứa các String.
Iterator<String> ite = test.iterator();
// Kiểm tra xem Iteractor còn phần tử không.
while (ite.hasNext()){
// Lấy ra phần tử tại vị trí con trỏ iterator đang đứng
// Đồng thời con trỏ nhẩy vị trí thêm 1 bước.
String s = ite.next();
System.out.println("Phần tử:" + s);
}
}
}
Kết quả chạy chương trình:
Kích thước:4
Phần tử: A
241

Phần tử: B
Phần tử: C
Phần tử: D
Ví dụ 7.10. Làm việc với hàng đợi có ưu tiên, các phần tử của hàng đợi có
kiểu String. Do String là một lớp thi hành giao diện Comparable, nên các đối
tượng của lớp có thể so sánh được với nhau. Vì vậy khi được lưu trữ trong hàng
đợi có ưu tiên thì các phần tử kiểu String phải được sắp xếp theo thứ tự
alphabet:
package vn.edu.mta.fit.tutorial.chapter7;
import java.util.PriorityQueue;
import java.util.Queue
public class PriorityQueueDemo {
public static void main(String[] args) {
// Với hàng đợi PriorityQueue phần tử sẽ được sắp xếp
//vị trí theo trật tự tự nhiên của chúng.
Queue<String> names =
new PriorityQueue<String>();
names.add("B");
names.add("A");
names.add("C");
names.add("B");
while (true) {
// Lấy giá trị từ hàng đợi
String name = names.poll();
if (name == null) break;
System.out.println("Phần tử=" + name);
}
}
}
Kết quả thực hiện:
Phần tử=A
Phần tử=B
Phần tử=B
Phần tử=C
Ví dụ 7.11. Sử dụng giao diện List là giao diện có tính tuần tự, với cách
242

truy cập phần tử qua lớp thi hành ListIerator thay vì lớp Iterator, cách này cho
phép tiến lùi vị trí con trỏ truy cập đến từng phần tử của danh sách:
package vn.edu.mta.fit.tutorial.chapter7;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class DemoListAndListIterator {
public static void main(String[] args) {
//Tạo danh sách các xâu ký tự
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
// Lấy ra đối tượng ListIterator để duyệt danh sách.
// Hiện tại con trỏ đang đứng tại ví trí đầu danh sách.
ListIterator<String> listIterator = list.listIterator();
String first = listIterator.next();
System.out.println("Phần tử 1:" + first);// -->"A"
String second = listIterator.next();
System.out.println("Phần tử 2:" + second);// -->"B"
// Kiểm tra xem con trỏ có thể nhẩy lùi 1 vị trí không
if (listIterator.hasPrevious()) {
// Lùi con trỏ 1 trị trí
String value = listIterator.previous();
System.out.println("Lùi vị trí:" + value);// -->"B"
}
while (listIterator.hasNext()) {
String value = listIterator.next();
System.out.println("Phần tử:" + value);
}
}
}
Kết quả chạy chương trình:
Phần tử 1:A
243

Phần tử 2:B
Lùi vị trí:B
Phần tử:B
Phần tử:C
Phần tử:D
Ví dụ 7.12. Sử dụng giao diện Set, là giao diện không cho phép các phần
tử trùng lặp. Khi thực hiện thêm một phần tử, nếu đã tồn tại trong tập hợp thì
phần tử cũ sẽ bị bỏ ra và ghi phần tử mới vào trong tập hợp đó.
package vn.edu.mta.fit.tutorial.chapter7;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class HashSetExample {
public static void main(String[] args) {
// Tạo một đối tượng Set có sức chứa ban đầu
//10 phần tử. Nếu số lượng phần tử thêm vào
//vượt quá sức chứa hiện tại,
//nó sẽ tự tăng sức chứa lên 80%.
// Đặc điểm của HashSet là phần tử thêm
// vào sau đứng trước.
Set<String> set = new HashSet<String>(10, (float) 0.8);
//Chèn giá trị vào set
set.add("A");
set.add("B");
set.add("A"); // Trùng lặp xẩy ra.
set.add("C");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next()); //-> kết quả: C A B
}
}
}
Kết quả chạy chương trình:
C
A
244

B
Ví dụ 7.13 thể hiện việc sử dụng giao diện SortedSet là giao diện thừa kế
từ giao diện Set, đây là giao diện đặc trưng cho tập hợp có sắp xếp, các phần tử
được thêm mới vào tập hợp sẽ được tự động sắp xếp (tăng dần hoặc giảm dần).
Vì lý do này, các phần tử của tập hợp phải là đối tượng của lớp cài đặt giao diện
Comparable, chương trình sẽ trả về một ngoại lệ nếu gặp phần tử không hợp lệ.
Ví dụ 7.13. Sử dụng giao diện SortedSet:
//File SinhVien.java
package vn.edu.mta.fit.tutorial.chapter7;
public class SinhVien implements Comparable<SinhVien> {
private String hoten;
private int diem;
public SinhVien(String hoten, int diem) {
this.hoten = hoten;
this.diem = diem;
}
// So sánh Sinh viên này với Sinh viên khác.
// Giá trị trả về < 0 nghĩa là Sinh viên này < Sinh viên other.
// Nếu trả về > 0 nghĩa là Sinh viên này > Sinh viên other
// Nếu trả về 0 nghĩa là Sinh viên này = Sinh viên other.
@Override
public int compareTo(SinhVien other) {
// So sánh điểm.
return = (this.diem - other.diem);
}
@Override
public String toString() {
return "[" + this.name + ", + ", Điểm: " + this.diem +
"]";
}
}

//File SortedSetDemo.java
package vn.edu.mta.fit.tutorial.chapter7;
import java.util.SortedSet;
245

import java.util.TreeSet;
public class SortedSetDemo {
public static void main(String[] args){
// Tạo một đối tượng SortedSet thông qua
//class con TreeSet
SortedSet<SinhVien> ds = new TreeSet<SinhVien>();
SinhVien sv1 = new SinhVien("An", 6);
SinhVien sv2 = new SinhVien("Hanh", 8);
SinhVien sv3 = new SinhVien("Binh", 5);
// Thêm các phần tử vào tập hợp
// Chúng sẽ tự động được sắp xếp (Tăng dần).
ds.add(sv1);
ds.add(sv2);
ds.add(sv3);
// In ra thông tin các phần tử.
for(SinhVien sv: ds) {
System.out.println("Sinh viên: "+ sv);
}
}
}
Kết quả chạy chương trình:
Sinh viên: [Hanh, Điểm: 8]
Sinh viên: [An, Điểm: 6]
Sinh viên: [Binh, Điểm: 5]
Ví dụ 7.14. Sử dụng giao diện SortedMap với tính chất tự động sắp xếp
theo giá trị khóa. Mỗi phần tử sinh viên gồm hai thuộc tính ID và Tên được lưu
trong tập hợp, và luôn được sắp xếp theo giá trị tăng dần của thuộc tính ID:
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public class SortedMapDemo {
public static void main(String[] args) {
Map<String, String> ds = new TreeMap<String, String>();
ds.put("01", "Tuyet"); ds.put("04", "Quynh");
246

ds.put("03", "Hanh"); ds.put("02", "An");


// Tập hợp này đã sắp xếp tăng dần
Set<String> keys = ds.keySet();
for (String key: keys) {
System.out.println("ID sinh viên: " + key);
}
}
}
Kết quả chạy chương trình:
ID sinh viên: 01
ID sinh viên: 02
ID sinh viên: 03
ID sinh viên: 04
7.7. LẬP TRÌNH TỔNG QUÁT
7.7.1. Java Generics
Thuật ngữ tổng quát (generic) nghĩa là tham số hóa kiểu dữ liệu, là một
khái niệm được bổ sung từ phiên bản Java JDK 5.0. Tham số hóa kiểu dữ liệu
rất quan trọng vì nó cho phép lập trình viên tạo ra và sử dụng một lớp, một giao
diện hoặc một phương thức làm việc được với nhiều kiểu dữ liệu tham
số khác nhau để cài đặt một thuật toán đã xác định.
Phương thức với tham số hóa kiểu dữ liệu không phải là một khuôn mẫu
để áp dụng cho tất cả các lớp hoặc giao diện. Trong thực tế thì nó chỉ dùng để áp
dụng cho các kiểu dữ liệu có tính chất tập hợp. Trong gói java.util có rất nhiều
các giao diện, lớp mô tả các tập hợp và chúng đã được viết lại trên khuôn mẫu
dạng tổng quát.
Sử dụng lập trình tổng quát có những ưu điểm sau đây:
- Trình biên dịch Java áp dụng việc kiểm tra đoạn mã được tham số hóa
để phát hiện các vấn đề như vi phạm an toàn kiểu dữ liệu. Việc sửa lỗi tại thời
gian biên dịch dễ dàng hơn nhiều khi sửa chữa lỗi tại thời điểm chạy chương
trình. Nếu một chương trình được dịch không có lỗi hoặc cảnh báo nào, thì trong
quá trình chạy, nó sẽ không trả về các ngoại lệ khi gặp các lỗi thực thi
(ClassCastException). Trong thực tế thì việc đưa tham số hóa vào chỉ có tác
dụng kiểm tra trong quá trình dịch mã nguồn chương trình, sản phẩm sau khi
dịch của một đoạn mã không dùng tham số hóa và đoạn mã có dùng tham số hóa
là giống nhau. Tuy nhiên đoạn mã dùng tham số hóa có tính an toàn cao hơn.
247

- Bằng cách sử dụng tham số hóa, người lập trình có thể thực hiện các
thuật toán tổng quát với các kiểu dữ liệu tùy chọn khác nhau, và nội dung đoạn
mã nguồn trở nên rõ ràng và dễ hiểu.
- Tham số hóa trừu tượng trên các kiểu dữ liệu: Các lớp, các giao diện và
các phương thức có thể tham số hóa bởi các kiểu dữ liệu. Khi sử dụng lập trình
tổng quát, với mỗi kiểu dữ liệu cụ thể, trong quá trình thao tác không phải thực
hiện việc ép kiểu.
Để thấy rõ hơn các ưu điểm trong việc hạn chế các vi phạm an toàn kiểu
dữ liệu cũng như các lợi điểm khác khi sử dụng tiếp cận lập trình tổng quát,
chúng ta cùng xem xét hai ví dụ sau đây để phân tích lý do nên sử dụng lập trình
tổng quát. Trong Ví dụ 7.15, chương trình không sử dụng lập trình tổng
quát,chương trình sử dụng cấu trúc mảng để chứa danh sách tên các sinh viên
với kiểu dữ liệu String.
Ví dụ 7.15. Vấn đề an toàn kiểu khi không sử dụng các lớp tổng quát:
package vn.edu.mta.fit.tutorial.chapter7;
import java.util.ArrayList;
public class TestNotJavaGeneric{
public static void main(String[] args) {
//Tạp đối tượng lớp ArrayList
ArrayList ds = new ArrayList();
//Chèn giá trị vào đối tượng ArrayList
ds.add("An");
ds.add("Bình");
//Do vô tình nên thêm đối tượng không phải kiểu String
ds.add(new Integer(100));
//Cần lấy ra các phần tử và hiển thị
// Khi lấy ra phần tử, phải ép kiểu về String.
Object obj = ds.get(0);
System.out.println("Tên sinh viên 1 = " + (String)(obj));
// Lấy ra phần tử thứ 3
//(Thực tế nó là một đối tượng Integer).
obj = ds.get(2);
//Lỗi ép kiểu xẩy ra tại đây
System.out.println("Tên sinh viên 3 = " + (String)(obj));
}
248

}
Chương trình trên sử dụng lớp ArrayList là một lớp cụ thể, các phần tử
được lưu trữ trong đối tượng (danh sách) của lớp này có kiểu Object, do đó đối
tượng lớp ArrayList có thể chứa các phần tử có kiểu bất kỳ (String, Integer…)
và điều này có thể gây ra lỗi ngoại lệ khi ép kiểu đối với phần tử không phải
kiểu cần ép. Tuy nhiên, khi sử dụng phiên bản lớp tổng quát ArrayList với sự cụ
thể hóa kiểu dữ liệu phần tử là xâu ký tự (cú pháp ArrayList<String>), thì đối
tượng danh sách chỉ cho phép thêm với các phần tử kiểu String. Nếu thêm một
phần tử không phải kiểu String, khi biên dịch Java sẽ báo lỗi ngay thay vì báo
lỗi khi thực thi như ở chương trình trên.
Chương trình sử dụng phiên bản lớp tổng quát ArrayList có dạng:
package vn.edu.mta.fit.tutorial.chapter7;
import java.util.ArrayList;
public class TestJavaGeneric{
public static void main(String[] args) {
//Tạo đối tượng lớp ArrayList tổng quát
ArrayList<String> ds = new ArrayList<String>();
//Chèn giá trị vào đối tượng
ds.add("An");
ds.add("Bình");
// Khi lấy ra phần tử, không phải ép kiểu về String.
System.out.println("Tên sinh viên 1 = " + ds.get(0));
//Do vô tình nên thêm đối tượng không phải kiểu String
ds.add(new Integer(100)); //-->Báo lỗi khi biên dịch
}
}
Với phiên bản cụ thể hóa kiểu dữ liệu phần tử lớp ArrayList là String thì
khi thêm phần tử kiểu Integer, Java sẽ báo lỗi khi biên dịch.
7.7.2. Lớp tổng quát
Lớp tổng quát (generic class) là lớp có thể nhận kiểu dữ liệu (cho các
thuộc tính, tham số phương thức, hay kiểu dữ liệu trả về của phương thức...) là
một lớp bất kỳ.
Khai báo một lớp tổng quát giống như khai báo một lớp không phải tổng
quát, ngoại trừ theo sau tên lớp là một tham số kiểu dữ liệu tổng quát, trong
trường hợp có nhiều tham số thì mỗi tham số được phân cách bởi dấu phẩy.
249

Cú pháp khai báo như sau:


Tên lớp <kiểu 1, kiểu 2, kiểu 3…> {
//Sử dụng kiểu 1, kiểu 2, kiểu 3… để định nghĩa các thành viên
//…
}
Các phương thức hay thuộc tính của lớp tổng quát có thể sử dụng các kiểu
được khai báo như mọi lớp bình thường khác. Khi sử dụng lớp tổng quát, cần
chỉ định rõ kiểu dữ liệu cho các tham số kiểu, kết quả nhận được là một lớp
thông thường. Với cách sử dụng thế này, có thể nói lớp tổng quát là khuôn đúc
ra các lớp thông thường.

Ví dụ 7.16. Khai báo và định nghĩa lớp tổng quát:


package vn.edu.mta.fit.tutorial.chapter7;
public class Information<T>{
private T value;
public Information(T value) {
this.value = value;
}
public T getValue(){
return value;
}
public static void main(String[] args) {
Information<String> xaukytu =
new Information<String>("A");
}
}
Ví dụ 7.17. Khai báo một lớp tổng quát có nhiều kiểu dữ
liệu KeyValue chứa một cặp khóa và giá trị (key/value):
package vn.edu.mta.fit.tutorial.chapter7;
public class KeyValue<K, V> {
private K key;
private V value;
public KeyValue(K key, V value) {
this.key = key;
this.value = value;
250

}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {this.value = value;
}
public static void main(String[] args) {
// Tạo một đối tượng KeyValue để
// lưu danh sách sinh viên
// Integer: Id của sinh viên (K = Integer)
// String: Tên sinh viên. (V = String).
KeyValue<Integer, String> sv =
new KeyValue<Integer, String>(1,"An");
Integer id = entry.getKey();
String name = entry.getValue();
System.out.println("Id = "+ id+ " / tên = "+ name);
}
}
Lớp tổng quát cũng có thể được thừa kế. Khi khai báo một lớp thừa kế từ
một lớp tổng quát, có thể chỉ định rõ kiểu cụ thể cho tham số tổng quát, hoặc giữ
nguyên các tham số tổng quát, hoặc thêm các tham số tổng quát mới vào lớp
con.
Ví dụ 7.18. Thừa kế lớp tổng quát:
package vn.edu.mta.fit.tutorial.chapter7;
// Class này mở rộng class KeyValue<K,V>;
// Thêm một tham số generics I.
public class KeyValueInfo <K, V, I> extends KeyValue<K, V> {
private I info;
public KeyValueInfo(K key, V value) {
251

super(key, value);
}
public KeyValueInfo(K key, V value, I info) {
super(key, value);
this.info = info;
}
public I getInfo() {
return info;
}
public void setInfo(I info) {
this.info = info;
}
}
Lưu ý là kiểu dữ liệu, mà một lớp tổng quát có thể nhận, phải là một kiểu
tham chiếu, vì vậy không thể sử dụng các kiểu dữ liệu nguyên thủy cho các lớp
tổng quát.
7.7.3. Giao diện tổng quát
Tương tự như lớp tổng quát, giao diện tổng quát (interface generic) có
khai báo và cách sử dụng hoàn toàn tương tự.
Ví dụ 7.19. Khai báo và sử dụng giao diện tổng quát:
//File GenericInterface.java
package vn.edu.mta.fit.tutorial.chapter7;
public interface GenericInterface<G> {
public G doSomething();
}
//File GenericInterfaceImplemetation.java
package vn.edu.mta.fit.tutorial.chapter7;
public class GenericInterfaceImplemetation<G> implements
GenericInterface<G>{
private G something;
@Override
public G doSomething() {
return something;
}
}
252

7.7.4. Phương thức tổng quát


Phương thức tổng quát (method generic) là phương thức mà có kiểu dữ
liệu tham số phương thức hoặc kiểu trả về là tham số, đây là phương thức mà có
thể được gọi với nhiều kiểu dữ liệu (tham số phương thức, kiểu trả về) khác
nhau. Dựa vào kiểu dữ liệu được truyền vào, trình biên dịch sẽ xử lý mỗi lời gọi
phương thức sao cho phù hợp.
Ví dụ 7.20. Sử dụng phương thức tổng quát:
package vn.edu.mta.fit.tutorial.chapter7;
public class GenericMethodTest {
//Phương thức generic printArray
public <T> void printArray(T[] inputArray){
for (T element: inputArray ){System.out.print(element);}
System.out.println();
}
public static void main( String args[] ){
Integer[] intArray = { 1, 2, 3, 4 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'V', 'K', 'T', 'Q', 'S' };
GenericMethodTest demo = new GenericMethodTest();
System.out.println( "Các phần tử của mảng số nguyên là: " );
demo.printArray( intArray ); //truyền một mảng Integer
System.out.println( "\nCác phần tử của mảng số thực là: " );
demo.printArray( doubleArray ); // truyền một mảng Double
System.out.println( "\nCác phần tử của mảng ký tự là: " );
demo.printArray( charArray ); // truyền một mảng Character
}
}
Kết quả chạy chương trình:
Các phần tử của mảng số nguyên là: 1 2 3 4
Các phần tử của mảng số thực là: 1.1 2.2 3.3 4.4
Các phần tử của mảng ký tự là: HVKTQS
7.7.5. Ký tự đại diện
Trong lập trình tổng quát, dấu chấm hỏi (?), được gọi là một đại diện
(wildcard), nó đại diện cho một loại không rõ. Một kiểu tham số đại diện là một
trường hợp của kiểu tổng quát, nơi mà ít nhất một kiểu tham số là ký tự đại diện.
253

Hình 7.11. Các kiểu ký tự đại diện


Các ký tự đại diện có thể được sử dụng trong một loạt các trường hợp như
kiểu của một tham số, trường, hoặc biến địa phương; đôi khi như một kiểu giá
trị trả về. Tuy nhiên, các ký tự đại diện sẽ không bao giờ được sử dụng như là
một tham số cho lời gọi một phương thức tổng quát, khởi tạo đối tượng lớp tổng
quát, hoặc kiểu cha.
Các ký hiệu đại diện nằm ở các vị trí khác nhau có ý nghĩa khác nhau
(Hình 7.11). Ví dụ như:
- Collection<?> mô tả một tập hợp chấp nhận tất cả các loại tham số
(chứa mọi kiểu đối tượng).
- List<? extends Number> mô tả một danh sách, nơi mà các phần tử là
kiểu Number hoặc kiểu con của Number.
- Comparator<? super String> Mô tả một bộ so sánh mà tham số phải là
String hoặc kiểu cha của String.
Ví dụ 7.21. Chương trình minh họa cách sử dụng ký tự đại diện để quy
định tham số của phương thức có kiểu dữ liệu danh sách các số (có thể là số
nguyên, số thực):
package vn.edu.mta.fit.tutorial.chapter7;
import java.util.ArrayList;
import java.util.List;
public class DemoGenericsWildcards {
public static void main(String[] args) {
//Tạo danh sách các số nguyên
List<Integer> lst = new ArrayList<>();
//Chèn giá trị vào danh sách
lst.add(3);
lst.add(5);
lst.add(10);
double sum = sum(lst);
System.out.println("Tổng các số ="+sum);
254

}
public static double sum(List<? extends Number> list){
double sum = 0;
for(Number n: list){
sum += n.doubleValue();
}
return sum;
}
}
Kết quả chạy:
Tổng các số =18
TỔNG KẾT CHƯƠNG
Trong chương 7, các nội dung chính được giới thiệu gồm:
- Cấu trúc dữ liệu là cách tổ chức và lưu trữ dữ liệu trong máy tính điện
tử sao cho dữ liệu có thể được sử dụng một cách hiệu quả khi thực hiện một giải
thuật nào đó.
- Danh sách liên kết là cấu trúc dữ liệu trừu tượng mô tả một tập hợp các
phần tử hữu hạn, có thứ tự và khi cần có thể thay đổi số lượng các phần tử trong
tập hợp. Danh sách liên kết được mô tả bởi một đối tượng head (hoặc first) chỉ
đến phần tử đầu tiên và một đối tượng tail (hoặc last) chỉ đến phần tử cuối cùng
của danh sách. Với cách tổ chức trên, danh sách liên kết có ưu điểm linh hoạt và
mạnh mẽ so với cấu trúc mảng khi làm việc với các bài toán xử lý danh sách mà
thường xuyên có các thao tác bổ sung hoặc loại bỏ phần tử, hoặc các các bài
toán cần phải thay đổi số chiều của mảng, hoặc thay đổi kích thước của mảng tại
thời điểm đang thực thi chương trình.
- Ngăn xếp (Stack) là một cấu trúc dữ liệu trừu tượng mô tả một danh
sách hoạt động theo nguyên tắc “vào sau ra trước” (LIFO). Ngăn xếp được mô
tả bởi đối tượng trỏ đến phần tử đỉnh. Các thao tác cơ bản làm việc ngăn xếp là
thêm một phần tử vào đỉnh (push) và lấy một phần tử từ đỉnh (pop). Cấu trúc
ngăn xếp có nhiều ứng dụng trong khoa học máy tính như tính giá trị của biểu
thức đại số, quản lý bộ nhớ khi thi hành chương trình, hoặc được sử dụng trong
các bài toán tìm kiếm theo chiều sâu…
- Hàng đợi (Queue) là cấu trúc tương tự như ngăn xếp, mô tả một danh
sách hoạt động theo nguyên tắc “vào trước ra trước” (FIFO). Các thao tác cơ bản
làm việc với hàng đợi gồm thêm một phần tử vào cuối hàng đợi và lấy một đối
255

tượng ra khỏi đầu hàng đợi. Cấu trúc dữ liệu kiểu hàng đợi có nhiều ứng dụng
trong lập trình như dùng để khử đệ quy, lưu vết các quá trình tìm kiếm theo
chiều rộng và quay lui, vét cạn, tổ chức quản lý và phân phối tiến trình trong
các hệ điều hành, tổ chức bộ đệm bàn phím…
- Cây là một cấu trúc phi tuyến, được thiết lập trên một tập hợp hữu hạn
các phần tử được gọi là nút, trong đó có một phần tử đặc biệt gọi là nút gốc
(root) và các nút được liên kết bởi một quan hệ phân cấp gọi là quan hệ cha con.
Một trường hợp đặc biệt của cấu trúc cây là cây tìm kiếm nhị phân, đó là nút con
bên trái của một nút phải có giá trị nhỏ hơn giá trị của nút cha (của nút con này)
và nút con bên phải của nút phải có giá trị lớn hơn giá trị của nút cha (của nút
con này). Các thủ tục chính trên cây tìm kiếm nhị phân gồm: chèn một phần tử
vào trong cây, tìm kiếm một phần tử trong một cây, duyệt cây theo thứ tự tiền
tố/trung tố/hậu tố.
- Để khắc phục các giới hạn của mảng trong giải quyết các bài toán xử lý
với dữ liệu dạng danh sách động (có thể mở rộng/thu hẹp số chiều, thay đổi kích
thước danh sách…), từ phiên bản 1.2 Java đã giới thiệu một thư viện mới, được
gọi là nền tảng các tập hợp (Java Collection Framework). Thư viện này cung cấp
các lớp và giao diện để giúp cho việc xử lý tập hợp các đối tượng được dễ dàng
hơn.
- Lập trình tổng quát, hay gọi là tham số hóa kiểu dữ liệu, là khái niệm rất
quan trọng vì nó cho phép lập trình viên tạo ra và sử dụng một lớp, một giao
diện hoặc một phương thức làm việc được với nhiều kiểu dữ liệu tham
số khác nhau để cài đặt một thuật toán đã xác định. Phương thức với tham số
hóa kiểu dữ liệu không phải là một khuôn mẫu để áp dụng cho tất cả các lớp
hoặc giao diện. Trong thực tế thì nó chỉ dùng để áp dụng cho các kiểu dữ liệu có
tính chất tập hợp, chủ yếu được Java hỗ trợ trong gói java.util. Sử dụng lập trình
tổng quát có nhiều ưu điểm như nâng cao tính an toàn kiểu, giúp xây dựng các
thuật toán tổng quát...
BÀI TẬP
1. Sử dụng kiểu dữ liệu mảng động (ArrayList) để cài đặt cấu trúc dữ liệu
ngăn xếp, hàng đợi.
2. Xây dựng một danh sách liên kết với kiểu dữ liệu của trường thông tin
có kiểu số nguyên và các phương thức:
- Đếm số phần tử trong danh sách liên kết đơn.
- Tìm phần tử có giá trị lớn nhất trong danh sách.
256

- Tìm phần tử có giá trị nhỏ nhất trong danh sách.


- Sắp xếp tăng dần các phần tử của danh sách và đưa kết quả ra màn hình
3. Xây dựng một danh sách liên kết với kiểu dữ liệu của trường thông tin
có kiểu số nguyên. Cài đặt các phương thức sau:
- Ghép nối một danh sách liên kết có kiểu dữ liệu tương tự vào sau danh
sách.
- Tách danh sách thành hai danh sách, vị trí tách tại phần tử thứ k trong
danh sách và danh sách sau bắt đầu từ phần từ thứ k.
4. Viết chương trình tạo cây nhị phân tìm kiếm từ một mảng số nguyên.
5. Viết chương trình nhập một dãy số nguyên dương từ bàn phím, cho đến
khi gặp số ≤ 0 thì dừng việc nhập và in kết quả theo thứ tự ngược lại thứ tự nhập
liệu.
6. Cho một xâu ký tự S. Sử dụng cấu trúc ngăn xếp, tạo xâu ký tự S1 từ
xâu S bằng cách đảo ngược trật tự các ký tự trong xâu S.
7. Nhập một số nguyên dương N, sử dụng cấu trúc ngăn xếp, đổi số N
sang biểu diễn trong hệ cơ số nhị phân.
8. Sử dụng cấu trúc ngăn xếp, kiểm tra tính hợp lệ của một biểu thức toán
học gồm các phép toán số học +, -, *, / và dấu ngoặc.
9. Sử dụng cấu trúc ngăn xếp để khử tính “đệ quy” khi giải bài toán “tháp
Hà Nội” như sau: Có ba cọc A, B, C. Ban đầu cọc A có một số đĩa xếp theo thứ
tự nhỏ dần về kích thước từ dưới lên phía trên đỉnh. Yêu cầu đặt ra là phải
chuyển toàn bộ chồng đĩa từ cọc A sang cọc B sao cho mỗi lần di chuyển chỉ
được một đĩa từ cọc này sang cọc khác và không được đặt đĩa có kích thước lớn
nằm trên đĩa có kích thước nhỏ hơn.
10. Viết chương trình hỗ trợ tra cứu dữ liệu từ điển Anh – Việt đơn giản.
Chương trình thực hiện lưu trữ các từ và nghĩa của các từ trong một Collection
hoặc một Map.
Câu hỏi ôn tập
1. Trình bày ưu, nhược điểm của cấu trúc kiểu danh sách liên kết (linked
list) so với cấu trúc mảng (array).
2. Trong cấu trúc kiểu danh sách liên kết, để biết nút “đứng trước” một
nút đã cho trong danh sách thì phải làm thế nào?
3. Có các phương pháp cài đặt nào với cấu trúc dữ liệu kiểu ngăn xếp, so
sánh đặc điểm các cách đó.
4. Có các phương pháp cài đặt nào với cấu trúc dữ liệu kiểu hàng đợi, so
257

sánh đặc điểm các cách đó.


5. Trình bày các phương pháp duyệt cây, cho ví dụ minh họa.
6. Cho biểu thức E = (5x+y) * (2a – b)2.
- Hãy tạo cây nhị phân T biểu diễn biểu thức E;
- Muốn có biểu thức dạng tiền tố, dạng hậu tố thì phải duyệt cây nhị phân
T theo phương pháp nào? Trình bày kết quả của phép duyệt tương ứng.
258

Chương 8
XỬ LÝ NGOẠI LỆ
8.1. ĐỊNH NGHĨA NGOẠI LỆ
Ngoại lệ (Exception) là một tình trạng bất thường xảy ra trong quá trình
thực hiện của chương trình. Trong Java, ngoại lệ là một sự kiện phá vỡ trình tự
thực hiện chuẩn của chương trình. Java xem ngoại lệ như một đối tượng mà sẽ
được đưa ra ngoài (throw exception) trong khi thực thi.
Một ngoại lệ có thể xảy ra với nhiều lý do khác nhau, gây ra bởi lỗi của
người dùng, lỗi của lập trình viên hay một số trường hợp do lỗi của thiết bị phần
cứng hay nguồn dữ liệu vật lý. Ví dụ một số trường hợp gây ra ngoại lệ sau đây:
- Người dùng nhập dữ liệu không hợp lệ.
- Người dùng thực hiện các phép tính toán không hợp lệ.
- Một file cần được mở nhưng không thể tìm thấy hoặc không thể truy
cập.
- Truy vấn dữ liệu khi cơ sở dữ liệu bị đóng.
- Kết nối mạng bị ngắt trong quá trình thực hiện giao tiếp hoặc chương
trình chiếm dụng hết bộ nhớ…
Java đưa ra khái niệm ngoại lệ và mô hình xử lý ngoại lệ để thay thế cho
mô hình xử lý lỗi truyền thống.
8.2. MÔ HÌNH XỬ LÝ LỖI TRUYỀN THỐNG
Theo mô hình cũ, việc xử lý lỗi thường được cài đặt ngay tại các bước
thực hiện của chương trình. Các hàm sẽ trả về một cấu trúc lỗi khi gặp lỗi.
Ví dụ:
int ERROR1=0; ERROR2=1;
int f1(double x, double y){// Tính biểu thức 1/x+1/(y-1)
if (x==0) return ERROR1;
else {
if (y==1) return ERROR2;
else return 1/x+1/(y-1);
}
}
int f2(double x, double y, double z){// tính z(1/x+1/(y-1))
if (f1(x,y)==ERROR1) ….
if (f1(x,y)==ERROR2) ….
return z*f1(x,y);
259

}
Cách xử lý lỗi như vậy tồn tại một số nhược điểm:
- Mã lệnh và mã xử lý lỗi nằm xen kẽ khiến lập trình viên khó theo dõi
được thuật toán chính của chương trình.
- Khi một lỗi xảy ra tại hàm A, tất cả các lời gọi hàm lồng nhau đến A đều
phải xử lý lỗi mà A trả về.
Ngôn ngữ Java và các ngôn ngữ bậc cao sau này đã khắc phục được
nhược điểm này nhờ hỗ trợ khả năng tung ra (throw) và nắm bắt (catch) để xử lý
các ngoại lệ có thể xảy ra trong chương trình đảm bảo cho chương trình hoạt
động tường minh và ổn định.
8.3. TỔ CHỨC CÁC LỚP NGOẠI LỆ TRONG JAVA
Trong Java, ngoại lệ là đối tượng một lớp, các lớp ngoại lệ được tổ chức
theo phân cấp như trên Hình 8.1.

Hình 8.1. Phả hệ các lớp ngoại lệ


Có hai lớp ngoại lệ chính là Error và Exception, hai lớp này kế thừa từ
lớp Throwable, trong đó lớp Throwable được kế thừa trực tiếp từ lớp Object.
Bảng 8.1 liệt kê một số phương thức của lớp Exception.
Bảng 8.1. Một số phương thức phổ biến của lớp Exception
STT Phương thức và miêu tả
1 public String getMessage();
Trả về một thông điệp cụ thể về ngoại lệ đã xảy ra. Thông điệp này được
khởi tạo bởi constructor của Throwable.
2 public Throwable getCause();
Trả về nguyên nhân xảy ra ngoại lệ, được biểu diễn bởi đối tượng
260

STT Phương thức và miêu tả


Throwable.
3 public String toString();
Trả về tên của lớp và kết hợp với kết quả từ phương thức getMessage().
4 public void printStackTrace();
In ra kết quả của phương thức toString() cùng với stack trace đến
System.err.
5 public StackTraceElement [] getStackTrace();
Trả về một mảng chứa mỗi phần tử trên stack trace. Phần tử tại chỉ mục 0
biểu diễn phần trên cùng của Call Stack, và phần tử cuối cùng trong mảng
biểu diễn phương thức tại dưới cùng của Call Stack.
Ngoại lệ được chia thành hai loại Checked Exception (Ngoại lệ được
kiểm soát) và Unchecked Exception (Ngoại lệ không được kiểm soát).
Checked Exception là ngoại lệ xảy ra do người dùng mà không thể lường
trước được bởi lập trình viên. Ví dụ, một file được mở, nhưng file đó không thể
tìm thấy hoặc không thể truy cập. Những ngoại lệ này không cho phép bỏ qua
trong quá trình biên dịch. Checked Exception bao gồm các lớp kế thừa từ lớp
Exception ngoại trừ lớp RuntimeException. Checked Exception được kiểm tra
tại thời điểm biên dịch compile-time. Ví dụ 8.1 trình bày về Checked Exception
khi làm việc với file. Trong Chương 8, có các ví dụ đề cập đến file nhưng chỉ
với mục đích thể hiện việc phải xử lý ngoại lệ khi làm việc với file, còn nội
dung chi tiết về file sẽ được giới thiệu trong Chương 9.
Ví dụ 8.1. Checked Exception:
package test.lqdtu.vn;
import java.io.BufferedReader;
import java.io.FileReader;
public class MainCheckedException {
public static void main(String[] args) {
BufferedReader br = null;
String sCurrentLine;
br = new BufferedReader(new
FileReader("C:\\test.dat"));
while ((sCurrentLine = br.readLine()) != null) {
System.out.println(sCurrentLine);
}
261

}
}
Trong đoạn code trên trình biên dịch sẽ báo lỗi khi biên dịch:
Unhandled exception type FileNotFoundException đối với phương thức
FileReader(…) và Unhandled exception type IOException đối với phương thức
readLine().
Để sửa lỗi này, có thể xử lý ngoại lệ bằng cách đưa ra ngoài bằng cách bổ
sung thêm từ khóa throws sau tên phương thức main() và chọn một trong các lớp
ngoại lệ FileNotFoundException, IOException, Exception hoặc Throwable:
public static void main(String[] args) throws FileNotFoundException
Hoặc bổ sung thêm khối try…catch để nắm bắt các lỗi ngoại lệ theo các
lớp tương ứng như trong Ví dụ 8.2.
Ví dụ 8.2. Xử lý ngoại lệ với try…catch:
package test.lqdtu.vn;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class MainCheckedException {
public static void main(String[] args) {
BufferedReader br = null;
try {
String sCurrentLine;
br = new BufferedReader(new
FileReader("C:\\test.dat"));
while ((sCurrentLine = br.readLine()) != null) {
System.out.println(sCurrentLine);
}
}
catch (FileNotFoundException e) {
e.printStackTrace();
}
catch (IOException e) {
e.printStackTrace();
}
262

finally {
try { if (br != null) br.close();
}
catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
Bảng 8.2 liệt kê một số lớp ngoại lệ phổ biến thuộc nhóm Checked
Exception.
Bảng 8.2. Các lớp ngoại lệ checked exception
Exception Miêu tả
ClassNotFoundException Lớp không tồn tại
Cố gắng clone một đối tượng không hỗ
CloneNotSupportedException
trợ Clonable interface
IllegalAccessException Truy cập lớp bị từ chối
Thử tạo mới một đối tượng từ lớp trừu
InstantiationException
tượng hoặc giao diện
Một thread (luồng thực thi) bị dừng lại
InterruptedException
bởi một thread khác.
NoSuchFieldException Trường dữ liệu yêu cầu không tồn tại
NoSuchMethodException Phương thức yêu cầu không tồn tại
Unchecked Exception thường được hiểu là những ngoại lệ được sinh ra do
lỗi logic khi lập trình và được trả về ở thời điểm thực thi (runtime), ngoại lệ này
có thể tránh được bởi lập trình viên. Các lớp thuộc nhóm Unchecked Exception
kế thừa từ lớp RuntimeException và Error (lỗi). Lỗi trong Java được xem như
một ngoại lệ đặc biệt, lỗi xảy ra vượt quá tầm kiểm soát của lập trình viên hay
người dùng. Unchecked Exception không được kiểm tra tại thời điểm biên dịch (
compile-time).
Bảng 8.3 liệt kê một số lớp phổ biến trong nhóm Unchecked Exception.
Bảng 8.3. Các lớp ngoại lệ thuộc nhóm Unchecked Exception
Exception Miêu tả
ArithmeticException Các lỗi số học, ví dụ chia cho 0
ArrayIndexOutOfBoundsException Lỗi vượt quá chỉ số mảng
263

Phép gán các thành phần của mảng


ArrayStoreException
không phù hợp kiểu
ClassCastException Ép kiểu không hợp lệ
Tham số được sử dụng để gọi phương
IllegalArgumentException
thức không hợp lệ
Môi trường hoặc ứng dụng không đúng
IllegalStateException
trạng thái
Yêu cầu hoạt động không phù hợp với
IllegalThreadStateException
thread hiện tại
IndexOutOfBoundsException Chỉ mục ngoài giới hạn
NegativeArraySizeException Tạo mảng với kích thước là số âm
Sử dụng không hợp lệ với tham chiếu
NullPointerException
null
Chuyển đổi không hợp lệ của một
NumberFormatException
string thành một số
SecurityException Lỗi vi phạm bảo mật
StringIndexOutOfBounds Chỉ mục ngoài giới hạn của một string
UnsupportedOperationException Một hoạt động không được hỗ trợ
Ví dụ 8.3. Ví dụ Unchecked Exception:
package test.lqdtu.vn;
public class MainUncheckedException {
public static void main(String[] args) {
printArray();
}
private static void printArray() {
int[] array = new int[1];
//Truy cập chỉ số mảng ngoài khoảng cho phép
System.out.println(array[1]);
}
}
Với đoạn code trên trình biên dịch sẽ không báo lỗi, tuy nhiên khi chạy sẽ
báo lỗi do vượt quá chỉ số của mảng:
Exception in thread "main"
java.lang.ArrayIndexOutOfBoundsException: 1
264

at test.lqdtu.vn.MainUncheckedException.printArray (
MainUncheckedException.java:11)
At
test.lqdtu.vn.MainUncheckedException.main(MainUncheckedException.java:6)
8.4. QUÁ TRÌNH LAN TRUYỀN NGOẠI LỆ
Khi một ngoại lệ xảy ra bên trong một phương thức, phương thức này tạo
ra một đối tượng ngoại lệ và chuyển đối tượng ngoại lệ này cho JVM (Java
Virtual Machine). Đối tượng ngoại lệ sẽ chứa thông tin kiểu ngoại lệ và trạng
thái của chương trình khi ngoại lệ xảy ra. JVM chịu trách nhiệm tìm kiếm một
trình xử lý ngoại lệ (exception handler) để xử lý các đối tượng ngoại lệ này, quá
trình đó gọi là quá trình lan truyền ngoại lệ.

Hình 8.2. Lan truyền ngoại lệ


Để làm rõ quá trình lan truyền ngoại lệ, ta xem xét một ví dụ minh họa.
Giả sử có 4 phương thức methodA(), methodB(), methodC() và methodD().
Phương thức main() gọi methodA(), methodA() gọi tới methodB(), methodB() gọi
methodC() và methodC() gọi methodD(). Lời gọi các phương thức được lưu
trong ngăn xếp Call Stack theo thứ tự vào trước ra sau (first in – last out). Giả sử
trong phương thức methodD() xảy ra bất thường và ném ra một ngoại lệ
XxxException cho JVM, khi đó JVM sẽ tìm kiếm trình xử lý ngoại lệ
XxxException thông qua các cuộc gọi trong Call Stack. JVM tìm thấy trình xử lý
ngoại lệ XxxException trong methodA() và truyền đổi tượng ngoại lệ tới phương
265

thức methodA() để xử lý. Với mô hình lan truyền để xử lý này thì trình xử lý
ngoại lệ không nhất thiết phải nằm ở nơi có thể xảy ra ngoại lệ, nghĩa là có thể
dồn việc xử lý ngoại lệ vào một nơi, tách biệt với mã xử lý chính. Hình 8.2 minh
họa quá trình lan truyền ngoại lệ như mô tả ở trên.

8.5. XỬ LÝ NGOẠI LỆ
Có 5 từ khóa được sử dụng trong xử lý ngoại lệ là: try, catch, finally,
throws và throw. Quá trình xử lý ngoại lệ gồm ba thao tác chính:
- Khai báo một ngoại lệ (Declaring exceptions)
- Đưa ngoại lệ ra ngoài (Throwing an exception)
- Bắt và xử lý ngoại lệ (Catching an exception)
Để khai báo một ngoại lệ, dùng từ khóa throws
Ví dụ:
public void methodD() throws XxxException,YyyException,..{
// Nội dung phương thức gây ra ngoại lệ thuộc lớp
XxxException và YyyException
}
Khai báo trên chỉ ra rằng phương thức methodD() có thể gặp các ngoại lệ
thuộc các lớp XxxException hoặc YyyException trong khi thực thi, hay nói cách
khác, các bất thường xảy ra trong phương thức methodD() có thể được xử lý bởi
trình xử lý ngoại lệ thuộc các lớp XxxException hoặc YyyException. Ngoại lệ
thuộc lớp Error, RuntimeException và các lớp con của nó không cần phải khai
báo. Những trường hợp ngoại lệ này được gọi là trường hợp ngoại lệ được kiểm
soát (checked exception) bởi vì nó không được kiểm tra bởi trình biên dịch.
Để đưa (ném) một ngoại lệ ra ngoài để xử lý, dùng từ khóa throw. Ví dụ:
public void methodD() throws XxxException, YyyException {
// Nội dung phương thức
// Ngoại lệ XxxException xảy ra
if (... )
throw new XxxException(...); // Khởi tạo đối tượng
//ngoại lệ XxxException và chuyển tới JVM
// Ngoại lệ YyyException xảy ra
if (... )
throw new YyyException(...); // Khởi tạo đối tượng
//ngoại lệ YyyException và chuyển tới JVM
266

..
}
Khi một phương thức đưa ra (ném ra) một ngoại lệ, các JVM sẽ tìm kiếm
ngược thông qua Call Stack một trình xử lý ngoại lệ. Mỗi trình xử lý ngoại lệ có
thể xử lý một lớp các ngoại lệ đặc trưng. Nếu không có trình xử lý ngoại lệ nào
được tìm thấy trong Call Stack, chương trình sẽ bị dừng lại.
Để bắt và xử lý một ngoại lệ, giả sử bắt và xử lý các ngoại lệ trong
methodD(), trong đó methodD() được gọi trong methodC(). Khi đó có thể sử
dụng hai cách:
- Cách 1: Đặt methodD() trong các khối try-catch hoặc try-catch-finally,
trong đó mỗi khối catch có thể chứa một trình xử lý ngoại lệ riêng cho mỗi loại
ngoại lệ. Ví dụ:
public void methodC() {// MethodC không khai báo ngoại lệ
...... //Nội dung methodC()
try {
......
// Sử dụng methodD() có khai báo hai ngoại lệ
// XxxException và YyyException
methodD(); //Gọi methodD()
...... //Nội dung methodC()
}
catch (XxxException ex) {
// Gọi trình xử lý ngoại lệ cho XxxException
......
}
….
catch (YyyException ex} {
// Gọi trình xử lý ngoại lệ cho YyyException
......
}
….
finally { // Tùy chọn
// Đoạn code trong khối này luôn được chạy, thường
// được sử dụng để dọn dẹp bộ nhớ hoặc đóng file sau khi sử dụng
......
267

}
......
}
- Cách 2: Nếu không muốn sử dụng các khối try-catch-finally trong
methodC(), có thể khai báo tại phương thức methodC() các ngoại lệ có thể gặp
trong phương thức methodD().

Ví dụ:
public void methodC() throws XxxException, YyyException
// Khai báo hai ngoại lệ XxxException và YyyException để
// xử lý các ngoại lệ trong phương thức methodD()
{
...
// Gọi methodD(). Phương thức methodD() Đã được khai
// báo hai ngoại lệ XxxException và YyyException
methodD(); // Không cần sử dụng khối try-catch-finally
...
}
Trong xử lý ngoại lệ, thường phải dùng đến khối try-catch-finally. Cú
pháp tổng quát như sau:
try {
// Nội dung phương thức hoặc gọi các phương thức khác
// mà có thể gây ra ngoại lệ
......
}
catch (Exception1 ex) {
// Xử lý ngoại lệ Exception1
......
}
catch (Exception2 ex) {
// Xử lý ngoại lệ Exception2
......
}
finally {
// Tùy chọn
268

// Các lệnh trong khối finally luôn được thực hiện


// không quan trọng kiểu ngoại lệ.
// Dùng để dọn dẹp bộ nhớ hoặc đóng file sau khi dùng
}
Lưu ý:
- Một khối try phải đi kèm với ít nhất một khối catch hoặc finally.
- Một khối try có thể đi có nhiều khối catch. Mỗi khối catch chỉ bắt một
kiểu ngoại lệ.
- Mỗi khối catch cần một tham số để nắm giữ đối tượng ngoại lệ được
ném ra. Tham số này có thể là một đối tượng thuộc lớp Throwable
(java.lang.Throwable) hoặc lớp con của nó.
- Thứ tự khối catch đóng vai trò quan trọng. Một lớp con phải bị bắt
(được đặt ở phía trước) trước lớp cha của nó. Nếu không, trình biên dịch sẽ báo
lỗi “Exception XxxException has already been caught” (ngoại lệ XxxException
đã bị bắt).
Các lệnh trong khối finally luôn được thực hiện bất kể là các lệnh trong
khối try có thực hiện thành công hay không và khối catch có lan truyền ngoại lệ
hay không. Các lệnh trong khối finally thường được dùng để dọn dẹp bộ nhớ,
đóng file đang mở, đóng kết nối cơ sở dữ liệu,…
- Các khối try-catch có thể lồng nhau.
Ví dụ 8.4. Sử dụng try-catch-finally:
package test.lqdtu.vn;
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
public class TryCatchFinally {
public static void main(String[] args) {
try {
System.out.println("Start of the main logic");
System.out.println("Try opening a file...");
Scanner in = new Scanner(new File("test.in"));
System.out.println("File Found, processing the
file...");
System.out.println("End of the main logic");
} catch (FileNotFoundException ex) {
269

System.out.println("File Not Found caught...");


}
finally {
System.out.println("finally-block runs regardless of the
state of exception");
}
System.out.println("After try-catch-finally, life goes on...");
}
}
Kết quả khi chạy chương trình nếu không tồn tại file “test.in” sẽ như sau:
Start of the main logic
Try opening a file...
File Not Found caught...
finally-block runs regardless of the state of exception
After try-catch-finally, life goes on...
Nếu tồn tại file “test.in”, kết quả hiển thị như sau:
Start of the main logic
Try opening a file...
File Found, processing the file...
End of the main logic
finally-block runs regardless of the state of exception
After try-catch-finally, life goes on...
Ví dụ 8.5. Khối try khi không có catch đi kèm:
package test.lqdtu.vn;
public class MethodCallStackDemo {
public static void main(String[] args) {
System.out.println("Enter main()");
methodA();
System.out.println("Exit main()");
}
public static void methodA() {
System.out.println("Enter methodA()");
try {
System.out.println(1 / 0);// A divide-by-0 triggers an
// ArithmeticException - an unchecked exception
270

// This method does not catch ArithmeticException


// It runs the "finally" and popped off the
// call stack
}
finally {
System.out.println("finally in methodA()");
}
System.out.println("Exit methodA()");
}
}
Kết quả sau khi chạy chương trình:
Enter main()
Enter methodA()
finally in methodA()
Exception in thread "main" java.lang.ArithmeticException: / by zero
At test.lqdtu.vn.MethodCallStackDemo.methodA
(MethodCallStackDemo.java:12)
At test.lqdtu.vn.MethodCallStackDemo.main
(MethodCallStackDemo.java:5)
8.6. NGOẠI LỆ DO NGƯỜI DÙNG ĐỊNH NGHĨA
Trong Java, lập trình viên có thể tự tạo ra các lớp ngoại lệ, kế thừa từ lớp
Exception hoặc các lớp con của lớp Exception. Tuy nhiên, cần lưu ý rằng lớp
RuntimeException và các lớp con của nó không được kiểm tra bởi trình biên
dịch và không cần khai báo khi xây dựng phương thức. Do đó, khi kế thừa từ
các lớp này, sẽ không nhận được thông báo và có thể không kiểm soát được
những trường hợp ngoại lệ do không có mã xử lý ngoại lệ thích hợp.
Để tạo một lớp ngoại lệ MyException, có thể sử dụng cú pháp sau:
public static class MyException extends Exception {
public MyException(String msg) {
super(msg);
}
}
Ví dụ sử dụng lớp ngoại lệ trên trong trường hợp copy file, nếu hai file
trùng tên thì sẽ ném ra ngoại lệ lớp MyException:
public static class ExampleException {
271

public void copy(String fileName1, String fileName2)


throws MyException {
if (fileName1.equals(fileName2))
throw new MyException("File trùng tên");
System.out.println("Hai file khác tên nhau. Copy file thành
công");
}
public static void main(String[] args) {
ExampleException obj = new ExampleException();
try {String a = "file1.txt";String b = "file1.txt";
obj.copy(a, b);}
catch (MyException e) {
System.out.println(e.getMessage());
}
}
}
Kết quả khi chạy chương trình:
File trùng tên
Sử dụng lớp ExampleException khi copy file có tên khác nhau trong
phương thức main():
public static void main(String[] args) {
ExampleException obj = new ExampleException();
try {
String a = "file1.txt";
String b = "file2.txt";
obj.copy(a, b);
}
catch (MyException e) {
System.out.println(e.getMessage());
}
}
Kết quả khi chạy chương trình:
Hai file khác tên nhau. Copy thành công
Ngoại lệ do người dùng định nghĩa được sử dụng khi những lớp ngoại lệ
do Java cung cấp không thể hiện được những lỗi phát sinh trong chương trình.
272

TỔNG KẾT CHƯƠNG


Chương 8 đã trình bày các nội dung sau:
- Ngoại lệ (exception) là một tình trạng bất thường xảy ra trong quá trình
thực hiện của chương trình. Ngoại lệ phá vỡ trình tự thực hiện chuẩn của
chương trình do đó cần phải đưa ra ngoài và xử lý trong quá trình thực thi.
- Trong mô hình xử lý lỗi truyền thống, việc xử lý lỗi được tiến hành ngay
sau khi lỗi xảy ra, do đó mã lệnh và mã xử lý lỗi nằm xen kẽ nhau khiến lập
trình viên khó theo dõi được thuật toán chính của chương trình. Ngoài ra khi
một lỗi xảy ra tại một hàm nào đó, thì tất cả các lời gọi hàm lồng nhau đến hàm
đó đều phải xử lý lỗi mà hàm bị gọi trả về.
- Mô hình tổ chức các lớp ngoại lệ trong Java bao gồm hai lớp ngoại lệ
chính là Error và Exception được kế thừa từ lớp Throwable, trong đó lớp
Throwable được kế thừa trực tiếp từ lớp Object. Ngoại lệ cũng được chia thành
hai loại ngoại lệ được kiểm soát (Checked Exception) và ngoại lệ không được
kiểm soát (Unchecked Exception)
- Quá trình lan truyền ngoại lệ trong Java theo trình tự gọi các phương
thức được lưu trong ngăn xếp Call Stack theo thứ tự vào trước-ra sau (first-in-
last-out).
- Mỗi ngoại lệ phát sinh ra phải bị bắt giữ, nếu không ứng dụng sẽ bị ngắt.
- Việc xử lý ngoại lệ cho phép kết hợp tất cả tiến trình xử lý lỗi trong một
nơi. Lúc đó đoạn mã sẽ rõ ràng hơn.
- Java sử dụng các khối try và catch để xử lý các ngoại lệ. Các câu lệnh
trong khối try chặn ngoại lệ còn khối catch xử lý ngoại lệ.
- Xử lý ngoại lệ trong Java gồm ba thao tác chính: Khai báo một ngoại lệ
(Declaring exceptions); đưa ngoại lệ ra ngoài (throwing an exception) và bắt và
xử lý ngoại lệ (Catching an exception). Để bắt và xử lý ngoại lệ có thể sử dụng
các khối try-catch, try-catch-finally.
- Lớp ngoại lệ do người dùng tự định nghĩa được xây dựng bằng cách kế
thừa từ lớp Exception hoặc các lớp con của lớp Exception. Lớp ngoại lệ do
người dùng tự định nghĩa cho phép kiểm soát linh hoạt và mềm dẻo các ngoại lệ
có thể phát sinh trong quá trình thực thi chương trình.
BÀI TẬP
1. Xây dựng chương trình giải phương trình bậc hai có xử lý các ngoại lệ
có thể xảy ra.
2. Xây dựng lớp ngoại lệ do người dùng tự định nghĩa để kiểm soát các lỗi
273

khi nhập vào dữ liệu ngày tháng năm.


3. Thay đổi phương thức cat để nó có thể biên dịch được:
public static void cat(File file) {
RandomAccessFile input = null;
String line = null;
try {
input = new RandomAccessFile(file, "r");
while ((line = input.readLine()) != null) {
System.out.println(line);}
return;
} finally {
if (input != null) {input.close();}
}}
Câu hỏi ôn tập
1. Khái niệm ngoại lệ là gì, mô hình xử lý ngoại lệ có gì khác biệt so với
mô hình xử lý lỗi truyền thống
2. Trình bày mô hình tổ chức các lớp ngoại lệ trong Java.
3. Mô tả quá trình lan truyền ngoại lệ trong Java. Cho ví dụ thể hiện việc
có thể tách rời mã xử lý lỗi và mã chính của chương trình.
4. Ý nghĩa của khối try-catch và try-catch-finally so sánh hai khối try-
catch và try-catch-finally
5. Đoạn code sau có đúng không:
try {

} finally {
}
6. Những lớp ngoại lệ nào có thể được nắm bắt bởi khối catch sau:
catch (Exception e) {

}
Sử dụng khối này có vấn đề gì không?
274

Chương 9
LẬP TRÌNH NHẬP XUẤT DỮ LIỆU
9.1. LUỒNG TRONG JAVA
Để lập trình nhập xuất dữ liệu, Java cung cấp khái niệm luồng (stream).
Luồng đơn giản là một lộ trình mà dữ liệu sẽ di chuyển giữa chương trình và các
thiết bị nhập xuất. Luồng là một sự biểu diễn trừu tượng (cấu trúc dữ liệu, lớp)
việc trao đổi dữ liệu giữa chương trình và các thiết bị vật lý.

Hình 9.1. Minh họa luồng


Lớp java.lang.System định nghĩa các luồng nhập và xuất chuẩn, chúng là
các lớp chính của các luồng byte mà Java cung cấp. Chúng ta cũng đã sử dụng
các luồng xuất để xuất dữ liệu và hiển thị kết quả trên màn hình. Các luồng
nhập/xuất gồm có:
- Lớp System.out: Luồng xuất chuẩn dùng để hiển thị kết quả trên màn
hình.
- Lớp System.in: Luồng nhập chuẩn thường đến từ bàn phím và được dùng
để đọc các ký tự dữ liệu.
- Lớp System.err: Đây là luồng lỗi chuẩn, mặc định chính là xuất ra màn
hình.
Trong phiên bản bản đầu, Java định nghĩa luồng byte; sau đó bổ sung
thêm luồng ký tự để hỗ trợ việc nhập xuất dữ liệu kiểu ký tự (Unicode). Java
hiện thực luồng bằng tập hợp các lớp phân cấp trong gói java.io. Trong phần
tiếp theo, chúng ta sẽ đi tìm hiểu luồng byte, luồng ký tự và các lớp trong java.io
(Hình 9.2).
275

Hình 9.2. Các kiểu luồng và lớp tương ứng trong java
9.2. LUỒNG BYTE
Luồng byte (hay luồng dựa trên byte) hỗ trợ việc xuất nhập dữ liệu trên
byte, thường được dùng khi đọc ghi dữ liệu nhị phân.
Các luồng byte được định nghĩa bởi hai phân cấp lớp. Mức trên cùng là
hai lớp trừu tượng InputStream và OutputStream:
- InputStream định nghĩa những đặc điểm chung cho những luồng nhập
byte.
- OutputStream mô tả cách xử lý của các luồng xuất byte.
Hai lớp InputStream và OutputStream là hai lớp trừu tượng, là lớp cơ sở
(cha) đối với tất cả những lớp luồng xuất nhập kiểu byte. Những phương thức
định nghĩa trong hai siêu lớp này có thể dùng trong các lớp con của chúng. Vì
vậy, tập các phương thức này là tập tối thiểu các chức năng nhập xuất mà những
luồng nhập xuất kiểu byte có thể sử dụng. Bảng 9.1 và 9.2 mô tả các phương
thức trong hai lớp này.
Bảng 9.1. Các phương thức của lớp InputStream
STT Phương thức và mô tả
int available( );
1
Trả về số luợng bytes có thể đọc được từ luồng nhập.
void close();
2 Đóng luồng nhập và giải phóng tài nguyên hệ thống gắn với luồng.
Nếu không thành công sẽ ném ra một lỗi ngoại lệ IOException.
void mark(int numBytes);
3
Đánh dấu ở vị trí hiện tại trong luồng nhập.
boolean markSupported();
4
Kiểm tra xem luồng nhập có hỗ trợ hai phương thức mark() và
276

STT Phương thức và mô tả


reset() không.

int read();
5
Đọc byte tiếp theo từ luồng nhập.
int read(byte buffer[]);
6 Đọc số lượng buffer.length byte và lưu vào trong vùng nhớ buffer.
Kết quả trả về số bytes thật sự đọc được.
int read(byte buffer[], int offset, int numBytes);
7 Đọc số lượng numBytes byte bắt đầu từ địa chỉ offset và lưu vào
trong vùng nhớ buffer. Kết quả trả về số byte thật sự đọc được.
void reset();
8 Nhảy con trỏ đến vị trí được xác định bởi việc gọi hàm mark() lần
sau cùng.
long skip(long numBytes);
9
Nhảy qua numBytes dữ liệu từ luồng nhập.
Bảng 9.2. Các phương thức của lớp OutputStream
STT Phương thức và mô tả
void close();
1 Đóng luồng xuất và giải phóng tài nguyên hệ thống gắn với luồng.
Không thành công sẽ ném ra một lỗi IOException
void flush();
2
Ép dữ liệu từ bộ đệm phải ghi ngay xuống luồng (nếu có)
void write(int b);
3
Ghi b byte dữ liệu chỉ định xuống luồng
void write(byte buffer[]);
4
Ghi buffer.length byte dữ liệu từ mảng chỉ định xuống luồng.
void write(byte buffer[], int offset, int numBytes);
5 Ghi numBytes byte dữ liệu từ vị trí offset của mảng chỉ định buffer
xuống luồng.
Trong khi lập trình, lập trình viên thường xuyên phải làm việc với các cấu
trúc dữ liệu khác nhau. Các dữ liệu này về bản chất là dãy các byte liên tiếp với
cấu trúc tùy thuộc vào dữ liệu. Chính vì vậy, có thể xem các mảng byte như là
nguồn của các luồng nhập hoặc đích của các luồng xuất. Các luồng byte cung
cấp các khả năng này thông qua hai lớp xuất nhập mảng byte là
277

ByteArrayInputStream và ByteArrayOutputStream.
Lớp ByteArrayInputStream tạo luồng đầu vào từ một mảng byte trong bộ
nhớ đệm (đóng vai trò là nguồn dữ liệu đầu vào).
Lớp ByteArrayInputStream không hỗ trợ các phương thức mới mà nó nạp
chồng các phương thức của lớp InputStream như read(), skip(), available() và
reset().
Lớp ByteArrayInputStream có hai constructor:
- public ByteArrayInputStream(byte[] buf): Constructor này khởi tạo một
đối tượng ByteArrayInputStream từ một mảng buf xác định. Mảng đầu vào được
nhập vào một cách trực tiếp. Luồng nhập kết thúc khi đọc hết mảng.
- public ByteArrayInputStream(byte[] buf, int offset, int length):
Constructor này khởi tạo một đối tượng ByteArrayInputStream từ một mảng buf
xác định. Khác với constructor trên, chỉ một phần của mảng buf ở đây từ
buf[offset] đến buf[offset+length-1] được sử dụng. Nếu chỉ số offset+length-1
vượt quá kích thước của mảng, luồng nhập sẽ dừng khi đọc đến chỉ số cuối của
mảng.
Lớp ByteArrayOutputStream tạo ra luồng kết xuất đến một mảng các
byte. Nó cũng cung cấp các khả năng bổ sung để mảng kết xuất có thể mở rộng
nhằm mục đích chừa chỗ cho mảng được ghi. Lớp này cũng cung cấp các
phương thức toByteArrray() và toString() để chuyển đổi luồng thành một mảng
byte hay đối tượng xâu ký tự.
Lớp ByteArrayOutputStream cũng cung cấp hai constructor. Một
constructor có một tham số kiểu int dùng để ấn định mảng byte kết xuất theo
một kích cỡ ban đầu. Constructor thứ hai không có tham số, thiết lập vùng đệm
kết xuất với kích thước mặc định.
Lớp ByteArrayOutputStream có một số phương thức bổ sung, không được
khai báo trong OutputStream:
- reset(): Thiết lập lại kết xuất vùng đệm nhằm cho phép tiến trình ghi
khởi động lại tại đầu vùng đệm.
- size(): Trả về số byte hiện tại đã được ghi tới vùng đệm.
- writeTo(): Ghi nội dung của vùng đệm kết xuất ra luồng xuất đã chỉ
định. Để thực hiện, nó chấp nhận một đối tượng của lớp OutputStream làm tham
số, tham số này chính là luồng xuất được chỉ định.
Ví dụ 9.1 sử dụng lớp ByteArrayInputStream và ByteArrayOutputStream
để nhập và xuất dữ liệu dạng mảng byte.
278

Ví dụ 9.1. Nhập xuất dữ liệu với ByteArrayInputStream và


ByteArrayOutputStream:
import java.lang.System;
import jạva.io.*;
public class byteexam {
public static void main(String args[]) throws IOException {
ByteArrayOutputStream os =new ByteArrayOutputStream();
String s ="Welcome to Byte Array Input Outputclasses";
for(int i=0; i<s.length( );i++)
os.write (s.charAt(i) );
System.out.println("Output Stream is:" + os);
System.out.println("Size of output stream is:"+ os.size());
ByteArraylnputStream in;
in = new ByteArraylnputStream(os.toByteArray());
int ib = in.available();

System.out.println("Input Stream has:" + ib + "available bytes");


byte ibufl ] = new byte[ib];
int byrd = in.read(ibuf, 0, ib);

System.out.println("Number of Bytes read are:" + byrd);


System.out.println("They are: " + new String(ibut));
}
}
Kết quả chạy chương trình:
Output Stream is: Welcome to Byte Array Input Outputclasses
Size of output stream is:41
Input Stream has:41available bytes
Number of Bytes read are:41
They are: Welcome to Byte Array Input Outputclasses
9.3. LUỒNG KÝ TỰ
Để làm việc với xuất nhập dữ liệu văn bản, Java thông qua luồng ký tự để
quản lý việc vào ra. Các luồng ký tự xử lý theo đơn vị 2 byte nên có thể làm việc
được với các ký tự Unicode. Trong một số trường hợp làm việc với các luồng ký
tự hiệu quả hơn luồng byte.
279

Các luồng ký tự được định nghĩa dùng hai phân cấp lớp. Mức trên cùng là
hai lớp trừu tượng Reader và Writer. Lớp Reader dùng cho việc nhập dữ liệu,
còn lớp Writer dùng cho việc xuất dữ liệu.
Những lớp dẫn xuất từ Reader và Writer cũng thao tác trên các luồng ký
tự Unicode (Hình 9.3).

Hình 9.3: Các lớp kế thừa từ Reader và Writer


Lớp Reader là lớp cha của tất cả các luồng nhập ký tự và có các phương
thức tương tự như lớp InputStream. Bảng 9.3 liệt kê các phương thức do Reader
cung cấp. Các phương thức của lớp Reader giống như các phương thức của lớp
InputStream ngoại trừ phương thức available() được thay thế bởi phương thức
ready().
Bảng 9.3. Các phương thức của lớp Reader
STT Phương thức và mô tả
1 abstract void close( );
Đóng luồng.
2 void mark(int numChars);
Đánh dấu vị trí hiện tại trên luồng.
3 boolean markSupported();
Kiểm tra xem luồng có hỗ trợ thao tác đánh dấu mark() không?
4 int read();
Đọc một ký tự.
5 int read (char buffer[ ]);
Đọc buffer.length ký tự cho vào buffer.
280

STT Phương thức và mô tả


6 abstract int read(char bufferf[], int offset, int numChars);
Đọc numChars ký tự cho vào vùng đệm buffer tại vị trí
buffer[offset].
7 boolean ready();
Kiêm tra xem luồng có đọc dược không?
8 void reset( );
Dời con trỏ nhập đến vị trí đánh dấu trước đó.
9 long skip(long numChars);
Bỏ qua numChars cùa luồng nhập.
Lớp Writer là lớp cha của tất cả các luồng xuất ký tự và có các phương
thức tương tự như luồng OutputStream. Bảng 9.4 liệt kê các phương thức do
Writer cung cấp.
Bảng 9.4. Các phương thức của lớp Writer
STT Phương thức và mô tả
abstract void close();
1
Đóng luồng xuất. Có lỗi ném ra IOException.
abstract void flush();
2
Dọn dẹp luồng (buffer xuất)
void writef(int ch);
3
Ghi một ký tự ch.
void write (byte buffer[]);
4
Ghi một mảng các ký tự.
abstract void write(char buffer[], int offset, int numChars);
5
Ghi một phần của mảng ký tự.
void write(String str);
6
Ghi một xâu ký tự.
void write(String str, int offset, int numChars);
7
Ghi một phần của một xâu ký tự
Khi cần đọc dữ liệu từ thiết bị nhập (System.in), lớp BufferedReader cung
cấp các phương thức làm việc thuận tiện. Tuy nhiên, chúng ta không thể xây
dựng một lớp BufferedReader trực tiếp từ System.in. Thay vào đó phải chuyển
System.in thành một luồng ký tự bằng cách dùng InputStreamReader.
Để có được một đối tượng InputStreamReader gắn với System.in ta dùng
281

constructor của InputStreamReader:


InputStreamReader(InputStream inputStream);
Tiếp theo dùng constructor của lớp BufferedReader để tạo ra đối tượng
BufferedReader với đối tượng InputStreamReader đã được tạo ra làm tham số:
BufferedReader(Reader inputReader);
Ví dụ, tạo một BufferedReader gắn với luồng nhập chuẩn (bàn phím):
BufferedReader br
= new BufferedReader(new InputStreamReader(System.in));
Sau khi thực hiện câu lệnh trên, br là một luồng ký tự gắn với thiết bị
nhập thông qua System.in. Ví dụ 9.2 và 9.3 dưới đây minh họa các sử dụng
BufferReader đọc dữ liệu từ bàn phím.
Ví dụ 9.2. Dùng BufferedReader đọc từng ký tự từ thiết bị nhập. Việc đọc
kết thúc khi gặp dấu chấm (dấu chấm để kết thúc chương trình):
import java.io.*;
class ReadChars{
public static void main(String args[]) throws IOException{
char c;
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
System.out.println("Nhap chuoi ky tu, gioi han dau cham.");
// read characters
do {
c = (char) br.read();
System.out.println(c);
} while(c != '.');
}
}
Ví dụ 9.3: Dùng BufferedReader đọc xâu ký tự từ thiết bị nhập. Chương
trình kết thúc khi gặp xâu đọc là “stop”:
import java.io.*;
class ReadLines {
public static void main(String args[]) throws IOException{
// create a BufferedReader using System.in
BufferedReader br = new BufferedReader(new
InputStreamReader(System.in));
282

String str;
System.out.println("Nhap chuoi.");
System.out.println("Nhap 'stop' ket thuc chuong trinh.");
do {
str = br.readLine();
System.out.println(str);
} while(!str.equals("stop"));
}
}
Trong ngôn ngữ Java, bên cạnh việc dùng System.out để xuất dữ liệu ra
thiết bị xuất (thường dùng để debug chương trình), chúng ta có thể dùng luồng
PrintWriter đối với các chương trình “chuyên nghiệp”.
PrintWriter cũng là một trong những lớp luồng ký tự. Việc dùng các lớp
luồng ký tự để xuất dữ liệu ra thiết bị xuất thường được “ưa chuộng” hơn.
Để xuất dữ liệu ra thiết bị xuất với PrintWriter, cần phải chỉ định
System.out cho luồng xuất. Ví dụ, tạo đối tượng PrintWriter để xuất dữ liệu ra
thiết bị xuất:
PrintWriter pw = new PrintWriter(System.out, true);
Ví dụ 9.4. Dùng PrintWriter để xuất dữ liệu ra thiết bị xuất:
import java.io.*;
public class PrintWriterDemo {
public static void main(String args[]) {
PrintWriter pw = new PrintWriter(System.out, true);
int i = 10;
double d = 123.67;
double r = i+d;
pw.println("Using a PrintWriter.");
pw.println(i);
pw.println(d);
pw.println(i + " + " + d + " = " + r);
}
}
9.4. NHẬP XUẤT VỚI FILE
Khi lập trình ứng dụng, chúng ta rất hay phải thao tác đọc dữ liệu từ các
file hoặc ghi vào dữ liệu vào file ở bộ nhớ ngoài như ổ đĩa cứng. Hai luồng trong
283

java.io thực hiện việc xuất nhập file là FileInputStream và FileOutputStream.


Đọc và ghi file dùng luồng byte:
Cần tạo một luồng byte gắn với file chỉ định dùng lớp FileInputStream để
nhập hoặc FileOutputStream để xuất. Để mở một file, ta chỉ cần tạo một đối
tượng của những lớp này, tên file cần mở là tham số trong constructor. Khi file
mở, việc đọc và ghi dữ liệu trên file được thực hiện một cách bình thường thông
qua các phương thức cung cấp trong luồng.
Để đọc dữ liệu từ file, đầu tiên cần sử dụng constructor với biến truyền
vào là tên file để tạo đối tượng luồng nhập:
FileInputStream(String fileName);
Nếu fileName không tồn tại thì constructor ném ra ngoại lệ kiểu
FileNotFoundException.
Sau khi tạo đối tượng luồng nhập, để đọc dữ liệu có thể dùng phương thức
read():
int read();
Phương thức read() đọc một byte từ file và trả về giá trị của byte đọc
được. Trả về -1 khi hết file, và ném ra ngoại lệ IOException khi có lỗi đọc.
Sau khi làm việc xong với file, cần đóng luồng bằng cách dùng phương
thức close() để giải phóng tài nguyên hệ thống đã cấp phát cho luồng file:
void close();
Ví dụ 9.5. Đọc dữ liệu từ file, hiển thị nội dung của một file tên test.txt
lưu tại ổ đĩa D:
import java.io.*;
class ShowFile {
public static void main(String args[]) throws IOException{
int i;
FileInputStream fin;
try {
fin = new FileInputStream(“D:\\test.txt”);
}
catch(FileNotFoundException exc){
System.out.println("File Not Found");
return;
}
catch(ArrayIndexOutOfBoundsException exc){
284

System.out.println("Usage: ShowFile File"); return;


}
// read bytes until EOF is encountered
do {
i = fin.read();
if (i != -1)
System.out.print((char) i);
} while(i != -1);
fin.close();
}
}
Để ghi dữ liệu ra file, cần tạo luồng xuất bằng constructor của lớp
FileOutputStream với tham số là tên file cần ghi:
FileOutputStream(String fileName);
Nếu đối tượng luồng xuất ra file không tạo ra được thì constructor ném ra
ngoại lệ FileNotFoundException.
Để ghi dữ liệu ra file dùng đối tượng luồng xuất, có thể dùng phương thức
write():
void write(int byteval);
Phương thức này ghi một byte xác định bởi tham số byteval ra luồng file,
và ném ra IOException khi có lỗi ghi.
Sau khi làm việc xong với file, cần đóng luồng xuất ra file bằng cách
dùng phương thức close():
void close();
Ví dụ 9.6. Ghi dữ liệu xuống file, copy nội dung một file text đến một file
text khác:
import java.io.*;
class CopyFile {
public static void main(String args[])throws IOException{
int i;
FileInputStream fin; FileOutputStream fout;
try{// open input file
try { fin = new FileInputStream(“D:\\source.txt”);
}
catch(FileNotFoundException exc){
285

System.out.println("Input File Not Found");


return;
}
// open output file
try {
fout = new FileOutputStream(“D:\\dest.txt”);
}
catch(FileNotFoundException exc) {
System.out.println("Error Opening Output File");
return;
}
}
catch(ArrayIndexOutOfBoundsException exc) {
System.out.println("Usage: CopyFile From To");
return;
}
try {// Copy File
do { i = fin.read();
if(i != -1) fout.write(i);
} while(i != -1);
}
catch(IOException exc){
System.out.println("File Error");
}
fin.close();
fout.close();
}
}
Kết quả, chương trình sẽ copy nội dung của file D:\source.txt và ghi vào
một file mới D:\dest.txt.
Đọc và ghi file dùng luồng ký tự:
Thông thường để đọc/ghi file chúng ta thường dùng luồng byte, nhưng đối
với luồng ký tự chúng ta cũng có thể thực hiện được. Ưu điểm của việc dùng
luồng ký tự là chúng thao tác trực tiếp trên các ký tự Unicode. Vì vậy luồng ký
tự là chọn lựa tốt nhất khi cần lưu những văn bản Unicode.
286

Hai lớp luồng thường dùng cho việc đọc/ghi dữ liệu ký tự với file là
FileReader và FileWriter.
Ví dụ 9.7. Đọc những dòng văn bản nhập từ bàn phím và ghi chúng ra file
tên là “test.txt”. Việc đọc và ghi kết thúc khi người dùng nhập vào chuỗi “stop”.
import java.io.*;
class GhiFileText {
public static void main(String args[]) throws IOException{
String str; FileWriter fw;
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
try{
fw = new FileWriter("D:\\test.txt");
}
catch(IOException exc){
System.out.println("Khong the mo file.");
return;
}
System.out.println("Nhap ('stop' de ket thuc chuong trinh).");
do {
System.out.print(": ");
str = br.readLine();
if (str.compareTo("stop") == 0)
break;
str = str + "\r\n";
fw.write(str);
} while(str.compareTo("stop") != 0);
fw.close();
}
}
Ví dụ 9.8. Đọc và hiển thị nội dung của file “test.txt” lên màn hình:
import java.io.*;
class DocFileText {
public static void main(String args[]) throws Exception {
//Tạo luồng đọc văn bản
FileReader fr = new FileReader("D:\\test.txt");
287

BufferedReader br = new BufferedReader(fr);


String s;
while((s = br.readLine()) != null) {
System.out.println(s);
}
fr.close();
}
}
Bên cạnh việc xử lý xuất nhập trên file theo kiểu tuần tự thông qua các
luồng, Java cũng hỗ trợ truy cập ngẫu nhiên nội dung của một file nào đó bằng
lớp RandomAccessFile.
RandomAccessFile không dẫn xuất từ InputStream hay OutputStream mà
nó hiện thực các interface DataInput, DataOutput (có định nghĩa các phương
thức nhập/xuất cơ bản).
RandomAccessFile hỗ trợ vấn đề định vị con trỏ file bên trong một file
bằng cách dùng phương thức seek(long newPos).
Ví dụ 9.9. Chương trình ghi 6 số kiểu double xuống file, rồi đọc lên theo
thứ tự ngẫu nhiên:
import java.io.*;
class RandomAccessDemo {
public static void main(String args[]) throws IOException {
double data[] = {19.4, 10.1, 123.54, 33.0, 87.9, 74.25};
double d;
RandomAccessFile raf;
try { raf =
new RandomAccessFile("D:\\random.dat","rw"); }
catch(FileNotFoundException exc){
System.out.println("Cannot open file.");
return;
}
// Write values to the file.
for(int i=0; i < data.length; i++){
try {
raf.writeDouble(data[i]);
}
288

catch(IOException exc) {
System.out.println("Error writing to file.");
return ;
}
}
try { // Now, read back specific values
raf.seek(0); // seek to first double
d = raf.readDouble();
System.out.println("First value is " + d);
raf.seek(8); // seek to second double
d = raf.readDouble();
System.out.println("Second value is " + d);
raf.seek(8 * 3); // seek to fourth double
d = raf.readDouble();
System.out.println("Fourth value is " + d);
System.out.println();
System.out.println("Here is every other value: ");
for(int i=0; i < data.length; i+=2){
raf.seek(8 * i); // seek to ith double
d = raf.readDouble();
System.out.print(d + " ");
}
System.out.println("\n");
}
catch(IOException exc) {
System.out.println("Error seeking or reading."); }
raf.close();
}
}
9.5. NHẬP XUẤT CÓ VÙNG ĐỆM
Những ví dụ chúng ta đã xem xét phần trên đều làm việc trực tiếp với các
thiết bị vào/ra (bàn phím, màn hình, file…). Với các chương trình cần đọc/ghi
thường xuyên, hiệu suất có thể sẽ bị giảm đi đáng kể. Nguyên nhân là các thao
tác đọc ghi tới các thiết bị lưu trữ như ổ cứng thường mất rất nhiều thời gian.
Chính vì vậy, Java cung cấp cơ chế nhập xuất có vùng đệm để giải quyết vấn đề
289

này.
Vùng đệm là nơi lưu trữ dữ liệu trung chuyển trên đường truyền, thường
được tạo ra trong bộ nhớ trong (có tốc độ truy cập nhanh hơn nhiều so với bộ
nhớ ngoài). Trong quá trình nhập/xuất, Java sẽ tạm thời nhập/xuất với vùng
đệm. Chỉ khi vùng đệm đầy, Java mới thực hiện các lệnh hệ thống để chuyển dữ
liệu sang nguồn nhập/xuất thật.
Trong khi thực hiện nhập có lập vùng đệm, một số lượng các byte lớn từ
nguồn nhập sẽ được đọc và lưu trữ trong vùng đệm nhập. Khi chương trình đọc
dữ liệu, thay vì phải đọc từ luồng nhập, các byte dữ liệu sẽ được đọc từ vùng
đệm nhập.
Tiến trình xuất với vùng đệm xuất cũng thực hiện tương tự. Khi dữ liệu
được một chương trình ghi ra một luồng, dữ liệu đó không được lưu trực tiếp ra
đích xuất (file chẳng hạn) mà được lưu trữ trong vùng đệm xuất. Dữ liệu được
lưu trữ đến khi vùng đệm trở nên đầy hoặc các luồng xuất được xả trống (thông
qua lệnh flush). Khi đó dữ liệu ở vùng đệm xuất có lập vùng đệm được chuyển
đến đích của luồng xuất.
Vùng đệm được phân bố nằm giữa chương trình và đích của luồng có lập
vùng đệm. Để nhập xuất có vùng đệm, Java cung cấp hai lớp
BufferedInputStream và BufferedOutputStream.
Đối tượng lớp BufferedInputStream được tự động tạo ra và chứa đựng
vùng đệm để hỗ trợ vùng đệm nhập. Nhờ đó chương trình có thể đọc dữ liệu
từng luồng theo byte một mà không ảnh hưởng đến khả năng thực hiện của hệ
thống. Bởi lớp BufferedInputStream là một bộ lọc, nên có thể áp dụng nó cho
một số đối tượng nhất định của lớp InputStream và cũng có thể phối hợp với các
file đầu vào khác.
Lớp này sử dụng vài biến để thực hiện các cơ chế lập vùng đệm đầu vào.
Các biến này được khai báo là protected và do đó chương trình không thể truy
cập trực tiếp. Lớp này định nghĩa hai constructor, một cho phép chỉ định kích cỡ
của vùng đệm nhập trong khi đó constructor kia thì không. Nhưng cả hai
constructor đều tiếp nhận đối tượng của lớp InputStream làm đối số.
BufferedInputStream nạp chồng tất cả các phương thức truy cập mà InputStream
cung cấp và không định nghĩa thêm bất kì phương thức mới nào.
Lớp BufferedOutputStream cũng định nghĩa hai constructor cho phép chỉ
định kích cỡ của vùng đệm xuất hay như cung cấp một kích cỡ vùng đệm ngầm
định. BufferedOutputStream nạp chồng tất cả các phương thức của
290

OutputStream và không định nghĩa thêm phương thức nào khác.


Ví dụ 9.10. Dùng các luồng nhập/xuất có lập vùng đệm:
import javaJang. * ;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.SequenceInputStream;
import java.io.IOException;
public class buffexam {
public static void main(String args[ ]) throws IOException{
// defining sequence input stream
SequenceInputStream Seq3;
FileInputStream Fis 1 ;
Fisl = new FileInputStream("byteexam.java");
FileInputStream Fis2;
Fis2= new FileInputStream("fileioexam.java");
Seq3 = new SequenceInputStream(Fisl, Fis2);
// create buffered input and output streams
BufferedInputStream inst;
inst.= new BufferedInputStream(Seq3);
BufferedOutputStream oust;
oust= new BufferedOutputStream(System.out);
inst.skip(1000);
boolean eof = false;
int bytcnt = 0;
while(!eof){
int num = inst.read();
if(num = = -1){
eof =true;
}
else{
oust.write((char) num);
+ + bytcnt;
}
}
291

String bytrd = String.valueOf(bytcnt);


bytrd + = "bytes were read";
//Ghi ra số byte đã đọc
oust.write(bytrd.getBytes(), 0, bytrd.length());
// close all streams.
inst.close(); oust.close();
Fisl.close();
Fis2.close();
}
}
9.6. NHẬP XUẤT DỮ LIỆU NHỊ PHÂN
Phần trên chúng ta đã đọc và ghi các bytes dữ liệu là các ký tự mã
Unicode. Để đọc và ghi những giá trị nhị phân của các kiểu dữ liệu trong Java,
chúng ta sử dụng lớp DataOutputStream và DataInputStream.
Lớp DataOutputStream hiện thực interface DataOuput. Interface
DataOutput có các phương thức cho phép ghi tất cả những kiểu dữ liệu cơ sở
của Java ra luồng (theo định dạng nhị phân). Bảng 9.5 liệt kê các phương thức
của DataOutputStream.
Bảng 9.5. Một số phương thức của DataOutputStream
STT Phương thức và mô tả
void writeBoolean(boolean val);
1
Ghi xuống luồng một giá trị boolean được xác định bởi val.
void writeByte (int val);
2
Ghi xuống luồng một byte được xác định bởi val.
void writeChar (int val);
3
Ghi xuống luồng một ký tự được xác định bởi val.
void writeDouble(double val);
4
Ghi xuống luồng một giá trị Double được xác định bởi val.
void writeFloat (float val);
5
Ghi xuống luồng một giá trị float được xác định bởi val.
void writeInt (int val);
6
Ghi xuống luồng một giá trị int được xác định bởi val.
292

STT Phương thức và mô tả


void writeLong (long val);
7
Ghi xuống luồng một giá trị long được xác định bởi val.
void writeShort (int val);
8
Ghi xuống luồng một giá trị short được xác định bởi val.

Contructor: DataOutputStream(OutputStream outputStream);


9 outputStream là luồng xuất dữ liệu. Để ghi dữ liệu ra file thì
outputStream có thể đối tượng FileOutputStream.
Lớp DataInputStream hiện thực interface DataInput. Interface DataInput
có các phương thức cho phép đọc tất cả những kiểu dữ liệu cơ sở của Java (theo
định dạng nhị phân). Bảng 9.6 thể hiện một số phương thức của
DataInputStream.
Bảng 9.6. Một số phương thức của DataInputStream
STT Phương thức và mô tả
boolean readBoolean();
1
Đọc một giá trị boolean.
byte readByte( );
2
Đọc một byte.
char readChar( );
3
Đọc một ký tự.
double readDouble( );
4
Đọc một giá trị double.
float readFloat( );
5
Đọc một giá trị float.
int readInt( );
6
Đọc một giá trị int.
long readLong( );
7
Đọc một giá trị long.
short readShort( );
8
Đọc một giá trị short.
293

STT Phương thức và mô tả


DataInputStream(InputStream inputStream);
9 Contructor: InputStream: là luồng nhập dữ liệu. Để đọc dữ liệu
từ file thì đối tượng InputStream có thể là FileInputStream.
Ví dụ 9.11. Dùng DataOutputStream và DataInputStream để ghi và đọc
những kiểu dữ liệu khác nhau trên file:
import java.io.*;
class RWData {
public static void main(String args[]) throws IOException{
DataOutputStream dataOut;
DataInputStream dataIn;
int i = 10; double d = 1023.56; boolean b = true;
try {
dataOut =
new DataOutputStream(
new FileOutputStream("D:\\testdata"));
}
catch(IOException exc) {
System.out.println("Cannot open file.");
return;
}
try {
System.out.println("Writing " + i);
dataOut.writeInt(i);
System.out.println("Writing " + d);
dataOut.writeDouble(d);
System.out.println("Writing " + b);
dataOut.writeBoolean(b);
System.out.println("Writing " + 12.2 * 7.4);
dataOut.writeDouble(12.2 * 7.4);
}
catch(IOException exc) {
System.out.println("Write error.");
}
294

dataOut.close();
System.out.println();
try {
dataIn = new DataInputStream(
new FileInputStream("D:\\testdata"));
}
catch(IOException exc) {
System.out.println("Cannot open file."); return;
}
try {
i = dataIn.readInt();
System.out.println("Reading " + i);
d = dataIn.readDouble();
System.out.println("Reading " + d);
b = dataIn.readBoolean();
System.out.println("Reading " + b);
d = dataIn.readDouble();
System.out.println("Reading " + d);
}
catch(IOException exc) { System.out.println("Read error."); }
dataIn.close();
}
}
9.7. LUỒNG ĐỐI TƯỢNG
Trong quá trình lập trình, để lưu lại một đối tượng ra file ta có thể lưu lần
lượt từng thuộc tính của nó. Khi đọc lại đối tượng ta phải tạo đối tượng mới từ
các thuộc tính đã ghi. Cách làm này khá dài dòng và kém linh hoạt.
Java hỗ trợ đọc/ghi các đối tượng một cách đơn giản thông qua lớp
ObjectInputStream và ObjectOutputStream (được gọi là các luồng đối tượng).
Một đối tượng muốn có thể được đọc/ghi thì lớp đối tượng phải cài đặt giao diện
java.io.Serializable. Ví dụ 9.12 minh họa cách sử dụng luồng đọc ghi đối tượng.
Ví dụ 9.12. Đọc ghi đối tượng ra file:
// file Student.java
public class Student implements Serializable {
private String name;
295

private int age;


Student(String name, int age){
this.name = name;
this.age = age;
}
public String toString(){
String ret = "My name is " + name +
"\nI am " + age + " years old";
return ret;
}
}
// file WriteMyObject.java
import java.io.*;
public class WriteMyObject {
public static void main(String[] args) {
try {
FileOutputStream f = new FileOutputStream("student.dat");
ObjectOutputStream oStream = new ObjectOutputStream(f);
Student x = new Student("Bill Gates", 18);
oStream.writeObject(x);
oStream.close();
} catch (IOException e) {
System.out.println(“Error IO file”);
}
try {
FileInputStream g = new FileInputStream("student.dat");
ObjectInputStream inStream = new ObjectInputStream(g);
//Đọc đối tượng từ luồng
Student y = (Student) inStream.readObject();
System.out.println(y.toString());
inStream.close();
}
catch (ClassNotFoundException e) {
System.out.println(“Class not found”);
}
296

catch (IOException e) {
System.out.println(“Error IO file”);
}
}
}
Kết quả chạy chương trình:
My name is Bill Gates
I am 18 years old
9.8. LỚP File
Lớp File nằm trong gói java.io được sử dụng để truy cập các đối tượng
file và thư mục. Các file đặt tên theo qui ước đặt tên file của hệ điều hành trên
máy. Các qui ước này được gói riêng bằng các hằng lớp File. Lớp này cung cấp
các thiết lập các file và các thư mục. Các thiết lập chấp nhận các đường dẫn file
tuyệt đối lẫn tương đối cùng các file và thư mục. Tất cả các tác vụ thư mục và
file chung được thực hiện thông qua các phương thức truy cập của lớp File.
Các phương thức lớp File:
- Cho phép tạo, xoá, đổi tên các file.
- Cung cấp khả năng truy cập tên đường dẫn file.
- Xác định đối tượng có phải file hay thư mục không.
- Kiểm tra sự cho phép truy cập đọc và ghi.
Giống như các phương thức làm việc với file, các phương thức làm việc
với thư mục cũng cho phép tạo, xoá, đặt tên lại và liệt kê các thư mục.
Lớp File không phục vụ cho việc nhập/xuất dữ liệu trên luồng. Lớp File
thường được dùng để biết được các thông tin chi tiết về file cũng như thư mục
(tên, ngày giờ tạo, kích thước, …)
Các constructor của lớp File:
- Tạo đối tượng File từ đường dẫn tuyệt đối:
public File(String pathname);
Ví dụ: File f = new File(“C:\\Java\\vd1.java”);
- Tạo đối tượng File từ tên đường dẫn và tên file tách biệt:
public File(String parent, String child);
Ví dụ: File f = new File(“C:\\Java”, “vd1.java”);
- Tạo đối tượng File từ một đối tượng File khác:
public File(File parent, String child);
Ví dụ: File dir = new File (“C:\\Java”);
297

File f = new File(dir, “vd1.java”);


Bảng 9.7 liệt kê một số phương thức thường gặp của lớp File.
Bảng 9.7. Một số phương thức của lớp File
STT Phương thức và mô tả
public String getName();
1
Lấy tên của đối tượng File;
public String getPath();
2
Lấy đường dẫn của file.
public boolean isDirectory();
3
Kiểm tra xem file có phải là thư mục không.
public boolean isFile();
4 Kiểm tra xem đối tượng lớp File có thực sự là một file
không.
public String[] list();
5 Lấy danh sách tên các file và thư mục con của đối
tượng File đang xét và trả về trong một mảng.
Ví dụ 9.13. Liệt kê danh sách file và thư mục trong ổ đĩa C:
import java.awt.*;
import java.io.*;
public class FileDemo {
public static void main(String args[]) {
//Tạo giao diện đồ họa dựa trên Frame (xem Chương 10)
Frame fr = new Frame ("File Demo");
fr.setBounds(10, 10, 300, 200);
fr.setLayout(new BorderLayout());
Panel p = new Panel(new GridLayout(1,2));
List list_C = new List();
list_C.add("C:\\");
//Đọc tất cả các file, thư mục ở ổ C
File driver_C = new File ("C:\\");
String[] dirs_C = driver_C.list();
for (int i=0;i<dirs_C.length;i++){
//Đọc nội dung từng file, thư mục của ổ C
File f = new File ("C:\\" + dirs_C[i]);
//Nếu là thư mục
298

if (f.isDirectory())
list_C.add("<DIR>" + dirs_C[i]);
else //Nếu là file
list_C.add(" " + dirs_C[i]);
}
List list_D = new List();
//Đọc tương tự với ổ D
list_D.add("D:\\");
File driver_D = new File ("D:\\");
String[] dirs_D = driver_D.list();
for (int i=0;i<dirs_D.length;i++){
File f = new File ("D:\\" + dirs_D[i]);
if (f.isDirectory())
list_D.add("<DIR>" + dirs_D[i]);
else
list_D.add(" " + dirs_D[i]);
}
p.add(list_C);
p.add(list_D);
fr.add(p, BorderLayout.CENTER);
fr.setVisible(true);
}
}
9.9. LỚP Scanner
Lớp Scanner cho phép chúng ta nhập dữ liệu từ bàn phím một cách thuận
tiện khi các biến được đọc trực tiếp mà không cần phải chuyển đổi kiểu dữ liệu.
Scanner chỉ được hỗ trợ từ phiên bản Java 1.5 và nằm trong gói java.util.
Scanner tách dòng nhập thành các xâu riêng biệt được phân cách với nhau
bởi các xâu kí tự tách (mặc định là khoảng trắng). Các giá trị này sẽ được nạp
vào các biến tương ứng bằng các phương thức khác nhau của lớp Scanner.
Ví dụ, đoạn mã sau cho phép đọc vào số nguyên từ System.in:
Scanner sc = new Scanner(System.in);
int i = sc.nextInt();
Dòng lệnh int i = sc.nextInt() sẽ chờ cho tới khi người sử dụng nhập giá
trị và nhấn phím Enter. Nếu như người sử dụng nhập sai kiểu dữ liệu thì Java sẽ
299

báo lỗi runtime.


Tương tự, để đọc các kiểu dữ liệu khác, Java cung cấp các phương thức
nextXXX() Với XXX là các kiểu dữ liệu tương ứng mà người dùng muốn đọc từ
bàn phím.
Trong ví dụ sau, đoạn mã cho phép đọc dữ liệu kiểu long vào biến từ file
myNumbers:
Scanner sc = new Scanner(new File("myNumbers"));
while (sc.hasNextLong()) {
long aLong = sc.nextLong();
}
Để nhập xâu ký tự ta có thể sử dụng phương thức next() hoặc nextLine().
Phương thức next() chỉ trả về nội dung trước khoảng trắng còn phương thức
nextLine() sẽ trả về nội dung của một hàng. Ví dụ 9.14 cho phép nhập nhiều kiểu
dữ liệu từ bàn phím.
Ví dụ 9.14. Nhập dữ liệu từ bàn phím với lớp Scanner:
import java.io.IOException;
import java.util.*; //thu vien dung lop Scanner
class Java_in_out_put {
public static void main(String agrv[]) throws IOException {
String s;
int age;
double M;
//tao doi tuong inp thuoc lop Scanner
Scanner inp = new Scanner(System.in);
System.out.print("Insert your name: "); //Lenh in ra man
hinh
s = inp.nextLine(); //nhap xâu ký tự
System.out.print("Insert your age: ");
age = inp.nextInt(); //nhap so nguyen
System.out.println("Insert your Math: ");
M = inp.nextDouble();
System.out.printf("My name is %s, I %d years old and I am
%.2f math score\n", s, age, M);
inp.close();
}
300

}
Đối tượng Scanner cũng có phép thay thế đoạn xâu kí tự tách mặc định
bằng các xâu tự định nghĩa bằng phương thức useDelimiter(). Đoạn mã sau cho
phép đọc nhiều kiểu dữ liệu khác nhau từ dòng nhập, với xâu kí tự tách tự định
nghĩa:
String input = "1 fish 2 fish red fish blue fish";
//Định nghĩa xâu kí tự tách mới
Scanner s = new Scanner(input).useDelimiter("\\s*fish\\s*");
//Đọc các thành phần của s:
System.out.println(s.nextInt());
System.out.println(s.nextInt());
System.out.println(s.next());
System.out.println(s.next());
s.close();
Kết quả chạy của đoạn mã trên sẽ là:
1
2
red
blue
Cũng có thể sử dụng biểu thức chính quy (regular expression) để đọc từ
bàn phím các biến giống như đoạn mã trên:
String input = "1 fish 2 fish red fish blue fish";
Scanner s = new Scanner(input);
//Dùng biểu thức chính quy
s.findInLine("(\\d+) fish (\\d+) fish (\\w+) fish (\\w+)");
MatchResult result = s.match();
for (int i=1; i<=result.groupCount(); i++)
System.out.println(result.group(i));
s.close();
Đoạn xâu kí tự tách mặc định có thể kiểm tra thông qua thuộc tính
Character.isWhitespace. Sau khi thay đổi xâu kí tự tách, nếu muốn sử dụng lại
xâu kí tự tách mặc định, ta có thể gọi phương thức reset().
Có một số phương thức khác hữu ích trong lớp Scanner như skip() để
nhảy qua đầu vào, findInLine() để tìm kiếm các xâu ký tự con.
Chú ý rằng, khi dùng các phương thức nextXXX() đọc dữ liệu, nếu xâu ký
301

tự đầu vào không thể chuyển được như kiểu XXX tương ứng, thì lỗi ngoại lệ
InputMismatchException xuất hiện. Khi đó, Scanner sẽ không bỏ qua giá trị xâu
đầu vào gây ra ngoại lệ. Xâu này có thể được người dùng xử lý hoặc bỏ qua bởi
phương thức skip().
Scanner mặc định hiểu các giá trị số đọc vào là ở dạng thập phân. Để đọc
dữ liệu ở các dạng có số khác, có thể sử dụng phương thức useRadix(int radix).
Phương thức reset() sẽ khôi phục mặc định sau khi sử dụng phương thức
useRadix(int).
TỔNG KẾT CHƯƠNG
Trong chương này chúng ta đã tìm hiểu các khái niệm căn bản về vào ra
bằng cách sử dụng các luồng. Chương này cũng giới thiệu các luồng hướng byte
và các luồng hướng ký tự trong nhập xuất file, nhập xuất bàn phím, màn hình.
Khái niệm vào ra mới bằng cách sử dụng vùng đệm (buffer), sử dụng luồng nhị
phân, hay luồng đối tượng cũng được giới thiệu trong chương này. Các vấn đề
xử lý file truy cập ngẫu nhiên dùng lớp RandomAccessFile, xử lý file và thư
mục dùng lớp File giúp người dùng có nhiều kỹ thuật phong phú để vào/ra dữ
liệu file. Phần cuối chương giới thiệu lớp Scanner, công cụ thuận tiện để đọc dữ
liệu từ bàn phím.
Cụ thể, chúng ta đã tìm hiểu:
- Luồng là một lộ trình mà dữ liệu sẽ di chuyển giữa chương trình và các
thiết bị nhập xuất, Java dùng để nhập/xuất dữ liệu.
- Luồng nhập/xuất bao gồm các lớp:
+ Lớp System.out: Luồng xuất chuẩn, mặc định là màn hình.
+ Lớp System.in: Luồng nhập chuẩn, mặc định là bàn phím.
+ Lớp System.err: Luồng lỗi chuẩn, mặc định là màn hình.
- Luồng byte hỗ trợ việc xuất nhập dữ liệu trên byte, được định nghĩa bởi
các lớp trừu tượng InputStream (định nghĩa cách nhận dữ liệu trên byte) và lớp
trừu tượng OutputStream (định nghĩa cách xử lý của các luồng xuất byte).
- Lớp ByteArrayInputStream tạo ra một luồng nhập từ bộ nhớ đệm; lớp
ByteArrayOutputStream tạo một luồng xuất trên một mảng byte.
- Các lớp Reader và Writer là lớp trừu tượng hỗ trợ đọc và ghi các luồng
ký tự Unicode. Những lớp dẫn xuất từ Reader và Writer giúp thao tác trên các
luồng ký tự Unicode.
- Java sử dụng cơ chế nhập/xuất có vùng đệm giúp các chương trình
đọc/ghi các lượng dữ liệu nhỏ mà không tác động ngược lên khả năng thực hiện
302

của hệ thống. Hai lớp giúp thao tác nhập/xuất sử dụng vùng đệm là
BufferedInputStream và BufferedOutputStream.
- Các lớp DataInputStream và DataOutputStream được sử dụng để đọc và
ghi những giá trị nhị phân của các kiểu dữ liệu trong Java.
- Java hỗ trợ việc nhập/xuất file với các lớp File, FileInputStream và
FileOutputStream. Hai lớp thường dùng cho việc đọc/ghi dữ liệu ký tự với file
là FileReader và FileWriter.
- Lớp RandomAccessFile cung cấp khả năng thực hiện nhập/xuất tới vị trí
ngẫu nhiên trong một file.
- Lớp Scanner cho phép nhập dữ liệu từ bàn phím một cách thuận tiện khi
các biến được đọc trực tiếp mà không cần phải chuyển đổi kiểu dữ liệu.
BÀI TẬP
1. Viết chương trình nhận một dòng văn bản từ người dùng và hiển thị
dòng văn bản đó lên màn hình.
2. Viết chương trình mycopy với hai biến string là filename1, filename2
có chức năng như sau:
- Nếu filename1 và filename2 là tên hai file thì chương trình copy nội
dung của filename1 sang filename2.
- Nếu filename2 là thư mục thì copy filename1 sang thư mục filename2.
- Nếu filename1 có tên là “con” thì cho phép tạo file filename2 với nội
dung gõ từ bàn phím.
3. Viết chương trình mydir có chức năng liệt kê hết các file có tên là
filename nhập từ bàn phím (phần mở rộng file là bất kỳ).
4. Viết chương trình quản lý một danh sách thí sinh:
- Chương trình cho phép thêm thí sinh, tìm kiếm, cập nhật...
- Khi bắt đầu, chương trình sẽ lấy dữ liệu từ file thisinh.dat.
- Khi kết thúc, chương trình ghi lại danh sách thí sinh vào file. Có thể
dùng RandomAccessFile hoặc dùng ObjectOutputStream và cài đặt Serializable.

Câu hỏi ôn tập


1. Chọn phát biểu đúng:
a. InputStream và OuputStream là hai luồng dữ liệu kiểu byte.
b. Reader và Writer là hai luồng dữ liệu kiểu kí tự.
303

c. Câu a) và b) đúng.
d. Tất cả các câu trên đều sai
2. Cho biết số byte mà chương trình sau ghi ra file temp.txt
import java.io.*;
public class TestlOApp {
public static void main(String argsQ) throws IOException {
FileOutputStream outStream = new
FileOutputStream("test.txt");
String s = "test";
for(int i=0;i<s.length();++i)
outStream.write(s.charAt(i));
outStream.close();
}
}
a. 2 bytes b. 4 bytes
c. 8 bytes d. 16 bytes
3. Chọn phát biểu đúng:
a. Một đối tượng của lớp File có thể được dùng để truy cập các file trong
thư mục hiện hành.
b. Khi một đối tượng của lớp File được tạo ra thì một file tương ứng cũng
được tạo ra trên đĩa.
c. Các đối tượng của lớp File được dùng để truy cập đến các file và thư
mục trên đĩa.
d. Câu a) và c) đúng.
4. Cho biết cách tạo một đối tượng của InputStreamReader từ một đối
tượng của InputStream:
a. Sử dụng phương thức createReader() của lớp InputStream.
b. Sử dụng phương thức createReader() của lớp InputStreamReader.
c. Tạo một đối tượng của InputStream rồi truyền vào cho hàm khởi tạo
của InputStreamReader.
d. Tất cả các câu trên đều sai.

5. Chọn phát biểu đúng:


a. Lớp Writer có thể được dùng để ghi các ký tự có cách mã hóa khác
nhau ra luồng xuất.
304

b. Lớp Writer có thể được dùng để ghi các ký tự Unicode ra luồng xuất.
c. Lớp Writer có thể được dùng để ghi giá trị các kiểu dữ liệu cơ sở ra
luồng xuất.
d. Câu a) và b) đúng.
305

Chương 10
LẬP TRÌNH GIAO DIỆN ĐỒ HỌA
10.1. GIỚI THIỆU
Các phần mềm ứng dụng hiện nay ngày càng thân thiện và dễ sử dụng với
người dùng là nhờ có sự hỗ trợ của giao diện đồ họa trực quan, dễ thao tác. Các
ngôn ngữ lập trình hướng đối tượng như Java, C# hay Visual Basic đều cung cấp
thư viện các đối tượng đồ họa rất phong phú, cho phép xây dựng lên các giao
diện đồ họa hợp lý, phù hợp với chức năng ứng dụng và yêu cầu của người
dùng. Chương này giới thiệu những vấn đề liên quan đến xây dựng giao diện đồ
họa người dùng (Graphical User Interface - GUI) với ngôn ngữ lập trình Java.
Thư viện GUI cung cấp các lớp giúp lập trình viên tạo ra các thành phần
hiển thị giao diện và các phương thức để thay đổi các thuộc tính hiển thị của các
thành phần giao diện đó, các thành phần kết hợp với nhau tạo lên giao diện ứng
dụng như mong muốn. Thư viện GUI cơ bản của Java được định nghĩa trong hai
gói java.awt (thư viện AWT) và javax.swing (thư viện Swing). Ở những phiên
bản Java 1.2 trở về trước chỉ có AWT, thư viện Swing được đưa ra từ phiên bản
Java 1.2 là một sự phát triển bổ sung cùng thay thế một số thành phần GUI trong
AWT, Swing đem lại khả năng hiển thị tốt hơn và độc lập thay vì hiển thị phụ
thuộc vào nền tảng như AWT. Gói java.awt còn chứa các lớp và giao diện giúp
lập trình viên xử lý các sự kiện (event) tương tác giữa người sử dụng và các
thành phần giao diện của ứng dụng (các lớp và giao diện này nằm trong gói con
java.awt.event của gói java.awt).
Nguyên lý và cách thức xây dựng giao diện với hai thư viện AWT và
Swing là tương đồng, vì vậy chương này sẽ tập trung vào giới thiệu cách thức
tạo giao diện với AWT, với Swing cách làm tương tự.
10.2. XÂY DỰNG GIAO DIỆN GUI VỚI THƯ VIỆN AWT
AWT là viết tắt của Abstract Windowing Toolkit, đây là tập hợp các lớp
cho phép tạo ra một giao diện GUI. Một GUI thông thường là sự kết hợp từ các
thành phần (component) GUI cơ bản như: nút nhấn (button), hộp văn bản (text
field, vùng văn bản (text area), hộp chứa danh sách các xâu ký tự (list box), nhãn
văn bản (label),... Các thành phần này là đối tượng của các lớp được định nghĩa
trong thư viện AWT như Button, TextField, TextArea, ListBost, Label, để tạo ra
các thành phần ta chỉ cần tạo ra các đối tượng của lớp tương ứng và kết hợp
chúng để tạo lên giao diện, khi chạy chương trình đó chính là những thành phần
nhìn thấy được trên màn hình và người dùng có thể tương tác với các thành phần
306

này thông qua chuột hay bàn phím.


Các lớp thành phần giao diện đồ họa của thư viện AWT được định nghĩa
trong gói java.awt. Trên Hình 10.1 là sơ đồ kế thừa các lớp GUI của AWT, trên
cùng là lớp Object, các lớp GUI đều thừa kế từ lớp Component.

Hình 10.1. Các lớp giao diện GUI cơ bản của thư viện AWT

Các lớp thành phần GUI cơ bản của thư viện AWT trên Hình 10.1 gồm
có:
- Component: là lớp đại diện cho tất cả các thành phần có khả năng hiển
thị trên màn hình, đóng vai trò tạo lên giao diện GUI của chương trình. Lớp
Component là lớp cơ sở của các lớp thành phần GUI như: Frame, Window,
Button, Label,... vì vậy các thành viên của lớp Component là những thành viên
mà tất cả các lớp con GUI đều có.
- Container: là lớp đại diện các thành phần GUI có khả năng chứa các
thành phần GUI khác. Các thành phần này giúp lập trình viên phân vùng, chia
nhỏ giao diện chính của chương trình thành các vùng khác nhau để đặt các thành
phần GUI cơ bản lên trên các vùng đó nhằm tạo ra các giao diện phức tạp. Lớp
Container kế thừa từ Component và là lớp cơ sở cho các lớp đóng vai trò vật
chứa. Các lớp vật chứa thường được sử dụng là: Frame, Panel, Window…Mỗi
307

một giao diện GUI khi xây dựng luôn cần ít nhất một vật chứa đại diện cho
khung hiển thị của chương trình.
- Các lớp thành phần GUI cơ bản: Là các lớp thành phần GUI hiển thị
trực tiếp tạo lên giao diện GUI của chương trình như: Button, Label, TextField,
List,... Những thành phần này không thể hiển thị độc lập mà chúng cần đặt lên
một vật chứa theo một cách thức bố cục (layout) nhất định. Thông thường chúng
là những thành phần đón nhận tương tác và các sự kiện từ người sử dụng. Các
thành phần này còn được gọi là các controls (các thành phần điều khiển).
- LayoutManager: là giao diện đại diện cho cách thức sắp xếp, bố cục các
thành phần GUI cơ bản lên trên các vật chứa theo quy tắc nhất định. Giao diện
này được hiện thực bởi các lớp layout cụ thể.
- AWTEvent: là lớp cơ sở của tất cả các lớp mô tả sự kiện. Ngoài lớp này,
Java còn cung cấp mô hình và thư viện chứa các giao diện giúp lập trình viên xử
lý các sự kiện (event) tương tác giữa người sử dụng và chương trình. Các giao
diện này nằm trong gói java.awt.event. Chi tiết về xử lý sự kiện sẽ được giới
thiệu ở những phần sau.
Trên hình 10.2 là ví dụ về giao diện chương trình được xây dựng bằng thư
viện AWT.

Hình 10.2. Một giao diện được tổ hợp từ các Controls cơ bản
Hình 10.2 cho thấy một giao diện đồ họa và các thành phần GUI cơ bản
để tạo nên giao diện đó, các GUI cơ bản được sử dụng ở đây là Menu, Canvas,
Label, TextField, Button, CheckBox, ListBoxt,...
Để xây dựng một ứng dụng GUI, cần thực hiện theo các bước sau:
- Lựa chọn một vật chứa (container);
- Tạo các thành phần GUI cơ bản (controls);
308

- Đưa các controls vào vùng chứa;


- Sắp xếp các controls trong vùng chứa;
- Thêm các xử lý sự kiện trên các controls.
Chúng ta tìm hiểu cách thức xây dựng giao diện GUI thông qua một ví dụ
đơn giản trên Hình 10.3.

Hình 10.3. Ví dụ giao diện ứng dụng đơn giản


Hình 10.3 thể hiện một giao diện một chương trình đơn giản. Giao diện
chương trình gồm có một nội dung hiển thị (label), một ô nhập nội dung (text
field) và ba nút bấm (button). Để thể hiện các thành phần hiển thị này, chúng ta
sử dụng các lớp thành phần GUI tương ứng: Label, TextField, Button. Nguyên
tắc xây dựng giao diện của Java không cho phép các thành phần GUI cơ bản
hiển thị độc lập, vì vậy cần lựa chọn các vật chứa phù hợp để sử dụng. Ở ví dụ
này ta có thể sử dụng vật chứa Frame đại diện cho khung chương trình với thanh
tiêu đề và các nút chức năng cơ bản, cùng với vật chứa Panel để chia nhỏ tổ hợp
các thành phần GUI. Cấu trúc giao diện được thể hiện như hình 10.4.

Hình 10.4. Cấu trúc các thành phần tổ hợp giao diện
Ví dụ 10.1 thể hiện chương trình với giao diện hình 10.3.
Ví dụ 10.1. Chương trình với giao diện GUI đơn giản:
1 import java.awt.*;
2
309

3 public class GUI_Sample {


4
5 public static void main(String[] args) {
6
7 Frame frame1 = new Frame();
8 Panel panel1 = new Panel();
9 Panel panel2 = new Panel();
10
11 Label label1 = new Label("label1");
12 TextField textfield1 = new TextField("textfield1");
13 panel1.add(label1);
14 panel1.add(textfield1);
15
16 Button button1 = new Button("button1");
17 Button button2 = new Button("button2");
18 Button button3 = new Button("button3");
19 panel2.add(button1);
20 panel2.add(button2);
21 panel2.add(button3);
22
23 frame1.setLayout(new FlowLayout());
24 frame1.setTitle("AWT Sample");
25 frame1.add(panel1);
26 frame1.add(panel2);
27 frame1.setSize(300, 150);
28 frame1.setBackground(Color.LIGHT_GRAY);
29 frame1.setVisible(true);
30 }
31 }

Dòng lệnh 1 của chương trình là câu lệnh import để khai báo các thư viện
được sử dụng trong chương trình. Ở đây ta khai báo sử dụng các thư viện
java.awt.* để sử dụng các lớp giao diện. Dòng lệnh 7, 8, 9 tạo ba đối tượng
trong đó một đối tượng của lớp Frame và hai đối tượng của lớp Panel để tạo ra
các vật chứa. Vật chứa Frame đại diện cho khung chứa ngoài cùng của giao
diện, hai đối tượng Panel là hai vật chứa con chia tách vật chứa Frame thành hai
vùng riêng biệt. Tiếp theo là các dòng lệnh tạo ra các thành phần GUI như Label
(dòng 11), TextField (dòng 12), Button (dòng 16, 17, 18). Các dòng lệnh 13, 14,
19, 20, 21 thực hiện đưa các thành phần GUI lên các vùng chưa tương ứng với
hai Panel và dòng lệnh 25, 26 thực hiện đưa hai Panel lên trên vật chứa chính
310

Frame. Sắp xếp, bố cục các thành phần lên trên Frame được thực hiện bởi dòng
lệnh 23, ở đây thiết lập layout cho Frame là FlowLayout, như vậy panel1,
panel2 được đặt lên frame1 theo nguyên tắc sắp xếp của FlowLayout (nội dung
về các layout sẽ được giới thiệu ở phần sau). Cuối cùng là việc thiết lập một số
thuộc tính hiển thị cho Frame được thực hiện ở dòng lệnh 24, 27, 28, 29, ở đây
chúng ta đã thiếp lập tiêu đề, kích thước, màu nền và cài đặt thuộc tính hiển thị
bằng true cho giao diện chương trình. Chạy chương trình sẽ nhận được kết quả
như trên Hình 10.3.
Thông thường một chương trình gồm nhiều giao diện (nhiều cửa sổ), để
tiện cho việc khởi tạo và chạy các giao diện cửa sổ ở nhiều nơi khác nhau trong
chương trình, khi xây dựng một chương trình GUI chúng ta thường sử dụng mô
hình xây dựng một lớp kế thừa trực tiếp từ một lớp vật chứa, lớp nhận được sẽ
đóng vai trò là một lớp cửa sổ và cho phép tái sử dụng để tạo nhiều đối tượng
cửa sổ dựa trên lớp này trong chương trình. Chương trình bên trên được viết lại
trong Ví dụ 10.2.
Ví dụ 10.2. Tạo lớp GUI mới:
1 import java.awt.*;
2
3 public class GUI_Sample extends Frame {
4
5 public GUI_Sample(String title) {
6
7 Panel panel1 = new Panel();
8 Panel panel2 = new Panel();
9
10 Label label1 = new Label("label1");
11 TextField textfield1 = new TextField("textfield1");
12 panel1.add(label1);
13 panel1.add(textfield1);
14
15 Button button1 = new Button("button1");
16 Button button2 = new Button("button2");
17 Button button3 = new Button("button3");
18 panel2.add(button1);
19 panel2.add(button2);
20 panel2.add(button3);
21
22 this.setLayout(new FlowLayout());
23 this.setTitle("AWT Sample");
311

24 this.add(panel1);
25 this.add(panel2);
26 this.setSize(300, 150);
27 this.setBackground(Color.LIGHT_GRAY);
28 }
29

30 public static void main(String[] args) {

31 GUI_Sample sample = new GUI_Sample("AWT Sample");


32 sample.setVisible(true);
33 }
34
35 }
Trong chương trình Ví dụ 10.2, chúng ta đã tạo ra lớp GUI_Sample kế
thừa trực tiếp từ lớp Frame, vì vậy bản thân nó cũng chính là một vật chứa mang
đầy đủ các thuộc tính và phương thức của Frame. Khi một chương trình chứa
nhiều file mã nguồn và nhiều giao diện khác nhau, ưu điểm của mô hình này
giúp ta có thể điều khiển việc khởi tạo và chạy giao diện thích hợp ở bất kì nơi
nào của chương trình theo luồng chức năng của chương trình.Việc khởi tạo giao
diện được thực hiện bằng việc khởi tạo đối tượng tương tự trong phương thức
main() bên trên, ví dụ:
Gui_Sample sample = new GUI_Sample(“AWT Sample”);
Với câu lệnh này, chương trình sẽ khởi tạo đối tượng giao diện sample và
khởi tạo các thành phần giao diện thông qua việc gọi contructor (thực thi các câu
lệnh trong contructor), sau đó có thể hiển thị sample bằng phương thức
setVisible() với tham số true:
sample.setVisible(true);
Với cách xây dựng giao diện này, chúng ta có thể tạo chương trình GUI
có nhiều cửa sổ hiển thị (bằng cách tạo nhiều đối tượng lớp GUI_Sample trong
phương thức main), các cửa sổ này có cấu trúc giống nhau. Muốn tạo chương
trình GUI có nhiều cửa sổ với các cấu trúc khác nhau, chúng ta chỉ cần tạo nhiều
lớp vật chứa giống như GUI_Sample với cách bố trí các thành phần giao diện
khác nhau, sau đó tạo các đối tượng của các lớp này trong phương thức main()
của chương trình. Cách tạo giao diện như trong Ví dụ 10.1 không tiện cho việc
tạo lớp cửa sổ có thể tái sử dụng cũng như không thích hợp cho việc xây dựng
chương trình có nhiều cửa sổ.
Tuy nhiên cách tạo giao diện trong Ví dụ 10.2 cũng còn chỗ hạn chế. Việc
312

khởi tạo các thành phần GUI ngay trong contructor khiến lập trình viên khó
khăn trong việc kiểm soát các thành phần GUI đã được tạo ra (panel1, panel2,
button1…), đồng thời cũng hạn chế phạm vi truy cập của các thành phần giao
diện đó (những thành phần này chỉ truy cập được trong contructor).
Để loại bỏ những hạn chế này, các thành phần GUI của chương trình
thường được khai báo thành các thuộc tính của lớp vật chứa.
Theo cách này, lớp GUI_Sample trong Ví dụ 10.2 có thể được viết lại như
sau:

//Khai báo các thành phần GUI dưới dạng thuộc tính của GUI_Sample
private Panel panel1, panel2;
private Label label1;
private TextField textfield1;
private Button button1, button2, button3;
//Trong constructor của GUI_Sample, không cần khai báo các
//thành phần GUI
public GUI_Sample(String title) {
panel1 = new Panel();
panel2 = new Panel();
label1 = new Label("label1");
textfield1 = new TextField("textfield1");
panel1.add(label1);
panel1.add(textfield1);
button1 = new Button("button1");
button2 = new Button("button2");
button3 = new Button("button3");
panel2.add(button1);
panel2.add(button2);
panel2.add(button3);
}

Với cách định nghĩa lớp giao diện như trên, phần khai báo các thành phần
GUI tách biệt với việc sử dụng, cách này giúp chúng ta dễ dàng thay đổi, chỉnh
sửa và khiến mã nguồn chương trình tường minh hơn. Trong những nội dung
tiếp theo của chương, chúng ta sẽ sử dụng cách thức xây dựng giao diện này.
Cách cách xây dựng giao diện đã nêu ở trên mới chỉ giúp tạo cửa sổ đồ
họa và hiển thị trên màn hình máy tính. Còn việc cửa sổ đó tương tác ra sao với
người dùng thì chưa rõ. Để giúp chương trình giao diện đồ họa tương tác với
người dùng theo cách mong muốn, cần xử lý các thao tác (hay còn gọi là sự kiện
313

- event) của người sử dụng với chương trình.


10.3. LẬP TRÌNH XỬ LÝ SỰ KIỆN
Sự kiện (event) phát sinh khi một thành phần giao diện thay đổi trạng thái,
hay có thể hiểu sự kiện là kết quả của việc tương tác giữa người dùng và các
thành phần giao diện. Ví dụ: khi kích chuột vào nút (button), di chuyển chuột
trên giao diện GUI, khi nhập dữ liệu vào ô văn bản (text field), hay lựa chọn một
phần tử trong hộp danh sách (list box)…
Xử lý sự kiện chính là việc viết code xử lý cho các sự kiện phát sinh. Java
cung cấp cho chúng ta cơ chế phát sinh sự kiện và xử lý sự kiện với các giao
diện (interfaces) tương ứng từng sự kiện, cơ chế này giúp lập trình viên điều
khiển, lựa chọn các thao tác sẽ thực thi khi các sự kiện được phát sinh.
10.3.1. Ví dụ xử lý sự kiện
Để tìm hiểu về lập trình sự kiện, trước hết ta tìm hiểu thông qua ví dụ một
chương trình đơn giản. Chương trình với giao diện gồm có hai nút bấm được
gắn nhãn là OK và CANCEL như hình minh họa 10.5, khi ta bấm vào nút bấm
OK, dòng tiêu đề của giao diện chuyển thành “You clicked OK” đồng thời màu
nền của giao diện chuyển sang màu xanh, khi ta bấm vào nút bấm CANCEL,
dòng tiêu đề của giao diện sẽ chuyển thành “You clicked CANCEL” đồng thời
màu nền của giao diện chuyển sang màu đỏ.

Hình 10.5. Giao diện khi chạy chương trình

Hình 10.6. Giao diện khi bấm button OK và CANCEL


Giao diện chương trình được xây dựng như cách đã đề cập ở mục 10.2 ở
314

trên, ở ví dụ này cần xây dựng hai nút bấm, việc khai báo và khởi tạo nút bấm
được thực hiện như sau:
import java.awt.Button;
private Button btn_ok, btn_cancel;
btn_ok = new Button(“OK”);
btn_cancel = new Button(“CANCEL”);
...
Khung giao diện chương trình được thể hiện bằng một vật chứa là đối
tượng lớp Frame. Ta có thể đưa các button trực tiếp lên vật chứa hoặc tạo ra một
vật chứa con lớp Panel đại diện cho phần nội dung của chương trình và đưa các
button lên panel:
import java.awt.Panel;
...
private Panel panel;
...
panel = new Panel();
panel.add(btn_ok);
panel.add(btn_cancel);
...
frame.add(panel);
...
Sau khi đã xây dựng giao diện hiển thị, bước tiếp theo cần thực hiện là lập
trình xử lý sự kiện như yêu cầu đề ra. Sự kiện được đề cập đến ở đây là sự kiện
bấm nút bấm (push button). Khi button được bấm, một sự kiện (push button)
phát sinh và button là nguồn phát sinh sự kiện (event source). Java cung cấp cơ
chế giúp lập trình viên viết các thao tác xử lý sự kiện khi các sự kiện tương ứng
phát sinh, việc này được thực hiện thông qua đối tượng lắng nghe sự kiện (event
listener).
Đối tượng lắng nghe sự kiện là đối tượng của lớp thực thi giao diện
(listener interface) tương ứng với loại sự kiện đã được định nghĩa trước trong
Java. Listener interface thường là các giao diện định nghĩa trước trong gói thư
viện java.awt.event, gắn liền với mỗi sự kiện là một interface tương ứng và chứa
các phương thức xử lý sự kiện.
Nếu đối tượng lắng nghe sự kiện được gắn với nguồn phát sinh sự kiện thì
đối tượng đó sẽ theo dõi và lắng nghe trên nguồn đó, khi một sự kiện phát sinh
315

đối tượng này sẽ lập tức tìm đến phương thức tương ứng để thực hiện đoạn
chương trình phản hồi lại sự kiện đã xảy ra.
Trong ví dụ này, tương ứng với sự kiện push button chúng ta cần tạo ra
một đối tượng lắng nghe sự kiện của lớp thực thi giao diện ActionListener.
ActionListener là giao diện được định nghĩa trong thư viện java.awt.event, trong
giao diện này có định nghĩa phương thức actionPerformed(), đây là phương thức
sẽ được thực thi khi sự kiện push button phát sinh.

Đoạn chương trình cài đặt việc xử lý sự kiện như sau:


//Tạo lớp thực thi giao diện lắng nghe sự kiện push button
class ButtonHandler implements ActionListener{
public void actionPerformed(ActionEvent e){
//Đoạn chương trình được thực thi khi sự kiện phát sinh
}
}
...
//Tạo đối tượng lắng nghe sự kiện
ButtonHandler handler = new ButtonHandler();
//Gắn đối tượng lắng nghe sự kiện vào nguồn phát sinh sự kiện
btn_ok.addActionListener(handler);
btn_cancel.addActionListener(handler);
...
Chúng ta thấy rằng, ActionListener định nghĩa duy nhất một phương thức
actionPerformed() để xử lý sự kiện push button, phương thức này nhận một
tham số truyền vào là đối tượng của lớp ActionEvent, đây chính là đối tượng đại
diện cho nguồn phát sinh sự kiện, nguồn phát sinh sự kiện có thể đến từ một
hoặc nhiều đối tượng giao diện, thông qua đối tượng này ta có thể biết và truy
cập đến các thuộc tính của đối tượng phát sinh sự kiện. Trong trường hợp này,
để lấy về nội dung trên button được bấm, có thể sử dụng các cách sau:
String clickedButton = e.getActionCommand();
hoặc:
Button clickedButton = (Button)e.getSource();
String buttontext = clickedButton.getText();
Để thay đổi các thuộc tính hiển thị của giao diện chương trình, có thể làm
như sau:
316

frame.setTitle(“You clicked OK”);


frame.setBackGround(Color.BLUE);
Toàn bộ mã nguồn chương trình được thể hiện trong Ví dụ 10.3.
Ví dụ 10.3. Lập trình xử lý sự kiện nhấn nút trên giao diện GUI:
import java.awt.*;
import java.awt.event.*;
public class GUI_Event_Sample extends Frame {
private Panel panel;
private Button btn_ok, btn_cancel;
private ButtonHandler handler;
//Constructor
public GUI_Event_Sample(String title) {
panel = new Panel();
btn_ok = new Button("OK");
btn_cancel = new Button("CANCEL");
panel.add(btn_ok);
panel.add(btn_cancel);
handler = new ButtonHandler(this);
btn_ok.addActionListener(handler);
btn_cancel.addActionListener(handler);
this.setTitle(title);
this.setLayout(new FlowLayout());
this.add(panel);
this.setSize(300, 200);
this.setBackground(Color.LIGHT_GRAY);
}
class ButtonHandler implements ActionListener {
private Frame root;
public ButtonHandler(Frame root) {
this.root = root;
}
public void actionPerformed(ActionEvent e) {
String action_command = e.getActionCommand();
if (action_command.equals("OK")) {
root.setTitle("You clicked OK");
root.setBackground(Color.BLUE);
}
if (action_command.equals("CANCEL")) {
root.setTitle("You clicked CANCEL");
root.setBackground(Color.RED);
}
317

}
}
public static void main(String[] args) {
GUI_Event_Sample sample =
new GUI_Event_Sample("Gui Sample");
sample.setVisible(true);
}
}
Chương trình trong Ví dụ 10.3 có thể được viết đơn giản hơn. Thay vì xây
dựng một lớp riêng biệt để xử lý sự kiện như ở chương trình trên, có thể xây
dựng lớp giao diện GUI trực tiếp thực thi giao diện listener, khi đó lớp này vừa
đóng vai trò là giao diện đồng thời vừa đóng vai trò là lớp đối tượng lắng nghe
và xử lý sự kiện. Áp dụng vào ví dụ ở trên, xây dựng lớp GUI_Event_Sample
thực thi giao diện ActionListener và lớp này trực tiếp định nghĩa phương thức xử
lý sự kiện xảy ra khi bấm nút. Chương trình được viết lại như sau:
import java.awt.*;
import java.awt.event.*;
public class GUI_Event_Sample extends Frame implements
ActionListener {
private Panel panel;
private Button btn_ok, btn_cancel;

public GUI_Event_Sample(String title) {


panel = new Panel();
btn_ok = new Button("OK");
btn_cancel = new Button("CANCEL");
panel.add(btn_ok);
panel.add(btn_cancel);
btn_ok.addActionListener(this);
btn_cancel.addActionListener(this);
this.setTitle(title);
this.setLayout(new FlowLayout());
this.add(panel);
this.setSize(300, 200);
this.setBackground(Color.LIGHT_GRAY);
}

public static void main(String[] args) {


GUI_Event_Sample sample =
new GUI_Event_Sample("Gui Sample");
sample.setVisible(true);
318

}
@Override
public void actionPerformed(ActionEvent e) {
String action_command = e.getActionCommand();
if (action_command.equals("OK")) {
this.setTitle("You clicked OK");
this.setBackground(Color.BLUE);
}
if (action_command.equals("CANCEL")) {
this.setTitle("You clicked CANCEL");
this.setBackground(Color.RED);
}
}
}
Với cách viết chương trình như thế này, khi gắn xử lý sự kiện vào button,
cần sử dụng tham số truyền vào là this:
btn_ok.addActionListener(this);
btn_cancel.addActionListener(this);
lý do vì lớp hiện tại đóng vai trò làm lớp đối tượng xử lý sự kiện, phương thức
actionPerformed() là phương thức của lớp hiện tại. Ưu điểm của cách viết này là
đơn giản hơn đồng thời phương thức xử lý sự kiện có thể truy cập dễ dàng hơn
tới các thành phần giao diện khác trong lớp.
*Chú ý: Có thể nhiều cách viết chương trình xử lý sự kiện khác nhau, tuy
nhiên nguyên tắc xử lý là tương đồng.
10.3.2. Mô hình xử lý sự kiện
Trong mục 10.3.1, chúng ta vừa tìm hiểu một cách khái quát về xử lý sự
kiện thông qua một ví dụ đơn giản, tiếp theo chúng ta cùng tìm hiểu sâu về mô
hình xử lý sự kiện trong Java. Mô hình gồm ba thành phần chính:
- Event source: nguồn gây ra sự kiện, thường là các thành phần
(component) GUI trong chương trình. Sự kiện thường phát sinh khi người sử
dụng tương tác với chương trình hoặc có sự thay đổi trạng thái của các thành
phần GUI.
- Event object: đối tượng lưu thông tin về sự kiện đã xảy ra.
- Event listener: đối tượng sẽ nhận được thông tin khi có sự kiện xảy ra.
Theo mô hình này, sự kiện (event) được phát sinh khi người dùng tương
tác với thành phần GUI, ví dụ như di chuyển chuột, ấn nút, nhập dữ liệu văn
bản, chọn menu... Thông tin về sự kiện được lưu trong một đối tượng sự kiện
thuộc lớp con của lớp AWTEvent (gói java.awt.event), sự kiện được xử lý bằng
319

cách đặt đối tượng “lắng nghe sự kiện” trên các thành phần GUI (Hình 10.7).

Hình 10.7. Mô hình xử lý sự kiện


Khi một sự kiện phát sinh, sự kiện sẽ được thông báo đến đối tượng lắng
nghe Event Listener. Việc thông báo sự kiện xảy ra thực chất là việc gọi một
phương thức của Event Listener với đối số truyền vào là Event Object. Java
cung cấp sẵn thư viện phong phú các giao diện lắng nghe với các phương thức
xử lý tương ứng với từng sự kiện có thể phát sinh trên GUI. Các giao diện này
có đặc điểm chung là các giao diện con kế thừa từ giao diện EventListener. Đối
tượng lắng nghe chính là đối tượng được tạo ra từ các lớp thực thi giao diện và
ghi đè các phương thức được định nghĩa trong giao diện đó. Việc ghi đè các
phương thức thực chất chính là việc cài đặt các thao tác phản hồi cho sự kiện.
Như vậy, để cài đặt xử lý sự kiện cần thực hiện theo các bước sau:
- Xác định nguồn phát sinh sự kiện. Ví dụ: Button;
- Xác định sự kiện cần xử lý trên nguồn đó. Mỗi một nguồn có thể phát
sinh nhiều loại sự kiện khác nhau và chúng có thể sẽ được xử lý bởi các Listener
tương ứng khác nhau. Ở bước này ta cần lựa chọn sự kiện cần xử lý. Ví dụ: bấm
Button;
- Xác định đối tượng lắng nghe sự kiện. Xây dựng lớp đối tượng thực thi
giao diện Listener tương ứng đồng thời cài đặt các phương thức xử lý sự kiện.
Ví dụ: Với sự kiện bấm Button cần tạo lớp thực thi giao diện ActionListener và
ghi đè phương thức actionPerformed();
- Đăng kí đối tượng lắng nghe trên đối tượng nguồn sự kiện (bằng phương
thức addXXXListener() tương ứng).
Các lớp sự kiện định nghĩa trong gói java.awt.event (Hình 10.8).
320

Hình 10.8. Sơ đồ kế thừa các lớp sự kiện


Theo cây phả hệ hình 10.8, có một số lớp sự kiện như sau:
- Sự kiện cấp thấp dùng cho hầu hết các thành phần:
+ FocusEvent: đặt/chuyển focus (tiêu điểm chú ý).
+ InputEvent: sự kiện phím (KeyEvent) hoặc chuột (MouseEvent).
+ ContainerEvent: thêm hoặc xoá các thành phần (component).
+ WindowEvent: đóng, mở, di chuyển cửa sổ.
- Sự kiện cấp cao dùng cho một số thành phần đặc thù:
+ ActionEvent: sự kiện sinh ra từ các thành phần giao tiếp với người dùng
như nhấn một nút, chọn menu…
+ ItemEvent: lựa chọn một item trong danh sách.
+ TextEvent: thay đổi giá trị của hộp văn bản.
Trên Hình 10.9 là một số bộ lắng nghe sự kiện (Listener Interface).
321

Hình 10.9. Sơ đồ kế thừa của các bộ lắng nghe sự kiện


Trên Hình 10.9, vai trò của một số bộ lắng nghe như sau:
- ActionListener: giao diện lắng nghe ActionEvent.
- AdjustmentListener: giao diện lắng nghe AdjustmentEvent.
- ItemListener: Giao diện lắng nghe ItenEvent.
- WindowListener: giao diện lắng nghe WindowEvent.
- FocusListener: giao diện lắng nghe FocusEvent.
- KeyListener: giao diện lắng nghe KeyEvent.
- MouseListener: giao diện lắng nghe MouseEvent.
Bảng 10.1 thể hiện sự tương ứng đã được định nghĩa trong Java giữa
nguồn phát sinh sự kiện, đối tượng sự kiện phát sinh và thao tác của người sử
dụng với giao diện chương trình.
Bảng 10.1. Các nguồn phát sự kiện cùng đối tượng sự kiện
Event source Event Object Chú thích
Button ActionEvent Nhấn nút
Checkbox ItemEvent Chọn, bỏ chọn một item
Choice
Component ComponentEvent Ẩn, hiện, di chuyển
FocusEvent Được chọn
MouseEvent Tương tác chuột
322

Event source Event Object Chú thích


KeyEvent Tương tác bàn phím
Container ContainerEvent Thêm, bớt component
List ActonEvent Nhấp kép chuột một item
ItemEvent Chọn, bỏ chọn một item
MenuItem ActionEvent Chọn một menu item
Scrollbar AdjustmentEvent Di chuyển thanh cuộn
TextComponent TextEvent Thay đổi văn bản
TextField ActionEvent Kết thúc thay đổi văn bản
Window WindowEvent Thay đổi cửa sổ
Bảng 10.2 liệt kê các phương thức được định nghĩa trong các giao diện
Listener.
Bảng 10.2. Các giao diện lắng nghe và các phương thức xử lý
Event Class Listener Interface Listener Method
ActionEvent ActionListener actionPerformed()
AdjustmentEvent AdjustmentListener adjustmentValueChanged()
ComponentEvent ComponentListener componentHidden()
componentMoved()
componentResized()
componentShown()
ContainerEvent ContainerListener componentAdded()
componentRemoved()
FocusEvent FocusListener focusGained()
focusLost()
ItemEvent ItemListener itemStateChanged()
KeyEvent KeyListener keyPressed()
keyReleased()
keyTyped()
MouseEvent MouseListener mouseClicked()
mousePressed()
mouseReleased()
MouseMotionListener mouseDragged()
mouseMoved()
TextEvent TextListener textValueChanged()
323

Event Class Listener Interface Listener Method


WindowEvent WindowListener windowClosed()
windowActivated()
Các bảng và sơ đồ kế thừa bên trên cho ta hiểu rõ hơn về cấu trúc và các
thành phần liên quan đến việc xử lý sự kiện. Qua đó ta thấy được tương ứng với
những đối tượng GUI sẽ có khả năng phát sinh những sự kiện nào? Những sự
kiện đó được lắng nghe bởi đối tượng lớp thực thi giao diện nào? Phương thức
nào sẽ được thực hiện khi sự kiện phát sinh? Bước cuối cùng để hoàn tất việc xử
lý sự kiện cho các tương tác người dùng với chương trình GUI là đăng kí đối
tượng lắng nghe sự kiện lên nguồn phát sinh sự kiện. Bước này là không thể
thiếu và được thực hiện rất đơn giản, tuy nhiên rất dễ bị bỏ quên khi chúng ta
xây dựng chương trình.
Để đăng ký đối tượng nghe, ta sử dụng tên phương thức có cấu trúc như
sau:
add + loại sự kiện + Listener(đối tượng nghe sự kiện)
Ví dụ với nút Button:
addActionListener(actionListener);
Ví dụ với danh sách List:
addActionListener(actionListener);
hoặc:
addItemListener(itemListener);
10.4. CÁC THÀNH PHẦN GIAO DIỆN (COMPONENTS)
Các nội dung đã học chúng ta đã tìm hiểu đầy đủ nguyên lý và các bước
xây dựng hoàn chỉnh một chương trình GUI với Java. Trong phần 4 của chương
10 này, chúng ta sẽ tìm hiểu kĩ hơn về các thành phần tạo nên giao diện GUI.
Nội dung được chia thành 2 phần: các thành phần giao diện cơ bản (controls) và
vật chứa (containers).
10.4.1. Các thành phần giao diện cơ bản (Controls)
Các thành phần giao diện cơ bản là những thành phần trực tiếp hiển thị
tạo lên giao diện của chương trình. Ở các ví dụ trước, chúng ta đã làm quen với
đối tượng lớp Button (nút ấn) và một số thao tác xử lý sự kiện trên Button, ở
phần này chúng ta tiếp tục tìm hiểu một số các thành phần cơ bản được sử dụng
nhiều trong Java AWT.
Đặc điểm chung của tất cả các thành phần giao diện cơ bản là không có
khả năng hiển thị độc lập, chúng phải được đặt trên vật chứa và khi hiển thị sẽ
324

chiếm một không gian hình chữ nhật trên vật chứa. Kích thước chiếm có thể
được chỉ định thông qua thiết lập thuộc tính chiều rộng, chiều cao của đối tượng
hoặc căn cứ vào bố cục trình bày của vật chứa.
Vị trí của thành phần được xác định thông qua tọa độ góc trái bên trên của
thành phần hoặc qua cài đặt bố cục trình bày. Cách tạo và hiển thiện các controls
lên vật chứa cơ bản đều giống nhau, gồm các bước chính: tạo đối tượng lớp,
thiết lập các thuộc tính hiển thị, xác định vị trí hiển thị, cài đặt xử lý sự kiện và
đưa lên vật chứa.
Chúng ta sẽ lần lượt tìm hiểu các thành phần Label (nhãn), TextField (hộp
nhập dòng văn bản), TextArea (vùng văn bản), …
a) Label
Label (nhãn) là lớp đối tượng dùng để trình bày một dòng văn bản (xâu ký
tự) ra màn hình trên một dòng giống như Button, tuy nhiên Label không có hiệu
ứng bấm và khả năng phát sinh sự kiện ActionEvent. Đồng thời ta không thể
thay đổi nội dung hiển thị của Label trực tiếp trên giao diện.
Để tạo và thay đổi thuộc tính của Label, có thể sử dụng một số phương
thức cơ bản sau:
- Label(): Constructor tạo một nhãn mà không có nội dung hiển thị.
- Label(String s): Constructor tạo một nhãn với nội dung hiển thị ban đầu
được lấy từ xâu ký tự s.
- Label(String s, int align): Constructor tạo một nhãn với nội dung hiển
thị s và cách thức căn lề hiển thị nội dung align.
- setText(), getText() là hai phương thức giúp thiết lập và lấy về nội dung
hiển thị trên Label.
Ví dụ 10.4 thể hiện chương trình đơn giản hiển thị một Label với nội dung
“Ví dụ với Label” như Hình 10.10.

Hình 10.10. Ví dụ với Label


Ví dụ 10.4. Chương tình GUI với một thành phần lớp Label:
325

import java.awt.*;

public class GUI_Label_Sample extends Frame {


private Label label;
private Panel panel;
public GUI_Label_Sample(String title) {
this.setTitle(title);
panel = new Panel();
panel.setLayout(new FlowLayout());
label = new Label();
label.setText(“Ví dụ với Label”);
panel.add(label);
this.add(panel);
this.setSize(300, 200);
this.setLayout(new FlowLayout());
this.setBackground(Color.LIGHT_GRAY);
}
public static void main(String[] args) {
GUI_Label_Sample sample = new
GUI_Label_Sample(“GUI Sample”);
sample.setVisible(true);
}
}
b) TextField
TextField là lớp thành phần cho phép nhập và hiển thị dữ liệu từ bàn phím
trên một dòng. Cũng giống như Button, đối tượng TextField có khả năng phát
sinh sự kiện ActionEvent khi người sử dụng bấm phím Enter trên bàn phím,
đồng thời TextField còn phát sinh sự kiện TextEvent khi có sự thay đổi nội dung
nhập vào. Sử lý sự kiện cho TextField được thực hiện bởi hai giao diện:
ActionListener và TextListener.
Một số phương thức cơ bản của TextField:
- Các constructor: ví dụ TextField(), TextField(String s),…
- setEditable (boolean b): thiết lập chế độ nhập cho TextField. Khi giá trị
thiết lập là false, TextField sẽ đóng vai trò hiển thị tương tự như Label.
- setEchoChar(char c): Thiết lập kí tự hiển thị khi nhập dữ liệu vào
TextField. Được sử dụng khi muốn ẩn nội dung nhập vào TextField bằng một kí
tự nào đó. Ví dụ: khi nhập vào mật khẩu mà người dùng không muốn hiển thị
326

mật khẩu lên màn hình giao diện, thay vào đó hiển thị kí tự ‘*’.
- setColumns(int columns): Thiết lập số kí tự có thể hiển thị trên
TextField.
- setText(String s), getText(): Thiết lập và lấy về nội dung trên TextField.
Ví dụ 10.5 thể hiện chương trình với giao diện hiển thị một TextField và
hai Button. Chương trình có xử lý sự kiện ActionEvent khi bấm vào hai Button
hoặc nhập dữ liệu vào TextField và bấm Enter.
Ví dụ 10.5. Chương trình với TextField:
import java.awt.*;
import java.awt.event.*;

public class GUI_TextField_Sample extends Frame implements


ActionListener {

private Button btn_ok, btn_cancel;


private TextField inputLine;
private Panel panel;

public GUI_TextField_Sample(String title) {


this.setTitle(title);

btn_ok = new Button("OK");


btn_cancel = new Button("CANCEL");
inputLine = new TextField();
inputLine.setColumns(25);
panel = new Panel(new FlowLayout());

btn_ok.addActionListener(this);
btn_cancel.addActionListener(this);
inputLine.addActionListener(this);

panel.add(inputLine);
panel.add(btn_ok);
panel.add(btn_cancel);

this.add(panel);
this.setSize(250, 200);
this.setBackground(Color.LIGHT_GRAY);
327

@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() instanceof Button) {
Button clickedButton = (Button) e.getSource();
String buttonText = clickedButton.getLabel();
this.setTitle("You clicked " + buttonText);
} else {
this.setTitle("You entered '" + inputLine.getText() +
"'");
}
}

public static void main(String[] args) {


GUI_TextField_Sample sample = new
GUI_TextField_Sample("GUI Sample");
sample.setVisible(true);
}
}

Hình 10.11. Ví dụ giao diện với TextField


Chạy chương trình Ví dụ 10.5 sẽ nhận được kết quả giao diện như trên
Hình 10.11. Nhập dữ liệu vào thành phần TextField và bấm các nút bấm sẽ thấy
được sự xử lý sự kiện của chương trình.
Điểm chú ý ở ví dụ này là sự kiện ActionEvent có thể phát sinh từ một
trong hai Button hoặc TextField. Chương trình sử dụng phương thức
e.getSource() để lấy về nguồn phát sinh sự kiện sau đó kiểm tra để đưa ra các xử
lý khác nhau với từng nguồn sự kiện khác nhau.
c) TextArea
TextArea cùng với Label, TextField thuộc nhóm các Text Component,
328

chuyên để làm việc với dữ liệu văn bản trên giao diện. Là thành phần để nhập dữ
liệu như TextField, tuy nhiên TextArea cho phép người dùng nhập nhiều dòng
dữ liệu văn bản. TextArea cũng có thể giống như một Label cho phép hiển thị
nhiều dòng khi ta thiết lập thuộc tính không cho phép sửa nội dung TextArea.
Với TextArea sự kiện đặc trưng là TextEvent. Các phương thức cơ bản của
TextField cũng tương tự như Label và TextField, chúng ta sẽ tìm hiểu thông qua
chương trình Ví dụ 10.6.

Hình 10.12. Ví dụ với giao diện với TextArea


Trên giao diện chương trình (Hình 10.12) có một thành phần TextArea,
một hộp nhập văn bản TextField cùng hai nút bấm Add và Clear, khi nhấn nút
Add, dòng chữ ở TextField sẽ được chèn vào TextArea, còn khi nhấn nút Clear
thì vùng văn bản TextArea sẽ bị xóa.
Ví dụ 10.6. Chương trình giao diện với TextArea:
import java.awt.*;
import java.awt.event.*;

public class GUI_TextArea_Sample extends Frame implements


ActionListener {
private TextArea textArea;
private TextField inputLine;
private Button btnAdd, btnClear;
private Panel panel;
//Constructor
public GUI_TextArea_Sample(String title) {
this.setTitle(title);

textArea = new TextArea();// Tạo đối tượng TextArea


textArea.setColumns(25);// Thiết lập chiều rộng ban đầu
textArea.setRows(8); // Thiết lập chiều cao ban đầu
329

textArea.setEditable(false);// Không cho phép soạn thảo


inputLine = new TextField(25);//Tạo đối tượng TextField
inputLine.addActionListener(this);//Đối tượng lắng nghe
btnAdd = new Button("ADD");
btnClear = new Button("CLEAR");
btnAdd.addActionListener(this);
btnClear.addActionListener(this);

panel = new Panel();


panel.setLayout(new FlowLayout());
panel.add(textArea);
panel.add(inputLine);
panel.add(btnAdd);
panel.add(btnClear);

this.add(panel);
this.setResizable(false);
this.setSize(250, 250);
this.setBackground(Color.LIGHT_GRAY);
}

@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() instanceof Button) {
Button clickedButton = (Button) e.getSource();
if (clickedButton == btnAdd) {
addText(inputLine.getText());//Chèn dòng
} else {
clearText();// Xóa các vùng văn bản
}
} else {// Nếu nhấn Enter trên TextField
addText(inputLine.getText());
}
}

private void clearText() {


textArea.setText("");
inputLine.setText("");
}

private void addText(String text) {


330

textArea.append(text + '\n');
inputLine.setText("");
}

public static void main(String[] args) {


GUI_TextArea_Sample sample = new
GUI_TextArea_Sample("GUI Sample");
sample.setVisible(true);
}
}
Chạy chương trình, lần lượt nhập các dòng chữ “one”, “two”, “three”,
“four”, “five”, “six” vào ô văn bản vào nhấn nút Add sẽ nhận được kết quả như
trên Hình 10.13.

Hình 10.13. Giao diện thực thi chương trình


d) Checkbox, CheckboxGroup và RadioButton
Lớp Checkbox trong thư viện java.awt được dùng cho hai loại đối tượng
hiển thị giao diện: checkbox button và radio button. Khi một đối tượng
Checkbox được tạo ra không thuộc vào bất kì một đối tượng lớp
CheckboxGroup nào thì khi đó đối tượng Checkbox là đối tượng thể hiện
checkbox button (hộp thoại lựa chọn cùng với nội dung). Ngược lại, nếu chúng
ta nhóm hai hay nhiều đối tượng Checkbox vào một đối tượng CheckboxGroup
thì các đối tượng Checkbox thể hiện trạng thái của radio button.
Điểm khác biệt giữa checkbox button và radio button là radio button nằm
trong nhóm thể hiện một nhóm các lựa chọn và chỉ có duy nhất một lựa chọn
trong nhóm được chọn tại một thời điểm còn với checkbox button thể hiện các
lựa chọn riêng lẻ, sự lựa chọn không liên quan đến các đối tượng checkbox
331

button khác.

Hình 10.14. Ví dụ checkbox button và radio button


Hình 10.14 minh họa cho trường hợp sử dụng và sự khác biệt giữa
checkbox button và radio button. Hình bên trái thể hiện sự sử dụng checkbox
button để tạo lên một giao diện ứng dụng thực hiện trả lời câu hỏi đa lựa chọn
cho câu hỏi “Chọn ngôn ngữ lập trình bạn biết?”, để trả lời chúng ta có thể lựa
chọn nhiều đáp án tương ứng với các checkbox button. Ở hình bên phải thể hiện
sử dụng radio button tạo lên một giao diện chương trình trả lời câu hỏi “Lựa
chọn ngôn ngữ lập trình bạn thích nhất?” và ở đây chỉ có một đáp án duy nhất,
khi các đối tượng Checkbox được nhóm để tạo thành một nhóm các radio button
thì chỉ có một đối tượng trong nhóm được chọn tại một thời điểm.
Lưu ý: Trong thư viện Swing, Java xây dựng hai lớp đối tượng riêng biệt
để thể hiện cho checkbox button và radio button là JCheckBox và JRadioButton.
Trạng thái của Checkbox được thay đổi bởi thao tác bấm của người sử
dụng và khi đó một sự kiện ItemEvent phát sinh, chúng ta có thể cài đặt xử lý sự
kiện với giao diện ItemListener cho CheckBox để lắng nghe và xử lý mỗi khi
Checkbox thay đổi trạng thái. Một số phương thức cơ bản của Checkbox:
- Các constructor khởi tạo nội dung hiển thị và trạng thái ban đầu của
Checkbox: Checkbox(), Checkbox(String label), Checkbox(String label, boolean
state),…
- getLabel(): Phương thức trả về một xâu ký tự là nội dung hiển thị của
Checkbox.
- getState(): Phương thức trả về trạng thái của Checkbox chọn hay không
chọn tương ứng với hai giá trị true/false.
- setCheckboxGroup(CheckboxGroup g): Phương thức đưa đối tượng
Checkbox vào một nhóm g để tạo thành một nhóm radio button.
Ví dụ 10.7 thể hiện chương trình có giao diện gồm một TextField để nhập
332

một nội dung văn bản và hai Checkbox như hình bên dưới. Khi các Checkbox
được chọn, font chữ hiển thị trên TextField thay đổi tương ứng theo.

Hình 10.15. Ví dụ checkbox button


Ví dụ 10.7. Chương trình với các Checkbox:
import java.awt.*;
import java.awt.event.*;

public class GUI_CheckBox_Sample extends Frame implements


ItemListener {

private Checkbox checkBold;


private Checkbox checkItalic;
private TextField txtText;
private Panel panel;
public GUI_CheckBox_Sample(String title) {
this.setTitle(title);
checkBold = new Checkbox("Bold");
checkItalic = new Checkbox("Italic");
checkBold.addItemListener(this);
checkItalic.addItemListener(this);
txtText = new TextField("Sample Text", 16);
Font font = new Font("Courier", Font.PLAIN, 14);
txtText.setFont(font);
panel = new Panel();
panel.add(txtText);
panel.add(checkBold);
panel.add(checkItalic);
this.add(panel);
this.setSize(300, 150);
this.setBackground(Color.LIGHT_GRAY);
}
public void itemStateChanged(ItemEvent event) {
int valBold = Font.PLAIN;
int valItalic = Font.PLAIN;
333

if (checkBold.getState())
valBold = Font.BOLD;
if (checkItalic.getState())
valItalic = Font.ITALIC;
Font font = new Font("Courier", valBold + valItalic, 14);
txtText.setFont(font);
}
public static void main(String[] args) {
GUI_CheckBox_Sample sample = new
GUI_CheckBox_Sample("GUI Sample");
sample.setVisible(true);
}
}
Chạy chương trình và chọn hoặc bỏ chọn các check box, kết quả nhận
được như trên Hình 10.16.

Hình 10.16. Kết quả khi chạy chương trình với Checkbox
Ví dụ 10.8 thể hiện chương trình có giao diện khá tương đồng với Ví dụ
10.7, nhưng Ví dụ 10.8 sử dụng radio button thay vì checkbox button.

Hình 10.17. Ví dụ radio buton


Ví dụ 10.8. Chương trình giao diện với radio button:
import java.awt.*;
334

import java.awt.event.*;

public class GUI_RadioButton_Sample extends Frame implements


ItemListener {

private Checkbox plain, bold, italic;


private CheckboxGroup group;
private TextField txtText;
private Panel panel;

public GUI_RadioButton_Sample(String title) {


this.setTitle(title);

group = new CheckboxGroup();


plain = new Checkbox("Plain", group, true);
bold = new Checkbox("Bold", group, false);
italic = new Checkbox("Italic", group, false);
txtText = new TextField("Sample Text");
txtText.setFont(new Font("Courier", Font.PLAIN, 14));
plain.addItemListener(this);
bold.addItemListener(this);
italic.addItemListener(this);
panel = new Panel();
panel.add(txtText);
panel.add(plain);
panel.add(italic);
panel.add(bold);
this.add(panel);
this.setSize(300, 150);
this.setBackground(Color.LIGHT_GRAY);
}
public void itemStateChanged(ItemEvent event) {
int mode = 0;
if (event.getSource() == plain)
mode = Font.PLAIN;
if (event.getSource() == italic)
mode = Font.ITALIC;
if (event.getSource() == bold)
mode = Font.BOLD;
txtText.setFont(new Font("Courier", mode, 14));
}
public static void main(String[] args) {
335

GUI_RadioButton_Sample sample = new


GUI_RadioButton_Sample("GUI Sample");
sample.setVisible(true);
}
}
Chạy chương trình, kết quả được thể hiện trên Hình 10.18.

Hình 10.18. Kết quả thực thi chương trình


e) List
List là thành phần giao diện cho phép hiển thị một danh sách nhiều phần
tử theo hàng, ví dụ hiển thị một danh sách sinh viên, một danh sách file,… Các
phần tử của List có thể được thay đổi khi cần thiết. Để khởi tạo các phần tử hiển
thị ban đầu cho List ta có thể khởi tạo thông qua việc tạo một mảng xâu ký tự
chứa các phần tử của danh sách, sau đó đưa các phần tử hiển thị lên List, ví dụ:
String animal _names[] = {"Ape", "Bat", "Bee", "Cat",
"Dog", "Eel", "Fox", "Gnu",
"Hen", "Man", "Sow", "Yak"};
và khởi tạo List thông qua một số constructor:
- List(): Tạo một thành phần List đơn giản với với số hàng hiển thị mặc
định.
- List(int rows): Tạo một thành phần List với số hàng hiển thị chỉ định là
tham số truyền vào rows.
- List(int rows, boolean multiMode): Khởi tạo một thành phần List với số
hàng hiển thị chỉ định rows đồng thời thiết lập chế độ (mode) lựa chọn
multiMode cho các phần tử của List, giá trị true cho phép lựa chọn một lúc nhiều
(đa lựa chọn) phần tử của List, còn false quy định List chỉ hỗ trợ lựa chọn đơn.
Với hai contructor ở trên, giá trị của multiMode được sử dụng mặc định là false.
Như vậy, List mặc định mode chọn các thành phần là đơn chọn. Muốn
thiết lập lại mode cho List thành đa lựa chọn có thể sử dụng phương thức:
list.setMultipleMode(true);
336

List quản lý các phần tử của nó dưới dạng danh sách. Có thể dễ dàng thêm
các thành phần hoặc xóa các phần tử của List thông qua các phương thức:
list.add(String item);//Thêm vào một xâu ký tự
list.add(String item, int index); //Chèn vào một xâu ký tự tại vị trí index
list.remove(int position);//Xóa phần tử tại vị trí position
list.remove(String item);//Xóa phần tử item
list.removeAlls();//Xóa tất cả các phần tử
Để lấy về các phần tử được chọn của List, có thể sử dụng các phương
thức:
list.getSelectedItem();//Trả về phần tử được chọn trong chế độ đơn chọn
list.getSelectedIndex();//Trả về chỉ số phần tử được chọn
list.getSelectedItems();//Trả về các phần tử được chọn trong chế độ đa
chọn.
list.getSelectedIndexes();//Trả về các chỉ số các phần tử được chọn trong
chế độ đa chọn.
List có khả năng phát sinh các sự kiện ItemEvent và ActionEvent, chúng ta
có thể cài đặt đối tượng lắng nghe để xử lý bằng hai giao diện ItemListener và
ActionListener.
Ví dụ 10.9 thể hiện chương trình với giao diện như Hình 10.19, khi bấm
nút OK giá trị và vị trí của thành phần được chọn trên List sẽ được hiển thị lên
màn hình Console.

Hình 10.19. Ví dụ giao diện với List


Ví dụ 10.9. Chương trình với giao diện danh sách List:
import java.awt.*;
import java.awt.event.*;
public class GUI_List_Sample extends Frame implements ActionListener
private List list; private Button button;
private Panel centerPanel;
337

private Panel southPanel;


//Constructor
public GUI_List_Sample(String title) {
this.setTitle(title);
this.setLayout(new BorderLayout());
String animal_names[] = { "Ape", "Bat", "Bee","Cat",
"Dog", "Eel", "Fox", "Gnu",
"Hen", "Man", "Sow","Yak" };
list = new List(7);
for (int i = 0; i < animal _names.length; i++) {
list.add(animal _names[i]);
}
list.setMultipleMode(true);
button = new Button("OK");
button.addActionListener(this);

centerPanel = new Panel();


centerPanel.add(list);
southPanel = new Panel();
southPanel.add(button);

this.add(centerPanel, BorderLayout.CENTER);
this.add(southPanel, BorderLayout.SOUTH);
this.setSize(250, 200);
this.setBackground(Color.LIGHT_GRAY);
}
public void actionPerformed(ActionEvent e) {
Object[] name;
int[] index;
name = list.getSelectedItems();
index = list.getSelectedIndexes();
System.out.println("Tên được chọn");
for (int i = 0; i < name.length; i++) {
//Đưa thông tin về phần tử được chọn ra màn hình
System.out.println((String) name[i] + " ở vị trí " +
index[i]);
}
}
public static void main(String[] args) {
GUI_List_Sample sample = new GUI_List_Sample("GUi
Sample");
sample.setVisible(true);
338

}
}
Ở trên chúng ta đã làm quen với một số những thành phần giao diện cơ
bản được sử dụng nhiều khi xây dựng ứng dụng giao diện với Java AWT. Cần
nhớ rằng những nội dung được đề cập đến chỉ là một phần nhỏ và là những nội
dung cơ bản nhất trong thư viện AWT. Tuy nhiên các thành phần khác không
được đề cập đến trong nội dung này cũng có nguyên lý sử dụng tương tự. Thông
qua nội dung giới thiệu, có thể mở rộng với bất kì thành phần giao diện nào
trong thư viện AWT.
10.4.2. Thành phần vật chứa (container)
Container là các lớp thành phần giao diện đóng vai trò làm vật chứa các
thành phần giao diện khác, là nơi bố cục, sắp xếp các controls hoặc các
container khác để tạo nên giao diện của chương trình. Vì vậy trong xây dựng
chương trình có giao diện đồ họa với Java AWT, container là thành phần không
thể thiếu.

Hình 10.20. Sơ đồ kế thừa các lớp container trong AWT


Các lớp thành phần container mang đặc điểm chung đều là các lớp kế thừa
từ lớp Container trong thư viện Java AWT (Hình 10.20), vì vậy chúng có các
thuộc tính và phương thức chung của vật chứa được định nghĩa trong lớp
Container.
Một số phương thức cơ bản của lớp Container:
- Các phương thức add(): được nạp chồng ở nhiều dạng khác nhau, có
chức năng đưa các thành phần GUI khác lên vật chứa đồng thời có thể khởi tạo
vị trí.
- addContainerListener(): cài đặt đối tượng lắng nghe để xử lý các sự
kiện.
- setLayout() và getLayout(): Cặp phương thức với chức năng thiết lập và
lấy về sắp xếp bố cục của container.
339

Chúng ta tìm hiểu chi tiết về một số container thông dụng trong thư viện
AWT.

a) Frame
Khung chứa Frame là khung chứa dùng để tạo các giao diện chương trình
dạng cửa sổ hoạt động độc lập. Khi được tạo ra, thành phần Frame có dạng một
khung hình chữ nhật chiếm kích thước nhất định (được thiết lập hoặc mặc định).
Cấu trúc hiển thị của Frame gồm có một thanh tiêu đề, các đường biên xung
quanh và phần nội dung là nơi để bố trí các thành phần giao diện khác (Hình
10.21). Thông thường Frame được sử dụng làm vật chứa ngoài cùng của giao
diện.

Hình 10.21. Cấu trúc Frame


Sơ đồ kế thừa ở Hình 10.20 cho chúng ta thấy rằng lớp Frame kế thừa
trực tiếp từ lớp container Window, có thể được lắng nghe và xử lý các sự kiện
xảy ra trên cửa sổ Frame thông qua giao diện WindowListener.
Chúng ta đã tìm hiểu việc viết chương trình giao diện với Frame ngay từ
đầu Chương 10, tuy nhiên có một nội dung chưa được đề cập đến nhiều đó là
cách thức bố cục, sắp xếp các thành phần giao diện lên trên một vật chứa, cụ thể
ở đây là vật chứa Frame. Cách bố cục, sắp xếp các thành phần giao diện lên một
vật chứa được gọi là layout, để thiết lập hoặc lấy về layout của Frame chúng ta
sử dụng trực tiếp cặp phương thức get, set trong lớp Container đã được giới
thiệu ở trên. Trong trường hợp ta tạo ra một Frame mặc định và không thực hiện
bất kì thay đổi nào về layout, thì khi đó Frame sử dụng bố cục mặc định
BorderLayout để sắp xếp các thành phần GUI lên trên nó. Cụ thể về việc thay
đổi và sử dụng các layout khác được giới thiệu ở mục 10.5.
Ví dụ 10.10 thể hiện chương trình xử lý sự kiện với giao diện
WindowListener trên Frame.
340

Ví dụ 10.20. Xử lý sự kiện cửa sổ trên Frame:


import java.awt.*;
import java.awt.event.*;

public class GUI_Sample {


public static void main(String[] args) {
Frame frame = new Frame("Example on Frame");
Label label =
new Label("This is a label in Frame",
Label.CENTER);
frame.add(label, BorderLayout.CENTER);
frame.setSize(300, 250);
frame.setVisible(true);
frame.addWindowListener(new MyWindowListener());
}
}
class MyWindowListener extends WindowAdapter {
public void windowClosing(WindowEvent event) {
System.exit(0);
}
}
Ở Ví dụ 10.10 trên, đối tượng lắng nghe sự kiện WindowEvent của Frame
được tạo ra từ lớp MyWindowListener, đây là lớp kế thừa từ lớp
WindowAdapter, còn lớp WindowAdapter thực thi giao diện WindowListener.
Như vậy, đối tượng của lớp MyWindowListener ở đây cũng chính là một đối
tượng của lớp thực thi giao diện WindowListener để lắng nghe sự kiện trên
Frame. Mục đích của việc xử lý sự kiện ở đây là đóng cửa sổ khi người dùng
nhấn vào nút “Close” trên góc phải phía trên cửa sổ.
b) Panel
Panel là một lớp thành phần vật chứa được sử dụng nhiều trong xây dựng
chương trình GUI. Lớp Panel được tạo ra kế thừa trực tiếp từ lớp Container vì
vậy cũng mang đầy đủ các phương thức đã giới thiệu ở lớp Container. Panel
thường đóng vai trò làm vật chứa con, giúp ta phân chia không gian giao diện
thành nhiều phần nhỏ để xây dựng giao diện riêng lẻ, nhằm tạo ra các giao diện
phức tạp hơn phù hợp với yêu cầu của chương trình. Khi được tạo ra, Panel
cũng chiếm một không gian hình chữ nhật giống như Frame, tuy nhiên nó không
có thanh tiêu đề. Điểm khác biệt lớn nhất giữa Panel và Frame chính là Panel
không có khả năng hiển thị độc lập như Frame mà phải được gắn vào một vật
chứa khác, vật chứa cho Panel thường là Frame như chúng ta đã thấy ở các ví
341

dụ trước. Thông thường khi xây dựng chương trình GUI độc lập, Frame thường
được sử dụng làm vật chứa ngoài cùng cũng giao diện, sau đó có thể tạo một
Panel đại diện cho vùng nội dung (content) của Frame hoặc đưa các thành phần
con trực tiếp vào phần content của Frame.
Phần content của Frame thường lại được chia nhỏ thành nhiều vùng riêng
biệt, mỗi vùng sẽ được đại diện bởi một Panel để tổ hợp và tạo lên các vùng có
layout và giao diện khác nhau. Hình 10.22 thể hiện một ví dụ và cấu trúc xây
các thành phần tạo lên giao diện chương trình.

Hình 10.22. Minh họa cấu trúc tạo lên giao diện
Ở ví dụ Hình 10.22, giao diện ngoài cùng là Frame, phần nội dung của
giao diện được chia nhỏ và sử dụng hai Panel để thể hiện, Panel bên trái chỉ
chứa duy nhất một Button, Panel bên phải được lại tiếp tục được chia nhỏ thành
hai phần là một TextField và một Panel con chứa 12 Button.
Thông qua các ví dụ đã được tìm hiểu ở phần trên chúng ta có thể rút ra
các bước để xây dựng giao diện với Panel như sau:
- Tạo panel: Tạo đối tượng của lớp Panel thông qua các constructor;
- Thiết lập bố cục cho panel: Có thể được thiết lập trực tiếp khi tạo ra
panel hoặc thông qua phương thức setLayout(). Layout mặc định của Panel là
FlowLayout;
- Đưa các thành phần controls lên trên từng vùng chứa panel bằng cách
gọi phương thức add();
- Đưa panel đặt lên vật chứa ngoài thông qua phương thức add() của vật
342

chứa ngoài.
10.5. BỘ QUẢN LÝ TRÌNH BÀY (LAYOUT MANAGER)
Ở những nội dung trước của Chương 10, chúng ta đã sử dụng FlowLayout
và BorderLayout trong một số ví dụ. Ở mục này chúng ta sẽ tìm hiểu rõ hơn về
các layout trong Java và cách thức sự dùng với từng loại layout.
Sử dụng layout cho phép tổ hợp các thành phần giao diện theo những quy
tắc đã được định nghĩa sẵn, giúp tiết kiệm thời gian trong lập trình, làm đơn giản
hóa việc xây dựng giao diện đồng thời hỗ trợ giao diện chương trình tự điều
chỉnh khi kích thước của các thành phần thay đổi. Ví dụ như khi đưa một thành
phần Button vào một Panel sử dụng FlowLayout, thì thành phần Button sẽ được
đặt vào vị trí chính giữa theo chiều ngang của Panel và khi kích thước của Panel
thay đổi thì vị trí tương đối chính giữa đó cũng thay đổi theo.
Java hỗ trợ bốn bộ quản lý trình bày cơ bản (bốn lớp layout). Chúng ta sẽ
cùng tìm hiểu kĩ về từng bộ quản lý trình bày.
a) FlowLayout
FlowLayout là một trong số những layout được sử dụng nhiều nhất. Khi
sử dụng FlowLayout, các thành phần giao diện được đặt lên giao diện vật chứa
theo nguyên tắc từ trái qua phải theo dòng và có kích thước như chúng được
thiết lập. Khi kích thước của vật chứa không đủ để chứa các thành phần theo
chiều ngang thì thành phần giao diện sẽ được đặt xuống dòng tiếp theo. Mặc
định, các thành phần sẽ được xếp thành hàng và căn lề chính giữa của vật chứa.
Nếu kích thước của vật chứa thay đổi, vị trí của các thành phần cũng sẽ tự động
thay đổi theo để đảm bảo vị trí tương đối và nguyên tắc xắp xếp theo hàng của
layout.
Tạo và sử dụng FlowLayout là một việc đơn giản, chỉ là việc tạo đối
tượng của lớp FlowLayout, thiết lập các thuộc tính bố cục và gắn đối tượng
layout vào vật chứa bằng phương thức setLayout() của vật chứa:

FlowLayout layout = new FlowLayout(FlowLayout.CENTER);
panel.setLayout(layout);

Lớp FlowLayout cung cấp một số constructor để tạo đối tượng layout:
- FlowLayout(): Constructor mặc định tạo đối tượng FlowLayout với việc
căn lề giũa các thành phần giao diện.
- FlowLayout(int align): Constructor tạo layout đồng thời cài đặt căn lề
343

bằng giá trị số nguyên truyền vào. Giá trị align có thể nhận một trong số các giá
trị được định nghĩa sẵn trong lớp FlowLayout: CENTER, LEADING, LEFT,
RIGHT, TRAILING.
- FlowLayout(int align, int hgap, int vgap): Tạo layout cùng với cách thức
căn lề theo giá trị align, khoảng cách giữa các thành phần theo chiều ngang là
hgap và theo chiều dọc là vgap.
Ví dụ 10.11 thể hiện chương trình sử dụng FlowLayout để tạo giao diện
ban đầu như trên Hình 10.23 và tự điều chỉnh khi bị thay đổi kích thước như trên
Hình 10.24.

Hình 10.23. Giao diện ban đầu với FlowLayout

Hình 10.24. Giao diện khi kích thước giao diện thay đổi
Ví dụ 10.11. Sử dụng FlowLayout:
import java.awt.*;

public class GUI_FlowLayout_Sample extends Frame {

private Button button1, button2, button3, button4, button5;


private Panel contentPanel;

public GUI_FlowLayout_Sample(String title) {


this.setTitle(title);

button1 = new Button("Button1");


button2 = new Button("Button2");
344

button3 = new Button("Button3");


button4 = new Button("Button4");
button5 = new Button("Button5");
contentPanel = new Panel();
contentPanel.setLayout(new
FlowLayout(FlowLayout.CENTER));
contentPanel.add(button1);
contentPanel.add(button2);
contentPanel.add(button3);
contentPanel.add(button4);
contentPanel.add(button5);

this.add(contentPanel);
this.setSize(300, 200);
this.setBackground(Color.LIGHT_GRAY);
}

public static void main(String[] args) {


GUI_FlowLayout_Sample sample = new
GUI_FlowLayout_Sample("GUI Sample");
sample.setVisible(true);
}
}
b) BorderLayout
BorderLayout quy định cách đặt các thành phần giao diện lên trên vật
chứa theo vùng, giao diện vật chứa được chia làm năm vùng riêng biệt: center,
north, south, east và west (trung tâm, bắc, nam, đông và tây) như trên Hình
10.25.

Hình 10.25. Các vùng của BorderLayout


Ta có thể tạo và thiết lập một số thuộc tính cho BorderLayout thông qua
các phương thức được định nghĩa trong lớp tương ứng. Khi một vật chứa sử
345

dụng BorderLayout làm bố cục trình bày, không gian của nó sẽ được chia làm
năm vùng và mỗi thành phần giao diện có thể được đặt vào một vùng. Nếu đặt
thành phần và vùng north hoặc south thì chiều cao của thành phần sẽ có kích
thước tùy ý, còn chiều rộng đúng theo kích thước vùng chứa. Nếu đặt thành
phần vào vùng east hoặc west thì chiều rộng của thành phần có kích thước tùy ý,
còn chiều cao theo kích thước vùng chứa. Vùng center thì có kích thước phụ
thuộc vào các vùng xung quanh.

Có thể thiết lập BorderLayout cho vật chứa (panel) và đưa các thành phần
lên vật chứa bằng các phương thức:

panel.setLayout(new BorderLayout());
panel.add(button1, Borderlayout.NORTH);
panel.add(button2, Borderlayout.SOUTH);
panel.add(button3, Borderlayout.EAST);
panel.add(button4, Borderlayout.WEST);
panel.add(button5, Borderlayout.CENTER);

Mặc định giữa các thành phần của BorderLayout không có khoảng cách,
tuy nhiên cũng có thể thiết lập khoảng cách thông qua việc tạo layout với
constructor có tham số là khoảng cách theo chiều ngang và chiều cao:
panel.setLayout(new BorderLayout(10,20));
Ví dụ 10.12 thể hiện chương trình có giao diện như Hình 10.26 với
BorderLayout.

Hình 10.26. Ví dụ giao diện BorderLayout


Ví dụ 10.12. Sử dụng BorderLayout:
import java.awt.Panel;
346

public class GUI_BL_Sample extends Frame {

private Button north, south, east, west, center;


private Panel contentPanel;

public GUI_BL_Sample(String title) {


this.setTitle(title);

north = new Button("North");


south = new Button("South");
east = new Button("East");
west = new Button("West");
center = new Button("Center");

contentPanel = new Panel(new BorderLayout());


contentPanel.add(north, BorderLayout.NORTH);
contentPanel.add(south, BorderLayout.SOUTH);
contentPanel.add(east, BorderLayout.EAST);
contentPanel.add(west, BorderLayout.WEST);
contentPanel.add(center, BorderLayout.CENTER);

this.add(contentPanel);
this.setSize(300, 200);
this.setBackground(Color.LIGHT_GRAY);
}

public static void main(String args[]) {


GUI_BL_Sample sample = new GUI_BL_Sample("GUI
Sample");
sample.setVisible(true);
}
}
c) GridLayout
Khi một vật chứa sử dụng bố cục GridLayout, vật chứa đó sẽ được chia
nhỏ thành các ô có kích thước bằng nhau hay có thể hiểu chiều rộng và chiều
cao của vật chứa được chia thành các cột và hàng có kích thước bằng nhau, toàn
bộ vật chứa được chia theo dạng lưới. Và theo đó, các thành phần khi được đưa
lên vật chứa sẽ được đặt vào vị trí từng ô theo thứ tự từ trái qua phải, từ trên
xuống dưới và chúng sẽ có kích thước tương đồng nhau (Hình 10.27).
347

Hình 10.27. Ví dụ giao diện với GridLayout


Đối với GridLayout ta có thể thiết lập thuộc tính số hàng, cột, khoảng
cách mỗi hàng, mỗi cột thông qua các phương thức tương tự như với
FlowLayout và BorderLayout.
Chương trình tạo nên giao diện Hình 10.27 được thể hiện trong Ví dụ
10.13.
Ví dụ 10.13. Sử dụng GridLayout:
import java.awt.*;

public class GUI_GridLayout_Sample extends Frame {

private Button button1, button2, button3;


private Checkbox checkBox;
private Label label;
private TextField textField;
private Panel contentPanel;

public GUI_GridLayout_Sample(String title) {


this.setTitle(title);
button1 = new Button("Red");
button2 = new Button("Green");
button3 = new Button("Blue");
checkBox = new Checkbox("Pick me", true);
label = new Label("Enter name here:");
textField = new TextField();

contentPanel = new Panel(new GridLayout(3, 2));


contentPanel.add(button1);
contentPanel.add(button2);
contentPanel.add(button3);
contentPanel.add(checkBox);
contentPanel.add(label);
contentPanel.add(textField);
348

this.add(contentPanel);
this.setSize(400, 150);
this.setBackground(Color.LIGHT_GRAY);
}

public static void main(String[] args) {


GUI_GridLayout_Sample sample = new
GUI_GridLayout_Sample("GridLayout Demo");
sample.setVisible(true);
}
}

d) Absolute Layout - Null Layout


Ở trên chúng ta đã tìm hiểu rõ về ba layout cơ bản. Với ba loại layout đã
tìm hiểu cùng với nguyên lý chia nhỏ để tổ hợp nên các giao diện phức tạp,
chúng ta hoàn toàn có thể sử dụng ba loại layout này để xây dựng và tạo ra mọi
giao diện người dùng phù hợp với yêu cầu. Những layout đã tìm hiểu có chung
đặc điểm là xây dựng sẵn các cách thức bố cục theo những nguyên tắc nhất định
và có thể áp dụng được trong hầu hết các trường hợp giao diện, tuy nhiên trong
một số trường hợp đặc biệt những layout này lại thể hiện lên tính tùy biến và khả
năng thay đổi chưa đáp ứng theo ý của người lập trình. Trong những trường hợp
cần có sự tùy biến linh hoạt, có thể sử dụng Absolute Layout hay còn được gọi
là Null Layout. Đây là một loại layout đặc biệt, không quy định bất kì nguyên
tắc sắp xếp nào đối với các thành phần khi đặt lên vật chứa, các thành phần được
đưa lên vật chứa với vị trí tuyệt đối, được xác định bằng điểm đặt (vị trí góc trái
bên trên của thành phần) và kích thước (chiều rộng, chiều cao) của thành phần
đó.
Để thiết lập Null Layout cho một vật chứa ta truyền tham số null vào cho
phương thức setLayout(), ví dụ:
contentPanel.setLayout(null);
Khi muốn đưa một thành phần lên vật chứa, cần phải xác định vị trí tuyệt
đối của nó, có thể thông qua các phương thức:
- setSize(int, int ): cài đặt kích thước của thành phần.
- setLocation(int, int): cài đặt vị trí của thành phần.
- setBound(int, int, int, int): Cài đặt vị trí và kích thước của thành phần, ví
dụ:
349

buttonOk.setBound(70, 125, 80, 30);


Lưu ý: Vị trí (location) của thành phần được tính từ góc trái bên trên của
khung chứa tới góc trái bên trên của thành phần. Bound là sự kết hợp của vị trí
và kích thước.

Hình 10.28. Giao diện với Null Layout


Ví dụ 10.14 trình bày chương trình sử dụng Null Layout để tạo giao diện
như Hình 10.28.
Ví dụ 10.14. Sử dụng Null Layout:
import java.awt.*;
public class GUI_NullLayout_Sample extends Frame {
private Button cancelButton;
private Button buttonOk;
private Panel contentPanel;
public GUI_NullLayout_Sample(String title) {
this.setTitle(title);
contentPanel = new Panel();
contentPanel.setLayout(null);
buttonOk = new Button("OK");
buttonOk.setBounds(70, 125, 80, 30);
contentPanel.add(buttonOk);
cancelButton = new Button("CANCEL");
cancelButton.setBounds(160, 125, 80, 30);
contentPanel.add(cancelButton);
this.add(contentPanel);
this.setSize(300, 220);
this.setResizable(false);
this.setBackground(Color.LIGHT_GRAY);
this.setLocation(150, 250);
}
public static void main(String[] args) {
GUI_NullLayout_Sample sample = new
GUI_NullLayout_Sample("Null Layout Sample");
350

sample.setVisible(true);
}
}
10.6. JAVA SWING
Trong các mục trước, chúng ta đã tìm hiểu đầy đủ nguyên lý và các nội
dung cần thiết để xây dựng chương trình GUI với thư viện AWT. Như đã nói
ngay từ đầu chương, Java cung cấp hai bộ thư viện GUI cơ bản là AWT và
Swing. Thư viện AWT sử dụng các thành phần GUI có liên quan đến nền tảng,
vì vậy giao diện chương trình có thể sẽ có chút khác biệt khi thực thi chương
trình trên những nền tảng khác nhau. Swing là sự kế thừa và phát triển của AWT
với sự bổ sung khả năng hiển thị độc lập với nền tảng, vì vậy mặc định chương
trình có giao diện tương đồng trên các nền tảng khác nhau. Ngoài ra, Swing
cung cấp thêm nhiều khả năng mới mà AWT không có, giúp các thành phần
giao diện hiển thị đẹp và phong phú hơn.
Ví dụ, khi tạo một nút bấm với Swing, có thể dễ dàng hiển thị hình ảnh
(icon) cùng với nội dung văn bản trên nút bấm, trong khi đó đối với AWT trên
nút bấm chỉ có thể hiển thị nội dung văn bản.
Swing không định nghĩa mới các sự kiện, giao diện xử lý sự kiện. Khi xây
dựng chương trình giao diện với Swing, chúng ta vẫn sử dụng nguyên lý xử lý
sự kiện tương tự như với AWT. Các bước xây dựng giao diện GUI với Swing
hoàn toàn tương tự như các bước xây dựng giao diện GUI với AWT. Tương ứng
với các lớp GUI trong AWT chúng ta gần như chắc chắn sẽ có một lớp tương
ứng trong Swing với tên lớp tương tự kết hợp thêm kí tự ‘J’ ở phía trước. Ví dụ:
để tạo thành phần giao diện nút bấm, với AWT ta có lớp Button trong gói
java.awt, còn với Swing ta có lớp JButton trong gói javax.swing, điều này cũng
tương tự với các thành phần giao diện khác.
Tuy nhiên cũng cần lưu ý: Không phải tất cả các lớp thành phần GUI
trong AWT đều có sự tương tự trong Swing. Swing đã loại bỏ một số lớp thành
phần giao diện ít được sử dụng, đồng thời bổ sung thêm nhiều lớp mới tiện ích
hơn cho quá trình xây dựng giao diện. Ví dụ, Swing sử dụng các lớp
JRadioButton và ButtonGroup thay cho cách dùng CheckBox và
CheckboxGroup để tạo nên các radio button như trong AWT.
Chúng ta sẽ tìm hiểu ví dụ sử dụng Swing qua chương trình với menu.
Menu là thành phần giao diện được sử dụng rộng rãi trong xây dựng chương
trình ứng dụng, gần như tất cả các chương trình ứng dụng hiện nay đều có menu.
Để tạo chương trình với giao diện menu, trong AWT có lớp Menu hỗ trợ, còn
351

trong Swing là lớp JMenu.


Để tạo ra một menu cho chương trình chúng ta cần sử dụng ba lớp trong
gói javax.swing: JMenu, JMenuItem và JMenuBar. JMenu là lớp đại diện cho
các lựa chọn menu cụ thể chúng ta vẫn thường thấy với menu khi lựa chọn. Đối
tượng JMenu được ra tạo bằng constructor, ví dụ:
fileMenu = new JMenu(“File”);
Trên dòng lệnh trên, xâu ký tự tham số truyền vào của constructor là tên
của menu. Sau khi tạo ra một menu, chúng ta cần tạo ra các thành phần lựa chọn
(menu item) của menu, các thành phần này được tạo ra từ lớp JMenuItem. Ta có
thể cài đặt đối tượng lắng nghe và xử lý sự kiện ActionListener trên JMenuItem
và sau đó đưa menu item lên trên menu. Ví dụ, tạo dòng lựa chọn New và thêm
vào menu File đã tạo ra ở trên:
new_item = new JMenuItem(“New”); item.addActionListener(this);
fileMenu.add(new_item);
Sau khi đã tạo ra các menu và các item của chúng, để đưa menu hiển thị
lên trên giao diện của chương trình, chúng ta sử dụng đối tượng lớp JMenuBar,
lớp JMenuBar đại diện cho toàn bộ thanh menu, là nơi để đặt các menu, giúp
menu hiển thị lên thanh ngang (mặc định) phía dưới thanh tiêu đề của giao diện.
Ví dụ:
JMenuBar menuBar = new JMenuBar();
menuBar.add(fileMenu);
menuBar.add(editMenu);
this.setMenuBar(menuBar);
Như vậy, các bước để tạo menu cho ứng dụng như sau:
1. Tạo đối tượng JMenuBar và gắn vào giao diện (frame);
2. Tạo đối tượng JMenu;
3. Tạo các đối tượng JMenuItem và đưa lên đối tượng JMenu;
4. Đưa đối tượng JMenu lên JMenuBar;
352

Hình 10.29. Giao diện chương trình


Ví dụ 10.15 thể hiện chương trình giao diện gồm có hai menu là File và
Edit cùng các thành phần lựa chọn như Hình 10.29. Khi lựa chọn bất kì thành
phần menu nào nội dung label sẽ thay đổi để hiện thị dòng thông tin tương ứng,
ngoại trừ khi chọn thành phần menu Quit để thoát chương trình.
Ví dụ 10.15. Chương trình với giao diện menu của Swing:
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class Menu_GUI_Sample extends JFrame implements
ActionListener {
private static final int FRAME_WIDTH = 300;
private static final int FRAME_HEIGHT = 250;
private static final int FRAME_X_ORIGIN = 150;
private static final int FRAME_Y_ORIGIN = 250;
private JLabel response;
private JMenu fileMenu;
private JMenu editMenu;

public static void main(String[] args) {


Menu_GUI_Sample frame = new Menu_GUI_Sample();
frame.setVisible(true);
}

public Menu_GUI_Sample() {
Container contentPane;
setTitle("GUI Sample");
353

setSize(FRAME_WIDTH, FRAME_HEIGHT);
setResizable(false);
setLocation(FRAME_X_ORIGIN, FRAME_Y_ORIGIN);
setBackground(Color.LIGHT_GRAY);
contentPane = getContentPane();
contentPane.setLayout(new FlowLayout());
createFileMenu();
createEditMenu();
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
menuBar.add(fileMenu);
menuBar.add(editMenu);
response = new JLabel("Hello, this is your menu tester.");
response.setSize(250, 50);
contentPane.add(response);
setDefaultCloseOperation(EXIT_ON_CLOSE);
}
public void actionPerformed(ActionEvent event) {
String menuName;
menuName = event.getActionCommand();
if (menuName.equals("Quit")) {
System.exit(0);
}
else {
response.setText("Menu Item '" + menuName + "' is
selected.");
}
}

private void createFileMenu() {


JMenuItem item;
fileMenu = new JMenu("File");
item = new JMenuItem("New");
item.addActionListener(this);
fileMenu.add(item);
354

item = new JMenuItem("Open");


item.addActionListener(this);
fileMenu.add(item);
item = new JMenuItem("Save");
item.addActionListener(this);
fileMenu.add(item);
item = new JMenuItem("Save As...");
item.addActionListener(this);
fileMenu.add(item);
fileMenu.addSeparator();
item = new JMenuItem("Quit");
item.addActionListener(this);
fileMenu.add(item);
}

private void createEditMenu() {


JMenuItem item;
editMenu = new JMenu("Edit");
item = new JMenuItem("Cut");
item.addActionListener(this);
editMenu.add(item);
item = new JMenuItem("Copy");
item.addActionListener(this);
editMenu.add(item);
item = new JMenuItem("Paste");
item.addActionListener(this);
editMenu.add(item);
}
}
Swing còn cung cấp một khả năng mới là “Look and Feel”. “Look” ở đây
là khả năng hiển thị của các thành phần giao diện và “Feel” là hành vi phản hồi
của người dùng chương trình giao diện. Mỗi thành phần giao diện trong Swing
được chia thành hai thành phần con riêng biệt: một định nghĩa các thuộc tính,
phương thức và một chịu trách nhiệm hiển thị giao diện người dùng tương ứng
với thành phần. Ví dụ một thành phần lớp JList chứa đựng hai thành phần riêng
355

biệt là ListUI và ComponentUI. ListUI định nghĩa các thuộc tính và phương
thức, ComponentUI định nghĩa hiển thị và hành vi phản hồi. ComponentUI tuân
theo một loạt các cách thực hiển thị và hành vi khác nhau, có thể là: “the UI”,
“component UI”, “UI delegate” và “look and feel delegate”.
Môi trường thực thi của Java cung cấp một số lựa chọn cơ bản cho “Look
and Feel”:
1. CrossFlatformLookAndFeel: Giao diện hiển thị tương đồng trên mọi
nền tảng. Được định nghĩa trong thư viện javax.swing.flaf.meta và là “Look and
Feel” mặc định khi xây dựng chương trình GUI với Swing.
2. SystemLookAndFeel: Giao diện hiển thị sử dụng “Look and Feel” trong
thư viện hiển thị tự nhiên của nền tảng, phụ thuộc vào nền tảng mà chương trình
thực thi và sẽ được xác định khi thực thi chương trình.
3. Synth: Cho phép tạo mới “Look and Feel” bằng XML.
4. Multiplexing: Cho phép sử dụng nhiều “Look and Feel” khác nhau
đồng thời.
Để xây dựng chương trình với khả năng “Look and Feel”, cần thiết lập
“Look and Feel” ngay đầu tiên, chương trình sẽ khởi tạo giao diện theo đúng
yêu cầu “Look and Feel”. Ví dụ, thiết lập “Look and Feel”
CrossFlatformLookAndFeel cho giao diện:
public static void main(String[] args){
UIManager.setLookAndFeel(
UIManager.getCrossFlatformLookAndFeelClassName());
//Khởi tạo giao diện
new MyGUIApplication();…
}
Còn với SystemLookAndFeel:
public static void main(String[] args){
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
new MyGUIApplication();

}
Chúng ta cũng có thể sử dụng trực tiếp tên của “Look and Feel” để đưa
vào làm tham số truyền vào cho phương thức UIManager.setLookAndFeel(). Ví
dụ:
356

public static void main(String[] args){


UIManager.setLookAndFeel(
“javax.Swing.flaf.meta.MetalLookAndFeel”);
new MyGUIApplication();

}
Chúng ta cùng xem sự thay đổi “Look and Feel” thông qua ví dụ giao
diện menu Hình 10.29. Khi sử dụng “Look and Feel” phụ thuộc vào nền tảng
bằng câu lệnh:
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());

Hình 10.30 và Hình 10.31 thể hiện giao diện khi chương trình thực thi
trên hai nền tảng khác nhau là Windows 10 và Ubuntu 16.04. Có thể thấy rằng,
khả năng “Look and Feel” giúp xây dựng chương trình mang đặc điểm giao
diện của từng nền tảng.

Hình 10.30 Giao diện chương trình thực thi trên Windows

Hình 10.31 Giao diện chương trình thực thi trên Ubuntu
357

TỔNG KẾT CHƯƠNG


Chương 10 đã giới thiệu chi tiết những nội dung sau:
- Trên ngôn ngữ lập trình Java có thể xây dựng một cách dễ dàng những
chương trình có giao diện đồ họa, giao diện đồ họa giúp người dùng tương tác
thuận tiện hơn với các chức năng của chương trình.
- Trong Java, để xây dựng chương trình có giao diện đồ họa, có thể sử
dụng thư viện AWT hoặc thư viện Swing. Các thư viện này cung cấp các lớp
thành phần giao diện, các lớp vật chứa, các giao diện giúp xử lý sự kiện…
Những lớp và giao diện trong các thư viện này giúp xây dựng giao diện đồ họa
có độ phức tạp bất kỳ, đáp ứng các yêu cầu của người dùng.
- Muốn xây dựng giao diện đồ họa, cần xác định lớp vật chứa, tạo các
thành phần giao diện cơ bản và đưa lên trên vật chứa theo một cách bố trí nào
đó. Để giao diện đồ họa tương tác với người dùng theo yêu cầu, cần thêm các
phương thức xử lý sự kiện trên các thành phần giao diện.
- Để xử lý sự kiện trên thành phần giao diện, cần xác định các sự kiện cần
xử lý, từ đó hiện thực giao diện lắng nghe tương ứng để xử lý và đăng ký đối
tượng lắng nghe với thành phần giao diện.
- Java cung cấp rất nhiều thành phần giao diện cơ bản, các thành phần này
đều có cách sử dụng khá giống nhau. Để sử dụng, cần tạo đối tượng thành phần
bằng constructor, thiết lập các thuộc tính hiển thị (kích thước, vị trí, màu sắc, nội
dung,…), cài đặt đối tượng lắng nghe để xử lý sự kiện rồi đưa đối tượng thành
phần lên một vật chứa.
- Java cung cấp các lớp vật chứa (container) để xây dựng giao diện ngoài
cùng của chương trình. Đối tượng của lớp vật chứa chiếm một vùng hình chữ
nhật trên màn hình.
- Java đã định nghĩa sẵn một số lớp bố cục trình bày, các lớp này giúp bố
trí các thành phần giao diện cơ bản lên vật chứa theo những quy tắc nhất định,
giúp vị trí các thành phần tự động được cập nhật khi kích thước vật chứa thay
đổi.
- Thư viện Swing là sự kế thứa và phát triển của thư viện AWT. Swing
cung cấp các lớp thành phần giao diện với khả năng hiển thị phong phú hơn, đẹp
hơn, tuy nhiên Swing vẫn sử dụng mô hình xử lý sự kiện của AWT.
BÀI TẬP
1. Viết chương trình xây dựng giao diện cho phép nhập vào 3 số nguyên,
tính tổng và hiển thị kết quả lên trên giao diện.
358

2. Xây dựng chương trình tạo một lớp giao diện kế thừa từ Frame có kích
thước 400x450. Xử lý sự kiện khi bấm vào phím tắt chương trình ở góc phải bên
trên giao diện để thoát chương trình.
3. Xây dựng chương trình tạo một lớp giao diện kế thừa từ Frame cùng
với một Button và một TextField. Khi bấm vào Button thì nội dung được nhập
vào TextField sẽ hiển thị lên trên tiêu đề cửa sổ chương trình. Xử lý sự kiện bấm
nút thoát để thoát chương trình.
4. Xây dựng chương trình có giao diện như hình dưới. Khi bấm Button thì
hiển thị nội dung của Button được bấm dưới dạng cửa sổ nổi Dialog.

5. Xây dựng giao diện như hình dưới. Giao diện cho phép nhập vào một
số nguyên n thông qua TextField. Khi Button được bấm cần hiển thị n số nguyên
tố trên một hàng của TextArea.

6. Sử dụng đối tượng giao diện Menu và xây dựng chương trình giao diện
như hình dưới. Khi ấn lựa chọn Quit thì thoát khỏi chương trình. Khi ấn các lựa
359

chọn màu sắc thì thay đổi màu nền cửa sổ chương trình theo màu tương ứng.
Khi ấn lựa chọn Erase thì thiết lập lại màu nền cửa sổ chương trình theo giá trị
ban đầu.

7. Xây dựng chương trình giao diện thực hiện chức năng dạy toán học cho
trẻ em. Giao diện và chức năng chương trình như hình dưới. Khi bấm OK thì
hiển thị đáp án, khi bấm Next thì hiển thị câu hỏi mới.

8. Phát triển từ bài tập số 7, lưu lại số câu trả lời đúng và sai trong quá
trình học và hiển thị ở một nơi nào đó trên giao diện. Bổ sung nút bấm Reset để
học lại từ đầu.
9. Viết chương trình giao diện và chức năng máy tính cá nhân như hình
dưới. Chương trình xử lý các thao tác nhập dữ liệu và tính toán cơ bản tương
ứng với giao diện.
360

Câu hỏi ôn tập


1. Cây phả hệ các thành phần giao diện đồ họa?
2. Quy trình xây dựng chương trình có giao diện đồ họa?
3. Mô hình xử lý sự kiện trên giao diện đồ họa?
4. Những thành phần nào phát sinh sự kiện? Những đối tượng nào xử lý
sự kiện?
5. Khi tạo một lớp thực thi giao diện ActionListener thì cần phải ghi đè
những phương thức nào?
6. Phương thức getActionCommand() của đối tượng lớp ActionEvent trả
về nội dung gì?
7. Đối tượng lớp TextField có khả năng phát sinh những sự kiện nào? Khi
nào phát sinh?
8. Chức năng của quản lý bố cục là gì?
9. FlowLayout sắp xếp các thành phần như thế nào?
10. Viết câu lệnh tạo một BorderLayout với khoảng cách theo chiều
ngang và dọc là 20 pixel?
11. Giao diện nào lắng nghe sự kiện chuyển động chuột? Giao diện nào
lắng nghe sự kiện click chuột?
361

TÀI LIỆU THAM KHẢO


1. Aptech Education, Core Java, Aptech Worldwide Express – 2005.
2. Barry A. Burd, Beginning Programming with Java For Dummies (For
Dummies (Computer/Tech)) 4th Edition, For Dummies – 2014.
3. C. Thomas Wu, An Introduction to Object-Oriented Programming with
Java, Fifth Edition, McGrawHill Inc – 2010.
4. Cay S. Horstmann, Big Java, Fourth Edition, John Wiley & Sons Inc –
2010.
5. Cay S. Horstmann, Core Java Volume I-Fundamentals (9th Edition)
(Core Series), Prentice Hall – 2012.
6. David J. Eck, Introduction to Programming Using Java, Seventh
Edition, Hobart and William Smith Colleges – 2014.
7. Elliot B. Koffman and Paul A.T. Wolfgang, Objects, Abstractions, Data
Structures and Designe using Java, John Wiley & Sons Inc – 2005.
8. H. M. Deitel, Java™ How to Program, Sixth Edition, Prentice Hall –
2004.
9. Herbert Schildt, Java: A Beginner's Guide, Sixth Edition, McGraw-Hill
Education – 2014.
10. Herbert Schildt, Java™: The Complete Reference, Seventh Edition,
McGraw-Hill – 2007.
11. Joshua Bloch, Effective Java™, Second Edition, Addison-Wesley –
2008.
12. Kathy Sierra, Bert Bates, Head First Java, 2nd Edition, O'Reilly
Media – 2005.
13. Oracle Technology Network, Java SE Documentation, Oracle
Corporation – 2016.

You might also like