Object[] objectArray = new Long[1];
objectArray[0] = "타입이 달라 넣을 수 없다" // ArrayStoreException
실체화(reify)된다 : 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.
제네릭
불공변 (invariant) - 서로 다른 타입Type1,Type2가 있을 때List<Type1>은List<Type2>의 하위타입도 아니고 상위타입도 아니다
리스트에서는 컴파일할 때 타입 오류를 바로 알 수 있다
List<Object> ol = new ArrayList<Long>(); // 호환되지 않는 타입이다.
ol.add("타입이 달라 넣을 수 없다.")
소거(erasure)된다 : 원소 타입을 컴파일 타임에만 검사하며 런타임에는 알 수 없다. 제네릭 지원 전과 제네릭 타입을 함게 사용할 수 있게 해주는 메커니즘이다.
2. 제네릭 배열은 사용불가하다.
배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. new List<E>[],new List<String>[],new E[]: 컴파일시 제네릭 생성오류를 일으킨다.
이유 :타입안전하지 않기 때문이다 컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다 : 제네릭 타입의 취지와 맞지 않다.
3. 실체화 불가 타입
E,List<E>,List<String>: 실체화 불가 타입
실체화 되지 않아 런타임에는 컴파일 타임보다 타입점보를 적게 가지는 타입
List<?>,Map<?,?>: 소거 메커니즘 때문에 매개변수화 타입 가운데 실체화 가능한 타입 - 비한정적 와일드카드 타입
4. 배열의 불편함
a. 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는게 불가능하다.
아이템 33의 타입 안전 이종 컨테이너를 이용하여 자신의 원소타입을 추론할 수있다 (우회)
b. 제네릭 타입과 가변인수 메서드를 함께 쓰면 해석하기 어려운 경고 메세지를 받게된다.
가변인수 메서드를 호출할 때마다. 가변인수를 담는 배열이 만들어지는데, 이때 그 배열의 원소가 실체화 불가 타입이면 경고가 뜬다. @SafeVarargs로 해결한다.
5. 배열대신 리스트를 사용하자
장점 : 타입 안정성과 상호 운용성이 좋아진다.
단점 : 코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있다.
public class Chooser<T>{
private final List<T> choiceList;
public Chooser(Collection<T> choices){
choiceList = new ArrayList<>(choices);
}
public T choose(){
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.netInt(choiceList.size()));
}
}
List를 사용하면 런타임에 ClassCastException을 만날 일은 없다.
public class Chooser<T>{
private final T[] choiceArray;
public Chooser(Collection<T> choices){
choicesArra = (T[]) choices.toArray;
}
public Object choose(){
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
T[] 로의 타입캐스팅 과정에서 경고가 뜬다. 제네릭에서는 원소의 타입정보가 소거되어 런타임에는 타임 정보를 알 수 없기 때문이다. item 27 비검사 경고를 제거하라는 말에 따라 위험요소를 제거할 수 있는 최선의 방법은 List로 구현하는 것이었다.
즉, 클래스가 어떤 인터페이스를 구현한다는 것은자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에게 이야기하는 것이며 인터페이스는 오직 이 용도로만 사용해야 한다.
public interface PhysicalConstants {
// 아보가드로 수 (1/몰)
static final double AVOGADROS_NUMBER = 6.022_140_857e23;
// 볼츠만 상수 (J/K)
static final double BOLTZMANN_CONSTANT = 1.380_648)52e-23;
// 전자 질량 (kg)
static final double ELECTRON_MASS = 9.109_383_56e-31;
}
클래스가 어떤 상수 인터페이스를 사용하든 사용자에게는 아무런 의미가 없다. 오히려 사용자에게 혼란을 주기도 하며, 더 심하게는 클라이언트 코드가 내부 구현에 해당하는 이 상수 들에 종속되게 한다.
final이 아닌 클래스가 상수 인터페이스를 구현한다면 모든 하위 클래스의 이름 공간이 그 인터페이스가 정의한 상수들로 오염되어 버린다.
public interface TestInterFace {
static String NAME = "incheol";
}
public class InterfaceImpl implements TestInterFace {
public static void main(String[] args) {
System.out.println(NAME);
}
}
// result : incheol
public class InterfaceImpl implements TestInterFace {
public static final String NAME = "test";
public static void main(String[] args) {
System.out.println(NAME);
}
}
// result : test
java.io.ObjectStreamContants 등, 자바 플랫폼 라이브러리에도 상수 인터페이스가 몇 개 있으나, 인터페이스를 잘못 활용한 예이니 따라 해서는 안된다.
public interface ObjectStreamConstants {
/**
* Magic number that is written to the stream header.
*/
final static short STREAM_MAGIC = (short)0xaced;
/**
* Version number that is written to the stream header.
*/
final static short STREAM_VERSION = 5;
/* Each item in the stream is preceded by a tag
*/
/**
* First tag value.
*/
final static byte TC_BASE = 0x70;
/**
* Null object reference.
*/
final static byte TC_NULL = (byte)0x70;
/**
* Reference to an object already written into the stream.
*/
final static byte TC_REFERENCE = (byte)0x71;
/**
* new Class Descriptor.
*/
final static byte TC_CLASSDESC = (byte)0x72;
/**
* new Object.
*/
final static byte TC_OBJECT = (byte)0x73;
/**
* new String.
*/
final static byte TC_STRING = (byte)0x74;
/**
* new Array.
*/
final static byte TC_ARRAY = (byte)0x75;
...
}
상수를 공개할 목적이라면 더 합당한 선택지가 몇 가지 있다. 특정 클래스나 인터페이스와 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가해야 한다. 모든 숫자 기본 타입의 박싱 클래스가 대표적으로, Integer와 Double에 선언된 MIN_VALUE와 MAX_VALUE 상수가 이런 예다
public final class Integer extends Number implements Comparable<Integer> {
/**
* A constant holding the minimum value an {@code int} can
* have, -2<sup>31</sup>.
*/
@Native public static final int MIN_VALUE = 0x80000000;
/**
* A constant holding the maximum value an {@code int} can
* have, 2<sup>31</sup>-1.
*/
@Native public static final int MAX_VALUE = 0x7fffffff;
...
}
적합한 상수라면 열거 타입으로 만들어 공개하면 된다. 그것도 아니라면, 인스턴스화 할 수 없는 유틸리티 클래스에 담아 공개하자.
pakcage ...constantutilityclass;
public class PhysicalConstants {
private PhysicalConstants() {} // 인스턴스화 방지
// 아보가드로 수 (1/몰)
static final double AVOGADROS_NUMBER = 6.022_140_857e23;
// 볼츠만 상수 (J/K)
static final double BOLTZMANN_CONSTANT = 1.380_648)52e-23;
// 전자 질량 (kg)
static final double ELECTRON_MASS = 9.109_383_56e-31;
}
유틸리티 클래스의 상수를 빈번히 사용한다면 정적 임포트(static import)하여 클래스 이름을 생략할 수 있다.
import static ...PhysicalConstants.*;
public class Test {
double atoms(double mols) {
return AVOGADROS_NUMBER * mols;
}
...
}
자바에는 인터페이스와 추상 클래스를 제공한다. 또한 자바 8 부터는 인터페이스에 default method를 제공하게 되어 인퍼테이스와 추상 클래스 모두 인스턴스 메소드를 구현 형태로 제공할 수 있게되었다. 그렇다면 둘의차이는 무엇일까? 추상 클래스를 상속받아 구현하는 클래스는 반드시 추상 클래스의 하위 타입이 되어야한다는 점이다. 즉, 자바에서는 단일 상속만 지원 하기 때문에 한 추상 클래스를 상속받은 클래스는 다른 클래스를 상속받을 수 없게되는 것이다. 이와 반대로 인터페이스는 구현해야할 메소드만 올바르게 구현한다면 어떤 다른 클래스를 상속했던 간에 같은 타입으로 취급된다.
인터페이스에는 다음과 같은 장점이 있다.
기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.
인터페이스는 믹스인 정의에 안성맞춤이다.
인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
래퍼 클래스 관용구와 함께 사용한다면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.
1. 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.
기존 클래스에 새로운 인터페이스를 구현하려면, 간단하게 해당 인터페이스를 클래스 선언문에 implements 구문으로 추가하고 인터페이스가 제공하는 메소드들을 구현하기만 하면 끝이다. 반면에 기존 클래스에 새로운 추상 클래스를 상속하기에는 어려움이 따른다. 두 클래스가 같은 추상 클래스를 상속하길 원한다면 계층적으로 두 클래스는 공통인 조상을 가지게 된다. 만약 두 클래스가 어떠한 연관도 없는 클래스라면 클래스 계층에 혼란을 줄 수 있다.
2. 인터페이스는 믹스인 정의에 안성맞춤이다.
믹스인이란 어떤 클래스의 주 기능에 추가적인 기능을 혼합한다 하여서 믹스인이라고 한다. 그러므로 믹스인 인터페이스는 어떤 클래스의 주 기능이외에 믹스인 인터페이스의 기능을 추가적으로 제공하게 해주는 효과를 준다. 추상 클래스가 믹스인 정의에 맞지않은 이유는 기존 클래스에 덧씌울 수 없기 때문이다. 자바는 단일 상속을 지원하기 때문에 한 클래스가 두 부모를 가질 수 없고 부모와 자식이라는 클래스 계층에서 믹스인이 들어갈 합리적인 위치가 없다.
믹스인 인터페이스엔 대표적으로 Comparable, Cloneable, Serializable 이 존재한다.
public class Mixin implements Comparable {
@Override
public int compareTo(Object o) {
return 0;
}
}
위의 코드와 같이 Comparable을 구현한 클래스는 같은 클래스 인스턴스 끼리는 순서를 정할 수 있는 것을 알 수 있다.
3. 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
현실의 개념 중에는 동물과 포유류, 파충류, 조류 와 같이 타입을 계층적으로 정의하여 구조적으로 잘 표현할 수 있는 개념이 있는가 하면 가수와 작곡가, 그리고 가수겸 작곡가(SingerSongWriter) 같이 계층적으로 표현하기 어려운 개념이 존재한다. 이런 계층구조가 없는 개념들은 인터페이스로 만들기 편하다.
가수, 작곡가 인터페이스가 존재한다. 또한 가수겸 작곡가도 존재한다. 가수와 작곡가를 인터페이스로 정의하였으니 사람이라는 클래스가 두 인터페이스를 구현해도 전혀 문제가 되지 않는다.
public class People implements Singer, SongWriter {
@Override
public void Sing(String s) {
}
@Override
public void Compose(int chartPosition) {
}
}
public abstract class Singer {
abstract void sing(String s);
}
public abstract class SongWriter {
abstract void compose(int chartPosition);
}
public abstract class SingerSongWriter {
abstract void strum();
abstract void actSensitive();
abstract void Compose(int chartPosition);
abstract void sing(String s);
}
추상 클래스로 만들었기 때문에 Singer 클래스와 SongWriter 클래스를 둘다 상속할 수 없어 SIngerSongWriter라는 또 다른 추상 클래스를 만들어서 클래스 계층을 표현할 수 밖에 없다. 만약 이런 Singer 와 SongWriter와 같은 속성들이 많이 있다면, 그러한 클래스 계층구조를 만들기 위해 많은 조합이 필요하고 결국엔 고도비만 계층구조가 만들어질 것이다. 이러한 현상을 조합 폭발이라고 한다.
4. 래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상키는 안전하고 강력한 수단이 된다.
타입을 추상 클래스로 정의해두면 해당 타입에 기능을 추가하는 방법은 상속 뿐이다. 상속해서 만든 클래스는 래퍼 클래스보다 활용도가 떨어지고 쉽게 깨진다.(ITEM18 래퍼클래스참고)
Java8 부터는 인터페이스에서 디폴트 메소드 기능을 제공해 개발자들이 중복되는 메소드를 구현하는 수고를 덜어줄 수 있게 되었다. 하지만 디폴트 메소드에도 단점은 존재한다. Object의 equals, hashcode 같은 메소드는 디폴트 메소드로 제공해서는 안 된다. 또한 public이 아닌 정적 멤버도 가질 수 없다. 또한 본인이 만들지 않은 인터페이스에는 디폴트 메소드를 추가할 수 없다.
한편, 인터페이스와 추상 골격 구현 클래스를 함께 제공하면 인터페이스와 추상 클래스의 장점을 모두 가져갈 수 있다.. 인터페이스로는 타입을 정의하고 필요한 일부 디폴트 메소드를 구현한다, 추상 골격 구현 클래스는 나머지 메소드들 까지 구현한다. 이렇게 하면 추상 골격 구현 클래스를 확장하는 것만으로 인터페이스를 구현하는데 대부분 일이 완료된다. 이는 템플릿 메소드 패턴과 같다. 이런 추상 골격 구현 클래스를 보여주는 좋은 예로는 컬렉션 프레임워크의 AbstractList, AbstractSet 클래스이다. 이 두 추상 클래스는 각각 List, Set 인터페이스의 추상 골격 구현 클래스이다.
public class BaverageVending implements Vending {
@Override
public void start() {
System.out.println("vending start");
}
@Override
public void chooseProduct() {
System.out.println("choose menu");
System.out.println("coke");
System.out.println("energy drink");
}
@Override
public void stop() {
System.out.println("stop vending");
}
@Override
public void process() {
start();
chooseProduct();
stop();
}
}
public class CoffeeVending implements Vending {
@Override
public void start() {
System.out.println("vending start");
}
@Override
public void chooseProduct() {
System.out.println("choose menu");
System.out.println("americano");
System.out.println("cafe latte");
}
@Override
public void stop() {
System.out.println("stop vending");
}
@Override
public void process() {
start();
chooseProduct();
stop();
}
}
두 구현체 모두 Vending 인터페이스를 구현한다. 그런에 상품을 선택하는 chooseProduct 메소드를 제외하고 전부 다 같은 동작을 한다. 중복 코드를 제거하기 위해 인터페이스를 추상 클래스로 대체하지 않고 추상 골격 구현을 이용해 중복 코드를 제거 해보자.
public abstract class AbstractVending implements Vending {
@Override
public void start() {
System.out.println("vending start");
}
@Override
public void stop() {
System.out.println("stop vending");
}
@Override
public void process() {
start();
chooseProduct();
stop();
}
}
public class BaverageVending extends AbstractVending implements Vending {
@Override
public void chooseProduct() {
System.out.println("choose menu");
System.out.println("coke");
System.out.println("energy drink");
}
}
public class CoffeeVending extends AbstractVending implements Vending {
@Override
public void chooseProduct() {
System.out.println("choose menu");
System.out.println("americano");
System.out.println("cafe latte");
}
}
위와 같은 간단한 예제로 인터페이스의 디폴트 메소드를 사용하지 않고 추상 골격 구현 클래스를 만들어 중복을 제거해 보았다. 그런데, 만약 Vending을 구현하는 구현 클래스가 VendingManuFacturer 라는 제조사 클래스를 상속받아야해서 추상 골격 구현을 확장하지 못하는 상황일 땐 어떻게 해야할까?
public class VendingManufacturer {
public void printManufacturerName() {
System.out.println("Made By JavaBom");
}
}
public class SnackVending extends VendingManufacturer implements Vending {
InnerAbstractVending innerAbstractVending = new InnerAbstractVending();
@Override
public void start() {
innerAbstractVending.start();
}
@Override
public void chooseProduct() {
innerAbstractVending.chooseProduct();
}
@Override
public void stop() {
innerAbstractVending.stop();
}
@Override
public void process() {
printManufacturerName();
innerAbstractVending.process();
}
private class InnerAbstractVending extends AbstractVending {
@Override
public void chooseProduct() {
System.out.println("choose product");
System.out.println("chocolate");
System.out.println("cracker");
}
}
}
위와 같이 인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고 각 메소드 호출을 내부 클래스의 인스턴스에 전달하여 골격 구현 클래스를 우회적으로 이용하는 방식을시뮬레이트한 다중 상속(simulated multiple inheritance)이라고 한다.
마지막으로단순 구현(simple implementation)을 알아보자. 단순 구현이란 골격 구현의 작은 변종으로 골격 구현과 같이 상속을 위해 인터페이스를 구현한 것이지만, 추상 클래스가 아니라는 점에서 차이점을 가지고 있다. 단순 구현은 추상 클래스와 다르게 그대로 써도 되거나 필요에 맞게 확장해도 된다. 단순 구현의 좋은 예로는 AbstractMap.SimpleEntry 가 있다.
1. 상속용 클래스는 재정의할 수 있는 메소드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다.
클래스의 API로 공개된 메소드에서 클래스 자신의 또 다른 메소드를 호출할 수도 있다. 이때 호출되는 메소드가 하위 클래스에서 재정의 가능한 메소드라면 이 사실을 호출하는 메소드의 API 설명에 명시해야한다. 더 넓은 의미로는. 재정의 가능 메소드를 호출할 수 있는 모든 상황을 문서로 남겨두어야 한다. 이를 도와주는 용도로 @implSpec 태그가 있는데 이는, javadoc으로 api 생성시 "Implementaion Requirements:"로 대체되고 그 메소드의 내부 동작방식을 설명하는 곳이다. 활성화 하기 위해선 javadoc -tag "implSpec:a:Implementation Requirements:" 를 이용한다.
2. 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 proteced 메소드 형태로 공개해야 할수도 있다.
표시된 주석을 보면 removeRange 메소드는 이 리스트 또는 부분 리스트의 clear 메소드에서 호출한다고 나와있다. 또한 리스트 구현의 내부 구조의 이점을 잘 활용하여 removeRange메소드를 재정의하면 이 리스트 또는 부분 리스트의 clear메소드 성능을 향상 시킬 수 있다 라고 나와있다. 이 메소드를 제공한 이유는 단지 하위 클래스에서 부분리스트의 clear 메소드를 고성능으로 만들기 쉽게 하기 위해서이다. 그렇다면, 상속용 클래스에서 어떤 메소드를 protected로 노출해야 할지는 어떻게 결정할까?
3. 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 '유일'하다. 또한 상속용 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야한다.
하위 클래스를 만들며 상속용 클래스를 검증한다. 놓친 protected 멤버는 검증 도중 빈자리가 확연히 드러날 것이고 반대로 여러 하위 클래스를 만들면서 전혀 쓰이지 않는 protected 멤버는 private이었야 할 가능성이 크다.
4. 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메소드를 호출해서는 안 된다.
이는 아이템[13] 에서도 나왔던 문제와 비슷하다. 아이템 [13]에선 상위 클래스의 clone 메소드가 재정의 가능한 메소드를 호출하도록 구현하였을 때, 하위 클래스의 clone 메소드를 호출하면 상위 클래스의 clone에서 하위 클래스의 재정의 된 메소드를 호출하여 문제가 발생할 수 있었다. 다음의 코드의 실행 결과를 보자.
public class Super {
public Super() {
overrideMe();
}
public void overrideMe() {
System.out.println("super method");
}
}
public class Sub extends Super{
private String str;
public Sub() {
str = "Sub String";
}
@Override
public void overrideMe() {
System.out.println(str);
}
public static void main(String[] args) {
Sub sub = new Sub();
}
}
하위 클래스의 생서자 보다 상위 클래스의 생성자가 먼저 호출되는데, 상위 클래스의 생성자에서 하위 클래스의 재정의 된 메소드를 호출 하여 String 값이 초기화가 되기도전에 접근하여 null이 출력이 되었다. 이와 같은 현상은 Cloneable의 clone과 Serializable의 readObject에서도 발생한다. 이는 두 메소드가 생성자와 비슷한 새로운 객체를 생성하는 효과를 가지기 때문이다. 다음 코드는 상위 클래스의 readObject에서 재정의 가능한 메소드를 호출하는 코드이다.
하위 클래스를 역직렬화 할때 상위 클래스의 readObject가 호출될때 하위 클래스의 overrideMe 메소드를 호출하여 NullPointerException을 던진다. 마지막으로 Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메소드를 갖는다면 이 메소드들은 protected로 선언하여야 한다. private으로 선언할 경우 하위 클래스에서 무시되기 때문이다.
5. 상속용으로 설계하지 않은 클래스는 상속을 금지한다.
상속을 금지하는 방법에는 2가지가 존재한다.
클래스를 final로 선언
모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩토리를 만들어준다.
특별히 2번째 방법은 내부에서 다양한 하위 클래스를 만들어 쓸 수 있는 유연성을 제공해주는 장점이 있다.
마지막으로 구체클래스가 표준 인터페이스를 구현하지 않았는데 상속을 허용해야 한다면 클래스 내부에서는 재정의 가능 메서드를 사용하지 않게 만들고 이 사실을 문서로 남겨둔다. 즉, 재정의 가능한 메소드를 호출하는 자기 사용코드를 제거해야 한다. 클래스의 동작을 유지하면서 재정의 가능 메소드를 사용하는 코드를 제거해야할 때는 private '도우미 메소드'를 만들어 재정의 가능 메소드의 기능을 옮기고 도우미 메소드를 호출하도록 수정한다. 이 방법으로 위의 생성자 관련 예제가 정상적으로 처리되도록 수정해보았다.
public class Super {
public Super() {
// overrideMe();
helpMethod();
}
public void overrideMe() {
helpMethod();
}
//도우미 메소드
private void helpMethod() {
System.out.println("super method");
}
}
public class Sub extends Super{
private String str;
public Sub() {
str = "Sub String";
}
@Override
public void overrideMe() {
System.out.println(str);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
실행결과 이전과는 다르게 하위 클래스의 멤버 변수가 초기화 되기도 전에 호출되지도 않았고, 하위 클래스에서 재정의 된 메소드가 정상적으로 동작한다.
- 클래스가 인터페이스를 구현하거나 인터페이스가 다른 인터페이스를 확장하는 인터페이스 상속과는 무관하다.
메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
예제
- 추가된 원소수를 저장하는 변수와 접근자 메서드를 추가한 HashSet을 사용하는 프로그램
- 하위 클래스에서 상위 클래스의 메서드 재정의 시 상위 클래스에서의 동작에 따라 결과가 달라진다.
public class InstrumentedHashSet<E> extends HashSet<E> {
// 추가된 원소 수
private int addCount = 0;
...
@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); // 각 원소를 add메서드를 사용해서 추가
}
public int getAddCount() {
return addCount;
}
}
아래 Hashset의 addAll함수에서 add()가 안에서 호출되므로,재정의한 add()와 addall()이 3씩 증가시켜서 6이
public boolean addAll(Collection<? extends E> c){
boolean modified = false;
for(E e : c)
if(add(e))
modified = true;
return modified;
}
addAll메서드를 Overriding하지 않은 경우 문제를 고칠수 있지만..
addAll메서드가 add를 통해 구현했음을 가정한 해법이라는 한계가 있다. addAll은 HashSet이 구현하는 메서드에 전적으로 달려있고 다음 릴리스에서 다르게 적용된다면 깨지기 쉽다.
addAll메서드를Overriding한경우,(주어진 컬렉션을 순회하며 원소 하나당 add메서드 한번만호출)
HashSet의 메서드를 더 이상 호출하지 않으니 addAll이 add를 사용하는지 상관없다. 그러나상위 클래스의 메서드 동작을 다시 구현하는 것은 어렵거나 시간이 들고 오류와 성능 저하를 유발 할 수 있다.만약, 하위 클래스에서 접근할 수 없는 private필드를 써야하는 상황이라면 구현자체가 불가능하다.
새로운 릴리스에 대응하기 어려움
다음 릴리스에서 상위클래스가 새로운 메서드를 추가하는 상황을 고려해보자. 상속받은 클래스에서 특정 조건을 만족해야 데이터를 추가할 수 있도록 재정의를 해놨다. 그런데 다음 릴리스에서새로운 메서드가 만들어지고 클라이언트가 상위클래스의 메서드를 직접 호출하면 허용되지 않은 값이 추가될 수 있다.실제로 컬렉션 프레임워크가 존재하기 전에 Vector와 HashTable을 컬렉션에 포함하자 이와 관련된 보안 구멍들을 수정해야하는 사태가 발생했다.
시그니처 중복
메서드를 아예 새롭게 만들면 위에 경우보다 안전하긴 하지만 역시 위험이 따른다. 다음 릴리스에 새로운 메서드가 추가된경우를 가정하자. 추가된 메서드가 내가 만든 메서드와시그니처가 같고 반환 타입이 다르면 컴파일 에러가 발생한다.반환 타입이 같다면 다시 메서드를 재정의하는 꼴이다.새롭게 메서드를 만든 메서드는 상위 클래스의 메서드가 요구하는 규약을 만족하지 못할 가능성이 크다.
결함 허용
상속을 결정하기 전에 확장하려는 클래스의 API의 아무런 결함이 없는지 확인해야 한다. 컴포지션으로 이런 결함을 숨기는 새로운 API를 설계할 수 있지만, 상속은 상위 클래스의 API를 '그 결함까지도' 그대로 승계한다.
불필요한 내부 구현 노출
컴포지션을 써야할 상황에서 상속을 쓰는 것은 불필요하게 내부 구현을 노출하는 꼴이다. 그 결과 API가 내부 구현에 묶이고 클래스의 성능도 제한된다.
Properties p = new Properties();
p.get(key); //Object를 받음
p.getProperty(key); //String을 받음
@SuperessWarnings("unchecked")
public synchronized V get(Object key){
Entry<?,?> tab[] = table;
int hash=key.hashCode();
int index= (hash & 0x7FFFFFFF) % tab.length;
for(Entry<?,?> e = tab[index] ; e!=null ; e=e.next){
if((e.hash=hash)&&e.key.equals(key)){
return (V)e.value;
}
}
return null;
}
get, getProperty는 각각 상위 클래스와 구현 클래스에 있는 메소드이다. 그런데p.get(key), p.getProperty(key)는 결과가 다를 수 있다. 가장 심각한 문제는 클라이언트가 직접 상위클래스의 메서드를 호출하면 불변식을 깨버릴 수 있다. Properties는 키와 값으로 문자열만 허용하도록 설계하려 했으나 HashTable의 메서드를 직접호출하는 경우이다. 불변식이 한번 깨지면 load, store같은 다른 Propertie API는 더이상 사용할 수 없다. 이 문제가 밝혀졌을때 이미 수많은 사용자가 Propertiey의 키나 값으로 문자열 이외의 타입을 사용하고 있었다.
컴포지션 (해결책)
기존 클래스가 새로운 클래스의 구성 요소로 사용되는 설계(composition) 기존 클래스를 확장(상속)하는 대신, 새로운 클래스를 만들고 private필드로 기존 클래스의 인스턴스를 참조하게 한다.
새 클래스의 메서드들은 기존 클래스의 대응하는 메서드를 호출해 결과를 반환한다(forwarding). 새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며 기존 크래스에 새로운 메서드가 추가되더라도 전혀 영향을 받지 않는다.
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
//추가된 원소의 수
private int addCount = 0;
public InstrumentedHashSet(Set<E> s){
super(s);
}
@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);
}
public int getAddCount() {
return addCount;
}
}
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 addAll(Collection<? extends E> c) { return s.addAll(c); }
...
}
InstrumentSet는 HashSet의 모든 기능을 정의한 Set인터페이스를 활용해 설계되어 견고하고 아주 유연하다. 구체적으로는 Set인터페이스를 ForwardingSet으로 구현했고 생성자에서 Set의 인스턴스를 받아 적용한다
s.addAll(List.asList("a", "b", "c"));
다시 위의 상황에서 addAll을 호출하면 InstrumentedHashSet은 ForwardSet의 addAll을 호출한다. ForwordSet의 addAll은 HashSet의 addAll을 호출한다.ForwordSet이 호출한 HashSet의 addAll은 InstrumentedSet의 add가 아닌 HashSet의 add를 사용한다.
static void walk(Set<Dog> dogs) {
InstrumentedSet<Dog> iDogs = new InstrumentedSet<>(dogs);
... //이 메서드에서는 dogs대신 idogs를 사용한다.
}
다른 Set인스턴스를 감싸고 있다는 뜻에서 InstrumentedSet같은 클래스를 wrapper class라고 한다.컴포지션과 전달의 조합은 넓은 의미로 위임(delegation)이라고 부른다. 단, 엄밀히 따지면 래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우만 위임에 해당한다.
래퍼 클래스의 단점
래페 클래스의 단점은 거의 없지만콜백 프레임 워크와는 어울리지 않는다는 점을 주의하면 된다.콜백 프레임워크에서는 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출때 사용하도록한다. 내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 대신 자신(this)의 참조를 넘기고 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다. 이를 SELF문제라고 한다. 전달 메서드가 성능에 주는 영향이나 래퍼 객체가 메모리리 사용에 주는 영향을 걱정하는 사람도 있지만, 실전에서는 둘 다 별다른 영향을 주지 않았다.
상속을 사용해야 하는 경우
상속은 반드시 하위 클래스가 상위 클래스의 `진짜` 하위 타입인 상황에서만 사용해야 한다. 상위 클래스가 A, 하위 클래스가 B라면 B is a A 관계일 때만 사용해야 한다. 조건을 만족한다고 확신할 수 없다면 상속하지 말자. 이런 상황은 A를 private 인스턴스로 두고, A와는 다른 API를 제공해야 하는 상황이 대다수이다. A는 B의 필수 구성 요소가 아니라 구현하는 방법의 하나일 뿐이다.
*자바 플랫폼 라이브러리에서도 이 원칙을 위반한 대표적인 예시가 Stack, Properties이다. 스택은 벡터가 아니므로 벡터를확장해서는 안 됐고, 속성 목록도 해시테이블이 아니므로 해시테이블을 확장해서는 안 됐다. 두 사례 모두 컴포지션을 사용했으면 더 좋았을 것이다.
-결론-
상속은 강력하지만, 캡슐화를 해친다. 상속은 is-a 관계일 때만 써야 하며, 하위 클래스의 패키지가 상위 클래스와 다르고 상위 클래스가 확장을 고려해 설계되지 않았다면 여전히 문제가 될 수 있다. 상속의 취약점을 피하려면 상속 대신 컴포지션과 전달을 사용하자. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다. 래퍼 클래스는 하위 클래스보다 견고하고 강력하다.
// 불변
BigInteger moby = ...;
moby = moby.flipBit(0); // BigInteger의 크기에 비례해 시간과 공간이 소요
// 가변
BitSet moby = ...;
moby.flip(0); // 원하는 비트 하나만 상수 시간에 바꿔주는 메서득 제공
불변 클래스
- 인스턴스 내부 값을 수정할 수 없는 클래스
- String, 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal
불변 클래스 생성 규칙
1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
2. 클래스를 확장할 수 없도록 한다. 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것이지만, 다른 방법도 뒤에 살펴볼 것이다.
3. 모든 필드를 final로 선언한다. 새로 생성된 인스턴스를 동기화없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하는 데도 필요하다.(JLS 17.5)
- 단, 성능을 위해 계산 비용이 큰 값을 나중에 (처음 쓰일 때) 계산하여 final이 아닌 필드에 캐시 해놓기도 한다. 예) PhoneNumber의 hashCode메서드는 처음 불렀을때 값을 계산하여 캐시한다.
4. 모든 필드를 private으로 선언한다. 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.
5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다.
// 불변 복소수 클래스
public final class Complex {
private final double re; // 실수부
private final double im; // 허수부
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public Complex realPart() { return re; }
public Complex imaginaryPart() { return im; }
// 사칙연산
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
public Complex minus(Complex c) {
return new Complex(re - c.re, im - c.im);
}
...
불변 영역을 높이는 함수형 프로그래밍 패턴
- 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴
- 사칙연산 메서드들이 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환하는 것
- 메서드 이름으로 add같은 동사 대신 plus같은 전치사를 사용하는 점도 해당 메서드가 객체의 값을 변경하지 않는다는 사실을 강조하려는 의도
불변 클래스의 장점
- 불변 객체에 대해서는 그 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니 불변 객체는 안심하고 공유할 수 있다. 따라서 불변 클래스라면 한번 만든 인스턴스를 최대한 재활용하기를 권한다. 가장 쉬운 재활용 방법은 자주 쓰이는 값들을 상수(public static final)로 제공하는 것이다.
// Complex클래스는 다음 상수를 제공할 수 있다.
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
- 불변 클래스는 자주 사용되는 인스턴스를 캐싱하여 같은 인스턴스를 중복 생성하지 않게 해주는 정적 팩터리를 제공한다. 박싱된 기본 타입 클래스 전부와 BigInteger가 여기 속한다.
// BigInteger
public static BigInteger valueOf(long val) {
if (val == 0)
return ZERO;
if (val > 0 && val <= MAX_CONSTANT)
return posConst[(int) val];
else if (val < 0 && val >= -MAX_CONSTANT)
return negConst[(int) -val];
return new BigInteger(val);
}
- 이런 정적 팩터리 메서드를 사용하면 여러 클라이언트가 인스턴스를 공유하여 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.
- 클래스 생성 시에 public생성자 대신 정적 팩터리 메서드를 만들어두면, 클라이언트를 수정하지 않고도 필요에 따라 캐시 기능을 나중에 덧붙일 수 있다.
- 불변 객체는 아무리 복사해도 원본과 똑같으니 복사 자체가 의미가 없다. 그러니 불변 클래스는 clone메서드나 복사 생성자를 제공하지 않는 것이 좋다.
- 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
public BigInteger negate() { // 크가가 같고 부호만 반대인 새로운 인스턴스 생성
return new BigInteger(this.mag, -this.signum); // 원본 인스턴스와 공유
BigInteger(int[] magnitude, int signum) {
this.signum = (magnitude.length == 0 ? 0 : signum);
this.mag = magnitude;
if (mag.length >= MAX_MAG_LENGTH) {
checkRange();
}
}
- 불변 객체는 실패의 원자성을 제공한다. 상태가 절대 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없다.
불변 클래스의 단점
- 값이 다르면 반드시 독립된 객체로 만들어야 한다는 것이다.
// 불변
BigInteger moby = ...;
moby = moby.flipBit(0); // BigInteger의 크기에 비례해 시간과 공간이 소요
// 가변
BitSet moby = ...;
moby.flip(0); // 원하는 비트 하나만 상수 시간에 바꿔주는 메서득 제공
- 원하는 객체를 완성하기까지의 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 심각해진다.
- 해결 방법?
1. 다단계 연산들을 예측하여 기본 기능으로 제공하는 방법
2. 다단계 연산 속도를 높여주는 가변 동반 클래스 사용 : 예) String의 가변 동반 클래스는 StringBuilder
불변 클래스의 상속을 막는 방법
1. final클래스 선언
2. 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩터리를 제공
// 생성자 대신 정적 팩터리 메서드를 사용한 불변 클래스
public class Complex {
private final double re; // 실수부
private final double im; // 허수부
// 확장할 수 없으므로 final과 같다.
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
BigInteger와 BigDecimal 모두 재정의할 수 있게 설계되어, 신뢰할 수 없는 클라이언트로부터 이 인스턴스를 받는다면 객체를 확인해야 한다. 다시 말해 신뢰할 수 없는 하위 클래스의 인스턴스라고 확인되면, 이 인수들을 가변이라 가정하고 방어적으로 복사해 사용해야 한다.
public static BigInteger safeInstance(BigInteger val) {
return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray());
}
public class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public void setX(double x) {
this.x = x;
}
public double getY() {
return y;
}
public void setY(double y) {
this.y = y;
}
}
패키지 바깥에서 접근할 수 있는 클래스라면 접근자를 제공함으로써 클래스 내부 표현 방식을 언제든 바꿀 수 있는 유연성을 얻을 수 있다.
public 클래스가 필드를 공개하면 이를 사용하는 클라이언트가 생겨날 것이므로 내부 표현 방식을 마음대로 바꿀 수 없게 된다.
package-private 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 문제가 없다.그 클래스가 표현하려는 추상 개념만 올바르게 표현해주면 된다.
패키지 바깥 코드는 전혀 손대지 않고 데이터 표현 방식을 바꿀 수 있다. private 중첩 클래스의 경우라면 수정 범위가 더 좁아져서 이 클래스를 포함하는 외부 클래스까지로 제한된다.
그러니까, public 클래스의 필드를 직접 노출하지 마라.
불변 필드를 노출한 public 클래스 - 과연 좋은가?
public final class Time {
private static final int HOURS_PER_DAY = 24;
private static final int MINUTES_PER_HOUR = 60;
public final int hour;
public final int minute;
public Time(int hour, int minute) {
validateTime(hour, minute);
this.hour = hour;
this.minute = minute;
}
private void validateTime(int hour, int minute) {
// .. 유효성 검증 로직, 불변식 보장
}
// ...
}
여전히 API를 변경하지 않고는 표현 방식을 바꿀 수 없다.
여전히 필드를 읽을 때 부수 작업을 수행할 수 없다.
단, 불변식은 보장할 수 있게 된다.
핵심 정리
public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다. 불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다. 하지만 package-private 클래스나 private 중첩 클래스에서는 종종 (불변이든 가변이든) 필드를 노출하는 편이 나을 때도 있다.
잘 설계된 컴포넌트는 클래스는 모든 내부구현을 완벽히 숨겨, 구현과 API를 깔끔하게 분리 해놨다. 오직 API를 통해서만 다른 컴포넌트와 소통하며 서로의 내부 동작 방식에는 전혀 개의치 않는다. 정보은닉, 혹은 캡슐화라고 하는 이 개념은 소프트웨어 설계의 근간이 되는 원리다.
이러한 회원탈퇴 로직에 대한 인터페이스를 제공하고, 일반회원 탈되, 해외회원 탈퇴, 비회원 탈퇴등의 로직이 상이하여 Service를 별도로 구현한다고하는 경우 Interface 스펙에 맞추어 여러 개발자가 개발할 수 있기 때문이다.
시스템 관리 비용을 낮춘다.
각 컴포넌트를 빨리 파악하여 디버깅 할 수 있고, 다른 컴포넌트로 교체하는 부담도 적다. 위에서 예시로 들은 일반회원 탈되, 해외회원 탈퇴, 비회원 탈퇴에서 비회원 탈퇴 로직에 변경이 생긴 경우 WithdrawalServiceinterface를 구현한 비회원 탈퇴 서비스를 만들면 되기 때문이다.
성능 최적화에 도움을 준다.
완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정한 다음 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화 할 수 있다. 위에서 예시로 들은 일반회원 탈되, 해외회원 탈퇴, 비회원 탈퇴에서 비회원 탈퇴 중 비회원 탈퇴에 대해 처리대상이 많아 startProcess 메서드를 확장하여 내부적으로 withdrawalMember 메서드를 사용하지 않고 회원 리스트에 대해 쿼리를 이용해 Insert~ Select로 한방에 처리 할 수도 있다.
소프트웨어 재사용성을 높인다.
외부에 의존하지 않고, 독자적으로 동작할 수 있는 컴포넌트라면, 그 컴포넌트와 함께 개발되지 않은 낯선 환경에서도 유용하게 쓰일 가능성이 크다. 예를들면 알림톡 서버 API를 호출 할 수 있는 인터페이스가 있는 경우 그 인터페이스를 그대로 사용할 수 있다.
큰 시스템을 제작하는 난이도를 낮춰준다.
시스템 전체가 완성되지 않은 상태에서도 개별 컴포넌트의 동작을 검증할 수 있다. 쉬운 예로 Mockito를 이용한 TestCase 작성이다.
모든 클래스와 멤버의 접근성을 가능한 좁혀야 한다.
소프트웨어가 올바로 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다.
클래스 레벨 접근제한자
톱레벨 수준(파일명 = 클래스명)이 같은 수준에서의 접근제한자는 public과 package-private만 사용 할 수 있다.
public으로 선언하는 경우 공개 API로 사용 - 하위호환을 평생 신경써야 한다.
package-private로 사용하는 경우 해당 패키지 안에서만 사용 가능 - 다음 릴리즈에 내부로직을 변경해도 괜찮다.
이너클래스 사용하기
한 클래스에서만 사용하는 package-private 톱레벨 클래스나 인터페이스는 사용하는 클래스 안에 private static으로 중첩해보자
public일 필요가 없는 클래스의 접근 수준을 package-private로 해보자 - 다른 패키지에서 사용하지 못하게 막아야 한다.
private과 package-private은 해당 클래스의 구현에 해당하므로 공개 API에 영향을 주지 않는다.
일단 처음에는 모든 멤버는 private으로 만들어야 한다.
같은 패키지에서 접근해야 하는 멤버가 있는 경우 package-private로 변경
단, Serialize를 구현한 클래스의 경우 공개 API에 의도치 않게 공개 될 수도 있다.
필드의 접근 권한을 package-private에서 protected로 바꾸는 순간 필드에 접근 할 수 있는 대상 범위가 늘어나니 주의해야 한다.
메서드를 재정의 할 경우에는 접근 수준을 상위 클래스에서보다 좁게 설정 할 수 없다.
상위 클래스에서 접근제한자가 protected인데 하위클래스에서 갑자기 package-private이나 private로 변경할 수 없다.
상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있어야 한다는 규칙(리스코프 치환원칙)을 위반하기 때문이다.
단 인터페이스를 구현하는 경우에는 클래스의 메서드는 모두 public으로 해야 한다.
코드를 테스트 하려는 목적으로 클래스, 인터페이스를 접근 범위를 넓히는 것을 주의하라
public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.
필드가 가변객체를 참조하거나(Collections이나 배열), final이 아닌 인스턴스 필드를 public으로 선언하면불변식을 보장할 수 없다. public 가변 필드를 갖는 클래스는 일반적으로 thread safe 하지않다. 내부 구현을 바꾸고 싶어도 public 필드를 없애는 방식으로는 리팩터링이 불가하다.
하지만 상수라면 관례대로 public static final 필드로 공개해도 좋다. 필드 명 네이밍은 관례 상대문자 + _(언더바)로 구성한다. 그리고 반드시 불변 객체를 참조하도록 한다. 불변성이 깨지는 순간 어마무시한 일이 일어난다.
public 클래스의 private -> package-private으로 바꾸는거는 괜찮다. 하지만 그이상의 경우 공개 API에 문제가 될 수 있다.