javaSE集合框架(一)—— Collection接口

目录

​编辑

一、java集合框架概述

1.1 集合和数组的对比

1. 集合的出现

2. 数组的优点和弊端

3. 如何选择?

1.2 Collection和Map两种体系

二、Collection接口方法

1~5 :add()、size()、addAll()、isEmpty()、clear()

6、contains()

7、containsAll()

8~9 remove()、removeAll() 

10、retainAll()求两个集合的交集

 11、equals():判断两个集合的所有元素是否都相等

12、hashcode():返回当前对象的哈希值

 13、toArray():集合------->数组

 14、iterator():遍历集合元素

1. Iterator接口

2. iterator()方法以及迭代器常用方法hasNext()、next、remove()演示

3. 迭代器的执行原理

三、Collection子接口(一):List接口

3.1面试题大题  ArraryList、LinkedList、Vector的异同

3.2 ArrayList源码分析

3.2.1 JDK 7当中

3.2.2 JDK 8中的变化 

3.2.3 哪种好?

3.3 LinkedList源码分析

3.3.1 创建对象

3.3.2 add(E e):尾插法添加元素

3.3.3 remove()移除一个元素

3.3.4 add(int index,E e)在指定位置插入一个元素

3.4 Vector的源码分析

3.5 List接口中的常用方法

1~3、 add()、addAll()、get()

4~5、 indexOf()、lastIndexOf()

 6~7、remove()、set()

 8、subList()

9、3种遍历方式

3.6 List常用方法总结

四、Collection子接口(二):Set接口

4.1 HashSet、LinkedHashSet、TreeSet的异同

4.2 以HashSet为例讲解Set接口

4.2.1 怎么理解“无序不可重复”

4.2.2 HashSet的add过程

4.2.3 hashcode()方法和equals()方法的重写

4.3 LinkedHashSet

4.4 TreeSet

4.4.1 常用类的排序 

4.4.2 自定义User类的排序

五、面试题

1. 考查List的常用方法中remove有两种重载形式

2. 去除List中的重复数值

3. HashSet的考查


一、java集合框架概述

什么是集合框架?

一句话来说,就是【用java实现数据结构】,精辟!

1.1 集合和数组的对比

1. 集合的出现

        集合的出现是为了代替数组。but集合的源码用的也是数组。

2. 数组的优点和弊端

优点

        1)数组一旦定义好,其元素的类型也就确定了,只能操作指定类型的数据,更严格,这是一个优点。(泛型的出现解决了集合在这方面不严格的缺点)

弊端

(其实这些弊端在之前刷题的时候,对比vector和数组就已经看出来了)。

        1)一旦初始化后,数组的长度就确定了。令程序员难以把握数组的长度到底要初始化成多少

        2)数组中提供的方法非常有限。对于添加、删除、插入数据等操作只能自己去写非常不便,需要依次前移、后移。获取数组中实际元素个数的方法也米有。

        3)如果有的项目需要存储不可重复的元素,数组表示我太难了

更精确的说,在集合框架中,用List去代替数组。

3. 如何选择?

        当我们调用的API形参或者返回值指定了用数组,就只能用数组,一旦可以自己写,一定用集合。

1.2 Collection和Map两种体系

  • |---------Collection接口:存储单列数据
    • |---------List接口:有序可重复,“动态数组”,数组的进化版
      • |---------ArraryList 、LinkedList 、Vector
    • |---------Set接口:无序不可重复,“高中讲的集合”
      • |---------HashSet 、LinkedList 、 TreeSet
  • |---------Map接口:存储双列数据,“key-value”。“高中学的函数”,2个不同的key可以对应同一个value,但一个key只能有一个value。
    • |---------HashMap 、LinkedHashMap 、TreeMap、 Hashtable 、Properties

二、Collection接口方法

        类似于Object类是所有类的祖先类,了解object类的方法就能了解所有类的通用方法。类似地,我们先看Collcetion接口的通用操作。Collection有15个方法,这里都讲一遍。由于接口不能实例化,这里我们利用多态的思想。

PS: 学起来很熟悉,发现很多API和C++中容器vector的API一样,比如clear()、size()、isEmpyt()。

1~5 :add()、size()、addAll()、isEmpty()、clear()

        要求添加进去对象的所在类重写equals()方法 。像下面的String类、Date类、包装类已经重写过了。自己定义的Person类才需要重写。至于为什么,下面讲contains方法的时候再说。

        Collection coll = new ArrayList();
        
        //1)add(E e):将对象元素e添加到集合中
        coll.add("july");
        coll.add("27");
        coll.add(123); //自动装箱,包装类
        coll.add(new Date());
        coll.add(new Person("hh",18);  //要求Person类重写Object类的equals方法
        
        //2)size():获取集合中元素的个数
        System.out.println(coll.size()); //4

        //3)addAll(Collection c) :把另一个集合中的元素添加进来
        Collection coll2 = new LinkedList();
        coll2.add("more time on stu");
        coll2.add(456);
        coll.addAll(coll2);  //可以把LinkedList的元素添加到ArrayList里
        System.out.println(coll.size());  //6

        //4)isEmpty():判断集合当中是为空(size()==0?)
        System.out.println(coll.isEmpty());  //false

        //5)clear():清空集合元素
        coll.clear();
        System.out.println(coll.isEmpty());//true

6、contains()

首先定义一个person类

public class Person {
    private String name;
    private int age;

    Person(){

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

}

1)contains函数的作用:判断两个对象的内容是否相等。拿字符串“july”来看,我们调用contains的时候重新new了一个对象,如果contains的目的是比较对象的地址是否相同,那么显然应该输出false。

2)contains函数的内部:调用对象所在类的equals函数

 下面这张图是contain函数内部的源码

3)Object类和其他类equals函数的区别

  • Object类的equals函数:用“==”判断两个对象的地址是否相同。

        ​​​​​ 

  • 其他类重写Object类的equals函数:判断两个对象的内容是否相同。以Date类为例:

     

        String类和Date类重写过object类的equals函数,意义为比较内容是否相等。之所以Person类会返回false是因为我们自定义的Person类没有重写equals方法,默认使用的object的equals方法。 

        Collection coll = new ArrayList();

        //1)add(E e):将对象元素e添加到集合中
        coll.add("july");
        coll.add("27");
        coll.add(new Date(123456));
        coll.add(new Person("jj",22));

        //6)contains(object obj):判断集合中是否存在对象obj
        System.out.println(coll.contains(new String("july")));  //true,比较内容是否相等
        System.out.println(coll.contains(new Date(123456))); //true,比较内容是否相等
        System.out.println(coll.contains(new Person("jj",22)));  //false

 所以正确的做法是:在Person类里重写equals方法(Alt+Insert快捷键):

package com.example.Collection;

import java.util.Objects;

public class Person {
    private String name;
    private int age;

    Person(){

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

    @Override
    public boolean equals(Object o) {
        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);
    }
    
}

这样Person对内容的比较就是true了:

        Collection coll = new ArrayList();
        coll.add(new Person("jj",22));
        System.out.println(coll.contains(new Person("jj",22)));  //true

7、containsAll()

见名知意

        Collection coll = new ArrayList();

        //1)add(E e):将对象元素e添加到集合中
        coll.add(new Date(123456));
        coll.add(new Person("jj",22));

        //7)containsAll(Collection c)
        Collection coll2 = new ArrayList();
        coll2.add(new Date(123456));
        coll2.add(new Person("jj",22));
        System.out.println(coll.containsAll(coll2)); //true

8~9 remove()、removeAll() 

remove()方法需要注意的点:如果试图移除一个本来就不存在的元素,remove函数会返回false。

removeAll()方法:coll.removeAll(coll2)作用是从coll中移除coll和coll2的并集。

        //1)add(E e):将对象元素e添加到集合中
        coll.add("july");
        coll.add("27");
        coll.add(123456);
        coll.add(new Date(123456));
        coll.add(new Person("jj",22));

        //8)remove(object o)
        System.out.println(coll.remove(123456));  //true,remove返回Boolean
        System.out.println(coll);

        //如果移除一个不存在的呢?
        System.out.println(coll.remove(2022));//false

        //9)removeAll(Collection c)
        Collection coll2 = new ArrayList();
        coll2.add("july");
        coll2.add("28");
        coll2.add(2022);

        System.out.println(coll.removeAll(coll2));  //true
        System.out.println(coll);  //coll和colle2有交集,只从coll中移除交集中的部分

10、retainAll()求两个集合的交集

注意:coll.retainAll(coll2),求coll和coll2的交集,并直接赋值给coll

        Collection coll = new ArrayList();

        //1)add(E e):将对象元素e添加到集合中
        coll.add("july");
        coll.add("27");
        coll.add(123456);
        coll.add(new Date(123456));
        coll.add(new Person("jj",22));

        Collection coll2 = new ArrayList();
        coll2.add("july");
        coll2.add("28");
        coll2.add(2022);

        //10)retainAll(Collection c),求两个集合的交集,并直接赋给调用这个函数的coll
        boolean b = coll.retainAll(coll2);
        System.out.println(coll);  // 输出[july]

 11、equals():判断两个集合的所有元素是否都相等

注意:如果是List的话,还需要元素的顺序相等才能返回true;如果是set的话只需要元素形同就能返回true;

 这里有个疑问:改成了HashSet以后反而输出false。经过排查发现,是两个Person实例不equal的原因,可是Person类重写equals方法用来比较内容了呀。

        Collection coll = new HashSet();
        //1)add(E e):将对象元素e添加到集合中
        coll.add("july");
        coll.add("28");
        coll.add(new Person("qiqi",22));

        Collection coll2 = new HashSet();
        //1)add(E e):将对象元素e添加到集合中
        coll2.add("july");
        coll2.add("28");
        coll2.add(new Person("qiqi",22));

        //11)equals(Object obj) 判断两个集合的所有元素是否相同
        System.out.println(coll.equals(coll2));  //false!!!

将这两个集合对象打印输出后,发现Person存储的是地址? 

我们对equals方法进行进一步探究,发现本质上调用的是containsAll方法,

containsAll调用的是contains方法,而contains本质上就是equals

 

 而且我们用Date类再去测试,输出的是true!

 Collection<Date> dates = new HashSet<>();
        dates.add(new Date(123));
        dates.add(new Date(12345));

        Collection<Date> dates2 = new HashSet<>();
        dates2.add(new Date(123));
        dates2.add(new Date(12345));

        System.out.println(dates.equals(dates2));  //true
        System.out.println(dates); //[Thu Jan 01 08:00:12 CST 1970, Thu Jan 01 08:00:00 CST 1970]
        System.out.println(dates2);//[Thu Jan 01 08:00:12 CST 1970, Thu Jan 01 08:00:00 CST 1970]

12、hashcode():返回当前对象的哈希值

System.out.println(coll.hashcode());

 13、toArray():集合------->数组

注意:返回的是Object类的数组

    Collection coll = new ArrayList();
        coll.add("july");
        coll.add("28");
        coll.add(123);
        coll.add(false);
        coll.add(new Person("qiqi",22));

        //13)toArray():集合----->数组
        Object[] obj = coll.toArray();
        for(int i=0;i<obj.length;i++){
            System.out.println(obj[i]);
        }

        //上面的toArray用法不正规(因为返回的是Object类),正规写法应该指定类型
        //------->变为一维数组的写法
        List<String> list = new ArrayList();
        list.add("2026");
        list.add("0426");
        
        String[] result = list.toArray(new String[0]);
        //或者
        String[] result = list.toArray(new String[list.size()]);
   
        //------->变为二维数组的写法
        List<int[]> list = new ArrayList<>();
        list.add(new int[]{1,2});
        list.add(new int[]{3,4});

        int[][] result = list.toArray(new int[0][]);
        //或者
        int[][] result = list.toArray(new int[list.size()][]);
        

输出

july
28
123
false
com.example.Collection.Person@78e03bb5

奇怪的是本应该输出Person的内容的,就像这样:

 而我输出的是Person的地址??

拓展:数组------->集合,调用Arrays类的静态方法asList()

asList的坑

坑(1):使用asList得到的list不能对其进行remove/add/clear操作。因为Arrays.asList() 方法返回的并不是 java.util.ArrayList ,而是 java.util.Arrays 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法

 List myList = Arrays.asList(1, 2, 3);
myList.add(4);//运行时报错:UnsupportedOperationException
myList.remove(1);//运行时报错:UnsupportedOperationException
myList.clear();//运行时报错:UnsupportedOperationException

坑(2):如上图所示,如果给asList传入的是int []数组的话,会把整个数组当成list中的一个元素。如果我们的需求是把数组中的每个元素变成list中的一个元素的话,就要用包装类数组Integer[] 

 14、iterator():遍历集合元素

        首先思考,遍历数组的时候我们用的是数组下标,那集合(List除外,List有下标index)没有下标如何遍历呢?我们可以用迭代器去模拟“数组下标”的作用。

        这个方法返回的是Iterator接口的实例,因为这个Iterator接口比较重要,所以要先学一下。

这里依然存疑,接口不能定义实例,这里内部是如何实现的呢

1. Iterator接口

        1)Iterator对象称为迭代器(设计模式的一种),用于遍历Collcetion中的元素,不用于迭代Map中的元素,迭代器本身并不具备承装对象的能力,它只是一个游标。

        2)

        3)Iterator接口有4个方法:        

  • hasNext()
  • next():取下一个元素。
    • 当Iterator对象(游标)被使用过了,我们又想从头遍历时,要重新创建一个Iterator对象
  • remove():在遍历的过程中帮助我们删除不想要的元素。
    • 必须要在使用next()让游标下移之后使用remove(),这样游标(指针)才指向我们想删除的元素
    • 使用迭代器进行remove集合的元素,不同于直接调用集合的remove方法

        4)异常:NoSuchElementException:当我们没有使用hasNext去判断是否有下一个元素而直接用next去取,取完所有元素再次取时会报NoSuchElementException这个异常

2. iterator()方法以及迭代器常用方法hasNext()、next、remove()演示

        Collection coll = new ArrayList();
        //1)add(E e):将对象元素e添加到集合中
        coll.add("july");
        coll.add("28");
        coll.add(false);
        coll.add(new Person("qiqi",22));
        
        //14)iterator():遍历集合内的元素
        Iterator i = coll.iterator();
        while(i.hasNext()){
            System.out.println(i.next());
        }

输出

july
28
false
com.example.Collection.Person@78e03bb5 

下面演示一下迭代器的remove方法(不常用)。我们用remove方法删掉了集合中的布尔对象

         Collection coll = new ArrayList();
        //1)add(E e):将对象元素e添加到集合中
        coll.add("july");
        coll.add("28");
        coll.add(false);
        coll.add(true);
        coll.add(true);
        coll.add(new Person("qiqi",22));

        //14)iterator():遍历集合内的元素
        Iterator i = coll.iterator();
        while(i.hasNext()){
            Object obj = i.next(); //多态,所有类型的对象都上转为Object类
            if(obj instanceof Boolean)
                i.remove();
        }

        //输出
        i = coll.iterator();  //注意这里要重新获取一遍游标!!因为上次遍历完游标指向底部了
        while(i.hasNext()){
            System.out.println(i.next());
        }

 输出(可以看到true和false被删掉了)

july
28
com.example.Collection.Person@78e03bb5

3. 迭代器的执行原理

注意:迭代器生成后,默认游标在第一个元素之前

三、Collection子接口(一):List接口

3.1面试题大题  ArraryList、LinkedList、(Vector)的异同

PS:一般不问vector

        同:都实现了List接口,都是存储有序可重复的数据

        

        |------ ArrayList: 最常用,是List接口的主要实现类(只要是给有序可重复的数据选一个实现类,一般就是ArrayList);线程不安全,效率高(不安全指的是,多线程并发访问list);底层使用Object []elementData存储;

        |------ LinkedList:底层用双向链表存储,所以对于频繁的插入和删除操作效率比ArrayList高

        |------ Vector: List最古老的实现类(老臣不受待见);线程安全,效率低;底层使用Object []elementData存储;

        然后再谈源码分析

问:ArraryList、LinkedList的不同之处

1)是否常用。Arraylist是List接口的主要实现类,比LinkedList要常用

2)底层实现。ArrayList底层是Object[]数组,LinkedList底层是内部类Node组成的双向链表

3)增删改查插方面的比较 (这才是最重要的区别,也是为什么用ArrayList不够,还要开发一个LinkedList的原因) 。:如果是在末尾增加一个元素,ArrayList和linkedlist的时间一样。插、删:如果在中间位置增加、删除一个元素,ArrayList会发生数组的移位,linkedlist效率更高。查找:如果是遍历,二者效率一样,如果指定下标查找当然是ArrayList的数组O(1)查到,而linkedlist需要遍历。

3.2 ArrayList源码分析

3.2.1 JDK 7当中

ArrayList的源码和stringbuffer/stringbuilder的源码很像,因为他们底层都是用数组去存储的。

那么ArrayList如何实现可变长度呢? 和StringBuffer也一样。

当我们调用ArrayList的空参构造函数时,默认创建容量为10的底层数组(和StringBuffer默认创建大小为16的一样)

ArrayList list = bew ArrayList(); //容量为10的Object[] emelentData

 如果我们一直执行add操作,导致底层数组elementData的容量不够,

list.add(1);//添加第1个元素
list.add(2);//添加第2个元素
list.add(3);//添加第3个元素
...
list.add(11);//添加第11个元素超过了数组大小怎么办呢?

内部就要进行扩容,新建一个容量为原来1.5倍(stringbuffer是2倍+2)的新数组,然后把旧数组的值一个个拷贝过去。

 同样,为了避免频繁的扩容(创建新数组和拷贝都浪费时间)降低代码的效率,开发中建议使用有参构造函数,由程序员直接指定数组的大小。

ArrayList list = new ArrayList(100);

3.2.2 JDK 8中的变化 

当我们调用ArrayList的空参构造函数时,默认创建一个空数组。 

ArrayList list = bew ArrayList(); /创建空数组Object[] emelentData ={}

当执行第一次add操作时,底层才创建了长度为10的数组

list.add(123);

 后续扩容的操作与JDK 一样。

3.2.3 哪种好?

肯定JDK 8好,JDK 8这种延长了数组的创建时间,节省内存。

JDK 7的方式类似于单例的“饿汉模式”,JDK 8类似于“懒汉模式”。

为什么说节省了内存? 按照JDK 7,数组在new Arraylist时就要创建好,如果有很多数组,一次性就会占用很大内存。JDK 8这种相当于在运行的时候再创建数组。

3.3 LinkedList源码分析

PS: LinkedList的源码非常好理解,不用特意去记任何东西,因为底层采用的双向链表,源码的所有操作都和数据结构中双向链表一样的

PS : JDK 7和8源码是一样的。

3.3.1 创建对象

因为底层不用数组存储,所以不用像ArrayList和stringbuffer一样关心数组大小初始化和扩容的问题,底层直接创建一个空链表

    /**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

这个空链表有一个头结点和尾结点,因为没有任何元素,初始值都赋为null。 

在添加的时候才去一个个创建新的node

其中,node是一个(仅供LinkedList使用的)内部类,定义为

调用Node的构造方法时,需要提供(前一个结点的引用,当前元素,下一个结点的引用暂且为null) 

3.3.2 add(E e):尾插法添加元素

add方法调用linklast方法,可以看出,LinkedList默认采用【尾插法】add一个元素

public boolean add(E e) {
        linkLast(e);
        return true;
    }
void linkLast(E e) {  //新结点的元素是e
            final LinkedList.Node<E> l = last;  //拷贝一份尾结点last
            final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);  //创建新结点
            last = newNode;   //新结点成为尾结点
            if (l == null)  //如果新插入的结点是第一个
                first = newNode;  //那么这个新结点也是头结点
            else
                l.next = newNode;  //上一次尾结点指向新结点
            size++;  //size++
            modCount++;  //不用管
        }

下图中红色部分是发生的变化。

 

3.3.3 remove()移除一个元素

源码就不看了,和数据结构的思想一样。

3.3.4 add(int index,E e)在指定位置插入一个元素

3.4 Vector的源码分析

只需要知道

  • JDK 8和JDK 7一样(被抛弃了,不更新了)
  • vector底层也是用Object[] elementData数组存储,和ArrayList在JDK 7中一样,默认创建一个大小为10的数组,扩容变为2倍(这里不一样)。 
  • 方法带有synchronize,线程安全的

3.5 List接口中的常用方法

除了collection接口中我们接触到的15个常用方法,由于List是有序的,可以用下标index操作元素,所以引入了下面几个关于index的方法:

PS:有的方法名(比如add,addAll、remove)和collection接口中的方法一致,但是参数不一致(引入了index),不要混了。

下面我们用最常用的ArrayList作为示范: 

1~3、 add()、addAll()、get()

        List arrayList = new ArrayList();
        arrayList.add(123);
        arrayList.add("august");
        arrayList.add(LocalDate.now());

        //1)void add(int index, Object obj) //插入到index的位置(index从0开始)
        arrayList.add(2,"3th");
        System.out.println(arrayList);  //输出:[123, august, 3th, 2022-08-03]

        //2)boolean addAll(int index,Collection coll)
        List list = Arrays.asList(1, 2, 3);
        arrayList.addAll(list);
        System.out.println(arrayList); //输出:[123, august, 3th, 2022-08-03, 1, 2, 3]

        //3)Object get(int index)获取指定index的元素
        System.out.println(arrayList.get(2)); //输出:3th

4~5、 indexOf()、lastIndexOf()

注意:和String类的indexOf方法一样,没有找到返回-1.

        List arrayList = new ArrayList();
        arrayList.add(123);
        arrayList.add("august");
        arrayList.add(LocalDate.now());

        //4)int indexOf(Object obj):返回元素第一次出现的下标,没有的话返回-1
        System.out.println(arrayList.indexOf("august"));  //1

        //5)int lastIndexOf(Object obj):返回元素从后往前第一次出现的下标,没有返回-1
        System.out.println(arrayList.lastIndexOf("hi"));  //-1

 6~7、remove()、set()

这两个函数返回的都是被删除或者被替换的元素

        //6)Object remove(int index):删除指定下标的元素,并返回这个元素
        Object rm = arrayList.remove(2);

        //7)Object set(int index, Object obj):将制定位置的元素替换,返回被替换的元素
        System.out.println(arrayList.set(0, "我是第一个了"));

 8、subList()

注意:左闭右开区间

//8)List sublist(int fromIndex,int toIndex):返回从fromIndex到toIndex的左闭右开子集合
List list1 = arrayList.subList(1, 3);
System.out.println(list1);

9、3种遍历方式

        ArrayList arrayList = new ArrayList();
        arrayList.add(123);
        arrayList.add(456);
        arrayList.add("August");
        arrayList.add(new Date());

        //1)iterator迭代器
        Iterator it = arrayList.iterator();
        while(it.hasNext()){
            System.out.println(it.next());
        }

        //2)增强for循环
        for(Object obj : arrayList){
            System.out.println(obj);
        }

        //3)普通for循环
        int size=arrayList.size();
        for(int i=0;i<size;i++){
            System.out.println(arrayList.get(i));
        }

3.6 List常用方法总结

四、Collection子接口(二):Set接口

PS: Set接口没有增加新的方法,使用的都是的collection的方法(因为List有index下标,所以List增加了一些关于index的方法)。

4.1 HashSet、LinkedHashSet、TreeSet的异同

        同:都是Set的实现类,都存储无序不可重复的数据

        异

        |------------HashSet:Set接口的主要实现类(意思是,一般用Set就用HashSet);线程不安全;可以存储null值;底层用数组存储

                        |----------LinkedHashSet:作为HashSet的子类;可以按照添加的顺序去遍历内部数据

        |------------TreeSet:底层是用“红黑树”存储,要求元素是同一个类的元素(这样才能根据对象的属性用红黑树对对象排序)

4.2 以HashSet为例讲解Set接口

4.2.1 怎么理解“无序不可重复”

1)无序性:不等于随机性。以HashSet为例,存储的数据在底层数组中并非按照数组索引的顺序从0开始依次添加,而是根据数据的哈希值安排位置。

解释:HashSet的底层用数组Entity []elementData存储,我们之前学过迭代器的原理,就是从数组的首个元素开始依次往下读的。下图中,我们用迭代器依次读取的结果和我们add的顺序不一样,体现的就是set的无序性。但并不是随机性,因为我们每次执行都是这个结果。为什么呢?因为add添加的时候是先计算元素的hashcode值,然后存储到数组的相应位置。

 2)不可重复性:相同的元素只能添加一个。每次add的时候都调用equals方法判断是否已经有内容相同的元素(和Collection接口的contain方法一样,判断的是内容是否相同而不是地址,逻辑上也不应该去判断地址!!)。

PS: TreeSet调用的不是equals方法而是compareTo方法

        我们会想,每次添加的时候在add函数内调用Collection接口的contains函数判断一下不就得了?但是源码不是这样做的,因为contains函数是用for循环依次比较集合里的所有元素,添加第99个元素时要依次和前98个比较,添加第100个元素时要依次和前99个元素比较,效率非常低。所以我们采用哈希值的方法。

4.2.2 HashSet的add过程

        HashSet的底层是HashMap:数组的初始大小、在上面时候创建数组、JDK7(数组+链表)、JDK 8(数组+链表+红黑树)、threshold变量、loadfactory变量都和hashSet一样。

记住:相同的元素计算出的哈希值一定相同。反过来则不然,哈希值相同的不一定是相同的元素

HashSet判断两个元素相等的标准:hashcode()方法计算出的哈希值相等,同时equals()方法的比较结果也相等。

        add过程中最重要的就是如何判断添加进来的元素和之前的元素是否相同。这里用到的就是数据结构中学的哈希列表。

        调用add(obj)的时候,首先调用哈希函数hashcode()计算obj的哈希值/hash值(注意这里的哈希值指的是用一个特定的数值去代表这个元素/对象,相当于数据结构中以数字为输入时本身的数值,并不是经过哈希函数计算后的散列地址),然后再用这个hash值去计算散列地址/数组下标

        |---------如果这个位置上没有人,直接放进去。

        |---------如果位置上有人了,比较hash值是否相同

                |----------如果哈希值不同,说明这个元素是新元素,添加成功

                |----------如果哈希值相同,再调用equals方法,

                        |------------如果equals返回false,添加成功

                        |------------如果equals返回true,添加失败

JDK 7和8在追加上的区别:“七上八下”

JDK 8:正常逻辑。新元素作为尾结点,追加到原来的链表中去。

JDK 7:为了让“刚刚添加进去”的元素最先参加下一次比较,把新元素放在头结点(局部性原理)

4.2.3 hashcode()方法和equals()方法的重写

在学习Collection接口的时候,需要重写这个类的equals方法。

对于Set接口,那么我们还需要重写hashcode方法。

要求:Set接口的equals方法和hashcode方法要保持一致性,即“相同的对象必须计算出相同的哈希值”

如何保证一致性:equals方法用来比较的属性,都拿来计算hashcode

4.3 LinkedHashSet

作为HashSet的子类,add的过程和HashSet一样,只不过在此基础上又加上了双向链表。

PS : LinkedList的底层和ArrayList的底层完全不一样。ArrayList用的是数组Object [] ElementData,而LinkedList并不是在ArrayList的基础上加上双向链表就行了,而是定义自己的内部类Node作为链表的结点,因为ArrayList的底层数组是有长度的,而LinkedList没有固定长度,所以不需要用数组。

LinkedHashSet却是在HashSet的基础上加上了双向链表就行了,因为LinkedHashSet也需要计算哈希值所以也需要用数组去存储(看一下下面这个图就明白了)

特点:用迭代器遍历LinkedHashSet时,遍历的顺序和添加的顺序一样。

如何实现:双向链表

 用途:对于频繁的遍历操作,LinkedHashSet要快其父亲HashSet。因为LinkedHashSet直接根据双向链表的next指针一个个往下找就行。HashSet还要先遍历数组,数组某个位置上还不一定有元素,有元素还要再去遍历那个位置上链表。

4.4 TreeSet

特点01:因为TreeSet按照对象的属性去排序,就要求向TreeSet中add的数据必须是同一个类的对象。

特点02:TreeSet判断两个元素是否相同时,不调用equals方法,也不需要重写hashcode()方法和equals()方法 。

  • 自然排序使用的是Comparable接口定义的compareTo()方法,返回0表示相同;
  • 定制排序使用的是Comparator接口定义的compare()方法,返回0表示相同;

4.4.1 常用类的排序 

         例(1)int型(Integer包装类)

    TreeSet set = new TreeSet();
        set.add(123);
        set.add(-123);
        set.add(0);
        set.add(2);
        set.add(100);

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

输出:
-123
0
2
100
123

        按照从小到大的顺序排序 

        (2)String类

        也是安装从小到大排序

        TreeSet set = new TreeSet();
        set.add("aaa");
        set.add("aab");
        set.add("Aaa");
        set.add("Aab");
        set.add("x");

        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
输出:
Aaa
Aab
aaa
aab
x

4.4.2 自定义User类的排序

(1)自然排序(Comparable接口)

要实现User类自然排序,需要User类实现Comparable接口,并重写compareTo()方法

自然排序中,判断两个对象相同的标准:compareTo方法返回0

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

    public User(){

    }

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

    @Override
    //按照姓名从小到大的顺序
    public int compareTo(Object o) {
        if(o instanceof User){
            User user =(User) o;
            return this.name.compareTo(user.name);
        }else{
            throw new RuntimeException("输出的类型不匹配");
        }
    }

}

输出如下,按照name从小到大的顺序排序,没有管age

 

 but,如果有两个name相同,age不相同的元素呢?

 结果证明,只能添加第一个。因为我们的compareTo没有对age进行比较(也不算错,看程序员怎么设计的了)。

    @Override
    //按照姓名从大到小排序,年龄从小到大排序
    public int compareTo(Object o) {
        if(o instanceof User){
            User user =(User) o;
            //return this.name.compareTo(user.name);
            int compare = - this.name.compareTo(user.name);
            if(compare != 0){
                return compare;
            }else{
                return Integer.compare(this.age,user.age);
            }
        }else{
            throw new RuntimeException("输出的类型不匹配");
        }
    }

(2)定制排序(Comparator)

      

PS: 当我们既实现了Compareble接口又new了Comparetor(that is 同 say自然排序和定制排序同时存在时),new TreeSet(参数)有参数就采用定制排序,new TreeSet()无参数的话采用自然排序。

        上面的compare()只比较了age,所以如果add(“aa”,13)再add一个("bb",13),那么bb添加不进来。 

五、面试题

1. 考查List的常用方法中remove有两种重载形式

 输出的是【1 2】。

在List接口中,remove有两种重载的形式,remove(Object obj)和remove(int index),2在这里既作为元素又作为下标,到底调用的哪一个呢?根据答案来看,2在这里被作为index下标了。

如果想删掉元素2,得这样:

2. 去除List中的重复数值

3. HashSet的考查

前提:Person类已经重写hashcode()和equals()

 

输出:

 ① 需要注意的第一点是,修改了p1的属性后,并不用重新计算hashcode然后给p1换位置;需要注意的第二点是,remove函数执行的时候要计算(1001,“CC”)的hashcode(而不是拿着p1的地址值在数组里挨个去找),因为这样设计是最快的。

 ② add(1001,“CC”)计算出hashcode,发现位置上没人,直接添加成功

③ add(1001,“AA”)计算出hashcode,发现位置上有人,进入哈希值的比较,发现相同,进入equals的比较,发现不同,添加成功。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值