본문 바로가기

개발/[스터디] 이펙티브 자바

아이템 17. 변경 가능성을 최소화해라

개념

불변 클래스

인스턴스 내부 값을 수정할 수 없는 클래스

 

어떻게 불변 클래스를 만들까 (규칙 5가지)

1. 객체의 상태를 변경하는 메서드를 제공하지 않는다. 

public class Car {

    private final int position;

    public Car(int position) {
        this.position = position;
    }

    public int getPosition() {
        return position;
    }

    public static void main(String[] args) {
        // 위치를 1로 초기화한 후 변경 불가.
        // 위치가 2인 차가 필요할 경우, 객체를 새로 만들어야만 한다.
        Car car1 = new Car(1);
        System.out.println("current position: " + car1.getPosition());
    }
}

 

2. 클래스를 확장할수 없도록 만든다. 

2.1  확장 가능

public class Human {
    private int age;
    private String name;

    public Human(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}
public class Student extends Human{
    private int grade;

    public Student(int age, String name, int grade) {
        super(age, name);
        this.grade = grade;
    }

    public int getGrade() {
        return grade;
    }


}

2.2 확장 불가 (final 키워드 사용)

final 키워드를 사용할 경우 1 related problem 알림이 뜹니다.
final 클래스는 상속할 수 없다고 알림

3. 모든 필드를 final로 선언

4. 모든 필드를 private으로 선언

- public final도 사용 가능하지만 다음 릴리즈 때 표현 변경이 불가능하므로 권장하지 않음 

5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 만든다.

 - 배열은 final로 선언해도, add 메서드를 통해 item을 추가할 수 있습니다.

   따라서 private으로 접근 제어자를 지정하여, 클라이언트의 접근을 막아야합니다. 

 

가변 객체 예시

 * equals, hashcode, toString은 intellij의 자동완성을 사용하여, 책과 다릅니다. 

 

예시(Complex 클래스)에서 주목할 점

- 함수형 프로그래밍으로 구현되었습니다.. (함수형프로그래밍의 정의는 코드 다음에 설명합니다.)

- 메서드명을 add 대신 plus를 사용하여, 객체의 값이 변경되지 않음을 강조하였습니다. 

 

* 참고)

BigDecimal과 BigInteger는 add를 사용하여, 사람들이 종종 함수의 뜻을 착각한다고 합니다. 

 

public final class Complex {
    private final double re;
    private final double im;

    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);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Complex complex = (Complex) o;
        return Double.compare(complex.re, re) == 0 && Double.compare(complex.im, im) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(re, im);
    }

    @Override
    public String toString() {
        return "Complex{" +
                "re=" + re +
                ", im=" + im +
                '}';
    }
}

 

함수형 프로그래밍 vs 명령형(=절차적) 프로그래밍

1. 함수형: 피연산자에 함수를 적용하여 결과 값을 리턴하지만, 피연산자는 그대로임

2. 명령형: 피연산자 자체의 상태가 바뀐다. 

 

불변객체의 장점

- 객체의 변경을 신경 쓸 필요가 없기 때문에, 디버깅이 편리합니다.

- 예외가 발생하더라도 그 상태값은 변경되지 않아 안전합니다.

- Thread-safe하므로, 객체 간 안심하고 공유할 수 있습니다. 

(예시. 아래 코드의 BigInteger 클래스)

signum만 반대로 만들고, mag 값은 공유 합니다. 


public class BigInteger extends Number implements Comparable<BigInteger> {
   	/**
     * 부호 
     */
    final int signum;

    /**
     * 크기(절댓값)
     */
    final int[] mag;
    
    // ... (생략) ....
    
    /**
     * 부호를 반대로한다
     */
    public BigInteger negate() {
        return new BigInteger(this.mag, -this.signum);
    }
    
    // ... (생략) ....
}

불변 객체를 잘 사용하는 방법 

- 스레드간 안전하므로, 한번 만든 인스턴스를 최대한 재활용하는 것이 좋습니다.

- clone 메서드를 제공하지 않아야합니다. 

  : 복사해도 원본과 똑같으므로 복사의 의미가 없습니다. 

 

어떻게 재할용할까

1. 자주 쓰이는 값듣을 상수로 사용할 수 있습니다.  (아래 예시 참고)

public static final Complex ZERO = new Complex(0,0);
public static final Complex ONE = new Complex(1,0);
public static final Complex I = new Complex(0,1);

2. 정적 팩토리를 사용

 - 메모리 사용량과 가비지 컬렉션 비용이 줄어듭니다.

 

 

불변 객체의 단점

값이 다르면 반드시 독립된 객체로 만들어야한다. 

예) 로또 넘버 생성기를 만든다고 가정하자. 이 때 로또 티켓 구매 이벤트가 발생할 때마다, 각 숫자를 새로 만든다고 가정한다면 어떨까. 메모리 낭비이다. 이 경우 정적 팩터리 메서드를 사용하여 성능을 향상 시킬 수 있다.