이펙티브자바, 57- 일반적인 프로그래밍 원칙

57 지역변수의 범위를 최소화하라

  • 지역변수에 대한 이해와 오남용을 방지하기 위하여, 지역변수의 사용은 최소화 한다.
  • 만약 사용한다면 선언을 먼저하지 않는다. 가능한 초기화할 때 선언한다.
  • 지역변수를 최소화 할 수 있는 방법을 활용한다. 반복문에서 지역변수를 최소화하기 위하여 while -> for -> 항상된 for문 순으로 사용을 우선한다.

58 전통적인 for 문보다는 for-each 문을 사용하라

List<String> list = sampleList();

// while 보다는 for문이 낫다.
// for문을 사용할 경우 향상된 for문을 사용한다.
for(String s : list) {
	System.out.println(s); // 반복문 블록 안에 변수가 단 하나 s 밖에 없다.
}

// iterator를 사용할 경우 아래와 같이 사용한다. 다만 반복문 블록 안에 i.next()로 지역변수를 초기화함을 확인할 수 있다. 
// 그러므로, 가능하다면 아래의 코드 역시 향상된 for문을 사용할 수 있도록 한다.
for(Iterator<String> i = list.iterator(); i.hasNext();) {
	String s = i.next();
	System.out.println(s);
}

// n의 초기화를 반복문 초기화 때 수행한다.
for(int i = 0, n = expensiveComputation(); i < n; i++) {
	System.out.println("하이" + i);
}

59 라이브러리를 익히고 사용하라

  • 만약 난수를 생성하려면 새롭게 난수 생성을 위한 기능을 구현해야 하는가? 그렇진 않다. 우리는 보통 자바가 제공하는 Random 라이브러리를 주로 사용한다.
  • 표준 라이브러리를 사용하는 것의 장점은 매우 많다.
  • 전문가의 지식과 경험이 녹아든 기능을 매우 편리하게 사용할 수 있다.
  • 부수적인 업무는 라이브러리로 대체하고, 중요한 업무에 더 집중할 수 있다.
  • 버전이 올라갈 수록 성능과 기능이 자동으로 추가된다.
  • 대부분의 자바 개발자가 공유하는 지식이기때문에 유지보수에 좋다.
  • 다만, 현대 자바는 Random보다는 ThreadLocalRandom를 주로 사용한다. Random의 자체적인 결함, 성능, 편의기능, 동시성 문제 등 다양한 이유에 의해서이다.
  • 표준라이브러리를 사용하는 것과 더불어 새로운 기능에 대하여 학습해야 할 필요가 있다.
  • 자바 개발자라면 적어도 java.lang, java.util, java.io + java.util.concurrent 에는 익숙해지자.

60 정확한 답이 필요하다면 float와 double은 피하라

  • 부동소수점 문제로 인하여, 금융 등 엄격한 숫자 계산이 필요할 경우, BigDecimal 을 사용해야 한다.

61 박싱된 기본타입보다는 기본타입을 사용하라

  • 박싱된 기본타입과 기본타입은 분리해서 사용해야 한다. 그렇지 않으면 코드의 문제가 발생하거나 성능 상 문제가 발생한다.
  • 특별하게 박싱된 기본타입이 필요하지 않으면 기본타입을 사용한다.
  • 박싱타입으로 인하여 발생할 수 있는 문제를 아래에 나열했다.

박싱된 기본타입을 비교할 경우

  • Comparator를 구현과정에서, 0을 리턴할 때는 동일성을 비교하는 == 연산자를 사용했다.
  • 기본타입일 경우 정상적으로 동작한다. 하지만 객체인 Integer의 경우 각각 다른 메모리를 차지하는 객체로 생성하였기 때문에, ==이 false가 되어 영엉 0을 리턴하지 못한다.
  • 이 경우 박싱된 타입을 강제로 꺼내야 한다.
Comparator<Integer> compare = (i, j) -> (i > j) ? -1 : (i == j ? 0 : 1);

System.out.println("compare.compare(1, 2) = " + compare.compare(1, 2));
System.out.println("compare.compare(1, 1) = "+compare.compare(1, 1));
System.out.println("compare.compare(2, 1) = "+compare.compare(2, 1));


System.out.println("===========");
System.out.println("compare.compare(1, 2) = " + compare.compare(new Integer(1), new Integer(2)));
System.out.println("compare.compare(1, 1) = "+compare.compare(new Integer(1), new Integer(1))); // 객체간 비교(==)를 할 경우 다르다고 나온다.
System.out.println("compare.compare(2, 1) = "+compare.compare(new Integer(2), new Integer(1)));

Comparator<Integer> compare2 = (ii, jj) -> {
	int i = ii;
	int j = jj;
	return (i > j) ? -1 : (i == j ? 0 : 1);
};

System.out.println("compare2.compare(1, 2) = " + compare2.compare(new Integer(1), new Integer(2)));
System.out.println("compare2.compare(1, 1) = "+compare2.compare(new Integer(1), new Integer(1))); // int로 언박싱을 한 후 정상동작함을 확인할 수 있다.
System.out.println("compare2.compare(2, 1) = "+compare2.compare(new Integer(2), new Integer(1)));

초기화되지 않은 박싱타입을 꺼낼 때

  • 만약 아래와 같이 초기화되지 않은 값을 꺼내려고 할 때 NullPointerException이 발생한다.
  • 이 경우 선언을 Integer가 아닌 int로 하면 해결된다.
Integer i; // int i;

@Test
void nullPoint() {
	if(i>100)
		System.out.println(i+"는 100보다 크다");
}

박싱타입으로 연산할 때

  • 자바는 연산을 할 때 박싱타입이 아닌 기본타입으로 한다. 박싱타입을 연상할 때, 해당 객체를 언박싱한 후, 연산한 후, 다시 박싱한다. 많은 오버헤드가 발생한다.
  • 실제로 아래의 테스트를 한 결과 약 24배의 차이가 발생했다.
Long boxedLong = 0l;
long primitiveLong = 0l;


long start1 = System.currentTimeMillis();
for(int i=0; i<Integer.MAX_VALUE; i++) {
	boxedLong += i;
}
System.out.println("첫 번째 시간 : "+(start1-System.currentTimeMillis()));

long start2 = System.currentTimeMillis();
for(int i=0; i<Integer.MAX_VALUE; i++) {
	primitiveLong += i;
}
System.out.println("두 번째 시간 : "+(start2-System.currentTimeMillis()));

62 다른 타입이 적절하다면 문자열 사용을 피하라

  • 숫자, enum 등 문자열보다 나은 대안이 있으면, 문자열은 사용하지 않는다.

63 문자열 연결은 느리니 주의하라

  • String 보다 StringBuilder를 사용한다.

64 객체는 인터페이스를 사용해 참조하라

  • 객체를 선언할 때 인터페이스를 데이터 타입한다. 불가피하게 인터페이스로 선언할 수 없으면, 클래스 계층 구조 중 가장 덜 구체적인(상위) 클래스를 사용한다.
  • 인터페이스로 선언할 때, 필요에 따라 구현 클래스를 변경하여, 유연한 코드를 작성할 수 있다.
Set<String> set1 = new HashSet<>(); // 좋은 예
HashSet<String> set2 = new HashSet<>(); // 나쁜 예

65 리플렉션보다는 인터페이스를 사용하라

  • 리플렉션은 강력한 기능을 제공하지만 몇 가지 단점이 존재한다.
    • 컴파일 시점에서 오류를 잡을 수 없다. 타입 검사 등의 이점을 누릴 수 없다. 오류가 런타임 때로 미뤄진다.
    • 코드가 복잡하고 장황하다.
    • 성능이 떨어진다.
  • 그러므로 리플렉션은 제한된 상황에서 사용한다. 리플렉션은 인스턴스 생성에만 쓰고, 인터페이스 등 상위 클래스로 형변환해 사용한다.
  • 사견인데, 리플렉션이 인스턴스를 생성하는 시점을 스프링 빈으로 등록하여 어플리케이션 로딩 시점으로 만들 수 있다. 이 경우, 빈 등록 과정에서 에러를 인지할 수 있다. 그리고 로딩은 느려지지만 그 시점에서 빈을 등록하기 때문에 성능 상 여러 문제가 해소된다고 알고 있다. 컴파일 - 런타임 가운데 어플리케이션 로딩 시점을 우리는 둘 수 있다.

66 네이티브 메서드는 신중히 사용하라

67 최적화는 신중히 하라

최적화를 할 때는 다음 두 규칙을 따르라. 첫 번째, 하지마라. 두 번째, (전문가 한정) 아직 하지 마라. 다시 말해, 완전히 명백하고 최적화되지 않은 해법을 찾을 때까지는 하지 마라. M.A 잭슨 (Jackson75)

  • 처음에 위 경구를 봤을 때 엄청 웃었다. 누구나 최적화를 이야기하는데 최적화를 하지 말라고 하다니!
  • (사실) 이펙티브자바를 읽는 이유는 좋은 프로그램을 만들기 위해서다. 저자는 빠른 프로그램보다는 좋은 프로그램을 작성하기를 제안한다.
  • 다만, 성능에 영향을 줄 수 있는 설계를 피해야 한다. API, 네트워크 프로토콜, 영구 저장용 데이터 포맷 등은 한 번 결정되면 이후 수정하기 어렵다.
  • 특히 외부에 노출되는 public API는 설계에 있어서 신중해야 한다. 내부의 설계는 숨기면 숨길수록, 차후 최적화의 가능성이 높다.
  • 최적화를 하기 전후로 프로파일링 도구를 활용하여 최적화를 어디에 할지 판단해야 한다. 자바는 코드와 cpu의 동작 사이에 추상화 격차가 큰 언어이다. 자바는 버전이 올라갈 수록 내부 구조가 더 복잡해진다. 그렇기 때문에 단순하게 코드를 읽고 최적화의 여부를 판단하기 어렵다. 최적화가 필요로 한 부분을 판단하고, 최적화 전후의 성능을 비교해야 한다.

68 일반적으로 통용되는 명명 규칙을 따르라.

  • 패키지는 8자 이하로 한다.
  • boolean 을 반환하는 메서드는 is 혹은 has로 시작한다.
  • boolean 이 아닌 값을 반환하는 경우, get으로 보통 시작하나, 필요에 따라 변경할 수 있다.
  • 객체의 타입을 변경할 경우 toType 형태를 가진다. (toString, toArray)
  • 객체를 다른 뷰로 보여줄 경우 asType 형태를 가진다. (asList)
  • 객체의 값을 기본타입값으로 반환하는 경우 typeValue 형태를 가진다. (intValue)
  • 정적 팩터리의 이름은 from, of, valueOf, instance, getInstance, newInstance, getType, newType 등을 사용한다.