본문 바로가기

java/이펙티브 자바

[item 17~18]

Item9~Item10 AJ

 

 


Item11~Item12 가을
Item13~Item14 AJ
Item15~Item16 김인호
Item17~Item18 민이/워니
Item19~Item20 Jake
Item21~Item22 워니/민이

 

 

4장 클래스와 인터페이스

 

item 17 변경 가능성을 최소화하라

 

클래스는 불변 클래스로 사용해라
불변 클래스가 아니라면 변경 부분을 최소한으로 줄이자
모든 필드는 private final

 

불변 클래스는 장점이 많다. 단점이라고는 잠재적 성능 저하
객체가 가질 수 있는 상태의 수를 줄이면
그 객체를 예측하기 쉬어지고, 오류 가능성이 낮아진다.
변경해야할 필드를 제외하고 final로 선언하자
모든 필드는 private final이어야 한다
생성자는 불변식 설정이 완료된
초기화가 끝난 객체를 생성해야한다

생성자, 초기화 메서드는 public으로 제공해서는 안된다.

예시:
java.util.concurrent   CountDownLatch 클래스

 

 

 

 

 

 

불변 클래스: 인스턴스의 내부 값을 수정할 수 없는 클래스다.

불변 인스턴스: 간직된 정보는 고정되어 객체가 파괴되기 전까지 절대 달라지지 않는다.

불변 클래스: String, BigInteager, BigDecimal

 

불변 클래스 가변 클래스
설계,구현,사용 쉬움

오류 적음
 
객체의 상태를 변경하는 메서드가 없다  
클래스 확장 할 수 없다 하위 클래스에서 
객체의 상태를 변하는 것을 막는다
상속을 막는다
모든 필드는 final 상속을 막는다
모든 필드는 private 클라이언트가 가변 객체를 접근해 수정할 수 없다.
자신 외에는 내부의 가변 컴포넌에 접근할 수 없다. 클래스에서 가변 객체를 참조하는 필드가 하나라도 있으면
클라이언트는 이 객체를 참조 사용 못하게 막아라.

접근자 메서드가 그 필드를 그대로 반환해도 안됨.

생성자, 접근자, readObject 메서드 : 방어적 복사해라

 

 

불변 객체 가변 객체
스레드 안전함
따로 동기화 할 필요 없음
어떤 스레드도 다른 스레드에 영향을 안 끼침
안심하고 공유할 수 있다
 
자주 쓰이는 값들을 상수로 제공하라
public static final
 
불변 클래스는
자주 사용되는 인스턴스를 캐싱함
인스턴스 중복을 안함
정적 팩터리를 제공함

여러 클라이언트가 인스턴스를 공유하여
메모리 사용량, 가비지 컬렉션 비용이 줄어든다.

새로운 클래스를 설계할 때
public 생성자 말고
정적 팩터리를 사용하면
클라이언트를 수정하지 않고, 캐시 기능을 덧붙일 수 있다.
 
불변 객체는 자유롭게 공유할 수 있다.
불변 객체 끼리 내부 데이터를 공유한다.

 

 

 

 

package com;

public final class Complex { // final 클래스

    private final double re;// final 필드 실수
    private final double im;// final 필드 허수

    public Complex(double re, double im) { // 생성자
        this.re = re;
        this.im = im;
    }

    public double realPart() {// 접근자 메서드
        return re;
    }

    public double imaginaryPart() {// 접근자 메서드
        return im;
    }

    public Complex plus(Complex c) {// 사칙연산
        return new Complex(re + c.re,im + c.im);// 새로운 인스턴스 반환
        // 실수+실수, 허수+허수
    }

    public Complex minus(Complex c) {// 사칙연산
        return new Complex(re - c.re,im - c.im);// 새로운 인스턴스 반환
        //실수-실수, 허수-허수
    }

    public Complex times(Complex c) {// 사칙연산
        return new Complex(re * c.re - im * c.im,re * c.im + im + c.re);
        // 새로운 인스턴스 반환
        // 실수*실수 - 허수*허수, 실수*허수 + 실수*허수
    }

    public Complex dividedBy(Complex c) {// 사칙연산
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp,
                (im * c.re - re + c.im) / tmp);// 새로운 인스턴스 반환
        //실수*실수 + 허수*허수 / 실수*실수 + 허수*허수 = 1
        //실수*허수 - 실수*허수 / 실수*실수 + 허수*허수
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Complex)) {
            return false;
        }

        Complex c = (Complex) o;
        return Double.compare(c.re, re) == 0
                && Double.compare(c.im, im) == 0;
    }

    @Override
    public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

    @Override
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }

}

 

BigDecimal BigInteager
java.math.BigDecimal java.math.BigInteager
불변 클래스다 int 범위 넘을 때 사용
부호 따로 저장
배열에는 값만 저장
String처럼 불변이다
new BigDecimal("숫자"); // 생성한다 new BigInteager("숫자"); // 생성한다
다른 타입으로 변환 가능
연산 메소드로 계산함
다른 타입으로 변환 가능
연산 메소드로 계산함

결론: 둘다 불변 클래스라고 한다.

Long의 숫자 범위를 벗어날 때 쓰는 숫자

3.00 - 2.10 을 하면 우리는

0.9라고 바로 알지만 컴퓨터는 메모리상 오류를 뱉어낸다. 실제로 0.89999..

그럴 때 사칙연산을 할 때 쓰는 것이 BigDecimal, BigInteager이다.

동영상에서는 두 개의 차이가 없었다.

https://www.youtube.com/watch?v=fVEwu0d_BRs 

 

 


   

 

 

 

item 18 상속보다는 컴포지션을 사용하라

 

 

기존 클래스를 확장하지 말고
새로운 클래스를 만들고, private 필드로 기존 클래스의 인스턴스를 참조하자.
상속은 진짜 하위타입인 상황에서만 써라
불필요하게 내부 구현만 노출하는 꼴이다

 

상속은 코드를 재사용하는 강력한 수단이지만,

캡슐화를 해친다

잘못 사용하면 객체의 유연성을 해치는 설계를 하게 되는 결과를 초래할 수 있습니다. 

 

위험한 경우

문서화가 안된 경우

다른 패키지의 구체 클래스를 상속함

 

상속이 캡슐화를 깨뜨리기 때문입니다.

상속은 하위 클래스가 상위 클래스에 대한 내부 구현 정보를 알게 합니다.

상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있습니다.

 

 

 

상속을 잘못 사용한 코드

package com;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;

public class InstrumentedHashSet <E> extends HashSet<E> {// HashSet을 상속받음

    private int addCount = 0;

    public InstrumentedHashSet() {
        System.out.println("생성자");
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        System.out.println(e+" add 메소드 오버라이드 addCount 전 "+addCount);
        addCount++;
        System.out.println(e+" add 메소드 오버라이드 addCount 후 "+addCount);
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {// 오류 발생
        System.out.println("c: "+c);
        addCount += c.size();
        System.out.println("addAll 메소드 오버라이드 addCount "+addCount);
        return super.addAll(c); //HashSet의 addAll() 메소드 호출 -> add 함수 호출
    }

    public int getAddCount() {
        return addCount;
    }

    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(List.of("틱", "틱틱", "펑"));

        System.out.println(s.getAddCount()); // 3이 아닌 6이 찍힌다.
    }
}

결과

 

AbstaractCollection.java

 

HashSet.java

List.of Arrays.asList
자바 9 부터 지원
정적 팩터리 메서드
리스트 생성

List listOf= List.of("a","b","c");
List<String> asList = Arrays.asList("a","b","c");

public boolean addAll(Collection c) {

Collection<? extends E> c
Public void showAllUnit(List<Units> units){

List내에서 <Units> 타입만 찾게 됩니다.
, Human, Fairy, Oak 찾을  없습니다.

이런 경우 사용할  있는 제너릭(generic)이
<? Extends E> 
와일드카드 “?” 사용해서 
아직 알려지지 않은 혹은 
미정의  Collection 타입이라는 의미에서 
Collection<?>
를 사용하면 
해결됩니다.

출처: https://tiboy.tistory.com/475 [신기한 연구소]
Public void showAllUnit(List<extends Units> units){

List
에서 와일드카드(wildcards) ? 의 타입을 찾아야 하는데 
바로 Units 클래스를 상속받은
Human, Fairy, Oak를 받을 수 있게 됩니다.

 

그 원인은 상위 클래스인 HashSet의 addAll() 메서드가

add 메서드를 사용해서 구현했기 때문이다

 

 

HashSet.java
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
 }


AbstractCollection.java
public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e)) // 여기서 호출되는 add는 우리가 재정의한 메서드이다.
            modified = true;
    return modified;
}

	//우리가 재정의한 메서드
    @Override
    public boolean add(E e) {
        addCount++; // 숫자를 올려줌
        return super.add(e);
    }

 

이렇게 자기 자신의 메소드를 사용하여 구현하는 것을 self-use라고 한다.

상위 클래스를 상속한 하위 클래스는 항상 이를 고려하여 메서드를 재정의해야한다. 또한, 보안 이슈도 있다. 상위 클래스에서 새로운 메서드가 추가되면, 하위 클래스에서 이 메서드를 사용해 “허용되지 않은” 원소를 추가할 수 있게 된다.

 

 

 

상속이 아닌 컴포지션(composition)으로 구성하자

기존 클래스를 확장하는 대신

1) 새로운 클래스를 만들고

2) private 필드로 기존 클래스의 인스턴스를 참조

기존 클래스가 새로운 클래스의 구성요소로 쓰인다 -> composition(컴포지션) 구성

 

새 클래스의 인스턴스 메서드는

기존 클래스의 메서드와 대응된다

--> 새로운 클래스는 기존 클래스클래스의 내부 구현 방식에서 벗어남

기존 클래스 새로운 메서드가 추가되도 영향 받지 않는다.

 

 

 

재사용할 수 있는 전달 클래스 public class ForwardingSet <E>

package com;

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

public class ForwardingSet <E> implements Set<E> { // HashSet이 아닌 인터페이스 타입을 구현한다.

    private final Set<E> s; // private 으로 인스턴스 변수를 가지고 생성자에서 매개변수로 받는다.
    public ForwardingSet(Set<E> s) {this.s = s;} // 아무 제너릭 타입 E가 아닌 Set 타입 E를 지정한다.

    public void clear()               { s.clear();            }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty()          { return s.isEmpty();   }
    public int size()                 { return s.size();      }
    public Iterator<E> iterator()     { return s.iterator();  }
    public boolean add(E e)           { return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection<?> c)
    { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
    { return s.addAll(c);      }
    public boolean removeAll(Collection<?> c)
    { return s.removeAll(c);   }
    public boolean retainAll(Collection<?> c)
    { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public <T> T[] toArray(T[] a)      { return s.toArray(a); }

    @Override public boolean equals(Object o)
    { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }

}

 

  • 인터페이스 Set을 구현한 ForwardingSet private final 접근 제어자로 파라미터로 받은 HashSet(기존 클래스)를 필드로 가지고 있는다. -> 컴포지션
  • 매개변수로 받은 HashSet(기존 클래스)의 기능을 그대로 전달한다.

 

 

 

package com;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class InstrumentedSet<E> extends ForwardingSet<E> {// HashSet을 상속-> ForwardingSet 상속 변경
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) { // 생성자
        super(s);
    }

    @Override public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }

    public static void main(String[] args) {
        Set<String> s = new InstrumentedSet<String>(new HashSet<>());
        s.addAll(List.of("틱", "틱틱", "펑"));

        System.out.println(((InstrumentedSet<String>) s).getAddCount()); // 3 반환
    }

}
  • 새로 구성한 InstrumentedSet은 컴포지션 방식으로 구현한 ForwardingSet을 상속한다.
  • 이렇게 하면 addAll 메서드를 호출했을 때, 새로운 클래스인 InstrumentedSet이 재정의한 add메서드가 호출되는게 아니라 기존 클래스인 HashSet의 addAll이 호출되게 된다.
  • HashSet 뿐만 아니라 어떠한 Set 구현체라도 이용할 수 있어서 유연성이 크다.
    • 이러한 클래스를 래퍼 클래스라고 하며, 다른 Set에 기능을 덧씌운다는 뜻에서 데코레이터 패턴(Decorator pattern)이라고도 한다.