Lambda

코코·2020년 8월 2일
0

Java

목록 보기
22/25
post-thumbnail

📚 자바의 정석을 정리한 내용입니다.

▪ 람다식Lambda Expression

자바는 두 번의 큰 변화가 있었다. JDK1.5의 지네릭스. JDK1.8의 람다.
람다의 등장으로 객체지향언어를 너머 함수형 언어가 가진 기능까지 갖추게 되었다.
말하자면 비로소 호모 사피엔스의 길에 접어든 것일까?

람다식이란 간단히 메서드를 하나의 식으로 표현한 것이다.
함수를 간략하면서도 명확한 식으로 편할 수 있다.

메서드를 람다식으로 표현하면 반환 값이 없어진다. 그래서 익명 함수라고 부르기도 한다.

그래서 어떻게 쓰는 거냐면.

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

두 값을 입력받고 큰 값을 반환하는 max라는 메서드가 있다.
이것을 람다로 변환하면 이렇게 쓸 수 있다.

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

//매개변수 타입이 같은 경우 타입을 생략할 수 있다.
(a,b) -> a > b ? a:b

이렇게 반환값이 있는 경우 return문을 식으로 대체할 수 있다. 연산 결과를 return한다.
문장이 아니라 '식expression'이므로 ';'을 붙이지 않는다.
만약, 매개변수가 하나라면 매개변수를 감싸는 괄호도 생략할 수 있다.

i -> i * i

메서드 -> 람다

MethodLambda
int max(int a, int b) {
return a > b ? a : b ;
}
(a,b)-> a > b ? a : b
void printVar(String name, int i) {
System.out.println(name + "="+i);
}
(name, i) -> System.out.println(name + "="+i)
int square(int x) { return x * x; }x-> x * x
int roll() { return (int)(Math.random()*6);}()->(int)(Math.random() * 6)
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;}
return sum;
}

▪ 함수형 인터페이스Functional Interface

람다는 익명 클래스 객체와 동등하다.

new Object() {
	int max(int a, int b) {
    	return a > b ? a : b;
    }
}

이 코드와

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

이 코드가 동등하다는 뜻이다.

그렇다면 람다로 정의된 익명 객체의 메서드를 어떻게 호출하려면 객체의 주소를 저장할 참조변수가 필요하다.

타입 f = (int a, int b) -> a > b ? a : b;

어떤 타입이 올 수 있을까?

조건

  • 람다식과 같은 메서드가 정의되어 있어야 한다.
    그래야만 참조변수를 이용해 람다식을 호출할 수 있다.
interface MyFuntion {
	public abstract int max(int a, int b);
}

이런 인터페이스가 있다고 하자. 이것을 익명클래스로도 구현한다면 이렇게 할 것이다.

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

위 익명클래스를 람다로 대체할 수 있다.

MyFunction f = (a, b) -> a > b ? a : b;	
int big = f.max(5,3);	//메서드 호출

익명객체를 람다로 대체 가능한 이유는 람다식도 실제로는 익명 객체이기 때문이다.
하나의 메서드가 선언된 인터페이스를 정의한 다음 람다를 다루는 것은 기존의 자바 규칙을 어기지 않으면서 자연스럽다.
그래서 인터페이스를 통해 람다를 다루기 시작했다. 이런 인터페이스를 함수형 인터페이스functional interface라고 한다. 인터페이스 선언부에 단순히 어노테이션 하나 추가하는 것 말고는 기존과 다르지 않다. 위 짧은 예제에서 보았듯이 없어도 상관은 없지만 컴파일러에서 확인해주므로 붙이는 것이 좋다.

@FunctionalInterface

  • 오직 하나의 추상메서드만 정의할 수 있다.
    인터페이스의 메서드와 람다를 1:1로 연결하기 위해서.

  • static메서드와 default메서드의 개수는 제약이 없다.

익명 클래스와 람다 비교

//익명 클래스
Collections.sort(list, new Comparator<String>() {
	public int compare(String s1, String s2) {
    	return s2.compareTo(s1);
    }
}

//람다
Collections.sort(list, (s1, s2) -> s2.compareTo(s1))

FunctionalInterface타입 매개변수와 반환타입

  • 메서드의 매개변수가 함수형 인터페이스타입이면, 호출 시 람다(또는 참조변수)를 인자로 입력해야 한다.

    @FunctionalInterface 
    interface MyFunction {
        void myMethod();	//추상메서드
    }
    
    ...
    void aMethod(MyFunction f) {
        f.myMethod();
    }
    -----------------------------------------
    //구현과 동시에 호출
    aMethod(() -> System.out.println("myMethod()"));	
  • 반환타입이 함수형 인터페이스타입이라면, 람다를 return한다.

    MyFunction myMethod() {
        //반환타입이 함수형 인터페이스라면, 람다를 return한다.
        return () -> System.out.println("myMethod()");
    }

♻ 람다식 타입과 형변환

람다의 타입이 함수형 인터페이스 타입과 일치하는 것은 아니다. 다만 함수형 인터페이스로 람다를 참조하는 것 뿐이다. 람다식은 익명 객체이므로 타입이 없다.

MyFunction f = (MyFunction)(() ->{}); //인터페이스 타입과, 람다 타입이 다르므로 형변환 필요

하지만 역시 컴파일러가 대신 해준다.
람다는 이름만 없을 뿐 객체다. 그러나 Object타입으로 형변환 할 수 없다.
굳이 하려면 먼저 MyFunction으로 형변환해야 한다.

Object obj = ((Object)(MyFunction))(()->{})).toString();

↗ 외부 변수를 참조하는 람다식

람다식도 익명 객체이므로 외부에 선언된 변수에 접근하는 방식은 익명클래스와 동일하다.

package com.javaex.lambda;

public class LambdaEx3 {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();

        inner.method(100);
    }
}


@FunctionalInterface
interface MyFunction{
    void myMethod();
}

class Outer {
    int val = 10;   //Outer.this.val

    class Inner {
        int val = 20;   //this.val

        void method(int i) {    //(final int i)
            int val = 30;   //final int val = 30;
//            i = 10;       //final이므로 변경 불가

            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();
        }
    }
}

람다식 내에서 참조하는 지역변수는 final이 붙지 않았어도 final로 간주한다.

📦 java.util.function

메서드 패턴은 대개 비슷하다.

  • 매개변수가 없거나, 하나 또는 두 개
  • return은 있거나 없거나
    제네릭을 사용하면 반환타입도 문제 되지 않는다.

java.util.package에는 자주 쓰이는 형식의 메서드들이 정의되어 있다. 늘 새로운 함수형 인터페이스를 쓰는 것보다 이 패키지의 인터페이스를 활용하는 것이 메서드 이름 통일, 재사용성, 유지보수 측면에서 모두 좋다.

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

이미지 출처 : 자바의 정석

4개의 함수형 인터페이스가 있다.

조건식에서 Predicate

Predicate는 Function의 변형이다. 반환값이 boolean이라는 점이 다르다. 수학에서 true 또는 false를 반환하는 함수를 Predicate라고 부른다. 조건식을 람다식으로 표현할 때 사용한다.

Predicate<String> isEmptyStr = s -> s.length() == 0;
String s = "";

if(isEmptyStr.test(s)) {	//if(s.length() == 0)
	....
}

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

이미지 출처 : 자바의 정석

BiSupplier가 없는 이유는 매개변수는 없고 반환값만 존재하는데 메서드는 두 개의 값을 반환할 수 없기 때문이다.

만약 이 외에 두 개 이상의 매개변수를 가지는 함수형 인터페이스가 필요하다면 직접 만들어 써야 한다.

@FunctionalInterface
interface TriFunction<T,U,V,R> {
	R apply(T t, U u, V v);
}

UnaryOperator와 BinaryOperator

Function의 또 다른 변형이다. 매개변수의 타입과 반환타입이 일치한다는 점이 다르다.

CollectionFramework & Functional Interface

컬렉션 프레임워크 인터페이스에 선언된 디폴트 메서드 중 일부는 함수형 인터페이스를 사용한다.

InterfaceMethod설명
Collectionboolean removeAll(Predicate<E> filter)조건에 맞는 요소 삭제
Listvoid replaceAll(UnaryOperator<E> operator)모든 요소를 변환하여 대체
Iterablevoid forEach(Consumer<T> action)모든 요소에 작업 action을 수행
MapV compute(K key, BiFunction<K,V,V> f)
V computeIfAbsent(K key, Function<K,V> f)
V computeIfPresent(K key Function<K,V> f)
V merge(K key, V value, BiFunction<V,V,V> f)
void forEach(BiConsumer<K,V> action)
void replaceAll(BiFunction<K,V,V> f)
지정한 키의 값에 작업 f를 수행
키가 없으면 작업 f 수행 후 추가
지정한 키가 있을 때 작업 f 수행
모든요소에 병합작업 f를 수행
모든 요소에 작업 action을 수행
모든 요소에 치환작업 f를 수행
package com.javaex.lambda;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class LambdaEx4 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        for (int i = 0; i < 10; i ++) {
            list.add(i);
        }

        //list의 모든 요소 출력
        list.forEach(i -> System.out.println(i));

        //list에서 2 또는 3의 배수 제거
        list.removeIf(x -> x%2==0 || x%3==0);   //removeIf(Predicate<E> filter)
        System.out.println(list);

        //list의 각 요소에 10을 곱한다.
        list.replaceAll(i -> i*10); //void replaceAll(UnaryOperator<E> operator)
        System.out.println(list);

        Map<String, String> map = new HashMap<>();
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
        map.put("4", "4");
        
        //void forEach(BiConsumer<K,V> action)
        map.forEach((k,v) -> System.out.println("["+k+","+v+"]"));
    }
}

그리고 앞에서 설명한 함수형 인터페이스를 사용하는 예제다.
쉬운 듯 헷갈려서 주석을 꼼꼼히 달았다.

package com.javaex.lambda;

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 LambdaEx5 {
    public static void main(String[] args) {
        /*Suplier<T>는 매개변수는 없고 반환값만 있는 함수형 인터페이스다.
        추상메서드 T get()가 1 ~ 100 사이 임의의 수를 리턴하도록 구현한다.*/
        Supplier<Integer> supplier = () -> (int)(Math.random()*100) + 1;

        /*Consumer<T>는 매개변수만 있고 반환값이 없는 함수형 인터페이스다.
        * void accept(T t)를 입력 받은 매개변수를 출력하도록 구현하였다.
        * */
        Consumer<Integer> consumer = i -> System.out.print(i + ",");

        /*
        * Predicate<T>는 하나의 매개변수를 입력 받고, boolean을 return하는 함수형 인터페이스다.
        * boolean test(T t)를 입력 받은 t가 2의 배수(i%2==0)라면 true 아니라면 false를 출력하도록 구현하였다.
        * */
        Predicate<Integer> predicate = i -> i%2==0;

        /*
        * Function<T,R>은 하나의 매개변수T를 받아 하나의 결과R를 출력한다.
        * R apply(T t)를 입력 받은 t의 일의 자리를 없애도록 구현하였다.
        * */
        Function<Integer, Integer> function = i -> i/10*10; //i의 일의 자리를 없앤다.

        List<Integer> list = new ArrayList<>();

        //list에 10개의 난수를 저장한다.
        makeRandomList(supplier, list);
        //확인
        System.out.println(list);

        //list에 저장된 값 중에, 2의 배수인 것만 출력한다
        printEvenNum(predicate, consumer, list);

        //list에 저장된 값들의 일의 자리를 없애서 List를 return하는 메서드
        List<Integer> newList = doSomething(function, list);
        System.out.println(newList);

    }

    static <T> List<T> doSomething(Function<T,T> f, List<T> list) {
        List<T> newList = new ArrayList<>();

        /*list에 저장된 값을 i에 대입한다.
        * i에 대입된 값에서 일의 자리를 없애고f.apply(i) newList에 저장한다.
        * */
        for (T i : list) { newList.add(f.apply(i)); }

        //list의 값들을 일의 자리만 없앤 채로 return
        return newList;
    }


    static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) {
        System.out.print("[");

        /*
        * 입력 list의 값을 하나씩 i에 대입한다.
        * 그 중에 2의 배수인 것만 Consumer<T> c에 담는다.
        * accept()는 매개변수 인자로 받은 i를 출력한다.
        * */
        for (T i : list ) {
            if (p.test(i)) {
                c.accept(i);
            }
        }
        System.out.println("]");
    }

    //입력받은 list에 난수 열 개를 저장한다.
    static <T> void makeRandomList(Supplier<T> s, List<T> list) {
        for (int i=0; i<10; i++) {
            /*
            * 반복문을 돌면서
            * s.get()을 호출할 때마다
            * 1 ~ 100 사이 임의의 수를 출력한다.
            * */
            list.add(s.get());
        }
    }
}

기본형을 사용하는 함수형 인터페이스

위 예제를 기본형을 사용하는 함수형 인터페이스로 바꾼 것이다.
주석은 위에 상세하게 달았으므로 생략한다.

package com.javaex.lambda;

import java.util.Arrays;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
import java.util.function.IntSupplier;
import java.util.function.IntUnaryOperator;

public class LambdaEx6 {
    public static void main(String[] args) {
        IntSupplier s = () -> (int)(Math.random() * 100) + 1;
        IntConsumer c = i -> System.out.print(i + ",");
        IntPredicate p = i -> i%2==0;
        IntUnaryOperator op = i -> i/10*10; //일의 자리 없앤다.

        int[] arr = new int[10];

        makeRandomList(s, arr);
        System.out.println(Arrays.toString(arr));
        printEvenNum(p, c, arr);
        int[] newArr = doSomething(op, arr);
        System.out.println(Arrays.toString(arr));
    }

    static void makeRandomList(IntSupplier s, int[] arr) {
        for (int i = 0; i<arr.length;i++){
            arr[i] = s.getAsInt();  //get이 아니라 getAsInt()
        }
    }
    static void printEvenNum (IntPredicate p, IntConsumer c, int[] arr) {
        System.out.print("[");
        for (int i : arr ) {
            if (p.test(i)) {
                c.accept(i);
            }
        } //for
        System.out.print("]");
        System.out.println();
    } // end printEvenNum

    static int[] doSomething(IntUnaryOperator op, int[] arr) {
        int[] newArr = new int[arr.length];

        for (int i = 0; i < newArr.length; i++) {
            newArr[i] = op.applyAsInt(arr[i]);  //apply()아님
        }

        return newArr;
    }


}

0개의 댓글