본문 바로가기

Java

익명 클래스 / 람다식 / 메소드 참조

람다식을 알기 전에 익명 클래스부터 짚고 넘어가자. (처음 부분은 정리가 뒤죽 박죽이니 1번부터 보셔도 됩니다.)

 

익명 클래스는 이름이 없는 클래스로 보통 한 번만 사용되는 클래스를 정의할 때 사용됩니다. 이름이 없다는 건 클래스 정의에 이름이 붙지 않았다는 의미입니다. 익명클래스는 주로 인터페이스나 추상 클래스를 구현할 때 사용 된다고 하는데 예시를 보죠.

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Runnable: " + i);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable task = new MyRunnable();
        Thread thread = new Thread(task);
        thread.start();
    }
}

 

이게 기존에 우리가 클래스를 선언해서 사용하던 방식입니다. 이걸 익명 클래스로 작성해 볼게요.

public class Main {
    public static void main(String[] args) {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("Runnable: " + i);
                }
            }
        };

        Thread thread = new Thread(task);
        thread.start();
    }
}

 

 

가장 큰 차이가 익명 클래스는 클래스를 정의함과 동시에 객체를 생성한다는 것입니다. 

 

 

여기서 익명 클래스를 더 간결하게 작성하게 나온 게 람다식입니다. 

 

람다식이란 함수를 따로 만들지 않고 코드 안에서 함수를 써서 호출하는 방식으로 식별자 없이 실행가능한 함수입니다. 

 

많이 쓰는지는 모르겠어요. 장단점이 존재하니까 저도 학교 다닐 때는 안 썼던 거 같은데.. (아닌가? 또 기억이 안 나는 건가)

 

람다식의 아주아주 기본 형태는 (element) -> {}입니다. 

 

(element) 이게 매게 변수와 같고, {}이 안에 내용이 함수 실행 내용과 같죠.

 

위의 익명 클래스 예제를 람다식으로 다시 바꿔볼게요.

public class Main {
    public static void main(String[] args) {
        // Runnable 인터페이스를 구현하는 람다식
        Runnable task = () -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("Runnable: " + i);
            }
        };

        Thread thread = new Thread(task);
        thread.start();
    }
}

 

간단히 함수를 사용할 수 있는 장점만 있는 건 아니고 단점도 있습니다. 재사용이 불가하고, 디버깅이 다소 까다롭다고 합니다. 

 

다시 제대로 정리해 볼게요.

 

1. 람다 표현식

 

자바는 객체지향 프로그래밍 언어로, 메서드를 사용하려면 클래스의 일부로 정의되어야 했습니다. 이런 불필요한 클래스 정의가 많아지는 문제를 해결하려고 나온 게 익명 클래스입니다. 클래스 이름이 없는 클래스로, 클래스 정의와 생성을 동시에 수행할 수 있습니다. 하지만 코드의 가독성이 떨어지는 문제가 있었고, 이를 해결하고자 람다 표현식이 나오게 되었습니다.

 

메수트 타입, 메서드 이름, 매개변수 타입, 중괄호, return 문을 생략한 표현식이죠.

interface Adder{
   int add(int a, int b);
}

public class Main{
   public static void main(String[] args){
      Adder adder = (a, b) -> a + b;
      
      int sum = adder.add(1,3); // 4
   }
}

 

예제를 보면 Adder 인터페이스의 추상 메서드 add를 람다식으로 짧게 구현해 놨습니다. 아무거나 다 람다식으로 구현할 수 있는 게 아니라 함수형 인터페이스만 람다 표현식으로 구현이 가능합니다.

 

1-1. 함수형 인터페이스

 

함수형 인터페이스는 단 하나의 추상 메서드를 가지는 인터페이스입니다. @FunctionalInterface 어노테이션을 사용해서 명시적으로 표현할 수 있습니다.

@FunctionalInterface
interface Adder {
    int add(int a, int b);
}

 

어노테이션은 선택 사항이지만 2개 이상의 메서드 선언 시 컴파일러가 오류를 발생시켜 주기 때문에 붙이는 게 권장됩니다. 

왜? 왜 함수형 인터페이스만 가능할까요? 

람다식은 하나의 메서드 정의를 나타내기 때문에 이를 할당할 수 있는 타입은 단 하나의 추상 메서드를 가진 인터페이스여야 하고 이를 함수형 인터페이스라 부르는 것입니다. 

만약 2개 이상의 추상 메서드를 가지고 있다면 람다식으로 구현할 때 어떤 메서드를 구현하는지 알 수가 없기 때문이죠.

 

자바 표준 라이브러리에는 다양한 함수형 인터페이스가 포함되어 있는데 자주 사용되는 인터페이스만 잠깐 알아보면

// Predicate<T> // 매개 변수 값을 받아 boolean 값 반환
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.print(isEven.test(4)) // true

// Function<T, R> // 하나의 입력, 하나의 출력
Function<String, Integer> f = s -> s.length();
System.out.print(f.apply("hello")); // 5

// Consumer<T>  // 하나의 입력, 출력 X
Consumer<String> consumer = s -> System.out.print(s);
consumer.accept("Hello"); // Hello

// Supplier<T> // 입력 X, 값 반환
Supplier<Double> random = () -> Math.random();
System.out.print(random.get()); // 랜덤 값

// BiFunction<T, U, R> // 2개의 입력, 하나의 출력
BiFunction<Integer, Integer, Integer> adder = (a,b) -> a + b;
System.out.print(adder.apply(1,2)); // 3

// UnaryOperator<T> // 입력과 출력이 같은 타입
UnaryOperator<Integer> square = n -> n * n;
System.out.print(square.apply(2)); // 4

// BinaryOperator<T> // 두 입력과 출력이 같은 타입
BinaryOperator<Integer> amx = (a,b) -> a > b ? a : b;
System.out.print(max.apply(5,3)); // 5

 

많죠..? 저도 알아보면서 처음 봤어요 ㅎ... UnaryOperator은 Function의 특수한 타입, BinaryOperator은 BiFunction의 특수한 형태입니다.

 

1-2. 람다식 수행 단계

 

뭐 다 생략했는데 어떻게 구현이 되는 건가 싶지 않나요? 

  • 람다식이 할당된 변수가 함수형 인터페이스 타입인지 확인한다.
  • 람다식의 매개변수와 반환 타입은 인터페이스(Adder)의 메서드(add)를 기반으로 추론된다.
  • 컴파일러가 람다식을 익명 클래스나 메서드 참조로 변환하여 처리한다.
  • 람다식이 함수형 인터페이스의 인스턴스로 사용되면, 해당 인스턴스의 추상 메서드를 호출하여 실행한다.

타입을 다 생략해도 되는 이유가 컴파일러 스스로 타입을 유추하기 때문이다! 

 

* 타입 추론이 여러 개의 가능성을 가지거나 불명확한 경우에는 명시적으로 타입을 지정하는 게 좋습니다. 

 

2. 메서드 참조

 

람다식을 접하다 보면 String::length 이런 거 본 적 없나요? 람다식에서 더 간단하게 표현하기 위해서 나온 기능입니다.. ㅎ.. 나중에는 다 줄여서 한 글자로 되겠어요... 메서드 참조는 -> 화살표가 없어졌다고 생각하시면 돼요. 매개변수가 제거됐다고 할 수 있겠네요. 어떻게 가능하냐..? 컴파일러가 추론하기 때문에 ㅎ..

 

메서드 참조는 4가지 유형으로 나뉜다고 합니다.

 

2-1. 정적 메서드 참조

Function<String, Integer> stringToInteger = Integer::parseInt
System.out.println(stringToInteger.apply("123")); // 123

 

* 원래는 = s -> Integer.parsInt(s); 이랬던게 Integer::parseInt로 간단해졌네요.

 

2-2. 인스턴스 메서드 참조

BiFunction<String, String, Boolean> stringEquals = String::equals;
System.out.println(stringEquals.apply("test", "test")); // true

 

2-3. 특정 객체의 메서드 참조

public class Main {
    public static void main(String[] args) {
        Main obj = new Main();
        Supplier<String> stringSupplier = obj::toString;
        System.out.println(stringSupplier.get()); // Hello
    }

    @Override
    public String toString() {
        return "Hello";
    }
}

 

2-4. 생성자 참조

Function<String, StringBuilder> str = StringBuilder::new;
System.out.println(str.apply("Hello")); // Hello

 

* 생성자 참조는 매개변수에 따른 생성자에 따라 접근하게 됩니다.

 

낯선게 많아서 바로바로 써먹지는 못할거 같은데 이런게 있고 이런 원리고 이렇게 쓰는거구나 정도로 알아봤습니다.


 

참고 출처

☕ 람다 표현식(Lambda Expression) 완벽 정리 (tistory.com)

 

☕ 람다 표현식(Lambda Expression) 완벽 정리

람다 표현식 (Lambda Expression) 람다 표현식(lambda expression)이란 함수형 프로그래밍을 구성하기 위한 함수식이며, 간단히 말해 자바의 메소드를 간결한 함수 식으로 표현한 것이다. 지금까지 자바에

inpa.tistory.com

 

'Java' 카테고리의 다른 글

Logger 딱 대  (2) 2024.06.17
Set / Map / Iterator  (0) 2024.06.14
형 변환 / valueOf() / stream  (0) 2024.06.07
String / StringBuilder / StringBuffer  (1) 2024.06.07
static과 final  (1) 2024.06.05