Backend/JAVA

Java 8에 추가된 것들 2

keepbang 2021. 9. 17. 20:48

람다(Lambda) 표현식 /

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

람다 표현식과 함수형 인터페이스는 밀접한 관계가 있어서 함께 정리했다.

 

메소드가 하나인 인터페이스를 익명클래스로 구현할 수 있는데 익명 클래스를 사용하면 가독성도 떨어지고 코드가 길어지는 단점이 있다.

 

이러한 단점을 보완하기 위해서 람다 표현식이 만들어졌고 익명 클래스로 구현된 인터페이스를 좀더 간단하게 표현하기위해서 사용되어진다.

 

람다표현식은 인터페이스에 메소드가 하나인 것들에만 적용이 가능하다.

 

아래는 익명 클래스를 람다표현식으로 바꾼 예제이다.

interface TestInterface{
	int calculate(int a, int b);
}


//익명 클래스
TestInterface calculateAdd = new TestInterface() {
	@Override
	public int calculate(int a, int b) {
		return a + b;
	}
};

//람다 표현식
TestInterface calculateAdd = (int a, int b) -> a + b;

람다표현식은 3부분으로 구성되어있다.

  1. 매개변수목록 : (int a, int b)
  2. 화살표 토큰 : ->
  3. 처리 식 : a + b

여기서 메소드가 하나인 인터페이스를 Functional(기능적) 인터페이스라고 부를 수 있다. 

 

인터페이스에 메소드가 두개 이상 존재하게 되면 람다표현식을 사용 할 수 없고 람다표현식을 사용하게되면 컴파일 오류가 발생하게 된다.

 

그래서 자바 8에서는 @FunctionalInterface를 선언하여 해당 인터페이스가 Funtional 인터페이스라고 명시할 수 있다.

 

@FunctionalInterface을 선언한 인터페이스에 메소드를 두개 이상 사용하게 되면 마찬가지로 컴파일 오류가 발생한다.

 

Java 8에서 제공하는 Functional 인터페이스는 java.util.function패키지에서 제공되며 아래와 같은 인터페이스를 가지고 있다.

  • Predicate<T> : T -> boolean
    • test(T t)라는 메소드를 가지고 있으며 boolean을 리턴한다.
  • Supplier<T> : () -> T
    • get() 메소드가 있으면 리턴값은 제네릭으로 선언된 타입을 리턴한다.
  • Consumer<T> : T -> void
    • accept(T t)라는 메소드를 가지고 있고 리턴값이 없다. 출력을 할 때처럼작업을 수행하고 결과가 필요없는 경우에 사용한다.
  • Function<T, R> : T -> R
    • 제네릭으로 두개의 타입을 받는데 T는 입력 타입, R은 출력 타입이다.
    • R apply(T t)형태의 메소드를 가지고 있다.
  • UnaryOperator<T> : T -> T
    • Function인터페이스를 상속받고있다.
    • 한 가지 타입에 대하여 결과값도 같은 타입일 경우 사용한다.
  • BiFunction<T, U, R> : (T t, U u) -> R
    • 제네릭으로 세개의 타입을 받는다, T와 U는 입력 타입이고 R은 출력타입니다.
    • apply 메서드를 사용하면 입력타입인 T와 U를 가지고 로직을 수행하고 R타입으로 리턴할 수 있다.
  • BinaryOperator<T> : (T, T) -> T
    • BiFunction인터페이스를 상속받고있다.
    • UnaryOperator와 마찬가지로 한가지 타입에 대하여 결과값도 같은 타입일 경우 사용한다.

스트림(Stream)

 

Stream은 컬렉션같은 연속적인 정보를 처리하는데 사용되며 함수형 프로그래밍을 지원하기 위한 클래스다.

 

Stream을 사용하면 코드를 좀 더 직관적이고 깔끔하게 작성 할 수 있다.

 

Stream 특징

- 원본 데이터를 변경하지 않고 결과를 새로운 스트림에 반환한다.
- 일회용이다.
- 내부 반복으로 작업을 처리한다.

 

Stream 구조

스트림의 구조는 스트림 생성 - 중간연산 - 종단연산 으로 되어있다.

 

  1. 스트림 생성
    • 컬렉션 목록을 스트림 객체로 변환한다.
    • Stream()메서드를 사용하면 Stream을 반환한다.
  2. 중간 연산
    • 데이터를 가공할 때 사용되며 연산결과로 Stream 타입을 반환한다.
    • 여러개의 중간 연산을 연결할 수 있다.
    • 중간 연산은 상황에따라 생략 할 수 있다.
    • filter(), map(), flatMap(), distinct(), sorted(), peek(), limit(), skip()
  3. 종단 연산
    • 스트림 처리를 마무리하기 위해 사용되며, 숫자값을 리턴하거나 목록형 테이터를 리턴한다.
    • forEach(), toArray(), reduce(), collect(), anyMatch(), findFirst()

 

[예제] 정수형 리스트에서 2를 곱했을때 3의 배수인 값 출력

List<Integer> list = Arrays.asList(1, 20, 45, 99, 3, 4);

list.stream().map(i -> i * 2).filter(i -> i % 3 == 0).reduce(0, (Integer a, Integer b) -> a + b);

 

여기서 마지막에 reduce는 메소드 참조를 통해서 다음과 같이 처리할 수 있다.

 

list.stream().map(i -> i * 2).filter(i -> i % 3 == 0).reduce(0, Integer::sum);

 

만약 위에 식을 함수형 인터페이스를 사용해서 메서드로 만든다면 아래와같이 사용 할 수 있다.

 

public int doubleThreeMultiple(
    List<Integer> list
    , Function<Integer, Integer> multiply
    , Predicate<Integer> multiple
){
	return list.stream()
            .map(multiply)
            .filter(multiple)
            .reduce(0, Integer::sum);
}

 

Stream의 다른 기능들은 따로 테스트를 진행해보면서 비교를 한번 해봐야할거같다