본문 바로가기
Language/Java

[Java] 람다식(Lambda expression)과 함수형 인터페이스(Functional Interface)

by 계범 2022. 3. 16.

람다식이란

람다식은 메서드를 하나의 '식(expression)'으로 표현한 것이다.

메서드를 람다식으로 표현하면 메서드의 이름과 반환타입이 없어지므로, '익명함수(anonymous function)'이라고도 한다.

 

 

int[] arr = new int[5];
Arrays.setAll(arr ,(i) -> (int)(Math.random()*5) +1);

위의 문장에서, () -> (int)(Math.random()*5) +1); 부분이 람다식이다.

 

이러한 람다식을 통해 보다 간결하고 이해하기 쉬워졌고, 메서드를 변수처럼 다루는 것이 가능해진다.

 

람다식 작성하기

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

// 람다
(매개변수 선언) -> { 문장들 }

반환 타입과 메서드이름을 없애고, 매개변수가 수행할 식이 들어간다.

 

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

// 람다(타입 추론 가능한 경우 생략 가능)
(int a , int b) -> a ? b : a : b;
(a , b) -> a ? b : a : b;

 

람다 규칙

  • 매개변수 타입 추론 가능할 시 생략 가능
  • 매개변수가 하나일 시 괄호() 생략 가능하나, 매개변수 앞에 타입이 붙어있을 시엔 생략불가
    • a -> a*a 가능, int a -> a*a 불가. (int a) -> a*a로 해야함.
  • 수행할 식이 하나일 경우 괄호{} 생략가능
  • {}안의 문장이 return문일 경우 생략 불가.

 

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

람다식은 익명 클래스의 객체와 동등하다.

만약, 람다식을 호출하고 싶으면 이 익명 객체의 주소를 참조변수에 저장해야한다.

 

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

그럼 참조타입을 줘야하는데, 이것을 함수형 인터페이스 타입으로 준다.

//함수형 인터페이스
interface MyFunction{
	public abstract int max(int a, int b);
}

// (1) 구현 익명 클래스 객체
MyFunction f = new Myfunction(){
	public int max(int a, int b){
    	return a > b ? a: b;
    }
};

// (2) 익명 객체를 람다식으로 대체
MyFunction f = (int a, int b) -> a > b ? a: b;

int big = f.max(5,3); // 익명 객체의 메서드를 호출

 

MyFunction인터페이스를 구현한 익명 객체를 람다식으로 대체 가능한 이유는,

람다식도 실제로는 익명 객체이고, MyFunction인터페이스를 구현한 익명 객체의 메서드 max()와

람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문이다.

 

하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서 자연스럽다. 그래서 인터페이스를 통해 람다식을 다루기로 결정되었으며,

람다식을 다루기 위한 인터페이스를 '함수형 인터페이스(functional interface)' 라고 부른다.

 

@FunctionalInterface
interface MyFunction{ // 함수형 인터페이스 MyFunction을 정의
	public abstract int max(int a, int b);
}
@FunctionalInterface 어노테이션을 붙이면, 함수형 인터페이스를 올바르게 정의하였는지 확인해준다.

 

함수형 인터페이스 규칙

  • 오직 하나의 추상 메서드만 정의되어 있어야함.
    • 그래야 람다식과 인터페이스의 메서드가 1:1 연결 가능
  • static메서드와 default메서드의 개수 제약 없음.

 

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

@FunctionalInterface
interface MyFunction{ // 함수형 인터페이스 MyFunction을 정의
	void myMethod(); // 추상 메서드
}

위와 같은 함수형 인터페이스가 정의되어있을 때,

 

메서드의 매개변수가 함수형 인터페이스 타입이면,

메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야한다는 뜻이다.

//함수형 인터페이스 매개변수 메서드 정의
void aMethod(MyFunction f){ // 매개변수 타입이 함수형 인터페이스
	f.myMethod(); // Myfunction에 정의된 메서드 호출
}


// 메서드 호출
Myfunction f = () - > System.out.println("myMethod()"); //람다식을 참조하는 참조변수 f
aMethod(f);

// 한번에 처리
aMethod( () -> System.out.println("myMethod()") );

 

메서드의 반환타입이 함수형 인터페이스 타입이면,

함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있다.

MyFunction myMethod(){
	MyFunction f = () -> {};
    return f;
    
    //위의 2줄 한줄요약
    // return () -> {};
}

 

람다식을 참조변수로 다룰 수 있는 것은 메서드를 통해 람다식을 주고 받을 수 있다는 것을 의미한다.

 

@FunctionalInterface
interface MyFunction {
	void run();  // public abstract void run();
}

class LambdaEx1 {
	static void execute(MyFunction f) { // 매개변수의 타입이 MyFunction인 메서드
		f.run();
	}

	static MyFunction getMyFunction() { // 반환 타입이 MyFunction인 메서드 
		MyFunction f = () -> System.out.println("f3.run()");
		return f;
	}

	public static void main(String[] args) {
		// 람다식으로 MyFunction의 run()을 구현
		MyFunction f1 = ()-> System.out.println("f1.run()");

		MyFunction f2 = new MyFunction() {  // 익명클래스로 run()을 구현
			public void run() {   // public을 반드시 붙여야 함
				System.out.println("f2.run()");
			}
		};

		MyFunction f3 = getMyFunction();

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

		execute(f1);
		execute( ()-> System.out.println("run()") );
	}
}
//결과
f1.run()
f2.run()
f3.run()
f1.run()
run()

 

람다식의 타입과 형변환

람다식은 익명 객체이고 익명 객체는 타입이 없다.

정확히는 타입은 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것이다.

그래서 함수형 인터페이스 타입으로 참조변수를 저장할 때, 함수형 인터페이스 타입으로 형변환이 필요하다.

형변환은 생략이 가능하므로, 실제적으로는 그냥 써도 무관하다.

MyFunction f = (MyFunction) ( () -> {} ); // (MyFunction) 생략가능

 

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

@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;
//			i = 10;      // 에러. 상수의 값을 변경할 수 없음.

			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클래스의 끝
} // Outer클래스의 끝

class LambdaEx3 {
	public static void main(String args[]) {
		Outer outer = new Outer();
		Outer.Inner inner = outer.new Inner();
		inner.method(100);
	}
}
//결과
 i :100
           val :30
      this.val :21
Outer.this.val :11

 

java.util.function패키지

자주 쓰이는 함수형 인터페이스

자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해놨다.

가능하면 이 패키지의 인터페이스를 활용하는 것이 좋다.

 

함수형 인터페이스 메서드 설 명
java.lang.Runnable void run() 매개변수도 없고, 반환값도 없음.
Supplier<T> T get() 매개변수는 없고, 반환값만 있음.
Consumer<T> void accept(T t) 매개변수만 있고, 반환값이 없음
Function<T,R> R apply(T t) 일반적인 함수. 하나의 매개변수를 받아서 결과를 반환
Predicate<T> boolean test(T t) 조건식을 표현하는데 사용됨. 매개변수는 하나, 반환 타입은 boolean
T는 Type, R은 Return Type을 뜻함.

 

예시

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

if(isEmptyStr.test(s)){
	System.out.println("This is an empty String.");
}

 

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

앞에 접두사 'Bi'가 붙음.

함수형 인터페이스 메서드 설 명
BiConsumer<T,U> void accept(T t, U u) 두개의 매개변수만 있고, 반환값 없음
BiPredicate<T,U> boolean test(T t, U u) 조건식을 표현하는 사용됨.
매개변수는 둘, 반환값은 boolean
BiFunction<T,U,R> R apply(T t, U u) 두 개의 매개변수를 받아서 하나의 결과를 반환

 

2개 이상 매개변수를 갖는 함수형 인터페이스는 만들어 써야한다.

 

UnaryOperator 와 BinaryOperator

함수형 인터페이스 메서드 설 명
UnaryOperator<T> T apply(T t) Function의 자손. Function과 달리 매개변수와 결과의 타입이 같다.
BinaryOperator<T> T apply(T t, T t) BiFunction의 자손. BiFunction과 달리 매개변수와 결과의 타입이 같다.

 

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

함수형 인터페이스 메서드 설 명
DoubleToIntFunction int applyAsInt(double d) AToBFunction은 입력이 A타입, 출력이 B타입
ToIntFunction<T> int applyAsInt(T value) ToBFunction은 출력이 B타입이다. 입력은 제네릭 타입
IntFunction<R> R apply(T t, U u) AFunction은 입력이 A타입이고 출력은 제네릭 타입
ObjintConsumer<T> void accept(T t, U u) ObjAFcuntion은 입력이 T,A타입이고 출력은 없다.

 

Function의 합성과 Predicate의 결합

추후 공부 예정

 

메서드의 참조

람다식이 하나의 메서드만 호출하는 경우,

'클래스이름::메서드이름' 또는 '참조변수::메서드이름' 형태로 메서드참조 방식으로 변경가능하다.

 

종류 람다 메서드 참조
static메서드 참조 (x) -> ClassName.method(x) ClassName::method
인스턴스메서드 참조 (obj, x) -> obj.method(x) ClassName::method
특정 객체 인스턴스메서드 참조 (x) -> obj.method(x) obj::method

 

Function<String, Integer> f = (String s) -> Integer.parseInt(s);
// 메서드 참조
Function<String, Integer> f = Integer::parseInt;


BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);
// 메서드 참조
BiFunction<String, String, Boolean> f = String::equals;

매개변수를 생략하고 나면 메서드만 남는데, 해당 메서드의 이름이 다른 클래스에도 존재할 수 있기 때문에,

앞에 클래스 이름을 붙여둔다.

 

 

이미 생성된 객체의 메서드를 람다식에서 사용한 경우에도 메서드 참조를 통해 간략하게 가능하다.

MyClass obj = new MyClass();
Function<String,Boolean> f = (x) -> obj.equals(x); // 람다식
Function<String,Boolean> f = obj::equals; // 메서드 참조

 

생성자도 가능하다.

() - > new MyClass();
MyClass::new;

x -> new int[x];
int[]::new;

 

참조

'Java의 정석' 책

댓글