본문 바로가기
개발자 준비/JAVA

[JAVA 더 깊게] Lamda(feat.컬랙션프레임웍)

by osul_world 2022. 3. 3.
728x90

Lamda


 

람다식

람다식(Lambda Expression)이란 함수를 하나의 식(expression)으로 표현한 것이다. 함수를 람다식으로 표현하면 메소드의 이름이 필요 없기 때문에, 람다식은 익명 함수(Anonymous Function)의 한 종류라고 볼 수 있다.

 

직관적이고 간결한 코드로 가독성을 향상시킬수있다.

또한, 컬랙션이나 스트림 등에서도 다양하게 쓰인다.

 

 

람다식은 메서드에서 이름과 반환타입을 제거한다.

//원형
int max(int a){
	return a+5;
}

//람다식
(int a) -> { return a+5;}

매개변수가 하나일때는 괄호 생략 등등 다양한 생략 가능한 조건이 있으나,

나는 원형을 최대한 유지하고 일관성 있게 코딩하는게 좋기 때문에 이정도 규칙으로 통일한다.

 

 

 

함수형 인터페이스


자바는 모든것을 객체로 다룬다. 따라서 모든 메서드도 클래스 안에 포함되어야 한다.

람다는 함수를 하나의 식으로 간결하게 표현한것이다.

람다식은 어떤 클래스에 담겨있는것일까?

람다식은 메서드가 아니라 익명클래스 객체이다.

 

 

반드시 하나의 추상 메서드만을 갖는 함수형 인터페이스 타입이다.

//함수형 인터페이스 : @FunctinalInterface를 붙이면 컴파일러가 정의내용을 검증해준다.
@FunctinalInterface
interface FunctionalInterface {
    public int sum(int a, int b); //반드시 하나의 추상 메서드
    
    default int pop(){  //추상메서드만 1개로 제약, default나 static 메서드는 제약 없음
        return 10
    }
}


FunctionalInterface myLambda = (a, b) -> a + b;
myLambda.sum(1,10) //11

추상메서드가 한개여야 람다와 1:1 매칭이 되므로 1개로 제한한다.

 

 

함수형 인터페이스를 람다식과 매칭시켜 객체를 사용할수있다.

 

 

자바가 제공하는 함수형 인터페이스


자바에서는 자주 사용되는 함수형 인터페이스들을 'java.util.function' 패키지에 미리 정의해놨다.

이 패키지에 있는 함수형 인터페이스 만으로 충분하다.

 

함수형 인터페이스메서드
java.lang.Runnablevoid run();
SupplierT get();
Consumervoid accept(T t);
Function<T, R>R apply (T t);
Predicateboolean test(T t);
ex)
Predicate<String> pd = (String msg) -> {msg.equals("안녕")};

String str = "안녕";

if(pd.test(str)){
    return "인사가 왔습니다.";
}

 

 

함수형 인터페이스메서드
BiConsumer<T, U>void accept(T t, U u);
BiPredicate<T, U>boolean test(T t, U u);
BiFunction<T, U, R>R apply(T t, U u);

Bi 매개변수가 2개일때 사용한다.

 

 

함수형 인터페이스메서드
UnaryOperatorT apply(T t);
BinaryOperatorT apply(T t1, T t2);

반환 타입이 매개변수와 동일하다.

 

 

함수형 인터페이스메서드
IntFunction, LongFunction, DoubleFunctionR apply(int value), R apply(long value), R apply(double value)
ToIntFunction, ToLongFunction, ToDoubleFunctionint applyAsInt(T t), long applyAsLong(T t), double applyAsDouble(T t)

위에 함수형 인터페이스들은 모두 매개변수화 반환값이 지네릭 타입으로 참조형이었다.

기본형 타입을 사용할때도 Wrapper 클래스를 사용했다.

자바는 기본형 전용 함수형 인터페이스를 따로 제공한다.

 

 

 

활용예시


속단일지 모르지만 아직 코딩을 하면서 개발자가 함수형 인터페이스를 직접 사용하는 경우는 드물다.

하지만 많은 자바 라이브러리에서 함수형 인터페이스를 자주 사용해 편리한 로직을 제공한다.

대표적으로 컬랙션 프레임웍

 

 

컬랙션 인터페이스들에 default로 정의된 이 메서드들을 사용하면 효과적으로 컬랙션을 다룰수 있을것이다.

 

인터페이스메서드설명
Collectionboolean removeIf(Predicate filter);조건에 맞는 엘리먼트를 삭제
Listvoid replaceAll(UnaryOperator operator);모든 엘리먼트에 operator를 적용하여 대체(replace)
Iterablevoid forEach(Consumer action);모든 엘리먼트에 action 수행

 

인터페이스메서드설명
MapV compute(K key, BiFunction<K, V, V> f);지정된 키에 해당하는 값에 f를 수행
MapV computeIfAbsent(K key, Function<K, V> f);지정된 키가 없으면 f 수행후 추가
MapV cumputeIfPresent(K key, BiFunction<K, V, V> f)지정된 키가 있을 때, f 수행
MapV merge(K key, V value, BiFunction<V, V, V> f);모든 엘리먼트에 Merge 작업 수행, 키에 해당하는 값이 있으면 f 수행해서 병합후 할당
Mapvoid forEach(BiConsumer<K, V> action);모든 엘리먼트에 action 수행
Mapvoid replaceAll(BiFunction<K, V, V> f);모든 엘리먼트에 f 수행후 대체

 

매개변수로 함수형 인터페이스를 받는 것을 확인할수있다. 알맞는 람다식을 정의해 효과적으로 컬랙션을 제어할수있을 것이다.

ex)
    
ArrayList<Integer> list = new ArrayList<>(); // ArrayList는 List 인터페이스 구현 클래스

list.forEach((int a)->{a+10;}); // 모든 요소에 + 10

이런식으로 for문을 통해 컬랙션 요소를 하나하나 순회하면서 작업하는 수고로움을 편리하게 처리할수있다.

ArrayList는 List 인터페이스의 구현 클래스이고 List 인터페이스는 forEach가 default 정의된 Iterable를 상속한 Collection 인터페이스를 상속하고있다.

따라서 ArrayList는 forEach를 가지고있다.

 

 

업그레이드


'java.util.function' 에 정의된 자바가 제공하는 함수형 인터페이스에는 추상 메서드 외에도 다양한 default , static 메서드들이 정의되어있다.

추상 메서드만 한개로 제한되고 default , static은 상관없다고 했었다.

 

 

함수형 인터페이스가 제공하는 이 기능들을 사용하면 함수형 인터페이스를 합치거나 결합할 수있다.

 

 

몇개만 살펴보자

 

Function의 합성

함수형 언어의 재미있는 특성은 함수들을 합성할 수 있다는 것이다.

Function의 합성
default Function <T, V> andThen (Function <? super R, ? extends V> after);
default Function <V, R> compose(Function <? super V, ? extends T> before);
static Function<T, T> identity();

f.andThen(g) 를 수행하면 f 함수를 실행한 결과 값을 다시 g 함수의 인자로 전달하여 결과를 얻는 새로운 함수를 만들어 내게 된다.

이 때, f 함수의 리턴 타입이 g 함수의 파라미터 타입과 호환되어야 한다.

 

 

Predicate 결합

Predicate 함수형 인터페이스는 boolean 값을 리턴하는 함수를 다룬다. 따라서 여러개의 Predicate을 논리 연산자(&&, ||, !)를 이용해 연결해서 하나의 Predicate으로 얻어 낼 수도 있다.

Predicate 결합
default Predicate and (Predicate<? super T> other) ;
default Predicate or (Predicate<? super T> other);
default Predicate negate ();
static Predicate isEqual(Object targetRef);
ex)
Predicate <Integer> greater = x -> x > 10;
Predicate <Integer> less = x -> x < 20;

Predicate between = greater.and(less);

이런 식으로 Predicate을 결합하면 10 < x < 20 인지를 판단하는 Predicate 함수를 얻어낼 수 있다.

or() 메소드와 negate() 메소드도 비슷하게 사용하면 된다.

 

 

 

더 간결한 표현


나는 생략이 가능하더라도 생략하지 않고 최대한 원형을 유지하는것이 원칙이지만 다른 소스코드나 참고자료를 볼때 더 간결하게 표현된 경우가 있어서 알아두고자 간단하게 정리한다.

 

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

//case2
Function<String, Boolean> f = x -> obj.equals(x);
Function <String, Boolean> f = obj :: equals;

지네릭 타입으로 매개변수와 반환 타입을 유추할수있기때문에 이렇게 생략가능하다.

  • :: 을 통해 staic 메서드를 불러온다.

 

 

//case1
Supplier <TestClass> s = () -> new TestClass();
Supplier <TestClass> s = TestClass::new;

//case2
Function<Integer, TestClass> f = (i) -> new TestClass(i);
Function<Integer, TestClass> f = TestClass::new;

생성자의 경우도 이렇게 할수있다.

 

 

Reference


https://hbase.tistory.com/78

https://mangkyu.tistory.com/113

728x90