概述
java源码分析-String类不可变性讨论
在很多面试过程中,有一个经常被问到的问题,请你谈一谈String对象为什么说是不可变的?
也许你会说,因为它被final修饰了,所以不可变。如果你这样回答,那么只能说,还是太年轻了。今天我们就好好讨论一下String对象的不可变性。
1.不可变对象
要聊String对象的不可变性,我们先要搞明白什么是不可变对象?不可变对象,顾名思义,对象在创建之后对象就不能在改变了。
对象的状态
对象的状态指的是存储在状态变量中的数据(状态变量包括实例或者静态域),还包括这个对象依赖的对象的域。
举例说明:
public class ObjectState {
private final int num;
private final Map<String,Object> map;
public ObjectState(int num, Map<String, Object> map) {
this.num = num;
this.map = map;
}
}
上面的例子我们知道num是实例变量,也就是ObjectState的状态变量,HashMap集合对象也是。但是我们还要知道其实HashMap的状态不仅存储在HashMap本身,还存储在许多的Map .Entry对象中。例如下面的类ObjectState, 它的状态是由num和map的状态共同构成的,而map中又会包含很多的Map.Entry,这些Map.Entry对象的状态也属于ObjectState对象状态的一部分。
final的作用
知道了对象的状态之后,我们再看一下final作用。
1)final修饰类的时候,表示该类不能被继承;
2)final修饰方法的时候,表示该方法不能被重写;
3)final修饰变量时,表示该变量不能被修改;
前面两个好理解,但是第三个的说法是不是与上面讲的map的例子有冲突了,map不也被final修饰了吗?应该不可变啊。其实不冲突!上面我们也提到了map本身没有改变,但是map集合中的entiry对象却并不受控制。本质上就是final修饰基本类型和引用类型的区别。
基本类型不多解释,final修饰之后,一旦赋值就不能再被改变了。而对于引用类型,final修饰他们的时候,只能保证引用本身没有变化,而对于引用所指向的对象内部的变化是不能够限制的。
我们再举个例子理解一下:
public class ObjectState2 {
private final int num;
private final String[] arr;
public ObjectState2(int num, String[] arr) {
this.num = num;
this.arr = arr;
}
public void modify(){
this.arr[0] = "2";
}
}
可以看到尽管arr数组被final修饰,按理来说,一旦通过构造函数创建arr就不在改变,但是我们依旧可以通过modify方法对arr数组中的元素进行修改。这样也就破坏了对象的不可变性了。
所以,对于引用类型,需要保证该引用类型的状态也是不可变的,也就是说要保证引用类型所依赖的对象域也是不可改变的。
2.String类不可变型分析
要理解String类的不可变性,首先看一下String类中都有哪些成员变量。在JDK1.8中,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
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
* <a href="{@docRoot}/../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 6.2, "Stream Elements"</a>
*/
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
首先可以看到,String类使用了final修饰符,表明String类是不可继承的。
然后,我们主要关注String类的成员变量value,value是char[]类型,因为String对象实际上是用这个字符数组进行封装的。再看value的修饰符,使用了private,也没有提供setter方法,所以在String类的外部不能修改value,同时value也使用了final进行修饰,那么在String类的内部也不能修改value,但是上面final修饰引用类型变量的内容提到,这只能保证value不能指向其他的对象,但value指向的对象的状态是可以改变的。通过查看String类源码可以发现,String类不可变,关键是因为SUN公司的工程师,在后面所有String的方法里都很小心的没有去动字符数组里的元素。所以String类不可变的关键都在底层的实现,而不仅仅是一个final。
值得注意的是,hash没有用final修饰呢?我们来看hash的计算源码:
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using {@code int} arithmetic, where {@code s[i]} is the
* <i>i</i>th character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
hash这个成员变量的值在没有被计算时,是默认的0,在调用hashCode才会真正计算hash值。而这个值的计算公式我们不难理解,对于一个长度为n的字符串s,hash=s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1],很明显,这个值每次计算都是一个固定值。
3.String真就不可变吗
String真就不可变吗?当我们思考这个的时候,可能已经想到了java有一个很强大的特性:反射。是的。虽然不能直接修改String对象的内容,但是,我们依然可以通过反射来进行一些骚操作,从而打破String对象的不可变性!
3.1通过反射
下面我们通过反射来修改String对象的内容:
/**
* 通过反射打破String的不可变性
*/
public class StringBreakImmutability {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str1 = new String("ABC");
System.out.println("反射前:"+str1);
//获取Class对象
Class clazz = str1.getClass();
//获取字段Field对象信息
Field value = clazz.getDeclaredField("value");
value.setAccessible(true);
//获取值
char[] ch = (char[]) value.get(str1);
ch[0] = 'B';
System.out.println("反射后:"+str1);
}
}
运行结果:
很明显,通过java的反射机制,我们成功打破String对象的不可变性,修改了其内容。
3.2通过JNI
其实还有其他方法能够改变String对象的内容。JNI(java native Interface)就是另一种可以打破String不可变性的方法。JNI是一种java通过调用c/c++的方式来完成相应的与操作系统相关的底层动作的技术。也就是说我们通过java可以调用c/c++方法,而c/c++是偏底层的语言,可以做一些java本身无法做到的事情,那么修改String对象内容也不在话下了。
下面我们就来一步一步实现一下(测试基于Linux环境下):
(1)写一个java类,调用native方法;
public class JNIDemo {
private String str = new String("hello java");
{
//系统加载其他语言的函数
System.load("/home/sj/test/jni_string.so");
}
//native标识本地方法
public native void stringJNI();
public static void main(String[] args) {
JNIDemo demo = new JNIDemo();
System.out.println("before jni, str:"+demo.str);
demo.stringJNI();
System.out.println("after jni, str:"+demo.str);
}
}
将java文件上传至linux系统/home/sj/test文件夹下。
(2)通过javac命令编译该文件生成字节码文件;
/usr/local/jdk1.8/bin/javac JNIDemo.java
这样该路径下就会生成.class字节码文件JNIDemo.class。
(3)通过javah命令获取头文件;
/usr/local/jdk1.8/bin/javah JNIDemo
打开这个头文件,我们看看里面有些什么?
vim JNIDemo.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNIDemo */
#ifndef _Included_JNIDemo
#define _Included_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JNIDemo
* Method: stringJNI
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JNIDemo_stringJNI
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
JNIEXPORT void JNICALL Java_JNIDemo_stringJNI 这句需要我们关注,因为后面的c语言实现的方法名称必须“Java_JNIDemo_helloJNI”一致。
(4)用c写一个native方法;
vim jni_string.c
#include <jni.h>
#include "JNIDemo.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_JNIDemo_stringJNI(JNIEnv *env,jobject obj){
//得到java类名
jclass java_class = (*env)->GetObjectClass(env,obj);
//获取strin类型,注意string的类型签名有个分号;
jfieldID id_str = (*env)->GetFieldID(env,java_class,"str","Ljava/lang/String;");
//修改java的string成员变量值
char* c_ch = "hello c";
//字符数组c_ch转换成jstring类型
jstring cstr = (*env)->NewStringUTF(env,c_ch);
//设置java的string类型变量s的值
(*env)->SetObjectField(env,obj,id_str,cstr);
}
(5)使用cjni.c生成动态链接库文件:cJNI.so
。
gcc -fPIC -I /usr/local/jdk1.8/include -I /usr/local/jdk1.8/include/linux -shared -o jni_string.so jni_string.c
/usr/local/jdk1.8 是linux系统安装jdk源码路径;
注意生成的动态链接库文件名称cJNI.so要与一开始的java代码中System.load("/home/sj/jni/test/jni_string.so");对应。
这样该路径下就有如下的五个文件了:
(6)运行java程序,查看结果;
可以看到,我们通过JNI技术成功的修改了String对象的值。也就是我们前面说的通过JNI也可以打破iString对象的不可变性!
最后
以上就是粗心大神为你收集整理的java源码分析-String类不可变性讨论java源码分析-String类不可变性讨论的全部内容,希望文章能够帮你解决java源码分析-String类不可变性讨论java源码分析-String类不可变性讨论所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复