JAVA (8) 컬렉션

통밀·2026년 7월 29일

JAVA

목록 보기
8/10

컬렉션 프레임워크

들어가며

프로그램은 대부분 여러 데이터를 저장하고 조회하고 수정하며 삭제하는 작업을 반복한다.

회원 관리 프로그램에서는 여러 회원을 저장해야 하고, 쇼핑몰에서는 상품과 주문 목록을 관리해야 한다. 게시판에서는 여러 게시글을 저장하고 검색해야 한다.

여러 데이터를 저장하는 가장 기본적인 방법은 배열이다.

String[] names = new String[3];

names[0] = "김자바";
names[1] = "이자바";
names[2] = "박자바";

하지만 배열은 생성할 때 길이가 결정되고 이후 크기를 변경할 수 없다. 새로운 데이터를 추가하려면 더 큰 배열을 만들고 기존 데이터를 복사해야 한다.

String[] oldNames = {
        "김자바",
        "이자바",
        "박자바"
};

String[] newNames = new String[5];

System.arraycopy(
        oldNames,
        0,
        newNames,
        0,
        oldNames.length
);

데이터의 개수가 계속 변하는 프로그램에서 배열을 직접 관리하는 것은 번거롭다.

Java는 이러한 문제를 해결하기 위해 여러 데이터를 효율적으로 저장하고 처리할 수 있는 컬렉션 프레임워크를 제공한다.

이번 글에서는 컬렉션 프레임워크의 기본 구조와 List, Set, Map의 차이를 살펴본다. 이어서 정렬, 불변 컬렉션, 람다 표현식, 표준 함수형 인터페이스, Optional, Stream API까지 함께 정리해보려고 한다.


자료구조와 컬렉션 프레임워크

자료구조란?

자료구조는 데이터를 효율적으로 저장하고 접근하고 수정하기 위한 데이터의 조직 방식이다.

단순히 여러 값을 보관하는 것뿐 아니라 다음 내용까지 포함한다.

  • 데이터가 어떤 형태로 저장되는가
  • 데이터 사이에 어떤 관계가 있는가
  • 데이터를 어떻게 추가하고 조회하는가
  • 데이터를 어떻게 수정하고 삭제하는가
  • 특정 데이터를 얼마나 빠르게 찾을 수 있는가

동일한 데이터를 저장하더라도 자료구조에 따라 작업 성능이 달라질 수 있다.

예를 들어 인덱스로 특정 요소를 빠르게 조회하려면 배열 기반 구조가 유리하다. 중간에 데이터를 자주 추가하거나 삭제한다면 연결 구조가 더 적합할 수 있다.

따라서 무조건 좋은 자료구조가 있는 것이 아니라, 어떤 작업을 자주 수행하는지에 따라 적합한 자료구조를 선택해야 한다.


배열과 컬렉션의 차이

배열은 같은 타입의 여러 값을 저장하는 가장 기본적인 자료구조다.

int[] numbers = {10, 20, 30};

배열에는 다음과 같은 특징이 있다.

  • 생성할 때 길이가 결정된다.
  • 인덱스로 요소에 빠르게 접근할 수 있다.
  • 같은 타입의 값만 저장할 수 있다.
  • 기본 타입과 참조 타입 모두 저장할 수 있다.

배열의 길이를 동적으로 변경할 수 없다는 점은 가장 큰 불편함이다.

컬렉션은 내부적으로 배열이나 연결 구조 등을 사용하지만 개발자가 크기 변경과 데이터 관리를 직접 처리하지 않아도 된다.

List<String> names = new ArrayList<>();

names.add("김자바");
names.add("이자바");
names.add("박자바");

컬렉션은 데이터가 추가되면 필요한 저장 공간을 내부적으로 관리한다.


Object만 사용했을 때의 문제

모든 객체는 Object를 상속하므로 Object[]를 사용하면 여러 종류의 객체를 함께 저장할 수 있다.

Object[] values = new Object[3];

values[0] = "Java";
values[1] = 100;
values[2] = new Member("김자바");

값을 저장할 때는 편리하지만 꺼낼 때는 Object로 반환되기 때문에 강제 타입 변환이 필요하다.

String language = (String) values[0];
Integer number = (Integer) values[1];

잘못된 타입으로 변환하면 실행 중 ClassCastException이 발생한다.

제네릭을 사용하면 컬렉션이 저장할 타입을 컴파일 단계에서 제한할 수 있다.

List<String> languages = new ArrayList<>();

languages.add("Java");

// languages.add(100);
// 컴파일 오류

값을 꺼낼 때도 형변환이 필요 없다.

String language = languages.get(0);

컬렉션 프레임워크의 구조

컬렉션 프레임워크는 다수의 데이터를 저장하고 처리하기 위한 인터페이스와 구현 클래스의 집합이다.

주요 클래스와 인터페이스는 java.util 패키지에 포함되어 있다.

Collection
├─ List
│  ├─ ArrayList
│  └─ LinkedList
│
└─ Set
   ├─ HashSet
   └─ TreeSet

Map
├─ HashMap
└─ TreeMap

List와 Set은 Collection 인터페이스를 상속한다.

Map은 Key와 Value의 쌍을 관리하는 구조이기 때문에 Collection과는 별도의 계층으로 존재한다.


List, Set, Map 비교

구분데이터 구조순서중복
List값을 순서대로 관리유지됨허용
Set중복되지 않는 값 관리일반적으로 보장되지 않음허용하지 않음
MapKey와 Value의 쌍 관리구현체에 따라 다름Key 중복 불가, Value 중복 가능
List<String> list = new ArrayList<>();
Set<String> set = new HashSet<>();
Map<String, Integer> map = new HashMap<>();

Collection의 공통 메서드

List와 Set은 Collection 인터페이스를 상속하므로 공통 메서드를 사용할 수 있다.

분류대표 메서드기능
추가add(E e)요소 하나 추가
추가addAll(Collection<? extends E> c)다른 컬렉션의 모든 요소 추가
조회contains(Object o)특정 요소 포함 여부 확인
조회isEmpty()비어 있는지 확인
조회size()요소 개수 반환
조회iterator()반복자 반환
삭제remove(Object o)특정 요소 삭제
삭제removeAll(Collection<?> c)전달받은 요소들을 삭제
삭제clear()모든 요소 삭제
변환toArray()배열로 변환
import java.util.ArrayList;
import java.util.List;

public class CollectionMethodExample {

    public static void main(String[] args) {
        List<String> languages = new ArrayList<>();

        languages.add("Java");
        languages.add("Python");
        languages.add("JavaScript");

        System.out.println(languages.size());
        System.out.println(languages.contains("Java"));
        System.out.println(languages.isEmpty());

        languages.remove("Python");

        System.out.println(languages);

        languages.clear();

        System.out.println(languages.isEmpty());
    }
}

List: 순서가 있는 데이터 관리하기

List의 특징

List는 입력한 순서를 유지하고 중복된 요소를 허용한다.

List<String> names = new ArrayList<>();

names.add("김자바");
names.add("이자바");
names.add("김자바");

결과는 다음과 같다.

[김자바, 이자바, 김자바]

동일한 값이 두 번 저장되어도 모두 유지된다.

순서가 있기 때문에 인덱스로 요소에 접근할 수 있다.

String first = names.get(0);

List의 주요 메서드

메서드기능
add(E e)마지막에 요소 추가
add(int index, E e)지정된 위치에 요소 추가
get(int index)지정된 위치의 요소 조회
set(int index, E e)지정된 위치의 요소 변경
remove(int index)지정된 위치의 요소 삭제
indexOf(Object o)요소의 첫 번째 인덱스 반환
lastIndexOf(Object o)요소의 마지막 인덱스 반환
import java.util.ArrayList;
import java.util.List;

public class ListMethodExample {

    public static void main(String[] args) {
        List<String> names = new ArrayList<>();

        names.add("김자바");
        names.add("박자바");
        names.add(1, "이자바");

        System.out.println(names);

        String firstName = names.get(0);

        System.out.println(firstName);

        names.set(0, "김Java");
        names.remove(2);

        System.out.println(names);
    }
}

ArrayList

ArrayList는 배열을 기반으로 구현된 List다.

List<String> names = new ArrayList<>();

내부적으로 객체 배열을 사용하고, 저장 공간이 부족해지면 더 큰 배열을 생성한 뒤 기존 요소를 복사한다.

개념적으로 다음과 같은 구조다.

ArrayList

index    0        1        2
       ┌────────┬────────┬────────┐
value  │ "Java" │ "SQL"  │ "HTML" │
       └────────┴────────┴────────┘

배열 기반이므로 인덱스를 이용한 조회가 빠르다.

names.get(1);

반면 배열의 중간에 요소를 추가하거나 삭제하면 뒤에 있는 요소들을 이동해야 한다.

삭제 전

[A][B][C][D]

B 삭제

[A][C][D][ ]

C와 D를 앞으로 이동

초기 용량 지정하기

저장할 데이터 개수를 어느 정도 알고 있다면 초기 용량을 지정할 수 있다.

List<String> names = new ArrayList<>(100);

이 코드는 리스트의 크기를 100으로 만드는 것이 아니다.

요소를 저장할 내부 공간의 초기 용량을 설정한다.

System.out.println(names.size());

결과는 여전히 0이다.

초기 용량을 적절히 지정하면 배열을 반복해서 확장하고 복사하는 비용을 줄일 수 있다. 다만 필요 이상으로 크게 지정하면 메모리를 낭비할 수 있다.


LinkedList

LinkedList는 각 요소가 앞뒤 요소의 정보를 가지고 연결된 구조다.

Java의 LinkedList는 이중 연결 리스트로 구현되어 있다.

null ← [A] ⇄ [B] ⇄ [C] → null

각 노드는 개념적으로 다음 정보를 가진다.

이전 노드 참조
현재 데이터
다음 노드 참조
List<String> names = new LinkedList<>();

배열처럼 모든 요소가 연속된 메모리 공간에 저장될 필요가 없다.

요소의 위치를 이미 알고 있다면 주변 링크를 변경하여 추가하거나 삭제할 수 있다.

A ⇄ B ⇄ C

B 삭제

A ⇄ C

다만 특정 인덱스의 요소를 찾으려면 처음이나 끝에서부터 연결을 따라 이동해야 하므로 인덱스 조회는 ArrayList보다 느릴 수 있다.


ArrayList와 LinkedList 비교

작업ArrayListLinkedList
인덱스 조회빠름상대적으로 느림
마지막 요소 추가일반적으로 빠름빠름
중간 요소 추가·삭제요소 이동 필요노드 탐색 후 연결 변경
메모리 사용요소 배열 중심노드별 참조 정보 추가
캐시 효율좋음상대적으로 낮음

이론적으로는 중간 삽입과 삭제가 LinkedList에 유리해 보이지만, 실제 작업에서는 먼저 해당 위치를 찾는 탐색 비용이 발생한다.

따라서 일반적인 Java 애플리케이션에서는 ArrayList를 기본 선택으로 사용하는 경우가 많다.

앞뒤에서 요소를 자주 추가하거나 제거하는 큐나 덱 구조가 필요하다면 LinkedList보다 ArrayDeque가 더 적합한 경우도 많다.


주요 시간 복잡도

작업ArrayListLinkedList
인덱스 조회O(1)O(n)
값 검색O(n)O(n)
마지막 추가평균 O(1)O(1)
중간 추가·삭제O(n)탐색 O(n), 연결 변경 O(1)
첫 요소 추가·삭제O(n)O(1)

ArrayList의 마지막 추가는 저장 공간 확장이 필요한 순간에는 배열 복사로 O(n)이 걸리지만, 여러 연산을 평균 내면 상각 O(1)로 본다.


반복문에서 요소 삭제 시 주의하기

인덱스를 증가시키면서 요소를 삭제하면 다음 요소의 인덱스가 앞으로 당겨져 검사하지 못하는 요소가 생길 수 있다.

import java.util.ArrayList;
import java.util.List;

public class RemoveExample {

    public static void main(String[] args) {
        List<Integer> numbers =
                new ArrayList<>(
                        List.of(1, 3, 5, 2, 4)
                );

        for (int i = 0; i < numbers.size(); i++) {
            if (numbers.get(i) % 2 == 1) {
                numbers.remove(i);
            }
        }

        System.out.println(numbers);
    }
}

1을 삭제하면 뒤의 3이 인덱스 0으로 이동하지만 i는 1로 증가하므로 3을 확인하지 못할 수 있다.

뒤에서부터 순회하면 인덱스가 밀리는 문제를 피할 수 있다.

for (int i = numbers.size() - 1; i >= 0; i--) {
    if (numbers.get(i) % 2 == 1) {
        numbers.remove(i);
    }
}

메모에 적힌 조건은 다음처럼 수정해야 한다.

i >= 0

i <= 0을 사용하면 마지막 인덱스가 양수인 경우 반복문이 시작되지 않는다.


Iterator로 안전하게 삭제하기

컬렉션을 순회하면서 요소를 삭제해야 한다면 Iterator를 사용할 수 있다.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class IteratorRemoveExample {

    public static void main(String[] args) {
        List<Integer> numbers =
                new ArrayList<>(
                        List.of(1, 2, 3, 4, 5)
                );

        Iterator<Integer> iterator =
                numbers.iterator();

        while (iterator.hasNext()) {
            int number = iterator.next();

            if (number % 2 == 1) {
                iterator.remove();
            }
        }

        System.out.println(numbers);
    }
}

Iterator로 순회 중일 때는 컬렉션의 remove()가 아니라 iterator.remove()를 사용해야 한다.


removeIf()

조건에 맞는 요소를 삭제할 때는 removeIf()가 더 간결하다.

List<Integer> numbers =
        new ArrayList<>(
                List.of(1, 2, 3, 4, 5)
        );

numbers.removeIf(
        number -> number % 2 == 1
);

System.out.println(numbers);

실행 결과는 다음과 같다.

[2, 4]

향상된 for문에서 수정하면 안 되는 이유

다음 코드는 실행 중 ConcurrentModificationException을 발생시킬 수 있다.

for (Integer number : numbers) {
    if (number % 2 == 1) {
        numbers.remove(number);
    }
}

향상된 for문은 내부적으로 Iterator를 사용한다. 순회 중 컬렉션의 구조가 예상하지 못하게 변경되면 반복자가 안전하게 동작할 수 없기 때문에 예외가 발생한다.

요소를 삭제하려면 다음 방법을 사용한다.

  • Iterator.remove()
  • removeIf()
  • 뒤에서부터 인덱스 순회
  • 삭제할 요소를 별도로 수집한 뒤 removeAll()

remove(int)와 remove(Object)

List<Integer>에서 remove()를 사용할 때 특히 주의해야 한다.

List<Integer> numbers =
        new ArrayList<>(
                List.of(10, 20, 30)
        );

numbers.remove(1);

1은 삭제할 값이 아니라 인덱스로 해석된다.

결과는 다음과 같다.

[10, 30]

값 10을 삭제하고 싶다면 Integer 객체로 전달해야 한다.

numbers.remove(Integer.valueOf(10));

제네릭은 불공변이다

SpiderMan이 Person의 자식이라고 가정해보자.

public class Person {
}

public class SpiderMan extends Person {
}

배열은 다음 대입이 가능하다.

SpiderMan[] spiderMen =
        new SpiderMan[3];

Person[] people = spiderMen;

배열은 공변이기 때문이다.

하지만 제네릭은 불공변이므로 다음 대입은 불가능하다.

List<SpiderMan> spiderMen =
        new ArrayList<>();

// List<Person> people = spiderMen;
// 컴파일 오류

List<SpiderMan>은 List<Person>의 하위 타입이 아니다.

여러 자식 타입을 읽기 위한 목적이라면 와일드카드를 사용할 수 있다.

List<? extends Person> people = spiderMen;

Arrays.asList()

Arrays.asList()는 배열을 기반으로 고정 크기의 리스트를 만든다.

import java.util.Arrays;
import java.util.List;

public class ArraysAsListExample {

    public static void main(String[] args) {
        List<String> names =
                Arrays.asList(
                        "김자바",
                        "이자바",
                        "박자바"
                );

        names.set(0, "김Java");

        System.out.println(names);
    }
}

요소 변경은 가능하다.

names.set(0, "김Java");

그러나 크기를 변경하는 작업은 불가능하다.

// names.add("최자바");
// UnsupportedOperationException

// names.remove("이자바");
// UnsupportedOperationException

원본 배열과 리스트가 연결되어 있다는 점도 주의해야 한다.

String[] values = {"A", "B"};

List<String> list =
        Arrays.asList(values);

values[0] = "Changed";

System.out.println(list);

결과는 다음과 같다.

[Changed, B]

독립적으로 수정 가능한 ArrayList가 필요하다면 새 리스트로 복사한다.

List<String> names =
        new ArrayList<>(
                Arrays.asList(
                        "김자바",
                        "이자바"
                )
        );

Set: 중복되지 않는 데이터 관리하기

Set의 특징

Set은 중복된 요소를 허용하지 않는 컬렉션이다.

Set<String> languages =
        new HashSet<>();

languages.add("Java");
languages.add("Python");
languages.add("Java");

"Java"를 두 번 추가했지만 한 번만 저장된다.

[Java, Python]

HashSet은 입력 순서를 보장하지 않는다.

System.out.println(languages);

출력 순서는 저장 순서와 다를 수 있다.

순서가 필요하면 목적에 따라 다음 구현체를 고려할 수 있다.

  • LinkedHashSet: 입력 순서 유지
  • TreeSet: 정렬된 순서 유지

HashSet

HashSet은 해시값을 이용해 데이터를 저장하고 검색한다.

Set<Member> members =
        new HashSet<>();

내부적으로는 HashMap을 사용하며, 저장할 객체를 Key처럼 관리한다.


해시란?

해시는 임의 길이의 데이터를 일정한 규칙에 따라 고정된 범위의 값으로 변환하는 방식이다.

Java 객체는 hashCode() 메서드로 정수 형태의 해시 코드를 반환한다.

String value = "Java";

System.out.println(value.hashCode());

해시 기반 자료구조는 해시 코드를 이용해 데이터가 저장될 위치를 빠르게 찾는다.

개념적인 과정은 다음과 같다.

객체
→ hashCode()
→ 저장 위치 계산
→ 해당 위치에서 equals()로 최종 비교

해시 충돌

서로 다른 객체가 같은 해시값이나 같은 저장 위치를 가질 수 있다. 이를 해시 충돌이라고 한다.

해시 코드는 int 범위의 값이지만 실제로 수십억 개의 배열을 생성할 수는 없다. 내부 배열의 크기에 맞게 저장 위치를 계산하기 때문에 서로 다른 해시 코드도 같은 위치에 배정될 수 있다.

Java의 HashMap은 같은 위치에 여러 요소가 배정되면 연결 구조로 관리한다. 충돌이 많이 발생하고 일정 조건을 만족하면 Java 8부터 일부 구간을 트리 구조로 전환해 검색 성능이 지나치게 떨어지는 것을 줄인다.

따라서 HashSet과 HashMap이 항상 단순한 연결 리스트만 사용하는 것은 아니다.


중복 객체를 판단하는 기준

HashSet은 일반적으로 다음 두 조건을 이용해 논리적으로 같은 객체인지 판단한다.

hashCode()가 같음
그리고
equals()가 true

회원 전화번호가 같으면 같은 회원으로 판단하는 예제를 만들어보자.

import java.util.Objects;

public class SmartPhone {

    private final String number;
    private final String model;

    public SmartPhone(
            String number,
            String model
    ) {
        this.number = number;
        this.model = model;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }

        if (!(object instanceof SmartPhone phone)) {
            return false;
        }

        return Objects.equals(
                number,
                phone.number
        );
    }

    @Override
    public int hashCode() {
        return Objects.hash(number);
    }

    @Override
    public String toString() {
        return "SmartPhone{"
                + "number='" + number + '\''
                + ", model='" + model + '\''
                + '}';
    }
}
import java.util.HashSet;
import java.util.Set;

public class HashSetExample {

    public static void main(String[] args) {
        Set<SmartPhone> phones =
                new HashSet<>();

        phones.add(
                new SmartPhone(
                        "010-1111-2222",
                        "Model A"
                )
        );

        phones.add(
                new SmartPhone(
                        "010-1111-2222",
                        "Model B"
                )
        );

        System.out.println(phones.size());
        System.out.println(phones);
    }
}

전화번호를 기준으로 equals()와 hashCode()를 재정의했으므로 두 객체를 같은 데이터로 판단한다.

1

equals()를 재정의할 때는 반드시 동일한 필드 기준으로 hashCode()도 함께 재정의해야 한다.


Map: Key와 Value를 함께 관리하기

Map의 특징

Map은 Key와 Value의 쌍으로 데이터를 관리한다.

Key          Value

"apple"   → "사과"
"banana"  → "바나나"
"grape"   → "포도"

하나의 Key와 Value 쌍을 Entry라고 부른다.

Key는 중복될 수 없지만 Value는 중복될 수 있다.

Map<String, Integer> scores =
        new HashMap<>();

scores.put("김자바", 90);
scores.put("이자바", 90);

점수 90은 중복될 수 있다.

같은 Key로 값을 다시 저장하면 기존 값이 교체된다.

scores.put("김자바", 100);

Map의 주요 메서드

메서드기능
put(K key, V value)Key와 Value 저장
get(Object key)Key에 해당하는 Value 조회
getOrDefault(Object key, V defaultValue)값이 없으면 기본값 반환
containsKey(Object key)Key 존재 여부 확인
containsValue(Object value)Value 존재 여부 확인
remove(Object key)Key에 해당하는 Entry 삭제
keySet()모든 Key를 Set으로 반환
values()모든 Value를 Collection으로 반환
entrySet()모든 Entry를 Set으로 반환
size()Entry 개수 반환
clear()모든 Entry 삭제

HashMap 사용하기

import java.util.HashMap;
import java.util.Map;

public class HashMapExample {

    public static void main(String[] args) {
        Map<String, Integer> scores =
                new HashMap<>();

        scores.put("김자바", 90);
        scores.put("이자바", 85);
        scores.put("박자바", 95);

        int kimScore = scores.get("김자바");

        System.out.println(kimScore);

        scores.put("김자바", 100);

        System.out.println(scores);

        scores.remove("이자바");

        System.out.println(scores);
    }
}

존재하지 않는 Key 조회하기

get()으로 존재하지 않는 Key를 조회하면 null이 반환된다.

Integer score = scores.get("없는 회원");

기본값이 필요하다면 getOrDefault()를 사용할 수 있다.

int score =
        scores.getOrDefault(
                "없는 회원",
                0
        );

Map 순회하기

Key와 Value를 함께 사용할 때는 entrySet()이 편리하다.

for (
        Map.Entry<String, Integer> entry
                : scores.entrySet()
) {
    System.out.println(
            entry.getKey()
                    + ": "
                    + entry.getValue()
    );
}

람다를 사용하면 더 간결하게 작성할 수 있다.

scores.forEach(
        (name, score) ->
                System.out.println(
                        name + ": " + score
                )
);

HashMap의 내부 구조

HashMap은 내부적으로 Key의 hashCode()를 이용해 저장 위치를 계산한다.

개념적으로 다음과 같은 구조를 가진다.

내부 배열

index 0 → Entry
index 1 → Entry → Entry
index 2 → 비어 있음
index 3 → Entry

Entry에는 Key, Value, 해시 정보, 다음 Entry를 가리키는 정보 등이 포함된다.

Key의 중복을 판단할 때도 hashCode()와 equals()가 사용된다.

따라서 사용자 정의 객체를 HashMap의 Key로 사용할 때는 논리적인 동등성 기준에 맞게 두 메서드를 재정의해야 한다.

또한 Key로 사용하는 객체의 비교 기준 필드는 저장 후 변경하지 않는 것이 안전하다. Key의 해시값이 변경되면 저장된 데이터를 다시 찾지 못할 수 있다.


Set과 Map의 관계

HashSet은 내부적으로 HashMap을 이용한다.

Set에 저장할 요소를 HashMap의 Key로 사용하고, Value에는 의미 없는 공통 객체를 저장하는 방식이다.

HashSet 요소
→ HashMap의 Key

내부 Value
→ 공통 더미 객체

이 때문에 HashSet도 HashMap과 마찬가지로 hashCode()와 equals()를 이용해 중복을 판단한다.


컬렉션 생성과 정렬

불변 컬렉션

Java 9부터 List.of(), Set.of(), Map.of()를 이용해 변경할 수 없는 컬렉션을 만들 수 있다.

List<String> languages =
        List.of(
                "Java",
                "Python",
                "JavaScript"
        );

다음 작업은 모두 불가능하다.

// languages.add("C++");
// languages.remove("Java");
// languages.set(0, "Kotlin");

실행하면 UnsupportedOperationException이 발생한다.


List.of(), Set.of(), Map.of()

List<String> list =
        List.of("A", "B", "C");

Set<String> set =
        Set.of("A", "B", "C");

Map<String, Integer> map =
        Map.of(
                "A", 10,
                "B", 20
        );

이 컬렉션들은 null을 허용하지 않는다.

// List.of("A", null);
// NullPointerException

Set.of()에는 중복값을 전달할 수 없다.

// Set.of("A", "A");
// IllegalArgumentException

Map.of()도 중복된 Key를 허용하지 않는다.


수정 가능한 컬렉션으로 복사하기

List.of()로 만든 데이터를 수정해야 한다면 새 컬렉션으로 복사한다.

List<String> languages =
        new ArrayList<>(
                List.of(
                        "Java",
                        "Python"
                )
        );

languages.add("JavaScript");

Arrays.asList()와 List.of() 비교

구분Arrays.asList()List.of()
크기 변경불가능불가능
요소 변경 set()가능불가능
null 요소허용허용하지 않음
원본 배열 연결연결됨해당 없음
도입 버전초기 Java부터Java 9

List 정렬하기

Collections.sort()를 이용해 리스트를 오름차순으로 정렬할 수 있다.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SortExample {

    public static void main(String[] args) {
        List<Integer> numbers =
                new ArrayList<>(
                        List.of(5, 2, 4, 1, 3)
                );

        Collections.sort(numbers);

        System.out.println(numbers);
    }
}

실행 결과는 다음과 같다.

[1, 2, 3, 4, 5]

List의 sort() 메서드를 사용할 수도 있다.

numbers.sort(null);

내림차순 정렬

numbers.sort(
        Collections.reverseOrder()
);

Comparator를 사용하면 다음과 같이 작성할 수도 있다.

numbers.sort(
        (left, right) ->
                Integer.compare(
                        right,
                        left
                )
);

사용자 정의 객체 정렬

회원 객체를 나이순으로 정렬해보자.

public class Member {

    private final String name;
    private final int age;

    public Member(
            String name,
            int age
    ) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return name + "(" + age + ")";
    }
}
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class MemberSortExample {

    public static void main(String[] args) {
        List<Member> members =
                new ArrayList<>(
                        List.of(
                                new Member("김자바", 25),
                                new Member("이자바", 20),
                                new Member("박자바", 30)
                        )
                );

        members.sort(
                Comparator.comparingInt(
                        Member::getAge
                )
        );

        System.out.println(members);
    }
}

나이가 같은 경우 이름으로 추가 정렬할 수 있다.

members.sort(
        Comparator
                .comparingInt(Member::getAge)
                .thenComparing(Member::getName)
);

Comparable과 Comparator

구분ComparableComparator
메서드compareTo()compare()
정렬 기준클래스 내부의 기본 정렬외부에서 다양한 정렬 기준 정의
패키지java.langjava.util
사용 예이름을 기본 정렬 기준으로 지정나이순, 점수순 등 상황별 정렬

클래스 자체에 자연스러운 기본 정렬 기준이 있다면 Comparable을 구현할 수 있다.

다양한 정렬 기준이 필요하다면 Comparator가 더 유연하다.


람다 표현식으로 동작 전달하기

람다 표현식이란?

람다 표현식은 하나의 동작을 간결한 코드 블록으로 표현하는 문법이다.

기존에는 간단한 동작 하나를 전달하기 위해 익명 구현 객체를 만들어야 했다.

Comparator<Integer> comparator =
        new Comparator<Integer>() {

            @Override
            public int compare(
                    Integer left,
                    Integer right
            ) {
                return Integer.compare(
                        left,
                        right
                );
            }
        };

람다 표현식을 사용하면 다음처럼 줄일 수 있다.

Comparator<Integer> comparator =
        (left, right) ->
                Integer.compare(
                        left,
                        right
                );

람다는 메서드 자체가 아니라 함수형 인터페이스의 구현 객체로 해석된다.


함수형 인터페이스

추상 메서드가 하나만 존재하는 인터페이스를 함수형 인터페이스라고 한다.

@FunctionalInterface
public interface Calculator {

    int calculate(int left, int right);
}

람다 표현식을 구현 객체로 대입할 수 있다.

Calculator add =
        (left, right) ->
                left + right;

int result = add.calculate(10, 20);

@FunctionalInterface는 해당 인터페이스가 함수형 인터페이스의 조건을 만족하는지 컴파일러가 검사하도록 한다.

default 메서드와 static 메서드는 여러 개 존재해도 상관없다. 추상 메서드만 하나여야 한다.


람다 문법

매개변수가 하나라면 괄호를 생략할 수 있다.

value -> System.out.println(value)

실행문이 하나라면 중괄호를 생략할 수 있다.

(left, right) -> left + right

실행문이 여러 개라면 중괄호를 사용한다.

(left, right) -> {
    int result = left + right;

    return result;
}

중괄호를 사용하면서 값을 반환하려면 return이 필요하다.


컬렉션과 람다

컬렉션 API에는 람다 표현식을 사용할 수 있는 메서드가 많다.

List<String> names =
        new ArrayList<>(
                List.of(
                        "김자바",
                        "이자바",
                        "박자바"
                )
        );

names.forEach(
        name ->
                System.out.println(name)
);

조건에 맞는 요소를 삭제할 수도 있다.

names.removeIf(
        name -> name.startsWith("김")
);

모든 요소를 변경할 수도 있다.

names.replaceAll(
        String::toUpperCase
);

메서드 참조

람다 표현식이 기존 메서드 하나만 호출한다면 :: 연산자를 이용한 메서드 참조로 표현할 수 있다.

names.forEach(
        name ->
                System.out.println(name)
);

다음처럼 변경할 수 있다.

names.forEach(
        System.out::println
);

정적 메서드 참조

Function<String, Integer> parser =
        Integer::parseInt;

다음 람다와 같다.

Function<String, Integer> parser =
        value -> Integer.parseInt(value);

특정 객체의 인스턴스 메서드 참조

String prefix = "Java";

Predicate<String> checker =
        prefix::equals;

다음 람다와 같다.

Predicate<String> checker =
        value -> prefix.equals(value);

임의 객체의 인스턴스 메서드 참조

Function<String, String> converter =
        String::toUpperCase;

다음 람다와 같다.

Function<String, String> converter =
        value -> value.toUpperCase();

생성자 참조

Supplier<StringBuilder> supplier =
        StringBuilder::new;

다음 람다와 같다.

Supplier<StringBuilder> supplier =
        () -> new StringBuilder();

매개변수가 있는 생성자도 참조할 수 있다.

Function<String, StringBuilder> creator =
        StringBuilder::new;

순회 중 같은 컬렉션을 수정하지 않기

다음 코드는 잘못된 예제다.

// names.forEach(names::add);

names를 순회하면서 같은 names에 다시 데이터를 추가하기 때문에 구조적 변경이 발생한다. ConcurrentModificationException이 발생하거나 의도하지 않은 동작으로 이어질 수 있다.

출력하려면 다음처럼 사용해야 한다.

names.forEach(System.out::println);

다른 컬렉션에 복사하려면 목적지 컬렉션을 별도로 둔다.

List<String> copied =
        new ArrayList<>();

names.forEach(copied::add);

표준 함수형 인터페이스

Java는 자주 사용하는 함수 형태를 java.util.function 패키지의 표준 인터페이스로 제공한다.

Consumer
Supplier
Function
Operator
Predicate

직접 함수형 인터페이스를 만들 필요 없이 목적에 맞는 표준 인터페이스를 사용할 수 있다.


Consumer

매개값을 받고 반환값이 없는 형태다.

T → 반환값 없음
Consumer<String> printer =
        value ->
                System.out.println(value);

printer.accept("Java");

메서드 참조로 표현할 수 있다.

Consumer<String> printer =
        System.out::println;

대표 메서드는 accept()다.


Supplier

매개값은 없고 값을 반환하는 형태다.

매개값 없음 → T
Supplier<StringBuilder> supplier =
        StringBuilder::new;

StringBuilder builder = supplier.get();

대표 메서드는 get()이다.


Function

매개값을 받아 다른 값을 반환한다.

T → R
Function<String, Integer> lengthFunction =
        String::length;

int length =
        lengthFunction.apply("Java");

대표 메서드는 apply()다.


Operator

매개값과 반환값의 타입이 같은 함수다.

T → T

하나의 값을 받아 같은 타입으로 반환하는 경우 UnaryOperator를 사용한다.

UnaryOperator<Integer> doubleValue =
        number -> number * 2;

System.out.println(
        doubleValue.apply(10)
);

두 값을 받아 같은 타입으로 반환하는 경우 BinaryOperator를 사용한다.

BinaryOperator<Integer> add =
        Integer::sum;

System.out.println(
        add.apply(10, 20)
);

Predicate

매개값을 받아 boolean을 반환한다.

T → boolean
Predicate<Integer> isEven =
        number -> number % 2 == 0;

System.out.println(
        isEven.test(10)
);

대표 메서드는 test()다.

필터링과 조건 검사에 많이 사용된다.


표준 함수형 인터페이스 비교

인터페이스매개변수반환값대표 메서드
Consumer<T>있음없음accept()
Supplier<T>없음있음get()
Function<T, R>있음있음apply()
UnaryOperator<T>있음동일 타입apply()
BinaryOperator<T>2개동일 타입apply()
Predicate<T>있음booleantest()

Optional과 Stream API

Optional이란?

Optional<T>은 값이 존재할 수도 있고 존재하지 않을 수도 있는 상황을 표현하는 래퍼 클래스다.

Optional<Member>

메서드가 null을 직접 반환하는 대신 값의 존재 여부를 명시적으로 표현할 수 있다.

import java.util.Optional;

public class MemberRepository {

    public Optional<String> findNameById(
            long id
    ) {
        if (id == 1L) {
            return Optional.of("김자바");
        }

        return Optional.empty();
    }
}

Optional 사용하기

Optional<String> name =
        repository.findNameById(1L);

if (name.isPresent()) {
    System.out.println(name.get());
}

다만 isPresent()와 get()을 반복해서 사용하는 것보다는 Optional 전용 메서드를 활용하는 것이 좋다.

name.ifPresent(
        System.out::println
);

값이 없을 때 기본값을 반환할 수 있다.

String result =
        name.orElse("이름 없음");

기본값 생성 비용이 크다면 orElseGet()을 사용할 수 있다.

String result =
        name.orElseGet(
                () -> createDefaultName()
        );

값이 없을 때 예외를 발생시킬 수도 있다.

String result =
        name.orElseThrow(
                () -> new IllegalArgumentException(
                        "회원을 찾을 수 없습니다."
                )
        );

orElse와 orElseGet의 차이

optional.orElse(
        createDefaultValue()
);

orElse()에 전달한 코드는 Optional에 값이 있어도 먼저 실행될 수 있다.

optional.orElseGet(
        () -> createDefaultValue()
);

orElseGet()은 값이 없을 때만 Supplier를 실행한다.

기본값 생성에 비용이 들거나 부수 효과가 있다면 orElseGet()이 적합하다.


Optional 사용 시 주의할 점

Optional은 주로 메서드 반환값에서 값이 없을 가능성을 표현하기 위해 사용한다.

다음과 같은 사용은 일반적으로 신중해야 한다.

  • 엔티티나 DTO의 필드 타입으로 사용
  • 메서드 매개변수로 사용
  • 컬렉션 자체를 Optional로 감싸기
  • Optional<List<T>>처럼 불필요하게 중첩하기
  • 무조건 get() 호출하기

컬렉션은 결과가 없으면 빈 컬렉션을 반환하는 것이 자연스럽다.

return List.of();

Stream API란?

Stream API는 배열과 컬렉션의 요소를 선언적인 방식으로 처리하기 위한 API다.

기존 반복문에서는 처리 절차를 직접 작성한다.

List<Integer> numbers =
        List.of(1, 2, 3, 4, 5);

List<Integer> evenNumbers =
        new ArrayList<>();

for (Integer number : numbers) {
    if (number % 2 == 0) {
        evenNumbers.add(number);
    }
}

Stream을 사용하면 어떤 작업을 할 것인지 중심으로 작성할 수 있다.

List<Integer> evenNumbers =
        numbers.stream()
                .filter(
                        number ->
                                number % 2 == 0
                )
                .toList();

Stream의 처리 흐름

Stream 연산은 크게 세 단계로 볼 수 있다.

데이터 소스
→ 중간 연산
→ 최종 연산
numbers.stream()
        .filter(number -> number % 2 == 0)
        .map(number -> number * 10)
        .forEach(System.out::println);
  • stream(): 스트림 생성
  • filter(): 조건에 맞는 요소 선택
  • map(): 요소 변환
  • forEach(): 결과 소비

중간 연산과 최종 연산

중간 연산은 새로운 Stream을 반환하며 여러 개 연결할 수 있다.

대표적인 중간 연산은 다음과 같다.

  • filter()
  • map()
  • sorted()
  • distinct()
  • limit()
  • skip()
  • peek()

최종 연산이 호출되어야 실제 처리가 시작된다.

대표적인 최종 연산은 다음과 같다.

  • forEach()
  • toList()
  • collect()
  • count()
  • reduce()
  • findFirst()
  • anyMatch()
  • allMatch()

Stream은 원본 컬렉션을 변경하지 않는다

List<Integer> numbers =
        new ArrayList<>(
                List.of(5, 2, 4, 1, 3)
        );

List<Integer> sortedNumbers =
        numbers.stream()
                .sorted()
                .toList();

System.out.println(numbers);
System.out.println(sortedNumbers);

실행 결과는 다음과 같다.

[5, 2, 4, 1, 3]
[1, 2, 3, 4, 5]

Stream 연산은 원본 컬렉션 자체를 정렬하지 않고 새로운 결과를 만든다.


map 연산

map()은 각 요소를 다른 값으로 변환한다.

List<String> names =
        List.of(
                "java",
                "spring",
                "vue"
        );

List<String> upperNames =
        names.stream()
                .map(String::toUpperCase)
                .toList();

reduce 연산

reduce()는 여러 요소를 하나의 결과로 합친다.

List<Integer> numbers =
        List.of(1, 2, 3, 4, 5);

int sum =
        numbers.stream()
                .reduce(
                        0,
                        Integer::sum
                );

System.out.println(sum);

0은 초기값이며 Integer::sum은 두 값을 합치는 함수다.


맵과 리듀스 모델

Map-Reduce 모델은 데이터를 변환한 뒤 결과를 하나로 합치는 처리 방식이다.

원본 데이터

[1, 2, 3, 4]

map
각 값을 제곱

[1, 4, 9, 16]

reduce
모든 값을 합산

30
int result =
        List.of(1, 2, 3, 4)
                .stream()
                .map(
                        number ->
                                number * number
                )
                .reduce(
                        0,
                        Integer::sum
                );

종합 예제: 회원 목록 처리하기

회원 데이터를 컬렉션에 저장하고 Stream으로 처리해보자.

Member 클래스

import java.util.Objects;

public class Member {

    private final long id;
    private final String name;
    private final int age;
    private final Grade grade;

    public Member(
            long id,
            String name,
            int age,
            Grade grade
    ) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.grade = grade;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Grade getGrade() {
        return grade;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }

        if (!(object instanceof Member member)) {
            return false;
        }

        return id == member.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return name
                + "("
                + age
                + ", "
                + grade
                + ")";
    }
}

Grade Enum

public enum Grade {
    BASIC,
    SILVER,
    GOLD
}

실행 클래스

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class CollectionExample {

    public static void main(String[] args) {
        List<Member> members =
                new ArrayList<>();

        members.add(
                new Member(
                        1L,
                        "김자바",
                        25,
                        Grade.GOLD
                )
        );

        members.add(
                new Member(
                        2L,
                        "이자바",
                        20,
                        Grade.BASIC
                )
        );

        members.add(
                new Member(
                        3L,
                        "박자바",
                        30,
                        Grade.SILVER
                )
        );

        members.add(
                new Member(
                        4L,
                        "최자바",
                        22,
                        Grade.GOLD
                )
        );

        List<String> adultGoldMembers =
                members.stream()
                        .filter(
                                member ->
                                        member.getAge() >= 20
                        )
                        .filter(
                                member ->
                                        member.getGrade()
                                                == Grade.GOLD
                        )
                        .sorted(
                                Comparator.comparingInt(
                                        Member::getAge
                                )
                        )
                        .map(Member::getName)
                        .toList();

        System.out.println(adultGoldMembers);

        Set<Member> memberSet =
                new HashSet<>(members);

        System.out.println(
                "중복 제거 후 회원 수: "
                        + memberSet.size()
        );

        Map<Long, Member> memberMap =
                new HashMap<>();

        for (Member member : members) {
            memberMap.put(
                    member.getId(),
                    member
            );
        }

        Member found =
                memberMap.get(1L);

        System.out.println(
                "조회 결과: " + found
        );
    }
}

이 예제에서는 다음 개념이 사용되었다.

  • ArrayList에 회원 목록 저장
  • HashSet으로 중복 관리
  • HashMap으로 ID 기반 조회
  • Enum으로 회원 등급 제한
  • 람다와 메서드 참조
  • Stream의 filter(), sorted(), map()
  • equals()와 hashCode()를 이용한 동등성 판단

헷갈리기 쉬운 개념

Collection과 Collections

Collection

List와 Set의 최상위 인터페이스다.

Collections

컬렉션을 정렬하거나 변환하는 정적 유틸리티 메서드를 제공하는 클래스다.

Collections.sort(list);
Collections.reverse(list);
Collections.shuffle(list);

List와 ArrayList

List<String> names =
        new ArrayList<>();

List는 인터페이스이고 ArrayList는 구현 클래스다.

변수는 인터페이스 타입으로 선언하는 것이 구현체 교체에 유리하다.

List<String> names =
        new LinkedList<>();

다만 ArrayList에만 존재하는 기능이 반드시 필요하다면 구체 타입을 사용할 수도 있다.


크기와 용량

List<String> names =
        new ArrayList<>(100);

초기 용량은 100이지만 실제 요소 개수는 0이다.

names.size();

결과는 0이다.

  • 크기: 현재 저장된 요소 개수
  • 용량: 내부 배열이 요소를 저장할 수 있는 공간

HashSet의 순서

HashSet은 입력 순서를 보장하지 않는다.

입력 순서가 필요하면 LinkedHashSet, 정렬된 순서가 필요하면 TreeSet을 고려한다.


Map은 Collection이 아니다

Map은 여러 데이터를 관리하지만 Collection 인터페이스를 상속하지 않는다.

Key와 Value의 쌍을 관리하는 구조가 List와 Set의 단일 요소 구조와 다르기 때문이다.


HashMap의 Key와 변경 가능한 객체

객체를 HashMap의 Key로 사용한 뒤 hashCode() 계산에 사용된 필드 값을 변경하면 해당 Key를 다시 찾지 못할 수 있다.

따라서 Map의 Key는 String, Integer, Enum, 불변 값 객체처럼 변경되지 않는 타입을 사용하는 것이 안전하다.


Stream과 컬렉션

컬렉션은 데이터를 저장한다.

Stream은 데이터를 저장하지 않고 데이터 처리 과정을 표현한다.

Collection
→ 데이터 저장

Stream
→ 데이터 처리

Stream은 한 번 최종 연산을 수행하면 다시 사용할 수 없다.

Stream<String> stream =
        names.stream();

stream.forEach(System.out::println);

// 같은 stream 재사용 불가

실무에서는 어떻게 사용할까?

일반적인 목록 관리에는 ArrayList가 가장 자주 사용된다.

List<Member> members =
        new ArrayList<>();

중복을 허용하지 않는 태그, 권한, 식별자 집합에는 HashSet이 적합하다.

Set<String> tags =
        new HashSet<>();

ID나 코드로 특정 객체를 빠르게 조회해야 한다면 HashMap을 사용할 수 있다.

Map<Long, Member> memberById =
        new HashMap<>();

하지만 데이터가 데이터베이스에 저장되어 있다면 애플리케이션에서 모든 데이터를 Map으로 복사하기보다 데이터베이스의 인덱스와 조회 기능을 활용해야 한다.

컬렉션 타입은 가능하면 인터페이스로 선언한다.

List<Member> members =
        new ArrayList<>();

Map<Long, Member> memberMap =
        new HashMap<>();

외부에 컬렉션을 반환할 때 내부 컬렉션이 직접 변경되면 안 된다면 불변 복사본을 반환할 수 있다.

public List<Member> getMembers() {
    return List.copyOf(members);
}

Stream은 필터링, 변환, 집계가 연속적으로 이어질 때 가독성이 좋다.

members.stream()
        .filter(Member::isActive)
        .map(Member::getName)
        .sorted()
        .toList();

반면 복잡한 상태 변경, 예외 처리, 여러 단계의 분기가 포함되면 일반 반복문이 더 명확할 수 있다. Stream을 사용한다고 항상 더 좋은 코드는 아니다.


오늘 배운 내용 정리

이번 글에서는 Java 컬렉션 프레임워크와 함수형 데이터 처리 방법을 정리했다.

  • 자료구조는 데이터를 효율적으로 저장하고 접근하고 수정하기 위한 조직 방식이다.
  • 배열은 길이가 고정되어 있어 데이터 개수가 자주 변하는 상황에서 불편할 수 있다.
  • 컬렉션 프레임워크는 여러 데이터를 쉽게 저장하고 처리하기 위한 인터페이스와 클래스의 집합이다.
  • List는 입력 순서를 유지하고 중복을 허용한다.
  • Set은 중복을 허용하지 않는다.
  • Map은 Key와 Value의 쌍으로 데이터를 관리한다.
  • Map의 Key는 중복될 수 없지만 Value는 중복될 수 있다.
  • ArrayList는 배열 기반이므로 인덱스 조회가 빠르다.
  • ArrayList의 중간 삽입과 삭제에는 요소 이동이 필요하다.
  • LinkedList는 각 요소가 앞뒤 노드의 참조를 가지는 이중 연결 구조다.
  • LinkedList는 인덱스 조회를 위해 연결을 따라 이동해야 한다.
  • 일반적인 목록에는 ArrayList가 기본 선택으로 자주 사용된다.
  • 컬렉션 순회 중 직접 요소를 삭제하면 ConcurrentModificationException이 발생할 수 있다.
  • 순회 중 삭제에는 Iterator.remove() 또는 removeIf()를 사용할 수 있다.
  • 인덱스로 삭제할 때는 뒤에서부터 순회하면 인덱스 이동 문제를 피할 수 있다.
  • List<Integer>의 remove(int)는 값이 아니라 인덱스를 삭제한다.
  • 제네릭은 불공변이므로 List<Dog>는 List<Animal>의 하위 타입이 아니다.
  • Arrays.asList()는 크기를 변경할 수 없지만 요소 교체는 가능하다.
  • HashSet과 HashMap은 hashCode()와 equals()를 이용해 객체를 구분한다.
  • equals()를 재정의하면 hashCode()도 같은 기준으로 재정의해야 한다.
  • HashSet은 내부적으로 HashMap을 사용한다.
  • 사용자 정의 객체를 Map의 Key로 사용할 때는 비교 기준 필드를 변경하지 않는 것이 좋다.
  • List.of(), Set.of(), Map.of()는 변경할 수 없는 컬렉션을 생성한다.
  • List.of() 계열은 null을 허용하지 않는다.
  • Collections.sort()나 List.sort()를 이용해 리스트를 정렬할 수 있다.
  • Comparable은 클래스의 기본 정렬 기준을 정의한다.
  • Comparator는 외부에서 다양한 정렬 기준을 제공한다.
  • 람다 표현식은 함수형 인터페이스의 구현을 간결하게 표현한다.
  • 함수형 인터페이스에는 추상 메서드가 하나만 존재해야 한다.
  • 메서드 참조는 기존 메서드 하나를 호출하는 람다를 간결하게 표현한다.
  • Consumer는 매개값을 받고 반환하지 않는다.
  • Supplier는 매개값 없이 값을 반환한다.
  • Function은 값을 받아 다른 값으로 변환한다.
  • Operator는 동일한 타입을 받아 동일한 타입으로 반환한다.
  • Predicate는 값을 받아 boolean을 반환한다.
  • Optional은 값이 존재할 수도 있고 없을 수도 있는 상황을 표현한다.
  • Optional의 값을 꺼낼 때 무조건 get()을 사용하는 것은 피하는 것이 좋다.
  • Stream API는 배열과 컬렉션의 요소를 선언적인 방식으로 처리한다.
  • Stream은 데이터를 저장하지 않으며 원본 컬렉션을 직접 변경하지 않는다.
  • filter()는 조건에 맞는 요소를 선택한다.
  • map()은 요소를 다른 값으로 변환한다.
  • reduce()는 여러 요소를 하나의 결과로 합친다.
  • Stream의 중간 연산은 최종 연산이 호출될 때 실행된다.
profile
넘어가 울타리 green green

0개의 댓글