不需要死记硬背,知道有,想得起来用,具体翻阅API帮助文档即可。
1. String
1.1 String 类是不可变类,也就是说 String 对象声明后,将不可修改
String类是引用数据类型,是不可变的,不可变指的是在一个地址中存放的字符串内容不可变(底层String源码中有一个属性:private final byte[] value;,而且数组长度不可变)
String s = "hello";
其中“hello”存储在字符串常量池中。- “hello”不可变。不能变成“hello123”。如果进行字符串拼接,必然创建新的字符串对象。
- 是 “hello”不可变,不是s不可变,s可以指向其它的字符串对象:s = “xyz”;
如下代码:
package stringtest;
public class StringTest01 {
public static void main(String[] args) {
//以下两行代码表示底层创建了3个字符串对象,都在字符串常量池中
String s1 = "abcdef";
String s2 = "abcdef" + "xy";
// 下面使用new方式创建字符串对象,"xy"也是来自字符串常量池
// 凡是""括起来的都在字符串常量池中有一份
// 此外,new对象的时候一定会在堆内存中开辟新的空间
String s3 = new String("xy");
}
}
注意: 凡是带有双引号的字符串,在编译期已经完全确定,这些字符串字面量将会放到字符串常量池中。在JVM启动的时候,会进行一系列的初始化,其中就包括字符串常量池的初始化,在初始化字符串常量池的时候,会将所有的字符串字面量全部提前创建好,放到字符串常量池中。在执行java程序的过程中,如果需要这个字符串字面量对象,直接从字符串常量池中获取。提高执行效率。
将字符串放入方法区中的字符串常量池的原因:字符串在实际开发中使用得非常频繁,字符串常量池是一种缓存技术,可以提高执行效率。
1.2 String s1 = “abc”和 String s2 = new String(“abc”)
package stringtest;
public class StringTest02 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println("s1==s2, " + (s1==s2));
System.out.println("s2==s3, " + (s2==s3));
System.out.println("s2 equlas s3," + (s2.equals(s3)));
System.out.println("s3==s4, " + (s3==s4));
}
}
运行结果:
内存结构图:
追加:如果单纯比较(new String(“abc”))=(=new String(“abc”))呢?
System.out.println("new String(\"abc\")==new String(\"abc\"), " + (new String("abc")==new String("abc")));
运行结果依然是false,因为比较的是两个new的对象的地址:
结论:
- 因为如果是采用双引号引起来的字符串常量,首先会到常量池中去查找,如果存在就不再分配,如果不存在就分配,常量池中的数据是在编译期赋值的,也就是生成 class 文件时就把它放到常量池里了,所以 s1 和 s2 都指向常量池中的同一个字符串“abc”;
- 关于 s3、s4,s3和 s4 采用的是 new 的方式,在 new 的时候存在双引号,所以他会到常量区中查找“abc”,而常量区中存在“abc”,所以常量区中将不再放置字符串,而 new 关键字会在堆中分配内存,所以在堆中会分别创建对象,对象 再指向 “abc”
- 如果比较 s2 和 s3 (或s4) 的值必须采用
equals
,String 已经对equals
方法进行了覆写。
1.3 字符串常量池在JDK1.7后不在方法区中,而改为在堆中
具体参见:Java 常量池详解(一)字符串常量池
1.4 String的拼接
1.动态拼接之后的新字符串不会自动放到字符串常量池中:
package stringtest;
public class StringTest03 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "def";
String s3 = s1 + s2;
String s4 = "abcdef";
// s3 指向的对象,没有在字符串常量池中,而是在堆中。
// 底层实际上在进行 + 的时候(注意:这个 + 两边至少有一个是变量),会创建一个StringBuilder对象,进行字符串的拼接,
//最后会自动调用StringBuilder对象的toString()方法,再将StringBuilder转换成string对象。
System.out.println(s3 == s4);// false
}
}
运行结果:
以上程序中字符串常量池中有三个: “abc”、“def”、“abcdef” ,除了字符串常量池的字符串之外,在堆中还有一个字符串对象 “abcdef”。
- 两个字符串字面量拼接会做编译阶段的优化,在编译阶段就会进行字符串的拼接。
如String s1 = "123"+ "456";
会在编译阶段进行拼接,在字符串常量池中只有一个:“123456”
String s1 = "123"+ "456";
String s2 = "123456";
System.out.println(s1 == s2); //true
1.5 String的构造方法
- String(char[] value):根据char数组创建一个新的字符串对象。
- String(char[] value, int offset, int count):根据char数组的指定部分创建一个新的字符串对象。
- String(byte[] bytes):根据byte数组创建一个新的字符串对象,默认使用平台默认的字符集进行解码。
- String(byte[] bytes, int offset, int length):根据byte数组的指定部分创建一个新的字符串对象,默认使用平台默认的字符集进行解码。
- String(byte[] bytes, Charset charset):根据byte数组和指定的字符集创建一个新的字符串对象,这是一个解码的过程。你需要提前知道“byte[] bytes”是通过哪个编码方式进行编码得到的。如果通过GBK的方式进行编码得到的“byte[] bytes”,调用以上构造方法时采用UTF-8的方式进行解码。就会出现乱码。比如:new String(bytes, Charset.defaultCharset());
- String(byte[] bytes, String charsetName):根据byte数组和指定的字符集名称创建一个新的字符串对象,同上5,也是一个解码过程,只不过这里是直接用字符串charsetName表示对应编码。
- String(String original):通过复制现有字符串创建一个新的字符串对象。这个方法被
@IntrinsicCandidate
标注,这个注解的作用是告诉编译器,该方法或构造函数是一个内在的候选方法,可以被优化和替换为更高效的代码。因此它是不建议使用的。比如:new String(“hello”); 这个代码会让常量池中有一个 “hello”,并且在堆中也有有一个String对象。
代码如下:
package stringtest;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class StringTest04 {
public static void main(String[] args) {
//1.将一个char数组转换为字符串
char[] c = {97,98,99}; // 97对应'a',98对应'b',99对应'c'
String str = new String(c);
System.out.println(str);//abc // String类中有重写toString()方法,所以输出不是一个地址,而是内容"abc";
//2.将char数组的部分转为字符串
String str1 = new String(c,0,2);//0代表获取数组元素的起始下标,2代表获取的长度
System.out.println(str1); //ab
// 3.将一个byte数组转为字符串,使用默认字符集解码
byte[] b = {97,98,99}; // 97对应'a',98对应'b',99对应'c'
String str2 = new String(b);
System.out.println(str2);//abc
//4.将byte数组的部分转为字符串,使用默认字符集解码
String str3 = new String(b,1,2);// 1代表获取数组元素的起始下标,2代表获取的长度
System.out.println(str3); //bc
//5.将一个byte数组转为字符串,使用指定字符集
byte[] str2Bytes = str2.getBytes(Charset.defaultCharset()); //Charset.defaultCharset()为默认字符集
String newStr = new String(str2Bytes, Charset.defaultCharset());
System.out.println(newStr); //abc
byte[] str2Bytes1 = str2.getBytes(StandardCharsets.UTF_8); //StandardCharsets中有多种可选字符集
String newStr1 = new String(str2Bytes1, StandardCharsets.UTF_8);
System.out.println(newStr); //abc
//6.将一个byte数组转为字符串,使用指定字符串说明的字符集
try {
byte[] str2Bytes2 = "早上好".getBytes("GBK"); //GBK编码
String newStr2 = new String(str2Bytes2, "UTF-8"); //UTF-8解码
System.out.println(newStr2); //���Ϻ� //编码和解码不一致,出现乱码
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
System.out.println("不支持该字符集!");
}
}
}
1.6 String的常用方法
- boolean startsWith(String prefix):判断字符串是否以指定的前缀开始;
- boolean endsWith(String suffix):判断字符串是否以指定的后缀结束;
- boolean equals(Object anObject):字符串相等比较,不忽略大小写;
- boolean equalsIgnoreCase(String anotherString):字符串相等比较,忽略大小写;
- int indexOf(String str):取得指定字符在字符串的第一次出现的位置;
- int indexOf(String str, int fromIndex); 从当前字符串的fromIndex下标开始往右搜索,获取当前字符串中str字符串的第一次出现处的下标。
- int lastIndexOf(String str):返回指定字符串在字符串最后一次出现的位置;
- int lastIndexOf(String str, int fromIndex);:从当前字符串的fromIndex下标开始往左搜索,获取当前字符串中str字符串的最后一次出现处的下标。
- int length():取得字符串的长度;
- String substring(int beginIndex):从指定下标beginIndex开始截取子字符串;
- String substring(int beginIndex, int endIndex):从指定区间[beginIndex,endIndex)截取子字符串
- static String valueOf(boolean b); 以下所有的静态方法valueOf作用是将非字符串类型的数据转换为字符串形式。(可以处理null,而toString方法不行,传入null会造成异常)
- static String valueOf(char c);
- static String valueOf(char[] data);
- static String valueOf(char[] data, int offset, int count);
- static String valueOf(double d);
- static String valueOf(float f);
- static String valueOf(int i);
- static String valueOf(long l);
- static String valueOf(Object obj);
- char charAt(int index):返回指定位置的字符
- int compareTo(String anotherString):按字典顺序(越往后的字母看作越大)比较字符串,相等返回1,前小后大返回-1,前大后小返回1;
- int compareToIgnoreCase(String str): 两个字符串按照字典顺序比较大小,比较时忽略大小写;
- boolean contains(CharSequence s):当前字符串中是否包含某个子字符串s;
- byte[] getBytes():将字符串转换成字节数组。其实就是对字符串进行编码。默认按照系统默认字符集。 byte[] bytes=“abc”.getBytes();//再遍历数组,输出97、98、99
- byte[] getBytes(String charsetName) throws UnsupportedEncodingException ; 将字符串按照指定字符集的方式进行编码。【需要进行异常处理】
- byte[] getBytes(Charset charset); 将字符串按照指定charset的方式进行编码。(暂时不会用)
- boolean isEmpty():判断某个字符串是否为空串;
- char[] toCharArray():将字符串转换为char数组。
- String toLowerCase():转小写
- String toUpperCase();:转大写
- String concat(String str):进行字符串的拼接操作。和 + 的区别?
- + 既可以进行求和,也可以进行字符串的拼接,进行底层拼接时(+两边> 至少有一个是变量)会创建StringBuilder对象进行拼接,而后再通过toString转为一个String对象。+ 拼接null时不会出现空指针异常。
- concat方法参数只能时字符串类型,拼接时不会创建StringBuilder对象,拼接完成后返回一个新的String对象。拼接null会出现空指针异常。
- + 使用较多。如果进行大量字符串拼接,这两个都不合适。
- String trim():去除字符串前后空白(只能去除ASCII码中的空格和制表符);
- String strip():去除字符串前后空白(支持所有的编码形式的空白,可以将全角空格去除,\u3000是全角空格,Java11新增);
- String stripLeading():去除前空白;
- String stripTrailing():去除后空白;
- String toString():对String类本身来说是返回其本身,也可以被其他引用数据类型覆写该方法,从而调用转换为想要输出的字符串信息;
- String intern(); 获取字符串常量池中的字符串,如果常量池中没有,则将字符串加入常量池并返回【可以对使用比较频繁的字符串使用该方法,将其加入到常量池】。
byte[] bytes = {97,98,99,100}; String s = new String(bytes); String s2 = s.intern(); // 将字符串"abcd"放入字符串常量池并返回常量池中 的字符串"abcd" System.out.println(s==s2); //true
- static String join(CharSequence delimiter, CharSequence… elements)
作用:将elements(CharSequence… 表示可变长度参数。这里是多个字符串)用指定的字符串delimeter连接起来,返回这个新组成的字符串- static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements);与上join类似,这里后面的参数是可迭代的对象,比如后续可以学到的List,Set。
- getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):将字符串中[srcBegin, srcEnd)范围的字符从当前字符串复制到目标char数组dst,dstBegin为目标数组的复制起始偏移量。
- CharSequence subSequence(int beginIndex, int endIndex):返回一个新的字符序列,它是此序列的一个子序列。【与subString的区别:目前只知道返回类型不一样,具体用法应该一样】
示例代码:
package stringtest;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class StringTest05 {
public static void main(String[] args) {
//常用方法示例
String str = "abcdefabc";
//判断字符串是否以指定的前缀开始
System.out.println(str.startsWith("ab")); //true
System.out.println(str.startsWith("c",2)); // 第一个参数是判断的字符串,第二个参数是偏移量 //true
//判断字符串是否以指定的后缀结束
System.out.println(str.endsWith("bc")); //true
System.out.println(str.endsWith("f")); //false
//字符串相等比较,不忽略大小写
System.out.println(str.equals("Abcdefabc")); //false
System.out.println(str.equals("abcdefabc")); //true
//字符串相等比较,忽略大小写
System.out.println(str.equalsIgnoreCase("Abcdefabc")); //true
//取得指定字符在字符串的第一次出现的位置
System.out.println(str.indexOf("ab")); //0
System.out.println(str.indexOf(99)); //直接输入字母对应的ASCII码 //2
System.out.println(str.indexOf("ab",4)); //第一个参数是查找的字符串,第二个参数是开始查找的索引(从左往右搜索) //6
System.out.println(str.indexOf(99,4)); //第一个参数直接输入字母对应的ASCII码,第二个参数是开始查找的索引(从左往右搜索) //8
//返回指定字符串在字符串最后一次出现的位置
System.out.println(str.lastIndexOf("bc")); //7
System.out.println(str.lastIndexOf(99)); //直接输入字母对应的ASCII码 //8
System.out.println(str.lastIndexOf("bc",4)); //第一个参数是查找的字符串,第二个参数是开始查找的索引(从右往左搜索) //1
System.out.println(str.lastIndexOf(99,4)); //第一个参数直接输入字母对应的ASCII码,第二个参数是开始查找的索引(从右往左搜索) //2
//取得字符串的长度
System.out.println(str + "字符串长度:" + str.length()); //abcdefabc字符串长度:9
//根据指定的表达式拆分字符串,返回字符串数组
String[] strings = str.split("f");
for (int i = 0; i < strings.length; i++) {
System.out.print(strings[i] + " "); //abcde abc
}
System.out.println();
//截子串,区间左闭右开,只有一个参数的话就是开始截取的下标
System.out.println(str.substring(1,3)); //bc
System.out.println(str.substring(3)); //defabc
//将其他类型转换成字符串,唯一的静态方法
System.out.println(String.valueOf(1)); //1
System.out.println(String.valueOf('b')); //b
System.out.println(String.valueOf(22.3));//22.3
System.out.println(String.valueOf(true));//true
System.out.println(String.valueOf(new char[]{'a','b'})); //ab
//返回指定位置的字符
System.out.println(str.charAt(3)); //d
//按字典顺序(越往后的字母看作越大)比较字符串,相等返回0,前小后大返回-1,前大后小返回1
System.out.println("a".compareTo("b")); //-1
System.out.println("a".compareToIgnoreCase("A")); //0
//判断前面的字符串是否包含后面的子字符串
System.out.println(str.contains("abc")); //true
//将字符串转化为一个byte数组
byte[] bytes = str.getBytes();
for (int i = 0; i < bytes.length; i++) {
System.out.print(bytes[i] + " "); //97 98 99 100 101 102 97 98 99
}
System.out.println();
//将字符串转化为一个byte数组(按照指定的字符集)
try {
byte[] bytes1 = str.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
System.out.println("不支持的字符集");
}
for (int i = 0; i < bytes.length; i++) {
System.out.print(bytes[i] + " "); //97 98 99 100 101 102 97 98 99
}
System.out.println();
//判断某个字符串是否为空串
System.out.println(str.isEmpty()); //false
//用新的字符串替换指定字符串(替换所有)
System.out.println(str.replace("abc","c")); //cdefc
//将字符串转换为char数组
char[] chars = str.toCharArray();
for (int i = 0; i < chars.length; i++) {
System.out.print(chars[i] + " "); //a b c d e f a b c
}
System.out.println();
//转小写
System.out.println(str.toLowerCase()); //abcdefabc
//转大写
System.out.println(str.toUpperCase()); //ABCDEFABC
//进行字符串拼接,与+的区别:
// + 既可以进行求和,也可以进行字符串的拼接,底层拼接时会创建StringBuilder对象进行拼接。+ 拼接null时不会出现空指针异常。
// concat方法参数只能时字符串类型,拼接时不会创建StringBuilder对象,拼接完成后返回一个新的String对象。拼接null会出现空指针异常。
System.out.println(str.concat("123")); //abcdefabc123
//去除字符串前后空白(只能去除ASCII码中的空格和制表符,无法去除其他编码的空白,比如全角空白"\u3000")
System.out.println("\u3000gvf \t ".trim()); // gvf[前面还有一个全角空格]
//去除字符串前后空白(支持所有的编码形式的空白,可以将全角空格去除,\u3000是全角空格,【Java11新增,我这是Java8,无法运行】
// System.out.println("\u3000gvf \t ".strip()); //
//去除前空白
// System.out.println(" 123".stripLeading()); //也是Java11新增吧
//去除后空白
// System.out.println(" 123 ".stripTrailing()); //也是Java11新增吧
//返回字符串本身
System.out.println(str.toString()); //abcdefabc
//获取字符串常量池中的字符串,如果常量池中没有,则将字符串加入常量池并返回
System.out.println(str.intern()); //abcdefabc
byte[] bytes1 = {97,98,99,100};
String s = new String(bytes1);
System.out.println(s.intern()); //abcd, 将字符串"abcd"放入字符串常量池并返回常量池中的字符串"abcd"
String s1 = "abcd";
String s2 = new String("abcd");
System.out.println(s==s.intern()); //true
System.out.println(s==s1); //true
System.out.println(s==s2); //false
//将多个字符串以某个分隔符连接(Java8新增)
System.out.println(String.join(",", "1","2","3")); //1,2,3
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
System.out.println(String.join("*", list)); //a*b*c
//将字符串中指定范围的字符从当前字符串复制到目标char数组
char[] chars1 = new char[6];
String s3 = "abc";
s3.getChars(0,2,chars1, 2);
System.out.println(Arrays.toString(chars1)); //[ , , a, b, , ]
//获取当前字符串的一个子字符序列
System.out.println(str.subSequence(1, 3)); //bc
}
}
1.7 关于System.out.println()
方法
查看源码可以发现:
System.out.println(x);
方法内部有调用String.valueOf(x)
方法,将数据转换为字符串,而String.valueOf(x)
方法在实现时内部有调用toString()
方法。这就是x为对象时输出的与该对象的toString()
方法返回值一致的原因了。
最终结论:一切在控制台打印输出的都是字符串!!!
1.8 判断数组的长度和判断字符串长度不一样!!
判断数组长度是:数组名.length
【length为属性】;
判断字符串长度:"xxx".length()
【为调用方法】。
1.9 正则表达式初步
- 正则表达式(regular expression),简称为regex或regexp,是一种用于描述特定模式的表达式。它可以匹配、查找、替换文本中与该模式匹配的内容,被广泛应用于各种文本处理和匹配相关的应用中。
- 正则表达式的应用:
- 验证输入内容的格式是否正确。例如,邮箱,手机号,密码等;
- 在文本编辑器中进行搜索和替换。例如,在代码编辑器中查找指定字符串或替换错误的代码成为正确的代码块;
数据挖掘和信息提取。正则表达式可以从HTML、XML、JSON等格式的数据中提取所需的信息; - 用于编写脚本语言,如awk,grep和sed服务器端编程。正则表达式在处理数据和字符串时具有高效的性能,可以在开发Web应用程序时被广泛应用
- 正则表达式和Java语言的关系?
Java语言中可以使用正则表达式。C语言以及其它大部分编程语言都是支持正则表达式的。
其他具体规则见:正则表达式-菜鸟教程
String类与正则表达式相关的两个方法:
- String replace(CharSequence target, CharSequence replacement):将当前字符串中所有的target替换成replacement,返回一个新的字符串。
- String replaceAll(String regex, String replacement):将当前字符串中所有符合正则表达式的regex替换成replacement。
- String[] split(String regex):将当前字符串以某个正则表达式表示的子字符串进行分割,返回一个字符串数组。
- boolean matches(String regex):判断当前字符串是否符合正则表达式regex。
如下代码示例:
package stringtest;
import java.util.Arrays;
public class StringTest07 {
public static void main(String[] args) {
String s1 = "asdd33dfsdaf33ddsd55fdd3dssf4343sdf455ddsdddh565gggh55ddhg";
//将 dd 替换为"中"
System.out.println(s1.replaceAll("dd","中"));
//将 dd 替换为"中"
System.out.println(s1.replaceAll("d{2}", "中"));
//将数字替换成"中"
System.out.println(s1.replaceAll("\\d","中"));
//将非数字替换成"中"
System.out.println(s1.replaceAll("\\D", "中"));
//将s1按33分割,返回分割后的字符串数组
String[] strings = s1.split("33");
System.out.println(Arrays.toString(strings));
//判断当前字符串是否符合正则表达式
System.out.println("1324234".matches("[0-9]+")); //0-9数字重复一次或多次
}
}
运行结果:
1.10 String面试题
- 以下代码创建了几个对象?
String s1 = new String("hello") ;
String s2 = new String("hello") ;
内存结构图:
通过以上分析可知:创建了 3 个对象,堆区中 2 个,字符串常量池中 1 个。
通过以上分析,使用 String 时,不建议使用 new 关键字,因为使用 new 会创建两个对象。
- 推断以下代码运行结果
String s1 = new String("abc");
String s2 = "abc";
System.out.println(s1==s2);
System.out.println(s1.equals(s2));
new String 会在堆中新建一个对象,s1指向堆中的对象,s2指向字符串常量池中的对象,而s1和s2的数据类型和内容是一致的,所以输出结果为先false后true。
- 推断以下代码运行结果
String s1 = "a" + "b" + "c";
String s2 ="abc";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
字面量直接拼接在编译期就完成了,s1和s2均指向字符串常量池中的对象,所以运行结果为均输出true。
- 以下代码的输出结果是什么?还是会出现空指针异常?
String s1 = null;
String s2 = s1 + null
System.out.println(s2);
第二行使用+拼接的时候,底层对s1调用的是s1.valueOf(),不会抛出异常,输出结果为nullnull。
- 推断以下代码运行结果
String s1 ="a1";
String s2 = "a" + 1;
System.out.println(s1 == s2);
1是一个字面量,所以该拼接是在编译期完成,所以运行结果为true。
- 以下代码创建了几个对象?
String s1 ="a";
String s2 = new String("b");
String s3 = s1 + s2;
s1中"a"为字符串常量池对象,s2中有一个堆对象和一个字符串常量池对象"b",s3由于是拼接而成,所以堆中分别有一个StringBuilder对象,一个String对象,所以一共创建了5个对象。
- 以下代码创建了几个对象?
String str =new String("a")+ new String("b")
字符串常量池中2个,new String在堆中产生2个String对象,+拼接时产生1个StringBuilder对象,1个String对象,所以一共创建了6个对象。
- 推断以下代码运行结果
String s1 ="ab";
String s2="abc";
String s3 = s1 + "c";
System.out.println(s2 == s3); //false
System.out.println(s2.equals(s3)); //true
s1为变量,对s3的拼接生成需要再运行期执行,所以s3中存的是堆中对象的地址,与s2中所存的字符串常量池的地址不一致,而二者类型和内容是一致的,所以结果为先false后true。
- 以下代码创建了几个对象?
//
String s= "a" + "b";
仅在编译器创建一个字符串常量对象"ab"
- 推断以下代码运行结果
String s1 = "ab";
final String s2 ="b";
String s3 ="a" + s2;
System.out.println(s1 == s3);
因为s2为final的,是不可变的量,相当于常量, 对s3来说相当于是"a" +“b”,在编译期就拼接成了"ab",所以与s1指向字符串常量池中的同一个对象,所以结果为true。
- 这个结果是true还是false?
String s1 = "abc";
StringBuilder s2 = new StringBuilder(s1);
System.out.println(s1.equals(s2));
因为在equals方法中首先就需要有instance of的判断,类型不一样比较没有意义,结果一定是false。
- 以下程序执行结果是什么?
public static void main(String[] args){
String s1 = "ab";
final String s2 = getB();
String s3 ="a" + s2;
System.out.println(s1 == s3);
}
public static String getB(){
return "b";
}
因为方法getB()只能在运行期执行,所以在编译期是不能确定s2的,s2还是一个变量,所以结果为false。
-
以下程序执行结果是什么?
null为引用类型,根据类型就近原则(可以理解为String是一个更具体的类),所以调用传入参数为String类型的方法,所以结果为String…。 -
以下程序执行结果是什么?
String m = "m";
String f = m + "e";
String str = f.intern();
System.out.println(str == "me");
f.intern()是将"me"放入到字符串常量池中,并将对应地址返回,所以结果为true。
2. StringBuffer 和StringBuilder
这两个类是专门为频繁进行字符串拼接而准备,为可变长度字符串。
2.1 StringBuffer
StringBuffer 称为字符串缓冲区,它的工作原理是:预先申请一块内存,存放字符序列,如果字符序列满了,会重新改变缓存区的大小,以容纳更多的字符序列。StringBuffer 是可变对象,这个是 String 最大的不同。
package stringtest.stringbuffertest;
public class StringBufferTest01 {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < 100; i++) {
// stringBuffer.append(i);
// stringBuffer.append(",");
//以上两行等价于下面这行,方法链的编程风格
stringBuffer.append(i).append(",") ;
}
System.out.println(stringBuffer);
System.out.println(stringBuffer.toString());
StringBuffer stringBuffer1 = new StringBuffer();
for (int i = 0; i < 100; i++) {
//拼串去除末尾逗号
stringBuffer1.append(i);
if(i!=99){
stringBuffer1.append(",");
}
}
System.out.println(stringBuffer1);
}
}
运行结果:
2.2 StringBuilder
用法同 StringBuffer,StringBuilder 和 StringBuffer 的区别是 StringBuffer 中所有的方法都是同步的(都有加synchronized修饰),是线程安全的,但速度慢,StringBuilder 的速度快,但不是线程安全的。
2.3 如何优化StringBuffer/StringBuilder的性能?
在创建StringBuffer/StringBuilder的时候尽可能定一个初始化容量(构造方法中,如StringBuffer stringBuffer = new StringBuffer(100);
),以减少底层数组的扩容次数。
StringBuffer/StringBuilder默认初始化容量:16
StringBuffer/StringBuilder的扩容策略是:从当前容量开始,每次扩容为原来的2倍再加上2。
2.4 String的不可变和StringBuffer/StringBuilder的可变
- String类被final修饰,其无法被继承!
面试题,字符串为什么不可变?
从源码看,其底层是一个private的被final修饰的byte数组(jdk1.9及之后),由于数组一旦创建长度就不可变,且被final
修饰的引用一旦指向某个对象后不可再指向其他对象,所以其是不可变的; - StirngBuffer/StringBuilder类也被final修饰,无法被继承
它底层实际上是一个byte数组(jdk1.9及之后),但是其未被final修饰,该数组初始容量为16,存满之后,则在底层调用System.arraycopy()
(将小数组移到大数组中,数组引用指向大数组的首地址)方法进行数组的扩容,所以其是可变的。
注意:jdk1.8及以前String/StrringBuffer/StringBuilder使用的是char数组,jdk1.9及以后使用的是byte数组。因为开发人员发现人们使用的字符串值是拉丁字符居多而之前使用的char数组每一个char占用两个字节而拉丁字符只需要一个字节就可以存储,剩下的一个字节就浪费了,造成内存的浪费,GC的更加频繁。因此在jdk1.9中将String底层的实现改为了byte数组。
2.5 StringBuffer和StringBuilder构造方法
- StringBuffer():构造一个字符串生成器,其中不包含任何字符,初始容量为16个字符。
- StringBuffer(int capacity):构造一个字符串生成器,其中不包含任何字符,并且具有由容量参数指定的初始容量。
- StringBuffer(String str):构造初始化为指定字符串内容的字符串生成器。
- StringBuffer(CharSequence seq):构造初始化为指定字符串序列(可以是String、StringBuilder、StringBuffer中任一类型)的字符串生成器。
StringBuilder与上类似。
package stringtest.stringbuffertest;
public class StringBufferTest02 {
public static void main(String[] args) {
StringBuffer sb1 = new StringBuffer(); //初始化容量为16
StringBuffer sb2 = new StringBuffer(100); //设定初始化容量为100
StringBuffer sb3 = new StringBuffer("abc");
StringBuffer sb4 = new StringBuffer(sb3);
}
}
2.6 StringBuffer和StringBuilder常用方法
示例代码:
package stringtest.stringbuffertest;
public class StringBufferTest03 {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer(30);
char[] c = {97,98,99};
s1.append("abc");
s1.append(11);
s1.append('c');
s1.append(false);
s1.append(2324243345.5);
s1.append(c,0,3);
System.out.println(s1);
s1.delete(1,3); //删除下标[1,3)的数据
System.out.println(s1);
s1.deleteCharAt(1); //删除下标为1的数据
System.out.println(s1);
s1.insert(3,0); //下标3处插入数据0
System.out.println(s1);
s1.replace(1,6,"hello"); //将下标[1,5)的数据替换为"hello"
System.out.println(s1);
System.out.println(s1.reverse()); //倒置
s1.setCharAt(0,'A');//将下标为0的数据修改为"A"
System.out.println(s1);
s1.setLength(3); //将长度设置为3,谨慎使用,会把底层的数据抹掉
System.out.println(s1);
}
}
运行结果:
2.7 String、StringBuilder/StringBuffer效率PK
因为 String 是不可变对象,如果多个字符串进行拼接,将会频繁创建多个对象,这样可能会造成内存溢出,会给垃圾回收带来工作量,如下面的应用场景最好不要用 String,改用StringBuffer或StringBuilder:
package stringtest;
public class StringTest06 {
public static void main(String[] args) {
long begin = System.currentTimeMillis();
String s = "";
/**
* 底层会新建一个StringBuilder对象
* 然后调用StringBuilder的append(i)方法进行追加
* 然后再调用StringBuilder toString()方法转成String类型
* 也就是说:这里会频繁的创建String对象,导致效率很低
* 同时给GC带来巨大压力。
* 建议使用 StringBuffer 或 StringBuilder
*/
for (int i = 0; i < 10000; i++) {
s = s + i;
}
long end = System.currentTimeMillis();
System.out.println(end - begin); //318毫秒
}
}
使用StringBuffer:
package stringtest;
public class StringTest06 {
public static void main(String[] args) {
long begin = System.currentTimeMillis();
StringBuffer s = new StringBuffer(10000);
for (int i = 0; i < 10000; i++) {
s = s.append(i);
}
long end = System.currentTimeMillis();
System.out.println(end - begin); //2毫秒
}
}