我是一名很普通的双非大三学生,跟很多同学一样,有着一颗想进大厂的梦。接下来的几个月内,我将坚持写博客,输出知识的同时巩固自己的基础,记录自己的成长和锻炼自己,备战2021暑期实习面试!奥利给!!
以主流的JDK版本1.8来说,String内部实际存储结构为char数组,源码如下:
public final class String implements java.io.Serializable,Comparable<String>,CharSequence{... /** The value is used for character storage. */ private final char value[]; /** 用来缓存Hash,避免每次都需要去重复计算 */ private int hash; // Default to 0 ...
注意:JDK9以后,不再是char[]数组了,而是使用byte数组,因为可以减少一半的内存,byte使用一个字节来存储一个char字符,char使用两个字节来存储一个char字符。只有当一个char字符大小超过0xFF时,才会将byte数组变为原来的两倍,用两个字节存储一个char字符。
这里我们可以看到String类型其实是被 final关键字
修饰的,这也是我们要探究的第一个问题
我们先来看一段简短的代码
String s1 = "final"; s1 = "test";
通过Debug我们可以看到,实际上value数组的引用是改变了的,也就说 s =“test” 这个看似简单的赋值,其实已经把 s 的引用指向了新的 String。
首先,我们在编码中,我们知道 如果你认为这个类已经定义完全并且不需要任何子类的话,可以将这个类声明为Final,Final类中的方法将永远不会被重写 。在Java中也是这样,String是被设计成一个不可变(immutable)类,一旦创建完后,字符串本身是无法通过正常手段被修改的。
第三点注意下,我们说的是无法修改其内存地址,并没有说无法修改其值。因为对于 List、Map 这些集合类来 说,被 final修饰后,是可以修改其内部值的,但却无法修改其初始化时的内存地址。
例子我们就不举了,本文的String 的不变性就是一个很好的例子。因为 String 具有不变性,所以 String 的大多数操作方法,都会返回新的 String,这里我们可以参考substring
public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }
我们可以看到,返回的其实是一个新的String,不会让你去修改内部的内容。
但是这还是没点到为什么要设计成不可变类的,我们并不是Java的设计者,所以我们只能综合好处去概述这样做的原因:
但是不可变并不是完全不可变,如果你非要变,通过反射也是可以实现的,例如:
String str = "不可变"; System.out.println(str); Field field = String.class.getDeclaredField("value"); field.setAccessible(true); char[] value = (char[]) field.get(str); value[0] = '可'; value[1] = '以'; value[2] = '变'; System.out.println(str); 输出结果: 不可变 可以变
因为String类型是不可变的,所以在字符串拼接的时候如果使用String的话性能会很低,因此我们就需要使用另外的数据类型 StringBuffer
,它提供了append和insert方法可用于字符串的拼接,StringBuffer使用synchronized来保证线程安全, 所以性能不是很高。
列举一个代码片段:
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
于是在JDK 1.5有了StringBuilder,它同样提供了append和insert的拼接方法,但它没有使用synchronized来修饰,因此在性能上要优于StringBuffer,所以在非并发操作的情况下可以使用后者。
这里还可以扩展一个什么情况下用+号,什么时候用StringBuilder、StringBuffer
String常见的创建方式有两种, String s1 = "Java"
和 String s2 = new String("Java")
的方式,两者在JVM的存储区域却截然不同,在JDK 1.8中,s1会先去字符串常量池中找字符串"Java” ,如果有相同的字符则直接返回常量句柄,如果没有此字符串则会先在常量池中创建此字符串,然后再返回常量句柄;而变量s2是直接在堆上创建一个变量 ,如果调用 intern方法
才会把此字符串保存到常量池中, intern
还是很少用到的,一般也只出现在面试题里,不用深究。如下代码所示:
String s1 = new String("南街"); String s2 = s1.intern(); String s3 = "南街"; System.out.println(s1 == s2); // false System.out.println(s2 == s3); // true
在 JDK 8 之后,取消了永久代的概念,取而代之的实现是元空间(MetaSpace), 原本位于永久代中的字符串常量由永久代转移到堆中 。元空间的本质和永久代类似,都是对JVM规范中方法区的实现,它们之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
这里就不对方法的使用一一例举了,还是得多动手自己实践呢!
还有replaceFirst() 替换匹配到的第一个字符;
这里要注意replace和replaceAll的区别,前者的参数是char和CharSequence,即可以支持字符的替换,也支持字符串的替换(CharSequence即字符串序列的意思,说白了也就是字符串)
replaceAll的参数是regex,即基于规则表达式的替换,比如:可以通过replaceAll("/d", "*")把一个字符串所有的数字字符都换成星号;
== 对于基本数据类型来说,是用于比较"值” 是否相等的;但对于引|用类型来说,是用于比较引用地址是否相同的。那么我们就要看equals和它的区别了,我们知道Java里Object类是所有类的父类,equals方法也是Object的方法,源码如下:
public boolean equals(Object obj) { return (this == obj); }
显而易见,没有重写的equals方法本质上也就是 == ,但是我们知道String类型在比较的时候,老师都教过,用equals,这是为什么呢?就是因为String已经重写equals方法,源码如下:
public boolean equals(Object anObject) { // 先判断引用 if (this == anObject) { return true; } // 在判断是不是String类型 if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; // char 一个一个对比 while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
这里不深究hashCode和equals的关系,下次专门在写
通过问题讲解知识点,看完这本文章,出几道题考考?