개요
메서드 파라미터에 값이 아닌 어떠한 로직을 넘기고 싶을 경우가 있다면, 어떻게 해야 할까?
인터페이스 구현 후, 구현한 클래스 넘기기
public class ExMain {
public static void hello(Procedure procedure) {
//4. 메서드 실행
procedure.run();
}
//1. 인터페이스 구현
static class Dice implements Procedure {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
}
public static void main(String[] args) {
//2. 클래스 생성
Procedure dice = new Dice();
//3. 클래스 전달
hello(dice);
}
}
//인터페이스
interface Procedure {
void run();
}
인터페이스를 직접 구현 한 후, 구현한 클래스를 인자로 넘기는 방법이 있다.
익명 클래스로 넘기기
public class ExMain {
public static void hello(Procedure procedure) {
procedure.run();
}
public static void main(String[] args) {
//익명 클래스로 전달 후 실행
hello(new Procedure() {
@Override
public void run() {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
}
});
}
}
//인터페이스
interface Procedure {
void run();
}
익명 클래스를 생성하여 로직을 작성 후, 익명 클래스 자체를 넘겨줄 수 있다.
이 쯤 되면 '클래스를 생성하지 않고, 함수만 넘겨주는 방법은 없나' 라는 생각이 든다.
람다란
람다는 Java 8부터 도입된 기능으로, 함수를 익명으로 간결하게 표현하는 방법이다. 함수형 프로그래밍 스타일을 지원하여 코드를 더 간단하고 읽기 쉽게 만들어준다.
앞서 우리는 어떤 로직을 전달해주기 위해서 직접 클래스를 생성해야 했지만, 람다를 이용하면 우리가 넘겨주고자 하는 로직만 파라미터로 넘겨줄 수 있다.
public class ExMain {
public static void hello(Procedure procedure) {
procedure.run();
}
public static void main(String[] args) {
//람다 적용
hello(() -> {
int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue);
});
}
}
람다의 특징
기본 문법
(매개변수) -> { 실행문 }
람다식의 기본 구조는 다음과 같다.
함수형 인터페이스
@FunctionalInterface
public interface SamInterface {
void run();
//void go();
}
함수형 인터페이스란 딱 하나의 추상 메서드를 가지는 인터페이스이다. 람다 타입 ≒ 함수형 인터페이스라고 봐도 무방할 것 같다.
람다는 함수형 인터페이스에만 할당이 가능하다.
여러 추상 메서드를 갖는 인터페이스는 람다를 할당 못하는걸까?
NotSamInterface notSamInterface = () -> {
System.out.println("not sam");
}
notSamInterface.run(); // ?
notSamInterface.go(); // ?
인터페이스에 추상 메서드가 2개 이상이라면, 람다식을 어느 메서드에 할당해야 할 지 알 수가 없다.
이럴 경우 컴파일 시점에 에러가 발생한다.
람다 시그니처
람다를 함수형 인터페이스에 할당할 때는 메서드 시그니처가 일치해야 한다.
메서드 이름-> 익명 함수이므로 메서드 이름은 제외- 매개변수의 수와 타입
- 반환 타입
람다와 생략
1. 단일 표현식일 경우, 중괄호와 리턴은 생략한다.
//MyFunction function2 = (int a, int b) -> {return a + b};
MyFunction function2 = (int a, int b) -> a + b;
2. 매개변수가 없는 경우 비운다.
Procedure procedure2 = () -> System.out.println("hello! lambda");
3. 함수형 인터페이스를 통해 타입을 추론하므로 매개변수 타입은 생략 가능하다
//MyFunction function1 = (int a, int b) -> a + b;
MyFunction function1 = (a, b) -> a + b;
람다 전달
1. 람다를 변수처럼 대입할 수 있다. 타입은 함수형 인터페이스이다.
MyFunction add = (a, b) -> a + b;
MyFunction cal = add;
2. 메서드에 람다를 전달할 수도 있다.
calculate((a, b) -> a + b);
3. 람다로 메서드의 반환 값을 줄 수 있다.
MyFunction getOperation(String operator) {
switch (operator) {
case "add":
return (a, b) -> a + b;
case "sub":
return (a, b) -> a - b;
default:
return (a, b) -> 0;
}
}
자바가 제공하는 함수형 인터페이스
람다식을 작성하겠다고 그때그때 필요한 함수형 인터페이스를 작성하는 건 비효율적이다.
자바는 함수형 인터페이스를 제공해서 이러한 문제를 해결한다. java.util.function 패키지에 43개의 함수형 인터페이스가 존재하지만, 핵심인 4가지 기본 타입에 나머지는 변형이다.
1. Function
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
매개변수 - ✅
반환 값 - ✅
2. Consumer
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
매개변수 - ✅
반환 값 - ❌
3. Supplier
@FunctionalInterface
public interface Supplier<T> {
T get();
}
매개변수 - ❌
반환 값 - ✅
4. Runnable
@FunctionalInterface
public interface Runnable {
void run();
}
매개변수 - ❌
반환 값 - ❌
참고로 Thread에 그 Runnable 맞다.
5. BiFunction
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
입력 2개, 반환 1개인 함수형 인터페이스이다.
Prefix로 Bi가 붙는 함수형 인터페이스들이 몇개 있는데 이런 경우는 입력 매개변수가 2개이다.
입력이 3개 이상이 필요하다면, 직접 만들어서 사용하면 될 것 같다.
함수형 프로그래밍이란
위에서 기술한 람다 설명 중 일부이다.
함수형 프로그래밍 스타일을 지원하여 코드를 더 간단하고 읽기 쉽게 만들어준다.
함수형 프로그래밍이란 무엇일까?
프로그래밍은 크게 명령형 프로그래밍과 선언형 프로그래밍으로 나누어진다.
명령형 프로그래밍은 '프로그램이 어떻게 동작해야하는지 세세한 제어 흐름을 기술하는 방식'이다.
선언형 프로그래밍은 '프로그램이 무엇을 해야하는지 목적만 선언하고 구현 방식은 추상화 하는 방식'이다
함수형 프로그래밍은 선언형 프로그래밍의 범주에 들어가며, 정의는 다음과 같다.
무엇을 해야하는지 수학적 함수들로 표현하는 프로그래밍 패러다임
함수형 프로그래밍의 개념과 특징
순수 함수
// 순수하지 않은 함수 - 외부 상태에 의존
int total = 0;
public int addToTotal(int value) {
total += value; // 외부 상태 변경
return total;
}
같은 입력에 대해 항상 같은 결과를 반환하는 함수를 의미한다.
그러기 위해서는 외부 상태에 의존하거나, 외부 상태에 영향을 주지 않아야 한다.
불변성 지향
// 명령형 방식 - 원본 수정
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3));
numbers.add(4); // 원본 변경
// 함수형 방식 - 새로운 리스트 생성
List<Integer> original = Arrays.asList(1, 2, 3);
List<Integer> newList = Stream.concat(
original.stream(),
Stream.of(4)
).collect(Collectors.toList());
생성된 데이터는 가능한 변경되지 않고, 변경이 필요할 경우 새로운 값을 생성한다.
고차 함수 (일급 시민 함수)
// 함수를 변수에 할당
Function<Integer, Integer> square = x -> x * x;
// 함수를 매개변수로 전달
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squared = numbers.stream()
.map(square) // 함수를 인자로 전달
.collect(Collectors.toList());
함수를 파라미터로 받거나 반환하는 함수이다. '함수는 일급 시민으로써 값처럼 다룰 수 있다'는 개념이 전제한다.
선언형 접근
List<Integer> evenSquares = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
'무엇'을 계산할 지 기술한다.
함수 합성
Function<Integer, Integer> multiplyBy2 = x -> x * 2;
Function<Integer, Integer> add3 = x -> x + 3;
// 함수를 조합하여 새로운 함수 생성
Function<Integer, Integer> multiplyThenAdd =
multiplyBy2.andThen(add3);
간단한 함수들을 조합해 복잡한 함수를 만드는 것을 권장한다.
'Language > Java' 카테고리의 다른 글
| [Java] ReentrantLock을 통한 스레드 동기화 (0) | 2026.01.02 |
|---|---|
| [Java] 자바 스레드 생명 주기와 메모리 가시성 (0) | 2025.12.30 |
| [Java] Stream Collector, Downstream Collector (0) | 2025.12.26 |
| [Java] Stream API와 지연 연산 (0) | 2025.12.25 |
| [Java] 메서드 참조 (0) | 2025.12.24 |