这里写目录标题
😀 续上篇,今天来复习下Java数组&字符串相关知识,冲冲冲!!!
📝 Java基础
数组
深入浅出,掌握更多数组知识!
数组理论基础
数组是存放在连续内存空间上的相同类型数据的集合。
在Java中,ArrayList的内部就是使用数组实现的。
数组是一种线性表结构,线性表结构,顾名思义就是,将存储的数据排成一条线一样的结构,存储的每个数据最多只有前后两个方向。
数组是一个对象,它包含了一组固定数量的元素,并且这些元素的类型是相同的。数组会按照索引的方式将元素放在指定的位置上,意味着我们可以通过索引来访问这些元素。在 Java 中,索引是从 0 开始的。
**因为数组的在内存空间的地址是连续的,**使得数组按照下标随机访问(随机访问:可以用同等的时间访问到一组数据中的任意一个元素)数组中数据元素时间复杂度达到 O(1) 级别。
计算机会为每一个内存单元分配一个地址,并通过该地址来访问内存中的数据。数组在内存空间中的地址是连续的,当我们知道数据的首地址后,通过公式便可以计算出元素的内存地址,让计算机直接访问,达到 O(1) 级别的时间复杂度。
我们是通过数组下标访问数据时,时间复杂度才是 O(1)
当我们通过数据查找元素时,我们需要遍历数组查找对应的数据,时间复杂度是 O(n)。
// i 表示数组下标, base_address 表示数组首地址,
// data_type_size 表示数组中每个数据大小
a[i]_address=base_address+i*data_type_size
但是我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
例如 删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:
数组具有以下特点:
-
在存储空间中按顺序存储,地址连续。
-
数值数组元素的默认值为 0,而引用元素的默认值为 null。
-
数组的索引从 0 开始,如果数组有 n 个元素,那么数组的索引是从 0 到(n-1)。
-
数组元素可以是任何类型,包括数组类型。
-
数组的元素是不能删的,只能覆盖,平时删除操作也是依次用后一位覆盖,因为申请且初始化后,存储空间就固定了。
数组的声明
首先必须声明数组变量,才能在程序中使用数组。
dataType[] arrayRefVar; // 首选的方法
int[] array;
//或
dataType arrayRefVar[]; // 效果相同,但不是首选方法
int array[];
数组的创建
//创建数组的方式
array =new dataType[arraySize];
int[] arrays =new int[5];
//创建方式二
dataType[] array={value0,value1,....,valuen};
int[] arrays0 = new int[]{1,2};
array =new dataType[arraySize];
代码分析
1、使用dataType[arraySize]创建一个数组。
2、把新创建的数组引用赋值给变量array。
数组变量的声明并创建数组:
dataType[] array=new dataType[arraySize];
数组的常用操作
1、数组的访问
变量名,加上中括号,加上元素的索引,就可以访问到数组。
由于数组的索引是从 0 开始,所以最大索引为 length - 1
。
int[] array= new int[]{1,2};
array[0]=1;
2、数组遍历
数组创建后,元素类型和数组的大小都是确定的。遍历数组时可以选取for循环或者For-Each循环。
- for循环
double[] arr={1.1,2.2,3.3};
for (int i=0;i<arr.length;i++){
System.out.println(arr[i]);
}
- For-Each循环
For-Each 循环或者加强型循环,它能在不使用下标的情况下遍历数组。
for (double element:arr
) {
System.out.println(element);
}
3、数组排序
对数组进行排序的话,可以使用 Arrays 类提供的 sort()
方法。
//全部元素升序
int[] array = new int[] {5, 2, 1, 4, 8};
Arrays.sort(array);//[1, 2, 4, 5, 8]
//对于部分元素排序,对 1-3 位置上的元素进行反序.
String[] arrays = new String[] {"A", "E", "Z", "B", "C"};
Arrays.sort(arrays, 1, 3,Comparator.comparing(String::toString).reversed());
//[A, Z, E, B, C]
4、数组复制
-
简单赋值
简单赋值会复制数组引用,而不是数组本身。这意味着修改新数组会影响原数组。
int[] originalArray = {1, 2, 3, 4, 5}; int[] newArray = originalArray; newArray[0] = 99; System.out.println(Arrays.toString(originalArray)); // 输出 [99, 2, 3, 4, 5]
💡System.out.println(Arrays.deepToString(array));输出二维数组 -
手动遍历复制
手动遍历复制每个元素,创建一个新的数组,这样修改新数组不会影响原数组。
int[] originalArray = {1, 2, 3, 4, 5}; int[] newArray = new int[originalArray.length]; for(int i = 0;i<originalArray.length;i++){ newArray[i]= originalArray[i]; } newArray[0] = 99; System.out.println(Arrays.toString(originalArray)); // 输出 [1, 2, 3, 4, 5] System.out.println(Arrays.toString(newArray)); // 输出 [99, 2, 3, 4, 5]
-
使用
Arrays.copyOf
Arrays.copyOf
方法用于复制整个数组或数组的前几个元素。int[] originalArray = {1, 2, 3, 4, 5}; int[] newArray = Arrays.copyOf(originalArray, originalArray.length); newArray[0] = 99; System.out.println(Arrays.toString(originalArray)); // 输出 [1, 2, 3, 4, 5] System.out.println(Arrays.toString(newArray)); // 输出 [99, 2, 3, 4, 5]
-
使用
Arrays.copyOfRange
Arrays.copyOfRange
方法用于复制数组的指定范围。int[] originalArray = {1, 2, 3, 4, 5}; //复制从索引 1 到索引 4(不包括索引 4)的元素。 int[] newArray = Arrays.copyOfRange(originalArray, 1, 4); newArray[0] = 99; System.out.println(Arrays.toString(originalArray)); // 输出 [1, 2, 3, 4, 5] System.out.println(Arrays.toString(newArray)); // 输出 [99, 3, 4]
-
使用
System.arraycopy
System.arraycopy
是一个 Java 中的本地方法(native method),用于高效地复制数组的一部分内容到另一个数组中。public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); /** src:源数组,即要复制数据的原始数组对象。 srcPos:源数组中的起始位置索引,从该索引开始复制数据。 dest:目标数组,即将数据复制到的目标数组对象。 destPos:目标数组中的起始位置索引,从该索引开始将数据复制到目标数组。 length:要复制的元素数量,即从源数组复制到目标数组的元素个数。 **/ int[] sourceArray = {1, 2, 3, 4, 5}; int[] targetArray = new int[5]; // 将 sourceArray 中索引为 1 到 3 的元素复制 // 到 targetArray 中的索引 2 到 4 (也就是3个元素)的位置 System.arraycopy(sourceArray, 1, targetArray, 2, 3); // 打印输出目标数组内容 System.out.println(Arrays.toString(targetArray)); // 输出 [0, 0, 2, 3, 4]
length - 1
,假设数组长度是5,那么数组索引最大就是 4,所以当我们使用 5 作为索引的时候,就会抛出ArrayIndexOutOfBoundsException
异常。
多维数组
二维数组是一种数据类型,可以存储多行和多列的数据。
二维数组可以看成是数组的数组,三维数组可以看成二维数组的数组。
比如二维数组就是一个特殊的一维数组,其每一个元素都是一个一维数组.
//一个3行4列的二维数组
array = [
[a, b, c, d],
[e, f, g, h],
[i, j, k, l]
]
//元素 array[1][2] 是第2行第3列的元素,它的值是 g。
多维数组声明
数据类型[][] 数组名称;
数据类型[] 数组名称[];
数据类型 数组名称[][];
int[][] array;
char arrays[][];
数据类型[][][] 数组名称;
多维数组声明以此类推。
数组声明以后在内存中没有分配具体的存储空间,也没有设定数组的长度。 Java 中多维数组不必都是规则矩阵形式,例如 int[][] arr = new int[][]{{1, 1, 2}, {2, 5}, {1, 2, 3, 4}};
注意:int[] x,y[]: x是一维数组,y 是二维数组。
多维数组初始化
- 静态初始化(整体赋值)
数组静态初始化时,必须和数组的声明写在一起。
int[][] arrays = {{ 1, 2, 3, 4 }, { 5, 6, 7, 8 },{ 9, 10, 11, 12 }};
/**定义一个名称为 arrays 的二维数组,二维数组中有三个一维数组,
每一个一维数组中具体元素也都初始化了。
*/
int[][][] arrays2 = { { {1,2,3} , {1,2,3} } ,{{3,4,1},{2,3,4}} };
/**
定义一个名称为 arrays2 的三维数组,三维数组中有两个二维数组,
每个二维数组中有两个一维数组,每一个一维数组中具体元素也都初始化了。
*/
- 动态初始化
数据类型[][] 数组名称= new 数据类型[第一维的长度][第二维的长度];
数据类型[][] 数组名称;
数组名称= new 数据类型[第一维的长度][第二维的长度];
int[][] array=new int[2][2];
int [][] arrays;
arrays=new int[3][4];
动态初始化可以和数组的声明分开,动态初始化只指定数组的长度,数组中每个元素的初始化是数组声明时数据类型的默认值。
int[][] a=new int [2][3];
int[][] b;
b=new int [1][2];
这种初始化方式的数组中,第二维长度都是相同的。也可以从最高维开始,分别为每一个维度分配空间。
String[][]s = newString[2][];
s[0] = newString[2];
s[1] = newString[3];
所以,在初始化第一维的长度时,其实是将数组看成了一个一维数组,初始化长度为2,而该数组包含2个元素,这两个元素分别也是一个一维数组。
Java 中多维数组不必都是规则矩阵形式,每个一维数组的长度可以各不相同。
C++中⼆维数组在地址空间上是连续的。像Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。
这⾥的数值也是16进制,这不是真正的地址,⽽是经过处理过后的数值了,我们也可以看出,⼆维数组的每⼀⾏头结点的地址是没有规则的,更谈不上连续。
Java的⼆维数组可能是如下排列的⽅式:
数组默认值(以二维数组为例)
二维数组分为外层数组的元素,内层数组的元素,例如外层元素:arr[1]等 ,内层元素:arr[1][2]等。
数组元素的默认初始化值:
- 针对于初始化方式一:比如:int[][] arr = new int[4][3];
外层元素的初始化值为:内层数组的地址值。
内层元素的初始化值为:与一维数组初始化情况相同
一维数组的默认值取决于数组元素的类型。以下是一些常见类型的默认值:
对于整数类型(如int、byte、short、long),默认值为0。
对于浮点数类型(如float、double),默认值为0.0。
对于布尔类型(boolean),默认值为false。
对于字符类型(char),默认值为\u0000,即空字符。
对于引用类型(如类、接口、数组),默认值为null,表示没有引用任何对象
- 针对于初始化方式二:比如:int[][] arr = new int[2][];
外层元素的初始化值为: null
内层元素的初始化值为:不能调用,否则报错。
多维数组的长度
//多维数组的长度
int[][] m = {{1,2,3,1},{1,3},{3,4,2}};
int sum=0;
for (int i=0;i<m.length;i++){
sum+=m[i].length;
}
System.out.println("sum="+sum);
遍历多维数组(以二维数组为例)
对二维数组中的每个元素,引用方式为 arrayName[index1][index2],例如:num[1][0]
for循环遍历
for (int i=0;i<m.length;i++){
for (int j=0;j<m[i].length;j++){
System.out.println(m[i][j]);
}
System.out.println();
}
foreach循环遍历
for (int[] type:m ) // 第一个循环,第一个参数代表循环中的类型,即数组,第二个参数为循环对象
{
for (int j:type) {System.out.println(j); }// 循环上一个循环中的第一个参数中的每一个即可
System.out.println();
}
可变参数与数组
在Java 中,可变参数用于将任意数量的参数传递给方法,来看 varargsMethod()
方法:
void varargsMethod(String... varargs) {}
该方法可以接收任意数量的字符串参数,可以是 0 个或者 N 个,本质上,可变参数就是通过数组实现的。
public class VarargsExample {
public static void varargsMethod(String... varargs) {
for (String s : varargs) {
System.out.println(s);
}
}
/**
该方法编译时会被编译器将可变参数转换为数组
public static void varargsMethod(String[] varargs) {
for (String s : varargs) {
System.out.println(s);
}
}
**/
public static void main(String[] args) {
varargsMethod("Hello", "World", "Varargs", "Example");
varargsMethod(new String[] {"Hello", "World", "Varargs", "Example"});
}
}
字符串
字符串源码理解
- Java 8中
String
类中关键部分的源码。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
// 字符串的存储字段
private final char value[];
private int hash; // 缓存字符串的哈希值
// 常量池中的字符串构造函数
public String() {
this.value = "".value;
}
// 从字符数组构造字符串
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
// 从字符串池中获取字符串
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
// 获取字符串长度
public int length() {
return value.length;
}
// 获取指定索引处的字符
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
// 返回指定字符串在此字符串中第一次出现的索引
public int indexOf(String str) {
return indexOf(str, 0);
}
public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length, str.value, 0, str.value.length, fromIndex);
}
public boolean equals(Object anObject) {
// 检查是否是同一个对象的引用,如果是,直接返回 true
if (this == anObject) {
return true;
}
// 检查 anObject 是否是 String 类的实例
if (anObject instanceof String) {
String anotherString = (String) anObject; // 将 anObject 强制转换为 String 类型
int n = value.length; // 获取当前字符串的长度
// 检查两个字符串长度是否相等
if (n == anotherString.value.length) {
char v1[] = value; // 当前字符串的字符数组
char v2[] = anotherString.value; // 另一个字符串的字符数组
int i = 0; // 用于遍历字符数组的索引
// 遍历比较两个字符串的每个字符
while (n-- != 0) {
// 如果在任何位置字符不同,则返回 false
if (v1[i] != v2[i])
return false;
i++;
}
// 所有字符都相同,返回 true
return true;
}
}
// 如果 anObject 不是 String 类型或长度不等,则返回 false
return false;
}
// 返回字符串的哈希值
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
// 返回子字符串
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, endIndex - beginIndex);
}
// String to StringBuilder conversion
public StringBuilder appendTo(StringBuilder sb) {
sb.append(value);
return sb;
}
}
- Java 11 中
String
类关键部分的源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 用于存储字符串内容的字节数组
@Stable
private final byte[] value;
// 编码器,用于区分Latin-1和UTF-16编码
private final byte coder;
// 缓存字符串的哈希值
private int hash; // Default to 0
// 无参构造函数,初始化为空字符串
public String() {
this.value = "".value;
this.coder = "".coder;
}
// 通过另一个字符串初始化
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
// 通过字节数组和指定的字符集构造字符串
public String(byte[] bytes, int offset, int length, Charset charset) {
// 检查字符集是否为空
if (charset == null)
throw new NullPointerException("charset");
// 检查字节数组的边界是否合法
checkBounds(bytes, offset, length);
// 使用指定字符集进行解码
StringDecoder sd = StringCoding.decode(charset, bytes, offset, length);
// 初始化value和coder
this.value = sd.value;
this.coder = sd.coder;
}
// 返回字符串的长度
public int length() {
return value.length >> coder(); // 根据coder确定字符串的长度
}
// 返回指定索引处的字符
public char charAt(int index) {
// 检查索引是否有效
if (index < 0 || index >= length()) {
throw new StringIndexOutOfBoundsException(index);
}
return (char) (value[index] & 0xff); // 返回指定索引处的字符
}
// 返回从beginIndex到endIndex的子字符串
public String substring(int beginIndex, int endIndex) {
// 检查边界是否合法
checkBoundsBeginEnd(beginIndex, endIndex, length());
// 返回子字符串
return new String(value, coder, beginIndex, endIndex - beginIndex);
}
// 比较两个字符串内容是否相同
public boolean equals(Object anObject) {
// 如果是同一个对象,返回true
if (this == anObject) {
return true;
}
// 如果是String实例,进行内容比较
if (anObject instanceof String) {
String anotherString = (String) anObject;
// 如果编码相同,比较内容
if (coder() == anotherString.coder()) {
return isLatin1() ? StringLatin1.equals(value, anotherString.value)
: StringUTF16.equals(value, anotherString.value);
}
}
return false; // 否则返回false
}
// 计算并返回字符串的哈希值
public int hashCode() {
int h = hash;
// 如果哈希值未缓存,计算哈希值
if (h == 0 && value.length > 0) {
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
hash = h; // 缓存哈希值
}
return h;
}
// 将字符串转换为小写
public String toLowerCase(Locale locale) {
if (locale == null) {
throw new NullPointerException();
}
return isLatin1() ? StringLatin1.toLowerCase(this, locale)
: StringUTF16.toLowerCase(this, locale);
}
// 将字符串转换为大写
public String toUpperCase(Locale locale) {
if (locale == null) {
throw new NullPointerException();
}
return isLatin1() ? StringLatin1.toUpperCase(this, locale)
: StringUTF16.toUpperCase(this, locale);
}
// 去除字符串两端的空白字符
public String trim() {
return isLatin1() ? StringLatin1.trim(value) : StringUTF16.trim(value);
}
// 将字符串中的oldChar替换为newChar
public String replace(char oldChar, char newChar) {
return isLatin1() ? StringLatin1.replace(value, oldChar, newChar)
: StringUTF16.replace(value, oldChar, newChar);
}
// 按正则表达式分割字符串
public String[] split(String regex) {
return split(regex, 0);
}
// 按正则表达式分割字符串,指定最大分割次数
public String[] split(String regex, int limit) {
return Pattern.compile(regex).split(this, limit);
}
// 检查字符串是否匹配给定的正则表达式
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
// 检查字符串是否包含指定的CharSequence
public boolean contains(CharSequence s) {
return indexOf(s.toString()) > -1;
}
// 检查字符串是否为空白(只有空格或为空)
public boolean isBlank() {
return isLatin1() ? StringLatin1.isBlank(value)
: StringUTF16.isBlank(value);
}
// 去除字符串两端的空白字符
public String strip() {
return isLatin1() ? StringLatin1.strip(value) : StringUTF16.strip(value);
}
// 去除字符串前面的空白字符
public String stripLeading() {
return isLatin1() ? StringLatin1.stripLeading(value)
: StringUTF16.stripLeading(value);
}
// 去除字符串末尾的空白字符
public String stripTrailing() {
return isLatin1() ? StringLatin1.stripTrailing(value)
: StringUTF16.stripTrailing(value);
}
}
String 底层为什么由 char 数组优化为 byte 数组
char[]
来表示 String 就会导致,即使 String 中的字符只用一个字节就能表示,也得占用两个字节。但是从
char[]
到
byte[]
,
中文是两个字节,纯英文就是一个字节,在此之前呢,中文是两个字节,英文也是两个字节。 - Java 9 以前,String 是用
char 型数组实现的,之后改成了
byte 型数组实现,并增加了 coder 来表示编码。这样做的好处是在 Latin1 字符为主的程序里,可以把 String 占用的
内存减少一半,节省字符串所占用的内存空间,同时GC次数也会减少。但是这个改进在节省内存的同时引入了编码检测的开销。” - Latin1(Latin-1)是一种单字节字符集(即每个字符只使用一个字节的编码方式),也称为 ISO-8859-1(国际标准化组织 8859-1),它包含了西欧语言中使用的所有字符,包括英语、法语、德语、西班牙语、葡萄牙语、意大利语等等。在 Latin1 编码中,每个字符使用一个 8 位(即一个字节)的编码,可以表示 256 种不同的字符,其中包括 ASCII 字符集中的所有字符,即 0x00 到 0x7F,以及其他西欧语言中的特殊字符,例如 é、ü、ñ 等等。由于 Latin1 只使用一个字节表示一个字符,因此在存储和传输文本时具有较小的存储空间和较快的速度.
字符串的创建
-
直接使用双引号创建字符串
String str = "Hello, World!";
-
使用
new
关键字创建字符串String str2 = new String("Hello, World!");
字符串的不可变性
String
对象一旦创建,其内容是不可变的。任何修改字符串的方法都会返回一个新的字符串对象。
String str = "Hello";
str = str.concat(" World"); //"Hello World" 实质上是返回了一个新的字符串对象。
不可变性原因:
-
String 类被 final 关键字修饰,所以它不会有子类,这就意味着没有子类可以重写它的方法,改变它的行为。
-
String 类的数据存储在
char[]或byte[]
数组中,而这个数组也被 final 关键字修饰了,这就表示 String 对象是没法被修改的,只要初始化一次,值就确定了。
不可变性优点:
- 安全性: 不可变对象在多线程环境下是天然安全的,因为它们的状态一旦创建就不能改变。
例如有很多人同时在看一本书。如果书的内容不能改变,那么每个人看到的内容都是一样的,不会因为有人在看书的同时修改内容而影响其他人的阅读。
- **缓存和重用:**由于字符串不可变,相同的字符串只需要存储一份,可以重用,减少内存浪费。Java的字符串池(String Pool)就是利用这一特性来缓存字符串实例。
就像家里有很多钥匙,但所有钥匙都只开同一扇门。每次有人需要这把钥匙,不需要重新做一把,只需要从钥匙架上拿下来就行了。
- **效率提升:**字符串的不可变性使得它们在哈希表中非常高效,因为哈希值可以缓存,不需要每次使用时重新计算。例如,字符串作为哈希表的键时,由于内容不可变,哈希值也不会改变。
就像一个书的目录页,它列出每章的页码。如果书的内容不能改变,那么目录页也是永远正确的,不需要每次查看时重新编写目录。
字符串常量池
Java 虚拟机为了提高性能和减少内存开销,在创建字符串对象的时候进行了一些优化,特意为字符串开辟了一块空间——也就是字符串常量池。”
//这行代码创建了几个对象?
String s = new String("李华");
这行代码创建了两个对象。
第一个对象:字符串常量池中的对象。当编译器遇到字符串字面量
"李华"
时,它会在字符串常量池中查找是否已经存在这个字符串。如果存在,就不会在字符串常量池中创建
"李华"
这个对象了,直接在堆中创建一个"李华"
的字符串对象,然后将堆中这个"李华"
的对象地址返回赋值。如果不存在,就会在常量池中创建一个新的字符串对象
"李华"
。第二个对象:堆中的对象。
new String("李华")
这段代码会在堆内存中创建一个新的String
对象。这个新的String
对象是通过调用字符串常量池中的"李华"
字符串字面量来初始化的。
//这行代码创建了几个对象?
String s2 = "李明";
这行代码创建了一个对象。
1、字符串常量池中的对象:编译器遇到字符串字面量
"李明"
时,会在字符串常量池中查找是否已经存在这个字符串。如果不存在,它会在常量池中创建一个新的字符串对象"李明"
。这行代码不会在堆内存中创建新的对象,而是直接使用字符串常量池中的对象。因此,只有一个对象被创建,并且该对象存储在字符串常量池中。
StringBuffer和StringBuilder
StringBuffer 类
public final class StringBuffer extends AbstractStringBuilder implements Serializable, CharSequence {
public StringBuffer() {
super(16);
}
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
public synchronized String toString() {
return new String(value, 0, count);
}
// 其他方法
}
StringBuilder 类
public final class StringBuilder extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
// ...
public StringBuilder append(String str) {
super.append(str);
return this;
}
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
// ...
}
StringBuffer
和 StringBuilder
都是可变的,可以通过调用方法来修改其内容,而不是创建新的对象。
区别:
-
StringBuffer
是线程安全的,所有的方法都是同步的(即synchronized
方法),这意味着多个线程可以安全地同时访问一个StringBuffer
对象。 -
StringBuilder
不是线程安全的,它的方法并没有同步修饰符。因此,它的性能比StringBuffer
更高,特别是在单线程环境下。
//应用
String s1 = new String("A") + new String("B");
// Java编译器解释后
new StringBuilder().append("A").append("B").toString();
在循环体内,拼接字符串最好使用 StringBuilder 的
append()
方法,而不是 + 号操作符。原因就在于循环体内如果用 + 号操作符的话,就会产生大量的 StringBuilder 对象,不仅占用了更多的内存空间,还会让 Java 虚拟机不停的进行垃圾回收,从而降低了程序的性能。在循环的外部新建一个 StringBuilder 对象,然后使用
append()
方法将循环体内的字符串添加进来。
常用字符串方法
1、length()
: 获取字符串的长度。
int len = s1.length();
2、charAt(int index)
:获取指定索引处的字符。
char c = s1.charAt(0);
3、substring(int beginIndex, int endIndex)
:获取从 beginIndex
到 endIndex
之间的子字符串。
String sub = s1.substring(0, 5);
4、indexOf(String str)
:查找指定字符串在当前字符串中的第一次出现的位置。
int index = s1.indexOf("e");
5、toUpperCase()
和 toLowerCase()
:将字符串转换为大写或小写。
String upper = s1.toUpperCase();
String lower = s1.toLowerCase();
6、trim()
:去除字符串两端的空白字符。
String trimmed = s1.trim();
7、replace(CharSequence target, CharSequence replacement)
:替换字符串中的指定字符或子字符串。
String replaced = s1.replace("l", "p");
8、格式化字符串
String name = "John";
int age = 30;
String formatted = String.format("Name: %s, Age: %d", name, age);
// 生成 "Name: John, Age: 30"
9、字符串拆分
split()
方法是最常用的字符串拆分方法,它使用正则表达式来分割字符串。、
public String[] split(String regex)
public String[] split(String regex, int limit)
/**
regex:表示分割的正则表达式。
limit:表示分割的最大次数。如果为负数,则表示无限次分割。
**/
public class Main {
public static void main(String[] args) {
String str = "apple,banana,orange";
// 使用逗号分割字符串
String[] fruits = str.split(",");
for (String fruit : fruits) {
System.out.println(fruit);
}
// 使用空格分割字符串
String str2 = "apple banana orange";
String[] fruits2 = str2.split(" ");
for (String fruit : fruits2) {
System.out.println(fruit);
}
// 限制分割次数
String[] limitedFruits = str.split(",", 2);
for (String fruit : limitedFruits) {
System.out.println(fruit);
}
}
}
StringTokenizer
类是 java.util
包中的一个类,用于分割字符串。它不使用正则表达式,而是基于单个字符作为分隔符。
public StringTokenizer(String str)
public StringTokenizer(String str, String delim)
public StringTokenizer(String str, String delim, boolean returnDelims)
/**
str:要分割的字符串。
delim:分隔符。
returnDelims:是否返回分隔符作为标记的一部分。
**/
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) {
String str = "apple,banana,orange";
// 使用逗号分割字符串
StringTokenizer st = new StringTokenizer(str, ",");
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
// 使用空格分割字符串
String str2 = "apple banana orange";
StringTokenizer st2 = new StringTokenizer(str2, " ");
while (st2.hasMoreTokens()) {
System.out.println(st2.nextToken());
}
// 返回分隔符作为标记的一部分
String str3 = "apple,banana,orange";
StringTokenizer st3 = new StringTokenizer(str3, ",", true);
while (st3.hasMoreTokens()) {
System.out.println(st3.nextToken());
}
}
}
Pattern
和 Matcher
类提供了更高级的字符串操作功能,包括字符串拆分。它们允许使用复杂的正则表达式。
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
String str = "apple,banana,orange";
// 使用逗号分割字符串
Pattern pattern = Pattern.compile(",");
String[] fruits = pattern.split(str);
for (String fruit : fruits) {
System.out.println(fruit);
}
// 使用空格分割字符串
String str2 = "apple banana orange";
Pattern pattern2 = Pattern.compile(" ");
String[] fruits2 = pattern2.split(str2);
for (String fruit : fruits2) {
System.out.println(fruit);
}
}
}
字符串的比较
1、equals(Object anObject)
:比较两个字符串的内容是否相同。
==
操作符,比较的是引用,判断两者是否是同一个对象。
Objects.equals()
这个静态方法的优势在于不需要在调用之前判空。但是如果是a.equals(b)
,则需要在调用之前对 a 进行判空。Objects.equals(“AB”, new String(“A” + “B”)) // --> true
Objects.equals(null, new String(“A” + “B”)); // --> false
Objects.equals(null, null) // --> true
String a = null; a.equals(new String(“A” + “B”)); // 空指针异常
String s1 = new String("A");
String s2 = new String("A");
System.out.println(s1.equals(s2)); // true
System.out.println(s1 == s2); // false
new String("A").equals("A") //true 比较的是内容
new String("A") == "A"
//false ==操作符左侧的是在堆中创建的对象,右侧是在字符串常量池中的对象,
//尽管内容相同,但内存地址不同,所以返回 false。
new String("C") == new String("C") //flase 两者完全不同
"C" == "C" // 字符串常量池中只会有一个相同内容的对象,所以返回 true。
String s1 = "AB";
String s2 = "A" + "B";
System.out.println(s1 == s2); // true
//true 两者都在字符串常量池,编译器在遇到‘+’操作符的时候将其自动优化为“AB”。
String a = "A";
String b = "B";
String s3 = a + b;
System.out.println(s1 == s3); // false 这种就是字符串拼接,是一个新的字符串。
new String("ABC").intern() == "ABC" ;
/** new String("ABC") 在执行的时候,会先在字符串常量池中创建对象,再在堆中创建对象
执行 intern() 方法的时候发现字符串常量池中已经有了‘ABC’这个对象,
所以就直接返回字符串常量池中的对象引用了,那再与字符串常量池中的‘ABC’比较,
当然会返回 true 了。**/
2、String 类的 .contentEquals()
可以将字符串与任何的字符序列(StringBuffer、StringBuilder、String、CharSequence)进行比较。
public class Main {
public static void main(String[] args) {
// 原始字符串
String str = "Hello World";
// 与 StringBuffer 比较
StringBuffer sb = new StringBuffer("Hello World");
boolean result1 = str.contentEquals(sb);
System.out.println("String equals StringBuffer: " + result1); //true
// 与 StringBuilder 比较
StringBuilder sb2 = new StringBuilder("Hello World");
boolean result2 = str.contentEquals(sb2);
System.out.println("String equals StringBuilder: " + result2); //true
// 与另一个 String 比较
String str2 = "Hello World";
boolean result3 = str.contentEquals(str2);
System.out.println("String equals String: " + result3); //true
// 与 CharSequence 比较
CharSequence cs = "Hello World";
boolean result4 = str.contentEquals(cs);
System.out.println("String equals CharSequence: " + result4);//true
// 另一个例子:不同的内容
StringBuffer sbDifferent = new StringBuffer("Hello Java");
boolean result5 = str.contentEquals(sbDifferent);
System.out.println(result5);//false
}
}
3、compareTo(String anotherString)
:按字典顺序比较两个字符串。
int result = s1.compareTo(s2);
4、equalsIgnoreCase(String anotherString)
:忽略大小写比较字符串内容是否相同。
boolean isEqualIgnoreCase = s1.equalsIgnoreCase(s2