자바의 람다식에 대해 학습하세요.

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

람다식 사용법

람다식

  • JDK 1.8 부터 추가
  • 람다식의 도입으로 인해 자바는 객체지향언어인 동시에 함수형 언어가 되었다.
  • 메서드를 하나의 식으로 표현한 것
  • 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로,
    람다식을 익명 함수(anonymous function) 이라고도 한다.

람다식 작성하기

반환타입 메서드이름(매개변수 선언){
   ...
}

(매개변수 선언) -> {
   ...
}

int method() {
    return (int) ((Math.random() * 5) + 1);
}

()->(int)(Math.random() * 5) + 1;
  • 반환값이 있는 메서드의 경우
    return문 대신 식으로 표현한다.
    식의 연산결과가 자동적으로 반환값이 된다.
    끝에 ; 를 붙이지 않는다.
(int a, int b) -> a > b ? a : b
  • 선언된 매개변수의 타입은 추론이 가능한 경우 생략 가능하다.
(a, b) -> a > b ? a : b
  • 선언된 매개변수가 하나인 경우 괄호()를 생랼할 수 있다
    매개변수의 타입이 있다면 괄호()를 생략할 수 없다.
a -> a + a // 정상
int a -> a + a // 비정상
  • 괄호{} 안의 문장이 하나일 경우 괄호{}를 생략할 수 있다.
() -> System.out.Println("Test") // 가능
() -> {System.out.Println("Test")} // 가능

함수형 인터페이스

  • 람다식은 익명 클래스의 객체와 동등하다.
  • 람다식과 아래(익명 클래스의 객체 내부 메소드)는 같다.
  • 람다식은 익명 클래스의 객체 내부 메소드를 호출하는 것과 비슷하다.
  • 람다는 익명 내부 클래스와 다르다.
(int a , int b) -> a > b ? a : b

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

익명 내부 클래스 vs 람다식

  • 컴파일 시, 익명클래스는 클래스명$1, 클래스명$2 ... 같이 클래스 파일이 생성된다.

  • 익명 내부 클래스는 자신을 감싸는 클래스와 다른 Scope 이다.
    람다는 자신을 감싸는 클래스와 같은 Scope 이다.

자바에서 람다를 익명클래스로 컴파일 하지 않는 이유

  • java9 이전에 람다를 쓰기 위한 retrolambda 같은 라이브러리나, kotlin 같은 언어에서는
    컴파일 시점에 람다를 단순히 익명 클래스로 치환한다.
  • 하지만 익명 클래스로 람다를 사용할 경우 아래와 같은 문제가 발생할 수 있다.
    항상 새로운 인스턴스로 할당된다.
    람다식마다 클래스가 하나씩 생기게 된다.
  • 람다식을 사용한 소스의 바이트 코드에는, INVOKEDYNAMIC CALL → INDY 부분이 있다.
  • INDY가 호출되게되면 bootstrap 영역의 lambdafactory.metafactory()를 수행하게 된다.
    • lambdafactory.metafactory() : java runtime library의 표준화 method
    • 어떤 방법으로 객체를 생성할지 dynamically 를 결정한다.
      • 클래스를 새로 생성, 재사용, 프록시, 래퍼클래스 등등 성능향상을 위한 최적화된 방법을 사용하게 된다.
  • java.lang.invoke.CallSite 객체를 return 한다.
    • LambdaMetaFactory ~ 이렇게 되어 있는 곳의 끝에 CallSite 객체를 리턴하게 된다.
    • 해당 lambda의 lambda factory, MethodHandle을 멤버변수로 가지게 된다.
    • 람다가 변환되는 함수 인터페이스의 인터페이스를 반환한다.
    • 한번만 생성되고 재호출시 재사용이 가능하다.

람다식으로 정의된 익명 객체의 메서드를 호출하는 방법

  • 참조변수를 만든다.
    참조변수의 타입은 클래스 또는 인터페이스가 될 것이다.
  • 해당 클래스 또는 인터페이스는 람다식과 동등한 메서드가 정의되어 있어야 한다.
타입 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;
	}
}

int big = f.max(5, 3);

함수형 인터페이스의 등장

  • 하나의 메서드가 선언된 인터페이스를 정의하여
    람다식을 다루는 것은 기존 자바의 규칙을 어기지 않으면서 자연스럽다.
  • 때문에 인터페이스를 통해 람다식을 다루기로 결졍되었으며,
    람다식을 다루기 위한 인터페이스를 함수형 인터페이스 (funtional interface)라고 부르기로 했다.
@FunctionalInterface
interface MyFunction{
	public abstract int max(int a, int b);
}
  • 함수형 인터페이스에서는 오직 하나의 추상 메서드만 정의되어 있어야 한다.
  • 다만 static, default 메서드의 개수에는 제약이 없다.

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

    @FunctionalInterface
    interface MyFunction {
        void myMethod();    // 추상 메서드
    }
  • 메서드의 매개변수가 함수형 인터페이스 MyFunction인 경우
    이 메서드 호출 시 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다.
    void aMethod(MyFunction f) {  
        f.myMethod();             // MyFunction에 정의된 메서드 호출
    }
 //1
 MyFunction f = () -> System.out.println("myMethod()");
 aMethod(f);
 
 //2
 aMethod(() -> System.out.println("myMethod()"));
  • 메서드의 반환타입이 함수형 인터페이스 타입이라면,
    이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나
    람다식을 직접 반환할 수 있다.
    MyFunction myMethod() {
        MyFunction f = () -> {};
        //1
        return f;
        //2
        return () -> {};
    }
  • 메서드를 변수처럼 주고받을 수 있다.

람다식의 타입과 형변환

  • 람다식은 익명 객체이고 익명 객체는 타입이 없다.
    대입 연산자의 양변의 타입을 일치시키기 위하여 형변환이 필요하다
  • () -> {}  람다식은 MyFunction 인터페이스를 직접 구현하지는 않았지만,
    인터페이스를 구현한 클래스 객체와 동일하기에 아래와 같은 형변환을 허용한다.
@FunctionalInterface
public interface MyFunction {
    void run();
}

public class MyFunctionTest {
    public static void main(String[] args) {
        MyFunction f = () -> {}; // 형변환 생략 가능 - 정상
        MyFunction f1 = (MyFunction) (() -> {}); // 정상
 
        Object obj = (Object) (() -> {}); // 에러
        Object obj1 = (Object)(MyFunction) (() -> {}); // 정상
        String str = ((Object)(MyFunction) (() -> {})).toString(); // 정상
    }
}
  • 오직 함수형 인터페이스로만 형변환이 가능하다.
  • Object 타입으로는 형변환 할 수 없다.
    굳이 Object 타입으로 형변환 하려면, 먼저 함수형 인터페이스로 변환해야 한다.

Variable Capture

Lambda Capturing (람다 캡쳐링)

  • 람다식에서 외부 지역변수를 참조하는 행위
  • 람다에서 접근 가능한 변수는 세가지 종류가 있다.
    1. 지역 변수
    2. static 변수
    3. 인스턴스 변수
  • 지역변수는 변경이 불가능하며, 나머지 변수들은 읽고 쓰기가 가능하다.
  • 람다는 지역변수가 존재하는 스택에 직접 접근하지 않고,
    지역 변수를 자신(람다가 동작하는 쓰레드)의 스택에 복사한다.
    각 쓰레드마다 고유한 스택을 가지고 있어서 지역 변수가 존재하는 쓰레드가 사라져도 람다는 복사된 값을 참조하여 에러가 발생하지 않는다.
  • 멀티 쓰레드 환경이라면, 여러 개의 쓰레드에서 람다식을 사용하며 람다 캡쳐링이 발생하게 된다.
  • 이 때 외부변수 값의 불변성을 보장하지 못하여 동기화 문제가 발생한다.
  • 이러한 문제로 지역변수는 final, Effectively Final 제약조건을 갖게 된다.
  • 인스턴스 / static 변수는 힙 영역에 위치하고, 힙 영역은 모든 쓰레드가 공유하는 메모리 영역이므로 동기화 문제가 일어나지 않는다.
@FunctionalInterface
public interface MyFunction {
    void run();
}
class Outer {
    int val = 10;
    static int staticVal = 0;

    class Inner {
        void method(int i) {
            int innerval = 30;

            MyFunction f = () -> {
                System.out.println(i);
                System.out.println(val);
                System.out.println(innerval);
                System.out.println(staticVal);
                System.out.println(this.getClass());

                val = 456;
                innerval = 123; // 에러, 지역변수는 final 제약조건을 갖는다. Variable used in lambda expression should be final or effectively final
                staticVal = 789;
            };
            f.run();
        }
    }

}
public class CaptureEx {
    public static void main(String[] args) {
        Outer.Inner testClass = new Outer().new Inner();
        testClass.method(100);
    }
}

// 100
// 10
// 30
// 0
// class lambdaex.Outer$Inner
  • 람다식 내에서 참조하는 지역변수는 final을 붙이지 않아도 상수로 간주된다.
  • 람다식에서 this는 내부적으로 생성되는 익명 객체의 참조가 아닌
    람다식을 실행한 객체의 참조이므로, this는 중첩 객체인 inner 이다.

java.util.function 패키지

  • java.util.function 패키지에 일반적으로 자주 쓰이는 형식의 메소드를
    함수형 인터페이스로 미리 정의해 놓았다.
  • 가능하면 이 패키지의 함수형 인터페이스를 사용하자.

기본적인 함수형 인터페이스

  • Function의 변형으로 Predicate가 있는데, 반환값이 boolean이 것만 제외하면 Function과 동일하다.


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


UnaryOperator, BinaryOperator

public class FuncInterfaceTest {
    public static void main(String[] args) {
        Supplier<Integer> s = () -> (int) (Math.random() * 100) + 1;
        Consumer<Integer> c = i -> System.out.print(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);
        printEvenNum(p,c,list);
        List<Integer> newList = doSomeTing(f,list);
        System.out.println(newList);
    }

    static <T> List<T> doSomeTing(Function<T,T> f, List<T> list){
        List<T> newList = new ArrayList<>(list.size());
        for (T t : list) {
            newList.add(f.apply(t));
        }
        return newList;
    }

    static <T> void printEvenNum(Predicate<T> p, Consumer<T> c, List<T> list) {
        System.out.print("[");
        for (T t : list) {
            if (p.test(t)) {
                c.accept(t);
            }
            System.out.println("]");
        }
    }

    static <T> void makeRandomList(Supplier<T> s, List<T> list) {
        for (int i=0; i < 10; i++){
            list.add(s.get());
        }
    }
}

// [48, 4, 25, 17, 42, 46, 85, 55, 47, 11]
// [48,4,42,46,]
// [40, 0, 20, 10, 40, 40, 80, 50, 40, 10]

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

public class FuncInterfaceTest1 {
    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 = doSomeTing(op,arr);
        System.out.println(Arrays.toString(newArr));
    }

    static int[] doSomeTing(IntUnaryOperator f, int[] arr){
        int[] newArr = new int[arr.length];
        for (int i=0;i<newArr.length;i++) {
            newArr[i] = f.applyAsInt(arr[i]);
        }
        return newArr;
    }

    static void printEvenNum(IntPredicate p, IntConsumer c, int[] arr) {
        System.out.print("[");
        for (int t : arr) {
            if (p.test(t)) {
                c.accept(t);
            }
        }
        System.out.println("]");
    }

    static void makeRandomList(IntSupplier s, int[] arr) {
        for (int i=0; i < arr.length; i++){
            arr[i] = s.getAsInt();
        }
    }
}

// [99, 27, 12, 21, 71, 84, 45, 11, 95, 18]
// [12,84,18,]
// [90, 20, 10, 20, 70, 80, 40, 10, 90, 10]

Function 함수형 인터페이스의 default, static 메서드

  • Default - andThen()
    • f.andThen(g)
    • 함수 f를 먼저 적용하고 그 다음에 함수 g를 적용
  • Default - compose()
    • f.compose(g)
    • andThen()과 반대 함수 g를 먼저 적용하고 그 다음 f를 적용
  • Static - identity()
    • 이전과 이후가 동일한 '항등함수'가 필요할 때 사용
    • x -> x, Function.identity() 두 개가 동일
    • 항등 함수는 잘 사용되지 않는 편이며, map()으로 변환작업할 때, 변환없이 그대로 처리하고자 할 때 사용

 


Predicate 함수형 인터페이스의 default, static 메서드

  • 여러 조건식들을 논리 연산자인 &&(and), ||(or), !(not) 으로 연결해서 하나의 식을 구성할 수 있는 것처럼, 여러 Predicateand(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.
  • static 메소드인 isEqual()두 대상을 비교하는 Predicate를 만들 때 사용한다.
public class PredicateExample {
    public static void main(String[] args) {
        Function<String, Integer> f = s -> Integer.parseInt(s, 16);
        Function<Integer, String> g = i -> Integer.toBinaryString(i);

        Function<String, String> h = f.andThen(g);
        Function<Integer, Integer> h2 = f.compose(g);

        System.out.println(h.apply("EE"));
        System.out.println(h2.apply(2));

        Function<String, String> f2 = x -> x;
        System.out.println(f2.apply("AAA"));

        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));
        System.out.println(all.test(150));

        String str1 = "ABC";
        String str2 = "abc";

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

// 11101110
// 16
// AAA
// true
// false

메소드, 생성자 레퍼런스

메소드 참조 (method reference)

  • 메소드를 참조하는 방법
    1. 클래스이름::메소드이름
    2. 참조변수::메소드이름
Function<String, Integer> f = s -> Integer.parseInt(s);

// 메소드 참조
Function<String, Integer> f = Integer::parseInt;
BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equlas(s2);

BiFunction<String, String, Boolean> f = String::equals;
종류 람다 메소드 참조
static 메소드 참조 x -> ClassName.method(x) ClassName::method
인스턴스 메소드 참조 (obj, x) -> obj.method(x) ClassName::method
특정 객체 인스턴스 메소드 참조 x -> obj.method(x) obj:method

생성자의 메소드 참조

Supplier<MyClass> s = () -> new MyClass();      //  람다식
Supplier<MyClass> s = MyClass:new;              //  메소드 참조

// 매개변수가 있는 경우
Function<Integer, MyClass> f = i -> new MyClasS(i);     //  람다식
Function<Integer, MyClass> f = MyClass:new;             //  메소드 참조

BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s);  //  람다식
BiFunction<Integer, String, MyClass> bf = MyClass::new;                 //  메소드 참조

// 배열을 생성하는 방법
Function<Integer, int[]> f = x -> new int[x];   //  람다
Function<Integer, int[]> f2 = int[]::new;       //  메소드 참조
class Myclass{
    int iv;

    public Myclass() {
    }

    public Myclass(int iv) {
        this.iv = iv;
    }
}
public class ReferenceExample {
    public static void main(String[] args) {
        Function<String, Integer> f = s -> Integer.parseInt(s);
        Function<String, Integer> f1 = Integer::parseInt;
        System.out.println(f1.apply("100") + 100);

        Supplier<Myclass> s = () -> new Myclass();
        Supplier<Myclass> s1 = Myclass::new;
        System.out.println(s1.get());

        Function<Integer, Myclass> f2 = Myclass::new;
        Myclass m = f2.apply(100);
        System.out.println(m.iv);
        System.out.println(f2.apply(200).iv);

        Function<Integer, int[]> f3 = int[]::new;
        System.out.println(f3.apply(10).length);
    };
}

// 200
// lambdaex.Myclass@5e9f23b4
// 100
// 200
// 10

 

'Back-end > java' 카테고리의 다른 글

스터디 14주차 - 제네릭  (0) 2021.07.08
스터디 13주차 - I/O  (0) 2021.07.07
스터디 12주차 - 어노테이션  (0) 2021.07.06
스터디 11주차 - Enum  (0) 2021.07.05
스터디 10주차 - 멀티쓰레드 프로그래밍  (0) 2021.07.02

+ Recent posts