[java] 불변이란 무엇입니까?

이것은 가장 까다로운 질문 일 수 있지만 Java 초보자에게는 혼란 스럽다고 생각합니다.

  1. 누군가 불변의 의미를 명확히 할 수 있습니까? ?
  2. String 불변인가?
  3. 불변 개체의 장점 / 단점은 무엇입니까?
  4. StringBuilderString과 그 반대의 변경 가능한 객체를 선호 해야 합니까?

좋은 예 (자바)는 정말 감사하겠습니다.



답변

불변은 객체의 생성자가 실행을 완료하면 해당 인스턴스를 변경할 수 없음을 의미합니다.

이는 다른 사람이 내용을 변경할 염려없이 객체에 대한 참조를 전달할 수 있다는 점에서 유용합니다. 특히 동시성을 처리 할 때 절대 바뀌지 않는 객체에 잠금 문제가 없습니다.

예 :

class Foo
{
     private final String myvar;

     public Foo(final String initialValue)
     {
         this.myvar = initialValue;
     }

     public String getValue()
     {
         return this.myvar;
     }
}

Foo호출자가 getValue()문자열의 텍스트를 변경할 수 있다고 걱정할 필요가 없습니다 .

와 클래스가 비슷 Foo하지만 멤버 가 StringBuilder아닌 String멤버 인 경우 호출자가 인스턴스 getValue()StringBuilder속성 을 변경할 수 있음을 알 수 있습니다 Foo.

Eric Lippert는 이에 대해 블로그 기사 를 작성했습니다. 기본적으로 인터페이스는 변경할 수 없지만 장면 뒤에 실제 변경 가능 개인 상태 (따라서 스레드간에 안전하게 공유 할 수 없음) 뒤에 객체를 가질 수 있습니다.


답변

불변 개체는 내부 필드 (또는 외부 동작에 영향을 미치는 모든 내부 필드)를 변경할 수없는 개체입니다.

변경 불가능한 문자열에는 많은 장점이 있습니다.

성능 : 다음 작업을 수행하십시오.

String substring = fullstring.substring(x,y);

substring () 메소드의 기본 C는 다음과 같습니다.

// Assume string is stored like this:
struct String { char* characters; unsigned int length; };

// Passing pointers because Java is pass-by-reference
struct String* substring(struct String* in, unsigned int begin, unsigned int end)
{
    struct String* out = malloc(sizeof(struct String));
    out->characters = in->characters + begin;
    out->length = end - begin;
    return out;
}

유의하십시오 문자 것도 복사 할 필요가! String 객체가 변경 가능하면 (문자가 나중에 변경 될 수 있음) 모든 문자를 복사해야합니다. 그렇지 않으면 하위 문자열의 문자 변경 사항이 나중에 다른 문자열에 반영됩니다.

동시성 : 불변 개체의 내부 구조가 유효한 경우 항상 유효합니다. 다른 스레드가 해당 개체 내에서 유효하지 않은 상태를 만들 가능성은 없습니다. 따라서 불변의 객체는 Thread Safe 입니다.

가비지 수집 : 가비지 수집기가 불변 개체에 대한 논리적 결정을 내리는 것이 훨씬 쉽습니다.

그러나 불변성에 대한 단점도 있습니다.

성능 : 잠깐, 성능이 불변성의 단점이라고 생각했습니다! 글쎄, 때로는 그렇지는 않지만 항상 그런 것은 아닙니다. 다음 코드를 사용하십시오.

foo = foo.substring(0,4) + "a" + foo.substring(5);  // foo is a String
bar.replace(4,5,"a"); // bar is a StringBuilder

두 줄 모두 네 번째 문자를 문자 “a”로 바꿉니다. 두 번째 코드는 더 읽기 쉽고 빠를뿐입니다. foo에 대한 기본 코드를 어떻게 수행해야하는지 살펴보십시오. 부분 문자열은 쉽지만, 이제 5 번째 공간에 문자가 있고 foo를 참조하는 다른 문자가 있기 때문에 변경할 수는 없습니다. 전체 문자열을 복사해야합니다 (물론이 기능 중 일부는 실제 기본 C의 함수로 추상화되었지만 여기서 요점은 한 곳에서 모두 실행되는 코드를 표시하는 것입니다).

struct String* concatenate(struct String* first, struct String* second)
{
    struct String* new = malloc(sizeof(struct String));
    new->length = first->length + second->length;

    new->characters = malloc(new->length);

    int i;

    for(i = 0; i < first->length; i++)
        new->characters[i] = first->characters[i];

    for(; i - first->length < second->length; i++)
        new->characters[i] = second->characters[i - first->length];

    return new;
}

// The code that executes
struct String* astring;
char a = 'a';
astring->characters = &a;
astring->length = 1;
foo = concatenate(concatenate(slice(foo,0,4),astring),slice(foo,5,foo->length));

연결은 두 번 호출 되므로 전체 문자열을 반복해야합니다. bar작업을 위해 이것을 C 코드와 비교하십시오 .

bar->characters[4] = 'a';

변경 가능한 문자열 작업이 훨씬 빠릅니다.

결론 : 대부분의 경우 변경 불가능한 문자열을 원합니다. 그러나 문자열에 많은 추가 및 삽입 작업이 필요한 경우 속도에 대한 변경 가능성이 필요합니다. 동시성 안전 및 가비지 콜렉션 이점을 원하는 경우 변경 가능한 오브젝트를 메소드의 로컬에 유지하는 것이 중요합니다.

// This will have awful performance if you don't use mutable strings
String join(String[] strings, String separator)
{
    StringBuilder mutable;
    boolean first = true;

    for(int i = 0; i < strings.length; i++)
    {
        if(!first) first = false;
        else mutable.append(separator);

        mutable.append(strings[i]);
    }

    return mutable.toString();
}

때문에 mutable객체가 로컬 참조이며, 당신은 동시성의 안전에 대해 걱정할 필요가 없습니다 (하나의 스레드는 이제까지 그것을 접촉). 그리고 다른 곳에서는 참조되지 않기 때문에 스택에만 할당되므로 함수 호출이 완료되는 즉시 할당이 해제됩니다 (가비지 수집에 대해 걱정할 필요가 없습니다). 또한 가변성과 불변성의 성능 이점을 모두 얻을 수 있습니다.


답변

위에서 제안한 Wikipedia 정의를 사용하면 실제로 String은 변경할 수 없습니다.

String의 상태는 시공 후 변경됩니다. hashcode () 메소드를 살펴보십시오. String은 해시 코드 값을 로컬 필드에 캐시하지만 hashcode ()를 처음 호출 할 때까지 값을 계산하지 않습니다. 해시 코드에 대한이 게으른 평가는 String을 상태가 변하는 불변의 객체로 흥미로운 위치에 배치하지만 리플렉션을 사용하지 않고는 변경 될 수 없습니다.

따라서 불변의 정의는 변경 될 수없는 개체 여야합니다.

불변 객체가 생성 된 후 상태가 변경되었지만 아무도 (반사없이) 그것을 볼 수 없다면 객체는 여전히 불변입니까?


답변

불변 개체는 프로그래밍 방식으로 변경할 수없는 개체입니다. 다중 스레드 환경 또는 둘 이상의 프로세스가 객체의 값을 변경 (돌연변이) 할 수있는 기타 환경에 특히 유용합니다.

그러나 명확히하기 위해 StringBuilder는 실제로 변경할 수없는 객체가 아닌 변경 가능한 객체입니다. 일반 Java 문자열은 변경할 수 없습니다. 일단 생성되면 객체를 변경하지 않고 기본 문자열을 변경할 수 없습니다.

예를 들어 String 값과 String 색상을 가진 ColoredString이라는 클래스가 있다고 가정 해 보겠습니다.

public class ColoredString {

    private String color;
    private String string;

    public ColoredString(String color, String string) {
        this.color  = color;
        this.string = string;
    }

    public String getColor()  { return this.color;  }
    public String getString() { return this.string; }

    public void setColor(String newColor) {
        this.color = newColor;
    }

}

이 예제에서, ColoredString은 새로운 ColoredString 클래스를 만들지 않고 주요 속성 중 하나를 변경 (변경) 할 수 있기 때문에 변경 가능하다고합니다. 이것이 좋지 않은 이유는 예를 들어 여러 스레드가있는 GUI 응용 프로그램이 있고 ColoredStrings를 사용하여 데이터를 창에 인쇄한다고 가정 해 봅시다. 다음과 같이 생성 된 ColoredString 인스턴스가있는 경우

new ColoredString("Blue", "This is a blue string!");

그러면 문자열이 항상 “파란색”이됩니다. 그러나 다른 스레드가이 인스턴스를 잡고 호출 한 경우

blueString.setColor("Red");

갑자기 “파란색”문자열을 원할 때 갑자기 “빨간색”문자열이 나타납니다. 이 때문에 불변의 객체는 거의 항상 객체의 인스턴스를 전달할 때 선호됩니다. 변경 가능한 객체가 실제로 필요한 경우에는 일반적으로 특정 제어 필드에서 사본을 전달하여 오브 제트를 보호해야합니다.

요약하자면, Java에서 java.lang.String은 변경할 수없는 객체 (생성 된 후에 변경할 수 없음 )이고 java.lang.StringBuilder는 새로운 인스턴스를 만들지 않고 변경할 수 있기 때문에 변경할 수있는 객체입니다.


답변

  1. 큰 응용 프로그램에서는 문자열 리터럴이 큰 메모리 비트를 차지하는 것이 일반적입니다. 따라서 메모리를 효율적으로 처리하기 위해 JVM은 “문자열 상수 풀”이라는 영역을 할당합니다 ( 메모리에서 참조되지 않은 문자열조차도 char [], 길이에 대한 int 및 해시 코드에 대한 int를 전달합니다. 대조적으로, 최대 8 개의 즉시 바이트가 필요합니다 )
  2. complier가 String 리터럴을 발견하면 풀이 동일한 리터럴이 있는지 확인합니다. 그리고 하나가 발견되면 새로운 리터럴에 대한 참조는 기존 문자열로 보내지고 새로운 ‘문자열 리터럴 객체’가 만들어지지 않습니다 (기존 문자열은 단순히 추가 참조를 얻습니다).
  3. 따라서 : 문자열 가변성은 메모리를 절약합니다 …
  4. 그러나 변수 중 하나가 값을 변경하면 실제로는 메모리의 값이 아니라 변경된 참조만이므로 (아래 참조하는 다른 변수에는 영향을 미치지 않습니다) ….

문자열 s1 = “오래된 문자열”;

//s1 variable, refers to string in memory
        reference                 |     MEMORY       |
        variables                 |                  |

           [s1]   --------------->|   "Old String"   |

문자열 s2 = s1;

//s2 refers to same string as s1
                                  |                  |
           [s1]   --------------->|   "Old String"   |
           [s2]   ------------------------^

s1 = “새 문자열”;

//s1 deletes reference to old string and points to the newly created one
           [s1]   -----|--------->|   "New String"   |
                       |          |                  |
                       |~~~~~~~~~X|   "Old String"   |
           [s2]   ------------------------^

메모리의 원래 문자열은 변경되지 않았지만 참조 변수는 새로운 문자열을 참조하도록 변경되었습니다. s2가 없다면 “Old String”은 여전히 ​​메모리에 있지만 액세스 할 수는 없습니다 …


답변

“불변”은 값을 변경할 수 없음을 의미합니다. String 클래스의 인스턴스가 있으면 값을 수정하는 것처럼 보이는 모든 메서드가 실제로 다른 String을 만듭니다.

String foo = "Hello";
foo.substring(3);
<-- foo here still has the same value "Hello"

변경 사항을 유지하려면 다음과 같이해야합니다. foo = foo.sustring (3);

컬렉션으로 작업 할 때 변경 불가능한 변경 가능과 변경 불가능할 수 있습니다. 변경 가능한 객체를지도의 키로 사용하고 값을 변경하면 어떻게 될지 생각합니다 (팁 : 생각 equalshashCode).


답변

java.time

약간 늦었지만 변경 불가능한 객체가 무엇인지 이해하기 위해 새로운 Java 8 Date and Time API ( java.time ) 의 다음 예제를 고려하십시오 . 아시다시피 Java 8의 모든 날짜 객체는 변경할 수 없으므로 다음 예제에서

LocalDate date = LocalDate.of(2014, 3, 18);
date.plusYears(2);
System.out.println(date);

산출:

2014-03-18

이것은 plusYears(2)새로운 객체를 반환 하기 때문에 초기 날짜와 같은 연도를 인쇄 하므로 불변의 객체이기 때문에 이전 날짜는 변경되지 않습니다. 일단 생성하면 더 이상 수정할 수 없으며 날짜 변수가 여전히 가리 킵니다.

따라서이 코드 예제는에 대한 호출에서 인스턴스화되어 반환 된 새 객체를 캡처하고 사용해야합니다 plusYears.

LocalDate date = LocalDate.of(2014, 3, 18);
LocalDate dateAfterTwoYears = date.plusYears(2);

date.toString ()… 2014-03-18

dateAfterTwoYears.toString ()… 2016-03-18