[JAVA] java.lang.UnsupportedOperationException 에러 해결법 (JAVA 16 toList())
💡 코드가 보이지 않으시다면 드래그 혹은 오른쪽 아래 🌜 아이콘을 눌러 테마 색을 변경해주세요.
안녕하세요!
키크니 개발자 입니다. 🦒
코드를 짜다가 낯선 에러를 발견합니다.
list와 list를 addAll()을 해준 것 밖에 없었는데 UnsupportedOperationException이 발생하니 당황했습니다.
UnsupportedOperationException의 뜻은 무엇일까?
검색해보니 지원되지 않는 작업을 요청했을 때 발생하는 에러였습니다.
문제발생 원인에 대해서 알고싶어 검색해보니
Arrays.asList() 관련해서 나온 글들을 많이 볼 수 있었습니다.
💡 Arrays.asList()를 addAll() 했을 때 UnsupportedOperationException이 발생하는 원인은?
보통 Arrays.asList(배열)는 String[], int[] 등의 배열을 List로 바꿀 때 자주 사용하는 메소드입니다.
Arrays.asList()를 메소드를 통해 변환한 List에 데이터를 추가할 경우 UnsupportedOperationException이 발생하였습니다,
원인
- 일반적으로 사용하는 ArrayList class
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8683452581122892189L;
...
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
...
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
...
}
- Arrays.asList() 로 만든 ArrayList class
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
@Override
public Object[] toArray() {
return a.clone();
}
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
if (a.length < size)
return Arrays.copyOf(this.a, size,
(Class<? extends T[]>) a.getClass());
System.arraycopy(this.a, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
@Override
public E get(int index) {
return a[index];
}
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
return oldValue;
}
@Override
public int indexOf(Object o) {
E[] a = this.a;
if (o == null) {
for (int i = 0; i < a.length; i++)
if (a[i] == null)
return i;
} else {
for (int i = 0; i < a.length; i++)
if (o.equals(a[i]))
return i;
}
return -1;
}
@Override
public boolean contains(Object o) {
return indexOf(o) != -1;
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(a, Spliterator.ORDERED);
}
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (E e : a) {
action.accept(e);
}
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
E[] a = this.a;
for (int i = 0; i < a.length; i++) {
a[i] = operator.apply(a[i]);
}
}
@Override
public void sort(Comparator<? super E> c) {
Arrays.sort(a, c);
}
}
- Arrays.asList()로 만든 ArrayList가 상속받은 AbstractList Class
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
protected AbstractList() {
}
...
// 해당 메소드가 실행되는 것이다.
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
boolean modified = false;
for (E e : c) {
add(index++, e); // (1)
modified = true;
}
return modified;
}
}
Arrays Class를 살펴보면 AbstractList를 상속받아 생성되었습니다.
하지만 Arrays Class 안에는 AbstractList Class에 있는 add()나 addAll() method를 override를 하지 않습니다.
그렇기 때문에 AbstractList Class에 있는 add()를 수행하면서 에러가 발생하게 됩니다.
(1) addAll()을 보면 add()를 호출하고 있고, add()를 호출하면 UnsupportedOperationException이 발생하게 되어있습니다.
해결방안
결론적으로는 Arrays.asList(배열)의 결과인 arrayList는 기존의 java.util.ArrayList과 다르기 때문에 add() 를 사용할 수 없습니다.
1. java.util.ArrayList로 리스트를 선언합니다.
2. 해당 Listdp addAll()를 사용하며 Arrays.asList로 만든 리스트를 저장합니다.
3. add()를 사용합니다.
String[] strArr = {"가", "나", "다"};
List<String> strList = new ArrayList<String>();
strList.addAll(Arrays.asList(strArr));
strList.add("테스트");
하지만 제가 짠 코드와는 관련이 없는 것 같아 검색하는데 시간이 오래 걸렸습니다.
주로 JAVA 8만 사용했기 때문에 JAVA 16 문법때문에 에러가 발생했을 것이라고 상상하지 못했습니다.
(인텔리제이에서 추천한다고 막 쓰다가 딱 걸렸습니다.. 😂)
보통 JAVA 8에서 사용하던 Stream 의 .collect(Collectors.toList())를 자주 사용했었는데
이번에 JAVA 17을 사용하니 toList()(Stream.toList())를 추천하여 곧장 수정하여 사용했습니다.
해당 글 중 2.3 Stream.toList() 메서드 사용을 보니 아래와 같은 설명이 있었습니다.
Java 16 introduces a new method on Stream API called toList(). This handy method returns anunmodifiable List containing the stream elements:
@Test
public void whenUsingStreamToList_thenReturnImmutableList() {
List<String> immutableList = Stream.of("a", "b", "c", "d").toList();
Assertions.assertThrows(UnsupportedOperationException.class, () -> {
immutableList.add("e");
});
}
As we can see in the unit test, Stream.toList() returns an immutable list. So, trying to add a new element to the list will simply lead to UnsupportedOperationException.
Please bear in mind that the new Stream.toList() method is slightly different from the existing Collectors.toList() as it returns an unmodifiable list.
해석해보면,
Java 16은 toList() 라는 Stream API 에 대한 새로운 메서드를 도입합니다 . 이 편리한 메서드는 스트림 요소를 포함 하는 수정 불가능한 목록 을 반환합니다 .
@Test
public void whenUsingStreamToList_thenReturnImmutableList() {
List<String> immutableList = Stream.of("a", "b", "c", "d").toList();
Assertions.assertThrows(UnsupportedOperationException.class, () -> {
immutableList.add("e");
});
}
단위 테스트에서 볼 수 있듯이 Stream.toList() 는 변경할 수 없는 목록을 반환합니다 . 따라서 목록에 새 요소를 추가하려고 하면 단순히 UnsupportedOperationException 이 발생 합니다.
새로운 Stream.toList() 메서드는 수정할 수 없는 목록을 반환하므로 기존 Collectors.toList() 와 약간 다릅니다 .
Stream.collect(Collectors.toList()) vs Stream.toList()
JAVA 8 : Stream.collect(Collectors.toList())는 리턴되는 List가 수정이 가능합니다.
JAVA 10 : 수정이 불가능한 (unmodifiable) List로 반환되도록 toUnmodifiableList()가 새롭게 등장하였습니다.
(하지만 딱봐도 사용하기 싫게 생겼죠..?)
JAVA 16 : 이를 보완하여 Stream.toList()가 등장하였습니다.
딱 수정이 불가능한 <- 여기서 놓친부분이 있었습니다.
Error Code
List<Object> newHistories = requestHistories.stream()
.filter(history -> !savedTransUniqueCodes.contains(history.getTransUniqueCode()))
.toList(); // (1)
List<Object> newHistories2 = new ArrayList<>(); // (2)
newHistories.addAll(newHistories2); // (3)
(1) 아무 생각없이 단순하게 list로 반환되는 것으로 이해하여 작성하였습니다. 하지만 여기서 문제가 시작됩니다.
(2) 원래 코드는 new ArrayList<>()로 초기화를 한 후 값을 담아 return하는 method로 이루어져있지만 간단하게 작성합니다.
(3) 해당 코드에서 UnsupportedOperationException이 발생합니다. 변할 수 없는 list인데 addAll()을 사용하여 해당 list를 변경하려고 시도하였기 때문입니다.
해결방안
List<Object> newHistories = requestHistories.stream()
.filter(history -> !savedTransUniqueCodes.contains(history.getTransUniqueCode()))
.toList();
List<Object> newHistories2 = new ArrayList<>();
List<Object> histories = new ArrayList<>(); // (1)
histories.addAll(newHistories);
histories.addAll(newHistories2);
(1) 새로운 List를 생성하여 해당 List의 각각의 List를 addAll() 해줍니다.
추후 적용할 수 있으면 아래의 코드를 적용하여 깔끔하게 사용해봐야겠다고 생각했습니다. 😤
Stream<String> stream3 = Stream.concat(stream1, stream2);
⭐️ 참고한 곳
https://www.baeldung.com/java-stream-immutable-collection