1、List 和 Set 的区别
1.1 异同点
首先List最重要的特点是能保证数据存入的顺序性,而set由于是根据hash映射的地址,因此是无序的。
其次,list可以存储相同的内容,允许多个null值,set不允许数据重复,只允许一个null值。
List和set都是集成Collection接口的。
List包括,ArrayList、LinkedList、Vector.其中ArrayList类似于数组,可以随意访问,适合查找频繁的场景,而LinkedList则适合插入删除频繁的场景。
Set包括,HashSet,TreeSet,LinkedSet;HashSet是基于HashMap实现的,TreeSet可以对保存的数据自动排序。
1.2、List、Set的使用场景
如果知道数据的索引位置,那可以用List中的ArrayList,可以很快的查找到数据。如果经常的增删数据则用LinkedList。
如果希望容器中的数据按照插入顺序进行存储,比如最近访问列表等,需要按照时间的先后顺序存储的,就可以用List。
如果是希望容器中的数据是不重复的,只保留一份,那就使用set,其中TreeSet还支持对存入数据进行排序。
2、HashSet 是如何保证不重复的
HashSet是由HasnMap实现的,可以直接看源码中HashSet的构造函数,直接new了一个hashMap
public HashSet() {
map = new HashMap<>();
}
所以,HashSet和HashMap的去重方式是一样的,可以看一下HashSet的add方法,直接调用了hasMap.put
方法,如下:
public V put(K key, V value) {
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;
}
可以看到上面的主要判重逻辑代码,首先,判断了原有元素e.hash与新传入key的hash,e是已存在的键值对中的元素,e.hash实际就是已经存在的key值的hash值。判断完后,判断key是否相同。如果重复,则用新的值替换旧的值。
3、HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?
HashMap不是线程安全的。
详细说明为什么会出现线程不安全->传送门
线程不安全主要是由于HashMap有扩容机制,HashMap的初始大小是16,如果超过了16,就会进行扩容,在扩容的过程中,如果两个线程同时达到了需要进行rehash的时候,会对链表进行重排,此时,在取链表数据时,可能会发生链表循环。以下是源码:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);//此处会进行数据重新存放到一个新的数组里面去。
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* 如果两个线程同时到达该方法,此时对于两个线程来说newTable是固定,且都保存一份数组
* 如果其中一个线程正确执行完,另外一个线程就有可能造成链表环。
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
4、HashMap 的扩容过程
hashmap有threshold,如果当前数组容量大于threshold,就会进行数组扩容,扩容大小是当前数组长度的2倍。
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
阈值等于 容量乘以负载因子与最大容量中较小的值。
数组扩大后,会对原有hashmap重新hash并定位。
5、HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?
hashMap1.8在hashMap的链表中增加了红黑树,如果链表长度超过了8,就会调用方法把 链表转换为红黑树,如果链表长度小于
6、final finally finalize
final是修饰符,可以修饰在类上,表示该类不能被继承;修饰在方法上,表示该方法不能被重写,但是可以重载;修饰在变量上面,如果是基本类型,值不能变,是对象的话,表示该变量引用不能被修改。
finally 是异常捕获时候使用的,一般在finally中执行关闭资源操作,即使在try中return了,也会执行finally中的代码。实际finally是在return前执行的。
finalize是垃圾回收时显示声明用的,finallize方法调用了,不一定会执行gc过程。
7、强引用 、软引用、 弱引用、虚引用
强引用:
Object o=new Object();
这是最常见的实例化对象的方法,这种方法就是强引用,除非设置 o = null;否则在垃圾回收时不会被清理。
软引用:
SoftReference:
会在内存溢出之前进行一次检查,将所有软引用内存释放。一般软引用可以用来做临时缓存;应用场景比如,需要读取经常使用的图片,图片可以缓存在内存中,加快读取速度,也可以从远程下载,放在内存中的话,因为图片size比较大,容易引起OOM,可以使用softReference,在发生内存溢出时才清理。此时如果还要请求图片,才重新从远程去下载图片。
用法:
Object o=new Object();
SoftReference<Object> softRef = new SoftReference<Object>(o);
弱引用:
在gc之前,清除弱引用内存。主要用来检测对象是不是被标记为可回收对象。
import java.lang.ref.WeakReference;
public class Main {
public static void main(String[] args) {
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}
输出结果为:
hello
null
虚引用就像名字一样是一个虚的,只要执行垃圾回收都会把他回收掉。虚引用的get方法永远获取的都是null值,主要用来判断这个对象是否已经从内存里面删除了。