每日一知识点 - Java 数组&字符串

😀 续上篇,今天来复习下Java数组&字符串相关知识,冲冲冲!!!

📝 Java基础

数组

深入浅出,掌握更多数组知识!

数组理论基础

数组是存放在连续内存空间上的相同类型数据的集合。

在Java中,ArrayList的内部就是使用数组实现的。

数组是一种线性表结构,线性表结构,顾名思义就是,将存储的数据排成一条线一样的结构,存储的每个数据最多只有前后两个方向。

在这里插入图片描述

💡
在Java中,“索引”和“下标”通常用来指代同一个概念,即数组、列表或字符串等数据结构中的位置或位置编号。

数组是一个对象,它包含了一组固定数量的元素,并且这些元素的类型是相同的。数组会按照索引的方式将元素放在指定的位置上,意味着我们可以通过索引来访问这些元素。在 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]
    
💡
因为数组的索引是从 0 开始的,所以最大索引为 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 是二维数组。

多维数组初始化

  1. 静态初始化(整体赋值)

数组静态初始化时,必须和数组的声明写在一起。

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 的三维数组,三维数组中有两个二维数组,
每个二维数组中有两个一维数组,每一个一维数组中具体元素也都初始化了。
*/
  1. 动态初始化
数据类型[][] 数组名称= 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 类型的数据在 JVM 中是占用两个字节的,并且使用的是 UTF-8 编码,其值范围在 '\u0000'(0)和 '\uffff'(65,535)(包含)之间。使用 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("李华");

这行代码创建了两个对象。

  1. 第一个对象:字符串常量池中的对象。当编译器遇到字符串字面量 "李华" 时,它会在字符串常量池中查找是否已经存在这个字符串。

    如果存在,就不会在字符串常量池中创建"李华"这个对象了,直接在堆中创建一个"李华"的字符串对象,然后将堆中这个"李华"的对象地址返回赋值。

    如果不存在,就会在常量池中创建一个新的字符串对象 "李华"

  2. 第二个对象:堆中的对象new String("李华") 这段代码会在堆内存中创建一个新的 String 对象。这个新的 String 对象是通过调用字符串常量池中的 "李华" 字符串字面量来初始化的。

//这行代码创建了几个对象?
String s2 = "李明";

这行代码创建了一个对象。

1、字符串常量池中的对象:编译器遇到字符串字面量 "李明" 时,会在字符串常量池中查找是否已经存在这个字符串。如果不存在,它会在常量池中创建一个新的字符串对象 "李明"

这行代码不会在堆内存中创建新的对象,而是直接使用字符串常量池中的对象。因此,只有一个对象被创建,并且该对象存储在字符串常量池中。

💡
new 的方式始终会创建一个对象,不管字符串的内容是否已经存在,而双引号的方式会重复利用字符串常量池中已经存在的对象。

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);
    }

    // ...
}

StringBufferStringBuilder 都是可变的,可以通过调用方法来修改其内容,而不是创建新的对象。

区别:

  • 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):获取从 beginIndexendIndex 之间的子字符串。

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());
        }
    }
}

PatternMatcher 类提供了更高级的字符串操作功能,包括字符串拆分。它们允许使用复杂的正则表达式。

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

📎 参考文章

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天马行空的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值