기강 자바-05
JDK8
- 이번에는 자바8에 들어온 문법들에 대해 배워본다.
- 함수형 프로그래밍
- Optional
- Stream
- CompletableFuture
- 그런데 이제 제네릭을 곁들인..
- Generic
제네릭
- 제네릭이란,
- 클래스 내부에서 사용할 타입을 타입 선언 시 외부에서 지정해주는 기법이다.
- 클래스, 인터페이스, 메서드 작성 시에 다양한 데이터 타입과 함께 사용할 수 있도록 해주는 기능이다.
- jdk 1.5부터 도입 되었다.
- 제네릭은 다음과 같은 특징들을 가진다.
- 타입 파라미터
- 여러 데이터 타입을 유동적으로 사용할 수 있어 재사용성이 뛰어나다.
- 컴파일 시점에 타입 결정
- 런타임에 발생할 수 있는 타입 관련 에러를 방지해, 타입 안전한(type-safety) 코드를 작성할 수 있다.
- 타입 추론
- 생성자 호출이나 메서드 호출에 타입을 명시하지 않아도, 컴파일러가 타입을 추론하여 박아준다.
- 타입 파라미터
- 제네릭이 사용될 수 있는 범위는 다음과 같다.
- 클래스
- 클래스 내에서 사용할 데이터 타입을 일반화
- 인터페이스
- 인터페이스를 구현하는 구현 클래스에서 사용할 데이터 타입을 일반화
- 메서드
- 메서드 내에서 사용될 데이터 타입 일반화
- 클래스
코드로 짜보기
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
class Box<T> { // 클래스 타입 파라미터
T data;
// constructor..
public T getData() {
return data;
}
// 메서드 타입 파라미터
public <R> R doNothing(R result) { // 파라미터의 타입과 같은 타입으로 리턴하도록 타입 파라미터를 지정함.
return result;
}
}
class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>("babo"); // 타입 추론으로 생성자 호출 시에는 String 타입을 명시해주지 않아도 됨.
Box<Integer> integerBox = new Box<>(123);
// Box를 제네릭으로 정의해둬서 한 번의 코드 작성으로 String, Integer 타입을 모두 사용할 수 있다.
sout(stringBox.getData()); // babo
sout(integerBox.getData()); // 123
Double doubleResult = stringBox.doNothing(2.0d); // String 타입으로 결정된 strBox지만,
// 해당 메서드에선 Double 타입으로 결정됨
Integer integerResult = stringBox.doNothing(1); // 이번엔 Integer 타입의 값을 넣었더니
// 해당 메서드의 리턴 타입이 Integer로 결정되었다.
}
}
제한된 제네릭
- 제한된 제네릭은 제네릭의 제약을 걸어 사용될 수 있는 타입에 제한을 거는 것이다.
- 제네릭에 제한을 거는 방법은 다음과 같다.
- T extends SuperClass
- SuperClass의 하위 클래스인 타입만 오도록 상한을 거는 법
- T extends SuperClass
- 또 제한의 기준이 되는 대상은 클래스, 인터페이스 모두 될 수 있다.
- 제네릭 타입에 하한을 거는 방법은 없다.
- T extends SubClass <= 이런 문법은 없음.
코드로 짜보기
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
37
38
39
40
class Korean {
public String koreanGreetings() {
return "안녕하세요";
}
}
class ChungCheoungIn extends Korean {
@Override
public String koreanGreetings() {
return "안녕하셔유";
}
}
class GyeongSangIn extends Korean {
@Override
public String koreanGreetings() {
return "안녕하싱교";
}
}
class Person<T extends Korean> { // 제네릭으로 올 수 있는 타입을
T korean; // Korean 클래스의 서브 클래스로 제한을 뒀다.
public void sayHello() {
sout(korean.koreanGreetings()); // Korean의 서브 클래스로 타입을 한정했기 때문에
} // Korean의 멤버 메서드를 사용할 수 있다고 일반화 할 수 있다.
}
class Main {
public static void main(String[] args) {
Person<ChungCheoungIn> cci = new Person<>(); // Korean의 서브 클래스인 ChungCheoungIn과 GyeongSangIn은
Person<GyeongSangIn> gsi = new Person<>(); // Person의 타입 파라미터로 올 수 있지만
Person<Japanese> jap = new Person<>(); // Compile error.
cci.sayHello(); // 안녕하셔유
gsi.sayHello(); // 안녕하싱교
}
}
- extends로 제네릭 타입에 상한을 거는 예시이다.
- Person의 제네릭 타입으로는 Korean의 서브 클래스들만이 들어올 수 있다.
- 컴파일 타임에 해당 타입이 결정되겠지만, 최소 Korean의 서브 클래스이기 때문에 Korean의 메서드를 사용할 수 있다.
- 서브 클래스 뿐만인 아닌 특정 인터페이스의 구현 클래스로도 타입을 한정할 수 있다.
와일드카드
- 와일드카드는 타입에 대한 유연성을 제공하기 위한 기능이다.
- 제네릭은 컴파일 시점에 한 가지로 구체적인 타입이 결정된다.
- 그러나 와일드카드는 런타임 시점 내내 구체적인 타입이 결정되지 않는다.
- 와일드카드는 주로 제네릭 코드에서 사용되며, 타입에 대한 제약을 덜어준다.
- 와일드카드는 주로 다음과 같은 곳에 위치한다.
- 메서드 파라미터
- 메서드 리턴 타입
- 제네릭 클래스의 필드 변수
코드로 짜보기
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
class Main {
public static void main(String[] args) {
List<String> strList = List.of("a", "b", "c");
List<Integer> intList = List.of(1, 2, 3);
printList(strList); // a b c
printList(intList); // 1 2 3
printNumberList(strList); // Compile error.
printNumberList(intList); // 1 2 3
}
private static void printList(List<?> list) { // 파라미터로 받을 리스트의 제네릭 타입을 <?> 와일드 카드로 주었다.
for(Object o : list) {
sout(o); // 리스트 요소의 타입이 어느 하나로 확정되지 않기 때문에 Object 타입으로 받아서 사용한다.
}
}
private static void printNumberList(List<? extends Number> list) { // 타입을 Number의 구현 클래스로 제한하여 파라미터로 받았다.
for(Number n : list) {
sout(n); // 리스트 요소의 타입이 Number로 제한됨.
}
}
}
함수형 프로그래밍
- 함수형 프로그래밍이란,
- 함수가 1등 시민(First class)인 프로그래밍 기법이다.
- 함수형 프로그래밍은 함수가 어디든 위치할 수 있다.
- 파라미터, 리턴 타입, 변수 등
- JDK8부터 함수형 프로그래밍을 지원한다.
- 함수형 인터페이스(Functional Interface)
- 람다 표현식 (Lambda Expression)
- 메소드 레퍼런스 (Method Reference)
함수형 인터페이스
- 함수형 인터페이스란,
- 간단히 말하면 하나의 추상 메서드만을 가지는 인터페이스이다.
- 수천 개의 default 메서드, private 메서드 가져도 노 상관
- @FunctionalInterface
- 이 메타 어노테이션으로 컴파일 시점에 해당 인터페이스가 FunctionalInterface 임을 알린다.
- 간단히 말하면 하나의 추상 메서드만을 가지는 인터페이스이다.
- 함수형 인터페이스로 선언된 애들만이 함수처럼 동작할 수 있다.
- 람다 표현식
- 메서드 레퍼런스
기본적인 함수형 인터페이스들
- 가장 기본적인 함수형 인터페이스들은 다음과 같은 것들이 있다.
- Function<T, R>
- T 값을 받아 동작 수행 후, R로 리턴한다.
- R apply(T)
- UnaryOperator<T>
- T 값을 받아 동작 수행 후, T로 리턴한다.
- T apply(T)
- Consumer<T>
- T 값을 받아 동작 수행 후, 리턴값 없다.
- void accept(T)
- Supplier<T>
- 인풋값을 받지 않고, 동작 수행 후 T로 리턴한다.
- T get()
- Predicate<T>
- T 값을 받아 동작 수행 후, boolean으로 리턴한다.
- boolean test(T)
- Runnable
- 인풋값을 받지 않고, 리턴값도 없다.
- run()
- Function<T, R>
코드로 짜보기
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
public class NumberConverter {
private int number;
// constructor..
public <R> R convertAnotherType(Function<Integer, R> function) {
return function.apply(number);
}
public Integer calculate(UnaryOperator<Integer> unaryOperator) {
return unaryOperator.apply(number);
}
public void printResult(Consumer<Integer> consumer) {
consumer.accept(number);
}
public <R> R getRandomNumber(Supplier<R> supplier) {
return supplier.get();
}
public boolean isOver100(Predicate<Integer> predicate) {
return predicate.test(number);
}
}
- 위처럼 FunctionalInterface 들을 파라미터로 받는 메서드들이 있다고 할 때.
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class Main {
public static void main(String[] args) {
NumberConverter numberConverter = new NumberConverter();
// T 타입의 값을 받아, R 타입의 값을 리턴하는 Function
String convertTypeA = numberConverter.convertAnotherType(new Function<Integer, String>() {
@Override
public String apply(Integer integer) {
return integer.toString();
}
});
String convertTypeB = numberConverter.convertAnotherType(x -> x.toString());
// T 타입의 값을 받아, T 타입의 값을 리턴하는 UnaryOperator
Integer calculateResultA = numberConverter.calculate(new UnaryOperator<Integer>() {
@Override
public Integer apply(Integer integer) {
return integer + 10;
}
});
Integer calculateResultB = numberConverter.calculate(x -> x + 10);
// T 타입의 값을 받아, 아무 값도 리턴하지 않는 Consumer
numberConverter.printResult(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
numberConverter.printResult(x -> System.out.println(x));
// 아무 값도 받지 않고, T 타입의 값을 리턴하는 Supplier
Integer randomNumberA = numberConverter.getRandomNumber(new Supplier<Integer>() {
@Override
public Integer get() {
return new Random().nextInt(10);
}
});
Integer randomNumberB = numberConverter.getRandomNumber(() -> new Random().nextInt(10));
// T 타입의 값을 받아, boolean으로 리턴하는 Predicate
boolean over100A = numberConverter.isOver100(new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
return integer > 100;
}
});
boolean over100B = numberConverter.isOver100(x -> x > 100);
}
}
- A 변수들은 FunctionalInterface의 익명 클래스를 구현해 메서드를 사용한 것을 볼 수 있다.
- 소스가 지저분하여 가독성이 좋지 못하다.
- 반면 B 변수들은 훨씬 깔끔하고 짧게 작성된 것을 볼 수 있다.
- 이것이 다음에 나올 람다 표현식이다.
- 훨씬 깔끔하고 가독성이 뛰어나다.
- 함수형 인터페이스를 쓰는 이유에는 함수를 파라미터나 리턴 값 등에 사용할 수 있어서도 있지만,
- 람다 표현식, 메서드 레퍼런스와 같이 깔끔하게 소스를 짤 수 있다는 것도 매우 크다.
람다 표현식
- 함수형 인터페이스를 익명 함수로 간단하게 표현하기 위한 표현식이다
- 함수형 인터페이스가 갖는 유일한 추상 메서드를 람다식으로 표현 함으로써 익명 함수를 만들 수 있다.
- 원래라면 익명 클래스 혹은 구현 클래스를 작성해 메서드를 오버라이딩하는 방식으로 구현해야 한다.
- 그러나 함수형 인터페이스라면, 위의 과정을 람다 표현식으로 간단하게 구현할 수 있다.
코드로 짜보기
- 백번 읽어보는 것보다 직접 짜보면서 체득하는 것이 가장 좋다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Main {
public static void main(String[] args) {
String strInput = "Hello I'm Bogeun.";
Integer intInput = 10;
// 제네릭을 사용해서 인풋값과 리턴값의 타입이 유동적이다.
String strResult = doSomething(x -> x.substring(0, 5), strInput); // Hello
String[] strArrResult = doSomething(x -> x.split("\\s"), strInput); // [Hello, I'm, Bogeun.]
Integer intResult = doSomething(x -> x + 10, intInput); // 20
}
// T : 인풋 타입 , R : 리턴 타입
// Function : 인풋을 하나 받아, 결과를 리턴하는 함수
// doSomething은 Function 함수와 인풋을 받아 해당 결과를 리턴하는 단순한 메서드이다.
private static <T, R> R doSomething(Function<T, R> function, T input) {
return function.apply(input);
}
}
메서드 레퍼런스
- 위에서 봤듯이 매우 간결하게 코드를 작성할 수 있는 람다 표현식이 있지만,
- 이보다 더 간결하게 표현할 수 있는 방법이 메서드 레퍼런스이다.
- 다음과 같이 세 가지로 분류할 수 있는데
- 정적 메서드 레퍼런스 (static method reference)
- 클래스의 static 메서드를 참조하는 방식
- 인스턴스 메서드 레퍼런스 (instance method reference)
- 인스턴스의 메서드를 참조하는 방식
- 생성자 메서드 레퍼런스 (constructor method reference)
- 클래스의 생성자 메서드를 참조하는 방식
- 정적 메서드 레퍼런스 (static method reference)
코드로 짜보기
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
37
class Person {
private String name;
private Integer age;
// constructor..
// toString..
public void sayHello() {
sout(String.format("안녕! 나는 %s야!", this.name));
}
}
class Main {
public static void main(String[] args) {
List<Person> list = List.of(new Person("park", 20),
new Person("gill", 29),
new Person("koh", 28));
// 정적 메서드 레퍼런스
list.forEach(x -> System.out.println(x)); // 람다 표현식
list.forEach(System.out::println); // 메서드 레퍼런스
// 인스턴스 메서드 레퍼런스
list.forEach(x -> x.sayHello()); // 람다 표현식
list.forEach(Person::sayHello); // 메서드 레퍼런스
// 생성사 메서드 레퍼런스
Supplier<Person> personSupplierA = () -> new Person(); // 람다 표현식
Supplier<Person> personSupplierB = Person::new; // 메서드 레퍼런스
Person personA = personSupplierA.get();
Person personB = personSupplierB.get();
}
}
Optional
- Optional은 null을 다루기 위한 클래스이다.
- 자바의 참조형 타입 변수들은 모두 null 값을 가질 수 있다.
- null이란, 참조형 변수가 아무 객체도 가르키고 있지 않는 상태이다.
- Optional은 null 값을 다루면서 NPE가 발생할 수 있는 상황들에 대처하기 위한 클래스이다.
- NPE란 NullPointerException 의 줄임말로
- 예상치 못한 곳에서 null 값을 맞닥뜨렸을 때 발생하는 RuntimeException이다.
- 주로 메서드의 반환값에 사용되며,
- 해당 메서드의 리턴값이 null일 수도 있을 때,
- 해당 메서드를 사용하는 측에서 대처할 수 있도록 Optional로 값을 한 번 감싸서 주는 형태로 많이 사용한다.
- 감싼다는 의미는 리턴 타입을 제네릭을 사용해 Optional<T> 이런 식으로 변환해서 준다는 뜻이다.
Optional 만들기
- 옵셔널의 스태틱 메서드를 이용해 옵셔널 객체를 만들 수 있다
- Optional.of(T value);
- 특정 타입의 객체를 Optional로 감싼다.
- 파라미터가 null이면 안됨.
- Optional.ofNullable(T value);
- 특정 타입의 객체를 Optional로 감싼다.
- 파라티머가 null이어도 됨.
- Optional.empty();
- 빈 옵셔널 객체 반환
- Optional.ofNullable(null); == Optional.empty();
- Optional.of(T value);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Main {
public static void main(String[] args) {
Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.of(null); // NullPointerException 발생!!
Optional<Integer> opt3 = Optional.ofNullable(1);
Optional<Integer> opt4 = Optional.ofNullable(null); // NPE 안뜸
Optional<Object> opt5 = Optional.empty(); // 둘 다 빈 옵셔널이지만,
Optional<Integer> opt6 = Optional.empty(); // 제네릭으로 선언된 타입이 다름.
}
}
Optional 사용하기
- 옵셔널 객체가 감싸고 있는 객체를 꺼내기 위한 방법들은 다음의 것들이 있다.
- get();
- 걍 안에 있는 거 꺼내서 주세요! 하는 것
- 만약 안에 값이 null이면 NoSuchElementException 발생
- 그래서 안에 있는지 없는지 확인하고 쓰던가
- try/catch로 익셉션이 발생할 걸 대비하고 사용해야 한다
- orElse(T val);
- 값이 있나요??
- 있으면, 주세요!
- 없으면, 파라미터로 넘긴 대체 객체(val)를 주세요!
- 값이 있나요??
- orElseGet(Supplier sup)
- 값이 있나요??
- 있으면, 주세요!
- 없으면, Supplier 메서드로 생성된 값을 주세요!
- 값이 있나요??
- orElseThrow(Supplier sup)
- 값이 있나요??
- 있으면, 주세요!
- 없으면, Supplier 메서드로 정의된 예외(Exception)을 던져 주세요!
- 값이 있나요??
- get();
- 옵셔널 객체를 확인하는 메서드들은 다음과 같은 것들이 있다.
- isPresent();
- 값이 있나요?
- 있으면, true
- 없으면, false
- 값이 있나요?
- isEmpty();
- 비어 있나요?
- 값이 없으면, true
- 값이 있으면, false
- 비어 있나요?
- ifPresent(Consumer con);
- 값이 있나요?
- 값이 있으면, 지정된 Consumer 메서드 실행
- 값이 없으면, 무시
- 값이 있나요?
- isPresent();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Main {
public static void main(String[] args) {
Optional<Object> opt = getFromAnnoymous(); // 익명으로부터 값을 받아왔다. 얘가 null일지 뭔지 모름..
// null이면 NPE가 뜰 수 있다 == try/catch로 지저분하게 잡아줘야 함;
// 근데 다행히도 Optional로 감싸서 와서 대처가 깔끔하게 가능하다.
Object o1 = opt.get(); // 그냥 막무가내로 꺼내기, 대신 null이면 예외 뜸
Object o2 = opt.orElse(new Object()); // 있으면 받고, 없으면 new Object()로 대체하여 받기
Object o3 = opt.orElseGet(() -> new Object()); // 있으면 받고, 없으면 Supplier 메서드의 결과로 넘긴 객체 받기
Object o4 = opt.orElseThrow(() -> new NoResultException()); // 있으면 받고, 없으면 Supplier 메서드의 넘긴 익셉션 throw
sout(opt.isPresent()); // 있으면 true, 없으면 false
sout(opt.isEmpty()); // 있으면 false, 없으면 true
opt.ifPresent(x -> {
sout(opt); // 있으면 실행됨, 없으면 무시
});
}
}
Stream
- 스트림은 데이터의 처리를 위한 추상화 시퀀스이다.
- 데이터 소스를 추상화 해서 일괄적으로 다루는 기능들을 제공한다.
- 스트림은 다음과 같은 특징들이 있다.
- 게으른 연산(Lazy Evaluation)
- 실제 데이터의 처리가 필요한 시점에 연산을 수행한다.
- 이를 통해 성능의 이점을 가질 수 있다.
- 원본 데이터 유지
- 실제 값을 변경하거나 다루는 것이 아닌, 값을 복사하여 수행한다.
- 그러나 참조형 타입인 경우 값이 변경될 수 있으니 주의하여야 한다.
- 함수형 프로그래밍 지원
- 병렬 처리 지원
- 게으른 연산(Lazy Evaluation)
- 스트림의 메서드는 두 가지로 분류할 수 있다.
- 중계 오퍼레이터
- 끝맺음이 아닌 중간 연산을 하는 메서드들이다.
- 스트림 객체를 리턴하여 메서드 체이닝을 한다.
- 게으른 처리라는 스트림의 특성상 종료 오퍼레이터가 실행되어야 중계 오퍼레이터의 실제로 처리가 이뤄진다.
- 종료 오퍼레이터
- 최종적으로 결과를 생성하거나 어떤 처리를 하는 메서드들이다.
- 종료 오퍼레이터가 실행되어야 앞에 쌓인 중계 오퍼레이터들이 처리된다.
- 중계 오퍼레이터
- 주요 스트림 메서드들은 다음과 같다.
- 중계 오퍼레이터
- distinct()
- equals()로 판단하여, 중복 요소들을 제거한다.
- filter(Predicate)
- Predicate 조건에서 false인 애들을 거른다. (true인 애들만 통과)
- sorted(Comparator)
- Comparator에 따라 요소들을 정렬한다.
- map(Function)
- 요소들의 타입을 변경한다.
- flatMap(Function)
- 요소들을 스트림으로 만들어 평면화한다.
- 평면화는 중첩된 구조를 펼쳐 단일 레벨로 만드는 것이다.
- peek(Consumer)
- 요소들을 가지고 Consumer에 정의된 행동을 수행한다.
- forEach랑 비슷한데, 얘는 중계 오퍼레이터라서 Stream을 반환한다.
- limit(long)
- 요소 시퀀스의 최대 개수를 한정한다.
- skip(long)
- 요소 시퀀스의 맨 처음 몇 개를 스킵한다.
- distinct()
- 종료 오퍼레이터
- count()
- 요소들의 개수를 반환한다.
- reduce()
- 모든 요소들을 하나의 값으로 축소하여 반환한다.
- anyMatch(Predicate)
- 요소들 중 하나라도 조건에 대해 일치하는가 (Predicate에 true인가)
- allMatch(Predicate)
- 모든 요소들이 조건에 대해 일치하는가 (Predicate에 true인가)
- nonMatch(Predicate)
- 모든 요소들이 조건에 대해 불일치하는가 (Predicate에 false인가)
- min(Comparator)
- 요소들 중 가장 최솟값 (Comparator 기준)
- max(Comparator)
- 요소들 중 가장 최댓값 (Comparator 기준)
- forEach(Consumer)
- 요소들을 사용해 순차적으로 Consumer 수행
- collect(Collector)
- 요소들을 하나의 컬렉션으로 묶어서 반환
- count()
- 중계 오퍼레이터
- 설명만 들으면 이해가 어려울 수 있으니, 계속 사용해보며 체득하는게 가장 좋은 방법인 것 같다.
코드로 짜보기 - 중계 오퍼레이터
- 가장 간단한 중계 오퍼레이터들부터 사용해보자.
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
class Main {
public static void main(String[] args) {
List<Integer> list = List.of(1, 2, 3, 3, 3, 3, 2, 1); // 중복된 요소가 많음
list.stream() // 해당 리스트를 Stream으로 만드는 메서드.
.distinct() // 중복 제거
.forEach(System.out::println); // 1, 2, 3
list.stream()
.filter(x -> x > 2) // 2보다 큰 애들만 통과
.forEash(System.out::println); // 3, 3, 3, 3 (위에서 중복을 제거한 것 같지만, 실제 list의 요소들을 건드리지 않은 걸 알 수 있다.)
list.stream()
.sorted((o1, o2) -> o1.compareTo(o2)) // Comparator를 사용하여 비교한 순서대로 정렬
.forEach(System.out::println); // 1, 1, 2, 2, 3, 3, 3, 3
list.stream()
.peek(System.out::println) // 1, 2, 3, 3, 3, 3, 2, 1
.forEach(System.out::println); // 1, 2, 3, 3, 3, 3, 2, 1
// 둘이 똑같은데 차이가 머징? => peek()은 중계 오퍼레이터 forEach()는 종료 오퍼레이터임
list.stream()
.limit(6) // 앞에서부터 6개만 통과 시킴
.peek(System.out::println) // 1, 2, 3, 3, 3, 3
.skip(2) // 앞에서부터 2개 날림
.forEach(System.out::println); // 3, 3, 3, 3
}
}
- 데이터의 형태를 바꾸는 중계 오퍼레이터들도 사용해보자
- map(Function)
- flatMap(Function)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Main {
public static void main(String[] args) {
List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = List.of(4, 5, 6);
List<Integer> list3 = List.of(7, 8, 9);
List<List<Integer>> list = List.of(list1, list2, list3); // 리스트의 리스트
List<String> stringList = list1.stream()
.map(String::valueOf) // 정수형이던 요소들을 String.valueOf() 메서드로 String 타입으로 변환
.collect(Collectors.toList()); // Collectors를 사용해 리스트로 요소 묶기
List<Integer> totalList = list.stream()
.flatMap(x -> x.stream()) // 각각의 리스트들을 Stream으로 변환하여 길다란 하나의 Stream으로 합침
.collect(Collectors.toList()); // 평면화의 결과로 리스트의 리스트였던 애들이 하나의 리스트로 합쳐진 것을 확인할 수 있음
}
}
코드로 짜보기 - 종료 오퍼레이터
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
37
38
39
40
41
class Main {
public static void main(String[] args) {
List<String> list = List.of("1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
Integer count = list.stream()
.count(); // 10
String sumStrings = list.stream() // 첫 번째 param + 모든 요소들의 합
.reduce("The sum of strings is ", (o1, o2) -> o1 + o2); // The sum of strings is 12345678910
boolean anyMatch = list.stream() // 요소 중에 하나라도 x.equals("1")이 true인게 있니?
.anyMatch(x -> x.equals("1")); // true
boolean allMatch = list.stream() // 모든 요소들이 null이 아니니?
.allMatch(x -> !x.equals(null)); // true
boolean noneMatch = list.stream() // 모든 요소들이 "babo"가 아니니?
.noneMatch(x -> x.equals("babo")); // true
Optional<String> min = list.stream() // Optional 객체로 감싸서 반환함
.min((o1, o2) -> o1.compareTo(o2)); // "1"
Optional<String> max = list.stream()
.max((o1, o2) -> o1.compareTo(o2)); // "9"
// 아니 이거 왜 10이 아니라 9인가요
// 문자열 비교는 사전 순서상의 비교를 하기 때문임
Optional<String> integerMax1 = list.stream()
.max((o1, o2) -> Integer.compare(Integer.parseInt(o1), Integer.parseInt(o2))); // "10"
// 이렇게 숫자로 변환해서 비교하면 10이 나옴
list.stream()
.forEach(x -> sout(x)); // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
List<String> newList = list.stream()
.collect(Collectors.toList()); // Collectors를 사용해 요소들을 묶는 컬렉션을 반환한다.
// 여기서는 똑같이 리스트로 반환했지만, List 외에도 Set, Map 등 다 가능하다.
}
}
CompletableFuture
- 비동기적인 연산을 수행하고 동작을 다루기 위한 클래스이다.
- 다음과 같은 특징을 가진다.
- 비동기 처리
- 비동기 결과를 기다리는 스레드와 메인 스레드가 나뉜다.
- 메인 스레드는 할 일을 하고, 다른 스레드는 비동기 결과가 끝나면 나머지 처리를 한다.
- 콜백 메서드
- 비동기 결과가 나오면 콜백 메서드를 통해 결과를 가공하거나, 특정 작업을 수행한다.
- thenApply(), thenAccept(), thenRun() 등
- 조합 메서드
- 여러 개의 CompletableFuture를 엮어서 좀 더 다양한 동작을 수행하도록 할 수도 있다.
- thenCompose(), thenCombine(), allOf(), anyOf() 등
- 타임아웃
- 타임아웃 관련 메서드를 통해 작업이 일정 시간 내에 끝나지 않을 경우 예외나 대체값을 제공하도록 동작을 설정할 수 있다.
- completeOnTimeout(), orTimeout() 등
- 비동기 처리
과제
- 영화 리뷰 프로그램을 완성하시오.
This post is licensed under CC BY 4.0 by the author.