Java Collections emptyList() Method with Examples: A Practical Guide

When I’m building a service that ships to production, the smallest decisions in the collections layer can have outsized effects on reliability. An empty list is one of those deceptively small choices. If you return null, someone will forget a null check. If you return a new ArrayList every time, you create noise in profiling and you risk accidental mutation. I’ve learned to reach for a single, explicit answer: the empty list that can’t be modified. That’s exactly what Collections.emptyList() gives you—an immutable, singleton list with no elements.

You’ll see why this method is a safe default for APIs, how it differs from other empty-list options, and how it behaves under generics. I’ll also show the kinds of errors you’ll trigger if you try to mutate it, and the right patterns for converting it when you really do need a mutable list. Along the way I’ll connect the method to real-world code: service layers, DTOs, caches, and tests. By the end, you’ll have a practical playbook for when to return it, when not to, and how to explain the choice to your team.

What Collections.emptyList() Actually Returns

The method signature is short but important:

public static final  List emptyList()

A few key points I call out to teams:

  • It accepts no parameters.
  • It returns a List with zero elements.
  • That list is immutable—attempting to add, remove, or set elements will throw an UnsupportedOperationException.

Think of it like a sealed envelope labeled “empty.” You can hand it to anyone, and they can read that it’s empty. But the seal is permanent; you can’t slip anything inside without breaking the rules. That guarantees that once you return an empty list, nobody can quietly mutate it and affect a shared state.

Basic example

This is the cleanest and most common usage:

// Java program to create an empty list

import java.util.*;

public class Demo {

public static void main(String[] args) {

// Create a list that is empty

List roles = Collections.emptyList();

// Display the list

System.out.println(roles);

}

}

Output:

[]

It prints exactly what you expect: an empty list literal.

Immutability and the Exception You’ll See

The most important behavior: this list is immutable. If you treat it like a normal list, Java will protect you with a runtime exception. That’s not a bug; it’s the contract.

// Java program to show an exception when adding elements to the list

import java.util.*;

public class Demo {

public static void main(String[] args) {

// Create a list that is empty

List scores = Collections.emptyList();

// Add elements to the created list

scores.add(95);

scores.add(88);

scores.add(73);

// Display the list

System.out.println(scores);

}

}

Output:

Exception in thread "main" java.lang.UnsupportedOperationException

at java.util.AbstractList.add(AbstractList.java:148)

at java.util.AbstractList.add(AbstractList.java:108)

at Demo.main(Demo.java:11)

This is a protective failure. If the list were mutable and shared, someone could accidentally add to it and affect other code that assumed it was empty. Immutability makes that assumption permanent.

Why I Prefer It Over new ArrayList()

When someone asks why I return Collections.emptyList() instead of new ArrayList(), I explain it in terms of intent and safety:

  • Intent is explicit. Returning an immutable empty list signals “this is empty and should remain empty.”
  • No accidental mutation. A new list can be modified by any caller, which can hide bugs.
  • Avoids unnecessary allocations. The method returns a shared instance, so you don’t allocate a new list each call.

Imagine a search endpoint that sometimes finds no results. If you return a mutable list, a caller might add an element for testing or debugging and accidentally send it downstream. An immutable empty list prevents that pattern entirely.

When I still use new ArrayList()

I only prefer a fresh list when I’m about to populate it. For example, building a result list in a method before returning it is a fine time for a mutable list. But if you already know the answer is empty, use the immutable one.

Generics and Type Inference Without Noise

A common question is how to use it with generics without ugly casting. This is the pattern I recommend:

List orders = Collections.emptyList();

Java’s type inference handles it well in most cases. In older codebases or complex inference contexts, you might see the explicit type parameter:

List orders = Collections.emptyList();

I only use the explicit form when the compiler complains. In modern Java, the simpler one usually works.

Returning from a method

When you’re writing an API, this is a clean and expressive return:

public List findProfiles(String teamId) {

if (teamId == null || teamId.isBlank()) {

// Team id missing; no results should be returned

return Collections.emptyList();

}

// Query omitted for brevity

return Collections.emptyList();

}

Notice how the method signature enforces the type, so inference is straightforward.

Real-World Patterns I Use in Services

In real service code, emptyList() helps remove ambiguity. Here are a few patterns I recommend.

Pattern 1: Defensive returns from repositories

When your data access layer returns null, you push the burden to every caller. I prefer to normalize in the repository itself:

public List findProjectsByOwner(UUID ownerId) {

List results = database.queryProjects(ownerId);

if (results == null || results.isEmpty()) {

return Collections.emptyList();

}

return results;

}

That way the caller can iterate safely without null checks.

Pattern 2: DTOs and serialization

Empty lists serialize cleanly to JSON as []. That’s much more predictable than null.

public class AccountSummary {

private final List activeRegions;

public AccountSummary(List activeRegions) {

// Keep API responses stable

this.activeRegions = (activeRegions == null)

? Collections.emptyList()

: activeRegions;

}

public List getActiveRegions() {

return activeRegions;

}

}

This is also more frontend-friendly. React or mobile clients can just map over the array.

Pattern 3: Caches and memoization

When caching expensive queries, I store empty lists explicitly so I don’t repeat the query:

public List findSubscriptions(String userId) {

return cache.computeIfAbsent(userId, id -> {

List dbResults = subscriptionDao.findByUserId(id);

return (dbResults == null || dbResults.isEmpty())

? Collections.emptyList()

: dbResults;

});

}

This avoids cache misses for “no data” cases.

When NOT to Use emptyList()

Even though I like it, it’s not always the right choice. Here’s when I avoid it.

1) You must mutate the returned list

If the caller is expected to add items, return a mutable list instead. Otherwise you’ll get runtime exceptions and angry consumers.

public List buildMetrics() {

List metrics = new ArrayList();

// Populate the list here

return metrics;

}

2) You need a specific list implementation

emptyList() returns an immutable list but not a specific type like ArrayList or LinkedList. If your API promises a concrete type, create it.

3) You require thread-local mutation

In some concurrency patterns, each thread expects its own mutable list. A shared immutable list won’t work.

4) You’re returning a view

Sometimes you return a view that should reflect changes in another collection. In those cases, emptyList() would be misleading. Use a view or wrapper instead.

Performance and Memory Notes

I don’t quote exact benchmarks in code reviews because they vary by JVM and workload, but here are the practical implications I rely on:

  • Allocation-free for empty returns. The method uses a shared instance, so there’s no new object to allocate for each call.
  • Better cache behavior. Fewer allocations mean less pressure on the young generation heap; in my experience, this trims GC pauses in high-volume endpoints.
  • Stable memory profile. If a method is called millions of times, returning a shared empty list avoids a long trail of short-lived objects.

If you’re working on a large system, even small changes like this can save milliseconds across many requests. I’ve seen reductions in GC time that are typically in the 10–15ms range per minute on busy JVMs—not a magic bullet, but a real win when combined with other improvements.

Comparing Options: Traditional vs Modern Patterns

Here’s how I frame this decision for teams in 2026. I avoid vague “pros and cons” and stick to recommendations.

Use Case

Traditional Approach

Modern Approach

My Recommendation

Return no results

return new ArrayList();

return Collections.emptyList();

Use emptyList() to signal immutability and avoid allocation.

API default for list field

null

Collections.emptyList()

Use emptyList() to avoid null checks and improve JSON output.

Build a list for later

Start with null, then init

new ArrayList()

Use new ArrayList() only if you’ll add elements.

Method expects caller mutation

Collections.emptyList()

new ArrayList()

Use a mutable list to avoid runtime errors.In teams I mentor, this table usually turns into a simple rule: “Immutable empty lists for returns, mutable lists for construction.”

Common Mistakes and How I Prevent Them

I’ve seen the same mistakes recur, even among senior devs. Here’s how I handle them in code review.

Mistake 1: Returning null

If a method returns a list, I expect a list—empty if necessary. Returning null shifts the burden to every caller. I push for emptyList() as a standard.

Mistake 2: Forgetting it’s immutable

When someone tries to add elements to the returned list, the exception surprises them. I prevent this by naming variables clearly and documenting expectations in method comments.

public List buildFeatureFlags() {

// Immutable empty list signals no flags

return Collections.emptyList();

}

Mistake 3: Assuming it’s a new instance

Some devs assume each call creates a new list. It doesn’t. If they attempt to compare with ==, they’ll get surprising results. I remind them to use .isEmpty() or .size() instead.

Mistake 4: Passing it into APIs that mutate lists

Some libraries expect a mutable list and will call .add(). If you pass emptyList() into them, you’ll get a runtime error. In those cases, I wrap it:

List safeList = new ArrayList(Collections.emptyList());

That gives you a mutable copy.

Collections.emptyList() vs List.of()

In modern Java, List.of() is another way to create immutable lists. Here’s how I think about it:

  • List.of() is great for small, fixed lists with known elements.
  • Collections.emptyList() is perfect when you explicitly want a shared empty list with no elements.

If you’re creating an empty list, List.of() works too, but it doesn’t communicate intent as clearly. I choose emptyList() for explicitness and to show that I mean “empty forever,” not “empty right now.”

How I Use It in Tests

Tests are a great place for emptyList(). It makes intent obvious and avoids clutter.

@Test

public void returnsEmptyWhenNoUsers() {

UserService service = new UserService(fakeRepoReturningEmpty());

List users = service.findActiveUsers();

assertTrue(users.isEmpty());

}

If the test expects “no results,” I prefer an immutable empty list to keep the setup simple and clear. If the test needs to build a dataset, I use a mutable list and populate it.

Patterns for Converting to Mutable Lists

Sometimes you start with emptyList() and later realize you need to add elements. The fix is simple: copy it into a mutable list.

List tags = Collections.emptyList();

List mutableTags = new ArrayList(tags);

mutableTags.add("backend");

This ensures the original empty list remains untouched. It’s also an explicit moment where you decide to mutate.

Thread Safety and Shared Instances

Because the list is immutable, it’s inherently thread-safe. That’s a big reason I like it for static constants and default fields:

public class FeatureFlags {

public static final List NONE = Collections.emptyList();

}

You can safely reference FeatureFlags.NONE across threads without synchronization. The list never changes.

API Design: Communicating Intent

If you’re designing APIs, emptyList() acts like a contract. It tells the caller:

  • This method always returns a list (never null).
  • If it’s empty, it will stay empty.
  • The caller should treat it as read-only.

When I document APIs, I explicitly mention this:

/

* Returns the active roles for the user.

* The returned list is immutable and may be empty.

*/

public List getActiveRoles(User user) {

// ...

return Collections.emptyList();

}

That single sentence prevents a lot of confusion.

A Practical Analogy I Use With Teams

I compare emptyList() to handing someone a sealed envelope labeled “no items inside.” It’s not just empty; it’s sealed. Anyone can read the label, but nobody can add anything without breaking the seal. That’s the core value: shared, consistent emptiness.

A Full Example in a Realistic Service

Here’s a more complete example that mirrors how I use it in production-style code. The pattern is consistent: return empty, not null, and keep it immutable.

import java.util.*;

public class InventoryService {

private final InventoryRepository repository;

public InventoryService(InventoryRepository repository) {

this.repository = repository;

}

public List findLowStockProducts(String warehouseId) {

if (warehouseId == null || warehouseId.isBlank()) {

// No warehouse means no products can be determined

return Collections.emptyList();

}

List results = repository.queryLowStock(warehouseId);

if (results == null || results.isEmpty()) {

return Collections.emptyList();

}

return results;

}

public static class Product {

private final String sku;

private final int stock;

public Product(String sku, int stock) {

this.sku = sku;

this.stock = stock;

}

public String getSku() { return sku; }

public int getStock() { return stock; }

}

public interface InventoryRepository {

List queryLowStock(String warehouseId);

}

}

This is the pattern I encourage: normalize upstream, return a safe empty list, and keep the rest of the code simple.

Edge Cases That Bite People

Most of the time emptyList() behaves exactly as expected, but there are a few subtle places it can surprise you. I include these in internal documentation so new engineers don’t learn them the hard way.

1) Identity vs equality

Because the empty list is a singleton, different calls often return the same instance. That means identity comparison might return true even though you shouldn’t rely on it:

List a = Collections.emptyList();

List b = Collections.emptyList();

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

System.out.println(a.equals(b)); // always true for empty lists

I advise teams to never use == for collections, even when they happen to be singletons. Use isEmpty() or equals() instead.

2) Methods that assume mutability

Some utility methods are poorly designed and call .add() on the list you pass them. If you pass emptyList() into those methods, you’ll get a runtime error. My rule is simple: if a method might mutate, pass a mutable list.

3) Subtyping and wildcard confusion

This one shows up in generic APIs:

List nums = Collections.emptyList();

// nums.add(1); // still fails because of both immutability and wildcard capture

Even if you convert to a mutable list, wildcard captures can still block add. The fix is usually to use a concrete type or copy into List.

4) Serialization with frameworks

Some serialization frameworks treat empty collections differently from null. That’s usually a benefit, but if your client expects null, you may need to be explicit. I mention this in API docs and make sure contract tests pin the expected output.

How It Behaves With Streams and Optionals

In modern Java, emptyList() often appears alongside streams, optionals, and mapping pipelines. I tend to make it the default in these flows.

Stream pipeline default

public List activeEmails(List users) {

if (users == null || users.isEmpty()) {

return Collections.emptyList();

}

return users.stream()

.filter(User::isActive)

.map(User::getEmail)

.toList();

}

If you need to support Java versions before toList(), I just collect into a mutable list and return it. But when the output is empty, I still default to emptyList().

Optional default

public List findTags(String projectId) {

return tagRepo.lookup(projectId)

.orElse(Collections.emptyList());

}

That line reads cleanly and keeps call sites safe. The key is that the optional is never forced to carry null.

Alternative Ways to Represent Empty Lists

It’s useful to know the other options so you can explain the tradeoffs. Here’s how I frame them.

Collections.emptyList()

  • Immutable singleton
  • Best for defaults and return values
  • Zero allocation

List.of()

  • Immutable and compact
  • Great for fixed lists with known elements
  • Equivalent to emptyList() when no elements are provided

Collections.unmodifiableList(new ArrayList())

  • Immutable wrapper around a mutable list
  • More overhead; useful when you need immutability around a pre-built list

new ArrayList()

  • Mutable
  • Best for construction and accumulation
  • Allocates a new list every time

I steer teams toward a simple rule: if the intent is “empty and read-only,” choose emptyList(). Otherwise use a constructor or an unmodifiable wrapper.

How I Explain It to Product Engineers

Not everyone on a team is deep in Java internals. So I explain it in terms of behavior and reliability:

  • It removes a whole class of null bugs.
  • It makes the API contract obvious: this list is not meant to be changed.
  • It saves small bits of memory repeatedly, which adds up at scale.

That usually gets buy-in without a debate over micro-optimizations.

Practical Scenarios: When It Shines

Here are some specific situations where I’ve seen emptyList() pay off.

1) Feature flags and configuration defaults

public List getEnabledFlags(String accountId) {

List flags = configStore.loadFlags(accountId);

return (flags == null) ? Collections.emptyList() : flags;

}

2) Search APIs with multiple filters

public List search(SearchQuery query) {

if (query == null || query.isEmpty()) {

return Collections.emptyList();

}

return index.search(query);

}

3) Analytics dashboards

If a metric is missing for a date range, I still return an empty list so charts render correctly and no exceptions bubble up in the client.

A Deeper Look at Immutability in Practice

Immutability isn’t just a theoretical benefit. In large codebases, it becomes a safety net:

  • Callers don’t need defensive copies. If I return an immutable empty list, the caller can’t corrupt my internal state.
  • Thread safety is free. No locks, no synchronized wrappers, no risk.
  • Reasoning becomes easier. When I see emptyList() I know it’s safe to pass around.

In code reviews, I treat immutability as a reliability feature, not just an optimization.

Handling Mutability Explicitly

When you do need to mutate, I like to make that transition explicit. This approach avoids accidental mutation while still giving you flexibility:

List orders = Collections.emptyList();

if (shouldLoadOrders(userId)) {

orders = new ArrayList();

orders.addAll(orderDao.fetch(userId));

}

The code reads clearly: we start immutable, then become mutable only when needed.

Empty List vs Empty Set vs Empty Map

I also teach teams the parallel methods:

  • Collections.emptySet() for sets
  • Collections.emptyMap() for maps

The same rules apply. If you return a collection type, return the immutable empty instance instead of null. This makes the entire API surface consistent.

Serialization and JSON Contracts

I often see issues here, especially when teams move from null to [].

  • [] means “no results,” which is usually what clients want.
  • null means “unknown or not provided,” which is often ambiguous.

I prefer emptyList() in DTO constructors to force consistent JSON output. In contract testing, I assert that empty lists serialize as []. That helps clients avoid extra null checks.

Interoperability With Arrays

Sometimes you want to convert between arrays and lists. Here’s the simple approach I recommend:

String[] tagsArray = new String[0];

List tags = Arrays.asList(tagsArray);

// If you want to guarantee immutability and singleton reuse

List tagsSafe = Collections.emptyList();

If the source is an empty array, Arrays.asList returns a fixed-size list but not necessarily a shared singleton. I still prefer emptyList() when I control the output contract and don’t need a view of the array.

Defensive Copies: When They Matter

A common question is whether you should always copy lists before returning them. Here’s my rule of thumb:

  • If you’re returning a mutable internal list, create an unmodifiable copy or return emptyList() when empty.
  • If you’re already returning immutable lists, no copy is necessary.

That rule keeps internal state protected without over-allocating.

Guidance for Library and Framework Integration

Some frameworks treat collections in a special way. Here’s how I handle that in practice.

JPA / ORM fields

For entity collections, many ORMs expect mutable lists so they can track changes. In those cases, I avoid returning emptyList() directly for entity fields. Instead, I initialize with a mutable list but keep API outputs immutable.

Spring and data binding

Data binders can attempt to mutate lists during deserialization. If you expose emptyList() as a writable field, you may see binding exceptions. My fix: keep emptyList() in read-only DTOs, and use mutable lists for binding targets.

Validation frameworks

Some validation tools iterate and mutate for normalization. If you see errors, it’s a hint that the framework expects a mutable list. Convert to a new ArrayList(list) before passing it in.

Kotlin and JVM Interop

If your team uses Kotlin alongside Java, emptyList() still plays well. Kotlin’s emptyList() returns a read-only list too, and Java interop is smooth. I just make sure Kotlin code doesn’t assume it can mutate the list returned from Java.

Migration Strategy: From Null to Empty Lists

If you’re refactoring legacy code, this is the process I follow:

1) Update method contracts and documentation to guarantee non-null lists.

2) Normalize at the source: repository or service layer.

3) Update call sites to remove null checks, replacing them with isEmpty().

4) Add tests to assert empty lists instead of null.

This makes the migration safe and avoids partial adoption that can confuse callers.

A Slightly More Complex Example With DTOs and Filters

Here’s a more realistic example that includes DTOs, filtering, and explicit empty handling:

public class ReportService {

public ReportSummary buildSummary(ReportQuery query) {

if (query == null || query.isEmpty()) {

return new ReportSummary(Collections.emptyList());

}

List rows = fetchRows(query);

if (rows == null || rows.isEmpty()) {

return new ReportSummary(Collections.emptyList());

}

// Example filter

List filtered = rows.stream()

.filter(ReportRow::isActive)

.toList();

return new ReportSummary(filtered.isEmpty() ? Collections.emptyList() : filtered);

}

private List fetchRows(ReportQuery query) {

// External call omitted

return Collections.emptyList();

}

}

class ReportSummary {

private final List rows;

public ReportSummary(List rows) {

this.rows = (rows == null) ? Collections.emptyList() : rows;

}

public List getRows() {

return rows;

}

}

This style keeps results predictable for API consumers and ensures you never return null.

Practical Performance Guidance (Ranges, Not Exact Numbers)

I avoid exact benchmarks, but I do talk about ranges:

  • If a method is called tens of thousands of times per minute, removing allocations typically saves a few MB of churn over the same period.
  • On busy services, this can shave single-digit to low double-digit milliseconds off minute-level GC metrics.
  • The biggest benefit is not speed but predictability: fewer random pauses and less variance in latency.

For most teams, this is the difference between a stable 99th percentile and a noisy one.

Additional Mistakes I See in Code Review

Here are a few more subtle ones that pop up:

Mistake 5: Treating it as a placeholder for later mutation

Sometimes engineers do this:

List list = Collections.emptyList();

// Later...

list.add("oops");

I catch it quickly because it will throw at runtime. The fix is to use a mutable list when you intend to add items later.

Mistake 6: Returning emptyList() but documenting it as mutable

If documentation says “caller may append,” then returning emptyList() is a contract violation. I align docs with the actual behavior.

Mistake 7: Accidentally mutating a list that was passed in

Even when you don’t return emptyList(), I advise treating input lists as immutable unless explicitly documented otherwise. That prevents accidental side effects.

Using emptyList() in Builders and Fluent APIs

Builders often want to initialize to an empty collection. I recommend emptyList() as a safe default, then copy into mutable state only when needed.

public class EmailBuilder {

private List cc = Collections.emptyList();

public EmailBuilder withCc(List cc) {

this.cc = (cc == null) ? Collections.emptyList() : cc;

return this;

}

}

That keeps the builder safe and reduces null checks later.

Practical Checklist I Use in Code Reviews

If I see a method returning a list, I ask these questions:

1) Does it ever return null? If so, can we return emptyList() instead?

2) Should the caller be able to mutate the result? If not, emptyList() or an unmodifiable list is a good default.

3) If the result is empty, are we allocating a new list unnecessarily?

4) Is the method contract clear in docs and tests?

If I can answer those clearly, the design is usually solid.

A Short FAQ I Use With Teams

Q: Is Collections.emptyList() always the same instance?

A: In practice it is a shared singleton, but I tell engineers not to rely on identity equality. Treat it as immutable and use standard list methods.

Q: Can I cast it to a specific list type?

A: Avoid it. The type is just List. If you need a specific implementation, create it explicitly.

Q: Should I use it in public constants?

A: Yes, for read-only defaults. It’s thread-safe and communicates intent well.

Q: Is it faster than new ArrayList()?

A: It avoids allocation, which is usually beneficial. The real win is clarity and safety, not micro-speed.

Putting It All Together

When I explain Collections.emptyList() to a team, I summarize the philosophy in one sentence: return an immutable empty list when you mean “empty forever.” That removes null checks, avoids accidental mutation, and clarifies intent.

It’s a small method with an outsized effect on code quality. If you normalize empty responses in your service layer, your callers get simpler code, your APIs become more predictable, and your system does less work under load. That’s a win I’ll take every time.

If you remember just a few rules, make them these:

  • Default to emptyList() for return values when there are no results.
  • Use mutable lists only when you intend to add elements.
  • Document immutability in your public APIs.
  • Never return null for list results unless there’s a strong reason.

That’s the practical playbook I use in production systems, and it scales—from tiny utilities to high-traffic services—without getting in your way.

Scroll to Top