학습일지/Language

Java8 Stream - Optional - Date 정리

inspirit941 2020. 10. 28. 16:40
반응형

인프런 '더 자바- 자바8' 백기선님 강의내용 정리

Stream

정의: 연속된 데이터를 처리하는 Operation 모음.


Collection이 데이터를 모아놓은 자료구조라면, Stream은 이걸 토대로 데이터를 원하는 방식으로 처리하는 것.

따라서 데이터 저장소의 개념이 아니다.

특징

  • Function in Nature. 원본 데이터를 변경하지 않는다.
    Stream<String> stringStream = names.stream().map(String::toUpperCase);

    Stream으로 어떤 연산을 수행한 결과는 stream 객체이고, 원본은 바뀌지 않는다.
  • 스트림으로 들어온 데이터는 한 번만 처리 (반복문 개념이 아님)
  • seamless하게 들어오는 데이터 처리 가능. (무제한으로 데이터가 들어오면, 무한히 처리. short circuit으로 제한 가능)
  • 중개 operation은 근본적으로 Lazy. 터미널 operator가 오기 전까지는 실행되지 않는다.
    stream에 제공하는 메소드 종류는 크게 두 가지.
  1. 중개 - 연산 마치고 다음 연산을 진행할 수 있는 것. 리턴타입이 stream
  2. 터미널(종료) - 연산 마치고 종료. 리턴타입 stream이 아니다.
  • 병럴처리를 쉽게 할 수 있다.
List<String> names = new ArrayList<>();
names.add("E1");
names.add("E2");
names.add("E3");
names.add("E4");

names.stream().map((s) -> {
    // collect 명령어 없이 실행하면, 터미널에 아무것도 찍히지 않는다.
    System.out.println(s);
    return s.toUpperCase();
}).collect(Colletors.toList());

// 병렬처리.
// 내부적으로 spliterator()가 사용된다고 함.
List<String> collect = names.parallelStream().map((s) -> {
    System.out.println(s + " " + Thread.currentThread().getName());
    return s.toUpperCase();
}).collect(Collectors.toList());
// return 시, worker 쓰레드가 여러 개 콘솔에 출력된다.

cf. parallelStream 쓴다고 항상 빨라지는 거 아니다. 느려질 수도 있음.

쓰레드 생성하기 / 병렬처리 후 수집 / Context Switching 비용 등등.

그럼에도 불구하고 써야 할 때는 "데이터 크기가 방대할 때".

그마저도 데이터 종류에 따라 / 처리연산 종류에 따라 다르다. 케바케마다 성능측정 해보고 결정하는 게 최선.

Stream 예시코드

수업 데이터를 저장할 객체 OnlineClass 생성

package com.inspirit941.java8to11;

public class OnlineClass {
    private Integer id;
    private String title;
    private boolean closed;
    // Constructor, getter, setter 전부 존재한다.
}

실습 코드

package com.inspirit941.java8to11;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class testStreamExample {
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot",true));
        springClasses.add(new OnlineClass(2, "spring data jpa",true));
        springClasses.add(new OnlineClass(3, "spring mvc",false));
        springClasses.add(new OnlineClass(4, "spring core",false));
        springClasses.add(new OnlineClass(5, "rest api development",false));

        List<OnlineClass> javaClass = new ArrayList<>();
        javaClass.add(new OnlineClass(6, "the java, test", true));
        javaClass.add(new OnlineClass(7, "the java, code manipulation", true));
        javaClass.add(new OnlineClass(8, "the java, 8 to 11", false));

        List<List<OnlineClass>> events = new ArrayList<>();
        events.add(springClasses);
        events.add(javaClass);

        System.out.println("spring으로 시작하는 수업");
        springClasses.stream()
                // filter = oc는 OnlineClass. 다음 메소드에서도 oc를 입력받음
                .filter(oc -> oc.getTitle().startsWith("spring"))
                // 종료 operation. oc 입력받고 void 리턴.
                .forEach(oc -> System.out.println(oc.getId()));
        // return 1 2 3 4

        System.out.println("not closed 수업");
        springClasses.stream()
                // 임의의 객체에 메소드 참조하기.
                // not closed를 찾아야 하므로, predicate의 not 메소드를 적용할 수 있음.
                .filter(Predicate.not(OnlineClass::isClosed))
                .forEach(onlineClass -> System.out.println(onlineClass.getId()));

        System.out.println("수업 이름만 모아서 Stream 생성");
        // map: 리턴타입을 변경할 수 있는 메소드 (onlineclass -> 다른 타입)
        springClasses.stream()
                // 객체를 받아서 String을 다음 메소드에 전달.
                .map(OnlineClass::getTitle)
                // String 받아서 화면에 출력.
                .forEach(s -> System.out.println(s));


        System.out.println("두 수업 목록의 모든 아이디 출력");
        // events는 리스트 안에 리스트가 들어 있는 구조.
        events.stream()
                // 스트림 input으로 들어온 List 자료구조를 flatten.
                // OnlineClass 객체 형태로 변환된다.
                .flatMap(Collection::stream)
                .forEach(oc -> System.out.println(oc.getId()));

        System.out.println("10부터 1씩 증가하는 무제한 스트림 중,\n 앞 10개 제외하고 최대 10개까지만.");
        // 무제한 스트림을 생성하는 방법 중 하나가 iterate
        // iterate는 중개 operator. 종료 operator가 있어야 작동함.
        Stream.iterate(10, i -> i+1)
                .skip(10)
                .limit(10)
                .forEach(System.out::println);

        System.out.println("자바수업 중 test가 들어있는 수업이 있는지 확인");
        // anyMatch는 bool을 리턴하므로 종료 Operator.
        boolean test = javaClass.stream().anyMatch(oc -> oc.getTitle().contains("test"));
        System.out.println(test);

        System.out.println("스프링 수업 중 제목에 spring이 들어간 것만 모아서 리스트로 반환");
        List<String> list = springClasses.stream()
                .filter(oc -> oc.getTitle().contains("spring")) // Stream OnlineClass
                .map(OnlineClass::getTitle) // Stream String
                .collect(Collectors.toList()); // String 리스트 생성
        list.forEach(System.out::println);
    }
}

Optional

자바8에 추가된 인터페이스.

NullPointerException이 발생하지 않도록 지원한다.

  • 이전까지는 코드에서 어떤 이유로든 리턴값이 null일 경우 Exception을 주는 식으로 처리했다.
  • Exception은 기본적으로 해당 오류가 발생하기까지의 stackTrace 데이터를 리턴하는데, 이게 불필요한 때가 있다.
  • 아니면 null을 받은 클라이언트 코드 측에서 대응작업을 해야 했음.

Null일 경우를 보다 명시적으로 표현하는 방법으로 Optional 사용.

별다른 제약은 없으나, 리턴 타입으로만 사용하는 것을 권장한다.

public Optional<ReturnObject> getReturnObject() {
    // ofNullable: 인자값 (여기서는 returnObject)가 null일 수도 있을 때 사용
    // null일 경우 Optional 안에 빈 값을 넣은 것과 동일하다.
    return Optional.ofNullable(returnObject);

    // of: 인자값이 절대 null이 아닌 경우 사용. null 넣으면 NullPointerException을 반환한다.
    // return Optional.of(returnObject); 
}

주의할 점

  • 리턴타입으로만 사용하길 권장하는 이유
    public void OptionalParameter(Optional<Object> obj) {
      // Optional 안에 실제 객체가 있는지 확인하는 코드가 필요
      obj.ifPresent(p -> System.out.println(p))
    }
    

// 하지만, 메소드를 호출하는 입장에서 input으로 null을 넣을 수 있다.
// 문법적으로 전혀 오류가 없음.
OptionalParameter(null);
// 그러나 함수 내부에서는, null에 ifPresent() 메소드를 호출하려 하기 때문에 NullPointerException 발생.
// 이렇게 되면 굳이 Optional을 써서 얻는 이득이 없음

- Map의 Key 타입으로도 권장하지 않는다. <br>Map의 가장 큰 특징이 "Null값을 key값으로 쓰지 않는다" 이기 때문.
- Optional을 리턴하는 메소드에서 Null을 리턴하지 않도록 유의. `Optional.empty()` 라는 대안이 있다.
- primitive type에 Optional을 바로 붙이면 성능저하 가능성이 크다.<br>`Optional.of(10)` -> 내부적으로 계속 boxing / unboxing 작업이 수행됨.
    - primitive type에 맞는 컨테이너 ex) `OptionalInt.of(10)` 처럼 사용하는 것을 권장한다.
- "비어 있음"을 표현할 수 있는 컨테이너 성격의 객체에 Optional을 감싸지 마라.<br> Collection, Map, Stream Array, Optional 자기자신 등.

### Optional API 사용법

```java

package com.inspirit941.java8to11;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class testStreamExample {
    public static OnlineClass createNewClass(){
        return new OnlineClass(10, "test", false);
    }
    public static void main(String[] args) {
        List<OnlineClass> springClasses = new ArrayList<>();
        springClasses.add(new OnlineClass(1, "spring boot",true));
        springClasses.add(new OnlineClass(2, "spring data jpa",true));
        springClasses.add(new OnlineClass(3, "spring mvc",false));
        springClasses.add(new OnlineClass(4, "spring core",false));
        springClasses.add(new OnlineClass(5, "rest api development",false));

        // 1. findFirst : 없을 수도 있으니 Optional<객체> 형태.
        Optional<OnlineClass> optionalOnlineClass = springClasses.stream()
                .filter(oc -> oc.getTitle().startsWith("spring"))
                .findFirst();

        // get() : 값 꺼내기. 없으면 NoSuchElementException - 런타임 에러.
        optionalOnlineClass.get();
        // isPresent() : null여부 확인. JDK11부터는 isEmpty()도 지원.
        // ifPresent(Consumer<Object>) : 값이 있으면 작업 수행
        optionalOnlineClass.ifPresent(oc -> System.out.println(oc.getTitle()));

        // orElse(인스턴스.)
        // 값이 있으면 할당하고, 없으면 다른 작업을 수행한다. 여기서는 OnlineClass라는 인스턴스를 넣어야 함.
        // 단, 값이 있어서 orElse 부분이 반영되지 않는다 해도, createNewClass() 메소드는 무조건 실행된다.
        // 즉, 이미 만들어져 있는 것을 가져올 경우는 orElse가 적합.
        OnlineClass test = optionalOnlineClass.orElse(createNewClass());
        System.out.println(test.getTitle());

        // orElseGet(Supplier)
        // 무조건 메소드가 실행되는 상황을 막기 위한 코드.
        // else에 걸려서 정말 실행이 필요한 경우에만 코드를 실행한다. Supplier를 input으로 받는다.
        // 즉, 동적으로 새로운 객체를 생성해 실행해야 할 경우 orElseGet이 적합.
        optionalOnlineClass.orElseGet(() -> createNewClass());
        // lambda 사용할 경우
        optionalOnlineClass.orElseGet(testStreamExample::createNewClass);

        // orElseThrow(Supplier)
        // 없을 경우 원하는 형태의 Exception 던지는 코드.
        optionalOnlineClass.orElseThrow(() -> {return new IllegalStateException();});

        // filter()
        // 값이 있다는 전제하에 실행되는 메소드.
        // return Optional<>. 없으면 빈 optional 객체 반환.
        optionalOnlineClass.filter(oc -> oc.getId() > 10);

        // map() : optional이 담고 있는 타입이 달라짐.
        // 만약 map으로 꺼낸 타입이 다시 Optional이면... 몇 번을 꺼내야 하므로 번거로움.
        Optional<Optional<String>> s = optionalOnlineClass.map(OnlineClass::getOptionalString);
        Optional<String> optionalString = s.orElse(Optional.empty());
        // 이 경우 flatMap 메소드를 사용.
        Optional<String> optionalStringByFlatMap = optionalOnlineClass.flatMap(OnlineClass::getOptionalString);
    }
}

Date and time

왜 새로운 API가 생겨났는가

  • java.util.Date 클래스는 mutable. 객체 값이 바뀔 수 있어 thread Safe하지 않았다.
  • 작명 문제. Date 클래스라고 명명해 사용하지만 근본적으로 이건 timestamp 객체임.
  • Type Safe하지 않음. (버그 발생가능성이 높음 - Calendar 클래스의 Month는 0이 1월이다. month의 데이터 타입이 int - 음수 들어오면?)
  • jorda-time 패키지가 자바 표준으로 편입되었음.
  1. 기계 시간 & 인간 시간
package com.inspirit941.java8to11;

import java.time.*;

public class testDateAPI {
    public static void main(String[] args) {
        // 기계어 시간.
        Instant instant = Instant.now();
        // 출력하면 UTC 0 기준 연월일시 timestamp 반환.
        // = instant.atZone(ZoneId.of("UTC"));
        System.out.println(instant);
        // 로컬 기준으로 변경하기
        ZoneId zone = ZoneId.systemDefault();
        ZonedDateTime zoneDateTime = instant.atZone(zone);
        System.out.println(zoneDateTime);

        // 인간용 시간.
        // 컴퓨터가 동작하는 지역의 시간대를 자동으로 설정한 것. 컴퓨터가 동작하는 지역의 시간을 반환함.
        LocalDateTime now = LocalDateTime.now();
        System.out.println("localDateTime " + now);
        // 특정 날짜 생성하기.
        LocalDateTime.of(1994, Month.NOVEMBER,30,10,22);
        // 특정 지역의 시간
        ZonedDateTime nowInKorea = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
        System.out.println(nowInKorea);

        // instant에서도 atZone으로 ZonedDateTime 객체를 생성할 수 있다.
        // ZonedDateTime을 기준으로 Instant <-> LocalDateTime 상호변환이 가능.
    }
}
  1. 기간 표현하기
package com.inspirit941.java8to11;

import java.time.*;
import java.time.temporal.ChronoUnit;

public class testDuration {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();
        LocalDate birthday = LocalDate.of(2020, Month.NOVEMBER, 30);
        // 인간용 시간을 비교하는 Period
        // 현재 날짜로부터 생일까지 얼마나 남았는지
        Period period = Period.between(today, birthday);
        System.out.println(period.getDays());
        // return 14

        // 동일한 결과를 얻는 또다른 방법
        Period until = today.until(birthday);
        System.out.println(until.get(ChronoUnit.DAYS));
        // return 14

        // 기계용 시간을 비교하는 Duration
        Instant now = Instant.now();
        Instant plus = now.plus(10, ChronoUnit.SECONDS);
        System.out.println(Duration.between(now, plus).getSeconds());
        // return 10
    }
}
  1. Formatting / Parsing, 구버전과의 호환
package com.inspirit941.java8to11;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

public class testFormatter {
    public static void main(String[] args) {
        // 레거시와 호환 - Date
        Date date = new Date();
        Instant instant = date.toInstant();

        Date newDate = Date.from(instant);

        // 그레고리안력 호환
        GregorianCalendar gregorianCalendar = new GregorianCalendar();
        ZonedDateTime zonedDateTime = gregorianCalendar.toInstant().atZone(ZoneId.systemDefault());
        LocalDateTime toLocalDatetime = zonedDateTime.toLocalDateTime();

        GregorianCalendar from = GregorianCalendar.from(zonedDateTime);

        // TimeZone과 Zone의 호환
        ZoneId zoneId = TimeZone.getTimeZone("PST").toZoneId();
        TimeZone timeZone = TimeZone.getTimeZone(zoneId);

    // Formatting & Parsing 정리
        LocalDateTime now = LocalDateTime.now();
        // Formatting
        // formatter에 정의된 패턴을 사용할 수도, 필요한 패턴을 생성할 수도 있다.
        // 미리 정의된 거 검색해보고 필요한 거 찾아쓰는 식으로 사용.
        DateTimeFormatter MMddyyyy = DateTimeFormatter.ofPattern("MM/dd/yyyy");
        System.out.println(now.format(MMddyyyy));
        // return 16/10/2020

        // Parsing
        LocalDate parse = LocalDate.parse("07/15/2016", MMddyyyy);
        System.out.println(parse);
        // return 2016-07-15
    }
}
반응형