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
•
람다를 통한 중복 제거, 왜 함수형 프로그래밍을 배워야 하는가, 자바지기 박재성