Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于String类的实现的补充:Java 9 之后,String 类的实现改用 byte 数组存储字符串 #675

Open
miraclewk opened this issue Mar 6, 2020 · 11 comments
Labels
discuss discuss a problem enhancement New feature or request or suggestion perfect Improve knowledge points or descriptions

Comments

@miraclewk
Copy link

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串

private final byte[] value;

@Snailclimb
Copy link
Owner

感谢补充!

@Snailclimb Snailclimb added the enhancement New feature or request or suggestion label Mar 7, 2020
@jinpenggit
Copy link

为什么使用byte字节而舍弃了char字符:

节省内存占用,byte占一个字节(8位),char占用2个字节(16),相较char节省一半的内存空间。节省gc压力。
针对初始化的字符,对字符长度进行判断选择不同的编码方式。如果是 LATIN-1 编码,则右移0位,数组长度即为字符串长度。而如果是 UTF16 编码,则右移1位,数组长度的二分之一为字符串长度。

@HE996
Copy link

HE996 commented Sep 20, 2020

String 可以存放汉字,String的 []方法返回值还是char,估计是两个byte实现的,

@HE996
Copy link

HE996 commented Sep 20, 2020

这是why?
public class Main {
private static final char[] value = {'1','2','3'};
public static void main(String[] args) {
System.out.println(value[0]);
value[0] = '3';
System.out.println(value[0]);
}
}
output:
1
3

@luonianxin
Copy link

这是why?
public class Main {
private static final char[] value = {'1','2','3'};
public static void main(String[] args) {
System.out.println(value[0]);
value[0] = '3';
System.out.println(value[0]);
}
}
output:
1
3

这里的被final修饰的变量指向的是一个数组,被final修饰的不可更改指的是 value变量存储的值也就是字符数组的地址不能再更改,即不能再将一个新的对象赋值给value(value = new char[3],这样编译是会报错的).但是value指向的数组里头的元素值还是可以修改的

@sslalxh
Copy link

sslalxh commented Feb 19, 2021

这是why?
public class Main {
private static final char[] value = {'1','2','3'};
public static void main(String[] args) {
System.out.println(value[0]);
value[0] = '3';
System.out.println(value[0]);
}
}
output:
1
3

这里的被final修饰的变量指向的是一个数组,被final修饰的不可更改指的是 value变量存储的值也就是字符数组的地址不能再更改,即不能再将一个新的对象赋值给value(value = new char[3],这样编译是会报错的).但是value指向的数组里头的元素值还是可以修改的

那为什么String对象是不可变的呢?String对象字符数组不是可以通过下标更改字符串的某个元素么。。。

@luonianxin
Copy link

虽然我们可以使用反射跳过安全检查,设置字符数组的值,改变值后,打印出的字符串确实是改变了,但是我们通过对象的hashCode方法查看hash值发现,hashCode并没有随着字符数组被修改而改变.根据hash值的计算规则我们知道,如果一个对象的被修改了,那么它的hash值肯定也会跟着变化,我理解的String不可变指的是这个.虽然我们可以通过反射来改变字符数组的值而在显示上改变字符串的值,实际上该字符串的hash值并没有改变.在实际中不建议这样做.

      public static void main(String [] args) throws IllegalAccessException, InstantiationException {
        Class clazz = String.class;
        String strObj = "12345";
        Field [] fields = clazz.getDeclaredFields();
        char [] obj = null;
        System.out.println("before change hashcode is "+ strObj.hashCode());
        // before change hashcode is 46792755
        for (int i = 0; i < fields.length; i++) {
            if(fields[i].getName().equals("value")) {
                fields[i].setAccessible(true);
                obj = (char[]) fields[i].get(strObj);
                obj[0] = 'c';
            }
        }
        System.out.println(strObj);
        // c2345
        System.out.println("after change new hashCode is "+strObj.hashCode());
        // after change new hashCode is 46792755 与之前相同
        System.out.println("12345".hashCode()); // 46792755
        System.out.println("c2345".hashCode()); // 92968805
    }

上面的代码只能解释一种情况,经测试又发现了新的情况,我也不知道为什么出现这样的情况,希望有大佬释疑一下?谢谢

    public static void main(String [] args) throws IllegalAccessException, InstantiationException {
        Class clazz = String.class;
        String strObj = new String("12345");
        Field [] fields = clazz.getDeclaredFields();
        char [] obj = null;
        Field hash = null;
        System.out.println("before change hashcode is "+ strObj.hashCode());
        // before change hashcode is 46792755
        for (int i = 0; i < fields.length; i++) {
            if(fields[i].getName().equals("value")) {
                fields[i].setAccessible(true);
                obj = (char[]) fields[i].get(strObj);
                obj[0] = 'c';
            }
        }

        System.out.println(strObj);
        // c2345
        System.out.println("after change new hashCode is "+strObj.hashCode());
        // after change new hashCode is 46792755 与之前相同
        System.out.println("12345".hashCode()); // 92968805
        System.out.println("c2345".hashCode()); // 92968805
    }

@Snailclimb Snailclimb reopened this Feb 20, 2021
@Snailclimb Snailclimb changed the title String类的实现 关于String类的实现的补充:Java 9 之后,String 类的实现改用 byte 数组存储字符串 Feb 20, 2021
@Snailclimb Snailclimb added discuss discuss a problem perfect Improve knowledge points or descriptions labels Feb 20, 2021
@YannySky
Copy link

YannySky commented Jul 6, 2021

这个问题有意思,:joy::joy:
String strObj = new String("12345");
这种字符串创建方式先在常量池里面创建了12345这个字符串,然后把常量池里面的char[]的引用赋值给了strObj

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

所有后面代码在修改这个数组的是否把常量池里的string对象数组给改了

System.out.println("12345".hashCode()); // 92968805
System.out.println("c2345".hashCode()); // 92968805

这两行代码打印hash的时候取的是常量池里面的对象所以计算结果实际就是“c2345"的hash
实际上这时候执行 System.out.println("12345");输出的也是”c2345"

虽然我们可以使用反射跳过安全检查,设置字符数组的值,改变值后,打印出的字符串确实是改变了,但是我们通过对象的hashCode方法查看hash值发现,hashCode并没有随着字符数组被修改而改变.根据hash值的计算规则我们知道,如果一个对象的被修改了,那么它的hash值肯定也会跟着变化,我理解的String不可变指的是这个.虽然我们可以通过反射来改变字符数组的值而在显示上改变字符串的值,实际上该字符串的hash值并没有改变.在实际中不建议这样做.

      public static void main(String [] args) throws IllegalAccessException, InstantiationException {
        Class clazz = String.class;
        String strObj = "12345";
        Field [] fields = clazz.getDeclaredFields();
        char [] obj = null;
        System.out.println("before change hashcode is "+ strObj.hashCode());
        // before change hashcode is 46792755
        for (int i = 0; i < fields.length; i++) {
            if(fields[i].getName().equals("value")) {
                fields[i].setAccessible(true);
                obj = (char[]) fields[i].get(strObj);
                obj[0] = 'c';
            }
        }
        System.out.println(strObj);
        // c2345
        System.out.println("after change new hashCode is "+strObj.hashCode());
        // after change new hashCode is 46792755 与之前相同
        System.out.println("12345".hashCode()); // 46792755
        System.out.println("c2345".hashCode()); // 92968805
    }

上面的代码只能解释一种情况,经测试又发现了新的情况,我也不知道为什么出现这样的情况,希望有大佬释疑一下?谢谢

    public static void main(String [] args) throws IllegalAccessException, InstantiationException {
        Class clazz = String.class;
        String strObj = new String("12345");
        Field [] fields = clazz.getDeclaredFields();
        char [] obj = null;
        Field hash = null;
        System.out.println("before change hashcode is "+ strObj.hashCode());
        // before change hashcode is 46792755
        for (int i = 0; i < fields.length; i++) {
            if(fields[i].getName().equals("value")) {
                fields[i].setAccessible(true);
                obj = (char[]) fields[i].get(strObj);
                obj[0] = 'c';
            }
        }

        System.out.println(strObj);
        // c2345
        System.out.println("after change new hashCode is "+strObj.hashCode());
        // after change new hashCode is 46792755 与之前相同
        System.out.println("12345".hashCode()); // 92968805
        System.out.println("c2345".hashCode()); // 92968805
    }

@ckp-0217
Copy link

ckp-0217 commented Oct 4, 2021

涉及到hash的初始化和缓存 new string和直接字面量 两者strObj 不一样

 public static void main(String[] args) throws  IllegalAccessException {

        Class clazz = String.class;
        String strObj = new String("12345");
//        相当于执行以下代码
//           public String(String original) {
//            this.value = original.value; 
//            this.hash = original.hash;     strObj 的hash为“12345” 的hash  !!! 两者都没有初始化  hash都为0
//        }
        
        Field [] fields = clazz.getDeclaredFields();
        
        System.out.println(strObj.hashCode());//第一次加载strObj的hash strObj 的hash为46792755 此时 “12345”hash依旧为0
//        (如果不执行这句代码,那么hash依旧为0,最后执行时会对hash初始化,变为92968805)
//        public int hashCode() {//初始化 会根据value的值进行hash
//            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;  !!!//strObj 的hash被缓存了 不会再随着value改变而改变
//            }
//            return h; 
//        }
        System.out.println("12345".hashCode()); //第一次加载“12345”的hash  “12345” 的hash为46792755 同上
        // (如果不执行这句代码,那么hash依旧为0,最后执行时会对hash初始化,变为92968805)

        char [] obj = null;
        Field hash = null;
        System.out.println("before change hashcode is "+ strObj.hashCode());//这地方调用了 hashcode函数 相当于初始化
        // before change hashcode is 46792755
        for (int i = 0; i < fields.length; i++) {
            if(fields[i].getName().equals("value")) {
                fields[i].setAccessible(true);
                obj = (char[]) fields[i].get(strObj);
                obj[0] = 'c';
            }
        }

        System.out.println(strObj);
        // c2345
        System.out.println("after change new hashCode is "+strObj.hashCode());
        // after change new hashCode is 46792755 与之前相同
        
        System.out.println("12345".hashCode());//strObj 的hash为46792755
        System.out.println(strObj.hashCode());//“12345” 的hash为46792755
        System.out.println("c2345".hashCode()); // 92968805

    }

虽然我们可以使用反射跳过安全检查,设置字符数组的值,改变值后,打印出的字符串确实是改变了,但是我们通过对象的hashCode方法查看hash值发现,hashCode并没有随着字符数组被修改而改变.根据hash值的计算规则我们知道,如果一个对象的被修改了,那么它的hash值肯定也会跟着变化,我理解的String不可变指的是这个.虽然我们可以通过反射来改变字符数组的值而在显示上改变字符串的值,实际上该字符串的hash值并没有改变.在实际中不建议这样做.

      public static void main(String [] args) throws IllegalAccessException, InstantiationException {
        Class clazz = String.class;
        String strObj = "12345";
        Field [] fields = clazz.getDeclaredFields();
        char [] obj = null;
        System.out.println("before change hashcode is "+ strObj.hashCode());
        // before change hashcode is 46792755
        for (int i = 0; i < fields.length; i++) {
            if(fields[i].getName().equals("value")) {
                fields[i].setAccessible(true);
                obj = (char[]) fields[i].get(strObj);
                obj[0] = 'c';
            }
        }
        System.out.println(strObj);
        // c2345
        System.out.println("after change new hashCode is "+strObj.hashCode());
        // after change new hashCode is 46792755 与之前相同
        System.out.println("12345".hashCode()); // 46792755
        System.out.println("c2345".hashCode()); // 92968805
    }

上面的代码只能解释一种情况,经测试又发现了新的情况,我也不知道为什么出现这样的情况,希望有大佬释疑一下?谢谢

    public static void main(String [] args) throws IllegalAccessException, InstantiationException {
        Class clazz = String.class;
        String strObj = new String("12345");
        Field [] fields = clazz.getDeclaredFields();
        char [] obj = null;
        Field hash = null;
        System.out.println("before change hashcode is "+ strObj.hashCode());
        // before change hashcode is 46792755
        for (int i = 0; i < fields.length; i++) {
            if(fields[i].getName().equals("value")) {
                fields[i].setAccessible(true);
                obj = (char[]) fields[i].get(strObj);
                obj[0] = 'c';
            }
        }

        System.out.println(strObj);
        // c2345
        System.out.println("after change new hashCode is "+strObj.hashCode());
        // after change new hashCode is 46792755 与之前相同
        System.out.println("12345".hashCode()); // 92968805
        System.out.println("c2345".hashCode()); // 92968805
    }

@JuiceApp1e
Copy link
Contributor

JuiceApp1e commented Mar 5, 2022

因为理想情况下 String 是不可变的, hashCode 按理说是不会发生改变的,因此在设计的时候是这样设计的:在新建 String 对象的时候就会计算一次 hashCode 的值,并保存在 String 类的成员变量 hash 中,当你调用 hashCode() 方法时,仅仅是返回了 hash 值,而不是重新计算一次该字符串对应的 hashCode。

虽然我们可以使用反射跳过安全检查,设置字符数组的值,改变值后,打印出的字符串确实是改变了,但是我们通过对象的hashCode方法查看hash值发现,hashCode并没有随着字符数组被修改而改变.根据hash值的计算规则我们知道,如果一个对象的被修改了,那么它的hash值肯定也会跟着变化,我理解的String不可变指的是这个.虽然我们可以通过反射来改变字符数组的值而在显示上改变字符串的值,实际上该字符串的hash值并没有改变.在实际中不建议这样做.

      public static void main(String [] args) throws IllegalAccessException, InstantiationException {
        Class clazz = String.class;
        String strObj = "12345";
        Field [] fields = clazz.getDeclaredFields();
        char [] obj = null;
        System.out.println("before change hashcode is "+ strObj.hashCode());
        // before change hashcode is 46792755
        for (int i = 0; i < fields.length; i++) {
            if(fields[i].getName().equals("value")) {
                fields[i].setAccessible(true);
                obj = (char[]) fields[i].get(strObj);
                obj[0] = 'c';
            }
        }
        System.out.println(strObj);
        // c2345
        System.out.println("after change new hashCode is "+strObj.hashCode());
        // after change new hashCode is 46792755 与之前相同
        System.out.println("12345".hashCode()); // 46792755
        System.out.println("c2345".hashCode()); // 92968805
    }

上面的代码只能解释一种情况,经测试又发现了新的情况,我也不知道为什么出现这样的情况,希望有大佬释疑一下?谢谢

    public static void main(String [] args) throws IllegalAccessException, InstantiationException {
        Class clazz = String.class;
        String strObj = new String("12345");
        Field [] fields = clazz.getDeclaredFields();
        char [] obj = null;
        Field hash = null;
        System.out.println("before change hashcode is "+ strObj.hashCode());
        // before change hashcode is 46792755
        for (int i = 0; i < fields.length; i++) {
            if(fields[i].getName().equals("value")) {
                fields[i].setAccessible(true);
                obj = (char[]) fields[i].get(strObj);
                obj[0] = 'c';
            }
        }

        System.out.println(strObj);
        // c2345
        System.out.println("after change new hashCode is "+strObj.hashCode());
        // after change new hashCode is 46792755 与之前相同
        System.out.println("12345".hashCode()); // 92968805
        System.out.println("c2345".hashCode()); // 92968805
    }

@WHeLiosC
Copy link

为什么使用byte字节而舍弃了char字符:

节省内存占用,byte占一个字节(8位),char占用2个字节(16),相较char节省一半的内存空间。节省gc压力。 针对初始化的字符,对字符长度进行判断选择不同的编码方式。如果是 LATIN-1 编码,则右移0位,数组长度即为字符串长度。而如果是 UTF16 编码,则右移1位,数组长度的二分之一为字符串长度。

https://openjdk.java.net/jeps/254

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discuss discuss a problem enhancement New feature or request or suggestion perfect Improve knowledge points or descriptions
Projects
None yet
Development

No branches or pull requests

10 participants