Java之字符串(String)全解析

字符串的本质

编程过程中,虽然字符串经常被像操作基本数据类型那样来使用,但实质上任何编程语言都没有提供字符串这种基本数据类型,字符串用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对象(常量)池

String对象(常量)池的目的是实现数据的共享处理,在Java中对象(常量)池分两种:

  • 静态常量池:程序(*.class)在加载的时候会自动将此程序之中保存的字符串、普通的常量、类和方法的信息等等静态数据,全部进行分配
  • 运行时常量池:当一个程序(*.class)加载之后,里面可能有一些变量,这时候提供的常量池叫做运行时常量池

举例:

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()的区别

  • “==”:进行的是数值上的比较,如果用在对象的比较上,比较的是两个对象的地址的数值是否相等。
  • equals():是类提供的一个比较方法,可以直接进行字符串的内容的判断。注意:比较时区分大小写。

引申内容

观察下面两段代码,第一段是将字符串常量放到了括号内,第二段将字符串常量放到了前面(推荐)。
正常情况下,两段代码不会有什么问题,但是,如果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);
  • 字符串转int、double、float
String str = "123";
int i = Integer.parseInt(str);
String str2 = "12.3444411111";
Double d = Double.valueOf(str2);
Float f = Float.valueOf(str2);
  • int、double、float转字符串(一样的操作)
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");
  • 实现字符串首字母转大写(String类没有提供这个方法,但可以结合其它方法来实现)
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、StringBuffer、StringBuilder的区别

首先可以确认一点,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类。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章