likes
comments
collection
share

java之String对象深入理解

作者站长头像
站长
· 阅读数 17

前言

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。

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