이 글의 내용은 다음 내용을 참조하여 작성되었습니다.
람다식의 도입으로 인해 자바는 객체지향 언어인 동시에 함수형 언어가 되었다고 한다. 함수형 언어의 장점들을 자바에서도 누릴 수 있게 되었으니 람다식이라는 강력한 무기에 대해 알아보자
- 왜 람다식을 사용하나요? : 일부 메서드에 인자로 전달할 수 있는 익명 함수를 정의함으로써 함수 인터페이스를 구현하는 것이 가능하다.
- 함수형 프로그래밍 활성화
- 모든 JVM 기반 언어는 객체지향 프로그래밍을 통해 작업해야 했지만, 람다식의 등장으로 함수 코드 작성이 가능해졌다.
- 읽기 쉽고 간결한 코드
- 람다식 사용으로 엄청난 양의 코드가 제거된 것이 보고되었다.
- 사용하기 쉬운 API와 라이브러리
- 람다식을 사용하여 설계된 API는 다른 API를 더 쉽게 사용하고 지원 가능하다.
- 병렬 처리 지원
- 람다식은 오늘날 모든 프로세서가 멀티 코어 프로세서라고 한다.
따라서 병렬 처리를 지원한다.- 람다식을 사용함으로써 얻는 장점은 무엇인가요?:
- 간결하고 읽기 쉬운 코드.
그리고 람다 표현식으로 가변 관찰할 가능성(no potential for variable shadowing)이 없으며, 내부 클래스를 사용할 때 큰 단점(클래스 증가)을 제거한다.- 객체지향이 유연한 변경을 얻는 대신 객체 중첩이라는 단점을 감수했었는데, 그 단점을 제거했다고 이해함, 참조
- 람다식의 목적은 무엇인가요?
- 익명 메서드 작성에 사용
- 간결하고 기능적인 구문을 제공
- 함수형 프로그래밍 개념을 기반으로 하며
- [람다식 사용 시 주의점]
- 람다 표현식은 컴파일러의 약어를 제공하여 대리자(delegates)에게 할당된 메서드를 방출할 수 있도록 한다.
- 컴파일러는 람다 인자에 대한 자동 유형 추론을 수행하며, 이는 주요 이점이다.
람다식이란?
"메서드를 하나의 식(expression)으로 표현한 것"이라고 간단히 말할 수 있다. 그렇다면 그렇게 함으로써 해결되는 문제는 무엇일까?
- 메서드의 이름과 반환값이 없어지므로 람다식을 익명 함수(anonymous function)라고도 한다.
- 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.
int [] arr = new int[5];
Arrays.setAll(arr ,(i) -> (int)(Math.random() *5)+1);
위 람다식이 하는 일을 메서드로 표현하면 다음과 같다.
int method(){
return (int)(Math.random()*5) +1;
간결하고 이해하기 쉽다는 것에 이견이 없을 것이다.
그렇다면 어떤 문제를 해결하는 것일까?
람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것이다.
익명 함수(anonymous function)란 무엇일까?
익명 함수답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 '->'를 추가한다!
dataType methodName(arguments) -> {...}
dataType methodName (arguments){
body
}
(arguments) ->{
body
}
// statement
(int a, int b) -> {return a>b ? a : b;}
// expression
(int a, int b) -> a > b ? a : b
// not elision
(int a, int b) -> a > b ? a : b
// elision
(a,b) -> a > b ? a : b
람다식이 메서드와 동등한 것이라고 생각할 수 있지만,
사실 익명 클래스의 객체와 동등하다.
그렇다면 람다식은 어떤 클래스에 포함되는 것일까?
간단한 예시 코드를 작성해보자
(int a, int b) -> a > b ? a : b
/**
new Object(){
int max(int a, int b){
return a > b ? a : b;
}
}
*/
람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을까?
참조변수가 있어야 객체의 메서드를 호출할 수 있으니까 일단 익명 객체 주소를 참조변수에 저장해보자.
type f = (int a, int b) -> a > b ? a : b;
그럼 참조변수의 타입은 뭘로 해야 할까?
interface Fuction{
public abstract int max(int a, int b);
}
Fuction function = new Function(){
public int max(int a, int b){
return a > b ? a : b;
}
};
int bigger = funtion.max(5,3);
Function 인터페이스에 정의된 메서드 max()는
람다식 '(int a, int b) -> a > b ? a : b;'와 메서드의 선언부가 일치한다. 그래서 위 코드의 익명 객체를 람다식으로 대체할 수 있다.
Function function = (int a, int b) -> a > b ? a : b;
int bigger = funtion.max(5,3);
익명 객체를 람다식으로 대체 가능한 이유
// 기존 인터페이스의 메서드 구현
List<String list = Arrays.asList("abc","aaa","bbb","ddd",aaa");
Collections.sort(list, new Comparator<String>(){
public int compare(String s1, String s2){
return s2.compareTo(s1);
}
});
// 람다식을 통해 간단히 구현
List<String> list = Arrays.asList("abc", "aaa", ""bbb","ddd",aaa");
Collections.sort(list, (s1, s2)) -> s2.compareTo(s1);
함수형 인터페이스로 람다식을 참조할 수 있지만, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다. 람다식은 익명 객체이고 익명 객체는 타입이 없다. (정확히는 타입이 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없다.) 그러므로 아래와 같이 형변환이 필요하다
Function function = (Function) (()->{}); //
람다식은 하나의 객체이다.
즉 파라미터, 반환타입으로도 사용가능하다.
interface Test{ // 함수형 인터페이스
void myMethod();
}
aMethod( () -> sout("myMethod()")); // 람다식을 메서드의 파라미터로 지정
Test myMethod(){
return () -> (); // 람다식을 직접 반환
}
람다식은 Function인터페이스를 직접 구현하지 않았지만,
이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다. 그리고 이 형변환은 생략 가능하다.
람다식은 이름이 없을 뿐 분명히 객체인데도,
Object 타입으로 형변환 할 수 없다.
람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.
굳이 Object 타입으로 형변환하려면 아래와 같이 먼저 함수형 인터페이스로 변환해야 한다.
Object obj = (Object)(MyFunction)(()->{});
String str = (Object)(MyFunction)(()->{})).toString();
람다식도 익명 객체, 즉 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근하는 규칙은 익명 클래스에서 배운 것과 동일하다.
람다식 내에서 외부에 선언된 변수에 접근하는 방법
이 패키지에 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 정의해놓았다.
함수형 인터페이스 | 메서드 | 설명 |
---|---|---|
java.lang.Runnable | void run() | 매개변수도 없고, 반환값도 없음 |
Supplier T | get() | 매개변수는 없고, 반환값만 있음 |
Consumer | void accept(T t) | |
Function | R apply(T t) | |
Predicate | boolean test(T t) | |
BiConsumer | void accept(T t, U u) | 두개의 매개변수만 있고, 반환값이 없음 |
BiPredicate | boolean test(T t, U u) | 조건식을 표현하는데 사용됨. 매개변수는 둘, 반환값은 boolean |
BiFunction | R apply(T t, U u) | 두개의 매개변수를 받아서 하나의 결과를 반환 |
// 조건식 표현에 사용되는 Predicate
Predicate<String> isEmptyStr = s -> s.length() == 0;
String s = "";
if(isEmptyStr.test(s))
System.out.println("This is an empty String.");
두 람다식을 합성해서 새로운 람다식을 만들 수 있다.
함수 f, g가 있을 때
f.andThen(g)는 함수 f를 먼저 적용하고 g 적용.
f.compose(g)는 함수 g를 먼저 적용하고 f 적용.
여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다. Predicate의 끝에 negate()를 붙이면 조건식 전체가 부정이 된다.
Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i%2 == 0;
Predicate<Integer> notP = p.negate();
// 100 <= i && (i < 200 || i%2==0)
Predicate<Integer> all = notP.and(q.or(r));
System.out.println(all.test(150)); // true
Predicate<String> p = Predicate.isEqual(str1);
boolean result = p.test(str2); //str1과 str2가 같은지 비교하여 결과를 반환
// 위의 두 문장을 하나로 합치면
boolean result = Predicate.isEqual(str1).test(str2);
람다식이 하나의 메서드만 호출하는 경우,
메서드 참조를 통해 람다식을 간략히 할 수 있다.
'클래스명::메서드명'
또는 '참조변수::메서드명'
// 기존
Function<String, Integer> f = (String s) -> Integer.parseInt(s);
// 메서드 참조
Funcation<String, Integer> f = Integer::parseInt;
생성자를 호출하는 람다식도 메서드 참조로 변환 가능
Supplier<MyClass> s = () -> new MyClass(); // 람다식
Supplier<MyClass> s = MyClass::new; // 메서드 참조
배열 생성할 경우
Function<Integer, int[]> f = x -> new int[x]; // 람다식
Function<Integer, int[]> f2 = int[]::new; // 메서드 참조