String类:代表字符串,Java 程序中的所有字符串字面值(如 “abc” )都作
为此类的实例实现
String是一个final类,代表不可被继承
字符串是常量,在创建之后不能更改(不可变性)
String底层使用char value[]数组来保存多个字符
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** char[]数组存储字符 */
private final char value[];
/** hash值*/
private int hash; // Default to 0
...
}
String str1 = “abc” ;
String str2 = “hello” ;
在方法区中的常量池创建常量"abc",“helloe”,不可变,str1,str2分别指向该内存地址
方法区中的常量只能定义唯一,相同的字面量不会定义两个,例如:
String str1 = “abc” ;
String str2 = “abc” ;
会指向常量池中的同一个常量,此时,str1 ==s tr2
这也表现了String的不可变性,str1与str2都指向常量池同一个字符数组,若str1=“hello”,不会影响到str2
String底层使用char value[]数组来保存多个字符
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** char[]数组存储字符 */
private final char value[];
注意:被final修饰的char数组不可变,是value指向char[]地址不可变,而非char[]的值不可变
public static void demo3(){
final char[] value ={'a','b','c'};
value[0] = 'd' ;
value[1] = 'e' ;
value[2] = 'f' ;
System.out.println(value);
}
输出结果为:def
String不可变表现:
内部所有方法都是返回一个新的String对象(有些特殊情况例外)
例如:
public String replace(char oldChar, char newChar) {
//若oldChar==newChar,条件不成立,直接返回this
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value;
//遍历char数组,寻找oldChar的位置
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
//若i==len则表示没有oldChar该字符,返回this
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
读源码得知replace方法返回当前this对象的情况有两种
oldChar与newChar相同,返回this
若oldChar不存在char[]数组中,返回this
public static void demo3(){
String s1 = "abc" ;
String s2 = s1.replace('b', 'b');
String s3 = s1.replace('e', 'g');
System.out.println(s1==s2);
System.out.println(s1==s3);
}
执行结果都为true
字面量
String str = “hello” ;
指向常量池中的"hello"字符数组地址
new 创建
new 关键字创建的对象都存在堆中
String str = new String();
本质上this.value=new char[0]
String str = new String(char[] a);
本质上this.value = Arrays.copyOf(a,a.length)
String str = new String(String original);
本质上this.value=original.value
String str = new String(char[] c,int startIndext,int count);
本质上this.value = Arrays.copyOfRange(value, startIndext, startIndext+count);
由于String的不可变性,任何重新赋值操作都是重新创建对象
String str = “hello” ;
str = “helloworld” ;
重新赋值不是在字符数组"hello"上修改,而是在常量池总创建新的字符数组"helloworld",然后str指向"helloworld"的地址
String str ;
未初始化
未指向内存任何地址
String str = null ;
初始化为null ;
JVM会让这个str 变量指向一个不确定类型的空对象内存(即null内存, 在静态区域永久固定的null内存 ) ,假如输出System.out.println(str),则会输出null。
null是一个固定的不确定类型的内存,即可以看做是什么类型也不是,也没有继承Object,也没有toString()方法,所以这句代码不会默认调用str的toString()方法
在Java中,未初始化变量是不能使用的,只是声明一个变量,告诉JVM可能要用到这个变量,在内存中没有指向任何地址
String str ;
str.replace('a', 'b');
编译不能通过
初始化为null,变量在内存中指向null内存,所以可以使用
String str = null ;
str.replace('a', 'b');
编译可以,但 运行时又会报错空指针异常 ,因为null不属于任何一个对象
String str = “” ;
初始化为空字符串常量
相当于this.value = new char[0] ;
此时str已经指向方法区中的常量池的字符数组""内存地址,长度为0
String str = “hello” ;
指向常量池中"hello"字符数组常量的内存地址
String str1 = new String("")
String str2 = new String();
由源码可知,两者都是空字符串
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
new 关键字都在堆内存创建一个对象,str1和str2都是指向堆中,不是同一个内存地址,所以 str1==str2 结果为false
但str1和str2在堆中的对象都是指向常量池中的同一个字符数组常量地址
String str = new String(“hello”);
首先判断方法区中的常量池是否为"hello"字符数组,若没有,会先在常量池中创建"hello"字符数组
然后在堆内存创建一个对象,然后str指向堆中的对象的地址,而堆内存中的对象会指向常量池中的"hello"地址
比较:
String str1 = "hello" ;
String str2 = new String("hello") ;
str1 == str2 ;
所以比较结果为false
String str = “hello” + “world” ;
会在常量池创建三个字符数组,分别是"hello",“world”,“helloworld”,然后str指向"helloworld"字符数组的地址
准确来说:可能会在内存中创建最多3个对象,可能为0,1,2,3个
比较:
String str1 = "helloworld" ;
String str2 = "hello" + "world" ;
str1==str2 ;
由于str1与str2都是指向常量池中的"helloworld"字符数组的地址,所以比较结果为true
若说两者有所不同,那就是前者直接在常量池中生成"helloworld",后者则会先生成"hello",“world”,然后再生成"helloworld"(浪费资源)
所以使用String尽可能少使用拼接"+",或者使用StringBuffer、StringBuilder
String在对象中的存储
public class Pet{
private String name ;
private Integer age ;
...
}
public static void demo3(){
Pet p1 = new Pet("json", 12);
Pet p2 = new Pet("json", 12);
boolean b = p2.getName() == p1.getName();//true
}
比较结果为true,两者都是字面量的定义,都是指向常量池中的"json"的地址
若Pet p1 = new Pet(new String(“json”) , 12 ) ; 两者比较自然为false
所以:String尽量使用字面量定义,使用new关键字会额外在堆中创建对象
以下讨论的情况均为内存地址,而非内容,若调用equals()方法比较内容,结果全为true
public static void demo02(){
String s1 = "hello" ;
String s2 = "world" ;
final String s3 = "hello" ;
String s4 = "helloworld" ;
String s5 = "hello"+"world" ;
String s6 = s1 + "world" ;
String s7 = "hello" + s2 ;
String s8 = s1 + s2 ;
String s9 = s3 + "world" ; //使用final修饰也为常量,所以在常量池中
String s10 = (s1 + s2).intern() ;
System.out.println(s4 == s5); //true
System.out.println(s4 == s6); //false
System.out.println(s4 == s7); //false
System.out.println(s4 == s8); //false
System.out.println(s4 == s9); //true
System.out.println(s3 == s10); //true
}
结论:
intern()方法会将String变量从 指向堆空间中 转为 指向常量池中的字符数组常量
相同变量拼接
public static void demo3(){
String str1 = "hello" ;
String str2 = "world" ;
String str3 = str1 + str2 ;
String str4 = str1 + str2 ;
String str5 = str1 + "world" ;
String str6 = str1 + "world" ;
System.out.println(str3 == str4);//false
System.out.println(str5 == str6);//false
}
即使str3和str4是相同的变量拼接,或者str5与str6是相同的变量与常量拼接,(可以看做new)都会在堆中创建不同的对象,然后该对象指向常量池中的地址
所以两组比较都为false
String str1 = “hello” ;
str1 = str1 + “world” ;
该情况生成的对象自然也存在堆中
说明:
String concat(String str)
将指定字符串连接到此字符串的结尾。 等价于用"+"
String str = "hello" ;
str = str.concat("world");
源码:
//参数不能为null
public String concat(@NotNull() String str) {
//获取拼接的字符串长度
int otherLen = str.length();
//若长度为0,直接返回当前对象
if (otherLen == 0) {
return this;
}
//获取原字符串长度
int len = value.length;
//复制新的字符数组
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
//返回创建新的String
return new String(buf, true);
}
若拼接的字符串为null,则抛异常,若拼接的字符串为长度为0,直接返回当前对象,其他返回一个在堆中的新的String
String toLowerCase()
将 String 中的所有字符转换为小写
String toUpperCase()
将 String 中的所有字符转换为大写
String replace(char oldChar , char newChar)
用 newChar 替换此单个字符中出现的所有 oldChar
String replace(CharSequence target , CharSequence replacement)
使用replacement字符序列替换String中匹配的target字符序列
根据下标返回字符(字符串)
char charAt(int indext)
返回某索引处的字符return value[index]
String substring(int beginIndext)
返回一个新的字符串,从下标beginIndex开始截取到最后的一个子字符串
String substring(int beginIndex , int endIndex)
返回一个新的字符串,从下标beginIndex开始截取到endIndex(不包含)的一个子字符串 [ beginIndex , endIndex )
根据字符(字符串)返回下标
int indexOf(String str)
返回指定子字符串在此字符串中**第一次出现的下标
int indexOf(String str , int fromIndex)
返回指定子字符串在此字符串中第一次出现处的索引,从指定的fromIndex位置开始
int lastIndexOf(String str)
返回指定子字符串在此字符串中最后出现的下标
int lastIndexOf(String str , int fromIndex)
返回指定子字符串在此字符串最后出现处的索引,从指定的fromIndex索引开始反向搜索
boolean isEmpty()
判断是否是空字符串:return value.length == 0
boolean equals(Object obj)
比较字符串的内容是否相同
boolean equalsIgnoreCase(String anotherString)
与equals方法类似,忽略大小写
boolean endsWith(String suffix)
判断此字符串是否为指定的后缀结束
boolean startsWith(String prefix)
判断此字符串是否为指定的前缀开始
boolean startsWith(String prefix , int toffset)
判断此字符串从toffset位置开始是否为指定的前缀开始
boolean contains(CharSequence s)
判断此字符串是否包含指定的字符序列
int length()
返回字符串的长度
String trim()
返回新的字符串,忽略前面空格和尾部空格 (中间不忽略)
int compareTo(String str)
比较两个字符串的大小
String[] split(String regex)
根据指定的字符串拆分该字符串,并存在String[]数组中
String[] split(String regex , int limit)
根据指定的字符串拆分该字符串,并存在String[]数组中,最多不能超过limit个,若超过了,剩下的全部放到最后一个元素中
String转换为基本数据类型
Integer包装类中的parseInt方法,可以将由**"数字"字符组成的字符串**转换为整型,若不是"数字"的字符串或者为null,则会抛数字格式化异常NumberFormatException
类似地,使用java.lang包中的Byte、Short、Long、Float、Double等包装类调相应
的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型
Boolean源码
public static boolean parseBoolean(String s) {
return ((s != null) && s.equalsIgnoreCase("true"));
}
可以看出:只有String不为null和忽略大小写的"true"两者都成立返回true,其他都返回false
基本数据类型转换为String
调用String类相应的方法(鸡肋,了解即可)
可由参数的相应基本数据类型转换为String
转换实质:
number类型(int、long、double、float…)调用包装类的toString方法转换
char类型使用构造器new String(char[] value, boolean share)
boolean类型直接返回return b ? “true” : "false"
基本类型与String类型相加则自动转换为String
String s1 = "" + 1 ;
String s2 = "" + 2.0 ;
String s3 = "" + 'a' ;
String s4 = "" + true ;
字符数组转换为String
使用String构造器
new String(char[] value)
将char[]中全部字符转换为String
new String(char value , int offset , int length)
将char[]中从offset位置开始,一共length长度的字符转换为String
String转化为字符数组
char[] toCharArray()
将字符串中的全部字符存放在一个字符数组
void getChars(int srcBegin , int srcEnd , char[] dst , int dstBegin)
截取String中**[srcBegin , srcEnd )**区间的字符放进字符数组dst中,从dstBegin下标开始
public static void demo3(){
String s = "hello" ;
char[] c=new char[5];
s.getChars(1,3,c,1);
System.out.println(c);
}
输出结果为:空格el空格空格
StringBuffer为String的增强类,与String最大的不同为StringBuffer中的字符可变
StringBuffer底层使用父类AbstractStringBuilder中的char[]数组存储字符
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* 使用char数组存储字符
*/
char[] value;
//记录字符的长度
int count;
...
}
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
...
}
由源码可知存储字符数组char[] value没有final修饰,可以改变内存地址
且StringBuffer内部许多提供了许多方法可以改变char[]数组的内容,例如:appen方法
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
public AbstractStringBuilder append(String str) {
//判断拼接的字符串str是否为null
if (str == null)
return appendNull();
int len = str.length();
//判断是否扩容(类似集合)
ensureCapacityInternal(count + len);
//将拼接的str的所有字符加入到字符数组value里,在count位置开始添加
str.getChars(0, len, value, count);
count += len;
return this;
}
str.getChars(0, len, value, count);将拼接的str的所有字符加入到字符数组value里,在count位置开始添加,即拼接字符串str直接在value上修改,而非创建一个新的StringBuffer类
而且StringBuffer类中多数方法为链式方法,进行逻辑后,返回当前对象
public static void demo3(){
String s = "hello" ;
StringBuffer strb1 = new StringBuffer(s);
StringBuffer strb2 = strb1.append(1).
append(true).
append(new char[]{'a', 'b'}).
append("string");
System.out.println(strb1==strb2);//true
}
StringBuffer构造器初始化底层字符数组char[]的情况
new StringBuffer()
空参构造器,初始化字符数组char[]长度为16
new StringBuffer(int capacity)
给定容量的构造器,初始化字符数组char[]长度为capacity
new StringBuffer(String str)
给定字符串的构造器,初始化字符数组char[]长度为该字符串str长度+16
new StringBuffer(CharSequence seq)
给定字符序列的构造器,初始化字符数组char[]长度为该字序列串seq长度+16
源码分析:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
...
}
//StringBuffer类
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
//空参构造器,初始化字符数组char[]长度为16
public StringBuffer() {
super(16);
}
/**
*给定容量的构造器,初始化字符数组char[]长度为capacity
*/
public StringBuffer(int capacity) {
super(capacity);
}
/**
*给定字符串的构造器,初始化字符数组char[]长度为该字符串str长度+16
*/
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
/**
* 给定字符序列的构造器,初始化字符数组char[]长度为该字序列串seq长度+16
*/
public StringBuffer(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
...
}
若开发中频繁添加字符串,建议使用StringBuffer,且使用有参有参构造器给定容量大小,避免不断扩容,影响性能
StringBuffer转换为String
调用toString方法即可
String 转换为StringBuffer
使用构造器new StringBuffer(String str)即可
StringBuffer底层使用char[]数组存储字符,数组的长度是确定的,若拼接的字符超过数组的长度,则需要扩容
判断是否需要扩容
//minimumCapacity为新的字符的长度
private void ensureCapacityInternal(int minimumCapacity) {
// 若字符的长度>char[]数组的长度
if (minimumCapacity - value.length > 0) {
/*
1、调用newCapacity()方法计算扩容后char[]字符数组的长度
2、Arrays.copyOf方法进行数组复制
*/
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
计算扩容后的长度
private int newCapacity(int minCapacity) {
// 扩容后的容量首先为 原来的字符数组长度 *2 + 2
int newCapacity = (value.length << 1) + 2;
//若新的容量还比字符串长度小,直接让字符串长度等于扩容后的容量
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
/**
* MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 ,char[]数组规定的最大容量
* 条件说明:
* newCapacity <= 0 (左移溢出)
* MAX_ARRAY_SIZE - newCapacity < 0(扩容后的容量大于char[]数组规定的最大容量)
* 若扩容后的容量没有溢出 和 没有大于char[]数组规定的最大容量
* 返回当前newCapacity
* 若扩容后的容量溢出或超过最大容量则调用hugeCapacity方法进行新的判断
* 返回MAX_ARRAY_SIZE或者抛异常
*/
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
private int hugeCapacity(int minCapacity) {
//若字符串长度minCapacity大于Integer的最大值,则抛溢出异常
if (Integer.MAX_VALUE - minCapacity < 0) {
throw new OutOfMemoryError();
}
//字符串长度大于数组规定最大的容量,取字符串长度为扩容后的容量,
//否则取数组规定最大的容量为扩容后的容量
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
步骤:
首先计算
扩容后的数组的长度 =原来的字符数组长度的2倍 + 2
若新的容量 < 字符串长度
判断新的容量是否溢出或大于数组规定的最大长度
扩容后的数组的长度有几种情况:
总结:扩容的最大容量为Integer的最大值,超过该值抛异常
提供了很多append重载(String、int、long、short、byte、double、float、char、boolean、CharSequence)的方法,用于进行字符串的拼接
String s = "hello" ;
StringBuffer strb1 = new StringBuffer(s);
StringBuffer strb2 = strb1.append(1).
append(1.0D).
append((short)4).
append(true).
append(new char[]{'a', 'b'}).
append(s);
StringBuffer delete( int start , int end )
删除指定位置的字符串(左闭又开)
StringBuffer deleteCharAt(int index)
删除指定索引的字符
StringBuffer strb1 = new StringBuffer("helloworld");
strb1 = strb1.delete(2, 4);
System.out.println(strb1);
结果为heoworld
StringBuffer replace(int start , int end , String str)
替换指定位置的字符串(左闭右开)
void setCharAt(int index ,char ch)
替换(设置)指定位置的字符
StringBuffer strb1 = new StringBuffer("helloworld");
strb1 = strb1.replace(5,6,"test");
System.out.println(strb1);
结果为hellotestorld
在指定位置插入xxx
重载方法,xxx(String、int、long、short、byte、double、float、char、boolean、CharSequence)
public static void demo3(){
StringBuffer strb1 = new StringBuffer("helloworld");
strb1 = strb1.insert(5," ").insert(6, 0.5D).insert(9,' ');
System.out.println(strb1);
}
结果为hello 0.5 world
StringBuffer reverse()
把当前字符序列逆转
StringBuffer strb1 = new StringBuffer("helloworld");
strb1 = strb1.reverse();
System.out.println(strb1);
结果为dlrowolleh
当append和insert时,如果原来value数组长度不够,扩容。
如上这些方法支持方法链操作
根据索引返回字符或字符串
char charAt(int index)
截取指定索引位置的字符
String substring(int beginIndext)
返回一个新的字符串,从下标beginIndex开始截取到最后的一个子字符串
String substring(int beginIndex , int endIndex)
返回一个新的字符串,从下标beginIndex开始截取到endIndex(不包含)的一个子字符串 [ beginIndex , endIndex )
根据字符串返回索引
int indexOf(String str)
返回指定子字符串在此字符串中第一次出现的下标
int indexOf(String str , int fromIndex)
返回指定子字符串在此字符串中第一次出现处的索引,从指定的fromIndex位置开始
int lastIndexOf(String str)
返回指定子字符串在此字符串中最后出现的下标
int lastIndexOf(String str , int fromIndex)
返回指定子字符串在此字符串最后出现处的索引,从指定的fromIndex索引开始反向搜索
int length()
获取字符串的长度(不是char[]数组的长度)
int capacrity()
获取char[]数组的长度
StringBuffer strb1 = new StringBuffer("helloworld");
int capacity = strb1.capacity();
int length = strb1.length();
System.out.println(capacity);//26
System.out.println(length);//10
相同点:
不同点:
例如:
源码
StringBuffer的append方法使用了synchronize,然后调用super.append具体实现
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuilder的append方法没有使用了synchronize,然后调用super.append具体实现
public StringBuilder append(String str) {
super.append(str);
return this;
}
String(JDK 1.0):不可变字符序列
StringBuffer(JDK 1.0):可变字符序列、效率低、线程安全
StringBuilder(JDK 5.0):可变字符序列、效率高、线程不安全
注意:作为参数传递的话,方法内部String不会改变其值,StringBuffer和StringBuilder会改变其值
面试题
public static void demo3(){
String str = "hello" ;
char[] chs = {'w','o','r','l','d'};
int i = 10 ;
boolean flag = true ;
test(str,chs,i,true);
System.out.println(str);
System.out.println(chs);
System.out.println(i);
System.out.println(flag);
}
public static void test(String str,char[] chs,int i,boolean flag){
str = "test" ;
chs[0] = 'a' ;
i = 20 ;
flag = false ;
}
public static void main(String[] args) {
demo3();
}
执行结果:
分析结果:
String str
char[] chs
char[] 为引用类型,调用test方法时,在内部chs[0]='a’相当于修改其属性,所以可以成功
这体现了String的不可变性,其他基本类型也类似
结论:
1、String与8个基本类型作为方法参数,在方法内部修改,不会改变外部值
2、除了String与8个基本类型之外其他类型,在方法内部修改属性,会影响到外部
拼接200000次的速度比较
public static void testStr(){
long start = System.currentTimeMillis();
String str = "" ;
for (int i = 0; i < 200000 ; i++) {
str = str + i ;
}
long end = System.currentTimeMillis();
System.out.println("String拼接200000次的时间为:"+(end-start));
}
public static void testBuffer(){
long start = System.currentTimeMillis();
StringBuffer buffer = new StringBuffer("") ;
for (int i = 0; i < 200000 ; i++) {
buffer.append(i);
}
long end = System.currentTimeMillis();
System.out.println("StringBuffer拼接200000次的时间为:"+(end-start));
}
public static void testBuilder(){
long start = System.currentTimeMillis();
StringBuilder builder = new StringBuilder("") ;
for (int i = 0; i < 200000 ; i++) {
builder.append(i);
}
long end = System.currentTimeMillis();
System.out.println("StringBuilder拼接200000次的时间为:"+(end-start));
}
public static void main(String[] args) {
testStr();
testBuffer();
testBuilder();
}
比较结果:
String str = null ;
StringBuffer buffer = new StringBuffer(null);
buffer.append("a");
System.out.println(buffer);//调用toString方法
若参数为null,则StringBuffer与StringBuilder只要该对象都会报空指针异常,包括toString,==比较