①
먼전 public API를 설계 하고 나머지 멤버들은 습관적으로 private로 만든다. 반드시 필요한 경우에만 private을 package-private으로 바꾼다.
②
public 클래스는 public 필드를 갖지 말아야 한다.
//엄청난 보안허점을 야기한다.
public static final Type[] VALUES = {...}
//public static final String [] VALUES = {};
//혹시나 해서 써놓는데..Type 이란 타입은 없다.
//외부에 제공되는 API는 배열의 복사본을 넘기는 방법이 좋다.
private static final Type[] PRIVATE_VALUES = {...};
public static final Type[] Values() {
return (Type[]) PRIVATE_VALUES.clone();
}
불변 클래스는 그 인스턴스의 내용을 절대로 바꿀수 없는 클래스다. 이 클래스의 각 인스턴스가 저장하는 정보는 인스턴스를 생성할때 단 한번 만든후에
인스턴스가 소멸할때 까지 바뀌지 않는다. (ex. String 과 기본타입에 대한 래퍼 클래스)
① 객체를 변경하는 메소드를 제공하지 않는다
② 재정의할 수 있는 메소드를 제공하지 않는다.
③ 모든 필드를 final로 만든다.
④ 모든 필드를 private으로 만든다.
⑤ 가변 객체를 잠조하는 필드는 배타적으로 접근해야 한다. (가변 객체를 참조하는 힐드가 있다면 클라이언트가 이 객체에 대한 참조를 얻지 못하게 한다.)
이후 책 99 페이지 까지는 지극히 당연한 내용이 나온다.
간단한 4칙 연산을 하는 예제 클래스를 제공해 주고 예제 클래스에서 2개의 정수가 들어올 경우 더하고 빼고 곱하고 나눈 결과를
내부 멤버로 처리 하는 것이 아니라 생성자를 통해 새로 생성 하고 있다.
이는 당연한 내용이 아닌가? 불변 클래스이므로 필요시점에 새로운 객체를 생성하는건 당연한 것이다.
두번재로 불변 객체는 원래부터 다중 스레드 환경에서 안전하기 때문에 동기화할 필요가 없다 라는 내용이 나오는데..
이또한 당연한 내용이다. 변화하지 않는 객체를 굳이 동기화할 필요가 있는가?? 여러 곳에서 참조 한다고 해도 불변 객체는 유일하므로
가져다가 써도 아무~ 문제 업다.
머 자주 스는 객체를 상수로 제공 하는 방법도 소개 된다.
그것은 다음과 같다.
public static final Complex ZERO = new Complex(0,0);
public static final Complex ONE = new Complex(1,0);
public static final Complex TWO = new Complex(0,1);
이거에서 한걸음 더 낳아가 상수가 아니라 스태틱 팩토리를 통하여 제공할수도 있다. 예제는
HeaddFirst 디자인 패턴의 팩토리 패턴을 참조 하자. 링크는 다음과 같다. 링크
//스태틱 펙토리 메소드를 생성자 대신 쓰는 불변 클래스
public class Complex {
private final float re;
private final float im;
Complex(float re, float im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(float re, float im) {
return new Complex(re, im);
}
..
}
불변객체의 단점으로는 각각의 값에 대해 서로 다른 객체가 필요하다는 사실이다.
부호비트만 다르고 나머지는 동일한 인스턴스가 있다고 가정 했을때
이 인스턴스가 100메가 라고 가정해 보자.
단지 1비트가 다른 인스턴스를 만들기 위해 100메가를 복사해야 한다면 이또한 엄청난 낭비일 것이다.
그럼에도 불구 하고 ... 모든 클래스는 특별한 이유가 ?다면 불변 클래스로 만들어야 한다.
다음은 페이지 102~103에 나온 간단한 방어 복사의 예시문이다.
BigInteger 나 BigDecimal 클래스가 초기에 잘못된 설계에 의해서 전부 재정의가 가능하게 설계 되었다.
하위 호환성을 포기 할수 없는 클래스이기에 클라이언트가 상속을 받을 경우 제대로된 인자가 넘어 왔는지
확인해야 하며 이에 대한 방어 복사를 구현해야 한다.
import java.math.BigInteger;
public class NewBigInteger extends BigInteger{
/**
*
*/
private static final long serialVersionUID = 1L;
public NewBigInteger(String s) {
super(s);
}
public void foo(BigInteger b) {
if (b.getClass() != BigInteger.class) {
b = new BigInteger(b.toByteArray());
System.out.println("BigInteger value : " + b);
}else{
System.out.println("out of 안중");
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
BigInteger bint = new BigInteger("123456789");
NewBigInteger nb= new NewBigInteger("987654321");
nb.foo(bint);
nb.foo(nb);
}
}
우리는 이전에 헤드퍼스트디자인 패턴을 통해서 확인한 이야기 이다.
다른 설명 보다는 책에서 나온 코드를 중심으로 살펴 보자.
| {code:java} import java.util.Arrays; import java.util.Collection; import java.util.HashSet; |
public class InstrumentedHashSet extends HashSet {
private int addCount = 0;
public InstrumentedHashSet() {
// TODO Auto-generated constructor stub
}
public InstrumentedHashSet(Collection arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public InstrumentedHashSet(int arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public InstrumentedHashSet(int arg0, float arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}
public boolean add(Object o) {
System.out.println("add() 메소드 호출");
addCount++;
return super.add(o);
}
public boolean addAll(Collection c) {
System.out.println("addAll() 메소드 호출");
addCount += c.size();
return super.addAll©;
}
public int getAddCount() {
return addCount;
}
}
| {code:java}
import java.util.Arrays;
import junit.framework.TestCase;
public class InstrumentHashSetTest extends TestCase {
private InstrumentedHashSet ih;
@Override
protected void setUp() throws Exception {
this.ih = new InstrumentedHashSet();
}
//천번째 테스트 케이스로 정해진 HashSet에 문자열을 넣어서 Size를 받아온다.
//3개의 문자열을 넣어서 3개의 사이즈를 기대하지만 잘못된 결과가 나오는걸 확인 한다.
public void testAddAll() {
ih.addAll(Arrays.asList(new String[] {"Snap","Crackle","Pop"}));
assertEquals(6, ih.getAddCount());
assertEquals(3, ih.getAddCount());
ih.clear();
}
}
|
위 코드를 실행해 보면 처음 상속을 통하여 얻고자 한 결과가 제대로 나오지 않는걸 확인할수 있다.
그리고 이 이유에 대한 해결책으로
첫번째 allAll메소드를 재정의 하지 않고 사용한다고 나와있는데(테스트 해보자)
이는 전적으로 부모클래스에 의존한 구현 방식이다. 제공되던 클래스가 수정이 없을때는 상관이 없지만
변경 될경우 상속받은 클래스까지 모두 구현을 변경하라는 사실은 어불성설이요 너무 무책임한 태도다.
두번째 방식인 addAll 메소드에서 인자로 받은 Collection 을 하나씩 순회 하면서 add 메소드를 호출하게
재정의 하는 방법인데.. 이럴거면 머하로 상속 받았는가? 부모로 부터 상속받은 메소드 전부를 재정의 할것인가?
이는 엄청난 자원 낭비임이 틀림없다.
물론 지금 까지 설명한 내용과 책(p108)에 제시된 몇가지 예제들은 너무 비관적으로만 보는게 아닐까 하고 책을 읽으면서
생각하기도 했다.(애초에 상속자체를 만들지 말던지...그렇다 되도록이면 상속을 쓰지 말라는 이야기 이다.)
부모 클래스에 추가된 메소드로 인하여 자식클래스에서는 보안 및 안정성을 확신할수 없다.
(헤드퍼스트의 첫장인 오리를 기억하라 단지 부모에 날수있다 라는 행동을 부여했더니.. 고무오리도 날게 되었다.)
또 언제 무슨 문제가 발생 할지 알수 없기 때문에 우리는 상속을 지양 하고 컴포지션을 통하여 문제에 접근을 해야 한다.
위 클래스를 컴포지션을 이용해서 처리해 보자
| {code:java} import java.util.Collection; import java.util.Iterator; import java.util.Set; |
public class InstrumentSet implements Set{
private final Set s;
private int addCount = 0;
public InstrumentSet(Set s) {
this.s = s;
}
public boolean add(Object arg0) {
addCount ++;
System.out.println("add Call()");
return s.add(arg0);
}
public boolean addAll(Collection arg0) {
addCount = arg0.size();
System.out.println("addAll Call()");
return s.add(arg0);
}
public int getAddCount() {
return addCount;
}
public void clear() {
// TODO Auto-generated method stub
s.clear();
}
public boolean contains(Object arg0) {
// TODO Auto-generated method stub
return s.contains(arg0);
}
public boolean containsAll(Collection arg0) {
// TODO Auto-generated method stub
return s.contains(arg0);
}
public boolean isEmpty() {
// TODO Auto-generated method stub
return s.isEmpty();
}
public Iterator iterator() {
// TODO Auto-generated method stub
return s.iterator();
}
public boolean remove(Object arg0) {
// TODO Auto-generated method stub
return s.remove(arg0);
}
public boolean removeAll(Collection arg0) {
// TODO Auto-generated method stub
return s.removeAll(arg0);
}
public boolean retainAll(Collection arg0) {
// TODO Auto-generated method stub
return s.retainAll(arg0);
}
public int size() {
// TODO Auto-generated method stub
return s.size();
}
public Object[] toArray() {
// TODO Auto-generated method stub
return s.toArray();
}
public Object[] toArray(Object[] arg0) {
// TODO Auto-generated method stub
return s.toArray(arg0);
}
}
| {code:java}
import java.util.Arrays;
import java.util.HashSet;
import junit.framework.TestCase;
public class InstrumentSetTest extends TestCase {
public void testAddAll() {
InstrumentSet s = new InstrumentSet(new HashSet());
s.addAll(Arrays.asList(new String[] {"Snap","Crackle","Pop"}));
assertEquals(3, s.getAddCount());
}
}
|
페이지 113쪽과 116쪽에 걸쳐서 문서를 만들때 어떻게 만들어야 하며 왜 만들어야 하는지 설명 한다.
위 사항을 위반할 경우 생기는 문제 코드를 살펴보자
| {code:java} /** * @author Administrator * */ public class Super { public Super() { // TODO Auto-generated constructor stub System.out.println("Super Class Construct Call()"); m(); } |
public void m() {
System.out.println("Super Class M Method Call()");
}
}
|{code:java}
/**
* @author Administrator
*
*/
public class Sub extends Super {
private final Date date;
/**
*
*/
public Sub() {
System.out.println("Sub Class Construct Call()");
date = new Date();
}
public void m() {
System.out.println("Sub Class M Method Call()");
System.out.println(date);
}
/**
* @param args
*/
public static void main(String[] args) {
// Sub s = new Sub();
// s.m();
Super s = new Sub();
s.m();
}
}
|
예상과 다른 결과를 보여주는 화면을 보게 될것이다.
책 에서는(p.118) final 필드의 상태가 2개가 될수 있다는 것에 주목 하라고 했는데... 이게 무슨 소리냐 하면은
메인 메소드 실행과 함게 만들어진 1개의 Thread 안에서 그려지는 과정을 머리속으로 그리면 답이 나온다.
이는 발표를 하면서 보여주도록 하겠다.
여지까지 나온 내용을 종합 하면 다음과 같은 결론이 가능하다.
여지껏 살펴본 바에 의하면 이미 존재하는 클래스가 새로운 인터페이스를 구현하도록 고치는 것은 어려운 일이 아니다.
But 이미 존재 하는 클래스가 새로운 추상 쿨래스를 상속 받도록 고치는 것은 거의 불가능 한 일이다.
(그래서 되도록 이면 쓰지 말고.. 만약에 쓰게 되면 각오를 하고 쓰라는 거다.
물론 인터페이스 또한 만들어 져서 공표 되고 불특정 다수의 클래스가 이를 구현한 시점에서는 수정은 불가 하다.)
데코레이터 패턴에서 보아온 거처럼 인터페이스나 혹은 참조 변수를 멤버로 가진 클래스를 구현 하면 안전하고 강력하게 클래스에 새로운 기능을 더할수 있다.
다음 코드를 통하여 인터페이스를 이용한 클래스 설계를 보자. 이코드는 크게 별다른 의미를 지닌것은 아니지만 완전 하게 동작이 가능한 List형태를 가진
정수 배열을 생성하는 역활을 하는 클래스 이다. (팩토리 메소드 패턴을 사용 하였다.)
import java.util.AbstractList;
import java.util.Collections;
import java.util.List;
public class IntList {
static List intArrayAsList(final int[] a) {
if (a == null)
throw new NullPointerException();
return new AbstractList() {
public Object get(int i) {
return new Integer(a[i]);
}
public int size() {
return a.length;
}
public Object set(int i, Object o) {
int oldVal = a[i];
a[i] = ((Integer) o).intValue();
return new Integer(oldVal);
}
};
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
int n = Integer.parseInt("20");
int a[] = new int[n];
for (int i = 0; i < n; i++) {
a[i] = i;
}
List l = intArrayAsList(a);
Collections.shuffle(l);
System.out.println("ListElements : " + l);
Collections.sort(l);
System.out.println("ListElements : " + l);
}
}
코드는 1차원 정수배열을 20개 생성 해서 List타입의로 만든후 Collections 클래스를 통해 섞고 또는 정렬 하는 결과를 보여준다.
이 항목은 딱히 설명한 내용이 없다. 요약 하자면 상수 집합을 제공하기 위해 굳이 인터페이스를 쓰지 말라
중첩 클래스는 다른 클래스 내부에 정의한 클래스 이고
1. 정정멤버 클랫,
2. 비정적 멤버 클래스
3. 익명 클래스
4. 지역 클래스
로 나뉘어 지는데 이중 정정 멤버 클래스를 제외한 나머지 클래스는 내부 클래스 라고 불리운다.
멤버클래스를 정의 할때 감싼 클래스의 인스턴스에 접근할 필요가 없다면 정적 멤버클래스로 만들고
참조가 필요 하다면 비정적 멤버 클래스로 만든다.(당연하다 비정적 멤버 클래스는 감싼 클래스를 생성 하지 않으면 생성이 안되니까.)
이후 여러 가지 설명이 이어지는데 실제로 기억할 내용은 다음만 보면 된다.
한 메소드 에서만 쓰는게 아니거나 너무 길어서 한 메소드에 넣기 힘들다면 멤버 클래스로 만든다.
자신을 감싼 인스턴스의 참조가 필요한 경우에만 비 정적 멤버 클래스로 만들고 나머지 경우에는 모두 비정적 멤버 클래스로 만든다.
예제로 제공된 18.1 예제는 왜 이렇게 작성 되어야 했는지 대단히 의문스럽다. 저자 본인이 14 15 16 항목에 걸쳐 설명한 내용을 스스로 위반한 예제이다.
차라리 잘 정의된 HashSet 클래스를 가져다가 사용하면 된다. (HashSet --> 이건 대단히 죽여준다..Set 인터페이스를 구현 한거중에 황태자로 지칭해도 좋다.)
발표하면서 작성하자. 힘들다.