Effetive java 2장 정리
1.생성자 대신 정적 팩터리 메서드를 고려해라
public staic Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
- 이름을 가질 수 있습니다.
- 메서드를 통해 반환될 객체의 특성을 메서드 이름을 통해 명확히 묘사할 수 있는 장점이 있습니다. 예로는 BigInteger 클래스의 BigInteger.probablePrime 메서드가 있습니다.
- 호출될 때마다 인스턴스를 새로 생성하지 않아도 됩니다.
- 이 덕분에 불변 클래스는 인스턴스를 미리 만들어 놓거나, 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있습니다.
- 반환 타입의 하위 타입 객체를 반환할 수 있습니다.
- 이는 반환할 객체의 클래스를 마음대로 선택할 수 있는 엄청난 유연성을 제공해줍니다.
- 입력 매개변수에 따라 다른 클래스의 객체를 반환할 수 있습니다.
- 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 없어도 됩니다.
그리고 다음과 같은 단점이 있습니다.
- 상속하려면 public이나 protected 생성자가 필요하므로 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없습니다.
- 정적 팩터리 메서드는 프로그래머가 찾기 어렵습니다.
2.생성자에 매개변수가 많다면 빌더를 고려해라
@Builder
public Product(Long number, String name, Integer price, Integer stock, LocalDateTime updatedAt,LocalDateTime createdAt) {
this.number = number;
this.name = name;
this.price = price;
this.stock = stock;
this.updatedAt = updatedAt;
this.createdAt=createdAt;
}
Product build = Product.builder()
.stock(stock)
.name(name)
.price(price)
.createdAt(createdAt)
.build();
return build;
3.인스턴스화를 막으려거든 private 생성자를 사용하라
객체와 인스턴스의 차이: 클래스의 타입으로 선언되었을 때 객체라고 부르고, 그 객체가 메모리에 할당되어 실제 사용될 때 인스턴스라고 함.
먼저 인스턴스로 만들어서 쓰지 않는 경우는 어떤 경우가 있을까요? 단순히 static 메서드나 필드를 담은 클래스가 필요할 수 있을 겁니다.
예를 들면 java.util.Arrays의 경우 배열 관련된 메서드들(public static void sort 같은 친구들)이 있겠습니다.
이런 유틸리티 클래스들은 인스턴스를 만들어서 사용하려고 설계한 애들이 아니죠. 하지만 클래스의 생성자를 따로 명시하지 않으면 컴파일러가 자동으로 기본 생성자(`default constructor) 를 만들어 줍니다. 기본 생성자는 public 생성자로, 아무데서나 인스턴스화될 수 있습니다.
그럼 인스턴스화를 막는 방법으론 어떤 것이 있을까요? 간단합니다. private 생성자를 만들어주면 됩니다. private 생성자는 클래스 외부에선 호출될 수 없으니 인스턴스가 만들어 지는 경우를 막을 수 있습니다.
public class UtilityClass {
// 인스턴스화 방지
private UtilityClass() {
...
}
}
4.자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
싱글턴을 만드는 방식은 두 가지가 있다. 두 방식 모두 생성자는 private으로 감춰두고,
유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련해 둔다.
1. public static 멤버가 final 필드인 방식
public static final 필드 방식의 싱글턴
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding()
}
private 생성자는 public static final필드인 Elvis.INSTANCE를 초기화할 때 딱 한 번만 호출된다.
public이나 protected 생성자가 없으므로 Elvis클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.
@Test
public void singletonTest(){
Elvis elvis1 = Elvis.INSTANCE;
Elvis elvis2 = Elvis.INSTANCE;
assertSame(elvis1, elvis2); // SUCCESS
}
장점
- 해당 클래스가 싱글턴임이 API에 명백히 드러난다.
public static 필드가 final이니 절대로 다른 객체를 참조할 수 없다. - 간결하다.
예외
리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다.
리플렉션 API: java.lang.reflect, class 객체가 주어지면, 해당 클래스의 인스턴스를 생성하거나 메소드를 호출하거나, 필드에 접근할 수 있다.
Constructor<Elvis> constructor = (Constructor<Elvis>) elvis2.getClass().getDeclaredConstructor();
constructor.setAccessible(true);
Elvis elvis3 = constructor.newInstance();
assertNotSame(elvis2, elvis3); // FAIL
해결 방법
생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.
private Elvis() {
if(INSTANCE != null){
throw new RuntimeException("생성자를 호출할 수 없습니다!");
}
}
5.자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
쪼끔 친숙한 패턴입니다. 의존성 주입(Dependency Injection) 은 스프링 프레임워크를 접해봤다면 한 번쯤은 들어봤을 단어입니다.
우리가 만드는 클래스들은 대개 하나 이상의 자원에 의존 합니다.
public class minwoo {
private static final Google google; // minwoo 가 구글에 의존
}
public class minwoo {
private static final Google google;
public minwoo(Google google) {
this.google = Objects.requireNonNull(google);
}
...
}
클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면
싱글턴과 정적유틸리티 클래스는 사용하지 않는 것이 좋다. 이 자원들을 클래스가 직접
만들게 해서도 안된다. 대신 필요한 자원을(혹은 그 자원을 만들어주는 팩터리를)
생성자에 혹은 빌더에 넘겨주자 의존 객체 주입이라 하는 이 기법은 클래스의 유연성,재사용성,테스트
용이성을 기막히게 개선해준다.
6.불필요한 객체 생성을 피하라
· 똑같은 기능의 객체를 매번 생성하기보다 객체 하나를 재사용하는 편이 나을 때가 많다. 재사용은 빠르고 세련되다. 특히 불변 객체(아이템 17)는 언제든 재사용할 수 있다.
· 생성자 대신 정적 팩터리 매서드를 제공하는 불변 클래스에서는 불필요한 객체 생성을 피할 수 있다.
ex) Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.
· 가변 객체라 해도 수용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.
· 생성 비용이 아주 비싼 객체가 반복해서 필요하다면, 캐싱하여 재사용하길 권한다.
ex) 아래는 주어진 문자열이 유효한 로마 숫자인지 확인하는 메서드다. 하지만 String.matches 메서드를 사용한다는 데 문제가 있다.
public class RomanNumerals {
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는 성능이 중요한 상황에서 반복해 사용하기에 적합하지 않다.
1. 메서드 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다.
2. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다.
TODO: 유한 상태 머신이란?
- 불변인 Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 재사용할 수 있다.
아래 코드는 성능만 좋아진 것이 아니라 코드도 더 명확해졌다.
public class RomanNumerals {
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();
}
}
- isRomanNumeral 메서드가 처음 호출될 때 필드를 초기화하도록 지연 초기화로 불필요한 초기화를 없앨 수는 있지만, 권하지 않는다.
- 지연 초기화는 코드를 복잡하게 만드는데, 성능은 크게 개선되지 않을 때가 많다(아이템 67).
7. 다 쓴 객체 참조를 해제하라
· 아래 스택 클래스는 메모리 누수가 발생한다.
- 이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하된다.
- 드물긴 하지만 심할 때는 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료된다.
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
· pop 메서드에서 메모리 누수가 발생한다.
- 스택이 커졌다가 줄어들 때, 스택에서 꺼내진 객체들은 프로그램에서 더 이상 사용하지 않더라도 가비지 컬렉터가 회수하지 않는다.
- 꺼내진 객체들이 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.
- elements 배열의 '활성 영역'밖의 참조들이 모두 여기에 해당한다. 활성 영역은 인덱스가 size보다 작은 원소들로 구성된다.
- 다쓴 참조: 앞으로 다시 쓰지 않을 참조
· 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(또 그 객체들이 참조하는 모든 객체)를 회수해가지 못한다.
- 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고, 잠재적으로 성능에 악영향을 줄 수 있다.
해결 방법
· 참조를 다 썼을 때 null(참조 해제)한다.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
· 다쓴 참조를 null 하면 프로그램 오류를 조기에 발견할 수도 있다.
- null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료된다.
- null 처리하지 않았다면, 아무 내색 없이 무언가 잘못된 일을 수행할 수 있다.
but 객체 잠조를 null처리하는 일은 예외적인 경우여야 한다. 가장 좋은 방법은 그 참조를 담은 변수를 유효범위(scope)
밖으로 밀어내는 것 이다.
8. finalizer 와 cleaner 사용을 피하라
· 자바는 finalizer, cleaner 두 가지 객체 소멸자를 제공한다.
· finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.
- 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다.
· 자바 9에서 finalizer가 deprecated되고, cleaner를 그 대안으로 소개했지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.
· C++에서의 파괴자(destructor)와 자바의 finalizer, cleaner는 다른 개념이다.
- C++의 파괴자는 비메모리 자원을 회수하는 용도로 쓰이고, 자바에서는 try-with-resources와 try-finally를 사용해 해결한다.
9. try -finally 보다는 try-with-resources 를 사용하라
public static void main(String args[]) throws IOException {
FileInputStream is = null;
BufferedInputStream bis = null;
try {
is = new FileInputStream("file.txt");
bis = new BufferedInputStream(is);
int data = -1;
while((data = bis.read()) != -1){
System.out.print((char) data);
}
} finally {
// close resources
if (is != null) is.close();
if (bis != null) bis.close();
}
}
- 자원 반납에 의해 코드가 복잡해짐
- 작업이 번거로움
- 실수로 자원을 반납하지 못하는 경우 발생
- 에러로 자원을 반납하지 못하는 경우 발생
- 에러 스택 트레이스가 누락되어 디버깅이 어려움
그래서 이러한 문제를 해결하기 위해 try-with-resources라는 문법이 Java7부터 추가되었다.
Java는 이러한 문제점을 해결하고자 Java7부터 자원을 자동으로 반납해주는 try-with-resources 문법을 추가하였다.
Java는 AutoCloseable 인터페이스를 구현하고 있는 자원에 대해 try-with-resources를 적용 가능하도록 하였고, 이를 사용함으로써 코드가 유연해지고, 누락되는 에러없이 모든 에러를 잡을 수 있게 되었다.
public static void main(String args[]) throws IOException {
try (FileInputStream is = new FileInputStream("file.txt"); BufferedInputStream bis = new BufferedInputStream(is)) {
int data;
while ((data = bis.read()) != -1) {
System.out.print((char) data);
}
}
}
- 꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고 ,try-with-resources 를 사용하자!
- 코드는 더 짧아지고 , 만들어지는 예외 정보도 훨씬 유용하다.
- Static Memory (Static 영역 또는 Class 영역):
- 정적 변수(static 변수)와 정적 메서드(static 메서드)가 저장됩니다.
- 클래스가 처음으로 로드될 때 메모리에 할당되며, 프로그램의 실행 동안 유지됩니다.
- 모든 인스턴스가 공유하는 데이터를 저장하기에 적합합니다.
- Heap Memory (Heap 영역):
- 객체와 배열이 동적으로 할당되는 영역입니다.
- new 키워드를 사용하여 객체를 생성할 때 메모리에 할당됩니다.
- 가비지 컬렉션(Garbage Collection)에 의해 관리되며, 사용하지 않는 객체는 자동으로 해제됩니다.
- 다수의 스레드가 공유하는 데이터를 저장하기에 적합합니다.
- Stack Memory (Stack 영역):
- 지역 변수와 메서드 호출 시 생성되는 임시 데이터가 저장됩니다.
- 메서드가 호출될 때 스택 프레임이 생성되며, 메서드의 실행이 완료되면 스택 프레임이 제거됩니다.
- 각 스레드는 자체 스택을 가지고 있으며, 스택 프레임은 메서드 호출 순서에 따라 쌓이고 해제됩니다.
- 스택은 메모리 관리가 빠르고 간단하여, 메서드 호출과 로컬 변수 저장에 적합합니다.