아이템 20. 추상 클래스보다 인터페이스를 우선하라.

무한성장개발자·2024년 10월 20일

아이템 20. 핵심 정리 - 인터페이스의 장점

package me.whiteship.chapter04.item20.defaultmethod;

import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public interface TimeClient {

    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();

    static ZoneId getZonedId(String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString + "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    ZonedDateTime getZonedDateTime(String zoneString);
//    default ZonedDateTime getZonedDateTime(String zoneString) {
//        return ZonedDateTime.of(getLocalDateTime(), getZonedId(zoneString));
//    }

}

ZonedDateTime getZonedDateTime(String zoneString); 가 추가된 경우 기존에 implements한 코드들에서 전부 해당 기능이 깨지기 때문에 컴파일 오류가 난다.

default 메소드로 제공할 수 있으면 굉장히 손쉽게 기능을 확장하고 인터페이스를 진화시킬 수 있다.

package me.whiteship.chapter04.item20.defaultmethod;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class SimpleTimeClient implements TimeClient {

    private LocalDateTime dateAndTime;

    public SimpleTimeClient() {
        dateAndTime = LocalDateTime.now();
    }

    public void setTime(int hour, int minute, int second) {
        LocalDate currentDate = LocalDate.from(dateAndTime);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(currentDate, timeToSet);
    }

    public void setDate(int day, int month, int year) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime currentTime = LocalTime.from(dateAndTime);
        dateAndTime = LocalDateTime.of(dateToSet, currentTime);
    }

    public void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
    }

    public LocalDateTime getLocalDateTime() {
        return dateAndTime;
    }

    public String toString() {
        return dateAndTime.toString();
    }

    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println(myTimeClient);
        System.out.println(myTimeClient.getZonedDateTime("America/Los_Angeles"));
    }

}

package me.whiteship.chapter04.item20.typeframework;

public interface SingerSongwriter extends Singer, Songwriter{

    AudioClip strum();
    void actSensitive();
}
package me.whiteship.chapter04.item20.typeframework;

public interface Singer {

    AudioClip sing(Song song);
}
package me.whiteship.chapter04.item20.typeframework;

public interface Songwriter {

    Song compose(int shartPosition);
}
package me.whiteship.chapter04.item18;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;

// 코드 18-1 잘못된 예 - 상속을 잘못 사용했다! (114쪽)
public class InstrumentedHashSet<E> extends HashSet<E> {
    // 추가된 원소의 수
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    private int getAddCount() {
        return addCount;
    }

    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(List.of("틱", "탁탁", "펑"));
        System.out.println(s.getAddCount());
    }
}

이것은 상위클래스의 동작에 따라 구현이 달라질 수 있게 된다.

package me.whiteship.chapter04.item18;

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

// 코드 18-3 재사용할 수 있는 전달 클래스 (118쪽)
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }

    public void clear()               { s.clear();            }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty()          { return s.isEmpty();   }
    public int size()                 { return s.size();      }
    public Iterator<E> iterator()     { return s.iterator();  }
    public boolean add(E e)           { return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection<?> c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
                                   { return s.addAll(c);      }
    public boolean removeAll(Collection<?> c)
                                   { return s.removeAll(c);   }
    public boolean retainAll(Collection<?> c)
                                   { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public <T> T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

추상클래스라고 가정해서 인터페이스를 통해서 개발을 했다고 구현했다고 쳐보자.

package me.whiteship.chapter04.item18;

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

// 코드 18-3 재사용할 수 있는 전달 클래스 (118쪽)
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }

    public void clear()               { s.clear();            }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty()          { return s.isEmpty();   }
    public int size()                 { return s.size();      }
    public Iterator<E> iterator()     { return s.iterator();  }
    public boolean add(E e)           { return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection<?> c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
                                   { return s.addAll(c);      }
    public boolean removeAll(Collection<?> c)
                                   { return s.removeAll(c);   }
    public boolean retainAll(Collection<?> c)
                                   { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public <T> T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

인터페이스를 가정한 경우 내부구현이 없으니까 구현이 바뀔게 없다. 굉장히 안전한 확장 방법이다.
상위클래스에서 메소드가 추가될 수도 있다. 추가된 경우 추가된 것을 확인 가능하다.
default, static을 사용해서 추가 구현을 할 수 있다.

아이템 20. 핵심 정리 - 인터페이스와 추상 골격(skeletal) 클래스

package me.whiteship.chapter04.item20.skeleton;

import java.util.AbstractList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

// 코드 20-1 골격 구현을 사용해 완성한 구체 클래스 (133쪽)
public class IntArrays {
    static List<Integer> intArrayAsList(int[] a) {
        Objects.requireNonNull(a);

        // 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다.
        // 더 낮은 버전을 사용한다면 <Integer>로 수정하자.
        return new AbstractList<>() {
            @Override public Integer get(int i) {
                return a[i];  // 오토박싱(아이템 6)
            }

            @Override public Integer set(int i, Integer val) {
                int oldVal = a[i];
                a[i] = val;     // 오토언박싱
                return oldVal;  // 오토박싱
            }

            @Override public int size() {
                return a.length;
            }
        };
    }

    public static void main(String[] args) {
        int[] a = new int[10];
        for (int i = 0; i < a.length; i++)
            a[i] = i;

        List<Integer> list = intArrayAsList(a);
        Collections.shuffle(list);
        System.out.println(list);
    }
}

모든 규약에 다 맞추려면 엄청나게 해야 하는데 AbstractClass를 사용하면 일부만 구현해도 된다.

package me.whiteship.chapter04.item20.multipleinheritance;

public abstract class AbstractCat {

    protected abstract String sound();

    protected abstract String name();
}
package me.whiteship.chapter04.item20.multipleinheritance;

public class MyCat extends AbstractCat implements Flyable {

    private MyFlyable myFlyable = new MyFlyable();

    @Override
    protected String sound() {
        return "인싸 고양이 두 마리가 나가신다!";
    }

    @Override
    protected String name() {
        return "유미";
    }

    public static void main(String[] args) {
        MyCat myCat = new MyCat();
        System.out.println(myCat.sound());
        System.out.println(myCat.name());
        myCat.fly();
    }

    @Override
    public void fly() {
        this.myFlyable.fly();
    }

    private class MyFlyable extends AbstractFlyable {
        @Override
        public void fly() {
            System.out.println("날아라.");
        }
    }
}
package me.whiteship.chapter04.item20.multipleinheritance;

public interface Flyable {

    void fly();
}

위 내용이 시뮬레이트한 다중상속이다.

아이템 20. 완벽 공략 - 템플릿 메서드 패턴

package me.whiteship.chapter04.item20.templatemethod;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public abstract class FileProcessor {

    private String path;

    public FileProcessor(String path) {
        this.path = path;
    }

    public final int process() {
        try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
            int result = 0;
            String line = null;
            while((line = reader.readLine()) != null) {
                result = getResult(result, Integer.parseInt(line));
            }
            return result;
        } catch (IOException e) {
            throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
        }
    }

    protected abstract int getResult(int result, int number);

}
package me.whiteship.chapter04.item20.templatemethod;

public class Plus extends FileProcessor {
    public Plus(String path) {
        super(path);
    }

    @Override
    protected int getResult(int result, int number) {
        return result + number;
    }

}
package me.whiteship.chapter04.item20.templatemethod;

public class Client {

    public static void main(String[] args) {
        FileProcessor fileProcessor = new Plus("number.txt");
        System.out.println(fileProcessor.process());
    }
}
1
2
3
4
5

템플릿 콜백 패턴

BiFunction : 2개의 인자를 받아서 하나의 결과값을 돌려주는 어떤 오퍼레이션을 정의할 때 쓸 수 있는 함수

package me.whiteship.chapter04.item20.templatemethod;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.function.BiFunction;

public abstract class FileProcessor {

    private String path;

    public FileProcessor(String path) {
        this.path = path;
    }

    public final int process(BiFunction<Integer, Integer, Integer> operator) {
        try(BufferedReader reader = new BufferedReader(new FileReader(path))) {
            int result = 0;
            String line = null;
            while((line = reader.readLine()) != null) {
                result = operator.apply(result, Integer.parseInt(line));
            }
            return result;
        } catch (IOException e) {
            throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e);
        }
    }

    protected abstract int getResult(int result, int number);

}
package me.whiteship.chapter04.item20.templatemethod;

public class Client {

    public static void main(String[] args) {
        FileProcessor fileProcessor = new Plus("number.txt");
        System.out.println(fileProcessor.process((a,b) -> a + b));
    }
}
package me.whiteship.chapter04.item20.templatemethod;

public class Client {

    public static void main(String[] args) {
        FileProcessor fileProcessor = new Plus("number.txt");
        System.out.println(fileProcessor.process(Integer::sum));
    }
}

아이템 20. 완벽 공략 - 디폴트 메서드와 Object 메서드

package me.whiteship.chapter04.item20.objectmethod;

public interface MyInterface {

    default String toString() {
        return "myString";
    }

    default int hashCode() {
        return 10;
    }

    default boolean equals(Object o) {
        return true;
    }

}

컴파일 에러가 나온다.

default는 메소드의 진화와 관련이 있다. 메소드에 새로운 기능을 추가할 때 기존의 인터페이스를 구현한 모든 클래스들을 그대로 유지하면서도 아주 간단한 추가 기능을 넣어줄 수 있는 그런 기능을 만들고 싶었다.

이런 설계에 굉장히 큰 변화, 그리고 위험을 가져다 주는 이런 변화를 끼워넣고자 함이 아니기 때문이다.

package me.whiteship.chapter04.item20.objectmethod;

public class MyClass extends Object implements MyInterface {
}

이렇게 되는 경우 Object의 equals를 사용하게 되는지 MyInterface에서 사용하는 equals를 사용하게 되는지를 모르게 된다.

equals, toString 등 interface에서는 어차피 변수가 들어갈 수 없으므로 해당 메소드를 지원할 수 없다.

0개의 댓글