文章目录
JDK中String类的声明
为什么String
被final
修饰?
被final修饰的类无法被继承,String类不存在子类,这样的话就可以保证所有使用JDK的人,大家用到的String类仅此一个,大家都相同
我们不妨假设String
允许被继承,每个人都可以继承String
类,并修改其内部的方法实现,可以说继承和方法覆写在带来灵活性的同时,也会带来很多子类的行为不一致导致的问题,比如String
里的equals
如果不一样,可能在某个人写的方法中是等于,另一个人中就是不等于,而final
就是为了避免这种情况的
什么时候会用到final
修饰类?
如果一个类不希望有别的版本,想让使用者统一其用法,就使用final
修饰类
创建字符串方式
创建字符串的四种方式
1.直接赋值:String str = "hello world";
2.通过构造方法产生对象:String str = new String("hello world");
3.通过字符数组产生对象:
char[] data = new char[] {'a', 'b', 'c'};
String str = new String(data);
4.通过String的静态方法valueOf(任意数据类型)转换为字符串:
String str = String.valueOf(10);
这里的第一种和第四种方式是最常用到的
字面量
直接写出来的数值就称之为字面量
10 -> int字面量
10.2 -> double字面量
true -> boolean字面量
“abc” -> String字面量 -> 就是一个字符串的对象
String str = "hello world"
这里的hello world既是字面量,又是一个字符串堆中
代码示例:
public class StringTest {
public static void main(String[] args) {
String str1 = "hello";
String str2 = str1;
str2 = "Hello";
System.out.println(str1); // hello 可见修改str2并没有影响str1
}
}
执行上面这条语句,在内存中会发生什么变化?
“Hello"是字符串的字面量,也是一个字符串对象,str2实际上指向了一个新的字符串对象"Hello”,而str1仍然指向原字符串对象"hello"
字符串之间的比较
我们在接口章节已经说过,所有引用数据类型在比较是否相等时,==比较的是地址是否相同,想要知道值是否相同要使用equals
方法进行比较
JDK中的常用类,都已经覆写了equals
方法,大家直接使用即可,比如:String、Integer
equals
方法是区分大小写的
public class StringTest {
public static void main(String[] args) {
String str1 = "hello";
String str2 = "Hello";
System.out.println(str1.equals(str2));// false
System.out.println(str1.equalsIgnoreCase(str2));// true
}
}
而equalsIgnoreCase
方法不区分大小写
牵扯到用户输入一定判空,因为用户可能会忘记输入,如果直接执行会空指针异常
如何避免这种问题是我们以后工作中时常会碰到且非常重要的:
public class StringTest {
public static void main(String[] args) {
String userName = null;
System.out.println(userName.equals("person"));// error 空指针异常
System.out.println("person".equals(userName));// false
}
}
由于我们要比较的特定内容本身就是字符串的字面量,一定不会是空对象,把要比较的内容放在equals的前面,就可以方便处理userName为空的问题
字符串的常量池
public class StringTest {
public static void main(String[] args) {
//第一种写法
String str1 = "hello";
String str2 = "hello";
String str3 = "hello";
System.out.println(str1 == str2);// true
System.out.println(str2 == str3);// ture 三个引用指向的都是同一个字符串对象
//第二种写法
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = new String("hello");
System.out.println(s1 == s2);// false
System.out.println(s2 == s3);// false 三个引用指向的是各自的字符串对象
}
}
为什么第一种写法的引用都指向同一个对象?
当使用直接赋值法产生字符串对象时,JVM会维护一个字符串的常量池,若该对象在堆中还不存在,则产生一个新的字符串对象并加入字符串的常量池中
当继续使用直接赋值法产生字符串对象时,JVM发现该引用指向的内容在常量池中已经存在了,此时不在创建新字符串对象,而是复用已有对象
如果是通过构造方法创建的字符串,第一个创建的字符串常量会先存储在字符串常量池中,然后在堆中开辟空间并把该常量字符串拷贝一份放到到堆中存储,也就是说,第一个引用依然是指向堆中的对象的,而不是常量池,而后续的相同字符串的创建同样是开辟了新的空间,此时的常量字符串就不存储在常量池中了,因为里面已经存在一个相同的常量字符串了,但依然在堆中开辟空间,并存储字符串,然后让引用指向该对象
有new就有新空间,这三行代码产生了四个字符串对象,其中一个在常量池,其余三个在堆中
通过字符数组创建对象:
char[] data = {'a', 'b', 'c'};
String str = new String(data);
执行完第一句时,内存池还没有任何字符串,然后str是new出来的,所以不入池,只在堆上产生了一个普通的字符串对象,值是由字符数组data赋值来的
手工入池
手工入池方法是指String
类提供的intern
方法
调用intern
方法会将当前字符串引用指向的对象保存到字符串常量池中
这里有两种情况:
如果当前常量池中已经存在了该对象,则不再产生新的对象,返回常量池中的String对象
如果当前常量池中不存在该对象,则将该对象入池,返回入池后的地址
代码示例一:
public class StringTest {
public static void main(String[] args) {
String str1 = new String("hello");
str1.intern();
String str2 = "hello";
System.out.println(str1 == str2);//fasle
}
}
内存空间:
常量池中已经存在"hello",因此不再产生新的对象,返回常量池中字符串对象地址,只不过代码中没有接收返回值,所以str1还是指向堆中的对象
如果想让结果变成true,只需要接受一下返回值即可str1 = str1.intern();
代码示例二:
public class StringTest {
public static void main(String[] args) {
char[] data = new char[] { 'a', 'b', 'c'};
String str1 = new String(data);
str1.intern();
String str2 = "abc";
System.out.println(str1 == str2);
}
}
内存空间
在执行String str1 = new String(data);
时,因为data是字符数组,此时还没有"abc",执行完构造方法之后,才开辟空间把字符数组合到一起成为"abc",所以在整个过程中都没有出现"abc"的字面常量,更不会被存入字符串常量池
因此在执行str1.intern();
语句时,由于常量池中不存在该对象,所有会将该对象直接移动到常量池中,注意,不是拷贝一份,而是直接移动到常量池中去了,这里隐含的结果是,该对象的地址改变了!所以即使没有接收返回值,原先指向该对象的地址也发发生了变化
而之后的str2是通过直接赋值的方法创建字符串的,所以会把常量池里的字符串地址返回给str2,那么最终结果就是str1和str2的地址相同,都指向了常量池里的字符串
字符串的不可变性
所谓的字符串不可变指的是字符串对象的内容不能变,而不是字符串引用不能变
代码示例:
public class StringTest {
public static void main(String[] args) {
String str = "hello";
str = str + "world";
System.out.println(str);// helloworld
}
}
这里的不可变是:“hello” 不能变,"world"也不能变,拼接后的"hello world"不能变,而其引用是一直都在变的
为什么字符串的对象内容无法被修改,而其他类的对象能修改内容?
那是因为,字符串其实就是一个字符数组 -> char[]
,字符串保存的值实际上在数组中保存
并且在String类的定义中,使用private
保护了value[]
,使得String外部无法访问value[]
,而且String并没有提供关于value
属性的getter
和setter
方法
对于String类的外部而言,value
数组是完全没法使用和读取的,因此字符串对象的内容就不可能被修改了
修改字符串的内容
两种办法
在运行时通过反射破坏value数组的封装(不推荐)
更换使用StringBuilder或者StringBuffer类 - 这其实已经不是String类型了
第一种方法不推荐使用,这里不进行演示,修改字符串内容主要看第二种方法
若需要频繁进行字符串的拼接,使用StringBuilde
r的append
方法,StringBuilder
类可以修改对象的内容
public class StringBuilderTest {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append("world");
System.out.println(sb);// helloworld
}
}
StringBuffer
的使用方法和StringBuilder
一样,区别在于它是线程安全,但是性能较差,在目前单线程的情况下,StringBuilder
就够用了
关于StringBuilder
类的具体使用
StringBuilder
类和String
类是两个独立的类,StringBuilder
就是为了解决字符串拼接问题产生的
因为String
的对象无法修改内容,为了方便字符串的拼接操作,产生StringBuilder
类,StringBuilder
类的对象是可以修改内容的
StringBuilder
和String
的相互转换
String转换成StringBuilder有两个办法:一是使用StringBuilder
的构造方法,二是使用append
方法
第一种:StringBuilder sb = new StringBuilder("hello");
第二种:sb.append("world");
StringBuilder
转换成String
使用toString
方法
String str = sb.toString();
因为StringBuilder类可以修改内容,因此具备一些String类不具备的修改内容的功能
除了拼接append方法外,还具备:
- 字符串反转:
sb.reverse();
- 删除指定范围的数组 ->
delete(int start, int end)
方法,删除索引为[start, end)
的字符串 - 插入操作 ->
insert(int start, 任意需要插入的字符串)
把字符串插入到索引为start的位置
String类、StringBuilder、StringBuffer的区别:
String的对象无法修改,但另外两个的对象内容可以修改
StringBuffer是线程安全的操作,性能较差,StringBuffer是线程不安全,性能较高
字符串的常见操作
1.字符串的比较
public boolean equals(Object anObject)
- 区分大小写的比较
public boolean equalsIgnoreCase(String anotherString);
- 不区分大小写的比较
public int comparaTo(String anotherString)
- 通过该方法可知String类也实现了Comparable接口,并覆写了comparaTo方法
public class StringCompara {
public static void main(String[] args) {
String str1 = "abc";
String str2 = "ABC";
System.out.println(str1.compareTo(str2));// 32
}
}
字符串的comparaTo方法按照字符串内部的每个数组进行ASCII的比较,A的ASCII码是65,a是97,正好差了32
2.字符和字符串的相互转换
这一部分非常重要,因为字符串的内部实际上就是使用字符数组来存储的,以后碰到的字符串处理的大部分问题都需要转为字符数组来一个个处理
String转换为char
1.public char charAt(int index)
- 取出字符串中指定索引的字符
String str = "hello";
System.out.println(str.charAt(1));// e
2.public char[] toCharArray()
- 将字符串中的内容转为字符数组
String str = "hello";
char[] data = str.toCharArray();
System.out.println(data);// hello
如果在这里加一句data[0] = 'H';
会不会改变原字符串?
显然是不能的,String对象是不可变的!
char转换为String
1.public String(char[] value)
- 构造方法
char[] ch = {'a', 'b', 'c'};
String str = new String(ch);
System.out.println(str);// abc
2.public String(char[] value, int offset, int count)
- 将字符数组的部分内容转换为字符串对象
char[] ch = {'a', 'b', 'c'};
String str = new String(ch,1, 2);//把字符数组ch中索引为1开始,长度为2的字符转换为字符串
System.out.println(str);// bc
这里的1是字符数组开始的索引,2是需要转换的字符的个数
思考题:如何判断一个字符串的对象是由纯数字组成的?
“123” -> true 数字型字符串,由纯数字组成
“123a” -> false 不是由纯数字组成的
public static boolean isAllNum(String str){
//如果传入一个空指针
if(str == null){
return false;
}
//如果不是空指针,转化为字符数组
char[] arr = str.toCharArray();
for(char ch : arr){
if(ch < '0' || ch > '9'){
//找到反例,直接返回
return false;
}
}
return true;
}
这个方法中for循环里头的判断实际上JDK已经帮我们实现好了,是Character.isDigit(char ch)
Character
是char的包装类,这个方法的作用是判断字符ch
是不是数字,是的话返回true,不是就返回false
for - each
循环里面可以直接这么写:
if(!Character.isDigit(ch)){
return false;
}
这思考题里有一种思路很重要,如果一个逻辑让你返回true或者false,那我们就在循环中找反例,找到了反例就返回false
3.字符串和字节数组的相互转换
String
转换为byte[]
和字符的转换不同,不能把字符串中的字符转成一个字节,只能转为一个字节数组
public byte[] getBytes()
- 将字符串以字节数组的形式返回
String str = "你好中国";
byte[] data = str.getBytes();
System.out.println(Arrays.toString(data));
//[-28, -67, -96, -27, -91, -67, -28, -72, -83, -27, -101, -67]
getByte
的作用是按照当前默认的字符编码转为字节,上面的结果是按照UTF-8编码格式转换的
public byte[] getBytes(String charsetName)
- 编码转换处理
该方法的作用是按照指定的编码格式转换为字节数组
String str = "你好中国";
byte[] data = str.getBytes("gbk");//按照gbk的编码方式转换为字节数组
System.out.println(Arrays.toString(data));
// [-60, -29, -70, -61, -42, -48, -71, -6]
可见,汉字在UTF-8编码下,一个汉字3个字节,在GBK编码下,一个汉字两个字节
byte[]
转换为String
这转换直接通过String的构造方法就可以实现
byte[] data = {97, 98, 99};
String str = new String(data);
System.out.println(str);// abc
什么时候会用到byte
?
将字符串保存到文件中或是通过网络传输都要用到字节数组
4.字符串的查找
public booleadn contains(CharSequence s)
- 判断字符串中是否包含子串
public boolean startsWith(String prefix)
- 判断是否以指定字符串开头
public boolean endsWith(String subfix)
- 判断是否以指定字符串结尾
String str = "hello world";
System.out.println(str.contains("hello"));// true
System.out.println(str.startsWith("hello"));// true
System.out.println(str.startsWith("hello1"));// false
System.out.println(str.endsWith("world"));// true
System.out.println(str.endsWith("world1"));// false
5.字符串替换操作
public String replaceAll(String regex, String replacement)
- 替换字符串中所有指定内容
public String replaceFirst(String regex, String replacement)
- 替换字符串中第一个指定内容
把regex
替换成replacement
,替换的内容可以是字符串
String str = "hello world";
System.out.println(str.replaceAll("l", "_"));// he__o wor_d
System.out.println(str.replaceAll("ll", "_"));// he_o world
System.out.println(str.replaceFirst("l", "_"));// he_lo world
System.out.println(str);// hello world
当然,String类的所有针对字符串的操作方法都不会修改原字符串,而是产生了一个新的字符串
6.字符串的拆分
public String[] spilt(String regex)
- 将字符串按照regex
进行拆分
publci String[] split(String regex, int limit)
- 将字符串部分拆分,拆分后的字符串长度为limit
String str = "Hello Java Hello World";
String[] data1 = str.split(" ");
String[] data2 = str.split(" ", 2);
System.out.println(Arrays.toString(data1));// [Hello, Java, Hello, World]
System.out.println(Arrays.toString(data2));// [Hello, Java Hello World]
如果发现按照指定的格式拆分字符串得到了一个空数组或没有发生拆分,只有两种情况
- 这个格式的字符在字符串中不存在
- 这个格式的字符是个特殊字符,需要进行转义,比如’\.’
String str = "192.168.1.1";
String[] data1 = str.split(".");
String[] data2 = str.split("~");
System.out.println(Arrays.toString(data1));// []
System.out.println(Arrays.toString(data2));// [192.168.1.1]
这里就需要对"."进行转义:
String str = "192.168.1.1";
String[] data = str.split("\\.");
System.out.println(Arrays.toString(data));// [192, 168, 1, 1]
进行转义后就能顺利拆分了
7.字符串的截取处理
public String substring(int beginIndex)
- 从指定索引截取到结尾
public String substring(int beginIndex, int endIndex)
- 截取部分内容[strat, end)
String str = "helloworld";
System.out.println(str.substring(5));// world
System.out.println(str.substring(0, 5));// hello
同样是产生新的字符串,而不是对原字符串str的修改
8.其他常用
public String trim()
- 只会去掉字符串的左右空格字符,保留中间的空格字符
public String toUpperCase()
- 字符串转大写
public String toLowerCase()
- 字符串转小写
public int length()
- 返回字符串的长度
String str = " hello world ";
System.out.println(str.trim());// hello world
System.out.println(str.toUpperCase());// HELLO WORLD
System.out.println(str.toLowerCase());// hello world
System.out.println(str.length());// 13
public boolean isEmpty()
- 判断字符串是否为空字符串,注意是判断字符串的长度是否为0,不是判断是否为null,因为如果是null就不能调用该方法,解决方法是,调用方法前判断是否为null
String str = "";
//先判空,确保str不为null
if(str != null){
System.out.println(str.isEmpty());// true
}