2011-07-05 2 views
3

나는 Effective Java라는 책을 읽었습니다.java : 불변 및 최종에 관한 질문

Mutability Minimize 항목에서 Joshua Bloch는 클래스를 불변으로 만드는 것에 대해 이야기합니다.

  1. 개체의 상태를 수정하는 방법을 제공하지 마십시오. 괜찮습니다.

  2. 클래스를 확장 할 수 없는지 확인하십시오. - 우리가 정말로해야합니까?

  3. 모든 필드를 마지막으로 만드십시오 - 정말로해야합니까? 예를 들어

의 내가 불변의 클래스가 있다고 가정하자

class A{ 
private int a; 

public A(int a){ 
    this.a =a ; 
} 

public int getA(){ 
    return a; 
} 
} 

이 어떻게에서 확장하는 클래스는, A의 불변성을 손상 할 수 있습니까? 이처럼

+0

권위에 의문을 던지는 것이 좋지만 여기서는 조슈아 블로치에 대해 이야기하고 있습니다. – duffymo

+3

불변 클래스 란 무엇을 의미합니까? 인스턴스가 변경 가능하지 않아야하거나이 유형으로 형변환 될 수있는 모든 객체 (예 : 하위 클래스에서 인스턴스화 된 객체)를 의미합니까? 두 번째 경우에는 설정할 수있는 새 속성 b가있는 하위 클래스 B를 만드는 것으로 충분합니다. 그러면 B의 인스턴스가 변경 될 수 있습니다. –

+0

Brian Goetz의 * Java Concurrency in Practice *를 권하고 싶습니다. 3 장 (또는 4?)을 읽고 나면, 불변 인 thread-safe 오브젝트의 필드가 왜 최종적인 것인지 분명해진다. 미리보기로서 아래의 내 대답을 참조하십시오. –

답변

6

:

public class B extends A { 
    private int b; 

    public B() { 
     super(0); 
    } 

    @Override 
    public int getA() { 
     return b++; 
    } 
} 

기술적으로, 당신은 수정하지 않는 필드는 A에서 상속하지만, 불변의 객체로, 같은 게터의 반복 호출은, 같은 번호를 생성 할 것으로 예상 물론있는 여기서는 그렇지 않습니다.

물론 규칙 1을 고수하면이 재정의를 만들 수 없습니다. 그러나 다른 사람들이 그 규칙에 복종한다는 것을 확신 할 수는 없습니다. 메서드 중 하나가 A을 매개 변수로 사용하고 getA()을 호출하면 다른 사람이 위와 같이 클래스 B을 만들고 해당 인스턴스를 메서드에 전달할 수 있습니다. 그런 다음 메서드는 알지 못해도 개체를 수정합니다.

+0

하지만 'a'는 비공개로 선언했습니다. B는 'a'에 대한 액세스 권한이 없습니다 – vinoth

+0

@cmv : * 테이블에 머리를 부딪치게하십시오 * - 당신은 완전히 맞습니다. 코드를 수정했습니다. –

+0

@Aasmund 대신 B에 대한 생성자를 정의하지 않았으므로 솔루션이 컴파일되지 않습니다. 대신 super()가 B에서 호출되며 A에 기본 생성자가 없으므로 컴파일되지 않습니다. –

4

Liskov 대체 원리에 따르면 하위 클래스는 수퍼 클래스가있는 곳이면 어디서나 사용할 수 있습니다. 고객의 관점에서 볼 때, 아이는 IS-A 부모입니다.

자식의 메소드를 덮어 쓰고 변경할 수있게하면 부모의 불변으로 간주되는 클라이언트와 계약을 위반하게됩니다.

+1

부모 클래스가 개인 속성 만 포함하는 경우 부모 클래스가 설정자를 제공하지 않으므로 하위 클래스는 값을 변경할 수 없습니다. – vinoth

+0

여전히 직렬화 및 직렬화 해제를 사용하여 수행 할 수 있습니다.그리고 당신은 궁극적 인 악을 사용하고 반사를 사용할 수 있습니다. – duffymo

+0

음 ... 직렬화 .. 그것에 대해 생각하지 마십시오. – vinoth

0

클래스가 확장되면 파생 클래스가 변경되지 않을 수 있습니다.

클래스가 변경 불가능한 경우 모든 필드는 생성 후에 수정되지 않습니다. 최종 키워드는이를 강제 할 것이고 미래의 유지 보수 담당자들에게 분명히 알릴 것입니다.

3

필드를 선언하는 경우 컴파일 타임 오류로 필드를 수정하거나 초기화하지 않는 것이 좋습니다.

멀티 스레드 코드에서 클래스 A의 인스턴스를 데이터 레이스 (즉, 동기화하지 않고, 즉 정적 필드와 같이 전역 적으로 사용 가능한 위치에 저장)와 공유하는 경우 일부 스레드는 getA() 값을 변경합니다!

Final 필드는 동기화 없이도 생성자가 완료된 후에 모든 스레드에서 해당 값을 볼 수 있도록 보장됩니다 (JVM specs).

이 두 클래스를 고려

final class A { 
    private final int x; 
    A(int x) { this.x = x; } 
    public getX() { return x; } 
} 

final class B { 
    private int x; 
    B(int x) { this.x = x; } 
    public getX() { return x; } 
} 

모두 AB는 초기화 경기장 x의 값을 수정할 수 없습니다 의미에서 불변 (의 반사 잊어 보자). 유일한 차이점은 x 필드에 A가 final으로 표시되어 있다는 것입니다.이 작은 차이가 큰 영향을 줄 것입니다. 이 추측은 사소한 보일 수 있지만,

class Main { 
    static A a = null; 
    static B b = null; 
    public static void main(String[] args) { 
    new Thread(new Runnable() { void run() { try { 
     while (a == null) Thread.sleep(50); 
     System.out.println(a.getX()); } catch (Throwable t) {} 
    }}).start() 
    new Thread(new Runnable() { void run() { try { 
     while (b == null) Thread.sleep(50); 
     System.out.println(b.getX()); } catch (Throwable t) {} 
    }}).start() 
    a = new A(1); b = new B(1); 
    } 
} 

두 스레드가보고있는 필드는 점에 유의 (주 스레드를 설정 한 후 null이 아닌 것을 볼 일이 가정 :

이제 다음 코드를 고려 그것은 JVM에 의해 보장되지 않습니다!). 이 경우

, 우리는 x 필드가 마지막이기 때문에 a 시계 스레드가 값 1를 인쇄 할 것이라는 점을 확신 할 수 있습니다 - 생성자가 완료되면 그래서, 그것은이 보장되는 객체를 참조하는 모든 스레드 x에 대한 올바른 값이 표시됩니다.

그러나 다른 스레드가 수행 할 작업에 대해서는 확신 할 수 없습니다. 사양은 0 또는 1 중 하나를 인쇄 할 수 있음을 보증합니다. 필드가 final이 아니기 때문에 어떤 종류의 동기화 (synchronized 또는 volatile)도 사용하지 않았 으면 스레드는 초기화되지 않은 필드를보고 0을 인쇄합니다. 다른 가능성은 실제로 필드가 초기화 된 것을보고 인쇄합니다. 1. 다른 값을 인쇄 할 수 없습니다.

또한

, 어떤 발생할 수있는 것은 당신이 읽고 bgetX()의 값을 인쇄 유지하는 경우, 그것은 0을 인쇄 잠시 후 1 인쇄를 시작할 수 있다는 것입니다! 이 경우 두 번째 스레드의 관점에서 볼 때 : 필드가 있어야하는 이유는 명확합니다. b은 변경자를 제공하지 않아도 변경할 수없는 경우에도 변경되었습니다.

당신이 필드 final하지 않고 두 번째 스레드가 x에 대한 올바른 가치를 볼 것이라는 점을 보장하려는 경우, 당신은 휘발성 B의 인스턴스 보유하고있는 필드 선언 할 수 :

class Main { 
    // ... 
    volatile static B b; 
    // ... 
} 

다른 가능성이다

final class B { 
    private int x; 
    private synchronized setX(int x) { this.x = x; } 
    public synchronized getX() { return x; } 
    B(int x) { setX(x); } 
} 

또는 홈페이지의 코드를 수정할 때 F로 동기화를 가산함으로써 : 어느 클래스 B를 변경하여 설정하고, 필드를 판독 할 때 때 동기화 b이 읽혀지고 쓰여질 때 두 작업이 동일한 객체에서 동기화되어야합니다!

가장 시원하고 안정적이며 성능이 우수한 솔루션은 필드를 x 결승으로 만드는 것입니다. 최종 참고로


, 최종 모든 필드를 가지고 불변, 스레드 안전 클래스 절대적으로 필요하지 않습니다.그러나 이러한 클래스 (스레드 안전, 변경 불가능, 최종 필드가 포함되지 않음)는주의 깊게 설계해야하며 전문가를 위해 남겨 두어야합니다.

예를 들어 java.lang.String 클래스가 있습니다. 그것은 마지막이 아닌 private int hash; 필드를 가지고 있으며, 해시 코드()에 대한 캐시로 사용됩니다

private int hash; 
public int hashCode() { 
    int h = hash; 
    int len = count; 
    if (h == 0 && len > 0) { 
    int off = offset; 
    char val[] = value; 
    for (int i = 0; i < len; i++) 
     h = 31*h + val[off++]; 
    hash = h; 
    } 
    return h; 
} 

당신이 볼 수 있듯이, 해시 코드() 메소드는 먼저 (비 최종)를 읽고 필드 hash . 초기화되지 않은 경우 (즉, 0 인 경우) 값을 다시 계산하고 설정합니다. 해시 코드를 계산하고 필드에 쓰여진 스레드의 경우이 값은 영원히 유지됩니다.

그러나 스레드가 다른 것으로 설정 한 후에도 다른 스레드는 여전히 해당 필드에 대해 0을 볼 수 있습니다. 이 경우 다른 스레드는 해시를 다시 계산하고 과 동일한 값을 얻은 다음 설정합니다.

여기서 클래스의 불변성과 스레드 안전성을 정당화한다는 것은 모든 스레드가 비 최종 필드에 캐시 된 경우에도 hashCode()에 대해 정확히 동일한 값을 얻게된다는 것입니다. 이는 다시 계산되고 정확한 값을 얻을 수 있습니다.

이 모든 추론은 매우 미묘하기 때문에 은 모든 필드가 스레드로부터 안전하지 않은 클래스에서 최종으로 표시되는 것이 좋습니다.

0

the exact section of the JVM spec을 가리키는이 대답을 추가하면 변경 불가능한 클래스에서 스레드로부터 안전 할 수 있도록 멤버 변수가 왜 최종적 일 필요가 있는지 설명합니다. 여기에 내가 생각하는 스펙에 사용 된 예는 매우 분명하다입니다 : 다시

class FinalFieldExample { 
    final int x; 
    int y; 
    static FinalFieldExample f; 

    public FinalFieldExample() { 
     x = 3; 
     y = 4; 
    } 

    static void writer() { 
     f = new FinalFieldExample(); 
    } 

    static void reader() { 
     if (f != null) { 
      int i = f.x; // guaranteed to see 3 
      int j = f.y; // could see 0 
     } 
    } 
} 

, 사양에서 :

클래스 FinalFieldExample가 최종 INT 필드 x와 비 최종의 INT 필드가 와이. 한 스레드는 메소드 작성기를 실행하고 다른 스레드는 메소드 판독기를 실행할 수 있습니다.

개체 생성자가 완료된 후에 writer 메서드가 f를 작성하기 때문에 reader 메서드는 f.x에 대해 올바르게 초기화 된 값을 볼 수 있습니다. 값은 3입니다. 그러나 f.y는 final이 아닙니다. 따라서 reader 메소드는 값 4를 볼 수는 없습니다.