1.String基础
1.1 认识String
String是Java中的一个类,属于引用数据类型,遵循引用类型的基本规律和要求。但是它也有自己的一些特点,例如用双引号去表示一个字符串,一般用双引号括起来的一串字符都是String的对象,如"hello world","java"
字符与字符串也有一定的关系,String字符串的内部是通过一个private final char[]数组来实现数据存储的,所以可以通过以下方式去表示一个字符串:
String string = new String(new char[]{'h','e','l','l','o'});
1.2 创建String的方式
1.2.1 通过构造方法创建
String类种有11种构造方法,这些方法提供不同的参数来初始化字符串,比如提供一个字符数组参数:
char[] charArray = new char[]{'h','e','l','l','o'};
String string = new String(charArray);
这种方式创建过于复杂,实际开发中很少使用
1.2.2 直接赋值
String string2 = "hello";
当编译器遇到字符串常量时,这里的常量是"hello",编译器会使用该值创建一个String对象。
String string2 = "";
String string3 = null;
System.out.println("string2 = " + string2);
System.out.println("string3 = " + string3);
使用""赋值时,是有对应的string对象创建的,但是使用null去赋值,就没有对象创建,即字符串对象的引用指向的是null。
需要注意的是,String类事不可改变,所以一旦创建了String对象,那它的值就无法被改变了,如果需要对字符串做很多修改,那么可以选择StringBuffer 或 StringBuilder 类, 关于这两者的用法,后续会介绍
2.String的基本用法
2.1 常用方法总结
常用方法如图:
转化成代码:
//string的常用方法
String str = "hello world";
//int length()
System.out.println("字符串的长度是 " + str.length());
System.out.println("l字符第一次出现的位置是 " + str.indexOf('l'));
System.out.println("ll字符串第一次出现的位置是 " + str.indexOf("ll"));
System.out.println("l字符最后一次出现的位置是 " + str.lastIndexOf('l'));
System.out.println("字符串从第一位到最后一位切割后的结果为" + str.substring(1));
System.out.println("字符串从第一位到第三位切割后的结果为 " + str.substring(1, 3));
System.out.println("与字符串hello是否相等" + str.equals("hello"));
System.out.println("将字符串转化为大写" + str.toUpperCase());
System.out.println("获取字符串第一位的字符" + str.charAt(1));
System.out.println("将字符串按照ll切割,并获取其结果 " + Arrays.toString(str.split("ll")));
System.out.println("转化为字符数组 " + Arrays.toString(str.getBytes()));
运行结果如下:
2.2 字符串与byte数组的相互转化
//创建字符串
String str = "hello world";
//使用utf-8 的方式 转化为字符数组
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < bytes.length; i++) {
System.out.println(bytes[i]);
}
//将bytes数组转化为字符串
String str2 = new String(bytes);
System.out.println(str2);
2.3 == 运算符与equals的区别
先说下结论:== 运算符比较的是地址,equals比较的是值
String str1 = "123";
String str2 = "123";
String str3 = new String("123");
System.out.println("str1 与 str2 ==比较" + (str1 == str2));
System.out.println("str1 与 str2 equals比较" +(str1.equals(str2)));
System.out.println("str1 与 str3 ==比较" +(str1 == str3));
System.out.println("str1 与 str3 equals比较" +(str1.equals(str3)));
运行结果如下:
在string的创建方式中有提到过,直接赋值就是将常量赋值给对象,所以str1与str2 的值与地址都相同,即== 和 equals都是相等的,如果通过构造方法去创建string对象,类似str3的创建方式,对象的地址很明显就不相等了,即 == 比较出来的地址是不同,而equals比较的是值,呈现的结果是相等的。
2.4 字符串的不可变性
String对象一旦被创建,则不能被更改,是不可变的
所谓的修改只是创建了新的对象,但是所指的内存空间不变
那什么是不可变性呢?
先讲结论:String不可变很简单,给一个已有字符串"abcd"第二次赋值成"abcedl",不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。
打开String的源码
可以看到String是被final 修饰的,所以final是不可被继承的,下面的value数组也是被final修饰的,所以value数组的地址初始化后也是不可变的。
只是数组的地址不可变,我们是可以修改内部元素的值的,从而去实现修改字符串吗?
final int[] value={1,2,3} ;
int[] another={4,5,6};
value=another; //编译器报错,final不可变 value用final修饰,编译器不允许我把value指向堆区另一个地址。
但如果我直接对数组元素动手,分分钟搞定。
final int[] value={1,2,3};
value[2]=100;
//这时候数组里已经是{1,2,100} 所以String是不可变,关键是因为SUN公司的工程师。
//在后面所有String的方法里很小心的没有去动Array里的元素,没有暴露内部成员字段。
//private final char value[]这一句里,private的私有访问权限的作用都比final大。
//而且设计师还很小心地把整个String设成final禁止继承,避免被其他人继承后破坏。
//所以String是不可变的关键都在底层的实现,而不是一个final。
//考验的是工程师构造数据类型,封装数据的功力。
那不可变有什么好处呢?
1.首先final修饰的类只保证不能被继承,并且该类的对象在堆内存中的地址不会被改变。
2.但是持有String对象的引用本身是可以改变的,比如它可以指向其他对象。
3.final修饰的char数组保证了char数组的引用不可变,但是可以通过修改数组中的元素去修改值,不过String内部并没有提供相应的方法去实现这一操作,所以String的不可变也是基于代码封装和访问控制的。
3. StringBuilder 与 StringBuffer
3.1 StringBuffer
3.1.1 StringBuffer简介
StringBuffer是一种可变的字符串类,即在创建StringBuffer对象后,我们还可以随意修改字符串的内容。每个StringBuffer的类对象都能够存储指定容量的字符串,如果字符串的长度超过了StringBuffer对象的容量空间,则该对象的容量会自动扩大。
另外我们在使用StringBuffer类时,比如每次调用toString()方法,都会直接使用缓存区的toStringCache 值来构造一个字符串,这每次都是对StringBuffer对象本身进行操作,而不会重新生成一个新对象。所以如果我们需要对大量字符串的内容进行修改,推荐大家使用StringBuffer。
3.1.2 StringBuffer特性
1.具有线程安全性:StringBuffer中的公开方法都由synchronized关键字(一种同步锁)修饰,保证了线程安全。
2.带有缓冲区:StringBuffer每次调用toString()方法时,都会直接使用缓冲区的toStringCache值来构造一个字符串。
3.内容可变性:StringBuffer可初始化容量,也可以指定容量,当字符串超过规定容量后可自动扩容。
4.内容类型多样性:StringBuffer中可以存储多种不同数据类型的数据。
3.1.3 基本用法
public static void stringBuffer(){
//创建StringBuffer对象
StringBuffer sb = new StringBuffer("hello");
//在字符串后面追加新的字符串
sb.append(" world");
System.out.println(sb);
//删除指定位置上的字符串,从指定的下标开始和结束,下标从0开始
sb.delete(2, 4);
//"ll"
System.out.println(sb);
//在指定下标位置上添加指定的字符串
sb.insert(0, "123");
//123heo world
System.out.println(sb);
//将字符串翻转
sb.reverse();
//dlrow oeh321
System.out.println(sb);
//将StringBuffer转换成String类型
String s = sb.toString();
System.out.println(s);
}
3.2 StringBuilder
3.2.1 StringBuilder 简介
要想实现可变字符串的操作,其实还有另一个StringBuilder类,该类是在java5种被提出来的。它和StringBuffer的基本用法几乎完全是一样的,所以这里就不讲解太多了。
但是StringBuilder与StringBuffer最大的区别在于,StringBuilder的各个方法都不是线程安全的,再多线程的情况下会存在线程安全的问题,与之对应的StringBuilder的执行效率要比StringBuffer快很多。
在大多数情况下,我们都是在单线程的环境下进行字符串的操作,所以使用StringBuilder并不会产生线程安全问题,针对大多数的单线程情况,还是建议大家使用StringBuilder,除非项目对线程安全有着很明确的高要求。
3.2.2 StringBuilder特性
1. StringBuilder是线程不安全的,但执行效率更快。
2.适用于单线程环境下,在字符缓冲区进行大量操作的情况。
3.2.3 StringBuilder基本用法
与StringBuffer基本一致,这里就不再赘述。
3.3 String、StringBuilder和StringBuffer的区别
String是Java中基础且重要的类,被声明为final class,除了hash这个属性其它属性都声明为final,因为它的不可变性,所以例如拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
而StringBuffer/StringBuilder就是为了解决大量拼接字符串时产生很多中间对象问题而提供的类,二者都继承了 AbstractStringBuilder ,底层都是利用可修改的char数组(JDK 9 以后是 byte数组)。
所以如果我们有大量的字符串拼接,如果能预知大小的话最好在new StringBuffer 或者StringBuilder 的时候设置好capacity容量,避免多次扩容的开销。扩容要抛弃原有数组,还要进行数组拷贝创建新的数组。
这里需要注意以下几点:
1.在字符串不经常发生变化的业务场景优先使用String(代码更清晰简洁)。如常量的声明,少量的字符串操作(拼接,删除等)。
2.在单线程情况下,如有大量的字符串操作情况,应该使用StringBuilder来操作字符串。不能使用String"+"来拼接而是使用,避免产生大量无用的中间对象,耗费空间且执行效率低下(新建对象、回收对象花费大量时间)。如JSON的封装等。
3.在多线程情况下,如有大量的字符串操作情况,应该使用StringBuffer。如HTTP参数解析和封装等。