네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

'dev book/Effective Java'에 해당되는 글 27건


반응형

# 아이템27. 비검사 경고를 제거하라

 

제네릭을 사용하기 시작하면 수많은 컴파일러 경고를 보게될 것이다. 비검사 형변환 경고, 비검사 메서드 호출 경고, 비검사 매개변수화 가변인수 타입 경고, 비검사 변환 경고 등이다.
Set<String> set = new HashSet();
$ javac WarningTest.java -Xlint:unchecked
WarningTest.java:8: [unchecked] unchecked conversion
Set<String> set = new HashSet();
                      ^
required: Set<String>
found: HashSet
1 warning

 

* javac 명령 인수에 -Xlint:uncheck 를 추가하면 해당 에러를 볼 수 있다.
 
컴파일러가 알려준 대로 수정하면 경고가 사라진다. 사실 컴파일러가 알려준 타입 매개변수를 명시하지 않고, 자바 7부터 지원하는 다이아몬드 연산자(<>)만으로 해결할 수 있다. 그러면 컴파일러가 올바른 실제 타입 매개변수를 추론해준다.
Set<String> set = new HashSet<>();
제거하기 훨씬 어려운 경고도 있다. 할 수 있는 한 모든 비검사 경고를 제거하라. 모두 제거한다면 그 코드는 타입 안정성이 보장된다. 즉, 런타임에 ClassCastException이 발생할 일이 없고, 여러분이 의도한 대로 잘 동작하리라 확신할 수 있다.
경고를 제거할 수는 없지만 타입이 안전하다고 확신할 수 있다면 @SuppressWarning("unchecked") 어노테이션을 달아 경고를 숨기자.
단, 타입 안전함을 검증하지 않은 채 경고를 숨기면 스스로에게 잘못된 보안 인식을 심어주는 꼴이다. 그 코드는 경고없이 컴파일이 되겠지만, 런타임에는 여전히 ClassCastException을 던질 수 있다. 한편 안전하다고 검증된 비검사 경고를 그대로 두면, 진짜 문제를 알리는 새로운 경고가 나와도 눈치채지 못할 수 있다. 제거하지 않은 수많은 거짓 경고 속에 새로운 경고가 파묻힐 것이기 때문이다.
@SuppressWarnings 어노테이션은 개별 지역변수 선언부터 클래스 전체까지 어떤 선언에도 달 수 있다. 하지만 @SuppressWarnings 어노테이션은 항상 가능한 좁은 범위에 적용하자. 보통은 변수 선언, 아주 짧은 메서드, 혹은 생성자가 될 것이다. 자칫 심각한 경고를 놓칠 수 있으니 절대로 클래스 전체에 적용해서는 안된다.
한줄이 넘는 메서드나 생성자에 달린 @SuppressWarnings 어노테이션을 발견하면 지역변수 선언 쪽으로 옮기자.
 

## 지역변수를 추가해 @SuppressWarning의 범위를 좁힌다.

public <T> T[] toArray(T[] a) {
  if (a.length < size) {
    // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[]로 같으므로 올바른 형변환이다.
    @SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
    return result;
  }

  System.arraycopy(elements, 0, 1, 0, size);
  if (a.length > size)
    a[size] = null;
  return a;
}
이 코드는 깔끔하게 컴파일되고 비검사 경고를 숨기는 범위로 좁혔다.
@SuppressWarnings("unchecked") 어노테이션을 사용할 때면 그 경고를 무시해도 안전한 이유를 항상 주석으로 남겨야 한다.
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

# 아이템26. 로타입은 사용하지 마라

 

## 용어 정리
public class<T> Example {
  private T member;
}
- 제네릭 클래스[인터페이스]: 클래스[인터페이스] 선언에 타입 매개변수(type parameter)가 쓰인다.
- 제네릭 타입(Generic Type): 제네릭 클래스와 제네릭 인터페이스를 통틀어 말함. Example<T>
- 매개변수화 타입(parameterized Type): 각각의 제네릭 타입은 parameterized Type을 선언함. Example<String>
- 타입 매개변수(Type parameter): 제네릭 선언에 사용된 매개변수 <T>
- formalType: Example<E>
- actualType: Exampe<String>
- 로타입(raw tyep): 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않았을 때

 

## 제네릭의 타입 안정성
오류는 가능한 한 발생 즉시, 이상적으로는 컴파일 타임에 발견하는 것이 좋다.

 

### 1. 로타입의 단점: 컴파일 타임에 타입 정보를 알지 못한다.
ClassCastException과 같은 런타임에야 알아챌 수 있는 에러를 만들기 쉽다.
아래 예제는 컴파일 타임에는 문제가 없으나, 런타임에서 ClassCastException이 발샌한다.
public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0); // 컴파일러가 자동으로 형변환 코드를 넣어준다.
    }

    private static void unsafeAdd(List list, Object o) {
        list.add(o);
    }
}

 

### 2. 제네릭의 장점: 컴파일 타임에 타입 선언이 녹아있다.
컴파일러가 타입 선언에 대해 인지하고 있기 때문에, 아무런 경고 없이 컴파일되면 의도대로 동작할 것임을 보장한다.
public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings, "42");   // 컴파일 타임에 에러를 체크해주어서 바꿀 수 있다.
        String s = strings.get(0);
    }

    private static <T> void unsafeAdd(List<T> list, T o) { // 제네릭 선언
        list.add(o);
    }
}
컴파일러가 컬렉션에서 원소를 넣는 모든 곳에 보이지 않은 형변환을 추가하여 절대 실패하지 않음을 보장하였다.

 

## 로타입은 절대로 사용하지 말자
제네릭이 안겨주는 안정성과 표현력을 모두 잃게 된다.
### 로타입이 만들어진 이유 - 호환성
기존 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와 맞물려 돌아가게 하기 위해서이다.
호환성: 로타입 지원 + 제네릭 구현 시, 소거(erasure) 방식을 이용

 

** List VS List<Object>
- List: 로타입
- List<Object>: 임의 객체를 허용하는 매개변수화 타입 - 타입에 대한 정보를 컴파일러에 알려주었다.

 

로타입을 사용하면 타입 안정성을 잃게된다.

 

## 타입 안전 + 유연: 비한정 와일드카 타입
- 비한정 와일드카드 타입(unbounded wildcard type): 제네릭 타입을 쓰고 싶지만, 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 대 사용한다. Set<E>의 비한정 와일드카드 타입은 Set<?>
static int numElementsInCommon(Set<?> s1, Set<?> s2) {...}
와일드카드 타입은 안전하지만 로타입은 안전하지 않다.

 

- 로타입: 아무 원소나 넣을 수 있어 타입 불변식을 훼손하기 쉽다.
- 와일드카드 타입: Collection<?>에는 어떤 원소도 넣을 수 없다. (null 외에는) 컴파일 타임에 오류 메시지를 볼 수 있고, 컬렉션의 타입 불변식을 훼손하지 못하게 막고, 컬렉션에서 꺼낼 수 있는 객체의 타입도 알 수 없게 한다.
 
## 로타입 규칙 예외
### 1. class 리터럴에는 로타입을 사용한다.
자바 명세 자체가 class 리터럴에 매개변수화 타입을 사용하지 못하게 하였다.
- 허용: List.class, String[].class, int.class
- 불가: List<String>.class, List<?>.class

 

### 2. instanceof 연산자는 로타입을 사용한다.
instanceof 연산자는 비한정 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.
로타입이든 비한정적 와일드타입이든 instanceof는 똑같이 동작한다.
if (o instanceof Set) { // 로타입
  Set<?> s = (Set<?>) o; // 와일드카드 타입
}
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

 

# 아이템25. 톱레벨 클래스는 한 파일에 하나만 담아라.

## 한 파일에 톱레벨 클래스가 여러 개일 때 문제점
public class Main {
  public static void main(String[] args) {
    System.out.println(Utensil.NAME + Dessert.NAME);
  }
}
// Utensil.java
class Utensil {
  static final String NAME = "pan";
}

class Dessert {
  static final String NAME = "cake";
}
javac Main.java Utensil.java
> pancake
Main 클래스를 실행하면 pancake가 출력된다.
이 상태에서 똑같은 두 클래스를 담은 Dessert.java가 추가되면
class Utensil {
  static final String NAME = "pot";
}

class Dessert {
  static final String NAME = "pie";
}
javac Main.java Dessert.java
> Duplicate class found in the file '~/src/main/java/Dessert.java'
javac Main.java Dessert.java 명령어로 컴파일을 한다면 컴파일 오류가 발생하고 클래스가 중복 정의되었다고 알려준다.
컴파일은 가장먼저 Main.java를 컴파일하고, 그 안에서 Utensil 참조를 먼저 만나 Utensil.java 파일을 살펴서 Utensil, Dessert 클래스를 모두 찾아낸다. 그 다음 두번째로 참조되는 Dessert에 따라 Dessert.java를 처리하려고 할 때 같은 클래스의 정의가 이미 되어있음을 알게 된다.
javac Main.java
> pancake
javac Main.java Utensil.java
> pancake
javac Dessert.java Main.java
> potpie
이처럼 컴파일러에 어느 소스파일을 먼저 전달하느냐에 따라 동작이 달라진다.

 

## 해결책
톱클래스를 서로 다른 소스 파일로 분리하면 된다.
// Utensil.java
class Utensil {
  static final String NAME = "pan";
}
// Dessert.java
class Dessert {
  static final String NAME = "cake";
}

 

## 정적 멤버 클래스
굳이 한 파일에 담고싶다면 정적 멤버 클래스를 사용하는 방법을 고려하자. 부차적인 클래스를 정적 멤버 클래스로 만들면 가독성도 높아지고, private로 선언하여 접근 범위도 최소로 관리할 수 있다.
단, 정적 멤버 클래스는 보통 하나의 클래스에 딸린 부차적인 클래스를 추가하는데 사용되므로, 패턴을 유지하기 위해 부차적인 클래스를 추가하는 경우에만 사용하자.
// example
class Utensil {
  static final String NAME = "pan";

  private static class Dessert {
    static final String Name = "cake";
  }
}
class Test {
  public static void main(String[] args) {
    System.out.println(Utensil.NAME + Dessert.NAME);
  }

  private static class Utensil {
    static final String NAME = "fan";
  }

  private static class Dessert {
    static final String NAME = "cake";
  }
}
## 정리
소스 파일 하나에는 반드시 톱레벨 클래스(혹은 인터페이스)를 하나만 담자. 이 규칙만 따른다면 소스파일을 어떤 순서로 컴파일 하든 프로그램의 동작이 달라지지 않는다.

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

# 아이템24. 멤버 클래스는 되도록 static으로 만들라.

- 중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.

 

- 중첩 클래스의 종류는 정적 멤버 클래스, (비정적) 멤버 클래스, 익명 클래스, 지역 클래스 4가지다.

 

## 정적 멤버 클래스
- 정적 멤버 클래스는 다른 클래스 안에 선언이 되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점을 제외하고는 일반 클래스와 똑같다. 정적 멤버 클래스는 흔히 바깥 클래스와 함께 쓰일 때만 유용한 private 도우미 클래스로 쓰인다.
public class StaticCar {
    static String _where="I am a Car from Germany!";
    Country _country;            // object of inner class country
    StaticCar(){
        _country=new Country();    // instantiate the inner class
    }
    static class Country {       // static member inner class
            String showCountry() {
            return _where;
        }
    }

    public static void main(String[] args) {
        
        StaticCar myCar= new StaticCar() ;  // instantiated object of class StaticCar
        System.out.print("Access through an Country reference");
        System.out.println(" created in an object of a StaticCar:");
        System.out.println(myCar._country.showCountry());
        
        // instantiated object of class StaticCar.Country
        StaticCar.Country country= new StaticCar.Country();
        System.out.println("Access through an Country reference that is local:");
        System.out.println(country.showCountry());
    }
}
 
Access through an Country reference created in an object of a StaticCar:
I am from Germany!
Access through an Country reference that is local:
I am from Germany!

 

## 비정적 멤버 클래스

- 정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어있고 없고 뿐이지만, 의미상 차이는 의외로 꽤 크다.

- 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 그래서 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.
- 정규화된 this란, 클래스명.this 형태로 바깥 클래스의 이름을 명시하는 용법을 말한다.
- 비정적 멤버 클래스의 인터페이스와 바깥 인터페이스 사이의 관계는 멤버 클래스가 인스턴스화 될 떄 확립되며, 더 이상 변경할 수 없다.
- 비정적 멤버 클래스는 어댑터를 정의할 때 자주 쓰인다. 즉, 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰로 사용하는 것이다.

- 비슷하게, Set과 List 같은 다른 컬렉션 인터페이스 구현들도 자신의 반복자를 구현할 때 비정적 멤버 클래스를 주로 사용한다.

public class MySet<E> extends AbstractSet<E> {
    ... // 생략

    @Override public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
    ...
    }
}
- 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자. static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게된다. 그러면 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다.

 

## 익명 클래스
- 익명 클래스는 바깥 클래스의 멤버도 아니다. 멤버와 달리, 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다. 그리고 오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다. 정적 문맥에서라도 상수 변수 이외의 정적 멤버는 가질 수 없다. 즉, 상수 표현을 위해 초기화된 final 기본 타입과 문자열 필드만 가질 수 있다.
interface Inter {
    public void hi();
}

class Anony {
    public void f() {
        System.out.println("Anony.f() 호출.");
    }
}

public class Test {
    private Integer number;

    public void nonStaticMethod(){
        Inter ob2 = new Inter() {
            public void hi() {
                System.out.println("비 정적문맥 메소드 호출" + number);
            }
        };
    }

    public static void staticMethod() {
        Inter ob2 = new Inter() {
            Integer number2 = 1;
            public void hi() {
                System.out.println("정적문맥 메소드 호출" + number); // comile Error
            }
        };
    }

    public static void main(String[] args) throws Exception {
        //Inter ob1 = new Inter(); //Compile Error!!!
        Inter ob2 = new Inter() {
            public void hi() {
                System.out.println("안녕하십니까?");
            }
        };

        ob2.hi();

        Anony a1 = new Anony();
        Anony a2 = new Anony() {
            public void f() {
                System.out.println("이히힛.");
            }
        };

        a1.f();
        a2.f();
    }
}
- 익명 클래스를 사용하는 클라이언트는 그 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출할 수 없다.
- 익명 클래스의 또 다른 주 쓰임은 정적 팩터리 메서드를 구현할 때다.

 

## 지역 클래스
- 지역 클래스는 지역 변수를 선언할 수 있는 곳이면 실질적으로 어디서든 선언할 수 있고, 유효 범위도 지역 변수와 같다. 멤버 클래스처럼 이름이 있고 반복해서 사용할 수 있다. 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있으며, 정적 멤버는 가질 수 없으며, 가독성을 위해 짧게 작성해야 한다.

 

## 정리
중첩 클래스에는 4가지가 있으며, 각각의 쓰임이 다르다. 메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만든다. 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않으면 정적으로 만들면 된다. 중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만들자.
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

# 태그 달린 클래스보다는 클래스 계층 구조를 활용하라.

 

## 태그 달린 클래스
- 두 가지 이상의 기능을 가지고 있으며, 그 중에서 어떠한 능력을 갖고 있는지 나타내는 태그(tag) 필드가 있는 클래스를 태그 달린 클래스라고 말한다. 이 클래스는 아래와 같은 형태를 가지고 있다.
class Figure {
  enum Shape {
    RECTANGLE, CIRCLE
  };

  final Shape shape; // 태그 필드 

  // 사각형(RECTANGLE)일 때
  double length;
  double width;

  // 원(CIRCLE)일 때
  double radius;

  // 사각형용 생성자
  Figure(double length, double width) {
    shape = Shape.RECTANGLE;
    this.length = length;
    this.width = width;
  }

  // 원용 생성자
  Figure(double radius) {
    shape = Shape.CIRCLE;
    this.radius = radius;
  }

  double area() {
    switch(shape) {
      case RECTANGLE:
        return length * width;
      case CIRCLE:
        return Math.PI * (radius * radius);
      default:
        throw new AssertionError(shape);
    }
  }
}
 
### 태그 달린 클래스의 단점
1. 열거(enum) 타입 선언, 태그 필드, switch 문장 등 쓸데 없는 코드가 많다.
2. 여러 구현이 하나의 클래스에 혼합되어 있어서 가독성이 좋지 않다.
3. 다른 의미를 위한 코드가 함께 있으니 상대적으로 메모리도 더 차지한다.
4. 필드를 final로 선어하려면 해당 의미에 사용되지 않는 필드까지 생성자에서 초기화해야 한다.
5. 또 다른 의미를 추가하려면 코드를 수정해야 한다. 특히 switch 문장에도
6. 인스턴스 타입만으로는 현재 나타내는 의미를 파악하기 어렵다.

 

## 개선 => 클래스 계층구조
태그 달린 클래스 형태를 클래스 계층 구조로 바꿔보자
### 클래스 계층 구조로 만드는 방법
1. 계층 구조의 루트(root)가 될 추상 클래스를 정의한다.
2. 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스에 추상 메서드로 선언한다.
3. 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가한다.
4. 모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올린다.
5. 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의한다.
6. 각 하위 클래스에는 각자의 의미에 해당하는 데이터 필드를 넣는다.
7. 루트 클래스가 정의한 추상 메서드를 각자의 의미에 맞게 구현한다.
abstract class Figure {
  abstract double area();
}

class Circle extends Figure {
  final dobule radius;
  Circle(double radius) {
    this.radius = radius;
  }
  @Override
  double area() {
    return Math.PI * (radius * radius);
  }
}

class Rectangle extends Figure {
  final double length;
  final double width;
  Rectangle(double length, double width) {
    this.length = length;
    this.width = width;
  }
  @Override
  double area() {
    return length * width;
  }
}

 

- 간결하고 명확해졌으며, 쓸데없는 코드들이 모두 사라졌다. 각 의미를 독립된 클래스에 담았기 때문에 관련없는 데이터 필드는 모두 제거되었다.
- 타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일 타임에서의 타입 검사 능력도 높여준다. 또한 클래스 계층 구조라면, 아래와 같이 정사각형(Square)가 추가될 때도 간단하게 반영할 수 있다.
class Square extends Rectangle {
  Square(double side) {
    super(side, side);
  }
}​



## 정리
- 태그 달린 클래스를 써야 하는 상황은 거의 없다.
- 새로운 클래스를 작성하는 데 태그 필드가 등장한다면 태그를 없애고 계층 구조로 대체하는 방법을 생각해보자.
- 기존 클래스가 태그 필드를 사용하고 있다면 계층 구조로 리팩터링 하는 것을 고민하자.
728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !