함수형 프로그래밍과 람다(Lambda)
자바 8부터 람다식이라는 기능이 제공된다.
람다식이 무엇을 위한 기능인지를 이해하기 위해서 우선 함수형 프로그래밍을 이해해야한다.
함수형 프로그래밍
함수형 프로그래밍은 함수를 정의하고, 이 함수를 데이터 처리부로 보내 데이터를 처리하는 기법이다.
외부에서 함수를 제공 받은 데이터 처리부는 입력 값, 즉 데이터를 넣고 함수에 정의된 로직을 실행한다.
람다식
람다식은 외부에서 제공되는 함수의 역할을 하는 파라미터를 가진 중괄호 블록이다.
파라미터를 데이터로 사용하여, 중괄호 내부의 로직을 실행하는 것이다.
단 하나의 추상 메소드를 가진 인터페이스를 함수형 인터페이스라고 한다.
람다식은 추상 메소드가 재정의된 함수형 인터페이스의 익명 구현 객체이다.
람다식 예시 코드
아래의 예시 코드는 람다식의 예를 보여준다.
Calculable은 단 하나의 추상 메소드를 가진 함수형 인터페이스이다.
@FunctionalInterface는 필수 어노테이션은 아니지만, 컴파일 과정에서 해당 인터페이스가 함수형 인터페이스인지 확인해준다.
LambdaExample의 action()을 살펴보자.
action()은 함수형 인터페이스 타입의 파라미터를 가진다.
action()은 데이터 처리부이며, Calculable이라는 함수형 인터페이스의 익명 구현 객체를 제공 받는다.
그리고 익명 구현 객체는 데이터를 파라미터로 입력 받아, 추상 메소드를 실행한다.
이제 main()을 살펴보자.
action()에서 파라미터로 사용되는 것이 람다식이며, 추상 메소드가 재정의된 함수형 인터페이스의 익명 구현 객체이다. 재정의된 추상 메소드의 로직을 통해 데이터 처리부에서 데이터를 처리한다.
@FunctionalInterface // 선택사항, but 컴파일 과정에서 함수형 인터페이스가 단 하나의 추상 메소드만 갖고 있는지 확인할 수 있다.
public interface Calculable {
// 추상 메소드
void calculate(int x, int y);
}
/////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////
public class LambdaExample {
public static void main(String[] args) {
// 외부에서 제공된 함수(람다식)
action((x, y) -> {
int result = x + y;
System.out.println("result = " + result);
});
// 외부에서 제공된 함수(람다식)
action((x, y) -> {
int result = x - y;
System.out.println("result = " + result);
});
}
// 데이터 처리부
public static void action(Calculable calculable) {
// 데이터
int x = 10;
int y = 4;
// 데이터 처리
calculable.calculate(x, y);
}
}
파라미터 유무에 따른 람다식 작성법
추상 메소드의 파라미터 유무에 따라 람다식 작성법에 다소 차이가 있다.
파라미터가 없는 람다식
파라미터가 없을 경우, ()로 파라미터 입력부를 작성하면된다.
로직의 실행문이 2개 이상일 경우 { ... }를 통해 로직 영역을 구분해줘야한다. 실행문이 1개인 경우에는 { ... }를 생략할 수 있다. { ... } 관련 규칙은 앞으로 계속 적용될 사항이니 기억해두자.
- () -> { ... }: 실행문이 2개 이상
- () -> ...: 실행문이 1개
@FunctionalInterface
public interface Workable {
// 매개변수가 없는 추상메소드
void work();
}
/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
public class Person {
public void action(Workable workable) {
workable.work();
}
}
/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
public class LambdaExample {
public static void main(String[] args) {
Person person = new Person();
// 매개변수가 없는 람다식
// 실행문이 두 개 이상인 경우 중괄호 필요.
person.action(() -> {
System.out.println("출근을 합니다.");
System.out.println("프로그래밍을 합니다.");
});
// 매개변수가 없는 람다식
// 실행문이 한 개일 경우, 중괄호 생략 가능
person.action(() -> System.out.println("퇴근합니다."));
}
}
파라미터가 있는 람다식
파라미터가 있는 람다식의 경우, (a, b, c, ...)를 통해 파라미터 입력부를 작성하면 된다.
파라미터가 1개인 경우, ( ... )를 생략할 수 있다.
- (a, b, c, ... ) -> { ... }: 파라미터가 2개 이상
- a -> { ... }: 파라미터가 1개
public class Person {
public void action1(Workable workable) {
workable.work("홍길동", "프로그래밍");
}
public void action2(Speakable speakable) {
speakable.speak("안녕하세요.");
}
}
////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
@FunctionalInterface
public interface Workable {
void work(String name, String job);
}
////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
@FunctionalInterface
public interface Speakable {
void speak(String content);
}
////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
public class LambdaExample {
public static void main(String[] args) {
Person person = new Person();
// 매개변수가 2개 이상인 경우
person.action1((name, job) -> {
System.out.printf("%s이 %s을 합니다.", name, job);
System.out.println();
});
// 매개변수가 1개인 경우
person.action2(content -> System.out.printf("%s라고 말합니다.", content));
}
}
리턴 값이 있는 람다식 작성법
리턴 값이 있는 람다식의 경우, 로직의 실행문 개수에 따라 작성법에 차이가 있다.
- ( ... ) -> { ...; retrun ...; }: 실행문이 2개 이상인 경우, return 키워드를 생략할 수 없다.
- ( ... ) -> ...: 실행문이 1개이며 연산 또는 함수의 결과가 리턴 타입과 일치하는 경우, return 키워드를 생략할 수 있다.
@FunctionalInterface
public interface Calculable {
double calc(double x, double y);
}
////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
public class Person {
public void action(Calculable calculable) {
double result = calculable.calc(10, 4);
System.out.println("결과: " + result);
}
}
////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
public class LambdaExample {
public static void main(String[] args) {
Person person = new Person();
// 실행문이 두 개 이상인 경우
// return 키워드 생략 불가능
person.action((x, y) -> {
double result = x + y;
return result;
});
// 실행문이 1개인 경우
// return 키워드 생략 가능
person.action((x, y) -> x + y);
// 실행문이 1개인 경우(함수 호출)
// return 키워드 생략 가능
person.action((x, y) -> sum(x, y));
}
public static double sum(double x , double y) {
return x + y;
}
}
메소드 참조
메소드 참조는 함수형 인터페이스의 추상 메소드와 파라미터, 리턴 타입이 동일한 메소드를 통해 익명 구현 객체를 대체하는 방법이다.
- 클래스::정적 메소드
- 인스턴스::인스턴스 메소드
아래의 예시 코드를 통해 메소드 참조를 이해해보자.
Calculable 함수형 인터페이스의 calc() 추상 메소드는 double 타입의 파라미터를 2개 가지며, double 타입을 반환한다.
Computer는 정적 메소드인 staticMethod(), 인스턴스 메소드인 instanceMethod()를 갖고 있다.
두 메소드 모두 calc()와 파라미터, 리턴 타입이 동일하다.
이런 경우, 해당 메소드를 참조하여 익명 구현 객체를 대체할 수 있다.
@FunctionalInterface
public interface Calculable {
double calc(double x, double y);
}
/////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
public class Person {
public void action(Calculable calculable) {
double result = calculable.calc(10, 4);
System.out.println("결과: " + result);
}
}
/////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
public class Computer {
public static double staticMethod(double x, double y) {
return x + y;
}
public double instanceMethod(double x, double y) {
return x + y;
}
}
/////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
/**
* 메소드 참조: 메소드를 참조하여 매개변수의 정보 및 리턴 타입을 알아내, 람다식의 불필요한 매개변수 작성을 제거하는 것을 목적으로 한다.
*/
public class LambdaExample {
public static void main(String[] args) {
Person person = new Person();
// 정적 메소드
person.action(Computer::staticMethod);
Computer computer = new Computer();
// 인스턴스 메소드
person.action(computer::instanceMethod);
}
}
파라미터의 메소드 참조
람다식의 파라미터 a의 메소드를 호출하여, 파라미터 b를 호출된 메소드의 파라미터로 사용할 수 있다.
아래의 예시 코드를 보자.
Comparable 함수형 인터페이스의 compare() 추상 메소드는 String 타입의 파라미터 a, b를 가진다.
이런 경우 람다식에 String.compareToIngnoreCase(String)을 참조하여 사용할 수 있다.
String::compareToIngnoreCase는 String 객체인 a로부터 compareToIgnore()를 호출하고, compareToIgnore()의 파라미터로 b를 사용한다는 의미이다.
@FunctionalInterface
public interface Comparable {
int compare(String a, String b);
}
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
public class Person {
public void ordering(Comparable comparable) {
String a = "홍길동";
String b = "김길동";
int result = comparable.compare(a, b);
if (result < 0) {
System.out.printf("%s은 %s보다 앞에 옵니다.", a, b);
} else if (result == 0) {
System.out.printf("%s은 %s와 같습니다.", a, b);
} else {
System.out.printf("%s은 %s보다 앞에 옵니다.", b, a);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////
public class MethodReferenceExample {
public static void main(String[] args) {
Person person = new Person();
// (a, b) -> a.compareToIgnoreCase(b)
person.ordering(String::compareToIgnoreCase);
}
}
생성자 참조
람다식이 단순히 객체를 생성하고 리턴하도록 구성된다면, 람다식을 생성자 참조로 대체할 수 있다.
함수형 인터페이스 Creatable1, Creatable2에는 각각 String 타입 파라미터 1개, String 파라미터 2개를 갖는 create() 추상메소드가 정의되어있다. 그리고 추상 메소드들은 Member 객체를 반환한다.
이 때 Member가 추상 메소드와 파라미터가 동일한 생성자를 갖고 있고, 람다식에서 처리되야할 일이 단순히 Member 객체를 생성하고 반환하는 것이라면, 익명 구현 객체를 생성자 참조로 대체할 수 있다.
@FunctionalInterface
public interface Creatable1 {
Member create(String id);
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
@FunctionalInterface
public interface Creatable2 {
Member create(String id, String name);
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
public class Person {
public Member getMember1(Creatable1 creatable) {
Member member = creatable.create("winter");
return member;
}
public Member getMember2(Creatable2 creatable) {
Member member = creatable.create("winter", "한겨울");
return member;
}
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
public class Member {
private String id;
private String name;
public Member(String id) {
this.id = id;
}
public Member(String id, String name) {
this.id = id;
this.name = name;
}
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/**
* 람다식이 단순히 객체를 생성하로 리턴하도록 구성된다면, 람다식을 생성자 참조로 대치할 수 있다.
* 클래스::new
* 추상 메소드의 매개변수 개수에 따라 실행되는 Member 생성자가 다르다.
*/
public class ConstructorReferenceExample {
public static void main(String[] args) {
Person person = new Person();
Member member1 = person.getMember1(Member::new);
System.out.println(member1.toString());
Member member2 = person.getMember2(Member::new);
System.out.println(member2.toString());
}
}