본문 바로가기
Java·Servlet·JSP

[JAVA] List 객체 복사 방법과 Collections.copy()에 관한 고찰

by Leica 2020. 1. 9.
반응형

Card 클래스 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Card {
 
    private int front;  // 카드 앞면 문구(숫자)
    private String back;    // 카드 뒷면 문구(문자열)
    private boolean isFront;    // 카드 앞/뒷면 여부
 
    public Card(int front, String back) {
        this.front = front;
        this.back = back;
        this.isFront = true;
    }
 
    // 카드의 앞/뒷면 여부에 따른 현재 문구 반환
    public String getCard() {
        if(isFront) {
            return front + "";
        } else {
            return back;
        }
    }
 
    // 카드 뒤집기
    public void flip() {
        this.isFront = !isFront;
    }
 
    // getter - front
    public int getFront() {
        return front;
    }
 
    // getter - back
    public String getBack() {
        return back;
    }
 
    // getter - isFront
    public boolean isFront() {
        return isFront;
    }
}
cs

위와 같이 앞면, 뒷면, 앞/뒷면 여부를 필드(인스턴스 변수)로 갖는 Card 클래스를 정의하였다. Card의 front(앞면 문구)에는 숫자를, back(뒷면 문구)에는 문자열을 저장한다. isFront(앞/뒷면 여부)가 true이면 앞면, false이면 뒷면인 상태이다.

 

복사 대상(Source) List 객체 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.ArrayList;
import java.util.List;
 
public class Main {
 
    public static void main(String[] args) {
 
        List<Card> cardList = new ArrayList<>();
        Card card1 = new Card(1"A");
        Card card2 = new Card(2"B");
        Card card3 = new Card(3"C");
        Card card4 = new Card(4"D");
        Card card5 = new Card(5"E");
        cardList.add(card1);
        cardList.add(card2);
        cardList.add(card3);
        cardList.add(card4);
        cardList.add(card5);
    }
}
cs

위에서 정의한 Card 클래스를 담는 ArrayList 객체 cardList를 생성하고, 5장의 카드 인스턴스를 생성하여 cardList에 add하였다. 각각의 카드는 앞면에는 1부터 5까지의 정수를, 뒷면에는 A부터 E까지의 문자를 저장하며 모두 앞면인 상태(isFront = true)로 생성된다.

 

List 객체 복사 방법1: ArrayList 생성자 사용

3라인의 List<Card> copiedCardList = new ArrayList<>(cardList);와 같이 ArrayList의 생성자를 호출할 때 복사할 source를 생성자의 인자로 전달하여 복사할 수 있다. ArrayList의 ArrayList​(Collection<? extends E> c) 생성자를 이용하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
    // ... 중략 ...
    List<Card> copiedCardList = new ArrayList<>(cardList);    // cardList 복사
 
    System.out.println("cardList: " + getCardListStr(cardList));    // cardList 출력
    System.out.println("copiedCardList: " + getCardListStr(cardList));    // copiedCardList 출력
}
 
// 카드 리스트를 String으로 변환하여 반환
public static String getCardListStr(List<Card> cardList) {
    String str = "";
    for(Card card : cardList) {
        str += card.getCard() + " ";
    }
    return str;
}
cs

copedCardList에 cardList를 복사하였다.

 

ArrayList
public ArrayList​(Collection<? extends E> c)
Constructs a list containing the elements of the specified collection, in the order they are returned by the collection's iterator.

Parameters:
c - the collection whose elements are to be placed into this list

Throws:
NullPointerException - if the specified collection is null

 

1
2
cardList: 1 2 3 4 5 
copiedCardList: 1 2 3 4 5
cs

실행 결과

 

 

1
2
3
4
5
6
7
8
public static void main(String[] args) {
 
    // ...중략...
    System.out.println("copiedCardList.get(2).flip();");
    copiedCardList.get(2).flip();    // copiedCardList의 세 번째 카드 뒤집기
    System.out.println("cardList: " + getCardListStr(cardList));
    System.out.println("copiedCardList: " + getCardListStr(copiedCardList));
}
cs

copiedCardList의 세 번째 카드를 뒤집은 뒤 두 개의 리스트를 각각 출력해보았다.

 

1
2
3
4
5
6
cardList: 1 2 3 4 5 
copiedCardList: 1 2 3 4 5 
 
copiedCardList.get(2).flip();
cardList: 1 2 C 4 5 
copiedCardList: 1 2 C 4 5 
cs

실행 결과

리스트의 세 번째 카드가 flip() 메소드의 호출로 인해 뒤집혀 C가 출력되었다. 그런데 이 때 원본 리스트인 cardList의 세 번째 카드도 뒤집힌 것을 알 수 있다.

 

Shallow Copy - cardList와 copiedCardList의 각 인덱스는 같은 카드를 참조한다.

ArrayList의 생성자를 이용한 복사는 'Shallow Copy = 얕은 복사'로, 참조만 복사하기 때문이다. new ArrayList(cardList)의 new 연산자를 통해 새로운 ArrayList 인스턴스를 생성하지만 Card 객체들은 객체 그 자체가 아닌 참조만 복사한다. 그래서 복사본인 copiedCardList의 카드 상태를 변경하면 원본인 cardList의 카드 상태도 변경되는 것이다. 즉 copiedCardList.get(n)과 cardList.get(n)은 같은 객체를 가리킨다.

 

List 객체 복사 방법2: Collections.copy() 사용

Collections 클래스에는 list를 복사하는 copy() 메소드가 존재한다.

 

copy
public static void copy​(List<? super T> dest, List<? extends T> src)
Copies all of the elements from one list into another. After the operation, the index of each copied element in the destination list will be identical to its index in the source list. The destination list's size must be greater than or equal to the source list's size. If it is greater, the remaining elements in the destination list are unaffected.
This method runs in linear time.

Type Parameters:
T - the class of the objects in the lists

Parameters:
dest - The destination list.
src - The source list.

Throws:
IndexOutOfBoundsException - if the destination list is too small to contain the entire source List.
UnsupportedOperationException - if the destination list's list-iterator does not support the set operation.

Collections 클래스의 copy() 메소드는 한 리스트의 모든 요소들을 다른 리스트에 복사한다. Destination 리스트의 size는 source 리스트의 사이즈보다 크거나 같아야 하며, destination 리스트의 size가 더 큰 경우, 나머지 요소들은 그대로 남는다.

 

1
2
3
4
5
6
public static void main(String[] args) {
 
    // ...중략...
    List<Card> copiedCardList2 = new ArrayList<>(cardList.size());
    Collections.copy(copiedCardList2, cardList);    
}
cs

그래서 위와 같이 ArrayList 생성자에 source 리스트의 size를 넘겨 destination 리스트를 생성하고 Collections.copy()를 호출하면 된다고 생각하기 쉽다.

 

1
2
3
Exception in thread "main" java.lang.IndexOutOfBoundsException: Source does not fit in dest
    at java.base/java.util.Collections.copy(Collections.java:559)
    at com.atozdevelop.Main.main(Main.java:41)
cs

실행 결과

 

그러나 위와 같이 IndexOutOfBoundsException이 발생한다. 이는 destination 리스트의 size가 source 리스트보다 작기 때문에 발생한다. 실제로 copiedCardList2.size()를 콘솔에 출력해보면 0이 출력된다.

 

ArrayList(int initialCapacity) 생성자는 Capacity를 지정한다.

ArrayList
public ArrayList​(int initialCapacity)
Constructs an empty list with the specified initial capacity.

Parameters:
initialCapacity - the initial capacity of the list

Throws:
IllegalArgumentException - if the specified initial capacity is negative

ArrayList(int initialCapacity) 생성자는 지정된 초기 capacity를 갖는 빈 리스트를 생성한다.

 

ArrayList에서 capacity와 size는 다른 개념이다. 위 생성자에서 initialCapacity 매개변수는 메모리를 할당 할 초기 용량을 지정한다. 쉽게 말해 capacity는 item을 담을 수 있는 잠재적인 크기를 의미하며 Size는 실제로 item을 담은 갯수를 의미한다. 따라서 정수값 n을 생성자의 인자로 넘겨 ArrayList를 생성해도 capacity가 n인 것이지, 실제 size는 0 이므로 Collections.copy()를 호출하면 IndexOutOfBoundsException이 발생하는 것이다. 매개변수가 없는 ArrayList의 기본 생성자도 초기 capacity 10을 갖는다.

 

Collections.copy() 메소드의 동작

Source list의 size가 10이면 index 0부터 9까지의 item을 destination list의 index 0부터 9로 동일하게 복사한다. 이 때 Collections.copy() 메소드는 capacity를 자동으로 늘려주지 않기 때문에 destination list가 source list보다 size가 같거나 커야한다.

 

따라서 Collections.copy() 메소드는 빈 list에 복사하는 용도로 쓰기에 적절하지 않다.

 

Collections.copy()의 용도

예를 들어 srcList의 item들을 dstList로 복사하려 하고, dstList의 size가 srcList보다 같거나 크다고 가정했을 때 Collections.copy() 메소드가 없다면 아래와 같이 코드를 작성해야 할 것이다. Collections.copy()는 개발자가 아래와 같은 반복문을 작성하지 않고도 list를 복사할 수 있도록 편리하게 제공해주는 메소드일 뿐이다.

1
2
3
for(int i=0; i<srcList.size(); i++) {
    dstList.set(i, srcList.get(i));
}
cs

 

 

Collections.copy()는 Deep Copy 인가?

결론부터 말하면 Collections.copy()도 shallow copy이다. shallow copy의 반대 개념이 deep copy, 깊은 복사이다. 참조를 복사하는 shallow copy와 달리 deep copy는 list의 객체 자체를 복사하는 것이다. 원본 list의 item 객체와 사본 list의 item 객체는 완전히 독립된 객체가 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
    // ...중략...
    System.out.println("cardList: " + getCardListStr(cardList));    // 원본 list 출력 : 1 2 C 4 5
    List<Card> copiedCardList2 = new ArrayList<>(cardList.size());    // Collections.copy()로 복사할 새로운 list 생성
    copiedCardList2.add(new Card(6"F"));
    copiedCardList2.add(new Card(7"G"));
    copiedCardList2.add(new Card(8"H"));
    copiedCardList2.add(new Card(9"I"));
    copiedCardList2.add(new Card(10"J"));
    copiedCardList2.add(new Card(11"K"));
    System.out.println("copiedCardList2: " + getCardListStr(copiedCardList2));    // 복사 전 사본 list 출력 : 6 7 8 9 10 11
    Collections.copy(copiedCardList2, cardList);
    System.out.println("copiedCardList2: " + getCardListStr(copiedCardList2));    // 복사 후 사본 list 출력 : 1 2 C 4 5 11
    copiedCardList2.get(4).flip();    // 카드 뒤집기
    System.out.println("cardList: " + getCardListStr(cardList));    // 카드 뒤집기 후 원본 list 출력 : 1 2 C 4 E
    System.out.println("copiedCardList2: " + getCardListStr(copiedCardList2));    // 카드 뒤집기 후 사본 list 출력 : 1 2 C 4 E 11
}
cs

Collections.copy() 메소드로 copiedCardList2에 cardList를 복사한 후 copiedCardList2에서 flip() 메소드를 호출하자, cardList의 card 객체도 변경되었다.

 

1
2
3
4
5
cardList: 1 2 C 4 5 
copiedCardList2: 6 7 8 9 10 11 
copiedCardList2: 1 2 C 4 5 11 
cardList: 1 2 C 4 E 
copiedCardList2: 1 2 C 4 E 11 
cs

실행 결과

 

List 객체 복사 방법3: addAll() 사용

addAll() 메소드는 전달된 컬렉션의 모든 element를 호출한 컬렉션의 끝에 추가한다.

 

addAll
boolean addAll​(Collection<? extends E> c)
Appends all of the elements in the specified collection to the end of this list, in the order that they are returned by the specified collection's iterator (optional operation). The behavior of this operation is undefined if the specified collection is modified while the operation is in progress. (Note that this will occur if the specified collection is this list, and it's nonempty.)

Specified by:
addAll in interface Collection

Parameters:
c - collection containing elements to be added to this list

Returns:
true if this list changed as a result of the call

Throws:
UnsupportedOperationException - if the addAll operation is not supported by this list
ClassCastException - if the class of an element of the specified collection prevents it from being added to this list
NullPointerException - if the specified collection contains one or more null elements and this list does not permit null elements, or if the specified collection is null
IllegalArgumentException - if some property of an element of the specified collection prevents it from being added to this list

 

1
2
3
4
5
6
7
8
public static void main(String[] args) {
    // ...중략...    
    List<Card> copiedCardList3 = new ArrayList<>();
    copiedCardList3.addAll(cardList);
    System.out.println("copiedCardList3: " + getCardListStr(copiedCardList3));    // 1 2 C 4 E
    copiedCardList3.get(1).flip();
    System.out.println("cardList: " + getCardListStr(cardList));    // 1 B C 4 E
    System.out.println("copiedCardList3: " + getCardListStr(copiedCardList3));}    // 1 B C 4 E
cs

 

1
2
3
copiedCardList3: 1 2 C 4 E 
cardList: 1 B C 4 E 
copiedCardList3: 1 B C 4 E 
cs

실행 결과

copiedCardList3.get(1).flip()을 호출했으나 cardList도 동일하게 변경되었다. 즉 addAll() 메소드를 통한 복사도 shallow copy이다.

 

Collection 객체의 Deep Copy

Java API는 list를 포함한 collection 객체의 shallow copy만을 제공하며 deep copy는 필요시 개발자가 구현해야 한다.

반응형

댓글