集合
- 常见的集合有哪些
- List、Set、Map 的区别
- ArrayList 和 Vector 的扩容机制
- Collection 和 Collections 有什么区别
- ArrayList 和 LinkedList 的区别是什么
- ArrayList 和 Vector 的区别是什么
- ArrayList 和 Array 有何区别
- ArrayList 集合加入1万条数据,应该怎么提高效率
- HashSet 的实现原理
- TreeSet 的实现原理
- TreeSet 和 HashSet 的区别
- HashMap 的实现原理
- HashMap 和 Hashtable 有什么区别
- 怎么确保一个集合不能被修改
- Iterator 怎么使用
- Iterator 和 ListIterator 有什么区别
- 队和栈是什么
- 队中方法的区别
- 哪些集合类是线程安全的
- 如何实现 Array 数组和 List 集合之间的转换
常见的集合有哪些
1、Collection
(1)list
ArrayList、LinkedList、Vector
(2)set
HashSet、TreeSet
2、Map
HashMap、HashTable、TreeMap
List、Set、Map 的区别
1、List 有序的,元素是允许重复的。
2、Set 无序的,元素是 不允许重复的,
3、Map 保存键值对映射,映射关系可以一对一、多对一。
ArrayList 和 Vector 的扩容机制
ArrayList无参构造
//Object类型的数组 elmentData []
transient Object[] elementData;
//{}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList无参构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
底层用的是一个Object类型的数组elementData,当使用无参构造方法ArrayList后elementData是空的,也就是说使用无参构造方法后容量为0。
//容量为10
private static final int DEFAULT_CAPACITY = 10;
//add添加元素方法
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
//所需最小容量方法
private void ensureCapacityInternal(int minCapacity) {
//空数组初始所需最小容量为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//是否需要扩容方法
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//所需最小容量当前数组能否存下,如果现在数组存不下进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//容器扩容方法
private void grow(int minCapacity) {
//旧容量(原数组的长度)
int oldCapacity = elementData.length;
//新容量(旧容量加上旧容量右移一位,也就是1.5倍)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果计算出的新容量比最小所需容量小就用最小所需容量作为新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果计算出的新容量比MAX_ARRAY_SIZE大, 就调用hugeCapacity计算新容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//数组扩容成新容量
elementData = Arrays.copyOf(elementData, newCapacity);
}
由此看出只有当第一次add添加元素的时候,才初始化容量,因为是空数组所需的最小容量为10,而 elementData 大小为0,新容量算出类也是0,此时最小所需容量作为新容量为10。
例如:
ArrayList<Object> objects = new ArrayList<>();
长度: 1 容量: 10
长度: 5 容量: 10
长度: 11 容量: 15
长度: 15 容量: 15
长度: 21 容量: 22
ArrayList有参构造
//有参构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
有参构造和无参构造区别就是给数组初始化了长度initialCapacity并且数组不为空,不为空的数组最小所需容量就是集合元素长度,集合元素长度超过初始化长度initialCapacity值才扩容,扩容逻辑和无参构造一致。
例如:
ArrayList<Object> objects = new ArrayList<>(5);
长度: 3 容量: 5
长度: 5 容量: 5
长度: 7 容量: 7
长度: 11 容量: 15
长度: 15 容量: 15
长度: 19 容量: 22
ArrayList<Object> objects = new ArrayList<>(13);
长度: 15 容量: 19
长度: 17 容量: 19
长度: 21 容量: 28
长度: 25 容量: 28
长度: 29 容量: 42
Vector 扩容机制
Vector 的底层也是一个数组 elmentData ,但相对于 ArrayList 来说,它是线程安全的,它的每个操作方法都是加了锁的。如果在开发中需要保证线程安全,则可以使用 Vector。扩容机制也与 ArrayList 大致相同。唯一需要注意的一点是,Vector 的扩容量是2倍。
结论
数据类型 | 底层数据结构 | 默认初始容量 | 加载因子 | 扩容增量 |
---|---|---|---|---|
ArrayList | 数组 | 10(jdk7)0(jdk8) | 加载因子1(元素满了扩容) | 0.5:扩容后容量为原容量的1.5倍 |
Vector | 数组 | 10 | 加载因子1(元素满了扩容) | 1:扩容后容量为原容量的2倍 |
LinkedList,链表结构,且是是双向链表,不涉及扩容的问题。
Collection 和 Collections 有什么区别
首先说下 collection,collection 它是一个接口,collection 接口的意义是为各种具体的集合提供统一的操作方式,它继承 Iterable 接口,Iterable 接口中有一个最关键的方法, Iterator iterator()方法迭代器,可以迭代集合中的元素。
Collections 是操作集合的工具类,Collections.sort(list)方法进行排序,使用此方法排序集合元素类型必须实现Comparable接口重写compareTo()方法,否则无法实现排序。
HashMap 解决hash冲突的办法有哪些?HashMap用的哪种?使用的hash算法?扩容过程?put方法流程?红黑树的特点?为什么使用红黑树而不使用AVL树?在解决 hash 冲突的时候,为什么选择先用链表,再转红黑树?HashMap 的长度为什么是 2 的幂次方?HashMap默认加载因子是多少?为什么是 0.75?一般用什么作为HashMap的key?HashMap为什么线程不安全?HashMap和HashTable的区别?
LinkedHashMap底层原理?
讲一下TreeMap?
HashSet底层原理?
HashSet、LinkedHashSet 和 TreeSet 的区别?
什么是fail fast?
什么是fail safe?
讲一下ArrayDeque?
并发容器 ConcurrentHashMap put执行流程?怎么扩容?ConcurrentHashMap 和 Hashtable 的区别? CopyOnWriteConcurrentLinkedQueue阻塞队列 JDK提供的阻塞队列原理
ArrayList 和 LinkedList 的区别是什么
- ArrayList 是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的,可以直接返回数组中 index 位置的元素,因此在随机访问集合元素上有较好的性能。对于随机访问,ArrayList 优于 LinkedList。
- LinkedList 的随机访问集合元素时性能较差,但在插入,删除操作是更快的。因为 LinkedList 不像 ArrayList 一样,不需要改变数组的大小,不需要在数组装满的时候要将所有的数据重新装入一个新的数组,对于插入和删除操作,LinkedList 优于 ArrayList。
- LinkedList 需要更多的内存,因为 ArrayList 的每个索引的位置是实际的数据,而 LinkedList 中的每个节点中存储的是实际的数据和前后节点的位置。
ArrayList 和 Vector 的区别是什么
- Vector 的方法都是同步的,线程安全;ArrayList 非线程安全,但性能比 Vector 好
- 默认初始化容量都是 10,Vector 扩容默认会翻倍,可指定扩容的大小;ArrayList 只增加 50%
ArrayList 和 Array 有何区别
- Array 可以容纳基本数据类型和对象,ArrayList 只能容纳对象
- Array 是制定大小的,ArrayList 大小是固定的但是可以扩容
ArrayList 集合加入1万条数据,应该怎么提高效率
因为ArrayList的底层是数组实现,并且数组的默认值是10,如果插入10000条要不断的扩容,耗费时间,所以我们调用ArrayList的指定容量的构造器方法ArrayList(int size) 就可以实现不扩容,就提高了性能。
HashSet 的实现原理
HashSet是基于HashMap实现的,数据存储结构都是数组+链表。HashSet中的元素都存放在HashMap的key上面,而value都是一个统一的对象PRESENT。
private static final Object PRESENT = new Object();
HashSet中add方法调用的是底层HashMap中的put方法,put方法要判断插入值是否存在,而HashSet的add方法,首先判断元素是否存在,如果存在则插入,如果不存在则不插入,这样就保证了HashSet中不存在重复值。
通过对象的hashCode和equals方法保证对象的唯一性。
TreeSet 的实现原理
TreeSet底层实际是用TreeMap实现的,内部维持了一个简化版的TreeMap,通过key来存储Set的元素。 TreeSet内部需要对存储的元素进行排序,因此,我们对应的类需要实现Comparable接口。这样,才能根据compareTo()方法比较对象之间的大小,才能进行内部排序。
TreeSet 和 HashSet 的区别
1、Hashset 的底层是由哈希表实现的,Treeset 底层是由红黑树实现的。
2、HashSet中的元素没有顺序,TreeSet保存的元素有顺序性(实现Comparable接口)
HashMap 的实现原理
HashMap的存储结构:
JDK1.7中采用数组+链表的存储形式。
JDK1.8中采用数据+链表+红黑树的存储形式。当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。
HashMap基于map接口,元素以键值对方式存储,允许有null值,HashMap是线程不安全的。我们使用 put(key, value)存储对象到 HashMap 中,使用 get(key)从 HashMap 中获取对象
以下是具体的 put 过程(JDK1.8 版):
- 对 Key 求 Hash 值,然后再计算下标
- 如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的 Hash 值相同,需要放到同一个 bucket 中)
- 如果碰撞了,以链表的方式链接到后面
- 如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于 6,就把红黑树转回链表
- 如果节点已经存在就替换旧值
- 如果桶满了(容量 16*加载因子 0.75),就需要 resize(扩容 2 倍后重排)
- initialCapacity:初始容量。指的是 HashMap 集合初始化的时候自身的容量。可以在构造方法中指定;如果不指定的话,总容量默认值是 16 。需要注意的是初始容量必须是 2 的幂次方。
- size:当前 HashMap 中已经存储着的键值对数量,即 HashMap.size()
- loadFactor:加载因子。所谓的加载因子就是 HashMap (当前的容量/总容量) 到达一定值的时候,HashMap 会实施扩容。加载因子也可以通过构造方法中指定,默认的值是 0.75 。
- threshold:扩容阀值。即 扩容阀值 = HashMap 总容量 * 加载因子。当前 HashMap 的容量
大于或等于扩容阀值的时候就会去执行扩容。扩容的容量为当前 HashMap 总容量的两倍。
举个例子,假设有一个 HashMap 的初始容量为 16 ,那么扩容的阀值就是 0.75 * 16 = 12 。
也就是说,在你打算存入第 13 个值的时候,HashMap 会先执行扩容,那么扩容之后为 32 。
HashMap 和 Hashtable 有什么区别
- 首先是它们的继承父类不同,HashMap 继承的是 AbstractMap,Hashtable 继承的是 Dictionary
- HashMap 是线程是线程不安全的,Hashtable 是线程安全的,所以 HashMap 比 Hashtable 效率高
- HashMap 中,null 可以作为键和值,但是 Hashtable 的键和值是 null,编译可以通过,但是会抛出 NullPointerException 异常
- HashMap 没有 contains 这个方法,它有的是 containsKey 和 containsValue 方法,Hashtable 有 contains 这个方法与 containsKey 功能相同
- HashMap 的默认的容量是 16,Hashtable 的默认容量是 11,它们的扩容方式也不同,HashMap 扩容二倍,Hashtable 扩容二倍加一
- 它们都使用了 iterator 遍历元素,但是 Hashtable 还使用了 Enumeration 方式 hasMoreElements()查询是否有数据 nextElement 取出数据
- Hashtable 计算 hash 值,直接用 key 的 hashCode(),而 HashMap 重新计算了 key 的 hash 值
怎么确保一个集合不能被修改
final关键字可以修饰类,方法,成员变量,final修饰的类不能被继承,final修饰的方法不能被重写,final修饰的成员变量必须初始化值,如果这个成员变量是基本数据类型,表示这个变量的值是不可改变的,如果说这个成员变量是引用类型,则表示这个引用的地址值是不能改变的,但是这个引用所指向的对象里面的内容还是可以改变的。
集合(map,set,list…)都是引用类型,所以我们如果用final修饰的话,集合里面的内容还是可以修改的。我们可以采用Collections包下来让集合不能修改:
1. Collections.unmodifiableList(List)
2. Collections.unmodifiableSet(Set)
3. Collections.unmodifiableSet(map)
Iterator 怎么使用
/**
* 测试Collection迭代对象的方式 迭代器的方式 Iterator接口,定义了迭代Collection 容器中对象的统一操作方式 集合对象中的迭代器是采用内部类的方式实现 这些内部类都实现了Iterator接口
* 使用迭代器迭代集合中数据期间,不能使用集合对象 删除集合中的数据
*/
@Test
public void test02() {
Collection<String> c1 = new HashSet<String>();
c1.add("java");
c1.add("css");
c1.add("html");
c1.add("javaScript");
Iterator<String> it = c1.iterator();
while (it.hasNext()) {
String str = it.next();
System.out.println(str);// css java javaScript html
if (str.equals("css")) {
// c1.remove(str);//会抛出异常
it.remove();
}
}
System.out.println(c1);// [java, javaScript, html]
}
Iterator 和 ListIterator 有什么区别
- ListIterator 继承 Iterator
- 使用范围不同,Iterator可以迭代所有集合,ListIterator 只能用于List及其子类
- ListIterator 比 Iterator 多方法
队和栈是什么
- 队列特殊的线性表,队列中限制了对线性表的访问只能从线性表的一端添加元素,从另一端取出,遵循先进先出(FIFO)原则。
- 栈是队的子接口,栈是继承队的,定义类"双端列"从队列的两端可以入队(offer)和出队(poll),LinkedList实现了该接口,如果限制Deque双端入队和出队,将双端队列改为单端队列即为栈,栈遵循先进后出(FILO)的原则。
队中方法的区别
- offer()和 add()区别:添加队尾
- offer()和 add()都是增加新项,如果队列满了,add会抛出异常,offer返回false。
- poll()和 remove()区别:移除队首
- poll()和 remove()都是从队列中删除第一个元素,为空时remove抛出异常,poll返回null。
- peek()和 element()区别:获取队首
- peek()和 element()用于查询队列头部元素,为空时element抛出异常,peek返回null。
哪些集合类是线程安全的
- Vector:就比ArrayList多了个同步机制(线程安全)
- Stack:栈,也是线程安全的,继承于Vector
- Hashtable:就比HashMap多了个线程安全
- ConcurrentHashMap:是一种高效但是线程安全的集合
如何实现 Array 数组和 List 集合之间的转换
① 基本数据类型数组转集合
错误代码如下(示例):
int[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
System.out.println("集合为:" + list + " 长度为:" + list.size());
//集合为:[[I@4554617c] 长度为:1
当把基础数据类型的数组转为集合时,由于Arrays.asList参数为可变长泛型,而基本类型是无法泛型化的,所以它把int[] arr数组当成了一个泛型对象,所以集合中最终只有一个元素arr。
正确代码如下(示例):
//(1)通过for循环遍历数组将其转为集合
Integer a[] = {1, 2, 3};
ArrayList<Integer> aList = new ArrayList<>();
for (Integer i : a) {
aList.add(i);
}
//(2)使用Java8的Stream实现转换(依赖boxed的装箱操作)
int [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).boxed().collect(Collectors.toList());
② 包装数据类型数组转集合
Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
System.out.println("集合为:" + list + " 长度为:" + list.size());
//集合为:[1, 2, 3] 长度为:3
③ 集合转数组
错误代码如下(示例):
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
Integer[] res = (Integer[]) list.toArray();
System.out.println(res);
//java.lang.ClassCastException
正确代码如下(示例):
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
Integer[] res = new Integer[list.size()];
for (int i = 0; i < list.size(); i++) {
res[i] = (Integer) list.toArray()[i];
}
更加简单的方式是使用toArray(T[] a) 参数必须为list元素类型的父类或本身
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
Integer[] res = list.toArray(new Integer[]{});