BottleH Blog

이펙티브자바 3판 4장 정리

    Tags

  • Java
이펙티브자바 3판 4장 정리 thumbnail

4장 클래스와 인터페이스

추상화의 기본 단위인 클래스와 인터페이스는 자바 언어의 심장과도 같다. 그래서 자바 언어에는 클래스와 인터페이스 설계에 사용하는 강력한 요소가 많이 있다. 이번 장에서는 이런 요소를 적절히 활용하여 클래스와 인터페이스를 쓰기 편하고, 견고하며, 유연하게 만드는 방법을 안내한다.

목차


아이템15. 클래스와 멤버의 접근 권한을 최소화하라


어설프게 설계된 컴포넌트와 잘 설계된 컴포넌트의 가장 큰 차이는 바로 클래스 내부 데이터와 내부 구현 정보를 외부 컴포넌트로부터 얼마나 잘 숨겼느냐다.(정보은닉, 캡슐화)

15-1. 정보은닉의 장점

  1. 시스템의 개발 속도를 높인다.
    • 여러 컴포넌트를 병렬로 개발할 수 있기 때문
  2. 시스템 관리 비용을 낮춘다.
    • 각 컴포넌트를 더 빨리 파악하여 디버깅할 수 있고, 다른 컴포넌트로 교체하는 부담도 적기 때문
  3. 정보 은닉 자체가 성능을 높여주지는 않지만, 성능 최적화에 도움을 준다.
    • 완성된 시스템을 프로파일링해 최적화할 컴포넌트를 정한 다음(아이템67) 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화할 수 있기 때문이다.
  4. 소프트웨어 재사용성을 높인다.
  5. 큰 시스템을 제작하는 난이도를 낮춰준다.

15-2. 정보은닉 원칙

접근 제한자(private, protected, public)이 정보은닉의 핵심이다.

  1. 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.

  2. public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다.(아이템16)

    • public 가변 필드를 갖는 클래스는 일반적으로 스레드 안전하지 않다.
  3. 클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안 된다.

결론: 프로그램 요소의 접근성은 가능한 한 최소한으로 하라. 꼭 필요한 것만 골라 최소한의 public API를 설계하자. 그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개된ㄴ 일이 없도록 해야 한다. public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다. public static final 필드가 참조하는 객체가 불변인지 확인하라.

아이템16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라


이따금 인스턴스 필드들을 모아놓는 일 외에는 아무 목적도 없는 퇴보한 클래스를 작성하려 할 때가 있다.

class Point { public double x; public double y; }

이런 클래스는 데이터 필드에 직접 접근할 수 있어 캡슐화의 이점을 제공하지 못한다.

  • API를 수정하지 않고는 내부 표현을 바꿀 수 없다.
  • 불변식을 보장할 수 없다.
  • 외부에서 필드에 접근할 때 부수 작업을 수행할 수도 없다.

위와 같은 클래스는 필드를 모두 private로 바꾸고 public 접근자(getter)를 추가한다.

package-private 클래스 혹은 private 중첩 클래스라면 데이터 필드를 노출한다 해도 하등의 문제가 없다.

결론: public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다. 불변 필드(final)라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다. 하지만 package-private 클래스나 private 중첩 클래스에서는 종종(불변이든 가변이든) 필드를 노출하는 편이 나을 때도 있다.

아이템17. 변경 가능성을 최소화하라


불변 클래스: 인스턴스의 내부 값을 수정할 수 없는 클래스

  • ex) String, 기본 타입의 박싱된 클래스들, BigInteger, BigDecimal
  • 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.

17-1. 불변 클래스 생성 규칙

  1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다.
    • 하위 클래스에서 부주의하게 혹은 나쁜 의도로 객체의 상태를 변하게 만드는 사태를 막아준다.
    • 대표적인 방법은 클래스를 final로 선언하는 것이지만, 다른 방법도 있다.
  3. 모든 필드는 final로 선언한다.
    • 시스템이 강제하는 수단을 이용해 설계자의 의도를 명확히 드러내는 방법
    • 새로 생성된 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작하게끔 보장하는 데도 필요하다
  4. 모든 필드를 private으로 선언한다.
    • 클라이언트에서 직접 접근해 수정하는 일을 막아준다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

17-2. 불변 객체

함수형 프로그래밍: 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴, 코드에서 불변이 되는 영역의 비율이 높아지는 장점을 누릴 수 있다.

  1. 불변 객체는 단순하다.
    • 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다.
  2. 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요 없다.
    • 불변 객체에 대해서는 그 어떤 스레드도 다른 스레드에 영향을 줄 수 없으니, 불변 객체는 안심하고 공유할 수 있다.
  3. 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다.
  4. 객체를 만들 때 다른 불변 객체들을 구성요소로 사용하면 이점이 많다.
    • 값이 바뀌지 않는 구성요소들로 이뤄진 객체라면 그 구조가 아무리 복잡하더라도 불변식을 유지하기 훨씬 수월하기 때문
  5. 불변 객체는 그 자체로 실패 원자성을 제공한다.
    • 실패 원자성: 메서드에서 예외로 발생한 후에도 그 객체는 여전히 (메서드 호출 전과 똑같은) 유효한 상태여야 한다는 성질

불변 클래스의 단점: 값이 다르면 반드시 독립된 객체로 만들어야 한다.

BigInteger moby = ...; moby = moby.flipBit(0);

flipBit 메서드는 새로운 BigInteger 인스턴스를 생성한다. 이 연산은 BigInteger의 크기에 비례해 시간과 공간을 잡아먹는다.

결론: 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자. 다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다. 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.

아이템18. 상속보다는 컴포지션을 사용하라


현재 말하고자 하는 상속은 클래스가 다른 클래스를 확장하는 구현 상속을 말한다.

  • 클래스가 인터페이스를 구현하거나 인터페이스가 다른 인터페이스를 확장하는 인터페이스 상속과는 무관하다.

18-1. 상속의 잘못된 예

메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.

  • 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
public class InstrumentedHashSet<E> extends HashSet<E>{ // 추가된 원소의 수 private int addCount = 0; public InstrumentedHashSet(){ } public InstrumentedHashSet(int initCap, float loadFactor){ super(initCap, loadFactor); } @Override public boolean add(E e){ addCount++; return super.add(e); } @Override public boolean addAll(Collection<? extends E> c){ addCount += c.size(); return super.addAll(c); } public int getAddCount(){ return addCount; } }

위의 코드는 제대로 작동하지 않는다.

InstrumentedHashSet<String> s = new InstrumentedHashSet<>(); s.addAll(List.of("스", "프", "링"))

getAddCount를 하면 3이 아니라 6을 반환한다!!

HashSetaddAll은 각 원소를 add 메서드로 호출해 추가한다. 이 때 불리는addInstrumentedHashSet에서 재정의한 메서드이다. 따라서 addCount에 값이 중복해서 더해진다.

문제를 해결하는 방법은 여러가지가 있지만, 만약 다음 릴리스에서 상위 클래스에 새로운 메서드를 추가한다면?? => 결국 하위클래스는 깨지기 쉽다.

메서드를 재정의하지않고, 새로운 메서드를 정의하면 훨씬 안전하긴 하다. 그런데, 다음 릴리스에서 상위 클래스에 새 메서드가 추가되었는데 하필이면 시그니처가 같고 반환 타입은 다르다면 새로운 메서드는 컴파일조차 안 된다.

18-2. 컴포지션

위의 문제를 모두 피할 수 있는 묘안이 있다. 바로 컴포지션이다!!

컴포지션: 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하는 것

  • 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻

전달(forwarding): 새 클래스의 인스턴스 메서드들은 (private 필드로 참조하는) 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환

  • 전달 메서드: 새 클래스의 메서드

컴포지션과 전달의 조합은 넓은 의미로 **위임(delegations)**이라고 부른다.

  • 엄밀히 따지면 래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우만 위임에 해당한다.

결론: 상속은 강력하지만 캡슐화를 해친다는 문제가 있다. 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야 한다. is-a 관계일 때도 안심할 수만은 없는 게, 하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았다면 여전히 문제가 될 수 있다. 상속의 취약점을 피하려면 상속 대신 컴포지션과 전달을 사용하자. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다. 래퍼 클래스는 하위 클래스보다 견고하고 강력하다.

아이템19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라


메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 한다. 즉, 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다.

  • 재정의 가능이란 public과 protected 메서드 중 final이 아닌 모든 메서드를 뜻한다.

✔ API 문서의 메서드 설명 끝에서 종종 "Implementation Reguirements"로 시작하는 절을 볼 수 있는데, 그 메서드의 내부 동작 방식을 설명하는 곳이다.

19-1. 상속용 클래스

  1. 내부 메커니즘을 문서로 남기자.
  2. 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.
  3. 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 '유일'하다.
  4. 상속용으로 설계한 클래스는 배포 전에 반드시 하위클래스를 만들어 검증해야 한다.
  5. 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
    • 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되기 때문
    • private, final, static 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.

19-2. 상속을 금지하는 방법

  1. 클래스를 final로 선언
  2. 모든 생성자를 private이나 package-private으로 선언하고, public 정적 팩터리를 만들어준다.

19-3. 참고: 피터코드의 상속규칙

  1. 자식 클래스와 부모 클래스 사이는 '역할 수행' 관계가 아니어야 한다.
  2. 한 클래스의 인스턴스는 다른 서브 클래스의 객체로 변환할 필요가 절대 없어야 한다.
  3. 자식 클래스가 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장만 수행해야 한다.
  4. 자식 클래스가 단지 일부 기능을 재사용할 목적으로 유틸리티 역할을 수행하는 클래스를 상속하면 안된다. (위임)
  5. 자식 클래스가 역할, 트랜잭션, 디바이스 등을 특수화 해야 한다.

아이템20. 추상 클래스보다는 인터페이스를 우선하라


자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스가 있다.

20-1. 추상 클래스와 인터페이스 차이

  • 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
    • 자바는 단일 상속만 지원하니, 추상 클래스는 새로운 타입을 정의하는 데 커다란 제약은 안게 된 것.
  • 인터페이스는 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급된다.
  • 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있지만, 추상 클래스는 어렵다.
  • 인터페이스는 믹스인(mixin) 정의에 안성맞춤이지만, 추상 클래스는 믹스인을 정의할 수 없다.
    • 믹스인: 클래스가 구현할 수 있는 타입
  • 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다. 하지만, 클래스는 조합 폭발을 야기할 수 있다.
  • 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이 된다.(래퍼 클래스 관용구 사용)

20-2. 템플릿 메서드 패턴

추상 골격 구현 클래스와 인터페이스를 함게 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취하는 방법

템플릿 메서드 패턴

아이템21. 인터페이스는 구현하는 쪽을 생각해 설계하라


자바 8 전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.

21-1. 디폴트 메서드

디폴트 메서드를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다. 자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었다. 자바 라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분 상황에서 잘 작동한다. 하지만 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.

default boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); boolean removed = false; Iterator each = this.iterator(); while(each.hasNext()) { if (filter.test(each.next())) { each.remove(); removed = true; } } return removed; }

디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다.

결론: 디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 한다.

아이템22. 인터페이스는 타입을 정의하는 용도로만 사용하라


인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다. 즉, 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것이다.

22-1. 상수 인터페이스 안티 패턴 - 사용하지 말 것.

public interface PhysicalConstants{ static final double AVOGADROS_NUMBER = 6.022_140_857e23; }

상수 인터페이스 안티패턴은 인터페이스를 잘못 사용한 예다. 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당한다. 따라서 상수 인터페이스를 구현하는 것을 이 내부 구현을 클래스의 API로 노출하는 행위다.

상수를 공개할 목적이라면

  1. 클래스나 인터페이스 자체에 추가
  2. 열거 타입으로 표현
  3. 인스턴스화할 수 없는 유틸리티 클래스
    • 정적 임포트 하면 클래스 이름 생략 가능

아이템23. 태그 달린 클래스보다는 클래스 계층구조를 활용하라


태그 달린 클래스: 두 가지 이상의 의미를 표현할 수 있으며, 그 중 현재 표현하는 의미를 태그 값으로 알려주는 클래스

23-1. 태그 클래스의 단점

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); } } }
  1. 열거 타입 선언, 태그 필드, switch 문 등 쓸데 없는 코드가 많다.
  2. 여러 구현이 한 클래스에 혼합돼 있어 가독성도 나쁘다.
  3. 다른 의미를 위한 코드도 언제나 ㅎ함께 하니 메모리도 많이 사용한다.
  4. 필드들을 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다.
  5. 또 다른 의미를 추가하려면 코드를 수정해야 한다.

✔ 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적이다.

23-2. 클래스 계층 구조

  1. 계층구조의 루트가 될 추상 클래스를 정의
  2. 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언
  3. 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가
  4. 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올린다.
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; } }
  • 장점
    • 간결하고 명확하고, 쓸데없는 코드가 없다.
    • 관련 없던 데이터 필드 제거
    • 추상 메서드를 모두 구현했는지 컴파일러가 확인해줌.
    • 확장성이 좋다.
    • 타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일타임 타입 검사 능력을 높여준다.

아이템24. 멤버 클래스는 되도록 static으로 만들라


중첩 클래스: 다른 클래스 안에 정의된 클래스

  • 자신을 감싼 바깥 클래스에서만 쓰여야 함.(그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.)
  • 종류: 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스
    • 정적 멤버 클래스를 제외한 3가지는 내부 클래스에 해당한다.

24-1. 정적 멤버 클래스, 비정적 멤버 클래스

정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하고는 일반 클래스와 같다.

정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어 있고 없고 차이이다. 하지만 의미상 차이는 꽤 크다. 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 그래서 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.

즉, 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다.

비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다. 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하는 것이다.

public class MySet<E> extends AbstractSet<E>{ ... // 생략 @Override public Iterator<E> iterator(){ return new MyIterator(); } private class MyIterator implements Iterator<E>{ ... } }

멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.

  • static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 된다. => 이 참조를 저장하려면 시간과 공간이 소비된다.

private 정적 멤버 클래스는 흔히 바깥 클래스가 표현하는 객체의 한 부분(구성요소)을 나타낼 때 쓴다.

24-2. 익명 클래스

익명클래스는 이름이 없으며, 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다.

  • instanceof 검사나 클래스의 이름이 필요한 작업은 수행할 수 없다.

  • 여러 인터페이스를 구현할 수 없고, 인터페이스를 구현하는 동시에 다른 클래스를 상속할 수도 없다.

  • 가독성이 떨어진다.

  • 정적 팩터리 메서드를 구현할 때 사용

  • 즉석에서 작은 함수 객체나 처리 객체를 만드는 데 익명 클래스를 주로 사용했다.(이제는 람다를 씀)

return new Test(){ ...// }

위와 같은 코드가 익명클래스다.

24-3. 지역 클래스

가장 드물게 사용하는 중첩 클래스

  • 지역변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있다.
  • 유효 범위도 지역변수와 같다.
  • 멤버 클래스처럼 이름이 있고 반복해서 사용할 수 있다.
  • 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있다.
  • 정적 멤버는 가질 수 없다.
  • 가독성을 위해 짧게 작성해야 한다.

결론: 메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만든다. 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않으면 정적으로 만들자. 중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고, 그렇지 않으면 지역클래스로 만들자.

아이템25. 톱레벨 클래스는 한 파일에 하나만 담으라


소스 파일 하나에 톱레벨 클래스를 여러 개 선언하더라도 자바 컴파일러는 불평하지 않는다. 하지만 득도 없고, 위험을 감수해야 한다.

  • 한 클래스를 여러 가지로 정의할 수 있으며, 그중 어느 것을 사용할지는 어느 소스 파일을 먼저 컴파일하냐에 따라 달라지기 때문
class Utensil{ static final String NAME = "pan"; } class Dessert{ static final String NAME = "cake"; }

굳이 써야 한다면 정적 멤버 클래스로 만들어라.

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"; } }

소스 파일 하나에는 반드시 톱레벨 클래스(혹은 톱레벨 인터페이스)를 하나만 담자.

Written by@BottleH
Back-End Developer

GitHub