함수형 프로그래밍(Stream,Lambda)

윤재열·2022년 1월 31일
0

Java

목록 보기
44/71
post-thumbnail
post-custom-banner

함수형 프로그래밍

함수형 프로그래밍은 대입문을 사용하지 않는 프로그래밍이며, 작은 문제를 해결하기 위한 함수를 작성합니다.

스트림

Stream API는 데이터를 추상화하고, 처리하는데 자주 사용되는 함수들을 정의해두었다. 여기서 데이터를 추상화하였다는 것은 데이터의 종류에 상관 없이 같은 방식으로 데이터를 처리할 수 있다는 것을 의미하며, 그에 따라 재사용성을 높일 수 있다.

  • 예를 들어 배열 요소의 특정 기준에 따라 정렬(sorting)하거나,요소 중 특정 값은 제외하고 출력하는(filter)기능이 있습니다. 이렇게 여러 자료의 처리에 대한 기능을 구현해 놓은 클래스가 스트림(stream)입니다.

  • 스트림을 활용하면 배열,컬렉션 등의 자료를 일관성 있게 처리할 수 있습니다.

  • 자료에 따라 기능을 각각 새로 구현하는 것이 아니라 처리해야 하는 자료가 무엇인지와 상관없이 같은 방식으로 메서드를 호출할 수 있기 때문입니다.(다른말로는 '자료를 추상화했다'라고 표현합니다.)

  • 배열을 예로 들어보겠습니다.
    (정수 5개의 요소로 가진 배열이고, 이를 모두 출력하는 출력문입니다.)

int[] arr = {1,2,3,4,5};
for(int i =0; i<arr.length; i++){
System.out.println(arr[i]);
}
  • 이 배열에 대한 스트림을 생성하여 추출하면 다음과 같습니다.
int[]arr ={1,2,3,4,5};
Arrays.stream(arr).foreach(n->System.out.println(n));
  • 무엇인가 상당히 간단해졌습니다. 스트림을 생성하고 미리 구현되어 있는 forEach()메서드를 사용하여 배열의 요소를 하나씩 꺼내어 출력할 수 있습니다.

스트림 연산

스트림 연산의 종류에는 크게 중간 연산과 최종 연산 두가지가 있습니다.

  • 중간연산 : 자료를 거르거나 변경하여 또 다른 자료를 내부적으로 생산합니다.
  • 중간연산 : 생성된 내부 자료를 소모해 가면서 연산을 수행합니다.
    (따라서 최종 연산은 마지막에 한 번만 호출됩니다.)
  • 그리고 최종연산이 호출되어야 중간 연산의 결과가 만들어집니다.

중간연산-filter(),map()

  • filter()는 조건을 넣고 그 조건에 맞는 참인 경우만 추출하는 경우에 사용합니다.

    문자열 배열이 있을 때 문자열의 길이가 5이상인 경우만 출력하는 코드는 다음과 같습니다.

sList.stream().filter(s -> s.length() >= 5).foreach(s -> System.out.println(s));
  • map()은 클래스가 가진 자료 중 이름만 출력하는 경우에 사용합니다.

    예를 들어 고객 클래스가 있다면 고객 이름만 가져와서 출력할 수 있습니다.map()은 요소들을 순회하여 다른 형식으로 변하기도 합니다.

customerList.stream().map(c -> c.getName()).forEach(s -> System.out.println(s));

-filter(),map() 둘 다 함수를 수행하면서 해당 조건이나 함수에 맞는 결과를 추출해 내는 중간 역할을 합니다.

최종 연산-forEach(),count(),sum(),reduce()

최종 연산은 스트림의 자료를 소모하면서 연산을 수행하기 때문에 최종 연산이 수행되고 나면 해당 스트림은 더 이상 사용할 수 없습니다.

  • 최종 연산은 결과를 만드는데 주로 사용합니다.forEach()는 앞에서도 보았듯이 요소를 하나씩 꺼내는 기능을 합니다.
  • 통계용으로 사용하는 sum(),count()는 배열 요소의 합계를 구한다든가 개수를 출력하는 등의 연산을 수행합니다.

스트림 생성하고 사용하기

정수 배열에 스트림 생성하고 사용하기

import java.util.Arrays;

public class IntArrayTest {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};

        int sumVal = Arrays.stream(arr).sum();
        int count = (int)Arrays.stream(arr).count();
        //count()메서드의 반환 값이 Long이므로 int형으로 변환시켜주어야 합니다.

        double average = Arrays.stream(arr).average().getAsDouble();
        //average()의 리턴값 : OptionalDouble
        //getAsDouble()은 OptionalDouble타입을 Double타입으로 바꿔준다.

        int max = Arrays.stream(arr).max().getAsInt();
        //max()의 리턴값 : OptionalInt

        int min = Arrays.stream(arr).min().getAsInt();


        System.out.println("총합: "+sumVal);
        System.out.println("수 :"+count);
        System.out.println("평균 : "+average);
        System.out.println("최고값"+max);
        System.out.println("최소값"+min);

    }
}

Collection에서 스트림 생성하고 사용하기

Collection 인터페이스를 구현한 클래스 중 가장 많이 사용하는 ArrayList에 스크림을 생성하고 활용해 보겠습니다.

List<String>sList = new ArrayList<>();
sList.add("Tomas");
sList.add("Edward");
sList.add("Jack");
  • 위와 같이 문자열을 요소로 가지는 ArrayList가 있습니다. 이 ArrayList의 스크림을 생성하여 출력하고,정렬하는 예를 살펴보겠습니다.
메서드설명
Stream<E> stream()스크림 클래스를 반환합니다.
  • Collection에서 stream()메서드를 사용하면 이 클래스는 제네릭형을 사용해 다음과 같이 자료형을 명시할 수 있습니다.
Stream<String> stream = sList.stream();
  • 이렇게 생성된 스트림은 내부적으로 ArrayList의 모든 요소를 가지고 있습니다.각 요소를 하나씩 출력하는 기능을 구현해 봅시다.
  • 모든 요소를 하나씩 가져와서 처리할 때 스트림의 forEach()메서드를 활용합니다.
Stream<String>stream = sList.stream();
stream.forEach(s -> System.out.println(s));
  • 이렇게 forEach()메서드는 내부적으로 반복문이 수행됩니다. 그럼 forEach()괄호 안에 구현되는 람다식의 의미를 알아봅시다.

  • forEach() 메서드가 수행되면 요소가 하나씩 차례로 변수 s에 대입되고 이를 매개변수로 받아 출력문이 호출됩니다.

  • 이번에는 ArrayList에 저장된 이름을 정렬하여 그 결과를 출력해 봅니다.

  • 앞에서 stream변수에 스트림을 생성했지만 forEach()메서드가 수행되면서 자료가 소모되었습니다. 따라서 스트림을 새로 생성해야 합니다.

Stream<String> stream2 = sList.stream()
stream2.sorted().forEach(s -> System.out.println(s));
  • 여기서는 중간 연산으로 정렬을 위한 sorted()메서드를 호출하고, 최종 연산으로 출력을 위해 forEach()메서드를 사용합니다.
  • sorted()메서드를 사용하려면 정렬 방식에 대한 정의가 필요합니다.
  • 따라서 자료 클래스가 Comparable 인터페이스를 구현해야 합니다.
  • 만약 구현되어 있지 않다면 sorted()메서드의 매개변수로 Comparator인터페이스로 구현한 클래스를 지정할 수 있습니다.
  • ArrayList 이외에 다른 Collection의 자료도 같은 방식으로 정렬하고 출력 할 수 있습니다.(이것이 스트림의 장점)

    지금까지의 코드를 정리한 예제입니다.

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class ArrayListStreamTest {
    public static void main(String[] args) {
        List<String> sList = new ArrayList<>();
        sList.add("Tomas");
        sList.add("Edward");
        sList.add("Jack");

        Stream<String> stream = sList.stream();
        stream.forEach(s -> System.out.println(s+" "));
        System.out.println();

        sList.stream().sorted().forEach(s -> System.out.println(s));
        
    }
}

스트림의 특징

  • 자료의 대상관 관계없이 동일한 연산을 수행한다.
    -배열에 저장된 요소값을 출력하기, 조건에 따라 자료를 추출하기, 자료가 숫자일 때 합계,평균을 구하기
  • 한번 생성하고 사용한 스트림은 재사용할 수 없다
    -어떤 자료에 대한 스트림을 생성하고 이 스트림에 메서드를 호출하여 연산을 수행했다면 해당 스트림을 다시 다른 연산에 사용할 수 없습니다.
    -만약 다른 기능을 호출하려면 스트림을 새로 생성해야 합니다.
  • 스트림의 연산은 기존자료를 변경하지 않는다.
    -스트림을 생성하거나 정렬한다거나 합을 구하는 등의 여러 연산을 수행한다고 해서 기존 배열이나 컬렉션이 변경되지 않습니다.
  • 스트림의 연산은 중간 연산과 최종 연산이 있다.
    -스트림에 중간연산은 여러개가 적용될 수 있고, 최종연산은 맨 마지막에 한 번만 적용됩니다.
    -만약 중간 연산이 여러개 호출되었더라도 최종 연산이 호출되어야 스트림의 중간 연산이 모두 적용됩니다.

스트림을 활용하여 여행객의 여행 비용 계산하기 예제

  • 우선 고객 클래스를 정의합니다. 고객 클래스는 이름, 나이, 비용을 멤버 변수로 가지며, 멤버 변수에 대한 get()메서드만 제공합니다.
package 스트림And람다식;

public class TravelCustomer {
    private String name;    //고객이름
    private int age;    //나이
    private int price;  //가격

    public TravelCustomer(String name, int age, int price) {
        this.name = name;
        this.age = age;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public int getPrice() {
        return price;
    }

    @Override
    public String toString() {
        return "name: "+name+"age: "+age+"price: "+price;
    }
}
  • 세명의 고객을 ArrayList에 추가하고 이에 대한 스트림을 생성하여 다음 연산을 수행해 봅니다.
    (고객의 명단을 출력합니다, 여행의 총 비용을 계산합니다, 고객 중 20세 이상인 사람의 이름을 정렬하여 출력합니다.)
    -만약 스트림을 사용하지 않고 위 내용을 구현한다면 코드를 여러 번 반복해서 사용해야 할것입니다.
    하지만 미리 구현되어 있는 스트림의 메서드로 코드를 간결하게 작성할 수 있습니다.
import java.util.ArrayList;
import java.util.List;

public class TravelTest {
    public static void main(String[] args) {
        //고객생성
        TravelCustomer customerLee = new TravelCustomer("이순신",40,100);
        TravelCustomer customerKim = new TravelCustomer("김유신",20,100);
        TravelCustomer customerHong = new TravelCustomer("홍길동",13,50);

        List<TravelCustomer> customerList = new ArrayList<>();
        //ArrayList에 고객 추가
        customerList.add(customerLee);
        customerList.add(customerKim);
        customerList.add(customerHong);

        System.out.println("==고객 명단 추가된 순서대로 출력==");

        customerList.stream().map(c ->c.getName()).forEach(s -> System.out.println(s));

        int total = customerList.stream().mapToInt(c ->c.getPrice()).sum();
        System.out.println("총 여행 비용은: "+total+" 입니다.");
        //여기서 mapToInt메서서드는 각 고객이 지불한 비용을 가져와서 정수로 변환후 sum()으로 합을 구합니다.

        System.out.println("==20세 이상 고객 명단 정렬하여 출력 ==");
        customerList.stream().filter(c ->c.getAge()>=20)
                .map(c ->c.getName()).sorted().forEach(s -> System.out.println(s));

    }
}

map,filter,sorted정리

  • map은 요소들을 특정조건에 해당하는 값으로 변환해 줍니다.
    -요소들을 대,소문자 변형 등 의 작업을 하고 싶을떄 사용 가능 합니다.

  • filter는 요소들을 조건에 따라 걸러내는 작업을 해줍니다.
    -길이의 제한, 특정문자포함 등 의 작업을 하고 싶을때 사용 가능합니다.

  • sorted는 요소들을 정렬해주는 작업을 해줍니다.

profile
블로그 이전합니다! https://jyyoun1022.tistory.com/
post-custom-banner

0개의 댓글