我是一名很普通的双非大三学生,跟很多同学一样,有着一颗想进大厂的梦。接下来的几个月内,我将坚持写博客,输出知识的同时巩固自己的基础,记录自己的成长和锻炼自己,备战2021暑期实习面试!奥利给!!
以主流的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 s1 = "final";
s1 = "test";
通过Debug我们可以看到,实际上value数组的引用是改变了的,也就说 s =“test” 这个看似简单的赋值,其实已经把 s 的引用指向了新的 String。
首先,我们在编码中,我们知道 如果你认为这个类已经定义完全并且不需要任何子类的话,可以将这个类声明为Final,Final类中的方法将永远不会被重写 。在Java中也是这样,String是被设计成一个不可变(immutable)类,一旦创建完后,字符串本身是无法通过正常手段被修改的。
第三点注意下,我们说的是无法修改其内存地址,并没有说无法修改其值。因为对于 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的设计者,所以我们只能综合好处去概述这样做的原因:
但是不可变并不是完全不可变,如果你非要变,通过反射也是可以实现的,例如:
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类型是不可变的,所以在字符串拼接的时候如果使用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常见的创建方式有两种, 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规范中方法区的实现,它们之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
这里就不对方法的使用一一例举了,还是得多动手自己实践呢!
还有replaceFirst() 替换匹配到的第一个字符;
这里要注意replace和replaceAll的区别,前者的参数是char和CharSequence,即可以支持字符的替换,也支持字符串的替换(CharSequence即字符串序列的意思,说白了也就是字符串)
replaceAll的参数是regex,即基于规则表达式的替换,比如:可以通过replaceAll("/d", "*")把一个字符串所有的数字字符都换成星号;
== 对于基本数据类型来说,是用于比较"值” 是否相等的;但对于引|用类型来说,是用于比较引用地址是否相同的。那么我们就要看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的关系,下次专门在写
通过问题讲解知识点,看完这本文章,出几道题考考?