1、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
...
}
- String被final关键字修饰,意味着String类不能被继承,并且它的成员方法都默认为final方法;字符串一旦创建就不能再修改。
- String类实现了Serializable、CharSequence、 Comparable接口。
- String实例的值是通过字符数组实现字符串存储的。
由上可以看出,Java中使用"+"连接字符串对象时,会创建一个StringBuilder()对象,并调用append()方法将数据拼接,最后调用toString()方法返回拼接好的字符串。
由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用append()方法手动拼接(如上面例子如果使用手动拼接运行时间将缩小到1/200左右)。
2、hashmap、hashtable、ConcurrentHashMap底层?分段锁?为什么要重写hashcode和equal?(网易)
2.1、为什么要重写hashcode和equal
Java中的超类Object类中定义的equals()方法是用来比较两个引用所指向的对象的内存地址是否一致,Object类中equals()方法的源码:
public boolean equals(Object obj) {
return (this == obj);
}
Object类中hashCode()方法的源码:
public native int hashCode();
Object类中的hashCode()方法,用的是native关键字修饰,说明这个方法是个原生函数,也就说这个方法的实现不是用java语言实现的,是使用c/c++实现的,并且被编译成了DLL,由java去调用,jdk源码中不包含。对于不同的平台它们是不同的,java在不同的操作系统中调用不同的native方法实现对操作系统的访问,因为java语言不能直接访问操作系统底层,因为它没有指针。
重点要注意的是:
- a. 在java应用程序运行时,无论何时多次调用同一个对象时的hashCode()方法,这个对象的hashCode()方法的返回值必须是相同的一个int值;
- b. 如果两个对象equals()返回值为true,则他们的hashCode()也必须返回相同的int值;
- c. 如果两个对象equals()返回值为false,则他们的hashCode()返回值也必须不同;
如果hashcode()相等,equals()未必相等。
现在,我们到这里可以看出,我们重写了equals()方法也要重写hashCode()方法,这是因为要保证上面所述的b,c原则
2.2、hashmap存储过程
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public V put(K key, V value) {
//数组为空时创建数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//key为空单独对待
if (key == null)
return putForNullKey(value);
//①根据key计算hash值
int hash = hash(key);
//②根据hash值和当前数组的长度计算在数组中的索引
int i = indexFor(hash, table.length);
//遍历整条链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//③情况1.hash值和key值都相同的情况,替换之前的值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
//返回被替换的值
return oldValue;
}
}
modCount++;
//③情况2.坑位没人,直接存值或发生hash碰撞都走这
addEntry(hash, key, value, i);
return null;
}
- 判断当前数组是否需要初始化。
- 如果 key 为空,则 put 一个空值进去。
- 根据 key 计算出 hashcode。
- 根据计算出的 hashcode 定位出所在桶。
- 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。
- 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置
2.3、ConcurrentHashMap底层,分段锁
2.3.1、 1.7 的实现
如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。
它的核心成员变量:
和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。
原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
2.3.2 1.8的实现
1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。
那就是查询遍历链表效率太低。
因此 1.8 做了一些数据结构上的调整。
首先来看下底层的组成结构:
看起来是不是和 1.8 HashMap 结构类似?
其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized
来保证并发安全性。
也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。
其中的 val next
都用了 volatile 修饰,保证了可见性。
put 方法
重点来看看 put 函数:
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
f
即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 - 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于
TREEIFY_THRESHOLD
则要转换为红黑树。