话不多说,我们来看大家最常用到的String类型
String 是如何实现的?它有哪些重要的方法?
以主流的JDK版本1.8来说,String内部实际存储结构为char数组,源码如下:
public final class String implements java.io.Serializable,Comparable<String>,CharSequence{...
/** The value is used for character storage. */
private final char value[];
/** 用来缓存Hash,避免每次都需要去重复计算 */
private int hash; // Default to 0
...
注意:JDK9以后,不再是char[]数组了,而是使用byte数组,因为可以减少一半的内存,byte使用一个字节来存储一个char字符,char使用两个字节来存储一个char字符。只有当一个char字符大小超过0xFF时,才会将byte数组变为原来的两倍,用两个字节存储一个char字符。
这里我们可以看到String类型其实是被final关键字
修饰的,这也是我们要探究的第一个问题
为什么String要用final修饰的,它的好处在哪?
我们先来看一段简短的代码
String s1 = "final";
s1 = "test";
通过Debug我们可以看到,实际上value数组的引用是改变了的,也就说 s =“test” 这个看似简单的赋值,其实已经把 s 的引用指向了新的 String。
首先,我们在编码中,我们知道如果你认为这个类已经定义完全并且不需要任何子类的话,可以将这个类声明为Final,Final类中的方法将永远不会被重写。在Java中也是这样,String是被设计成一个不可变(immutable)类,一旦创建完后,字符串本身是无法通过正常手段被修改的。
Final关键字知识点补充,final 的意思是不变的,一般来说用于以下三种场景:
- 被 final 修饰的类,表明该类是无法继承的;
- 被 final修饰的方法,表明该方法是无法覆写的;
- 被 final 修饰的变量,说明该变量在声明的时候,就必须初始化完成,而且以后也不能修改其内存地址。
第三点注意下,我们说的是无法修改其内存地址,并没有说无法修改其值。因为对于 List、Map 这些集合类来 说,被 final修饰后,是可以修改其内部值的,但却无法修改其初始化时的内存地址。
例子我们就不举了,本文的String 的不变性就是一个很好的例子。因为 String 具有不变性,所以 String 的大多数操作方法,都会返回新的 String,这里我们可以参考substring
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
我们可以看到,返回的其实是一个新的String,不会让你去修改内部的内容。
但是这还是没点到为什么要设计成不可变类的,我们并不是Java的设计者,所以我们只能综合好处去概述这样做的原因:
- 可以实现多个变量引用JVM内存中的同一个字符串实例,也就是字符串常量池 String Pool。
- 增强安全性,我们不如反向举例,如果String是可变的,那么会带来什么问题
- 比如Map的key,其实Map的key也是需要用不可变类的,不然就会出现找不到key等问题。
- 当你在调用其他方法,比如调用一些系统级操作之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,其内部的值被改变了,可能引起严重的系统崩溃问题。
- 提高效率,我们知道在程序中会大量用到String,正如上第一点实现了池以后,避免了每次都是堆中创建对象,可以提高效率,且由于String的不可变性,可以只计算一次哈希值,然后缓存在内部,后续直接取就好了,所以在Hash操作也能提高效率。
但是不可变并不是完全不可变,如果你非要变,通过反射也是可以实现的,例如:
String str = "不可变";
System.out.println(str);
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[0] = '可';
value[1] = '以';
value[2] = '变';
System.out.println(str);
输出结果:
不可变
可以变
String 和 StringBuilder、StringBuffer 的区别
因为String类型是不可变的,所以在字符串拼接的时候如果使用String的话性能会很低,因此我们就需要使用另外的数据类型StringBuffer
,它提供了append和insert方法可用于字符串的拼接,StringBuffer使用synchronized来保证线程安全, 所以性能不是很高。
列举一个代码片段:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
于是在JDK 1.5有了StringBuilder,它同样提供了append和insert的拼接方法,但它没有使用synchronized来修饰,因此在性能上要优于StringBuffer,所以在非并发操作的情况下可以使用后者。
这里还可以扩展一个什么情况下用+号,什么时候用StringBuilder、StringBuffer
String类型的常见操作
1、String 类型在 JVM(Java 虚拟机)中是如何存储的?
String常见的创建方式有两种, String s1 = "Java"
和 String s2 = new String("Java")
的方式,两者在JVM的存储区域却截然不同,在JDK 1.8中,s1会先去字符串常量池中找字符串"Java” ,如果有相同的字符则直接返回常量句柄,如果没有此字符串则会先在常量池中创建此字符串,然后再返回常量句柄;而变量s2是直接在堆上创建一个变量 ,如果调用intern方法
才会把此字符串保存到常量池中,intern
还是很少用到的,一般也只出现在面试题里,不用深究。如下代码所示:
String s1 = new String("南街");
String s2 = s1.intern();
String s3 = "南街";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true
在 JDK 8 之后,取消了永久代的概念,取而代之的实现是元空间(MetaSpace),原本位于永久代中的字符串常量由永久代转移到堆中。元空间的本质和永久代类似,都是对JVM规范中方法区的实现,它们之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
2、String常用方法
这里就不对方法的使用一一例举了,还是得多动手自己实践呢!
- indexOf():查询字符串首次出现的下标位置
- lastIndexOf():查询字符串最后出现的下标位置
- contains():查询字符串中是否包含另一个字符串
- toLowerCase():把字符串全部转换成小写
- toUpperCase():把字符串全部转换成大写
- length():查询字符串的长度
- trim():去掉字符串首尾空格
- replace():替换字符串中的某些字符
还有replaceFirst() 替换匹配到的第一个字符;
这里要注意replace和replaceAll的区别,前者的参数是char和CharSequence,即可以支持字符的替换,也支持字符串的替换(CharSequence即字符串序列的意思,说白了也就是字符串)
replaceAll的参数是regex,即基于规则表达式的替换,比如:可以通过replaceAll("\d", "*")把一个字符串所有的数字字符都换成星号;
- split():把字符串分割并返回字符串数组,split也是正则匹配
- join0: 把字符串数组转为字符串.
== 和 equals 的区别
== 对于基本数据类型来说,是用于比较"值” 是否相等的;但对于引|用类型来说,是用于比较引用地址是否相同的。那么我们就要看equals和它的区别了,我们知道Java里Object类是所有类的父类,equals方法也是Object的方法,源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
显而易见,没有重写的equals方法本质上也就是 == ,但是我们知道String类型在比较的时候,老师都教过,用equals,这是为什么呢?就是因为String已经重写equals方法,源码如下:
public boolean equals(Object anObject) {
// 先判断引用
if (this == anObject) {
return true;
}
// 在判断是不是String类型
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// char 一个一个对比
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
这里不深究hashCode和equals的关系,下次专门在写
小结
通过问题讲解知识点,看完这本文章,出几道题考考?
- 为什么String 类型要用final修饰?
- ==和equals的区别是什么?
- String和StringBuilder. StringBuffer 有什么区别?
- String的intern()方法有什么含义?
- String类型在JVM (Java 虚拟机)中是如何存储的?编译器对String做了哪些优化?
- String 一些常用操作问题,如问如何分割、合并、替换、删除、截取等等问题
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。