책 <이펙티브 자바 3판>과 백기선님 강의 <이펙티브 자바 완벽 공략 1부> 내용을 요약해서 정리하려고 합니다.
이름
을 가질 수 있음AS-IS와 같이 시그니처가 중복될 때 사용한다
- AS-IS: 생성자를 사용했을 경우
같은 타입의 시그니처(매개변수)prime
,urgent
이름만 다르게 생성자를 두 개를 만들 경우 자바 컴파일 오류가 난다.
인스턴스
호출될 때마다 같은 인스턴스를 생성해준다인스턴스가 같아야 할 떄 사용한다
public class Settings {
private Settings() {} // 기본 생성자를 사용하지 못 하도록 막음
private static final Settings SETTINGS = new Settings(); // 정적 팩토리 사용
public static Settings getInstance() { // 인스턴스를 통제
return SETTINGS;
}
}
// Settings 인스턴스 사용
public class Product {
public static void main(String[] args) {
Settings settings1 = Settings.getInstance();
Settings settings2 = Settings.getInstance();
System.out.println(settings1);
System.out.println(settings2);
Boolean.valueOf(false);
EnumSet.allOf(Difficulty.class);
}
}
하위 타입 객체
를 반환할 수 있음입력 매개변수
에 따라 다른 클래스의 객체 반환public class HelloServiceFactory {
public static HelloService of(String lang) {
if (lang.equals("ko")) {
return new KoreanHelloService(); // HelloService를 상속받고 있는 객체
}
return new KoreanHelloService();
}
}
반환할 객체
의 클래스가 존재하지 않아도 됨
public interface HelloService {
// 인터페이스만 있고 구현체가 없음
String hello();
}
public class HelloServiceFactory {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
Optional<HelloService> helloServiceOptional = loader.findFirst();
helloServiceOptional.ifPresent(h -> {
System.out.println("하위 클래스 KoreanHelloService의 hello가 호출된다." + h.hello());
});
}
}
왜 ServiceLoader를 이용해서 가져올까? 의존성
위 코드는 의존성이 전혀 없지만,
아래 코드는 KoreanHelloService를 사용한다는 것을 명확히 하기에 의존한다.HelloService koreanHelloService = new KoreanHelloService();
하위 클래스를 만들 수 없다.
Settings를 deligation 해서 사용하면 되므로 장점일 수 있음
public class AdvancedSettings { Settings settings; }
public class Settings {
private Settings() {} // 기본 생성자를 사용하지 못 하도록 막음
private static final Settings SETTINGS = new Settings(); // 정적 팩토리 사용
public static Settings getInstance() { // 인스턴스를 통제
return SETTINGS;
}
}
java doc
을 이용할 수 있다.Settings()
인스턴스는 어떻게 생성하는거지? 하고 의아해할 수 있다. 그리하여, public static Settings of(boolean useABS) {
return SETTINGS;
}
/**
* 이 클래스의 인스턴스는 #getInstance()를 통해 사용한다.
* @see #getInstance()
*/
public class Settings {
mvn javadoc:javadoc
생성자를 쓰지 말라는 것이 아니라, 정적 팩터리 메소드를 쓰는 것을 고려하라는 의미이다.
열거 타입
은 인스턴스가 하나만 만들어짐을 보장한다.플라이웨이트 패턴
을 사용할 수 있다.인터페이스가 정적 메서드
를 가질 수 없다는 제한이 풀렸기 때문에 인스턴스화 불가 동반 클래스를 둘 이유가 별로 없다.서비스 제공자 프레임워크
를 만드는 근간이 된다.리플렉션
을 사용해야 한다.브리지 패턴
의존 객체 주입 프레임워크
Type-Safety
하지 않다. // 0 - 주문 받음
// 1 - 준비 중
// 2 - 배송 중
// 3 - 배송 완료
private int status = 100;
Type-Safety
를 보장할 수 있게 된다.public enum OrderStatus {
PREPARING(0), SHIPPED(1), DELIVERING(2), DELIVERED(3);
private int number;
OrderStatus(int number) {
this.number = number;
}
}
Arrays.stream(OrderStatus.values()).forEach(System.out::println);
public enum OrderStatus {
PREPARING(0), SHIPPED(1), DELIVERING(2), DELIVERED(3);
private int number;
OrderStatus(int number) {
this.number = number;
}
}
Order order = new Order();
if (order.orderStatus == OrderStatus.DELIVERED) {
System.out.println("delivered");
}
// OrderStatus가 Null이므로 equals로 비교시 NPE 발생
Order order = new Order();
if (order.orderStatus.equals(OrderStatus.DELIVERED)) {
System.out.println("delivered");
}
근거
1. 값이 예측 가능한 순서로 저장
2. 각 계산에 대해 1비트만 검사
3. 해시 코드를 계산할 필요 없음
If we compare EnumSet with other Set implementations like HashSet,
the first is usually faster because the values are stored in a predictable order
and only one bit needs to be examined for each computation.
Unlike HashSet, there's no need to compute the hashcode to find the right bucket.
Moreover, due to the nature of bit vectors, an EnumSet is very compact and efficient.
Therefore, it uses less memory, with all the benefits that it brings.
enum Flag { A, B, C, D, E, F, G }
// ...
static void benchmarkEnumSet() {
System.gc();
long beg = System.nanoTime();
Set<Flag> a = EnumSet.of(Flag.A, Flag.B, Flag.G);
for (int i = 0; i < 1_000_000_000; i++) {
Set<Flag> b = EnumSet.of(Flag.A, Flag.B, Flag.G);
assert a.equals(b);
}
long end = System.nanoTime();
System.out.println("EnumSet\t" + (end - beg)/1e9);
}
A benchmark for a classical bitfield:
static final int A = 1 << 0;
static final int B = 1 << 1;
static final int C = 1 << 2;
static final int D = 1 << 3;
static final int E = 1 << 4;
static final int F = 1 << 5;
static final int G = 1 << 6;
// ...
static void benchmarkBitfield() {
System.gc();
long beg = System.nanoTime();
int a = A | B | G;
for (int i = 0; i < 1_000_000_000; i++) {
int b = A | B | G;
assert a == b;
}
long end = System.nanoTime();
System.out.println("bitfield\t" + (end - beg)/1e9);
}
결과
HashSet 104.486260884
EnumSet 3.900099588
bitfield 0.003371834
HashSet 109.827488593
EnumSet 3.484818891
bitfield 0.003430366
HashSet 107.106317379
EnumSet 3.742689517
bitield 0.000000057
예시를 보면서 살펴보자
public class Character {
private char value;
private String color;
private String fontFamily;
private int fonrSize;
public Character(char value, String color, String fontFamily, int fonrSize) {
this.value = value;
this.color = color;
this.fontFamily = fontFamily;
this.fonrSize = fonrSize;
}
}
fontFamily
, fonrSize
value
, color
Font
를 만든다.public class Font {
private final String fontFamily;
private final int fonrSize;
public Font(String fontFamily, int fonrSize) {
this.fontFamily = fontFamily;
this.fonrSize = fonrSize;
}
}
Font
클래스를 가지고 FontFactory
클래스에 담아준다.// 자주 사용하는 것의 캐싱하는 알고리즘
public class FontFactory {
private final Map<String, Font> cache = new HashMap<>();
public Font getFont(String font) {
if (cache.containsKey(font)) {
return cache.get(font);
} else {
String[] split = font.split(":");
Font newFont = new Font(split[0], Integer.parseInt(split[1]));
cache.put(font, newFont);
return newFont;
}
}
}
Client
클래스에서 FontFactory를 이용한다.public class Client {
public static void main(String[] args) {
FontFactory fontFactory = new FontFactory();
Character character = new Character('h', "white", fontFactory.getFont("nanum:12"));
Character character2 = new Character('e', "white", fontFactory.getFont("nanum:12"));
Character character3 = new Character('l', "white", fontFactory.getFont("nanum:12"));
}
}
public interface HelloService {
String hello(); // 1. java 8
}
public interface HelloService {
default String bye() {
return "bye"; // 2. default method로 정의
}
static String hi() { // 접근지시자 생략되어 있다면 public
prepareMessage(); // 3. static method
return "hi";
}
static private void prepareMessage() { // private 가능
}
}
public class ListQuiz {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(3);
list.add(5);
list.add(2);
Comparator<Integer> desc = (o1, o2) -> o2 - o1;
list.sort(desc.reversed());
System.out.println(list);
}
}
확장 가능한 애플리케이션을 만드는 방법
= 애플리케이션 코드는 그대로 유지되고, 외적인 것(ex 설정)을 변경했을 때 결과가 달라지게 하는 것
서비스 제공자 인터페이스: HelloService
여러 구현체들이 만들어 질 수 있는 확장가능한 서비스
- 서비스 제공자(구현체): KoreanHelloService
구현할 수 있는
public interface HelloService {
String hello();
static String hi() { // 구현체는 제공자 안에 있어도 됨
prepareMessage();
return "hi";
}
}
응용
java.util.ServiceLoader
서비스 접근 API는 해당 서비스에 접근해서 가져오는 것
https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html
https://docs.oracle.com/javase/tutorial/ext/basics/spi.html
응용
브릿지 패턴
구체적인 것과 추상적인 것을 분리하여 중간 다리를 놓는 것
서로 영향을 주지 않으면서 개별적인 계층 구조를 가져갈 수 있게 함
예시 Champion과 Skin이 구분되지 않으면 Skin 하나 추가될 때마다 Champion도 늘어난다. 해당 브릿지 패턴을 쓰면 Skin만 추가한다고 하면, Skin만 늘어나게 된다.
서비스 제공자를 등록하는 API를 제공하는 것
@Configuration // 클래스 정의
public class AppConfig {
@Bean // 서비스 구현체 등록
public HelloService helloService() {
return new KoreanHelloService();
}
}
서비스를 가져오는 방법
public class App {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); // 정의
HelloService helloService = applicationContext.getBean(HelloService.class); // 빈 가져와서
System.out.println(helloService.hello()); // 사용
}
}
응용
의존 객체 주입 프레임워크
META-INF.services.me.whiteshipo.chapter01.HelloService 에 해당하는 파일
은 서비스 등록 API가 된다.
정의: 클래스로더를 통해 읽어온 클래스 정보를 사용하는 기술
가능한 것: 클래스를 읽기 / 인스턴스 만들기 / 메소드 실행 / 필드의 값 가져오기 혹은 변형하기
언제 사용?
특정 애노테이션(ex- @Bean)이 붙어있는 필드 또는 메소드 읽어오기 (JUnit, Spring)
특정 이름 패턴에 해당하는 메소드 목록 가져와 호출하기 (getter, setter)
https://docs.oracle.com/javase/tutorial/reflect/
Class<?> aClass = Class.forName("me.whiteship.chapter01.item01.KoreanHelloService");
Constructor<?> constructor = aClass.getConstructor(); // 생성자 가져오기
HelloService helloService = (HelloService) constructor.newInstance(); // 인스턴스 생성
System.out.println(helloService.hello()); // 오브젝트 사용
만약 메소드를 모른다고 하면, 해당 클래스의 메소드, 생성자, 애노테이션 등을 가져올 수 있다.