람다식
함수를 간략하게 표현할 수 있는 표현 방식이다. 함수의 이름, 반환타입, 파라미터 타입을 생략할 수 있어 익명 함수의 한 종류라고 볼 수 있다.
void print(int value) {
System.out.println(value);
}
위의 함수를 람다식을 통해서 다음과 같이 표현할 수 있다.
value -> System.out.println(value);
반환값이 있는 함수들은 아래와 같이 변환할 수 있다.
double pow(double base, double exponent) {
return Math.pow(base, exponent);
}
(base, exponent) -> Math.pow(base, exponent); // return 생략 가능
// or
(base, exponent) -> { // return 생략하지 않을 수도 있음
return Math.pow(base, exponent);
}
사용해보면 알겠지만, 자바에서는 메소드를 단독으로 람다식으로 사용할 수는 없다. 그렇다면 어떤 경우에 람다식으로 쓸 수 있을까?
함수형 인터페이스
람다식을 어떤 경우에 사용할 수 있는 지는 차치하고 먼저 함수형 인터페이스가 어떤 것인지 알아보자.
함수형 인터페이스는 딱 단 하나의 추상 메소드가 선언된 인터페이스이다. 인터페이스 안에 추상 메소드 말고 다른 default, static, private 메소드가 있어도 추상 메소드가 단 한 개라면 함수형 인터페이스로 볼 수 있다.
@FunctionalInterface 를 붙여서 단 하나의 추상메소드를 갖도록 제한할 수 있다.
@FunctionalInterface
public interface Calculator {
long add(long value1, long value2);
default long plusOne(long value) { // default 메소드는 있어도 된다.
return plus(value, 1L);
}
private long plus(long value1, long value2) { // private 메소드는 있어도 된다.
return value1 + value2;
}
static long plusArray(long[] values) { // static 메소드는 있어도 된다.
long result = 0;
for (long value : values) {
result += value;
}
return result;
}
}
람다식과 함수형 인터페이스
자바에서는 함수형 인터페이스의 추상 메소드를 구현할 때에 람다식을 사용할 수 있다.
람다식 이전에 익명 함수로 선언하여 사용하던 것을 람다식을 사용하여 간단하게 사용할 수 있다.
// 익명함수 사용
public static void main(String[] args) {
Calculator calculator = new Calculator() {
@Override
public long add(long value1, long value2) {
return value1 + value2;
}
};
System.out.println(calculator.add(1, 2));
}
// 람다식 사용
public static void main(String[] args) {
Calculator calculator = (value1, value2) -> value1 + value2;
System.out.println(calculator.add(1, 2));
}
자바에서 기본적으로 제공하는 함수형 인터페이스
자바에서는 많이 사용될만한 함수형 인터페이스를 기본적으로 제공하고 있다. 가장 많이 사용하는 4가지 함수형 인터페이스를 살펴보자.
Supplier<T>
- 인자를 받지 않고 T 타입의 객체를 반환한다.
- 아래의 단 하나의 추상 메소드를 가진다.
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
public static void main(String[] args) {
Supplier<String> supplier = () -> {
return "Supplier Test";
};
String result = supplier.get();
System.out.println(result);
}
Consumer<T>
- T 타입의 객체를 인자로 받고 반환값이 없다.
- 아래의 단 하나의 추상 메소드를 가진다.
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
...
}
public static void main(String[] args) {
Consumer<String> consumer = (value) -> System.out.println(value);
consumer.accept("Consumer Test");
}
Function<T, R>
- T 타입 객체를 인자를 받고 R 타입의 객체를 반환한다.
- 아래의 단 하나의 추상 메소드를 가진다.
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
...
}
public static void main(String[] args) {
Function<Long, Long> increasor = (value) -> value + 1;
Long increased = increasor.apply(1L);
System.out.println(increased);
}
Predicate<T>
- T 타입의 객체를 인자로 받고 boolean 값을 반환한다.
- 아래의 단 하나의 추상 메소드를 가진다.
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
...
}
public static void main(String[] args) {
Predicate<Long> predicate = (value) -> value > 0;
boolean isPositive = predicate.test(1L);
System.out.println(isPositive);
}
Variable Capture
람다는 파라미터로 넘겨온 변수 이외에 바깥에 선언된 변수에도 접근할 수 있다. 특히, 지역변수를 가져와 사용할 때에 variable capture 가 일어난다.
이 때 지역변수에 값이 재할당이 되면 안 된다는 제약사항이 있다.
long standard = 2; // final 키워드는 없지만 재할당이 되지 않으면 capture 가능하다.
final long standard = 2; // final 키워드가 붙은 변하지 않는 지역 변수 capture 가능하다.
Predicate<Long> biggerThanStandard = (value) -> value > standard;
boolean isBigger = biggerThanStandard.test(1L);
System.out.println(isBigger);
long standard = 2;
Predicate<Long> biggerThanStandard = (value) -> value > standard; // (1)
standard = 3; // 만약에 재할당이 일어나면 (1) 에서 컴파일 에러가 발생한다.
boolean isBigger = biggerThanStandard.test(1L);
System.out.println(isBigger);
람다식은 별도의 쓰레드로 실행시킬 수 있다. 지역변수는 쓰레드마다 가지고 있는 스택 영역에 저장된다.
람다를 호출한 쓰레드가 종료되고 스택 메모리에서 지역변수가 사라지고 난 후에 람다가 실행될 수도 있기 때문에 지역변수에 직접 접근할 수 없고 값을 캡쳐, 즉 복사해와서 사용한다.
그렇기 때문에 재할당, 즉 값의 변화가 없어야 한다는 제약사항이 생겼다.
그렇다면 인스턴스 변수, static 변수는 각각 힙 영역, data 영역에 저장된다. 이는 여러 쓰레드가 접근할 수 있다. 그래서 주소로 직접 접근해서 사용하고 람다 내에서 값도 변화시킬 수 있다.
public class LambdaDemoApplication {
static Long instanceVariable = 0L;
public static void main(String[] args) {
Consumer<Long> modifyInstanceVariable = (value) -> {
instanceVariable += value;
};
modifyInstanceVariable.accept(1L);
System.out.println(instanceVariable);
}
}
1
메소드, 생성자 레퍼런스
람다식을 사용할 때 메소드 및 생성자 레퍼런스로 표현하면 더 가독성이 좋을 때가 있다.
메소드 레퍼런스
// 람다식
Function<String, Long> convertStringToLong = (value) -> {
return Long.parseLong(value);
};
// 메소드 레퍼런스
Function<String, Long> convertStringToLong = Long::parseLong;
:: 구분자를 사용하여 (클래스 타입)::(참조할 메소드 이름) 으로 메소드 레퍼런스를 사용할 수 있다.
생성자 레퍼런스
public record Car(String name) {}
// 람다식
Function<String, Car> createCar = (name) -> {
return new Car(name);
};
// 생성자 레퍼런스
Function<String, Car> createCar = Car::new;
크게 보면 메소드 레퍼런스와 비슷하다. :: 와 더불어 new 라는 키워드로 (클래스 타입)::new 로 생성자 레퍼런스를 사용할 수 있다.
그래서 클래스의 생성자를 호출해 객체를 생성할 수 있다.
'자바' 카테고리의 다른 글
java.util.ConcurrentModificationException 원인과 해결 (1) | 2025.01.28 |
---|---|
자바 제네릭 (0) | 2024.08.21 |
자바 I/O (0) | 2024.07.15 |
자바 애노테이션 프로세서 (1) | 2024.04.12 |
자바 애노테이션 (0) | 2024.04.03 |