Files
tarun-elango be31df2d44 more text
2026-04-26 14:09:04 -04:00

13 KiB

File 3: Core Java APIs

The Java language by itself is only part of the story. What makes Java productive in real work is the standard library. Once you move beyond syntax, you spend large amounts of time with String, collections, generics, exceptions, file and stream APIs, time APIs, and utility classes.

This file focuses on the APIs that show up constantly in real applications. These topics are foundational because they influence how data moves through the system, how failures are handled, and how code remains readable and type-safe at scale.

Strings and Immutability

Intuition

A String is one of the most frequently used objects in Java. It represents text such as user names, JSON fields, URLs, tokens, file paths, SQL fragments, log messages, and HTTP headers.

Java makes String immutable, which means once a string object is created, its contents do not change.

Why Immutability Matters

Immutability makes strings:

  • safer to share across threads
  • easier to reason about
  • more predictable as map keys and cache keys
  • less error-prone in APIs that pass text through multiple layers

Example

String original = "order";
String upper = original.toUpperCase();

System.out.println(original); // order
System.out.println(upper);    // ORDER

original is unchanged. toUpperCase() returns a new String.

Internal Detail That Matters

Java often interns string literals, meaning identical literals may refer to the same pooled object. That is an optimization detail, not something business logic should rely on.

This is why using == for strings is dangerous.

String a = new String("paid");
String b = new String("paid");

System.out.println(a == b);      // false
System.out.println(a.equals(b)); // true

Performance Concern

Repeated string concatenation inside loops can create many temporary objects.

String result = "";
for (String item : items) {
    result += item;
}

Prefer StringBuilder for heavy concatenation.

StringBuilder builder = new StringBuilder();
for (String item : items) {
    builder.append(item);
}
String result = builder.toString();

Real-World Use Cases

  • building log lines or audit entries
  • parsing and validating incoming request fields
  • constructing file paths, cache keys, and search queries
  • formatting API responses or templated messages

Common Pitfalls

  • comparing strings with ==
  • forgetting that strings are immutable and assuming a method call mutates them
  • building large strings inefficiently in loops

Collections Framework

Collections are how Java code represents groups of related elements. In real systems, almost every workflow involves a collection somewhere: request headers, order items, search results, cache entries, event batches, deduplicated IDs, grouped metrics, or lookup tables.

Collections Structure Diagram

flowchart TD
    A[Collection Framework] --> B[List]
    A --> C[Set]
    A --> D[Map]
    B --> E[ArrayList]
    B --> F[LinkedList]
    C --> G[HashSet]
    C --> H[TreeSet]
    D --> I[HashMap]
    D --> J[TreeMap]

List

A List is ordered and allows duplicates.

List<String> steps = new ArrayList<>();
steps.add("validate");
steps.add("charge");
steps.add("notify");

Use a list when order matters or when repeated values are acceptable.

ArrayList

This is the default List choice in most business applications. It provides fast indexed access and good general-purpose performance.

Typical use cases:

  • ordered API results
  • batched work items
  • DTO lists in controller responses

LinkedList

Used far less often in typical backend applications than beginners expect. It can be helpful in queue-like patterns, but ArrayList and dedicated queue implementations are usually more practical.

Set

A Set stores unique elements.

Set<String> roles = new HashSet<>();
roles.add("ADMIN");
roles.add("USER");
roles.add("ADMIN");

The duplicate ADMIN is ignored.

Use a set when uniqueness matters.

Real-world examples:

  • deduplicating email addresses
  • tracking processed event IDs for idempotency checks
  • storing permission names

Map

A Map stores key-value pairs.

Map<String, Integer> inventory = new HashMap<>();
inventory.put("laptop", 15);
inventory.put("mouse", 48);

Maps are everywhere in backend systems.

Typical examples:

  • ID to entity lookup
  • configuration name to value
  • region to tax rule
  • user ID to session metadata

Choosing Common Implementations

Type Default Choice Why
List ArrayList Fast iteration and index-based reads
Set HashSet Fast uniqueness checks
Map HashMap Fast key-based lookup

Choose tree-based variants like TreeSet or TreeMap when you explicitly need sorted ordering.

Practical Usage Pattern

Suppose you receive a batch of orders and want to group them by customer.

Map<String, List<Order>> ordersByCustomer = new HashMap<>();

for (Order order : orders) {
    ordersByCustomer
            .computeIfAbsent(order.getCustomerId(), key -> new ArrayList<>())
            .add(order);
}

This is a very common pattern in reporting, aggregation, and batch-processing pipelines.

Pitfalls

  • assuming HashMap preserves insertion order; it does not
  • forgetting that mutable keys can break map behavior
  • choosing a list when you really need uniqueness or fast lookup
  • modifying a collection while iterating in unsupported ways, causing ConcurrentModificationException

Generics

Generics allow Java to express type-safe reusable data structures and APIs.

Intuition

Without generics, collections would hold generic Object values, forcing you to cast manually and discover mistakes late. Generics move those checks to compile time.

List<String> names = new ArrayList<>();
names.add("Anita");

Now the compiler knows this list is supposed to contain strings.

Why This Matters in Real Systems

Generics make API contracts clearer.

Examples:

  • List<Order> means a list of orders, not arbitrary objects
  • Map<String, UserSession> means string keys mapped to session objects
  • ResponseEntity<CustomerDto> clearly communicates response payload type in a web application

Generic Methods

public static <T> T first(List<T> items) {
    if (items.isEmpty()) {
        throw new IllegalArgumentException("List cannot be empty");
    }
    return items.get(0);
}

Wildcards at a High Level

You will eventually see forms such as List<? extends Number> or List<? super Integer>. The full details take practice, but the intuition is:

  • extends is for reading from a producer
  • super is for writing to a consumer

This is often summarized as PECS: producer extends, consumer super.

Internal Note

Java generics use type erasure, meaning generic type information is mostly removed at runtime. This is why you cannot do everything with generics that reified generic systems in some other languages allow.

Common Pitfalls

  • using raw types like List instead of List<String>
  • overcomplicating code with advanced wildcards when a simpler API would do
  • expecting generic type arguments to remain fully available at runtime

Exception Handling

Failure handling is a core part of Java engineering. Good code does not just describe the happy path. It makes failure modes visible and deliberate.

The Idea Behind Exceptions

An exception represents a failure or abnormal condition that interrupts normal execution.

Some failures are expected operationally, such as missing files or invalid user input. Others represent programming bugs, such as null dereferences or invalid assumptions.

Checked vs Unchecked Exceptions

Java has two broad exception categories.

Checked exceptions

These are enforced by the compiler. You must catch them or declare them.

Examples include many file and I/O related exceptions.

Unchecked exceptions

These extend RuntimeException. The compiler does not force handling.

Examples include:

  • NullPointerException
  • IllegalArgumentException
  • IllegalStateException

Why the Distinction Exists

Checked exceptions signal recoverable or expected external failure modes.

Unchecked exceptions often represent programming mistakes or invalid internal states.

In real engineering, teams vary on how much they like checked exceptions, but understanding the model is still important.

Example

public String readFile(Path path) throws IOException {
    return Files.readString(path);
}

The caller must decide whether to handle IOException or propagate it.

Practical Guidance

  • throw exceptions that communicate intent clearly
  • do not catch exceptions just to ignore them
  • wrap low-level exceptions when exposing a cleaner domain-level abstraction
  • include useful context in logs and messages

Real-World Pattern

Suppose a service calls a third-party payment gateway. The low-level HTTP client might throw transport exceptions, but your business layer may convert those into a domain-specific exception like PaymentUnavailableException.

That makes the rest of the system easier to understand.

try, catch, finally, and try-with-resources

try (BufferedReader reader = Files.newBufferedReader(path)) {
    return reader.readLine();
} catch (IOException exception) {
    throw new IllegalStateException("Failed to read config", exception);
}

The try-with-resources form is important because it closes resources automatically.

Pitfalls

  • catching Exception too broadly and hiding real issues
  • using exceptions for normal control flow
  • logging and rethrowing the same exception repeatedly, creating noisy duplicate logs
  • losing the original cause when wrapping exceptions badly

Java I/O Basics

I/O means interacting with systems outside your program's in-memory state.

Examples include:

  • reading files
  • writing logs
  • handling network sockets
  • streaming data to cloud storage
  • loading configuration from disk

Intuition

I/O is slower and less predictable than pure in-memory operations because it depends on disks, networks, operating system buffers, remote services, and external state.

That is why I/O-heavy code needs stronger error handling and performance awareness.

Core Modern APIs

The java.nio.file package is the common modern entry point for file work.

Path path = Path.of("config/app.properties");
String content = Files.readString(path);
Files.writeString(path, content + "\nmode=prod");

Streams of Data

Buffered APIs are often used for efficient reading and writing.

try (BufferedReader reader = Files.newBufferedReader(path)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

Production Use Cases

  • loading service configuration or templates at startup
  • ingesting CSV or log files in batch jobs
  • exporting reports to disk or object storage
  • reading secrets or certificates mounted into a container

Common Pitfalls

  • reading huge files into memory when streaming would be safer
  • forgetting character encoding concerns
  • failing to close resources properly
  • performing blocking I/O on critical request threads without understanding throughput impact

Practical Usage Patterns That Show Up Everywhere

Pattern 1: Deduplication

Use a Set when idempotency or uniqueness matters.

Set<String> processedEventIds = new HashSet<>();

Pattern 2: Lookup by Key

Use a Map for fast retrieval.

Map<Long, Customer> customersById = new HashMap<>();

Pattern 3: Ordered Results

Use a List when sequence matters.

List<Transaction> transactions = new ArrayList<>();

Pattern 4: Safe Text Handling

Use immutable strings for identifiers, payload fragments, and logs, but switch to StringBuilder for heavy concatenation.

Pattern 5: Resource Safety

Use try-with-resources for anything that must be closed, such as readers, streams, sockets, and many database-facing abstractions.

How These APIs Show Up in Production Systems

Consider a batch job that processes order exports:

  1. it reads a file using Java I/O APIs
  2. it parses each line into objects and stores them in collections
  3. it uses maps for lookups and sets for deduplication
  4. it uses strings to validate and normalize text fields
  5. it throws or wraps exceptions when bad data or file issues occur
  6. it emits a summary report built safely and efficiently

This is ordinary Java engineering. It is not glamorous, but a large amount of production reliability depends on doing these basics well.

Key Takeaways

  • String is immutable, which improves safety and predictability, but you should still be mindful of comparison and concatenation costs.
  • The collections framework gives you distinct tools for ordering, uniqueness, and lookup, and choosing the right one affects both clarity and performance.
  • Generics move many type errors to compile time and make library and application code much easier to reason about.
  • Exception handling is about making failure explicit and meaningful, not just preventing crashes.
  • Java I/O interacts with slow and failure-prone external systems, so resource handling and error handling matter a lot.
  • These APIs form the everyday vocabulary of production Java code, especially in backend services, batch jobs, and integration-heavy systems.