1. The Hook (The "Byte-Sized" Intro)
In a Nutshell: Java 8 best practices: (1) Keep lambdas short (1-2 lines max, extract complex logic), (2) Use method references when clearer, (3) Prefer streams for collections (declarative), (4) Avoid side effects in streams (pure functions), (5) Don't overuse parallel streams (overhead, thread safety), (6) Use Optional functionally (not as null check), (7) Choose right functional interface (Predicate for tests, Function for transforms), (8) Handle exceptions properly in lambdas, (9) Use primitive streams (avoid boxing), (10) Know when NOT to use (simple tasks, 1-2 elements)!
Think of power tools. Java 8 = power drill (fast, efficient). But don't use drill to hang picture frame (overkillβuse hammer). Don't overtighten (parallel streams everywhere). Do use right bit (functional interface). Do maintain tool (handle exceptions). Right tool, right job!
2. Conceptual Clarity (The "Simple" Tier)
π‘ The Analogy
- Lambda: Swiss Army knife blade (one function, sharp, focused)
- Stream: Assembly line (each station does one thing)
- Parallel stream: Multiple assembly lines (overhead if product simple)
- Optional: Safety net (explicit null handling)
3. Technical Mastery (The "Deep Dive")
The 10 Golden Rules
- Keep Lambdas Short (1-2 lines)
- Use Method References (when clearer)
- Avoid Side Effects (pure functions)
- Don't Overuse Parallel (measure first)
- Use Optional Functionally (not
isPresent + get) - Choose Right Interface (Predicate/Function/Consumer)
- Handle Exceptions Properly (wrap checked exceptions)
- Use Primitive Streams (avoid boxing)
- Know Stream Laziness (intermediate ops deferred)
- Know When NOT to Use (simplicity over cleverness)
4. Interactive & Applied Code
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class Java8BestPracticesDemo {
// RULE 1: Keep lambdas short
static void demonstrateLambdaLength() {
System.out.println("=== LAMBDA LENGTH ===");
List<String> words = Arrays.asList("apple", "banana", "cherry");
// β BAD: Complex lambda (hard to read/debug)
words.stream()
.filter(w -> {
String upper = w.toUpperCase();
boolean hasVowel = upper.contains("A") || upper.contains("E");
return hasVowel && w.length() > 5;
})
.forEach(System.out::println);
// β
GOOD: Extract to method
words.stream()
.filter(Java8BestPracticesDemo::isComplexCondition)
.forEach(System.out::println);
}
static boolean isComplexCondition(String word) {
String upper = word.toUpperCase();
return (upper.contains("A") || upper.contains("E")) && word.length() > 5;
}
// RULE 3: Avoid side effects
static void demonstrateSideEffects() {
System.out.println("\n=== SIDE EFFECTS ===");
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// β BAD: Side effect in filter (non-deterministic in parallel!)
List<Integer> evens = new ArrayList<>();
numbers.stream()
.filter(n -> {
if (n % 2 == 0) {
evens.add(n); // β Side effect!
return true;
}
return false;
})
.count();
// β
GOOD: Pure function, use collect
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
}
// RULE 4: Don't overuse parallel streams
static void demonstrateParallelStreams() {
System.out.println("\n=== PARALLEL STREAMS ===");
// β BAD: Parallel for small collection (overhead > benefit)
List<Integer> small = Arrays.asList(1, 2, 3, 4, 5);
small.parallelStream()
.map(n -> n * 2)
.collect(Collectors.toList()); // Overhead NOT worth it!
// β
GOOD: Parallel for large, CPU-intensive operations
List<Integer> large = IntStream.rangeClosed(1, 1_000_000)
.boxed()
.collect(Collectors.toList());
long sum = large.parallelStream() // β
Worth it for large dataset
.mapToLong(n -> expensiveComputation(n))
.sum();
}
static long expensiveComputation(int n) {
return n * n; // Imagine complex calculation
}
// RULE 5: Use Optional functionally
static void demonstrateOptionalUsage() {
System.out.println("\n=== OPTIONAL USAGE ===");
// β BAD: Using Optional like null check
Optional<String> opt = findUser(1);
if (opt.isPresent()) {
String user = opt.get();
System.out.println(user.toUpperCase());
}
// β
GOOD: Functional approach
findUser(1)
.map(String::toUpperCase)
.ifPresent(System.out::println);
}
static Optional<String> findUser(int id) {
return id == 1 ? Optional.of("Alice") : Optional.empty();
}
// RULE 7: Handle exceptions in lambdas
static void demonstrateExceptionHandling() {
System.out.println("\n=== EXCEPTION HANDLING ===");
List<String> numbers = Arrays.asList("1", "2", "invalid", "4");
// β BAD: Unchecked exception breaks stream
try {
numbers.stream()
.map(Integer::parseInt) // Throws for "invalid"
.forEach(System.out::println);
} catch (NumberFormatException e) {
System.out.println("Stream broke!");
}
// β
GOOD: Wrap in helper method
numbers.stream()
.map(Java8BestPracticesDemo::parseIntSafe)
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(System.out::println);
}
static Optional<Integer> parseIntSafe(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
// RULE 8: Use primitive streams (avoid boxing)
static void demonstratePrimitiveStreams() {
System.out.println("\n=== PRIMITIVE STREAMS ===");
// β BAD: Boxing overhead (Integer wrapping)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum1 = numbers.stream()
.reduce(0, Integer::sum); // Boxes/unboxes each element!
// β
GOOD: Primitive stream (no boxing)
int sum2 = IntStream.rangeClosed(1, 5)
.sum(); // Works directly with primitive ints
// β BAD: Stream<Integer>
double avg1 = numbers.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
// β
GOOD: IntStream directly
double avg2 = IntStream.rangeClosed(1, 5)
.average()
.orElse(0.0);
}
// RULE 10: Know when NOT to use Java 8 features
static void demonstrateWhenNotToUse() {
System.out.println("\n=== WHEN NOT TO USE ===");
List<String> words = Arrays.asList("apple", "banana");
// β OVERKILL: Stream for simple task
long count1 = words.stream().count();
// β
SIMPLER: Direct method
int count2 = words.size();
// β OVERKILL: Stream for single element access
Optional<String> first = words.stream().findFirst();
// β
SIMPLER: Direct access
String firstWord = words.isEmpty() ? null : words.get(0);
}
}
// Real-world example: Clean stream pipeline
class CleanStreamExample {
static class Employee {
String name;
String dept;
int salary;
Employee(String name, String dept, int salary) {
this.name = name;
this.dept = dept;
this.salary = salary;
}
}
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Alice", "Engineering", 80000),
new Employee("Bob", "Sales", 60000),
new Employee("Carol", "Engineering", 90000)
);
// β
CLEAN PIPELINE: Each operation is clear
Map<String, Double> avgSalaryByDept = employees.stream()
.filter(e -> e.salary > 50000) // Remove low earners
.collect(Collectors.groupingBy(
e -> e.dept, // Group by department
Collectors.averagingInt(e -> e.salary) // Average salary
));
System.out.println("Average salaries: " + avgSalaryByDept);
}
}5. The Comparison & Decision Layer
| Scenario | Do This | Don't Do This |
|---|---|---|
| Complex logic | Extract method | Multi-line lambda |
| Small data | For-loop or direct access | Stream |
| Null handling | Optional functionally | isPresent() + get() |
| Large dataset | Consider parallel | Always parallel |
| Primitive operations | IntStream, LongStream | Stream<Integer> |
6. The "Interview Corner" (The Edge)
The "Killer" Interview Question: "When should you NOT use streams?" Answer: 5 scenarios where streams add complexity:
- Small datasets (1-10 elements) β overhead not worth it
- Index needed β traditional for-loop clearer
- Early break needed β for + break simpler than stream
- Exception handling β checked exceptions awkward in lambdas
- State modification β imperative clearer than side-effect streams
// β OVERKILL: Stream for 2 elements
int sum = Arrays.asList(1, 2).stream().mapToInt(i -> i).sum();
// β
SIMPLER
int sum = 1 + 2;Pro-Tips:
- Parallel stream guidelines:
// β
DO parallel: Large data (10K+), CPU-intensive, independent operations
// β DON'T parallel: Small data, I/O ops, stateful (e.g., forEach side effects)- Stream debug:
list.stream()
.filter(...)
.peek(x -> System.out.println("After filter: " + x)) // Debug
.map(...)
.peek(x -> System.out.println("After map: " + x)) // Debug
.collect(...);- Lambda limitations:
// Can't modify captured variables
// Can't throw checked exceptions (without wrapper)
// Can't break/continue/return from enclosing method