Preface
이번 장의 제목에 병렬 처리도 포함되어 있지만, 간단한 개념만 소개할 뿐 대부분은 스트림에 관한 내용이었다.
스트림의 개념과 사용 방법 등 다양한 내용을 공부했는데, 람다식과 제네릭, 참조 등 앞 장 내용에 관한 지식이 요구되는 부분이 많았다.
어찌저찌 이해는 할 수 있었지만, 아무래도 해당 내용들을 직접 코드에 적용하기 위해선 많은 복습과 추가적인 공부가 필요할 듯하다.
1권은 자바의 기본적인 사용 방법에 관한 내용들이었다면, 2권에선 조금 더 딥하고 전문적인 내용을 소개하는 것 같다.
2권의 첫 장인 멀티 스레드에 관한 글을 업로드하며 말했듯 모든 내용을 완벽히 이해하려고 시간을 쓰며 스트레스를 받기보단 대충 이런 내용이 있구나 정도의 느낌만 얻은 뒤 필요할 때 직접 나만의 코드를 작성해보며 경험해보는 것이 좋을 것 같다.
물론 책을 모두 마친 후 2권의 전체적인 복습은 반드시 필요할 듯.
1. 스트림 소개
- 스트림(stream): 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자
1) stream( ) 메소드로 스트림 객체를 얻은 후 stream.forEach(변수 -> System.out.println(변수)); 메소드를 통해 컬렉션 요소를 하나씩 콘솔에 출력한다.
2) forEach( ) 메소드는 Consumer 함수적 인터페이스 타입(매개값 O, 리턴값 X)의 매개값을 가지므로 람다식으로 기술할 수 있다.
package ch16;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;
public class IteratorVSStreamExample {
public static void main(String[] args) {
List<String> list = Arrays.asList("Kim", "Park", "Lee");
// Iterator
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
System.out.println();
// Stream
Stream<String> stream = list.stream();
stream.forEach(name -> System.out.println(name));
}
}
- 스트림의 특징
1) 람다식으로 요소 처리 코드를 제공한다.
2) 내부 반복자를 사용하므로 병렬 처리가 쉽다.
3) 중간 처리와 최종 처리 작업을 수행한다.
- Stream이 제공하는 대부분의 요소 처리 메소드는 함수적 인터페이스 타입을 가지므로 람다식 또는 메소드 참조를 통해 요소 처리 내용을 매개값으로 전달할 수 있다.
package ch16;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
}
public class LambdaExpressionsExample {
public static void main(String[] args) {
List<Student> list = Arrays.asList(new Student("Kim", 90), new Student("Park", 92));
Stream<Student> stream = list.stream();
stream.forEach(s -> {
String name = s.getName();
int score = s.getScore();
System.out.println(name + "-" + score);
});
}
}
- 외부 반복자: 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴
→ index를 사용하는 for문, Iterator를 이용하는 while문 등
- 내부 반복자: 컬렉션 내부에서 요소들을 반복시키고, 개발자는 요소당 처리해야 할 코드만 제공하는 코드 패턴
1) 코드가 간결해진다.
2) 요소의 병렬 처리가 컬렉션 내부에서 처리된다.
- 병렬(parallel) 처리: 한 가지 작업을 서브 작업으로 나눈 후 서브 작업들을 분리된 스레드에서 병렬적으로 처리하는 것
→ 병렬 처리 스트림을 이용하면 런타임 시 하나의 작업을 서브 작업으로 자동으로 나눈 후 서브 작업의 결과를 자동으로 결합해 최종 결과물을 생성한다.
package ch16;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
public class ParallelExample {
public static void main(String[] args) {
List<String> list = Arrays.asList("A", "B", "C", "D", "E");
// 순차처리
Stream<String> stream = list.stream();
stream.forEach(s -> ParallelExample.print(s)); //람다식 이용
System.out.println();
// 병렬처리
Stream<String> parallelStream = list.parallelStream();
parallelStream.forEach(ParallelExample::print); //메소드 참조 이용
}
public static void print(String str) {
System.out.println(str + ": " + Thread.currentThread().getName());
}
}
- 스트림의 중간 처리와 최종 처리
1) 중간 처리: 매핑, 필터링, 정렬 등을 수행
2) 최종 처리: 반복, 카운팅, 평균, 총합 등의 집계 처리를 수행
2. 스트림의 종류
- BaseStream 인터페이스: 모든 스트림에서 사용할 수 있는 공통 메소드들이 정의되어 있을 뿐 코드에서 직접 사용되지는 않는다.
1) Stream: 객체 요소를 처리하는 스트림
2) IntStream, LongStream, DoubleStream: 기본 타입인 int, long, double 요소를 처리하는 스트림
- 배열로부터 스트림 얻기
package ch16;
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class FromArrayExample {
public static void main(String[] args) {
String strArray[] = { "Kim", "Park", "Lee" };
Stream<String> strStream = Arrays.stream(strArray);
strStream.forEach(a -> System.out.print(a + ","));
System.out.println();
int intArray[] = { 1, 2, 3, 4, 5 };
IntStream intStream = Arrays.stream(intArray);
intStream.forEach(a -> System.out.print(a + ","));
System.out.println();
}
}
- 숫자 범위로부터 스트림 얻기
1) rangeClosed( ) 메소드: 첫 번쨰 매개값에서부터 두 번째 매개값까지 순차적으로 제공하는 IntStream을 리턴
2) range( ) 메소드: rangeClosed( ) 메소드와 동일한 IntStream을 리턴하지만, 두 번째 매개값은 포함되지 않는다.
package ch16;
import java.util.stream.IntStream;
public class FromIntRangeExample {
public static int sum;
public static void main(String[] args) {
// IntStream stream = IntStream.rangeClosed(1, 100);
// stream.forEach(new IntConsumer() {
// @Override
// public void accept(int a) {
// sum += a;
// }
// });
// System.out.println("total: " + sum);
IntStream stream = IntStream.rangeClosed(1, 100);
stream.forEach(a -> sum += a);
System.out.println("total: " + sum);
}
}
3. 스트림 파이프라인
- 리덕션(Reduction): 대량의 데이터를 가공해서 축소하는 것
1) 데이터의 합계, 평균값, 카운팅, 최대·최소값 등
2) 컬렉션의 요소를 리덕션의 결과물로 바로 집계할 수 없을 땐 중간 처리가 필요하다.
- 파이프라인: 여러 개의 스트림이 연결되어 있는 구조
1) 최종 처리를 제외한 스트림은 모두 중간 처리 스트림이다.
2) 중간 처리 스트림은 최종 처리가 시작되기 전까지 지연된다.
- 중간 처리 메소드는 중간 처리된 스트림을 리턴하며, 해당 스트림에서 다시 중간 처리 메소드를 호출하여 파이프라인을 형성한다.
package ch16;
import java.util.Arrays;
import java.util.List;
class Member {
public static int MALE = 0;
public static int FEMALE = -1;
private String name;
private int sex;
private int age;
public Member(String name, int sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public int getSex() {
return sex;
}
public int getAge() {
return age;
}
}
public class StreamPipelinesExample {
public static void main(String[] args) {
List<Member> list = Arrays.asList(new Member("Kim", Member.MALE, 30), new Member("Park", Member.FEMALE, 20),
new Member("Lee", Member.MALE, 45), new Member("Cha", Member.FEMALE, 27));
double ageAvg = list.stream().filter(m -> m.getSex() == Member.MALE).mapToInt(Member::getAge).average()
.getAsDouble();
System.out.println("남자 평균 나이: " + ageAvg);
}
}
- 리턴 타입이 스트림이라면 중간 처리 메소드, 기본 타입이거나 OptionalXXX라면 최종 처리 메소드이다.
4. 필터링
- 필터링: 중간 처리 기능으로 요소를 걸러내는 역할을 한다.
- distinct( ) 메소드: 중복을 제거
1) Stream의 경우: Object.equals(Object)가 true이면 동일한 객체로 판단하고 중복을 제거
2) IntStream, LongStream, DoubleStream의 경우: 동일값일 경우 중복을 제거
- filter( ) 메소드: 매개값으로 주어진 Predicate가 true를 리턴하는 요소만 필터링
package ch16;
import java.util.Arrays;
import java.util.List;
public class FilteringExample {
public static void main(String[] args) {
List<String> names = Arrays.asList("홍길동", "신용권", "감자바", "신용권", "신민철");
names.stream().distinct().forEach(n -> System.out.println(n));
System.out.println();
names.stream().filter(n -> n.startsWith("신")).forEach(n -> System.out.println(n));
System.out.println();
names.stream().distinct().filter(n -> n.startsWith("신")).forEach(n -> System.out.println(n));
}
}
5. 매핑
- 매핑: 중간 처리 기능으로 스트림의 요소를 다른 요소로 대체하는 작업
- flatMapXXX( ) 메소드: 요소를 대체하는 복수 개의 요소들로 구성된 새로운 스트림을 리턴
package ch16;
import java.util.Arrays;
import java.util.List;
public class FlatMapExample {
public static void main(String[] args) {
List<String> inputList1 = Arrays.asList("java8 lambda", "stream ampping");
inputList1.stream().flatMap(data -> Arrays.stream(data.split(" "))).forEach(word -> System.out.println(word));
System.out.println();
List<String> inputList2 = Arrays.asList("10, 20, 30", "40, 50, 60");
inputList2.stream().flatMapToInt(data -> {
String strArr[] = data.split(",");
int intArr[] = new int[strArr.length];
for (int i = 0; i < strArr.length; i++) {
intArr[i] = Integer.parseInt(strArr[i].trim());
}
return Arrays.stream(intArr);
}).forEach(number -> System.out.println(number));
}
}
- mapXXX( ) 메소드: 요소를 대체하는 요소로 구성된 새로운 스트림을 리턴
package ch16;
import java.util.Arrays;
import java.util.List;
public class MapExample {
public static void main(String[] args) {
List<Student> studentList = Arrays.asList(new Student("Kim", 10), new Student("Park", 20),
new Student("Lee", 30));
studentList.stream().mapToInt(Student::getScore).forEach(score -> System.out.println(score));
}
}
- asDoubleStream( ) 메소드: IntStream의 int 또는 LongStream의 long 요소를 double 요소로 타입 변환해서 DoubleStream을 생성
- asLongStream( ) 메소드: IntStream의 int 요소를 long 요소로 타입 변환해서 LongStream을 생성
- boxed( ) 메소드: int, long, double 요소를 Integer, Long, Double 요소로 박싱해서 Stream을 생성
package ch16;
import java.util.Arrays;
import java.util.stream.IntStream;
public class AsDoubleStreamAndBoxedExample {
public static void main(String[] args) {
int intArr[] = { 1, 2, 3, 4, 5 };
IntStream intStream = Arrays.stream(intArr);
intStream.asDoubleStream().forEach(d -> System.out.println(d));
System.out.println();
intStream = Arrays.stream(intArr);
intStream.boxed().forEach(obj -> System.out.println(obj.intValue()));
}
}
6. 정렬
- 스트림은 요소가 최종 처리되기 전에 중간 단계에서 요소를 정렬해서 최종 처리 순서를 변경할 수 있다.
- 객체 요소일 경우에는 클래스가 Comparable을 구현하지 않으면 sorted( ) 메소드를 호출했을 때 ClassCastException이 발생하므로 Comparable을 구현한 요소에서만 sorted( ) 메소드를 호출해야 한다.
→ 사용자 정의 클래스에서도 Comparable을 구현하면 자동 정렬이 가능하다.
- 객체 요소가 Comparable을 구현한 상태에서 기본 비교 방법으로 정렬
sorted();
sorted( (a,b) -> a.compareTo(b) );
sorted( Comparator.naturalOrder() );
- 객체 요소가 Comparable을 구현하고 있지만, 기본 비교 방법과 정반대 방법으로 정렬
sorted( (a,b) -> b.compareTo(a) );
sorted( Comparator.reverseOrder() );
- 객체 요소가 Comparable을 구현하지 않은 경우엔 Comparator을 매개값으로 갖는 sorted( ) 메소드를 사용하면 된다.
sorted( (a,b) -> { ... } );
//중괄호 안에는 compare() 메소드와 같은 리턴값을 갖도록 코드를 작성
package ch16;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.IntStream;
class Student1 implements Comparable<Student1> {
private String name;
private int score;
public Student1(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
@Override
public int compareTo(Student1 o) {
return Integer.compare(score, o.score);
}
}
public class SortingExample {
public static void main(String[] args) {
// 숫자 요소일 경우
IntStream intStream = Arrays.stream(new int[] { 5, 3, 2, 1, 4 });
intStream.sorted().forEach(n -> System.out.print(n + ","));
System.out.println();
// 객체 요소일 경우
List<Student1> studentList = Arrays.asList(new Student1("Kim", 30), new Student1("Park", 10),
new Student1("Lee", 20));
studentList.stream().sorted().forEach(s -> System.out.print(s.getScore() + ","));
System.out.println();
studentList.stream().sorted(Comparator.reverseOrder()).forEach(s -> System.out.print(s.getScore() + ","));
}
}
7. 루핑
- 루핑: 요소 전체를 반복하는 것
- peek( ) 메소드: 중간 처리 단계에서 전체 요소를 루핑하면서 추가적인 작업을 하기 위해 사용한다.
→ 최종 처리 메소드가 호출되어야 동작한다.
- forEach( ) 메소드: 요소를 소비하는 최종 처리 메소드이므로 이후에 다른 최종 메소드를 호출할 수 없다.
8. 매칭
- 매칭 메소드: 최종 처리 단계에서 요소들이 특정 조건에 만족하는지 조사하는 역할
- allMatch( ) 메소드: 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
- anyMatch( ) 메소드: 퇴소한 한 개의 요소가 조건을 만족하는지 조사
- noneMatch( ) 메소드: 모든 요소들이 매개값으로 주어진 조건을 만족하지 않는지 조사
package ch16;
import java.util.Arrays;
public class MatchExample {
public static void main(String[] args) {
int intArr[] = { 2, 4, 6 };
boolean result = Arrays.stream(intArr).allMatch(a -> a % 2 == 0);
System.out.println("모두 2의 배수인가?" + result);
result = Arrays.stream(intArr).anyMatch(a -> a % 3 == 0);
System.out.println("하나라도 3의 배수가 있는가?" + result);
result = Arrays.stream(intArr).noneMatch(a -> a % 3 == 0);
System.out.println("3의 배수가 없는가?" + result);
}
}
9. 기본 집계
- 집계(Aggregate): 최종 처리 기능으로 요소들을 처리해서 하나의 값으로 산출하는 것
→ 리덕션이라고 볼 수 있다.
- 집계 메소드의 종류
리턴 타입 | 메소드 | 설명 |
long | count( ) | 요소 개수 |
OptionalXXX | findFirst( ) | 첫 번째 요소 |
Optional<T> OptionalXXX |
max(Comparator<T>) max( ) |
최대 요소 |
Optional<T> OPtionalXXX |
min(Comparator<T>) min( ) |
최소 요소 |
OptionalDouble | average( ) | 요소 평균 |
int, long, double | sum( ) | 요소 총합 |
- OptionalXXX: java.util 패키지의 Optional, OptioanlDouble, OptionalInt, OptionalLong 클래스 타입
→ 값을 얻기 위해선 get( ), getAsXXX( )를 호출하면 된다.
- Optional 클래스: 단순히 집계 값만 저장하는 것이 아니라 집계 값이 존재하지 않을 경우 디폴트 값을 설정하거나 집계 값을 처리하는 Consumer(매개값 O, 리턴값 X: accept( ) 메소드)도 등록할 수 있다.
- Optional 클래스의 메소드
리턴 타입 | 메소드 | 설명 |
boolean | isPresent( ) | 값이 저장되어 있는지 여부 |
T double int long |
orElse(XXX) | 값이 저장되어 있지 않을 경우 디폴트 값 지정 |
void | ifPresent(XXXConsumer) | 값이 저장되어 있을 경우 Consumer에서 처리 |
- 요소가 없을 경우 NoSuchElementException 예외를 피하는 방법
1) Optional 객체를 얻어 isPresent( ) 메소드로 평균값 여부를 확인
2) orElse( ) 메소드로 디폴트 값을 세팅
3) ifPresent( ) 메소드로 평균 값이 있을 경우에만 값을 이용하는 람다식 실행
package ch16;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalDouble;
public class OptionalExample {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
OptionalDouble optional = list.stream().mapToInt(Integer::intValue).average();
if (optional.isPresent()) {
System.out.println("방법1_평균: " + optional.getAsDouble());
} else {
System.out.println("방법1_평균: 0.0");
}
double avg = list.stream().mapToInt(Integer::intValue).average().orElse(0.0);
System.out.println("방법2_평균: " + avg);
list.stream().mapToInt(Integer::intValue).average().ifPresent(a -> System.out.println("방법3_평균: " + a));
}
}
10. 커스텀 집계
- reduce( ) 메소드: 기본 집계 메소드 대신 다양한 집계 결과물을 만들 수 있는 메소드
1) 각 인터페이스에는 매개 타입으로 XXXOperator, 리턴 타입으로 OptionalXXX, int, long, double을 가지는 reduce( ) 메소드가 오버로딩되어 있다.
2) 스트림에 요소가 없을 경우엔 디폴트 값인 identity 매개값이 리턴된다.
3) XXXOperator 매개값은 집계 처리를 위한 람다식을 대입한다.
int sum = studentList.stream().map(Student :: getScore)
.reduce((a,b) -> a+b).get();
int sum = studentList.stream().map(Student :: getScore)
.reduce(0, (a, b) -> a+b);
11. 수집
- collect( ) 메소드: 요소들을 필터링 또는 매핑한 후 요소들을 수집하는 최종 처리 메소드
→ 필요한 요소만 컬렉션으로 담을 수 있고, 요소들을 그룹핑한 후 집계할 수 있다.
- collect(Collector<T, A, R> collector) 메소드: 필터링 또는 매핑된 요소들을 새로운 컬렉션에 수집한 뒤 컬렉션을 리턴
1) T: 요소
2) A: 누적기(accumulator)
3) R: 요소가 저장될 컬렉션
- Collectors 클래스의 정적 메소드
메소드 | 설명 |
toList( ) | T를 List에 저장 |
toSet( ) | T를 Set에 저장 |
toCollection(Supplier<Collection<T>>) | T를 Supplier가 제공한 Collection에 저장 |
toMap( Function<T,K> keyMapper, Function<T,U> valueMapper ) |
T를 K와 U로 매핑해서 K를 키로, U를 값으로 Map에 저장 |
toConcurrentMap( Function<T,K> keyMapper, Function<T,U> valueMapper ) |
T를 K와 U로 매핑해서 K를 키로, U를 값으로 ConcurrentMap에 저장 |
- 값을 사용자 정의 컨테이너 객체에 수집할 수도 있다.
1) 요소를 그룹핑해서 수집할 수도 있다.
2) 요소를 그룹핑 후 매핑 및 집계할 수도 있다.
package ch16;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Student2 {
public enum Sex {
MAlE, FEMALE
}
public enum City {
Seoul, Pusan
}
private String name;
private int score;
private Sex sex;
private City city;
public Student2(String name, int score, Sex sex) {
this.name = name;
this.score = score;
this.sex = sex;
}
public Student2(String name, int score, Sex sex, City city) {
this.name = name;
this.score = score;
this.sex = sex;
this.city = city;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
public Sex getSex() {
return sex;
}
public City getCity() {
return city;
}
}
class MaleStudent {
private List<Student2> list;
public MaleStudent() {
list = new ArrayList<Student2>();
System.out.println("[" + Thread.currentThread().getName() + "] MaleStudent()");
}
public void accumulate(Student2 student) {
list.add(student);
System.out.println("[" + Thread.currentThread().getName() + "] accumulate()");
}
public void combine(MaleStudent other) {
list.addAll(other.getList());
System.out.println("[" + Thread.currentThread().getName() + "] combine()");
}
public List<Student2> getList() {
return list;
}
}
public class MaleStudentExample {
public static void main(String[] args) {
List<Student2> totalList = Arrays.asList(new Student2("Kim", 10, Student2.Sex.MAlE),
new Student2("Park", 6, Student2.Sex.FEMALE), new Student2("Lee", 10, Student2.Sex.MAlE),
new Student2("Cha", 6, Student2.Sex.FEMALE));
MaleStudent maleStudent = totalList.stream().filter(s -> s.getSex() == Student2.Sex.MAlE)
.collect(MaleStudent::new, MaleStudent::accumulate, MaleStudent::combine);
maleStudent.getList().stream().forEach(s -> System.out.println(s.getName()));
}
}
12. 병렬 처리
- 병렬 처리(Parallel Operation): 멀티 코어 CPU 환경에서 하나의 작업을 분할해서 각각의 코어가 병렬적으로 처리하는 것
- 동시성(Concurrency): 멀티 작업을 위해 멀티 스레드가 번갈아가며 실행하는 성질
- 병렬성(Parallelism): 멀티 작업을 위해 멀티 코어를 이용해 동시에 실행하는 성질
1) 데이터 병렬성: 전체 데이터를 쪼개 서브 데이터들로 만들고 이들을 병렬 처리해서 작업을 빨리 끝내는 것
→ 하나의 작업
2) 작업 병렬성: 서로 다른 작업을 병렬 처리하는 것
→ 여러 개의 작업
- 병렬 스트림은 요소들을 병렬 처리하기 위해 포크조인(ForkJoin) 프레임워크를 사용한다.
1) 포크 단계: 전체 데이터를 서브 데이터로 분리
2) 서브 데이터를 멀티 코어에서 병렬로 처리
3) 조인 단계: 서브 결과를 결합해 최종 결과를 생성
4) 스레드풀인 ForkJoinPool을 제공한다.
- 병렬 처리를 위해 코드에서 포크조인 프레임워크를 직접 사용할 순 있지만, 병렬 스트림을 이요하는 것이 훨씬 간편하다.
1) parallelStream( ) 메소드: 컬렉션으로부터 병렬 스트림을 바로 리턴
2) parallel( ) 메소드: 순차 처리 스트림을 병렬 처리 스트림으로 변환하여 리턴
- 병렬 처리에 영향을 미치는 요인
1) 요소의 수와 요소당 처리 시간
2) 스트림 소스의 종류
3) 코어의 수
'Java > 이것이 자바다' 카테고리의 다른 글
IO 기반 입출력 (0) | 2023.05.12 |
---|---|
이자바 16장(스트림과 병렬 처리) 확인문제 (0) | 2023.05.07 |
이자바 15장(컬렉션 프레임워크) 확인문제 (0) | 2023.05.02 |
컬렉션 프레임워크 (0) | 2023.05.02 |
이자바 14장(람다식) 확인문제 (0) | 2023.04.28 |
댓글