자바8 이후, 메서드의 1급 시민화 - 스트림 api - 인터페이스의 디폴트 메서드 기술들이 도입되고 강화되었다. 디폴트 메서드를 통해 컬렉션이 강화되고, 분산 환경에서 거대한 컬렉션을 다루기 위해 병렬화 기술이 강화되었다.
이 컬렉션을 더 효율적으로 다루기 위해 스트림이 강화되고,
스트림을 편리하게 사용하기 위해 선언형 - 함수형 프로그래밍이 도입되었다.
선언형 - 함수형 프로그래밍을 위해 람다가 도입되었다.
데이터가 복잡해지고, "무엇을" 하려는지 보다 "어떻게" 하는지가 중요해졌다.
스트림을 보다 편리하게 사용하기 위해 선언형-함수형 프로그래밍이 도입 되고, 선언형적인 프로그래밍을 위해 람다가 도입되었고, 람다를 위해 함수형 인터페이스가 도입되었다.
자바는 더이상 객체 지향적이지만은 않고, 선언형-함수형적이기도 한 언어가 되었다.
1️⃣람다란?
- 메서드를 하나의 식으로 표현한 것. 코드 블록.
- 이름과 반환값이 없어지는, 익명함수라고 불린다.
- (인자 목록) -> {로직} 의 구조
예시 1
//기존 메서드
void foo() {
sout("lambda");
}
//lambda식으로 전환
() -> {
sout("lambda");
};
//한줄로 전환
() -> sout("lambda");
예시 2
//메서드
int max(int a, int b){
return a>b ? a:b;
}
//람다
(int a, int b) -> {
return a>b ? a:b;
}
//한줄
(int a, int b) -> a>b ? a:b
//매개변수 타입 추론 가능한경우, 매개변수 타입 생략가능
(a,b) -> a>b ? a:b
//매개변수 하나일 경우
(a) -> a*a
- 타입을 명시해야 코드가 더 명확한 경우가 아니라면, 람다의 매개변수 타입은 생략하는 것이 좋다.
2️⃣ 일급 값
기존 자바의 모든 정의는 클래스 안에서 이루어지는 객체지향적 언어였지만, 이제는 람다식과 메서드 레퍼런스로 메서드는 주고받을 수 있는 값인 1급 값이 되었다. (메서드를 변수처럼 다루는 것이 가능해짐)
자바 8의 "동작의 파라미터화" -> 메서드를 변수처럼 다룬다.
기존 자바 -> 주고받을 수 있는 값은 primitive값과 객체의 인스턴스 주소(레퍼런스)
이러한 자유롭게 주고받을 수 있는 값을 1급 값이라고 부른다.
람다는 메서드를 1급 값으로 만들며 "동작"을 "파라미터화" 시켰다.
전략패턴과 비슷한 템플릿 콜백 패턴에 활용할 수 있다.
예시
//weapon interface, attack method
public class Soldier{
public void attack(Weapon weapon){
weapon.attack();
}
}
//weapon1,2 -> Weapon 구현체
Soldier sol = new Soldier();
soldier.attack(new weapon1());
soldier.attack(new weapon2());
//to Lambda
Soldier sol = new Soldier();
soldier.attack(() -> sout("y"));
soldier.attack(() -> sout("z"));
- 메서드 자체를 전달해, weapon1,2 인스턴스를 만들 필요조차 없어진 것.
3️⃣ 익명 클래스
람다식은 메서드 자체를 전달하는 것이 아닌, 익명 클래스의 객체를 전달하는 것이다.
//람다식
(a,b) -> a>b ? a:b ;
//내부 구조
new Object() {
int max(int a, int b){
return a>b ? a:b;
}
}
함수형 인터페이스가 가지고 있는 public 메서드를 식으로 묘사한다면, 익명 객체가 만들어지면서 식을 전달할 수 있도록 감싸준다.
"함수형 인터페이스 : 추상 메서드를 하나만 갖는 인터페이스"
//함수형 인터페이스
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
//void 반환 람다식을, void 반환 추상 메서드를 가진 함수형 인터페이스 Runnable형 참조 변수로 받는다.
//유일 메소드인 run을 호출하면, 람다식이 실행된다.
Runnable runnable = () -> sout("lambda");
runnable.run();
예시
//함수형 인터페이스
@FunctionalInterface
public interface TestFunctionalInterface {
public abstract int max(int a, int b);
}
//1의 식(override 명시)
MyFunctionalInterface test = new MyFunctionalInterface() {
@Override
public int max(int a, int b) {
return a > b ? a : b;
}
};
//2의 식(람다식 표현 - 익명 클래스의 객체)
MyFunctionalInterface test1 = (int a, int b) -> a > b ? a : b;
함수형 인터페이스 타입의 매개변수 및 반환 타입
- 람다식은 참조변수로 다룰 수 있다.
- 타입이 함수형 인터페이스라면, 추상형 메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있다.
- soldier 클래스에서 weapon이 함수형 인터페이스였다.
- weapon의 attack 메서드가 추상형 메서드였고, 그 추상형 메소드와 파라미터와 반환이 같은 람다식을 전달해주었다.
- 람다식의 본질은 익명 클래스(객체)이기에, 객체를 주고받는 것이라 내부적으로는 같지만 간결하고 이해하기 쉽다.
@FunctionalInterface
public interface TestFunctionalInterface {
void printGo();
}
//참조 변수로 함수형 인터페이스 받기 (람다함수 참조 변수로 변수가 된다)
static TestFunctionalInterface getFunction(){
return () -> sout("run")
}
//함수형 인터페이스를 파라미터로 받기
static void execute(TestFunctionalInterface test){
test.run();
}
//in main
execute(() -> sout("run!"));
//참조 변수로 받기
TestFunctionalInterface test = () -> sout("run run!")
- 그렇다고 해서, 람다식의 타입이 함수형 인터페이스 타입과 일치하는 것은 아니다.
- 람다식은 타입은 있지만, 컴파일러가 임의로 이름을 정하기에 알 수가 없다.
- 묵시적으로(TestFunctionalInterface)가 붙어 형변환이 이루어 지는 것
- 컴파일러는 람다식의 이름을 외부클래스 이름을 이용해서 만든다.
- 익명객체 타입 만들때와 동일
- 일반적 익명객체 -> 외부 클래스 이름과 조합되어 외부클래스$번호와 같은 타입
- 람다식의 경우 : 외부클래스이름$$Lambda$번호 형식
- 익명객체 타입 만들때와 동일
4️⃣ 익명 클래스 사용 이유
Collections.sort(names, (s1, s2) ->
Integer.compare(s1.length(), s2.length())
);
- 위의 코드는 names 리스트의 객체들을 길이순 정렬하는 코드
- 위 표현이 가능한 이유는, 함수형 인터페이스 Comparator<T>가 존재하기 때문
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
- 함수형 인터페이스를 람다식으로 치환하여 가독성 좋게 사용해준 사례이다.
- 만약 람다식 표현이 없었다면 아래와 같이 표현(템플릿 콜백 패턴)
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
외부 변수를 참조하는 람다식
- 람다식은 익명 객체이기에 참조 클래스 지역변수를 상수(final)로 간주한다
- 하나의 스코프로 취급, 메서드에서 사용중인 변수와 같은 이름의 매개변수 선언이 불가능하다
5️⃣ 메서드 레퍼런스
- 메서드 참조를 재활용해 람다와 같이 사용할 수 있다.
- "어떻게" 메서드 호출하는지를 숨기고 "이 메서드를 호출" 하라는 선언형, 축약형 선언이다.
람다식이 하나의 메서드만을 호출하는 경우, 메서드 레퍼런스 형식으로 바꿀 수 있다.
- 정적 메서드 참조 : class::static method
- 인스턴스 메서드 참조 : class::instance method
- 기존 객체 인스턴스 메서드 참조 : instance::instance method
Double[] nums = {
1.0, 4.0, 9.0, 16.0, 25.0
};
//기존 람다식
Arrays.stream(nums)
.map(num -> Math.sqrt(num))
.forEach(sqrtNum -> sout(sqrtNum));
//레퍼런스 형태
Arrays.stream(nums)
.map(Math::sqrt)
.forEach(sout::println)
- Math.sqrt() -> 클래스::정적메서드형식
- forEach -> 인스턴스::인스턴스메서드
- 입력으로 들어갈 인자를 특정하지 않았는데, 위 상황의 인자는 오해소지 없이 명확하기에 가능하다.
- map, forEach 모두 내부 원소 하나씩 순회하기에
생성자를 메서드 레퍼런스로 표현해보자
Supplier<TestClass> factory = () -> new TestClass();
Supplier<TestClass> factory = TestClass::new;
Function<Integer, TestClass> factory1 = (elem) -> new TestClass(elem);
Function<Integer, TestClass> factory1 = TestClass::new;
BiFunction<Integer, String, TestClass> factory2 = (elem, elem2) -> new TestClass(elem, elem2);
BiFunction<Integer, String, TestClass> factory2 = TestClass::new;
// 배열
Function<Integer, int[]> factory3 = (x) -> new int[x];
Function<Integer, int[]> factory3 = int[]::new;
다음번에는 스트림에 대해 자세히 알아보자.
'Java&Kotlin' 카테고리의 다른 글
[Java] Generic Type & Wildcard Type (0) | 2024.06.27 |
---|---|
[Java] Stream & Lambda - Stream (0) | 2024.06.26 |
[JAVA] Record: 불변 데이터 클래스의 활용 (1) | 2024.06.12 |
[JAVA] Optional : 안전한 null 처리 방법 (0) | 2024.06.12 |
[Java & Intellij IDEA] Java 22 & Gradle 8.7 (0) | 2024.05.28 |