다음과 같은 예시를 먼저 살펴보자
int []arr={1,2,3,4,5,6,7,8,9,10};
int sum=0;
for(int value: arr){
if(value%2==0)sum+=value;
}
System.out.println(sum);
배열 arr에서 짝수만을 뽑아서 그 수들의 합을 구하는 로직이다.
이 로직을 stream을 사용하면
System.out.println(Arrays.stream(arr).filter(i->i%2==0).sum());
이렇게도 표현이 가능하다.
Java의 stream을 사용하면 다음과 같은 2가지의 장점을 가진다.
첫번째 기존에 사용하던 변수 sum과 value의 접근을 없애서 side effect를 줄일 수 있다.
두번째로는 기존에는 요구사항이 변경시, 코드도 변경해야하지만, 지금은 filter에 들어가는 인자만 바꾸면 된다는 장점을 가진다.
위 두가지의 장점을 가지는 프로그래밍을 함수형 프로그래밍이라고 한다.
조금 더 자세히 살펴보자
함수형 프로그래밍은 이름 그대로 함수를 통해서 프로그래밍한다는 것이다. 함수를 통해서 인자로 넘기고, 함수를 변수로 사용하고, 함수를 반환값으로 사용하는 프로그래밍을 의미한다.
함수를 일급 객체처럼 활용하는 것도 위의 문장과 동의어이다.
일급 객체는 프로그래밍 언어에서 변수 및 데이터처럼 사용될 수 있는 것을 의미한다.
즉 변수가 아닌것을 변수처럼 사용할 수 있는 객체를 일급 객체라고 한다.
일반적으로 자바에서는 Function 인터페이스를 통해서 보통 구현이 가능하다.
public static int applyFunction(Function<Integer, Integer> func, int value) {
return func.apply(value);
}
public static void main(String[] args) {
Function<Integer, Integer> doubleIt = x -> x * 2;
System.out.println(applyFunction(doubleIt, 10)); // 20
}
이를 통해서 위에서 정의한 함수형 프로그래밍의 정의를 실현할 수 있다.
함수형 프로그래밍을 한다는 것은 위험성 즉 Side Effect를 줄여주는 것에 큰 의의가 있다.
하나의 함수내에서 변수를 줄일 수 있다는 것은 위처럼 여러 부수효과를 줄이는데 아주 큰 기여를 할 수 있고, 이를 통해서 예상치 못한 실수를 줄일 수 있다는데 매우 큰 의의가 있다.
특히 stateless하게 유지해야하는 싱글톤 컨테이너 기반의 스프링 환경에서 왜 함수형 프로그래밍 언어인 코틀린을 사용하는 이유를 간접적으로나마 이해할 수 있었다.
또한 만들어둔 함수를 아래처럼 변수로 지정하고,
인자로 받아서 사용하면 보다 코드의 재사용성을 올릴 수 있다.
IntPredicate isEven = i -> i % 2 == 0;
System.out.println(Arrays.stream(arr).filter(isEven).sum());
후에 짝수인지를 반환하는 로직을 작성하지 않고 위처럼 변수로 선언해서도 사용할 수 있다.
자바에서 함수형 프로그래밍을 얘기할 때 stream이야기를 빼놓을 수 없다고 생각한다.
실제 스프링의 로직을 훨씬 간결하고, 부수효과를 줄일 수 있게 해준다.
stream의 활용법에 대해서 한번 알아보자
List<Integer> list=new ArrayList<>();
Map<Integer,Integer> map=new HashMap<>();
Arrays.stream(arr)
list.stream()
map.keySet()
map.values().stream()
위처럼 List와 set등의 컬렉션은 stream()을 바로 사용이 가능하다.
하지만 Array는 한번 감싸서 스트림을 사용한다.
Arrays.stream(arr).filter(i->i%2==0)
위처럼 filter에서는 stream을 돌면서 각 값들이 해당 조건을 만족하는지를 판별하고 true로 판별하면 추출해낸다.
System.out.println(Arrays.toString(Arrays.stream(strs)
.filter(str -> str.startsWith("aaa")).toArray()));
이런식으로 시작하는 접두사를 통해서 추출해낼 수도 있다.
map은 stream의 값을 그대로 사용하는 것이 아닌, 변환해서 사용하는 경우에 쓸 수 있다.
System.out.println(Arrays.toString(Arrays.stream(arr).map(i->i*2).toArray()));
배열의 값을 모두 다 2배로 증가시킬 수 있다.
List<UserDTO> userDTOList = userEntityList.stream()
.map(user -> new UserDTO(user.getId(), user.getName(), user.getEmail()))
.collect(Collectors.toList());
스프링 환경에서는 이렇게 entity를 dto로 변환하거나 반대의 상황에서 사용할 때 map을 이용하면 편하다.
개인적으로 Builder패턴과 함께 사용하는 것을 굉장히 추천한다.(편하고 복잡하게 변수를 만들 필요가 없다)
말그대로 컬렉션을 정렬할 때 사용한다.
arr=Arrays.stream(arr).sorted().toArray();
중복을 제거하는 set연산을 해준다.
List<Integer> uniqueNumbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5)
.stream()
.distinct()
.collect(Collectors.toList());
처음부터 연속된 몇개, 처음부터 몇개를 건너 뛰고 배열을 준다.
List<String> limited = list.stream()
.limit(2) // 처음 2개만 선택
.collect(Collectors.toList());
List<String> skipped = list.stream()
.skip(1) // 첫 번째 요소를 건너뜀
.collect(Collectors.toList());
최종 연산을 통해서 스트림의 형태를 객체의 형태로 역직렬화하거나 결과값을 반환하는 과정이다.
우리가 가장 많이 사용하는 패턴이 아닐까 싶다
반환을 원하는 컬렉션의 형태를 넣으면 된다.
List<String> collectedList = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
뭔가 너무 많이 사용해서 collect까지 쓰기 번거롭다는 생각을 좀 했는데
아래와 같이 단순하게 중간연산 후 toArray를 하거나 toList를 사용해서도
collect의 기능을 대체가 가능하다.
arr=Arrays.stream(arr).sorted().toArray();
가공한 결과를 통해서 반복문을 수행하면서 특정 함수를 수행하고 싶을 때 사용한다.
list.stream().forEach(System.out::println);
가공한 결과의 갯수를 구하는 것이다.
long count = list.stream().filter(s -> s.contains("a")).count();
가공한 결과를 합쳐서 결과를 줄일 때 사용한다.
List<String> words = Arrays.asList("Hello", "World", "Java");
String sentence = words.stream()
.reduce("", (a, b) -> a + " " + b);
System.out.println(sentence.trim()); // "Hello World Java"
이것처럼 문자열을 합칠 때나, 숫자를 더하는 등의 작업을 가능하게 한다.
초기값과 함수를 인자로 넣어주면 해당 작업을 수행한다.
근데 이것도 .sum(), .average(), .max(), .min() 형태로도 제공해주니,
편하게 사용이 가능하다(Java 8부터 사용이 가능해졌다고 한다.
System.out.println(Arrays.stream(arr).filter(isEven).sum());
모두 boolean값을 반환한다.
왼쪽 부터 하나라도 만족하면 true, 모두 만족하면 true, 모두 만족하지 않으면 true를 반환한다.
뭔가 if문 이후에 사용하면 매우 편할 것 같은 느낌?(굳이 함수로 따로 안 빼도 되니까)
if(Arrays.stream(arr).anyMatch(a->a%2==0)){
System.out.println("짝수가 존재합니다!");
}
이런식으로 사용이 가능할 것이다.