- Published on
자바의 함수
- 글쓴이
자바의 함수
자바의 함수는 메소드로 불린다.
자바의 기본 프로그래밍 단위는 Class이자 Object이기 때문에 메소드
는 객체에 종속된 동작를 명명하기 위해 사용된다.
JDK 1.8(2014년)부터 메소드의 의미가 확장되었다.
이전까지 자바에서 전달할 수 있는 값value은 원시타입과 사용자 정의타입이라는 불리는 Object 두 가지였다. Java가 함수가 값이 될 수 있도록 설계되기 전부터 Scala, Groovy Groovy는 2003년에 초기버전이 나왔다. ㄴㅇㄱ 는 함수가 값이 될 수 있는 설계를 중요하게 생각했다.
일급시민First Class Citizen 이란?
프로그래밍 언어론에서는 함수의 파라미터로 넘어가고, 함수의 반환값이 되고, 변수에 담길 수 있다면 일급시민First Class Citizen으로 분류한다.
함수가 First Class Citizen 이라면
- 함수가 함수의 파라미터로 넘어갈 수 있다.
- 함수가 함수의 반환값이 될 수 있다.
- 함수가 변수에 담길 수 있다.
자바는 함수형프로그래밍을 지원하기 위한 도구로 함수형 인터페이스Functional Interface를 지원한다.
@FunctionalInterface
public interface FunctionalInterface {
public abstract int convertToInt(String text);
}
public static void main(String[] args) {
App.FunctionalInterface func = text -> Integer.valueOf(text);
System.out.println(func.convertToInt("1"));
}
함수형 인터페이스를 직접 구현하는 경우보다, 자바에서 제공하는 함수형 인터페이스를 사용하는 것이 일반적이다.
Predicate<Integer> isEven = number -> number % 2 == 0;
System.out.println(isEven.test(10)); // 출력: true
System.out.println(isEven.test(15)); // 출력: false
자바에서 제공하는 함수형 인터페이스는 java.util.function
패키지에 정의되어 있다. 자바에서 제공하는 대표적인 함수형 인터페이스는 다음과 같다.
인터페이스 | 설명 | 예시 |
---|---|---|
Function<T, R> |
입력을 받아서 결과를 반환 | Function<String, Integer> length = str -> str.length(); |
Predicate<T> |
조걱 입력을 받아서 참 또는 거짓을 반환 | Predicate<Integer> isEven = num -> num % 2 == 0; |
Consumer<T> |
입력을 받아서 소비하는 함수 -> 결과를 반환하지 않음 | Consumer<String> printer = str -> System.out.println(str); |
Supplier<T> |
입력 없이 결과를 제공하는 함수 | Supplier<Double> random = () -> Math.random(); |
UnaryOperator<T> |
단항 연산자 -> 입력과 결과의 타입이 같음 | UnaryOperator<Integer> square = num -> num * num; |
BinaryOperator<T> |
이항 연산자 -> 함수는 두 개의 입력과 하나의 결과 -> 입력과 결과의 타입이 같음 | BinaryOperator<Integer> sum = (num1, num2) -> num1 + num2; |
메소드가 값이 될 수 있다는 것의 의미
메소드가 값이 될 수 있다는 것은 일급 시민이라는 것이다. 일급 시민이 되면 개발자에게 무엇이 좋고 유리할까? 일단 코드가 간결해지고, 가독성이 좋아진다.
첫 번째 코드 스타일의 변화 : 익명 클래스와 람다
익명 클래스는 이름 없이 선언하고 사용한다. 이는 특히 인터페이스를 구현하거나 콜백을 사용할 때 유용하다.
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("익명 클래스를 이용한 스레드 생성");
}
}).start();
하지만, 익명 클래스를 사용하면 코드가 복잡해지고 가독성이 떨어질 수 있다.
람다 표현식은 이러한 문제를 해결하는 방법이다. 람다 표현식을 사용하면 함수를 간결하고 의도를 명확하게 표현할 수 있다. 또한, 람다 표현식을 사용하면 함수를 변수처럼 사용하여 코드의 유연성을 높일 수 있다.
new Thread(() -> System.out.println("람다를 이용한 스레드 생성")).start();
아래의 예시는 데이터 리스트를 정렬하는 익명 클래스와 람다 표현식을 비교한 것이다.
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");
// 익명 클래스
Collections.sort(fruits, new Comparator<String>() {
@Override
public int compare(String fruit1, String fruit2) {
return fruit1.compareTo(fruit2);
}
});
// 람다 표현식
Collections.sort(fruits, (fruit1, fruit2) -> fruit1.compareTo(fruit2));
이 예시에서 볼 수 있듯이, 람다 표현식을 사용하면 코드가 훨씬 간결해지며, 의도가 더 명확하게 드러난다. 또한, 람다 표현식은 함수를 변수처럼 다룰 수 있어서 코드의 유연성을 높일 수 있다.
두 번째 코드 스타일의 변화 : 메소드 참조 Method Reference
이미 정의된 메소드를 재사용하여 람다 표현식을 간결하게 만들어주는 방법이다. ::
연산자를 사용하여 특정 메소드를 직접 참조하게 된다.
람다 표현식을 사용하면 메소드의 로직을 직접 작성해야 하지만, 메소드 참조를 사용하면 이미 작성된 메소드를 재사용할 수 있으므로 코드의 중복을 줄일 수 있다. 또한, 메소드 참조를 사용하면 코드가 간결해져서 가독성이 향상된다.
List<String> list
= Arrays.asList("Java", "Python", "JavaScript", "TypeScript");
// 람다 표현식
list.forEach(s -> System.out.println(s));
// 메소드 참조
list.forEach(System.out::println);
예를 들어, System.out.println(s); 이라는 로직을 갖는 람다 표현식 (s) -> System.out.println(s) 대신에 System.out::println 이라는 메소드 참조를 사용할 수 있다. 이렇게 하면 람다 표현식이 간결해지고 가독성이 향상된다. 또한, 이미 정의된 println 메소드를 재사용하므로 코드의 중복도 줄일 수 있다.
세 번째 코드 스타일의 변화 : 스트림 Stream
스트림은 Java 8에서 소개된 기능으로, 스트림의 각 요소에 대한 함수형 스타일 연산을 지원하는 클래스이다. 스트림을 사용하면 데이터를 선언적으로 처리할 수 있다. 즉, '어떻게'가 아닌 '무엇을' 수행할 것인지에 초점을 맞추게 되어 코드의 가독성과 유지 보수성이 향상된다.
아래의 예시는 리스트의 모든 요소를 대문자로 변환하는 코드를 비교한 것이다.
List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
// enhanced loop만 활용한 방법
List<String> uppercasedWords = new ArrayList<>();
for (String word : words) {
uppercasedWords.add(word.toUpperCase());
}
// Stream
List<String> uppercasedWords = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
이처럼 스트림을 사용하면 복잡한 작업을 몇 줄의 코드로 간단하게 표현할 수 있다.
또한, 스트림은 스트림 내 요소를 쉽게 병렬처리할 수 있는 환경을 제공한다.아래 코드는 순차 스트림을 병렬 스트림으로 변환하는 코드이다.
다만 병렬 스트림은 대량의 데이터일때 의미가 있으며, 스트링 포킹 처리가 필요하며, 멀티코어 간의 데이터 이동이라는 비싼 코스트가 있다. JMH 라이브러리를 통해 성능 측정 후 따라서 코어 간에 데이터 전송 시간보다 훨씬 오래 걸리는 작업만 병렬처리하는 것이 바람직하다.
int sumOfWeights = widgets.parallelStream()
.filter(b -> b.getColor() == RED)
.mapToInt(b -> b.getWeight())
.sum();
일급 시민은 변수에 할당하고, 함수에 전달하고, 다른 함수에서 반환할 수 있는 모든 것이다.
Ref.
- MDN 일급함수 문서
- JAVA 17 공식문서 - Stream
- 책
모던 자바인 액션