Java之集合

😀😀😀创作不易,各位看官点赞收藏.

Java之集合

集合:集合、数组都是对多个数据进行存储操作的结构。但是数组一旦初始化后长度就确定了并且只能操作指定的数据类型,数组中提供的方法有限单调,而集合长度可以修改并且分封装了很多其它有用的实际方法来操作数据。Java集合中可分为Collection和Map两个体系。

1、Collection集合

Collection接口:用于存放单个数据,定义了存取一组对象的方法集合。下面有两个子接口(List、Set),在这两个接口下有很多实现类,我们一般使用这些来操作数据。

image-20220517115046545

1.1、Collection接口

​ 它是一个接口其中定义了很多抽象方法,提供给子类去实现。

boolean add(E e); // 向集合中添加一个元素,支持泛型
boolean addAll(Collection<? extends E> c); // 将另一个Collection中的元素添加到集合中,支持泛型
boolean isEmpty(); // 判断集合是否为空
boolean contains(Object o); // 判断集合中是否包含参数元素,一般通过o中重写的equals()方法来判断是否相同
boolean containsAll(Collection<?> c); // 判断c集合的元素都在集合中
boolean remove(Object o); // 移除元素,使用o中重写的equals()方法来判断是否为移除元素,移除成功返回true,没有这个元素或者移除失败返回false,如果存在相同元素,只删除其中的一个
boolean removeAll(Collection<?> c); // 当前集合中移除c中有的元素,移除共有元素 ===> 差集
boolean retainAll(Collection<?> c); // 当前集合与c集合的交集,===> 交集
boolean equals(Object o); // 判断当前集合中所有元素是否都和o元素相等,一般o也是一个集合就是判断集合中每一个元素是否对应相
int hashCode(); // 获取对象的hash值 
Object[] toArray(); // 将集合转换成一个数组
Iterator<E> iterator(); // 返回一个遍历该集合元素的迭代器,用于遍历集合元素
void clear(); // 清除List集合中所有元素
// 数组转换成结合,将一个数组转换成List集合,本质返回的是一个ArrayList这个子类
Arrays.asList(x[] c); // 如果是基本数据类型数组会识别成一个元素,而其它类型的数组直接将数组元素添加到集合中

1.2、Iterator 迭代器

Iterator迭代器:主要用于遍历集合元素,是迭代器模式(设计模式),为容器而生。Collection接口中有一个Iterator<E> iterator();方法,返回一个实现了iterator接口对象。迭代器仅用于集合不提供封装类的功能,创建一个iterator对象必须有一个Collection集合。集合每次调用iterator()方法就会生成一个新的迭代器,游标从第一个元素开始。

// 使用迭代器遍历集合
public static void main(String[] args) {
    Collection collection = new ArrayList();
    collection.add("你好");
    collection.add(1234);
    collection.add("Hello");
    collection.add('d');
    collection.add(true);

    // 遍历集合
    Iterator iterator = collection.iterator(); // 获取集合迭代器对象
    while (iterator.hasNext()){ // 判断是否存在下一个元素
        Object next = iterator.next(); // 开始游标指向一个空,先游标下移,返回下移以后游标指向的元素
        System.out.println(next); 
    }
}

image-20220517171712264

remove():可以在遍历时使用迭代器来删除集合中的元素,不同于集合使用remove()来删除元素。

// 遍历集合
Iterator iterator = collection.iterator(); // 获取集合迭代器对象
while (iterator.hasNext()){ // 判断是否存在下一个元素
    Object next = iterator.next(); // 获取到每一个元素,游标下移
    if ("Hello".equals(next)){
        iterator.remove(); // 移除集合元素
    }
}

注意:

  • 如果还未调用next()方法,直接调用remove()方法,会报错。
  • 如果调用了remove()方法,但是后面还没调用next()方法,再次调用remove()方法也会报错。

1.3、foreach

JDK5.0新增一种遍历集合、数组的方法,它的地底层依旧使用的是迭代器实现的。

public static void main(String[] args) {
    Collection collection = new ArrayList();
    collection.add("你好");
    collection.add(1234);
    collection.add("Hello");
    collection.add('d');
    collection.add(true);

    // o表示遍历到的每一个元素,collection是遍历的集合,这种方式不需要考虑下标因素
    for (Object o : collection){ // o是新创建的一个临时元素
        System.out.println(o);
    }
}

面试题:

public static void main(String[] args) {
    String[] array = new String[]{"AA","BB","CC"};

    // 由于s是一个新创建的临时变量,同时指向数组中元素,但是由于String的不可变性,不能修改数组中字符串元素
    for (String s : array) { 
        s = "DD";
    }
    System.out.println(Arrays.toString(array)); // 输出 AA BB CC
}

1.4、List接口

List接口:继承于Collection接口,集合中的元素是有序、可重复的,每个元素都有自己对应的顺序索引。实现类由ArrayListLinkedList、Vector,通常用来代替数组,称为动态数组。

1.4.1、ArrayList实现类

ArrayList:作为List接口的主要实现类,存储有序、不重复数据,线程不安全的。它的底层采用的是Object[]类型的数组。

// jdk7之前的ArrayList源码分析
public ArrayList() { // 无参构造器,直接创建默认10长度ObjecT[]
   this(10)
}

public ArrayList(int initialCapacity) { // 有参构造器,创建指定长度Object[]
   this.elementData = new Object[initialCapacity];
}

public boolean add(E e) { // 添加元素
    ensureCapacityInternal(size + 1); // 保证Object[]的容量足够
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    // calculateCapacity 计算需要的最小容量
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
	// 如果需要最小容量比容量大,进行扩容	
    if (minCapacity - elementData.length > 0)
        grow(minCapacity); // 扩容
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 在原来基础上扩大1.5倍
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 把原数据拷贝到新Object[]中
    elementData = Arrays.copyOf(elementData, newCapacity);
}
//jdk8之后的ArrayList分析,就是初始化发生了变化
public ArrayList() { // 无参构造器,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个{}常量
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(int initialCapacity) { // 也可以指定Object[]数组长度
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}

// 当执行第一个添加元素操作时,如果添加的元徐个数小于10,则直接创建长度10的Object[],否则创建指定长度的数组
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 扩容操作和jdk7类似
1.4.2、LinkedList实现类

LinkedList:存储有序、不重复数据,线程不安全的。它的底层采用的是双向链表存储数据的。

// LinkedList 源码分析
transient int size = 0; // 元素个数
transient Node<E> first; // 头结点,第一个元素
transient 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;
    }
}

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;
    else
        l.next = newNode; // 不是第一个节点,就把尾节点的前驱设置成新创建的节点
    size++;
    modCount++;
}
1.4.3、List常见方法

​ List继承至Collection接口,继承了Collection的所有抽象方法并且还增加了一些自己的抽象方法,还有一些default方法提供给实现类使用。

void add(int index,T e); // 将元素添加到index这个索引下面,如果index超过(集合元素个数+1)的范围就会报错
boolean addAll(int index, Collection<?> c); // 将c集合元素,冲index下标开始一个一个往后面添加
T get(int index); // 返回下标index对应元素
int indexOf(T e); // 返回e元素在集合第一次出现的下标,不存在元素就返回-1
int lastIndexOf(T e); // 返回集合中从后向前查询,元素e第一次出现的下标,不存在返回-1
T remove(int index); // 移除index下标的元素,并返回移除元素
T set(int index, T e); // 将index下标的元素设置为e,并返回之前的元素
List subList(int start, int end); // 截取集合,下标start到end-1的所有元素返回一个新List集合

// 在ArrayList中有一个特有的方法
public void trimToSize(); // 将这个ArrayList实例的容量裁减为列表的当前大小

ArrayListLinkedList、Vector的相同点和不同点?

  • 相同点:他们都是存放元素是有序、可重复的,都可以存放数据。
  • 不同点:
    1. ArrayList:线程不安全,对于查询、修改操作更加高效,底层使用Object[],扩容时扩为原来的1.5倍。
    2. LinkedList:线程不安全,对于增加、删除操作更加高效,底层使用双向链表。
    3. Vector:线程安全但是效率低,对于查询、修改操作更加高效,底层使用Object[],扩容时扩容长度是原来2倍。

1.5、Set接口

Set接口:继承至Collection接口,存放无序、不重复数据,主要实现类有HashSetLinkedHashSetTreeSet。Set中没有额外定义其它方法,它直接继承了Collection中所有方法。

无序性:
是指数据存放位置不连续并不是与加入元素的先后循序有关,而有序性是数据存放的位置是连续的,List和数组都是有序结构。在Set集合中,存放位置并不是按照底层数组下标进行添加,而是根据添加元素的hash值来确定添加位置。

不可重复性:集合中不能添加两个相同的元素。在Set中添加元素会按照元素重写的equals()判断元素是否相同,如果相同就不能添加,而且需要重写hashcode()方法,因为Set集合添加元素是通过元素的hash值来添加的。

Set添加元素:
HashSet为例,底层采用HashMap结构,元素作为keyvalue是一个finalObject对象。在HashSet中添加元素实质是采用HashMap中的put()方法。

  • 添加元素a,首先计算a的hashcode值,通过对应的算法找到元素应该存放的位置。
  • 如果对应位置上没有元素,直接添加元素。如果存在元素就比较两个元素的hashcode值是否相同(不同hashcode可能对应同一个位置)。
  • 如果hashcode不相同,说明元素不一样就采用链表方式,将新添加的元素链接到以存在元素后面。
  • 如果hashcode相同,在通过元素的equals()方法判断元素是否相同。
  • 如果equals()返回false,表示元素不相同,就是用链表链接到元素后面,如果返回true表示元素相同,则就不能添加成功。

向Set集合中添加的元素,其类应该重写hashcode()和equals()方法,来实现对象的相等性。并且应该保证hashcode()和equals()的一致性,相同对象应该有一个相同的hash值。

LinkedHashSet:继承hashSet类,也具有无序性、不可重复性。但是它可以以添加的顺序来遍历其中的元素。在LinkedHashSet中在添加数据中有两个引用分别作为前驱和后继,用来记录元素添加顺序,对于频繁遍历Set集合可以使用LinkedHashSet

TreeSet: 继承Set,也具有无序性、不可重复性。但是它只能存放一个类型的元素,如果存放不同类型元素直接报错。而且元素类必须实现Comparable接口,或者定义自己的排序规则,不然元素不能添加成功,这样在遍历元素的时候遍历出的元素是经过compareTo排序后的顺序元素。它底层是一个红黑树的的一个树形结构。

TreeSet自然排序:
直接在添加元素中实现Comparable接口,实现compareTo()方法,定义排序规则。

// Student定义的排序规则,先按名称排序,再按年龄排序
@Override
public int compareTo(Student o) {
    if (!this.name.equals(o.name)){
        return this.name.compareTo(o.name);
    }else{
        return this.age-o.age;
    }
}
public static void main(String[] args) {
    Set treeSet = new TreeSet<>();
    treeSet.add(new Student("张三21",19));
    treeSet.add(new Student("张三62",20));
    treeSet.add(new Student("张三43",22));
    treeSet.add(new Student("张三43",3));
	// 添加好元素后就自动排序完成了,遍历就可以得到有序的元素
    for (Object o : treeSet) {
        System.out.println(o);
    }
}

TreeSet定制排序:
在创建TreeSet对象时,传递一个Comparator接口的实现类,然后重写compare()方法,定制一个排序规则。

public static void main(String[] args) {
    Set<Student> treeSet = new TreeSet<>(new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            if (!Objects.equals(o1.getName(),o2.getName())){
                return o1.getName().compareTo(o2.getName());
            }
            return o1.getAge()-o2.getAge();
        }
    });
    treeSet.add(new Student("张三21",19));
    treeSet.add(new Student("张三62",20));
    treeSet.add(new Student("张三43",22));
    treeSet.add(new Student("张三43",3));

    for (Object o : treeSet) {
        System.out.println(o);
    }
}

注意:

  • 自然排序中,比较两个元素相同的标准就是compareTo()方法是否返回0,如果返回0,这两个元素就是相同的,在添加元素时也是根据这种比较方式来判断两个元素是否相同,如果相同就不能继续添加了。
  • 如果采用定制排序规则,就采用定制排序来判断两个元素是否相同。

经典面试题:

// User类重写了equals()和hashCode()方法
public class User {
    int id;
    String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id && Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}
public static void main(String[] args) {
    HashSet<User> set = new HashSet<>();
    User user1 = new User(1001, "张三");
    User user2 = new User(1002, "李四");
    set.add(user1);
    set.add(user2);
    System.out.println(set.size()); // 2  

    user1.name = "王五";  // 修改了use1的name,导致user1的hashcode发生改变,去移除user1时通过hashcode找到的是一个空位置
    set.remove(user1);
    System.out.println(set.size()); // 2

    set.add(new User(1001,"王五")); // 这个对象的hashcode值对应位置为空可以添加
    System.out.println(set.size()); // 3

    set.add(new User(1001,"张三")); // 虽然这个对象的hashcode值和第一次添加的user1相同,所以查询的位置相同,但是之前修改了user1的name,导致equals()返回false,可以通过链表链接到user1后面
    System.out.println(set.size()); // 4
}

2、Map集合

Map集合:双列数据,采用key-value对来存储数据,在它下面有HashtableHashMapSortedMapTreeMap等主要实现类。

image-20220518170432695

2.1、Map接口

key-value:key值是不能重复的并且是无序(存放位置不连续),value的值可以重复并且无序。但是在Map中添加数据会把key和value封装成一个Entry对象,key和value就是Entry对象的属性,然后在使用Set进行存储,也是无序不可重复的。

image-20220518171840170

注意:

  • 因为key是不重复的,所以在使用自定义类作为key时,这个类需要重写hashCode()、equals()方法来保证每一个key不一样。
  • value可以重复的,但是为了保证数据相等,自定义类也需要重写equals(),可以不重写hashCode()

Map接口中定义抽象方法:

Object put(Object key, Object value); // 向map中添加一个数据,如果map中没有存在这个数据就返回null,如果已经存在这个数据修改原来的数据,并返回原来的数据的value值
void putAll(Map<? extends K, ? extends V> m); // 将m中的key-value添加到map中,如果key重复就修改
Object remove(Object key); // 如果key在map的映射中,就将这个映射删除并返回映射对应的vlaue值,如果不存在就返回null
void clear(); // 清除map中的所有映射

Object get(Object key); // 获取以key的映射对应的value值,如果没有这个映射返回null
boolean containsKey(Object key); // 判断map中是否存在key对应的映射
boolean containsValue(Object value); // 判断map中是否有映射到value
int size(); // 返回有多少个映射
boolean isEmpty(); // 判断map是否存在映射
boolean equals(Object o); // 将指定对象与此映射比较是否相等。如果给定对象也是一个映射且两个映射表示相同的映射,则返回true。更正式地说,如果m1. entryset ().equals(m2. entryset()),两个映射m1和m2表示相同的映射。(key-value都必须对应相同)

Map中遍历操作:

public static void main(String[] args) {
    Map<String,String> map = new HashMap<>();
    map.put("BB","bb");
    map.put("CC","cc");
    map.put("DD","dd");
    map.put("EE","dd");
    map.put("FF","dd");

    // 获取map中所有key,返回是一个set
    Set<String> k1 = map.keySet();
    Iterator<String> iterator = k1.iterator();
    while (iterator.hasNext()){
        System.out.println(iterator.next());
    }
    System.out.println("=======================");

    // 获取map中所有value,返回是一个List
    Collection<String> values = map.values();
    Iterator<String> iterator1 = values.iterator();
    while (iterator1.hasNext()){
        System.out.println(iterator1.next());
    }
    System.out.println("=======================");

    // 获取map中所有的Entry对象,返回是一个Set
    Set<Map.Entry<String, String>> entrySet = map.entrySet();
    Iterator<Map.Entry<String, String>> iterator2 = entrySet.iterator();
    while (iterator2.hasNext()){
        Map.Entry<String, String> entry = iterator2.next(); // entry对象
        System.out.print(entry.getKey()+"-->"); // entry的key
        System.out.println(entry.getValue()); // entry的value
    }
}

2.2、HashMap实现类

Map中的put()方法执行步骤:(以HashMap为例,JDK7.0之前)

  • 首先计算key的hash值,确定在Map中存放的位置,并判断位置上是否已经存在数据。
    • 如果不存在数据就直接封装成Entry对象添加到Map中。
    • 如果存在数据(一个或多个数据,但是每一个数据都要判断),就比较key的hash值是否相等。(不同hash值可能对应相同存储位置)
      • 如果key的hash值不同就通过链表的方式链接到当前元素的前面。
      • 如果key的hash值相同,再判断key的equals()方法返回值。
        • 如果equals()返回false,将数据通过链表链接到当前元素的后面。
        • 如果equals()返回true,表示key相同,直接使用value值去替换就的value值,并返回旧的value值。

所以,自定义类的key值一定要重写hashCode()和equals()方法,并且只有最后一种情况会返回一个旧的value值,前面的情况都是返回null。在不断的添加过程中,当容量不够时也是需要扩容的,一般是扩大到原来容量的两倍,并将原来的数据拷贝。

JDK8之后HashMap变化:

  • JDK7之前,key-value封装成一个Entry对象。在JDK8之后,key-value封装成的是一个Node对象,Node对象也是实现了Entry接口,所以在JDK8之后底层Map创建的是Node数组来存放数据。
  • 7之前实例一个map对象,它是直接创建一个长度为16的Entry数组,8之后实例一个对象而是直接创建一个 [ ] 空数组,只有在执行第一次put()方法时才会创建一个长度为16的Node数组。
  • 7的底层使用的是 数组+链表,8的底层使用的是 数组+链表+红黑树
  • 在8中,如果某一个位置上的元素以链表形式存放的数据超过8个(>8)并且数组长度超过64(>64),此时当前位置上的数据就采用红黑树的形式来存放(这样提高了元素的遍历效率)。

JDK8HashMap源码分析:

// 无参构造器,7之前是直接创建一个容量为16的Entry数组,8之后没有直接初始化,直接是一个null,它们的加载因子都是0.75
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // 0.75
}

// 有参构造器,初始容量和初始加载因子
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor; // 加载因子
    this.threshold = tableSizeFor(initialCapacity); // 根据初始化容量计算临界值(当数组元素个数超过临界值是就开始扩容)
}

// put()方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true); // hash()计算key的hash值
}

// 添加元素
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table是map中的数组,如果是第一次添加,需要初始化一个Node数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    // 添加数据
    if ((p = tab[i = (n - 1) & hash]) == null) // 找到hash值对应位置的数据为空时
        tab[i] = newNode(hash, key, value, null); // 直接添加数据
    else { 									// 添加位置上以存在数据
        Node<K,V> e; K k;
        // 如果位置上的元素与添加元素相同(key的hash值相同并且equals()返回true)
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            e = p; // 将位置上的数据给e
        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) // 判断链表存放数据个数是否大于等于7,TREEIFY_THRESHOLD:8
                        treeifyBin(tab, hash); // 就将链表中的元素改写成红黑树
                    break;
                }
                // 如果链表中存在一个元素与添加元素相同
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e; // 下一个元素
            }
        }
        if (e != null) { // e不为null就表示当前位置上存在与添加元素相同的元素
            V oldValue = e.value; 
            if (!onlyIfAbsent || oldValue == null)
                e.value = value; // 修改相同元素的value值
            afterNodeAccess(e);
            return oldValue; // 返回当前位置就的value值
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

// 初始化数组或者是扩容
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 当前数组容量
    int oldThr = threshold; // 临界值,第一次添加为0
    
    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)
            // 容量扩大2倍,DEFAULT_INITIAL_CAPACITY值为16
            newThr = oldThr << 1; // 临界值扩大2倍
    }else if (oldThr > 0) // 第一次添加元素,并且初始map是指定了加载因子,将加载因子算出的临界值作为容量
        newCap = oldThr;
    else {               // 第一次添加元素,并且使用无参构造器,使用默认容量和加载因子
        newCap = DEFAULT_INITIAL_CAPACITY; // 默认容量16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // DEFAULT_LOAD_FACTOR默认加载因子0.75,临界值算出为12
    }
    
    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]; // 第一次添加初始化数组,如果不是第一次天添加就创建一个新长度的数组
    table = newTab;
    // 不是第一次添加,将原来的数据拷贝到新的数组中
    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 order
                    Node<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;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.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;
                    }
                }
            }
        }
    }
    return newTab; // 返回一个初始化或者新扩容后的数组
}

HashMap源码常量:

  • DEFAULT_INITIAL_CAPACITY:第一次初始化时默认常量:16
  • DEFAULT_LOAD_FACTOR:创建map的默认加载因子:0.75
  • TREEIFY_THRESHOLD:链表数据多余或等于这个值,就会将链表数据装换成红黑树:8
  • MAXIMUM_CAPACITY:链表的最大容量,总是2n,初始为16每次扩容都扩大2倍:230

临界值的作用:
由加载因子计算得到的临界值(初始默认为12),容量*加载因子。当数组的元素个数超过临界值,数组容量就会扩容。

为什么超过临界值就需要扩容而不是超过容量就扩容呢?

​ 因为数组可能永远无法存满,全部以链表的形式存放在某个位置下,这样在遍历元素时大多数元素会通过链表去查询这样查询效率就会降低。如果超过临界值就扩容,扩容后每一个元素都会重新有一个存放位置就不会存在大面积的链表数据。加载因子越小,它的临界值就会越小,出现大面积链表的机会就会越小,但是数组的利用率就会降低,所以一般默认使用0.75来作为加载因子。

2.3、LinkedHashMap实现类

LinkedHashMap:它继承至HashMap,但是它在遍历的时候后可以按照数据添加的顺序遍历出来。

// 本质上它的底层原理还是使用的HashMap,就是在创建Node对象时重写了newNode()方法,并且创建在内部创建了Entyr内部类并且继承至HashMap中的Node内部类
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p; // 返回的是一个自己内部的创建的Node对象
}

// Node对象,继承HashMap得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);
    }
}

2.4、TreeMap实现类

TreeMap实现类: 继承至Map接口,与HashMap不同的是,添加元素时可以按照key进行排序。但是key值必须是同一种类型并且实现了Comparable接口或者自定义了一个Comparator实现类。

TreeMap treeMap = new TreeMap();

treeMap.put(234,234);
treeMap.put(23,23);
treeMap.put(13,13);
treeMap.put(12,11);
treeMap.put(45,45);

System.out.println(treeMap); // {12=11, 13=13, 23=23, 45=45, 234=234}

HashMapLinkedHashMapTreeMapHashtable之间的区别?

  • 相同点:都是key-value键值对形式存放,都是无序不重复的。
  • 不同点:
    1. HashMap:线程不安全效率高,可以存储null为key或value的数据。
    2. LinkedHashMap: 线程不安全效率高,可以存储null为key或value的数据,但是可以按照元素添加顺序来遍历map。
    3. TreeMap:线程不安全效率高,可以存储null为key或value的数据,添加元素时可以按照key类定义的排序规则进行排序。
    4. Hashtable:作为一个古老的map,它是线程安全的效率低,不能存放null为key或value的数据。

2.5、Properties实现类

Properties实现类:继承至Hashtable类,它用于处理.properties这类配置文件。

  1. 创建一个test.properties配置文件。
name=张三
age=18
# 等号两边不能有空格
  1. 使用Properties类读取配置文件。
public static void main(String[] args) throws IOException {

    Properties properties = new Properties();
    properties.load(new FileInputStream("test.properties"));
    String name = properties.getProperty("name");  // name就是key值
    String age = properties.getProperty("age");

    System.out.println("name = "+name+",age = "+age);
}

注意:
当读取的配置文件中存在乱码问题,需要修改idea配置,将properties的编码设置为UTF-8,然后删除文件重新创建文件。

image-20220519180852320

3、Collections工具类

Collections:操作集合的工具类,可以操作List、Map。类似于Object和Objects之间的关系。

public static void reverse(List<?> list); // 将list中的元素进行反转
public static void shuffle(List<?> list); // 将list中的元素随机重新排列
public static <T extends Comparable<? super T>> void sort(List<T> list); // 将按照元素实现的comparaTo进行自然排序
public static <T> void sort(List<T> list, Comparator<? super T> c); // 自定义一个Comparator接口实现类来进行排序
public static void swap(List<?> list, int i, int j); // 交换list中下标为i和j的元素
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll); // 返回list按照自然排序中的最大值
public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp); // 返回list按照comp自定义排序中的最大值
public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll); // 最小值
public static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp); // 最小值
public static int frequency(Collection<?> c, Object o); // 在集合中元素o出现的次数,比较方式是equals()方法


public static <T> void copy(List<? super T> dest, List<? extends T> src); // 将scr中的元素拷贝到dest这个list中
// 使用copy()方法注意事项
public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("Hello");
    list.add("你好");
    list.add("world");

  // 当使用copy()方法时,dest的size()的值必须大于或者等于src中的size(),即在dest中存在元素的个数必须等于或多于src中的个数
    // 否则会报错IndexOutOfBoundsException: Source does not fit in dest
    // 如果在dest的元素个数多于src中个数,多的元素不会被修改

    // 通常拷贝一个新的List集合编写方法
    List<String> list2 = Arrays.asList(new String[list.size()]);
    Collections.copy(list2,list);
    System.out.println(list2);
}

Collections工具类中提供了多个synchronizedXXX()的方法,该方法可以使参数集合变成一个线程同步的新集合,从而解决集合中的线程安全问题。

image-20220519200133108

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值