이펙티브 자바[아이템1]
아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라.
이번 아이템의 제목은 생성자로 객체를 생성하는 것과 더불어 static 객체 생성 메서드를 고려하라는 것이다. 왜 그럴까?
장점1. 이름을 가질 수 있다.
기본적으로 자바에서는 시그니처가 동일한 생성자를 두 개 이상 만들 수 없다.
public class Order {
private boolean prime;
private boolean urgent;
private Product product;
public Order(Product product, boolean prime){
this.product = product;
this.prime = prime;
}
public Order(Product product, boolean urgent){
this.product = product;
this.urgent = urgent;
}
}
- urgent와 prime필드가 둘다
boolean타입으로 동일하여 생성자 시그니처가 동일하다. - 따라서 위 코드는 컴파일 에러가 발생한다.
- 또한 생성자는 이름이 없기 때문에 어떤 객체가 만들어지는지 파악하기 불리하다.
위와 같은 이유로 정적 팩토리 메서드를 만들어 객체 생성을 분리하면 아래와 같다.
public class Order {
private boolean prime;
private boolean urgent;
private Product product;
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;
}
}
- prime, urgent 필드를 각각 지정하여 생성하는 정적 팩터리 메서드를 만들었다.
- 따라서 생성자의 시그니처가 동일한 경우 객체 생성에 용이하다.
- 메서드 이름으로 어떤 객체가 만들어지는지 알아보기 좋다.
장점2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
자바의 생성자는 매번 호출될 때마다 새로운 인스턴스를 만든다.
public class Settings {
private boolean useAutoSteering;
private boolean useABS;
private Difficulty difficulty;
public static void main(String[] args){
System.out.println(new Settings());
System.out.println(new Settings());
System.out.println(new Settings());
}
}
// 출력
chap1.item1.Settings@776ec8df
chap1.item1.Settings@4eec7777
chap1.item1.Settings@3b07d329
- 인스턴스가 만들어지는 것을 통제할 수 없다.
- 즉, 어디서든 인스턴스가 생성되는걸 막지 못한다.
여기서 정적 팩터리 메서드와 private 생성자로 무분별한 객체 생성을 방지할 수 있다.
public class Settings {
private boolean useAutoSteering;
private boolean useABS;
private Difficulty difficulty;
private Settings(){}
private final static Settings SETTINGS = new Settings();
public static Settings newInstance(){
return SETTINGS;
}
}
- 생성자를 public으로 열어두는 순간부터 인스턴스를 컨트롤할 수 없다.
- 이제 다른 클래스에서는 정적 팩터리 메서드를 통하여만 인스턴스를 받아갈 수 있다.
아래는 Boolean.valueOf() 메서드의 구현 코드이다.
...
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
...
@IntrinsicCandidate
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
- Boolean 클래스의 valueOf() 메서드 또한 파라미터에 따라서 미리 정의된 TRUE, FALSE 상수를 리턴한다.
- 정적 팩터리 메서드를 이용하면 파라미터에 따라 다른 객체를 리턴하기 좋다.
장점3,4 . 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
생성자를 이용하면 해당하는 클래스의 인스턴스만 생성할 수 있다.
그러나 정적 팩터리 메서드는 인터페이스를 리턴할 수 있으며 즉, 인터페이스의 하위 타입도 리턴할 수 있다는 뜻이다.
따라서 유연하게 객체를 생성할 수 있는 장점이 있다.
public interface HelloService {
String hello();
static HelloService of(String lang){
if(lang.equals("ko")){
return new KoreanHelloService();
}else{
return new EnglishHelloService();
}
}
}
- 자바 8이후, 인터페이스에도 정적 메서드를 만들 수 있다.
- 따라서 xxxFactory클래스를 따로 만들 필요는 없다.
이후 아래와 같이 사용할 수 있다.
public class HelloServiceFactory{
public static void main(String[] args) {
HelloService eng = HelloService.of("eng");
System.out.print(eng.hello());
}
}
// 출력
hello
장점 5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다
아래와 같이 인터페이스만 있고 구현체가 존재하지 않는다고 가정해보자.
public interface HelloService {
String hello();
}
이후 아래 코드를보자.
public class HelloServiceFactory{
public static void main(String[] args) {
ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
Optional<HelloService> helloServiceOptional = loader.findFirst();
helloServiceOptional.ifPresent( h -> {
System.out.println(h.hello());
});
}
}
- 자바에서는
ServiceLoader.load()라는 classpath내에 있는 구현체를 불러오는 정적 메서드를 제공한다. ServiceLoader는 ServiceProvider 프레임워크의 자바 기본 구현체이다.- 구현체를 직접 만들어서 쓸수도 있지만 위의 코드는 인터페이스 기반의 코드이므로 의존성이 생기지 않는다
단점 1. 상속을 하려면 public이나 protected 생성하기 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
다시 위의 코드를 가져와보자.
public class Settings {
private boolean useAutoSteering;
private boolean useABS;
private Difficulty difficulty;
private Settings(){}
private final static Settings SETTINGS = new Settings();
public static Settings newInstance(){
return SETTINGS;
}
}
- 상속을 하면 부모의 기본생성자가 먼저 호출된다. 그러나 지금 Settings의 기본생성자는 private이다.
- 따라서 상속이 불가능하다.
- 아래 코드와 같이 Delegation하여 우회적으로 사용하는 것이 장점이라고 보는 견해도 존재한다.
public class AdvencedSettings { Settings settings; // Settings의 기능 이용가능 }
단점 2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다
javadoc을 만들게 되면 Constructors와 Method가 따로 표시된다.
이때 메소드가 많아진다면 어떤 메소드가 인스턴스를 만드는 메소드인지 구분하기 힘들것이다.
따라서 책에서는 흔히 사용하는 NamingPattern을 이용해서 메서드 이름을 구분짓자고 제안한다.
- of(), getInstance(), newInstance() 등
열거 타입(Enum)
- 상수 목록을 담을 수 있는 데이터 타입
public enum OrderStatus {
PREPARING, SHIPPED, DELIVERING, DELIVERED
}
- 일반적으로 주문에는 주문상태가 포함이 된다.
- OrderStatus를 enum으로 두어 상태값을 제한할 수 있다.
만약 주문상태를 나타내는 enum이 없다면, String, int 혹은 상수로 주문의 상태를 표현했을 것이다.
// 0 - 주문 받음
// 1 - 준비중
// 2 - 배송중
// 3 - 배송완료
private int status;
- 만약 status에 0,1,2,3 을 제외한 다른 값이 들어오면 안된다.
- 따라서 저 필드값을 검증하는 코드가 추가적으로 필요하다.
따라서 enum을 사용할 때의 장점은 아래와 같다.
- 특정한 변수가 가질 수 있는 값을 제한할 수 있다. 타입-세이프티 (Type-Safety)를 보장할 수 있다.
- 각각의 상수 인스턴스 딱 하나만 만들어진다.
enum 관련 정보
values(): enum 상수 value를 모아둔 배열을 리턴해주는 메서드- enum도 생성자 및 메서드, 필드를 가질 수 있다.
public enum OrderStatus { PREPARING(0), SHIPPED(1), DELIVERING(2), DELIVERED(3); private int n; OrderStatus(int n) { this.n = n; } } - enum 필드간의 비교는
==을 사용할 것을 권장.Order order = new Order(); if(order.orderStatus == OrderStatus.PREPARING){ System.out.println("preparing"); }이런 상황에서 처음 orderStatus의 상태는 null이다. 만약 equals를 사용하여 비교하면
NullPointerException이 발생할 것이다. - enum을 key로 사용하는 컬렉션으로는
EnumSet,EnumMap을 사용하는 것이 좋다.- 내부적으로 배열을 통해 구현되어 성능적으로 더 우수함
플라이웨이트 패턴
- 객체를 가볍게 만들어 메모리 사용을 줄이는 패턴
- 자주 변하는 속성(또는 외적인 속성)과 변하지 않는 속성(또는 내적인 속성)을 분리하고 재사용하여 메모리를 줄일 수 있다.
같은 객체가 빈번하게 사용된다면, 어딘가에 저장 혹은 캐싱하여 재사용하는 방향이 좋을 것임.