Giới thiệu Functional Programming
Sau bài này, bạn sẽ:
- Hiểu được Functional Programming là gì và sự khác biệt với Imperative Programming
- Nắm được khái niệm Pure Functions và lợi ích của nó
- Hiểu được First-Class Functions trong Java
- So sánh được Declarative và Imperative style
- Biết được lý do Java thêm Functional Programming features
Functional Programming (FP) là gì?
Lập trình hàm (Functional Programming - FP) là một mô hình lập trình tập trung vào việc sử dụng hàm như khối xây dựng chính của chương trình.
Trong FP:
- Chương trình được coi như là quá trình tính toán các hàm toán học
- Tránh thay đổi trạng thái (state) và dữ liệu có thể thay đổi (mutable data)
- Hàm là thực thể hạng nhất (first-class citizens)
- Tập trung vào "what to do" thay vì "how to do it"
Java không phải ngôn ngữ lập trình hàm thuần túy (pure functional language) như Haskell hay Scala, nhưng từ Java 8 trở đi đã hỗ trợ nhiều tính năng của FP, cho phép bạn kết hợp cả OOP và FP trong cùng một ứng dụng.
Imperative vs Declarative Programming
Imperative Programming (Lập trình mệnh lệnh)
Imperative programming tập trung vào "HOW" - bạn phải mô tả từng bước cụ thể để đạt được kết quả.
// Imperative style: Tìm tổng các số chẵn trong list
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = 0;
for (int i = 0; i < numbers.size(); i++) {
int number = numbers.get(i);
if (number % 2 == 0) { // Nếu là số chẵn
sum += number; // Cộng vào tổng
}
}
System.out.println("Tổng các số chẵn: " + sum); // 30
Declarative Programming (Lập trình khai báo)
Declarative programming tập trung vào "WHAT" - bạn khai báo kết quả mong muốn, không cần quan tâm chi tiết cách thực hiện.
// Declarative/Functional style: Tìm tổng các số chẵn
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.stream()
.filter(n -> n % 2 == 0) // Lọc số chẵn
.mapToInt(n -> n) // Chuyển sang IntStream
.sum(); // Tính tổng
System.out.println("Tổng các số chẵn: " + sum); // 30
So sánh
| Khía cạnh | Imperative | Declarative (Functional) |
|---|---|---|
| Tập trung | HOW (cách thực hiện) | WHAT (kết quả mong muốn) |
| Mutable state | Có (biến sum, i thay đổi) | Không (immutable) |
| Độ dài code | Dài hơn | Ngắn gọn hơn |
| Dễ đọc | Phải theo dõi từng bước | Rõ ràng, tự mô tả |
| Bug potential | Cao hơn (nhiều state) | Thấp hơn (ít side effects) |
| Parallel processing | Khó parallelization | Dễ parallelization |
Pure Functions
Hàm thuần túy (pure function) là một trong những khái niệm cốt lõi của FP.
Đặc điểm của Pure Function
- Tính xác định (Deterministic): Với cùng input, luôn trả về cùng output
- Không có tác dụng phụ (No side effects): Không thay đổi state bên ngoài, không I/O, không exception
// ✅ Pure function
public class MathUtils {
// Pure: cùng input → cùng output, không side effects
public static int add(int a, int b) {
return a + b;
}
// Pure: không thay đổi input array, tạo array mới
public static int[] doubleValues(int[] numbers) {
int[] result = new int[numbers.length];
for (int i = 0; i < numbers.length; i++) {
result[i] = numbers[i] * 2;
}
return result;
}
}
// ❌ Impure functions
public class Counter {
private int count = 0; // Mutable state
// Impure: thay đổi state bên ngoài (count)
public int increment() {
count++; // Side effect!
return count;
}
// Impure: kết quả phụ thuộc vào external state
public int getCount() {
return count; // Không deterministic qua các lần gọi
}
// Impure: I/O là side effect
public void printMessage(String msg) {
System.out.println(msg); // Side effect!
}
// Impure: phụ thuộc vào thời gian hệ thống
public long getCurrentTime() {
return System.currentTimeMillis(); // Không deterministic
}
}
Lợi ích của Pure Functions
| Lợi ích | Mô tả |
|---|---|
| Dễ kiểm thử (Testability) | Dễ test - không cần mock dependencies |
| Tái sử dụng (Reusability) | Dễ tái sử dụng ở nhiều context khác nhau |
| Song song hóa (Parallelization) | An toàn khi chạy song song (thread-safe) |
| Bộ nhớ đệm (Caching) | Có thể cache kết quả (memoization) |
| Gỡ lỗi (Debugging) | Dễ debug - chỉ cần quan tâm input/output |
First-Class Functions
Trong FP, hàm là thực thể hạng nhất (first-class citizens), nghĩa là:
- Gán cho biến (Assign to variables)
- Truyền như tham số (Pass as arguments)
- Trả về từ hàm khác (Return from functions)
- Lưu trong cấu trúc dữ liệu (Store in data structures)
import java.util.function.Function;
import java.util.function.BiFunction;
public class FirstClassFunctionDemo {
public static void main(String[] args) {
// 1. Assign to variables
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
// 2. Pass as arguments
int result = calculate(10, 5, (a, b) -> a + b);
System.out.println(result); // 15
// 3. Return from functions
Function<Integer, Integer> multiplier = createMultiplier(3);
System.out.println(multiplier.apply(4)); // 12
// 4. Store in data structures
List<Function<Integer, Integer>> operations = Arrays.asList(
x -> x + 1,
x -> x * 2,
x -> x * x
);
int value = 5;
for (Function<Integer, Integer> op : operations) {
value = op.apply(value);
}
System.out.println(value); // ((5 + 1) * 2) ^ 2 = 144
}
// Function as parameter
public static int calculate(int a, int b, BiFunction<Integer, Integer, Integer> operation) {
return operation.apply(a, b);
}
// Function as return value
public static Function<Integer, Integer> createMultiplier(int factor) {
return x -> x * factor;
}
}
Functional Programming trong Java
Timeline
| Version | Năm | Tính năng FP |
|---|---|---|
| Java 8 | 2014 | Lambda expressions, Stream API, Functional interfaces, Method references, Optional |
| Java 9 | 2017 | Stream improvements, Optional improvements |
| Java 10 | 2018 | Local variable type inference (var) |
| Java 11 | 2018 | New String methods, Optional.isEmpty() |
| Java 16 | 2021 | Stream.toList() |
| Java 17 | 2021 | Sealed classes (giúp pattern matching) |
Tại sao Java thêm Functional Programming?
- Concise code: Code ngắn gọn, dễ đọc hơn
- Readability: Tự mô tả, tập trung vào business logic
- Parallel processing: Big Data era cần xử lý song song hiệu quả
- Modern paradigm: Bắt kịp xu hướng lập trình hiện đại
- Reduce bugs: Ít mutable state → ít bug
Tại sao Java cần lập trình hàm? — Bối cảnh lịch sử
Trước Java 8 (2014), Java thuần túy là ngôn ngữ hướng đối tượng. Nhưng thế giới phần mềm đã thay đổi:
Kỷ nguyên đa nhân (Multicore Era): Từ khoảng 2005, CPU không còn tăng tốc độ đơn nhân mà chuyển sang nhiều nhân. Chương trình muốn nhanh hơn phải chạy song song — và lập trình hàm với dữ liệu bất biến (immutable data) giúp việc này an toàn hơn rất nhiều so với chia sẻ biến giữa các thread.
Big Data và xử lý luồng: Hadoop, Spark và các framework xử lý dữ liệu lớn đều dựa trên mô hình map-filter-reduce — bản chất là lập trình hàm. Java cần Stream API để cạnh tranh.
Cạnh tranh từ Scala và Kotlin: Các ngôn ngữ chạy trên JVM như Scala (2004) và Kotlin (2011) đã hỗ trợ lập trình hàm từ đầu và thu hút nhiều lập trình viên Java. Oracle buộc phải bổ sung tính năng FP để giữ chân cộng đồng.
Hãy tưởng tượng lập trình hàm giống như viết công thức nấu ăn: bạn mô tả món ăn muốn có (lọc rau, xào thịt, trộn nước sốt) thay vì chỉ dẫn từng bước bật bếp, đổ dầu, chờ nóng. Người đầu bếp (JVM) sẽ tự biết cách thực hiện hiệu quả nhất — thậm chí có thể nấu nhiều món song song!
FP trong Java vs FP thuần túy
Java là ngôn ngữ lai (hybrid) — kết hợp cả OOP và FP. Điều này khác biệt đáng kể so với các ngôn ngữ FP thuần túy:
| Đặc điểm | Java (Hybrid) | Haskell (FP thuần túy) | Scala (Hybrid) |
|---|---|---|---|
| Mutable state | Cho phép, nhưng khuyến khích immutable | Không cho phép (mặc định) | Cho phép, nhưng khuyến khích val |
| Side effects | Tự do | Kiểm soát qua Monad (IO) | Tự do |
| Hàm hạng nhất | Từ Java 8 (lambda) | Có từ đầu | Có từ đầu |
| Pattern matching | Từ Java 16+ (instanceof), Java 21 (switch) | Có từ đầu, rất mạnh | Có từ đầu |
| Tail recursion | Không tối ưu | Tối ưu tự động | Annotation @tailrec |
Lưu ý về Tail Recursion Optimization (TCO): TCO là kỹ thuật compiler chuyển đổi đệ quy đuôi (tail recursion) thành vòng lặp để tránh StackOverflowError. JVM của Java không hỗ trợ TCO — đây là quyết định thiết kế có chủ đích vì JVM ưu tiên stack traces chính xác cho debugging. Trong thực tế, với đệ quy sâu, bạn nên chuyển sang vòng lặp thủ công:
// Tail recursion style (vẫn gây StackOverflowError với n lớn trong Java)
public static long factorialTailRec(int n, long acc) {
if (n <= 1) return acc;
return factorialTailRec(n - 1, n * acc);
}
// Iterative (an toàn với n lớn)
public static long factorialIterative(int n) {
long result = 1;
for (int i = 2; i <= n; i++) result *= i;
return result;
}
Trong thực tế, hầu hết ứng dụng Java production sử dụng cả OOP và FP:
- OOP để thiết kế hệ thống: class, interface, dependency injection
- FP để xử lý dữ liệu: stream pipeline, lambda, Optional
Spring Framework từ phiên bản 5 trở đi cũng tích cực áp dụng FP qua WebFlux (reactive programming).
Ví dụ thực tế: Filter và Transform Data
Bài toán: Tìm tên các sinh viên có điểm >= 8, in hoa, sắp xếp theo alphabet
import java.util.*;
import java.util.stream.*;
class Student {
private String name;
private double score;
public Student(String name, double score) {
this.name = name;
this.score = score;
}
public String getName() { return name; }
public double getScore() { return score; }
}
public class StudentFilterExample {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("Nguyễn Văn An", 7.5),
new Student("Trần Thị Bình", 8.5),
new Student("Lê Văn Cường", 9.0),
new Student("Phạm Thị Dung", 6.5),
new Student("Hoàng Văn Em", 8.2)
);
// ❌ Imperative style
System.out.println("=== Imperative Style ===");
List<String> resultImperative = new ArrayList<>();
for (Student student : students) {
if (student.getScore() >= 8.0) {
resultImperative.add(student.getName().toUpperCase());
}
}
Collections.sort(resultImperative);
for (String name : resultImperative) {
System.out.println(name);
}
// ✅ Functional/Declarative style
System.out.println("\n=== Functional Style ===");
students.stream()
.filter(s -> s.getScore() >= 8.0) // Lọc điểm >= 8
.map(s -> s.getName().toUpperCase()) // Chuyển in hoa
.sorted() // Sắp xếp
.forEach(System.out::println); // In ra
}
}
Output:
=== Imperative Style ===
HOÀNG VĂN EM
LÊ VĂN CƯỜNG
TRẦN THỊ BÌNH
=== Functional Style ===
HOÀNG VĂN EM
LÊ VĂN CƯỜNG
TRẦN THỊ BÌNH
So sánh 2 cách tiếp cận
| Tiêu chí | Imperative | Functional |
|---|---|---|
| Số dòng code | 10+ dòng | 4 dòng (1 pipeline) |
| Mutable state | Có (resultImperative) | Không |
| Readability | Phải đọc từng bước | Tự mô tả rõ ràng |
| Dễ song song hóa | Khó | Dễ (chỉ cần .parallelStream()) |
| Maintainability | Khó bảo trì khi logic phức tạp | Dễ thêm/bớt operations |
- Không phải lúc nào FP cũng tốt hơn: Với logic đơn giản, imperative có thể rõ ràng hơn
- Performance: Stream có overhead, với collection nhỏ có thể chậm hơn loop truyền thống
- Learning curve: Cần thời gian để làm quen với functional thinking
- Debugging: Stack trace của stream có thể khó đọc hơn
- OCP không hỏi lý thuyết về lập trình hàm (khái niệm pure function, side effects...), nhưng hỏi rất nhiều về lambda, Stream API, và Optional
- Nắm chắc sự khác biệt giữa imperative và declarative style — đề thi thường cho 2 đoạn code và hỏi output
- Effectively final là khái niệm hay ra trong đề — biến dùng trong lambda phải effectively final
- Hiểu rõ lazy evaluation của Stream: intermediate operations không chạy cho đến khi có terminal operation
Các khái niệm sẽ học trong module này
- Lambda Expressions: Cách viết anonymous functions ngắn gọn
- Functional Interfaces: Interfaces dành cho functional programming
- Stream API: Xử lý collections theo functional style
- Method References: Shorthand cho lambda expressions
- Optional: Xử lý null-safety theo functional way
Tóm tắt
- Functional Programming tập trung vào functions, immutability, và declarative style
- Pure functions không có side effects, deterministic → dễ test, dễ parallel
- First-class functions có thể gán, truyền, trả về như data
- Java 8+ hỗ trợ FP qua Lambda, Stream API, Functional Interfaces
- FP giúp code ngắn gọn, dễ đọc, dễ maintain, dễ parallel nhưng có learning curve
Bài tập
Bài 1: So sánh Imperative vs Functional
Cho danh sách số nguyên. Viết code theo 2 styles:
- Imperative: Tìm bình phương của các số lẻ, sau đó lọc ra các số > 50
- Functional: Dùng Stream API
List<Integer> numbers = Arrays.asList(2, 5, 8, 11, 3, 7, 12, 6);
// Expected output: [121, 81]
Bài 2: Phân biệt Pure vs Impure Function
Xác định functions sau là pure hay impure, giải thích lý do:
// Function 1
public int multiply(int a, int b) {
return a * b;
}
// Function 2
public List<String> addToList(List<String> list, String item) {
list.add(item);
return list;
}
// Function 3
public double calculateDiscount(double price) {
return price * 0.9;
}
// Function 4
private int counter = 0;
public int getNext() {
return ++counter;
}
Bài 3: First-Class Functions
Viết một method applyOperation nhận vào một list số nguyên và một function, trả về list mới sau khi áp dụng function cho từng phần tử.
// Ví dụ sử dụng
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubled = applyOperation(numbers, x -> x * 2);
// [2, 4, 6, 8, 10]
Bài tiếp theo: Lambda Expressions →
Đọc thêm
- Oracle: Lambda Expressions
- Java Language Specification §15.27 — Lambda Expressions
- Modern Java in Action — Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft — Ch. 1-3 (Introduction to Lambdas and FP concepts)
- Lambda Expressions →
- Stream API →