Java中String对象的深入理解

 更新时间:2023年05月23日 08:22:19   作者:Change666  
String对象作为Java语言中重要的数据类型之一,是我们平时编码最常用的对象之一,因此也是内存中占据空间最大的一个对象,然而很多人对它是一知半解,今天我们就来好好聊一聊这个既熟悉又陌生的String,需要的朋友可以参考下

一、 String认识你,你认识它么?

假如面试的时候问你,什么是String(或者谈谈你对String的理解)?你会如何回答?“String是基础对象类型之一,是Java语言中重要的数据类型之一”。恐怕这是大多数人的回答,能力强些的可能会说,String底层是用char[ ]数组来实现的;如果面试官让你再继续呢?估计很多人会一脸尴尬,脑海里极力搜索关于String的相关知识,最后也只能恨自己平时对String关注的太少。下面就让我们一步一步地去认识String。
首先,来看一个面试经常遇到,错误率又很高的问题:

1 String str1 = “java”;
2 String str2 = new String(“java”);
3 String str3= str2.intern();
4 System.out.println(str1 == str2);
5 System.out.println(str2 == str3);
6 System.out.println(str1 == str3);

答案先不揭晓,各位先想一下,咱们继续往下看:

二、String对象的实现

我们把String对象的实现分为三个阶段来分析:java7之前的版本、java7/8版本、java8之后的版本。
1、 java7之前的版本中,String对象中主要由四个成员变量:char[]、偏移量offset、字符数量count、哈希值hash。String对象通过offset和count来定位char[],这么做可以高效、快速地共享数组对象,节省内存空间,但这种方式很有可能会导致内存泄漏。
2、 java7/8版本中,String 去除了offset 和 count 两个变量。这样的好处是String 对象占用的内存稍微少了些,同时,String.substring()方法也不再共享char[],从而解决了使用该方法可能导致的内存泄漏问题。
3、 java8之后的版本中,char[] 属性改为了 byte[] 属性,增加了一个新的属性coder,它是一个编码格式的标识。为什么这么做呢?我们知道一个char字符占16位,2 个字节。这种情况下,存储单字节编码内的字符(占一个字节的字符)就显得非常浪费。JDK1.9 的String类为了节约内存空间,于是使用了占8位,1个字节的 byte 数组来存放字符串。而新属性coder的作用是,在计算字符串长度或者使用 indexOf()函数时,我们需要根据这个字段,判断如何计算字符串长度。coder属性默认有0和1两个值,0代表Latin-1(单字节编码),1代表UTF-16。如果 String判断字符串只包含了Latin-1,则coder属性值为0,反之则为1。

三、String是不可变对象

1、为什么String是不可变对象很多人背面试题的时候想必都对此很熟悉,那为什么String对象是不可变的呢?你有想过这其中的原因么?通过源码我们知道,String类被final关键字修饰了,而且变量char[]也被final修饰了。Java语法告诉我们:被final修饰的类不可被继承,被final修饰的变量不可被改变,一旦赋值了初始值,该final变量的值就不能被重新赋值,即不可更改,而char[]被 final+private修饰,说明String对象不可被更改。即String对象一旦创建成功,就不能再对它进行改变。
2、为什么String被设计成不可变对象首先,是为了保证String对象的安全性,避免被恶意篡改。比如将值为“abc”的引用赋值给str对象,即String str = “abc”,如果此时有人恶意将“abc”改为“abcd”或其他值就会造成意想不到的错误。
其次,确保属性值hash不频繁变动,保证其唯一性。
3、为实现字符串常量池提供方便举一个反例来证明String对象的不可变性
针对String对象不可变性,有人可能会说:对于一个String str =“hello”,然后改为String str =“world”,这个时候str的值变成了“world”,str值确实改变了,为什么还说String对象不可变呢?
首先,我们来解释一下对象和引用。对象在内存中是一块内存地址,str则是一个指向该内存地址的引用,所以在这个例子中,第一次赋值的时候,创建了一个“hello”对象,str引用指向“hello”地址;第二次赋值的时候,又重新创建了一个对象“world”,str引用指向了“world”,但“hello”对象依然存在于内存中。也就是说str并不是对象,而只是一个对象引用。真正的对象依然还在内存中,没有被改变。所以在Java中要比较两个对象是否相等,通常是用“==”,而要判断两个对象的值是否相等,则需要用equals方法来判断。

四、String常量池

在java中,创建字符串通常有两种方式:一种是通过字符串常量池的形式,比如String str = “abcd”;另一种是直接通过new的形式,如String string = new String(“abcd”);
针对第一种方式创建字符串时,JVM首先会检查该对象是否存在于字符串常量池中,如果存在,就返回该引用,否则在常量池中创建新的字符串对象,然后将引用返回。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。
采用new形式创建字符串时,首先在编译类文件时,"abcd"常量字符串将会放入到常量结构中,在类加载时,“abcd"将会在常量池中创建;其次,在调用new时,JVM命令将会调用String的构造函数,同时引用常量池中的"abcd”字符串,在堆内存中创建一个 String对象;最后,string将引用String对象。

五、String.intern()方法详解

先来看一个示例:

  String a =new String("abc").intern();
  String b = new String("abc").intern();
  System.out.print(a==b);

你觉得输出的是false还是true?
答案是:true
在字符串常量中,默认会将对象放入常量池中;在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。如果调用intern()方法,会去查看字符串常量池中是否有等于该对象的字符串,如果没有,就在常量池中新增该对象,并返回该对象引用;如果有,就返回常量池中的字符串引用。堆内存中原有的对象由于没有引用指向它,将会通过垃圾回收器回收。
所以针对上面的例子中,在一开始创建a变量时,会在堆内存中创建一个对象,同时会在加载类时,在常量池中创建一个字符串对象,在调用intern()方法之后,会去常量池中查找是否有等于该字符串的对象,有就返回引用。在创建b字符串变量时,也会在堆中创建一个对象,此时常量池中有该字符串对象,就不再创建。调用 intern 方法则会去常量池中判断是否有等于该字符串的对象,发现有等于"abc"字符串的对象,就直接返回引用。而在堆内存中的对象,由于没有引用指向它,将会被垃圾回收。所以a和b引用的是同一个对象。
看完这些内容后,文章开头的问题,相比你也有了答案了。分别是:false、false、true。

六、String、StringBuffer和StringBuilder的区别

1.对象的可变与不可变String是不可变对象,原因上面的内容已经解释过了,这里不再赘述。
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存数据,这两种对象都是可变的。如下:
char[ ] value;
2.是否是线程安全String中的对象是不可变的,也就可以理解为常量,所以是线程安全。
AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。看如下源码:

1  public synchronized StringBuffer reverse() {
2      super.reverse();
3      return this;
4  }
5
6  public int indexOf(String str) {
7      return indexOf(str, 0);        //存在 public synchronized int indexOf(String str, int fromIndex) 方法
8  }

StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
3.StringBuilder与StringBuffer共同点StringBuilder与StringBuffer有公共的抽象父类AbstractStringBuilder。
抽象类与接口的一个区别是:抽象类中可以定义一些子类的公共方法,子类只需要增加新的功能,不需要重复写已经存在的方法;而接口中只是对方法的申明和常量的定义。
StringBuilder、StringBuffer的方法都会调用AbstractStringBuilder中的公共方法,如super.append(…)。只是StringBuffer会在方法上加synchronized关键字,进行同步。
如果程序不是多线程的,那么使用StringBuilder效率高于StringBuffer。

下面来几道测试题,看看自己对String究竟掌握了多少

七、测试题

test1、如下代码中创建了几个对象

1 String str1 = "abc";
2 String str2 = new String("abc");

对于1中的 String str1 = “abc”,首先会检查字符串常量池中是否含有字符串abc,如果有则直接指向,如果没有则在字符串常量池中添加abc字符串并指向它.所以这种方法最多创建一个对象,有可能不创建对象。
对于2中的String str2 = new String(“abc”),首先会在堆内存中申请一块内存存储字符串abc,str2指向其内存块对象。同时还会检查字符串常量池中是否含有abc字符串,若没有则添加abc到字符串常量池中。所以 new String()可能会创建两个对象。
所以如果以上两行代码在同一个程序中,则1中创建了1个对象,2中创建了1个对象。如果将这两行代码的顺序调换一下,则String str2 = new String(“abc”)创建了两个对象,而 String str1 = "abc"没有创建对象。

test2、看看下面的代码创建了多少个对象:

1     String temp="apple";  
2     for(int i=0;i<1000;i++) {  
3           temp=temp+i;  
4     }

答案:1001个对象。

test3、下面的代码创建了多少个对象:

1     String temp = new String("apple")  
2     for(int i=0;i<1000;i++) {  
3            temp = temp+i;  
4     }

答案:1002个对象。

test4:

1 String ok = "ok";  
2 String ok1 = new String("ok");  
3 System.out.println(ok == ok1);//fasle 

ok指向字符串常量池,ok1指向new出来的堆内存块,new的字符串在编译期是无法确定的。所以输出false。

test5:

1 String ok = "apple1";  
2 String ok1 = "apple"+1;  
3 System.out.println(ok==ok1);//true 

编译期ok和ok1都是确定的,字符串都为apple1,所以ok和ok1都指向字符串常量池里的字符串apple1。指向同一个对象,所以为true。

test6:

1 String ok = "apple1";  
2 int temp = 1;  
3 String ok1 = "apple"+temp;  
4 System.out.println(ok==ok1);//false

主要看ok和ok1能否在编译期确定,ok是确定的,放进并指向常量池,而ok1含有变量导致不确定,所以不是同一个对象.输出false。

test7:

1 String ok = "apple1";  
2 final int temp = 1;  
3 String ok1 = "apple"+temp;  
4 System.out.println(ok==ok1);//true 

ok确定,加上final后使得ok1也在编译期能确定,所以输出true。

test8:

 1 public static void main(String[] args) {    
 2     String ok = "apple1";  
 3     final int temp = getTemp();  
 4     String ok1 = "apple"+temp;  
 5     System.out.println(ok==ok1);//false       
 6 }  
 7   
 8 public static int getTemp(){  
 9     return 1;  
10 }

ok一样是确定的。而ok1不能确定,需要运行代码获得temp,所以不是同一个对象,输出false。

以上内容如有不对的地方,还请各位指正!多谢!

以上就是Java中String对象的深入理解的详细内容,更多关于Java String对象的资料请关注脚本之家其它相关文章!

相关文章

  • 详解如何使用SpringBoot封装Excel生成器

    详解如何使用SpringBoot封装Excel生成器

    在软件开发过程中,经常需要生成Excel文件来导出数据或者生成报表,为了简化开发流程和提高代码的可维护性,我们可以使用Spring Boot封装Excel生成器,本文将介绍如何使用Spring Boot封装Excel生成器,并提供一些示例代码来说明其用法和功能
    2023-06-06
  • 使用Spring自定义实现IOC和依赖注入(注解方式)

    使用Spring自定义实现IOC和依赖注入(注解方式)

    这篇文章主要介绍了使用Spring自定义实现IOC和依赖注入(注解方式),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • Mybatis下动态sql中##和$$的区别讲解

    Mybatis下动态sql中##和$$的区别讲解

    今天小编就为大家分享一篇关于Mybatis下动态sql中##和$$的区别讲解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-03-03
  • 解决MultipartFile.transferTo(dest) 报FileNotFoundExcep的问题

    解决MultipartFile.transferTo(dest) 报FileNotFoundExcep的问题

    这篇文章主要介绍了解决MultipartFile.transferTo(dest) 报FileNotFoundExcep的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • 实战分布式医疗挂号系统登录接口整合阿里云短信详情

    实战分布式医疗挂号系统登录接口整合阿里云短信详情

    这篇文章主要为大家介绍了实战分布式医疗挂号系统登录接口整合阿里云短信详情,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪<BR>
    2022-04-04
  • java读取csv文件示例分享(java解析csv文件)

    java读取csv文件示例分享(java解析csv文件)

    这篇文章主要介绍了java读取csv文件示例,这个java解析csv文件的例子很简单,下面直接上代码,大家参考使用吧
    2014-03-03
  • Java详细解析下拉菜单和弹出菜单的使用

    Java详细解析下拉菜单和弹出菜单的使用

    这篇文章主要介绍了怎么用Java来创建和使用下拉菜单和弹出菜单,下拉菜单和弹出菜单是我们经常要用的工具,但是你有想过自己怎么去实现它吗,感兴趣的朋友跟随文章往下看看吧
    2022-04-04
  • 给JavaBean赋默认值并且转Json字符串的实例

    给JavaBean赋默认值并且转Json字符串的实例

    这篇文章主要介绍了给JavaBean赋默认值并且转Json字符串的实例,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • Springboot框架实现自动装配详解

    Springboot框架实现自动装配详解

    在使用springboot时,很多配置我们都没有做,都是springboot在帮我们完成,这很大一部分归功于springboot自动装配。本文将详细为大家讲解SpringBoot的自动装配原理,需要的可以参考一下
    2022-08-08
  • Eclipse 2022 设置中文汉化的超详细图文教程

    Eclipse 2022 设置中文汉化的超详细图文教程

    这篇文章主要介绍了Eclipse 2022 设置中文汉化的超详细图文教程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-03-03

最新评论