Một variable trong Java là một cái hộp có tên và có type cố định. Type được quyết định lúc compile, được javac kiểm tra, và nó trả lời hai câu hỏi khác nhau: hộp đó chứa được những giá trị nào, và giá trị thật sự nằm ở đâu trong bộ nhớ khi chương trình chạy.
Java chỉ có đúng hai họ type. Tám type primitive lưu giá trị trực tiếp bên trong variable. Mọi thứ còn lại là reference type: variable lưu địa chỉ của một object nằm trên heap. Phần lớn lỗi của người mới ở chủ đề này đến từ việc không biết một variable thuộc họ nào.
![]()
Mọi con số, dòng output và message lỗi bên dưới đều lấy từ việc compile và chạy code thật trên OpenJDK 21.0.6.
Khai báo và khởi tạo một variable
Cú pháp là type name = value;. Type đứng trước, và không được bỏ.
public class VariableDemo {
public static void main(String[] args) {
int itemCount = 3;
double unitPrice = 19.99;
boolean inStock = true;
char grade = 'A';
String productName = "USB-C cable";
System.out.println(productName + " x" + itemCount);
System.out.println("unit price: " + unitPrice);
System.out.println("in stock: " + inStock + ", grade: " + grade);
}
}
USB-C cable x3
unit price: 19.99
in stock: true, grade: A
Java là ngôn ngữ static typing: compiler biết type của mọi expression trước khi chương trình chạy, và từ chối mọi thứ không khớp. Nhét một String vào một int không phải là bất ngờ lúc runtime, mà là lỗi ngay lúc build.
int age = "twenty";
TypeErr.java:3: error: incompatible types: String cannot be converted to int
int age = "twenty";
^
1 error
Chính lớp kiểm tra này làm cho việc refactor một codebase Java lớn trở nên an toàn. Tất nhiên vẫn có cách chuyển đổi giữa các type tương thích — widening, narrowing và explicit cast có bộ quy tắc riêng, sẽ nói ở một bài sau — nhưng không có phép chuyển đổi ngầm nào được phép làm mất thông tin.
Quy tắc đặt tên (identifier): tên được chứa chữ cái, chữ số, _ và $, không được bắt đầu bằng chữ số, và không được trùng keyword. Cả int 2fast lẫn int class đều hỏng ngay khi parse.
Convention: camelCase cho variable và method, PascalCase cho class, UPPER_SNAKE_CASE cho hằng số. Không có gì bắt buộc, nhưng mọi codebase Java và mọi người review code đều mặc định như vậy.
8 kiểu dữ liệu primitive trong Java
Tám primitive được cố định bởi language specification, nên các range bên dưới giống hệt nhau trên mọi JVM và mọi platform. Đây là lựa chọn có chủ đích: khác với C, Java không để phần cứng quyết định một int rộng bao nhiêu.
| Type | Số bit | Range | Default |
|---|---|---|---|
byte | 8 | -128 đến 127 | 0 |
short | 16 | -32768 đến 32767 | 0 |
int | 32 | -2147483648 đến 2147483647 | 0 |
long | 64 | -9223372036854775808 đến 9223372036854775807 | 0L |
float | 32 | 1.4E-45 đến 3.4028235E38, cả âm lẫn dương | 0.0f |
double | 64 | 4.9E-324 đến 1.7976931348623157E308, cả âm lẫn dương | 0.0d |
char | 16 | 0 đến 65535, không dấu | '\u0000' |
boolean | không quy định | true hoặc false | false |
Không con số nào ở trên được viết theo trí nhớ. Mọi giới hạn đều là một hằng số bạn in ra được:
public class Limits {
public static void main(String[] args) {
System.out.println("byte " + Byte.SIZE + " bits " + Byte.MIN_VALUE + " .. " + Byte.MAX_VALUE);
System.out.println("short " + Short.SIZE + " bits " + Short.MIN_VALUE + " .. " + Short.MAX_VALUE);
System.out.println("int " + Integer.SIZE + " bits " + Integer.MIN_VALUE + " .. " + Integer.MAX_VALUE);
System.out.println("long " + Long.SIZE + " bits " + Long.MIN_VALUE + " .. " + Long.MAX_VALUE);
System.out.println("float " + Float.SIZE + " bits " + Float.MIN_VALUE + " .. " + Float.MAX_VALUE);
System.out.println("double " + Double.SIZE + " bits " + Double.MIN_VALUE + " .. " + Double.MAX_VALUE);
System.out.println("char " + Character.SIZE + " bits " + (int) Character.MIN_VALUE + " .. " + (int) Character.MAX_VALUE);
}
}
byte 8 bits -128 .. 127
short 16 bits -32768 .. 32767
int 32 bits -2147483648 .. 2147483647
long 64 bits -9223372036854775808 .. 9223372036854775807
float 32 bits 1.4E-45 .. 3.4028235E38
double 64 bits 4.9E-324 .. 1.7976931348623157E308
char 16 bits 0 .. 65535
Output đó cho thấy rõ hai chi tiết.
MIN_VALUE mang nghĩa khác đối với các type dấu phẩy động. Integer.MIN_VALUE là số int âm nhất, nhưng Double.MIN_VALUE là 4.9E-324, tức giá trị dương nhỏ nhất khác 0. Số double âm nhất là -Double.MAX_VALUE:
Double.MIN_VALUE = 4.9E-324
-Double.MAX_VALUE = -1.7976931348623157E308
boolean không được quy định kích thước. Không có hằng số Boolean.SIZE nào để in, và hỏi tới nó thì không compile được:
BoolSize.java:3: error: cannot find symbol
System.out.println(Boolean.SIZE);
^
symbol: variable SIZE
location: class Boolean
1 error
JLS chỉ định nghĩa boolean qua hai giá trị của nó và để phần biểu diễn cho từng JVM implementation, nên câu "một boolean là một bit" là truyền miệng chứ không phải quy tắc.
Literal: cách viết một giá trị trong source code
Integer literal và cái bẫy hệ octal
Cùng một int có thể viết ở bốn hệ cơ số. Ba trong số đó hữu ích, và một là cái bẫy.
int dec = 42;
int hex = 0xFF;
int bin = 0b1010_1010;
int oct = 012;
dec = 42
0xFF = 255
0b1010_1010 = 170
012 = 10
⚠️ Số 0 đứng đầu nghĩa là octal, không phải "số thập phân tình cờ bắt đầu bằng 0".
012là 10 chứ không phải 12. Cái bẫy này chờ sẵn bất cứ ai thêm số 0 cho thẳng hàng, hoặc dán thẳng một mã tháng, mã bưu chính hay số tài khoản có số 0 ở đầu vào source code.
Dấu gạch dưới, hậu tố L và hậu tố f
Dấu gạch dưới được phép đặt giữa các chữ số và bị compiler xóa đi, nên 1_000_000 đúng bằng 1000000. Nó dùng được ở mọi hệ cơ số, và đó là lý do 0b1010_1010 dễ đọc.
Một integer literal luôn là int trừ khi bạn thêm L. Đây là tính chất của bản thân literal chứ không phải của variable bên trái, nên đổi type bên trái cho rộng hơn cũng không cứu được:
long big = 10000000000;
TooLarge.java:3: error: integer number too large
long big = 10000000000;
^
1 error
10000000000 được parse thành int trước khi có ai nhìn tới vế trái, và nó không vừa 32 bit. Viết 10_000_000_000L thì compile được và in ra 10000000000.
Literal dấu phẩy động thì ngược lại: một literal thập phân luôn là double trừ khi bạn thêm f. Gán một double vào float sẽ mất độ chính xác, nên compiler từ chối:
float f = 3.14;
LossyFloat.java:3: error: incompatible types: possible lossy conversion from double to float
float f = 3.14;
^
1 error
Viết 3.14f. Hậu tố không phân biệt hoa thường, nhưng hãy dùng L viết hoa: 10l viết thường gần như không phân biệt được với 101.
char literal và unicode escape
Một char literal là một ký tự đặt trong nháy đơn. Nháy kép tạo ra String, một type hoàn toàn khác.
char a = 'A';
char e = 'ế';
char tab = '\t';
System.out.println("a = " + a + " (" + (int) a + ")");
System.out.println("e = " + e + " (" + (int) e + ")");
System.out.println("tab code = " + (int) tab);
System.out.println("char + char = " + ('A' + 'B'));
a = A (65)
e = ế (7871)
tab code = 9
char + char = 131
char là số nguyên 16 bit không dấu, nên 'A' + 'B' là phép cộng trên 65 và 66 và cho ra int 131. Unicode escape cũng là literal: 'A' in ra A còn 'ế' in ra ế. Các escape sequence như '\n' và '\t' cũng chỉ là một ký tự, mã 10 và 9.
Chọn type nào trong thực tế
Mặc định dùng int và double

Nguyên nhân nằm ở cách lưu trữ, không phải ở phép cộng:
Đừng tối ưu vặt ngay ở dòng khai báo. byte và short không tiết kiệm được gì trên một local variable — JVM vẫn mở rộng chúng thành một slot 32 bit trên stack — mà lại làm code khó đọc và bắt bạn cast gần như ở mọi phép gán. Chúng chỉ đáng dùng trong array lớn và trong binary protocol, không phải trong code thông thường.
| Trường hợp | Type |
|---|---|
| Đếm, kích thước, chỉ số vòng lặp, số nguyên thông thường | int |
| Id trong database, epoch timestamp, số byte vượt 2 GB | long |
| Số thập phân thông thường, số đo, giá trị khoa học | double |
| Tiền, giá, thuế, mọi thứ người dùng sẽ đối soát | BigDecimal |
| Một cờ bật/tắt | boolean |
| Array lớn chứa giá trị nhỏ, dữ liệu nhị phân thô | byte, short |
Dùng long cho timestamp không phải là chuyện sở thích. Một timestamp tính bằng millisecond đã vượt Integer.MAX_VALUE từ tháng 1 năm 1970 và không bao giờ quay lại:
long now = System.currentTimeMillis();
System.out.println("currentTimeMillis fits in int? " + (now <= Integer.MAX_VALUE));
System.out.println("value has " + String.valueOf(now).length() + " digits");
currentTimeMillis fits in int? false
value has 13 digits
Không bao giờ dùng float hay double cho tiền
float và double là dấu phẩy động nhị phân. Những giá trị như 0.1 và 0.42 không có biểu diễn nhị phân chính xác, nên thứ được lưu lại lệch đi một chút, và sai số lộ ra ngay khi bạn cộng hoặc trừ.
System.out.println(0.1 + 0.2);
System.out.println(1.03 - 0.42);
System.out.println(0.1 + 0.2 == 0.3);
0.30000000000000004
0.6100000000000001
false
Đây không phải bug của Java. Đây là IEEE 754, và mọi ngôn ngữ dùng dấu phẩy động của phần cứng đều hành xử y hệt. Bạn có thể nhìn thấy chính xác thứ đã được lưu bằng cách đưa double đó cho BigDecimal:
System.out.println(new BigDecimal(0.1));
0.1000000000000000055511151231257827021181583404541015625
Cách xử lý cho tiền là BigDecimal dựng từ một String. new BigDecimal(0.1) chép nguyên giá trị nhị phân đã lệch vào; new BigDecimal("0.1") lưu đúng các chữ số thập phân bạn viết ra.
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Cart {
public static void main(String[] args) {
double dTotal = 0.1 * 3;
System.out.println("double : " + dTotal);
BigDecimal total = new BigDecimal("0.10").multiply(new BigDecimal("3"));
System.out.println("BigDecimal: " + total);
BigDecimal subtotal = new BigDecimal("19.99").multiply(new BigDecimal("3"));
BigDecimal tax = subtotal.multiply(new BigDecimal("0.08"))
.setScale(2, RoundingMode.HALF_UP);
System.out.println("subtotal = " + subtotal + ", tax = " + tax);
}
}
double : 0.30000000000000004
BigDecimal: 0.30
subtotal = 59.97, tax = 4.80
Cách còn lại cũng được chấp nhận rộng rãi là lưu tiền dưới dạng long đếm theo đơn vị nhỏ nhất — cent, hoặc đồng — và chỉ format lại khi hiển thị. Cả hai cách đều ổn. double thì không.
Integer overflow: chuyện gì xảy ra khi giá trị không vừa
Một int có đúng 32 bit, không hơn. Khi kết quả cần tới bit thứ 33, Java không throw và cũng không tự nâng type. Nó giữ lại 32 bit thấp và đi tiếp với một kết quả sai.

int max = Integer.MAX_VALUE;
System.out.println("Integer.MAX_VALUE = " + max);
System.out.println("Integer.MAX_VALUE + 1 = " + (max + 1));
System.out.println("Integer.MIN_VALUE = " + Integer.MIN_VALUE);
Integer.MAX_VALUE = 2147483647
Integer.MAX_VALUE + 1 = -2147483648
Integer.MIN_VALUE = -2147483648
Nhìn vào bit thì rõ. Integer.MAX_VALUE là một sign bit bằng 0 rồi tới 31 bit 1. Cộng thêm 1 làm mọi bit 1 đó thành 0 và carry chạy vào sign bit, giờ sign bit bằng 1 — mà trong bù hai, sign bit bằng 1 nghĩa là số âm.
Phiên bản thật sự lên production là một phép nhân mà từng thừa số trông đều vô hại:
int msPerDay = 24 * 60 * 60 * 1000;
System.out.println("ms per day = " + msPerDay);
int msPerYear = 365 * 24 * 60 * 60 * 1000;
System.out.println("ms per year (int) = " + msPerYear);
long msPerYearL = 365L * 24 * 60 * 60 * 1000;
System.out.println("ms per year (long) = " + msPerYearL);
ms per day = 86400000
ms per year (int) = 1471228928
ms per year (long) = 31536000000
Để ý chỗ đặt chữ L. 365L * 24 * ... nâng cả chuỗi phép nhân lên long. Viết long msPerYear = 365 * 24 * 60 * 60 * 1000; thì compile sạch sẽ và vẫn sai, vì phép nhân diễn ra trong int và chỉ có kết quả đã hỏng mới được mở rộng.
Khi không thể chấp nhận sự im lặng đó, Math.addExact, Math.multiplyExact và các method cùng họ sẽ throw thay vì wrap:
System.out.println(Math.addExact(max, 1));
Exception in thread "main" java.lang.ArithmeticException: integer overflow
at java.base/java.lang.Math.addExact(Math.java:911)
at Overflow.main(Overflow.java:16)
Reference type: String, array và mọi class
Mọi thứ không thuộc tám primitive đều là reference type: String, array, mọi class trong JDK, và mọi class bạn tự viết. Một reference variable không chứa object. Nó chứa một reference tới object đang nằm trên heap.

Với một local variable, bản thân variable nằm trong stack frame của method hiện tại và bị bỏ đi khi method return. Local variable kiểu primitive giữ giá trị ngay tại chỗ đó trong frame. Local variable kiểu reference giữ một địa chỉ, còn object mà nó gọi tên thì nằm hẳn ở nơi khác trên heap, sống lâu hơn cái frame nếu vẫn còn thứ gì trỏ tới nó.
Chính khác biệt đó quyết định = làm gì.
int count = 42;
int copy = count;
copy = 99;
System.out.println("count = " + count + ", copy = " + copy);
List<String> a = new ArrayList<>();
a.add("Java");
List<String> b = a;
b.add("21");
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("a == b -> " + (a == b));
b = new ArrayList<>();
b.add("other");
System.out.println("after b = new: a = " + a + ", b = " + b);
count = 42, copy = 99
a = [Java, 21]
b = [Java, 21]
a == b -> true
after b = new: a = [Java, 21], b = [other]
Gán một primitive là copy giá trị, nên sửa copy không đụng tới count. Gán một reference là copy reference, nên a và b trở thành hai cái tên của cùng một object và thay đổi qua tên nào cũng nhìn thấy được từ tên kia. Gán lại b chỉ trỏ b sang chỗ khác, không ảnh hưởng gì tới object cũ hay tới a. Array cũng vậy, vì array là một object:
int[] nums = {1, 2, 3};
int[] alias = nums;
alias[0] = 100;
System.out.println("nums[0] = " + nums[0]);
nums[0] = 100
Một reference cũng có thể không trỏ tới đâu cả. null là giá trị hợp lệ cho mọi reference type và không hợp lệ cho bất kỳ primitive nào. Dereference nó — gọi method hoặc đọc field qua nó — sẽ throw:
String name = null;
System.out.println(name.length());
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
at Npe.main(Npe.java:4)
Từ Java 14, và bật mặc định từ Java 15 trở đi, đây là helpful NullPointerException: nó gọi tên method không gọi được và đúng expression đang null, nhiều hơn hẳn cái NullPointerException trống trơn mà các tutorial cũ hay cho xem. Có một chi tiết đáng biết vì nó hay làm người compile bằng tay bối rối: tên của một local variable lấy từ debug information trong file class. Chạy javac Npe.java bình thường thì message là because "<local1>" is null; chạy javac -g Npe.java thì ra đúng tên như trên. Field thì luôn có tên — một static field in ra because "Npe2.name" is null mà không cần thêm flag nào.
Default value: field thì có, local variable thì không
Một field khai báo mà không khởi tạo sẽ nhận default value của type đó. Đây là bảo đảm của ngôn ngữ, không phải may rủi.

Khai báo ở hai bên là như nhau — chỉ khác ở chuyện gì xảy ra khi bạn đọc nó.
public class Defaults2 {
static int count;
static double rate;
static boolean active;
static char grade;
static String name;
static int[] scores;
public static void main(String[] args) {
System.out.println("int " + count);
System.out.println("double " + rate);
System.out.println("boolean " + active);
System.out.println("char code " + (int) grade);
System.out.println("String " + name);
System.out.println("int[] " + scores);
}
}
int 0
double 0.0
boolean false
char code 0
String null
int[] null
Field kiểu số mặc định bằng 0, boolean bằng false, char bằng code point 0, và mọi reference type — kể cả array — bằng null.
Local variable không được hưởng điều đó. Thay vào đó compiler áp dụng definite assignment: nó chứng minh mọi local variable đều được ghi trước khi được đọc, và từ chối chương trình nếu không chứng minh được.
int x;
System.out.println(x);
NotInit.java:4: error: variable x might not have been initialized
System.out.println(x);
^
1 error
Đây là tính năng chứ không phải sự thiếu nhất quán. Một field âm thầm mặc định về 0 có thể giấu một bug suốt nhiều tháng; một local variable từ chối mặc định biến đúng bug đó thành lỗi build.
var: local variable type inference từ Java 10
Java 10 thêm var. Nó không phải dynamic typing, và cũng không phải var của JavaScript: compiler suy ra một type cố định từ phần khởi tạo, và type đó chốt luôn cho tới hết đời của variable.
var count = 42; // int
var name = "Java"; // String
var rate = 0.075; // double
var items = new ArrayList<String>(); // ArrayList<String>
for (var s : items) System.out.println(s); // String
Vì type đến từ phần khởi tạo, var chỉ dùng được ở nơi có phần khởi tạo. Cả bốn trường hợp sau đều không compile được:
var x = null;
VarNull.java:3: error: cannot infer type for local variable x
var x = null;
^
(variable initializer is 'null')
1 error
var x;
x = 5;
VarNoInit.java:3: error: cannot infer type for local variable x
var x;
^
(cannot use 'var' on variable without initializer)
1 error
public class VarField {
var count = 10;
}
VarField.java:2: error: 'var' is not allowed here
var count = 10;
^
1 error
static void show(var x) { }
VarParam.java:2: error: 'var' is not allowed here
static void show(var x) {
^
1 error
Tóm lại: chỉ dùng cho local variable, biến của vòng for và resource trong try-with-resources. Không dùng cho field, không cho parameter, không cho return type, và không cho null.
Về style: dùng var khi vế phải đã nói rõ type, kiểu var scanner = new Scanner(System.in);, và viết hẳn type ra khi vế phải không nói gì, vì var result = process(input); chẳng cho người đọc biết điều gì. Dễ đọc tại chỗ dùng quan trọng hơn việc gõ ít ký tự.
final và hằng số
final nghĩa là variable chỉ được gán đúng một lần. Gán lại là lỗi compile, và đó chính là mục đích — nó cho người đọc quyền ngừng theo dõi giá trị đó.
final int limit = 10;
limit = 20;
FinalBad.java:4: error: cannot assign a value to final variable limit
limit = 20;
^
1 error
Một hằng số có tên là static final với tên viết UPPER_SNAKE_CASE, khai báo một lần và dùng ở mọi nơi:
static final double TAX_RATE = 0.08;
static final int MAX_RETRIES = 3;
Phiên bản hai giây của lưu ý quan trọng: final khóa variable, không khóa object. Một reference final vĩnh viễn không trỏ sang object khác được, còn object nó đang trỏ tới thì vẫn thay đổi thoải mái.
final List<String> names = new ArrayList<>();
names.add("Hoang");
names.add("Lan");
System.out.println(names);
[Hoang, Lan]
Thêm phần tử vào một final List vẫn compile và chạy bình thường. Chỉ có names = new ArrayList<>(); mới hỏng, với đúng message cannot assign a value to final variable ở trên. Nếu bạn cần khóa cả nội dung thì đó là tính chất của type — một record, một unmodifiable collection, một class không có setter — chứ không phải của keyword.
Wrapper class và autoboxing
Mỗi primitive đều có một wrapper class: int và Integer, long và Long, double và Double, boolean và Boolean, char và Character, cứ thế. Wrapper là reference type, nên chúng có thể là null và có thể nằm trong collection, còn primitive thì không. Autoboxing chuyển đổi giữa hai bên tự động, nên cả Integer boxed = 42; lẫn int raw = boxed; đều compile được.

Chính cái cache này làm cùng một phép so sánh đảo kết quả:
Hai method trông như thay thế được cho nhau nhưng không phải. Integer.parseInt("42") trả về primitive int; Integer.valueOf("42") trả về một object Integer. Dùng parseInt khi bạn cần một con số, và chỉ dùng valueOf khi bạn thật sự cần object.
Giờ tới cái bẫy. Vì Integer là object nên == so sánh reference chứ không so sánh giá trị — mà JDK lại cache các giá trị nhỏ, nên câu trả lời phụ thuộc vào con số:
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println("a == b (127) -> " + (a == b));
System.out.println("c == d (128) -> " + (c == d));
System.out.println("c.equals(d) -> " + c.equals(d));
a == b (127) -> true
c == d (128) -> false
c.equals(d) -> true
Autoboxing đi qua Integer.valueOf, và method này trả về object đã cache cho mọi giá trị trong khoảng -128 đến 127. Hai cái hộp chứa 127 đúng là cùng một object, nên == cho true. Hai cái hộp chứa 128 là hai object khác nhau, nên == cho false. Giá trị chẳng thay đổi gì cả. Quy tắc rút ra là tuyệt đối: so sánh wrapper bằng equals.
Cái bẫy còn lại của wrapper là unbox một null, biến một phép gán trông vô hại thành NPE:
Integer boxed = null;
int raw = boxed;
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "boxed" is null
at UnboxNull.main(UnboxNull.java:4)
Message gọi đúng tên thao tác thật: unboxing là một lời gọi boxed.intValue() ẩn, và gọi method trên null thì throw. Đó là lý do một cột database cho phép NULL phải đọc vào Integer rồi kiểm tra, không bao giờ đọc thẳng vào int.
Scope của variable trong một đoạn ngắn
Một variable chỉ tồn tại bên trong block khai báo nó — cặp ngoặc nhọn bao quanh nó — và biến mất tại dấu ngoặc đóng, nên biến đếm khai báo trong for (int i = 0; ...) không còn nữa sau vòng lặp. Dùng lại cái tên đó sau đấy không phải lỗi runtime mà là lỗi compile, cannot find symbol, vì tại điểm đó trong chương trình cái tên ấy đơn giản là không tồn tại.
FAQ
String có phải kiểu primitive trong Java không?
Không. Java có đúng tám primitive type và String không nằm trong đó. String là một class thuộc java.lang, nên một variable String giữ reference tới một object trên heap và có thể là null. Nó được ngôn ngữ ưu ái đặc biệt — có cú pháp literal riêng và dùng được toán tử + — nên rất hay bị nhầm là primitive.
int và Integer khác nhau thế nào?
int là primitive: nó giữ trực tiếp một giá trị 32 bit, không bao giờ null được, và không bỏ vào List được. Integer là class bọc giá trị đó: nó là object trên heap, có thể null, và dùng được với generic và collection. Cứ dùng int trừ khi bạn thật sự cần giá trị null hoặc cần bỏ vào collection, và hãy so sánh Integer bằng equals thay vì ==.
Vì sao 0.1 + 0.2 không bằng 0.3 trong Java?
Vì double là dấu phẩy động nhị phân và 0.1 không có biểu diễn chính xác ở hệ cơ số 2, đúng như một phần ba không có biểu diễn chính xác ở hệ cơ số 10. Giá trị được lưu lệch đi một chút, nên tổng in ra là 0.30000000000000004. Đây là hành vi của IEEE 754, giống hệt trong C, Python và JavaScript. Hãy dùng BigDecimal dựng từ String, hoặc đếm theo đơn vị nhỏ nhất, mỗi khi kết quả bắt buộc phải chính xác.
Có nên dùng var ở mọi nơi không?
Không. var đáng dùng khi vế phải đã nói rõ type và gây hại khi nó giấu type đi. var users = new ArrayList<User>(); thì rõ ràng; var x = load(); đẩy người đọc vào IDE để tra xem x là gì. var cũng không dùng được cho field, parameter hay return type, nên nó không bao giờ xóa bỏ việc phải viết type trong một API.
final có làm object trở thành immutable không?
Không. final chỉ ngăn variable bị gán lại và không nói gì về object. Một final List vẫn thêm, xóa, thay phần tử được; chỉ có ràng buộc giữa cái tên và đúng object đó là bị đóng băng. Muốn dữ liệu immutable thì dùng record, unmodifiable collection, hoặc một class không có setter.
Local variable trong Java có giá trị mặc định là gì?
Không có. Field được khởi tạo mặc định về 0, false, code point 0 hoặc null, nhưng local variable thì hoàn toàn không được khởi tạo, và compiler từ chối mọi chương trình có khả năng đọc nó trước khi ghi, với message variable x might not have been initialized. Luôn gán giá trị cho local variable trước khi dùng.
Kết luận
Hai quy tắc gánh phần lớn bài này. Thứ nhất, primitive variable giữ giá trị còn reference variable giữ địa chỉ — chỉ một khác biệt đó giải thích được = copy cái gì, vì sao == trên wrapper lại gây bất ngờ, và null thật ra là gì. Thứ hai, mọi type đều có giới hạn cứng, nên int wrap trong im lặng, double không biểu diễn được 0.1, và cả hai đều là vấn đề của bạn chứ không phải của JVM.
Phần còn lại là thói quen: mặc định int và double, long cho id và timestamp, BigDecimal cho tiền, equals cho wrapper, final cho thứ không nên thay đổi, và var chỉ ở nơi nó làm dòng code dễ đọc hơn.
Bài tiếp theo trong series: operator trong Java — nhóm arithmetic, comparison, logical, assignment và toán tử ternary, kèm phép chia số nguyên, phần dư của số âm, short-circuit evaluation, và khác biệt giữa =, == và equals.