자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임
우리에게 익숙한 절차지향, 객체지향 프로그래밍은 모두 명령형 프로그래밍 패러다임이다.
함수형 프로그래밍은, 선언형 프로그래밍 패러다임의 일종이다.
함수형 프로그래밍의 핵심은 side effects를 최소화하여 프로그램의 동작을 예측 가능하게 만드는 것이다.
side effects를 최소화하기 위해서는 상태 변경이나 변경 가능한 데이터를 피해야 한다.
➜ 순수 함수를 사용한다.
함수형 프로그래밍의 주요 특징은 다음과 같다.
특징 | 설명 |
---|---|
순수 함수 | 같은 입력에 대해 항상 같은 출력 반환한다. 외부 상태를 변경하지 않는다. |
불변성 | 데이터는 한 번 생성되면 변경되지 않는다. |
고차 함수 | 함수를 다른 함수의 인자로 전달할 수 있다. 함수를 리턴할 수 있다. |
함수 합성 | 여러 함수를 조합하여 새로운 함수를 만들 수 있다. |
게으른 평가 | 필요할 때까지 계산을 지연시킨다. |
함수형 프로그래밍의 장점은 다음과 같다.
자바는 Java 8 버전부터 함수형 프로그래밍을 지원하기 위해 람다와 스트림이 도입되었다.
람다는 메소드를 하나의 식으로 표현한 것이다.
람다 표현식은 기본적으로 아래와 같은 형태를 가진다.
(parameter list) -> {body}
일반적인 자바 코드와 람다를 사용한 코드를 비교해보자.
// 일반적인 자바 코드
class MyCalculator {
public int sum(int a, int b){
return a+b;
}
}
위의 코드를 람다식을 사용하도록 고치면 다음과 같이 표현 가능하다.
// 람다 표현식을 사용한 코드
(int a, int b) -> a+b;
람다는 함수를 일급 시민으로 만들어 준다.
즉, 함수를 다른 함수의 인자로 전달하고나, 결과 값으로 반환하거나, 데이터 구조에 저장할 수 있다.
다음 코드를 보면, 람다 표현식으로 작성된 익명 함수(여기서는 함수라 칭하겠다)를 forEach()
메소드의 인자로 전달한다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(n -> System.out.println(n));
어떻게 위와 같은 구조가 가능할까?
처음 위의 식을 봤을때는 전혀 이해가 되지 않았다.
필자의 의문점은 바로n
이었다.
변수n
을 넘겨줘야 함수가 실행될텐데, 누가 넘겨주는 것일까?
답은forEach()
메소드에 있었다.
forEach()
메소드는Consumer
타입의 함수형 인터페이스를 인수로 받는다.
람다식n -> System.out.println(n)
는Consumer
타입의 익명 구현 객체인 것이다.
forEach()
메소드는 컬렉션의 각 요소를 익명 구현 객체의 매개변수로 전달한다.
스트림 API는 데이터 컬렉션을 추상화한 것이다.
List, Set, Map 등의 컬렉션을 다루기 위해서는 각각 다른 메소드를 사용해야 한다.
이러한 문제점을 해결하기 위해 도입된 것이 스트림이다.
스트림을 사용하면, 데이터 컬렉션을 추상화하여 공통된 메소드를 적용시킬 수 있다.
스트림은 3가지 과정을 통해 사용할 수 있다.
String[] arr = new String[]{"a","b","c"};
Stream<String> stream = Arrays.stream(arr);
List<String> list = Arrays.asList("a","b","c");
Stream<String> stream = list.stream();
// 컬렉션은 stream() 메소드를 가지고 있다.
IntStream intStream = IntStream.range(1, 5);
// 제너릭을 사용하지 않기 때문에, 박싱을 하지 않아도 된다.
이외에도 다양한 생성 방식이 존재한다.
생성된 스트림 인스턴스에 대해서 0~n회 중간 연산을 적용할 수 있다.
중간연산은 다양한 종류가 존재한다.
그 중에서 filter()
메소드와 map()
메소드에 대해서 알아보자.
filter
필터는 스트림 내의 요소들을 하나씩 평가해서 걸러낸다.
Predicate
인터페이스를 인자로 받는다.
Stream<T> filter(Predicate<? super T> predicate)
Predicate
인터페이스를 구현한 익명 객체를 전달하는 것이 일반적이다.
예시:List<String> names = Arrays.asList("Eric", "Elena", "ju"); Stream<String> stream = names.stream() .filter(name -> name.contains("a"));
map
맵은 스트림 내의 각 요소에 함수를 적용한다.
Fucntion
인터페이스를 인자로 받는다.
Stream<R> map(Function<? super T, ? extends R> mapper);
예시:List<String> names = Arrays.asList("Eric", "Elena", "ju"); Stream<String> stream = names.stream() .map(name -> name.toUpperCase);
여러개의 중간 연산을 이어 붙일 수 있으므로, 다음과 같이 작성 가능하다.
List<String> names = Arrays.asList("Eric", "Elena", "ju");
Stream<String> stream = names.stream()
.filter(name -> name.contains("a"))
.map(name -> name.toUpperCase);
최종 연산은 스트림 파이프라인을 실행하고 결과를 도출한다.
위의 예제에 collect 연산을 적용하여, 다시 List로 변환하여보자.
List<String> names = Arrays.asList("John", "Michael", "ju");
Stream<String> stream = names.stream()
.filter(name -> name.contains("a"))
.map(name -> name.toUpperCase)
.collect(Collector.toList());
점프 투 자바 - 박은용