본문 바로가기

JAVA

14장 람다와 스트림 21. 04. 30.

1. 람다식(Lambda expression)

람다식의 도입으로 인해, 자바는 객체지향언어인 동시에 함수형 언어가 됨.

1.1 람다식이란?

메서드를 하나의 '식'으로 표현한 것.
메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수'라고도 한다.

() -> (int)(Math.random() * 5) + 1

위와 같은 람다식을 메서드로 표현하면 아래와 같다.

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

메서드를 사용하려면 클래스도 만들고, 객체를 생성해야만 하는데, 람다식은 그 자체만으로 메서드의 역할을 할 수 있는 것이다.
또한, 람다식은 메서드의 매개변수로 전달되는 것이 가능하고, 메서드의 결과로 반환될 수도 있어서, 람다식 덕분에 메서드를 변수처럼 다루는 것이 가능해졌다!


 

1.2 람다식 작성하기

'익명함수'답게 메서드에서 반환타입과 이름을 제거하고, 매개변수 선언부와 몸통{} 사이에 에로우 '->'를 추가한다.

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

 

또한 return문 대신 '식'으로 대신할 수 있는데 식의 연산결과가 자동으로 반환값이 된다. 주의할 점은 '식'은 문장이 아니므로 끝에 ';'를 붙이지 않는다.

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

 

게다가 람다식에 선언된 매개변수의 타입이 추론이 가능한 경우라면 생략될 수 있다.

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

 

선언된 매개변수가 하나뿐인 경우에는 괄호()마저 생략할 수 있다. 단, 나입이 있는 매개변수는 괄호를 생략해서는 안 된다.

a -> a * a

 

마찬가지로 중괄호 {} 안의 문장이 하나일 때, 괄호를 생략할 수 있고 문장의 끝에 ';'를 붙이면 안 된다.

(String name, int i) -> System.Out.println(name + "=" + i)

 

💥 괄호 {} 안의 문장이 return문일 경우에는 괄호를 생략해서는 안 된다.

 

실습 예제

        List<String> list = Arrays.asList(new String[] {"고길동", "홍길동", "일길동", "========"});
        list.forEach((String s) -> {System.out.println(s);});
        list.forEach((String s) -> System.out.println(s));
        list.forEach((s) -> System.out.println(s));
        list.forEach(s -> System.out.println(s));

 


 

1.3 함수형 인터페이스

람다식은 익명 클래스의 객체와 동등하다. 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스'라고 부른다.

 

 

LamdaExPrev.java

package chap14;

import java.util.Arrays;
import java.util.List;

public class LamdaExPrev {
    public static void main(String[] args) {
        Object obj = new Object() {
            int max(int a, int b) {
                return a > b ? a : b;
            }
        };
        MyFunction mf = new MyFunction() {

            @Override
            public int max(int a, int b) {
                // TODO Auto-generated method stub
                return a > b ? a : b;
            }
        };
        MyFunction mf2 = (a, b) -> a > b ? a : b;


        System.out.println(mf.max(3, 5));
        System.out.println(mf2.max(5, 2));

        System.out.println(mf);
        System.out.println(mf2);
    }
}

interface MyFunction {
    int max(int a, int b);
}

 

🔷 인터페이스 MyFunction을 아래에 정의해 뒀기 때문에, 메인 메서드에서 인터페이스를 구현한 익명 클래스의 객체를 생성할 수 있다. MyFunction 객체를 생성하면 자동으로 익명클래스가 오버라이딩 된다.
🔷 mf2는 익명 객체를 람다식으로 간단하게 표현한 것이다.
🔷 interface 위에 @FunctionalInterface를 붙이면 더 확실하게 함수형 인터페이스임을 알려줄 수 있는데, 단 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의 되어 있어야 한다.

 

  • 함수형 인터페이스 타입의 매개변수와 반환타입
    메서드의 매개변수가 함수형 인터페이스 타입이면, 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야 한다.

LamdaEx01.java

package chap14.ex01;

@FunctionalInterface
interface MyFunction {
    void run();
}
public class LamdaEx01 {
    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() { // 익명클래스 

            @Override
            public void run() {
                System.out.println("f2.run()");
            }
        };

        MyFunction f3 = getMyFunction();

        f1.run();
        f2.run();
        f3.run();

        execute(f1);
        execute(() -> System.out.println("run()"));
    }
}

 

🔷 파라미터로 MyFunction 타입이 왔는데 다형성에 의해 MyFunction의 조상 클래스가 오면 된다. (다형성: 조상 클래서의 타입을 가진 참조변수에 자손 인스턴스가 들어갈 수 있는 것!)
🔷 f1은 람다식 형태, f2는 익명 클래스 형태.
🔷 자바스크립트의 콜백함수 같은 형태

 



  • 람다식의 타입과 형변환
    람다식은 익명 객체이고 익명 객체는 타입이 없다. 그래서 아래와 같이 형변환이 필요하다.
    MyFunction f = (MyFunction)(() -> {});
    람다식은 MyFunction 인터페이스를 직접 구현하지 않았지만, 이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 이와 같은 형변환이 허용되고, 이 형변환은 생략도 가능하다.
    💥 주의할 점은 람다식은 분명히 객체이지만, Object 타입으로 형변환 할 수 없다는 것이다. 람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.

 

LamdaEx02.java

package chap14.ex02;

interface MyFunction {
    void run();
}

public class LamdaEx02 {
    public static void main(String[] args) {
        MyFunction f = () -> {}; // 다형성 묵시적 형변환으로 (MyFunction)이 생략되어 있음.
        Object obj = (MyFunction)() -> {}; // Object는 뭐든 들어갈 수 있지만 단 하나, 람다식은 못 들어감.
        String str = ((MyFunction)() -> {}).toString(); // 투스트링은 오브젝트에 있는 메서드.

        System.out.println(f);
        System.out.println(obj);
        System.out.println(str);
        System.out.println((MyFunction)() -> {});
        System.out.println(((MyFunction)() -> {}).toString());
    }
}

 

 

  • 외부 변수를 참조하는 람다식
    익명클래스의 특징 중 하나는, 익명클래스가 갖는 특정 메서드의 파라미터는 상수여야만 한다는 것이다.

LamdaEx03.java

package chap14.ex03;

import chap14.ex03.Outer.Inner2;

@FunctionalInterface
interface MyFunction {
    void myMethod();
}

class Outer {
    int val = 10; // 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;

            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 끝

    static class Inner2 {

    }
} // Outer 끝
public class LamdaEx03 {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner(); // static 키워드가 안 붙어서 .으로 접근. 객체로부터 생성
        Inner2 inner2 = new Inner2(); // 클래스로부터 생성
        inner.method(100);
    }
}

 

 

🔷 람다식 내에서 외부에 선언된 변수에 접근하는 방법을 보여주는 예제다.
🔷 람다식 내에서 참조하는 지역변수는 final이 붙지 않아도 상수로 간주된다.
🔷 그렇기 때문에 람다식 내에서나 다른 곳에서 이 변수들의 값을 변경할 수 없다.
🔷 한편, Inner클래스와 Outer 클래스의 인스턴스 변수인 this.val과 Outer.vhis.val은 상수로 간주되지 않으므로 값을 변경할 수 있다.
🔷 클래스 앞에 static이 붙을 수 있는 것은 내부 클래스만 가능하다.