> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

# Java Fundamentals for AEM Developers

## 30 – Java Fundamentals for AEM Developers

> **Target:** 3–4 years experienced AEM Developer **Covers from your additional list:** Java fundamentals — the language features that show up constantly in AEM code, built up properly for a developer whose Java is still a work in progress. **Project domain:** a global energy technology company's marketing site.

***

## Before we start — why this file exists, and what it deliberately isn't

File 27 already built JUnit and Mockito up from first principles, because "how do you test this" is a constant AEM interview question and it needed its own foundation. This file does the same thing for a different gap: the Java language features that show up _inside_ the code being tested — collections, generics, exceptions, `equals` / `hashCode`, streams, and the concurrency rules that make an OSGi service safe or dangerous — without which a Sling Model or a service class is something you can copy-paste but not confidently modify or defend under a follow-up question.

**This is not a general Java course.** It's the specific slice of Java that keeps appearing across files 02 through 27 of this repository, explained once, properly, with the AEM context that makes it matter rather than abstract syntax rules. If a concept here feels familiar from an earlier file, that's intentional — this is where it gets the depth those files didn't have room for.

**Why this is worth taking seriously for your specific transition.** Support work rarely requires writing new Java — it requires reading logs, restarting things, and occasionally patching a config. Development work requires writing Java that another developer, and an interviewer, will read and question. The gap between "I can follow this code" and "I can defend a design decision in this code" is almost entirely the gap this file is trying to close.

***

## 1. Introduction

### 1.1 The shape of Java in a typical AEM class

Nearly every piece of AEM Java you'll write fits one of a small number of shapes: a Sling Model interface plus implementation, an OSGi service with a `@Reference` or two, a servlet handling a request, or a small utility class. All four shapes lean on the same underlying Java features repeatedly — collections to hold multifield values, generics to make an adapter or a service reusable, exceptions to signal what went wrong, and (for OSGi services specifically) an understanding of thread safety, because a service is typically a **singleton shared across every concurrent request**.

### 1.2 A real project example

**Requirement.** A Sling Model needed to expose a list of related products for a product detail page, deduplicated by product code, sorted by relevance score, with a sensible default when the multifield was empty.

**What made it a genuine Java exercise, not just an AEM one.** The naive version used a `List<Product>` and manually looped to deduplicate — nested loops, an `ArrayList.contains()` check inside the outer loop, and a bug where two `Product` objects with the same code but built by different code paths weren't recognised as equal because `equals()` had never been overridden.

**The fix.** Overriding `equals()` and `hashCode()` consistently based on product code, switching the intermediate collection to a `LinkedHashSet<Product>` for order-preserving deduplication, and expressing the sort and null-handling with a stream pipeline instead of manual loops.

**Why this is worth remembering as a story, not just a technique.** The bug wasn't in any AEM API — it was a textbook Java mistake (missing `equals()` / `hashCode()`) that happened to surface through an AEM component. That's the pattern this whole file exists to prepare you for: an interviewer asking "why doesn't this work" and the honest answer being pure Java, not Sling.

***

## 2. Core Concepts

### 2.1 Collections — which one, and why it matters in a Sling Model

- **`List`** — ordered, allows duplicates. The right choice when order matters and duplicates are meaningful — a multifield's raw contents, in authoring order.
- **`Set`** — no duplicates, and `LinkedHashSet` specifically keeps insertion order while still deduplicating, which is exactly what the product-list story above needed.
- **`Map`** — key-value lookup. AEM's `ValueMap` and `ResourceResolver` 's adaptables both lean on map-like structures, and a `Map<String, Object>` is the shape you'll build constantly for passing structured data (auth info to `getServiceResourceResolver`, for instance — file 13).

**Why the choice matters beyond style:** picking `List` when you actually need deduplication means writing manual dedup logic (and the bug the story above hit); picking `HashSet` when you need order means a page's product list silently re-orders itself between renders, because `HashSet` makes no order guarantee at all.

### 2.2 Generics — what they buy you, concretely

A method like `resource.adaptTo(ProductModel.class)` only compiles the way it does because `adaptTo` is generic — `<T> T adaptTo(Class<T> type)` — letting the same method return a `ProductModel`, a `Resource`, or any other type based purely on the `Class` argument, with the compiler enforcing the return type at the call site instead of you casting by hand. Without generics, every adaptation would return `Object` and need an explicit, unchecked cast — exactly the kind of thing that fails at runtime instead of compile time.

**The concept worth being able to state plainly:** generics move a class of error from runtime (a `ClassCastException` discovered when a page renders) to compile time (a type mismatch the IDE catches before you even save the file).

### 2.3 Exceptions — checked vs. unchecked, and why AEM APIs are full of both

- **Checked exceptions** (`LoginException`, `RepositoryException`) must be declared or caught — the compiler forces you to acknowledge them. AEM uses these for failures that are expected to happen sometimes under normal operation (a service user's configuration is wrong, a repository operation genuinely fails) and that calling code should be forced to think about.
- **Unchecked exceptions** (`RuntimeException` and its subclasses, like `NullPointerException`) don't need to be declared. They typically represent a programming error rather than an expected failure mode.

**The AEM-specific habit this explains:** `resourceResolverFactory.getServiceResourceResolver(...)` throws a checked `LoginException` precisely because a service user misconfiguration is a real, expected-to-sometimes-happen failure — not a bug — and the API design forces every caller to decide how to handle it rather than let it propagate as a surprise.

### 2.4 Try-with-resources — the mechanism behind file 20 and file 28's leak warnings

Both file 20 (JCR sessions) and file 28 (Sonar findings) treat an unclosed `ResourceResolver` as a serious, recurring bug. The Java mechanism that fixes it: any class implementing `AutoCloseable` (which `ResourceResolver` does) can be opened in a `try (...)` statement, and Java guarantees `close()` runs when the block exits — **on every path, including an exception** — without a `finally` block written by hand.

```java
// The exception-safe pattern used throughout this repository from file 05 onward.
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(authInfo)) {
    return process(resolver);
} // close() is guaranteed here, whether process() returned normally or threw
```

### 2.5 equals() and hashCode() — the contract, and why breaking it is silent

Java's contract: if two objects are `equals()`, they **must** return the same `hashCode()`. Breaking this doesn't throw an error — it silently corrupts anything backed by a hash structure (`HashSet`, `HashMap`), because an object can be "equal" to another by your own logic while landing in a different hash bucket, meaning a `Set` never recognises them as duplicates and a `Map` lookup never finds an existing key.

**Why this is the exact bug in this file's opening story:** two `Product` instances representing the same product, built by different code paths, were logically the same product but were never recognised as such — because Java's default `equals()` (inherited from `Object`) compares object identity, not the fields you actually care about, unless you override it.

```java
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Product)) return false;
    return Objects.equals(productCode, ((Product) o).productCode);
}

@Override
public int hashCode() {
    return Objects.hash(productCode);
}
```

### 2.6 Streams and lambdas — expressing "what," not "how"

A stream pipeline describes the _result_ you want, letting the JVM handle the _mechanics_ of the loop:

```java
// The opening story's fix, expressed as a pipeline: dedupe by product code,
// sort by relevance, and fall back to an empty list rather than null.
List<Product> related = Optional.ofNullable(rawRelatedProducts)
    .orElse(Collections.emptyList())
    .stream()
    .collect(Collectors.toMap(Product::getCode, p -> p, (first, second) -> first, LinkedHashMap::new))
    .values()
    .stream()
    .sorted(Comparator.comparingDouble(Product::getRelevanceScore).reversed())
    .collect(Collectors.toList());
```

**The habit worth internalising, not just the syntax:** a stream pipeline that reads like a sentence ("take these, keep the first by code, sort by relevance") is doing exactly what the nested-loop version did, but the intent is visible in the code itself rather than buried in loop-index bookkeeping — which is also why it's easier for a reviewer (or an interviewer reading your code live) to verify it's correct.

### 2.7 Optional — for return values, not for every variable

`Optional<T>` exists to make "this might not have a value" visible in a method's signature, forcing a caller to handle the missing case explicitly rather than risk a `NullPointerException` three calls later. The AEM-specific discipline: use it for a method's **return type** when absence is a normal outcome (`Optional<String> findConfiguredEndpoint()`), but don't scatter `Optional` fields or method parameters through a Sling Model — that's a well-known Java anti-pattern, and AEM's `@ValueMapValue` injection already has its own, simpler way to express "this property might be absent" (a default value, or `Optional<String>` specifically as an injectable field type, which the Sling Models framework supports directly).

### 2.8 Static vs. instance state — the concept that makes or breaks a thread-safe OSGi service

An OSGi component (file 06) is typically instantiated **once** and shared across every concurrent request the instance handles. That single fact changes how you're allowed to write it:

- An **instance field** that's mutated per-request (`private List<String> results;` set inside a method and read later) is shared, mutable state across every thread calling that service concurrently — a race condition waiting to happen, and one of the most common real bugs in AEM services written by developers used to request-scoped web frameworks where "one instance per request" is often the default assumption.
- A **local variable** inside a method, by contrast, is stack-allocated per call and inherently thread-safe — nothing to synchronise, because nothing is shared.

**The rule worth stating outright:** an OSGi service should be effectively **stateless** — instance fields should hold only immutable configuration (set once via `@Activate`, never mutated afterward), and any data specific to one request should live in local variables or be passed as parameters, never stored on the service instance.

### 2.9 A minimal, honest concurrency vocabulary

You don't need to be a concurrency expert for a 3-4 year AEM interview, but three terms come up and are worth being precise about:

- **`synchronized`** — a block or method that only one thread can execute at a time on a given object. A last resort for genuinely shared mutable state, and a performance cost if overused.
- **`volatile`** — guarantees a field's writes are visible to other threads immediately, without providing mutual exclusion. Used for simple flags, not compound operations.
- **`ConcurrentHashMap`** — a thread-safe map without needing to `synchronized` every access yourself, useful for the rare legitimate case of a service caching something across requests.

**The honest framing for an interview:** the best answer to "how do you make this OSGi service thread-safe" is usually "avoid mutable shared state entirely" — reaching for `synchronized` is what you do when statelessness genuinely isn't possible, not a first resort.

### 2.10 Interfaces vs. abstract classes — why Sling Models use interfaces the way they do

File 05 and file 23 already established the pattern: a Sling Model interface with an `Impl` implementation, or a Sling Model exported via `ComponentExporter`. The Java reasoning underneath: an **interface** defines a contract with no shared implementation, letting the HTL template (or a headless JSON exporter) depend only on the interface's method signatures — never on how the data is actually fetched — which is the same dependency-inversion idea behind testing a Sling Model against `AemContext` rather than a real repository (file 27). An **abstract class** is the right tool instead when several related classes genuinely share common implementation, not just a common contract — less common in typical AEM component code, more common in a shared base class for several similar OSGi services.

***

## 3. Internal Working

### 3.1 Why string concatenation in a loop is a real, measurable problem

`String` is immutable — every `+` concatenation creates a **new** `String` object rather than modifying one in place. Inside a loop, that means allocating and discarding a new string on every iteration:

```java
// Quietly O(n²): each += allocates a new String and copies everything so far.
String html = "";
for (Product p : products) {
    html += "<li>" + p.getName() + "</li>";
}

// StringBuilder mutates an internal buffer — genuinely O(n).
StringBuilder html = new StringBuilder();
for (Product p : products) {
    html.append("<li>").append(p.getName()).append("</li>");
}
```

This rarely matters for three items in a dropdown; it matters for the kind of loop that renders every product on a large listing page — exactly file 02's Load More component territory — where the naive version's cost grows quietly worse as the page grows.

### 3.2 Why a checked exception on a factory method forces a design decision

`getServiceResourceResolver(...)` throwing `LoginException` as a checked exception means the compiler won't let a caller ignore the possibility. That forces one of a small number of honest choices at every call site: handle it and degrade gracefully (return an empty result, log and move on), or declare it and let a caller further up decide — but never silently swallow it without a decision being made, because the compiler won't compile code that just ignores a checked exception.

### 3.3 How autoboxing quietly causes a subtle bug

`Integer` and `int` aren't quite interchangeable — comparing two boxed `Integer` objects with `==` compares references, not values, and it only appears to work for small values because the Java language specification requires autoboxing to reuse cached `Integer` objects from -128 to 127. Larger values, or objects created with `new`, are separate objects, so never rely on `==` for boxed numbers:

```java
Integer a = 200;
Integer b = 200;
a == b;          // false — not guaranteed to be the same cached object
a.equals(b);     // true — always correct for comparing values
```

**The rule:** always use `.equals()` (or unbox to a primitive `int` first) to compare boxed numeric types — never `==`.

***

## 8. Coding Examples

### 8.1 Deduplicating while preserving order — the opening story's actual fix

```java
public List<Product> getRelatedProducts() {
    if (rawRelatedProducts == null) {
        return Collections.emptyList();
    }
    // LinkedHashMap keyed by product code: keeps the FIRST occurrence
    // of each code, in the order encountered — exactly "dedupe, keep order."
    Map<String, Product> deduped = new LinkedHashMap<>();
    for (Product product : rawRelatedProducts) {
        deduped.putIfAbsent(product.getCode(), product);
    }
    List<Product> result = new ArrayList<>(deduped.values());
    result.sort(Comparator.comparingDouble(Product::getRelevanceScore).reversed());
    return result;
}
```

### 8.2 A stateless OSGi service versus the leaking version from Story 2

```java
// WRONG: currentContext is shared, mutable state on a singleton component —
// a race condition the moment two threads call generatePreview() concurrently.
@Component(service = PreviewService.class)
public class PreviewServiceImpl implements PreviewService {
    private PageContext currentContext; // <-- the bug

    @Override
    public String generatePreview(Resource pageResource) {
        currentContext = buildContext(pageResource);
        return render(currentContext);
    }
}

// FIXED: no shared state at all — currentContext is a local variable,
// so each concurrent call gets its own, safely.
@Component(service = PreviewService.class)
public class PreviewServiceImpl implements PreviewService {
    @Override
    public String generatePreview(Resource pageResource) {
        PageContext currentContext = buildContext(pageResource);
        return render(currentContext);
    }
}
```

### 8.3 A readable stream pipeline, split from Story 3's unreadable version

```java
// Before: correct, but everything happens in one unreadable chain.
List<String> result = products.stream()
    .filter(p -> p.isActive() && p.getStock() > 0)
    .peek(p -> log.debug("Considering {}", p.getCode()))
    .collect(Collectors.groupingBy(Product::getCategory))
    .values().stream().flatMap(List::stream)
    .map(Product::getName).collect(Collectors.toList());

// After: named intermediate steps, no side effects hidden inside the stream.
List<Product> availableProducts = products.stream()
    .filter(p -> p.isActive() && p.getStock() > 0)
    .collect(Collectors.toList());

log.debug("Considering {} available products", availableProducts.size());

Map<String, List<Product>> byCategory = availableProducts.stream()
    .collect(Collectors.groupingBy(Product::getCategory));

List<String> result = byCategory.values().stream()
    .flatMap(List::stream)
    .map(Product::getName)
    .collect(Collectors.toList());
```

***

## 9. Common Mistakes

| Mistake                                                                         | Why it happens                                                            | The actual cost                                                                      |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Not overriding `equals()` / `hashCode()` on a class used as a `Set` / `Map` key | Default identity comparison "usually" seems to work in small tests        | Silent deduplication failures with real data from multiple sources                   |
| Storing per-request data in an OSGi service's instance field                    | Habit from request-scoped frameworks                                      | A race condition invisible until real concurrent traffic                             |
| String concatenation in a loop over many items                                  | Reads simply, works fine at small scale                                   | Quietly quadratic cost on a large listing                                            |
| Comparing boxed `Integer` / `Long` with `==`                                    | Small test values happen to be cached and compare "correctly" by accident | A bug that only appears with larger real values                                      |
| Reaching for `synchronized` as a first resort                                   | Feels like the "safe" answer to a thread-safety question                  | Unnecessary contention where a stateless redesign would have been simpler and faster |
| Scattering `Optional` through fields and parameters                             | Read about it as a "modern Java" pattern without the caveat               | Verbose, unidiomatic code — `Optional` is for return values, not everywhere          |
| An unreadable one-line stream pipeline                                          | "It's more concise" without checking it's still clear                     | A future reader (including you, under pressure) misreads or fears touching it        |
| Swallowing a checked exception with an empty catch block                        | Made the compiler stop complaining, fastest path to a green build         | A real failure disappears silently instead of being handled or logged                |

***

## 10. Best Practices

- **Override `equals()` and `hashCode()` together, consistently, on any class used as a `Set` / `Map` key or compared for logical equality** — never leave it to default identity comparison.
- **Treat an OSGi service as stateless by default.** Instance fields hold only immutable configuration set once; per-request data lives in local variables or parameters.
- **Use `StringBuilder` for concatenation inside a loop**, and plain `+` for a handful of one-off concatenations where readability wins and the cost is irrelevant.
- **Compare boxed numeric types with `.equals()`**, never `==`.
- **Use `Optional` as a return type for genuinely absent values — not as a field or parameter type scattered through a class.**
- **Name intermediate steps in a stream pipeline once it stops being obviously readable in one chain** — a pipeline should be easier to read than the loop it replaced, not harder.
- **Never swallow a checked exception silently** — handle it with an explicit, logged decision, or declare it and let a caller decide.
- **Reach for `synchronized` / `volatile` /concurrent collections only after confirming statelessness genuinely isn't possible** — not as a first response to a thread-safety question.

***

## 11. Debugging Tips

| Symptom                                                                        | Where to look                                                             | What it usually means                                                |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| A `Set` or `Map` "loses" entries that look identical                           | Whether `equals()` / `hashCode()` are overridden on the key/element class | Default identity comparison silently failing to recognise duplicates |
| Data from one request appears to leak into another                             | Any mutable instance field on an OSGi service                             | Shared singleton state under concurrent access                       |
| A listing page gets noticeably slower as content grows                         | String concatenation inside the render loop                               | Quadratic cost from repeated `String` allocation                     |
| A numeric comparison works in tests but fails with real data                   | Whether boxed types are compared with `==`                                | The `Integer` cache masking the bug for small values                 |
| A stream pipeline "does something wrong" but the logic looks right at a glance | Whether operations are ordered/chained in a way that's easy to misread    | Split it into named steps to verify each stage independently         |

***

## 12. Performance Notes

- `StringBuilder` versus repeated `+=` concatenation matters proportionally to loop size — negligible for a handful of items, real for a large listing page (file 02's territory).
- Stream pipelines are not inherently faster than an equivalent loop, and sometimes slightly slower due to overhead — the reason to prefer them is readability and correctness of intent, not raw speed. Don't claim a performance win in an interview that isn't the actual reason you'd choose one.
- Overusing `synchronized` on a hot path (a servlet or Sling Model method called on every request) can become a real bottleneck under load — which is exactly why statelessness is the preferred fix, avoiding the need for synchronisation entirely rather than making synchronisation fast.

***

## 15. Comparison Tables

|                 | **`List`**                                                | **`Set`**                              | **`Map`**                            |
| --------------- | --------------------------------------------------------- | -------------------------------------- | ------------------------------------ |
| Duplicates      | Allowed                                                   | Not allowed                            | Keys unique, values can repeat       |
| Order           | Insertion order (implementation-dependent for some types) | No guarantee, except `LinkedHashSet`   | No guarantee, except `LinkedHashMap` |
| Typical AEM use | A multifield's raw values                                 | Deduplicating by identity/business key | Structured data, auth info maps      |

|                         | **Checked exception**                   | **Unchecked exception**                            |
| ----------------------- | --------------------------------------- | -------------------------------------------------- |
| Must be declared/caught | Yes                                     | No                                                 |
| Represents              | An expected, sometimes-happens failure  | Usually a programming error                        |
| AEM example             | `LoginException`, `RepositoryException` | `NullPointerException`, `IllegalArgumentException` |

|                                                        | **Instance field**                       | **Local variable**                   |
| ------------------------------------------------------ | ---------------------------------------- | ------------------------------------ |
| Shared across concurrent calls on a singleton service? | Yes                                      | No                                   |
| Safe for per-request data on an OSGi service?          | **No**                                   | Yes                                  |
| Appropriate use                                        | Immutable config set once at `@Activate` | Anything specific to one method call |

|        | **`synchronized`**                       | **`volatile`**                        | **Stateless design**         |
| ------ | ---------------------------------------- | ------------------------------------- | ---------------------------- |
| Solves | Mutual exclusion for compound operations | Visibility of a single field's writes | Removes the need for either  |
| Cost   | Contention under load                    | None, but limited guarantee           | None — the preferred default |

***

## Next topic

**System design basics for AEM developers** — how to approach an open-ended "design a system" question when the system is a multi-site, headless-capable AEM platform: scalability, caching layers, content modelling for reuse, and the trade-offs an AEM-specific system design answer is expected to name.

***

_Topic 30 of the AEM Interview Preparation repository. Teaching style, energy-sector project domain._
