1.集合
数组:
- 长度开始需要指定,且指定后无法修改
- 保存的必须为同一类型元素
- 增/删元素时很麻烦,需要新建数组再重新赋值
集合:
- 可以动态保存任意多个对象
- 提供一系列方便的操作对象的方法
- 增/删元素时更加方便
集合框架体系:
- 单列集合
Collection
:以单个元素存在,比如list.add("psj")
- 双列集合
Map
:以键值对形式存在,比如hashMap.put(1,"psj")
2.Collection
2.1 基本特性
Collection
接口实现的子类可以存放多个元素,每个元素可以是object
:List list = new ArrayList(); list.add("psj"); list.add(10); System.out.println(list); // [psj, 10]
Collection
接口没有直接实现子类,而是通过子接口Set
和List
实现tips:
- 如果集合中有多个重复元素,
remove
方法移除的是集合中首先出现的元素,不会将所有元素移除
2.2 遍历元素
使用
Iterator
:
- 所有实现
Collection
接口的集合类都有一个iterator
方法,用于返回一个迭代器Iterator
仅用于遍历集合,本身不存放对象List list = new ArrayList(); list.add("psj"); list.add(10); list.add(true); Iterator iterator = list.iterator(); // 最开始的位置在集合首个元素的上方 while (iterator.hasNext()){ // 执行next方法后将iterator下移,并将下移后集合位置上的元素返回(返回类型是object) System.out.println(iterator.next()); }
使用增强
for
循环:
- 底层依旧使用
Iterator
List list = new ArrayList(); list.add("psj"); list.add(10); list.add(true); for (Object o : list) { System.out.println(o); }
2.3 List
List
接口的集合类中元素有序,即添加顺序和取出顺序一致,且可以重复List
接口的集合类中每个元素都有对应的顺序索引List list = new ArrayList(); list.add("psj"); list.add(10); list.add("psj"); list.add(true); for (Object o : list) { System.out.println(o); } //psj 10 psj true
2.3.1 ArrayList
- 可以存放空值:
List list = new ArrayList(); list.add(null); System.out.println(list); // [null]
- 底层由数组实现存储
- 线程不安全(没有
synchronized
关键字),但是执行效率高- 维护了一个
Object
类型的数组elementData
- 创建对象时如果使用无参构造器,则初始
elementData
容量为0,第一次添加元素会将elementData
扩容为10,再次扩容则将其容量扩容为当前数组大小的1.5倍;使用指定大小的构造器,初始容量为指定大小,扩容后容量变为当前数组大小的1.5倍// 使用无参构造器 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // DEFAULTCAPACITY_EMPTY_ELEMENTDATA为{} } // 执行add方法 private void add(E e, Object[] elementData, int s) { // 数组还没有添加元素时 if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; } // 第一次添加元素时扩容 private Object[] grow(int minCapacity) { // minCapacity表示此时添加元素需要的数组大小 return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity)); // 使用的copyOf保证扩容后原数据不会丢 } // newCapacity方法 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) return Math.max(DEFAULT_CAPACITY, minCapacity); // 第一次添加元素扩容为10的原因,DEFAULT_CAPACITY=10 int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容1.5倍的原因
tips:
transient
表示短暂的,被其修饰的属性不会被序列化ArrayList
中元素的添加/删除通过数组完成(即涉及到扩容),所以添加/删除效率较低,但是改/查的效率较高
2.3.2 Vector
- 底层由数组实现存储
- 维护了一个
Object
类型的数组elementData
- 线程安全(有
synchronized
关键字),但是效率不高- 创建对象时如果使用无参构造器,则初始
elementData
容量为0,第一次添加元素会将elementData
扩容为10,再次扩容则将其容量扩容为当前数组大小的2倍;使用指定大小的构造器,初始容量为指定大小,扩容后容量变为当前数组大小的2倍// 扩容2倍的原因,capacityIncrement表示在构造器中指定每次扩容的数量大小 int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
2.3.3 LinkedList
- 实现了双向链表和双端队列特点
- 底层维护了一个双向链表
- 属性包括一个
first
指针和一个last
指针- 线程不安全
tips:
LinkedList
中元素的添加/删除不是通过数组完成(即不涉及到扩容),所以添加/删除效率较高,但是改/查的效率较低LinkedList
添加时采用尾插法,删除时默认删除第一个节点
2.4 Set
- 无序(添加和取出的顺序不一致,但是取出的顺序是固定的),没有索引(即不能使用索引的方式遍历Set集合):
HashSet set = new HashSet(); set.add("psw2"); set.add(null); set.add("psj"); set.add("psw"); set.add(null); System.out.println(set); // 输出[null, psw, psj, psw2],调换添加顺序或者再次运行输出内容都是一样的
- 不允许重复元素(所以最多包含一个null),但是在怎样算重复元素和
add
方法的底层原理有关:HashSet set = new HashSet(); set.add(new Person("psj")); set.add(new Person("psj")); System.out.println(set); // [psj, psj] // Person类 class Person{ private String name; public Person(String name) { this.name = name; } @Override public String toString() { return name; } }
2.4.1 HashSet
HashSet
实际上是HashMap
(HashMap
的底层是数组+链表+红黑树,table[0...]
上也是存放元素的,并不是把元素全部存放在链表上):
public HashSet() { // 执行new HashMap<>()会初始化加载因子loadfactor=0.75,且table为null,添加元素时才会执行resize方法扩容 map = new HashMap<>(); }
add
方法的底层机制:
- 添加元素时会先得到hash值,再将该值转为索引值
- 查看
table
的索引位置上是否存放元素:没存放就直接加入;存放了就调用equals
方法比较,相同就不添加,不同就添加到链表最后- 链表的元素个数达到
TREEIFY_THRESHOLD
(默认为8)并且table
的大小达到MIN_TREEIFY_CAPACITY
(默认为64),就进行树化,否则依旧采用数组扩容机制public boolean add(E e) { return map.put(e, PRESENT)==null; // **PRESENT是final static修饰的对象,不会改变,起到占位的作用 } // add方法中的put方法,这是HashMap中定义的方法 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } // hash方法会调用key的hashCode方法,但是并不等于hashCode的值(为了尽量避免碰撞) static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } // putVal方法解析 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table是一个Node数组(如上图的数组) if ((tab = table) == null || (n = tab.length) == 0) // 在resize方法中声明了初始的table大小newCap为16,newThr=0.75*16=12,即当table中元素到了12个就要开始 准备扩容,防止一次性增加大量元素时出现卡顿 // newCap = DEFAULT_INITIAL_CAPACITY; // newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // resize方法最后返回一个初始化后的table n = (tab = resize()).length; // 根据key的hash值计算key应该存放在table中的位置,并将该位置的对象赋值给p // 如果该位置的对象为null,就创建Node并放置在该位置 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 如果该位置不为null,即碰巧不同对象计算i = (n - 1) & hash后的值相同,就要开始走链表或者红黑树 else { Node<K,V> e; K k; // 如果当前数组位置对应的链表的第一个位置和准备添加的key对象的hash值一样 // 并且满足下面两个条件之一: // (1) 准备添加的key对象和p指向的Node的key对象是同一个对象 // (2) p指向的Node的key对象和准备添加的key对象使用equals方法比较后相同 // 就不能加入,即不再遍历链表上的元素 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 判断p是不是红黑树 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 走到该步则比较的是当前数组位置对应的链表的第二个位置及之后的元素和准备添加的key对象 // 如果对应的数组位置已经是一个链表,就使用for循环依次比较: // (1)不相同就加入链表尾部,加入完后就break // (2)相同就break,不会添加 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 如果链表的长度达到TREEIFY_THRESHOLD就将链表进行树化判断 // (treeifyBin方法会先将table扩容,如果扩容后的table过大再进行树化) if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); // 返回null表示添加成功 return null; }
tips:
HashSet
第一次添加元素时,table
扩容到16,临界值为16*0.75=12
。如果table
中的元素个数达到12后(元素个数并不单指占据数组位置的个数,包括在链表上的元素个数),就扩容到16*2=32
,临界值为32*0.75=24
,依次类推table
扩容有两种情况:
table
中使用位置个数到达临界值table
某个位置上的链表到达TREEIFY_THRESHOLD
,之后每有一个元素到该链表上table
就扩容(链表在table
上的位置会发生改变)- 两个对象经过
hashcode
方法后得到的值不相同,但是经过hash
方法的(h = key.hashCode()) ^ (h >>> 16)
后的值可能相同,此时就会放置在table
数组上的同一个链表(能否放置还要继续用equals
方法判断)。假设要保证自定义对象属性值一样就在添加时一定无法加入,就要重写equals
方法和hashCode
方法:HashSet set = new HashSet(); set.add(new Person("psj")); set.add(new Person("psj")); System.out.println(set); // 只打印[psj],如果只重写equals方法或者hashCode方法会输出[psj,psj] // 重写方法 class Person{ private String name; public Person(String name) { this.name = name; } // 判断值 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return Objects.equals(name, person.name); } // 不同的对象使用原始的hashCode得到的值一定不相同,所以需要重写该方法,保证其值是根据的是属性值判断 // 属性值相同得到的hash值就相同,而不关心是否为同一个对象 @Override public int hashCode() { return Objects.hash(name); } }
- 对于
HashSet
的remove
方法也是依据hash
值进行删除的:// Person类重写了hashcode和equals方法 class Person{ public String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } } HashSet set = new HashSet(); Person p1 = new Person("psj", 11); Person p2 = new Person("psw", 12); set.add(p1); set.add(p2); p1.name = "psj2"; set.remove(p1); System.out.println(set); // 还是输出两个对象:p1对象的name值变了,p1对象的hash值也会变化(原始的hashcode只是根据对象的地址而定),执行remove根据当前p1的hash值找table表时发现没有该元素,所以删除失败 set.add(new Person("psj2", 11)); System.out.println(set); // 输出三个对象:尽管新建的对象和修改后p1的属性值一样,但是p1在table表的位置和原始位置一致,并没有占据新建对象的位置,所以新建的对象可以添加到table表中 set.add(new Person("psj",11)); System.out.println(set); // 输出四个对象:该对象和未修改前的p1计算的hash值一样,所以会判断是否能放置在p1所在链表上,但是由于name和修改后的p1不一样,根据重写的equals方法判断可以添加到链表中
2.4.2 LinkedHashSet
它是
HashSet
的子类:底层是一个
LinkeHashMap
,底层维护了数组+双向链表根据
hashCode
值决定元素存储位置,同时使用链表维护元素次序,使得元素看起来是以插入顺序保存的(即插入顺序和遍历顺序一致):
Set set = new LinkedHashSet(); set.add(456); set.add(456); set.add(123); set.add(new String("psw")); set.add(new String("psj")); System.out.println(set); // 打印[456, 123, psw, psj]
add
方法的底层机制:
- 第一次添加元素时,
table
数组扩容到16,且table
数组的类型为HashMap$Node
,但是存放的节点类型为LinkeHashMap$Entry
,说明Entry
是HashMap
的静态内部类Node
的子类:static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
- 在执行
add
方法时还是去执行HashSet
的add
方法,:public boolean add(E e) { return map.put(e, PRESENT)==null; // PRESENT是final static修饰的对象,不会改变,起到占位的作用 }
2.4.3 TreeSet
TreeSet
底层是TreeMap
,但是使用add
方法时value
值依旧是固定的PRESENT
:public TreeSet() { this(new TreeMap<>()); } public boolean add(E e) { return m.put(e, PRESENT)==null; }
- 要实现指定排序方式,可以使用
TreeSet
提供的构造器,在其中传入一个比较器:TreeSet set = new TreeSet(new Comparator() { @Override public int compare(Object o1, Object o2) { return ((String)o2).compareTo((String)o1); } }); set.add("tom"); set.add("jack"); set.add("psj"); set.add("aim"); set.add("bill"); System.out.println(set); // [tom, psj, jack, bill, aim]
tips:
- 不同于
HashMap
根据hash
值确定能否添加成功,TreeSet
能否成功添加元素和定义的比较器有关,不在于添加的元素是否相等:TreeSet set = new TreeSet(new Comparator() { @Override public int compare(Object o1, Object o2) { return ((String) o2).length() - ((String) o1).length(); } }); set.add("tom"); set.add("jack"); set.add("psj"); System.out.println(set); // 只会打印[jack, tom] // 尽管tom和psj是两个不同的字符串对象,但是根据TreeMap中的put方法: if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); // 判断完这两个字符串长度一致后会执行该行代码,不会再往下执行,即不会添加 } while (t != null); } ... Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e;
- 如果使用无参构造器创建
TreeSet
对象,会调用key
值类型的比较器进行比较:// TreeMap中的put方法 if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; // k对象的运行类型还是key所属的类型 do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null);
- 在自定义类中如果没有实现
Comparable
接口,将该类的对象添加到TreeSet
中会报异常:// A类 class A{} public static void main(String[] args) { TreeSet set = new TreeSet(); set.add(new A()); // 报ClassCastException异常, } // 执行add方法会执行到下面方法,因为A类没有实现Comparable接口,所以执行((Comparable<? super K>)k1)会报类型转换异常(将一个类转为毫无关系的接口肯定报异常) final int compare(Object k1, Object k2) { return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2) : comparator.compare((K)k1, (K)k2); }
3.Map
3.1 基本特性
Map
中的key
和value
可以是任何引用类型,会封装到HashMap$Node
Map
中的key
不允许重复,如果有重复的key
添加会将新的value
进行替换Map
中的value
可以重复Map
中的key
和value
都可以为null
,但是key
只能有一个为null
,value
可以有多个tips:
HashSet
的底层是HashMap
,只不过把键值对的value
固定为PRESENT
HashMap
的插入顺序和遍历顺序不一致(分析HashSet
源码可以得知)- 为了方便遍历,会创建
EntrySet
集合,存放类型为接口Entry<K,V>
,把HashMap$Node
(Entry
的实现类)对象存放到EntrySet
中方便遍历是因为Map.Entry
提供了getKey
和getValue
方法,所以Map
中的keys
和values
并不是直接分别放在Set
集合和Collection
集合中,还是通过遍历Map.Entry
得到key
和value
值:Map map = new HashMap(); map.put(new A("psj"), "psj"); map.put(new A("psw"), "psw"); Set entrySet = map.entrySet(); for (Object obj : entrySet) { Map.Entry entry = (Map.Entry) obj; System.out.println(entry.getKey()); System.out.println(entry.getValue()); } // 在下面两个方法中都有该方法:public final void forEach(Consumer<? super V> action) Set keys = map.keySet(); Collection values = map.values();
3.2 遍历元素
- 取出所有的
key
值,通过key
值得到value
:Map map = new HashMap(); map.put(1, "psj"); map.put(2, "psw"); map.put(3, "psj2"); map.put(4, "psw2"); Set keySet = map.keySet(); // 使用增强for循环 for (Object key : keySet) { System.out.println(map.get(key)); } // 使用迭代器 while (iterator.hasNext()){ Object key = iterator.next(); System.out.println(map.get(key)); }
- 直接取出所有的
value
:Collection values = map.values(); // 使用增强for循环 for (Object value : values) { System.out.println(value); } // 使用迭代器 Iterator iterator = values.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); }
- 通过
EntrySet
获取K-V
对:Set entrySet = map.entrySet(); for (Object entry : entrySet) { Map.Entry m = (Map.Entry) entry; System.out.println(m.getKey()); System.out.println(m.getValue()); } Iterator iterator = entrySet.iterator(); while (iterator.hasNext()) { //正常情况下应该要向下转型为HashMap$Node类型,但是该内部类Node为默认访问修饰符,无法访问,只能转为接口 Entry(Node实现了该接口),但是运行类型还是Node Map.Entry entry = (Map.Entry) iterator.next(); System.out.println(entry.getKey()); System.out.println(entry.getValue()); }
3.3 HashMap
HashMap
没有实现同步,所以线程不安全(没有synchronized
关键字)- 扩容机制和
HashSet
一致(具体参考2.4.1
源码分析)- 当
key
值相同时会走该步判断进行value
值的替换:if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
- 当桶中节点数由于移除或者
resize
(扩容) 变少后,红黑树会转变为普通的链表,这个阈值是UNTREEIFY_THRESHOLD
(默认值6)- 红黑树虽然查询效率比链表高,但是结点占用的空间大,只有达到一定的数目才有树化的意义
3.4 Hashtable
Hashtable
存放的key
值和value
值都不能存放null
,会抛出空指针异常Hashtable
线程安全(方法中有synchronized
关键字),但是效率没HashMap
高- 不同于
HashMap
初始化时table
为null
,在添加元素时执行resize
方法才将数组大小扩容为16,Hashtable
初始化生成的table
数组大小为11,同时也会初始化加载因子:// Hashtable初始化 public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); } // HashMap初始化 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; }
- 扩容机制和
HashMap
类似,当到达阈值threshold=initialCapacity * loadFactor
进行扩容(并不是table
大小超过阈值才扩容,HashMap
需要超过阈值才扩容):int newCapacity = (oldCapacity << 1) + 1; // 扩容后的table大小,不同于HashMap直接乘以2,而是再加上1 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); // 阈值计算方式一样
HashMap
的table
数组类型为Node
,而Hashtable
为Entry
3.5 Properties
- 继承
Hashtable
类,并且实现了Map
接口- 主要运用在从
xxx.properties
配置文件中加载数据到Properties
类的对象,然后进行读取和修改- 存放的
key
值和value
值都不能存放null
,会抛出空指针异常- 和
HashMap
、Hashtable
一样,添加和取出的顺序不一致,但是取出的顺序是固定的,并且没有索引(即不能使用get(index)
的方式遍历)
3.6 TreeMap
- 要实现指定排序方式,可以使用
TreeMap
提供的构造器,在其中传入一个比较器(具体操作可以参考2.4.3)tips:
- 如果使用无参构造器创建
TreeMap
对象,会调用key
值类型的比较器进行比较:// put方法 if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; // k对象的运行类型还是key所属的类型 do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null);
4.如何选择集合实现类
- 判断存储的类型
- 一组对象使用
Collection
接口- 一组键值对使用
Map
接口- 对于
Collection
接口:
- 允许重复使用
List
接口:
- 增/删操作多:
LinkedList
(底层维护了一个双向链表)- 查/改操作多:
ArrayList
(底层维护了一个可变数组)- 不允许重复使用
Set
接口:
- 无序:
HashSet
(底层是HashMap
,维护了一个哈希表,即数组+链表+红黑树)- 有序:
TreeSet
(底层是TreeMap
,通过定义比较器可以自定义排序规则)- 插入和取出顺序一致:
LinkedHashSet
(底层是LinkedHashMap
,维护数组+双向链表)- 对于
Map
接口:
- 键无序:
HashMap
(底层维护了一个哈希表,即数组+链表+红黑树,jdk8
之前为数组+链表)- 键有序:
TreeMap
- 键插入顺序和取出顺序一致:
LinkedHashMap
(底层是HashMap
)- 读取文件:
Properties
5.Collections工具类
Collections
是一个操作Set
、List
和Map
等集合的工具类- 提供了一系列静态方法对集合元素进行排序、查询和修改等操作:
ArrayList list = new ArrayList(); list.add("1"); list.add(2); // 打印 System.out.println(list); // [1, 2] Collections.reverse(list); System.out.println(list); // [2, 1]
tips:
- 使用
Collectios
的sort
、max
等方法时,需要保证集合内存放的元素是相同类型的