疯狂的String

本文转载自疯狂的String

导语

在java中字符串是我们比较常用的一个类型,字符串是不可变的,类被声明为final , 存储字符的char[] value数据也被声明为final ,我们对String真的了解么?我们看一下String是有多么的疯狂。本文中是在JDK8下面测试,不同的JDK可能会有不一样的结果。

测试一下

private static String B = "B";
  private static String K = "K";
  private static final String B1 = "B";
  private static final String K1 = "K";
  private static void demo1() {
      String s1 = "BK";
      String s2 = "BK";
      String emp = "";
      String s3 = "B" + "K";
      String s4 = "B" + emp + "K";
      String s5 = "B" + new String("K");
      String s6 = new String("BK");
      String s7 = s6.intern();
      String s8 = "B";
      String s9 = "K";
      String s10 = s8 + s9;
      String s11 = B + K;
      String s12 = B1 + K1;
      System.out.println("1 : s1 == s2 : " + (s1 == s2));
      System.out.println("2 : s1 == s3 : " + (s1 == s3));
      System.out.println("3 : s1 == s4 : " + (s1 == s4));
      System.out.println("4 : s1.equals(s4): " + s1.equals(s4));
      System.out.println("5 : s1 == s5 : " + (s1 == s5));
      System.out.println("6 : s1 == s10 : " + (s1 == s10));
      System.out.println("7 : s5 == s6 : " + (s5 == s6));
      System.out.println("8 : s1 == s7 : " + (s1 == s7));
      System.out.println("9 : s1 == s11 : " + (s1 == s11));
      System.out.println("10: s1 == s12 : " + (s1 == s12));
  }
public static void main(String[] args) {
  		demo1();
}

看到这里可以停下来想一下每一个输出的结果是什么?

收藏一下本文,回家在电脑上亲自试一下结果,结果可能出乎你的意料。

输出结果

1 : s1 == s2 : true
2 : s1 == s3 : true
3 : s1 == s4 : false
4 : s1.equals(s4): true
5 : s1 == s5 : false
6 : s1 == s10 : false
7 : s5 == s6 : false
8 : s1 == s7 : true
9 : s1 == s11 : false
10: s1 == s12 : true

看到结果可能中有些会和我们想象中的不一样,出乎你的意料,到现在头脑已经有些疯狂了,静下心来仔细想一下

为什么是这样的结果

  • 常量池中一般存放.class文件中的常量,主要包含 字面量 (如文本字符串、声明为final的常量值等)和符号引用量 (类和接口的全限定名、字段名称和描述符、方法名称和描述符)这些信息会存储在常量池中,这个常量池被称为静态常量池

  • 在类完成装载操作之后,在运行阶段也可以将新的常量放到池中,比如String的intern()方法就是这样的,这时候操作的常量池被称为动态常量池

  • 结果1. s1 == s2 : true
    对于这条输出应该不会有问题,”BK”是一个字符串常量,在编译阶段就会存放到静态常量池中比如存放地址为0x01,所以两个变量都指向常量池的同一个对象,比较它们的地址相等,结果是true

  • 结果2 : s1 == s3 : true
    s1的指向常量池中”BK”的内存地址0x01
    s3因为是两个常量相加,编译器会将其优化为s3="BK"是终指向的也地址0x01
    所以两个对象的地址也是相同的,结果为true

  • 结果3 : s1 == s4 : false
    s4因为连接的字符中存在一个变量emp引用类型所以不编译器不会对其进行优化,产生的对象不会被加入到字符串池中,而是在运行时在堆上创建一个新的对象s4值为”BK”,并将s4指向堆上对象的引用地址 0x02
    这时s1 的地址为0x01 s4的地址为0x02两个变量指向了不同的地址,所以返回结果是false

  • 结果4 : s1.equals(s4): true
    因为使用的是equals方法比较,所以首先比较两个对象地址是还相同,如果不相同,再去比较两个地址里面的内容是还相等,很显然,两个对象引用的地址不同,内容相同所以结果是true

  • 结果5 : s1 == s5 : false
    String s5 = "B" + new String("K");
    B是常量会在常量池,new操作这部分不是已知字面量,只能运行时才能确定结果,在编译器不优化的情况下,运行时会在堆上创建一个对象值为”BK”的对象, 同时让s5指各它的地址0x03
    s1的地址是0x01,所以比较两个对象的地址不是同一个结果 为false

  • 结果6 : s1 == s10 : false

  >  String s8 = "B";
  >  String s9 = "K";
  >  String s10 = s8 + s9;

在编译时`s8`,`s9`的字面量是确定的,所以在常量池中会有`B`和`K`,`s8`,`s9` 分别指向常量池的两个地址

s10赋值时,使用的是s8,s9两个变量,变量初始化时候是指向常量池,但是在运行时候指向什么地址,鬼才知道,所以在编译期是不可预料的,编译器是不做优化的,只有在运行时才会在堆中拼接B和K生成新对象在堆中,并将引用赋给s10,比如这时候分配的地址是0x04,这时候对比s1的地址0x01s10的地址0x04, 返回结果一定是false

  • 结果 7 : s5 == s6 : false
    s5和s6的赋值时,因为存在new对象,所以在编译其无法确定其字面量,只能在运行时才会确定,所以s5和s6都是堆上的两个对象,在比较两个对象的地址,一定是不相等的,所以结果一定是false

  • 结果8 : s1 == s7 : true
    String s7 = s6.intern();
    在运行到该行代码时,s6的值是确定的,然后调用intern方法,发现常量池中已经存在BK,所以s7指向常量池中的地址,在比较s1s7的值时,返回结果为 true

  • 结果9 : s1 == s11 : false
    String s11 = B + K;
    BK是静态变量,在编译期是无法确定字面量,所以只能在运行时才能确定其真实值,所以s11指向的是堆上的一个地址,在比较s1s11时候,返回的结果为false

  • 结果10: s1 == s12 : true

    String s12 = B1 + K1;
    因为B1K1static final修饰对于static final类型,在类加载的准备阶段就会被赋上正确的值,因为static final类型被认为是常量,两个常量相加之后的值也是常量,字面量是确定的,这时候 BK在常量池中已经存在,所以s12也是指向常量池中的地址,在比较s1s12的地址返回的结果是true

总结

按照下面的规则来判断,不会被String搞迷路

  • 变量在定义时如果存在new String()非static final修饰的变量进行+运算,都只能在运行时才能确定结果,所产生的对象一定是在堆上面
  • 如果一定变量在定义时字面量已经确定,会在常量池中创建,并且变量指向常量池中的地址
  • 在编译期可以确定的常量才会被放入常量池,在运行时的变量,如果不调用intern方法是不会把常量添加到常量池中的
  • statci final修饰的变量在准备阶段已经确定正确的值,会被认为是常量,存放在常量池中

再来一发

/**
   * 比如我们玩游戏时候经常用的QWER四个键,可以组合出不同的操作
   */
  private static void demo2() throws NoSuchFieldException, IllegalAccessException {
      //定义操作A QWER
      String operateA = "QWER";
      //获取字符串对象中存储字符的value字段  private final char value[];
      Field valueFieldString = String.class.getDeclaredField("value");
      valueFieldString.setAccessible(true);
      //获取value数组中的值 [Q,W,E,R]
      char[] value = (char[]) valueFieldString.get(operateA);
      //将value数组的值改为 [Q,Q,Q,Q]
      value[1] = 'Q';
      value[2] = 'Q';
      value[3] = 'Q';
      //定义操作B和操作A一样 QWER
      String operateB = "QWER";
      System.out.println("1.operateA :" + operateA);
      System.out.println("2.operateB :" + operateB);
      System.out.println("3.operateA == operateB :" + (operateA == operateB));
      System.out.println("4.\"QWER\" == operateB :" + ("QWER" == operateB));
      System.out.println("5.\"QQQQ\" == operateA : " + ("QQQQ" == operateA));
      System.out.println("6.operateA.equals(\"QQQQ\") : " + operateA.equals("QQQQ"));
      System.out.println("7.operateA.equals(\"QWER\") : " + operateA.equals("QWER"));
      System.out.println("8.\"QWER\".equals(\"QQQQ\") : " + "QWER".equals("QQQQ"));
  }

输出结果

1.operateA :QQQQ
2.operateB :QQQQ
3.operateA == operateB :true
4."QWER" == operateB :true
5."QQQQ" == operateA : false
6.operateA.equals("QQQQ") : true
7.operateA.equals("QWER") : true
8."QWER".equals("QQQQ") : true

为什么会输出这样的结果

图片.png

没错,这结果简直让人抓狂,太离谱了,

6.skillA.equals("QQQQ") : true
7.skillA.equals("QWER") : true
8."QWER".equals("QQQQ") : true

凭直觉大多数人会认为6 和 7 应该是一个对一个错,8应该是false,可这结果结果倒底怎么了,刚看到这结果感觉很惊讶what a fuck !

代码逻辑

  1. 首先我们先定义一个操作A QWER,
  2. 对A底层的字符数组进行修改,修改为QQQQ(直接对底层数据修改,直接改的地址里面存放的内容,而不是通过String运算符修改)
  3. 再定义一个操作B,同样为QWER
  4. 然后进行各种比较,判断输出内容

分析

编译阶段搞的事情

1、由于QWER在编译阶段是一个字面量,所以QWER在常量池中分配空间0x01,并存储

2、operateA指向常量池中QWER所在的地址0x01

3、operateB的字面量也是QWER,这时候常量池中也存在,引用直接指向地址0x01

最终的结果是operateAoperateB指向了同一个地址0x01 ,字面量为QWER的地址是0x01

字面量为QQQQ的变量指向了0x05的地址

img

运行阶段搞的事情

  1. 读取operateA的值,然后通过反射获取到字符存储数据的char[]数组value

  2. 将value里面的内容个性为QQQQ

String类equals方法的代码

   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;
   }

结果分析

接下来就是进行各种比较了,在看结果之间先看一下String equals方法的逻辑

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;
}

先判断对象的地址是不是同一个,如果指向同一个地址,那么就认为两个对象相等

如果指向的地址不相等,然后判断长度是还相等,如果长度不相等,则返回false

如果地址不等,长度相等的话,就取出地址中的值,逐位进行比较,如果有一位不相等则返回false ,否则返回 true

接下来我们逐个看一下结果

  • 1.operateA :QQQQ
    ​ 在运行到该行代码时候,地址中的值已经被修改了,所以operateA的值为QQQQ

  • 2.operateB :QQQQ
    operateB和operateA指向了同一个引用,在运行到该行代码时候,地址中的值已经是QQQQ了 ,所以operateB的值为QQQQ

  • 3.operateA == operateB :true
    因为operateA和operateB的指向的地址都是0x01所以比较两个对象的地址值是true

  • 4.”QWER” == operateB :true
    “QWER”这个匿名变量的字面量是个常量,并且在常量池中已经存在,所以指向常量池的0x01地址,operateB的地址也是0x01所以比较两个对象的地址值是true

  • 5.”QQQQ” == operateA : false
    “QQQQ”这个匿名变量的字面量是个常量,在常量池中不存在,所以会被加入到常量池中地址为 0x05,operateA的地址也是0x01所以比较两个对象的地址值是false

  • 6.operateA.equals(“QQQQ”) : true
    operateA指向的内存地址是0x01,但是值是QQQQ
    “QQQQ”指向的内存地址是0x05,值为QQQQ

    在对比equals方法时,先比较两个对象的地址值是false,然后再去比较两个地址中的值是否相等,因为0x01地址中的值是QQQQ与0x05地址的值QQQQ的值相等,所以结果是 true

  • 7.operateA.equals(“QWER”) : true
    “QWER” 指向的内存地址是0x01,值是QQQQ
    operateA指向的内存地址是0x01,值是QQQQ
    在对比equals方法时,先比较两个对象的地址值是false,然后再去比较两个地址中的值是否相等,因为0x01地址中的值已经是QQQQ与0x05的值相等,所以结果是 true

  • 8.”QWER”.equals(“QQQQ”) : true
    “QWER”指向的内存地址是0x01,值是QQQQ
    “QQQQ”指向的内存地址是0x05,值为QQQQ
    在对比equals方法时,先比较两个对象的地址值是false,然后再去比较两个地址中的值是否相等,因为0x01地址中的值是QQQQ与0x05地址的值QQQQ的值相等,所以结果是 true

总结

其实这个示例中,主要是直接操作了底层的数组,破坏了字符串的不变性,才会出现这么奇怪的现象。

猜你喜欢

转载自www.cnblogs.com/yungyu16/p/13198054.html