老生常谈 String、StringBuilder、StringBuffer
[TOC]
字符串就是一连串的字符序列,Java提供了String、StringBuilder、StringBuffer三个类来封装字符串
String
String
类是不可变类,String对象被创建以后,对象中的字符序列是不可改变的,直到这个对象被销毁
为什么是不可变的
jdk1.8 public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; //jdk1.9中将char数组替换为byte数组,紧凑字符串带来的优势:更小的内存占用,更快的操作速度。 //构造函数 public String(String original) { this.value = original.value; this.hash = original.hash; } //构造函数 public String(char value[]) { this.value = Arrays.copyOf(value, value.length); } //返回一个新的char[] public char[] toCharArray() { // Cannot use Arrays.copyOf because of class initialization order issues char result[] = new char[value.length]; System.arraycopy(value, 0, result, 0, value.length); return result; } }
根据上面的代码,我们看看String究竟是怎么保证不可变的。
- String类被final修饰,不可被继承
- string内部所有成员都设置为私有变量,外部无法访问
- 没有向外暴露修改
value
的接口 -
value
被final修饰,所以变量的引用不可变。 -
char[]·
为引用类型仍可以通过引用修改实例对象,为此String(char value[])
构造函数内部使用的copyOf
而不是直接将value[]
复制给内部变量`。 - 在获取value时,并没有将value的引用直接返回,而是采用了
arraycopy()
的方式返回一个新的char[]
-
String
类中的函数也处处透露着不可变的味道,比如:replace()
public String replace(char oldChar, char newChar) { if (oldChar != newChar) { int len = value.length; int i = -1; char[] val = value; /* avoid getfield opcode */ while (++i < len) { if (val[i] == oldChar) { break; } } if (i < len) { //重新创建新的char[],不改变原有对象中的值 char buf[] = new char[len]; for (int j = 0; j < i; j++) { buf[j] = val[j]; } while (i < len) { char c = val[i]; buf[i] = (c == oldChar) ? newChar : c; i++; } //最后返回新创建的String对象 return new String(buf, true); } } return this; }
当然不可变也不是绝对的,还是可以通过反射获取到变value引用,然后通过value[]修改数组的方式改变value对象实例
String a = "Hello World!"; String b = new String("Hello World!"); String c = "Hello World!"; //通过反射修改字符串引用的value数组 Field field = a.getClass().getDeclaredField("value"); field.setAccessible(true); char[] value = (char[]) field.get(a); System.out.println(value);//Hello World! value[5] = '&'; System.out.println(value);//Hello&World! // 验证b、c是否被改变 System.out.println(b);//Hello&World! System.out.println(c);//Hello&World!
写到这里该如何引出不可变的好处呢?忘记反射吧,我们聊聊不可变的好处吧
不可变的优点
保证了线程安全
同一个字符串实例可以被多个线程共享。
保证了基本的信息安全
比如,网络通信的IP地址,类加载器会根据一个类的完全限定名来读取此类诸如此类,不可变性提供了安全性。
字符串缓存(常量池)的需要
具统计,常见应用使用的字符串中有大约一半是重复的,为了避免创建重复字符串,降低内存消耗和对象创建时的开销。JVM提供了字符串缓存的功能——字符串常量池。如果字符串是可变的,我们就可以通过引用改变常量池总的同一个内存空间的值,其他指向此空间的引用也会发生改变。
支持hash映射和缓存。
因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。
不可变的缺点
由于它的不可变性,像字符串拼接、裁剪等普遍性的操作,往往对应用性能有明显影响。
为了解决这个问题,java为我们提供了两种解决方案
- 字符串常量池
- StringBuilder、StringBuffer是可变的
字符串常量池
还是刚才反射的示例
String a = "Hello World!"; String b = new String("Hello World!"); String c = "Hello World!"; //判断字符串变量是否指向同一块内存 System.out.println(a == b); System.out.println(a == c); System.out.println(b == c); // 通过反射观察a, b, c 三者中变量value数组的真实位置 Field a_field = a.getClass().getDeclaredField("value"); a_field.setAccessible(true); System.out.println(a_field.get(a)); Field b_field = b.getClass().getDeclaredField("value"); b_field.setAccessible(true); System.out.println(b_field.get(b)); Field c_field = c.getClass().getDeclaredField("value"); c_field.setAccessible(true); System.out.println(c_field.get(c)); //通过反射发现String对象中变量value指向了同一块内存
输出
false true false [C@6f94fa3e [C@6f94fa3e [C@6f94fa3e
字符串常量的创建过程:
- 判断常量池中是否存在"Hello World!"常量,如果有直接返回该常量在池中的引用地址
- 如果没有,先创建一个
char["Hello World!".length()]
数组对象,然后在常量池中创建一个字符串对象并用数组对象初始化字符串对象的成员变量value,然后将这个字符串的引用返回,比如赋值给a
由此可见,a和c对象指向常量池中相同的内存空间不言自明。
而b对象的创建是建立在以上的创建过程的基础之上的。"Hello World!"
常量创建完成时返回的引用,会经过String
的构造函数。
public String(String original) { this.value = original.value; this.hash = original.hash; }
构造函数内部将引用的对象成员变量value
赋值给了内部成员变量value
,然后将新创建的字符创对象引用赋值给了b,这个过程发生在堆中。
再来感受下下面这两行代码有什么区别
String b = new String(a); String b = new String("Hello World!");
StringBuilder和StringBuffer
二者都是可变的
为了弥补String的缺陷,Java先后提供了StringBuffer和StringBuilder可变字符串类。
二者都继承至AbstractStringBuilder,AbstractStringBuilder使用了char[] value
字符数组
abstract class AbstractStringBuilder implements Appendable, CharSequence { /** * The value is used for character storage. */ char[] value; AbstractStringBuilder(int capacity) { value = new char[capacity]; } }
可以看出AbstractStringBuilder类和其成员变量value都没有使用final关键字。
value数组的默认长度
StringBuilder和StringBuffer的value数组默认初始长度是16
public StringBuilder() { super(16); } public StringBuffer() { super(16); }
如果我们拼接的字符串长度大概是可以预计的,那么最好指定合适的capacity,避免多次扩容的开销。
扩容产生多重开销:抛弃原有数组,创建新的数组,进行arrycopy。
二者的区别
StringBuilder是非线程安全的,StringBuffer是线程安全的。
StringBuffer类中的方法使用了synchronized
同步锁来保证线程安全。
关于锁的话题非常大,会单独成文来说明,这里推荐一篇不错的博客,有兴趣的可以看看
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
MyBatis 缓存
一级缓存 对于一级缓存来说,Mybatis是直接单个线程隔离的在执行add,update,delete 的时候,会自动清空缓存,避免脏读造成的影响此时mapper为线程隔离的,而管理对象为所有线程所共享的. 修改展示层 <%@ page import="org.apache.ibatis.session.SqlSession" %> <%@ page import="com.ming.Util.SqlSessionFactoryUtil" %> <%@ page import="com.ming.MyBatis.RoleMapper" %> <%@ page import="java.util.List" %> <%@ page import="java.util.Iterator" %> <%@ page import="com.ming.MyBatis.POJO.Student" %> <html> <body> <h2>Hello World!</h2> <...
- 下一篇
MyBatis 二级缓存
二级缓存 需要在映射文件中添加该标签 <cache/> 映射语句中的select语句将会被缓存, 映射语句中的insert update delete 语句将会刷新缓存缓存使用LRU算法回收现在完整的配置文件如下 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- 定义接口类 --> <mapper namespace="com.ming.MyBatis.RoleMapper"> <resultMap type="role" id="roleMap"> <!-- id为主键映射关系 其中数据库中的id为主键 --> <id column="id" property="id" javaType="int" jdbcType="INTEGER"/>...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7设置SWAP分区,小内存服务器的救世主
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题