String类的深入理解(未完待续)

  String不是基本数据类型,String和8种包装类型是不可变类。String和8种基本数据类型采用值传递。

 

0.不可变类的设计原则

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];//数组是引用传递
    /** The offset is the first index of the storage that is used. */
    private final int offset;
    /** The count is the number of characters in the String. */
    private final int count;
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    ....
    public String(char value[]) {
         this.value = Arrays.copyOf(value, value.length); // deep copy操作
     }
    ...
     public char[] toCharArray() {
     // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }
    ...
}

如上代码所示,可以观察到以下设计细节:

  1. String类被final修饰,不可继承
  2. string内部所有成员都设置为私有变量
  3. 不存在value的setter
  4. 并将value和offset设置为final。
  5. 当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量.
  6. 获取value时不是直接返回对象引用,而是返回对象的copy.

这都符合上面总结的不变类型的特性,也保证了String类型是不可变的类。

例如:

package cn.qlq.test;

public class ArrayTest {
    public static void main(String[] args) {
        String str = "x1x1";
        str.replace("1", "2");
        System.out.println(str);// x1x1

        str = str.replace("1", "2");
        System.out.println(str);// x2x2
    }
}

1.创建过程与字符串拼接过程

1.创建过程研究

例如:

package cn.qlq.test;

public class ArrayTest {
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";
        String str3 = new String("abc");
        String str4 = new String("abc");
    }
}

  String s1 = new String("abc"); 是在堆中创建一个String对象,并检查常量池中是否有字面量为"abc"的常量,没有的话在常量区创建"abc"并将堆中的对象指向该常量,有的话堆中的对象直接指向"1";

  String s2 = new String("abc"); 又在堆中创建一个String对象,并将s2指向该对象,其字面量"abc"在前面已经创建,所以不会再创建常量区中创建字符串;

  

  String s3 = "abc";   检查常量池中有没有字面量为"abc"的字符串,如果没有则创建并将s3指向该常量;有的话直接指向该该常量;

  String s4 = "abc"  的时候常量池已经有abc,所以不会再创建对象,也就是s3与s4指向同一个对象。

所以我们可以用下面图解解释,String s = new String("xxx")在检查常量池的时候会涉及到堆中创建对象;String s = "x"直接检查常量池,不会涉及堆。

如下图解:

 

一道经典的面试题:new String("abc")创建几个对象?

  简单的回答是一个或者两个,如果是常量区有值为"abc"的值,则只在堆中创建一个对象;如果常量区没有则会在常量区创建"abc",此处的常量区是方法区的运行时常量池(也称为动态常量区)。

   我们需要明白只要是new都会在堆中创建对象。直接String s = "xxx"不会涉及堆,只在常量区检查是否有该常量。

反编译查看编译后的信息:  

package cn.qlq.test;

public class ArrayTest {
    public static void main(String[] args) {
        String str1 = "abc";
        String str2 = "abc";
        String str3 = new String("abc");
        String str4 = new String("abc");
    }
}

编译并且查看反编译信息:

C:\Users\liqiang\Desktop>javap -c -verbose ArrayTest.class
Classfile /C:/Users/liqiang/Desktop/ArrayTest.class
  Last modified 2018-9-2; size 383 bytes
  MD5 checksum 0ab23a2d60142821a621d4d345b50622
  Compiled from "ArrayTest.java"
public class cn.qlq.test.ArrayTest
  SourceFile: "ArrayTest.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         //  java/lang/Object."<init>":()V
   #2 = String             #16            //  abc
   #3 = Class              #17            //  java/lang/String
   #4 = Methodref          #3.#18         //  java/lang/String."<init>":(Ljava/lang/String;)V
   #5 = Class              #19            //  cn/qlq/test/ArrayTest
   #6 = Class              #20            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               ArrayTest.java
  #15 = NameAndType        #7:#8          //  "<init>":()V
  #16 = Utf8               abc
  #17 = Utf8               java/lang/String
  #18 = NameAndType        #7:#21         //  "<init>":(Ljava/lang/String;)V
  #19 = Utf8               cn/qlq/test/ArrayTest
  #20 = Utf8               java/lang/Object
  #21 = Utf8               (Ljava/lang/String;)V
{
  public cn.qlq.test.ArrayTest();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: ldc           #2                  // String abc
         2: astore_1
         3: ldc           #2                  // String abc
         5: astore_2
         6: new           #3                  // class java/lang/String
         9: dup
        10: ldc           #2                  // String abc
        12: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        15: astore_3
        16: new           #3                  // class java/lang/String
        19: dup
        20: ldc           #2                  // String abc
        22: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        25: astore        4
        27: return
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 6
        line 8: 16
        line 9: 27
}

上面的Constant pool:是所设计的常量信息,包括类名字、方法名字、字符串常量池信息信息。

下面就是编译之后的方法:

第一个构造方法研究:

  public cn.qlq.test.ArrayTest();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

编译器给我们生成的无参构造方法,访问类型是public,

aload_0                     将第一个引用类型本地变量推送至栈顶(将this引用推送至栈顶,即压入栈。)

 invokespecial #1                  // Method java/lang/Object."<init>":()V      调用Object的<init>(构造方法)

return  函数结束(返回类型是void)

第二个main方法研究:

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: ldc           #2                  // String abc
         2: astore_1
         3: ldc           #2                  // String abc
         5: astore_2
         6: new           #3                  // class java/lang/String
         9: dup
        10: ldc           #2                  // String abc
        12: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        15: astore_3
        16: new           #3                  // class java/lang/String
        19: dup
        20: ldc           #2                  // String abc
        22: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
        25: astore        4
        27: return
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 6
        line 8: 16
        line 9: 27

访问标志符号是static、public类型

ldc:  该系列命令负责把数值常量或String常量值从常量池中推送至栈顶。该命令后面需要给一个表示常量在常量池中位置(编号)的参数(#2代表上面标记为#2的常量)

astore_1                  将栈顶引用型数值存入指定第二个本地变量,超过3的格式变为  astore 4  此种格式

new  代表创建对象

invokespecial   代表调用方法

return   代表函数结束,返回类型是void

  然后对着命令自己去查去吧。。。。。。。。

2.拼接过程研究

2.Intern()方法详解

 

3.equals()和hashCode方法详解

hashCode()源码查看:

public int hashCode() {
        int h = hash;//默认为0
        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;
    } 

看出来String是遍历每个char,h乘以31加上对应char的ASCII码。

验证:

        String s1 = "a";
        String s2 = "b";
        System.out.println(s1.hashCode());//97
        System.out.println(s2.hashCode());//98

equals(obj)源码查看:  是将形参转变为String,然后遍历里面的char[],两个char[]进行依次对比。也就是比较字符串的值是否相等。

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String) anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                            return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

我们利用两个hashCode相等的字符串作为key存入map,查看:

package cn.qlq.test;

import java.util.HashMap;

public class ArrayTest {
    public static void main(String[] args) {
        String s1 = "Aa";
        String s2 = "BB";
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());

        HashMap map = new HashMap();
        map.put(s1, "xxx");
        map.put(s2, "xxxdddd");
        System.out.println(map);
    }

}

2112
2112
{BB=xxxdddd, Aa=xxx}

"Aa" 与"BB"的hashCode相等,那么是如何存入map的?--验证hashmap的实现原理基于数据+链表

     

  先存入Aa,并放在第五个数组位置,当存BB的时候发现hashCode一样,会将BB存到第五个位置,并将第五个位置元素的next(也是一个Entry)存为Aa。也就是数组加链表实现原理

4..String引用传值图解(由于是不可变类,所以给形参赋值的时候相当于新建对象,不会影响实参)

更进一步的理解:"引用传值也是按值传递,只不过传的是对象的地址"。

比如下面一段代码:

package cn.qlq.test;

import java.util.Arrays;

public class ArrayTest {
    public static void main(String[] args) {
        String s = "hello";        
        test(s);
        System.out.println(s);
    }

    public static void test(String s1) {
        s1 = "world";
    }
}

结果:

hello

解释:调用test方法的时候采用引用传递(将s的地址传下去),执行s1="world"是新创一个"world"并赋值给s1,也就是s1此时已经指向其他对象,不再与s指向相同对象。

图解:

  

 

 

 

 

 

 

 

 

 

 

 

 

猜你喜欢

转载自www.cnblogs.com/qlqwjy/p/9576320.html