[Java] Functional Programming - 1. Java 함수형 프로그래밍의 개요와 Stream

2022. 12. 8. 02:51Java/Basic

반응형
  1. 함수형 프로그래밍
  2. 람다식
  3. 함수형 인터페이스
  4. Collection vs Stream
  5. 중간 연산, 종료 연산
    1. filter, distinct
    2. sorted
    3. map
    4. flatMap
    5. peek
  6. 메서드 참조
    1. static method 참조
    2. 인스턴스의 인스턴스 메서드 참조
    3. 생성자 new 참조
    4. 클래스의 인스턴스 메서드 참조
  7. array를 Stream으로 변환하기

1. 함수형 프로그래밍

입력을 넣을 경우 동일한 결과값을 리턴하는 함수와 같은 특징을 갖는 프로그램을 말합니다.
코드의 이해가 쉽고, 유지 보수 및 테스트가 쉽다는 장점이 있습니다.

함수형 프로그래밍의 핵심 개념은 일급 함수, 순수 함수, 고차 함수라는 특징입니다.

1.1. 일급 함수

함수를 값으로 다룰 수 있는 성질로, 변수에 담을 수 있습니다.
함수를 다른 함수의 매개변수로 전달할 수 있는 특징을 의미합니다.

자바의 메서드는 일급 객체는 아니고,
람다식을 이용하여 함수의 매개변수로 전달할 수 있습니다.

1.2. 순수함수

함수를 실행한 후 부수적인 효과가 일어나지 않는 함수. (= stateless)
입력값이 같을 경우 결과값이 언제나 동일합니다.

예기치 못한 변동을 줄이고 싶다면,
순수함수 내에 사용되는 변수들을 불변 변수로 정의하는 것이 좋습니다.

1.2.1. 순수함수가 아닌 메서드 예시 1

함수를 실행한 후, 매개변수로 이용한 member객체가 변하기 때문에 순수함수가 아닙니다.

void setAge(Member member, int age) {
    member.setAge(age);
}

1.2.2. 순수함수가 아닌 메서드 예시 2

함수를 실행했을 때 외부에 정의된 member 객체가 변하기 때문에 순수함수가 아닙니다.

Member member = new Member();

void setAge(int age) {
    member.setAge(age);
}

1.3. 고차 함수

하나 이상의 함수를 매개변수로 갖거나
다른 함수를 결과로 리턴하는 특징을 말합니다.

자바에서는 하나이상의 람다식을 인자로 받거나
다른 람다식을 리턴하는 형태로 이용합니다.


2. 람다식

(x, y) -> x * y

람다식은 람다 매개변수 -> 람다 실행문 으로 구성되어있습니다.

익명함수이고
특정 클래스에 종속적이지 않으며
함수형 인터페이스 환경에서만 사용 가능합니다


3. 함수형 인터페이스

abstract method를 하나만 갖고 있는 인터페이스로 구현해야할 함수가 오직 하나만 있어야 합니다.

default, static 함수는 가지고 있어도 됩니다.


함수형 인터페이스로 활용될 인터페이스에는 @FunctionalInterface 를 붙입니다.


함수형 인터페이스 예제

@FunctionalInterface
interface PlusCalculator<T extends Number> {
    T plus(T x, T y);

    default void introduce(T x, T y) {
        System.out.printf("%s, %s\n", x, y);
    }
}
public class FunctionalInterfaceExample {

    public static void main(String[] args) {
        PlusCalculator<Integer> plusInteger = (x, y) -> x + y;
        int x = 4, y = 5;

        System.out.println(plusInteger.plus(x, y));
        plusInteger.introduce(x, y);
    }
}
9
4, 5

4. Collection vs Stream

Collection

컬렉션은 데이터를 저장하는 것이 목적입니다.
(가변 컬렉션의 경우) 요소의 추가 및 삭제가 가능합니다.

컬랙션은 선언시점부터 모든 요소가 존재합니다.

Stream

데이터에 fitler, map 등의 연산을 적용하는 것이 목적입니다.

스트림에서 일어난 연산은 원본 컬렉션에 영향을 주지 않습니다.
(= 연산의 수행결과를 리턴은 하지만, 원본 소스에 수정을 가할 수 없습니다.)

하나의 스트림은 한번의 순회만 가능합니다. (= 여러번 순회 불가능)

스트림은 중간 연산 중에는 요소들이 적용되지 않고, 종료 연산이 호출될때 결과가 적용됩니다.

중간연산 결과는 Stream이고, 종료 연산 결과가 List나 int와 같은 원하는 최종 결과값입니다.


여러 스레드를 사용하여 병렬로 실행할 수 있는 병렬스트림도 있습니다.
병렬스트림은 여러 스레드에서 스트림의 서로 다른 부분을 처리하여 처리 결과를 결합합니다.


5. 중간 연산, 종료 연산

중간 연산은 Stream을 리턴하는 연산을 말하고,
종료 연산은 컬랙션이나 스칼라 값을 리턴하는 연산을 말합니다.

filter, map, sorted 등이 중간 연산에 해당되고
collect, toList, sum, min, max 등이 종료 연산에 해당됩니다.

중간연산은 종료 연산이 호출되기 전까지 실행되지 않습니다. (= 스트림 지연)


5.1. filter, distinct

filter는 특정 조건을 만족하는 요소만 필터링합니다.
distinct는 중복 요소를 제거합니다.

List<Integer> numbers = List.of(3, 11, 49, 22, 55, 1, 29, 4, 49);

List<Integer> result = numbers.stream()
        .filter(f -> f > 20)
        .distinct()
        .toList();
System.out.println(result); // [49, 22, 55, 29]

5.2. sorted

sorted 함수는 기본적으로 오름차순을 스트림을 정렬하는데
매개변수로 정렬 기준을 설정할 수도 있습니다.

List<Integer> numbers = List.of(3, 11, 49, 22, 55, 1, 29, 4, 49);

List<Integer> result = numbers.stream()
        .distinct()
        .sorted()
        .limit(5)
        .toList();
System.out.println(result); // [1, 3, 4, 11, 22]
List<Integer> numbers = List.of(3, 11, 49, 22, 55, 1, 29, 4, 49);

List<Integer> result = numbers.stream()
        .distinct()
        .sorted(Comparator.reverseOrder())
        .limit(5)
        .toList();
System.out.println(result); // [55, 49, 29, 22, 11]

5.3. map

Stream의 중간 연산자 중 하나로, 객체 타입을 다른 타입으로 변환하거나, 컬렉션 전체에 특정 계산식을 적용하고자 할 때 사용합니다.

아래와 같이 타입을 변환할 수도 있고,

List<String> list = List.of("31", "2", "11");

List<Integer> result = list.stream()
        .map(Integer::valueOf).toList();
System.out.println(result); // [31, 2, 11]

특정 계산식을 적용하는 매핑도 할 수 있습니다.

List<Integer> numbers = List.of(3, 5, 10);

List<Integer> result = numbers.stream()
        .map(n -> n * n).toList();
System.out.println(result); // [9, 25, 100]

5.4. flatMap

map을 확장한 함수
flattening 을 통해 중첩 컬랙션을 단일 컬랙션으로 만드는 중간 연산자 입니다

List<List<Integer>> list = List.of(List.of(35, 12, 5, 10), List.of(9, 8, 12, 11));

Set<Integer> result = list.stream()
        .flatMap(List::stream)
        .filter(f -> f > 10)
        .collect(Collectors.toSet());
System.out.println(result); // [35, 11, 12]

5.5. peek

스트림 실행 중 스트림 내부 요소를 조회해보고 싶을 때 사용합니다.
peek 함수는 조회에만 사용하기를 권장합니다. (데이터 소스 변경에 이용하는 것은 권장하지 않습니다.)

List<String> list = List.of("apptestle", "melon", "watesttermelon", "banana", "graph", "ortestange");

List<String> result = list.stream()
        .filter(f -> f.contains("test"))
        .peek(p -> System.out.println("\ttest 가 들어있는 것: " + p))
        .map(String::toUpperCase)
        .toList();

System.out.println("최종: " + result);
    test 가 들어있는 것: apptestle
    test 가 들어있는 것: watesttermelon
    test 가 들어있는 것: ortestange
최종: [APPTESTLE, WATESTTERMELON, ORTESTANGE]

6. 메서드 참조

Integer::valueOf, System.out::println 와 같이 :: 를 사용하여 메서드를 호출하는 방식입니다.


6.1. static method 참조

List<String> list = List.of("23", "5", "6", "16");
List<Integer> numbers = list.stream()
    .map(Integer::valueOf).toList();

아래와 동일한 코드입니다.

List<Integer> numbers = list.stream()
    .map(m -> Integer.valueOf(m)).toList();

6.2. 인스턴스의 인스턴스 메서드 참조

List<Integer> numbers = List.of(2, 6, 11, 8);
numbers.forEach(System.out::println);

아래와 동일한 코드입니다.

numbers.forEach(number -> System.out.println(number));

6.3. 생성자 new 참조

List<Integer> ages = List.of(5, 1, 22, 6, 94, 72);
List<Member> members = ages.stream()
    .map(Member::new).toList();

아래와 동일한 코드입니다.

List<Member> members = ages.stream()
    .map(age -> new Member(age)).toList();

6.4. 클래스의 인스턴스 메서드 참조

List<String> names = List.of("coco", "kei", "michael", "v");
List<Integer> result = names.stream()
    .map(String::length).toList();

아래와 동일한 코드입니다.

List<Integer> result = names.stream()
    .map(name -> name.length()).toList();

7. array를 Stream으로 변환하기

Arrays.stream, Stream.of 를 이용하여 Stream으로 변환할 수 있고,
array를 List로 변환한 후 stream으로 변환하는 것도 가능합니다.

static <T> Stream<T> toStream1(T[] arr) {
    return Arrays.stream(arr);
}

static <T> Stream<T> toStream2(T[] arr) {
    return Stream.of(arr);
}

static <T> Stream<T> toStream3(T[] arr) {
    return Arrays.asList(arr).stream();
}

int array를 IntStream로, double array를 DoubleStream으로 변환할 수도 있습니다.
(마찬가지로 LongStream도 변환할 수 있습니다.)

static IntStream toStream1(int[] arr) {
    return Arrays.stream(arr);
}

static IntStream toStream2(int[] arr) {
    return IntStream.of(arr);
}
static DoubleStream toStream1(double[] arr) {
    return Arrays.stream(arr);
}

static DoubleStream toStream2(double[] arr) {
    return DoubleStream.of(arr);
}
728x90
반응형