Collection
- 特点: 单列集合
继承结构(JDK21)
- 有序集合
- HashSet , HashMap
- 无序集合
- 除了上面两个, 其他元素都或多或少的具有有序的特征, 统称为有序集合
- List集合中存储的元素可以重复, Set集合中存储的元素不可重复
Collection接口
-
常用方法
-
boolean add(E e); 向集合中添加元素
-
int size(); 获取集合中元素个数
-
boolean addAll(Collection c); 将参数集合中所有元素全部加入当前集合
-
boolean contains(Object o); 判断集合中是否包含对象o
-
boolean remove(Object o); 从集合中删除对象o
-
void clear(); 清空集合
-
boolean isEmpty(); 判断集合中元素个数是否为0
-
Object[] toArray(); 将集合转换成一维数组
-
@Test public void test01() { Collection c = new ArrayList(); /** *for (int i = start; i < end; i++) { * if (o.equals(es[i])) { * return i; * } *} * * contains()方法进行比较时,是调用equals方法进行比较, o代指"张三", * 因为String底层默认重写equals方法 */ String s = new String("张三"); System.out.println(c.contains(s)); //true Object o1 = new Object(); Object o2 = new Object(); c.add(o1); System.out.println(c.contains(o2)); //false 删除集合中的元素 System.out.println(c); //[100, hello, 张三, 李四, java.lang.Object@71a794e5] /** * for (; i < size; i++) * if (o.equals(es[i])) * break found; *} * remove同样是调用equals方法进行比较 */ c.remove(s); System.out.println(c); //[100, hello, 李四, java.lang.Object@71a794e5] }
-
-
Collection中的迭代方法
-
迭代器遍历 --> Iterator iterator(); 获取迭代器, 该迭代器属于最高级(集合的通用遍历方式)
-
Collection<String> strings = new ArrayList<>(); strings.add("zhangsan"); strings.add("lisi"); strings.add("wangwu"); Iterator<String> iterator = strings.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } -
迭代器原理:
-
指针先下移判断元素知否为null, 再进行获取元素
-
private class Itr implements Iterator<E> { int cursor; // 指针,默认值为0 int lastRet = -1; // (返回最后一个元素的索引;如果没有-1) int expectedModCount = modCount; //将之前记录的集合操作此数赋值给迭代器记录 public boolean hasNext() { //size:实际元素的个数,指针指向的位置与元素个数判断是否相等 /* 例如:实际长度为3,获取到索引为3的元素后指针下移(此时cursor==4), 与实际元素个数判断,不等,则终止遍历 */ return cursor != size; } public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; //指针先下移 return (E) elementData[lastRet = i]; //获取元素 }
-
-
增强for循环
-
底层还是迭代器遍历
-
@Test public void test2() { Collection<String> strings = new ArrayList<>(); strings.add("zhangsan"); strings.add("lisi"); strings.add("wangwu"); for (String string : strings) { System.out.println(string); } }
-
-
default void forEach(Consumer<? super T> action)方法遍历
-
Iterable接口中的默认方法
-
源码 ==> 底层是增强for循环
-
default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } }
-
-
代码示例
-
@Test public void test3() { Collection<String> strings = new ArrayList<>(); strings.add("zhangsan"); strings.add("lisi"); strings.add("wangwu"); strings.forEach(new Consumer<String>() { //Consumer: 函数式接口 @Override public void accept(String s) { System.out.println(s); } }); strings.forEach(s->System.out.println(s)); }
-
-
-
-
集合迭代过程中的并发修改异常
-
fail-fast 快速失败机制:
/** * 对集合进行增删改查时,会使用modCound进行记录, 每一次执行添加,删除,修改都会使modCount+1 * 当new出迭代器时,会先将modConunt赋值给expectedModCount: expectedModCount=modCount * * 若在迭代的过程中,使用集合删除元素,则会执行fastRemove方法,进行删除元素,同时modCount++ * * 执行完删除操作后,会将expectedModCount值与modCount进行比较,若不相等,抛出并发修改异常 * * 当使用迭代器进行删除时,也是调用fastRemove方法,只是调用完后会将modCount值加重新赋值给expectedModCount * * 这种检查机制就是fail-fast快速失败机制,避免并发修改问题 */ -
@Test public void test03() { Collection<String> c = new ArrayList<>(); c.add("zhangsan"); c.add("lisi"); c.add("wangwu"); c.add("zhaoliu"); Iterator<String> iterator = c.iterator(); while (iterator.hasNext()) { String name = iterator.next(); if (name.equals("lisi")) { //Collection接口自带的remove方法进行删除 c.remove(name); iterator.remove(); //迭代器删除时,不报错 } } }
-
List接口
-
List集合存储元素特点:
- 元素可重复
- 有索引: 可以通过索引处理元素
-
常用方法(只适合List家族使用的方法,这些方法都和下标有关系)
-
void add(int index, E element); 在指定索引处插入元素
-
E set(int index, E element); 修改索引处的元素
-
E get(int index); 根据索引获取元素
-
E remove(int index); 删除索引处的元素
-
int indexOf(Object o); 获取对象o在当前集合中第一次出现时的索引。
-
int lastIndexOf(Object o); 获取对象o在当前集合中最后一次出现时的索引。
-
List subList(int fromIndex, int toIndex); 截取子List集合生成一个新集合(对原集合无影响)
-
static List of(E... elements); 静态方法, 返回包含任意数量元素的不可修改列表 (获取的集合是只读的, 不可修改的)
-
-
List接口特有迭代方法
-
ListIterator迭代器遍历 --> ListIterator listIterator(): 获取List集合特有的迭代器(该迭代器功能更加强大,但只适合于List集合使用)
-
boolean hasNext(); 判断光标当前指向的位置是否存在元素。
-
E next(); 将当前光标指向的元素返回,然后将光标向下移动一位。
-
boolean hasPrevious(); 判断当前光标指向位置的上一个位置是否存在元素。
-
E previous(); 获取上一个元素(将光标向上移动一位,然后将光标指向的元素返回)
==注:== 使用倒序遍历时 , 需要先进行正序遍历 , 使指针指向末尾
-
int nextIndex(); 获取光标指向的那个位置的下标
-
int previousIndex(); 获取光标指向的那个位置的上一个位置的下标
==注:==
-
/** * ListIterator: 包含add(), set(), remove()方法, 也就是为了避免并发修改异常, * 一般对集合进行数据修改等操作都是使用迭代器进行处理 * <p> * 使用迭代器中的set() remove()方法时, * 不是根据索引处理,而是处理next()方法取出的元素;所以使用set() remove()时,需要先执行next()方法; */ -
void add(E e); 添加元素(将元素添加到光标指向的位置,然后光标向下移动一位。)
-
void set(E e); 修改的是上一次next()方法返回的那个数据 (修改的是集合中的) . set()方法调用的前提是:先调用了next()方法。不然会报错。
-
void remove(); 删除上一次next()方法返回的那个数据 (删除的是集合中的) . remove()方法调用的前提是:先调用next()方法。不然会报错。
-
代码示例
-
@Test public void test2() { List<String > list = new ArrayList<>(); list.add("hello"); list.add("world"); list.add("java"); list.add("java"); ListIterator<String> listIterator = list.listIterator(); while (listIterator.hasNext()) { System.out.println(listIterator.nextIndex()); //获取指针索引 System.out.println(listIterator.next()); } }
-
List接口使用 get() 方法遍历
-
@Test public void test1() { List<String > list = new ArrayList<>(); list.add("hello"); list.add("world"); list.add("java"); list.add("java"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } }
-
-
-
==List接口特有排序方法, Java8新增== --> void sort( Comparator<? super E> comparator) : Comparator--> 比较器
-
/** * 数组排序对象时实现了Comparable接口,重写了compareTo()方法 --> 返回int类型判断 * * List排序 * default void sort(Comparator<? super E> c) * <p> * Comparator 比较器 * Comparable接口只有一个抽象方法,是一个函数式接口 */ -
代码示例:
-
@Test public void test03() { List<Student> list = new ArrayList<>(); list.add(new Student(18, "张三")); list.add(new Student(19, "李四")); list.add(new Student(20, "王五")); /** * 按照年龄从大到小排序 * * 之前想要对对象进行排序需要让对象实现Comparable接口,重写compareTo()方法 * * 现在对对象排序只需要传入一个Comparator接口即可 */ list.sort(new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return o2.getAge() - o1.getAge(); } }); ListIterator<Student> listIterator = list.listIterator(); while (listIterator.hasNext()) { System.out.println(listIterator.next()); } }
-
ArrayList实现类测试
-
特点
- ArrayList集合底层采用了数组这种数据结构。
- 优点
- 查询效率高: 由于数组中存储的使想同数据结构且连续的线性结构, 所以可以根据索引与偏移量直接计算出需要查询的地址 , 时间复杂度使O(1)
- 缺点
- 在数组中间增删效率低, 由于需要保持内存的连续性, 若对数组中间进行增删操作, 则会涉及数组整体移位(对末尾增删不受影响) , 时间复杂度O(n)
-
构造方法分析
-
初始化容量: 0 , 即只是创建ArrayList,不添加元素时 , 集合容量为0 . (1.8版本前创建ArrayList就直接给10个容量)
-
/** * public ArrayList() { * // Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; * this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; *} */ ArrayList<String> list = new ArrayList<>();
-
-
-
添加方法分析
-
执行添加方法时 , 先判断集合长度是否与size(实际包含元素的个数)是否相等, 若相等, 则执行扩容操作
-
private void add(E e, Object[] elementData, int s) { //s就是size if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; }
-
-
==扩容原理解析== (每次扩容都是原容量的二分之一, 即每次扩容长度为原数组长度的1.5倍)
-
private Object[] grow(int minCapacity) { //minCapacity(最小容量) = size +1 ==> 避免索引越界 int oldCapacity = elementData.length; if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, //值为1 oldCapacity >> 1 //当前数组长度的二分之一 return elementData = Arrays.copyOf(elementData, newCapacity); } else {//只执行一次,就是第一个添加方法执行时执行int DEFAULT_CAPACITY = 10;即立即给10个容量 return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } } public static int newLength(int oldLength, int minGrowth, int prefGrowth) { //minGrowth = 1, prefGrowth = 当前数组长度的二分之一. 即扩容长度为原数组长度的1.5倍 int prefLength = oldLength + Math.max(minGrowth, prefGrowth); if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) { return prefLength; } else { // put code cold in a separate method return hugeLength(oldLength, minGrowth); } }
-
-
-
修改方法分析
- 没啥说的, 就获取索引赋值而已
-
插入方法分析
-
与删除类似,先获取对应索引,再获取后续元素,再覆盖,最后将元素赋值
-
public void add(int index, E element) { rangeCheckForAdd(index); modCount++; final int s; Object[] elementData; if ((s = size) == (elementData = this.elementData).length) elementData = grow(); System.arraycopy(elementData, index, elementData, index + 1, s - index); elementData[index] = element; size = s + 1; }
-
-
删除方法分析
-
/** * 删除集合元素(list特有的删除方法) * System.arraycopy(es, i + 1, es, i, newSize - i); *获取需要删除的索引,将该索引后续所有的元素全部获取 * 将被获取到数组从原数组索引位置开始拷贝覆盖 * 最后将最后的元素从新赋值为null */ list.remove(0); /** * Collection的删除方法 * 流程: * 先遍历数组,获取到对应索引,再调用 fastRemove(es, i)与上面删除方法类似 */ list.remove("bbb");
-
Vector实现类测试
-
特点
- Vector底层也是数组,和ArrayList相同
- Vector几乎所有的方法都是线程同步的(被synchronized修饰:线程排队执行,不能并发),因此Vector是线程安全的,但由于效率较低,很少使用。因为控制线程安全有新方式。
-
构造方法分析
-
Vector初始化就给10个容量: public Vector() { this(10); }
-
自动扩容(与ArrayList类似) : 每次扩容都是原容量的大小, 即原容量的2倍
-
private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, //oldCapacity原容量 capacityIncrement > 0 ? capacityIncrement : oldCapacity); return elementData = Arrays.copyOf(elementData, newCapacity); }
-
-
LinkedList实现类测试
-
特点
-
底层是双链表
-
代码实现双链表结构:
-
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; } }
-
-
链表优点:
- 因为链表节点在空间存储上,内存地址不是连续的。因此删除某个节点时不需要涉及到元素位移的问题。因此随机增删元素效率较高。时间复杂度O(1)
-
链表缺点:
- 链表中元素在查找时,只能从某个节点开始顺序查找,因为链表节点的内存地址在空间上不是连续的。链表查找元素效率较低,时间复杂度O(n)
-
-
添加方法分析
-
LinkedList的add方法,默认添加到链表的最后
-
void linkLast(E e) { final Node<E> l = last; //将末尾结点使用临时变量存储 /** * 创建新节点: * l: 上一个结点的地址 * e: 元素 * null: 下一个结点的地址, 由于当前结点为末尾节点,所以Node.next值为null */ final Node<E> newNode = new Node<>(l, e, null); last = newNode; //将该结点赋值给last, 重新刷新末尾结点地址 if (l == null) first = newNode; else l.next = newNode; //将前节点的Node.next值存储为当前结点地址 size++; //长度加一 modCount++; }
-
-
-
修改方法分析
-
==插入方法分析==
-
public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); //若索引值不是末尾则执行该方法 } Node<E> node(int index) { //先获取对应索引的节点node(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: 对应索引的节点 final Node<E> pred = succ.prev; //获取当前节点的前一个节点地址 final Node<E> newNode = new Node<>(pred, e, succ); //创建一个节点,同时存储前一个节点,下一个节点 succ.prev = newNode; //新节点的地址赋值给当前节点的前一个节点地址 if (pred == null) first = newNode; else pred.next = newNode; //同上 size++; modCount++; }
-
-
删除方法分析
Queue接口
-
特点
- 单向队列, 一般使用双端队列, 也就是其子类接口Deque
-
Queue接口基于Collection扩展的方法:
-
boolean offer(E e); 入队。
-
E poll(); 出队,如果队列为空,返回null。
-
E remove(); 出队,如果队列为空,抛异常。
-
E peek(); 查看队头元素,如果为空则返回null。
-
E element(); 查看对头元素,如果为空则抛异常。
-
Deque子接口
-
Deque接口基于Queen接口扩展的方法:
-
以下2个方法可模拟队列:
- boolean offerLast(E e); 从队尾入队
- E pollFirst(); 从队头出队
-
以下4个方法可模拟双端队列:
-
boolean offerLast(E e); 从队尾入队
-
E pollFirst(); 从队头出队
-
boolean offerFirst(E e); 从队头入队
-
E pollLast(); 从队尾出队
-
-
ArrayDeque实现类测试
-
特点
-
实现了Deque接口 , 所以是可以实现双端队列结构
-
底层是环形数组 , 同时也实现了栈结构
-
Set接口
-
Set集合存储元素特点:
-
==无索引 :== 可以通过索引处理元素, HashSet底层为数组加链表, 理论上有索引, 但无法通过索引操作元素, 所以被认为无索引
-
元素不可重复 : 相对而言, 可以通过比较器定义是否需要存储重复元素
-
//根据年龄做主要排序(降序) int ageResult = o.age - this.age; //根据年龄相同的情况下,根据姓名做次排序(升序) int nameResult = ageResult == 0 ? this.name.compareTo(o.name) : ageResult; //同姓名,同年龄需要保留 return nameResult == 0 ? 1 : nameResult; //如果相同,则强制性返回为1,就可以骗过比较器不重复
-
-
-
Set接口无自身特有方法 , 所有方法皆继承于 Collection接口
HashSet实现类测试
-
HashSet集合底层是new了一个HashMap。往HashSet集合中存储元素实际上是将元素存储到HashMap集合的key部分。
-
//静态常量,内存中只有一个 private static final Object PRESENT = new Object(); public boolean add(E e) { //使用PRESENT将value填充 return map.put(e, PRESENT)==null; } -
HashMap集合的key是无序不可重复的,因此HashSet集合就是无序不可重复的。
-
HashMap集合底层是哈希表/散列表数据结构,因此HashSet底层也是哈希表/散列表。
-
根据源码可以看到向Set集合中add时,底层会向Map中put。value只是一个固定不变的常量,只是起到一个占位符的作用。主要是key。
-
@Test public void test01(){ /** * HashSet构造器底层是调用的HashMap * public HashSet() { * map = new HashMap<>(); *} */ HashSet<String> hashSet = new HashSet<>(); /** * HashSet的add()方法也是调用的put方法,将数据存入HashMap的key中 * public boolean add(E e) { * return map.put(e, PRESENT)==null; * } */ hashSet.add("1"); }
LinkedHashSet实现类测试
-
底层调用的LinkedHashMap
-
同上, 底层调用的HashMap的put()方法
-
@Test public void test02(){ /** * * HashSet(int initialCapacity, float loadFactor, boolean dummy) { * map = new LinkedHashMap<>(initialCapacity, loadFactor); *} */ LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>(); //同上,底层调用的HashMap的put()方法 linkedHashSet.add("001"); linkedHashSet.add("002"); System.out.println(linkedHashSet); //[001, 002]:存储顺序与取出顺序一致 }
SortedSet接口
TreeSet实现类测试
-
TreeSet集合底层是new了一个TreeMap。往TreeSet集合中存储元素实际上是将元素存储到TreeMap集合的key部分。
-
TreeMap集合的key是不可重复但可排序的,因此TreeSet集合就是不可重复但可排序的。
-
TreeMap集合底层是红黑树,因此TreeSet底层也是红黑树。它们的排序通过java.lang.Comparable和java.util.Comparator均可实现。
Map
- 特点:
- Map集合以key和value的键值对形式存储。key和value存储的都是引用
- Map集合中key起主导作用。value是附属在key上的
- Map集合的key都是不可重复的。key重复的话,value会覆盖
继承结构(JDK21)
Map接口
-
常用方法
-
V put(K key, V value) 添加键值对
-
void putAll(Map<? extends K,? extends V> m) 添加Map集合
-
boolean containsKey(Object key) 是否包含某个key
-
boolean containsValue(Object value) 是否包含某个value
-
V remove(Object key) 通过key删除key-value
-
void clear() 清空Map
-
int size() 键值对个数
-
boolean isEmpty() 判断是否为空Map
-
V get(Object key) 通过key获取value
-
Set keySet() 获取所有的key
-
Collection values() 获取所有的value
-
Set<Map.Entry<K,V>> entrySet() 将Map集合转为Set集合(迭代方法)
-
//Map的迭代方式 Set<Map.Entry<Integer, String>> entries = map.entrySet(); for (Map.Entry<Integer, String> entry : entries) { System.out.println(entry); } -
default void forEach(BiConsumer<? super K, ? super V> action) Map默认方法, 通用遍历集合方式
-
//使用默认方法forEach()遍历Map集合 map.forEach(new BiConsumer<Integer, String>() { @Override public void accept(Integer key, String value) { System.out.println(key+"="+value); } });
-
HashMap实现类测试
-
集合特性
- key和value的键值对形式存储
- 无序, key不可重复, key具有唯一性, key起主导作用。value是附属在key上的
- 由于是散列表结构, 增删与查询效率都比较快
- 缺点: 扩容成本大
-
内存结构
-
数组 + 单向链表 + 红黑树
-
transient Node<K,V>[] table; /*HashMap结构: 底层是Node[]数组,Node就是结点*/ static class Node<K,V> implements Map.Entry<K,V> { final int hash; //hash值 final K key; V value; Node<K,V> next; //下一个结点的地址 }
-
-
存储原理-->put() 方法 : 里面包含初始化数组长度, 是否需要树化操作, 是否需要扩容操作
- 存入数据时, 先根据存入对象的key计算出hash值 , 然后使用这个hash值与数组长度取余计算. 计算出需要插入数组中的索引位置. 然后判断该位置是否存在是元素, 若没有元素, 则直接插入. 若有元素, 则开始遍历该索引位置的所有结点 , 将存入数据hash值依此与这些结点中hash值比对, 若hash值相同, 则覆盖value , 返回oldValue. 若未找到相同hash值的结点对象. 则执行尾插, 返回value. 插入完之后, 元素个数size++ , 然后判断是否需要扩容.
- 源码解析
-
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K, V>[] tempTable; //临时teble 变量 Node<K, V> IndexNode; //tempTable[index] 计算出的索引位置结点 int tableLength; //数组长度 int index; //通过hash值算出的索引 //第一次调用put()方法时执行 if ((tempTable = table) == null || (tableLength = tempTable.length) == 0) { tableLength = (tempTable = resize()).length; //将初始化后的数组长度赋值给tableLength } //判断tempTable[index]是否为null,即数组中第一个结点是否时null if ((IndexNode = tempTable[index = (tableLength - 1) & hash]) == null) { tempTable[index] = new Node(hash, key, value, null); //为null 直接插入 } else { //执行到这里,说明索引位置有结点存在,即遍历链表,使用equals()方法判断key是否相同 Node<K, V> e; //结点临时变量,用于获取当前链表的最后结点对象或相同key的Node结点对象 K k; //该索引位置链表中第一个结点的key对象 //与对应索引位置的第一个结点的key对象是否相同. 这样判断可以减少遍历 if (IndexNode.hash == hash&&((k=IndexNode.key) == key||(key!= null && key.equals(k)))){ e = IndexNode; }else if (IndexNode instanceof TreeNode) { //判断是否是红黑树结构 e = ((TreeNode<K, V>) IndexNode).putTreeVal(this, tempTable, hash, key, value); //执行到这里说明传入的结点对象不是第一个,必须开始遍历链表 } else { for (int binCount = 0; ; ++binCount) { //遍历链表中的所有结点 e = IndexNode.next; if (e == null) { //遍历到末尾,e==null IndexNode.next = new Node(hash, key, value, null); //开始尾插 if (binCount >= 8 - 1) { //同时判断链表长度,是否需要>=8 treeifyBin(tempTable, hash); } break; } //判断结点对象中的key是否与传入的key相等,若相等,则停止遍历,由于e = IndexNode.next; 停止遍历时,也就拿到了相同key结点的对象 if(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){ break; } IndexNode = e; } } //执行到这里,说明有相同key的结点,则覆盖value值并返回oldValue值即可. e就是对应结点对象 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) { e.value = value; } return oldValue; } } ++modCount; //threshold = 当前数组长度与负载因子DEFAULT_LOAD_FACTOR计算出的数组扩容阈值 if (++size > threshold) { resize(); } return null; }
-
手写put()方法 :
-
/** * 添加元素 * * @param key * @param value * @return oldValue */ public V put(K key, V value) { /* 【第一步】:处理key为null的情况 如果添加键值对的key就是null,则将该键值对存储到table数组索引为0的位置。 */ if (key == null) { return putForNullKey(value); } //key不为0 //判断是否是首位 int hash = key.hashCode(); int index = Math.abs(hash % table.length); //hash值可能为复数 Node<K, V> node = table[index]; Node<K, V> newNode = new Node<>(hash, key, value, null); if (null == node) { table[index] = newNode; size++; return value; } //不是首位,判断hash值是否重复 Node<K, V> lastNode = null; while (null != node) { if (hash == node.hash) { V oldValue = node.value; node.value = value; return oldValue; } lastNode = node; node = node.next; } //无重复值,执行尾插 lastNode.next = newNode; return value; } private V putForNullKey(V value) { //如果索引0没有元素,直接添加 Node<K, V> node = table[0]; Node<K, V> newNode = new Node<K, V>(0, null, value, null); if (node == null) { table[0] = newNode; size++; return value; } //执行到这里,说明table[0]有结点 /** *遍历table[0]中所有的结点 * while (node != null){ //node代指table[0]的第一个结点 * node = node.next; //将当前结点的下一个结点的内存地址赋值. 当遍历到最后一个结点时,node.next == null *} */ Node<K, V> lastNode = null; //获取到最后的结点 while (node != null) { if (node.key == null) { V oldValue = node.value; node.value = value; return oldValue; } lastNode = node; node = node.next; } //执行到这里,则说明链表中没有key==null的结点,将新结点进行尾插 lastNode.next = newNode; size++; return value; } -
手写get()方法 :
-
/** * 通过key获取value */ public V get(K key) { //先判断key是否为null if (null == key) { Node<K, V> node = table[0]; if (null == node) { return null; } //不是null,遍历链表 while (node != null) { if (node.key == null) { return node.value; } node = node.next; } } //key不是null int hash = key.hashCode(); int index = Math.abs(hash % table.length); Node<K, V> node = table[index]; //健壮性判断,防止乱传key if (null == node) { return null; } while (node != null) { if (node.key.equals(key)) { return node.value; } node = node.next; } return null; }
-
HashMap的遍历原理
-
集合的遍历顺序:
- 从0索引开始遍历,取出0索引中所有的结点,再往下遍历,直到遍历完所有的结点
-
手写toString()方法:
-
/** * 重写toString,直接输出map集合 */ @Override public String toString() { StringBuilder stu = new StringBuilder(); for (int i = 0; i < table.length; i++) { Node<K, V> node = table[i]; while (null != node) { stu.append("{"); stu.append(node); stu.append("}"); node = node.next; } } return stu.toString(); }
-
-
HashMap在Java8后的改进
-
初始化时机:
- Java8之前,构造方法执行时初始化table数组。
- Java8之后,第一次调用put方法时初始化table数组。
-
插入法:
- Java8之前,头插法
- Java8之后,尾插法
-
数据结构:
-
Java8之前:数组 + 单向链表
-
Java8之后:数组 + 单向链表 或 + 红黑树。
-
树化条件
-
if (binCount >= TREEIFY_THRESHOLD - 1){ //TREEIFY_THRESHOLD == 8 treeifyBin(tab, hash); } // -1 for 1st final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; } } -
当任意链表长度大于等于8时, 数组长度小于64
- 执行扩容操作, 打乱全部结点重新插入
-
当任意链表长度大于等于8时, 且数组长度大于等于64
- 将链表转化为红黑树
-
-
当删除红黑树上的结点时,结点数量 <= 6 时。红黑树转换为单向链表。
-
static final int TREEIFY_THRESHOLD = 8; //红黑树化阈值 static final int UNTREEIFY_THRESHOLD = 6; //取消红黑树阈值 static final int MIN_TREEIFY_CAPACITY = 64; //进行树化的最小表容量
-
-
-
HashMap的容量问题
-
HashMap集合初始化容量16(第一次调用put方法时初始化)
-
HashMap集合的容量永远都是2的次幂(包括自动扩容),假如给定初始化容量为27,它底层也会变成32的容量。
-
//Returns a power of two size for the given target capacity. (返回给定目标容量的2次幂大小。) static final int tableSizeFor(int cap) { int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }- 作用 :
-
加快哈希计算
- hash & (length-1) 的结果和 hash % length的结果相同
- 注意:只有是2的次幂时,以上等式才会成立。因为了使用 & 运算符
-
减少哈希冲突
- 底层运算是:hash & length - 1
- 如果length是偶数:length-1后一定是奇数,奇数二进制位最后一位一定是1,1和其他二进制位进行与运算,结果可能是1,也可能是0,这样可以减少哈希冲突,让散列分布更加均匀。
- 如果length是奇数:length-1后一定是偶数,偶数二进制位最后一位一定是0,0和任何数进行与运算,结果一定是0,这样就会导致发生大量的哈希冲突,白白浪费了一半的空间。
-
- 作用 :
-
HashMap初始化容量设置值
-
自动扩容原理(判断的是元素个数)
-
//在put()方法底部判断是否需要扩容 ++modCount; if (++size > threshold) //threshold : 数组长度与负载因子算出来的值 resize();-
当执行put()操作的时候,如果HashMap中存储==元素的个数==超过“数组长度* loadFactor”的结果(loadFactor指的是负载因子,loadFactor的默认值一般为0.75),那么就需要执行数组扩容操作.(loadFactor可以自定义)
-
public HashMap(int initialCapacity, float loadFactor) { this.loadFactor = loadFactor; //负载因子 this.threshold = tableSizeFor(initialCapacity); }
-
-
HashMap扩容就是数组长度变化. 而每一个结点的存入的索引位置是根据hash值与数组长度取余计算的, 若长度变化, 那将会使HashMap中所有的结点根据新长度重新计算索引, 重新插入. 这是非常消耗性能的, 所以创建集合的时候尽量给出一个比较合理的长度值. 如此假如需要存储1000个元素, 若给1024的长度实际可以存储的容量 ==> 1024 * 0.75 = 768.
-
-
-
==多学一招:==
- 当对象根据内容计算出hash值后, 存入HashMap中后. 此时再修改对象属性值, 不会重新计算hash值重新插入. 而是使用之前的hash值存入的位置.
LinkedHashMap
-
特点:
-
LinkedHashMap集合和HashMap集合的用法完全相同
-
不过LinkedHashMap可以==保证插入顺序与取出顺序一致==
-
底层采用了双向链表来记录顺序
-
-
LinkedHashMap集合底层采用的数据结构是:哈希表 + 双向链表
-
static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; //使用Entry对象记录插入顺序 Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } -
构造方法, put()等方法底层也都是调用HashMap的
SortedMap接口
TreeMap实现类测试
-
元素在集合中是有序排列的。(key为主导, 所以可排序是key可以排序)
-
底层为红黑树结构(自平衡二叉树)
- 相较与平衡二叉树, 增删效率更高一点. 因为平衡二叉树为了保证每个节点的子节点高度差不超过1, 则每一次增删操作都会使结构进行左右旋进行调整. 而红黑树结构的条件没有这么苛刻, 对元素修改后整体结构不一定会旋转修改.
-
红黑树的特点:
- 节点由红色和黑色组成
- 根节点必须使黑色
- 没有子节点,则指向Nil节点. Nil节点必须使黑色
- 任意节点的到Nil节点的相对路径中的黑色节点数量相同
- 两个相邻的节点不能同为红色.
-
这些约束强化了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。这样就让树大致上是平衡的。
-
==注意:==
- 当同时存在自然排序与比较器排序的排序规则时,会优先执行比较器排序规则
-
TreeMap 排序 (自然排序)
-
类实现Comparable接口(自然排序),重写comparTo()方法,在comparTo()方法中定义排序规则
-
/* 根据方法的返回值, 来组织排序规则 - 负数 : 左边走 - 正数 : 右边走 - 0 : 不存 */ @Override public int compareTo(Student o) { //根据年龄做主要排序(降序) int ageResult = o.age - this.age; //根据年龄相同的情况下,根据姓名做次排序(升序) int nameResult = ageResult == 0 ? this.name.compareTo(o.name) : ageResult; //同姓名,同年龄需要保留 return nameResult == 0 ? 1 : nameResult; }
-
-
TreeMap 排序 (比较器排序)
-
使用Compartor比较器排序: 在创建TreeSet集合的构造器中传入Comparator比较器对象
-
TreeMap <String> strings = new TreeMap <>(new Comparator<String>() { @Override public int compare(String o1, String o2) { //按照长度排序 return o1.length() - o2.length() == 0 ? 1 : o1.length() - o2.length(); } });
-
Hashtable实现类
- Hashtable和HashMap一样,底层也是哈希表。
- Hashtable是线程安全的,方法上都有synchronized关键字。使用较少,因为保证线程安全有其他方式。
- Hashtable的初始化容量:11。默认加载因子:0.75
- Hashtable的扩容策略:2倍。
Properties实现类测试
- 特点:
- Properties被称为属性类。通常和xxx.properties属性文件一起使用。
- Properties的父类是Hashtable。因此Properties也是线程安全的。
- Properties不支持泛型,key和value只能是String类型。
- Properties相关方法:
-
Object setProperty(String key, String value); 和put方法一样。
-
String getProperty(String key); 通过key获取value
-
Set propertyNames(); 获取所有的key
-
Collections
-
Collections是 Java 中的一个工具类,位于java.util包中。它提供了一系列对集合(如List、Set、Map等)进行操作的静态方法,这些方法可以用于排序、搜索、修改等多种操作,极大地丰富了集合的功能。 -
常用方法
-
public static boolean addAll(Collection<? super T> c, T... elements) 给集合对象批量添加元素
-
public static void shuffle(List<?> list) 打乱List集合元素的顺序
-
public static int binarySearch (List list, T key) 以二分查找法查找元素
-
public static void max/min(Collection coll) 根据默认的自然排序获取最大/小值
-
public static void swap(List<?> list, int i, int j) 交换集合中指定位置的元素
-
泛型
定义
-
在类上定义泛型
-
/** * 泛型测试(在类上定义泛型) * * <T> T(Type)只是一个代称, 可以写任意字符. 当MyClass<String> myClass = new MyClass<>("liny")执行后 * 后续会就获取到<String>类型,所以后续代码都改为String类型了 */ public class MyClass<T> { private T name; }
-
-
在接口上定义泛型
-
/** * 在接口中定义泛型(与类一样) */ public interface MyInterface<T> { void show(T product); }
-
-
在静态方法上定义泛型
-
/** * 在静态方法上定义泛型 */ public class MyStatic { public static <T> void show(T product){ System.out.println("展示: "+product); } }
-
使用
- 无限定通配符 : <?>, 此处 ? 可以是任意引用数据类型
- 上限通配符 : <? extends Comparable>, 此处 ? 只能是Comparable 或 Comparable 的子类
- 下限通配符 : <? super Comparable>, 此处 ? 只能是Comparable 或 Comparable 的父类