百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

真没想到,一个小小的String居然还有这么多窍门?

liebian365 2024-10-16 13:08 23 浏览 0 评论

推荐学习

真没想到,一个小小的String居然还有这么多窍门?

1. 看看源码

大家都知道, String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)。我们先来看看 String 的源码。

在 Java 8 中,String 内部使用 char 数组存储数据。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}

value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

2. 不可变有什么好处呢

2.1 可以缓存 hash 值

因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。

2.2 String Pool 的使用

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

2.3 安全性

String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。

2.4 线程安全

String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

3. 再来深入了解一下 String

3.1 “+” 连接符

字符串对象可以使用“+”连接其他对象,其中字符串连接是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的,对象转换为字符串是通过 toString 方法实现的。可以通过反编译验证一下:

/**
 * 测试代码
 */
public class Test {
    public static void main(String[] args) {
        int i = 10;
        String s = "abc";
        System.out.println(s + i);
    }
}

/**
 * 反编译后
 */
public class Test {
    public static void main(String args[]) {    //删除了默认构造函数和字节码
        byte byte0 = 10;      
        String s = "abc";      
        System.out.println((new StringBuilder()).append(s).append(byte0).toString());
    }
}

由上可以看出,Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象,并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。那这个 “+” 的效率怎么样呢?

3.2 “+”连接符的效率

使用“+”连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。比如:

String s = "abc";
for (int i=0; i<10000; i++) {
    s += "abc";
}

这样由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用append()方法手动拼接(如上面例子如果使用手动拼接运行时间将缩小到1/200左右)。

与此之外还有一种特殊情况,也就是当"+"两端均为编译期确定的字符串常量时,编译器会进行相应的优化,直接将两个字符串常量拼接好,例如

System.out.println("Hello" + "World"); /** * 反编译后 */ System.out.println("HelloWorld");

4. 字符串常量

4.1 为什么使用字符串常量?

JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。

4.2 实现字符串常量池的基础

实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享。

运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收。

我们来看个小例子,了解下不同的方式创建的字符串在内存中的位置:

String string1 = "abc";   // 常量池
String string2 = "abc";     // 常量池
String string3 = new String("abc");  // 堆内存

5. String类常见的面试题

5.1 判断字符串s1和s2是否相等

public static void main(String[] args) {
    String s1 = "123";
    String s2 = "123";
    String s3 = "1234";
    String s4 = "12" + "34";
    String s5 = s1 + "4";
    String s6 = new String("1234");
    System.out.println(s1 == s2);   // true
    System.out.println(s1.equals(s2));  //true
    System.out.println(s3 == s4);   //true
    System.out.println(s3 == s5);   // false
    System.out.println(s3.equals(s5)); //true
    System.out.println(s3 == s6);   // false
}

解析:

  • s1和s2:

String s1 = "123";先是在字符串常量池创建了一个字符串常量“123”,“123”常量是有地址值,地址值赋值给s1。接着声明 String s2=“123”,由于s1已经在方法区的常量池创建字符串常量"123",进入常量池规则:如果常量池中没有这个常量,就创建一个,如果有就不再创建了,故直接把常量"123"的地址值赋值给s2,所以s1==s2为true。

由于String类重写了equals方法,s1.equals(s2)比较的是字符串的内容,s1和s2的内容都是"123",故s1.equals(s2)为true。

  • s3和s4:

s3创建了一个新的字符串"1234",s4是两个新的字符串"12"和"34"通过"+“符号连接所得,根据Java中常量优化机制, “12” 和"34"两个字符串常量在编译期就连接创建了字符串"1234”,由于字符串"1234"在常量池中存在,故直接把"1234"在常量池的地址赋值给s4,所以s3==s4为true。

  • s3和s5:

s5是由一个变量s1连接一个新的字符串"4",首先会在常量池创建字符串"4",然后进行"+“操作,根据字符串的串联规则,s5会在堆内存中创建StringBuilder(或StringBuffer)对象,通过append方法拼接s1和字符串常量"4”,此时拼接成的字符串"1234"是StringBuilder(或StringBuffer)类型的对象,通过调用toString方法转成String对象"1234",所以s5此时实际指向的是堆内存中的"1234"对象,堆内存中对象的地址和常量池中对象的地址不一致,故s3==s5为false。

看下JDK8的API文档里的解释:

Java语言为字符串连接运算符(+)提供特殊支持,并为其他对象转换为字符串。字符串连接是通过StringBuilder (或StringBuffer )类及其append方法实现的。字符串转换是通过方法来实现toString,由下式定义0bject和继承由在Java中的所有类。有关字符串连接和转换的其他信息,请参阅Gosling,Joy 和Steele,Java 语言规范。

不管是常量池还是堆,只要是使用equals比较字符串,都是比较字符串的内容,所以s3.equals(s5)为true。

Java常量优化机制:给一个变量赋值,如果等于号的右边是常量,并且没有一个变量,那么就会在编译阶段计算该表达式的结果,然后判断该表达式的结果是否在左边类型所表示范围内,如果在,那么就赋值成功,如果不在,那么就赋值失败。但是注意如果一旦有变量参与表达式,那么就不会有编译期间的常量优化机制。

  • s3和s6:

String s6 = new String("1234");在堆内存创建一个字符串对象,s6指向这个堆内存的对象地址,而s3指向的是字符串常量池的"1234"对象的地址,故s3==s6为false。

5.2 创建多少个字符串对象?

String s0 = "123";
String s1 = new String("123"); 
String s2 = new String("1" + "2");
String s3 = new String("12") + "3";

解析:

  • String s0 = “123”;

字符串常量池对象:“123”,1个;

共1个。

  • String s1 = new String(“123”);

字符串常量池对象:“123”,1个;

堆对象:new String(“123”),1个;

共2个。

  • String s2 = new String(“1” + “2”);

字符串常量池对象:“12”,1个(Jvm在编译期做了优化,“1” + "2"合并成了 “12”);

堆对象:new String(“12”),1个

共2个。

由于s2涉及字符串合并,我们通过命令看下字节码信息:

javac StrTest.java  //编译源文件得到class文件
javap -c StrTest.class  // 查看编译结果

得到字节码信息如下:

备注:以上编译结果基于Jdk1.8运行环境

我们可以很清晰看到,创建了一个新的String对象和一个字符串常量"12",new String("1" + "2") 相当于 new String("12"),共创建了2个字符串对象。

  • String s3 = new String(“12”) + “3”;

字符串常量池对象:“12”、“3”,2个,

堆对象: new Stringbuilder().append(“12”).append(“3”).toString();转成String对象,1个;

共3个。

我们同样看下编译后的结果:

可以看到,包括StringBuilder在内,共创建了4个对象,字符串"12"和字符串"3"是分开创建的,所以共创建了3个字符串对象。

总结:

new String()是在堆内存创建新的字符串对象,其构造参数中可传入字符串,此字符串一般会在常量池中先创建出来,new String()创建的字符串是参数字符串的副本,看下API中关于String构造器的解释:

String(String original)
初始化新创建的String对象,使其表示与参数相同的字符序列;换句话说,新创建的字符串是参数字符串的副本。

所以new String()的方式创建字符串百分百会产生一个新的字符串对象,而类似于"123"这样的字符串对象则需要在创建之前看常量池中有没有,有的话就不创建,没有则创建新的对象。 "+"操作符连接字符串常量的时候会在编译期直接生成连接后的字符串,若该字符串在常量池已经存在,则不会创建新的字符串;连接变量的话则涉及StringBuilder等字符串构建器的创建,会在堆内存生成新的字符串对象。

以上就是我们给您带来的关于Java字符串的一些知识总结和面试技巧,你学废了吗?

作者:xqnode

原文链接:https://blog.csdn.net/xqnode/article/details/106663571

相关推荐

精品博文嵌入式6410中蓝牙的使用

BluetoothUSB适配器拥有一个BluetoothCSR芯片组,并使用USB传输器来传输HCI数据分组。因此,LinuxUSB层、BlueZUSB传输器驱动程序以及B...

win10跟这台计算机连接的前一个usb设备工作不正常怎么办?

前几天小编闲来无事就跑到网站底下查看粉丝朋友给小编我留言询问的问题,还真的就给小编看到一个问题,那就是win10跟这台计算机连接的一个usb设备运行不正常怎么办,其实这个问题的解决方法时十分简单的,接...

制作成本上千元的键盘,厉害在哪?

这是稚晖君亲自写的开源资料!下方超长超详细教程预警!!全文导航:项目简介、项目原理说明、硬件说明、软件说明项目简介瀚文智能键盘是一把我为自己设计的——多功能、模块化机械键盘。键盘使用模块化设计。左侧的...

E-Marker芯片,USB数据线的“性能中枢”?

根据线缆行业的研究数据,在2019年搭载Type-C接口的设备出货量已达到20亿台,其中80%的笔记本电脑和台式电脑采用Type-C接口,50%的智能手机和平板电脑也使用Type-C接口。我们都知道,...

ZQWL-USBCANFD二次开发通讯协议V1.04

修订历史:1.功能介绍1.1型号说明本文档适用以下型号:  ZQWL-CAN(FD)系列产品,USB通讯采用CDC类实现,可以在PC机上虚拟出一个串口,串口参数N,8,1格式,波特率可以根据需要设置(...

win10系统无法识别usb设备怎么办(win10不能识别usb)

从驱动入手,那么win10系统无法识别usb设备怎么办呢?今天就为大家分享win10系统无法识别usb设备的解决方法。1、右键选择设备管理器,如图:  2、点击更新驱动程序,如图:  3、选择浏览...

微软七月Win8.1可选补丁有内涵,含大量修复

IT之家(www.ithome.com):微软七月Win8.1可选补丁有内涵,含大量修复昨日,微软如期为Win7、Win8.1发布7月份安全更新,累计为6枚安全补丁,分别修复总计29枚安全漏洞,其中2...

如何从零开始做一个 USB 键盘?(怎么制作usb)

分两种情况:1、做一个真正的USB键盘,这种设计基本上不涉及大量的软件编码。2、做一个模拟的USB键盘,实际上可以没有按键功能,这种的需要考虑大量的软件编码,实际上是一个单片机。第一种设计:买现成的U...

电脑识别U盘失败?5个实用小技巧,让你轻松搞定USB识别难题

电脑识别U盘失败?5个实用小技巧,让你轻松搞定USB识别难题注意:有些方法会清除USB设备里的数据,请谨慎操作,如果不想丢失数据,可以先连接到其他电脑,看能否将数据复制出来,或者用一些数据恢复软件去扫...

未知usb设备设备描述符请求失败怎么解决

出现未知daousb设备设备描述符请求失du败解决办zhi法如下:1、按下Windows+R打开【运行】;2、在版本运行的权限输入框中输入:services.msc按下回车键打开【服务】;2、在服务...

读《飘》47章20(飘每章概括)

AndAhwouldn'tleaveMissEllen'sgrandchildrenfornotrashystep-patobringup,never.Here,Ah...

英翻中 消失的过去 37(消失的英文怎么说?)

翻译(三十七):消失的过去/茱迪o皮考特VanishingActs/JodiPicoult”我能做什么?“直到听到了狄利亚轻柔的声音,我才意识到她已经在厨房里站了好一会儿了。当她说话的时候,...

RabbitMQ 延迟消息实战(rabbitmq如何保证消息不被重复消费)

现实生活中有一些场景需要延迟或在特定时间发送消息,例如智能热水器需要30分钟后打开,未支付的订单或发送短信、电子邮件和推送通知下午2:00开始的促销活动。RabbitMQ本身没有直接支持延迟...

Java对象拷贝原理剖析及最佳实践(java对象拷贝方法)

作者:宁海翔1前言对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po、Dto、Do、Vo各个表现层数据的转换,也存在于系统交互如序列化、反序列化。Java对象拷贝分为深拷贝和浅拷贝,目前常用的...

如何将 Qt 3D 渲染与 Qt Quick 2D 元素结合创建太阳系行星元素?

Qt组件推荐:QtitanRibbon:遵循MicrosoftRibbonUIParadigmforQt技术的RibbonUI组件,致力于为Windows、Linux和MacOSX提...

取消回复欢迎 发表评论: