二、Java 集合框架
(引自知乎,略修改)
1、List(线性结构)
- ArrayList Object[] 数组实现,默认大小为 10 ,支持随机访问,连续内存空间,插入末尾时间复杂度 o(1),插入第 i 个位置时间复杂度 o(n - i)。扩容,大小变为 1.5 倍,Arrays.copyOf(底层 System.ArrayCopy),复制到新数组,指针指向新数组。
ArrayList常见方法:
- sort()方法:
import java.util.Comparator; ArrayList<String> sites = new ArrayList<>(); sites.sort(Comparator.naturalOrder());// 元素进行升序排列 //不排序: [Runoob, Google, Wiki, Taobao] //排序后: [Google, Runoob, Taobao, Wiki] sites.sort(Comparator.reverseOrder());// 降序 //不排序: [Runoob, Google, Wiki, Taobao] //降序排序: [Wiki, Taobao, Runoob, Google]
import java.util.Collections; // 引入 Collections 类 Collections.sort(sites); // 字母排序
add()方法:增加元素
contains()方法:判断元素是否在 arraylist,如果指定的元素存在于动态数组中,则返回 true,如果指定的元素不存在于动态数组中,则返回 false。
get() 方法:通过索引值获取动态数组中的元素。arraylist.get(int index)
size():返回 arraylist 里元素数量。
toArray() :方法将 Arraylist 对象转换为数组。
toString() 方法将 Arraylist 对象转换为字符组。
import java.util.ArrayList; class Main { public static void main(String[] args){ // 创建一个动态数组 ArrayList<String> sites = new ArrayList<>(); sites.add("Runoob"); sites.add("Google"); sites.add("Wiki"); sites.add("Taobao"); System.out.println("网站列表: " + sites); // 将ArrayList转换为String类型 String list = sites.toString(); System.out.println("String: " + list); } } //网站列表: [Runoob, Google, Wiki, Taobao] //String: [Runoob, Google, Wiki, Taobao]
- Vector 类似 ArrayList,线程安全,扩容默认增长为原来的 2 倍,还可以指定增长空间长度。
- LinkedList 基于链表实现,1.7 为双向链表,1.6 为双向循环链表,取消循环更能分清头尾。
与 ArrayList 相比,LinkedList 的增加和删除对操作效率更高,而查找和修改的操作效率较低。
LinkedList常见方法:
- public boolean add(E e);public void add(int index, E element) 向末尾或指定位置插入元素。
- public void addFirst(E e);public void addLast(E e) 向开头末尾插入元素。
- public E get(int index) 返回Index位置元素。
- public E getFirst();public E getLast() 返回开头末尾元素。
- public int size() 返回链表元素个数。
public Object[] toArray() 返回一个由链表元素组成的数组。 public T[] toArray(T[] a) 返回一个由链表元素转换类型而成的数组。就是可以先转换元素为其他类型,再返回一个这样指定类型的数组。
2、Map(K,V 对)
- HashMap
- 底层数据结构,JDK 1.8 是数组 + 链表 + 红黑树,JDK 1.7 无红黑树。链表长度大于 8 时,转化为红黑树,优化查询效率。
- 个人觉得此链接文章对红黑树解释很清楚
- 初始容量为 16,通过 tableSizeFor 保证容量为 2 的幂次方。寻址方式,高位异或,(n-1)&h 取模,优化速度。
- 扩容机制,当元素数量大于容量 x 负载因子 0.75 时,容量扩大为原来的 2 倍,新建一个数组,然后转移到新数组。
- 基于 Map 实现。
- 线程不安全。
- 最多允许一条记录的键为 null,不支持线程同步。
HashMap常见方法:
- HashMap (1.7) 多线程循环链表问题
- 在多线程环境下,进行扩容时,1.7 下的 HashMap 会形成循环链表。头插法导致的问题。
- 怎么形成循环链表: 假设有一 HashMap 容量为 2 , 在数组下标 1 位置以 A -> B 链表形式存储。有一线程对该 map 做 put 操作,由于触发扩容条件,需要进行扩容。这时另一个线程也 put 操作,同样需要扩容,并完成了扩容操作,由于复制到新数组是头部插入,所以 1 位置变为 B -> A 。这时第一个线程继续做扩容操作,首先复制 A ,然后复制 B ,再判断 B.next 是否为空时,由于第二个线程做了扩容操作,导致 B.next = A,所以在将 A 放到 B 前,A.next 又等于 B ,导致循环链表出现。
- HashTable
- 线程安全,方法基本全用 Synchronized 修饰。
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;(我们取得的是对象锁,也就是说,一个对象一个锁,而不是锁住整个类或者代码或者方法。)
- 修饰静态方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
- 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
- 初始容量为 11 ,扩容为 2n + 1 。
- 继承 Dictionary 类。
- ConcurrentHashMap
- 线程安全的 HashMap。
- 1.7 采用分段锁的形式加锁;1.8 使用 Synchronized 和 CAS 实现同步,若数组的 Node 为空,则通过 CAS 的方式设置值,不为空则加在链表的第一个节点。获取第一个元素是否为空使用 Unsafe 类提供的 getObjectVolatile 保证可见性。
- 分段锁在我的理解中是先分段再锁,将原本的一整个的Entry数组分成了若干段,分别将这若干段放在了不同的新的Segment数组中(分房间),每个Segment有各自的锁,以此提高效率。Entry是hashMap中封装key-value键值对的。
- 乐观锁和悲观锁:
加锁是一种悲观的策略,它总是认为每次访问共享资源的时候,总会发生冲突,所以宁愿牺牲性能(时间)来保证数据安全。
无锁是一种乐观的策略,它假设线程访问共享资源不会发生冲突,所以不需要加锁,因此线程将不断执行,不需要停止。一旦碰到冲突,就重试当前操作直到没有冲突为止。
- 无锁的策略使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。
- 算法思路:V是共享变量,我们拿着自己准备的这个E,去跟V去比较,如果E == V ,说明当前没有其它线程在操作,所以,我们把N 这个值 写入对象的 V 变量中。如果 E != V ,说明我们准备的这个E,已经过时了,所以我们要重新准备一个最新的E ,去跟V 比较,比较成功后才能更新 V的值为N。
- 对于读操作,数组由 volatile 修饰,同时数组的元素为 Node,Node 的 K 使用 final 修饰,V 使用 volatile 修饰,下一个节点也用 volatile 修饰,保证多线程的可见性。
如果对变量i加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,因为线程一般读取变量是在jvm的工作内存中,此时强迫线程A重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值。
- LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。
- TreeMap 有序的 Map,红黑树结构,可以自定义比较器来进行排序。
- Collections.synchronizedMap 如何实现 Map 线程安全? 基于 Synchronized ,实际上就是锁住了当前传入的 Map 对象。
3、Set(唯一值)
- HashSet 基于 HashMap 实现,使用了 HashMap 的 K 作为元素存储,V 为 new Object() ,在 add() 方法中如果两个元素的 Hash 值相同,则通过 equals 方法比较是否相等。
- LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。
- TreeSet 红黑树实现有序唯一。
- HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。
- HashSet 允许有 null 值。
- HashSet 是无序的,即不会记录插入的顺序。
- HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。
HashSet常见方法:
add() 添加指定元素;contains()判断元素是否存在于集合当中,返回值true,false;
remove()删除指定元素;size()返回HashSet 中的元素数量。