String类的介绍
- String类实现了Serializable, Comparable, CharSequence接口。
- String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。
- String是一个不可变对象,它是一个不可变的字符数组。
- 因为String类不可变,所以是多线程安全的。
- 因为String类不可变,所以保证了hash码的唯一性,因此可以放心地进行缓存。
1、String类构造方法源码分析(从JDK1.0版本开始)
//字符串是常量,它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。
String s="abc";//因为在实际开发中,大部分时间,开发人员都是和字符串打交道,所以默认给出一种简单的创建方法
//String s="abc" 等效于:
char[] charArray = {
'a','b','c'};
String s = new String(charArray);//每一个对象都会增加内存 所以要尽量少创建对象 节省内存
//其他创建String对象的构造方法:
char[] a = {
'a','b','c'};
String s03 = new String(new Char[]{
'a','b','c'},1,2);//等效于String s03 = new String(a,1,2)
(1)源码分析:
-
当传的值为字符串时
String s = new String("abc"); //对右边的String按ctrl键进入String构造函数源码 public String(String original) { this.value = original.value; //把传进来的original的value赋值到自身的value,这里的value是一个char value[]数组 this.hash = original.hash; //默认int hash等于0,hash是为了判定这个对象是不是唯一的,就是对象的地址 }
-
当没有传值时
String s1 = new String(); //对右边的String按ctrl键进入String构造函数源码 public String() { this.value = "".value; //把空的字符串的value赋值到自身的value,这里的value是一个char value[]数组 }
-
当传的值为char数组时
String s1 = new String(new char[]{ 'a','b','c'}; //对右边的String按ctrl键进入String构造函数源码 public String(char value[]) { this.value = Arrays.copyOf(value, value.length);//复制一份一模一样的数组赋值到自身的value }
-
当传的值为char数组,从第几个下标开始,子数组的长度时
String s1 = new String(new char[]{ 'a','b','c'},1,2); //对右边的String按ctrl键进入String构造函数源码 public String(char value[], int offset, int count) { if (offset < 0) { //如果开始的下标小于0,则抛出字符串下标越界异常 throw new StringIndexOutOfBoundsException(offset); } if (count <= 0) { if (count < 0) { //如果子数组的长度小于0,则抛出字符串下标越界异常 throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { //因为count<=0 所以取得的子数组为空 返回空 this.value = "".value; return; } } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { //假设数组长度为3,count为2,即子数组开始的下标最大为3-2=1,所以offset>value.length - count就会抛出字符串下标越界异常 throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count);//copyOfRange方法:给我一个value数组,把这个value数组从指定下标offset到指定下标offset+count的这个范围拆分下来,得到一个子数组 }
(2)不可变性
//1、当数组长度一致时
String s = "123";
s = "456";
s = "789";
System.out.println(s);//输出:789
//分析:当s = "123"时,s会形成一个final数组,即不可变形态。
//当s = "456"时,会先检查s原来的数组的长度,检查为长度一致,所以会直接覆盖原先的s,此时s输出为456
//当s = "789"时,会先检查s原来的数组的长度,检查为长度一致,所以会直接覆盖原先的s,此时s输出为789
//2、当数组长度不一致时
String s = "123";
s = "4567";
s = "78901";
System.out.println(s);//输出:78901
//分析:当s = "123"时,s会形成一个final数组,即不可变形态。
//当s = "456"时,会先检查s原来的数组的长度,检查为长度不一致,重新生成了一个新的数组,再装载新的元素
//当s = "789"时,会先检查s原来的数组的长度,检查为长度不一致,重新生成了一个新的数组,再装载新的元素
//测试String对象用"+"拼接字符串性能
long startTime=System.currentTimeMillis(); //生成当前时间戳(单位:ms)
String s = "";
for(int i = 0 ; i < 100000 ; i++){
s = s + "1";
}
System.out.println(System.currentTimeMillis()-startTime);
//输出:3092
//当i<200000 输出13564
//假设此时有10万用户使用需要拼接一个字符串,需要大概等待3秒(电脑性能不同,结果不同),20万时大概需要等待13秒,以此类推,越来越久。
总结:因为String对象的不可变性,在拼接字符串的时候会生成新的数组,消耗大量内存,而且手动释放不了,垃圾回收效率低,所以String对象处理拼接字符串的性能极低,所以要避免多个用户频繁修改同一个字符串变量。
2、一些常用的相关方法
(1) charAt(int index)方法:返回指定索引处的 char 值。返回char
//索引 == 下标 字符串也是从0下标开始的
char[] c = {
'a','b','c','d','e'};
String s = new String(c);
char c1 = s.charAt(1);//charAt(1) 这里的1是下标
System.out.println(c1);//输出: b
//
//源码分析
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
//index即索引,如果索引值小于0或者大于value数组的长度的话 就会抛出字符串越界异常
(2) concat(String str)方法:将指定字符串连接到此字符串的结尾。 返回String
//拼接字符串的一般方法:
String s01 = "123";
String s02 = "456";
String s03 = s01 + s02;
System.out.println(s03);//输出:123456
//concat方法:
String s01 = "123";
String s02 = "456";
String s03 = s01.concat(s02);
System.out.println(s03);//输出:123456
//源码分析
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
//如果拼接的字符串为空,假设s2="",则直接返回原字符串,即s03=s01.
return this;
}
int len = value.length;//value是当前对象,即s01,value.length就是s01的长度
char buf[] = Arrays.copyOf(value, len + otherLen);//基于value数组和len + otherLen长度进行复制来生成一个新的数组
str.getChars(buf, len);//从buf数组的len索引开始将str字符串复制到buf目标字符数组中
return new String(buf, true);//返回一个新的String对象
}
与用”+“连接字符串的性能相比:
long startTime=System.currentTimeMillis(); //生成当前时间戳(单位:ms)
String s="";
for(int i=0;i<100000;i++){
s=s.concat("1");
}
System.out.println(System.currentTimeMillis()-startTime);
//输出:1761
//当i<200000时,需要6390ms 大概需要6秒
//上面测试的使用"+"号连接字符串:
//当i<100000时,输出6390 当i<200000时,输出13564
//结论:相对于+号 性能有提升,将近一倍的优化效率,但性能还是很差
(3)startsWith(String suffix) 和endsWith(String suffix) : 测试此字符串是否以指定的后缀开始,测试此字符串是否以指定的后缀结束。 返回boolean
String s1 = "[email protected]";
String s2 = "[email protected]";
String s3 = "one.png";
String s4 = "two.jpg";
boolean b1 = s1.endsWith("qq.com");
boolean b2 = s2.endsWith("qq.com");
boolean b3 = s3.startsWith("one");
boolean b4 = s4.startsWith("one");
System.out.println(b1);//输出:true
System.out.println(b2);//输出:false
System.out.println(b3);//输出:true
System.out.println(b4);//输出:false
//源码分析(以s3.startsWith("one")为例)
public boolean startsWith(String prefix, int toffset) {
char ta[] = value; //value是使用startsWith()方法的对象,转换成char数组,即s3
int to = toffset; //toffset是从指定索引开始,toffset默认值为0
char pa[] = prefix.value;//传进来的字符串(参数)转换成char数组,即s3.startsWith("one")中的one字符串
int po = 0;
int pc = prefix.value.length;//传进来的字符串(参数)的长度
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
//索引小于0,或者大于比较的两个字符串长度之差,即s3长度为7,s3.startsWith("one")中one的长度为3,所以只能取4个字符,所以索引不能大于4。假设索引为5,即从s3的n开始取3个字符,但此时s3只剩下两个字符,就会报越界错误。
return false;
}
while (--pc >= 0) {
//每次执行pc都会减一,直到pc小于0时结束
if (ta[to++] != pa[po++]) {
//因为没有设置索引,所以,to和po都为0,即s3和one字符串都从下标0开始两两比较,如果比较不相等,则false
return false;
}
}
//如果整个数组都比较完成没有返回false,则返回true
return true;
}
//源码分析(以s1 = "[email protected]";s1.endsWith("qq.com")为例)
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
//suffix即qq.com,参数中传进来的字符串。
//value.length - suffix.value.length,value.length为s1的长度15,suffix.value.length长度为6
//他们之差等于9,就以索引9开始用startsWith()方法,进行s1和参数中传进来的字符串qq.com开始比较,见上一个源码分析
(4)equals(Object anObject):将此字符串与指定的对象比较。返回boolean
对象类型中“==”比较(面试题):
//"=="的比较
String s01 = new String("123");
String s02 = new String("123");//只要在java中出现new就是新的对象
String s03 = "123"; //jvm把"123"字符串放在内存之中,如果"123"要用,就把"123"拿给你
String s04 = "123"; //同上
String s05 = s01;
String s06 = new String(s01);
boolean b = s01 == s02;
boolean b1 = s01 == s03;
boolean b2 = s03 == s04;
boolean b3 = s05 == s04;
boolean b4 = s05 == s01;
boolean b5 = s06 == s01;
System.out.println(b); //输出:false
System.out.println(b1);//输出:false 因为s01还在new 只要是new 就是新的对象 新的地址
System.out.println(b2);//输出:true
System.out.println(b3);//输出:false 因为s01是新的,赋值给s05,变成和s01一样新的对象
System.out.println(b4);//输出:true 同上
System.out.println(b5);//输出:false 因为s06new了
总结:在基本类型中,双等号比较的是值,但是在对象类型中比较的是内存地址
equals()方法:
//对象类型大部分都是通过一个特别的方法来完成值的比较
String s01 = new String("123");
String s02 = new String(s01);
boolean b = s02.equals(s01); //比较值
System.out.println(b); //输出:true
//源码分析
public boolean equals(Object anObject) {
if (this == anObject) {
//先判断是否是同一个对象,即比较地址,地址相同就是同一个对象
return true;
}
if (anObject instanceof String) {
//instanceof:测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型,即判断anObject对象是否是String类的实例
String anotherString = (String)anObject; //如果是String类的实例,就强转为String类型
int n = value.length; //使用此方法的对象自身的长度
if (n == anotherString.value.length) {
//强转后的String对象
char v1[] = value; //自身对象转换成char数组
char v2[] = anotherString.value; //强转后的String对象转换成char数组
int i = 0;
while (n-- != 0) {
//每次执行n减一,直到n等于0退出循环
if (v1[i] != v2[i]) //从索引0开始两两比较,如果比较出不相等,则返回false
return false;
i++; //若相等,则索引加一,继续比较
}
return true; //整个数组比较完n=0退出循环后,返回true
}
}
return false; //判断anObject对象不是String类的实例,返回false
}
前后端接收机制问题:
当前端发送的username传来值时,这样写是对的
String username = "tpp"; //前端发送
String username1 = null; //后端接收机制
if(username.equals(username1)) //值比较null,这种操作java中允许
{
System.out.println("登录成功");
}
System.out.println("登录失败")
当前端发送的username传来null时
String username = null; //前端发送
String username1 = null; //后端接收机制
if(username.equals(username1)) //null比较值,这种操作java中是不允许的
{
System.out.println("登录成功");
}
System.out.println("登录失败")
//输出结果:Exception in thread "main" java.lang.NullPointerException 会报空指针错误
在实际开发中,正确的写法:
String username = null; //前端发送
String username1 = null; //后端接收机制
if(username != null && username.equals(username1))//&&不能写成&
{
System.out.println("登录成功");
}
System.out.println("登录失败")
//输出结果:登录失败 这样就不会报空指针错误了
//为什么&&不能写成&?因为&对每一个条件都要判断,然而&&只要前面的条件是false就输出false,而不继续判断后面的条件了。这里如果写成&,执行了username != null之后也要执行username.equals(username1),就会报空指针错误。
希望可以和大家互相学习,谢谢大家!!!