학습일지/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에 제공하는 메소드 종류는 크게 두 가지.
- 중개 - 연산 마치고 다음 연산을 진행할 수 있는 것. 리턴타입이 stream
- 터미널(종료) - 연산 마치고 종료. 리턴타입 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 패키지가 자바 표준으로 편입되었음.
- 기계 시간 & 인간 시간
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 상호변환이 가능.
}
}
- 기간 표현하기
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
}
}
- 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
}
}
반응형