Java 문자열 소스 코드 분석
불변 객체란 무엇인가요?
우리 모두 알고 있듯이 Java에서 String 클래스는 변경할 수 없습니다. 그렇다면 불변 객체란 정확히 무엇입니까? 다음과 같이 생각할 수 있습니다. 객체가 생성된 후 상태를 변경할 수 없으면 객체는 불변입니다. 상태는 변경될 수 없습니다. 즉, 참조 유형의 변수는 다른 객체를 가리킬 수 없으며 참조 유형이 가리키는 객체의 상태는 변경할 수 없습니다. 변경됩니다.
객체와 객체 참조 구별
Java 초보자의 경우 String이 불변 객체인지에 대해 항상 의구심을 품습니다. 다음 코드를 살펴보세요.
String s = "ABCabc"; System.out.println("s = " + s); s = "123456"; System.out.println("s = " + s);
인쇄된 결과는 다음과 같습니다.
s = ABCabc s = 123456
First String Object s를 생성한 다음 s의 값을 "ABCabc"로 설정하고 s의 값을 "123456"으로 설정합니다. 인쇄된 결과를 보면 s 값이 실제로 변경되었음을 알 수 있습니다. 그런데 왜 여전히 String 객체는 불변이라고 말하는 걸까요? 사실 여기에는 오해가 있습니다. s는 객체 자체가 아니라 String 객체에 대한 참조일 뿐입니다. 객체는 메모리 내의 메모리 영역입니다. 멤버 변수가 많을수록 이 메모리 영역이 차지하는 공간이 커집니다. 참조는 가리키는 개체의 주소를 저장하는 4바이트 데이터입니다. 이 주소를 통해 개체에 액세스할 수 있습니다.
즉, s는 단지 특정 객체를 가리키는 참조일 뿐입니다. 이 코드가 실행된 후 s="123456"; 이 되면 새로운 객체 "123456"이 생성되고 해당 참조는 다시 생성됩니다. -하트 개체를 가리키며 원래 개체 "ABCabc"는 여전히 메모리에 존재하며 변경되지 않았습니다. 메모리 구조는 아래와 같습니다.
Java와 C++의 한 가지 차이점은 Java에서는 객체 자체를 직접 동작시킬 수 없다는 점입니다. 객체 그것들은 모두 참조에 의해 지시되며 이 참조는 멤버 변수의 값 획득, 객체의 멤버 변수 변경, 객체의 메소드 호출 등을 포함하여 객체 자체에 액세스하는 데 사용되어야 합니다. C++에는 참조, 개체, 포인터의 세 가지가 있으며 모두 개체에 액세스할 수 있습니다. 실제로 Java의 참조는 개념적으로 C++의 포인터와 유사합니다. 둘 다 메모리에 저장된 개체의 주소 값입니다. 그러나 Java에서는 참조의 유연성이 일부 손실되었습니다. 덧셈과 뺄셈은 C++의 포인터처럼 수행됩니다.
String 객체는 왜 불변인가요?
String의 불변성을 이해하려면 먼저 String 클래스의 멤버 변수를 살펴보세요. JDK1.6에서 String의 멤버 변수에는 다음이 포함됩니다.
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** The offset is the first index of the storage that is used. */ private final int offset; /** The count is the number of characters in the String. */ private final int count; /** Cache the hash code for the string */ private int hash; // Default to 0
JDK1.7에서는 String 클래스가 일부 변경되었으며 주로 동작이 변경되었습니다. 실행될 때 하위 문자열 메서드의 내용은 이 문서의 주제와 관련이 없습니다. JDK1.7에는 String 클래스의 주요 멤버 변수가 두 개만 있습니다.
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0
위 코드에서 볼 수 있듯이 실제로 Java의 String 클래스는 문자 배열을 캡슐화한 것입니다. JDK6에서 value는 String으로 캡슐화된 배열이고, offset은 값 배열에서 String의 시작 위치이며, count는 String이 차지하는 문자 수입니다. JDK7에는 값 변수가 하나만 있습니다. 즉, value의 모든 문자는 String 객체에 속합니다. 이 변경 사항은 이 문서의 논의에 영향을 미치지 않습니다. 또한 String 객체의 해시 값을 캐시하는 해시 멤버 변수도 있습니다. 이 멤버 변수는 이 기사의 논의와도 관련이 없습니다. Java에서는 배열도 객체입니다(이전 기사인 Java 배열의 특성을 참조하세요). 따라서 값은 실제 배열 객체를 가리키는 참조일 뿐입니다. 실제로 String s = "ABCabc"; 코드를 실행한 후 실제 메모리 레이아웃은 다음과 같아야 합니다:
value, offset 및 count의 세 가지 변수는 비공개이며 이러한 값을 수정하기 위한 setValue, setOffset 및 setCount와 같은 공개 메소드가 제공되지 않으므로 String 클래스 외부에서는 String을 수정할 수 없습니다. 즉, 일단 초기화되면 수정할 수 없으며 이 세 멤버는 String 클래스 외부에서 액세스할 수 없습니다. 또한 세 가지 변수 value, offset 및 count는 모두 최종이므로 String 클래스 내에서 이 세 가지 값이 초기화되면 변경할 수 없습니다. 따라서 String 개체는 변경할 수 없는 것으로 간주될 수 있습니다.
그러면 String에는 분명히 몇 가지 메소드가 있고, 이를 호출하면 변경된 값을 얻을 수 있습니다. 이러한 메서드에는 부분 문자열, 바꾸기, 바꾸기All, toLowerCase 등이 포함됩니다. 예를 들어 다음 코드는
String a = "ABCabc"; System.out.println("a = " + a); a = a.replace('A', 'a'); System.out.println("a = " + a);
인쇄된 결과는 다음과 같습니다.
a = ABCabc a = aBCabc
那么a的值看似改变了,其实也是同样的误区。再次说明, a只是一个引用, 不是真正的字符串对象,在调用a.replace('A', 'a')时, 方法内部创建了一个新的String对象,并把这个心的对象重新赋给了引用a。String中replace方法的源码可以说明问题:
读者可以自己查看其他方法,都是在方法内部重新创建新的String对象,并且返回这个新的对象,原来的对象是不会被改变的。这也是为什么像replace, substring,toLowerCase等方法都存在返回值的原因。也是为什么像下面这样调用不会改变对象的值:
String ss = "123456"; System.out.println("ss = " + ss); ss.replace('1', '0'); System.out.println("ss = " + ss);
打印结果:
ss = 123456 ss = 123456
String对象真的不可变吗?
从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。
那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。下面是实例代码:
public static void testReflection() throws Exception { //创建字符串"Hello World", 并赋给引用s String s = "Hello World"; System.out.println("s = " + s); //Hello World //获取String类中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); //改变value属性的访问权限 valueFieldOfString.setAccessible(true); //获取s对象上的value属性的值 char[] value = (char[]) valueFieldOfString.get(s); //改变value所引用的数组中的第5个字符 value[5] = '_'; System.out.println("s = " + s); //Hello_World }
打印结果为:
s = Hello World s = Hello_World
在这个过程中,s始终引用的同一个String对象,但是再反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,他组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个Car对象,它组合了一个Wheel对象,虽然这个Wheel对象声明成了private final 的,但是这个Wheel对象内部的状态可以改变, 那么就不能很好的保证Car对象不可变。
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!
更多Java String源码分析相关文章请关注PHP中文网!