Search

Lambda & Stream 주요개념

1. JAVA8 람다

람다란 메소드를 하나의 식으로 표현한 것으로, 1급 객체처럼 사용할 수 있음
람다는 익명객체에 선언된 메소드이기 때문에 함수의 매개변수 또는 반환형으로 사용하더라도 결국 객체를 매개변수 반환형으로 사용하는 것과 같음
익명객체 기반의 메소드이기 때문에 코드의 간결성이 대폭 향상된 결과를 얻을 수 있음

가. 람다 표현식이란

메소드를 하나의 식으로 표현한 것
// 일반 메소드 형식 int max(int x, int y){ return x > y ? x : y; } // 람다 표현식 (x, y) -> x > y ? x: y;
Java
복사
람다의 장점: 클래스와 객체를 선언하지 않고 메소드를 사용할 수 있기 때문에 코드의 간결성이 향상됨
→ 자바는 클래스의 선언과 객체를 생성해야 메소드를 사용할 수 있음.
익명 클래스: 클래스의 이름을 선언하지 않고 하나의 객체만을 생성
→ 람다 표현식은 익명 클래스에 해당함
→ 메소드의 반환값에 추가적으로 변수 선언하지 않을 때 사용
new boolean[]{true, false, true};
Java
복사

나. 람다 표현식 유의사항

Calulator 클래스의 calc() 메소드를 람다로 구현하는 예제
→ interface Calculator를 익명클래스 방식으로 인스턴스화함
interface Calculator { int calc(int n); } class Driver { public static void main(String[] args) { int n = 2; // 생성과 동시에 구현할 메소드를 override 함 Calculator cal = new Calculator() { @Override int calc(int n) { return n + 1; } } System.out.println(cal.calc(n)); // 3 } }
Java
복사
람다로 메소드 간략화
1) 유지하는 것: 매개변수(타입 + 값), 메소드 내부로직
2) 제거하는 것: 생성자, 메소드의 반환타입, 메소드 이름
3) 추가하는 것: →
class Driver { public static void main(String[] args) { int n = 2; // (매개변수) -> {구현로직} Calculator cal = (int n) -> {return n + 1;}; System.out.println(cal.calc(n)); // 3 } }
Java
복사
매개변수 타입을 추론할 수 있는 경우, 타입 생략 가능
class Driver { public static void main(String[] args) { int n = 2; // (매개변수) -> {구현로직} Calculator cal = (n) -> {return n + 1;}; System.out.println(cal.calc(n)); // 3 } }
Java
복사
매개변수가 하나인 경우, 괄호 생략 가능
→ 매개변수가 없는 경우, ()가 필요. ex) () -> {return "hi";}
class Driver { public static void main(String[] args) { int n = 2; // (매개변수) -> {구현로직} Calculator cal = n -> {return n + 1;}; System.out.println(cal.calc(n)); // 3 } }
Java
복사
함수의 몸체가 하나의 명령문만 있을 경우, 중괄호와 return 생략가능
→ 이 때, 하나의 명령문 뒤에 세미콜론 X
class Driver { public static void main(String[] args) { int n = 2; Calculator cal = n -> n + 1; // 간단하게 변경 System.out.println(cal.calc(n)); // 3 } }
Java
복사
함수의 몸체가 하나의 return문인 경우, 중괄호 생략 불가
return문에 람다를 사용할 경우, return 값은 람다의 결과값

다. 함수형 인터페이스

람다 표현식을 참조 변수에 대입하는 경우, 해당 참조 변수의 타입이 함수형 인터페이스
하나의 함수형 인터페이스에는 하나의 추상 메소드만 정의됨
annotation으로 함수형 인터페이스 명시
→ 아래와 같이 maxNum이라는 참조 변수에 람다식 또는 익명객체가 할당되기 때문에 메소드의 변수로서 전달이 가능한 것
// 함수형 인터페이스 선언 @FunctionalInterface interface Calculator { public int max(int x, int y); } // 함수형 인터페이스 활용 public static void main(String[] args){ Calculator maxNum = (x, y) -> x > y ? x : y; System.out.println(maxNum.max(19, 34); }
Java
복사
함수형 인터페이스
→ 함수형 인터페이스 타입은 공식 문서에서 각종 함수를 정의할 때 참고할 수 있음
함수형 인터페이스
메서드
설명
java.lang.Runnable
void run()
매개변수 X 반환값 X
Function<T, R>
R apply(T t)
하나의 매개변수, 하나의 반환값
Predicate<T>
boolean test(T t)
하나의 매개변수, 반환 타입은 boolean
Supplier<T>
T get()
매개변수 X, 반환값 O
Consumer<T>
void accept(T t)
매개변수 O, 반환값 X
BiFunction<T, R>
R apply(T t, U u)
두개의 매개변수, 하나의 반환값
BiPredicate<T>
boolean test(T t, U t)
두개 매개변수, 반환 타입은 boolean
UnaryOperator<T>
T apply(T t)
Function<T, R>의 자손 단 매개변수와 반환값의 타입이 같음
BinaryOperator<T>
T apply(T t, T t)
BiFunction<T, R>의 자손

라. 메소드 참조

람다 표현식이 오직 하나의 메소드만 호출하는 경우, 해당 메소드에 매개변수 전달 부분을 생략
사용방법
1.
클래스 이름 or 참조변수 이름 뒤에 :: 붙임
2.
매개변수 입력부분을 생략
MyClass obj = new MyClass; Function<String, Boolean> func = (a) -> obj.equals(a); // 람다 표현식 // to Function<String, Boolean> func = obj::equals(a); // or Function<String, Boolean> func = MyClass::equals(a);
Java
복사
메소드 참조 미적용 vs 적용
DoubleUnaryOperator oper; oper = (n) -> Math.abs(n); // 람다 표현식 System.out.println(oper.applyAsDouble(-5)); oper = Math::abs; // 메소드 참조 System.out.println(oper.applyAsDouble(-5));
Java
복사

마. 생성자 참조

추후 람다와 메소드 참조가 익숙해 질 때 다시 학습

2. JAVA8 스트림

Stream이란?

가. Stream API

Stream 탄생 배경: 다양한 형태(배열, 컬렉션, 파일 등)로 저장된 데이터에 대해 공통된 방식으로 다루기 위해
→ 기존 방식으로는 데이터의 형태(배열, 컬렉션)에 따라 다른 방식으로 데이터에 접근
→ 데이터에 따라 반복문 or iterator 사용함에 따라 가독성이 덜어지고 코드의 재사용이 불가능
Stream API의 특징
→ 스트림은 원본 데이터를 변경하지 않고, 원본 데이터를 기반으로 수정한 데이터를 새로 생성해서 반환한다
→ 최종 연산으로 닫힌 스트림은 재사용이 불가능하다
→ 스트림의 간결성의 핵심은 반복문을 드러나지 않게 내부적으로 처리하는 것
→ 중개연산은 해당 연산이 진행될 것이라는 표시, 실제 연산은 중개 연산의 과정을 모두 모아서 최종연산 단계에서 실행됨

나. 스트림 생성

Stream 생성 부분 추가
List to Stream
List<Task> values = categories.get(category); for(Task task : values){ if(!task.isCompleted()){ result.add(task); } } // to categories.get(category).stream() .filter(task -> !task.isCompleted()) .forEach(result::add);
Java
복사
임의의 수 생성, ints(), longs(), doubles() 등의 메소드를 Random 클래스에서 사용
IntStream intStream = new Random().ints(); intStream.limit(5).forEach(System.out::println); // or IntStream intStream = new Random().ints(5).forEach(System.out::println);
Java
복사
iterate(), generate(): 람다식을 매개변수로 받아서, 람다식의 결과로 무한 스트림 생성
→ iterate은 seed값을 매개변수로 전달, generate은 매개변수 없이
반환타입이 Integer이므로 IntStream으로 사용 시, mapToInt를 사용해야 함
타입 간 변환
→ IntStream → Stream<Integer>: boxed
→ Stream<Integer> → IntStream: mapToInt
→ IntStream → Stream<T>: mapToObj()
IntStream randomNumbers = new Random().ints(1, 46); Stream<String> lottoNumbers = randomNumbers.distinct().limit(6).sorted() .mapToObj(i -> i+","); lottoNumbers.forEach(System.out::println);
Java
복사

다. 스트림 중개연산(intermediate operation)

중개연산은 연산 결과가 스트림인 연산으로, 다른 중개연산으로 연결할 수 있다
중개연산의 핵심: map(), flatMap()
map: 매개변수로 전달된 함수의 반환값으로 이루어진 새로운 스트림 반환
fileStream.map(File::getName) // Stream<File> -> Stream<String> .filter(s -> s.indexOf('.') != -1) // 확장자가 있는 것만 반환 .map(s -> s.substring(s.indexOf('.')+1)) // 확장자 이하의 문자열만 반환 .map(String::toUpperCase) // 대문자로 반환 .distinct() // 중복 제거 .forEach(System.out::print); // 출력
Java
복사
mapToInt(), mapToLong, mapToDouble(): 본 중개연산을 활용하여 기본형 스트림으로 반환 것이 유용함
→ 단순 map()은 연산 결과로 Stream<T> 반환하여 auto unboxing의 과정을 거치므로 비효율적임.
→ mapToObj(): IntStream → Stream <T>
→ Stream<Integer>: IntStream → Stream <Integer>
Stream<Integer> studentScore = studentStream.map(Student::getTotalScore); // to IntStream studentScore = studentStream.mapToInt(Student::getTotalScore);
Java
복사
→ 기본형 스트림은 다양한 연산을 지원하는 최종 연산 메서드를 제공함. 반면 Stream<Integer>의 경우, count, max, min만 제공함
int sum() OptionalDouble average() OptionalInt max() OptionalInt min()
Java
복사
summaryStatistics(): 중간연산까지 완료된 기본형 스트림에 대해 연산 관련 최종 스트림을 연속으로 사용가능
IntStream studentScore = studentStream.mapToInt(Student::getTotalScore); long totalScore = studentScore.sum() // O OptionalDouble average = studentScore.average() // X // to IntStream studentScore = studentStream.mapToInt(Student::getTotalScore); IntSummaryStatistics stat = studentScore.summaryStatistics(); long totalScore = stat.getSum() // O OptionalDouble average = stat.getAverage() // O
Java
복사
flatMap: Stream<T[]> → Stream<T>
→ Stream의 형태가 배열 또는 Collection의 형태와 같이 요소의 집합일 대, 각각의 요소를 stream으로 반환
List<Task> result = new LinkedList<>(); for (List<Task> tasks : categories.values()) { tasks.stream() .filter(task -> !task.isCompleted()) .forEach(result::add); // to List<Task> result = new LinkedList<>(); categories.values().stream() .flatMap(Collection::stream) .filter(task -> !task.isCompleted()) .forEach(result::add);
Java
복사
스트림 정렬 : sorted()
→ sorted(): default 정렬로 오름차순, 사전순으로 정렬
→ sorted(Comparator.reverseOrder()): 역순으로 정렬
Stream<String> stream1 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS"); Stream<String> stream2 = Stream.of("JAVA", "HTML", "JAVASCRIPT", "CSS"); stream1.sorted().forEach(s -> System.out.print(s + " ")); System.out.println(); // CSS HTML JAVA JAVASCRIPT stream2.sorted(Comparator.reverseOrder()).forEach(s -> System.out.print(s + " ")); // JAVASCRIPT JAVA HTML CSS
Java
복사
Comparator 인터페이스 사용
comparing(): 정렬에 사용되는 기본 메소드
→ comparing(Function<T, U> keyExtractor)
comparingInt, comparingLong, comparingDouble: 비교 대상이 기본 자료형일 경우 사용되며, 오토박싱과 언박싱이 없으므로 성능면에서 낫다
thenComparing: 정렬 조건을 추가할 때 사용
studentStream.sorted(Comparator.comparing(Student::getBan) .thenComparing(Student::getTotalScore) .thenComapring(Student::getName) .forEach(System.out::println);
Java
복사
스트림 필터링 : filter(), distinct()
→ fileter: 조건에 맞는 요소로 구성된 새로운 스트림 반환
→ distinct: 중복값 제거
스트림 제한 : limit(), skip()
→ limit: 스트림 요소 중 첫번째 요소부터 전달 받은 매개변수까지의 요소를 반환
→ skip: 스트림 요소 중 첫번째 요소부터 전달 받은 매개변수까지의 요소를 제외하고 반환
IntStream stream1 = IntStream.range(0, 10); stream3.skip(3).limit(5).forEach(n -> System.out.print(n + " ")); // 출력 결과: 3 4 5 6 7
Java
복사
스트림 연산 결과 확인 : peek()
→ 연산과 연산 사이의 결과 확인 시 사용
→ 디버깅 용도로 많이 사용함
IntStream stream = IntStream.of(7, 5, 5, 2, 1, 2, 3, 5, 4, 6); stream.peek(s -> System.out.println("원본 스트림 : " + s)) .skip(2) .peek(s -> System.out.println("skip(2) 실행 후 : " + s)) .limit(5) .peek(s -> System.out.println("limit(5) 실행 후 : " + s)) .sorted() .peek(s -> System.out.println("sorted() 실행 후 : " + s)) .forEach(n -> System.out.println(n));
Java
복사

라. 스트림 최종 연산(terminal operation)

최종 연산은 연산 결과가 스트림이 아닌 연산으로, 최종 연산 후 스트림은 재사용이 불가능하다
최종 연산의 핵심: 요소의 소모(reduce), 요소의 수집(collect)
reduce():
→ 스트림의 모든 요소를 순차적으로 소모하여 연산함
→ 초기값을 전달하면 초기값을 포함하여 연산
Java
복사
collect():
collect(), Collector, Collectors
→ collect(): 스트림의 최종연산으로 매개변수로 Collector를 필요함
→ Collector: 이 인터페이스를 구현해야 함
toList(), toSet(), toMap(), toCollect(), toArray()
→ toList, toSet: 해당 컬렉션으로 반환
→ toCollect: 특정 컬렉션으로 반환
List<String> names = stuStream.map(Student::getName) .collect(Collectors.toList()); ArrayList<String> list = names.stream() .collect(Collectors.toCollection(ArrayList::new));
Java
복사
→ toMap: 키와 쌍으로 초기화
요소의 출력 : forEach()
→ 내부적으로 추가적인 stream 연산 시 사용 가능
for (Category category : categories.keySet()) { if (Objects.equals(category.getId(), categoryId)){ categories.get(category).stream() .filter(task -> !task.isCompleted()) .forEach(result::add); } } // to categories.keySet().stream() .filter(category -> Objects.equals(category.getId(), categoryId)) .forEach(category -> categories.get(category).stream() .filter(task -> !task.isCompleted()) .forEach(result::add));
Java
복사
요소의 검색 : findFirst(), findAny()
요소의 검사 : anyMatch(), allMatch(), noneMatch()
→ 특정 조건을 만족하는 요소의 여부에 따라 boolean 값 반환
요소의 통계 : count(), min(), max()
→ 반환값을 Optional이 아니라 int로 받기 위해 getAsInt() 활용
int max = Arrays.stream(arr).max().getAsInt();
Java
복사
요소의 연산 : sum(), average()

마. 스트림의 변환

스트림 → 기본형 스트림
기본형 스트림 → 스트림
기본형 스트림 → 기본형 스트림
스트림 → 부분 스트림
두 개의 스트림 → 스트림
스트림의 스트림 → 스트림
스트림 → 병렬 스트림
스트림 → 컬렉션
컬렉션 → 스트림
스트림 → Map
스트림 → 배열

바. Opional 객체

Reference

자바의 정석, 남궁성
람다와 스트림 연습, https://developer-ek.tistory.com/20
람다를 통한 중복 제거, 왜 함수형 프로그래밍을 배워야 하는가, 자바지기 박재성