String 字符串的不可变性 定义一个字符串 String s = "abcd"; s 中保存了 string 对象的引用。下面的箭头可以理解为“存储他的引用”。 使用变量来赋值变量 String s2 = s; s2 保存了相同的引用值,因为他们代表同一个对象。 字符串连接 s = s.concat("ef"); s 中保存的是一个重新创建出来的 string 对象的引用。 总结 一旦一个 string 对象在内存(堆)中被创建出来,他就无法被修改。特别要注意的是, String 类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。 如果你需要一个可修改的字符串,应该使用 StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的 string 对象被创建出来。 JDK 6 和 JDK 7 中 substring 的原理及区别 String 是 Java 中一个比较基础的类,每一个开发人员都会经常接触到。而且, String 也是面试中经常会考的知识点。String 有很多方法,有些方法比较常用,有些方法不太常用。今天要介绍的 subString 就是一个比较常用的方法,而且围绕 subString 也有很多面试题。 substring(int beginIndex, int endIndex) substring(int beginIndex, int endIndex) 方法在不同版本的 JDK 中的实现是不同的。了解他们的区别可以帮助你更好的使用他。为简单起见,后文中用 substring()代表方法。 substring() 的作用 substring(int beginIndex, int endIndex) 方法截取字符串并返回其[beginIndex, endIndex-1]范围内的内容。 String x = "abcdef"; x = x.substring(1,3); System.out.println(x); 输出内容: bc 调用 substring()时发生了什么? 你可能知道,因为 x 是不可变的,当使用 x.substring(1,3)对 x 赋值的时候,它会指向一个全新的字符串: 然而,这个图不是完全正确的表示堆中发生的事情。因为在 jdk6 和 jdk7 中调用 substring 时发生的事情并不一样。 JDK 6 中的 substring String 是通过字符数组实现的。在 jdk 6 中,String 类包含三个成员变量: char value[] , int offset , int count 。他们分别用来存储真正的字符数组,数组的第一个位置索引以及字符串中包含的字符个数。 当调用 substring 方法的时候,会创建一个新的 string 对象,但是这个 string 的值仍然指向堆中的同一个字符数组。这两个对象中只有 count 和 offset 的值是不同的。 下面是证明上说观点的 Java 源码中的关键代码: //JDK 6 String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; } public String substring(int beginIndex, int endIndex) { //check boundary return new String(offset + beginIndex, endIndex - beginIndex, value); } JDK 6 中的 substring 导致的问题 如果你有一个很长很长的字符串,但是当你使用 substring 进行切割的时候你只需要很短的一段。这可能导致性能问题,因为你需要的只是一小段字符序列,但是你却引用了整个字符串(因为这个非常长的字符数组一直在被引用,所以无法被回收,就可能导致内存泄露)。在 JDK 6 中,一般用以下方式来解决该问题,原理其实就是生成一个新的字符串并引用他。 x = x.substring(x, y) + "" 关于 JDK 6 中 subString 的使用不当会导致内存系列已经被官方记录在 Java Bug Database 中: 内存泄露:在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。 内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。 JDK 7 中的 substring 上面提到的问题,在 jdk 7 中得到解决。在 jdk 7 中,substring 方法会在堆内存中创建一个新的数组。 Java 源码中关于这部分的主要代码如下: //JDK 7 public String(char value[], int offset, int count) { //check boundary this.value = Arrays.copyOfRange(value, offset, offset + count); } public String substring(int beginIndex, int endIndex) { //check boundary int subLen = endIndex - beginIndex; return new String(value, beginIndex, subLen); } new String 以上是 JDK 7 中的 subString 方法,其使用创建了一个新字符串,避免对老字符串的引用。从而解决了内存泄露问题。 所以,如果你的生产环境中使用的 JDK 版本小于 1.7,当你使用 String 的 subString 方法时一定要注意,避免内存泄露。 replaceFirst、replaceAll、replace 区别 replace、replaceAll 和 replaceFirst 是 Java 中常用的替换字符的方法,它们的方法定义是: replace(CharSequence target, CharSequence replacement) ,用 replacement 替换所有的 target,两个参数都是字符串。 replaceAll(String regex, String replacement) ,用 replacement 替换所有的 regex 匹配项,regex 很明显是个正则表达式,replacement 是字符串。 replaceFirst(String regex, String replacement) ,基本和 replaceAll 相同,区别是只替换第一个匹配项。 可以看到,其中 replaceAll 以及 replaceFirst 是和正则表达式有关的,而 replace 和正则表达式无关。 replaceAll 和 replaceFirst 的区别主要是替换的内容不同,replaceAll 是替换所有匹 配的字符,而 replaceFirst()仅替换第一次出现的字符。 用法例子 以下例子参考:http://www.51gjie.com/java/771.html 1、 replaceAll() 替换符合正则的所有文字 //文字替换(全部) Pattern pattern = Pattern.compile("正则表达式"); Matcher matcher = pattern.matcher("正则表达式 Hello World,正则表达式 Hello World "); //替换第一个符合正则的数据 System.out.println(matcher.replaceAll("Java")); 2、 replaceFirst() 替换第一个符合正则的数据 //文字替换(首次出现字符) Pattern pattern = Pattern.compile("正则表达式"); Matcher matcher = pattern.matcher("正则表达式 Hello World,正则表达式 Hello World "); //替换第一个符合正则的数据 System.out.println(matcher.replaceFirst("Java")); 71 3、 replaceAll()替换所有 html 标签 //去除html标记 Pattern pattern = Pattern.compile("", Pattern.DOTALL); Matcher matcher = pattern.matcher("主页"); String string = matcher.replaceAll(""); System.out.println(string); 4、 replaceAll() 替换指定文字 //替换指定{}中文字 String str = "Java目前的发展史是由{0}年-{1}年"; String[][] object = { new String[] { "\\{0\\}", "1995" }, new String[] { "\\{1\\}", "2007" } }; System.out.println(replace(str, object)); public static String replace(final String sourceString, Object[] object) { String temp = sourceString; for (int i = 0; i < object.length; i++) { String[] result = (String[]) object[i]; Pattern pattern = Pattern.compile(result[0]); Matcher matcher = pattern.matcher(temp); temp = matcher.replaceAll(result[1]); } return temp; } 5、 replace()替换字符串 System.out.println("abac".replace("a", "\a")); //\ab\ac String 对“+”的重载 1、 String s = "a" + "b",编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),即变成 String s = "ab" 2、 对于能够进行优化的(String s = "a" + 变量 等)用 StringBuilder 的 append() 方法替代,最后调用 toString() 方法 (底层就是一个 new String()) 字符串拼接的几种方式和区别 字符串,是 Java 中最常用的一个数据类型了。 本文,也是对于 Java 中字符串相关知识的一个补充,主要来介绍一下字符串拼接相关的知识。本文基于 jdk1.8.0_181。 字符串拼接 字符串拼接是我们在 Java 代码中比较经常要做的事情,就是把多个字符串拼接到一起。 我们都知道,String 是 Java 中一个不可变的类,所以他一旦被实例化就无法被修改。 不可变类的实例一旦创建,其成员变量的值就不能被修改。这样设计有很多好处,比如可以缓存 hashcode、使用更加便利以及更加安全等。 但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢? 字符串不变性与字符串拼接 其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码: 其实最后我们得到的 s 已经是一个新的字符串了。如下图: s 中保存的是一个重新创建出来的 String 对象的引用。 那么,在 Java 中,到底如何进行字符串拼接呢?字符串拼接有很多种方式,这里简单介绍几种比较常用的。 使用+拼接字符串 在 Java 中,拼接字符串最简单的方式就是直接使用符号+来拼接。如: String wechat = "Hollis"; String introduce = "每日更新Java相关技术文章"; String hollis = wechat + "," + introduce; 这里要特别说明一点,有人把 Java 中使用+拼接字符串的功能理解为运算符重载。其实并不是,Java 是不支持运算符重载的。这其实只是 Java 提供的一个语法糖。后面再详细介绍。 运算符重载:在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。 语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。 Concat 除了使用+拼接字符串之外,还可以使用 String 类中的方法 concat 方法来拼接字符串。如:
String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章"; String hollis = wechat.concat(",").concat(introduce); StringBuffer 关于字符串,Java 中除了定义了一个可以用来定义字符串常量的 String 类以外,还 提供了可以用来定义字符串变量的 StringBuffer 类,它的对象是可以扩充和修改的。使用 StringBuffer 可以方便的对字符串进行拼接。如:
StringBuffer wechat = new StringBuffer("Hollis");
String introduce = "每日更新Java相关技术文章"; StringBuffer hollis = wechat.append(",").append(introduce); StringBuilder 除了 StringBuffer 以外,还有一个类 StringBuilder 也可以使用,其用法和 StringBuffer 类似。如:
StringBuilder wechat = new StringBuilder("Hollis");
String introduce = "每日更新Java相关技术文章"; StringBuilder hollis = wechat.append(",").append(introduce); StringUtils.join 除了 JDK 中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,如 apache.commons 中提供的 StringUtils 类,其中的 join 方法可以拼接字符串。 String wechat = "Hollis"; String introduce = "每日更新Java相关技术文章"; System.out.println(StringUtils.join(wechat, ",", introduce)); 这里简单说一下,StringUtils 中提供的 join 方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,如: String []list ={"Hollis","每日更新Java相关技术文章"}; String result= StringUtils.join(list,","); System.out.println(result); //结果:Hollis,每日更新Java相关技术文章 并 且 , J a v a 8 中 的 S t r i n g 类 中 也 提 供 了 一 个 静 态 的 j o i n 方 法 , 用 法 和 StringUtils.join 类似。 以上就是比较常用的五种在 Java 种拼接字符串的方式,那么到底哪种更好用呢?为什么 Java 开发手册中不建议在循环体中使用+进行字符串拼接呢? 使用+拼接字符串的实现原理 前面提到过,使用+拼接字符串,其实只是 Java 提供的一个语法糖, 那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。 还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。 String wechat = "Hollis"; String introduce = "每日更新Java相关技术文章"; String hollis = wechat + "," + introduce; 反编译后的内容如下,反编译工具为 jad。
String wechat = "Hollis";
String introduce = "\u6BCF\u65E5\u66F4\u65B0Java\u76F8\u5173\u6280\u672F\u6
587\u7AE0";//每日更新Java相关技术文章
String hollis = (new StringBuilder()).append(wechat).append(",").append(int
roduce).toString(); 通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将 String 转成了 StringBuilder 后,使用其 append 方法进行处理的。 那 么 也 就 是 说 , J a v a 中 的 + 对 字 符 串 的 拼 接 , 其 实 现 原 理 是 使 用 StringBuilder.append。 concat 是如何实现的 我们再来看一下 concat 方法的源代码,看一下这个方法又是如何实现的。
public String concat(String str) int otherLen = str.length();
if (otherLen == 0) {
return this; }
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); } 这段代码首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的 String 对象并返回。 通过源码我们也可以看到,经过 concat 方法,其实是 new 了一个新的 String,这也就呼应到前面我们说的字符串的不变性问题上了。 StringBuffer 和 StringBuilder 接下来我们看看 StringBuffer 和 StringBuilder 的实现原理。 和 String 类类似,StringBuilder 类也封装了一个字符数组,定义如下: char[] value; 与 String 不同的是,它并不是 final 的,所以他是可以修改的。另外,与 String 不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下: int count; 其 append 源码如下: 该类继承了 AbstractStringBuilder 类,看下其 append 方法: append 会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。 StringBuffer 和 StringBuilder 类似,最大的区别就是 StringBuffer 是线程安全的,看一下 StringBuffer 的 append 方法。 StringBuilder 该方法使用 synchronized 进行声明,说明是一个线程安全的方法。而则不是线程安全的。 StringUtils.join 是如何实现的 StringBuilder 通过查看 StringUtils.join 的源代码,我们可以发现,其实他也是通过来实现的。 效率比较 既然有这么多种字符串拼接的方法,那么到底哪一种效率最高呢?我们来简单对比一下。 我们使用形如以上形式的代码,分别测试下五种字符串拼接代码的运行时间。得到结果如下: 从结果可以看出,用时从短到长的对比是: StringBuilder
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?


微信扫码登录