1. 람다식
1) 람다식이란?
메서드의 이름과 반환값 없이, 하나의 식처럼 표현한 것으로 '익명 함수'라고도 한다.
int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int) (Math.random()*5 + 1)); // 람다식
/*
int max(int a, int b) {
return a > b ? a : b;
}
*/
(int a, int b) -> a > b ? a : b
(a, b) -> a > b ? a : b // 타입 추론이 가능한 경우는 타입 힌트를 생략 가능
a -> a*a // 매개변수가 하나인 경우는 괄호 생략 가능
(int a) -> a*a // 매개변수 타입이 있으면 괄호 생략 불가능
/*
(String name, int i) -> {
System.out.println(name+"="+i);
}
*/
(String name, int i) -> // 괄호안에 문장이 하나면 괄호 생략가능
System.out.println(name+"="+i)
(int a, int b) => { return a > b ? a : b; } // 괄호 안에 return 이면 괄호 생략 불가!
2) 함수형 인터페이스
자바에서 모든 메서드는 클래스 내에 포함되어야 하는데, 람다식은 익명 클래스의 객체와 동등하다.
(int a, int b) -> a > b ? a : b
/* 위와 아래가 동등한 표현 */
new Object() {
int max(int a, int b) { // max 이름은 의미가 없다.
return a > b ? a : b;
}
}
그러면 위와 같이 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을까? 일단 참조변수가 필요하니, 익명 객체의 주소를 f라는 참조변수에 저장해보자
타입 f = (int a, int b) -> a > b ? a : b; // 참조변수 타입??
참조형은 클래스와 인터페이스가 가능하다. 그리고 람다식과 동일한 메서드가 정의되어 있는 것이어야 한다. 즉 아래와 같이 할 수 있다.
interface MyFunction {
public abstract int max(int a, int b);
}
/*
MyFunction f = new MyFunction() {
public int max(int a, int b) {
return a > b ? a : b;
}
}; */
MyFunction f = (int a, int b) -> a > b ? a : b; // 익명 객체를 람다식으로 대체
int big = f.max(5, 3) // 익명 객체의 메서드를 호출
람다식도 실제로는 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메서드 max() 와 람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문에, 위처럼 사용하는 게 가능하다.
하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다르는 것은 기존의 자바의 규칙들을 어기지 않으면서도 자연스럽다. 그래서 인터페이스를 통해 람다식을 다루기로 결정하였으며, 람다식을 다루기 위한 인터페이스르 '함수형 인터페이스(functional interface)' 라고 부르기로 했다.
/* 이 어노테이션을 붙이면 컴파일러가 함수형 인터페이스를 올바르게
* 정의하였는지 확인해주므로, 꼭 붙이자 */
@FunctionalInterface
interface MyFunction { // 함수형 인터페이스 MyFunction을 정의
public abstract int max(int a, int b);
}
단, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어야 한다. 그래야 람다식과 인터페이스의 메서드가 1:1로 유일하게 연결될 수 있기 때문이다. 만면에 static 메서드와 default 메서드의 개수에는 제약이 없다.
매개변수로 함수형 인터페이스 사용
@FunctionalInterface
interface Myfunction {
void myMethod(); // 추상메서드
}
void someMethod(MyFunction f) {
f.myMethod();
}
// 익명 객체를 매개변수로 넘김!
// 익명 객체 추상 메소드와 [(1)리턴타입/(2)매개변수타입/(3)매개변수개수]가 동일하면 됨.
someMethod(() -> System.out.println("hello!));
반환타입으로 함수형 인터페이스 사용
MyFunction myMethod() {
MyFunction f = ()->{};
return f;
// return ()->{}; 가능
}
람다식의 타입과 형변환
익명 클래스의 타입은 컴파일 시에 컴파일러에 의해 '외부클래스명$숫자' 와 같은 형식으로 지정된다. 익명 클래스와 비슷한 람다 클래스는 '외부클래스명$Lambda$숫자'의 형식으로 생성된다. 즉 엄밀히 말하면 아래 코드에서 양변의 타입이 다르기 때문에 형변환을 해주어야한다.
MyFunction f = (MyFunction)(()->{});
MyFunction f = ()->{}; // 형변환 생략 가능
Object obj = (Object)(()->{}); // 에러, 함수형 인터페이스로만 형변환 가능
람다식은 MyFunction 인터페이스를 직접 구현하진 않았지만, 이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다. 생략가능하다.
외부 변수를 참조하는 람다식
람다식도 익명 객체이므로 람다식에서 외부에 선언된 변수에 접근하는 규칙은 익명클래스와 동일하다.
@FunctionalInterface
interface Myfunction {
void myMethod(); // 추상메서드
}
class Outer {
int val = 1; // Outer.this.val
class Inner {
int val = 20; // this.val
void method(int i) { // void method(final int i)
int val = 30; // final int val = 30;
// i = 10; // 에러, 상수 변경 불가
MyFunction f = () -> {
System.out.println(" i :" + i);
System.out.println(" val :" + val);
System.out.println(" this.val :" + this.val);
System.out.println("Outer.this.val :" + Outer.this.val);
};
f.myMethod();
}
} // Inner 끝
} Outer 끝
class LambdaEx {
public static void main(String args[]) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.method(100);
}
}
3) java.util.function
대부분은 메서드 타입이 비슷하다. 매개변수가 없거나 한 두개, 반환 값은 없거나 한개. 게다가 제네릭 메서드로 정의하면 매개변수나 반환 타입이 달라도 문제가 되지 않는다. 그래서 java.util.function 패키지에 일밪넉으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 정의해 놓았다. 매번 새로운 함수형 인터페이스를 정의하는 것보다, 이 패키지의 인터페이스를 활용하는 것이 유지 보수에 더 좋다.
기본 4가지의 함수형 인터페이스
함수형 인터페이스 | method | 설명 |
java.lang.Runnable | void run() | 매개변수, 반환 모두 없음 |
Supplier<T> | T get() | 매개변수 없음, 반환 T |
Comsumer<T> | void accept(T t) | 매개변수 T, 반환 없음 |
Function<T, R> | R apply(T t) | 매개변수 T, 반환 R |
Predicate<T> | boolean test(T t) | 매개변수 T, 반환 boolean |
Predicate<String> isEmptyStr = s -> s.length() == 0;
String s = "";
if (isEmptyStr.test(s))
//do something
매개변수가 두 개인 함수형 인터페이스
위에 정의된 기본형인 Comsumer<T>, Predicate<T>, Function<T>를 매개변수 2개 받도록 변경
함수형 인터페이스 | method | 설명 |
BiComsumer<T, U> | void accept(T t, U u) | 매개변수 2개, 반환 없음 |
BiPredicate<T, U> | boolean test(T t, U u) | 매개변수 2개, 반환 boolean |
BiFunction<T, U, R> | R apply(T t, U u) | 매개변수 2개, 반환 R |
UnaryOperator와 BinaryOperator
UnaryOperation와 BinaryOperation는 매개변수 2개, 반환타입을 갖는데 입력타입과 반환타입이 모두 동일한 경우이다.
함수형 인터페이스 | method | 설명 |
UnaryOperation<T> | T apply(T t) | 매개변수 1개로 반환타입과 동일 |
BinaryOperation<T> | T apply(T t, T t) | 매개변수 2개, 매개변수와 반환유형 동일 |
컬렉션 프레임워크
인터페이스 | 메서드 | 설명 |
Collection | boolean removeif(Predicate<E> filter) | 조건에 맞는 요소를 삭제 |
List | void replaceAll(UnaryOperator<E> opertator) | 모든 요소를 반환 값으로 대체 |
Iterable | void forEach(Consumer<T> action) | 모든 요소 순회하여 action 수행 |
Map | V compute(K key, BiFunction<K,V,V> f) | 지정된 키의 값에 작업 f 수행 |
V computeIfAbsent(K key, Function<K,V> f) | 키가 없으면, 작업 f수행 후 추가 | |
V computeIfPresent(K key, BiFunction<K,V,V> f) | 지정된 키가 있을 때, 작업 f 수행 | |
V merge(K key, V value, BiFunction<V,V,V> f) | 모든 요소에 병합작업 f 수행 | |
void forEach(BiConsumer<K,V> action) | 모든 요소에 작업 action 수행 | |
void replaceAll(BiFunction<K,V,V> f) | 모든 요소에 치환작업 f 수행 |
기본형을 사용
함수형 인터페이스 | method | 설명 |
DoubleToIntFunction | int applyAsInt(double d) | AToBFunction(A타입->B타입) |
ToIntFunction<T> | int applyAsInt(T value) | ToBFunction(제네릭->B타입) |
IntFunction<R> | R apply(int i) | AFunction(A타입->제네릭) |
ObjIntConsumer<T> | void accept(T t, int i) | ObjAConsumer(제네릭, A타입 ->) |
4) Function의 합성과 Predicate의 결합
Function 합성
java.util.function 패키지에는 마치 합성함수를 만드는 것처럼 두개의 Function을 합성하는 default 함수가 정의되어 있다. andthen과 compose로 두개의 종류가 있다. 두개의 함수는 단순히 역순 관계이다.
Function<String, Integer> f = (s) -> Integer.parseInt(s, 16); // 문자열->16진수
Function<Integer, String> g = (i) -> Integer.toBinarystring(i); // 숫자->2진 문자열
/* andThen */
Function<String, String> h = f.andThen(g); // 문자열 -> 16진수 -> 2진 문자열
System.out.println(h.apply("FF")) // "FF" -> 255 -> "11111111"
/* compose */
Function<Integer, Integer> h = f.compose(g); // 숫자 -> 2진 문자열 -> 16진수
System.out.println(h.apply(2)) // 2 -> "10" -> 16
Predicate의 결합
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i % 2 == 0;
Predicate<Integer> notP = p.negate();
Predicate<Integer> all = notP.and(q.or(r));
//Predicate<Integer> all = notP.and(i -> i < 200).or(i -> i%2 == 0);
System.out.println(all.test(150)); // true
5) 메서드 참조
람다식이 하나의 메서드만 호출하는 경우에는 '메서드 참조'라는 방법으로 아래와 같이 람다식을 간략히 할 수 있다. 람다식이 많이 생략되었지만 (1) 우변의 메서드 선언부와 (2) 좌변의 Function인터페이스 타입으로부터 컴파일러가 충분히 추론할 수 있다.
/*
Fucntion<String, Integer> f = new Object {
Integer wrappter(Sring s) { // 메서드 이름은 의미 없다.
return Interger.parseInte(s);
}
}*/
// Fucntion<String, Integer> f = (String s) -> Integer.parseInt(s);
Fucntion<String, Integer> f = Integer::parseInt; //메서드 참조
2. 스트림
1) 스트림이란?
스트림은 서로 다른 컬렉션을 같은 방식으로 다룰 수 있도록 만들어졌다. String[] 이나 List<String> 이나 둘 다 컬렉션을 순회하면서 데이터 처리하는 연산은 거의 비슷하지만, 결국 데이터 타입이 다르기 때문에 중복되는 의미의 코드를 반복할 수 밖에 없다는 단점이 있었다. 스트림은 다음과 같은 특징이 있다.
- 데이터 소스를 직접 변경하지 않는다. 읽기만한다.
- 일회용이다. 스트림을 한번 실행하면 결과를 다시 참조할 수 없다.
- 작업을 내부 반복으로 처리한다.
- 메소드 체이닝이 가능해서 중간연산과 최종연산으로 구분할 수 있다.
- 최종 연산이 수행되기 전까지는 중간연산이 수행되지 않는데 이를 지연된 연산이라고 한다.
- 스트림은 기본적으로 Stream<T> 이지만, 오토박싱 언박싱의 오버헤드를 줄이기 위해 IntStream 과 같은 형태를 지원한다.
- fork&join 프레임워크를 내부적으로 사용하는 병렬 스트림을 사용할 수 있다.
3) 스트림의 중간연산
- 자르기 : skip(), limit()
- 걸러내기 : filter(), distinct()
- 정렬 : sort()
- 변환 : map(), mapToInt(), mapToLong(), flatMap()
- 조회 : peek()
4) Optional<T>와 OptionalInt
5) 스트림의 최종 연산
- forEach()
- 조건검사: AllMatch(), anymatch(), noneMatch(), findFirst(), findAny()
- 통계: count(), sum(), average(), max(), min()
- 변환: reduce()
- 컬렉션으로 반환: collect() - 특정 컬렉션 타입이나, 통계함수, 그룹 및 분할 함수 등을 적용할 수 있다.
'Java > Java의 정석' 카테고리의 다른 글
[Java의 정석] 12. 제네릭스, 열거형, 어노테이션 (0) | 2022.04.04 |
---|---|
[Java의 정석] 11. Collections Framework (0) | 2022.03.23 |
[Java의 정석] 09. java.lang 패키지 (0) | 2022.03.17 |
[Java의 정석] 08. 예외 처리 (0) | 2022.03.17 |
[Java의 정석] 7.2 메소드 오버라이딩 시 예외 선언 (0) | 2022.03.14 |