본문 바로가기
JAVA/기본

람다(Lambda)

by 히포파타마스 2021. 11. 28.

람다(Lambda)

 

1. 람다식(Lambda Expression)

람다식은 간단히 말해서 메서드를 하나의 식으로 표현한 것이다.

 

람다식은 기본적으로 다음과 같이 작성된다.

 

[람다식 작성_기본]

람다식은 위의 예제와 같이 반환 타입과 메서드 이름을 제거하고 매개변수 선언부몸통( {} )  사이에 -> 를 추가한다.

 

이처럼 람다식은 메서드 이름이 없기 때문에 익명 함수라고도 불린다.

 

 

[람다식 작성_여러 형태]

// 전부 같은 기능을 의미하는 함수
int sum(int a, int b) {
    return a + b;
}

//람다식 기본
(int a, int b) -> {return a + b;}

//return 생략
(int a, int b) -> a + b

//매개변수 타입 생략
(a, b) -> a + b

람다식은 경우에  따라 여러 표현을 생략할 수 있다.

 

람다식의 괄호{} 안의 문장이 하나일 때는 괄호{}를 생략할 수 있다.

※ return문을 사용할 경우 괄호를 생략 할 수 없다.

 

반환 값이 있는 메서드의 경우, return문 대신 식(expression) 으로 대신할 수 있다. 다만 이때는 문장이 아닌 식이기 때문에 끝에 ; 를 붙이지 않는다.

 

람다식에서 매개변수의 타입은 추론 가능할 경우 생략할 수 있는데, 대부분의 경우 생략이 가능하다.

 

 

[람다식 작성_매개 변수 괄호 생략]

(a) -> a * a
        
//매개변수 괄호 생략        
a -> a * a

매개 변수가 하나뿐인 경우에는 괄호()를 생략할 수 있다.

 

 

 

 

2. 함수형 인터페이스(Functional Interface)

람다식은 매개변수로도 사용될 수 있고 반환값으로도 사용될 수 있다. 

이처럼 객체로써 취급되는 듯한 람다식은 어떤 클래스의 메서드로 사용되는 것일까?

답은 익명 클래스 다.

즉, 람다식은 익명 클래스 객체에서 메서드를 정의한 것으로, 아래의 람다식과 익명 클래스는 동등하다.

 

[람다식과 익명 클래스]

(int a, int b) -> a + b

//람다식과 동등한 익명 클래스
new Object() {
	int sum(int a, int b) {
		return a + b;       
		}
}

 

람다식과 익명클래스가 동등하다는 것은 람다식을 특정 메서드가 정의된 인터페이스 타입으로 참조할 수 있음을 의미한다.

 

[람다식의 참조형]

public class Main {
	public static void main(String[] args) {

		MyFunction myFunction = new MyFunction() {
			@Override
			public int sum(int a, int b) {
				return a + b;
			}
		};

		//위와 같은 표현
		MyFunction f = (a, b) -> a + b;

	}

	@FuntionalInterface
	interface MyFunction {
		int sum(int a, int b);
	}

위의 myFunction 이라는 변수는 MyFunction 인터페이스를 사용한 익명 클래스를 참조한다.

익명 클래스와 람다식은 동등하기 때문에 익명 클래스를 람다식으로 바꿀 수 있고 결과적으로 람다식은 조건에 맞는 메서드가 정의된 인터페이스를 참조한다.

 

이처럼 하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다룰 수 있으며, 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스(funtional interface)' 라고 부르기로 했다.

※ 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다(단, static, default는 제약 없음).

※ @FunctionalInterface 를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해준다.

 

 

 

■ 람다식의 타입과 형변환

함수형 인터페이스로 람다식을 참조할 수 있지만, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다.

람다식은 익명 객체인데 익명 객체에는 타입이 없기 때문이다.

이 때문에 대입 연산자의 양변의 타입을 일치시키기 위해서는 아래와 같은 형 변환이 필요하다.

 

[람다식 형 변환]

MyFunction f = (MyFunction) ((a, b) -> a + b);

이 형 변환은 생략 가능하다..

 

 

람다식은 분명 객체이지만 Object타입으로 형 변환할 수 없다.

람다식은 오직 함수형 인터페이스로만 형 변환이 가능하다.

굳이 Object타입으로 형 변환하려면, 먼저 함수형 인터페이스로 변환한 뒤 Object타입으로 변환해야 한다.

 

 

 

 

3. java.utill.function 패키지

람다식을 사용하기 위해서 매번 새로운 함수형 인터페이스를 정의하는 것은 굉장히 번거롭다.

그래서 java.util.function패키지에 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓았다.

 

자주 쓰이는 가장 기본적인 함수형 인터페이스는 다음과 같다.

 

[함수형 인터페이스_기본]

매개변수와 반환 값의 유무에 따라 크게 4개의 함수형 인터페이스가 정의되어 있다.

 

매개변수가 두 개인 함수형 인터페이스는 이름 앞에 접두사 'Bi'가 붙는다.

 

[함수형 인터페이스_Bi]

만약 두 개 이상의 매개변수를 갖는 함수형 인터페이스가 필요하다면 직접 만들어서 사용해야 한다.

 

Function의 또 다른 변형으로 UnaryOperator와 BinaryOperator가 있는데, 매개변수와 반환 타입의 타입이 모두 일치한다는 점만 제외하고는 Function과 같다.

 

[UnaryOperator&BinaryOperator]

 

 

 

■ 컬렉션 프레임웍과 함수형 인터페이스

컬렉션 프레임웍에는 함수형 인터페이스를 사용하는 디폴트 메서드가 다수 존재한다.

 

[컬렉션 프레임웍 메서드_함수형 인터페이스]

 

 

 

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

앞서 소개된 함수형 인터페이스는 매개변수와 반환 값의 타입이 모두 지네릭 타입이기 때문에, 기본형 타입의 값을 처리할 때도 래퍼(wrapper) 클래스를 사용해야 했다.

이는 비효율적이기 때문에 기본형을 사용하는 함수형 인터페이스가 제공된다.

 

[함수형 인터페이스_기본형]

※ 위 표의 메서드 부분은 혼란을 줄 수 있는 표기법으로 참조하지 않는 것을 권장함

 

 

[함수형 인터페이스 사용_기본형]

IntUnaryOperator f = a -> 2 * a;

Function f = a -> 2 * a  //a의 타입을 알 수 없으므로 연산불가

Function<Integer, Interger> f = a -> 2 * a; //오토박싱&언박싱으로 비효율

위 예제처럼 UnaryOperator에 Int를 붙인 IntUnaryOperator를 사용하면 매개변수와 반환 타입의 타입이 int라는 것을 의미한다.

 

IntUnaryOperator를 사용한 것처럼 Function만 사용하면 람다식 a의 타입을 알 수 없기 때문에 에러가 발생한다.

따라서, Function을 사용하려면 지네릭으로 타입을 지정해주어야 한다.

다만 이 경우 오토박싱&언박싱으로 IntUnaryOperator를 사용하는 것보다 비효율적이다.

※IntFunction은 있지만 IntToIntFunction은 없는데 IntUnaryOperator가 그 역할을 대신하기 때문이다.

 

 

 

■ 합성과 결합

java.utill.function패키지의 함수형 인터페이스에는 추상 메서드 외에도 디폴트 메서드와 static메서드가 정의되어있다.

 

[default&static method_Function&Predicate]

 

 

□ Function의 합성

Function은 andTen과 compose를 사용해서 두 함수를 합성할 수 있다.

 

예를 들어, 함수 f, g가 있을 때, f.andThen(g)는 함수 f를 먼저 적용하고 그다음에 함수 g를 적용한다.

f.compose(g)는 반대로 g를 먼저 적용하고 f를 적용한다.

 

[Function_andThen]

Function<String, Integer> f = s -> Integer.parseInt(s, 16);

Function<Integer, String> g = i -> Integer.toBinaryString(i);

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

위 예제의 h는 함수 f를 적용하고 g를 적용하는 람다식이다.

h는 String을 받아 숫자로 변환한 후, 2진 문자열로 변환시킨다.

합성된 람다식 h는 먼저 적용되는 함수 f의 매개변수 타입인 String을 받아 뒤에 적용되는 함수 g의 반환 타입 String을 반환한다.

 

 

Function의 identity()는 함수를 적용하기 이전과 이후가 동일한 '항등 함수'를 반환한다.

즉, Function.identity()는 람다식 'x -> x' 와 동등하다. 

 

 

□ Predicate 결합

여러 조건식을 논리 연산자인 &&(and), ||(or), !(not)으로 연결해서 하나의 식을 구성할 수 있는 것처럼, 여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다.

 

[Predicate 결합 예]

Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i%2 == 0;

//!p => i >= 100
Predicate<Integer> notP = p.negate();

//!p && (q || r)
Predicate<Integer> all = p.negate().and(q.or(r));

 

 

isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용한다.

 

[Predicate_isEqual()]

Predicate<String> p = Predicate.isEqual("hello");

//"hello"와 "hellow"가 같은지 비교하여 결과를 반환
boolean result = p.test("hellow");

isEqual()의 매개변수로 비교대상을 하나 지정하고, 또 다른 비교대상은 test()의 매개변수로 지정한다.

 

 

 

 

4. 메서드 참조

람다식이 하나의 메서드만 호출하는 경우에는 '메서드 참조(method reference)'라는 방법으로 람다식을 간략화할 수 있다.

 

[메서드 참조 예]

Fucntion<String, Integer> f = (String s) -> Integer.parseInt(s);

//같은 식
Fucntion<String, Integer> f = Integer::parseInt;

위 예시의 첫 람다식은 Integer.parseInt()를 호출해서 매개변수를 넘길 뿐이다.

 

이 경우 예시의 두 번째 식처럼 Integer.parseInt() 메서드를 Integer::parseInt와 같이 바로 호출할 수 있다.

기존 람다식에서 매개변수 부분이 생략되었지만 parseint메서드의 선언부, 또는 Function인터페이스에 지정된 지네릭 타입으로부터 매개변수의 타입을 추정할 수 있다.

 

다음과 같이 매개변수가 2개인 경우에도 메서드 참조를 사용해서 람다식을 간략화할 수 있다.

 

[메서드 참조 예_매개변수 두 개]

BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2)

//같은 식
BiFunction<String, String, Boolean> f = String::equals;

위 예시의 경우도 결국 String의 equals함수를 호출한 것이다.

따라서 String::equals와 같이 메서드 참조를 사용할 수 있다.

 

 

메서드 참조를 사용할 수 있는 경우가 한 가지 더 있는데, 이미 생성된 객체의 메서드를 람다식에서 사용하는 경우이다.

이 때는 클래스 이름 대신 그 객체의 참조 변수를 적어줘야 한다.

 

[메서드 참조 예_생성된 객체의 메서드 사용]

MyClass obj = new MyClass();

Function<String, Boolean> f = x -> obj.equals(x);

//같은 식
Fuction<String, Boolean> f = obj::equals;

 

'JAVA > 기본' 카테고리의 다른 글

스트림(Stream)  (0) 2021.12.08
Optional  (0) 2021.12.03
예외 처리(exception handling)  (0) 2021.08.02
#5 생성자(Constructor)  (0) 2021.04.28
#4 오버로딩(overloading)  (0) 2021.04.28

댓글