0X JavaSE-- Wrapper class 及与基本类型 String 类型的转换、大数类、String 相关类

Wrapper class 包装类

  • 什么是包装类?
    • 包装类是 Java 提供的一种机制,用于将基本数据类型封装为对象类型。
  • 为什么要使用包装类?
    • 基本数据类型 int 等不是对象,无法通过向上转型获取到 Object 提供的方法
    • 基本数据类型不支持与对象有关的特性,如:多态、反射、泛型
    • 基本数据类型无法直接存入集合中,因为集合元素必须为对象
    • 包装类对象可以为 null 值,这在集合/数据库/反序列化中可能很有用
      • DTO 中尽量使用包装类。由于基本类型不能表示 null 值,如果从 JSON 等数据源反序列化时遇到 null,尝试将其直接赋值给基本类型变量会导致空指针异常
    • 包装类提供了许多有用的方法
    • 大部分包装类提供了缓存机制,可以减少创建销毁对象的开销
    • 包装类提供了自动装箱/拆箱机制,使得基础数据类型与它们对应的包装类之间的转换更加简洁
      在这里插入图片描述
  • 包装类重写了 equals 方法,比较的是包装类的内容而不是地址

1. 包装类的比较

  • 包装类重写了 equals 方法,比较的是包装类的内容而不是地址
  • 但是,如果用 “==” 比较两个包装类,比较的是地址
  • 作为对比,String 类型由于字符串常量池的存在,equals 和 “==” 比出来的效果实际上是一样。

2. 缓存机制

包装类通过缓存一些常见的值来优化性能,减少对象的频繁创建和内存开销。

除 float、double 外的 6 个包装类,都提供了对象的缓存。
实现方式是在类初始化时提前创建好会频繁使用的包装类对象,当需要使用某个包装类的对象时,如果该对象包装的值在缓存的范围内,就返回缓存的对象,否则就创建新的对象并返回。

3. 装/拆箱

  • 装箱 : 基本类型 ——> 包装类型(或者叫对象类型,引用类型)
  • 拆箱 : 包装类型 ——> 基本类型

以下以 Integer 为例

3.1 手动装/拆箱

JDK5 之前,拆装箱均是手动完成的。

  • 手动装箱:JDK 9 以前,可以使用包装类的构造器/包装类调用静态 valueOf 方法完成;JDK 9 以后,只能通过包装类调用静态 valueOf 方法完成。
 public static void main(String[] args) {

        //手动装箱(基本类型包装/引用类型)

        // 第一种手动装箱,已于 JDK 9 之后废弃
        // Integer integer_0 = new Integer(5);
        
        // 第二种手动装箱,推荐使用
        Integer integer_1 = Integer.valueOf(5);
        }
  • ​​​​​​​手动拆箱:包装类调用静态 intValue 方法完成。

3.2 自动装/拆箱

JDK5 开始,提供了自动拆装箱的机制。(不需要手动调用构造器或者方法了)

  • 自动拆箱:实际上底层仍然调用了 valueOf 方法
  • 自动装箱:实际上底层仍然调用了 intValue 方法
  • 自动装/拆箱都是在编译阶段完成的。
Integer integerValue = 10;  // 自动装箱
int intValue = integerValue;  // 自动拆箱

//集合类(如ArrayList)只能存储包装类对象,自动装箱可以免去手动类型转换
List<Integer> integerList = new ArrayList<>();
integerList.add(1);  // 自动装箱

// 方法调用中的自动装箱:方法参数类型为包装类时,传入基本类型会自动装箱,方法内部使用时可以自动拆箱。
public static int sum(Integer a, Integer b) {
    return a + b;  // 自动拆箱
}

4. Integer 类型的转换

4.3.1 装箱 valueOf

// Integer 类中,valueOf方法的源码
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
  • IntegerCache

IntegerCache 是一个静态内部类,用于缓存一定范围内的 Integer 对象。该缓存的范围和缓存数组的初始化是在 IntegerCache 类中完成的。

  • 缓存范围检查

在 valueOf 方法中,首先检查传入的 int 值是否在缓存范围内( -128~127 )。如果在范围内,则直接返回缓存数组中的相应 Integer 对象。

  • 创建新对象

如果传入的 int 值不在缓存范围内,则使用 new Integer(i) 创建一个新的 Integer 对象。

5. String 类型的转换

在这里插入图片描述
String 类型可同时转为包装类、基本数据类型,下图只是 int 类型的转换,实质上八种基本类型都是类似的。

以下详细叙述

5.1 String ——> 7 种基本类型

  • 通过各个包装类自带的静态方法 parseXxx,在满足被转类型可以接收的情况下,可以把 String 类型转成 7 个基本数据类型(排除 char)
  • 显然,parseXxx 方法的返回值是基本数据类型。
        byte temp_byte =Byte.parseByte("11");
        short temp_short = Short.parseShort("141");
        int temp_int =Integer.parseInt("430");
        long temp_long =Long.parseLong("11211");
        float temp_float =Float.parseFloat("66.66F");
        double temp_double =Double.parseDouble("666.666");
        boolean temp_boolean=Boolean.parseBoolean("true");

5.2 String ——> char

String 类型转 char,只能调用 String 的静态方法实现

转换功能的方法 —— char[] toCharArray() : 将字符串转换成字符数组
获取功能的方法 —— char charAt(int index) : 获取指定索引位置的字符

       //定义一个字符串
        String string = "Avocado";
        //利用 toCharArray 方法将字符串转换为字符数组
        char[] charArray = string.toCharArray();

        System.out.println("已将字符串转为字符数组,下面从字符数组中读取头两个字符");
        System.out.println("第1个字符是:" + charArray[0]);
        System.out.println("第1个字符是:" + charArray[1]);

        System.out.println("----");
        
        // 利用 charAt 方法来直接获取字符串中的每一个字符元素
        char temp_char_0 = string.charAt(0);
        char temp_char_1 = string.charAt(1);

        System.out.println("已将字符数组中的字符单个提取出为 char 类型的变量,下面输出头两个变量");
        System.out.println("第1个字符是:" + temp_char_0);
        System.out.println("第2个字符是:" + temp_char_1);

在这里插入图片描述

5.3 8 种基本类型 ——> String

基本类型要转字符串那就太简单了,最常见的两种方式——

  • 基本类型数据直接与空字符串进行拼接
  • 调用 String 类的静态 valueOf 方法。
	// int 类型拼接空字符串转 String
	int int_temp = 10;
	String string_temp_1 = int_temp + "";
	// ... 省略其余七种,完全一致

	// 调用 String 类的静态 valueOf 方法转 String
	String string_temp_2 = String.valueOf(int_temp);
	// ... 省略其余七种,完全一致

5.4 String类 ——> 包装类

有两种方式,如下 :

  • 调用包装类的静态方法 parseXXX,但是必须确保包装类能接收。
  • 这种方法自 JDK 9 以后就不再支持。利用包装类的构造器,例如 : Integer integer_1 = new Integer(字符串类型变量);
	// 调用包装类的静态方法 parseInt
	String string_temp = "10"
	// 这里比较复杂
	// parseInt 返回值是 int 类型
	// 但是由于检测到所需类型是包装类 Integer,启用自动装箱机制将 int 返回值转化成 Integer
	Integer integer_temp = Integer.parseInt("10");
	// 默认就是返回 int,可以直接赋值
	int int_temp = Integer.parseInt("10");

	// ... 省略其余七种,完全一致

5.5 包装类 ——> String类

有三种方式,如下 :

  • 包装类型数据直接与空字符串进行拼接
  • 调用 String 的 静态 valueOf 方法
  • 调用包装类的 toString 方法
        //方式一:包装类型数据直接与空字符串进行拼接
        Integer integer_0 = 10;//自动装箱
        String string_0 = integer_0 + "";
        //方式二:调用 String 的 静态 valueOf 方法
        Integer integer_2 = 431;
        String string_2 = String.valueOf(integer_2) + " world";
        // 实际上是非必要,由于自动拆箱机制,一句 integer_2 + " world" 已经足够

        //方式三:
        Integer integer_1 = 20;
        String string_1 = Integer.toString(integer_1) + " hello";
        // 实际上是非必要,由于自动拆箱机制,一句 integer_1 + " world" 已经足够

6. 包装类的常用方法

      	static int compare(int x, int y); 比较大小
        static int max(int a, int b); 最大值
        static int min(int a, int b); 最小值
        static int parseInt(String s); 将字符串数字转换成数字类型。其它包装类也有这个方法:Double.parseDouble(String s)
        int compareTo(Integer,anotherInteger); 比较大小,只不过传入的形参是包装类
        boolean equals(Object obj); 包装类已经重写了equals()方法,比较的是内容而非地址
        String toString(); 包装类已经重写了toString()方法,输出的是包装类的内容
        int intValue(); 将包装类拆箱为基本数据类型
        static String toString(int i); 将基本数据类型转换成字符串
        static Integer valueOf(int i); 将基本数据类型转换成Integer
        static Integer valueOf(String s) 将字符串转换成Integer(这个字符串必须是数字字符串才行,不然出现NumberFormatException

大数类

包装类好处不再介绍,但需要知道的是,包装类并未解决基本类型遗留的问题:超过范围,例如 int 、Integer 最大值都是 2^31-1。

因此,对于 Integer 包装类,进一步引入 BigInteger;对于 Double 包装类,进一步引入 BigDecimal

  • 大数类的取值范围原则上是没有上限的,取决于你的计算机的内存
  • 使用这些大数类时,由于它们是不可变对象,因此进行任何算术运算都会返回一个新的实例,而不会改变原始对象的值。
  • 区别于包装类,大数类没有自动装/拆箱机制,因此两个对象之间不能直接做基本运算,用add、subtract、mutiply、divide 等方法替代。
  • 在进行除法或取余运算时,需要注意处理可能出现的除数为零的情况,以及在使用 divide 方法时指定舍入模式以避免ArithmeticException。

1. BigInteger

1. API – 构造器

BigInteger 与 String 有几分相似,然而它的初始化方式却没有 String 那么方便可以直接赋值,而是跟其他自定义的类一样,要调用构造器进行初始化

// 这个构造器通过一个表示整数的字符串来创建一个 BigInteger 对象。
// 字符串中的数字可以是正数、负数,也可以包含加号 + 或减号-作为前缀。
// 例如,new BigInteger("123456") 或 new BigInteger("-987654")。
public BigInteger(String val)// radix 参数指定进制,不指定的情况下默认是 10  
public BigInteger(String val, int radix)// 这个构造器根据给定的符号( signum,其中 -1 表示负数,0 表示零,1 表示正数)
// 和一个无符号字节数组(magnitude)来构造一个 BigInteger。
//字节数组的最高有效字节在数组的开头。例如,一个表示整数 123 的大端字节数组为{0, 0, 0, 123}。
public BigInteger(int signum, byte[] magnitude)

// 这个构造器从指定的字节数组的一部分创建一个BigInteger。val是字节数组,off是数组中开始复制的偏移量,len是要使用的字节数。
// 同样地,字节数组被解释为无符号的,并且最高有效字节在前。
public BigInteger(byte[] val, int off, int len)/** 
   这个构造器在处理与位宽相关的算法和协议(例如,在密码学中处理密钥或消息的填充)时特别有用,确保数据按照预期的位宽对齐和格式化。
   这个构造器创建一个具有指定比特长度的新 BigInteger,传入的参数是一个 BigInteger 对象
   如果 val 的二进制表示已经至少有 bitLength 位,那么构造器将直接返回 val,不做任何修改。
   如果 val 的二进制表示少于 bitLength 位,构造器会在 val 的二进制表示的最左边补足 0,直到达到至少 bitLength 位。这实际上不改变数值的大小,只是改变了数值的表示形式,使其在位级操作中能够与指定宽度的其他数值对齐。

假设有一个 Integer 对象 val 其值为 1024,其二进制表示为10000000000(11位)。
如果我们想要得到一个至少有16位的二进制表示的大整数,可以这样调用构造器:
结果result的二进制形式将会是0000100000000000,保持数值不变,但确保了至少有16位。
 */ 
BigInteger result = new BigInteger(val, 16);

public BigInteger(BigInteger val, int bitLength)

1.2 API – 函数

BigInteger 中都是实例方法,因此必须通过实例调用,返回的也是 BigInteger 对象

// 四种基本运算
BigInteger add / subtract / multiply / divide(BigInteger val);

double doubleValue()float floatValue()BigInteger 对象中的值以双精度等类型数返回返回。
long longValue()int intValue() 

BigInteger max / min(BigInteger val) 返回两个大整数的最大 / 小者
String toString() 将当前大整数转换成十进制的字符串形式

2. BigDecimal

2.1 API

构造器:

  BigDecimal(int)BigDecimal(long)    创建一个具有参数所指定 int/long 值的对象。
  BigDecimal(String)    创建一个具有参数所指定以字符串表示的数值的对象。
 //该构造方法比较特殊,存在潜在的精度丢失风险
  BigDecimal(double)   
//当传入的数值是 double 时,应该使用 BigDecimal.valueOf(double)
// 此外,BigDecimal.valueOf(xxx ) 是静态工厂类,永远优先于构造函数(摘自<<Effecitve java>>,)

 BigDecimal d1 = BigDecimal.valueOf(12.3)//结果是12.3 你预期的
 BigDecimal d2 = new BigDecimal(12.3) //结果是12.30000000000000071054...具体原因可查看底层源码

函数:

// 四种基本运算
BigDecimal add / subtract / multiply / divide(BigDecimal val);

toString()BigDecimal 对象的数值转换成字符串。

double doubleValue()float floatValue()BigDecimal 对象中的值以双精度等类型数返回返回。
long longValue()int intValue()    
// 尽管两个方法的作用都是比较大小,但推荐 BigDecimal 搭配 compareTo 使用
BigDecimal.compareTo(BigDecimal val)
BigDecimal.equals(BigDecimal val)

        BigDecimal decimal1 = new BigDecimal("200");
        BigDecimal decimal2 = new BigDecimal("200.0");
        System.out.println(decimal1.equals(decimal2));
        System.out.println(decimal1.compareTo(decimal2));

// out : false;0
// 可见 equals 方法同时关注了小数的位数,尽管二者值完全相同,但就是输出 false

3. Double 和 BigDecimal 的比较

3. 两种类型使用过程中都有可能出坑

  1. double 计算时容易出现不精确的问题

double 的小数部分容易出现使用二进制无法准确表示
如十进制的0.1,0.2,0.3 都不能准确表示成二进制;

  1. 对两个 double 进行" === " 比较容易出现不精确的问题
    在这里插入图片描述

  2. 两个 BigDecimal 作除法,如果结果是无限小数,会抛出异常:ArithmeticException,因此,除法时必须每次都指定位数,避免异常
    在这里插入图片描述

  3. BigDecimal 是不可变对象

任何针对 BigDecimal 对象的修改都会产生一个新对象;

BigDecimal newValue = BigDecimal.valueOf(1.2222).add(BigDecimal.valueOf(2.33333));

BigDecimal newValue = BigDecimal.valueOf(1.2222).setScale(2);

总之每次修改都要重新指向新对象,才能保证计算结果是对的。

  1. BigDecimal 没有 equals 方法,

2. 优缺点总结

  • double:
    • double 在计算过程中容易出现丢失精度问题
    • 使用方便,有包装类,可自动拆装箱,计算效率高
  • BigDecimal:
    • 精度准确,但做除法时要注意除不尽的异常
    • BigDecimal 是对象类型,也没有自动拆封箱机制,操作起来总是有些不顺手

2. 使用场景

  • 涉及到精准计算如金额,一定要使用 BigDecimal 或转成 long、int 计算、
  • 若不需要精准的,如一些统计值:(本身就没有精确值)用户平均价格,店铺评分,用户经纬度等本身就没有精准值一说的推荐使用double、float,写代码更方便,计算效率也高得多;
  • 如果 double、float 仅是用于传值,并不会有精度问题;但如果参与了计算就要小心了,要区分是不是需要精准值,如果需要精准值,需要转成 BigDecimal 计算以后再转成 double;
  • 约定在 DTO 定义金额时使用 BigDecimal 或整形值,是为了避免 double 参与金额计算的机会,避免出 bug;

实例:

	@ApiModel(value = "当前位置的信息")
	public class LocationInfo {

    @ApiModelProperty(value = "当前地址的经度", example = "121.471231")
    private Double longitude;

    @ApiModelProperty(value = "当前地址的维度", example = "31.231121")
    private Double latitude;

    // 如果只是整数时,可使用Integer
    @ApiModelProperty(value = "店铺平均消费范围下限", example = "12")
    private Double costFrom;

    // 如果只是整数时,可使用Integer
    @ApiModelProperty(value = "店铺平均消费范围内限", example = "22")
    private Double costTo;

    // 价格,这个就关键了,这个需要精确值,且常常用于订单购物车计算,因此要使用BigDecimal
    @ApiModelProperty(value = "当前价格", example = "12.22")
    private BigDecimal price;
}

通过以上代码可以看出,对于经纬度、平均消费额度等不需要精确到极致的数据,Double 已经完全够用了,强行用 Decimal 徒增功耗

2. MySQL 中如何选用这两种类型

  • 与 Java 不同的,MS 是用来持久化数据的,而 Java 中使用的数据一般更多的是过一下内存;
  • 数据库都要除了指定数据类型指外还需要指定精度,因此在 DB 中 Double 计算时精度的丢失比Java高得多;因为Java默认精确到15-16位了;
  • 更改数据类型的成本,MS 比 Java 代码要高得多;
  • 考虑到以上与 Java 中不同几点:
    • 与商业金融相关字段要使用 Decimal 来表示,如金额,费率等字段;
    • 参与各类计算如加,减,乘,除,sum,avg等等,也要使用Decimal;
    • 经纬度,可以使用 double 来表示,这个可参考 Java,只要保证精度范围即可;
    • 如果确实不确定使用什么 double 或 Decimal 哪种类型合适,那最好使用 Decimal,毕竟稳定,安全高于一切;
    • 阿里的编码规范中强调带小数的类型一律使用Decimal类型,也是有道理的,使用 Decimal 可以大大减少计算踩坑的概率

数字格式化

https://blog.csdn.net/wdd1324/article/details/70153896


String 类

  • Java 中没有内置的字符串类型,而是在 java.lang 包中提供了一个预定义的类 String,每个用双引号引起来的字符串都是 String 类的实例。
    • Java 默认导入 java.lang 包下的所有类,因此仅从使用的角度说,可以认为 String 也被 Java 预先定义了。
    • JDK 9 以前,String 底层是用 char 数组实现的
  • 总的来说 Java 中规定了 String 不属于基本数据类型,只是代表一个类,属于引用类型,因此 String 的默认值是 null。
    • 但 String 可以直接赋值,也可以 new 出实例。
      这是因为 Java 有字符串常量池机制
String str1 = "avocado";
String str2 = new String("avocado");

1. 字符串常量池机制

  • 字符串常量池位于 JVM 的

1.1 字符串常量池的设计思想:

字符串属于引用数据类型,如果没有字符串常量池机制,那么分配一个 String 类型就和其他的对象分配一样,耗费高昂的时间与空间代价。这样,作为最基础的数据类型,大量频繁的创建字符串,就会极大程度地影响程序的性能。

  • JVM 在实例化字符串常量的时候进行了一些优化:
    • 为字符串开辟一个字符串常量池,类似于缓存区。
    • 创建字符串常量时,首先查找字符串常量池是否存在该字符串。
    • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。
        String a = "a";
        String b = "a";

/* 由于字符串常量池的机制,尽管 String 是引用数据类型,== 仍会返回 true
 * == 用于判断引用所指向的实例是否为同一个。如果比较两个对象,当然无论如何也不会相等
 * 对于 String,由于字符串常量池的存在,a b两个引用指向的是同一个实例,因此自然相等
*/
        if (a == b && a.equals(b) )
          System.out.println("a != b");
        else
            System.out.println("a = b");

    }
  • 字符串常量池的实现基础
    • 实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享。
    • 运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收。

1.2 常量池机制何时生效?

  • 仅当使用双引号声明字符串对象时,字符串常量池机制生效,具体过程上述。
  • 使用 new 关键字创建的字符串对象会先从字符串常量池中找,如果没找到就创建一个,然后再在堆中创建字符串对象;如果找到了,就直接在堆中创建字符串对象。
// new String 时不生效,但仍可通过 intern 方法将创建的 String 加入到常量池       
        // 使用字面量赋值字符串
        String str1 = "hello";
        String str2 = "hello";
        
        // 使用 new 关键字
        String str3 = new String("hello");
        String str4 = new String("hello");
        
        // 手动调用 intern() 方法
        String str5 = str3.intern();
        String str6 = str4.intern();
        
        System.out.println("str1 == str2: " + (str1 == str2)); // true
        System.out.println("str3 == str4: " + (str3 == str4)); // false
        System.out.println("str1 == str3: " + (str1 == str3)); // false
        System.out.println("str1 == str5: " + (str1 == str5)); // true
        System.out.println("str5 == str6: " + (str5 == str6)); // true

1.3 new String 的值存储过程

        // 字符串字面量
        String str1 = "avocado";    
        // 通过 new 关键字创建的字符串
        String str2 = new String("avocado");
        
        // 比较引用
        System.out.println("str1 == str2: " + (str1 == str2)); // false
        // 比较内容
        System.out.println("str1.equals(str2): " + str1.equals(str2)); // true
  • 第一步,首先检查字符串常量池中是否存在 avocado。这与 字面量赋值创建字符串是完全一致的
    • 如果 “avocado” 不存在于字符串常量池中,首先会将这个字面量字符串 “avocado” 放入字符串常量池中。
    • 如果 “avocado” 存在于字符串常量池中,则进入下一步
    • 也就是说,即使通过 new 关键字创建字符串 avocado,但如果常量池中不存在 avocado,那么编译器依然会将字面量部分放入字符串常量池中 。
  • 第二步,堆内存中创建一个新的字符串对象,其内容为 “avocado”。这个新对象是一个完全独立的对象,与字符串常量池中的 “avocado” 不是同一个对象。
  • 第三步,str2 引用指向堆内存中的 avocado对象,而非常量池中的 hello 对象

最终,由于 str1 指向常量池中的 avocado,str2 指向堆内存中的 avocado 对象,因此 == 对比它们的引用会输出 false;
equals 对比他们的内容会输出 true。

	// 这段代码创建了两个对象,一个存储在常量池中,一个存储在常量池外堆中
     String str2 = new String("avocado");

在这里插入图片描述

1.5 字符串的比较

  • 如果需要比较两个字符串的内容,必须用 equals 以避免潜在的错误。
  • " == " 运算符比较的是两个对象的引用是否相同,即它们是否指向同一个内存地址。因此,如果如果两个字符串对象的引用不同,即使它们的内容相同,比较结果也会是 false。
  • equals 方法比较的是两个对象的内容是否相同,即字符串的字符序列是否一致。
  • 通过以下代码应当知道:尽量避免 new String 和 “==” ,尤其是后者。
		// 使用字符串字面量
        String str1 = "hello";
        String str2 = "hello";
        
        // 使用 new 关键字
        String str3 = new String("hello");
        String str4 = new String("hello");
        
        //str3 和 str4 是用 new 创建的,因此常量池不生效,二者是不同的对象,
        //因此尽管二者内容相同,但 “==” 返回的是 false
        System.out.println("str3 == str4: " + (str3 == str4)); // false
        // 而对于字面量赋值的 str1 和 str2 ,常量池机制生效,两者指向同一对象,“==”返回自然是 true。
        System.out.println("str1 == str2: " + (str1 == str2)); // true

2. API

String 是 Java 预先定义好的一个类,自然,其内提供了不少方法。


length() 用于返回字符串长度。

isEmpty() 用于判断字符串是否为空。

charAt() 用于返回指定索引处的字符。

valueOf() 用于将其他类型的数据转换为字符串。

str1.concat(str2) 拼接字符串
String.join("所需连接符",str1,str2)

// 返回该字符串对应的 hash 值,且该值极大概率是唯一的.
// 因此 String 很适合来作为 HashMap(后面会细讲)的键值。
int hashCode()

// 可以从指定区间内截取,也可以从指定起点截取其后所有
String substring(int beginIndex)
String subString(int beginIndex,int endIndex)

// indexOf 方法用于查找一个子字符串在原字符串中第一次出现的位置,并返回该位置的索引。
int indexOf(int ch)
int indexOf(String str)
// 从指定位置开始查找查找子字符串的位置
indexOf(int ch, int fromIndex)

// 删除字符串两端的空白字符。这里的“空白字符”指的是空格、制表符(\t)、换行符(\n)以及其他形式的空白字符。
String trim()

// 将字符串按照给定的正则表达式拆分为字符串数组((https://blog.csdn.net/m0_46671240/article/details/135484151?csdn_share_tail=%7B%22type%22:%22blog%22,%22rType%22:%22article%22,%22rId%22:%22135484151%22,%22source%22:%22m0_46671240%22%7D))
String [] split(regex)

//subString 方法实例
String str = "avocado";
String prefix = str.substring(0, 5);  // 提取前 5 个字符即 "avoca"
String suffix = str.substring(2);     // 提取从第 2 个字符开始的所有字符,即 "ocado"
// 需求:将 "  Hello   world  !" 提取出 H
String str = "   Hello   world!  ";
String trimmed = str.trim();                  // trim 去除字符串开头和结尾的空格
System.out.println(trimmed);
// split 按照指定的 regex 分割字符串,在这里 \\s 表示 所有空格符号,即遇到空格符号就分割
String[] words = trimmed.split("\\s+");   
String string = Arrays.toString(words);
System.out.println(string);    

String firstWord = words[0].substring(0, 1);  // subString 提取第一个单词的首字母
System.out.println(firstWord);                // 输出 "H"

在这里插入图片描述

3. 字符串拼接

3.1 + 和 StringBuilder.append 静态方法

        String avocado = "牛油果";
        String apple = "苹果";
        System.out.println(avocado + apple); // out: avocadoapple
        
// 最简单拼接字符串的方法就是对两个字符串使用 + 号,但 + 号实质是一个语法糖,编译器会对其进行重新解释
// JDK 8 下,分析字节码可知,编译器把 “+” 号操作符替换成了 StringBuilder 的 append() 方法
  		System.out.println((new StringBuilder(avocado)).append(apple).toString());

3.2 某些地方可能不得不手动使用 append

尽管编译器会将 + 重构成 append 方法,但有些时候最好手动使用

  • 在循环中进行字符串拼接时。
  • 处理大量字符串拼接操作时。
// 每次循环都会创建新的 StringBuilder 对象和临时字符串
String result = "";
for (int i = 0; i < 10; i++) {
    result += i;  
}

// 只创建一个 StringBuilder 对象然后传入,整段代码只使用了一个对象
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
    sb.append(i);  
}
String result = sb.toString();

3.4 append 静态方法源码

// 位于 StringBuilder 类中,重写了父类 AbstractStringBuilder 的 append() 方法
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

这 3 行代码其实没啥看的。我们来看父类 AbstractStringBuilder 的 append() 方法:

  • 判断拼接的字符串是不是 null,如果是,当做字符串“null”来处理。appendNull() 方法的源码如下:
  • 获取字符串的长度。
  • 由于字符串内部是用数组实现的,所以需要先判断拼接后的字符数组长度是否超过当前数组的长度,如果超过,先对数组进行扩容,然后把原有的值复制到新的数组中。
  • 将拼接的字符串 str 复制到目标数组 value 中。
  • 更新数组的长度 count。
public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

3.5 String.concat 实例方法

  • String 类的 str1.concat(str2) 方法,有点像 StringBuilder 类的 append() 方法。
  • 底层通过对字符数组扩容实现
  • 缺点是:
    • 如果传递的参数为 null,会抛出 `NullPointerException
    • 只能拼接两个字符串,
String fruit1 = "avocado";
String fruit2 = "apple";
System.out.println(fruit1.concat(fruit2));

3.6 String.join 静态方法

  • String.join 静态,即使参数之一为 null,也不会出现空指针异常
// String.join("String separator",str1,str2)

String fruit1 = null;
String fruit2 = "apple";
String fruit= String.join("", fruit1, fruit2);
System.out.println(fruit); // out: nullapple

由于所需连接符处没有指定,因此没有连接符,输出 nullapple。

3.7 StringUtils.join 静态方法

  • 使用该方法必须引入依赖
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>
  • StringUtils.join 底层使用的仍然是 StringBuilder。
  • StringUtils.join 可以确保不出现空指针异常
  • StringUtils.join 参数类型极其随意。既可以是各种类型的数组;又可以是各种类型的数据。弥补了 String.join 只能拼接 String/StringBuilder 的缺点。
// 传入对象类型数组
StringUtils.join(Object[] array, String separator);
// 其他基本类型一致
StringUtils.join(int[] array, String separator)
// 传入迭代器
StringUtils.join(Iterator<?> iterator, String separator);

// 传入各种类型的数据
StringUtils.join(int elements,...);
       String fruit1 = "avocado";
        String fruit2 = "apple";
        String[] fruits = {"avocado", "apple"};

// 可接受参数的形式,随意到了令人发指的地步
        String join1 = StringUtils.join(1,".avocado\n", 2,".apple");
        String join2 = StringUtils.join(fruit1, fruit2);
        String join3 = StringUtils.join(fruits, "");

        System.out.println(join1);
        System.out.println(join2);
        System.out.println(join3);

在这里插入图片描述

4. 字符串拆分

字符串拆分,要么是 String.split,要么是 Regex 包下的 Pattern 和 matcher 联合使用,其内有探究皮毛


3. StringBuffer 和 StringBuilder

  • 在字符串不经常发生变化的场景优先使用 String (代码更清晰简洁)。如常量的声明,少量的字符串操作(拼接,删除等)。
  • 在单线程情况下,如有大量的字符串操作情况,应使用 StringBuilder 来操作字符串。避免产生大量无用的中间对象,耗费空间且执行效率低下(新建对象、回收对象花费大量时间)。如 JSON 的封装等。
  • 在多线程情况下,如有大量的字符串操作情况,应使用 StringBuffer。如 HTTP 参数解析和封装等。

以下详细解释

  • 不可变性:每个 String 对象一旦创建,其内容就不能被改变。对一个 String 实例执行拼接操作(如使用 + 运算符)时,实际上是在创建一个新的 String 对象来存储拼接后的结果,而不是修改原有的字符串。这意味着每次拼接操作都会伴随至少一个新的字符串对象的创建。

相比之下,StringBuilder 或 StringBuffer 类通过在内部维护一个可变的字符数组来优化这一过程。它们在初始化时预分配一定的容量 ,并在必要时自动扩容,从而减少了内存分配的次数。在拼接字符串时,它们直接在现有字符数组上进行修改和追加,避免了频繁创建新对象的开销,大大提高了字符串操作的效率。

3.1 二者的区别

  • 两个类,除了类名不同,StringBuffer 类中方法带有 synchronized 关键字,其他内容基本上完全一样。
  • 尽管 StringBuilder 非线程安全,但如果要在多线程环境下修改字符串,仍可以使用 ThreadLocal 来避免多线程冲突。
    因此实际开发中,StringBuilder 的使用频率远高于 StringBuffer,甚至可以说,StringBuilder 完全取代了 StringBuffer。
  • 总结就是:
    • 字符串少改变的,用 String
    • 字符串多改变且不要求线程安全的,用 StringBuilder
    • 字符串多改变且要求线程安全的,用 StringBuffer

3.2 StringBuilder

new String("avocado") + new String("apple") 

// + 被编译器解释为以下代码:
new StringBuilder().append("avocado").append("apple").toString();
3.3.1 constructor
// 首先要知道,字符串对象底层是用数组存储字符实现的,因此容量就是指数组大小

// 无参数构造器:创建一个空的 StringBuilder 对象,初始容量通常为 16 个字符。
StringBuilder sb = new StringBuilder();


// 初始化一个指定内容的 StringBuilder 对象
// 其初始容量足以容纳提供的字符串加上额外的 16 个字符(默认情况)。
StringBuilder sb = new StringBuilder("Hello");

// 初始化一个指定容量、但内容为空的 StringBuilder 对象
StringBuilder sb = new StringBuilder(100);

// 初始化一个指定容量、指定内容的 StringBuilder 对象
// 确保底层数组的初始容量至少为指定的容量值。如果指定的容量小于字符串长度,则容量会自动调整为字符串长度加 16。
StringBuilder sb = new StringBuilder("Hello", 20);
3.3.2 API
int	capacity()	返回当前容量。
char charAt(int index)	在指定的索引处返回此序列中的char值。
void ensureCapacity(int minimumCapacity)	确保容量至少等于指定的最小值。
void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)	字符从此序列复制到目标字符数组dst中。
StringBuilder insert(int offset, String str)	将字符串插入此字符序列中。
int	length() 返回长度(字符计数)。
String subString(int start, int end)	截取指定的子串
// StringBuilder 提供的 API,其方法体都是直接在原对象上进行修改,而不是创建一个新对象
StringBuilder append(String str)	将指定的字符串附加到此字符序列中。
StringBuilder reverse()	反转字符串。
StringBuilder replace(int start, int end , String str)	将此序列子字符串中的字符替换为指定String中的字符。

StringBuilder stringBuilder = new StringBuilder("avocado");
StringBuilder reverseBuilder stringBuilder.reverse(); 
System.out.println(reverseBuilder); // Prints "odacova"
String substring = stringBuilder.substring(0, 2); // 截取0,1个字符
System.out.println(substring); // Prints "od"

// 以上代码说明两个事实:
// 1. stringBuilder 对象截取字符串,但截取到的是逆转后的字符串,说明逆转操作是在原字符串上进行的.
// 2. stringBuilder 和 reverseBuilder 都指向同一个被修改后的对象.
// 因此可以说指定一个新的引用 reverseBuilder 是没有意义的
// 3. 对于在原有对象上进行修改的方法,既可以指定新的引用指向其,也可以不指定.

3.4 StringBuffer

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值