重学java-8.String的基本概念
String的基本概念
String的匿名对象
注意:字符串常量就是String的匿名对象。
public static void main(String[] args) {
String a = "wow";
System.out.println("wow".equals(a));//这里的"wow"就是一个匿名对象,由系统自动生成
}
输出结果:
为了加深我们对String匿名对象的理解,我们再举个栗子:
public static void main(String[] args) {
String a = new String("wow");//这里的申请了两段内存
String b = "wow";
System.out.println(a==b);
}
输出结果:
之前在重学java-4.面对对象基本概念 的学习中,我们可以知道,每当new关键字出来,就代表堆中申请了一段内存。因而在这个例子中,第一个语句申请了 两段内存
。
一段是a指向的堆内存,一段是"wow"匿名对象。并且"wow"匿名对象在第一个语句结束之后,就会变成垃圾内存等待回收。
字符串的常量池
我对常量池的理解就是,为了节省各种申请空间的开销,java搞了个常量池,只要字符串在池里面,变量就可以直接引用这个字符串,不需要重复申请空间了。java7版本以后,字符串池也在堆中存储。
public static void main(String[] args) {
String a = "wow";
String b = "wow";
System.out.println(a==b);
}
输出结果:
是的,a与b所指向的内存一样,这就是因为"wow"在 编译的时候就已经放入了常量池中,后续的a与b实际上都是对常量池中"wow"的 共享
。
再看一个经典题目
public static void main(String[] args) {
String a = new String("wow");//这里的"wow"入池
String b = "wow";
System.out.println(a==b);
}
是的,正是上面匿名对象所举的例子。在这里"wow"匿名对象在 入池后
就变成了垃圾内存等待GC回收。a指向的是堆中new出来的内存,而b指向的是 之前的匿名对象"wow"
入池后的地址,因此输出为false。
如果想让a在堆中的内存在第一个语句中入池,则需要使用intern()类。
public static void main(String[] args) {
String a = new String("wow").intern();
String b = "wow";
System.out.println(a==b);
}
输出结果:
注意,intern()的用法是:“如果常量池中存在当前字符串, 就会直接返回 常量池中的
当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回常量池中的字符串
”。
举个例子:
public static void main(String[] args) {
String a = new String("wow");//匿名对象"wow"入池,a指向堆中的对象
String c = a.intern(); //c指向常量池中的对象
String b = "wow";//b指向常量池中的对象
System.out.println(a==b);
System.out.println(b==c);
}
输出结果:
String的不可变性
下面是String源码的截取
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
可以看到,在String中字符串以字符数组的形式存储(在java9之后,以byte的形式存储)。
value[]数组以关键字 final
修饰,且类中没有修改value[]数组的方法。可见String一旦定义则 不可改变
。
比如下面这个例子:
public static void main(String[] args) {
String a = "hello";
a = "bye";
}
我们并不是将"hello"字符串改变成了"bye"字符串,而只是将a的引用由"hello"指向了"bye".之后"hello"将作为垃圾等待回收。
那么为什么将String设为不可变的呢?事实上,String不可变有以下5点好处:
1-字符串池
前面我们提到,字符串在直接赋值的时候,会先去字符串池里找,找不到再申请内存定义。
所以很多时候会有许多个句柄指向字符串池的同一个常量。如果字符串是可修改的,将字符串池中的常量修改后,所有指向该常量的字符串的值将全部改变。
举个例子:
public static void main(String[] args) {
String a = "hello";//这个时候a与b指向同一个常量
String b = "hello";
}
我们看上面这个代码,a与b同时指向常量池中的"hello"字符串,如果String的值是可变的,当我们改变a值的时候,b值也会同时改变。当数据多的时候,我们将不敢修改或定义任何的字符串,因为会随时改变常量池中的内容。
2-hashcode的缓存
hash值是String的一个私有成员变量,因为它经常被使用(hashmap,hashset中),且String的不可变性,我们可以直接缓存hash值,节省了大量的运算。
3-利于其他数据结构的使用
比如我们把String放在set里,如果String是可修改的,很有可能导致set的结构混乱,无法进行去重与排序的操作。
4-安全性
String经常用于文件的传送与连接,如果String可以修改,可能会出现连接到另一个机器的情况等。
5-不可修改的对象天然是线程安全的
3、4、5本人接触较少,参考博客中的解释更为详细。总之,String的不可变性对效率与安全性都有好处。
String的基本操作
取出指定索引的字符
我们可以使用charAt()方法取出指定索引的字符,该方法源码如下:
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
前面我们提到过,String在java8中内部存储方式是字符数组
,因而上面的代码也不难理解。
举个使用的例子:
public static void main(String[] args) {
String a = "abc";
char b = a.charAt(1);
System.out.println(b);
}
输出结果:
字符数组与String的转换
举个简单的例子:
public static void main(String[] args) {
String a = "abc";
char b[] = a.toCharArray();
System.out.println(b);
String c = new String(b);
System.out.println(c);
}
输出结果:
object.toCharArray()的源码:
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
可以发现,源码中使用的是System.arraycopy()方法,这个方法我们在 重学java-5.数组的基本概念 中有详细的介绍。
字符串的比较
举个例子说明4个方法的用法:
public static void main(String[] args) {
String a = "abc";
String b = "aBc";
System.out.println(a.equals(b));
System.out.println(a.equalsIgnoreCase(b));//忽略大小写
//compare方法的返回值有三种情况:大于0表示表示a比b大,等于0表示a==b,小于0表示a比b小
System.out.println(a.compareTo(b));
System.out.println(a.compareToIgnoreCase(b));
}
equalsIgnoreCase()方法中忽略大小写的实现用到了regionMatches()方法,本质是把所有字符转化为大写进行比较。
字符串的查找
举个例子:
public static void main(String[] args) {
String a = "abcdefg";
String b = "de";
String c = "ab";
String d = "fg";
System.out.println(a.contains(b));
//indexOf()找不到的时候返回-1
System.out.println(a.indexOf(b));
System.out.println(a.indexOf(b,4));//从第4个下标开始从前往后找
System.out.println(a.lastIndexOf(b));
System.out.println(a.lastIndexOf(b, 2));//从第2个下标开始从后往前找
System.out.println(a.startsWith(c));
System.out.println(a.startsWith(b,3));
System.out.println(a.endsWith(d));
}
输出结果:
字符串的替换
举个例子:
public static void main(String[] args) {
String a = "aaaa";
System.out.println(a.replaceAll("a", "A"));
System.out.println(a.replaceFirst("a", "A"));
}
字符串的截取与连接
public static void main(String[] args) {
String a = "abcdefg";
//截取
System.out.println(a.substring(3));
System.out.println(a.substring(3, 5));
//连接
System.out.println(a.substring(3).concat(a.substring(3,5)));//等价于+
}
字符串的拆分
举个例子:
public static void main(String[] args) {
//对特殊符号的拆分
String ip = "192.168.1.111";
String a[] = ip.split("\\.");//用\\转义,防止与正则表达式冲突
System.out.println(Arrays.toString(a));
//第二个参数limit表示将字符串拆分为几段
String b[] = ip.split("\\.",2);
System.out.println(Arrays.toString(b));
//用空字符拆分
String c[] = ip.split("");
System.out.println(Arrays.toString(c));
}
输出结果:
如果点开split源码我们可以发现,它是用list与String.substring()实现的。
另外,还有一个方法在拆分的时候经常使用。举个例子:
public static void main(String[] args) {
String ip = " 192 168 1 111 ";
String a[] = ip.split("\\.");
//trim的作用是省去字符串两边的空格(中间的不省去
String b[] = ip.trim().split("\\.");
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
}
输出结果: