메소드 참조

gustjtmd·2022년 1월 14일
0

Java

목록 보기
33/40
람다식은 결국 메소드의 정의이다. 따라서 다음과 같이 생각해 볼 수 있다.
'이미 정의되어 있는 메소드가 있다면, 이 메소드의 정의가 람다식을 대신할수있지 않을까'
실제로 메소드 정의는 람다식을 대신할 수 있다. 메소드 참조라는 방법을 통해서

메소드 참조의 4가지 유형과 메소드 참조의 장점

메소드 참조의 유형은 총 4가지로 이를 정리하면 다음과 같다
(1) static 메소드의 참조
(2) 참조변수를 통한 인스턴스 메소드 참조
(3) 클래스 이름을 통한 인스턴스 메소드 참조
(4) 생성자 참조.

근래에는 코드의 양을 줄이는데 초점이 맞춰지고 있다. 코드의 양을 줄이면 코드의 생산성도
향상되고, 이는 결국 코드의 가독성 개선으로 이어진다는 논리이다.
이러한 흐름에 '메소드 참조'는 람다식으로 줄어든 코드의 양을 조금 더 줄일수 있게 한다.

static 메소드의 참조.

ClassName::staticMethodName
public class ArrageList {
    public static void main(String[] args) {
        List<Integer> ls = Arrays.asList(1,3,5,7,9);
        ls = new ArrayList<>(ls);

        Consumer<List<Integer>> c = (l) -> Collections.reverse(l);  //람다식
        c.accept(ls);   //순서 뒤집기 진행
        System.out.println(ls); //출력

    }
}
//Consumer<T> void accept(T t)
---------------------------------------------------------------------
[9, 7, 5, 3, 1]
Collections 클래스의 메소드
public static void reverse(List<?> list)	//저장 순서를 뒤집는다.

람다식을 작성할때 이미 정의되어 있는 메소드를 사용하는 것에는 큰 의미가 있다.
그리고 이러한 경우를 고려하여 자바 8에서는 람다식을 작성할 필요 없이 다음과
같이 메소드 정보만 전달할수 있도록 하고있다.

Consumer<List<Integer>> c = Collections::reverse;

즉 stati 메소드의 참조 방법은 다음과 같다

ClassName::staticMethodName(클래스 이름 :: 메소드 이름)

정리하자면 다음과 같이 '메소드 참조'를 기반으로 람다식을 대신할수 있다.

Consumer<List<Integer>> c = l -> Collections.reverse(l);
-> Consumer<List<Integer>> c = Collections::reverse;

위의 '메소드 참조'에서 람다식에는 있는 인자 전달에 대한 정보를 생략할수 있는 
다음 약속에 근거한다.
'accept 메소드 호출시 전달되는 인자를 reverse 메소드를 호출하면서 그대로 전달'
public class ArrangeList2 {
    public static void main(String[] args) {
        List<Integer> ls = Arrays.asList(1,3,5,7,9);
        ls = new ArrayList<>(ls);

        Consumer<List<Integer>> c = Collections::reverse;
        c.accept(ls);
        System.out.println(ls);
    }
}
---------------------------------------------------------------------
[9, 7, 5, 3, 1]

인스턴스 메소드의 참조 1:인스턴스가 존재하는 상황에서 참조

ReferenceName::instanceMethodName
class JustSort{
    public void sort(List<?> lst) {   //인스턴스 메소드
        Collections.reverse(lst);
    }
}
public class ArrangeList3 {
    public static void main(String[] args) {
        List<Integer> ls = Arrays.asList(1,3,5,7,9);
        ls = new ArrayList<>(ls);
        JustSort js = new JustSort();
        //Consumer<List<Integer>> c = i -> js.sort(i); 람다식
        Consumer<List<Integer>> c = js::sort;   //메소드 참조 기반
        c.accept(ls);
        System.out.println(ls);
    }
}
---------------------------------------------------------------------
[9, 7, 5, 3, 1]

-----------------------------------------------------------------------
	Consumer<List<Integer>> c = i -> js.sort(i); 람다식
        -> Consumer<List<Integer>> c = js::sort;   메소드 참조 기반

위의 예제 내용의 독특한 점을 확인할 수 있다.
	JustSort js = new JustSort();
        Consumer<List<Integer>> c = i -> js.sort(i); 람다식
        -> Consumer<List<Integer>> c = js::sort;   //메소드 참조 기반
        
'람다식에서 같은 지역 내에 선언된 참조변수 js에 접근하고 있다.'

코드만 보면 문제가 없어 보인다. 그러나 람다식이 인스턴스 생성으로 이어진다는 사실을
고려하면 이는 다소 특이한 일이 될 수 있다. 결론을 말하면 위의 코드에서 보이듯이 람다식에서
같은 지역에 선언된 참조변수에 접근이 가능하다. 단 다음 조건을 만족해야 한다
'람다식에서 접근 가능한 참조변수는 final 선언이거나 effectively final이어야 한다.'

변수가 effectively final이라는 것은 사실상 final선언이 된것과 다름이 없다. 
-----------------------------------------------------------------------
public class ForEachDemo {
    public static void main(String[] args) {
        List<String> ls = Arrays.asList("Box","Robot");
        ls = new ArrayList<>(ls);
        ls.forEach(s -> System.out.println(s));
        ls.forEach(System.out::println);
    }
}
-----------------------------------------------------------------------
Box
Robot
Box
Robot
----------------------------------------------------------------------
Collection<E>인터페이스는 iterable<T>를 상속한다. 따라서 컬렉션 클래스들은
Iterable<T>를 대부분 구현하게 되는데 이 인터페이스에는 다음 디폴드 메소드가 정의되어 있다.

default void forEach(Consumer<? super T> action){
	for(T t:this)	//this는 이 메소드가 속한 컬렉션 인스턴스를 의미함.
    	action.accept(t)//모든 저장된 데이터들에 대한 이 문장 반복.
}

즉 위의 메소드가 호출되면 컬렉션 인스턴스에 저장되어 있는 모든 인스턴스들을 대상으로
다음 문장을 실행하게 된다.
action.accept(t)	//이때 t는 저장되어 있는 인스턴스 각각을 의미함.

따라서 우리는 forEach 메소드 호출을 위해서 Consumer<T> 인터페이스에 대한 람다식
또는 메소드 참조를 전달해야 한다. 그런데 Consumer<T>의 추상 메소드는 다음과 같다.

void accept(T t);	//반환하지 않고 전달된 인자를 대상으로 어떤 결과를 보임

그리고 이에 딱 맞는 메소드중 하나가 System.out.println이다. 이는 아래에서 보이듯이
accept와 반환형 및 매개변수 선언이 동일하다
(물론 accpet의 TString일 경우에 매개변수 선언이 동일하다. 위 예제처럼 말이다)

public void println(String x)

그리고 앞서 System.out이 PrintStream 인스턴스를 참조하는 참조변수이니
다음과 같이 람다식 또는 메소드 참조를 통한 forEach문의 호출을 진행할수 있다.

ls.forEach(System.out::println);

인스턴스 메소드의 참조2 : 인스턴스 없이 인스턴스 메소드 참조

ClassName::instanceMethodName
class Ibox{
    private int n;
    public Ibox(int i){n = i;}
    public int larger(Ibox b){
        if(n>b.n)
            return n;
        else
            return b.n;
    }
}
public class NoObjectMethodRef {
    public static void main(String[] args) {
        Ibox ib1 = new Ibox(5);
        Ibox ib2 = new Ibox(7);

        //두 상자에 저장된 값 비교하여 더 큰 값 반환
    //  ToIntBiFunction<Ibox,Ibox> bf = (b1,b2) -> b1.larger(b2);
        ToIntBiFunction<Ibox,Ibox> bf = Ibox::larger;
        int bigNum = bf.applyAsInt(ib1,ib2);
        System.out.println(bigNum);
    }
}
// ToIntBiFunction<T,U> int applyAsInt(T t, U u)
----------------------------------------------------------------------
7

----------------------------------------------------------------------
위 예제에서 등장한 람다식은 다음과 같다
ToIntBiFunction<Ibox,Ibox> bf = (b1,b2) -> b1.larger(b2);

위 람다식에서 호출하는 메소드 larger가 '첫 번째 인자로 전달된 인스턴스의 메소드'라는
사실에 주목하자. 이러한 경우 다음과 같이 메소드 참조가 이를 대신할 수 있다.(약속이다)
        ToIntBiFunction<Ibox,Ibox> bf = (b1,b2) -> b1.larger(b2);
        ToIntBiFunction<Ibox,Ibox> bf = Ibox::larger; //메소드 참조방식


메소드 참조를 위한 Ibox::larger만 보면 static 메소드 참조처럼 보인다. 
그러나 이는 인스턴스 메소드의 참조이다.

ToIntBiFunction<Ibox, Ibox> bf = iBox::larger; 이 문장 이후
bf.applyAsInt(ib1,ib2);
이때 bf가 참조하는 메소드는 Ibox::larger이다 그리고 이는 ib1도 ib2도 갖는 인스턴스
메소드이다. 그런데 '첫번째 전달인자를 대상으로 이 메소드를 호출하기로 약속했으므로'
다음과 같이 위의 문장이 실행된다.

생성자 참조

ClassName::New
public class StringMaker {
    public static void main(String[] args) {
   	 /*
            Function<char[], String> f = (s) -> {
                return new String(s)
            };
         */
        Function<char[], String> f = String::new;
        char[] src = {'R','o','b','o','t'};
        String str = f.apply(src);
        System.out.println(str);
    }
}//Funciont<T,R>    R apply(T t);

Robot

-------------------------------------------------------------------------
위 예제에서는 STring 클래스의 다음 생성자를 이용한 String 인스턴스의 생성을 보이고 있다.


            Function<char[], String> f = (s) -> {
                return new String(s)
            };
	-> Function<char[], String> f = String::new;
아래 메소드 참조 방식으로 바꿀수 있다.

람다식을 이루는 문장이 '단순히 인스턴스의 생성 및 참조 값의 반환'일 경우 ClaaName::new
형태의 메소드 참조로 바꿀수 있다.
이러한 줄임말이 가능한 이유는

char[] src = {'R','o','b','o','t'}
String str = f.apply(src)
위의 코드에서 f의 참조 대상이 String::new 이므로 f는 String의 생성자를 참조하게 되는데
참조 변수 f의 자료형이 Function<char[],String>이므로 
매개변수 형이 char[]인 다음 생성자를 참조하게 된다
public String(char[] value)

따라서 이후에 다음 문장을 실행하게 되면

String str = f.apply(src);

profile
반갑습니다

0개의 댓글