메서드 참조
메서드 참조가 필요한 이유
V1
import java.util.function.BinaryOperator;
public class MethodRefStartV1 {
public static void main(String[] args) {
BinaryOperator<Integer> add1 = (x, y) -> x + y;
BinaryOperator<Integer> add2 = (x, y) -> x + y;
System.out.println(add1.apply(1, 2)); // 3
System.out.println(add2.apply(1, 2)); // 3
}
}두 정수를 더하는 간단한 연산을 수행하는 람다를 정의한 코드에는 다음과 같은 문제점이 있다.
동일한 기능을 하는 람다를 여러 번 작성해야 한다.
코드가 중복되어 있어 유지보수가 어려울 수 있다.
만약 덧셈 로직이 변경되어야 한다면 모든 람다를 각각 수정해야 한다.
V2
import java.util.function.BinaryOperator;
public class MethodRefStartV2 {
public static void main(String[] args) {
BinaryOperator<Integer> add1 = (x, y) -> add(x, y);
BinaryOperator<Integer> add2 = (x, y) -> add(x, y);
System.out.println(add1.apply(1, 2));
System.out.println(add2.apply(1, 2));
}
public static int add(int x, int y) {
return x + y;
}
}덧셈 로직을 별도의 add() 메서드로 분리하여 코드 중복 문제를 해결했다. 로직이 한 곳으로 모여 유지보수가 쉬워졌다. 그러나 여전히 남은 문제가 있다.
람다를 작성할 때마다
(x, y) -> add(x, y)과 같은 코드를 반복해서 작성해야 한다.매개변수를 전달하는 부분이 장황하다.
V3
import java.util.function.BinaryOperator;
public class MethodRefStartV3 {
public static void main(String[] args) {
//메서드 참조
BinaryOperator<Integer> add1 = MethodRefStartV3::add;
BinaryOperator<Integer> add2 = MethodRefStartV3::add;
System.out.println(add1.apply(1, 2));
System.out.println(add2.apply(1, 2));
}
public static int add(int x, int y) {
return x + y;
}
}메서드 참조 문법을 사용하여 람다를 더욱 간단하게 표현할 수 있다.
메서드 참조는 이미 정의된 메서드를 람다로 변환하여 더욱 간결하게 사용할 수 있도록 해주는 문법적 편의 기능이다. 람다를 작성할 때 이미 정의된 메서드를 그대로 호출하는 경우라면 메서드 참조를 통해 더욱 직관적이고 간결한 코드를 작성할 수 있다.
메서드 참조의 장점
메서드 참조를 사용하면 코드가 더욱 간결해지고 가독성이 향상된다.
컴파일러가 자동으로 매개변수를 매칭하기 때문에 매개변수를 명시적으로 작성할 필요가 없다.
별도의 로직 분리와 함께 재사용성 역시 높아진다.
메서드 참조의 유형
메서드 참조는 이미 정의된 메서드를 람다처럼 간결하게 표현할 수 있게 해주는 문법이다. 즉 람다 내부에서 단순히 어떤 메서드를 호출만 하고 있을 경우 메서드 참조를 사용해 이를 축약해주는 문법이다.
이러한 메서드 참조에는 4가지 유형이 있다.
정적 메서드 참조
특정 객체의 인스턴스 메서드 참조
생성자 참조
임의 객체의 인스턴스 메서드 참조
다음 Person 클래스를 예로 각 유형에 대해 알아보자.
public class Person {
private String name;
//생성자
public Person() {
this("Unknown");
}
//생성자(매개변수)
public Person(String name) {
this.name = name;
}
//정적 메서드
public static String greeting() {
return "Hello";
}
//정적 메서드(매개변수)
public static String greetingWithName(String name) {
return "Hello " + name;
}
//인스턴스 메서드
public String introduce() {
return "I am " + name;
}
//인스턴스 메서드(매개변수)
public String introduceWithNumber(int number) {
return "I am " + name + ", my number is " + number;
}
public String getName() {
return name;
}
}V1 - 매개변수가 없는 예제
import java.util.function.Supplier;
public class MethodRefEx1 {
public static void main(String[] args) {
// 1. 정적 메서드 참조
Supplier<String> staticMethod1 = () -> Person.greeting();
Supplier<String> staticMethod2 = Person::greeting; // 클래스::정적메서드
System.out.println("staticMethod1: " + staticMethod1.get());
System.out.println("staticMethod2: " + staticMethod2.get());
// 2. 특정 객체의 인스턴스 참조
Person person = new Person("Kim");
Supplier<String> instanceMethod1 = () -> person.introduce();
Supplier<String> instanceMethod2 = person::introduce; // 객체::인스턴스메서드
System.out.println("instanceMethod1: " + instanceMethod1.get());
System.out.println("instanceMethod2: " + instanceMethod2.get());
// 3. 생성자 참조
Supplier<Person> newPerson1 = () -> new Person();
Supplier<Person> newPerson2 = Person::new; // 클래스::new
System.out.println("newPerson1: " + newPerson1.get());
System.out.println("newPerson2: " + newPerson2.get());
}
}👆 메서드 참조에서 ()를 사용하지 않는 이유
메서드 참조의 문법을 보면 메서드명 뒤에
()가 없다.
()는 메서드를 즉시 호출한다는 의미를 가진다.()가 없는 것은 메서드 참조를 하는 시점에는 메서드를 호출하는게 아니라 단순히 메서드의 이름으로 해당 메서드를 참조만 한다는 뜻이다.해당 메서드의 실제 호출 시점은 함수형 인터페이스를 통해서 이후에 이루어진다.
V2 - 매개변수가 있는 예제
import java.util.function.Function;
public class MethodRefEx2 {
public static void main(String[] args) {
// 1. 정적 메서드 참조
Function<String, String> staticMethod1 = name -> Person.greetingWithName(name);
Function<String, String> staticMethod2 = Person::greetingWithName; // 클래스::정적메서드
System.out.println("staticMethod1 = " + staticMethod1.apply("Kim"));
System.out.println("staticMethod2 = " + staticMethod2.apply("Kim"));
// 2. 인스턴스 메서드 참조(특정 객체의 인스턴스 메서드 참조)
Person instance = new Person("Kim");
Function<Integer, String> instanceMethod1 = n -> instance.introduceWithNumber(n);
Function<Integer, String> instanceMethod2 = instance::introduceWithNumber; // 객체::인스턴스메서드
System.out.println("instanceMethod1 = " + instanceMethod1.apply(1));
System.out.println("instanceMethod2 = " + instanceMethod2.apply(1));
// 3. 생성자 참조
Function<String, Person> supplier1 = name -> new Person(name);
Function<String, Person> supplier2 = Person::new; // 클래스::new
System.out.println("newPerson = " + supplier1.apply("Kim"));
System.out.println("newPerson = " + supplier2.apply("Lee"));
}
}매개변수가 있는 메서드 참조의 경우 매개변수를 생략한다. 매개변수가 여러 개라면 순서대로 전달된다.
함수형 인터페이스의 시그니처(매개변수와 반환 타입)가 이미 정해져 있고, 컴파일러가 그 시그니처를 바탕으로 메서드 참조와 연결해 주기 때문에 명시적으로 매개변수를 작성하지 않아도 자동으로 추론되어 호출된다.
따라서 매개변수를 포함한 메서드 호출도 메서드 참조를 사용하여 더욱 간편하게 작성할 수 있다.
V3 - 임의 객체의 인스턴스 메서드 참조 예제
import java.util.function.Function;
public class MethodRef3 {
public static void main(String[] args) {
//4. 임의 객체의 인스턴스 메서드 참조(특정 타입의)
Person kim = new Person("Kim");
Person park = new Person("Park");
Person lee = new Person("Lee");
// 람다
Function<Person, String> fun1 = (Person person) -> person.introduce();
System.out.println("kim.introduce = " + fun1.apply(kim));
System.out.println("park.introduce = " + fun1.apply(park));
System.out.println("lee.introduce = " + fun1.apply(lee));
// 메서드 참조, 타입이 첫 번째 매개변수가 됨
// 그리고 첫 번째 매개변수의 메서드를 호출, 나머지는 순서대로 매개변수에 전달
Function<Person, String> fun2 = Person::introduce; // 타입::인스턴스메서드
System.out.println("kim.introduce = " + fun2.apply(kim));
System.out.println("park.introduce = " + fun2.apply(park));
System.out.println("lee.introduce = " + fun2.apply(lee));
}
}Function<Person, String>함수형 인터페이스에Person타입의 인스턴스를 인자로 받고,String을 반환하는 람다를 정의했다.이 람다는 매개변수로 지정한 특정 타입의 객체에 대해 동일한 메서드를 호출하는 패턴을 보인다.
즉 매개변수로 지정한 특정 타입의 임의 객체의 인스턴스 메서드를 참조한다.
매개변수로 지정한 타입 :
Person임의 객체 :
kim,park,lee등Person타입을 구현한 어떠한 객체인스턴스 메서드 :
introduce()
이러한 메서드 참조를 특정 타입의 임의 객체의 인스턴스 참조라 한다. 앞서 본 메서드 참조의 4가지 유형을 한번 정리해보자.
정적 메서드 참조 :
클래스명::클래스메서드특정 객체의 인스턴스 메서드 참조 :
객체명::인스턴스메서드생성자 참조 :
클래스명::new임의 객체의 인스턴스 메서드 참조 :
클래스명::인스턴스메서드
여기서 2. 특정 객체의 인스턴스 메서드 참조와 4. 임의 객체의 인스턴스 메서드 참조가 비슷해 보이는데, 두 기능은 완전히 다른 기능이다. (기본적으로 둘다 인스턴스 메서드를 호출하지만 하나는 객체명을 사용하고, 하나는 클래스명을 사용하는 것이 다름)
2. 특정 객체의 인스턴스 메서드 참조이 기능은 메서드 참조를 선언할 때부터 이름 그대로 특정 객체(인스턴스)를 지정해야 한다.
특정 객체의 인스턴스 메서드 참조는 선언 시점부터 이미 인스턴스가 지정되어 있다. 따라서 람다를 실행하는 시점에 인스턴스를 변경할 수 없다.
4. 임의 객체의 인스턴스 메서드 참조이 기능은 메서드 참조를 선언할 때는 어떤 객체(인스턴스)가 대상이 될지 모른다.
임의 객체의 인스턴스 메서드 참조는 선언 시점에 호출할 인스턴스를 지정하지 않는다. 대신에 호출 대상을 매개변수로 선언해두고, 실행 시점에 호출할 인스턴스를 받는다.
실행 시점에 되어야 어떤 객체가 호출되는지 알 수 있으므로 임의 객체의 인스턴스 메서드 참조라 한다.
정리하면 둘의 핵심적인 차이는 메서드 참조나 람다를 정의하는 시점에 호출할 대상 인스턴스가 고정되는 것인지 아닌지이다.
특정 객체의 인스턴스 메서드 참조는 선언 시점에 호출할 특정 객체가 고정된다.
임의 객체의 인스턴스 메서드 참조는 선언 시점에 메서드를 호출할 특정 객체가 고정되지 않는다. 대신에 실행 시점에 인자로 넘긴 임의의 객체가 사용된다.
메서드 참조 활용 예제
임의 객체의 인스턴스 메서드 참조는 꼭 필요해 보이지 않는다. 하지만 메서드 참조 유형 중에는 이 기능이 가장 많이 사용된다.
예제 1 - 기본
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class MethodRef4 {
public static void main(String[] args) {
List<Person> list = List.of(
new Person("Kim"),
new Person("Park"),
new Person("Lee")
);
List<String> result1 = mapPersonToString(list, (Person p) -> p.introduce()); //람다
List<String> result2 = mapPersonToString(list, Person::introduce); //메서드 참조
List<String> result3 = mapStringToString(result1, (String s) -> s.toUpperCase()); //람다
List<String> result4 = mapStringToString(result2, String::toUpperCase); //메서드 참조
}
static List<String> mapPersonToString(List<Person> list, Function<Person, String> function) {
List<String> result = new ArrayList<>();
for (Person p : list) {
result.add(function.apply(p));
}
return result;
}
static List<String> mapStringToString(List<String> list, Function<String, String> function) {
List<String> result = new ArrayList<>();
for (String s : list) {
result.add(function.apply(s));
}
return result;
}
}예제 2 - MyStream 적용
import java.util.List;
public class MethodRef5 {
public static void main(String[] args) {
List<Person> list = List.of(
new Person("Kim"),
new Person("Park"),
new Person("Lee")
);
//람다
List<String> result1 = MyStreamV3.of(list)
.map(person -> person.introduce())
.map(s -> s.toUpperCase())
.toList();
//메서드 참조
List<String> result2 = MyStreamV3.of(list)
.map(Person::introduce) // person -> person.introduce()
.map(String::toUpperCase) // (String name) -> name.toUpperCase()
.toList();
}
}예제 3 - 매개변수가 여러 개인 경우
import java.util.function.BiFunction;
public class MethodRef6 {
public static void main(String[] args) {
// 4. 임의 객체의 인스턴스 메서드 참조(특정 타입의)
Person person = new Person("Kim");
// 람다
BiFunction<Person, Integer, String> fun1 = (Person p, Integer number) -> p.introduceWithNumber(number);
System.out.println("person.introduceWithNumber = " + fun1.apply(person, 1));
// 메서드 참조
// 타입이 첫 번째 매개변수가 됨, 그리고 첫 번째 매개변수의 메서드를 호출
// 나머지는 순서대로 매개변수에 전달
BiFunction<Person, Integer, String> fun2 = Person::introduceWithNumber; // 타입::메서드명
System.out.println("person.introduceWithNumber = " + fun2.apply(person, 1));
}
}정리
람다 대신에 메서드 참조를 사용하면 코드가 더 간결해지고 의도가 더 명확하게 드러난다.
메서드 참조를 사용하면 람다 표현식을 더욱 직관적으로 표현할 수 있으며, 각 처리 단계에서 호출되는 메서드가 무엇인지 쉽게 파악할 수 있다.
람다로도 충분히 표현할 수 있지만, 내부적으로 호출만 하는 간단한 람다라면 메서드 참조가 더 짧고 명확하게 표현될 수 있으며 가독성을 높일 수 있다.
Last updated