sungsu9022 / study-effective-java-3e Goto Github PK
View Code? Open in Web Editor NEWstudy for effective java 3 edition
study for effective java 3 edition
적합한 인터페이스만 있다면 매개변수뿐 아니라 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라. 인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해질 것이다.
//좋은 예
Set<Son> sonSet = new LinkedHashSet<>();
//나쁜 예
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
적합한 인터페이스가 없다면 당연히 클래스로 참조해야한다.
리플렉션의 단점
public static void main(String[] args) {
// Translate the class name into a Class object
Class<? extends Set<String>> cl = null;
try {
cl = (Class<? extends Set<String>>) // Unchecked cast!
Class.forName(args[0]);
} catch (ClassNotFoundException e) {
fatalError("Class not found.");
}
// Get the constructor
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
fatalError("No parameterless constructor");
}
// Instantiate the set
Set<String> s = null;
try {
s = cons.newInstance();
} catch (IllegalAccessException e) {
fatalError("Constructor not accessible");
} catch (InstantiationException e) {
fatalError("Class not instantiable.");
} catch (InvocationTargetException e) {
fatalError("Constructor threw " + e.getCause());
} catch (ClassCastException e) {
fatalError("Class doesn't implement Set");
}
// Exercise the set
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
컴파일타임에는 알 수 없는 클래스를 사용하는 프로그램을 작성한다면 리플렉션을 사용해야 할 것이다.
단, 되도록 리플렉션은 인스턴스 생성에만 쓰고, 이렇게 만든 인스턴스는 인터페이스나 상위 클래스로 참조해 사용하자.
네이티브 메서드란 C나 C++같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다. 네이티브 메서드의 주요 쓰임은 다음과 같다.
성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 거의 권장하지 않는다.
네이티브 메서드의 심각한 단점은 다음과 같다.
빠른 프로그램보다는 좋은 프로그램을 작성하라
우리는 어떻게 최적화를 하고 있는가?
예외처리로 인해 발생할 수 있는 문제
상태의존적인 메서드를 제공하는 클래스라면 상태 검사 메서드도 함께 제공해야한다. ex) Iterator의 next와 hasNext
혹은 올바르지 않은 상태 일 때 빈 옵셔널이나 null 같은 특수한 값을 반환하는 방법도있다.
자바에는 문제상황을 알리는 타입으로 검사 예외, 런타임 예외, 에러 세가지를 제공한다.
언제 무엇을 사용해야할까?
Object
+--- Throwable
+--- Exception
| +--- RuntimeException
| +--- ...
|
+--- Error
+--- ...
Exception 역시 어떤 메서드라도 정의할 수 있는 완벽한 객체이므로 예외의 메서드는 주로 그 예외를 일으킨 상황에 관한 정보를 코드형태로 전달하는데 쓰인다.
API 호출자가 예외 상황에서 복구할 방법이 없다면 비검사 예외를 던지자.
복구가 가능하고 호출자가 그 처리를 해주길 바란다면 우선 옵셔널을 반환해도 될지 고민하자.
옵셔널만으로는 상황을 처리하기에 충분한 정보를 제공할 수 없을 때만 검사 예외를 던지자.
표준 예외를 사용하면 좋은점
Exception, RuntimeException, Error는 직접 재사용하지는 말자. 이 예외들은 상위 클래스이므로, 즉 여러 성격의 예외들을 포괄하는 클래스이므로 안정적으로 테스트할 수 없다.
상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다. 이를 예외 번역(Exception translation)이라 한다.
try {
...
} catch (LowerLevelException e) {
throw new HigherLevelException(...);
}
예외를 번역할 때, 저수준 예외가 디버깅에 도움이 된다면 예외 연쇄(exception chaining)를 사용하는게 좋다. 예외 연쇄란 문제의 근본 원인인 저수준 예외를 고수준 예외에 실어 보내는 방식이다. getCause 메서드로 저수준의 예외를 꺼내볼 수 있다.
try {
...
} catch (LowerLevelException cause) {
throw new HigherLevelException(cause);
}
무턱대고 예외를 전파하는 것보다 예외 번역이 우수한 방법이지만 가능하다면 저수준 메서드가 반드시 성공하도록하는게 좋다.
검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하자.
비검사 예외는 메서드 선언의 throws 목록에 넣지 말자. 명확하게 사용자가 해야할 일을 구분해주는 것이 좋다.
Cloneable
은 복제해도 되는 클래스를 명시하는 Mixin Interface 이지만 의도한 목적을 제대로 이루지 못했음.
clone
메서드가 선언된것이 Object
-> 재 정의를 강요할 수 없음
그마저도 protected
로 정의되어 있기 때문에 외부 객체에서 호출할 수 없음.
리플렉션을 사용하면 가능하지만 이 마저도 100%가 아니다.
그럼에도 불구하고 널리 쓰이니까 알아두면 좋음
clone
메서드가 잘 동작하게 하는 구현 방법과 언제 그렇게 해야하는지, 가능한 다른 선택지에 대해 논한다.
Cloneable
인터페이스는 놀랍게도 Object
의 protected
메서드인 clone()
의 동작방식을 결정함.
Cloneable
을 구현한 클래스에서는 clone()
을 호출하면 그 객체의 필드들을 하나한 복사한 클래스를 반환하고 구현하지 않았을 경우에는 CloneNotSupportException
을 뱉는다.
이는 인터페이스를 굉장히 이례적으로 사용한 케이스니 따라하지 말것. 인터페이스를 구현한다는 것은 일반적으로 그 클래스가 인터페이스에서 정의한 기능을 제공한다는 의미지만 Cloneable
은 상위 클래스에 정의된 메서드의 동작방식을 변경한다.
!왜 이따위로 만들었지
Object
의 명세에서 가져온 clone
의 규약은 굉장히 허술함
객체의 복사본을 생성해 반환한다. 일반적으로 다음과 같은 규약을 따른다
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)
관습적으로 반환되는 객체는 super.clone() 을 호출하여 얻어야 한다. 만약 Object 를 제외한 모든 클래스와 슈퍼클래스가 이 규약을 지킨다면 x.clone().getClass() == x.getClass() 는 참이다.
관례상 반환된 객체와 원본 객체는 독립적이여야 한다. 이를 만족하려면 super.clone()
으로 얻은 객체의 필드중 하나 이상을 반환전에 수정해야 함. 강제성이 없다는 것을 제외하면 생성자 체인과 같다.
super.clone()
이 아닌 자기 자신의 생성자를 호출하면 하위 객체에서 clone()
을 호출했을 때 원하는 타입을 받지 못한다. 사실 말도 안되는게 super.clone()
으로 Object.clone()
의 동작방식에 기대지 않을 꺼면 Cloneable
을 구현할 필요 자체가 없다.
공변 반환 타이핑 covariant return type : 재정의한 메서드의 반환타입은 상위클래스가 반환하는 타입의 하위타입일 수 있다. super.~ 로 호출한 아이는 하위타입으로 캐스팅이 가능하다.
clone
의 구현이 가변 객체를 참조하면 재앙이 벌어짐
clone
메소드는 사실상 생성자와 같은 효과를 내므로 원본 객체에 아무런 영향을 끼치지 않는 동시에 객체의 불변식을 보장해야한다.
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportException e) {
log.error(e);
// re-throwing
}
}
배열의 clone()
은 형변환이 필요없다. 런타임타입/컴파일타임타입 모두 원본의 타입을 반환하기 때문 (Object.clone()
이 아닌 Array 의 clone()
을 사용한다.)
여기서 elements
가 final
이면 이 방법은 동작하지 않기 때문에 Cloneable
의 아키텍처는 '가변 객체를 참조하는 필드는 final
로 선언하라' 는 일반적인 용법과 충돌한다.
깊은 복사를 할때, clone
을 재귀적으로 호출하는 것만으로는 부족한 경우가 있음 (HashTable 의 clone
메소드)
Entry // LinkedList
의 길이가 너무 길어지면 재귀호출 중 스택오버플로우가 발생, 이런 경우 순회로 깊은 복사를 구현할 것
또다른 방법은 super.clone()
으로 초기상태를 결정한 다음 원본 객체와 똑같은 상태로 만드는 고수준 API들을 만드는것이다. (같은 값으로 set 하는것)
생성자에서는 재정의 될 수 있는 메소드를 호출하지 말아야 하는데, clone
메서드에서도 마찬가지.
따라서 앞서 언급한 메서드는 final
이거나 private
일 것이다.
재정의한 값을 throw
을 없앨것
상속용 클래스는 Cloneable
을 구현하지 말것
!복사하는게 의미가 없어서? 하위클래스에게 위임하는게 맞아서?
근데 이런거 복잡하니까 복사 생성자 (변환 생성자) / 복사 팩터리 (변환 팩터리) 를 만드는게 더 낫다.
public Yum (Yum yum) {...}
public Yum newInstance(Yum yum) {...}
왜냐하면
final
용법과도 충돌하지 않으며checked-exception
을 뱉지도 않고더욱이 인터페이스를 인자로 받음으로써 유연한 변환도 제공할 수 있게 된다.
범용 컬렉션들이 Collection
이나 Map
을 인자로 받는 생성자를 사용하는 이유
배열 외에 clone
은 쓰지마라
equals
와 비슷하지만 동치성 비교외에 순서까지 비교할 수 있다.
Comparable
을 구현했다는 것은 자연적인 순서가 있다는 것을 의미한다.
Comparable
을 구현하면 이 인터페이스를 활용하는 수많은 제네릭 알고리즘과 컬렉션의 힘을 누릴 수 있다. 좁쌀만한 노력으로 코끼리만한 효과를 누릴수 있는것이다. 자연적인 순서를 갖는 값 클래스라면 꼭 구현하도록 하자.
다음과 같은 규약을 지켜야 한다.
역 성립 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
추이성 성립 x.compareTo(y) > 0 && y.compareTo(z) -> x.compareTo(z) > 0
x.compareTo(y) == 0 && y.compareTo(z) == 0 -> x.compareTo(z) == 0
x.compareTo(y) == 0 -> x.equals(y) = true
3번은 필수는 아니지만 만약 Comparable
을 구현하는데 이게 성립되지 않을 경우 꼭 클래스의 순서는 equals 메서드와 일치하지 않는다 를 명시해야한다.
마지막 규약은 되도록 지키는게 좋은게 정렬된 컬렉션들은 equals
메서드의 규약을 따른다고 되어 있지만 실제로는 동치성을 비교할 때 equals
대신 compareTo
를 사용하는 경우가 많다.
TreeMap.java
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Collections.java
int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
int low = 0;
int high = list.size()-1;
while (low <= high) {
int mid = (low + high) >>> 1;
Comparable<? super T> midVal = list.get(mid);
int cmp = midVal.compareTo(key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}
구현은 다음처럼 자바에서 제공하는 비교자나 직접 비교자를 구현하면 된다.
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.comapre(s, cis.s);
}
}
public int compareTo(PhoneNumber pn) {
int result = Short.compare(areaCode, pn.areaCode);
if (result == 0) {
result = Short.comapre(prefix, pn.prefix);
if (result == 0) {
result = Short.comapre(lineNum, pn.lineNum)
}
}
return result;
}
아니면 함수형 인터페이스 Comparator
를 구현하면 좀 더 선언적으로 가능
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
관계 연산자를 (> , <, ==) 를 직접 쓰는것보다 (거추장스럽고 오류를 유발한다. 맨날 헷갈림) 될 수 있으면 박싱 클래스의 compare
정적 메서드를 활용해라.
비교자를 구현할 때는 성능을 위해 핵심적인 요소부터 비교할것
값의 차 (-) 를 기준으로 Comparator
를 구현하지 말것.
정수 오버플로우와 부동소수점 계산 바식에 따른 오류를 낼 수 있다.
다음 두가지 방법을 써라
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable
인터페이스를 구현하여, 그 인스턴스를 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야한다.
compareTo
메서드에서 필드의 값을 비교할 때 <와 >연산자는 쓰지 말아야한다. 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare
메서드나 Comparator
인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
내부 데이터와 내부 구현은 외부로부터 완벽히 숨겨 구현과 API 를 분리하고, 외부에서는 이 API 를 통해서만 소통해야한다.
public 클래스의 인스턴스 필드는 되도록 public 이 아니여야 한다.
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
};
Java9 모듈?
프로그램 요소의 접근성은 가능한 최소한으로 하라. 꼭 필요한 것만 골라 최소한의 public API 를 설계하자.
그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야한다.
public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다. public static final 필드가 참조하는 객체가 불변인지 확인하라.
접근자를 제공하고 필드는 private 로 선언할것
public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다.
불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다.
하지만 package-private 클래스나 private 중첩 클래스에서는 종종 필드를 노출하는 편이 나을 때도 있다.
불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.
불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요 없다.
그러므로 안심하고 공유할 수 있고, 최대한 재활용 하기를 권한다.
방어적 복사도 필요없다. 아무리 복사해봐야 원본과 똑같으니 복사 자체가 의미가 없다.
불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
불변 객체는 그 자체로 실패 원자성을 제공한다.
메서드에서 예외가 발생한 후에도 그 객체는 여전히 유효한 상태여야 한다..
단점 -> 값이 다르면 반드시 독립된 객체로 만들어야 한다.
BigInteger.java
// Compute the modular inverse
int inv = -MutableBigInteger.inverseMod32(mod[modLen-1]);
// Convert base to Montgomery form
int[] a = leftShift(base, base.length, modLen << 5);
MutableBigInteger q = new MutableBigInteger(),
a2 = new MutableBigInteger(a),
b2 = new MutableBigInteger(mod);
MutableBigInteger r= a2.divide(b2, q);
table[0] = r.toIntArray();
// Pad table[0] with leading zeros so its length is at least modLen
if (table[0].length < modLen) {
int offset = modLen - table[0].length;
int[] t2 = new int[modLen];
for (int i=0; i < table[0].length; i++)
t2[i+offset] = table[0][i];
table[0] = t2;
}
// Set b to the square of the base
int[] b = squareToLen(table[0], modLen, null);
b = montReduce(b, mod, modLen, inv);
// 생략
return new BigInteger(1, t2);
생성
자신을 상속하지 못하게 하는 가장 쉬운 방법은 final 클래스로 선언하는 것이지만, 모든 생성자를 private 나 package-private 로 두고 public 정적 팩터리 메서드를 제공하는 방법이 더 유연하다.
BigInteger 와 BigDecimal 을 설계할땐 불변 객체가 사실상 final 이어야 하는 인식이 널리 퍼지지 않았다.
그래서 이 두 클래스 모두 재정의할 수 있게 설계되었고, 안타깝게도 하위호환성이 발목을 잡아 지금까지도 이 문제를 고치지 못했다. 만약 신뢰할 수 없는 클라이언트로 부터 이 두 클래스의 인스턴스를 인수로 받는다면 주의해야한다.
신뢰할 수 없는 하위클래스라고 생각된다면 가변이라 가정하고 방어적으로 복사해서 사용해야한다.
게터가 있다고 무조건 세터를 만들지 마라. 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
String과 BigInteger 처럼 무거운 값 객체도 불변으로 만들 수 있는지 고심해야 한다. 어쩔 수 없다면 불변 클래스와 쌍을 이루는 가변 동반 클래스를 제공해라
불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야한다.
private final FieldType field1 = computeFieldValue();
초기화 순환성(initialization circularity)을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용해야 한다.
private FieldType field2;
private synchronized FieldType getField2() {
if (field2 == null)
field2 = computeFieldValue();
return field2;
}
성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스(lazy initialization holder class) 관용구를 사용한다.
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() { return FieldHolder.field; }
성능 때문에 인스턴스 필드를 지연 초기화해야 하는 경우 - 이중검사(double-check) 관용구를 사용한다.
private volatile FieldType field4;
private FieldType getField4() {
FieldType result = field4;
if (result != null) // 첫 번째 검사 (락 사용 안 함)
return result;
synchronized(this) {
if (field4 == null) // 두 번째 검사 (락 사용)
field4 = computeFieldValue();
return field4;
}
}
반복해서 초기화해도 상관없는 인스턴스 필드를 지연 초기화해야 할때는 단일검사(single-check) 관용구를 사용한다.
// 코드 83-5 단일검사 관용구 - 초기화가 중복해서 일어날 수 있다! (445쪽)
private volatile FieldType field5;
private FieldType getField5() {
FieldType result = field5;
if (result == null)
field5 = result = computeFieldValue();
return result;
여러 스레드가 실행 중이면 운영체제의 스레드 스케줄러가 어떤 스레드를 얼마나 오래 실행할지를 결정함. 구체적인 스케줄링 정책이 운영체제마다 다를수 있음.
그래서 잘 작성된 프로그램은 이 정책에 좌지우지돼서는 안 된다.(정확성이나 성능이 스레드 스케줄러에 따라 달리진다면 다른 플랫폼에 이식하기 어려움.)
public class SlowCountDownLatch {
private int count;
public SlowCountDownLatch(int count) {
if (count < 0)
throw new IllegalArgumentException(count + " < 0");
this.count = count;
}
public void await() {
while (true) {
synchronized(this) {
if (count == 0)
return;
}
}
}
public synchronized void countDown() {
if (count != 0)
count--;
}
}
자바가 객체를 바이트 스트림으로 인코딩하고(직렬화) 그 바이트 스트림으로부터 다시 객체를 재구성하는(역질렬화) 매커니즘
public class DeserializationBomb {
public static void main(String[] args) throws Exception {
System.out.println(bomb().length);
deserialize(bomb());
}
static byte[] bomb() {
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for (int i = 0; i < 100; i++) {
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo"); // t1을 t2와 다르게 만든다.
s1.add(t1);
s1.add(t2);
s2.add(t1);
s2.add(t2);
s1 = t1;
s2 = t2;
}
return serialize(root); // 이 메서드는 effectivejava.chapter12.Util 클래스에 정의되어 있다.
}
}
기본 전제는 레거시 시스템에서 완전비 배제할수 없다면 차선책은 "신뢰할 수 없는 데이터는 절대 역직렬화 하지 않는 것"
private void readObjectNoData(0 throws InvalidObjectException {
throw new InvalidObjectException("스트림 데이터가 필요합니다");
}
-직렬화 형태도 공개 API에 속하기 떄문에 모두 문서화 해야 한다.
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry {
String data;
Entry next;
Entry previous;
}
// 지정한 문자열을 이 리스트에 추가한다.
public final void add(String s) { }
/**
* 이 {@code StringList} 인스턴스를 직렬화한다.
*
* @serialData 이 리스트의 크기(포함된 문자열의 개수)를 기록한 후
* ({@code int}), 이어서 모든 원소를(각각은 {@code String})
* 순서대로 기록한다.
*/
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
s.writeInt(size);
// 모든 원소를 올바른 순서로 기록한다.
for (Entry e = head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
int numElements = s.readInt();
// 모든 원소를 읽어 이 리스트에 삽입한다.
for (int i = 0; i < numElements; i++)
add((String) s.readObject());
}
...
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
transient(일시적) 한정자를 사용하여 처리하면 직렬화 되지 않음.
클래스의 인스턴스 필드 모두가 transient이면 defaultWriteObject와 defaultReadObject를 무조건 호출해야 한다.(직렬화 명세)
private synchronized void wirteObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
}
-wirteObject 메소드 안에서 동기화하고 싶다면 클래스의 다른 부분에서 사용하는 락 순서를 똑같이 적용해야 자원 순서 교착 상태(resource-ordering deadlock)을 피할수 있음.
// 인스턴스 통제를 위한 readResovle - 개선의 여지가 있다!
private Object readResolve() {
// 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
return INSTANCE;
}
// 방어적 복사를 사용하는 불변 클래스
public final class Period implements Serializable {
private final Date start;
private final Date end;
/**
* @param start 시작 시각
* @param end 종료 시각; 시작 시각보다 뒤여야 한다.
* @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
* @throws NullPointerException start나 end가 null이면 발행한다.
*/
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(
start + "가 " + end + "보다 늦다.");
}
public Date start () { return new Date(start.getTime()); }
public Date end () { return new Date(end.getTime()); }
public String toString() { return start + " - " + end; }
// 코드 90-1 Period 클래스용 직렬화 프록시 (479쪽)
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
// Period.SerializationProxy용 readResolve 메서드
private Object readResolve() {
return new Period(start, end);
}
private static final long serialVersionUID =
234098243823485285L; // 아무 값이나 상관없다. (아이템 87)
}
// 직렬화 프록시 패턴용 writeReplace 메서드 (480쪽)
private Object writeReplace() {
return new SerializationProxy(this);
}
// 직렬화 프록시 패턴용 readObject 메서드 (480쪽)
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("프록시가 필요합니다.");
}
}
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : BOolean.FALSE;
}
1 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
2 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
명명 규칙 | 설명 | 예시 |
---|---|---|
from | 매개변수를 하나를 받아서 생성 | Date d = Date.from(instant) |
of | 여러 매개변수를 받아서 생성 | Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING); |
valueOf | from과 of의 더 자세한 버전 | BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE); |
instace 혹은 getInstance | 매개변수로 명시한 인스턴스를 반환, 같은 인스턴스 보장(x) | StackWalker luke = StackWalker.getInstnace(options); |
create 혹은 newInstance | 매번 새로운 인스턴스를 생성해 반환 | Object newArray = Array.newInstace(classObject, arrayLen); |
get"Tpye" | 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 | FileStore fs = Files.getFileStore(path); |
new"Type" | 매번 새로운 인스턴스를 생성해 반환하지만 다른 클래스에 팩터리 메서드를 정의할 때 | BufferedReader br = Files.newBufferedReader(path); |
"type" | getType과 newType의 간결한 버전 | List<Complaint> litany = Collections.list(legacyLitany); |
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27); // 각각의 값이 어떤걸 뜻하는지 한눈에 파악할 수 없음.
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
@Entity
@Getter
@Builder
public class Category {
@Id
@GeneratedValue
private int id;
@Column
private String name;
@Column
private String icon;
private Level level;
// @OneToMany(mappedBy = "id", cascade = CascadeType.ALL)
private List<Category> subCagetoryList;
}
@Test
public void builderTest() {
Category.builder()
.id(1)
.level(Level.FIRST)
.name("팬션의류/잡화")
.icon("패션의류 아이콘.png")
.subCagetoryList(new ArrayList<>())
.build();
}
@NoArgsConstructor
@Getter
@Builder
public class BaseFashionItem implements FashionItem {
private String brand;
private Category category;
private String name;
private long price;
}
public class Shirt extends BaseFashionItem {
private ColorType colorType;
private SizeType sizeType;
@Builder
public Shirt(String brand, Category category, String name, long price, ColorType colorType, SizeType sizeType) {
super(brand, category, name, price);
this.colorType = colorType;
this.sizeType = sizeType;
}
}
@Test
public void builderTest() {
Shirt shirt = Shirt.builder()
.category(Category.builder().build())
.brand("brand")
.name("name")
.price(15000)
.colorType(ColorType.BLACK)
.sizeType(SizeType.M)
.build();
}
빌더 패턴의 단점
API는 시간이 지날수록 매개변ㄴ수가 많아지기 때문에 애초에 빌더를 고려하는 편이 나을 때가 많다.
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
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!");
}
// 이 메서드는 보통 클래스 바깥(다른 클래스)에 작성해야 한다!
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance();
elvis.leaveTheBuilding();
}
}
private Object readResolve() {
return INSTNACE;
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ContentLanguageConfig {
public static final List<Language> SERVICE_LIST;
public static final List<Language> EMAIL_PUSH_SUPPORT_LIST;
public static final List<Language> EMAIL_JOIN_SUPPORT_LIST;
static {
SERVICE_LIST = Arrays.asList(Language.ENGLISH, Language.SIMPLIFIED_CHINESE, Language.TRADITIONAL_CHINESE, Language.THAI, Language.INDONESIAN, Language.JAPANESE);
EMAIL_PUSH_SUPPORT_LIST = Arrays.asList(Language.ENGLISH, Language.SIMPLIFIED_CHINESE, Language.TRADITIONAL_CHINESE, Language.JAPANESE);
EMAIL_JOIN_SUPPORT_LIST = Arrays.asList(Language.ENGLISH, Language.SIMPLIFIED_CHINESE, Language.TRADITIONAL_CHINESE, Language.JAPANESE);
}
/**
* 서비스중인 언어인지 확인
* @param language
* @return
*/
public static boolean isService(Language language) {
return SERVICE_LIST.contains(language);
}
}
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isVlaid(String word) { ... }
public List<String> suggestions(String type) { ... }
}
Mosaic create(Supplier<? extends Tile> titleFactory) { ... }
자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.(의존 객체 주입을 통해 하자)
똑같은 기능의 객체를 매번 생성하기 보다는 객체 하나를 재사용하는 편이 나을 때가 많다.(당연한 이야기)
String s = new String("bikini"); // 따라하지 말것.
String s = "bikini";
생성자로 생성하는 케이스는 매번 새로운 String 인스턴스를 생성한다.
2번쨰 방식을 사용하면 하나의 String 인스턴스를 사용하고, 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.
Boolean(String) 생성자 <<<< Boolean.valueOf(String)
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
sum 변수의 long이 아닌 Long으로 선언해서 불필요한 Long 인스턴스가 생성됨.
박싱된 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* 원소를 위한 공간을 적어도 하나 이상 확보한다.
* 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다쓴 참조 해제
return result;
}
public class WeakHashMapTest {
public static void main(String[] args) {
WeakHashMap<Integer, String> map = new WeakHashMap<>();
Integer key1 = 1000;
Integer key2 = 2000;
map.put(key1, "test a");
map.put(key2, "test b");
key1 = null;
System.gc(); //강제 Garbage Collection
map.entrySet().stream().forEach(el -> System.out.println(el));
}
}
LinkedHashMap
리스너(Listener) 혹은 콜백(Callback)
출석 | 박성수 | 김래선 | 윤지수 | 조민혜 | 지성인 | 양성만 |
---|---|---|---|---|---|---|
2/14 | V | V | V | 지각 | V | V |
2/21 | V | 지각 | V | 지각 | 지각 | 개인 사정으로 인한 불참 |
3/7 | V | V | 지각 | V | V | V |
3/21 | V | V | V | 사정으로 불참 | V | V |
3/28 | V | V | 불참 | V | V | V |
4/18 | 사정으로 불참 | 불참 | 불참 | 불참 | V | V |
4/24 | 사정으로 불참 | 불참 | V | V | 불참 | V |
5/9 | V | V | V | V | V | V |
이펙티브 자바 3/E 효과적으로 읽기
자바 6까지 다루던 이펙티브 자바 2판이 자바 7, 8, 9를 다루기 위해 3판으로 다시 돌아왔습니다.
그동안 객체 지향에 치중하던 자바에 새로 도입된 함수형 프로그래밍 요소도 자세히 알아봅니다.
이펙티브 자바 Effective Java 3/E(http://www.yes24.com/24/Goods/65551284)를 읽으며 스터디를 진행합니다.
자바 8, 9에서 새로 다룬 내용이 과연 무엇인지 비교 정리합니다.
기존 2판에서 다루던 내용보다 3판에서 새로 다루는 내용에 많은 시간을 편성합니다.
2명 씩 조를 나누고 돌아가면서 각 주차의 주제에 대해 공부 후 발표하는 식으로 진행합니다.
Java Enum 활용기(http://woowabros.github.io/tools/2017/07/10/java-enum-uses.html)와 같은 사례들을 찾아보고 정리하면 더 좋습니다.
아래의 유튜브를 참고하며 진행합니다.
1주차 : 1장 들어가기, 2장 객체 생성과 파괴 읽기
2주차 : 3장 모든 객체의 공통 메서드 읽기, 4장 클래스와 인터페이스 읽기
3주차 : 5장 제네릭 읽기, 6장 열거 타입과 애너테이션 읽기
4주차 : 7장 람다와 스트림 읽기
5주차 : 중간 세미나
6주차 : 8장 메서드, 9장 일반적인 프로그래밍 원칙 읽기
7주차 : 10장 예외, 11장 동시성 읽기
8주차 : 12장 직렬화 읽기
9주차 : 회고
상속은 코드를 재사용하는 강력한 수단이지만 항상 최선은 아니다.
확장할 목적으로 설계되었고 문서화도 잘 된 클래스(item19)이거나 같은 프로그래머가 통제한다면 괜찮지만
상위클래스에 따라 하위 클래스의 동작에 이상이 생길 수 있음
상위클래스는 릴리즈 마다 내부 구현이 달라질 수 있음 -> 하위클래스 영향 받을 수 있음
상위클래스를 확장을 고려하지 않고 설계 하거나, 문서화도 하지 않으면 하위 클래스도 매번 수정 돼야 함
HashSet을 확장한 MyHashSet
public class MyHashSet<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 = addCount + c.size(0;
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
MyHashSet<String> mySet = new MyHashSet<>();
mySet.addAll(List.of("탁","탁탁","펑"));
// 출력되는 값은? 6
System.out.println(mySet.getAddCount());
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
이렇게 되면 새로운 클래스는 기존 클래스의 영향이 적어지고 기존 클래스 안에 새로운 메서드가 추가되어도 안전함
public class MySet<E> extends ForwardingSet<E> {
private int addCount = 0;
public MySet(Set<E> set) {
super(set);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> collection) {
addCount = addCount + collection.size();
return super.addAll(collection);
}
public int getAddCount() {
return addCount;
}
}
public class ForwardingSet<E> implements Set<E> {
private final Set<E> set;
public ForwardingSet(Set<E> set) { this.set = set; }
public void clear() { set.clear(); }
public boolean isEmpty() { return set.isEmpbty(); }
public boolean add(E e) { return set.add(e); }
public boolean addAll(Collection<? extends E> c) { return set.addAll(c); }
// ... 생략
}
상속가능한 클래스는 재정의 할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
'어떻게' 가 아닌 '무엇을' 하는지를 설명해야 하지만, 상속이 캡슐화를 해치기 때문에 클래스를 안전하게 상속하기 위해서 내부 구현 방식을 설명할 수 밖에 없음..
-tag "implSpec:a:Implementation Requirements:"
상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증하라.
public class Super {
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
public final class Sub extends Super {
private final Instant instant;
Sub() {
instant = Instant.now();
}
//상위 클래스의 생성자 호출 됨
@Override public void overrideMe() {
System.out.println(instnat);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
클래스 내부에서 어떻게 하는지 모두 문서로 남기고 문서화 한 것은 반드시 지켜야 한다.
추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 함
인터페이스가 선언한 메서드를 모두 정의하고 그 일반 규약을 지켰다면 어떤 클래스를 상속하든 같은 타입
인터페이스 추가 구현은 다소 쉽지만, 기존 클래스 위에 새로운 추상 클래스를 끼워 넣는일은 일반적으로 어려 움
static List<Integer> intArrayAsList(int[] a) {
Object.requireNonNull(a);
return new AbstractList<>() {
@Override public Integer get(int i) {
return a[i];
}
@Override public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; //오토언박싱
return oldVal;
}
@Override public int size() {
return a.length;
}
};
}
default를 선언하면 인터페이스를 구현한 후 디폴트 메서드를 재정의 하지 않은 모든 클래스에서 디폴트가 쓰임
java8에서 추가된 자바 라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분 잘 동작하지만,
java7까지는 모든 클래스가 현재 인터페이스에 새로운 메서드가 추가될 일이 없다고 생각하고 구현 됨
java8 컬렉션 인터페이스들에 다수의 디폴트 메서드 추가 됨 (람다 활용을 위해, 실제로 기존의 많은 코드들이 영향 받음)
기존 인터페이스에 새로운 디폴트 메서드를 추가 하는 일은 꼭 필요한 경우가 아니라면 피해야 함
새로운 인터페이스라면 표준 메서드 구현을 제공하는데 아주 유용한 수단이며, 그 인터페이스를 더 쉽게 구현에 활용할 수 있게 하는 것 임
- 인터페이스를 설계 할 때는 여전히 주의 해야 함
자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 함
클래스가 인터페이스를 구현하는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에게 알려주는 것
=> 이 용도로만 사용해야한다!
인터페이스는 타입을 정의하는 용도로만 사용해야한다. 상수 공개용 수단으로 사용하지 말자.
두 가지 이상의 의미를 표현하고, 그 중 현재 표현하는 의미를 태그 값으로 알려주는 클래스
//code
객체 지향 언어는 타입 하나로 다양한 의미의 객체를 표현하는 "서브타이핑" 제공
abstract class Figure {
abstract double area();
}
class circle extends Figure {
final double radius;
Circle(double radius) { this.radius = radius; }
@Override double area() {return Math.PI * (radius * radius);}
}
class Rectangle extends Figure {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override double area() { return length * width ; }
}
태그 달린 클래스를 써야 하는 상황은 거의 없다. 새로운 클래스를 작성하는 데 태그 필드가 등장한다면 태그를 없애도 계층구조로 대체하는 방법을 생각 해 보고, 그렇게 사용하고 있다면 리펙토링을 고민 해 보자.
중첩 클래스를 언제, 왜 사용해야 하는지..
정적 멤버 클래스
비정적 멤버 클래스
바깥 인스턴스 클래스.new MemberClass(args)
로 생성 가능멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여 정적 멤버 클래스로 만들자.
멤버 클래스가 공개된 클래스의 public 이나 protected 멤버라면, 멤버클래스 역시 공개 API가 됨으로 추후 하위 호환성을 위해서라도 정적/비정적은 더 중요함
자바독
공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.
메서드용 문서화 주석에는 해당 메서드와 클라이언트 사이의 규약을 기술 해야 한다
스레드가 안전하든/안전하지 않든 스레드 안전 수준을 반드시 API설명에 포함해야 한다
직렬화할 수 있는 클래스라면 직렬화 형태도 API에 기술 해야한다.
지역변수, 제어구조, 라이브러리, 데이터 타입, 리플렉션, 네이티브 메서드, 최적화와 명명 규칙
enum Suit { CLUB, DIAMOND, HEART, SPACE}
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, TEN, JACK, QUEEN, KING}
...
static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for(Iterator<Rank> j = ranks.iterator(); j.hasNext();)
deck.add(new Card(i,next(), j.next()));
for (Suit suit : suits)
for(Rnak rank : ranks)
deck.add(new Card(suit, rank);
for-each를 사용하지 못하는 경우
가능한 모든 곳에서 for 대신 foreach를 사용하라
표준 라이브러리는 알고리즘에 능통한 개발자가 설계와 구현과 검증에 시간을 들여 개발했고, 여러 전문가가 동작을 검증 함
릴리즈 후 수 많은 개발자들이 사용하고, 버그가 발견 된다면 다음 릴리즈에서 수정 될 것이다~~
Java7~ Random 대신 ThreadLocalRandom 을 사용하면 대부분 잘 동작 (고 품질의 무작위 수 생산, 속도 빠름)
기본타입 | 참조타입 |
---|---|
값 | 값 + 식별성 |
값은 언제나 유효 | null 가능 |
시간 메모리 효율적 | 덜 효율적 |
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
naturalOrder.compare(new Integer(42), new Integer(42));
?? 1
i < j
는 잘 동작 : i, j가 참조하는 기본 값 타입으로 변환 됨
i == j
에서 객체 참조의 식별성 검사를 하면 서로 다른 Integer 인스턴스라면 값은 같아도 비교 결과는 false
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
int i = iBoxed; int j = jBoxed; //오토박싱
return i < j ? -1 : (i == j ? 0 : 1);
static Integer i; // int i;
public static void main(String[] args) {
if(i == 42)
System.out.println("블라블라");
}
}
+
는 편리하지만 성능 저하+
로 문자열 N개를 잇는 시간은 N의 제곱에 비례 함public class Room implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
// 청소가 필요한 자원. 절대 Room을 참조해서는 안 된다!
private static class State implements Runnable {
int numJunkPiles; // Number of junk piles in this room
State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
// close 메서드나 cleaner가 호출한다.
@Override public void run() {
System.out.println("Cleaning room");
numJunkPiles = 0;
}
}
// 방의 상태. cleanable과 공유한다.
private final State state;
// cleanable 객체. 수거 대상이 되면 방을 청소한다.
private final Cleaner.Cleanable cleanable;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state);
}
@Override public void close() {
cleanable.clean();
}
}
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
}
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
목표는 객체 식별성이 아니라 논리적 동치성을 확인해야 한다.
상위 클래스가 논리적 동치성을 비교하도록 되어있지 않을 경우다.
주로 값을 표현하는 클래스들이 해당한다.(ex: integer, String)
값 클래스라 하더라도 인스턴스가 둘 이상 만들어지지 않는(Enum) 의 경우도 재정의가 불필요하다. (스태틱도?)
대칭성
public final class CaseInsenstiveString{
private final String s;
public CaseInsensitiveString(String s){
this.s = Objects.requireNorNull(s);
}
}
@Override
public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(
((CaseInsensitiveString) o).s);
if (o instanceof String) // 한 방향으로만 작동한다!
return s.equalsIgnoreCase((String) o);
return false;
}
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point)o;
return p.x == x && p.y == y;
}
}
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
대칭성 실패
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
추이성 실패
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
- 리스코프 치환법칙 실패
* 리스코프 치환 법칙 : 타입의 메소드는 하위 타입에도 모두 작동해야한다.
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass())
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
/**
* 이 ColorPoint의 Point 뷰를 반환한다.
*/
public Point asPoint() {
return point;
}
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
equlas 의 판단에 가변적인 자원이 사용되지 않도록 한다.
모든 객체는 null 이 아니어야한다.
primitive type (float, double 제외) 는 "==" 연산자로 비교하고, 레퍼런스 타입 필드는 equals 로 float,double은 compare 메소드로 비교한다.
성능이 걱정된다면 cost가 적을 것 같은 필드부터 비교한다.
상태(lock)등을 연산하지 말자. (논리적 상태만을 비교하자)
캐쉬된 값을 저장하는 파생클래스 변수가 있을 경우 활용하자.
완벽한 equals 를 보여주는 코드
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
}
HashMap<PhonNumber,String> m = new Hashmap<>();
m.put(new PhoneNumber(707, 867, 5309),"jenny");
m.get(new PhoneNumber(707, 867, 5309));
what happened?
@override
public int hashcode(){ return 42;}
@Override
public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
@Overrride
public int hashCode(){
return Object.hash(lineNum,prefix,areaCode);
}
// 나라면 그냥 이렇게 쓸듯 하지만 성능이 아쉽다고 한다.
/**
* 이 전화번호의 문자열 표현을 반환한다.
* 이 문자열은 "XXX-YYY-ZZZZ" 형태의 12글자로 구성된다.
* XXX는 지역 코드, YYY는 프리픽스, ZZZZ는 가입자 번호다.
* 각각의 대문자는 10진수 숫자 하나를 나타낸다.
*
* 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
* 앞에서부터 0으로 채워나간다. 예컨대 가입자 번호가 123이라면
* 전화번호의 마지막 네 문자는 "0123"이 된다.
*/
@Override
public String toString() {
return String.format("%03d-%03d-%04d",
areaCode, prefix, lineNum);
}
이름 | 책임비 |
---|---|
조민혜 | O |
지성인 | O |
윤지수 | O |
양성만 | O |
허세조자룡선생 | O |
박성수 | O |
메서드를 설계할 때 주의할 점
오류는 가능한 한 빨리 (발생한 곳에서) 잡아야 한다.
메서드 바디가 실행되기 전에 잡지 않으면 수행되는 중간에 모호한 에러를 뱉으며 죽을지도 모른다. 더 최악엔 에러도 없이 객체를 망가뜨려 실패 원자성을 해칠지도 모른다.
공개되지 않은 메서드라면 단언문 (assert) 를 사용해라.
단언문은 다음과 같은 특징이 있는데
물론 예외는 있다
유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때, 혹은 계산 과정에서 암묵적으로 검사가 수행될 때
예를 들면 Collection.sort(List)
에서 상호 비교가능 유무 같은 케이스가 있겠다. (어차피 정렬 과정에서 검사가 수행되므로)
그렇다 한들 시간이 지나면 수행하면서 검사되는 지점과 매개변수를 받는 지점이 멀어지는것 같다. 명시적으로 검사하는게 좋지 않을까
매개변수에 제약을 두는게 좋다는게 아니다. 메서드는 최대한 범용저긍로 설계하되, 구현하려는 개념 자체가 특정한 제약을 내재한 경우도 드물지 않다.
메서드나 생성자를 작성할 때면 그 매개변수에 어떤 제약이 있을지 검사해야한다. 그 제약들을 문서화하고 메서드 코드 시작 부분에서 명시적으로 검사해야한다.
클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 해야한다.
public Period(Date start, Date end) {}
// no-setter
가 존재할 때 다음과 같이 setter 가 없더라도 객체 내부를 수정할 수 있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부가 수정된다.
다음과 같이 방어한다
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
두번째로는 getter 로 반환받은 객체를 수정해 내부를 변경할 수 있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.getEnd().setYear(98); // p 내부가 수정된다.
다음과 같이 방어한다.
public Date getStart() {
return new Date(start.getTime());
}
클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변이라면, 그 요소는 반드시 방어적으로 복사해야한다.
복사 비용이 너무 크거나 클라이언트가 그 요소를 수정할 일이 없음을 신뢰하고 불변식이 깨지더라도 그 영향이 오로지 클라이언트에게만 끼친다면
방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때 책임이 클라이언트에 있음을 문서에 명시한다.
직교성: 공통점이 없는 기능들이 잘 분리되어 있다. 기능을 원자적으로 쪼개 제공한다.
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "집합"
}
public static String classify(List<?> l`) {
return "리스트"
}
public static String classify(Collection<?> c) {
return "그외"
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
for (Collection<?> c: collections)
System.out.println(classify(c));
}
}
다음 코드의 결과는 예상과 다르게 "그외" 만 세번 출력된다.
재정의한 메서드는 동적으로 (런타임에) 선택되고, 다중정의한 메서드는 정적으로 (컴파일타임에) 선택되기 때문인데
왜? 이해안됨
API 사용자가 매개변수를 넘기면서 어떤 다중정의 메서드가 호출될지 모른다면 프로그램이 오동작하기 쉽다. 런타임에 이상하게 행동할 것이며 API 사용자들은 디버깅하는데 시간을 낭비할 것이다.
그러니 다중정의가 혼동을 일으키는 상황은 피해야 한다.
위 코드는 다음과 같이 바꾼다면 깔끔하다
public static String classify(Collection<?> c) {
return c instanceof Set ? "집합" :
c instanceof List ? "리스트" : "그외";
}
근본적으로 다르다는건 두 타입의 (null이 아닌) 값을 서로 어느쪽으로든 형변환 할 수 없다는 뜻이다. 다만 한가지 함정이 있는데
오토박싱과 제네릭이 이 생태계를 교란했다.
List 를 예로 들 수 있겠는데
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
List 인터페이스가 remove(Object) 와 remove(int) 를 다중정의 했다. 전자는 Object 를 제거하고 후자는 해당 int 인덱스의 원소를 제거한다. 반면 set.remove(Object) 밖에 없으므로 i가 오토박싱되어 기대한 대로 동작을 한것
두번째 함정은 람다와 메서드 참조에서 발생한다.
new Thread(System.out::println).start();
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
양쪽다 Runnable을 받는 다중정의 메서드를 갖고 있지만 1은 제대로 동작하고 2는 제대로 동작하지 않는데 그 이유는 submit 의 다중정의 메서드 중에 Callable 를 받는 메서드도 있다는 데 있다.
상식적으로 생각하기엔 println 이 void 를 반환하니 반환값이 존재하는 Callable 과 헷갈릴리 없다고 생각되지만 다중정의 해소 알고리즘은 이렇게 동작하지 않는다.
암시적 타입 람다식이니 부정확한 메서드 참조 인수 표현식이니 하는데 넘어가래서 넘어가기러 했다
정리하자면 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안된다. 서로 다른 함수형 인터페이스라도 서로 근본적으로 다르지 않다는 뜻이다.
흠
프로그래밍 언어가 다중정의를 허용한다고 해서 다중정의를 꼭 활용하란 뜻은 아니다.
일반적으로 매개변수 수가 같을 때는 다중정의를 피하는게 좋다. 상황에 따라, 특히 생성자라면 이 조언을 따르기가 불가능 할 수 있다.
그럴때는 헷갈릴 만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 해야 한다.
이것이 불가능하면, 예컨대 기존 클래스를 수정해 새로운 인터페이스를 구현해야 할 때는 같은 객체를 입력받는 다중 정의 메서드들이 모두 동일하게 동작하도록 만들어야 한다.
그렇지 못하면 프로그래머들은 다중정의된 메서드나 생성자를 효과적으로 사용하지 못할 것이고, 의도대로 동작하지 않는 이유를 이해하지도 못할 것이다.
인수 개수가 일정하지 않은 메서드를 정의해야 한다면 가변인수가 반드시 필요하다. 메서드를 정의할 때 필수 매개변수는 가변인수 앞에 두고, 가변인수를 사용할 때는 성능 문제까지 고려하자. (인수를 0개 넣었을때 런타임에서야 알아차리는걸 막기위해)
흔히 성능 핑계를 대는데 두가지 면에서 틀린 주장이다.
첫번째, 성능 분석 결과 이 할당이 성능 저하의 주범이라고 확인되지 않는 한 이정도의 성능차이는 신경 쓸 수준이 못 된다.
두번째, 빈 컬랙션과 빈 배열은 굳이 새로 할당하지 않고도 반환할 수 있다.
성능이 문제가 되면 매번 똑같은 빈 "불변" 컬랙션을 반환하면 되지 않는가
길이 0인 배열도 불변이다.
null 이 아닌 빈 배열이나 컬랙션을 반환하라. null 을 반환하는 api 는 사용하기 어렵고 오류 처리 코드도 늘어난다. 그렇다고 성능이 좋은것도 아니다.
옵셔널은 checked exception 과 취지가 비슷하다. 즉 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려준다. 클라이언트는 값을 받지 못했을 때 취할행동을 선택할 것이다.
결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional 를 반환하자
값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 하는 메서드라면 옵셔널을 반환해야 할 상황일 수 있다. 하지만 옵셔널 반환에는 성능 저하가 뒤따르니, 성능에 민감한 메서드라면 null 을 던지는 편이 나을수도 있다. 그리고 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물며, 옵셔널을 필드로 가지고 있어야할 클래스가 있다면 나쁜 냄새일 가능성이 높으니 다시한번 생각해볼것
catch
블록을 비워두면 예외가 존재할 이유가 없어진다.catch
블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수의 이름도 ignored
로 바꿔놓도록 하자.long
과 double
외의 변수를 읽고 쓰는 동작은 원자적(atomic)이다.
// 원래 코드
while (!stopRequested)
i++;
// 최적화한 코드
if (!stopRequested)
while (true)
i++;
volatile
한정자는 배타적 수행과는 상관없지만 항상 가장 최근의 기록된 값을 일게 됨을 보장한다.private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
++
)는 원자적이지 않다.CopyOnWriteArrayList
는 내부를 변경하는 작업은 항상 깨끗한 복사본을 만들어 수행하도록 구현했다.Runnable
과 Callable
)Executors.newCachedThreadPool
Executors.newFixedThreadPool
wait
와 notify
보다는 동시성 유틸리티를 애용하라wait
와 notify
는 올바르게 사용하기가 아주 까다로우니 고수준 동시성 유틸리티를 사용하자.ConcurrentHashMap
은 get
같은 검색 기능에 최적화 되었다.Collections.synchronizedMap
보다는 ConcurrentHashMap
을 사용하는 게 훨씬 좋다.BlockingQueue
는 작업 큐(생산자-소비자 큐)로 쓰기에 적합하다.CountDownLatch
는 일회성 장벽으로, 하나 이상의 스레드가 또 다른 하나 이상의 스레드 작업이 끝날 때까지 기다리게 한다.System.currentTimeMillis
가 아닌 System.nanoTime
을 사용하자.
System.nanoTime
은 더 정확하고 정밀하며 시스템의 실시간 시계의 시간 보정에 영향받지 않는다.CyclicBarrier
는 여러 개의 CountDownLatch
를 대체할 수 있다.wait
메서드를 사용할 때는 반드시 대기 반복문(wait loop) 관용구를 사용하라. 반복문 밖에서는 절대로 호출하지 말자.synchronized
한정자를 선언할지는 구현 이슈일 뿐 API에 속하지 않는다.synchronized
메서드 대신 비공개 락 객체를 사용해야 한다.소스 파일 하나에 톱레벨 클래스를 여러개 선언해도 자바 컴파일러는 문제삼지 않으나, 경우에 따라 문제될 수 있다.
동일한 클래스명으로 각각 다른 파일에 중복 정의되어 있는 경우 컴파일이 실패하거나, 컴파일 순서에 따라 어떻게 동작할 지 예측 할 수 없다.
26-1. raw type이란?
클래스와 인터페이스 선언에 타입 매개변수(ex. )가 있으면 제네릭 클래스, 제네릭 인터페이스라고 함.
제네릭 타입을 정의할 때 타입 매개변수를 쓰지 않는 경우를 raw type이라한다.
List<E>은 제네릭 타입이며, 이의 raw type은 List임
26-2. raw type의 사용을 비추천한다.
오류는 컴파일 타임에 발견되는 것이 가장 이상적이다. raw type을 사용할 경우 unchecked 경고가 나오며
잘못된 타입을 add할 수도 있다.
// 코드 26-4 런타임에 실패한다. - unsafeAdd 메서드가 로 타입(List)을 사용 (156-157쪽)
public class Raw {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0); // 컴파일러가 자동으로 형변환 코드를 넣어준다.
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
}
제네릭 타입을 쓴다면 컴파일 단계에서 타입 불변, 안정성을 확보 할 수 있다.
26-3. raw type, 와일드 카드(), 의 차이 raw type 은 타입에 안전하지 않으나, 와일드카드는 안전하다.
private static void addObjectToList1(final List<?> aList, final Object o ) {
aList.add(o);
} //컴파일 실패함. null만 add가능
private static void addObjectToList2(final List<Object> aList, final Object o ) {
aList.add(o);
} //컴파일 실패함. List<Object>인데 main에서 List<String>을 인자로 넘기려함
private static <T> void addObjectToList3(final List<T> aList, final T o ) {
aList.add(o);
} //정상적으로 컴파일되고 실행 가능함.
public static void main(String[] args) {
List<String> testList = new ArrayList<String>();
String s = "Add me!";
addObjectToList1(testList, s);
addObjectToList2(testList, s);
addObjectToList3(testList, s);
}
26-4. raw type을 쓰는 예외 상황
class의 리터럴은 raw type으로 써야함. List.class
는 되지만 List<String>.class
는 허용되지 않는다.
instanceof
연산자는 런타임에서 타입을 비교한다. 제네릭 타입은 컴파일 단계에서 소거되므로 제네릭 타입으로 비교할 수 없다.
비검사 경고를 제거할 수록 타입 안정성이 높아진다. 만약 타입 안정성이 확실한데 컴파일러의 경고를 없애고 싶다면
@SuppressWarnings("unchecked")
를 사용한다.
@SuppressWarnings("unchecked")
의 범위는 최대한 줄여서 달아야 한다. 메소드 레벨, 클래스 레벨보다는
비검사 경고가 뜨는 지역변수 레벨에 다는 것이 가장 좋다. 또한 타입에 안전한 이유를 주석으로 추가해주자.
배열은 공변(covariant)이다. class Sub extends Super
라면 Sub[]는 Super[]의 하위 타입이다. 그러나 리스트는 불공변이다. List<Sub>
와 List<Super>
는 상하위 관계가 아니다.
Object[] objectArray = new Integer[1];
objectArray[0] = "Hello world"; // 런타임에 ArrayStoreException 발생
List<Object> objectList = new ArrayList<Integer>; // 컴파일 실패
objectList.add("Hello world"); // 위에서 이미 컴파일에 실패했으며, 타입이 달라 넣을 수도 없다
배열이나 리스트나 Integer용 저장소에 String을 넣을 순 없으나, 전자는 런타임에 실수를 알 수 있고 후자는 컴파일타임에 알 수 있다. 후자가 당연히 좋으니 배열보다는 리스트를 사용하는 것이좋다. 다만 성능적인 측면에선 배열이 앞설 수 있다.
일반 클래스를 제네릭 클래스로 만드는 방법
다음은 일반 클래스 Stack을 제네릭 클래스로 바꿔본 예제이다.
public class Stack { // => Stack<E>
private Object[] elements; // => E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// => @SuppressWarnings("unchecked")
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY]; // => (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) { // => push(E e)
ensureCapacity();
elements[size++] = e;
}
private void ensureCapacity() {
if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1);
}
public Object pop() { // => E pop()
if (size == 0) throw new EmptyStackException();
Object result = elements[--size]; // => E result = elements[--size];
elements[size] = null;
return result;
}
...
}
추가적으로 기본 타입은 제네릭 타입List<int>
으로 쓸 수 없으며 박싱된 기본타입을 써야한다.
클래스와 마찬가지로 메소드도 제네릭이 가능하다면 사용을 권장한다. 사용자 측에서 형변환하는 것보다 안전하고 유연해진다.
public static Set union(Set s1, Set s2) { // => <E> Set<E> union(Set<E> s1, Set<E> s2)
Set result = new HashSet(s1); // => Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
제네릭 싱글톤 팩토리
불변 객체를 여러 타입으로 활용할 수 있게 만들어야하는데, 이때는 제네릭 싱글톤 팩토리를 만들면 된다.
@SuppressWarnings("unchecked")
public static <T> Comparator<T> reverseOrder() {
return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}
만약 제네릭을 쓰지 않았다면 요청 타입마다 형변환하는 정적 팩토리를 만들어야했을 것이다.
재귀적 타입 한정
자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용 범위를 한정할 수 있다.
다음과 같이 타입 매개변수를 한정적으로 기술해주는 방식이다. 이를 통해 모든 타입 E는 자신과 비교할 수 있다라는 것을 나타낸다. max 메소드의 리턴값은 Comparable<E>
을 구현했으므로, 다른 E와 비교할 수 있다.
public static <E extends Comparable<E>> E max(Collection<E> c)
제네릭 타입과 마찬가지로 클리아언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기 쉽다.
매개변수화 타입은 불공변이기 때문에 때로는 유연한 처리방식이 필요하다.
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
// 코드 31-1 와일드카드 타입을 사용하지 않은 pushAll 메서드 - 결함이 있다!
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e); //stack의 매개변수타입은 Number인데 e가 intVal이라면 불공변이기 때문에 오류가난다.
}
// 코드 31-2 E 생산자(producer) 매개변수에 와일드카드 타입 적용
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
// 코드 31-3 와일드카드 타입을 사용하지 않은 popAll 메서드 - 결함이 있다!
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
// 코드 31-4 E 소비자(consumer) 매개변수에 와일드카드 타입 적용
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라.
PESC : produce-extends, consumer-super
###제네릭 활용 사례
지금까지 사용해왔던 object mapper는 class단위로만 desirialize를 해왔는데 nested object에 대한 desirialize를 어떻게 수행 할 수 있을까에 대한 고민
jackson라이브러리의 TypeReference로부터 데이터를 읽는 과정에 대한 소개
//desirialize하려는 매개변수 타입을 인자로 넘겨준다.
response = RedisHelper.inquireListResponse(redisKey, redisField, new TypeReference<ListResponse<WebArticle>>() {});
public static <T> ListResponse<T> inquireListResponse(String key, String feild, TypeReference<ListResponse<T>> typeReference) {
String cache = hget(key, feild);
if (cache != null) {
try {
return JsonUtil.jsonToNestedObject(cache, typeReference);
} catch (Exception e) {
log.error(e.getClass().getSimpleName() + "! redis inquireList error : " + e.getLocalizedMessage() + ", key=" + key, e);
}
}
return null;
}
//매개변수 타입을 이용하는 jackson mapper.read를 호출한다.
public static <T> ListResponse<T> jsonToNestedObject(String json, TypeReference<ListResponse<T>> typeReference) {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
try {
return mapper.readValue(json, typeReference);
} catch (IOException e) {
log.error(e.getClass().getSimpleName() + ": jsonToObject error : " + e.getLocalizedMessage());
}
return null;
}
//타입팩토리의 constructType메소드를 통해 바인딩된 JavaType을 가져오고 해당 타입을 통해 문서를 desirialize한다.
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> T readValue(String content, TypeReference valueTypeRef) throws IOException, JsonParseException, JsonMappingException
{
return (T) _readMapAndClose(_jsonFactory.createParser(content), _typeFactory.constructType(valueTypeRef));
}
class Plant {
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override public String toString() {
return name;
}
}
// 코드 37-1 ordinal()을 배열 인덱스로 사용 - 따라 하지 말 것! (226쪽)
Set<Plant>[] plantsByLifeCycleArr = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
plantsByLifeCycleArr[i] = new HashSet<>();
}
for (Plant p : garden) {
plantsByLifeCycleArr[p.lifeCycle.ordinal()].add(p);
}
// 결과 출력
for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
}
// 코드 37-2 EnumMap을 사용해 데이터와 열거 타입을 매핑한다. (227쪽)
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for (Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
System.out.println(plantsByLifeCycle);
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
implements java.io.Serializable, Cloneable
{
/**
* Array representation of this map. The ith element is the value
* to which universe[i] is currently mapped, or null if it isn't
* mapped to anything, or NULL if it's mapped to null.
*/
private transient Object[] vals;
/**
* Creates an empty enum map with the specified key type.
*
* @param keyType the class object of the key type for this enum map
* @throws NullPointerException if {@code keyType} is null
*/
public EnumMap(Class<K> keyType) {
this.keyType = keyType;
keyUniverse = getKeyUniverse(keyType);
vals = new Object[keyUniverse.length];
}
}
// 코드 37-3 스트림을 사용한 코드 1 - EnumMap을 사용하지 않는다! (228쪽)
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle))
);
// 코드 37-4 스트림을 사용한 코드 2 - EnumMap을 이용해 데이터와 열거 타입을 매핑했다. (228쪽)
System.out.println(Arrays.stream(garden)
.collect(groupingBy(p -> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet()))
);
public enum Phase {
SOLID, LIQUID, GAS;
private static final Transition[][] = TRANSITIONS = {
{ null, MELT, SUBLINE },
{ FREEZE, null, BOIL },
{ DEPOSIT, CONDENSE, null }
};
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
ordinal을 이용한 맵핑의 문제
EnumMap을 이용한 방식 적용
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
// 상전이 맵을 초기화한다.
private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values())
.collect(groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
// 이후 상태를 전이에 대응시키는 EnumMap 생성
toMap(t -> t.to,
t -> t,
(x, y) -> y,
() -> new EnumMap<>(Phase.class))
)
);
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
// 코드 38-1 인터페이스를 이용해 확장 가능 열거 타입을 흉내 냈다. (232쪽)
public interface Operation {
double apply(double x, double y);
}
// 코드 38-1 인터페이스를 이용해 확장 가능 열거 타입을 흉내 냈다. - 기본 구현 (233쪽)
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
여기서 BasicOperation은 확장할 수 없지만 Operation interface를 확장할 수 있음.
ExtendedOperation Enum 정의
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
public static void main(String[] args) {
double x = 1;
double y = 2;
test(ExtendedOperation.class, x, y);
test(BasicOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test(Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
}
}
// 주로 개발하는 형태로 만든다면
List<Operation> operationList = new ArrayList<>();
operationList.addAll(Arrays.asList(ExtendedOperation.values()));
operationList.addAll(Arrays.asList(BasicOperation.values()));
for (Operation op : operationList) {
System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y));
}
/* 실행결과
1.000000 ^ 2.000000 = 1.000000
1.000000 % 2.000000 = 1.000000
1.000000 + 2.000000 = 3.000000
1.000000 - 2.000000 = -1.000000
1.000000 * 2.000000 = 2.000000
1.000000 / 2.000000 = 0.500000
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test {
/**
* Default empty exception
*/
static class None extends Throwable {
private static final long serialVersionUID = 1L;
private None() {
}
}
/**
* Optionally specify <code>expected</code>, a Throwable, to cause a test method to succeed if
* and only if an exception of the specified class is thrown by the method. If the Throwable's
* message or one of its properties should be verified, the
* {@link org.junit.rules.ExpectedException ExpectedException} rule can be used instead.
*/
Class<? extends Throwable> expected() default None.class;
/**
* Optionally specify <code>timeout</code> in milliseconds to cause a test method to fail if it
* takes longer than that number of milliseconds.
* <p>
* <b>THREAD SAFETY WARNING:</b> Test methods with a timeout parameter are run in a thread other than the
* thread which runs the fixture's @Before and @After methods. This may yield different behavior for
* code that is not thread safe when compared to the same test method without a timeout parameter.
* <b>Consider using the {@link org.junit.rules.Timeout} rule instead</b>, which ensures a test method is run on the
* same thread as the fixture's @Before and @After methods.
* </p>
*/
long timeout() default 0L;
}
// 코드 39-8 반복 가능한 애너테이션 타입 (243-244쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
// 반복 가능한 애너테이션의 컨테이너 애너테이션 (244쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
// 코드 39-9 반복 가능 애너테이션을 두 번 단 코드 (244쪽)
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
// equals 메소드는 매배견수가 Object임.
@Override public boolean equals(Bigram b) {
return ..
}
@Override public boolean equals(Object o) {
return ..
}
* @author unascribed
* @see java.io.ObjectOutputStream
* @see java.io.ObjectInputStream
* @see java.io.ObjectOutput
* @see java.io.ObjectInput
* @see java.io.Externalizable
* @since 1.1
*/
public interface Serializable {
}
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
// 코드 42-1 익명 클래스의 인스턴스를 함수 객체로 사용 - 낡은 기법이다! (254쪽)
Collections.sort(words, new Comparator<String>() {
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
// 코드 42-2 람다식을 함수 객체로 사용 - 익명 클래스 대체 (255쪽)
Collections.sort(words,
(s1, s2) -> Integer.compare(s1.length(), s2.length()));
Collections.sort(words, Comparator.comparingInt(String::length));
words.sort(Comparator.comparingInt(String::length));
public enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
public abstract double apply(double x, double y);
}
public enum Operation {
PLUS ("+", (x, y) -> x + y),
MINUS ("-", (x, y) -> x - y),
TIMES ("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
map.merge(key, 1, (count, incr) -> count + incr);
map.merge(key, 1, Integer::sum);
/**
* Adds two integers together as per the + operator.
*
* @param a the first operand
* @param b the second operand
* @return the sum of {@code a} and {@code b}
* @see java.util.function.BinaryOperator
* @since 1.8
*/
public static int sum(int a, int b) {
return a + b;
}
service.execute(GoshThisClassNameIsHumongous::action);
service.execute(() -> action());
interface G1 {
<E extends Exception> Object m() throws E;
}
interface G2 {
<F extends Exception> String m() throws Exception;
}
interface G extends G1, G2 {}
// 인터페이스 G를 함수 타입으로 표현하면
<F extends Exception> () -> String throws F
service.execute(GoshThisClassNameIsHumongous::action);
Integer::parseInt
str -> Interge.parseInt(str)
Instant.now()::isAfter
Instant than = Instant.now();
t -> then.isAfter(t)
String::toLowerCase
str -> str.toLowerCase()
TreeMap<K,V>::new
() -> new TreeMap<K,V>()
int[]::new
len -> new int[len]
warning: [unchecked] Possible heap pollution from
parameterized vararg type List<String>
// 제네릭과 varargs를 혼용하면 타입 안전성이 깨진다!
// signature ([Ljava/util/List<Ljava/lang/String;>;)V
// declaration: void dangerous(java.util.List<java.lang.String>[])
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
// 제네릭 varargs 매개변수가 타입이 다른 객체 참조시 힙 오염(heap pollution) 발생
objects[0] = intList;
// ClassCastException: java.lang.Integer cannot be cast to java.lang.String
// INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; (itf)
// CHECKCAST java/lang/String
String s = stringLists[0].get(0);
}
public static void main(String[] args) {
// INVOKESTATIC Dangerous.dangerous ([Ljava/util/List;)V
dangerous(List.of("There be dragons!"));
}
dangerous
메서드에 형변환 코드는 없지만 ClassCastException
발생@SafeVarargs
Arrays.asList(T... a)
Collections.addAll(Collection<? super T> c, T... elements)
Enums.of(E first, E... rest)
@SafeVarargs
애너테이션이 추가되어 클라이언트 측에서 발생하는 경고를 숨길 수 있음@SafeVarargs
는 작성자가 타입 안전을 보증하는 것@SafeVarargs
을 달 수 있다.
@SafeVarargs
는 오버라이드 할 수 없는 메서드에서만 유효함. 오버라이드 가능한 메서드가 안전하다는 것을 보장 할 수 없기 때문
// 자신의 제네릭 매개변수 배열의 참조를 노출한다. - 안전하지 않다!
// signature <T:Ljava/lang/Object;>([TT;)[TT;
// declaration: T[] toArray<T>(T[])
static <T> T[] toArray(T... args) {
return args;
}
// signature <T:Ljava/lang/Object;>(TT;TT;TT;)[TT;
// declaration: T[] pickTwo<T>(T, T, T)
static <T> T[] pickTwo(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
// INVOKESTATIC PickTwo.toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError(); // 도달할 수 없다.
}
pickTow
메서드는 제네릭 가변인수를 받는 toArray
를 호출한다는 점만 빼면 위험하지 않고 경고도 내지 않음
pickTwo
로 전달되는 객체 타입에 상관 없이 Object[]
타입의 varargs 매개변수 배열을 만드는 코드를 생성toArray
메서드는 단순히 배열을 pickTwo
에 반환하고 다시 호출한 클라이언트까지 반환되므로 pickTwo
는 항상 Object[]
타입을 반환public static void main(String[] args) {
// ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
// INVOKESTATIC PickTwo.pickTwo (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)[Ljava/lang/Object;
// CHECKCAST [Ljava/lang/String;
String[] attributes = pickTwo("좋은", "빠른", "저렴한");
}
pickTwo
호출시 형변환 코드는 없지만 ClassCastException
발생
Object[]
는 String[]
하위 타입이 아니기 때문@SafeVarargs
로 제대로 애노테이트된 또 다른 varargs 메서드에 넘기는 것은 안전함// 제네릭 varargs 매개변수를 안전하게 사용하는 메서드
@SafeVarargs
static <T> List<T> flatten(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
@SafeVarargs
의 대안// 제네릭 varargs 매개변수를 List로 대체한 예 - 타입 안전하다.
static <T> List<T> flatten(List<List<? extends T>> lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists)
result.addAll(list);
return result;
}
List
로 변경하면 메서드가 타입 안전함을 컴파일러에 의해 증명됨Set<E>
, Map<K, V>
, ThreadLocal<T>
, AtomicReference<T>
등은 하나의 컨테이너에서 매개변수화할 수 있는 타입의 수가 제한됨. 일반적인 용도에 맞게 설계된 것이니 문제되지 않음Favorites
클래스를 생각해보자
class
의 클래스가 제네릭이기 때문에 각 타입의 Class
객체를 매개변수화한 키 역할로 사용할 수 있음
class
리터럴을 타입 토큰(type token)이라 함public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
// 타입 안전 이종 컨테이너 패턴 - 클라이언트
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());
}
Favorites
인스턴스는 타입 안전
String
을 요청했는데 Integer
를 반환하는 일은 절대 없음. 따라서 Favorites
는 타입 안전 이종(heterogeneous) 컨테이너라 부름public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
favorites
맵의 키가 Class<?>
타입이기 때문에 모든 키가 서로 다른 매개변수화 타입일 수 있음 (Class<String>
, Class<Integer>
등)
favorites
맵의 값 타입은 단순히 Object
putFavorite
메서드는 키와 값 사이의 '타입 링크(type linkage)' 정보는 버려짐
getFavorite
메서드에서 이 관계를 되살릴 수 있음getFavorite
메서드는 맵에서 Object
타입을 꺼내서 동적 형변환
cast
메서드는 형변환 연산자의 동적 버전cast
메서드는 Class
클래스가 제네릭이라는 이점을 완벽히 활용
T
로 비검사 형변환하는 손실 없이도 Favorites
를 타입 안전하게 만듬Class
객체를 로 타입으로 넘기면 Favorites
인스턴스의 타입 안전성이 쉽게 깨짐
checkedSet
, checkedList
, checkedMap
등이 있음// 동적 형변환으로 런타임 타입 안전성 확보
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNull(type), type.cast(instance));
}
Favorites
가 사용하는 타입 토큰은 비한정적. 즉 getFavorite
, putFavorite
은 어떤 Class
객체든 받아들임
annotationType
인수는 애너테이션 타입을 뜻하는 한정적 타입 토큰public <T extends Annotation> T getAnnotation(Class<T> annotationType);
Class<?>
타입의 객체가 있고, getAnnotation
처럼 한정적 타입 토큰을 받는 메서드에 넘긴다고 가정하자
Class<? extends Annotaion>
으로 형변환하면 비검사이므로 컴파일 경고 발생Class
클래스는 이런 형변환을 안전하고 동적으로 수행하는 메서드를 제공
asSubclass
메서드로 호출된 인스턴스 자신의 Class
객체를 인수가 명시한 클래스로 형 변환하고 실패하면 ClassCastException
발생// asSubclass를 사용해 한정적 타입 토큰을 안전하게 형변환한다.
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null; // 비한정적 타입 토큰
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
// 정수 열거 패턴 - 상당히 취약하다!
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
// 향긋한 오렌지 향의 사과 소스!
int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;
// 가장 단순한 열거 타입
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
toString
메서드는 출력하기 적합한 문자열을 내어줌Object
메서드들의 고품질 구현을 제공하며 Comparable
과 Serializable
을 구현함// 데이터와 메서드를 갖는 열거 타입
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
...;
private final double mass; // 질량(단위: 킬로그램)
private final double radius; // 반지름(단위: 미터)
private final double surfaceGravity; // 표면중력(단위: m / s^2)
// 중력상수(단위: m^3 / kg s^2)
private static final double G = 6.67300E-11;
// 생성자
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("%s에서의 무게는 %f이다.%n", p, p.surfaceWeight(mass));
}
// 값에 따라 분기하는 열거 타입 - 이대로 만족하는가?
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// 상수가 뜻하는 연산을 수행한다.
public double apply(double x, double y) {
switch(this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x / y;
}
throw new AssertionError("알 수 없는 연산: " + this);
}
}
throw
문이 없으면 컴파일되지 않음case
문 추가 필요. 빠져도 컴파일되지만 새로 추가한 연산 수행시 런타임 오류 발생// 상수별 메서드 구현을 활용한 열거 타입
public enum Operation {
PLUS ("+") { public double apply(double x, double y) { return x + y; } },
MINUS ("-") { public double apply(double x, double y) { return x - y; } },
TIMES ("*") { public double apply(double x, double y) { return x * y; } },
DIVIDE("/") { public double apply(double x, double y) { return x / y; } };
public abstract double apply(double x, double y);
}
apply
메서드가 상수 선언 바로 옆에 붙어 있으니 메서드 재정의를 깜빡하기 어려움
apply
메서드가 추상 메서드이므로 누락시 컴파일 오류// 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입
public enum Operation {
PLUS ("+") { public double apply(double x, double y) { return x + y; } },
MINUS ("-") { public double apply(double x, double y) { return x - y; } },
TIMES ("*") { public double apply(double x, double y) { return x * y; } },
DIVIDE("/") { public double apply(double x, double y) { return x / y; } };
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
public abstract double apply(double x, double y);
}
valueOf(String)
메서드가 있음
toString
메서드를 재정의하는 경우 fromString
메서드를 함께 제공하는 걸 고려하자// 열거 타입용 fromString 메서드 구현하기
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(toMap(Object::toString, e -> e));
// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
// 값에 따라 분기하여 코드를 공유하는 열거 타입 - 좋은 방법인가?
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate;
int overtimePay;
switch(this) {
case SATURDAY: case SUNDAY: // 주말
overtimePay = basePay / 2;
break;
default: // 주중
overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
case
문을 넣지 않는다면 (컴파일은 되지만) 휴가 기간에도 평일과 같은 임금을 받게 됨PayrollDay
에 평일 잔업수당 계산용 메서드인 overtimePay
를 구현해 놓고 주말 상수에만 재정의해 쓰면 장황한 부분을 줄일 수 있지만, switch
문과 동일한 단점을 가짐
overtimePay
메서드를 재정의하지 않으면 평일용 코드를 그대로 물려받게 됨// 전략 열거 타입 패턴
enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY), THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
SATURDAY(WEEKEND), SUNDAY(WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// 전략 열거 타입
enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
switch
문이 좋은 선택이 될 수 있음
Operation
열거 타입이 있는데 각 연산의 반대 연산을 반환하는 메서드가 필요하다면// switch 문을 이용해 원래 열거 타입에 없는 기능을 수행한다.
public static Operation inverse(Operation op) {
switch(op) {
case PLUS: return Operation.MINUS;
case MINUS: return Operation.PLUS;
case TIMES: return Operation.DIVIDE;
case DIVIDE: return Operation.TIMES;
default: throw new AssertionError("알 수 없는 연산: " + op);
}
}
ordinal
이라는 메서드를 제공// ordinal을 잘못 사용한 예 - 따라하지 말 것!
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() { return ordinal() + 1; }
}
// 인스턴스 필드에 정수 데이터를 저장하는 열거 타입
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
Enum
의 API 문서를 보면 ordinal
에 대해
EnumSet
과 EnumMap
같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었다.// 비트 필드 열거 상수 - 구닥다리 기법!
public class Text {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
// 매개변수 styles는 0개 이상의 STYLES_ 상수를 비트별 OR한 값이다.
public void applyStyles(int styles) { ... }
}
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
Set
인터페이스를 구현한 EnumSet
은 타입 안전하고 다른 Set
구현체와도 함께 사용 가능// EnumSet - 비트 필드를 대체하는 현대적 기법
public class Text {
public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}
// 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
public void applyStyles(Set<Style> styles) { ... }
}
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
public interface Code<T> {
T getCode();
}
public enum MemberStatus implements Code<String> {
ACTIVATED("ACT"),
PENDING ("PEN");
@Getter private final String code;
MemberStatus(String code) { this.code = code; }
}
public abstract class AbstractCodeConverter<E extends Enum<E> & Code<T>, T>
implements AttributeConverter<E, T> {
private final Class<E> type;
public AbstractCodeConverter(Class<E> type) { this.type = type; }
@Override public final T convertToDatabaseColumn(E attribute) {
if(attribute == null) return null;
return attribute.getCode();
}
@Override public final E convertToEntityAttribute(T dbData) {
return EnumUtils.lookupFromCode(type, dbData);
}
}
@Converter(autoApply = true)
public class MemberStatusConverter
extends AbstractCodeConverter<MemberStatus, String> {
public MemberStatusConverter() { super(MemberStatus.class); }
}
public class EnumUtils {
public static <E extends Enum<E> & Code<T>, T> E lookupFromCode(
Class<E> type, T code) {
for (E e : EnumSet.allOf(type))
if (e.getCode().equals(code)) return e;
return null;
}
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.