在JAVA开发中,使用最多的数据类型恐怕是String 和 HasMap两种数据类型。在开发的过程中我们每天都使用的不亦乐乎。但是相信很多人都没有考虑过String数据类型的实现原理或者说是在数据结构中的存储原理,还有一个就是是HashMap,也很少有人去了解。而且随着jdk版本的发展,这两个类型的实现都一直在优化,而不是一成不变的。有时候只有在找工作面试的时候被面试官问题,然后是一脸蒙蔽。每天都在用,也不用考虑它的原理啊。懂得用不就行了吗?这句话其实对其实也不对,对于实现业务来说,是已经足够了。但是对于对每一行代码做到极致的性能,很多东西还是需要知道它的原理。今天这一节就来梳理梳理String和HashMap的实现原理。
String数据类型:
无处不在的String类型。它位于java jdk 的lang包中,所以无需import就可以直接使用。从源码上看String是被final修饰的,所以它是不能被继承的。
/** ID */
private Integer id;
/** 用户姓名 */
private String userName;
/** 登录名 */
private String loginName;
/** 密码 */
private String password;
public User() {
super();
}
public User(Integer id, String userName, String loginName, String password) {
super();
this.id = id;
this.userName = userName;
this.loginName = loginName;
this.password = password;
}
由于其使用的广泛性,几乎在每一代的JDK优化升级中都存在对String的优化。
首先在JDK1.7中,我们都知道 String s = "maoheyeren";这个对象是存在常量池中的,JDK1.6的常量池位于方法区。但是到了JDK1.7,就讲常量池优化到了堆中,因为堆的内存更大,方便常量池的扩展。
在JDK1.8和JDK1.8以前,我们可以看一下源码,它的构造函数,可以知道String的下一层是使用char类型存储,但是到了JDK9就使用了byte[]存储,进行了优化减少了存储空间消耗。
所以就是大家平时天天使用的String类型,也不是一层不变的!这个需要注意,算法的优化永无止境!
String对象的创建方式:
String s = "茅河野人是大神";
String s = new String ("茅河野人是大神")
String的源码,实现了序列化、Comparable以及CharSequence接口。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 在JDK1.8中String的值使用char[]数组保存
private final char value[];
// 使用私有成员变量hash来缓存String的哈希值
private int hash; // Default to 0
// 构造方法
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
...省略
}
还有一个比较关心的问题String到底能存多长的数据。
查看String源代码返回长度的方法,int类型所能表示的最大范围为[0, 2^31-1],实际根据String的两种创建方式还有所不同。
1、使用String s = "茅河野人是大神"; 方式创建对象时javac编译会校验字符串的长度,0xFFFF所能表示的最大长度为2^16=65536,因此这种方式创建的字符串长度会小于65536。而且CONSTANT_Utf8_info型常量的最大长度是是65535 - 1 = 65534个字节,若是中文字符,长度为65535 / 3字节。如果运行时方法区设置的比较小,实际长度可能达不到理论字节。
2、使用new关键字创建对象时,对象在堆内存中分配空间,调用系统的copyOf(),方法,所支持的理论最大长度为Integer.MAX_VALUE,2^31-1;实际情况受虚拟机和堆内存的大小限制。
HashMap数据类型:
HaspMap我们天天往里面put东西,put字符串,put对象,用的时候就get出来,也是从来没思考过里面的实现原理。
JDK1.8 之前 HashMap 的数据结构是 数组+链表 。数组是 HashMap 的主体,单向链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)时,且tab.length>=64时,将链表转化为红黑树,以减少搜索时间。
注意,HashMap的底层数据结构中链表是单向链表,也就是只有next指针,没有prev指针。而LinkedHashMap维护了before、 after、head以及tail。
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash值 判断当前元素存放的位置(这里的 n 指的是数组的长度)。
如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖;不相同就通过拉链法解决冲突。
hashcode的计算算法:
hash(Object key)–扰动函数
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法造成的碰撞,换句话说使用扰动函数之后可以减少碰撞。
先获得key的hashCode的值 h,然后 h 和 h右移16位 做异或运算。
实质上是把一个数的低16位与他的高16位做异或运算,因为在 (n - 1) & hash 的计算中,hash变量只有末x位会参与到运算。使高16位也参与到hash的运算能减少冲突。
JDK1.7之前的hashMap存储结构:
到了JDK1.8以后进行额优化,这一点经常有些找事情的面试官会问到。可以看出HashMap以链表的方式存储在数组中,到了JDK1.8以后,以红黑树的方式存储在数组中。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}