컬렉션 인터페이스 살펴보기

가장 먼저 알아야 할 인터페이스는 Collection 인터페이스입니다. 이 인터페이스는 요소를 저장하고 다양한 방법으로 검색할 수 있는 일반 컬렉션을 모델링합니다.

이 파트의 예제를 실행하려면 컬렉션을 만드는 방법을 알아야 합니다. 아직 ArrayList 클래스를 다루지 않았으므로 나중에 다루도록 하겠습니다.

 

개별 요소를 처리하는 메서드

컬렉션에서 요소를 저장하고 제거하는 것부터 시작해 보겠습니다. 관련된 두 가지 메서드는 add()remove()입니다.

  • add(element): 컬렉션에 요소를 추가합니다. 이 메서드는 연산이 실패할 경우 boolean을 반환합니다. 소개에서 List의 경우 실패하지 않아야 하지만, Set의 경우 집합은 중복을 허용하지 않기 때문에 실패할 수 있음을 보았습니다.
  • remove(element): 컬렉션에서 지정된 요소를 제거합니다. 이 메서드는 작업이 실패할 수 있으므로 boolean도 반환합니다. 예를 들어 제거를 요청한 항목이 컬렉션에 없는 경우 제거가 실패할 수 있습니다.

다음 예제를 실행할 수 있습니다. 여기서는 ArrayList 구현을 사용하여 Collection 인터페이스의 인스턴스를 생성합니다. 사용된 제네릭은 Java 컴파일러에 이 컬렉션에 String 객체를 저장할 것임을 알려줍니다. ArrayList만이 Collection의 유일한 구현은 아닙니다. 이에 대해서는 나중에 자세히 설명합니다.

Collection<String> strings = new ArrayList<>();
strings.add("one");
strings.add("two");
System.out.println("strings = " + strings);
strings.remove("one");
System.out.println("strings = " + strings);

이전 코드를 실행하면 다음과 같은 내용이 출력됩니다:

strings = [one, two]
strings = [two]

컬렉션에 요소가 있는지 여부는 contains() 메서드를 사용하여 확인할 수 있습니다. 모든 타입의 엘리먼트가 있는지 확인할 수 있다는 점에 유의하세요. 예를 들어, String 컬렉션에 User 객체가 있는지 확인하는 것은 유효합니다. 이 검사가 true를 반환할 가능성이 없기 때문에 이상하게 보일 수 있지만 컴파일러에서는 허용됩니다. IDE를 사용하여 이 코드를 테스트하는 경우, String 객체 컬렉션에 User 객체가 있는지 테스트할 때 IDE가 경고할 수 있습니다.

Collection<String> strings = new ArrayList<>();
strings.add("one");
strings.add("two");
if (strings.contains("one")) {
    System.out.println("one is here");
}
if (!strings.contains("three")) {
    System.out.println("three is not here");
}
 
User rebecca = new User("Rebecca");
if (!strings.contains(rebecca)) {
    System.out.println("Rebecca is not here");
}

이 코드를 실행하면 다음과 같은 결과가 생성됩니다:

one is here
three is not here
Rebecca is not here

 

다른 컬렉션을 처리하는 메서드

앞서 살펴본 첫 번째 메서드 세트를 사용하면 개별 요소를 처리할 수 있습니다.

이러한 메서드에는 containsAll(), addAll(), removeAll()retainAll()의 네 가지가 있습니다. 이 함수는 객체 집합에 대한 네 가지 기본 연산을 정의합니다.

첫 번째는 정말 간단합니다: containsAll()은 다른 컬렉션을 인수로 받아 다른 컬렉션의 모든 요소가 이 컬렉션에 포함되어 있으면 true를 반환합니다. 인자로 전달된 컬렉션은 이 컬렉션과 동일한 타입일 필요는 없습니다. 타입 Collection<String>String 컬렉션이 타입 Collection<User> 컬렉션에 포함되어 있는지 묻는 것은 합법적입니다.

다음은 이 방법의 사용 예시입니다:

Collection<String> strings = new ArrayList<>();
strings.add("one");
strings.add("two");
strings.add("three");
 
Collection<String> first = new ArrayList<>();
first.add("one");
first.add("two");
 
Collection<String> second = new ArrayList<>();
second.add("one");
second.add("four");
 
System.out.println("Is first contained in strings? " + strings.containsAll(first));
System.out.println("Is second contained in strings? " + strings.containsAll(second));

이 코드를 실행하면 다음과 같은 결과가 생성됩니다:

Is first contained in strings? true
Is second contained in strings? false

두 번째는 addAll()입니다. 주어진 컬렉션의 모든 요소를 이 컬렉션에 추가할 수 있습니다. add() 메서드와 마찬가지로 경우에 따라 일부 요소에 대해 이 메서드가 실패할 수 있습니다. 이 메서드는 이 호출에 의해 이 컬렉션이 수정된 경우 true를 반환합니다. 여기서 중요한 점은 true 값을 반환한다고 해서 다른 컬렉션의 모든 요소가 추가되었다는 의미는 아니며, 적어도 하나의 요소가 추가되었다는 의미입니다.

다음 예제에서 addAll()가 작동하는 모습을 확인할 수 있습니다:

Collection<String> strings = new ArrayList<>();
strings.add("one");
strings.add("two");
strings.add("three");
 
Collection<String> first = new ArrayList<>();
first.add("one");
first.add("four");
 
boolean hasChanged = strings.addAll(first);
 
System.out.println("Has strings changed? " + hasChanged);
System.out.println("strings = " + strings);

이 코드를 실행하면 다음과 같은 결과가 생성됩니다:

Has strings changed? true
strings = [one, two, three, one, four]

이 코드를 실행할 때 Collection의 구현을 변경하면 다른 결과가 생성된다는 점에 유의해야 합니다. 아래에서 볼 수 있듯이 이 결과는 ArrayList에 대한 것이며, HashSet에 대해서는 동일하지 않을 것입니다.

세 번째는 removeAll()입니다. 이 함수는 다른 컬렉션에 포함된 이 컬렉션의 모든 요소를 제거합니다. contains() 또는 remove()의 경우와 마찬가지로, 다른 컬렉션은 이 컬렉션과 호환될 필요 없이 모든 타입에 정의할 수 있습니다.

다음 예제에서 removeAll()이 작동하는 모습을 확인할 수 있습니다:

Collection<String> strings = new ArrayList<>();
strings.add("one");
strings.add("two");
strings.add("three");
 
Collection<String> toBeRemoved = new ArrayList<>();
toBeRemoved.add("one");
toBeRemoved.add("four");
 
boolean hasChanged = strings.removeAll(toBeRemoved);
 
System.out.println("Has strings changed? " + hasChanged);
System.out.println("strings = " + strings);

이 코드를 실행하면 다음과 같은 결과가 생성됩니다:

Has strings changed? true
strings = [two, three]

마지막은 retainAll()입니다. 이 연산은 이 컬렉션에서 다른 컬렉션에 포함된 요소만 유지하고 나머지는 모두 제거합니다. 다시 한 번, contains() 또는 remove()의 경우와 마찬가지로 다른 컬렉션은 모든 타입에 정의할 수 있습니다.

다음 예제에서 retainAll()이 작동하는 모습을 확인할 수 있습니다:

Collection<String> strings = new ArrayList<>();
strings.add("one");
strings.add("two");
strings.add("three");
 
Collection<String> toBeRetained = new ArrayList<>();
toBeRetained.add("one");
toBeRetained.add("four");
 
boolean hasChanged = strings.retainAll(toBeRetained);
 
System.out.println("Has strings changed? " + hasChanged);
System.out.println("strings = " + strings);

이 코드를 실행하면 다음과 같은 결과가 생성됩니다:

Has strings changed? true
strings = [one]

 

컬렉션 자체를 처리하는 메서드

다음 메서드는 컬렉션 자체를 처리합니다.

컬렉션의 콘텐츠를 확인하는 방법에는 두 가지가 있습니다.

  • size(): 컬렉션의 요소 수를 int로 반환합니다.
  • isEmpty(): 주어진 컬렉션이 비어 있는지 여부를 알려줍니다.
Collection<String> strings = new ArrayList<>();
strings.add("one");
strings.add("two");
if (!strings.isEmpty()) {
    System.out.println("Indeed strings is not empty!");
}
System.out.println("The number of elements in strings is " + strings.size());

이 코드를 실행하면 다음과 같은 결과가 생성됩니다:

Indeed strings is not empty!
The number of elements in strings is 2

그런 다음 컬렉션에서 clear()를 호출하기만 하면 컬렉션의 콘텐츠를 삭제할 수 있습니다.

Collection<String> strings = new ArrayList<>();
strings.add("one");
strings.add("two");
System.out.println("The number of elements in strings is " + strings.size());
strings.clear();
System.out.println("After clearing it, this number is now " + strings.size());

이 코드를 실행하면 다음과 같은 결과가 생성됩니다:

The number of elements in strings is 2
After clearing it, this number is now 0

 

컬렉션의 요소 배열 가져오기

요소를 배열에 넣는 것보다 컬렉션에 저장하는 것이 애플리케이션에서 더 합리적일 수 있지만, 배열로 가져오는 것이 필요한 경우도 있습니다.

Collection 인터페이스는 컬렉션의 요소를 배열로 가져오는 세 가지 패턴을 제공하며, 이는 toArray() 메서드의 세 가지 오버로드 형태로 제공됩니다.

첫 번째는 인수가 없는 일반 toArray() 호출입니다. 이 함수는 일반 객체 배열로 요소를 반환합니다.

이것이 필요한 것이 아닐 수도 있습니다. Collection<String>이 있는 경우 String 배열을 사용하는 것이 좋습니다. Object[]String[]으로 형변환할 수는 있지만 이 형변환이 런타임에 실패하지 않는다고 보장할 수는 없습니다. 타입 안전성이 필요한 경우 다음 메서드 중 하나를 호출할 수 있습니다.

마지막 두 패턴의 차이점은 무엇인가요? 첫 번째는 가독성입니다. IntFunction<T[]>의 인스턴스를 만드는 것이 처음에는 이상하게 보일 수 있지만, 메서드 참조를 사용하여 작성하는 것은 정말 쉬운 일입니다.

다음은 첫 번째 패턴입니다. 이 첫 번째 패턴에서는 해당 타입의 배열을 전달해야 합니다.

Collection<String> strings = ...; // suppose you have 15 elements in that collection
 
String[] tabString1 = strings.toArray(new String[] {}); // you can pass an empty array
String[] tabString2 = strings.toArray(new String[15]);   // or an array of the right size

인자로 전달된 이 배열의 용도는 무엇인가요? 컬렉션의 모든 요소를 담을 수 있을 만큼 충분히 크면 이러한 요소가 배열에 복사되어 반환됩니다. 배열에 필요한 것보다 많은 공간이 있으면 배열의 사용하지 않는 셀 중 첫 번째 셀이 null로 설정됩니다. 전달한 배열이 너무 작으면 컬렉션의 요소를 담을 수 있는 정확한 크기의 배열이 새로 만들어집니다.

이 패턴이 실제로 작동하는 모습은 다음과 같습니다:

Collection<String> strings = List.of("one", "two");
 
String[] largerTab = {"three", "three", "three", "I", "was", "there"};
System.out.println("largerTab = " + Arrays.toString(largerTab));
 
String[] result = strings.toArray(largerTab);
System.out.println("result = " + Arrays.toString(result));
 
System.out.println("Same arrays? " + (result == largerTab));

이전 코드를 실행하면 다음과 같은 결과가 표시됩니다:

largerTab = [three, three, three, I, was, there]
result = [one, two, null, I, was, there]
Same arrays? true

인자 배열의 첫 번째 셀에 배열이 복사되고 그 바로 뒤에 null이 추가되어 이 배열의 마지막 요소는 그대로 남아있는 것을 볼 수 있습니다. 반환된 배열은 인자로 제공한 배열과 동일한 배열이지만 내용은 다릅니다.

다음은 길이가 0인 배열을 사용한 두 번째 예시입니다:

Collection<String> strings = List.of("one", "two");
 
String[] zeroLengthTab = {};
String[] result = strings.toArray(zeroLengthTab);
 
System.out.println("zeroLengthTab = " + Arrays.toString(zeroLengthTab));
System.out.println("result = " + Arrays.toString(result));

이 코드를 실행하면 다음과 같은 결과가 나타납니다:

zeroLengthTab = []
result = [one, two]

이 경우 새 배열이 생성되었습니다.

두 번째 패턴은 생성자 메서드 참조를 사용하여 IntFunction<T[]>를 구현하기 위해 작성되었습니다:

Collection<String> strings = ...;
 
String[] tabString3 = strings.toArray(String[]::new);

이 경우 이 함수를 사용하여 올바른 타입의 0 길이 배열을 생성한 다음 이 배열을 인수로 전달하여 toArray()를 호출합니다.

이 코드 패턴은 toArray() 호출의 가독성을 개선하기 위해 JDK 8에 추가되었습니다.

 

Predicate를 사용하여 컬렉션의 요소 필터링하기

Java SE 8에는 Predicate를 사용하여 컬렉션의 요소를 필터링할 수 있는 새로운 기능인 Collection 인터페이스가 추가되었습니다.

List<String>가 있고 널 문자열, 빈 문자열, 5자보다 긴 문자열을 모두 제거해야 한다고 가정해 보겠습니다. Java SE 7 이하에서는 Iterator.remove() 메서드를 사용하여 if 문에서 호출하여 이 작업을 수행할 수 있습니다. 이 패턴은 Iterator 인터페이스와 함께 볼 수 있습니다. removeIf()를 사용하면 코드가 훨씬 더 간단해집니다:

Predicate<String> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNullOrEmpty = isNull.or(isEmpty);
 
Collection<String> strings = new ArrayList<>();
strings.add(null);
strings.add("");
strings.add("one");
strings.add("two");
strings.add("");
strings.add("three");
strings.add(null);
 
System.out.println("strings = " + strings);
strings.removeIf(isNullOrEmpty);
System.out.println("filtered strings = " + strings);

이 코드를 실행하면 다음과 같은 결과가 생성됩니다:

strings = [null, , one, two, , three, null]
filtered strings = [one, two, three]

다시 한 번 강조하지만, 이 방법을 사용하면 애플리케이션 코드의 가독성과 표현력이 크게 향상됩니다.

 

컬렉션 인터페이스의 구현 선택하기

이 모든 예제에서는 ArrayList를 사용하여 Collection 인터페이스를 구현했습니다.

사실 컬렉션 프레임워크는 Collection 인터페이스의 직접 구현을 제공하지 않습니다. ArrayListList를 구현하고, ListCollection을 확장하기 때문에 Collection도 구현합니다.

애플리케이션에서 컬렉션을 모델링하기 위해 Collection 인터페이스를 사용하기로 결정했다면, ArrayList를 기본 구현으로 선택하는 것이 대부분의 경우 최선의 선택입니다. 이 튜토리얼의 뒷부분에서 올바른 구현에 대해 자세히 설명합니다.