Chapter 14. 람다와 스트림_2(스트림)

|

2. 스트림(stream)

2.1 스트림이란?

  • 스트림(Stream): 데이터 소스를 추상하하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의
    • 데이터 소스를 추상화하였다는 것은 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미
  • 스트림을 이용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있음

  • 예시

    • 문자열 배열과 같은 내용의 문자열을 저장하는 List가 있을 때

      String[] strArr = {"aaa", "ddd", "ccc"};
      List<String> strLisst = Arrays.asList(strArr);
      
    • 두 데이터 소스를 기반으로 하는 스트림은

      Stream<String> strStream1 = strList.stream();
      Stream<String> strStream2 = Arrays.stream(strArr);
      
    • 두 스트림으로 데이터소스의 데이터를 읽어 정렬하고 화면에 출력하는 방법

      // 기존 방법
      Arrays.sort(strArr);
      Collections.sort(strList);
          
      for (String str : strArr)
          System.out.println(str);
      for (String str : strList)
          System.out.println(str);
          
      // 스트림 사용
      strStream1.sorted().forEach(Ststem.out::println);
      strStream2.sorted().forEach(Ststem.out::println);
      

스트림은 데이터 소스를 변경하지 않음

  • 스트림은 데이터 소스로부터 데이터를 읽기만할 뿐, 데이터 소스를 변경하지 않음

  • 정렬된 결과를 컬렉션이나 배열에 담아 반환할 수도 있음

    // 정렬된 결과를 샐로운 List에 담아서 반환
    List<String> sortedList = strStream2.sorted().collect(Collectors.toLost());
    

스트림은 일회용

  • Iterator처럼 일회용. Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없음
  • 필요하다면 스트림을 다시 생성해야 함

스트림은 작업을 내부 반복으로 처리

  • 내부 반복은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미
  • forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용

스트림의 연산

  • 스트림이 제공하는 다양한 연산을 이용해 복잡한 작업들을 간단히 처리할 수 있음

    • 연산(operation): 스트림에 정의된 메서드 중에서 데이터 소스를 다루는 작업을 수행하는 것
  • 스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류

    중간 연산 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음

    최종 연산 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능

지연된 연산

  • 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 것
  • 스트림에 대해 distinct()sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되는 것은 아님
    • 중간 연산을 호출하는 것은 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐
    • 최종 연산이 수행되어야 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모됨

**Stream와 IntStream**

  • 요소의 타입이 T인 스트림은 기본적으로 Steam이지만, 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림을 제공
    • IntStream, LongStream, DoubleStream이 제공됨
  • Stream 대신 IntStream을 사용하는 것이 더 효율적. IntStream은 int 타입의 값으로 작업하는데 유용한 메서드들이 포함되어 있음

병렬 스트림

  • 스트림으로 데이터를 다룰 경우 병렬 처리가 쉬움

  • 병렬 스트림은 내부적으로 fork&join 프레임웍을 이용해 자동적으로 연산을 병렬로 수행

  • 스트림에 parallel()이라는 메서드를 호출해 병렬로 연산을 수행하도록 지시

    • 병렬로 처리되지 않게 하려면 sequential()을 호출하면 됨
    • 모든 스트림은 기본적으로 병렬 스트림이 아니므로 sequential() 을 호출할 필요가 없음

    • sequential()parallel()을 호출한 것을 취소할 때만 사용

2.2 스트림 만들기

컬렉션

  • 컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있음

    • Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림 생성 가능
  • stream()은 해당 컬렉션을 소스(source)로 하는 스트림을 반환

    Stream<T> Collection.stream()
    
  • List로부터 스트림을 생성하는 코드

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);	// 가변인자
    Stream<Integer> intstream = list.stream();	// list를 소스로 하는 컬렉션 생성
    
  • forEach(): 지정된 작업을 스트림의 모든 요소에 대해 수행

    intStream.forEach(System.out::println);	// 스트림의 모든 요소를 출력
    intStream.forEach(System.out::println);	// 에러. 스트림이 이미 닫힘
    
    • forEach()는 스트림의 요소를 소모하면서 작업을 수행하므로 같은 스트림에 forEach()를 두 번 호출할 수 없음
    • 스트림의 요소를 한번 더 출력하려면 스트림을 새로 생성해야 함
      • forEach()에 의해 스트림의 요소가 소모되는 것이지, 소스의 요소가 소모되는 것은 아니기 때문에 같은 소스로부터 다시 스트림을 생성할 수 있음

배열

  • 배열을 소스로 스트림을 생성하는 메서드는 Stream과 Arrays에 static 메서드로 정의되어 있음

    Stream<T> Stream.of(T... values)	// 가변인자
    Stream<T> Stream.of(T[])
    Stream<T> Arrays.stream(T[])
    Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)
    
  • int, long, double과 같은 기본형 배열을 소스로 하는 스트림을 생성하는 메서드

    IntStream IntStream.of(int... values)	// Stream이 아니라 IntStream
    IntStream IntStream.of(int[])
    IntStream Arrays.stream(int[])
    IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive)
    

특정 범위의 정수

  • IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해 반환하는 range()rangeClosed()를 가짐

    IntStream	IntStream.range(int begin, int end)
    IntStream	IntStream.rangeClosed(int begin, int end)
    
    • range()의 경우 경계의 끝인 end가 범위에 포함되지 않고, rangeClosed()의 경우 포함됨
    • int보다 큰 범위의 스트림을 생성하려면 LongStream에 있는 동일한 이름의 메서드를 사용하면 됨

임의의 수

  • 난수를 생성하는데 사용하는 Random 클래스에 포함된 인스턴스 메서드

    IntStream		ints()
    LongStream		longs()
    DoubleStream	doubles()
    
    • 해당 타입의 난수들로 이루어진 스트림을 반환

    • 반환하는 스트림은 크기가 정해지지 않은 ‘무한 스트림(infinite stream)’이므로 limit()도 함께 사용해 스트림의 크기를 제한해주어야 함

    • limit()은 스트림의 개수를 지정하는데 사용되며, 무한 스트림을 유한 스트림으로 만들어 줌

      IntStream	intStream = new Random().ints();	// 무한 스트림
      intStream.limit(5).forEach(System.out::println)	// 5개의 요소만 출력
      
    • 매개변수로 스트림의 크기를 지정해 유한 스트림을 생성해 반환할수도 있음

    • 매개변수로 범위를 지정해줄 수 있음. 단 end는 범위에 포함되지 않음

람다식 - iterate(), generate()

  • Stream 클래스이 iterate()generate()는 람다식을 매개변수로 받아, 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성

    static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
    static <T> Stream<T> generate(Supplier<T> s)
    
    • iterate()는 seed로 지정된 값부터 시작해, 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복
    • generate()는 람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해 반환하지만, 이전 결과를 이용해 다음 요소를 계산하지 않음
  • generate()에 정의된 매개변수 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용

파일

  • java.nio.file.Files는 파일을 다루는데 필요한 유용한 메서드들을 제공

  • list()는 지정된 디렉터리에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 반환

    Stream<Path>	Files.list(Path dir)
    
  • 파일의 한 행을 요소로 하는 스트림을 생성하는 메서드

    Stream<String> Files.lines(Path path)
    Stream<String> Files.lines(Path path, Charset cs)
    Stream<String> lines()	// BufferedReader 클래스의 메서드
    

빈 스트림

  • 요소가 하나도 없는 비어있는 스트림 생성 가능

  • 스트림에 연산을 수행한 결과가 하나도 없을 때, null보다는 빈 스트림을 반환하는 것이 나음

    Stream emptyStream = Stream.empty();	// empty()는 빈 스트림을 생성해 반환
    long count = emptyStream.count();	// count 값은 0
    
    • count(): 스트림 요소의 개수를 반환

두 스트림의 연결

  • Stream의 static 메서드인 concat()을 사용하면, 두 스트림을 하나로 연결할 수 있음

    • 연결하려는 두 스트림의 요소는 같은 타입이어야 함
    String[] str1 = {"123", "456"};
    String[] str2 = {"AAA", "BBB"};
      
    Stream<String> strs1 =  Stream.of(str1);
    Stream<String> strs2 =  Stream.of(str2);
    Stream<String> str3 = Stream.concat(strs1, strs2);	// 두 스트림을 하나로 연결
    

2.3 스트림의 중간연산

스트림 자르기 - skip(), limit()

Stream<T> skip(long n)
Stream<T> limit(long maxSize)
  • skip()은 처음 n개의 요소를 건너뛰고, limit()는 스트림의 요소를 5개로 제한

    IntStream intStream = IntStream.rangeClosed(1, 10);	// 1~10의 요소를 가진 스트림
    intStream.skip(3).limit(5).forEach(System.out::println);	// 45678
    

스트림의 요소 걸러내기 - filter(), distinct()

  • distinct()는 스트림에서 중복되는 요소들을 제거하고, filter()는 주어진 조건(Predicate)에 맞지 않는 요소를 걸러냄
  • filter()는 매개변수로 Predicate를 필요로 하는데, 연산결과가 boolean인 람다식을 사용해도 됨
    • 필요하다면 filter()을 다른 조건으로 여러 번 사용할 수도 있음

정렬 - sorted()

Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
  • 지정된 Comparator로 스트림을 정렬하는데, Comparator 대신 int 값을 반환하는 람다식을 사용하는 것도 가능
    • Comparator을 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)로 정렬
    • 단, 스트림의 요소가 Comparable을 구현한 클래스가 아니면 예외 발생
  • Comparator인터페이스의 static 메서드

변환 - map()

Stream<R> map(Function<? super T, ? extends R> mapper)
  • 스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 경우 사용
  • 매개변수로 T타입을 R타입으로 변환해서 반환하는 함수를 지정해야 함
  • map()은 중간연산이므로, 연산결과는 String을 요소로하는 스트림
  • map()도 하나의 스트림에 여러 번 적용 가능

조회 - peek()

  • 연산과 연산 사이에 올바르게 처리되었는지 확인할 때 사용
  • forEach()와 다르게 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 끼워 넣어도 괜찮음
  • filter()나 map()의 결과를 확인할 때 유용하게 사용됨

mapToInt(), mapToLong(), mapToDouble()

  • map()은 연산의 결과로 Stream타입의 스트림을 반환하는데, 스트림의 요소를 숫자로 변환하는 경우 IntStream과 같은 기본형 스트림으로 변환하는 것이 더 유용함

  • mapToInt()를 사용해 Stream가 아닌 IntStream 타입의 스트림을 생성 가능

  • count()만 지원하는 Stream와 달리 기본형 스트림은 숫자를 다루는데 편리한 메서드들을 제공

    int				sum()		// 스트림의 모든 요소의 총합
    OptionalDouble	average()	// sum() / (double) count()
    OptionalInt		max()		// 스트림의 요소 중 제일 큰 값
    OptionalInt		min()		// 스트림의 요소 중 제일 작은 값
    
    • 이 메서드들은 최종연산이므로 호출 후에 스트림이 닫힘
    • sum()과 average()를 모두 호출해야할 때, 스트림을 또 생성해야하는 불편함을 없애기 위해 summaryStatistics()라는 메서드를 제공

2.4 Optional와 OptionalInt

  • Optional은 지네릭 클래스로 'T타입의 객체'를 감싸주는 래퍼 클래스
    • Optional 타입의 객체에는 모든 타입의 참조변수를 담을 수 있음
  • 최종 연산 결과를 Optional 객체에 담아서 반환
    • 객체에 담아서 반환하면, 반환된 결과가 null인지 if문으로 체크하는 대신 Optional 에 정의된 메서드를 통해 간단히 처리할 수 있음

Optional 객체 생성하기

  • of() 또는 ofNullable()을 사용
  • of()는 매개변수의 값이 null이면 NullPointerException이 발생
  • 참조변수의 값이 null일 가능성이 있으면, of()대신 ofNullable() 사용
  • Optinal 타입의 참조변수를 기본값으로 초기화할 때는 empty()를 사용

Optional 객체의 값 가져오기

  • 값을 가져올 때는 get()을 사용
  • 값이 null일 경우 NoSuchElementException이 발생하며, 이를 대비해 orElse()로 대체 값을 지정할 수 있음
  • orElse()의 변형으로 null 값을 대체할 값을 반환하는 람다식을 지정할 수 있는 orElseGet()과 null일 때 지정된 예외를 발생시키는 orElseThrow()가 있음
  • Optional 객체에도 filter(), map(), flatmap() 사용 가능
    • 만약 Optional 객체 값이 null인 경우 이 메서드들은 아무 일도 하지 않음
  • isPresent()는 Optional 객체의 값이 null이면 false를, 아니면 true를 반환
  • ifPresent(Consumer<T> block)은 값이 있으면 주어진 람다식을 실행하고, 없으면 아무 일도 하지 않음
    • Optional를 반환하는 findAny()나 findFirst()와 같은 최종 연산과 잘 어울림

OptionalInt, OptionalLong, OptionalDouble

  • 반환 타입이 Optional가 아닌 것을 제외하면 Stream에 정의된 것과 비슷

2.5 스트림의 최종 연산

  • 최종 연산은 스트림의 요소를 소모해서 결과를 만들어내기 때문에 최종 연산 후에는 스트림이 닫히게 되고 더 이상 사용할 수 없음
  • 최종 연산의 결과는 단일 값이거나, 스트림의 요소가 담긴 배열 또는 컬렉션일 수 있음

forEach()

  • 반환 타입이 void이므로 스트림의 요소를 출력하는 용도로 많이 사용

조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()

  • 스트림의 요소에 대해 지정된 조건에 모든 요소가 일치하는지, 일부가 일치하는지 아니면 어떤 요소도 일치하지 않는지 확인하는데 사용하는 메서드
  • 메서드들은 모두 매개변수로 Predicate를 요구하며, 연산결과로 boolean을 반환
  • findFirst()는 스트림의 요소 중에서 조건에 일치하는 첫 번째 것을 반환
    • 주로 filter()와 함께 사용되어 조건에 맞는 스트림의 요소가 있는지 확인하는데 사용됨
    • 병렬 스트림인 경우 findFirst() 대신 findAny()를 사용
    • findAny()와 findFirst()의 반환 타입은 Optional이며, 스트림의 요소가 없을 때는 비어있는 Optional 객체를 반환

통계 - count(), sum(), average(), max(), min()

  • 기본형 스트림의 요소들에 대한 통계 정보를 얻을 수 있는 메서드

  • 기본형 스트림이 아닌 경우 통계와 관련된 메서드는 아래 3개뿐

    long		count()
    Optional<T>	max(Comparator<? super T> comparator)
    Optional<T>	min(Comparator<? super T> comparator)
    

리듀싱 - reduce()

  • 스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환하기 때문에 매개변수의 타입이 BinaryOperator인 것
  • 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산
    • 이 과정에서 스트림의 요소를 하나씩 소모하게 되고, 스트림의 모든 요소를 소모하게 되면 그 결과를 반환
  • 연산결과의 초기값(indentity)을 갖는 reduce()도 존재
    • 초기값과 스트림의 첫 번째 요소로 연산을 시작
    • 스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로, 반환 타입이 Optional가 아니라 T
  • reduce()를 사용하기 위해 초기값(identity)과 어떤 연산(BinaryOperator)으로 스트림의 요소를 줄여나갈 것이지만 결정하면 됨

2.6 collect()

  • collect()는 스트림의 요소를 수집하는 최종 연산으로 reducing()과 유사
  • collect()가 스트림의 요소를 수집하기 위해서는 어떻게 수집할 것인가에 대한 방법이 정의되어 있어야 하는데, 이 방법을 정의한 것이 컬렉터(collector)
  • 컬렉터는 Collector 인터페이스를 구현한 것. 직접 구현할 수도 있고 미리 작성된 것을 사용할 수도 있음
    • Collectors 클래스는 미리 작성된 다양한 종류의 컬렉터를 반환하는 static 메서드를 가지고 있음

collect() 스트림의 최종연산, 매개변수로 컬렉터를 필요로 함

Collector 인터페이스, 컬렉터는 이 인터페이스를 구현해야 함

Collectors 클래스, static 메서드로 미리 작성된 컬렉터를 제공

  • collect()는 매개변수의 타입이 Collector
    • 매개변수가 Collector를 구현한 클래스의 객체이어야 한다는 의미
    • collect()는 이 객체에 구현된 방법대로 스트림의 요소를 수집

스트림을 컬렉션과 배열로 변환

  • 스트림의 모든 요소를 컬렉션에 수집하려면 Collectors 클래스의 toList()와 같은 메서드를 사용하면 됨
  • List나 Set이 아닌 특정 컬렉션을 지정하려면, toCollection()에 해당 컬렉션의 생성자 참조를 매개변수로 넣어주면 됨
  • Map은 키와 값의 쌍으로 저장해야하므로 객체의 어떤 필드를 키로 사용할지와 값으로 사용할지를 지정해줘야 함
  • 스트림에 저장된 요소들을 ‘T[]’ 타입의 배열로 변환하려면, toArray()를 사용하면 됨
    • 단, 해당 타입의 생성자 참조를 매개변수로 지정해줘야 함
    • 만일 매개변수를 지정하지 않으면 반환되는 타입은 ‘Object[]’

문자열 결합 - joining()

  • 문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환
    • 구분자와 접두사, 접미사 지정 가능
  • 스트림의 요소가 String이나 StringBuffer처럼 CharSequence의 자손인 경우에만 결합이 가능
    • 스트림의 요소가 문자열이 아닌 경우 먼저 map()을 이용해서 스트림의 요소를 문자열로 변환해야 함
    • map()없이 스트림에 바로 joining()하면, 스트림의 요소에 toString()을 호출한 결과를 결합

그룹화와 분할 - groupingBy(), partitioningBy()

  • 그룹화: 스트림의 요소를 특정 기준으로 그룹화하는 것을 의미
  • 분할: 스트림의 요소를 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로의 분할을 의미

  • groupingBy()는 스트림의 요소를 Function으로, partitioningBy()는 Predicate로 분류
  • 그룹화와 분할의 결과는 Map에 담겨 반환됨

2.7 Collector 구현하기

  • 컬렉터를 작성한다는 것은 Collector 인터페이스를 구현한다는 것을 의미

  • Collector 인터페이스 정의

    public interface Collector<T, A, R> {
        Supplier<A>			supplier();
        Biconsumer<A, t>	accumulator();
        BinaryOperator<A>	combiner();
        Fynction<A, R>		finisher();
          
        Set<Characteristics>	characteristics();	// 컬렉터의 특성이 담긴 Set을 반환
        	...
    }
    
    • 직접 구현해야하는 것은 위의 5개 메서드
    • chracteristics()를 제외하면 모두 반환 타입이 함수형 인터페이스. 즉, 4개의 람다식을 작성하면 됨

supplier() 작업 결과를 저장할 공간을 제공

accumulator() 스트림의 요소를 수집(collect)할 방법을 제공

combiner() 두 저장공간을 병합할 방법을 제공 (병렬 스트림)

finisher() 결과를 최종적으로 변환할 방법을 제공

  • supplier()는 수집 결과를 저장할 공간을 제공하기 위한 것
  • accumulatoer()는 스트림의 요소를 어떻게 supplier()가 제공한 공간에 누적할 것인지를 정의
  • combiner()는 병렬 스트림인 경우, 여러 쓰레드에 의해 처리된 결과를 어떻게 합칠 것인가를 정의
  • finisher()는 작업결과를 변환하는 일을 하는데 변환이 필요없다면, 항등 함수인 Function.identity()를 반환하면 됨

Characteristics.CONCURRENT 병렬로 처리할 수 있는 작업

Characteristics.UNORDERED 스트림의 요소의 순서가 유지될 필요가 없는 작업

Characteristics.IDENTITY_FINISH finisher()가 항등 함수인 작업

  • characteristtics()는 컬렉터가 수행하는 작업의 속성에 대한 정보를 제공하기 위한 것
  • 3가지 속성 중 해당하는 것을 Set에 담아 반환하도록 구현
    • 아무런 속성도 지정하고 싶지 않다면 비어있는 Set을 반환

Chapter 14 끝!!!

Chapter 15. 입출력

|

1. 자바에서의 입출력

1.1 입출력이란?

  • I/O란 Input과 Output의 약자로 입력과 출력, 간단히 줄여 입출력이라고 함
  • 입출력: 컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터를 주고받는 것을 의미

1.2 스트림(stream)

  • 자바에서 어느 한쪽에서 다른 쪽으로 데이터를 전달하려면, 두 대상을 연결하고 데이터를 전송할 수 있는 무언가가 필요한데 이것을 스트림(stream)이라고 정의

    스트림이란 데이터를 운반하는데 사용되는 연결 통로

  • 스트림은 단방향통신만 가능하기 때문에 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없음
    • 입력과 출력을 동시에 수행하려면 입력을 위한 입력 스트림(input stream)과 출력을 위한 출력 스트림(output stream), 모두 2개의 스트림이 필요
  • 스트림은 먼저 보낸 데이터를 먼제 받게 되어 있으며 중간에 건너뜀 없이 연속적으로 데이터를 주고받음

1.3 바이트기반 스트림 - InputStream, OutputStream

  • 스트림은 바이트단위로 데이터를 전송

    입력스트림 출력스트림 입출력 대상의 종류
    FileInputStream FileOutputStream 파일
    ByteArrayInputStream ByteArrayOutputStream 메모리(byte 배열)
    PipedInputStream PipedOutputStream 프로세스(프로세스간의 통신)
    AudioInputStream AudioOutputStream 오디오 장치
    • InputStream 또는 OutputStream의 자손들
    • 각각 읽고 쓰는데 필요한 추상메서드를 자신에 맞게 구현해놓음
  • 자바에서는 java.io 패키지를 통해 많은 종류의 입출력 관련 클래스들을 제공
    • 입출력을 처리할 수 있는 표준화된 방법을 제공함으로써 입출력의 대상이 달라져도 동일한 방법으로 입출력이 가능
  • InputStream과 OutputStream에 정의된 읽기와 쓰기를 수행하는 메서드

    InputStream OutputStream
    abstract int read() abstract void write(int b)
    int read(byte[] b) void write(byte[] b)
    int read(byte[] b, int off, int len) void write(byte[] b, int off, int len)
    • read()와 write(int b)는 입출력의 대상에 따라 읽고 쓰는 방법이 다를 것이기 때문에 각 상황에 맞게 구현하라고 추상메서드로 정의되어 있음

1.4 보조 스트림

  • 보조 스트림은 실제 데이터를 주고받는 스트림이 아니기 때문에 데이터를 입출력할 수 있는 기능은 없음

  • 보조 스트림은 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있음

  • 스트림을 먼저 생성한 후 생성된 스트림을 이용해 보조 스트림을 생성

  • 보조 스트림의 종류

    입력 출력 설명
    FilterInputStream FilterOutputStream 필터를 이용한 입출력 처리
    BufferedInputStream BufferedOutputStream 버퍼를 이용한 입출력 성능향상
    DataInputStream DataOutputStream 기본형 단위로 데이터를 처리하는 기능
    SequenceInputStream 없음 두 개의 스트림을 하나로 연결
    LineNumberInputStream 없음 읽어 온 데이터의 라인 번호를 카운트 (JDK1.1부터 LineNumberReader로 대체)
    ObjectInputStream ObjectOutputStream 데이터를 객체단위로 읽고 쓰는데 사용
    주로 파일을 이용하며 객체 직렬화와 관련있음
    없음 PrintStream 버퍼를 이용하며, 추가적인 print 관련 기능 (print, printf, println 메서드)
    PushbackInputStream 없음 버퍼를 이용해서 읽어 온 데이터를 다시 되돌리는 기능 (unread, push back to buffer)

1.5 문자기반 스트림 - Reader, Writer

  • 위의 스트림은 모두 바이트기반의 스트림
    • 바이트기반이란 입출력의 단위가 1byte라는 의미
    • 자바에서는 한 문자를 의미하는 char형이 2byte이기 때문에 바이트기반의 스트림으로 2byte인 문자를 처리하는데 어려움이 있음
  • 문자기반의 스트림이 제공됨. 문자 데이터를 입출력할 때는 바이트기반 스트림 대신 문자기반 스트림을 사용하는 것이 좋음
  • 문자기반 스트림의 이름은 바이트기반 스트림의 이름에서 InputStream은 Reader로, OutputStream은 Writer로 바꾸면 됨
    • 단, ByteArrayInputStream에 대응하는 문자기반 스트림은 char 배열을 사용하는 CharArrayReader
  • 문자기반 스트림의 읽기와 쓰기에 사용되는 메서드는 byte 배열 대신 char 배열을 사용
  • 문자기반 보조스트림도 존재

2. 바이트기반 스트림

2.1 InputStream과 OutputStream

  • InputStream과 OutputStream의 메서드

  • 스트림의 종류에 따라 mark()와 reset()을 사용해 이미 읽은 데이터를 되돌려 다시 읽을 수 있음
    • 이 기능을 지원하는 스트림인지 확인하는 markSupported()를 통해 알 수 있음
  • flush()는 버퍼가 있는 출력스트림의 경우에만 의미가 있음
    • OutputStream에 정의된 flush()는 아무 일도 하지 않음
  • 프로그램이 종료될 때, 사용하고 닫지 않은 스트림을 JVM이 자동적으로 닫아 주기는 하지만, 스트림을 사용해서 모든 작업을 마치고 난 후에는 close()를 호출해 반드시 닫아주여야 함
    • ByteArrayInputStream과 같이 메모리를 사용하는 스트림과 System.in, System.out과 같은 표준 입출력 스트림은 닫아 주지 않아도 됨

2.2 ByteArrayInpuStream과 ByteArrayOutputStream

  • 메모리, 즉 바이트배열에 데이터를 입출력 하는데 사용되는 스트림
    • 주로 다른 곳에 입출력하기 전 데이터를 임시로 바이트 배열에 담아서 변환 등의 작업을 하는데 사용
  • 바이트배열은 사용하는 자원이 메모리 밖에 없으므로 가비지컬렉터에 의해 자동적으로 자원을 반환하므로 close()를 이용해 스트림을 닫지 않아도 됨
  • read()와 write(int b)를 사용하기 때문에 한 번에 1byte만 읽고 쓰므로 작업효율이 떨어짐
    • 배열을 이용한 입출력을 사용하면 작업의 효율을 증가시킬 수 있음

3. 바이트기반의 보조스트림

3.1 FilterInputStream과 FilterOutputStream

  • InputStream/OutputStream의 자손이면서 모든 보조스트림의 조상

  • 보조스트림은 자체적으로 입출력을 수행할 수 없기 때문에 기반스트림을 필요로 함

  • FilterInputStream/FilterOutputStream의 생성자

    protected FilterInputStream(InputStream in)
    public FilterOutputStream(OutputStream out)
    
  • FilterInputStream/FilterOutputStream의 모든 메서드는 기반스트림의 메서드를 그대로 호출

    • FilterInputStream/FilterOutputStream 자체로는 아무런 일도 하지 않음을 의미
  • FilterInputStream/FilterOutputStream는 상속을 통해 원하는 작업을 수행하도록 읽고 쓰는 메서드를 오버라이딩해야 함

3.2 BufferedInputStream과 BufferedOutputStream

  • 스트림의 입출력 효율을 높이기 위해 버퍼를 사용하는 보조스트림

  • 한 바이트씩 입출력하는 것보다는 버퍼(바이트배열)를 이용해 한 번에 여러 바이트를 입출력하는 것이 빠르기 때문에 대부분의 입출력 작업에서 사용됨

  • BufferedInputStream의 생성자

    생성자 설명
    BufferedInputStream(InputStream in, int size) 주어진 InputStream인스턴스를 입력소스로 하며 지정된 크기(byte 단위)의 버퍼를 갖는 BufferedInputStream인스턴스를 생성
    BufferedInputStream(InputStream in) 주어진 InputStream인스턴스를 입력소스로 하며 버퍼의 크기를 지정해주지 않으므로 기본적으로 8192 byte 크기의 버퍼를 갖게 됨
    • 버퍼 크기는 입력소스로부터 한 번에 가져올 수 있는 데이터의 크기로 지정하면 좋음
    • 프로그램에서 입력소스로부터 데이터를 읽기 위해 처음으로 read를 호출하면, BufferedInputStream은 입력소스로부터 버퍼 크기만큼의 데이터를 일거와 자신의 내부 버퍼에 저장
    • 프로그램에서는 BuffferedInputStream의 버퍼에 저장된 데이터를 읽으면 됨
      • 외부의 입력소스로부터 읽는 것보다 내부의 버퍼로부터 읽는 것이 훨씬 빠르기 때문에 그만큼 작업 효율이 높아짐
    • 프로그램에서 버퍼에 저장된 모든 데이터를 다 읽고 그 다음 데이터를 읽기위해 read 메서드가 호출되면, BufferedInputStream은 입력소스로부터 다시 버퍼크기만큼의 데이터를 읽어다 버퍼에 저장해 놓음
  • BufferedOutputStream의 생성자와 메서드

    메서드 / 생성자 설명
    BufferedOutputStream(OutputStream out, int size) 주어진 OutputStream 인스턴스를 출력소스로하며 지정된 크기(byte 단위)의 버퍼를 갖는 BufferedOutputStream 인스턴스를 생성
    BufferedOutputStream(OutputStream out) 주어진 OutputStream인스턴스를 출력소스로하며 버퍼의 크기를 지정해주지 않으므로 기본적으로 8192byte 크기의 버퍼를 갖게 됨
    flush() 버퍼의 모든 내용을 출력소스에 출력한 다음, 버퍼를 비움
    close() flush()를 호출해서 버퍼의 모든 내용을 출력소스에 출력하고, BufferedOutputStream 인스턴스가 사용하던 모든 자원을 반환
    • 버퍼를 이용해 출력소스와 작업을 하게 됨
    • 프로그램에서 write 메서드를 이용한 출력이 BufferedOutputStream의 버퍼에 저장됨
    • 버퍼가 가득 차면, 그 때 버퍼의 모든 내용을 출력소스에 출력한 후, 버퍼를 지우고 다시 프로그램으로부터 출력을 저장할 준비를 함
    • 버퍼가 가득 찼을 때만 출력소스에 출력을 하기 때문에, 마지막 출력부분이 출력소스에 쓰이지 못하고 BufferedOutputStream의 버퍼에 남아있는 채로 프로그램이 종료될 수 있음
      • 프로그램에서 모든 출력작업을 마친 후 BufferedOutputStream에 close()나 flush()를 호출해 마지막에 버퍼에 있는 모든 내용이 출력소스에 출력되도록 해야 함
      • close()는 flush()를 호출해 버퍼의 내용을 출력스트림에 쓰도록 한 후, BufferedOutputStream인스턴스의 참조변수에 null을 지정함으로써 사용하던 자원들이 반환되게 함

3.3 DataInputStream과 DataOutputStream

  • DataInputStream은 DataInput 인터페이스를, DataOutputStream은 DataOutput 인터페이스를 각각 구현하였기 때문에, 데이터를 읽고 쓰는데 있어서 byte 단위가 아닌, 8가지 기본 자료형의 단위로 읽고 쓸 수 있다는 장점이 있음
  • DataOutputStream이 출력하는 형식은 각 기본 자료형 값을 16진수로 표현하여 저장함
    • int값을 출력하면, 4byte의 16진수로 출력됨
    • 각 자료형의 크기가 다르므로, 출력한 데이터를 다시 읽어올 때는 출력했을 때의 순서를 염두에 두어야 함
  • DataInputStream의 readInt()와 같이 데이터를 읽는 메서드는 더 이상 읽을 데이터가 없으면 EOFException을 발생시킴
    • 다른 입력스트림들과는 달리 무한반복문과 EOFException을 처리하는 catch문을 이용해 데이터를 읽음
  • try 블럭 내에서 스트림을 닫아주는 방법보다는, 작업도중에 예외가 발생해 스트림을 닫지 못하고 try 블럭을 빠져나갈 수 있기 때문에, finally 블럭을 이용해 스트림을 닫아주는 것이 더 확실한 방법
    • JDK1.7부터는 try-with-resources문을 이용해 close()를 직접 호출하지 않아도 자동호출되도록 할 수 있음

3.4 SequenceInputStream

  • 여러 개의 입력스트림을 연속적으로 연결해서 하나의 스트림으로부터 데이터를 읽는 것과 같이 처리할 수 있도록 도와줌

  • 생성자를 제외하고 나머지 작업은 다른 입력스트림과 다르지 않음

  • 큰 파일을 여러 개의 작은 파일로 나누었다가 하나의 파일로 합치는 것과 같은 작업을 수행할 때 사용하면 좋음

  • SequenceInputStream의 생성자

    메서드 / 생성자 설명
    SequenceInputStream(Enumeration e) Enumeration에 저장된 순서대로 입력스트림을 하나의 스트림으로 연결
    SequenceInputStream(InputStream s1, InputStream s2) 두 개의 입력스크림을 하나로 연결
    • Vector에 연결할 입력스트림들을 저장한 다음 Vector의 Enumeration elements()를 호출해서 생성자의 매개변수로 사용
    • Vector에 저장된 순서대로 입력되므로 순서에 주의

3.5 PrintStream

  • 데이터를 기반스트림에 다양한 형태로 출력할 수 있는 print, println, printf와 같은 메서드를 오버로딩하여 제공
  • 데이터를 적절한 문자로 출력하는 것이기 때문에 문자기반 스트림의 역할을 수행
  • PrintStream과 PrintWriter는 거의 같은 기능을 가지고 있지만 PrintWriter가 PrintStream에 비해 다양한 언어의 문자를 처리하는데 적합하기 때문에 가능하면 PrintWriter를 사용하는 것이 좋음
  • print()나 println()을 이용해 출력하는 중 PrintStream의 기반스트림에서 IOException이 발생하면 checkError()를 통해 인지할 수 있음
    • println()이나 print()는 예외를 던지지 않고 내부에서 처리하도록 정의함. println()과 같은 메서드는 매우 자주 사용되는 것이기 때문

4. 문자기반 스트림

4.1 Reader와 Writer

  • InputStream/OutputStream과 같은 역할을 함. byte 배열 대신 char 배열을 사용한다는 것이 차이점
  • Reader, Writer와 자손들은 여러 종류의 인코딩과 자바에서 사용하는 유니코드간의 변환을 자동적으로 처리해줌
    • Reader는 특정 인코딩을 읽어서 유니코드로 변환하고 Writer는 유니코드를 특정 인코딩으로 변환하여 저장

4.2 FileReader와 FileWriter

  • 파일로부터 텍스트데이터를 읽고, 파일에 쓰는데 사용됨
  • 사용방법은 FileInputStream/FileOutputStream과 다르지 않음

4.3 PipedReader와 PipedWriter

  • 쓰레드 간에 데이터를 주고받을 때 사용됨
  • 다른 스트림과는 달리 입력과 출력스트림을 하나의 스트림으로 연결해서 데이터를 주고받음
  • 스트림을 생성한 다음 어느 한쪽 쓰레드에서 connect()를 호출해서 입력 스트림과 출력 스트림을 연결. 입출력을 마친 후 어느 한쪽 스트림만 닫아도 나머지 스트림은 자동으로 닫힘

4.4 StringReader와 StringWriter

  • 입출력 대상이 메모리인 스트림

  • StringWriter에 출력되는 데이터는 내부의 StringBuffer에 저장되며 StringWriter의 메서드를 이용해 저장된 데이터를 얻을 수 있음

    StringBuffer getBuffer() StringWriter에 출력한 데이터가 저장된 StringBuffer를 반환

    String toString() StringWriter에 출력된 (StringBuffer에 저장된) 문자열을 반환

5. 문자기반의 보조스트림

5.1 BufferedReader와 BufferedWriter

  • 버퍼를 이용해 입출력의 효율을 높일 수 있도록 해주는 역할을 함
  • BufferedReader의 readLine()을 사용하면 데이터를 라인단위로 읽을 수 있고, BufferedWriter는 newLine()이라는 줄바꿈 해주는 메서드를 가지고 있음

5.2 InputStreamReader와 OutputStreamWriter

  • 바이트기반 스트림을 문자기반 스트림으로 연결시켜주는 역할
  • 바이트기반 스트림의 데이터를 지정된 인코딩의 문자 데이터로 변환하는 작업을 수행

6. 표준입출력과 File

6.1 표준입출력 - System.in, System.out, System.err

  • 표준 입출력: 콘솔을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미

System.in 콘솔로부터 데이터를 입력받는데 사용

System.out 콘솔로부터 데이터를 출력하는데 사용

System.err 콘솔로부터 데이터를 출력하는데 사용

  • in, out, err은 System 클래스에 선언된 클래스변수(static 변수)
    • out, err, in 타입은 실제로는 버퍼를 이용하는 BufferedInputStream과 BuffeeredOutputStream의 인스턴스를 사용
  • 콘솔 입력은 버퍼를 가지고 있기 때문에 Backspace키를 이용해서 편집이 가능하며 한 번에 버퍼의 크기만큼 입력이 가능
  • 콘솔에 데이터를 입력하고 Enter키를 누르면 입력대기상태에서 벗어나 입력된 데이터를 읽기 시작하고 입력된 데이터를 모두 읽으면 다시 입력대기 상태가 됨

6.2 표준입출력의 대상변경 - setOut(), setErr(), setIn()

메서드 설명
static void setOut(PrintStream out) System.out의 출력을 지정된 PrintStream으로 변경
static void setErr(PrintStream err) System.err의 출력을 지정된 PrintStream으로 변경
static void setIn(PrintStream in) System.in의 입력을 지정된 PrintStream으로 변경

6.3 RandomAccessFile

  • 하나의 클래스로 파일에 대한 입력과 출력을 모두 할 수 있음
  • DataInput 인터페이스와 DataOutput 인터페이스를 모두 구현했기 때문에 읽기와 쓰기가 모두 가능
  • 기본자료형 단위로 데이터를 읽고 쓸 수 있음
  • 장점: 파일의 어느 위치에나 읽시/쓰기가 가능하다는 것
    • 다른 입출력 클래스들은 입출력소스에 순차적으로 읽기/쓰기를 하기 때문에 읽기와 쓰기가 제한적인데 반해 RandomAccessFile 클래스는 파일에 읽고 쓰는 위치에 제한이 없음
  • 내부적으로 파일 포인터를 사용. 입출력 시에 작업이 수행되는 곳이 바로 파일 포인터가 위치한 곳이 됨
    • 파일 포인터의 위치는 파일의 제일 첫 부분(0부터 시작). 읽기 또는 쓰기를 수행할 때마다 작업이 수행된 다음 위치로 이동
      • 순차적으로 읽기나 쓰기를 한다면, 파일 포인터를 이동시키기 위해 별도의 작업이 필요하지 않음
      • 파일의 임의의 위치에 있는 내용에 대해 작업하고자 한다면, 먼저 파일 포인터를 원하는 위치로 옮긴 다름 작업 해야 함
    • 현재 작업 중인 파일에서 파일 포인터의 위치를 알고 싶을 때는 getFilePointer()을 사용.
    • 파일 포인터의 위치를 옮기기 위해서는 seek(long pos)나 skipBytes(int n)를 사용

6.4 File

  • File 클래스를 통해 파일과 디렉토리를 다룰 수 있음
    • File 인스턴스는 파일일 수도 있고 디렉토리일 수도 있음
  • File 인스턴스를 생성했다고 해서 파일이나 디렉토리가 생성되는 것은 아님
    • 파일명이나 디렉토리명으로 지정된 문자열이 유효하지 않더라도 컴파일 에러나 예외를 발생시키지 않음
  • 새로운 파일을 생성하기 위해서는 File 인스턴스를 생성한 다음, 출력 스트림을 생성하거나 createNewFile()을 호출해야 함

7. 직렬화(Serialization)

7.1 직렬화란?

  • 직렬화(Serialization): 객체를 데이터 스트림으로 만드는 것을 의미
    • 객체에 저장된 데이터를 스트림에 쓰기위해 연속적인 데이터로 변환하는 것
  • 역직렬화(deserialization): 스트림으로부터 데이터를 읽어서 객체를 만드는 것

Review

  • 객체: 클래스에 정의된 인스턴스 변수의 집합
    • 클래스 변수나 메서드가 포함되지 않음
    • 오직 인스턴스변수들로만 구성됨
  • 객체에는 메서드가 포함되지 않음
    • 인스턴스 변수는 인스턴스마다 다른 값을 가질 수 있어야하기 때문에 별도의 메모리공간이 필요하지만 메서드는 변하는 것이 아니라 메모리를 낭비해가면서 인스턴스마다 같은 내용의 코드를 포함시킬 이유가 없음
  • 객체를 저장한다는 것은 객체의 모든 인스턴스 변수의 값을 저장한다는 것과 같은 의미
    • 어떤 객체를 저장하고자 한다면, 현재 객체의 모든 인스턴스 변수의 값을 저장하면 됨
    • 저장했던 객체를 다시 생성하려면, 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스변수에 저장하면 됨

7.2 ObjectInputStream, ObjectOutputStream

  • 직렬화(스트림에 객체를 출력)에는 ObjectOutputStream을 사용하고 역직렬화(스트림으로부터 객체를 입력)에는 ObjectInputStream을 사용
    • 기반스트림을 필요로 하는 보조 스트림
    • 객체를 생성할 때 입출력(직렬화/역직렬화)할 스트림을 지정해주어야 함
  • 직렬화 과정
    • 출력할 스트림(FileOutputStream)을 생성해 이를 기반으로 하는 ObjectOutputStream()을 생성
    • ObjectOutputStream의 writeObject(Object obj)를 사용해 객체를 출력하면, 객체가 파일에 직렬화되어 저장됨
  • 역직렬화 과정
    • 입력스트림을 사용하고 writeObject(Object obj) 대신 readObject()를 사용해 저장된 데이터를 읽기만 하면 객체로 역직렬화됨
    • readObj()의 반환타입이 Object이기 때문에 객체 원래의 타입으로 형변환해주어야 함

7.3 직렬화가 가능한 클래스 만들기 - Serializable, transient

  • 직렬화가 가능한 클래스를 만들기 위해서는 직렬화하고자 하는 클래스가 java.io.Serializable 인터페이스를 구현하도록 하면 됨
  • Serializable 인터페이스는 아무런 내용 없는 빈 인터페이스. 직렬화를 고려해 작성한 클래스인지를 판단하는 기준이 됨
  • Serializable을 구현한 클래스를 상속받으면, Serializable을 구현하지 않아도 됨
    • 부모 클래스에서 Serializable을 구현한 경우 자식 클래스에서는 Serializable을 구현하지 않아도 직렬화 가능. 이 경우 자식 클래스 객체를 직렬화하면 조상 클래스에 정의된 인스턴스변수도 함께 직렬화 됨
    • 부모 클래스에서 Serializable을 구현하지 않은채 자식 클래스에서 직렬화를 하기 위해서는 자식 클래스에서 Serializable을 구현해주어야 함. 이때 자식 클래스의 객체를 직렬화하면 부모 클래스에 정의된 인스턴스변수는 직렬화 대상에서 제외됨
    • 부모 클래스에 정의된 인스턴스 변수를 직렬화 대상에 포함시키기 위해서는 부모 클래스에 Serializabel을 구현하던가, 자식 클래스에서 조상의 인스턴스 변수들이 직렬화되도록 처리하는 코드르 직접 추가해주어야 함
  • 직렬화 여부는 인스턴스변수의 타입이 아닌 실제로 연결된 객체의 종류에 의해 결정됨
    • Object 클래스는 Serializable을 구현하지 않았기 때문에 직렬화할 수 없음
  • 직렬화하고자 하는 객체의 클래스에 직렬화가 안 되는 객체에 대한 참조를 포함하고 있다면, 제어자 transient를 붙여 직렬화 대상에서 제외되도록 할 수 있음
    • 보안상 직렬화되면 안 되는 값에 대해서도 transient를 사용할 수 있음
    • transient가 붙은 인스턴스 변수의 값은 그 타입의 기본값으로 직렬화 됨
  • 객체를 역직렬화 할 때는 직렬화할 때의 순서와 일치해야 하기 때문에 직렬화할 객체가 많을 경우 각 객체를 개별적으로 직렬화하는 것보다 ArrayList와 같은 컬렉션에 저장해서 직렬화하는 것이 좋음
    • 역직렬화시 ArrayList 하나만 역직렬화하면 되므로 역직렬화할 객체의 순서를 고려하지 않아도 되기 때문

7.4 직렬화가능한 클래스의 버전관리

  • 직렬화된 객체를 역직렬화할 때는 직렬화했을 때와 같은 클래스를 사용해야 함
  • 클래스의 이름이 같더라도 클래스의 내용이 변경된 경우 역직렬화는 실패하며 예외가 발생
    • 예외 내용: 직렬화할 때와 역직렬화 할 때의 클래스의 버전이 같아야 하지만 다르다는 것
    • 객체가 직렬화될 때 클래스에 정의된 멤버들의 정보를 이용해 serialVersionUID라는 클래스의 버전을 자동생성해 직렬화 내용에 포함
    • 역직렬화할 때 클래스의 버전을 비교함으로써 직렬화할 때의 클래스의 버전과 일치하는지 확인 가능
  • static 변수나 상수 또는 transient가 붙은 인스턴스 변수가 추가되는 경우에는 직렬화에 영향을 미치지 않기 때문에 클래스의 버전을 다르게 인식하도록 할 필요가 없음
  • 클래스의 버전을 수동으로 관리하기 위해서는 serialVersionUID를 추가로 정의해야 함
    • 클래스 내에 serialVersionUID를 정의해주면, 클래스의 내용이 바뀌어도 클래스의 버전이 자동생성된 값으로 변경되지 않음
    • serialVersionUID의 값은 정수값이면 어떤 값으로도 지정 가능하지만, 서로 다른 클래스간에 같은 값을 갖지 않도록 serialver.exe를 사용해 생성된 값을 사용하는 것이 일반적
    • serialver.exe 뒤에 SerialVersionUID를 얻고자 하는 클래스의 이름만 적어주면 클래스의 serialVersionUID를 알아낼 수 있음
    • serialver.exe는 클래스에 serialVersionUID가 정의되어 있으면 그 값을 출력하고 정의되어 있지 않으면 자동 생성한 값을 출력
    • serialver.exe에 의해 생성되는 serialVersionUID 값은 클래스의 멤버들에 대한 정보를 바탕으로 하기 때문에 이 정보가 변경되지 않는 한 항상 같은 값을 생성

Chapter 15 끝!!!

2022-02-04 TIL (자바)

|

1. 자바

Chapter 14. 람다와 스트림의 스트림 부분과 Chapter 15. 입출력를 공부했다.

  • Chapter 14. 람다와 스트림
    • 스트림이란? + 스트림 특징
    • 스트림 만들기: 컬렉션, 배열, 람다식
    • 스트림 중간 연산: 자르기, 요소 걸러내기, 정렬, 변환, 조회
    • Optional, OptionalInt
    • 스트림 최종 연산: 조건 검사, 통계, 리듀싱
    • collect()
  • Chapter 15. 입출력
    • 입출력에서의 스트림
    • 바이트기반 스트림(InputStream, OutputStream) + 바이트기반 보조 스트림
    • 문자기반 스트림(Reader, Writer) + 문자기반 보조 스트림
    • 표준 입출력과 File
    • 직렬화: Serializable, transient

2

오늘 자바의 정석을 끝내는 것을 목표로 했지만 마지막 장인 16장 네트워킹 부분을 끝내지 못했다. 내일 자바의 정석을 마무리하고, 이번 주에 알고리즘 문제를 하나도 풀지 못해서 내일은 반드시 한 문제라도 풀어야겠다!

Chapter 13. 쓰레드

|

1. 프로세스와 쓰레드

  • 프로세스(process): 실행 중인 프로그램
    • 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 됨
  • 프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원과 쓰레드로 구성
  • 쓰레드: 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것
    • 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재
    • 멀티쓰레드 프로세스(multi-htreaded process): 둘 이상의 쓰레드를 가진 프로세스
    • 하나의 프로세스가 가질 수 있는 쓰레드의 개수는 제한되어 있지 않으나 쓰레드가 작업을 수행하는데 개별적인 메모리 공간을 필요로 하기 때문에 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정됨

멀티태스킹과 멀티쓰레딩

  • 대부분의 OS는 멀티태스킹(multi-tasking, 다중 작업)을 지원하기 때문에 여러 개의 프로세스가 동시에 실행될 수 있음
  • 멀티쓰레딩: 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것
    • CPU의 코어가 한 번에 단 하나의 작업만 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 개수는 코어의 개수와 일치
    • 처리해야하는 쓰레드의 수는 코어의 개수보다 훨씬 많기 때문에 각 코어가 아주 짧은 시간 동안 여러 작업을 번갈아 가며 수행함으로써 여러 작업들이 모두 동시에 수행되는 것처럼 보이게 함

멀티쓰레딩의 장점

  • CPU 사용률 향상
  • 자원을 효율적으로 사용
  • 사용자에 대한 응답성 향상
  • 작업이 분리되어 코드가 간결해짐

멀티쓰레딩 단점

  • 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화(synchronization), 교착상태(deadlock)와 같은 문제 발생 가능
    • 교착상태: 두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태

2. 쓰레드의 구현과 실행

  • 구현 방법 1. Thread 클래스를 상속받는 방법

    class MyThread extends Thread {
        public void run() {/* 작업내용 */}	// Thread 클래스의 run()을 오버라이딩
    }
    
  • 구현 방법 2. Runnable 인터페이스를 구현하는 방법

    class MyThread implements Runnable {
        public void run() {/* 작업내용 */}	// Runnable 인터페이스의 run()을 구현
    }
    
    • Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에 Runnable 인터페이스를 구현하는 방법이 일반적

    • 재사용성(reusablility)가 높고 코드의 일관성(consistency)을 유지할 수 있기 때문에 주로 사용

    • Runnable 인터페이스를 구현한 경우, Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 Thread 클래스의 생성자의 매개변수로 제공해야 함

      Runnable r = new ThreadEx1_2();
      Thread t2 = new Thread(r);
      
  • Runnable 인터페이스

    public interface Runnable {
        public abstract void run();
    }
    
    • run()만 정의되어 있는 간단한 인터페이스
  • Thread 클래스를 상속받으면, 자손 클래스에서 조상인 Thread 클래스의 메서드를 직접 호출할 수 있지만, Runnable을 구현하면 Thread 클래스의 static 메서드인 currentThread()를 호출하여 쓰레드에 대한 참조를 얻어 와야만 호출이 가능

    static currentThread() 현재 실행 중인 쓰레드의 참조를 반환

    String getName() 쓰레드의 이름을 반환

쓰레드의 실행 - start()

  • start()를 호출해야만 쓰레드가 실행됨
  • start()를 호출하면 실행대기 상태에 있다가 자신의 차례가 되면 실행 됨. 실행대기 중인 쓰레드가 하나도 없으면 곧바로 실행상태가 됨
  • 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없음
    • 쓰레드의 작업을 한 번 더 수행해야 한다면 새로운 쓰레드를 생성한 다음 start()를 호출해야 함
    • 하나의 쓰레드에 대해 start()를 두 번 이상 호출하면 실행시에 IllegalThreadStateException이 발생

3. start()와 run()

  • main 메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 클래스에 선언된 메서드를 호출하는 것일 뿐
  • start()는 새로운 쓰레드가 작업을 싱행하는데 필요한 호출스택(call stack)을 생성한 다음 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 올라가게 함
  • 모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하므로, 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 상용된 호출스택은 소멸됨

새로운 쓰레드를 생성하고 start()를 호출한 후 호출스택의 변화

start()

  • 출처: https://watrv41.gitbook.io/devbook/java/java-live-study/10_week
  1. main 메서드에서 쓰레드의 start()를 호출
  2. start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 생성
  3. 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행
  4. 호출스택이 2개이므로 스케줄러가 정한 순서에 의해 번갈아 가면서 실행됨
  • 스케줄러는 실행대기 중인 쓰레드들의 우선순위를 고려해 실행순서와 실행시간을 결정하고, 각 쓰레드들은 작성된 스케줄에 따라 자신의 순서가 되면 지정된 시간동안 작업을 수행
    • 주어진 시간동안 작업을 마치지 못한 쓰레드는 자신의 차례가 돌아올 때까지 대기상태로 있게 되며, run()의 수행이 종료된 쓰레드는 호출스택이 모두 비워지면서 이 쓰레드가 사용하던 호출스택은 사라짐

main 쓰레드

  • main 메서드의 작업을 수행하는 쓰레드
  • 프로그램을 실행하면 기본적으로 하나의 쓰레드를 생성하고, 그 쓰레드가 main 메서드를 호출해 작업이 수행되도록 함
  • main 메서드가 수행을 마쳤다하더라도 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않음
  • 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료됨

4. 싱글쓰레드와 멀티쓰레드

  • 두 개의 작업을 하나의 쓰레드로 처리하는 경우와 두 개의 쓰레드로 처리하는 경우를 가정해보면
    • 하나의 쓰레드로 두 작업을 처리하는 경우는 한 작업을 마친 후에 다른 작업을 시작
    • 두 개의 쓰레드로 작업하는 경우에는 짧은 시간동안 2개의 쓰레드가 번갈아 가면서 작업을 수행해 동시에 두 작업이 처리되는 것과 같이 느껴짐
  • 두 개의 쓰레드로 작업한 시간이 싱글쓰레드로 작업한 시간보다 더 걸림
    • 이유: 쓰레드간의 작업 전환(context switching)에 시간이 걸리기 때문
    • 작업 전환시, 현재 진행 중인 작업 상태 등의 정보를 저장하고 읽어 오는 시간이 소요됨
  • 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이라면 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 더 효율적
  • 두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 더 효율적
    • ex) 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와의 입출력을 필요호 하는 경우

5. 쓰레드의 우선순위

  • 쓰레드는 우선순위(priority)라는 속성(멤버변수)을 가지고 있는데, 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라짐
  • 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있음
    • 시각적인 부분이나 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선순위는 다른 작업을 수행하는 쓰레드에 비해 높아야 함

쓰레드의 우선순위 지정하기

  • 쓰레드의 우선순위와 관련된 메서드와 상수

    void setPriority(int new Priority)	// 쓰레드의 우선순위를 지정한 값으로 변경
    int getPriority()					// 쓰레드의 우선순위를 반환
          
    public static final int MAX_PRIORITY = 10		// 최대우선순위
    public static final int MIN_PRIORITY = 10		// 최소우선순위
    public static final int NORM_PRIORITY = 10		// 보통우선순위
    
    • 숫자가 높을수록 우선순위가 높음
  • 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받음

    • main 메서드를 수행하는 쓰레드는 우선순위가 5이므로 main 메서드내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 됨
  • 쓰레드를 실행하기 전에만 우선순위 변경이 가능

  • 멀티코어에서는 쓰레드의 우선순위에 따른 차이가 전혀 없음

    • 우선순위에 차등을 두어 쓰레드를 실행하는 것보다 작업에 우선순위를 두어 PriorityQueue에 저장해 놓고, 우선순위가 높은 작업이 먼저 처리되도록 하는 것이 나을 수 있음

6. 쓰레드 그룹(thread group)

  • 서로 관련된 쓰레드를 그룹으로 다루기 위한 것
  • 쓰레드 그룹에 다른 쓰레드 그룹을 포함시킬 수 있음
  • 쓰레드 그룹은 보안상의 이유로 도입된 개념으로, 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경 가능하지만, 다른 쓰레드 그룹의 쓰레드를 변경할 수는 없음
  • ThreadGroup을 사용해 생성 가능함
  • 쓰레드를 쓰레드 그룹에 포함시키려면 Thread 의 생성자를 이용해야 함

    Thread(ThreadGroup group, String name)
    Thread(ThreadGroup group, Runnable target)
    Thread(ThreadGroup group, Runnable target, String name)
    Thread(ThreadGroup group, Runnable target, String name, long stackSize)
    
  • 모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 함

    • 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 됨
  • 자바 어플리케이션이 실행되면, JVM은 main과 system이라는 쓰레드 그룹을 만들고 JVM 운영에 필요한 쓰레드들을 생성해 이 쓰레드 그룹에 포함시킴

  • 우리가 생성한 모든 쓰레드 그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹이 되고, 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동적으로 main 쓰레드 그룹에 속하게 됨

  • Thread의 쓰레드 그룹과 관련된 메서드

    ThreadGroup getThreadGroup()	// 자신이 속한 쓰레드 그룹을 반환
    void uncaughtException(Thread t, Throwable e)	// 쓰레드 그룹의 쓰레드가 처리되지 않은 예외에 의해 실행이 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출됨
    

7. 데몬 쓰레드(daemon thread)

  • 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드
  • 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료됨
    • 이유: 데몬 쓰레드는 일반 쓰레드의 보조역할을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재의 의미가 없기 때문
  • 데몬 쓰레드의 예: 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신

  • 데몬 쓰레드는 무한루프와 조건문을 이용해 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성

  • 데몬 쓰레드는 일반 쓰레드의 작성방법과 실행방법이 같으며, 쓰레드를 생성한 다음 실행하기 전 setDaemon(true)를 호출하기만 하면 됨

    boolean isDaemon()			// 쓰레드가 데몬 쓰레드인지 확인 (데몬 쓰레드이면 true 반환)
    void setDaemon(boolean on)	// 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경
        						// 매개변수 on의 값을 true로 지정하면 데몬쓰레드가 됨
    
    • 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 됨
    • setDaemon 메서드는 반드시 start()를 호출하기 전에 실행되어야 함. 그렇지 않으면 예외 발생

8. 쓰레드의 실행제어

  • 쓰레드 스케줄링 관련 메서드

    thread

    • suspend(), resume(), stop()은 deprecated 됨
      • 이유: suspend(), stop()은 교착상태를 일으키기 쉽게 작성되었기 때문
      • deprecated는 전에는 사용되었지만, 앞으로 사용하지 않을 것을 권장한다는 의미. 하위 호환성을 위해 삭제하지 않은 것일 뿐 사용해서는 안됨
  • 쓰레드 상태

    상태 설명
    NEW 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
    RUNNABLE 실행 중 또는 실행 간으한 상태
    BLOCKED 동기화블럭에 의해 일시정지된 상태(lock)이 풀릴 때까지 기다리는 상태
    WAITING,
    TIMED_WAITING
    쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은 일시정지 상태. TIMED_WAITING은 일시정지시간이 지정된 경우를 의미
    TERMINATED 쓰레드의 작업이 종료된 상태

쓰레드의 상태

thread_state

  1. 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 함
    • 실행대기열은 큐와 같은 구조로 먼저 실행디기열에 들어온 쓰레드가 먼저 실행됨
  2. 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 됨
  3. 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 됨
  4. 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지 상태가 될 수 있음
    • I/O block은 입출력작업에서 발생하는 지연상태를 의미
  5. 지정된 일시정지시간이 다되거나(time-out), notify(), resume(), interrupt()가 호출되면 일시정시상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 됨
  6. 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸됨

sleep(long millis) - 일정시간동안 쓰레드를 멈추게 함

static void sleep(long millis)
static void sleep(long millis, int nanos)
  • 밀리세컨드와 나노세컨드의 시간단위로 세밀하게 값을 지정할 수 있지만 어느 정도 오차가 발생할 수 있음
  • sleep 메서드에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 디거나 interrupt()가 호출되면, 잠에서 깨어나 실행대기 상태가 됨
  • sleep 메서드를 호출할 때는 항상 try-catch문으로 예외를 처리해줘야 함
  • sleep 메서드는 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에 th1.sleep(2000)과 같이 호출했더라도 실제로 영향을 맏는 것은 main 메서드를 실행하는 main 쓰레드
    • 따라서 sleep()은 static으로 선언되어 있으며 참조변수를 이용해 호출하기 보다는 Thread.sleep(2000)과 같이 해야 함

**interrupt()와 interrupted() - 쓰레드의 작업을 취소함 **

void interrupt()				// 쓰레드의 interrupted 상태를 false에서 true로 변경
boolean isInterrupted()			// 쓰레드의 interrupted 상태를 반환
static boolean interrupted()	// 현재 쓰레드의 interrpted 상태를 반환 후, false로 변경
  • interrupt()는 쓰레드에게 작업을 멈추라고 요청함
    • 멈추라고 요청만 하는 것이고 쓰레드를 강제로 종료시키지는 못함
    • 쓰레드의 interrupted 상태(인스턴스 변수)를 바꾸는 것
  • interrupted()는 쓰레드에 대해 interrupt()가 호출되었는지 알려줌
    • interrupt()가 호출되지 않았다면 false를, interrupt()가 호출되었다면 true를 반환
  • sleep(), wait(), join()에 의해 WAITING 상태에 있을 때, 해당 쓰레드에 대해 interupt()를 호출하면 Interrupted Exception이 발생하고 쓰레드는 RUNNABLE 상태로 바뀜
    • 멈춰있던 쓰레드를 깨워 실행가능한 상태로 만드는 것

yield() - 다른 쓰레드에게 양보함

  • 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보
  • yield()와 interrupt()를 적절히 사용하면, 프로그램의 응답성을 높이고 효율적인 실행이 가능하게 할 수 있음

join() - 다른 쓰레드의 작업을 기다림

void join()
void join(long millis)
void join(long millis, int nanos)
  • 쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 사용
    • 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야할 필요가 있을 때 사용
  • 시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다림
  • interrupt()에 의해 대기상태에서 벗어날 수 있으며, join()이 호출되는 부분을 try-catch문으로 감싸야 함

9. 쓰레드의 동기화

  • 멀티쓰레드 프로세스인 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게됨
    • 쓰레드 A가 작업하던 도중 다른 쓰레드 B에게 제어권이 넘어갔을 때, 쓰레드 A가 작업하던 공유 데이터를 쓰레드 B가 임의로 변경했다면, 다시 쓰레드 A가 제어권을 받아 나머지 작업을 마쳤을 때 원래 의도했던 것과는 다른 결과를 얻을 수 있음
  • 이것을 방지하기 위해 한 쓰레드가 특정 작업을 마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요
    • 이로 인해 도입된 개념이 임계영역(critical section)과 잠금(락, lock)
  • 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 함
    • 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 됨
  • 쓰레드 동기화(synchronization): 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것
    • JDK1.5부터 java.util.concurrent.locksjava.util.concurrent.atomic 패키지를 통해 다양한 방식으로 동기화를 구현할 수 있도록 지원하고 있음

9.1 synchronized를 이용한 동기화

  • synchronized 키워드는 임계 영역을 설정하는데 사용됨

  • 방식 1. 메서드 전체를 임계 영역으로 지정

    public synchronized void calcSum(){
        // ...
    }
    
    • 메서드 앞에 synchronized를 붙이는 방식. 메서드 전체가 임계 영역으로 설정됨
    • 쓰레드는 synchronized 메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다 메서드가 종료되면 lock을 반환
  • 방식 2. 특정한 영역을 임계 영역으로 지정

    synchronized(객체의 참조변수){
        // ...
    }
    
    • 메서드 내의 코드 일부를 {}으로 감싸고 블럭 앞에 synchronized(참조변수)를 붙이는 방식. 이 블럭을 synchronized 블럭이라고 함
    • 참조변수는 락을 걸고자하는 객체를 참조하는 것이어야 함
    • 이 블럭의 영역 안으로 들어가면서부터 쓰레드는 지정된 객체의 lock을 얻게 되고, 이 블럭을 벗어나면 lock을 반납
  • 두 방식 모두 lock의 획득과 반납이 자동적으로 이루어지므로 임계 영역만 설정해주면 됨
  • 모든 객체는 lock을 하나씩 가지고 있으며, 해당 객체의 lock을 가지고 있는 쓰레드만 임계 영역의 코드를 수행할 수 있음. 다른 쓰레드들은 lock을 얻을 때까지 기다리게 됨
  • 임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 메서드 전체에 락을 거는 것보다 synchroized 블럭으로 임계 영역을 최소화해 효율적인 프로그램이 되도록 노력해야 함

9.2 wait()와 notify()

  • 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록하기 위해 고안된 것이 wait()notify()

  • synchronized 블록 내에서만 사용 가능

  • 동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait() 을 호출해 쓰레드가 락을 반납하고 기다리게 함. 그러면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있게 됨. 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 함

  • 단, 오래 기다린 쓰레드가 락을 얻는다는 보장이 없음

    • wait()가 호출되면, 실행 중이던 쓰레드는 해당 객체의 대기실(waiting pool)에서 통지를 기다림
    • notify()가 호출되면 해당 객체의 대기실에 있던 모든 쓰레드 중 임의의 쓰레드만 통지를 받음
    • nofifyAll()은 기다리고 있는 모든 쓰레드에게 통보를 하지만, lock을 얻을 수 있는 것은 하나의 쓰레드이고 나머지 쓰레드는 통보를 받긴 했지만, lock을 얻지 못하면 다시 lock을 기다리게 됨
  • wait()notify()는 특정 객체에 대한 것이므로 Object 클래스에 정의되어 있음

    void wait()
    void wait(long timeout)
    void wait(long timeout, int nanos)
    void notify()
    void notifyAll()
    
    • wait()notify() 또는 notifyAll()이 호출될 때까지 기다리지만, 매개변수가 있는 wait()는 지정된 시간동안만 기다림. 지정된 시간이 지난 후에 자동적으로 notify()가 호출되는 것과 같음
  • waiting pool은 객체마다 존재하는 것이므로 notifyAll()이 호출된다고 해서 모든 객체의 waiting pool에 있는 쓰레드가 깨워지는 것은 아님

    • notifyAll()이 호출된 객체의 waiting pool에 대기 중인 쓰레드만 해당됨

기아 현상과 경쟁 상태

  • 기아 (starvation) 현상: 어떤 하나의 쓰레드가 계속 통지를 받지 못하고 오랫동안 기다리게 되는 현상
    • notifyAll()을 사용해 해결
  • 경쟁 상태(race condition): 여러 쓰레드가 lock을 얻기 위해 서로 경쟁하는 것
    • Lock과 Condition을 이용해 해결

9.3 Lock과 Condition을 이용한 동기화

  • synchronized 블럭으로 동기화를 하면 자동적으로 lock이 잠기고 풀리기 때문에 편리

    • synchronized 블럭 내에서 예외가 발생해도 lock은 자동적으로 풀림
    • 하지만 같은 메서드 내에서만 lock을 걸 수 있다는 제약이 있어 불편하기도 함. 이럴 때 lock 클래스를 사용

    ReentrantLock 재진입이 가능한 lock. 가장 일반적인 배타 lock

    ReentrantReadWriteLock 읽기에는 공유적이고, 쓰기에는 배타적인 lock

    StampedLock ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가

  • ReentrantLock은 가장 일반적인 lock
    • 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로 들어와 이후의 작업을 수행할 수 있음
  • ReentrantLock은 배타적인 lock이라서 무조건 lock이 읽어야만 임계 영역의 코드를 수행할 수 있지만, ReentrantReadWriteLock은 읽기 lock이 걸려있으면, 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행할 수 있음
    • 읽기를 위한 lock과 쓰기를 위한 lock을 제공
    • 읽기는 내용을 변경하지 않으므로 동시에 여러 쓰레드가 읽어도 문제가 되지 않음
    • 읽기 lock이 걸린 상태에서 쓰기 lock을 거는 것은 허용되지 않음
  • StampedLock은 lock을 걸거나 해지할 때 ‘‘스탬프(long타입의 정수값)’를 사용하며, 읽기와 쓰기를 위한 lock외에 ‘낙관적 읽기 lock(optimistic reading lock)’이 추가된 것
    • 읽기 lock이 걸려있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야하는데 비해 낙관적 읽기 lock은 쓰기 lock에 의해 발로 풀림
    • 낙관적 읽기에 실패하면, 읽기 lock을 얻어 다시 읽어 와야 함
    • 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 거는 것

ReentrantLock의 생성자

ReentrantLock ()
ReentrantLock (boolean fair)
  • 생성자의 매개변수를 true로 주면, lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock을 획득할 수 있게 공정하게 처리
    • 공정하게 처리할 경우 어떤 쓰레드가 가장 오래 기다렸는지 확인하는 과정을 거쳐야하므로 성능은 떨어짐
void lock()			// lock을 잠금
void unlock()		// lock을 해지
boolean isLocked()	// lock이 잠겼는지 확인

boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
  • synchronized 블럭은 lock의 잠금과 해제가 자동적으로 관리되지만, ReentrantLock과 같은 lock 클래스들은 수동으로 lock을 잠그고 해제해야 함
  • 임계 영역 내에서 예외가 발생하거나 return문으로 빠져 나가게 되면 lock이 풀리지 않을 수 있으므로 unlock()은 try-finally문으로 감싸는 것이 일반적
  • tryLock()은 다른 쓰레드에 의해 lock이 걸려 있으면 lock을 얻으려고 기다리지 않거나 지정된 시간만큼 기다림
    • lock을 얻으면 true를 반환하고, 얻지 못하면 false를 반환
    • 응답성이 중요한 경우, 지정된 시간동안 lock을 얻지 못하면 다시 작업을 시도할 것이지 포가힐 것인지를 사용자가 결정할 수 있게 하는 것이 좋음
    • 예외을 발생시킬 수 있는데, 지정된 시간동안 lock을 얻으려고 기다리는 중에 interrupt()에 의해 작업을 취소될 수 있도록 코드를 작성할 수 있다는 의미

9.4 volatile

  • 코어는 메모리에서 읽어온 값을 캐시에 저장하고 캐시에서 값을 읽어서 작업. 다시 같은 값을 읽어올 때는 먼저 캐시에 있는지 확인하고 없을 때문 메모리에서 읽어옴. 그러다보니 도중에 메모리에 저장된 변수의 값이 변경되었는데도 캐시에 저장된 값이 갱신되지 않아 메모리에 저장된 값이 다른 경우가 발생
  • 변수 앞에 volatile을 붙이면 코어가 변수의 값을 읽어올 때 캐시가 아닌 메모리에서 읽어오기 때문에 캐시와 메모리간의 값의 불일치가 해결됨
  • 변수에 volatile을 붙이는 대신 synchronized 블럭을 사용해도 같은 효과를 얻을 수 있음
    • 쓰레드가 synchronized 블럭으로 들어갈 때와 나올 때, 캐시와 메모리 간의 동기화가 이루어지기 때문에 값의 불일치가 해소되기 때문

volatile로 long과 double을 원자화

  • JVM은 데이터를 4byte 단위로 처리하기 때문에, int와 int보다 작은 타입들은 한 번에 읽거나 쓰는 것이 가능
    • 단 하나의 명령어로 읽거나 쓰기가 가능하다는 의미
    • 하나의 명령어는 더 이상 나눌 수 없는 최소의 작업단위이므로, 작업의 중간에 다른 쓰레드가 끼어들 수 없음
  • 크기가 8byte인 long과 double 타입의 변수는 하나의 명령어로 값을 읽거나 쓸 수 없기 때문에, 변수의 값을 읽는 과정에 다른 쓰레드가 끼어들 수 있음
    • 다른 쓰레드가 끼어들지 못하게 변수를 읽고 쓰는 모든 문장을 synchronized 블럭으로 감쌀 수도 있지만, 변수를 선언할 때 volatile을 붙이는 방법으로도 해결 가능
  • volatile는 해당 변수에 대한 읽거나 쓰기가 원자화됨
    • 원자화: 작업을 더 이상 나눌 수 없게 함
    • synchronized 블럭도 일종의 원자화. synchronized 블럭은 여러 문장을 원자화함으로써 쓰레드의 동기화를 구현한 것
  • volatile은 해당 변수에 대한 읽거나 쓰기를 원자화 할 뿐, 동기화하는 것은 아님

9.5 fork & join 프레임웍

  • 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하는 것을 쉽게 만들어줌

  • 수행할 작업에 따라 RecursiveAction과 RecursiveTask, 두 클래스 중 하나를 상속받아 구현해야 함

    RecursiveAction 반환값이 없는 작업을 구현할 때 사용

    RecursiveTask 반환값이 있는 작업을 구현할 때 사용

    • 두 클래스 모두 compute()라는 추상 메서드를 가짐
    • 상속을 통해 compute()를 구현한 후, 쓰레드풀과 수행할 작업을 생성하고, invoke()로 작업을 시작
  • ForkJoinPool: fork&join 프레임웍에서 제공하는 쓰레드 풀

    • 지정된 수의 쓰레드를 생성해서 미리 만들어 놓고 반복해서 재사용할 수 있게 함
      • 쓰레드 풀은 기본적으로 코어의 개수와 동일한 개수의 쓰레드를 생성
    • 쓰레드를 반복해서 생성하지 않아도 되고, 너무 많은 쓰레드가 생성되어 성능이 저하되는 것을 막아준다는 장점이 있음
    • 쓰레드 풀은 쓰레드가 수행해야하는 작업이 담긴 큐를 제공하며, 각 쓰레드는 자신의 작업 큐에 담긴 작업을 순서대로 처리

compute() 의 구현

  • 수행할 작업 외에도 작업을 어떻게 나눌 것인가에 대해서도 알려줘야 함

다른 쓰레드의 작업 훔쳐오기

  • 작업 훔쳐오기(work stealing): fork()가 호출되어 작업 큐에 추가된 작업도 compute()에 의해 더 이상 나눌 수 없을 때까지 반복해서 나뉘고, 자신의 작업 큐가 비어있는 쓰레드는 다른 쓰레드의 작업 큐에서 작업을 가져와 수행
    • 쓰레드풀에 의해 자동적으로 이루어짐

fork()와 join()

  • fork()는 작업을 쓰레드의 작업 큐에 넣는 것이고, 작업 큐에 들어간 작업은 더 이상 나눌 수 없을 때까지 나뉨
    • compute()로 나누고 fork()로 작업 큐에 넣는 작업이 계속해서 반복됨
    • 나눠진 작업은 각 쓰레드가 골고루 나눠서 처리하고, 작업의 결ㄱ허눈 join()을 호출해서 얻을 수 있음
  • fork()join()의 차이점: fork()는 비동기 메서드(asynchronous method)이고, join()은 동기 메서드(synchronous method)
    • 비동기 메서드는 메서드를 호출만 할 뿐, 결과를 기다리지 않음. 내부적으로는 다른 쓰레드에게 작업을 수행하도록 지시만 하고 결과를 기다리지 않고 돌아오는 것

Chapter 14. 람다와 스트림_1(람다)

|

1. 람다식(Lambda expression)

1.1 람다식이란?

  • 람다식(Lambda expression): 메서드를 하나의 식(expression)으로 표현한 것
    • 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해줌
  • 메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 ‘익명 함수(anonymous function)‘이라고도 함
int[] arr = new int[5];
Arrays.setAll(arr, (i) -> (int)(Math.random()*5)+1);

// 람다식이 하는 일을 메서드로 표현
int method() {
    return (int)(Math.random()*5) + 1;
}
  • 모든 메서드는 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 메서드를 호출할 수 있지만, 람다식은 람다식 자체만으로도 메서드의 역할을 대신할 수 있음
  • 람다식은 메서드의 매개변수로 전달되어지는 것이 가능하고, 메서드의 결과로 반환될 수도 있음

1.2 람다식 작성하기

  • 메서드에서 이름과 반환타입을 젲거하고 매개변수 선언부와 몸통 {} 사이에 ‘->’를 추가

    (매개변수 선언) -> {
        문장들
    }
    
  • 반환값이 있는 메서드의 경우, return문 대신 ‘식(expression)’으로 대신할 수 있음

    (int a, int b) -> a > b ? a : b
    
    • 식의 연산결과가 자동적으로 반환값이 됨
    • 문장(statement)이 아닌 식이므로 끝에 ‘;’을 붙이지 않음
  • 람다식에 선언된 매개변수의 타입은 추론이 가능한 경우 생략이 가능한데, 대부분의 경우에 생략이 가능함

    (a, b) -> a > b ? a : b
    
    • 두 매개변수 중 어느 하나의 타입만 생략하는 것은 허용되지 않음
  • 선언된 매개변수가 하나뿐인 경우 괄호 ()를 생략할 수 있음. 단, 매개변수의 타입이 있으면 괄호 () 생략 불가

    (a) -> a* a
    a -> a*a	// 위의 식과 같은 의미
    
  • 괄호 {} 안의 문장이 하나일 때는 괄호 {} 생략 가능

    (String name, int i) -> {
        System.out.println(name+"="+i);
    }
      
    // 괄호 생략
    (String name, int i) -> 
        System.out.println(name+"="+i)
    
    • 문장의 끝에 ‘;’를 붙이지 않아야 한다는 것에 주의

1.3 함수형 인터페이스(Functional Interface)

  • 람다식은 익명 클래스의 객체와 동등

  • 람다식으로 정의된 익명 객체의 메서드를 호출하기 위해서는 참조변수가 필요

    타입 f = (int a, int b) -> a > b ? a: b;
    
    • 익명 객체의 주소를 f라는 참조변수에 저장
    • 이 때, 참조변수 f의 타입은 클래스 또는 인터페이스가 가능
    • 람다식과 동등한 메서드가 정의되어 있어야 함. 그래야만 참조변수로 익명 객체(람다식)의 메서드를 호출할 수 있기 때문
interface Myfunction {
    public abstract int max(int a, int b);
}

MyFunction f = new MyFunction() {
    					public int max(int a, int b) {
                            return a > b ? a : b;
                        }
				};
int big = f.max(5, 3);

// 람다식 사용
MyFunction f = (int a, int b) -> a> b ? a: b;	// 익명 객체를 람다식으로 대체
int big = f.max(5, 3)	// 익명 객체의 메서드 호출
  • MyFunction 인터페이스를 구현한 익명 객체를 람다식으로 대체 가능한 이유
    • 람다식도 실제로는 익명 객체
    • MyFunction 인터페이스를 구현한 익명 객체의 메서드 max()와 람다식의 매개변수의 타입과 개수 그리고 반환값이 일치하기 때문
  • 함수형 인터페이스(functional interface): 람다식을 다루기 위한 인터페이스

    @FunctionalInterface
    interface MyFunction {	// 함수형 인터페이스 MyFunction을 정의
        public abstract int max(int a, int b)
    }
    
    • 람다식과 인터페이스의 메서드가 1:1로 연결되기 위해, 함수형 인터페이스에는 오직 하나의 추상 메서드만 정의되어 있어야 한다는 제약이 있음
    • 반면에 static 메서드와 default 메서드의 개수에는 제약이 없음

함수형 인터페이스 타입의 매개변수와 반환타입

  • 매서드의 매개변수가 MyFunction타입이면, 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야한다는 의미

  • 참조변수 없이 람다식을 직접 매개변수로 지정하는 것도 가능

    aMethod(() -> System.out.println("myMethod()"));
    
  • 메서드의 반환타입이 함수형 인터페이스타입이라면, 이 함수형 인터페이스의 추상메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있음

  • 람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미

    • 변수처럼 메서드를 주고받는 것이 가능해짐

람다식의 타입과 형변환

  • 함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스 타입과 일치하는 것은 아님
  • 람다식은 익명 객체이고 익명 객체는 타입이 없음. 따라서 대입 연산자의 양변의 타입을 일치시키기 위해서는 형변환이 필요
    • 정확히는 타입이 있지만, 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것
  • 람다식은 함수형 인터페이스로만 형변환이 가능
    • Object 타입으로도 형변환 불가

1.4 java.util.function 패키지

  • 일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해 놓음

    functionalinterface

    • 매개변수와 반환값의 유무에 따라 4개의 함수형 인터페이스가 정의되어 있음
    • Function의 변형으로 Predicate가 있음. 반환값이 boolean이라는 것만 제외하면 Function과 동일

조건식의 표현에 사용되는 Predicate

  • 조건식을 람다식으로 표현하는데 사용됨

    Predicate<String> isEmptyStr = s -> s.length() == 0;
    String s = "";
      
    if (isEmptyStr.test(s))
        System.out.println("This is an empty String.");
    

매개변수가 두 개인 함수형 인터페이스

  • 매개변수 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 ‘Bi’가 붙음

    functionalinterface2

UnaryOperator와 BinaryOperator

  • 매개변수의 타입과 반환타입의 타입이 모두 일치한다는 점만 제외하면 Function과 같음

    functionalinterface3

컬렉션 프레임웍과 함수형 인터페이스

  • 컬렉션 프레임웍의 인터페이스에 다수의 디폴트 메서드가 추가되었는데, 그 중 일부는 함수형 인터페이스를 사용

    functionalinterface4

    • Map 인터페이스에 있는 ‘comput’로 시작하는 메서드들은 맵의 value를 변환하는 일을 하고 merge()는 Map을 병합하는 일을 함

기본형을 사용하는 함수형 인터페이스

  • 지금까지 살펴본 함수형 인터페이스는 매개변수와 반환값의 타입이 모두 지네릭 타입
  • 기본형 타입의 값을 처리할 때는 래퍼 클래스를 사용해왔는데, 기본형 대신 래퍼클래스를 사용하는 것은 비효율적. 따라서 효율적으로 처리하기 위해 기본형을 사용하는 함수형 인터페이스들이 제공됨

사진 출처: https://velog.io/@oyeon/14-78-java.util.function-%ED%8C%A8%ED%82%A4%EC%A7%80

1.5 Function의 합성과 Predicate의 결합

// Function
default <V> Function<T,V> andThen(Function<? super R,? extends V> after)
default <V> Function<V,R> compose(Function<? super V,? extends T> before)
static <T> Function<T, T> identity()
    
// Predicate
default Predicate<T> and(Predicate<? super T> other)
default Predicate<T> or(Predicate<? super T> other)
default Predicate<T> negate()
static <T> Predicate<T> isEqual(Object targetRef)

Function의 합성

  • 두 함수의 합성은 어느 함수를 먼저 적용하느냐에 따라 달라짐

    • 함수 f, g가 있을 때, f.andThen(g)는 함수 f를 먼저 적용하고, 그 다음에 함수 g를 적용
    • f.compose(g)는 반대로 g를 먼저 적용하고 f를 적용
  • 문자열을 숫자로 변환하는 함수 f와 숫자를 2진 문자열로 변환하는 함수 g를 andThen()으로 합성해 새로운 함수 h 만들기

    Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);
    Function<Integer, String> g = (i) -> Integer.toBinaryString(i);
    Function<String, String> h = f.andThen(g);
    
  • identity()는 함수를 적용하기 이전과 이후가 동일한 항등 함수가 필요할 때 사용

Predicate의 결합

  • 여러 Predicate를 and(), or(), negate()로 연결해서 하나의 새로운 Predicate로 결합 가능

  • static 메서드인 isEqual()은 두 대상을 비교하는 Predicate를 만들 때 사용

    boolean result = Predicate.isEqual(str1).test(str2);
    
    • isEqual()의 매개변수로 비교대상을 하나 지정하고, 또 다른 비교대상은 test()의 매개변수로 지정

1.6 메서드 참조

  • 람다식이 하나의 메서드만 호출하는 경우 ‘메서드 참조(method reference)’라는 방법으로 람다식을 간략히 할 수 있음

    Function<String, Integer> f = (String s) -> Integer.parseInt(s);
      
    // 메서드 참조
    Function<String, Integer> f = Integer::parseInt;
    
    • 컴파일러는 생략된 부분을 우변의 parseInt 메서드의 선언부로부터, 혹은 좌변의 Function 인터페이스에 지정된 지네릭 타입으로부터 알아낼 수 있음
  • 3가지 경우의 메서드 참조

    종류 람다 메서드 참조
    static 메서드 참조 (x) -> ClassName.method(x) ClassName::method
    인스턴스메서드 참조 (obj, x) -> obj.method(x) ClassName::method
    특정 객체 인스턴스메서드 참조 (x) -> obj.method(x) obj::method

하나의 메서드만 호출하는 람다식은 ‘클래스이름::메서드이름’ 또는 ‘참조변수::메서드이름’으로 바꿀 수 있음

생성자의 메서드 참조

  • 생성자를 호출하는 람다식도 메서드 참조로 변환 가능

    Supplier<MyClass> s = () -> new MyClass(); 	// 람다식
    Supplier<MyClass> s = MyClass::new;			// 메서드 참조
    
  • 매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용

    Function<Integer, MyClass> f = (i) -> new MyClass(i);	// 람다식
    Function<Integer, MyClass> f2 = MyClass::new;			// 메서드 참조
      
    BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s);
    BiFunction<Integer, String, MyClass> bf = MyClass::new;	// 메서드 참조
    
  • 메서드 참조는 람다식을 마치 static 변수처럼 다룰 수 있게 해줌

  • 메서드 참조는 코드를 간략히 하는데 유용해서 많이 사용됨