009 认识 String 类

1. 创建字符串

常见的构造 String 的方式:

// 方式一
String str = "Hello Bit";

// 方式二
String str2 = new String("Hello Bit");

// 方式三
char[] array = {'a', 'b', 'c'};
String str3 = new String(array);

在官方文档上(https://docs.oracle.com/javase/8/docs/api/index.html)可以看到 String 还支持很多其他的构造方式,用到的时候去查就可以了。

注意事项:
(1)“hello” 这样的字符串字面值常量,类型也是 String。
(2)String 也是引用类型,String str = “Hello”; 这样的代码内存布局如下:
内存布局

回忆 “引用”
之前在讲数组的时候就提到了引用的概念。
引用类似于 C 语言中的指针,只是在栈上开辟了一小块内存空间保存一个地址。但是引用和指针又
不太相同,指针能进行各种数字运算(指针+1之类的),但是引用不能,这是一种 “没那么灵活” 的指针。
另外,也可以把引用想象成一个标签,“贴” 到一个对象上,一个对象可以贴一个标签,也可以贴多个。如果一个对象上面一个标签都没有,那么这个对象就会被 JVM 当做垃圾对象回收掉。
Java 中,数组、String 以及自定义的类都是引用类型。

由于 String 是引用类型,因此对于以下代码:

String str1 = "Hello";
String str2 = str1;

内存布局如图:
内存布局
那么,是不是修改 str1,str2 也会随之变化呢?

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = str1;
        System.out.println(str2);

        str1 = "World";
        System.out.println(str2);
    }
}

运行结果

我们发现,“修改” str1 之后,str2 也没发生变化,还是 “Hello”?
事实上,str1 = “World” 这样的代码并不算 “修改” 字符串,而是让 str1 这个引用指向了一个新的String 对象。

内存布局

2. 字符串比较相等

2.1 字符串比较的剖析

大部分编程语言中,比较两个字符串内容的相等,主要使用 ==。
但是 C 和 Java 语言例外,因为 C 语言中的字符串实际是字符数组,两个字符数组用 == 来比较,会隐式转换成指向数组首元素的指针,实际上比较的是两个指针指向是否一致,并不是比较字符串内容。
在 Java 语言中,如果针对引用类型使用 ==,是在比较两个引用保存的地址是否相等。

如果现在有两个 int 类型的变量,判断其相等可以使用 == 完成。

public class Test{
    public static void main(String[] args) {
        int x = 10;
        int y = 10;
		
        System.out.println(x == y);
    }
}

运行结果

如果在 String 类对象上使用 == 呢?
代码1:

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = "Hello";
        System.out.println(str1 == str2);
    }
}

运行结果
看起来貌似没什么问题,再换个代码试试,发现情况不太妙。
代码2:

public class Test{
    public static void main(String[] args) {
        String str1 = new String("Hello");
        String str2 = new String("Hello");
		
        System.out.println(str1 == str2);
    }
}

运行结果
分析两种创建 String 类的方式差异。
代码1内存布局:
运行结果
str1 和 str2 是指向同一个对象的,此时如 “Hello” 这样的字符串常量是保存在 字符串常量池 中。

关于字符串常量池
如 “Hello” 这样的字符串字面值常量,也是需要一定的内存空间来存储的。这样的常量具有一个特点,就是不需要修改(常量嘛)。所以如果代码中有多个地方引用都需要使用 “Hello” 的话,就直接引用到常量池的这个位置就行了,而没必要把 “Hello” 在内存中存储两次。

代码2内存布局:
内存布局
通过 String str1 = new String(“Hello”); 这样的方式创建的 String 对象相当于在堆上另外开辟了空间来存储 “Hello” 的内容,也就是内存中存在两份 “Hello”。

String 使用 == 比较字符串,并不是在比较字符串内容,而是比较两个引用是否指向同一个对象(比较两个引用中保存的地址是否相等)。

关于对象的比较
面向对象编程语言中,涉及到对象的比较,有三种不同的方式,比较身份、比较值、比较类型。
在大部分编程语言中,== 是用来比较比较值的,但是在 Java 中是用来比较身份的。
如何理解比较值和比较身份呢?
可以想象一个场景,快递储物柜有很多格子,每个格子里都放着东西。
快递柜
例如,“第二行, 左数第五列” 这个柜子和 “第二行,右数第二列” 这个柜子是同一个柜子,就是 身份相同。如果身份相同,那么里面放的东西一定也相同(值一定也相同)。
例如,“第一行,左数第一列” 这个柜子和 “第一行,左数第二列” 这两个柜子不是同一个柜子,但是柜子打开后,发现里面放着的是完全一模一样的两双鞋子,这个就是 值相同

2.2 equals 方法

Java 中想要比较字符串的内容,必须采用 String 类提供的 equals 方法。

public class Test{
    public static void main(String[] args) {
        String str1 = new String("Hello");
        String str2 = new String("Hello");

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

运行结果

equals 方法使用的技巧:
比较 str 和 “Hello” 两个字符串是否相等,应该如何来写呢?

String str = new String("Hello");

// 方式一
System.out.println("Hello".equals(str));

// 方式二
System.out.println(str.equals("Hello"));

更推荐使用 “方式一”,因为一旦 str 是 null,方式二的代码会抛出异常,而方式一不会。

public class Test{
    public static void main(String[] args) {
        String str = null;
        // 方式一
        System.out.println("Hello".equals(str));
        // 方式二
        System.out.println(str.equals("Hello"));
    }
}

运行结果
注意事项: “Hello” 这样的字面值常量,本质上也是一个 String 对象,完全可以使用 equals 等 String 对象的方法。

3. 字符串常量池

在上面的例子中,String 类的两种实例化操作,直接赋值以及 new 一个新的 String 类对象。

3.1 直接赋值

public class Test{
    public static void main(String[] args) {
        String str1 = "hello";
        String str2 = "hello";
        String str3 = "hello";

        System.out.println(str1 == str2);
        System.out.println(str1 == str3);
        System.out.println(str2 == str3);
    }
}

运行结果
内存布局:
内存布局
为什么没有开辟新的堆内存空间呢?
String 类的设计使用了共享设计模式
在 JVM 底层实际上会自动维护一个对象池(字符串常量池)。
(1)如果采用了直接赋值的方式进行 String 类的对象实例化操作,那么该实例化对象(字符串内容)将自动保存到这个对象池中。
(2)如果下次继续使用直接赋值的方式声明 String 类对象,此时对象池中如果有指定内容,将直接进行引用。如果没有,则开辟新的字符串对象,然后将其保存在对象池中以供下次使用。

理解 “池” (pool)
“池” 是程序中的一种重要的提升效率的方式,目的就是为了降低开销,提高效率。本质是把频繁使用过的东西提前保存好,以备用到的时候,随时可以使用。
在未来的学习中,我们会遇到各种 “内存池”,“线程池”,“数据库连接池” …

3.2 采用构造方法

类对象使用构造方法实例化是标准做法。分析如下程序:

String str = new String("hello");

内存布局
这种做法有两个缺点:
(1)如果使用 String 构造方法就会开辟两块堆内存空间,并且其中一块堆内存将成为垃圾空间(字符串常量 “hello” 也是一个匿名对象,用了一次之后就不再使用了,就成为垃圾空间,会被 JVM 自动回收掉)。
(2)字符串共享问题,同一个字符串可能会被存储多次,浪费空间。

String 类的 intern 方法:
可以使用 String 的 intern 方法来手动把 String 对象加入到字符串常量池中。

public class Test{
    public static void main(String[] args) {
        String str1 = new String("hello");
        String str2 = "hello";
        System.out.println(str1 == str2);
        
        // 使用 intern 方法
        String str3 = new String("hello").intern();
        System.out.println(str2 == str3);
    }
}

运行结果
内存布局:
调用 intern 方法,拿着当前字符串里的内容在字符串常量池中寻找,看当前内容是否存在于池中。如果存在,直接返回池中的地址;如果不存在,则把当前字符串的内容加到字符串常量池中,返回池中的地址。
内存布局

面试题:请解释 String 类中两种对象实例化的区别
(1)直接赋值:只会开辟一块堆内存空间,并且该字符串对象可以自动保存在对象池中以供下次
使用。
(2)构造方法:会开辟两块堆内存空间,不会自动保存在对象池中,可以使用 intern() 方法手动入
池。

综上,一般采取直接赋值的方式创建 String 对象。

4. 理解字符串不可变

4.1 字符串不可变

Java 的 String 是一种不可变对象,它的内容不可改变。

String 类的内部实现也是基于 char[ ] 来实现的,但是 String 类并没有提供 set 方法之类的来修改内部的字符数组。

形如下面的代码:

public class Test{
    public static void main(String[] args) {
        String str = "hello";
        str = str + " world";
        str += "!!!";
        
        System.out.println(str);
    }
}

运行结果
如 += 这样的操作,表面上好像修改了字符串,其实并没有。内存的变化如下:
运行结果
+= 之后 str 打印的结果改变,但并不是 String 对象本身发生改变,而是 str 引用到了其他的对象。

回顾引用:
引用相当于一个指针,里面保存的内容是一个地址。要区分清楚当前的修改到底是修改了地址对应内存的内容,还是引用中保存的地址改变了。

为什么 Java 的 String 为什么要设计成不可变的?(不可变对象的优点?)
1、方便放入字符串对象池,如果 String 可变,池中的内容发生变化,就会影响到所有引用这个池对象的结果。
2、不可变对象是线程安全的。
3、不可变对象更方便缓存 hash code,作为key 时可以更高效地保存到 HashMap 中。

注意事项: 如下代码不应该在实际开发中出现,会产生大量的临时对象,效率比较低。

public class Test{
    public static void main(String[] args) {
        String str = "Hello";

        for (int i = 0; i < 10; i++) {
            str += i;
        }

        System.out.println(str);
    }
}

运行结果

4.2 必须要修改字符串

例如,有字符串 str = “Hello”,想改成 str = “hello”,该怎么办?

1、常见办法:借助原字符串,创建新的字符串。

public class Test{
    public static void main(String[] args) {
        String str = "Hello";
        str = "h" + str.substring(1);

        System.out.println(str);
    }
}

运行结果

2、特殊办法:使用 “反射” 这样的操作可以破坏封装,访问一个类内部的 private 成员。
IDEA 中 “ctrl + 鼠标左键” 跳转到 String 类的定义,可以看到内部包含了一个 char[ ],保存了字符串的内容。
IDEA

import java.lang.reflect.Field;

public class Test{
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String str = "Hello";
        
        // 获取 String 的类对象
        // 根据 value 字段,这个 value 和 String 源码中的 value 是匹配的,在类对象中拿到对应的字段
        Field valueField = String.class.getDeclaredField("value");
        
        // 将这个字段的访问属性设为 true,让 value 这个 private 的成员也能被访问的
        valueField.setAccessible(true);
        
        // 将 str 中的 value 属性获取到
        char[] value = (char[]) valueField.get(str);
        
        // 修改 value 的值
        value[0] = 'h';

        System.out.println(str);
    }
}

运行结果

关于反射
反射是面向对象编程的一种重要特性,有些编程语言也称为 “自省”。
指的是程序运行过程中,获取/修改某个对象的详细信息(类型信息,属性信息等),相当于让一个对象更好地 “认清自己”。

5. 字符、字节与字符串

5.1 字符与字符串之间的转换

6. 字符串常见操作

6.1 字符串比较

6.1.1 equals() 方法 - 区分大小写的比较(比较相等)

1、equals() 方法

String str1 = "hello";
String str2 = "Hello";
System.out.println(str1.equals(str2));

2、==
String 使用 == 比较字符串,并不是在比较字符串内容,而是比较两个引用是否指向同一个对象(比较两个引用中保存的地址是否相等)。

6.1.2 equalsIgnoreCase() 方法 - 不区分大小写的比较(比较相等)

equalsIgnoreCase() 方法

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = "hello";
        
        System.out.println(str1.equals(str2));
        System.out.println(str1.equalsIgnoreCase(str2));
    }
}

运行结果

6.1.3 compareTo() 方法 - 比较大小

compareTo() 方法 :该方法返回一个整型,该数据会根据大小关系返回三类内容。
(1)相等:返回0;
(2)小于:返回内容小于0;
(3)大于:返回内容大于0。
注意: 这里的返回结果不是 -1 / +1,而是大于、小于0的数字。

字符串的比较大小规则:字典序
两个字符串,先比较首个字符的大小(根据 unicode 的值来判定),如果相同,就依次比较后面字符的大小,直到分出大小。

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = "hello";

        System.out.println(str1.compareTo(str2));
        System.out.println("ABC".compareTo("aBC"));
        System.out.println("A".compareTo("a"));
        
        System.out.println("ABCLMN".compareTo("ABCDEF"));
        System.out.println("A".compareTo("A"));
        System.out.println("刘".compareTo("王"));
    }
}

运行结果

6.2 字符串查找

6.2.1 contains() 方法

判断一个子字符串是否存在

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello World";
        String str2 = "World";
        String str3 = "World!";

        System.out.println(str1.contains(str2));
        System.out.println(str1.contains(str3));
    }
}

运行结果

6.2.2 indexOf() 方法

indexOf(String str):从头开始查找指定字符串的位置,查到后返回该位置的开始索引,如果查不到返回 -1。

indexOf(String str, int fromIndex):从指定位置开始查找子字符串位置。

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello World";
        String str2 = "World";
        String str3 = "Hi";

        System.out.println(str1.indexOf(str2));
        System.out.println(str1.indexOf(str3));

        System.out.println(str1.indexOf(str2, 4));
        System.out.println(str1.indexOf(str2, 7));

        System.out.println(str1.indexOf(str3, 2));
    }
}

运行结果
如果存在多个相同的子字符串,返回最左侧的结果的下表(从左往右查找,找到第一个结果就直接返回)。

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello World World";
        String str2 = "World";

        System.out.println(str1.indexOf(str2));
    }
}

运行结果

6.2.3 lastIndexOf() 方法

lastIndexOf(String str):从后向前查找指定字符串的位置,查到后返回该位置的开始索引,如果查不到返回 -1。

lastIndexOf(String str, int fromIndex):从指定位置由后向前查找子字符串位置。

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello World World";
        String str2 = "World";
        String str3 = "Hi";

        int result = str1.lastIndexOf(str2);
        System.out.println(result);

        result = str1.lastIndexOf(str2, result - 1);
        System.out.println(result);

        System.out.println(str1.lastIndexOf(str3));
    }
}

运行结果

6.2.4 startsWith() 方法

startsWith(String prefix):判断是否以指定字符串开头,返回 true / false。

startsWith(String prefix, int toffset):从指定位置开始判断是否以指定字符串开头,返回 true / false。

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello Hello World";
        String str2 = "Hello";
        String str3 = "Hi";

        System.out.println(str1.startsWith(str2));
        System.out.println(str1.startsWith(str3));

        System.out.println(str1.startsWith(str2, 2));
        System.out.println(str1.startsWith(str2, 6));
    }
}

运行结果

6.2.5 endsWith() 方法

endsWith(String suffix):判断是否以指定字符串结尾,返回 true / false。

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello Hello World";
        String str2 = "World";
        String str3 = "Hello";

        System.out.println(str1.endsWith(str2));
        System.out.println(str1.endsWith(str3));
    }
}

运行结果

startsWith() 与 endsWith() 的典型用法
1、判断某个链接的协议类型,会使用 startsWith()
如:https://www.baidu.com/
2、判断某个文件的类型,会使用 endsWith() 判断扩展名
如:Test.java

6.3 字符串替换

6.3.1 replaceAll() / replaceFirst() 方法

replaceAll(String regex, String replacement):替换所有的指定内容。

replaceFirst(String regex, String replacement):替换首个内容。

注意事项: 使用一个指定的新的字符串替换掉已有的字符串数据。由于字符串是不可变对象,替换操作不修改当前字符串,而是产生一个新的字符串。

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello Hello World";
        String str2 = "Hello";

        String result1 = str1.replaceAll(str2,"Big");
        System.out.println("result1:" + result1);
        System.out.println("str1:" + str1 + "\n");

        String result2 = str1.replaceFirst(str2,"Big");
        System.out.println("result2:" + result2);
        System.out.println("str1:" + str1);
    }
}

运行结果

6.4 字符串拆分 - split() 方法

一个完整的字符串,按照指定的分隔符,划分为若干个子字符串,再把这些部分放在一个数组中。

split(String regex):按照指定的分隔符,将字符串全部拆分。

split(String regex, int limit):按照指定的分隔符,将字符串部分拆分,该数组长度就是 limit。

import java.util.Arrays;

public class Test{
    public static void main(String[] args) {
        String str1 = "Hello World Java C++";

        // 按照空格拆分
        String[] result1 = str1.split(" ");
        System.out.println(Arrays.toString(result1));

        String[] result2 = str1.split(" ", 2);
        System.out.println(Arrays.toString(result2));
    }
}

运行结果

拆分是特别常用的操作,需要重点掌握。另外,正则表达式中有许多特殊字符代表特殊含义,作为分割符可能无法正确切分,需要加上转义字符。

import java.util.Arrays;

public class Test{
    public static void main(String[] args) {
        String str1 = "192.168.0.1";

        // 拆分 IP 地址
        // 正则表达式见到 '.' 是当成特殊符号对待
        // 正则表达式见到 '\.' 才是当成 '.' 对待
        // Java 的字符串中又将 '\' 当成 Java 的转义字符,为了表示一个原始的 '\',需要再次转义
        String[] result1 = str1.split("\\.");
        System.out.println(Arrays.toString(result1));
    }
}

运行结果

注意事项:
(1)字符 ’ | ‘,’ * ‘,’ + ’ 都得加上转义字符,前面加上 ’ \ '。
(2)如果是字符 ’ . ',那么就得写成 ’ \\. '。
(3)如果一个字符串中有多个分隔符,可以用 ’ | ’ 作为连字符。

6.5 字符串截取 - substring() 方法

从一个完整的字符串中截取出部分内容。

substring(int beginIndex):从指定索引截取到结尾。

substring(int beginIndex, int endIndex):截取部分内容。

public class Test{
    public static void main(String[] args) {
        String str = "HelloWorld!!!";
        
        // [begin, end) 前闭后开
        System.out.println(str.substring(5));
        System.out.println(str.substring(5,10));
    }
}

运行结果

注意事项:
(1)索引从0开始;
(2)注意前闭后开区间的写法,substring(0, 5) 表示包含 0 号下标的字符,不包含 5 号下标。

6.6 其他操作方法

这些操作同样都是创建了新的字符串,没有修改原有字符串,因为字符串是不可变对象。

trim():去掉字符串开头和结尾的空白字符(空格、换行、回车、制表符、翻页符等)。

toUpperCase():字符串转大写。

toLowerCase():字符串转小写。

length():获取字符串长度。
注意: 数组长度使用 数组名称.length 属性,而 String 中使用的是 length() 方法

intern():字符串入池操作。

concat():字符串连接,等同于 “+”,不入池。

isEmpty():判断是否为空字符串,但不是 null,而是长度为 0。
注意:String str1 = “”; 是空字符串;而 String str2 = null; 是空引用。

public class Test{
    public static void main(String[] args) {
        // 去掉字符串开头和结尾的空白字符
        String str1 = "  hello world  ";
        System.out.println("[" + str1 + "]");
        System.out.println("[" + str1.trim() + "]");
        System.out.println("");

        // 大小写转换,只转换字母
        String str2 = "  HELLO%$$%@#$%world  哈哈哈";
        System.out.println(str2.toUpperCase());
        System.out.println(str2.toLowerCase());
        System.out.println("");

        // 求字符串长度
        String str3 = " hello%$$%@#$%world 哈哈哈";
        System.out.println("str3.length = " + str3.length());
        System.out.println("");

        // 观察 isEmpty() 方法
        String str4 = "Hi";
        String str5 = "";
        String str6 = new String("Java");
        System.out.println(str4.isEmpty());
        System.out.println(str5.isEmpty());
        System.out.println(str6.isEmpty());

        System.out.println("hello".isEmpty());
        System.out.println("".isEmpty());
        System.out.println(new String().isEmpty());
    }
}

运行结果

7. StringBuffer 和 StringBuilder

首先来回顾下 String 类的特点:任何的字符串常量都是 String 对象,而且 String 的常量一旦声明不可改变,如果改变对象内容,改变的只是其引用的指向而已。
由于 String 的不可更改特性,为了方便字符串的修改,提供 StringBuffer 和 StringBuilder 类。
StringBuffer 和 StringBuilder 大部分功能是相同的。

在 String 中使用 “+” 来进行字符串连接,但是这个操作在 StringBuffer 类中需要更改为 append() 方法。

public class Test{
    public static void main(String[] args) {
        //  1、String 的 += 会产生新的 String 对象,在循环中使用是比较低效的
        String str = "hello";
        for (int i = 0; i < 100; i++) {
            str += i;
        }
        System.out.println(str);

        // 2、使用 StringBuffer
        StringBuffer SB = new StringBuffer("hello");
        for (int i = 0; i < 100; i++) {
            SB.append(i);
        }
        System.out.println(SB);
    }
}

运行结果

常用方法

reverse():字符串翻转。

delete(int start, int end):删除指定范围的数据( [begin, end) 前闭后开)。

insert(int offset, 各种数据类型 b):插入数据。

public class Test{
    public static void main(String[] args) {

        // 使用 StringBuffer
        StringBuffer stringBuffer1 = new StringBuffer("hello");
        // reverse()
        // 直接修改本身
        stringBuffer1.reverse();
        System.out.println(stringBuffer1.toString());

        // delete()
        StringBuffer stringBuffer2 = new StringBuffer("hello");
        stringBuffer2.delete(2, 4);
        System.out.println(stringBuffer2.toString());

        // insert()
        StringBuffer stringBuffer3 = new StringBuffer("hello");
        stringBuffer3.insert(4, "World");
        System.out.println(stringBuffer3.toString());
    }
}

运行结果

String、StringBuffer、StringBuilder 的区别
(1)String 的内容不可修改,StringBuffer 与 StringBuilder 的内容可以修改。
(2)StringBuffer 与 StringBuilder 大部分功能是相似的。
(3)StringBuffer 采用同步处理,属于线程安全操作;而 StringBuilder 未采用同步处理,属于线程不安全操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值