[윤성우의 열혈 Java 프로그래밍] Chapter 28 - 메소드 참조와 Optional

Update:     Updated:

카테고리:

태그:

28-1. 메소드 참조 (Method References)

이미 정의되어 있는 메소드가 있다면, 이 메소드의 정의가 람다식을 대신할 수 있지 않을까?

실제로 메소드 정의는 람다식을 대신할 수 있다.

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

메소드 참조의 유형은 총 4가지이다.

  • static 메소드의 참조
  • 참조변수를 통한 인스턴스 메소드 참조
  • 클래스 이름을 통한 인스턴스 메소드 참조
  • 생성자 참조

- static 메소드의 참조

static 메소드 참조 방법
ClassName::staticMethodName

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;

public class ArrangeList {
    public static void main(String[] args) {
        List<Integer> ls = new ArrayList<>(Arrays.asList(1, 3, 5, 7, 9));
        System.out.println(ls);

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

        System.out.println(ls);
    }
}

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

인스턴스 메소드 참조 방법 - 1
ReferenceName::instanceMethodName

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;

class JustSort {
    public void sort(List<?> list) {
        Collections.reverse(list);
    }
}

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

        JustSort js = new JustSort();

        // Consumer<List<Integer>> c = e -> js.sort(e);
        Consumer<List<Integer>> c = js::sort;
        c.accept(ls);

        System.out.println(ls);
    }
}

람다식에서 같은 지역 내에 선언된 참조변수 js에 접근하고 있다.
람다식이 인스턴스의 생성으로 이어진다는 사실을 고려하면 특이한 일이지만 결론적으로 람다식에서 같은 지역에 선언된 참조변수에 접근하는 것은 가능하다. 단, 여기에는 조건이 있다.

람다식에서 접근 가능한 참조변수는 final로 선언되었거나 effectively final이어야 한다.

이렇게 제한한 이유는 참조변수가 참조하는 대상이 중간에 바뀔경우 논리적 혼란을 일으키거나 예측 불가능한 상황으로 이어질 수 있기 때문이다.


좀 더 실질적인 상황을 보자.

import java.util.Arrays;
import java.util.List;

public class ForEachDemo {
    public static void main(String[] args) {
        List<String> ls = Arrays.asList("Box", "Robot");

        ls.forEach(s -> System.out.println(s)); // 람다식 기반
        ls.forEach(System.out::println);    // 메소드 참조 기반
    }
}

Collection<E> 인터페이스는 Iterable<T>를 상속한다.
이 인터페이스에 정의되어있는 forEach메소드를 살펴보면 다음과 같다.

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

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

인스턴스 메소드 참조 방법 - 2
ClassName::instanceMethodName

import java.util.function.ToIntBiFunction;

class IBox {
    private int n;

    public IBox(int i) {
        n = i;
    }

    public int larger(IBox box) {
        if (n > box.n)
            return n;
        return box.n;
    }
}

public class NoObjectMethodRef {
    public static void main(String[] args) {
        IBox iBox1 = new IBox(5);
        IBox iBox2 = new IBox(7);

        // 두 상자에 저장된 값 비교하여 더 큰 값 반환
        // ToIntBiFunction<IBox, IBox> bf = (box1, box2) -> box1.larger(box2);
        ToIntBiFunction<IBox, IBox> bf = IBox::larger;

        int bigNum = bf.applyAsInt(iBox1, iBox2);
        System.out.println(bigNum);
    }
}

위의 방식처럼 람다를 메소드 참조로 바꿀 수 있는 이유는 larger가 ‘첫 번째 인자로 전달된 인스턴스의 메소드’임에 있고, 이는 일종의 약속이다.
bf가 참조하는 메소드는 IBox::larger이고, 이는 ibox1, ibox2도 갖는 인스턴스 메소드지만, ‘첫 번째 전달인자를 대상으로 이 메소드를 호출하기로 약속하였으므로’ 그에 근거하여 실행이 된다.

- 생성자 참조

람다식을 작성하다 보면 인스턴스를 생성하고 이의 참조 값을 반환해야 하는 경우가 있다.
ClassName::new

import java.util.function.Function;

public class StringMaker {
    public static void main(String[] args) {
        // Function<char[], String> f = ar -> new String(ar);
        Function<char[], String> f = String::new;

        char[] src = {'R', 'o', 'b', 'o', 't'};
        String str = f.apply(src);
        System.out.println(str);
    }
}

28-2. Optional 클래스

Optional 클래스를 사용하여 조건에 따라 코드의 흐름이 나뉘어지는 상황을 막을 수 있음

클래스를 디자인할 때 가급적 클래스의 인스턴스 변수는 null로 두지 않는 것이 좋다.
다만, 어쩔 수 없이 null을 허용해야 하는 상황에 대한 대비책으로 Otional 클래스가 하나의 대답이 될 수 있다.

- NullPointerException 예외의 발생 샹황

class Friend {
    String name;
    Company cmp;    // null 일 수 있음

    public Friend(String n, Company c) {
        name = n;
        cmp = c;
    }

    public String getName() { return name; }
    public Company getCmp() { return cmp; }
}

class Company {
    String cName;
    ContInfo cInfo;    // null 일 수 있음

    public Company(String cn, ContInfo ci) {
        cName = cn;
        cInfo = ci;
    }

    public String getCName() { return cName; }
    public ContInfo getCInfo() { return cInfo; }

}

class ContInfo {
    String phone;   // null 일 수 있음
    String adrs;    // null 일 수 있음

    public ContInfo(String ph, String ad) {
        phone = ph;
        adrs = ad;
    }

    public String getPhone() { return phone; }
    public String getAdrs() { return adrs; }

}

class NullPointerCaseStudy {
    public static void showCompAddr(Friend f) { // 친구의 회사 주소를 출력하는 메소드
        String addr = null;
    
        if(f != null) {
            Company com = f.getCmp();
            
            if(com != null) {
                ContInfo info = com.getCInfo();
                
                if(info != null)
                    addr = info.getAdrs();   
            }
        }
        
        if(addr != null)
            System.out.println(addr);
        else
            System.out.println("There's no address information.");
    }

    public static void main(String[] args) {

        ContInfo ci = new ContInfo("321-444-577", "Republic of Korea");
        Company cp = new Company("YaHo Co., Ltd.", ci);
        Friend frn = new Friend("LEE SU", cp);

        // 친구 정보에서 회사 주소를 출력
        showCompAddr(frn);
    }
}

다음과 같이 클래스의 멤버 변수가 null인 경우 NullPointerException 예외가 발생하는 것을 막기 위해 if-else로 case에 따라 수행 내용을 분리해놓을 경우 코드의 가독성도 떨어지고 불필요하게 코드가 길어진다.
위의 코드를 Optional 클래스를 통해 적절하게 수정해보자

- Optional 클래스의 기본적인 사용 방법

import java.util.Optional;

class StringOptional1 {
    public static void main(String[] args) {
        Optional<String> os1 = Optional.of(new String("Toy1"));
        Optional<String> os2 = Optional.ofNullable(new String("Toy2"));

        if(os1.isPresent()) // 내용물이 존재하면 true
            System.out.println(os1.get());  // get을 통한 내용물 반환

        if(os2.isPresent())
            System.out.println(os2.get());
    }
}

Optional은 멤버에 인스턴스를 저장하는 일종의 래퍼 클래스이다.
image

위의 코드를 Optional의 인스턴스 메소드인 ifPresent 메소드를 통해 간략화시킬 수 있다.

import java.util.Optional;

class StringOptional2 {
    public static void main(String[] args) {
        Optional<String> os1 = Optional.of(new String("Toy1"));
        Optional<String> os2 = Optional.ofNullable(new String("Toy2"));

        /* ifPresent(): Consumer 인스턴스를 입력인자로 받음 */
        os1.ifPresent(s -> System.out.println(s));  // 람다식 버전
        os2.ifPresent(System.out::println); // 메소드 참조 버전
    }
}

- map 메소드의 소개

map 메소드는 apply 메소드가 반환하는 대상을 ‘Optional 인스턴스에 담아서’ 반환한다.

import java.util.Optional;

class OptionalMap {
    public static void main(String[] args) {
        Optional<String> os1 = Optional.of("Optional String");
        Optional<String> os2 = os1.map(s -> s.toUpperCase());
        System.out.println(os2.get());

        Optional<String> os3 = os1.map(s -> s.replace(' ', '_'))
                                  .map(s -> s.toLowerCase());
        System.out.println(os3.get());
    }
}

image

map 함수의 매개변수 형은 Function이다.
apply 함수를 람다식을 통해 구현해주게 되면 그 구현한 내용을 통해 해당 인스턴스를 처리한 뒤
Optional 인스턴스로 한 번 감싸서 반환한다는 것이 특징이다.

따라서 반환한 Optional 인스턴스 안에 값을 가져오기 위해 get 등의 메소드를 사용해주었다.

- orElse 메소드의 소개

Optional 인스턴스에 저장된 내용물을 반환하는 메소드에는 get과 orElse가 있다.
차이점은 orElse는 저장된 내용물이 없을 때, 대신해서 반환할 대상을 지정할 수 있다는 점이다.

import java.util.Optional;

class OptionalOrElse {
    public static void main(String[] args) {
        Optional<String> os1 = Optional.empty();
        Optional<String> os2 = Optional.of("So Basic");

        String s1 = os1.map(s -> s.toString())
                       .orElse("Empty");

        String s2 = os2.map(s -> s.toString())
                       .orElse("Empty");

        System.out.println(s1);
        System.out.println(s2);
    }
}

- NullPointerCaseStudy.java의 개선 결과

map과 orElse 메소드를 통해 처음 봤던 코드를 간략히 줄여보았다.

import java.util.Optional;

class Friend {
    String name;
    Company cmp;    // null 일 수 있음

    public Friend(String n, Company c) {
        name = n;
        cmp = c;
    }
    public String getName() { return name; }
    public Company getCmp() { return cmp; }
}

class Company {
    String cName;
    ContInfo cInfo;    // null 일 수 있음

    public Company(String cn, ContInfo ci) {
        cName = cn;
        cInfo = ci;
    }
    public String getCName() { return cName; }
    public ContInfo getCInfo() { return cInfo; }

}

class ContInfo {
    String phone;   // null 일 수 있음
    String adrs;    // null 일 수 있음

    public ContInfo(String ph, String ad) {
        phone = ph;
        adrs = ad;
    }
    public String getPhone() { return phone; }
    public String getAdrs() { return adrs; }

}

class NullPointerCaseStudy2 {
    public static void showCompAddr(Optional<Friend> f) {
        String addr = f.map(Friend::getCmp)
                .map(Company::getCInfo)
                .map(ContInfo::getAdrs)
                .orElse("There's no address information.");

        System.out.println(addr);
    }

    public static void main(String[] args) {

        ContInfo ci = new ContInfo("321-444-577", "Republic of Korea");
        Company cp = new Company("YaHo Co., Ltd.", ci);
        Friend frn = new Friend("LEE SU", cp);

        // 친구 정보에서 회사 주소를 출력
        showCompAddr(Optional.of(frn));
    }
}

- Optional 클래스의 flatMap 메소드

map은 람다식이 반환하는 내용물을 Optional로 감싸서 반환한다.
flatMap은 그냥 반환한다.

클래스의 멤버를 Optional로 두면 이 멤버와 관련된 코드 전반에 걸쳐서 코드의 개선을 기대할 수 있다.
이렇게 멤버를 Optional로 두는 경우에는 map보다 flatMap이 더 어울린다.

import java.util.Optional;

class Friend {
    String name;
    Optional<Company> cmp;    // null 일 수 있음

    public Friend(String n, Optional<Company> c) {
        name = n;
        cmp = c;
    }
    public String getName() { return name; }
    public Optional<Company> getCmp() { return cmp; }
}

class Company {
    String cName;
    Optional<ContInfo> cInfo;    // null 일 수 있음

    public Company(String cn, Optional<ContInfo> ci) {
        cName = cn;
        cInfo = ci;
    }
    public String getCName() { return cName; }
    public Optional<ContInfo> getCInfo() { return cInfo; }

}

class ContInfo {
    Optional<String> phone;   // null 일 수 있음
    Optional<String> adrs;    // null 일 수 있음

    public ContInfo(Optional<String> ph, Optional<String> ad) {
        phone = ph;
        adrs = ad;
    }
    public Optional<String> getPhone() { return phone; }
    public Optional<String> getAdrs() { return adrs; }

}

class NullPointerCaseStudy3 {
    public static void showCompAddr(Optional<Friend> f) {

        String addr = f.flatMap(Friend::getCmp)
                .flatMap(Company::getCInfo)
                .flatMap(ContInfo::getAdrs)
                .orElse("There's no address information.");

        System.out.println(addr);    
    }

    public static void main(String[] args) {
        Optional<ContInfo> ci = Optional.of(
                    new ContInfo(Optional.ofNullable(null), Optional.of("Republic of Korea"))
        );
        Optional<Company> cp = Optional.of(new Company("YaHo Co., Ltd.", ci));
        Optional<Friend> frn = Optional.of(new Friend("LEE SU", cp));

        // 친구 정보에서 회사 주소를 출력
        showCompAddr(frn);
    }
}

28-3. OptionalInt, OptionalLong, OptionalDouble 클래스

Optional 클래스는 제네릭 클래스이다.
따라서 기본 자료형을 다루기 위해서는 다른 방식이 필요하다.

- Optional과 OptionalXXX와의 차이점

import java.util.OptionalInt;

class OptionalIntBase {
    public static void main(String[] args) {
        OptionalInt oi1 = OptionalInt.of(3);
        OptionalInt oi2 = OptionalInt.empty();
        
        System.out.print("[Step 1.] : ");
        oi1.ifPresent(i -> System.out.print(i + "\t"));
        oi2.ifPresent(i -> System.out.print(i));
        System.out.println();

        System.out.print("[Step 2.] : ");
        System.out.print(oi1.orElse(100) + "\t");
        System.out.print(oi2.orElse(100) + "\t");
        System.out.println();
    }
}

기본적인 코드구성은 동일하다.
단, OptionalXXX 클래스들에는 map과 flatMap 메소드가 정의되어 있지 않다.

Java lang 카테고리 내 다른 글 보러가기

댓글 남기기