람다식 - Lamda expression

김주형·2022년 7월 27일
0

Java

목록 보기
3/8

Reference 🙇‍♂️

이 글의 내용은 다음 내용을 참조하여 작성되었습니다.


Search 💻

람다식의 도입으로 인해 자바는 객체지향 언어인 동시에 함수형 언어가 되었다고 한다. 함수형 언어의 장점들을 자바에서도 누릴 수 있게 되었으니 람다식이라는 강력한 무기에 대해 알아보자

  • 왜 람다식을 사용하나요? : 일부 메서드에 인자로 전달할 수 있는 익명 함수를 정의함으로써 함수 인터페이스를 구현하는 것이 가능하다.
    • 함수형 프로그래밍 활성화
      • 모든 JVM 기반 언어는 객체지향 프로그래밍을 통해 작업해야 했지만, 람다식의 등장으로 함수 코드 작성이 가능해졌다.
    • 읽기 쉽고 간결한 코드
      • 람다식 사용으로 엄청난 양의 코드가 제거된 것이 보고되었다.
    • 사용하기 쉬운 API와 라이브러리
      • 람다식을 사용하여 설계된 API는 다른 API를 더 쉽게 사용하고 지원 가능하다.
    • 병렬 처리 지원
  • 람다식을 사용함으로써 얻는 장점은 무엇인가요?:
    • 간결하고 읽기 쉬운 코드.
      그리고 람다 표현식으로 가변 관찰할 가능성(no potential for variable shadowing)이 없으며, 내부 클래스를 사용할 때 큰 단점(클래스 증가)을 제거한다.
    • 객체지향이 유연한 변경을 얻는 대신 객체 중첩이라는 단점을 감수했었는데, 그 단점을 제거했다고 이해함, 참조
  • 람다식의 목적은 무엇인가요?
    • 익명 메서드 작성에 사용
    • 간결하고 기능적인 구문을 제공
    • 함수형 프로그래밍 개념을 기반으로 하며
  • [람다식 사용 시 주의점]
    • 람다 표현식은 컴파일러의 약어를 제공하여 대리자(delegates)에게 할당된 메서드를 방출할 수 있도록 한다.
    • 컴파일러는 람다 인자에 대한 자동 유형 추론을 수행하며, 이는 주요 이점이다.

람다식이란?

"메서드를 하나의 식(expression)으로 표현한 것"이라고 간단히 말할 수 있다. 그렇다면 그렇게 함으로써 해결되는 문제는 무엇일까?

  • 메서드의 이름과 반환값이 없어지므로 람다식을 익명 함수(anonymous function)라고도 한다.
  • 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.
int [] arr = new int[5];
Arrays.setAll(arr ,(i) -> (int)(Math.random() *5)+1);

위 람다식이 하는 일을 메서드로 표현하면 다음과 같다.

int method(){
	return (int)(Math.random()*5) +1;

간결하고 이해하기 쉽다는 것에 이견이 없을 것이다.
그렇다면 어떤 문제를 해결하는 것일까?

  • 모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 메서드를 호출할 수 있다.
  • 그러나 람다식은 모든 과정없이 람다식 자체만으로도 메서드의 역할을 대신할 수 있다.
  • 메서드의 매개변수로 전달되어지는 것이 가능하고. 메서드의 결과로 반환될 수도 있다.

람다식으로 인해 메서드를 변수처럼 다루는 것이 가능해진 것이다.


람다식을 작성하는 방법

익명 함수?

익명 함수(anonymous function)란 무엇일까?

  • 식별자에 바인딩되지 않는 함수 정의 (an anonymous function (function literal, lambda abstraction, lambda function, lambda expression or block) is a function definition that is not bound to an identifier.)
  • 일반적으로 고차함수 또는 함수를 반환해야 하는 고차함수의 결과를 구성하는데 사용되는 인자로써 사용된다.
  • 함수 사용 횟수가 제한되어 있는 경우, 명명된 함수보다 구문이 가벼울 수 있다.

익명 함수답게 메서드에서 이름과 반환타입을 제거하고 매개변수 선언부와 몸통{} 사이에 '->'를 추가한다!

dataType methodName(arguments) -> {...}

  • 기존
dataType methodName (arguments){
body
}
  • 람다식 적용
(arguments) ->{
body
}
  • 반환값이 있는 메서드
    • return 문장(statement)대신 '식(expression)'으로 표현 가능
      (문장이 아닌 '식'이므로 끝에 ;을 붙이지 않는다.)
// statement
(int a, int b) -> {return a>b ? a : b;}
// expression
(int a, int b) -> a > b ? a : b
  • 반환 타입이 없는 경우
    • 선언된 매개변수의 타입이 추론 가능한 경우 생략할 수 있다.(대부분의 경우)
    • 람다식에 반환 타입이 없는 이유는 항상 추론이 가능하기 때문이라고 한다.(어느 하나의 타입만 생략하는 것은 허용되지 않는다.)
// not elision
(int a, int b) -> a > b ? a : b
// elision
(a,b) -> a > b ? a : b

함수형 인터페이스(Fuctional Interface)

람다식이 메서드와 동등한 것이라고 생각할 수 있지만,
사실 익명 클래스의 객체와 동등하다.
그렇다면 람다식은 어떤 클래스에 포함되는 것일까?
간단한 예시 코드를 작성해보자

(int a, int b) -> a > b ? a : b
/**
new Object(){
	int max(int a, int b){
    	return a > b ? a : b;
    }
 }
 */

람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을까?
참조변수가 있어야 객체의 메서드를 호출할 수 있으니까 일단 익명 객체 주소를 참조변수에 저장해보자.

type f = (int a, int b) -> a > b ? a : b;

그럼 참조변수의 타입은 뭘로 해야 할까?

  • 참조형이기 때문에 클래스 또는 인터페이스가 가능하다.
  • 람다식과 동등한 메서드가 정의되어 있는 것이어야 한다.
    그래야만 참조변수로 익명 객체(람다식, 보통의 메서드와 달리 이름이 없는 객체)의 메서드를 호출할 수 있기 때문이다.
  • max() 메서드가 정의되어있는 Fuction 인터페이스
interface Fuction{
	public abstract int max(int a, int b);
}
  • 익명 객체의 메서드 호출
Fuction function = new Function(){
				public int max(int a, int b){
                	return a > b ? a : b;
                    }
                };
int bigger = funtion.max(5,3);

Function 인터페이스에 정의된 메서드 max()는
람다식 '(int a, int b) -> a > b ? a : b;'와 메서드의 선언부가 일치한다. 그래서 위 코드의 익명 객체를 람다식으로 대체할 수 있다.

  • 익명 객체를 람다식으로 대체
Function function = (int a, int b) -> a > b ? a : b;
int bigger = funtion.max(5,3);

익명 객체를 람다식으로 대체 가능한 이유

  • 람다식은 실제로 익명 객체이며,
    Test 인터페이스를 구현한 익명 객체의 메서드 max()와
    람다식의 파라미터 타입, 개수, 반환값이 모두 일치하기 때문이다.
  • 인터페이스를 통해 람다식을 다루기로 결정되었으며,
    • 하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 기존 자바 규칙들을 어기지 않으면서도 자연스럽기 때문에
  • 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스(functional interface)'라고 부르기로 했다.
  • 이 때문에 함수형 인터페이스에는 단 하나의 추상 메서드만 존재해야 한다. 그래야 람다식과 인터페이스의 메서드가 1:1로 매핑되기 때문이다.
// 기존 인터페이스의 메서드 구현
List<String list = Arrays.asList("abc","aaa","bbb","ddd",aaa");
Collections.sort(list, new Comparator<String>(){
	public int compare(String s1, String s2){
    	return s2.compareTo(s1);
    }
});
// 람다식을 통해 간단히 구현
List<String> list = Arrays.asList("abc", "aaa", ""bbb","ddd",aaa");
Collections.sort(list, (s1, s2)) -> s2.compareTo(s1);
  • @FunctionalInterface를 붙이면 컴파일러가 함수형 인터페이스를 올바르게 정의하였는지 확인해준다.

함수형 인터페이스로 람다식을 참조할 수 있지만, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다. 람다식은 익명 객체이고 익명 객체는 타입이 없다. (정확히는 타입이 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없다.) 그러므로 아래와 같이 형변환이 필요하다

Function function = (Function) (()->{}); //

함수형 인터페이스 타입의 매개변수와 반환타입

람다식은 하나의 객체이다.
즉 파라미터, 반환타입으로도 사용가능하다.

interface Test{ // 함수형 인터페이스
	void myMethod();
}

aMethod( () -> sout("myMethod()")); // 람다식을 메서드의 파라미터로 지정

Test myMethod(){ 
		return () -> ();  // 람다식을 직접 반환
}

람다식 타입의 형변환

람다식은 Function인터페이스를 직접 구현하지 않았지만,
이 인터페이스를 구현한 클래스의 객체와 완전히 동일하기 때문에 위와 같은 형변환을 허용한다. 그리고 이 형변환은 생략 가능하다.

람다식은 이름이 없을 뿐 분명히 객체인데도,
Object 타입으로 형변환 할 수 없다.
람다식은 오직 함수형 인터페이스로만 형변환이 가능하다.
굳이 Object 타입으로 형변환하려면 아래와 같이 먼저 함수형 인터페이스로 변환해야 한다.

Object obj = (Object)(MyFunction)(()->{});
String str = (Object)(MyFunction)(()->{})).toString();

외부 변수를 참조하는 람다식

람다식도 익명 객체, 즉 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근하는 규칙은 익명 클래스에서 배운 것과 동일하다.
람다식 내에서 외부에 선언된 변수에 접근하는 방법


java.util.function 패키지

이 패키지에 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 정의해놓았다.

함수형 인터페이스메서드설명
java.lang.Runnablevoid run()매개변수도 없고, 반환값도 없음
Supplier Tget()매개변수는 없고, 반환값만 있음
Consumervoid accept(T t)
FunctionR apply(T t)
Predicateboolean test(T t)
BiConsumervoid accept(T t, U u)두개의 매개변수만 있고, 반환값이 없음
BiPredicateboolean test(T t, U u)조건식을 표현하는데 사용됨. 매개변수는 둘, 반환값은 boolean
BiFunctionR apply(T t, U u)두개의 매개변수를 받아서 하나의 결과를 반환
  • 수학에서 결과로 true 또는 false를 반환하는 함수를 Predicate 라고 한다.
  • 매개변수가 2개인 함수형 인터페이스는 이름 앞에 ‘Bi’가 붙는다.
  • Supplier는 매개변수는 없고 반환값만 존재하는데 메서드는 두 개의 값을 반환할 수 없으므로 BiSupplier가 없다.
  • 매개변수의 타입과 반환타입이 일치할 때는 Function 대신 UnaryOperator를 사용한다. (매개 변수 2개면 BinaryOperator)
// 조건식 표현에 사용되는 Predicate

Predicate<String> isEmptyStr = s -> s.length() == 0;
String s = "";

if(isEmptyStr.test(s))
  System.out.println("This is an empty String.");

Function의 합성과 Predicate의 결합

Function의 합성

두 람다식을 합성해서 새로운 람다식을 만들 수 있다.
함수 f, g가 있을 때
f.andThen(g)는 함수 f를 먼저 적용하고 g 적용.
f.compose(g)는 함수 g를 먼저 적용하고 f 적용.

Predicate의 결합

여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합할 수 있다. Predicate의 끝에 negate()를 붙이면 조건식 전체가 부정이 된다.

Predicate<Integer> p = i -> i < 100;
Predicate<Integer> q = i -> i < 200;
Predicate<Integer> r = i -> i%2 == 0;
Predicate<Integer> notP = p.negate();

// 100 <= i && (i < 200 || i%2==0)
Predicate<Integer> all = notP.and(q.or(r));
System.out.println(all.test(150));  // true
  • static 메서드인 isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용한다.
  • isEqual()의 매개변수로 비교대상을 하나 지정하고, 또 다른 비교대상은 test()의 매개변수로 지정한다.
Predicate<String> p = Predicate.isEqual(str1);
boolean result = p.test(str2);  //str1과 str2가 같은지 비교하여 결과를 반환

// 위의 두 문장을 하나로 합치면
boolean result = Predicate.isEqual(str1).test(str2);

메서드 참조(Method reference)

람다식이 하나의 메서드만 호출하는 경우,
메서드 참조를 통해 람다식을 간략히 할 수 있다.
'클래스명::메서드명'
또는 '참조변수::메서드명'

// 기존
Function<String, Integer> f = (String s) -> Integer.parseInt(s);

// 메서드 참조
Funcation<String, Integer> f = Integer::parseInt;
생성자를 호출하는 람다식도 메서드 참조로 변환 가능

Supplier<MyClass> s = () -> new MyClass();  // 람다식
Supplier<MyClass> s = MyClass::new; // 메서드 참조
배열 생성할 경우

Function<Integer, int[]> f = x -> new int[x]; // 람다식
Function<Integer, int[]> f2 = int[]::new; // 메서드 참조
profile
도광양회

0개의 댓글