集合
常用的几个集合
- Collection接口存储一组不唯一,无序的对象
- List接口存储一组不唯一,有序的对象
- Set接口存储一组唯一,无序的对象
- Map接口存储一组键值对象,key->value
- ArrayList实现了可变长度数组,在内存中分配连续空间。遍历和随机访问元素的效率较高
- LinkedList采用链表方式存储,插入和删除的效率较高
ArrayList源码分析
ArrayList是可以动态增长和缩减的索引序列,是基于数组实现的一个List类
该类中封装了一个动态再分配的Object[]数组,每个类对象都有一个capacity属性,表示所封装的Object[]数组长度,当向ArrayList中添加元素是,该属性会自动增加。如果要向ArrayList中添加大量元素,可以使用ensureCapacity方法一次性的增加capacity,减少其增加次数,增强性能
ArrayList extends AbstractList
AbstractListt extends AbstractCollection
问:为什么不直接让ArrayList来实现List接口,而是先由AbstractList实现List,再由ArrayList继承AbstractList?
接口中全是抽象方法,而抽象类中可以由抽象方法也可以由具体的实现方法,因此,让AbstractList类去实现List接口里的一些通用方法,ArrayList来实现其特有方法,减少了重复的代码
问:ArrayList实现了哪些接口?
List接口:作者失误
RandomAccess接口:标记性接口,用来快速随机存储,告诉我们使用for循环遍历性能更好,为实现该接口的化,使用Iterator来迭代性能更好。
Cloneable接口:实现了该接口就能使用clone()方法
Serializable接口:实现了该接口,表明该类可以被序列化
总结
- ArrayList可以存放null
- ArrayList本质是一个elementData数组
- ArrayList区别于数组的地方是能自动的扩展大小,grow()方法,grow()方法每当容量达到最大值就会自动扩容到原来容量的1.5倍(不一定,可能会抛出异常<0,可能返回Integer.Max_VALUE)。如果扩容前的容量为0,则先扩容到10.
- ArrayList实现了RandomAccess,所以再遍历它的时候推荐使用for循环
LinkedList
LinkedList是基于双向链表实现的
它是一个继承与AbstractSequentialList的双向链表。可以被当做堆栈,队列或双端队列进行操作
实现了哪些接口:
- 实现了List接口
- 实现了Deque接口,可以当做双端队列使用
- 实现了Cloneable接口,可以使用clone()
- 实现了serializable接口,支持序列化
- 是非同步的
- 没有实现RandomAccess,所以使用Iterator效率更高,foreach的原理也是Iterator
Stack
Stack是Vector(线程安全的)的子类,其也是线程安全的
- push()入栈
- pop()出栈
- peek()返回栈顶元素
- empty()是否为空
- search():查询指定元素的索引
Vector
Vector线程安全是因为其方法都加了synchronized关键字
Vector的本质是一个数组,也能自动扩增,扩增的方法与capacityIncrement的值有关
它也会fail-fast
List总结
ArrayList和LinkedList的区别
ArrayList底层是用数组实现的顺序表,是随机存取类型,可以自动扩增,但不能无限扩增,有一个上限,在查询操作时性能更好
LinkedList底层使用链表实现的双向链表,顺序存取类型,元素个数没有限制,可以无限扩增,直到内存占满吗,增删操作时性能更好
两个都是线程不安全的,在Iterator时会发生fail-fast
ArrayList与Vector的区别
ArrayList线程不安全,在使用Iterator时,会发生fail-fast
Vector线程安全,其在方法前都加了synchronized关键字
fail-fast和fail-safe区别
java.util下的集合都发生fail-fast,而在java.util.concurrent下的都是fail-safe
fail-fast:快速失败:如在ArrayList中使用迭代器进行遍历时,有另外的线程对ArrayList存储的数组进行了改变,这时Iterator会快速的报一个java.util.ConcurrentModificationException异常(并发修改异常),这就是快速失败
fail-safe:安全失败:在java.util.concurrent下的类都是线程安全的,在进行迭代时,如果有线程对其结构进行改变,也不会报异常,而是正常遍历,这就是安全失败
为什么在迭代时,java.util.concurrent下的集合结构改变不会报异常
对此包下的类进行增加和删除时,会出现一个副本,在副本上进行增加删除操作,等迭代完了再原引用指向副本,所以不会报异常
为什么Vector是线程安全的还会出现fail-fast
因为Vector的底层增删操作和ArrayList,LinkedList都是直接对真正的引用进行操作,所以都会出现fail-fast,其与java.util.concurrent下的包底层是不同的。
既然是线程安全的为什么在迭代时会有其他的线程来改变集合结构呢
因为在迭代过程中并没有用到对应的锁,所以其他线程可以拿到锁进行增加和删除。
Vector在不需要进行线程安全时也会加锁,导致额外开销,在jdk1.5就已经弃用,如果要使用线程安全集合,可以去java.util.concurrent包下拿对应的类
HashMap
HashMap是基于哈希表的Map接口实现的,存储的内容是键值对映射<key,.value>,不保证映射的顺序
HashMap在JDK1.8以前的数据结构和存储原理
链表散列:将数组和链表结合在一起使用就是链表散列
HashMap的内部有一个entry的内部类,其中有四个属性,包括Key,Value,下一个Entry对象和key的hashcode
static class Entry<K,V> implements Map.Entry<K,V> { final K key; //就是我们说的map的key V value; //value值,这两个都不陌生 Entry<K,V> next;//指向下一个entry对象 int hash;//通过key算过来的你hashcode值。 }
首先会将key和value保存带Entry类创建的对象中
构造好了Entry对象就将该对象放入数组中,通过Entry对象的hashcode值来确定将该对象存放在数组的那个位置,如果该位置有其他元素,就通过链表来存储这个元素,通过头插法插入
JDK1,8后HashMap的数据结构
JDK1.8后的数据结构就变成了数组+链表+红黑树了
桶中的结构可能是链表,也可能是红黑树
HashMap的属性
影响HashMap的性能有两个参数
- 初始容量:哈希表中桶的数量
- 加载因子:hashmap在容量达到自动扩容之前的一种度量
当hash表中的条目数超出当前容量与加载因子的乘积是,就需要对hash表进行扩容,rehash操作,将hash表扩容至两倍的桶数
默认的初始容量是16,加载因子是0.75
加载因子趋近于1,哈希表越密集,即链表的长度越长;加载因子趋近于0,数组越稀疏
当put一个元素时吗,除了满足size>=初始容量*加载因子外,还要满足对应的数组位置上有元素才会扩容
HashMap的源码分析
实现接口:
- Map<K,V>接口
- Cloneable接口:可以使用clone()方法,在HashMap中实现的是浅拷贝
- Serializable接口:能够序列化
HashMap使用resize()进行扩容时,会伴随着一次重新hash分配,并且会遍历hash表中的所有元素,非常耗时
数组扩容
hashmap中插入一个元素时,size就会+1,当size大于threshold时,就会进行扩容,threshold=capacity*loadFator
如果threshold=24,此时插入了25个元素,且25个元素都在一个桶中,则桶中的数据结构为红黑树,此时虽然还有只有一个桶内装有元素,但是还是会进行扩容处理
迭代器
所有实现了Collection接口的容器类都有一个iterator()方法用来返回一个实现了Iterator接口的对象
Iterator对象称作迭代器,来对容器内的元素进行遍历操作,其内部定义了如下方法:
- boolean hashNext():判断是否有元素没有被遍历
- Object next():返回游标当前位置的元素并将游标移向下一个位置
- void remove():删除,每次调用next()方法后只能调用一次该方法,即在调用remove()方法之前必须先调用next()方法
Collections工具类
Java提供了一个操作Set,List和Map等集合的工具类Collections,java,util.collections
排序操作
public class JavaTest { public static void main(String[] args) throws IOException { List<Integer> list=new ArrayList<>(); list.add(1); list.add(2); list.add(2,7); //反转列表中的元素 Collections.reverse(list); //对列表中的元素随机排序 Collections.shuffle(list); //对列表中的元素进行升序排序 Collections.sort(list); //将列表中的指定位置元素交换位置 Collections.swap(list,0,2); //将列表后1个元素移到最前面 Collections.rotate(list,1); //将列表前一个元素移到后面 Collections.rotate(list,-1); for (int i:list) { System.out.println(i); } } }
查找,替换操作
public class JavaTest { public static void main(String[] args) throws IOException { List<Integer> list=new ArrayList<>(); list.add(1); list.add(2); list.add(6); list.add(7); list.add(9); //通过二分查找法查找有序列表的对应元素的索引 System.out.println(Collections.binarySearch(list,2)); //返回列表中的最大值 System.out.println(Collections.max(list)); //返回列表中的最小值 System.out.println(Collections.min(list)); //返回列表中指定元素出现的频率 System.out.println(Collections.frequency(list,1)); //返回列表中子列表第一次出现的起始索引位置 List<Integer> list2=new ArrayList<>(); list2.add(7); list2.add(9); System.out.println(Collections.indexOfSubList(list,list2)); //将列表中的所有元素替换成新的元素 Collections.replaceAll(list,1,2); for (int i:list) { System.out.println(i); } } }
同步控制
Collections提供了很多synchronizedXxx()的方法,来将指定结合包装为线程同步的集合,从而解决了多线程并发访问集合时的线程安全问题
public class JavaTest { public static void main(String[] args) throws IOException { //创建同步集合对象 Collection c=Collections.synchronizedCollection(new ArrayList<>()); List list=Collections.synchronizedList(new ArrayList<>()); Set set=Collections.synchronizedSet(new HashSet<>()); Map map=Collections.synchronizedMap(new HashMap<>()); } }
设置不可变集
可以通过3个方法来设置集合不可变,可以时List,Set,Map
public class JavaTest { public static void main(String[] args) throws IOException { //emptyXxx()返回一个空的不可变的Xxx对象 List<Integer> list=Collections.emptyList(); //创建一个只有一个元素的不可变集合对象 Set<Integer> set=Collections.singleton(1); //创建一个不可变的集合对象 Map<String,Integer> map1=new HashMap<>(); map1.put("age",11); Map map=Collections.unmodifiableMap(map1); //均抛出java.lang.UnsupportedOperationException list.add(1); set.add(2); map.put("age",113); } }