资讯专栏INFORMATION COLUMN

Java杂记17—String全面解析

jeffrey_up / 3086人阅读

摘要:所以也就是说在没有的基础上,执行代码会在串池中创建一个,也会在堆内存中再出来一个。不可变性的优点安全性字符串不可变安全性的考虑处于两个方面,数据安全和线程安全。

摘要: String基本特性,String源码,为什么String不可变?

前言

基于字符串String在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 )。

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

字面值的方式赋值

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, 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出来一个。最终的结果如下图:

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

关于这一点我们通过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。

我不能保证每一个地方都是对的,但是可以保证每一句话,每一行代码都是经过推敲和斟酌的。希望每一篇文章背后都是自己追求纯粹技术人生的态度。

永远相信美好的事情即将发生。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69575.html

相关文章

  • Java杂记9—NIO

    摘要:前言非阻塞,也被称之为新,它重新定义了一些概念。缓冲通道通道选择器阻塞,几乎所有的程序员都会的字节流,字符流,输入流,输出流等分类就是针对而言的。缓冲区基本属性容量表示最大数据容量,缓冲区容量不能为负,并且创建后不能更改。 前言 非阻塞IO,也被称之为新IO,它重新定义了一些概念。 缓冲buffer 通道 channel 通道选择器 BIO 阻塞IO,几乎所有的java程序员都会的...

    tainzhi 评论0 收藏0
  • webpack系列-插件机制杂记

    摘要:系列文章系列第一篇基础杂记系列第二篇插件机制杂记系列第三篇流程杂记前言本身并不难,他所完成的各种复杂炫酷的功能都依赖于他的插件机制。的插件机制依赖于一个核心的库,。是什么是一个类似于的的库主要是控制钩子函数的发布与订阅。 系列文章 Webpack系列-第一篇基础杂记 Webpack系列-第二篇插件机制杂记 Webpack系列-第三篇流程杂记 前言 webpack本身并不难,他所完成...

    Neilyo 评论0 收藏0
  • 笃行杂记之Zookeeper SessionTimeOut分析

    摘要:前言本文为笃行日常工作记录,烂笔头系列。最终通过分析源码了解到最终的确定是一个协商的过程,而不是简单的配置生效。根据客户端上报的和服务端自身的。如果上报的小于则设置为如果上报的大于则设置为如果介于两则之间,则以上报的时间为准。 0.前言 本文为笃行日常工作记录,烂笔头系列。 源码前面,了无秘密 — by 侯杰 近期的一个C++项目里使用了Zookeeper做服务发现,期间遇到了Sess...

    mcterry 评论0 收藏0
  • Java杂记3—流程控制之条件

    摘要:流程控制首先来介绍程序的流程控制。后面跟一个代码块逻辑关系是当布尔表达式为真的时候执行代码块,为假的时候不执行。 流程控制 首先来介绍程序的流程控制。 所谓的流程控制,就是在我们前面的课程中我们已经学过了变量,数据类型,运算符,表达式,这些都是计算机编程的基本元素,但是我们程序的基本执行单元应该是语句,程序执行,执行的不是表达式,而是执行语句。就好像我们小时候先学认字儿,再学组词,但最...

    jackzou 评论0 收藏0
  • JavaScript 杂记(一)

    摘要:基础简单基础数据类型值得注意的是的值是,表示一个空对象指针,没有指向任何对象。的值是,表示申明变量或对象的属性却未初始化。值是派生自的,所以对他们执行相等测试会返回。字符串单引号和双引号都可以用来表示字符串,只要前后一致即可。 目的 记忆总是会随着时间而淡化,学习了点什么,就记录下点什么。 做点什么,总比不做要好。 基础 简单(基础)数据类型 Number ...

    raise_yang 评论0 收藏0

发表评论

0条评论

jeffrey_up

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<