Buổi 1: Làm quen với Java: JVM, biến, kiểu dữ liệu, vào/ra
Hãy tưởng tượng căng-tin của trường muốn có một chiếc máy tính tiền nhỏ: nhân viên gõ tên món, số lượng, máy cộng tiền, tính thuế rồi in hóa đơn. Nghe đơn giản, nhưng để máy làm được việc đó bạn phải trả lời rất nhiều câu hỏi nền tảng: chương trình được viết bằng gì, máy hiểu nó ra sao, số lượng món lưu ở đâu, giá tiền có lẻ thì lưu kiểu nào, nhập từ bàn phím và in ra màn hình thế nào, làm sao cộng trừ nhân chia đúng và không bị "tràn" số.
Buổi đầu tiên của khóa Lập trình Java đi qua đúng những câu hỏi đó. Bạn sẽ hiểu Java chạy như thế nào (JDK, JRE, JVM, bytecode), viết và chạy chương trình đầu tiên, rồi làm quen với biến, kiểu dữ liệu, vào/ra và các toán tử — bộ "chữ cái" mà mọi chương trình Java sau này đều dùng. Mọi ví dụ có nút Chạy thử đều chạy thật trên Java 13, bạn có thể sửa và chạy lại ngay trong trang.
Mục tiêu buổi học
- Sau buổi này bạn có thể giải thích Java là ngôn ngữ biên dịch-rồi-thông dịch, phân biệt được JDK, JRE, JVM, mã nguồn và bytecode.
- Sau buổi này bạn có thể biên dịch và chạy một chương trình Java bằng
javac/javahoặc bằng IDE, đọc được cấu trúc của chương trình Hello World. - Sau buổi này bạn có thể khai báo biến, hằng, chọn đúng kiểu nguyên thủy (và hiểu khác biệt với kiểu tham chiếu) cho từng loại dữ liệu.
- Sau buổi này bạn có thể ép kiểu đúng cách, nhận ra và tránh lỗi tràn số, mất độ chính xác.
- Sau buổi này bạn có thể nhập dữ liệu bằng
Scanner, định dạng đầu ra bằngprintf, và xử lý tình huống nhập hỗn hợp số và dòng văn bản. - Sau buổi này bạn có thể dùng thành thạo toán tử số học, so sánh, logic, gán, tăng/giảm, ba ngôi và đoán đúng thứ tự tính của một biểu thức.
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| 1 | Ngôn ngữ lập trình và ngôn ngữ máy | 10 phút |
| 2 | Tổng quan về Java: lịch sử, đặc điểm, JDK/JRE/JVM, phiên bản | 15 phút |
| 3 | Kiến trúc máy ảo Java (JVM) | 15 phút |
| 4 | Biên dịch, thực thi và chương trình Hello World | 20 phút |
| 5 | Biến và hằng | 15 phút |
| 6 | Kiểu dữ liệu nguyên thủy và kiểu tham chiếu | 20 phút |
| 7 | Vào/ra dữ liệu: System.out, printf, Scanner (kèm thực hành nhóm 1.1) |
30 phút |
| 8 | Ép kiểu và tràn số (kèm thực hành nhóm 1.2) | 20 phút |
| 9 | Toán tử số học, gán, tăng/giảm (kèm thực hành nhóm 1.3) | 20 phút |
| 10 | So sánh, logic, ba ngôi và thứ tự ưu tiên (kèm thực hành nhóm 1.4) | 15 phút |
Tổng cộng 180 phút, trong đó khoảng 100 phút là gõ code và làm bài tập.
1. Ngôn ngữ lập trình và ngôn ngữ máy
Máy tính chỉ hiểu số 0 và 1
Bộ xử lý (CPU) thực chất chỉ thực hiện một tập lệnh rất nhỏ như "nạp số vào thanh ghi", "cộng hai thanh ghi", "nhảy tới địa chỉ khác". Mỗi lệnh được mã hóa thành một dãy bit. Dãy lệnh nhị phân đó gọi là ngôn ngữ máy (machine code). Ngôn ngữ máy chạy nhanh nhất, nhưng gần như không ai viết trực tiếp vì cực kỳ khó đọc và gắn chặt với từng loại CPU.
Để dễ hình dung, đây là cùng một việc "gán 5 vào thanh ghi rồi cộng thêm 3" ở ba mức khác nhau:
Ngôn ngữ máy (x86, viết dạng thập lục phân): B8 05 00 00 00 83 C0 03
Hợp ngữ (assembly): mov eax, 5 add eax, 3
Ngôn ngữ bậc cao (Java): int result = 5 + 3;Hợp ngữ (assembly) thay mã nhị phân bằng tên gợi nhớ (mov, add) nhưng vẫn phụ thuộc phần cứng. Ngôn ngữ bậc cao như Java, Python, C# gần với tư duy con người: bạn viết biểu thức, câu lệnh, lớp, hàm; công cụ dịch sẽ lo phần chuyển thành lệnh máy.
Biên dịch và thông dịch
Có hai cách chính để biến mã nguồn bậc cao thành thứ máy chạy được:
| Biên dịch (compile) | Thông dịch (interpret) | |
|---|---|---|
| Cách làm | Dịch toàn bộ chương trình thành mã đích một lần, rồi mới chạy | Đọc và thực thi từng câu lệnh ngay khi chạy |
| Ưu điểm | Chạy nhanh, lỗi cú pháp bị phát hiện sớm | Linh hoạt, dễ thử nghiệm nhanh |
| Nhược điểm | Mỗi lần sửa phải dịch lại | Thường chậm hơn |
| Ví dụ điển hình | C, C++, Go | Python, JavaScript (ban đầu) |
Java chọn con đường lai: trình biên dịch javac dịch mã nguồn thành một dạng trung gian gọi là bytecode, rồi máy ảo Java (JVM) nạp bytecode và thực thi, đồng thời biên dịch những đoạn "nóng" thành mã máy thật ngay lúc chạy (kỹ thuật JIT). Nhờ bước trung gian này, cùng một tệp .class chạy được trên Windows, macOS hay Linux miễn là máy có JVM.
Lưu ý: Đừng nhầm "bytecode" với "ngôn ngữ máy". Bytecode là ngôn ngữ máy của một cỗ máy giả tưởng (JVM), còn JVM cụ thể trên từng hệ điều hành mới là thứ dịch tiếp ra lệnh của CPU thật.
2. Tổng quan về Java
Lịch sử ngắn gọn
- 1991: nhóm của James Gosling tại Sun Microsystems bắt đầu dự án ngôn ngữ cho thiết bị điện tử gia dụng; ngôn ngữ ban đầu tên Oak.
- 1995: ngôn ngữ được đặt tên Java và công bố với khẩu hiệu "Write once, run anywhere" (viết một lần, chạy mọi nơi), nhanh chóng phổ biến cùng sự bùng nổ của web.
- 2006–2007: Sun mở mã nguồn Java, hình thành dự án OpenJDK; năm 2010 Oracle hoàn tất việc mua lại Sun.
- Từ Java 9 (2017): phát hành đều đặn 6 tháng một phiên bản, và cứ vài năm có một bản LTS (Long-Term Support) được hỗ trợ dài hạn.
Đặc điểm nổi bật
| Đặc điểm | Ý nghĩa với bạn |
|---|---|
| Hướng đối tượng | Chương trình tổ chức quanh các lớp và đối tượng (học kỹ từ buổi 5) |
| Độc lập nền tảng | Một bản bytecode, nhiều hệ điều hành |
| An toàn | Kiểm tra kiểu chặt, không có con trỏ thô, kiểm tra biên mảng |
| Bộ dọn rác tự động | Bạn không phải tự giải phóng bộ nhớ (garbage collector) |
| Đa luồng sẵn có | Hỗ trợ chạy nhiều luồng ngay trong ngôn ngữ (buổi 9) |
| Thư viện chuẩn lớn | Chuỗi, tập hợp, tệp, mạng, thời gian… đều có sẵn |
| Có kiểu tĩnh | Lỗi kiểu bị bắt ngay lúc biên dịch, không đợi đến khi chạy |
JDK, JRE và JVM
Ba chữ viết tắt này hay bị nhầm. Cách nhớ: JVM nằm trong JRE, JRE nằm trong JDK.
| Thành phần | Là gì | Gồm những gì |
|---|---|---|
| JVM (Java Virtual Machine) | Máy ảo thực thi bytecode | Bộ nạp lớp, vùng nhớ, bộ thực thi, bộ dọn rác |
| JRE (Java Runtime Environment) | Môi trường chạy chương trình | JVM + thư viện lớp chuẩn |
| JDK (Java Development Kit) | Bộ công cụ phát triển | JRE + javac, jar, javadoc, jshell, javap… |
Người viết chương trình cần cài JDK. Từ Java 11, hầu hết nhà phân phối không còn phát hành JRE riêng mà để JDK đảm nhiệm cả hai vai trò. Hai lệnh đầu tiên nên thử trong terminal để kiểm tra máy đã sẵn sàng:
java -version
javac -versionCác phiên bản và lựa chọn của khóa học
Các bản LTS thường gặp trong doanh nghiệp là Java 8, 11, 17, 21 (và bản LTS mới hơn ra mắt gần đây). Java 8 (2014) đưa vào lambda và Stream nên vẫn được dùng rất rộng rãi. Trong khóa này:
- Bạn có thể cài JDK mới nhất trên máy cá nhân (IntelliJ IDEA hoặc VS Code đều dùng được).
- Ô chạy thử trên Codelab và hệ thống chấm bài dùng Java 13, nên mọi ví dụ và bài tập chỉ dùng cú pháp từ Java 8 đến 13. Những tính năng mới hơn (như
record) sẽ được nhắc ở dạng đọc thêm.
Mẹo: Muốn thử nhanh một biểu thức mà không tạo tệp, hãy mở JShell (gõ
jshelltrong terminal, có từ Java 9). Gõ2 + 3rồi Enter là thấy kết quả ngay.
3. Kiến trúc máy ảo Java
JVM không phải một khối đơn lẻ mà gồm ba cụm chức năng chính:
- Class Loader (bộ nạp lớp): tìm và nạp các tệp
.classvào bộ nhớ khi chương trình cần, kiểm tra tính hợp lệ của bytecode. - Runtime Data Areas (vùng dữ liệu lúc chạy): nơi lưu mọi thứ chương trình dùng đến.
- Execution Engine (bộ thực thi): gồm trình thông dịch chạy từng lệnh bytecode, trình biên dịch JIT (Just-In-Time) biên dịch đoạn mã chạy lặp nhiều thành mã máy, và bộ dọn rác (Garbage Collector, GC) thu hồi đối tượng không còn ai dùng.
Phần quan trọng nhất với lập trình viên là các vùng nhớ. Hình dưới tóm tắt chúng, hãy đọc từ trái sang phải để thấy cái nào là của riêng từng luồng, cái nào dùng chung.
void main() {
int age = 20;
Student s = new Student("An");
greet(s); // p trong greet() cũng trỏ tới đối tượng đó
}Biến nguyên thủy lưu giá trị ngay trong ô ở stack; biến tham chiếu chỉ lưu địa chỉ, còn đối tượng thật nằm ở heap. Hai biến s và p cùng trỏ một đối tượng — không có bản sao.
| Vùng nhớ | Lưu gì | Phạm vi |
|---|---|---|
| Stack (ngăn xếp) | Mỗi lần gọi phương thức tạo một "khung" chứa biến cục bộ, tham số; ra khỏi phương thức thì khung bị hủy | Riêng từng luồng |
| Heap (đống) | Mọi đối tượng và mảng được tạo bằng new; GC dọn ở đây |
Dùng chung toàn chương trình |
| Method Area | Thông tin lớp, mã phương thức, hằng số, biến static |
Dùng chung |
| PC Register | Địa chỉ lệnh bytecode đang chạy | Riêng từng luồng |
Hãy xem một ví dụ nhỏ để thấy vì sao phải phân biệt Stack và Heap. Biến a kiểu int nằm thẳng trong stack; còn arr và alias chỉ là hai "tay cầm" trong stack cùng trỏ tới một mảng trên heap, nên sửa qua tay cầm này sẽ thấy ở tay cầm kia.
int a = 5;
int b = a;
b = 99;
int[] arr = {1, 2, 3};
int[] alias = arr;
alias[0] = 99;
System.out.println("a = " + a);
System.out.println("arr[0] = " + arr[0]);Kết quả a = 5 (đổi b không ảnh hưởng a vì giá trị được sao chép) nhưng arr[0] = 99 (hai tham chiếu cùng một mảng). Chi tiết này sẽ quay lại trong phần kiểu tham chiếu.
Ví dụ thực tế: Hãy nghĩ heap như kho hàng của cửa hàng, còn stack là tờ phiếu ghi "hàng nằm ở kệ số mấy" mà mỗi nhân viên cầm trên tay. Hai nhân viên cầm hai phiếu cùng kệ thì ai lấy bớt hàng, người kia cũng thấy kho vơi đi.
Lưu ý: Java không có cách nào "free" bộ nhớ thủ công. Khi một đối tượng không còn biến nào tham chiếu tới, GC sẽ tự thu hồi vào thời điểm thích hợp. Bạn có thể quên chuyện rò rỉ bộ nhớ kiểu C/C++, nhưng vẫn phải cẩn thận không giữ tham chiếu thừa.
4. Biên dịch, thực thi và chương trình Hello World
Từ mã nguồn tới kết quả
Quy trình hoàn chỉnh gồm hai bước chính: biên dịch bằng javac và thực thi bằng java.
println("Hello");CA FE BA BE …javac biên dịch mã nguồn thành bytecode (.class) — không phụ thuộc máy. Mỗi hệ điều hành có một JVM riêng đọc cùng một bytecode, nên 'viết một lần, chạy mọi nơi'.
Đọc hình từ trái sang phải: tệp mã nguồn .java được javac kiểm tra cú pháp, kiểu dữ liệu rồi sinh ra tệp bytecode .class; lệnh java khởi động JVM, nạp tệp .class và cho trình thông dịch/JIT biến bytecode thành lệnh máy của CPU hiện tại.
Chương trình đầu tiên
Dưới đây là chương trình Hello World phiên bản căng-tin. Hãy bấm Chạy thử rồi thử đổi nội dung chuỗi.
public class Main {
public static void main(String[] args) {
System.out.println("Xin chào, Java!");
}
}Từng phần của chương trình:
| Thành phần | Ý nghĩa |
|---|---|
public class Main |
Khai báo lớp tên Main. Mọi mã Java đều nằm trong lớp. Lớp public phải nằm trong tệp trùng tên (Main.java) |
public static void main(String[] args) |
Điểm vào của chương trình, JVM tìm đúng chữ ký này để bắt đầu chạy |
System.out.println(...) |
In một dòng ra màn hình rồi xuống dòng |
{ ... } |
Khối lệnh; mỗi lệnh kết thúc bằng dấu ; |
"Xin chào, Java!" |
Một chuỗi ký tự (String) |
Ý nghĩa của từng từ trong dòng main: public (ai cũng gọi được), static (thuộc về lớp, không cần tạo đối tượng), void (không trả giá trị), String[] args (các đối số dòng lệnh).
Biên dịch và chạy bằng dòng lệnh
Lưu đoạn mã trên thành Main.java rồi chạy trong terminal:
javac Main.java
java MainLệnh javac sinh ra tệp Main.class; lệnh java Main (không có đuôi) chạy lớp Main. Từ Java 11 bạn còn có thể chạy thẳng tệp nguồn bằng java Main.java. Để xem bytecode, dùng javap -c Main; phần thân main trông như sau:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Xin chào, Java!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: returnBốn lệnh bytecode: lấy đối tượng System.out, nạp hằng chuỗi, gọi phương thức println, rồi kết thúc. Bạn không cần thuộc bytecode, chỉ cần biết nó tồn tại và là đầu vào của JVM.
Chú thích và quy ước đặt tên
// Chú thích một dòng
/* Chú thích
nhiều dòng */
/** Chú thích tài liệu (JavaDoc) — sẽ học ở buổi 4 */Quy ước đặt tên giúp mã dễ đọc: lớp theo PascalCase (Invoice), biến và phương thức theo camelCase (totalPrice), hằng theo UPPER_SNAKE_CASE (VAT_RATE). Java phân biệt chữ hoa/thường: Main khác main.
Cảnh báo: Lỗi hay gặp nhất của người mới là quên dấu
;, sai chữ hoa/thường (system.outthay vìSystem.out), hoặc đặt tên tệp khác tên lớppublic. Trình biên dịch báo lỗi ngay ở bướcjavac, và thông báo thường chỉ rõ dòng lỗi. Hãy đọc dòng đầu tiên của thông báo trước.
5. Biến và hằng
Biến là gì
Biến (variable) là một ô nhớ có tên dùng để lưu dữ liệu. Java là ngôn ngữ có kiểu tĩnh: mỗi biến phải khai báo kiểu và kiểu đó không đổi suốt đời biến. Cú pháp:
kiểu tênBiến = giá trị; // khai báo + khởi tạo
kiểu tênBiến; // chỉ khai báo
tênBiến = giá trị; // gán (về sau)Ví dụ sau tính hóa đơn một món ở căng-tin: ba biến thông thường và một hằng thuế.
String itemName = "Cà phê sữa";
int quantity = 3;
double unitPrice = 25000.0;
final double VAT_RATE = 0.08;
double subtotal = quantity * unitPrice;
double total = subtotal * (1 + VAT_RATE);
System.out.println(itemName + " x" + quantity);
System.out.println("Tạm tính: " + subtotal);
System.out.println("Thành tiền: " + total);Chú ý toán tử + giữa chuỗi và số sẽ nối chuỗi: itemName + " x" + quantity cho ra Cà phê sữa x3.
Hằng với final
Từ khóa final biến một biến thành hằng: gán đúng một lần, sau đó cố sửa sẽ bị trình biên dịch từ chối. Hằng giúp bạn tránh "số ma thuật" (magic number): thay vì rải con số 0.08 khắp chương trình, hãy đặt một tên có nghĩa và chỉ sửa tại một chỗ khi thuế đổi.
final double VAT_RATE = 0.08;
VAT_RATE = 0.1; // LỖI biên dịch: cannot assign a value to final variableQuy tắc đặt tên
| Quy tắc | Đúng | Sai |
|---|---|---|
Bắt đầu bằng chữ cái, _ hoặc $ |
total, _temp |
2nd, #id |
Gồm chữ, số, _, $ |
unitPrice2 |
unit-price, unit price |
| Không trùng từ khóa | count |
class, int, new |
| Phân biệt hoa/thường | Total và total là hai biến |
|
| Nên viết camelCase, tên có nghĩa | studentCount |
x1, sc |
Tên biến và tên lớp trong khóa này dùng tiếng Anh để khớp thư viện chuẩn; chú thích và chuỗi in ra có thể tiếng Việt.
Từ khóa var và biến cục bộ
Từ Java 10, với biến cục bộ bạn có thể viết var và để trình biên dịch tự suy ra kiểu từ giá trị khởi tạo. Biến vẫn có kiểu tĩnh, chỉ là bạn không phải gõ.
var count = 10;
var price = 19.5;
var title = "Java";
System.out.println(count + " " + price + " " + title);
final int MAX_SEATS = 40;
int a = 1, b = 2;
System.out.println(MAX_SEATS + a + b);Ở đây count là int, price là double, title là String. Dòng cuối còn cho thấy bạn có thể khai báo nhiều biến cùng kiểu trên một dòng. Biến cục bộ phải được gán giá trị trước khi dùng, nếu không javac báo lỗi "variable might not have been initialized".
Mẹo: Dùng
varkhi kiểu đã rõ ngay từ vế phải (var list = new ArrayList<String>();), tránh dùng khi nó làm người đọc phải đoán kiểu. Với người mới, cứ khai báo kiểu tường minh để luyện tư duy về kiểu.
Lưu ý: Biến chỉ tồn tại trong khối lệnh (cặp
{ }) chứa nó. Ra khỏi khối thì tên biến không còn dùng được. Khái niệm phạm vi này sẽ được học kỹ ở buổi 2.
6. Kiểu dữ liệu nguyên thủy và kiểu tham chiếu
Java chia kiểu dữ liệu thành hai nhóm lớn: kiểu nguyên thủy (primitive) lưu trực tiếp giá trị, và kiểu tham chiếu (reference) lưu địa chỉ của một đối tượng trên heap.
Tám kiểu nguyên thủy
| Kiểu | Kích thước | Phạm vi / giá trị | Dùng cho |
|---|---|---|---|
byte |
8 bit | -128 đến 127 | Dữ liệu thô, tệp nhị phân |
short |
16 bit | -32.768 đến 32.767 | Ít dùng |
int |
32 bit | khoảng ±2,1 tỷ | Số nguyên mặc định |
long |
64 bit | khoảng ±9,2 × 10^18 | Số nguyên lớn, thời gian, tiền (đơn vị nhỏ nhất) |
float |
32 bit | khoảng ±3,4 × 10^38, ~7 chữ số chính xác | Số thực độ chính xác thấp |
double |
64 bit | khoảng ±1,8 × 10^308, ~15 chữ số chính xác | Số thực mặc định |
char |
16 bit | 0 đến 65.535 (một ký tự Unicode) | Một ký tự |
boolean |
không cố định | true hoặc false |
Điều kiện đúng/sai |
Hình sau cho thấy các kiểu số xếp theo "độ rộng", cùng chiều mũi tên của phép ép kiểu mở rộng và thu hẹp. Hãy nhìn kiểu nhỏ nằm trọn trong kiểu lớn: đó là lý do ép từ nhỏ lên lớn thì an toàn.
int n = 'A'; // 65 long big = n; // int → long double d = big; // long → double
int a = (int) 3.9; // 3 (cắt phần thập phân) byte b = (byte) 130; // -126 (tràn số)
Bậc thang không theo đúng tỉ lệ. Chiều đi từ nhỏ sang lớn (mũi tên xanh) tự động; chiều ngược lại (mũi tên cam) phải ép kiểu tay và có thể mất dữ liệu. Lưu ý float chỉ 32 bit nhưng phạm vi lớn hơn long (đổi lại kém chính xác).
Biểu đồ dưới so sánh kích thước bit của từng kiểu, giúp bạn thấy long và double chiếm gấp đôi int và float.
Chiều dài cột tỉ lệ với số bit. Kích thước và phạm vi của các kiểu nguyên thủy được quy định cố định trong đặc tả ngôn ngữ, không phụ thuộc hệ điều hành — riêng boolean chỉ có hai giá trị nên kích thước thực do JVM quyết định.
Chương trình sau in ra giới hạn của các kiểu số. Hãy nhớ các con số này ở mức "cỡ chục", đặc biệt int chỉ tới khoảng 2,1 tỷ.
System.out.println("byte : " + Byte.MIN_VALUE + " .. " + Byte.MAX_VALUE);
System.out.println("short: " + Short.MIN_VALUE + " .. " + Short.MAX_VALUE);
System.out.println("int : " + Integer.MIN_VALUE + " .. " + Integer.MAX_VALUE);
System.out.println("long : " + Long.MIN_VALUE + " .. " + Long.MAX_VALUE);
System.out.println("float max : " + Float.MAX_VALUE);
System.out.println("double max: " + Double.MAX_VALUE);Cách viết hằng số (literal)
Giá trị viết trực tiếp trong mã có kiểu mặc định: số nguyên là int, số thực là double. Muốn kiểu khác phải thêm hậu tố:
| Cách viết | Kiểu | Ghi chú |
|---|---|---|
42, 0xFF, 0b1010 |
int |
thập phân, thập lục phân, nhị phân |
8_100_000_000L |
long |
hậu tố L, dấu _ cho dễ đọc |
3.14, 1.5e3 |
double |
1.5e3 nghĩa là 1500.0 |
3.14f |
float |
hậu tố f |
'A', 'A' |
char |
dấu nháy đơn, hỗ trợ mã Unicode |
"Java" |
String |
dấu nháy kép |
long population = 8_100_000_000L;
int hex = 0xFF;
int bin = 0b1010;
float rate = 3.14f;
double sci = 1.5e3;
char letter = 'A';
boolean open = true;
System.out.println(population + " " + hex + " " + bin);
System.out.println(rate + " " + sci + " " + letter + " " + open);Cảnh báo:
long big = 8100000000;sẽ lỗi biên dịch vì literal8100000000được coi làintmà vượt phạm vi. Phải viết8100000000L. Tương tựfloat f = 3.14;lỗi vì3.14làdouble.
Kiểu char thực chất là số
char lưu mã Unicode của ký tự, nên bạn có thể làm toán với nó. Đây là chìa khóa của nhiều bài xử lý chữ cái, ví dụ đổi hoa thành thường chỉ bằng cộng 32.
char c = 'A';
System.out.println(c);
System.out.println(c + 1);
System.out.println((char) (c + 1));
c++;
System.out.println(c);
System.out.println((int) 'a');
System.out.println('a' - 'A');c + 1 cho 66 vì khi cộng, char được nâng lên int; muốn ra chữ cái phải ép lại bằng (char). Còn c++ giữ nguyên kiểu char nên in ra B.
Số thực không chính xác tuyệt đối
Số thực được lưu theo chuẩn nhị phân nên nhiều số thập phân như 0,1 không biểu diễn chính xác. Hệ quả là sai số nhỏ khi tính toán, và không bao giờ nên so sánh hai số thực bằng ==.
System.out.println(0.1 + 0.2);
System.out.println(0.1f + 0.2f);
System.out.println(0.1 + 0.2 == 0.3);
System.out.println(Math.abs(0.1 + 0.2 - 0.3) < 1e-9);
System.out.println(1.0 / 0);
System.out.println(0.0 / 0);Dòng đầu in 0.30000000000000004. Cách đúng để so sánh là kiểm tra hiệu hai số nhỏ hơn một ngưỡng sai số (dòng 4). Chia số thực cho 0 không ném lỗi mà cho Infinity hoặc NaN (not a number).
Ví dụ thực tế: Với tiền tệ, đừng lưu bằng
double. Hãy dùnglongđể lưu số đồng (hoặc xu), hoặc lớpBigDecimalở các hệ thống tài chính thật. Ví dụ25000 * 3bằnglongluôn cho đúng75000.
Kiểu tham chiếu
Mọi kiểu còn lại — String, mảng, và các lớp bạn tự viết hay lấy từ thư viện — đều là kiểu tham chiếu. Biến tham chiếu không chứa đối tượng mà chứa địa chỉ của nó trên heap; đối tượng nào chưa có thì biến mang giá trị null.
| Kiểu nguyên thủy | Kiểu tham chiếu | |
|---|---|---|
| Lưu cái gì | Chính giá trị | Địa chỉ của đối tượng trên heap |
| Giá trị mặc định (thuộc tính) | 0, false, '\0' |
null |
Gán b = a |
Sao chép giá trị | Sao chép địa chỉ (cùng một đối tượng) |
So sánh == |
So giá trị | So địa chỉ (có phải cùng đối tượng) |
| Có phương thức không | Không | Có (s.length()) |
| Ví dụ | int, double, char |
String, int[], Integer |
String s = null;
System.out.println(s);
System.out.println("Độ dài: " + (s == null ? 0 : s.length()));
String t = "Java";
System.out.println(t.length() + " " + t.toUpperCase());Gọi s.length() khi s là null sẽ ném NullPointerException — một trong những lỗi phổ biến nhất của Java; vì vậy ví dụ trên kiểm tra s == null trước.
Lớp bao (wrapper) và so sánh bằng ==
Mỗi kiểu nguyên thủy có một lớp bao tương ứng: Integer, Double, Character, Boolean, Long… Chúng cho phép đưa số vào các cấu trúc chỉ chứa đối tượng (như danh sách ở buổi 7). Java tự động chuyển qua lại (autoboxing/unboxing), nhưng chính điều đó tạo ra một cái bẫy với ==:
Integer a = 127, b = 127;
Integer c = 1000, d = 1000;
System.out.println(a == b);
System.out.println(c == d);
System.out.println(c.equals(d));
int p = 1000;
System.out.println(p == c);Kết quả true, false, true, true. Java lưu sẵn (cache) các Integer từ -128 đến 127 nên a == b cùng đối tượng; với 1000 thì tạo hai đối tượng khác nhau nên c == d là false, trong khi equals so sánh giá trị. Khi một vế là int (dòng cuối), Java tự mở hộp nên so sánh giá trị.
Cảnh báo: Với mọi kiểu tham chiếu (kể cả
Integer,String), hãy so sánh giá trị bằngequals, chỉ dùng==khi thật sự muốn hỏi "có phải cùng một đối tượng không". Buổi 2 sẽ quay lại vớiString.
Chọn kiểu cho dữ liệu thực tế
| Dữ liệu | Kiểu nên dùng | Lý do |
|---|---|---|
| Số lượng món, tuổi, điểm danh | int |
Nhỏ, nguyên |
| Số dư tài khoản (đồng) | long |
Có thể vượt 2,1 tỷ |
| Điểm trung bình, chiều cao | double |
Có phần thập phân |
| Trạng thái đã thanh toán | boolean |
Hai giá trị |
| Mã loại thẻ (một chữ cái) | char |
Một ký tự |
| Họ tên, số điện thoại, mã sinh viên | String |
Không tính toán, có thể bắt đầu bằng 0 |
7. Vào/ra dữ liệu: System.out, printf và Scanner
Chương trình hữu ích phải giao tiếp với người dùng: nhận dữ liệu vào và đưa kết quả ra. Với chương trình dòng lệnh, đầu ra chuẩn là System.out, đầu vào chuẩn là System.in (bàn phím hoặc dữ liệu được chuyển hướng vào). Trên Codelab, bài tập của bạn đọc từ đầu vào chuẩn và ghi ra đầu ra chuẩn, hệ thống chấm so khớp kết quả.
Đầu ra: print, println, printf
| Phương thức | Hành vi |
|---|---|
System.out.print(x) |
In x, không xuống dòng |
System.out.println(x) |
In x rồi xuống dòng |
System.out.printf(fmt, ...) |
In theo chuỗi định dạng, không tự xuống dòng (dùng %n) |
String.format(fmt, ...) |
Giống printf nhưng trả về chuỗi thay vì in |
System.err.println(x) |
In ra luồng lỗi chuẩn (tách biệt với kết quả chính) |
Chuỗi định dạng của printf chứa chữ thường và các đặc tả bắt đầu bằng %:
| Đặc tả | Ý nghĩa | Ví dụ printf |
Kết quả |
|---|---|---|---|
%d |
số nguyên | ("%d", 42) |
42 |
%5d / %-5d |
rộng 5, căn phải / căn trái | ("%5d", 42) |
42 |
%05d |
độn số 0 | ("%05d", 42) |
00042 |
%,d |
dấu phân cách hàng nghìn | ("%,d", 1234567) |
1,234,567 |
%f / %.2f |
số thực / làm tròn 2 chữ số thập phân | ("%.2f", 3.456) |
3.46 |
%s |
chuỗi (hoặc đối tượng bất kỳ) | ("%s", "An") |
An |
%c |
một ký tự | ("%c", 'J') |
J |
%b |
boolean | ("%b", true) |
true |
%x |
thập lục phân | ("%x", 255) |
ff |
%n |
xuống dòng (đúng cho mọi hệ điều hành) | ||
%% |
in dấu % |
("%d%%", 15) |
15% |
String name = "An";
int age = 20;
double gpa = 3.456;
System.out.print("Xin chào ");
System.out.println(name);
System.out.printf("Tên: %s, tuổi: %d, GPA: %.2f%n", name, age, gpa);
System.out.printf("|%5d|%-5d|%05d|%n", 42, 42, 42);
System.out.printf("|%8.3f|%,d|%n", Math.PI, 1234567);
System.out.println(String.format("%x %o %c %b", 255, 8, 'J', true));Dòng %.2f làm tròn 3.456 thành 3.46. Mỗi đặc tả được thay bằng đối số tương ứng theo thứ tự, và kiểu đặc tả phải khớp kiểu đối số (đưa số thực cho %d sẽ gây lỗi lúc chạy).
Mẹo: Muốn in dấu phần trăm thật, viết
%%. Muốn chắc chắn dấu thập phân luôn là dấu chấm bất kể cấu hình ngôn ngữ của máy, dùngString.format(Locale.US, "%.2f", x).
Nối chuỗi và thứ tự cộng
Khi dùng + để in, Java tính từ trái sang phải: hai số cộng với nhau trước, nhưng ngay khi gặp một chuỗi thì phần còn lại bị nối thành chuỗi. Lỗi này hay làm bạn in ra 85 thay vì 13.
String s = "Điểm: " + 8 + 5;
System.out.println(s);
System.out.println("Tổng: " + (8 + 5));
System.out.println(8 + 5 + " là tổng");
char ch = 'x';
System.out.println("" + ch + 1);
System.out.println(ch + 1 + "");Kết quả: Điểm: 85, Tổng: 13, 13 là tổng, x1, 121. Dòng cuối cho 121 vì ch + 1 cộng số trước (mã của x là 120, cộng 1 thành 121) rồi mới nối chuỗi rỗng.
Đầu vào: lớp Scanner
Scanner (gói java.util) đọc và tách dữ liệu từ nguồn đầu vào. Chương trình đầy đủ cần dòng import java.util.Scanner; đứng đầu tệp (ô chạy thử của Codelab đã thêm sẵn các import quen thuộc).
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Nhập tên: ");
String name = sc.nextLine();
System.out.print("Nhập tuổi: ");
int age = sc.nextInt();
System.out.println("Chào " + name + ", năm sau bạn " + (age + 1) + " tuổi.");
}
}Hai dòng // @stdin ngay dưới // @run là dữ liệu "gõ" vào bàn phím giả: dòng đầu cho nextLine(), dòng sau cho nextInt(). Các phương thức hay dùng:
| Phương thức | Đọc gì |
|---|---|
nextInt(), nextLong(), nextDouble(), nextBoolean() |
Một token (chuỗi liền, ngăn bởi khoảng trắng) rồi chuyển thành số/boolean |
next() |
Một token kiểu String |
nextLine() |
Cả dòng còn lại tới ký tự xuống dòng (kể cả dấu cách) |
hasNext(), hasNextInt(), hasNextLine() |
Kiểm tra còn dữ liệu (đọc tới hết đầu vào) |
Ví dụ đọc họ tên có dấu cách, tuổi và chiều cao trên ba dòng, đúng kiểu bài "Thẻ giới thiệu":
Scanner sc = new Scanner(System.in);
String fullName = sc.nextLine();
int age = sc.nextInt();
double height = sc.nextDouble();
System.out.println(fullName + " - " + age + " tuổi - " + height + " m");Cái bẫy kinh điển: nextInt() rồi nextLine()
nextInt() chỉ đọc số, bỏ lại ký tự xuống dòng trong bộ đệm. Nếu ngay sau đó bạn gọi nextLine() thì nó đọc đúng phần thừa đó và trả về chuỗi rỗng.
Scanner sc = new Scanner(System.in);
int age = sc.nextInt();
String name = sc.nextLine();
System.out.println("[" + name + "]");Kết quả in [] — tên bị "nuốt mất". Có hai cách sửa thường dùng: gọi thêm một sc.nextLine() để bỏ phần thừa, hoặc luôn đọc cả dòng rồi tự chuyển kiểu:
Scanner sc = new Scanner(System.in);
int age = Integer.parseInt(sc.nextLine().trim());
String name = sc.nextLine();
System.out.println(name + " (" + age + ")");Integer.parseInt chuyển chuỗi thành int; tương tự có Double.parseDouble, Long.parseLong. Chuỗi không phải số (ví dụ "abc") sẽ ném NumberFormatException.
Đọc đến hết dữ liệu và trộn token
Khi đề bài không cho biết trước số lượng dữ liệu, dùng vòng lặp với hasNextInt(). Hàm này không quan tâm số nằm trên mấy dòng.
Scanner sc = new Scanner(System.in);
int sum = 0;
while (sc.hasNextInt()) {
sum += sc.nextInt();
}
System.out.println("Tổng = " + sum);Vòng while sẽ học kỹ ở buổi 2; ở đây bạn chỉ cần hiểu nó lặp "chừng nào còn số để đọc". Cuối cùng, ví dụ trộn next() và nextLine() trên cùng một dòng:
Scanner sc = new Scanner(System.in);
String w1 = sc.next();
String w2 = sc.next();
String rest = sc.nextLine();
String line2 = sc.nextLine();
System.out.println(w1 + "|" + w2 + "|" + rest.trim() + "|" + line2);next() lấy java, la; nextLine() đầu lấy phần còn lại của dòng 1 (" ngon ngu"); nextLine() sau lấy trọn dòng 2.
Cảnh báo:
Scannerphụ thuộc cấu hình vùng (locale) của máy khi đọc số thực: máy đặt tiếng Việt có thể chỉ chấp nhận1,72thay vì1.72. Nếu cần đọc dấu chấm cho chắc, gọisc.useLocale(Locale.US);ngay sau khi tạoScanner. Trên hệ thống chấm của Codelab, dữ liệu luôn dùng dấu chấm.
Lưu ý: Với bài tập có lượng dữ liệu rất lớn,
Scannerchậm; ở buổi 8 bạn sẽ họcBufferedReadernhanh hơn. Trong buổi nàyScannerlà đủ.
Bạn đã có đủ công cụ để in lời chào, đọc tên, tuổi, chiều cao rồi in thẻ giới thiệu. Hãy thử ngay.
Vừa học xong System.out, printf và Scanner, hãy làm các bài trên để quen với việc nhận dữ liệu đúng định dạng và in kết quả khớp từng ký tự (kể cả dấu cách, dấu chấm).
8. Ép kiểu và tràn số
Ép kiểu mở rộng (widening) — tự động
Khi gán giá trị của kiểu "nhỏ" cho biến kiểu "lớn" hơn, Java tự chuyển mà không mất dữ liệu. Thứ tự mở rộng:
byte → short → int → long → float → double, và char → int.
int i = 100;
long l = i;
double d = i;
float f = l;
System.out.println(l + " " + d + " " + f);
char ch = 'Z';
int code = ch;
System.out.println(code);
long big = 123456789012L;
float fb = big;
System.out.println(fb);Lưu ý dòng cuối: long sang float là "mở rộng" nhưng float chỉ có khoảng 7 chữ số chính xác nên giá trị 123456789012 hiển thị thành 1.23456791E11 — bị mất độ chính xác dù không báo lỗi.
Ép kiểu thu hẹp (narrowing) — phải viết tường minh
Đi ngược chiều (từ lớn sang nhỏ) có thể mất dữ liệu, nên Java bắt bạn xác nhận bằng toán tử ép kiểu (kiểuĐích):
double pi = 3.99;
int truncated = (int) pi;
System.out.println(truncated);
System.out.println((int) -3.99);
System.out.println(Math.round(3.5) + " " + Math.round(-3.5) + " " + Math.round(3.49));
int x = 200;
byte b = (byte) x;
System.out.println(b);
short s = (short) 70000;
System.out.println(s);
System.out.println((int) 3.9e10);
System.out.println((int) Double.NaN);Đọc kết quả: ép số thực sang số nguyên cắt bỏ phần thập phân (hướng về 0, nên -3.99 thành -3), khác với Math.round làm tròn tới số nguyên gần nhất. Ép số nguyên sang kiểu hẹp hơn thì giữ lại các bit thấp: 200 không vừa byte nên thành -56, 70000 vào short thành 4464. Số thực quá lớn khi ép sang int bị "kẹp" ở Integer.MAX_VALUE, còn NaN thành 0.
| Biểu thức | Kết quả | Vì sao |
|---|---|---|
(int) 3.99 |
3 |
Cắt phần thập phân |
(int) -3.99 |
-3 |
Cắt về hướng 0 |
Math.round(3.5) |
4 |
Làm tròn gần nhất (nửa làm tròn lên) |
(byte) 200 |
-56 |
Giữ 8 bit thấp, bit cao nhất là dấu |
(char) 66 |
'B' |
Mã 66 là chữ B |
(double) 7 / 2 |
3.5 |
Ép 7 trước rồi chia số thực |
Hãy tự thử với mô phỏng dưới đây: kéo giá trị và quan sát điều gì xảy ra khi ép từ kiểu lớn xuống kiểu nhỏ.
Ép kiểu: mở rộng, thu hẹp và tràn số
Kéo giá trị int — xem bit nào bị cắt khi thu hẹp11001000₂ → -56 (bit 7 là bit dấu — màu vàng). Các bit đỏ gạch bị vứt bỏ nên giá trị thay đổi.Khi giá trị vượt phạm vi, các bit cao bị cắt, và nếu bit đầu tiên còn lại là 1 thì số trở thành âm. Đây chính là cơ chế của tràn số mà phần tiếp theo bàn tới.
Nâng kiểu trong biểu thức
Với phép toán hai ngôi trên số, Java tự nâng kiểu hai toán hạng về kiểu chung theo quy tắc:
- Nếu có
double→ cả hai thànhdouble. - Ngược lại nếu có
float→float. - Ngược lại nếu có
long→long. - Còn lại (
byte,short,char,int) →int.
byte a = 10, b = 20;
int sum = a + b;
System.out.println(sum);
long l = 5;
float f = l * 1.5f;
System.out.println(f);
System.out.println(5 / 2 * 2.0);
System.out.println(5 / 2.0 * 2);
char c = 'a';
System.out.println(c + 'b');
short s = 10;
s += 5;
System.out.println(s);Chú ý byte c = a + b; sẽ lỗi biên dịch vì a + b đã thành int. Hai dòng 5 / 2 * 2.0 và 5 / 2.0 * 2 cho 4.0 và 5.0: phép chia cho kết quả khác nhau tùy phép chia nào được tính trước — 5 / 2 là chia nguyên (bằng 2). Dòng s += 5 hợp lệ vì toán tử gán phức hợp tự ép kiểu về kiểu của vế trái.
Tràn số (overflow)
Mỗi kiểu số nguyên có phạm vi hữu hạn. Khi kết quả vượt quá phạm vi, Java không báo lỗi mà "quay vòng": sau Integer.MAX_VALUE là Integer.MIN_VALUE, giống đồng hồ cơ chỉ đến 12 rồi về lại 1.
int max = Integer.MAX_VALUE;
System.out.println(max + 1);
System.out.println(max + 1 == Integer.MIN_VALUE);
byte b = 127;
b++;
System.out.println(b);
int big = 100000;
System.out.println(big * big);
System.out.println((long) big * big);
System.out.println(big * (long) big);
try {
Math.addExact(max, 1);
} catch (ArithmeticException e) {
System.out.println("Bắt được: " + e.getMessage());
}Bốn điều rút ra:
big * bigra1410065408(sai!) vì phép nhânint * inttính bằngintrồi mới gán kết quả; phải ép một toán hạng sanglongtrước khi nhân ((long) big * big).- Ép cả kết quả sau khi nhân, kiểu
(long) (big * big), vẫn sai vì tràn đã xảy ra rồi. - Các hàm
Math.addExact,subtractExact,multiplyExactnémArithmeticExceptionkhi tràn, dùng khi bạn muốn phát hiện thay vì im lặng sai. - Số thực không tràn kiểu "quay vòng" mà tiến tới
Infinity.
Ví dụ thực tế: Một hệ thống đếm lượt truy cập bằng
intsẽ đột ngột báo số âm sau khoảng 2,1 tỷ lượt. Đó là lý do ID và tổng tiền thường dùnglong.
Mẹo: Nghi ngờ tràn thì tính trung gian bằng
long(hoặc kiểu lớn hơn), và nhớ rằng thứ tự ép kiểu quyết định kết quả: ép trước khi tính, không phải sau.
Đến đây bạn đã nắm kiểu dữ liệu, ép kiểu và tràn số. Phần luyện tập sẽ chính là những cái bẫy vừa thấy.
Vừa học xong ép kiểu và tràn số, hãy làm các bài trên để thấy tận mắt (int), (byte), (short) và int + int hoạt động ra sao với dữ liệu thật; chú ý bài nào cần dùng long để tránh tràn.
9. Toán tử số học, gán và tăng/giảm
Toán tử số học
| Toán tử | Ý nghĩa | Ví dụ | Kết quả |
|---|---|---|---|
+ |
Cộng (hoặc nối chuỗi) | 7 + 2 |
9 |
- |
Trừ | 7 - 2 |
5 |
* |
Nhân | 7 * 2 |
14 |
/ |
Chia (nguyên nếu cả hai là số nguyên) | 7 / 2 |
3 |
% |
Chia lấy dư | 7 % 2 |
1 |
Điểm khác biệt quan trọng nhất so với toán học: chia hai số nguyên cho kết quả nguyên (cắt phần lẻ, hướng về 0); chỉ cần một toán hạng là số thực thì phép chia mới ra số thực. Còn % trả về dấu theo số bị chia.
System.out.println(7 / 2);
System.out.println(7 % 2);
System.out.println(-7 / 2);
System.out.println(-7 % 2);
System.out.println(7 / 2.0);
System.out.println(7 % 2.5);
int a = 7, b = 2;
System.out.println((double) a / b);
System.out.println((double) (a / b));
System.out.println(Math.floorMod(-7, 3));Hai dòng cuối cùng của phần (double) cho 3.5 và 3.0: ép kiểu a trước khi chia thì ra số thực, còn ép sau khi chia nguyên thì đã mất phần lẻ. Dòng cuối dùng Math.floorMod để lấy số dư luôn không âm (2).
Bộ đôi / và % là công cụ rất mạnh để tách số: đổi đơn vị, lấy chữ số, quy đổi thời gian. Ví dụ đổi số ngày ra tuần và ngày lẻ, hoặc tách chữ số của một số:
int days = 100;
int weeks = days / 7;
int remain = days % 7;
System.out.println(weeks + " tuần " + remain + " ngày");
int n = 4827;
System.out.println(n % 10);
System.out.println(n / 10);
System.out.println(n / 10 % 10);
System.out.println(n % 10 + n / 10 % 10);Chữ số hàng đơn vị là n % 10; bỏ chữ số cuối là n / 10; chữ số hàng chục là n / 10 % 10. Mẫu "chia lấy nguyên, chia lấy dư" này sẽ quay lại hầu hết các bài tính toán.
Chia cho 0 và các bẫy chia
Chia số nguyên cho 0 ném ArithmeticException ("/ by zero"), còn chia số thực cho 0 cho Infinity hoặc NaN mà không lỗi. Một bẫy khác là tính trung bình:
int a = 7, b = 8;
System.out.println((a + b) / 2);
System.out.println((a + b) / 2.0);
System.out.println((double) (a + b) / 2);
System.out.println((double) a + b / 2);Kết quả 7, 7.5, 7.5, 11.0. Dòng cuối là sai vì b / 2 đã chia nguyên (4) trước, rồi mới cộng với 7.0. Quy tắc gọn: muốn ra số thực, cho ít nhất một toán hạng là số thực trước phép chia, và dùng ngoặc cho rõ.
Lớp Math
Lớp Math có sẵn các hàm toán học tĩnh (gọi trực tiếp, không cần tạo đối tượng):
| Hàm | Ý nghĩa | Ví dụ |
|---|---|---|
Math.sqrt(x) |
Căn bậc hai | Math.sqrt(2) cho 1.4142135623730951 |
Math.pow(x, y) |
Lũy thừa, kết quả double |
Math.pow(3, 4) cho 81.0 |
Math.abs(x) |
Giá trị tuyệt đối | Math.abs(-7) cho 7 |
Math.max(a, b), Math.min(a, b) |
Lớn nhất, nhỏ nhất | Math.max(4, 9) cho 9 |
Math.ceil(x), Math.floor(x) |
Làm tròn lên, xuống (kết quả double) |
Math.ceil(3.2) cho 4.0 |
Math.round(x) |
Làm tròn gần nhất (kết quả long/int) |
Math.round(2.5) cho 3 |
System.out.println(Math.sqrt(2));
System.out.println(Math.pow(3, 4));
System.out.println(Math.ceil(7 / 2.0) + " " + Math.floor(-2.5));
System.out.println(Math.max(4, 9) + " " + Math.min(-1, 3) + " " + Math.abs(-7));
System.out.println(Math.round(2.567 * 100) / 100.0);
int a = 17, b = 5;
System.out.println((a + b - 1) / b);Hai kỹ thuật đáng nhớ trong ví dụ: làm tròn tới 2 chữ số thập phân bằng Math.round(x * 100) / 100.0, và chia làm tròn lên với số nguyên bằng (a + b - 1) / b (ví dụ cần bao nhiêu xe 5 chỗ cho 17 người: 4 xe), không cần dùng số thực.
Cảnh báo:
%với số âm cho kết quả âm:-7 % 3là-1, không phải2. Nếu cần số dư không âm, dùngMath.floorMod(a, b)hoặc công thức((a % b) + b) % b.
Toán tử gán và gán phức hợp
Toán tử gán = ghi giá trị vế phải vào biến vế trái, và bản thân nó là biểu thức nên có thể gán dây chuyền a = b = c = 4. Gán phức hợp (+=, -=, *=, /=, %=) viết gọn x = x + y thành x += y, và tự ép về kiểu của biến bên trái.
Tăng và giảm: ++ và --
++ tăng biến 1 đơn vị, -- giảm 1. Vị trí đặt toán tử quyết định giá trị của biểu thức:
| Dạng | Tên | Làm gì |
|---|---|---|
i++ |
hậu tố (postfix) | Lấy giá trị hiện tại dùng trong biểu thức, rồi mới tăng |
++i |
tiền tố (prefix) | Tăng trước, rồi dùng giá trị mới |
int n = 10;
n += 5;
n -= 3;
n *= 2;
n /= 5;
n %= 3;
System.out.println(n);
byte b = 10;
b += 5;
System.out.println(b);
int i = 5;
int j = i++;
int k = ++i;
System.out.println(i + " " + j + " " + k);
int x = 3;
int y = x++ + ++x;
System.out.println(x + " " + y);Theo dõi n: 10 → 15 → 12 → 24 → 4 → 4 % 3 = 1. Với j = i++: j nhận 5 rồi i thành 6; với k = ++i: i thành 7 rồi k nhận 7; vậy i = 7, j = 5, k = 7. Biểu thức x++ + ++x ra 3 + 5 = 8, x cuối là 5.
Mẹo: Dùng
++/--như một câu lệnh riêng (count++;) thì không có gì rối. Đừng nhồi nhiều++vào cùng một biểu thức nhưx++ + ++x: ngôn ngữ vẫn tính được, nhưng người đọc sẽ không hiểu, và đây chỉ là bài toán ví dụ trong lớp.
Toán tử bit (đọc thêm)
Nếu cần làm việc ở mức bit, Java có các toán tử trên số nguyên: & (AND), | (OR), ^ (XOR), ~ (NOT), << (dịch trái), >> (dịch phải có dấu), >>> (dịch phải không dấu).
int a = 12, b = 10;
System.out.println(a & b);
System.out.println(a | b);
System.out.println(a ^ b);
System.out.println(~a);
System.out.println(a << 2);
System.out.println(-16 >> 2);
System.out.println(-16 >>> 28);
System.out.println(Integer.toBinaryString(12) + " " + Integer.toBinaryString(10));Với 12 là 1100 và 10 là 1010 trong nhị phân: AND cho 1000 (8), OR cho 1110 (14), XOR cho 0110 (6). Dịch trái n bit tương đương nhân với 2^n. Phần này không bắt buộc cho các bài của buổi nhưng xuất hiện nhiều trong kho bài tập nâng cao.
Bạn đã đủ vốn để làm các bài tính toán số học trên số nguyên và số thực.
Vừa học xong toán tử số học, / và %, hãy làm các bài trên để luyện tách số, quy đổi đơn vị, tính trung bình và dùng Math; hãy tự hỏi ở mỗi phép chia: đây là chia nguyên hay chia thực?
10. So sánh, logic, toán tử ba ngôi và thứ tự ưu tiên
Toán tử so sánh
Toán tử so sánh trả về giá trị boolean: == (bằng), != (khác), lớn hơn là >, nhỏ hơn là <, cùng >= và <=.
int a = 7, b = 7;
System.out.println(a == b);
System.out.println(a != b);
System.out.println(a >= 8);
boolean ok = a > 5 && b < 10;
boolean any = a < 0 || b == 7;
System.out.println(ok + " " + any + " " + !ok);
System.out.println(true ^ true);Đừng nhầm = (gán) với == (so sánh). Với số thực, tránh == vì sai số làm tròn; với kiểu tham chiếu (String, Integer) dùng equals. Java không cho viết 1 < v < 10: phải tách thành 1 < v && v < 10.
Toán tử logic
Các toán tử này kết hợp giá trị boolean:
&&(và): đúng khi cả hai vế đúng.||(hoặc): đúng khi ít nhất một vế đúng.!(phủ định): đảo đúng thành sai và ngược lại.^(hoặc loại trừ): đúng khi hai vế khác nhau.&và|(một ký tự) cũng làm việc trênbooleannhưng luôn tính cả hai vế.
| a | b | a && b |
a ^ b |
!a |
|---|---|---|---|---|
| true | true | true | false | false |
| true | false | false | true | false |
| false | true | false | true | true |
| false | false | false | false | true |
Đoản mạch (short-circuit)
Với &&, nếu vế trái sai thì kết quả chắc chắn sai nên vế phải không được tính. Với ||, nếu vế trái đúng thì vế phải bỏ qua. Đây không chỉ là tối ưu mà còn là cách viết điều kiện an toàn: kiểm tra mẫu số khác 0 trước khi chia.
int d = 0;
System.out.println(d != 0 && 10 / d > 1);
System.out.println(d == 0 || 10 / d > 1);
int count = 0;
boolean r = (count++ > 5) && (count++ > 5);
System.out.println(r + " " + count);
boolean r2 = (count++ > 5) & (count++ > 5);
System.out.println(r2 + " " + count);Hai dòng đầu không ném lỗi chia cho 0 vì vế phải không được tính. Với &&, count++ chỉ chạy một lần (count = 1); với & thì cả hai vế đều chạy (count tăng thêm 2, thành 3).
Lưu ý: Điều kiện kiểu
x != null && x.length() > 0an toàn nhờ đoản mạch: nếuxlànullthì vế sau không bao giờ chạy. Đảo thứ tự hai vế sẽ gâyNullPointerException.
Toán tử ba ngôi
Dạng điều_kiện ? giá_trị_nếu_đúng : giá_trị_nếu_sai là một biểu thức chọn một trong hai giá trị. Có thể lồng nhau nhưng nên dừng ở một cấp để dễ đọc.
int score = 7;
String result = score >= 5 ? "Đạt" : "Trượt";
System.out.println(result);
int a = 12, b = 30, c = 21;
int max = a > b ? (a > c ? a : c) : (b > c ? b : c);
System.out.println(max);
int n = -4;
System.out.println(n > 0 ? "dương" : n < 0 ? "âm" : "không");Thứ tự ưu tiên và tính kết hợp
Khi biểu thức có nhiều toán tử, Java tính theo mức ưu tiên từ cao xuống thấp; cùng mức thì theo tính kết hợp (thường từ trái sang phải). Danh sách rút gọn:
- Hậu tố
i++,i-- - Một ngôi:
++i,--i,+,-,!,~, ép kiểu(type) - Nhân chia:
*,/,% - Cộng trừ:
+,- - Dịch bit:
<<,>>,>>> - So sánh thứ tự:
<,>,<=,>= - So sánh bằng:
==,!= &, rồi^, rồi|(theo bit/logic không đoản mạch)&&||- Ba ngôi
? : - Gán:
=,+=,-=, … (kết hợp từ phải sang trái)
System.out.println(2 + 3 * 4);
System.out.println((2 + 3) * 4);
System.out.println(10 - 4 - 3);
System.out.println(100 / 10 / 5);
System.out.println(2 + 3 + "a" + 2 + 3);
System.out.println(1 + 2 * 3 % 4 - 5 / 2);
int x = 5;
x += x++ * 2;
System.out.println(x);
boolean t = 1 + 1 == 2 || 3 > 5 && false;
System.out.println(t);Giải thích vài dòng: 10 - 4 - 3 ra 3 (trái sang phải); 2 + 3 + "a" + 2 + 3 ra 5a23, vì 2 + 3 được tính thành số 5 trước khi gặp chuỗi; 1 + 2 * 3 % 4 - 5 / 2 tính 2 * 3 % 4 thành 2 rồi 1 + 2 - 2, bằng 1. && có ưu tiên cao hơn ||, nên dòng cuối là true || (...).
Mẹo: Đừng cố nhớ toàn bộ bảng ưu tiên. Hãy dùng dấu ngoặc để thể hiện ý định rõ ràng:
(a + b) * c,(x > 0) && (y > 0). Mã đọc dễ hơn và bạn khỏi phụ thuộc vào trí nhớ.
Vừa học xong so sánh, logic, toán tử ba ngôi và thứ tự ưu tiên, hãy làm các bài trên để luyện viết điều kiện đúng và đoán chính xác kết quả của biểu thức nhiều toán tử.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
Quên ;, sai hoa/thường (system.out), tên tệp khác tên lớp public |
Lỗi biên dịch, chương trình không chạy | Đọc kỹ dòng lỗi đầu tiên; đặt tên tệp trùng tên lớp |
Dùng 7 / 2 rồi mong ra 3.5 |
Kết quả 3, mất phần lẻ |
Cho một toán hạng là số thực: 7 / 2.0 hoặc (double) 7 / 2 |
int * int có thể vượt 2,1 tỷ |
Kết quả âm hoặc sai im lặng | Ép sang long trước khi nhân: (long) a * b |
nextInt() rồi nextLine() |
Dòng đọc được là chuỗi rỗng | Đọc cả dòng rồi Integer.parseInt, hoặc gọi thêm một nextLine() |
So sánh số thực bằng == |
0.1 + 0.2 == 0.3 cho false |
So sánh hiệu với ngưỡng sai số nhỏ |
So sánh Integer/String bằng == |
1000 == 1000 có thể false |
Dùng equals cho kiểu tham chiếu |
Dùng double lưu tiền |
Sai số làm tròn tích lũy | Dùng long (đơn vị nhỏ nhất) hoặc BigDecimal |
Quên L khi viết số lớn: long x = 8100000000; |
Lỗi biên dịch "integer number too large" | Viết 8100000000L |
-7 % 3 mong ra 2 |
Ra -1 |
Dùng Math.floorMod khi cần số dư không âm |
"Tổng: " + a + b |
Nối chuỗi chứ không cộng số | Thêm ngoặc: "Tổng: " + (a + b) |
Nhồi nhiều ++ vào một biểu thức |
Khó đọc, dễ ra kết quả bất ngờ | Tách thành nhiều câu lệnh |
Tóm tắt
| Chủ đề | Điều cần nhớ |
|---|---|
| Java chạy thế nào | .java → javac → .class (bytecode) → JVM thực thi; JVM ⊂ JRE ⊂ JDK |
| JVM | Class Loader + vùng nhớ (Stack, Heap, Method Area) + bộ thực thi (JIT, GC) |
| Chương trình đầu tiên | Lớp public trùng tên tệp, điểm vào public static void main(String[] args) |
| Biến và hằng | Khai báo kiểu rõ ràng; final cho hằng; var suy ra kiểu; camelCase / UPPER_SNAKE_CASE |
| Kiểu dữ liệu | 8 kiểu nguyên thủy (lưu giá trị) và kiểu tham chiếu (lưu địa chỉ, có thể null) |
| Ép kiểu | Mở rộng tự động; thu hẹp phải viết (type) và có thể mất dữ liệu |
| Tràn số | int quay vòng sau 2,1 tỷ; dùng long hoặc Math.addExact |
| Vào/ra | printf/String.format để định dạng; Scanner đọc token và dòng, cẩn thận nextInt rồi nextLine |
| Toán tử | / chia nguyên giữa số nguyên, % lấy dư; && và toán tử hoặc đều đoản mạch; ?: chọn giá trị |
| Thứ tự ưu tiên | Nhân chia trước cộng trừ; gặp nghi ngờ thì thêm ngoặc |