• 深入理解Java字符串


    字符串常量池

    常量池

    • 常量池是 JAVA 的一项技术,八种基础数据类型(byteshortintlongfloatdoublebooleanchar)除了floatdouble都实现了常量池技术
    • 将经常用到的数据存放在一块内存中,实现数据共享,从而避免了数据的重复创建与销毁,提高了系统性能

    字符串常量池

    字符串常量池是 JAVA 常量池技术的一种实现,在JDK1.8以后,字符串常量池也被是现在JAVA堆内存中

    为什么存在字符串常量池
    • 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
    • JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
      • 为字符串开辟一个字符串常量池,类似于缓存区
      • 创建字符串常量时,首先判断字符串常量池是否存在该字符串,存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
    字符串常量池实现的基础
    • 实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享
    • 运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收
    字符串常量池存在哪里

    JVM 运行时数据区

    堆:存储的是对象,每个对象都包含一个与之对应的Class,堆中不存放基本数据类型和对象引用,只存放对象本身,对象由垃圾回收器负责回收,因此大小和生命周期不需要确定

    栈:每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),每个栈中的数据(原始类型和对象引用)都是私有的。栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令),数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失

    方法区:静态区,跟堆一样,被所有的线程共享。方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量

    字符串常量池则存在于方法区

    注意:

    • java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变
    • java7中,static变量从永久代移到堆中
    • java8中,取消永久代,方法区存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中
    • JDK1.8 中字符串常量池和运行时常量池逻辑上属于方法区,但是实际存放在堆内存中,因此既可以说两者存放在堆中,也可以说两者存在于方法区中,这就是造成误解的地方

    字符串创建案例

    字符串的两种创建方式

    方式一
    String s1 = "hello";
    

    先在栈中创建一个对String类的对象引用变量s1,然后通过符号引用去字符串常量池里找有没有"hello"

    如果没有则将"hello"存放到字符串常量池,并且将此常量的引用返回给s1

    如果已有"hello"常量,则直接返回字符串常量池中"hello" 的引用给s1

    eg:

    String str1 = "abc"; 
    String str2 = "abc"; 
    System.out.println(str1==str2); //true 
    

    可以看出str1str2是指向同一个对象的

    方式二
    String s2 = new String("hello");
    

    • 每一次创建都会在堆中(非字符串常量池中)创建字符串对象,而且并不会把"hello"对象地址加入到字符串常量池中,最终把该对象的引用返回给s1
    • new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间
    String str1 =new String ("abc"); 
    String str2 =new String ("abc"); 
    System.out.println(str1==str2); // false 
    

    new的方式是生成不同的对象。每new次生成一个,因此返回false

    intern() 方法

    String s1 = new String("hello");
    String s1intern = s1.intern();
    String s2 = "hello";
    

    当调用 intern 方法时:

    ​ 如果常量池中已经该字符串,则返回池中的字符串

    ​ 否则将此字符串添加到常量池中,并返回字符串的引用

    常见案例

    案例一

    public static void main(String[] args){
        //“ab”在编译的时候就能确定,所以编译的时候,ab被放进了常量池中,同时s1指向常量池中的ab对象
    	String s1 = "ab";
        //a和b这两个常量都能在编译时确定,所以他们相加的结果也能确定,因此编译器检查常量池中是否有值为ab的String对象,发现有了,因此s2也指向常量池中的ab对象
    	String s2 = "a" + "b";
    	System.out.println(s1 == s2);//true
    }
    

    案例二

    public static void main(String[] args){
        String s1 = "ab";
        String temp = "b";
        String s2 = "a" + temp; //编译时期不能确定为常量
        System.out.println(s1 == s2); //false
        System.out.println(s1 == s2.intern()); //true
    }
    

    案例三

    public static void main(String[] args){
        String s1 = "ab";
        final String temp = "b";
        String s2 = "a" + temp; //temp加final后是常量,可以在编译期确定b
        System.out.println(s1 == s2);  //true
    }
    

    案例四

    public class test4 {
        public static void main(String[] args){
            String s1 = "ab";
            final String temp = m1();
            //temp是通过函数返回的,虽然知道它是final的,但不知道具体是啥,要到运行期才知道temp的值
            String s2 = "a" + temp;
            System.out.println(s1 == s2);  //false
            System.out.println(s1 == s2.intern());  //true
        }
        
        private static String m1(){ 
            return "b"; 
        }
    }
    
    

    案例五

    public class test5 {
        private static String a = "ab";
        public static void main(String[] args){
            String s1 = "a";
            String s2 = "b";
            String s = s1 + s2;  // s1、s2 在编译期不能确定是否是常量
            System.out.println(s == a);  // flase
            System.out.println(s.intern() == a); //intern的含义 // true
    	}
    }
    

    案例六

    public class test6 {
        private static String a = new String("ab");
        public static void main(String[] args){
            String s1 = "a";
            String s2 = "b";
            String s = s1 + s2;
            System.out.println(s == a); // flase
            System.out.println(s.intern() == a); // flase
            System.out.println(s.intern() == a.intern()); // true
        }
    }
    

    案例七

    String s1 = new String("s1") ;
    String s2 = new String("s1") ;
    

    上面的代码创建了3个String对象,在编译期字符串常量池中创建了一个,运行期在堆中创建了两个(用new创建时,每new一次就在堆上创建一个对象,用引号创建的如果在常量池中已有就直接指向,不用创建)

    案例八

    String s1 = "s1";
    String s2 = s1;
    s2 = "s2";
    

    s1指向的对象中的字符串是"s1",字符串是不可变的,s2 = “s2”实际上s2的指向就变了  

    小总结

    • 当用new关键字创建字符串对象时, 不会查询字符串常量池

    • 当用""直接声明字符串对象时, 会查询字符串常量池

    • 通俗来讲就是字符串常量池提供了字符串的复用功能, 除非我们要显式创建新的字符串对象, 否则对同一个字符串虚拟机只会维护一份拷贝

    字符串操作类

    String

    查看String源码可以发现,String底层是通过char类型数组实现的

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

    StringBuilder & StringBuffer

    查看StringBuilder以及StringBuffer源码也可以发现,他们都继承了AbstractStringBuilder类,当查看AbstractStringBuilder类时,其也使用char类型数组实现

    abstract class AbstractStringBuilder implements Appendable, CharSequence {
        /**
         * The value is used for character storage.
         */
        char[] value;
    }
    

    通过两者都继承同一父类可以推断两者方法都是差不多的,只不过通过查看源码发现StringBuffer 的方法上添加了synchronized关键字,说明``StringBuffer 绝大部分方法都是线程安全的 ,因此在多线程的环境下应该使用StringBuffer以保证线程安全, 在单线程环境下我们应使用StringBuilder`以获得更高的效率

    String 与 StrintgBuilder的区别

    通过查看StringBuilder和String的源码我们会发现两者之间一个关键的区别:

    对于String, 凡是涉及到返回参数类型为String类型的方法, 在返回的时候都会通过new关键字创建一个新的字符串对象

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

    而对于StringBuilder, 大多数方法都会返回StringBuilder对象自身.

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
    

    以字符串拼接为例:

    当用String类拼接字符串时,

    ​ 每次都会生成一个StringBuilder对象, 然后调用两次append()方法把字符串拼接好, 最后通过StringBuildertoString()方法new出一个新的字符串对象

    ​ 也就是说每次拼接都会new出两个对象, 并进行两次方法调用, 如果拼接的次数过多, 创建对象所带来的时延会降低系统效率, 同时会造成巨大的内存浪费. 而且当内存不够用时, 虚拟机会进行垃圾回收, 这也是一项相当耗时的操作, 会大大降低系统性能

    当用StringBuilder类拼接字符串时

    StringBuilder拼接字符串就简单多了, 直接调用append方法就完事了, 除了最开始时需要创建StringBuilder对象, 运行时期没有创建过其他任何对象, 每次循环只调用一次append方法. 所以从效率上看, 拼接大量字符串时, StringBuilder要比String类快很多

    人生没有白走的路,每一步都算数
  • 相关阅读:
    浅谈C++多态性
    OSI七层模型具体解释
    文本框仅仅同意输入数字
    MessageDigest简单介绍
    CF 161D Distance in Tree【树DP】
    第六届蓝桥杯JavaA组国(决)赛真题
    Java实现 蓝桥杯 历届真题 稍大的串
    Java实现 蓝桥杯 历届真题 稍大的串
    Java实现 蓝桥杯 历届真题 稍大的串
    Java实现 蓝桥杯 历届真题 稍大的串
  • 原文地址:https://www.cnblogs.com/erhuoweirdo/p/14491013.html
Copyright © 2020-2023  润新知