11、Java基础教程之面向对象·第三讲

  • 本节学习目标
  • 1️⃣ 概念
    • 1.1 String类的实例化
  • 1.2 字符串的比较
  • 1.3 字符串常量
  • 1.4 两种实例化方式的区别
    • *String类设计模式——享元设计模式(Flyweight pattern)
    • *String类的两种实例化方式之间有什么区别?
  • 1.5 字符串的不可改变性
  • 2️⃣ String 类的常用方法
    • 2.1 获取字符串长度
  • 2.2 获取字符及索引
  • 2.3 字符串截取
  • 2.4 字符串比较
  • 2.5 字符串判断
  • 2.6 字符串拆分
  • 2.7 字符串替换
  • 2.8 字符串和其他类型的转换
  • 2.9 字符串格式化
  • 2.10 字符串连接
  • 2.11 手动入池
  • 2.12 其他操作
  • 3️⃣ 字符串常量池
  • 4️⃣ 应用场景
  • * 总结

*

本节学习目标

  • 掌握字符串String类的特征以及类中常用方法;

1️⃣ 概念

Java的String类是Java标准库中最常用的类之一,它用于表示字符串,并提供了许多用于操作和处理字符串的方法。

在Java中,String类是一种特殊的引用类型,虽然字符串在通过直接赋值方式使用时看起来像是基本数据类型一样直观,但实际上它是通过String类来实现的。

前面文章已经介绍过,引用类型指的是这种类型的变量存储了对象的引用而不是实际的对象本身。当我们声明一个String类型的变量时,它实际上是一个引用,它指向内存中的某个位置,该位置包含字符串对象。

下面是String类的一些重要特征:

1、 不可变性:一旦创建了一个String对象,它的值就不能被修改如果我们对字符串进行拼接、替换或者其他操作,实际上是创建了一个新的String对象,这种不可变性的特性使得Java中的字符串能够更有效地进行共享和缓存而基本数据类型如intchar等是可变的;
2、 继承自Object:所有引用类型都直接或间接地继承自ObjectString类也不例外这意味着String类具有与其他引用类型相同的特性,例如可以调用Object类中定义的方法,支持多态和类型转换等;
3、 传递参数时的行为:在Java中,当将一个字符串作为参数传递给方法时,实际上传递的是该字符串对象的引用,而不是其值的副本因此,如果在方法内部修改了字符串对象的值,原来的变量也将受到影响这与基本数据类型的行为有所不同,基本数据类型作为参数传递时,传递的是值的副本;

虽然String类在Java中表现出引用类型的特性,但它又具有一些独特的性质和方法,使得我们可以像使用基本数据类型一样直接操作字符串。这种设计使得字符串处理在Java中更加方便和灵活,下面我们将详细介绍。

1.1 String类的实例化

在String类的实例化方式中, 最简单也最常用的方式是直接采用 " String 变量名 = "字符串"; " 语法形式完成的,这种形式称为直接赋值。

//	范例 1: String类对象的两种实例化方法之一:直接赋值
public class StringDemo {
   
     
	public static void main(String args[]){
   
     
		String str = "https://lst66.blog.csdn.net";	//直接赋值
		System.out.println(str);		//输出字符串数据
	}
}

程序执行结果:

https://lst66.blog.csdn.net

此程序首先定义了一个String类的对象,并且采用直接赋值的形式为其进行实例化操作。

以上这种直接赋值实例化String 对象的方式,代码中未使用关键字 new 。而在 String 类里面实际上也有一个构造方法:

public String(String str);

可以看到在构造方法里面,需要接收一个String类对象。

//	范例 2: String类对象的两种实例化方法之一:利用构造方法实例化
public class StringDemo {
   
     
	public static void main(String  args[]){
   
     
		String str = new String("https://lst66.blog.csdn.net");  //直接赋值
		System.out.println(str);                 	//输出字符串数据
	}
}

程序执行结果:

https://lst66.blog.csdn.net

此程序采用 String 类的构造方法实现了 String 类对象的实例化操作,其最终的结果与直接赋值结果相同。

面对String 类对象的两种实例化方式,一些朋友主观上会认为第二种构造方法的形式更加直观,因为按照类与对象的概念来解释,只要是类就要用关键字 new, 所以这样的方式似乎才是符合道理的,但是实际的情况却并非如此,接下来继续分析。

1.2 字符串的比较

在Java 中如果要判断两个 int 型变量的内容是否相等,按照之前所学的内容, 可以使用 “==” 符号来完成。

//	范例 3: 判断两个int 型整数是否相等
public class StringDemo  {
   
     
	public static void main(String args[]){
   
     
		int x =10;   	//整型变量
		int y =10;    	//整型变量
		System.out.println(x == y);		//判断是否相等
	}
}

程序执行结果:

true

此程序直接定义了两个 int 型的变量,由于这两个变量的内容都是10, 所以使用“==” 判断的最终结果也是true

而在Java 中,“==” 可以应用在所有数据类型中(包括基本数据类型与引用数据类型), 下面在String 类的对象比较中使用“==”。

//	范例 4: 在 String 对象上使用“==”比较
public class StringDemo{
   
     
	public static void main(String  args[]){
   
     
		String stra = "hello";	//直接赋值定义字符串 
		String strb = new String("hello");	//构造方法定义字符串
		String strc = strb;		//引用传递
		System.out.println(stra == strb);	//比较结果
		System.out.println(stra == strc);
		System.out.println(strb == strc);
	}
}

程序执行结果:

false
false
true

此程序首先采用直接赋值的形式为 String 类对象实例化 (String stra = "hello"), 然后使用构造方法为 String 类对象实例化 (String strb = new String("hello") ), 而在定义 strc 对象时采用了引用传递的方式,即将 strb 的引用地址赋给 strc ( 此时 strbstrc 指向同一内存空间)。
虽然3个 String 类对象的内容是完全一样的,但最后的结果却出现 “false”, 所以“==”并不能够实现准确的字符串内容比较,此过程解析如下图所示。

*

图1程序执行内存关系解析

通过上图可以发现,strastrb 虽然包含的内容是一样的,但是其所在的内存地址空间不同 ( 因为 strb 使用关键字 new 开辟了新的堆内存空间), 而在使用“==”比较引用类型时,实际上比较的只是内存地址值,所以地址值不相同的 String 类对象在使用 “==”比较相等时其结果一定返回的是 “false”; 而 strc 由于与 strb 指向了同一块内存空间,所以地址值相同,那么返回的结果就是 “true”。所以以上案例也说明了 “==” 在 String 比较时比较的只是内存地址的数值,而并不是内容。

因此“==” 用于所有的引用数据类型的比较时,仅仅是地址值,这样的操作往往只用于判断两个不同名的对象是否指向同一内存空间。而如果要比较字符串的内容,可以使用 String 类里面定义的比较方法,内容比较操作(区分大小写)语法如下。

public boolean equals(Object str);

此处的Object类将在后面文章中详细讲解。而此方法的使用方式如下案例所示:

//	范例 5: 实现字符串内容比较
public class StringDemo{
   
     
	public static void main(String args[]){
   
      
		String stra = "hello";				//直接赋值定义字符串
		String strb = new String("hello");	//构造方法定义字符串
		String strc = strb;					//引用传递
		System.out.println(stra.equals(strb));		//比较结果
		System.out.println(stra.equals(strc));
		System.out.println(strb.equals(strc));
	}
}

程序执行结果:

true
true
true

此程序与范例4一样,依然采用不同的方式定义了相同内容的3个 String 类对象,唯一的区别是在进行比较时,使用的是 equals() 方法,此方法是 String 类内部提供的比较方法,专门判断内容是否相等,由于3个String类的对象内容完全一致,所以最终的判断结果全部都是 “true”。

在实际的开发中,由于字符串的地址是不好确定的,所以都不会使用 “==”比较,都要通过 equals() 等其他方法完成。

1.3 字符串常量

在编程语言中,字符串是一种常见的数据类型,用于表示文本或字符序列。虽然一些编程语言使用字符数组来描述字符串,但Java提供了一个特殊的类String,用于操作和处理字符串。在Java中,可以使用双引号(")将字符串括起来进行定义。例如:

String str = "Hello, World!";

这里的str是一个变量,它声明为String类型,并且存储了字符串对象的引用。

尽管字符串在Java中以类的形式存在,但它不是基本数据类型。基本数据类型包括intcharboolean等,而字符串确实是通过String类来处理和表示的。所以,虽然我们在使用字符串时可以像使用基本数据类型一样直接操作,实际上字符串常量数据是作为String类的匿名对象存在的。

//	范例 6: 观察字符串是String类匿名对象的验证
public class StringDemo{
   
     
	public static void main(String args[]){
   
     
		String str = "hello";	//str是对象名称,而"hello"是内容 
		System.out.println("hello".equals(str));	//内容比较,由字符串直接调用
	}
}

程序执行结果:

true

此程序的最大特点在于直接利用字符串 “hello” 调用了 equals() 方法 ( "hello".equals(str)), 由于 equals() 方法是 String 类定义的,而类中的方法只有实例化对象才可以调用,那么就可以得出一个结论:字符串常量就是String类的匿名对象。

再结合上文介绍的 String 类对象的两种实例化方式中,直接赋值的实例化操作,可以发现实际上直接赋值就相当于将一个匿名对象设置了一个名字。

1.4 两种实例化方式的区别

了解完String 类的比较操作后,下面就需要解决一个最为重要的问题,对于String 类的对象存在两种实例化的操作形式。那么这两种有什么区别,在开发中应该使用哪一种更好呢?

  • 直接赋值
    直接赋值实际上就是将一个字符串的匿名对象设置了一个名字。其语法如下。
String 变量名 = 字符串常量(匿名对象);
例:String str = "hello";

此时在内存中会开辟一块堆内存,内存空间中将保存 “hello” 字符串数据,并且栈内存将直接引用此堆内存空间,如下图所示。

*

图2直接赋值内存关系

通过上图可以发现,使用直接赋值的方式为 String 类对象实例化只会开辟一块堆内存空间,而除了这一特点外,利用直接赋值还可以实现堆内存空间的重用,即采用直接赋值的方式, 在相同内容的情况下不会开辟新的堆内存空间,而会直接指向已有的堆内存空间。

//	范例 7:  直接赋值时的堆内存自动引用
public class StringDemo  {
   
     
	public static void main(String args[]){
   
      
		String stra ="hello";	//直接赋值实例化
		String strb ="hello";
		String strc ="hello";
		String strd ="world";
		System.out.println(stra == strb); 
		System.out.println(stra == strc); 
		System.out.println(strb == strc); 
		System.out.println(stra == strd);
	}
}

程序执行结果:

true
true
true
false

通过本程序的执行结果可以发现,由于使用了直接赋值的实例化操作方式,当设置的内容相同时,即使没有直接发生对象的引用操作,最终 3个 String对象 (strastrbstrc) 也都自动指向了同一块堆内存空间,但是如果在直接赋值时内容与之前不一样,则会自动开辟新的堆内存空间(String strd = "world"; )。 本程序的内存关系图如下所示。

*

图3案例程序内存关系

*String类设计模式——享元设计模式(Flyweight pattern)

在JVM的内存结构中,存在一个称为"字符串常量池"的对象池。当使用直接赋值的方式定义一个String对象时,如果该字符串在常量池中不存在,则会将这个字符串对象添加到常量池中保存。如果后续有其他 String对象也采用了相同的直接赋值,并且具有相同的内容,就会直接引用已经存在于常量池中的对象,而不是再次创建新的对象。

这种共享设计模式的好处是节省了内存空间和提高了性能。由于String对象是不可变的,所以多个对象可以安全地共享同一个实例。通过共享字符串对象,避免了重复创建拥有相同内容的字符串,减少了内存占用和垃圾回收的负担,同时提高了程序的执行效率。

共享设计模式的简单解释就像一个家庭中的工具箱一样,当需要螺丝刀时,如果家里已经有了螺丝刀(即字符串常量池中已经有了相同的字符串),那么就可以直接使用现有的工具而不需要购买新的。这样可以节省成本并提高效率,因为工具可以为家庭中的每个成员提供服务,而不需要每次都创建新的对象。

  • 构造方法实例化 String 类对象的情况
    如果要明确地调用 String 类中的构造方法进行String 类对象的实例化操作,那么一定要使用关键字 new, 而每当使用关键字 new 就表示要开辟新的堆内存空间,这块堆内存空间的内容就是传入到构造方法中的字符串数据。
String str = new String("hello");

通过之前的介绍已经清楚,每一个字符串常量都是 String 类的匿名对象,所以这行代码的含义是, 根据 “hello” 这个匿名对象的内容创建一个新的String 类对象,所以此时的内存关系如下图所示。

*

图4构造方法实例化内存关系

因为每一个字符串都是一个 String 类的匿名对象,所以会首先在堆内存中开辟一块空间保存字符串 “hello”, 然后使用关键字 new, 开辟另一块堆内存空间。因此真正使用的是用关键字 new 开辟的堆内存, 而之前定义的字符串常量的堆内存空间将不会有任何的栈内存指向,将成为垃圾,等待被 GC 回收。

所以,使用构造方法的方式开辟的字符串对象,实际上会开辟两块空间,其中有一块空间将成为垃圾。除了内存的浪费外,如果使用构造方法实例化 String 类对象,由于关键字 new 永远表示开辟新的堆内存空间,所以其内容也不会保存在对象池中。

//	范例 8: 不自动保存对象池操作
public class StringDemo{
   
     
	public static void main(String args[]){
   
      
		String stra = new String("hello");		//使用构造方法定义了新的内存空间,不会自动入池 
		String strb = "hello";				//直接赋值
		System.out.println(stra == strb);	//判断结果
	}
}

程序执行结果:

false

此程序首先利用构造方法开辟了一个新的 String 类对象,由于此时不会自动保存到对象池中,所以在使用直接赋值的方式声明 String 类对象后将开辟新的堆内存空间,因为两个堆内存的地址不同,所以 最终的地址判断结果为 false

如果希望开辟的新内存数据也可以进行对象池的保存,那么可以采用 String 类定义的一个手工入池的操作方法。方法如下:

public String intern();

//	范例 9: 手工入池
public class StringDemo{
   
     
	public static void main(String args[]){
   
     
		String stra = new String("hello").intern();		//使用构造方法定义新的内存空间,并手工入池 
		String strb = "hello";				//直接赋值
		System.out.println(stra ==  strb);	//判断结果
	}
}

程序执行结果:

true

此程序由于使用了 String 类的 intern()方法,所以会将指定的字符串对象保存在对象池中,随后如果使用直接赋值的形式将会自动引用已有的堆内存空间,所以地址判断的结果为 true

*String类的两种实例化方式之间有什么区别?

1、直接赋值
String类的设计使⽤了共享设计模式,JVM底层会自动维护一个字符串的对象池(对象数组)。
采用直接赋值 的方式进行对象实例化操作,那么该实例化对象(字符串内容)将自动保存到对象池 之中。
如果下次继续使用直接赋值的方式声明String类对象,此时对象池之中若已有相同内容,则直接进行引用;若没有,则创建新的字符串对象而后将其保存在对象池之中。

2、构造方法
构造方法方式的实例化,JVM会先在堆上创建一个匿名字符串,再将这个字符串复制一份并由变量指向,导致出现垃圾空间(匿名String对象),且此方式不会将该字符串对象加入到JVM的 “字符串常量池” 中,导致字符串共享问题。
解决办法是使用String类的intern()方法将以构造方法方式创建的对象手动加入到"字符串常量池"中。

综上,对于String类的两种对象实例化方式的区别说明如下:

  • 直接赋值(String str = "字符串"; ): 只会开辟一块堆内存空间,并且会自动保存在对象池中以供下次重复使用;
  • 构造方法(String str = new String("字符串")): 会开辟两块堆内存空间,其中有一块空间将成为垃圾,并且不会自动入池,但可以使用intern()方法手工入池。

在所有开发中,String对象的实例化永远都采用直接赋值的方式完成。

1.5 字符串的不可改变性

在使用String 类进行操作时,还有一个特性是特别重要的,那就是字符串的内容一旦定义则不可改变,下面通过一个代码案例来讲解此特性。

//	范例 10: 修改字符串对象引用
public class StringDemo {
   
     
	public static void main(String args[]){
   
      
		String str = "Hello ";	//直接赋值实例化 String类对象
		str = str + "World";	//字符串连接,同时修改String类对象的引用关系
		str += "!";
		System.out.println(str);	 //输出当前的String类对象内容
	}
}

程序执行结果:

Hello World !

此程序首先声明了一个 String 类对象,然后修改了两次String 类对象的内容 (注意:实际上是发生了两次引用改变), 所以最终 String 类对象的内容就成为了 “Hello World !”。在整个操作过程中,其实是 String 类的对象引用发生了改变,而字符串的内容并没有发生改变。下面通过下图进行说明。

*

图5案例程序内存关系

通过上图可以发现,在进行 String 类对象内容修改时,实际上原始的字符串都没有发生变化 (最终没有引用的堆内存空间将成为垃圾空间),而改变的只是 String 类对象的引用关系。所以可以得出结论:字符串一旦定义则不可改变。也正因为存在这样的特性,所以在实际开发中应该避免以下这种代码。

//	范例 11: 观察以下代码问题
public class StringDemo{
   
     
	public static void main(String args[]){
   
     
		String str = "";                  	//实例化字符串对象
		for(int x=0; x<1000; x++){
   
           	//循环1000次
			str += x;                   	//修改字符串对象引用
		}
		System.out.println(str);
	}
}

上述范例的代码修改了 String 对象的引用关系 1000次 (所有数据类型遇见 String 连接操作时都会自动向String 类型转换), 会产生大量的垃圾空间,所以此类代码在开发中是严格禁止的,String 的内容不要做过多频繁的修改。

通过前面的解释,我们已经了解到在Java中 String对象是不可变的,对其内容进行修改会产生大量的垃圾空间。然而,在项目开发中,假如确实有业务场景需要频繁地修改一个字符串。那么该如何解决这类问题呢?

一种解决方法就是使用StringBufferStringBuilder类来代替String。这两个类都是可变的字符串操作类,可以用于频繁修改字符串内容而不会产生大量的垃圾空间。

  • StringBuffer:线程安全的可变字符串序列,适用于在多线程环境下进行字符串修改操作。
  • StringBuilder:非线程安全的可变字符串序列,适用于在单线程环境下进行字符串修改操作。

这两个类提供了各种方法来添加、删除、替换和修改字符串,可以高效地进行字符串操作。与String不同,它们允许直接修改字符串的内容而不是创建新的对象。

关于这两个类的更多详细信息将在后面文章中介绍。

尽管Java提供了可修改内容的字符串操作类,实际上,在开发中如果只需要简单地修改字符串(可能只修改一两次),并且产生的垃圾空间问题并不重要,仍然可以使用String类。根据实际需求和性能要求,选择合适的字符串类进行操作。

2️⃣ String 类的常用方法

String 类在所有的项目开发里面都一定会使用到,因此 String 类提供了一系列的功能操作方法。除了上文所介绍到的两个方法 (equals()intern() )之外,还提供了大量的其他操作方法。这些方法可以通过Java Doc 文档查阅,而考虑到 String类在实际的工作中使用得非常广泛,这里也建议大家尽可能将所有讲解过的方法都背下来,并且希望将以下所讲解的每一个方法的名称、返回值类型、参数的类型及个数、方法的作用全部记下来。

那么为了方便大家记忆,下面的信息表已经列出了String 类中基本所有方法的说明。

操作类别 方法 说明
构造方法 String(char[] value) 将字符数组变为字符串
String(char[] value,int offset, int count) 将部分字符数组变为字符串
String(byte[] bytes) 将字节数组变为字符串
String(byte[] bytes, int offset, int length) 将部分字节数组变为字符串
获取 length() 获取字符串长度
charAt(int index) 获取指定位置的字符
indexOf(Char char) 获取指定字符在字符串中首次出现的位置
lastIndexOf(Char char) 反向查找指定字符在字符串中首次出现的位置
subString(int start) 字符串截取,从指定位置到字符串末尾
subString(int start,int end) 字符串截取,从指定位置到指定位置,范围左闭右开
比较 equals(String s) 比较字符串内容是否相同,区分大小写
equalsIgnoreCase(String s) 比较字符串内容是否相同,不区分大小写
compareTo(String s) 比较字符串大小(按照字符编码),结果=0表示相等、>0表示大于、<0表示小于
判断 contains(String s) 判断该字符串是否包含指定字符串
startsWith(String prefix) 判断字符串是否以指定字符串开头
endsWith(String suffix) 判断字符串是否以指定字符串结尾
isEmpty() 判断字符串是否为空
拆分 split(String regex) 通过指定正则表达式分隔符来拆分字符串
split(String regex, int limit) 通过指定正则表达式分隔符来拆分字符串,limit参数指定返回的数组长度限制
替换 replaceFirst(String regex, String replacement) 用新内容替换首个旧内容
replaceAll(String regex, String replacement) 用新内容替换全部的旧内容
replace(char oldChar, char newChar) 将字符串中的指定字符 替换为新字符
replace(CharSequence target, CharSequence replacement) 将字符串中与目标字符序列 匹配的部分替换为指定字符序列
数据类型转换 getBytes() 使用平台的默认字符集将此 String 编码为字节数组
getBytes(String charsetName) 使用指定字符集将 String 编码为字节数组
toCharArray() 将字符串转化为字符数组
valueOf(char[] data) 将传递的字符数组,组合成字符串
格式化 String.format(String format, Object... args) 使用格式规范化字符串。第一个参数是格式字符串,后续参数是需要插入到格式字符串中的变量
连接 concat(String s) 字符串连接
其他 toLowerCase() 转小写
toUpperCase() 转大写
trim() 去掉字符串左右两边的空格
手动入池 intern() 手动将字符串加入字符串常量池

2.1 获取字符串长度

在编程中,字符串的长度指的是字符串中字符的数量。每个字符都占用一个位置或索引,可以使用这些索引来计算字符串的长度。

在Java中,可以使用length()方法获取字符串的长度。

需要注意的是,字符串的长度与它所占据的内存空间大小不一定相同。在Java中,每个字符通常使用Unicode编码表示,而Unicode字符可以采用不同的编码方案(如UTF-8UTF-16 等),每种编码方案有不同的字节数。因此,字符串的长度与其实际占用的内存空间之间并不一定是一对一的关系。

通过获取字符串的长度,我们可以进行更多字符串操作,如截取子串、遍历字符等。了解字符串的长度可以帮助我们更好地处理和操作字符串数据。

使用方式如下:

//	范例 12: 获取字符串长度
public class Demo{
   
     
	public static void main(String args[]){
   
     
		String str = "Hello World";
	    int length = str.length();
	    System.out.println(length);
	}
}

运行结果:

11

2.2 获取字符及索引

在字符串操作中,我们经常需要获取特定位置的字符或查找某个字符或子串的索引。通过这些操作,可以对字符串进行更精确和灵活的处理。

其中,可通过访问字符串的指定位置来获取单个字符。在Java中,字符串的每个字符都有一个对应的索引值。注意索引从0开始,因此第一个字符的索引为0,第二个字符的索引为1,以此类推。可以使用charAt(index)方法来获取给定索引处的字符。

对于字符子串或者字符索引的查找操作,常用的方法有indexOf(int ch)lastIndexOf(int ch)。它们用于返回指定字符或子串在字符串中的位置。

  • indexOf(int ch) 和 indexOf(String str):从前往后的正向查找指定字符或子串在字符串中的位置。若找不到,则返回-1;
  • lastIndexOf(int ch) 和 lastIndexOf(String str):从后往前的反向查找指定字符或子串在字符串中的位置。若找不到,则返回-1;
  • charAt(int index):获取指定位置的字符。

使用方式如下:

//	范例 13: 获取字符及索引
public class Demo{
   
     
	public static void main(String args[]){
   
     
		String str = "Hello World";
        char c = str.charAt(0);
        System.out.println(c);

		int index1 = str.indexOf('o');
        int index2 = str.indexOf("World");
        int lastIndex1 = str.lastIndexOf('o');
        int lastIndex2 = str.lastIndexOf("World");
        System.out.println(index1);
        System.out.println(index2);
        System.out.println(lastIndex1);
        System.out.println(lastIndex2);
	}
}

运行结果:

H
4
6
7
6

charAt() 方法的主要作用是从一个字符串中截取指定索引的字符,由于 Java 中的字符串的索引下标从0开始,所以截取的第一个字符为 “H”。而 indexOf()主要用于从前往后的正向查找指定字符或子串在字符串中的位置;lastIndexOf() 则用于从后往前的反向查找指定字符或子串在字符串中的位置。当查找不到时,会返回-1

2.3 字符串截取

字符串截取是指从一个字符串中提取出部分子串的操作,注意字符串截取操作并不修改原始字符串,而是返回一个新的字符串对象。因为Java中的字符串是不可变的,一旦创建,就无法更改。而字符串截取在处理文本数据时非常有用。

在Java中,可以使用substring(int beginIndex, int endIndex)方法和substring(int beginIndex)方法来实现字符串截取功能。其中第一个方法会返回指定索引范围内的子串,包括开始索引处的字符,但不包括结束索引处的字符。注意,索引从0开始。

  • substring(int beginIndex, int endIndex)方法用来截取从第一个参数表示的起始位置(包括)到第二个参数表示结束位置(不包括)的子字符串;
  • substring(int beginIndex)方法用于从从第一个参数表示的指定位置截取到字符串末尾。

使用方式如下:

//	范例 14: 字符串截取
public class Demo{
   
     
	public static void main(String args[]){
   
     
        String str = "Hello World!";
        String substr = str.substring(6, 11);
        String substr2 = str.substring(6);
        System.out.println(substr);
        System.out.println(substr2);
    }
}

运行结果:

World
World!

2.4 字符串比较

字符串比较是指在编程中对两个字符串进行逐字符的比较,以确定它们是否相等或者哪一个字符串在字典序中排在前面。

通常情况下,字符串比较是基于字符编码值(ASCIIUnicode等)来执行的。字符串中的每个字符都与另一个字符串中对应位置的字符进行逐个比较。比较从第一个字符开始,如果两个字符不相等,则根据字符编码值的大小关系来判断它们的顺序。这个过程一直持续,直到出现不相等的字符或任意一个字符串到达末尾。

比较字符串时可以使用多种不同的算法,如按字符逐个比较、字符串哈希比较等。需要注意的是,在Java语言中字符串的比较区分大小写,当然也可以选择特定方法在比较时忽略大小写。

在Java中,用于比较字符串的内容是否相同有以下方法:

  • boolean equals(Object anObject):比较两个字符串的内容是否相同;
  • boolean equalsIgnoreCase(String anotherString):忽略字符串的大小写,比较两个字符串的内容;
  • int compareTo(String anotherString):按照字符编码比较字符串大小,结果 =0表示相等、>0表示大于、<0表示小于。

使用方式例如:

//	范例 15: 字符串比较
public class Demo{
   
     
	public static void main(String args[]){
   
     
        String str1 = "hello";
        String str2 = "Hello";

        System.out.println(str1.equals(str2));  
        System.out.println(str1.equalsIgnoreCase(str2));  
        System.out.println(str1.compareTo(str2));
    }
}

运行结果:

false
true
32

2.5 字符串判断

字符串判断指的是在编程中对字符串进行各种条件判断的操作。通过对字符串进行不同类型的判断,我们可以确定字符串是否满足某种条件,如是否为空、是否以特定字符开头或结尾、是否包含子字符串等。

字符串判断在编程中非常常见且实用。通过这些判断操作,我们可以根据字符串的属性和内容来做出相应的处理和决策。

常见的字符串判断操作包括:空判断(检查字符串是否为空或长度为0)、前缀和后缀判断(检查字符串是否以指定的前缀或后缀开头或结尾)、包含判断(检查字符串是否包含指定的子字符串)等等。

各方法如下所示:

  • boolean contains(String s):判断该字符串是否包含传进来的字符串;
  • boolean startsWith(String prefix):判断字符串是否以传进来的字符串开头;
  • boolean endsWith(String suffix):判断字符串是否以传进来的字符串结尾;
  • boolean isEmpty():判断字符串是否为空。

使用方式如下:

//	范例 16: 字符串判断
public class Demo{
   
     
	public static void main(String args[]){
   
     
        String str = "Hello, World!";

        // 判断字符串是否包含传进来的字符串
        String searchStr1 = "Hello";
        boolean containsStr1 = str.contains(searchStr1);
        System.out.println("字符串 \"" + str + "\" 是否包含 \"" + searchStr1 + "\": " + containsStr1);

        String searchStr2 = "ChinaAi";
        boolean containsStr2 = str.contains(searchStr2);
        System.out.println("字符串 \"" + str + "\" 是否包含 \"" + searchStr2 + "\": " + containsStr2);

        // 判断字符串是否以传进来的字符串开头
        String prefix1 = "Hello";
        boolean startsWithPrefix1 = str.startsWith(prefix1);
        System.out.println("字符串 \"" + str + "\" 是否以 \"" + prefix1 + "\" 开头: " + startsWithPrefix1);

        String prefix2 = "Hi";
        boolean startsWithPrefix2 = str.startsWith(prefix2);
        System.out.println("字符串 \"" + str + "\" 是否以 \"" + prefix2 + "\" 开头: " + startsWithPrefix2);

        // 判断字符串是否以传进来的字符串结尾
        String suffix1 = "World!";
        boolean endsWithSuffix1 = str.endsWith(suffix1);
        System.out.println("字符串 \"" + str + "\" 是否以 \"" + suffix1 + "\" 结尾: " + endsWithSuffix1);

        String suffix2 = "Java";
        boolean endsWithSuffix2 = str.endsWith(suffix2);
        System.out.println("字符串 \"" + str + "\" 是否以 \"" + suffix2 + "\" 结尾: " + endsWithSuffix2);

        // 判断字符串是否为空
        boolean isEmpty = str.isEmpty();
        System.out.println("字符串 \"" + str + "\" 是否为空: " + isEmpty);
    }
}    

输出结果:

字符串 "Hello, World!" 是否包含 "Hello": true
字符串 "Hello, World!" 是否包含 "ChinaAi": false
字符串 "Hello, World!" 是否以 "Hello" 开头: true
字符串 "Hello, World!" 是否以 "Hi" 开头: false
字符串 "Hello, World!" 是否以 "World!" 结尾: true
字符串 "Hello, World!" 是否以 "Java" 结尾: false
字符串 "Hello, World!" 是否为空: false

2.6 字符串拆分

字符串拆分指的是将一个字符串按照指定的分隔符或规则进行分割,将其拆分成多个子字符串的操作。

Java中常见的字符串拆分方式包括以下几种:

  • 基于固定分隔符拆分:按照给定的特定字符或字符串作为分隔符,将原始字符串拆分成多个子字符串。例如,使用空格、逗号、冒号等字符作为分隔符进行拆分。
  • 基于正则表达式拆分:使用正则表达式模式作为分隔符,根据模式匹配的位置将字符串拆分成多个子字符串。这种方式更加灵活,可以使用复杂的模式来满足特定需求。

字符串拆分在处理文本数据时经常使用,可以根据需要将字符串拆分成单词、句子、行等。拆分后的子字符串可以进一步进行处理、存储、打印或进行其他操作。

字符串的切割拆分操作主要通过split(String regex)split(String regex, int limit) 方法来实现,其中参数regex是一个正则表达式,用于指定分隔符;limit参数指定返回的数组长度限制。

使用方式如下:

//	范例 17: 字符串拆分
public class Demo{
   
     
	public static void main(String args[]){
   
     
        String str = "Java is a programming language";
        String[] splitStr = str.split(" ");
        for (String s : splitStr) {
   
     
            System.out.println(s);
        }
        System.out.println();
        
		splitStr = str.split(" ", 3);
        for (String s : splitStr) {
   
     
            System.out.println(s);
        }
    }
}

输出结果:

Java
is
a
programming
language

Java
is
a programming language

2.7 字符串替换

Java中提供了多个方法用于替换字符串中的字符或子串,分别是:

  • replace(char oldChar, char newChar) 方法用于将字符串中的指定字符 替换为新字符。该方法会返回一个新字符串,原始字符串不会改变。若原始字符串中有多个匹配的字符,所有匹配的字符都会被替换;
  • replace(CharSequence target, CharSequence replacement) 方法用于将字符串中与目标字符序列 匹配的部分替换为指定字符序列。该方法同样会返回一个新字符串,而不改变原始字符串;
    注意这个方法的参数是 CharSequence 类型,这意味着可以传递任何实现了 CharSequence 接口的对象,包括 StringStringBuilder 等。这使得它更具灵活性,可以进行更复杂的字符串替换操作;
  • replaceFirst(String regex, String replacement)方法将字符串中首次出现的与正则表达式regex匹配的部分替换;
  • replaceAll(String regex, String replacement) 方法会将字符串中所有与正则表达式regex匹配的部分全部替换。

使用方式如下:

//	范例 18: 字符串替换
public class Demo{
   
     
	public static void main(String args[]){
   
     
        String str = "Hello World";
        String newStr1 = str.replace('l', 'L');
        String newStr2 = str.replace("World", "Java");
        System.out.println(newStr1);
        System.out.println(newStr2);

        String newStr3 = str.replaceFirst("l", "L");
        String newStr4 = str.replaceAll("l", "L");
        System.out.println(newStr3);
        System.out.println(newStr4);
    }
}

运行结果:

HeLLo WorLd
Hello Java
HeLlo World
HeLLo WorLd

2.8 字符串和其他类型的转换

在Java中,可以通过类型转换将字符串与其他数据类型相互转换。这种转换可以帮助我们在不同的数据类型之间进行交互和操作。

1、 其他类型到字符串的转换:将其他数据类型转换为字符串可以使用多种方式,最常见的是使用字符串连接运算符+,它可以将其他类型的值与一个空字符串连接起来从而得到字符串此外,也可以使用下面的方法将对象转换为字符串:;

  • String.valueOf(Object obj):将其他类型转换为字符串;

  • toString():将对象转换为字符串表示形式的方法。它是定义在顶级父类Object中的方法,默认情况下 toString() 方法返回对象的哈希码。
    2、 字符串到其他类型的转换(字符串解析):通过将字符串解析为目标类型的值,将字符串转换为对应的基本数据类型或其他类型的值;

  • toCharArray():将字符串转换为字符数组;

  • getBytes():使用平台的默认字符集将此 字符串编码为字节数组;

  • getBytes(String charsetName):使用指定字符集将 字符串编码为字节数组。
    3、 包装类对字符串转换的支持:Java各个包装类提供了对字符串转换的支持,这些方法将字符串作为输入,返回相应类型的值;

  • int intValue = Integer.parseInt(stringValue); : 将字符串转换为整数;

  • double doubleValue = Double.parseDouble(stringValue); : 将字符串转换为双精度浮点数;

  • float floatValue = Float.parseFloat(stringValue); : 将字符串转换为单精度浮点数;

  • boolean booleanValue = Boolean.parseBoolean(stringValue);: 将字符串转换为布尔值。

关于包装类对字符串转换的操作支持更多情况,请参考我的另一篇文章:
【Java高级语法】(四)包装类:关于包装类的那些细节你都清楚吗?~

使用方式如下:

//	范例 19:  字符串和其他类型的转换
public class Demo{
   
     
	public static void main(String args[]){
   
     
        String str = "Hello, world!";

        // 将字符串转换为字符数组
        char[] charArray = str.toCharArray();
        System.out.println("字符串转换为字符数组:");
        for (char c : charArray) {
   
     
            System.out.print(c + "、");
        }
        System.out.println();

        // 将其他类型转换为字符串
        int number = 123;
        String numberStr = String.valueOf(number);
        System.out.println("将整数转换为字符串:" + numberStr);

        double decimal = 3.14;
        String decimalStr = String.valueOf(decimal);
        System.out.println("将浮点数转换为字符串:" + decimalStr);

        boolean bool = true;
        String boolStr = String.valueOf(bool);
        System.out.println("将布尔值转换为字符串:" + boolStr);

        // 使用平台的默认字符集将字符串编码为字节数组
        byte[] bytes1 = str.getBytes();
        System.out.println("默认字符集下的字节数组:");
        for (byte b : bytes1) {
   
     
            System.out.print(b + " ");
        }
        System.out.println();

        // 使用指定字符集将字符串编码为字节数组(UTF-8)
        byte[] bytes2 = str.getBytes(Charset.forName("UTF-8"));
        System.out.println("UTF-8字符集下的字节数组:");
        for (byte b : bytes2) {
   
     
            System.out.print(b + " ");
        }
        System.out.println();

        // 使用指定字符集将字符串编码为字节数组(GBK)
        byte[] bytes3 = str.getBytes(Charset.forName("GBK"));
        System.out.println("GBK字符集下的字节数组:");
        for (byte b : bytes3) {
   
     
            System.out.print(b + " ");
        }
        System.out.println();
    }
}

输出结果:

字符串转换为字符数组:
H、e、l、l、o、,、 、w、o、r、l、d、!、
将整数转换为字符串:123
将浮点数转换为字符串:3.14
将布尔值转换为字符串:true
默认字符集下的字节数组:
72 101 108 108 111 44 32 119 111 114 108 100 33 
UTF-8字符集下的字节数组:
72 101 108 108 111 44 32 119 111 114 108 100 33 
GBK字符集下的字节数组:
72 101 108 108 111 44 32 119 111 114 108 100 33 

此程序主要实现了字符串的转换操作,利用 toCharArray() 方法可以将一个字符串拆分为字符数组,而拆分后的字符数组长度就是字符串的长度。当利用toCharArray() 方法将字符串拆分为字符数组后,实际上就可以针对每一个字符进行操作。

2.9 字符串格式化

Java中使用String.format()方法可以对字符串进行格式化操作。

  • String.format(String format, Object... args):使用格式规范化字符串。第一个参数是格式字符串,后续参数是需要插入到格式字符串中的变量。

使用方式如下:

//	范例 20: 字符串格式化
public class Demo{
   
     
	public static void main(String args[]){
   
     
        int number = 123;
        double decimal = 3.14;

        // 使用格式化字符串
        String formattedString = String.format("The number is %d and the decimal is %.2f", number, decimal);
        System.out.println("格式化字符串:" + formattedString);
    }
}

输出结果:

格式化字符串:The number is 123 and the decimal is 3.14

2.10 字符串连接

在Java中,可以使用+运算符或String concat(String str)方法来拼接字符串。使用案例如下:

//	范例 21: 字符串连接
public class Demo{
   
     
	public static void main(String args[]){
   
     
        String str1 = "Hello";
        String str2 = "World";
        String result1 = str1 + ", " + str2;
        String result2 = str1.concat(", ").concat(str2);
        System.out.println(result1); 
        System.out.println(result2);
    }
}

运行结果:

Hello, World
Hello, World

需要注意的是,频繁拼接字符串时,使用StringBuilderStringBuffer类会更高效。后面文章会详细介绍关于这两个类的更多情况。

2.11 手动入池

在Java中,通过手动将字符串加入字符串池,可以实现字符串的共享,从而优化内存使用。可以使用以下方式将字符串手动添加到字符串池中:

  • intern():这个方法会返回字符串池中对应的字符串,如果池中没有该字符串,则将它添加到池中并返回。

使用方式如下:

//	范例 22:  手动入池
public class Demo{
   
     
	public static void main(String args[]){
   
     
        // 创建两个不同的字符串对象
        String str1 = new String("Hello");
        String str2 = new String("Hello");

        // 执行intern()方法后,将字符串加入字符串常量池
        String internStr1 = str1.intern();
        String internStr2 = str2.intern();

        // 检查两个字符串是否引用同一个字符串对象
        boolean isSameObject = (internStr1 == internStr2);
        System.out.println("是否引用同一个字符串对象: " + isSameObject);

        // 使用equals()方法检查字符串内容是否相同
        boolean isEqual = internStr1.equals(internStr2);
        System.out.println("字符串内容是否相同: " + isEqual);
    }
}

输出结果:

是否引用同一个字符串对象: true
字符串内容是否相同: true

2.12 其他操作

Java中String 类还包括以下一些操作方法:

  • toLowerCase():将字符串中的英文字母转小写;
  • toUpperCase():将字符串中的英文字母转大写;
  • trim():去掉字符串左右两边的空格。

使用方式如下:

//	范例 23: 英文字母转小写、转大写、去掉字符串左右两边的空格
public class Demo{
   
     
	public static void main(String args[]){
   
     
        String input = "   Hello, World!   ";
        System.out.println("原始字符串: " + input);

        // 转小写
        String lowercase = input.toLowerCase();
        System.out.println("转小写后: " + lowercase);

        // 转大写
        String uppercase = input.toUpperCase();
        System.out.println("转大写后: " + uppercase);

        // 去掉左右两边的空格
        String trimmed = input.trim();
        System.out.println("去掉空格后: " + trimmed);
    }
}  

输出结果:

原始字符串:    Hello, World!   
转小写后:    hello, world!   
转大写后:    HELLO, WORLD!   
去掉空格后: Hello, World!

3️⃣ 字符串常量池

前面我们介绍了String类的两种实例化方式,分别是将字符串存储在不同的内存位置。

第一种***通过String类构造方法创建的对象,会存储在JVM内存结构的堆中***。这些字符串对象在堆上分配空间,并包含有关字符串长度、字符内容等信息。使用方法和属性来操作和访问字符串对象。在实际使用时,使用这种方式是很少的。

而不同于第一种字符串存储方式,通过直接赋值的字符串是存储在方法区(也即元空间metaspace)的字符串常量池里的。字符串常量池是一个特殊的字符串存储区域,用于存储程序中的字符串常量。

字符串常量池主要有以下特点:

  • 字面量优化:编译器会对代码中的字符串常量进行优化,将其放入字符串常量池中。如果同一个字面量字符串在源代码中多次出现,只会保留其一个副本,以节省内存空间;
  • 共享和复用:字符串常量池中的字符串是共享的,意味着如果代码中使用了相同内容的字符串常量,它们将被存储为同一个对象。这种共享和复用能够提高性能并减少内存占用;
  • 运行时常量池:在Java中,字符串常量池是java.lang.String类的一个属性,属于其运行时常量池。除了存储字面量字符串,它还可以包含通过调用String类的intern()方法手动添加的字符串;
  • 不可变性:字符串常量池中的字符串是不可变的,即不允许修改操作。当对字符串进行拼接、截取等操作时,会生成新的字符串对象,而不影响原始的字符串对象。这种不可变性提供了字符串的线程安全性和缓存机制。

总结来说,字符串常量池是为了优化字符串的存储和提高性能而设计的特殊存储区域,其中存放着程序中使用的字符串常量,并且这些字符串是共享和不可变的。

4️⃣ 应用场景

字符串操作在编程中有很多常见的使用场景。下面是几个常见的例子:

1、 用户输入验证:当需要验证用户输入是否符合某种格式时,可以使用字符串操作来检查输入的有效性例如,验证电子邮件地址、手机号码或密码等;
2、 数据清洗和处理:在数据处理和清洗阶段,字符串操作经常被用于提取关键信息、去除无效字符、转换大小写、删除空格等操作这些操作通常涉及到文本文档、日志文件、CSV文件等;
3、 字符串拼接和格式化:很多情况下,我们需要将多个字符串拼接成一个完整的句子或消息这时可以使用字符串连接操作符(+)或者使用字符串的format()方法来处理字符串的格式化;
4、 字符串比较和搜索:字符串操作经常涉及对字符串进行比较和搜索,如判断两个字符串是否相等、字符串的包含关系、查找特定子串在字符串中出现的位置等;
5、 URL和路径处理:在Web开发和文件处理中,字符串操作用于处理URL和文件路径的拼接、解析和规范化;
6、 文本分析和自然语言处理:在文本分析和自然语言处理任务中,字符串操作广泛用于分词、词频统计、句子切分、语义规则匹配等算法;
7、 字符串加密和哈希:在安全领域中,字符串操作常被用于密码哈希、加密和解密等操作,以确保数据的保密性和完整性;

综上所述,字符串操作在各种应用场景中都起着重要作用。了解字符串操作的各种方法和技巧,可以提高程序的效率和可靠性,并为开发者提供更多处理字符串的灵活性。

* 总结

在本文中,我们探讨了关于Java中String类的一些重要概念和方法。首先,我们学习了String类的实例化方式,并比较了直接赋值和使用new关键字的区别。接着介绍了字符串常量的概念,并解释了享元设计模式如何优化字符串存储。我们还提到了字符串的不可改变性,说明了其在多线程环境下的好处。

随后,详细讨论了String类的常用方法。了解了如何获取字符串的长度以及访问特定索引处的字符、如何截取子字符串、比较字符串是否相等以及判断字符串是否包含特定的字符或子字符串。此外,我们还学习了如何拆分字符串、替换字符串中的字符或子字符串以及将字符串转换为其他类型等各种操作。

然后还介绍了如何格式化字符串输出以及连接多个字符串。在讨论到字符串的手动入池时,讲述了使用 intern()方法的作用。最后,我们简要概述了一些字符串操作的使用场景。

总而言之,String类是Java中一个非常重要的类,它提供了丰富的方法来处理字符串。通过深入理解这些概念和方法,我们可以更加高效地处理字符串操作,从而提升编码能力。


[* ]nbsp_nbsp 6