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) amortizedadd(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, ThirdWhen 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 orderTreeMap: 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, CharlieAdvanced 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 10Intermediate 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)); // 15Built-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)); // 8Method 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 treeGradle (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 dependenciesMaven 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:
- Java Collections Framework →
- Streams & Functional Programming →
- Exception Handling Best Practices →
- Modules and Build Tools →
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.