diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5796ed43 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.DS_Store +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar +/out/ +/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + + +build/ +gradle/ +test/ +racingcar/src/test/ diff --git a/java basics/Collection Framework.md b/java basics/Collection Framework.md new file mode 100644 index 00000000..53f8f0ed --- /dev/null +++ b/java basics/Collection Framework.md @@ -0,0 +1,54 @@ +# Collection Framework +## 컬렉션 프레임워크란? + +배열은 크기가 고정적이라 여러 비효율적인 문제가 생긴다. + +배열의 크기는 생성할 때 결정된다. + +- 배열의 크기를 벗어나면 데이터를 더 이상 저장할 수 없다. +- 데이터를 삭제하면 해당 인덱스의 데이터는 비어있어 메모리가 낭비되는 문제가 발생한다. + +이런 문제점들을 해결하기 위해 객체나 데이터들을 효율적을 관리, 추가, 삭제, 검색, 저장할 수 있는 자료구조들을 만들어 두었다. 이런 라이브러리를 컬렉션 프레임워크라고 한다. + +![컬렉션 프레임워크]([https://blog.kakaocdn.net/dn/bdy438/btqEjPZKIY0/e5Wm8ZJmdRNza4tKBzaK6k/img.png](https://blog.kakaocdn.net/dn/bdy438/btqEjPZKIY0/e5Wm8ZJmdRNza4tKBzaK6k/img.png)) + +## List 컬렉션 + +컬렉션 프레임워크를 상속받고 있는 List 컬렉션은 객체를 일렬로 늘어놓은 구조를 가지고 있다. List 컬렉션은 객체를 인덱스를 관리하기 때문에 객체를 저장하면 자동 인덱스가 부여되고 인덱스로 객체를 검색, 삭제할 수 있는 기능을 제공한다.(인덱스에는 데이터가 저장되어 있는 참조 값을 가지고 있다.) + +![List 컬렉션]([https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxhCVv%2FbtqEg09LXoG%2Fm26SctApZoPjJtRaAEmlSk%2Fimg.png](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxhCVv%2FbtqEg09LXoG%2Fm26SctApZoPjJtRaAEmlSk%2Fimg.png)) + +List 컬렉션은 객체 자체를 저장하는 것이 아니라 위와 같이 객체의 번지를 참조합니다. 동일한 객체를 중복 저장할 수 있는데 이 경우 동일한 번지가 참조됩니다. null도 저장이 가능한데 이 경우에는 해당 인덱스는 객체를 참조하지 않는다. List 컬렉션을 구현하는 대표적인 클래스들은 ArrayList, LinkedList, Vector가 있으며 이 3가지 클래스는 List 인터페이스를 같이 상속하고 있으므로 공통적으로 사용할 수 있는 메소드들이 많다. + +- List 클래스 주요 메소드 + +[제목 없음](https://www.notion.so/f9122f0852d3491b8f8c9735430501f4) + +## Set 컬렉션 + +List 컬렉션은 선형구조를 가지고 있으므로 추가한 순서대로 저장돼어 순서를 유지했지만 Set 컬렉션은 저장 순서가 유지되지 않는다. Set 컬렉션은 순서 자체가 없어 인덱스로 객체를 검색해서 가져오는 get(index) 메소드도 존재하지 않는다. 대신 전체 객체를 대상으로 한 번씩 반복해서 가져오는 반복자(Iterator)를 제공한다. Iterator는 .iterator() 메소드를 호출하면 얻을 수 있다. + +![Set 컬렉션]([https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLMuJG%2FbtqEgQzQaFv%2F18xV7JmoktO3gKPnYitGZ0%2Fimg.png](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLMuJG%2FbtqEgQzQaFv%2F18xV7JmoktO3gKPnYitGZ0%2Fimg.png)) + +또한 Set은 객체를 중복해서 저장할 수 없고 하나의 중복 저장이 안 되기에 null값도 하나만 저장할 수 있다. Set 컬렉션을 구현하는 대표적인 클래스들은 HashSet과 TreeSet이 있다. 이 2가지 클래스는 Set 인터페이스를 같이 상속하고 있으므로 공통적으로 사용할 수 있는 메소드들이 존재한다. + +- Set 클래스 주요 메소드 + +[제목 없음](https://www.notion.so/8643a61c33a74b5a825cffb7fc4ab359) + +## Map 컬렉션 + +Map 컬렉션은 키Key 와 값Value으로 구성된 객체를 저장하는 구조를 가지고 있는 자료구조이다. 키는 중복으로 저장할 수 없고 값은 중복으로 저장할 수 있으며 중복된 key값이 들어온다면 기존의 값은 없어지고 새로운 값으로 대치된다. + +![Map 컬렉션]([https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDaHeK%2FbtqEjQx07Ng%2FPQSBhv0USEnMzQnzuMFw61%2Fimg.png](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDaHeK%2FbtqEjQx07Ng%2FPQSBhv0USEnMzQnzuMFw61%2Fimg.png)) + +Map은 Key와 Value라는 것을 한 쌍으로 갖는 자료형이다. Map은 리스트나 배열처럼 순차적으로(sequential) 해당 요소 값을 구하지 않고 key를 통해 value를 얻는다. Map 컬렉션을 구현하는 대표적인 클래스들은 HashMap, hashtable, LinkedHashMap, TreeMap 등이 있다. + +- Map 클래스 주요 메소드 + +[제목 없음](https://www.notion.so/ca00a7b06d9c445c8fa05ad3f02ed378) + +# Reference + +- [자바 컬렉션 프레임워크 총정리. 티스토리]([https://coding-factory.tistory.com/550](https://coding-factory.tistory.com/550)) +- [자바 ArrayList 사용법, 예제 총정리. 티스토리.[([https://coding-factory.tistory.com/551?category=758267](https://coding-factory.tistory.com/551?category=758267)) \ No newline at end of file diff --git a/java basics/Diff Collection.forEach, Stream.forEach.md b/java basics/Diff Collection.forEach, Stream.forEach.md new file mode 100644 index 00000000..455cfd4b --- /dev/null +++ b/java basics/Diff Collection.forEach, Stream.forEach.md @@ -0,0 +1,174 @@ +# Diff Collection.forEach, Stream.forEach +아래처럼 Collection.forEach메소드로 반복할 때와 Stream.forEach메소드로 반볼할 때는 무슨 차이가 있을까? + +```java +public void print(List nums) { + nums.forEach(System.out::println); + nums.stream() + .forEach(System.out::println); +} +``` + +사실 대부분의 경우 별 차이는 없고, 작은 차이가 있다. + +## Stream 객체 사용 여부 + +```java +public void print(List nums) { + nums.forEach(System.out::println); + nums.stream() + .forEach(System.out::println); +} +``` + +Collection.forEach는 따로 객체를 생성하지 않고 forEach 메소드를 호출한다. forEach메소드는 Iterable인터페이스의 default 메소드인데, Collection 인터페이스에서 Iterable 인터페이스를 상속하고 있기에 바로 호출할 수 있는 것이다. + +```java +public interface Iterable { + default void forEach(Consumer action) { + Objects.requireNonNull(action); + for (T t : this) { + action.accept(t); } + } + ... +} + +public interface Collection extends Iterable { + ... +} + +``` + +반면에 Stream.forEach는 Collection 인터페이스의 default 메소드 stream() 으로 Stream 객체를 생성해야만 forEach를 호출할 수 있다. + +```java +public interface Collection extends Iterable { + default Stream stream() { + return StreamSupport.stream(spliterator(), false); + } +} +``` + +위처럼 단순 반복이 목적이라면 Stream.forEach는 stream()으로 생성된 Stream객체가 버려지는 오버헤드가 있기에, filter, map등의 Stream 기능들과 함께 사용할 때만 Stream.forEach를 사용하고 나머지의 경우엔 Collection.forEach를 쓰는 것이 좋아 보인다. + +- 참고로 Stream의 foreach와 for-loop는 다르다. + +## parallelStream + +stream 메소드로 생성한 Stream.forEach를 했을 땐 Collection.forEach와 별 차이가 없었지만 Collection 인터페이스의 또 다른 Stream 객체 생성 메소드 parallelStream()을 사용해서 Stream.forEach를 한다면 그 차이점이 명확하다. + +```java +public void print(){ + List nums = Arays.asList(1,2,3,4,5); + System.out.println("Collection.forEach 출력 시작"); + nums.forEach(System.out::println); + System.out.println("Stream.forEach 출력 시작"); + nums.parallelStream(). + forEach(System.out::println); +} +``` + +```java +Collection.forEach 출력 시작 +1 +2 +3 +4 +5 +Stream.forEcah 출력 시작 +3 +4 +1 +5 +2 +``` + +parallelStream() 메소드로 생성한 Stream 객체는 여러 스레드에서 스트림을 실행하기 때문에 forEach를 했을 때 실행 순서가 매번 달라지며 예측 불가능하다. + +반면에 Colleciton.forEach는 내부적으로 아래와 같이 실행되기 때문에 일정한 순서를 보장한다. + +```java +default void forEach(Consumer action { + Objects.requireNonNull(action); + for (T t : this) { + action.accept(t); + } +} +``` + +## 동시성 문제 + +Collection.forEach의 경우엔 수정을 감지한 즉시 ConcurrentModificationException을 던지며 프로그램을 멈춘다. + +ConcurrentModificationExcption이란 한 오브젝트에 대해 허가되지 않은 변경이 동시적으로 이루어질 때 발생한다. 대표적으로 Collection이 반복되는 동안 Collection을 수정할 때 발생한다. + +아래의 예제는 List의 element가 짝수이면 remove하는 Consumer를 forEach로 돌린 것이다. + +```java +@Test +void test() { + List nums = new ArayList<>(Arrays.asList(1,2,3,4,5,6)); + Consumer remo eIfEven = num -> { + System.out.println(num); + if (num % 2 == 0) { + nums.remove(num); + } + }; + assertThatThrownBy(() -> nums.forEach(removeIfEven)) + .isInstanceOf(ConcurrentModificationException.class); +} +``` + +위의 예제는 List의 첫 번째 짝수 2가 지워지면 바로 ConcurrentModificationException이 발생한다. + +아래는 Stream.forEach의 경우이다. + +```java +@Test +void test() { + List nums = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6)); + Consumer removeIfEven = num -> { + System.out.println(num); + if (num % 2 == 0) { + nums.remove(num); + } + }; + assertThatThrownBy(() -> nums.stream().forEach(removeIfEven)) + .isInstanceOf(NullPointerException.class); +} + +``` + +Collection.forEach처럼 Collection이 수정되자마자 예외를 던지는 것이 아니라 무조건 리스트를 끝까지 돌고 예외를 던진다. 또 던지는 예외가 ConcurrentModificatonException이 아니라 NullPointerException이라는 차이점이 있다. + +Collectioni.forEach는 일반적으로 해당 컬렉션의 Iterator를 사용하고 Stream.forEach는 해당 컬렉션의 spliterator를 사용한다. + +Collections.java에서 보면 아래의 코드처럼 Collection.forEach에는 synchronized 키워드가 붙어있고 Stream.forEach를 위해 필요한 spliterator메소드는 안 붙어있는 것을 확인할 수 있다. + +```java +@Override +public void forEach(Consumer consumer) { + synchronized (mutex) { + c.forEach(consumer); + } +} +@Override +public Spliterator spliterator() { + return c.spliterator(); // Must be manually synched by user! +} + +``` + +즉, collection.forEach는 락이 걸려있어 멀티쓰레드에서 더 안전하다. 반면에 Stream.forEach는 반복 도중에 다른 쓰레드에 의해 수정될 수 있고, 무조건 요소의 끝까지 반복을 돌게된다. 이 과정에서 일관성 없는 동작이 발생하고 예상치 못한 에러가 발생할 확률이 높다. + +## 결론 + +결국 반복을 위해 존재하는 Collection.forEach와 Stream.forEach의 차이는 작다. + +Stream.forEach는 Stream의 컨셉에 맞게 병렬 프로그래밍에 특화된 반복을 위해 존재한다. + +일반적인 반복의 경우엔 thread-safe한 Collection.forEach를 쓰는 게 좋아보인다. + +# Reference + +- [Velog. Collectio.forEach와 Stream.forEach는 뭐가 다를까?]([https://dundung.tistory.com/247](https://dundung.tistory.com/247)) \ No newline at end of file diff --git a/java basics/Features of Stream.md b/java basics/Features of Stream.md new file mode 100644 index 00000000..f156a87d --- /dev/null +++ b/java basics/Features of Stream.md @@ -0,0 +1,902 @@ +# Stream의 특징 +## Stream이란? + +Stream은 Java8 에서 추가되었다. 기존 Java I/O에서 나오는 InputStream, OutputSteram과는 다른 것으로 함수형 프로그램에서 단계적으로 정의된 계산을 처리하기 위한 interface이다. + +Stream은 Stream형태의 요소에 함수형 연산자를 지원해주는 class로 stream은 Array, collections와 같이 연속된 형태의 객체이다. + +**Stream은 자료구조가 아니다.** 위와 같은 데이터를 입력으로 받아 method로 처리할 뿐이다. + +stream은 입력 받은 원래의 자료구조를 바꾸지는 못한다. + +대신, 파이프라인 형태로 연결된 method의 결과를 제공한다. + +원본 데이터를 바꾸지 못하는 특성 덕분에 side effect를 제거할 수 있다. + +최종 연산자(terminal operation)는 stream의 끝을 의미하며 모든 연산자를 수행한 결과를 반환한다. + +최종 연산자가지 모두 수행한 stream은 재활용할 수 없다. + +- stream 예제 + +```java +int result = list.stream()// 스트림 생성 + .filter(...)// 중간 연산자 + .map(...)// 중간 연산자 + .count();// 최종 연산자 +``` + +stream은 데이터의 흐름으로 배열 또는 컬렉션 인스턴스에 함수를 조합하여 원하는 결과를 필터링하고 가공된 결과를 손쉽게 처리할 수 있다. + +Stream은 데이터 소스를 추상화하고 있으므로 데이터 소스에 상관 없이 같은 방식으로 처리할 수 있다는 장점이 있으며 데이터를 다루는 데 자주 사용되는 method들을 정의해두고 있어 기존 방식보다 간결하고 유연한 구현이 가능하다. + +## Stream 연산 구조 + +Stream은 어떻게가 아니라 무엇을 할 것인지에 목적을 두고 사용해야 하며 연산의 파이프라인은 스트림 생성(Create) → 중간연산(Intermediate operating) → 최종연산(Final operation)의 형태를 가지며 이들은 .을 이용한 메소드 체이닝(Method Chaining)으로 구현된다. + +```java +Collections 등 Object 집합.스트림생성().중간연산().최종연산(); +``` + +중간연산 메소드는 리턴타입이 스트림이므로 계속해서 다른 스트림 메소드를 연결해서 사용할 수 있다. + +최종연산 메소드는 리턴 타입이 스트림이 아닌 것으로 메소드 체이닝을 끝내는 역할을 한다. + +최종연산이 실행 되어야 중간연산도 처리되기 때문에 중간연산들만으로 구성된 메소드 체인은 실행되지 않는다. + +## 기존 방식과의 비교 + +### 기존 방식 + +```java + +//동일한 데이터를 가지는 배열과 리스트를 선언. +//각각 데이터 정렬을 위한 메서드를 통해 데이터를 정렬. +//정렬된 값을 확인하기 위해 출력문을 이용해 출력. +String[] strArr = { "data1", "data2", "data3" } +List strList = Arrays.asList(strArr); + +//데이터 정렬을 위해 각각 Arrays, Collections 의 sort 메서드를 이용 +//정렬한 다음 for 문을 이용해 결과를 출력하는 형식. +0 +Arrays.sort(strArr); +Collections.sort(strList); + +for(String str : strArr) { + System.out.println(str); +} + +for(String str : strList) { + System.out.println(str); +} +``` + +### Stream API 사용 + +```java +//데이터 소스(배열 혹은 리스트)로 부터 스트림을 생성. +//정렬을 위해 sorted() 메서드를 호출. +//출력을 위해 forEach() 메서드를 호출. + +strList.stream() + .sorted() + .forEach(System.out::println); +Arrays.stream(strArr) + .sorted() + .forEach(System.out::println); + +//여기에서 forEach는 void forEach(Consumer action) 로 정의되어 있음. +//Cosumer 함수형 인터페이스를 인자로 가진다. +//메서드 레퍼런스를 사용하지 않고 람다식으로 표현하면 아래와 같다. + +strList.stream() + .sorted() + .forEach(x -> System.out.println(x)); +``` + +## Stream API 제공 method + +### 생성 메소드 + +Collections, Array, String, File등으로부터 stream을 생성할 수 있다. + +### Empty stream + +비어 있는 스트림을 생성하기 위해서는 empty() 메소드를 사용한다. + +```java +Stream streamEmpty = Stream.empty(); +``` + +### Arrays.stream() + +배열로부터 스트림을 생성하는 방법은 여러 가지가 있다. + +```java +Stream arrayStream = Stream.of("a", "b", "c"); +String[] arr = new String[]{"a", "b", "c"}; +Stream arrayFullStream = Arrays.stream(arr); +Stream arrayPartStream = Arrays.stream(arr, 1, 3); +``` + +### Collections.stream() + +자바 컬렉션 인터페이스를 사용하는 Collection, List, Set은 stream()메소드와 parallelStream() 메소드를 사용할 수 있다. + +Map의 경우 Key 혹은 Value 값만 리스트로 추출한 다음 스트림을 만들어 사용할 수 있다. + +```java +Collection collection = Arrays.asList("a", "b", "c"); +Stream collectionStream = collection.stream(); + +List names = new ArrayList<>(); +names.add("Kang"); +names.add("Hong"); +names.stream() + .forEach(System.out::println); +``` + +### String Stream + +문자열을 다루는 클래스인 String, StringBuffer, StringBuilder는 문자열 시퀀스를 반환하는 chars() 메소드를 가지고 있는데 이를 통해 스트림을 생성하게 된다. + +```java +IntStream charsStream = "abc".chars(); +String str = "Hello World"; +str.chars().filter(....) +``` + +### File Stream + +파일의 경우 자바 NIO의 Files 클래스를 이용해 문자열 스트림 생성이 가능하다. + +```java +Path path = Paths.get("C:/Tmp/testfile.txt"); +Stream streamOfStrings = Files.lines(path); +Stream streamWithCharset = Files.lines(path, Charset.forName("UTF-8")); +``` + +### build() + +```java +tream generatedStream = Stream.builder() + .add("Hello") + .add("World") + .build(); +``` + +### generate() + +크기를 지정하지 않으면 무한하기 때문에 특정 사이즈만큼 생성하려면 반드시 limit을 통해 최대 크기를 제한해야 한다. + +```java +Stream generatedStream = Stream.generate(() -> "gen").limit(5); +``` + +### iterate() + +초기 값을 시작으로 계속해서 2씩 증가된 값을 생성한다. + +generate()와 마찬가지로 크기를 지정하지 않으면 무한하기 때문에 limit을 통해 크기를 제한해야 한다. + +```java +Stream iteratedStream = Stream.iterate(30, n -> n + 2).limit(5); +``` + +### parallel stream + +병렬 스트림은 내부적으로 fork & join 프레임워크를 이용해 자동적으로 연산을 병렬로 수행한다. 병렬처리를 구현하기 위해 개발자가 신경써야 하는 많은 부분을 해결할 수 있으며 스트림 생성시 parallel() 메소드를 실행하기만 하면 된다. + +병렬 스트림 처리에서 병렬처리를 중단하려면 sequential()을 호출하면 된다. + +```java +int sum = strStream.parallel() + .mapToInt(s -> s.length()) + .sum(); +``` + +## 기본 타입형 스트림 + +IntStream, LongStream , DoubleStream + +제네릭을 사용하지 않고 기본 값을 생성하는 방법이다. + +제네릭을 사용하지 않기 때문에 불필요한 오토 박싱이 발생하지 않는다. + +## 중간 연산 메소드 + +- 스트림 필터링: filter(), distinct() +- 스트림 변환: map(), flatMap() +- 스트림 제한: limit(), skip() +- 스트림 정렬: sorted() +- 스트림 연산 결과 확인: peek() +- 타입 변환: asDoubleStream(), asLongStream(), boxed() + +여기에서 사용 되는 예제들은 모두 다음과 같은 리스트 데이터를 사용한다고 가정 한다. + +```java +List intList = Arrays.asList(1,2,3); +List strList = Arrays.asList("Hwang", "Hong", "Kang"); +``` + +### filter(), distinct() + +스트림 요소를 필터링 하기 위한 메소드다. + +filter()는 스트림 요소마다 비교문을 만족(true)하는 요소로 구성된 스트림을 반환한다. + +즉, 특정 조건에 맞는 값만 추리기 위한 용도로 사용한다. + +distinct()는 요소들의 중복을 제거하고 스트림을 반환한다. + +```java +intList.stream() + .filter(x -> x <= 2) + .forEach(System.out::println); +//1,2 + +Arrays.asList(1,2,3,2,5).stream() + .distinct() + .forEach(System.out::println); +//1,2,3,5, +``` + +### map() + +스트림의 각 요소마다 수행할 연산을 구현할 때 사용한다. + +```java +intList.stream() + .map(x -> x * x) + .forEach(System.out::println); +//1,4,9 +``` + +### flatMap() + +기존의 요소를 새로운 요소로 대체한 스트림을 생성한다. + +```java +Arrays.asList(intList, Arrays.asList(2,5)).stream() + .flatMap(i -> i.stream()) + .forEach(System.out::println); +// 1,2,3,2,5 + +strList.stream() + .flatMap(message -> Arrays.stream(message.split("an"))) + .forEach(System.out::println); +``` + +### limit() + +스트림의 시작 요소로부터 인자로 전달된 인덱스까지의 요소를 추출해 새로운 스트림을 생성한다. + +```java +intList.stream(). + .limit(2) + .forEach(System.out::println); +// 1,2 +``` + +### skip() + +스트림의 시작 요소로부터 인자로 전달된 인덱스까지를 제외하고 새로운 스트림을 생성한다. + +```java +intList.stream() + .skip(2) + .forEach(System.out::println); +//3 +``` + +### sorted + +스트림 요소를 정렬하는 메소드로 기본적으로 오름차순으로 정렬한다. + +sorted()를 활용하는 방법은 몇 가지가 있다. + +### peek() + +결과 스트림의 요소를 사용해 추가로 동작을 수행한다. + +원본 스트림을 이용하는 것이 아니므로 스트림 연산 과정에서 중간중간 결과를 확인할 때 사용할 수 있다. + +최종 연산인 forEach()처럼 반복해서 요소를 처리하는 메소드이며 중간연산이므로 최종연산 메소드가 실행되지 않으면 지연되기 때문에 반드시 최종연산 메소드가 호출되어야 동작한다. + +앞의 filter() 예제를 보면 최종 연산으로 forEach() 를 이용해 출력하고 있다. + +**만일 최종 연산이 forEach() 가 sum() 이나 다른 최종연산이라면 값을 출력해볼 방법이 없다.** + +그렇다고 변환된 컬렉션을 가지고 출력을 위해 다시 스트림 연산을 한다면 불편할 수 있다. + +**이 경우 peak() 이 유용하게 사용될 수 있다.** + +```java +int sum = intList.stream().filter(x -> x<=2) + .peek(System.out::println) + .mapToInt(Integer::intValue).sum(); +System.out.println("sum: "+sum); +``` + +## 최종 연산 메소드 + +최종연산은 결과값만 리턴되므로 별도의 출력문을 연결해 사용하기 어렵다. + +- 요소의 출력: forEach() +- 요소의 소모: reduce() +- 요소의 검색: anyFirst(), findAny() +- 요소의 검사: antMatch(), allMatch(), nonMatch() +- 요소의 통계: count(), min(), max() +- 요소의 연산: sum(), average() +- 요소의 수집: collect() + +### forEach() + +스트림의 요소들을 순환하면서 반복해서 처리해야 하는 경우 사용한다. + +```java +intList.stream() + .forEach(System.out::println); +// 1,2,3 + +intList.stream() + .forEach(x -> System.out.printf("%d : %d\n",x,x*x)); +// 1,4,9 +``` + +### reduce() + +map 과 비슷하게 동작하지만 개별 연산이 아니라 누적연산이 이루어진다는 차이가 있다. + +두 개의 인자 즉, n, n+1을 가지며 연산 결과는 n이 되고 다시 다음 요소와 연산을 하게 된ㄴ다. 즉 1,2 번째 요소를 연산하고 그 결과와 3번째 요소를 연산하는 식이다. + +```java +int sum = intList.stream(). + reduce((a,b) -> a + b.get(); + +System.out.println("sum: " + sum); +//6 +``` + +### findFirst(), findAny() + +두 메소드는 스트림에서 지정한 첫 번째 요소를 찾는 메소드이다. + +보통 filter() 와 함께 사용되고 finAny()는 parallelStream() 에서 병렬 처리시 가장 먼저 발견된 요소를 찾는 메소드로 결과는 스트림 원소의 정렬 순서와 상관 없다. + +```java +strList.stream().filter(s -> s.startsWith("H")).findFirst().ifPresent(System.out::println); //Hwang +strList.parallelStream().filter(s -> s.startsWith("H")).findAny().ifPresent(System.out::println); //Hwang or Hong +``` + +findAny() 를 parallelStream() 과 함께 사용하는 경우 일반적으로 findFrist()와 결과가 같다.(반드시 보장되는 것은 아니다.) + +arallelStream() 과 사용한 경우 실제 스트림 순서와는 다르게 선택될 수 있다. + +findFirst()와 findAny()의 리턴값은 Optional이므로 ifPresent()를 이용해 출력한다. + +### anyMatch(), allMatch(), nonMatch() + +스트림의 요소 중 특정 조건을 만족하는 요소를 검사하는 메소드다. + +원소 중 일부, 전체 혹은 일치하는 것이 없는 경우를 검사하고 boolean값을 리턴한다. + +nonMatch()의 경우 일치하는 것이 하나도 없을 때 true를 리턴한다. + +```java +boolean result1 = strList.stream().anyMatch(s -> s.startsWith("H")); //true +boolean result2 = strList.stream().allMatch(s -> s.startsWith("H")); //false +boolean result3 = strList.stream().noneMatch(s -> s.startsWith("T")); //true +System.out.printf("%b, %b, %b",result1,result2, result3); +``` + +### count(), min(), max() + +스트림의 원소들로부터 전체개수, 최솟값, 최댓값을 구하기 위한 메소드다. + +min(), max()의 경우 Comparator를 인자로 요구하고 있으므로 기본 Comparator들을 사용하거나 직접 람다 표현식으로 구현해야 한다. + +```java +intList.stream() + .count(); +// 3 +intList.stream() + .filter(n -> n != 2) + .count() +// 2 +intList.stream() + .min(Integer::compare) + .ifPresent(System.out::println); +// 3 + +strList.stream() + .count(); +// 3 +strList.stream() + .min(String::compareToIgnoreCase) + .ifPresent(System.out::println); +strList.stream() + .max(String::compateTo) + .ifPresent(System.out::println); +//Kang +``` + +### sum(), average() + +스트림 원소들의 합계를 구하거나 평균을 구하는 메소드이다. + +reduce(), map()을 이용해도 구현이 가능하다. + +이 경우 리턴값이 Optional이기 때문에 ifPresent()를 이용해 갑슬 출력할 수 있다. + +```java +intList.stream() + .mapToInt(Integer::intValue) + .sum(); +// 6 +intList.stream() + .reduce((a,b) -> a + b) + .ifPresent(System.out::println); +// 6 + +intList.stream() + .mapToInt(Integer::intValue) + .average(); +// 2 +intList.stream() + .reduce((a, b) -> a + b) + .map(n -> n / intList.size()) + .ifPresent(System.out::println); +// 2 +``` + +### collect() + +스트림의 결과를 모으기 위한 메소드로 Collectors 객체에 구현된 방법에 따라 처리하는 메소드다. + +최종 처리 후 데이터를 변환하는 경우가 많기 때문에 자주 사용된다. + +Collectors 의 메소드는 다음과 같다. + +- 스트림을 열이나 컬렉션으로 변환: toArray(), toCollection(), toList(), toSet(), toMap() +- 요소의 통계와 연산 메소드와 같은 동작을 수행: counting(), maxBy(), minBy(),, summingInt(), averagingInt() +- 요소의 소모와 같은 동작을 수행: reducing(), joining() +- 요소의 그룹화와 분할: groupingBy(), partitioningBy() + +```java +strList.stream() + .map(String::toUpperCase) + .collect(Collectors.joining("/")); +// Hwang/Hong/Kang +strList.stream() + .collect(Collectors.toMap(k -> k, v -> v.length())); +// {Hong=4, Hwang=5, Kang=4} + +intList.stream() + .collect(Collectors.counting()); +intList.stream() + .collect(Collectors.maxBy(Integer::compare)); +intList.stream() + .collect(Collectors.reducing((a,b) -> a + b)); +// 6 +intList.stream() + .collect(Collectors.summarizinInt(x -> x)); +//IntSummaryStatistics{count=3, sum=6, min=1, average=2.000000, max=3} + +Map> group = strList.stream() + .collect(Collectors.groupingBy(s -> s.startsWtith("H"))); +group.get(true).forEach(System.out::println); +// Hwang, Hong + +Map> partition = strList.stream() + .collect(Collectors.partitioningBy(s -> s.startsWith("H"))); + +partition.get(true).stream() + .forEach(System.out::println); +// Hwang, Hong +``` + +- toMap()을 사용해서 문자열 스트림의 값을 키로 하고 문자열의 길이를 값으로 하는 맵으로 변환 +- counting, maxBy, reducing은 각각 count(), max9), reuce() 메소드와 동일한 결과 +- summarizinInt는 IntSummaryStatistics를 리턴하여 count, sum, min, average, max 값을 참조할 수 있음 +- groupingBy는 특정 조건에 따라 데이터를 구분해서 저장 +- partitioningBy는 특정 조건으로 데이터를 두 그룹으로 나누어 저장 + +## 스트림 Streams + +자바 8에서 추가한 스트림은 람다를 활용한 수 있는 기술 중 하나이다. 자바 8 이전에는 배열 또는 컬렉션 인스턴스를 다루는 방법은 for 또는 foreach 문을 돌면서 요소 하나씩 꺼내서 다루는 방법이었다. 간단한 경우라면 상관 없지만 로직이 복잡해질수록 코드의 양이 많아져 여러 로직이 섞이게 되고, 메소드를 나눌 경우 루프를 여러 번 도는 경우가 발생한다. + +스트림은 '데이터의 흐름'이다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있다. + +또 하나의 장점은 간단하게 병렬처리(multi-threding)가 가능하다는 점이다. 하나의 작업을 둘 이상의 작업으로 잘게 나눠서 동시에 진행하는 것을 병렬처리(parallen processing)라고 한다. 즉 쓰레드를 이용해 많은 요소들을 빠르게 처리할 수 있다. + +스트림에 대한 내용은 크게 세 가지로 나눌 수 있다. + +- 생성하기: 스트림 인스턴스 생성. +- 가공하기: 필터링(filtering) 및 맵핑(mapping) 등 원하는 결과를 만들어가는 중간 작업(intermediate operations). +- 결과 만들기: 최종적으로 결과를 만들어내는 작업(terminal operations). +- 전체 → 맵핑 → 필터링1 → 필터링2 → 결과 만들기 → 결과물 + +## 생성하기 + +보통 배열과 컬렉션을 이용해 스트림을 만들지만 이 외에도 다양한 방법으로 스트림을 만들 수 있다. + +### 배열 스트림 + +스트림을 이용하기 위해서는 먼저 생성해야 한다. 스트림은 배열 또는 컬렉션 인스턴스를 이용해서 생성할 수 있다. 배열은 다음과 같이 [Arrays.stream](http://arrays.stream) 메소드를 사용한다. + +```java +String[] arr = new String[]{"a", "b", "c"}; +Stream stream = Arrays.stream(arr); +Stream stream0fArrayPart = Arrays.stream(arr, 1, 3);// 1~2 요소 [b, c] +``` + +### 컬렉션 스트림 + +컬렉션 타입(Collection, List, Set)의 경우 인터페이스에 추가된 디폴트 메소드 stream을 이용해서 스트림을 만들 수 있다. + +```java +public interface Collection extends Iterable { + default Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + // ... +} +``` + +그러면 다음과 같이 생성할 수 있다. + +```java +List list = Arrays.asList("a", "b", "c"); +Stream stream = list.stream(); +Stream parallelStream = list.parallelStream(); // 병렬 처리 스트림 +``` + +### 비어 있는 스트림 + +비어 있는 스트림(empty streams)도 생성할 수 있다. 언제 빈 스트림이 필요할까? 빈 스트림은 요소가 없을 때 null 대신 사용할 수 있다. + +```java +public Stream streamOf(List list) { + return list == null || list.isEmpty() + ? Stream.empty() + : list.stream(); +} +``` + +### Stream.builder() + +빌더(Builder)를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있다. 마지막에 build 메소드로 스트림을 리턴한다. + +```java +Stream builderStream = + Stream.builder() + .add("Eric").add("Elena").add("Java") + .build(); // [Eric, Elena, Java] +``` + +### Stream.generate() + +generate 메소드를 이용하면 Supplier 에 해당하는 람다로 값을 넣을 수 있다. Supplier는 인자는 없고 리턴 값만 있는 함수형 인터페이스이다. 람다에서 리턴하는 값이 들어간다. + +```java +public static Stream generate(Supplier s) {...} +``` + +이때 생성되는 스트림은 크기가 정해져있지 않고 무한하기 때문에 특정 사이즈로 최대 크기를 제한해야 한다. + +```java +Stream generatedStream = Stream.generate(() -> "gen").limit(5); // [el, el, el, el, el] +``` + +5개의 "gen"이 들어간 스트림이 생성된다. + +### Stream.iterate() + +iterate 메소드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만든다. 아래에서는 30이 초기값이고 값이 2씩 증가하는 값들이 들어가게 된다. 즉 요소가 다음 요소의 인풋으로 들어간다. 이 방법도 스트림의 사이즈가 무한하기 때문에 특정 사이즈로 제한해야 한다. + +```java +Stream iteratedStream = + Stream.iterate(30, n -> n + 2).limit(5); // [30, 32, 34, 36, 38] +``` + +### 기본 타입형 스트림 + +제네릭을 사용하면 리스트나 배열을 이용해서 기본 타입(int, long, double) 스트림을 생성할 수 있다. 하지만 제네릭을 사용하지 않고 직접적으로 해당 타입의 스트림을 다룰 수도 있다. range와 rangeClosed 는 범위의 차이가 잇다. 두 번째 인자인 종료지점이 포함되느냐 안 되느냐의 차이를 가진다. + +```java +IntStream intStream = IntStream.range(1, 5); // [1, 2, 3, 4] +LongStream longStream = LongStream.rangeClosed(1, 5); // [1, 2, 3, 4, 5] +``` + +제네릭을 사용하지 않기 때문에 불필요한 오토토박싱이 일어나지 않는다. 필요한 경우 boxed 메소드를 이용해서 박싱할 수 있다. + +```java +Stream boxedIntStream = IntStream.range(1, 5).boxed(); +``` + +자바 8의 Random 클래스는 난수를 가지고 세 가지 타입의 스트림(IntStream, LongStream, doubleStream)을 만들어낼 수 있다. 쉽게 난수 스트림을 생성해서 여러 가지 후속 작업을 취할 수 있어 유용하다. + +```java +DoubleStream doubles = new Random().doubles(3); // 난수 3개 생성 +``` + +### 스트림 연결하기 + +Stream.concat 메소드를 이용해 두 개의 스트림을 연결해서 새로운 스트림을 만들어낼 수 있다. + +```java +Stream stream1 = Stream.of("Java", "Scala", "Groovy"); +Stream stream2 = Stream.of("Python", "Go", "Swift"); +Stream concat = Stream.concat(stream1, stream2); +// [Java, Scala, Groovy, Python, Go, Swift] +``` + +## 가공하기 + +전체 요소 중에서 다음과 같은 API를 이용해 원하는 것만 뽑아낼 수 있다. 이러한 가공 단계를 중간 작업(intermediate operations)이라고 하는데, 이러한 작업은 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining)작성할 수 있다. + +```java +List names = Arrays.asList("Eric", "Elena", "Java"); +``` + +아래 예저들은 위의 리스트를 대상으로 한다. + +### Filtering + +필터는 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업이다. 인자로 받는 Perdicate 는 boolean을 리턴하는 함수형 인터페이스로 평가식이 들어가게 된다. + +```java +Stream filter(Predicate predicate); +``` + +```java +Stream stream = + names.stream() + .filter(name -> name.contains("a")); +// [Elena, Java] +``` + +스트림의 각 요소에 대해서 평가식을 실행하게 되고 'a'가 들어간 이름만 들어간 스트림이 리턴된다. + +### Mapping + + 맵은 스트림 내 요소들을 하나씩 특정 값으로 변환해준다. 이때 값을 변환하기 위한 람다를 인자로 받는다. + +```java + Stream map(Function mapper); +``` + +스트림에 들어가 있는 값이 input이 되어서 특정 로직을 거친 후 output이 되어 리턴되는 새로운 스트림에 담기게 된다. 이러한 작업을 맵핑이라고 한다. + +아래는 스트림 내 String의 toUpperCase 메소드를 실행해서 대문자로 변환한 값들이 단긴 스트림을 리턴하는 예제이다. + +```java +Stream stream = + names.stream() + .map(String::toUpperCase); +// [ERIC, ELENA, JAVA] +``` + +다음처럼 요소 내 들어있는 Product 개체의 수량을 꺼내올 수도 있다. 각 '상품'을 '상품의 수량'으로 맵핑하는 것이다. + +```java +Stream stream = + productList.stream() + .map(Product::getAmount); +// [23, 14, 13, 23, 13] +``` + +### Sorting + +정렬의 방법은 다른 정렬과 마찬가지로 Comparator를 이용한다. + +```java +Stream sorted(); +Stream sorted(Comparator comparator); +``` + +인자 없이 그냥 호출할 경우 오름차순으로 정렬한다. + +```java +IntStream.of(14, 11, 20, 39, 23) + .sorted() + .boxed() + .collect(Collectiors.toList()); +// [11, 14, 20, 23, 39] +``` + +인자를 넘기는 경우와 비교해보자. 스트링 리스트에서 알파벳 순으로 정렬한 코드와 Comparator를 넘겨서 역순으로 정렬한 코드이다. + +```java +List lang = + Arrays.asList("Java", "Scala", "Groovy", "Python", "Go", "Swift"); + +lang.stream() + .sorted() + .collect(Collectors.toList()); +// [Go, Groovy, Java, Python, Scala, Swift] + +lang.stream() + .sorted(Comparator.reverseOrdeder()) + .collect(Collectors.toList()); +// [Swift, Scala, Python, Java, Groovy, Go] +``` + +Comparator의 comparte 메소드는 두 인자를 비교해서 값을 리턴한다. + +```java +int compare(T o1, T o2) +``` + +기본적으로 Comparator 사용법과 동일하다. 이를 이용해서 문자열 길이를 기준으로 정렬하자. + +```java +lang.stream() + .sorted(Comparator.comparingInt(String::length)) + .collect(Collectors.toList()); +// [Go, Java, Scala, Swift, Groovy, Python] + +lang.stream() + .sorted((s1, s2) -> s2.length() - s1.length()) + .collect(Collectors.toList()); +// [Groovy, Python, Scala, Swift, Java, Go] +``` + +### Iterating + +스트림 내 요소들 각각을 대상으로 특정 연산을 수행하는 메소드로는 peek이 있다. 'peek'은 그냥 확인해본다는 단어 뜻처럼 특정 결과를 반환하는 않는 함수형 인터페이스 Consumer 를 인자로 받는다. + +```java +Stream peek(Consumer acton); +``` + +따라서 스트림 내 요소들 각각에 특정 작업을 수행할 뿐 결과에 영향을 미치지 않는다. 다음처럼 작업을 처리하는 중간에 결과를 확인해볼 때 사용할 수 있다. + +```java +int sum = IntStream.of(1, 3, 5 , 7, 9) + .peek(System.out::println) + .sum(); +``` + +## 결과 만들기 + +가공한 스트림을 가지 내가 사용할 결과값으로 만들어내는 단계이다. 따라서 스트림을 끝내는 최종 작업(terminal operations)이다. + +### Calcuating + +스트림 API는 다양한 종료 작업을 제공한다. 최소, 최대, 합, 평균 등 기본형 타입으로 결과를 만들어낼 수 있다. + +```java +long count = IntStream.of(1, 3, 5, 7, 9).count(); +long sum = LongStream.of(1, 3, 5, 7, 9).sum(); +``` + +만약 스트림이 비어 있는 경우 count와 sum은 0을 출력하면 된다. 하지만 평균, 최소, 최대의 경우에는 표현할 수 없기 때문에 Optional을 이용해 리턴한다. + +```java +OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min(); +OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max(); +``` + +스트림에서 바로 ifPresent 메소드를 이용해서 Optional을 처리할 수 있다. + +```java +DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5) + .average() + .ifPresent(System.out::println); +``` + +이 외에도 사용자가 원하는대로 결과를 만들어내기 위해 reduce와 collect 메소드를 제공한다. + +# Java 스트림 Stream + +배열의 원소를 가공하는 데 있어 아래와 같은 것을 사용할 수 있다. + +- map: 요소들을 특정 조건에 해당하는 값으로 변환해 준다.(요소들을 대소문자 변형 등의 작업을 하고 싶을 때) +- filter: 요소들을 조건에 따라 걸러내는 작업을 해준다. (길이의 제한, 특정문자포함 등의 작업을 하고 싶을 때) +- sorted: 요소들을 정렬해주는 작업을 해준다. + +요소들의 가공이 끝났다면 리턴해줄 결과를 collect를 통해 만들어준다. + +## Test Set + +```java +ArrayList list = new ArrayList<>(Arrays.asList("Apple","Banana","Melon","Grape","Strawberry")); + +System.out.println(list); + +//[Apple, Banana, Melon, Grape, Strawberry] +``` + +### map + +```java +list.stream().map(s->s.toUpperCase()); +list.stream().map(String::toUpperCase); +``` + +리스트의 요소들을 대문자로 변경해준다. + +요소들을 대문자로 가공했다면 collect를 이용해 결과를 리턴받을 수 있고, forEach를 이용해 바로 출력해볼 수 있다. + +```java +System.out.println(list.stream() + .map(s->s.toUpperCase()) + .collect(Collectors.joining(" "))); +//APPLE BANANA MELON GRAPE STRAWBERRY + +System.out.println(list.stream() + .map(s->s.toUpperCase()) + .collect(Collectors.toList())); +//[APPLE, BANANA, MELON, GRAPE, STRAWBERRY] + +System.out.println(list.stream() + .map(String::toUpperCase) + .collect(Collectors.toList())); +//[APPLE, BANANA, MELON, GRAPE, STRAWBERRY] + +list.stream() + .map(String::toUpperCase) + .forEach(s -> System.out.println(s)); +//APPLE +//BANANA +//MELON +//GRAPE +//STRAWBERRY +``` + +Collectors.joining을 이용해 리스트의 조인을 기준으로 배치할 수 있다. String으로 리턴한다. + +Collectors.toList를 이용해 리스트로 리턴 받을 수 있다. + +forEach를 이용해 요소마다 각각 작업을 할 수 있다. + +### filter + +```java +list.stream() + .filter(t->t.length()>5) +``` + +filter는 요소를 특정 기준으로 걸러낼 수 있다. + +요소의 크기가 5이상인 값만 뽑아낸다. + +```java +System.out.println(list.stream() + .filter(t->t.length()>5) + .collect(Collectors.joining(" "))); +// Banana Strawberry + +System.out.println(list.stream() + .filter(t->t.length()>5) + .collect(Collectors.toList())); +// [Banana, Strawberry] +``` + +마찬가지로 filter로 가공한 결과를 얻을 수 있다. + +### sorted + +```java +list.stream() + .sorted() +``` + +리스트의 요소를 정렬한다. + +```java +System.out.println(list.stream() + .sorted() + .collect(Collectors.toList())); +//[Apple, Banana, Grape, Melon, Strawberry] +``` + +# Reference + +- [Java 스트림 Stream (1) 총정리]([https://futurecreator.github.io/2018/08/26/java-8-streams/](https://futurecreator.github.io/2018/08/26/java-8-streams/)) +- [Java 스트림 Stream (2) 고급]([https://futurecreator.github.io/2018/08/26/java-8-streams-advanced/](https://futurecreator.github.io/2018/08/26/java-8-streams-advanced/)) +- [자바 스트림Stream(map, filter, sorted / collect, foreach)]([https://dpdpwl.tistory.com/81](https://dpdpwl.tistory.com/81)) +- [Java8의 stream distinct 사용 방법 및 예제. chacha.]([https://codechacha.com/ko/java8-stream-distinct/](https://codechacha.com/ko/java8-stream-distinct/)) +- [Stream?. Velog.]([https://velog.io/@gillog/Java-Stream-Class](https://velog.io/@gillog/Java-Stream-Class)) \ No newline at end of file diff --git a/java basics/Stream.forEach, for-loop.md b/java basics/Stream.forEach, for-loop.md new file mode 100644 index 00000000..57ae1b10 --- /dev/null +++ b/java basics/Stream.forEach, for-loop.md @@ -0,0 +1,126 @@ +# Stream.forEach, for-loop +자바에서 Stream은 배열, 컬렉션 등의 요소를 하나씩 참조해 함수형 인터페이스(람다식)를 통해 반복적인 작업의 처리를 가능하게 해준다. Stream이 반복적인 일의 처리가 가능하므로, 반복문(for-loop등)을 대신해 Stream을 사용하는 경우가 많다. + +하지만 무조건 반복문 대신 Stream을 써야하는 것을 아니다. + +## Stream 사용에 대해 + +Stream을 쓰면 가독성도 올라가고 중첩된 for, if stmt 여러 개를 보는 것보다 훨씬 읽기 쉽고 이해하기도 편해진다. + +간단한 리스트 순회 예시를 봐도 알 수 있다. + +```java +// for-loop +for (int i = 0; i < list.size(); i++{ + System.out.println(list.get(i)); +} + +// enhanced for-each +for (String item : list) { + System.out.println(item); +} + +//stream.forEach() +list.stream() + .forEach(System.out::println); +``` + +하지만 남용은 금물이다. + +특히 모든 요소를 순회하는 Stream.forEach() 사용에 대해선 생각해볼 필요가 있다. + +## 언제 주의해야 할까 + +- Stream의 forEach는 요소를 돌면서 실행되는 stream연산의 최종 작업이다. 보통 System.out.println메소드를 넘겨서 결과를 출력할 때 사용한다. + +Stream.forEach()를 사용할 때, 로직이 들어가 있는 경우 자신이 Stream을 잘 활용하고 있는 건지 생각해봐야 한다. + +**종료 조건이 있는 로직을 구현할 때 주의해야 한다.** + +Stream 자체를 강제적을 종료시키는 방법은 없다. 무언가 강제적인 종료 조건에 대한 로직이 있는 for-loop를 stream.forEach()로 구현한다면, 기존 for-loo에 비해 비효율이 발생한다. + +```java +// for-loop +for (int i = 0; i < 100; i++) { + if (i > 50) { + break; + // 50호 돌고 반복을 종료한다. + } +} + +IntStream.range(1, 100).forEach(i -> { + if (i > 50) { + return; + // 각 수행에 대해 다음 수행을 막을 뿐이다. + // 100번 모두 조건을 확인한 후에 종료한다. + } + System.out.println(i); +}); +``` + +위의 예시처럼 반복문이라고 무작정 stream.forEach()를 사용하게 되면 동작은 정상적으로 할지 몰라도 for문에 비해 비효율이 발생할 수 있다. + +```java +IntStream.range(1, 100) + .filter(i -> i <= 50) + .forEach(System.out::println); + +``` + +물론, Stream은 지연 연산을 하기 대문에 100번 모두 검사를 하긴 하지만 Stream.forEach()의 올바른 사용은 위처럼 forEach()를 최종 연산으로만 사용하는 것이다. 굳이 Stream.forEach()내에 로직이 들어가지 않더라도, 중간 연산인 filter, map, sort 등을 통해 충분히 로직을 수행할 수 있다. + +- forEach연산은 최종 연ㅅ나 중 기능이 가장 적고 가장 '덜' 스트림답기 때문에, forEach연산은 스트림 계산 결과를 보고할 때(주로 input기능)만 사용하고 계산하는 데는 쓰지 말자. + +```java +public void validateInput() { + List names = splitInputByComma(); + if (CollectionUtils.isEmpty(names)) { + throw new IllegalArgumentException(LENGTH_ERROR_MESSAGE); + } + names.stream() + .forEach(Input::validateNameLength); +} +``` + +```java +pieces.keySet() + .forEach( + positionKey -> model.addAttribute( + positionKey,pieces.get(positionKey))); +``` + +위의 두 예제를 살펴보면 짧고 간단한 로직이라서 가독성 측면에서는 큰 문제를 생기지 않는다. + +하지만 forEach 내부에 로직이 하나라도 더 추가된다면 **동시성 보장이 어려워지고 가독성이 떨어질** 위험이 있다. + +또한 Stream의 의도를 벗어나게 된다. 본래 forEach는 스트림의 종료를 우히ㅏㄴ 연산이다. 로직을 수행하는 역할은 Stream을 반환하는 중간 연산이 해야하는 일이다. + +```java +public void validateInput() { + List names = splitInputByComma(); + if (CollectionUtils.isEmpty(names)) { + throw new IllegalArgumentException(LENGTH_ERROR_MESSAGE); + } + for (String name : names) { + validateNameLength(name); + } +} +``` + +```java +for (String positionKey: pieces.keySet()) { + model.addAttribute(positionKey, pieces.get(positionKey)); +} +``` + +Stream.forEach() 대신 향상된 for문을 사용해도 충분히 가독성 좋은 코드가 될 수 있다. + +즉, 조건 혹은 로직이 추가된다면 forEach 내부를 손봐야 하는 것이 아니라, Stream의 다양한 연산 도구(filter, map 등)를 활용하거나 반복문 혹은 forEach가 아닌 다른 최종연산을 사용하는 것이 올바른 방향이다. + +- Steam의 본래 목적과 장점을 해치는 잘못된 사용은 지양하자. + +편리하기 위해 사용한 stream이 가독성을 해치고 성능도 저하시킨다면 사용할 이유가 없기 때문이다. + +# Reference + +- [우테코. githubio. Stream의 foreach와 for-loop는 다르다.]([https://woowacourse.github.io/javable/post/2020-05-14-foreach-vs-forloop/](https://woowacourse.github.io/javable/post/2020-05-14-foreach-vs-forloop/)) \ No newline at end of file diff --git a/racingcar/build.gradle b/racingcar/build.gradle new file mode 100644 index 00000000..95687b5b --- /dev/null +++ b/racingcar/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' +} + +group 'org.example' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/racingcar/docs/todo.md b/racingcar/docs/todo.md new file mode 100644 index 00000000..085f23af --- /dev/null +++ b/racingcar/docs/todo.md @@ -0,0 +1,13 @@ +## todo +- Validation + - [x] 이름 중복 검사 + - [x] 이름 길이 검사 + +- Validator.java + - [x] indent, for 요구사항 충족 + - [x] indent, for 요구사항 충족 + +- GameController.java + - [ ] ```for (int i = 0; i < cycle; i++)``` for -> stream 변경 + - [ ] ```for (Car car : cars)``` for -> stream 변경 + diff --git a/racingcar/gradlew b/racingcar/gradlew new file mode 100644 index 00000000..4f906e0c --- /dev/null +++ b/racingcar/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/racingcar/gradlew.bat b/racingcar/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/racingcar/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/racingcar/settings.gradle b/racingcar/settings.gradle new file mode 100644 index 00000000..563821bb --- /dev/null +++ b/racingcar/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'racingcar' + diff --git a/racingcar/src/main/java/racingcar/Application.java b/racingcar/src/main/java/racingcar/Application.java new file mode 100644 index 00000000..d2237884 --- /dev/null +++ b/racingcar/src/main/java/racingcar/Application.java @@ -0,0 +1,36 @@ +package racingcar; + +import view.InputView; +import utils.Validator; +import utils.ErrorCatcher; + +public class Application { + + public static void main(String[] args) { + InputView inputView = new InputView(); + + String[] carNames = inputView.inputCarNames(); + int cycle = inputView.inputCycle(); + int nameLongCheck = Validator.isGoodLength(carNames); + boolean nameOnlyCheck = !Validator.isNameOnly(carNames); + + if (checkError(nameLongCheck, nameOnlyCheck)) { + run(carNames, cycle); + } + } + + public static boolean checkError(int nameLongCheck, boolean nameOnlyCheck) { + ErrorCatcher.catchNameError(nameLongCheck, nameOnlyCheck); + + if (nameLongCheck == 0 && nameOnlyCheck == true) { + return true; + } + + return false; + } + + public static void run(String[] carNames, int cycle) { + GameController gameController = new GameController(); + gameController.startGame(carNames, cycle); + } +} diff --git a/racingcar/src/main/java/racingcar/Car.java b/racingcar/src/main/java/racingcar/Car.java new file mode 100644 index 00000000..aca73393 --- /dev/null +++ b/racingcar/src/main/java/racingcar/Car.java @@ -0,0 +1,29 @@ +package racingcar; + +import utils.RandomUtils; + +public class Car { + private static final int START_INCLUSIVE = 0; + private static final int END_INCLUSIVE = 9; + private static final int MOVE_THRESHOLD = 4; + private final String name; + private int position = 0; + + public Car(String name) { + this.name = name; + } + + public void move() { + if (RandomUtils.nextInt(START_INCLUSIVE, END_INCLUSIVE) >= MOVE_THRESHOLD) { + position++; + } + } + + public String getName() { + return this.name; + } + + public int getPosition() { + return this.position; + } +} diff --git a/racingcar/src/main/java/racingcar/GameController.java b/racingcar/src/main/java/racingcar/GameController.java new file mode 100644 index 00000000..3655c9dc --- /dev/null +++ b/racingcar/src/main/java/racingcar/GameController.java @@ -0,0 +1,39 @@ +package racingcar; + +import view.OutputView; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class GameController { + private List cars; + + public void startGame(String[] carNames, int cycle) { + cars = Arrays.asList(carNames) + .stream() + .map(Car::new) + .collect(Collectors.toList()); + OutputView.printResultTitle(); + + for (int i = 0; i < cycle; i++) { + OutputView.printResultPosition(moveCar()); + System.out.println(); + } + OutputView.printWinner(winner()); + } + + public List moveCar() { + for (Car car : cars) { + car.move(); + } + return cars; + } + + public Winners winner() { + return new Winners(cars ,cars.stream() + .mapToInt(Car::getPosition) + .max() + .getAsInt()); + } +} diff --git a/racingcar/src/main/java/racingcar/Winners.java b/racingcar/src/main/java/racingcar/Winners.java new file mode 100644 index 00000000..c96a5459 --- /dev/null +++ b/racingcar/src/main/java/racingcar/Winners.java @@ -0,0 +1,26 @@ +package racingcar; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class Winners { + private List cars; + private int maxPosition; + + public Winners(List cars, int maxPosition) { + this.cars = cars; + this.maxPosition = maxPosition; + } + + public List getWinnersName() { + List winnerName = new ArrayList<>(); + + winnerName = cars.stream() + .filter(x -> x.getPosition() == maxPosition) + .map(x -> x.getName()) + .collect(Collectors.toList()); + + return winnerName; + } +} \ No newline at end of file diff --git a/racingcar/src/main/java/utils/ErrorCatcher.java b/racingcar/src/main/java/utils/ErrorCatcher.java new file mode 100644 index 00000000..c6a02f25 --- /dev/null +++ b/racingcar/src/main/java/utils/ErrorCatcher.java @@ -0,0 +1,17 @@ +package utils; + +public class ErrorCatcher { + private ErrorCatcher() { + + } + + public static void catchNameError(int nameLongCheck, boolean nameOnlyCheck) { + if (nameOnlyCheck == false) { + System.out.println("[ERROR] 중복된 이름이 있습니다."); + } + + if (nameLongCheck != 0) { + System.out.println("[ERROR] 이름의 길이가 부적절합니다."); + } + } +} diff --git a/racingcar/src/main/java/utils/RandomUtils.java b/racingcar/src/main/java/utils/RandomUtils.java new file mode 100644 index 00000000..d631e64f --- /dev/null +++ b/racingcar/src/main/java/utils/RandomUtils.java @@ -0,0 +1,26 @@ +package utils; + +import java.util.Random; + +public class RandomUtils { + private static final Random RANDOM = new Random(); + + private RandomUtils() { + } + + public static int nextInt(final int startInclusive, final int endInclusive) { + if (startInclusive > endInclusive) { + throw new IllegalArgumentException("[ERROR] 랜덤값이 잘못되었습니다."); + } + + if (startInclusive < 0) { + throw new IllegalArgumentException(); + } + + if (startInclusive == endInclusive) { + return startInclusive; + } + + return startInclusive + RANDOM.nextInt(endInclusive - startInclusive + 1); + } +} diff --git a/racingcar/src/main/java/utils/Validator.java b/racingcar/src/main/java/utils/Validator.java new file mode 100644 index 00000000..2b52d463 --- /dev/null +++ b/racingcar/src/main/java/utils/Validator.java @@ -0,0 +1,23 @@ +package utils; + +import java.util.Arrays; + +public class Validator { + private Validator() { + + } + + public static boolean isNameOnly(String[] carNames) { + return (Arrays.asList(carNames)) + .stream() + .distinct() + .count() != (Arrays.asList(carNames)).size(); + } + + public static int isGoodLength(String[] carNames) { + return (int)(Arrays.asList(carNames) + .stream() + .filter(x -> x.length() > 5) + .count()); + } +} diff --git a/racingcar/src/main/java/view/InputView.java b/racingcar/src/main/java/view/InputView.java new file mode 100644 index 00000000..136e971f --- /dev/null +++ b/racingcar/src/main/java/view/InputView.java @@ -0,0 +1,21 @@ +package view; + +import java.util.Scanner; + +public class InputView { + private static Scanner scanner = new Scanner(System.in); + + public InputView(){ + + } + + public String[] inputCarNames(){ + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + return scanner.nextLine().split(","); + } + + public int inputCycle(){ + System.out.println("시도할 회수는 몇회인가요?"); + return scanner.nextInt(); + } +} diff --git a/racingcar/src/main/java/view/OutputView.java b/racingcar/src/main/java/view/OutputView.java new file mode 100644 index 00000000..2b84813a --- /dev/null +++ b/racingcar/src/main/java/view/OutputView.java @@ -0,0 +1,44 @@ +package view; + +import racingcar.Car; + +import java.util.List; + +import racingcar.Winners; + +public class OutputView { + public static final String PROGRESS = "-"; + public static int maxReach; + + private OutputView() { + + } + + public static void printResultTitle() { + System.out.println("\n실행 결과"); + } + + public static void printResultPosition(List cars) { + for (Car car : cars) { + printCarPosition(car); + } + } + + public static void printCarPosition(Car car) { + System.out.print(car.getName() + " : "); + + for (int i = 0; i < car.getPosition(); i++) { + System.out.print(PROGRESS); + } + System.out.println(); + + if (maxReach < car.getPosition()) { + maxReach = car.getPosition(); + } + } + + public static void printWinner(Winners winners) { + System.out.println("최종 우승자"); + System.out.print(winners.getWinnersName()); + } +}