아이템 15 - 클래스와 멤버의 접근 권한을 최소화하라
•
어설프게 설계된 컴포넌트와 잘 설계된 컴포넌트의 가장 큰 차이는 바로 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐다.
•
잘 설계된 컴포넌트는 내부 규현을 완벽히 숨겨, 구현과 API를 깔끔히 분리한다.
•
오직 API를 통해서만 다른 컴포넌트와 소통하며 서로의 내부 동작 방식에는 전혀 개의치 않는다.
정보 은닉(캡슐화)
정보 은닉, 캡슐화라 불리는 개념은 소프트웨어 설계의 근간이 되는 원리다.
장점
•
시스템 개발 속도를 높인다.
◦
여러 컴포넌트를 병렬로 개발할 수 있기 때문
•
시스템 관리 비용을 낮춘다.
◦
각 컴포넌트를 더 빨리 파악하여 디버깅할 수 있고, 다른 컴포넌트로 교체하는 부담도 적기 때문
•
정보 은닉 자체가 성능을 높여주지는 않지만, 성능최적화에 도움을 준다.
◦
완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정한 후 다른 컴포넌트에 영향을 주지않고 최적화가 가능함
•
소프트웨어 재사용성을 높인다.
◦
외부에 의존하지않고 독자적으로 동작할 수 있는 컴포넌트는 낯선환경에서 유용하게 쓰일 가능성이 큼
•
큰 시스템 제작 난이도를 낮춰준다.
◦
컴포넌트가 완성되지 않은 상태에서도 개별 컴포넌트 동작을 검증할 수 있기 때문
원칙
•
모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.
◦
달리 말하면, 소프트웨어가 올바로 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다는 것
•
패키지 외부에서 쓸 이야가 없다면 package-private로 선언하자.
•
권한을 풀어주는일이 자주 발생한다면 컴포넌트를 더 분해해야 하는게 아닌지 고민해보자
접근제어자
•
private : 멤버를 선언한 톱레벨 클래스만 접근가능
•
package-private : 멤버가 소속된 패키지 안의 모든 클래스에서 접근 가능 (기본 접근제한자)
•
protected : package-private 접근 범위를 포함하며, 하위 클래스에서 접근 가능
•
public : 모든 곳에서 접근 가능
public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.
•
필드가 가변 객체를 참조하거나, final이 아닌 인스턴스 필드를 public으로 선언하면 그 필드에 담을 수 있는 값을 제한할 힘을 잃게 된다.
•
public 가변 필드를 갖는 클래스는 일반적으로 Thread safety 하지 않다.
◦
내부 구현을 바꾸고싶어도 그 public 필드를 없애는 방식으로 리팩터링 할 수 없게된다.
•
길이가 0이 아닌 배열은 모두 변경이 가능하다.
◦
클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안된다.
모듈 시스템
•
자바 9부터 도입된 개념
•
모듈은 패키지들의 묶음이다.
•
모듈은 자신이 속하는 패키지 중 공개(export)할 것들을 선언한다.
◦
protected, public 멤버라도 해당 패키지를 공개하지 않았다면 모듈 외부에서는 접근할 수 없다.
•
모듈은 여러 자바 프로그래밍에 영향을 준다
•
장점을 제대로 누리려면 해야 할 일이 많다.
◦
아직 모듈 개념이 널리 받아들여질지 예측하기는 아직 이른 감이 있다.
◦
그러니 꼭 필요한 경우가 아니라면 당분간은 사용하지 않는 게 좋을 것 같다.
아이템 16 - public 클래스에선 public 필드가 아닌 접근자 메서드를 사용하라
•
클래스 내부의 데이터 필드는 모두 private접근 제한자로 설정하고, 해당 변수에 접근 가능한 Getter 메서드를 제공한다.
•
public 클래스가 필드를 모두 공개하면 내부 표현 방식을 마음대로 바꿀 수 없게 된다.
•
package-private 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 하등의 문제가 없다.
•
필드가 public이라도 final하면 되는게 아닐까?
◦
필드를 읽을 때 부수 작업을 수행할 수 없기 때문에 단점이 있다.
•
public 클래스를 가변필드를 노출하지 말아야하고, 불변이라도 되도록 노출은 피하자.
•
package-private 클래스나 private 중첩 클래스에서는 종종 필드를 노출하는편이 나을 때도 있다.
아이템 17 - 변경 가능성을 최소화하라
•
불변 클래스란 간단히 말해 그 인스턴스의 내부 값을 수정할 수 없는 클래스다.
•
불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.
객체를 불변으로 만들기 위한 다섯 가지 규칙
1.
객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
2.
클래스를 확장할 수 없도록 한다.
a.
하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하게 만드는 상태를 막아준다.
b.
상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것
3.
모든 필드를 final로 선언한다.
a.
시스템이 강제하는 수단을 이용해 설계짜의 의도를 명확히 드러내는 방법이다.
b.
새로 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하는 데도 필요하다.
c.
하지만 final로 선언된 변수에 변경 가능한 객체가 지정되어 있다면 해당 변수에 들어있는 객체의 값을 사용하려고 하는 부분을 모두 동기화 시켜야 한다.
4.
모든 필드를 private으로 선언한다.
a.
필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.
5.
자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
a.
클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다.
b.
이런 필드는 절대 클라이언트가 제공한 객체 참조를 가리키게 해서는 안되며, 접근자 메서드가 그 필드를 그대로 반환해서도 안된다.
c.
생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행하라.
불변객체를 사용하면 얻을 수 있는 장점
불변 객체는 단순하다.
•
불변 객체는 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다.
•
모든 생성자가 클래스 불변식을 보장한다면 그 클래스를 사용하는 프로그래머가 다른 노력을 들이지 않더라도 영원히 불변으로 남는다.
•
반면 가변 객체는 임의의 복잡한 상태에 놓일 수 있기 때문에 가변 객체는 믿고 사용하기 어렵다.
불변 객체는 근본적으로 Thread safety하여 따로 동기화할 필요가 없다.
•
여러 스레드가 동시에 사용해도 절대 훼손되지 않는다.
•
클래스를 thread safe하게 만드는 가장 쉬운 방법이기도 하다.
불변객체는 안심하고 공유할 수 있다.
•
불변 인스턴스를 최대한 재활용 하기를 권한다.
•
방어적 복사도 필요없다.
◦
아무리 복사해봐야 원본과 똑같으니 복사 자체가 의미가 없다.
◦
그러니 불변 클래스는 clone 메서드나 복사 생성자를 제공하지 않는 게 좋다.
◦
String 클래스의 복사생성자는 되도록 사용하지 말자
불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객채끼리는 내부 데이터를 공유할 수 있다.
•
BigInteger 클래스를 살펴보면 부호(sign)와 크기(magnitude)를 각각의 필드로 표현한다.
•
negate 메서드는 크기는 같고 부호만 반대인 새로운 BigInteger를 생성하는데, 배열은 가변이지만 복사하지 않고 원본 인스턴스와 공유해도 됨
객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
•
값이 바뀌지 않는 구성요소들로 이뤄진 객체라면 그 구조가 복잡해도 불변식 유지가 수월하다.
•
구조가 복잡해도 불변식 유지을 유자하기가 수월하다.
불변객체는 그 자체로 실패 원자성을 제공한다.
•
상태가 절대 변하지 않으니 잠깐이라도 불일치 상태에 빠질 가능성이 없다.
불변객체의 단점
값이 다르면 반드시 독립된 객체로 만들어야 한다.
•
값의 가짓수가 많다면 이들을 모두 만드는 데 큰 비용을 치러야 한다.
•
원하는 객체를 완성하기까지의 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려져야 한다면 성능 문제가 더 불거진다.
성능 문제에 대처하는 방법
•
다단계 연산들을 예측하여 기본 기능으로 제공하는 방법
◦
다단계 연산 속도를 높여주는 모듈을 package-private로 만들어두기
•
예측이 어려울때 사용할 수 있는 방법
◦
클래스를 public으로 제공하자
◦
예를 들어 String과 가변 동반 클래스 StringBuilder
불변 클래스를 설계하는 방법
•
final을 사용해 상속하지 못한게 만든다.
•
생성자를 외부에 공개하지 않고, 정적 팩터리를 통해서 제공하는 방법
BigInteger, BigDecimal 사용시 주의점
•
BigInteger, BigDecimal을 설계할 당시 불변 객체가 사실상 final이어야 한다는 생각이 널리 퍼지지 않았다.
•
때문에 이 두 클래스의 메서드들은 모두 재정의할 수 있게 설계되었고, 호환성 문제로 현재까지 문제가 해결되지 못하고 있다.
•
인수로 받은 객체가 '진짜'인지 확인하고, 신뢰할 수 없는 하위 클래스의 인스턴스라고 확인되면, 이 인수들은 가변으로 가정하고 방어적으로 복사해서 사용해야한다.
성능을 위한 기준 완화
•
어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다.
•
계산 비용이 큰 값을 나중에 계산하여 final이 아닌 필드에 계산해두기도 한다.
◦
hashCode를 계산해 캐시해둘 수 있다!
아이템 18 - 상속보다는 컴포지션을 사용하라
•
상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다.
•
잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다.
이상적인 상속 설계
•
상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안정한 방법
◦
일반적인 구체 클래스를 채키지 경계를 넘어, 다른 패키지의 구체 클래스를 상속하는 일은 위험하다.
•
확장할 목적으로 설계되었고 문서화도 잘 된 클래스도 마찬가지로 안전하다.
상속의 단점
메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
•
상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
•
상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오동작 할 수 있다.
•
때문에 상위 클래스 설계자가 확장을 충분히 고려하고 문서화도 제대로 해두지 않으면 하위 클래스는 상위 클래스의 변화에 발맞춰 수정되야 한다.
상속의 단점을 극복하는 방법 (컴포지션)
•
기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 한다.
•
새 클래스에 대응하는 메서드를 호출해 그 결과를 반환한다.
◦
이 방식을 forwarding이라 하며, 새 클래스의 메서드들을 forwarding method라 부른다.
•
결과적으로 새 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어난다.
◦
기존 클래스에 새로운 메서드가 추가되더라도 전형 영향받지 않는다.
래퍼클래스
•
다른 인스턴스를 감사꼬 있다는 뜻에서 감싼 클래스를 래퍼 클래스라고 부른다. (Integer는 int를 감싸고있음)
•
이러한 형태를 데코레이터 패턴이라 부른다.
•
콜백 프레임워크와는 어울리지 않는다.
아이템 19 - 상속을 고려해 설계하고 문서화하라, 그러지 않았다면 상속을 금지하라
•
앞서 문서화해놓지 않은 ‘외부’ 클래스를 상속할 때의 위험성을 경고했다.
•
프로그래머의 통제권 밖에 있어 언제 변경될지 모르는 클래스를 상속하는것은 위험하다.
상속을 고려한 설계와 문서화
상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야한다.
•
호출될 수 있는 메서드가 재정의 가능 메서드라면 그 사실을 호출하는 메서드의 API 설명에 적시해야 한다.
◦
재정의할 수 있는 메서드들을 호출할 수 있는 모든 상황을 문서로 남겨야 한다.
•
@implSpec 태그는 자바 8에서 처음 도입되어 자바 9부터 본격적으로 사용되기 기작했다.
◦
이는 내부 매커니즘에 대한 설명을 적시해야한다.
내부 매커니즘을 문서로 남기는 것만이 상속을 위한 설계의 전부인가?
•
효율적인 하위 클래스를 큰 어려운 없이 만들 수 있게 하는 방법
◦
클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별해 protected 메서드 형태로 공개하는 것
상속용 클래스를 시험하는 방법
•
상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다.
•
상속용으로 설계한 클래스는 배포 전 반드시 하위 클래스를 만들어 검증해야 한다.
상속을 허용하는 클래스의 제약
•
상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
•
clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
•
상속용으로 설계하지 않은 클래스는 상속을 금지한다.
아이템 20 - 추상 클래스보다는 인터페이스를 우선하라
추상 클래스와 인터페이스의 차이점
•
추상 클래스가 정의한 타입을 구현한 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
•
인터페이스는 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 어떤 클래스를 상속했든 같은 타입으로 취급한다.
인터페이스의 장점
•
기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.
◦
기존 클래스 위에 새로운 추상 클래스를 끼워넣기는 어려운게 일반적이다.
◦
인터페이스는 필요에 따라 implements로 다중 구현이 가능하다.
•
믹스인(mixin) 정의에 안성맞춤이다.
◦
믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 주된 타입 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
◦
대상 타입의 주된 기능에 선택적 기능을 혼합(mixed in)한다는 의미이다.
◦
Comparable이 대표적인 믹스인 인터페이스이다.
•
계층구조가 없는 타입 프레임워크를 만들 수 있다.
◦
거대한 클래스 계층구조에는 공통기능을 정의해놓은 타입이 없으니, 자칫 매개변수 타입만 다른 메서드들을 수없이 많이 가진 거대한 클래스를 낳을 수 있다.
◦
인터페이스끼리 상속하여 새로운 인터페이스를 정의해 문제를 해결할 수 있다.
▪
유연성이 매우 뛰어나다.
•
디폴트 메서드를 제공하여 반복되는 코드의 작성을 줄일 수 있다.
◦
다만 디폴드 메서드를 상속하려는 사람을 위해 @implSpec태그를 붙여 문서화 해야한다.
•
인터페이스와 추상 골격 구현(skeleton implementation) 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취할 수 있다.
◦
인터페이스로는 타입을 정의하고, 필요하면 디폴트 메서드 몇 개도 함께 제공한다.
◦
단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는 데 필요한 일 대부분이 완료된다.
◦
바로 템플릿 메서드 패턴이다.
골격 구현 클래스
•
추상 클래스처럼 구현을 도와주는 동시에, 추상 클래스로 타입을 정의할 때 따라오는 심각한 제약에서는 자유롭다.
골격 구현 클래스를 우회적으로 이용하는 방법
•
인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의한다.
•
각 메서드 호출을 내부 클래스의 인스턴스에 전달한다.
•
일정의 래퍼 클래스오 비슷한 방식으로 이용할 수 있다.
•
이를 시뮬레이트한 다중 상속(simulated multiple inheritance)이라 하며, 다중 상속의 많은 장점을 제공하는 동시에 단점을 피하게 해준다.
주의점
•
equals와 hashCode 같은 Object의 메서드는 디폴트 메서드로 제공하면 안 된다.
단순 구현(simple implementation)
•
골격 구현의 작은 변종이다.
•
대표적으로 Abstract.Map.SimpleEntry가 좋은 예시다.
•
상속을 위해 인터페이스를 구현한 것이지만, 추상 클래스가 아니란 점이 다르다.
아이템 21 - 인터페이스는 구현하는 쪽을 생각해 설계하라
•
자바 8 전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.
•
자바 8에 와서 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.
◦
이는 주로 람다를 활용하기 위해서다.
◦
자바 라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분 상황에서 잘 작동한다.
◦
하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.
Collection 인터페이스의 removeIf 메서드
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean result = false;
for(Iterator<E> it = iterator(); it.hasNext(); ) {
if(filter.test(it.next())) {
it.remove();
result = true;
}
}
return result;
}
Java
복사
•
이 코드 보다 더 범용적으로 구현하기 어렵겠지만, 그렇다고 해서 현존하는 모든 Collection 구현체와 잘 어우러지는 것은 아니다.
•
대표적으로 SynchronizedCollection 으로 모든 메서드에 주어진 락을 객체로 동기화 한 후 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스이다.
◦
removeIf 는 락에 대해서 아무것도 모르므로 락 객체를 사용할 수 없고, 예기치 못한 결과로 이어질 수 있다.
결론
•
디폴트 메서드는 기존 구현체에 런타임 오류를 일으킬 수 있다.
◦
디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니라면 피해야 한다.
•
때문에 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 한다.
•
인터페이스를 릴리즈한 후라도 결함을 수정하는 게 가능한 경우도 있겠지만, 절대 그 가능성에 기대서는 안된다.
아이템 22 - 인터페이스는 타입을 정의하는 용도로만 사용하라
•
인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.
◦
인터페이스는 오직 이 용도로만 사용해야 한다.
상수 인터페이스 안티패턴
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;
}
Java
복사
사용 금지
•
상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예시
•
클래스 내부에서 사용하는 상수는 외부 인터페이스가 아닌 내부 구현에 해당함
•
상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스의 API로 노출하는 행위다.
•
클래스가 어떤 상수 인터페이스를 사용하든 사용자에게는 아무런 의미가 없다.
•
사용자에게 혼란을 주기도 하고, 클라이언트 코드가 내부 구현에 해당하는 이 상수들에 종속되게 한다.
상수 유틸리티 클래스
public class PhysicalConstants {
// 아보가드로 수 (1/몰)
public static double AVOGADROS_NUMBER = 6.022_140_857e23;
// 볼츠만 상수 (J/K)
public static double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
// 전자 질량 (kg)
public static double ELECTRON_MASS = 9.109_383_56e-31;
}
Java
복사
•
해당 방법이 더 나은 방법이다.
•
상수를 빈번히 사용한다면 정적 임포트 하여 클래스 이름은 생략할 수 있다.
아이템 23 - 태그 달린 클래스보다는 클래스 계층구조를 활용하라
태그 달린 클래스
static class Figure {
enum Shape { RECTANGLE, CIRCLE };
// 태그 필드 - 현재 모양을 나타낸다.
final Shape shape;
// 다음 필드들은 모양이 사각형(RECTANGLE)일 때만 쓰인다.
double length;
double width;
// 다음 필드는 모양이 원(CIRCLE)일 때만 쓰인다.
double radius;
// 원용 생성자
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// 사각형용 생성자
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch(shape) {
case RECTANGLE:
return this.length * this.width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError(shape);
}
}
}
Java
복사
•
태그 달린 클래스는 단점이 한가득 하다.
•
열거 타입 선언, 태그 필드, switch 문 등 쓸떼없이 코드가 많다.
•
다른 의미를 위한 코드를 클래스 내부에 함께 저장해야 하니 메모리도 많이 사용하게 된다.
•
필드를 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다.
•
또 다른 의미를 추가하려면 코드를 수정해야하고, 하나라도 빠뜨리면 런타임에 문제가 불거져 나올 것이다.
•
태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.
•
태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다.
클래스 계층구조
abstract static class Figure {
abstract double area();
}
static class Circle extends Figure {
final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * (radius * radius);
}
}
static class Rectangle extends Figure {
final double width;
final double length;
public Rectangle(double width, double length) {
this.width = width;
this.length = length;
}
@Override
double area() {
return width * length;
}
}
Java
복사
•
계층구조의 루트가 될 추상 클래스를 정의한다.
•
태그 값에 따라 동작이 달리즌 메서드들을 루트가 클래스의 추상 메서드로 선언한다.
◦
area()메서드가 여기에 해당한다.
•
태그 값에 상관없이 동작이 일정한 메서드들을 모두 루트 클래스에 일반 메서드로 추가한다.
•
공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올린다.
•
결과적으로 루트 클래스에는 추상 메서드인 area() 하나만 남게 된다.
장점
•
쓸떼없는 코드가 모두 사라져 간결하고 명확해졌다.
•
실수로 빼먹은 case문 때문에 런타임 오류가 발생할 일도 없다.
•
루트 클래스의 코드를 건들이지 않고도 다른 프로그래머들이 독립적으로 계층구조를 확장하고 함께 사용할 수 있다.
•
타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일타임 타입 검사 능력을 높여준다.
결론
•
새 클래스를 작성하는데 태그 필드가 등장하면, 태그를 없애고 계층구조로 대체하는 방법을 생각해보자.
•
기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩터링 하는 것을 고려해보자.
아이템 24 - 멤버 클래스는 되도록 static으로 만들라
•
중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스를 말한다.
•
자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.
중첩 클래스의 종류
•
정적 멤버 클래스
•
(비정적) 멤버 클래스
•
익명 클래스
•
지역 클래스
정적 멤버 클래스를 제외한 나머지는 내부 클래스(inner class)에 해당한다.
정적 멤버 클래스에 대해 알아보기
•
정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할수 있다는 점만 제외하고는 일반 클래스와 동일
•
정적 멤버 클래스는 다른 정적 멤버와 똑같은 접근 규칙을 적용 받음
◦
private로 선언하면 바깥 클래스에서만 접근할 수 있는 식
비정적 멤버 클래스에 대해 알아보기
•
정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어 있고 없고 차이뿐이지만, 의미상 차이는 의외로 꽤 크다.
•
비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결 됨
◦
때문에 this를 이용해 바깥 인스턴스의 메서드를 호출하거나 참조를 가져올 수 있음
•
비정적 멤버 클래스는 바깥 인스터스 없이는 생성할 수 없기 때문
•
이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸린다.
•
어댑터를 정의할 때 자주 사용된다.
멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.
•
암묵적 참조 때문에 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다.
익명 클래스에 대해 알아보기
•
이름이 없는 클래스
•
바깥 클래스의 멤버도 아니다.
•
선언한 지점에서만 인스턴스를 만들 수 있고, instanceof검사나 클래스의 이름이 필요한 작업을 수행할 수 없음
•
익명 클래스를 사용하는 클라이언트는 그 익명 클래스가 상위 타입에 상속한 멤버 외에는 호출 불가
•
표현식 중간에 등장하므로 짧지 않으면 가독성이 떨어짐
•
람다 지원 전 즉석에서 작은 함수 객체나 처리 객체를 만드는데 주로 사용함
지역 클래스에 대해 알아보기
•
네 가지 클래스 중 가장 드물게 사용됨
•
지역변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있다.
•
유효 범위도 지역 변수와 같다.
•
익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있고, 정적 멤버는 가질 수 없으며 가독성을 위해 짧게 작성해야 함
아이템 25 - 톱레벨 클래스는 한 파일에 하나만 담으라
•
소스파일 하나에 톱레벨 클래스를 여러 개 선언하더라도 자바 컴파일러는 불평하지 않는다.
•
하지만 아무런 득이 없을 뿐더러 심각한 위험을 감수해야하는 행위다.
두 클래스가 한 파일에 정의되어 있다면
class Utensil {
static final String NAME = "pan";
}
class Dessert {
static final String NAME = "cake";
}
Java
복사
Utiensil.java
class Utensil {
static final String NAME = "pan";
}
class Dessert {
static final String NAME = "cake";
}
Java
복사
Dessert.java
public class Main {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Desert.NAME);
}
}
Java
복사
Main.java
•
javac 명령 순서에 따라 컴파일 오류가 나거나 나지 않을 수 있음
◦
javac Main.java Dessert.java - 에러
◦
javac Main.java, javac Main.java Utensil.java - "pancake" 출력
◦
javac Dessert.java Main.java - "potpie" 출력
•
Main.java가 먼저 인수에 들어왔을 때
◦
Main.java를 실행시키며, Utensil.NAME을 만나고, Utensil.java 파일을 찾아서 클래스를 로드하려한다.
◦
Dessert.java를 추가로 로드할 경우 클래스가 중복으로 선언되었다고 알린다.
•
Dessert.java가 먼저 인수에 들어왔을 때
◦
Utensil 클래스와 Dessert 클래스의 정의를 불러와 놓는다.
해결책
•
서로 다른 소스파일로 분리한다.