요약

클라이언트에서 따로 형변환 할 필요없이 편리하게 사용할 수 있게 하기 위해 웬만하면 클래스를 제네릭 타입으로 만들어라!

 

일반 클래스 → 제네릭 클래스

1. Class<E>처럼 클래스 선언에 타입 매개변수(E) 추가

  • Stack 클래스를 Stack<E>바꾸고, Object 타입을 E로 바꾸기
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY]; // E[]에서 컴파일 에러 발생
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        E result = elements[--size];
        elements[size] = null; // 다 쓴 객체 참조 해제

        return result;
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

 

2. 실체화 불가 타입 E로 배열 선언하는 2가지 방법

E는 실체화 불가 타입이므로 배열로 선언할 수 없는데, 이를 해결할 수 있는 2가지 방법이 있다.

 

방법1 : 제네릭 배열 생성 제약을 우회하는 방법

  1. Object 배열 → 제네릭 배열
  2. 컴파일 에러는 발생하지 않지만, 타입이 불안정해 경고가 발생한다
  • 타입 불안정문제 해결하기
@SuppressWarnings("unchecked")
public Stack() {
	elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
  1. 이 경우에는 타입이 안정한지 증명
    1. Stack<E>의 경우 elements는 private 필드
    2. 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 아예 없다.
    → 비검사 형변환은 확실히 안전하다!
  2. @SuppressWarnings로 경고를 제거해주면 된다
  • 장점
    • 가독성이 좋고 코드 길이가 짧다.
    • 형변환은 배열 생성시 한 버난 하면 된다.
  • 단점
    • E가 Object가 아닌 한 배열의 런타임 타입이 컴파일 타입과 달라 힙 오염이 발생할 수 있다.

 

방법2 : elements는 Object 타입으로 두고, result 타입을 E로 변경하기

1. result의 타입 Object → E

public E pop() {
    ..
    E result = (E) elements[--size]; // Object -> E
    ...
}

E는 실체화 불가 타입이므로 컴파일러는 런타임에서 이뤄지는 형변환이 안전한지 증명할 길이 없다. 이 경우에도 타입 안정성을 직접 증명하고, 경고를 숨기면 된다. 경고는 비검사 형변환을 수행하는 할당문에서만 숨긴다.

 

2. @SuppressWarnings("unchecked") 선언

public E pop() {
    ...
    @SuppressWarnings("unchecked") E result = (E) elements[--size];
    ...
}

elements 타입에는 E 타입만 들어오기 때문에 위 형변환은 안전하므로 @SuppressWarnings 로 경고를 제거한다.

  • 장점
    • 힙 오염 걱정을 안 해도 됨
  • 단점
    • 형변환을 배열에서 원소를 읽을 때마다 해줘야 함

 

 

제테릭 타입 특징

  1. 타입 매개변수에 거의 제약이 없다
    1. Stack<Object>, Stack<int[]>, Stack<List<String>>, Stack 등 가능
  2. 타입 매개변수에 기본 타입은 안됨
    1. Stack<int>, Stack<double> → xxx
  3. 타입 매개변수에 제약 설정 가능함(한정적 타입 매개변수)
    1. DelayQueue<E extends Delayed>

힙 오염

매개변수 유형이 서로 다른 타입을 참조할 때 발생하는 문제이다.

컴파일은 되지만 런타임에 문제가 발생하게 된다(ClassCastException).

ArrayList<String> list1 = new ArrayList<>();
list1.add("홍길동");
list1.add("임꺾정");

// 로직 수행...
Object obj = list1; -> String에서 Object로 업캐스팅
// 로직 수행...

ArrayList<Double> list2 = (ArrayList<Double>) obj;
list2.add(1.0);
list2.add(2.0);

System.out.println(list2); // [홍길동, 임꺾정, 1.0, 2.0]

for(double n : list2) {
    System.out.println(n);
}

 

 

Reference

[제네릭(Generic)] (2) 이것만은 주의해줘

정리

타입 안정성을 지키고 컴파일 에러를 보장하기 위해 로 타입이 아닌 제네릭 타입을 사용하라!

제네릭 타입이란?

클래스/인터페이스에 <타입 매개변수>를 붙인 타입을 의미한다. 제네릭 클래스는 클래스<타입 매개변수>, 제네릭 인터페이스는 인터페이스<타입 매개변수> 형태이다.

  • 제네릭 예시
    • List<E> → E는 정규 타입 매개변수
    • List<String> → String은 실제 타입 매개변수

 

제네릭 타입 사용 시 이점

private final Collection<Stamp> stamps = ...;
  • 타입 안정성
    • 컴파일러가 stamps에는 Stamp 인스턴스만 넣어야함을 인지함
    • stamps에 다른 타입의 인스턴스를 넣으려할 때 컴파일 에러 발생
  • 표현력

 

로 타입

List와 같이 타입 매개변수가 없는 제네릭 타입을 의미

  • 로 타입 사용을 하면 안되는 이유
    • 타입 안정성과 표현력이 없음
    • 런타임 에러 가능성 존재
  • 예시 코드
    • 에러 발견 시기가 컴파일 시점이 아닌 런타임 시점이다.
    // 로 타입
    private final Collection stamps = {...};
    stamps.add(new Coin(...)); 
    
    // 위 코드에서 발생할 수 있는 문제점
    add() 하는 시점에는 에러가 발생하지 않지만
    추후에 런타임 에러 가능성이 있고, 원인 코드를 추적하기가 어려워짐
    
    // 해결책
    private final Collection<Stamp> stamps = {...}; -> 제네릭으로 타입 안정성 확보
    stamps.add(new Coin(...)); -> 컴파일 에러 발생
    

 

로 타입이 만들어진 이유

제네릭이 나오기 전 코드와의 호환성을 위해 로 타입이 만들어졌다.

List vs. List<Object>

  • List
    • 제네릭 타입을 배제한다는 것을 의미
    • List 타입 매개변수에 List<String> 넘길 수 있음 → 타입 안정성 보장되지 않음
public static void main(String[] args) {
		List<String> strings = new ArrayList<>();

		// List 에 List<String> 넘길 수 있음
		unsafeAdd(strings, Integer.valueOf(42));
		strings.get(0); // 컴파일러가 자동 형변환
}

// 매개변수에 로 타입 List 존재
private static void unsafeAdd(List list, Object o) {
		list.add(o);
}
  • List<Object>
    • 모든 타입(Object)을 허용한다는 의사를 컴파일러에 명확히 전달한다는 것을 의미
    • List<Object> 타입 매개변수에 List<String> 넘길 수 없음 → 타입 안정성 보장, 무공변성?
      • List<Object>, List<String>
    public static void main(String[] args) {
    		List<String> strings = new ArrayList<>();
    
    		// List<Object> 에 List<String> 넘길 수 없음
    		safeAdd(strings, Integer.valueOf(42)); -> 컴파일 에러 발생
    }
    
    // 매개변수에 제네릭 타입 List<Object> 존재
    private static void safeAdd(List<Object> list, Object o) {
    		list.add(o);
    }
    

→ 로 타입 말고 제네릭 타입 사용해라

비한정적 와일드 카드 <?>

제네릭 타입을 쓰고 싶지만, 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않을 때 사용하는 타입이다.

Set<?>과 같은 형태로 사용하면 된다. 비한정적 와일드 카드를 사용하면 타입 안정성을 보장하고, 유연한 코드를 작성할 수 있다.

 

 

로 타입을 사용하는 예외 케이스

  1. class 리터럴에는 로 타입을 사용하라
    1. List.class, String[].class, int.class 사용 가능
    2. List<String>.class, List<?>.class 사용 불가능
    Class<List> listClass = List.class; // 가능
    List<?>.class; // 불가능
    
  2. instanceof 연산자
    1. 런타임에는 제네릭 타입 정보가 지워지므로 <?>(비한정적 와일드카드 타입) 외에 매개변수 타입은 instanceof 사용에 적용할 수 없다.
    if (o instanceof Set) { // 로 타입
    	Set<?> s = (Set<?>) o; // (로 타입 -> 와일드 카드 타입) 형변환
    }
    

요약

클래스 내부에 클래스를 만드는 방법은 정적 멤버 클래스, 비정적 멤버 클래스, 익명 클래스, 지역 클래스가 있다.

상황에 맞춰서 가장 적절한 중첩 클래스를 사용하도록 하자. 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들어서 사용하자.

메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길고, 바깥 클래스를 참조한다 → 비정적 멤버 클래스

메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길고 바깥 클래스를 참조하지 않음 → 정적 멤버 클래스

중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한곳이고, 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다 → 익명 클래스로 만들자.

아니면 지역 클래스로 만들자.

 

중첩 클래스란?

다른 클래스 안에 정의된 클래스이고, 자신을 감싼 바깥 클래스에서만 사용되어야 한다.

종류로는 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스가 있다.

정적 멤버 클래스를 제외한 모든 중첩 클래스는 내부 클래스(inner class)이다.

  • 중첩 클래스를 왜 쓰는가?
    • 클래스 간 논리적 그룹을 나타낼 때 사용한다.
      • model 객체에서 상위 모델과 하위 모델이 있을 때 static nested class 사용
    • 향상된 캡슐화
    • 좋은 가독성과 유지보수성

 

1. 정적 멤버 클래스

클래스 내부에 있는 static 클래스이다.

// 모양새
class Outer {
    변수;
    메소드;
		private 멤버;

    public static class Inner {

    }
}

// 객체 생성
Outer.Inner 객체 = new Outer.Inner();
  • 특징
    • 다른 클래스 내부에서 선언된다
    • 바깥 클래스의 private 멤버에도 접근할 수 있다
    • 정적 멤버와 같은 접근 규칙을 적용받는다 (ex. private 선언시 바깥 클래스에서만 접근 가능)
  • private 정적 멤버 클래스
    • 바깥 클래스가 표현하는 객체의 한 부분(구성 요소)을 나타낼 때 주로 사용한다.
    • Map 인터페이스 구현체는 엔트리 객체를 private static 멤버 클래스로 만든다?

→ 바깥 클래스 인스턴스와 독립적으로 존재해야하는 경우 사용해야 한다.

 

2. 비정적 멤버 클래스

클래스 내부에 있는 non-static 중첩 클래스이다.

비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없다.

비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다(숨은 외부참조).

즉, 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this(**바깥 *클래스명.this*** 형태)로 바깥 인스턴스 메서드 호출 하거나 바깥 인스턴스의 참조를 가져올 수 있다.

// 모양새
class Outer {
    변수;
    메소드;
		this.~~~
		
		메서드1 {
			new Inner(); // 내부 클래스 생성자 호출
    }

    public class Inner {
		
		이너 메서드 {
			Outer.this.메서드1
		}
  }
}
  • 바깥 인스턴스와 비정적 멤버 인스턴스 관계 형성 방법
    • 자동
      • 바깥 클래스 메서드 안에 비정적 멤버 클래스 생성자가 호출되고 멤버 클래스 인스턴스 생성 시 자동으로 관계 형성
    • 수동
      • 바깥 인스턴스 [클래스.new](<http://클래스.new>) Member Class(args) 호출로 수동 생성도 가능
  • 숨은 외부 참조 특징
    • 한 번 형성된 관계는 변경 불가능하다
    • 관계 정보는 비정적 멤버 클래스 인스턴스 안에 만들어진다
      • 메모리 공간 차지
      • 생성 시간 더 걸림
    • GC에서 멤버 클래스 인식이 안돼 메모리 누수 발생 가능
    • 참조가 눈에 보이지 않아 문제 원인을 찾기 어려워짐
  • 사용 예시 - 어댑터(Adapter)
    • 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰를 사용할 때

→ 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.

 

3. 익명 클래스

이름이 없는 클래스를 의미한다.

  • 장점
    • 사용되는 시점에 선언과 동시에 인스턴스 생성
    • 코드 어디에서든 만들 수 있다
    • 비정적 익명 클래스
      • 바깥 클래스 인스턴스 참조 가능
    • 정적 익명 클래스
      • 상수표현을 위해 초기화된 final 기본 타입과 문자열 필드만 가질 수 있다
  • 단점
    • 선언한 지점에서만 인스턴스 생성 가능
    • instanceof 검사나 클래스 이름이 필요한 작업 수행 불가능
    • 인터페이스 구현 및 상속 불가능
    • 코드가 길어질 경우 가독성 저하

→ 람다 출시 이후에는 람다가 익명 클래스를 거의 대체할 수 있다.

 

4. 지역 클래스

지역 변수를 선언할 수 있는 곳에 어디든 만들 수 있는 클래스를 의미한다. 유효 범위도 지역변수와 같다. 메서드 내부에서 new로 생성한 다음에 메서드 내부에서만 사용이 가능하다.

class Outer {
    변수;
    메소드1;

    메소드2() {
        지역변수;

        class Inner {

        }
    }
}
  • 특징
    • 이름이 있고, 반복해서 사용 가능
    • 비정적 문맥에서 사용될 때만 바깥 인스턴스 참조 가능
    • 정적 멤버 가질 수 없다
    • 가독성을 위해 짧게 작성해야 한다

본 포스트는 책 '이펙티브 자바' 에서 학습한 내용을 기반으로 작성되었습니다.

요약

자바 8부터 디폴트 메서드의 도입으로 기존의 인터페이스를 문제없이 수정할 수 있게 되었지만, 되도록이면 신중하게 디폴트 메서드를 추가하자.

 

디폴트 메서드

디폴트 메서드는 자바 8부터 추가된 내용으로, 기존 인터페이스에 메서드를 추가할 수 있는 새로운 방법이다. 인터페이스의 기존 구현체에서 디폴트 메서드를 재정의 하지 않아도 디폴트 구현이 자동으로 쓰이기 때문에 웬만하면 문제없이 인터페이스에 추가할 수 있다고 한다. (100%는 아님)

자바 8에서는 람다를 활용하기 위한 목적으로 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었음.

 

예시 : removeIf()

// 예시
arrayList.removeIf('람다?')

// 디폴트 메서드 안에 함수형 인터페이스를 활용해 람다를 사용할 수 있다? 
default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean result = false;
        for (Iterator<E> it = iterator(); it.hasNext();) {
            if (filter.test(it.next())) {
                it.remove();
                result = true;
            }
        }
        
        return result;
    }
  • 반복자를 이용해 순회하면서 각 원소를 인수로 넣어 프레디키트를 호출
  • 프레디키드가 반환값이 true라면 반복자의 remove 메서드를 호출
  • 해당 원소 제거

 

문제

  • 디폴트 메서드 추가로 문제를 안 일으킨다는 보장이 없음
    • 변경 전 인터페이스를 사용하는 클라이언트에서 문제 발생 가능성 존재
    • 컴파일 에러가 발생하지 않더라도, 런타임 에러의 위험성
    • 예기치 못한 충돌/API 재앙, 사용자 불편 상황 발생 가능성 존재

→ 되도록이면 기존 인터페이스에는 디폴트 메서드를 추가하지 말자.

 

문제 해결 방법

  1. 인터페이스 구현체에서 디폴트 메서드 재정의 강제
  2. 다른 메서드에서는 디폴트 메서드를 호출하기 전에 필요한 작업 강제
  3. 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 하지 말자
  4. 기존 인터페이스가 아닌 새로운 인터페이스를 설계하는 경우라면 위험성 적음

 

인터페이스 설계 후 해야할 일들

바로 잡을 기회가 아직 남았을 때 결함을 테스트로 찾아내자

  1. 새로운 인터페이스인 경우 릴리스 전에 반드시 테스트를 거쳐야 함
  2. 인터페이스의 서로 다른 구현 케이스를 생각해 테스트 해야함
  3. 인터페이스를 사용하는 다양한 클라이언트도 만들어서 테스트 해야함

+ Recent posts