Java-String 知识链

开始

文章从源码切入,按照正常做设计的思维逻辑展开,探寻一个知识点背后的知识链。

1.重要的成员变量value,hash

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
}

2.String特性-不可变

final关键字决定了一个String对象被创建后,其value只能指向一个地址不可被修改,而且value是private的,String没有提供setValue等公共方法来修改这些值,所以在String类的外部无法修改String。

在使用replace(),substring()等这种方法时,如果有改变的话返回的都是一个新的String对象

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > value.length) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    int subLen = endIndex - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return ((beginIndex == 0) && (endIndex == value.length)) ? this
            : new String(value, beginIndex, subLen);
}

3.为什么String要被设计为不可变的呢?

优点1. 便于实现字符串常量池(String pool)

知识铺垫:
(1)使用字符串常量池是为了提高效率
(2)java的内存模型分为 堆 和 栈 。
a.基本类型的变量放在栈里;
b.封装类型中,对象放在堆里,对象的引用放在栈里。

(3)为什么会提高效率呢?

Java 的字符串池属于 JVM 专门给指定的特殊内存区域,用来存储字符串字面量。在 Java 7 之前,分配于 JVM 的方法区内,属于常量池的一部分;而 Java7 之后字符串池被移至堆内存进行管理,这样的好处就是允许被 JVM 进行垃圾回收操作,将未被引用的字符串所占内存即使回收,以此节省内存。
常量池使用了共享设计模式,并且可以被共享使用
使用字符串常量池可以在编译时节省空间以及对象创建的时间,但是为了避免重复,也会有JVM遍历常量池的消耗,但总时间还是缩短的。在不使用intern()的情况下,字符串常量池对效率并没有什么影响,intern()方法便是将字符串对象的字符数组放入字符串常量池中。

(4)String在不同时期创建的差异

		String str1=”abc”;
		String str2=”abc”;

第二行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为“abc”的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。
这种使用字面量赋值的情况,JVM在编译时就会对此进行优化,str1和str2都指向相同的地址,这个地址便是”abc”在字符串常量池中的地址

		String str1=”abc”;
		String str3=new String("abc");

这时候str1==str3是false的,
因为创建str3的时候是在运行期,所以并没有将str3的地址加入字符串常量池,在

		str3=str3.intern();

之后str1==str3是true的,因为intern()方法便是将字符串对象的字符数组放入字符串常量池中,而常量池中已有了相同的值,所以两者指向的地址空间是一致的

		String str3=new String("abc");
		String str4=new String(JSON.get(key).toString());

这里即使JSON.get(key).toString()的值便是”abc”,str3==str4也是false的,因为这两者的创建都是在运行期,是不会直接将变量加入到字符串常量池的。

		String a=“ab”+“cd”;

“ab”和“cd”分别创建了一个对象,它们经过“+”连接后又创建了一个对象“abcd”,因此一共三个,并且它们都被保存在字符串池里了。

		String a = “ab”;
		String b = “cd”;
		String c = new String(“ef”);
		String d = a+b;
		String e = a+c;

只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中(比如变量d)。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中(比如变量e),因为一个在编译期就确定了,另一个运行期就运行了,对此我们不再赘述。因此提倡大家用引号包含文本的方式来创建String对象以提高效率,实际上这也是我们在编程中常采用的。

Java中newString(abc)创建几个对象解释?原文参考:https://blog.csdn.net/songylwq/article/details/7297004
为什么 String 需要不可变?原文参考:
https://zhuanlan.zhihu.com/p/78946350
String是值传递还是引用传递?原文参考:
https://www.cnblogs.com/zhangliwei/p/12056692.html
画个图形容一下上述情况

优点2. 允许String对象缓存HashCode

字符串作为基础的数据结构,大量地应用在一些集合容器之中,尤其是一些散列集合,在散列集合中,存放元素都要根据对象的 hashCode() 方法来确定元素的位置。由于字符串 hashcode 属性不会变更,保证了唯一性,使得类似 HashMap,HashSet 等容器才能实现相应的缓存功能。由于 String 的不可变,避免重复计算 hashcode,只有使用缓存的 hashcode 即可,这样一来大大提高了在散列集合中使用 String 对象的性能。

优点3. 保证线程安全

在多线程中,只有不变的对象和值是线程安全的,可以在多个线程中共享数据。由于 String 的不可变性,当一个线程”修改“了字符串的值,只会产生一个新的字符串对象(String传递的是栈中的变量的拷贝,当重新赋值时这个拷贝会指向新的地址),不会对其他线程的访问产生副作用,访问的都是同样的字符串数据,不需要任何同步操作。

优点4. 安全性

由于字符串无论在任何 Java 系统中都广泛使用,会用来存储敏感信息,如账号,密码,网络路径,文件处理等场景里,保证字符串 String 类的安全性就尤为重要了,如果字符串是可变的,容易被篡改,那我们就无法保证使用字符串进行操作时,它是安全的,很有可能出现 SQL 注入,访问危险文件等操作。

总体来说, String不可变的原因包括 设计考虑,效率优化问题,以及安全性这三大方面. 事实上,这也是Java面试中的许多 “为什么” 的答案。

为什么 String 需要不可变
原文参考:https://zhuanlan.zhihu.com/p/78946350
String是值传递还是引用传递
原文参考: https://blog.csdn.net/u010469514/article/details/80838678

4. String-hash成员变量:让String更适合做key值

原因:

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值的计算规则:
以字符串"123"为例:
字符’1’的ascii码是49
hashCode = (49*31 + 50)*31 + 51
或者这样看:
hashCode=(‘1’ * 31 + ‘2’ ) * 31 + ‘3’
这实际可以看作是一种权重的算法,在前面的字符的权重大
这样有个明显的好处,就是前缀相同的字符串的hash值都落在邻近的区间
好处有两点:

  1. 字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码。
  2. hash值相邻,如果存放在容器,比好HashSet,HashMap中时,实际存放的内存的位置也相邻,则存取的效率也高。(程序局部性原理

5. 反射修改字符串数组内

这里说的不可变是指value指向的地址空间不可变,而不是改地址中的内容不可变,在java中可以使用反射来修改字符串数组内的值,如下:

public static void main(String[] args) throws Exception{
    String str = "hello" ;    //实例化一个String类对象
    String s = str ;    //用于后面的比较测试
    //打印字符串和hashCode编码
    System.out.println(str + "::" + str.hashCode());//hello::99162322
    Class<?> cls = String.class;
    Field value = cls.getDeclaredField("value");
    value.setAccessible(true);
    char[] arr = (char[]) value.get(str);    //反射取得str对象的字符数组
    arr[0] = 's' ;   //修改字符数组的内容
    System.out.println(str + "::" + str.hashCode());//sello::99162322
}

同样,使用Field中的set方法也可以设置一个新的字符数组,如下:

public static void main(String[] args) throws Exception {
    String str = "hello"; // 实例化一个String类对象
    char c[] = new char[]{'a','a','a','a','a','a','a','a'};
    String s = str; 
    System.out.println(str + "::" + str.hashCode());
    Class<?> cls = String.class;
    Field value = cls.getDeclaredField("value");
    value.setAccessible(true);
    value.set(str, c);
    System.out.println(str + "::" + str.hashCode());
    System.out.println(s == str);
}

6. “”、null、new String()三者的区别

String并不是基本数据类型,而是一个对象。字符串为对象,那么在初始化之前,它的值为null,到这里就有必要提下””、null、new String()三者的区别。null 表示string还没有new ,也就是说对象的引用还没有创建,也没有分配内存空间给他,而””、new String()则说明了已经new了,只不过内部为空,但是它创建了对象的引用,是需要分配内存空间的。

	@Test
	public void testString2(){
		String s1=new String();
		String s2="";
		String s3=null;
		System.out.println("s1.equals(s2):"+s1.equals(s2));
		System.out.println("s1==s2:"+ s1==s2);
		System.out.println("s1==s3:"+ s1==s3);
		System.out.println("s2==s3:"+ s2==s3);
	}

在这里插入图片描述
这里s1.equals(s2)是为true的,因为String的无参构造函数是如下代码所示的,是拿 “” 做的初始化

    /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = "".value;
    }

ps:
这里如下图所示,我对四个输出前面都加了字符串,为什么只有第一行输出前面字符串了呢?
在下认为是编译器做变异的时候对于能确定的东西做了优化,造成了现在的显示,但是真正的原因是否如此还有待考证,如果有别的答案,欢迎评论!
字符串没显示

7. 待补充内容

StringBuffer 和 StringBuilder 类
String类,StringBuffer 和 StringBuilder 类三者的区别

以上内容是根据本人有限的知识面做出的理解,如有不足之处,敬请指教!

谢谢阅读

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值