java12基础(day12)
目录
一.String字符串和编码
1. String字符串类
2. 字符串比较
3. 字符串的搜索
4. 截取子字符串
5. 去除首尾空白字符
6. 替换字符串
7. 分割字符串
8. 拼接字符串
9. 格式化字符串
10. 类型转换
11. 转换为char[ ]字符数组
12. 字符编码
14. 小结
提个问题:String字符串不可变性的缺点
二.StringBuilder
1. 普通String字符串的拼接
2. 使用StringBuilder进行拼接
3. 小结
三.StringJoiner
1. 使用StringBuilder拼接
2. 使用StringJoiner拼接
3. String.join( )方法
4. 小结
四.包装类型
1. 包装类型
2. 自动装箱
3. 缓存池
4. 进制转换
一.String字符串和编码
1. String字符串类
在Java
中,String
是一个引用类型,它本身也是一个class
类。
实际上字符串在String
内部是通过一个char[]
数组表示的,因为String
太常用了,所以Java
提供了"..."
这种字符串字面量表示方式。
String s1 = "Hello!";
public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {}// 底层
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
Java
字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的private final char[]
字段,以及没有任何修改char[]
的方法实现的。
public class Main {public static void main(String[] args) {String s = "Hello";System.out.println(s);// s未指向新的字符串s.toUpperCase();System.out.println(s);// s重新指向新的字符串s = s.toUpperCase();System.out.println(s);}
}
2. 字符串比较
想要比较两个字符串是否相同时,要特别注意,我们实际上是想比较字符串的内容是否相同。必须使用equals()
方法而不能用==
关系运算符。
public class Main {public static void main(String[] args) {String s1 = "hello";String s2 = "hello";System.out.println(s1 == s2);System.out.println(s1.equals(s2));}
}
从表面上看,两个字符串用==
和equals()
比较都为true
,但实际上那只是Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,s1
和s2
的引用地址就是相同的,结果为true
。所以,这种==
比较返回true
纯属巧合。换一种写法,==
比较就会失败.
public class Main {public static void main(String[] args) {String s1 = "hello";String s2 = "HELLO".toLowerCase();System.out.println(s1 == s2);System.out.println(s1.equals(s2));}
}
结论:两个字符串比较,必须总是使用equals()方法。要忽略大小写比较,使用equalsIgnoreCase()方法。
3. 字符串的搜索
使用indexOf()
方法可以从字符串的首部进行搜索,当前字符串中指定子字符串的下标位置,返回值为int
类型。如果存在,则返回该子字符串的下标位置。如果不存在,则返回-1
;
lastIndexOf()
方法是从字符串的尾部进行搜索,返回值与indexOf()
方法一致;
startsWith()
和endsWith()
方法是用于判断字符串是否以指定字符串开头或结尾,返回值为boolean
类型;
contains()
方法用于查找当前字符串中是否存在指定子字符串,返回值为boolean
类型。
"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
"Hello".contains("lo"); // true
4. 截取子字符串
使用substring()
方法可以从当前字符串中,截取指定下标区间的子字符串。
"大漠孤烟直".substring(2); // 孤烟直
"大漠孤烟直".substring(0,2); // 大漠
5. 去除首尾空白字符
使用trim()
方法可以移除字符串首尾空白字符。空白字符包括空格,\t
,\r
,\n
:
" \tHello\r\n ".trim(); // 返回 "Hello"
注意:trim()
并没有改变字符串的内容,而是返回了一个新字符串。
String
还提供了isEmpty()
判断字符串是否为空字符串:
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
6. 替换字符串
要在字符串中替换子串,有两种方法。
第一种:根据字符或字符串替换。
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
第二种:是通过正则表达式替换。
下面的代码通过正则表达式,把匹配的子串统一替换为","
。
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
7. 分割字符串
要分割字符串,使用split()
方法,并且传入的也是正则表达式:
String str1 = "北京#上海#大连#连云港#南京#杭州#深圳#齐齐哈尔#乌鲁木齐";
String[] ret1 = str1.split("#"); // 按照普通字符内容切割
System.out.println(Arrays.toString(ret1));String str2 = "北京.上海.大连.连云港.南京.杭州.深圳.齐齐哈尔.乌鲁木齐";
String[] ret2 = str2.split("\\."); // "."是正则表达式中符号,所以需要转义
System.out.println(Arrays.toString(ret2));String str3 = "北京0上海1大连2连云港3南京4杭州5深圳6齐齐哈尔7乌鲁木齐";
String[] ret3 = str3.split("[0-9]"); // 按照正则表达式切割字符串
System.out.println(Arrays.toString(ret3));
8. 拼接字符串
拼接字符串使用静态方法join()
,它用指定的字符串连接字符串数组:
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
9. 格式化字符串
字符串提供了format()
静态方法,可以传入其他参数,替换占位符,然后生成新的字符串。
String s = "Hi %s, your score is %d!";
System.out.println(s.format("Alice", 80)); // 字符串对象调用
System.out.println(String.format("Hi %s, your score is %.2f!", "Bob", 59.5)); // 字符串类调用
有几个占位符,后面就传入几个参数。参数类型要和占位符一致。我们经常用这个方法来格式化信息。常用的占位符有:
%s
:显示字符串%d
:显示整数%f
:显示浮点数
占位符还可以带格式,例如%.2f
表示显示两位小数。如果你不确定用啥占位符,那就始终用%s
,因为%s
可以显示任何数据类型。
10. 类型转换
要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()
。这是一个重载方法,编译器会根据参数自动选择合适的方法:
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
要把字符串转换为其他类型,就需要根据情况。
例如:把字符串转换为int
类型。
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
例如:把字符串转换为boolean
类型:
boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false
11. 转换为char[ ]字符数组
String
和char[]
类型可以互相转换。
例如:
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String
如果修改了char[]
数组,String
并不会改变,这是因为通过new String(char[])
创建新的String
实例时,它并不会直接引用传入的char[]
数组,而是会复制一份,所以,修改外部的char[]
数组不会影响String
实例内部的char[]
数组,因为这是两个不同的数组。
char[] cs = "Hello".toCharArray();
String s = new String(cs);
System.out.println(s);
cs[0] = 'X';
System.out.println(s);
12. 字符编码
┌────┐
ASCII: │ 41 │└────┘┌────┬────┐
Unicode: │ 00 │ 41 │└────┴────┘中文字符'中'的GB2312编码和Unicode编码┌────┬────┐
GB2312: │ d6 │ d0 │└────┴────┘┌────┬────┐
Unicode: │ 4e │ 2d │└────┴────┘
那我们经常使用的UTF-8
又是什么编码呢?因为英文字符的Unicode
编码高字节总是00,包含大量英文的文本会浪费空间,所以,出现了UTF-8
编码,它是一种变长编码,用来把固定长度的Unicode
编码变成1~4字节的变长编码。通过UTF-8
编码,英文字符'A'
的UTF-8
编码变为0x41
,正好和ASCII
码一致,而中文'中'
的UTF-8
编码为3字节0xe4b8ad
。
UTF-8
编码的另一个好处是容错能力强。如果传输过程中某些字符出错,不会影响后续字符,因为UTF-8
编码依靠高字节位来确定一个字符究竟是几个字节,它经常用来作为传输编码。
在Java中,char
类型实际上就是两个字节的Unicode
编码。如果我们要手动把字符串转换成其他编码,可以这样做:
byte[] b1 = "中".getBytes(); // 按系统默认编码Unicode转换
byte[] b2 = "中".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b3 = "中".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
byte[] b4 = "中".getBytes("GBK"); // 按GBK编码转换
注意:转换编码后,就不再是char
类型,而是byte
类型表示的数组。
如果要把已知编码的byte[]
转换为String
,可以这样做:
byte[] b = {-28, -72, -83};
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
Java的String
和char
在内存中总是以Unicode
编码表示。
扩展:
对于不同版本的JDK
,String
类在内存中有不同的优化方式。具体来说,早期JDK版本的String
总是以char[]
存储,它的定义如下:
public final class String {private final char[] value;private final int offset;private final int count;
}
自JDK1.9版本开始,String
则以byte[]
存储:如果String
仅包含ASCII字符,则每个byte
存储一个字符,否则,每两个byte
存储一个字符,这样做的目的是为了节省内存,因为大量的长度较短的String
通常仅包含ASCII
字符:
public final class String {private final byte[] value;private final byte coder; // 0 = LATIN1, 1 = UTF16
}
14. 小结
- Java字符串
String
是不可变对象 - 字符串操作不改变原字符串内容,而是返回新字符串
- 常用的字符串操作:提取子串、查找、替换、大小写转换等
Java
使用Unicode
编码表示String
和char
- 转换编码就是将
String
和byte[]
转换,需要指定编码 - 转换为
byte[]
时,始终优先考虑UTF-8
编码
提个问题:String字符串不可变性的缺点
答:由于字符串的不可变性,每次对字符串进行修改(如 concat()
、substring()
、replace()
等)都会创建新的字符串对象。在需要频繁修改字符串的场景(如循环拼接字符串)中,会产生大量临时对象,导致以下问题:
- 内存浪费:频繁创建新对象,增加垃圾回收(GC)压力。
- 性能下降:对象创建和垃圾回收会消耗大量 CPU 时间。
- 字符串常量池的潜在问题:如果大量不同的字符串被放入常量池(如动态生成的字符串),可能导致常量池内存溢出(PermGen 或 Metaspace 空间不足)。
二.StringBuilder
1. 普通String字符串的拼接
首先来回顾下String类的特点: 任何的字符串常量都是String对象,而且String的常量一旦声明不可改变,如果改变对象内容,改变的是其引用的指 向而已。Java编译器对String做了特殊处理,使得我们可以直接用+拼接字符串。例如:
String s = "";
for (int i = 0; i < 1000; i++) {s = s + "," + i;
}
虽然可以直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
2. 使用StringBuilder进行拼接
为了能高效拼接字符串,Java标准库提供了StringBuilder,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder中新增字符时,不会创建新的临时对象:
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {sb.append(',');sb.append(i);
}
String s = sb.toString();
StringBuilder
还可以进行链式操作:
StringBuilder sb = new StringBuilder(1024);sb.append("Mr ").append("Bob").append("!").insert(0, "Hello, ");System.out.println(sb.toString());
3. 小结
StringBuilder
是可变对象,用来高效拼接字符串。StringBuilder
可以支持链式操作,实现链式操作的关键是返回实例本身。StringBuffer
线程安全(现在很少使用),性能较差;StringBuilder
线程不安全,但性能较好。
package model.apsource;import java.util.StringJoiner;public class Demo09 {public static void main(String[] args) {StringBuffer sb = new StringBuffer();// 追加for (int i = 0; i < 100; i++) {sb.append(i);// 往末尾追加元素sb.append(",");}sb.insert(0,"hello:");// 指定下标进行插入元素System.out.println("追加后的字符串:"+sb);// 删除后的字符串sb.deleteCharAt(0).delete(0,5);System.out.println("删除后的字符串:"+sb);sb.replace(0,5,"hello");System.out.println("替换后的字符串:"+sb);// 反转sb.reverse();System.out.println("反转后的字符串:"+sb);// 使用有参创建,每次以指定长度的作为数组的初始化容量StringBuffer sb1 = new StringBuffer(1000);// 以制定字符串进行初始化StringBuffer sb2 = new StringBuffer("hello");StringBuffer sb3 = new StringBuffer("hello");// 查看字符串sb1的长度System.out.println("sb1的长度:"+sb1.length());System.out.println("地址相等:"+(sb3 == sb2));System.out.println("内容相等:"+(sb2.equals(sb2)));System.out.println("内容相等:"+(sb2.toString().equals(sb3.toString())));///*StringBuffer 1.0 加锁 线程安全 速度慢 效率低StringBuilder 1.5 不加锁 线程不安全 速度快 效率高扩容相同:当容量不足时候,扩为原来的2倍+2如果还不够,以需要的最小容量大小为准*/String[] names = {"Bob", "Alice", "Tom"};// 单参构造:参数为拼接符// StringJoiner sj = new StringJoiner(",");// 如果拼接结果需要前面的“hello"和结尾的”!“ ,需要给StringJoinewr指定”开头和结尾“StringJoiner sj = new StringJoiner(",", "hello", "!");for(String name : names){sj.add(name);}System.out.println(sj);// 如果想快速拼接,可以使用静态方法,底层就是StringJoinerString str = String.join(",", names);System.out.println(str);}
}
三.StringJoiner
1. 使用StringBuilder拼接
String[] names = { "Bob", "Alice", "Grace" };StringBuilder sb = new StringBuilder();
sb.append("Hello ");
for (String name : names) {sb.append(name).append(", ");
}// 注意去掉最后的", ":
sb.delete(sb.length() - 2, sb.length());
sb.append("!");System.out.println(sb.toString());
2. 使用StringJoiner拼接
类似用分隔符拼接数组的需求很常见,所以Java标准库还提供了一个StringJoiner
实现类似需求:
import java.util.StringJoiner;public class Main {public static void main(String[] args) {String[] names = {"Bob", "Alice", "Grace"};StringJoiner sj = new StringJoiner(", ");for (String name : names) {sj.add(name);}System.out.println(sj.toString());}
}
如果拼接结果需要前面的"Hello "
和结尾的"!"
,需要给StringJoiner
指定“开头”和“结尾”:
import java.util.StringJoiner;public class Main {public static void main(String[] args) {String[] names = {"Bob", "Alice", "Grace"};StringJoiner sj = new StringJoiner(", ", "Hello ", "!");for (String name : names) {sj.add(name);}System.out.println(sj.toString());}
}
3. String.join( )方法
String还提供了一个静态方法join(),这个方法在内部使用了StringJoiner来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()更方便:
String[] names = {"Bob", "Alice", "Grace"};
String result = String.join(", ", names);
4. 小结
- 用指定分隔符拼接字符串数组时,使用
StringJoiner
或者String.join()
更方便 - 用
StringJoiner
拼接字符串时,还可以额外附加一个“开头”和“结尾”
四.包装类型
1. 包装类型
在Java中,数据类型被分两种:基本类型和引用类型。引用类型可以赋值为null
,表示空,但基本类型不能赋值为null
:
- 基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
- 引用类型:所有
class
和interface
类型、数组 - String s = null;
int n = null; // compile error! -
public class Integer {private int value;public Integer(int value) {this.value = value;}public int intValue() {return this.value;} }
定义好了Integer类,我们就可以把int和Integer互相转换:
-
Integer n = null; Integer n2 = new Integer(99); int n3 = n2.intValue();
对应的包装类型, 我们可以直接使用,并不需要自己去定义:
基本类型
对应的引用类型
boolean
java.lang.Boolean
byte
java.lang.Byte
short
java.lang.Short
int
java.lang.Integer
long
java.lang.Long
float
java.lang.Float
double
java.lang.Double
char
java.lang.Character
2. 自动装箱
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()
这种直接把int
变为Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer变为int的赋值写法,称为自动拆箱(Auto Unboxing)。
注意:自动装箱和自动拆箱只发生在编译阶段,目的是减少代码量
装箱和拆箱会影响代码的执行效率,因为编译后的class
代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException
3. 缓存池
对两个Integer
实例进行比较要特别注意:绝对不能用==
比较,因为Integer
是引用类型,必须使用equals()
比较,例如:
Integer x = 127;
Integer y = 127;
Integer m = 99999;
Integer n = 99999;System.out.println("x == y: " + (x==y)); // true
System.out.println("m == n: " + (m==n)); // false
System.out.println("x.equals(y): " + x.equals(y)); // true
System.out.println("m.equals(n): " + m.equals(n)); // true
可以发现,==
比较,较小的两个相同的Integer
返回true
,较大的两个相同的Integer
返回false
,这是因为Integer
内部已经把-128~+127在缓存池中已经创建好了。所以,编译器把Integer x = 127;
自动变为Integer x = Integer.valueOf(127);
就可以直接使用缓存池中的127
,从而节省内存。所以,基于缓存池的存在,Integer.valueOf()
对于-128~+127之间的数字,始终返回相同的实例,因此,==
比较“恰好”为true
,但我们绝不能因为Java标准库的Integer
内部有缓存优化就用==
比较,必须用equals()
方法比较两个Integer
。
因为Integer.valueOf()
可能始终返回同一个Integer实例,因此,在我们自己创建Integer
的时候,以下两种方法:
方法1:Integer n = new Integer(100); // 创建新实例
方法2:Integer n = Integer.valueOf(100); // 使用缓存池
4. 进制转换
Integer类本身还提供了大量方法,例如,最常用的静态方法parseInt()可以把字符串解析成一个整数:
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
Integer
还可以把整数格式化为指定进制的字符串:
System.out.println(Integer.toString(100)); // "100",表示为10进制
System.out.println(Integer.toString(100, 36)); // "2s",表示为36进制
System.out.println(Integer.toHexString(100)); // "64",表示为16进制
System.out.println(Integer.toOctalString(100)); // "144",表示为8进制
System.out.println(Integer.toBinaryString(100)); // "1100100",表示为2进制
● Java核心库提供的包装类型可以把基本类型包装为class
● 自动装箱和自动拆箱都是在编译期完成的(JDK>=1.5)
● 装箱和拆箱会影响执行效率,且拆箱时可能发生NullPointerException
● 包装类型的比较必须使用equals()
● 整数和浮点数的包装类型都继承自Number