Java 源码解读之 String 类

String 类

定义

package java.lang; 

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
.....
}

String 类是 java lang 包下的一个类

被 final 修饰的一个常量类,不能被任何类继承,被创建后,该对象的字符序列是不可变的,包含该类后续的所有方法都不能修改该对象,直至该对象被销毁,该类的一些方法,看似改变了字符串,其实内部是创建了一个新的字符串。

因为字符串对象是不可变的,所以它们可以被共享。

该类实现了 Serializable接口,这是一个序列化标志

String 字符串存储于常量池中,创建字符串时,先去字符串常量池查找是否包含该字符串,如果没有,就实例化该对象放入常量池。

基本属性

/**用来存储字符串  */
private final char value[];

/** 缓存字符串的哈希码 */
private int hash; // Default to 0

/** 实现序列化的标识 */
private static final long serialVersionUID = -6849794470754667710L;

String 类其实是一个 char[]

构造方法 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mRju3cql-1606400767128)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\image-20201125154153789.png)]

String s = new String(new char[]{'a','b','c'});

equals()

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String aString = (String)anObject;
            if (coder() == aString.coder()) {
                return isLatin1() ? StringLatin1.equals(value, aString.value)
                                  : StringUTF16.equals(value, aString.value);
            }
        }
        return false;
}

String 重写了 equals()

常用方法

# 获取字符串长度
lenth();
# 比较值是否相等
equals()
# 转换为 大写
toUpperCase()
# 转换为小写
toLowerCase()
# 去掉前后空格
trim();
# 将字符串放入常量池
intern()
# 获取字符的下标
indexOf()
# 将字符串连接到末尾
concat(String str)    
# 比较两个字符串某些位置上是否相等
boolean regionMatches(int toffset, String other, int ooffset, int len)    

常量池

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R66DiQYK-1606400767131)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\image-20201125160610002.png)]

  1. 程序计数器:也成为 PC 寄存器,线程私有。

    保存的是程序当前执行的指令的地址(也可以说保存下一条指令读的所在存储单元的地址),

    当 CPU 需要执行指令时,需要从程序计数器中得到当前执行的指令的所在存储单元的地址,然后根据地址获取指令,在得到指令后,程序计数器便自动加 1 ,或者根据转移指令指向下一条指令的地址,如此循环,直至执行完所有的指令。

  2. 虚拟机栈:基本数据类型、对象的引用都放在这里,线程私有。

  3. 本地方法栈:虚拟机栈是为执行 java 方法服务的,而本地方法栈是为执行本地方法(Native Method)服务的

  4. 方法区:存储每个类的信息(包括类的名称、方法、字段)、静态变量、常量、编译后的代码。

  5. 常量池:保存编译期间生成的字面量。

  6. 堆:用来存储对象本身

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HlKhZIPU-1606400767132)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\image-20201125163258413.png)]

jvm 常量池中维护了一个字符串缓冲区,用于存放运行时产生的各种字符串,缓冲区的字符串不重复。

创建 String 有几种方式:

  • 字面量

    字面量或者字符串拼接时,先在常量池中查找是否存在该字符串,如果存在就直接引用,避免重复创建,如果没有就创建该对象,并放入常量池。

  • new 的方式

    new关键字创建时,直接在堆中创建一个新对象,变量所引用的都是这个新对象的地址,但是如果通过new关键字创建的字符串内容在常量池中存在了,会将堆中创建的对象指向常量池的引用;但是反过来,如果通过new关键字创建的字符串对象在常量池中没有,那么通过new关键词创建的字符串对象是不会额外在常量池中维护的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j0lrSbGm-1606400767135)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\image-20201125172559535.png)]

在这里插入图片描述

知识点

intern() 方法

// 这是一个本地方法

public native String intern();

当调用intern方法时,如果池中已经包含一个与该String确定的字符串相同equals(Object)的字符串,则返回该字符串。否则,将此String对象添加到池中,并``返回此对象的引用。调用一个String对象的intern()方法,如果常量池中有该对象了,直接返回该字符串的引用(存在堆中就返回堆中,存在池中就返回池中),如果没有,则将该对象添加到池中,并返回池中的引用。

String str1 = "hello";//字面量 只会在常量池中创建对象
String str2 = str1.intern();
System.out.println(str1==str2);//true

String str3 = new String("world");//new 关键字只会在堆中创建对象
String str4 = str3.intern();
System.out.println(str3 == str4);//false

String str5 = str1 + str2;//变量拼接的字符串,会在常量池中和堆中都创建对象
String str6 = str5.intern();//这里由于池中已经有对象了,直接返回的是对象本身,也就是堆中的对象
System.out.println(str5 == str6);//true

String str7 = "hello1" + "world1";//常量拼接的字符串,只会在常量池中创建对象
String str8 = str7.intern();
System.out.println(str7 == str8);//true

String 真的不可变吗?

private final char value[];

value 被 final 修饰,只能保证引用不被改变,但是 value 所指向的堆中的数组,才是真实的数据,只要能够操作堆中的数组,依旧能改变数据。而且 value 是基本类型构成,那么一定是可变的,即使被声明为 private,我们也可以通过反射来改变。

String str = "vae";
//打印原字符串
System.out.println(str);//vae
//获取String类中的value字段
Field fieldStr = String.class.getDeclaredField("value");
//因为value是private声明的,这里修改其访问权限
fieldStr.setAccessible(true);
//获取str对象上的value属性的值
char[] value = (char[]) fieldStr.get(str);
//将第一个字符修改为 V(小写改大写)
value[0] = 'V';
//打印修改之后的字符串
System.out.println(str);//Vae

通过前后两次打印的结果,我们可以看到 String 被改变了,但是在代码里,几乎不会使用反射的机制去操作 String 字符串,所以,我们会认为 String 类型是不可变的。

String 类为什么要这样设计成不可变呢?我们可以从性能以及安全方面来考虑:

  • 安全
  • 引发安全问题,譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
  • 保证线程安全,在并发场景下,多个线程同时读写资源时,会引竞态条件,由于 String 是不可变的,不会引发线程的问题而保证了线程。
  • HashCode,当 String 被创建出来的时候,hashcode也会随之被缓存,hashcode的计算与value有关,若 String 可变,那么 hashcode 也会随之变化,针对于 Map、Set 等容器,他们的键值需要保证唯一性和一致性,因此,String 的不可变性使其比其他对象更适合当容器的键值。
  • 性能
  • 当字符串是不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。若字符串可变,字符串常量池失去意义,基于常量池的String.intern()方法也失效,每次创建新的 String 将在堆内开辟出新的空间,占据更多的内存。

比较 String 和 StringBuffer 的 equals 是否相等?

StringBuilder 不是线程安全的,效率高
StrngBuffer 是线程安全的,效率低

String a = “hello”;
StringBuffer s = new StringBuffer(“hello”);
a.equals(s) // false

String 转化为 基本数据类型

String s = “-1”;
Integer a = Integer.valueOf(s);
int b = Integer.parseInt(s);

Integer.valueOf (String s) 和 Integer.parseInt(String s) 的源码解读

// parseInt 方法有两个参数,第二个参数是进制,默认是 10进制
public static int parseInt(String s) throws NumberFormatException {
        return parseInt(s,10);
}

// 也可以直接调用该方法,指定要转换的进制
public static int parseInt(String s, int radix)
                throws NumberFormatException
    {
    ...
}        

// valueOf(s) 方法调用 parseInt(), 返回 Integer 类型
public static Integer valueOf(String s) throws NumberFormatException {
    return Integer.valueOf(parseInt(s, 10));
}

StringBuffer 和 StringBuilder 源码分析

相同的继承关系,都继承自 AbstractStringBuilder


// @since   1.0 
public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, Comparable<StringBuffer>, CharSequence
 {
     ...
 }


// @since    1.5
public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, Comparable<StringBuilder>, CharSequence
{
    ...
}

StringBuilder 调用 append() 方法时的源码分析

StringBuilder sb = new StringBuilder("1");

public StringBuilder() {
    super(16); // 调用无参构造方法,默认初始化容量是 16 
}

@HotSpotIntrinsicCandidate
public StringBuilder(String str) {
    super(str.length() + 16); // 如果没有指定,默认初始化容量是 16 + 字符串长度
    append(str); // 调用父类的 append 方法
}

// 调用父类的 append 方法
public AbstractStringBuilder append(String str) {
    if (str == null) {
        return appendNull();
    }
    int len = str.length();
    ensureCapacityInternal(count + len);
    putStringAt(count, str);
    count += len;
    return this;
}

// 判断是否需要扩容
private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    int oldCapacity = value.length >> coder;
    if (minimumCapacity - oldCapacity > 0) {
        value = Arrays.copyOf(value,
                              newCapacity(minimumCapacity) << coder);
    }
}

// 扩容大小是原来的2倍
private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = value.length >> coder;
    int newCapacity = (oldCapacity << 1) + 2;
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    int SAFE_BOUND = MAX_ARRAY_SIZE >> coder;
    return (newCapacity <= 0 || SAFE_BOUND - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}

StringBuffer 在调用 append() 时的源码分析

@Override
@HotSpotIntrinsicCandidate
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

两者都是调用 AbstractStringBuilder 的 append(),StringBuffer 最早是 JDK 1 就出现了,append 方法加了同步标识,StringBuilder 出现于 JDK 1.5,因此在多线程下, StringBuffer 安全性高于 StringBuilder, 而效率就相对低了。

微信公众号搜索【MAMBA 碎碎念】, 关注我的公众号!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值