Java 제네릭, 컬렉션, 람다와 스트림 핵심 정리

Alchemist·2026년 10월 8일

KT AIVLE

목록 보기
73/130

Java 기초 문법과 객체지향을 익혔다면 다음으로 자주 만나는 개념이 제네릭, 컬렉션, 람다, 스트림이다.

이 기능들은 데이터를 타입 안전하게 보관하고, 반복되는 코드를 줄이며, 컬렉션을 선언적으로 처리하는 데 사용된다.

1. 제네릭은 타입을 매개변수로 받는다

타입마다 비슷한 클래스를 따로 만든다고 생각해보자.

class StringBox {
    String value;
}

class IntegerBox {
    Integer value;
}

동작은 같은데 타입만 다르다. 제네릭을 사용하면 하나의 클래스로 만들 수 있다.

class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}
Box<String> textBox = new Box<>();
textBox.set("response");

Box<Integer> countBox = new Box<>();
countBox.set(10);

컴파일러가 타입을 확인하므로 잘못된 값을 넣는 실수를 줄일 수 있다.

2. 제네릭 메소드

메소드 단위로도 타입 매개변수를 사용할 수 있다.

public static <T> void printValue(T value) {
    System.out.println(value);
}
printValue(100);
printValue("hello");
printValue(true);

동작은 같고 타입만 달라지는 경우 코드 중복을 줄일 수 있다.

3. Wrapper 클래스

제네릭 타입에는 기본 자료형을 직접 넣을 수 없다.

// List<int>는 사용할 수 없음

대신 기본 자료형에 대응하는 Wrapper 클래스를 사용한다.

기본 자료형Wrapper
intInteger
longLong
doubleDouble
booleanBoolean
charCharacter

Java는 기본 자료형과 Wrapper 사이를 자동으로 변환해주는 오토박싱/언박싱을 지원한다.

Integer count = 10; // autoboxing
int value = count;  // unboxing

단, Wrapper는 객체이므로 null이 될 수 있다.

Integer count = null;
// int value = count; // NullPointerException 가능

4. ArrayList: 가장 자주 쓰는 리스트

크기가 고정된 배열과 달리 ArrayList는 요소를 동적으로 추가하거나 제거할 수 있다.

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

List<String> endpoints = new ArrayList<>();
endpoints.add("/users");
endpoints.add("/orders");
System.out.println(endpoints.get(0));
System.out.println(endpoints.size());
System.out.println(endpoints.contains("/orders"));

endpoints.set(0, "/members");
endpoints.remove("/orders");

가능하면 변수 타입은 구현체보다 인터페이스인 List로 선언하면 구현 교체가 쉬워진다.

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

5. LinkedList: 링크로 연결된 리스트

import java.util.LinkedList;

LinkedList<String> queue = new LinkedList<>();
queue.add("job-1");
queue.addFirst("urgent-job");
queue.addLast("job-2");

LinkedList를 단순히 "삽입과 삭제가 언제나 빠르다"고 외우면 정확하지 않다. 특정 노드 위치를 이미 알고 있다면 연결 변경은 빠르지만, 인덱스로 원하는 위치를 찾는 데는 O(n) 이 걸릴 수 있다.

일반적인 순차 데이터 처리에서는 ArrayList가 더 자주 선택된다. 큐/덱 용도라면 ArrayDeque가 더 적합한 경우도 많다.

6. HashSet: 중복 없는 집합

import java.util.HashSet;
import java.util.Set;

Set<String> roles = new HashSet<>();
roles.add("ADMIN");
roles.add("USER");
roles.add("ADMIN");

System.out.println(roles.size()); // 2

HashSet은 중복 요소를 허용하지 않고 기본적으로 반복 순서를 보장하지 않는다.

System.out.println(roles.contains("ADMIN"));
roles.remove("USER");

중복 제거, 포함 여부 검사에 잘 맞는다.

7. HashMap: Key와 Value로 데이터 저장

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

Map<String, Integer> requestCount = new HashMap<>();
requestCount.put("/users", 120);
requestCount.put("/orders", 85);
System.out.println(requestCount.get("/users"));
System.out.println(requestCount.containsKey("/orders"));
System.out.println(requestCount.size());

HashMap은 키의 중복을 허용하지 않지만 값은 중복될 수 있다. 같은 키로 다시 put()하면 기존 값이 교체된다.

또한 일반 HashMap은 순서를 보장하지 않는다.

8. Iterator

컬렉션 요소를 순차적으로 방문하기 위한 표준 인터페이스다.

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

List<String> jobs = new ArrayList<>();
jobs.add("sync");
jobs.add("backup");

Iterator<String> iterator = jobs.iterator();

while (iterator.hasNext()) {
    String job = iterator.next();
    System.out.println(job);
}

반복 중 안전하게 현재 요소를 제거해야 할 때도 사용할 수 있다.

while (iterator.hasNext()) {
    String job = iterator.next();
    if (job.equals("backup")) {
        iterator.remove();
    }
}

9. 익명 클래스

한 번만 사용할 구현체를 별도 클래스 파일 없이 즉석에서 만들 수 있다.

Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("작업 실행");
    }
};

작은 구현에는 편리하지만 코드가 길어질 수 있다. 함수형 인터페이스라면 람다로 더 간결하게 표현할 수 있다.

10. 람다식

람다는 이름 없는 동작을 값처럼 전달하는 문법이다.

(x, y) -> x + y

Java의 람다는 아무 메소드나 대신하는 것이 아니라 함수형 인터페이스의 구현으로 사용된다.

@FunctionalInterface
interface Calculator {
    int calculate(int x, int y);
}
Calculator add = (x, y) -> x + y;
Calculator multiply = (x, y) -> x * y;

System.out.println(add.calculate(2, 3));      // 5
System.out.println(multiply.calculate(2, 3)); // 6

함수형 인터페이스는 추상 메소드가 하나인 인터페이스다.

11. 스트림으로 컬렉션 처리하기

스트림은 배열이나 컬렉션 데이터를 필터링, 변환, 집계하는 흐름을 선언적으로 표현한다.

List<Integer> responseTimes = List.of(120, 80, 310, 95, 220);

responseTimes.stream()
    .filter(time -> time >= 200)
    .map(time -> time / 10)
    .forEach(System.out::println);

흐름을 나누면 다음과 같다.

데이터 소스
  ↓
filter: 필요한 데이터만 선택
  ↓
map: 다른 값으로 변환
  ↓
forEach: 최종 처리

결과를 새 리스트로 받기

List<Integer> slowTimes = responseTimes.stream()
    .filter(time -> time >= 200)
    .toList();

원본 리스트를 직접 변경하지 않고 새로운 결과를 얻을 수 있다.

12. 스트림은 언제 좋은가?

반복문보다 스트림이 항상 좋은 것은 아니다.

단순한 순회나 복잡한 상태 변경이 필요한 코드에서는 반복문이 더 읽기 쉬울 수 있다. 반면 필터 → 변환 → 집계처럼 데이터 처리 단계가 명확한 경우 스트림이 의도를 잘 드러낸다.

long count = responseTimes.stream()
    .filter(time -> time >= 200)
    .count();

코드가 짧다는 이유보다 처리 과정이 더 명확해지는지를 기준으로 선택하면 된다.

핵심 정리

  • 제네릭은 타입을 매개변수화해 코드 재사용성과 타입 안정성을 높인다.
  • 기본 자료형을 제네릭에서 사용할 때는 Integer, Double 같은 Wrapper 클래스를 쓴다.
  • ArrayList는 일반적인 동적 리스트로 가장 자주 사용된다.
  • HashSet은 중복을 제거하고, HashMap은 Key-Value 구조로 데이터를 저장한다.
  • HashMap은 키 중복은 허용하지 않지만 값 중복은 가능하다.
  • Iterator는 컬렉션을 순회하며 반복 중 요소를 제거할 때 유용하다.
  • 람다는 함수형 인터페이스를 간결하게 구현한다.
  • 스트림은 컬렉션의 필터링·변환·집계를 선언적으로 표현한다.
profile
html_programming_language

0개의 댓글