함수적 프로그래밍 방식으로 익명 함수(annonymous function)를 단순한 계산식으로 표현
이름 없이 실행 가능한 함수 표현식
함수는 코드들의 집합으로, 코드의 재사용성을 높이고자 내부에서 고정적인 데이터 값을 쓰지 않고, 외부로부터 인수를 통해 값을 전달받아 코드를 실행한다.
자바는 객체지향 언어이기 때문에 최소 실행 단위가 객체다. 그래서 함수는 객체의 구성원인 메서드로 활용된다.
하지만 자바 8부터는 함수적 프로그래밍을 위해 람다식을 지원하고 있다.
(자료형 매개 변수, ...) -> {실행문;}
클래스형 참조변수 = 람다식;
하나의 변수에 하나의 함수를 매핑한다.
람다식 = 딱 하나의 추상 메서드를 가진 인터페이스 클래스
자바스크립트는 약타입 언어라서 익명 함수(annonymous function)을 변수에 담을 때 타입을 고려하지 않고 자유롭게 받아도 되지만, 자바는 강타입 언어라서 람다식을 변수에 담을 때 반드시 타입을 고려해야 하는데, 인터페이스를 해당 익명 구현 객체 타입으로 보고 람다식을 해당 인터페이스 타입으로 변수에 담는다.
람다식도 결국 객체다. 인터페이스를 익명 클래스로 구현하고, 인터페이스 타입의 변수에 람다식을 담아서 변수에서 메서드를 호출해서 사용한다.
@FunctionalInterface
interface MyTypeAnno {
public int sum(int a, int b);
final boolean isNumber = true; // final 상수
default void print() {}; // 디폴트 메서드
static void print2() {}; // static 메서드
}
public static void main(String[] args) {
MyTypeAnno mt = (int a, int b) -> a+b;
System.out.println(mt.sum(10,20));
}
// or
public static MyTypeAnno myFunction() {
return (int a, int b) -> a + b;
}
public static void main(String[] args) {
MyTypeAnno func = myFunction();
int result = func.sum(1,2);
System.out.println(result);
}
내부 클래스(inner class)의 일종으로 이름이 없는 클래스
이름이 없다는 것은 기억되지 않는다는 의미로 일회성의 성격을 가지고 있다.
보통 부모 클래스의 자원을 상속 받아서 재정의하여 딱 한번만 사용할 용도에 쓰인다. 따로 자식 클래스를 정의하고 선언할 필요가 없다.
// 부모 클래스
class Animal {
public String bark() {
return "동물이 웁니다";
}
}
public class Main {
public static void main(String[] args) {
// 익명 클래스 : 클래스 정의와 객체화를 동시에. 일회성으로 사용
Animal dog = new Animal() {
@Override
public String bark() {
return "개가 짖습니다";
}
// 새로 정의한 메소드로 익명 클래스 내에서만 동작 가능
public String run() {
return "달리기 ㄱㄱ싱";
}
}; // 단 익명 클래스는 끝에 세미콜론을 반드시 붙여 주어야 한다.
// 익명 클래스 객체 사용
dog.bark();
dog.run(); // Compile Error - 외부에서 호출 불가능
}
}
람다식의 코드가 훨씬 간결하다.
익명 내부 클래스는 람다식과 달리 새로운 클래스 파일을 만든다.
익명 내부 클래스 내부의 this 키워드는 현재 익명 내부 클래스 객체를 참조한다.
람다식의 this는 람다가 있는 클래스를 가리킨다.
변수의 명시적인 변경이나 재할당 문제를 피할 수 있다.
쉽게 병렬화(멀티 쓰레딩)가 가능하다.
코드가 간결해지고 직관적이여서 의도 파악이 쉽다.
컬렉션 필터링, 반복, 추출 등에서 코드 생산성이 높아진다.
람다는 이름이 없기 때문에 문서화를 할 수 없다.
람다식은 기본적으로 익명 객체를 구현한 것인데, 익명 객체는 콜 스택(call stack) 추적이 어려워서 디버깅이 힘들다.
stream에서 forEach문으로 람다를 사용할 시 for문 보다 성능이 떨어진다.
추상 및 구체 클레스를 확장할 수 없고 단 하나의 추상 메서드만 사용할 수 있기 때문에 여러 메서드를 사용하고 싶고 확장성이 있는 경우 익명 클래스를 사용하는 것이 좋다.
람다식을 사용하기 위해서 매번 함수형 인터페이스를 만드는 것은 귀찮기 때문에 이미 만들어 놓은 표준 함수형 인터페이스를 쓰는 것을 추천한다.
| 함수형 인터페이스 | 매개 변수 | 반환값 | 메서드 | 역할 |
|---|---|---|---|---|
| Runnable | X | X | void run() | 단순히 람다식 형태의 메서드를 만듦 |
| Supplier</T/> | X | O (T) | T get() | 아무 매개값 없이 리턴값만을 반환 |
| Consumer</T/> | O (T) | X | void accept(T t) | 매개값만 받고 처리 (리턴값 X) |
| Function</T, R> | O (T) | X | R apply(T t) | 매핑(타입 변환)하기 |
| Predicate</T/> | O (T) | O (boolean) | boolean Test(T t) | 매개값을 받고 true / false 리턴 |
생산(supply)한다는 말은 데이터를 반환(공급) 한다는 뜻으로 보면 된다.
// Boolean 값을 리턴하는 함수 정의
BooleanSupplier booleanSup = () -> true;
System.out.println(booleanSup.getAsBoolean());
// double 값을 리턴하는 함수 정의
DoubleSupplier doubleSup = () -> 1.0;
System.out.println(doubleSup.getAsDouble());
매개값만 받고 처리 (리턴값 X)
소비한다는 말(consume)은 사용만 할 뿐 리턴값이 없다는 뜻이다.
BiConsumer의 Bi 뜻은, 라틴어에서 파생된 영어 접두사 bi- 로 "둘"을 의미한다. 따라서 매개변수를 두개 받는다로 이해하며 암기하면 된다.
// 객체 T를 받아 출력하는 함수 정의
Consumer<String> c1 = t -> System.out.println("입력값 : "+ t);
c1.accept("홍길동"); // 입력값 : 홍길동
// 객체 T와 U를 받아 출력하는 함수 정의
BiConsumer<String, Integer> c2 = (a, b) -> System.out.println("입력값1 : "+ a+ ", 입력값2 : "+ b);
c2.accept("홍길동", 100); // 입력값1 : 홍길동, 입력값2 : 100
// int 값을 받아 출력하는 함수 정의
IntConsumer c3 = a -> System.out.println("입력값 : "+ a);
c3.accept(100); // 입력값 : 100
// double 값을 받아 출력하는 함수 정의
DoubleConsumer c4 = a -> System.out.println("입력값 : "+ a);
c4.accept(100.01); 입력값 : 100.01
https://inpa.tistory.com/entry/☕-함수형-인터페이스-API#runnable_인터페이스 [Inpa Dev 👨💻:티스토리]
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++){
System.out.println(i);
}
});
new Thread() 생성자 안에 매개 변수로 람다식을 넣었다.
// 람다식을 사용하지 않을 때
enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x * y; }
};
private final String symbol;
Operation (String symbol) { this.symbol = symbol; }
// toString을 재정의하여 열거 객체의 매핑된 문자열을 반환하도록
@Override
public String toString() {
return symbol;
}
// 열거 객체의 메소드에 사용될 추상 메서드 정의
public abstract double apply(double x, double y);
}
// 람다식을 사용했을 때
enum Operation {
PLUS("+", (x,y) -> x+y),
MINUS("-", (x,y) -> x-y),
TIMES("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);
private final String symbol;
// DoubleBinaryOperator는 함수형 인터페이스로,
// 두 개의 double 값을 받아서 다른 double 값을 반환한다.
private final DoubleBinaryOperator op;
public Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
@Override
public String toString() {
return symbol;
}
// apply 메서드가 호출되면 DoubleBinaryOperator의 applyAsDouble()이 호출됨
public double apply(double x, double y) {
return op.applyAsDouble(x,y);
}
}
실행하려는 메서드의 매개 변수의 정보 및 리턴 타입을 알아내어 람다식의 불필요한 부분을 생략한 표현
// 람다식
IntBinaryOperator result = (a, b) -> Math.max(a,b);
// 메서드 참조
IntBinaryOperator result = Math::max
result.applyAsInt(100, 200); // 200