Back to blog

Phase 3: Core Java APIs - Collections, Streams, and More

javacollectionsstreamsapibackend
Phase 3: Core Java APIs - Collections, Streams, and More

Welcome to Phase 3

Welcome to the Core Java APIs phase! If you've completed Phase 1 and Phase 2, you have a solid foundation in Java syntax and object-oriented programming. Now we'll explore Java's powerful standard libraries that you'll use in every real-world application.

These aren't just utility classes—they're battle-tested, optimized APIs that form the backbone of modern Java development. Understanding Collections, Streams, and Exception Handling is essential before building Spring Boot applications.

Time commitment: 2 weeks, 1-2 hours daily
Prerequisite: Completed Phase 1 & 2 or equivalent Java OOP knowledge

What You'll Learn

By the end of Phase 3, you'll be able to:

✅ Choose the right collection type for any scenario
✅ Work efficiently with List, Set, Map, and Queue
✅ Write functional-style code with Streams and lambdas
✅ Handle exceptions gracefully with best practices
✅ Read and write files using modern Java APIs
✅ Work with dates and times correctly
✅ Use Optional for null-safe code
✅ Understand basic multithreading concepts
✅ Manage dependencies with Maven/Gradle

Collections Framework Overview

Java's Collections Framework provides data structures to store, organize, and manipulate groups of objects.

The Collection Hierarchy

Collection (interface)
├── List (interface)
│   ├── ArrayList (class)
│   ├── LinkedList (class)
│   └── Vector (class)
├── Set (interface)
│   ├── HashSet (class)
│   ├── LinkedHashSet (class)
│   └── TreeSet (class)
└── Queue (interface)
    ├── LinkedList (class)
    ├── PriorityQueue (class)
    └── ArrayDeque (class)
 
Map (interface - separate hierarchy)
├── HashMap (class)
├── LinkedHashMap (class)
├── TreeMap (class)
└── Hashtable (class)

Key interfaces:

  • Collection: Root interface for all collections
  • List: Ordered collection (allows duplicates)
  • Set: Unordered collection (no duplicates)
  • Queue: Collection for holding elements before processing
  • Map: Key-value pairs (not a true Collection)

List: Ordered Collections

Lists maintain insertion order and allow duplicates.

ArrayList: Dynamic Array

import java.util.ArrayList;
import java.util.List;
 
// Create ArrayList
List<String> fruits = new ArrayList<>();
 
// Add elements
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
fruits.add("Apple");  // Duplicates allowed
 
// Access by index
String first = fruits.get(0);  // "Apple"
 
// Size
int count = fruits.size();  // 4
 
// Check existence
boolean hasOrange = fruits.contains("Orange");  // true
 
// Remove
fruits.remove("Banana");           // Remove by value
fruits.remove(0);                  // Remove by index
 
// Iterate
for (String fruit : fruits) {
    System.out.println(fruit);
}
 
// Modern iteration with forEach
fruits.forEach(System.out::println);

When to use ArrayList:

  • Random access is frequent (index-based lookup)
  • Few insertions/deletions in the middle
  • Most common List implementation

Performance:

  • get(index): O(1)
  • add(element): O(1) amortized
  • add(index, element): O(n)
  • remove(index): O(n)

LinkedList: Doubly-Linked List

import java.util.LinkedList;
import java.util.List;
 
List<String> tasks = new LinkedList<>();
 
// LinkedList also implements Queue
LinkedList<String> taskQueue = new LinkedList<>();
taskQueue.addFirst("Urgent task");
taskQueue.addLast("Regular task");
String nextTask = taskQueue.removeFirst();

When to use LinkedList:

  • Frequent insertions/deletions at both ends
  • Implementing queue or deque behavior
  • Less common than ArrayList

Performance:

  • get(index): O(n)
  • addFirst/addLast(): O(1)
  • removeFirst/removeLast(): O(1)

Common List Operations

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
 
// Sublist
List<Integer> subset = numbers.subList(1, 4);  // [2, 3, 4]
 
// Sort
numbers.sort(Comparator.naturalOrder());
// Or: Collections.sort(numbers);
 
// Reverse
Collections.reverse(numbers);
 
// Search (binary search requires sorted list)
Collections.sort(numbers);
int index = Collections.binarySearch(numbers, 3);
 
// Replace all
numbers.replaceAll(n -> n * 2);  // [2, 4, 6, 8, 10]
 
// Clear
numbers.clear();

Set: Unique Collections

Sets store unique elements (no duplicates).

HashSet: Fast, Unordered

import java.util.HashSet;
import java.util.Set;
 
Set<String> uniqueNames = new HashSet<>();
 
// Add elements
uniqueNames.add("Alice");
uniqueNames.add("Bob");
uniqueNames.add("Alice");  // Duplicate - won't be added
 
System.out.println(uniqueNames.size());  // 2
 
// Check membership
boolean hasAlice = uniqueNames.contains("Alice");  // true
 
// Remove
uniqueNames.remove("Bob");
 
// Iterate (order not guaranteed)
for (String name : uniqueNames) {
    System.out.println(name);
}

When to use HashSet:

  • Need unique elements
  • Order doesn't matter
  • Fast lookups are critical

Performance:

  • add/remove/contains: O(1) average

LinkedHashSet: Insertion Order

Set<String> orderedSet = new LinkedHashSet<>();
orderedSet.add("First");
orderedSet.add("Second");
orderedSet.add("Third");
 
// Iterates in insertion order: First, Second, Third

When to use LinkedHashSet:

  • Need unique elements
  • Want to maintain insertion order

TreeSet: Sorted Set

import java.util.TreeSet;
 
Set<Integer> sortedNumbers = new TreeSet<>();
sortedNumbers.add(5);
sortedNumbers.add(1);
sortedNumbers.add(3);
 
// Always sorted: [1, 3, 5]
System.out.println(sortedNumbers);
 
// Range operations
TreeSet<Integer> tree = new TreeSet<>(Set.of(1, 3, 5, 7, 9));
Set<Integer> lessThan5 = tree.headSet(5);    // [1, 3]
Set<Integer> greaterThan5 = tree.tailSet(5);  // [5, 7, 9]

When to use TreeSet:

  • Need unique elements
  • Need sorted order
  • Need range queries

Performance:

  • add/remove/contains: O(log n)

Map: Key-Value Pairs

Maps store associations between keys and values.

HashMap: Fast Key-Value Store

import java.util.HashMap;
import java.util.Map;
 
Map<String, Integer> ages = new HashMap<>();
 
// Put key-value pairs
ages.put("Alice", 25);
ages.put("Bob", 30);
ages.put("Charlie", 28);
 
// Get value by key
Integer aliceAge = ages.get("Alice");  // 25
 
// Check key existence
boolean hasAlice = ages.containsKey("Alice");  // true
 
// Check value existence
boolean has25 = ages.containsValue(25);  // true
 
// Get or default
int unknownAge = ages.getOrDefault("Unknown", 0);  // 0
 
// Put if absent
ages.putIfAbsent("Alice", 30);  // Won't update (Alice exists)
 
// Remove
ages.remove("Bob");
 
// Iterate over entries
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
 
// Iterate keys
for (String name : ages.keySet()) {
    System.out.println(name);
}
 
// Iterate values
for (Integer age : ages.values()) {
    System.out.println(age);
}
 
// Modern forEach
ages.forEach((name, age) -> System.out.println(name + " is " + age));

When to use HashMap:

  • Need key-value associations
  • Fast lookups by key
  • Order doesn't matter
  • Most common Map implementation

Performance:

  • get/put/remove: O(1) average

LinkedHashMap: Insertion Order

Map<String, String> orderedMap = new LinkedHashMap<>();
orderedMap.put("First", "1");
orderedMap.put("Second", "2");
orderedMap.put("Third", "3");
 
// Iterates in insertion order

TreeMap: Sorted by Keys

import java.util.TreeMap;
 
Map<String, Integer> sortedMap = new TreeMap<>();
sortedMap.put("Charlie", 28);
sortedMap.put("Alice", 25);
sortedMap.put("Bob", 30);
 
// Always sorted by key: Alice, Bob, Charlie

Advanced Map Operations (Java 8+)

Map<String, Integer> scores = new HashMap<>();
 
// Compute if absent
scores.computeIfAbsent("Alice", k -> 100);  // Alice: 100
 
// Compute if present
scores.computeIfPresent("Alice", (k, v) -> v + 10);  // Alice: 110
 
// Compute
scores.compute("Alice", (k, v) -> v == null ? 0 : v + 5);
 
// Merge (useful for aggregation)
Map<String, Integer> wordCount = new HashMap<>();
String[] words = {"apple", "banana", "apple", "orange", "banana", "apple"};
 
for (String word : words) {
    wordCount.merge(word, 1, Integer::sum);
}
// {apple=3, banana=2, orange=1}

Queue and Deque

Queues hold elements for processing.

Queue Interface

import java.util.LinkedList;
import java.util.Queue;
 
Queue<String> queue = new LinkedList<>();
 
// Add (throws exception if fails)
queue.add("First");
 
// Offer (returns false if fails)
queue.offer("Second");
queue.offer("Third");
 
// Peek (doesn't remove)
String head = queue.peek();  // "First"
 
// Poll (removes and returns, null if empty)
String first = queue.poll();  // "First"
 
// Remove (removes and returns, throws if empty)
String second = queue.remove();  // "Second"

PriorityQueue: Heap-Based Priority Queue

import java.util.PriorityQueue;
 
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(1);
pq.offer(3);
 
// Always polls smallest element
System.out.println(pq.poll());  // 1
System.out.println(pq.poll());  // 3
System.out.println(pq.poll());  // 5
 
// Custom comparator (reverse order)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());

Deque: Double-Ended Queue

import java.util.ArrayDeque;
import java.util.Deque;
 
Deque<String> deque = new ArrayDeque<>();
 
// Add at both ends
deque.addFirst("First");
deque.addLast("Last");
 
// Remove from both ends
String first = deque.removeFirst();
String last = deque.removeLast();
 
// Use as stack
deque.push("Top");
String top = deque.pop();

ArrayDeque is preferred over Stack and LinkedList for stack/queue operations.

Streams: Functional Data Processing

Streams provide a functional approach to processing collections.

Creating Streams

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
 
// From collection
List<String> list = List.of("a", "b", "c");
Stream<String> stream1 = list.stream();
 
// From array
String[] array = {"a", "b", "c"};
Stream<String> stream2 = Arrays.stream(array);
 
// Stream builder
Stream<String> stream3 = Stream.of("a", "b", "c");
 
// Infinite streams
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1);
Stream<Double> randomStream = Stream.generate(Math::random);
 
// Range
IntStream range = IntStream.range(1, 10);  // 1 to 9
IntStream rangeClosed = IntStream.rangeClosed(1, 10);  // 1 to 10

Intermediate Operations (Lazy)

Operations that return a Stream:

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
 
// Filter
numbers.stream()
    .filter(n -> n % 2 == 0)  // [2, 4, 6, 8, 10]
    .forEach(System.out::println);
 
// Map
numbers.stream()
    .map(n -> n * 2)  // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
    .forEach(System.out::println);
 
// FlatMap (flatten nested structures)
List<List<Integer>> nested = List.of(
    List.of(1, 2),
    List.of(3, 4),
    List.of(5, 6)
);
 
nested.stream()
    .flatMap(List::stream)  // [1, 2, 3, 4, 5, 6]
    .forEach(System.out::println);
 
// Distinct (remove duplicates)
List.of(1, 2, 2, 3, 3, 3).stream()
    .distinct()  // [1, 2, 3]
    .forEach(System.out::println);
 
// Sorted
List.of(3, 1, 4, 1, 5).stream()
    .sorted()  // [1, 1, 3, 4, 5]
    .forEach(System.out::println);
 
// Limit and Skip
numbers.stream()
    .skip(3)     // Skip first 3
    .limit(5)    // Take next 5
    .forEach(System.out::println);  // [4, 5, 6, 7, 8]
 
// Peek (for debugging)
numbers.stream()
    .filter(n -> n % 2 == 0)
    .peek(n -> System.out.println("Filtered: " + n))
    .map(n -> n * 2)
    .forEach(System.out::println);

Terminal Operations (Eager)

Operations that trigger computation and return a result:

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
 
// forEach
numbers.stream().forEach(System.out::println);
 
// Collect to List
List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
 
// Collect to Set
Set<Integer> uniqueNumbers = numbers.stream()
    .collect(Collectors.toSet());
 
// Collect to Map
Map<Integer, String> numberMap = numbers.stream()
    .collect(Collectors.toMap(
        n -> n,                    // Key
        n -> "Number " + n         // Value
    ));
 
// Count
long count = numbers.stream()
    .filter(n -> n > 5)
    .count();  // 5
 
// Sum, Min, Max, Average
int sum = numbers.stream().mapToInt(Integer::intValue).sum();
OptionalInt min = numbers.stream().mapToInt(Integer::intValue).min();
OptionalInt max = numbers.stream().mapToInt(Integer::intValue).max();
OptionalDouble avg = numbers.stream().mapToInt(Integer::intValue).average();
 
// Reduce
int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);  // 3628800
 
// AnyMatch, AllMatch, NoneMatch
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0);  // true
boolean allPositive = numbers.stream().allMatch(n -> n > 0);   // true
boolean noneNegative = numbers.stream().noneMatch(n -> n < 0); // true
 
// FindFirst, FindAny
Optional<Integer> first = numbers.stream().findFirst();
Optional<Integer> any = numbers.stream().findAny();

Collectors Examples

import java.util.stream.Collectors;
 
List<String> names = List.of("Alice", "Bob", "Charlie", "David", "Eve");
 
// Joining strings
String joined = names.stream()
    .collect(Collectors.joining(", "));  // "Alice, Bob, Charlie, David, Eve"
 
// Grouping
List<Person> people = List.of(
    new Person("Alice", 25),
    new Person("Bob", 30),
    new Person("Charlie", 25)
);
 
Map<Integer, List<Person>> byAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge));
// {25=[Alice, Charlie], 30=[Bob]}
 
// Partitioning
Map<Boolean, List<Integer>> partitioned = numbers.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0));
// {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}
 
// Summarizing
IntSummaryStatistics stats = numbers.stream()
    .collect(Collectors.summarizingInt(Integer::intValue));
System.out.println("Average: " + stats.getAverage());
System.out.println("Max: " + stats.getMax());

Practical Stream Examples

Example 1: Find top 3 highest salaries

List<Employee> employees = getEmployees();
 
List<Double> top3Salaries = employees.stream()
    .map(Employee::getSalary)
    .sorted(Comparator.reverseOrder())
    .distinct()
    .limit(3)
    .collect(Collectors.toList());

Example 2: Count words in a file

Map<String, Long> wordFrequency = Files.lines(Paths.get("file.txt"))
    .flatMap(line -> Arrays.stream(line.split("\\s+")))
    .map(String::toLowerCase)
    .collect(Collectors.groupingBy(
        word -> word,
        Collectors.counting()
    ));

Example 3: Filter and transform

List<String> activeUserEmails = users.stream()
    .filter(User::isActive)
    .filter(user -> user.getAge() >= 18)
    .map(User::getEmail)
    .sorted()
    .collect(Collectors.toList());

For more advanced Stream patterns, see our deep dive on Java Streams.

Lambda Expressions

Lambdas enable functional-style programming in Java.

Lambda Syntax

// Traditional anonymous class
Runnable runnable1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Running");
    }
};
 
// Lambda expression
Runnable runnable2 = () -> System.out.println("Running");
 
// With parameters
Comparator<Integer> comparator = (a, b) -> a.compareTo(b);
 
// Multiple statements
Comparator<String> stringComparator = (s1, s2) -> {
    int result = s1.length() - s2.length();
    if (result == 0) {
        return s1.compareTo(s2);
    }
    return result;
};

Functional Interfaces

Lambdas work with functional interfaces (interfaces with single abstract method):

@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);
}
 
// Usage
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
 
System.out.println(add.calculate(5, 3));       // 8
System.out.println(multiply.calculate(5, 3));  // 15

Built-in Functional Interfaces

import java.util.function.*;
 
// Predicate<T>: T -> boolean
Predicate<String> isEmpty = String::isEmpty;
System.out.println(isEmpty.test(""));  // true
 
// Function<T, R>: T -> R
Function<String, Integer> length = String::length;
System.out.println(length.apply("Hello"));  // 5
 
// Consumer<T>: T -> void
Consumer<String> print = System.out::println;
print.accept("Hello");
 
// Supplier<T>: () -> T
Supplier<Double> random = Math::random;
System.out.println(random.get());
 
// BiFunction<T, U, R>: (T, U) -> R
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println(add.apply(5, 3));  // 8
 
// UnaryOperator<T>: T -> T
UnaryOperator<Integer> square = x -> x * x;
System.out.println(square.apply(5));  // 25
 
// BinaryOperator<T>: (T, T) -> T
BinaryOperator<Integer> sum = Integer::sum;
System.out.println(sum.apply(5, 3));  // 8

Method References

// Static method reference
Function<String, Integer> parser = Integer::parseInt;
 
// Instance method reference
String str = "Hello";
Supplier<Integer> lengthSupplier = str::length;
 
// Constructor reference
Supplier<List<String>> listSupplier = ArrayList::new;
 
// Examples
List<String> names = List.of("Alice", "Bob", "Charlie");
 
// Instance method of arbitrary object
names.forEach(System.out::println);  // Same as: s -> System.out.println(s)
 
names.stream()
    .map(String::toUpperCase)  // Same as: s -> s.toUpperCase()
    .forEach(System.out::println);

Exception Handling

Exceptions provide a structured way to handle errors.

Exception Hierarchy

Throwable
├── Error (system errors - don't catch)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── ...
└── Exception
    ├── RuntimeException (unchecked)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── IllegalArgumentException
    │   └── ...
    └── Checked Exceptions
        ├── IOException
        ├── SQLException
        ├── ClassNotFoundException
        └── ...

Checked vs Unchecked:

  • Checked: Must be caught or declared (compiler enforced)
  • Unchecked (RuntimeException): Optional to catch

Try-Catch-Finally

public void readFile(String path) {
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new FileReader(path));
        String line = reader.readLine();
        System.out.println(line);
    } catch (FileNotFoundException e) {
        System.err.println("File not found: " + path);
    } catch (IOException e) {
        System.err.println("Error reading file: " + e.getMessage());
    } finally {
        // Always executes (cleanup)
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                // Handle close exception
            }
        }
    }
}

Try-With-Resources (Java 7+)

Automatically closes resources:

public void readFile(String path) {
    try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
        String line = reader.readLine();
        System.out.println(line);
    } catch (IOException e) {
        System.err.println("Error: " + e.getMessage());
    }
    // reader.close() called automatically
}
 
// Multiple resources
try (FileInputStream input = new FileInputStream("input.txt");
     FileOutputStream output = new FileOutputStream("output.txt")) {
    // Use streams
} catch (IOException e) {
    // Handle
}

Requirements: Resource must implement AutoCloseable or Closeable.

Throwing Exceptions

public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("Age cannot be negative");
    }
    this.age = age;
}
 
public void processFile(String path) throws IOException {
    Files.readString(Paths.get(path));  // Throws IOException
}

Custom Exceptions

// Custom checked exception
public class InsufficientFundsException extends Exception {
    private final double amount;
    
    public InsufficientFundsException(double amount) {
        super("Insufficient funds: " + amount);
        this.amount = amount;
    }
    
    public double getAmount() {
        return amount;
    }
}
 
// Usage
public void withdraw(double amount) throws InsufficientFundsException {
    if (amount > balance) {
        throw new InsufficientFundsException(amount);
    }
    balance -= amount;
}
 
// Custom unchecked exception
public class InvalidConfigurationException extends RuntimeException {
    public InvalidConfigurationException(String message) {
        super(message);
    }
    
    public InvalidConfigurationException(String message, Throwable cause) {
        super(message, cause);
    }
}

Exception Best Practices

// ✅ Good: Specific exceptions
public User findUserById(Long id) {
    return userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
 
// ❌ Bad: Generic exception
public User findUserById(Long id) {
    throw new Exception("Error");  // Too generic
}
 
// ✅ Good: Preserve stack trace
try {
    // ...
} catch (IOException e) {
    throw new DataAccessException("Failed to read data", e);  // Include cause
}
 
// ❌ Bad: Swallow exception
try {
    // ...
} catch (Exception e) {
    // Empty catch block - exception lost!
}
 
// ✅ Good: Don't catch what you can't handle
public void processFile(String path) throws IOException {
    Files.readString(Paths.get(path));  // Let caller handle
}
 
// ❌ Bad: Catch and rethrow unnecessarily
public void processFile(String path) {
    try {
        Files.readString(Paths.get(path));
    } catch (IOException e) {
        throw new RuntimeException(e);  // Why wrap?
    }
}

For comprehensive exception handling patterns, see our Exception Handling Deep Dive.

File I/O

Java provides multiple APIs for file operations.

Reading Files (Modern Way)

import java.nio.file.*;
import java.io.IOException;
 
// Read entire file as string
String content = Files.readString(Paths.get("file.txt"));
 
// Read all lines
List<String> lines = Files.readAllLines(Paths.get("file.txt"));
 
// Read bytes
byte[] bytes = Files.readAllBytes(Paths.get("file.bin"));
 
// Stream lines (memory efficient for large files)
try (Stream<String> stream = Files.lines(Paths.get("large.txt"))) {
    stream.filter(line -> line.contains("ERROR"))
          .forEach(System.out::println);
}

Writing Files

// Write string to file
String content = "Hello, World!";
Files.writeString(Paths.get("output.txt"), content);
 
// Write lines
List<String> lines = List.of("Line 1", "Line 2", "Line 3");
Files.write(Paths.get("output.txt"), lines);
 
// Append to file
Files.writeString(
    Paths.get("log.txt"),
    "New log entry\n",
    StandardOpenOption.APPEND
);
 
// Write bytes
byte[] bytes = {1, 2, 3, 4, 5};
Files.write(Paths.get("data.bin"), bytes);

BufferedReader and BufferedWriter

// Reading with BufferedReader
try (BufferedReader reader = Files.newBufferedReader(Paths.get("file.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}
 
// Writing with BufferedWriter
try (BufferedWriter writer = Files.newBufferedWriter(Paths.get("output.txt"))) {
    writer.write("Line 1");
    writer.newLine();
    writer.write("Line 2");
    writer.newLine();
}

File Operations

Path path = Paths.get("file.txt");
 
// Check existence
boolean exists = Files.exists(path);
 
// Check type
boolean isFile = Files.isRegularFile(path);
boolean isDirectory = Files.isDirectory(path);
 
// Get file attributes
long size = Files.size(path);
FileTime modified = Files.getLastModifiedTime(path);
 
// Create directory
Files.createDirectory(Paths.get("newdir"));
Files.createDirectories(Paths.get("parent/child/grandchild"));
 
// Copy
Files.copy(
    Paths.get("source.txt"),
    Paths.get("dest.txt"),
    StandardCopyOption.REPLACE_EXISTING
);
 
// Move/Rename
Files.move(
    Paths.get("old.txt"),
    Paths.get("new.txt"),
    StandardCopyOption.REPLACE_EXISTING
);
 
// Delete
Files.delete(Paths.get("file.txt"));
Files.deleteIfExists(Paths.get("maybe.txt"));
 
// List directory contents
try (Stream<Path> paths = Files.list(Paths.get("."))) {
    paths.filter(Files::isRegularFile)
         .forEach(System.out::println);
}
 
// Walk directory tree
try (Stream<Path> paths = Files.walk(Paths.get("."))) {
    paths.filter(p -> p.toString().endsWith(".java"))
         .forEach(System.out::println);
}

Date and Time API

Java 8 introduced a modern Date and Time API (java.time package).

Core Classes

import java.time.*;
 
// Current date
LocalDate today = LocalDate.now();  // 2024-01-24
 
// Specific date
LocalDate birthday = LocalDate.of(1990, 5, 15);
LocalDate parsed = LocalDate.parse("2024-01-24");
 
// Current time
LocalTime now = LocalTime.now();  // 14:30:15.123
 
// Specific time
LocalTime lunch = LocalTime.of(12, 30);
LocalTime precise = LocalTime.of(14, 30, 15, 500000000);
 
// Date and time
LocalDateTime dateTime = LocalDateTime.now();
LocalDateTime meeting = LocalDateTime.of(2024, 1, 24, 14, 30);
 
// With timezone
ZonedDateTime zonedNow = ZonedDateTime.now();
ZonedDateTime paris = ZonedDateTime.now(ZoneId.of("Europe/Paris"));
 
// Instant (timestamp)
Instant timestamp = Instant.now();

Date Arithmetic

LocalDate today = LocalDate.now();
 
// Add/subtract
LocalDate tomorrow = today.plusDays(1);
LocalDate nextWeek = today.plusWeeks(1);
LocalDate nextMonth = today.plusMonths(1);
LocalDate nextYear = today.plusYears(1);
 
LocalDate yesterday = today.minusDays(1);
 
// Chaining
LocalDate future = today.plusMonths(2).plusDays(5);
 
// With specific values
LocalDate modified = today.withYear(2025)
                         .withMonth(12)
                         .withDayOfMonth(31);

Time Periods and Durations

// Period: date-based (days, months, years)
LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2024, 12, 31);
Period period = Period.between(start, end);
System.out.println(period.getMonths());  // 11
System.out.println(period.getDays());    // 30
 
// Duration: time-based (hours, minutes, seconds)
LocalTime startTime = LocalTime.of(9, 0);
LocalTime endTime = LocalTime.of(17, 30);
Duration duration = Duration.between(startTime, endTime);
System.out.println(duration.toHours());   // 8
System.out.println(duration.toMinutes()); // 510
 
// Create durations
Duration oneHour = Duration.ofHours(1);
Duration fiveMinutes = Duration.ofMinutes(5);
Duration thirtySeconds = Duration.ofSeconds(30);

Formatting and Parsing

LocalDate date = LocalDate.now();
 
// Format
String formatted = date.format(DateTimeFormatter.ISO_DATE);
String custom = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
String text = date.format(DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy"));
// "Friday, January 24, 2024"
 
// Parse
LocalDate parsed = LocalDate.parse("24/01/2024",
    DateTimeFormatter.ofPattern("dd/MM/yyyy"));
 
LocalDateTime dateTime = LocalDateTime.parse("2024-01-24 14:30",
    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));

Practical Examples

// Check if date is in range
public boolean isValidDate(LocalDate date) {
    LocalDate start = LocalDate.of(2024, 1, 1);
    LocalDate end = LocalDate.of(2024, 12, 31);
    return !date.isBefore(start) && !date.isAfter(end);
}
 
// Calculate age
public int calculateAge(LocalDate birthDate) {
    return Period.between(birthDate, LocalDate.now()).getYears();
}
 
// Get next business day
public LocalDate nextBusinessDay(LocalDate date) {
    LocalDate next = date.plusDays(1);
    while (next.getDayOfWeek() == DayOfWeek.SATURDAY ||
           next.getDayOfWeek() == DayOfWeek.SUNDAY) {
        next = next.plusDays(1);
    }
    return next;
}

Optional: Null Safety

Optional is a container that may or may not contain a value.

Creating Optionals

// Empty optional
Optional<String> empty = Optional.empty();
 
// Optional with value
Optional<String> name = Optional.of("Alice");
 
// Optional that might be null
String nullableValue = getName();
Optional<String> optional = Optional.ofNullable(nullableValue);

Checking and Retrieving Values

Optional<String> optional = Optional.of("Hello");
 
// Check if present
if (optional.isPresent()) {
    String value = optional.get();
    System.out.println(value);
}
 
// Better: ifPresent with lambda
optional.ifPresent(value -> System.out.println(value));
optional.ifPresent(System.out::println);
 
// Get or default
String value = optional.orElse("Default");
 
// Get or compute default
String value2 = optional.orElseGet(() -> computeDefault());
 
// Get or throw
String value3 = optional.orElseThrow(() -> 
    new NoSuchElementException("Value not found")
);

Transforming Optionals

Optional<String> name = Optional.of("Alice");
 
// Map
Optional<Integer> length = name.map(String::length);
 
// FlatMap (when mapper returns Optional)
Optional<String> upperCase = name.flatMap(n -> Optional.of(n.toUpperCase()));
 
// Filter
Optional<String> longName = name.filter(n -> n.length() > 5);

Practical Examples

// Instead of:
public String getUserEmail(Long userId) {
    User user = findUserById(userId);
    if (user != null) {
        return user.getEmail();
    }
    return "unknown@example.com";
}
 
// Use Optional:
public String getUserEmail(Long userId) {
    return findUserById(userId)
        .map(User::getEmail)
        .orElse("unknown@example.com");
}
 
// Chaining
Optional<String> result = getUser()
    .filter(User::isActive)
    .map(User::getProfile)
    .map(Profile::getEmail)
    .filter(email -> email.contains("@"));
 
// Java 9+: ifPresentOrElse
optional.ifPresentOrElse(
    value -> System.out.println("Found: " + value),
    () -> System.out.println("Not found")
);
 
// Java 9+: or (fallback Optional)
Optional<String> result = findInCache()
    .or(() -> findInDatabase())
    .or(() -> fetchFromAPI());

Basic Multithreading

Understanding threads is essential for concurrent programming.

Creating Threads

// Extending Thread
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running: " + getName());
    }
}
 
MyThread thread = new MyThread();
thread.start();
 
// Implementing Runnable (preferred)
Runnable task = () -> System.out.println("Task running");
Thread thread2 = new Thread(task);
thread2.start();
 
// With lambda
new Thread(() -> {
    System.out.println("Lambda thread");
}).start();

Thread Methods

Thread thread = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("Count: " + i);
        try {
            Thread.sleep(1000);  // Sleep 1 second
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
});
 
thread.start();  // Start thread
thread.join();   // Wait for thread to complete
 
// Thread info
String name = thread.getName();
long id = thread.getId();
Thread.State state = thread.getState();

ExecutorService (Preferred)

import java.util.concurrent.*;
 
// Fixed thread pool
ExecutorService executor = Executors.newFixedThreadPool(4);
 
// Submit tasks
Future<String> future = executor.submit(() -> {
    Thread.sleep(1000);
    return "Task completed";
});
 
// Get result (blocks)
String result = future.get();
 
// Shutdown
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
 
// Single thread executor
ExecutorService single = Executors.newSingleThreadExecutor();
 
// Cached thread pool (creates threads as needed)
ExecutorService cached = Executors.newCachedThreadPool();
 
// Scheduled executor
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
scheduled.schedule(() -> System.out.println("Delayed task"), 5, TimeUnit.SECONDS);
scheduled.scheduleAtFixedRate(() -> System.out.println("Periodic"), 0, 1, TimeUnit.SECONDS);

Synchronized Access

public class Counter {
    private int count = 0;
    
    // Synchronized method
    public synchronized void increment() {
        count++;
    }
    
    // Synchronized block
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }
    
    public synchronized int getCount() {
        return count;
    }
}
 
// Atomic classes (better performance)
import java.util.concurrent.atomic.AtomicInteger;
 
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet();
atomicCount.decrementAndGet();
int value = atomicCount.get();

Note: Concurrency is a deep topic. This is just an introduction. Spring Boot handles most threading for you.

Maven and Gradle Basics

Build tools manage dependencies and project lifecycle.

Maven (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    
    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>
    
    <dependencies>
        <!-- Example dependency -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>32.1.3-jre</version>
        </dependency>
        
        <!-- Test dependency -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Common Maven commands:

mvn clean          # Clean build artifacts
mvn compile        # Compile source code
mvn test           # Run tests
mvn package        # Create JAR/WAR
mvn install        # Install to local repository
mvn dependency:tree  # Show dependency tree

Gradle (build.gradle)

plugins {
    id 'java'
    id 'application'
}
 
group = 'com.example'
version = '1.0.0'
 
sourceCompatibility = '17'
targetCompatibility = '17'
 
repositories {
    mavenCentral()
}
 
dependencies {
    // Example dependency
    implementation 'com.google.guava:guava:32.1.3-jre'
    
    // Test dependency
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
 
application {
    mainClass = 'com.example.Main'
}
 
test {
    useJUnitPlatform()
}

Common Gradle commands:

gradle clean        # Clean build artifacts
gradle build        # Compile and package
gradle test         # Run tests
gradle run          # Run application
gradle dependencies  # Show dependencies

Maven vs Gradle:

  • Maven: XML-based, convention over configuration, widely adopted
  • Gradle: Groovy/Kotlin DSL, more flexible, faster builds
  • Spring Boot works seamlessly with both

For a complete guide, see our Maven and Gradle Deep Dive.

Best Practices

1. Choose the Right Collection

// ✅ ArrayList for random access
List<String> names = new ArrayList<>();
 
// ✅ LinkedList for frequent insertions at ends
Deque<String> queue = new LinkedList<>();
 
// ✅ HashSet for uniqueness
Set<String> uniqueIds = new HashSet<>();
 
// ✅ HashMap for key-value lookups
Map<String, User> userMap = new HashMap<>();
 
// ✅ TreeSet/TreeMap when you need sorted order
Set<Integer> sortedScores = new TreeSet<>();

2. Use Streams for Readability

// ❌ Imperative
List<String> result = new ArrayList<>();
for (User user : users) {
    if (user.isActive()) {
        result.add(user.getEmail());
    }
}
Collections.sort(result);
 
// ✅ Declarative with streams
List<String> result = users.stream()
    .filter(User::isActive)
    .map(User::getEmail)
    .sorted()
    .collect(Collectors.toList());

3. Handle Exceptions Appropriately

// ✅ Use try-with-resources
try (BufferedReader reader = Files.newBufferedReader(path)) {
    // Use reader
} catch (IOException e) {
    logger.error("Failed to read file: " + path, e);
}
 
// ✅ Don't swallow exceptions
try {
    riskyOperation();
} catch (Exception e) {
    logger.error("Operation failed", e);
    throw new ServiceException("Failed to process", e);
}

4. Use Optional Instead of Null

// ❌ Null checks everywhere
public String getUserEmail(Long id) {
    User user = findUser(id);
    if (user != null && user.getEmail() != null) {
        return user.getEmail();
    }
    return "unknown";
}
 
// ✅ Optional
public Optional<String> getUserEmail(Long id) {
    return findUser(id)
        .map(User::getEmail);
}

5. Use Modern Date/Time API

// ❌ Old Date/Calendar
Date date = new Date();
Calendar calendar = Calendar.getInstance();
 
// ✅ Modern java.time
LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.now();
Instant timestamp = Instant.now();

Summary

You've now mastered Java's core APIs:

✅ Collections Framework (List, Set, Map, Queue)
✅ Streams and functional programming
✅ Lambda expressions and method references
✅ Exception handling with best practices
✅ File I/O with modern NIO.2 API
✅ Date and Time API
✅ Optional for null safety
✅ Basic multithreading concepts
✅ Maven and Gradle fundamentals

These are the essential tools you'll use daily in Spring Boot development.

Next Steps

You're ready to move to Phase 4: Spring Boot Fundamentals, where you'll learn:

  • Spring Boot project structure
  • Dependency injection and IoC
  • RESTful API development
  • Spring Data JPA
  • Spring Security basics
  • Testing with Spring Boot

🚀 Continue to Spring Boot Learning Roadmap →


Previous: Phase 2: Object-Oriented Programming ← Next: Spring Boot Learning Roadmap → Deep Dives:

Happy coding! ☕

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.