Java集合的使用

集合的使用

集合框架的概述

集合、数组都是对多个数据进行存储操作的结构,简称 Java 容器。

此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(比如 .txt、.jpg、数据库等)

数组在存储多个数据的特点

  • 一旦初始化之后长度就是确定的
  • 定义数组的时候需要指明数组的元素类型(声明是什么类型就必须存储什么类型的数据)

比如:String[] arr; int[] arri;

数组在存储多个数据的缺点

  1. 长度一旦确定就不能修改了
  2. 只能存储单个类型的数据(有利(更加严格管控数据)有弊(有多个类型数据时不方便),根据需求而定)
  3. 数组中提供的方法比较有限,对于添加、删除、插入数据等操作,非常不方便,效率也比较低
  4. 获取数组中实际元素的个数的需求(比如:一个长度为10的数组中有几个可用元素),数组没有现成的属性或方法可以使用
  5. 数组存储数据的特点:有序、可重复;对于无序且不能重复的需求,数组是不能实现的

Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组

Java 集合可分为 Collection 和 Map 两种体系

Collection接口

单列数据,单列集合,用于存储一个一个的对象

  • List:存储有序、可重复的数据(常用:ArrayList、LinkedList、Vector)

  • Set:存储无序、不可重复的数据(常用:HashSet、LinkedHashSet、ThreeSet)

在这里插入图片描述

Map接口

双列集合,用于存储一对(key-value)一对(key-value)的数据

常用:HashMap、LinkedHashMap、ThreeMap、Hashtable、Properties

在这里插入图片描述

Collection 接口的使用

说明

  • Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
  • JDK 不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set 和 List)实现。
  • 在 JDK5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。

常用方法

方法作用
add(Object obj)添加指定元素到指定集合中(使用了自动装箱)
addAll(Collection coll)将指定集合中的元素添加到当前集合中
int size()获取指定集合中元素的个数
void clear()清空指定集合中所有元素
boolean isEmpty()判断当前集合是否为空
boolean contains(Object obj)判断当前集合中是否包含某个元素
boolean containsAll(Collection coll)判断形参 coll 中的所有元素是否都存在于当前集合中
boolean remove(Object obj)从当前集合中删除 obj 元素,成功返回 true,失败返回 false
boolean removeAll(Collection coll)从当前集合中移除 coll 中所有的元素
boolean retainAll(Collection coll)交集,获取当前集合和 coll 集合的交集,并返回给当前集合
boolean equals(Object obj)要想返回 true,需要当前集合和形参集合的元素都相同
Object[] toArray()将集合转成对象数组
hashCode()获取指定集合对象的哈希值
iterator()返回 Iterator 接口的实例,用于遍历集合元素
  • 在调用 contains、containsAll、remove、removeAll 的时候都是去调用 equals 方法来找到对应的值的

  • 数组 转 集合:Arrays.asList();

  • 集合 转 数组:toArray()

案例一

package com.laoyang.test.day1;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;

/**
 * @ClassName CollectionTest
 * @Description: 集合框架的使用
 * @Author Laoyang
 * @Date 2021/9/28 15:42
 */
public class CollectionTest {
    /**
     * Collection 接口中的方法使用
     */
    @Test
    public void testOne() {
        Collection collection = new ArrayList();
        // add(Object e):将指定的元素添加到对应的集合中(自动装箱)
        collection.add("小白");
        collection.add(123);

        // addAll(Collection coll):将指定集合中的元素添加到当前集合中(在部分需求中可用于整合多个集合)
        Collection coll = new ArrayList();
        coll.add("AAa");
        coll.addAll(collection);
        //Collection collection1 = new ArrayList();
        //collection1.addAll(coll);     // 小白   123   AAa

        // size():获取指定集合中元素的个数
        System.out.println(collection.size());  // 2

        // isEmpty():判断当前集合是否为空
        System.out.println(coll.isEmpty());     // false

        // clear():清空指定集合中所有元素
        System.out.println(collection);  // [小白, 123]
        collection.clear();
        System.out.println(collection);  // []
    }
}

案例二

注意:向 Collection 接口的实现类的对象中添加 obj 数据时,要求 obj 所在类要重写 equals 方法

package com.laoyang.test.day1;

import org.junit.Test;
import java.util.*;

/**
 * @ClassName CollectionMethod
 * @Description: Collection 常用方法
 * @Author Laoyang
 * @Date 2021/9/30 10:15
 */
public class CollectionMethod {
    @Test
    public void testOne() {
        Collection collection = new ArrayList();
        collection.add("AAA");
        collection.add(123);
        collection.add("ABC");
        collection.add(new String("QQ"));
        Person person = new Person();
        collection.add(person);
        collection.add(new Person("Laoyang", 20));

        // contains(Object obj):判断当前集合中是否包含某个元素
        System.out.println(collection.contains("ABC"));     // true
        System.out.println(collection.contains("CCC"));     // false
        /*
        注意
        String 内部重写了 equals 方法,所以在使用 contains 方法的时候会自动调用重写后的 equals 方法进行判断,因此两个相同值的类比较起来就是 true
        而部分类中是没有重写 equals 方法的,就会默认调用 Object 中的 equals 方法,而 Object 类中的 equals 方法是使用 == 号进行判断的(也就是判断地址值的),因此两个相同值的类比较起来就是 false
        Ps:如果想要让两个相同值的类为 true,则需要手动重写 equals
         */
        System.out.println(collection.contains(new String("QQ")));     // true
        System.out.println(collection.contains(person));     // true
        System.out.println(collection.contains(new Person("Laoyang", 20)));     // false(重写equals方法后为 true)

        System.out.println("----------------------------------");

        // containsAll(Collection coll):判断形参 coll 中的所有元素是否都存在于当前集合中
        Collection coll = Arrays.asList("QQ", 123, "嘿嘿嘿");  // 只要有一个不一样的就会返回 false
        System.out.println(coll.containsAll(collection));   // false
    }

    @Test
    public void testTwo() {
        Collection collection = new ArrayList();
        collection.add("AAA");
        collection.add(123);
        collection.add("ABC");
        collection.add(new String("QQ"));
        collection.add(new Person("Laoyang", 20));

        // remove(Object obj):从当前集合中删除 obj 元素,成功返回 true,失败返回 false
        System.out.println(collection);  // [AAA, 123, ABC, QQ, Person{name='Laoyang', age=20}]
        System.out.println(collection.remove("AAA"));   // true
        System.out.println(collection);  // [123, ABC, QQ, Person{name='Laoyang', age=20}]

        System.out.println("----------------------------------");

        // removeAll(Collection coll):从当前集合中移除 coll 中所有的元素
        Collection coll = Arrays.asList(123, "QQ", "WW");
        System.out.println(collection.removeAll(coll)); // true
        System.out.println(coll);       // [123, QQ, WW]
        System.out.println(collection);     // [ABC, Person{name='Laoyang', age=20}]
    }

    @Test
    public void testThree() {
        Collection collection = new ArrayList();
        collection.add("AAA");
        collection.add(123);
        collection.add("ABC");
        collection.add(new String("QQ"));
        collection.add(new Person("Laoyang", 20));

        // retainAll(Collection coll):交集,获取当前集合和 coll 集合的交集,并返回给当前集合
        Collection coll = Arrays.asList(123, "QQ", "WW");
        System.out.println(collection.retainAll(coll)); // true
        System.out.println(collection);     // [123, QQ]

        System.out.println("----------------------------------");

        // equals(Object obj):要想返回 true,需要当前集合和形参集合的元素都相同
        Collection collA = new ArrayList();
        collA.add("AAA");
        collA.add(123);

        Collection collB = new ArrayList();
        collB.add("AAA");
        collB.add(123);

        Collection collC = new ArrayList();
        collC.add(123);
        collC.add("AAA");

        System.out.println(collA.equals(collB)); // true
        System.out.println(collA.equals(collC)); // false(因为 List 存储的是有序且可重复的数据,如果值一样顺序不一样也会返回 false)
    }

    @Test
    public void testFour() {
        Collection collection = new ArrayList();
        collection.add("AAA");
        collection.add(123);
        collection.add("ABC");
        collection.add(new String("QQ"));
        collection.add(new Person("Laoyang", 20));

        // hashCode():返回当前对象的哈希值
        System.out.println(collection.hashCode());

        /*
        集合 转 数组:toArray()
        数组 转 集合:Arrays.asList()
         */
        Object[] objects = collection.toArray();
        System.out.println(Arrays.toString(objects));   // [AAA, 123, ABC, QQ, Person{name='Laoyang', age=20}]

        List<Object> list = Arrays.asList(objects);
        System.out.println(list);   // [AAA, 123, ABC, QQ, Person{name='Laoyang', age=20}]

        /*
         注意:在使用基本数据类型的数组时,List 只会识别出一个元素!!!
         如果有需要的话,可以使用包装类的方式进行传输
         */
        List<int[]> ints = Arrays.asList(new int[]{1, 2, 3, 4});
        System.out.println(ints.size());    // 1
        System.out.println(ints);   // [[I@22927a81]

        List<char[]> chars = Arrays.asList(new char[]{'q', 'a'});
        System.out.println(chars);  // [[C@78e03bb5]

        // 包装类形参的方式
        List<Integer> integers = Arrays.asList(new Integer[]{1, 2, 3, 4});
        System.out.println(integers.size());    // 4
        System.out.println(integers);   // [1, 2, 3, 4]

        List<Double> doubles = Arrays.asList(new Double[]{1.2, 2.3, 3.4});
        System.out.println(doubles.size());    // 3
        System.out.println(doubles);    // [1.2, 2.3, 3.4]
    }
}

class Person {
    private String name;
    private int age;

    /**
     * 重写 equals 方法
     */
    @Override
    public boolean equals(Object o) {
        System.out.println("进入Person类中的equals方法");
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    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 Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }
}

注意

  • String 内部重写了 equals 方法,所以在使用 contains 方法的时候会自动调用重写后的 equals 方法进行判断,因此两个相同值的类比较起来就是 true。

  • 而部分类中是没有重写 equals 方法的,就会默认调用 Object 中的 equals 方法,而 Object 类中的 equals 方法是使用 == 号进行判断的(也就是判断地址值的),因此两个相同值的类比较起来就是 false。

    如果想要让两个相同值的类为 true,则需要手动重写 equals

  • 在使用基本数据类型的数组时,List 只会识别出一个元素!!!如果有需要的话,可以使用包装类的方式进行传输。

Iterator 迭代器

说明

  • Iterator 对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。

  • GOF 给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生

    类似于 “公交车上的售票员”、“火车上的乘务员”、“空姐”。

  • 集合对象每次调用 iterator() 方法都会得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。

Iterator 内部定义的方法

  • hasNext():判断是否还有下一个元素
  • next():指针下移,并且将下移以后集合位置上的元素返回
  • remove():在遍历的时候,删除集合中指定的元素,此方法不同于集合直接调用 remove()

在这里插入图片描述

案例

这里简单演示三种遍历方式

package com.laoyang.test.day1;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

/**
 * @ClassName IteratorTest
 * @Description: Ierator 迭代器的使用
 * @Author Laoyang
 * @Date 2021/9/30 11:18
 */
public class IteratorTest {
    @Test
    public void testOne() {
        Collection collection = new ArrayList();
        collection.add("AAA");
        collection.add(123);
        collection.add(new String("QQ"));
        collection.add(new Person("Laoyang", 20));

        Iterator iterator = collection.iterator();
        // 方式一
//        System.out.println(iterator.next());  // AAA
//        System.out.println(iterator.next());  // 123
//        System.out.println(iterator.next());  // QQ
//        System.out.println(iterator.next());  // Person{name='Laoyang', age=20}
//        System.out.println(iterator.next());  // 报错:NoSuchElementException

        // 方式二:不推荐
//        for (int i = 0; i < collection.size(); i++) {
//            System.out.println(iterator.next());
//        }

        // 方式三:推荐(hasNext():判断是否还有下一个元素)
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

Iterator 执行原理

  1. 创建 Iterator 对象的时候会生成一个指针
  2. hashNext 方法判断是否还有下一个元素
  3. 如果 hashNext 返回 true,那么指针会向下移动,指向集合中的第一个元素,如果为 false,则直接结束…以此类推

Iterator 遍历集合的两种错误写法

错误一:直接使用 next() 进行判断,导致跳着遍历

错误二:使用 Iterator 匿名对象,导致死循环

package com.laoyang.test.day1;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

/**
 * @ClassName IteratorTest
 * @Description: Ierator 迭代器的使用
 * @Author Laoyang
 * @Date 2021/9/30 11:18
 */
public class IteratorTest {
    /**
     * Iterator 遍历集合的两种错误写法
     */
    @Test
    public void testTwo() {
        Collection collection = new ArrayList();
        collection.add("AAA");
        collection.add(123);
        collection.add(new String("QQ"));
        collection.add(new Person("Laoyang", 20));

        /*
         错误一:
         第一次执行的时候获取的是 AAA,然后用 AAA 进行判断
         判断成功后,返回下一个指针所指向的值 123
         最后的效果就会是跳着输出集合元素,然后跳到最后一个的时候,没有下一个了,就会报:NoSuchElementException
         */
//        Iterator iterator = collection.iterator();
//        while ((iterator.next()) != null) {        // 第一次执行的时候获取的是 AAA,然后用 AAA 进行判断
//            System.out.println(iterator.next());   // 判断成功后,返回下一个指针所指向的值 123
//        }

        /*
        错误二:使用 Iterator 匿名对象 - 导致死循环
         */
        while (collection.iterator().hasNext()) {
            System.out.println(collection.iterator().next());
        }
    }
}

remove 方法的使用

注意

  • Iterator 可以删除集合的元素,但是是遍历过程中通过迭代器对象的 remove 方法,不是集合对象的 remove 方法。

  • 如果还未调用 next() 的时候调用 remove() 就会报 IllegalStateException

  • 在上一次调用 next 方法之后调用了两次 remove 方法也会报 IllegalStateException。

案例
package com.laoyang.test.day1;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

/**
 * @ClassName IteratorTest
 * @Description: Ierator 迭代器的使用
 * @Author Laoyang
 * @Date 2021/9/30 11:18
 */
public class IteratorTest {
    /**
     * remove 方法的使用
     * 情况一:如果还未调用 next() 的时候调用 remove() 就会报 IllegalStateException
     * 情况二:在上一次调用 next 方法之后调用了两次 remove 方法也会报 IllegalStateException。
     */
    @Test
    public void testTherr() {
        Collection collection = new ArrayList();
        collection.add("AAA");
        collection.add(123);
        collection.add(new String("QQ"));
        collection.add(new Person("Laoyang", 20));

        // 删除集合中指定的元素
        Iterator iterator = collection.iterator();
        while (iterator.hasNext()) {
            // 情况一:不能再 hasNext() 方法执行后马上执行 remove 方法,因为这个时候指针还未指到任何一个值
            //iterator.remove();    // 报错:IllegalStateException

            Object next = iterator.next();
            if ("QQ".equals(next)) {
                iterator.remove();
                // 情况二:不能在 next() 方法后连续调用两次 remove(),但是可以在外面调用,比如可以在当前 while 结构外调用 remove()
                //iterator.remove();  // 报错:IllegalStateException
            }
        }
        // 遍历集合,因为上面已经遍历完了,所以我们这里需要在重新赋一下值,然后在进行遍历
        iterator = collection.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

foreach 方式遍历集合元素

说明

  • foreach 是 JDK5 新增的 for 循环新特性,用于遍历集合、数组。
  • 遍历操作不需获取 Collection 或数组的长度,无需使用索引访问元素。
  • 遍历集合的底层调用 Iterator 完成操作。

案例

package com.laoyang.test.day1;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;

/**
 * @ClassName ForTest
 * @Description: JDK5 新增的 for 循环新特性
 * @Author Laoyang
 * @Date 2021/9/30 15:52
 */
public class ForTest {
    /**
     * 遍历集合:for(集合中的元素类型 局部变量 : 集合对象) { }
     */
    @Test
    public void testOne() {
        Collection collection = new ArrayList();
        collection.add("AAA");
        collection.add(123);
        collection.add(new String("QQ"));
        collection.add(new Person("Laoyang", 20));

        // 这种遍历方式内部仍然调用了迭代器
        for (Object o : collection) {
            System.out.println(o);
        }
    }

    /**
     * 遍历数组:for(数组的元素类型 局部变量 : 数组对象) { }
     */
    @Test
    public void testTwo() {
        int[] arr = new int[]{1, 5, 3, 2};
        for (int i : arr) {
            System.out.println(i);
        }
    }
}

小练习

使用两种不同的 for 循环赋值方式,最后打印的结果分别是什么?

package com.laoyang.test.day1;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;

/**
 * @ClassName ForTest
 * @Description: JDK5 新增的 for 循环新特性
 * @Author Laoyang
 * @Date 2021/9/30 15:52
 */
public class ForTest {
    /**
     * 小练习:最后打印的结果是 WW 还是 MM?
     */
    @Test
    public void testThree() {
        String[] strs = new String[]{"WW", "WW", "WW"};
        /*
        方式一:普通 for 赋值
        这种赋值操作是把 strs 数组中指定下标位置的元素替换成新的值,所以最后遍历的结果就是替换后的元素
         */
//        for (int i = 0; i < strs.length; i++) {
//            strs[i] = "MM";
//        }

        /*
         方式二:增强 for 赋值
         这种赋值操作并不会保存在 strs 中,因为它是先从 strs 中获取到每一个值,然后依次赋值给 str 进行使用
         但是 str 是一个新的局部变量,它的值并不会影响到 strs 中的元素,所以最后打印的结果依然会是 WW
         */
        for (String str : strs) {
            str = "MM";
        }

        for (String str : strs) {
            System.out.println(str);
        }
    }
}

大家可以打开/关闭注释的代码进行测试

List 的使用

说明

  1. 对于 Java 中数组用来存储数据的局限性,我们通常使用 List 替代数组。
  2. List 集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
  3. List 容器中的元素都对应一个整数型的序号记载在容器中的位置,可以根据序号存取容器中的元素。
  4. JDK API 中 List 接口的实现类常用的有:ArrayList、LinkedList 和 Vector。

常见面试题ArrayList、LinkedList 和 Vector 的相同点与不同点?

相同点:三个类都实现了 List 接口,存储数据的特点相同(都是有序且可重复的数据)

不同点

ArrayList:作为 List 接口的主要实现类(JDK1.2出现);执行效率高(线程不安全的);底层使用 Object[] elementData 进行存储;

LinkedList:JDK1.2出现;对于频繁的插入、删除操作使用此类效率比 ArrayList 要高;底层使用双向链表存储;

Vector:作为 List 接口的古老实现类(JDK1.0出现);执行效率低(线程安全的);底层使用 Object[] elementData 进行存储;

List 接口也是 JDK1.2 的时候出现的

ArrayList 源码分析(JDK7 & JDK8)

JDK7 中的 ArrayList

ArrayList list = new ArrayList();   // 底层创建了长度为 10 的数组
list.add(111);   // elementData[0] = new Integer(111);
   ...
list.add(999);   // 如果此次的添加操作导致底层 elementData 数组容量不够,则会进行扩容
> 默认情况下,会扩容为原来容量的 1.5 倍,同时需要将原有数组中的数据赋值到新的数组中!
> 建议在实际开发中,使用带参的构造器:ArrayList list = new ArrayList(int initialCapacity);
部分源码
package java.util;

import java.util.*;

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
	// ArrayList 的元素存储在其中的数组缓冲区。ArrayList 的容量就是这个数组缓冲区的长度
	private transient Object[] elementData;
	
	// ArrayList 的大小(它包含的元素数)。
	private int size;
	
	/**
	 * 要分配的数组的最大大小。 一些 VM 在数组中保留一些头字。
	 * 尝试分配更大的数组可能会导致 OutOfMemoryError:请求的数组大小超出 VM 限
	 */
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

	// 空参构造器,自动设置 elementData 原始大小
	public ArrayList() {
        this(10);
    }
	
	// 带参构造器,手动设置 elementData 原始大小
	public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Cap" +
                    "acity: "+ initialCapacity);
        this.elementData = new Object[initialCapacity];
    }
	
	// 返回当前元素总数
	public int size() {
        return size;
    }
	
	// 添加元素
	public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
	
	// 确保内容能够正常放入当前集合中,如果当前添加的内容超过了集合的存储长度,则会使用底层的扩容机制
	private void ensureCapacityInternal(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
	
	// 增加容量以确保它至少可以容纳由最小容量参数指定的元素数量
	private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 默认扩容成原来长度的 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            // 如果还是不够,则直接把当前值的长度当做集合的长度
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
			// 如果还是不够,那么就会将int最大值作为长度
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
		// 将当前的 elementData 数组里面的所有元素复制到新的数组中
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
	
	private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
}

中文注释是我自己加的,可结合理解,也可以忽略

JDK8 中的 ArrayList

ArrayList list = new ArrayList();   // 底层 Object[] elementData 初始化为 {},并没有创建长度为 10 的数组
list.add(111);     // 第一次调用 add() 时,底层才创建了长度为 10 的数组,并将数据 111 添加到 elementDate[0] 中
  ...
list.add(999);  // 如果此次的添加操作导致底层 elementData 数组容量不够,则会进行扩容
... 扩容操作与 JDK7 没什么区别
部分源码
package java.util;

import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
	// 用于默认大小的空实例的共享空数组实例。我们将其与 EMPTY_ELEMENTDATA 区分开来,以了解添加第一个元素时要膨胀多少
	private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	
	// ArrayList 的大小(它包含的元素数)。
    private int size;
	
	// 要分配的数组的最大大小。一些 VM 在数组中保留一些头字。尝试分配更大的数组可能会导致 OutOfMemoryError:请求的数组大小超出 VM 限
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	
	// 构造一个初始容量为 10 的空列表。
	public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
	
	// 将指定的元素附加到此列表的末尾
	public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
	
	// 确保内部容量
	private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

	// 确保显式容量
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
	
	// 增加容量以确保它至少可以容纳由最小容量参数指定的元素数量。
	private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        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);
    }

	// 巨大的容量
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
}

中文注释是我自己加的,可结合理解,也可以忽略

小结

JDK7 中的 ArrayList 的创建类似于单例模式中的饿汉式,JDK8 中的 ArrayList 的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。

LinkedList 源码解析

LinkedList list = new LinkedList();  // 内部声明了 Node 类型的 first 和 last 属性,默认值为 null
list.add(111);     // 将 111 封装到 Node 中,创建了 Node 对象

其中 Node(体现了 LinkedList 的双向链表的说法)定义为:
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

部分源码

package java.util;

import java.util.function.Consumer;

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    // 第一个元素
    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;
        }
    }

    // 添加方法
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    // 链接 e 作为最后一个元素
    void linkLast(E e) {
        // 首次执行的last为null,有元素之后则为最后一个元素
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        // 如果last为空,则把当前要添加的元素当做是一个last元素和first元素(因为第一次只有一个元素新增进来,所以它既是第一个也是最后一个)
        if (l == null)
            first = newNode;
        else
            // 如果last不为空,则把新的元素添加在last元素的下一个元素位置上
            l.next = newNode;
        size++;
        modCount++;
    }
}

中文注释是我自己加的,可结合理解,也可以忽略

Vector 源码解析

因为 Vector 是很久之前的东西了,现在基本不会用到这个实现类,所以这里就简单说明一下

  • JDK7 和 JDK8 中通过 Vector() 构造器创建对象时,底层创建了长度为 10 的数组

  • 在扩容方面,默认扩容为原来数组的 2 倍

  • Vector 是线程安全的

部分源码

package java.util;

import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
	// 存储向量组件的数组缓冲区。向量的容量是这个数组缓冲区的长度,并且至少足够包含向量的所有元素。Vector 中最后一个元素之后的任何数组元素都为空
	protected Object[] elementData;
	
	// 此Vector对象中的有效组件数
	protected int elementCount;
	
	// 当向量的大小变得大于其容量时,向量的容量自动增加的量。如果容量增量小于或等于零,则每次需要增长时,向量的容量都会增加一倍
	protected int capacityIncrement;
	
	// 构造一个空向量,使其内部数据数组的大小为10 ,其标准容量增量为零。
	public Vector() {
        this(10);
    }
	
	// 将指定的元素附加到此 Vector 的末尾。
	public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
	
	// 这实现了 ensureCapacity 的非同步语义。此类中的同步方法可以在内部调用此方法以确保容量,而不会产生额外同步的成本。
	private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
	
	// 要分配的数组的最大大小。一些 VM 在数组中保留一些头字。尝试分配更大的数组可能会导致 OutOfMemoryError:请求的数组大小超出 VM 限制
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	
	private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int 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);
    }
	
	private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
}

中文注释是我自己加的,可结合理解,也可以忽略

常用方法

方法作用
void add(int index, Object ele)在index位置插入ele元素
boolean addAll(int index, Collection eles)从index位置开始将eles中的所有元素添加进来
Object get(int index)获取指定index位置的元素
int indexOf(Object obj)返回obj在集合中首次出现的位置
int lastIndexOf(Object obj)返回obj在当前集合中末次出现的位置
Object remove(int index)移除指定index位置的元素,并返回此元素
Object set(int index, Object ele)设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex)返回从fromIndex到toIndex位置的子集合

还有部分方法这里就不列出来了,大家可以看后面案例中的使用

案例

package com.laoyang.test.day2;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * @ClassName ListMethod
 * @Description: List 接口常用方法
 * @Author Laoyang
 * @Date 2021/10/2 9:41
 */
public class ListMethod {
    @Test
    public void testOne() {
        ArrayList list = new ArrayList();
        // add(Object obj)
        list.add("Laoyang");
        list.add(20);
        list.add(new Person("Xiaobai", 18));
        list.add(20);

        System.out.println(list);   // [Laoyang, 20, Person{name='Xiaobai', age=18}, 20]

        System.out.println("-----------------------------");

        // add(int index, Object ele):在index位置插入ele元素
        list.add(2, "QQ");
        System.out.println(list);   // [Laoyang, 20, QQ, Person{name='Xiaobai', age=18}, 20]

        System.out.println("-----------------------------");

        // addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
        List strings = Arrays.asList("WW", "EE", "RR");
        list.addAll(4, strings);
        // 如果是使用 add 的方式传入集合的话,list 会默认将它作为一个整体然后添加到对应的list中
        //list.add(strings);
        System.out.println(list);  // [Laoyang, 20, QQ, Person{name='Xiaobai', age=18}, WW, EE, RR, 20]
        System.out.println(list.size());    // 使用 addAll() 返回 9,使用 add() 返回 6

        System.out.println("-----------------------------");

        // get(int index):获取指定index位置的元素
        Object o = list.get(3);
        System.out.println(o);  // Person{name='Xiaobai', age=18}

        System.out.println("-----------------------------");

        // indexOf(Object obj):返回obj在集合中首次出现的位置,找不到对应值时返回 -1
        System.out.println(list.indexOf(20));     // 1
        System.out.println(list.indexOf(200));    // -1,找不到对应值时返回 -1

        System.out.println("-----------------------------");

        // lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置,找不到对应值时返回 -1
        System.out.println(list.lastIndexOf(20));   // 7
        System.out.println(list.lastIndexOf(205));   // -1,找不到对应值时返回 -1

        System.out.println("-----------------------------");

        /*
         remove(int index):移除指定index位置的元素,并返回此元素
         remove(Object o):移除对应数组中指定的元素(注意:这种方式不会返回要移除的元素)
         */
        Object remove = list.remove(3);// 根据索引删除
        System.out.println(remove);     // Person{name='Xiaobai', age=18}

        list.remove("EE");   // 根据元素删除
        System.out.println(list);   // [Laoyang, 20, QQ, WW, RR, 20]

        System.out.println("-----------------------------");

        // set(int index, OPbject ele):设置指定index位置的元素为ele
        list.set(3, "QWER");
        System.out.println(list);   // [Laoyang, 20, QQ, QWER, RR, 20]

        System.out.println("-----------------------------");

        // subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合,不会对原来的集合元素有影响
        List list1 = list.subList(0, 3);
        System.out.println(list1);  // [Laoyang, 20, QQ]
        System.out.println(list);   // [Laoyang, 20, QQ, QWER, RR, 20]
    }
}

class Person {
    private String name;
    private int age;

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    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 Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }
}

List 集合遍历的三种方式

Iterator迭代器、foreach()、for()

package com.laoyang.test.day2;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * @ClassName ListMethod
 * @Description: List 接口常用方法
 * @Author Laoyang
 * @Date 2021/10/2 9:41
 */
public class ListMethod {
    /**
     * List 集合遍历的三种方式
     * Iterator迭代器、foreach()、for()
     */
    @Test
    public void testTwo() {
        ArrayList list = new ArrayList();
        list.add("Laoyang");
        list.add(20);
        list.add(new Person("Xiaobai", 18));
        list.add(20);

        // 方式一:Iterator 迭代器
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        System.out.println("------------------------------");

        // 方式二:foreach()
        for (Object o : list) {
            System.out.println(o);
        }

        System.out.println("------------------------------");

        // 方式三:for()
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

class Person {
    private String name;
    private int age;

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    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 Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person() {
    }
}

小结

:add(Object ele)、addAll(Collection eles)
:remove(int index)、remove(Object obj)
:set(int index, Object ele)
:get(int index)
:add(int index, Object ele)
长度:size()
遍历:Iterator迭代器、foreach()、for()

Set 的使用

说明

  1. Set 接口是 Collection 的子接口,set 接口没有提供额外的方法。

  2. Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。

  3. Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法。

特点:存储无序、不可重复的数据

使用要求

  1. 向 Set 中添加的数据,其所在的类一定要重写 hashCode() 和 equals()
  2. 重写的 hashCode() 和 equals() 尽可能保持一致性:相等的对象必须具有相等的散列码(哈希值)
  3. 重写两个方法的小技巧:对象中用作 equals() 方法比较的属性,都应该用来计算 hashCode() 值

通常参与计算 hashCode() 的对象的属性也应该参与到 equals() 中进行计算。

建议:IDEA 中有自动生成的 equals() 和 hashCode(),在开发中建议直接使用自动生成的,可以避免一些不必要的麻烦。

IDEA 快捷键:ALT + INS

了解

Set 接口中没有额外定义新的方法,使用的都是 Collection 接口中声明过的方法

Set 实现类

  • HashSet:作为 Set 接口的主要实现类;线程不安全;可以存储 null 值。

    HashSet 底层结构:数组 + 链表(JDK7)

  • LinkedHashSet:作为 HashSet 的子类;遍历内部数据时,可以按照添加的顺序进行遍历。

  • ThreeSet:可以按照添加对象的指定属性进行排序。

Set 的无序性与不可重复性(以 HashSet 为例)

  • 无序性:不等于随机性;存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
  • 不可重复性:保证添加的元素按照 equals() 判断时,不能返回 true(因为返回 true 的时候就表示两个元素相同);相同的元素只能添加一个。

大家可以参照下面的案例代码了解 HashSet 的使用和 HsahSet 中的无序性与不可重复性

案例

实体类

package com.laoyang.test.day3;

import java.util.Objects;

/**
 * @ClassName User
 * @Description:
 * @Author Laoyang
 * @Date 2021/10/8 11:02
 */
public class User implements Comparable {
    private String name;
    private int age;

    public User() {
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    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 User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /**
     * 重写 equals() 方法
     */
    @Override
    public boolean equals(Object o) {
        System.out.println("调用了 User 的 equals 方法");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age && Objects.equals(name, user.name);
    }

    /**
     * 重写 hashCode() 方法
     */
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

测试类

package com.laoyang.test.day3;

import org.junit.Test;
import java.util.*;

/**
 * @ClassName SetTest
 * @Description: Set 接口的使用
 * @Author Laoyang
 * @Date 2021/10/2 10:52
 */
public class SetTest {
    /**
     * 无序性
     * 不等于随机性;存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
     */
    @Test
    public void testOne() {
        // HashSet 会默认创建一个长度为 16 的数组
        Set set = new HashSet();
        set.add(123);
        set.add("QQ");
        set.add(new String("ABC"));
        set.add(12.5);

        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

    /**
     * 不可重复性
     * 保证添加的元素按照 equals() 判断时,不能返回 true(因为返回 true 的时候就表示两个元素相同);相同的元素只能添加一个。
     */
    @Test
    public void testTwo() {
        Set set = new HashSet();
        set.add(123);
        set.add("QQ");
        set.add(new String("ABC"));
        set.add(12.5);
        /*
        在 User 类中重写 hashCode() 方法用于判断两个对象的哈希值是否相同(如果哈希值相同,那么才会调用 equals() 方法进行判断)
        如果不重写 hashCode() 方法,则会默认调用 Object 类中的 hashCode() 方法,而 Object 中的 hashCode() 是随机进行判断的
        所以最后可能根本就没有与现有的相同值进行判断,而是直接插入在了不同的位置上,导致最后获取的还是两个相同的对象
         */
        set.add(new User("Xiaobai", 18));
        set.add(new User("Xiaobai", 18));

        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            // 最后打印的结果中只会有一个 User 对象
            System.out.println(iterator.next());
        }
    }
}

如果想要看到最后打印两个相同元素的效果,只需要把 User 类中的 hashCode() 方法注释就可以了

LinkedHashSet 的使用

说明
  • LinkedHashSet 是 HashSet 的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据

  • 优点:对于频繁的遍历操作,LinkedHashSet 的效率要高于 HashSet

案例
package com.laoyang.test.day3;

import org.junit.Test;
import java.util.*;

/**
 * @ClassName SetTest
 * @Description: Set 接口的使用
 * @Author Laoyang
 * @Date 2021/10/2 10:52
 */
public class SetTest {
    /**
     * LinkedHashSet 的使用
     */
    @Test
    public void testThree() {
        Set set = new LinkedHashSet();
        set.add(123);
        set.add("QQ");
        set.add(new String("ABC"));
        set.add(12.5);
        set.add(new User("Xiaobai", 18));
        set.add(new User("Xiaobai", 18));

        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

TreeSet 的使用

说明
  1. 向 TreeSet 中添加的数据,要求是相同类的对象。
  2. 两种排序:自然排序(实现 Comparable) 和 定制排序(Comparator)
  3. 自然排序中,比较两个对象是否相同的标准为:compareTo() 返回 0,不会在意 equals()
  4. 定制排序中,比较两个对象是否相同的标准为:compare() 返回 0,不会在意 equals()

特点:有序,查询速度比 List 快

TreeSet 底层使用红黑树结构存储数据

案例

实体类-User

package com.laoyang.test.day3;

import java.util.Objects;

/**
 * @ClassName User
 * @Description:
 * @Author Laoyang
 * @Date 2021/10/8 11:02
 */
public class User implements Comparable {
    // 省略部分代码 - 这个类就是上面那个User类

    /**
     * 按照姓名从小到大进行排序
     */
    @Override
    public int compareTo(Object o) {
        if (o instanceof User) {
            User user = (User) o;
            //return this.name.compareTo(user.name);	// 只判断name是否相同
            int i = this.name.compareTo(user.name);
            if (i != 0) {
                return i;
            } else {
                return Integer.compare(this.age, user.age);
            }
        } else {
            throw new RuntimeException("类型不一致");
        }
    }
}

测试类

package com.laoyang.test.day3;

import org.junit.Test;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;

/**
 * @ClassName TreeSetTest
 * @Description: TreeSet 的使用
 * @Author Laoyang
 * @Date 2021/10/8 10:56
 */
public class TreeSetTest {
    /**
     * 自然排序
     */
    @Test
    public void testOne() {
        TreeSet set = new TreeSet();
        // 不能添加不同类的对象:ClassCastException
//        set.add(123);
//        set.add("QQ");
//        set.add(12.5);

        // 举例一
//        set.add(123);
//        set.add(-1);
//        set.add(233);
//        set.add(99);

        // 举例二
        set.add(new User("Tom", 22));
        set.add(new User("Jerry", 18));
        set.add(new User("Jim", 35));
        // 如果只是判断了 name,那么最后打印的时候只会出现一个 Mike
        set.add(new User("Mike", 6));
        set.add(new User("Mike", 15));

        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

    /**
     * 定制排序
     */
    @Test
    public void testTwo() {
        Comparator comparator = new Comparator() {
            // 按照年龄从小到大排序
            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof User && o2 instanceof User) {
                    User userA = (User) o1;
                    User userB = (User) o2;
                    return Integer.compare(userA.getAge(), userB.getAge());
                } else {
                    throw new RuntimeException("类型不一致");
                }
            }
        };
        TreeSet set = new TreeSet(comparator);
        set.add(new User("Tom", 18));
        set.add(new User("Jerry", 18));
        set.add(new User("Jim", 35));
        set.add(new User("Mike", 6));
        set.add(new User("Mike", 15));

        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

Set 中添加元素的过程(以 HashSet 为例)

  1. 我们向 HashSet 中添加元素 A,首先调用元素 A 所在类的 hashCode() 方法,计算出元素 A 的哈希值。

  2. 然后该哈希值通过某种算法计算出在 HashSet 底层数组中的存放位置(也就是索引位置)。

  3. 判断数组当前位置上是否已经存在元素:

    如果此位置没有其它元素,那么元素 A 则添加成功(情况一
    如果存在其它元素 B(或者以链表形式存在的多个元素),则比较元素 A 与 B 的哈希值

    ​ 如果哈希值不相同,则元素 A 添加成功(情况二
    ​ 如果哈希值相同,则需要调用元素 A 所在类中的 equals() 方法

    ​ 如果 equals() 方法返回 true,则表示元素 A 与元素 B 是相同的,添加失败
    ​ 如果 equals() 方法返回 false,则表示元素 A 与元素 B 是不相同的,添加成功(情况三

    对于添加成功的情况二和情况三而言:元素 A 与已经存在指定索引位置上的数据以链表的方式进行存储

具体存储方式

JDK7:元素 A 放在数组中,指向原来的元素

在这里插入图片描述

JDK8:原来的元素放在数组中,指向元素 A

在这里插入图片描述

总结:七上八下

练习

练习一

创建员工类的 5 个对象,并把这些对象放入 TreeSet 集合中,分别按以下两种方式对集合中的元素进行排序,并遍历输出:

  1. 使员工类实现 Comparable 接口,并按 name 排序。
  2. 创建 TreeSet 时传入 Comparator 对象,按生日日期的先后排序。

员工生日类

package com.laoyang.test.day4;

/**
 * @ClassName MyDate
 * @Description: 生日类
 * @Author Laoyang
 * @Date 2021/10/10 10:16
 */
public class MyDate implements Comparable {
    private int year;
    private int month;
    private int day;

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public int getMonth() {
        return month;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    public int getDay() {
        return day;
    }

    public void setDay(int day) {
        this.day = day;
    }

    public MyDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public MyDate() {
    }

    @Override
    public String toString() {
        return "MyDate{" +
                "year=" + year +
                ", month=" + month +
                ", day=" + day +
                '}';
    }

    @Override
    public int compareTo(Object o) {
        if (o instanceof MyDate) {
            MyDate myDate = (MyDate) o;
            // 判断年
            int compareA = Integer.compare(this.getYear(), myDate.getYear());
            if (compareA != 0) {
                return compareA;
            }
            // 判断月
            int compareB = Integer.compare(this.getMonth(), myDate.getMonth());
            if (compareB != 0) {
                return compareB;
            } else {
                // 判断日
                return Integer.compare(this.getDay(), myDate.getDay());
            }
        }
        throw new RuntimeException("类型不一致");
    }
}

员工类

package com.laoyang.test.day4;

/**
 * @ClassName Employee
 * @Description: 员工类
 * @Author Laoyang
 * @Date 2021/10/10 10:15
 */
public class Employee implements Comparable {
    private String name;
    private int age;
    private MyDate birthday;

    public Employee() {
    }

    public Employee(String name, int age, MyDate birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    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 MyDate getBirthday() {
        return birthday;
    }

    public void setBirthday(MyDate birthday) {
        this.birthday = birthday;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", birthday=" + birthday +
                '}';
    }

    @Override
    public int compareTo(Object o) {
        if (o instanceof  Employee) {
            Employee employee = (Employee) o;
            return this.name.compareTo(employee.name);
        }
        throw new RuntimeException("类型不一致");
    }
}

测试类

package com.laoyang.test.day4;

import org.junit.Test;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;

/**
 * @ClassName EmployeeTest
 * @Description: 测试类
 * @Author Laoyang
 * @Date 2021/10/10 10:21
 */
public class EmployeeTest {
    /**
     * 自然排序
     * 根据员工姓名进行排序
     */
    @Test
    public void testOne() {
        Employee employeeA = new Employee("David",18,new MyDate(2003,2,20));
        Employee employeeB = new Employee("Jerome",32,new MyDate(1988,5,18));
        Employee employeeC = new Employee("Aaron",21,new MyDate(2000,3,11));
        Employee employeeD = new Employee("Dennis",15,new MyDate(2006,6,1));
        Employee employeeE = new Employee("Robinson",19,new MyDate(2002,10,6));
        TreeSet set = new TreeSet();
        set.add(employeeA);
        set.add(employeeB);
        set.add(employeeC);
        set.add(employeeD);
        set.add(employeeE);

        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

    /**
     * 定制排序
     * 根据员工生日信息进行排序
     */
    @Test
    public void testTwo() {
        Employee employeeA = new Employee("David",18,new MyDate(2003,2,20));
        Employee employeeB = new Employee("Jerome",32,new MyDate(1988,5,18));
        Employee employeeC = new Employee("Aaron",21,new MyDate(2000,3,11));
        Employee employeeD = new Employee("Dennis",15,new MyDate(2006,6,1));
        Employee employeeE = new Employee("Robinson",19,new MyDate(2003,10,6));

        Comparator comparator = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof Employee && o2 instanceof Employee) {
                    Employee employeeA = (Employee) o1;
                    Employee employeeB = (Employee) o2;

                    MyDate myDateA = employeeA.getBirthday();
                    MyDate myDateB = employeeB.getBirthday();

                    // 方式一:在对应方法方法中编写对应判断
//                    // 判断年
//                    int compareA = Integer.compare(myDateA.getYear(), myDateB.getYear());
//                    if (compareA != 0) {
//                        return compareA;
//                    }
//                    // 判断月
//                    int compareB = Integer.compare(myDateA.getMonth(), myDateB.getMonth());
//                    if (compareB != 0) {
//                        return compareB;
//                    } else {
//                        // 判断日
//                        return Integer.compare(myDateA.getDay(), myDateB.getDay());
//                    }

                    // 方式二:在MyDate类中进行判断,然后直接调用
                    return myDateA.compareTo(myDateB);
                } else {
                    throw new RuntimeException("类型不一致");
                }
            }
        };

        TreeSet set = new TreeSet(comparator);
        set.add(employeeA);
        set.add(employeeB);
        set.add(employeeC);
        set.add(employeeD);
        set.add(employeeE);

        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

练习二

在List内去除重复数字值,要求尽量简单

package com.laoyang.test.day4;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

/**
 * @ClassName ListDeduplication
 * @Description: 练习二:在List内去除重复数字值,要求尽量简单
 * @Author Laoyang
 * @Date 2021/10/10 11:06
 */
public class ListDeduplication {
    public static List duplicateList(List list) {
        HashSet set = new HashSet();
        set.addAll(list);
        return new ArrayList(set);
    }

    public static void main(String[] args) {
        List list = new ArrayList();
        /*
          如果list中加入的是自定义的类,那么在自定义类中就需要重写 hashCode() 和 equals() 方法
         */
        list.add(new Integer(1));
        list.add(new Integer(2));
        list.add(new Integer(2));
        list.add(new Integer(4));
        list.add(new Integer(4));
        List list2 = duplicateList(list);
        for (Object integer : list2) {
            System.out.println(integer);
        }
    }
}

Map 的使用

介绍

Map:双列数据,存储 key-value 对的数据

  • Map 与 Collection 并列存在。用于保存具有映射关系的数据:key-value
  • Map 中的 key 和 value 都可以是任何引用类型的数据
  • Map 中的 key 用 Set 来存放,不允许重复,也就是说同一个 Map 对象所对应的类,必须重写 hashCode() 和 equals() 方法
  • 常用 String 类作为 Map 的 “键”
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value

常用方法

添加、删除、修改操作

方法作用
Object put(Object key,Object value)将指定key-value添加到(或修改)当前map对象中
void putAll(Map m)将m中的所有key-value对存放到当前map中
Object remove(Object key)移除指定key的key-value对,并返回value
void clear()清空当前map中的所有数据

元素查询的操作

方法作用
Object get(Object key)获取指定key对应的value
boolean containsKey(Object key)是否包含指定的key
boolean containsValue(Object value)是否包含指定的value
int size()返回map中key-value对的个数
boolean isEmpty()判断当前map是否为空
boolean equals(Object obj)判断当前map和参数对象obj是否相等

元视图操作的方法

方法作用
Set keySet()返回所有key构成的Set集合
Collection values()返回所有value构成的Collection集合
Set entrySet()返回所有key-value对构成的Set集合

案例

package com.laoyang.test.day5;

import org.junit.Test;
import java.util.*;

/**
 * @ClassName MapMethod
 * @Description: Map中的常用方法
 * @Author Laoyang
 * @Date 2021/10/13 11:04
 */
public class MapMethod {
    /**
     添加、删除、修改操作:
     Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
     void putAll(Map m):将m中的所有key-value对存放到当前map中
     Object remove(Object key):移除指定key的key-value对,并返回value
     void clear():清空当前map中的所有数据
     */
    @Test
    public void testOne(){
        // put(Object key,Object value)
        Map map = new HashMap();
        // 添加
        map.put("AA", 123);
        map.put(111, 321);
        map.put("bb", 56);
        // 修改
        map.put("AA", 99);

        System.out.println(map);    // {AA=99, bb=56, 111=321}

        // putAll(Map m)
        Map maps = new HashMap();
        maps.put("CC", 123);
        maps.put("DD", 123);
        map.putAll(maps);
        System.out.println(map);    // {AA=99, bb=56, CC=123, DD=123, 111=321}

        // remove(Object key)
        map.remove("CC");
        System.out.println(map);    // {AA=99, bb=56, DD=123, 111=321}

        // clear():注意:该方法并不是将所有的元素变为null,而是直接变回刚创建的样子(没有任何元素)
        map.clear();
        System.out.println(map.size());  // 0
        System.out.println(map);    // {}
    }

    /**
     元素查询的操作:
     Object get(Object key):获取指定key对应的value
     boolean containsKey(Object key):是否包含指定的key
     boolean containsValue(Object value):是否包含指定的value
     int size():返回map中key-value对的个数
     boolean isEmpty():判断当前map是否为空
     boolean equals(Object obj):判断当前map和参数对象obj是否相等
     */
    @Test
    public void testTwo() {
        Map map = new HashMap();
        map.put("AA", 123);
        map.put(111, 321);
        map.put("bb", 56);

        // get(Object key):如果找不到对应的 key 值,则返回 null
        System.out.println(map.get("BB"));    // null
        System.out.println(map.get("bb"));    // 56

        // containsKey(Object key)
        boolean A1 = map.containsKey("AA");
        boolean A2 = map.containsKey("aa");
        System.out.println(A1);   // true
        System.out.println(A2);   // false

        // containsValue(Object value):如果存在相同的 value 值,那么只要找到第一个,就不会在往下继续找了
        boolean B1 = map.containsValue(123);
        boolean B2 = map.containsValue(345);
        System.out.println(B1);   // true
        System.out.println(B2);   // false

        // size()
        System.out.println(map.size());     // 3

        // isEmpty()
        boolean empty = map.isEmpty();
        System.out.println(empty);      // false

        Map map1 = new HashMap();
        System.out.println(map1.isEmpty());  // true

        // equals(Object obj)
        System.out.println(map.equals(map1));   // false
        Map maps = new HashMap();
        maps.put("AA", 123);
        maps.put(111, 321);
        maps.put("bb", 56);
        System.out.println(map.equals(maps));   // true
    }

    /**
     元视图操作(遍历)的方法:
     Set keySet():返回所有key构成的Set集合
     Collection values():返回所有value构成的Collection集合
     Set entrySet():返回所有key-value对构成的Set集合
     Ps:因为都是集合的一种,所以都可以使用 for、foreach、iterator以及 jkd8 中的 Lambda 表达式
     */
    @Test
    public void testThree() {
        Map map = new HashMap();
        map.put("AA", 123);
        map.put(111, 321);
        map.put("bb", 56);

        // map.iterator()   // 不能直接调用 iterator()

        // keySet():遍历所有的 key 值
        Iterator iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        System.out.println("-----------------------------");

        // values():遍历所有的 value 值
        Collection values = map.values();
        for (Object value : values) {
            System.out.println(value);
        }

        System.out.println("-----------------------------");

        /*
        遍历所有的 key-value
         */
        // 方式一:entrySet()
        Set set = map.entrySet();
        Iterator iterator1 = set.iterator();
        while (iterator1.hasNext()) {
            Object obj = iterator1.next();
            // entrySet 集合中的元素都是 entry
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + " = " + entry.getValue());
        }

        System.out.println("-----------------------------");

        // 方式二
        Iterator key = map.keySet().iterator();
        while (key.hasNext()) {
            Object obj = key.next();
            Object value = map.get(obj);
            System.out.println(obj + " = " + value);
        }
    }
}

总结
增:put(Object key, Object value)、putAll(Map m)
删:remove(Object key)
改:put(Object key, Object value)
查:get(Object key)
长度:size()
遍历:keySet()、values()、entrySet()

Map 实现类

  • HashMap:作为 Map 的主要实现类(JDK1.2 发布);线程不安全的,效率较高;存储 null 的 key 和 value;

    JDK7 及之前的底层结构:数组 + 链表

    JDK8 的底层结构:数组 + 链表 + 红黑树

  • LinkedHashMap:(JDK1.4 发布);HashMap 的子类;保证遍历 Map 元素时,可以按照添加的顺序进行遍历(在原有的 HashMap 底层结构基础上,添加了一对指针,指向前一个和后一个元素)

    对于频繁的遍历操作,此类执行的效率高于 HashMap

  • TreeMap:(JDK1.2 发布);保证添加的 key-value 对进行排序,实现排序遍历(此时考虑 key 的自然排序或定制排序);底层使用的是红黑树;

  • Hashtable:作为古老的实现类(JDK1.0 发布);线程安全的,效率较低;不能存储 null 的 key 和 value;

  • Properties:(JDK1.0 发布);Hashtable 的子类;常用来处理配置文件;key 和 value 都是 String 类型;

HashMap 的使用

HashMap 结构的理解

Map 中的 key:无序,且不能重复,使用 Set 存储所有的 key(key 所在类需要重写 equals() 和 hashCode(),以 HashMap 为例)
Map 中的 value:无序,可重复,使用 Collection 存储所有的 value(value 所在类需要重写 equals())

一个键值对:key-value 构成了一个 Entry 对象
Map 中的 entry:无序且不可重复的,使用 Set 存储所有的 entry

HashMap 的底层实现原理

以 JDK7 为例:

HashMap map = new HashMap();   // 在实例化以后,底层创建了长度是 16 的一维数组 Entry[] table;
..假设前面我们已经put过一些数据了..
map.put(key1, value1);

下面可以看到 JDK7 在添加元素的过程是如何实现的

HashMap 添加元素的过程

JDK7

  1. 首先,调用 key1 所在类的 hashCode() 计算 key1 的哈希值;

  2. 然后,此哈希值经过某种算法计算后得到在 Entry 数组中的存放位置;

  3. 判断当前位置上否是有数据:

    如果此位置上的数据为空,则 key1-value1 添加成功(情况一)
    如果此位置上的数据不为空(也就表示此位置上存在一个或多个数据(以链表形式存在)),则比较 key1 和已存在的一个或多个数据的哈希值

    ​ 如果 key1 与已存在的数据的哈希值都不相同,则 key1-value1 添加成功(情况二)
    ​ 如果 key1 与已存在的某一个数据的哈希值相同,则调用 key1 所在类的 equals() 比较 key1 和已存在的一个或多个数据的值

    ​ 如果 equals() 返回 false,则 key1-value1 添加成功(情况三)
    ​ 如果 equals() 返回 true,则使用 value1 替换相同 key 的 value 值

    比如现在有两个值 A 和 B,然后又添加了一个新的值 C,这时候 C 的 key(哈希值和值)和 A 的一模一样,这个时候就会把 C 的 value 值赋给 A 的 value
    就相当于修改操作

    补充:关于情况二和情况三:此时 key1-value1 和原来的数据以链表的方式存储

扩容:在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,就会开始扩容。

默认会扩容为原来容量的两倍,并将原有数据复制到扩容好的数组中

JDK8 相较于 JDK7 在底层实现方面的不同

  1. HashMap map = new HashMap(); // 底层没有创建一个长度为 16 的数组

  2. JDK8 底层的数组是:Node[],并不是 Entry

  3. 首次调用 put() 方法时,底层才会去创建长度为 16 的数组

  4. JDK7 底层结构只有:数组 + 链表;JDK8 底层结构:数组 + 链表 + 红黑树

    当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 并且当前数组长度 > 64,此时此索引位置上的所有数据改为使用红黑树存储。

    这样做是为了提高我们的查询效率,一旦数据多起来了,那么使用数组查找的时候效率就会比较低(因为每次都需要完整遍历一遍);而红黑树是有序的,小的排左边,大的排右边,在查找的时候就可以先排除一半的数据在进行查找,可以节省更多的时间。

部分源码

注意:中文注释是我自己加的,有些是根据自己的意思解释,有些是根据源码中的注释进行翻译。所以大家不要太较劲

JDK7
package java.util;
import java.io.*;

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	/**
     * 默认初始容量 - 必须是 2 的幂
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;
	
	/**
     * 在构造函数中未指定时使用的负载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
	
	/**
     * 最大容量,在两个带参数的构造函数隐式指定更高值时使用。必须是 2 的幂 <= 1<<30
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
	
	/**
     * 存放数据的数组。长度必须始终是 2 的幂
     */
    transient Entry<K,V>[] table;
	
	public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
	
	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);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;   // 决定了数组创建的长度(比如我们手动设置HashMap的长度为 15,但其实还是创建了一个长度为16的数组)
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
		 /*
         threshold:临界值(capacity * loadFactor),HashMap 在扩容的时候并不是数组放不下的时候才开始扩容,因为可能以链表的方式一直向下存,所以一般达到临界值的时候就会开始扩容
         capacity * loadFactor = 16 * 0.75 = 12
         MAXIMUM_CAPACITY:最大容量
         */
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }
	
	/**
     * 将指定值与此映射中的指定键相关联。如果映射先前包含键的映射,则旧值将被替换
     *
     * @param key   与指定值相关联的键
     * @param value 要与指定键关联的值
     * @return 与 key 关联的先前值,如果 key 没有映射,则为 null。(null 返回也可以表明映射先前将 null 与 key 关联。)
     */
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        // 获取哈希值
        int hash = hash(key);
        // 通过对应的算法计算出数据在HashMap底层存放的位置
        int i = indexFor(hash, table.length);
        // 判断当前位置上是否存在元素
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 判断哈希值和值本身是否相同
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                // 如果哈希值和 key 值相同,则替换已存在 key 的 value 值
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        // 没有元素、哈希值不相同、值本身不相同时就会执行添加方法
        addEntry(hash, key, value, i);
        return null;
    }
	
	/**
	 * 计算哈希值
	 */
	final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

        // 此函数可确保在每个位位置仅相差常数倍的 hashCode 具有有限数量的冲突(在默认加载因子下约为 8)。
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
	
	/**
     * 根据哈希值找到对应的存放位置
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
	
	/**
	 * 扩容方法
	 * 需要扩容则先扩容在添加数据,不需要则可以直接执行添加方法
	 */
	void addEntry(int hash, K key, V value, int bucketIndex) {
        // 如果当前的临界值小于或等于 key-value 的映射数,并且当前数组位置不为空,则进行扩容操作
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 扩容:当达到临界值并且当前数组位置不为空,则默认会将数组扩容为原来的两倍
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
	
	/**
	 * 新增方法
	 */
	void createEntry(int hash, K key, V value, int bucketIndex) {
        // 将原有位置上的数据取出来
        Entry<K,V> e = table[bucketIndex];
        // 然后在将新造的 Entry 对象放到当前位置上,以链表方式存放(从上往下存,跟 HashSet(JDK7)差不多)
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
	
	Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
}
JDK8
package java.util;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	/**
     * 在构造函数中未指定时使用的负载因子。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
	
	/**
     * 哈希表的负载因子
     * @serial
     */
    final float loadFactor;
	
	/**
	 * 存放数据的数组。
	 */
	transient Node<K,V>[] table;
	
	/**
     * 构造器
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
	
	/**
     * 将指定值与此映射中的指定键相关联。如果映射先前包含键的映射,则旧值将被替换
     *
     * @param key   与指定值相关联的键
     * @param value 要与指定键关联的值
     * @return 与 key 关联的先前值,如果 key 没有映射,则为 null。(null 返回也可以表明映射先前将 null 与 key 关联。)
     */
	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);
    }
	
    /**
     * 实现 Map.put 及相关方法
     *
     * @param hash 密钥的散列
     * @param key 钥匙
     * @param value 要放置的值
     * @param onlyIfAbsent 如果为 true,则不要更改现有值
     * @param evict 如果为 false,则表处于创建模式
     * @return 以前的值,如果没有,则为 null
     */
	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 1st
                            treeifyBin(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 key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
	
	/**
     * 初始化或加倍表大小。
	 * 如果为空,则根据字段阈值中持有的初始容量目标进行分配。
	 * 否则,因为我们使用的是 2 的幂扩展,所以每个 bin 中的元素必须保持相同的索引,或者在新表中以 2 的幂的偏移量移动
     */
	final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        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 threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (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];
        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;
    }
	
	/**
     * 除非表太小,否则替换给定哈希索引处 bin 中的所有链接节点,在这种情况下改为调整大小。
     */
	final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
}

HashMap 源码中的重要常量

  • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量:16
  • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
  • threshold:扩容的临界值,= 容量 * 填充因子:16 * 0.75 = 12
  • TREEIFY_THRESHOLD:Bucket 中链表长度大于该默认值,转化为红黑树:8
  • MIN_TREEIFY_CAPACITY:桶中的 Node 被树化时最小的 hash 表容量:64

这里只举例个别几个,还有一些大家可自行查看源码

LinkedHashMap 的使用

  • LinkedHashMap 是 HashMap 的子类

  • 在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序

  • 与 LinkedHashSet 类似

LinkedHashMap 底层原理(了解)

LinkedHashMap 底层使用的结构与 HashMap 相同,因为 LinkedHashMap 继承于 HashMap
区别:LinkedHashMap 内部提供了 Entry,替换了 HashMap 中的 Node

LinkedHashMap 内部类

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中的内部类

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

TreeMap 的使用

介绍

  1. TreeMap 存储 Key-Value 对时,需要根据 key-value 对进行排序。 TreeMap 可以保证所有的 Key-Value 对处于有序状态。
  2. TreeSet 底层使用红黑树结构存储数据。
  3. 向 TreeMap 中添加 key-value,要求 key 必须是由同一个类创建的对象,因为要按照 key 进行排序:自然排序定制排序

注意:只能根据 key 进行排序,不能根据 value 排序!!!

案例

package com.laoyang.test.day5;

import org.junit.Test;
import java.util.*;

/**
 * @ClassName TreeMapTest
 * @Description: TreeMap 的使用
 * @Author Laoyang
 * @Date 2021/10/13 15:08
 */
public class TreeMapTest {
    /**
     * 自然排序
     */
    @Test
    public void testOne() {
        TreeMap map = new TreeMap();
        User userA = new User("Tom", 22);
        User userB = new User("Jerry", 18);
        User userC = new User("Jim", 35);
        User userD = new User("Mike", 6);
        map.put(userA, 60);
        map.put(userB, 76);
        map.put(userC, 32);
        map.put(userD, 100);
        Set set = map.entrySet();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "的成绩为:" + entry.getValue());
        }
    }

    /**
     * 定制排序
     */
    @Test
    public void testTwo() {
        Comparator comparator = new Comparator() {
            // 根据年龄排序
            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof User && o2 instanceof User) {
                    User userA = (User) o1;
                    User userB = (User) o2;
                    return Integer.compare(userA.getAge(), userB.getAge());
                }
                throw new RuntimeException("类型不一致");
            }
        };

        TreeMap map = new TreeMap(comparator);
        User userA = new User("Tom", 22);
        User userB = new User("Jerry", 18);
        User userC = new User("Jim", 35);
        User userD = new User("Mike", 6);
        map.put(userA, 60);
        map.put(userB, 76);
        map.put(userC, 32);
        map.put(userD, 100);
        Set set = map.entrySet();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "的成绩为:" + entry.getValue());
        }
    }
}

class User implements Comparable {
    private String name;
    private int age;

    /**
     * 重写 equals() 方法
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age &&
                Objects.equals(name, user.name);
    }

    /**
     * 重写 hashCode() 方法
     */
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    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 User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public User() {
    }

    /**
     * 按照姓名从小到大进行排序
     */
    @Override
    public int compareTo(Object o) {
        if (o instanceof User) {
            User user = (User) o;
            int i = this.name.compareTo(user.name);
            if (i != 0) {
                return i;
            } else {
                return 0;
            }
        }
        throw new RuntimeException("类型不一致");
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Hashtable 的使用

  • Hashtable 是个古老的 Map 实现类,JDK1.0 就提供了。不同于 HashMap, Hashtable 是线程安全的。
  • Hashtable 实现原理和 HashMap 相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。与 HashMap 不同,Hashtable 不允许使用 null 作为 key 和 value。
  • 与 HashMap 一样,Hashtable 也不能保证其中 Key-Value 对的顺序。
  • Hashtable 判断两个 key 相等、两个 value 相等的标准,与 HashMap 一致。

Properties 的使用

介绍

  1. Properties 类是 Hashtable 的子类,该对象用于处理属性文件
  2. 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
  3. 存取数据时,建议使用 setProperty(String key,String value) 方法和 getProperty(String key) 方法

案例

配置文件 - jdbc.properties

username=root
password=123456

该文件创建在父工程下面(因为这里只是测试一下效果,所以创建后就可以随便写一点东西)

在这里插入图片描述

注意:配置文件中等号(=)两边不能出现空格,比如 name = “AAA”

测试类

package com.laoyang.test.day5;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

/**
 * @ClassName PropertiesTest
 * @Description: Properties 的使用
 * @Author Laoyang
 * @Date 2021/10/13 15:28
 */
public class PropertiesTest {
    /**
     Properties:常用来处理配置文件,key 和 value 都是 String 类型
     */
    public static void main(String[] args) {
        FileInputStream stream = null;
        try {
            Properties properties = new Properties();
            stream = new FileInputStream("jdbc.properties");
            properties.load(stream);
            String name = properties.getProperty("username");
            String password = properties.getProperty("password");
            System.out.println("账号为:" + name + ",密码为:" + password);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

有时候写配置文件导致乱码,可能是编码格式没有设置好

扩展:如果不想配置文件出现乱码,则需要在创建配置文件之前,设置好对应的编码格式(如果已经创建好文件并且已经出现乱码,则需要先删除对应的配置文件,然后在进行编码设置,之后才能接着编写,否则并不会实现正确的效果
如果是使用 IDEA ,那么需要把 File > Editor > File Encodings > Iransparent native-to-ascii conversion 勾上

Collections 工具类的使用

操作数组的工具类:Arrays

介绍

  1. Collections 是一个操作 Set、List 和 Map 等集合的工具类
  2. Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作, 还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

常用方法

排序操作

方法作用
reverse(List list)反转集合中的所有元素
shuffle(List list)将集合元素进行随机排序
sort(List list)将集合元素进行自然排序
sort(List list, Comparator com)将集合元素进行定制排序
swap(List list,int i, int j)交换集合中指定位置上的元素

查找、替换

方法作用
Object max(Collection)根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator)根据元素的定制顺序,返回给定集合中的最大元素
Object min(Collection)根据元素的自然顺序,返回给定集合中的最小元素
Object min(Collection,Comparator)根据元素的定制顺序,返回给定集合中的最小元素
int frequency(Collection,Object)返回指定集合中指定元素的出现次数
void copy(List dest,List src)将 src 中的内容复制到 dest 中
boolean replaceAll(List list,Object oldVal,Object newVal)使用新值(oldVal)替换 List 中的旧值(newVal)

需要注意 copy()

同步控制

Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

方法作用
synchronizedCollection(Collection c)返回由指定集合支持的同步(线程安全)集合
synchronizedList(List list)返回由指定列表支持的同步(线程安全)列表
synchronizedMap(Map map)返回由指定Map支持的同步(线程安全)映射
synchronizedSet(Set set)返回由指定集合支持的同步(线程安全)集
synchronizedSortedMap(SortedMap m)返回由指定的排序映射支持的同步(线程安全)排序映射
synchronizedSortedSet(SortedSet s)返回由指定的排序集支持的同步(线程安全)排序集

案例

package com.laoyang.test.day6;

import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * @ClassName CollectionUtil
 * @Description: Collections 工具类的使用
 * @Author Laoyang
 * @Date 2021/10/13 15:59
 */
public class CollectionsUtil {
    /**
     排序操作:(均为static方法)
     reverse(List):反转 List 中元素的顺序
     shuffle(List):对 List 集合元素进行随机排序
     sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
     sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
     swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
     */
    @Test
    public void testOne() {
        List list = new ArrayList();
        list.add(22);
        list.add(57);
        list.add(33);
        list.add(19);
        System.out.println(list);   // [22, 57, 33, 19]

//        Collections.reverse(list);      // 反转集合元素
//        Collections.shuffle(list);      // 将集合元素进行随机排序
//        Collections.sort(list);         // 将集合元素进行自然排序,如果想使用定制排序,则需要在添加一个 Comparator 参数
        Collections.swap(list, 1, 3);   // 交换集合中指定位置的元素

        System.out.println(list);
    }

    /**
     查找、替换
     Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
     Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
     Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
     Object min(Collection,Comparator):根据元素的定制顺序,返回给定集合中的最小元素
     int frequency(Collection,Object):返回指定集合中指定元素的出现次数
     void copy(List dest,List src):将src中的内容复制到dest中
     boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
     */
    @Test
    public void testTwo() {
        List list = new ArrayList();
        list.add(22);
        list.add(57);
        list.add(57);
        list.add(33);
        list.add(19);
        System.out.println(list);   // [22, 57, 57, 33, 19]

        // max(Collection coll):根据自然排序找到集合中的最大值,定制排序需添加 Comparator 参数
        System.out.println(Collections.max(list));    // 57

        // min(Collection coll):根据自然排序找到集合中的最小值,定制排序需添加 Comparator 参数
        System.out.println(Collections.min(list));    // 19

        // frequency(Collection coll, Object obj):
        System.out.println(Collections.frequency(list, 57));    // 2

        System.out.println("------------------------");

        //copy(List dest, List src):复制,将 src 中的值复制到 dest 中
        /*
         // 报异常:java.lang.IndexOutOfBoundsException: Source does not fit in dest
         List list1 = new ArrayList();
         Collections.copy(list1, list);
         */

        // 正确写法
        List list1 = Arrays.asList(new Object[list.size()]);
        System.out.println(list1.size());   // 5
        Collections.copy(list1, list);      // 执行成功

        System.out.println(list1);

        System.out.println("------------------------");

        //replaceAll(List list,Object oldVal,Object newVal):使用新值(oldVal)替换 List 中的旧值(newVal)
        Collections.replaceAll(list, 22, 99);

        System.out.println(list);   // [99, 57, 57, 33, 19]
    }

    /**
     Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,
     从而可以解决多线程并发访问集合时的线程安全问题
     */
    @Test
    public void testThree() {
        List list = new ArrayList();
        list.add(22);
        list.add(57);
        list.add(57);
        list.add(33);
        list.add(19);
        System.out.println(list);   // [22, 57, 57, 33, 19]

        // 返回的 list1 就是线程安全的
        List list1 = Collections.synchronizedList(list);
    }
}

练习一

请从键盘随机输入10个整数保存到List中,并按倒序、从大到小的顺序显示出来

package com.laoyang.test.day6;

import java.util.*;

/**
 * @ClassName Practise
 * @Description: 练习一
 * @Author Laoyang
 * @Date 2021/10/13 17:15
 */
public class ExerciseOne {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        List list = new ArrayList();
        int j;
        for (int i = 0; i < 10; i++) {
            System.out.print("请输入整数:");
            j = input.nextInt();
            list.add(j);
        }
        Collections.sort(list);
        Collections.reverse(list);
        System.out.println(list);
    }
}

练习二

将学生名与考试分数录入到集合中,并按分数显示前三名成绩学员的名字

package com.laoyang.test.day6;

import org.junit.Test;
import java.util.*;

/**
 * @ClassName ExerciseTwo
 * @Description: 练习二
 * @Author Laoyang
 * @Date 2021/10/13 17:28
 */
public class ExerciseTwo {
    @Test
    public void testOne() {
        List list = new ArrayList();
        list.add(new Student("Tom", 60));
        list.add(new Student("Jerry", 80));
        list.add(new Student("Jim", 73));
        list.add(new Student("Mike", 52));
        list.add(new Student("Noob", 90));

        Collections.sort(list, new Comparator<Object>() {
            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof Student && o2 instanceof Student) {
                    Student stuA = (Student) o1;
                    Student stuB = (Student) o2;
                    return -Double.compare(stuA.getFraction(), stuB.getFraction());
                }
                throw new RuntimeException("类型不一致");
            }
        });

        for (int i = 0; i < 3; i++) {
            System.out.println(list.get(i));
        }
    }
}

class Student {
    private String name;
    private double fraction;

    public Student() {
    }

    public Student(String name, double fraction) {
        this.name = name;
        this.fraction = fraction;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getFraction() {
        return fraction;
    }

    public void setFraction(double fraction) {
        this.fraction = fraction;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", fraction=" + fraction +
                '}';
    }
}

面试题

一、ArrayList、LinkedList 和 Vector 的相同点与不同点?

相同点:三个类都实现了 List 接口,存储数据的特点相同(都是有序且可重复的数据)

不同点

ArrayList:作为 List 接口的主要实现类(JDK1.2出现);执行效率高(线程不安全的);底层使用 Object[] elementData 进行存储;

LinkedList:JDK1.2出现;对于频繁的插入、删除操作使用此类效率比 ArrayList 要高;底层使用双向链表存储;

Vector:作为 List 接口的古老实现类(JDK1.0出现);执行效率低(线程安全的);底层使用 Object[] elementData 进行存储;

List 接口也是 JDK1.2 出现的

二、下面代码最终打印的结果是什么?

题一、

package com.laoyang.test.day2;

import org.junit.Test;
import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName ListInterviewQuestions
 * @Description: 面试题
 * @Author Laoyang
 * @Date 2021/10/2 10:43
 */
public class ListInterviewQuestions {
    @Test
    public void testOne() {
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        updateList(list);
        System.out.println(list);  //1,2
    }

    private static void updateList(List list) {
        // 默认情况下去删除整型元素的时候,会优先调用 List 中的 remove(int index),而不是 remove(Object obj)
        list.remove(2);    				   // 根据下标删除,[1, 2]
        //list.remove(new Integer(2));     // 根据元素删除,[1, 3]
    }
}

主要考察点:区分 list 中 remove(int index) 和 remove(Object obj)

这里写了两种方式,大家可以打开注释进行测试,查看最后的效果

题二、

package com.laoyang.test.day4;

import org.junit.Test;
import java.util.HashSet;
import java.util.Objects;

/**
 * @ClassName Set
 * @Description: 面试题
 * @Author Laoyang
 * @Date 2021/10/10 11:09
 */
public class SetInterviewQuestions {
    @Test
    public void test() {
        HashSet set = new HashSet();
        Person p1 = new Person(1001,"AA");
        Person p2 = new Person(1002,"BB");
        set.add(p1);
        set.add(p2);

        // 将 set 中 p1 的 name 值进行修改
        p1.name = "CC";
        /*
         此时的 remove 找的是 1001,CC 的存放位置
         而 p1 在存储的时候是根据 1001,AA 的哈希值进行存储的
         所以这个时候 remove 是找不到 p1 的,所以也就无法删除成功
         */
        set.remove(p1);
        System.out.println(set);    // 1002,BB;1001,CC

        /*
          因为 p1 是根据 1001,AA 找到的位置进行存放的,即使后面修改成了 1001,CC,位置也是没有发生改变的
          所以这里的 1001,CC 是可以添加成功的
         */
        set.add(new Person(1001,"CC"));
        System.out.println(set);     // 1002,BB;1001,CC;1001,CC

        /*
         这里的 1001,AA 的哈希值和 p1 是一致的,但是 p1 的值现在是 1001,CC
         所以哈希值相同以后,在调用 equals() 判断两个对象的值是否相同,AA != CC,所以可以添加成功
         */
        set.add(new Person(1001,"AA"));
        System.out.println(set);    // 1002,BB;1001,CC;1001,CC;1001,AA
    }
}

class Person {
    public int id;
    public String name;

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

    public Person() {
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

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

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

三、 HashMap 的底层实现原理?

如果上面的看不懂的话那就自行百度吧

四、HashMap 和 Hashtable 的相同点和不同点?

如果上面的看不懂的话那就自行百度吧

五、CurrentHashMap 与 Hashtable 的相同点和不同点?

暂时略过

六、Collection 和 Collections 的区别?

Collection 是存储单列接口的集合接口
Collections 是操作 Collection 的工具类

七、负载因子的大小,对 HashMap 有什么影响?

  1. 负载因子的大小决定了 HashMap 的数据密度
  2. 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  3. 负载因子越小,就越容易触发扩容,数据密度也越小,也就以为着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高,但是会浪费一定的内容空间,经常扩容也会影响性能。
  4. 按照其它语言的参考及研究经验,会考虑将负载因子设置为 0.7~0.75,此时的平均检索长度接近于常数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值