본문 바로가기
Develop/Java

[Java] Lambda, Functional Interface 그리고 Stream API

by 독서왕뼝아리 2023. 9. 8.

이브, 프시케 그리고 푸른 수염 어쩌고 따라하고 싶었어요ㅎㅎ..

 

Lambda

익명 함수로써 이름 없이 정의되며, 코드 블록을 1급 시민으로 다룰 수 있는 프로그래밍 언어의 기능

  • 🧐 1급 시민 First-Class Citizen | 1급 객체
    1. 변수에 할당할 수 있어야 한다.
    2. 함수의 매개변수로 전달할 수 있어야 한다.
    3. 함수의 리턴값으로 사용할 수 있어야 한다.
  • 함수나 메소드가 1급 시민으로 간주되기 위한 조건
  • 함수형 프로그래밍 스타일을 지원
  • 코드를 간결하게 관리
  • 주로 컬렉션 처리, 스트림 연산, 콜백 함수 등에 사용

→ 자바에선 함수를 일급시민으로 쓸 수 없었음, 람다 개념 등장 → 일급시민으로 사용할 수 있게됨

 

람다 표현식 작성방법

어떤 언어에서든 익명함수를 다 써보셨을 거라고 예상됩니다. 자바에서도 마찬가지로 위와 같이 람다를 사용합니다.

 


Functional Interface

@FunctionalInterface
interface CustomInterface<T> {
    // abstract method 오직 하나
    T myCall();

    // default method 는 존재해도 상관없음
    default void printDefault() {
        System.out.println("Hello Default");
    }

    // static method 는 존재해도 상관없음
    static void printStatic() {
        System.out.println("Hello Static");
    }
}

1개의 추상 메소드를 갖는 인터페이스를 뜻함 (SAM, Single Abstract Method)

→ 2개 이상의 추상 메소드가 존재하면 함수형 인터페이스가 될 수 없다

하지만 default, static 메소드는 여러 개 있어도 괜찮다!

 

 

이 하나의 추상 메소드가 1급 시민의 특성을 가진다!!!

더보기

Java의 인터페이스에서 default, static 키워드는 언제 왜 등장했을까요?

 

Java8에서 등장. 이전까지는 인스턴스 메소드만 가질 수 있었음

  • 유틸리티나 헬퍼 함수 같은 공통 코드
  • 기존 코드와 호환성과 업데이트 용이성

 

(어!!! 인터페이스에 추상 메소드를 추가하니까 구현체들 모두 상속시켜야 되네!!! 귀찮은데 디폴트 구현체로 넣자!!!)

public interface Collection<E> extends Iterable<E> {
	...
	default Stream<E> stream() {
	    return StreamSupport.stream(spliterator(), false);
  }
	...
}

 

@FunctionalInterface 어노테이션은 뭔가요?

해당 인터페이스가 함수형 인터페이스 조건에 맞는지를 검사한다

필수는 아니지만 인터페이스 검증과 유지보수를 위해 붙여 주는 게 좋아요

 

 

근데요 함수형 인터페이스를 우리가 구현하는 일이 거의 없어용>.<

기본적으로 제공하는 인터페이스를 사용

  • Function<T, R>
  • BiFunction<T, U, R>
  • Consumer<T>
  • Supplier<T>
  • Predicate<T>
  • Runnable
  • Callable 기타 등등

 

Predicate<T>로 알아보는 함수형 인터페이스 사용법

인자를 받아 boolean 타입을 리턴하는 인터페이스를 제공

T → boolean

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

// 간단 사용 예제
Predicate<Integer> sample = (count) -> count.equals(50); // Integer::equals
// 오른쪽 람다식이 'test 추상메소드의 구현체다'라고 생각하면 됨

sout(sample.test(10)); // false
sout(sample.test(50)); // true

 

Consumer<T>

인자 하나 받고 아무것도 리턴하지 않음

T → void

소비자라는 이름에 맞게 무언가 소비만 하고 끝낸다

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

 

Supplier<T>

인자를 받지 않고 T타입의 객체를 리턴

() → T

공급자라는 이름처럼 아무것도 받지 않고 특정 객체를 리턴

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
  • Callable 또한 ()→T 함수형 인터페이스다. Supplier와 거의 차이가 없다고 생각하면 된다. 하지만 Callable은 Runnable과 함께 등장한 개념으로 ExecutorService 와 사용됨

 

Function<T, R>

T타입의 인자를 받아 R타입 객체를 리턴

T → R

말 그대로 수학적인 의미에서 함수!

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

BiFunction 은 Function에서 파생된 클래스로 (T, U) → R

 

Comparator<T>

T타입 인자 2개를 입력 받아서 int타입으로 리턴

(T, T) -> int

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

Stream API

람다 표현식, Optional, 메소드 참조(Class::method) 개념이 있어야 함

 

.map( i → sout(i) ) ⇒ .map(System.out::println)

 

stream이란 ‘줄줄이 이어지다’라는 뜻으로 일련의 연속성을 갖는 흐름을 의미

stream API는 자바 컬렉션을 다루는데 사용되는 강력한 기능을 제공하는 인터페이스의 집합

  • 일관성 있는 연산 지원
  • 한 번 생성하고 사용한 스트림은 재사용할 수 없다
  • 생성(creation), 중개(intermediate), 최종(terminal) 연산으로 구분된다
  • lazy invocation이 적용된다. ⇒ terminal 연산이 호출돼야 최종 결과가 반영된다.

what 위주의 내부 반복 연산을 수행!

 

Stream Creation

스트림의 데이터 소스로 컬렉션도 배열도 파일도 string도 무한스트림으로도 사용될 수 있음

Stream.empty()

*Collection*.stream()

Arrays.stream(arr)

Stream.generate( ()→ T ).limit(10) : 스트림 무한하기 때문에 limit를 걸어줘야 함

Stream.iterate(T, Collable).limit(10) 

*Primitive*.range(start, end)

 

Stream Intermediate

함수형 인터페이스를 인자로 중개 연산을 합니다!

public interface Stream<T> extends BaseStream<T, Stream<T>> {
	Stream<T> filter(Predicate<? super T> predicate);
	<R> Stream<R> map(Function<? super T, ? extends R> mapper);
}

filter 메소드를 보면 Predicate를 인자로 받는 걸 볼 수 있죠?

 

Stream 생성 클래스

public final class StreamSupport {
	public static <T> Stream<T> stream(Supplier<? extends Spliterator<T>> supplier,
                                       int characteristics,
                                       boolean parallel) {
        Objects.requireNonNull(supplier);
        return new ReferencePipeline.Head<>(supplier,
                                            StreamOpFlag.fromCharacteristics(characteristics),
                                            parallel);
    }
		//... IntStream, DoubleStream 등등 존재
}

 

 

스트림 문법의 장점 : 간결함, 가독성 높아짐, 순차처리, 병렬처리 지원

스트림 문법의 단점 : 연산 비용 증가, 언박싱/박싱 비용

 

하지만 요즘은 하드웨어가 워낙 발전해서 연산 비용보다 유지보수 비용이 더 크기 때문에 스트림을 사용하는 것을 권장한다고 합니다. (물론 회사마다 다르겠지만요?)