본문 바로가기

프로그래밍/JAVA

Collection remove에 대해



Collection의 element를 remove 할 때,





1. Arrays.asList()


아래와 같이 Arrays.asList() 를 통해 생성한 리스트의 0번째 인덱스를 삭제해보자.

List<String> aaList = Arrays.asList("a", "1", "c", "d", "e");
aaList.remove(1);


두둥.... 지원하지 않는 연산이란다. 

Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.remove(AbstractList.java:161)
at JavaExamples01.marathon(JavaExamples01.java:22)
at ModernJava.main(ModernJava.java:12)




그럼 아래와 같이 java.util.ArrayList로 감싸보자. 

List<String> bbList = new ArrayList<>(Arrays.asList("a", "1", "c", "d", "e"));
bbList.remove(1);

성공한다. 



Arrays.asList를 찾아가보면 Arrays.java 에서 ArrayList가 구현되어 있다. 

즉, java.util.ArrayList와 java.util.arrays.ArrayList로 다르게 구현되어 있다. 

@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

/**
* @serial include
*/
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);
}



java.util.ArrayList의 remove를 살펴보면 아래와 같이 구현이 되어 있는 반면, 

java.util.arrays.ArrayList는 구현되어 있지 않아 상위 객체인 AbstractList에서 throw Exception처리를 한 것이다. 

/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices).
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}



Arrays.asList()의 주석을 보면 다음과 같이 기재되어 있다. 

고정된 사이즈의 리스트를 리턴한다. 

리스트에 add/remove 등 수정이 필요한 경우에는 Arrays.asList()을 사용해선 안됨을 명심하자. 

/**
* Returns a fixed-size list backed by the specified array.






2. 순회할 때 삭제하기



컬렉션 순회중에 remove를 할일이 생겼을 경우, 아래와 같은 코드가 쉽게 생각난다.  

List<String> bList = new ArrayList<>(Arrays.asList("a", "1", "c", "d", "e"));
for (String str : bList) {
if ("1".equals(str)) {
bList.remove(str);
}
}


허나 이 코드는  ConcurrentModificationException을 뱉는다. 

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at ModernJava.main(ModernJava.java:20)



그럼 인덱스를 이용해서 remove 해보면 어떨까?

음.. 삭제가 잘 됐네?(안될거라 기대했었는데..)

List<String> bList = new ArrayList<>(Arrays.asList("a", "1", "c", "d", "e"));
for (int i=0; i<bList.size(); i++) {
if ("1".equals(bList.get(i))) {
bList.remove(i);
}
}

for (String str : bList) {
System.out.println(str);
}



다시.. "1"을 지울거라 연속으로 둬봤다. 

List<String> bList = new ArrayList<>(Arrays.asList("a", "1", "1", "c", "d", "e"));
for (int i=0; i<bList.size(); i++) {
if ("1".equals(bList.get(i))) {
bList.remove(i);
}
}

for (String str : bList) {
System.out.println(str);
}


다 안지워졌다. 

a
1
c
d
e

이유는 다음과 같다. 

index=1일때 첫번째 "1"이 나옴.

첫번째 "1"을 지움.

리스트가 당겨짐. (index=1에 두번째 "1"이 옴)


index=2가 된다. 

두번째 "1"은 이미 index=1로 가버려서 건너 띄어지고 "c"로 갔다.




그럼 뭘로 삭제를 실행할 수 있을까?



Iterator가 해결책이 되어 준다. 

Iterator 인터페이스는 remove 메서드를 제공하는데, 다음과 같이 상속 및 인터페이스 구현체를 따라가다보면 아마도(?) AbstactList에서 이를 구현해둔 듯 하다. 

ArrayList<E> extends AbstractList<E>
AbstractList<E> extends AbstractCollection<E>
private class Itr implements Iterator<E> {

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}

AbstractCollection<E> implements Collection<E>
Collection<E> extends Iterable<E>
interface Iterable<T> {    
    /**
* Returns an iterator over elements of type {@code T}.
*
* @return an Iterator.
*/
Iterator<T> iterator();

public interface Iterator<E> {

/**

* Removes from the underlying collection the last element returned
* by this iterator (optional operation). This method can be called
* only once per call to {@link #next}. The behavior of an iterator
* is unspecified if the underlying collection is modified while the
* iteration is in progress in any way other than by calling this
* method.
*
* @implSpec
* The default implementation throws an instance of
* {@link UnsupportedOperationException} and performs no other action.
*
* @throws UnsupportedOperationException if the {@code remove}
* operation is not supported by this iterator
*
* @throws IllegalStateException if the {@code next} method has not
* yet been called, or the {@code remove} method has already
* been called after the last call to the {@code next}
* method
*/
default void remove() {
throw new UnsupportedOperationException("remove");
}

List<String> bList = new ArrayList<>(Arrays.asList("a", "1", "1", "c", "d", "e"));
for (Iterator<String> iter = bList.iterator(); iter.hasNext();) {
if ("1".equals(iter.next())) {
iter.remove();
}
}

for (String str : bList) {
System.out.println(str);
}


그래서 위와 같이 Iterator를 이용하면 순회중에도 element를 정확하게 remove할 수 있다.




JDK1.8 부터는 더~~~욱 심플하게 해결할 수 있다. 

Predicate 함수형 인터페이스에 filter(람다 표현식)만 넣어주면 한줄이면 끝이다. 

List<String> bList = new ArrayList<>(Arrays.asList("a", "1", "1", "c", "d", "e"));
bList.removeIf(e -> e.equals("1"));

for (String str : bList) {
System.out.println(str);
}



원본 훼손 없이 새로운 Collection을 생성하는 방법도 있다!

bList.stream()
.filter(e -> !"1".equals(e))
.collect(Collectors.toList())




참고한 블로그:

http://www.daleseo.com/how-to-remove-from-list-in-java/



'프로그래밍 > JAVA' 카테고리의 다른 글

자바8 Optional  (0) 2019.04.23
WebFlux & Non-blocking  (0) 2019.04.22
String, StringBuffer, StringBuilder  (0) 2019.03.19
Object의 메서드/equals/hashCode/clone  (0) 2019.03.19
JVM  (0) 2019.03.19