目录
面试题:请解释String、StringBuffer、StringBuilder的区别
String类的所有针对字符串的操作方法都不会修改原字符串,而是产生了一个新的字符串!! ! ! ! ! !
字符串的不可变性!! !
这一点一定要牢记,牢记这点去看问题。
JKD中String类的声明
发现它继承了多个接口,其中Comparable接口就是我们上次刚学的。
为何String类被final修饰?
被final修饰的类无法被继承,String类不存在子类。
这样的话就可以保证所有使用JDK的人,大家用到的String类仅此一个,大家都相同。.
那么大家都随意的拓展自己的子类,你的String代码在你自己拓展的子类里面可以跑起来,在别人没有覆写该子类的话就跑步起来。大家都是自己的方法和想法,得不到统一,那还怎么写代码,总要有个规矩。
灵活不一定是好事,灵活就代表着不确定性,不确定就表示不稳定,如果代码都是不确定的,那可不行,所以需要final修饰。
继承和方法覆写在带来灵活性的同时,也会带来很多子类行为不一致导致的问题。
什么时候会用到final修饰类
你的这个类不希望有别的版本,到此为止。
所有使用者用的这个类完全相同,没有别的实现。
创建字符串的四种方式
// 方式一 直接赋值
String str = "Hello Bit";
// 方式二 通过构造方法产生对象(String也是一个类)
String str2 = new String("Hello Bit");
// 方式三 通过字符数组产生对象
char[] array = {'a', 'b', 'c'};
String str3 = new String(array);
//方式四:通过String的静态方法valueOf(任意数据类型)=>转为字符串
String str4 = String.valueOf(10);
最常用的是 方式一和方式四
在官方文档上 (Overview (Java Platform SE 8 ) (oracle.com)) 我们可以看到 String 还支持很多其他的构 造方式, 我们用到的时候去查就可以了.
字面量
String引用数据类型。
后面这个“hello world”也是字符串的对象,那么为什么没有new?因为JDK对String做了优化。
在内存中的分布:
如果此时再创建一个String对象,里面的内容一致。
String str2 = "hello world";
现在来比较他们的地址用 ==
true,表示地址相等。当然可能你会说这是因为内容一致才是true的,现在换个表示方法
equals方法是区分大小写的,如果你想不区分大小写的比较,也是有方法的。
当字符串对象的内容一模一样时,并不会开辟新的空间,而是去指向相同的空间。
这一步在内存中是怎么变化的?
注释:把图中的str2想像成str3即可,写错了。
不过是在栈区又开辟了一个局部变量,来保存这个堆区中String对象地址。
问题来了!现在修改原理的str1的内容,然后打印每个str会是什么结果?
运行结果:
为什么会是这样的结果?
关键在于这一步:
"abc”也是字符串的字面量,是一个新的字符串对象,str1实际上指向了新的字符串象"abc"
str3任指向原字符串对象"hello world"
2.字符串比较相等
所有引用数据类型在比较是否相等时,一定要使用equals方法比较。
JDK常用类,都已经覆写了equals方法,大家直接使用即可。(学会用现成的工具)如:String,Integer。
强调:引用数据类型使用“=="比较的仍然是数值(地址是否相等)
还有一种场景,小知识。如果String这个变量由用户从浏览器输入,下面两种方法更合理!
判断用户输入的是否为字符串张三,那种方式更好。
第二中方法更好,以后牵扯到用户输入就—定要做判空处理。万一这个用户在使用的时候忘记输出了这个内容呢?那第一种比较岂不是空指针引用了,肯定会报错的。
空指针异常了! 现在把第一种方法注释了。
这样就不会报错了。
要比较的特定内容本身就是字符串的字面量,一定不是空对象,把要比较的内容放在equals的前面。就可以方便处理userName为空的问题。
3.关于字符串的常量池问题(重点)
什么是字符串的常量池?
当使用直接赋值法产生字符串对象时,JVM会维护一个字符串的常量池,若该对象在堆中还不存在,则产生一个新的字符串对象加入字符串的常量池中。
当继续使用直接赋值法产生字符串对象时,JVM发现该引用指向的内容在常量池中已经存在了,则此时不再新建字符串对象,而是复用已有对象。
注释:在JDK8之前 常量池 在方法区中开辟,在8之后就把 常量池 挪到了堆区中
重新回顾下这一步在内存中的开辟顺序
第一步str1,因为不存在“hello world”,所以产生了一个新对象。
第二步str2,因为发现常量池中已经存在了一模一样的对象,所以不在产生新的对象,复用已有对象,直接指向这个一样对象的地址。
第三步依旧如初。 所以这三步就只产生了一个对象。
那么现在这三个语句在内存执行中会有怎么样的不同呢?
1. 因为是通过构造方法产生的对象,所以str1.2.3跟上面第一种情况不一样,各各都是不同的地址。
测试一下:果然地址不相同。
现在开始分析:程序都是从右向左执行的。
先是碰到“hello world”,因为此时的常量池不存在这个对象,所以先在字符串常量池中开辟一个“hello world”空间。
然后碰到 new,要知道,有new就会有新空间,所以又在常量池外面,却还是在堆区中开辟一块内容为 “hello world” 的空间,暂时给这块空间地址取名为h1
最后是在栈区中开辟一块空间名为 str1,里面保存的就是h1空间地址。
注释:有new了之后,就不是直接指向常量池了。
--------至此为止第一行执行结束
第二行也是先遇到“hello world”,发现现在的常量池中已经存在了一个一模一样的了,就不会再次在常量池中开辟空间了,然后碰到new,在常量池外,堆区内开辟一个也是内容为 “hello world”的空间,暂时给这块空间地址取名为h2,最后在栈区中的str2保存的是h2的地址。
第三步操作跟第二步一样。
大概原理图:
注释: 有new就有新空间,这三行代码产生四个字符串对象,其中一个在常量池中,其余三个在堆中.
浅浅的小知识,随便看看。
一个JVM进程中,所有常量都是共享的。
手工入池:String类提供的intern方法。
调用intern方法会将当前字符串引用指向的对象保存到字符串常量池中。
a.若当前常量池中已经存在了该对象,则不再产生新的对象,返回常量池中的String对象
b.若当前常量池中不存在该对象,则将该对象入池,返回入池后的地址。
思考题:根据上面两条规则,问下面这几条代码的运行结果是什么?
解析:
程序从右向左执行,第一天语句跟之前说的一样,没区别。
关键是第二句:str1.intern(); 这语句先是问你常量池中有没有str1里面保存的 hello对象?
发现常量池中已经有了,所以执行 a分支,不再产生新的对象,返回常量池中的字符串对象地址,但我们此时没有用变量来接收啊,所以说str1还是指向常量池外面的那块空间,没动过。
第三句,因为常量池有中间存在了heoll所以直接复用,让str2指向了这个对象的地址。
这就是最后呈现出来的样子
答案肯定是:false
怎么让答案变成 true呢?让str1接收intern方法的返回值就行了
内存图更新:
现在用字符数组 创建字 符串
这执行的结果还会是什么?
解析:第一条语句就是new一个字符数组,有new就有新空间,新空间肯定是在堆区上的。
注释:这个abc此时还是字符数组,常量池中还没有它的存在。
"abc",这个本身就算一个对象了,在常量池中存放着,之前构造方法只是开辟了新空间,并没有把这个字符串放在常量池中这一步骤,做到这一步的要是因为它本身 " " 就是个对象,自己就已经完成了这一步。
所以现在看第二个语句:
构造方法里面现在放着的是一个字符数组,而不是字符串了,所以并没有往常量池中放东西,只是普通的有new就有新空间。
此时常量池还是空着的,第二个只做了开辟一块空间,里面保存字符串“abc”,然后str1变量指向它就没了。
第三语句 str1.intern(); 现在此时由于常量池中没有字符串“abc”的存在,所以执行的是分支二。
若当前常量池中不存在该对象,则将该对象入池,返回入池后的地址。
它就会把原本在常量池外,堆内,str1执行的空间搬移到常量池中。
注释:str1还是一直执行本来的那块空间,只是现在这块空间的位置变到了常量池中。
现在看第四句: 是直接赋值的形式,现在常量池中已经存在了abc这个字符串,所以进行复用,同样执行这块空间地址。
所以,最终比较地址的答案:true
4.字符串的不可变性
所谓的字符串不可变指的是字符串对象的内容不能变,而不是字符串引用不能变。
可能会有很多疑惑,不是说字符串内容不可变吗?我怎么觉得想改变就跟改变啊!
这里的不可变指的 "hello" "world" "helloworld" "!!!"
"helloworld!!!"这些字符串对象一旦声明后就无法修改其内容。他们都是一个个单独的个体!
内存图:
因为都是直接赋值,所以在直接在常量池中创建对象。
第一句执行后
第二句:str += "wolrd ";从右向左执行,先是在常量池中创建这个对象“wolrd”,然后是 += 这是拼接,拼接后的会形成一个新的字符串对象,在常量池中生成。让str重新指向这个对象的地址。
第三局语句也是同样的道理。
所以我们所说的不可变性指的是这个字符串对象一旦产生赋值后,这个内容里面就无法被修改了。而不是指 str 这个引用指向的对象不可变。引用指向谁是可以随意改变的,但字符串对象不可变。
我们的拼接是完全实实在在生成了新的字符串在常量池在,原来要求的字符串对象根本没有对它进行任何的更改,依旧保留着。
为何字符串的对象无法修改内容而其他类的对象能修改内容。
字符串其实就是一个字符数组 ——》 char[ ],字符串保存的值实际上在数组中保存。
看看源代码的内容:
这是1.7的JDK 新的
这是JDK8的 以前的
以前是char数组,现在改成了byte数组 ,知道就行。
我们产生一个字符串对象的时候,它保存的这个内容、数值,实际上最终都在value这个数组都保存了。发现这个value这个字节数组是被private封装起来了。String外部无法访问这个value数组。而且String类里面也没有写get和set的方法。所以对外部而言根本不可见,无法使用和访问。
因此字符串对象的内容无法修改->String类的外部根本拿不到这个value数组
举例: str直接赋值的方法写一个 hello 字符串
在常量池 "hello"这个字符串内部其实有个数组 value,之所以长成 hello,是因为这个数组是hello,为啥改变不了,因为这个value是私有权限,String也并没有提供get和set方法。
5.如何修改字符串的内容
a.在运行时通过反射破坏value数组的封装(了解,不推荐,反射是所有框架的基础)
b.更换使用StringBuilder或者StringBuffer类——已经不是一个类了
a.在运行时通过反射破坏value数组的封装:
用a方法的案例代码:
import java.lang.reflect.Field;
public class StringChange {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// String str = "hello ";
// str += "wolrd ";
// str += "!!!";
// System.out.println(str);
String str = "hello";
System.out.println(str);
Class<String> cls = String.class;
// 获取这个属性
Field field = cls.getDeclaredField("value");
// 破坏封装,破坏private
field.setAccessible(true);
// 在String类的外部通过反射拿到value数组
char[] value = (char[]) field.get(str);
value[0] = 'H';
System.out.println(str);
}
}
运行结果:被改变了。
b.更换使用StringBuilder或者StringBuffer类:
若需要频繁进行字符串的拼接,使用StringBuilder类的append方法
StringBuilder类可以修改对象的内容。
注释:此时的字符串对象跟之前不一样,从始至终都只是一个对象。在一个对象的基础上做更改。
StringBuffer使用方法和StringBuilder完全—样,线程安全,性能较差。
在单线程的情况下一般使用StringBuilder
String,StringBuilder,StringBuffer之间的区别:
6. 字符串常见操作
6.1 字符串比较
上面使用过String类提供的equals()方法,该方法本身是可以进行区分大小写的相等判断。除了这个方法之外,String 类还提供有如下的比较操作:
NO | 方法名称 | 类型 | 描述 |
1 | public boolean equals(Object anObject) | 普通 | 区分大小写的比较 |
2 | public boolean equalsIgnoreCase(String anotherString) | 普通 | 不区分大小写的比较 |
3 | public int compareTo(String anotherString) | 普通 | 比较两个字符串的大小 |
代码示例: 不区分大小写比较
String str1 = "hello" ;
String str2 = "Hello" ;
System.out.println(str1.equals(str2)); // false
System.out.println(str1.equalsIgnoreCase(str2)); // true
在String类中compareTo()方法是一个非常重要的方法,该方法返回一个整型,该数据会根据大小关系返回数值:
1. 相等:返回0.
2. 小于:返回内容小于0.
3. 大于:返回内容大于0。
范例:观察compareTo()比较
String str1 = "abc";
String str2 = "aBc";
System.out.println(str1.compareTo(str2));
System.out.println("A".compareTo("a")); // -32
System.out.println("a".compareTo("A")); // 32
System.out.println("A".compareTo("A")); // 0
System.out.println("AB".compareTo("AC")); // -1
System.out.println("王".compareTo("李"));//3133
compareTo()是一个可以区分字符串大小关系的方法,是String方法里是一个非常重要的方法。
字符串的compareTo方法按照字符串内部的每个字符进行 unicode 的比较。
这里有个概念了解一下:按照"字典序"排列字符串,这是什么意思?
就是按照字符串内部的字符的unicode 码大小排序
字符串的比较大小规则, 总结成三个字 "字典序" 相当于判定两个字符串在一本词典的前面还是后面.
先比较第一 个字符的大小(根据 unicode 的值来判定), 如果不分胜负, 就依次比较后面的内容
6.⒉字符和字符串的相互转换。
字符串的内部实际上就是使用字符数组来存储的。
char数组通过构造方法变成字符串。
注释:这里是不能用toString方法来转字符串的
将字符数组的部分内容转为字符串对象
字符串内部包含一个字符数组,String 可以和 char[] 相互转换.
代码示例: 获取指定位置的字符
String str = "hello" ;
System.out.println(str.charAt(0)); // 下标从 0 开始
// 执行结果
h
System.out.println(str.charAt(10));
// 执行结果
产生 StringIndexOutOfBoundsException 异常
代码示例: 字符串与字符数组的转换
String str = "helloworld" ;
// 将字符串变为字符数组
char[] data = str.toCharArray() ;
for (int i = 0; i < data.length; i++) {
System.out.print(data[i]+" ");
}
// 字符数组转为字符串
System.out.println(new String(data)); // 全部转换
System.out.println(new String(data,5,5)); // 部分转换
⒉将字符串中的内容转为字符数组String => char[]
如果此时把data第一个数组内容改成 ‘H’,那么原来的字符串会受影响吗?
肯定不会的,很明显。这个数组知识做了,产生了一个新的字符数组,将字符串的内容复制过去。
强调:String对象不可变!!!内容改不了! ! !
代码示例: 给定字符串一个字符串, 判断其是否全部由数字所组成
public static void main(String[] args) {
String str = "1a23456" ;
System.out.println(isNumber(str) ? " 字符串由数字所组成!" : "字符串中有非数字成员!");
}
public static boolean isNumber(String str) {
char[] data = str.toCharArray() ;
for (int i = 0; i < data.length; i++) {
if (data[i]<'0' || data[i]>'9') {
return false ;
}
}
return true ;
}
编程时的重要思路:
在处理一个逻辑让你返回true或者false
我们的思路就是在循环中找反例,有一个反例直接return false。有时候找返例比正确的要清晰的多。
我们平常常用的一些方法Java里基本都有
比如我们把之前的if判断换成下面这个,一样可以完成功能。
这两个类型间的相互转换是非常重要的。字符串转换为字符数组这个点非常重要
咱碰到的字符串处理的大部分问题都需要转为字符数组来一个个处理。
字节常用于数据传输以及编码转换的处理之中,String 也能方便的和 byte[] 相互转换
代码示例: 实现字符串与字节数组的转换处理
String str = "helloworld" ;
// String 转 byte[]
byte[] data = str.getBytes() ;
for (int i = 0; i < data.length; i++) {
System.out.print(data[i]+" ");
}
// byte[] 转 String
System.out.println(new String(data));
byte 转换为 String
String 转 byte
四个中国字却转了这么多数字。它是按照当前默认的字符编码转为字节
上面的方法4:按照指定的编码格式转为字节数组。
如果没有指定就按照默认的编码格式进行。
将字符串保存到文件中或是通过网络传输都要用到字节数组。
我们在平常打字的时候,都是先Stirng转byte字节码,然后传送到了再用byte转String显示。
4.字符串查找操作
从一个完整的字符串之中可以判断指定内容是否存在,对于查找方法有如下定义:
(该图出自比特资料)
代码示例: 字符串查找,最好用最方便的就是contains()
String str = "helloworld" ;
System.out.println(str.contains("world")); // true
注释:该判断形式是从JDK1.5之后开始追加的,在JDK1.5以前要想实现与之类似的功能,就必须借助、indexOf()方法完 成。
代码示例: 使用indexOf()方法进行位置查找
String str = "helloworld" ;
System.out.println(str.indexOf("world")); // 5,w开始的索引
System.out.println(str.indexOf("bit")); // -1,没有查到
if (str.indexOf("hello") != -1) {
System.out.println("可以查到指定字符串!");
}
现在基本都是用contains()方法完成。 使用indexOf()需要注意的是,如果内容重复,它只能返回查找的第一个位置
代码示例: 使用indexOf()的注意点
String str = "helloworld" ;
System.out.println(str.indexOf("l")); // 2
System.out.println(str.indexOf("l",5)); // 8
System.out.println(str.lastIndexOf("l")); // 8
在进行查找的时候往往会判断开头或结尾。
代码示例: 判断开头或结尾
String str = "**@@helloworld!!" ;
System.out.println(str.startsWith("**")); // true
System.out.println(str.startsWith("@@",2)); // ture
System.out.println(str.endsWith("!!")); // true
6.3 字符串替换
使用一个指定的新的字符串替换掉已有的字符串数据,可用的方法如下:
代码示例: 字符串的替换处理
String str = "helloworld" ;
System.out.println(str.replaceAll("l", "_"));
System.out.println(str.replaceFirst("l", "_"));
两个的区别:
注意事项: 由于字符串是不可变对象 , 替换不修改当前字符串, 而是产生一个新的字符串.
6.4 字符串拆分
可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串。
可用方法如下:
代码示例: 实现字符串的拆分处理
String str = "hello world hello Java" ;
String[] result = str.split(" ") ; // 按照空格拆分
for(String s: result) {
System.out.println(s);
}
代码示例: 字符串的部分拆分
String str = "hello world hello Java" ;
String[] result = str.split(" ",2) ;
for(String s: result) {
System.out.println(s);
}
拆分是特别常用的操作. 一定要重点掌握. 另外有些特殊字符作为分割符可能无法正确切分, 需要加上转义.
代码示例: 拆分IP地址
String str = "192.168.1.1" ;
String[] result = str.split("\\.") ;
for(String s: result) {
System.out.println(s);
}
注意事项:
1. 字符"|","*","+"都得加上转义字符,前面加上"\".
2. 而如果是"",那么就得写成"\\".
3. 如果一个字符串中有多个分隔符,可以用"|"作为连字符.
代码示例: 多次拆分
String str = "name=zhangsan&age=18" ;
String[] result = str.split("&") ;
for (int i = 0; i < result.length; i++) {
String[] temp = result[i].split("=") ;
System.out.println(temp[0]+" = "+temp[1]);
}
当你发现你按照指定格式,字符串却没有拆成功,说明是 特殊字符,需要转义。
这种代码在以后的开发之中会经常出现
6.5 字符串截取
从一个完整的字符串之中截取出部分内容。可用方法如下:
代码示例: 观察字符串截取
String str = "helloworld" ;
System.out.println(str.substring(5));
System.out.println(str.substring(0, 5));
注意事项:
1. 索引从0开始
2. 注意前闭后开区间的写法, substring(0, 5) 表示包含 0 号下标的字符, 不包含 5 号下标
6.6 其他操作方法
代码示例: 观察trim()方法的使用
String str = " hello world " ;
System.out.println("["+str+"]");
System.out.println("["+str.trim()+"]");
trim 会去掉字符串开头和结尾的空白字符(空格, 换行, 制表符等).
代码示例: 大小写转换
String str = " hello%$$%@#$%world 哈哈哈 " ;
System.out.println(str.toUpperCase());
System.out.println(str.toLowerCase());
这两个函数只转换字母。
代码示例: 字符串length()
String str = " hello%$$%@#$%world 哈哈哈 " ;
System.out.println(str.length());
注意:数组长度使用数组名称.length属性,而String中使用的是length()方法
代码示例: 观察isEmpty()方法
System.out.println("hello".isEmpty());
System.out.println("".isEmpty());
System.out.println(new String().isEmpty());
String类并没有提供首字母大写操作,需要自己实现.
代码示例: 首字母大写
public static void main(String[] args) {
System.out.println(fistUpper("yuisama"));
System.out.println(fistUpper(""));
System.out.println(fistUpper("a"));
}
public static String fistUpper(String str) {
if ("".equals(str)||str==null) {
return str ;
}
if (str.length()>1) {
return str.substring(0, 1).toUpperCase()+str.substring(1) ;
}
return str.toUpperCase() ;
}
再次强调:上面所有的操作都是产生了新字符串,原字符串是不可能改变的!!!
这个是成员方法,只能判断字符串的长度是否为0,不能判断null。
如果此时str = null;则肯定不能使用 str.isEmpty();
那么该怎么判断呢?
这里面的先后顺序不能换,只有不为空,才能引用方法。
写—个方法将字符串的首字母大写处理。
测试案例:
补充小知识:
关于StringBuilder类的具体使用:
首先来回顾下String类的特点: 任何的字符串常量都是String对象,而且String的常量一旦声明不可改变,如果改变对象内容,改变的是其引用的指 向而已。
通常来讲String的操作比较简单,但是由于String的不可更改特性,为了方便字符串的修改,提供StringBuffer和 StringBuilder类。
StringBuffer 和 StringBuilder 大部分功能是相同的,我们主要介绍 StringBuffer
在String中使用"+"来进行字符串连接,但是这个操作在StringBuffer类中需要更改为append()方法:
public synchronized StringBuffer append(各种数据类型 b)
范例:观察StringBuffer使用
public class Test{
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("Hello").append("World");
fun(sb);
System.out.println(sb);
}
public static void fun(StringBuffer temp) {
temp.append("\n").append("www.bit.com.cn");
}
}
String和StringBuffer最大的区别在于:String的内容无法修改,而StringBuffer的内容可以修改。频繁修改字符串的 情况考虑使用StingBuffer。
为了更好理解String和StringBuffer,我们来看这两个类的继承结构:
可以发现两个类都是"CharSequence"接口的子类。这个接口描述的是一系列的字符集。所以字符串是字符集的子 类,如果以后看见CharSequence,最简单的联想就是字符串。
注意:String和StringBuffer类不能直接转换。如果要想互相转换,可以采用如下原则:
String变为StringBuffer:利用StringBuffer的构造方法或append()方法
StringBuffer变为String:调用toString()方法。
使用方法:
因为StringBulider类可以修改内容,因此具备一些String类不具备的修改内容的功能,除了拼接append方法外
比如:
a.字符串反转操作,sb提供的reverse();
public synchronized StringBuffer reverse()
b.删除指定范围的数据
public synchronized StringBuffer delete(int start, int end)
删除从start索引开始end之前的所有内容。[start,end)
示例代码:
注释:java里面的范围都是遵循左闭右开的原则。 这里范围的索引是从0开始的,所以第一个没有被删除。如果想删除这里的第七位数,右边需要写 7。左闭右开。
c.插入数据
public synchronized StringBuffer insert(int offset, 各种数据类型 b)
如果以后想字符串它进行拼接、翻转、删除、插入等等一系列操作,可以先讲String转换为StringBuffer,操作完成后再用toString转换为原来的String。
面试题:请解释String、StringBuffer、StringBuilder的区别
1. String的内容不可修改,StringBuffer与StringBuilder的内容可以修改.
2. StringBuffer与StringBuilder大部分功能是相似的
3. StringBuffer采用同步处理,属于线程安全操作;StringBuilder未采用同步处理,属于线程不安全操作
注释:StringBuffer和StringBuilder除了线程安不安全的区别,其他完完全全一模一样。上面的案例用StringBuilder也一样可以。
小结
强调:一般情况下,要使用String类,就采用直接赋值的方式。要比较内容是否相等使用equals方法。除了特性情况,不然就别整些花里胡哨的操作了。
注意的点:
1. 字符串的比较, == 、 equals 、 compareTo 之间的区别.
2. 了解字符串常量池, 体会 "池" 的思想.
3. 理解字符串不可变
4. split 的应用场景
5. StringBuffer 和 StringBuilder 的功能.