现在写没有见过的程序基本上都是网上查找,然后copy,这样找个人培训一下也能够做到,那么你的核心竞争力到底在哪呢?答案就是基础。基础牢固的童鞋更容易快速上手和掌握理解新东西。
1. equals和hashcode问题。
在effective java中有这样一条:覆盖equals时总要覆盖hashCode。
在什么场景下会需要去覆盖这两个方法呢?究竟为什么需要在覆盖equals时去覆盖hashCode呢?
还记得之前有过一个场景:网络上有一段交易信息,每3秒刷新一次,我的程序需要记录下来上一次刷新的结果以便跟本次刷新做一些比较和运算。为了方便快捷,我使用了SortedDirectory(可以认为是一个LinkedHashMap)来保存上一次交易记录的信息,当本次记录出现时,我只需要用directory.contains(transaction)这样的方法就能快速的判断是否上次交易记录在本次还存在,另外还需要在其他地方用transaction.equals(transaction2)这样的方法判断两个交易记录是否相等。于是我需要重写这两个方法以适应上面两个需求。
那么为什么要在覆盖equals时也覆盖hashCode呢?在java中有3个Hash集合,分别是HashMap,HashSet(其实就是一个HashMap),Hashtable,当这3个集合判断对象是否存在本集合时会判断hashCode是否相等,以及key是否相等,请看HashMap的代码,在保存(获取,是否存在)一个元素时执行如下程序。如此一来,我们就明白,两个equals但hash不一样的元素放到HashMap中实际上是不同的,这会导致一个问题,即将a对象放到HashMap中后,再以a为key则不能取到值。这是很奇怪的现象,因此java规范规定覆盖equals时需要同时覆盖hashCode。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
好了,那么我们该如何覆盖一个hashCode方法呢?在effective java中有一大段篇幅描述这部分内容,实际上每种类型如何写他的hashCode可以参考该类型对象的源码,比如float, String。其中,对String或者其他对象,可以将其看做是一堆子对象的集合(String是char的集合),对每个对象做h=31*h+val[i]。
public int hashCode() {
return floatToIntBits(value);
}
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;
}
那么hash这个方法为什么要乘31这个数呢?要知道,31是个魔数,一方面它是质数,另一方面它可以通过移位运算获得,即1<<5-1,当一个数a乘31时,等价于a<<5 - a,这比实际计算a * 31要快。17其实也可以看做是1<<4+1,为什么没选择17,原因应该是希望散列的时候范围更大更均匀。
2. hashmap问题。
HashMap是java中常用的集合对象之一。正因为经常使用,且关乎性能,因而需要更好的了解它。我们可以从如下几个方面去深入了解HashMap:
1. 内部的数据结构是什么?
实际上,HashMap的内部使用了一个transient Entry<K,V>[] table;来存储键值对。通过table[hash & (table.length-1)]来迅速定位到table中某个具体的Entry,为什么是hash & (table.length - 1),其实这是取模的位操作算法。由于Hash会存在冲突,这里解决冲突的方式就是线性探测法,即顺着冲突的Entry链表寻找键值和hash值相同的Entry,所以,这里是有一个循环的,可以参考getEntry方法
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
2. 接下来的问题是table的容量如何确定,loadFactor是什么,该容量如何变化,这种变化会带来什么问题?
table数组大小是由capacity这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30。
loadFactor是装载因子,主要目的是用来确认table是否需要动态扩展,默认值是0.75,比如table数组大小为16,装载因子为0.75时,threshold就是12,当table的实际大小超过12时,table就需要动态扩展。
扩展(addEntry方法)时会调用resize方法,将table长度变为原来的两倍(注意是table长度,而不是threshold)
扩展主要是resize方法中的transfer,transfer主要做两件事,一是将原table中的元素拷贝到新table,二是将数组中的所有元素重新添加到新table中(如有必要还需要再hash)。从这里可以看出来,如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。
以下是我机器上的一组数据,我使用uuid向hashmap插入数据,并且监控等待时间超过1秒的数据,可以发现,这个等待时间在900w数据时是相当惊人的:
插入数量 | 等待时间(ms) |
1751814 | 1382 |
2947305 | 1772 |
4058029 | 2349 |
6959909 | 4328 |
9514029 | 5037 |
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
可以注意到,最后的一些移位和异或是为了更均匀的散列。另外,如果开启了useAltHashing(即capacity超过了jdk.map.althashing.threshold值),sun.misc.Hashing.stringHash32((String) k)会让不同时间的散列不同从而避免碰撞和攻击。
3. 深拷贝和浅拷贝如何实现
所谓深拷贝即将对象中所有关联的对象都复制一份,两者是独立的;所谓浅拷贝即对象中所有关联的对象都不做复制,取而代之所有关联的对象都指向关联对象的引用。
浅拷贝依靠Object的clone方法实现,深拷贝有两种方式,一是覆盖clone方法,对对象中的对象引用做拷贝,然后赋值到clone后的对象;二是依靠ObjectOutputStream和ByteArrayOutputStream对象写出,再通过ObjectInputStream和ByteArrayInputStream对象读入,从而实现对象的深拷贝。下面是一个做深拷贝的例子(序列化对象会占用更多时间,如果需要大量循环请慎用)。
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.close();
ByteArrayInputStream bis = new ByteArrayInputStream(bos
.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
o = ois.readObject();
ois.close();
来看一个实际的问题:
以下方法是浅拷贝吗?
List desc = new ArrayList(List c)
Collections.copy是另一种拷贝方法,那么,如下的程序正确吗?这种拷贝是深拷贝还是浅拷贝呢?
List src1 = new ArrayList(3);
src1.add("a");
src1.add("b");
src1.add("c");
List des1 = new ArrayList(3);
System.out.println(src1.size() + " " + des1.size());
Collections.copy(des1,src1);
实际上,以上两种方式都是浅拷贝,调试结果两者list对象指向同一地址。归结下来,第一种方式最终用的是Arrays.copyof,第二种方式则是一个循环将src1中的元素赋值到des1中,实际上也是同一份内容。
另外,上面的程序不能正常运行,Collections.copy首先会比较src1和des1的size大小,当src1.size>des1.size时会抛出IndexOutofBound异常。如要修改这个问题,应该写作如下
List des1 = new ArrayList(Array.asList( new object[src1.size]));
4. Integer的坑
Integer这个类有个坑,比如这段程序
Integer it1 = Integer.valueOf(123);
Integer it2 = Integer.valueOf(123);
System.out.println(it1 == it2);
it1 = Integer.valueOf(129);
it2 = Integer.valueOf(129);
System.out.println(it1 == it2);
不知道你会不会想到第一个输出的是true而第二个是false。这非常让人费解,但当你看他的是实现就会发现,Integer中有一个IntegerCache,缓存了从-128到127之间的Integer对象,当你使用valueOf时,如果是在缓存范围内的,直接返回缓存对象,否则,new一个新对象给你。所以有上面这个例子。以下是他的逻辑
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
另外,Integer有非常方便的方法可以将整数转为2进制,8进制,16进制的字符串,这在网络编程的时候尤其有用。程序如下:
System.out.println(Integer.toBinaryString(123));
System.out.println(Integer.toOctalString(123));
System.out.println(Integer.toHexString(123));
这三个方法都是调用同一个方法去生成字符串,这里digits是一张表,如果注意看会发现这里居然存了超出f的字母,猜想是为了扩展性,这样以后可以支持32进制。牛x的程序是buf[--charPos] = digits[i & mask];
private static String toUnsignedString(int i, int shift) {
char[] buf = new char[32];
int charPos = 32;
int radix = 1 << shift;
int mask = radix - 1;
do {
buf[--charPos] = digits[i & mask];
i >>>= shift;
} while (i != 0);
return new String(buf, charPos, (32 - charPos));
}
5. 软引用,弱引用和虚引用
首先是强引用:当内存不足时,虚拟机宁愿抛出OOM异常也不会回收强引用对象的空间。
软引用仅次于强引用:当内存不足,要抛出OOM之前,虚拟机首先回收软引用对象所占空间,如果空间足够,则不会抛出OOM从而继续运行下去。
弱引用再次于软引用:每次虚拟机做gc时,都会回收弱引用对象所占之空间,相对软引用,弱引用的生命周期更短。
虚引用很类似软引用,也是gc可以任意回收,虚引用并不决定对象的生命周期。
其实主要是了解这几种引用用在什么地方。
软引用最适合的地方是缓存。通常我们希望缓存既要尽可能多的保存对象,又要不影响正常引用的使用。所以这个时候使用软引用可以很好的完成这点。当我们用缓存保存对象时,如果遇到内存不足,则自动回收缓存内的对象,如此一来,就不会影响应用,达到透明收缩的目的。
弱引用可能会用在优化器或者调试器,一般这些程序希望透明的观察对象而不能对对象有额外的影响。
虚引用会用来跟踪对象的垃圾回收活动。
hashCode的性能优化 http://it.deepinmind.com/java/2014/03/31/hashcode-method-performance-tuning.html
java面试 https://codejuan.gitbooks.io/java_interview/content/io/syn-ays-blocked/bio-nio-aio.html
你应该知道的JAVA面试题 http://ifeve.com/java-interview-question/comment-page-1/
各大公司Java后端开发面试题总结 https://www.jianshu.com/p/f29f52726c87
超详细的Java面试题总结(二)之Java基础知识 https://juejin.im/post/5a339d936fb9a04501680492