# How to Fix Java UnsupportedOperationException: API Design Patterns
Your application crashes when trying to modify a collection:
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.Immutable collections - Trying to modify
Collections.unmodifiableList(),List.of(),Set.of() - 2.Fixed-size lists - Modifying lists from
Arrays.asList() - 3.Abstract implementations - Using default methods that subclasses didn't override
- 4.Read-only views - Collections backed by non-modifiable sources
- 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.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.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.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 whereadd()andremove()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