Java # Java容器

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 中的容量使⽤完之后,则需要对容量进⾏扩容:

  1. ArrayList 容量使⽤完后,会“⾃动”创建容量更⼤的数组,并将原数组中所有元素拷⻉过去,这会导致效率降低 。

  2. 优化:可以使⽤构造⽅法 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 构造⽅法:

  1. ArrayList():创建⼀个初始化容量为 10 的空列表

  2. ArrayList(int initialCapacity):创建⼀个指定初始化容量为 initialCapacity 的空列表

  3. ArrayList(Collection c):创建⼀个包含指定集合中所有元素的列表

ArrayList 优缺点:

优点:

  1. 向 ArrayList 末尾添加元素(add() ⽅法)时,效率较⾼

  2. 查询效率⾼

缺点:

  1. 扩容会造成效率较低(可以通过指定初始化容量,在⼀定程度上对其进⾏改善)

  2. 另外数组⽆法存储⼤数据量(因为很难找到⼀块很⼤的连续的内存空间)

  3. 向 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 ⽅法

  1. ListIterator 是 Iterator 的⼀个⼦接⼝

3、Iterator 中 remove ⽅法

  1. 调⽤ next 之后,remove ⽅法删除的是迭代器左侧的元素(类似键盘的 backspace)

  2. 调⽤ previous 之后,remove 删除的是迭代器右侧的元素

4、ListIterator 中 add ⽅法

  1. 调⽤ next 之后,在迭代器左侧添加⼀个元素 2. 调⽤ previous 之后,add 是在迭代器右侧添加元素
底层结构增删效率改查效率
ArrayList可变数组较低、数组扩容较高
LinkedList双向链表较高、通过链表追加较低

ArrayList 和 LinkedList 有什么区别:

  1. 内部实现不同。ArrayList 是使用数组的实现,通过索引来访问元素,支持快速随机访问。LinkedList 内部使用双向链表来实现,每个元素都包含对前一个元素和后一个元素的引用,适合插入和删除操作。

  2. 数据访问的时间复杂度不同。ArrayList 由于使用数组的实现,它的时间复杂度是O(1),即常量复杂度。LinkedList 需要从头部或者尾部开始遍历链表,知道找到目标元素的位置,它的时间复杂度是O(n)。

  3. 空间占用不同。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

无序、唯一、相同的覆盖

  1. HashSet实现了Set接口

  2. HashSet实际上是HashMap

public HashSet(){map = new HashMap<>();
}
  1. 可以存放null值,但只能有一个null

  2. HashSet不保证元素是有序的,取决于hash后,再确定索引的结果

  3. 不能有重复元素/对象

HashSet底层机制

HashSet底层是HashMap,HashMap底层是数组+链表+红黑树

  1. HashSet底层是HashMap。

  2. 添加一个元素时,先得到一个hash值 - 会转成-> 索引值。

  3. 找到存储数据表table,看这个索引位置是否已经存放的有元素。

  4. 如果没有,直接加入。

  5. 如果有,调用equals比较,如果相同,则放弃添加,如果不相同,则添加到最后。

  6. 在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;}

扩容机制&转成红黑树机制

  1. HashSet底层是HashMap,第一次添加时,table 数组扩容到 16,临界值(threshold)是 16*加载因子(loadFactor)是0.75= 12。

  2. 如果table 数组使用到了临界值 12,就会扩容到 16 * 2=32,新的临界值就是32 * 0.75= 24,依次类推。

  3. 在Java8中,如果一条链表的元素个数到达 TREEIFY THRESHOLD(默认是8)。并且table的大小 >= MIN_ TREEIFY_ CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制。

LinkedHashSet

  • LinkedHashSet是HashSet的子类

  • LinkedHashSet底层是一个LinkedHashMap,底层维护了一个 数组+双向链表

  • LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,使元素看起来是以插入的顺序保存的

  • LinkedHashSet不允许添加重复元素

  1. LinkedHashset 加入顺序和取出元素/ 数据的顺序一致

  2. LinkedHashset 底层维护的是一个LinkedHashMap(是HashMap的子类)

  3. LinkedHashset 底层结构(数组table+双向链表)

  4. 添加第一次时,直接将 数组table扩容到 16,存放的结点类型是 LinkedHashMap$Entry

  5. 数组是 HashMap$Node[]

  6. 存放的元素/数据是 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 部分。

  1. 当使用无参构造器,创建TreeSet时,仍然是无序的

TreeSet treeSet = new TreeSet();
  1. 使用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接口和常见方法

  1. Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value(双列元素)。

public static void main(String[] args) {Map map = new HashMap();map.put("aa", 1);
}
  1. Map 中的key 和value 可以是任何引用类型的数据,会封装到HashMap$Node对象中。

  2. Map 中的key 不允许重复,原因和Hashset 一样。

  3. Map 中的value 可以重复。

  4. Map 的key 可以为mull, value 也可以为null,注意 key 为null, 只能有一个,value 为null,可以多个。

  5. 常用String类作为Map的 key。

  6. 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

  1. HashMap 底层是⼀个数组

  2. 数组中每个元素是⼀个单向链表(即,采⽤拉链法解决哈希冲突) 单链表的节点每个节点是 Node 类型

  3. 同⼀个单链表中所有 Node 的 hash值不⼀定⼀样,但是他们对应的数组下标⼀定⼀样 数组下标利⽤哈希函数/哈希算法根据 hash值计算得到的

  4. HashMap 是数组和单链表的结合体

    1. 数组查询效率⾼,但是增删元素效率较低

    2. 单链表在随机增删元素⽅⾯效率较⾼,但是查询效率较低

    3. HashMap 将⼆者结合起来,充分它们ݱ自的优点

  5. HashMap 特点

    1. ⽆序、不可重复

    2. ⽆序:因为不⼀定挂在那个单链表上了

  6. 为什么不可重复?

    通过重写 equals ⽅法保证的

  7. k-v 最后是 HashMap$Node node = newNode (hash, key, valve, null)

  8. k-v 为了方便程序员的遍历,还会 创建 EntrySet 集合,该集合存放的元素的类型 Entry,而一个Entry对象就有K,V Entryset<Entry<k,V>>即:transient Set<Map.Entry<K, V>> entrySet;

  9. entrySet中,定义的类型 Map.entry,但是实际上存放的还是HashMap$Node 这是因为 static class Node<K, V> implements Map.Entry<K, V>

  10. 当把 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没有实现同步,因此是线程不安全的。

扩容机制:

  1. HashMap底层维护了Node类型的数组table,默认为null。

  2. 当创建对象时,将加载因子(loadfactor)初始化为0.75。

  3. 当添加key-val时,通过key的哈希值得到在table的索引。然后判断该索引处是否有元素。如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key相是否等,如果相等,则直接替换val:如果不相等需要判断是树结构还是键表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。

  4. 第1次添加,则需要扩容table容量为16,临界值(threshold)为12。

  5. 以后再扩容,则需要扩容table容量为原来的2倍,临界值为原來的2倍,即24,依次类推。

  6. 在Java8中,如果一条链表的元素个数超过 TREEIFY THRESHOLD(默认是8),并且 table的大小>=MIN_TREEIFY_ CAPACITY((默认64),就会进行树化(红黑树)。

  7. HashMap 默认初始化容量: 16

    1. 必须是 2 的次幂,这也是 jdk 官⽅推荐的

    2. 这是因为达到散列均匀,为了提⾼ HashMap 集合的存取效率,所必须的

  8. HashMap 默认加载因⼦:0.75

    数组容量达到 3/4 时,开始扩容

  9. JDK 8 之后,对 HashMap 底层数据结构(单链表)进⾏了改进

    1. 如果单链表元素超过8个,则将单链表转变为红黑树;

    2. 如果红黑树节点数量⼩于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() ⽅法原理
  1. 先将 key, value 封装到 Node 对象中

  2. 底层会调⽤ key 的 hashCode() ⽅法得出 hash 值

  3. 通过哈希函数/哈希算法,将 hash 值转换为数组的下标

    • 如果下标位置上没有任何元素,就把 Node 添加到这个位置上;

    • 如果下标位置上有但链表,此时会将当前 Node 中的 key 与链表上每⼀个节点中的 key 进⾏ equals ⽐较

      • 如果所有的 equals ⽅法返回都是 false,那么这个新节点 Node 将被添加到链表 的末尾;

      • 如果其中有⼀个 equals 返回了 true,那么链表中对应的这个节点的 value 将会被 新节点 Node 的 value 覆盖。(保证了不可重复)

注:

  1. HashMap 中允许 key 和 value 为 null,但是只能有⼀个(不可重复)!

  2. HashTable 中 key 和 value 都不允许为 null。

get() ⽅法原理

  1. 先调⽤ key 的 hashCode() ⽅法得出 hash 值

  2. 通过哈希函数/哈希算法,将 hash 值转换为数组的下标

  3. 通过数组下标快速定位到数组中的某个位置:

    • 如果这个位置上什么也没有(没有链表),则返回 null;

    • 如果这个位置上有单链表,此时会将当前 Node 中的 key 与链表上每⼀个节点中的 key 进⾏ equals ⽐较。

      • 如果所有的 equals ⽅法返回都是 false,那么 get ⽅法返回 null;

      • 如果其中有⼀个 equals 返回了 true,那么这个节点的 value 便是我们要找的 value,此时 get ⽅法最终返回这个要找的 value。

注:

  1. 放在 HashMap 中 key 的元素(或者放在 HashSet 中的元素)需要同时重写 hashCode() 和 equals() ⽅法!!!

同时重写 hashCode() 和 equals() ⽅法

  • 重写 hashCode() ⽅法时要达到散列分布均匀

    • 如果 hashCode() ⽅法返回⼀个固定的值,那么 HashMap 底层则变成了⼀个单链表;

    • 如果 hashCode() ⽅法所有返回的值都不同,此时 HashMap 底层则变成了⼀个数组。

这两种情况称之为,散列分布不均匀

equals 和 hashCode⽅法⼀定要同时重写(直接⽤ eclipse ⽣成就⾏)

TreeMap

  1. TreeSet/TreeMap 是自平衡二叉树

  2. TreeSet/TreeMap 迭代器采⽤的是中序遍历⽅式

  3. 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 种⽅法:

        1. 定义⼀个 Comparator 接⼝的实现类

        2. 使⽤匿名内部类

        3. lambda 表达式(Comparator 是函数式接⼝)

          • 利⽤ -> 的 lambda表达式 重写 compare ⽅法

          • 利⽤ Comparator.comparing ⽅法

两种解决方法如何选择?

  1. 当⽐较规则不会发⽣改变的时候,或者说⽐较规则只有⼀个的时候,建议实现 Comparable 接⼝

  2. 当⽐较规则有多个,并且需要在多个⽐较规则之间频繁切换时,建议使⽤ 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

  1. 存放的元素是键值对:即K-V

  2. hashtable的键和值都不能为null, 否则会拋出NullPointerException

  3. hashTable 使用方法基本上和HashMap一样

  4. hashTable 是线程安全的,hashMap 是线程不安全的

底层原理:

  1. 底层有数组 Hashtable$Entry[] 初始化大小为 11

  2. 临界值 threshold 8= 11* 0.75

  3. 扩容:按照自己的扩容机制来进行即可。

  4. 执行 方法 addEntry (hash, key, valve, index);添加K-V 封装到Entry

  5. 当if(count >= threshold) 满足时,就进行扩容

  6. 按照 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值
HashMap1.2不安全可以
Hashtable1.0安全不可以

Properties

  1. Properties类继承自Hashtable类井且实现了Map接口,也是使用一种键值对的形式来保存数据。

  2. 他的使用特点和Hashtable类似。

  3. Properties 还可以用于 从xxx-properties 文件中,加载数据到Properties类对象,并进行读取和修改。

  4. 说明:工作后 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

  1. HashTable

  2. Properties

  3. Properties 的 key 和 values 都是 String

  4. 常⽤⽅法

    String getProperty(String key)

    Object setProperty(String key, String value)

开发中如何选择集合实现类

  1. 先判断存储的类型(一组对象或一组键值对)

  2. 一组对象:Collection接口

    • 允许重复:List

      • 增删多:LinkedList [底层维护了一个双向链表]

      • 改查多:ArrayList [底层维护 Object类型的可变数组]

    • 不允许重复:Set

      • 无序:Hashset [底层是HashMap,维护了一个哈希表 即(数组+链表+红黑树)】

      • 排序:Treeset

      • 插入和取出顺序一致: LinkedHashSet,维护数组+双向链表

  3. 一组键值对 [双列] :Map

    • 键无序:HashMap [底层是:哈希表,jdk7:数组+链表,jdk8:数组+链表+红黑树]

    • 键排序:TreeMap

    • 键插入和取出顺序一致:LinkedHashMap

    • 读取文件:Properties

Collections工具类

Collections工具类介绍

  1. Collections 是一个操作 Set、List 和Map 等集合的工具类

  2. Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作

排序操作:(均为static方法)

  1. reverse(List):反转 List 中元素的顺序

  2. shuffle(List):对 List 集合元素进行随机排序

  3. sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序

  4. sort(List, Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序

  5. swap(List, int, int):将指定 list 集合中的i 处元素和j处元素进行交换

查找、替换:

  1. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素。

  2. Object max(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素。

  3. Object min(Collection)

  4. Object min(Collection, Comparator)

  5. int frequency(Collection, Object):返回指定集合中指定元素的出现次

  6. void copy (List dest, List src):将src中的内容复制到dest中

  7. 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);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/70701.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python代码雨

系列文章 序号文章目录直达链接1浪漫520表白代码https://want595.blog.csdn.net/article/details/1306668812满屏表白代码https://want595.blog.csdn.net/article/details/1297945183跳动的爱心https://want595.blog.csdn.net/article/details/1295031234漂浮爱心https://want…

InnoDB全文索引是如何实现的?

分析&回答 全文索引的底层实现为倒排索引。 为什么叫倒排索引&#xff08;反向索引&#xff09; 当表上存在全文索引时&#xff0c;就会隐式的建立一个名为FTS_DOC_ID的列&#xff0c;并在其上创建一个唯一索引&#xff0c;用于标识分词出现的记录行。你也可以显式的创建…

LeetCode刷题笔记【26】:贪心算法专题-4(柠檬水找零、根据身高重建队列、用最少数量的箭引爆气球)

文章目录 前置知识860.柠檬水找零题目描述解题思路代码 406.根据身高重建队列题目描述解题思路代码 452. 用最少数量的箭引爆气球题目描述踩坑-进行模拟正确思路的贪心 总结 前置知识 参考前文 参考文章&#xff1a; LeetCode刷题笔记【23】&#xff1a;贪心算法专题-1&#x…

2023年05月 C/C++(八级)真题解析#中国电子学会#全国青少年软件编程等级考试

C/C编程&#xff08;1~8级&#xff09;全部真题・点这里 第1题&#xff1a;道路 N个以 1 … N 标号的城市通过单向的道路相连:。每条道路包含两个参数&#xff1a;道路的长度和需要为该路付的通行费&#xff08;以金币的数目来表示&#xff09; Bob and Alice 过去住在城市 1.在…

百度云智大会:科技与创新的交汇点

​ 这次的百度云智大会&#xff0c;可谓是亮点云集—— 发布了包含42个大模型、41个数据集、10个精选应用范式的全新升级千帆大模型平台2.0&#xff0c;发布首个大模型生态伙伴计划&#xff0c;而且也预告了文心大模型4.0的发布&#xff0c;大模型服务的成绩单也非常秀&#x…

ABAP BP维护客户cl_md_bp_maintain=>maintain

ps_head结构如下 下面是封装好的form示例 *&---------------------------------------------------------------------* *& Form frm_modify_customer *&---------------------------------------------------------------------* *& text *&--------…

TypeScript类型守卫

概念 在语句的块级作用域【if语句内或条目运算符表达式内】缩小变量类型的一种类型推断的行为。 类型守卫可以帮助我们在块级作用域中获得更为需要的精确变量类型&#xff0c;从而减少不必要的类型断言。 类型判断&#xff1a;typeof实例判断&#xff1a;instanceof字面量相等…

2023-08-31 LeetCode每日一题(一个图中连通三元组的最小度数)

2023-08-31每日一题 一、题目编号 1761. 一个图中连通三元组的最小度数二、题目链接 点击跳转到题目位置 三、题目描述 给你一个无向图&#xff0c;整数 n 表示图中节点的数目&#xff0c;edges 数组表示图中的边&#xff0c;其中 edges[i] [ui, vi] &#xff0c;表示 ui…

Python入门学习14(面向对象)

一、内置方法 二、封装 1. 封装的概念是指&#xff1f; 将现实世界事物在类中描述为属性和方法&#xff0c;即为封装。 2. 什么是私有成员&#xff1f;为什么需要私有成员&#xff1f; 现实事物有部分属性和行为是不公开对使用者开放的。同样在类中描述属性和方法的时…

如何分库分表?

分析&回答 分库&#xff1f;分表&#xff1f;还是既分库又分表&#xff1f; 如果需要分表&#xff0c;那么分多少张表合适&#xff1f; 由于所有的技术都是为业务服务的&#xff0c;那么&#xff0c;我们就先从数据方面回顾下业务背景。 如果每天产生 8w 笔交易单&#…

SCRUM敏捷产品负责人(CSPO)认证培训课程

课程简介 Scrum是目前运用最为广泛的敏捷开发方法&#xff0c;是一个轻量级的项目管理和产品研发管理框架。产品负责人是Scrum的三个角色之一&#xff0c;产品负责人在Scrum产品开发当中扮演舵手的角色&#xff0c;他决定产品的愿景、路线图以及投资回报&#xff0c;他需要回答…

学单片机有前途吗?

学单片机有前途吗? 个人认为学习任何一门技术都比不学的强&#xff0c;针对学单片机有前途吗?那么比较对象当然就是在整个IT行业做对比。因此我们可以从职业前景、钱景、这几方面综合考量。 学单片机有前途吗?我觉得重要的一点就是是否适合职业生涯发展&#xff0c;总说程序…

C++内存管理(2)new、delete详解

目录 new operator&#xff08;new操作&#xff09; new类对象时加不加括号的差别 new工作任务 delete工作任务 new和delete 堆区空间操作&#xff08;对比malloc和free&#xff09; new和delete操作基本类型的空间 new和delete操作基本类型的数组 new和delete操作类的…

elasticsearch访问9200端口 提示需要登陆

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; elasticsearch访问9200端口 提示需要登陆 问题描述 提示&#xff1a;这里描述项目中遇到的问题&#xff1a; 在E:\elasticsearch-8.9.1-windows-x86_64\elasticsearch-8.9.1\bin目录下输入命令 ela…

python Playwright优化页面等待和处理异步操作

在使用 Playwright 进行页面自动化时&#xff0c;优化页面等待和处理异步操作是非常重要的&#xff0c;可以提高脚本的稳定性和执行效率。 优化页面等待和处理异步操作的建议 **1. 使用正确的等待条件&#xff1a;**Playwright 提供了多种等待条件&#xff0c;如等待元素出现…

【CSS】简记CSS效果:通过transition(动画过渡属性)实现侧边栏目滑入滑出

需求 在资金明细的页面中&#xff0c;点击按钮时筛选区域从左侧滑出&#xff0c;完成筛选点击确认后调用接口完成数据查询&#xff0c;筛选区域滑入左侧&#xff1b; 基于微信小程序页面实现 wxml代码 <view><!-- 操作按钮 --><button type"primary&qu…

昨天面试的时候被提问到的问题集合(答案)

1、vue的双向绑定原理是什么&#xff1f;里面的关键点在哪里&#xff1f; Vue的双向绑定原理是基于Object.defineProperty或者Proxy来实现的&#xff0c;其关键点在于数据劫持&#xff0c;即对数据的读取和修改进行拦截&#xff0c;在数据发生变化时自动更新视图 2、实现水平垂…

JVM调优记录

因为大量数据备份&#xff1b;导致在备份过程出现堆溢出的情况 当前情况 总内存&#xff1a;7.92G 已使用&#xff1a;3.7G jvm总内存最大&#xff1a;3.06G jvm非堆内存&#xff1a;最大1.23G&#xff0c;使用<170M jvm堆内存&#xff1a;最大1.83G 计算 如果预留2G扩展…

PSP - 蛋白质结构预测 OpenFold Multimer 重构训练模型的数据加载

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132602155 OpenFold Multimer 在训练过程的数据加载时&#xff0c;需要将 MSA 与 Template 信息转换成 Feature&#xff0c;再进行训练&#xff0…

[machineLearning]非监督学习unsupervised learning

1.什么是非监督学习 常见的神经网络是一种监督学习,监督学习的主要特征即为根据输入来对输出进行预测,最终会得到一个输出数值.而非监督学习的目的不在于输出,而是在于对读入的数据进行归类,选取特征,打标签,通过对于数据结构的分析来完成这些操作, 很少有最后的输出操作. 从…