ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스트림 생성 (collection , 배열)
    JAVA/스트림, 컬렉션 프레임워크, 람다 2024. 6. 13. 10:46

    ▤ 목차

       

      스트림 소개

      단어의 뜻은 '흐름'이다. 어떤 데이터의 흐름으로 유추해 볼 수 있다.

       

      자바 8버전부터 추가된 컬렉션 요소를 하나씩 참조해서 람다식(함수적 인터페이스)으로 처리할 수 있도록 해주는 반복자를 의미한다.

      • 스트림 인터페이스는 컬렉션, 배열.. 등의 저장 요소를 하나씩 참조하여 인터페이스(람다식)을 적용하며 반복 처리가 가능하게 한다.
      • 반복자 역할을 한다.(internal iteration 내부 반복을 이용)
      • 정렬, 집계, 빅데이터 처리 등도 가능하다.
      • 1회용이기에 람다식과 많이 사용한다.
      • 내부 반복으로 작업 처리를 진행하며 원본 데이터를 변경하지 않는다.
      • 새로운 요소를 추가하는 것은 불가능하다.
      • 기존 요소 삭제는 불가능하다.
      • 무한 요소를 지원한다. (일부 데이터를 제한하여 활용)
      자바 8 이전에는 컬렉션 처리에는무조건 Iterator가 이용되었으나, 선택의 폭이 넓어졌다.

       

      언제 사용할까?

      • 원소들의 시퀀스를 일관되게 반환하는 경우
      • 필터링을 원하는 경우
      • 컬렉션을 모을 경우
      • 시퀀스에서 특정 조건을 만족하는 원소를 찾는 경우

      값을 변형하지 않는 로직에서 사용하는 것이 좋다. 

       

      [ 반복문을 사용하는 경우 ]

      더보기
      1. 지역변수를 읽고 수정해야 할 때
      2. return, break, continue를 사용해야 할 

       

       


       

       

       

      스트림 생성, 중간연산, 최종 연산

      스트림은 생성 방식이 달라도 데이터 처리 방법은 같다는 장점이 있다.

      아래 코드를 보면 collection과 배열의 스트림 생성 방식은 다르다.

      하지만 사용하는 방식은 동일하다.

      그래서 사용방식도 물론 중요하지만 각 타입을 스트림으로 생성하는 방법도 중요하다.

      코드로 확인해보자.

      💻 스트림 생성하기

      public class MyStream {
      
      	public MyStream() {
      		test1(); // Stream 생성
      	}
      
      	private void test1() {
      		//1) collection의 스트림 생성
      		List<String> list = Arrays.asList("a","b","c");
      		Stream<String> listStream = list.stream();
      		
      		//2) 배열의 스트림 생성
      		Stream<String> stream1 = Stream.of("a","b","c");
      		Stream<String> stream2 = Stream.of(new String[]{"a","b","c"});
      		Stream<String> stream3 = Arrays.stream(new String[]{"a","b","c"});
      		Stream<String> stream4 = Arrays.stream(new String[]{"a","b","c"},0,3); //3미만
              
              //위의 스트림 출력
           		stream1.forEach(System.out::println);
      		System.out.println();
      		stream2.forEach(System.out::println);
      		System.out.println();
      		stream3.forEach(System.out::println);
      		System.out.println();
      		stream4.forEach(System.out::println);
      		System.out.println();
              
      		
      		//3) 원시(기본형 데이터) stream 생성
      		IntStream istream = IntStream.range(5, 10);
      		int hap = 0;
      		istream.forEach(para -> System.out.println(para));
      	}
      }

       

       

      👏 중요

      List<String> list = Arrays.asList("레밍스","팩맨","마리오");
      list.add("소닉");

       새로운 요소 추가 불가, 기존 요소 삭제 불가

       

      Exception in thread "main" java.lang.IllegalStateException : stream has already been operated upon or closed

      위와 같은 에러가 나타나는 이유는 스트림을 2번 사용했기 때문이다.

      stream 객체는 한 번만 사용하고 사라진다.  2번 이상 사용했을 경우 이미 사용했다는 오류가 발생한다.

       

       

      기본형(원시) 데이터 스트림 생성 코드

      더보기

      [기본형 스트림 생성]

      임의의 수

      IntStream intStream = new Random().ints();
      intStream.limit(3).forEach(System.out::println);

       

       

      특정 범위의 연속된 정수

      LongStream longStream = LongStream.range(5,10);
      longStream.limit(4).forEach(System.out::println);

       

      //(begin,end)
      DoubleStream doubleStream =new Random().doubles(0.1,5);
      doubleStream.limit(5).forEach(System.out::println);

       

       

      람다식 스트림 생성 코드

      더보기

      Iterate 사용

      //seed값(람다식의 첫번째 값:시작값)과 람다식을 받는다
      public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f){..}
      Stream.iterate(0, n ->n+3)
            .limit(5).forEach(System.out::println);

       

       

       


       

      💻 중간 연산과 최종연산

      스트림은 생성부터 중간연산, 최종 연산으로 이루어져 있다.

       

      public class basic {
          public static void main(String[] args) {
              List<String> jjanggus = Arrays.asList("짱구","훈","유리","철수","맹구","훈");
      
              jjanggus.stream() //객체 생성
                      .distinct().sorted().filter(i->i.length() >1) //중간 연산
                      .forEach(System.out::println); //최종연산
          }
      }

       

      중간 연산을 통해 원하는 형태로 데이터 가공이 가능하다. 위의 코드처럼 여러 개의 메서드를 이어 붙일 수 있다.

      최종 연산은 원하는 형태로 변환하는 것이다.

       

      결과를 보면 아래와 같다.

       

      스트림에 존재하는 메서드는 엄청 많다. 아래 링크는 오라클 API이다. 확인해 보며 필요한 메서드를 사용하면 된다.

      https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#method.detail

       

       

      🔶 peek()

      많은 메서드 중 peek() 메서드는 한번 짚고 넘어가고자 한다.

       

      forEach()는 내부반복을 하는 메서드를 말한다. 즉, forEach() 문은 스트림의 데이터를 소모하며 주로 출력하는 용도로 사용한다.

      return 값이 void라서 최종 처리 메서드로 쓰일 수 있지만 peek메서드는 return값이 stream이라서 최종 처리메서드로 불가능하다.  

       

      비슷한 역할을 하지만 stream을 사용하다 디버깅을 위해 만들어진 메서드이다.

       

      오라클 공식문서 설명을 풀어보자면, 스트림의 요소로 구성된 스트림을 반환한다.. 병렬 스트림 파이프라인의 경우, 업스트림 작업을 통해 요소를 사용할 수 있게 된 스레드와 시간에 관계없이 작업을 호출할 수 있다.

       

       

         이 메서드는 주로 파이프라인의 특정 지점을 지나
      흐르는 요소를 확인하려는 디버깅을 지원하기 위해 존재한다.

       

       

       

      Stream.of("one", "two", "three", "four")
               .filter(e -> e.length() > 3)
               .peek(e -> System.out.println("Filtered value: " + e))
               .map(String::toUpperCase)
               .peek(e -> System.out.println("Mapped value: " + e))
               .collect(Collectors.toList());

       

       

       

       


       

       

       

       

      ✔외부 반복자와 내부 반복자 비교

       

      • 외부 반복자(extenal iterator) : 개발자가 코드로 직접 컬렉션 요소를 반복해서 가져오는 패턴이다. (for, while, iterator)
      • 내부 반복자(internal iterator) : 컬렉션 내부에서 요소들을 반복시켜 처리하고, 개발자는 반복당 처리해야 할 코드(보통 콜백 함수로 전달)만 제공하는 코드 패턴이다.(stream)

       

       

      💻 코드로 보기

      public class MyStream {
      
      	public MyStream() {
      		test2(); // 컬렉션에 스트림 적용
      	}
      
      	private void test2() {
      		System.out.println();
      		List<String> list = Arrays.asList("레밍스","팩맨","마리오");
      		
              //외부 반복자 사용. 전통적인 방식
      		Iterator<String> iter = list.iterator();
      		while(iter.hasNext()) {
      			System.out.println(iter.next());
      		}
      		System.out.println();
      		for(String str : list) { //향상된 for문
      			System.out.println(str);
      		}
      		
      		System.out.println();
              //내부 반복자 사용
      		Stream<String> stream = list.stream(); //Stream 객체 생성
      		stream.forEach(str ->System.out.println(str)); //내부 반복
      		//stream.forEach(str ->System.out.println(str)); //스트림은 일회용
      		
      		//다시 사용하려면 다시 만들어야한다.(스티림 객체 생성 후 출력)
      		list.stream().forEach(str ->System.out.println(str));
      		list.stream().forEach(System.out::println);
      		System.out.println();
      		//스트림을 사용하여 체이닝 작업 : 모든 필요한 작업을 단일 스트림 파이프라인(일련의 처리 단계)에서 처리 가능
      		//어떤 스트림의 요소들의 합을 구하는 과정에서 요소값을 먼저 출력하고 싶은 경우
      		//파이프라인: 여러가지 작업을 한줄로 표현
      		int sum = IntStream.of(1,3,5,7).peek(System.out::println).sum();
      		System.out.println("합은 : "+ sum);
      		
      		list.stream().peek(System.out::println).forEach(System.out::println);
      		
      		System.out.println("\n 병렬처리");
      		Stream<String> streamPar = list.parallelStream(); //병렬 스크림 객체 생성
      		streamPar.forEach(str -> System.out.println(str)); //결과 처리는 램덤이다.
      		
      		System.out.println("\n 정렬");
      		Stream<String> streamSort = list.stream().sorted(); //기본은 오름차순
      		Stream<String> streamSort2 = list.stream().sorted(Comparator.reverseOrder());
      		streamSort.forEach(System.out::println);
      		streamSort2.forEach(System.out::println);
      		
      		Stream<String> streamSort3 = list.stream().sorted().distinct().sorted(); //중복 제거
      		streamSort3.forEach(System.out::println);
      	}

       

      👻체이닝 작업(파이프라인)?

      체이닝 작업은 쇠사슬을 생각하면 된다. 디자인 패턴 중 빌더 패턴도 메서드 체이닝 방식을 사용하는 방식이다.

      메서드를 줄줄이 엮어서 메소드를 계속해서 사용할 수 있게끔 하는 방법이다.

      스트림에서 많이 사용하는 방식이다.

      변수.메서드().메서드().메서드();

      중간 연산과 최종 연산을 수행하도록 하기 위해 파이프라인을 사용한다.

      파이프라인은 여러 개의 스트림이 연결되어 있는 것을 뜻한다. 연결된 스트림에서 최종 연산을 제외하고는 중간 연산이라고 한다. 

       

       


       

       

      ✔ 스트림 특징

      • 간결하고 직관적인 코드를 제공한다. (익숙해지면 무리하게 사용하면 오히려 가독성이 떨어진다.) 
      • 원본 데이터를 훼손하지 않는다. (객체의 값을 사용하기만 할 뿐 변경하지 않는다. 원본과 무관한 새로운 객체 생 )
      • 스트림 파이프라인은 지연 연산을 제공한다.
      • 최종 연산 메서드를 사용하면 스트림이 닫혀버린다. 만약 최종 연산을 여러 번 하고 싶다면 그때마다 새로운 스트림 객체를 생성해야 한다.

       

      ⌨ 컬렉션과 스트림

      컬렉션에 정의된 메서드 add, remove, contain들은 반복문을 순회하며 데이터에 하나하나 접근하여 요소를 추가하거나 빼는 처리를 할 수 있다. 

      스트림은 자료를 더하고 빼는 작업을 할 수 없다.

      이름과 같이 연속된 자료들을 다루고 연산하고 조건에 필터링하는 역할을 한다.

      스트림은 내부 반복으로 작업하기 때문에 코드 처리 과정도 드러나지 않는다.

       

       

       


       

       

       

      지연 연산(Lazy evaluation)

      직역하면 게으른 연산이라고 한다. 불필요한 연산을 피하기 위해 연산을 지연시키는 것을 말한다.

      최종 연산이 수행되기 전까지 중간 연산이 수행되지 않는다.

      중간 연산을 호출해도 즉각적으로 연산이 수행되지 않는다.

       

      ⌨ 게으른 연산(Lazy) VS 조급한 연산(Eager)

      🔶 즉시 연산(Eager)

      실행할 코드가 보이는 순간 곧바로 실행되는 연산을 말한다. 특정 작업의 결과가 필요하다면 미리 연산하는 것이 성능적으로 우월하지만 그렇지 않은 경우 낭비가 발생한다.

       

      🔶 지연 연산(Lazy)

      코드를 만났을 때 바로 실행하는 것이 아닌 실행 결과가 필요해지는 시점에 실행하는 연산을 말한다.  

      객체가 실제로 사용되는 시점까지 객체 생성을 늦추는 것을 의미한다.

       

       

      💻 게으른 연산 코드로 이해하기

      🔶Short-circuiting

      public class LazyTest {
          @Test
          public void lazyRes() {
              //&& 연산
              boolean result = true && true;
              assertTrue(result);
      
              result = false && true;
              assertFalse(result);
      
              result = true && false;
              assertFalse(result);
          }
      }

       

      이런 코드를 확인해 보자. and 연산은 첫 번째 값이 false면 뒤의 조건들을 계산할 의미가 사라진다. (때문에 보통 and 연산 시 false의 값이 많이 나올 것을 예상하는 계산을 앞쪽에 두는 경우가 많다.)

      or 연산도 마찬가지이다. true || false 인 경우에는 앞쪽이 이미 true값이기에 뒤에 false인 값은 계산하지 않는다.

       

       

       

       

      👏 참고 사이트

      https://velog.io/@minseojo/Java-Lazy-Evaluation-%EC%A7%80%EC%97%B0-%EC%97%B0%EC%82%B0

       

      [Java] Lazy Evaluation (지연 연산)

      Lazy Evaluation는 불필요한 연산을 피하기 위해 연산을 지연 시켜 놓았다가 필요할 때 연산하는 방법이다.

      velog.io

       

      https://bugoverdose.github.io/development/stream-lazy-evaluation/

       

      [Java] 스트림: 지연 연산과 최적화

      Stream API의 주요 특성인 지연 연산의 의미, 그리고 이를 기반으로 일어나는 루프퓨전과 쇼트서킷에 대해 알아보자.

      bugoverdose.github.io

       

      😊정리

      지나친 스트림 사용은 오히려 가독성을 떨어뜨린다.

      복잡한 로직은 스트림과 반복문을 적절히 조합하며 상용하는 것이 좋다.

      스트림 파이프라인이 실행하게 되면 JVM은 스트림 연산을 실행하지 않는다. 최소한의 필수적인 작업을 위해 지연 연산을 위한 준비작업을 수행한다. 

      스트림 파이프라인이 어떤 중간연산과 최종연산으로 구성되어 있는지 검사를 하고 JVM은 미리 최적화 방식을 계획하고 스트림 연산을 시작한다. 

    Designed by Tistory.