Mini-challenge: thư viện NumberUtils
Bài thực hành khép lại Module 4 — xây class NumberUtils với các method tĩnh gcd, lcm, isPrime, sum varargs, factorial. Kết hợp method, overloading, varargs, recursion.
Đây là mini-challenge khép lại Module 4. Không lý thuyết mới — bạn viết một class NumberUtils chứa các method tĩnh xử lý số nguyên thường dùng: GCD, LCM, kiểm tra nguyên tố, tính tổng varargs, giai thừa. Kết hợp đủ các khái niệm: khai báo method, overloading, varargs, recursion, scope — tất cả cùng một file.
Sau bài này bạn có 1 class NumberUtils tái sử dụng được, và hiểu quy trình thiết kế một bộ utility: tên method rõ, signature nhất quán, không leak state, có validation input.
🎯 Đề bài
Viết class NumberUtils với các method public static sau:
gcd(int a, int b)— Ước chung lớn nhất (Greatest Common Divisor). Returnint.gcd(int... nums)— Overload varargs cho nhiều số:gcd(12, 18, 24). Returnint.lcm(int a, int b)— Bội chung nhỏ nhất (Least Common Multiple). Dùnggcd.isPrime(int n)—truenếunlà số nguyên tố;falsecho số ≤ 1.sum(int... nums)— Tổng tất cả argument.sum()trả0.factorial(int n)— Giai thừa (recursion). NémIllegalArgumentExceptionnếun < 0.
Output mẫu:
gcd(12, 18) = 6
gcd(12, 18, 24) = 6
lcm(4, 6) = 12
isPrime(17) = true
isPrime(15) = false
sum() = 0
sum(1, 2, 3, 4, 5) = 15
factorial(5) = 120
🔍 Phân tích I-P-O
Input: Tham số tuỳ method. Varargs với 0+ số.
Processing: Dùng công thức toán / thuật toán:
- GCD: thuật toán Euclid —
gcd(a, b) = gcd(b, a % b), base caseb == 0 → a. - LCM:
lcm(a, b) = a * b / gcd(a, b). Chú ý overflow với số lớn. - isPrime: trial division đến
sqrt(n). - Sum: vòng for cộng.
- Factorial: đệ quy
n * factorial(n-1), base0! = 1.
Output: Giá trị trả về kiểu tương ứng, hoặc exception khi input không hợp lệ.
📦 Concept dùng trong bài
| Concept | Bài đã học | Dùng ở đây |
|---|---|---|
| Khai báo method tĩnh | Module 4, bài 1 | Mọi method là public static |
| Overloading | Module 4, bài 2 | gcd(int, int) vs gcd(int...) |
| Varargs | Module 4, bài 3 | gcd(int...), sum(int...) |
| Recursion | Module 4, bài 4 | gcd (Euclid), factorial |
| Scope | Module 4, bài 5 | Local trong method không leak ra ngoài |
Vòng for | Module 3, bài 4 | isPrime, sum iterative |
IllegalArgumentException | Module 2 (String) | Validation |
▶️ Starter code
Copy file này, điền vào các TODO:
public class NumberUtils {
// TODO: gcd(int a, int b) — thuat toan Euclid (recursion hoac iterative)
// TODO: gcd(int... nums) — reduce qua day, dung gcd(int, int)
// Nem IllegalArgumentException neu so luong < 2
// TODO: lcm(int a, int b) — a / gcd(a, b) * b de tranh overflow
// TODO: isPrime(int n) — false neu n <= 1, true neu n == 2,
// false neu n chia het 2, loop i tu 3 den sqrt(n) buoc 2
// TODO: sum(int... nums) — for-each cong
// TODO: factorial(int n) — recursion, nem IllegalArgumentException neu n < 0
public static void main(String[] args) {
System.out.println("gcd(12, 18) = " + gcd(12, 18));
System.out.println("gcd(12, 18, 24) = " + gcd(12, 18, 24));
System.out.println("lcm(4, 6) = " + lcm(4, 6));
System.out.println("isPrime(17) = " + isPrime(17));
System.out.println("isPrime(15) = " + isPrime(15));
System.out.println("sum() = " + sum());
System.out.println("sum(1, 2, 3, 4, 5) = " + sum(1, 2, 3, 4, 5));
System.out.println("factorial(5) = " + factorial(5));
}
}
javac NumberUtils.java
java NumberUtils
Dành 20–25 phút tự làm trước khi xem gợi ý.
💡 Gợi ý
Thuật toán Euclid (recursion):
public static int gcd(int a, int b) {
if (b == 0) return Math.abs(a);
return gcd(b, a % b);
}
Base case: b == 0 thì gcd(a, 0) = |a|. Recursive case: gcd(a, b) = gcd(b, a mod b) — dãy giảm rất nhanh (log operations).
Overload varargs — reduce:
public static int gcd(int... nums) {
if (nums.length < 2) throw new IllegalArgumentException("Need at least 2 numbers");
int result = nums[0];
for (int i = 1; i < nums.length; i++) {
result = gcd(result, nums[i]);
}
return result;
}
LCM tránh overflow — không viết a * b / gcd(a,b) (overflow với int lớn). Viết a / gcd(a,b) * b — chia trước để giữ số nhỏ:
public static int lcm(int a, int b) {
if (a == 0 || b == 0) return 0;
return Math.abs(a / gcd(a, b) * b);
}
isPrime — trial division đến sqrt(n):
public static boolean isPrime(int n) {
if (n <= 1) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; (long) i * i <= n; i += 2) {
if (n % i == 0) return false;
}
return true;
}
Chú ý (long) i * i <= n — tránh overflow khi i gần sqrt(Integer.MAX_VALUE).
Factorial đệ quy với validation:
public static long factorial(int n) {
if (n < 0) throw new IllegalArgumentException("n must be non-negative, got " + n);
if (n <= 1) return 1;
return n * factorial(n - 1);
}
Dùng long vì int overflow từ 13! (13! > 2^31).
✅ Lời giải
public class NumberUtils {
public static int gcd(int a, int b) {
if (b == 0) return Math.abs(a);
return gcd(b, a % b);
}
public static int gcd(int... nums) {
if (nums.length < 2) {
throw new IllegalArgumentException("Need at least 2 numbers, got " + nums.length);
}
int result = nums[0];
for (int i = 1; i < nums.length; i++) {
result = gcd(result, nums[i]);
}
return result;
}
public static int lcm(int a, int b) {
if (a == 0 || b == 0) return 0;
return Math.abs(a / gcd(a, b) * b);
}
public static boolean isPrime(int n) {
if (n <= 1) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; (long) i * i <= n; i += 2) {
if (n % i == 0) return false;
}
return true;
}
public static int sum(int... nums) {
int total = 0;
for (int n : nums) total += n;
return total;
}
public static long factorial(int n) {
if (n < 0) {
throw new IllegalArgumentException("n must be non-negative, got " + n);
}
if (n <= 1) return 1;
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println("gcd(12, 18) = " + gcd(12, 18));
System.out.println("gcd(12, 18, 24) = " + gcd(12, 18, 24));
System.out.println("lcm(4, 6) = " + lcm(4, 6));
System.out.println("isPrime(17) = " + isPrime(17));
System.out.println("isPrime(15) = " + isPrime(15));
System.out.println("sum() = " + sum());
System.out.println("sum(1, 2, 3, 4, 5) = " + sum(1, 2, 3, 4, 5));
System.out.println("factorial(5) = " + factorial(5));
}
}
Giải thích từng method:
-
gcd(int, int)recursion — Thuật toán Euclid:gcd(a, b) = gcd(b, a mod b), baseb == 0 → |a|. Depth log(min(a,b)) — không lo stack overflow vớiint.Math.absđể kết quả luôn dương dù input âm. -
gcd(int...)overload varargs — Reduce bằng vòng for, mỗi bước ápgcd(result, nums[i]). Yêu cầu ≥ 2 số vìgcd()haygcd(5)không có ý nghĩa rõ. Khi gọigcd(12, 18)(2 argument), phase 1 resolution chọngcd(int, int)trực tiếp (fixed-arity thắng varargs) — đúng ý đồ. -
lcm— Công thứclcm(a, b) = a * b / gcd(a, b). Viếta / gcd(a, b) * bđể chia trước, tránha * boverflow với số lớn.Math.abscho kết quả dương. -
isPrime— Trial division. Loạin <= 1, xử lý riêngn == 2(nguyên tố chẵn duy nhất), loại số chẵn. Sau đó thử chỉ số lẻ từ 3 đếnsqrt(n)— tiết kiệm 50% vòng.(long) i * i <= ntránh overflow khiilớn. -
sum— varargs đơn giản;sum()không argument trả0vì mảng rỗng, for-each không chạy. -
factorial— recursion với validation.longreturn vì13!đã overflowint. ThrowIllegalArgumentExceptionđúng spec Java cho "input không hợp lệ" (khácNullPointerExceptionchonull,IllegalStateExceptioncho state sai).
🎓 Mở rộng
Đã hoàn thành bài cơ bản? Thử tiếp:
Mức 1 — Viết unit test với JUnit 5:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class NumberUtilsTest {
@Test void gcdBasic() { assertEquals(6, NumberUtils.gcd(12, 18)); }
@Test void gcdVarargs() { assertEquals(6, NumberUtils.gcd(12, 18, 24)); }
@Test void lcmZero() { assertEquals(0, NumberUtils.lcm(0, 5)); }
@Test void isPrimeEdge() { assertFalse(NumberUtils.isPrime(1)); }
@Test void factorialNegative() {
assertThrows(IllegalArgumentException.class, () -> NumberUtils.factorial(-1));
}
}
Mức 2 — Thêm BigInteger cho factorial lớn:
factorial(20) vẫn fit long, nhưng factorial(25) đã overflow. Thêm overload:
import java.math.BigInteger;
public static BigInteger factorialBig(int n) {
if (n < 0) throw new IllegalArgumentException(...);
BigInteger result = BigInteger.ONE;
for (int i = 2; i <= n; i++) {
result = result.multiply(BigInteger.valueOf(i));
}
return result;
}
Chú ý: đổi sang iterative vì recursion với BigInteger tạo nhiều intermediate object — iterative hiệu quả hơn.
Mức 3 — Memoize isPrime:
import java.util.BitSet;
private static final BitSet PRIME_CACHE = new BitSet(10_000);
private static final BitSet COMPUTED = new BitSet(10_000);
public static boolean isPrime(int n) {
if (n < COMPUTED.size() && COMPUTED.get(n)) {
return PRIME_CACHE.get(n);
}
boolean result = /* tinh nhu cu */;
if (n < COMPUTED.size()) {
COMPUTED.set(n);
if (result) PRIME_CACHE.set(n);
}
return result;
}
Trade-off: tiết kiệm thời gian với queries lặp lại, tốn memory. Chỉ worth nếu benchmark cho thấy bottleneck.
Mức 4 — Sieve of Eratosthenes cho primesUpTo(int n):
Thay vì check từng số, xây bảng nguyên tố đến n bằng sàng:
public static int[] primesUpTo(int n) {
boolean[] isComposite = new boolean[n + 1];
List<Integer> primes = new ArrayList<>();
for (int i = 2; i <= n; i++) {
if (!isComposite[i]) {
primes.add(i);
for (int j = i + i; j <= n; j += i) isComposite[j] = true;
}
}
return primes.stream().mapToInt(Integer::intValue).toArray();
}
O(n log log n) — nhanh hơn gọi isPrime cho từng số từ 2 đến n.
✨ Điều bạn vừa làm được
Hoàn thành mini-challenge này, bạn đã:
- Khai báo 6 method tĩnh với signature khác nhau, theo đúng Java convention (tên camelCase, động từ hoặc
is<X>). - Áp dụng overloading cho
gcd(int, int)vsgcd(int...)— compiler tự chọn phiên bản theo số argument. - Dùng varargs cho số lượng tham số tuỳ ý, kèm validation khi số argument không đủ.
- Viết recursion an toàn: base case rõ, recursive case tiệm cận base, depth có giới hạn.
- Xử lý overflow bằng kỹ thuật "chia trước, nhân sau" (
lcm) và castlong(isPrime). - Validate input với
IllegalArgumentException— spec Java chuẩn cho "input không hợp lệ". - Chọn đúng return type (
intđủ cho gcd/lcm;longcho factorial;booleancho isPrime).
Chúc mừng — bạn đã hoàn thành Module 4! Đây là lúc bạn có thể phân rã chương trình thành nhiều hàm nhỏ thay vì dồn vào main. Module 5 sẽ bước vào OOP: class, object, constructor, field, encapsulation — nơi method thôi làm việc một mình mà gắn vào đối tượng có trạng thái.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên