框架图
Collection
- Collection:采用线性列表的方式存储,长度可动态改变。
- List:记录元素插入顺序,允许存在重复元素。
- ArrayList:使用动态数组方式存储元素,支持随机读取,适合做查询操作,是线程不安全的。
- Vector:是ArrayList线程安全的实现。因为是线程安全的,所以性能较ArrayList稍差。
- LinkedList:使用双向链表的方式存储元素,适合做增删操作,是线程不安全的。
- Set:不记录元素插入顺序,不允许存在重复元素。
- HashSet:容器中的元素是无序的。
- TreeSet:容器中的元素是按照自然排序进行排序的,排序规则可自定义。
- Queue:提供了一种先进先出的操作方式,只允许在容器的前端进行删除,在后端进行插入。
- List:记录元素插入顺序,允许存在重复元素。
List
-
ArryaList:使用动态数组实现,其容量随着元素的增加可以自动扩张(1+50%),特点是查询效率高。
ArrayList<String> al = new ArrayList<>(); al.add("张三"); al.add("李四"); al.add("王五"); System.out.println(al);
结果:[张三, 李四, 王五]
- 读取过程:动态数组的物理存储位置是连续的,知道ArrayList第一个元素的物理存储地址和每个元素的大小,即可通过下标算出任意元素的物理地址,所以查询效率高。
例如:张三的地址为0000001,下标为1,每个对象占两个字节,则下标为3的对象的物理起始地址为:
(3-1)x 2 + 0000001 = 0000005
- 增删过程:删除ArrayList中间的元素后,被删除元素后的所有元素要向前移动,所以删除元素时效率较低。
- 读取过程:动态数组的物理存储位置是连续的,知道ArrayList第一个元素的物理存储地址和每个元素的大小,即可通过下标算出任意元素的物理地址,所以查询效率高。
-
Vector:特性与ArrayList一直,只是在线程安全方面进行了处理,因此它是同步的,自动扩容容量(1+1)。在存在大量元素时,因为是线程安全的,所有性能逊色于ArrayList
Vector<String> al = new Vector<>(); al.add("张三"); al.add("李四"); al.add("王五"); System.out.println(al);
结果:[张三, 李四, 王五]
-
LinkedList:基于双向链表实现的,所以对元素的增删支持好,查询方面不如ArrayList
LinkedList<String> al = new LinkedList<>(); al.add("张三"); al.add("李四"); al.add("王五"); System.out.println(al);
结果:[张三, 李四, 王五]
- 读取过程:双向链表的物理存储位置是不连续的,每个元素保存着下一个元素和上一个元素的物理位置。查找下标为3的元素时,需要一次查询每一个元素用以获取下一个元素的物理地址,所以查询性能低于ArrayList。
- 增删过程:删除元素时,其余元素位置不需要移动,只需改变删除元素前后元素的地址指向即可。
- 读取过程:双向链表的物理存储位置是不连续的,每个元素保存着下一个元素和上一个元素的物理位置。查找下标为3的元素时,需要一次查询每一个元素用以获取下一个元素的物理地址,所以查询性能低于ArrayList。
Set
-
HashSet
HashSet是基于Hash算法实现的,其性能比TreeSet好,特点是增删元素较快。HashSet<String> hs = new HashSet<>(); hs.add("张三"); hs.add("张三"); hs.add("李四"); hs.add("李四"); hs.add("王五"); hs.add("王五"); System.out.println(hs);
输出结果:[李四, 张三, 王五]
TIPS:HashSet每次添加对象时,会使用equals(),根据散列码来判断是否重复,可以通过Object.hashCode()获取对象的散列码。
-
TreeSet
TreeSet中的元素除了没有顺序和不能重复外,还会进行自然排序,或自定义排序。TreeSet<String> ts = new TreeSet<>(); ts.add("张三"); ts.add("张三"); ts.add("李四"); ts.add("李四"); ts.add("王五"); ts.add("王五"); System.out.println(ts);
输出结果:[张三, 李四, 王五]
-
TreeSet自定义排序规则
/* * 通过实现Comparable接口,并重写compareTo方法来实现自定义排序规则。 */ public class Student implements Comparable<Student> { private String name; private Integer age; public Student(String name,Integer age) { this.name = name; this.age = age; } @Override public String toString() { return "Student [name=" + name + ", age=" + age + "]"; } @Override public int compareTo(Student o) { if(this.age < o.age) { // 负整数 当前对象小于传入的对象 return -1; }else if(this.age > o.age) { // 正整数 当前对象大于传入的对象 return 1; }else { // 相等 元素不可加入 return 0; } } }
public static void main(String[] args) { Student one = new Student("张三", 1); Student two = new Student("李四", 3); Student three = new Student("王五", 2); Student four = new Student("刘六", 2); TreeSet<Student> ts = new TreeSet<>(); ts.add(one); ts.add(two); ts.add(three); ts.add(four); ts.add(four); System.out.println(ts); }
结果:[Student [name=张三, age=1], Student [name=王五, age=2], Student [name=李四, age=3]]
Queue
提供了一种先进先出的操作方式,只允许在容器的前端进行删除,在后端进行插入(类似于现实中的排队),不可对中间元素进行操作。是对增删操作进行限制的List,二者实现方式也不同。
Queue<String> qu = new LinkedList<>();
qu.add("1");
qu.add("2");
qu.add("3");
qu.add("1");
System.out.println(qu);
System.out.println(qu.poll());
System.out.println(qu);
输出结果:
[1, 2, 3, 1]
1
[2, 3, 1]
Map
Map接口及其实现类采用键值对的方式存储,长度可动态改变。
- HashMap:基于散列表的Map接口实现类,是线程不安全的。
- HashTable:实现方式与HashMap一致,但是线程安全的。
- TreeMap:根据红黑树算法实现的Map接口,支持自然排序,是线程不安全的。
HashMap
基于散列表的Map接口实现类,是线程不安全的。
假设张三的hashCode为17,则其table数组位置为:17%16=1,若存在多个余数为1的情况,则链接在前一个元素后。
HashMap中顶一个一些成员变量:
// hashMap数组的初始容量 16(大小必须是2的幂次方)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 负载因子 0.75f;(元素达到16*0.75个时,会自动扩容,负载因子大于1时,永不扩容)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树形化阈值 8(链表元素个数达到8个时,自动转换为红黑色)
static final int TREEIFY_THRESHOLD = 8;
// 解除树形化阈值 6(当链表的节点个数小于等于这个值时,会将红黑树转换成普通的链表。)
static final int UNTREEIFY_THRESHOLD = 6;
// 树形化的另一条件 Map数组的长度阈值 64(树形化阈值的第二条件。当数组的长度小于这个值时,就算树形化阈达标,链表也不会转化为红黑树,而是优先扩容数组resize()。)
static final int MIN_TREEIFY_CAPACITY = 64
// 这个就是hashMap的内部数组了,而Node则是链表节点对象。
transient Node<K,V>[] table;
// 数组扩容阈值。(即:HashMap数组总容量 * 加载因子。当前容量大于或等于该值时会执行扩容)
int threshold;
// 扩容的容量为当前HashMp的两倍
resize();
使用示例:
HashMap<Integer, String> hm = new HashMap<>();
hm.put(1, "中国");
hm.put(2, "江苏");
hm.put(3, "南京");
System.out.println(hm);
结果:{1=中国, 2=江苏, 3=南京}
HashTable
实现方式与HashMap一致,但是线程安全的,因此相对而言HashMap性能会高一些。
HashMap和Hashtable的区别:
-
线程安全
两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。
Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。 -
HashMap可以使用null作为key,而Hashtable则不允许null作为key
HashMap以null作为key时,总是存储在table数组的第一个节点上。 -
继承结构
HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类。 -
初始容量与扩容
HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。
HashMap扩容时是当前容量翻倍即:capacity2,Hashtable扩容时是容量翻倍+1即:capacity2+1。 -
两者计算hash的方法不同
-
Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取余。
int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length;
-
hashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取余。
int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } static int indexFor(int h, int length) { return h & (length-1);
-
TreeMap
是根据红黑树(平衡二叉树)算法实现的,并支持自然排序。
红黑树规则如下:
1、每个节点都只能是红色或者黑色
2、根节点是黑色
3、每个叶节点(NIL节点,空节点)是黑色的。
4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。