• Java集合(一):泛型与Collection集合


    目录

    集合预热:泛型

    泛型的优点

    自定义泛型类型

    自定义泛型类/接口

    泛型使用细节

    自定义泛型方法

     泛型与继承关系

    不存在继承关系的情况

     通配符与存在继承关系的情况

    泛型受限

    集合概述

    集合的作用与存储内容

    集合与数据结构

    集合:Collection接口的继承结构

    集合:Map接口继承结构

    Collection接口

    Collection接口的常用方法

    增加方法

    删除方法

    查询方法

    迭代器

    List接口的常用方法

    添加元素的方法

    获取元素的方法

    删除元素的方法

     ArrayList类

    jdk1.7源码分析

    jdk1.8源码分析

     Vector类

    LinkedList类

    LinkedList类的常用方法

    LinkedList的遍历

    LinkedList底层原理

    模拟LinkedList的源码

     LinkedList-JDK18源码

    ※面试题:Iterator()、Iterator和Iterable关系

    ※ListIterator迭代器

     Set接口

    Set接口的常用方法

    HashSet类

    HashSet:成员方法

     ※HashSet底层原理与应用:集合元素去重

     LinkedHashSet类

    ※比较器的使用

    内部比较器

    外部比较器

    比较器使用建议

    TreeSet类

     存放Integer类型

     存放String类型

     存放自定义数据类型
          使用内部比较器

          使用外部比较器


    集合预热:泛型

            泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。

            Java集合容器类在声明阶段无法确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前,将集合元素默认设计为Object对象类型;但是在JDK1.5之 后使用泛型来替代对象类型,因为此时除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理、如何过滤、如何转化为String字符串等都是确定的,即:将集合元素的类型设计成为一个参数,这个类型参数就称其为泛型

    泛型的优点

            像ArrayList、Vector底层都是维护了一个Object对象数组,如果不加以规范,那么,加入本意是想向集合中存入一个Student学生对象,但是在实际存储时候,放入了一个Integer整型元素,那么,程序也是可执行的。但是,在后续进行处理时,由于Integer整型元素不存在Student学生的属性,那么,就可能导致程序抛出异常。

            而使用了泛型之后,那么,在定义集合时,就可以指定要存储的元素类型,这样,在实际调用add()方法存储元素时,编译器就会自动进行检查,并提示一些错误元素(例如:Integer整型元素)的的添加操作。

             同时,我们称用来指定泛型的尖括号(<>)为“钻石运算符”,会根据变量声明包含的泛型类型,自动进行类型推断。

    自定义泛型类型

            要注意一点,泛型类型对应的一定是引用数据类型。

    自定义泛型类/接口

            如下为自定义泛型类和使用的示例代码,

    1. import java.util.Arrays;
    2. public class GenericTest_Class {
    3. //properties
    4. private String name;
    5. private Integer age;
    6. private E gender;
    7. //methods
    8. public void a(E gender){
    9. System.out.println(gender);
    10. }
    11. public void b(E[] m){
    12. System.out.println(Arrays.toString(m));
    13. }
    14. }
    15. //子类继承父类时,可以指定泛型类型为Integer-那么在实例化时,默认就是Integer类型
    16. class SubGenericTest_Class extends GenericTest_Class{
    17. }
    18. class Test{
    19. public static void main(String[] args) {
    20. //实例化子类
    21. SubGenericTest_Class subGenericTest_class = new SubGenericTest_Class();
    22. subGenericTest_class.a(65);
    23. //不指定泛型
    24. GenericTest_Class genericTest_class = new GenericTest_Class();
    25. genericTest_class.a(15);
    26. genericTest_class.b(new String[]{"a","b","c"});
    27. //指定泛型
    28. GenericTest_Class genericTest_class1 = new GenericTest_Class<>();
    29. genericTest_class1.a(15);
    30. genericTest_class1.b(new Integer[]{1,2,3});
    31. }
    32. }

    泛型使用细节

            【1】一个泛型类,可以指定多个泛型参数,

             【2】泛型类的构造器不能添加泛型参数。

             【3】不同的泛型的引用类型不可以相互赋值,

             【4】泛型擦除:如果不指定泛型类型,那么默认就是Object类型。

             【5】泛型类的静态方法参数中,无法使用泛型参数。

             【6】不允许直接使用泛型类型创建数组,但是可以通过Object对象数组类型强转实现。

    自定义泛型方法

            泛型方法:并不是带泛型参数的方法就是泛型方法,要求如下:

    该方法的泛型的参数类型要和当前的类的泛型无关,直观地将:泛型方法的泛型参数应当和所在类的泛型参数类型不一样。

            示例如下,

    1. public class GenericTest_Class {
    2. //properties
    3. private String name;
    4. private Integer age;
    5. private E gender;
    6. //methods
    7. //不是泛型方法
    8. public void a(E gender){
    9. E[] es = (E[]) new Object[5];
    10. System.out.println(gender);
    11. }
    12. //不是泛型方法
    13. public void b(E[] m){
    14. System.out.println(Arrays.toString(m));
    15. }
    16. //是泛型方法-{T和当前类的泛型参数E不同}
    17. public void genericTest(T t){
    18. System.out.println(t);
    19. }
    20. }

             泛型方法的泛型参数类型是在方法被调用时,才确定下来的。

            对于静态方法,其定义形式只需要在泛型参数之前,添加static关键字即可。

     泛型与继承关系

    不存在继承关系的情况

            默认情况下,List与List之间是不存在继承关系的,两者实质上是并列的因为在底层,ArrayList维护的是一个Object类型的数组。例如:

     通配符与存在继承关系的情况

            虽然List与List之间不存在继承关系,但是通过使用通配符(?),我们可以让List变为List与List的父类

            如下,

             在Collection接口方法中,也使用到了通配符。

             使用了通配符之后,在调用get(index)方法获取元素时,建议使用Object类型的变量进行接收,避免出错。

    泛型受限

            通过使用泛型,可以指定集合参数的兼容性上限和兼容性下限。这种用法在集合的API方法中经常使用,如下图所示,

            示例代码如下,

    1. class Person{
    2. }
    3. class Teacher extends Person{
    4. }
    5. public class FanXing_Limit {
    6. //properties
    7. //methods
    8. public static void main(String[] args) {
    9. List objects = new ArrayList<>();
    10. List people = new ArrayList<>();
    11. List teachers = new ArrayList<>();
    12. //指定泛型的上限-表示:元素只能是Person及其子类的对象
    13. Listextends Person> list_upper = null;
    14. list_upper = people;
    15. list_upper = teachers;
    16. //指定泛型的下限-表示:元素元素可以存放Person及其父类的对象
    17. Listsuper Person> list_lower = null;
    18. list_lower = people;
    19. list_lower = objects;
    20. }
    21. }
    22. 集合概述

      集合的作用与存储内容

              集合是一个容器,可以用来容纳多个对象。在实际开发中,最经典的应用场景就是:数据库相关的操作。

              例如:连接数据库之后,从数据库中查询n条记录,将其封装为n个Java对象;然后,用一个集合对象来存放这n个Java对象(的引用);最终将这个集合返回给前端,由前端进行数据解析、页面渲染。

              值得一提的是:Java集合只能用于存储引用数据类型,更准确的说——集合中存储的是对N个Java对象内存地址的引用

      集合与数据结构

              不同的集合,底层是对不同的数据结构的具体实现。

              例如:ArrayList底层是对动态数组的实现,LinkedList底层是对链表的实现,TreeSet底层是对二叉树的实现。

              但是这并非要求我们一开始就去精通数据结构,只需要我们掌握:在什么样的情况下,选择合适的集合类型去使用即可

      集合:Collection接口的继承结构

              Collection接口的继承结构如下图所示,

      Collection接口继承结构

      集合:Map接口继承结构

              Map接口的继承结构如下图所示,

      Map接口的继承结构

      Collection接口

              Collection接口及其子类都可以获取一个Iterator迭代器对象,用以完成集合元素的遍历操作。

      Collection接口的常用方法

              Collection接口提供了与增加、删除、查询相关的方法。

      增加方法

              用于实现增加单个元素、通过集合增加一批元素,返回值都是boolean类型,

      【1】boolean   add(E e);

      【2】boolean   addAll(Collection col);

      删除方法

              用于删除单个元素、部分元素、所有元素、满足指定条件的元素,返回值为void或者boolean类型,

      【1】删除所有:void   clear();

      【2】删除单个元素:boolean   remove(E   e);

      【3】删除coll参数包含的部分元素:boolean   removeAll(Collection   coll);

      【4】删除满足条件的元素:boolean   removeIf(PredicateE> filter);

              其中:removeAll()方法,可以实现集合的求差运算;removeIf()方法,可以实现集合元素的筛选操作(详见:《JDK8-Predicate接口使用举例》)。

      查询方法

              用于判断某个/某几个元素是否存在、集合是否为空、集合元素个数的方法,

      【1】判断某个元素是否存在:boolean contains(Object o);

      【2】判断某几个元素是否存在:boolean containsAll(Collection coll);

      【3】集合是否为空:boolean  isEmpty();

      【4】集合中元素个数:int size();

      迭代器

              用于获取一个迭代器对象,进行集合元素的遍历操作。

      Iterator  iterator();

              使用增强for循环迭代器对象遍历Collection集合对象的示例代码如下,

      1. public static void main(String[] args) {
      2. Collection collection = new ArrayList();
      3. //调用方法
      4. collection.add(3);
      5. collection.add(4);
      6. collection.add(7);
      7. collection.add(1);
      8. collection.add(10);
      9. //普通for循环-失败-[Collection接口本身没有提供通过下标获取元素的方法]
      10. //方式1:增强for循环
      11. for(Object integer:collection){
      12. System.out.println(integer);
      13. }
      14. //方式2:迭代器遍历
      15. Iterator iterator = collection.iterator();
      16. while (iterator.hasNext()) {
      17. Object next = iterator.next();
      18. System.out.println(next);
      19. }
      20. }

              迭代器执行原理:代码在执行时,通过迭代器Iterator接口对象的hasNext()方法,判断当前指针的下一个位置是否存在元素;如果存在,那么就进入while()循环,并通过next()移动指针,获取下一个位置的元素;如果不存在,那么就结束while()循环,表示Collection集合中的元素已经遍历结束。

      List接口的常用方法

             List集合相比Collection父接口,多出来的成员方法都是和index索引/下标操作元素相关的。

      public interface List extends Collection 

              如上所示,List接口继承了Collection父接口,那么,Collection借口中包含的成员方法,在List接口中也存在。

              以下,我们主要关注List接口中拓展出来的特有方法。

      添加元素的方法

              通过索引添加集合元素的方法。

      【1】添加元素到指定的索引位置:void   add(int  index,E e);

      【2】替换指定索引位置的元素:E  set(int  index,E  element);

      获取元素的方法

              通过索引获取集合元素的方法。 

      Object get(int  index);

      删除元素的方法

              通过索引删除集合元素。

      boolean remove(int  index);

           关于List集合的示例代码如下,

      1. public static void main(String[] args) {
      2. List list = new ArrayList();
      3. list.add(3);
      4. list.add(1);
      5. list.add(5);
      6. list.add(2);
      7. //添加元素
      8. list.add(4,10);
      9. //替换元素
      10. list.set(0,100);
      11. //删除集合元素
      12. list.remove(2);//删除下标为2的元素
      13. //遍历集合元素
      14. //普通for循环
      15. System.out.println("普通for循环");
      16. for (int i = 0; i < list.size(); i++) {
      17. System.out.println(list.get(i));
      18. }
      19. //增强for循环
      20. System.out.println("增强for循环");
      21. for (Object o : list) {
      22. System.out.println(o);
      23. }
      24. //迭代器遍历
      25. System.out.println("迭代器遍历");
      26. Iterator iterator = list.iterator();
      27. while (iterator.hasNext()) {
      28. Object next = iterator.next();
      29. System.out.println(next);
      30. }
      31. }

       ArrayList类

              ArrayList类是List接口的实现子类,常用方法和List父接口基本一致。

              ArrayList底层是通过动态数组实现的,但是在jdk1.7和jdk1.8的底层实现有所不同。

      jdk1.7源码分析

           【1】在设计结构上, ArrayList继承了AbstractList抽象父类,AbstractList抽象父类实现了List接口

           【2】在构造器初始化上,ArrayList在初始化时,为内部维护的elementData对象数组分配了长度为10的内存空间。

               初始化时的内存结构如下图所示,

               【3】在添加元素、调用add()方法时,会向内部维护的elementData对象数组中添加元素,添加成功时,返回true。

               【4】在初始数组已满、需要进行数组扩容操作时,会自动进行1.5倍的扩容操作。

      jdk1.8源码分析

              【1】JDK1.8在底层设计上ArrayList底层依旧维护了两个重要成员属性:对象数组elementData和size元素个数

               【2】在构造器初始化时,直接给elementData对象进行赋值(引用一个空的数组,长度为0,所占内存空间为0)。

               【3】在添加元素、调用add()方法时,才会执行自动分配长度为10的数组内存空间操作。

                【4】在初始数组已满、需要进行数组扩容操作时,仍旧会执行1.5倍的扩容操作。

       Vector类

               Vector是List接口的实现子类,它是线程安全的。

              【1】在底层设计上,内部维护了一个Object对象数组elementData用于存储元素、一个elementCount用于存储元素的实际个数。

              【2】在构造器初始化时,会直接提供10个长度的内存空间。

              【3】在添加元素上,被调用的add()方法被synchronized关键字修饰,是线程安全的方法。

              【4】在elementData数组已满,进行扩容时, 执行2倍的容量扩充操作。

      LinkedList类

              LinkedList类底层是对链表数据结构的实现。

      LinkedList类的常用方法

              LinkedList提供了大量操作头尾元素的方法。

      【1】增加操作

      void  addFirst(E e):在列表开头添加元素;

      void  addLast(E e):在列表的结尾添加元素。

      boolean  offer(E e):将指定元素添加为列表的的尾部(最后一个元素)

      boolean offerFirst(E e):在此列表的前面插入指定的元素

      boolean offerLast(E e):在该列表的末尾插入指定的元素

      void  push(E e):将元素推入堆栈中

      【2】获取/查询操作

      E  element():获取第一个元素(但不删除);

      E  getFirst():获取头元素

      E  getLast():获取尾元素

      int indexOf(Object obj):返回此元素的第一个索引值;找不到则返回-1

      E  peek():查询但不删除此列表的头

      E  peekFirst():查询但不删除此列表的第一个元素,如果此列表为空,则返回 null

      E  peekLast():查询但不删除此列表的尾部元素,如果此列表为空,则返回 null

      【3】 删除操作

      E  poll():查询并删除此列表的头

      E  pollFirst():查询并删除此列表的第一个元素,如果此列表为空,则返回 null

      E  pollLast():查询并删除此列表的尾部元素,如果此列表为空,则返回 null

      E  pop():从堆栈中删除一个元素

      removeFirst():删除头元素

      removeLast():删除尾元素

              示例代码如下,

      1. public static void main(String[] args) {
      2. LinkedList linkedList = new LinkedList<>();
      3. //添加元素
      4. linkedList.addFirst(3);
      5. linkedList.addFirst(2);
      6. linkedList.addFirst(1);
      7. System.out.println("*******************");
      8. Integer pop = linkedList.pop();
      9. System.out.println(pop);
      10. linkedList.push(123);
      11. linkedList.addLast(321);
      12. linkedList.offerFirst(456);
      13. linkedList.offerLast(654);
      14. System.out.println(linkedList);
      15. //删除元素
      16. Integer poll = linkedList.poll();
      17. System.out.println(poll);
      18. System.out.println(linkedList.pollFirst());
      19. System.out.println(linkedList.pollLast());
      20. System.out.println(linkedList.removeFirst());
      21. System.out.println(linkedList.removeLast());
      22. System.out.println("*******************");
      23. //遍历LinkedList
      24. System.out.println(linkedList);
      25. }

               其中:poll()方法是从jdk1.6开始的,删除元素时,列表为空也不会抛出异常,而是返回一个null;但是像之前的removeFirst()、removeLast()方法,列表为空时删除的操作会导致抛出异常。

              因此,更建议使用从jdk1.6开始提供的poll()、pollFirst()、pollLast()方法。

      LinkedList的遍历

      1. //for循环遍历
      2. for (int i = 0; i < linkedList.size(); i++) {
      3. System.out.println(linkedList.get(i));
      4. }
      5. //迭代器遍历1
      6. Iterator iterator1 = linkedList.iterator();
      7. while (iterator1.hasNext()) {
      8. System.out.println(iterator1.next());
      9. }
      10. //迭代器遍历2——更建议此种迭代器方式,因为此时Iterator迭代器存在于局部作用域中;for循环结束之后,迭代器对象自动销毁;更加节省内存.
      11. for (Iterator iterator = linkedList.iterator();iterator.hasNext();){
      12. System.out.println(iterator.next());
      13. }

      LinkedList底层原理

              LinkedList底层是对线性表-双向链表数据结构的实现。图示如下,

      模拟LinkedList的源码

              此处模拟LinkedList的添加add()、获取长度getSize()、获取元素get()三个方法,示例代码如下,

      1. public class MyLinkedList {
      2. //properties
      3. private Node first;//首结点
      4. private Node last;//尾结点
      5. private int count;//元素数量
      6. //methods
      7. public MyLinkedList() {
      8. this.first = this.last = null;//链表为空,首尾结点均为空
      9. this.count = 0;//总元素为0个
      10. }
      11. //添加元素
      12. public void add(Object data){
      13. if (this.first == null){
      14. // 添加第1个结点
      15. Node node = new Node();
      16. node.setPre(null);
      17. node.setData(data);
      18. node.setNext(null);
      19. //双指针指向当前结点
      20. this.first = node;
      21. this.last = node;
      22. }else {
      23. // 添加第n>=2个结点
      24. Node node = new Node();
      25. node.setPre(this.last);//上一个节点一定是最后一个结点
      26. node.setData(data);
      27. node.setNext(null);
      28. //前一个节点的尾指针指向当前结点
      29. this.last.setNext(node);
      30. //修改双指针指向
      31. this.last = node;
      32. }
      33. //长度自增
      34. this.count+=1;
      35. }
      36. //获取集合中元素的数量
      37. public int getSize() {
      38. return count;
      39. }
      40. //通过下标获取元素
      41. public Object get(int index){
      42. Node n = this.first;
      43. //获取下标为index的元素
      44. for (int i = 0; i < index; i++) {
      45. n = n.getNext();
      46. }
      47. return n.getData();
      48. }
      49. }
      50. class Test{
      51. public static void main(String[] args) {
      52. MyLinkedList list = new MyLinkedList();
      53. list.add("aa");
      54. list.add("bb");
      55. list.add("cc");
      56. System.out.println(list.getSize());
      57. System.out.println(list.get(2));
      58. }
      59. }

       LinkedList-JDK18源码

              JDK1.8的LinkedList链表源码片段摘取如下,

      Node结点定义
      LinkedList链表定义
      添加元素方法:add(E e)
      根据下标获取元素-get(int)

      ※面试题:Iterator()、Iterator和Iterable关系

               (1)Iterable接口是Collection的父接口,提供了iterator()的抽象方法,可用于返回一个Iterator迭代器对象。

               (2)Iterable接口的抽象方法iterator()方法,由具体的子类实现,例如:ArrayList子类就实现了这个方法,

              此处,Itr类是ArrayList的内部类,对Iterator接口进行了实现,主要是对hasNext()、next()方法进行了具体实现,

      1. /**
      2. * An optimized version of AbstractList.Itr
      3. */
      4. private class Itr implements Iterator {
      5. int cursor; // index of next element to return
      6. int lastRet = -1; // index of last element returned; -1 if no such
      7. int expectedModCount = modCount;
      8. public boolean hasNext() {
      9. return cursor != size;
      10. }
      11. @SuppressWarnings("unchecked")
      12. public E next() {
      13. checkForComodification();
      14. int i = cursor;
      15. if (i >= size)
      16. throw new NoSuchElementException();
      17. Object[] elementData = ArrayList.this.elementData;
      18. if (i >= elementData.length)
      19. throw new ConcurrentModificationException();
      20. cursor = i + 1;
      21. return (E) elementData[lastRet = i];
      22. }
      23. public void remove() {
      24. if (lastRet < 0)
      25. throw new IllegalStateException();
      26. checkForComodification();
      27. try {
      28. ArrayList.this.remove(lastRet);
      29. cursor = lastRet;
      30. lastRet = -1;
      31. expectedModCount = modCount;
      32. } catch (IndexOutOfBoundsException ex) {
      33. throw new ConcurrentModificationException();
      34. }
      35. }
      36. @Override
      37. @SuppressWarnings("unchecked")
      38. public void forEachRemaining(Consumersuper E> consumer) {
      39. Objects.requireNonNull(consumer);
      40. final int size = ArrayList.this.size;
      41. int i = cursor;
      42. if (i >= size) {
      43. return;
      44. }
      45. final Object[] elementData = ArrayList.this.elementData;
      46. if (i >= elementData.length) {
      47. throw new ConcurrentModificationException();
      48. }
      49. while (i != size && modCount == expectedModCount) {
      50. consumer.accept((E) elementData[i++]);
      51. }
      52. // update once at end of iteration to reduce heap write traffic
      53. cursor = i;
      54. lastRet = i - 1;
      55. checkForComodification();
      56. }
      57. final void checkForComodification() {
      58. if (modCount != expectedModCount)
      59. throw new ConcurrentModificationException();
      60. }
      61. }

               (3)Iterator接口提供了hasNext()、next()方法,用于实现集合对象的遍历操作,

      ※ListIterator迭代器

              ListIterator迭代器允许程序员沿着任一方向遍历List列表。

               为什么要引入ListIterator这个迭代器呢?

              原因:在使用Iterator迭代器遍历List集合时,无法同时调用List集合的方法,执行修改元素的操作。例如:添加、删除、修改等都是不允许的。但是,ListIterator迭代器,提供了add()、set()方法,可以执行添加和设置操作。

              示例代码如下,

      1. public static void main(String[] args) {
      2. List list = new ArrayList<>();
      3. list.add("aa");
      4. list.add("bb");
      5. list.add("cc");
      6. list.add("dd");
      7. //使用迭代器遍历在cc之后添加一个元素hh
      8. ListIterator listIterator = list.listIterator();
      9. while (listIterator.hasNext()) {
      10. if ("cc".equals(listIterator.next())) {
      11. listIterator.add("hh");
      12. }
      13. }
      14. //打印操作
      15. System.out.println(list);
      16. //逆向遍历
      17. ListIterator iterator = list.listIterator(list.size());//获取cursor==size位置处的游标
      18. while (iterator.hasPrevious()) {
      19. System.out.println(iterator.previous());
      20. }
      21. }

       Set接口

              List接口中的元素是有序的、可重复的,但是Set接口中的元素则是无序的、唯一的

      Set接口的常用方法

              Set接口中的常用方法和List接口的基本相同,但是Set——集合之中,是没有和索引相关的方法的。这意味着,我们无法使用for()循环来遍历Set接口中的元素,而只能使用增强for循环或者迭代器来遍历Set集合中的元素。

      HashSet类

      HashSet:成员方法

              HashSet类中的方法如下,

              示例代码如下,HashSet继承了Set之中元素的不可重复性。我们执行了两次添加28的操作,但是遍历结果中,只出现了一个28。

      1. //methods
      2. public static void main(String[] args) {
      3. HashSet hashSet = new HashSet<>();
      4. hashSet.add(19);
      5. hashSet.add(24);
      6. hashSet.add(28);
      7. hashSet.add(28);
      8. System.out.println("size="+hashSet.size());
      9. //遍历HashSet
      10. for (Iterator iterator = hashSet.iterator();iterator.hasNext();){
      11. System.out.println(iterator.next());
      12. }
      13. }

       ※HashSet底层原理与应用:集合元素去重

      1. public class Student {
      2. //properties
      3. private Integer age;
      4. private String name;
      5. //methods
      6. public Integer getAge() {
      7. return age;
      8. }
      9. public void setAge(Integer age) {
      10. this.age = age;
      11. }
      12. public String getName() {
      13. return name;
      14. }
      15. public void setName(String name) {
      16. this.name = name;
      17. }
      18. public Student() {
      19. }
      20. public Student(Integer age, String name) {
      21. this.age = age;
      22. this.name = name;
      23. }
      24. @Override
      25. public String toString() {
      26. return "Student{" +
      27. "age=" + age +
      28. ", name='" + name + '\'' +
      29. '}';
      30. }
      31. }

              例如:我们有一个List集合,存放了若干条Student记录,Student类的定义如上,提供了toString()方法。但是其中可能存在重复的元素,此时,就可以使用HashSet集合,来执行元素去重操作。

              此时,如果直接使用HashSet来存放元素,会发现,竟然可以存放两个属性相同的Student对象。

      1. public static void main(String[] args) {
      2. test_hashSet();
      3. }
      4. private static void test_hashSet(){
      5. HashSet hashSet = new HashSet<>();
      6. hashSet.add(new Student(19,"Tom"));
      7. hashSet.add(new Student(18,"Lily"));
      8. hashSet.add(new Student(20,"Linda"));
      9. hashSet.add(new Student(19,"Tom"));
      10. for ( Iterator iterator = hashSet.iterator();iterator.hasNext();){
      11. Student student = iterator.next();
      12. System.out.println(student.toString());
      13. }
      14. }

               上述现象与我们之前的理解,表面上看是“相悖”的,但其实是和HashSet的底层原理有关。

               HashSet底层的数据结构就是哈希表(散列结构),实质上是通过“数组+链表”的结构实现的。这里的链表就是用来存储散列函数结果值相同的一串元素。

               那么,如何判断两个是否需要延伸链表呢

              其实,在向底层数组存放元素时,如果当前位置index处已经有元素了,就调用equals()方法进行比较;如果比较结果为true,就直接舍弃;如果为false,就延伸出一个链表,用来存储下标位置相同,但是元素值不同的多个元素

              此时,我们就应当明白了,之前存储Student的HashSet集合中出现两个属性相同的Student元素的原因所在——Student类没有重写equals()方法。

              如下,我们为Student类添加equals()、hashCode()方法,再次运行相同的代码,就可以看到存放的Student记录都是唯一的。

      1. public class Student {
      2. //properties
      3. private Integer age;
      4. private String name;
      5. //methods
      6. public Integer getAge() {
      7. return age;
      8. }
      9. public void setAge(Integer age) {
      10. this.age = age;
      11. }
      12. public String getName() {
      13. return name;
      14. }
      15. public void setName(String name) {
      16. this.name = name;
      17. }
      18. public Student() {
      19. }
      20. public Student(Integer age, String name) {
      21. this.age = age;
      22. this.name = name;
      23. }
      24. @Override
      25. public String toString() {
      26. return "Student{" +
      27. "age=" + age +
      28. ", name='" + name + '\'' +
      29. '}';
      30. }
      31. @Override
      32. public boolean equals(Object o) {
      33. if (this == o) return true;
      34. if (o == null || getClass() != o.getClass()) return false;
      35. Student student = (Student) o;
      36. return Objects.equals(age, student.age) &&
      37. Objects.equals(name, student.name);
      38. }
      39. @Override
      40. public int hashCode() {
      41. return Objects.hash(age, name);
      42. }
      43. }

       LinkedHashSet类

              LinkedHashSet是HashSet的子类,通过LinkedHashSet子类,可以使得集合中的元素是有序的,即:按照元素添加顺序进行元素输出。示例代码如下,

      1. public static void main(String[] args) {
      2. HashSet hashSet = new LinkedHashSet<>();
      3. hashSet.add(new Student(19,"Tom"));
      4. hashSet.add(new Student(18,"Lily"));
      5. hashSet.add(new Student(20,"Linda"));
      6. hashSet.add(new Student(19,"Tom"));
      7. for (Iterator iterator = hashSet.iterator(); iterator.hasNext();){
      8. Student student = iterator.next();
      9. System.out.println(student.toString());
      10. }
      11. }

               LinkedHashSet底层实现依旧是哈希表,只是多了一个总的链表,通过这个链表,可以实现元素的有序存储和遍历操作

      ※比较器的使用

              基本数据类型的比较,可以通过对应包装类提供的compareTo(value)方法完成,该方法是通过implements接口Comparable获得的,并返回一个int类型的值,用于表示比较结果。

               ①如果当前对象/值等于参数param,返回0;

              ②如果当前对象/值大于参数param,返回正值;

              ③如果当前对象/值小于参数param,返回负值。

      内部比较器

              同样的,对于自定义数据类型,也可以通过实现Comparable接口,重写compareTo(value)方法,来进行对象之间的比较操作。

               以Student学生类为例,示例代码如下,

      1. public class Student implements Comparable{
      2. //properties
      3. private Integer age;
      4. private String name;
      5. //methods
      6. public Integer getAge() {
      7. return age;
      8. }
      9. public void setAge(Integer age) {
      10. this.age = age;
      11. }
      12. public String getName() {
      13. return name;
      14. }
      15. public void setName(String name) {
      16. this.name = name;
      17. }
      18. public Student() {
      19. }
      20. public Student(Integer age, String name) {
      21. this.age = age;
      22. this.name = name;
      23. }
      24. @Override
      25. public String toString() {
      26. return "Student{" +
      27. "age=" + age +
      28. ", name='" + name + '\'' +
      29. '}';
      30. }
      31. @Override
      32. public boolean equals(Object o) {
      33. if (this == o) return true;
      34. if (o == null || getClass() != o.getClass()) return false;
      35. Student student = (Student) o;
      36. return Objects.equals(age, student.age) &&
      37. Objects.equals(name, student.name);
      38. }
      39. @Override
      40. public int hashCode() {
      41. return Objects.hash(age, name);
      42. }
      43. @Override
      44. public int compareTo(Student o) {
      45. //按照年龄比较
      46. // return this.getAge() - o.getAge();
      47. //按照姓名比较
      48. return this.getName().compareTo(o.getName());
      49. }
      50. }

      外部比较器

              此外,通过额外编写比较类CompareStudent,实现Comparator接口,也可以实现Student自定义类型的比较操作。示例代码如下,

      1. //外部比较器
      2. class CompareStudent implements Comparator{
      3. @Override
      4. public int compare(Student o1, Student o2) {
      5. //按照年龄比较
      6. // return o1.getAge() - o2.getAge();
      7. //按照姓名比较
      8. return o1.getName().compareTo(o2.getName());
      9. }
      10. }

      比较器使用建议

              更建议使用外部比较器,基于多态,其扩展性更好。

      TreeSet类

               TreeSet底层是通过二叉树数据结构实现的,在存放集合元素时,遵循二叉排序树的规则;在遍历集合元素时,按照中序遍历(根结点->左子树结点->右子树节点)的方式进行,得到一个升序的元素序列

       存放Integer类型

              特点:元素唯一,不可重复;存放的时候是无序的,不会按照输入顺序存储;但是,遍历的时候是有序的,按照升序排列

      1. //methods
      2. public static void main(String[] args) {
      3. TreeSet treeSet = new TreeSet<>();
      4. treeSet.add(12);
      5. treeSet.add(3);
      6. treeSet.add(7);
      7. treeSet.add(9);
      8. treeSet.add(3);
      9. treeSet.add(16);
      10. System.out.println(treeSet);
      11. }

                   底层存储元素时,是按照二叉排序树的规则组织的,即:会对元素的值进行比较(内部比较器Comparable或者外部比较器Comparator),然后将其放入左子树或者右子树中,

       

       存放String类型

               由于String类型实现了内部比较器Comparable接口,因此,存入TreeSet集合中的String字符串也是有序的。

      1. private static void test_String(){
      2. TreeSet treeSet = new TreeSet<>();
      3. treeSet.add("absk");
      4. treeSet.add("pog");
      5. treeSet.add("udds");
      6. treeSet.add("psng");
      7. treeSet.add("bnsl");
      8. System.out.println(treeSet);
      9. }

       

       存放自定义数据类型

              对于自定义数据类型在TreeSet中的存放,为了遵循二叉排序树的规则,强烈建议为自定义数据类型提供内部比较器Comparable接口的实现,否则会报错。

      使用内部比较器

      如下所示,按照Student的姓名name字段进行排序,

      1. public class Student implements Comparable{
      2. //properties
      3. private Integer age;
      4. private String name;
      5. //methods
      6. public Integer getAge() {
      7. return age;
      8. }
      9. public void setAge(Integer age) {
      10. this.age = age;
      11. }
      12. public String getName() {
      13. return name;
      14. }
      15. public void setName(String name) {
      16. this.name = name;
      17. }
      18. public Student() {
      19. }
      20. public Student(Integer age, String name) {
      21. this.age = age;
      22. this.name = name;
      23. }
      24. @Override
      25. public String toString() {
      26. return "Student{" +
      27. "age=" + age +
      28. ", name='" + name + '\'' +
      29. '}';
      30. }
      31. @Override
      32. public boolean equals(Object o) {
      33. if (this == o) return true;
      34. if (o == null || getClass() != o.getClass()) return false;
      35. Student student = (Student) o;
      36. return Objects.equals(age, student.age) &&
      37. Objects.equals(name, student.name);
      38. }
      39. @Override
      40. public int hashCode() {
      41. return Objects.hash(age, name);
      42. }
      43. @Override
      44. public int compareTo(Student o) {
      45. //按照姓名比较
      46. return this.getName().compareTo(o.getName());
      47. }
      48. }

              使用TreeSet存放Student元素,如下所示,

      1. private static void test_Student(){
      2. TreeSet treeSet = new TreeSet<>();
      3. treeSet.add(new Student(12,"bnsl"));
      4. treeSet.add(new Student(31,"absk"));
      5. treeSet.add(new Student(14,"pog"));
      6. treeSet.add(new Student(51,"udds"));
      7. treeSet.add(new Student(18,"psng"));
      8. for (Student student : treeSet) {
      9. System.out.println(student);
      10. }
      11. }

       使用外部比较器

              如下所示,为Student类提供外部比较器,根据年龄age进行比较。

      1. //外部比较器
      2. class CompareStudent implements Comparator{
      3. @Override
      4. public int compare(Student o1, Student o2) {
      5. //按照年龄比较
      6. return o1.getAge().compareTo(o2.getAge());
      7. }
      8. }

               创建TreeSet集合对象时,将外部比较器实例作为参数传入即可,如下所示,

      1. private static void test_Student_1(){
      2. Comparator comparator = new CompareStudent();
      3. TreeSet treeSet = new TreeSet<>(comparator);//传入比较器参数
      4. treeSet.add(new Student(12,"bnsl"));
      5. treeSet.add(new Student(31,"absk"));
      6. treeSet.add(new Student(14,"pog"));
      7. treeSet.add(new Student(51,"udds"));
      8. treeSet.add(new Student(18,"psng"));
      9. for (Student student : treeSet) {
      10. System.out.println(student);
      11. }
      12. }

       

    23. 相关阅读:
      Java:性能优化细节31-45
      Java计算机毕业设计电费管理系统源码+系统+数据库+lw文档
      ASO优化之关于健身应用的优化策略
      安科瑞基于物联网技术的智能电力抄表服务平台-Susie 周
      东北大学acm暑期夏令营结构体
      十二条后端开发经验分享,纯干货
      LeetCode //C - 18. 4Sum
      vscode + gdb +gdbserver 远程调试Pg源码
      前缀和与树状数组(手撕算法篇)
      矩阵分析学习笔记(五):数域上矩阵的特征矩阵
    24. 原文地址:https://blog.csdn.net/weixin_43524214/article/details/127778553