람다(Lambda)
객체 지향 프로그래밍 언어 중 하나인 자바는 기존의 함수형 프로그래밍들보다
현업에서 많이 사용하고 선호하는 경향이 있었다
그러나 병렬 처리, 이벤트 지향 프로그래밍에 함수형 프로그래밍이 다시금 손을 들고 있는 추세라서
자바는 기존의 객체 지향 프로그래밍에 함수형 프로그래밍을 첨가하며 새로운 패러다임을 만들었다
이른 바, 람다식(Lambda Expressions)을 지원하면서 기존 코드 패턴이 달라진 것이다
수학에서 쓰이는 람다 계산법에서 사용된 식이고 이를 프로그래밍에 도입했다는 것
프로그래밍에선,
익명 함수를 생성하기 위한 식으로 객체 지향 언어라기보다는 함수 지향 언어다
자바에서 이걸 수용한 이유는 자바 코드가 간결해지고 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를
쉽게 집계할 수 있기 때문이다.
람다식의 형태는 매개변수를 가진 코드블록이지만 런타임에 익명 구현 객체를 생성한다
기본 작성
(타입 매개변수) -> { ... }
매개변수가 1개 일 때, 매개변수 ( ) 소괄호 생략가능
매개변수 -> { ... }
매개변수가 2개 이상이고, 리턴문만 존재할 때는 return을 생략가능
(매개변수1, 매개변수2) -> 리턴값;
(num1, num2) -> {return num1 + num2}
(num1, num2) -> num1 + num2
// return문만 존재하니까 return 생략가능, 중괄호도 생략
매개변수가 2개 이상이고, 실행문을 실행하고 결과값을 리턴할 경우
(매개변수1, 매개변수2) -> { ... };
(타입 매개변수)는 오른쪽 중괄호 블록을 실행하는데 필요한 값을 제공하는 역할이다
매개변수 이름은 자유롭게 지어도 상관없다
-> 이 기호는 매개변수를 이용해 중괄호 실행블록을 실행한다는 뜻이다
그리고 매개변수의 타입은 런타임에 대응되는 값에 따라 자동으로 인식될 수 있기 때문에
람다식에서는 매개변수의 타입을 일반적으로 언급하진 않는다
매개변수가 1개만 있으면 ( ) 소괄호를 생략할 수 있고 실행문이 1개만 있으면 { } 중괄호도 생략이 가능하다
그런데 만약 매개변수가 없으면 람다식에서 매개변수 자리가 사라지기 때문에
( ) -> { ... } 의 형태로 빈 괄호 ( ) 를 반드시 사용해야한다
또한 중괄호 안에 return 문만 있을 때 람다식에선 return 문을 사용하지 않고 생략하는 것이 관례다
함수형 인터페이스
자바에선 메소드를 사용하려면 클래스 객체를 먼저 생성한 다음에 생성한 객체로 메소드를 호출해야 한다
클래스에서 공통적으로 사용하는 기능이 있다하더라도 클래스마다 메소드를 정의해야 한다
이 부분을 람다식으로 편리하게 쓸 수 있다
자바는 새로운 함수 문법을 정의하는 대신에 인터페이스 문법을 활용해서 람다식을 표현한다
딱 한 개의 추상 메소드만 포함하는 인터페이스를 함수형 인터페이스라고 한다
추상 메소드가 하나라면 굳이 안써줘도 되는데
실수로라도 2개 이상의 추상 메소드를 선언하는 것을 방지하고자 붙이는게 좋다
@java.lang.FunctionalInterface
public interface FunctionalInterface {
public void accept();
// public void otheraccept();
// 2개 이상이 되어버리면 애너테이션 선언부에 에러가 난다
}
매개변수와 리턴 값이 없는 람다식
@java.lang.FunctionalInterface
public interface FunctionalInterface {
public void accept();
}
매개변수, 리턴 값이 없는 추상 메소드 가진 함수형 인터페이스가 있다
이 인터페이스를 타겟 타입으로 갖는 람다식은
FunctionalInterface example = () -> { ... };
// example.accept();
이렇게 작성해야 하는데, 람다식에서 매개변수가 없는 이유는 accept() 가 매개변수를 가지지 않기 때문이다
람다식이 대입된 인터페이스의 참조 변수(위의 코드에서 example) 는 주석에 있는 것 처럼
accept()를 호출 할 수 있고 람다식의 중괄호 { } 를 실행시킨다
public class FunctionalInterfaceExample {
public static void main(String[] args) {
FunctionalInterface example; // 인터페이스 추상메소드를 참조변수 example에 담고
example = () -> { // 람다식 선언
String str = "메소드 호출, 첫번째" ;
System.out.println(str); // 실행문 1개
};
example.accept(); // 람다식 대입된 인터페이스 참조변수로 accept() 호출
example = () -> System.out.println("메소드 호출, 두번째");
// 실행문이 1개라서 중괄호 { } 생략 가능한 것
example.accept(); // 람다식 대입된 인터페이스 참조변수로 accept() 호출
}
}// output
// 메소드 호출, 첫번째
// 메소드 호출, 두번째
example 이 참조변수이자 지역변수이기 때문에 저렇게 .accept()로 쓰인 후에 소멸하고
다시 example 람다식을 새롭게 작성해서 다시 .accpet()로 호출 할 수 있다
두 번째 호출할 때는 실행문이 1개라서 중괄호를 생략한 것이다
매개변수가 없기 때문에 소괄호는 필요하다 (매개변수 자리가 사라지기 때문)
매개변수가 있는 람다식
public class FunctionalInterfaceExample2 {
public static void main(String[] args) {
FunctionalInterface2 example2; //인터페이스 추상메소드를 참조변수 example2에 담음
example2 = (x) -> { // 람다식 선언 (매개변수 있는)
int result = x * 2; // int형 result 에 x * 2 담고
System.out.println(result); // 프린트 출력
};
example2.accept2(2);
// 람다식 대입된 인터페이스 참조변수 example2 로 accept2() 호출
// accept2()는 매개변수 int형 x 가진 추상 메소드
example2 = x -> System.out.println(x * 3);
// 실행문이 1개라서 중괄호 { } 생략, 매개변수도 1개라서 소괄호 () 생략
example2.accept2(2);
}
}
// output
// 4
// 6
매개변수 x가 있는 람다식 example2을 선언하고
int형 result에 x*2 를 할당 후, 프린트 출력
람다식 대입된 인터페이스 참조변수 example2로 accpet2() 매개변수 int형 x를 가진 추상메소드를 호출한다
x에 2를 대입해서 호출, 결과는 2*2 = 4
실행문이 1개라서 중괄호를 생략하고
매개변수 또한 1개라서 소괄호까지 생략할 수 있다
x*3 람다식 2대입해서 6 출력
리턴 값이 있는 람다식
import static java.lang.Integer.sum;
public class FunctionalInterfaceExample3 {
public static void main(String[] args) {
FunctionalInterface3 example3;
example3 = (x, y) -> { // 람다식 생성, 매개변수 2개라서 소괄호 생략불가
int answer = x + y;
return answer;
};
int answer2 = example3.accept3(3, 7);
System.out.println(answer2); // 10
example3 = (x, y) -> {return x + y;};
int answer3 = example3.accept3(3,7);
System.out.println(answer3); // 10
// int answer에 x+y 할당하지 않고도
// 그냥 x+y 자체 값이 리턴 가능한 값이니까
// 불필요한 변수 선언 과정을 생략
example3 = (x, y) -> x + y;
int answer4 = example3.accept3(3,7);
System.out.println(answer4); // 10
// 리턴문만 존재하니까 return을 생략하고
// 실행문이 1개니까 중괄호도 생략
example3 = (x, y) -> sum(x, y);
int answer5 = example3.accept3(3,7); // 10
System.out.println(answer5);
// x+y 를 sum 메소드를 사용해서 sum(x, y)로 표현 가능하다
// 지금 전부 같은 값을 출력한다
// 얼마든지 이렇게 다르게 표현이 가능하다
}
}
example3 람다식은 매개변수가 2개라서 소괄호 생략이 불가하다
지금 위의 코드 흐름은 같은 결과를 다른 코드 형태로 구현한 것인데
주석을 보면 알 수 있듯이 불필요한 변수 선언 과정을 생략할 수 있고
리턴문만 존재해서 return 자체를 생략할 수도 있고
실행문이 1개이기 때문에 중괄호도 생략 가능한 것을 알 수 있다
그 외에 코드가 간결해지기 위한 다양한 시도들이 담겨 있다
이러한 시도들은 밑에 메소드 레퍼런스에서 자세히 다루도록 한다
메소드 레퍼런스
람다식에서 불필요한 매개변수를 제거하는 것이 목적
람다식은 종종 기존 메소드를 단순히 호출만 하는 경우가 많다
example3 = (x, y) -> sum(x, y);
이 람다식을 메소드 레퍼런스로 바꾸면
example3 = Integer::sum;
메소드 레퍼런스, 람다식과 마찬가지로 인터페이스의 익명 구현 객체로 생성되기 때문에
타겟 타입인 인터페이스의 추상 메소드가 어떤 매개 변수를 가지고 리턴 타입이 무엇인지에 따라 달라진다
FunctionalInterface3 example3 = Integer::sum;
인터페이스는 2개 int 매개 값을 받아서 int 값을 리턴하기 때문에
Integer::sum 메소드 레퍼런스에 대입할 수 있는 것이다
메소드 레퍼런스는 정적 or 인스턴스 메소드를 참조할 수 있고 생성자 참조도 가능하다
정적 메소드, 인스턴스 메소드 레퍼런스
정적 메소드를 참조할 경우,
클래스 이름 뒤에 :: 기호를 붙이고 정적 메소드 이름을 작성하면 된다
클래스 :: 메소드
인스턴스 메소드를 참조할 경우,
먼저 객체를 생성하고 참조 변수 뒤에 :: 기호 붙이고 인스턴스 메소드 이름을 작성하면 된다
참조변수 :: 메소드
package FunctionalInterface;
public interface CalculatorInterface {
int applyAsInt(int x, int y);
}
// applyAsInt(int x, int y) 라는 추상 메소드를 인터페이스에서 만든다
package FunctionalInterface;
public class Calculator_SI {
public static int staticMethod(int x, int y){
return x + y;
}
public int instanceMethod(int x , int y){
return x * y;
}
}
// Calculator_SI 라는 클래스에 스태틱메소드와 인스턴트 메소드를 만든다
package FunctionalInterface;
public class MethodReferences {
public static void main(String[] args) {
CalculatorInterface operator;
//static method
operator = Calculator_SI::staticMethod;
System.out.println("정적 메소드 결과 : " + operator.applyAsInt(3, 5));
//instance method
Calculator_SI calculator_si = new Calculator_SI();
operator = calculator_si::instanceMethod;
// 참조변수 calculator_si를 만들고 :: 기호뒤에 메소드
System.out.println("인스턴스 메소드 결과 : " + operator.applyAsInt(3,5));
}
}
// output
// 정적 메소드 결과 : 8
// 인스턴스 메소드 결과 : 15
MethodReferences 클래스, main 메소드에서 스태틱 메소드와 인스턴스 메소드 참조하는 과정이다
정리하자면, 메소드 레퍼런스는 람다식과 마찬가지로 인터페이스 익명 구현 객체로 생성되기 때문에
추상메소드가 어떤 매개 변수를 가지는지, 리턴 타입이 무엇인지에 따라 달라지고
정적, 인스턴스 메소드 레퍼런스를 참조할 수 있다
생성자 참조
생성자를 참조한다는 것은 객체 생성을 의미하는데
단순 메소드 호출로 구성된 람다식을 메소드 레퍼런스로 대치할 수 있듯,
단순 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치 가능하다
(a, b) -> { return new 클래스(a, b);};
생성자 참조로 표현하자면 클래스 이름 뒤 :: 기호를 붙이고 new 연산자를 작성하면 된다
생성자가 오버로딩 돼서 여러 개 있을 경우
컴파일러는 함수형 인터페이스의 추상 메소드와 동일한 매개 변수 타입과 개수를 가지고 있는 생성자를 찾아서
실행하게 된다. 그리고 만약 해당 생성자가 존재하지 않으면 컴파일 오류가 발생하게된다
클래스 :: new
package ConstructorReference;
public class Worker {
private String name;
private String id;
public Worker(){
System.out.println("Worker() 실행");
}
public Worker(String id){
System.out.println("Worker(String id) 실행");
this.id=id;
}
public Worker(String name, String id){
System.out.println("Worker(String name, String id) 실행");
this.id = id;
this.name = name;
}
public String getName(){
return name;
}
public String getId(){
return id;
}
}
package ConstructorReference;
import java.util.function.BiFunction;
import java.util.function.Function;
public class ConstRef {
public static void main(String[] args) {
Function<String, Worker> function1 = Worker::new;
Worker worker1 = function1.apply("jphwany");
// Function<String,Worker> 함수형 인터페이스의 Worker apply(String) 메소드를 이용, Worker 객체 생성
BiFunction<String, String, Worker> function2 = Worker::new;
Worker worker2 = function2.apply("박재환", "jphwany");
// BiFunction <String, String, Worker> 함수형 인터페이스의 Worker 객체 생성
}
}
// output
// Worker(String id) 실행
// Worker(String name, String id) 실행
'Java' 카테고리의 다른 글
[Java 심화] 스트림(Stream) (매-우 기초적인 부분만) (0) | 2022.06.06 |
---|---|
[Java 심화] 애너테이션(Annotation) (소스코드 미수록) (0) | 2022.06.01 |
[Java 심화] Enum (0) | 2022.06.01 |
[Java 컬렉션] 내부 클래스 (Inner Class) (0) | 2022.06.01 |
[Java 컬렉션] 컬렉션 프레임워크(Collection Framework) (0) | 2022.06.01 |
댓글