참고 : http://programming.oreilly.com/2014/04/whats-new-in-java-8-lambdas.html
아직 익숙한 개념은 아니지만 Java 8에서 새롭게 추가된 기능 중 큰 비중을 차지하는 람다 표현식에 대해 이야기 해보려고 합니다.
람다란 무엇인가?
람다는 어떠한 행위를 나타내는 축약된 단일 메소드 클래스이며 변수 처럼 어딘가에 할당시키거나 다른 메소드들에 변수를 이용해 값을 전달 하는 것과 동일한 형태로 전달 될 수 있습니다. 솔식히.. 말로는 잘 와닿지 않습니다.. 그렇기 때문에 진행하면서 코드로 이야기를 하도록 하죠..
람다 구문
input arguments -> body
람다 표현식은 일반 메소드와 같이 입력 변수부분과 동작을 기술하는 부분 그리고 선택적인 반환값으로 이루어져 있습니다. 위와 같이 화살표(->)를 기준으로 좌측은 메소드 변수를 우측은 이 변수들로 무엇을 할 것인가 하는 행위를 나타냅니다.
람다의 타입 : Functional Interface
자바는 형식화된 언어(Typed Language)로 필수적으로 타입을 선언하는 것이 일반적입니다. 그럼 람다의 타입은 대체 무엇인가? 라는 질문을 하지 않을 수 없는데 람다는 다음과 같이 설계 되었다고 합니다.
람다는 기존의 익명 클레스를 통해서 익숙한 익명 메소드 전략을 재활용한 것으로 새롭게 타입이 추가된 것은 아니라고 합니다. 대신 특별한 인터페이스인 Functional Interface를 갖습니다. 이는 일반적인 인터페이스와 동일하지만 다음의 추가적인 2개의 특징을 갖는다고 합니다.
- 단 하나의 추상 메소드를 갖음.
- 선택적 이지만 @FunctionalInterface 주석을 추가하여 람다 표현식으로 사용 될 수 있음(이 방식이 강력 추천 됨).
자바에는 무수히 많은 단일 메소드의 인터페이스들이 존재하며 이것들은 Functional Interface로 개조 되었습니다. 만약 필요에 의해 Functional Interface를 만들고자 한다면 하나의 추상 메소드를 갖는 인터페이스를 정의하고 @FunctionalInterface만 상단에 붙여주면 됩니다. 버전업 된 API 문서에서 Functional Interface가 적용된 것인지 확인 할 수 있습니다.
조건에 맞춰 다음과 같은 인터페이스를 정의해 보았습니다.
package lambda.works;
@FunctionalInterface
public interface TestFunctionalInterface<T> {
public T doSomething(T t1, T t2);
}
그리고 다음과 같은 모델 클레스를 하나 작성 하였습니다.
package lambda.works;
public class CargoWorks {
private int boxQty;
public CargoWorks(int boxQty) {
this.boxQty = boxQty;
}
public int getBoxQty() {
return boxQty;
}
public void setBoxQty(int boxQty) {
this.boxQty = boxQty;
}
}
람다 표현식을 이용하여 Functional Interface를 다음과 같이 구현해 보았습니다.
//두 스트링의 연결
TestFunctionalInterface<String> stringAdder = (String s1, String s2) -> s1 + s2;
//두 수의 곱
TestFunctionalInterface<Integer> multipleNumbers = (Integer i1, Integer i2) -> i1 * i2;
//두 박스의 합
TestFunctionalInterface<CargoWorks> quantityAdder = (CargoWorks c1, CargoWorks c2) -> {
c1.setBoxQty(c1.getBoxQty() + c2.getBoxQty());
return c1;
};
TestFunctionalInterface가 generic type 인터페이스이기 때문에 서로 다른 타입으로 구현이 가능한 것을 알 수 있습니다. 결국 람다 표현식의 타입은 Functional Interface라고 볼 수 있습니다.
이렇게 정의된 것은 다음과 같이 사용 될 수 있습니다.
package lambda.works;
public class ImplFunctions {
//두 스트링의 연결
TestFunctionalInterface<String> stringAdder = (String s1, String s2) -> s1 + s2;
private void concatnateStrings(String s1, String s2) {
System.out.println("Concatenated result : " + stringAdder.doSomething(s1, s2));
}
}
매우 다양하게 정의 된 비지니스 로직이 위와 같이 단순한 절차에 의해 사용될 수 있습니다.
Java 8 이전과의 차이
package lambda.works;
public class TestPreJava8 {
TestFunctionalInterface<CargoWorks> quantityMerger = new TestFunctionalInterface<CargoWorks>() {
@Override
public CargoWorks doSomething(CargoWorks c1, CargoWorks c2) {
c1.setBoxQty(c1.getBoxQty() + c2.getBoxQty());
return c1;
}
};
}
Java 8 이전이라면 앞서 소개한 람다 표현식은 위의 익명 클레스와 같이 표현 될 것입니다. 이걸 사용하려면 다음과 같겠죠..
public void preJava8Method() {
TestFunctionalInterface<CargoWorks> quantityMerger = ...;
CargoWorks c1 = new CargoWorks(1000);
CargoWorks c2 = new CargoWorks(2000);
CargoWorks mergedQunatiy = quantityMerger.doSomething(c1, c2);
}
아마도 기존의 방식대로면 람다 표현식과 같은 기능을 구현할 수는 있겠지만 불필요한 코드들이 많이 늘어나게 될 것 입니다. 람다의 간결함을 예로 들기 위해 다음은 다양한 행위가 있을 경우에 대한 예 입니다.
//두 박스의 합
TestFunctionalInterface<CargoWorks> quantityAdder = (CargoWorks c1, CargoWorks c2) -> {
c1.setBoxQty(c1.getBoxQty() + c2.getBoxQty());
return c1;
};
//수가 많은 박스
TestFunctionalInterface<CargoWorks> compareBoxes = (CargoWorks c1, CargoWorks c2) -> {
if(c1.getBoxQty() > c2.getBoxQty()) {
return c1;
} else {
return c2;
}
};
//또 다른 박스관련 작업(람다에서는 기존에 존재하는 다른 메소드(여기에선 thatThingYouDo)도 사용이 가능합니다)
TestFunctionalInterface<CargoWorks> otherJobs = (CargoWorks c1, CargoWorks c2) -> thatThingYouDo(c1, c2);
위와 같은 여러 행위가 있다고 가정하면 다음의 메소드는 어떻게 처리할 것인지를 매개변수로 함께 받아서 처리할 수 있습니다.
private void applyBehavior(TestFunctionalInterface<CargoWorks> applySomething, CargoWorks c1, CargoWorks c2) {
applySomething.doSomething(c1, c2);
}
Functional Interface인 Runnable
단일 메소드를 갖는 인터페이스 중 가장 있기 있는 걸 꼽으라면 아마도 Runnable일 것 입니다. 아무것도 반환하지 않는 run 메소드를 갖고 있는데 보통 쓰레드를 이용해서 프로그램의 성능을 향상시키기 위해 많이 사용되는 인터페이스 입니다.
기존에 익명클래스 방식을 사용한 코드는 다음과 같습니다.
new Thread(new Runnable() {
@Override
public void run() {
doSomething();
}
}).start();
이것을 람다 표현식을 사용하면 다음과 같이 사용할 수 있습니다.
new Thread(() -> doSomething()).start();
바로 Thread의 생성자에 람다 표현식을 이용해서 행위를 전달할 수 있게 되는 거죠. 바로 Thread 클래스가 받아들이는 Runnable이 Functional Interface이기 때문에 가능해 지는 코드 입니다.
이것을 조금 더 응용해본다면 다음과 같은 코드도 작성할 수 있습니다.
package lambda.works;
public class AsyncManager {
public void runAsync(Runnable r) {
new Thread(r).start();
}
}
그리고 이것을 이용하여..
package lambda.works;
public class TestFunctions {
private AsyncManager manager = new AsyncManager();
//두 스트링의 연결
TestFunctionalInterface<String> stringAdder = (String s1, String s2) -> s1 + s2;
//두 수의 곱
TestFunctionalInterface<Integer> multipleNumbers = (Integer i1, Integer i2) -> i1 * i2;
//두 박스의 합
TestFunctionalInterface<CargoWorks> quantityAdder = (CargoWorks c1, CargoWorks c2) -> {
c1.setBoxQty(c1.getBoxQty() + c2.getBoxQty());
return c1;
};
private void takeResults() {
CargoWorks cargoNoOne = new CargoWorks(1000);
CargoWorks cargoNoTwo = new CargoWorks(2000);
manager.runAsync(() -> System.out.println("Running in Async mode"));
manager.runAsync(() -> {
for(int i = 0; i < 100; i++) {
System.out.println("just counting : " + i);
}
});
manager.runAsync(() -> System.out.println("String : " + stringAdder.doSomething("A", "b")));
manager.runAsync(() -> System.out.println("Number : " + multipleNumbers.doSomething(1, 2)));
manager.runAsync(() -> System.out.println("Qty : " + quantityAdder.doSomething(cargoNoOne, cargoNoTwo).getBoxQty()));
}
public static void main(String[] args) throws Exception {
new TestFunctions().takeResults();
}
}
이렇게 테스트를 해보면 각각은 쓰레드로 동작하기 때문에 결과가 뒤섞여서 나옴을 알 수 있습니다. 참 쉽죠?
마치며..
개인적으로 람다 표현식은 기존의 Java 프로그래밍에 꽤 많은 변화를 가져오지 않을까 생각됩니다. 아마도 병렬 프로그래밍과 비동기 프로그래밍이 보편화된 시대의 도래를 촉진하겠죠.. 아직 람다에 대해 많은 것을 알지는 못하지만 Java 8에서 가장 핫한 녀석인것 같다는 생각입니다.