4장 - 클래스와 인터페이스
- Created
- 2022년 9월 3일 PM 8:38
아이템 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 - 변경 가능성을 최소화하라#
- 불변 클래스란 간단히 말해 그 인스턴스의 내부 값을 수정할 수 없는 클래스다.
- 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.
객체를 불변으로 만들기 위한 다섯 가지 규칙#
- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다.
- 하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하게 만드는 상태를 막아준다.
- 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것
- 모든 필드를 final로 선언한다.
- 시스템이 강제하는 수단을 이용해 설계짜의 의도를 명확히 드러내는 방법이다.
- 새로 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하는 데도 필요하다.
- 하지만 final로 선언된 변수에 변경 가능한 객체가 지정되어 있다면 해당 변수에 들어있는 객체의 값을 사용하려고 하는 부분을 모두 동기화 시켜야 한다.
- 모든 필드를 private으로 선언한다.
- 필드가 참조하는 가변 객체를 클라이언트에서 직접 접근해 수정하는 일을 막아준다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
- 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다.
- 이런 필드는 절대 클라이언트가 제공한 객체 참조를 가리키게 해서는 안되며, 접근자 메서드가 그 필드를 그대로 반환해서도 안된다.
- 생성자, 접근자, 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 메서드#
java
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;
}- 이 코드 보다 더 범용적으로 구현하기 어렵겠지만, 그렇다고 해서 현존하는 모든 Collection 구현체와 잘 어우러지는 것은 아니다.
- 대표적으로
SynchronizedCollection으로 모든 메서드에 주어진 락을 객체로 동기화 한 후 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스이다.removeIf는 락에 대해서 아무것도 모르므로 락 객체를 사용할 수 없고, 예기치 못한 결과로 이어질 수 있다.
결론#
- 디폴트 메서드는 기존 구현체에 런타임 오류를 일으킬 수 있다.
- 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니라면 피해야 한다.
- 때문에 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 한다.
- 인터페이스를 릴리즈한 후라도 결함을 수정하는 게 가능한 경우도 있겠지만, 절대 그 가능성에 기대서는 안된다.
아이템 22 - 인터페이스는 타입을 정의하는 용도로만 사용하라#
- 인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.
- 인터페이스는 오직 이 용도로만 사용해야 한다.
상수 인터페이스 안티패턴#
java
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;
}- 상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예시
- 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아닌 내부 구현에 해당함
- 상수 인터페이스를 구현하는 것은 이 내부 구현을 클래스의 API로 노출하는 행위다.
- 클래스가 어떤 상수 인터페이스를 사용하든 사용자에게는 아무런 의미가 없다.
- 사용자에게 혼란을 주기도 하고, 클라이언트 코드가 내부 구현에 해당하는 이 상수들에 종속되게 한다.
상수 유틸리티 클래스#
java
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;
}- 해당 방법이 더 나은 방법이다.
- 상수를 빈번히 사용한다면 정적 임포트 하여 클래스 이름은 생략할 수 있다.
아이템 23 - 태그 달린 클래스보다는 클래스 계층구조를 활용하라#
태그 달린 클래스#
java
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);
}
}
}- 태그 달린 클래스는 단점이 한가득 하다.
- 열거 타입 선언, 태그 필드,
switch문 등 쓸떼없이 코드가 많다. - 다른 의미를 위한 코드를 클래스 내부에 함께 저장해야 하니 메모리도 많이 사용하게 된다.
- 필드를
final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다. - 또 다른 의미를 추가하려면 코드를 수정해야하고, 하나라도 빠뜨리면 런타임에 문제가 불거져 나올 것이다.
- 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.
- 태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐이다.
클래스 계층구조#
java
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;
}
}- 계층구조의 루트가 될 추상 클래스를 정의한다.
- 태그 값에 따라 동작이 달리즌 메서드들을 루트가 클래스의 추상 메서드로 선언한다.
area()메서드가 여기에 해당한다.
- 태그 값에 상관없이 동작이 일정한 메서드들을 모두 루트 클래스에 일반 메서드로 추가한다.
- 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올린다.
- 결과적으로 루트 클래스에는 추상 메서드인
area()하나만 남게 된다.
장점#
- 쓸떼없는 코드가 모두 사라져 간결하고 명확해졌다.
- 실수로 빼먹은 case문 때문에 런타임 오류가 발생할 일도 없다.
- 루트 클래스의 코드를 건들이지 않고도 다른 프로그래머들이 독립적으로 계층구조를 확장하고 함께 사용할 수 있다.
- 타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일타임 타입 검사 능력을 높여준다.
결론#
- 새 클래스를 작성하는데 태그 필드가 등장하면, 태그를 없애고 계층구조로 대체하는 방법을 생각해보자.
- 기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩터링 하는 것을 고려해보자.
아이템 24 - 멤버 클래스는 되도록 static으로 만들라#
- 중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스를 말한다.
- 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.
중첩 클래스의 종류#
- 정적 멤버 클래스
- (비정적) 멤버 클래스
- 익명 클래스
- 지역 클래스
정적 멤버 클래스를 제외한 나머지는 내부 클래스(inner class)에 해당한다.
정적 멤버 클래스에 대해 알아보기#
- 정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의
private멤버에도 접근할수 있다는 점만 제외하고는 일반 클래스와 동일 - 정적 멤버 클래스는 다른 정적 멤버와 똑같은 접근 규칙을 적용 받음
private로 선언하면 바깥 클래스에서만 접근할 수 있는 식
비정적 멤버 클래스에 대해 알아보기#
- 정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지
static이 붙어 있고 없고 차이뿐이지만, 의미상 차이는 의외로 꽤 크다. - 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결 됨
- 때문에
this를 이용해 바깥 인스턴스의 메서드를 호출하거나 참조를 가져올 수 있음
- 비정적 멤버 클래스는 바깥 인스터스 없이는 생성할 수 없기 때문
- 이 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸린다.
- 어댑터를 정의할 때 자주 사용된다.
멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.#
- 암묵적 참조 때문에 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다.
익명 클래스에 대해 알아보기#
- 이름이 없는 클래스
- 바깥 클래스의 멤버도 아니다.
- 선언한 지점에서만 인스턴스를 만들 수 있고,
instanceof검사나 클래스의 이름이 필요한 작업을 수행할 수 없음 - 익명 클래스를 사용하는 클라이언트는 그 익명 클래스가 상위 타입에 상속한 멤버 외에는 호출 불가
- 표현식 중간에 등장하므로 짧지 않으면 가독성이 떨어짐
- 람다 지원 전 즉석에서 작은 함수 객체나 처리 객체를 만드는데 주로 사용함
지역 클래스에 대해 알아보기#
- 네 가지 클래스 중 가장 드물게 사용됨
- 지역변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있다.
- 유효 범위도 지역 변수와 같다.
- 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있고, 정적 멤버는 가질 수 없으며 가독성을 위해 짧게 작성해야 함
아이템 25 - 톱레벨 클래스는 한 파일에 하나만 담으라#
- 소스파일 하나에 톱레벨 클래스를 여러 개 선언하더라도 자바 컴파일러는 불평하지 않는다.
- 하지만 아무런 득이 없을 뿐더러 심각한 위험을 감수해야하는 행위다.
두 클래스가 한 파일에 정의되어 있다면#
java
class Utensil {
static final String NAME = "pan";
}
class Dessert {
static final String NAME = "cake";
}java
class Utensil {
static final String NAME = "pan";
}
class Dessert {
static final String NAME = "cake";
}java
public class Main {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Desert.NAME);
}
}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클래스의 정의를 불러와 놓는다.
해결책#
- 서로 다른 소스파일로 분리한다.