ch2 객체 생성과 파괴 - LenKIM/everyone-is-effective-java-study GitHub Wiki
https://www.slipp.net/wiki/pages/viewpage.action?pageId=30771479
- 클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다.
- 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다.
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
- 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못하지만 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
- 하나의 시그니처로는 생성자를 하나만 만들 수 있지만, 이름을 가질 수 있는 정적 팩터리 메서드에는 이런 제약이 없다.
(e.g.
BigInteger(int, int, Random)
vsBigInteger#probablePrime(int, Random)
)
- 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.
- 대표적인 예인
Boolean.valueOf(boolean)
메서드는 객체를 아예 생성하지 않는다. - 플라이웨이트 패턴(Flyweight pattern)도 이와 비슷한 기법이라 할 수 있다.
(e.g.
java.lang.Integer#valueOf(int)
)
tips)
플라이웨이트 패턴?
final Integer n1 = 1;
final Integer n2 = 1;
System.out.println(n1 == n2); // true
final Integer n3 = 128;
final Integer n4 = 128;
System.out.println(n3 == n4); // false
왜?
- 반환할 객체의 클래스를 자유롭게 선택할 수 있는 '엄청난 유연성'을 선물한다.
- API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
- API가 작아진 것은 물론 개념적인 무게, 즉 프로그래머가 API를 사용하기 위해 익혀야 하는 개념의 수와 난이도도 낮췄다.
- 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다. 심지어 다음 릴리스에서는 또 다른 클래스의 객체를 반환해도 된다.
(e.g.
EnumSet#noneOf(Class<E>)
)
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
-
EnumSet.nonOf
의 인자로 무엇을 받는가에 따라 반환되는 값이 달라진다.
- 이런 유연함은 서비스 제공자 프레임워크(service provider framework)를 만드는 근간이 된다.
- 대표적인 서비스 제공자 프레임워크로는 JDBC(Java Database Connectivity)가 있다.
- JDBC의
getConnection()
메서드에서 나오는Connection
객체는 DB 마다 다르다.
- API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화해줘야 한다.
명명 | 설명 |
---|---|
from | 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드 |
of | 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드 |
valueOf | from과 of의 더 자세한 버전 |
instance 혹은 getInstance | (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다. |
create 혹은 newInstance | instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다. |
getXXX | getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.(Array.newInsatance(classobject, arrayLen)) |
newXXX | newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. (Files.newBufferedReader(path)) |
XXX | getXXX과 newXXX의 간결한 버전(Collections.list(legacyLitany)) |
- Difference Between static and default methods in interface - Stack Overflow
- Where are static methods and static variables stored in Java? - Stack Overflow
- Where static method and variables stored in JVM? - Quora
- Java의 static, 득과 실, 어떻게 사용할 것인가? - 여름으로 가는 문
- 정적 팩터리와 생성자에는 똑같은 제약이 하나 있다. 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 점이다.
- 프로그래머들은 이럴 때 **점층적 생성자 패턴(telescoping constructor pattern)**을 즐겨 사용했다.
- 점층적 생성자 패턴은 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
- 매개변수가 없는 생성자로 객체를 만든 후, 세터(setter) 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식이다.
- 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다.
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
- 이러한 단점을 완화하고자 생성이 끝난 객체를 수동으로 '얼리고(freezing)', 얼리기 전에는 사용할 수 없도록 하기도 한다.
- 이 방법을 쓴다고 하더라도 객체 사용 전에 프로그래머가 freeze 메서드를 확실히 호출해줬는지를 컴파일러가 보증할 방법이 없어서 런타임 오류에 취약하다.
- 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴(Builder pattern)
- 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개 변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더 객체를 얻는다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build()
;
- 이 클라이언트 코드는 쓰기 쉽고, 무엇보다도 읽기 쉽다. 빌더 패턴은 **명명된 선택적 매개변수(named optional parameters)**를 흉내 낸 것이다.
- Lombok - @Builder
@Builder
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static void main(String[] args) {
NutritionFacts.builder()
.calories(100)
.sodium(35)
.carbohydrate(27)
.build()
;
}
}
- Kotlin - 명명된 선택적 매개변수
class NutritionFacts(servingSize: Int, servings: Int, calories: Int, fat: Int, sodium: Int, carbohydrate: Int)
fun main(args: Array<String>) {
NutritionFacts(servingSize = 240, servings = 8, calories = 100, fat = 0, sodium = 35, carbohydrate = 27)
}
- 빌더 패턴을 사용하는 이유를 잘 모르겠습니다. - OKKY
- Object.freeze() - MDN
- How do we freeze an object while constructing an object using JavaBeans pattern. - Coderanch
- @Builder - Lombok
- Named Arguments - Kotlin
- 싱글턴(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.
- 싱글턴의 전형적인 예로는 함수와 같은 무상태(stateless) 객체나 설계상 유일해야 하는 시스템 컴포넌트를 들 수 있다.
클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.- 리플렉션 API인
AccessibleObject.setAccessible
을 사용해private
생성자를 호출할 수 있다.
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {
}
}
-
private
생성자는public static final
필드인Elvis.INSTANCE
를 초기화할 때 딱 한번만 호출된다.
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {
}
public static Elvis getInstance() {
return INSTANCE;
}
}
public enum Elvis {
INSTANCE;
}
- 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.
- 조금 부자연스러워 보일 수 있으나 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
- 이따금 단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때가 있을 것이다.
final 클래스와 관련한 메서드들을 모아놓을 때도 사용한다.- 정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 게 아니다.
- 추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.
- private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.
public class UtilityClass {
// 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용).
private UtilityClass() {
throw new AssertionError();
}
}
- 명시적 생성자가 private이니 클래스 바깥에서는 접근할 수 없다.
- 이 방식은 상속을 불가능하게 하는 효과도 있다.
- 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
- 이 조건을 만족하는 간단한 패턴이 있으니, 바로 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.
- 스프링 프레임워크에서
@Autowire
와 비슷하지만, 사용함에 주의해야 한다. - 팩터리 메서드 패턴을 구현할 때 생성자로
Supplier<T>
를 활용해 클라이언트는 자신이 명시한 타입의 하위타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
- 똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.
- 생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않다.
- '비싼 객체'가 반복해서 필요하다면 캐싱하여 재사용하길 권한다.
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
//끔찍하게 느리다는 사실을 이해하라!
//객체가 만들어지는 위치를 찾았는가?
- 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
-
객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.
-
자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다.
-
캐시 역시 메모리 누수를 일으키는 주범이다.
- finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.
- 자바 9에서는 finalizer를 사용 자체(deprecated) API로 지정하고 cleaner를 그 대안으로 소개했다.
- finalizer와 cleaner는 즉시 수행된다는 보장이 없다.
- finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다.
- 자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. (e.g. InputStream#close(), OutputStream#close(), java.sql.Connection#close())
- 믿기 어렵겠지만 훌륭한 프로그래머조차 잘못을 흔히 저지른다.
// 코드 9-1 try-finally - 더 이상 자원을 회수하는 최선의 방책이 아니다! (47쪽)
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
여기서 만약, 하나의 자원을 더 사용하게 된다면?
// 코드 9-2 자원이 둘 이상이면 try-finally 방식은 너무 지저분하다! (47쪽)
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-with-resources
가 생겼다.
- 이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다.
- 짧고 읽기 수월할 뿐 아니라 문제를 진단하기도 훨씬 좋다.
// 코드 9-3 try-with-resources - 자원을 회수하는 최선책! (48쪽)
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
}
// 코드 9-4 복수의 자원을 처리하는 try-with-resources - 짧고 매혹적이다! (49쪽)
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);
}
}
// 코드 9-5 try-with-resources를 catch 절과 함께 쓰는 모습 (49쪽)
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}