在API中是这样描述:
String 类代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。
字符串是常量;它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。
定义:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence{
...
}
成员变量:
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
*
* Object Serialization Specification, Section 6.2, "Stream Elements"
*/
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
从源码看出String底层使用一个字符数组来维护的。通过成员变量可以知道 String类 的值是 final 类型的,不能被改变的,所以只要一个值改变就会生成一个新的 String 类型对象,存储 String 数据也不一定从数组的第0个元素开始的,而是从 offset 所指的元素开始。
构造方法:
//初始化一个新创建的 String 对象,使其表示一个空字符序列。
public String() {
}
//初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
public String(String original) {
}
//分配一个新的 String,使其表示字符数组参数中当前包含的字符序列。
public String(char value[]) {
}
//分配一个新的 String,它包含取自字符数组参数一个子数组的字符。
public String(char value[], int offset, int count) {
}
//分配一个新的 String,它包含 Unicode 代码点数组参数一个子数组的字符。
public String(int[] codePoints, int offset, int count) {
}
@Deprecated
public String(byte ascii[], int hibyte, int offset, int count) {
}
@Deprecated
public String(byte ascii[], int hibyte) {
}
//通过使用指定的字符集解码指定的 byte 子数组,构造一个新的 String。
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
}
//通过使用指定的 charset 解码指定的 byte 子数组,构造一个新的 String。
public String(byte bytes[], int offset, int length, Charset charset) {
}
//通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。
public String(byte bytes[], String charsetName)
throws UnsupportedEncodingException {
}
//通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。
public String(byte bytes[], Charset charset) {
}
//通过使用平台的默认字符集解码指定的 byte 子数组,构造一个新的 String。
public String(byte bytes[], int offset, int length) {
}
//通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
public String(byte bytes[]) {
}
//分配一个新的字符串,它包含字符串缓冲区参数中当前包含的字符序列。
public String(StringBuffer buffer) {
}
//分配一个新的字符串,它包含字符串生成器参数中当前包含的字符序列。
public String(StringBuilder builder) {
}
/*
* Package private constructor which shares value array for speed.
* this constructor is always expected to be called with share==true.
* a separate constructor is needed because we already have a public
* String(char[]) constructor that makes a copy of the given char[].
*/
String(char[] value, boolean share) {
}
直接赋值方式创建对象是在方法区的常量池
//直接赋值的方式
String str = "hello";
直接赋值方式在编译阶段就会在内存中创建, 在编译期,JVM会去常量池来查找是否存在“hello”,如果不存在,就在常量池中开辟一个空间来存储“hello”;如果存在,就不用新开辟空间。然后在栈内存中开辟一个名字为str的空间,来存储“hello”在常量池中的地址值。
通过构造方法创建字符串对象是在堆内存
//实例化的方式
String str = new String("hello");
构造方法创建方式在编译阶段JVM先去常量池中查找是否存在“abc”,如果过不存在,则在常量池中开辟一个空间存储“abc”。在运行时期通过 String 类的构造器在堆内存中 new 了一个空间,然后将 String 常量池中的“abc”复制一份存放到该堆空间中,在栈中开辟名字为 str 的空间,存放堆中 new 出来的这个 String 对象的地址值。
测试示例:
public class TestString {
public static void main(String[] args) {
String str1 = "Lance";
String str2 = new String("Lance");
//引用传递,str3直接指向str2的堆内存地址
String str3 = str2;
String str4 = "Lance";
/*
* ==运算符说明:
* 1.基本数据类型:比较的是基本数据类型的值是否相同
* 2.引用数据类型:比较的是引用数据类型的地址值是否相同
* 所以在这里的话:String类对象使用 == 比较,比较的是地址,而不是内容
*/
System.out.println(str1 == str2);//false
System.out.println(str1 == str3);//false
System.out.println(str3 == str2);//true
System.out.println(str1 == str4);//true
}
}
在字符串中,如果采用直接赋值的方式(String str="Hello"
)进行对象的实例化,则会将匿名对象“Hello”放入对象池,每当下一次对不同的对象进行直接赋值的时候会直接利用池中原有的匿名对象,这样,所有直接赋值的 String 对象,如果利用相同的“Hello”,则String对象 == 返回true。
对象手工入池:
public class TestString {
public static void main(String args[]){
String str1 = "Lance";
//对匿名对象"hello"进行手工入池操作
String str = new String("Lance").intern();
System.out.println(str == str1);//true
}
}
两种实例化方式的区别分析:
String str = "hello";
):只开辟一块堆内存空间,并且会自动入池,不会产生垃圾。String str = new String("hello");
):会开辟两块堆内存空间,其中一块堆内存会变成垃圾被系统回收,而且不能够自动入池,需要通过 public String intern();
方法进行手工入池。避免空指针:
首先了解: == 和 equals() 比较字符串的区别
示例:
String str = null;
//此时会出现空指针异常
if(str.equals("hello")) {
...
}
//此时equals会处理 null 值,可以避免空指针异常
if("hello".equals(str)) {
...
}
使用分析:
String类被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。例如:
String str = “hello";
str = str + "world“;
String 类对象一旦声明则不可以改变;而改变的只是地址,原来的字符串还是存在内存中的,所以产生了垃圾,当上文 str 指向了一个 String 对象(内容为“hello”),然后对 str 进行“+”操作,str原来指向的对象并没有变,而是 str 又指向了另外一个对象(“hello world”),原来的对象还在内存中。由此也可以看出,频繁的对String对象进行修改,会造成很大的内存开销。此时应该用 StringBuffer 或 StringBuilder 来代替 String。而 new String() 更加不适合,因为每一次创建对象都会调用构造器在堆中产生新的对象,性能低下且内存更加浪费。
String 对象都是只读的,所以多线程并发访问也不会有任何问题,由于不可变,用来存储数据也是极为安全的。
//比较字符串的内容是否相同
boolean equals(Object obj);
//比较字符串的内容是否相同,忽略大小写
boolean equalsIgnoreCase(String str);
//判断字符串对象是否以指定的str开头
boolean startsWith(String str);
//判断字符串对象是否以指定的str结尾
boolean endsWith(String str);
//获取字符串的长度,其实也就是字符个数
int length();
//获取指定索引处的字符
char charAt(int index);
//获取str在字符串对象中第一次出现的索引
int indexOf(String str);
//从start开始截取字符串
String substring(int start);
//从start开始,到end结束截取字符串。包括start,不包括end
String substring(int start,int end);
//把字符串转换为字符数组
char[] toCharArray();
//把字符串转换为小写字符串
String toLowerCase();
//把字符串转换为大写字符串
String toUpperCase();
//去除字符串两端空格:
String trim();
//按照指定符号分割字符串
String[] split(String str);
查看Jdk String源码,有这么一段描述:
Strings are constant; their values cannot be changed after they
are created. String buffers support mutable strings.
大概意思就是:String 是个常量,从一出生就注定不可变。StringBuffer支持可变字符串。
我想大家应该就知道为什么 String 不可变了,String 类被 final 修饰,官方注释说明创建后不能被改变,但是为什么 String 要使用 final 修饰呢?
经典面试题:
public class TestString {
public static void main(String[] args) {
String a = "abc";
String b = "abc";
String c = new String("abc");
System.out.println(a == b); //true
System.out.println(a.equals(b)); //true
System.out.println(a == c); //false
System.out.println(a.equals(c)); //true
}
}
分析:
因为String太过常用,JAVA 类库的设计者在实现时做了个小小的变化,即采用了享元模式,每当生成一个新内容的字符串时,他们都被添加到一个共享池中,当第二次再次生成同样内容的字符串实例时,
就共享此对象,而不是创建一个新对象,但是这样的做法仅仅适合于通过=符号进行的初始化。
需要说明一点的是,在 object 中,equals() 是用来比较内存地址的,但是 String 重写了 equals() 方法,用来比较内容的,即使是不同地址,只要内容一致,也会返回 true,这也就是为什么 a.equals© 返回 true 的原因了。
String不可变的好处
字符串常量池概述
Class文件中存储所有常量(包括字符串)的 table。这是 Class 文件中的内容,还不是运行时的内容,不要理解它是个池子,其实就是 Class 文件中的字节码指令。
JVM 内存中方法区的一部分,这是运行时的内容。这部分内容(绝大部分)是随着 JVM 运行时候,从常量池转化而来,每个 Class 对应一个运行时常量池,这里说绝大部分是因为:除了 Class 中常量池内容,还可能包括动态生成并加入这里的内容
这部分也在方法区中,但与 Runtime Constant Pool 不是一个概念, String Pool 是 JVM 实例全局共享的,全局只有一个。JVM 规范要求进入这里的 String 实例叫“被驻留的interned string”,各个 JVM 可以有不同的实现,HotSpot 是设置了一个哈希表 StringTable 来引用堆中的字符串实例,被引用就是被驻留。
享元模式
其实字符串常量池这个问题涉及到一个设计模式,叫“享元模式”,顾名思义——共享元素模式。也就是说:一个系统中如果有多处用到了相同的一个元素,那么我们应该只存储一份此元素,而让所有地方都引用这一个元素。Java 中 String 部分就是根据享元模式设计的,而那个存储元素的地方就叫做“字符串常量池——String Pool”
详细分析
举例:
int x = 10;
String y = "hello";
首先,10和"hello"会在经过javac(或者其他编译器)编译过后变为 Class 文件中 constant_pool table 的内容。
当我们的程序运行时,也就是说 JVM 运行时,每个 Class constant_pool table 中的内容会被加载到 JVM 内存中的方法区中各自 Class 的 Runtime Constant Pool。
一个没有被 String Pool 包含的 Runtime Constant Pool 中的字符串(这里是"hello")会被加入到 String Pool 中(HosSpot 使用hashtable 引用方式),步骤如下:
一、在 Java Heap 中根据"hello"字面量 create 一个字符串对象。
二、将字面量"hello"与字符串对象的引用在 hashtable 中关联起来,键 - 值 形式是:“hello” = 对象的引用地址。
另外,当一个新的字符串出现在 Runtime Constant Pool 中时怎么判断需不需要在 Java Heap 中创建新对象呢?
策略:先根据 equals 来比较 Runtime Constant Pool 中的这个字符串是否和 String Pool 中某一个是相等的(也就是找是否已经存在),如果有那么就不创建,直接使用其引用;反之,进行上面第3个步骤。如此,就实现了享元模式,提高的内存利用效率。
举例:
使用String s = new String("hello");
会创建几个对象?
答:会创建2个对象
首先,出现了字面量"hello",那么去 String Pool 中查找是否有相同字符串存在,因为程序就这一行代码所以肯定没有,那么就在 Java Heap 中用字面量"hello"首先创建1个String对象。
接着,new String("hello")
,关键字 new 又在 Java Heap 中创建了1个对象,然后调用接收 String 参数的构造器进行初始化。最终 s 的引用是这个 String 对象。
如有不正,恳请指出;持续更新中…