튜토리얼

Java에서 두 리스트 비교하는 방법 (공통·누락·동일 요소)

작성자 10분 소요

Java에서는 두 리스트를 비교할 일이 자주 있습니다. 데이터베이스 레코드를 동기화하거나, 테스트에서 API 응답을 확인하거나, 두 번의 내보내기 사이에 추가·삭제된 ID를 찾을 때입니다. 알고 싶은 것에 따라 써야 할 방법이 다릅니다.

  • 두 리스트가 같은가? – 순서와 중복을 고려하는지 여부에 따라
  • 두 리스트에 모두 있는 요소는? – 교집합
  • 한쪽에만 있는 요소는? – 차집합

이 가이드는 표준 라이브러리(Stream.toList()와 record는 Java 16 이상)로 각 경우를 설명하고, Apache Commons와 Guava를 이용한 대안도 소개합니다. 모든 코드 예제는 다음 import를 사용합니다.

import java.util.*;
import java.util.stream.*;

두 리스트를 한 번만 비교하면 된다면 무료 두 목록 비교 도구에 붙여넣으세요. 코드 없이 A에만, B에만, 양쪽 모두에 있는 항목을 확인할 수 있습니다.


1. 두 리스트가 같은지 확인하기

List.equals()는 두 리스트가 같은 요소를 같은 순서로 가지고 있을 때만 true를 반환합니다.

List<Integer> a = List.of(1, 2, 3);
List<Integer> b = List.of(1, 2, 3);
List<Integer> c = List.of(3, 2, 1);

System.out.println(a.equals(b)); // true
System.out.println(a.equals(c)); // false – same elements, different order

요소는 각자의 equals() 메서드로 비교되므로 문자열, 숫자, equals()를 올바르게 구현한 모든 클래스에 사용할 수 있습니다. 구현 클래스가 달라도 동작하므로, 같은 요소를 가진 ArrayList와 LinkedList는 같다고 판단됩니다.


2. 순서를 무시하고 리스트 비교하기

순서를 무시하는 방법은 중복이 의미가 있는지에 따라 달라집니다.

같은 요소가 같은 횟수만큼(중복이 중요) – 복사본을 정렬해 비교합니다.

List<Integer> x = Arrays.asList(3, 1, 2, 2);
List<Integer> y = Arrays.asList(2, 3, 2, 1);

List<Integer> sortedX = new ArrayList<>(x);
List<Integer> sortedY = new ArrayList<>(y);
Collections.sort(sortedX);
Collections.sort(sortedY);

System.out.println(sortedX.equals(sortedY)); // true

요소를 정렬할 수 없다면 빈도 맵을 비교합니다.

Map<Integer, Long> countX = x.stream()
        .collect(Collectors.groupingBy(i -> i, Collectors.counting()));
Map<Integer, Long> countY = y.stream()
        .collect(Collectors.groupingBy(i -> i, Collectors.counting()));

System.out.println(countX.equals(countY)); // true

고유 요소가 같음(중복 무시) – Set으로 비교합니다.

List<Integer> z = List.of(1, 2, 3);
System.out.println(new HashSet<>(x).equals(new HashSet<>(z))); // true

3. retainAll로 공통 요소(교집합) 찾기

retainAll()은 다른 컬렉션에 없는 요소를 모두 제거합니다. 호출한 리스트 자체를 바꾸므로 복사본에서 작업하세요.

List<String> listA = List.of("apple", "banana", "cherry", "date");
List<String> listB = List.of("banana", "date", "fig");

List<String> common = new ArrayList<>(listA);
common.retainAll(new HashSet<>(listB));

System.out.println(common); // [banana, date]

새 ArrayList로 복사하는 데는 또 다른 이유도 있습니다. List.of()나 Arrays.asList()로 만든 리스트는 삭제를 지원하지 않아서, retainAll()을 호출하면 UnsupportedOperationException이 발생합니다.


4. removeAll로 누락된 요소(차집합) 찾기

removeAll()은 반대로 다른 컬렉션에 있는 요소를 모두 제거해, 첫 번째 리스트에만 있는 요소를 남깁니다.

List<String> onlyInA = new ArrayList<>(listA);
onlyInA.removeAll(new HashSet<>(listB));
System.out.println(onlyInA); // [apple, cherry]

List<String> onlyInB = new ArrayList<>(listB);
onlyInB.removeAll(new HashSet<>(listA));
System.out.println(onlyInB); // [fig]

두 메서드 모두 시작한 리스트의 원래 순서를 유지하며, 남은 요소의 중복도 그대로 둡니다.


5. 스트림으로 같은 작업 하기

스트림은 원본 리스트를 바꾸지 않으며, 추가 조건으로 거를 때도 읽기 쉽습니다.

Set<String> setB = new HashSet<>(listB);

List<String> onlyInA = listA.stream()
        .filter(item -> !setB.contains(item))
        .toList();                       // Java 16+; use collect(Collectors.toList()) before that

List<String> inBoth = listA.stream()
        .filter(setB::contains)
        .toList();

System.out.println(onlyInA); // [apple, cherry]
System.out.println(inBoth);  // [banana, date]

두 리스트 중 한쪽에만 있는 요소(대칭 차집합)를 얻으려면 양방향의 차이를 합칩니다.

Set<String> setA = new HashSet<>(listA);

List<String> inOnlyOne = Stream.concat(
        listA.stream().filter(item -> !setB.contains(item)),
        listB.stream().filter(item -> !setA.contains(item)))
        .toList();

System.out.println(inOnlyOne); // [apple, cherry, fig]

대소문자를 구분하지 않고 비교하려면 Set을 만들기 전에 값을 정규화하고(예: listB.stream().map(String::toLowerCase).collect(Collectors.toSet())), 확인하는 항목도 소문자로 바꾸세요.


6. 객체 리스트 비교하기

컬렉션은 객체를 equals()로 비교하고, 해시 기반 컬렉션은 hashCode()도 사용합니다. Java record는 필드로부터 둘 다 자동 생성하므로 바로 사용할 수 있습니다.

record Customer(int id, String name) {}

List<Customer> oldCustomers = List.of(new Customer(1, "Ann"), new Customer(2, "Bob"));
List<Customer> newCustomers = List.of(new Customer(2, "Bob"), new Customer(3, "Cy"));

Set<Customer> newSet = new HashSet<>(newCustomers);
List<Customer> removed = oldCustomers.stream()
        .filter(c -> !newSet.contains(c))
        .toList();

System.out.println(removed); // [Customer[id=1, name=Ann]]

일반 클래스라면 equals()와 hashCode()를 둘 다 재정의하세요. equals()만 재정의하면 HashSet에서 일치하는 객체를 찾지 못합니다.

이름이 바뀌어도 다른 고객으로 취급되지 않도록, 모든 필드가 아니라 키로 객체를 비교하고 싶을 때가 많습니다.

Set<Integer> newIds = newCustomers.stream()
        .map(Customer::id)
        .collect(Collectors.toSet());

List<Customer> removedById = oldCustomers.stream()
        .filter(c -> !newIds.contains(c.id()))
        .toList();

System.out.println(removedById); // [Customer[id=1, name=Ann]]

7. 라이브러리: Apache Commons와 Guava

프로젝트에서 이미 이 라이브러리들을 쓰고 있다면 집합 연산을 한 줄로 처리할 수 있습니다.

Apache Commons Collections(org.apache.commons.collections4.CollectionUtils):

Collection<String> onlyInA = CollectionUtils.subtract(listA, listB);     // [apple, cherry]
Collection<String> inBoth = CollectionUtils.intersection(listA, listB);  // banana, date (any order)
Collection<String> inOnlyOne = CollectionUtils.disjunction(listA, listB); // apple, cherry, fig (any order)

Guava(com.google.common.collect.Sets)는 Set을 대상으로 하며 실시간 뷰를 반환합니다.

Set<String> setA = new HashSet<>(listA);
Set<String> setB = new HashSet<>(listB);

Set<String> onlyInA = Sets.difference(setA, setB);
Set<String> inBoth = Sets.intersection(setA, setB);
Set<String> inOnlyOne = Sets.symmetricDifference(setA, setB);

CollectionUtils는 각 요소가 몇 번 나오는지 고려하지만, Guava의 Sets는 고유 요소만 다룹니다. 기반 Set이 나중에 바뀐다면 new HashSet<>(view)로 Guava 뷰를 복사하세요.


8. 성능: 반복문 안에서 List를 검색하지 마세요

list.contains()는 요소를 하나씩 확인하므로, ArrayList.removeAll(otherList)와 filter(otherList::contains)는 한 리스트의 모든 요소를 다른 리스트의 모든 요소와 비교합니다. 10만 개짜리 리스트 두 개라면 최대 100억 번의 비교가 필요할 수 있습니다.

위의 모든 예제처럼 다른 리스트를 먼저 HashSet으로 감싸면 각 조회가 상수 시간이 되어, 전체 비교가 약 10만 단계로 끝납니다.

// Slow: O(n × m)
listA.stream().filter(listB::contains).toList();

// Fast: O(n + m)
Set<String> lookup = new HashSet<>(listB);
listA.stream().filter(lookup::contains).toList();

요약

목표 추천 방법 순서 유지? 참고
같음(같은 순서) a.equals(b) – List 구현이 달라도 동작
같음(순서 무관, 중복 포함) 복사본 정렬 또는 빈도 맵 비교 – 맵은 정렬할 수 없는 요소에도 사용 가능
같음(순서 무관, 중복 무시) new HashSet<>(a).equals(new HashSet<>(b)) – 반복 요소 무시
공통 요소 copy.retainAll(new HashSet<>(b)) 예 먼저 복사; List.of는 변경 불가
A에만 있음 copy.removeAll(new HashSet<>(b)) 또는 스트림 필터 예 스트림 방식은 아무것도 변경하지 않음
한쪽에만 있음 양방향 차이를 Stream.concat 예 또는 CollectionUtils.disjunction
객체 리스트 record 또는 equals() + hashCode() 예 또는 id 같은 키로 비교

HashSet 조회와 스트림 필터로 시작하세요. 빠르고, 리스트를 바꾸지 않으며, 거의 모든 경우에 맞습니다. 코드 밖에서 빠르게 확인하려면 온라인 목록 비교 도구가 브라우저에서 같은 일을 해 줍니다. Python을 쓰고 있다면 Python에서 두 리스트 비교하는 방법을 참고하세요.

태그

#Java#리스트 비교#튜토리얼

작성자 소개

songshiqiang1인 개발자

AI 도구로 재미있는 웹사이트를 만듭니다.

관련 글

튜토리얼

Google 스프레드시트에서 두 목록 비교하는 방법 (일치 항목과 차이)

조건부 서식, COUNTIF, MATCH, XLOOKUP, FILTER로 Google 스프레드시트에서 두 목록을 비교하는 방법: 일치 항목 강조, 빠진 항목 표시, 차이를 새 목록으로 추출, 서로 다른 시트의 목록 비교까지 다룹니다.

9분 소요
#Google 스프레드시트#리스트 비교#튜토리얼
더 읽기
튜토리얼

Python에서 두 리스트 비교하는 방법 (예제 포함)

Python에서 두 리스트를 비교하는 완벽 가이드: 집합으로 공통·차이 요소 찾기, 리스트 컴프리헨션으로 순서 유지하기, 리스트가 같은지 확인하기, 요소별로 비교하기, 딕셔너리 리스트 비교하기, pandas로 대용량 데이터 처리하기까지 다룹니다.

12분 소요
#Python#리스트 비교#튜토리얼
더 읽기
튜토리얼

Excel에서 두 목록 비교하는 방법 (일치 항목과 차이 찾기)

Excel에서 두 목록을 비교하는 9가지 방법: 조건부 서식으로 일치 항목 강조, COUNTIF나 MATCH로 표시, VLOOKUP이나 XLOOKUP으로 조회, FILTER로 추출, 행별 비교, 대용량 데이터는 Power Query로 처리하기.

12분 소요
#Excel#리스트 비교#XLOOKUP#튜토리얼
더 읽기