
어느 날 메서드 참조를 사용하다가 이런 의문이 들었다.
내가 임의로 만든 클래스의 인스턴스 메서드는 왜 메서드 참조가 안되지?
의문이 든 코드는 이거였다.
public class Main {
public static void main(String[] args) {
Function<String,String> f = MyClass::MyMethod;
Function<String,Integer> f2 = String::length;
}
}
class MyClass{
public String MyMethod(String str){
System.out.println(str);
return str;
}
}
static 메서드가 아니기 때문에 불가능하다고 메시지가 출력되었다.
하지만 String클래스의 경우에는 문제가 없었다.
Function<String,Integer> struo = String::length;
하지만 lengh()는 String 클래스의 인스턴스 메서드이다. 왜 이런 문제가 발생할까?
Unbound 인스턴스 메서드 참조의 특성을 이해하면 왜 이런 문제가 발생하는지 알 수 있다.
특정 객체 없이 클래스 이름을 사용하여 인스턴스 메서드를 참조하는 방식으로, 메서드 참조 방법 중 인스턴스 메서드 참조가 바로 Unbound 인스턴스 메서드 참조이다.
객체 바인딩이 없기 때문에 메서드를 참조하는 시점에 호출 객체가 정해지지 않는다. 대신, 나중에 함수형 인터페이스의 메서드를 호출할 때 호출 객체가 전달된다.
Function<String,Integer> f = String::length; //참조 시점에 호출 객체 정해지지 않음
f.apply(new String("Hello")); //호출 시 호출 객체가 전달
Unbound 인스턴스 메서드 참조는 다음과 같은 규칙을 따라야 한다.
- 함수형 인터페이스의 첫 번째 제네릭 타입 매개변수는 호출 객체의 타입으로 사용된다
- 호출 객체 타입과 함수형 인터페이스의 첫 번째 제네릭 타입은 항상 동일해야 한다
- 표준 클래스(String,Integer 등)와 사용자 정의 클래스 모두 동일한 규칙을 따른다
자, 이해를 위해 간단한 java.util.function 패키지의 함수형 인터페이스 Function<T,R>을 예로 들어보자.
여기서 T는 입력 매개변수의 타입인 동시에 호출 객체의 타입으로 사용된다.
위의 코드에서 String의 경우 Function<String,String>과 매칭이 잘되었기 때문에 가능했던 것이다.
입력 매개변수도 String타입인 동시에 호출 객체 또한 String타입이기 때문이다.
그래서 String클래스의 인스턴스 메서드 중 입력매개변수 타입이 int형인 lastIndexOf(int ch)의 경우에는 컴파일 에러가 발생한다.
Function<Integer,Integer> f2 = String::lastIndexOf;
왜냐하면 함수형 인터페이스의 첫 번째 타입이 Integer이기 때문에 호출 객체의 타입도 Integer이어야 하는데 여기서는 String이기 때문이다.
자 그렇다면, 사용자 정의 클래스는 어떻게 해야 할까? 입력매개변수 타입과 호출 객체가 다를텐데 방법이 없을까?
BiFunction<T,U,R>을 사용하면 된다.
BiFunction<T,U,R>은 T,U 타입의 매개변수 2개를 받고 R 타입의 반환값을 반환하지만 이 외에도 T에 호출 객체 타입을 받고 U가 메서드 매개변수 타입으로 처리하는데 최적화 되어 있기 때문이다.
class MyClass{
public String MyMethod(String str){
System.out.println(str);
return str;
}
}
이 사용자 정의 클래스의 메서드의 경우 매개변수는 1개이지만 매개변수 타입과 클래스 타입이 다르기 때문에 BiFunction<T,U,R>과 매칭을 할 경우
으로 처리할 수 있다.
BiFunction<MyClass,String,String> bf = MyClass::MyMethod;
bf.apply(new MyClass(), "Hello"); //호출 객체 전달
그래서 apply() 호출 시 원래는 매개변수를 2개 넣지만 이러한 경우에는 첫 번째 매개변수로 호출 객체를 넣어주고, 두 번째 매개변수에는 메서드 매개변수를 입력해주면 된다.
이러면 매개변수가 1개인 메서드여도 BiFunction<T,U,R>을 사용할 수 있다.
하지만 그렇다고 매개변수가 1개인 메서드가 전부 BiFunction<T,U,R>를 사용할 수 있는 건 아니다.
BiFunction<String,String,Integer> bf = String::length;
위와 같은 경우에는 호출 객체 타입과 입력 매개변수 타입이 일치하기 때문에 BiFunction<T,U,R>가 아닌 Function<String,Integer>을 사용해야 한다.
그렇다면 마지막으로 방금 위에서 설명한
Function<Integer,Integer> f2 = String::lastIndexOf;
이 코드는 어떻게 해야 컴파일 에러가 발생하지 않을까?
우선 람다식으로 풀어보자.
str,i -> str.lastIndexOf(i)
매개변수를 2개를 받아 str은 호출객체로서의 역할을 하는 매개변수, i는 lastIndexOf()의 입력매개변수로 사용을 하고 제네릭 타입을 <String,Integer,Integer>로 설정하면 된다.
서로 다른 두 입력매개변수 타입을 받아야 하기 때문에 선언타입은 BiFunction으로 사용하면 된다.
BiFunction<String,Integer,Integer> bf = (str, i) -> str.lastIndexOf(i);
//String::lastIndexOf
BiFunction<String,Integer,Integer> bf = String::lastIndexOf;
이렇게 하면 인스턴스 메서드 참조를 사용할 수 있다.