Chuyển tới nội dung chính

Kiểu dữ liệu và Biến

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Phân biệt được 8 primitive types và reference types trong Java
  • Hiểu sự khác nhau giữa cách lưu trữ primitive và reference trong memory (stack vs heap)
  • Sử dụng final keyword cho constants
  • Hiểu variable scope (local, instance, class) và lifetime của biến

Bài trước: Cú pháp cơ bản Java — Đã học về naming conventions và cấu trúc chương trình. Bài này sẽ tìm hiểu các kiểu dữ liệu và cách khai báo biến trong Java.

Java là strongly typed language - mọi variable phải có type rõ ràng. Bài học này sẽ giới thiệu các kiểu dữ liệu trong Java và cách làm việc với biến.

Kiểu dữ liệu trong Java

Java có 2 loại kiểu dữ liệu:

  1. Primitive types (8 types) - Lưu giá trị trực tiếp
  2. Reference types - Lưu địa chỉ (reference) của object
┌─────────────────────────────────┐
│ Java Data Types │
├────────────────┬────────────────┤
│ Primitive │ Reference │
├────────────────┼────────────────┤
│ - byte │ - Classes │
│ - short │ - Interfaces │
│ - int │ - Arrays │
│ - long │ - Enums │
│ - float │ │
│ - double │ │
│ - char │ │
│ - boolean │ │
└────────────────┴────────────────┘

Primitive Types (8 types)

Primitive types lưu trữ giá trị trực tiếp trong memory, nhanh và hiệu quả.

1. Integer Types (Số nguyên)

TypeSizeRangeDefault ValueExample
byte1 byte (8 bits)-128 to 1270byte age = 25;
short2 bytes (16 bits)-32,768 to 32,7670short year = 2024;
int4 bytes (32 bits)-2³¹ to 2³¹-1 (~-2B to 2B)0int population = 1000000;
long8 bytes (64 bits)-2⁶³ to 2⁶³-10Llong distance = 9460730472580800L;

Two's Complement — Tại sao byte range là -128 to 127?

📖 Theo JLS §4.2.1 (Integral Types)

Java dùng two's complement representation cho tất cả integer types.

Two's complement là cách biểu diễn số nguyên có dấu (signed integers) trong binary.

Byte (8 bits) breakdown:

Positive numbers (0 to 127):
0000 0000 = 0
0000 0001 = 1
0000 0010 = 2
...
0111 1111 = 127 ← Maximum positive (bit đầu = 0)

Negative numbers (-128 to -1):
1000 0000 = -128 ← Minimum negative (bit đầu = 1)
1000 0001 = -127
1000 0010 = -126
...
1111 1110 = -2
1111 1111 = -1
💡 Cách nhớ: Bit đầu là "sign bit"
  • Bit đầu = 0: Số dương
  • Bit đầu = 1: Số âm

Range của n-bit signed integer:

  • Min: -2^(n-1)
  • Max: 2^(n-1) - 1

Ví dụ byte (8 bits):

  • Min: -2^7 = -128
  • Max: 2^7 - 1 = 127

Tại sao -128 nhưng +127 (không đối xứng)?

8 bits có 2^8 = 256 giá trị:
- 1 giá trị cho 0
- 127 giá trị cho positive (1 to 127)
- 128 giá trị cho negative (-128 to -1)

→ Range: -128 to +127 (total 256 values)

Chuyển đổi từ binary:

// Positive: Đọc trực tiếp
byte b1 = 0b0010_1010; // 42
System.out.println(b1); // 42

// Negative: Two's complement
// 1111_1110 = ?
// Step 1: Invert bits → 0000_0001
// Step 2: Add 1 → 0000_0010 = 2
// Step 3: Add minus sign → -2
byte b2 = (byte) 0b1111_1110;
System.out.println(b2); // -2

Tính toán Two's complement:

Để tìm -X (X > 0):

  1. Lấy binary của X
  2. Invert tất cả bits (0→1, 1→0)
  3. Cộng 1
Ví dụ: Tìm -5 (byte):
5 = 0000_0101
Step 1: Invert → 1111_1010
Step 2: Add 1 → 1111_1011 = -5

Verify:
1111_1011
Invert → 0000_0100
Add 1 → 0000_0101 = 5 ✅

Ứng dụng: Overflow behavior

byte max = 127;   // 0111_1111
max++; // 1000_0000 = -128 (overflow!)
System.out.println(max); // -128

byte min = -128; // 1000_0000
min--; // 0111_1111 = 127 (underflow!)
System.out.println(min); // 127
🔥 Bẫy OCP: Overflow wraps around

Integer overflow không throw exception — nó wrap around (mod 2^n)!

int max = Integer.MAX_VALUE;  // 2147483647
int overflow = max + 1; // -2147483648 (wraps to MIN_VALUE!)

System.out.println(Integer.MAX_VALUE); // 2147483647
System.out.println(overflow); // -2147483648
System.out.println(overflow == Integer.MIN_VALUE); // true

Giải pháp: Dùng Math.addExact() (throws ArithmeticException khi overflow):

int safe = Math.addExact(Integer.MAX_VALUE, 1);  // ArithmeticException

Ví dụ:

byte age = 25;              // Small numbers (-128 to 127)
short year = 2024; // Moderate numbers
int population = 8000000; // Default cho integer literals
long distanceToMoon = 384400000L; // L suffix cho long literals
Khi nào dùng type nào?
  • byte: Very small numbers (age, status codes)
  • short: Moderate numbers (year)
  • int: DEFAULT CHOICE cho integers (hầu hết trường hợp)
  • long: Very large numbers (timestamps, distances)

Ví dụ thực tế:

// File size
long fileSize = 2147483648L; // 2GB (> int max value)

// Color RGB values (0-255)
// byte range là -128 to 127, nên dùng int cho RGB!
int red = 255;
int green = 128;
int blue = 64;

// Timestamp
long timestamp = System.currentTimeMillis(); // Milliseconds since 1970

2. Floating-Point Types (Số thực)

TypeSizePrecisionRangeDefault ValueExample
float4 bytes~6-7 decimal digits±3.4e380.0ffloat pi = 3.14f;
double8 bytes~15 decimal digits±1.7e3080.0ddouble price = 99.99;

Ví dụ:

float temperature = 36.5f;       // f suffix required
double accountBalance = 1234.56; // Default cho floating literals
double scientific = 1.23e5; // 123000.0 (scientific notation)
Lưu ý
  • float: PHẢI có suffix f hoặc F
    float x = 3.14f;   // ✅ OK
    float y = 3.14; // ❌ Error! (3.14 là double)
  • double: Suffix d optional
    double x = 3.14;   // ✅ OK (default)
    double y = 3.14d; // ✅ OK (explicit)

Precision comparison:

float f = 0.1f + 0.2f;
double d = 0.1 + 0.2;

System.out.println(f); // 0.30000001 (imprecise)
System.out.println(d); // 0.30000000000000004 (more precise but still imprecise)

IEEE 754 — Tại sao 0.1 + 0.2 ≠ 0.3?

📖 Theo JLS §4.2.3 (Floating-Point Types)

Java sử dụng IEEE 754 standard cho floatdouble — định dạng binary floating-point representation.

Vấn đề: Nhiều số thập phân không biểu diễn chính xác trong binary.

💡 Cách nhớ: Giống như 1/3 trong hệ thập phân
  • Hệ thập phân: 1/3 = 0.333333... (vô hạn)
  • Hệ nhị phân: 0.1 = 0.00011001100110011... (vô hạn)
  • Computer chỉ lưu hữu hạn bits → làm tròn → sai số!

Ví dụ cụ thể:

double a = 0.1;
double b = 0.2;
double c = a + b;

System.out.println(c); // 0.30000000000000004
System.out.println(c == 0.3); // false !!!
System.out.println(0.1 + 0.2 == 0.3); // false !!!

// In binary representation:
// 0.1 = 0.0001100110011001100110011001100110011... (recurring)
// 0.2 = 0.001100110011001100110011001100110011... (recurring)
// → Tổng có sai số tích lũy

Cấu trúc IEEE 754 (double — 64 bits):

Formula: Value = (-1)^sign × 1.mantissa × 2^(exponent - 1023)

Giải pháp:

  1. Dùng BigDecimal cho financial calculations:
// ❌ BAD - Floating-point arithmetic imprecise
double price = 0.1 + 0.2; // 0.30000000000000004

// ✅ GOOD - Exact decimal arithmetic
import java.math.BigDecimal;
BigDecimal price = new BigDecimal("0.1").add(new BigDecimal("0.2"));
System.out.println(price); // 0.3 (chính xác!)
  1. So sánh với epsilon (tolerance):
double a = 0.1 + 0.2;
double b = 0.3;

// ❌ Never compare floats with ==
if (a == b) { } // false

// ✅ Compare with tolerance
double EPSILON = 0.0001;
if (Math.abs(a - b) < EPSILON) {
System.out.println("Equal enough!"); // ✅
}
  1. Dùng int/long cho tiền (cents):
// Thay vì: double dollars = 12.34
// Dùng: int cents = 1234 (store in cents)

int priceInCents = 1234; // $12.34
System.out.printf("$%.2f%n", priceInCents / 100.0); // $12.34
🔥 Bẫy OCP: Floating-point edge cases

Special values:

double positive_infinity = 1.0 / 0.0;     // Infinity
double negative_infinity = -1.0 / 0.0; // -Infinity
double not_a_number = 0.0 / 0.0; // NaN

System.out.println(positive_infinity); // Infinity
System.out.println(negative_infinity); // -Infinity
System.out.println(not_a_number); // NaN

// NaN so sánh:
System.out.println(not_a_number == not_a_number); // false !!!
System.out.println(Double.isNaN(not_a_number)); // true ✅

Underflow/Overflow:

double tooSmall = 1e-400;   // 0.0 (underflow)
double tooBig = 1e400; // Infinity (overflow)
Tránh dùng float/double cho tiền!

Lý do:

  • IEEE 754 binary floating-point không phù hợp cho decimal arithmetic
  • Sai số tích lũy qua nhiều phép tính
  • Financial calculations yêu cầu exact precision

Giải pháp:

  • Dùng BigDecimal (chậm hơn nhưng chính xác)
  • Hoặc lưu bằng int/long (cents, mili-dollars)

3. Character Type

TypeSizeRangeDefault ValueExample
char2 bytes0 to 65,535 (Unicode)'\u0000'char grade = 'A';

Ví dụ:

char letter = 'A';           // Single quotes
char digit = '9'; // Character, not integer 9
char unicode = '\u0041'; // 'A' in Unicode
char newline = '\n'; // Escape sequence

Escape sequences:

SequenceMeaningExample
\nNewline"Line 1\nLine 2"
\tTab"Col1\tCol2"
\\Backslash"C:\\Users"
\'Single quote'\''
\"Double quote"He said \"Hi\""
\rCarriage return"Text\r"
System.out.println("Line 1\nLine 2");    // Two lines
System.out.println("Name:\tAlice"); // Tab-separated
System.out.println("Path: C:\\Users"); // Backslash
System.out.println("He said \"Hello\""); // Quotes

4. Boolean Type

TypeSizeValuesDefault ValueExample
boolean1 bit (implementation-dependent)true, falsefalseboolean isActive = true;

Ví dụ:

boolean isStudent = true;
boolean hasLicense = false;
boolean canVote = age >= 18; // Expression result

Common use cases:

// Flags
boolean isActive = true;
boolean isDeleted = false;

// Conditions
boolean isAdult = age >= 18;
boolean isEligible = isAdult && hasLicense;

// Control flow
if (isActive) {
System.out.println("Active!");
}
Naming convention

Boolean variables nên bắt đầu bằng is, has, can:

boolean isValid;      // ✅
boolean hasPermission; // ✅
boolean canDelete; // ✅
boolean active; // ❌ Không rõ ràng

Primitive Types Summary

// Integer types
byte b = 127; // 1 byte
short s = 32000; // 2 bytes
int i = 2000000; // 4 bytes (DEFAULT)
long l = 9000000000L; // 8 bytes (suffix L)

// Floating-point types
float f = 3.14f; // 4 bytes (suffix f)
double d = 3.14159265359; // 8 bytes (DEFAULT)

// Character type
char c = 'A'; // 2 bytes (Unicode)

// Boolean type
boolean bool = true; // 1 bit (true/false)

Default values (for class fields):

public class Defaults {
byte b; // 0
short s; // 0
int i; // 0
long l; // 0L
float f; // 0.0f
double d; // 0.0d
char c; // '\u0000' (null character)
boolean bool; // false
}

Reference Types

Reference types lưu địa chỉ (reference) của object trong memory, không lưu giá trị trực tiếp.

Reference vs Primitive

// Primitive: Lưu giá trị trực tiếp
int x = 10;
int y = x; // Copy giá trị
y = 20;
System.out.println(x); // 10 (không thay đổi)

// Reference: Lưu địa chỉ
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1; // Copy địa chỉ (cùng trỏ đến object)
arr2[0] = 99;
System.out.println(arr1[0]); // 99 (thay đổi!)

Memory diagram:

Common Reference Types

1. String

String name = "Alice";
String greeting = new String("Hello");

2. Arrays

int[] numbers = {1, 2, 3, 4, 5};
String[] names = {"Alice", "Bob", "Charlie"};

3. Classes

Scanner scanner = new Scanner(System.in);
ArrayList<String> list = new ArrayList<>();

4. Interfaces

List<String> list = new ArrayList<>();  // List is interface

null - Special value

Reference types có thể có giá trị null (không trỏ đến object nào).

String name = null;  // ✅ OK - reference type
int age = null; // ❌ Error - primitive type không có null

NullPointerException:

String name = null;
System.out.println(name.length()); // ❌ NullPointerException!

// Safe check
if (name != null) {
System.out.println(name.length()); // ✅ OK
}

Khai báo và Khởi tạo Biến

Syntax

// Declaration (khai báo)
int age;

// Initialization (khởi tạo)
age = 25;

// Declaration + Initialization (một dòng)
int height = 175;

// Multiple variables (cùng type)
int x, y, z;
x = 1;
y = 2;
z = 3;

// Multiple variables với giá trị
int a = 1, b = 2, c = 3;
Lưu ý

Local variables PHẢI được khởi tạo trước khi sử dụng!

public void method() {
int x;
System.out.println(x); // ❌ Error: variable might not have been initialized

int y = 10;
System.out.println(y); // ✅ OK
}

Variable Naming Rules

Rules (bắt buộc):

  • Bắt đầu bằng letter (a-z, A-Z), $, hoặc _
  • Có thể chứa letters, digits, $, _
  • KHÔNG thể là keyword (int, class, public, ...)
  • Phân biệt HOA/thường: ageAge
// ✅ Valid
int age;
int _count;
int $price;
int studentName;
int student2;

// ❌ Invalid
int 2student; // Bắt đầu bằng số
int my-name; // Có dấu gạch ngang
int class; // Keyword

Best practices:

// ✅ GOOD - Descriptive, camelCase
int studentAge;
double accountBalance;
boolean isActive;

// ❌ BAD - Not descriptive
int a;
int x123;
int temp;

final Keyword - Constants

final để khai báo constants (giá trị không thay đổi).

final int MAX_SIZE = 100;
MAX_SIZE = 200; // ❌ Error: cannot assign a value to final variable

Convention:

// Constants: UPPER_SNAKE_CASE
final int MAX_USERS = 1000;
final double PI = 3.14159;
final String APP_NAME = "My Application";

Class-level constants:

public class Config {
public static final int MAX_CONNECTIONS = 100;
public static final String DATABASE_URL = "jdbc:mysql://localhost:3306/mydb";
}

// Usage
System.out.println(Config.MAX_CONNECTIONS);

Benefits:

  • ✅ Prevent accidental changes
  • ✅ More readable than magic numbers
  • ✅ Easier to maintain (change once, effect everywhere)
// ❌ BAD - Magic number
if (age >= 18) {
// ...
}

// ✅ GOOD - Named constant
final int VOTING_AGE = 18;
if (age >= VOTING_AGE) {
// ...
}

Variable Scope

Scope là phạm vi mà variable có thể được truy cập.

1. Local Variables

Khai báo trong method hoặc block { }.

public void method() {
int x = 10; // Local variable
System.out.println(x); // ✅ OK
}

// System.out.println(x); // ❌ Error: x not visible here

Block scope:

public void method() {
int x = 10;

if (true) {
int y = 20;
System.out.println(x); // ✅ OK (x visible)
System.out.println(y); // ✅ OK (y visible)
}

System.out.println(x); // ✅ OK
// System.out.println(y); // ❌ Error: y not visible outside block
}

2. Instance Variables (Fields)

Khai báo trong class, bên ngoài methods. Mỗi object có copy riêng.

public class Student {
// Instance variables
private String name;
private int age;

public void setName(String name) {
this.name = name; // Access instance variable
}
}

3. Class Variables (Static)

Khai báo với static keyword. Shared giữa tất cả objects.

public class Counter {
private static int count = 0; // Class variable

public Counter() {
count++; // Tăng cho tất cả instances
}

public static int getCount() {
return count;
}
}

// Usage
Counter c1 = new Counter();
Counter c2 = new Counter();
System.out.println(Counter.getCount()); // 2

Scope Summary

ScopeKhai báoLifetimeAccess
LocalTrong method/blockKhi method/block executeChỉ trong method/block
InstanceTrong class (non-static)Khi object tồn tạiQua object reference
Class (static)Trong class (static)Khi class loadQua class name

Literals

Literal là giá trị cố định trong code.

Integer Literals

int decimal = 100;       // Decimal (base 10)
int binary = 0b1100100; // Binary (base 2) - prefix 0b
int octal = 0144; // Octal (base 8) - prefix 0
int hex = 0x64; // Hexadecimal (base 16) - prefix 0x

// Tất cả = 100 (decimal)

Underscore for readability (Java 7+):

int million = 1_000_000;      // 1 million
long creditCard = 1234_5678_9012_3456L;
int binary = 0b1010_1100_0011;

Floating-Point Literals

double d1 = 123.456;
double d2 = 1.23e2; // 123.0 (scientific notation)
double d3 = 1.23E-2; // 0.0123

float f = 3.14f; // f suffix

Character Literals

char c1 = 'A';
char c2 = 65; // ASCII value of 'A'
char c3 = '\u0041'; // Unicode for 'A'
char newline = '\n'; // Escape sequence

String Literals

String s1 = "Hello";
String s2 = "Hello\nWorld";
String s3 = "C:\\Users\\Alice";
String s4 = ""; // Empty string

Text Blocks (Java 13+ preview, Java 15+ final):

// Text block - multi-line string literal
String json = """
{
"name": "Alice",
"age": 25
}
""";

// Không cần escape quotes hay dùng \n
String html = """
<html>
<body>Hello</body>
</html>
""";

Boolean Literals

boolean t = true;
boolean f = false;

Bài tập thực hành

Bài 1: Khai báo và sử dụng biến

Viết chương trình calculator đơn giản:

  1. Nhập 2 số (double)
  2. In ra: tổng, hiệu, tích, thương
  3. Format output với 2 chữ số thập phân

Expected output:

Nhập số thứ nhất: 10.5
Nhập số thứ hai: 3.2
Tổng: 13.70
Hiệu: 7.30
Tích: 33.60
Thương: 3.28
Xem đáp án
import java.util.Scanner;

public class Calculator {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);

System.out.print("Nhập số thứ nhất: ");
double num1 = sc.nextDouble();

System.out.print("Nhập số thứ hai: ");
double num2 = sc.nextDouble();

double sum = num1 + num2;
double diff = num1 - num2;
double product = num1 * num2;
double quotient = num1 / num2;

System.out.printf("Tổng: %.2f%n", sum);
System.out.printf("Hiệu: %.2f%n", diff);
System.out.printf("Tích: %.2f%n", product);
System.out.printf("Thương: %.2f%n", quotient);

sc.close();
}
}

Bài 2: Student Info

Viết chương trình khai báo thông tin sinh viên sử dụng các kiểu dữ liệu khác nhau:

  1. Khai báo: name (String), age (byte), gpa (double), isActive (boolean), grade (char)
  2. In ra tất cả thông tin
Xem đáp án
public class StudentInfo {
public static void main(String[] args) {
String name = "Nguyen Van A";
byte age = 20;
double gpa = 3.75;
boolean isActive = true;
char grade = 'A';

System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.printf("GPA: %.2f%n", gpa);
System.out.println("Active: " + isActive);
System.out.println("Grade: " + grade);
}
}

Tổng kết

Trong bài này, bạn đã học:

Primitive types: byte, short, int, long, float, double, char, boolean ✅ Reference types: String, arrays, objects (lưu địa chỉ, có thể null) ✅ final keyword: Constants (UPPER_SNAKE_CASE) ✅ Variable scope: Local, instance, class (static) ✅ Literals: Integer, floating-point, character, string, boolean, text blocks (Java 15+)

Key differences:

AspectPrimitiveReference
Lưu trữGiá trị trực tiếpĐịa chỉ (reference)
null❌ Không có✅ Có
Default0, false, '\u0000'null
Comparison== (giá trị)== (địa chỉ), .equals() (giá trị)
MemoryStackHeap (object), Stack (reference)
Bước tiếp theo

Bài tiếp theo chúng ta sẽ học về Type Casting và var Keyword - chuyển đổi kiểu dữ liệu và type inference!

👉 Tiếp theo: Type Casting và var Keyword →

Đọc thêm