Post

기강 자바-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 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()



코드로 짜보기

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)
      • 클래스의 생성자 메서드를 참조하는 방식



코드로 짜보기

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();



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)을 던져 주세요!


  • 옵셔널 객체를 확인하는 메서드들은 다음과 같은 것들이 있다.
    • isPresent();
      • 값이 있나요?
        • 있으면, true
        • 없으면, false
    • isEmpty();
      • 비어 있나요?
        • 값이 없으면, true
        • 값이 있으면, false
    • ifPresent(Consumer con);
      • 값이 있나요?
        • 값이 있으면, 지정된 Consumer 메서드 실행
        • 값이 없으면, 무시



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)
      • 실제 데이터의 처리가 필요한 시점에 연산을 수행한다.
      • 이를 통해 성능의 이점을 가질 수 있다.
    • 원본 데이터 유지
      • 실제 값을 변경하거나 다루는 것이 아닌, 값을 복사하여 수행한다.
      • 그러나 참조형 타입인 경우 값이 변경될 수 있으니 주의하여야 한다.
    • 함수형 프로그래밍 지원
    • 병렬 처리 지원


  • 스트림의 메서드는 두 가지로 분류할 수 있다.
    • 중계 오퍼레이터
      • 끝맺음이 아닌 중간 연산을 하는 메서드들이다.
      • 스트림 객체를 리턴하여 메서드 체이닝을 한다.
      • 게으른 처리라는 스트림의 특성상 종료 오퍼레이터가 실행되어야 중계 오퍼레이터의 실제로 처리가 이뤄진다.
    • 종료 오퍼레이터
      • 최종적으로 결과를 생성하거나 어떤 처리를 하는 메서드들이다.
      • 종료 오퍼레이터가 실행되어야 앞에 쌓인 중계 오퍼레이터들이 처리된다.
  • 주요 스트림 메서드들은 다음과 같다.
    • 중계 오퍼레이터
      • distinct()
        • equals()로 판단하여, 중복 요소들을 제거한다.
      • filter(Predicate)
        • Predicate 조건에서 false인 애들을 거른다. (true인 애들만 통과)
      • sorted(Comparator)
        • Comparator에 따라 요소들을 정렬한다.
      • map(Function)
        • 요소들의 타입을 변경한다.
      • flatMap(Function)
        • 요소들을 스트림으로 만들어 평면화한다.
        • 평면화는 중첩된 구조를 펼쳐 단일 레벨로 만드는 것이다.
      • peek(Consumer)
        • 요소들을 가지고 Consumer에 정의된 행동을 수행한다.
        • forEach랑 비슷한데, 얘는 중계 오퍼레이터라서 Stream을 반환한다.
      • limit(long)
        • 요소 시퀀스의 최대 개수를 한정한다.
      • skip(long)
        • 요소 시퀀스의 맨 처음 몇 개를 스킵한다.
    • 종료 오퍼레이터
      • 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)
        • 요소들을 하나의 컬렉션으로 묶어서 반환


  • 설명만 들으면 이해가 어려울 수 있으니, 계속 사용해보며 체득하는게 가장 좋은 방법인 것 같다.



코드로 짜보기 - 중계 오퍼레이터

  • 가장 간단한 중계 오퍼레이터들부터 사용해보자.
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.