[Java] 함수형 인터페이스와 익명 클래스, 람다 표현식을 이용한 구현

Woody의 기록·2023년 7월 27일
2

⛳ Keyword

  • 함수형 인터페이스(Functional Interface)
  • 익명 클래스(Anonymous Class)
  • 람다 표현식(Lambda Expression)

📌 함수형 인터페이스

✔️ 함수형 인터페이스란?

함수형 인터페이스는 추상 메서드가 하나만 존재하는 인터페이스를 의미한다. Default 메서드 또는 Static 메서드의 경우 존재하던 존재하지 않던 무관하다. 오직 추상 메소드가 하나만 존재하는지에 대한 기준으로만 판단한다.

추상 메소드(Abstract Method)란?

추상 메서드는 프로그래밍에서 객체 지향 프로그래밍 언어에서 사용되는 중요한 개념 중 하나이다. 추상 메서드는 구현을 포함하지 않고 선언만 되어 있는 메서드로, 자식 클래스에서 반드시 구현되어야 하는 메소드이다. 자식 클래스에서 이 메서드를 구현하지 않으면 컴파일 오류가 발생한다. 추상 메소드는 인터페이스(Interface)와 추상 클래스(Abstract class)에서 사용된다. 이 두 가지 모두 추상 메서드를 포함할 수 있다.

함수형 인터페이스는 선언시 명시적으로 어노테이션(@FunctionalInterface)을 통해 함수형 인터페이스라는 것을 명시할 수 있다.

✔️ 함수형 인터페이스의 예시

예를 들어 run 이라는 추상 메서드 하나만 정의된 MyRunnable 이라는 인터페이스가 있다고 가정하자. 해당 인터페이스는 추상 메서드를 하나만 가지므로 함수형 인터페이스이다.

@FunctionalInterface
interface MyRunnable{
    void run();
}

조금 더 이해를 돕기 위해 아래의 MyRunnable 1, 2, 3 에서 함수형 인터페이스를 찾아보자.

@FunctionalInterface
interface MyRunnable1{
    void run();
}

@FunctionalInterface
interface MyRunnable2{
    void run();
    default void sayHello(){
        System.out.println("hello");
    }
    default void sayBye(){
        System.out.println("Bye");
    }
}

@FunctionalInterface
interface MyRunnable3{
    void run1();
    void run2();
}

MyRunnable1은 추상 메서드가 run 하나이므로 함수형 인터페이스이다.

MyRunnable2는 메서드가 3개이지만 2개는 Default 메서드이고, 추상 메서드는 1개 뿐이므로 함수형 인터페이스이다.

MyRunnable3는 추상 메서드가 2개이다. 함수형 인터페이스는 추상 메서드가 1개인 인터페이스를 의미하기 때문에 MyRunnable3는 함수형 인터페이스가 아니다. 실제로 IntelliJ에서 MyRunnable3@FunctionalInterface 를 사용하게되면 빨간줄이 생기면서 함수형 인터페이스가 아니라는 문구가 나오게 된다.

✔️ 인터페이스와 구현체

run 메소드에서 Hello Woody! 를 출력하는 MyRunnable 인터페이스의 구현체를 만든다고 가정하면 직관적으로 이렇게 만들 수 있을 것이다.

@FunctionalInterface
interface MyRunnable{
    void run();
}

class MyRunnableImpl implements MyRunnable{
    @Override
    public void run() {
        System.out.println("Hello Woody!");
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnableImpl myRunnableImpl = new MyRunnableImpl();
        myRunnableImpl.run(); // "Hello Woody!"
    }
}

우선 MyRunnable 인터페이스는 구현체가 필요하므로 MyRunnableImpl 이라는 구현체를 만들어주고 MyRunnableImpl의 인스턴스를 만들어서 run 메소드를 호출하는식으로 사용해 볼 수 있을 것이다.

해당 구현체를 단발성으로 사용하고 싶은 경우에 위와 같은 방식으로 구현체 클래스를 따로 생성하여 만들 경우, 구현체를 단 한번만 사용할 것인데도 불구하고 MyRunnableImpl 과 같이 부수적인 임시 클래스가 작성되기 때문에 지저분해지는 것을 볼 수 있다.

그래서 이렇게 단발성으로 사용되는 경우 코드의 복잡해지는 것을 방지하기 위해서 익명 클래스(Anonymous Class) 를 사용하기도 한다.

📌 익명 클래스 (Anonymous Class)

✔️ 익명 클래스(Anonymous Class)란?

익명 클래스(Anonymous Class)는 이름이 없는 클래스로, 클래스의 정의와 동시에 인스턴스를 생성하여 사용하는 방식이다. 익명 클래스는 주로 인터페이스를 구현하거나 추상 클래스를 상속받는 작은 구현체를 생성할 때 사용된다.

✔️ 익명 클래스로 구현한 예시

해당 코드는 동일한 인터페이스(MyRunnable)을 익명 클래스로 구현한 예시이다.

@FunctionalInterface
interface MyRunnable {
    void run();
}

public class Main {
    public static void main(String[] args) {

        MyRunnable myRunnable = new MyRunnable() {
            @Override
            public void run() {
                System.out.println("Hello Woody!");
            }
        };

        myRunnable.run(); // Hello Woody!
    }
}

new MyRunnable(){...} 부분을 확인해보면 인터페이스 이름으로 인스턴스르 생성할 수 없는데 new로 myRunnable 인스턴스를 생성하고 있는 것을 볼 수 있다. 인터페이스는 자신의 추상 메소드를 정의한 구현체를 통해서만 인스턴스화 될 수 있는데 어떻게 가능한걸까?

new MyRunnable() { ... } 부분의 중괄호 내부를 보면, MyRunnable의 추상메서드를 @Override한 구현부를 가지고 있는 것을 볼 수 있다. 구현부를 가지고 있기 때문에 구현체인 것은 맞는데 기존방식처럼 별도의 클래스로 작성한 것이 아니라서 구현체의 이름이 없다. 해당 구현체는 별도의 이름이 없고 인터페이스의 추상 메소드에 대한 구현만 있는 클래스로 볼 수 있다. 따라서 이와 같은 형태를 익명 클래스라고 부른다.

이렇게 익명 클래스를 가지고 작성하면 인터페이스를 사용하는 위치에서 바로 익명클래스로 구현하고 인스턴스화 할 수 있기 때문에 코드가 복잡해지는 것을 방지할 수 있다.

new MyRunnable(){...} 자체가 Instance 이기 때문에 인터페이스 타입 변수에 담지 않고, 아래와 같이 생성한 인스턴스에서 바로 run을 호출해도 무관하다.

public class Main {
    public static void main(String[] args) {
        new MyRunnable() {
            @Override
            public void run() {
                System.out.println("Hello Woody!");
            }
        }.run(); // Hello Woody!
    }
}

이렇게 익명 클래스를 사용하면 좀 더 간단한 코드를 구성할 수 있다.

함수형 인터페이스는 람다 표현식을 이용하면 더 작성할 수 있다.

📌 람다 표현식(Lambda Expression)

✔️ 람다 표현식이란?

람다 표현식(Lambda Expression)은 자바 8부터 도입된 기능으로, 함수형 프로그래밍을 지원하기 위해 사용되는 간결하고 편리한 문법이다. 익명 함수(Anonymous Function)를 작성하는데 람다 표현식이 사용된다. 주로 함수형 인터페이스를 구현하는 데 사용되며, 이는 단 하나의 추상 메서드를 가지는 인터페이스를 말한다.

✔️ 람다 표현식의 기본 문법


(parameter_list) -> {body}
  • parameter_list: 메서드의 매개변수를 나타냅니다. 매개변수가 여러 개일 경우 쉼표(,)로 구분한다. 매개변수의 타입은 컴파일러가 유추할 수 있으므로 생략이 가능하다.
  • ->: 람다 표현식의 화살표(arrow) 기호로, 매개변수와 메서드의 구현부를 분리한다.
  • {body}: 메서드의 구현부 블록을 나타낸다. 중괄호로 묶어서 표현하며, 메서드의 동작을 구현한다.
    • body에 return 문 한줄만 존재하는 경우, 중괄호를 생략가능 하다.

✔️ 람다 표현식을 이용한 인터페이스 구현

아래는 동일한 MyRunnable 인터페이스를 Lambda 표현식을 이용해서 구현한 방식이다.

앞서 나왔던 방식에 비해서 코드가 매우 간결해진 것을 볼 수 있다.

@FunctionalInterface
interface MyRunnable{
    void run();
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = 
                () -> System.out.println("Hello Woody!");
        myRunnable.run();
    }
}

위 코드에서 람다표현식이 사용된 부분은 아래의 Line이다.

() -> System.out.println("Hello Woody!");

앞에서 익명 클래스를 통해 작성했던 것과 비교해보자.

// 익명 클래스 사용한 case
MyRunnable myRunnable = new MyRunnable() {
            @Override
            public void run() {
                System.out.println("Hello Woody!");
            }
   };

// 람다를 사용한 case
MyRunnable myRunnable = () -> System.out.println("Hello Woody!");

전부 생략되고 필요한 파라미터를 표현하기 위한 ( ) 와 Body 인 System.out.println("Hello Woody!"); 만 남은것을 볼 수 있다.

함수형 인터페이스는 추상 메소드가 하나만 존재하기 때문에 함수형 인터페이스에 람다식을 사용하면 명시하지 않아도 해당 메소드를 의미하게 된다. 위의 예시의 경우 MyRunnable 인터페이스가 추상 메소드를 run() 밖에 가지지 않는 함수형 인터페이스이기 때문에, 람다표현식 부분은 인터페이스에 딱하나 존재하는 추상 메소드인 run() 대한 구현을 의미하게 된다.

run은 아무것도 인자로 받지 않기 때문에 람다 표현식의 파라미터 부분은 ()가 되고, Body 부분에는 Hello Woody! 를 출력하는 코드를 넣어주었다. 인터페이스에 정의된 run 메소드가 void 타입이기 때문에 별도의 리턴값도 가지지 않으므로 위와 작성된다.

이렇게 함수형 인터페이스를 사용할 때 람다 표현식을 사용하면 코드가 간결하게 작성되기 때문에 같이 사용되는 경우가 많다.

람다 표현식에 대한 부분은 별도의 포스팅으로 자세히 다룰 예정이다.

profile
Github - https://www.github.com/woody35545

0개의 댓글