자바 스트림: 초보자 가이드

안녕하세요, 미래의 자바 마법사 여러분! 오늘 우리는 자바 스트림의 세계에 대한 흥미로운 여정을 시작할 것입니다. 프로그래밍에 새로운 사람이라면 걱정하지 마세요 - 저는 여러분의 친절한 가이드가 되겠습니다. 단계별로 설명하겠습니다. 이 튜토리얼이 끝나면, 프로처럼 데이터를 스트리밍할 수 있을 것입니다!

Java - Streams

자바에서 스트림이란?

자바에서 스트림은 like a conveyor belt sushi restaurant에서의寿司 접시를 상상해 보세요.寿司 접시는 지속적으로 당신의 앞으로 이동하고, 원하는 것을 골라 먹을 수 있습니다. 자바에서 스트림은 이와 같습니다 - 하나씩 처리할 수 있는 요소의 시퀀스이며, 모든 것을 메모리에 저장할 필요 없이 처리할 수 있습니다.

스트림은 자바 8에서 도입되어 데이터 콜렉션을 더 쉽고 효율적으로 처리할 수 있도록 했습니다. 스트림을 사용하면 데이터에 대한 연산을 더 선언적(declarative) 방식으로 수행할 수 있어, 자바에게 무엇을 하고 싶은지 말하는 것보다 어떻게 하고 싶은지 말하는 것을 피할 수 있습니다.

자바에서 스트림 생성하기

우리는 첫 번째 스트림을 생성해 보겠습니다. 여러 가지 방법이 있지만, 간단한 예제부터 시작하겠습니다:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {
public static void main(String[] args) {
// List에서 스트림 생성
List<String> fruits = Arrays.asList("apple", "banana", "cherry", "date");
Stream<String> fruitStream = fruits.stream();

// 직접 스트림 생성
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);
}
}

이 예제에서 우리는 두 개의 스트림을 생성했습니다. 첫 번째 fruitStream은 과일의 List에서 생성되었습니다. 두 번째 numberStreamStream.of() 메서드를 사용하여 직접 생성되었습니다.

일반 스트림 연산

이제 우리는 스트림을 가지고 있으므로, 수행할 수 있는 일반적인 연산을 살펴보겠습니다.

forEach 메서드

forEach 메서드는 스트림의 각 요소를 통해 이동하고 해당 요소에 대해 무언가를 수행하는 친절한 로봇입니다. 우리는 이를 사용하여 과일을 인쇄해 보겠습니다:

fruits.stream().forEach(fruit -> System.out.println(fruit));

이렇게 하면 다음과 같이 인쇄됩니다:

apple
banana
cherry
date

여기서 fruit -> System.out.println(fruit)은 람다 표현식입니다. 각 요소에 대해 무엇을 하고 싶은지 자바에게 간단히 알리는 방법입니다.

map 메서드

map 메서드는 스트림의 각 요소를 변환하는 마법의 지팡이입니다. 우리는 이를 사용하여 모든 과일을 대문자로 변환해 보겠습니다:

List<String> upperCaseFruits = fruits.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseFruits);

이렇게 하면 다음과 같이 인쇄됩니다:

[APPLE, BANANA, CHERRY, DATE]

String::toUpperCase은 메서드 참조로, 각 문자열에 대해 toUpperCase 메서드를 사용하라는 것과 같습니다.

filter 메서드

filter 메서드는 클럽의 보안원처럼 특정 요소만을 통과시킵니다. 우리는 이를 사용하여 'a'로 시작하는 과일만 남겨보겠습니다:

List<String> aFruits = fruits.stream()
.filter(fruit -> fruit.startsWith("a"))
.collect(Collectors.toList());
System.out.println(aFruits);

이렇게 하면 다음과 같이 인쇄됩니다:

[apple]

limit 메서드

limit 메서드는 "이만큼만, 감사합니다!"라고 말하는 것과 같습니다. 스트림을 특정 수의 요소로 제한합니다:

List<String> firstTwoFruits = fruits.stream()
.limit(2)
.collect(Collectors.toList());
System.out.println(firstTwoFruits);

이렇게 하면 다음과 같이 인쇄됩니다:

[apple, banana]

sorted 메서드

sorted 메서드는 책장을 정리하는 것과 같습니다. 요소를 순서대로 정렬합니다:

List<String> sortedFruits = fruits.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedFruits);

이렇게 하면 다음과 같이 인쇄됩니다:

[apple, banana, cherry, date]

병렬 처리

스트림의 멋진 기능 중 하나는 병렬 처리가 쉽게 가능하여, 큰 데이터셋에서의 연산을 가속화할 수 있습니다. 병렬 스트림을 생성하는 방법은 다음과 같습니다:

fruits.parallelStream().forEach(fruit -> System.out.println(fruit + " " + Thread.currentThread().getName()));

이렇게 하면 다음과 같이 인쇄될 수 있습니다:

banana ForkJoinPool.commonPool-worker-1
apple main
date ForkJoinPool.commonPool-worker-2
cherry ForkJoinPool.commonPool-worker-3

순서는 각 실행 시 다를 수 있습니다. 왜냐하면 과일이 병렬로 처리되기 때문입니다!

Collectors

Collectors는 스트림 연산의 결과를 수집하는 특별한 도구입니다. 우리는 collect(Collectors.toList())을 사용하여 스트림 결과를 다시 List로 변환해 왔습니다. 다른 유용한 Collectors도 많이 있습니다:

// 요소를 문자열로 결합
String fruitString = fruits.stream().collect(Collectors.joining(", "));
System.out.println(fruitString);  // 인쇄: apple, banana, cherry, date

// 요소 수 계산
long fruitCount = fruits.stream().collect(Collectors.counting());
System.out.println(fruitCount);  // 인쇄: 4

// 요소 그룹화
Map<Character, List<String>> fruitGroups = fruits.stream()
.collect(Collectors.groupingBy(fruit -> fruit.charAt(0)));
System.out.println(fruitGroups);  // 인쇄: {a=[apple], b=[banana], c=[cherry], d=[date]}

통계

스트림을 사용하면 숫자 데이터에 대한 통계를 계산할 수 있습니다:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream().mapToInt(Integer::intValue).summaryStatistics();
System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Average: " + stats.getAverage());

이렇게 하면 다음과 같이 인쇄됩니다:

Count: 5
Sum: 15
Min: 1
Max: 5
Average: 3.0

자바 스트림 예제

이제 모든 것을 하나로 모아 더 복잡한 예제를 보겠습니다. 가령 학생들의 나이와 성적이 있는 List가 있다고 가정해 봅시다:

class Student {
String name;
int age;
double grade;

Student(String name, int age, double grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
}

public class StreamExample {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("Alice", 22, 90.5),
new Student("Bob", 20, 85.0),
new Student("Charlie", 21, 92.3),
new Student("David", 23, 88.7)
);

// 21세 이상 학생들의 평균 성적 계산
double averageGrade = students.stream()
.filter(student -> student.age > 21)
.mapToDouble(student -> student.grade)
.average()
.orElse(0.0);

System.out.println("Average grade of students over 21: " + averageGrade);

// 성적이 가장 높은 상위 2명의 이름 가져오기
List<String> topStudents = students.stream()
.sorted((s1, s2) -> Double.compare(s2.grade, s1.grade))
.limit(2)
.map(student -> student.name)
.collect(Collectors.toList());

System.out.println("Top 2 students: " + topStudents);
}
}

이 예제는 우리가 여러 스트림 연산을 연속적으로 수행하여 복잡한 데이터 처리 작업을 짧고 읽기 쉬운 방식으로 수행할 수 있음을 보여줍니다.

그럼 여러분, 자바 스트림의 세계로的第一步을 걸었습니다. 연습이 완벽을 이루는 열쇠이므로, 이 개념들을 실험해 보지 마세요. 언제든지 프로처럼 데이터를 스트리밍할 수 있을 것입니다! 행복한 코딩을!

Credits: Image by storyset