[자바의 정석 - 기초편] ch14-15,16 스트림, 스트림의 특징
스트림
다양한 데이터 소스(컬렉션, 배열 등) 를 표준화된 방법으로 다루기 위한 것
컬렉션 프레임워크: 컬렉션(List, Set, Map, ...)들을 표준화된 방법으로 다루기 위해 정리했지만 실패함. List, Set, Map의 성격이 달라서 사용방법도 다름
JDK 1.8부터 스트림이 등장하면서 통일됨
중간 연산: 연산 결과가 스트림. 반복적으로 적용 가능
최종 연산: 연산 결과가 스트림이 아님. 단 한 번만 적용 가능 (스트림의 요소를 소모)
스트림으로 변환(생성)
1
2
3
4
5
6
7
8
9
10
|
List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream = list.stream();
Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"});
Stream<Integer> evenStream = Stream.iterator(0, n -> n+2);
Stream<Double> randomStream = Stream.generate(Math::random);
IntStream intStream = new Random().ints(5);
|
cs |
예제
1
2
3
4
5
6
|
stream
.distinct() // 중간 연산
.limit(5) // 중간 연산
.sorted() // 중간 연산
.forEach(System.out::println) // 최종 연산
|
cs |
끊어서 사용 가능
1
2
3
4
5
6
7
8
9
10
|
String[] strArr = {"dd", "aaa", "CC", "cc", "b"};
Stream<String> stream = Stream.of(strArr); // stream 생성
Stream<String> filteredStream = stream.filter(); // 중간 연산
Stream<String> distinctedStream = stream.distinct();
Stream<String> sortedStream = stream.sort();
Stream<String> limitedStream = stream.limit(5);
int total = stream.count(); // 최종 연산
|
cs |
스트림의 특징
1. 데이터 소스를 읽기만 할 뿐, 변경하지 않는다.
1
2
3
4
5
6
7
8
|
List<Integer> list = Arrays.asList(3, 1, 5, 4, 2);
List<Integer> sortedList = list.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(list); // 3, 1, 5, 4, 2
System.out.println(sortedList); // 1, 2, 3, 4, 5
|
cs |
2. Iterator처럼 일회용 (필요하면 다시 스트림 생성해야 함)
1
2
|
strStream.forEach(System.out::println);
int numOfStr = strStream.count(); // 에러 -> 스트림이 이미
|
cs |
3. 지연 연산: 최종 연산 전가지 중산 연산이 수행되지 않음.
1
2
3
|
IntStream intStream = new Random().ints(1, 46);
intStream.distinct().limit(5).sorted() // 무한 스트림이지만 distinct()
.forEach(System.out::println) // 최종 연산
|
cs |
4. 작업을 내부 반복으로 처리
1
2
3
4
5
|
for(String str : strList)
System.out.println(str);
// 위와 동일
stream.forEach(System.out::println);
|
cs |
5. 병렬 스트림: 스트림의 작업을 병렬로 처리 (멀티 쓰레드)
1
2
3
4
5
|
Stream<String> strStream = Stream.of("dd", "aaa", "CC", "cc", "b");
int sum = strStream
.parallel() // 병렬 스트림으로 전환
.mapToInt(s -> s.length()) // 문자열 길이 계산
.sum(); // 모든 문자열 길이의 합
|
cs |
6. 기본형 스트림: IntStream, LongStream, DoubleStream
- 오토박싱, 언박싱의 비효율 제거 (Stream<Integer> 대신 IntStream 사용)
- 숫자와 관련된 유용한 메서드를 Stream<T>보다 더 많이 제공 (sum(), average() 등)
오토박싱: 1 (기본형) -> new Integer(1) (참조형)
언박싱 : new Integer(1) (참조형) -> 1 (기본형)
[자바의 정석 - 기초편] ch14-17~22 스트림만들기
컬렉션으로 스트림 생성
Collection 인터페이스의 stream()으로 컬렉션을 스트림으로 변환
Stream<E> stream()
예제
1
2
3
4
5
|
List<Integer> list = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream = list.stream();
intStream.forEach(System.out::print); // 정상 출력
intStream.forEach(System.out::print); // 에러, 스트림이 이미 닫힘
|
cs |
배열로 스트림 생성
1
2
3
4
5
|
Stream<String> strStream = Stream.of("a", "b", "c");
Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"}, 0, 3);
|
cs |
난수를 요소로 갖는 스트림 생성
1
2
3
4
5
6
7
8
|
IntStream intStream = new Random().ints();
intStream
.limit(5)
.forEach(System.out::println);
// 위와 동일
IntStream intStream = new Random().ints(5);
.forEach(System.out::println);
|
cs |
특정 범위의 정수를 요소로 갖는 스트림 생성(IntStream, LongStream)
1
2
|
IntStream intStream = IntStream.range(1, 5);
IntStream intStream = IntStream.rangeClosed(1, 5);
|
cs |
람다식을 소스로 하는 스트림 생성
1
2
|
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f); // 이전 요소에 종속적
static <T> Stream<T> generate(Supplier<T> s); // 이전 소에 독립적
|
cs |
예제
1
2
3
4
|
Stream<Integer> evenStream = Stream.iterate(0, n -> n+2); // 0, 2, 4, 6, ...
Stream<Double> randomStream = Stream.generate(Math::random);
Stream<Integer> oneStream = Stream.generate( () -> 1);
|
cs |
파일을 소스로 하는 스트림 생성
1
2
3
4
5
|
Stream<Path> Files.list(Path dir); // dir에 있는 파일 또는 디렉토리
Stream<String> Files.lines(Path path); // 파일 내용을 라인 단위로 읽어서 String으로 변환
Stream<String> Files.lines(Path path, Charset cs);
Stream<String> lines();
|
cs |
빈 스트림 생성
1
|
Stream emptyStream = Stream.empty();
|
cs |
[자바의 정석 - 기초편] ch14-23~25 스트림의 연산
중간 연산
최종 연산
[자바의 정석 - 기초편] ch14-26~29 스트림의 중간연산(1)
filter()
1
2
3
4
5
6
7
8
9
10
11
|
IntStream intStream = IntStream.rangeClosed(1, 10); // 1 ~ 10
intStream
.filter(i -> i%2 != 0 && i%3 != 0)
.forEach(System.out::print);
// 위와 동일
intStream
.filter(i -> i%2 != 0)
.filter(i -> i%3 != 0)
.forEach(System.out::print);
|
cs |
ㅇ
문자열 스트림 정렬 방법
Comparator의 comparing()으로 정렬 기준 제공
1
2
3
4
5
|
studentStream
.sorted(Comparator.comparing(Student::getBan)) // 반 별로 정렬
.thenComparing(Student::getTotalScore) // 총점 별로 정렬
.thenComparing(Student::getName)) // 이름 별로 정렬
.forEach(System.out::println);
|
cs |
[자바의 정석 - 기초편] ch14-30~34 스트림의 중간연산(2)
map(): 스트림의 요소 변환
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Stream<File> fileStream = Stream.of(
new File("Ex1.java"),
new File("Ex1"),
new File("Ex1.bak"),
new File("Ex2.java"),
new File("Ex1.txt"));
fileStream
.map(File::getName) // Stream<File> -> Stream<String>
.filter(s -> s.indexOf('.') != -1) // 확장자 없는 것 제외
.map(s -> s.substring(s.indexOf('.') + 1) // 확장자(. 뒤) 추출
.map(String::toUpperCase) // 대문자로 변환
.distinct() // 중복 제거
.forEach(System.out::print);
|
cs |
peek(): 스트림의 요소를 소비하지 않고 읽기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Stream<File> fileStream = Stream.of(
new File("Ex1.java"),
new File("Ex1"),
new File("Ex1.bak"),
new File("Ex2.java"),
new File("Ex1.txt"));
fileStream
.map(File::getName) // 파일명 반환
.filter(s -> s.indexOf('.') != -1) // 확장자 없는 것 제외
.peek(s -> System.out.printf("filename = %s%n", s)) // 파일명 출력
.map(s -> s.substring(s.indexOf('.') + 1) // 확장자(. 뒤) 추출
.peek(s -> System.out.printf("extension = %s%n", s)) // 확장자출력
.map(String::toUpperCase) // 대문자로 변환
.distinct() // 중복 제거
.forEach(System.out::print); // 최종 연산 스트림 소비
|
cs |
flatMap(): 스트림의 스트림(Stream<Stream<T> >)을 스트림(Stream<T>)로 변환
1
2
3
4
5
6
7
8
9
10
|
Stream<String[]> strArrStrm = Stream.of(new String[]{"abc", "def", "ghi"},
new String[]("ABC", "GHI", "JKLMN"});
// 1. map()
Stream<Stream<String> > strStrStrm = strArrStrm.map(Arrays::stream);
// 2. flatMap()
Stream<String> strStrStrm = strArrStrm.flatMap(Arrays::stream);
|
cs |
예제 1
1
2
3
4
5
6
7
8
9
|
Stream<String[]> strArrStrm = Stream.of(new String[]{"abc", "def", "ghi"},
new String[]("ABC", "GHI", "JKLMN"});
Stream<String> strStrm = strArrStrm.flatMap(Arrays::stream);
strStrm
.map(String::toLowerCase)
.distinct()
.sorted()
.forEach(System.out::println);
|
cs |
예제 2
1
2
3
4
5
6
7
8
9
10
11
|
String[] lineArr = {
"Believe or not It is true",
"Do or do not There is no try",
};
Stream<String> lineStream = Arrays.stream(lineArr);
lineStream
.flatMap(line -> Stream.of(line.split(" +"))) // 정규표현식, 공백 1개 이상
.map(String::toLowerCase)
.distinct()
.sorted()
.forEach(System.out::println);
|
cs |
[자바의 정석 - 기초편] ch14-35~39 Optional에 대한 강의입니다.
Optional<T>
T 타입 객체의 래퍼 클래스
1
2
3
4
|
public final clas Optional<T> {
private final T value; // T타입의 참조변수
...
}
|
cs |
null을 Optional<>에 담아서 반환
return null;
=> return Optional<null>;
Optional<T> 객체 생성 방법
1
2
3
4
5
|
String str = "abc";
Optional<String> optVal = Optional.of(str);
Optional<String> optVal = Optional.of("abc");
Optional<String> optVal = Optional.of(null); // NPE 발생
Optional<String> optVal = Optional.ofNullable(null); // OK
|
cs |
null 대신 빈 Optional<T> 객체 사용
1
2
3
|
Optional<String> optVal = null; // 바람직하지 않음
Optional<String> optVal = Optional.empty(); // 빈 객체로 초기화
|
cs |
Optional 객체의 값 가져오기: get(), orElse(), orElseGet(), orElseThrow()
1
2
3
4
5
6
|
Optional<String> optVal = Optional.of("abc");
String str1 = optVal.get(); // null이면 예외 발생
String str2 = optVal.orElse(""); // null이면 "" 반환
String str3 = optVal.orElseGet(String::new); // 람다식 사용 가능 () -> new String()
String str4 = optVal.orElseThrow(NullPointerException::new) // null이면 예외 발생 (예외 지정 가능)
|
cs |
isPresent(): Optional 객체의 값이 null이면 false, 아니면 true 반환
1
2
3
|
if(Optional.ofNullable(str).isPresent()) { // if (str != null)
System.out.println(str);
}
|
cs |
기본형을 감싸는 래퍼 클래스: OptionalInt, OptionalLong, OptionalDouble
일반 Optional<T>보다 성능 좋음
OptionalInt의 값 가져오기
빈 Optional 객체와의 비교
1
2
3
4
5
6
|
OptionalInt opt = OptionalInt.of(0); // 0 저장
OptionalInt op2 = OptionalInt.empty(); // 초기값 0이지만 실제로는 값이 없음
opt.isPresent() -> true
opt2.isPresent() -> false
opt.equals(opt2) -> false
|
cs |
예제 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Optional<String> optStr = Optional.of("abcde");
Optional<Integer> optInt = optStr.map(String::length); // .map(s -> s.length());
int result1 = Optional.of("123")
.filter(x -> x.length() > 0)
.map(Integer::parseInt)
.get();
int result2 = Optional.of("")
.filter(x -> x.length() > 0)
.map(Integer::parseInt)
.orElse(-1);
Optional.of("456")
.map(Integer::parseInt)
.ifPresent(x -> System.out.printf("result3 = %d%n", x);
|
cs |
예제 2
1
2
3
4
5
|
OptionalInt optInt1 = OptionalInt.of(0);
OptionalInt optInt2 = OptionalInt.empty();
System.out.println(optInt1.getAsInt()); // 0
System.out.println(optInt2.getAsInt()); // NoSuchElementException
|
cs |
[자바의 정석 - 기초편] ch14-40~44 스트림의 최종연산에 대한 강의입니다.
forEach(), forEachOrdered(): 스트림의 모든 요소에 지정된 작업 수행
1
2
3
4
5
6
7
8
|
void forEach(Comsumer<? super T> action) // 병렬 스트림인 경우 순서 보장 X
void forEachOrdered(Consumer<? super T> action) // 병렬 스트림인 경우 순서 보장 O
IntStream.range(1, 10).sequential().forEach(System.out::print);
IntStream.range(1, 10).sequential().forEachOrdered(System.out::print);
IntStream.range(1, 10).parallel().forEach(System.out::print); // 순서 보장 X
IntStream.range(1, 10).parallel().forEachOrdered(System.out::print); // 순서 보장 O
|
cs |
|
allMatch(), anyMatch(), noneMatch(): 조건 검사
1
2
3
4
5
6
|
boolean allMatch (Predicate<? super T> predicate)
boolean anyMatch (Predicate<? super T> predicate)
boolean noneMatch (Predicate<? super T> predicate)
boolean hasFailedStu = stuStream
.anyMatch(s -> s.getTotalScore() <= 100);
|
cs |
findFirst(), findAny(): 조건에 일치하는 요소 찾기
1
2
3
4
5
6
7
8
9
10
|
Optional<T> findFirst();
Optional<T> findAny();
Optional<Student> result = stuStream
.filter(s -> s.getTotalScore() <= 100)
.findFirst();
Optional<Student> result = parallelStream
.filter(s -> s.getTotalScore() <= 100)
.findAny();
|
cs |
reduce(): 스트림의 요소를 하나씩 줄여가며 누적연산 수행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
Optional<T> reduce (BinaryOperator<T> accumulator);
T reduce (T identity, BinaryOperator<T> accumulator);
U reduce (T identity, BiFunction<U, T, U> accumulator, BinaryOperator<T> combiner);
* identity : 초기값
* accumulator : 이전 연산결과와 스트림의 요소에 수행할 연산 (누적해서 수행할 작업)
* combiner : 병렬처리된 결과를 합치는데 사용할 연산 (병렬 스트림)
int count = intStream.reduce(0, (a, b) -> a + 1);
/*
int a = identity;
for(int b : stream)
a += 1;
*/
int sum = intStream.reduce(0, (a, b) -> a + b);
/*
int a = identity;
for(int b : stream)
a += b;
*/
int max = intStream.reduce(Integer.MIN_VALUE, (a, b) -> a > b ? a : b);
int min = intStream.reduce(Integer.MAX_VALUE), (a, b) -> a < b ? a : b);
|
cs |
예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
String[] strArr = {
"Inheritance", "Java", "Lambda", "stream", "OptionalDouble", "IntStream", "count", "sum"
};
Stream
.of(strArr)
.forEach(System.out::println);
boolean noEmptyStr = Stream
.of(strArr)
.noneMatch(s -> s.length() == 0);
Optional<String> sWord = Stream
.of(strArr)
.filter(s -> s.charAt(0) == 's')
.findFirst();
System.out.println("noEmptyStr = " + noEmptyStr);
System.out.println("sWord= " + sWord.get());
// Stream<String[]>을 IntStream으로 변환
IntStream intStream1 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream2 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream3 = Stream.of(strArr).mapToInt(String::length);
IntStream intStream4 = Stream.of(strArr).mapToInt(String::length);
int count = intStream1.reduce(0, (a,b) -> a + 1);
int sum = intStream2.reduce(0, (a,b) -> a + b);
OptionalInt max = intStream3.reduce(Integer::max);
OptionalInt min = intStream4.reduce(Integer::min);
System.out.println("count = " + count);
System.out.println("sum = " + sum);
System.out.println("max = " + max.getAsInt());
System.out.println("min = " + min.getAsInt());
|
cs |
[자바의 정석 - 기초편] ch14-45~49 collect()와 Collectors에 대한 강의입니다.
collect(): Collector를 매개변수로 하는 스트림의 최종 연산
그룹별 리듀싱
1
2
|
Object collect(Collector collector);
Object collect(Supplier supplier, Biconsumer accumulator, Biconsumer combiner); // 잘 안씀
|
cs |
Collector: 수집(collect)에 필요한 메서즈를 정의해 놓은 인터페이스
1
2
3
4
5
6
7
8
|
public interface Collector<T, A, R> { // T(요소)를 A에 누적한 다음, 결과를 R로 변환해서 반환
Supplier<A> supplier(); // StringBuilder::new 누적할 곳
BiConsumer<A, T> accumulator(); // (sb, s) -> sb.append(s) 누적해서 수행할 작업
BinaryOperator<A> combiner(); // (sb1, sb2) -> sb1.append(sb2) 결합 방법 (병렬)
Function<A, R> finisher(); // ab -> ab.toString() 최종 변환
Set<Characteristics> characteristics(); // 컬렉터의 특성이 담긴 Set을 반환
...
}
|
cs |
Collectors 클래스는 다양한 기능의 컬렉터(Collector를 구현한 클래스)를 제공
* 변환: mapping(), toList(), toSet(), toMap(), toCollection(), ...
* 통계: counting(), summingInt(), averageInt(), maxBy(), minBy(), summarizingInt(), ...
* 문자열 결합: joining()
* 리듀싱: reducing()
* 그룹화와 분할: groupingBy(), partitioningBy(), collectingAndThen()
collect(): 최종 연산
Collector: 인터페이스
Collectors: Collector를 구현한 클래스
스트림을 컬렉션으로 변환: toList(), toSet(), toMap(), toCollection()
1
2
3
4
5
6
7
8
9
10
|
List<String> names = stuStream // Stream<Student>
.map(Student::getName) // -> Stream<String>
.collect(Collectors.toList()); // -> List<String>
ArrayList<String> list = names // List<String>
.stream() // Stream<String>
.collect(Collectors.toCollection(ArrayList::new)); //-> ArrayList<String>
Map<String, Person> map = personStream // Stream<Person>.
.collect(Collectors.toMap(p -> p.getRegId(), p -> p)); // -> Map<String, Person>
|
cs |
스트림을 배열로 변환: toArray()
1
2
3
|
Student[] stuNames = studentStream.toAray(Student[]::new);
Object[] stuNames = studentStream.toAray(); // 매개변수 없으면 Object[]
Student[] stuNames = studentStream.toAray(); // ERROR
|
cs |
스트림의 통계정보 제공: counting(), summingInt(), maxBy(), minBy()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
long count = stuStream.count();
// 위와 동일
long count = stuStream.collect(counting());
long totalScore = stuStream
.mapToInt(Student::getTotalScore)
.sum();
// 위와 동일
long totalScore = stuStream
.collect(summingInt(Student::getTotalScore));
OptionalInt topScore = studentStream
.mapToInt(Student::getTotalScore)
.max();
Optional<Student> topStudent = stuStream
.max(Comparator.comparingInt(Student::getTotalScore));
// 위와 동일
Optional<Student> topStudent = stuStream
.collect(maxBy(Comparator.comparingInt(Student::getTotalScore)));
|
cs |
스트림을 리듀싱: reducing()
1
2
3
|
Collector reducing(BinaryOperator<T> op)
Collector reducing(T identity, BinaryOperator<T> op)
Collector reducing(U identity, Function<T, U> mapper, BinaryOperator<T> op)
|
cs |
예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
IntStream intStream = new Random()
.ints(1, 46)
.distinct()
.limit(6);
OptionalInt max = intStream.reduce(Integer::max);
OptionalInt<Integer> max = intStream.boxed()
.collect(reducing(Integer::max));
long sum = intStream.reduce(0, (a,b) -> a+b);
long sum = intStream.boxed()
.collect(reducing(0, (a,b) -> a+b));
int grandTotal = stuStream.map(Student::getTotalScore)
.reduce(0, Integer::sum);
int grandTotal = stuStream.collect(reducing(0, Student::getTotalScore, Integer::sum));
|
cs |
문자열 스트림의 요소를 모두 연결: joining()
1
2
3
4
|
String studentNames = stuStream.map(Student::getName).collect(joining()); // ABC
String studentNames = stuStream.map(Student::getName).collect(joining(", ")); // A, B, C
String studentNames = stuStream.map(Student::getName).collect(joining(", ", "[", "]")); // [A, B, C]
String studentInfo = stuStream.collect(joining(", ")); // Student의 toString()으로 결합
|
cs |
[자바의 정석 - 기초편] ch14-50~55 스트림의 그룹화와 분할에 대한 강의입니다.
partitioningBy(): 스트림을 2분할 (예제: https://github.com/castello/javajungsuk3/blob/master/source/ch14/StreamEx7.java)
groupingBy(): 스트림을 n분할 (예제: https://github.com/castello/javajungsuk3/blob/master/source/ch14/StreamEx8.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
Map<Boolean, List<Student> > stuBySex = stuStream
.collect(partitioningBy(Student::isMale)); // 학생들을 성별로 분할
List<Student> maleStudent = stuBySex.get(true); // 남자: true
List<Student> femaleStudent = stuBySex.get(false); // 여자: false
Map<Boolean, Long> stuNumBySex = stuStream
.collect(partitioningBy(Student::isMale,
counting())); // 분할 + 통계
int maleStudentNum = stuNumBySex.get(true); // 남자: true
int femaleStudentNum = stuNumBySex.get(false); // 여자: false
Map<Boolean, Optional<Student> > topScoreBySex = stuStream
.collect(partitioningBy(Student::isMale,
maxBy(comparingInt(Student::getScore))); // 분할 + 통계
int maleTopScoreBySex = topScoreBySex.get(true); // 남자: true
int femaleTopScoreBySex = topScoreBySex.get(false); // 여자: false
Map<Boolean, Map<Boolean, List<Student> > > failedStuBySex = stuStream
.collect(partitioningBy(Student::isMale,
partitioningBy(s -> s.getScore() < 150))); // 다중 분할
List<Student> failedMaleStudent = failedStuBySex.get(true); // 남자: true
List<Student> failedFemaleStudent = failedStuBySex.get(false); // 여자: false
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
Map<Integer, List<Student> > stuByBan = stuStream
.collect(groupingBy(Student::getBan, toList()));
Map<Integer, Map<Integer, List<Student> > > stuByHakAndBan = stuStream
.collect(groupingBy(Student::getHak, // 1. 학년 별 그룹화
groupingBy(Student::getBan) // 2. 반 별 그룹화
));
Map<Integer, Map<Integer, Set<Student.Level> > > stuByHakAndBan = stuStream
.collect(
groupingBy(Student::getHak,
groupingBy(Student::getBan,
mapping(s -> {
if (s.getScore() >= 200) return Student.Level.HIGH;
else if (s.getScore() >= 100) return Student.Level.MID;
else return Student.Level.LOW;
}, toSet())))
);
|
cs |
'오늘 배운 것' 카테고리의 다른 글
스트림 요약 (0) | 2021.08.23 |
---|---|
람다 (자바의 정석) (0) | 2021.08.20 |
Docker, Kubernetes, DevOps의 개념 (0) | 2021.08.17 |
[스프링 핵심 원리 기본] 객체지향 설계와 스프링 (0) | 2021.07.28 |
[스프링 MVC 2] 스프링 타입 컨버터(2), 파일 업로드 (0) | 2021.07.26 |