Java字符串——String深入

转载请注明原文地址:https://www.cnblogs.com/ygj0930/p/10840495.html

一:字符串的不可变性

   1、可变 与 不可变 辨析

    Java中的对象按照创建后,对象的内容是否可以被修改,分为 mutable object 和 immutable object。【注意:是对象的内容不可变,而不是指向该对象的引用变量内容不可变。】

    我们常见的不可变对象是几个基本数据类型的包装类——Integer、Double、String等。【想想为什么?——Tips:出于节省内存开销,避免重复创建。】

    不可变类有5大基本原则:

    1)类定义时,添加final修饰符,保证类不被继承【即:不允许在子类中被修改】

    2)类定义时,其成员变量一概使用 final private 修饰,保证变量私有的同时不允许修改

    3)不提供可以修改成员变量的方法,包括setter

    4)在构造函数中采用deep copy的形式将参数值拷贝给成员变量,而不是直接将参数值赋给成员变量【因为引用类型的参数只是传了一个地址,这样在外部改变该地址的内容会导致不可变对象的成员变量改变】

    5)在成员变量的getter方法中,不能直接返回成员变量本身,而是返回成员变量的copy对象【这也是为了防止引用类型的成员变量被外部获取后,改变引用指向的对象值引起不可变对象的内容变化】

    2、不可变对象的优缺点

    1)字符串常量池的需要——避免每次使用相同的字符串常量都重新创建相同的对象、节省存储空间

    2)线程安全:当一个String对象被多个线程共享时,无需担心线程安全问题

    3)支持hash和缓存:由于字符串对象是不可变的并且hashcode就缓存在对象中【在下文讲解】了,不需要重新计算,因此很适合作为Map中的键,因为字符串键的哈希处理速度要快过其它的键对象【这就是HashMap中的键往往都使用字符串的原因】

    4)使用类加载器要用字符串来传递加载的类名,而字符串的不可变性提供了安全性,以保证正确的类被加载。

    5)  缺点:当对String变量有重新赋值、修改等操作时,会不断创建大量的String对象。【当修改后的值未出现在字符串常量池的前提下】————【延伸:因此,在代码中涉及大量字符串操作时,使用StringBuilder或StringBuffer来进行

    3、“不可变对象”的非常规手段修改

    对于不可变对象,可以通过反射机制的手段改变其值——获取类的字段定义->改变该字段的可见性和可修改性->修改对象的变量值

    例如:

  //创建字符串"Hello World", 并赋给引用s
    String s = "Hello World"; 
    System.out.println("s = " + s); //Hello World

    //获取String类中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");
    //改变value属性的访问权限
    valueFieldOfString.setAccessible(true);

    //获取s对象上的value属性的值
    char[] value = (char[]) valueFieldOfString.get(s);
    //改变value所引用的数组中的第5个字符
    value[5] = '_';
    System.out.println("s = " + s);  //Hello_World


结果为:
s = Hello World
s = Hello_World

二:replaceFirst、replaceAll、replace 区别

    我们先来看一下程序:

String s = "my.test.txt";
System.out.println(s.replace(".", "#"));
System.out.println(s.replaceAll(".", "#"));
System.out.println(s.replaceFirst(".", "#"));

    它们的结果会一样吗?——No。

my#test#txt
###########
#y.test.txt

    这三个函数中,replaceFirst、replaceAll在替换时使用了正则表达式,因此上面三个函数的参数含义是不同的

    replace(src,des):将字符串中的src子串[也是一段字符串]替换成des。

    replaceAll(reg,des):将字符串中符合reg模式的内容替换成des。

    replaceFirst(reg,des):将字符串中,第一个匹配reg模式的内容替换成des。

    所以上面示例代码中,replace函数是将字符串中的“.”字符换成“#”,而replaceAll则是将所有字符[“.”是正则表达式的通配符]替换成“#”,replaceFirst则是将第一个字符替换成“#”。

三:String对“+”运算符的重载

    “在Java中是不支持重载运算符的!”

    java不支持运算符重载,因为java的语法比较繁杂,会导致使用类对象 像基本数据类型那样 用运算符进行操作时,无法做到像c++一样流畅。因此,Java中针对类对象的运算操作一般都是通过方法来定义,而不是运算符重载。

    唯一例外的是String类,它的拼接运算(+) 经过了重载,这个重载是通过jvm编译实现的,具体原理可以 手写一个字符串相+的java类文件并编译,然后通过 javap -c 文件.class 查看具体的过程。

   其原理是:String的+会被转化为StringBuilder的append方法,并生成一个新的String对象返回。

四:字符串拼接的5种方式比较

    Java中有以下五种方法处理字符串拼接:

    1. 加号 “+”:适用于小数据量的操作,代码简洁方便。

    2. String contact() 方法:适用于小数据量的操作,代码简洁方便。

    3. StringUtils.join() 方法:适用于将ArrayList转换成字符串的情景,可以省掉用for循环读取ArrayList手动拼接的过程。

    4. StringBuffer append() 方法:继承自AbstractStringBuilder,效率高,大批量的数据处理的好选择,该方法线程安全,由于加了线程锁,速度会比下面第5中慢一点。

    5. StringBuilder append() 方法:继承自AbstractStringBuilder,效率最高,大批量的数据处理的好选择,该方法线程不安全,因此速度最快,如果不涉及多线程操作,优先使用此方法。

五:String.valueOf(val) 和 obj.toString 的异同

    同:都返回参数的字符串表示形式。

    异: 对空值的调用结果不同:

           java.lang.Object类里已有public方法.toString(),所以对任何严格意义上的java对象都可以调用此方法。但在使用时要注意,必须保证object不是null值,否则将抛出NullPointerException异常。

           而valueOf(Object obj)对null值进行了处理,不会报任何异常。但当object为null 时,String.valueOf(object)的值是字符串”null”,而不是null。

     

六:switch 对 String 的支持

    Java1.7之前,switch只能局限于int 、short 、byte 、char四类做判断条件,因为在JVM内部实际大部分字节码指令只有int类型的值。

    在使用switch的时候,如果是非int型,会先转为int型,再进行条件判断

    但是在Java1.7中,switch增加了对String作为判断条件的支持,可String并不能直接转为int型,这是怎么做到的呢?

    原理:switch比较的是字符串常量的哈希值(缓存的int类型值,前文提到过),但是hash值可能会有冲突,所以还需要再调用equals方法将 switch(param)的param 与 case str的str 进行二次比较,二者综合之下达到唯一匹配的目的。

七:字符串池、常量池(运行时常量池、Class 常量池)、intern

    全局字符串常量池(string pool):全局常量池在每个JVM中只有一份,存放的是字符串常量值。

                                                string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的),
                                                字符串在第一次出现时被创建并且把引用放到stringtable中,在后面再出现时就不会重复创建而是直接从stringtable中找到字符串字面值的地址,返回给字符串引用变量。

  

    Class常量池:class常量池是在编译的时候每个class都有的,用于编译阶段,存放的是class文件中的字面量(常量)和符号引用

                       字面量就是我们所说的常量,如文本字符串、八种基本类型的值、被声明为final的常量值等。

                       符号引用是一组符号,用来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
                       直接引用是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄。

                                               

    运行时常量池:运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,将符号引用替换成直接引用,与全局常量池中的引用值保持一致

                         运行时常量池更具动态性,在运行期间也可以将新的变量放入常量池中,而不是一定要在编译时确定的常量才能放入。最主要的运用便是String类的intern()方法

    str.intern()函数:把字符串对象str加入常量池中,如果常量池中已有该字符串字面值,则返回stringtable中的引用值。(这样做主要是为了避免在堆中不断地创建新的字符串对象)

                           常量池我们都知道他是存在于方法区的,他是方法区的一部分,而方法区是线程共享的,所以常量池也就是线程共享的,但是他并不是线程不安全的,他其实是线程安全的,因为它让有相同值的引用指向同一个位置。如果引用值变化了,但是常量池中没有新的值,那么就会新开辟一个常量结果来交给新的引用。

    详细原理请看:https://www.cnblogs.com/ygj0930/p/6581009.html

八:String的New操作创建了几个对象?——两个

我们以String s1=new String("abc");为例

首先当我们的类Class在被ClassLoader加载时,"abc"被作为常量读入,在String Pool(字符串常量池)创建了一个"abc"的实例。

然后,调用到new String("abc")的时候,会在Heap里面复制一个相同的对象。

(1)类加载对一个类只会进行一次。"abc"在类加载时就已经创建并驻留了(如果该类被加载之前已经有"abc"字符串被驻留过则不需要重复创建用于驻留的"abc"实例)。驻留的字符串引用是放在全局共享的字符串常量池中的。[加载时创建一次]

(2)在这段代码后续被运行的时候,"abc"字面量对应的String实例已经固定了,不会再被重复创建。所以这段代码将常量池中的对象实例复制一份放到heap中,并且把heap中的这个对象的引用交给s1 持有。[运行时复制一次]

  因此,这条语句创建了2个对象。【这两个引用,它们的对象实例是不同的。】

九:String的hashCode的缓存和懒加载初始化

    String类中定义了一个私有成员变量——hash,它是一个整数,保存String对象的哈希值,也就是说String类型的对象的哈希值不会重复计算,计算过一次后就保存起来了,以后在被hash时直接取该值即可,无需重新计算

    这个值在String对象第一次被调用时进行初始化(懒加载,不是在String对象创建时初始化)。

 

原文地址:https://www.cnblogs.com/ygj0930/p/10840495.html