Java 语言基础
String
字符串的不可变性
定义一个字符串
使用变量来赋值变量
String s2 = s;
s2 保存了相同的引用值, 因为他们代表同一个对象
字符串连接
s = s.concat("ef");
s 中保存的是一个重新创建出来的 string 对象的引用
总结
一旦一个 string 对象在内存(堆)中被创建出来, 他就无法被修改。特别要注意的是,String 类的所有方法都没有改变字符串本身的值, 都是返回了一个新的对象。
如果你需要一个可修改的字符串, 应该使用 StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上, 因为每次试图修改都有新的 string 对象被创建出来
JDK 6 和 JDK 7 中 substring 的原理及区别
调用 substring()时发生了什么?
你可能知道, 因为 x 是不可变的, 当使用 x.substring(1,3)对 x 赋值的时候, 它会指向一个全新的字符串:
JDK 6 中的 substring
String 是通过字符数组实现的。在 jdk 6 中, String 类包含三个成员变量:char value[], int offset, int count。他们分别用来存储真正的字符数组, 数组的第一个位置索引以及字符串中包含的字符个数。
String 是通过字符数组实现的。在 jdk 6 中, String 类包含三个成员变量:char value[], int offset, int count。他们分别用来存储真正的字符数组, 数组的第一个位置索引以及字符串中包含的字符个数。
JDK 6 中的 substring 导致的问题
如果你有一个很长很长的字符串, 但是当你使用 substring 进行切割的时候你只需要很短的一段。这可能导致性能问题, 因为你需要的只是一小段字符序列, 但是你却引用了整个字符串( 因为这个非常长的字符数组一直在被引用, 所以无法被回收, 就可能导致内存泄露) 。在 JDK 6 中, 一般用以下方式来解决该问题, 原理其实就是生成一个新的字符串并引用他。
内存泄露:在计算机科学中, 内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失, 而是应用程序分配某段内存后, 由于设计错误, 导致在释放该段内存之前就失去了对该段内存的控制, 从而造成了内存的浪费
JDK 7 中的 substring
上面提到的问题, 在 jdk 7 中得到解决。在 jdk 7 中, substring 方法会在堆内存中创建一个新的数组。
以上是 JDK 7 中的 subString 方法, 其使用 new String 创建了一个新字符串, 避免对老字符串的引用。从而解决了内存泄露问题。
所以, 如果你的生产环境中使用的 JDK 版本小于 1.7, 当你使用 String 的 subString方法时一定要注意, 避免内存泄露。
replaceFirst、 replaceAll、 replace 区别
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 和正则表达式无关。
http://www.51gjie.com/java/771.html
String 对“ +” 的重载
1、 String s = "a" + "b", 编译器会进行常量折叠(因为两个都是编译期常量, 编译期可知), 即变成 String s = "ab
2、 对于能够进行优化的(String s = "a" + 变量 等)用 StringBuilder 的 append()方法替代, 最后调用 toString() 方法 (底层就是一个 new String())
字符串拼接的几种方式和区别
字符串不变性与字符串拼接
其实, 所有的所谓字符串拼接, 都是重新生成了一个新的字符串。下面一段字符串拼接代码:String s = "abcd";s = s.concat("ef");其实最后我们得到的 s 已经是一个新的字符串了。
使用+拼接字符串
运算符重载:在计算机程序设计中, 运算符重载( 英语:operator overloading) 是多态的一种。运算符重载, 就是对已有的运算符重新进行定义, 赋予其另一种功能, 以适应不同的数据类型。
语法糖:语法糖( Syntactic sugar), 也译为糖衣语法, 是由英国计算机科学家彼得· 兰丁发明的一个术语, 指计算机语言中添加的某种语法, 这种语法对语言的功能没有影响, 但是更方便程序员使用。语法糖让程序更加简洁, 有更高的可读性。
Concat
StringBuffer
StringBuilder
StringUtils.join
除了 JDK 中内置的字符串拼接方法, 还可以使用一些开源类库中提供的字符串拼接方法名, 如 apache.commons 中提供的 StringUtils 类, 其中的 join 方法可以拼接字符串。String wechat = "Hollis";String introduce = "每日更新 Java 相关技术文章";System.out.println(StringUtils.join(wechat, ",", introduce));
以上就是比较常用的五种在 Java 种拼接字符串的方式, 那么到底哪种更好用呢?为什么 Java 开发手册中不建议在循环体中使用+进行字符串拼接呢?
使用+拼接字符串的实现原理
通过查看反编译以后的代码, 我们可以发现, 原来字符串常量在拼接过程中, 是将String 转成了 StringBuilder 后, 使用其 append 方法进行处理的。
那 么 也 就 是 说 , J a v a 中 的 + 对 字 符 串 的 拼 接 , 其 实 现 原 理 是 使 用StringBuilder.append
concat 是如何实现的
concat 方法的源代码
这段代码首先创建了一个字符数组, 长度是已有字符串和待拼接字符串的长度之和, 再把两个字符串的值复制到新的字符数组中, 并使用这个字符数组创建一个新的 String 对象并返回。
通过源码我们也可以看到, 经过 concat 方法, 其实是 new 了一个新的 String, 这也就呼应到前面我们说的字符串的不变性问题上了。
StringBuffer 和 StringBuilder
append 会直接拷贝字符到内部的字符数组中, 如果字符数组长度不够, 会进行扩展。
StringBuffer 和 StringBuilder 类似, 最大的区别就是 StringBuffer 是线程安全的,看一下 StringBuffer 的 append 方法。
StringUtils.join 是如何实现的
通过查看 StringUtils.join 的源代码, 我们可以发现, 其实他也是通过 StringBuilder来实现的。
效率比较
从结果可以看出, 用时从短到长的对比是:StringBuilderStringBuffer 在 StringBuilder 的基础上, 做了同步处理, 所以在耗时上会相对多一些。
StringUtils.join 也是使用了 StringBuilder, 并且其中还是有很多其他操作, 所以耗时较长, 这个也容易理解。其实 StringUtils.join 更擅长处理字符串数组或者列表的拼接。
那么问题来了, 前面我们分析过, 其实使用+拼接字符串的实现原理也是使用的StringBuilder, 那为什么结果相差这么多, 高达 1000 多倍呢?
我们可以看到反编译后的代码, 在 for 循环中, 每次都是 new 了一个 StringBuilder,然后再把 String 转成 StringBuilder, 再进行 append。而频繁的新建对象当然要耗费很多时间了, 不仅仅会耗费时间, 频繁的创建对象, 还会造成内存资源的浪费。
所以, Java 开发手册建议:循环体内, 字符串的连接方式, 使用 StringBuilder 的append 方法进行扩展。而不要使用+。
总结
本文介绍了什么是字符串拼接, 虽然字符串是不可变的, 但是还是可以通过新建字符串的方式来进行字符串的拼接。常用的字符串拼接方式有五种, 分别是使用+、 使用 concat、 使用 StringBuilder、 使用 StringBuffer 以及使用 StringUtils.join。由于字符串拼接过程中会创建新的对象, 所以如果要在一个循环体中进行字符串拼接,就要考虑内存问题和效率问题。
因此, 经过对比, 我们发现, 直接使用 StringBuilder 的方式是效率最高的。因为StringBuilder 天生就是设计来定义可变字符串和字符串的变化操作的。但是, 还要强调的是:1、 如果不是在循环体中进行字符串拼接的话, 直接使用+就好了。2 、 如 果 在 并 发 场 景 中 进 行 字 符 串 拼 接 的 话 , 要 使 用 St r i n g B u f f e r 来 代 替StringBuilder。
String.valueOf 和 Integer.toString 的区别
String.valueOf 和 Integer.toString 的区别
1.int i = 5;2.String i1 = "" + i;3.String i2 = String.valueOf(i);4.String i3 = Integer.toString(i);第三行和第四行没有任何区别, 因为 String.valueOf(i)也是调用 Integer.toString(i)来实现的。第二行代码其实是 String i1 = (new StringBuilder()).append(i).toString();, 首先创建一个 StringBuilder 对象, 然后再调用 append 方法, 再调用 toString 方法。
字符串池
在 JVM 中, 为了减少相同的字符串的重复创建, 为了达到节省内存的目的。会单独开辟一块内存, 用于保存字符串常量, 这个内存区域被叫做字符串常量池
当代码中出现双引号形式( 字面量) 创建字符串对象时, JVM 会先对这个字符串进行检查, 如果字符串常量池中存在相同内容的字符串对象的引用, 则将这个引用返回;否则,创建新的字符串对象, 然后将这个引用放入字符串常量池, 并返回该引用
这种机制, 就是字符串驻留或池化
字符串常量池的位置
在 JDK 7 以前的版本中, 字符串常量池是放在永久代中的。
因为按照计划, JDK 会在后续的版本中通过元空间来代替永久代, 所以首先在 JDK7 中, 将字符串常量池先从永久代中移出, 暂时放到了堆内存中。在 JDK 8 中, 彻底移除了永久代, 使用元空间替代了永久代, 于是字符串常量池再次从堆内存移动到永久代中
Class 常量池
谈到常量池, 在 Java 体系中, 共用三种常量池。分别是字符串常量池、 Class 常量池和运行时常量池。
Class 常量池可以理解为是 Class 文件中的资源仓库。Class 文件中除了包含类的版本、 字段、 方法、 接口等描述信息外, 还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
常量池中有什么
字面量
用于表达源代码中一个固定值的表示法( notation)
字面量只可以右值出现, 所谓右值是指等号右边的值, 如:int a=123 这里的 a 为左值, 123 为右值。在这个例子中 123 就是字面量
符号引用
符号引用是编译原理中的概念, 是相对于直接引用来说的。主要包括了以下三类常量:* 类和接口的全限定名 * 字段的名称和描述符 * 方法的名称和描述符
Class 常量池有什么用
Class 常量池是 Class 文件中的资源仓库, 其中保存了各种常量。而这些常量都是开发者定义出来, 需要在程序的运行期使用的
Class 是用来保存常量的一个媒介场所, 并且是一个中间场所。在 JVM 真的运行时, 需要把常量池中的常量加载到内存中
运行时常量池
它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池扮演了类似传统语言中符号(SymbolTable)的角色,不过它存储数据范围比通常意义上的符号表要更为广泛
每一个运行时常量池都分配在 Java 虚拟机的方法区之中, 在类和接口被加载到虚拟机后, 对应的运行时常量池就被创建出来
intern (运行时)
在 JVM 中, 为了减少相同的字符串的重复创建, 为了达到节省内存的目的。会单独开辟一块内存, 用于保存字符串常量, 这个内存区域被叫做字符串常量池。
intern 的功能很简单:在每次赋值的时候使用 String 的 intern 方法, 如果常量池中有相同值, 就会重复使用该对象, 返回对象引用。
String 的长度限制
那么, 明明 String 的构造函数指定的长度是可以支持 2147483647(2^31 - 1)的, 为什么像以上形式定义的时候无法编译呢?其实, 形如 String s = "xxx";定义 String 的时候, xxx 被我们称之为字面量, 这种字面量在编译之后会以常量的形式进入到 Class 常量池
常量池限制
字符串有长度限制, 在编译期, 要求字符串常量池中的常量不能超过 65535, 并且在javac 执行过程中控制了最大值为 65534。在运行期, 长度不能超过 Int 的范围, 否则会抛异常
运行期限制
前面提到的这种 String 长度的限制是编译期的限制, 也就是使用 String s= “ ” ;这种字面值方式定义的时候才会有的限制。
那么。String 在运行期有没有限制呢, 答案是有的, 就是我们前文提到的那个 Integer.MAX_VALUE , 这个值约等于 4G, 在运行期, 如果 String 的长度超过这个范围, 就可能会抛出异常
总结
字符串有长度限制, 在编译期, 要求字符串常量池中的常量不能超过 65535, 并且在javac 执行过程中控制了最大值为 65534。在运行期, 长度不能超过 Int 的范围, 否则会抛异常。
最后, 这个知识点 , 我录制了视频(https://www.bilibili.com/video/BV1uK4y1t7H1/),
其中有关于如何进行实验测试、如何查阅 Java 规范以及如何对 javac 进行 deubg 的技巧。欢迎进一步学习。
这是笔记!
作者 Hollis:https://github.com/hollischuang