Chapter 16 - 람다식

김태원·2023년 1월 26일
0
post-custom-banner

람다식이란?

자바는 함수형 프로그래밍을 위해 Java8부터 람다식(Lambda Expressions)을 지원한다. 람다식은 데이터 처리부에 제공되는 함수 역할을 하는 매개변수를 가진 중괄호 블록이다. 데이터 처리부는 람다식을 받아 매개변수에 데이터를 대입하고 중괄호를 실행시켜 처리한다.

함수형 프로그래밍(Functional Programming)이란 함수를 정의하고 이 함수를 데이터 처리부로 보내 데이터를 처리하는 기법을 말한다. 데이터 처리부는 데이터만 가지고 있을 뿐, 처리 방법이 정해져 있지 않아 외부에서 제공된 함수에 의존한다.

람다식: (매개변수, ...) -> { 처리 내용 }

자바는 람다식을 익명 구현 객체로 변환한다.다음과 같이 Calculable 인터페이스가 있다고 가정해보자.

public interface Calculable {
    // 추상 메소드
    void calculate(int x, int y);
}

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

new Calculable() {
    @Override
    public void calculate(int x, int y) { 처리 내용 }
}

이것을 람다식으로 표현하면 다음과 같다. 추상 메소드인 calculate()는 두 개의 매개변수를 가지므로 (x, y)로 표현되었고, 화살표 뒤에 calculate()의 실행 블록이 온다.

(x, y) -> { 처리 내용 };

람다식은 인터페이스의 익명 구현 객체이므로 인터페이스 타입의 매개변수에 대입될 수 있다.
예를 들어 다음과 같이 Calculable 매개변수를 가지고 있는 action() 메소드가 있다고 가정해보자.

public void action(Calculabe calculable) {
    int x = 10;
    int y = 4;
    caculable.calculate(x, y);  // 데이터를 제공하고 추상 메소드를 호출
}

action() 메소드를 호출할 때 매개값으로 다음과 같이 람다식을 제공할 수 있다.
action() 메소드에서 calculable.calculate(x, y)를 실행하면 람다식의 중괄호 블록이 실행되면서 데이터가 처리된다.
여기서 action() 메소드는 제공된 람다식을 이용해서 내부 데이터를 처리하는 처리부 역할을 한다.

action( (x, y) -> {
    int result = x + y;
    System.out.println(result);
});

다음 그림과 같이 람다식1과 2 중에서 어떤 람다식을 매개값으로 제공하느냐에 따라 계산 결과는 달라질 수 있다.

인터페이스의 익명 구현 객체를 람다식으로 표현하려면 인터펭,스가 단 하나의 추상 메소드만 가져야 한다.

인터페이스가 단 하나의 추상 메소드를 가질 때, 이를 함수영 인터페이스(Functional Interface)라고 한다.

인터페이스가 함수형 인터페이스임을 보장하기 위해서는 @FunctionalInterface 어노테이션을 붙이면 된다.
@FunctionalInterface를 붙이는 것은 선택사항이지만, 컴파일 과정에서 추상 메소드가 하나인지 검사하기 때문에 정확한 함수형 인터페이스를 작성할 수 있게 도와주는 역할을 한다.

매개변수가 없는 람다식

함수형 인터페이스의 추상 메소드에 매개변수가 없을 경우 람다식은 다음과 같이 작성할 수 있다.
실행문이 두 개 이상일 경우에는 중괄호를 생략할 수 없고, 하나일 경우에만 생략할 수 있다.

() -> {
    실행문;
    실행문;
}
() -> 실행문

매개변수가 있는 람다식

함수영 인터페이스의 추상 메소드에 매개변수가 잇을 경우 람다식은 다음과 같이 작성할 수 있다.
매개변수를 선언할 때 타입은 생략할 수 있고, 구체적인 타입 대신에 var를 사용할 수도 있다.
하지만 타입을 생략하고 작성하는 것이 일반적이다.


매개변수가 하나일 경우에는 괄호를 생략할 수도 있다. 이때는 타입 또는 var를 붙일 수 없다.

예제를 보자

package ch16.sec03;

@FunctionalInterface
public interface Workable {
	void work(String name, String job);
}
package ch16.sec03;

@FunctionalInterface
public interface Speakable {
	void speak(String content);
}
package ch16.sec03;

public class Person {
	public void action1(Workable workable) {
		workable.work("홍길동", "프로그래밍");
	}

	public void action2(Speakable speakable) {
		speakable.speak("안녕하세요");
	}
}
package ch16.sec03;

public class LambdaExample {
	public static void main(String[] args) {
		Person person = new Person();

		// 매개변수가 두 개일 경우
		person.action1((name, job) -> {
			System.out.print(name + "이 ");
			System.out.println(job + "을 합니다.");
		});
		person.action1((name, job) -> System.out.println(name + "이 " + job + "을 하지 않습니다."));
		
		// 매개변수가 한 개일 경우
		person.action2(word -> {
			System.out.print("\"" + word + "\"");
			System.out.println("라고 말합니다.");
		});
		person.action2(word -> System.out.println("\"" + word + "\"라고 외칩니다."));
	}
}

리턴값이 있는 람다식

함수형 인터페이스의 추상 메소드에 리턴값이 있을 경우 람다식은 다음과 같이 작성할 수 있다.
return 문 하나만 있을 경우에는 중괄호와 함께 return 키워드를 생략할 수 있다. 리턴값은 연산식 또는 리턴값 있는 메소드 호출로 대체할 수 있다.

(매개변수, ...) -> {
    실행문;
    return;
}
(매개변수, ...) -> return;
(매개변수, ...) ->;

예제를 보자

package ch16.sec04;

@FunctionalInterface
public interface Calcuable {
    double calc(double x, double y);
}
package ch16.sec04;

public class Person {
    public void action(Calcuable calcuable) {
        double result = calcuable.calc(10, 4);
        System.out.println("결과: " + result);
    }
}
package ch16.sec04;

public class LambdaExample {
    public static void main(String[] args) {
        Person person = new Person();

        // 실행문이 두 개 이상일 경우
        person.action((x, y) -> {
            double result = x + y;
            return result;
        });

        // 리턴문이 하나만 있을 경우(연산식)
        // person.action((x, y) -> {
        //  return (x + y);
        // });
        person.action((x, y) -> (x + y));

        // 리턴문이 하나만 있을 경우(메소드 호출)
        // person.action((x, y) -> {
        //  return sum(x, y);
        // });
        person.action((x, y) -> sum(x, y));
    }

    public static double sum(double x, double y) {
        return (x + y);
    }
}

메소드 참조

메소드 참조는 말 그대로 메소드를 참조해서 매개변수의 정보 및 리턴 타입을 알아내 람다식에서 불필요한 매개변수를 제거하는 것을 목적으로 한다.

예를 들어 두 개의 값을 받아 큰 수를 리턴하는 Math 클래수의 max() 정적 메소드를 호출하는 람다식은 다음과 같다.

(left, right) -> Math.max(left, right);

람다식은 단순히 두 개의 값을 Math.max() 메소드의 매개값으로 전달하는 역할만 하기 떄문에 다소 불편해 보인다.
이 경우에는 다음과 같이 메소드 참조를 이용하면 매우 깔끔하게 처리할 수 있다.

Math :: max;

정적 메소드와 메소드 참조

정적 메소드를 참조할 경우에는 클래스 이름 뒤에 :: 기호를 붙이고 정적 메소드 이름을 기술한다.

클래스 :: 메소드

인스턴스 메소드일 경우에는 먼저 객체를 생성한 다음 참조 변수 뒤에 :: 기호를 붙이고 인스턴스 메소드 이름을 기술한다.

참조변수 :: 메소드

예제를 보자

package ch16.sec05.exam01;

@FunctionalInterface
public interface Calcuable {
    double calc(double x, double y);
}
package ch16.sec05.exam01;

public class Person {
    public void action(Calcuable calcuable) {
        double result = calcuable.calc(10, 4);
        System.out.println("결과: " + result);
    }
}
package ch16.sec05.exam01;

public class Computer {
    public static double staticMethod(double x, double y) {
        return x + y;
    }

    public double instanceMethod(double x, double y) {
        return x * y;
    }
}
package ch16.sec05.exam01;

public class MethodReferenceExample {
    public static void main(String[] args) {
        Person person = new Person();

        // 정적 메소드일 경우
        // 람다식
        // person.action((x, y) -> Computer.staticMethod(x, y));
        // 메소드 참조
        person.action(Computer::staticMethod);

        // 인스턴스 메소드일 경우
        Computer com = new Computer();
        // 람다식
        // person.action((x, y) -> com.instanceMethod(x, y));
        // 메소드 참조
        person.action(com::instanceMethod);
    }
}
결과: 14.0
결과: 40.0

매개변수의 메소드 참조

다음과 같이 람다식에서 제공되는 a 매개변수의 메소드를 호출해서 b 매개변수를 매개값으로 사용하는 경우도 있다.

(a, b) -> { a.instanceMethod(b); }

이것을 메소드 참조로 표현하면 다음과 같다. a의 클래스 이름 뒤에 :: 기호를 붙이고 메소드 이름을 기술한다.
작성 방법은 정적 메소드 참조와 동일하지만, a의 인스턴스 메소드가 사용된다는 점에서 다르다.

클래스 :: instanceMethod

예제를 보자.

package ch16.sec05.exam02;

@FunctionalInterface
public interface Comparable {
    int compare(String a, String b);
}
package ch16.sec05.exam02;

public class Person {
    public void ordering(Comparable comparable) {
        String a = "홍길동";
        String b = "김길동";

        int result = comparable.compare(a, b);

        if (result < 0) {
            System.out.println(a + "은 " + b + "보다 앞에 옵니다.");
        } else if (result == 0) {
            System.out.println(a + "은 " + b + "과 같습니다.");
        } else {
            System.out.println(a + "은 " + b + "보다 뒤에 옵니다.");
        }
    }
}
package ch16.sec05.exam02;

public class MethodReferenceExample {
    public static void main(String[] args) {
        Person person = new Person();
        person.ordering(String::compareToIgnoreCase);
    }
}
홍길동은 김길동보다 뒤에 옵니다.

생성자 참조

생성자를 참조한다는 것은 객체를 생성하는 것을 의미한다. 람다식이 단순히 객체를 생성하고 리턴하도록 구성된다면 람다식을 생성자 참조로 대치할 수 있다.

다음 코드를 보면 람다식은 단순히 객체를 생성한 후 리턴만 한다.

(a, b) -> { return new 클래스(a, b); }

이것을 생성자 참조로 표현하면 다음과 같다. 클래스 이름 뒤에 :: 기호를 붙이고 new 연산자를 기술하면 된다.

클래스 :: new

생성자가 오버로딩되어 여러 개가 있을 경우, 컴파일러는 함수형 인터페이스의 추상 메소드와 동일한 매개변수 타입과 개수를 가지고 있는 생성자를 찾아 실행한다.
만약 해당 생성자가 존재하지 않으면 컴파일 오류가 발생한다.
예제를 보자.

package ch16.sec05.exam03;

@FunctionalInterface
public interface Creatable1 {
    Member create(String id);
}
package ch16.sec05.exam03;

@FunctionalInterface
public interface Creatable2 {
    Member create(String id, String name);
}
package ch16.sec05.exam03;

public class Member {
    private final String id;
    private String name;

    public Member(String id) {
        this.id = id;
        System.out.println("Member(String id)");
    }

    public Member(String id, String name) {
        this.id = id;
        this.name = name;
        System.out.println("Member(String id, String name)");
    }

    @Override
    public String toString() {
        String info = "{ id: " + id + ", name: " + name + " }";
        return info;
    }
}
package ch16.sec05.exam03;

public class Person {
    public Member getMember1(Creatable1 creatable) {
        String id = "winter";
        Member member = creatable.create(id);
        return member;
    }

    public Member getMember2(Creatable2 creatable) {
        String id = "winter";
        String name = "한겨울";
        Member member = creatable.create(id, name);
        return member;
    }
}
package ch16.sec05.exam03;

public class ConstructorReferenceExample {
    public static void main(String[] args) {
        Person person = new Person();

        Member m1 = person.getMember1(Member::new);
        System.out.println(m1);
        System.out.println();

        Member m2 = person.getMember2(Member::new);
        System.out.println(m2);
    }
}
Member(String id)
{ id: winter, name: null }

Member(String id, String name)
{ id: winter, name: 한겨울 }
profile
개발이 재밌어서 하는 Junior Backend Developer
post-custom-banner

0개의 댓글