Khám Phá Sâu: Java Streams và Lập Trình Hàm

Java Streams và lập trình hàm đã thay đổi cách chúng ta viết code Java. Được giới thiệu trong Java 8 và được cải tiến trong các phiên bản hiện đại, Streams API cho phép xử lý dữ liệu theo cách khai báo, ngắn gọn và có thể song song hóa. Bài viết này bao gồm mọi thứ từ các kiến thức cơ bản về lambda đến các pattern stream nâng cao.
Tại Sao Lập Trình Hàm Trong Java?
Trước Java 8 (Phong Cách Mệnh Lệnh):
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> filtered = new ArrayList<>();
for (String name : names) {
if (name.length() > 3) {
filtered.add(name.toUpperCase());
}
}
Collections.sort(filtered);Sau Java 8 (Phong Cách Hàm):
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList(); // Java 16+Lợi ích:
- Khai báo: Diễn tả cái gì bạn muốn, không phải cách làm
- Có thể kết hợp: Móc nối các thao tác một cách tự nhiên
- Có thể song song hóa: Xử lý song song dễ dàng với
.parallel() - Đánh giá lười: Các thao tác chỉ thực thi khi cần thiết
Lambda Expressions
Lambda expressions là các hàm ẩn danh triển khai functional interfaces.
Cú pháp
// Cú pháp đầy đủ
(parameters) -> { statements; return value; }
// Đơn giản hóa
(x, y) -> x + y // Nhiều tham số
x -> x * 2 // Một tham số (không cần ngoặc)
() -> System.out.println("Hi") // Không có tham số
x -> { return x * 2; } // Return tường minh (nhiều câu lệnh)Ví dụ Thực Tế
// Comparator
List<String> names = List.of("Charlie", "Alice", "Bob");
names.sort((a, b) -> a.compareTo(b));
// Hoặc đơn giản hơn:
names.sort(String::compareTo);
// Runnable
Thread thread = new Thread(() -> {
System.out.println("Running in thread: " + Thread.currentThread().getName());
});
// Custom functional interface
@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
}
MathOperation add = (a, b) -> a + b;
MathOperation multiply = (a, b) -> a * b;
System.out.println(add.operate(5, 3)); // 8
System.out.println(multiply.operate(5, 3)); // 15Method References
Method references (::) là cách viết tắt cho lambda gọi một phương thức duy nhất.
Các loại Method References
1. Static Method Reference
// Lambda: x -> Math.sqrt(x)
// Method ref: Math::sqrt
List<Double> numbers = List.of(4.0, 9.0, 16.0);
List<Double> roots = numbers.stream()
.map(Math::sqrt)
.toList();2. Instance Method Reference (trên đối tượng cụ thể)
String prefix = "Hello, ";
// Lambda: name -> prefix.concat(name)
// Method ref: prefix::concat
List<String> greetings = names.stream()
.map(prefix::concat)
.toList();3. Instance Method Reference (trên đối tượng tùy ý)
// Lambda: str -> str.toLowerCase()
// Method ref: String::toLowerCase
List<String> lower = names.stream()
.map(String::toLowerCase)
.toList();4. Constructor Reference
// Lambda: len -> new String[len]
// Method ref: String[]::new
String[] array = names.stream()
.toArray(String[]::new);
// Khởi tạo đối tượng
record Person(String name) {}
List<Person> people = names.stream()
.map(Person::new)
.toList();Built-in Functional Interfaces
Java cung cấp các functional interfaces tiêu chuẩn trong java.util.function.
Các Interface Cốt Lõi
import java.util.function.*;
// Predicate<T>: T -> boolean (kiểm tra điều kiện)
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Hello")); // false
// Function<T, R>: T -> R (chuyển đổi)
Function<String, Integer> length = String::length;
System.out.println(length.apply("Hello")); // 5
// Consumer<T>: T -> void (tác dụng phụ)
Consumer<String> printer = System.out::println;
printer.accept("Hello");
// Supplier<T>: () -> T (tạo ra giá trị)
Supplier<Double> random = Math::random;
System.out.println(random.get());
// BiFunction<T, U, R>: (T, U) -> R
BiFunction<Integer, Integer, Integer> max = Math::max;
System.out.println(max.apply(5, 3)); // 5
// UnaryOperator<T>: T -> T (Function đặc biệt)
UnaryOperator<String> toUpper = String::toUpperCase;
// BinaryOperator<T>: (T, T) -> T (BiFunction đặc biệt)
BinaryOperator<Integer> sum = Integer::sum;Composition
// Kết hợp Predicate
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> startsWithA = s -> s.startsWith("A");
Predicate<String> combined = isLong.and(startsWithA);
// Kết hợp Function
Function<Integer, Integer> multiplyBy2 = x -> x * 2;
Function<Integer, Integer> add3 = x -> x + 3;
Function<Integer, Integer> composed = multiplyBy2.andThen(add3);
System.out.println(composed.apply(5)); // (5 * 2) + 3 = 13Tạo Streams
Từ Collections
List<String> list = List.of("a", "b", "c");
Stream<String> stream1 = list.stream();
Stream<String> parallelStream = list.parallelStream();Từ Arrays
String[] array = {"a", "b", "c"};
Stream<String> stream2 = Arrays.stream(array);
Stream<String> partial = Arrays.stream(array, 1, 3); // chỉ số 1-2Từ Values
Stream<String> stream3 = Stream.of("a", "b", "c");
Stream<String> empty = Stream.empty();Từ Generators
// Stream vô hạn (dùng với limit!)
Stream<Double> randoms = Stream.generate(Math::random);
Stream<Integer> naturals = Stream.iterate(0, n -> n + 1);
// Java 9+: iterate với điều kiện
Stream<Integer> finite = Stream.iterate(0, n -> n < 10, n -> n + 1);Từ Files
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
lines.filter(line -> !line.isEmpty())
.forEach(System.out::println);
}Từ Các Nguồn Khác
// IntStream, LongStream, DoubleStream
IntStream.range(1, 5); // 1, 2, 3, 4
IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5
// Từ String
"Hello".chars() // IntStream của mã ký tự
.mapToObj(c -> (char) c)
.forEach(System.out::print);Intermediate Operations
Intermediate operations là lazy và trả về một stream mới. Chúng chỉ thực thi khi một terminal operation được gọi.
filter
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);
// Chỉ giữ các số chẵn
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.toList(); // [2, 4, 6, 8]map
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25)
);
// Trích xuất tên
List<String> names = people.stream()
.map(Person::name)
.toList();
// Chuyển đổi sang chữ hoa
List<String> upper = names.stream()
.map(String::toUpperCase)
.toList();flatMap
Làm phẳng các cấu trúc lồng nhau (stream của streams → stream đơn).
List<List<Integer>> nested = List.of(
List.of(1, 2, 3),
List.of(4, 5),
List.of(6, 7, 8, 9)
);
// Làm phẳng thành một danh sách duy nhất
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.toList(); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// Thực tế: Lấy tất cả các từ từ các dòng
List<String> lines = List.of("Hello world", "Java streams");
List<String> words = lines.stream()
.flatMap(line -> Arrays.stream(line.split(" ")))
.toList(); // ["Hello", "world", "Java", "streams"]sorted
// Thứ tự tự nhiên
List<Integer> sorted = numbers.stream()
.sorted()
.toList();
// Comparator tùy chỉnh
List<String> byLength = names.stream()
.sorted(Comparator.comparing(String::length))
.toList();
// Thứ tự đảo ngược
List<Integer> reversed = numbers.stream()
.sorted(Comparator.reverseOrder())
.toList();
// Nhiều tiêu chí
people.stream()
.sorted(Comparator.comparing(Person::age)
.thenComparing(Person::name))
.toList();distinct
List<Integer> numbers = List.of(1, 2, 2, 3, 3, 3, 4);
List<Integer> unique = numbers.stream()
.distinct()
.toList(); // [1, 2, 3, 4]limit & skip
// 3 phần tử đầu tiên
List<Integer> first3 = numbers.stream()
.limit(3)
.toList();
// Bỏ qua 2 phần tử đầu, lấy 3 phần tử tiếp theo
List<Integer> page = numbers.stream()
.skip(2)
.limit(3)
.toList();peek
Hữu ích cho việc debug (tác dụng phụ mà không tiêu thụ stream).
List<String> result = names.stream()
.peek(name -> System.out.println("Before: " + name))
.map(String::toUpperCase)
.peek(name -> System.out.println("After: " + name))
.toList();Terminal Operations
Terminal operations kích hoạt việc thực thi stream và tạo ra kết quả.
collect
// Sang List (Java 16+)
List<String> list = stream.toList();
// Sang mutable List
List<String> mutableList = stream.collect(Collectors.toList());
// Sang Set
Set<String> set = stream.collect(Collectors.toSet());
// Sang collection cụ thể
LinkedList<String> linked = stream.collect(Collectors.toCollection(LinkedList::new));reduce
Kết hợp các phần tử thành một kết quả duy nhất.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Tổng
int sum = numbers.stream()
.reduce(0, Integer::sum); // 15
// Tích
int product = numbers.stream()
.reduce(1, (a, b) -> a * b); // 120
// Max
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
// Nối chuỗi
String joined = List.of("a", "b", "c").stream()
.reduce("", (acc, s) -> acc + s); // "abc"forEach
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
// forEachOrdered (giữ thứ tự trong parallel streams)
names.parallelStream()
.forEachOrdered(System.out::println);count
long count = names.stream()
.filter(name -> name.length() > 5)
.count();min / max
Optional<Integer> min = numbers.stream()
.min(Integer::compareTo);
Optional<Person> youngest = people.stream()
.min(Comparator.comparing(Person::age));anyMatch / allMatch / noneMatch
boolean hasLong = names.stream()
.anyMatch(name -> name.length() > 10);
boolean allLong = names.stream()
.allMatch(name -> name.length() > 2);
boolean noneEmpty = names.stream()
.noneMatch(String::isEmpty);findFirst / findAny
Optional<String> first = names.stream()
.filter(name -> name.startsWith("A"))
.findFirst();
// findAny (nhanh hơn cho parallel streams)
Optional<String> any = names.parallelStream()
.filter(name -> name.startsWith("A"))
.findAny();Advanced Collectors
Collectors cung cấp khả năng tổng hợp dữ liệu mạnh mẽ.
Joining Strings
String csv = names.stream()
.collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"
String quoted = names.stream()
.collect(Collectors.joining(", ", "[", "]"));
// "[Alice, Bob, Charlie]"Grouping
record Person(String name, String city, int age) {}
List<Person> people = List.of(
new Person("Alice", "NYC", 30),
new Person("Bob", "NYC", 25),
new Person("Charlie", "LA", 35)
);
// Nhóm theo thành phố
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::city));
// {NYC=[Alice, Bob], LA=[Charlie]}
// Đếm theo thành phố
Map<String, Long> countByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.counting()
));
// {NYC=2, LA=1}
// Tuổi trung bình theo thành phố
Map<String, Double> avgAgeByCity = people.stream()
.collect(Collectors.groupingBy(
Person::city,
Collectors.averagingInt(Person::age)
));Partitioning
// Chia thành hai nhóm dựa trên predicate
Map<Boolean, List<Person>> partition = people.stream()
.collect(Collectors.partitioningBy(p -> p.age() >= 30));
// {false=[Bob], true=[Alice, Charlie]}Custom Collector
// Thu thập vào Map
Map<String, Integer> nameToAge = people.stream()
.collect(Collectors.toMap(
Person::name,
Person::age
));
// Xử lý các key trùng lặp
Map<String, Person> byName = people.stream()
.collect(Collectors.toMap(
Person::name,
p -> p,
(existing, replacement) -> existing // Giữ phần tử đầu tiên
));
// Sử dụng triển khai Map cụ thể
TreeMap<String, Integer> sorted = people.stream()
.collect(Collectors.toMap(
Person::name,
Person::age,
(a, b) -> a,
TreeMap::new
));Statistics
IntSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingInt(Person::age));
System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Avg: " + stats.getAverage());Parallel Streams
Parallel streams tự động chia công việc trên nhiều threads.
Khi nào nên sử dụng
// Tuần tự
long count = hugeList.stream()
.filter(expensivePredicate)
.count();
// Song song
long count = hugeList.parallelStream()
.filter(expensivePredicate)
.count();Cân nhắc về Hiệu suất
✅ Tốt cho song song:
- Dataset lớn (10,000+ phần tử)
- Các thao tác tốn CPU
- Các thao tác độc lập (không có shared state)
- Các thao tác tốn thời gian (file I/O, tính toán)
❌ Không tốt cho song song:
- Dataset nhỏ (overhead > benefit)
- Các thao tác đã nhanh
- Các thao tác có tác dụng phụ trên shared state
- Các thao tác có thứ tự (limit, skip, findFirst)
Ví dụ: Parallel vs Sequential
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
.boxed()
.toList();
// Tuần tự
long start = System.currentTimeMillis();
int sum = numbers.stream()
.mapToInt(n -> n * 2)
.sum();
System.out.println("Sequential: " + (System.currentTimeMillis() - start) + "ms");
// Song song
start = System.currentTimeMillis();
sum = numbers.parallelStream()
.mapToInt(n -> n * 2)
.sum();
System.out.println("Parallel: " + (System.currentTimeMillis() - start) + "ms");Tránh Race Conditions
// ❌ TỒI: Shared mutable state
List<Integer> results = new ArrayList<>();
numbers.parallelStream()
.forEach(results::add); // Race condition!
// ✅ TỐT: Sử dụng collector
List<Integer> results = numbers.parallelStream()
.collect(Collectors.toList());Optional cho Null Safety
Optional<T> đại diện cho một giá trị có thể có hoặc không có.
Tạo Optional
Optional<String> full = Optional.of("Hello");
Optional<String> nullable = Optional.ofNullable(null);
Optional<String> empty = Optional.empty();Sử dụng Optional
// Kiểm tra nếu có giá trị
if (optional.isPresent()) {
String value = optional.get();
}
// ifPresent (tốt hơn)
optional.ifPresent(value -> System.out.println(value));
// ifPresentOrElse (Java 9+)
optional.ifPresentOrElse(
value -> System.out.println("Found: " + value),
() -> System.out.println("Not found")
);Giá trị Mặc định
// orElse: Luôn được đánh giá
String value = optional.orElse("default");
// orElseGet: Chỉ được đánh giá nếu rỗng (ưu tiên)
String value = optional.orElseGet(() -> expensiveDefault());
// orElseThrow
String value = optional.orElseThrow();
String value = optional.orElseThrow(() -> new IllegalStateException("Missing value"));Chuyển đổi Optional
Optional<String> name = Optional.of("Alice");
// map
Optional<Integer> length = name.map(String::length);
// flatMap (khi mapper trả về Optional)
Optional<String> upper = name.flatMap(n ->
n.isEmpty() ? Optional.empty() : Optional.of(n.toUpperCase())
);
// filter
Optional<String> longName = name.filter(n -> n.length() > 5);Ví dụ Thực Tế
record User(String id, Optional<String> email) {}
// Tìm user, lấy email, hoặc sử dụng mặc định
String email = userRepository.findById("123")
.flatMap(User::email)
.filter(e -> e.contains("@"))
.orElse("[email protected]");Ví dụ Thực Tế
Ví dụ 1: Pipeline Xử lý Dữ liệu
record Transaction(String id, String category, double amount, LocalDate date) {}
List<Transaction> transactions = getTransactions();
// Lấy 5 danh mục hàng đầu theo tổng chi tiêu trong 30 ngày qua
Map<String, Double> topCategories = transactions.stream()
.filter(t -> t.date().isAfter(LocalDate.now().minusDays(30)))
.collect(Collectors.groupingBy(
Transaction::category,
Collectors.summingDouble(Transaction::amount)
))
.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(5)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(a, b) -> a,
LinkedHashMap::new
));Ví dụ 2: Chuyển đổi Đối tượng Lồng nhau
record Order(String id, List<OrderItem> items) {}
record OrderItem(String product, int quantity, double price) {}
List<Order> orders = getOrders();
// Lấy tất cả các sản phẩm duy nhất đã đặt trong tháng trước
Set<String> products = orders.stream()
.filter(o -> isLastMonth(o.id()))
.flatMap(order -> order.items().stream())
.map(OrderItem::product)
.collect(Collectors.toSet());
// Tính tổng doanh thu theo sản phẩm
Map<String, Double> revenueByProduct = orders.stream()
.flatMap(order -> order.items().stream())
.collect(Collectors.groupingBy(
OrderItem::product,
Collectors.summingDouble(item -> item.quantity() * item.price())
));Ví dụ 3: Lọc & Ánh xạ Phức tạp
record Employee(String name, String department, int salary, int age) {}
List<Employee> employees = getEmployees();
// Lấy tên nhân viên ở phòng Engineering kiếm > $80k, sắp xếp theo lương
List<String> highEarners = employees.stream()
.filter(e -> "Engineering".equals(e.department()))
.filter(e -> e.salary() > 80_000)
.sorted(Comparator.comparing(Employee::salary).reversed())
.map(Employee::name)
.toList();
// Nhóm theo phòng ban, tính lương trung bình, lọc các phòng có avg > $75k
Map<String, Double> highPayingDepts = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.averagingInt(Employee::salary)
))
.entrySet().stream()
.filter(entry -> entry.getValue() > 75_000)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));Best Practices
✅ NÊN Sử dụng Streams Cho
-
Xử lý dữ liệu khai báo
// Ý định rõ ràng List<String> result = names.stream() .filter(name -> name.length() > 5) .map(String::toUpperCase) .sorted() .toList(); -
Móc nối nhiều thao tác
double average = transactions.stream() .filter(t -> t.amount() > 100) .mapToDouble(Transaction::amount) .average() .orElse(0.0); -
Xử lý song song các dataset lớn
long count = hugeList.parallelStream() .filter(expensivePredicate) .count();
❌ KHÔNG NÊN Sử dụng Streams Cho
-
Lặp đơn giản
// Tồi: Phức tạp không cần thiết names.stream().forEach(System.out::println); // Tốt: Vòng lặp trực tiếp for (String name : names) { System.out.println(name); } -
Vòng lặp có chỉ số
// Tồi: Khó hiểu với streams IntStream.range(0, list.size()) .forEach(i -> process(list.get(i), i)); // Tốt: Vòng lặp for truyền thống for (int i = 0; i < list.size(); i++) { process(list.get(i), i); } -
Checked exceptions
// Tồi: Cần phải wrap files.stream() .map(file -> { try { return Files.readString(file); } catch (IOException e) { throw new UncheckedIOException(e); } }); // Tốt: Try-catch truyền thống for (Path file : files) { try { String content = Files.readString(file); process(content); } catch (IOException e) { handleError(e); } } -
Kết thúc sớm với điều kiện phức tạp
// Tồi: Khó break sớm Optional<String> result = names.stream() .filter(complexCondition) .findFirst(); // Tốt: Break khi cần String result = null; for (String name : names) { if (complexCondition(name)) { result = name; break; } }
Mẹo Hiệu suất
-
Tránh boxing/unboxing với primitive streams
// Tồi: Overhead của boxing List<Integer> numbers = List.of(1, 2, 3, 4, 5); int sum = numbers.stream() .reduce(0, Integer::sum); // Tốt: Primitive stream int sum = IntStream.of(1, 2, 3, 4, 5) .sum(); -
Sắp xếp các thao tác hiệu quả
// Tồi: Map tất cả, sau đó filter list.stream() .map(expensiveTransform) .filter(result -> result.isValid()) .toList(); // Tốt: Filter trước (ít thao tác đắt tiền hơn) list.stream() .filter(item -> cheapCheck(item)) .map(expensiveTransform) .toList(); -
Sử dụng các phương thức chuyên biệt
// Tồi: Reduce chung chung long count = stream.reduce(0L, (acc, item) -> acc + 1, Long::sum); // Tốt: Count built-in long count = stream.count();
Rõ ràng Code
-
Trích xuất các lambda phức tạp
// Tồi: Khó đọc employees.stream() .filter(e -> e.department().equals("Engineering") && e.salary() > 80000 && e.age() < 40) .toList(); // Tốt: Predicate có tên Predicate<Employee> isYoungHighPaidEngineer = e -> e.department().equals("Engineering") && e.salary() > 80000 && e.age() < 40; List<Employee> result = employees.stream() .filter(isYoungHighPaidEngineer) .toList(); -
Giữ streams ngắn
// Tồi: Quá nhiều thao tác result = data.stream().filter(...).map(...).flatMap(...).sorted(...) .distinct().skip(...).limit(...).filter(...).map(...).toList(); // Tốt: Chia thành các bước logic Stream<Item> filtered = data.stream() .filter(predicate) .map(transformer); Stream<Item> processed = filtered .flatMap(nestedMapper) .sorted(); List<Item> result = processed .distinct() .toList();
Kết nối với Spring Boot
Streams rất cần thiết trong các ứng dụng Spring Boot (được đề cập trong Giai đoạn 3 của Lộ trình Java):
@Service
public class UserService {
// Repository trả về Stream cho hiệu quả bộ nhớ
@Transactional(readOnly = true)
public List<UserDTO> getActiveUsers() {
try (Stream<User> users = userRepository.findAllByActiveTrue()) {
return users
.filter(user -> user.getLastLoginDate()
.isAfter(LocalDate.now().minusMonths(6)))
.map(this::convertToDTO)
.toList();
}
}
// Xử lý song song cho các thao tác batch
public void sendNotifications(List<User> users) {
users.parallelStream()
.forEach(user -> {
notificationService.send(user);
});
}
}Tóm tắt
Java Streams và lập trình hàm cho phép:
- Code sạch hơn: Xử lý dữ liệu khai báo, dễ đọc
- Ít boilerplate hơn: Không cần vòng lặp thủ công và biến tạm thời
- Hiệu suất tốt hơn: Song song hóa dễ dàng cho các dataset lớn
- Có thể kết hợp: Móc nối các thao tác một cách tự nhiên
Điểm chính:
- Thành thạo lambda expressions và method references
- Hiểu lazy evaluation và vòng đời stream
- Sử dụng collectors cho tổng hợp mạnh mẽ
- Tận dụng Optional cho null safety
- Biết khi KHÔNG nên sử dụng streams (đơn giản hơn phức tạp)
- Sử dụng parallel streams một cách thận trọng
Bước tiếp theo:
- Xây dựng một pipeline xử lý dữ liệu với streams
- Refactor code mệnh lệnh sang phong cách hàm
- Thực hành với các bài toán stream trên LeetCode/HackerRank
- Khám phá các collectors nâng cao và triển khai tùy chỉnh
- Áp dụng trong các REST APIs Spring Boot (Giai đoạn 3)
Bắt đầu với các chuyển đổi đơn giản và dần dần xây dựng các pipeline phức tạp. Lập trình hàm không phải là về việc sử dụng streams ở khắp mọi nơi—mà là về việc chọn đúng công cụ cho công việc.
Thuộc series Lộ Trình Học Java
Quay lại: Giai Đoạn 3: Core Java APIs
Các Deep Dive Liên Quan:
- Java Collections Framework
- Exception Handling Best Practices
- Java Generics Giải Thích
- Modules và Build Tools
Bước Tiếp Theo: Bắt Đầu Với Spring Boot
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.