• 疯狂的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

    总结

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

  • 相关阅读:
    ARP 协议
    天梯赛L1 题解
    DNS域名系统
    LeetCode 三角形最小路径和
    sql注入漏洞的利用
    XSS漏洞防御
    忘记密码功能漏洞挖掘
    sql bypass
    Web环境搭建组合
    常用数据库的总结
  • 原文地址:https://www.cnblogs.com/yungyu16/p/13198054.html
Copyright © 2020-2023  润新知