본 내용은 라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트의 "모던 자바 인 액션" 책을 보고 정리한 내용입니다. 저작권 보호를 위해 책의 내용은 요약되었습니다.
함수형 인터페이스란?
- 함수형 인터페이스는 단 하나의 추상 메서드를 가지는 인터페이스로 하나의 추상메서드를 지정한다. 이러한 특징 때문에 함수형 인터페이스는 함수를 객체로 표현하기 위한 용도로 사용된다.
- 람다표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달하여, 표현식을 함수형 인터페이스의 인스터스로 취급할 수 있다.
함수 디스크립터란?
- 함수 디스크립터는 함수에 대한 정보를 나타내는 객체 또는 데이터 구조이다.
- 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시크니처르 ㄹ가리키고 이런 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라 한다.
- 함수 디스크립터는 함수의 이름, 매개변수, 반환값, 함수의 속성 등과 같은 함수에 관련된 다양한 정보를 포함한다.
실행 어라운드 패턴
자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태로, 이 패턴은 주로 리소스 관리나 예외 처리와 같이 중요한 보조 작업을 수행해야 할 때 유용하게 활용된다.
예시) 1. 함수형 인터페이스 정의
먼저 실행 어라운드 패턴을 구현하기 위한 함수형 인터페이스를 정의해야하는데 이 인터페이스는 주요 동작을 수행하는 메서드를 포함해야 한다.
@FunctionalInterface
interface FileOperation {
void performFileOperation(BufferedReader br) throws IOException;
}
예시) 2. 실행 어라운드 메서드 작성
실행 어라운드 메서드를 작성하는데 이 메서드는 주요 동작을 람다 표현식으로 전달받아 실행한다.
public void processFile(String filePath, FileOperation operation) {
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
// 보조 작업 (리소스 할당, 예외 처리, 기타 작업)
// ...
operation.performFileOperation(br); // 주요 작업 (람다 표현식 실행)
} catch (IOException e) {
// 주요 작업에서 발생한 예외 처리
System.err.println("파일 처리 중 예외 발생: " + e.getMessage());
} finally {
// 보조 작업 (리소스 해제, 정리, 기타 작업)
// ...
}
}
예시) 3. 실행 어라운드 패턴 사용
실행 어라운드 패턴을 사용하여 파일에서 텍스트를 읽는다.
public static void main(String[] args) {
FileProcessor fileProcessor = new FileProcessor();
String filePath = "파일경로.txt";
// 실행 어라운드 패턴 사용
fileProcessor.processFile(filePath, (br) -> {
// 주요 작업 (파일에서 텍스트 읽기)
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
});
}
}
1. Predicate<T> 인터페이스
Predicate 인터페이스는 단일 인자를 받아서 test 추상 메서드를 정의하고 boolean 값을 반환하는 함수를 나타내며, 주로 조건을 검사하거나 필터링을 수행하는 데 사용한다.
예시)
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
예시) 1~10 중 짝수만 고른다.
public static void main(String[] args) {
// 1 ~ 10
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
numbers.add(i);
}
// Predicate를 사용하여 짝수를 필터링한다.
Predicate<Integer> isEven = num -> num % 2 == 0;
List<Integer> evenNumbers = filter(numbers, isEven);
System.out.println("짝수: " + evenNumbers); // 출력 : 짝수: [2, 4, 6, 8, 10]
}
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
// Predicate를 사용하여 리스트를 필터링하는 메서드
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> result = new ArrayList<>();
for (T item : list) {
if (predicate.test(item)) {
result.add(item);
}
}
return result;
}
2. Consumer<T> 인터페이스
Consumer 인터페이스는 단일 인자를 받고 accept추상 메서드를 정의한다. 반환하지 않는 함수를 나타낸다. 주로 데이터를 소비하거나 처리할 때 사용한다.
예시)
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
예시) 이름을 순차적으로 출력한다.
public static void main(String ...args){
List<String> names = new ArrayList<>();
names.add("sjmoon");
names.add("jjmoon");
names.add("gdhong");
forEach(names, (String s) -> System.out.print(s + " ")); // 출략 : sjmoon jjmoon gdhong
}
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
public static <T> void forEach(List<T> list, Consumer<T> c){
for(T t: list){
c.accept(t);
}
}
3. Function<T, R> 인터페이스
Function은 단일 인자를 받고, apply 추상 메서드를 정의한다. 다른 타입의 결과를 반환하는 함수를 나타내며, 주로 데이터 변환 및 매핑 작업에 사용된다.
예시)
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
예시) 이름의 길이를 구한다.
public static void main(String[] args) {
List<String> members = new ArrayList<>();
members.add("sjmoon");
members.add("hi");
members.add("hello");
// Function을 사용하여 각문자의 길이를 구한다.
Function<String, Integer> len = x -> x.length();
List<Integer> strLength = map(members, len);
System.out.println("문자 길이: " + strLength); // 출력 : 문자 길이: [6, 2, 5]
}
@FunctionalInterface
public interface Function<T,R> {
R apply(T t);
}
// Function을 사용하여 리스트를 매핑하는 메서드
public static <T, R> List<R> map(List<T> list, Function<T, R> function) {
List<R> result = new ArrayList<>();
for (T item : list) {
result.add(function.apply(item));
}
return result;
}
4. Supplier<T> 인터페이스
Supplier는 매개변수 없이 값을 제공하는 역할을 합니다. get 추상 메서드를 정의하고, 주로 데이터나 값을 생성하는 데 사용된다.
예시)
@FunctionalInterface
public interface Supplier<T> {
T get();
}
예시) 현재 시간을 반환한다.
public static void main(String[] args){
Supplier<Date> getCurrentTime = () -> new Date();
Date currentTime = getCurrentTime .get();
// 현재 시간을 반환
System.out.println("현재 시간: " + currentTime); // 현재 시간: Sun Sep 24 16:17:58 KST 2023
}
5. UnaryOperator<T> 인터페이스
UnaryOperator 는 단일 인자를 받고 입력과 같은 타입을 반환하는 apply 추상 메서드를 정의한다.
예시)
@FunctionalInterface
public interface UnaryOperator<T> {
T apply(T t);
}
예시) 대문자로 변환하여 출력
public static void main(String[] args) {
UnaryOperator<String> toUpperCase = str -> str.toUpperCase();
String name = "moon";
String upperName = toUpperCase.apply(name);
System.out.println("name: " + name); // 출력 : name: moon
System.out.println("upperName: " + upperName); // 출력 : upperName: MOON
}
6. BinaryOperator<T> 인터페이스
BinaryOperator는 두 개의 인자를 받아 같은 타입을 반환하는 apply 추상 메서드를 정의한다.
예시)
@FunctionalInterface
public interface BinaryOperator<T> {
T apply(T t, T u);
}
예시) 덧셈 연산
public static void main(String[] args) {
BinaryOperator<Integer> add = (a, b) -> a + b;
int result = add.apply(2, 3);
System.out.println("2 + 3 = " + result); // 출력 : 2 + 3 = 5
}
기본형 특화
- 제네릭 함수형 인터페이스에서 사용하는 제네릭 파라미터는 참조형(Byte, Integer, List)만 사용할 수 있다.
- 자바에서는 기본형(int, double, char)을 참조형으로 변환하는 오토 박싱 기능을 제공하는데 이런 변환 과정은 많은 메모리와 비용을 소모한다.
- 자바 8에서는 오토방식을 피할 수 있는 특수한 함수형 인터페이스를 제공하는데 일반적으로 특정 형식의 입력을 받는 함수형 인터페이스 앞에 형식을 붙힌다.
함수형 인터페이스 | 함수 디스크립터 | 기본형 특화 |
Predicate<T> | T → boolean | IntPredicate, LongPredicate, DoublePredicate |
Consumer<T> | T → void | IntConsumer, LongConsumer, DoubleConsumer |
Function<T, R> | T → R | IntFunction<R>, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, IntToIntFunction, DoubleFunction, DoubleToIntFunction, DoubleToLongFunction |
Supplier<T> | () → T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator<T> | T → T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator<T, T> | (T, T) → T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
함수형 인터페이스와 예외 처리
- 함수형 인터페이스는 확인된 예외를 던지는 동작이 허용되지 않기에 직접 정의하거나 람다를 try catch 블록으로 감싸야 한다.
예시) 인자가 숫자가 아니면 0을 반환
public static void main(String[] args) {
Function<String, Integer> parseToInt = s -> {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return 0; // 예외 발생 시 기본값으로 0을 반환
}
};
String input = "123";
int result = parseToInt.apply(input);
System.out.println("결과값: " + result) // 출력 : 결과값 : 123;
}
형식검사
- 형식 검사는 람다 표현식이 사용되는 컨텍스트에서 예상되는 형식과 일치하는지 확인한다.
- 콘텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라고 한다.
- 대상 형식이라는 특징 때문에 같은 람다 표현식이라도 다양한 함수형 인터페이스를 사용할 수 있다.
예시) 주어진 문자열이 대문자로만 이루어져 있는지를 검사
Predicate<String> isUpperCase1 = s -> s.equals(s.toUpperCase());
boolean result = isUpperCase.test("HELLO"); // 출력 : true
Function<String, Boolean> isUpperCase2 = s -> s.equals(s.toUpperCase());
boolean result = checkUpperCase.apply("HELLO"); // 출력 : true
형식추론
- 자바 8에서는 람다 표현식을 사용할 때 명시적으로 형식을 지정하지 않아도 컴파일러가 컨텍스트를 기반으로 형식을 추론할 수 있다.
예시) name의 형식을 포함하지 않아도 컴파일러가 String 형태를 추론할 수 있다.
ArrayList<String> names = new ArrayList<>();
names.add("sjmoon");
names.add("jjmoon");
names.forEach(name -> System.out.println(name));
지역변수 사용
- 람다표현식도 익명함수 처럼 지역변수를 활용할 수 있고 이와 같은 동작을 람다 캡처링이라고 한다.
- 람다는 인스턴스 변수와 정적 변수를 참조 할 수 있지만 지역변수는 명시적으로 final로 선언되어 있거나 한 번만 할당할 수 있는 변수여야한다.
- 내부적으로 인스턴스 변수는 힙에 저장되는 반면 지역 변수는 스택에 위치하므로 람다에서 지역 변수에 접근할 때 람다가 스레드에서 실행 된다면 변수를 할당하 스레드가 사라져 원래 변수에 접근을 하지 못할 수 있다.
예시)
String name = "sjmoon";
Runnable runnable = () -> {
System.out.println("Hello! " + name); // 출력 : Hello! sjmoon
};
runnable.run();
'Java' 카테고리의 다른 글
[자바8] 스트림 (0) | 2023.12.30 |
---|---|
[자바 8] 메서드 참조 (0) | 2023.09.24 |
[자바 8] 람다 표현식 (0) | 2023.09.18 |
[자바 8] 동작 파라미터화 (0) | 2023.09.06 |
[자바] 자바 8 (0) | 2023.09.01 |