Java Stream 정리 - with 메서드 참조

Chooooo·2023년 9월 4일
0

TIL

목록 보기
12/28
post-thumbnail
post-custom-banner

😎 Java Stream

자바8에서 추가된 Stream은 람다를 활용할 수 있는 기술 중 하나이다. 자바8 이전에는 배열이나 컬렉션에 있는 요소들을 다룰 때 for문을 사용하여 하나씩 꺼내서 사용했었다. 단순한 로직인 경우에는 괜찮지만 코드가 길어지거나 복잡해질수록 여러 로직이 섞이고 다루기가 쉽지 않았다.

🎈 자바 8 이후 stream 사용 방식은 훨씬 간결하고 익숙해진다면 가독성도 좋은 코드가 된다.

stream은 사전적 의미로 시내,흐르다,이어지다 라는 의미를 가지고 있다. 프로그래밍 언어에서는 데이터의 흐름을 의미한다. 배열 또는 컬렉션 인스턴스에 여러 함수를 조합하여 원하는 결과를 가져올 수 있다. 또한 사용하는 함수에서 람다표현식을 통해 간결하게 코드를 구현할 수 있다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있다.

  • 또 다른 장점은 병렬처리(multi-threading)가 가능하다는 것이다. 따라서 쓰레드를 이용해 많은 요소들을 빠르게 처리할 수 있다.

😉 스트림 특징

  • 스트림은 데이터 소스를 변경하지 않는다.

스트림은 데이터 소스로 부터 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않는다. 필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수 있다.

  • 스트림은 일회용이다.

스트림은 Iterator처럼 일회용이다. Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야한다.

  • 스트림은 작업을 내부 반복으로 처리한다.

스트림을 이용한 작업이 간결할 수 있는 비결중의 하나가 바로 ‘내부 반복’이다. 내부 반복이라는 것은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다.

😉 스트림 사용 설명

Stream을 사용할 때는 크게 3가지로 나뉜다.

생성하기 > 중간연산 > 최종연산

Collections같은 인스턴스.생성하기().중개연산().최종연산()

😁 생성하기 (스트림 인스턴스 생성)

  • 컬렉션(collection)의 경우 .stream()으로 스트림 변환
Stream<Integer> stream = Arrays.asList(1, 2, 3, 4, 5).stream();
  • 객체 배열로부터 스트림 생성
Stream.of("a", "b", "c");
Stream.of(new String[]{"a", "b", "c"});
Arrays.stream(new String[]{"a", "b", "c"});
Arrays.stream(new String[]{"a", "b", "c"}, startIndex, EndIndex+1);
  • 람다식 iterate(), generate()
//Stream.iterate(T seed, UnaryOperator f); 이전 결과에 종속적
Stream.iterate(0, n->n+2); //0, 2, 4, ... 무한 스트림<br>
//generate는 초기값 지정x, 이전 결과와 무관하다.
Stream.generate(Math::random);
Stream.generate(()->1);
  • 사실 실무에서 컬렉션을 가독성있게 그리고 효율적으로 하기 위해 대부분 사용해

스트림은 크게 중간연산과 최종연산으로 나눌 수 있다. filter, map, limit 등 파이프라이닝 할 수 있는 스트림 연산을 중간 연산이라고 하고, count, colelct 등 스트림을 닫는 연산을 최종연산이라고 한다.

  • 왜 스트림은 연산을 두가지로 나누는 걸까?

방금 중간연산은 파이프라이닝을 할 수 있다고 했다. 이 말은 중간 연산자는 스트림을 반환해야한다는 것이다. 중간 연산은 스트림 파이프라인에 실행하기 전까지 아무 연산도 수행하지 않고 중간 연산을 합친 다음 합쳐진 연산을 최종 연산으로 한번에 처리하게 된다.

중간 연산으로는 어떠한 결과도 생성할 수 없기 때문에 결과를 반환하는 최종 연산이 존재하는 것이다.

😁 스트림의 중간 연산

.distinct() //중복제거
.filter(Predicate<T> predicate) //조건에 안 맞는 요소는 제외
.limit(long maxSize) //maxSize 이후의 요소는 잘래냄
.skip(long n) //앞에서부터 n개 건너뛰기
.sorted() //기본 정렬로 정렬
.sorted(Comparator<T> comparator) //조건에 맞게 요소 정렬. 추가 정렬 기준을 제공할 때는 thenComparing()사용
    
 //스트림의 요소를 변환. ex) map(File::getName), map(s->s.subString(3))
.map(Function<T> mapper) 
    
 //요소에 작업수행. 보통 중간 작업결과 확인으로 사용. peek(s->System.out.println(s))
.peek(Consumer<T> action)
    
 //스트림의 스트림을 스트림으로 변환
 //ex) Stream<String> strStrm=strArrStrm.flatMap(Arrays::stream)
.flatMap()

😁 스트림의 최종 연산

void forEach(Consumer<? super T> action) //각 요소에 지정된 작업 수행
void forEachOrdered(Consumer<? super T> action) //병렬 스트림의 경우 순서를 유지하며 수행
long count() //스트림의 요소 개수 반환
    
Optional<T> max(Comparator<? super T> comparator) //스트림의 최대값 반환
Optional<T> min(Comparator<? super T> comparator) //스트림의 최소값 반환
    
Optional<T> findAny() //아무거나 하나 반환. 벙렬 스트림에 사용
Optional<T> findFirst() //첫 번째 요소 반환. 순차 스트림에 사용
    
boolean allMatch(Predicate<T> p) //모든 조건을 만족?
boolean anyMatch(Predicate<T> p) //조건을 하나라도 만족?
boolean noneMatch(Predicate<T> p) //모든 조건을 만족하지 않음?
    
Object[] toArray() //모든 요소를 배열로 반환
A[] toArray(IntFunction<A[]> generator) //특정 타입의 배열로 반환
    
//스트림의 요소를 하나씩 줄여가면서 계산
//아래에서 자세히 보자
Optional<T> reduce(BinaryOperator<T> accumulator) 

//데이터를 변형 등의 처리를 하고 원하는 자료형으로 변환해 줍니다.
//아래에서 자세히 보자.
collect( ~ )

참고 : Optional

Optional클래스는 값의 존재나 여부를 표현하는 컨테이너 클래스이다.

  • optional을 이용해 null확인 관련 버그를 피할 수 있다.
  • Optional은 값이 존재하는지 확인하고 싶고 값이 없을 때 어떻게 처리할 것인지 강제하는 기능을 제공한다.
  • isPresent()는 Optional이 값을 포함하면 참을 반환
  • ifPresent(Consumer block)은 값이 있으면 주어진 블록을 실행

🎈 참고 : Method Reference

  • Method reference(메소드 참조)란 ?
    class::methodName 구문을 사용하여 클래스 또는 객체에서 메소드를 참조할 수 있다.

🎈 람다식(Lambda Expression)의 가장 큰 장점 중 하나는 코드가 짧아진다는 것인데, 람다식에 메서드 참조를 사용하면 코드를 더 간결하고 가독성 있게 만들 수 있다.

메서드 참조는 이중 콜론(::)을 사용하여 클래스 이름과 메서드 이름을 구분하여, 람다식과 달리 인수를 전달할 필요가 없다. 인수는 메서드 참조 타입에 따라 처리된다.

// 람다식
str -> str.toString()
// 메서드 참조
String::toString

// 람다식
str -> str.length()
// 메서드 참조
String::length

// 람다식
(int x, int y) -> x.compareTo(y)
// 메서드 참조
Integer::compareTo

메소드 참조 유형

  • 정적 메소드에 대한 메소드 참조(class::StaticMethodName)
  • Object 인스턴스 메서드에 대한 참조(Object::instanceMethodName)
  • 특정 타입(또는 클래스)에 대한 인스턴스 메서드에 대한 메서드 참조(Class::instanceMethodName)
  • 생성자에 대한 메서드 참조(Class::new)

정적 메소드 참조

  • 아래와 같은 함수형 인터페이스가 존재한다고 가정
@FunctionalInterface
public interface IAdd {
  int add(int x, int y);
}

그리고 두 개의 매개변수 합산을 반환하는 MathUtils 클래스가 있습니다.

public class MathUtils {
  public static int AddElement(int x, int y) {
    return x + y;
  }
}

다음 예제는 람다식을 사용하는 방법과 메서드 참조를 사용하는 방법을 보여준다.

public class Main {
  public static void main(String args[]) {
    System.out.println("Lambda Expression");
    IAdd addLambda = (x, y) -> MathUtils.AddElement(x, y);
    System.out.println(addLambda.add(10, 20));

    System.out.println("Method Reference");
    IAdd addMethodRef = MathUtils::AddElement;
    System.out.println(addMethodRef.add(20, 40));
  }
}

Stream에서 메소드 참조

사실 메소드 참조는 람다식을 사용할 일이 많은 stream에서 많이 쓰게 될 것이다.
그럼 스트림에서 메소드 참조가 어떻게 쓰이는지 간단하게 알아보자.

List<String> list = List.of("A","AB","ABC","ABCD"); 

// 람다식 사용 
list.stream() 
	.map(str -> str.length()) 
	.forEach(str -> System.out.println(str)); 

// 메소드 참조 사용 
list.stream() 
	.map(String::length) 
	.forEach(System.out::println);

  • 파라미터 개수는 상관 없으므로 잘 활용하자.

정리하면, 메소드 참조를 사용할 떄 메서드가 선언된 클래스를 메서드 참조의 앞 부분에 명시해야 한다. 메서드 참조는 클래스명::메서드명 형식을 갖는다.

⚽예를 들어, convertToReportDtoResponse 메서드ReportDtoResponse 클래스에 선언되어 있으며 static 메서드인 경우에는 다음과 같이 메서드 참조를 사용할 수 있다.

Page<ReportDtoResponse> res = temp.map(ReportDtoResponse::convertToReportDtoResponse);

여기서 ReportDtoResponse::convertToReportDtoResponse에서 ReportDtoResponse는 메서드를 찾을 클래스를 나타내며, convertToReportDtoResponse는 실제 메서드 이름이다.

또한 temp.map(ReportDtoResponse::convertToReportDtoResponse)에서 tempReportDtoResponse::convertToReportDtoResponse 메서드의 인자로 사용된다.

map 메서드는 스트림 내의 각 요소에 대해 지정된 함수를 호출하며, 해당 함수의 인자로 현재 요소가 전달됩니다. 따라서 temp의 각 ReportEntity 요소가 convertToReportDtoResponse 메서드의 입력으로 사용되며, 그 결과로 ReportDtoResponse 객체가 생성됩니다. 이렇게 생성된 객체로 새로운 스트림이 생성되어 반환됩니다.

😎 쉽게 말해, map 메서드는 현재 요소를 다른 형태로 변환할 때 사용된다.

  • 메서드가 인스턴스 메서드인 경우, 해당 메서드를 호출할 객체도 앞에 명시해야 한다. 예를 들어:
someList.forEach(someObject::instanceMethod);

😎 여기서 someObject는 instanceMethod를 호출할 객체이며, instanceMethod는 someList의 각 요소에 대해 호출된다.

😓 Stream API 사용 예시

⚽ Filter(Predicate)

  • if문이라고 생각하면 될 것 같다.
    람다식의 리턴값은 boolean true면 다음 단계 진행, false면 버려진다.
classes.stream()
	.filter(c->c.getTitle().startWith("spring"))
	.forEach(c->System.out.println(oc.getId));

classes.stream()
	.filter(Predicate.not(OnlineClass::isClosed))
	.forEach(c->System.out.println(oc.getId));

⚽ distinct()

  • distinct()는 요소들의 중복을 제거하고 스트림을 반환한다.

⚽ Map(Function) 또는 FlatMap(Function)

  • Stream을 우리가 원하는 모양의 새로운 스트림으로 변환

    	- ex. 각각의 File에서 String name만 새로운 스트림으로
    • String 타입 요소를 짤라서 새로운 스트림으로
 map(File::getName)
 map(s->s.subString(3))

⚽ limit(long) 또는 skip(long)

  • ex. 최대 5개의 요소가 담긴 스트림을 리턴한다.
  • ex. 앞에서 3개를 뺀 나머지 스트림을 리턴한다.
Stream.iterate(10, i->i+1)
		.skip(10)
        .limit(10)
        .forEach(System.out::println)

⚽ anyMatch(), allMatch(), nonMatch()

  • ex. k를 포함한 문자열이 있는지 확인한다. (true 또는 false 리턴)
boolean test=javaClasses.stream()
				.anyMatch(oc->oc.getTitle().contains("k"));

⚽ findFirst() VS findAny()

  • 스트림을 직렬로 처리할 때는 차이가 없다.

  • 하지만 병렬로 처리할 경우에 차이가 생기는데,

    • findFirst()는 stream의 순서를 고려해, 가장 앞쪽에 있는 요소를 반환
    • findAny()는 멀티 쓰레드에서 가장 먼저 찾은 요소를 반환. stream의 뒤쪽에 있는 요소가 반환될 수도 있다.

collect 자세히 보기

  • Stream의 요소들을 원하는 자료형으로 변환할 수 있다.
stream.collect(Collectors.toSet()); //set으로 변환
stream.collect(Collectors.toList()); //list 변환
stream.collect(Collectors.joining()); //한개의 string으로 변환
stream.collect(Collectors.joining(", ")); //요소들 사이에 ","을 넣어서 한개의 string 반환

스프링과 연관 지어서 최종 사용 방법

  1. 걸러내기 : Filter(Predicate)
  2. 변경하기 : Map(Function), FlatMap(Function)
  3. 생성하기 : generate(Supplier) 또는 Iterate(T seed, UnaryOperator)
  4. 제한하기 : limit(long) 또는 skip(long)
  5. 스트림에 있는 데이터가 특정 조건을 만족하는지 확인 : anyMatch(), allMatch(), nonMatch()
  6. 개수 세기 : count()
  7. 스트림을 데이터 하나로 뭉치기 : reduce, collect, sum, max
profile
back-end, 지속 성장 가능한 개발자를 향하여
post-custom-banner

0개의 댓글