[Java] Lambda expressing

He SEO·2022년 3월 15일
0

Lambda?

익명 함수. Anonymous function. 이름없이 사용되는 형태.
특징으로는 메서드의 인수로 전달될 수 있고, 변수로 저장될 수 있음.
기본적인 표현의 구성

(People p1, People p2) -> p1.getAg().compareTo(p2.getAge());
  • parameter list : poeple p1, people p2
  • → : 람다의 파라미터와 바디를 구분
  • lambda body : 람다의 반환값에 해당하는 표현식

즉, 아래와 동일
(parameters) → expression
(parameters) → {statements}

장점

  • 코드를 간결하게 만들 수 있음
  • 개발자의 의도가 명확히 드러나 가독성이 높아짐
  • 함수를 만드는 과정없이 한번에 처리할 수 있어 생산성이 높아짐
  • 병렬 프로그래밍이 용이

단점

  • 람다를 사용하면서 만든 무명함수는 재사용이 불가
  • 디버깅이 어려움
  • 람다를 남발하면 비슷한 함수가 중복 생성되어 코드가 지저분해질 수 있음
  • 재귀로 만들 경우에 부적합

Behavior Parameterization

어떤 동작을 parameter로 만들 수 있음. 즉, 함수의 인자로 어떤 동작을 하는 함수를 받을 수 있음.

ArrayList<Product> filteredByName = 
	filter(products, (Product product) -> product.getName().equals("새우깡")); 

ArrayList<Product> filteredByComplex = 
	filter(products, (Product product) -> { 
		return product.getName().equals("새우깡") 
			&& product.getStore().equals("이마트"); 
});

Anonymous class의 치환 - comparator

products.sort(Product proc1, Product proc2) 
		-> proc1.getPrice()-proc2.getPrice());
/* java 8 부터는 List<T> interface에 sort()가 default method로 추가되어 있음 */

Functional interface

우리는 람다식으로 순수 함수를 선언할 수 있게 되었지만 java는 기본적으로 객체지향 언어이기 때문에 순수 함수와 일반 함수를 다르게 취급하고 있으며 java에서는 이를 구분하기 위해서 함수형 인터페이스가 등장하게 됨.

함수형 인터페이스란 함수를 1급 객체처럼 다룰 수 있게 해주는 어노테이션으로, 인터페이스에 선언하여 단 하나의 추상 메소드만을 갖도록 제한하는 역할을 함. 함수형 인터페이스를 사용하는 이유는 java의 람다식이 함수형 인터페이스를 반환하기 때문임.

java에서 제공하는 함수형 인터페이스

위의 표에 따르면 아래처럼 변수로 취급되어 사용되어, 해당 interface를 구현한게 됨

Predicate<String> isEmptyString = (String s) -> s.isEmpty();
Consumer<Integer> printInt = (int i) -> System.out.println("" + i);
Function<String, Integer> strCount = (String s) -> s.length();
Supplier<People> makeObject = () -> new People();
UnaryOperator<Integer, Integer> sum = (int i) -> i+i;

Framework에서 제공하는 Functional interface는 Generic을 사용하기 때문에 Primitive type을 사용할 수 없음. Primitive type을 사용하면 자동으로 auto boxing 됨.

하지만 다량의 배치작업 수행 시, auto boxing으로 인한 오버헤드가 발생하는 것을 무시할 수 없음.

List<Integer> list = new ArrayList<>();
for (int i=0; i<1000; i++) {
	list.add(i); //auto boxing
}

만약 primitive type을 사용하여 functional interface를 써야 한다면, 각 interface의 특화형을 사용하는 것이 좋음.

public interface IntPredicate {
	boolean test(int i);
}
/* boxing 없음 */
IntPredicate evenNumber = (int i) -> i%2==0;
/* boxing 됨 */
Predicate<Integer> evenNumber = (int i) -> i%2==0;

void 호환 규칙

Lambda body에 표현식이 있으면 void를 반환하는 method descriptor와 호환 됨.
즉, void를 반환하는 signature의 경우 다른 타입도 받을 수 있음.

Predicate<String> p = s -> list.add(s);
Consumer<String> c = s -> list.add(s);

list.add()는 return으로 boolean을 반환하지만 Consumer<T>: (T) → void에서도 사용할 수 있음.

Type Inference (형식 추론)

Comparator<Person> p = (Person p1, Person p2) -> p1.getAge() - p1.getAge();

/* 인자의 추론형식 */
Comparator<Person> p = (p1, p2) -> p1.getAge() - p2.getAge();

/* 인자가 한개인 경우 인자의 "()" 생략 가능 */
Predicate<String> s = s -> s.length();

Local Variable의 사용

대부분 람다 사용 시 람다의 body 안에서 주어진 인자만 사용하지만, 익명 클래스처럼 외부의 변수(free variable)도 활용할 수 있으며, 이런 동작을 Lambda capturing이라고 함.

int baseValue = 1000;
Function<Integer, Integer> sum = input -> input + baseValue;

단, 외부에서 정의된 free variable은 final or final 속성을 띄어야 함.

아래 경우는 baseValue를 재정의하여 compile error 발생.

int baseValue = 1000;
Function<Integer, Integer> sum = input -> input + baseValue; //compile error
baseValue = 123; //재정의

Lambda 내부에서 접근 가능한 free variable은 세가지임.

  • local variable
  • static variable
  • member variable - heap 메모리에 할당되어 thread 공유가 가능

이 중에서 지역변수만 변경 불가하고 나머지 변수들은 람다 내부에서 읽거나 쓰기가 가능. 왜냐면 java의 메모리와 관련이 있음. 지역변수는 stack 영역에 저장되는데, 람다식 외부에 정의된 지역변수는 해당 thread가 종료되면서 사라지기 때문에 람다 내부에서 수정이 불가능해지는 상황이 올 수 있음. 따라서 람다 내부에서는 해당 함수를 참조만 할 수 있도록 해당 값을 capturing 해서 사용함.

Method Reference

함수형 인터페이스를 람다식이 아닌 일반 메소드를 참조시켜 선언하는 방법. 아래 3가지 조건을 만족해야 함.

  • 함수형 인터페이스의 매개변수 타입 = 메소드의 매개변수 타입
  • 함수형 인터페이스의 매개변수 개수 = 메소드의 매개변수 개수
  • 함수형 인터페이스의 반환형 = 메소드의 반환형

참조 가능한 메소드는 일반 메소드, static 메소드, 생성자가 있으며 클래스 이름::메소드이름 으로 참조할 수 있음.

  1. 일반 메소드 참조
//기존의 람다식
Function<String, Integer> function = (str) -> str.length();
function.apply("Hello world");

//method reference
Function<String, Integer> function = String::length;
function.apply("Hello world");
//일반 메소드를 참조하여 Consumer를 선언함
Consumer<String> consumer = System.out::println;
consumer.accept("Hello world");

//method reference를 통해 Consumer를 매개변수로 받는 forEach를 쉽게 사용할 수 있음
List<String> list = Arrays.asList("red", "orange", "yellow", "green", "blue");
list.forEach(System.out::println);

//inteface Iterable<T>
default void forEach(Consumer<? super T> action) {
	Objects.requrieNonNull(action);
	for(T t: this) {
		action.accept(t);
	}
}
  1. Static method reference
Predicate<Boolean> predicate = Objects::isNull;

//isNull function
public static boolean isNull(Object obj) {
	return obj == null;
}
  1. 생성자 참조

Supplier는 매개변수 없이 반환값만을 갖는 인터페이스이기 때문에 매개변수 없이 String 객체를 새롭게 생성하는 String의 생성자를 참조하여 Supplier로 선언할 수 있음.

Supplier<String> supplier = String::new;

생성자 fererence를 사용할 경우 객체의 생성이 delay 됨. 즉, lazy initialize가 가능.

실제 객체는 호출하는 순간에 생성됨.

참고 사이트

profile
BACKEND 개발 기록 중. 감사합니다 😘

0개의 댓글