ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아이템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

     

     

    * 오브젝트를 바이트스트림으로 만들어주는 것이 직렬화고, 바이트스트림으로 되어있는 것을 보관하는 과정이 역직렬화이다.

     

     

    댓글

Designed by Tistory.