관리 메뉴

루시와 프로그래밍 이야기

9장 일반적인 프로그래밍 원칙(ITEM 57~60) 본문

스터디/이펙티브 자바

9장 일반적인 프로그래밍 원칙(ITEM 57~60)

Lucy_Ko 2022. 12. 15. 04:07

이번 장에서는 자바 언어의 핵심 요소에 집중한다.

  • 지역변수
  • 제어구조
  • 라이브러리
  • 데이터타입
  • 언어 경게를 넘나드는 기능인 리플렉션과 네이티브 메서드

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

<ITEM15.클래스와 멤버의 접근 권한을 최소화하라 >와 취지가 비슷하다

지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아진다.

*지역변수를 코드 블록의 첫머리에 선언하는 경우가 많고, 습관처럼 따르지만 (C등에서) 자바에서는 어디서든 선언가능 (C도 가능하게 변하긴함)

 

지역변수를 최소화하는 방법

  • 지역변수를 줄이는 가장 강력한 기법, "사용하기 직전에 선언하기"
    지역변수를 사용하면 가독성도 떨어지고, 블록이 끝나고 살아있어 잘못 사용하여 끔찍한 결과를 초래할 수 있음
  • 거의 모든 지역변수는 선언과 동시에 초기화 해야한다.
    초기화에 필요한 정보가 충분하지 않다면 충분할때까지 선언을 미뤄라!
    단, try-catch 에서는 예외다. try안에서 초기화해라
  • 반복문은 독특한 방식으로 범수 범위를 최소화 해준다.
    반복변수의 값을 반복문이 종료된 뒤에도 써야하는 상황이 아니면 while문보다 for문을 써라
57-1. 컬렉션이나 배열을 순회할 때. 권장 for
for (Element e:c){
	... //e로 무언가를 한다.
}

57-2. 반복자가 필요할 때
//while 문 권장x
Iterator<Element> i = c.iterator();
while(i.hasNext()){
		doSomthing(i.next());
}
...
Iterator<Element> i2 = c2.iterator();
while(i.hasNext()){                //복사했다는 가정시에, i2가 아니라 i로 오타가 생길 수 있다.
	doSomethingElse(i2.next());
}
//그러나 있는 변수이므로, 컴파일 오류가 나지 않아 찾기 힘들다.

//for문 권장
for(Iterator<Element> i = c.iterator(); i.hasNext();){
		Element e = i.next();
		...
		//e와 i 모두를 사용한다.
}

// 오타가 발생해도 i를 찾을 수 없다는 컴파일 오류를 받을 수 있다.
for(Iterator<Element> i2 = c2.iterator(); i.hasNext();){
		Element e2 = i2.next();
		...
}
  • 지역변수를 최소화하는 마지막 방법, 메서드를 작게 유지하고 한 가지 지능에 집중하는 것
    메서드를 기능별로 쪼개라

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

*전통적인 for문으로 컬렉션을 순회하는 코드 예시

58-1. 컬렉션 순회하기
for(Iterator<Element> i = c.iterator(); i.hasNext()) {
  Element e = i.next();
  ... // e로 무언가를 한다
}

58-2. 배열 순회하기
for (int i = 0; i < a.length; i++) {
  ... // a[i]로 무언가를 한다.
}

while문보다는 낫지만 가장 좋은 방법은 아니다. 이유는?

  • 쓰이는 요소 종류가 늘어나면 오류가 생길 가능성이 높아진다
  • 변수를 잘못 사용하여 오류가 생길 가능성이 높아진다 (컴파일러가 잡아주지 못하면 최. 악.)
  • 컬렉션이냐 배열이냐에 따라 코드 형태가 상당히 달라진다

for-each 문을 사용하면 모두 해결

*참고로 for-each문의 정식 이름은 '향상된 for문'임

반복자와 인덱스변수를 사용하지 않으니 코드가 깔끔해지고 오류가 날 일이 없다.

// 컬렉션과 배열을 순회하는 올바른 관용구
for (Element e : elements) { //콜론(:)은 "안의(in)"이라고 읽으면 된다.반복 대상이 컬렉션이든 배열이든 for-each 문을 사용해도 속도는 그대로다.
    ... //e로 무언가를 한다. 
}

 

*기존  for문의 문제점 예시

58-4 버그를 찾아보자
enum Suit {CLUB, DIAMOND, HEART, SPACE}
enum Rank {ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING}
...
static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());

List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext()) {
  for (Iterator<Rank> j = ranks.iterator(); j.hasNext()) {
    deck.add(new Card(i.next(), j.next()));
  }
}

바깥 컬렉션(suits)의 반복자에서 next 메서드가 너무 많이 불린다.

마지막 줄의 i.next()는 숫자(Suit) 하나당 한 번씩만 불려야 하는데

안쪽 반복문에서 호출되는 바람에 카드(Rank)하나당 한 번씩 불리고 있다.

→ 숫자가 바닥난다면 NoSuchElementException 을 던질 것이다.

// 예외를 고치는 방법
for (Iterator<Suit> i = suits.iterator(); i.hasNext()) {
  Suit suit = i.next();
  for (Iterator<Rank> j = ranks.iterator(); j.hasNext()) {
    deck.add(new Card(suit, j.next()));
  }
}

보기좋지 않다. for-each를 사용해보자

for (Suit suit : suits) 
    for (Rank rank : ranks)
        deck.add(new Card(suit, rank));

 

*for-each를 사용할 수 없는 상황

  • 파괴적인 필터링
    컬렉션을 순회하면서 선택된 원소를 제거해야 한다면,  remove메소드를 사용한다. 오류가 발생한다.
    →자바8부터는 Collection의 romveIf메서드를 사용해 컬렉션을 명시적으로 순회하는 일을 피할 수 있다.
  • 변형
    리스트나 배열을 순회하면서 그 원소의 값 일부 혹은 전체를 교체해야한다면
    리스트의 반복자나 배열의 인덱스를 사용해야 한다.
  • 병렬반복 (ex.58-4)
    여러 컬렉션을 병렬로 순회해야 한다면 각각의 반복자와 인덱스 변수를 상용해
    엄격하고 명시적으로 제어해야 한다

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

직접만든 코드 나쁜 예시

static Random rnd = new Random();
static int random(int n) {
    return Math.abs(rnd.nextInt()) % n;
}
  • n이 그리 크지 않거나 n이 2의 제곱수 일때 같은 수열 반복...뷀류임
  • n이 2의 제곱수가 아니라면 몇몇 숫자가 더 자주 반환
  • n값이 크다면? 현상 두드러쥠!!

이 결함을 해결하려면 의사난수 생성기, 정수론, 2의 보수 계산 등에 조예가 깊어햐 한다

그러나, Random.nextInt(int)가 이미 해결해뒀다.

메서드의 자세한 동작 방식을 몰라도 사용할 수 있다 (방식이 궁금하면 api문서나 소스코드를 살펴보면 된다)

 

표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과

여러분보다 앞서 사용한 다른 프로그래머들의 경험을 확용할 수 있다.

자바7부터는 Random보다는 ThreadLocalRandom으로 대패하면 더 작동을 잘한다.

 

  • 핵심적인 일과 크게 관련 없는 문제를 해결하느라 시간을 허비하지 않아도 된다
  • 따로 노력하지 않아도 성능이 지속해서 개선된다
  • 기능이 점점 많아진다
  • 여러분이 작성한 코드가 많은 사람에게 낯익은 코드가 된다
    더 읽기 쉽고, 유지보수 및 재확용하기 쉬운 코드가 된다. (장점 good)
  • 메이저 릴리즈마다 주목할만한 수 많은 기능이 라이브러리에 추가된다
    웹페이지로 제공하니 한번쯤 읽어볼만 하다. (ex.https://www.java.com/ko/download/help/release_changes.html)

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

float와 double 타입은 과학과 공학예산용으로 설계되어있다.

이진 부동소수점연산에 쓰이며, 넓은 범위의 수를 빠르게 정밀한 '근사치'로 계산하도록 세심하게 설계되었다.

float와 double 타입은 특히 금융 관련 계산과는 맞지 않는다.
→0.1혹은 10의 음의 거듭제곱수를 표현할 수 없기 때문이다.

        System.out.println(1.03 - 0.42);
        // 결과 : 0.6100000000000001
        System.out.println(1.00 - 9 * 0.10);
        // 결과 : 0.09999999999999998
		System.out.println("======== 코드 60-1 ========");
		double funds = 1.00;
		int itemsBought = 0;
		for(double price = 0.10; funds >= price; price +=0.10){
			System.out.println(price);
			funds -= price;
			itemsBought++;
		}
		System.out.println(itemsBought + "개 구입");
		System.out.println("잔돈(달러):" + funds);
		System.out.println("! 실제로 4개가 구입되어야 하지만 세번째 값이 0.3000000...4 가 되면서 

https://velog.io/@yhlee9753/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C%EC%9E%90%EB%B0%94-item60.-%EC%A0%95%ED%99%95%ED%95%9C-%EB%8B%B5%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%98%EB%8B%A4%EB%A9%B4-float%EC%99%80-double%EC%9D%80-%ED%94%BC%ED%95%98%EB%9D%BC

금융 계산에서는 BigDecimal, int 혹은 long 을 사용해야 한다

*BigDecimal의 생성자 중 문자열을 받는 생성자를 사용했음에 주목

→계산시 부정확한 값이 사용되는 걸 막기 위해 필요한 조치

BigDecimal bigNumber = new BigDecimal("10000.12345");

단점은, 기본 타입보다 쓰기가 불편하고 느리다.

단발성 계산이라면 상관으며 대안으로 int 나 long 을 쓸 수 있다.

int 나 long 을 쓴 다면 소수점을 직접 관리한다.

혹은 소수점을 쓰지 않을 수 있도록 단위를 바꾼다.(달러 -> 센트)

Comments