Java基础知识之字符串

一、理解内存

了解字符串之前我们得先理解什么是内存?
通常我们所说的内存就是暂时存储程序以及数据的地方,包括随机存储器(RAM),只读存储器(ROM),以及高速缓存(CACHE)。只不过因为RAM是其中最重要的存储器。
我们来看RAM组成:

  • 寄存器:速度最快的存储场所,因为寄存器位于处理器内部,所以在程序中我们无法控制。
  • 栈(Stack) :存放基本类型的对象和引用,但是对象本身不存放在栈中,而是存放在堆中。
    Java中存在8大基本类型,他们的变量值中存放的就是具体的数值,而其他的类型都叫做引用类型(对象也是引用类型,你只要记住除了基本类型,都是引用类型)他们的变量值中存放的是他们在堆中的引用(内存地址)。
  • 堆(Heap):在堆上分配内存的过程称作 内存动态分配过程。在java中堆用于存放由new创建的对象和数组。堆中分配的内存
  • 静态存储区/方法区(Static Field):是指在固定的位置上存放应用程序运行时一直存在的数据,java在内存中专门划分了一个静态存储区域来管理一些特殊的数据变量如静态的数据变量。
  • 常量池(Constant Pool):顾名思义专门存放常量的。常量池就是该类型所有用到地常量的一个有序集合包括直接常量(基本类型,String)和对其他类型、字段和方法的符号引用。

总结:

  1. 定义一个局部变量的时候,java虚拟机就会在栈中为其分配内存空间,局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。因为它们属于方法中的变量,生命周期随方法而结束。
  2. 成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体),因为它们属于类,类对象终究是要被new出来使用的。当堆中对象的作用域结束的时候,这部分内存也不会立刻被回收,而是等待系统GC进行回收。

二、字符串常量池

我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。

优缺点

字符串常量池的好处就是减少相同内容字符串的创建,节省内存空间。

如果硬要说弊端的话,就是牺牲了CPU计算时间来换空间。CPU计算时间主要用于在字符串常量池中查找是否有内容相同对象的引用。不过其内部实现为HashTable,所以计算成本较低。

创建字符串的两种方式:

  1. 字面量形式:如:String s = “hello”;
    JVM检测这个字面量,如果JVM通过字符串常量池查找不到内容为hello的字符串对象存在,那么会创建这个字符串对象,然后将刚创建的对象的引用放入到字符串常量池中,并且将引用返回给变量s。如果发现内容为“hello”的字符串存在字符串常量池中,那么直接将已经存在的字符串引用返回给变量s。
  2. 使用new创建:如:String s = new String(“hello”);
    new创建字符串时首先查看池中是否有相同值的字符串,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果池中没有,则在堆中创建一份,然后返回堆中的地址(注意,此时不需要从堆中复制到池中,否则导致浪费池的空间)

三、String、StringBuffer、StringBuilder拼接字符串探究

如果细心大家就可以发现,String、StringBuffer、StringBuilder类都实现了CharSequence接口,所以,我们先去看一下这个接口

/**
 * 此接口表示有序的一组字符,并定义了探测它们的方法。
 */
public interface CharSequence {

    /**
     * 返回此序列中的字符数。
     */
    public int length();

    /**
     * 返回索引处的字符
     */
    public char charAt(int index);

    /**
     * 从起始索引(包括)返回到此序列的结束索引(排他)。
     */
    public CharSequence subSequence(int start, int end);

    /**
     * 以与此顺序相同的顺序返回具有相同字符的字符串。
     */
    public String toString();
}

也就是说,CharSequence其实也就是定义了字符串操作的接口,其他具体的实现是由String、StringBuilder、StringBuffer完成的,String、StringBuilder、StringBuffer都可以转化为CharSequence类型。

四、字符串实战

实例1:

    String str = "ABC";
    String str1 = new String("ABC");
    System.out.println(str == str1);    //false    
    System.out.println(str.equals(str1));  //true

分析:第一句:创建一个常量,放于字符串常量池中。
第二句:创建一个对象,将字符串常量池中的”ABC”赋值到堆中。
第三句:两个对象不是同一个对象,所以输出false。
第四句:因String的equals方法重写过,两个对象的内容相等,所以true。

实例2:

String str1 = "123";
System.out.println("123" == str1.substring(0));     //true
System.out.println("23" == str1.substring(1));      //false

补充:substring源码:

public String substring(int start) {
        if (start == 0) {
            return this;
        }
        if (start >= 0 && start <= count) {
            return fastSubstring(start, count - start);
        }
        throw indexAndLength(start);
    }

分析:第二句:由substring源码可知,如果start==0,就返回当前对象,所以为true
第三句:由substring源码可知,如果start不等于0,则创建了新的对象,所以为false

实例3:

String str3 = new String("ijk");
String str4 = str3.substring(0);
System.out.println(str3 == str4);       //true
System.out.println((new String("ijk") == str4));        //false

分析:第三句:两个相同的对象,输出true
第四句:新创建了一个对象,两个不同的对象,输出false

实例4:

String str5 = "NPM";
String str6 = "npm".toUpperCase();
System.out.println(str5 == str6);   //false 
System.out.println(str5.equals(str6));      //true

补充:toUpperCase源码:

public String toUpperCase() {
        return CaseMapper.toUpperCase(Locale.getDefault(), this, count);
    }
public static String toUpperCase(Locale locale, String s, int count) {
        String languageCode = locale.getLanguage();
        if (languageCode.equals("tr") || languageCode.equals("az") || languageCode.equals("lt")) {
            return ICU.toUpperCase(s, locale);
        }
        if (languageCode.equals("el")) {
            return EL_UPPER.get().transliterate(s);
        }
        .......
        .......
        .......
        .......
        if (output == null) {
            if (newString != null) {
                return newString;
            } else {
                return s;
            }
        }
        return output.length == i || output.length - i < 8 ? new String(0, i, output) : new String(output, 0, i);

注意1:toUpperCase()和toLowerCase()只对英文字母有效,对除了A~Z和a~z的其余字符无任何效果
注意2:toUpperCase()和toLowerCase()都创建了新的对象。

分析:第三句:两个不同的对象,输出false
第四句:虽然对象不同但是内容相同,输出true

实例5:

String str9 = "a1";
String str10 = "a" + 1;
System.out.println(str9 == str10);      //true

分析:当两个字符串常量连接时(相加)得到的新字符串依然是字符串常量且保存在常量池中。

实例6:

String str11 = "ab";
String str12 = "b";
String str13 = "a" + str12;
System.out.println(str11 == str13); //false

分析:当字符串常量与 String 类型变量连接时得到的新字符串不再保存在常量池中,而是在堆中新建一个 String 对象来存放,很明显常量池中要求的存放的是常量,有String类型变量当然不能存在常量池中了。str11 是字符串常量池中的对象,str13 是指向堆中的对象,不是同一个对象,所以输出false。

实例7:

String str14 = "ab";
final String str15 = "b";
String str16 = "a" + str15; 
System.out.println(str14 == str16);     //true

分析:字符串常量与 String 类型常量连接,得到的新字符串依然保存在常量池中。

实例8:

private static String getBB() {   
    return "b";   
}
String str17 = "ab";   
final String str18 = getBB();   
String str19 = "a" + str18;   
System.out.println(str17 == str19);     //false

分析:final String str18 = getBB()其实与final String str18 = new String(“b”)是一样的,也就是说 return “b” 会在堆中创建一个 String 对象保存 ”b”,虽然 str18 被定义成了 final,所以可见看见,并非定义为 final 的就保存在常量池中,很明显此处 str18 常量引用的 String 对象保存在堆中,因为 getBB() 得到的 String 已经保存在堆中了,final 的 String 引用并不会改变 String 已经保存在堆中这个事实。

实例9:

String str20 = "ab";
String str21 = "a";   
String str22 = "b";   
String str23 = str21 + str22;   
System.out.println(str23 == str20);         //false
System.out.println(str23.intern() == str20);        //true
System.out.println(str23 == str20.intern());        //false
System.out.println(str23.intern() == str20.intern());       //true

分析:而对于调用 intern 方法如果字符串常量池中已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定)则返回字符串常量池中的字符串,否则将此 String 对象添加到字符串常量池中,并返回此 String 对象的引用,所以str23.intern() == str20实质是常量比较返回 true,str23 == str20.intern()中 str23 就是上面说的堆中新对象,相当于一个新对象和一个常量比较,所以返回 false,str23.intern() == str20.intern() 就没啥说的了,指定相等。

实例10:

String s1 = "abc";
StringBuffer s2 = new StringBuffer(s1);
System.out.println(s1.equals(s2));  //false

补充:
equals源码:

@Override public boolean equals(Object other) {
        if (other == this) {
          return true;
        }
        if (other instanceof String) {
            String s = (String)other;
            int count = this.count;
            if (s.count != count) {
                return false;
            }
            // TODO: we want to avoid many boundchecks in the loop below
            // for long Strings until we have array equality intrinsic.
            // Bad benchmarks just push .equals without first getting a
            // hashCode hit (unlike real world use in a Hashtable). Filter
            // out these long strings here. When we get the array equality
            // intrinsic then remove this use of hashCode.
            if (hashCode() != s.hashCode()) {
                return false;
            }
            for (int i = 0; i < count; ++i) {
                if (charAt(i) != s.charAt(i)) {
                    return false;
                }
            }
            return true;
        } else {
            return false;
        }
    }

分析:instanceof运算符用法:左面的操作元是一个对象实例,右面是一个类.当 左面的对象是右面的类创建的对象时,该运算符运算的结果是true,否则是false(一个类的实例包括本身的实例,以及所有直接或间接子类的实例 )
我们再看StringBuffer 并不继承String类,所以到第五行判断不成立,直接返回了false。

总结:要玩明白 Java String 对象的核心其实就是玩明白字符串的堆栈和常量池,虚拟机为每个被装载的类型维护一个常量池,常量池就是该类型所用常量的一个有序集合,包括直接常量(String、Integer 和 Floating Point常量)和对其他类型、字段和方法的符号引用,池中的数据项就像数组一样是通过索引访问的;由于常量池存储了相应类型所用到的所有类型、字段和方法的符号引用,所以它在 Java 程序的动态链接中起着核心的作用。

参考资料:java字符串常量池
详解内存优化的来龙去脉

猜你喜欢

转载自blog.csdn.net/u013277209/article/details/76091200