티스토리 뷰

자바 8에는 다양한 기술들을 살펴보려고 하는데, 그 첫 번째는 함수형 인터페이스다.

함수형 인터페이스에 대한 설명

  • 예전부터 사용이 되던 인터페이스
  • 추상 메서드가 딱 하나만 있으면 함수형 인터페이스가 된다.
  • Single Abstract Method(SAM) 인터페이스
  • abstarct는 생략 가능

인터페이스임에도 불구하고 인터페이스 내부에 static 메서드, default 메서드를 다룰 수 있다.

static 메서드나 default 메서드가 있더라도 이 외의 추상 메서드가 단 하나라면 함수형 인터페이스가 된다.

자바가 제공해주는 @FunctionalInterface 애너테이션을 통해 인터페이스를 보다 견고하게 관리할 수 있다.

@FunctionalInterface
public interface DoSomething {
    void print();
}

 

이 애너테이션을 사용하는 이유는 @Override를 사용하는 이유와 비슷하다.

프로그래머의 의도를 명시하는 것으로, 크게 세 가지 목적이 있다.

  1. 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려준다.
  2. 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
  3. 그 결과 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.

그러니 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 애너테이션을 사용하자.

함수형 인터페이스 사용 방법 [1]. 익명 내부 클래스를 활용

public class UseDoSomething {
    public static void main(String[] args) {
        DoSomething doSomething = new DoSomething() {
            @Override
            public void print() {
                
            }
        }
    }
}

함수형 인터페이스 사용 방법 [2]. 람다를 활용

public class UseDoSomething {
    public static void main(String[] args) {
        DoSomething doSomething = () ->
                System.out.println("eric test");

        doSomething.print();
    }
}

// 출력결과
test

한 줄일 경우에만 대괄호까지 생략 가능하다.

위 같은 형태(람다)를 메서드 파라미터로 전달하거나 자체를 리턴도 가능하다.

순수 함수

자바에서 함수형 프로그래밍을 구현하려면 순수 함수여야 한다.

순수 함수란 동일한 값을 넣었을 때 결과가 동일해야 한다.

이것이 보장이 안되면 함수형 프로그래밍이라 보기 어렵다.

보장이 안되는 상황 [1] 함수 안에서 바깥에 있는 값을 사용하는 경우

아래 예제에서는 outValue를 사용했기 때문에 보장이 안 된다고 볼 수 있다.

public class App {
    public static void main(String[] args) {
        int outValue = 10;
        DoSomething doSomething = (int value) -> {
            System.out.println(value + outValue);
        };
        doSomething.print(10);

    }
}

보장이 안되는 상황 [2] 메서드 외부의 값을 변경하려는 경우

public class App {
    public static void main(String[] args) {
        DoSomething doSomething = new DoSomething() {
            int outValue = 10;
            @Override
            public void print(int value) {
                outValue++;
            }
        };

        doSomething.print(10);

    }
}


이렇게 DoSomething 인터페이스 내에 존재하면 문법적으로 가능하지만 다음 코드는 컴파일 에러가 뜬다.

public class App {
    public static void main(String[] args) {
        int outValue = 10;
        DoSomething doSomething = new DoSomething() {
            @Override
            public void print(int value) {
                outValue++;
            }
        };

        doSomething.print(10);

    }
}


위 사진과 같이 final 또는 effective final만 사용이 가능하다고 컴파일 에러 메시지가 뜬다.

🐻
하지만 문법이 허용하므로 ‘무조건 하지마’는 아니고 필요에 따라 사용하면 될 것 같다.

 

함수형 인터페이스 종류

위에서 정의한 DoSomething 인터페이스는 사실 abstract 메서드인 print를 정의할 필요가 없다.

왜냐하면 function 패키지에서 제공하는 Function 함수 인터페이스를 사용하면 된다.

[1]. Interface Function<T,R>

⇒ T 타입을 받아서 R 타입을 리턴하는 함수 인터페이스

  • R apply(T t)

이제 DoSomething 인터페이스를 수정해보자.

import java.util.function.Function;

@FunctionalInterface
public class DoSomething implements Function<Integer, Integer> {
        @Override
        public Integer apply(Integer integer) {
            return integer + 10;
        }
}

혹은 DoSomething이라는 인터페이스를 따로 구현하지 않고 사용하는 방법도 있다.

import java.util.function.Function;

public class App {
    public static void main(String[] args) {
        Function<Integer, Integer> plus10 = (i) -> i + 10;
        System.out.println(plus10.apply(10));
    }
}

Function<T, R> 함수형 인터페이스에는 다음과 같은 함수 조합용 default 메서드가 존재한다.

[1.1]. andThen

import java.util.function.Function;

public class App {
    public static void main(String[] args) {
        Function<Integer, Integer> plus10 = (i) -> i+10;
        Function<Integer, Integer> multiply2 = (i) -> i*2;
        System.out.println(plus10.andThen(multiply2).apply(10));
    }
}
  • plus10을 먼저 한 뒤, multiply2를 실행한다.

[1.2]. compose

import java.util.function.Function;

public class App {
    public static void main(String[] args) {
        Function<Integer, Integer> plus10 = (i) -> i+10;
        Function<Integer, Integer> multiply2 = (i) -> i*2;
        System.out.println(plus10.compose(multiply2).apply(10));
    }
}
  • plus10을 하기 전에, multiply2를 실행한다.

ex) Arrays::asList

[2]. Interface BiFunction<T,U,R>

⇒ Function이 하나의 값(T 타입)을 받는 함수형 인터페이스라면 이건 두 개의 값(T, U 타입)을 받는다.

  • R apply(T t, U t)

[3]. Interface Consumer<T>

⇒ T 타입을 받아서 아무것도 리턴하지 않는 함수 인터페이스

  • void accept(T t)

함수 조합용 메서드로 andThen이 존재한다.

ex) System.out::println

public class App {
    public static void main(String[] args) {
        Consumer<Integer> print = (i) -> System.out.println(i);
        print.accept(10);
    }
}

[4]. Interface Supplier<T>

⇒ T 타입의 값을 제공해주는 함수 인터페이스

  • T get()

ex) Instant::now

public class App {
    public static void main(String[] args) {
        Supplier<Integer> supplier = () -> 10;
        Integer s = supplier.get();
        System.out.println(s);
    }
}

[5]. Interface Predicate<T>

⇒ T 타입을 받아서 boolean을 리턴하는 함수 인터페이스

  • boolean test(T t)

함수 조합용 메서드로 and, or, negate가 존재한다.

ex) Collection::isEmpty

[6]. Interface UnaryOperater<T>

⇒ Function<T, R>의 특수한 형태로, T와 R의 타입이 같을 때 사용하는 함수형 인터페이스

ex) String::toLowerCase

[7]. Interface BinaryOperator<T>

⇒ BiFunction<T,U,R>의 특수한 형태로, T, U, R이 모두 같은 타입일 때 사용하는 함수형 인터페이스

ex) BigInteger::add

 


effective java 아이템 44에서는 표준 함수형 인터페이스를 사용하라고 권고한다.

필요한 용도에 맞는게 있다면 위에서 소개한 7가지의 함수형 인터페이스를 사용하여 다른 코드와의 상호운용성도 높이자.

 

함수형 인터페이스를 API에서 사용할 때의 주의할 점

서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안 된다.

클라이언트에게 불필요한 모호함만 안겨줄 뿐이며, 이 모호함으로 인해 실제로 문제가 일어나기도 한다.

ExecutorService의 submit 메서드는 Callable<T>를 받는 것과 Runnable을 받는 것을 다중 정의했다.

그래서 올바른 메서드를 알려주기 위해 형변환해야 할 때가 종종 생긴다.

이런 문제를 피하는 가장 쉬운 방법은 서로 다른 함수형 인터페이스를 같은 위치의 인수로 사용하는 다중정의를 피하는 것이다.

이는 "다중정의는 주의해서 사용하라"는 effective java 아이템 52 조언의 특수한 예라 할 수 있다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함