-
아이템3. 생성자나 열거 타입으로 싱글턴임을 보증하라Java/이펙티브 자바 2022. 10. 31. 16:50
첫번째 방법 : private 생성자 + public static final 필드
- 장점 : 간결하고 싱글턴임을 API에 들어낼 수 있다.
단점
1. 싱글톤을 사용하는 클라이언트 테스트하기 어려워진다.
ㆍ하지만 인터페이스를 만든다면 Mock를 만들어서 테스트 가능
public interface IElvis { void leaveTheBuilding(); void sing(); }
public class MockElvis implements IElvis{ @Override public void leaveTheBuilding() { } @Override public void sing() { System.out.println("You ain't nothin' but a hound dog."); } }
@Test void perform() { Concert concert = new Concert(new MockElvis()); concert.perform(); assertTrue(concert.isLightsOn()); assertTrue(concert.isMainStateOpen()); }
2. 리플렉션으로 private 생성자를 호출할 수 있다
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); // false System.out.println(elvis1 == Elvis.INSTANCE); // false } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { e.printStackTrace(); } }
ㆍ리플렉션도 막아야한다.
public static final Elvis INSTANCE = new Elvis(); private static boolean created; private Elvis() { if(created){ throw new UnsupportedOperationException("can't be created by constructor"); } created = true; }
3. 역직렬화 할 때 새로운 인스턴스가 생길 수 있다.
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); // false } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } }
ㆍ역직렬화시 readResolve함수가 사용됨. - 싱글톤 유지 가능
○ Override 처럼 동작
public class Elvis implements IElvis{ .... private Object readResolve() { return INSTANCE; } }
- 단점 : 간결하지 않다.
두번째 방법 : private 생성자 + 정적 팩터리 메서드
장점
1. API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다
// 코드3-2 정적 팩터리 방식의 싱글턴 public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() {} // public static Elvis getInstance() { return INSTANCE; } public static Elvis getInstance() { return new Elvis(); } public void leaveTheBuilding() { System.out.println("Whoa baby, I'm outta here!"); } public static void main(String[] args) { Elvis elvis = Elvis.getInstance(); elvis.leaveTheBuilding(); } }
2. 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다
// 코드 3-2 제네릭 싱글톤 팩토리 (24쪽) public class MetaElvis<T> { // 인스턴스 scope private static final MetaElvis<Object> INSTANCE = new MetaElvis<>(); private MetaElvis() {} @SuppressWarnings("unckecked") public static <E> MetaElvis<E> getInstance() {return (MetaElvis<E>) INSTANCE;} // static scope public void say(T t){ System.out.println(t); } public void leaveTheBuilding() { System.out.println("Whoa baby, I'm outta here!"); } public static void main(String[] args) { MetaElvis<String> elvis1 = MetaElvis.getInstance(); MetaElvis<Integer> elvis2 = MetaElvis.getInstance(); } }
- 내가 원하는 타입으로 형변환을 해줄 수 있다.
3. 정적 팩터리의 메서드 참조를 공급자(Supplier)로 사용할 수 있다
public class Concert { public void start(Supplier<Singer> singerSupplier){ Singer singer = singerSupplier.get(); singer.sing(); } public static void main(String[] args) { Concert concert = new Concert(); concert.start(Elvis::getInstance); } }
- 자바8 에는 @FunctionalInterface 붙어있는 기본적인 function 들을 제공한다.
- 메서드 참조를 공급자로 쓸 수도 있다. ( 메서드 레퍼런스로 활용할 수 있다. )
단점 : 위와 동일
세번째 방법 : 열거 타입
- 가장 간결한 방법이며, 직렬화와 리플렉션에도 안전하다
- 대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
// 열거 타입 방식의 싱글턴 - 바람직한 방법 (25쪽) public enum Elvis { INSTANCE; public void leaveTheBuilding() { System.out.println("기다려 자기야, 지금 나갈게!"); } }
- Enum은 Constructor을 가져올 수 없다. ( 실제론 존재 )
- 테스트 시에는 인터페이스를 선언하고 별도의 구현체 만들어주면 된다.
완벽공략
p23, 리플렉션 API로 private 생성자 호출하기
p24, 메서드 참조를 공급자로 사용할 수 있다
p24, Supplier<T>, 함수형 인터페이스
p24, 직렬화, 역직렬화, Serializable, transient
아이템3. 완벽공략11 - 메서드 참조
: 메소드 하나만 호출하는 람다 표현식을 줄여쓰는 방법
스태틱 메소드 레퍼런스
public class Person { LocalDate birthday; public Person(LocalDate birthday){ this.birthday = birthday; } 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, 3, 2))); people.add(new Person(LocalDate.of(2013, 1, 28))); people.sort(Person::compareByAge); } }
인스턴스 메소드 레퍼런스
public class Person { LocalDate birthday; public Person(LocalDate birthday){ this.birthday = birthday; } 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, 3, 2))); people.add(new Person(LocalDate.of(2013, 1, 28))); Person person = new Person(null); people.sort(person::compareByAge); } }
임의 객체의 인스턴스 메소드 레퍼런스
ㆍ인스턴스 메소드 레퍼런스를 보면 불필요한 인스턴스를 꼭 선언해야하나? 할때 사용하는 방법
public class Person { LocalDate birthday; public Person(LocalDate birthday){ this.birthday = birthday; } public int compareByAge(Person b){ return this.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, 3, 2))); people.add(new Person(LocalDate.of(2013, 1, 28))); // Comparator<Person> compareByAge = Person::compareByAge; people.sort(Person::compareByAge); } }
ㆍ첫번째 인자가 자기자신이다.
생성자 레퍼런스
public class Person { LocalDate birthday; public Person(LocalDate birthday){ this.birthday = birthday; } public static void main(String[] args) { List<LocalDate> dates = new ArrayList<>(); dates.add(LocalDate.of(1982,7,15)); dates.add(LocalDate.of(1982,7,15)); dates.add(LocalDate.of(1982,7,15)); // Function<LocalDate, Person> aNew = Person::new; // List<Person> collect = dates.stream().map(aNew).collect(Collectors.toList()); List<Person> collect = dates.stream().map(Person::new).collect(Collectors.toList()); } }
아이템3. 완벽공략12 - 함수형 인터페이스
: 자바가 제공하는 기본 함수형 인터페이스
- 함수형 인터페이스는 람다 표현식과 메소드 참조에 대한 "타겟 타입"을 제공한다.
- 타겟 타입은 변수 할당, 메소드 호출, 타입 변환에 활용할 수 있다
- 자바에서 제공하는 기본 함수형 인터페이스 익혀 둘 것 (java.util.function 패키지)
ㆍex) Predicate, Function, Supplier, Consumer
public static void main(String[] args) { // 첫번째인자 : 인풋, 두번째인자 : 아웃풋 Function<Integer, String> intToString = Object::toString; // 인풋이 없고 아웃풋만 있다. Supplier<Person> personSupplier = Person::new; // Function<LocalDate, Person> personFunction = Person::new; // 매개변수가 다른 생성자 참조시 // args는 있지만 return이 없다. Consumer<Integer> integerConsumer = System.out::println; // 인자를 하나 받아서 boolean을 리턴한다. Predicate<Integer> predicate; }
- 함수형 인터페이스를 만드는 방법
- 심화 학습 1) Understanding Java method invocation
- 심화 학습 2) LambdaMetaFactory
아이템3. 완벽공략13 - 객체를 바이트스트림으로 상호 변환하는 기술
- 바이트스트림으로 변환한 객체를 파일로 저장하거나 네트워크를 통해 다른 시스템으로 전송할 수 있다.
- Serializable 인터페이스 구현
- transient를 사용해서 직렬화 하지 않을 필드 선언하기
public class Book implements Serializable { ... // 직렬화하고 싶지 않은 필드일 때 transient 사용 private transient int numberOfSold; }
ㆍ직렬화를 한 객체에는 numberOfSold 값이 있지만, 역직렬화한 객체에는 numberOfSold 값이 0이된다.
* static 한 필드는 직렬화되지 않는다.
- serialVersionUID는 언제 왜 사용하는가?
ㆍ클래스가 바뀌면 serialVersionUID가 변경된다.
ㆍ직렬화 후 클래스가 변경되면 역직렬화가 불가능하다.
ㆍ하지만 UID를 이용하면 클래스가 변경되도 역직렬화가 가능하다.
- 심화 학습) 객체 직렬화 스팩, Externalizable
* 오브젝트를 바이트스트림으로 만들어주는 것이 직렬화고, 바이트스트림으로 되어있는 것을 보관하는 과정이 역직렬화이다.
'Java > 이펙티브 자바' 카테고리의 다른 글
아이템5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) 2022.11.01 아이템4. 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) 2022.11.01 아이템2. 생성자에 매개변수가 많다면 빌더를 고려하라 (0) 2022.10.31 아이템1. 생성자 대신 정적 팩터리 메서드를 고려하라 (0) 2022.10.28 자바 이펙티브 - 1. 들어가기 (0) 2022.06.10