zoukankan      html  css  js  c++  java
  • String全面解析

    前言

    public class Test {
        public static void main(String[] args) {
            String a = "abc";
            String b = "abc";
            String c = new String("abc");
            System.out.println(a==b);
            System.out.println(a.equals(b));
            System.out.println(a==c);
            System.out.println(a.equals(c));
        }
    }

    那么上段代码的结果是什么呢?答案是:true true false true,有初学java的朋友肯定会纳闷,a==c为什么会是false呢?equals判断的为什么都是true呢?

    根据这些问题,我们就通过对String的解读来一步一步的了解。

    为什么a==c的结果是false

    明白这个问题需要对JVM的内存结构有一定的了解,说是了解也不需要太多,能够get到下图的知识点就行了。

    ps:本文中所有的图示均是为了方便理解,画出来的大致样子,如果想要了解的更加清楚,请自行研究虚拟机原理。

    java语法设计的时候针对String,提供了两种创建方式和一种特殊的存储机制(String intern pool )。

    两种创建字符串对象的方式:

    1. 字面值的方式赋值
    2. new关键字新建一个字符串对象

    这两种方法在性能和内存占用方面存在这差异

    String Pool串池:是在内存堆中专门划分一块空间,用来保存所有String对象数据,当构造一个新字符串String对象时(通过字面量赋值的方法),Java编译机制会优先在这个池子里查找是否已经存在能满足需要的String对象,如果有的话就直接返回该对象的地址引用(没有的话就正常的构造一个新对象,丢进去存起来),这样下次再使用同一个String的时候,就可以直接从串池中取,不需要再次创建对象,也就避免了很多不必要的空间开销。

    根据以上的概念,我们再来看前言中的代码,当JVM执行到String a = "abc";的时候,会先看常量池里有没有字符串刚好是“abc”这个对象,如果没有,在常量池里创建初始化该对象,并把引用指向它,如下图。

    当执行到String b = "abc";时,发现常量池已经有了abc这个值,于是不再在常量池中创建这个对象,而是把引用直接指向了该对象,如下图:

    继续执行到 String c = new String("abc");这时候我们加了一个new关键字,这个关键字呢就是告诉JVM,你直接在堆内存里给我开辟一块新的内存,如下图所示:

    这时候我们执行四个打印语句,我们需要知道==比较的是地址,equals比较的是内容(String中的重写过了),abc三个变量的内容完全一样,因此equals的结果都是true,ab是一个同一个对象,因此地址一样,a和c很显然不是同一个对象,那么此时为false也是很好理解的。

    String相关源码

    在本文中只有String的部分源码,毕竟String的源码有3000多行,全部来写进来不那么现实,我们挑一些比较有意思的代码来做一定的分析说明。

    属性

    我们先来看一下String都有哪些成员变量,比较关键的属性有两个,如下:

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        char数组
        private final char value[];
        /** Cache the hash code for the string */
        private int hash; // Default to 0
    

    从源码中我们能够看到,在String类中声明了一个char[]数组,变量名value,声明了一个int类型的变量hash(该String对象的哈希值的缓存)。也就是说java中的String类其实就是对char数组的封装。

    构造方法

    接下来我们通过一句代码来了解一下字符串创建的过程,String c = new String("abc");我们知道使用new关键字就会使用到构造方法,所以如下。

    public String(String original) {
            this.value = original.value;
            this.hash = original.hash;
        }
    

    构造方法中的代码非常简单,把传进来的字符串的value值,也就是char数组赋值给当前对象,hash同样处理,那么问题来了WTF original?

    在这里需要注意的是java中的一个机制,在Java中,当值被双引号引起来(如本示例中的"abc"),JVM会去先检查看一看常量池里有没有abc这个对象,如果没有,把abc初始化为对象放入常量池,如果有,直接返回常量池内容。所以也就是说在没有“abc”的基础上,执行代码会在串池中创建一个abc,也会在堆内存中再new出来一个。最终的结果如下图:

    String4

    那么这时候如果再有一个String c2 = new String("abc");呢?如图

    String5

    关于这一点我们通过IDEA的debug功能也能够看到,你会发现,c和c2其中的char数组的地址是相同的。足以说明在创建c和c2的时候使用的是同一个数组。

    equals方法

    public boolean equals(Object anObject) {
         //如果两个对象是同一个引用,那么直接返回true
            if (this == anObject) {
                return true;
            }
         /*
         1.判断传入的对象是不是String类型
         2.判断两个对象的char数组长度是否一致
         3.循环判断char数组中的每一个值是否相等
         以上条件均满足才会返回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不可变?

    串池需要

    为什么说是串池需要呢?在开篇的时候我们提到过,串池中的字符串会被多个变量引用,这样的机制让字符串对象得到了复用,避免了很多不必要的内存消耗。

    那么大家试想一下,如果String对象本身允许二次修改的话,我有一个字符串“abc”同时被100个变量引用,其中一个引用修改了String对象,那么将会影响到其他99个引用该对象的变量,这样会对其他变量造成不可控的影响。

    不可变性的优点

    安全性

    字符串不可变安全性的考虑处于两个方面,数据安全和线程安全。

    数据安全,大家可以回忆一下,我们都在哪些地方大量的使用了字符串?网络数据传输,文件IO等,也就是说当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定的损失。

    线程安全,因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享,这样便不用因为线程安全问题而使用同步。

    性能效率

    关于性能效率一方面是复用,另一方面呢需要从hash值的缓存方向来说起了。

    String的Hash值在很多的地方都会被使用到,如果保证了String的不可变性,也就能够保证Hash值始终也是不可变的,这样就不需要在每次使用的时候重新计算hash值了。

    String不可变性是如何实现的?

    通过对属性私有化,final修饰,同时没有提供公开的get set方法以及其他的能够修改属性的方法,保证了在创建之后不会被从外部修改。

    同时不能忘了,String也是被final修饰的,在之前的文章中我们提到过,final修饰类的结果是String类没有子类。

    那么String真的不能改变吗?不是,通过反射我们可以,代码如下:

    String c = new String("abc");
    System.out.println(c);
    //获取String类中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");
    
    //改变value属性的访问权限
    valueFieldOfString.setAccessible(true);
    
    //获取s对象上的value属性的值
    char[] value = (char[]) valueFieldOfString.get(c);
    
    //改变value所引用的数组中的第5个字符
    value[1] = '_';
    System.out.println(c);
    执行的结果是
    
    abc
    a_c
    

    也就是说我们改变了字符串对象的值,有什么意义呢?没什么意义,我们从来不会这么做。

    其他问题

    不是特别需要请不要使用new关键字创建字符串

    从前文我们知道使用new关键字创建String的时候,即便串池中存在相同String,仍然会再次在堆内存中创建对象,会浪费内存,另一方面对象的创建相较于从串池中取效率也更低下。

    String StringBuffer StringBuilder的区别

    关于三者的区别,在面试题中经常的出现,String对象不可变,因此在进行任何内容上的修改时都会创建新的字符串对象,一旦修改操作太多就会造成大量的资源浪费。

    StringBuffer和StringBuilder在进行字符串拼接的时候不会创建新的对象,而是在原对象上修改,不同之处在于StringBuffer线程安全,StringBuilder线程不安全。所以在进行字符串拼接的时候推荐使用StringBuffer或者StringBuilder。

      

      

      

      

  • 相关阅读:
    1021. 从前有座山——java
    1004. 西西弗斯式的命运——java
    1025. 水枪灭火——java
    1020. 分解质因数——java
    1024.排序——C语言
    1024. 排序——java
    1022. Fib数列——java
    1006. 求和游戏——java
    1005. 数独——java
    SpringBoot 升级到 2.1 后,启动程序时控制台不打印 API 的解决方法及一些感想
  • 原文地址:https://www.cnblogs.com/ganchuanpu/p/6067814.html
Copyright © 2011-2022 走看看