본문 바로가기
Effective Java

item 3

by irerin07 2023. 9. 21.
728x90

아이템 3. 생성자가 열거 타입으로 싱글턴임을 보증하라.

방법 1. private 생성자 + public static final 필드

public class Elvis {
  /**
   * 싱글톤 오브젝트
   */
  public static final Elvis INSTANCE = new Elvis();

  private Elvis() {}

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm otta here!");
  }

  public void sing() {
    System.out.println("I'll have a blue~ Christmas without you~");
  }

}

이 방식의 장점이라면 굉장히 간결하고, 싱글턴임을 API에 드러낼 수 있다.

하지만 이 역시 단점을 동반하는데 다음과 같다

  1. (인터페이스 없이는) 싱글톤을 사용하는 클라이언트 코드를 테스트하기 어려워진다.
  2. 리플렉션으로 private 생성자를 호출할 수 있다.
  3. 역직렬화 할 떄 새로운 인스턴스가 생길 수 있다.

각각의 단점을 자세히 알아보자

  • (인터페이스 없이는) 싱글톤을 사용하는 클라이언트 코드를 테스트하기 어려워진다.
public class Concert {
  
  private boolean lightsOn;
  
  private boolean mainStateOpen;
  
  private Elvis elvis;
  
  public Concert(Elvis elvis) {
    this.elvis = elvis;
  }
  
  public void perform() {
    mainStateOpen = true;
    lightsOn = true;
    elvis.sing();
  }
  
  public boolean isLightsOn() {
    return lightsOn;
  }
  
  public boolean isMainStateOpen() {
    return mainStateOpen;
  }
  
}

Concert는 Elvis를 사용하는 Elvis의 클라이언트 코드이다.

이 Concert를 테스트하기 위해서는 매번 Elvis를 부르는 것은 비효율 적이다. 그렇기에 우리는 Elvis의 대역을 써야하는데 지금은 대역으로 쓸만한 것이 보이지 않는다.

그렇기 때문에 실제 테스트에서도 매번 Elvis를 호출해야 한다.

class ConcertTest {

	@Test
	void perform() {
		Concert concert = new Concert(Elvis.INSTANCE);
		concert.perform();
		
		assertTrue(concert.isLightsOn());
		assertTrue(concert.isMainStateOpen());
	}

}

매번 실제 객체를 호출하는 것은 비효율적이다.

이를 개선하기 위해선 Interface를 사용할 수 있을것이다.

public interface IElvis {

  void leaveTheBuilding();

  void sing();

}
public class Elvis implements IElvis{
  /**
   * 싱글톤 오브젝트
   */
  public static final Elvis INSTANCE = new Elvis();

  private Elvis() {}

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm otta here!");
  }

  public void sing() {
    System.out.println("I'll have a blue~ Christmas without you~");
  }

}
public class Concert {

  private boolean lightsOn;

  private boolean mainStateOpen;

  private IElvis elvis;

  public Concert(IElvis elvis) {
    this.elvis = elvis;
  }

  public void perform() {
    mainStateOpen = true;
    lightsOn = true;
    elvis.sing();
  }

  public boolean isLightsOn() {
    return lightsOn;
  }

  public boolean isMainStateOpen() {
    return mainStateOpen;
  }

}
public class MockElvis implements IElvis {
	@Override
	public void leaveTheBuilding(){
	}
	
	@Override
	public void sing() {
		System.out.println("You ain't nothin' but a hound dog.");
	}
}
class ConcertTest {

	@Test
	void perform() {
		Concert concert = new Concert(new MockElvis());
		concert.perform();
		
		assertTrue(concert.isLightsOn());
		assertTrue(concert.isMainStateOpen());
	}

}

이제 좀 더 효율적으로 테스트를 진행할 수 있게 되었다.

즉 이 방법을 선택하면 싱글톤을 사용하는 클라이언트를 테스트함에 있어 인터페이스가 필수적으로 있어야 한다.

  • 리플렉션으로 private 생성자를 호출할 수 있다.

리플렉션을 사용하면 싱글톤이 깨질 수 있다는 내용이다.

public class ElvisReflection {

  public static void main(String[] args) {
    try {
      Constructor<Elvis> defaultConstructor = Elvis.class.getDeclaredConstructor();

      defaultConstructor.setAccessible(true);

      Elvis elvis1 = defaultConstructor.newInstance();
      Elvis elvis2 = defaultConstructor.newInstance();

			System.out.println(elvis1 == elvis2);
			System.out.println(elvis1 == Elvis.INSTANCE);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

위의 코드를 실행해보면 알 수 있지만 elvis1 과 elvis2는 싱글톤임에도 불구하고 서로 다른 인스턴스임을 확인할 수 있다. System.out.println(elvis1 == elvis2);

심지어 elvis1과 elvis2는 우리가 사용하고자 하는 Elvis의 인스턴스와도 다르다는 것을 확인할 수 있다. System.out.println(elvis1 == Elvis.INSTANCE);

이런 문제를 막기 위해 인스턴스가 두 번 생성되는 것을 막기를 권장한다.

public class Elvis implements IElvis{
  /**
   * 싱글톤 오브젝트
   */
  public static final Elvis INSTANCE = new Elvis();
  **private static boolean cretaed;**

  private Elvis() {
    **if (cretaed) {
      throw new UnsupportedOperationException("Cannot be created by constructor");
    }

    cretaed = true;**
  }

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm otta here!");
  }

  public void sing() {
    System.out.println("I'll have a blue~ Christmas without you~");
  }

}
  • 역직렬화 할 떄 새로운 인스턴스가 생길 수 있다.
public class ElvisSerialization {
  public static void main(String[] args) {
    try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("elvis.obj"))) {
      out.writeObject(Elvis.INSTANCE);
    } catch (IOException e){
      e.printStackTrace();
    }

    try (ObjectInput in = new ObjectInputStream(new FileInputStream("elvis.obj"))) {
      Elvis elvis3 = (Elvis) in.readObject();
      System.out.println(elvis3 == Elvis.INSTANCE);
    } catch (IOException | ClassNotFoundException e) {
      e.printStackTrace();
    }
  }
  
}

직렬화로 객체의 정보를 어딘가에 저장할 수 있고, 역직렬화로 어딘가에 저장된 객체 정보를 읽어올 수 있다.

위의 코드에서 객체의 정보를 저장하고 읽어올 때 새로운 인스턴스가 생성이 되는것을 확인 할 수 있다. System.out.println(elvis3 == Elvis.INSTANCE);

역직렬화를 할 때 호출되는 메소드가 있는데 그 메소드를 오버라이딩하는 방법이 있다. (다만 기선님은 이것을 오버라이딩과는 좀 다른 느낌이라고 설명하셨다.)

public class Elvis implements IElvis{
  /**
   * 싱글톤 오브젝트
   */
  public static final Elvis INSTANCE = new Elvis();
  private static boolean cretaed;

  private Elvis() {
    if (cretaed) {
      throw new UnsupportedOperationException("Cannot be created by constructor");
    }

    cretaed = true;
  }

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm otta here!");
  }

  public void sing() {
    System.out.println("I'll have a blue~ Christmas without you~");
  }

  **private Object readResolve() {
    return INSTANCE;
  }**

}

위 코드에서 readResolve() 라는 메서드를 추가 했는데 이 메소드에는 @Override 어노테이션을 붙일 수 없다. 문법적으로 오버라이딩이라 볼 수 없다는 것이다.

하지만 역직렬화 하는 과정에서 해당 메서드가 호출이 되고, 사용된다. 즉, 우리가 재정의한 readResolve()가 사용이 된다는 것이다.

위처럼 코드를 추가한 뒤 다시 실행하면 생성된 인스턴스와 우리가 사용하고자 하는 인스턴스가 동일함을 확인할 수 있다.

이제 Elvis는 Reflection으로도 우회할 수 없고 역직렬화 후에도 객체가 새로 생성되지 않게 되었지만 큰 장점이었던 간결함을 잃게 되었다.

이를 대체할 수 있는 다른 방법을 알아보자

방법 2. private 생성자 + 정적 팩터리 메서드

public class Elvis {
  private static final Elvis INSTANCE = new Elvis();
  private Elvis() {

  }

  public static Elvis getInstance() {
    return INSTANCE;
  }

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm outta here!");
  }

}

이 방법 역시 생성자를 사용하는 방법이기 때문에 이전과 동일한 문제를 가지고 있다.

  1. (인터페이스 없이는) 싱글톤을 사용하는 클라이언트 코드를 테스트하기 어려워진다.
  2. 리플렉션으로 private 생성자를 호출할 수 있다.
  3. 역직렬화 할 떄 새로운 인스턴스가 생길 수 있다.

하지만 이 방법은 다른 장점들을 가지고 있는데 다음과 같다

  1. API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
  2. 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.
  3. 정적 팩터리의 메소드 참조를 공급자(Supplier)로 사용할 수 있다.

하나씩 알아보자

  1. API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
public class Elvis {
  private static final Elvis INSTANCE = new Elvis();
  private Elvis() {

  }

  public static Elvis getInstance() {
    **return new Elvis();**
  }

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm outta here!");
  }

}

위와 같이 코드를 변경하면 매번 새로운 인스턴스를 생성해서 반환하게 되지만 클라이언트는 여전히 getInstance()메서드를 통해 변경 없이 인스턴스를 가져올 수 있다.

메서드를 통해 인스턴스를 가져오도록 하면 클라이언트 코드에 영향을 주지 않으면서 변경을 할 수 있다는 장점이 있다.

  1. 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.
public class MetaElvis<T> {

  private static final MetaElvis<Object> INSTANCE = new MetaElvis<>();

  private MetaElvis() {

  }

  // 제네릭 싱글톤 팩터리
  @SuppressWarnings("unchecked")
  public static <T> MetaElvis<T> getInstance() {
    return (MetaElvis<T>) INSTANCE;
  }

  public void say(T t) {
    System.out.println(t);
  }

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm outta here!");
  }

}
MetaElvis<String> elvis1 = MetaElvis.getInstance();
MetaElvis<Integer> elvis2 = MetaElvis.getInstance();

제네릭한 타입으로 동일한 싱글턴 인스턴스를 사용하고 싶을 때 제네릭 싱글톤 팩터리를 사용할 수 있다.

이렇게 구현하면 서로 다른 두 인스턴스를 동일한 인스턴스지만(싱글턴) 우리가 원하는 타입으로 각각의 인스턴스를 형변환 할 수 있게 된다는 장점이 있다.

다만 둘의 해쉬코드는 동일하지만 서로 타입이 다르기 때문에 == 로는 비교를 할 수 없고 .equals() 혹은 Objects.equals()를 통해 비교해야 한다.

public class MetaElvis<T> {

  private static final MetaElvis<Object> INSTANCE = new MetaElvis<>();

  private MetaElvis() {

  }

  // 제네릭 싱글톤 팩터리
  @SuppressWarnings("unchecked")
  public static <T> MetaElvis<T> getInstance() {
    return (MetaElvis<T>) INSTANCE;
  }

  public void say(T t) {
    System.out.println(t);
  }

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm outta here!");
  }

}

위 코드에서 getInstance() 메소드를 보면 <T>가 선언 되어 있는것을 볼 수 있다.

이미 클래스에서 T가 선언 되어 있는데 왜 다시 한 번 더 선언을 한것일까?

이유는 두 개의 T는 같은 이름일지라도 서로의 scope가 다르다.

클래스에 선언된 T는 인스턴스 스코프이고, getInstance()메소드에 선언된 T는 스태틱한 스코프이다.

하지만 여기서 또 주의해야 할 것이 say() 메서드에 있는 T는 클래스에 선언된 T와 같은 T 라는 것이다.

  1. 정적 팩터리의 메소드 참조를 공급자(Supplier)로 사용할 수 있다.
public interface Singer {

  void sing();

}
public class Concert {

  public void start(Supplier<Singer> singerSupplier) {
    Singer singer = singerSupplier.get();
    singer.sing();
  }

}
public class Elvis implements Singer{

  private static final Elvis INSTANCE = new Elvis();

  private Elvis() { }

  public static Elvis getInstance() { return INSTANCE; }

  public void leaveTheBuilding() {
    System.out.println("Whoa baby, I'm outta here!");
  }

  @Override
  public void sing() {
    System.out.println("my way~");
  }
  
}

Supplier는 자바 8에서 들어간 functional interface중 하나이다.

Supplier 인터페이스만 만족하면 어떠한 메서드든 Supplier Functional Type으로 사용할 수 있는데, 어떤 타입이든 하나를 리턴할 수 있으면 된다.

public static void main(String[] args) {
  Concert concert = new Concert();
  concert.start(Elvis::getInstance);   
}
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

Concert의 start() 메서드는 Singer 타입의 Supplier를 전달 받아 Supplier의 get() 메서드를 사용해 Singer 인스턴스를 사용한다.

우리가 기존에 만든 getInstance() 메서드는 Supplier interface에 준하는 기능인데, 이는 getInstance() 메서드가 파라미터 없이 호출되어 인스턴스를 리턴해주는 Supplier의 get() 메서드에 준하는 Key 메서드이기 때문이다.

인자 없는 메서드를 호출해서 무언가를 리턴해주는 메서드들은 Supplier에 준하는 Key 메서드라 볼 수 있다.

그렇기에 우리는 Supplier를 따로 구현하지 않아도 이에 준하는 getInstance() 메서드를 Supplier의 구현체처럼 파라미터에 넘기고 넘긴 Suplier의 get()을 호출하면 getInstance()에서 리턴하는 Elvis가 넘어오게 된다.

익명 내부 클래스와 람다는 서로 다르다.

방법 3. 열거 타입

가장 간결한 방법이며 직렬화와 리플렉션에도 안전하다.

대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

public enum Elvis {
  INSTANCE;

  public void leaveTheBuilding() {
    System.out.println("lala");
  }
  
}

리플렉션에 안전한 이유는 생성자를 가져올 수 없기 때문이다.

실제로 컴파일된 코드를 보면 생성자를 확인할 수 있지만 이늄은 태생적으로 new 를 사용해 만들 수 없게 되어있다. 오직 열거형으로 선언 된 것들만 사용할 수 있다.

테스트시에도 해당 이늄이 인터페이스를 구현하도록 만들어 테스트시에 쉽게 사용할 수 있다.

메서드 참조

메서드 하나만 호출하는 람다 표현식을 줄여쓰는 방법

public class Person {

  LocalDate birthday;

  public Person(LocalDate birthday) {
    this.birthday = birthday;
  }

  public int getAge() {
    return LocalDate.now().getYear() - birthday.getYear();
  }

  public static int compareByAge(Person a, Person b) {
    return a.birthday.compareTo(b.birthday);
  }

  public static void main(String[] args) {
    List<Person> people = new ArrayList<>();
    people.add(new Person(LocalDate.of(1982, 7, 15)));
    people.add(new Person(LocalDate.of(2011, 8, 4)));
    people.add(new Person(LocalDate.of(2013, 4, 30)));

    **people.sort(new Comparator<Person>() {
      @Override
      public int compare(Person a, Person b) {
        return a.birthday.compareTo(b.birthday);
      }
    });**
  }

}

자바 8 이전에는 내부 익명 클래스를 사용하여 직접 구현하는 방식을 사용했다.

위 예제에서는 직접 Comparator를 직접 구현해 사용하고 있다.

하지만 자바 8부터는 람다 표현식이 지원이 되고 아래처럼 바꿔 쓸 수 있게 되었다.

public class Person {

  LocalDate birthday;

  public Person(LocalDate birthday) {
    this.birthday = birthday;
  }

  public int getAge() {
    return LocalDate.now().getYear() - birthday.getYear();
  }

  public static int compareByAge(Person a, Person b) {
    return a.birthday.compareTo(b.birthday);
  }

  public static void main(String[] args) {
    List<Person> people = new ArrayList<>();
    people.add(new Person(LocalDate.of(1982, 7, 15)));
    people.add(new Person(LocalDate.of(2011, 8, 4)));
    people.add(new Person(LocalDate.of(2013, 4, 30)));

    **people.sort((p1, p2) -> p1.birthday.compareTo(p2.birthday));
		// Comparator<Person> personComparator = (p1, p2) -> p1.birthday.compareTo(p2.birthday);
    // people.sort(personComparator);**
  }

}

개발을 하다 보면 람다 표현식을 자주 사용하게 되는데 만약 이 람다 표현식을 사용해서 하는 일이 하나의 메서드만 호출하면 끝나는 경우가 있다.

아래의 코드는 우리가 미리 만들어둔 compareByAge() 메서드 하나만 호출하여 나이를 비교하고 있다.

public class Person {

  LocalDate birthday;

  public Person(LocalDate birthday) {
    this.birthday = birthday;
  }

  public int getAge() {
    return LocalDate.now().getYear() - birthday.getYear();
  }

  public static int compareByAge(Person a, Person b) {
    return a.birthday.compareTo(b.birthday);
  }

  public static void main(String[] args) {
    List<Person> people = new ArrayList<>();
    people.add(new Person(LocalDate.of(1982, 7, 15)));
    people.add(new Person(LocalDate.of(2011, 8, 4)));
    people.add(new Person(LocalDate.of(2013, 4, 30)));

    **people.sort((p1, p2) -> Person.compareByAge(p1, p2));**
  }

}

이런 경우 우리는 메서드 레퍼런스를 통해 람다식을 좀 더 간결하게 쓸 수 있다.

public class Person {

  LocalDate birthday;

  public Person(LocalDate birthday) {
    this.birthday = birthday;
  }

  public int getAge() {
    return LocalDate.now().getYear() - birthday.getYear();
  }

  public static int compareByAge(Person a, Person b) {
    return a.birthday.compareTo(b.birthday);
  }

  public static void main(String[] args) {
    List<Person> people = new ArrayList<>();
    people.add(new Person(LocalDate.of(1982, 7, 15)));
    people.add(new Person(LocalDate.of(2011, 8, 4)));
    people.add(new Person(LocalDate.of(2013, 4, 30)));

    **people.sort(Person::compareByAge); //스태틱 메소드 레퍼런스**
  }

}

메소드 레퍼런스에는 크게 4종류가 있다.

  1. 스태틱 메소드 레퍼런스 : 말 그대로 스태틱한 메소드를 참조하는 방법
  2. 인스턴스 메소드 레퍼런스
    1. 인스턴스 메소드를 참조하기 위해서는 해당 인스턴스를 생성한 뒤 사용해야 한다.
  3. 임의 객체의 인스턴스 메소드 레퍼런스
    1. 인스턴스 메소드를 사용하고 싶지만 굳이 해당 인스턴스를 생성한 뒤 사용해야 할 필요가 없을때 사용한다.
    2. 스태틱 메소드 레퍼런스와 똑같이 클래스 이름으로 해당 메서드를 참조 하는데 이때 오류가 날 수 있다
      1. 이는 임의 객체의 인스턴스 메소드 레퍼런스를 사용할 수 없다는 오류가 아니라 호환이 안되는 상태이기 때문에 코드를 수정해야 한다.
      public class Person {
      
        LocalDate birthday;
      
        public Person(LocalDate birthday) {
          this.birthday = birthday;
        }
      
        public int getAge() {
          return LocalDate.now().getYear() - birthday.getYear();
        }
      
        public int compareByAge(Person a, Person b) {
          return a.birthday.compareTo(b.birthday);
        }
      
        public static void main(String[] args) {
          List<Person> people = new ArrayList<>();
          people.add(new Person(LocalDate.of(1982, 7, 15)));
          people.add(new Person(LocalDate.of(2011, 8, 4)));
          people.add(new Person(LocalDate.of(2013, 4, 30)));
      
          people.sort(Person::compareByAge); //이 라인에서 오류가 나는 이유는 현재 compareByAge()메서드가 Comparator와 호환 가능한 상태가 아니기 때문이다.
        }
      
      }
      
      위 예시에서는 현재 compareByAge() 메서드가 Comparator와 호환이 되는 상태가 아니가.이는 임의 객체 메서드 레퍼런스의 특성 떄문인데, 임의 객체에 대한 메서드 레퍼런스의 첫번째 인자는 자기 자신, 즉 본인의 인스턴스가 된다.
      public class Person {
      
        LocalDate birthday;
      
        public Person(LocalDate birthday) {
          this.birthday = birthday;
        }
      
        public int getAge() {
          return LocalDate.now().getYear() - birthday.getYear();
        }
      
        **public int compareByAge(Person b) { //첫번쨰 인자가 자기 자신이 되기 때문에 비교대상만 받고
          return this.birthday.compareTo(b.birthday); //본인 인스턴스가 되기 때문에 this를 사용한다
        }**
      
        public static void main(String[] args) {
          List<Person> people = new ArrayList<>();
          people.add(new Person(LocalDate.of(1982, 7, 15)));
          people.add(new Person(LocalDate.of(2011, 8, 4)));
          people.add(new Person(LocalDate.of(2013, 4, 30)));
      
          people.sort(Person::compareByAge);
        }
      
      }
      
      이제 어떤 값이 넘어와도 본인과 비교를 하기 때문에 인자가 하나만 넘어오면 된다. 그렇기 때문에 인자를 하나만 받음에도 Comparator 인터페이스에 준하게 되는 것이다.
    3. 그러므로 다음과 같이 compareByAge() 메소드를 바꾸어 줘야 한다.
    4. 그런데 Comparator는 분명 두개의 인자를 받아서 하나의 int 리턴값을 던져주는 형태이기 때문에 호환이 되야 하는거 아닌가 하는 의문이 들 수 있다.
  4. 생성자 레퍼런스
public class Person {

  LocalDate birthday;

  public Person(LocalDate birthday) {
    this.birthday = birthday;
  }

  public int getAge() {
    return LocalDate.now().getYear() - birthday.getYear();
  }

  public int compareByAge(Person b) {
    return this.birthday.compareTo(b.birthday);
  }

  public static void main(String[] args) {
    List<LocalDate> dates = new ArrayList<>();
    dates.add(LocalDate.of(1989, 7, 21));
    dates.add(LocalDate.of(1988, 7, 21));
    dates.add(LocalDate.of(1987, 7, 21));

    **List<Person> collect = dates.stream().map(Person::new).collect(Collectors.toList());**

    List<Person> people = new ArrayList<>();
    people.add(new Person(LocalDate.of(1982, 7, 15)));
    people.add(new Person(LocalDate.of(2011, 8, 4)));
    people.add(new Person(LocalDate.of(2013, 4, 30)));

    people.sort(Person::compareByAge);
  }

}

그런데 만약 다음과 같이 기본 생성자가 있는 경우, 람다식에서 기본 생성자를 사용하고 싶을때는 어떻게 해야 할까?

public class Person {

  LocalDate birthday;

	public Person() {
	}

  public Person(LocalDate birthday) {
    this.birthday = birthday;
  }

  public int getAge() {
    return LocalDate.now().getYear() - birthday.getYear();
  }

  public int compareByAge(Person b) {
    return this.birthday.compareTo(b.birthday);
  }

  public static void main(String[] args) {
    List<LocalDate> dates = new ArrayList<>();
    dates.add(LocalDate.of(1989, 7, 21));
    dates.add(LocalDate.of(1988, 7, 21));
    dates.add(LocalDate.of(1987, 7, 21));

    List<Person> collect = dates.stream().map(Person::new).collect(Collectors.toList());

    List<Person> people = new ArrayList<>();
    people.add(new Person(LocalDate.of(1982, 7, 15)));
    people.add(new Person(LocalDate.of(2011, 8, 4)));
    people.add(new Person(LocalDate.of(2013, 4, 30)));

    people.sort(Person::compareByAge);
  }

}

이런 경우 함수형 인터페이스를 사용해 해결할 수 있다.

Supplier<Person> personSupplier = Person::new;
Function<LocalDate, Person> personFunction = Person::new;

두 함수형 인터페이스는 동일하게 Person의 인스턴스를 반환하지만 Supplier는 기본 생성자를 활용해 생성된 Person을, Function은 LocalDate를 받는 생성자를 활용해 생성된 Person의 인스턴스를 반환한다.

https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

함수형 인터페이스

가장 기본적인 4개의 함수형 인터페이스

  1. Function
    1. 두개의 제네릭 타입을 갖는다. 첫번째는 Input, 두번째는 Output.
    Function<Integer, String> intToString = (i) -> "hello";
    
  2. Supplier
    1. 오로지 Output만 제공
  3. Consumer
    1. 오로지 Input만 있음
  4. Predicate
    1. 일종의 Function으로 볼 수 있다.
    2. Input을 입력 받아 Boolean을 Return한다.

함수형 인터페이스는 람다 표현식과 메소드 참조에 대한 “타겟 타입”을 제공한다.

public class Main {
  public static void main(String[] args) {
    List<LocalDate> dates = new ArrayList<>();
    dates.add(LocalDate.of(1989, 7, 21));
    dates.add(LocalDate.of(1989, 7, 22));
    dates.add(LocalDate.of(1989, 7, 23));

    Predicate<LocalDate> localDatePredicate = d -> d.isBefore(LocalDate.of(2000, 1, 1));
    Function<LocalDate, Integer> getYear = LocalDate::getYear;
    
    List<Integer> collect = dates.stream()
      .filter(localDatePredicate)
      .map(getYear)
      .collect(Collectors.toList());
  }
  
}

위 코드에서 보이는 Predicate, Function이 타겟 타입이라 불리는데 이들이 바로 함수형 인터페이스이다.

만약 자바에서 제공해주는 기본 함수형 인터페이스 외에 사용하고 싶다면 직접 정의 할 수 있다.

@FunctionalInterface
public interface MyFunction {
  String valueOf(Integer integer);
}

정의 하는 방법은 다음과 같다

  1. 인터페이스 안에는 구현되어있지 않은 선언의 경우 단 한개만 존재할 수 있다.
    1. 만약 선언되지 않은 메서드 하나만 존재하는 인터페이스의 경우 @FunctionalInterface 어노테이션 없이도 함수형 인터페이스 취급을 받는다.

https://blogs.oracle.com/javamagazine/post/understanding-java-method-invocation-with-invokedynamic

https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html

728x90

'Effective Java' 카테고리의 다른 글

item 4  (0) 2023.10.04
item 2  (0) 2023.09.21
item 1  (0) 2023.09.09