Java double-brace (더블 브레이스) 사용을 주의하자
double brace initialization : 이중 중괄호 초기화
자바에서는 객체 생성 시 double breace initialization 을 이용하여 생성과 동시에 초기화
할 수 있다.
- 사실 자바에는 double breace initialization이란 것은 존재하지 않는다.
- 익명 클래스와 이니셜라이저 블록이라는 두 가지 기능이 함께 사용되어서 그렇게 보일 뿐이다.
- 그리고 생성과 동시에 함수 호출이 더 맞는 표현이다.
Map<Integer, String> map = new HashMap<>(){
{
put(1, "영수");
put(2, "별이");
}
};
Person person = new Person(){
{
setName("영수");
}
};
이름에서도 알 수 있듯이 중괄호 {{ }}
2개를 이용해서 객체를 초기화 하는것 처럼 보이는 방법이다.
- 첫번째 브라켓{}(중괄호) 으로 익명 내부 클래스를 생성한다
- 두번째 브라켓{} 으로 내부 클래스의 메소드를 실행한다. 이 때 set나 put 같은 메소드이면 값을 저장한다.
생성과 동시에 초기화 하므로 매우 편리해 보이며 가독성에도 좋아보인다.
하지만 double brace initioalization은 안티패턴
으로 간주되며 사용하지 않는것이 좋다.
왜 그럴까?
Double brace initialization 방식은 익명 내부 클래스를 사용한다
double brace로 객체를 초기화 하고 테스트를 해보았다.
class DoubleBraceTest {
@Test
void test() {
Map<Integer, String> notDoubleBraceMap = new HashMap<>();
notDoubleBraceMap.put(1, "영수");
notDoubleBraceMap.put(2, "별이");
Map<Integer, String> doubleBraceMap = new HashMap<>(){{
put(1, "영수");
put(2, "별이");
}};
Map<Integer, String> anotherDoubleBraceMap2 = new HashMap<>(){{
put(1, "영수");
put(2, "별이");
}};
System.out.println(notDoubleBraceMap); // 값은 같다
System.out.println(doubleBraceMap); // 값은 같다
System.out.println(notDoubleBraceMap.getClass()); // class java.util.hashMap
System.out.println(doubleBraceMap.getClass()); // class DoubleBraceTest$1
System.out.println(anotherDoubleBraceMap2.getClass());// class DoubleBraceTest$2
}
}
{1=영수, 2=별이}
{1=영수, 2=별이}class java.util.HashMap
class DoubleBraceTest$1
class DoubleBraceTest$2
테스트 결과를 확인해보면, double brace를 이용해서 초기화를 하면 매번 익명 내부 클래스가 생성되는것을 알 수 있다. 생성 할 때마다 매번 익명 내부 클래스가 생성되므로 클래스 로더에 상당한 오버헤드가 발생할 수 있다.
한 번만 하면 시간이 많이 걸리진 않지만 애플리케이션 전역에 걸쳐 이 작업을 수행한다면..?
생성된 익명 내부 클래스는 static (정적)이 아니다 !. 그렇다면 외부에 대한 참조가 어떻게 될까?
메모리 누수가 발생할 가능성이 있으며, 직렬화가 되지 않는다. - memory leak
익명의 내부 클래스가 생성되었을 때 익명의 내부 클래스는 주변에 대한 암묵적인 this
포인터가 있다.
위 결과의 getClass의 결과를 보면 생성되는 인스턴스를 참조하는것을 알 수 있다
모든 내부 클래스가 부모 인스턴스에 대한 참조를 유지하기 때문에 이러한 개체들이, 선언 개체보다 더 많은 개체에서 참조되는 경우 가비지 컬렉터가 동작하지 않을 수 있다.
- 실제론 사용되지 않지만 내부적으론 참조하고 있으므로 높은 확률로 가비지 컬렉터가 수거해 가지 않을 수 있다.
가비지 컬렉터가 동작하지 않는것은 곧 메모리 누수로 이어질 수 있다.
직렬화가 되지 않는다?
익명 내부 클래스에는 외부 클래스에 대한 암시적 참조가 있으므로 익명 개체가 직렬화되면 참조 개체인 외부 클래스도 직렬화하려고 시도한다. 이 때, 외부 클래스가 개체가 Serializable 인터페이스를 구현하지 않는다면, doubleBrace로 초기화된 객체를 직렬화하려고 할 때 java.io.NotSerializableException 예외가 발생한다. 또한 참조가 암시적이므로 일시적인 것으로 표시할 수 없으므로 transient 키워드를 사용하여 직렬화 대상에서 제외할 수 없다.
다음은 oracle docs에서 에 나와있는 내용이다
Note - Serialization of inner classes (i.e., nested classes that are not static member classes), including local and anonymous classes, is strongly discouraged for several reasons. Because inner classes declared in non-static contexts contain implicit non-transient references to enclosing class instances, serializing such an inner class instance will result in serialization of its associated outer class instance as well. Synthetic fields generated by
javac
(or other JavaTM compilers) to implement inner classes are implementation dependent and may vary between compilers; differences in such fields can disrupt compatibility as well as result in conflicting defaultserialVersionUID
values. The names assigned to local and anonymous inner classes are also implementation dependent and may differ between compilers. Since inner classes cannot declare static members other than compile-time constant fields, they cannot use theserialPersistentFields
mechanism to designate serializable fields. Finally, because inner classes associated with outer instances do not have zero-argument constructors (constructors of such inner classes implicitly accept the enclosing instance as a prepended parameter), they cannot implementExternalizable
. None of the issues listed above, however, apply to static member classes.
참고 - 로컬 및 익명 클래스를 포함한 내부 클래스(즉, 정적 멤버 클래스가 아닌 중첩 클래스)의 직렬화는 몇 가지 이유로 매우 권장되지 않습니다. 정적이지 않은 컨텍스트에서 선언된 내부 클래스는 포함된 클래스 인스턴스에 대한 암묵적인 비과도적 참조를 포함하기 때문에 이러한 내부 클래스 인스턴스를 직렬화하면
연관된 외부 클래스 인스턴스도 직렬화
됩니다.내부 클래스
를 구현하기 위해 javac(또는 다른 JavaTM 컴파일러)에 의해 생성된 합성 필드는 구현에 의존하며 컴파일러마다 다를 수 있다. UID 값. 로컬 및 익명 내부 클래스에 할당된 이름도 구현에 따라 다르며 컴파일러 간에 다를 수 있습니다. 내부 클래스는 컴파일 시간 상수 필드 이외의 정적 멤버를 선언할 수 없기 때문에 serialPersistentFields 메커니즘을 사용하여 serializable 필드를 지정할 수 없습니다. 마지막으로, 외부 인스턴스와 연관된 내부 클래스는 0 인수 생성자를 가지고 있지 않기 때문에(이러한 내부 클래스의 생성자는 내포 인스턴스를 추가 매개 변수로 암시적으로 받아들인다), 외부 가능을 구현할 수 없다. 그러나 위에 나열된 문제는 정적 멤버 클래스에 적용되지 않습니다.
장 / 단점
장점
- 기존 생성 및 초기화 방법에 비해 코드 라인이 적다.
- 코드가 더 읽기 쉽다.
- 생성 및 초기화는 동일한 표현식에서 수행된다.
단점
- 모호하며 초기화를 수행하는 널리 알려진 방법이 아니다.
- 사용할 때마다 추가 클래스 (익명 내부 클래스)를 생성한다.
- Java 7에 도입된 기능인 "diamond operator" 의 사용을 지원하지 않는다.
- 확장하려는 클래스가 final이면 작동하지 않는다.
- 둘러싸는 인스턴스에 대한 숨겨진 참조를 보유함으로써 메모리 누수를 일으킬 수 있다.
double brace initialization 이 안티 패턴으로 간주되는 것은 이러한 단점들 때문이며 심각한 버그를 야기할 수 있다.
결론
자바에서 double brace를 통한 개체의 초기화는 지양해야 하며, 다른 다양한 대안이 있다.
- double brace initialization 방식의 anti-pattern은 사용하지 말자.
- 진짜 굳이 꼭 쓰고싶다면 외부 클래스에 serializable을 선언을 한다 -> 그러나 다른 방법들이 있으므로 사용하지 말자
Java API 사용
- Java 8 Stream API 의 factory method를 사용해서 초기화 (Stream.of(). collect())
- Java 9 Collection factory method (List.of(), Set.of(), Map.of() )
참조
- https://www.geeksforgeeks.org/double-brace-initialization-java/
- https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serial-arch.html#4539
- https://stackoverflow.com/questions/1958636/what-is-double-brace-initialization-in-java
- https://alykhantejani.github.io/double_brace_initialization/
- https://www.baeldung.com/java-double-brace-initialization
'Java' 카테고리의 다른 글
에러와 예외 (Error, Exception) (1) | 2022.11.26 |
---|---|
Java List to String array. 리스트를 스트링으로 (0) | 2022.11.14 |
Java interface 사용 이유, interface의 장단점. (2) | 2022.09.29 |
Java 동시성 제어 - 멀티스레드, Syncronized, volatile, Atomic (0) | 2022.09.04 |