목차
자바 8부터 도입된 스트림 API(Stream API)는 컬렉션 데이터를 함수형 프로그래밍 스타일로 간결하고 효율적으로 처리할 수 있는 강력한 기능을 제공합니다. 그중에서도 map
연산은 스트림의 각 요소를 변환하여 새로운 스트림을 생성하는 데 핵심적인 역할을 합니다. 오늘은 이 map
연산에 대해 다양한 예제 코드를 통해 깊이 있게 알아보겠습니다.
1. map
이란 무엇일까요? 기본 개념 잡기
스트림의 map
연산은 입력 스트림의 각 요소를 특정 함수(Function)에 적용하여, 그 반환값으로 이루어진 새로운 스트림을 생성하는 중간 연산(intermediate operation)입니다. 마치 공장에서 컨베이어 벨트를 따라오는 재료들을 하나씩 가공하여 새로운 제품으로 만드는 과정과 유사합니다.
- 입력:
Stream<T>
(T 타입 요소들의 스트림) - 적용:
Function<T, R>
(T 타입 요소를 받아 R 타입 요소로 변환하는 함수) - 출력:
Stream<R>
(R 타입 요소들의 스트림)
핵심은 "변환"입니다. 기존 요소를 다른 값이나 다른 타입의 요소로 매핑(mapping)하는 것이죠.
2. 가장 기본적인 map
사용법: 숫자 리스트 변환
가장 간단한 예제로 숫자 리스트의 각 요소를 제곱하는 경우를 살펴보겠습니다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MapExample1 {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 각 숫자를 제곱하여 새로운 리스트 생성
List<Integer> squaredNumbers = numbers.stream()
.map(number -> number * number) // 각 숫자를 제곱으로 매핑
.collect(Collectors.toList());
System.out.println("원본 숫자 리스트: " + numbers);
System.out.println("제곱된 숫자 리스트: " + squaredNumbers);
// 출력:
// 원본 숫자 리스트: [1, 2, 3, 4, 5]
// 제곱된 숫자 리스트: [1, 4, 9, 16, 25]
}
}
위 코드에서 map(number -> number * number)
부분이 핵심입니다. 리스트의 각 number
요소에 대해 number * number
연산을 수행하고, 그 결과들을 모아 새로운 스트림을 만듭니다. 마지막으로 collect(Collectors.toList())
를 통해 스트림을 다시 리스트로 변환합니다.
3. 문자열 리스트 변환: 길이 구하기 및 대문자 변환
이번에는 문자열 리스트를 다뤄보겠습니다. 각 문자열의 길이를 구하거나, 모든 문자열을 대문자로 변환하는 예제입니다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MapExample2 {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "banana", "cherry");
// 각 단어의 길이를 구하는 리스트
List<Integer> wordLengths = words.stream()
.map(String::length) // 메서드 참조 사용
.collect(Collectors.toList());
System.out.println("단어 길이 리스트: " + wordLengths);
// 출력: 단어 길이 리스트: [5, 6, 6]
// 각 단어를 대문자로 변환하는 리스트
List<String> upperCaseWords = words.stream()
.map(String::toUpperCase) // 메서드 참조 사용
.collect(Collectors.toList());
System.out.println("대문자 단어 리스트: " + upperCaseWords);
// 출력: 대문자 단어 리스트: [APPLE, BANANA, CHERRY]
}
}
여기서는 람다 표현식 대신 메서드 참조(String::length
, String::toUpperCase
)를 사용했습니다. s -> s.length()
는 String::length
와 동일하게 동작하며, 코드를 더 간결하게 만들어줍니다.
4. 객체 리스트에서 특정 정보 추출하기
map
은 객체 리스트에서 원하는 정보만 추출하여 새로운 리스트를 만들 때 매우 유용합니다. 예를 들어, User
객체 리스트에서 사용자 이름만 뽑아내거나 나이만 추출하는 경우입니다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
class User {
private String name;
private int age;
public User(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 "User{name='" + name + "', age=" + age + "}";
}
}
public class MapExample3 {
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("Alice", 30),
new User("Bob", 25),
new User("Charlie", 35)
);
// 사용자 이름만 추출하여 리스트 생성
List<String> userNames = users.stream()
.map(User::getName) // User 객체에서 이름(String)으로 매핑
.collect(Collectors.toList());
System.out.println("사용자 이름 리스트: " + userNames);
// 출력: 사용자 이름 리스트: [Alice, Bob, Charlie]
// 사용자 나이만 추출하여 리스트 생성
List<Integer> userAges = users.stream()
.map(User::getAge) // User 객체에서 나이(int)로 매핑
.collect(Collectors.toList());
System.out.println("사용자 나이 리스트: " + userAges);
// 출력: 사용자 나이 리스트: [30, 25, 35]
}
}
User
객체 스트림을 map(User::getName)
을 통해 String
타입의 이름 스트림으로 변환하고, map(User::getAge)
를 통해 Integer
타입의 나이 스트림으로 변환했습니다. 이처럼 map
은 객체의 특정 필드 값을 추출하는 데 효과적입니다.
5. map
을 이용한 타입 변환: 문자열을 숫자로
map
연산의 강력함은 단순히 값을 변경하는 것을 넘어, 요소의 타입을 변환할 수 있다는 점에서도 드러납니다. 예를 들어, 문자열로 된 숫자 리스트를 실제 숫자(Integer) 리스트로 변환할 수 있습니다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MapExample4 {
public static void main(String[] args) {
List<String> numberStrings = Arrays.asList("10", "25", "50", "100");
// 문자열 리스트를 Integer 리스트로 변환
List<Integer> actualNumbers = numberStrings.stream()
.map(Integer::parseInt) // String을 Integer로 매핑
.collect(Collectors.toList());
System.out.println("문자열 숫자 리스트: " + numberStrings);
System.out.println("변환된 숫자 리스트: " + actualNumbers);
// 변환된 숫자로 연산 수행
int sum = actualNumbers.stream()
.mapToInt(Integer::intValue) // IntStream으로 변환 (효율적)
.sum();
System.out.println("숫자들의 합: " + sum);
// 출력:
// 문자열 숫자 리스트: [10, 25, 50, 100]
// 변환된 숫자 리스트: [10, 25, 50, 100]
// 숫자들의 합: 185
}
}
map(Integer::parseInt)
를 통해 각 문자열 요소를 Integer
객체로 변환했습니다. 만약 기본형(primitive type)인 int
스트림이 필요하다면 mapToInt()
와 같은 특화된 map
연산을 사용할 수 있으며, 이는 숫자 연산 시 오토박싱/언박싱 오버헤드를 줄여 성능상 이점을 가질 수 있습니다.
6. map
사용 시 주의할 점 및 팁
- 반환 타입:
map
연산에 전달되는 람다 표현식이나 메서드 참조는 반드시 값을 반환해야 합니다. 반환된 값들이 새로운 스트림의 요소가 됩니다. - 새로운 스트림 생성:
map
은 원본 스트림(또는 컬렉션)을 변경하지 않고, 항상 새로운 스트림을 생성합니다. 이는 함수형 프로그래밍의 불변성(immutability) 원칙과 맞닿아 있습니다. - 지연 연산(Lazy Evaluation):
map
은 중간 연산이므로, 최종 연산(e.g.,collect
,forEach
,sum
)이 호출되기 전까지는 실제로 실행되지 않습니다. - Null 처리:
map
함수 내부에서null
을 반환하면, 결과 스트림에는null
요소가 포함될 수 있습니다. 만약 매핑 과정에서NullPointerException
이 발생할 수 있는 코드가 있다면, 적절한 예외 처리나Optional
사용을 고려해야 합니다.
마무리
오늘은 자바 스트림 API의 핵심 연산 중 하나인 map
에 대해 다양한 예제 코드를 통해 알아보았습니다. map
은 스트림의 각 요소를 변형하여 새로운 형태의 데이터를 만들어내는 강력하고 유연한 도구입니다. 컬렉션 데이터를 다룰 때 map
을 적재적소에 활용하면 코드를 더욱 간결하고 표현력 있게 작성할 수 있습니다. 직접 다양한 시나리오에 map
을 적용해보면서 그 편리함을 느껴보시길 바랍니다!