5. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
상속을 염두에 두지 않고 설계했거나, 상속할 때의 주의점을 문서화해놓지 않은 '외부' 클래스를 상속할 때 위험을 경고한다
여기서 '외부'란 프로그래머의 통제권 밖에 있어서 언제 어떻게 변경될지 모른다는 뜻이다
5.1 그렇다면 상속을 고려한 설계와 문서화란 정확히 무얼 뜻할까?
우선 메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 한다
즉, 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다
재정의할 수 있는 메서드(public과 protected 메서드 중 final이 아닌 모든 메서드)는 API 설명을 적시해야 한다
어떤 순서로 호출하는지, 각각의 호출 겨로가가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다
더 넓게 말하면, 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야 한다
예를 들어 백그라운드 스레드나 정적 초기화 과정에서도 호출이 일어날 수 있다
API 문서의 메서드 설명 끝에는 종종 'Implementation Requirements'로 시작하는 절을 볼 수 있는데, 그 메서드의 내부 동작 방식을 설명하는 곳이다
이 절은 메서드 주석에 @implSpec 태그를 붙여주면 자바독 도구가 생성해 준다
/**
* Performs a calculation based on the given inputs.
*
* @param input1 The first input value.
* @param input2 The second input value.
* @return The result of the calculation.
* @throws IllegalArgumentException If the inputs are invalid.
* @implSpec This method uses a specific algorithm to perform the calculation.
* It first checks the validity of the inputs and throws an exception if they are invalid.
* Then, it applies the algorithm by following a series of steps:
* 1. Step 1 description.
* 2. Step 2 description.
* 3. Step 3 description.
* ...
* Finally, it returns the calculated result.
*/
public int performCalculation(int input1, int input2) {
// Implementation code goes here
}
내부 메커니즘을 문서로 남기는 것만이 상속을 위한 설계의 전부는 아니다
효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다
드물게는 protected 필드로 공개해야 할 수도 있다
5.2 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다
상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다
5.3 상속용 클래스의 생성자는 직접적이든 간접적이든 재정의 가능 메서드를 호출해서는 안된다
이 규칙을 어기면 프로그램이 오동작할 것이다
상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다
이때 그 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것이다
이 규칙을 어기는 잘못된 코드를 통해 확인해 보자
public class Super {
// 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다.
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
public class Sub extends Super {
// 초기화되지 않은 final 필드. 생성자에서 초기화한다.
private final Instant instant;
public Sub() {
instant = Instant.now();
}
// 재정의 가능 메서드. 상위 클래스의 생성자가 호출한다.
@Override
public void overrideMe() {
System.out.println("instant = " + instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
이 프로그램이 instant를 두 번 출력하리라 기대했겠지만, 첫 번째는 null을 출력한다
instant = null
instant = 현재 시간
상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문이다
println이 아닌 일반 호출이었다면 NullPointerException을 던졌을 것이다
5.4 Cloneable과 Serializable 인터페이스는 상속용 설계의 어려움을 한층 더해준다
둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각이다
그 클래스를 확장하려는 프로그래머에게 엄청난 부담을 주기 때문이다
clone과 readObject 메서드는 생성자와 비슷한 효과를 낸다 (새로운 객체를 만든다)
이들을 구현할 때 따르는 제약도 생성자와 비슷하다는 점에 주의하자
즉, clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다
readObject의 경우 하위 클래스의 상태가 역직렬화가 되기도 전에 재정의한 메서드부터 호출하게 된다
clone의 경우 하위 클래스의 clone 메서드가 복제본의 상태를 수정하기 전에 재정의한 메서드를 호출한다
특히 clone이 잘못되면 복제본뿐 아니라 원본 객체에도 피해를 줄 수 있다
마지막으로, Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private이 아닌 protected로 선언해야 한다
private으로 선언한다면 하위 클래스에서 무시되기 때문이다
5.5 이 문제를 해결하는 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이다
상속을 금지하는 방법은 두 가지다
- 클래스를 final로 선언하는 방법
- 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법
- 핵심 정리
상속용 클래스를 설계하기란 결코 만만치 않다
클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 하며, 일단 문서화한 것은 클래스가 쓰이는 한 반드시 지켜야 한다
그러지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있다
다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있다
그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 나을 것이다
상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다
6. 추상 클래스보다는 인터페이스를 우선하라
인터페이스와 추상 클래스의 가장 큰 차이는 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점이다
자바는 단일 상속만 지원하니, 추상 클래스 방식은 새로운 타입을 정의하는 데 커다란 제약을 안게 되는 셈이다
반면, 인터페이스가 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급한다
기본 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다
인터페이스가 요구하는 메서드를 추가하고, 클래스 선언에 implements 구문만 추가하면 끝이다
6.1 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다
믹스인이란 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 '주된 타입' 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다
예를 들어 Comparable은 자신을 구현한 클래스의 인스턴스들끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스다
이처럼 대상 타입의 주된 기능에 선택적 기능을 혼합한다고 해서 믹스인이라 부른다
추상 클래스로는 믹스인을 정의할 수 없다
기존 클래스에 덧씌울 수 없기 때문이다
6.2 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다
타입을 계층적으로 정의하면 수많은 개념을 구조적으로 잘 표현할 수 있지만, 현실에는 계층을 엄격히 구분하기 어려운 개념도 있다
예를 들어 가수(Singer) 인터페이스와 작곡가(Songwriter) 인터페이스가 있다고 해보자
코드 타입을 인터페이스로 정의하면 가수 클래스가 Singer와 Songwriter 모두를 구현해도 전혀 문제 되지 않는다
public interface SingerSongwriter extends Singer, Songwriter {
AudioClip strum();
void actSensitive();
}
6.3 래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다
타입을 추상 클래스로 정의해 두면 그 타입에 기능을 추가하는 방법은 상속뿐이다
상속해서 만든 클래스는 래퍼 클래스보다 활용도가 떨어지고 깨지기는 더 쉽다
인터페이스와 추상 골격 구현 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법도 있다
인터페이스로 타입을 정의하고 골격 구현 클래스는 메서드를 구현한다
단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는데 필요한 일이 대부분 완료된다
바로 템플릿 메서드 패턴이다
관례로 인터페이스 이름이 interface라면 그 골격 구현 클래스의 이름은 AbstractInterface로 짓는다
예로 컬렉션 프레임워크의 AbstradctCollection, AbstractSet, AbstractList, AbstractMap 각각이 핵심 컬렉션 인터페이스의 골격 구현이다
제대로 설계했다면 골격 구현은 (독립된 추상 클래스든 디폴트 메서드로 이뤄진 인터페이스든) 그 인터페이스 나름의 구현을 만들려는 프로그래머의 일을 상당히 덜어준다
/* 골격 구현 클래스 예시 */
interface Shape {
void draw();
void resize(int width, int height);
}
abstract class AbstractShape implements Shape {
public void draw() {
System.out.println("Drawing the shape");
}
}
위의 코드에서 AbstractShape 클래스는 Shape 인터페이스를 상속받아 draw() 메서드를 구현한다
draw() 메서드는 "Drawing the shape"라는 메시지를 출력한다
resize() 메서드는 추상 메서드로 남겨져 있으므로 AbstractShape 클래스를 상속받은 클래스에서 구현해야 한다
단순 구현은 골격 구현의 작은 변종으로, AbstractMap.SimpleEntry가 좋은 예다
단순 구현도 골격 구현과 같이 상속을 위해 인터페이스를 구현한 것이지만, 추상 클래스가 아니란 점이 다르다
쉽게 말해 동작하는 가장 단순한 구현이다
이러한 단순 구현은 그대로 써도 되고 필요에 맞게 확장해도 된다
/* 단순 구현 클래스 예시*/
interface Shape {
void draw();
void resize(int width, int height);
}
class SimpleShape implements Shape {
public void draw() {
System.out.println("Drawing a simple shape");
}
public void resize(int width, int height) {
System.out.println("Resizing the simple shape to " + width + "x" + height);
}
}
- 핵심 정리
일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다
복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해 보자
골격 구현은 '가능한 한' 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋다
'가능한 한'이라 한 이유는 인터페이스에 걸려있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문이다
7. 인터페이스는 구현하는 쪽을 생각해 설계하라
자바 8 이전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다
자바 8에 와서 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드를 소개했지만 위험이 완전히 사라진 것은 아니다
디폴트 메서드를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다
자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었는데 주로 람다를 활용하기 위해서다
자바 라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분 상황에서 잘 동작한다
하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다
자바 8의 Collection 인터페이스에 추가된 removeIf 메서드를 예로 들어보자
이 메서드는 주어진 boolean 함수(Predicate)가 true를 반환하는 모든 원소를 제거한다
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 구현체와 잘 어우러지는 것은 아니다
예를 들어, removeIf의 구현은 동기화에 관해 아무것도 모르므로 락 객체를 사용할 수 없다
SynchronizedCollection 인스턴스 환경에서 한 스레드가 removeIf를 호출하면 예기치 못한 결과를 발생할 수 있다
핵심은 명백하다
디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 한다
디폴트 메서드로 기존 인터페이스에 새로운 메서드를 추가하면 커다란 위험도 딸려온다
8. 인터페이스는 타입을 정의하는 용도로만 사용하라
클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해 주는 것이다
인터페이슨느 오직 이 용도로만 사용해야 한다
8.1 이 지침에 맞지 않는 예로 상수 인터페이스라는 것이 있다
상수 인터페이스란 메서드 없이, 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스를 말한다
/* 상수 인터페이스 안티패턴 - 사용금지! */
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로 노출하는 행위다
클래스가 어떤 상수 인터페이스를 사용하든 사용자에게는 아무런 의미가 없다
ObjectStreamConstants 등 자바 플랫폼 라이브러리에도 상수 인터페이스가 몇 개 있으나, 인터페이스를 잘못 활용한 예이니 따라 해서는 안된다
8.2 상수를 공개할 목적이라면 더 합당한 선택지가 몇 가지 있다
열거 타입으로 나타내기 적합한 상수라면 열거 타입으로 만들어 공개하면 된다
그것도 아니라면, 인스턴스화할 수 없는 유틸리티 클래스에 담아 공개하자
/* 상수 유틸리티 클래스 */
public class PhysicalConstantUtils {
private PhysicalConstantUtils() {} // 인스턴스화 방지
// 아보가드로 수 (1/몰)
public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
// 볼츠만 상수 (J/K)
public static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
// 전자 질량 (kg)
public static final double ELECTRON_MASS = 9.109_383_56e-31;
}
숫자 리터럴에 사용한 '_'에 주목해 보자
자바 7부터 허용되는 이 언더바는 숫자 리터럴의 값에는 아무런 영향을 주지 않으면서, 읽기는 훨씬 편하게 해 준다
고정소수점 수든 부동소수점 수든 5자리 이상이라면 언더바를 사용하는 걸 고려해 보자
십진수 리터럴도 밑줄을 사용해 세 자릿씩 묶어주는 것이 좋다
유틸리티 클래스에 정의된 상수를 클라이언트에서 사용하려면 클래스 이름까지 함께 명시해야 한다
PhysicalConstant.AVOGADROS_NUMBER처럼 말이다
유틸리티 클래스의 상수를 빈번히 사용한다면 정적 임포트하여 클래스 이름은 생략할 수 있다
import static .....PhysicalConstants.*;
public class Test {
double atoms(double mols) {
return AVOGADROS_NUMBER * mols;
}
...
// PhysicalConstants를 빈번히 사용한다면 정적 임포트가 값어치를 한다
}
- 핵심 정리
인터페이스는 타입을 정의하는 용도로만 사용해야 한다
상수 공개용 수단으로 사용하지 말자
9. 태그 달린 클래스보다는 클래스 계층구조를 활용하라
두 가지 이상의 의미를 표현할 수 있으며, 그중 현재 표현하는 의미를 태그 값으로 알려주는 클래스를 본 적이 있을 것이다
다음 코드는 원과 사각형을 표현할 수 있는 클래스다
/* 태그 달린 클래스 - 클래스 계층구조보다 훨씬 나쁘다! */
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 length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError(shape);
}
}
}
9.1 태그 달린 클래스의 단점
- 열거 타입 선언, 태그 필드, switch문 등 쓸데없는 코드가 많다
- 여러 구현이 한 클래스에 혼합돼 있어서 가독성이 나쁘다
- 다른 의미를 위한 코드도 언제나 함께 하니 메모리도 많이 사용한다
- 필드들을 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다
즉, 쓰지 않는 필드를 초기화하는 불필요한 코드가 늘어난다 - 엉뚱한 필드 초기화 시 컴파일 단계에서 알 수 없다
- 다른 의미를 추가하려면 코드를 수정해야 한다
- 인스턴스의 타입만으로는 현재 나타내는 의미를 알 길이 전혀 없다
종합하자면 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다
9.2 태그 달린 클래스를 클래스 계층구조로 바꾸는 방법
- 계층구조의 루트(root)가 될 추상 클래스를 정의
- 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언
(상단 코드에서 area()가 이러한 메서드에 해당) - 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가
(상단 코드에서 태그 값에 상관없는 메서드가 하나도 없고, 모든 하위 클래스에서 사용하는 공통 데이터 필드도 없다
그 결과 루트 클래스에는 추상 메서드인 area 하나만 남게 된다) - 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의
(예제에서는 Figure를 확장한 원(Circle) 클래스와 사각형(Rectangle) 클래스를 만들면 된다)
상단 Figure 클래스를 계층구조 방식으로 구현한 코드다
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;
}
}
타입이 의미별로 따로 존재하니 변수의 의미를 명시하거나 제한할 수 있고, 특정 의미만 매개변수로 받을 수 있다
또한, 타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일타임 타입 검사 능력을 높여준다는 장점도 있다
만약 정사각형이 추가될 경우에도 쉽게 반영할 수 있다
public class Square extends Rectangle{
public Square(double side) {
super(side, side);
}
}
- 핵심 정리
태그 다린 클래스를 써야 하는 상황은 거의 없다
새로운 클래스를 작성하는데 태그 필드가 등장한다면 태그를 없애고 계층구조로 대체하는 방법을 생각해 보자
기존 클래스가 태그 필드를 사용하고 있다면 계층구조로 리팩터링 하는 걸 고민해 보자
10. 멤버 클래스는 되도록 static으로 만들라
중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스를 말한다
중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 Top레벨 클래스로 만들어야 한다
10.1 중첩 클래스의 종류
- 정적 멤버 클래스
- (비정적) 멤버 클래스
- 익명 클래스
- 지역 클래스
정적 멤버 클래스를 제외한 나머지는 매부 클래스(inner class)에 해당한다
10.1.1 정적 멤버 클래스
정적 멤버 클래스는 바깥 클래스의 인스턴스와 상관없이 독립적으로 존재하는 클래스다
다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고 일반 클래스와 똑같다
주로 바깥 클래스와 강한 연관이 있는 도우미 클래스를 구현하는 데 사용된다
public class OuterClass {
private static int outerVariable = 10;
// 정적 멤버 클래스
static class StaticMemberClass {
void display() {
System.out.println("Outer variable: " + outerVariable);
}
}
public static void main(String[] args) {
// 정적 멤버 클래스의 인스턴스 생성 및 사용
OuterClass.StaticMemberClass staticClass = new OuterClass.StaticMemberClass();
staticClass.display(); // Outer variable: 10
}
}
10.1.2 (비정적) 멤버 클래스
멤버 클래스는 바깥 클래스의 인스턴스와 관련이 있으며, 바깥 클래스의 인스턴스를 통해 생성되고 사용된다
멤버 클래스는 바깥 클래스의 멤버 변수 및 메서드에 접근할 수 있다
public class OuterClass {
private int outerVariable = 10;
// 멤버 클래스
class MemberClass {
void display() {
System.out.println("Outer variable: " + outerVariable);
}
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
// 멤버 클래스의 인스턴스 생성 및 사용
OuterClass.MemberClass memberClass = outerClass.new MemberClass();
memberClass.display(); // Outer variable: 10
}
}
10.1.3 익명 클래스
익명 클래스는 이름이 없는 클래스로, 클래스 정의와 동시에 인스턴스를 생성한다
주로 인터페이스나 추상 클래스를 구현하거나 상속받는 클래스를 구현할 때 사용된다
익명 클래스는 한 번만 사용되는 간단한 구현을 제공하며, 코드의 가독성을 높일 수 있다
public class OuterClass {
public void displayGreeting() {
// 익명 클래스
Greeting greeting = new Greeting() {
@Override
public void greet() {
System.out.println("Hello, World!");
}
};
greeting.greet(); // Hello, World!
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
outerClass.displayGreeting();
}
// 인터페이스
interface Greeting {
void greet();
}
}
10.1.4 지역 클래스
지역 클래스는 메서드 내부나 코드 블록 내부에 선언되는 클래스다
지역 클래스는 해당 블록 내에서만 사용될 수 있으며, 해당 블록의 변수에 접근할 수 있다
public class OuterClass {
private int outerVariable = 10;
public void displayMessage() {
// 지역 클래스
class LocalClass {
void display() {
System.out.println("Outer variable: " + outerVariable);
}
}
LocalClass localClass = new LocalClass();
localClass.display(); // Outer variable: 10
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
outerClass.displayMessage();
}
}
- 핵심 정리
중첩 클래스에는 네 가지가 있으며, 각각의 쓰임이 다르다
메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만든다
멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로
그렇지 않다면 정적으로 만들자
중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고,
그렇지 않다면 지역 클래스로 만들자
11. 탑레벨 클래스는 한 파일에 하나만 담으라
소스 파일 하나에 탑레벨 클래스를 여러 개 선언하더라도 자바 컴파일러는 불평하지 않는다
하지만 아무런 득이 없을뿐더러 심각한 위험을 감수해야 하는 행위다
11.1 어느 소스 파일을 먼저 컴파일하냐에 따라 그중 어느 것을 사용할지 달라진다
구체적인 예를 통해 알아보자
public static void main(String[] args) {
System.out.println(Utensil.NAME + " " + Dessert.NAME);
}
Utensil.java 파일을 만들고 클래스를 2개 정의해 보자
/* Utensil.java 파일안에 두 클래스를 정의했다 */
class Utensil {
static final String NAME = "pan";
}
class Dessert {
static final String NAME = "cake";
}
main을 실행하면 pancake를 출력한다
이제 우연히 똑같은 두 클래스를 담은 Dessert.java라는 파일을 만들었다고 해보자
/* Dessert.java 파일안에 두 클래스를 정의했다 */
class Utensil {
static final String NAME = "pot";
}
class Dessert {
static final String NAME = "pie";
}
컴파일러에서 어떤 순서로 컴파일하냐에 따라 컴파일러 오류가 날 수 있고,
pancake가 출력될 수도 potpie가 출력될 수도 있다
11.2 해결책은 아주 간단하다
서로 다른 클래스로 분리하면 그만이다
굳이 여러 탑레벨 클래스를 한 파일에 담고 싶다면 정적 멤버 클래스를 사용하는 방법도 가능하다
public class Test {
public static void main(String[] args) {
System.out.println(Utensil.NAME + " " + Dessert.NAME);
}
private static class Utensil {
static final String NAME = "pan";
}
private static class Dessert {
static final String NAME = "cake";
}
}
- 핵심 정리
교훈은 명확하다
소스 파일 하나에는 반드시 탑레벨 클래스(혹은 탑레벨 인터페이스)를 하나만 담자
이 규칙만 따른다면 컴파일러가 한 클래스에 대한 정의를 여러 개 만들어내는 일은 사라진다
소스 파일을 어떤 순서로 컴파일하든 바이너리 파일이나 프로그램의 동작이 달라지는 일은 결코 일어나지 않을 것이다
마무리 느낌점
인터페이스를 평소 외부 라이브러리 정도에만 적용하는 걸 떠올렸던 것 같은데
이번 장을 통해 활용 방법과 사용 여부를 판단하는데 많은 도움이 된 것 같다
'개발 서적 > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바 3/E - 5.제네릭(1) (0) | 2023.07.14 |
---|---|
이펙티브 자바 3/E - 4.클래스와 인터페이스(1) (1) | 2023.06.10 |
이펙티브 자바 3/E - 3.모든 객체의 공통 메서드 (1) | 2023.05.20 |
이펙티브 자바 3/E - 2.객체 생성과 파괴 (0) | 2023.04.30 |
이펙티브 자바 3/E - 1.들어가기 (0) | 2023.04.19 |