Java Lambda (3) 메소드 참조

⏱ 7 분

메소드 참조 Method References

메소드 참조는 메소드를 간결하게 지칭할 수 있는 방법으로 람다가 쓰이는 곳 어디서나 사용할 수 있습니다. '참조’라는 말에서 알 수 있듯이 이미 존재하는 이름을 가진 메소드를 람다로써 사용할 수 있도록 참조하는(가리키는) 역할을 합니다.

즉, 일반 함수를 람다 형태로 사용할 수 있도록 해줍니다. 그리고 메소드를 호출(실행)하는 것이 아니라 참조만 하기 때문에, 이름 뒤에 소괄호는 쓰지 않습니다.

우리가 메소드 참조로 작성하면 컴파일러는 메소드를 참조를 보고 람다를 생성합니다. 사용법은 다음과 같습니다.

1
2
3
4
5
6
7
8
// 원래 함수
public static String valueOf(Object obj) { ... }

// Class::method 형태로 사용
String::valueOf

// 메소드 () 소괄호는 쓰지 않는다.
String::valueOf(); // error

메소드 참초를 이용해 동일한 형식의 람다를 해당 인터페이스에 할당할 수 있습니다.

1
2
3
4
5
interface Example {
String theNameIsUnimporant(Object object);
}

Example a = String::valueOf;

다양한 메소드 참조를 살펴보겠습니다.

  1. 기본 사용법
  2. 생성자 참조
  3. 스태틱 메소드 참조
  4. 인스턴스 메소드 참조 (1)
  5. 인스턴스 메소드 참조 (2)

기본

기본적인 사용법입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 람다로 사용하기 위한 함수형 인터페이스 작성
@FunctionalInterface
interface Conversion {
String convert(Integer number);
}

// Conversion 을 사용하는 메소드
public static String convert(Integer number, Conversion function) {
return function.convert(number);
}

// 메소드 참조를 동일한 람다로 변환하기 위한 충분한 정보를 제공함
// Convert 메소드를 호출할 때 람다를 인자로 넘겨줄 수 있다.
convert(100, (number) -> String.valueOf(number));

// valueOf() 메소드가 Integer 를 받고 String 을 반환하는 조건에 일치한다
// 따라서 메소드 참조로 대체할 수 있음
convert(100, String::valueOf);

생성자 참조

실제로 생성자를 호출해서 인스턴스를 생성하는 것이 아니라 생성자 메소드를 참조하는 것 뿐입니다.

1
2
String::new
() -> new String() // 위 코드와 같은 의미

예제를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Factory 는 임의의 객체를 반환하는 create 메소드를 가진 함수형 인터페이스
@FunctionalInterface
interface Factory<T> {
T create();
}

// 새로운 리스트를 만들고 10개의 빈 객체를 저장한다고 하자.
public void usage () {
List<Object> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(new Object());
}
}

// 객체 생성하는 부분을 다음과 같이 메소드로 뽑아낸다.
public void usage () {
List<Object> list = new ArrayList<>();
initialize(list, ...);
}

public void initialize(List<Object> list, Factory<Object> factory) {
for (int i = 0; i < 10; i++) {
list.add(factory.create());
}
}

// 그럼 다음과 같이 사용할 수 있다.
public void usage() {
List<Object> list = new ArrayList<>();
init(list, () -> new Object());
init(list, Object::new); // 메소드 참조
}

제네릭 타입을 추가해도 잘 동작합니다.

1
2
3
4
5
6
7
8
9
10
public void usage () {
List<String> list = new ArrayList<>();
initialize(list, String::new);
}

private <T> void initialize (List<T> list, Factory<T> factory) {
for (int i = 0; i < 10; i++) {
list.add(factory.create());
}
}

인자를 받는 생성자의 경우를 생각해보겠습니다. 컴파일러가 함수형 인터페이스를 통해 어떤 생성자를 사용할 지 판단합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Person 클래스와 여러 인자를 받는 생성자
class Person {
public Person(String forename, String surname, LocalDate birthday, Gender gender, String emailAddress, int age) {
// initialize
}
}

// Person 객체를 리턴하는 함수형 인터페이스
@FunctionalInterface
interface PersonFactory {
Person create(String forename, String surname, LocalDate birthday, Gender gender, String emailAddress, int age);
}

public void example() {
List<Person> list = new ArrayList<>();

// 이렇게 사용할 수도 있지만
PersonFactory factory = (a, b, c, d, e, f) -> new Person(a, b, c, d, e, f);

// 이렇게 처리해도 적합한 생성자를 유추한다.
PersonFactory factory = Person::new;

// 람다를 넘겨주기
init(list, factory, a, b, c, d, e, f);

// 인라인으로 처리 가능
init(list, Person::new, a, b, c, d, e, f);
}

private void init(List<Person> list, PersonFactory factory, String forename, String surname, LocalDate birthday, Gender gender, String emailAddress, int age) {
for (int i = 0; i < 10; i++) {
list.add(factory.create(forename, surname, birthday, gender, emailAddress, age));
}
}

스태틱 메소드 참조

메소드 참조는 스태틱 메소드를 직접적으로 가리킬 수 있습니다.

1
2
String::valueOf
x -> String.valueOf(x) // 위와 동일

예제를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
public static class Comparators {
public static Integer asc(Integer first, Integer second) {
return first.compareTo(second);
}
}

// 스태틱 메소드를 람다로 사용하는 경우
Collections.sort(Arrays.asList(5, 12, 4), (a, b) -> Comparators.asc(a, b));
Collections.sort(Arrays.asList(5, 12, 4), Comparators::asc); // 메소드 참조

인스턴스 메소드 참조 (1)

특정 인스턴스의 메소드를 참조할 수 있습니다. 클래스 이름이 아닌 인스턴스명을 적어주면 됩니다.

1
2
x::toString // x 는 접근하고자 하는 인스턴스
() -> x.toString() // 위와 동일

이러한 방법은 이미 정의되어 있는 메소드를 (함수형 인터페이스가 맞다면) 람다로 재사용할 수 있게 해줍니다. 함수형 인터페이스 간의 전환도 가능합니다.

1
2
Callable<String> c = () -> "Hello"; // 함수형 메소드는 call
Factory<String> f = c::call; // 다른 함수형 인터페이스를 사용 가능

좀 더 자세한 예제를 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
public void example () {
String x = "hello";
// 함수형 인터페이스 시그니처에 맞는 인스턴스 메소드를 전달할 수 있다!
function(x::toString); // 내부에는 x 가 없고 외부 범위의 x 에 접근하는 클로저
}

public static String function(Supplier<String> supplier) {
return sullier.get();
}

인스턴스 메소드 참조 (2)

앞에서 살펴 본 내용에 따르면 다음 메소드 참조에서 Obejct 는 클래스를 의미할 것입니다. 하지만 여기서 toString 메소드는 스태틱 메소드가 아니라 일반적인 인스턴스 메소드입니다.

1
Object::toString

어떤 점이 다른걸까요?

1
2
() -> x.toString() // 클로저라서 알고 있었다
(x) -> x.toString() // 외부에서 받아오기 때문에 알 수 없음

헷갈릴 수 있는데 좀 더 자세히 살펴보겠습니다.

1
2
3
4
5
6
7
8
public void lambdaExample() {
function("value", x -> x.toString()); // 넘겨 받은 x 를 사용
function("value", String::toString); // 메소드 참조
}

public static String function(String value, Function<String, String> function) {
return function.apply(value); // 클로저 아님
}

내부적으로는 인스턴스 메소드 참조 (1)은 클로저이고, (2)는 람다입니다. (1)은 미리 알 수 있는 특정 객체의 인스턴스 메소드이고, (2)는 나중에 전달받는 임의의 객체의 인스턴스라고 볼 수 있습니다.

요약

이번 포스팅에서 살펴본 메소드 참조를 정리해보겠습니다.[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 생성자 참조
String::new // ClassName::new
() -> new String()

// 정적 메소드 참조
String::valueOf // ClassName::staticMethodName
(s) -> String.valueOf(s)

// 인스턴스 메소드 참조 (1) 클로저
x::toString // instanceName::instanceMethodName
() -> "hello".toString()

// 인스턴스 메소드 참조 (2) 람다
String::toString // ClassName::instanceMethodName
(s) -> s.toString()

참고