数组可以存字符串吗_JavaString对象,你真的了解了吗?
String 对象的实现
String对象是 Java 中使⽤最频繁的对象之⼀,所以 Java 公司也在不断的对String对象的实现进⾏优化,以便提升String对象的性能,看下⾯这张图,⼀起了解⼀下String对象的优化过程。
1. 在 Java6 以及之前的版本中
String对象是对 char 数组进⾏了封装实现的对象,主要有四个成员变量: char 数组、偏移量 offset、字符数量 count、哈希值 hash。
String对象是通过 offset 和 count 两个属性来定位 char[] 数组,获取字符串。这么做可以⾼效、快速地共享数组对象,同时节省内存空间,但这种⽅式很有可能会导致内存泄漏。
2. 从 Java7 版本开始到 Java8 版本
从 Java7 版本开始,Java 对String类做了⼀些改变。String类中不再有 offset 和 count 两个变量了。这样的好处是String对象占⽤的内存稍微少了些,同时 String.substring ⽅法也不再共享 char[],从⽽解决了使⽤该⽅法可能导致的内存泄漏问题。
3. 从 Java9 版本开始
将 char[] 数组改为了 byte[] 数组,为什么需要这样做呢?我们知道 char 是两个字节,如果⽤来存⼀个字节的字符有点浪费,为了节约空间,Java 公司就改成了⼀个字节的byte来存储字符串。这样在存储⼀个字节的字符是就避免了浪费。
在 Java9 维护了⼀个新的属性 coder,它是编码格式的标识,在计算字符串长度或者调⽤ indexOf() 函数时,需要根据这个字段,判断如何计算字符串长度。coder 属性默认有 0 和 1 两个值, 0 代表Latin-1(单字节编码),1 代表 UTF-16 编码。如果 String判断字符串只包含了 Latin-1,则 coder 属性值为 0 ,反之则为 1。
String 对象的创建⽅式
1、通过字符串常量的⽅式
String str= "pingtouge"的形式,使⽤这种形式创建字符串时, JVM 会在字符串常量池中先检查是否存在该对象,如果存在,返回该对象的引⽤地址,如果不存在,则在字符串常量池中创建该字符串对象并且返回引⽤。使⽤这种⽅式创建的好处是:避免了相同值的字符串重复创建,节约了内存
2、String()构造函数的⽅式
String str = new String("pingtouge")的形式,使⽤这种⽅式创建字符串对象过程就⽐较复杂,分成两个阶段,⾸先在编译时,字符
串pingtouge会被加⼊到常量结构中,类加载时候就会在常量池中创建该字符串。然后就是在调⽤new()时,JVM 将会调⽤String的构造函数,同时引⽤常量池中的pingtouge字符串, 在堆内存中创建⼀个String对象并且返回堆中的引⽤地址。
了解了String对象两种创建⽅式,我们来分析⼀下下⾯这段代码,加深我们对这两种⽅式的理解,下⾯这段代码⽚中,str是否等于str1呢?
String str = "pingtouge";
String str1 = new String("pingtouge");
system.out.println(str==str1)
我们逐⼀来分析这⼏⾏代码,⾸先从String str = "pingtouge"开始,这⾥使⽤了字符串常量的⽅式创建字符串对象,在创建pingtouge字符串对象时,JVM会去常量池中查是否存在该字符串,这⾥的答案肯定是没有的,所以JVM将会在常量池中创建该字符串对象并且返回对象的地址引⽤,所以str指向的是pingtouge字符串对象在常量池中的地址引⽤。
然后是String str1 = new String("pingtouge")这⾏代码,这⾥使⽤的是构造函数的⽅式创建字符串对象,根据我们上⾯对构造函数⽅式创建字符串对象的理解,str1得到的应该是堆中pingtouge字符串的引⽤地址。由于str指向的是pingtouge字符串对象在常量池中的地址引⽤⽽str1指向的是堆中pingtouge字符串的引⽤地址,所以str肯定不等于str1。
String 对象的不可变性
从我们知道String对象的那⼀刻起,我想⼤家都知道了String对象是不可变的。那它不可变是怎么做到的呢?Java 这么做能带来哪些好处?我们⼀起来简单的探讨⼀下,先来看看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 修饰符,我们知道当⼀个类被 final 修饰时,表明这个类不能被继承,所以String类不能被继承。这是String不可变的第⼀点
再往下看,⽤来存储字符串的char value[]数组被private 和final修饰,我们知道对于⼀个被final的基本数据类型的变量,则其数值⼀旦在初始化之后便不能更改。这是String不可变的第⼆点。
Java 公司为什么要将String设置成不可变的,主要从以下三⽅⾯考虑:
1、保证 String 对象的安全性。假设 String 对象是可变的,那么 String 对象将可能被恶意修改。
2、保证 hash 属性值不会频繁变更,确保了唯⼀性,使得类似 HashMap 容器才能实现相应的 key-value 缓存功能。
3、可以实现字符串常量池
String 对象的优化
字符串是我们常⽤的Java类型之⼀,所以对字符串的操作也是避免不了的,在对字符串的操作过程中,如果使⽤不当,性能会天差地别。那么在字符串的操作过程中,有哪些地⽅需要我们注意呢?
优雅的拼接字符串
字符串的拼接是对字符串操作使⽤最频繁的操作之⼀,由于我们知道String对象的不可变性,所以我们在做拼接时尽可能少的使⽤+进⾏字符串拼接或者说潜意识⾥认为不能使⽤+进⾏字符串拼接,认为使⽤+进⾏字符串拼接会产⽣许多⽆⽤的对象。事实真的是这样吗?我们来做⼀个实验。我们使⽤+来拼接下⾯这段字符串。
String str8 = "ping" +"tou"+"ge";
⼀起来分析⼀下这段代码会产⽣多少个对象?如果按照我们理解的意思来分析的话,⾸先会创建ping对象,然后创建pingtou对象,最后才会创建pingtouge对象,⼀共创建了三个对象。真的是这样吗?其实不是这样的,Java 公司怕我们程序员⼿误,所以对编译器进⾏了优化,上⾯的这段字符串拼接会被我们的编译器优化,优化成⼀个String str8 = "pingtouge";对象。除了对常量字符串拼接做了优化以外,对于使
⽤+号动态拼接字符串,编译器也做了相应的优化,以便提升String的性能,例如下⾯这段代码:
String str = "pingtouge";
java中split的用法for(int i=0; i<1000; i++) {
str = str + i;
}
编译器会帮我们优化成这样
String str = "pingtouge";
for(int i=0; i<1000; i++) {
str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}
可以看出 Java 公司对这⼀块进⾏了不少的优化,防⽌由于程序员不⼩⼼导致String性能急速下降,尽管 Java 公司在编译器这⼀块做了相应的优化,但是我们还是能看出 Java 公司优化的不⾜之处,在动态拼接字符串时,虽然使⽤了 StringBuilder 进⾏字符串拼接,但是每次循环都会⽣成⼀个新的 StringBuilder 实例,同样也会降低系统的性能。
「在动态的拼接字符串时,如果不涉及到线程安全的情况下,我们显⽰所以我们在做字符串拼接时,我们需要从代码的层⾯进⾏优化,「在动态的拼接字符串时,如果不涉及到线程安全的情况下,我们显⽰的使⽤ StringBuilder 进⾏拼接,提升系统性能,如果涉及到线程安全的话,我们使⽤ StringBuffer 来进⾏字符串拼接」
巧妙的使⽤ intern() ⽅法
* <p>
* 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.
* <p>
public native String intern();
这是 intern() 函数的官⽅注释说明,⼤概意思就是 intern 函数⽤来返回常量池中的某字符串,如果常量池中已经存在该字符串,则直接返回常量池中该对象的引⽤。否则,在常量池中加⼊该对象,然后 返回引⽤。
有⼀位Twitter⼯程师在QCon全球软件开发⼤会上分享了⼀个他们对 String对象优化的案例,他们利⽤String.intern()⽅法将以前需要20G内存存储优化到只需要⼏百兆内存。这⾜以体现String.intern()的威⼒,我们⼀起来看⼀个例⼦,简单的了解⼀下String.intern()的⽤法。
public static void main(String[] args) {
String str = new String("pingtouge");
String str1 = new String("pingtouge");
System.out.println("未使⽤intern()⽅法:"+(str==str1));
System.out.println("未使⽤intern()⽅法,str:"+str);
System.out.println("未使⽤intern()⽅法,str1:"+str1);
String str2= new String("pingtouge").intern();
String str3 = new String("pingtouge").intern();
System.out.println("使⽤intern()⽅法:"+(str2==str3));
System.out.println("使⽤intern()⽅法,str2:"+str2);
System.out.println("使⽤intern()⽅法,str3:"+str3);
}
从结果中可以看出,未使⽤String.intern()⽅法时,构造相同值的字符串对象返回不同的对象引⽤地址,使⽤String.intern()⽅法后,构造相同值的字符串对象时,返回相同的对象引⽤地址。这能帮我们节约不少空间
「String.intern()⽅法虽然好,但是我们要结合场景使⽤,不能乱⽤,因为常量池的实现是类似于⼀个HashTable的实现⽅
式,HashTable 存储的数据越⼤,遍历的时间复杂度就会增加。如果数据过⼤,会增加整个字符串常量池的负担。」
灵活的字符串的分割
字符串的分割是字符串操作的常⽤操作之⼀,对于字符串的分割,⼤部分⼈使⽤的都是 Split() ⽅法,Split() ⽅法⼤多数情况下使⽤的是正则表达式,这种分割⽅式本⾝没有什么问题,但是由于正则表达
式的性能是⾮常不稳定的,使⽤不恰当会引起回溯问题,很可能导致 CPU 居⾼不下。在以下两种情况下 Split() ⽅法不会使⽤正则表达式:
传⼊的参数长度为1,且不包含“.$|()[{^?*+”regex元字符的情况下,不会使⽤正则表达式
传⼊的参数长度为2,第⼀个字符是反斜杠,并且第⼆个字符不是ASCII数字或ASCII字母的情况下,不会使⽤正则表达式
「所以我们在字符串分割时,应该慎重使⽤ Split() ⽅法,⾸先考虑使⽤ String.indexOf() ⽅法进⾏字符串分割,如果
String.indexOf() ⽆法满⾜分割要求,再使⽤ Split() ⽅法,使⽤ Split() ⽅法分割字符串时,需要注意回溯问题。」
❝ ⽂章不⾜之处,望⼤家多多指点,共同学习,共同进步
❞
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论