ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 람다 1편) 람다의 시작 , 함수형 인터페이스 (@FunctionalInterface)
    Java 2023. 9. 14. 17:34

    자바 8에는 새로운 기능들이 많이 추가되었는데, 그중 많이 쓰이는 람다에 대해 정리해보려한다.

     

    본인도 람다에 대해 이해중인 과정이기에, 최대한 쉽게 풀어쓰는데 중점을 두려한다.

     

    람다식을 '왜 쓰는지' 에 대해 먼저 살펴보자.

     

    1. 먼저 람다를 쓰지 않는 경우

    interface A {
        void method1(String k );
    }
    
    class TestApplication {
    
        public static void main(String[] args) {
          	A a = new A(){
    
                @Override
                public void method1(String k) {
                    System.out.println(k);
                }
            };
    
            a.method1("test1");
    
        }
    }

    A클래스를 오버라이딩하는 B클래스를 생성했다. 

     

    그렇다면 람다(람다 함수 공식은 일단 제쳐두자)를 쓴 경우는?

     

    interface A {
        void method1(String k );
    }
    
    class TestApplication {
    
        public static void main(String[] args) {
    
            A a = (k) -> {System.out.println(k)};
            a.method1("test1");
    
        }
    }

    훨씬 간결해졌음을 볼 수 있다.

    여기에 람다의 장점을 극대화하는 Stream(역시 자바8 출시) 기능이 있다.(추후에 Stream에 대해서도 알아볼것이다)

     

    이제 람다를 어떻게 사용할 수 있는것인지 알아보자.

     

    람다식 정의

    메서드를 하나의 "식(expression)"으로 표현한 것

    람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, "익명함수" 라고도 한다.

    즉, 함수명을 선언하고 사용하는 것이 아닌 식별자 없이 실행가능한 함수다.

     

    A a = (k) -> {System.out.println(k)}; //위에서 실행한 람다식

     

    정말로 메서드 명도 없고 반환값도 안보인다. 이게 무슨 메서드인지 어떻게 알고 실행하지?

     


    키는 함수형 인터페이스에 

     

    람다식을 사용하기 위한 가장 중요한 조건은 '함수형 인터페이스'다.

     

    - @FunctionalInterface 

    An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. Conceptually, a functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract. If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface's abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere. 

    함수형 인터페이스는 오직 1개만의 추상메서드를 갖고있다. static메서드나 default 메서드의 개수와는 무관하다.

     

    @FunctionalInterface를 이용하면 친절하게 컴파일에러까지 체크해준다.

     

    A a = (k) -> {System.out.println(k)}; //위에서 실행한 람다식

    추상메서드를 오직 1개만 갖고있는 인터페이스를 이용해서 람다식을 만들기 때문에 

    어떤 메서드를 실행시키고싶고, return type이 무엇인지 이미 알고 있는것이었다.

     

    람다식을 세우는 방식은
    
    () -> {}   구조다.
    
    소괄호()는 넘겨주는 파라미터 변수고
    중괄호{}는 메서드의 body 부분이다.
    
    소괄호는 파라미터가 없을경우엔 (), 1개인 경우에만 생략가능하고, 2개인상인 경우 ()필수다.
    중괄호는 body부분이 한줄이면 생략가능하고, 초과시 필수작성이다.
    
    ex)  A a = k -> System.out.println(k);
    ex)  A a = (a,b)->{ 
    			System.out.println(a);
    			System.out.println(b);
                       }

     


    함수형 인터페이스를 통해 람다식을 세울 수 있음을 알아보았다.

     

    실무에서는 자바(8이상)에서 제공하는 함수형 인터페이스들을 많이 사용한다고 한다. 

     

    자바에서 제공하는 함수형 인터페이스들 중 자주쓰이는 4가지에 대해 정리해보려한다.

     

    위에처럼 람다식에서 중요한 요소는 parameter type과 return type 두가지다보니, 함수형 인터페이스들에도 대부분 반영되어있다.

     

     

    1. Supplier 

     

     

    Supplier의 abstract method 는 get()이다. (사실 이름은 몰라도 된다)

    파라미터는 없고, return type만 명시되어있다.

     

    2. Consumer

    Consumer의 abstract method는 accpet()다.

    파라미터는 명시되어있고, return type이 void다.

     

    3. Function

     

    abstract method는 apply()다.

    파라미터와 return type모두 명시되어있다.

     

    4. Predicate

     

    abstract method는 test()다.

    파라미터는 제네릭 처리되어있고,  return type은 boolean으로 고정이다.

     

     


    그래서 저것들이 언제 쓰이는데?

    이게 제일 궁금했다.

    블로그를 참조해서 공부할때 해당 인터페이스들을 대부분 정리하셨는데 정작 언제 쓰이는지가 없어서 개념으로만 중요한건가...했는데

     

    바로 Stream 기능을 이용할때 해당 인터페이스들이 쓰이는것이었다.

     

    Stream에는 여러 메서드들이 있는데, 그 중 자주 쓰고 위 4개 인터페이스들을 활용하는 것들만 가져와봤다.

     

     

    Stream의 자주쓰이는 4개의 메서드들에는 파라미터로 해당 인터페이스들을 활용하고 있었다. 

    어떻게 활용하는지 예제를 통해 알아보자.

            Arrays.asList("A", "AB", "ABC", "ABCD", "ABCDE", "ABCDE")
                    .stream()
                    .map((str) -> str.length())  // 결과: 1,2,3,4,5,5
                    .filter((len) -> len % 2 == 1) //필터링: 1,3,5,5 통과
                    .collect(Collectors.toSet()) //결과: 1,3,5 (set처리)
                    .forEach(i-> System.out.println(i));

     

    1.  map() 은 Function인터페이스를 파라미터로 받는다.

     

    map(Function<String,Integer> mapper = (str) -> str.length()) 으로 풀어쓸 수 있다. 

     

    str이라는 파라미터가 있고,  첫 "A"의 길이인 1이 return  된다.

     

    이때 Stream 데이터도 return (Integer type)과 같아진다. (map 메서드 특성)

     

     

    2. filter() 는 Predicate인터페이스를 받는다.

     

    filter(Predicate<Integer>predicate = (len)-> len%2==1) 으로 풀어쓸 수 있다.

     

    len이라는 파라미터가 있고, boolean 타입을 return 한다.

     

    넘어가는 Stream은 파라미터 값(len)을 넘긴다.

     

     

    3. collect()는 Supplier인터페이스를 받는다.

     

    collect(Supplier<Set>supplier = Collectors.toSet()) 으로 풀어쓸 수 있다.

     

    파라미터는 없고, Stream으로 넘어온 len이 Set으로 바뀐 상황이다.

     

    그리고 더이상 stream값으로 넘기지 않고, 그냥 Set<Integer>로 return 된 상황이다.

     

     

    ** collect()에서 stream값이 넘어가지 않아 위의 예제는 Iterable의  forEach()다. 

    따라서 위의 collect()가 없는 상태. 즉 2번으로 돌아가서 filter()까지만 하는 경우 예를 들겠다.

     

    4. forEach()는  Consumer인터페이스를 받는다.

     

    Stream으로 각각 1,3,5,5 가 넘어온 상황이다.

     

     

    forEach(Consumer<Integer>action = (i) -> System.out.println(i);); 로 풀어쓸 수 있다.

    파라미터만 있고 return 값은 없다.  

     

    print가 실행되고 Stream은 종료된다.

     


    stream 메서드를 몇번 써봤지만 원리를 모른채 사용해서

    이런 인터페이스들이 숨어있다는것을 처음 알았다.

     

    함수형 인터페이스들은 어렵게 숨어있지만

    그만큼 복잡한것을 가독성 좋게  풀어낼 수 있다는 장점이 있다.

     

    해당 글에서는 '함수형 인터페이스'를 중점으로 풀어냈고, 다음 글에선 Stream에 대해 더 깊게 파고들려한다.

     

     

     

    Reference

     

    - https://hudi.blog/functional-interface-of-standard-api-java-8

    - https://mangkyu.tistory.com/113

    - https://hbase.tistory.com/171

    - https://developer-talk.tistory.com/460

     

     

     

Designed by Tistory.