본문 바로가기
Java&Kotlin

[Java] Stream & Lambda - Lambda

by 모모_모 2024. 6. 25.
자바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️⃣ 메서드 레퍼런스

  • 메서드 참조를 재활용해 람다와 같이 사용할 수 있다.
  • "어떻게" 메서드 호출하는지를 숨기고 "이 메서드를 호출" 하라는 선언형, 축약형 선언이다.

람다식이 하나의 메서드만을 호출하는 경우, 메서드 레퍼런스 형식으로 바꿀 수 있다.

  1. 정적 메서드 참조 : class::static method
  2. 인스턴스 메서드 참조 : class::instance method
  3. 기존 객체 인스턴스 메서드 참조 : 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;

 

다음번에는 스트림에 대해 자세히 알아보자.