람다, 함수형 인터페이스, 메서드 참조
0. 개요
- 이번 포스팅에서는 Java 8부터 도입된 함수형 인터페이스, 람다 표현식, 그리고 메서드 참조에 대해 알아보겠습니다.
- 이들은 Java에서 함수형 프로그래밍을 가능하게 하는 핵심 요소로, 코드의 간결성과 가독성을 크게 향상시켜 줍니다.
- 문법적 설탕에 해당하는 요소들이라서 다른 방식으로도 충분히 구현 가능하지만, 코드의 간결성과 가독성을 크게 향상시키기 위해 주로 쓰이는 문법들입니다.
- 람다 표현식
- 함수형 인터페이스의 구현을 더 간단하게 표현할 수 있는 방법입니다.
- 익명 클래스를 대체하여 가독성 높은 코드를 작성할 수 있도록 도와주며, 다양한 상황에서 효율적으로 사용할 수 있습니다.
- 함수형 인터페이스
- 하나의 추상 메서드를 가진 인터페이스로, Java에서 람다 표현식의 기반이 되는 요소입니다.
- 함수형 인터페이스의 개념과 함께, 자바에서 기본으로 제공하는 함수형 인터페이스들에 대해서 다룹니다.
- 메서드 참조
- 람다 표현식을 더욱 간단하게 표현하는 방식입니다.
- 메서드를 직접 참조하여 더 직관적이고 간결한 코드를 작성할 수 있습니다.
1. 람다 표현식 (Lambda Expression)
- 람다 표현식은 Java 8부터 도입된 기능으로, 함수형 프로그래밍을 가능하게 하며, 코드의 간결성을 크게 향상시킵니다.
- 람다 표현식은 익명 클래스를 대체할 수 있으며, 함수형 인터페이스와 함께 사용됩니다.
1.1 람다 표현식의 정의
- 람다 표현식은 메서드처럼 보이지만, 익명 함수라고 할 수 있습니다.
- 이를 통해 메서드를 단순히 인수로 전달하거나, 메서드처럼 사용할 수 있습니다.
- 람다 표현식의 기본 문법은 아래와 같습니다.
(parameters) -> expression
(parameters) -> { statements }
- 람다 표현식은 매개변수 목록, 화살표 (
->) 그리고 메서드 본문으로 구성됩니다.
1.1.1 기본 예제
(int a, int b) -> a + b
- 위의 예제에서 (int a, int b)는 매개변수 목록이고, a + b는 메서드 본문으로, 두 정수를 더하는 기능을 제공합니다.
- 이때 람다 표현식은 함수형 인터페이스에 종속되어 사용됩니다.
- 함수형 인터페이스는 아래에서 자세히 다룹니다.
1.1.2 파라미터와 리턴 타입 생략 가능한 경우
- Java는 타입 추론을 통해, 람다 표현식에서 파라미터의 타입을 생략할 수 있습니다.
- 또한, 람다 표현식이 하나의 문장만을 포함하는 경우, 중괄호와 return 키워드도 생략 가능합니다.
(a, b) -> a + b
s -> System.out.println(s);
1.2 람다 표현식과 함수형 인터페이스
- 람다 표현식은 함수형 인터페이스를 구현할 때 사용됩니다.
- 함수형 인터페이스는 오직 하나의 추상 메서드를 가지며, 그 메서드의 구현을 람다 표현식으로 할 수 있습니다.
- 다음은
Runnable 인터페이스를 람다로 구현한 예시입니다.
Runnable runnable = () -> System.out.println("Running");
runnable.run();
- Runnable 인터페이스는 run()이라는 하나의 메서드를 가지므로, 이를 람다 표현식으로 구현할 수 있습니다.
1.3 람다 표현식의 다양한 형태
- 람다 표현식은 다양한 형태로 사용될 수 있으며, 코드를 간결하게 작성하는 데 큰 역할을 합니다.
1.3.1 매개변수가 없는 람다 표현식
- 매개변수가 없는 경우, 빈 괄호 ()를 사용합니다.
() -> System.out.println("No parameters");
1.3.2 하나의 매개변수를 가진 람다 표현식
- 하나의 매개변수를 가질 경우, 괄호를 생략할 수 있습니다.
s -> System.out.println(s);
1.3.3 여러 매개변수를 가진 람다 표현식
- 매개변수가 여러 개일 경우, 괄호로 묶어 사용해야 합니다.
(a, b) -> System.out.println(a + b);
1.3.4 블록을 사용하는 람다 표현식
- 람다 표현식이 여러 문장을 포함하는 경우, 중괄호
{}를 사용해야 합니다.
(int a, int b) -> {
int result = a + b;
System.out.println(result);
return result;
};
1.4 람다 표현식의 장점
- 코드의 간결성: 익명 클래스에 비해 훨씬 짧고 직관적인 코드를 작성할 수 있습니다.
- 가독성 향상: 복잡한 메서드 호출을 간단하게 표현할 수 있습니다. (다만 복잡한 코드는 가독성이 오히려 떨어질 수 있습니다)
- 병렬 프로그래밍에 유리:
Stream API와 결합하여 병렬 처리를 간단하게 구현할 수 있습니다.
1.5 람다 표현식과 익명 클래스 비교
- 람다 표현식은 익명 클래스와 비슷한 역할을 하지만, 몇 가지 차이가 있습니다.
- 타입 추론: 람다 표현식은 컴파일러가 타입을 추론하여 코드를 간결하게 작성할 수 있지만, 익명 클래스는 명시적으로 타입을 선언해야 합니다.
- 간결성: 익명 클래스에 비해 코드가 매우 간결합니다.
- this 참조: 람다 표현식 내에서의 this는 람다를 감싸고 있는 외부 객체를 가리키지만, 익명 클래스에서는 익명 클래스 자신을 가리킵니다.
예시
Runnable runnable1 = new Runnable() {
@Override
public void run() {
System.out.println("Running");
}
};
Runnable runnable2 = () -> System.out.println("Running");
- 익명 클래스를 사용할 때는
new 키워드를 사용해 객체를 생성하고 메서드를 오버라이드해야 하지만, 람다 표현식은 단순한 화살표(->) 구문으로 동일한 작업을 수행할 수 있습니다.
2. 함수형 인터페이스 (Functional Interface)
- 함수형 인터페이스(Functional Interface)는 단 하나의 추상 메서드를 가진 인터페이스입니다.
- Java 8부터 도입된 이러한 인터페이스는 주로 람다 표현식과 함께 사용되며, Java에서 함수형 프로그래밍을 가능하게 해주는 중요한 요소입니다.
2.1 함수형 인터페이스의 정의
- 함수형 인터페이스는 단 하나의 추상 메서드만을 가져야 합니다.
- 이러한 특성 덕분에, 함수형 인터페이스는 바로 람다 표현식으로 구현할 수 있습니다.
- Java에서는
@FunctionalInterface Annotation을 사용하여 정의할 수 있습니다.
- 이는 함수형 인터페이스임을 명시적으로 나타내기 위한 Annotation입니다.
- 이 Annotation은 인터페이스가 단일 추상 메서드만을 가지도록 강제하는 역할을 합니다.
- 이 Annotation은 선택 사항이지만, 함수형 인터페이스임을 컴파일러가 검증하도록 도와줍니다.
예시
@FunctionalInterface
interface MyFunctionalInterface {
void myMethod();
}
- 위의 코드에서
MyFunctionalInterface는 단 하나의 추상 메서드인 myMethod()를 가지고 있기 때문에 함수형 인터페이스입니다.
- 여기에
@FunctionalInterface 애너테이션을 붙이면, 추가적인 추상 메서드가 선언되었을 때 컴파일 오류가 발생하게 됩니다.
2.2 함수형 인터페이스와 람다 표현식의 관계
- 함수형 인터페이스는 람다 표현식의 기초가 됩니다.
- 함수형 인터페이스는 람다 표현식을 통해 구현할 추상 메서드를 정의합니다.
MyFunctionalInterface example = () -> System.out.println("Hello, Lambda!");
example.myMethod();
- 이 예제에서 함수형 인터페이스
MyFunctionalInterface를 람다 표현식으로 구현했습니다.
myMethod() 메서드를 실행하면 Hello, Lambda!가 출력됩니다.
- 익명 클래스와 달리, 람다 표현식은 매우 간결하고 직관적인 형태로 코드를 작성할 수 있습니다.
- 물론 익명클래스로도 동일하게 정의하여 사용할 수 있습니다.
2.3 자바에서 제공하는 주요 함수형 인터페이스
- Java 8부터 자바 표준 라이브러리에는 다양한 기본 함수형 인터페이스가 포함되어 있습니다.
- 이들은 모두
@FunctionalInterface로 정의되어 있으며, 여러 상황에서 유용하게 사용할 수 있습니다.
2.3.1 Runnable
- 인자를 받지 않고 반환값도 없는
run() 메서드를 가지고 있습니다.
java.lang 패키지에 소속되어 있어서 별도의 import 없이 바로 사용 가능합니다.
- 주로 쓰레드 작업에서 사용됩니다.
Runnable runnable = () -> System.out.println("Running");
runnable.run();
2.3.2 Supplier<T>
- 인자를 받지 않고, T 타입의 값을 반환하는
get() 메서드를 가지고 있습니다.
java.util.function 패키지에 소속되어 있습니다.
- 데이터를 생성하거나 제공하는 용도로 자주 사용됩니다.
Supplier<String> supplier = () -> "Supplied Value";
System.out.println(supplier.get());
2.3.3 Consumer<T>
- 인자를 받고, 반환값이 없는
accept() 메서드를 가지고 있습니다. (void)
java.util.function 패키지에 소속되어 있습니다.
- 주어진 데이터를 소비하는 작업(예: 출력, 저장 등)에 사용됩니다.
Consumer<String> consumer = (message) -> System.out.println(message);
consumer.accept("Hello, Consumer!");
2.3.4 Function<T, R>
T 타입의 인자를 받고, R 타입의 결과를 반환하는 apply() 메서드를 가지고 있습니다.
java.util.function 패키지에 소속되어 있습니다.
- 주로 데이터를 변환하는 작업에 사용됩니다.
Function<Integer, String> function = (number) -> "Number: " + number;
System.out.println(function.apply(5));
2.3.5 Predicate<T>
T 타입의 인자를 받고, boolean 값을 반환하는 test() 메서드를 가지고 있습니다.
java.util.function 패키지에 소속되어 있습니다.
- 주어진 조건에 맞는지 여부를 판별할 때 사용됩니다.
Predicate<Integer> isPositive = (num) -> num > 0;
System.out.println(isPositive.test(10));
System.out.println(isPositive.test(-10));
2.3.6 그외 함수형 인터페이스
UnaryOperator<T>
Function<T,T>를 상속 받는 인터페이스입니다.
- 타입
T 인자를 받아서 동일한 T 타입을 반환합니다.
BiConsumer<T,U>
- 타입
T와 타입 U 두개의 인자를 받아서 반환값 없이 소비합니다.
BiFunction<T,U,R>
- 타입
T와 타입 U 두개의 인자를 받아서 타입 R을 반환합니다.
Bipredicate<T,U>
- 타입
T와 타입 U 두개의 인자를 받아서 boolean 값을 반환합니다.
BinaryOperator<T>
BiFunction<T,T,T>를 상속 받는 인터페이스입니다.
- 타입
T 인자 두개를 받아서 동일한 T 타입을 반환합니다.
2.4 사용자 정의 함수형 인터페이스
- 필요에 따라 사용자 정의 함수형 인터페이스를 만들 수도 있습니다.
- 함수형 인터페이스는 특정 작업을 더 명확하게 표현하거나, 커스텀한 로직을 위한 추상화를 제공하는 데 유용합니다.
@FunctionalInterface
interface Adder {
int add(int a, int b);
}
public class Main {
public static void main(String[] args) {
Adder adder = (a, b) -> a + b;
System.out.println(adder.add(10, 20));
}
}
- 이 코드에서
Adder라는 함수형 인터페이스를 정의하고, 이를 람다 표현식으로 구현하여 두 정수를 더하는 작업을 수행합니다.
2.5 디폴트 메서드와 정적 메서드
- Java 8 이후부터 인터페이스에 디폴트 메서드와 정적 메서드를 선언할 수 있습니다.
- 디폴트 메서드는 기본 구현을 제공할 수 있으며, 정적 메서드는 객체 생성 없이 호출할 수 있습니다.
- 함수형 인터페이스의 정의에서 디폴트 메서드와 정적 메서드는 추상 메서드로 간주되지 않으므로 한개의 메서드만 포함해야한다는 함수형 인터페이스의 규칙을 위반하지 않습니다.
@FunctionalInterface
interface Calculator {
int calculate(int x, int y);
default void printMessage() {
System.out.println("This is a default method.");
}
static void staticMethod() {
System.out.println("This is a static method.");
}
}
- 위 예제에서
Calculator는 함수형 인터페이스이지만, 디폴트 메서드와 정적 메서드를 포함하고 있어도 문제가 되지 않습니다.
3. 메서드 참조 (Method Reference)
- 메서드 참조(Method Reference)는 람다 표현식을 더욱 간결하게 표현할 수 있는 방식입니다.
- 람다 표현식이 단순히 메서드 호출을 수행하는 경우, 메서드 참조를 사용하여 코드를 간단하고 읽기 쉽게 만들 수 있습니다.
- 메서드 참조는
:: 연산자를 사용하여 클래스 또는 객체의 메서드를 직접 참조합니다.
3.1 메서드 참조의 기본 형태
- 메서드 참조는 주로 다음과 같은 네 가지 형태로 나뉩니다.
- 정적 메서드 참조
- 인스턴스 메서드 참조 (객체::인스턴스 메서드)
- 특정 객체의 인스턴스 메서드 참조
- 생성자 참조 (Class::new)
3.1.1 정적 메서드 참조
- 클래스의 정적(static) 메서드를 참조하는 방법입니다.
- 형식:
클래스::메서드명
Function<Integer, String> intToString = String::valueOf;
System.out.println(intToString.apply(123));
- 위 예제에서는
String 클래스의 valueOf 정적 메서드를 참조하여, 정수를 문자열로 변환하는 작업을 수행하고 있습니다.
3.1.2 인스턴스 메서드 참조
- 특정 객체의 인스턴스 메서드를 참조하는 방법입니다.
- 형식:
객체::메서드명
String str = "Hello, World!";
Supplier<Integer> stringLength = str::length;
System.out.println(stringLength.get());
- 이 예제에서는 문자열 객체
str의 length 메서드를 참조하여 문자열의 길이를 반환하는 메서드 참조를 사용하고 있습니다.
3.1.3 임의 객체의 인스턴스 메서드 참조
- 특정 클래스의 인스턴스 메서드를 참조할 때, 그 객체는 람다 표현식의 매개변수에서 전달됩니다.
- 형식:
클래스::메서드명
Function<String, Integer> stringLength = String::length;
System.out.println(stringLength.apply("Java"));
- 이 예제에서는
String 클래스의 length() 메서드를 참조하여, 매개변수로 전달된 문자열의 길이를 반환합니다.
3.1.4 생성자 참조
- 생성자를 참조할 때는 클래스명 뒤에 ::new를 붙여서 사용합니다.
- 형식:
클래스명::new
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get();
- 위 예제에서는 ArrayList의 생성자를 참조하여 새로운 리스트를 생성하는 메서드 참조를 사용하고 있습니다.
3.2 메서드 참조와 람다 표현식의 차이
- 메서드 참조는 람다 표현식과 같은 기능을 제공하지만, 코드를 더 간결하게 만들 수 있습니다.
람다 표현식
Function<String, Integer> stringLength = s -> s.length();
System.out.println(stringLength.apply("Java"));
- 람다 표현식으로 작성된 코드가 단순히 메서드를 호출하는 것이라면, 메서드 참조로 대체할 수 있습니다.
메서드 참조
Function<String, Integer> stringLength = String::length;
System.out.println(stringLength.apply("Java"));
- 코드 간결성: 람다 표현식보다 더 간단하게 메서드를 참조할 수 있어 코드를 간결하게 작성할 수 있습니다.
- 가독성 향상: 람다 표현식이 길어질 경우 메서드 참조를 통해 코드의 가독성을 높일 수 있습니다.
3.3 메서드 참조 사용 시 주의점
- 메서드 참조는 람다 표현식과 유사하지만, 모든 경우에 적합한 것은 아닙니다.
- 람다 표현식이 더 직관적이거나 이해하기 쉬운 경우, 메서드 참조 대신 람다를 사용하는 것이 좋습니다.
- 특히, 복잡한 로직을 포함하거나 여러 작업을 수행해야 하는 경우에는 메서드 참조보다는 람다 표현식이 더 적합할 수 있습니다.
3.4 메서드 참조의 활용 예시
문자열 리스트 정렬
List<String> names = Arrays.asList("Kim", "Lee", "Park");
names.sort(String::compareToIgnoreCase);
System.out.println(names);
String::compareToIgnoreCase 메서드 참조를 사용하여 리스트를 대소문자 구분 없이 정렬하는 예제입니다.
생성자 참조를 사용한 객체 생성
Function<String, Person> personCreator = Person::new;
Person person = personCreator.apply("Alice");
System.out.println(person.getName());
Person 클래스의 생성자를 메서드 참조로 사용하여, 새로운 Person 객체를 생성하는 예제입니다.
마무리
- 이번 포스팅에서는 Java 8에서 도입된
람다 표현식, 함수형 인터페이스, 그리고 메서드 참조에 대해 다뤘습니다.
- 이들은 Java에서 함수형 프로그래밍을 가능하게 하며, 코드의 간결성, 가독성, 유지보수성을 크게 향상시키는 중요한 요소들입니다.
- 람다 표현식은 함수형 인터페이스를 기반으로, 더 간결하고 직관적으로 코드를 작성할 수 있게 도와줍니다.
- 익명 클래스의 대체로 사용되며, 코드의 복잡성을 줄여줍니다.
- 함수형 인터페이스는 람다 표현식의 기반이 되는 요소로, Java에서 함수형 프로그래밍을 가능하게 합니다.
- 표준 라이브러리에 다양한 기본 함수형 인터페이스가 포함되어 있어, 이를 통해 더 쉽게 람다 표현식을 활용할 수 있습니다.
- 메서드 참조는 람다 표현식의 단순한 호출을 더욱 간결하게 표현할 수 있는 방식으로, 코드를 읽기 쉽고 유지보수하기 좋게 만들어줍니다.
- 이번 포스팅에서 정리한 문법적 요소들은 모두 함수형 프로그래밍 패러다임의 일부이며, Java에서 더욱 직관적이고 강력한 코드를 작성하는 데 큰 도움을 줍니다.
- 다음 포스팅에서는 Java
Stream API와 같은 함수형 프로그래밍 기법을 깊이 있게 다루어 보겠습니다.