编程过程中,虽然字符串经常被像操作基本数据类型那样来使用,但实质上任何编程语言都没有提供字符串这种基本数据类型,字符串用String类来表示。
String本身是一个类,与int,char等基本数据类型有本质的区别。只不过字符串在实际编程过程中使用的实在是非常频繁,所以在Java里面利用其JVM的支持提供了可以简单使用Stirng类,使其可以像普通变量那样采用直接赋值的方式进行字符串的定义。
字符串必须包含在一对双引号(“ ”)之内,实际上描述的是一个String类的匿名对象。引申:因为“ ”用来表示字符串,所以要在字符串中使用引号时则需要用到转义字符\,在打包Json数据时会经常用到,Json数据中的key和value大多是字符串类型,例如:
String str = "\"version\": \"1.0.0\"";
String类之所以可以保存字符串的主要原因是其中定义了一个数组(byte类型数组),字符串中的每个字符数据都保存在了此数组之中,从 官方文档 或Stirng类源代码中可以看出。所以,所谓的字符串其实是对数组的一种包装应用,那么既然包装的是数组,所以字符串里面的内容是无法改变的。
我们编程时,好像明明可以使用字符串拼接或者重新赋值来改变内容。观察下面的代码,如果可以通过“+”操作来改变字符串内容的话,应该打印true,但打印的是false。
String strA = "helloworld";
String strB = "hello";
String strC = strB + "world";
// false
System.out.println(strA == strC);
既然String类之中包含的是一个数组,数组的长度是固定的,那么设置了一个字符串之后,会自动的进行一个数组空间的开辟,开辟内容的长度是固定的。事实上,不管是重新赋值还是使用“+”操作,都会在内存中创建新的字符串对象,所以代码中strA和strB的地址是不相等的。在整个的处理过程中,字符串常量的内容没有发生任何的改变,改变的只是一个String类对象的引用。
如果想要改变字符串的内容,可以使用StringBuffer类或StringBuilder类。
String对象(常量)池的目的是实现数据的共享处理,在Java中对象(常量)池分两种:
举例:
String strA = "helloworld";
String strB = "hello" + "world";
String strC = "hello";
String strD = strC + "world";
// true
System.out.println(strA == strB);
// false
System.out.println(strA == strD);
上面代码中,strA、strB和strC的内容会被放到静态常量池,并且strA和strB的引用指向同一块堆内存空间。strD的内容会被放到运行时常量池,因为使用“+”进行字符串拼接时使用的是变量strC而不是“hello”,程序加载时并不能确定strC是什么内容。
使用直接赋值的方式(推荐)
String str = "hello";
使用构造函数的方式
String str = new String("hello");
两种方式的区别
如下图所示:第一段代码,使用直接赋值的方式创建字符串时,会先到堆内存的字符串池中去查找是否已存在该数据,不存在时则创建,存在时则重用;第二段代码,使用构造方法创建字符串时,会开辟两块堆内存空间,而实际只会用到一块,另一块由匿字符串常量"hello"创建的匿名对象将会变成垃圾空间。
在字符串定义时,“""”和“null”不是一个概念,“""”表示有实例化对象,可以使用isEmpty()方法来判断,“null”表示没有实例化对象,使用isEmpty()方法时会报空指针异常。isEmpty()是用来判断字符串的内容,所以一定要在有实例化对象的时候才能进行调用,equal()方法也是一样的道理。
String str = “” 和 String str = null的区别
String str = “” 会在堆内存开辟空间,只不过存储的内容是空的,String str = null则不会在堆内存开辟空间,只会在栈内存上创建String类对象的引用。
对int类型的比较使用 == ,字符串可以使用 == 进行比较,但得到的结果并非我们想要的,要比较两个字符串的内容是否相等可以使用equal()方法。
“==”和equal()的区别
引申内容
观察下面两段代码,第一段是将字符串常量放到了括号内,第二段将字符串常量放到了前面(推荐)。
正常情况下,两段代码不会有什么问题,但是,如果str是用户输入,并且用户输入了null,那么第一段代码会报空指针异常:Exception in thread “main” java.lang.NullPointerException,第二段则正常。原因:如果字符串常量写到前面的话永远不会出现空指针异常,字符串是一个匿名对象,匿名对象一定是开辟好堆内存空间的对象;而将字符串常量放到括号内,用户输入null时,相当于拿了一个没有开辟内存空间的内容去做比较,所以会出现空指针异常。
String str = null;
//会出现空指针异常
if(str.equals("abc")){
System.out.println(str.length());
}
String str = null;
if("abc".equals(str)){
System.out.println(str.length());
}
先定义一个字符串:String str = “hello world”;
char[] c = str.toCharArray();
String str2 = str.toUpperCase();
String str3 = str.toLowerCase();
byte[] bytes = str.getBytes();
String str2 = new String(bytes);
String str = "123";
int i = Integer.parseInt(str);
String str2 = "12.3444411111";
Double d = Double.valueOf(str2);
Float f = Float.valueOf(str2);
int i = 123;
String str = String.valueOf(i);
先定义一个字符串:String str = “hello world”;
char c = str.charAt(0);
int len = str.length();
String str2 = str.trim();
注意:这里只会去除掉字符串前方和尾部的空格,字符串中间的空格不会被去除。在验证用户输入时应用较多。
String[] str2 = str.split("l");
// 参数2表示分隔成两部分
String[] str3 = str.split("l", 2);
注意:上面使用“l”对字符串进分隔,那么生成的字符串数组中将不会在出现字符“l”。有时在传输数据时会以字符串的形式传输,多个数据之间会使用“,”隔开,解析时只要使用“,”进行分隔,就可以拿到我们想要的数据了。
boolean isExistStr = str.contains("hello");
// 返回-1时表示字符串不存在,也可用此方法来判断字符串是否存在
int strLocation = str.indexOf("world");
class StringUtil{
public static String initcap(String str){
if (str == null || "".equals(null)){
return str;
}
if(str.length() == 1){
return str.toUpperCase();
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}
Date date = new Date();
// 2020-04-30
String data = String.format("%tF", date);
// 22:33:00
String time = String.format("%tT", date);
// 周四 4月 30 22:33:00 CST 2020
String dataAndTime = String.format("%tc",date);
首先可以确认一点,String类是字符串的首选类型,绝大多数情况下使用String类已足够。但是,创建成功的字符串,其内容是不能被改变的。虽然可以使用“+”来达到改变字符串的目的,但“+”会产生一个新的String实例,会在内存中创建新的字符串对象,如果重复的对字符串进行操作,将极大增加系统开销。
StringBuffer类的append()方法则可以实现字符串内容的修改,并且不会生产新的对象,StringBuffer转成String时直接调用toStirng()方法。
StringBuilder类的功能和StringBuffer类基本一致,两者最大的区别在于StringBuffer类中的方法属于线程安全的,全部使用了synchronized关键字进行了标注(从类的源代码中可以看到),而StringBuilder类属于非线程安全的。
下面的代码显示了频繁修改字符串时,String类和StringBuilder类的效率:
String str = "";
long startTime = System.currentTimeMillis();
// str引用的指向在此处将被修改10000次,并产生大量垃圾空间
for(int i = 0; i < 10000; i++){
str = str + i;
}
long endTime = System.currentTimeMillis();
long time = endTime - startTime;
// 运行结果是122
System.out.println("String消耗时间:" + time);
StringBuilder stringBuilder = new StringBuilder("");
startTime = System.currentTimeMillis();
for(int i = 0; i < 10000; i++){
stringBuilder.append(i);
}
endTime = System.currentTimeMillis();
time = endTime - startTime;
// 运行结果是1
System.out.println("stringBuilder消耗时间:" + time);
如何选择使用哪个类?
其实,根据三个类的特性来选择就好。
绝大多数编程情况下会使用String类,String类的最大特点是其内容不允许修改。
StringBuilder类是非线程安全的,但也因此效率会高于StringBuffer类,在单线程的情况下,或者不存在共享数据的情况下可以使用StringBuilder类。
StringBuffer类是线程安全的,在拥有共享数据的多条线程并行工作的情况下,可以利用同步机制来保证数据的安全,可以说是以安全换时间,效率会低于StringBuilder类,多线程的情况下应该选择
StringBuffer类。