前言
字符串类型广泛应用在 Java 编程当中,在之前 【Java】数据类型和运算符 这一小节里面有简单介绍过一点 String 类型,但是那不过是 String 类型的相关内容的九牛一毛
在本小节当中将向大家深刻剖析 String 类,了解其创建方法,内存存储,各项基本操作以及深刻理解 StringBuffer 和 StringBuilder 类从而对字符串做出更多的改变。
文章目录
- 前言
- 一、创建字符串
- 二、字符串常量池
- 三、字符串比较相等
- 四、实例分析(:red_circle:)
- 五、字符串与字符 & 字符串与字节
- 5.1 字符串与字符
- 5.2 字符串与字节
- 六、String类常见的操作(:red_circle:)
- 6.1 字符串比较
- 6.2 字符串查找
- 6.3 字符串替换
- 6.4 字符串拆分
- 6.5 字符串截取
- 6.6 其余常用操作
- 七、StringBuffer 类 & StringBuilder类(:red_circle:)
一、创建字符串
常见的创建字符串的方式:
//方法一
String str1 = "Hello world";
//方法二
String str2 = new String("Hello");
//方法三
char[] array = {'W','o','r','l','d'};
String str3 = new String(array);
内存布局:
💬代码解释:
- String 是
引用类型
,其变量是引用变量
,存放的是堆里面的一块地址- “Hello” 是
字符串字面值常量
,也是 String 类型- 方法一是
直接赋值
方法,直接让 str1 指向堆里面 “Hello world” 这一字符串- 方法二是 String 的
构造方法
,先实例化一个对象,让该对象指向堆里面“Hello“ 这一字符串- 方法三是将
一个字符数组转化为一个字符串
。向实例化的 String 类里传一个字符数组 array ,调用其中这样类型的构造方法,该构造方法中将 array 数组的内容拷贝了一份,让 String 类里的value 数组接收了新拷贝的内容- 一般来说,方法一使用的更加的多
另外,在C语言的同学知道字符串是以’\0’结尾的,但是在 Java 中并没有这样的说法。
二、字符串常量池
📑代码示例:
String str1 = "hello" ;
String str2 = "hello" ;
System.out.println(str1 == str2);
💬代码解释:
将该代码放入main 函数中,会发现结果为 true
- String 类型是引用类型,因此使用 == 并不是在比较字符串的内容是否相等,而是在比较 str1 和str2 这两个引用
是否指向同一个对象
- 结果为 true 说明,str1 和 str2 指向的 “Hello” 字符串是同一个 ,并没有在堆中开辟两块不一样的空间分别存放两个 “Hello”
实际上,为了避免每次都创建相同的字符串对象,多余的进行内存的分配,JVM内部对字符串对象的创建做了一定的优化,在堆中有一块区域用来存储字符串,该区域就是字符串常量池
。当直接赋值时,字符串内容若在字符串常量池中
,就直接进行引用
,若字符串常量池中没有
,则将字符串内容自动保存
到字符串常量池中
三、字符串比较相等
📑代码示例:
//例1
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
//例2
String str3 = new String("Hello");
String str4 = new String("Hello");
System.out.println(str3 == str4);
💬代码解释:
将该代码放入main 函数中,会发现例1结果为 true,例2结果为 false
在字符串常量池这一小节中有说到 == 是在比较 str1 和str2 这两个引用是否指向同一个对象,那么。。。
内存布局:
- str1 和 str2 采取的是直接赋值法,因此两者指向的是同一个对象,比较之后结果为 true
- str3 和 str4 采取的是 new String 的方法,相当于在堆中由开辟了两块新的空间来存储 “Hello” 的内容,比较后结果为 false
若真的想要对字符串的内容进行比较,就需要采用 equals
方法
📑代码示例:
String str3 = new String("Hello");
String str4 = new String("Hello");
System.out.println(str3.equals(str4));
//实例:
//System.out.println("Hello".equals(str3));
💬代码解释:
将该代码放入main 函数中,结果为 true
注意:
- 在使用该方法时一定要
确保 str3 不是 null
,如果是就会报空指针异常。但是 str4 可以是 null- 用实例的方法来比较 “Hello” 和 str3 的值的内容是否相等是比较好的,因为 “Hello” 也是 String 对象,也可以使用 equals 方法,且不存在会报错的风险
四、实例分析(🔴)
实例一:
📑代码示例:
public static void main(String[] args) {
//例一
String str1 = "Hello world";
String str2 = "Hello" + " world";
System.out.println(str1 == str2)
//例二
String str3 = "Hello world";
String str4 = "Hello";
String str5 = str4 + " world";
System.out.println(str3 == str5);
}
💬代码解释:
例一的结果为 true,例二的结果为 false
- 在例一中,“Hello” 和 " world" 都是字符串常量,在编译的时候会进行优化,
自动拼接
。因此实际上,str2 指向的对象的内容就是 “Hello world”- 在例二中,str4 是一个变量,在编译的时候并不知道它的值,运行时才会知晓,因此 str5 指向的是 “Hello” 和 “ world” 拼接后产生的新的对象
内存布局:
实例二:
📑代码示例:
public static void main(String[] args) {
//例一
String str1 = "Hello world";
String str2 = "Hello" + new String(" world");
System.out.println(str1 == str2);
// 例二
String str3 = "Hello world";
String str4 = new String("Hello") +new String(" world");
System.out.println(str3 == str4);
}
💬代码解释:
例一和例二的结果都是 false
内存布局:
例一:
例二:
实例三:
📑代码示例:
public static void main(String[] args) {
//例一
String str1 = new String("Hello") + new String(" world");
str1.intern();
String str2 = "Hello world";
System.out.println(str1 == str2);
//例二
String str3 = new String("Hello") + new String(" world");
String str4 = "Hello world";
str3.intern();
System.out.println(str3 == str4);
}
💬代码解释:
例一的结果为 true,例二的结果为 false
inter()
的作用是手动将字符串入池
(字符串常量池)
判断 str1 目前指向的对象在常量池中有没有,发现没有,就将这个对象放入到常量池中
判断 str3 目前指向的对象在常量池中有没有,发现有,就无需有所行动
内存布局:
例一:
例二:
inter方法的优点:
如果用构造方法创建字符串,每次都会在堆上开辟两块内存空间,传入的字符串参数是一个匿名对象,使用一次后将不会被使用,将会成为垃圾空间。还有就是同一个字符串可能会被存储很多次,浪费空间,使用 inter 方法,将池里没有的字符串手动导入池中,就可以避免这一麻烦。
实例四:
📑代码示例:
public static void main(String[] args) {
String str1 = "Hello";
String str2 = str1;
str1 = "world";
System.out.println(str1 == str2);
}
💬代码解释:
结果为 false
str1 和 str2 都是引用类型,str2 指向了 str1 指向的对象,但是却没有办法通过 str1 去改变 str2 指向的对象,因此就算这两个引用指向同一片空间,也是没有办法通过其中一个修改另一个的内容
实例五:
众所周知,字符串常量是没有办法进行修改的,若是想要将字符串 “Hello” 改成 “hello”,应当怎么操作呢?
📑代码示例1:
public static void main(String[] args) {
String str = "Hello";
str = "h" + str.substring(1);
System.out.println(str);
}
💬代码解释:
结果为 hello
- 该方法实际上就是借助原来的字符串,创建新的字符串,并没有将原来的 “Hello” 真正的改成 “hello”
- substring(1) 表示从 str 字符串偏移量为1开始提取字符串
📑代码示例2:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str = "Hello";
Class c = String.class;
//获取String类中的value字段
Field field = c.getDeclaredField("value");
//权限修改了
field.setAccessible(true);
//把str中的value属性获取到.
char[] vals = (char[]) field.get(str);
//修改value的值
vals[0] = 'h';
//hello
System.out.println(str);
}
💬代码解释:
结果为 hello
- 这里用到了
反射
的操作(对于该操作以后会进行仔细的讲解),该操作指的就是在程序运行过程中,获取或修改某个对象的详细信息,会破坏封装
实例六:
📑代码示例:
public static void main(String[] args) {
String str = "hello" ;
for(int x = 0; x < 1000; x++) {
str+= x ;
}
System.out.println(str);
}
💬代码解释:
结果为一字符串,“hello01234…998999”
这样的代码不应该出现在开发当中,每次循环都会产生新的对象,效率低下
五、字符串与字符 & 字符串与字节
5.1 字符串与字符
字符串内部包含一个字符数组 value,String 可以和 char[] 相互转换
📑代码示例:
public static void main(String[] args) {
//例一
char[] value = {'h','e','l','l','o'};
String str1 = new String(value,1,4);
System.out.println(str1);
//例二
String str2 = "hello";
char ch = str2.charAt(1);
System.out.println(ch);
//例三
String str3 = "hello";
char[] chars = str3.toCharArray();
System.out.println(Arrays.toString(chars));
}
🏸 代码结果:
💬代码解释:
例一
public String(char[] value,int offset,int count)
是 String 类的一个构造方法,表示从偏移量为1的位置开始取4个字符来构造String对象,将取出的部分变成字符串,切记偏移量和取的字符个数不可超出数组,否则会数组越界异常
例二
public char charAt(int index)
是取得指定索引位置的字符,切记索引的大小不可超过字符串中字符的个数减一,否则会数组越界异常
例三
public char[] toCharArray()
是将字符串以字符数组的方式进行存储
📑代码示例:
判断给定的字符串其是否全部由数字所组成
public class TestDemo {
public static boolean isNumber (String str ) {
if(str == null) return false;//str不指向任何对象
if(str.length() == 0) return false;//空串
char[] chars = str.toCharArray();
for (char ch :chars) {
if(ch < '0' || ch >'9') {
return false;
}
}
return true;
}
public static void main(String[] args) {
String str = "";
if(isNumber(str)) {
System.out.println("全都是数字字符!");
}else {
System.out.println("不全都是数字字符!");
}
}
}
注意:
一定要考虑完善,空串以及引用变量不指向任何对象多需要考虑到
5.2 字符串与字节
字节常用于数据传输以及编码转换的处理之中,String 也能方便的和 byte[] 相互转换
📑代码示例:
public class TestDemo {
public static void main(String[] args) throws UnsupportedEncodingException {
//例一
byte[] bytes = {97,98,99,100};
String str = new String(bytes);
System.out.println(str);
//例二
String str3 = new String(bytes,1,3);
System.out.println(str3);
//例三
String str4 = "abcd";
byte[] bytes5 = str4.getBytes();
System.out.println(Arrays.toString(bytes5));
//例四
String str5 = "你好";
byte[] bytes3 = str5.getBytes("utf-8");
System.out.println(Arrays.toString(bytes3));
byte[] bytes4 = str5.getBytes("gbk");
System.out.println(Arrays.toString(bytes4));
}
}
🏸 代码结果:
💬代码解释:
例一
public String(byte[ ] bytes)
是将字节数组中的每一个元素变成对应的字符,字符组成字符串
例二
public String(byte[ ] bytes,int offset,int length)
是 String 类的一个构造方法,表示从偏移量为1的位置开始取3个字节数组的元素来构造String对象,将取出的部分变成字符串,切记偏移量和取的字符个数不可超出数组,否则会数组越界异常
例三
pulic byte[ ] getBytes( )
是将字符串以字节数组的形式返回
例四
public byte[ ] getBytes(String charsetName) throws UnsupportedEncodingException
是编码转换处理,以不同的字节码去获取字符串,将其转换为 byte 数组(一个汉字 utf-8 占3个字节,gbk 占2个字节)
📑示例:
使用该方法 String 类型上会出现一道
横线
,点进去发现有@Deprecated
这一注解,说明该方法已经被弃用
六、String类常见的操作(🔴)
6.1 字符串比较
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
String str1 = "abc";
String str2 = "aBc";
//例一(比较相等)
System.out.println(str1.equals(str2));
//例二(比较相等)
System.out.println(str1.equalsIgnoreCase(str2));
//例三(比较大小)
System.out.println(str1.compareTo(str2));
}
}
🏸 代码结果:
💬代码解释:
例一
public boolean equals(Object anObject)
在之前的字符串比较相等中有介绍过,作用就是比较两个字符串是否相等(区分大小写)
例二
public boolean equalsIgnoreCase(String anotherString)
作用也是比较两个字符串是否相等(不区分大小写)
例三
public int compareTo(String anotherString)
作用为比较两个字符串的大小关系
str1 大于 str2 返回正数,等于 str2 返回 0,小于 str2 返回负数。
在两比较的字符串从后往后找的过程中,找到的第一个不相同的字符,这俩字符的大小关系就是整个字符串的大小关系。例如 str1 中的字符 b 比 str2 中的字符 B 大32,就返回正数32
如果两个字符串前面的字符都相等,但长度有所不一样,就返回长度的差值
6.2 字符串查找
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
String str1 = "ccabbabbcc";
//例一(是否存在)
System.out.println(str1.contains("abb"));
//例二(从头查找指定字符串)
System.out.println(str1.indexOf("abb"));
//例三(从指定位置查找指定字符串)
System.out.println(str1.indexOf("abb", 3));
//例四(从后向前查找指定字符串)
System.out.println(str1.lastIndexOf("abb"));
//例五(从指定位置从后向前查找指定字符串)
System.out.println(str1.lastIndexOf("abb", 5));
//例六(是否以指定字符串开头)
System.out.println(str1.startsWith("cca"));
//例七(从指定位置判断是否以指定字符串开头)
System.out.println(str1.startsWith("cab", 1));
//例八(是否以指定字符串结尾)
System.out.println(str1.endsWith("bcc"));
}
}
🏸 代码结果:
💬代码解释:
例一
public boolean contains(CharSequence s)
作用为判断一个子字符串是否存在,当且仅当字符串包含指定的 char 值序列时才返回 true。String 类有实现了 CharSequence 这个接口,因此可以直接传 String 类型的参数
例二
public int indexOf(String str)
作用为从头开始查找指定的字符串的位置,返回指定字符第一次出现的字符串内的索引,查找不到返回-1
例三
public int indexOf(String str, int fromIndex)
作用为从指定的索引开始,返回指定字符第一次出现的字符串内的索引,查找不到返回-1
例四
public int lastIndexOf(String str)
作用为返回指定子字符串最后一次出现的字符串中的索引,查找不到返回-1
例五
public int lastIndexOf(String str, int fromIndex)
作用为从指定的索引开始从后向前搜索,返回指定子字符串最后一次出现的字符串中的索引,查找不到返回-1
例六
public boolean startsWith(String prefix)
作用为测试此字符串是否以指定的前缀开头
例七
public boolean startsWith(String prefix, int toffset)
作用为测试在指定索引处开始的此字符串的子字符串是否以指定的前缀开头
例八
public boolean endsWith(String suffix)
作用为测试此字符串是否以指定的后缀结尾
6.3 字符串替换
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
String str1 = "cab cab cab";
//例一
System.out.println(str1.replace('a', 'p'));
//例二
System.out.println(str1.replaceAll("ab", "qq"));
//例三
System.out.println(str1.replaceFirst("ab", "pp"));
}
}
🏸 代码结果:
💬代码解释:
例一
public String replace(char oldChar, char newChar)
作用为将字符串中出现的字符(oldChar)替换为新的字符(newChar)
例二
public String replaceAll(String regex, String replacement)
作用为将字符串中出现的子字符串(regex)替换为新的字符串(replacement)
例三
public String replaceFirst(String regex, String replacement)
作用为将字符串中出现的第一个子字符串(regex)替换为新的字符串(replacement)
字符串是没有办法进行改变的,此处并未修改字符串 str1,而是产生了新的字符串
6.4 字符串拆分
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
String str1 = "aaa bbb ccc";
String[] result1 = str1.split(" ");//例一
for (String s:result1) {
System.out.println(s);
}
System.out.println("===========最多分两组===========");
String[] result2 = str1.split(" ",2);//例二
for (String s:result2) {
System.out.println(s);
}
}
}
🏸 代码结果:
💬代码解释:
例一
public String[] split(String regex)
作用为以字符串(regex)为分隔符,将字符串全部拆分
例二
public String[] split(String regex, int limit)
作用为以字符串(regex)为分隔符,将字符串拆分,拆分的极限为 limit
📑代码示例:
多次拆分
public class TestDemo {
public static void main(String[] args) {
String str1 = "name=Java&price=18";
String[] strings = str1.split("&");//第一次拆分
for (int i = 0; i < strings.length; i++) {
String[] strings1 = strings[i].split("=");//第二次拆分
System.out.println(Arrays.toString(strings1));
}
}
}
🏸 代码结果:
💬代码解释:
每一次的拆分实际上都形成了一个数组,多次拆分这样的代码会经常用到
注意:
字符
"|","*",",","+","."
都是一些特殊的字符,需要进行转义
,例如"\\+"(第一条 \ 将第二条 \ 转义为真正的斜杠,第二条 \ 将 +转义为真正的 + )如果是“”(空字符串),就需要写成“\\”
若想要字符串同时被多个分隔符进行分割,那么分隔符之间需要用
"|"
将其分开,例如str.split(" |#|@");
6.5 字符串截取
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
String str1 = "Hello World!";
//例一
System.out.println(str1.substring(1));
//例二
System.out.println(str1.substring(2, 7));
}
}
🏸 代码结果:
💬代码解释:
例一
public String substring(int beginIndex)
作用为从指定索引截取到结尾
例二
public String substring(int beginIndex, int endIndex)
作用为截取部分内容,从索引 beginIndex 截取到 endIndex ,前闭后开
6.6 其余常用操作
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
String str1 = " Hello 中国! ";
//例一
System.out.println(str1.trim());
//例二
System.out.println(str1.toUpperCase());
//例三
System.out.println(str1.toLowerCase());
//例四
System.out.println(str1.concat("你好!"));
//例五
System.out.println(str1.length());
//例六
System.out.println(str1.isEmpty());
}
}
🏸 代码结果:
💬代码解释:
例一
public String trim()
作用为删除字符串任何前导和尾随的空白字符,比如空格、换行符、制表符等(保留中间的),返回一个新的字符串
例二
public String toUpperCase()
作用为字符串转大写,只转字母
例三
public String toLowerCase()
作用为字符串转小写,只转字母
例四
public String concat(String str)
作用为字符串拼接作用,相当于’+’
例五
public int length()
作用为取得字符串的长度,注意这是一个方法,要和数组的 length 区分开来,那只是数组的一个属性
例六
public boolean isEmpty()
作用为判断字符串是否为空,注意是指字符串的长度为0,不是指 null
实际上,上面的很多方法都有很多的重载,在此不将各种重载的方法一一过一遍,举一反三
七、StringBuffer 类 & StringBuilder类(🔴)
众所周知,String 类型的字符串常量是不可以进行改变的,StringBuffer
和 StringBuilder
类的出现就可以实现对字符串的修改,这两个类大多数的功能都是一样的,接下来讲主要以 StringBuffer 类为范例,来说说 String 类和这两个类的区别。
区别一:
虽然这两者都能够表示字符串,但是方式有所不同,String 类创建字符串的方式有文章开头介绍的那三种,但是 StringBuffer
类是不能够进行直接赋值
,可以通过其构造方法
进行创建字符串。
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer("Hello");
System.out.println(stringBuffer);
}
}
区别二:
在 Sting 类里面想要实现拼接作用,用的是 ‘+’ 号,在 StringBuffer 类中采取 append() 方法
进行拼接,想要一直拼接就一直点 append() 方法即可。
更加重要的是 StringBuffer 类的这个对象在 append() 的时候是可以进行改变的,所进行的拼接实际上是在原来的对象中进行拼接
,没有产生新的对象,拼接完后,返回的是当前对象,也并没有对字符串常量池中的字符串有做过什么修改。
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer("Hello");
stringBuffer.append(" world").append("!!!");
System.out.println(stringBuffer);
}
}
👁🗨 查看底层:
众所周知,在拼接 String 类的字符串时,原理上是会产生新的对象,但是编译器在编译的时候进行了优化
📑代码示例:
public static void main(String[] args) {
String str = "hello" ;
for(int x = 0; x < 10; x++) {
str += x ;
}
System.out.println(str);
}
👁🗨 查看底层:
如果按照反汇编显示的那样进行代码实现,就应该是这样的。。。
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
String str = "hello" ;
for(int x = 0; x < 10; x++) {
StringBuilder stringBuilder = new StringBuilder();
str = stringBuilder.append(str).append(x).toString();
}
System.out.println(str);
}
}
然而,该代码在实现的时候仍然有不完善的地方,每次进入循环都需要实例化一个新的对象,因此可以这样升级代码,使之只需要 new 一个 StringBuilder。。。
📑升级代码示例:
public class TestDemo {
public static void main(String[] args) {
String str = "hello" ;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(str);
for(int x = 0; x < 10; x++) {
stringBuilder.append(x);
}
str = stringBuilder.toString();
System.out.println(str);
}
}
区别三:
StringBuffer 类有很多 String 类没有的方法,比如之前提到的 append() 方法
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer("lufituaeb olleH");
System.out.println(stringBuffer);
//例一
System.out.println(stringBuffer.reverse());
//例二
System.out.println(stringBuffer.delete(2, 5));
//例三
System.out.println(stringBuffer.insert(12,"!!!"));
}
}
🏸 代码结果:
💬代码解释:
例一
public synchronized StringBuffer reverse()
作用为反转字符串
例二
public synchronized StringBuffer delete(int start, int end)
作用为删除指定范围的数据,[start,end)
例三
public synchronized StringBuffer insert(int offset, String str)
作用为在指定索引处插入指定的数据
String 类和 StringBuffer 类之间的转换:
public class TestDemo {
public static void main(String[] args) {
//String——>StringBuffer
StringBuffer stringBuffer = new StringBuffer("Hello");
String str = stringBuffer.toString();
System.out.println(str);
//StringBuffer——>String
//方法一
String str2 = "world";
StringBuffer stringBuffer1 = new StringBuffer(str2);
System.out.println(stringBuffer1);
//方法二
StringBuffer stringBuffer2 = new StringBuffer();
stringBuffer2.append(str2);
System.out.println(stringBuffer2);
}
}
StringBuffer 类和 StringBuilder 类的区别:
StringBuffer 类被
synchronized
关键字修饰,而 StringBuilder 类 就没有
StringBuffer
采用同步处理
,属于线程安全操作,适合多线程
情况;而StringBuilder未采用同步处理
,属于线程不安全操作,适合单线程
情况
关于线程会在之后的博客中进行详细讲解。。。
完!
sa