티스토리 뷰
Lambda와 Stream은 Java8부터 도입되었지만, 그동안 개발하면서 Lambda와 Stream 사용에 대한 경험이 적어서 제대로 알지 못했다. 그래서 Lambda와 Stream이 무엇이고 왜 사용하는지에 대해 알아보려고 한다.
Lambda 정의
익명 함수(anonymous function)로, 메서드나 기능을 간단하게 표현할 수 있는 방법이다.
Java 8에서 도입되었으며, 주로 함수형 인터페이스(Functional Interface)의 인스턴스를 생성하기 위해 사용된다.
Lambda 문법
(매개변수1, 매개변수2, ...) -> { 실행할 코드 }
1. 람다는 메서드 이름이 없다.
2. 람다는 반환 타입이 없다. 정확하게 말하자면 람다식은 값을 반환하는 식이지만, 그 자체로 변수를 할당하는 방식이 아니라 함수형 인터페이스에 할당해야 한다.
함수형 인터페이스 (Functional Interface)
단 하나의 추상 메서드를 갖는 인터페이스이다. 예를 들어 Runnable 이 있다.
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
//함수형 인터페이스인 Runnable을 람다 표현식으로 사용하기
public class Main {
public static void main(String[] args) {
//익명 클래스 사용하여 인터페이스 구현하였을 때
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
});
//Lambda 사용 했을 때
Thread thread = new Thread(() -> System.out.println("Hello Lambda"));
thread.start();
}
}
자바에서 제공하는 함수형 인터페이스
함수형 인터페이스는 java.util.function 패키지에 존재한다.
다음은 주로 사용하는 함수형 인터페이스이다. 더 많은 함수형 인터페이스를 제공하니 여기를 참고하도록 한다.
Runnable 주로 스레드 실행 또는 비동기 작업 정의와 같이 입력 값, 반환 값이 없는 작업을 정의할 때 사용
Supplier<T> T는 반환될 객체의 타입, 매개변수 없이 값을 반환하는 함수를 정의할 때 사용
Consumer<T> T는 입력된 객체의 타입, 입력 값을 받지만 값을 반환하지 않는 함수를 정의할 때 사용
Function<T, R> T는 입력된 객체의 타입이고 R은 반환될 객체의 타입, 입력 값을 받아 처리한 후 결과를 반환하는 함수를 정의할 때 사용
Predicate<T> T는 입력된 객체의 타입, 입력 값에 대해 조건 검사나 필터링하는 함수를 정의할 때 사용
public class Main {
public static void main(String[] args) {
Supplier<String> suplier = () -> "Supplier";
suplier.get();
Consumer<String> consumer = str -> System.out.println(str);
consumer.accept("Consumer");
Function<String, Integer> function = s -> s.length();
function.apply("Function");
Predicate<String> predicate = s -> "Predicate"::equals;
predicate.test("Predicates");
}
}
함수형 인터페이스 만들어 보기
함수형 인터페이스를 만들기 위해서는 단 하나의 추상 메서드만 포함해야 한다.
인터페이스를 생성하고, @FunctionalInterface 어노테이션을 선언하여 함수형 인터페이스임을 명시한다. 이 어노테이션은 선택 사항이지만, 사용하였을 때 컴파일러가 함수형 인터페이스 규칙을 체크해준다. 예를 들어, 두 개 이상의 추상 메서드를 생성하면 컴파일 에러가 발생한다.
Stream 정의
Stream 의 데이터를 흐름(flow) 형태로 처리할 수 있게 해주는 API이다.
컬렉션(Collection)이나 배열(Array) 등의 데이터 소스를 함수형 프로그래밍 스타일로 처리할 수 있게 지원하며, 선언적이고 간결한 방식으로 데이터 변환, 필터링, 집계 등의 연산을 수행할 수 있게 도와준다.
!! 컬렉션에서는 바로 컬렉션.stream()을 사용할 수 있지만, 배열같은 경우는 Arrays.stream(배열)과 같이 사용해야한다.
Stream의 구성요소
스트림은 연산을 수행하기 위해 스트림 파이프라인으로 구성된다.
스트림 파이프라인은 데이터를 처리하는 일련의 작업들을 순차적으로 연결한 흐름을 의미한다.
Source (소스)
소스는 스트림의 출발점이 되는 데이터이다. 스트림을 만들기 위한 데이터의 출발점이 되는 데이터 구조로 배열, 컬렉션, 파일 I/O, Generator 함수 등이 있다.)
Intermediate Operations (중간 연산)
스트림을 변환하여 새로운 스트림을 반환하는 연산으로 실행이 지연된다. 즉, 실제로 실행되기 전까지 어떠한 동작도 수행하지 않는다. 마지막 종료 연산이 호출되었을 때, 그때 스트림을 처리한다.
filter(Predicate), map(Function), sorted() 등이 있다.
Terminal Operation (종료 연산)
스트림 파이프라인을 종료하는 연산이다. 스트림을 소비하여 최종 결과를 반환하거나 사이드 이펙트를 발생시킨다.
count(), forEach(Consumer) 등이 있다.
Stream 특징
지연 실행 (Lazy Evaluation)
스트림은 지연 실행을 지원한다. 중간 연산은 종료 연산이 호출될 때까지 실행되지 않는다.
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 5, 3, 4, 5);
//중간 연산자 반환
System.out.println("#####중간 연산자의 lazy 실행 확인#####");
IntStream stream = numbers.stream()
.filter(n -> {
System.out.println("filter 중간 연산 " + n);
return n % 2 == 0;
})
.mapToInt(n -> {
System.out.println("map 중간 연산 " + n);
return n * n;
});
System.out.println("#####중간 연산자인 Intstream 생성 완료#####");
long sum = stream.sum(); //종료 연산자
System.out.println("#####종료 연산자 호출완료, sum 계산 완료 : " + sum);
}
}
.
.
.
//출력 결과
#####중간 연산자의 lazy 실행 확인#####
#####중간 연산자인 Intstream 생성 완료#####
filter 중간 연산 1
filter 중간 연산 2
map 중간 연산 2
filter 중간 연산 3
filter 중간 연산 4
map 중간 연산 4
filter 중간 연산 5
#####종료 연산자 호출완료, sum 계산 완료 : 20
sum이라는 종료 연산자가 실행되기 전까지 중간 연산자에 작성된 코드는 실제로 실행되지 않는다. 종료 연산인 sum 을 호출하는 시점에 중간 연산자들이 실행되고, 그 때 처리된 내용들이 출력된다.
상태 없는 (Parallel)
스트림 연산은 대부분 상태가 없어야 한다. 각 연산은 이전 연산의 결과에 의존하지 않으며, 외부 상태를 변경하지 않고 독립적으로 처리된다. 멀티스레드 환경에서 스레드 안전하게 스트림을 처리할 수 있게 한다.
순차적 (Sequential) 또는 병렬적 (Parallel) 처리
스트림은 순차적 또는 병렬적으로 실행할지 선택할 수 있다. 예를 들어 Collection.stream()은 순차 스트림을 생성하고 Collection.parallelStream은 병렬 스트림을 생성한다. 병렬 스트림을 선택하면 멀티코어를 활용하여 데이터 처리를 병렬적으로 수행할 수 있는데, 큰 데이터셋을 빠르게 처리할 수 있도록 한다.
불변성 (Immutability)
스트림의 소스(원본 데이터)는 변경하지 않고, 스트림의 연산은 새로운 결과를 반환한다.
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubledNumbers = numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList()); // 새로운 리스트 생성
System.out.println(numbers); // [1, 2, 3, 4, 5] (원본 데이터는 변하지 않음)
System.out.println(doubledNumbers); // [2, 4, 6, 8, 10] (새로운 데이터)
}
}
Stream 을 사용하는 이유
코드 간결화와 가독성 향상
예를 들어, 자동차로 구성 된 List 컬렉션에서 자동차의 생산국가가 한국인 자동차를 찾고 모델명을 기준으로 내림차순해서 모델명만 찾아아야 한다고 가정해본다.
public class Main {
public static void main(String[] args) {
List<Car> cars = Arrays.asList(new Car("tucson", "korea"),
new Car("model 3", "USA"),
new Car("a6", "germany"),
new Car("sportage", "korea"),
new Car("accord", "japan"));
//1. 한국 자동차만 리스트로 추출한다.
List<Car> koreaCars = new ArrayList<>();
for (Car car : cars) {
if ("korea".equals(car.getOrigin())) {
koreaCars.add(car);
}
}
//2. 모델 명 기준으로 내림차순 정렬을 한다.
koreaCars.sort((o1, o2) -> o2.getModel().compareTo(o1.getModel()));
//3. 한국 자동차의 모델명만 추출한다.
List<String> koreaCarModels = new ArrayList<>();
for (Car car : koreaCars) {
koreaCarModels.add(car.getModel());
}
for (String model : koreaCarModels) {
System.out.println(koreaCarModels);
}
}
}
먼저, 한국 자동차를 리스트로 추출하기 위해서 koreaCars 리스트를 만든다. 그런 다음 모델명을 기준으로 내림차순 정렬을 진행한 다음에 koreaCars 리스트를 만들어서 모델명을 추출하여 결과를 얻는다.
이 코드는 명령형 프로그래밍 스타일로 작성되었으며, 코드가 "어떻게" 실행될지를 세부적으로 명시하는 방식으로 구현되어있다. 자세한 내용은 다음 포스팅에서 이야기 할 것이다.
코드가 "어떻게" 실행될지를 단계별로 구현하면서 결과를 얻기까지의 과정이 길어지고, 중간에 사용되었던 변수들이 메모리에 남게된다.
그럼 Stream 을 사용해본다.
public class Main {
public static void main(String[] args) {
List<Car> cars = Arrays.asList(new Car("tucson", "korea"),
new Car("model 3", "USA"),
new Car("a6", "germany"),
new Car("sportage", "korea"),
new Car("accord", "japan"));
cars.stream()
.filter(car -> "korea".equals(car.getOrigin())) //1. 한국 자동차를 찾는다.
.sorted(Comparator.comparing(Car::getBrand).reversed()) //2. 모델 명 기준으로 내림차순 정렬을 한다.
.map(car -> car.getBrand()) //3. 한국 자동차의 모델명만 추출한다.
.toList()
.forEach(System.out::println);
}
}
불필요한 중간 변수들을 제거하고, 훨씬 더 간결하고 가독성있게 코드를 구현할 수 있다.
Lambda, Stream은 Java 8에서 도입된 함수형 프로그래밍을 할 수 있도록 도와준다.
명령형 프로그래밍에서의 복잡한 반복문을 대체하고, 데이터 처리를 직관적이고 효율적으로 만들어준다.
Lambda 는 익명 함수로, 함수형 인터페이스에서만 사용할 수 있다.
Stream은 컬렉션 또는 배열의 데이터 흐름을 순차적 또는 병렬적으로 처리할 수 있게 도와주는 API이다.
다음에는 위에서 몇 번 언급되었던 프로그래밍 패러다임에 대해 알아봐야겠다.
참고
https://docs.oracle.com/javase/8/docs/api/?java/util/stream/Stream.html
'Language > Java' 카테고리의 다른 글
메모리 관리: Generational Garbage Collection (1) | 2024.11.13 |
---|---|
HashMap : 해시 충돌이 발생하였을 때 (0) | 2024.11.11 |
컬렉션 이터레이터의 Fail-safe 와 Fail-fast (0) | 2024.11.10 |
Hash 기반 자료구조 (1) | 2024.11.09 |
Hash에 대해서 (0) | 2024.11.08 |
- Total
- Today
- Yesterday
- fail-safe
- Load Balancer
- 추상클래스
- 인스턴스변수
- Security
- HashSet
- HashMap
- nginx
- Spring
- AutoConfiguration
- Caching
- Red-Black Tree
- 다중화
- fail-fast
- spring boot
- java
- 티스토리챌린지
- syncronized
- JPA
- Hash
- 고정 세션
- Sticky Session
- 로드 밸런서
- 자동구성
- nosql
- @conditional
- 인터페이스
- 정적변수
- 오블완
- object
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |