728x90

예시로 시작해보자!!

  • 통화별로 트랜잭션을 그룹화한 다음에 모든 트랜잭션 합계를 계산하는 예제
  • Map<Currency, Integer> 을 반환해야 한다.
  • 명령형 코드
    • 코드가 길어서 무엇을 실행하는지 파악하기 어렵다.
    • Map<Currency, List<Transaction>> transactionByCurrencies = new HashMap<>(); // 그룹화한 트랜잭션을 저장할 맵을 생성 for (Transactin transaction : transactions) { // 트랜잭션 리스트를 반복 Currency currency = transaction.getCurrency(); List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency); if (transactionForCurrency = null) { // 현재 통화를 그룹화하는 맵에 항목이 없으면 항목을 만든다. transactionsForCurrency = new ArrayList<>(); transactionsByCurrencies.put(currency, transactionsForCurrency); } transactinsForCurrency.add(transaction); // 같은 통화를 가진 트랜잭션 리스트에 현재 탐색 중인 트랜잭션을 추가 }
  • 함수형 코드
    Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));

이번 주제 키워드


reduce 연산(하나로 수렴)을 통해 스트림을 다양한 형태로 collect 하는 방법에 대한 내용

  • Collectors
  • reduce, reducing
  • 데이터의 그룹화(groupingBy)와 분할(Predicate)
  • Collector 인터페이스 살펴보기
    • toList() 살펴보기

Collector란??

  • 자바 8에서 Collector는 Stream 요소들을 어떻게 분류하고 모을지(reducing)를 담당하는 인터페이스이다.
    • 고급 리듀싱 기능을 수행
  • Collector 인터페이스를 implement하고 구현하여 스트림의 요소를 어떤 식으로 도출할지 지정한다.
  • 스트림에 collect()를 호출하면 스트림의 요소에 (컬렉터로 파라미터화된) 리듀싱 연산이 수행된다.
    • 즉 collect에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리한다.
    • 따라서 Collector 인터페이스의 메서드를 어떻게 구현하느냐에 따라 스트림에 어떤 리듀싱 연산을 수행할지 결정

미리 정의된 컬렉터

Collectors 유틸티리 클래스에는 자주 사용하는 컬렉터 인스턴스를 쉽게 생설할 수 있는 정적 팩터리 메서드를 제공한다.

Collectors에서 제공하는 메서드의 기능은 크게 세가지

  1. 스트림 요소를 하나의 값으로 reduce하고 요약
  2. 스트림 요소들의 그룹화
  3. 스트림 요소들의 분할 (이것도 결국 bool 그룹화)

첫번째, Reducing과 요약

다양한 계산을 수행할 때 사용하는 유용한 컬렉터

  • 스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산

counting()

  • 다른 컬렉터와 함께 사용할 때 유용하다.
menu.stream().collect(Collectors.counting())
menu.stream().count()

maxBy(), minBy()

  • 스트림값에서 최댓값과 최솟값 검색할 때 사용하는 컬렉터
  • 해당 컬렉터는 스트림의 요소를 비교하는데 사용할 Comparator인수를 받는다
Optional<Dish> mostCalorieDish = menu.stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));

요약 연산

  • 필드의 합계나 평균 등을 반환하는 연산
  • 리듀싱 기능이 자주 사용된다.
  • 대표적으로 Collectors.summingInt
public static <T> Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper) {
  return new CollectorImpl<>(
          () -> new int[1],
          (a, t) -> { a[0] += mapper.applyAsInt(t); },
          (a, b) -> { a[0] += b[0]; return a; },
          a -> a[0], CH_NOID);
}
@FunctionalInterface
public interface ToIntFunction<T> {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    int applyAsInt(T value);
}
int totalColories = menu.stream().collect(summingInt(Dish:getCalories));
  • 위의 코드처럼 객체를 int로 매핑하는 함수를 인수로 받는다.
  • 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환한다.
  • summingInt가 collect 메서드로 전달되면 요약 작업을 수행한다.
  • 이외에도 averagingInt 메서드도 존재한다.

만약 두 개 이상의 연산을 한번에 수행해야 할 때는?

  • summarizingInt()
IntSummaryStatistics statistic = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));

문자열 연결

  • joining()
  • 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.
menu.stream().map(Dish::getName).collect(Collectors.joining(", "));

범용 리듀싱 요약 연산

이러한 모든 Collector는 reducing 팩토리 메서드로도 정의할 수 있다. (가독성이나 편리성 측면에서 권장하지 않는다)

  • reducing()
public static <T, U> Collector<T, ?, U> reducing(
        U identity,
        Function<? super T, ? extends U> mapper,
        BinaryOperator<U> op) {
    ...
}
  • 첫 번째 인수 : reducing 연산의 초기값. 스트림에 인수가 없을 때는 반환값.
  • 두 번째 인수 : 요소를 변환하는 변환함수.
  • 세 번째 인수 : 같은 종류의 두 항목을 하나의 값으로 더하는 합계 함수(BinaryOperator.)
  • 예시 - 모든 메뉴 요소의 칼로리 합계
    menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j)
    menu.stream().collect(Collectors.reducing(0, Dish::getCalories, Integer::sum)

한개짜리 인수를 갖는 reducing()

public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op) {
    ...
}
  • 초기값은 스트림의 첫 요소
  • 두번째 인수는 자신을 그대로 반환하는 항등 함수
  • 따라서 빈 스트림인 경우를 대비해 Optional를 반환한다.

잠깐 BinaryOperator을 살펴보자

public interface BinaryOperator<T> extends BiFunction<T,T,T> {
}

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}
  • 즉 두 인수의 타입과 반환하는 타입은 같아야 한다.
    • 따라서 아래와 같은 사용은 컴파일 error
menu.stream().collect(Collectors.reducing((d1, d2) -> d1.getName() + d2.getName()))

collect vs reduce

  • 이 두가지 메서드로 같은 기능을 구현할 수 있지만 의미론적인 문제와 실용성 문제 등 몇가지 문제가 있다.
  • 의미론적인 문제
    • collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드
    • reduce 메서드는 두 값을 하나로 도출하는 불변형 연산
  • 실용성 문제
    • 여러 스레드가 동시에 같은 데이터 구조체를 고치면 리스트 자체가 망가져버리므로 리듀싱 연산을 병렬로 수행할 수 없다.
    • 이를 피하기 위해 매번 리스트를 새로 할당하고 객체를 할당하느라 성능이 낮을 것 이다.

두번째, 그룹화

맨 처음 트랜잭션 currency 그룹화 예제를 생각해보면 명령형으로 그룹화를 구현하려면 까다롭고, 에러도 신경써야 하고, 코드가 길다.

  • Collectors.groupingBy 이용하면 쉽게 그룹화가 가능하다.
  • gropingBy 메서드의 인수로 전달되는 함수를 기준으로 그룹화되므로 이를 분류 함수라고 부른다.

1. 타입에 따른 메뉴 그룹 구하기

Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(Collectors.groupingBy(Dish::getType));

2. 복잡한 기준으로 그룹화하기

  • 메서드 참조 대신 람다 사용
public enum CaloricLevel { DIET, NORMAL, FAT }

Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
    groupingBy(dish -> {
    if (dish.getCalories() <= 400) {
        return CaloricLevel.DIET;
    } else if (dish.getCalories() <= 700) {
        return CaloricLevel.NORMAL;
    } else {
        return CaloricLevel.FAT;
    }
    }));

3. 다수준 그룹화

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = 
    menu.stream().collect(
        groupingBy(Dish::getType,
            groupingBy(dish -> {
                if (dish.getCalories() <= 400) {
                    return CaloricLevel.DIET;
                } else if (dish.getCalories() <= 700) {
                    return CaloricLevel.NORMAL;
                } else {
                    return CaloricLevel.FAT;
                }
            })
        )
    );

// 결과
{FISH={NORMAL=[salmon], DIET=[prawns]}, OTHER={NORMAL=[fries, pizza], DIET=[rice, fruit]}, MEAT={FAT=[pork], NORMAL=[beef], DIET=[chicken]}}
  • n수준 그룹화를 통해 n 수준 맵이 된다.

4. 서브그룹으로 데이터 수집

  • 분류 함수 한 개의 인수를 갖는 groupingBy(f)는 사실 groupingBy(f, toList())의 축약형이다.
  • 따라서 요리의 종류를 분류하는 컬렉터로 메뉴에서 가장 높은 칼로리를 가진 요리를 찾는 로직도 구현할 수 있다.
Map<Dish.Type, Optional<Dish>> mostCaloricByType = 
    menu.stream().collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));

// 결과
{OTHER=Optional[pizza], MEAT=Optional[pork], FISH=Optional[salmon]}

5. 컬렉션 결과를 다른 형식에 적용하기

  • collectingAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다.
  • 반환되는 컬렉터는 기존 컬렉터의 래퍼 역할을 하며 collect 마지막 과정에서 변환 함수로 자신이 반환하는 값을 매핑한다.
Map<Dish.Type, Dish> mostCaloricByType =
    menu.stream()
        .collect(groupingBy(Dish::getType,  //  분류함수
                 collectingAndThen(maxBy(comparingInt(Dish::getCalories)),  //  감싸인 컬렉터
                 Optional::get)));  //  변환함수. Optional에 포함된 값을 추출

세번째, 분할

분할은 분할 함수(프레디케이트)를 분류 함수로 사용하는 특수한 그룹화다

  • 프레디케이트를 사용하므로 맵의 키 형식은 bool
  • 따라서 두 개의 그룹으로 분류된다.

채식요리와 아닌 요리 분류

Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));

// 결과 : {false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season fruit, pizza]}

분할의 장점

  • filter를 사용해서도 간단히 원하는 내용을 얻을 수 있는데 왜 사용해야하는가?
  • 참, 거짓 두가지 요소의 스트림 리스트를 모두 유지한다는 것이 장점이다.
    • filter는 말그대로 필터링하여 보여주는 것이고, 분할은 데이터를 분할하는 용도로 사용하는 것이다.
  • 두 번째 인수로 컬렉터를 사용하는 오버로드 메서드도 있다.
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = menu.stream().collect(
    partitioningBy(Dish::isVegetarian,  //  분할 함수
        groupingBy(Dish::getType)));    //  두 번째 컬렉터

// 결과 : 
{false={MEAT=[pork, beef, chicken], FISH=[prawns, salmon]}, 
 true={OTHER=[french fries, rice, season fruit, pizza]}}

지금까지 살펴본 모든 Collector는 Collector 인터페이스를 구현한다. Collector 인터페이스를 자세히 살펴보자.

Collector 인터페이스란?

  • reducing 연산(Collector)를 어떻게 구현할지 제공하는 메서드 집합으로 구성된 인터페이스.
  • Collector 인터페이스를 직접 구현하면 문제를 더 효율적으로 해결할 수 있다.
public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();
    Set<Characteristics> characteristics();
  • T는 수집된 스트림 항목의 제네릭 타입
  • A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 타입
  • R은 수집 연산 결과 객체의 타입
    • 주로 컬렉션 타입

toList()를 확인해보면서 파악해보자!!

public static <T> Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

supplier 메서드

새로운 결과 컨테이너 만들기

  • supplier 메서드는 빈 결 이루어진 Supplier를 반환해야 한다.
@FunctionalInterface
public interface Supplier<T> {
    T get();
}
  • 즉, supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수다.
  • toList()에서 supplier()은 빈 리스트를 반환한다.
public Supplier<List<T>> supplier() {
    return () -> new ArrayList<T>();
}

public Supplier<List<T>> supplier() {
    return ArrayList::new;
}

accumulator 메서드

결과 컨테이너에 요소 추가하기

@FunctionalInterface
public interface BiConsumer<T, U> {
    void accept(T t, U u);
  • 스트림에서 n번째 요소를 탐색할 때 누적자와 n번째 요소를 함수에 적용한다.,
  • 함수의 반환값은 void, 즉 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없다.
  • toList()
public BiConsumer<List<T>, T> accumulator() {
    return (list, item) -> list.add(item);
}

public BiConsumer<List<T>, T> accumulator() {
    return List::add;
}

finisher 메서드

최종 변환 값을 결과 컨테이너로 적용하기

  • finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수(Function<T, R>)를 반환해야 한다.
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

        static <T> Function<T, T> identity() {
                return t -> t;
        }
}
  • 때로 누적자 객체가 이미 최종 결과인 상황도 있는데 이럴 때는 변환 과정이 필요하지 않으므로 finisher 메서드는 항등 함수를 반환한다.
    • toList()도 마찬가지
public Function<List<T>, List<T>> finisher() {
    return Function.identity();
}

위 3가지 메소드는 순차적 스트림 리듀싱 기능을 수행할 수 있다. 하지만 실제로 collect가 동작하기 전, 다른 중간 연산과 파이프라인을 구성할 수 있게 해주는 게으른 특성 그리고 병렬 실행 등도 고려해야 하므로 스트림 리듀싱 기능 구현은 생각보다 복잡하다.

combiner 메서드 : 두 결과 컨테이너 병합

  • combiner는 리듀싱 연산에서 사용할 함수를 반환하는 메서드이다.
  • combiner는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할 지 정의한다.
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
}

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}
  • toList()의 경우
    • 스트림의 두번째 서브 파트에서 수집한 항복 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 된다.
public BinaryOperator<List<T>> combiner() {
    return (list1, list2) -> {
        list1.addAll(list2);
        return list1;
    }
}
  • combiner 메서드를 이용하면 스트림의 리듀싱을 병렬로 수행할 수 있다.
  • 이 때 자바7의 포크/조인 프레임워크와 Spliterator를 사용한다.

  1. 스트림의 분할을 정의하는 조건이 성립할 경우 원래 스트림을 재귀적으로 분할한다.
  2. 서브스트림의 각 요소에 리듀싱 연산을 순차적으로 적용하여 서브스트림을 병렬로 처리한다.
  3. combiner 메서드가 반환하는 함수를 통해 각 서브스트림의 결과를 합쳐 연산을 완료한다.

Characteristics 메서드

  • Characteristics 메서드는 컬렉터의 연산을 정의하는 Charactieristics 형식의 불변 집합을 반환한다.
  • Characteristics는 스트림을 병렬로 리듀스할건지, 그리고 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.
  • Characteristics는 다음 세 항목의 특성을 갖는 열거형이다.

UNORDERED

  • 리듀싱 결과는 스트림의 방문 순서나 누적 순서에 영향을 받지 않는다.

CONCURRENT

  • 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다.
  • 컬렉터의 플래그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정렬되어 있지 않은(집합처럼 요소의 순서가 무의미한) 상황에서만 병렬 리듀싱을 수행할 수 있다.

DENTITY_FINISH

  • finisher 메서드가 반환하는 함수는 identity를 적용할 뿐이므로 이를 생략할 수 있다.
  • 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있다.
  • 또한 누적자 A를 결과 R로 안전하게 형변환 할 수 있다.
  • toList()
    • 마찬가지로 스트림의 요소를 누적하는데 사용한 리스트가 최종 결과 형식이므로 추가 변환이 필요 없다.
    static final Set<Collector.Characteristics> CH_ID
                = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));

요약

  • collect는 스트림의 요소를 요약 결과로 누적하는 다양한 방법(컬렉터)을 인수로 갖는 최종 연산이다.
  • 다양한 컬렉터 등이 Collections 유틸 클래스에 미리 정의되어 있다.
  • 컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계되어 있다.
  • Collector 인터페이스에 정의된 메서드를 구현해서 커스텀 컬렉터를 개발할 수 있다,

참고 출처

728x90
728x90

스트림 API가 지원하는 다양한 연산들을 살펴보자.

스트림의 요소 걸러내기


filter()

  • Predicate로 필터링
  • filter 메서드는 Predicate를 인수로 받아서 Predicate와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
    Stream<T> filter(Predicate<? super T> predicate);
  • 다른 조건으로 여러 번 사용 가능하다.
    intStream.filter(i -> i%2 != 0 && i%3 != 0)...
    intStream.filter(i -> i%2 != 0).filter(i%3 != 0)...

distinct()

  • 고유 요소로 이루어진 스트림을 반환한다.
    • 즉 중복된 요소들을 제거
    • 고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다.
    Stream<T> distinct();

정렬


sorted()

  • 스트림을 정렬할 때 사용하는 메서드
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
  • Comparator 대신 int 값을 반환하는 람다식을 사용하는 것도 가능하다.
  • Comparator를 지정하지 않으면 스트림 요소의 Comparable으로 정렬한다.
    • 만약 Comparable을 구현한 클래스가 아니라면예외가 발생한다
  • jdk 1.8부터 Comparator 인터페이스에 정렬에 도움을 주는 static 메서드와 디폴트 메서드가 많이 추가되었다.
    • default
      • reversed
      • thenComparing
      • theComparingInt..
    • static
      • comparing
      • comparingInt..
  • 예를 들어 선수 스트림을 팀별, 성적순, 이름순으로 정렬하려면??
playerStream.sorted(Comparator.comparing(Player::getTeam)
        .thenComparing(Player::getScore)
        .thenComparing(Player::getName))
        .forEach(System.out::println);

스트림 슬라이싱


자바9에서 추가된 메서드인 takeWhile(), dropWhile()

takeWhile()

  • Predicate를 통한 슬라이싱이다.
  • 무한 스트림을 포함한 모든 스트림에 Predicate를 적용해 Predicate의 결과가 true인 동안 요소를 가져와 스트림을 슬라이스 할 수 있다.
  • 즉 Predicate와 일치하지 않는 요소를 발견하면 스트림의 나머지 부분이 버려진다.
    default Stream<T> takeWhile(Predicate<? super T> predicate)
  • 칼로리가 300보다 작은 요소들 구성된 스트림으로 슬라이스
    List<Food> slicedMenu = calorieSortedMenu.stream()
                             .takeWhile(dish -> dish.getCalories() < 300)
                                                            .collect(toList());

dropWhile()

  • Predicate를 통한 슬라이싱이다.
  • takeWhile()과 정반대의 작업을 수행한다.
  • Predicate이 처음으로 거짓이 되는 지점까지 발견된 요소를 버리고 작업을 중단하고 남은 모든 요소를 반환한다.
  • 마찬가지로 무한스트림에서도 동작
    default Stream<T> dropWhile(Predicate<? super T> predicate) 
  • 300 칼로리보다 높은 요소들로 구성된 스트림으로 슬라이스
    List<Food> slicedMenu = calorieSortedMenu.stream()
                         .dropWhile(dish -> dish.getCalories() < 300)
                         .collect(toList());

limit()

  • 스트림 축소
  • 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환한다.
  • 정렬되지 않은 스트림에도 사용가능하다.
    Stream<T> limit(long maxSize);

skip()

  • 요소 건너뛰기
  • 처음 n개 요소를 제외한 스트림을 반환한다.
  • 만약 n개 이하의 요소를 포함하는 스트림에 호출하면 빈 스트림이 반환된다.
    Stream<T> skip(long n);

매핑


특정 객체에서 특정 데이터를 선택하는 처리 과정에서 자주 수행되는 연산

Map()

  • 스트림의 각 요소에 함수 적용하기 위한 메서드
  • Function을 인수로 받아 각 요소에 적용한 결과가 새로운 요소로 매핑된 스트림을 반환한다.
  • 기본형 요소에 대한 mapToType 메서드도 지원한다 (mapToInt, mapToLong, mapToDouble).
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
  • 각 단어들의 글자 수의 리스트를 반환
    List<Integer> wordLengths = Arrays.asList("one", "two", "three").stream()
                                    .map(String::length)
                                    .collect(toList());

flatMap()

  • 스트림의 요소가 배열기나 map()의 연산결과가 배열인 경우, 즉 스트림의 타입이 Stream<T[]>인 경우 Stream<T>로 다루고 싶거나 Stream가 더 편리할 때 사용하는 메서드

  • 스트림의 각 값을 다른 스트결과적으로 하나의 평면화된 스트림을 반환한다.


한번 살펴보자!!

  • 해당 String 중에 길이가 5 이상인 String을 출력해보자
String[][] wordArr = new String[][]{
     new String[]{"team", "victory", "fighting"},
     new String[]{"flatMap", "is", "too", "difficult"}
};
  • map 사용
    Arrays.stream(wordArr) // Stream<String[]>
                    .map(innerArray -> Arrays.stream(innerArray)) // Stream<Stream<String>>
                    .forEach(innerStream -> innerStream.filter(word -> word.length() >  5) // Stream<String>
                            .forEach(System.out::println));

)))

  • flatMap 사용
    Arrays.stream(wordArr) //  Stream<String[]>
                    .flatMap(innerArray -> Arrays.stream(innerArray)) // // Stream<String>
                    .filter(word -> word.length() > 5)
                    .forEach(System.out::println);

))


stream으로 카드쌍 만들기

List<String> kinds = Arrays.asList("스페이드", "하트", "다이아몬드", "클로버");
List<String> nums = Arrays.asList("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K");
List<String[]> cards = kinds.stream()
                .flatMap(kind -> nums.stream()
                         .map(num -> new String[] {kind, num}))
                .peek(arr -> System.out.println(Arrays.toString(arr)))
                            .collect(Collectors.toList());
  • peek는 forEach와 달리 스트림 요소를 소모하지 않는다.


검색과 매칭 (쇼트 서킷)


특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리 유틸리티 메서드를 제공한다.

anyMatch

  • 적어도 한 요소와 일치하는지 확인하는 메서드
  • boolean 반환값이므로 최종 연산이다.
boolean anyMatch(Predicate<? super T> predicate);

allMatch

  • 모든 요소와 일치하는지 검사하는 메서드
  • 마찬가지로 최종 연산이다
boolean allMatch(Predicate<? super T> predicate);

noneMatch

  • allMatch와 반대 연산
  • 즉, 모든 요소가 Predicate 와 일치하지 않으면 true
boolean noneMatch(Predicate<? super T> predicate);

anyMatch, allMatch, noneMatch 세 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다.

findAny

  • 현재 스트림에서 임의의 요소를 반환한다.
  • 마찬가지로 최종연산이며 쇼트서킷을 이용해서 결과를 찾는 즉시 실행 종료
Optional<T> findAny();

findFirst

  • 첫 번째 요소를 찾아 반환한다. 순서가 정해져 있을 때 사용한다.
Optional<T> findFirst();

그렇다면 findAny와 findFirst는 언제 사용하는 메서드일까??

  • 바로 병렬성 때문이다.
  • 병렬 실행에서는 첫 번째 요소를 찾기 어렵기 때문에 findFirst 메서드를 사용하고
  • 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

참고 - Optional이란??

  • Optional는 값의 존재나 부재 여부를 표헌하느 컨테이너 클래스이다.
  • 앞서 본 findAny 혹은 findFirst 메서드는 아무 요소도 반환하지 않을 수 있으므로 NPE가 발생할 수 있다.
  • 주요 메서드
    • isPresent() : Optional이 값을 포함하면 true, 아니라면 false
    • ifPresent(Consumer<T> block) : 값이 있으면 주어진 블록 실행
    • T get() : 값이 존재하면 값 반환, 없으면 NoSuchElementException
    • T orElse(T other) : 값이 있으면 값 반환, 없으면 기본값 반환

리듀싱


reduce

  • 모든 스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환하는 메서드
  • reduce()는 두개의 인수를 갖는다.
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
  • 초기값 T
  • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator
  • combiner : 병렬처리된 결과를 합치는데 사용할 연산(병렬 스트림)
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}

 @FunctionalInterface
 public interface BinaryOperator<T> extends BiFunction<T,T,T> { ... }
  • 초기값스트림의 첫 요소를 가지고 연산한 결과(누적 값, accumulated value)를 가지고 그 다음 요소와 연산한다.
  • 이 과정에서 스트림의 요소를 하나씩 소모하게 되며, 모든 요소를 소모하게 되면 그 결과를 반환한다.
  • 최정연산 count(), sum() 등은 내부적으로 모두 reduce()를 이용해서 작성된 것이다.

reduce를 이용한 요소의 합

int sum = 0;
for(int x : numbers) {
        sum += x;
}
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int sum = numbers.stream().reduce(0, Integer::sum);


이미지 출처

  • 0 + 4 의 결과인 4가 새로운 누적값

  • 누적값으로 람다를 다시 호출하며 다음 요소인 5를 소비

  • 반복하며 마지막 요소 9로 람다를 호출하면 최종적으로 21


초기값이 없는 경우

Optional<T> reduce(BinaryOperator<T> accumulator);
  • 처음 두 요소를 가지고 연산한 누적값을 사용

  • Optional 객체 반환한다.

    • 왜일까??
    • 스트림에 아무 요소도 없는 경우 초기값조차 없다면 합계가 없음을 나타낼 수 있도록 Optional 객체로 감싼 결과를 반환

최대값과 최소값

Option<Integer> max = numbers.stream().reduce(Integer::max);
Option<Integer> min = numbers.stream().reduce(Integer::min);

map - reduce

int count = menu.stream().map(d -> 1).reduce(0, (a, b) -> a + b);

map과 reduce를 연결하는 기법을 맵 리듀스 패턴이라 하며, 쉽게 병렬화하는 특징을 이용한 것을 구글이 발표하면서 유명해졌다.

기존 코드에 비해 reduce 메서드의 장점은??
바로 reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다. 기존 코드에서는 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어렵고 동기화의 cost가 매우 크기 때문이다.

스트림 연산 : 상태 없음 vs 상태 있음

  • 스트림을 이용해서 연산을 쉽게 구현할 수 있으며 parrell 메서드를 통해 쉽게 병렬성을 얻을 수 있다.

  • 하지만 스트림 연산은 각각 다양한 연산을 수행하기 때문에 내부적인 상태를 고려해야 한다.

  • 내부 상태가 없는 연산

    • 사용자가 제공한 람다나 메서드 참조가 내부적인 가변 상태를 갖지 않는다는 가정하에
    • map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.
    • 따라서 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산이다.
  • 내부 상태가 있는 연산

    • reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다.
    • 스트림에서 처리하는 요소 수와 관계없이 내부 상태의 크기는 한정되어 있다.
    • sorted, distinct는 값을 비교하기 위해 모든 요소가 버퍼에 추가되어 있어야 함.

숫자형 스트림


  • 스트림 API는 오토박싱 & 언박싱으로 인한 비용을 줄이기 위해 기본형 특화 스트림(primitive stream specialization)을 제공한다.

  • sum, max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다.


기본형 특화 스트림

  • 기본형 특화 스트림으로 IntStream, DoubleStream, LongStream이 존재한다.
  • 각각의 인터페이스에는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 메서드를 제공한다.
int sum()
OptionalDouble average()
Optional*Int* max()
Optional*Int* min()
  • 해당 메서드들은 최종연산인 것을 잊지 말아야 한다

  • sum 메서드를 제외한 나머지 메서드들은 요소가 없을 때 0을 반환할 수 없으므로 이를 구분하기 위해 Optional 래퍼 클래스를 반환

  • Optional도 기본형에 대하여 지원한다. OptionalInt, OptionalDouble, optionalLong 세 가지 기본형 특화 스트림 버전의 Optional이 제공된다.


숫자 스트림으로 매핑

IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

객체 스트림으로 복원하기

  • boxed메서드를 이용하면 특화 스트림을 일반 스트림으로 변환할 수 있다.
Stream<Integer> boxed(); // IntStream to Stream<Integer>
<U> Stream<U> mapToObj(IntFunction<? extends U> mapper); // IntStream to Stream<U>

스트림 만들기


이제 스트림으로 작업하기 위해 스트림을 생성하는 다양한 방법들을 알아보자

컬렉션

  • 컬렉션의 최고 조상인 Collection 인터페이스의 stream()
default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

값으로 스트림 만들기

  • 정적 메서드 Stream.of 을 이용하여 스트림을 만들 수 있다.
public static<T> Stream<T> of(T... values) {
        return Arrays.stream(values);
}
Stream<String> strStream = Stream.of("hello", "world";)

null이 될 수 있는 객체로 스트림 만들기

  • 자바 9부터 지원되며 Stream.ofNullable 메서드를 이용하여 null이 될 수 있는 객체를 지원하는 스트림을 만들 수 있다.
  • null이면 빈 스트림 반환
public static<T> Stream<T> ofNullable(T t) {
         return t == null ? Stream.empty()
                                            : StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}

배열로 스트림 만들기

  • 배열을 인수로 받는 정적 메서드 Arrays.stream 을 이용하여 스트림을 만들 수 있다.
public static <T> Stream<T> stream(T[] array)
public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive)
public static *Int*Stream stream(int[] array)
public static *Int*Stream stream(int[] array, int startInclusive, int endExclusive)

특정 범위의 정수 스트림 만들기

  • 특정 범위의 숫자를 이용해야 할 때 rangerangeClosed 메서드를 사용할 수 있다.
  • IntStream, LongStream 두 기본형 특화 스트림에서 지원된다.
  • range는 end가 범위에 포함되지 않으며, rangeClosed는 포함한다.
public static IntStream range(int startInclusive, int endExclusive)
public static IntStream rangeClosed(int startInclusive, int endInclusive)

파일로 스트림 만들기

  • 자바의 NIO API(논블록 I/O)도 스트림 api를 활용할 수 있다.
  • java.nio.file.Files의 많은 static 메서드가 스트림을 반환한다.
Stream<Path> Files.list(Path dir)
Stream<String> Files.lines(Path path)

함수로 무한(언바운드) 스트림 만들기

  • Stream.iterateStream.generate를 통해 함수를 이용하여 무한 스트림을 만들 수 있다.
  • iterate와 generate에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다.
  • 따라서 무제한으로 값을 계산할 수 있지만, 보통 무한한 값을 출력하지 않도록 limit(n) 함수를 함께 연결해서 사용한다.
  • Stream.iterate
    • 초기값과 람다식 f에 의해 계산된 결과를 다시 seed 값으로 해서 계산을 반복한다.
    public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
    public static<T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)
    Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2) // 0, 2, 4, 6, ...
    // 0 -> 0 + 2
    // 2 -> 2 + 2
    // 4 -> 4 + 2
    // ....
  • Stream.generate
    • Supplier를 인수로 받아서 새로운 값을 생산한다.
      public static<T> Stream<T> generate(Supplier<? extends T> s)
    Stream<Double> randomStream = Stream.generate(Math::random);
  • Random 클래스의 메서드들 이용
    public IntStream ints(long streamSize)
    public LongStream longs(long streamSize)
    public DoubleStream doubles(long streamSize)

    InStream intStream = new Random().ints(); // 무한 스트림 o
    InStream intStream = new Random().ints(5); // 무한 스트림 x

무한 스트림을 이용해서 피보나치 수열을 만들어보자

  • iterate()
    IntStream.iterate(new int[] {0, 1}, t -> new int[] {t[1], t[0] + t[1]})
          .limit(20)
                .map(t -> t[0])
          .forEach(System.out::println);
  • generate()
    IntSupplier fib = new IntSupplier() {
            private int prev = 0;
            private int cur = 1;
            @Override
            public int getAsInt() {
                int oldPrev = this.prev;
                int nextValue = this.prev + this.cur;
                this.prev = this.cur;
                this.cur = nextValue;
                return oldPrev;
            }
    };

    IntStream.generate(fib)
              .limit(20)
  • IntSupplier 인스턴스는 기존 피보나치 요소와 두 인스턴스 변수에 어떤 요소가 들어있는지 추적하므로 가변 상태 객체다.
  • iterate를 사용했을 때는 각 과정에서 새로운 값을 생성하면서 기존 상태를 바꾸지 않는 순수한 불변 상태를 유지했다.
  • 스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 불변 상태 기법을 고수해야 한다.
728x90
728x90

스트림이 등장한 배경


  1. 거의 모든 자바 어플리케이션은 컬렉션을 이용하고 처리하기 위해서는 for문과 Iterator를 이용해서 코드를 작성해왔다.

    • 하지만 이러한 방식으로 작성된 코드는 길고 가독성이 좋지 않으며 재사용성 마저 떨어진다.
    • 데이터 소스마다 다른 방식으로 다뤄야한다. Collection이나 Iterator와 같은 인터페이스를 이용해서 표준화했지만, 각 컬렉션 클래스에는 같은 기능의 메서드들이 중복 정의 되어 있다.
      • ex) Collection.sort(), Arrays.sort()
  2. 많은 요소를 포함하는 컬렉션은 어떻게 처리해야 할까?

    • 멀티코어 아키텍처를 활용해서 병렬로 컬렉션의 요소를 처리해야 한다.
    • 하지만 병럴 처리 코드의 구현은 어렵고 복잡하며 디버깅도 어렵다.

스트림

컬렉션으로도 sql 질의처럼 고수준으로 추상화해서 처리할 수 있는 기능과 병렬 처리 기능을 만들 수 없을까??

List<Stirng> names = player.stream() 
        .filter(player -> player.getAge > 30) // 30살 이상 player 선택
        .map(Player::getName) // 요리명 추출
        .limit(3) // 선착순 3개만 추출
        .collect(toList()); // 스트림을 리스트로 변환

스트림이란 무엇인가?


스트림(Stream)은 자바 8 API에 새로 추가된 기능이다.

  • 스트림을 이용하면 선언형으로 컬렉션 데이터를 처리할 수 있다.
  • 여러 연산을 파이프라인으로 연결해 유연성이 좋다.
  • 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다.

스트림의 정의

스트림이란 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소 로 정의할 수 있다

연속된 요소 : 컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공한다.

  • 컬렉션 : 자료구조, 요소의 저장 및 접근 연산 위주
  • 스트림 : 표현 계산식 위주

소스: 스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비한다.

  • ex) 정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지된다.

데이터 처리 연산: 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산을 지원하며, 순차적으로 또는 병렬로 실행할 수 있다.

  • ex) filter, map, reduce, find, match, sort 등

스트림의 특징


스트림은 다음과 같은 주요 특성이 있다.

파이프라이닝

  • 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다.
  • 그 덕에 게으름(lazyness), 쇼트서킷(short-circuiting) 같은 최적화도 얻을 수 있다.

내부 반복

  • 반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원한다.

스트림과 컬렉션


자바의 기존 컬렉션과 자바8에서 추가된 스트림 모두 연속된(순차적, sequenced) 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다.


그렇다면 컬렉션과 스트림의 차이는 무엇일까??

가장 큰 차이는 데이터를 계산하는 시점이다

  • 컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조이다.
    • 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다.
    • 예를 들어 컬렉션에 요소를 추가하거나 삭제하는 연산을 수행하기 전에 컬렉션의 모든 요소들은 메모리에 미리 저장되어 있어야 하며 계산되어야 한다는 뜻
  • 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조이다.
    • 즉 사용자가 요청하는 값만 스트림에서 추출한다.
    • 스트림에 요소를 추가하거나 제거할 수 없다.
    • 이러한 특징으로 인해 생산자-소비자 관계를 형성하며 게으르게 만들이지는 컬렉션과 같다.
  • 책에서 예시를 든 것 처럼 다운로드 방식(컬렉션)과 스트리밍 방식(스트림)을 생각하면 차이를 이해하기 쉽다.

스트림은 일회용이다

  • 스트림은 Iterator와 마찬가지로 일회용이다.
  • 한 번 탐색한 요소를 다시 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야 한다.
    • 마찬가지로 반복 사용할 수 있는 데이터 소스여야 한다는 뜻

외부 반복과 내부 반복

  • 컬렉션 인터페이스를 사용하려면 사용자가 직접 요소를 반복해야 한다.
    • for-each나 Iterator를 이용해서 반복할 수 있으며 이를 외부 반복(external iteration)이라고 한다.
  • 스트림 라이브러리는 반복을 알아서 처리하고 결과 스트림 값을 저장해주는 내부 반복(internal iteration)을 사용한다.
    List<String> names = menu.stream()
                     .map(Dish::getName) // 요리명 추출
                     .collect(toList()); // 파이프라인 실행, 반복자 x


이미지 출처

  • 스트림의 내부 반복의 장점은 자동으로 병렬성 구현을 선택하기 때문에 병렬성을 쉽게 얻을 수 있다는 점이며, 내부적으로 더 최적화된 다양한 순서로 처리할 수 있는 점이다.

스트림 연산


스트림 인터페이스의 연산을 크게 두 가지로 구분할 수 있다.

중간 연산과 최종 연산

  • 중간 연산(intermediate operation): 연결할 수 있는 스트림 연산
  • 최종 연산(terminal operation): 스트림을 닫는 연산
    List<Stirng> names = menu.stream() // 요리 리스트에서 스트림 얻기
            .filter(dish -> dish.getCalories > 300) // 중간 연산 시작
            .map(Dish::getname)
            .limit(3) // 중간 연산 끝
            .collect(toList()); // 스트림을 리스트로 변환, 종단 연산

중간 연산

  • 중간 연산다른 스트림을 반환한다.
    • 따라서 여러 중간 연산을 연결해 질의를 만들 수 있다.
    • 연산 결과는 스트림이지만, 연산 전의 스트림과 같은 것은 아니다.
  • 중간 연산은 최종 연산을 스트림 파이프라인에 실행하기 전까지는 아무도 연산을 수행하지 않는다는 것이다.
    • 게으르다(lazy)는 것이다.
    • 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종 연산으로 한 번에 처리하기 때문이다.
  • lazy 특성에 의한 최적화 효과
    List<Stirng> names = menu.stream() // 스트림 open
            .filter(dish -> {
                Syste.out.println("filtering:" + dish.getName());
                return dish.getCalories > 300;
            })
            .map(dish -> {
                Syste.out.println("mapping:" + dish.getName());
                return dish.getName();
            })
            .limit(3)
            .collect(toList());
    filtering:pork
    mapping:pork
    filtering:beef
    mapping:beef
    filtering:chicken
    mapping:chicken
    [pork, beef, chicken]
  • 첫 번째는 쇼트 서킷이다.
    • 300칼로리가 넘는 요리는 여러개였다고 가정해보자
    • 그럼에도 오직 처음 3개만 선택되었다.
    • limit 연산 쇼트서킷이라 불리는 기법 덕분이다.
  • 두 번째는 루프 퓨전이다.
    • filter와 map은 서로 다른 연산이지만 한 과정으로 병합되었다.

최종 연산

  • 최종 연산은 스트림 파이프라인에서 결과를 도출한다.
    • 보통 List, Integer, void 등 스트림 이외의 결과가 반환된다.
    • reduce(), collect(), count(), forEach()
  • 스트림의 요소를 소모하므로 단 한번만 가능하다.

마무리


스트림의 이용과정은 세 가지로 요약 가능하다

  1. 질의를 수행할 데이터 소스
  2. 스트림 파이프라인을 구성할 중간 연산 연결
  3. 스트림 파이프라인을 실행하고 결과를 만들 최종 연산

스트림의 파이프라인의 개념은 빌더 패턴과 비슷하다.

  • 호출을 연결해서 설정을 만든다(스트림의 중간 연산 연결)
  • 준비된 설정에 build 메서드 호출(스트림의 최종 연산)
728x90
728x90

람다 표현식이란?


람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화 한 것.

  • 람다 표현식은 익명 클래스처럼 이름이 없는 함수면서 메서드를 인수로 전달할 수 있으므로 익명 클래스와 비슷하다고 일단 생각하며 이해를 하나씩 해보자
  • 람다 표현식은 이름은 가질 수 없지만 파라미터, 바디, 리턴 타입, 예외 리스트는 가질 수 있다.
  • 람다는 기술적으로 자바8 이전의 자바로 할 수 없었던 일을 제공하는 것이 아니라는 점을 명심하자, 코드가 간결해지고 유연해지는 것!

람다의 특징

  • 익명 : 이름이 없으므로 익명이라 표현하고 구현해야할 코드가 적고 간결하다.
  • 함수 : 메서드처럼 특정 클래스에 종속되지 않고 독립적인 기능을 하기 때문에 함수라고 불린다.
  • 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성 : 익명클래스처럼 로직과 필요없는 코드를 구현할 필요가 없다.

람다 사용법

  • 파라미터 + 화살표 + 바디로 이루어진다
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//   람다 파라미터
//                  화살표
//                                  람다 바디
(parameters) -> expression
          또는 블록스타일
(parameters) -> { statements; }

그렇다면 람다 표현식은 어디에 사용할 수 있을까??


람다 표현식은 함수형 인터페이스 문맥에서 사용할 수 있다.

함수형 인터페이스

함수형 인터페이스는 오직 하나의 추상메서드만 지정하는 인터페이스이다.

  • java.util.function, 기본제공 함수형 인터페이스
  • 함수형 인터페이스로 뭘 할 수 있을까?
    • 람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스를 구현한 클래스의 인스턴스 라고 취급 할 수 있다.
    • 람다를 이해하기 위해 가장 중요한 개념
interface Runnable {
        void run();
}
public class Main {
    public static void process(Runnable r){
        r.run();
    }
    public static void main(String[] args)
    {
        Runnable r1 = () -> System.out.println("Hello, r1");
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello, r2");
            }
        };

        r1.run(); // hello, r1
        r2.run(); // hello, r2
        process(() -> System.out.println("Hello, r3")); // hello, r3
    }
}

함수 디스크립터(function descriptor)

  • 함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 가리킨다.
  • 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다.
    • 예를들어 MyFunc 인터페이스의 함수 디스크립터는 (int, int) → int 이다.
  • 람다 표현식은 함수형 인터페이스를 인수로 받는 메서드에만 람다 표현식을 사용할 수 있다.

람다 활용 : 실행 어라운드 패턴


  • 자원 처리(예를 들면 데이터베이스의 파일 처리)에 사용하는 순환 패턴(recurrent pattern)은 자원을 열고, 처리한 다음에, 자원을 닫는 순서로 이루어 진다.
  • 설정(setup)과 정리(cleanup) 과정은 대부분 비슷하다. 즉, 실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 갖는다.
    • 설정 - 작업 a - 정리
    • 이와 같은 형식의 코드를 실행 어라운드 패턴이라고 한다.
public String processFile() throws IOException {
    try ( BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
        return br.readLine();   // 실제 작업 코드
    }
}

1단계 : 동작(메서드) 파라미터화를 기억해보자

  • 현재 코드는 파일에서 한 번에 한 줄만 읽을 수 있다.
  • 만약 한 번에 두 줄을 읽거나 가장 자주 사용되는 단어를 반환하려면 어떻게 해야 할까?
    • 기존의 설정, 정리 코드는 재사용 하고 싶고 processFile() 메서드만 다른 동작을 수행하도록 바꾸고 싶다.
  • 바로 processFile()을 동작 파라미터화 시키는 것이다.
  • 한 번에 두 줄 출력
    • BufferedReader를 인수로 받아서 String을 반환하는 람다 사용
    String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());

2단계 : 함수형 인터페이스를 이용해서 동작 전달

  • 함수형 인터페이스 자리에 람다를 사용할 수 있다.
  • 따라서 BufferedReader -> String 과 IOException을 던질(throw) 수 있는 시그니처와 일치하는 함수형 인터페이스를 만들어야 한다.
    • 이 인터페이스를 BufferedReaderProcessor라고 정의하자.
    @FunctionalInterface
    public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOException;
    }

    public String processFile(BufferedReaderProcessor p) throws IOException {
     // ...
    }

3단계 : 동작 실행

  • 이제 BufferedReaderProcessor에 정의된 process 메서드의 시그니처(BufferedReader -> String)와 일치하는 람다를 전달할 수 있다.
public String processFile(BufferedReaderProcessor p) throws IOException {
    try(BufferedREader br = new BufferedReader(new FileReader("data.txt"))) {
        return p.process(br);
    }
}

4단계 : 람다 전달

이제 람다를 이용해서 다양한 동작을 processFile 메서드로 전달할 수 있다.

String oneLine = processFile((BufferedReaer br) -> br.readLine());
String twoLine = processFile((BufferedReaer br) -> br.readLine() + br.readLine());

함수형 인터페이스 사용

자바8 라이브러리 설계자들은 java.util.function 패키지로 여러 가지 새로운 함수형 인터페이스를 제공

Predicate<T>
  • 조건식을 표현하는데 사용
  • T -> boolean
Supplier<T>
  • void를 받아 제네릭 형식 T로 반환
  • () -> T
Consumer<T>
  • Supplier 와 반대
  • T -> ()
Function<T, R>
  • 일반적인 함수, T를 받아 R을 반환
  • T -> R

기본형 특화

  • 자바의 모든 형식은 참조형 혹은 기본형
  • 하지만 제네릭은 내부 구현상 어쩔 수 없이 참조형만 사용 가능하다.
  • 그래서 박싱(기본형 -> 참조형)과 언박싱(참조형->기본형) 제공한다.
  • 박싱한 값은 기본형을 감싸는 래퍼이며 힙에 저장된다.
  • 따라서 박싱한 값은 메모리를 더 소비하며 기본형을 가져올 때도 메모리를 탐색하는 과정이 필요하다.
    • 오토박싱으로 저 과정은 자동으로 해주지만, 자원을 소모하게 된다.
    • 그래서 오토박싱을 피할 수 있는 버전의 함수형 인터페이스 제공
    • IntConsumer, LongConsumer...

형식 검사, 형식 추론, 제약


람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 포함되어 있지 않다.
따라서 람다 표현식을 더 제대로 이해하려면 람다의 실제 형식을 파악해야 한다.

대상형식

  • 어떤 컨텍스트에서 기대되는 람다 표현식의 형식
  • 람다 표현식이 예외를 던질 수 있다면, 추상 메소드도 같은 예외를 던질 수 있어야 함.

형식 검사

  • 람다가 사용되는 콘텍스트(context)를 이용해서 람다의 형식(type)을 추론할 수 있다.
List<Apple> heavierThan150g = filter(apples, (Apple apple) -> apple.getWeight() > 150**);
1. 람다가 사용된 콘텍스트는 무엇이지? filter의 정의 확인
   -> filter(List<Apple> apples, Predicate<Apple> p)

2. 대상 형식은 Predicate<Apple>이다.

3. Predicate<Apple> 인터페이스의 추상메서드는 무엇이지? 
   -> boolean test(Apple apple)

4. test 메소드의 Apple -> boolean 함수 디스크립터 묘사

5. 찾은 함수의 디스크립터가 전달된 람다 표현식과 일치하는지 확인

6. 형식 검사 성공적으로 완료

형식 추론

  • 자바 컴파일러는 람다 표현식이 사용된 컨텍스트(대상 형식)를 이용해서 함수 디스크립터를 알 수 있으므로 람다의 시그니처도 추론할 수 있다.
  • 따라서 컴파일러는 람다 표현식의 파라미터 형식을 추론할 수 있으므로 람다 문법에서 생략할 수 있다.
// 형식 추론 하지 않음
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

// 형식 추론 함
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

자유변수(free variable) 와 람다 캡처링(capturing lambda)

  • 지금까지 살펴본 모든 람다 표현식은 인수를 자신의 바디 안에서만 사용했다.
  • 하지만 람다 표현식에서는 익명 함수가 하는 것처럼 자유 변수(파라미터로 넘겨진 변수가 아닌 외부에서 정의된 변수)를 활용할 수 있다.
    • 이와 같은 동작을 람다 캡처링이라고 한다.
  • 다음은 portNumber 변수를 캡처하는 람다 예제이다.
    int portNumber = 1337;
    Runnable r = () -> System.out.println(portNumber);
  • 하지만 자유 변수에도 제약이 있다.
    • 람다는 인스턴스 변수정적 변수를 자유롭게 캡처(자신의 바디에서 참조할 수 있도록) 할 수 있다.
    • 하지만 그러러면 지역 변수명시적으로 final로 선언되어 있어야 하거나, 실질적으로 final로 선언된 변수와 똑같이 사용(effectively final)되어야 한다.
    • 한번만 할당할 수 있는 지역 변수를 캡처 가능
  • 아래는 portNumber에 값을 두 번 할당하므로 컴파일 할 수 없는 코드이다.
    int portNumber = 1337;
    // error
    Runnable r = () -> System.out.println(portNumber);
    portNumber = 31337;
  class CapturingTest {
    private int a = 1;

    public void test() {
        final int b = 2;
        int c = 3;
        int d = 4;

        final Runnable r = () -> {
            // 인스턴스 변수 a는 final로 선언돼있을 필요도, final처럼 재할당하면 안된다는 제약조건도 적용되지 않는다.
            a = 123;
            System.out.println(a);
        };
        r.run();
        // 지역변수 b는 final로 선언돼있기 때문에 OK
        final Runnable r2 = () -> System.out.println(b);
        r2.run();

        // 지역변수 c는 변수에 값을 재할당하지 않았으므로 OK
        final Runnable r3 = () -> System.out.println(c + " " + b);
        r3.run();

        // 지역변수 d는 final이 아니고 effectively final도 아니다.
        d = 12;
        final Runnable r4 = () -> System.out.println(d);
    }
}

이유가 무엇일까??

  1. 람다 표현식은 여러 쓰레드에서 사용할 수 있다.
  2. 힙 영역에 저장되는 인스턴스 변수와 달리 스택 영역에 저장되는 지역 변수는 외부 쓰레드에서 접근 불가능하다.
    1. 지역 변수에 바로 접근할 수 있다는 가정하에 변수를 할당한 스레드가 사라져서 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다.
  3. 따라서 원래 변수에 접근을 허용하는 것이 아니라 자유 지역 변수의 복사본을 제공하는데. 이를 람다 캡쳐링이라고 한다.
  4. 복사본은 원본의 값이 바뀌어도 알 수 없기 때문에 쓰레드 동기화를 위해 지역 변수는 final 또는 effectively final 상태여야 한다.

즉 가변 지역 변수를 새로운 스레드에서 캡쳐할 수 있다면 안전하지 않은 동작이 수행될 가능성이 생기기 때문!!!

메서드 참조


메서드 참조는 특정 람다 표현식을 축약한 것이라고 생각하면 좋다.

  • 메서드 참조를 이용하면 기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.
  • 람다식이 하나의 메서드만 호출하는 경우에는 메서드명을 참조함으로써 가독성을 높일 수 있다.
// (Apple apple) -> apple.getWeight()
Apple::getWeight

// () -> Thread.currentThread().dumpStack()
Thread.currentThread()::dumpStack()

// (str, i) -> str.substring(i)
String::substring

// (String s) -> System.out.println(s) 
System.out::println

// (String s) -> this.isValidName(s)
this::isValidName

메서드 참조의 세 가지 유형

메서드 참조는 세 가지 유형으로 구분할 수 있다.

  • 정적 메서드 참조
    • 람다 : (x) → ClassName.method(x)
    • 메서드 참조 : ClassName::method
    • ex) Integer::parseInt
  • 인스턴스 메서드 참조
    • 람다 : (obj, x) → obj.method(x)
    • 메서드 참조 : ClassName::method
    • ex) String::length
  • 기존 객체의 인스턴스 메서드 참조
    • 이미 생성된 객체의 메서드를 람다식에서 사용하는 경우
        MyClass obj = new MyClass();
        (x) -> obj.equals(x);
        obj::equals
- 람다 : (x) → obj.method(x)
- 메서드 참조 : obj::method
  • 컴파일러는 람다 표현식의 형식을 검사하던 방식과 비슷하게 함수형 인터페이스와 호환하는지 확인한다.
  • 즉, 메서드 참조는 콘텍스트의 형식과 일치해야 한다.

생성자 참조

생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있다.

Supplier<MyClass> s = () -> new MyClass();
Supplier<MyClass> s = MyClass::new;
MyClass obj1 = s.get();
  • 매개변수가 있는 생성자라면, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하거나 새로 정의해서 사용하면 된다.
Function<Integer, MyClass> f = (i) -> new MyClass(i);
Function<Integer, MyClass> f2 = MyClass::new;

Function<Integer, int[]> ff = x -> new int[x];
Function<Integer, int[]> ff2 = int[]::new;

요약


  • 람다 표현식은 익명 함수의 일종이다.
  • 함수형 인터페이스는 하나의 추상 메서드만을 정의하는 인터페이스이다.
  • 함수형 인터페이스를 기대하는 곳에서 람다 표현식을 사용할 수 있으며, 추상 메서드를 즉석으로 제공할 수 있다. 이 때 람다 표현식 전체가 함수형 인터페이스의 인스턴스로 취급한다.
  • 메서드 참조를 이용하면 기존의 메서드 구현을 재사용하고 직접 전달할 수 있다.

부족했던 부분(TODO)


Function의 합성과 Predicate의 결합

  • Function의 addThen, compose 디폴트 메서드 코드 이해 및 활용 해보기
  • Predicate의 and, or, negate 디폴트 메서드 코드 이해 및 활용 해보기

왜 람다에서 지역변수는 final or effectively final?

참고 문헌

728x90
728x90

동작 파라미터화


소비자의 요구사항은 항상 바뀐다. 이런 변화하는 요구사항에 대해 효과적으로 대응하기 위해서 동작 파라미터화(behavior parameterization) 를 이용하면 좋다.

  • 동작 파라미터화란 아직은 어떻게 실행할 것인지 결정하지 않은 코드 블록을 의미한다.
  • 코드 블록은 나중에 프로그램에서 호출한다.
    • 즉, 코드 블록의 실행은 나중으로 미뤄진다.
  • 따라서 메서드에 코드 블록을 인수로 전달할 수 있다면??
    • 메서드의 동작이 코드 블록 파라미터에 의해 결정된다.
    • 즉 동작이 파라미터화 된다

변화하는 요구사항 대응의 발전 단계


영화 관련 객체를 필터링하는 예시
자바 인 액션 책의 예제와 비슷한 예제로 해보았다.

public class Movie {
    private String name;
     private String director;
    private Genre genre;
    private int rating;

    // getters and setters omitted...
}
  • 영화를 평점으로 필터링하는 기능을 추가해보자!!

번째 시도 : rating을 파라미터화

public static List<Movie> filterMoviesByRating(List<Apple> movies, int rating) {
    List<Movie> result = new ArrayList<>();
     for(Movie movie : movies) {
        if(movie.getRating() > rating) {
            result.add(movie);
        }
    }
    return result;
}
  • 하지만 사용자가 평점 이외에도 감독, 장르, 등등 계속해서 추가되는 필터 조건이 생긴다면 어떻게 해야 할까?

두번째 시도: 가능한 모든 속성으로 필터링

절대 사용하지 말아야 하는 방법

public static List<Movie> filterMovies(List<Apple> movies,int rating, Genre genre, boolean flag) {
    List<Movie> result = new ArrayList<>();
    for(Movie movie : movies) {
        if((flag && movie.getRating() > rating) || 
           (!flag && movie.getGenre().equals(genre)){
            result.add(movie);
        }
    }
     return result;
}

List<Movie> TopRatedMovies = filterMovies(movies, 4, null, true);
List<Movie> horrorMovies = filterMovies(movies, 4, HORROR, false);
  • 메서드의 파라미터가 계속해서 늘어날 것이다.
  • 코드의 가독성이 매우 떨어지고 직관적이지 못하다.
  • 요구사항을 유연하게 대응할 수도 없는 구조
  • 계속 필터 조건이 추가된다면?
    • 중복된 필터 메서드를 여러개 만들거나
    • 위 처럼 하나의 거대한 필터 메서드를 구현해야 한다.
    • 요구사항의 변경이 없고 정해져있다면 혹시 괜찮을 수 있다고 생각하지만 보통은 아니기에 코드의 비용이 너무 크다.

세번째 시도: 동작 파라미터화

기존까지는 값을 파라미터화했다면 요구사항 변화에 유연하게 대응하도록 동작을 파라미터화를 사용해보자!

선택조건을 결정하는 인터페이스 정의

public interface MoviePredicate {
    boolean test(Movie movie);
}
public class TopRatedMoviePredicate implements MoviePredicate {
    public boolean test(Movie movie) {
        return movie.getRating() > 4;
    }
}

public class HorrorMoviePredicate implements MoviePredicate {
    public boolean test(Movie movie) {
        return movie.getGenre().equals(HORROR);
    }
}

public class MovieDirectorPredicate implements MoviePredicate {
    private String director;

    public MovieDirectorPredicate(String director) {
         this.director = director;
    }

    public boolean test(Movie movie) {
        return movie.getDiretor().equals(director);
    }
}

  • 이제 다양한 필터 조건을 대표하는 여러 MoviePredicate를 구현할 수 있다.
  • 위 조건에 따라 filter 메서드가 다르게 동작할 것이라고 예상할 수 있다.
  • 이를 전략 디자인패턴(strategy design patter)이라고 부른다.
  • 전략 디자인 패턴은 각 알고리즘(전략이라 부르는)을 캡슐화 하는 알고리즘 패밀리를 정의해둔 다음에 런타임에 알고리즘을 선택하는 기법이다.
  • 여기서는 MoviePredicate가 알고리즘 패밀리이며, 이를 구현한 클래스들이 전략이다.
public static List<Movie> filterMovies(List<Apple> movies, MoviePredicate p) {
    List<Movie> result = new ArrayList<>();
    for(Movie movie : movies) {
        if(p.test(movie)) {
            result.add(movie);
        }
    }
    return result;
}

List<Movie> horrorMovies = filterMovies(movies, new HorrorMoviePredicate());
List<Movie> bongMovies = filterMovies(movies, new MovieDirectorPredicate("Bong"));

  • filterMovies 메서드의 동작을 파라미터화했다.
    • 더 유연한 코드를 얻었으며 동시에 가독성도 좋아졌을 뿐 아니라 사용하기도 쉬워 졌다.
    • 즉, 전략 디자인 패턴(Strategy Design Pattern)과 동작 파라미터화를 통해서 필터 메서드에 전략(Strategy)을 전달 함으로써 더 유연한 코드를 만들었다.
  • 하지만 filterMovies에 새로운 동작을 전달하려면 MoviePredicate를 구현하는 클래스를 정의하고 인스턴스화 해야한다.
  • 이러한 코드들은 로직과 관련없는 코드들이며 복잡해보인다.
  • 이를 개선 하기 위해서 자바는 클래스의 선언과 인스턴스화를 동시에 수행할 수 있도록 익명 클래스 를 이용하여 개선 가능하다.

네 번째 시도 : 익명 클래스 사용

클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용)

List<Movie> TopRatedMovies = filterMovies(movies, new MoviePredicate() { 
    public boolean test(Movie movie) { // but 여기 까지 필요없는 코드
        return movie.getRating() > 4;
    }
});

List<Movie> horrorMovies = filterMovies(movies, new MoviePredicate() {
    public boolean test(Movie movie) { // but 여기 까지 필요없는 코드
        return HORROR.equals(movie.getGenre());
    }
});

List<Movie> bongMovies = filterMovies(movies, new MoviePredicate() {
    public boolean test(Movie mioive) { // but 여기 까지 필요없는 코드
        return "Bong".equals(movie.getDirector())
    }
});

  • 하지만 아직도 코드가 길고 장황하며 코드들은 여전히 보일러 플레이트이다.

다섯 번째 시도 : 람다 표현식 사용

MoviePredicate는 함수형 인터페이스이므로 람다 표현식 사용 가능하다.

List<Movie> horrorMovies = filterMovies(movies, (Movie m) -> HORROR.equals(movie.getGenre());
List<Movie> bongMovies = filterMovies(movies, (Movie m) -> "Bong".equals(movie.getDirector());
  • 마지막으로 지네릭을 이용해 참조 타입에 관계없이 추상적인 리스트의 필터 메소드를 만들 수 있다.
public interface Predicate<T> {
    boolean test(T t);
}

public static List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for(T e : list) {
        if(p.test(e)) {
            result.add(e);
        }
    }
    return result;
}
  • 람다를 사용해서 훨씬 간결해졌으며 복잡성 문제를 해결하였다!

요약


  • 동작 파라미터화는 메서드 내부적으로 다양한 동작을 수행할 수 있도록 코드를 메서드 인수로 전달한다.
  • 동작 파라미터화를 이용하면 의존성을 줄여 요구사항에 유연하고 유지보수가 용이한 코드작성이 가능하다.
  • 코드 전달 기법을 이용해 메서드의 인수로 전달할 수 있으나 자바8 이전까지는 코드가 간결하지 못했으나 람다를 이용하여 더욱 간결하고 직관적으로 구현할 수 있게 되었다.

공부하며 궁금했던점


Comparator 인터페이스는 왜 함수형 인터페이스인가?

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
}
  • equals()는 java.lang.Object에서 오버라이딩 되는 메서드들 중 하나이다.
    • 따라서 인터페이스가 java.lang의 공용 메서드 중 하나를 오버라이딩되는 추상 메서드를 선언하는 경우
    • 인터페이스 구현 시 java.lang에서 구현되기 때문에 인터페이스의 추상 메서드 개수에도 포함되지 않는다.

참고 문헌 및 출처


728x90
728x90

자바 8, 9, 10, 11에서 일어난 일


자바 역사를 통틀어 가장 큰 변화가 자바 8에서 일어났다.
자바8은 간결한 코드, 멀티코어 프로세서의 쉬운 활용 이 두가지 요구사항을 기반으로 한다.


자바 8에서 제공하는 새로운 기술

  • 스트림 API
  • 메서드에 코드를 전달하는 방법
  • 인터페이스의 디폴트 메서드

스트림을 이용하면 에러를 자주 일으키며, 멀티코어 CPU를 이용하는 것보다 비용이 훨씬 비싼 키워드 synchronized를 사용하지 않아도 된다.

메서드에 코드를 전달하는 기법을 사용하면 동작 파라미터화(behavior parameterization)를 구현할 수 있다.

메서드에 코드를 전달(뿐만 아니라 결과를 반환하고 다른 자료구조로 전달할 수 도 있음)하는 자바 8 기법은 함수형 프로그래밍(functional-style programming)에서 위력을 발휘한다.


자바 8 설계의 밑바탕을 이루는 세가지 프로그래밍 개념


스트림 처리(stream processing)

  • 스트림이란 한 번에 한 개씩 만들어지는 연속적인 데이터 항목들의 모임이다.

  • 자바 8에는 java.util.stream 패키지에 스트림 API가 추가 되었다.

    • 스트림 패키지에 정의된 Stream는 T형식으로 구성된 일련의 항목을 의미한다.
    • 데이터소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들이 정의되어 있다.
  • 스트림 API의 핵심은 기존에는 한 번에 한 항목을 처리했지만, 우리가 하려는 작업을 데이터베이스 질의 처럼 고수준으로 추상화해서 일련의 스트림으로 만들어 처리할 수 있다는 것이다.

    • 또한 파이프라인을 이용해서 입력 부분을 여러 CPU 코어에 쉽게 할당할 수 있어 스레드라는 복잡한 작업을 사용하지 않으면서도 공짜로 병렬성을 얻을 수 있다.

메서드에 코드 전달하기

  • 자바 8에 추가된 두 번째 프로그램 개념은 코드 일부를 API로 전달하는 기능이다.
  • 기존에 자바는 메서드를 다른 메서드의 파라미터로 전달 할 수 없었다.
    • 물론 정렬기능을 위해서 익명함수 형태로 Comparator를 구현하는 방법도 있지만 복잡하다.
  • 자바 8에서는 메서드를 다른 메서드의 파라미터로 전달 할 수 있다.
    • 이러한 기능을 이론적으로 동작 파라미터화라고 부른다.
  • 동작 파라미터화가 중요한 이유는 스트림 API는 연산의 동작을 파라미터화할 수 있는 코드를 전달한다는 사상에 기초하기 때문이다.

병렬성과 공유 가변 데이터

  • 세 번째 프로그래밍의 개념은 병렬성을 공짜로 얻을 수 있다라는 말에서 시작된다.
  • 병렬성을 공짜로 얻기 위해서는 다른 한가지를 포기해야하는데, 스트림 메서드로 전달하는 코드의 동작 방식을 조금 바꿔야 한다. 처음에는 불편하지만 나중에는 편하게 느껴질 것이다.
  • 스트림 메서드로 전달하는 코드는 다른 코드와 동시에 실행하더라도 안전하게 실행될 수있어야 한다.

보통 다른 코드와 동시에 실행 하더라도 안전하게 실행할 수 있는 코드를 만들려면 가변 데이터(shared mutable data)에 접근하지 않아야 한다. 이러한 함수를 순수(pure) 함수, 부작용 없는 함수(side-effect-free), 상태 없는(stateless) 함수 라고 부른다.


자바 함수


일급 시민과 이급 시민

  • 프로그래밍 언어의 핵심은 값을 바꾸는 것이다.
  • 전통적으로 프로그래밍 언어에서는 이 값을 일급(first-class) 값 또는 시민(citizens) 이라고 부른다.
  • 자바 프로그래밍 언어의 다양한 구조체(메서드, 클래스 같은)가 값을 구조를 표현하는데 도움이 될 수 있다.
    • 하지만 프로그램을 실행하는 동안 이러한 모든 구조체를 자유롭게 전달할 수는 없다.
    • 이렇게 전달할 수 없는 구조체는 이급 시민이다.
  • 자바 8에서는 이급 시민을 일급 시민으로 바꿀 수 있는 기능을 추가했다.

그래서 1급 객체가 뭐지??

일급객체(First-class Object)란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다.

다음과 같은 조건을 가진다.

  • 변수에 할당(assignment)할 수 있다.
  • 다른 함수를 인자(argument)로 전달 받는다.
  • 다른 함수의 결과로서 리턴될 수 있다.

메서드와 람다를 일급 시민으로

  • 메서드 참조(method reference)

    • ::
  • 이를 통해 메서드가 이급값이 아닌 일급값이다.

  • 메소드 블럭의 메모리상 주소 값

  • 람다 : 익명함수

  • 자바 8에서는 메서드를 일급 값으로 취급할 뿐 아니라 람다(또는 익명함수 anonymous functions)를 포함하여 함수도 값으로 취급할 수 있다.

  • 람다 문법 형식으로 구현된 프로그램을 함수형 프로그래밍이라고 한다.


스트림


  • 거의 모든 자바 애플리케이션은 컬렉션을 만들고 활용하지만 컬렉션으로 모든 문제가 해결되는 것은 아니다.
  • 예를 들어 고가의 트랜잭션(transaction)(거래) 만 필터링한 다음에 통화로 결과를 그룹화 해야 한다고 가정하자. 아래와 같은 많은 기본 코드를 구현해야한다.
    Map<Curreny, List<Transaction>> transactionByCurrencies = new HashMap<>(); // 그룹화된 트랜잭션을 더할 Map 생성
    for(Transaction transaction : transactions) {
      if(transaction.getPrice() > 1000) {
        Curreny curreny = transacation.getCurrency(); // 트랜잭션의 통화를 추출
        List<Transcation> transactionsForCurrency = transactionsByCurrencies.get(currency);
        if(transactionsForCurrency == null) {
          transactionsForCurrency = new ArrayList<>();
          transactionsByCurrencies.put(currenc, transcationsForCurrency);
        }
        transactionsForCurrency.add(transacation);
      }
    }
  • 위의 예제는 중첩된 제어 흐름 문장이 많아서 코드를 한 번에 이해하기 어렵다.
  • 스트림 API를 이용하면 다음처럼 문제를 해결할 수 있다.
    import static java.util.stream.Collectors.groupingBy;
    Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream()
                                                                  .filter((Transcations t) -> t.getPrice() > 1000); // 고가의 트랜잭션 필터링
                                                                  .collect(groupingBy(Transcation::geturrency)); // 통화로 그룹화

내부 반복과 외부 반복

  • 외부 반복(external iteration)은 for-each루프를 이용해서 각 요소를 반 작업을 수행하는 것들을 말한다.
  • 반면 내부 반복(internal iteration)은 스트림 API와 같이 루프를 신경 쓸 필요 없이, 스트림 API라는 라이브러리 내부에서 모든 데이터가 처리되는 것을 말한다.

멀티 스레딩은 어렵다

  • 자바 8은 스트림(API, java.util.stream)로 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제멀티코어 활용 어려움이라는 두 가지 문제를 모두 해결했다.
  • 컬렉션은 어떻게 데이터를 저장하고 접근할지에 중점을 두는 반면 스트림은 데이터에 어떤 계산을 할 것인지 묘사하는 것에 중점을 둔다.

포킹단계(forking step)

  • 예를들어 두 CPU를 가진 환경에서 리스트를 필터링할 때 한 CPU는 앞 부분을 처리하고, 다른 CPU는 리스트의 뒷 부분을 처리하도록 요청할 수 있는데 이 과정을 포킹 단계라고 한다.
  • 각각의 cpu는 자신이 맡은 절반의 리스트를 처리하고, 마지막으로 하나의 cpu가 두 결과를 정리한다.

자바 8에서 제공하는 두 가지 요술방망이

  • 흔히 사람들은 자바의 병렬성은 어렵고 synchronized는 쉽게 에러를 일으킨다고 생각한다. 자바8은 어떤 요술방망이를 제공할까?
  • 자바 8은 두 가지 요술 방망이를 제공한다.
    • 우선 라이브러리에서 분할을 처리한다. 즉, 큰 스트림을 병렬로 처리할 수 있도록 작은 스트림으로 분할한다. 또한 filter 같은 라이브러리 메서드로 전달된 메서드가 상호작용을 하지 않는다면 가변 공유 객체를 통해 공짜로 병렬성을 누릴 수 있다.
    • 상호작용을 하지 않는다는 제약은 프로그래머 입장에서 상당히 자연스러운 일이다. 함수형 프로그래밍에서 함수란 함수를 일급값으로 사용한다라는 의미도 있지만, 부가적으로 프로그램이 실행되는 동안 컴포넌트 간에 상호작용이 일어나지 않는다라는 의미도 포함한다.

디폴트 메서드와 자바 모듈


  • 자바 9의 모듈 시스템은 모듈을 정의하는 문법을 제공하므로 이를 이용해 패키지 모음을 포함하는 모듈을 정의할 수 있다. 또한 자바 8에서는 인터페이스를 쉽게 바꿀 수 있도록 디폴트 메서드를 지원한다.
  • 디폴트 메서드는 특정 프로그램을 구현하는 데 도움을 주는 기능이 아니라 미래에 프로그램이 쉽게 변화할 수 있는 환경을 제공하는 기능이다.
  • 어떻게 기존의 구현을 고치지 않고도 이미 공개된 인터페이스를 변경할 수 있을까라는 딜레마를 디폴트 메서드가 해소시켜준다.
  • 기존에는 인터페이스에 메서드가 하나 추가되면 인터페이스를 사용하는 모든 곳에서 메서드를 추가해야하지만, 디폴트 메서드는 구현하지 않아도 되는 메서드이다.
    • 메서드 본문(bodies)은 클래스 구현이 아니라 인터페이스 일부로 포함된다.(그래서 이를 디폴트 메서드라고 한다.)

함수형 프로그래밍에서 가져온 다른 유용한 아이디어

  • 자바 8에서는 NullPointer 예외를 피할 수 있도록 도와주는 Optional<T> 클래스를 제공한다.
  • Optional는 값을 갖거나 갖지 않을 수 있는 컨테이너 객체이다.
  • 어떤 변수에 값이 없을 때 어떻게 처리할지 명시할 수 있다.

참고 문헌

728x90

'java' 카테고리의 다른 글

[모던 자바 인 액션] 람다 표현식  (0) 2024.07.07
[모던 자바 인 액션] 동작 파라미터화  (0) 2024.07.07
static inner vs non-static inner class  (0) 2024.07.07
자바 제네릭스  (0) 2024.07.07
java 버전별 차이 & 특징  (0) 2024.07.07
728x90

중첩 클래스(Nested Class)는 무엇이고 4가지 종류 중 정적 내부 클래스비정적 내부 클래스에 대해 다뤄보고자 한다.

중첩 클래스란(Nested Class)

먼저 중첩 클래스에 대해 설명을 해야하는데, 중첩 클래스란 말 그대로 다른 클래스의 내부에 존재하는 클래스를 의미한다. 중첩클래스를 포함하는 외부 클래스를 Outer class라고 하며 내부에 포함된 클래스를 nested class 또는 Inner class라고 한다.

중첩 클래스는 4가지 종류가 존재한다.

  • 정적 멤버 클래스(static inner class) : static 키워드를 이용해서 클래스가 정의된 경우
  • 비정적 멤버 클래스(non-static inner class) : Outer 클래스의 멤버변수나 메소드처럼 클래스가 정의된 경우
  • 익명 클래스(anonymous class) : 익명 클래스를 이용해서 클래스가 정의된 경우
  • 지역 클래스(local class) : Outer 클래스의 특정 메소드 또는 초기화 블록에서 클래스가 정의된 경우
class Outer {
        class InstanceInner {} // non static inner class
        static class StaticInner {}  // static inner class

        void myMethod() {      // local class
                class LocalInner {}
        }

        // anonymous class
    Ex ex = new Ex() { 
      public void getEx() {
        System.out.println("anonymous class");
      }
    };
  }
}

interface Ex {
  public void getEx();
}

중첩 클래스는 특정 클래스를 자신의 클래스 내부적인 용도로만 사용하고자 할때 효율적이기 때문에 불필요한 노출을 줄이면서 캡슐화를 통해 유지 보수하기 좋은 코드를 작성하게 된다.

이펙티브 자바에서는 중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱 레벨 클래스로 만들어야 한다고 권장하고 있으며 특히 static 으로 선언하도록 권장한다. (아이템 24)

그렇다면 왜인지 알아보자!!

정적 멤버 클래스

  • static이 붙는 중첩 클래스
  • 동일한 static 멤버들을 사용 가능
  • static의 특징에 따라 외부 인스턴스 멤버의 직접참조가 불가능

static 예약어가 있음으로 인해 독립적으로 생성할 수 있다.

정적 멤버 클래스는 바깥 클래스의 private 멤버에도 접근할 수 있다는 점을 제외하고 일반 클래스와 쓰임새는 동일하다.

class Outer {
    static int x = 10;
    int y = 20;
    private static int z = 30;

    static class StaticInner {  // static inner class
        void get() {
            System.out.println("x: " + x);
            System.out.println("z: " + z);
        }
    }
}

Outer.StaticInner staticIneer = new Outer.StaticInner();
staticIneer.get();

비정적 멤버 클래스

  • Inner class라고 하며 외부 인스턴스에 대한 참조가 유지된다.
  • 외부 인스턴스는 내부 클래스를 new를 통한 인스턴스 할당으로 멤버변수처럼 사용할 수 있다.
  • 외부에 대한 참조가 유지되므로 내부 클래스도 외부 클래스의 자원을 사용할 수 있다.
class Outer {

    static int x = 10;
    int y = 20;
    public int z = 30;

    class InstanceInner {
        void get() {
            System.out.println("x: " + x);
            System.out.println("y: " + y);
            System.out.println("z: " + z);
        }
    }
}

Outer outer = new Outer();
Outer.InstanceInner insttanceInner = outer.new InstanceInner();
insttanceInner.get();

비정적 내부 클래스를 생성하는 경우에는 반드시 Outer 객체를 생성한 뒤 객체를 이용해서 생성해야 한다. 즉, 비정적 내부 클래스는 Outer 클래스에 대한 참조가 필요하다는 것이다.

그렇다면 왜 멤버 클래스는 static으로 선언하기를 권장할까?

바로 Outer 객체에 대한 참조 때문이다.

위의 예제처럼 InstanceInner 와 같은 중첩 클래스를 선언하면 인스펙터가 다음과 같이 경고를 해준다.

경고 주제는 메모리 누수 가능성이 있기 때문이다.

인용: 이펙티브 자바

정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어있고 없고 뿐이지만, 의미상 차이는 의외로 꽤 크다. 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결되기 때문에 바깥 클래스는 더 이상 사용되지 않지만 내부 클래스의 참조로 인해 GC가 수거하지 못해서 바깥 클래스의 메모리 해제를 하지 못하는 경우가 발생할 수 있기 때문이다. 이 문제는 IDE에서 경고해주기 때문에 흔하게 볼 수 있으며, 조슈아 블로흐가 이펙티브 자바에서도 강조하고 있는 내용이기도 하다.

비정적 멤버 클래스의 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며, 더 이상 변경할 수 없다. 이 관계는 바깥 클래스의 인스턴스 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어지는 게 보통이지만, 드물게는 직접 바깥 인스턴스의 클래스.new MemberClass(args)를 호출해 수동으로 만들기도 한다. 예상할 수 있듯, 이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸린다.

멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자. static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 된다. 앞서도 얘기했듯 이 참조를 저장하려면 시간과 공간이 소비된다. 더 심각한 문제는 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다는 점이다(아이템 7). 참조가 눈에 보이지 않으니 문제의 원인을 찾기 어려워 때때로 심각한 상황을 초래하기도 한다.

결론

  • static이 아닌 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다.
  • 왜냐하면 static이 아닌 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없기 때문이다.
  • 두 클래스의 관계는 멤버 클래스의 인스턴스 안에 만들어지며, 메모리를 차지한다. 생성도 느리다.
  • 바깥 클래스 인스턴스의 참조를 멤버 클래스가 갖고 있으므로, 바깥 클래스 인스턴스가 쓰레기 수거 대상에서 빠지게 된다.
  • 이는 메모리 누수를 불러일으킬 수 있는 치명적인 위험요소

결국 외부 인스턴스에 대한 참조가 필요하지 않고 내부 클래스가 독립적으로 사용된다면 static nested class로 만드는 것이 낫다.

참고 출처

728x90

'java' 카테고리의 다른 글

[모던 자바 인 액션] 동작 파라미터화  (0) 2024.07.07
[모던 자바 인 액션] 자바 8, 9, 10, 11에서 일어난 일  (0) 2024.07.07
자바 제네릭스  (0) 2024.07.07
java 버전별 차이 & 특징  (0) 2024.07.07
Object 클래스  (0) 2024.07.07
728x90

제네릭스(Generics)

제네릭스란 JDK 1.5부터 도입한 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법이다. 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스컴파일 시의 타입 체크를 해주는 기능이다.

제네릭스가 필요한 이유는 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다. 타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

장점

  • 타입 안정성을 제공한다.
  • 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

즉 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다는 이야기이다.


지네릭 클래스의 선언

지네릭 타입은 클래스와 메서드에 선언할 수 있다.

class Box<T> {
        T item;

        void setItem(T item) { this.item = item; }
        T getItem( return item; )
}

지네릭 클래스가 된 클래스의 객체를 생성할 때는 다음과 같이 참조변수와 생성자에 타입 T대신에 사용될 실제 타입을 지정해주어야 한다.

Box<String> b = new Box<String>(); // 타입 T 대신, 실제 타입을 지정
b.setItem(new Object())  // 에러, String이외에 타입은 지정불가
b.setItem("ABC")  // ok
String item = b.getItem(); // (String)b.getItem(); 처럼 형변환이 필요없음 

컴파일 후에 Box 과 Box 들은 이들의 '원시타입'인 Box로 바뀐다.

즉 지네릭 타입이 제거된다. 지네릭 타입의 제거에서 자세히 다뤄보자

지네릭이 도입되기 이전의 코드와 호환을 위해, 지네릭 클래스인데도 예전의 방식으로 객체를 생성하는 것이 허용된다. 다만 지네릭 타입을 지정하지 않아서 경고가 발생한다.

Box b = new Box(); // ok, T는 Object로 간주
b.setItem("ABC"); // 경고, uncheck or unsafe operation
b.setItem(new Object()); // 경고, uncheck or unsafe operation

타입 파라미터 컨벤션

제네릭에서 사용하는 타입 파라미터에 자주 봤던 T 같은 문자가 아니고 아무런 문자나 넣어도 코드가 동작하는 데는 문제가 없다.

하지만 타입 파라미터에도 컨벤션이 존재한다. 컨벤션을 왜 지켜야 하는지는 다들 잘 아실 것이다. 기억이 안 난다면 Code Conventions for the Java Programming Language 글의 Why Have Code Conventions 부분을 보자.

그래서 타입 파라미터 컨벤션은 아래와 같다.

https://tecoble.techcourse.co.kr/static/b268c4f8f30a082779a188af838767eb/3e3fe/2020-11-09-generics-2.png

제네릭 클래스나 메서드를 구현할 일이 있다면 컨벤션에 맞춰서 구현하자!


지네릭스의 제한

제네릭스 클래스의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다. 하지만 모든 객체에 대해 동일하게 동작해야하는 static 멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스 변수로 간주되고, static 멤버는 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.

class Box<T> {
        T[] itemArr; // ok, T타입의 배열을 위한 참조변수
    T[] toArray() {
                T[] tmpArr = new T[itemArr.length]; // error, 지네릭 배열 생성불가
                ...
        }

}

또, 지네릭 배열 타입의 배열을 생성하는 것도 허용되지 않는다.

지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만 new T[10]과 같이 배열을 생성하는 것은 안된다. 생성할 수 없는 이유는 new 연산자 때문, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다.

그런데 위의 코드는 정의된 Box 클래스를 컴파일 하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없다. intanceof 연산자도 마찬가지이다.

꼭 지네릭 배열을 생성해야할 필요가 있을 때는 new 연산자 대신 Reflection API의 newInstance()와 같이 동적으로 객체 생성하거나 Object 배열을 생성해서 복사한 다음에 T[]로 형변환 하는 방법이 존재한다.

지네릭 클래스의 객체 생성과 사용

class Box<T> {
    ArrayList<T> list = new ArrayList<T>();

    void add(T item) { list.add(item); }
    T get(int i)     { return list.get(i); }
    ArrayList<T> getList() { return list; }
    int size()       { return list.size(); }
    public String toString() { return list.toString(); }
}

Box의 객체를 생성할 때는 다음과 같이 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다. 일치하지 않으면 에러가 발생

Box<Apple> appleBox = new Box<Apple>(); // ok
Box<Apple> appleBox = new Box<Grape>(); // error

Apple이 Fruit의 자손이라고 가정해보자 하지만 그럼에도 error이다.

Box<Fruit> appleBox = new Box<Apple>(); // error, 대입된 타입이 다르다.

단, 두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다. FruitBox는 Box의 자손이라고 가정

Box<Apple> appleBox = new FruitBox<Apple>(); // ok, 다형성

와일드 카드

제네릭 클래스가 아닌 클래스에 static 메서드의 매개변수로 특정 타입을 지정해줬을 때, 제네릭 타입을 특정 타입으로 고정해 놓으면 다른 타입의 객체가 메서드의 매개변수가 될 수 없으므로 여러 가지 타입의 매개변수를 갖는 메서드를 만들어야 한다.

그러나 이와 같이 오버로딩하면, 컴파일 에러가 발생한다. 제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다. 제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버린다. 따라서 위 설명과 같은 경우에 메서드들은 오버로딩이 아니라 ‘메서드 중복 정의’가 된다.

이럴 때 사용하기 위해 고안된 것이 ‘와일드 카드’이다. 와일드 카드는 기호 ?로 표현하며, 어떠한 타입도 될 수 있다.

? 만으로는 Object타입과 다를 게 없으므로, 다음과 같이 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.

< ? extends T >      와일드 카드의 상한 제한. T와 그 자손들만 가능
< ? super T >      와일드 카드의 하한 제한. T와 그 조상들만 가능
< ? >      제한 없음. 모든 타입이 가능. < ? extends Object > 와 동일(raw type)

제네릭 메서드

제네릭 메소드는 메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라 하며 선언된 제네릭으로 리턴 타입, 파라미터의 타입이 정해지는 메소드이다.

그리고 중요한 점이 제네릭 클래스가 아닌 일반 클래스 내부에도 제네릭 메서드를 정의할 수 있다. 그 말은, 클래스에 지정된 타입 파라미터와 제네릭 메서드에 정의된 타입 파라미터는 상관이 없다는 것이다.

즉, 제네릭 클래스에 를 사용하고, 같은 클래스의 제네릭 메서드에도 로 같은 이름을 가진 타입파라미터를 사용하더라도 둘은 전혀 상관이 없다는 것을 의미한다.

 class GenericClass<T> {
      ...
    static <T> void sort(List<T> list, Comparator<? super T> c) {
      ...
    }
  }

위 코드에서 제네릭 클래스에 선언된 타입 매개변수 T와 제네릭 메서드 sort()에 선언된 타입 매개변수 T는 타입 문자만 같고 서로 다른 것이다. sort()가 static메서드이므로 타입 매개변수를 사용할 수 없지만, 메서드에 제네릭 타입을 선언하고 사용하는 것은 가능하다.

메서드에 선언된 제네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 된다. 이 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 메서드가 static이건 아니건 상관이 없다.

| 참고 | 같은 이유로 내부 클래스에 선언된 타입 문자가 외부 클래스의 타입 문자와 같아도 구별될 수 있다.

제네릭 메서드를 호출할 때는 타입 변수에 타입을 대입해야 한다. 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략할 수 있다.


Erasure

제네릭은 타입의 안정성을 보장하며 실행시간에 오버헤드가 발생하지 않도록 하기 위해 추가 되었다. 컴파일러는 컴파일 시점에 제네릭에 대하여 type erasure(타입 이레이저)라고 부르는 프로세스를 적용한다.

이렇게 하는 주된 이유는 지네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해서이다.

타입 이레이저는 모든 타입의 파라미터들을 제거하고 나서 그 자리를 제한하고 있는 타입으로 변경하거나 타입 파라미터의 제한 타입이 지정되지 않았을 경우에는 Object로 대체한다. 따라서 컴파일 후에 바이트 코드는 새로운 타입이 생기지 않도록 보장하는 일반 클래스들과 인터페이스, 메소드들만 포함한다. Object 타입도 컴파일 시점에 적절한 캐스팅이 적용된다.

public <T> List<T> genericMethod(List<T> list) {
    return list.stream().collect(Collectors.toList());
}

타입 이레이저가 적용되면서 특정 타입으로 제한되지 않은 T는 Object로 대체된다.

public List<Object> withErasure(List<Object> list) {
    return list.stream().collect(Collectors.toList());
}

public List withErasure(List list) {
    return list.stream().collect(Collectors.toList());
}

타입이 제한되어 있을 경우 그 타입은 컴파일 시점에 제한된 타입으로 교체된다.

public <T extends Building> void genericMethod(T t) {
    ...
}

위 코드는 컴파일 후 다음과 같이 변경된다.

public void genericMethod(Building t) {
    ...
}

브릿지 메서드

java compiler는 제네릭의 타입안정성을 위해 Bridge Method도 만들어낼 수있다.

Bridge Method는 java 컴파일러가 컴파일 할 때 메서드 시그니처가 조금 다르거나 애매할 경우에대비하여 작성된 메서드이다. 이 경우는 파리미터화된 클래스나 인터페이스를 확장한 클래스를 컴파일 할 때 생길 수 있다.타입안정성을 위해 Bridge Method를 만들수도 있다.

public class IntegerStack extends Stack<Integer> {
    public Integer push(Integer value) {
        super.push(value);
        return value;
    }
}

Java 컴파일러는 다형성을 제네릭 타입 소거에서도 지키기 위해, IntegerStackpush(Integer) 메서드와 Stack의 push(Object) 메서드 시그니처 사이에 불일치가 없어야 했다. 따라서 컴파일러는 런타임에 해당 제네릭 타입의 타입소거를 위한 Bridge 메서드를 만드는데 아래와같은 방식으로 만든다.

public class IntegerStack extends Stack {
// Bridge method generated by the compiler

    public Integer push(Object value) {
        return push((Integer) value);
    }

    public Integer push(Integer value) {
        return super.push(value);
    }
}

extends Stack<Integer> -> Stack 으로 변경한 것을 볼 수 있으며, push에 parameter를 Object가 아닌 Integer로 맞추기 위한 도우미 메서드가 늘어났다는 것을 알 수 있다. 결과적으로 Stack 클래스의 push method는 타입소거를 진행한 후에, IntegerStack 클래스의 원본 push 방법을 사용하게 한다.


참고 출처

728x90
728x90

Java 버전별 특징들을 찾아보면서 좋은 글이 있어 해당 글을 정리한 포스팅입니다.
따라서 잘못된 내용이 존재할 수 있습니다!

어떤 자바 버전을 사용해야 할까?

최신 Java 버전은 이제 6개월마다 따른다.

수많은 새로운 버전이 출시됨에 따라 기본적으로 다음과 같은 사용 시나리오가 존재

  • 기업의 기존 프로젝트에서는 Java 8을 사용해야 하는 경우가 많음
  • 일부 레거시 프로젝트는 Java 1.5(2004년 출시) 또는 1.6(2006년 출시)에서 중단되기도 함
  • 최신 IDE, 프레임워크 및 빌드 도구를 사용하고 그린 필드 프로젝트를 시작하는 경우 Java 11(LTS) 또는 최신 Java 17 LTS를 망설임 없이 사용할 수 있다.
  • 안드로이드 개발의 특별한 분야가 있는데, 자바 버전은 기본적으로 자바 7에 고정되어 있고, 특정한 자바 8 기능들을 이용할 수 있다. 또는 코틀린 프로그래밍 언어를 사용하는 것으로 전환

 

특정 자바 버전을 학습해야 할까?

12, 17와 같은 특정 Java 버전만을 "학습"할 필요가 없다.

Python 2에서 3과 같이 릴리스 사이에 심각한 문제가 있는 것과 달리 자바는 하위 호환성이 매우 높기 때문

  • 즉, Java 5 또는 8 프로그램이 Java 8-17 가상 머신에서 실행되도록 보장된다.
  • 이걸 backward compatible (하위 호환성) 이라 한다.

반대로 java 8 JVM에서는 사용할 수 없는 java 17 기능을 의존한다면 컴파일 되지 않는다.

  • 대부분 java.lang.UnsupportedClassVersionError 발생

그러므로 오히려 Java 8의 내용들로 토대를 쌓고 Java 9-17에 추가된 기능에 대해 알아보고 언제든지 사용할 수 있다.

 

자바 Distribution

JDK 다운로드를 제공하는 다양한 사이트가 있으며 "누가 어떤 라이선스로 무엇을 제공하는지"가 불분명하다.

OpenJDK 프로젝트

Java 소스 코드(RE/JDK의 소스 코드)의 경우 OpenJDK 프로젝트 사이트에 존재하는 유일한 소스 코드

그러나 이것은 소스 코드일 뿐 배포 가능한 빌드가 아니다.

  • 이론적으로, 해당 소스 코드로 빌드를 만들 수 있다
  • 예를 들어 MarcoJDK라고 부르며 배포하기 시작 가능
  • 하지만 합법적으로 자바 SE 호환이라고 부를 수 있는 우리의 배포판은 인증이 부족할 것

이러한 이유로 실제로 이러한 빌드를 만들고 인증을 받은 후 배포하는 벤더가 많지 않다.

OpenJDK 빌드(오라클) 및 OracleJDK 빌드

자바를 소스에서 빌드하는 벤더 중 하나가 오라클이다.

Oracle JDK와 OpenJDK의 차이점

  • Oracle JDK는 상용(유료)이지만, OpenJDK는 오픈소스기반(무료)
  • Oracle JDK의 라이선스는 Oracle BCL(Binary Code License) Agreement이지만, OpenJDK의 라이선스는 Oracle GPL v2
  • Oracle JDK는 LTS(장기 지원) 업데이트 지원을 받을 수 있지만, OpenJDK는 LTS 없이 6개월마다 새로운 버전이 배포된다.
  • Oracle JDK는 Oracle이 인수한 Sun Microsystems 플러그인을 제공하지만, OpenJDK는 제공하지 않는다.
  • Oracle JDK는 OpenJDK 보다 CPU 사용량과 메모리 사용량이 적고, 응답시간이 높다.

Java 8 이전 OpenJDK 빌드와 OracleJDK 빌드 사이에는 실제 소스 차이가 존재했는데, 최신의 두 버전은 본질적으로 동일하며 약간의 차이만 존재한다.

Adoptium (formerly AdoptOpenJDK)

2017년에 자바 유저 그룹 멤버, 개발자, 벤더(아마존, 마이크로소프트, 피보탈, 레드햇 등)로 구성된 그룹이 AdoptOpenJDK라는 커뮤니티를 시작

  • 참고: 2021년 8월 현재, AdoptOpenJDK 프로젝트는 새로운 집으로 옮겨졌고 지금은 Eclipse Adoptium 프로젝트로 불린다.

또한 보다 긴 가용성/업데이트를 갖춘 강력한 무료 OpenJDK 빌드를 제공하며 다음과 같은 두 가지 Java 가상 머신도 선택 가능

Java를 설치하려는 경우 권장된다.

Azul Zulu, Amazon Corretto, SAPMachine

  • 그 외에 주목할만한 distribution

 

java8-17 특징


java 8

Java 8은 대규모 릴리스였으며 Oracle 웹 사이트 에서 모든 기능 목록을 확인 가능

  • Lambda,
  • stream
  • interface default method
  • Optional
  • new Date and Time API(LocalDateTime, …)

 

Lambda

Java 8 이전 익명 클래스의 사용을 람다를 이용하여 더욱 간결하고 직관적으로 구현 가능

Runnable runnable = new Runnable(){
   @Override
   public void run(){
     System.out.println(*"Hello world !"*);
   }
 };
Runnable runnable = () -> System.out.println(*"Hello world two!"*);

Stream

자바 8은 스트림 API를 통해 컬렉션을 처리하면서 발생하는 모호함과 반복적인 코드 문제멀티코어 활용 어려움이라는 두 가지 문제를 모두 해결

List<String> list = Arrays.asList(*"franz"*, *"ferdinand"*, *"fiel"*, *"vom"*, *"pferd"*);
list.stream()
    .filter(name -> name.startsWith(*"f"*))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);

 

Java 9

Java 9는 다음과 같은 몇 가지 추가 사항이 포함된 상당히 큰 릴리스

  • 모듈시스템 등장(jigsaw)

 

컬렉션

컬렉션에는 list, set, map을 쉽게 구성할 수 있는 몇 가지 추가 기능

List<String> list = List.of(*"one"*, *"two"*, *"three"*);
Set<String> set = Set.of(*"one"*, *"two"*, *"three"*);
Map<String, String> map = Map.of(*"foo"*, *"one"*, *"bar"*, *"two"*);

스트림

takeWhile, dropWhile, iterate 메서드의 형태로 몇 가지 추가 기능

Stream<String> stream = Stream.iterate(*""*, s -> s + *"s"*)
  .takeWhile(s -> s.length() < 10);

optional

ifPresentOrElse 추가 기능

user.ifPresentOrElse(this::displayAccount, this::displayLogin);

인터페이스

인터페이스에 private method 사용 가능

public interface MyInterface {

    private static void myPrivateMethod(){
        System.out.println(*"Yay, I am private!"*);
    }
}

기타 언어 기능

try-with-resources 문 또는 다이아몬드 연산자(<>) 확장, HTTP클라이언트와 같은 몇 가지 다른 개선 사항 존재

 

Java 10

가비지 컬렉션 등과 같은 Java 10에 몇 가지 변경 사항이 존재

개발자로서 보게 될 유일한 실제 변경 사항은 로컬 변수 유형 추론이라고도 하는 "var" 키워드의 도입

  • var 키워드
  • 병렬 처리 가비지 컬렉션 도입으로 인한 성능 향상
  • JVM 힙 영역을 시스템 메모리가 아닌 다른 종류의 메모리에도 할당 가능

 

지역 변수 유형 추론: var-keyword

// Pre-Java 10
String myName = "Marco";

// With Java 10
var myName = "Marco"

JAVA에서 var 예약어를 사용하면 중복을 줄임으로써 코드를 간결하게 만들 수 있다.

  • var 키워드는 지역 변수 타입 추론을 허용한다.
  • 메서드 내부의 변수에만 적용 가능

 

Java 11

Java 11은 개발자의 관점에서 볼 때 약간 작은 릴리스

  • Oracle JDK와 OpenJDK 통합
  • Oracle JDK가 구독형 유료 모델로 전환
  • 서드파티 JDK 로의 이전 필요
  • lambda 지역변수 사용법 변경
  • 기타 추가

 

Strings & Files

Strings and Files에는 몇 가지 새로운 메서드 추가

*"Marco"*.isBlank();
*"Mar\nco"*.lines();
*"Marco  "*.strip();

Path path = Files.writeString(Files.createTempFile(*"helloworld"*, *".txt"*), *"Hi, my name is!"*);
String s = Files.readString(path);

Run Source Files

Java 10부터 Java 소스 파일 을 먼저 컴파일 하지 않고도 실행할 수 있다. 스크립팅을 향한 한 걸음

ubuntu@DESKTOP-168M0IF:~$ java MyScript.java

람다 매개변수에 대한 지역 변수 유형 추론(var)

람다 표현식에 var 사용 가능

(var firstName, var lastName) -> firstName + lastName

 

Java 12

Java 12에는 몇 가지 새로운 기능과 정리가 포함 되어 있지만

  • 언급할 가치가 있는 것은 유니코드 11 지원과 새로운 스위치 표현식의 preview 뿐

 

Java 13

여기 에서 전체 기능 목록을 찾을 수 있지만 기본적으로 유니코드 12.1 지원과 두 가지 새롭거나 개선된 preview 기능(향후 변경될 수 있음)이 제공

 

스위치 표현식(preview)

이제 스위치 표현식이 값을 반환 가능하며 fall-through/break 문제 없이 표현식에 람다 스타일 구문을 사용 가능

switch(status) {
  case SUBSCRIBER:
    *// code block*break;
  case FREE_TRIAL:
    *// code block*break;
  default:
    *// code block*}
boolean result = switch (status) {
    case SUBSCRIBER -> true;
    case FREE_TRIAL -> false;
    default -> throw new IllegalArgumentException(*"something is murky!"*);
};

Multiline Strings (Preview)

String htmlBeforeJava13 = *"<html>\n"* +
              *"    <body>\n"* +
              *"        <p>Hello, world</p>\n"* +
              *"    </body>\n"* +
              *"</html>\n"*;
String htmlWithJava13 = *"""
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """*;

 

자바 14

  • 스위치 표현시 표준화
  • instanceof 패턴 매칭 (preview)
  • record (data object) 선언 기능 추가 (preview)

 

스위치 표현(Standard)

버전 12 및 13에서 preview 였던 스위치 표현식 이 이제 표준화 되었다.

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    default      -> {
      String s = day.toString();
      int result = s.length();
      yield result;
    }
};

record(preview)

Java로 많은 상용구를 작성하는 고통을 완화하는 데 도움이 되는 레코드 클래스가 있다.

데이터, (잠재적으로) getter/setters, equals/hashcode, toString만 포함하는 이 Java 14 이전 클래스

final class Point {
    public final int x;
    public final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}
    *// state-based implementations of equals, hashCode, toString// nothing else*

레코드를 사용

record Point(int x, int y) { }

유용한 NullPointerExceptions

마지막으로 NullPointerExceptions는 정확히 어떤 변수가 null 인지 설명한다 .

author.age = 35;
---
Exception in thread *"main"* java.lang.NullPointerException:
     Cannot assign field *"age"* because *"author"* is null

Pattern Matching For InstanceOf (Preview)

이전에는 다음과 같이 instanceof 내부에서 객체를 캐스팅 필요

if (obj instanceof String) {
    String s = (String) obj;
    *// use s*}

이제 이 작업을 수행하여 캐스트를 효과적으로 삭제 가능

if (obj instanceof String s) {
    System.out.println(s.contains(*"hello"*));
}

 

Java 15

  • Text-Blocks / Multiline Strings
  • Records & Pattern Matching(2차 preview, 상단 Java 14 참조)
  • 스케일링 가능한 낮은 지연의 가비지 컬렉터 추가(ZGC)
  • 레코드 (2차 preview, 상단 Java 14 참조)
  • Sealed Classes - Preview
  • Nashorn JavaScript Engine 제거

 

Text-Blocks / Multiline Strings

Java 13의 실험 기능으로 도입된 여러 줄 문자열은 이제 프로덕션 준비 완료

String text = *"""
                Lorem ipsum dolor sit amet, consectetur adipiscing \
                elit, sed do eiusmod tempor incididunt ut labore \
                et dolore magna aliqua.\
                """*;

Sealed Classes - Preview

  • 상속 가능한 클래스를 지정할 수 있는 봉인 클래스가 제공된다.
  • 상속 가능한 대상은 상위 클래스 또는 인터페이스 패키지 내에 속해 있어야 한다.
public abstract sealed class Shape
    permits Circle, Rectangle, Square {...}

즉, 클래스가 public인 동안 하위 클래스로 허용되는 유일한 Shape 클래스들은 Circle, Rectangle 및 Square 이다.

 

Java 16

  • Pattern Matching for instanceof
  • Unix-Domain Socket Channels
  • Foreign Linker API - Preview
  • Records & Pattern Matching

 

Unix-Domain Socket Channels

이제 Unix 도메인 소켓에 연결할 수 있다(macOS 및 Windows(10+)에서도 지원됨).

 socket.connect(UnixDomainSocketAddress.of(
        *"/var/run/postgresql/.s.PGSQL.5432"*));

Foreign Linker API - Preview

JNI(Java Native Interface)에 대한 계획된 교체로, 기본 라이브러리에 바인딩할 수 있다(C 생각).

 

Java 17

Java 17은 Java 11 이후의 새로운 Java LTS(장기 지원) 릴리스

  • Pattern Matching for switch (Preview)
  • Sealed Classes (Finalized)
  • Foreign Function & Memory API (Incubator)
  • Deprecating the Security Manager

 

Pattern Matching for switch (Preview)

이제 객체를 전달하여 기능을 전환하고 특정 유형을 확인할 수 있다.

public String test(Object obj) {

    return switch(obj) {

    case Integer i -> *"An integer"*;

    case String s -> *"A string"*;

    case Cat c -> *"A Cat"*;

    default -> *"I don't know what it is"*;

    };

}

Sealed Classes (Finalized)

Java 15에서 preview 제공되었던 기능 완료

Foreign Function & Memory API (Incubator)

Java Native Interface(JNI)를 대체한다. 기본 함수를 호출하고 JVM 외부의 메모리에 액세스할 수 있다. 지금은 C가 C++, 포트란과 같은 추가 언어 지원 계획을 가지고 있다고 생각

Deprecating the Security Manager

자바 1.0 이후로 보안 관리자가 존재해 왔었지만 현재는 더 이상 사용되지 않으며 향후 버전에서는 제거될 예정

 

참고 출처

728x90

'java' 카테고리의 다른 글

[모던 자바 인 액션] 자바 8, 9, 10, 11에서 일어난 일  (0) 2024.07.07
static inner vs non-static inner class  (0) 2024.07.07
자바 제네릭스  (0) 2024.07.07
Object 클래스  (0) 2024.07.07
String vs StringBuilder vs StringBuffer  (0) 2024.07.07
728x90

Object 클래스


  • 모든 클래스의 최고 조상이기 때문에 Object 클래스의 멤버들은 모든 클래스에서 바로 사용 가능
  • 멤버변수는 없고 오직 11개의 메서드만 가지고 있다.
    • 모든 클래스의 조상인 Object 클래스의 메서드들은 대부분 네이티브 메서드이다.

Native Method란

  • 네이티브 메서드는 JVM이 설치된 OS의 메서드를 말한다.
  • 네이티브 메서드는 보통 C언어로 작성되어 있는데, 자바에서는 메서드의 선언부만 정의하고 구현은 하지 않는다.
  • Java Method와 Native Function을 Mapping하는 역활은 JNI(java native interface)가 한다.

주요 메서드


  • protected Object clone() : 객체 자신의 복사본을 반환한다.
  • public boolean equals(Object obj) :객체 자신과 객체 obj가 같은 객체인지 알려준다.
  • protected void finalize() : 객체가 소멸될 때 가비지 컬렉터에 의해 자동적으로 호출된다.
    • 이 때 수행되어야하는 코드가 있을 때 오버라이딩 한다.
    • finalize 예측이 불가능하고, 위험하며 대부분 불필요하다
    • Deprecated
  • public Class getClass() : 객체 자신의 클래스 정보를 담고 있는 Class 인스턴스를 반환한다.
  • public int hashCode() : 객체 자신의 해시코드를 반환한다.
  • public String toString() : 객체 자신의 정보를 문자열로 반환한다.
  • public void notify() : 객체 자신을 사용하려고 기다리는 쓰레드를 하나만 깨운다.
  • public void notifyAll() : 객체 자신을 사용하려고 기다리는 모든 쓰레드를 깨운다.
  • public void wait(), wait(long timeout), wait(long timeout, int nanos)
    • 다른 쓰레드가 notify()나 notifyAll()을 호출할 때까지 현재 쓰레드를 무한히 또는 지정된 시간(timeout, nanos)동안 기다리게 한다.
    • timeout: 천 분의 1초, nanos는 10^9분의 1초

Clone()


  • 자신을 복제하여 새로운 인스턴스를 생성하는 일을 한다
  • clone() 사용법
    • 복제할 클래스가 Cloneable 인터페이스를 구현한 클래스에서만 clone() 호출 가능
    • 그리고 접근제어자를 protected에서 public으로 변경
    • 마지막으로 조상클래스의 clone()을 호출하는 코드가 포함된 try - catch문 작성
      class Poketmon implements Cloneable {
          String name;

          Poketmon(String name) {
              this.name = name;
          }

          @Override
          public Object clone() {
              Object obj = null;
              try {
                  obj = super.clone();  // clone()은 반드시 예외처리를 해주어야 한다.
              } catch(CloneNotSupportedException e) {}
              return obj;
          }
      }

      class CloneEx {
          public static void main(String[] args){
              Poketmon original = new Poketmon(3, 5);
              Poketmon copy = (Poketmon)original.clone(); // 복제(clone)해서 새로운 객체를 생성
              System.out.println(original);
              System.out.println(copy);
          }
      }
    Poketmon{name='메타몽'}
    Poketmon{name='메타몽'}
  • 하지만 clone()은 재정의하지 않는게 좋다고 한다.
  • jdk 1.5부터 추가된 기능
  • 오버라이딩할 때 조상 메서드의 반환타입을 자손 클래스의 타입으로 변경을 허용하는 것
  • 이처럼 조상의 타입이 아닌, 실제로 반환되는 자손 객체의 타입으로 반환할 수 있어서 번거로운 형변환이 줄어드는 장점이 존재.
    @Override
    public Poketmon clone() {
     Object obj = null;
     try {
     obj = super.clone();
     } catch(CloneNotSupportedException e) {}
     return obj;
    }
    // Poketmon copy = (Poketmon)original.clone();
    Poketmon copy = original.clone();

얕은 복사와 깊은 복사

  • clone()은 단순히 객체에 저장된 값을 그대로 복제할 뿐, 객체가 참조하고 있는 객체까지 복제하지는 않는다.
  • 기본형 배열인 경우에는 아무런 문제가 없지만, 객체배열을 clone()으로 복제하는 경우에는 원본과 복제본이 같은 객체를 공유하므로 완전한 복제라고 보기 어렵다.
    • 이러한 복제를 얕은 복사라고 한다, shallow copy
    • 얕은 복사는 원본을 변경하면 복사본도 영향을 받는다.
  • 반면에 원본이 참조하고 있는 객체까지 복제하는 것을 깊은 복사, deep copy
    • 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 원본의 변경이 복사본에 영향을 미치지 않는다. ## getClass()

  • 자신이 속한 클래스의 Class 객체를 반환하는 메서드
  • Class 객체는 이름이 'Class'인 클래스 객체이다.
  • 다음과 같이 정의
    public final class Class implements ... { ... }
  • 클래스 객체는 클래스의 모든 정보를 담고 있으며, 클래스 당 1개만 존재
  • 클래스 파일이 '클래스 로더(ClassLoader)'에 의해 메모리에 올라갈 때, 자동으로 생성
    • 먼저 기존에 생성된 클래스 객체가 메모리에 존재하는지 확인하고
    • 있으면 클래스 객체의 참조를 반환
    • 없으면 클래스 패스에 지정된 경로를 따라서 클래스 파일을 찾는다.
      • 못찾으면 ClassNotFoundException 발생
        • 찾으면 해당 클래스 파일을 읽어서 Class 객체로 변환
  • 즉 파일 형태로 저장되어 있는 클래스를 읽어서 Class 클래스에 정의된 형식으로 변환하는 것
  • 따라서 클래스 파일을 읽어서 사용하기 편한 형태로 저장해 놓은 것이 클래스 객체이다.### Class 객체를 얻는 방법
  • 클래스의 정보가 필요할 때, 먼저 Class 객체에 대한 참조를 얻어 와야 하는데, 해당 Class 객체에 대한 참조를 얻는 방법으로 여러 가지가 있다.
    // case1: 생성된 객체로부터 얻는 방법
    Class cObj = new Poketmon().getClass(); 

    // case2: 클래스 리터럴(*.class)로 부터 얻는 방법
    Class cObj = Poketmon.class;

    // case3: 클래스 이름으로 부터 얻는 방법
    // 특히 클래스 파일, 예를 들어 데이터베이스 드라이버를 메모리에 올릴 때 주로 사용
    Class cObj = Class.forName("Poketmon");
  • 클래스 객체를 이용하면 클래스에 정의된 멤버의 이름이나 개수 등, 클래스에 대한 모든 정보를 얻을 수 있기 때문에 Class 객체를 통해서 객체를 생성하고 메서드를 호출하는 등 보다 동적인 코드 작성 가능
    Poketmon p = new Poketmon();                  // new 연산자를 이용한 객체 생성
    Poketmon p = Poketmon.class.newInstance();    // Class객체를 이용한 객체 생성

toString()


목적 : 인스턴스에 대한 정보를 문자열로 제공하기 위해

  • 일반적으로, toString 메소드는 this 객체를 "텍스트로 표현"한 문자열을 리턴합니다. 결과는 간결하면서도 충분한 정보를 담고 있어야 하며, 사람이 읽기 쉬운 형태여야 합니다.
  • 대부분의 경우 인스턴스 변수에 저장된 값들을 문자열로 표현
  • Object class의 toString()
    public String toString() {
     return getClass().getName()+"@"+Integer.toHexString(hashCode());
    }
  • 가능하다면 toString 메소드는 객체 내의 중요 정보를 전부 담아 반환해야 한다.
  • toString이 반환하는 문자열의 형식을 명시하건 그렇지 않건 간에, 어떤 의도인지는 문서에 분명하게 남겨야 한다.
  • toString이 반환하는 문자열에 포함되는 정보들은 전부 프로그래밍을 통해서 가져올 수 있도록 하라.
    • toString 반환값을 파싱해서 쓰는 일이 없도록 한다. toString 구현이 바뀌면 그런 코드는 다 못 쓰게 된다

equals(Object obj)


목적 : 물리적으로 다른 메모리에 위치하는 객체여도 논리적으로 동일함을 구현하기 위해

  • 두 객체의 같고 다름을 참조변수의 값으로 판단
  • 즉 두 참조변수에 저장된 주소값이 같은지를 판단하는 기능
    public boolean equals(Object obj) {
     return (this == obj)
    }
  • 클래스의 인스턴스변수 값으로 객체의 같고 다름을 비교하게 하고 싶다면 오버라이딩을 하면된다
    • String class의 equals 메서드도 오버라이딩을 통해 String 인스턴스가 갖는 문자열 값을 비교하도록 되어 있다.
    • String, Date, File, wrapper 클래스(Integer, Double 등)
  • equals 메소드는 null이 아닌 객체 참조들에 대한 동치 관계를 구현하며, 동치 관계의 조건은 null이 아닌 참조 x, y, z에 대하여,
    • 반사관계: x.equals(x)true여야 한다.
    • 대칭관계: y.equals(x)true이면, x.equals(y)true여야 한다.
    • 추이관계: x.equals(y)true이고, y.equals(z)true이면, x.equals(z) 또한 true여야 한다.
    • 일관성: equals 비교에 필요한 정보가 수정되지 않았다면, x.equals(y)를 여러 차례 실행한 결과는 일관성 있게 true만 리턴하거나 false만 리턴해야 한다.
    • x.equals(null)false여야 한다. ### 실제 위반되는 경우
  • 실제 Timestamp 객체와 Date 객체를 같은 컬렉션에 보관하거나 섞어 쓰면 문제가 생길 수 있다고 한다.
  • 대칭관계 위반
    // Date Class
    public boolean equals(Object obj) {
        return obj instanceof Date && getTime() == ((Date) obj).getTime();
    }
    // Timestamp class
    public boolean equals(Timestamp ts) {
        if (super.equals(ts)) {         // java.util.Date.equals
            if  (nanos == ts.nanos) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    public boolean equals(java.lang.Object ts) {
      if (ts instanceof Timestamp) {
        return this.equals((Timestamp)ts);
      } else {
        return false;
      }
    }
  • date.equals(timestamp) : date 기준으로 시간이 같은지만 검사하므로 true가 된다.
  • timestamp.equals(date) : date는 Timestamp의 인스턴스가 아니므로 false가 된다. 인스턴스 검사를 하지 않고 date가 timestamp로 형변환이 가능하다 쳐도, nanos 검사에서 false가 나올 수 밖에 없다.

hashCode()


목적: 객체의 동일성을 위해 구현

  • 해싱기법에 사용되는 해시함수를 구현한 것이다.
    • 인스턴스가 저장된 가상머신의 주소를 10진수로 반환
  • 오버라이딩 된 hashCode()는 세가지 조건을 만족해야 한다.
    1. 변경되지 않은 한 객체의 hashCode 메소드를 호출한 결과는 항상 똑같은 integer 값이어야 한다.
      • 객체가 변경됐더라도 equals 메소드가 참고하는 정보가 변경되지 않았다면 hashCode 값은 달라지지 않는다.
    2. equals 메서드를 이용한 비교에 의해서 true를 얻은 두 객체에 대해 각각 hashCode()호출해서 얻은 결과는 반드시 같아야 한다.
      • f1 f2의 equals 메서드의 결과가 true라면 hashCode1, 2의 값은 같아야 한다
      • Fruit f1 = new Fruit("apple", 10); Fruit f2 = new Fruit("apple", 10); boolean b = f1.equals(f2); int hashCode1 = f1.hashCode(); int hashCode2 = f2.hashCode();
      1. 그러나 equals() 메소드가 다르다고 판별한 두 객체의 hashCode 값이 반드시 달라야 하는 것은 아니다.
        • 해시 테이블 성능이 향상을 위해 같지 않은 객체들이 각기 다른 hashCode 값을 가지면 좋다.
        • 서로 다른 객체에 대해서 해시코드값이 중복되는 경우가 많아질수록 해싱을 사용하는 컬렉션의 검색속도가 떨어진다.
  • 따라서 hashCode()메서드는 equals() 메서드와 밀접한 관계가 있다.
    • 2번 조건에 위배되지 않기 위해

참고 및 출처

https://atoz-develop.tistory.com/
https://yoonemong.tistory.com/193
https://github.com/castello/javajungsuk3
https://docs.oracle.com/en/java/javase/11/docs/api/index.html

728x90

+ Recent posts