람다식

박주현·2022년 10월 28일
0

국비 공부

목록 보기
23/44

람다식으로 인해 자바는 객체지향언어인 동시에 함수형 언어가 되었다. 람다식(Lambda expression)은 간단히 말해서 메서드를 하나의 '식(expression)'으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.
메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수(anonymous function)'라고도 한다.

// 기존 배열을 선언하고 할당
int[] arr = new int[5];
// 람다식을 이용한 배열의 선언과 할당
Arrays.setAll(arr, (i) -> (int)(Math.random() * 5) + 1);

모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 메서드를 호출할 수 있다. 그러나 람다식은 이 모든 과정없이 오직 람다식 자체만으로도 메서드 역할을 대신할 수 있다.
게다가 람다식은 메서드의 매개 변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있다. 람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것이다.

메서드와 함수의 차이?
전통적으로 프로그래밍에서 함수라는 이름은 수학에서 따온 것입니다. 
수학의 함수와 개념이 유사하기 때문이죠. 
그러나 객체지향개념에서는 함수(function)대신 객체의 행위나 동작을 의미하는 
메서드(method)라는 용어를 사용합니다. 메서드는 함수와 같은 의미이지만, 특정 클래스에 
반드시 속해야 한다는 제약이 있기 때문에 기존의 함수와 같은 의미의 다른 용어를 선택해서 
사용한 것입니다. 그러나 이제 다시 람다식을 통해 메서드가 하나의 독립적인 기능을 하기 때문에
함수라는 용어를 사용하게 되었습니다.

람다식 의미와 문법

특징
1. 메서드와 달리 이름이 없다.
2. 메서드와 달리 특정 클래스에 종속되지 않지만, 매개변수, 반환 타입, 본체를 가지며, 심지어 예외도 처리할 수 있다.
3. 메서드의 인수로 전달될 수도 있고 변수에 대입될 수 있다.
4. 익명 구현 객체와 달리 메서드의 핵심 부분만 포함한다.

람다식의 규칙

  • 선언부의 타입은 추론할 수 있으므로 타입을 생략해도 된다.
  • 매개변수가 하나 있다면 괄호를 생략해도 된다.
  • 실행문이 하나 있다면 중괄호와 세미콜론을 생략할 수 있다. 단, 실행문이 하나의 return문이면 return 키워드도 생략해야 한다.
public class Lambda2Demo {
	public static void main(String[] args) {
		Negative negative;
		Printable printable;

		negative = new Negative() {
			
			@Override
			public int neg(int x) {
				return -x;
			}
		};
		
		negative = (int x) -> {
			return -x;
		};

		negative = (x) -> {
			return -x;
		};

		negative = x -> {
			return -x;
		};

		negative = (x) -> -x;
		
		negative = x -> -x;

		printable = new Printable() {
			
			@Override
			public void print() {
				System.out.println("하이");
				
			}
		};
		
		printable = () -> {
			System.out.println("ㅎㅇ");
		};

		printable = () -> System.out.println("GD");

		printable.print();

	}

}

메서드 참조

interface Computable {
	int compute(int x, int y);
}

class Utils {
	int add(int a, int b) {
		return a + b;
	}
}

public class MethodRefDemo {

	public static void main(String[] args) {
//		Computable computable;

//		// 방법 1
//		computable = new Computable() {
//
//			@Override
//			public int compute(int x, int y) {
//				// TODO Auto-generated method stub
//				return new Utils().add(x, y);
//			}
//		};
//
		Utils utils = new Utils();
//
//		// 방법 2
//		computable = (x, y) -> utils.add(x, y);

		// 방법 3
		Computable computable = utils::add;

		System.out.println(computable.compute(5, 5));

	}
}

함수형 인터페이스

Annotation : @FunctionalInterface
자바에서 모든 메서드는 클래스 내에 포함되어야 하는데, 람다식은 어떤 클래스에 포함되는 것일까? 지금까지 람다식이 메서드와 동등한 것처럼 느껴왔지만, 사실 람다식은 익명 클래스의 객체와 동등하다.

//람다식
(int a, int b) -> a > b ? a : b

// 익명 클래스의 객체
new Object() {
	int max(int a, int b) {
    	return a > b ? a : b;
    }
}

위의 람다식 코드에서는 이름 max는 임의로 붙인 것일 뿐 의미는 없다. 어쨌든 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을 것인가? 이미 알고 있는 것처럼 참조변수가 있어야 객체의 메서드를 호출할 수 있으니까 일단 이 익명 객체의 주소를 f라는 참조변수에 저장해 보자.

타입 f = (int a, int b) -> a > b ? a : b; // 참조변수의 타입을 뭘로 해야 할까?

참조변수 f의 타입은 어떤 것이어야 할까? 참조형이니까 클래스 또는 인터페이스가 가능하다. 그리고 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다. 그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문이다.

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

public class Java_bible {
	public static void main(String[] args) {
    	// 기존 익명 클래스의 객체로 사용 하던 방법.
		MyFunction f = new MyFunction() {

			@Override
			public int max(int a, int b) {
				return a > b ? a : b;
			}
		};
		int big = f.max(5, 3);
        
		// 람다식으로 익명 개체의 메서드를 호출
		MyFunction f1 = (a, b) -> a > b ? a : b;
		int big2 = f1.max(5, 3);
		System.out.println(big);
		System.out.println(big2);

	} // main의 끝

}

이처럼 MyFunction인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는, 람다식도 실제로는 익명 객체이고, MyFunction인터페이스를 구현한 익명 객체의 메서드 max()와 람다식 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문이다.

하나의 메서드가 선언된 인터피이스를 정의해서 람다식을 다루는 것은 기존의 자바의 규칙들을 어기지 않으면서도 자연스럽다. 그래서 인터페이스를 통해 람다식을 다루기로 결정되었으며, 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스(functional Interface)'라고 부르기로 했다.

단, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있다
그래야 람다식과 인터페이스의 메서드가 1:1로 연결될 수 있기 때문이다. 반면에 static메서드와 default메서드의 개수에는 제약이 없다.

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

함수형 인터페이스 MyFunction이 아래와 같이 정의되어 있을 때,

@FunctionalInterface
MyFunction {
	void myMethod();	// 추상 메서드
}

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

@FunctionalInterface
interface MyFunciton {
	void myMethod(); // 추상 메서드
}

public class Java_bible {
	public static void main(String[] args) {
    	// 참조변수를 사용하여 람다식 사용.
		MyFunciton f = () -> System.out.println("myMethod()");
		aMethod(f);
		
        // 참조변수 없이 직접 람다식을 매개변수로 지정.
		aMethod(() -> System.out.println("myMethod()"));

	} // main의 끝

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

}

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

MyFunction myMethod() {
	MyFunction f = () -> {};
    return f;		// 이줄과 윗 줄을 한 줄로 줄이면, return () -> {};
}

람다식의 타입과 형변환

함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다. 람다식은 익명 객체이고 익명 객체는 타입이 없다. 정확히는 타입은 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것이다. 그래서 대입 연산자의 양변의 타입을 일치시키기 위해 아래와 같이 형변환이 필요하다.

MyFunction f = (MyFunction) (() -> {});	// 양변의 타입이 다르므로 형변환이 필요

람다식은 MyFunction인터페이스를 직접 구현하지 않았지만, 이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다. 그리고 이 형변환은 생략 가능하다.
람다식은 이름이 없을 뿐 분명히 객체인데도, Object타입으로 형변환 할 수 없다. 람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.

@FunctionalInterface
interface MyFunction {
	void myMethod(); // publci abstract void myMethod();
}

public class Java_bible {

	public static void main(String[] args) {
		MyFunction f = () -> {
		};
		Object obj = (MyFunction) (() -> {
		});
		String str = ((Object) (MyFunction) (() -> {
		})).toString();

		System.out.println(f);
		System.out.println(obj);
		System.out.println(str);

//		System.out.println(() -> {}); // 에러. 람다식은 Object타입으로 형변환 안됨.
		System.out.println((MyFunction) (() -> {
		}));
//		System.out.println((MyFunction) (() -> {}).toString);	//에
		System.out.println(((Object) (MyFunction) (() -> {
		})).toString());
	} // main의 끝

}

출력

org.tutorials.java.days.tutorails.Java_bible$$Lambda$1/0x0000000800000bd8@4cc77c2e
org.tutorials.java.days.tutorails.Java_bible$$Lambda$2/0x0000000800000de0@2437c6dc
org.tutorials.java.days.tutorails.Java_bible$$Lambda$3/0x0000000800001000@12bb4df8
org.tutorials.java.days.tutorails.Java_bible$$Lambda$4/0x0000000800001208@e73f9ac
org.tutorials.java.days.tutorails.Java_bible$$Lambda$5/0x0000000800001410@7b1d7fff

출력을 보면, 컴파일러가 람다식의 타입을 어떤 형식으로 만들어내는지 알 수 있다. 일반적인 익명 객체라면, 객체의 타입이 '외부클래스이름$번호'와 같은 형식으로 타입이 결정되었을 텐데, 람다식의 타입은 '외부클래스이름$$Lambda$번호'와 같은 형식으로 되어 있는 것을 확인할 수 있다.

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

람다식도 익명 객체, 즉 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근하는 규칙은 앞서 익명 클래스에서 배운 것과 동일하다.

@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();
		}
	}
}

public class Java_bible {

	public static void main(String[] args) {
		Outer outer = new Outer();
		Outer.Inner inner = outer.new Inner();
		inner.method(100);

	} // main의 끝

}

위의 예제는 람다식 내에서 외부에 선언된 변수에 접근하는 방법을 보여준다. 람다식 내에서 참조하는 지역변수는 final이 붙지 않았어도 상수로 간주된다. 람다식 내에서 지역변수 i와 val을 참조하고 있으므로 람다식 내에서나 다른 어느 곳에서도 이 변수들의 값을 변경하는 일을 허용되지 않는다.
반면에 Inner 클래스와 Outer클래스의 인스턴스 변수인 this.val과 Outer.this.val은 상수로 간주되지 않으므로 값을 변경해도 된다.

0개의 댓글