Java中String类型不可变、String在内存的位置、StringBuffer和StringBuild,equals、==、comparTo区别等常见陷阱详解

一、String类型不可变

1、什么是不可变
        String str = "hello" ;
        str = str + " world" ;
        str += "!!!" ;
        System.out.println(str);// 执行结果 hello world!!!       

当给一个旧的字符串再次赋值的时候,并没有在原内存地址上修改数据,而是重新指向了一个新的对象,产生了新的地址;str只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。

  • 也就是说,str只是一个引用,它指向了一个具体的对象,当str=“hello”;
  • str += “!!!” ;原来的对象"hello"还在内存中存在,并没有改变。
  • str则指向最新生成的实例对象,之前的实例对象仍然存在,如果没有被再次引用,则会被垃圾回收

不可变类只是其实例不能被修改的类。 给 str 重新赋值仅仅只是改变了它的引用而已,并不会真正去改变它本来的内存地址上的值

内存结构如下图所示:

在这里插入图片描述

2、为什么不可变

JDK1.8源码如下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

String 类是 final 修饰的,保证了它不会被继承,而String底层是有value数组实现的,value是个char[ ]数组,且是用final修饰的。fina|修饰的字段创建以后就不可改变。
但是当final修饰的是引用类型时候,不能改变的是引用指向的对象,然而引用所指向的对象的值是可以改变的

也就是说Array变量只是stack上的一个引用,数组的本体结构在heap。String类里的value用final修饰,只是说stack里的这个叫value的引用地址不可变。没有说堆里array本身数据不可变。

  • 通过引用修改值是支持的,这时候的 value 对象在内存中已经是 a b d
    final char[] value = {'a', 'b', 'c'};
    value[2] = 'd';
  • final 修饰的仅仅只是 value 这个引用,你无法再将 value 指向其他内存地址,例如下面这段代码就是无法通过编译的:
    final char[] value = {'a', 'b', 'c'};
	char [] another = {'q', 'w', 'e', 'r'};
	value =  another; // 编译报错,final修饰 无法指向其它对象

所以仅仅通过一个 final 是无法保证其值不变的,如果类本身提供方法修改实例值,那就没有办法保证不变性了。Effective Java 中的第一条原则 不要提供任何会修改对象状态的方法 。String 类也很好的做到了这一点。在 String 中有许多对字符串进行操作的函数,例如 concat replace replaceAll 等等,这些函数是否会修改类中的 value 域呢?我们看一下 concat() 函数的内部实现:

    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

注意其中的每一步实现都不会对 value产生任何影响。首先使用 Arrays.copyOf() 方法来获得 value 的拷贝,最后重新 new 一个String对象作为返回值。其他的方法和 contact 一样,都采取类似的方法来保证不会对 value 造成变化。的的确确,String 类中并没有提供任何可以改变其值的方法。相比 final 而言,这更能保障 String 不可变。

String 不可变都是依靠底层实现,而不只是依靠一个final

3、不可变的好处
①字符串常量池的需要
  • 字符串常量池(String pool, String intern pool, String保留池) 是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象
  • 这样在大量使用字符串的情况下,可以节省内存空间,提高效率。之所以能实现这个特性,String的不可变性是最基本的一个必要条件。要是内存里字符串内容能改来改去,这么做就完全没有意义了。
    如下面的代码所示,将会在堆内存中只创建一个实际String对象.
String str1 = "abcd";
String str2 = "abcd";

在这里插入图片描述

②更加安全的
  • 在并发场景下,如果多个线程同时读一个资源,是线程安全的。但是只
    要对资源做写操作就有危险。String对象不可变导致不能被写,所以保证了线程安全。
  • String作为核心类,很多的内部方法的实现都是本地调用的,即调用操作系统本地API,其和操作系统交流频繁,假如这个类被继承重写的话,难免会是操作系统造成巨大的隐患。
    其它方面的安全:
public class Test {
    public static String appendStr(String s){
        s+="你好" ;
        return s;
    }
    public static StringBuilder appendSb(StringBuilder sb){
        return sb.append("世界");
    }
    public static void main(String[] args){
        //String做参数
        String s =new String("aa");
        String ss =  Test.appendStr(s);
        System. out.println("String aa-------" +s.toString());
        //StringBuilder做参数
        StringBuilder sb=new StringBuilder("ccc");
        StringBuilder ssbbb= Test.appendSb(sb);
        System.out.println("StringBuilder ccc -------"+sb. toString());
    }
}
③String存缓HashCode

Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。
字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码。

反射其实可以做到改变字符串

String str = "Hello";
// 获取 String 类中的 value 字段. 这个 value 和 String 源码中的 value 是匹配的.
Field valueField = String.class.getDeclaredField("value");
// 将这个字段的访问属性设为 true
valueField.setAccessible(true);
// 把 str 中的 value 属性获取到.
char[] value = (char[]) valueField.get(str);
// 修改 value 的值
value[0] = 'G';
System.out.println(str);
// 执行结果
Gello

二、String对象在内存中位置

  • 使用字符串常量池,每当我们使用字面量(String s=”1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。
  • 使用字符串常量池,每当我们使用关键字new(String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接常量池中复制该对象的副本,然后将常量池中对象的地址赋值到堆上,再将堆上的地址赋给引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s。

1、同时new两个相同的字符串

        String  str1 = new String("Hello");
        String  str2 = new String("Hello");
        System.out.println(str1==str2); //false

在这里插入图片描述

2、先赋值,再new,在赋值

        String  str1 = "hello";
        String  str2 = new String("hello");
        System.out.println(str1==str2); //false
        String str3 = "hello";
        System.out.println(str1==str3); // true

在这里插入图片描述
3、拼接情况

在这里插入图片描述
4、引用的指向发生了改变,再次修改就不会影响原来的值。
在这里插入图片描述
5、intern方法手动入池
intern方法手动入池后,每次引用指向的都将是常量池中的地址

        String str1 = "ha";
        String str2 = new String("ha").intern() ;
        System.out.println(str1==str2); //true		
        
        String str3 = new String("你好").intern() ;
        String str4 = "你好";        
 	    System.out.println(str3==str4); //true	

在这里插入图片描述

三、String、StringBuffer、StringBuild区别

在这里插入图片描述

1、可变与不可变

  • String类中使用字符数组保存字符串,如下就是,因为有“final”修饰符,所以可以知道string对象是不可变的private final char value[];
  • StringBuilder与StringBuffer继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,如下就是,可知这两种对象都是可变的char[] value;

2、多线程是否安全

  • String中的对象是不可变的,也就可以理解为常量,显然线程安全

  • AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
    StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的

  • StringBuilder并没有对方法进行加同步锁,所以是非线程安全的

3、StringBuffer比String多的方法

public synchronized StringBuffer reverse()//反转
public synchronized StringBuffer delete(int start, int end)//删除指定范围
public synchronized StringBuffer insert(int offset, 各种数据类型 b) //插入数据

相同点:
StringBuilder与StringBuffer有公共父类AbstractStringBuilder(抽象类)。
StringBuilder、StringBuffer的方法都会调用AbstractStringBuilder中的公共方法,如super.append(…)。只是StringBuffer会在方法上加synchronized关键字,进行同步。最后,如果程序不是多线程的,那么使用StringBuilder效率高于StringBuffer。

四、equals、==、comparTo的区别

1、equals
.equals() 用于比较两个对象的内容是否相等。
Java 的所有类都默认地继承着 Object 这个超类,该类有一个名为 .equals() 的方法,源码如下。

public boolean equals(Object obj) {
    return (this == obj);
}

Object 类的 .equals() 方法默认采用的是== 操作符进行比较,也就是说比较的是地址。假如子类没有重写该方法的话,那么 ==操作符和 .equals() 方法的功效就完全一样,比较两个对象的内存地址或者对象的引用是否相等
但实际情况中,有不少类重写了 .equals() 方法,因为比较内存地址太重了,不太符合现实的场景需求。String 类就重写了 .equals() 方法,所以String类使用.equals方法比较的是值。

        String aa = new String("Java");
        String cc = new String("Java");
        System.out.println(aa.equals(cc)); //true

2、==
基本类型比较值,引用类型比较的是地址

  • 基本类型的值相等我,所以true
 int a =99;
 int b =99;
 System.out.println(a == b);//true
  • 对于对象的比较是将对象引用的比较,对于a和b ,他们在内存中对应的地址是不一样的,b重新开辟内存空间,把“abc”存到里面。所以 a==b 返回的值是一个false.
String  a=new String("abc");
String  b=new String("abc");
System.out.println(a == b);//false
  • 赋值的方式创建字符串将在常量池中,第二次直接使用常量池中的地址,所以true
String str1 = "abc"; 
String str2 = "abc"; 
System.out.println(str1==str2); //true 

.equals() 方法在比较的时候需要判 null,而“==”操作符则不需要。

3、comparTo
compareTo()的返回值是int,它是先比较对应字符的大小(ASCII码顺序),也就是字典序

str1 > str2 正数
str1 < str2 负数
str1 == str2 0

字符串的比较大小规则, 总结成三个字 “字典序” 相当于判定两个字符串在一本词典的前面还是后面. 先比较第一个字符的大小(根据 unicode 的值来判定), 如果不分胜负, 就依次比较后面的内容

        System.out.println("A".compareTo("a")); // -32
        System.out.println("a".compareTo("A")); // 32
        System.out.println("A".compareTo("A")); // 0
        System.out.println( "abc".compareTo("abcd") ); // -1 (前面相等,s1长度小1)
        System.out.println( "abc".compareTo("abcdfg") ); // -3 (前面相等,s1长度小3)
        System.out.println( "abc".compareTo("1bcdfg") ); // 48 ("a"的ASCII码是97,"1"的的ASCII码是49,所以返回48)
        System.out.println( "abc".compareTo("cdfg") ); // -2 ("a"的ASCII码是97,"c"的ASCII码是99,所以返回-2)
        System.out.println("包".compareTo("子"));
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值