JAVA从零开始09_String字符串

一、String类的特性

Java 中的 String 类具有以下特性:

不可变性:字符串在创建后其内容无法更改。这意味着所有对字符串的修改操作都会生成一个新的字符串对象。不可变性有助于提高程序的安全性和稳定性。

存储在常量池:字符串常量(如 “Hello, world!”)会存储在常量池(字符串池)中。当创建具有相同值的字符串时,它们会共享同一个字符串对象,从而节省内存。这种行为仅适用于字符串字面量和使用 intern() 方法创建的字符串。

实例化:String 类既可以使用 new 关键字创建对象,也可以直接使用字符串字面量。当使用字面量创建字符串时,会自动将其放入字符串常量池(如果池中不存在相同内容的字符串)。

可以用作键:String 类重写了 hashCode() 和 equals() 方法,因此字符串对象可以用作哈希表(例如 HashMap)中的键。由于字符串是不可变的,它们的哈希值在创建后不会更改,这有助于提高查找性能。

字符串拼接:Java 支持使用 + 和 += 运算符进行字符串拼接。但是,由于字符串不可变,频繁的拼接操作可能导致性能下降。在这种情况下,可以使用 StringBuilder 或 StringBuffer 类优化性能。

支持 Unicode 字符:String 类支持 Unicode 字符,因此可以在字符串中使用各种语言和符号。

序列化:String 类实现了 Serializable 接口,因此字符串对象可以在网络上传输或写入文件。

这些特性使得 String 类适用于处理各种文本数据,并具有良好的性能和安全性。在进行大量字符串操作时,可以使用 StringBuilder 或 StringBuffer 类优化性能。


二、String字符串的实现原理

Java 中的 String 类是用来表示字符串的,其内部通过字符数组(char[])实现。在 Java 9 之后,String 类使用字节数组(byte[])和一个编码标记(COMPACT_STRINGS)实现,以节省内存。在这种情况下,字符串可以使用一个字节(即 Latin-1 编码)或两个字节(即 UTF-16 编码)表示每个字符。

Java 的 String 是不可变的,这意味着一旦创建了一个字符串,就无法更改其内容。这样做有几个好处:

  1. 安全性:不可变字符串在多线程环境中是线程安全的,无需担心同步问题。
  2. 缓存:由于字符串不可变,可以缓存它们的哈希值。这在使用字符串作为哈希表键时特别有用,因为只需计算一次哈希值。
  3. 字符串常量池:不可变字符串可以在字符串常量池中共享,减少了内存消耗。例如,当创建两个相同的字符串时,它们可以指向同一个内部字符数组。
  4. 可靠性:不可变对象通常更易于维护和调试,因为它们的状态不会在程序运行过程中发生变化。
    然而,字符串的不可变性也带来了一些缺点,特别是在大量字符串操作时,可能导致性能下降。为解决这个问题,Java 提供了 StringBuilder 和 StringBuffer 类,它们允许对字符串进行可变操作。

总之,Java 中的 String 类基于字符数组(Java 9 之后是字节数组和编码标记)实现,具有不可变性。这种设计旨在提高安全性、缓存性能、内存效率和可靠性。当需要对字符串执行大量操作时,可以使用 StringBuilder 和 StringBuffer 类以提高性能。


三、StringBuffer、StringBuilder、StringJoiner

StringBuffer、StringBuilder和StringJoiner都是用于处理字符串的Java类,它们之间的区别和使用场景如下:

  1. StringBuffer:

    1. 区别:StringBuffer是一个线程安全的可变字符序列。它在执行字符串拼接、插入、删除等操作时性能优于String,但略低于StringBuilder,因为它需要同步机制来确保线程安全。
    2. 使用场景:适用于在多线程环境下共享和修改字符串。当需要在多线程程序中处理字符串时,可以选择StringBuffer来确保线程安全。
  2. StringBuilder:

    1. 区别:StringBuilder是一个非线程安全的可变字符序列,性能优于StringBuffer和String。它在执行字符串拼接、插入、删除等操作时性能更高。
    2. 使用场景:适用于单线程环境下处理字符串。当需要在单线程程序中频繁地执行字符串操作(如拼接、插入、删除等)时,推荐使用StringBuilder。
  3. StringJoiner:

    1. 区别:StringJoiner是Java 8中引入的一个实用类,用于方便地将多个字符串连接在一起,同时可以添加定制的分隔符、前缀和后缀。它内部使用了StringBuilder来处理字符串操作,因此性能优于普通的字符串拼接。
    2. 使用场景:适用于需要将多个字符串连接在一起,且需要添加定制的分隔符、前缀和后缀的场景。当需要执行此类操作时,StringJoiner是一个简便、高效的选择。

总结:

当需要在多线程环境下处理字符串时,选择StringBuffer以确保线程安全。
当需要在单线程环境下频繁地执行字符串操作时,选择StringBuilder以获得更高的性能。
当需要将多个字符串连接在一起,并添加定制的分隔符、前缀和后缀时,选择StringJoiner以获得简便、高效的操作。


四、常用方法

这里将分别列举StringBuffer、StringBuilder和StringJoiner的一些常用方法:

StringBuffer
StringBuffer和StringBuilder的方法非常相似,因为它们都继承自AbstractStringBuilder。以下是StringBuffer的一些常用方法:

append():将指定的字符串、字符、数字或其他数据类型的值添加到StringBuffer的末尾。

StringBuffer sb = new StringBuffer();
sb.append("Hello, ");
sb.append("world!");

insert():将指定的字符串、字符、数字或其他数据类型的值插入到StringBuffer的指定位置。

StringBuffer sb = new StringBuffer("Hello world!");
sb.insert(5, ","); // 结果为 "Hello, world!"

delete():删除StringBuffer中的指定范围内的字符。

StringBuffer sb = new StringBuffer("Hello, world!");
sb.delete(5, 7); // 结果为 "Hello world!"

replace():将StringBuffer中的指定范围内的字符替换为给定字符串。

StringBuffer sb = new StringBuffer("Hello, world!");
sb.replace(0, 5, "Hi"); // 结果为 "Hi, world!"

reverse():反转StringBuffer中的字符顺序。

StringBuffer sb = new StringBuffer("Hello, world!");
sb.reverse(); // 结果为 "!dlrow ,olleH"

StringBuilder
StringBuilder的方法与StringBuffer相同,因为它们都继承自AbstractStringBuilder。这里不再重复列举方法,可以直接参考StringBuffer的方法。

StringJoiner
StringJoiner的常用方法如下:

add():将字符串添加到StringJoiner中,并用分隔符分隔。

StringJoiner joiner = new StringJoiner(", ");
joiner.add("apple");
joiner.add("banana");
joiner.add("cherry");

merge():将另一个StringJoiner的内容合并到当前StringJoiner中。合并后,两个StringJoiner之间将使用当前StringJoiner的分隔符。

StringJoiner joiner1 = new StringJoiner(", ");
joiner1.add("apple");
joiner1.add("banana");

StringJoiner joiner2 = new StringJoiner(", ");
joiner2.add("cherry");
joiner2.add("orange");

joiner1.merge(joiner2); // 结果为 "apple, banana, cherry, orange"

setEmptyValue():设置当StringJoiner为空时返回的字符串。默认情况下,空的StringJoiner返回一个空字符串。

StringJoiner joiner = new StringJoiner(", ");
joiner.setEmptyValue("No elements"); // 当joiner为空时,toString()将返回"No elements"

toString():将StringJoiner的内容转成一个字符串。这个方法将StringJoiner中添加的所有字符串连接在一起,并使用分隔符、前缀和后缀(如果有的话)进行格式化。

StringJoiner joiner = new StringJoiner(", ", "[", "]");
joiner.add("apple");
joiner.add("banana");
joiner.add("cherry");

String result = joiner.toString(); // 结果为 "[apple, banana, cherry]"

在这个例子中,我们创建了一个StringJoiner,其中分隔符为逗号和空格(“, “),前缀为左方括号(”[”),后缀为右方括号(“]”)。我们添加了三个字符串(“apple”、“banana"和"cherry”),然后使用toString()方法将StringJoiner的内容转换为一个格式化的字符串。最后,结果为:“[apple, banana, cherry]”。


五、String一些常用方法

Java 中的 String 类提供了许多有用的方法来处理字符串。以下是一些常用方法:

  1. length():返回字符串的长度。
  2. charAt(int index):返回字符串中指定索引处的字符。
  3. substring(int beginIndex):返回从 beginIndex 到字符串末尾的子字符串。
  4. substring(int beginIndex,int endIndex):返回从 beginIndex(包含)到 endIndex(不包含)的子字符串。
  5. concat(String str):将指定的字符串连接到此字符串的末尾。
  6. indexOf(String str):返回指定子字符串在此字符串中第一次出现的索引,如果没有找到,则返回 -1。
  7. lastIndexOf(String str):返回指定子字符串在此字符串中最后一次出现的索引,如果没有找到,则返回 -1。
  8. startsWith(String prefix):检查字符串是否以指定的前缀开头。
  9. endsWith(String suffix):检查字符串是否以指定的后缀结尾。
  10. replace(char oldChar, char newChar):将字符串中所有的 oldChar 替换为 newChar。
  11. replace(CharSequence target, CharSequence replacement):将字符串中所有的target 子序列替换为 replacement。
  12. toLowerCase():将字符串中的所有字符转换为小写。
  13. toUpperCase():将字符串中的所有字符转换为大写。
  14. trim():删除字符串开头和结尾的空白字符。
  15. equals(Object obj):比较字符串和指定对象是否相等。
  16. equalsIgnoreCase(Stringanothe rString):比较字符串和指定字符串是否相等,忽略大小写。
  17. compareTo(String anotherString):按照字典顺序比较两个字符串。

六、一些练习

键盘录入一个字符串,打乱字符串的内容。

import java.util.Scanner;
import java.util.Random;

public class ShuffleString {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个字符串:");
        String input = scanner.nextLine();
        scanner.close();

        String shuffledString = shuffle(input);
        System.out.println("打乱后的字符串为:");
        System.out.println(shuffledString);
    }

    private static String shuffle(String input) {
        char[] chars = input.toCharArray();
        Random random = new Random();

        for (int i = chars.length - 1; i > 0; i--) {
            int randomIndex = random.nextInt(i + 1);
            char temp = chars[i];
            chars[i] = chars[randomIndex];
            chars[randomIndex] = temp;
        }

        return new String(chars);
    }
}

程序接受键盘输入的字符串,然后使用 shuffle 方法打乱字符串的内容。shuffle 方法首先将字符串转换为字符数组,然后使用 Fisher-Yates(也称为 Knuth)洗牌算法随机交换字符。最后,通过将字符数组转换回字符串得到打乱的字符串。

洗牌算法(Shuffling Algorithm)是一种用于将有限序列(如数组、列表等)中的元素随机重新排列的算法。在计算机科学中,洗牌算法通常用于模拟随机事件,如模拟纸牌游戏中的洗牌过程。最著名的洗牌算法是 Fisher-Yates 洗牌算法,也称为 Knuth 洗牌算法。

Fisher-Yates 洗牌算法的基本思路是从后向前遍历数组,对于每个元素,随机选择一个之前的元素(包括当前元素)与之交换。以下是 Fisher-Yates 洗牌算法的简化步骤:

  1. 初始化一个索引变量 i 为数组的最后一个元素的索引。
  2. 当 i 大于 0 时,执行以下操作:
    a. 生成一个介于 0(包括)和 i(包括)之间的随机整数 randomIndex。
    b. 交换数组中索引为 i 和 randomIndex 的元素。
    c. 将 i 减 1。
  3. 遍历结束后,数组中的元素被随机打乱。
    这种洗牌算法的优点是其随机性能保证,因为每个元素都有相同的概率被放置在任何位置。此外,Fisher-Yates 算法的时间复杂度为 O(n),空间复杂度为 O(1),因此效率很高。

请定义一个方法用于判断一个字符串是否是对称的字符串,并在主方法中测试方法。例如:“abcba”、"上海自来水来自海上"均为对称字符串。

public class Test {
    public static void main(String[] args) {
        String str = "上海自来水来自海上";
        System.out.println(isSym(str));
    }

    public static boolean isSym(String str) {
        // 为了程序的健壮,如果传递的是空值,返回false
        if (str == null) {
            return false;
        }
        // 转换为StringBuilder
        StringBuilder sb = new StringBuilder(str);
        // 反转,再转成String
        String reStr = sb.reverse().toString();
        // 比较与原字符串是否相等
        // 相等返回true,不相等返回false,正好与equals的返回值一致,直接返回即可。
        return reStr.equals(str);
    }
}

定义方法,返回值类型为boolean,参数列表为String类型的一个参数,将字符串转换为StringBuilder类型,调用StringBuilder的reverse()方法将字符串反转,将反转后的字符串再转回String类型,并与原字符串比较,如果相等,返回true,否则返回false,在主方法中,定义一个字符串,调用方法测试结果。

现有如下文本:“Java语言是面向对象的,Java语言是健壮的,Java语言是安全的,Java是高性能的,Java语言是跨平台的”。请编写程序,统计该文本中"Java"一词出现的次数。

public class Test {
    public static void main(String[] args) {
        String str = "Java语言是面向对象的,Java语言是健壮的,Java语言是安全的,Java是高性能的,Java语言是跨平台的";
        String tar = "Java";
        // 调用方法并输出
        System.out.println(search(str, tar));

    }
    // 返回值int表示次数,参数列表str表示在哪个字符串中查找,tar表示要查找的目标子串
    public static int search(String str, String tar) {
        // 定义统计变量表示次数
        int count = 0;
        // 定义索引变量,表示每次找到子串出现的索引
        int index = -1;
        // 定义循环,判断条件为在字符串中找到了目标子串
        while ((index = str.indexOf(tar)) != -1) { // 将找到的索引赋值给变量并判断
            // 次数累加
            count++;
            // 把查找过的部分剪切掉,从找到的索引+子串长度的位置开始截取。
            str = str.substring(index + tar.length());
        }
        return count;
    }
}
public class Test {
    public static void main(String[] args) {
        String str = "Java语言是面向对象的,Java语言是健壮的,Java语言是安全的,Java是高性能的,Java语言是跨平台的";
        String tar = "Java";
        // 调用方法并输出
        System.out.println(search(str, tar));

    }

    // 替换之后求长度差
    public static int search(String str, String tar) {
        String newStr = str.replace(tar, "");
        int count = (str.length() - newStr.length()) / tar.length();
        return count;

    }
}

定义方法,返回值int表示次数,参数列表两个字符串,第一个表示在哪个字符串中查找,第二个表示要查找的目标子串,定义统计变量表示次数。定义索引变量,表示每次找到子串出现的索引。定义循环,判断条件为在字符串中找到了目标子串,使用indexOf实现。如果找到的索引不是-1,在循环中,统计变量累加。把查找过的部分剪切掉,从找到的索引+子串长度的位置开始截取,使用substring实现。将统计变量返回,在主方法中,定义字符串表示题目中的文本,定义字符串表示要查找的子串,调用方法,获取结果。


七、扩展

可以用作键的特性意味着 String 类的对象可以用作数据结构(如哈希表、哈希集等)的键。这得益于 String 类重写了 hashCode() 和 equals() 方法,以便于根据字符串的内容生成唯一的哈希值和判断字符串是否相等。

举个例子,假设您要创建一个 HashMap,用于存储每个国家的首都。在这种情况下,可以将国家名称(字符串)作为键,首都(字符串)作为值。示例代码如下:

import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<String, String> capitals = new HashMap<>();

        capitals.put("China", "Beijing");
        capitals.put("Japan", "Tokyo");
        capitals.put("USA", "Washington D.C.");

        System.out.println("China's capital: " + capitals.get("China"));
        System.out.println("Japan's capital: " + capitals.get("Japan"));
        System.out.println("USA's capital: " + capitals.get("USA"));
    }
}

在上述代码中,capitals 是一个 HashMap,将字符串键(国家名)映射到字符串值(首都)。当添加或检索元素时,HashMap 会使用键的 hashCode() 方法生成哈希值,并使用 equals() 方法来确定两个键是否相等。因为 String 类已经实现了这些方法,所以能够正确地在哈希表中存储和检索数据。

需要注意的是,字符串的不可变性有助于其作为键的特性。因为字符串内容不会更改,所以它们的哈希值在创建后也不会更改。这有助于提高查找性能,避免因为哈希值变化导致的数据丢失。

JAVA9:
在 Java 9 之后,String 类的内部实现发生了变化。为了提高空间和时间效率,Java 9 引入了 COMPACT_STRINGS 优化。在此优化下,String 类使用字节数组(byte[])来存储字符数据,同时使用一个编码标记(coder)来指示字符串的编码格式。

coder 是一个字节变量,用于表示字符串中字符的编码。Java 9 中的 String 类支持两种编码:ISO-8859-1(单字节编码)和UTF-16(双字节编码)。如果字符串仅包含 ISO-8859-1 编码的字符,它将使用单字节编码存储,从而节省空间。否则,将使用 UTF-16 编码存储。

具体地说,coder 的值可以是:

StringLatin1:表示字符串使用 ISO-8859-1(Latin-1)编码。在这种情况下,每个字符占用一个字节。这种编码包含了大多数西欧语言字符,如英语、法语、德语等。
StringUTF16:表示字符串使用 UTF-16 编码。在这种情况下,每个字符通常占用两个字节(对于某些辅助字符,可能需要四个字节)。UTF-16 编码可以表示大多数现代语言的字符,包括亚洲语言和其他非拉丁语言。
通过使用 coder 和字节数组,Java 9 的 String 类可以在保持字符串处理速度的同时,减少内存占用。这对于那些使用大量字符串的应用程序(例如文本处理或网络应用)来说是一个很大的优势。

让我们来看一个简单的例子来说明 Java 9 之后的 String 类如何使用字节数组和编码标记来存储字符串。

假设我们有以下两个字符串:

String englishText = "Hello, World!";
String chineseText = "你好,世界!";

对于 englishText,因为它仅包含 ISO-8859-1(Latin-1)编码的字符,所以它将使用单字节编码(StringLatin1)进行存储。在这种情况下,englishText 的字节数组将如下所示(十六进制表示):

48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21

同时,englishText 的 coder 将被设置为 StringLatin1,表示使用 ISO-8859-1 编码。

对于 chineseText,因为它包含中文字符,无法使用 ISO-8859-1 编码表示,所以它将使用双字节编码(StringUTF16)进行存储。在这种情况下,chineseText 的字节数组将如下所示(十六进制表示):

4F 60 59 7D FF 0C 4E 16 51 6B

同时,chineseText 的 coder 将被设置为 StringUTF16,表示使用 UTF-16 编码。

这个例子展示了如何在 Java 9 之后的 String 类中使用字节数组和编码标记来存储字符串。这种实现在减少内存占用的同时,保持了字符串处理的速度。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值