이펙티브 자바 1

Seung jun Cha·2023년 1월 19일
0

1. 정적 팩토리 메서드

1-1 장점

  1. 이름을 가질 수 있다 : 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다.
    또한 동일한 시그니처의 생성자를 두 개이상 가질 수 없으므로, 한 클래스에서 시그니처가 같은 생성자가 여러 개 필요할 것 같으면 생성자를 정적 팩토리 메서드로 바꾸고 이름을 잘 지어주자.
 public static Order primeOrder(Product product) {
        Order order = new Order();
        order.prime = true;
        order.product = product;

        return order;
    }

    public static Order urgentOrder(Product product) {
        Order order = new Order();
        order.urgent = true;
        order.product = product;
        return order;
    }
  1. 호출될 때마다 인스턴스를 생성하지 않아도 된다 : 자바의 생성자는 호출될 때마다 새로운 객체를 만드는데, 정적-는 반복되는 요청에 같은 객체를 반환한다. 싱글톤으로 만들 수도, 인스턴스화 불가로 만들 수도 있다.
private Settings() {}

private static final Settings SETTINGS = new Settings();

public static Settings getInstance() {
        return SETTINGS;
    }
  1. 반환타입의 하위타입객체를 반환할 수 있다 따라서 하위타입이기만 하면 따라 다른 클래스의 객체를 반환할 수 있다 즉, 반환타입은 인터페이스나 클래스로 하고 실제 반환은 인터페이스나 상위클래스의 하위 객체를 반환활 수 있으므로 굉장히 유연하다.
 static HelloService of(String lang){  
 // 이 메서드를 인터페이스에 선언할 수 있고
        if (lang.equals("ko")){
            return new KoreanHelloService();
            // 다른 객체를 반환할 수 있다.
        }else{
            return new EnglishHelloService();
        }
    }
  1. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다. 인터페이스만 존재하고 인터페이스의 구현체가 존재하지 않아도 된다. 아직 어려운 부분이므로 나중에 필요할 때 찾아보자
ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
        Optional<HelloService> helloServiceOptional = loader.findFirst();
        helloServiceOptional.ifPresent(h -> {
            System.out.println(h.hello());
        });

1-2 공략1 열거타입

  1. 애플리케이션 전체에서 딱 1개만 만들어진다. 따라서 동일성을 비교할 때 ==를 사용할 수 있으며 equals()보다 권장된다. equals()는 nullPointException이 발생할 수 있기 때문이다.
  2. enum 클래스가 가지고 있는 모든 값을 가지고 오는 방법
enum클래스.values(); 모든 객체를 배열로 만들어서 리턴
valueOf() : 주어지는 문자열과 동일한 문자열을 가지는 열거 객체를 리턴한다.
이 메소드는 외부로부터 문자열을 입력받아 열거 객체로 변환할 때 유용하다.
  1. EnumMap<> 와 EnumSet<>를 사용할 수 있다
Map<Role, String> map = new EnumMap<Role, String>(Role.class);
  • Map과의 차이점
    EnumMap은 내부에 데이터를 Array에 저장하기 때문에 map과 달리 순서가 보장되며 해시를 만들고 해시충돌을 대비하는 작업이 필요 없어지게 된다. 그리고 HashMap는 일정 이상의 자료가 저장되면 자동으로 resizing하지만 EnumMap는 처음부터 사이즈가 enum으로 제한되기 떄문에 문제가 발생할 일이 없다

1-3 인터페이스에 정적메서드

  • 인터페이스가 기본 메소드 (default method)와 정적 메소드를 가질 수 있다
  • 기본 메소드
    • 인터페이스에서 메소드 선언 뿐 아니라, 기본적인 구현체까지 제공할 수 있다.
    • 기존의 인터페이스를 구현하는 클래스에 새로운 기능을 추가할 수 있다.
  • 정적 메소드
    • 자바 9부터 private static 메소드도 가질 수 있다

2. 빌드패턴

  • 생성자에 매개변수가 많다면 빌드패턴을 고려하라
  1. 생성자 체이닝 : this()를 이용하여 같은 클래스 내의 생성자를 호출

  2. 자바빈즈 패턴 : setter메서드를 사용, 필수로 설정되어야 하는 값이 설정되지 않아 불완전한 객체가 생성될 수 있다.
    그리고 set을 사용하다보니 불변객체를 만들기 어렵다

  3. 빌더패턴 : @Builder을 사용하면 자동으로 모든 파라미터를 받는 생성자가 생성된다. 외부에 생성자를 노출하지 않고 빌더만을 사용하여 객체를 생성하고 싶으면 @AllArgsConstructor(access=) 레벨을 설정하면 된다.
    @Builder을 사용하는 경우 객체 생성시 반드시 들어가야하는 필수값을 설정하는 방법이 없다. 필수값을 설정해야하는 경우 직접 빌더패턴 코드를 작성하는 것이 더 나을 수 있다.

2-1 자바빈

  • 자바16부터 제공하는 record 클래스도 추가로 공부하자
  1. 필드, 생성자, toString(), equals(), hashCode() 메소드 등을 자동으로 생성해 준다. 컴파일 타임에 컴파일러가 코드를 추가해주기 때문이다.
  2. 자동으로 private final로 선언된다.
  3. record 클래스는 final 클래스이라 상속할 수 없다.
  4. 각 필드의 getter는 getXXX()가 아닌, 필드명을 딴 getter가 생성된다.
  5. Record는 extends를 사용하여 다른 클래스를 상속할 수 없다. 모든 Record는 java.lang.Record를 암묵적으로 상속한다.
  6. 필드에 직접 접근하는 것이 불가능하고, 접근자 메서드를 통해 접근이 가능하다.
public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);

        // 필드는 직접 접근할 수 없지만, 접근자 메소드를 통해 값을 얻을 수 있습니다.
        String name = person.name(); // "Alice"
        int age = person.age();     // 30

        // 필드에 대한 직접적인 접근은 불가능합니다.
        // String name = person.name; // 컴파일 에러
    }
}

2-2 illegalargumentexception

  • 잘못된 인자를 넘겨받았을 때 발생하는 unchecked Exception

2-3 가변인자

  • 같은 타입의 매개변수가 여러개 전달될 때 사용할 수 있는 표현
    반환타입은 배열이며 하나의 메서드에 2개 이상의 가변인자가 들어갈 수 없다.
public void variable(String... s) {
		System.out.println(s);
	}

3. 싱글톤 보장

  • 인터페이스를 구현한 Singleton 객체가 아니라면 mock 객체를 만들 수 없어 이를 사용하는 클라이언트를 테스트하기 어려워 질 수 있다.

3-1 생성자 사용

3-1-1 public static final

  • 이 방법은 리플렉션과 직렬화, 역직렬화 방법에 의해 싱글톤이 깨질 수 있다.
    리플렉션을 방어하는 방법은 다음과 같다.
public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
    boolean created = false;
    
    private Elvis() { 
          if(created) {
            throw new RuntimeException("Can't create Constructor");
        }    // 처음 인스턴스가 생성될 때는 그냥 지나간다.
         created = true;
    }

두번째는 역직렬화를 할 때 싱글톤에 문제가 생긴다. 직렬화로 객체의 정보를 어딘가에 저장하고 역직렬화로 저장된 객체정보를 가지고 오는데 역직렬화를 할 때 새로운 객체가 생성이 되어 싱글톤이 깨지게 된다. 이 문제를 막기 위해 readResolve 메서드를 제공해야한다.

 private Object readResolve() {
    // 역직렬화가 되어 새로운 인스턴스가 생성되더라도 INSTANCE를 반환하여 싱글턴 보장
    // 새로운 인스턴스는 GC에 의해 UnReachable 형태로 판별되어 제거
          return INSTANCE;
    }

3-2 enum 사용

  • 개발자가 별다른 코드를 작성하지 않아도 리플렉션과 역직렬화에 모두 안전하다. 가장 안전하면서 쉬운 방법이지만 싱글톤이 enum외의 클래스를 상속해야하는 경우에는 이 방법을 사용할 수 없다.
public enum Elvis {
      INSTANCE;
      
      싱글톤으로 만들 Elvis가 사용할 메서드들을 여기에 작성
}

3-3 메서드 참조

  • 람다식에서 하는 일이 메서드 하나만 호출하는 일이라면class::methodName 구문을 사용하여 클래스 또는 객체에서 메서드를 참조

3-3-1 static 메서드

public class MathUtils {
  public static int AddElement(int x, int y) {
    return x + y;
  }
}

IAdd addLambda = (x, y) -> MathUtils.AddElement(x, y);
IAdd addMethodRef = MathUtils::AddElement;

3-3-2 인스턴스 메서드

public class MathUtils {
  public int AddElement(int x, int y) {
    return x + y;
  }
}
 MathUtils mu = new MathUtils();

IAdd addLambda = (x, y) -> mu.AddElement(x, y);
IAdd addMethodRef = mu::AddElement;

3-3-3 특정 타입(클래스)에 대한 인스턴스 메서드에 대한 메서드 참조

String[] strArr = {"a", "B", "e", "c", "D"};

Arrays.sort(strArr, new Comparator<String>() {

  @Override
  public int compare(String s1, String s2) {
    return s1.compareToIgnoreCase(s2);
  }
});

Comparator 객체는 함수형 인터페이스를 구현한 클래스와 유사합니다. 그러므로 람다식으로 변경할 수 있습니다.

Arrays.sort(strArr, (s1, s2) -> s1.compareToIgnoreCase(s2));

Arrays.sort(strArr, String::compareToIgnoreCase);

3-3-4 생성자 참조

dates.stream().map( 
  d -> return new Person(d); // 이 경우에도 가능
).collect(Collectors.toList());

dates.stream().map( 
 Person::new // stream에서 전달받은 값을 사용해서 객체 생성
).collect(Collectors.toList());

생성자가 여러 개일 때 어떤 생성자를 메서드 참조하는지 어떻게 알 수 있을까?

3-4 함수형 인터페이스

  • 람다식과 메서드 참조에 대한 타겟타입을 제공한다. java.utill.function패키지에 정의되어 있는데 직접 정의를 하고 싶으면 인터페이스에 하나의 메서드만 정의하면 함수형 인터페이스로 정의된다. 우선 자바에서 제공하는 기본 함수형 인터페이스를 알아야하는데 그 중 가장 중요한 4개를 공부한다

3-4-1 Function, Supplier, Consumer, Predicate

  1. Function<Input, ouput> : 파라미터가 있고 결과값을 리턴한다
    ex) (i) -> "aaa";
    객체를 생성하는데 생성자에 파라미터가 있다면 Function<LocalDate, Person> = Person::new

  2. Supplier<output> : 파라미터는 없지만 return이 있음
    ex) Person::new

  3. Consumer<input> : 파라미터를 받지만 리턴값이 없다

  4. Predicate<input> : 파라미터를 받고 boolean을 리턴한다

3-5 객체 직렬화

  • 객체를 바이트스트림으로 변환하는 기술로 바이트스트림으로 변환한 객체를 파일로 저장하거나 다른 시스템으로 전송한다. 객체를 받는 곳이 JVM이라면 직렬화도 괜찮은 선택이지만 그게 아니라면 역직렬화를 하지못하므로 직렬화는 무의미하다. 그래서 요즘에 xml이나 JSON을 많이 사용한다

4. private 생성자

  • 클래스를 abstract로 만들더라도 해당하는 추상클래스를 상속 받는 자식클래스에서 부모 생성자를 호출하여 객체를 만드는 것이 가능하므로 인스턴스화를 막기 위해서 abstract를 선언하는 것은 부적절하다.
    인스턴스화를 막기위해서는 private 생성자를 만들고 private 생성자가 있는 클래스 내에서도 해당 인스턴스의 생성을 막으려면
    throw new AssertionError(); 코드를 만들어 두고 주석으로 설명을 쓰는 것이 좋다
private ImageUtility() {
        throw new AssertionError();
    }

5. 의존객체 주입

  • 사용하는 자원에 따라 동작이 달라지는 클래스는 자원을 직접 명시하는 것이 아닌 의존 객체주입을 사용하는 것이 적절하다. 여기서 의존객체주입이란 인스턴스를 생성할 때 필요한 자원을 넘겨주는 방식을 말한다. 사용하는 자원을 직접 명시한다는 것은 new 같은 경우를 말한다.
  • 팩토리 메서드 패턴

6. 불필요한 객체생성 피하기

6-1 문자열

  • JVM은 생성된 문자열을 저장해 놓고 있다가 같은 문자열이 사용될 때 참조한다

6-2 정규표현식

  • String.matches 메서드 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려져 곧 바로 가비지 컬렉션 대상이 된다. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들어 인스턴스 생성 비용이 높다. 이렇게 생성 비용이 많이 드는 객체가 반복해서 필요하다면, 캐싱하여 재사용하는 것을 권장한다.
static boolean isRomanNumeral(String s){
  return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
  //string.matches 메서드는 내부에서 정규표현식용 Pattern 인스턴스를 만들고 한 번 쓰고 버려져 곧 바로 가비지 컬렉션 대상이 된다.
}

// string.matches 메서드로 아래의 Pattern 객체가 만들어진다.
 public boolean matches(String regex) {
        return Pattern.matches(regex, this);
       이렇게 정규표현식 인증에 사용하는 pattern 객체는 생성에 많은 비용이 든다.
    }
  • 동일한 패턴의 정규식이 자주 사용된다면, 인스턴스를 클래스 초기화 과정에서 static finalPattern객체를 직접 생성해 캐싱해두고, 나중에 isRomanNumeral 메서드 호출을 통해 이 인스턴스를 재사용하여 성능을 개선할 수 있다.
 private static final Pattern ROMAN 
 = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

  static boolean isRomanNumeral(String s) {
    return ROMAN.matcher(s).matches();
  }
  • 사용할 때마다 Pattern 객체가 만들어지는 메서드
  1. split : 문자 하나(ex ",")를 기준으로 split을 할 때는 빠르다. 따라서 미리 컴파일해서 쓸 필요가 없지 않고 그냥 split메서드를 사용하는 것이 좋을 수 있다. 한 글자 이상일 경우 미리 컴파일해서 재사용하는 것이 좋다.
    대안) Pattern.compile(regex).split(str)
  2. replace
a.replace(".", "/"); 
.을 /로 바꾸어준다
  1. replaceFirst
a.replace(".", "/"); 
처음 나오는 .을 /로 바꾸어준다

Pattern.compile(regex).matcher(str).replaceFirst(repl)
4. replaceAll
Pattern.compile(regex).matcher(str).replaceAll(repl)
5. match

6-3 오토박싱, 언박싱

  • 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의해야 한다.

6-4 가비지 컬렉션(GC)

1. 개념

  1. mark : 사용중인 인스턴스인지 아닌지 체크
  2. sweep : 사용 중이지 않은 인스턴스를 정리
  3. compact : 파편화 되어있는 메모리들을 모아서 정리한다(디스크 조각모음 같은 것)
  4. Young Generation(Eden, S0, S1) : Eden, S0, S1를 사용해서 사용하지 않는 인스턴스를 계속 정리하다가 오래 살아남는 인스턴스를 Old Generation로 옮긴다. (=> Minor GC)
  5. Old Generation : 오래 살아남는 객체가 관리되는 곳 (Full GC)

2. 공부해야할 것

  1. throughput : GC별 리소스 사용량
  2. Latency(Stop-THe-World) : GC가 작동하는 동안 애플리케이션의 작동이 멈추는 시간
  3. Footprint : GC알고리즘 때문에 사용해야하는 메모리 공간
  4. JVM 버전마다 기본적으로 사용하는 GC가 다르다. 물론 다른걸로 설정할 수 있음 (Serial, Parallel, CMS, G1, ZGC, Shenandoah)

7. 다 쓴 객체의 참조해제

7-1 핵심정리

// 공간이 부족할 때 스택을 확장한다
 private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
    
     public void push(Object e) {
        ensureCapacity();
        elements[size++] = 0;
    }
	// 잘못된 코드
    // pop으로 객체가 stack에서 나와도 스택이 객체를 참조하고 있다.
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        return elements[--size];
    }
  • Collection 클래스 안에 담겨있는 인스턴스는 프로그램에서 사용여부와 관계 없이 모두 사용되는 것으로 판단되어 GC의 대상이 되지 않아 메모리 누수의 흔한 원인이 된다. 객체 참조 하나를 살려두면 GC는 그 객체뿐만 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다. 따라서 다음처럼 null을 삽입하여 참조를 해제해야 한다.
public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }

메모리 누수의 문제가 되는 경우는 List, 배열, Map 등의 컬렉션, 캐시, 리스너인데 모두 객체를 쌓아 놓는 경우이다.

캐시의 경우 WeakHashMap 을 사용해 볼 수 있다. HashMap의 경우 해당 객체가 사라지더라도 GC대상으로 잡지 못하여 컬렉션에 쌓여, 메모리 누수의 원인이 된다.
반면에 WeakHashMap은 WeakHashMap에 있는 Key값이 더이상 사용되지 않는다고 판단되면 다음 GC때 해당 Key, Value 쌍을 제거한다. 임의로 제거되어도 상관없는 데이터들을 위해 주로 사용된다.
또 주기적으로 캐시를 정리하는 백그라운드 쓰레드를 활용하는 방법이 있다.

7-2 NullPointException

  • 처리방법
  1. 예외를 던진다 (throw)
  2. null을 return 한다.
  3. Optional을 사용한다 : 가급적 Optional에서 제공하는 메서드를 사용한다.
    • 매개변수 타입, 맵의 키 타입, 인스턴스의 필드 타입으로 사용하지 말 것
    • Optional을 리턴하는 메서드에서 null을 리턴하지 말 것
      (값이 없다면 Optional.empty 로 리턴)
    • Collection을 Optional로 감싸지 말 것

7-3 WeakHashMap

  • 더 이상 사용하지 않는 객체를 GC가 자동으로 삭제해주는 Map
  1. value가 더 중요한 가치를 지닌 경우로, value가 유효한동안은 key도 유효해야하는 경우에는 WeakHashMap을 사용하지 않는다. value가 더 중요한 경우가 대부분이다. 반대로 key가 가치가 없어지면 value의 가치가 없어지는 경우에 사용한다

  2. key를 Integer 등의 타입으로는 사용하지 말고 인스턴스로 한 번 감싸야 한다. 그냥 사용하면 key를 null만들더라도 어딘가에 값이 남아있어 참조가 되기 때문이다.

  3. weakReference도 찾아보자.. 사용할 일 거의 없다.

7-4 scheduledthreadpoolexecutor

  1. new Thead() 로 다수의 쓰레드를 만드는 방식은 리소스를 많이 사용하기 때문에 비효율적이다

  2. Executor는 쓰레드 풀을 만들어서 가져다가 쓰는 방식이다. 총 4가지의 방식이 있는데 쓰레드 풀을 만드는 방식은 찾아서 적절한 것을 사용하자
    쓰레드풀의 갯수를 정할 때는 cpu에 집중적인 작업인지, 입출력 중심 작업인지 고려해야한다.
    cpu중심의 작업이라면 cpu개수 이상의 쓰레드 풀을 만들어봤자 의미가 없다. 입출력 중심 작업이라면 성능에 따라 적절한 개수를 정해야한다.

runtime.getruntime().availableprocessors()
cpu의 개수를 구하는 코드

7-4-1 Runnable

  • 쓰레드를 생성하기 위해 사용하는 인터페이스로 쓰레드 작업만 호출하고 반환값은 없다

7-4-2 Callable<>

  • 쓰레드 작동 후 리턴 값을 받고 싶을 때 사용하며 리턴 타입은 Future<> 이다
Future<String> submit = ExcutorService.submit(new Task());
submit.get() // 여기서 리턴값을 가지고 온다.(" world)

static class Task implement Callable<String> {

@Override
public String call() {
	return " world"

8. 생략

9. try-with-resorces

  • 우리가 일반적으로 알고 있는 try-finally는 두가지 이상의 자원을 사용할 때 코드가 지저분해진다.
 static void copy(String src, String dst) throws IOException {
        InputStream in = new FileInputStream(src);
        try {
            OutputStream out = new FileOutputStream(dst);
            try {
                byte[] buf = new byte[BUFFER_SIZE];
                int n;
                while ((n = in.read(buf)) >= 0)
                    out.write(buf, 0, n);
            } finally {
                out.close();  // 안에 있는걸 먼저 실행하고
            }
        } finally {
            in.close();  // 밖의 것을 실행하고 차례대로 해야한다
        }
    }
    // 그렇다고 try-finally 하나로 묶어버리면 finally안의 앞부분 코드를
    실행했을 때 에러가 나면 finally 뒤의 코드가 실행이 안된다. 
    또는 첫번째와 두번째 모두에서 예외가 발생하면, 두번째 예외가 첫번째 예외를 삼켜, 
    스택 추적 내역에 첫번째 예외에 대한 정보는 남지 않게 된다. 
    즉 가장 나중에 발생한 예외만 보인다.

이러한 문제를 해결하기 위해 나온 것이 try-with-resources 이다. 첫번째 에외뿐만아니라 후속으로 발생한 예외도 Suppressed로 보여준다. 심지어 close를 호출하지 않아도 자동으로 해준다.
. try-with-resources 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야한다. (상위 인터페이스로 올라가서 확인해보자)

  static void copy(String src, String dst) throws IOException{
        try(InputStream in = new FileInputStream(src);
            OutputStream out = new FileOutputStream(dst))
        {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while((n = in.read(buf))>= 0)
                out.write(buf, 0, n);
        }
    }

try에 자원 객체를 전달하면, try 코드 블록이 끝나면 자동으로 자원을 종료해주는 기능이 구현되어 있다.

10. equals 규약

  • 각 인스턴스가 본질적으로 고유한 경우( 동작하는 개체를 표현하는 클래스 ex-Thread )
  • 인스턴스의 논리적 동치성(logical equality)을 검사할 일이 없는 경우 : 객체 자체가 아닌 객체가 가지고 있는 값이 같은지 검사할 필요가 없는경우
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 적합한 경우(AbstractSet, AbstractList)
  • 클래스가 private or package-private이고 equals 메서드를 호출할 일이 없는 경우

11.

12.

13.

14.

0개의 댓글