JAVA/Effective Java

상시 업데이트함 -- Effective java 스터디

히어로맛쿠키 2024. 7. 22. 17:43

(24.07.00) ItemXX: ~~~ 

~~

 

 


(24.07.00) Item77: 예외를 무시하지 말라

~~

 

 

 

(24.07.00) Item76: 가능한 한 실패 원자적으로 만들라

~~

 

 

 

(24.07.00) Item75: 예외의 상세 메시지에 실패 관련 정보를 담으라

~~

 

 

 

 

(24.07.00) Item74: 메서드가 던지는 모든 예외를 문서화하라

~~

 

 

 

(24.07.22) Item73: 추상화 수준에 맞는 예외를 던지라 

## 예외 번역이란

너무 저수준 예외를 계속 전파하면, 내부 구현 방식도 드러나고 혼란스러워진다. 그래서 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다.

try {
    ...
} catch (LowerLevelException e) {
    // 추상화 수준에 맞게 번역!
    throw new HigherLevelException(...);
}
다음은 AbstractSequentialList에서 수행하느 ㄴ예외 변역의 예다!
참고로 이 예외번역은, List<E> interface의 get() 명세에 명시된 필수사항이다.

/**
  * 이 리스트 안의 지정한 위치의 원소를 반환한다.
  * @throws IndexOutOfBoundsException index가 범위 밖이라면,
  *        즉 ({@code index < 0 || index >= size()}) 이면 발생한다.
  */
  public E get(int index) {
      ListIterator<E> i = listIterator(index);
      try {
          return i.next();
      } catch (NoSuchElementException e) {
          throw new IndexOutOfBoundsException("인덱스: " + index);
      }
  }

## 예외 연쇄란

만약 저수준예외가 디버깅에 도움이 된다면, 예외 연쇄(exception chaining)을 사용하는 것이 좋다.

문제의 근본원인(저수준)을 고수준예외에 실어 보내는 방식!

언제든 Throwable의 getCause()를 통해, 저수준 예외를 꺼내볼 수 있다. 

고수준 예외의 생성자는, 예외 연쇄용으로 설계된 상위 클래스의 생성자에 이 '원인'을 건네주어 최종적으로 Throwable 생성자까지 건네지게 한다. 아래 코드 참고! (대부분 표준 예외는 예외 연쇄용 생성자를 갖추고 있다)

// 참고: 예외 연쇄용 생성자
class HigherLevelException extends Exception {
    HigherLevelException(Throwable cause) {
        super(cause);
    }
}

 

 

(24.07.22) Item72: 표준 예외를 사용하라

## 나만의 예외를 만들기보다는 표준 예외를 재사용하자. 

## 예를들어 많이 재사용되는 예외는...

- IllegalArgumentException (Item49): 넘어온 인수 상태가 부적절

- IllegalStateException: 객체 상태가 부적절

- ConcurrentModificationException: 단일스레드용 객체를 여러 스레드가 동시수정 하려할 때

## 직접 재사용하지 말아야 할 예외

Exception, RuntimeException, Throwable, Error → 이런 너무 상위 클래스..

 

(24.07.22) Item71: 필요 없는 검사 예외 사용은 피해라

검사 예외는 안정을 주지만, 검사 예외가 만약 단 한개 뿐이어서 API 사용자가 try catch 블록을 추가해야 하고 스트림에서 직접 사용하지 못하게 만든다면, 부담스러울 수 있다. 이럴 때는 검사 예외를 안 던지는 방법이 있는지 고민해볼만 하다.

## 검사 예외를 회피하는 방법

- 적절한 결과타입을 담은 옵셔널을 반환 (ex: 빈 옵셔널): 부가 정보 제공 불가

- 예외 사용: 부가 정보 제공 가능

- 검사 예외를 던지는 메서드를 2개로 쪼개서 비검사 예외로 바꾸기

// 리팩터링 전 (검사예외를 던지는 메서드 단독 사용)
try {
    obj.action(args);
} catch (TheCheckedException e) {
    ...예외처리
}

// 리팩터링 후 (상태검사 메서드 + 비검사 예외를 던지게 수정!)
// 즉, action이 가능한지 먼저 검토하여 더욱 유연합니다.
// BUT 단점 존재: Item69: 멀티스레드에서 적합하지X 및 성능 손해 가능성
if (obj.actionPermitted(args)) {
    obj.action(args);
} else {
    ...예외처리
}

// 실패 시 그냥 스레드가 중단하길 원한다면 그저..
obj.action(args); // 라고 써도 되죠..

 

 

 

(24.07.22) Item70: 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라

## 자바의 throwable

- 검사 예외, 런타임 예외, 에러 → 언제 무엇을 사용해야 하는가?

- 복구할수 있는 상황: 검사예외

API 호출자에게 예외 발생 가능성을 얘기해주는 것임

└ 검사 예외를 던지면, 호출자가 catch 처리하거나 더 밖으로 전파하게 강제할 수 있다 (조치 취하는 것을 권장, Item77)

└ 이때, 복구에 필요한 정보를 알려주는 메서드도 제공하자 (카드잔고 부족 예외면, 잔고가 얼마나 부족한지 알려주는 접근자 메서드를 제공하는 등)

- 프로그래밍 오류: 비검사 예외 (런타임 예외 or 에러)

└ 터지게 두는 것이 적절하다. 

- 확실하지 않은 경우: 비검사 예외

- Error는 상속하지도, throw문으로 직접 던지지도 말자 (AssertionError는 괜찮)

- 예외도 객체임을 잊지 말자.

 

(24.07.18) Item69: 예외는 진짜 예외 상황에만 사용하라

## 제어흐름용으로 사용하면 안된다.

- 예를들어 배열 반복문을 while true로 돌다가 인덱스예외로 멈추게 한다던지 하지 마라.

느리고, try-catch블록 안에서는 JVM의 최적화가 제한적이고, 예외를 거기서 잡아버리면 디버깅이 어렵다.

- 표준 관용구를 쓰자

## API 설계할때, 사용자가 예외를 사용할 일이 없게해야 함

- 함께 제공해야 한다: 상태 의존적 메서드 & 상태 검사 메서드 (ex: Iterator 인터페이스의 next와 hasNext)

- for-each문에서도 내부적으로 hasNext를 사용한다.

// 상태 의존적 메서드 (next같은 것!)
// & 상태 검사 메서드(hasNext같은 것!)가 존재할 때 컬렉션 순회 코드

// 표준 for 관용구에 hasNext 사용하기 좋음
for(Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
    Foo foo = i.next();
    ...
}

// hasNext가 제공되지 않을 경우
try {
    Iterator<Foo> i = collection.iterator();
    while(true) {
        Foo foo = i.next();
        ...
    }
} catch (NoSuchElementException e) { }


// hasNext 말고 또 다른 방법!
// 올바르지 않은 상태에 대해서 빈옵셔널이나 null, 이런 특정값 반환하는 방법도 있다.
// => 멀티스레드 환경에서, hasNext검사시점이랑 작업시점 사이에 객체상태 변동 가능성이 있는데, 그럴 때 좋다.

 


(24.07.18) Item68: 일반적으로 통용되는 명명 규칙을 따르라. 

## 정리하지 않겠다!

 

(24.07.18) Item67: 최적화는 신중히 하라. 

## 막 하지 마라! 빠른 프로그램보다는 좋은 프로그램을 작성하기!!

견고한 아키텍처가 우선이다. 최적화는 구조가 명확한 이후 후순위로!

- 최적화하느라 구조 해치지 말기

- 설계단계에서 성능 고려하기 (API, 네트워크 프로토콜, 데이터포맷, 알고리즘...)

## API 설계시 성능에 주는 영향을 고려하라

- 가변 클래스 만들지 않기 => 이곳저곳에서 방어적 복사 과정 일어나서 성능저하

(예를들어 java.awt.Component 클래스의 getSize()는 Dimension 인스턴스를 반환하는데 가변이다! 그래서 getSize() 호출시 매번 방어적 복사(새로 생성) 해야 한다. 성능 해결 방법은 Dimension을 불변으로 구현하는 것인데 좀 어렵겠고, 간단한 방법으로는 getHeight()와 getWidth() 를 따로 둬서 기본타입 리턴하는 방법이 있다. 자바2 이후에 추가되어있다.)

- 상속 대신 컴포지션 (Item18)

- 구현체 대신 인터페이스 타입 (Item64)

## 최적화 시도 전후에는 성능 측정 해야함

- 자바 코드는, 작성한 코드와 CPU의 명령 수행 사이의 추상화 격차가 커서 연산비용을 추측하기 어려움

- 시도한 코드가 더 악화시키거나, 미미할 때도 있음. 성능이 좋지 않은 부분은 추측하기가 어렵다. 느리다고 짐작한 원인이 틀릴 수도 있음. 

- 일반적으로 90%의 시간을 단 10%의 코드에서 사용한다는 것을 기억해두기

- 프로파일링 도구 사용 또는 jmh를 통한 자바코드 성능 검토도 좋은 방법임

 

(24.07.18) Item66: 네이티브 메서드는 신중히 사용하라. 

## 네이티브 메서드

다른 언어로 작성된 메서드를 자바 프로그램에서 부를 수 있다. 

## 네이티브 메서드의 쓰임

1) OS, 레지스트리같은 플랫폼 특화 기능 사용시

2) 다른 레거시 라이브러리 이용하고 싶을 때

3) 성능 개선할 영역을 네이티브 언어로 작성

(유의! 성능을 개선해주는 경우는 많지 않다)

(JVM도 가파르게 발전해옴 & 빠름)

## 심각한 단점

안전하지 않음. 이식성도 낮고.

가비지 컬렉터도 못쓰고. 추적도 불가능. (=> 메모리 훼손!)

자바코드와 네이티브 코드를 넘나들 때의 비용도 있고,

그 사이에 접착코드를 작성하는 것은 귀찮고 가독성 떨어짐!

 

(24.07.18) Item65: 리플렉션보다는 인터페이스를 사용하라. 

## java.lang.reflect (리플렉션 기능)을 통해 클래스의 생성자, 메서드, 필드 인스턴스를 가져올 수 있다.

- 컴파일때 없었던 클래스도 이용할 수 있음 => 컴파일시의 타입검사, 예외 관련 이점 X

- 지저분 and 성능 떨어짐

## 적절한 인터페이스(or 상위 클래스)로 참조해 사용할 수 있을 것임!

## 나중에 다시 보자 (복잡한 특수 시스템 개발시 강력하지만, 일반적으로는 사용하지 않고, 단점이 많다)

 

(24.04.16) Item64: 객체는 인터페이스를 사용해 참조하라. 

##클래스를 타입으로 사용 금지

리턴, 변수, 필드, 파라미터 타입 모두 해당. 인터페이스 타입으로 선언해라. 

##다만 구현체를 갈아끼울 때 조심하자.

예를들면 A클래스의 반복자의 특정 순회 정책에 따라 주변 코드가 동작하는 상황에서, B클래스로 갈아끼우면 순회 순서를 보장하지 않아 문제가 될 수 있다. 갈아끼울 구현체는 같은 기능을 제공하고 있어야 한다. 

## 만약 마땅한 인터페이스가 없다면 당연히 클래스로 참조해야 한다.

1) String, BigInteger 등 값 클래스는 더이상 확장할 일도 없으니 final인 경우도 많고.. 그냥 자연스럽게 이미 파라미터, 변수, 필드, 리턴 타입으로 사용하고 있을 것이다.

2) 클래스 기반으로 작성된 프레임워크들이 있다. OutputStream 등 java.io 패키지의 여러 클래스들이 그렇다. 그래도 최대한 기반 클래스(보통 추상 클래스임)을 타입으로 두는게 좋다.

3) 특별히 어떤 클래스의 추가적인 메서드를 "꼭" 사용해야 한다면 어쩔 수 없긴 하다. 그래도 최소화하자.

 

(24.04.15) Item63: 문자열 연결은 느리니 주의하라. 

##문자열연산 '+' 로 문자열 n개를 잇는 시간은 n^2

문자열은 불변이라서 2개를 잇는 경우 2개 문자열 내용을 다 복사해야해서 성능 저하가 있다.

String 대신 StringBuilder를 사용하면 좋다. 제곱과 선형의 시간차이가 크다! 참고로 StringBuilder를 전체 결과 크기만큼 키워놓고 사용하면 성능이 더 좋다. 

cf: 문자 배열을 사용하거나, 연결하지 않는 방안을 생각해볼 수도 있다.

 

(24.04.15) Item62: 다른 타입이 적절하다면 문자열 사용을 피하라.

##진짜 문자열 데이터일 때만 써라. 적절한 데이터 타입이 없다면 새로 작성하라.

##구분자로 구분된 데이터가 문자열로 들어올 때

구분자로 파싱하면 느리고 오류 가능성이 커진다. 그리고 그런 데이터는 String의 기능에만 의존하게 된다. equals, toString, compareTo 사용 불가하단 말이다. 그러니까 전용 클래스를 새로 작성하는게 좋다.

##권한 key를 문자열로 두지 말자. 보안에 취약하다.

스레드가 자신만의 변수(key)를 가지게 할 때, 클라이언트가 문자열 키를 주입해주는 방식으로 하면 위험하다. 서로 다른 클라이언트가 서로 다른 스레드에 우연히 동일한 키를 부여하게 될 수도 있다. 이 허점을 통한 보안 문제도 생긴다. 의도적으로 같은 키를 넣어서 다른 클라이언트의 값을 가져올 수도 있다. 문자열 말고, 위조할 수 없는 진정한 권한, 진정한 키를 사용해야 한다. 다음과 같이 ThreadLocal을 매개변수화타입(Item29)로 선언하면 깔끔하다.

// 문자열 기반 API의 문제를 해결
// 키 기반 API보다 빠르고 우아함 (별도의 Key를 멤버로 가질 필요가X) 
// java.lang.ThreadLocal과 흡사
public final class ThreadLocal<T> {
    public ThreadLocal();
    public void set(T value);
    public T get();
}

 

(24.04.14) Item61: 박싱된 기본 타입보다는 기본 타입을 사용하라

## 기본타입과 박싱타입: 최대한 기본타입을 쓰자.

기본타입 값은 언제나 유효하지만 박싱타입은 null을 가질 수 있고, 기본타입이 시간과 메모리 효율적임을 유의하자. 

박싱타입은 체감될 정도로 느릴 수 있고, 성능이 좋지 않다.

## 박싱타입 대소비교시 주의하자

박싱타입의 부등호 연산(>, <)시에는 기본타입 값으로 변환되어 시행된다. 그러나 ==에는 두 객체 참조의 동일성을 검사한다. 예를들어 아래와 같은 Comparator 구현시 주의하자.

// 잘못됨
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

// 옳음
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
    int i = iBoxed, j = jBoxed; //오토언박싱
    return i < j ? -1 : (i == j ? 0 : 1);
};

참고: 실무에서는 그냥 Comparator.naturalOrder()를 사용하면 된다.

 ## 기본타입과 박싱타입을 혼용한 연산에서는 언박싱된다.

그래서 박싱타입이 null 참조였다면 그런 연산 시점에 NPE가 터진다.

## 그럼 박싱타입은 언제 쓰나?

컬렉션, 제네릭에서 사용한다. 컬렉션은 기본타입을 담을 수 없기에 어쩔 수 없다. 제네릭도 마찬가지!

리플렉션을 통해 메서드를 호출할 때도 박싱된 타입을 작성해야 한다.

 

(24.04.13) Item60: 정확한 답이 필요하다면 float와 double은 피하라.

##금융 계산과는 맞지 않음: 10의 음의 거듭제곱수를 표현할 수 없음

금융 계산에는 BigDecimal, int, long을 사용 : int는 9자리 십진수 / long은 18자리 십진수 / 그 이상은 BigDecimal 꼬!

BigDecimal이 느리고 불편하다면? 성능이 중요하고 손수 소수점 다루기가 가능한 경우 int, long을 사용하자 (예를들어, 달러 계산을 할 때 소수점이 생긴다면, 센트 단위로 하면 된다.)

 

(24.04.09) Item59: 라이브러리를 익히고 사용하라.

##표준 라이브러리를 사용하라 (검증됨, 시간절약, 지속적 관리됨)

##최소 lang, util, io, collection, stream, concurrent 하위 패키지들에 익숙해져라

concurrent의 동시성기능은 멀티스레드 프로그래밍 작업을 단순화해주는 고수준의 편의기능을 제공한다. 또는 개발자가 자신만의 고수준 개념을 직접 구현할 수 있도록 저수준 요소들을 제공한다.

##major release마다 주목할만한 기능이 라이브러리에 추가되니 웹 들어가서 읽어보자. 

ex: 자바9에서는 지정한 URL의 내용을 가져오는 기능을 InputStream클래스의 transferTo()로 제공하는 등

 

(24.04.08) Item58: 전통적인 for문보다는 향상된for문을 사용하라.

##반복문에서 필요한게 원소 뿐이라면 향상된for문을 사용하라

- 성능 저하도 없고, 명료하고, 유연하고, 버그 예방

- 가능한 모든 곳에서 향상된for문을 사용하라

##향상된 for문을 사용할 수 없는 상황을 기억해두자

1) 파괴적인 필터링: 컬렉션 순회하며 원소제거하려면 Iterator의 remove()를 사용해야 한다. 근데 참고로 자바8부터는 명시적으로 순회하는 일을 피할 수 있다. 컬렉션의 removeIf() 메서드를 사용하면 된다.

2) 변형 (https://inkyu-yoon.github.io/docs/Language/Java/FailSafeFast)

리스트 or 배열을 순회하면서 원소의 값을 교체해야한다면, Iterator 또는 배열인덱스로 접근해야 한다.

왜냐하면 향상된 for문에서는 내부적으로 그 반복가능 객체의 자료구조가 구현한 Iterator 인터페이스의 메서드를 사용한다. 따라서 향상된 for문에서 내부적으로 Iterator를 통해 순환중일 때 데이터가 수정(추가, 삭제)되면 ConcurrentModificationException 에러가 난다. 컬렉션은 요소가 추가, 삭제될 때마다 증가하는 modCount 변수를 가지고 있다. iterator의 next()를 호출할 때마다 modCount 값의 변동을 확인하고, 다르면 에러를 발생시킨다. 참고로 remove, add, set, get, size 등 컬렉션 구현체의 수많은 메서드에도 아래 메서드를 호출한다. 

private void checkForComodification() {
    if (root.modCount != modCount)
        throw new ConcurrentModificationException();
}

3) 병렬 반복

여러 컬렉션을 병렬로 순회해야 한다면, 인덱스 기반으로 접근해야겠지!

##향상된for문은 Iterable 인터페이스를 구현한 객체를 순회할 수 있다.

- 내 객체가 원소묶음을 가지고 있어서 순회할 수 있는 형태라면, Iteratable를 구현하자. 그러면 그 객체를 사용하는 상황에서, 향상된 for문에 사용가능해서 편리할 것이다. 

- Iterable은 iterator() 메서드 단 하나만 가지고 있다. 

- 즉 Iterable을 implements하려면, iterator()을 구현해야 하는데, 이 iterator()가 리턴하는 것은 Iterator 객체 이다.

class Collection<T> implements Iterable<T> {
    ...
    @Override
    public Iterator<T> iterator() {
        return new Iterator<T>() {
            // hasNext(), next()를 Override
        }
}

- 참고로 어떤 Collection의 Iterator 인스턴스는 직접 new Iterator해서 생성하는 게 아니라 Collection 인스턴스의 iterator() 메서드를 통해 생성되어 리턴된다. Collection은 Iterable<E>를 상속받고 있고, 이것은 iterator() 구현을 강제한다. iterator()에서는 Iterator의 구현체를 생성하여 리턴하도록 되어있는데, 그런 Iterator는 내부 클래스로 존재하고, 이 Iterator는 컬렉션의 내부 클래스이기 때문에, 바깥 클래스의 데이터에 접근할 수 있다. 

 

(24.04.07) Item57: 지역변수의 범위를 최소화하라

가독성, 유지보수, 오류가능성 이점

##추천

- 가장 처음 쓰일 때 선언하기

- 선언과 동시에 초기화하기 (try-catch문 예외)

- 반복문 내에서만 변수를 사용할 거라면 while문 말고 for문 사용하기

- Iterator를 사용할 때의 관용구 예시: for-each문 말고 전통적 for문 사용한다.

for(Iterator<String> it = array.iterator(); it.hasNext(); ) {
    String s = it.next();
}

- 지역변수의 범위를 최소화하는 반복문 관용구 예시

// 반복 때마다 n을 다시 계산해야 하는 비용을 줄인다!
// 반복 때마다 같은 값을 반환하는 메서드를 매번 호출한다면 기억해둘 관용구다.
for (int i=0, n = expensiveComputation(); i < n; i++) {
    ....
}

- 메서드를 짧게 유지하고 하나의 기능에만 집중하면 지역변수 범위를 최소화 가능

 

(24.04.06) Item25: 톱레벨 클래스는 한 파일에 하나만 담으라. 

## 소스파일 하나에 톱레벨 클래스를 여럿 두면 위험함

- 컴파일 순서에 영향을 받게 된다.

- main()에서 Utensil과 Dessert를 순서대로 참조하고 있다고 하자.

그리고 Utensil.java에 Utensil, Dessert 클래스가 모두 정의되어 있다.

그리고 동시에 Dessert.java에도 마찬가지로 Utensil, Dessert 클래스가 모두 정의되어 있다.

이때 컴파일 명령 순서에 따라서 실행결과가 다르다.

첫번째, javac Main.java Dessert.java 명령이었을 경우, Main에서 Utensil 참조를 가장 먼저 만나서 Utensil.java를 살펴보고 Utensil과 Dessert 클래스 정의를 발견한다. 그 다음 명령 인수로 넘어온 Dessert.java를 처리할 차례인데, 앞서 같은 클래스를 찾아낸 이력이 있으므로 '클래스 중복 정의'를 잡아낼 수 있다.

두번째, javac Main.java 또는 javac Main.java Utensil.java 명령이었을 경우에는 중복정의에 걸리지 않으므로 에러가 나지 않는다.

세번째, javac Dessert.java Main.java 명령 순서로 컴파일하면, Dessert.java에 정의된 Utensil, Dessert의 내용을 사용하게 된다. 즉 Utensil.java에 정의된 Utensil, Dessert를 사용했을 때와 결과가 다르다는 것이다.

## 톱레벨 클래스는 서로 다른 소스파일로 분리하자.

## 굳이 하나의 파일에 담고 싶다면, 정적 멤버 클래스(Item24)를 사용하는 방법을 고려해라.

 

(24.04.06) Item24: 멤버 클래스는 되도록 static으로 만들라.

## 중첩 클래스: 다음의 네 가지 종류가 있다. 

1) 정적 멤버 클래스

-- ex -- Operation 열거 타입은 Calculator 클래스의 public 정적 멤버 클래스가 되어야 한다. 그러면 Calculator 클라이언트에서 Calculator.Operation.PLUS 등으로 참조 가능하다. 

-- private 정적 멤버 클래스는 바깥클래스의 구성요소를 나타낼 때 흔히 쓴다. 예를들어 Map 구현체는 각각의 키-값 쌍을 표현하는 Entry 객체들을 가지고 있고, 모든 Entry는 맵과 연관되어 있다. 그렇지만 Entry의 메서드(getKey, getValue, setValue)는 맵을 직접 사용하지 않는다. 즉 Entry가 바깥 맵으로 참조를 할 필요가 없다는 것이다. 그래서 Entry를 비정적 멤버로 두면 안된다.

2) 비정적 멤버 클래스 (inner class): 정적으로 할 수 있음 정적으로 해라!

-- 정적 멤버 클래스와 구문상의 차이는 없다. 그치만 바깥 인스턴스가 있어야하냐 없어야하냐의 차이가 크다. 그렇게 바깥 인스턴스 클래스와 비정적 멤버 클래스간 관계가 만들어지면, 비정적 멤버 클래스 안에 그 관계 정보(숨은 외부 참조)가 만들어진다. 그런 참조를 저장하려면 시간, 공간을 차지하는 건 당연하다.

-- 개념상으로 봤을 때 바깥과 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다.

-- 또는, 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 정적 멤버 클래스로 만들어야 한다.

-- 또 단점은, 가비지 컬렉션이 바깥클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수도 있다. (심각할 수 있음)

-- ex -- (어댑터 개념) Map 인터페이스의 구현체들을 생각해보면, keySet, entrySet, values 메서드가 반환하는 '자신의 컬렉션 뷰' (예: Set<Map.Entry<K, V>>)를 구현할 때 비정적 멤버 클래스(예: Entry)를 사용한다.

-- ex -- 또 흔한 예는 Set이나 List 등 컬렉션들의 '반복자' 이다. 반복자는 아래와 같이 private class (비정적)으로 선언되어서 바깥컬렉션의 요소를 순회하는 Iterator를 만들 것이다.

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

3) 익명 클래스 (inner class)

-- 선언한 지점에서만 인스턴스를 만들 수 있다. 이제 람다가 익명클래스 기능을 대신하고 있긴 하다.

-- 표현식 중간에 등장하므로 10줄이하로 짧게 작성해 가독성을 챙겨야  한다. 

4) 지역 클래스 (inner class)

-- 네가지 중첩 클래스 중 가장 드물게 사용된다. 지역변수랑 성격이 유사하다.

-- 익명클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있고, 정적 멤버는 가질 수 없으며, 가독성 위해 짧게 작성해야 한다. 

 

(24.04.05) Item23: 태그 달린 클래스보다는 클래스 계층구조를 활용하라.

## 태그 달린 클래스

현재 클래스가 어떤 상태인지 나타내려는 필드를 두지 말자. 나쁘다. (예를들이 class Figure가 어떤 Shape인지 표현하려는 상태 필드) 그걸 '태그 달린 클래스'라고 하는데 단점이 많다. 조건문같은 쓸데없는 코드가 많이 들어가 장황하여 수정이 어렵고 메모리를 먹는다. 안 사용할 경우까지 계속 필드로 가져가야 한다. 

## 클래스 계층구조를 활용하자 (subtyping이라고 함)

1. 계층구조의 루트클래스인 추상클래스를 정의한다.

- 태그에 따라 동작이 다른 메서드를 추상 메서드로 선언하자.

- 공통적인 건 일반 메서드와 필드로 추가한다.

2. 루트클래스를 확장한 구체클래스를 의미별로 하나씩 정의한다. 

예를 들어 Figure라는 추상클래스를 확장한 Circle 클래스와 Rectangle 클래스를 둔다.

각 구현체에는 각자가 필요한 데이터 필드를 따로 정의해주면 되겠지. 

=> 훨씬 유연하고, 자연스럽다!

*참고:  추상클래스와 인터페이스 사용처, 차이 비교

 

(24.04.05) Item22: 인터페이스는 타입을 정의하는 용도로만 사용하라. 

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

- 인터페이스를 타입으로 명시한다는 것은, 자신의 인스턴스인 구현체로 무엇을 할 수 있는지 클라이언트에게 말해주는 것이다. 그 용도 뿐이다.

- 상수 인터페이스는 메서드 없이 static final 필드로만 가득 찬 인터페이스인데, 안티패턴이다. 상수라는 것은 구현체의 내부 구현으로 두어야 한다. 그런 내부 구현을 API(인터페이스)로 노출하는 건 혼란을 주고 쓸데없이 클라이언트 코드가 상수들에 종속된다. 즉 상수를 안쓸거라도 상수 인터페이스를 구현하고 있어야 해서 앞으로 코드가 오염된다. 

## 그럼 상수는 공개하면 안되나요? 아뇨! 아래와 같은 방법. 

- 특정 클래스, 특정 인터페이스 자체에 상수를 추가하기

- ENUM 타입으로 만들어 공개하기

- 인스턴스화 할 수 없는 유틸리티 클래스에 static으로 담아 공개하기

 

(24.04.05) Item21: 인터페이스는 구현하는 쪽을 생각해 설계하라.

## 기존 인터페이스에 메서드를 추가하는 건 BAD..

인터페이스에 메서드를 추가하는건 당연히 기존 구현체에 컴파일 오류를 부른다. 디폴트 메서드를 '잘 삽입'하면 괜찮지만(이를테면 자바8에 컬렉션인터페이스들에 람다를 활용하기 위한 디폴트 메서드가 추가된 것처럼) 모든 상황에서 불변식을 해치지 않도록 삽입하긴 어렵다. 또는 디폴트 메서드의 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다 (앞서 말한 자바8 컬렉션 인터페이스에 많은 디폴트 메서드가 추가되고나서 많은 코드가 영향을 받았다. 아파치의 SynchronizedCollection 클래스는 자바8이후 추가된 removeIf 디폴트 메서드를 재정의하고 있지 않다. 다시말해서 removeIf의 구현은 동기화에 관해 아무것도 모르므로 락 객체를 사용할 수 없다. 따라서 SynchronizedCollection 인스턴스를 여러 스레드가 공유하는 환경에서, 한 스레드가 removeIf를 호출하면 런타임에러가 발생하거나 예기치못한 결과가 나온다.) 그렇게 기존 구현체들과 충돌할 수 있으므로 기존 인터페이스에 디폴트 메서드를 추가하는 것도 피해야 한다.

## 핵심: 인터페이스를 설계할 때는 '세심하게'

수많은 개발자가 구현체를 만들 것이다. 그래서 테스트를 할 때도 최소한 구현체 세개는 만들어봐야 하고, 다양한 작업에 활용하는 클라이언트도 여럿 만들어봐야 한다.

 

(24.03.31) Item20: 추상 클래스보다는 인터페이스를 우선하라.

## 인터페이스는 맘대로 mixed in 할 수 있지만, 추상클래스는 일단 단일상속만 된다는 것이 제약적이다.

## 계층구조가 아닌 관계는 인터페이스를 적용하자

Singer랑 Sonwriter는 계층관계가 아니다. 그럼 이 두개를 둘다 인터페이스로 두고(interface Singer, interface Songwriter) 이 둘을 모두 implements하는 클래스를 두면 된다. 더 나아가 좀 더 유연하게 만들고 싶다면, 두 인터페이스를 다중확장하는 제 3의 인터페이스를 다음과 같이 또 만들어둬도 된다.

public interface SingerSongwriter extends Singer, Songwriter {
    AudioClip strum();
    void actSensivive();
}

 

만약 상속을 통해 기능하나를 추가하려면 언젠가 필요 이상으로 많은 수의 클래스를 추가해야 할 수도 있는데, 이런 경우를 가리켜 '클래스 폭발' 또는 '조합의 폭발'이라고 부른다. 여러 기능을 조합해야하는 설계에 그렇게 상속을 이용하면, 모든 조합 가능한 경우마다 클래스를 추가해야 한다.

## 기능을 확장하려는 목적이라면, 래퍼클래스(재사용 가능한 전달클래스를 상속한 클래스였지)를 활용하여 유연하게 확장해보자.

굳이 상속을 통해 불필요한 기능을 확장하지 않아도 되고, 활용도를 떨어뜨릴 필요가 없다.

## 인터페이스 + 골격 구현 클래스 = 둘의 장점을 취한다! (템플릿 메서드 패턴)

- 인터페이스로는 타입을 정의한다. 골격구현클래스는 골격느낌으로 나머지 메서드들을 구현한다. 인터페이스명이 만약 Set이라면 골격구현클래스의 이름은 AbstractSet로 짓는다. 

- 템플릿 메서드 패턴이라고도 한다. 추상클래스에서는 '템플릿메서드'를 구현한다. 그 템플릿에서는 추상메서드들을 활용해서 전체적인 어떤 알고리즘이 구현되어있다는 것이다. 이 추상메서드는 구현체에서 따로 구현된다. 주의점은 템플릿을 구현하는게 아니다 템플릿은 구현체가 구현한 메서드를 사용하게되는 거지. 그니까 템플릿 메서드를 통해 골격(기본 로직)은 공통화하고, 클라이언트는 특정 부분만 재정의해서 사용하고자 하는 거다. 다시 말해서 클라이언트가 알고리즘의 특정 단계만 확장하고, 전체 알고리즘이나 구조자체는 확장하지 않는 것이다.

- 예를 들어 Student 인터페이스에서 eat(), study(), goHome(), dailyRoutine()을 가진다. 만약 각각의 학생 객체가 이 인터페이스를 직접 implements해서 구현해야 한다면, 4개의 메서드나 구현해야 한다. 그런데 골격화를 사용해서, 예를 들어 public abstract class CnuStudent라는 골격 클래스가 implements Students해서 eat()이나 goHome()이라는 기반(공통) 메서드를 정의하고 dailyRoutine()에서 eat(), study(), goHome()으로 순서가 정해진 알고리즘을 크게 정의해뒀다고 하자. 그럼 Cnu학생 객체는 이 골격 클래스를 extends하고 Students를 implements해서, 특정 메서드, 그러니까 여기선 study()만 재정의하면 된다. 그럼 골격 클래스 CnuStudent 에 의해 기반메서드나, dailyRoutine같은 템플릿은 내가 따로 정의 안해도 되는 간편함이 있다. 참고

 

 

(24.03.30) Item19: 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.

## 상속용 클래스는 꼭 문서에 명시해라

- 클래스에서 재정의 가능(final이 아닌 public, protected) 메서드를 호출할 수 있는 모든 상황을 문서에 남겨야 한다. 어떤 클래스의 메서드 내부에서 재정의가능 메서드를 호출하는 상황을 적시하라는 말이다. 어떤 순서로 호출하고, 호출 결과가 처리에 어떤 영향을 주는지 말이다.

- java.util.AbstractCollection의 remove(Object o)를 예로 들자. 해당 컬렉션의 iterator()를 통해 얻은 반복자(Iterator)를 통해 요소를 순회하면서, 그 Iterator의 remove를 사용한다. 다시 말해 AbstractCollection의 remove는 iterator의 remove를 사용한다. 

- 안좋은 예는, HashSet에서 add를 재정의하면 addAll에 영향을 줌을 addAll에 문서화해두지 않았던 예이다.

##  상속용 클래스에서 성능 관련 커스텀이 필요할 수 있는 부분은 protected로 두고 문서화해라

- List 구현체의 최종 사용자는 removeRange(int fromIndex, int toIndex)에 관심이 없을 테지만, 리스트 구현체에서 clear메서드에서 removeRange를 호출한다. 이 메서드를 재정의하면 clear 연산을 크게 개선할 수 있기에 '문서화되었다'

## 상속용 클래스는 배포 전에 꼭 구현체 만들어서 검증해야 한다.

## 상속용 클래스의 생성자는 절대 재정의 가능 메서드를 호출해서는 안된다.

- 상위클래스 생성자가 구현체의 생성자보다 먼저 수행되므로, 만약 상위클래스 생성자에서 구현체에서 재정의한 메서드를 호출해버리면 구현체의 생성자가 초기화하는 값을 못 사용한다.

- private, final, static 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출 가능

- 사실상 생성자인 (새로운 객체를 만드는) clone과 readObject에서도 마찬가지다. Cloneable과 Serializable 인터페이스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스에 안기는 제약도 상당하다.

## 상속용으로 설계하지 않은 클래스는 상속을 금지하자.

클래스를 final로 선언하기 / 생성자 모두를 외부에서 접근 불가하게 하기

## 클래스 내부에서 재정의가능 메서드를 아예 사용하지 않게 하는 방법도 방법이다. private 메서드에 그 기능을 구현하고, 재정의가능 메서드 내부에서 그 private 메서드를 호출하게 구현해두는 방법이다.

 

(24.03.27) Item18:  상속보다는 컴포지션을 사용하라

## 상속은 캡슐화를 깨뜨린다. (상위클래스의 변화에 종속됨)

순수하게 is-a, is kind of 관계일 때만 사용하라. has-a, is part of 관계라면 컴포지션을 사용하라.

문제점1) addAll과 add를 재정의할 때, 상위클래스의 addAll이 만약 add를 사용한다는 걸 모르고 내 addAll에서 카운트를 증가시키는 코드를 넣었다고 해보자. 여기서 의도치 않게 내가 재정의한 add도 호출되어서 카운팅이 두배로 될 수 있다.

문제점2) 상위클래스에 새로운 메서드가 추가되면, 내가 재정의하지 못한 그 메서드를 통한 취약점이 생길 수 있다. 구현체 목적에 맞게 다시 애써 재정의해야한다.

문제점3) 내가 구현체에서 정의한 메서드명이랑, 다음 릴리스에서 새로 정의된 상위클래스의 메서드가 우연히 시그니처가 같고 반환타입이 다르면 컴파일조차 되지 않는다. 완전히 같아서 재정의한 꼴이 된다고 해도 그 규약이 아닐테니 별로다.

## 컴포지션과 전달을 사용하자.

기존 클래스에게서 상속받지 말고, 기존 클래스를 private 필드로 참조해서 사용하기!

기존 클래스가 구현체의 구성요소가 되는 상태이므로 컴포지션이라 불린다.

구현체의 메서드가 기존 클래스의 메서드를 호출해서 쓰게 되므로, 전달 메서드라고 한다. 

이런 컴포지션의 장점은, 구현체가 기존클래스의 내부 구현에 영향받지 않는다는 것이다.

## 래퍼클래스(내 구현체에 기능을 덧씌우려는 의도, 데코레이터 패턴)

임의의 Set 구현체, 예를들면 HashSet 기능을 확장하고 싶다고 해서, HashSet을 extends하지 말자.

일단 interface Set을 구현한 전달클래스를 만들고 '래퍼클래스'를 만들어서 내 전달클래스를 extends하게 하자.

그럼 이 래퍼클래스는 HashSet같은 구현체를 extends해서 영향받는 상태가 아니라, 내 전달클래스를 extends해서 예측 가능한 상태다. 이제 내가 HashSet을 확장하려고 할 때, extends HashSet 이 아니라, 래퍼클래스를 통해 확장하면 된다. 래퍼클래스는 전달클래스를 가지고 있어서 안전하기 때문이다. 어떠한 set 구현체를 확장할 때 그 구현체를 재정의하는게 아니라 래퍼클래스에서 전달클래스를 재정의해서 쓰면 되는 것이다. 반면 직접 구현체를 extends하면 내가 만들지 않은 그 구현체의 기능에 끌려다닐 수 있다는 결론이다.

[생성의 유연함 예시1]
Set<Instant> times = new InstrumentedSet<>(new TreeSet(cmp));
[생성의 유연함 예시2]
Set<E> = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));

즉, 어떤 Set 구현체라도 나의 InstrumentedSet 기능을 가지도록 안전하게 확장할 수 있다는 말이다.

래퍼 클래스는 단점이 거의 없다 (전달메서드, 래퍼객체는 성능에 영향 X) 전달 클래스를 만드는게 지루하긴 하지만, 인터페이스 하나당 하나씩만 만들어두면, 원하는 기능을 마음껏 안전히 덧씌울 수 있다.

 

 

(24.03.26) Item17:  접근 가능성을 최소화하라

## 클래스를 불변으로 만들기

- 변경자 제공 금지

- 클래스 확장 금지

- 합당한 이유가 없다면 모든 필드는 private final 

- 확실한 이유가 없다면 생성자와 정적팩터리 외에는 어떤 초기화 메서드도 public으로 두지 말아야 함. 초기화가 완벽히 끝난 상태의 객체를 생성해야 함. 상태를 다시 초기화하는 메서드를 두면 안됨. 복잡성만 커지고 성능 이점 없음.

(ex: CountDownLatch는 카운트가 0에 달하면 더는 재사용 불가)

- 기본 타입 필드나 불변 객체를 참조하는 필드를 public final로만 선언해도 불변 객체가 되지만, 이렇게 하면 다음 릴리스에서 '내부 표현을 바꾸지 못하므로' 권하지는 않는다.

## 함수형 프로그래밍 방식은 코드에서 불변이 되는 부분이 많아진다.

- Complex 클래스에서 사칙연산 메서드(plus, minus, mines, dividedBy)들에 반환할 때, 자신의 필드를 수정하는 게 아니라 new Complex()로 이루어졌다. 

- 함수형 프로그래밍: 피연산자에 함수를 적용해 그 결과를 반환하지만 피연산자 자체는 그대로인 프로그래밍 패턴

## 불변 객체는 안심 공유 가능: 만든 인스턴스는 최대한 재활용하기

- 정적 팩터리를 통해, 자주 사용하는 인스턴스를 캐싱하여 중복생성을 하지 않아도 됨. 메모리 사용량과 가비지 컬렉션 비용이 줄어든다.

- 실패 원자성을 제공

## 불변 객체는 Map의 key, Set의 원소로 쓰기 딱좋다. 불변식 깨질 일 없음.

## 단점: 원하는 객체 완성까지 단계가 많고, 중간단계 객체들이 모두 버려질 때의 성능문제

해결법 두가지

1. ?흔히 쓰일 다단계 연산들을 예측하여 기본 기능으로 제공

2. ?불변 클래스와 쌍을 이루는 가변 동반 클래스를 public 클래스로 제공

## 불변식을 지키기 위해서는 클래스 확장 금지

- final class 선언

- 생성자를 (package-)private로 만들고, public 정적 팩터리를 제공 (최선인 경우가 많음) : public, protected 생성자가 없기 때문에 다른 패키지에서 이 클래스 확장하는 것은 불가함

## 신뢰불가능한 클라이언트가 BigInteger나 BigDecimal을 인수로 주면 방어적복사하기

## 계산 비용이 큰 값을 처음사용시점에 계산하여 final이 아닌 필드에 캐시해둘 수 있다

 

(24.03.25) Item16:  public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라.

- public 클래스에서는 필드를 노출(public)하면 캡슐화가 깨진다. 가변이 아닌 불변 필드는 public이어도 불변식은 보장하겠지만, 굳이 직접 노출을 할 필요가 없다. 차라리 의미를 담아 접근자를 두는 것이 낫다.

- 그러나 package-private 또는 private 클래스는 불변이든 가변이든 노출하는 것이 문제가 없으며, 더 나을 수도 있다. 어차피 클라이언트는 이 패키지 내부에 존재한다. 코드도 접근자를 두는 것보다 깔끔할 수도 있다.

 

 

(24.03.24) Item15: 클래스와 멤버의 접근 권한을 최소화하라

## 내부구현과 API를 완벽히 분리: 캡슐화

- 의존성 없이 최적화할 컴포넌트만 딱 최적화

- 전체 시스템이 완성되지 않아도 동작 검증

## 은닉의 핵심: 접근 제한

- 가능한 한 좁혀라

- public 클래스는 그 패키지의 API다. 패키지 외부에서 쓸 이유가 없다면 public이 아닌 package-private으로 선언한다. 그러면 클라이언트에 피해 없이 언제든 내부 구현 수정 가능. 중요한 일이다.

- 한 클래스에서만 사용하는 건 private static으로 중첩시킨다면 바깥 클래스 하나에서만 접근할 수 있다. 

- 공개 API 빼고 모든 멤버를 private으로 하고, 같은 패키지의 다른 클래스가 접근해야 한다면 그때 private 떼주면 된다.

- protected 멤버 수는 적을수록 좋다. 접근 범위가 아주 넓어진다. 게다가 public 클래스의 protected 멤버는 공개 API이게 된다.

- 접근성을 맘대로 좁히지 못하는 경우도 있다. 클래스를 재정의할 때 or 인터페이스 구현할 때다. 리스코프 치환 원칙에 의해 접근제한을 좁히진 못한다.

- 클래스가 public 필드(특히 가변필드)를 가진다면 스레드 안전하지 않다.

- public static final 상수는 괜찮다. (단 기본타입이거나, 불변객체를 참조해야 하죠!) 

- 길이 0아닌 배열은 모두 가변임. public static final로 두면 안됨. 접근자 제공해서도 안됨.

[대안1]
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

[대안2]
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}

 

 

 

(24.03.23) Item56: 옵셔널 반환은 신중히 하라

## 값을 반환할 수 없을때 예외나 null을 던지는 행위는 좀...

예외는 진짜 예외상황에서만 사용해야 하고, null 반환의 문제는 Item54에서 얘기했다.  

## 옵셔널

- 옵셔널은 원소를 담거나 담지않거나 (즉 1개 또는 0개 원소) 불변컬렉션이다.

- 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.

- 컴파일시 잡을 수 있는 Checked Exception과 취지가 비슷하다. 대처 코드를 반드시 작성해넣게됨. 이는 즉 반환값 존재 여부를 API 사용자에게 명확히 알려주는 것임. 예를들면 orElseThrow로 예외를 던질 때 반드시 대처 코드를 넣도록 컴파일시 잡아준다.

- 옵셔널도 새로 할당하고 초기화해야 하는 객체이다보니 성능 저하가 뒤따르기는 함. 성능에 민감하다면 null 반환이나 예외던지기가 나을 수도.

## 예시: 컬렉션 반환하는 메서드에서 반환값

컬렉션이 비었을 경우,

throw new Exception("빈 컬렉션")  vs  return Optional.empty()

## null을 허용하는, 허용하지 않는 옵셔널

Optional.ofNullable(value)  vs  Optional.of(value)는 NPE

## 스트림의 종단 연산 중 상당수가 옵셔널을 반환한다.

## 컬렉션, 스트림, 배열, 옵셔널같은 컨테이너 타입은 또 옵셔널로 감싸지 말아라. 그러니까 Optional<List<T>>를 반환하지 말고 빈 List<T>를 반환하자.

## 박싱된 기본타입 옵셔널은 지양하자. OptionalInt, OptionalLong, OptionalDouble 같은 거.

 

 

 

(24.03.22) Item54: null이 아닌, 빈 컬렉션이나 배열을 반환하다.

## 받는쪽에서 null을 방어하게 하지 말자 (복잡, 피곤)

컬렉션이 비었어도 null을 반환하지 말고, 비어있는 컬렉션을 반환하자.

때때로 빈 컬렉션(new ArrayList<>(list)같은) 할당이  사용 패턴에 따라 성능을 크게 저하시킬 수 있는데, 그런 것이 확인되었을 경우에는 비어있는 불변 컬렉션을 반환해주자. (성능 저하의 주범인 게 확인되었을 때 얘기다!)

Collections.emptyList(), Collections.emptySet(), Collections.emptyMap()

배열도 마찬가지로 길이 0인 배열을 반환하면 된다. 참고로 길이가 0인 배열은 모두 불변이다.

불변 객체는 자유롭게 써도 안전하다 

## 리스트.toArray(배열) 에서 배열을 미리 할당하진 말자.

return cheesesInStock.toArray(new Cheese[0])이나

return cheesesInStock.toArray(new Cheese[chessesInStock.size()])나 

결과가 같다고 해서 후자처럼 cheesesInStock.size()만큼의 배열을 미리 할당하는 것은   성능이 떨어진다는 연구 결과가 있으므로 추천하지 않는다.

 

 

(24.03.22) Item53: 가변인수는 신중히 사용하라

## 인수가 최소 1개여야 한다면, 그 하나는 꼭 들어오게 보장해줘야 한다.

즉 파라미터를 (int... args)가 아니라, (int firstArg, int... remainingArgs)로 둬야 한다.

참고로 printf()는 가변인수 덕을 톡톡히 보고 있는 예시다.

## 가변인수는 매번 새로 배열을 할당하기 때문에 성능에 유의하자.

메서드 호출의 95%가 인수를 3개 이하로 사용하고 있다면 차라리 인수 개수마다 오버로딩 메서드를 두자.

EnumSet의 정적 팩터리가 그렇게 생성비용을 최소화하고 있다.

EnumSet의 정적 팩터리를 확인해봅시다.
public static <E extends Enum<E>> EnumSet<E> of(E e)
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2)
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)

인자가 5개까지 들어갈 수 있도록 오버로딩 되어 있고,
6개부터는 가변인자를 이용하여 오버로딩됨
@SafeVarargs 
public static <E extends Enum<E>> EnumSet<E> of​(E first, E... rest)

 

 

(24.03.21) Item52: 다중정의는 신중히 사용하라

## 파라미터 수가 같을 때는 다중정의를 피하라 
어떤 오버로딩 메서드를 호출할지는 컴파일에 결정되어서 의도치 않은 결과를 볼 위험이 있다.
다만 파라미터 타입이 근본적으로 다르면 괜찮다.
## 공개 API는 특히 오버로딩 메서드를 두지 말자.
사용자가 매개변수를 넘기면서 어떤 오버로딩 메서드가 호출될지 모른다.
## 생성자의 다중정의
대안으로 정적팩터리가 있다.
## [기본vs참조 타입] 오토박싱에서의 혼동
Object와 int가 근본적으로 다르지 않다
예를들어 List 인터페이스의 remove는 취약해졌다. remove(Object element)와 remove(int index)가 있는데, 정수 요소를 지우려고 int를 넣은 경우 인덱스를 지워버릴 수 있다.
## 람다와 메서드 참조
1) 부정확한 메서드 참조 문제(inexact method reference) 컴파일에러
아래 코드는 둘 다 스레드를 할당해 콘솔 출력을 하는 코드인데 아래꺼에서 컴파일에러가 난다.

1)
new Thread(System.out::println).start()
2)
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);

println()과 submit()메서드가 모두 다중정의가 되어있기 때문이다. 다중정의 해소 알고리즘에서 정상적으로 다중정의 메서드를 선택할 수 없기 때문에 생기는 문제라고 한다. 잘 모르겠다. 
2) 함수형 인터페이스는 같은 위치의 파라미터로 받아서는 안 된다. 함수형 인터페이스(람다식)는 근본적으로 다르지 않다.
 
 

(24.03.21) Item51: 메서드 시그나처를 신중히 설계하라

## 메서드 시그니처란?
메서드 이름, 파라미터 개수, 순서, 타입을 의미
리턴값이나 예외등은 포함하지 않다.
## API 설계 요령 (사용쉽고 오류 적은 API 만들기!)
1. 메서드 이름: 표준 명명규칙 따라 짓기, 같은 패키지 내에서 일관된 이름들로 짓기
2. 메서드는 '기능' 단위에 충실하기. 편의 메서드는 아주 자주 쓰인다고 확신할  때만 두기. 
3. 매개변수는 4개 이하가 좋다. 기억하기 쉽다는 점에서.
    ㄴ같은 타입 매개변수가 연속으로 있는건 해롭다!  그대로 컴파일되고 실행되어버림.
    ㄴ 매개변수 짧게 하기: 여러 메서드로 쪼개기, 도우미 클래스 만들기,
         매개변수가 많은데 당장은 일부만 쓴다면 빌더패턴  써서 필요한거 세팅한 뒤 넘겨주기
4. 매개변수 타입은 인터페이스로 두자. 특정 구현체가 아닌.
    ㄴ 메서드에 HashMap을 넘길 일은 전혀 없다. Map을 사용하라.
5. 명확한 경우 빼고는 true/false보다는 enum이 낫다. (가독성, 확장성)
    ㄴ double값을 받아 섭씨/절대/캘빈온도로 변환해주는 메서드를 각각의 enum 타입 상수에 정의해두면 좋다.
 
 

(24.03.21) Item50: 적시에 방어적 복사본을 만들라

## 개요
객체(클래스)의 허락 없이는 객체가 수정되면 안된다.
즉 내가 코딩할때 객체의 불변식을 깨뜨리면 안되므로, 방어적으로 프로그래밍해야 한다.
1) 객체를 설계할 때는 불변식을 지키자.
2) 객체를 사용할 때는 그 객체의 불변식을 깨뜨리지 말자.
## 생성자, 메서드: 외부의 가변타입을 받아올 때 그대로 받아오지 말자
1) 가변 클래스를 필드로 가지면 클래스의 불변식을 깨뜨릴 수 있다.
예를들면 Period라는 클래가 필드로 가변 클래스인 Date를 가지고 있다면,
외부에서 악의적인 Date가 들어와서 나중에 변경됨으로써 Period의 불변식을 깨뜨릴 수 있다.
2) 생성자에서 Date를 받을 때 방어적 복사를 하자. 
단, 유효성 검사보다 방어적 복사를 먼저 하자. 유효성 검사를 먼저하면, 검사 후 악의적으로  변동될 수 있다. 그래서  일단 복사해두고나서 복사본을 통해 유효성을 검사한다. 
3) 가변타입 복사시 clone()이 재정의되었을 위험을 생각하며, clone 사용에 주의하자.
4) 생성자 말고 일반 메서드도 같은 맥락.
## 접근자: 내 가변 필드의 수정가능성을 막자
1) 접근자에서 복사본을 리턴하도록 한다.
2) 접근자에서 리턴시엔 clone()으로 리턴해도 된다. 내 필드는 신뢰할 수 있어서다.
즉 java.util.Date인 것이 확실하고, 이상하게 재정의한 하위클래스가 아님. 
## 방어적 복사는 비용이 크다.
신뢰한다면 안해도 되겠지. 또는 수정의 영향이 오직 클라이언트에게만 간다면 상관없다. 예를들면 래퍼클래스. 

반응형