java源码分析-String类不可变性讨论

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数组中的元素进行修改。这样也就破坏了对象的不可变性了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-viiNRoPs-1614417022625)(C:\Users\viruser.v-desktop\AppData\Roaming\Typora\typora-user-images\image-20210225203026684.png)]

所以,对于引用类型,需要保证该引用类型的状态也是不可变的,也就是说要保证引用类型所依赖的对象域也是不可改变的。

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);
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rHaOgjYf-1614417022628)(C:\Users\viruser.v-desktop\AppData\Roaming\Typora\typora-user-images\image-20210227151415753.png)]

很明显,通过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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-91amNn0H-1614417022630)(C:\Users\viruser.v-desktop\AppData\Roaming\Typora\typora-user-images\image-20210227164609425.png)]

打开这个头文件,我们看看里面有些什么?

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");对应。

这样该路径下就有如下的五个文件了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YFk4CEMu-1614417022632)(C:\Users\viruser.v-desktop\AppData\Roaming\Typora\typora-user-images\image-20210227170012005.png)]

(6)运行java程序,查看结果;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SZ7KdYLR-1614417022633)(C:\Users\viruser.v-desktop\AppData\Roaming\Typora\typora-user-images\image-20210227170826671.png)]

可以看到,我们通过JNI技术成功的修改了String对象的值。也就是我们前面说的通过JNI也可以打破iString对象的不可变性!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值