본문 바로가기
Develop/Java

[Java] UnaryOperator란?

by 연로그 2023. 6. 26.
반응형

📚 UnaryOperator란?

  • : Java에서 제공하는 함수형 인터페이스
  • 인수(argument)와 반환 결과(return)가 동일한 타입을 가진 경우에 사용하는 특수한 Function
  • Java 8 부터 사용 가능

 

함수형 인터페이스란?
* = SAM Interface = Single Abstract Method Interface
* 1개의 추상 메서드를 가진 인터페이스
* UnaryOperator 외에도 Predicate, Consumer, Function 등이 있다.

 

UnaryOperator의 코드를 살펴보면 Function<T, T>를 상속받고 있는 것을 볼 수 있다. 제네릭 부분을 살펴보면 인수와 반환 결과 모두 동일한 타입으로 취급하는 것을 볼 수 있다.

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

 

위 설명들만으로는 UnaryOperator가 뭔지 아직 감이 잘 안잡힐거라 생각한다. 예제 코드를 통해 어떤 메서드를 지원하고 있는지 살펴보자.

 

📕 apply()

UnaryOperator는 람다 표현식으로 구현할 수 있다. apply()를 통해 인자를 전달하면, 해담 표현식에 따라 결과가 반환된다.

@Test
void multiply2() {
    UnaryOperator<Integer> unaryOperator = i -> i * 2;

    assertThat(unaryOperator.apply(1)).isEqualTo(2);  // 1*2 = 2
    assertThat(unaryOperator.apply(2)).isEqualTo(4);  // 2*2 = 4
    assertThat(unaryOperator.apply(3)).isEqualTo(6);  // 3*2 = 6
    assertThat(unaryOperator.apply(4)).isEqualTo(8);  // 4*2 = 8
}

 

📙 identity()

위에서 잠깐 UnaryOperator를 살펴봤을 때, static 메서드인 identity를 확인할 수 있었다. return t -> t; 라는 아주 간단한 코드를 통해 예상할 수 있다시피, identity()를 통해 생성한 UnaryOperator는 인자로 받은 값을 그대로 반환하는 것을 확인할 수 있다.

@Test
void identity() {
    UnaryOperator<Boolean> unaryOperator = UnaryOperator.identity();
    assertThat(unaryOperator.apply(true)).isTrue();
    assertThat(unaryOperator.apply(false)).isFalse();
}

 

📗 andThen()

여러개의 UnaryOperator를 엮을 수 있다. 아래와 같이 호출할 순서를 지정하고 싶은 경우 등에서 사용할 수 있다.

@Test
void andThen() {
    UnaryOperator<Integer> plus10 = i -> i + 10;
    UnaryOperator<Integer> multipleDouble = i -> i * 2;

    assertThat(plus10.andThen(multipleDouble).apply(1)).isEqualTo(22);  // (1+10)*2 = 22
    assertThat(multipleDouble.andThen(plus10).apply(1)).isEqualTo(12);  // (1*2)+10 = 12
}

 

📘 compose()

여러개의 UnaryOperator를 엮을 수 있다. andThen()과는 다르게 인자로 넣은 UnaryOperator를 먼저 수행한다.

@Test
void compose() {
    UnaryOperator<Integer> plus10 = i -> i + 10;
    UnaryOperator<Integer> multipleDouble = i -> i * 2;

    assertThat(plus10.compose(multipleDouble).apply(1)).isEqualTo(12);  // (1*2)+10 = 12
    assertThat(multipleDouble.compose(plus10).apply(1)).isEqualTo(22);  // (1+10)*2 = 22
}

 

📝 기타 예제

Java에서 UnaryOperator를 쓰는 곳이 있나 궁금해져서 예제를 찾아봤다. Stream::iterator에서도 사용하고 있었다. 두번째 인자에 UnaryOperator를 넣을 수 있다.

@Test
void iterate() {
    Stream.iterate(1,n->n + 1)
        .limit(10)
        .forEach(System.out::println);
}

 

이외에도 어떤 상황에 사용할 수 있을지 예제를 생각해보았다. 아래의 multiply2 예제처럼 stream에서 여러번 재사용하기 위해 선언할 수도 있고, andThen() 예제처럼 여러 로직의 순서를 명시적으로 나타내며 실행하고 싶을 때 사용할 수도 있을 것 같다.

@Test
void multiply2() {
    UnaryOperator<Integer> unaryOperator = i -> i * 2;
    List<Integer> list = List.of(1, 2, 3, 4, 5);

    List<Integer> doubleList = list.stream()
        .map(unaryOperator)
        .collect(Collectors.toList());

    System.out.println(list);
    System.out.println(doubleList);
}

@Test
void andThen() {
    UnaryOperator<Integer> plus10 = i -> i + 10;
    UnaryOperator<Integer> multipleDouble = i -> i * 2;

    assertThat(plus10.andThen(multipleDouble).apply(1)).isEqualTo(22);  // (1+10)*2 = 22
    assertThat(multipleDouble.andThen(plus10).apply(1)).isEqualTo(12);  // (1*2)+10 = 12
}

multiply2의 실행 결과

 

함수형 인터페이스를 반드시 써야하냐고 묻는다면 나는 반드시는 아니라고 생각한다. 너무 많이 사용하면 코드의 복잡성만 올라가는데 가끔 함수형 인터페이스를 사용함으로써 좀 더 가독성이 좋은 케이스들이 있다. 개인적으로는 함수형 인터페이스를 적용한 뒤에 아 이게 더 읽기 편한데? 라는 생각이 들 때 쓰는 것 같다.

 


참고

반응형