Java - lambda(람다) 간단한 사용법 !

2019. 3. 10. 16:57프로그래밍언어/Java&Servlet

Java - lambda(람다) 간단한 사용법 !


람다란 무엇일까? 람다(lambda)란 간단하게 표현하면 메서드에 전달 가능한 함수형 인터페이스의 구현체이다.

그럼 여기서 함수형 인터페이스란 무엇인가? 함수형 인터페이스는 하나의 메소드만 선언되어 있는 인터페이스를 함수형 인터페이스라고 부른다.

이것의 예제 소스코드를 보면,

1
2
3
4
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
cs

이것은 java.util.concurrent의 Callable 인터페이스이다. 이러한 인터페이스를 함수형 인터페이스라고 부른다.

(@FunctionalInterface 어노테이션을 붙이면 이 인터페이스가 함수형 인터페이스 조건에 어긋나면 예외가 발생)


여기서 이런 생각이 들 수 있다. 하나의 함수만 선언된 인터페이스가 무슨 소용인가..? 함수형 인터페이스는 무조건 우리가 매개변수로 전달한 람다식만 사용가능한 인터페이스인가? 아니다! 자바 1.8의 혁신적인 변화중 람다,스트림등등 이외에도 인터페이스 디폴트 메소드가 존재한다.


1
2
3
4
5
6
7
8
9
10
11
12
@FunctionalInterface
public interface Function<T, R> {
 
    R apply(T t);
 
    default <V> Function<V, R> compose(Function<super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
 
    ....
}
cs


위는 자바의 Function 인터페이스 코드의 일부를 발췌한 것이다. 위의 코드를 보면 이상한 생각이 들수도 있다.

분명 함수형 인터페이스는 하나의 메소드 선언만 갖는다고 했는데, 위의 인터페이스는 default라는 선언이 된 메소드를 갖고 있다.


사실 위에서 정의한 바를 정확히 얘기하면 함수형 인터페이스란 구현 해야될 메소드가 하나인 인터페이스를 말하는 것이다. 위의 default메소드의 기능이란 무엇인가 하면, 이전 인터페이스는 메소드 선언만 가능했다. 그런데 지금은 모든 인터페이스 구현체가 동일한 기능을 쓴다면 계속 반복해서 오버라이드하는 것이 아닌, 인터페이스에 구현 메소드를 아예 선언해 버리는 기능인 것이다. 이것이 default 메소드의 기능이다. 


그렇다면 위의 Function 인터페이스는 사용할때 구현해야하는 메소드를 하나만 갖는 함수형 인터페이스인 것이다.



Lambda(람다)에는 아주 다양한 함수형 인터페이스가 있다. 여기서는 몇가지의 함수형 인터페이스를 이용하여

사용법을 다루어볼 것이다.




BiConsumer<T,U>

Represents an operation that accepts two input arguments and returns no result.

BiFunction<T,U,R>

Represents a function that accepts two arguments and produces a result.

BinaryOperator<T>

Represents an operation upon two operands of the same type, producing a result of the same type as the operands.

BiPredicate<T,U>

Represents a predicate (boolean-valued function) of two arguments.

BooleanSupplier

Represents a supplier of boolean-valued results.

Consumer<T>

Represents an operation that accepts a single input argument and returns no result.

DoubleBinaryOperator

Represents an operation upon two double-valued operands and producing a double-valued result.

DoubleConsumer

Represents an operation that accepts a single double-valued argument and returns no result.

DoubleFunction<R>

Represents a function that accepts a double-valued argument and produces a result.

DoublePredicate

Represents a predicate (boolean-valued function) of one double-valued argument.

DoubleSupplier

Represents a supplier of double-valued results.

DoubleToIntFunction

Represents a function that accepts a double-valued argument and produces an int-valued result.

DoubleToLongFunction

Represents a function that accepts a double-valued argument and produces a long-valued result.

DoubleUnaryOperator

Represents an operation on a single double-valued operand that produces a double-valued result.

Function<T,R>

Represents a function that accepts one argument and produces a result.

IntBinaryOperator

Represents an operation upon two int-valued operands and producing an int-valued result.

IntConsumer

Represents an operation that accepts a single int-valued argument and returns no result.

IntFunction<R>

Represents a function that accepts an int-valued argument and produces a result.

IntPredicate

Represents a predicate (boolean-valued function) of one int-valued argument.

IntSupplier

Represents a supplier of int-valued results.

IntToDoubleFunction

Represents a function that accepts an int-valued argument and produces a double-valued result.

IntToLongFunction

Represents a function that accepts an int-valued argument and produces a long-valued result.

IntUnaryOperator

Represents an operation on a single int-valued operand that produces an int-valued result.

LongBinaryOperator

Represents an operation upon two long-valued operands and producing a long-valued result.

LongConsumer

Represents an operation that accepts a single long-valued argument and returns no result.

LongFunction<R>

Represents a function that accepts a long-valued argument and produces a result.

LongPredicate

Represents a predicate (boolean-valued function) of one long-valued argument.

LongSupplier

Represents a supplier of long-valued results.

LongToDoubleFunction

Represents a function that accepts a long-valued argument and produces a double-valued result.

LongToIntFunction

Represents a function that accepts a long-valued argument and produces an int-valued result.

LongUnaryOperator

Represents an operation on a single long-valued operand that produces a long-valued result.

ObjDoubleConsumer<T>

Represents an operation that accepts an object-valued and a double-valued argument, and returns no result.

ObjIntConsumer<T>

Represents an operation that accepts an object-valued and a int-valued argument, and returns no result.

ObjLongConsumer<T>

Represents an operation that accepts an object-valued and a long-valued argument, and returns no result.

Predicate<T>

Represents a predicate (boolean-valued function) of one argument.

Supplier<T>

Represents a supplier of results.

ToDoubleBiFunction<T,U>

Represents a function that accepts two arguments and produces a double-valued result.

ToDoubleFunction<T>

Represents a function that produces a double-valued result.

ToIntBiFunction<T,U>

Represents a function that accepts two arguments and produces an int-valued result.

ToIntFunction<T>

Represents a function that produces an int-valued result.

ToLongBiFunction<T,U>

Represents a function that accepts two arguments and produces a long-valued result.

ToLongFunction<T>

Represents a function that produces a long-valued result.

UnaryOperator<T>

Represents an operation on a single operand that produces a result of the same type as its operand.


위의 표는 자바 api가 제공하는 많은 함수형 인터페이스이다. 사실 위의 표 이외에도 Callable,Runnable 등의 많은 함수형 인터페이스가 존재한다.


이중 몇가지만 예제로 구현해 보았다.



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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
public class LambdaTest {
 
    public static void main(String[] args) {
        /*
         * Predicate
         * 문자열을 받아서 해당 문자열이 빈 문자열인지를 반환
         */
        String str = "asd";
        System.out.println(lambdaIsEqual(p->p.isEmpty(), str));
        
        /*
         * Cunsumer
         * 인수를 각각 +1 해줌.
         */
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        lambdaProcessParam( (Integer c)->{
                                        list.set(c-1,c+1);
                                        } , list);
        System.out.println(Arrays.toString(list.toArray()));
        
        /*
         * Function
         * 문자열 리스트를 받아서 각각의 문자열의 길이를 담은 리스트로 반환
         */
        List<String> list2 = Arrays.asList("ab","cds","ewwqd","a");
        List<Integer> result = lambdaConvertParam(list2, f->f.length());
        System.out.println(Arrays.toString(result.toArray()));
        
        /*
         * Supplier
         * 랜덤한 숫자를 반환한다.
         */
        System.out.println(lambdaSupplier(()->{
            return (int)(Math.random()*10)+1;
        }));
        
        /*
         * UnaryOperator
         * 숫자 리스트를 받아 각 숫자에 +2한 값을 담은 리스트로 반환
         */
        List<Integer> ele = Arrays.asList(1,2,3,4,5);
        List<Integer> result2 = lambdaUnaryOper(ele, uo->uo+2);
        System.out.println(Arrays.toString(result2.toArray()));
        
        /*
         * BinaryOperator
         * 두개의 문자열을 받아서 공백을 기준으로 두개의 문자열을 합하여 반환
         */
        String str1 = "yeoseong";
        String str2 = "yoon";
        System.out.println(lambdaBinaryOper(str1, str2, (bo1,bo2)->bo1+" "+bo2));
        
        /*
         * 하나의 정수와 문자열을 입력받아 정수와 문자열의 길이가 같은지 검사
         */
        System.out.println(lambdaBiPred(4"yoon", (bp1,bp2)->bp1 == bp2.length()));
        
        /*
         * 문자열 리스트에 인덱스한 숫자 번호를 붙여준다.
         */
        List<String> list3 = Arrays.asList("a","b","c","d");
        List<String> result4 = new ArrayList<>();
        lambdaBiConsumer(result4,list3, ()->(int)(Math.random()*10)+1, (bc1,bc2)->{
            bc1 = bc1.concat("-"+bc2+"");
            result4.add(bc1);
        });
        System.out.println(Arrays.toString(result4.toArray()));
        
        /*
         * lambda를 포함하면서 길이가 5이상인 문자열인가?
         */
        System.out.println(lambdaPredAnd("It's lambda", (String p1)->p1.contains("lambda"), (String p2)->p2.length()>5));
        
        /*
         * 숫자를 입력받아서 +1한 후에 *2를 수행한 숫자를 반환
         */
        System.out.println(lambdaFuncAndThen(1, f1->f1+1, f2->f2*2));
    }
    
    /**
     * boolean Predicate<T>
     * 하나의 인수를 받아서 적절한 로직을 처리한 후에 boolean을 반환한다.
     */
    public static <T> boolean lambdaIsEqual(Predicate<T> predicate,T t) {
        return predicate.test(t);
    }
    
    /**
     * void Consumer<T>
     * 하나의 인수를 받아서 인수를 소모한후 void를 반환.
     */
    public static <T> void lambdaProcessParam(Consumer<T> consumer,List<T> list) {
        list.forEach(e->consumer.accept(e));
    }
    
    /**
     * R Function<T>
     * <T,R>의 인수를 받아서 T타입을 R타입으로 변환후 반환한다.
     */
    public static <T,R> List<R> lambdaConvertParam(List<T> list, Function<T, R> f) {
        List<R> result = new ArrayList<>();
        list.forEach(c->{
            result.add(f.apply(c));
        });
        return result;
    }
    
    /**
     * T Supplier
     * 매개변수는 없고 T타입을 반환한다.
     * @return 
     */
    public static <T> T lambdaSupplier(Supplier<T> s) {
        return s.get();
    }
    /**
     * T타입의 매개변수를 받아 같은 타입의 T타입의 값을 반환
     */
    public static <T> List<T> lambdaUnaryOper(List<T> list,UnaryOperator<T> uo){
        List<T> result = new ArrayList<>();
        list.forEach(c->{
            result.add(uo.apply(c));
        });
        return result;
    }
    
    /**
     * T타입의 매개변수를 2개 받아서, T타입의 값으로 반환
     */
    public static <T> T lambdaBinaryOper(T a,T b,BinaryOperator<T> bo) {
        return bo.apply(a, b);
    }
    
    /**
     * T,R타입의 매개변수를 받아서 boolean 값을 반환
     */
    public static <T,R> boolean lambdaBiPred(T t,R r,BiPredicate<T, R> bp) {
        return bp.test(t, r);
    }
    
    /**
     * T,R타입의 매개변수를 받아서 적절한 처리를 한다. void 반환
     */
    public static <T,R> void lambdaBiConsumer(List<T> result,List<T> t , Supplier<R> r , BiConsumer<T, R> bc) {
        t.forEach(c->{
            bc.accept(c,r.get());
        });
    }
    
    /**
     * Predicate and/or/negate
     * 두개 이상의 Predicate를 and로 묶을 수 있다.
     */
    public static <T> boolean lambdaPredAnd(T t,Predicate<T> p1,Predicate<T> p2) {
        return p1.and(p2).test(t);
    }
    
    /**
     * Function andThen,compose
     * andThen a.andThen(b) a를 먼저 수행한 결과를 b의 함수의 입력으로 가져간다.
     * compose a.compose(b) b를 먼저 수행한 결과를 a의 함수의 입력으로 가져간다.
     */
    public static <T> T lambdaFuncAndThen(T t, Function<T, T> f1, Function<T, T> f2) {
        return f1.andThen(f2).apply(t);
    }
}
 
cs




이제는 람다에서 알아야할 몇가지 정보가 있다. 

1) 예외, 람다, 함수형 인터페이스의 관계 - 함수형 인터페이스는 확인된 예외를 던지는 동작을 허용하지 않는다. 즉, 예외를 던지는 람다 표현식을 만드려면 확인된 예외를 선언하는 함수형 인터페이스를 직접 정의하거나 람다를 try/catch 블록으로 감싸야한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FunctionalInterface
public interface Lamda{
    T process(T t) throws Exception;
}
 
 
Function<BufferedReader, String> f = 
    (BufferedReader b)-> {
        try{
            return b.readLine();
        }catch(IOException e){
            throw new RuntimeException(e);
        }
    };
cs


2)형식 추론 - 위의 예제를 보면 매개변수로 전달하는 람다의 인자에는 인자의 형식을 캐스팅하지 않는다. 인자의 형식은 선언된 메소드에서 내부적으로 추론해낸다.


3)람다의 조합 - Predicate(and,or,negate),Function(andThen,compose).... 람다식끼리 조합해서 더 제한적인 조건 혹은, 더 많은 처리를 파이프라인처럼

묶을 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    /**
     * Predicate and/or/negate
     * 두개 이상의 Predicate를 and로 묶을 수 있다.
     */
    public static <T> boolean lambdaPredAnd(T t,Predicate<T> p1,Predicate<T> p2) {
        return p1.and(p2).test(t);
    }
    
    /**
     * Function andThen,compose
     * andThen a.andThen(b) a를 먼저 수행한 결과를 b의 함수의 입력으로 가져간다.
     * compose a.compose(b) b를 먼저 수행한 결과를 a의 함수의 입력으로 가져간다.
     */
    public static <T> T lambdaFuncAndThen(T t, Function<T, T> f1, Function<T, T> f2) {
        return f1.andThen(f2).apply(t);
    }
cs


위의 예제에도 나왔지만 다시 한번 설명하기 위해 발췌했다. 이렇게 람다식끼리 조건부로 연결가능하다. 


이상 자바8의 람다에 대한 설명이었다. 사실 람다에 대해 설명하지 못한 것이 훨씬 많다. 내가 설명한 것은 람다의 일부일 것이다. 하지만 이번 포스팅에서는

람다란 무엇이고 어떤 식으로 접근하여 어떻게 사용할 것인가를 설명하기 위한 포스팅이었기에 이 글을 읽고 간단한 사용법을 익혀

나중에 응용해 나갔으면 하는 생각에 더 복잡하고 많은 설명을 하지 않았다.