Java容器部分上(重要)
1.java容器都有哪些?(容器指的是集合类)
基本概念
Java容器类类库的用途是“持有对象”,并将其划分为两个不同的概念:
1)Collection:一个独立元素的序列,这些元素都服从一条或者多条规则。 List必须按照插入的顺序保存元素,而set不能有重复的元素。Queue按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
2)Map:一组成对的“键值对”对象,允许你使用键来查找值。
|Collection
| ├List
| │-├LinkedList
| │-├ArrayList
| │-└Vector
| │ └Stack
| ├Set
| │├HashSet
| │├TreeSet
| │└LinkedSet
|
|Map
├Hashtable
├HashMap
└WeakHashMap
2.Collection 和 Collections 有什么区别?
注: 1、java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
2、java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
3.List、Set、Map 之间的区别是什么?
答:
1.List:
可以允许重复对象、可以插入多个null元素、是一个有序容器
2.Set:
不允许重复对象、只允许一个null元素、无序容器
3.Map:
Map不是Collection的子接口或实现类。Map是一个接口、Map 的每个Entry都特有两个对象,也就是一个键一个值,Map可能会持有相同的值对象但键对象必须是唯一的、Map里可以拥有随意个niull值但最多只能有一个null键
4.HashMap 和 Hashtable 有什么区别?
1.Map是一个以键值对存储的接口。Map下有两个具体的实现,分别是HashMap和HashTable.
2.HashMap是线程非安全的,HashTable是线程安全的,所以HashMap的效率高于HashTable.
3.HashMap允许键或值为空,而HashTable不允许键或值为空.
4.继承关系不同:
HashTable
public class Hashtable<K,V> extends Dictionary<K,V>1.0
implements Map<K,V>, Cloneable, java.io.Serializable {}
HashMap
public class HashMap<K,V> extends AbstractMap<K,V>1.2
implements Map<K,V>, Cloneable, Serializable{}
5.HashMap性能为什么优于Hashtable?
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
附加问题:
我们可以使用CocurrentHashMap来代替Hashtable吗?
我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。它们都可以用于多线程的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割(segmentation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。CocurrentHashMap在JAVA8中存在一个bug,会进入死循环,原因是递归创建ConcurrentHashMap 对象
6.如何决定使用HashMap还是 TreeMap?
7.说一下 HashMap 的实现原理?
如图所示,HashMap底层是基于数组和链表实现的。其中有两个重要的参数:
容量、负载因子
容量的默认大小是16,负载因子是 0.75,当 HashMap 的 size > 16*0.75时就会发生扩容(容量和负载因子都可以自由调整)。
Put方法:
首先会将传入的Key做 hash运算计算出 hashcode,然后根据数组长度取模计算出在数组中的 index 下标。
由于在计算中位运算比取模运算效率高的多,所以HashMap规定数组的长度为 2^n。这样用 2^n - 1做位运算与取模效果一致,并且效率还要高出许多。
由于数组的长度有限,所以难免会出现不同的Key通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 table[index]处形成链表,采用头插法将数据插入到链表中。
get方法:
get和 put 类似,也是将传入的 Key 计算出 index ,如果该位置上是一个链表就需要遍历整个链表,通过 key.equals(k)来找到对应的元素。
遍历方式:
Iterator<Map.Entry<String, Integer>> entryIterator = map.entrySet().iterator();
while (entryIterator.hasNext()) {
Map.Entry<String, Integer> next = entryIterator.next();
System.out.println("key=" + next.getKey() + " value=" + next.getValue());
}
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()){
String key = iterator.next();
System.out.println("key=" + key + " value=" + map.get(key));
}
map.forEach((key,value)->{
System.out.println("key=" + key + " value=" + value);});
强烈建议使用第一种EntrySet进行遍历。
第一种可以把key value同时取出,第二种还得需要通过 key 取一次 value,效率较低, 第三种需要 JDK1.8以上,通过外层遍历 table,内层遍历链表或红黑树。
死循环问题:
在并发环境下使用HashMap容易出现死循环。
并发场景发生扩容,调用resize()方法里的 rehash()时,容易出现环形链表。这样当获取一个不存在的 key时,计算出的index正好是环形链表的下标时就会出现死循环。
所以HashMap只能在单线程中使用,并且尽量的预设容量,尽可能的减少扩容。
在JDK1.8中对 HashMap进行了优化: 当 hash碰撞之后写入链表的长度超过了阈值(默认为8)并且 table的长度不小于64(否则扩容一次)时,链表将会转换为红黑树。
假设hash冲突非常严重,一个数组后面接了很长的链表,此时重新的时间复杂度就是 O(n)。
如果是红黑树,时间复杂度就是O(logn)。
大大提高了查询效率。
多线程场景下推荐使用CocurrentHashMap。
8.说一下 HashSet 的实现原理?
HashSet是一个不允许存储重复元素的集合,它的实现比较简单,只要理解了 HashMap,HashSet就水到渠成了。
成员变量:
首先了解下HashSet的成员变量:
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
发现主要就两个变量:
map:用于存放最终数据的。
PRESENT:是所有写入 map 的 value值。
构造函数:
public HashSet() {
map = new HashMap<>();
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
构造函数很简单,利用了HashMap初始化了 map。
add:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
比较关键的就是这个add()方法。 可以看出它是将存放的对象当做了 HashMap的健,value都是相同的 PRESENT。由于 HashMap的 key是不能重复的,所以每当有重复的值写入到 HashSet时,value会被覆盖,但 key不会受到影响,这样就保证了 HashSet中只能存放不重复的元素。
总结:
HashSet的原理比较简单,几乎全部借助于 HashMap来实现的。
所以HashMap会出现的问题 HashSet依然不能避免。
9.ArrayList 和 LinkedList 的区别是什么?
Arraylist:底层是基于动态数组,根据下表随机访问数组元素的效率高,向数组尾部添加元素的效率高;但是,删除数组中的数据以及向数组中间添加数据效率低,因为需要移动数组。例如最坏的情况是删除第一个数组元素,则需要将第2至第n个数组元素各向前移动一位。而之所以称为动态数组,是因为Arraylist在数组元素超过其容量大,Arraylist可以进行扩容(针对JDK1.8 数组扩容后的容量是扩容前的1.5倍),Arraylist源码中最大的数组容量是Integer.MAX_VALUE-8,对于空出的8位,目前解释是 :①存储Headerwords;②避免一些机器内存溢出,减少出错几率,所以少分配③最大还是能支持到Integer.MAX_VALUE(当Integer.MAX_VALUE-8依旧无法满足需求时).只要ArrayList的当前容足够大,add()操作向数组的尾部的效率非常高的,当向数组指定位置添加yi据时,会进行大量的数组移动复制操作。而数组复制时,最终将调用System.arraycopy()方法,因此add()操作的效率还是相当高的。尽管这样当向指定位置添加数据时也还是比Linkedlist慢,后者添加数据只需要改变指针指向即可。Arraylist删除数组也需要移动数组,效率较慢。
Linkedlist基于链表的动态数组,数据添加删除效率高,只需要改变指针指向即可,但是访问数据的平均效率低,需要对链表进行遍历。
总结:
1、对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
2、各自效率问题:
10.如何实现数组和List之间的转换?
List转数组:
toArray(arraylist.size()方法
数组转List:
1.Arrays的asList(a)方法(效率不高)
2.通过集合工具类Collections.addAll()方法(最高效)通过Collections.addAll(arrayList, strArray)方式转换,根据数组的长度创建一个长度相同的List,然后通过Collections.addAll()方法,将数组中的元素转为二进制,然后添加到List中,这是最高效的方法。
11.ArrayList 和 Vector 的区别是什么?
ArrayList实现于 List、RandomAccess 接口。可以插入空数据,也支持随机访问。
ArrayList相当于动态数据,其中最重要的两个属性分别是: elementData 数组,以及 size 大小。 在调用 add() 方法的时候:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
首先进行扩容校验。
将插入的值放到尾部,并将size + 1。
如果是调用add(index,e)在指定位置添加的话:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
//复制,向后移动
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
也是首先扩容校验。
接着对数据进行复制,目的是把index位置空出来放本次插入的数据,并将后面的数据向后移动一个位置。
其实扩容最终调用的代码:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
也是一个数组复制的过程。
由此可见ArrayList的主要消耗是数组扩容以及在指定位置添加数据,在日常使用时最好是指定大小,尽量减少扩容。更要减少在指定位置插入数据的操作。
序列化:
由于ArrayList是基于动态数组实现的,所以并不是所有的空间都被使用。因此使用了 transient 修饰,可以防止被自动序列化。
transient Object[] elementData;
因此ArrayList自定义了序列化与反序列化:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
//只序列化了被使用的数据
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
当对象中自定义了writeObject和 readObject 方法时,JVM 会调用这两个自定义方法来实现序列化与反序列化。
从实现中可以看出ArrayList只序列化了被使用的数据。
Vector:
Vector也是实现于 List 接口,底层数据结构和 ArrayList 类似,也是一个动态数组存放数据。不过是在 add() 方法的时候使用 synchronized 进行同步写数据,但是开销较大,所以 Vector 是一个同步容器并不是一个并发容器。
以下是add()方法:
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
以及指定位置插入数据:
public void add(int index, E element) {
insertElementAt(element, index);
}
public synchronized void insertElementAt(E obj, int index) {
modCount++;
if (index > elementCount) {
throw new ArrayIndexOutOfBoundsException(index
+ " > " + elementCount);
}
ensureCapacityHelper(elementCount + 1);
System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
elementData[index] = obj;
elementCount++;
}
12.Array 和 ArrayList 有何区别?
Array可以包含基本类型和对象类型,ArrayList只能包含对象类型
Array大小固定,ArrayList的大小是动态变化的。
ArrayList提供了更多的方法和特性:比如 :addAll(),removeAll(),iterator()等等。
对于基本数据类型,集合使用自动装箱来减少编码工作量。但是,当处理固定大小基本数据类型的时候,这种方式相对较慢。
13.在Queue中poll()和remove()有什么区别?
poll()和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
14.哪些集合类是线程安全的?
Vector:就比ArrayList多了一个同步化机制(线程安全)
LinkedList因为成员方法大多是synchronized的,因此LinkedList是线程安全的。而ArrayList不是线程安全的。在扩容机制上,当Vector的元素数量超过它的初始化大小的时候会将容量翻倍,而ArrayList只会增长50%。
ArrayList的数据结构是基于数组的(Object[]),而LinkList内部结构是基于一组链接的记录,形式上属于链表的。所以在增加元素方面linkList的效率更高,因为在ArrayList中增加元素,会牵扯一次重新排序。删除也是类似,所以ArrayList的查询性能要好些。反之LinkList增加,删除性能更好。如果是迭代读取的话,就没有什么差别了。
HashTable:比hashMap多了一个线程安全。hashTable的方法都提供了同步机制。hashTable不允许插入空值,hashMap是允许的。
ConcurrentHashMap:是一种高效但是也线程安全的集合。它比Hashmap来讲,是线程安全的,但是效率也比较高,因为它引入了一个分段锁的概念,可以理解为把一个大的Map拆分成了N个小的hashTable。根据key.hashCode()决定把key放到哪个hashtable中。HashMap的数据结构是数据和链表。通过hash算法计算该key所在的数组下标,根据equals取比较值。通俗的说救赎ConcurrenthashMap是对每个数组进行加锁,当通过hash算法得出的结果相同时才需要去同步数据。
15.迭代器Iterator 是什么?
对Collection 进行迭代的类,称其为迭代器。还是面向对象的思想,专业对象做专业的事情,迭代器就是专门取出集合元素的对象。但是该对象比较特殊,不能直接创建对象(通过new),该对象是以内部类的形式存在于每个集合类的内部。
16.Iterator 怎么使用?有什么特点?
Collection接口中定义了获取集合类迭代器的方法(iterator()),所以所有的Collection体系集合都可以获取自身的迭代器。
17.Iterator 和 ListIterator 有什么区别?
1. ListIterator有add()方法,可以向List中添加对象,而Iterator不能
2. ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
3. ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
4. 都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。
18.怎么确保一个集合不能被修改?
Collections.unmodifiableList(List)
Collections.unmodifiableMap(Map)
Collections.unmodifiableSet(Set)
可以返回一个只读视图不能修改
19.List、Map、Set三个接口,存取元素时,各有什么特点?
List 以特定次序来持有元素,可有重复元素。即,有序可重复。
访问时可以使用for循环,foreach循环,iterator迭代器 迭代。
Set 无法拥有重复元素,内部排序。即,无序不可重复。
访问时可以使用foreach循环,iterator迭代器 迭代。
Map 保存 key-value 值,一一映射。key值 是无序,不重复的。value值可重复。
访问时可以map中key值转为为set存储,然后迭代这个set,用map.get(key)获取value
20.对比Hashtable、HashMap、TreeMap有什么不同?
HashTable HashMap TreeMap都是最常见的一些Map实现,是以键值对的形式存储和操作数据的容器类型。
HashTable是早期Java类库提供的一个哈希表实现,本身是同步的,不支持null键和值,由于同步导致的性能开销,所以已经很少被推荐使用。
HashMap是应用更加广泛的哈希表实现,行为大致上与HashTable一致,主要区别在于HashMap不是同步的,支持null键和值等。通常情况下,HashMap进行put或者get操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存储场景的首选,比如,实现一个用户ID和用户信息对应的运行时存储结构。
TreeMap则是基于红黑树的一种提供顺序访问的Map,和HashMap不同,它的get remove之类操作都是O(long(n)的时间复杂度,具体顺序可以由指定的Comparator来决定或者根据键的自然顺序来判断。