development

Java 문자열이 변경 불가능합니까?

big-blog 2020. 2. 23. 11:55
반응형

Java 문자열이 변경 불가능합니까?


우리는 이것이 String자바에서 불변 이라는 것을 알고 있지만 다음 코드를 확인하십시오.

String s1 = "Hello World";  
String s2 = "Hello World";  
String s3 = s1.substring(6);  
System.out.println(s1); // Hello World  
System.out.println(s2); // Hello World  
System.out.println(s3); // World  

Field field = String.class.getDeclaredField("value");  
field.setAccessible(true);  
char[] value = (char[])field.get(s1);  
value[6] = 'J';  
value[7] = 'a';  
value[8] = 'v';  
value[9] = 'a';  
value[10] = '!';  

System.out.println(s1); // Hello Java!  
System.out.println(s2); // Hello Java!  
System.out.println(s3); // World  

이 프로그램은 왜 이렇게 작동합니까? 그리고 왜 가치가 s1있고 s2변화 되었는가 s3?


String 변경할 수 없지만 공개 API를 사용하여 변경할 수 없음을 의미합니다.

여기서하는 일은 리플렉션을 사용하여 일반 API를 우회하는 것입니다. 같은 방법으로 열거 형의 값을 변경하고 정수 자동 상자 등에 사용되는 조회 테이블을 변경할 수 있습니다.

이제 이유 s1s2변경 값은 둘 다 동일한 내부 문자열을 참조하기 때문입니다. 컴파일러는 이것을 수행합니다 (다른 답변에서 언급했듯이).

그 이유는 s3않습니다 하지 나는 그것이 공유 할 생각으로, 실제로 나에게 의외 조금했다 value배열 ( 은 자바의 이전 버전에서했던 자바 7u6 전에). 그러나의 소스 코드 를 보면 부분 문자열 Stringvalue문자 배열이 실제로 복사되어 Arrays.copyOfRange(..)있음을 알 수 있습니다. 이것이 변경되지 않는 이유입니다.

SecurityManager이러한 일을하는 악성 코드를 피하기 위해를 설치할 수 있습니다 . 그러나 일부 라이브러리는 이러한 종류의 리플렉션 트릭 (일반적으로 ORM 도구, AOP 라이브러리 등)의 사용에 의존합니다.

*) 나는 처음에 Strings가 실제로 불변이 아니라 단지 "불변 불변"이라고 썼다. 현재 구현 String에서 value배열이 실제로 표시되어 있는 경우이 오류가 발생할 수 있습니다 private final. 그래도 Java에서 배열을 변경할 수없는 것으로 선언 할 수있는 방법이 없기 때문에 적절한 액세스 수정자를 사용하더라도 클래스 외부에 배열을 노출시키지 않도록주의해야합니다.


이 주제가 압도적으로 인기가있는 것처럼, 여기에 더 읽을 거리가 있습니다 : JavaZone 2009 의 Heinz Kabutz의 Reflection Madness 강연 은 OP의 많은 문제와 다른 반성 ... 음 ... 광기.

왜 이것이 유용한 지에 대해 다룹니다. 왜 대부분의 경우 피해야합니다. :-)


Java에서 두 문자열 기본 변수가 동일한 리터럴로 초기화되면 두 변수에 동일한 참조를 지정합니다.

String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true

초기화

이것이 비교가 true를 반환하는 이유입니다. 세 번째 문자열은 substring()동일한 문자열을 가리키는 대신 새 문자열을 만드는 데 사용 됩니다.

하위 문자열

리플렉션을 사용하여 문자열에 액세스하면 실제 포인터가 나타납니다.

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);

따라서 이것으로 변경하면 포인터를 보유하는 문자열이 변경되지만 s3새 문자열로 작성 substring()되므로 변경되지 않습니다.

변화


String의 불변성을 피하기 위해 리플렉션을 사용하고 있습니다. 이것은 "공격"의 한 형태입니다.

이와 같이 만들 수있는 예제가 많이 있지만 (예 : 객체를 인스턴스화 할 수도Void 있음) String이 "불변"이 아니라는 의미는 아닙니다.

이 유형의 코드가 유리한 순간에 (GC 이전) 메모리에서 암호를 지우는 것과 같이 "유용한 코딩"이 될 수있는 사용 사례가 있습니다 .

보안 관리자에 따라 코드를 실행하지 못할 수 있습니다.


리플렉션을 사용하여 문자열 객체의 "구현 정보"에 액세스하고 있습니다. 불변성은 객체의 공용 인터페이스 기능입니다.


가시성 수정 자 및 최종 (즉, 불변성)은 Java의 악성 코드에 대한 측정이 아닙니다. 그것들은 단지 실수로부터 보호하고 코드를 유지 관리하기 쉽게 만드는 도구 일뿐입니다 (시스템의 큰 판매 포인트 중 하나). 그렇기 때문에 String리플렉션을 통해 지원되는 char 배열과 같은 내부 구현 세부 정보에 액세스 할 수 있습니다 .

당신이 보는 두 번째 효과는 모든 String것이 변하는 동안 변화하는 것 s1입니다. Java 문자열 리터럴의 특정 속성은 자동으로 인터 닝, 즉 캐시됩니다. 동일한 값을 가진 두 개의 문자열 리터럴은 실제로 동일한 객체입니다. 문자열을 만들면 new자동으로 구속되지 않으며이 효과가 나타나지 않습니다.

#substring최근까지 (Java 7u6) 비슷한 방식으로 작동하여 질문의 원래 버전에서 동작을 설명했습니다. 새로운 백업 문자 배열을 만들지 않고 원래 문자열의 배열을 재사용했습니다. 방금 오프셋과 길이를 사용하여 해당 배열의 일부만 나타내는 새로운 String 객체를 만들었습니다. 이것을 피하지 않으면 일반적으로 문자열을 변경할 수 없습니다. 이 속성은 #substring또한 더 짧은 하위 문자열에서 여전히 생성 된 경우 전체 원본 문자열을 가비지 수집 할 수 없음을 의미했습니다.

현재 Java 및 현재 버전의 질문에서 이상한 동작은 없습니다 #substring.


문자열 불변성은 인터페이스 관점에서입니다. 리플렉션을 사용하여 인터페이스를 무시하고 String 인스턴스의 내부를 직접 수정합니다.

s1그리고 s2그들은 모두 동일한 "인턴"문자열 인스턴스에 할당되기 때문에 모두 변경됩니다. 이 기사 에서 문자열 평등과 인턴에 대한 부분에 대해 좀 더 자세히 알 수 있습니다 . 샘플 코드에서 다음을 s1 == s2반환 한다는 사실에 놀랄 수도 있습니다 true!


어떤 버전의 Java를 사용하고 있습니까? Java 1.7.0_06부터 Oracle은 String, 특히 하위 문자열의 내부 표현을 변경했습니다.

오라클의 인용은 Java의 내부 문자열 표현을 조정합니다 .

새로운 패러다임에서는 문자열 오프셋 및 개수 필드가 제거되었으므로 하위 문자열은 더 이상 기본 char [] 값을 공유하지 않습니다.

이 변경으로 인해 반사 (???)없이 발생할 수 있습니다.


여기에는 실제로 두 가지 질문이 있습니다.

  1. 문자열은 정말 불변인가?
  2. s3가 왜 변경되지 않습니까?

포인트 1 : ROM을 제외하고는 컴퓨터에 불변 메모리가 없습니다. 오늘날 ROM조차도 때때로 쓰기 가능합니다. 메모리 주소에 쓸 수있는 코드는 항상 어딘가에 있습니다 (커널 또는 네이티브 코드가 관리되는 환경을 방해하는지 여부). 따라서 "현실"에서 절대로 변하지 않는 것은 아닙니다 .

포인트 2 : 이것은 부분 문자열이 아마도 새로운 문자열 인스턴스를 할당하고 있기 때문에 배열을 복사하기 때문입니다. 복사하지 않는 방식으로 하위 문자열을 구현할 수 있지만 그렇다고해서 그런 것은 아닙니다. 트레이드 오프가 관련되어 있습니다.

예를 들어, reallyLargeString.substring(reallyLargeString.length - 2)많은 양의 메모리가 살아 있거나 몇 바이트 만 유지되도록 참조를 보유해야 합니까?

그것은 부분 문자열이 어떻게 구현되는지에 달려 있습니다. 딥 카피는 적은 메모리를 유지하지만 약간 느리게 실행됩니다. 얕은 복사본은 더 많은 메모리를 유지하지만 더 빠릅니다. 딥 카피를 사용하면 2 개의 개별 힙 할당이 아닌 문자열 개체와 해당 버퍼를 한 블록에 할당 할 수 있으므로 힙 조각화를 줄일 수 있습니다.

어쨌든 JVM이 하위 문자열 호출에 깊은 사본을 사용하기로 선택한 것 같습니다.


@haraldK의 답변에 추가하려면 앱에 심각한 영향을 줄 수있는 보안 해킹입니다.

첫 번째는 문자열 풀에 저장된 상수 문자열을 수정하는 것입니다. string String s = "Hello World";이로 선언되면 잠재적 재사용을 위해 특수 객체 풀에 배치됩니다. 문제는 컴파일러가 컴파일 타임에 수정 된 버전에 대한 참조를 배치하고 사용자가 런타임에이 풀에 저장된 문자열을 수정하면 코드의 모든 참조가 수정 된 버전을 가리 킵니다. 이로 인해 다음과 같은 버그가 발생합니다.

System.out.println("Hello World"); 

인쇄합니다 :

Hello Java!

그런 위험한 문자열에 대해 무거운 계산을 구현할 때 경험 한 또 다른 문제가있었습니다. 계산 중에 1000000 번 중 1 번과 같은 버그가 발생하여 결과가 결정적이지 않았습니다. JIT를 꺼서 문제를 찾을 수있었습니다. JIT를 끈 상태에서 항상 같은 결과를 얻었습니다. 내 생각 엔 JIT 최적화 계약을 위반 한이 문자열 보안 해킹이 원인이었습니다.


풀링 개념에 따라 동일한 값을 포함하는 모든 문자열 변수는 동일한 메모리 주소를 가리 킵니다. 따라서 같은 값의 "Hello World"를 포함하는 s1과 s2는 동일한 메모리 위치 (예 : M1)를 가리 킵니다.

반면에 s3에는 "World"가 포함되어 있으므로 다른 메모리 할당 (예 : M2)을 가리 킵니다.

이제 일어나고있는 일은 S1의 값이 변경되고 있다는 것입니다 (char [] 값을 사용하여). 따라서 s1과 s2가 가리키는 메모리 위치 M1의 값이 변경되었습니다.

결과적으로, 메모리 위치 M1이 수정되어 s1 및 s2의 값이 변경됩니다.

그러나 위치 M2의 값은 변경되지 않은 상태로 유지되므로 s3에는 동일한 원래 값이 포함됩니다.


s3이 실제로 변경되지 않는 이유는 Java에서 하위 문자열을 수행 할 때 하위 문자열에 대한 값 문자 배열이 내부적으로 복사되기 때문입니다 (Arrays.copyOfRange () 사용).

s1과 s2는 동일합니다. Java에서는 둘 다 동일한 내부 문자열을 참조하기 때문입니다. Java에서 의도적으로 설계된 것입니다.


문자열은 변경할 수 없지만 리플렉션을 통해 String 클래스를 변경할 수 있습니다. String 클래스를 실시간으로 변경 가능하도록 재정의했습니다. 원하는 경우 메소드를 공용 또는 개인용 또는 정적으로 재정의 할 수 있습니다.


[면책 조항 이것은 "집에서 아이들에게하지 마십시오"라는 대답이 더 필요하다고 생각하기 때문에 고의적으로 의견이있는 답변 스타일입니다.]

죄는 field.setAccessible(true);개인 분야에 대한 접근을 허용함으로써 공공 API를 위반한다고 말하는 입니다. 보안 관리자를 구성하여 잠글 수있는 거대한 보안 허점입니다.

문제의 현상은 리플렉션을 통해 액세스 수정자를 위반하기 위해 위험한 코드 라인을 사용하지 않을 때 볼 수없는 구현 세부 사항입니다. 분명히 두 개의 (보통) 불변 문자열은 동일한 문자 배열을 공유 할 수 있습니다. 하위 문자열이 동일한 배열을 공유하는지 여부는 하위 배열이 공유 배열을 공유 할 수 있는지 여부에 따라 다릅니다. 일반적으로 이것들은 보이지 않는 구현 세부 사항이며 해당 코드 줄로 헤드를 통해 액세스 수정자를 촬영하지 않는 한 알 필요가 없습니다.

리플렉션을 사용하여 액세스 수정자를 위반하지 않고는 경험할 수없는 이러한 세부 사항에 의존하는 것은 좋은 생각이 아닙니다. 해당 클래스의 소유자는 일반적인 공개 API 만 지원하며 향후 구현을 자유롭게 변경할 수 있습니다.

코드 라인은 총이 당신에게 그런 위험한 일을 강요하도록 머리를 잡을 때 정말 매우 유용하다고 말했습니다. 백도어를 사용하면 일반적으로 코드 냄새가 나므로 죄를 짓지 않아도되는 더 나은 라이브러리 코드로 업그레이드해야합니다. 그 위험한 코드 라인의 또 다른 일반적인 용도는 "부두 프레임 워크"(orm, injection container, ...)를 작성하는 것입니다. 많은 사람들이 그러한 프레임 워크에 대해 종교적 관계를 갖기 때문에 (그들 모두와 반대), 나는 대다수의 프로그래머가 거기에 갈 필요가 없다는 것을 말함으로써 화염 전쟁을 피하는 것을 피할 것입니다.


문자열은 JVM 힙 메모리의 영구 영역에 작성됩니다. 예, 변경이 불가능하며 생성 후에는 변경할 수 없습니다. JVM에는 세 가지 유형의 힙 메모리가 있으므로 다음과 같이하십시오. 1. 젊은 세대 2. 이전 세대 3. 영구 생성.

객체가 생성되면 객체는 젊은 세대 힙 영역과 문자열 풀링을 위해 예약 된 PermGen 영역으로 이동합니다.

가비지 콜렉션이 Java에서 작동하는 방식에서 자세한 정보를 얻을 수 있습니다 .


String은 본질적으로 불변입니다. String 객체를 수정하는 방법이 없기 때문입니다. 그것이 그들이 StringBuilderStringBuffer 클래스를 소개 한 이유입니다

참고 URL : https://stackoverflow.com/questions/20945049/is-a-java-string-really-immutable



반응형