ch9 일반적인_프로그래밍_원칙 - LenKIM/everyone-is-effective-java-study GitHub Wiki
지역변수, 제어구조, 라이브러리, 데이터 타입, 그리고 언어 경계를 넘나드는 기능인 리플렉션과 네이티브 메소드를 다룸. 마지막으로는 최적화와 명명 규칙을 논한다.
클래스와 멤버의 접근 권한을 최소화하라.
-
**지역변수의 범위를 줄이는 가장 강력한 기법은 역시 '가장 처음 쓰일 때 선언하기'**다. 사용하려면 멀었는데, 미리 선언부터 해두면 코드가 어수선해져 가독성이 떨어 진다. 변수를 실제로 사용하는 시점엔 타입과 초깃값이 기억나지 않을 수도 있다.
-
거의 모든 지역변수는 선언과 동시에 초기화해야 한다. 초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 한다. Try-catch 문은 이 규칙에서 예외다. 변수를 초기화하는 표현식에서 검사 예외를 던질 가능성이 있다면 try 블록 안에서 초기화 해야 한다.(그렇지 않으면 메소드로 전파됨).
// Reflective instantiaion demo (Page 283) public class ReflectiveInstantiation { // Reflective instantiation with interface access public static void main(String[] args) { // Translate the class name into a Class object Class<? extends Set<String>> cl = null; try { cl = (Class<? extends Set<String>>) // Unchecked cast! Class.forName(args[0]); } catch (ClassNotFoundException e) { fatalError("Class not found."); } // Get the constructor Constructor<? extends Set<String>> cons = null; try { cons = cl.getDeclaredConstructor(); } catch (NoSuchMethodException e) { fatalError("No parameterless constructor"); } // Instantiate the set Set<String> s = null; try { s = cons.newInstance(); } catch (IllegalAccessException e) { fatalError("Constructor not accessible"); } catch (InstantiationException e) { fatalError("Class not instantiable."); } catch (InvocationTargetException e) { fatalError("Constructor threw " + e.getCause()); } catch (ClassCastException e) { fatalError("Class doesn't implement Set"); } // Exercise the set s.addAll(Arrays.asList(args).subList(1, args.length)); System.out.println(s); } private static void fatalError(String msg) { System.err.println(msg); System.exit(1); } }
-
Iterator
와 같은 반복자를 사용해야 하는 상황이라면,while
문보다for-each
문을 사용하는 것이 낫다. 휴먼에러를 발생시킬 확률이 높기 때문이다.for(Iterator<Element> i = c.iterator(); i.hasNext();){ Element e = i.next(); ... // e와 i로 무엇가를 한다. }
-
지역변수 범위를 최소화하는 마지막 방법은 메서드를 작게 유지하고 한 가지 기능에 집중하는 것이다. 한 메서드에서 여러 가지 기능을 처리한다면 그중 한 기능과만 관련된 지역변수라도 다른 기능을 수행하는 코드에서 접근할 수 있을 것이다. 한 메서드에서 여러 가지 기능을 처리한다면 그중 한 기능과만 관련된 지역변수라도 다른 기능을 수행하는 코드에서 접근할 수 있을 것이다. 해결책은 단순히 메서드를 기능별로 쪼개면 된다.
for(Iterator<Element> i = c.iterator; i.hasNext();){
Element e = i.next();
... // 무엇가를 한다.
}
for(int i = 0; i < a.length; i++){
... // a[i]로 무엇가를 한다.
}
이 관용구들은 while 문보다도 낫지만 가장 좋은 방법은 아니다. 가장 좋은 방법은 for-each 를 사용하는 것.
for (Element e: element){
... // e로 무엇가를 한다.
}
for (Suit suit : suits)
for (Rank rank: ranks)
deck.add(new Card(suit,rank));
이렇게 for-each
쓸 수 없는 조건도 있다.
- 파괴적인 필터링(destructive filtering) - 컬렉션을 순회하면서 선택된 원소를 제거해야 한다면 반복자의 remove 메서드를 호출해야 한다. 자바8부터는 Collection의 removeIf 메서드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다.
- 변형(transforming) - 리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 교체해야 한다면 리스트의 반복자나 배열의 인덱스를 사용해야 한다.
- 병렬 반복(parallel iteration) - 여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 사용해 엄격하고 명시적으로 제어해야 한다.
전통적인 for문과 비교했을 때 for-each 문은 명료하고, 유연하고, 버그를 예방해준다. 성능 저하도 없다. 가능한 모든 곳에서 for문이 아닌 for-each문을 사용하자.
바퀴를 다시 발명하지 말자. 아주 특별한 나만의 기능이 아니라면 누군가 이미 라이브러리 형태로 구현해놓았을 가능성이 크다. 그런 라이브러리가 있다면, 쓰면 된다. 있는지 잘 모르겠다면 찾아보라. 일반적으로 라이브러리의 코드는 여러분이 직접 작성한 것보다 품질이 좋고, 점차 개선될 가능성이 크다. 여러분의 실력을 폄하하는게 아니다. 코드 품질에도 규모의 경제가 적용된다. 즉, 라이브러리 코드는 개발자 각자가 작성하는 것보다 주목을 휠씬 많이 받으므로 코드 품질은 그만큼 높아진다.
float
와double
타입은 과학과 공학 계산용으로 설게뙤었다. 이진 부동소수점연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 '근사치' 로 계산하도록 세심하게 설겨되었다. 따라서, 정확한 결과가 필요할 때는 사용하면 안되. float와 double 타입은 특히 금융 관련 계산과는 맞지 않는다. 이유는 음의 거듭 제곱 수를 표현할 수 없기 때문이다.
System.out.println(1.03 - 4.32) // 0.610000000002
반올림도 틀린 값이 나올수 있다.
금융 계산에는 부동 소수를 사용해선 안된다. 금융 계산에는 BigDecimal, int 혹은 long을 사용해야 한다.
import java.math.BigDecimal;
public class BigDecimalChange {
public static void main(String[] args) {
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS;
funds.compareTo(price) >= 0;
price = price.add(TEN_CENTS)) {
funds = funds.subtract(price);
itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds);
}
}
그러나 이런 BigDecimal
도 두 가지 단점이 있다. 기본 타입보다 쓰기가 휠씬 불편하고, 휠씬 느리다. BigDecimal의 대안으로 int 혹은 long 타입을 쓸 수도 있다. 그럴 경우 다룰 수 있는 값의 크기는 제한되고, 소수점을 직접 관리해야 한다.
18자리가 넘어가면 BigDecimal을 사용해야 한다.
박싱된 기본타입과 기본타입의 주된 차이점은 세가지.
- 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성(identity)이란 속성을 갖는다. 달리 말하면 박싱된 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별될 수 있다.
- 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유혀하지 않는 값, 즉 null을 가질 수 있다.
- 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
위 차이점을 이해하지 못할 경우 어떤 일이 일어나는지 살펴보자.
Interger
값을 오름차순으로 정렬하는 비교자다. Integer 그 자체로 순서가 있으니 이 비교자가 실질적인 의미는 없지만,
Comparator<Integer> naturalOrder =
(i, j) -> ( i < j ) ? -1 : (i==j ? 0 : 1)
위 코드는 겉으로 보기에는 문제가 없는 것처럼 느껴지지만, naturalOrder.compare(new Integer(42), new Integer(42))
의 값을 출력해보면 0을 출력해야 하지만, 실제로는 1이 출력된다.
왜 일까?
i < j
를 비교하는 과정에서는 오토박싱이 비교되어 잘 동작된다 . 그러나 i == j
의 경우 서로 다른 인스턴스의 비교로 이어져 1
출력된다. 즉 박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어난다.
실무에서 이와 같이 기본 타입을 다루는 비교자가 필요하다면 Comaprator.naturalOrder()
를 사용하자. 비교자를 직접 만들면 비교자 생성 메서드나 기본 타입을 받는 정적 compare 메서드를 사용해야 한다.
위 문제를 해결하기 위해서는 아래와 같이 코드를 수정해야 한다.
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
int i = iBoxed, j = jBoxed;
return i < j ? -1 : (i == j ? 0 : 1)
}
또한 박싱된 타입은 null을 일으킬 잠재적 요소를 가지고, 실수로 지역변수 sum을 박싱된 기본 타입으로 선언하여 느려질 수 있다.
기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 가능하면 기본 타입을 사용하라. 기본 타입은 간단하고 빠르다. 박싱된 기본 타입을 써야 한다면 주의를 기울이자. 오토박싱이 박싱된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 그 위험까지 없애주지는 않는다. 두 박싱된 기본 타입을 == 연산자로 비교한다면 식별자 비교가 이뤄지는데, 이는 여러분이 원한 게 아닐 가능성이 크다. 같은 연산에서 기본 타입과 박싱된 기본 타입을 혼용하면 언박싱이 이뤄지며, 언박싱 과정에서 NullPointerException을 던질 수 이다. 마지막으로, 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 나을 수 있다.
문자열은 다른 값 타입을 대신하기에 적절하지 않다. 일반화해 이야기하자면, 기본 타입이든 참조타입이든 적절한 값 타입이 있다면 그것을 사용하고, 없다면 새로 하나 작성하라.
-
문자열은 열거 타입을 대신하기에 적절하지 않다. 상수를 열거할 때는 문자열보다는 열거 타입이 월등히 낫다.
-
문자열은 혼합 타입을 대신하기에 적합하지 않다.
String compoundKey = className + "#" + i.next();
위와 같은 코드는 문자열을 파싱해야 해서 느리고, 귀찮고, 오류 가능성이 커진다. 적절한 equals, toString, compareTo 메서드를 제공할 수 없으며, String이 제공하는 기능에만 의존해야 한다. -
문자열은 권한을 표현하기에 적합하지 않다. 권한(capacity)을 문자열로 표현하는 경우가 종종 있다. 예를 들어 스레드 지역변수 기능을 설계한다고 해보자. 그 이름처럼 각 스레드가 자신만의 변수를 갖게 해주는 기능이다. 자바가 이 기능을 지원하기 시작한 때는 자바 2부터로, 그 전에는 프로그래머가 직접 구현해야 했다.
public class ThreadLocal { private ThreadLocal(){ } // 객체 생성 불가 // 현 스레드의 값을 키로 구분해 저장한다. public statuc void set(String key, Object value); // (키가 가리키는) 현 스레드의 값을 반환한다. public static Object get(String key); }
이 방식의 문제는 스레드 구분용 문자열 키가 전역 이름공간에서 공유된다는 점. 그러나 고유한 키가 제공되지 않는다면 중복을 일으킬테고, 의도치 않는 버그를 일으킬 것이다.
public class ThreadLocal { private ThreadLocal() {} public static class Key { //권한 key() {} } //위조 불가능한 고유 키를 생성한다. public static Key getKey() { return new Key(); } public static void set(Key key, Object value); public static Object get(Key key); }
이 방법은 앞선 문자열 기반 API의 문제 두 가지를 모두 해결해주지만, 개선할 여지가 있다. set과 get은 이제 정적 메서드일 이유가 없으니 key 클래스의 인스턴스 메서드로 바꾸자. 이렇게 하면 Key는 더 이상 스레드 지역변수를 구분하기 위한 키가 아니라, 그 자체가 스레드 지역변수가 된다.
//리팩터링 Key를 ThreadLocal로 변경 public final class ThreadLocal{ public ThreadLocal(); public void set(Object value); public Object get(); }
타입에 안전하게 만들기 위해서
public final class ThreadLocal<T> { public ThreadLocal(); public void set(T value); public T get(); }
- 문자열 연결 연산자는 문자열 n개를 잇는 시간은 n^2 에 비례한다. 문자열은 불변이라서 두 문자열을 연결할 경우 양쪽의 내용을 모두 복사해야 하므로 성능 저하를 피할 수 없는 결과다.
- 성능을 포기하고 싶지 않다면 String 대신 StringBuilder를 사용하자.