包括:字符串和编码、StringBuilder类、StringJoiner类、包装类型、缓存机制、JavaBean
输出结果都写在了注释中
字符串和编码
基本特性
在Java中,String是引用类型,本身也是一个class(java.lang.String
类),可以使用字面量 "..."
或构造方法 new String(...)
来表示,字符串内容存储在不可变的 char[]
或 byte[]
中,一旦创建,字符串内容不可更改。
String s1 = "Hello!";
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
不可变性:是通过内部的private final char[]
字段,以及没有任何修改char[]
的方法实现的。
public class demo26 {
public static void main(String[] args){
String s = "Hello";
System.out.println(s);
// Hello
s = s.toUpperCase();
System.out.println(s);
// HELLO
}
}
原字符串 s
并未被修改,而是返回了新字符串
字符串比较
比较方式 | 描述 | 示例 | 使用 |
---|---|---|---|
== | 比较引用是否相同 | s1 == s2 | ❌ |
equals() | 比较内容是否相同 | s1.equals(s2) | ✅ |
equalsIgnoreCase() | 比较内容忽略大小写 | s1.equalsIgnoreCase(s2) | ✅ |
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
String s3 = "hello";
System.out.println(s1 == s3);
// true
System.out.println(s1 == s2);
// false
System.out.println(s1.equals(s2));
// true
Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然s1
和s3
的引用就是相同的,所以,这种==
比较返回true
纯属巧合。
常用字符串方法
方法 | 作用 | 示例 | 返回值 |
---|---|---|---|
contains() | 判断是否包含子串 | "Hello".contains("ll") | true |
indexOf() | 查找首次出现位置 | "Hello".indexOf("l") | 2 |
lastIndexOf() | 查找最后出现位置 | "Hello".lastIndexOf("l") | 3 |
startsWith() | 是否以...开头 | "Hello".startsWith("He") | true |
endsWith() | 是否以...结尾 | "Hello".endsWith("lo") | true |
substring() | 从索引开始提取 | "Hello".substring(2) | "llo" |
substring() | 提取指定范围子串 | "Hello".substring(2, 4) | "ll" |
trim() | 去除首尾空白字符 | " Hello ".trim() | "Hello" |
strip() | 去除首尾Unicode空白 | "u3000Hellou3000".strip() | "Hello" |
isEmpty() | 是否为空字符串 | "".isEmpty() | true |
isEmpty() | 是否为空字符串 | " ".isEmpty() | false |
isBlank() | 是否全为空白字符 | " n".isBlank() | true |
字符串替换
方法 | 描述 | 示例 | 结果 |
---|---|---|---|
replace() | 替换字符 | "hello".replace('l', 'w') | "hewwo" |
replace() | 替换子串 | "hello".replace("ll", "~~") | "he~~o" |
replaceAll(regex, replacement) | 使用正则替换 | "A,,B;C ,D".replaceAll("[,;\s]+", ",") | "A,B,C,D" |
字符串分割与拼接
分割:split()
String s = "A,B,C,D";
String[] parts = s.split(","); // {"A", "B", "C", "D"}
拼接:join()
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
字符串格式化
方法 | 示例 |
---|---|
formatted(...) | "Hi %s".formatted("Tom") |
String.format(...) | String.format("Score: %.2f", 88.5) |
public class Main {
public static void main(String[] args) {
String s = "Hi %s, your score is %d!";
System.out.println(s.formatted("Alice", 80));
// Hi Alice, your score is 80!
System.out.println(String.format("Hi %s, your score is %.2f!", "Bob", 59.5));
// Hi Bob, your score is 59.50!
}
}
字符串类型转换
转换 | 示例 | 结果 |
---|---|---|
任意类型 → String | String.valueOf(123) | "123" |
String → int | Integer.parseInt("123") | 123 |
String → int (hex) | Integer.parseInt("ff", 16) | 255 |
String → boolean | Boolean.parseBoolean("TRUE") | true |
Integer.getInteger("key")
是读取系统属性,不用于字符串转整数。
Integer.getInteger("java.version"); // 版本号,17
String 与 char[] 相互转换:
char[] cs = "Hello".toCharArray(); // String → char[]
String s = new String(cs); // char[] → String
不可变性:
char[] cs = "Hello".toCharArray();
String s = new String(cs);
cs[0] = 'X';
System.out.println(s);
// Hello
这是因为通过new String(char[])
创建新的String
实例时,它并不会直接引用传入的char[]
数组,而是会复制一份,所以,修改外部的char[]
数组不会影响String
实例内部的char[]
数组,因为这是两个不同的数组。
从String
的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。
import java.util.Arrays;
public class demo27 {
public static void main(String[] args){
int[] scores = new int[]{88,77,51,66};
Score s = new Score(scores);
s.printScore();
// [88, 77, 51, 66]
scores[2] = 99;
s.printScore();
// [88, 77, 99, 66]
}
}
class Score{
private int[] scores;
public Score(int[] scores){
this.scores = scores;
}
public void printScore(){
System.out.println(Arrays.toString(scores));
}
}
由于Score
内部直接引用了外部传入的int[]
数组,这会造成外部代码对int[]
数组的修改,影响到Score
类的字段。如果外部代码不可信,这就会造成安全隐患。
正确示例:
class Score {
private int[] scores;
public Score(int[] scores) {
this.scores = Arrays.copyOf(scores, scores.length);
}
}
字符编码与转换
编码 | 用途 | 举例 |
---|---|---|
ASCII | 英文、数字、基本符号 | 'A' → 0x41 |
GB2312 | 中文编码 | '中' → 0xd6d0 |
Unicode | 全球统一编码 | '中' → 0x4e2d |
UTF-8 | Unicode变长编码(1-4字节) | '中' → 0xe4b8ad |
英文字符的Unicode
编码高字节总是00
,包含大量英文的文本会浪费空间,所以,出现了UTF-8
编码,它是一种变长编码,用来把固定长度的Unicode
编码变成1~4字节的变长编码。通过UTF-8
编码,英文字符'A'
的UTF-8
编码变为0x41
,正好和ASCII
码一致,而中文'中'
的UTF-8
编码为3字节0xe4b8ad
。
编码转换方法:
Java的String
和char
在内存中总是以Unicode
编码表示。
byte[] b1 = "Hello".getBytes("UTF-8"); // String → byte[]
String s1 = new String(b1, "UTF-8"); // byte[] → String
建议使用 StandardCharsets.UTF_8
替代字符串常量:
byte[] b = "Hello".getBytes(StandardCharsets.UTF_8);
String s = new String(b, StandardCharsets.UTF_8);
StringBuilder
普通字符串拼接:
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}
由于String
在Java中是不可变对象,一旦创建就不能修改,所以每次拼接都会创建一个新的String
对象,大量临时对象会占用内存并增加GC负担。
使用StringBuilder
拼接:
public class demo28 {
public static void main(String[] args){
StringBuilder sb = new StringBuilder(10); // 预分配缓冲区,提高效率
for(int i = 0;i<10;i++){
sb.append(i);
sb.append(',');
}
String s = sb.toString();
System.out.println(s);
// 0,1,2,3,4,5,6,7,8,9,
//链式操作
// append()、insert()方法返回的是this(当前对象),从而支持链式调用
var sb1 = new StringBuilder(10); // var?
sb1.append("Mr ")
.append("Bob")
.append("!")
.insert(0, "Hello, ");
System.out.println(sb1.toString());
// Hello, Mr Bob!
}
}
StringBuilder
是java.lang.StringBuilder
类型,是可变对象,可以预分配缓冲区
var是什么?
项目 | 内容 |
---|---|
引入版本 | Java 10 |
是否是关键字 | 否,是保留类型名 |
是否强类型语言 | 是,var 只是简化书写,类型在编译时确定 |
使用位置限制 | 只能用于局部变量,不能用于字段、方法参数、返回值等 |
是否必须初始化 | 是,且不能赋 null ,必须能推断出类型 |
不能使用的情况 | var a; ❌(未初始化)var x = null; ❌(类型不明确) |
可读性建议 | 对复杂泛型或长类型名建议使用 var ,简单类型可写明类型提高可读性 |
常见等价示例 | var name = "Tom"; 等价于 String name = "Tom"; |
错误示例 | var x = 123; x = "str"; ❌(类型冲突) |
仿照StringBuilder
,我们也可以设计支持链式操作的类:
public class demo29 {
public static void main(String[] args) {
Adder adder = new Adder();
adder.add(3) // sum = 0 + 3 = 3
.add(5) // sum = 3 + 5 = 8
.inc() // sum = 8 + 1 = 9
.add(10); // sum = 9 + 10 = 19
System.out.println(adder.value());
// 19
}
}
class Adder{
private int sum = 0;
//该方法的返回类型是Adder
public Adder add(int n){
sum += n;
// 返回当前对象,返回类型是Adder,从而实现链式调用
return this;
}
public Adder inc() {
sum++;
return this;
}
public int value() {
return sum;
}
}
优化的字符串+
拼接
对于普通的变量拼接,并不需要改写为StringBuilder
因为Java编译器在编译时就自动把多个连续的+
操作编码为StringConcatFactory
,优化为更高效的字节码(通常内部使用StringBuilder
或数组复制)
String s = "Hello, " + "world" + "!";
StringBuilder
和 StringBuffer
特性 | StringBuilder | StringBuffer |
---|---|---|
可变性 | 是 | 是 |
线程安全 | 否 | 是(使用synchronized ) |
性能 | 更高 | 较低(同步开销) |
是否推荐使用 | 推荐 | 不推荐(除非必须线程安全) |
接口 | 完全相同 | 完全相同 |
使用StringBuilder
构造INSERT
语句
public class demo14 {
public static void main(String[] args){
String[] fields = {"name","position","salary"};
String table = "employee";
String insert = buildInsertSql(table,fields);
System.out.println(insert);
// INSERT INTO employee(name,position,salary) VALUES(?,?,?)
String s = "INSERT INTO employee(name,position,salary) VALUES(?,?,?)";
System.out.println(s.equals(insert) ? "测试成功":"测试失败");
// 测试成功
}
static String buildInsertSql(String table,String[] fields){
var sb = new StringBuilder(1024);
sb.append("INSERT INTO ").append(table).append("(");
for(int i = 0;i<fields.length;i++){
sb.append(fields[i]);
if(i != fields.length-1){
sb.append(",");
}
}
sb.append(") VALUES(");
for(int i = 0;i<fields.length;i++){
sb.append("?");
if(i != fields.length-1){
sb.append(",");
}
}
sb.append(")");
return sb.toString();
}
}
StringJoiner
在 Java 中,频繁拼接字符串时如果使用 +
运算符,会因产生大量中间字符串而导致性能低下。为了提高效率,可以使用如下工具:
方法 | 描述 | 示例 |
---|---|---|
StringBuilder | 可变字符串容器,适合频繁拼接 | sb.append(...) |
StringJoiner | 自动添加分隔符、可指定前缀/后缀 | new StringJoiner(", ", "前缀", "后缀") 或 new StringJoiner("分隔符") |
String.join() | 静态方法,内部使用StringJoiner 实现 | String.join(", ", arr) |
import java.util.StringJoiner;
public class demo15 {
public static void main(String[] args){
String[] names = {"Bob","Alice","Grace"};
// 用StringBuilder来打印:
var sb = new StringBuilder();
sb.append("Hello ");
for (String name : names) {
sb.append(name).append(", ");
}
// sb.delete(start, end) 会删除[start,end)之间的字符,start和end代表索引
sb.delete(sb.length() - 2, sb.length());
sb.append("!");
System.out.println(sb.toString());
// Hello Bob, Alice, Grace!
// 用StringJoiner来打印:
// new StringJoiner(分隔符,前缀,后缀)
var sj = new StringJoiner(", ","Hello ","!");
for(String name:names){
sj.add(name);
}
System.out.println(sj.toString());
// Hello Bob, Alice, Grace!
// 用String.join()来打印:
var result = String.join(", ", names);
System.out.println(result);
// Bob, Alice, Grace
}
}
使用StringJoiner
构造SELECT
语句
import java.util.StringJoiner;
public class demo16 {
public static void main(String[] args){
String[] fields = {"name","position","salary"};
String table = "employee";
String select = buildSelectSql(table,fields);
System.out.println(select);
// SELECT name, position, salary FROM employee
System.out.println("SELECT name, position, salary FROM employee".equals(select) ? "测试成功" : "测试失败");
// 测试成功
}
static String buildSelectSql(String table,String[] fields){
var sj = new StringJoiner(", ","SELECT "," FROM " + table);
for(String field:fields){
sj.add(field);
}
return sj.toString();
}
}
包装类型
Java的数据类型分两种:
类别 | 类型说明 |
---|---|
基本类型 | byte ,short ,int ,long ,float ,double ,boolean ,char |
引用类型 | 所有class 和interface 类型 |
基本类型不能为 null
,但引用类型可以
String s = null; // OK
int n = null; // 编译错误!
可以定义一个类,把一个基本类型视为对象(引用类型):
public class Integer {
private int value;
public Integer(int value) {
this.value = value;
}
public int intValue() {
return this.value;
}
}
Integer
对象只包含一个实例字段int
,可以视为是int
的包装类(Wrapper Class)
实际上Java核心库为每个基本类型提供了对应的包装类,使其可以作为对象使用
基本类型 | 包装类型 | 所属包 |
---|---|---|
boolean | Boolean | java.lang |
byte | Byte | |
short | Short | |
int | Integer | |
long | Long | |
float | Float | |
double | Double | |
char | Character |
可以直接使用,并不需要自己去定义:
int i = 100;
// 通过new操作符创建Integer实例(不推荐使用,会有编译警告):
Integer n1 = new Integer(i);
// 通过静态方法valueOf()创建Integer实例:
Integer n2 = Integer.valueOf(i);
Integer n3 = Integer.valueOf("100");
System.out.println(n3.intValue());
Integer.valueOf()
是创建包装类对象的一个静态工厂方法, 它尽可能地返回缓存的实例以节省内存
自动装箱与自动拆箱(Auto Boxing / Unboxing)
int
和Integer
可以互相转换:
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();
所以,Java编译器可以帮助我们自动在int
和Integer
之间转型,提高效率
操作 | 示例代码 | 编译器处理方式 |
---|---|---|
自动装箱 | Integer n = 100; | 编译器转换为Integer.valueOf(100) |
自动拆箱 | int x = n; | 编译器转换为n.intValue() |
⚠ 注意: 自动拆箱可能导致
NullPointerException
:
Integer n = null;
int i = n; // 抛出 NullPointerException
不变类
包装类如 Integer
是不可变的,它的核心代码如下:
public final class Integer {
private final int value;
}
不要使用 ==
比较包装类对象,应使用 equals()
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true (缓存优化)
System.out.println(x.equals(y)); // true
Integer m = 99999;
Integer n = 99999;
System.out.println(m == n); // false
System.out.println(m.equals(n)); // true
因为Integer
是不变类,编译器把Integer x = 127
;自动变为Integer x = Integer.valueOf(127)
但它并不是每次都创建一个新的Integer
对象,而是会在一定范围内复用已有的对象实例,也就是会使用缓存
缓存机制(Cache):
Java 标准库中 Integer.valueOf(int)
的实现逻辑大致如下:
public static Integer valueOf(int i) {
if (i >= -128 && i <= 127) {
return IntegerCache.cache[i + 128];
}
return new Integer(i);
}
-128
到 127
范围内的整数会被缓存在一个叫 IntegerCache
的数组中
这些值的 Integer
实例会提前创建并缓存
每次调用 valueOf()
都返回这个缓存数组中的实例
超过这个范围的值,valueOf()
每次都会新建一个 Integer
对象的实例
缓存机制能够节省内存,提高性能,避免重复创建大量相同的 Integer
对象,减少垃圾回收压力,提升运行效率
而==
比较的是对象的地址,不是值
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true 因为使用缓存,x和y指向同一个对象(实例)
Integer m = 128;
Integer n = 128;
System.out.println(m == n); // false 因为超出缓存范围,m和n是Integer对象的两个不同的实例
常用静态方法
- 字符串与整数互转
int a = Integer.parseInt("100");
int b = Integer.parseInt("100", 16); // 256(按16进制解析)
- 整数转不同进制字符串
Integer.toString(100); // "100"
Integer.toString(100, 36); // "2s",36进制
Integer.toHexString(100); // "64",16进制
Integer.toOctalString(100); // "144",8进制
Integer.toBinaryString(100); // "1100100",2进制
常用静态字段
// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;
// int可表示的最大/最小值:
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648
// long类型占用的bit和byte数量:
int sizeOfLong = Long.SIZE; // 64 (bits)
int bytesOfLong = Long.BYTES; // 8 (bytes)
Number类
包装类 Integer
, Float
, Double
等都继承自 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
都是带符号整型,最高位是符号位
例如,byte是有符号整型,范围是-128
~+127
,但如果把byte看作无符号整型,它的范围就是0
~255
把负的byte
按无符号整型转换为int
:
public class Main {
public static void main(String[] args) {
byte x = -1;
byte y = 127;
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
}
}
因为byte
的-1
的二进制表示是11111111
,以无符号整型转换后的int
就是255
类型 | 转换目标 | 方法 |
---|---|---|
byte | →int 无符号 | Byte.toUnsignedInt(byte) |
short | →int 无符号 | Short.toUnsignedInt(short) |
int | →long 无符号 | Integer.toUnsignedLong(int) |
JavaBean
JavaBean是一种符合特定命名规范的 Java 类,主要用于数据封装和传输。
JavaBean 的特征:
- 类必须是
public
。 - 提供一个无参构造方法。
- 所有字段为
private
。 - 提供
getter
和setter
方法来访问字段。
JavaBean 属性命名规范:
类型 | 命名格式 | 示例方法名 |
---|---|---|
普通字段属性 | getXxx() /setXxx(Type value) | getName() ,setName(...) |
布尔类型属性 | isXxx() /setXxx(boolean value) | isChild() ,setChild(...) |
属性(Property)说明:
类型 | 特征 | 示例方法 |
---|---|---|
读写属性 | 同时有 getter 和 setter | getName() ,setName(...) |
只读属性 | 只有 getter | getAge() |
只写属性 | 只有 setter | setPassword(...) (较少见) |
标准的JavaBean:
public class Person {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
// 只读属性
public boolean isChild() {
return age <= 6;
}
}
JavaBean 的作用:
用途 | 描述 |
---|---|
数据封装 | 将一组数据组合成一个对象传输 |
IDE工具支持 | 可视化工具可以自动识别 getter/setter 并快速生成 |
属性反射处理 | 使用Introspector 可反射获取属性信息,用于框架/工具自动配置 |
使用 Introspector枚举 JavaBean 的属性:
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
public class demo17 {
public static void main(String[] args) throws Exception {
BeanInfo info = Introspector.getBeanInfo(Person.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println("属性: " + pd.getName());
// 属性: age
// 读方法: public int Exp03核心类.Person.getAge()
// 写方法: public void Exp03核心类.Person.setAge(int)
System.out.println(" 读方法: " + pd.getReadMethod());
// 属性: class
// 读方法: public final native java.lang.Class java.lang.Object.getClass()
// 写方法: null
System.out.println(" 写方法: " + pd.getWriteMethod());
// 属性: name
// 读方法: public java.lang.String Exp03核心类.Person.getName()
// 写方法: public void Exp03核心类.Person.setName(java.lang.String)
}
}
}
class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Comments NOTHING