1 String
1.1 String类的属性
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 用于存储字符串
private final char value[];
// 缓存字符串的哈希码
private int hash; // Default to 0
// 使用JDK 1.0.2中的serialVersionUID进行互操作
private static final long serialVersionUID = -6849794470754667710L;
// 将String实例写入ObjectOutputStream中,类字符串在序列化流协议中是特殊情况,根据<a href={@docRoot}..platformserializationspecoutput.html">对象序列化规范
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
}
char value[]
String底层的存储结构是一个字符类型的数组,同样也是被final修饰,因此一旦这个字符数组被创建后,value变量不可再指向其他数组。
但是可以改变value数组中某一个元素的值。
int hash
hash用来保存某一个String实例自己的哈希值,可以说是哈希值的一个缓存,因此String特别适合放入HashMap中,作为key来使用。
每次插入一个键值対时,不需要重新计算key的哈希值,直接取出key的缓存hash值即可,在一定程度上,加快了HashMap的效率。
long serialVersionUID
用于保证版本一致性,由于String实现了Serializable接口,因此需要拥有一个序列化的ID。
序列化时,将此ID与对象一并写入到文件中,反序列化时,检测该类中的ID与文件中的ID是否一致,一致的话,说明版本一致,序列化成功。
ObjectStreamField[] serialPersistentFields
通过定义ObjectStreamField [] serialPersistentFields并明确声明保存的特定字段。
“优势”是它在 Javadoc中执行的操作:定义哪些字段是序列化的,没有它,所有非瞬态非静态字段都会被序列化
final关键字
可以看到String类和char数组都被final修饰了!那么final的作用是什么呢?
① final修饰属性
- final修饰变量是我们用到最多的地方,被final修饰的变量,如果是基本数据类型的变量,则一旦被赋值后便不可更改。
- 如果是引用类型的变量,则一旦实例化对象后,便不可让这个变量指向其他对象,但是可以改变该对象里面的属性值
② final修饰方法
final关键字修饰父类的一个方法时,子类不可重写这个方法,提示重写的方法被父类final修饰。
所有的private方法其实都被隐式地声明为final,将父类的final方法修饰为private后,虽然方法不报错,但是子类依然没有重写eat()方法,而仅仅只是声明了一个新方法。
③ final修饰类
被final修饰的类不可以被继承,final类里面的成员方法都会被隐式地声明为final。
当我们写了一个类,却不希望别人对其进行任何改动时,我们就可以把该类声明为final。
String类就是被final修饰的。
1.2 String类的构造器
String的构造器非常多!
String比较重要的是4个构造方法
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
进入到Arrays.copyOf()方法中,发现调用的是System.arraycopy()方法,而System.arraycopy()方法是一个本地方法,由其他语言实现。
从以上源码,可以看得出,这个构造方法没有直接使用传入的字符数组的引用,而是使用该数组的一个拷贝,保证了String类的不可变性。
我们无法通过在外部改变此数组中某些元素的值,来改变构造后的String的值。
1.3 String常用方法
int length():返回字符串的长度: return value.length
char charAt(int index): 返回某索引处的字符return value[index]
boolean isEmpty():判断是否是空字符串:return value.length == 0
String toLowerCase():使用默认语言环境,将 String 中的所字符转换为小写
String toUpperCase():使用默认语言环境,将 String 中的所字符转换为大写
String trim():返回字符串的副本,忽略前导空白和尾部空白
boolean equals(Object obj):比较字符串的内容是否相同
boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”
int compareTo(String anotherString):比较两个字符串的大小串。
String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
注:indexOf和lastIndexOf方法如果未找到都是返回-1
替换:
String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所 oldChar 得到的。
String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所匹配字面值目标序列的子字符串。
String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所匹配给定的正则表达式的子字符串。
String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
匹配:
boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。
切片:
String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。
1.4 String面试经典
==
和equals
的区别?
- ==
== 对基本数据类型是比较内容值,而引用数据类型是比较引用值。
1 Object类的equals方法
public boolean equals(Object obj) {
return (this == obj);
}
可见Object下equals的效果就是 == 的效果
2 大多数类重写了equals方法
private final char value[];
public boolean equals(Object anObject) {
if (this == anObject) { //如果引用地址是否一致
return true;
}
if (anObject instanceof String) { //如果传进来的是String类型
String anotherString = (String)anObject; //向下转型
int n = value.length;
if (n == anotherString.value.length) { //如果当前字符串长度和被传入的字符串长度一致
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i]) //如果两者转化成的字符数组中某个索引位置的值不一样
return false;
i++;
}
return true;
}
}
return false;
}
Integer a = 127;
Integer b = 127;
System.out.println(a == b);
Integer c = 128;
Integer d = 128;
System.out.println(c == d);
测试结果:
true
false
因为 Integer 在常量池中的存储范围为[-128,127],127在这范围内,因此是直接存储于常量池的,而128不在这范围内。
所以会在堆内存中创建一个新的对象来保存这个值,所以m,n分别指向了两个不同的对象地址,故而导致了不相等。
代码示例:
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
System.out.println(s6 == s7);//false
String s8 = s6.intern();//返回值得到的s8使用的常量值中已经存在的“javaEEhadoop”
System.out.println(s3 == s8);//true
****************************
String s1 = "javaEEhadoop";
String s2 = "javaEE";
String s3 = s2 + "hadoop";
System.out.println(s1 == s3);//false
final String s4 = "javaEE";//s4:常量
String s5 = s4 + "hadoop";
System.out.println(s1 == s5);//true
小总结:
1.常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量。
2.只要其中一个是变量,结果就在堆中。
3.如果拼接的结果调用intern()方法,返回值就在常量池中
- equals
Java中String为什么是不可变的?
String s = "abcd";
System.out.println("s = " + s);
s = "1234";
System.out.println("s = " + s);
测试结果:
s = abcd
s = 1234
从打印结果可以看出,s的值确实改变了。那么怎么还说String对象是不可变的呢?
其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大。
引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。
也就是说,s只是一个引用,它指向了一个具体的对象,当s=“1234”; 这句代码执行过之后,又创建了一个新的对象“1234”。
而引用s重新指向了这个新的对象,原来的对象“abcd”还在内存中存在,并没有改变。
String ss = "123456";
System.out.println("ss = " + ss);
ss.replace('1', '0');
System.out.println("ss = " + ss);
测试结果:
ss = 123456
ss = 123456
那么ss的值看似改变了,其实也是同样的误区。
再次说明, ss只是一个引用, 不是真正的字符串对象,在调用ss.replace(‘1’, ‘0’)时, 方法内部创建了一个新的String对象,并把这个新的对象重新赋给了引用ss。
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 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++;
}
return new String(buf, true);
}
}
return this;
}
可以清楚地看到只是帮我们又new 了一个对象进行存放替换后的值。
String、Stringbuffer、StringBuilder三者的区别?
先直接上结论!
String | StringBuffer | StringBuilder |
---|---|---|
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 | StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 | 可变类,速度更快 |
不可变 | 可变 | 可变 |
线程安全 | 线程不安全 | |
适用于用在多线程操作同一个 StringBuffer 的场景 | 单线程场合 StringBuilder 更适合 |
- 线程安全问题
StringBuffer的所有公开方法都是 synchronized 修饰的,保证了线程的安全性。
StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。
StringBuffer 对缓存区优化,不过 StringBuffer 的这个toString 方法仍然是同步的,以至于性能稍慢一些。
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
StringBuilder并没有 synchronized 修饰。
StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}