14、集合:

14、集合:

主要包括:

  1. 集合框架体系
  2. Collection:
    1. List:
      1. ArrayList
      2. LinkedList;
      3. Vector
    2. Set:
      1. HashSet
      2. LinkedHashSet;
      3. TreeSet。
  3. Map:
    1. HashMap
    2. HashTable;
    3. LinkedHashMap;
    4. TreeMap;
    5. Properties。
  4. Collections。

使用不难,难点在于原理,源码,什么时候使用(应用场景)。

集合的理解和好处:

前面我们保存多个数据使用的是数组,但是数组也有许多不足的地方:

  1. 长度开始时必须指定,而且一旦指定,不能更改;
  2. 保存的必须为同一类型的元素;
  3. 使用数组进行增加元素的示意代码——比较麻烦(创建一个新的数组,长度为原数组的长度+1,将原数组拷贝过来,在末尾加入新数组)。

集合:

  1. 可以动态保存任意多个对象,使用比较方便;
  2. 提供了一系列方便的操作对象的方法:add、remove、set、get等;
  3. 使用集合添加,删除新元素的示意代码——简洁明了。

集合的框架体系:

主要分为两大类:

  1. 集合主要是两组(单列集合,双列集合)

Collection(单列集合):

在集合里面放的是单个对象。

有==两个重要 子接口:==List 和 Set,他们实现子类都是单列集合。

Map(双列集合):

在集合中放置的是键值对。

Map 接口实现子类 是双列集合,存放的 K-V。


Collection接口和常用方法:

Collection 接口实现类的特点:

public interface Collection<E> extends Iterable<E>

  1. Collection 实现子类可以存放多个元素,每个元素可以是 Object;
  2. 有些 Collection 的实现类,可以存放重复的元素,有些不可以;
  3. 有些 Collection 的实现类,有些是有序的(List),有些是无序的(Set);
  4. Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的。

Collection 接口常用方法,以实现子类 ArrayList 来演示:

  1. add:添加单个元素;
  2. remove:删除指定元素;
  3. contains:查找元素是否存在;
  4. size:获取元素个数;
  5. isEmpty:判断是否为空;
  6. clear:清空;
  7. addAll:添加多个元素;
  8. containsAll:查找多个元素是否都存在;
  9. removeAll:删除多个元素。
package com.jiangxian.collection_;import java.util.ArrayList;
import java.util.List;/*** @author JiangXian~* @version 1.0*/
public class CollectionMethod {@SuppressWarnings({"all"})public static void main(String[] args) {List list = new ArrayList();// add:添加单个元素list.add("jack");list.add(10); // 相当于List.add(Integer(10))list.add(true);System.out.println("list=" + list);// remove:删除指定元素// list.remove(0); // 删除第一个元素list.remove(true); // 删除特定的某个元素System.out.println("list=" + list);// contains:查找元素是否存在System.out.println(list.contains("jack"));System.out.println(list.contains(true));// size:获取元素个数System.out.println(list.size());// isEmpty:判断是否为空System.out.println(list.isEmpty());// clear:清空list.clear();System.out.println("list是否为空:" + list.isEmpty());// addAll:添加多个元素ArrayList list2 = new ArrayList();list2.add("红楼梦");list2.add("三国演义");list.addAll(list2);System.out.println("newlist=" + list);// containsAll:查找多个元素是否都存在System.out.println(list.containsAll(list2));// removeAll:删除多个元素list.removeAll(list2);System.out.println("list=" + list);}
}

Collection 接口遍历元素方式1-使用Iterator(迭代器):

基本介绍:

接口 Iterator——是Collection的父接口

  1. Iterator 对象称为迭代器,主要是用于遍历 Collection 集合中的元素;
  2. 所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器;
  3. Iterator 的结构——看一张图;
  4. Iterator 仅用于遍历集合,Iterator 本身并不存放对象。

Iterator接口方法:

  • hasNext():若迭代器中还有元素,返回true。(即判断是否还有下一个元素)
  • next():
    • 指针下移;
    • 将下移以后集合位置上的元素返回;
    • 一开始的指向并不再迭代器内,而是在迭代器外,即一开始为空指向。
  • remove():从底层集合中删除此迭代器返回的最后一个元素(可选操作)。

注意:

​ 在调用.next()方法之前必须调用.hasNext()进行检查,若不调用,且下一条记录无效,直接调用时,会抛出异常。

package com.jiangxian.collection_;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;/*** @author JiangXian~* @version 1.0*/
public class CollectionIterator_ {public static void main(String[] args) {Collection col = new ArrayList();col.add(new Book("三国演义","罗贯中",10.1));col.add(new Book("小李飞刀","古龙",5.1));col.add(new Book("红楼梦","曹雪芹",34.6));// System.out.println("col =" + col);// 我们希望能够遍历 col 集合:// 1. 先得到 col 对应的迭代器Iterator iterator = col.iterator();// 2. 使用 while 循环 =》快捷键ititwhile(iterator.hasNext()){Object obj = iterator.next(); // 向下转型,编译类型为Object,允许类型为 BookSystem.out.println(obj);}// 3. 当退出 while 循环后,这时iterator 迭代器,指向最后的元素;// iterator.next(); //NoSuchElementException// 4. 若希望再次遍历迭代器,需要重置我们的迭代器// 相当于我们将指针返回到未处理的状态iterator = col.iterator();while (iterator.hasNext()) {Object next =  iterator.next();System.out.println(next);}}
}class Book{private String name;private String author;private double price;public Book(String name, String author, double price) {this.name = name;this.author = author;this.price = price;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getAuthor() {return author;}public void setAuthor(String author) {this.author = author;}public double getPrice() {return price;}public void setPrice(double price) {this.price = price;}@Overridepublic String toString() {return "Book{" +"name='" + name + '\'' +", author='" + author + '\'' +", price=" + price +'}';}
}

Collection 接口遍历元素方式2-使用增强for循环:

非常适合用来遍历容器。

在底层其实仍然是一个迭代器。

可以理解成一个简化版本的迭代器。

快捷方式是输入一个:大写的i——I

package com.jiangxian.collection_;import java.util.ArrayList;
import java.util.Collection;/*** @author JiangXian~* @version 1.0*/
public class CollectionFor_ {public static void main(String[] args) {Collection col = new ArrayList();col.add(new Book("三国演义","罗贯中",10.1));col.add(new Book("小李飞刀","古龙",5.1));col.add(new Book("红楼梦","曹雪芹",34.6));// 语法:for(数据类型 对象:要遍历的容器对象){}for(Object obj : col){ System.out.println(obj);}}
}

练习:

package com.jiangxian.collection_;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;/*** @author JiangXian~* @version 1.0*/
public class CollectionExercise {public static void main(String[] args) {Collection col = new ArrayList();col.add(new Dog("小白", 3));col.add(new Dog("小黄", 4));col.add(new Dog("小黑", 5));Iterator iterator = col.iterator();while(iterator.hasNext()){Object obj = iterator.next();System.out.println(obj);}System.out.println("====增强for循环====");for(Object obj : col){System.out.println(obj);}}
}class Dog{private String name;private int age;public Dog(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}@Overridepublic String toString() {return "Dog{" +"name='" + name + '\'' +", age=" + age +'}';}public int getAge() {return age;}public void setAge(int age) {this.age = age;}
}

List 接口(Collection子接口之一) 和 常用方法:

基本介绍:

List 接口是 Collection 接口的子接口:

  1. List 集合类中元素有序(即添加顺序和取出顺序一致),且可重复;
  2. List 集合中的每个元素都有其对应的顺序所以,即支持索引(底层是一个数组);
  3. List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素;
  4. JDK API 中 List 接口的常用实现类:
    1. ArrayList;数组
    2. LinkedList;链表
    3. Vector。向量
package com.jiangxian.list_;import java.util.ArrayList;
import java.util.List;/*** @author JiangXian~* @version 1.0*/
public class List_ {@SuppressWarnings({"all"})public static void main(String[] args) {// 1.List 集合类中元素有序(即添加顺序和取出顺序一致),且可重复List list = new ArrayList();list.add("jack");list.add("tom");list.add("marry");list.add("hsp");list.add("tom");System.out.println(list);// 2. List 集合中的每个元素都有其独赢的顺序索引,支持索引(从0开始)System.out.println(list.get(0));}
}

常用方法:

List 中添加了一些根据索引来操作集合元素的方法:

  1. void add(int index, Object ele):在 index 位置插入 ele 元素;
  2. boolean addAll(int index, Collection eles):从 index 位置开始将eles中的所有元素添加进来;
  3. Object get(int index):获取指定 index 位置的元素;
  4. int indexOf(Object obj):返回 List 中第一次出现 obj 的 index;
  5. int lastIndexOf(Object obj):返回 List 中最后一次出现 obj 的 index;
  6. Object remove(int index):删除指定位置的元素,并返回该元素;
  7. Object set(int index, Object ele):设置指定位置的元素为 ele ,相当于是替换;
  8. List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex-1 位置的子集合(左闭右开)。
package com.jiangxian.list_;import java.util.ArrayList;
import java.util.List;/*** @author JiangXian~* @version 1.0*/
public class ListMethod_ {@SuppressWarnings({"all"})public static void main(String[] args) {List list = new ArrayList();list.add("jack");list.add("tom");list.add("marry");list.add("hsp");list.add("tom");System.out.println(list);// 1. void add(int index, Object ele):在 index 位置插入 ele 元素;System.out.println("==================void add==========");list.add(0,"刘备");System.out.println(list);// 2. boolean addAll(int index, Collection eles):从 index 位置开始将eles中的所有元素添加进来;System.out.println("=========boolean addAll==========");List list1 = new ArrayList();list1.add("刘邦");list1.add("项羽");list.addAll(list1);System.out.println(list);// 3. Object get(int index):获取指定 index 位置的元素;System.out.println("=========Object get==========");System.out.println(list.get(0));// 4. int indexOf(Object obj):返回 List 中第一次出现 obj 的 index;System.out.println("================int indexOf=================");System.out.println(list.indexOf("刘邦"));// 5. int lastIndexOf(Object obj):返回 List 中最后一次出现 obj 的 index;System.out.println("=========int lastIndexOf==========");System.out.println(list.lastIndexOf("tom"));// 6. Object remove(int index):删除指定位置的元素,并返回该元素;System.out.println("=========Object remove==========");System.out.println(list.remove(0));// 7. Object set(int index, Object ele):设置指定位置的元素为 ele ,相当于是替换;System.out.println("=========Object set==========");System.out.println(list.set(0, "曹操")); // 返回的是将被修改的原始元素System.out.println(list.get(0));// 8. List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex-1 位置的子集合(左闭右开)。System.out.println("=========List subList==========");System.out.println(list.subList(0, 2));}
}

三种遍历方式:

  1. 使用迭代器 iterator;
  2. 使用增强 for 循环;
  3. 使用普通 for 循环。

ArrayList(List的实现类之一):

ArrayList 的注意事项:

  1. permits all elements, including null,即 ArrayList 可以存放所有类型的元素,也包括 null(会被转换为String 类型的"null"),并且可以多个;
  2. ArrayList 底层是由数组来实现存储的;
  3. ArrayList 基本等同于 Vector,除了 ArrayList 是线程不安全(执行效率高),多线程的情况下,不建议使用 ArrayList。

ArrayList 底层结构和源码分析(重点):

  1. ArrayList 中维护了一个 Object 类型的数组 elementData。
    1. transient Object[] elementData;
    2. transient 关键字——表示瞬间,短暂的;表示该属性不会被序列化。
  2. 当创建 ArrayList 对象时,若使用无参构造器,则初始 elementData 容量为 0,第1次添加,则扩容elementData 为10,若需要再次扩容,则扩容 elementData 为1.5倍;
  3. 若使用的是指定大小的构造器,则初始 elementData 容量为指定大小,若需要扩容,则直接扩容 elementData 为1.5倍。

add 以及 扩容机制源码解读:

package com.jiangxian.list_.arrayList_;import java.util.ArrayList;
import java.util.Iterator;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class ArrayListSource_ {public static void main(String[] args) {// 使用无参构造器创建 ArrayList对象ArrayList list = new ArrayList();/*public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}ctrl + 鼠标左键 DEFAULTCAPACITY_EMPTY_ELEMENTDATA:private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};我们发现其就是一个空数组所以我们创建了一个空elementData数组*/// ArrayList list = new ArrayList(8);for (int i = 1; i <= 10; i++) {list.add(i);/*    public boolean add(E e) {ensureCapacityInternal(size + 1);  // 确定容量够不够elementData[size++] = e; // 然后再执行赋值操作return true;}*//*private void ensureCapacityInternal(int minCapacity) { // minCapacity 初始值为1if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 判断传进来的是不是空minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取最大值为minCapacity// private static final int DEFAULT_CAPACITY = 10;所以最大值为 10,minCapacity 现在的值为10.}// 上面的步骤是为了确定一个真正的 minCapacity,下面才是判断是否需要扩容ensureExplicitCapacity(minCapacity);}*//*private void ensureExplicitCapacity(int minCapacity) {modCount++; // modCount记录集合被修改的次数,一开始为0// overflow-conscious codeif (minCapacity - elementData.length > 0) // 若需要的最小容量大于 数组的长度,调用底层的growgrow(minCapacity);}*//*private void grow(int minCapacity) {// overflow-conscious code// 将原来数组的长度,存放在OldCapacity中int oldCapacity = elementData.length;// 新容量 = 原始长度加上原始长度/2,即原来的1.5倍int newCapacity = oldCapacity + (oldCapacity >> 1);// 若计算出来的新容量比 minCapacity 小,就仍然使用 minCapacityif (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 若新的容量比 MAX_ARRAY_SIZE 大,使用 hugeCapacity方法// private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);}*/}for (int i = 11; i <= 15; i++) {list.add(i);}list.add(100);list.add(200);for (Object o : list) {System.out.println(o);}}
}

有参构造器就是在构造器的源码有差别,其它没有差别。


Vector(List的实现类之一):

  1. Vector 底层也是一个对象数组,protected Object[] elementData;
  2. Vector 是线程同步的,即线程安全,Vector 类的操作方法带有 sychronized;
  3. 在开发中,需要线程同步安全时,考虑使用 Vector。

Vector 和 ArrayList 的比较:

底层结构版本线程安全(同步)效率扩容倍数
ArrayList可变数组jdk1.2不安全,效率高若时有参构造——1.5倍;若是无参——1.第一次设置为10;2.第二次扩容为1.5倍
Vector可变数组jdk1.0安全,效率不高若是无参,默认10,填满后,按两倍扩容;若指定大小,则每次直接按两倍扩容。

底层结构与源码分析:

package com.jiangxian.list_.vector_;import java.util.Vector;/*** @author JiangXian~* @version 1.0*/public class Vector_ {public static void main(String[] args) {Vector vector = new Vector();/*public Vector() {this(10); // 使用无参构造器,容量会被设置为10}*//*public Vector(int initialCapacity) {this(initialCapacity, 0);}*//*public Vector(int initialCapacity, int capacityIncrement) {super();if (initialCapacity < 0)throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);this.elementData = new Object[initialCapacity];this.capacityIncrement = capacityIncrement;}*/for(int i = 0; i < 10; i++){vector.add(i);/*public synchronized boolean add(E e) {modCount++; // 修改次数,开始为0,在AbstractList中定义ensureCapacityHelper(elementCount + 1); // 确定现在的容量是否足够elementData[elementCount++] = e;return true;}*//*private void ensureCapacityHelper(int minCapacity) {// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);}*//*private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;// capacityIncrement = 0int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);elementData = Arrays.copyOf(elementData, newCapacity);}*/}}
}

LinkedList(List的实现类之一):

  1. LinkedList 底层实现了双向链表和双端队列特点;
  2. 可以添加任意元素(元素可以重复),包括null;
  3. 线程不安全,没有实现同步。

LinkedList 的底层操作机制:

  1. LinkedList 底层维护了一个双向链表;
  2. LinkedList 中维护了两个属性 first 和 last 分别指向 首节点尾节点
  3. 每个节点(Node对象),里面又维护了 prev、next、item 三个属性,其中通过 prev 指向前一个,通过 next 指向下一个。最终实现双向队列。
  4. 所以 LinkedList 的元素的添加和删除,不是通过数组完成的,相对来说效率较高。

Node 演示:

package com.jiangxian.list_.LinkedList_;/*** @author JiangXian~* @version 1.0*/public class LinkedList01 {public static void main(String[] args) {// 模拟一个简单的双向链表Node jack = new Node("jack");Node queen = new Node("queen");Node tom = new Node("tom");// 连接三个节点,形成双向链表// jack -> queen -> tomjack.next = queen;queen.next = tom;// tom -> queen -> jackqueen.pre = jack;tom.pre = queen;Node first = jack; // 让 first 引用指向 jack,就是双向链表的首节点Node last = tom; // 让 last 引用指向 tom,就是双向链表的尾节点// 演示,从头到尾的遍历System.out.println("=====从头到尾遍历=====");while(true){if(first == null){break;}// 输出 first 信息System.out.println(first);first = first.next;}// 演示,从尾到头System.out.println("=====从尾到头遍历=====");while(true){if(last == null){break;}System.out.println(last);last = last.pre;}// 演示链表的添加对象/数据,是多么的方便// 1. 先创建一个要添加的 NodeNode smith = new Node("smith");queen.next = smith;smith.pre = queen;smith.next = tom;tom.pre = smith;first = jack;System.out.println("=====从头到尾遍历=====");while(true){if(first == null){break;}// 输出 first 信息System.out.println(first);first = first.next;}last = tom;System.out.println("=====从尾到头遍历=====");while(true){if(last == null){break;}System.out.println(last);last = last.pre;}}
}// 定义一个Node类,Node对象,表示双向链表的一个点
class Node{public Object item; // 真正存放数据public Node next; // 指向下一个节点public Node pre; // 指向前一个节点public Node(Object name){this.item = name;}@Overridepublic String toString() {return item.toString();}
}

LinkedList的增删改查:

package com.jiangxian.list_.LinkedList_;import java.util.LinkedList;/*** @author JiangXian~* @version 1.0*/
@SuppressWarnings({"all"})
public class LinkedListCRUD {public static void main(String[] args) {LinkedList linkedList = new LinkedList();linkedList.add(1);/*public boolean add(E e) {linkLast(e);return true;}*//*将传入的元素添加进双向列表:void linkLast(E e) {final Node<E> l = last; // 将原来的last 传递给final修饰的 lfinal Node<E> newNode = new Node<>(l, e, null); // 创建一个新的节点,前节点是 last(尾插法),值是传入的e,尾节点是 nulllast = newNode; // 插入在末尾的元素自然是最后一个元素if (l == null) // 若以前的双向链表中没有元素,即开始的last为空first = newNode; // 那么现在里面只有一个元素,所以头节点和尾节点,都指向新添加的节点elsel.next = newNode; // 否则,将l即原始的last的下一个指向修改为新添加的节点size++; // 链表中元素的个数+1modCount++; // 修改次数加一}*/linkedList.add(2);/*public boolean add(E e) {linkLast(e);return true;}*//*void linkLast(E e) {final Node<E> l = last;final Node<E> newNode = new Node<>(l, e, null);last = newNode;if (l == null)first = newNode;elsel.next = newNode;size++;modCount++;}*/linkedList.add(3);System.out.println("linkedList=" + linkedList);// 演示删除最后的节点(注意有多种删除,仅演示一种删除first指向的节点)linkedList.remove();System.out.println("linkedList=" + linkedList);/*public E remove() {return removeFirst();}*//*public E removeFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return unlinkFirst(f); // 真正实现删除首节点的代码}*//*private E unlinkFirst(Node<E> f) {// assert f == first && f != null;final E element = f.item; // 将首节点的值提取出来,作为返回值final Node<E> next = f.next; // 将首节点指向的下一个节点提取出来f.item = null; // 将原始首节点的值设置为空f.next = null; // help GC,将原始首节点的下一个指向设置为空first = next; // 将首节点设置为nextif (next == null)last = null;elsenext.prev = null; // 将现在首节点的前指针指向设置为空,不然会指向原来的firstsize--; // 双向链表的长度减一modCount++; // 修改次数加一return element; // 返回原来节点中的值}*/// 修改:linkedList.set(1, "abc");/*public E set(int index, E element) {checkElementIndex(index); // 检验是否是合法的indexNode<E> x = node(index);E oldVal = x.item;x.item = element;return oldVal;}*//*private void checkElementIndex(int index) {if (!isElementIndex(index))throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}*//*private boolean isElementIndex(int index) {return index >= 0 && index < size;}*//*Node<E> node(int index) {// assert isElementIndex(index);if (index < (size >> 1)) { // 当下标比链表长度的一半小时Node<E> x = first; // 设置x为 首节点for (int i = 0; i < index; i++) // 遍历要查询的 index 次x = x.next; // 一直更新 x 为 后一个节点return x; // 直到x的下标为index位置,返回(链表不是顺序存储所以查找麻烦)} else {// 否则,反向遍历即可Node<E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}}*/System.out.println("linkedList=" + linkedList);// 查找linkedList.get(1);/*public E get(int index) {checkElementIndex(index);return node(index).item;}// 和上面的修改是一样的,自己领悟下。*/}
}

这里的源码涉及到数据结构,没学过数据结构的理解起来可能比较困难。

ArrayList 和 LinkedList 的比较:

底层结构增删的效率改查的效率
ArrayList可变数组较低,数组扩容较高
LinkedList双向链表较高,通过链表节点追加较低
  1. 若改查操作较多,使用ArrayList;
  2. 若增删操作较多,使用LinkedList;
  3. 都是线程不安全。

Set 接口(Collection子接口之一)和常用方法:

Set基本介绍:

  1. 无序(添加和取出元素的顺序不一样,但是取出的顺序是固定的),没有索引
  2. 不允许重复元素,所以最多包含一个null;
  3. JDK API 中 Set 接口的实现类常用的有:
    1. HashSet;
    2. TreeSet。

Set接口的常用方法:

和 List 接口一样,Set 接口也是 Collection 的子接口,所以常用方法和 Collection 接口一样。

Set接口的常用遍历方法:

同 Collection 的遍历方式一样,因为 Set 接口是 Collection 接口的子接口:

  1. 可以使用迭代器iterator;
  2. 可以使用增强for循环;
  3. 不能使用索引方式来获取
package com.jiangxian.set_;import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class SetMethod {public static void main(String[] args) {// 1. 以Set接口的实现类 HashSet 来讲解 Set 接口的方法;// 2. Set接口的实现类的对象(Set 接口对象),不能存放重复的元素,可以添加一个 null// 3. Set接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)// 4. 注意:取出的顺序虽然不是添加的顺序,但是顺序是固定的Set set = new HashSet();set.add("jack");set.add("john");set.add("jack"); // 重复set.add("john");set.add("hsp");set.add("mary");set.add(null);set.add(null);System.out.println("set=" + set);// set=[null, hsp, mary, john, jack]// 遍历:// 方式1 :iteratorIterator iterator = set.iterator();while(iterator.hasNext()){Object obj = iterator.next();System.out.println("obj=" + obj);}// 方式2 :增强for循环:System.out.println("========增强for循环=======");for(Object o : set){System.out.println("o=" + o);}}
}

HashSet(Set的实现类之一):

HashSet的全面说明:

  1. HashSet 实现了 Set 接口;

  2. HashSet 实际上是 HashMap,源码如下:

        public HashSet() {map = new HashMap<>();}public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}
    
  3. 可以存放 null 值,但是只能有一个 null;

  4. HashSet 不保证元素是有序的,取决于 hash 后,再确定索引的结果。(即不能保证存放元素的顺序和取出顺序一致);

  5. 不能有重复元素/对象,再前面 Set 接口使用已经讲过。

底层机制说明:

模拟数组链表:

对结构的了解。

package com.jiangxian.set_;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class HashSetStructure {public static void main(String[] args) {// 模拟一个 HashSet 底层——实际上就是 HashMap 底层// 1. 创建一个数组,数组的类型是 Node[]// 2. 有些人直接将 Node[] 数组称为表(Table)Node[] table = new Node[16];System.out.println("table = " + table);// 3. 创建节点Node john = new Node("john", null);table[2] = john;Node jack = new Node("jack", null);john.next = jack; // 将jack节点挂载到john节点下Node rose = new Node("rose", null);jack.next = rose;System.out.println("table = " + table);}
}class Node{public Object item;public Node next;public Node(Object item, Node next) {this.item = item;this.next = next;}
}

源码:

分析HashSet 的添加元素底层是如何实现的。

  1. HashSet 的底层是 HashMap;
  2. 添加一个元素时,先得到 hash 值,会转换成索引值。
  3. 找到存储数据表 table,看这个索引位置是否已经存放有元素;
  4. 若没有,直接加入;
  5. 若有,调用 equals 进行比较,若相同,放弃添加,若不同,则添加到最后(说的是链表的最后,参照上面的数组lian’bi);
  6. 再Java8中,若一条链表的元素个数超过 TREEIFY_THRESHOLD(默认时8),并且table 的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进化成红黑树
第一次添加与第二次添加源码解读:
package com.jiangxian.set_;import java.util.HashSet;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class HashSetSource {public static void main(String[] args) {HashSet hashSet = new HashSet();// 1. 执行HashSet()/*public HashSet() { // 构造函数,底层是HashMapmap = new HashMap<>();}*/hashSet.add("java"); // 到此处,第一次 add 添加完成。// 2. 执行 add()/*public boolean add(E e) { // e就是我们传入的对象,此处为java,E表示泛型。return map.put(e, PRESENT)==null; // private static final Object PRESENT = new Object();}*//*public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}*/// 使用 force step into 强行进入hash(key),这个是用来计算hash值的/*static final int hash(Object key) {int h;// 可以看到,hash并不完全是hashCode,而是经过算法优化的结果return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}*//*// 是最重要的,需要仔细体会final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// 这一行,是我们定义的辅助变量,tab为table的复制,p为要对应索引位置处的空间// n为tab的容量Node<K,V>[] tab; Node<K,V> p; int n, i;// table是HashMap的一个属性,放Node节点的数据//transient Node<K,V>[] table;// 由源码可值,一开始为空if ((tab = table) == null || (n = tab.length) == 0)// 将table赋值给tab,判断是否为空,或者长度是否为0// 这个代码块较长,放置在紧接的代码块中解释。n = (tab = resize()).length;// 将tab[i = (n - 1) & hash]存储的节点赋值给p,判断是否为空if ((p = tab[i = (n - 1) & hash]) == null)// 为空,就直接将新节点填入tab[i] = newNode(hash, key, value, null);else {// 一个开发技巧提示:在需要局部变量(辅助变量)时候,在需要的时候创建Node<K,V> e; K k;// p实际上是指向我们当前的索引位置对应的链表的第一个元素// 1.若p的hash与我们想要添加的key的hash值相等// 2.同时满足 key 和 p的key属性一样或者是key不为空且内容与p的key属性一样//   所以我们需要看下对象的equals是否被重写// 就不能加入,跳转到if (e != null),return oldValue;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 那么将 p 赋值给 辅助变量 ee = p;// 判断 p 是不是一棵红黑树// 若是一颗红黑树,就调用 putTreeVal(暂时不展开),来进行添加else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 只剩下一种情况了,就是 p 为一条链表// 1.依次去和链表中的各个节点进行比较,若都不相同,遍历到null,将null 赋值为空,// 	添加新节点到末尾,若bigCount达到了TREEIFY_THRESHOLD,转换为红黑树//  在添加新节点后,立即判断 该链表是否已经达到8个节点,若满足,要对当前链表进行树化//	但是在进行树化的时候,还需要一个对table 长度的判断,若小于 64 也不会马上树化//  此时会先进行扩容// 2.若与链表中的一个节点相同,那么跳出循环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为p.next,e中存储的是p.nextp = 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();// 这个方法现在是空,在LinkedHashSet中实现afterNodeInsertion(evict);// 添加成功就返回空return null;}*/hashSet.add("hsp");}
}
对resize()的源码解读

final Node<K,V>[] resize() {// oldTab 用于存放原始的 tableNode<K,V>[] oldTab = table;// oldCap 用于存放原始的通量int oldCap = (oldTab == null) ? 0 : oldTab.length;// oldThr 用于存放阈值,用于防止多线程出现拥堵int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;// 由于是第一次,所以 oldCap = 0,执行这段代码else {               // zero initial threshold signifies using defaults// static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 为16newCap = DEFAULT_INITIAL_CAPACITY;// static final float DEFAULT_LOAD_FACTOR = 0.75f; 默认的缓冲系数// 现在 newThr 为12newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 将新的 newTab 存放在属性table中table = newTab;// 由于第一次添加,oldTab = null,所以这一段代码块不执行if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}// 返回新的newTabreturn newTab;}
扩容和转换成红黑树:
  1. HashSet 底层是 HashMap,第一次添加时,table 数组从0扩容到了 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),就会进行树化为红黑树,否则仍然采用数组
扩容:

扩容发生在哪里呢?当我们每次加入一个节点,不管节点是加在 table表的空位置,还是在链表上,都会使得++size。

package com.jiangxian.set_;import java.util.HashSet;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class HashSetIncrement {public static void main(String[] args) {/*1. HashSet 底层是 HashMap,第一次添加时,table 数组从0扩容到了 16,临界值(threshold)是16*加载因子(loadFactor)——0.75 = 12;2. 若 table 数组使用到了临界值 12,就会执行扩容,16 * 2 = 32,那么新的临界值就是 32 * 0.75 = 24*/HashSet hashSet = new HashSet();for (int i = 0; i < 100; i++) {hashSet.add(i);}}
}

final Node<K,V>[] resize() {// oldTab 用于存放原始的 tableNode<K,V>[] oldTab = table;// oldCap 用于存放原始的通量int oldCap = (oldTab == null) ? 0 : oldTab.length;// oldThr 用于存放阈值,用于防止多线程出现拥堵int oldThr = threshold;int newCap, newThr = 0;// oldCap>0说明非第一次执行resizeif (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) { // 一般是不会超过最大容量的,所以我们看下一个else-ifthreshold = Integer.MAX_VALUE;return oldTab;}// 若两倍 oldCap 比最大值小,且oldCap 大于等于默认值16// 那么新的容量为 oldCap的两倍,代码块中执行 newThr = 2倍的oldThrelse if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;// 由于是第一次,所以 oldCap = 0,执行这段代码else {               // zero initial threshold signifies using defaults// static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 为16newCap = DEFAULT_INITIAL_CAPACITY;// static final float DEFAULT_LOAD_FACTOR = 0.75f; 默认的缓冲系数// 现在 newThr 为12newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 将新的 newTab 存放在属性table中table = newTab;// 由于第一次添加,oldTab = null,所以这一段代码块不执行if (oldTab != null) { // 这段代码暂时我也没看明白for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}// 返回新的newTabreturn newTab;}
转换成红黑树:
package com.jiangxian.set_;import java.util.HashSet;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class HashSetIncrement {public static void main(String[] args) {/*1. HashSet 底层是 HashMap,第一次添加时,table 数组从0扩容到了 16,临界值(threshold)是16*加载因子(loadFactor)——0.75 = 12;2. 若 table 数组使用到了临界值 12,就会执行扩容,16 * 2 = 32,那么新的临界值就是 32 * 0.75 = 24*/HashSet hashSet = new HashSet();
//        for (int i = 0; i < 100; i++) {
//            hashSet.add(i);
//        }/*在Java8中,若一条链表的元素个数达到了 (TREEIFY_THRESHOLD)默认为8,并且 table的大小 >= MIN_TREEIFY_CAPACITY(默认为64),就会进行树化为红黑树,否则仍然采用数组扩容机制,在扩容后需要重写计算hash。*/for (int i = 1; i <= 12; i++) {hashSet.add(new A(i));// 什么意思呢?// 在执行到第9次时,会执行一次resize,从16扩容到32// 执行到第10次时,会再执行一次resize,从32扩容到64// 执行到11次时,会进行树化(红黑树,在数据结构中再深入)}}
}class A{private int n;public A(int n) {this.n = n;}// 为了确保hashCode一样@Overridepublic int hashCode() {return 100;}
}

第一次扩容:索引在4,节点类型为Node,还是链表,此时容量为16:

第二次扩容:索引在4,节点类型为Node,还是链表,此时容量为32:

第三次扩容:索引在36,节点类型为Node,还是链表,此时容量为64:

然后便不再执行扩容,而是将该链表转换为红黑树,节点类型为TreeNode,table的容量仍然时64:

关于红黑树,此处不展开,另外扩容后的hash更改,应该在resize处,但是resize第一次后的源码暂时我也没看懂,所以不写,以免误导。

Exercise:

Exercise1:

定义一个Employee 类,该类包含:private成员属性——name,age;

  1. 创建3个Employee 对象放入 HashSet;
  2. 当 name 和 age 的值相同时,认为是相同员工,不能添加到 HashSet 集合中。
package com.jiangxian.set_;import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class HashSetExercise01 {public static void main(String[] args) {HashSet hashSet = new HashSet();hashSet.add(new Employee("milan",18,10000));hashSet.add(new Employee("smith",18,10000));hashSet.add(new Employee("milan",18,20000));System.out.println(hashSet);}
}class Employee{private String name;private int age;private double salary;public Employee(String name, int age, double salary) {this.name = name;this.age = age;this.salary = salary;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public double getSalary() {return salary;}public void setSalary(double salary) {this.salary = salary;}@Overridepublic boolean equals(Object o) {// 判断是否相等,首先,判断是否是同一个对象if (this == o) return true;// 若传入的数据为空,或者类型不一致返回falseif (o == null || getClass() != o.getClass()) return false;// 向下转型Employee employee = (Employee) o;return age == employee.age  && Objects.equals(name, employee.name);}@Overridepublic int hashCode() {return Objects.hash(name, age);// 下面为计算hash的源码
//        public static int hash(Object... values) { 传入的是一个可变参数
//            return Arrays.hashCode(values);
//        }//        public static int hashCode(Object a[]) { 计算的是一个数组的hash
//            if (a == null)
//                return 0;
//
//            int result = 1;
//
//            for (Object element : a) // 使用增强for循环,将内部的值遍历,并计算到result中
//                result = 31 * result + (element == null ? 0 : element.hashCode());
//
//            return result;
//        }}@Overridepublic String toString() {return "Employee{" +"name='" + name + '\'' +", age=" + age +", salary=" + salary +'}';}
}
Exercise02:

定义一个Employee类,该类包含:private成员属性——name,sal,birthday(MyData类型——包含属性:year,month,day)

package com.jiangxian.set_;import java.util.HashSet;
import java.util.Objects;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class HashSetExercise02 {public static void main(String[] args) {HashSet hashSet = new HashSet();hashSet.add(new Employee_("Smith",21,new MyData("12","11","1")));hashSet.add(new Employee_("Smith",21,new MyData("12","11","1")));hashSet.add(new Employee_("Smith",21,new MyData("12","11","1")));System.out.println(hashSet);}
}class Employee_{private String name;private int age;MyData birthday;public Employee_(String name, int age, MyData birthday) {this.name = name;this.age = age;this.birthday = birthday;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public MyData getBirthday() {return birthday;}public void setBirthday(MyData birthday) {this.birthday = birthday;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Employee_ employee = (Employee_) o;return age == employee.age && Objects.equals(name, employee.name) && Objects.equals(birthday, employee.birthday);}@Overridepublic int hashCode() {return Objects.hash(name, age, birthday);}@Overridepublic String toString() {return "Employee_{" +"name='" + name + '\'' +", age=" + age +", birthday=" + birthday +'}';}
}class MyData{private String year;private String month;private String day;public MyData(String year, String month, String day) {this.year = year;this.month = month;this.day = day;}public String getYear() {return year;}public void setYear(String year) {this.year = year;}public String getMonth() {return month;}public void setMonth(String month) {this.month = month;}public String getDay() {return day;}public void setDay(String day) {this.day = day;}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;MyData myData = (MyData) o;return Objects.equals(year, myData.year) && Objects.equals(month, myData.month) && Objects.equals(day, myData.day);}@Overridepublic int hashCode() {return Objects.hash(year, month, day);}//[Employee_{name='Smith', age=21, birthday=com.jiangxian.set_.MyData@4554617c}, Employee_{name='Smith', age=21, birthday=com.jiangxian.set_.MyData@74a14482}, Employee_{name='Smith', age=21, birthday=com.jiangxian.set_.MyData@1540e19d}]// 这是不重写MyData的hashCode方法的结果,我们发现由于birthday的hashCode不同,即使重写了equals也没有意义@Overridepublic String toString() {return "MyData{" +"year='" + year + '\'' +", month='" + month + '\'' +", day='" + day + '\'' +'}';}
}

若不重写MyData的hashCode(),会导致计算Employee的hash值都是不同的,这是为什么呢?

其实我们上面贴出过源码能很好的解释这一点:

此时我们传进a[]的参数有:name,age 和 MyData;

name是String,age是int类型的hashCode已经被重写过了,所以特定的值返回的hashCode都是一样的;

但是由于我们MyData的hashCode()没有被重写,所以,其每个对象的hashCode的计算按默认来计算,导致不同对象的hashCode一定不同;

所以我们在add时,由于hash值都不同,就直接添加了。

//        public static int hashCode(Object a[]) { 计算的是一个数组的hash
//            if (a == null)
//                return 0;
//
//            int result = 1;
//
//            for (Object element : a) // 使用增强for循环,将内部的值遍历,并计算到result中
//                result = 31 * result + (element == null ? 0 : element.hashCode());
//
//            return result;
//        }

LinkedHashSet(Set的实现类之一):

LinkedHashSet的全面说明:

  1. LinkedHashSet 是 HashSet 的子类;

  2. LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组 + 链表 + 双链表(怎么理解呢?就是在HashMap的Node基础上,又多了两个指针,用来指向按顺序添加的节点);

  3. LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置(和HashSet一样),同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的,实际不然,只是逻辑有序,不是物理有序,物理上的存储和HashSet一摸一样,逻辑的有序是通过多的双链表实现的;

  4. LinkedHashSet 不允许重复元素。

  5. table 为 Node[]的类型,但是内部存放的元素为 Entry(LinkedHashSet的,与后文map中的Entry不是一样的!!!!那个里面的Entry是接口),其为Node的子类,添加了before 和 after 两个指针,用来实现双向链表。

        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);}}
    
package com.jiangxian.set_;import java.util.LinkedHashSet;
import java.util.Set;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class LinkedHashSetSource {public static void main(String[] args) {Set set = new LinkedHashSet();/*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); // 这个调用的就是 HashMap.Node(hash, key, value, next)}}*//*private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {// 将新添加的节点置为尾节点tail,然后将双向链表形成// 将tail赋值给lastLinkedHashMap.Entry<K,V> last = tail;// 将传入的节点设置为尾节点 tailtail = p;if (last == null)head = p;else { // 形成双向链表p.before = last;last.after = p;}}*/set.add(new String("AA"));set.add(456);set.add(456);set.add(123);System.out.println("set=" + set);}
}

除此以外,大部分的操作都和HashSet一摸一样,不再赘述。


TreeSet(Set的实现类之一):

其可以实现排序,因为内置有一个Comparator 类型的属性。在调用无参构造器时,直接使用的是compareTo,在调用有参构造器时,我们可以重写Comparator中的 compare方法,实现按我们想要的方式排序:

package com.jiangxian.set_;import java.util.Comparator;
import java.util.TreeSet;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class TreeSet_ {public static void main(String[] args) {// 1.当我们使用无参构造器,创建TreeSet时,仍然是无序的// 2.使用TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)//      并可以指定排序规则// 浅浅看看个源码:/*1. 构造器将传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap属性 comparatorpublic TreeSet(Comparator<? super E> comparator) {this(new TreeMap<>(comparator));}public TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;}2. 执行add操作:public boolean add(E e) {return m.put(e, PRESENT)==null;}private transient Entry<K,V> root;public V put(K key, V value) {Entry<K,V> t = root; // 创建一个t,将root赋值给他,一开始root为nullif (t == null) {// 当t为空时,即第一次compare(key, key); // type (and possibly null) check// 这个比较只是为了防止key为空,且没有返回值root = new Entry<>(key, value, null);// 将新的Entry存入root(),且此处的Entry不是map.Entry这个接口了,而是一个内部类(与Node类似)size = 1; // 将内部实际存储的数据个数记录为sizemodCount++; // 修改次数自增return null; // 返回null}int cmp;Entry<K,V> parent;// split comparator and comparable paths// cpr是我们传入的 comparator对象,若没有,就是空Comparator<? super K> cpr = comparator;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 // 若相等,则添加不进去了,更改value(在set中没有意义)return t.setValue(value);} while (t != null);}else { // 若使用的是无参构造器,使用compareTo方法if (key == null)throw new NullPointerException();@SuppressWarnings("unchecked")Comparable<? super K> k = (Comparable<? super K>) key;do {parent = t;cmp = k.compareTo(t.key);if (cmp < 0)t = t.left;else if (cmp > 0)t = t.right;elsereturn t.setValue(value);} while (t != null);}Entry<K,V> e = new Entry<>(key, value, parent);if (cmp < 0)parent.left = e;elseparent.right = e;fixAfterInsertion(e);size++;modCount++;return null;}*/TreeSet treeSet = new TreeSet(new Comparator() {@Overridepublic int compare(Object o1, Object o2) {return ((String)o1).compareTo((String)o2);}});// 添加元素:treeSet.add("jack");treeSet.add("tom");treeSet.add("smith");treeSet.add("a");System.out.println(treeSet);}
}

Map 接口和常用方法:

Map 接口实现类的特点(很实用):

存放双列k-v;

Set也用k-v,k也是存放我们传入的对象,但是v的值确是确定的,是Present(Object,仅仅用来占位)。

此处仅讲JDK8的Map接口的特点:

  1. Map 和 Collection 是并列存在的,用于保存具有映射关系的数据:Key-Value(键值对);
  2. Map 中的 key 和 value 可以是任何引用类型的数据,其会被封装到 HashMap$Node 对象中,也就是说Map中的键值对(Key-Value)实际上就是存放在 Node 类型中的;
  3. Map 中的 key 不允许重复,所以只能最多有一个空;
  4. Map 中的 value 可以重复,所以可以有多个空;
  5. 我们常用 String 类作为 Map 的 key;
  6. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value。
  7. Map 存放数据的key-value示意图,一对k-v 是放在 HashMap$Node 中的,又因为 Node 实现了 Entry 接口,有些书上也说一对k-v就是一个Map.Entry。

源码:

Example:
package com.jiangxian.map_.mapsource_;import java.util.HashMap;
import java.util.Map;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class MapSource {public static void main(String[] args) {// 1. Map 与 Collection 并列存在,用于保存具有映射关系的数据;// 2. Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中// 3. Map 中的 key 不允许重复,所以只能有一个 null;// 4. Map 中的 value 可以冲覅,所以可以有多个 null;// 5. 经常使用 String 类作为 Map 的 key// 6. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到指定的 key 总能找到对应的 valueMap map = new HashMap();map.put("no1","韩顺平"); // 源码在HashSet中看过了。map.put("no2","张无忌");map.put("no1","张三丰"); // 当有相同的k,就等价于替换,用新的value去替换oldValue。System.out.println(map.get("no1"));System.out.println("map=" + map);}
}
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;}}// 此处会和HashSet的结果不同,为什么呢?我们来分析下:// 当e不为空时,说明key有相同的,即有重复的元素// HashSet 中的 value 都是一样的,key 也都是一样的,所以没有什么影响,就是不添加// 但是Map中的value是有实际意义的,不再是HashSet中起到占位作用的了// 它会将新的value覆盖原本的value。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;}
get(key):
public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;}
Key-Value的存储:

一对k-v就是放在 HashMap$Node 中的,我们怎么得到这个结论的呢,从putval的源码中有这样一段代码:

if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);

newNode的返回类型是啥呢?

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {return new Node<>(hash, key, value, next);}

可以看到我们存放到tab中的是一个Node类型的节点,那么自然,一对k-v是存放在Node中的啦,那么Node这个内部类是什么样的呢?我们可以看到,Node中有一个接口,Map.Entry。怎么去理解下面的这种图呢,我们会把指向实际存放k-v的Node引用存放进一个Map.Entry类型的Entry中,Entry组成的Set叫做EntrySet,在

 static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey()        { return key; }public final V getValue()      { return value; }public final String toString() { return key + "=" + value; }public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}}
// 1. k-v 最后是 HashMap$Node node = newNode(hash, key, value, null);
// 2. k-v 为了方便程序员遍历,还会创建 EntrySet(是一个集合)
// 		该集合存放的元素类型是Map.Entry,而一个Entry对象就包含key和value
// 		源码——transient Set<Map.Entry<K,V>> entrySet;
// 		在这段代码中,我们只是让K指向了Node中的key,V指向了Node中的value,
//		只是一个简单的指向,没有存放新的东西,
//		所以当我们迭代EntrySet时,实际上是在迭代HashMap中所有的Node对象
// 3. entrySet 中,定义的类型是 Map.Entry,但实际存放的是 HashMap$Node 的类型,为什么呢,
//	这是因为Node实现了Map.Entry 这个接口,所以运行类型是 Node(编译类型是Map.Entry,运行类型是Node)
// 4. 这样,当把 HashMap$Node 对象的引用存放到 entrySet 就方便我们进行遍历,其提供了两个非常重要的方法
//    getKey和getValue

欸,那我们为什么要这么写呢,Node明明实现类Entry接口,那么肯定有getKey 和 getValue方法呀?

看下源码,我们发现了什么?Node类型是一个默认类型的内部类,所以我们在外部是不能够调用的!

static class HashMap. Node<K, V> implements Map. Entry<K, V>

而Map.Entry呢?

public static interface Map. Entry<K, V>

我们来看一下代码:

package com.jiangxian.map_.mapsource_;import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class MapSource {public static void main(String[] args) {// 1. Map 与 Collection 并列存在,用于保存具有映射关系的数据;// 2. Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中// 3. Map 中的 key 不允许重复,所以只能有一个 null;// 4. Map 中的 value 可以冲覅,所以可以有多个 null;// 5. 经常使用 String 类作为 Map 的 key// 6. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到指定的 key 总能找到对应的 valueMap map = new HashMap();map.put("no1","韩顺平"); // 源码在HashSet中看过了。map.put("no2","张无忌");map.put("no1","张三丰"); // 当有相同的k,就等价于替换,用新的value去替换oldValue。System.out.println(map.get("no1"));System.out.println("map=" + map);Set set = map.entrySet();for (Object object : set) {Map.Entry entry = (Map.Entry) object;System.out.println(entry.getKey() + "-" + entry.getValue());}// keySet就是我们图上的keySetSet set1 = map.keySet();for (Object object : set1) {System.out.println(object );}// values就是我们图上的valuesCollection collection = map.values();for (Object object : collection) {System.out.println(object );}}
}

我们发现了一个小问题,为什么一个接口可以有实例化的对象呢?

这其实是一个理解误区,这不是实例化,只是类型转换(向下转换)

Map.Entry entry = (Map.Entry) object;

所以这段代码仅仅是类型转换,而不是实例化,实例化需要new,是一个创建对象的过程。

Map常用方法:

  1. map.put(Object key,Object value):添加k-v;
  2. map.remove(Object key):根据键删除映射关系;
  3. map.get(Object key):根据键返回值;
  4. map.size():获取元素的个数;
  5. map.clear():清除k-v;
  6. map.containsKey():查找键是否存在;
package com.jiangxian.map_.mapmethod_;import java.util.HashMap;
import java.util.Map;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class MapMethod {public static void main(String[] args) {// 演示Map的常用方法:System.out.println("========put==========");Map map = new HashMap();map.put("邓超",new Book("",100));map.put("邓超","孙俪");map.put("JiangXian",null);map.put(null,"JiangXian");System.out.println("map=" + map);// removeSystem.out.println("========remove========");map.remove(null);System.out.println("map=" + map);// getSystem.out.println("=======get==========");System.out.println(map.get("JiangXian"));// sizeSystem.out.println("=======size==========");System.out.println("size=" + map.size());// isEmptySystem.out.println("========isEmpty==========");System.out.println("isEmpty=" + map.isEmpty());// containsKeySystem.out.println("=======contains==========");System.out.println("contains=" + map.containsKey("邓超"));// clearSystem.out.println("========clear=======");map.clear();System.out.println("map=" + map);System.out.println(map.isEmpty());}
}class Book{private String name;private int num;public Book(String name, int num) {this.name = name;this.num = num;}
}

Map接口遍历方式:

  1. 使用keySet——获得key的set后,用get去访问值;
  2. 使用values——直接获得Collection形式的值,遍历即可;
  3. 使用entrySet。
package com.jiangxian.map_.mapmethod_;import java.util.*;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class MapFor_ {public static void main(String[] args) {Map map = new HashMap();map.put("邓超","孙俪");map.put("JiangXian",null);map.put(null,"JiangXian");map.put("hsp","hsp's wife");// 使用HashMap的keySet,获取所有键Set set = map.keySet();// 1. 使用迭代器:System.out.println("======keySet's iterator======");Iterator iterator = set.iterator();while(iterator.hasNext()){Object key = iterator.next();System.out.println(key + "-" + map.get(key));}// 2. 增强forSystem.out.println("======keySet's for======");for(Object key : set){System.out.println(key + "-" + map.get(key));}// 使用HashMap的values:Collection value = map.values();// iteratorSystem.out.println("======values's iterator======");Iterator iterator1 = value.iterator();while(iterator1.hasNext()){Object object = iterator1.next();System.out.println(object);}// forSystem.out.println("======values's for======");for(Object o : value){System.out.println(o);}// 使用entrySet获取所有关系的k-vSet set1 = map.entrySet();// iteratorSystem.out.println("======entrySet's iterator======");Iterator iterator2 = set1.iterator();while(iterator2.hasNext()){Map.Entry entry = (Map.Entry)iterator2.next();System.out.println(entry.getKey() + "-" + entry.getValue());}// forSystem.out.println("======entrySet's for======");for(Object o : set1){Map.Entry entry = (Map.Entry)o;System.out.println(entry.getKey() + "-" + entry.getValue());}}
}

Exercise:

package com.jiangxian.map_.exercise;import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;/*** @author JiangXian~* @version 1.0*/
@SuppressWarnings({"all"})
public class Exercise {public static void main(String[] args) {Map map = new HashMap();Employee jiangxian = new Employee("Jiangxian", 20000);Employee Hsp = new Employee("Hsp", 30000);Employee king = new Employee("King", 1000000000);map.put(jiangxian.getId(), jiangxian);map.put(Hsp.getId(), Hsp);map.put(king.getId(), king);System.out.println(map);Set set = map.keySet();// 输出薪水大于20000的,遍历方式1for (Object key : set) {Employee employee = (Employee) map.get(key);if(employee.getSalary() > 20000){System.out.println(employee);}}// 遍历方式2System.out.println("=======value========");Collection value = map.values();for(Object o : value){Employee employee = (Employee) o;if(employee.getSalary() > 20000){System.out.println(employee);}}// 遍历方式3:Set set1 = map.entrySet();System.out.println("=====entrySet======");for(Object o : set1){Map.Entry entry = (Map.Entry) o;Employee employee = (Employee) entry.getValue();if(employee.getSalary() > 20000){System.out.println(employee);}}}
}class Employee{public static int num;private int id;private String name;private double salary;public Employee(String name, double salary){this.name = name;this.salary = salary;this.id = ++num;}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public double getSalary() {return salary;}public void setSalary(double salary) {this.salary = salary;}@Overridepublic String toString() {return "Employee{" +"id=" + id +", name='" + name + '\'' +", salary=" + salary +'}';}
}

HashMap(Map接口实现类之一):

HashMap小结:

  1. HashMap 是 Map 接口使用频率最高的实现类;
  2. HashMap 是以 key-val 对的方式来存储数据(实际存放在HashMap$Node中)
  3. key 不能重复,但是value可以重复,允许使用null键和null值;
  4. 若添加相同的key,会用新的value覆盖掉原先旧的value;
  5. 与 HashSet 一样,不能保证映射的顺序,因为底层是 hash表的方式存储的(JDK8的hashMap 底层为数组+链表(+红黑树));
  6. HashMap 没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized。

底层机制和源码剖析:

扩容机制:

和HashSet相同(因为HashSet实际上创建的就是HashMap):

  1. HashMap 底层维护了Node类型的数组 table,默认为 null;
  2. 当创建对象时,将加载因子(loadFactor)初始化为0.75;
  3. 当添加k-v时,通过key的hash值,得到在 table 的索引位置。然后判断索引位置是否有元素:
    1. 若没有元素,直接添加;
    2. 若有元素,判断该元素的key和我们准备加入的key是否相等:
      1. 若相等,替换原始的value为新的value
      2. 若不相等,需要判断是树结构还是链表结构,做出相应的处理。(若容量不够还需要进行扩容)。
  4. 第一次添加后,table 的 capacity会被设置为默认的16,临界值 threshold 为 12;
  5. 以后每次扩容,都是将原始的 capacity * 2,临界值 threshold 也会变为原来的两倍;
  6. 在 JDK8 中,要满足以下两个条件,那么链表就会进化为树:
    1. 一条链表的长度超过了8;
    2. table 的大小 >= MIN_TREEIFY_CAPACITY(默认64)。
  7. 要是一条链表的长度>=8且还要往上添加节点,但是table的大小没有超过64,那么会执行的是扩容操作,哪怕没有达到阈值。

源码:

已经在 HashSet 和 Map 中分析过了,就不再啰嗦了。


HashTable(Map接口实现类之一):

基本介绍:

  1. 存放的是键值对k-v;
  2. HashTable 的键和值都不能是null,否则会抛出 NullPointerException的异常;
  3. HashTable 使用方法基本和 HashMap一样;
  4. HashTable 是线程安全的(synchronized),HashMap是线程不安全的。

与HashMap的对比:

版本线程安全(同步)效率允许null键和null值
HashMap1.2不安全较高
HashTable1.0安全较低

Properties(Map接口实现类之一):

基本介绍:

  1. Properties类继承自HashTable类并且实现了Map接口,也是一种键值对的形式来保存数据;
  2. 他的使用特点和Hashtable类似;
  3. Properties 还可以用于 从 xxx.properties文件中,加载数据到 Properties类对象,并进行读取和修改;
  4. 说明:工作后,常使用 xxx.properties 文件为配置文件,这个知识点会在IO流举例。

基本使用:

package com.jiangxian.map_;import java.util.Properties;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class Properties_ {public static void main(String[] args) {// 1. Properties 继承 Hashtable// 2. 可以通过 k-v 存放数据,当然 key 和 value 不能为 nullProperties properties = new Properties();
//        properties.put("abc",null);
//        properties.put(null,"def");properties.put("lucy",100);System.out.println("properties = " + properties);}
}

基本用法外,在IO流中介绍。


总结——开发中如何选择集合实现类:

在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:

  1. 先判断存储的类型(一组对象[单列] 或 一组键值对[双列])
  2. 一组对象[单列]:Collection
    1. 允许重复:List
      1. 增删多:LinkedList [底层维护了一个双向链表];
      2. 改查多:ArrayList [底层维护 Object 类型的可变数组]。
    2. 不允许重复:Set
      1. 无序:HashSet——数组+链表(+红黑树);
      2. 排序:TreeSet;
      3. 插入和取出顺序一致:LinkedHashSet——数组+链表+双向链表;
  3. 一组键值对[双列]:Map
    1. 键无序:HashMap
    2. 键排序:TreeMap
    3. 键插入和取出顺序一致:LinkedHashMap;
    4. 读取文件:Properties。

Collections 工具类:

Collections 工具类介绍:

  1. 是一个操作 Set、List 和 Map 等集合的工具类;
  2. 提供了一系列静态的方法对集合元素进行排序、查询和修改等操作。

排序操作:

  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):根据 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对象中的所有旧值。
    ity * 2,临界值 threshold 也会变为原来的两倍;
  8. 在 JDK8 中,要满足以下两个条件,那么链表就会进化为树:
    1. 一条链表的长度超过了8;
    2. table 的大小 >= MIN_TREEIFY_CAPACITY(默认64)。
  9. 要是一条链表的长度>=8且还要往上添加节点,但是table的大小没有超过64,那么会执行的是扩容操作,哪怕没有达到阈值。

源码:

已经在 HashSet 和 Map 中分析过了,就不再啰嗦了。


HashTable(Map接口实现类之一):

基本介绍:

  1. 存放的是键值对k-v;
  2. HashTable 的键和值都不能是null,否则会抛出 NullPointerException的异常;
  3. HashTable 使用方法基本和 HashMap一样;
  4. HashTable 是线程安全的(synchronized),HashMap是线程不安全的。

与HashMap的对比:

版本线程安全(同步)效率允许null键和null值
HashMap1.2不安全较高
HashTable1.0安全较低

Properties(Map接口实现类之一):

基本介绍:

  1. Properties类继承自HashTable类并且实现了Map接口,也是一种键值对的形式来保存数据;
  2. 他的使用特点和Hashtable类似;
  3. Properties 还可以用于 从 xxx.properties文件中,加载数据到 Properties类对象,并进行读取和修改;
  4. 说明:工作后,常使用 xxx.properties 文件为配置文件,这个知识点会在IO流举例。

基本使用:

package com.jiangxian.map_;import java.util.Properties;/*** @author JiangXian~* @version 1.0*/@SuppressWarnings({"all"})
public class Properties_ {public static void main(String[] args) {// 1. Properties 继承 Hashtable// 2. 可以通过 k-v 存放数据,当然 key 和 value 不能为 nullProperties properties = new Properties();
//        properties.put("abc",null);
//        properties.put(null,"def");properties.put("lucy",100);System.out.println("properties = " + properties);}
}

基本用法外,在IO流中介绍。


总结——开发中如何选择集合实现类:

在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:

  1. 先判断存储的类型(一组对象[单列] 或 一组键值对[双列])
  2. 一组对象[单列]:Collection
    1. 允许重复:List
      1. 增删多:LinkedList [底层维护了一个双向链表];
      2. 改查多:ArrayList [底层维护 Object 类型的可变数组]。
    2. 不允许重复:Set
      1. 无序:HashSet——数组+链表(+红黑树);
      2. 排序:TreeSet;
      3. 插入和取出顺序一致:LinkedHashSet——数组+链表+双向链表;
  3. 一组键值对[双列]:Map
    1. 键无序:HashMap
    2. 键排序:TreeMap
    3. 键插入和取出顺序一致:LinkedHashMap;
    4. 读取文件:Properties。

Collections 工具类:

Collections 工具类介绍:

  1. 是一个操作 Set、List 和 Map 等集合的工具类;
  2. 提供了一系列静态的方法对集合元素进行排序、查询和修改等操作。

排序操作:

  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):根据 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对象中的所有旧值。

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

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

相关文章

AI表情神同步!LivePortrait安装配置,一键包,使用教程

快手在AI视频这领域还真有点东西&#xff0c;视频生成工具“可灵”让大家玩得不亦乐乎。 现在又开源了一个超好玩的表情同步&#xff08;表情控制&#xff09;项目。 一看这图片&#xff0c;就充满了娱乐性。发布没几天就已经有8000Star。 项目****简介 LivePortrait 是一款…

OODA循环在网络安全运营平台建设中的应用

OODA循环最早用于信息战领域&#xff0c;在空对空武装冲突敌对双方互相较量时&#xff0c;看谁能更快更好地完成“观察—调整—决策—行动”的循环程序。 双方都从观察开始&#xff0c;观察自己、观察环境和敌人。基于观察&#xff0c;获取相关的外部信息&#xff0c;根据感知…

css使盒子在屏幕的地点固定

在 CSS 中&#xff0c;要将一个元素固定在页面的某个位置&#xff0c;可以使用 position: fixed 属性。以下是详细的代码示例和中文解释&#xff1a; <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta n…

阿里云服务器(centos7.6)部署前后端分离项目

Mysql8安装部署 确定一下系统的glibc版本&#xff0c;可以使用以下命令进行查看&#xff0c;当前系统glibc版本&#xff1a;2.17&#xff08;重要&#xff01;&#xff01;&#xff01;&#xff09; 要根据自己服务器的版本去选择对应的mysql&#xff0c;不然后续安装会报错&a…

Java中TimedCache缓存对象的详细使用

一、TimedCache 是什么&#xff1f; TimedCache是一个泛型类&#xff0c;它的主要作用通常是在一定时间范围内对特定键值对进行缓存&#xff0c;并且能够根据设定的时间策略来自动清理过期的缓存项。 TimedCache是一种带有时间控制功能的缓存数据结构。在 Java 中&#xff0c…

11、数组

1、数组概念 数组就是存储多个相同数据类型的数据。 比如&#xff1a;存储26个字母&#xff0c;存储一个班级的学生成绩。 2、数组使用 数组要遵循先定义再使用 2.1、数组定义的格式 存储数据---空间 ---- 数据类型 多少个 --- 数据个数 >> 数据类型 数…

六、文本搜索工具(grep)和正则表达式

一、grep工具的使用 1、概念 grep&#xff1a; 是 linux 系统中的一个强大的文本搜索工具&#xff0c;可以按照 正则表达式 搜索文本&#xff0c;并把匹配到的行打印出来&#xff08;匹配到的内容标红&#xff09;。 2、语法 grep [options]…… pattern [file]…… 工作方式…

【python】爬去二手车数据 未完成

技术方案 python selenium 先下载Microsoft Edge WebDriver Microsoft Edge WebDriver 官网 先看一下自己的edge版本 搜索到版本然后下载自己的版本 安装依赖 pip install seleniumimport time from selenium import webdriverdriver webdriver.Edge(executable_pathr&qu…

玩游戏常常出现vc++runtime library error R6025 这是什么意思,该怎么解决?

当玩游戏时常常出现“vc runtime library error R6025”错误&#xff0c;这通常表明微软C开发运行库组件存在问题。以下是对该错误及其解决方法的详细解释&#xff1a; 错误含义 “vc runtime library error R6025”是一个与Visual C运行时库相关的错误&#xff0c;该错误表明…

C++设计模式:桥接模式(Bridge)

什么是桥接模式&#xff1f; 桥接模式&#xff08;Bridge Pattern&#xff09;是一个用来解耦的设计模式&#xff0c;它将抽象层和实现层分离开&#xff0c;让它们可以独立变化。用最简单的话来说&#xff0c;就是让你能够改变抽象的功能和具体的实现&#xff0c;而不需要修改…

【深度学习基础】一篇入门模型评估指标(分类篇)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;深度学习_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前言 2. 模…

深度学习基础02_损失函数BP算法(上)

目录 一、损失函数 1、线性回归损失函数 1.MAE损失 2.MSE损失 3.SmoothL1Loss 2、多分类损失函数--CrossEntropyLoss 3、二分类损失函数--BCELoss 4、总结 二、BP算法 1、前向传播 1.输入层(Input Layer)到隐藏层(Hidden Layer) 2.隐藏层(Hidden Layer)到输出层(Ou…

从技术视角看AI在Facebook全球化中的作用

在全球化日益加深的今天&#xff0c;人工智能&#xff08;AI&#xff09;作为一种变革性技术&#xff0c;正在深刻影响全球互联网巨头的发展方向。Facebook作为全球最大的社交媒体平台之一&#xff0c;正通过AI技术突破语言、文化和技术的障碍&#xff0c;推动全球化战略的实现…

41 基于单片机的小车行走加温湿度检测系统

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于51单片机&#xff0c;采样DHT11温湿度传感器检测温湿度&#xff0c;滑动变阻器连接数码转换器模拟电量采集传感器&#xff0c; 电机采样L298N驱动&#xff0c;各项参数通过LCD1602显示&#x…

OkHttp3 - 2. OkHttp的核心组件与架构

1 OkHttp的工作原理 OkHttp3 的核心设计遵循以下原则&#xff1a; 请求与响应的分离&#xff1a;通过 Request 和 Response 对象解耦请求构建与结果处理。异步与同步支持&#xff1a;使用 Call 对象管理请求&#xff0c;可以同步或异步执行。高效连接复用&#xff1a;通过连接…

Python3 爬虫 Scrapy的使用

安装完成Scrapy以后&#xff0c;可以使用Scrapy自带的命令来创建一个工程模板。 一、创建项目 使用Scrapy创建工程的命令为&#xff1a; scrapy startproject <工程名> 例如&#xff0c;创建一个抓取百度的Scrapy项目&#xff0c;可以将命令写为&#xff1a; scrapy s…

好文推荐——sympy化简表达式高级处理——「SymPy」符号运算(2) 各种形式输出、表达式的化简合并与展开

「SymPy」符号运算(2) 各种形式输出、表达式的化简合并与展开 「SymPy」符号运算(2) 各种形式输出、表达式的化简合并与展开_sympy解方程怎么格式化输出-CSDN博客

12 设计模式之工厂方法模式

一、什么是工厂方法模式&#xff1f; 1.定义 在软件开发中&#xff0c;设计模式 是解决常见软件设计问题的最佳实践。而 工厂方法模式&#xff08;Factory Method Pattern&#xff09; 作为创建型设计模式之一&#xff0c;常常被用来解决对象创建问题。它通过将对象的创建交给…

【S500无人机】--地面端下载

之前国庆的时候导师批了无人机&#xff0c;我们几个也一起研究了几次&#xff0c;基本把无人机组装方面弄的差不多了&#xff0c;还差个相机搭载&#xff0c;今天我们讲无人机的调试 硬件配置如下 首先是地面端下载&#xff0c;大家可以选择下载&#xff1a; Mission Planne地…

Android -- 简易音乐播放器

Android – 简易音乐播放器 播放器功能&#xff1a;* 1. 播放模式&#xff1a;单曲、列表循环、列表随机&#xff1b;* 2. 后台播放&#xff08;单例模式&#xff09;&#xff1b;* 3. 多位置同步状态回调&#xff1b;处理模块&#xff1a;* 1. 提取文件信息&#xff1a;音频文…