深入了解java中的string对象

这里来对Java中的String对象做一个稍微深入的了解。

Java对象实现的演进

String对象是Java中使用最频繁的对象之一,所以Java开发者们也在不断地对String对象的实现进行优化,以便提升String对象的性能。

Java6以及之前版本中String对象的属性

在Java6以及之前版本中,String对象是对char数组进行了封装实现的对象,其主要有4个成员成员变量,分别是char数组、偏移量offset、字符数量count和哈希值hash。String对象是通过offset和count两个属性来定位char[]数组,获取字符串。这样做可以高效、快速地共享数组对象,同时节省内存空间,但是这种方式却可能会导致内存泄漏的发生。

Java7、8版本中String对象的属性

从Java7版本开始,Java对String类做了一些改变,具体是String类不再有offset和count两个变量了。这样做的好处是String对象占用的内存稍微少了点,同时String.substring()方法也不再共享char[]了,从而解决了使用该方法可能导致的内存泄漏问题。

Java9以及之后版本中String对象的属性

从Java9版本开始,Java将char[]数组改为了byte[]数组。我们都知道,char是两个字节的,如果用来存一个字节的情况下就会造成内存空间的浪费。而为了节约这一个字节的空间,Java开发者就改成了一个使用一个字节的byte来存储字符串。

另外,在Java9中,String对象维护了一个新的属性coder,这个属性是编码格式的标识,在计算字符串长度或者调用indexOf()方法的时候,会需要根据这个字段去判断如何计算字符串长度。coder属性默认有0和1两个值,其中0代表Latin-1(单字节编码),1则表示UTF-16编码。

String对象的创建方式与在内存中的存放

在Java中,对于基本数据类型的变量和对对象的引用,保存在栈内存的局部变量表中;而通过new关键字和Constructor创建的对象,则是保存在堆内存中。而String对象的创建方式一般为两种,一种是字面量(字符串常量)的方式,一种则是构造函数(String())的方式,两种方式在内存中的存放有所不同。

字面量(字符串常量)的创建方式

使用字面量的方式创建字符串时,JVM会在字符串常量池中先检查是否存在该字面量,如果存在,则返回该字面量在内存中的引用地址;如果不存在,则在字符串常量池中创建该字面量并返回引用。使用这种方式创建的好处是避免了相同值的字符串在内存中被重复创建,节约了内存,同时这种写法也会比较简单易读一些。

String str = "i like yanggb.";

字符串常量池

这里要特别说明一下常量池。常量池是JVM为了减少字符串对象的重复创建,特别维护了一个特殊的内存,这段内存被称为字符串常量池或者字符串字面量池。在JDK1.6以及之前的版本中,运行时常量池是在方法区中的。在JDK1.7以及之后版本的JVM,已经将运行时常量池从方法区中移了出来,在Java堆(Heap)中开辟了一块区域用来存放运行时常量池。而从JDK1.8开始,JVM取消了Java方法区,取而代之的是位于直接内存的元空间(MetaSpace)。总结就是,目前的字符串常量池在堆中。

我们所知道的几个String对象的特点都来源于String常量池。

1.在常量池中会共享所有的String对象,因此String对象是不可被修改的,因为一旦被修改,就会导致所有引用此String对象的变量都随之改变(引用改变),所以String对象是被设计为不可修改的,后面会对这个不可变的特性做一个深入的了解。

2.String对象拼接字符串的性能较差的说法也是来源于此,因为String对象不可变的特性,每次修改(这里是拼接)都是返回一个新的字符串对象,而不是再原有的字符串对象上做修改,因此创建新的String对象会消耗较多的性能(开辟另外的内存空间)。

3.因为常量池中创建的String对象是共享的,因此使用双引号声明的String对象(字面量)会直接存储在常量池中,如果该字面量在之前已存在,则是会直接引用已存在的String对象,这一点在上面已经描述过了,这里再次提及,是为了特别说明这一做法保证了在常量池中的每个String对象都是唯一的,也就达到了节约内存的目的。

构造函数(String())的创建方式

使用构造函数的方式创建字符串时,JVM同样会在字符串常量池中先检查是否存在该字面量,只是检查后的情况会和使用字面量创建的方式有所不同。如果存在,则会在堆中另外创建一个String对象,然后在这个String对象的内部引用该字面量,最后返回该String对象在内存地址中的引用;如果不存在,则会先在字符串常量池中创建该字面量,然后再在堆中创建一个String对象,然后再在这个String对象的内部引用该字面量,最后返回该String对象的引用。

String str = new String("i like yanggb.");

这就意味着,只要使用这种方式,构造函数都会另行在堆内存中开辟空间,创建一个新的String对象。具体的理解是,在字符串常量池中不存在对应的字面量的情况下,new String()会创建两个对象,一个放入常量池中(字面量),一个放入堆内存中(字符串对象)。

String对象的比较

比较两个String对象是否相等,通常是有【==】和【equals()】两个方法。

在基本数据类型中,只可以使用【==】,也就是比较他们的值是否相同;而对于对象(包括String)来说,【==】比较的是地址是否相同,【equals()】才是比较他们内容是否相同;而equals()是Object都拥有的一个函数,本身就要求对内部值进行比较。

String str = "i like yanggb.";
String str1 = new String("i like yanggb.");

System.out.println(str == str1); // false
System.out.println(str.equals(str1)); // true

因为使用字面量方式创建的String对象和使用构造函数方式创建的String对象的内存地址是不同的,但是其中的内容却是相同的,也就导致了上面的结果。

String对象中的intern()方法

我们都知道,String对象中有很多实用的方法。为什么其他的方法都不说,这里要特别说明这个intern()方法呢,因为其中的这个intern()方法最为特殊。它的特殊性在于,这个方法在业务场景中几乎用不上,它的存在就是在为难程序员的,也可以说是为了帮助程序员了解JVM的内存结构而存在的(?我信你个鬼,你个糟老头子坏得很)。

/**
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
**/
public native String intern();

上面是源码中的intern()方法的官方注释说明,大概意思就是intern()方法用来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后返回引用。然后我们可以从方法签名上看出intern()方法是一个native方法。

下面通过几个例子来详细了解下intern()方法的用法。

第一个例子

String str1 = new String("1");
System.out.println(str1 == str1.intern()); // false
System.out.println(str1 == "1"); // false

在上面的例子中,intern()方法返回的是常量池中的引用,而str1保存的是堆中对象的引用,因此两个打印语句的结果都是false。

第二个例子

String str2 = new String("2") + new String("3");
System.out.println(str2 == str2.intern()); // true
System.out.println(str2 == "23"); // true

在上面的例子中,str2保存的是堆中一个String对象的引用,这和JVM对【+】的优化有关。实际上,在给str2赋值的第一条语句中,创建了3个对象,分别是在字符串常量池中创建的2和3、还有在堆中创建的字符串对象23。因为字符串常量池中不存在字符串对象23,所以这里要特别注意:intern()方法在将堆中存在的字符串对象加入常量池的时候采取了一种截然不同的处理方案——不是在常量池中建立字面量,而是直接将该String对象自身的引用复制到常量池中,即常量池中保存的是堆中已存在的字符串对象的引用。根据前面的说法,这时候调用intern()方法,就会在字符串常量池中复制出一个对堆中已存在的字符串常量的引用,然后返回对字符串常量池中这个对堆中已存在的字符串常量池的引用的引用(就是那么绕,你来咬我呀)。这样,在调用intern()方法结束之后,返回结果的就是对堆中该String对象的引用,这时候使用【==】去比较,返回的结果就是true了。同样的,常量池中的字面量23也不是真正意义的字面量23了,它真正的身份是堆中的那个String对象23。这样的话,使用【==】去比较字面量23和str2,结果也就是true了。

第三个例子

String str4 = "45";
String str3 = new String("4") + new String("5");
System.out.println(str3 == str3.intern()); // false
System.out.println(str3 == "45"); // false

这个例子乍然看起来好像比前面的例子还要复杂,实际上却和上面的第一个例子是一样的,最难理解的反而是第二个例子。

所以这里就不多说了,而至于为什么还要举这个例子,我相信聪明的你一下子就明白了(我有医保,你来打我呀)。

String对象的不可变性

先来看String对象的一段源码。

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

  /** Cache the hash code for the string */
  private int hash; // Default to 0

  /** use serialVersionUID from JDK 1.0.2 for interoperability */
  private static final long serialVersionUID = -6849794470754667710L;
}

从类签名上来看,String类用了final修饰符,这就意味着这个类是不能被继承的,这是决定String对象不可变特性的第一点。从类中的数组char[] value来看,这个类成员变量被private和final修饰符修饰,这就意味着其数值一旦被初始化之后就不能再被更改了,这是决定String对象不可变特性的第二点。

Java开发者为什么要将String对象设置为不可变的,主要可以从以下三个方面去考虑:

1.安全性。假设String对象是可变的,那么String对象将可能被恶意修改。

2.唯一性。这个做法可以保证hash属性值不会频繁变更,也就确保了唯一性,使得类似HashMap的容器才能实现相应的key-value缓存功能。

3.功能性。可以实现字符串常量池(究竟是先有设计,还是先有实现呢)。

String对象的优化

字符串是常用的Java类型之一,所以对字符串的操作是避免不了的。而在对字符串的操作过程中,如果使用不当的话,性能可能会有天差地别,所以有一些地方是要注意一下的。

拼接字符串的性能优化

字符串的拼接是对字符串的操作中最频繁的一个使用。由于我们都知道了String对象的不可变性,所以我们在开发过程中要尽量减少使用【+】进行字符串拼接操作。这是因为使用【+】进行字符串拼接,会在得到最终想要的结果前产生很多无用的对象。

String str = 'i';
str = str + ' ';
str = str + 'like';
str = str + ' ';
str = str + 'yanggb';
str = str + '.';

System.out.println(str); // i like yanggb.

事实上,如果我们使用的是比较智能的IDE编写代码的话,编译器是会提示将代码优化成使用StringBuilder或者StringBuffer对象来优化字符串的拼接性能的,因为StringBuilder和StringBuffer都是可变对象,也就避免了过程中产生无用的对象了。而这两种替代方案的区别是,在需要线程安全的情况下,选用StringBuffer对象,这个对象是支持线程安全的;而在不需要线程安全的情况下,选用StringBuilder对象,因为StringBuilder对象的性能在这种场景下,要比StringBuffer对象或String对象要好得多。

使用intern()方法优化内存占用

前面吐槽了intern()方法在实际开发中没什么用,这里又来说使用intern()方法来优化内存占用了,这人真的是,嘿嘿,真香。关于方法的使用就不说了,上面有详尽的用法说明,这里来说说具体的应用场景好了。有一位Twitter的工程师在Qcon全球软件开发大会上分享了一个他们对String对象优化的案例,他们利用了这个String.intern()方法将以前需要20G内存存储优化到只需要几百兆内存。具体就是,使用intern()方法将原本需要创建到堆内存中的String对象都放到常量池中,因为常量池的不重复特性(存在则返回引用),也就避免了大量的重复String对象造成的内存浪费问题。

什么,要我给intern()方法道歉?不可能。String.intern()方法虽好,但是也是需要结合场景来使用的,并不能够乱用。因为实际上,常量池的实现是类似于一个HashTable的实现方式,而HashTable存储的数据越大,遍历的时间复杂度就会增加。这就意味着,如果数据过大的话,整个字符串常量池的负担就会大大增加,有可能性能不会得到提升却反而有所下降。

字符串分割的性能优化

字符串的分割是字符串操作的常用操作之一,对于字符串的分割,大部分人使用的都是split()方法,split()方法在大部分场景下接收的参数都是正则表达式,这种分割方式本身没有什么问题,但是由于正则表达式的性能是非常不稳定的,使用不恰当的话可能会引起回溯问题并导致CPU的占用居高不下。在以下两种情况下split()方法不会使用正则表达式:

1.传入的参数长度为1,且不包含“.$|()[{^?*+\”regex元字符的情况下,不会使用正则表达式。

2.传入的参数长度为2,第一个字符是反斜杠,并且第二个字符不是ASCII数字或ASCII字母的情况下,不会使用正则表达式。

所以我们在字符串分割时,应该慎重使用split()方法,而首先考虑使用String.indexOf()方法来进行字符串分割,在String.indexOf()无法满足分割要求的时候再使用Split()方法。而在使用split()方法分割字符串时,需要格外注意回溯问题。

总结

虽然说在不了解String对象的情况下也能使用String对象进行开发,但是了解String对象可以帮助我们写出更好的代码。

以上所述是小编给大家介绍的java中的string对象,希望对大家有所帮助!

(0)

相关推荐

  • java对象转换String类型的三种方法

    一.采用Object.toString()toString方法是java.lang.Object对象的一个public方法.在java中任何对象都会继承Object对象,所以一般来说任何对象都可以调用toString这个方法.这是采用该种方法时,常派生类会覆盖Object里的toString()方法.但是在使用该方法时要注意,必须保证Object不是null值,否则将抛出NullPointerException异常. 二.采用(String)Object 该方法是一个标准的类型转换的方法,可以将

  • Java 中的 String对象为什么是不可变的

    什么是不可变对象? String对象是不可变的,但这仅意味着你无法通过调用它的公有方法来改变它的值. 众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的.不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变. 区分对象和对象的引用 对于Java初学者, 对于String是不可变对

  • Java中的String对象数据类型全面解析

    1. 首先String不属于8种基本数据类型,String是一个对象. 因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性. 2. new String()和new String("")都是申明一个新的空字符串,是空串不是null; 3. String str="kvill"; String str=new String ("kvill");的区别: 在这里,我们不谈堆,也不谈栈,只

  • Java String创建对象实例解析

    本文研究的主要是Java String创建对象的问题,具体介绍如下. 首先我们要明白两个概念,引用变量和对象,对象一般通过new在堆中创建,String只是一个引用变量. 所有的字符串都是String对象,由于字符串常量的大量使用,java中为了节省时间,在编译阶段,会把所有字符串常量放在字符串常量池中,字符串常量池的一个好处就是可以把相同的字符串合并,占用一个空间. 虽然在Java中无法直接获取变量的地址,但是可以用==判断一下两个引用变量是否指向了一个地址即一个对象. 栈内存 堆内存 基础类

  • JSON的String字符串与Java的List列表对象的相互转换

    在前端: 1.如果json是List对象转换的,可以直接遍历json,读取数据. 2.如果是需要把前端的List对象转换为json传到后台,param是ajax的参数,那么转换如下所示: var jsonStr = JSON.stringify(list); var param= {}; param.jsonStr=jsonStr; 在后台: 1.把String转换为List(str转换为list) List<T> list = new ArrayList<T>(); JSONAr

  • JAVA面试题String产生了几个对象

    面试官Q1:请问String s = new String("xyz");产生了几个对象? 对于这个Java面试题,老套路先上代码: public class StringTest { public static void main(String[] args){ String s1="Hello"; String s2="Hello"; String s3=new String("Hello"); System.out.pr

  • 深入了解java中的string对象

    这里来对Java中的String对象做一个稍微深入的了解. Java对象实现的演进 String对象是Java中使用最频繁的对象之一,所以Java开发者们也在不断地对String对象的实现进行优化,以便提升String对象的性能. Java6以及之前版本中String对象的属性 在Java6以及之前版本中,String对象是对char数组进行了封装实现的对象,其主要有4个成员成员变量,分别是char数组.偏移量offset.字符数量count和哈希值hash.String对象是通过offset和

  • javascript实现Java中的Map对象功能的实例详解

    javascript  自定义对象实现Java中的Map对象功能 Java中有集合,Map等对象存储工具类,这些对象使用简易,但是在JavaScript中,你只能使用Array对象. 这里我创建一个自定义对象,这个对象内包含一个数组来存储数据,数据对象是一个Key,可以实际存储的内容! 这里Key,你要使用String类型,和Java一样,你可以进行一些增加,删除,修改,获得的操作. 使用很简单,我先把工具类给大家看下: /** * @version 1.0 * @author cuisuqia

  • java 中Spark中将对象序列化存储到hdfs

    java 中Spark中将对象序列化存储到hdfs 摘要: Spark应用中经常会遇到这样一个需求: 需要将JAVA对象序列化并存储到HDFS, 尤其是利用MLlib计算出来的一些模型, 存储到hdfs以便模型可以反复利用. 下面的例子演示了Spark环境下从Hbase读取数据, 生成一个word2vec模型, 存储到hdfs. 废话不多说, 直接贴代码了. spark1.4 + hbase0.98 import org.apache.spark.storage.StorageLevel imp

  • 浅谈java中对集合对象list的几种循环访问

    java中对集合对象list的几种循环访问的总结如下  1 经典的for循环 public static void main(String[] args) { List<String> list = new ArrayList(); list.add("123"); list.add("java"); list.add("j2ee"); System.out.println("=========经典的for循环=======

  • Java 中组合模型之对象结构模式的详解

    Java 中组合模型之对象结构模式的详解 一.意图 将对象组合成树形结构以表示"部分-整体"的层次结构.Composite使得用户对单个对象和组合对象的使用具有一致性. 二.适用性 你想表示对象的部分-整体层次结构 你希望用户忽略组合对象与单个对象的不同,用户将统一使用组合结构中的所有对象. 三.结构 四.代码 public abstract class Component { protected String name; //节点名 public Component(String n

  • js接收并转化Java中的数组对象的方法

    在做项目时,要向ocx控件下发命令,就要在js中得到java中的对象,然后拼成一种格式,下发下去...当对象是一个时比较简单,但如果对象是一个数组时,就略显麻烦了. 开始我以为有简单的方式,可以直接进行内容的转化,后来发现不可以,网上说js与java没有桥接的东西,所以呢: 我的解决方案是:在action层,将java的对象数组转化为Json串,而在js中,再把json转化为数组对象. 1.将java的对象数组转化为Json串: 要用到两个类: net.sf.json.JSONObject ne

  • 基于java中两个对象属性的比较

    两个对象进行比较相等,有两种做法: 1.情况一:当仅仅只是判断两个对象是否相等时,只需重写equals()方法即可.这里就不用说明 2.情况二:当除了情况一之外,还需知道是那个属性不同,那么就需要采用类反射, 具体代码如下: public static void main(String[] args) { A a = new A(); a.setUserName("a"); a.setPassword("p"); a.setQq("q"); a.

  • Java 中桥接模式——对象结构型模式的实例详解

    Java  中桥接模式--对象结构型模式的实例详解 一.意图 将抽象部分与它的实现部分分离,使他们都可以独立的变化. 二.适用性 以下一些情况使用Bridge模式 你不希望在抽象和它的实现部分之间有一个固定的绑定关系.例如这种情况可能因为,在程序运行时刻实现部分应可以被选择或者切换. 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充.这时Bridge模式使你可以对不同的抽象接口和实现部分进行组合,并分别对他们进行扩充. 对一个抽象的实现部分的修改应对客户不产生影响,即客户代码不必重新编译

随机推荐