# How to Fix Java UnsupportedOperationException: API Design Patterns

Your application crashes when trying to modify a collection:

bash
Exception in thread "main" java.lang.UnsupportedOperationException: null
    at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1062)
    at com.myapp.service.DataService.addItem(DataService.java:45)
    at com.myapp.controller.ItemController.handleAdd(ItemController.java:32)

UnsupportedOperationException signals that a method is not supported by the implementing class. This commonly occurs with immutable collections, skeleton implementations, and restricted interfaces.

Understanding the Error

UnsupportedOperationException is thrown when:

  1. 1.Immutable collections - Trying to modify Collections.unmodifiableList(), List.of(), Set.of()
  2. 2.Fixed-size lists - Modifying lists from Arrays.asList()
  3. 3.Abstract implementations - Using default methods that subclasses didn't override
  4. 4.Read-only views - Collections backed by non-modifiable sources
  5. 5.Intentional restrictions - API methods explicitly disabled

Diagnosing the Problem

Step 1: Identify the Collection Type

```java public void addItem(String item) { items.add(item); // Line 45 - throws UnsupportedOperationException }

// Find where items is initialized public DataService() { this.items = getListFromSomewhere(); }

// Trace back to the source private List<String> getListFromSomewhere() { return Collections.unmodifiableList(internalList); // Found the problem! } ```

Step 2: Check Collection Factory Methods

```java // These create immutable collections (Java 9+) List<String> list = List.of("a", "b", "c"); // Immutable! Set<String> set = Set.of("a", "b", "c"); // Immutable! Map<String, Integer> map = Map.of("a", 1); // Immutable!

// These also create immutable collections List<String> unmodifiable = Collections.unmodifiableList(original); Set<String> singleton = Collections.singleton("only");

// This creates a fixed-size list backed by array List<String> fixedSize = Arrays.asList("a", "b", "c"); // set() works, add/remove don't! ```

Step 3: Add Debugging

```java public void addItem(String item) { System.out.println("Collection class: " + items.getClass().getName()); System.out.println("Is modifiable: " + isModifiable(items)); items.add(item); }

private boolean isModifiable(Collection<?> collection) { try { // Create a test copy and try to modify Collection<Object> test = new ArrayList<>(collection); test.add(null); test.remove(null); return true; } catch (UnsupportedOperationException e) { return false; } } ```

Solutions

Solution 1: Create a Mutable Copy

```java // Problem: Received an immutable collection public void processItems(List<String> items) { // items.add("new item"); // UnsupportedOperationException!

// Solution: Create a mutable copy List<String> mutableItems = new ArrayList<>(items); mutableItems.add("new item"); }

// For fixed-size lists from Arrays.asList() public void processArray() { String[] array = {"a", "b", "c"}; List<String> fixedSize = Arrays.asList(array);

// Create a truly mutable copy List<String> mutable = new ArrayList<>(fixedSize); mutable.add("d"); // Works! }

// For Java 9+ immutable collections public void processImmutable() { List<String> immutable = List.of("a", "b", "c");

// Create mutable copy List<String> mutable = new ArrayList<>(immutable); mutable.add("d"); // Works! } ```

Solution 2: Use Mutable Factory Methods

```java // When you need a mutable collection, use mutable factories

// For lists List<String> mutableList = new ArrayList<>(); // Empty mutable list List<String> mutableList2 = new ArrayList<>(Arrays.asList("a", "b", "c"));

// For sets Set<String> mutableSet = new HashSet<>(); // Empty mutable set Set<String> mutableSet2 = new HashSet<>(Set.of("a", "b", "c"));

// For maps Map<String, Integer> mutableMap = new HashMap<>(); Map<String, Integer> mutableMap2 = new HashMap<>(Map.of("a", 1, "b", 2));

// Java 10+ convenience methods List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); var mutableCopy = List.copyOf(immutableList); // Still immutable!

// Actually mutable copy: var trulyMutable = new ArrayList<>(List.copyOf(immutableList)); ```

Solution 3: Proper API Design - Return Defensive Copies

```java public class DataService { private final List<String> internalItems = new ArrayList<>();

// Bad: Returns internal mutable list public List<String> getItems() { return internalItems; // Exposes internal state! }

// Good: Returns an unmodifiable view public List<String> getItems() { return Collections.unmodifiableList(internalItems); }

// Good: Returns a defensive copy public List<String> getItemsCopy() { return new ArrayList<>(internalItems); }

// Good: Provides a way to add items public void addItem(String item) { internalItems.add(item); }

// Good: Provides bulk operations public void addAll(Collection<String> items) { internalItems.addAll(items); }

// Good: Returns immutable copy for iteration public Iterable<String> getItemsIterable() { return List.copyOf(internalItems); } } ```

Solution 4: Design Abstract Classes Properly

```java // Abstract class with optional and required methods public abstract class Repository<T> { private final List<T> items = new ArrayList<>();

// Fully implemented - optional to override public void add(T item) { items.add(item); }

// Fully implemented - optional to override public void addAll(Collection<T> newItems) { items.addAll(newItems); }

// Abstract - must implement public abstract T findById(Long id);

// Default implementation that throws - override if needed public void delete(T item) { throw new UnsupportedOperationException("Delete not supported by " + getClass().getSimpleName()); }

// Default implementation that throws - override if needed public void update(T item) { throw new UnsupportedOperationException("Update not supported by " + getClass().getSimpleName()); }

// Protected getter for subclasses protected List<T> getItems() { return items; } }

// Implementation that supports all operations public class FullRepository<T> extends Repository<T> { @Override public T findById(Long id) { return getItems().stream() .filter(item -> getId(item).equals(id)) .findFirst() .orElse(null); }

@Override public void delete(T item) { getItems().remove(item); }

@Override public void update(T item) { int index = getItems().indexOf(item); if (index >= 0) { getItems().set(index, item); } } }

// Read-only implementation public class ReadOnlyRepository<T> extends Repository<T> { @Override public T findById(Long id) { return getItems().stream() .filter(item -> getId(item).equals(id)) .findFirst() .orElse(null); }

// delete() and update() throw UnsupportedOperationException by default } ```

Solution 5: Use Interface Segregation

```java // Separate interfaces for different capabilities public interface ReadableRepository<T> { T findById(Long id); List<T> findAll(); boolean exists(Long id); }

public interface WritableRepository<T> { void save(T item); void delete(Long id); }

public interface Repository<T> extends ReadableRepository<T>, WritableRepository<T> { // Full repository with read and write capabilities }

// Implementations public class InMemoryRepository<T> implements Repository<T> { private final Map<Long, T> storage = new HashMap<>();

@Override public T findById(Long id) { return storage.get(id); }

@Override public List<T> findAll() { return new ArrayList<>(storage.values()); }

@Override public boolean exists(Long id) { return storage.containsKey(id); }

@Override public void save(T item) { storage.put(getId(item), item); }

@Override public void delete(Long id) { storage.remove(id); } }

// Read-only view of a repository public class ReadOnlyRepositoryView<T> implements ReadableRepository<T> { private final Repository<T> delegate;

public ReadOnlyRepositoryView(Repository<T> delegate) { this.delegate = delegate; }

@Override public T findById(Long id) { return delegate.findById(id); }

@Override public List<T> findAll() { return Collections.unmodifiableList(delegate.findAll()); }

@Override public boolean exists(Long id) { return delegate.exists(id); } // No write methods - compile-time safety! } ```

Solution 6: Handle Fixed-Size Arrays.asList()

```java public void processArray(String[] array) { // Arrays.asList() returns a fixed-size list List<String> list = Arrays.asList(array);

// This works list.set(0, "modified"); // OK - replaces existing element

// This throws UnsupportedOperationException // list.add("new element"); // Error! // list.remove(0); // Error!

// Solution 1: Create a mutable copy List<String> mutable = new ArrayList<>(Arrays.asList(array)); mutable.add("new element"); // Works!

// Solution 2: Use Arrays.copyOf() if you need a new array String[] newArray = Arrays.copyOf(array, array.length + 1); newArray[array.length] = "new element";

// Solution 3: Use ArrayList constructor directly List<String> list2 = new ArrayList<>(); Collections.addAll(list2, array); list2.add("new element"); } ```

Common Scenarios

Stream Collectors to Immutable Collections

```java // Problem: Collecting to immutable list List<String> result = stream.collect(Collectors.toList()); // Java 8-15: Mutable, Java 16+: Mutable

// Java 10+ Immutable collectors List<String> immutable = stream.collect(Collectors.toUnmodifiableList());

// To modify, create mutable copy List<String> mutable = new ArrayList<>(stream.collect(Collectors.toList()));

// Or use Collectors.toCollection() for specific implementation List<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new)); Set<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new)); ```

Converting Between Array and List

```java // Array to List conversions String[] array = {"a", "b", "c"};

// Fixed-size, mutable elements, immutable size List<String> fixed = Arrays.asList(array);

// Fully immutable (Java 9+) List<String> immutable = List.of(array);

// Fully mutable List<String> mutable1 = new ArrayList<>(Arrays.asList(array)); List<String> mutable2 = new ArrayList<>(List.of(array));

// List to Array List<String> list = new ArrayList<>(List.of("a", "b", "c")); String[] array = list.toArray(new String[0]); // Always works String[] array2 = list.toArray(String[]::new); // Java 11+ ```

API Returning Immutable Collections

```java public class Configuration { private final Map<String, String> settings;

public Configuration(Map<String, String> settings) { // Defensive copy at construction this.settings = Map.copyOf(settings); }

// Good: Returns immutable view public Map<String, String> getSettings() { return settings; // Already immutable }

// Good: Returns immutable copy of values public List<String> getValues() { return List.copyOf(settings.values()); }

// If you need to allow modifications, return a copy public Map<String, String> getModifiableCopy() { return new HashMap<>(settings); } } ```

Verification Steps

  1. 1.Test collection mutability:

```java @Test void testCollectionMutability() { List<String> immutable = List.of("a", "b", "c"); assertThrows(UnsupportedOperationException.class, () -> immutable.add("d"));

List<String> mutable = new ArrayList<>(immutable); assertDoesNotThrow(() -> mutable.add("d")); assertEquals(4, mutable.size()); } ```

  1. 1.Test defensive copies:

```java @Test void testDefensiveCopy() { DataService service = new DataService(); service.addItem("original");

List<String> items = service.getItems(); // Unmodifiable assertThrows(UnsupportedOperationException.class, () -> items.add("external"));

// Internal state is protected assertEquals(1, service.getItems().size()); } ```

  1. 1.Test repository operations:

```java @Test void testReadOnlyRepository() { ReadOnlyRepository<String> repo = new ReadOnlyRepository<>();

assertDoesNotThrow(() -> repo.findById(1L)); assertThrows(UnsupportedOperationException.class, () -> repo.delete(1L)); } ```

Key Takeaways

  • Check collection type before attempting modifications
  • Use new ArrayList<>(collection) to create mutable copies
  • Arrays.asList() returns a fixed-size list where add() and remove() throw exceptions
  • Java 9+ List.of(), Set.of(), Map.of() create fully immutable collections
  • Return unmodifiable views or defensive copies from APIs to protect internal state
  • Use interface segregation to distinguish between readable and writable operations
  • When implementing abstract classes, document which methods throw UnsupportedOperationException