标签:很多 app ensure 字符数组 str 试题 arraylist already sync
来,进来的Java程序猿,我们认识一下。
我是俗世游子,在外流浪多年的Java程序猿
首先,我们来看个小栗子,保证你看了既陌生又熟悉:
public class St {
public static void main(String[] args) {
System.out.println("Hello World! ! !");
}
}
熟悉不,有没有感觉回到了刚刚入门的时刻?
上面,我们输出了一个字符串,在之后的开发生涯中,用String定义的字符串对象也频繁的出现在我们的代码中,比如下面的两种方式,就是我们使用的定义方式:
String s1 = "abc";
String s3 = "abc";
String s2 = new String("abc");
// ----- s1 == s2 true
s2.intern();
这里,我想到了一道常见的面试题:已上面为例
- s1 == s2 ?
- s1 == s3 ?
- s2一共创建了几个对象?
而且,我想让大家想一想,如果让你们来介绍String,会如何介绍呢?
String是一个不可变对象,API方法返回的其实是一个新的String对象
好,那么接下来我们就好好的剖析下这个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;
public String() {
this.value = "".value;
}
// 其他的构造方法
}
我们可以看到,整个String类被final修饰,我们都知道,被final修饰过的类或者对象
我们通过查看其构造方法可以发现,同样被final修饰的char数组存储着我们定义的字符串,看下图也可以看得出字符串的一个存储结构
所以说我们通过chatAt()
方法的下标能够得到指定的字符,原因就在于字符串是通过char数组来储存的。
同样的,char数组被final修饰,权限是private,而且没有提供对外设置的方法
但是我们要明白一点,这里的不可变指的是:底层存储char数组在内存中地址引用不可变,但是其本身的内容我们是可以通过一些手段来改变的,比如:反射
如果我们以后也想实现一个不可变的类,就可以参考String来实现
我们在看源码的时候,我们要重点看一看作者在实现一些方法时的思路,有什么地方的想法是我们可以借鉴的,好用在我们以后的代码设计中(逼格高)
作为重写方法出现频率最高的两个方法,我们就看String是如何实现的
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
为什么在计算的时候,采用的是 31 ?
很简单,我们都知道,计算机的底层数据都是0和1,而31的二进制数值正好是 11111,在计算上可以进行移位操作,效率较高
在Java开发中,我们判断两者是否相等的时候,使用的两种方式
而我们在比较字符串的时候会推荐使用equals,下面是String中的实现方式
public boolean equals(Object anObject) {
if (this == anObject) {
/**如果是当前对象,就直接返回true*/
return true;
}
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;
/**循环字符数组进行item的验证*/
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
方法实现上还是比较简单的。对比的char数组每个值是否相等。
如果以后有这样的需求:判断两者是否相等?那么我们就可以参考上面的实现,从底层结构出发,我们就可以通过底层结构快速想到贴合实际的思路。
所以说,看源码重点是学习这种思路
那有人就会问了,既然equals比较的是具体内容,那么==比较的是什么呢?
其实,如果对比项是基本数据类型, 那么对比的就是基本数据类型的值是否相等。比如 5 == 5 = true
还有对比的一点:对比的是内存空间地址是否相等。
这里应该怎么说呢?下面说
/* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
*/
public native String intern();
简单一点来说,如果定义的字符串在常量池中定义过,并且通过equals()
对比相等的话,那么就直接返回字符串在常量池中的内存地址给变量,看下图
回到最初我给大家列出的那个面试题:
String s1 = "abc";
String s3 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s3); true
System.out.println(s1 == s2); false
s2 = s2.intern();
System.out.println(s1 == s2); true
大家亲自尝试下,看看我输出的对不对
讲解这一点之前,需要先跟大家说明下String字符串在内存空间中的一个存放
String定义的字符串会存放到一个叫做常量池的地方,这个常量池在JDK1.7之后放在了堆空间中。
首先,s1="abc"
,会在常量池中开辟一块空间存放字符串abc,然后将abc的引用地址指向s1。
接下来是s3="abc"
,这里和之前有区别:如果常量池中存在当前字符串,那么就直接将当前字符串的引用地址再指向定义的对象。如果不存在,就先存放字符串然后再指向引用地址
也就是说,s1和s3虽然定义了两个变量,但是在内存空间中它们指向的地址都是一样的,所以说s1==s3
s1还是之前的s1
s2是通过new来定义出来的变量,这样在堆空间中会开辟一块新内存,构造方法传入‘abc‘字符串,常量池中的abc字符串的引用地址会先指向堆空间开辟的内存空间,然后new出来的地址再指向s2,这和直接从常量池中引用的地址肯定是不一样的。
所以s1 != s2
但是,如果调用了
intern()
之后,s2的空间地址会直接指向常量池中字符串的地址,和s1的空间地址就是一样的,所以s1和s2也就相等了这里也从侧面印证出了==还会对比内存空间中的引用地址是否相等
String s2 = new String("abc");
这里就不用多说了吧,看上面的图就知道了,创建了2个对象
上面我们说到,String是不可变对象,在进行字符串操作时每次都会产生新的对象,这样存在一些缺点:
针对这些问题,Java为我们提供了另外两个新的操作字符串的类,
从功能和API方法上讲,这两个类没有任何区别,都是用来操作字符串的类并且可以多次修改,并不会产生新的对象。
那为什么还会有两个不同的类呢? 我们通过源码来看下其中的区别
首先,两者都继承自 AbstractStringBuilder
而且,通过查看其父类源码,我们可以发现一点:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
}
StringBuffer和StringBuilder在底层存储结构上,和String没有区别,而且,两类同样被final
修饰,
唯一不同是:
StringBuffer stringBuffer = new StringBuffer(20);
StringBuilder stringBuilder = new StringBuilder(20);
还有一点:相信我们很多人都这样写过:
/**
无参方法
*/
StringBuffer stringBuffer = new StringBuffer();
StringBuilder stringBuilder = new StringBuilder();
/**
默认初始值方法
*/
StringBuffer stringBuffer = new StringBuffer("abc");
StringBuilder stringBuilder = new StringBuilder("abc");
直接看源码,通过构造方法查看两者区别:
我们抽出其中常用的一个方法来看看:append()
StringBuffer 的 append()
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuilder 的 append()
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
通过查看两者的方法,很明显的一个区别:
synchronized
, 而且如果我们翻一下StringBuffer的源码的话,我们会发现其所有的方法都存在这个关键字对线程有了解的童鞋都知道这个关键字的意义:同步锁,对应方式是同步方法
所以说,如果我们在多线程环境下需要对字符串进行操作的话,那么优先推荐采用StringBuffer,其他方面的话,对效率没要求两者都行,否则的话,就推荐采用StringBuilder
关于append(),这里要额外说一点:
我们前面说过,StringBuffer和StringBuilder构造方法是会传递数组的初始长度,那么我们来看看append方法是如何进行长度扩容的(没错,不止ArrayList会扩容):
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
针对数组扩容,很简单的逻辑:
到这里,我们关于字符串的内容也就完结了,关于具体的API方法,推荐直接查看官方文档:
标签:很多 app ensure 字符数组 str 试题 arraylist already sync
原文地址:https://blog.51cto.com/14948012/2541114