String
Java中,String
是一个引用类型,它本身也是一个class
但是Java编译器对String有特殊处理——字符串字面量"…"
String内部通过一个
char[]
数组表示的
1. 不可变
String的特点——不可变
原因:内部的char[]是final的 + 没有修改char[]的方法
private final char[];
例子:
String s = "lzh";
System.out.println(s);//lzh
s = s.toUpperCase();
System.out.println(s);//LZH
不要以为字符串变了,并没有!变得是s这个引用变量的指向而已,只是原来指向lzh的变量没有了
s.toUpperCase();如果不指向s,那么打印s仍然是lzh
这个方法没有修改原字符串,而是产生了一个新字符串,只不过把返回值指向了s
2. 字符串比较
要比较两个字符串是否相同,比较的是内容,必须使用equals()
,而不是==
String s1 = "lzh";
String s2 = "lzh";
System.out.println(s1==s2);//true
System.out.println(s1.equals(s2));//true
==如果用在引用类型上,它比较的是二者的引用是否相同,是否指向同一个对象
这里对于字符串字面量
比较特殊,因为编译器编译时把字面量一样的字符串当做一个对象放入到常量池
,所以这里s1和s2都指向了常量池的那个对象
将s2进行修改
String s2 = "LZH".toLowerCase();
此时==就会返回false
3. 字符串API
搜寻、提取子串
- s.contains(“…”)
- s.indexOf(“…”)
- s.lastIndexOf(“…”)
- s.startsWith(“…”)
- s.endsWith(“…”)
提取子串:
- s.substring(索引)
- s.substring(起始索引,结束索引) 不包含结束索引
去除首尾空格
包括空格、回车、Tab
- s.trim()
判空
- s.isEmpty() s是否为空串""
- s.isBlank() s是否为空白字符串:" “、”\n \t"等
替换子串
- s.replace(‘.’,‘.’) 根据字符替换所有的该字符
- s.replace(“…”,“…”)
根据正则替换:
s.replaceAll(正则表达式,“…”)
分割
s.split(“…”) 得到String[]
拼接
String.join(“…”,arr) arr是一个String[],用指定字符串来连接数组
格式化
s中包含若干占位符
- s.formatted(占位符)
- String.format(s,占位符) 静态的方法
String ss = "name is %s,age is %d";
System.out.println(ss.formatted("jfc",22));//name is jfc,age is 22
System.out.println(String.format(ss, "jfc",22));//name is jfc,age is 22
类型转换
- 把其他类型转为String类型——静态方法String.valueOf()
引用类型转为String,得到的是全类名和地址
System.out.println(String.valueOf(111)+1);//1111
System.out.println(String.valueOf(new Object()));//java.lang.Object@2a84aee7
- 字符串转为其他类型,要根据情况
例如把字符串转为int:
System.out.println(Integer.parseInt("34"));//34
转换为char[]
String和char[]可以互相转换
- s.toCharArray() 得到一个char[]
- new String(char[]) 得到一个String
修改char[],String不会变
char[] cs = "Hello".toCharArray();
String s = new String(cs);
System.out.println(s);
cs[0] = 'X';
System.out.println(s);
因为通过new String(char[])创建新的String实例时,它并不会直接引用传入的char[]数组,而是会复制一份,所以,修改外部的char[]数组不会影响String实例内部的char[]数组,因为这是两个不同的数组。
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
如果this.value = value;这样写,那就是指向了同一个数组了,外面修改,里面也会变
4. 字符编码
早期,ANSI制定了
ANSII
码,用来表示英文字母、数字、常用符号,这些共有128个,编码范围:0~127,使用1个字节,但是只用了7位,最高位始终为0。例如’A’:0x41为了把汉字纳入,1字节不够。
GB2312
使用2字节表示汉字,并把最高位始终设为1,以便和ASCII区分。例如’中’:0xd6d0为统一各国语言,发布
Unicode
编码,Unicode 给所有的字符指定了一个数字用来表示该字符。它仅仅只是一个字符集,规定了符合对应的二进制代码,至于这个二进制代码如何存储则没有任何规定。
'A’的ASCII:0x41,Unicode:0x0041英文字符的Unicode编码就是简单地在前面添加一个00字节。
假如 Unicode 中最大的字符用 4 字节就可以表示了,那么我们就将所有的字符都用 4 个字节来表示,不够的就往前面补 0。这样确实可以解决编码问题,但是却造成了空间的极大浪费,如果是一个英文文档,那文件大小就大出了 3 倍,这显然是无法接受的。UTF-8 是目前互联网上使用最广泛的一种 Unicode 编码方式,它的最大特点就是可变长。它可以使用 1 - 4 个字节表示一个字符,根据字符的不同变换长度。编码规则如下:
对于单个字节的字符,第一位设为 0,后面的 7 位对应这个字符的 Unicode 码点。因此,对于英文中的 0 - 127 号字符,与 ASCII 码完全相同。这意味着 ASCII 码那个年代的文档用 UTF-8 编码打开完全没有问题。
对于需要使用 N 个字节来表示的字符(N > 1),第一个字节的前 N 位都设为 1,第 N + 1 位设为0,剩余的 N - 1 个字节的前两位都设位 10,剩下的二进制位则使用这个字符的 Unicode 码点来填充。
下面以汉字“汉”为利,具体说明如何进行 UTF-8 编码和解码
“汉”的 Unicode 码点是
0x6c49
(110 1100 0100 1001),通过上面的对照表可以发现,0x0000 6c49
位于第三行的范围,那么得出其格式为1110xxxx 10xxxxxx 10xxxxxx
。接着,从“汉”的二进制数最后一位开始,从后向前依次填充对应格式中的 x,多出的 x 用 0 补上。这样,就得到了“汉”的 UTF-8 编码为11100110 10110001 10001001
,转换成十六进制就是0xE6 0xB7 0x89
。解码的过程也十分简单:如果一个字节的第一位是 0 ,则说明这个字节对应一个字符;如果一个字节的第一位1,那么连续有多少个 1,就表示该字符占用多少个字节。
UTF-8编码的另一个好处是容错能力强。如果传输过程中某些字符出错,不会影响后续字符,因为UTF-8编码依靠高字节位来确定一个字符究竟是几个字节,它经常用来作为传输编码。
在Java中,char就是2字节的Unicode编码
如果我们要手动把字符串转换成其他编码,可以这样做:
byte[] b1 = "Hello".getBytes(); // 按系统默认编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
注意:转换编码后,就不再是char类型,而是byte类型表示的数组。
如果要把已知编码的byte[]转换为String,可以这样做:
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
始终牢记:Java的String和char在内存中总是以Unicode编码表示。
延伸
新版JDK采用byte[]
如果字符串只包含ASCII字符,则每个byte存储一个字符;否则每两个字节存储一个字符
目的:大量较短的字符串通常只包含ASCII字符,可以节省内存
每个char是2字节,而ASCII只需要1字节就可以,这样每个ASCII字符就浪费了一个字节
使用byte[],都是ASCII就可以使用1byte;有汉字就要使用2byte了(英文00+一个0x数字,中文:两个16进制数,每个16进制数是两位)
4个二进制数字可以转为1位16进制数字,8个二进制为1字节——两位16进制
而Unicode的一个汉字使用两个16进制数字表示
小结
Java字符串String是不可变对象;
字符串操作不改变原字符串内容,而是返回新字符串;
常用的字符串操作:提取子串、查找、替换、大小写转换等;
Java使用Unicode编码表示String和char;
转换编码就是将String和byte[]转换,需要指定编码;
转换为byte[]时,始终优先考虑UTF-8编码。
StringBuilder
Java编译器对String做了特殊处理,使得我们可以直接用+拼接字符串。
由于字符串是不可变的,所以每次拼接都是产生了一个新字符串
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}
每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
StringBuilder
是一个可变对象
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();
StringBuilder还可以进行链式操作:
sb.append("Mr ")
.append("Bob")
.append("!")
.insert(0, "Hello, ");
进行链式操作的关键是,定义的append()方法会返回this,这样,就可以不断调用自身的其他方法。
StringJoiner
很多时候,我们拼接的字符串像这样: Hello Bob, Alice, Grace!
String[] names = {"Bob", "Alice", "Grace"};
var 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());
类似用分隔符拼接数组的需求很常见,所以Java标准库还提供了一个StringJoiner来干这个事
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ", "Hello ", "!");//把String[]的除最后一个都用这个拼接,可以指定开头和结尾
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());//最后还转成String类型
静态方法join()
,这个方法在内部使用了StringJoiner来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()更方便:
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);
包装类
Java的数据类型分两种:
基本类型:byte,short,int,long,boolean,float,double,char
引用类型:所有class和interface类型
如何把一个基本类型视为对象
可以定义一个Integer类,它只包含一个实例字段int,这样,Integer类就可以视为int的包装类
public final class Integer {
private final int value;
public Integer(int value) {
this.value = value;
}
public int intValue() {
return this.value;
}
}
Java核心库为每种基本类型都提供了对应的包装类型
int i = 100;
Integer n1 = new Integer(i);// 通过new操作符创建Integer实例(不推荐使用,会有编译警告)
Integer n2 = Integer.valueOf(i);// 通过静态方法valueOf(int)创建Integer实例
Integer n3 = Integer.valueOf("100");// 通过静态方法valueOf(String)创建Integer实例
System.out.println(n3.intValue());// 通过intValue方法返回数值
Auto Boxing
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();
Java编译器可以帮助我们自动进行转换
Integer n = 100; // 编译器自动使用Integer.valueOf(int) 自动装箱
int x = n; // 编译器自动使用Integer.intValue() 自动拆箱
注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。
装箱和拆箱会影响代码的执行效率,因为编译后的class代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException
Integer n = null;
int i = n;
不可变
所有的包装类型都是不变类
public final class Integer {
private final int value;
}
一旦创建了Integer对象,该对象就是不变的。
对两个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
为什么使用==时,127是true,而99999是false?
这是为了节省内存,Integer.valueOf()方法对于较小的数进行了缓存,这些数使用不同的变量指向的都是缓存中的那一个
不能因此而使用==
来判断两个Integer是否相等
因为Integer.valueOf()可能始终返回同一个Integer实例,因此,在我们自己创建Integer的时候,以下两种方法:
- 方法1:Integer n = new Integer(100);
- 方法2:Integer n = Integer.valueOf(100);
方法1总是创建新的Integer实例,方法2做了优化
我们把能创建“新”对象的静态方法称为静态工厂方法。Integer.valueOf()就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
创建新对象时,优先选用静态工厂方法而不是new操作符。
Byte.valueOf(),返回的Byte实例全部是缓存实例,byte表示-128~127都在缓存中
进制转换
Integer类本身还提供了大量方法
- 静态方法parseInt()
可以把k进制的字符串解析成对应的十进制整数
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
int x = Integer.parseInt("10001100", 2);//140
System.out.println(x);//输出的是该字符串转换后的 十进制数
- 把十进制数字转为k进制的字符串
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进制
注意:进制转换时,k进制的数字都是字符串
在计算机内存中,只用二进制表示,不存在十进制或十六进制的表示方法
int n = 100在内存中总是以4字节的二进制表示
System.out.println(n);输出的是n的十进制,这是依靠核心库自动把整数格式化为10进制输出并显示在屏幕上,
使用Integer.toHexString(n)则通过核心库自动把整数格式化为16进制
所有的整数和浮点数的包装类型都继承自Number,因此,可以非常方便地直接通过包装类型获取各种基本类型
// 向上转型为Number:
Number num = new Integer(999);
// 获取byte, int, long, float, double:
byte b = num.byteValue();
int n = num.intValue();
long ln = num.longValue();
float f = num.floatValue();
double d = num.doubleValue();
处理无符号整型
在Java中,并没有无符号整型(Unsigned)的基本数据类型。byte、short、int和long都是带符号整型,最高位是符号位。
无符号整型和有符号整型的转换在Java中就需要借助包装类型的静态方法完成。
例如,byte是有符号整型,范围是-128 - 127,但如果把byte看作无符号整型,它的范围就是0~255。
byte x = -1;
byte y = 127;
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
-1的二进制是11111111
,因为负数是以补码存储的(为了减法操作和0这个特殊数字,0是+0,没有-0)
11111111按无符号转换就是255
JavaBean
在Java中,有很多class的定义都符合这样的规范:
- 若干private实例字段;
- 通过public方法来读写实例字段。
那么这种class被称为JavaBean
boolean字段比较特殊,它的读方法一般命名为isXyz()
通常把一组对应的读方法(getter)和写方法(setter)称为属性(property)。例如,name属性
属性只需要定义getter和setter方法,不一定需要对应的字段。
public boolean isChild() {
return age <= 6;
}
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。
要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的Introspector