Home [자바의 정석] 14장 람다와 스트림(Lambda & Stream)
Post
Cancel

[자바의 정석] 14장 람다와 스트림(Lambda & Stream)

Java의 정석 - 14장 람다와 스트림

새로운것을 막 공부하는 것보다 기초부터 튼튼하게 하는것이 중요하다는 것을 뼈아프게 느낀 나…
1년전에 돌렸던 자바의 정석을 다시 호로록 돌리기로 결심하다..!

람다식(Lambda Expression)

람다식(Lambda Expression) : 람다식은 간단하게 말해서 메서드를 하나의 '식'으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확하게 표현할 수 있게 해준다.
메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로 람다식을 익명함수 라고도 한다.

1
2
int[] arr = new int[5];
Arrays.setAll(arr, (i)->(int)(Math.random()*5)+1);

람다식의 장점

  • 람다식을 사용하면 간결하고 이해하기 쉽다.
  • 모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 새로 만들어야 하지만, 람다식을 사용할 경우 이 과정을 생략할 수 있다. ( 람다식 자체만으로 이 메서드의 역할을 대신할 수 있다. )
  • 메서드를 변수처럼 다루는 것이 가능하다. ( 동작 파라미터화 )

람다식 작성하기

메서드를 람다식으로 만드는 방법을 알아보자.
메서드를 람다식으로 만드는 것은 간단하다. 메서드에서 이름이랑 반환타입을 제거하고, 매개변수 선언부와 함수 바디 사이에 ‘->’를 추가하기만 하면 된다.

1
2
3
int max(int a, int b){
    return a>b?a:b;
}

람다식으로 표현하면 다음과 같다.

1
2
3
(int a, int b) -> {
    return a>b?a:b;
}

반환값이 있는 메서드의 경우, return문 대신 식(expression)으로 대신할 수 있다. 식의 연산결과가 자동적으로 반환값이 된다. 이때는 문장(statement)이 아닌 식이므로 끝에 ‘;’를 불이지 않는다. 그리고 괄호{}, () 안에 문장이 한문장이거나, 매개변수가 하나만 존재하는 경우 괄호를 생략할 수 있다. 그러나 return이나 , 매개변수 타입이 있다면 괄호를 생략할 수 없다.

1
(int a , int b) -> a>b?a:b
1
a->a*a

그리고 람다식에 선언된 매개변수 타입은 대부분 추론이 가능하기 때문에 생략이 가능하다. 반환타입이 없는 이유도 항상 추론이 가능하기 때문이다.

1
(a,b) -> a>b?a:b

람다식의 예

다음은 메서드와 메서드를 람다식으로 변환한 예제들이다.

예제1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 메서드
int printVar(String name, int i){
    System.out.println(name+"i ");
}
// 람다식
// 방법 1.
(String name, int i)->{
    System.out.println(name+"i ");
}
// 방법 2.
(name, i) -> {
    System.out.println(name+"i ");
}
// 방법 3.
(name, i) -> System.out.println(name+"i ")

예제2.

1
2
3
4
5
6
7
8
9
10
11
//메서드
int square(int x){
    return x*x;
}
//람다식
// 방법 1.
(int x)->x*x
// 방법 2.
(x) ->x*x
// 방법 3.
x->x*x

예제3.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//메서드
int sumArr(int[] arr){
    int sum =0;
    for(int i: arr){
        sum+=i;
    }
    return sum
}
//람다식
(int[] arr)-> {
    int sum =0;
    for(int i: arr){
        sum+=i;
    }
    returm sum;
}

함수형 인터페이스

람다식은 어떤 클래스에 포함되는 것일까? 람다식은 익명 클래스의 객체와 동일하다. 원래 같은 경우에는 참조변수가 있어야 객체의 메서드를 호출할 수 있었다. 그렇다면 익명 객체의 주소응 어떤 타입의 참조변수로 지정해야 할까? 참조형이기 때문에 클래스나 인터페이스가 가능하다. 그리고 람다식과 동등한 메서드가 정의되어 있는 것이여야 한다. 그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이다.

다음 예제를 통해 함수형 인터페이스(Functional Interface)에 대해 알아보자

1
2
3
interface MyFunction{
    public abstract int mac(int a, int b)
}

이 인터페이스를 구현한 익명 클래스의 객체는 다음과 같이 생성할 수 있다.

1
2
3
4
5
6
MyFunction f = new MyFunction(){
    public int max(int a, int b){
        return a>b?a:b;
    }
};
int big = f.max(5,3);

그리고 익명객체를 다음과 같이 람다식으로 대체할 수 있다.

1
2
MyFunction f = (int a, int b)-> a>b?a:b
int big = f.max(5,3);

인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는, 람다식도 실제로 익명 객체이고, MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수 타입과 개수 그리고 반환값이 일치하기 때문이다. 따라서 인터페이스를 람다식으로 다루기로 결정하였으며, 람다식을 다루기 위한 인터페이스를 "함수형 인터페이스(Functional Interface)"라고 한다.

1
2
3
4
@FunctionalInterface
interface MyFunction{
    public abstract int max(int a, int b);
}

단, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다. 그래야 람다식과 인터페이스가 1:1로 연결될 수 있기 때문이다.

함수형 인터페이스 타입의 매개변수, 반환 타입

함수형 인터페이스 MyFunction이 아래와 같이 정의되어 있을때,

1
2
3
4
@FunctionalInterface
interface MyFunction{
    void myMethod();
}

메서드의 매개변수가 MyFunction타입이면, 이 메서드를 호출할 때 람다식을 참조하는 참조 변수를 매개변수로 지정해야 한다.

1
2
3
4
5
6
7
8
9
10
11
void requesstMethod(MyFunction f){
    f.myMethod();
}
...

//방법 1.
MyFunction f = () -> System.out.println("myMethod()");
requestMethod(f);
//방법 2.
requestMethod(()->System.out.println("myMethod()")); //참조 변수 없이 람다식을 매개변수로 지정

그리고 메서드의 반환타입이 함수형 인터페이스타입이라면, 이 함수형 인터페이스의 추상 메서드와 동등한 람다식을 가리키는 참조 변수를 반환하거나 람다식을 직접 반환할 수 있다.

1
2
3
MyFunction myMethd(){
    return ()->{};
}

람다식을 참조 변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고 받을 수 있다는 것을 의미한다. 즉, 변수처럼 메서드를 주고받는 것이 가능해진 것이다. 사실상 메서드가 아니라 객체를 주고 받는 것이기 때문에 근본적으로 달라진 것은 아무것도 없지만, 람다식 덕분에 예전보다 코드가 더 간결하고 이해하기 쉬워진 것이 분명하다.

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
32
33
@FunctionalInterface
interface MyFunction{
    void run();
}
class Main{
    static void execute(MyFunction f){
        f.run();
    }
    static MyFunction getMyFunction(){
        return () -> System.out.println("f3.run()");
    }
    public static void main(String[]args){
        //람다식으로 지정
        MyFunction f1 = () ->System.out.println("f1.run");
        //익명 클래스로 지정
        MyFunction f2 = new MyFunction(){
            public void run(){
                System.out.println("f2.run()");
            }
        };
        //getMyMethod 로 지정
        MyFunction f3 = getMyFunction();
        f1.run();
        f2.run();
        f3.run();

        execute(f1);
        //람다식 이용해서 execute 메서드 호출
        execute( () -> System.out.println("run" ));

    }
}

java.util.function 패키지

대부분의 메서드는 타입이 비슷하다. 그래서 java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았다. 매번 새로운 함수형 인터페이스를 정의하지 말고 가능하면 이 페키지의 인터페이스를 사용하는 것이 좋다. 자주 쓰이는 가장 기본적인 함수형 인터페이스는 다음과 같다.

java.util.function 패키지의 함수형 인터페이스는 크게 Consumer, Supplier, Function, Operator, Predicate로 구분된다. 구분 기준은 인터페이스에 선언된 추상 메소드의 매개값과 리턴값의 유무다.

함수형 인터페이스메서드실행
java.lang.Runnablevoid run()매개변수도 없고, 반환값도 없음
Supplier< T >T get()매개변수 없고, 반환값도 있음
Consumer< T >void accept(T t)매개변수 있고, 반환값은 없음
Function< T , R >R apply(T t)매개변수 있고, 반환값도 있음
Predicate < T >boolean test(T t)조건식을 표현하는데 사용함. 매개변수는 하나, 반환타입은 boolean

Predicate는 Function의 변형으로 반환 타입이 boolean 이라는 것만 제외하면 Function과 동일하다. Predicate는 조건식을 람다식으로 표현하는데 사용된다.

1
2
3
4
Predicate<String> isEmptyStr = s ->s.length ==0;
String s = " ";
if(isEmptyStr.test(s)) System.out.println("This is Empty Str");

매개변수가 두 개인 함수형 인터페이스

매개변수의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 “Bi”가 붙는다. |함수형 인터페이스|메서드|실행| |:–:|:–:|:–:| |BiConsumer<T,U>|void accept(T t, U u)|두 개의 매개변수만 있고, 반환값이 없음| |BiPredicate<T,U>|boolean test(T t, U u)|조건식을 표현하는데 사용됨. 매개변수는 둘, 반환값은 boolean| |BiFunction<T,U,R>|R apply (T t, U u)|두 개의 매개변수를 받아서 하나의 결과를 반환|

  • 두개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 직접 만들어서 써야한다. 만일 3개의 매개변수를 갖는 함수형 인터페이스를 선언한다면 다음과 같을 것이다.
    1
    2
    3
    4
    
    @FunctionalInterface
    interface TriFunction<T,U,V,R> {
      R apply(T t, U u, V v);
    }
    

UnaryOperator 와 BinaryOperator

Function의 또 다른 변형으로 UnaryOperator와 BinaryOperator가 있는데, 매개변수의 타입과 반환 타입의 타입이 모두 일치한다는 점만 제외하고는 Function과 같다.

함수형 인터페이스메서드실행
UnaryOperatorT apply(T t)Function의 자손, Function과 달리 매개변수와 결과의 타입이 같다.
BinaryOperatorT apply(T t, T t)BiFunction의 자손, BiFunction과 달리 매개변수와 결과의 타입이 같다.
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class Main {
    static <T> List<T> doSomthing(Function<T ,T> f, List<T> list){
        List<T> newList = new ArrayList<T>(list.size());
        for(T i :list){
            newList.add(f.apply(i));
        }
        return newList;
    }
    static <T> void printEventNum(Predicate<T> p, Consumer<T> c, List<T> list){
        System.out.print("[");
        for(T i: list){
            if(p.test(i)){
                c.accept(i);
            }
        }
        System.out.println("]");
    }
    static <T> void makeRandomList(Supplier<T> s, List<T> list){
        for(int i=0;i<10;i++){
            list.add(s.get());
        }
    }
    public static void main(String[] args) {

        Supplier<Integer> s = ()->(int)(Math.random()*100)+1;
        Consumer<Integer> c = i-> System.out.println(i+" ");
        Predicate<Integer> p = i->i%2==0;
        Function<Integer,Integer> f = i->i/10*10;

        List<Integer> list = new ArrayList<>();
        makeRandomList(s,list);
        System.out.println(list);
        printEventNum(p,c,list);
        List<Integer> newList = doSomthing(f,list);
        System.out.println(newList);
    }
}

Predicate의 결합

여러 Predicate를 and() or() negate() 로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.

  • predicate 끝에 negate()를 붙이면 조건식 전체가 부정이 된다.
1
2
3
4
5
6
7
8
9
10
11
Predicate<Integer>  p = i->i<100;
Predicate<Integer>  q = i->i<200;
Predicate<Integer>  r = i->i%2==0;
Predicate<Integer>  notP = p.negate();  //i>=100

//100<=i && (i<200 || i%2==0)
Predicate<Integer> all = notP.and(q.or(r));
System.out.println(all.test(150));

//람다식을 바로 넣어도 된다.
Predicate<Integer> all = notP.and(i->i<200).or(i->i%2==0);

그리고 static 메서드인 isEqual()은 두 대상을 비교하는 Predicate를 만들 대 사용한다. 먼저, isEqual()의 매개변수로 비교대상을 하나 지정하고, 또 다른 비교대상은 test()의 매개변수로 지정한다.

1
2
3
4
5
Predicate<String> p = Predicate.isEqual(str1);
boolean result = p.test(str2);

//위의 두 문장을 합친 것
boolean result = Predicate.isEqual(str1).test(str2);
1
2
3
4
5
6
7
8
9
10
11
12
import java.util.function.Predicate;

public class Main { 
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";

        Predicate<String> p2 = Predicate.isEqual(str1);
        boolean result = p2.test(str2);
        System.out.println(result);
    }
}

컬렉션 프레임웤과 함수형 인터페이스

This post is licensed under CC BY 4.0 by the author.

[모던 자바 인 액션 ch2] 동작 파라미터화 코드 전달하기

[실전 스프링부트] 스프링부트 시작하기