Professional Documents
Culture Documents
- Giáo Trình Lập Trình JAVA Cơ Bản (2017)
- Giáo Trình Lập Trình JAVA Cơ Bản (2017)
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
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
- 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.
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.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
- Để 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.
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.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
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.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
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.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.
- 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.
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).
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
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
Độ ư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
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.
- 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
2 4 .. 2(n 1) 2n
}
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
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
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
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
}
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);
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
}
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
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
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
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.
"");
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
}
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 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
}
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);
}
}
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
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
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
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()
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
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
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
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
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
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
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
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
- 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.
}
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) {
}
}
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);
}
}
}
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
Để 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ả
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
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
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
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).
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
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
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
}
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
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.
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
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
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
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).
//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ả
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.
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.
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.
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
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
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
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á
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
Để 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
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).
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.
Iterable Interface
Class
Collection
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
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
- 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
}
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
}
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
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.
}
}
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
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ệ.
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
} 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.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
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
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).
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
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
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
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
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
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
}
Đố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. 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.
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
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
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
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
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
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.
}
}
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;
}
@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).
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.
import java.awt.*;
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.*;
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() +
"'");
}
}
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.
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());
}
}
textArea.append(text + '\n');
inputLine.setText("");
}
button khác.
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.
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.
import java.awt.event.*;
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.
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.
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.
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.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.*;
this.add(contentPanel);
this.setSize(300, 200);
this.setBackground(Color.LIGHT_GRAY);
}
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.
this.add(contentPanel);
this.setSize(300, 200);
this.setBackground(Color.LIGHT_GRAY);
}
this.add(contentPanel);
this.setSize(400, 150);
this.setBackground(Color.LIGHT_GRAY);
}
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
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.");
}
}
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
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
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