字符串是程序开发中,使用最频繁的类型之一,与基本类型有着相同的地位。在使用字符串拼接时,JVM(Java虚拟机)有时会对字符串做特殊处理,使其合成一个最终的字符串,从而达到高效的运行目的。
String 是不可变类(immutable),底层是被final修饰的数组 private final byte[] value,对它进行任何改动,将会重新创建对象;
String对象赋值后就会在常量池中缓存,如果再次被创建会判断常量池中是否已有缓存对象,如果有将会直接返回该对象的引用,这是为了节省空间。
创建字符串的两种方式:
String str = "abc";
String str = new String("abc");
区别:
package testDemo1;
public class TestOne {
public static void main(String[] args) {
String s1 = "a";
String s2 = new String("a");
System.out.println(s1==s2); //false
String s3 = "a";
System.out.println(s1==s3); //true
System.out.println(s2==s3); //false
}
}
第一种:仅仅是一个赋值语句,在创建的时候,JVM 会检查在字符串池中,是否已经存在该字符串,如果已经存在了,那么会返回这个字符串的引用给s3。如果不存在,那么会创建一个 a 字符串对象,再赋值给 s1。因此,第一种可能只创建 1 个或者 0 个对象。(即上面提到的常量池)
第二种:会在内存中创建 1 个或者 2 个对象。把 new String(“a”) 这句话拆成两个部分来看,一个是”a”, 另一个是 new String()。如果 a 字符串已经在字符串池中存在了,那么就不需要在创建 a 字符串的对象了,但是 new String 这行代码会再构造出一个和 a 一样的字符串,并且是放在堆上。
字符串的拼接
String str = “hello” + “world”;
String str = “hello”; str += “world”;
String str = “hello”; String str2 = str + “world”;
JVM对拼接字符串的优化
package testDemo1;
public class TestTwo {
public static void main(String[] args) {
String str = "hello " + "world";
String str2 = "hello world";
System.out.println(str==str2); //true
}
}
通过javap -c Main的反编译代码,可以看出JVM对此做了如下的优化:
说明 JVM 在某些情况下会特殊处理 String 类型。
字符串截取
使用 substring() 方法:
package testDemo1;
public class TestThree {
public static void main(String[] args) {
String str = "123abc456";
int i = 3;
System.out.println(str.substring(0,i)); //取字符串前i个字符 ==>123
System.out.println(str.substring(i)); //去掉字符串前i个字符 ==>abc456
System.out.println(str.substring(str.length() - i)); //从右边开始取i个字符 ==>456
System.out.println(str.substring(0,str.length() - i)); //从右边开始去掉i个字符 ==>123abc
}
}
通过StringUtils提供的方法 (运用StringUtils需要导入相关jar包,commons-lang3-3.1.jar):
package testDemo1;
public class TestThree {
public static void main(String[] args) {
System.out.println(StringUtils.substringBefore(“dssswabww”, “w”)) // dsss 这里是以第一个”w”,为标准。
System.out.println(StringUtils.substringBeforeLast(“dssswabww”, “w”)) //dssswabw 这里以最后一个“w”为准。
}
}
字符串格式化
String 类中,可以使用 format() 方法格式化字符串:
1)String.format(String format, Object… args)
2)String.format(Locale locale, String format, Object… args)
区别:前者使用本地语言环境,后者使用指定语言环境
源码如下:
public static String format(String format, Object... args) {
return new Formatter().format(format, args).toString();
}
public static String format(Locale l, String format, Object... args) {
return new Formatter(l).format(format, args).toString();
}
小小 14 打游戏 采用格式化可以动态修改
package testDemo1;
public class TestThree {
public static void main(String[] args) {
String str = String.format("我叫%s,今年%d岁,喜欢%s","小小",14,"打游戏");
System.out.println(str.toString());
}
}
字符对比
package testDemo1;
public class TestOne {
public static void main(String[] args) {
String s1 = "abc";
String s2 = new String("abc");
String s3 = "abc";
System.out.println(s1.equals(s2)); //true
System.out.println(s1.equals(s3)); //true
System.out.println(s2.equals(s3)); //true
}
}
==:
基本类型比较值是否相同;
引用类型比较引用是否相同。
equals解析:
package testDemo1;
public class TestOne {
public static void main(String[] args) {
Dog d1 = new Dog("二狗子");
Dog d2 = new Dog("二狗子");
System.out.println(d1.equals(d2)); //false
String s1 = "狗剩子";
String s2 = "狗剩子";
System.out.println(s1.equals(s2)); //true
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1.equals(str2)); //true
String st1 ="AB";
String st2 = new String("AB");
System.out.println(st1.equals(st2)); //true
}
}
class Dog{
private String name;
public Dog(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
出乎我们的预料,同是对堆中新建对象,为什么两个值相同的 String 对象返回true?
看看 equals 源码:
所有类的基类Object中进行定义的equals方法
public boolean equals(Object obj) {
return (this == obj);
}
equals 的本质是 ==
String类对equals方法
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
String 对 Object 方法进行了重写,例如 Integer 等也是重写了 equals 方法,所以像这种 equals 被重写类型所新建的对像,就变成了内容的比较。
从源码可以得出:d1 和 d2 是 Object 类中 equals 方法进行的比较,比较方式和 == 相同,d1.equals(d2) 等同于 d1==d2 比较一样,比较的是引用。(如有描述不当之处,请评论区指出)
== 和 equals的区别详解
对equals重新需要注意五点:
自反性:对任意引用值X,x.equals(x)的返回值一定为true;
对称性:对于任何引用值x,y,当且仅当y.equals(x)返回值为true时,x.equals(y)的返回值一定为true;
传递性:如果x.equals(y)=true, y.equals(z)=true,则x.equals(z)=true ;
一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变;
非空性:任何非空的引用值X,x.equals(null)的返回值一定为false 。
如果忽略字符串的大小写对比值可以使用 equalsIgnoreCase() 方法:
package testDemo1;
public class TestOne {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "ABC";
System.out.println(s1.equalsIgnoreCase(s2)); //true
}
}
String、StringBuffer、StringBuilder
字符串相关类型主要有这三种:String、StringBuffer、StringBuilder,其中StringBuffer 和 StringBuilder 都是可变字符串类型。StringBuffer 在字符串拼接时使用了 synchronized 来保障线程安全,因此在多线程中字符串拼接推荐使用它。
package testDemo1;
public class TestOne {
public static void main(String[] args) {
String i = "abc";
i = "ab";
System.out.println("i = " + i); // i = ab
}
}
由此可见:虽然 i 发生了改变,但是它是重新创建了一个对象,所以String 类是不可变对像,对其修改就是重新创建对象。
StringBuffer的使用:
package testDemo1;
public class TestOne {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("world ");
sb.append("world");
System.out.println("sb = " + sb); // sb = world world
sb.insert(0, "hello ");
System.out.println("sb = " + sb); // sb = hello world world
sb.setCharAt(0, 'H');
System.out.println("sb = " + sb); // sb = Hello world world
}
}
由此可见,StringBuffer 发生了改变(即进行了字符串拼接),所以说 StringBuffer 是可变字符序列。
StringBuilder 和 StringBuffer 的使用方法一样,它们都继承于AbstractStringBuilder。
JDK源码深入了解
StringBuffer类也被定义成final类,与String类相同。但是StringBuffer类对象的字符序列是可修改的,类本身写好了方法改变序列类容和长度。并且StringBuffer是线程安全的,类的各个方法都用synchronized修饰。可以这样理解StringBuffer类:可修改的String,并且线程安全。
StringBuilder类比之StringBuffer类效率更高,所以单线程采用StringBuilder。
String、StringBuilder、StringBuffer三者的执行效率:
StringBuilder > StringBuffer > String
当然这个是相对的,不一定在所有情况下都是这样。比如String str = “hello”+ "world"的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高。
因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。
String s1 = new String(“abc”)创建了几个对象?
创建了一个或两个对象。如果字符串常量池中已有 abc 则创建了一个对象,String(“abc”) 就会创建一个引用对象 s1 指向常量池中的 abc;反之则创建了两个对象。
什么是String,它是什么数据类型
String是定义在 java.lang 包下的一个类。它不是基本数据类型。
String是不可变的,JVM使用字符串池来存储所有的字符串对象。
String, StringBuffer,StringBuilder的区别?
String是不可变类,每当我们对String进行操作的时候,总是会创建新的字符串。操作String很耗资源,所以Java提供了两个工具类来操作String - StringBuffer和StringBuilder。
StringBuffer和StringBuilder是可变类,StringBuffer是线程安全的,StringBuilder则不是线程安全的。所以在多线程对同一个字符串操作的时候,我们应该选择用StringBuffer。由于不需要处理多线程的情况,StringBuilder的效率比StringBuffer高。
String是不可变的有什么好处
String在多线程中使用是安全的,我们不需要做任何其他同步操作。
String的值也不能被改变,所以用来存储数据密码很安全。
因为java字符串是不可变的,可以在java运行时节省大量java堆空间。因为不同的字符串变量可以引用池中的相同的字符串。
什么是字符串常量池?
字符串常量池就是用来存储在 Java 堆中的字符串池,是为了防止每次新建字符串带的时间和空间消耗的一种解决方案。