Java容器的分类
List
ArrayList
源码
public static vooid main(String[] args){ArrayList<String> list = new ArrayList<>();list.add("hello");list.add(1, "hello");list.remove("hello");
}
list.add("hello");
// 默认列表的初始容量
private static final int DEFAULT_CAPACITY = 10;
// 定义一个空数组,用于一个空的实例
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于默认大小空实例的共享空数组实例
// 我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// object数组定义
transient Object[] elementData;
// ArrayList所包含的元素的数量
private int size;
//带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始
public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];// 如果参数等于0,则创建空数组} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);}
}
// 默认大小的构造方法
// DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
public ArrayList(Collection<? extends E> c) {//将指定集合转换为数组elementData = c.toArray();//如果elementData数组的长度不为0if ((size = elementData.length) != 0) {// 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断) if (elementData.getClass() != Object[].class)//将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组elementData = Arrays.copyOf(elementData, size, Object[].class);} else {// 其他情况,用空数组代替this.elementData = EMPTY_ELEMENTDATA;}
}
ArrayList扩容机制
扩容机制,默认先扩大到原数组的1.5倍,如果还不够就扩大到新数组的大小,如果够了就保持之前的1.5倍大小。
ArrayList 中的容量使⽤完之后,则需要对容量进⾏扩容:
-
ArrayList 容量使⽤完后,会“⾃动”创建容量更⼤的数组,并将原数组中所有元素拷⻉过去,这会导致效率降低 。
-
优化:可以使⽤构造⽅法 ArrayList (int capacity) 或 ensureCapacity(int capacity) 提供⼀个初始化容量,避免刚开始就⼀直扩容,造成效率较低。
private Object[] grow() {return grow(size + 1);
}
private Object[] grow(int minCapacity) {// 原数组长度int oldCapacity = elementData.length;// 如果数组长度大于0或者数组实例不是默认创建的数组实例// 进行扩容if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, /* minimum growth */oldCapacity >> 1 /* preferred growth */);return elementData = Arrays.copyOf(elementData, newCapacity);} else {// 否则直接new一个默认为10长度的数组或者指定大小容量的数组对象return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];}
}
list.add(1, "hello");
public void add(int index, E element){rangeCheckForAdd(index);ensureCapacityInternal(size + 1);System.arraycopy(elementData, index, elementData, index + 1, size - index);elementData[index] = element;size ++;
}
private void rangeCheckForAdd(int index){if(index > size || index < 0){throw new IndexOfBoundsException(outOfBoundsMsg(index));}
}
list.remove("hello");
public boolean remove(Object o){if(o == null){for(int index = 0;index < size;index ++){if(elementData[index] == null){fastRemove(index);return true;}}}else{for(int index = 0;index < size;index ++){if(o.equals(elementData[index])){fastRemove(index);return true;}}}return false
}
底层原理:
-
在构造ArrayList时,如果没有指定容量,那么内部会构造一个空数组,如果指定了容量,那么就构造出对应容量大小的数组。
-
在添加元素时,会先判断数组容量是否足够,如果不够则会扩容,扩容按照1.5倍扩容,容量足够后,再把元素添加到数组中。
-
在添加元素时,如果指定了下标,先检查下标是否越界,然后再确认数组容量是否足够,不够则扩容,然后再把新元素添加到指定位置,如果该位置后面有元素则后移。
-
在获取指定下标的元素时,先检查下标是否越界,然后从数组中取出对应位置的元素。
ArrayList 构造⽅法:
-
ArrayList():创建⼀个初始化容量为 10 的空列表
-
ArrayList(int initialCapacity):创建⼀个指定初始化容量为 initialCapacity 的空列表
-
ArrayList(Collection c):创建⼀个包含指定集合中所有元素的列表
ArrayList 优缺点:
优点:
-
向 ArrayList 末尾添加元素(add() ⽅法)时,效率较⾼
-
查询效率⾼
缺点:
-
扩容会造成效率较低(可以通过指定初始化容量,在⼀定程度上对其进⾏改善)
-
另外数组⽆法存储⼤数据量(因为很难找到⼀块很⼤的连续的内存空间)
-
向 ArrayList 中间添加元素(add(int index)),需要移动元素,效率较低 1. 但是,向 ArrayList 中间位置增/删元素的情况较少时不影响; 2. 如果增/删操作较多,可以改⽤链表
LinkedList
源码:
public static void main(String[] args){LinkedList arrayList = new LinkedList<>();}
成员变量:
public class LinkedList<E> extends AbstractSequentialList<E>implements List<E>, Deque<E>, Cloneable, java.io.Serializable{// 序列化唯一表示 UIDprivate static final long serialVersionUID = 876323262645176354L;// LinkedList的大小,其实就是其内部维护的双向链表存储元素的数量transient int size = 0;// 头结点,指向第一个节点的指针或引用,默认为 nulltransient Node<E> first;// 尾节点,指向最后一个节点的指针或引用,默认为 nulltransient Node<E> last;private static class Node<E> {E item; //元素Node<E> next;//指向后一个元素的指针Node<E> prev;//指向前一个元素的指针
Node(Node<E> prev, E element, Node<E> next) {this.item = element;this.next = next;this.prev = prev;}
}
add(E e)
将指定的元素追加到此列表的末尾
public boolean add(E e) {linkLast(e);return true;
}void linkLast(E e) {//记录当前末尾节点final Node<E> l = last; //产生一个新的节点 该节点的前驱节点是之前的last,后继节点是nullfinal Node<E> newNode = new Node<>(l, e, null);//将last 尾指针指向新的节点 last = newNode; if (l == null)first = newNode; //链表中没有节点, 头指针指向新节点elsel.next = newNode; // 当前last的next指向新节点size++;//更新节点数量modCount++;
}
步骤:
1:记录当前末尾节点,用添加元素产生一个新的节点
2:将last指针指向新的尾节点
3:情况1:如果之前的尾节点为空,表明链表中没有元素,头指针first指向新节点
4:情况2:如果之前的链表不为空,旧的尾节点的next指针指向新的尾节点
add(int index,E element)
指定位置插入指定元素
public void add(int index, E element) {checkPositionIndex(index);// 校验索引位置if (index == size) // 在最后插入linkLast(element);else linkBefore(element, node(index));// 在索引位置之前插入
}// 折半查找,返回指定索引位置的节点
//(1)首先确定index的位置,,是靠近first还是靠近last
//(2)靠近first就从头开始查,否则就从尾部开始
Node<E> node(int index) {// assert isElementIndex(index);if (index < (size >> 1)) { // 前半段Node<E> x = first;for (int i = 0; i < index; i++)x = x.next;return x;} else { // 后半段Node<E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}
}void linkBefore(E e, Node<E> succ) {// succ是之前index位置上的节点,将e插到succ节点前面// assert succ != null;final Node<E> pred = succ.prev; // 拿到succ节点的prev节点//构造新节点,新插入节点的前驱节点是succ的前一个位置的节点,后继节点是succ节点final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode;// 将succ节点的prev属性设置为newNodeif (pred == null) first = newNode; // 之前只有一个节点elsepred.next = newNode; //将pred节点的next属性设为newNodesize++;modCount++;
}
get(int index):
public E get(int index) {checkElementIndex(index); // 检查index是否越界return node(index).item; // 根据index,调用node方法寻找目标节点,返回目标节点的item
}
remove(int index):
public E remove(int index) {checkElementIndex(index); //检查index index >= 0 && index < size;return unlink(node(index));
}
remove(Object o)
public boolean remove(Object o) {//判断删除的元素是否是nullif (o == null) {for (Node<E> x = first; x != null; x = x.next) {if (x.item == null) {unlink(x);return true;}}} else {for (Node<E> x = first; x != null; x = x.next) {if (o.equals(x.item)) {unlink(x);return true;}}}return false;
}
unlink(Node<E> e)
E unlink(Node<E> x) {// 移除链表上的x节点// assert x != null;final E element = x.item; // x节点的值final Node<E> next = x.next;// x节点的下一个节点final Node<E> prev = x.prev;// x节点的上一个节点if (prev == null) {//如果prev为空,则代表x节点为头节点,删除的是第一个节点,first向后移动first = next;} else {// x不为头节点prev.next = next; //将prev节点的next书香指向x节点的next属性x.prev = null; // 将x节点prev置空}if (next == null) { // 如果next为空,则代表x节点为尾节点,则将last指向prev即可last = prev;} else {// x不是尾节点next.prev = prev; // 将next节点的prev属性指向x节点的prev属性x.next = null;// 该节点next置空}x.item = null; // 将x的值清空,以便垃圾回收器回收对象size--; // 节点个数减1modCount++;return element;
}
数据结构: LinkedList 底层是⼀个双向链表
-
优点: 增/删效率⾼
-
缺点: 查询效率较低
-
LinkedList 也有下标,但是内存不⼀定是连续的
-
LinkedList 可以调⽤ get(int index) ⽅法,返回链表中第 index 个元素 但是,每次查找都要从头结点开始遍历
ListIterator 接⼝
1、LinkedList.add ⽅法只能将数据添加到链表的末尾
2、如果要将对象添加到链表的中间位置则需要使⽤ ListIterator 接⼝的 add ⽅法
-
ListIterator 是 Iterator 的⼀个⼦接⼝
3、Iterator 中 remove ⽅法
-
调⽤ next 之后,remove ⽅法删除的是迭代器左侧的元素(类似键盘的 backspace)
-
调⽤ previous 之后,remove 删除的是迭代器右侧的元素
4、ListIterator 中 add ⽅法
1. 调⽤ next 之后,在迭代器左侧添加⼀个元素 2. 调⽤ previous 之后,add 是在迭代器右侧添加元素
底层结构 | 增删效率 | 改查效率 | |
---|---|---|---|
ArrayList | 可变数组 | 较低、数组扩容 | 较高 |
LinkedList | 双向链表 | 较高、通过链表追加 | 较低 |
ArrayList 和 LinkedList 有什么区别:
-
内部实现不同。ArrayList 是使用数组的实现,通过索引来访问元素,支持快速随机访问。LinkedList 内部使用双向链表来实现,每个元素都包含对前一个元素和后一个元素的引用,适合插入和删除操作。
-
数据访问的时间复杂度不同。ArrayList 由于使用数组的实现,它的时间复杂度是O(1),即常量复杂度。LinkedList 需要从头部或者尾部开始遍历链表,知道找到目标元素的位置,它的时间复杂度是O(n)。
-
空间占用不同。ArrayList 使用数组来存储数据,所以占用的空间是连续的。LinkedList 使用链表来访问元素,每个元素都包含对前一个元素和后一个元素的引用,占用的空间相对比较大。
Vector
-
Vector 底层是数组
-
初始化容量为 10
-
扩容: 原容量使⽤完后,会进⾏扩容。新容量扩⼤为原始容量的 2 倍
-
Vector 是线程安全的(⾥⾯⽅法都带有 synchronized 关键字),效率较低,现在使⽤较少
-
如何将 ArrayList 变成线程安全的?
-
调⽤ Collections ⼯具类中的 static List synchronizedList(List list) ⽅法
-
Set
不能通过下标取值,没有get()方法
泛型的好处:
1、减少了强制类型转换的次数 获取数据值更⽅便
2、类型安全 调⽤⽅法时更安全
3、泛型只在编译时期起作⽤ 运⾏阶段 JVM 看不⻅泛型类型(JVM 只能看⻅对应的原始类型,因为进⾏了类型擦除)
4、带泛型的类型
在使⽤时没有指定泛型类型时,默认使⽤ Object 类型
List list = new HashTestarrayList(); // 默认可以放任意 Object 类型
5、lambda 表达式
代码:
// set集合的共同点:都是唯一
// HashSet:底层是 数组+链表+红黑树 ===> 哈希表
// TreeSet:底层是 红黑树
// linkedHashSet:底层是 链表+哈希表
public static void main(String[] args){Set<String> set1 = new HashSet<>();Set<String> set2 = new TreeSet<>();set<String> set3 = new LinkedHashSet<>();set1.add("A");set1.add("C");set1.add("B");set1.add("E");set1.add("A"); // 重复的元素不可添加,保证唯一System.out.println(set1); // HashSet存储无序set2.add("A");set2.add("C");set2.add("B");set2.add("E");set2.add("A");System.out.println(set2); // TreeSet存储无序set3.add("A");set3.add("C");set3.add("B");set3.add("E");set3.add("A");System.out.println(set3); // 链表保证输出有序set1.addALL();
}
output:
[A,B,C,E]
[A,B,C,E]
[A,C,B,E]
HashSet
无序、唯一、相同的覆盖
-
HashSet实现了Set接口
-
HashSet实际上是HashMap
public HashSet(){map = new HashMap<>(); }
-
可以存放null值,但只能有一个null
-
HashSet不保证元素是有序的,取决于hash后,再确定索引的结果
-
不能有重复元素/对象
HashSet底层机制
HashSet底层是HashMap,HashMap底层是数组+链表+红黑树
-
HashSet底层是HashMap。
-
添加一个元素时,先得到一个hash值 - 会转成-> 索引值。
-
找到存储数据表table,看这个索引位置是否已经存放的有元素。
-
如果没有,直接加入。
-
如果有,调用equals比较,如果相同,则放弃添加,如果不相同,则添加到最后。
-
在java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64)就会进行树化(红黑树)。
扩容机制:
public boolean add(E e) {return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}
value值永远定义为object对象PRESENT
private static final Object PRESENT = new Object();
通过这个PRESENT值计算一个hash值,目的是为了防止重叠
h = key.hashCode()) ^ (h >>> 16) // 无符号右移16位
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
扩容机制&转成红黑树机制
-
HashSet底层是HashMap,第一次添加时,table 数组扩容到 16,临界值(threshold)是 16*加载因子(loadFactor)是0.75= 12。
-
如果table 数组使用到了临界值 12,就会扩容到 16 * 2=32,新的临界值就是32 * 0.75= 24,依次类推。
-
在Java8中,如果一条链表的元素个数到达 TREEIFY THRESHOLD(默认是8)。并且table的大小 >= MIN_ TREEIFY_ CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。
LinkedHashSet
-
LinkedHashSet是HashSet的子类
-
LinkedHashSet底层是一个LinkedHashMap,底层维护了一个 数组+双向链表
-
LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,使元素看起来是以插入的顺序保存的
-
LinkedHashSet不允许添加重复元素
-
LinkedHashset 加入顺序和取出元素/ 数据的顺序一致
-
LinkedHashset 底层维护的是一个LinkedHashMap(是HashMap的子类)
-
LinkedHashset 底层结构(数组table+双向链表)
-
添加第一次时,直接将 数组table扩容到 16,存放的结点类型是 LinkedHashMap$Entry
-
数组是 HashMap$Node[]
-
存放的元素/数据是 LinkedHashMap$Entry类型
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);} }
TreeSet
TreeSet ⽆序(没下标),不可重复,但是可以排序
HashSet 为 HashMap 的 key 部分;
TreeSet 为 TreeMap 的 key 部分。
-
当使用无参构造器,创建TreeSet时,仍然是无序的
TreeSet treeSet = new TreeSet();
-
使用Comparator时,返回有序的TreeSet
TreeSet treeSet = new TreeSet(new Comparator() {@Overridepublic int compare(Object o1, Object o2) {return ((String)o2).compareTo((String)o1);// return ((String)o1).length() - ((String)o2).length(); // 字符串长度相同就判断为相同}
});
底层源码:
// 1.构造器把传入的比较器对象,赋给了TreeSet的底层的TreeSet的底层的TreeMap的属性this.comparator
public TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;
}
// 2.再调用treeSet.add("aa"),底层会调用public V put(K key, V value)
if (cpr != null) { // cpr是匿名内部类(对象) do {parent = t;// 动态绑定到匿名内部类(对象)cmp = cpr.compare(key, t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;else // 如果相等,即返回0,这个数据就加入return t.setValue(value);} while (t != null);
}
Map
Map接口和常见方法
-
Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value(双列元素)。
public static void main(String[] args) {Map map = new HashMap();map.put("aa", 1);
}
-
Map 中的key 和value 可以是任何引用类型的数据,会封装到HashMap$Node对象中。
-
Map 中的key 不允许重复,原因和Hashset 一样。
-
Map 中的value 可以重复。
-
Map 的key 可以为mull, value 也可以为null,注意 key 为null, 只能有一个,value 为null,可以多个。
-
常用String类作为Map的 key。
-
key 和vaiue 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value。
常用方法:
-
put():添加
-
remove():根据键删除映射关系
-
get():根据键获取值
-
size():获取元素个数
-
isEmpty():判断个数是否为零
-
clear():清除
-
containsKey():查找键是否存在
Map接口遍历方法
//第一组:先取出 所有的Key, 通过Key 取出对应的Value
Set keySet = map.keySet();
//(1) 增强for
for(Object key : keyset){System.out.println(key + "-" + map.get(key));
}
//(2) 迭代器
Iterator iterator = keyset.iterator();
while(iterator.hasNext()){Object next = iterator.next();System.out.println(key + "-" + map.get(key))
}
//第二组:把所有的values取出
Collection values = map.values();
//这里可以使用所有的Collections使用的遍历方法
//(1)增强for
for(Object value : values){System.out.println(value);
}
//(2)迭代器
Iterator iterator2 = values.iterator();
while(iterator2.hasNext()){Object value = iterator2.next();System.out.println(value)
}
//第三组:通过EntrySet 来获取k-v
Set entrySet = map.entrySet();
//(1)增强for
for(Object entry : entrySet){Map.Entry m = (Map.Entry) entry();System.out.println(m.getKey() + "-" + m.getValue());
}
//(2)迭代器
Iterator iterator3 = entrySet.itertor();
while(iterator3.hasNext()){Object next = iterator3.next();Map.Entry m = (Map.Entry) entry();System.out.println(m.getKey() + "-" + m.getValue());
}
HashMap
-
HashMap 底层是⼀个数组
-
数组中每个元素是⼀个单向链表(即,采⽤拉链法解决哈希冲突) 单链表的节点每个节点是 Node 类型
-
同⼀个单链表中所有 Node 的 hash值不⼀定⼀样,但是他们对应的数组下标⼀定⼀样 数组下标利⽤哈希函数/哈希算法根据 hash值计算得到的
-
HashMap 是数组和单链表的结合体
-
数组查询效率⾼,但是增删元素效率较低
-
单链表在随机增删元素⽅⾯效率较⾼,但是查询效率较低
-
HashMap 将⼆者结合起来,充分它们ݱ自的优点
-
-
HashMap 特点
-
⽆序、不可重复
-
⽆序:因为不⼀定挂在那个单链表上了
-
-
为什么不可重复?
通过重写 equals ⽅法保证的
-
k-v 最后是 HashMap$Node node = newNode (hash, key, valve, null)
-
k-v 为了方便程序员的遍历,还会 创建 EntrySet 集合,该集合存放的元素的类型 Entry,而一个Entry对象就有K,V Entryset<Entry<k,V>>即:transient Set<Map.Entry<K, V>> entrySet;
-
entrySet中,定义的类型 Map.entry,但是实际上存放的还是HashMap$Node 这是因为 static class Node<K, V> implements Map.Entry<K, V>
-
当把 HashMap$Node 对象 存放到 entrySet 方便遍历,因为Map.Entry提供了重要方法 K getKey(); V getValue();
小结:
-
Map接口的常用实现类:HashMap、Hashtable和Properties。
-
HashMap是 Map 接口使用频率最高的实现类。
-
HashMap 是以 key-val 对的方式来存储数据(HashMap$Node类型)。
-
key 不能重复,但是值可以重复,允许使用null健和null值。
-
如果添加相同的key,则会覆盖原来的key-val,等同于修改.(key不会替换,val会替换)。
-
与Hashset一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。
-
HashMap没有实现同步,因此是线程不安全的。
扩容机制:
-
HashMap底层维护了Node类型的数组table,默认为null。
-
当创建对象时,将加载因子(loadfactor)初始化为0.75。
-
当添加key-val时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素。如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key相是否等,如果相等,则直接替换val:如果不相等需要判断是树结构还是键表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
-
第1次添加,则需要扩容table容量为16,临界值(threshold)为12。
-
以后再扩容,则需要扩容table容量为原来的2倍,临界值为原來的2倍,即24,依次类推。
-
在Java8中,如果一条链表的元素个数超过 TREEIFY THRESHOLD(默认是8),并且 table的大小>=MIN_TREEIFY_ CAPACITY((默认64),就会进行树化(红黑树)。
-
HashMap 默认初始化容量: 16
-
必须是 2 的次幂,这也是 jdk 官⽅推荐的
-
这是因为达到散列均匀,为了提⾼ HashMap 集合的存取效率,所必须的
-
-
HashMap 默认加载因⼦:0.75
数组容量达到 3/4 时,开始扩容
-
JDK 8 之后,对 HashMap 底层数据结构(单链表)进⾏了改进
-
如果单链表元素超过8个,则将单链表转变为红黑树;
-
如果红黑树节点数量⼩于6时,会将红黑树重新变为单链表。
-
这种⽅式是为了提⾼检索效率,⼆叉树的检索会再次缩小扫描范围,提⾼效率
底层源码:
/*
1. 执行构造器 new HashMap()初始化加载因子 loadfactor = 0.75HashMap$Node[] table = null
*/
Map map = new HashMap();
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // 当创建对象时,将加载因子(loadfactor)初始化为0.75。
}
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 当创建对象时,将加载因子(loadfactor)初始化为0.75。
/*
2. 执行put 会调用hash方法计算key的hash值
*/
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/*
3. 执行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;if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}
put() ⽅法原理
-
先将 key, value 封装到 Node 对象中
-
底层会调⽤ key 的 hashCode() ⽅法得出 hash 值
-
通过哈希函数/哈希算法,将 hash 值转换为数组的下标
-
如果下标位置上没有任何元素,就把 Node 添加到这个位置上;
-
如果下标位置上有但链表,此时会将当前 Node 中的 key 与链表上每⼀个节点中的 key 进⾏ equals ⽐较
-
如果所有的 equals ⽅法返回都是 false,那么这个新节点 Node 将被添加到链表 的末尾;
-
如果其中有⼀个 equals 返回了 true,那么链表中对应的这个节点的 value 将会被 新节点 Node 的 value 覆盖。(保证了不可重复)
-
-
注:
-
HashMap 中允许 key 和 value 为 null,但是只能有⼀个(不可重复)!
-
HashTable 中 key 和 value 都不允许为 null。
get() ⽅法原理
-
先调⽤ key 的 hashCode() ⽅法得出 hash 值
-
通过哈希函数/哈希算法,将 hash 值转换为数组的下标
-
通过数组下标快速定位到数组中的某个位置:
-
如果这个位置上什么也没有(没有链表),则返回 null;
-
如果这个位置上有单链表,此时会将当前 Node 中的 key 与链表上每⼀个节点中的 key 进⾏ equals ⽐较。
-
如果所有的 equals ⽅法返回都是 false,那么 get ⽅法返回 null;
-
如果其中有⼀个 equals 返回了 true,那么这个节点的 value 便是我们要找的 value,此时 get ⽅法最终返回这个要找的 value。
-
-
注:
-
放在 HashMap 中 key 的元素(或者放在 HashSet 中的元素)需要同时重写 hashCode() 和 equals() ⽅法!!!
同时重写 hashCode() 和 equals() ⽅法
-
重写 hashCode() ⽅法时要达到散列分布均匀
-
如果 hashCode() ⽅法返回⼀个固定的值,那么 HashMap 底层则变成了⼀个单链表;
-
如果 hashCode() ⽅法所有返回的值都不同,此时 HashMap 底层则变成了⼀个数组。
-
这两种情况称之为,散列分布不均匀
equals 和 hashCode⽅法⼀定要同时重写(直接⽤ eclipse ⽣成就⾏)
TreeMap
-
TreeSet/TreeMap 是自平衡二叉树
-
TreeSet/TreeMap 迭代器采⽤的是中序遍历⽅式
-
TreeMap 特点:⽆序,不可重复,但是可排序(使用comparator构造器)
排序规则:
-
TreeSet/TreeMap中key 可以⾃动对 String 类型或8⼤基本类型的包装类型进⾏排序
-
TreeSet ⽆法直接对⾃定义类型进⾏排序
-
直接将⾃定义类型添加到 TreeSet/TreeMap中key 会报错 java.lang.ClassCastException 是因为⾃定义没有实现 java.lang.Comparable 接⼝(此时,使⽤的是 TreeSet 的⽆参构造器) 对 TreeSet/TreeMap 中 key部分元素,必须要指定排序规则。
-
主要有两种解决方法:
-
⽅法⼀: 放在集合中的⾃定义类型实现 java.lang.Comparable 接⼝,并重写 compareTo ⽅法
-
⽅法⼆: 选择 TreeSet/TreeMap 带⽐较器参数的构造器 ,并从写⽐较器中的 compare ⽅法 此时,在传递⽐较器参数给 TreeSet/TreeMap 构造器时,有 3 种⽅法:
-
-
定义⼀个 Comparator 接⼝的实现类
-
使⽤匿名内部类
-
lambda 表达式(Comparator 是函数式接⼝)
-
利⽤ -> 的 lambda表达式 重写 compare ⽅法
-
利⽤ Comparator.comparing ⽅法
-
-
-
-
两种解决方法如何选择?
-
当⽐较规则不会发⽣改变的时候,或者说⽐较规则只有⼀个的时候,建议实现 Comparable 接⼝
-
当⽐较规则有多个,并且需要在多个⽐较规则之间频繁切换时,建议使⽤ Comparator 接⼝
// 使用默认构造器是无序的
TreeMap treeMap = new TreeMap();
// 使用comparator构造器,可以实现排序
TreeMap treeMap = new TreeMap(new Comparator() {@Overridepublic int compare(Object o1, Object o2) {// 按照传入的key的大小排序return ((String)o1).compareTo((String)o2);// return ((String)o1).length() - ((String)o2).length(); // 按照传入的key的大小排序}
});
底层源码:
TreeMap底层是Entry不是Node
// 1.构造器,把传入的实现了Comparator接口的匿名内部类(对象),传给treeMap的comparator
public TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;
}
// 2.调用put方法
// 2.1第一次添加 把k-v封装到Entry对象,放入
Entry<K,V> t = root;if (t == null) {compare(key, key); // type (and possibly null) check 检查key是否为null
root = new Entry<>(key, value, null);size = 1;modCount++;return null;}
}
// 2.2以后添加
Comparator<? super K> cpr = comparator;
if (cpr != null) {do { // 遍历所有key,给当前的key找到适当的位置parent = t;cmp = cpr.compare(key, t.key); // 动态绑定到匿名内部类的comporeif (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;else // 如果遍历过程中,发现准备添加key和当前已有的key相等,就不添加return t.setValue(value);} while (t != null);
}
Hashtable
-
存放的元素是键值对:即K-V
-
hashtable的键和值都不能为null, 否则会拋出NullPointerException
-
hashTable 使用方法基本上和HashMap一样
-
hashTable 是线程安全的,hashMap 是线程不安全的
底层原理:
-
底层有数组 Hashtable$Entry[] 初始化大小为 11
-
临界值 threshold 8= 11* 0.75
-
扩容:按照自己的扩容机制来进行即可。
-
执行 方法 addEntry (hash, key, valve, index);添加K-V 封装到Entry
-
当if(count >= threshold) 满足时,就进行扩容
-
按照 int newCapacity = (oldCapacity << 1)+ 1;的大小扩容。
底层源码:
public synchronized V put(K key, V value) {// Make sure the value is not nullif (value == null) {throw new NullPointerException();}// Makes sure the key is not already in the hashtable.Entry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;Entry<K,V> entry = (Entry<K,V>)tab[index];for(; entry != null ; entry = entry.next) {if ((entry.hash == hash) && entry.key.equals(key)) {V old = entry.value;entry.value = value;return old;}}addEntry(hash, key, value, index);return null;
}
private void addEntry(int hash, K key, V value, int index) {modCount++;
Entry<?,?> tab[] = table;if (count >= threshold) {// Rehash the table if the threshold is exceededrehash();
tab = table;hash = key.hashCode();index = (hash & 0x7FFFFFFF) % tab.length;}
// Creates the new entry.@SuppressWarnings("unchecked")Entry<K,V> e = (Entry<K,V>) tab[index];tab[index] = new Entry<>(hash, key, value, e);count++;
}
使用synchronized关键字使线程安全
Hashtable & HashMap
版本 | 线程安全(同步) | 效率 | 允许null键null值 | |
---|---|---|---|---|
HashMap | 1.2 | 不安全 | 高 | 可以 |
Hashtable | 1.0 | 安全 | 低 | 不可以 |
Properties
-
Properties类继承自Hashtable类井且实现了Map接口,也是使用一种键值对的形式来保存数据。
-
他的使用特点和Hashtable类似。
-
Properties 还可以用于 从xxx-properties 文件中,加载数据到Properties类对象,并进行读取和修改。
-
说明:工作后 xxx.properties 文件通常作为配置文件。
public static void main(String[] args) {Properties properties = new Properties();//增properties.put("aa", 10);//删properties.remove("aa"); //改properties.put("aa", 20);//查properties.get("aa");
}
HashTable & Properties
-
HashTable
-
Properties
-
Properties 的 key 和 values 都是 String
-
常⽤⽅法
String getProperty(String key)
Object setProperty(String key, String value)
开发中如何选择集合实现类
-
先判断存储的类型(一组对象或一组键值对)
-
一组对象:Collection接口
-
允许重复:List
-
增删多:LinkedList [底层维护了一个双向链表]
-
改查多:ArrayList [底层维护 Object类型的可变数组]
-
-
不允许重复:Set
-
无序:Hashset [底层是HashMap,维护了一个哈希表 即(数组+链表+红黑树)】
-
排序:Treeset
-
插入和取出顺序一致: LinkedHashSet,维护数组+双向链表
-
-
-
一组键值对 [双列] :Map
-
键无序:HashMap [底层是:哈希表,jdk7:数组+链表,jdk8:数组+链表+红黑树]
-
键排序:TreeMap
-
键插入和取出顺序一致:LinkedHashMap
-
读取文件:Properties
-
Collections工具类
Collections工具类介绍
-
Collections 是一个操作 Set、List 和Map 等集合的工具类
-
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作
排序操作:(均为static方法)
-
reverse(List):反转 List 中元素的顺序
-
shuffle(List):对 List 集合元素进行随机排序
-
sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
-
sort(List, Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
-
swap(List, int, int):将指定 list 集合中的i 处元素和j处元素进行交换
查找、替换:
-
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素。
-
Object max(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素。
-
Object min(Collection)
-
Object min(Collection, Comparator)
-
int frequency(Collection, Object):返回指定集合中指定元素的出现次
-
void copy (List dest, List src):将src中的内容复制到dest中
-
boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换 List 对象的所有旧值
public static void main(String[] args) {ArrayList arrayList = new ArrayList();arrayList.add("tom1");arrayList.add("tom22");arrayList.add("tom333");arrayList.add("tom4444");
System.out.println(arrayList);
// 翻转顺序Collections.reverse(arrayList);System.out.println(arrayList);
// 随机打乱顺序Collections.shuffle(arrayList);System.out.println(arrayList);
// 按照自然顺序排序Collections.sort(arrayList);System.out.println(arrayList);
// 按照字符串长度大小排序Collections.sort(arrayList, new Comparator() {public int compare(Object o1, Object o2){// 可以加入校验代码return ((String)o1).length() - ((String)o2).length();}});System.out.println(arrayList);
// 交换位置Collections.swap(arrayList, 0, 1);System.out.println(arrayList);
// 自然排序最大元素System.out.println(Collections.max(arrayList));
// 长度最大的元素Object maxObject = Collections.max(arrayList, new Comparator() {public int compare(Object o1, Object o2){return ((String)o1).length() - ((String)o2).length();}});System.out.println(maxObject);
// 自然排序最小元素System.out.println(Collections.min(arrayList));
// 长度最小的元素Object minObject = Collections.max(arrayList, new Comparator() {public int compare(Object o1, Object o2){return ((String)o2).length() - ((String)o1).length();}});System.out.println(minObject);
// 出现次数System.out.println(Collections.frequency(arrayList, "tom1"));
// 将src中的内容复制到dest中ArrayList arrayList2 = new ArrayList();// 为了完成一个完整的拷贝,需要先给dest赋值,大小和list.size()一样for(int i = 0;i < arrayList.size();i ++){arrayList2.add("");}Collections.copy(arrayList2, arrayList);System.out.println(arrayList2);
// 替换Collections.replaceAll(arrayList, "tom1", "汤姆");System.out.println(arrayList);
}