Java基础进阶 集合框架详解

今日任务

1、List接口介绍(掌握常用List特有方法)
2、练习
3、ArrayList介绍(必须清楚集合的特征、掌握集合中的方法)
4、LinkedList介绍(必须清楚集合的特征、掌握集合中的方法)
5、Vector 类介绍(了解)
6、List下的子类总结(掌握)
7、Set 接口介绍(掌握Set集合的特性)
8、HashSet 集合(掌握HashSet集合的应用)

1、List接口介绍

在学习Collection接口的时候,api中告诉我们,在Collection接口的下面有2个直接的子接口,分别是Set和List。我们这里先学习List接口。

1.png

List接口:
1)是Collection接口的子接口,继承了Collection接口中的所有方法;
2)List接口定义的所有集合中的元素都可以重复,并且还可以保证存取的顺序一致(存储和取出的顺序一致的);
3)List接口下的所有集合全部拥有下标,List接口更像数组;
4)由于List接口规定自己的真实实现类(集合)都拥有下标,因此我们在操作List接口下的所有集合容器的时候,都可以通过下标操作;
5)因此在List接口中它不仅仅继承到Collection接口中的所有函数,同时java 还根据List的下标的特性,定义了适合List接口的特有函数;

1.1、List的特有方法

List集合和Collection集合不同之处,就是List接口中的特有方法都是围绕集合的下标而设计的。
List集合中的特有方法:

1)添加方法:
void add(int index,Object element ):根据指定的角标位置,向集合中添加指定的元素对象;
index >= 0 && index <= size();

2.png

boolean addAll(int index, Collection coll):根据指定的角标位置,向集合中添加指定集合中所有的元素;
index >= 0 && index <= size();


3.png

分析和步骤:
1)定义一个ListDemo类,在这个类中定义一个method_1()函数;
2)在method_1()函数中使用new关键字创建ArrayList类的对象list,并赋值给接口List类型;
3)使用集合对象list调用属于Collection接口中的add(E e)函数向集合List中添加字符串”aaaa”;
4)使用集合对象list调用属于接口List中特有的函数add(int index,Object element )根据指定的下标向集合中添加数据”abc”;

5)使用输出语句输出list对象中的值;

public static void method_1() {
        // 创建集合对象
        List list = new ArrayList();
        List list1 = new ArrayList();
        // 向集合中添加数据
        list.add("aaaa");
        list.add("bbbb");
        list.add("cccc");
        list1.add("hhhh");
        list1.add("哈哈");
        // 使用List接口中特有的函数向接口中添加数据
        // list.add(1, "dddd");//1表示要添加数据的下标位置
        // list.add(4,"xyz");//指定的下标前面一定要有元素
        // 向集合list的下标为2的位置添加集合list1中所有的数据
        list.addAll(2, list1);
        // 输出数据
        System.out.println(list);
    }

注意:
1)指定的下标前面一定要有数据;
2)添加不是覆盖,指定的位置添加了元素后,之前存在元素就会向后移动;

2)获取方法:
List subList(int startIndex, int endIndex): 返回集合中从指定角标开始(包含头角标)到指定角标结束(不包含尾角标)的所有元素,以集合方式返回

4.png

int indexOf(Object obj)返回指定元素在集合中第一次出现的角标。没有匹配元素则返回-1


5.png

分析和步骤:
1)在上述ListDemo类中定义一个method_2()函数;
2)在method_2()函数中使用new关键字创建ArrayList类的对象list,并赋值给接口List类型;
3)使用集合对象list调用属于Collection接口中的add(E e)函数向集合List中添加几个字符串数据;
4) 使用集合对象list调用属于接口List中特有的函数get(int index )根据指定的下标获取下标对应的数据;
5)使用输出语句输出获得的数据obj;

public static void method_2() {
        // 创建集合对象
        List list = new ArrayList();
        // 向集合中添加数据
        list.add("nba");
        list.add("nba");
        list.add("cba");
        list.add("wbna");
        list.add("wcba");
        // 根据下标获得对应的元素
        // Object obj = list.get(2);
        // Object obj = list.get(7);//下标不能是空白区域
        // System.out.println(obj);
        // List subList = list.subList(1,0);//结束角标要大于等于起始角标
        // System.out.println(subList);
        // 返回指定元素第一次出现的角标
        int index = list.indexOf("nbaa");// 没有返回-1
        System.out.println(index);
    }

注意: 下标的范围:index >= 0 && index <= size()-1;

1.2、List的特有迭代器

List接口继承了Collection接口,Collection接口中的获取某个集合对应的迭代器的函数,List一定也继承到了。但是由于List集合有下标,因此Java中针对List这类集合同时也提供了自己特有的迭代器。

ListIterator接口:


6.png

ListIterator接口:它是专门针对List接口而设计的,这个迭代器在遍历List集合的时候,可以对这个集合中的元素进行增 删 改 查操作。并且它还可以逆向遍历。
注意:ListIterator迭代器只能被List集合使用,其它集合都不能使用。
使用ListIterator逆向迭代的时候一定要使用带有参数的listIterator(index)函数,然后集合的长度作为参数下标,否则该迭代光标就会从前往后迭代;

7.png

这里的index>=0&&index<=list.size();
使用list.size()作为参数。

分析和步骤:
1)定义一个ListIteratorDemo类;
2)在这个类中使用new关键字创建ArrayList类的对象list,并赋值给接口List类型;
3)使用集合对象list调用属于Collection接口中的add(E e)函数向集合List中添加几个字符串数据;
4)使用集合对象list调用属于接口Collection中的iterator()函数获得迭代器对象it,并在for循环中使用迭代器对象it调用hasNext()函数判断是否有元素,使用迭代器对象it调用next()函数获取list集合中的数据,并打印;
5)使用集合对象list调用listIterator()函数生成ListIterator 类型,并使用for循环对其迭代遍历;
6)同样使用ListIterator 类型的对象调用listIterator(list.size())函数,这里需要注意,由于是从集合中的最后元素开始向前遍历,所以这里我们对于listIterator()函数应该给一个参数,否则不会出现结果;
7)lit.hasPrevious()判断是否有元素,如果有前一个元素可以迭代,则返回 true ,lit.previous()返回列表中的前一个元素;

package cn.xuexi.list;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
/*
 * List接口的特有的迭代器
 */
public class ListIteratorDemo {
    public static void main(String[] args) {
        //创建集合对象
        List list=new ArrayList();
        //向集合中添加数据
        list.add("nba");
        list.add("aaa");
        list.add("cba");
        list.add("wnba");
        list.add("wcba");
        //使用Collection接口中的Iterator进行迭代
        System.out.println("使用Iterator进行迭代");
        for (Iterator it = list.iterator(); it.hasNext();) {
            //输出数据
            System.out.println(it.next());
        }
        //使用List接口中的特有迭代器ListIterator进行迭代
        System.out.println("使用ListIterator进行正向迭代");
        for (ListIterator it = list.listIterator();it.hasNext();) {
            System.out.println(it.next());
        }
        System.out.println("使用ListIterator进行逆向迭代");
        //it.hasPrevious()如果为true表示集合中从后往前迭代开始有元素
        for (ListIterator it = list.listIterator(list.size()); it.hasPrevious();) {
            //it.previous()表示返回集合中的上一个元素
            System.out.println(it.previous());
        }
    }
}

注意:
1)使用ListIterator逆向迭代的时候一定要使用带有参数的listIterator(index)函数,然后集合的长度作为参数下标,否则该迭代光标就会从前往后迭代;


8.png

这里的index>=0&&index<=list.size();

2)这里为何使用list.size()作为参数而不是list.size()-1,因为我们要从集合中最后一个元素后面开始迭代,如果是list.size()-1,迭代光标会从最后一个元素前面开始,这样会导致忽略了集合中的最后一个元素;

扩展:演示在使用ListIterator迭代器遍历List集合的时候,可以对这个集合中的元素进行修改操作,代码如下:

案例:List集合的特有迭代器对象。
在使用ListIterator迭代器遍历集合的时候是可以修改集合的;

需求:迭代取出集合中的元素的时候,判断是否等于某个数据,如果相等使用迭代器对象调用迭代器中的add()函数向集合中添加一个新的数据或者使用迭代器中的set函数更改数据。

分析和步骤:
1)在这个类中使用new关键字创建ArrayList类的对象list,并赋值给接口List类型;
2)使用集合对象list调用属于Collection接口中的add(E e)函数向集合List中添加几个字符串数据;
3)使用集合对象list调用listIterator()函数生成ListIterator 类型的迭代器对象;
4)使用while循环遍历集合,将获得的数据强制转换为String类型;
5)使用判断结构判断找到的对象数据是否是集合中的一个数据,如果是,则使用迭代器对象调用add()函数向集合中添加一个新的字符串数据;
6)循环结束输出集合;

9.png

补充:注意,使用Iterator迭代器遍历集合的时候,在Iterator接口中只提供了remove()函数进行修改集合;

总结:(很重要,经常犯错误)
我们在迭代集合时,如果要对集合修改,一定要使用迭代器本身的功能!一定不能使用集合对象调用集合中的函数去修改集合。

Iterator中只提供了一个:remove功能
ListIterator中提供了:add、remove、set功能

1.3、List集合遍历方法总结

说明:
迭代List集合有多种写法,有五种写法:
1)toArray(); Object[] arr = list.toArray();
2)使用for循环借助Iterator接口可以实现;
3)使用for循环借助ListIterator接口可以实现;
4)使用普通for循环

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

5)使用foreach循环:

for( Object obj : list )
{
     System.out.println(obj);
}
package cn.xuexi.list;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
/*
 * 迭代集合的五种方法:
 * 1.toArray()
 * 2.使用Iterator迭代器遍历集合
 * 3.使用ListIterator迭代器遍历集合
 * 4.使用普通for循环遍历集合
 * 5.使用foreach循环遍历集合
 */
public class ListIteratorDemo2 {
    public static void main(String[] args) {
        //创建集合对象
        List list=new ArrayList();
        //向集合添加数据
        list.add("及时雨");
        list.add("白麒麟");
        list.add("黑旋风");
        list.add("母老虎");
        //使用toArray()函数遍历集合
        System.out.println("使用toArray()函数遍历集合");
        Object[] arr = list.toArray();
        //遍历数组
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
        System.out.println("使用Iterator迭代器遍历集合");
//使用Iterator迭代器遍历集合
        for (Iterator it = list.iterator(); it.hasNext();) {
            System.out.println(it.next());
        }
        System.out.println("使用ListIterator迭代器遍历集合");
        //使用ListIterator迭代器遍历集合
        for (ListIterator it = list.listIterator(); it.hasNext();) {
            System.out.println(it.next());
        }
        System.out.println("使用普通for循环遍历集合");
        //使用普通for循环遍历集合
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
        System.out.println("使用foreach循环遍历集合");
        //使用foreach循环遍历集合
        for (Object obj : list) {
            System.out.println(obj);
        }
    }
}

2、练习

需求:使用List存储字符串,并去除重复元素,要求在同一个集合中去除。
假如有一个集合,里面有一些重复的字符串。把重复给我去除。
分析和步骤:
思路:
1):创建一个空的集合l2
2):循环遍历l1,取出每个字符串
3):判断取出的元素在l2中是否存在
是:存在,舍弃
否:不存在,添加到l2
4):清空L1
5):把L2的元素添加到L1

注意:这里使用for循环遍历迭代的是l1集合,不是l2集合。

/**
 * 需求:去除集合中的重复元素。
 */
public class CollectionTest {
    public static void main(String[] args) {
        
        List l1= new ArrayList();
        
        l1.add("aaaa");
                l1.add("abc");
        l1.add("abc");
        l1.add("bbbb");
        l1.add("aaaa");
        l1.add("xyz");
        l1.add("xyz");
        l1.add("aaaa");
        
        //新建一个新的集合容器
        List l2 = new ArrayList();
        
        for (Iterator it = l1.iterator(); it.hasNext();) {
            Object obj =  it.next();
            //判断从原始集合中取出的元素在新的集合中是否存在
            if( !l2.contains(obj) ){
                l2.add(obj);
            }
        }
        //循环结束之后,l2集合中一定保存的都是不重复的元素
        //把原始集合中的数据清空,把l2中的不重复元素添加到原始集合中
        l1.clear();
        l1.addAll(l2);
        System.out.println(l1);
    }
}

3、LinkedList介绍(掌握)

3.1 概述

List集合

|------ArrayList集合

|------LinkedList集合
10.png

LinkedList集合:它也是List接口的实现类,它的底层使用的链接列表(链表)数据结构。

链表结构的特点:有头有尾。

补充概念:什么是数据结构?
数据结构:数据的存储方式,不同的集合容器它们存储数据的方式都不一样。而我们学习众多的集合容器,重点是知道每个集合存储数据的方式即可。不同集合的存储方式不同,导致集合中的数据存取,以及元素能否重复等相关操作也都不相同。
LinkedList集合它采用的是数据结构中的链表结构:
链表结构:由一个链子把多个节点连接起来的数据结构。
节点:在链表结构中每个可以保存数据的空间称为节点,而一个链表结构是由多个节点组成的。
也就是说链表结构使用节点来存储数据。
每个节点(存储数据的空间)可以分成若干部分,其中有一部分存储数据,另外一部分存储的是其他节点的地址。

说明:
1)节点:实际存储自己的数据+其他节点的地址,而作为链表结构中的最后一个节点的存储地址的空间是null。
2)链表结构查询或遍历时都是从头到尾的遍历。
3)链表结构是有头有尾的。因此LinkedList集合中定义了自己的特有的方法都是围绕链表的头和尾设计的。
4)链表分为两种:单向链表和双向链表。
如下图所示就是数组结构和链表结构对数据的查询、添加、删除的区别对比图如下所示:

11.png

总结:
1)数组结构查询快,但是增删慢;
2)链表结构查询慢,但是增删快;
上述两种数据结构快慢只是相对来说。

LinkedList集合特点:
1、它的底层使用的链表结构;
2、有头有尾,其中的方法都是围绕头和尾设计的;
3、LinkedList集合可以根据头尾进行各种操作,但它的增删效率高,查询效率低;
LinkedList集合增删效率高是因为底层是链表结构,如果增加或者删除只需要在增加或者删除节点的位置上记住新的节点的地址即可,而其他节点不需要移动,所以速度会快。
而查询遍历由于链表结构的特点,查询只能从头一直遍历到链表的结尾,所以速度会慢。
4、LinkedList集合底层也是线程不安全。效率高;
5、也可以存储null元素;

3.2 LinkedList集合的特有方法

12.png

说明:
这里只有两个构造函数,不像ArrayList集合有三个构造函数,而这里不需要给容器初始化容量大小,因为LinkedList集合的特点就是链表结构,在底层如果新增加一个元素那么前一个节点记住新增加的节点地址,而新增加的节点记住后一个节点地址就可以,什么时候增加什么记住地址即可,不需要初始化容量大小。
由于LinkedList是链表结构,而链表有头有尾,所以在LinkedList集合中专门为链表结构提供了特有的函数。

13.png
14.png
15.png
16.png

练习:需求练习LinkedList集合中特有的函数。

分析和步骤:
1)使用new关键字创建LinkedList类的对象list,并赋值为LinkedList类型;
2)使用对象list调用LinkedList集合中的addFirst()函数向集合中添加字符串;
3)利用for循环和迭代器迭代遍历LinkedList集合;
4)使用对象list调用LinkedList类中的getLast()函数获取最后一个元素并输出;
5)使用对象list调用LinkedList类中的removeFirst()函数删除第一个元素并输出;

17.png

3.3 队列和堆栈结构(面试题)

由于LinkedList集合底层使用的链表结构:导致LinkedList集合在存储数据的时候可以根据头和尾进行增、删、改、查各种操作。可以使用LinkedList集合模拟常见的2种数据结构:
队列结构:先进的先出或者后进的后出。(排队买票)
堆栈结构:先进的后出或者后进的先出。(手枪的弹夹)
经常使用LinkedList模拟上述的2种结构:

1)案例:使用LinkedList模拟队列结构 。先进的先出 (排队买票)
注意:队列的特点是获取一个元素便同时将获取的元素直接删除;
可以理解为一个人买票,买完票就不在队伍中了;
由于队列的特点,是先进先出,所以这里可以删除第一个元素,并返回删除的元素;

分析和步骤:
1)新建一个模拟队列的类QueueDemo;
2)在这个类Queue中创建LinkedList类的集合对象list;
3)定义一个添加元素的函数addElement(Object obj),在函数体里面使用集合对象list调用LinkedList类的集合中的addLast(obj)函数,将元素添加尾部;
4)定义一个获取元素的函数getElement(),返回值是Object,由于元素获取之后就要从集合中删除,所以在函数体里面使用集合对象list调用LinkedList类的集合中的removeFirst()函数,移除头部元素并返回删除的元素;
5)定义一个判断队列中是否还有元素存在的函数isNull(),函数返回值是boolean,在函数体里面使用集合对象list调用LinkedList类的集合中的isEmpty()函数,不包含元素返回true,包含返回false;
6)定义一个测试类LinkedListDemo1,在这个类中使用new关键字创建刚才创建好的Queue类的对象q;
7)使用对象q调用addElement()函数向集合中添加数据;
8)使用while循环依次取出集合中的数据,对象q调用isNull()函数判断是否还有元素,对象q调用getElement(),获得元素输出并打印;

package cn.xuexi.list.test;
import java.util.LinkedList;
/*
 * 模拟队列结构 特点:先进先出,类似买票
 */
//创建模拟队列的类
class QueueDemo
{
    //创建LinkedList集合对象
    LinkedList list=new LinkedList();
    //定义函数模拟向队列中添加元素
    public void addElement(Object obj)
    {
        //每次都向集合最后面添加数据 添加到链表的尾部
        list.addLast(obj);
    }
    //定义函数,让外界获取元素
    public Object getElement()
    {
        /*
         * 由于队列的特点是获取一个元素便同时将获取的元素直接删除
         * 可以理解为一个人买票,买完票就不在队伍中了
         * 由于队列的特点,是先进先出,所以这里可以删除第一个元素,并返回删除的元素
         */
        return list.removeFirst();
    }
    //判断队列中是否还有元素存在
    public boolean isNull()
    {
        /*
         * 在LinkedList函数中虽然没有判断集合中是否还含有数据
         * 但是它的接口List中含有,所以我们可以使用list.isEmpty()来判断集合中是否还含有数据
         * isEmpty()函数是判断集合中没有元素返回true,有元素返回false
         */
        return list.isEmpty();
    }
}
public class LinkedListQueue {
    public static void main(String[] args) {
//创建模拟队列类的对象
        QueueDemo q=new QueueDemo();
        //向队列中添加数据
        q.addElement("元素1");
        q.addElement("元素2");
        q.addElement("元素3");
        //判断集合中是否还含有元素,有,则输出数据
        while(!q.isNull())//!q.isNull()如果为true,表示集合中还有数据
        {
            //说明集合中还有元素,取出数据 输出元素结果 "元素1" "元素2" "元素3" 
            System.out.println(q.getElement());
        }
    }
}

2)案例:使用LinkedList模拟堆栈结构。先进的后出,(手枪的弹夹)

代码和上述相同,只是将QueueDemo类中的getElement()函数体中的代码改成
return list.removeLast()即可
removeLast()表示移除尾部元素并返回删除的元素。

package cn.xuexi.list.test;
import java.util.LinkedList;
/*
 * 模拟堆栈结构 特点:先进后出,类似子弹弹夹
 */
//创建模拟堆栈的类
class QueueDemo
{
    //创建LinkedList集合对象
    LinkedList list=new LinkedList();
    //定义函数模拟向堆栈中添加元素
    public void addElement(Object obj)
    {
        //每次都向集合最后面添加数据 添加到链表的尾部
        list.addLast(obj);
    }
    //定义函数,让外界获取元素
    public Object getElement()
    {
        /*
         * 由于堆栈的数据结构的特点是先进后出,
         * 所以我们可以将最后添加的数据先移除
         */
        return list.removeLast();
    }
    //判断堆栈中是否还有元素存在
    public boolean isNull()
    {
        /*
         * 在LinkedList函数中虽然没有判断集合中是否还含有数据
         * 但是它的接口List中含有,所以我们可以使用list.isEmpty()来判断集合中是否还含有数据
         * isEmpty()函数是判断集合中没有元素返回true,有元素返回false
         */
        return list.isEmpty();
    }
}
public class LinkedListQueue {
    public static void main(String[] args) {
        //创建模拟堆栈类的对象
        QueueDemo q=new QueueDemo();
        //向堆栈中添加数据
        q.addElement("元素1");
        q.addElement("元素2");
        q.addElement("元素3");
        //判断集合中是否还含有元素,有,则输出数据
        while(!q.isNull())//!q.isNull()如果为true,表示集合中还有数据
        {
            //说明集合中还有元素,取出数据 输出元素结果 "元素3" "元素2" "元素1" 
            System.out.println(q.getElement());
        }
    }
}

4、Vector介绍(了解)

Vector集合是JDK1.0的时候出现的集合,它在jdk1.2的时候被收编到List接口的下面。而这个集合被JDK1.2中的ArrayList集合代替。

18.png

演示Vector类的函数:

分析和步骤:
1)定义一个测试类VectorDemo,并创建这个类的对象v;
2)使用对象v调用Vector类中的addElement()函数向Vector集合中添加字符串数据;
3)使用集合对象v调用iterator()函数获得迭代器的对象it;
4)使用循环借助迭代器对象调用hasNext()函数和next()函数遍历集合,并输出,可是iterator()不是Vector集合原来就开始使用的迭代方式;
5)使用对象v调用Vector类中的elements()函数来获得类似迭代器的对象en,并给Enumeration类型;
6)同样使用en对象调用Enumeration中的hasMoreElements()和nextElement()函数借助集合进行遍历,而这种方式是最开始诞生Vector类使用迭代的方式;

问题:
通过查阅API得知,Vector集合从jdk1.0版本就已经存在了,而迭代器Iterator是从jdk1.2版本开始才引入的,那么在jdk1.2版本之前是怎么对Vector集合进行迭代和遍历取出集合中的数据呢?

解释说明:
1)在1.2版本之前我们借助于另一个接口Enumeration来实现迭代的;
2)使用接口Enumeration对集合进行迭代,可以先使用Vector集合的对象调用Vector集合中的elements()函数来获得接口Enumeration的对象;
3)然后使用接口Enumeration的对象调用hasMoreElements()判断集合中是否还有元素,有返回true;
4)然后使用接口Enumeration的对象调用nextElement()函数获取集合中的元素;

package cn.xuexi.vector;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Vector;
/*
 * Vector集合的演示
 */
public class VectorDemo {
    public static void main(String[] args) {
        //创建Vector集合对象
        Vector v=new Vector();
        //向Vector集合中添加数据
        v.addElement("黑旋风");
        v.addElement("刘德华");
        v.addElement("成龙");
        v.addElement("李连杰");
        //遍历集合
        /*for (Iterator it = v.iterator(); it.hasNext();) 
        {
            //获取集合里面的数据
            System.out.println(it.next());
        }*/
        //使用jdk1.2版本之前对Vector集合进行遍历
        for (Enumeration en = v.elements(); en.hasMoreElements();) {
                //输出集合中的数据
            System.out.println(en.nextElement());
        }
    }
}

Vector集合它就是ArrayList集合,可以使用Enumeration迭代Vector集合,但是由于Enumeration迭代器中的方法的名字太长,被Iterator代替。后期如果需要迭代器Vector应该优先考虑使用Iterator迭代。

Vector集合的特点:
1)Vector是jdk1.0出现的集合,它的增删,查询效率都比较低;
2)由于Vector底层是线程安全的。ArrayList 的底层是不安全的,因此ArrayList各方面的效率都比Vector高。
3)对于Vector类现在开发中已经几乎不再使用。根本就不用。

5、List下的子类总结(掌握)

  Collection集合(接口)
                |----List集合(接口):可以存储重复元素、可以存储null、有角标、存取有序。
                |----ArrayList集合(类):实现List接口。ArrayList集合中的特有方法是实现List
                                         底层使用可变数组结构。
                                         查询遍历的效率比较高、增删的效率比较低
                                         属于线程不安全的集合类。执行效率比较高
                 |----LinkedList集合(类):实现List接口。
                                           底层使用链表结构。(链表:有头有尾)
                                           LinkedList集合中的特有方法都是围绕链表的头尾设计
                                           查询遍历的效率比较慢、增删的效率比较高
                                           属于线程不安全的集合类。执行效率比较高
                 |----Vector集合(类):实现List接口。线程安全的集合类。
                                       底层使用可变数组结构。查询遍历效率、增删效率都比较低。

                Vector类属于线程安全的集合类。效率比较慢。现在开发中已经不再使用。

问题:遇到对线程有需求的情况,应该使用哪个集合类?
还使用LinkedList、ArrayList(在后面学习过程中,可以解决LinkedList\ ArrayList线程不安全的问题)

疑问:
1)解释:为什么ArrayList集合增删效率低,而查询速度快?
因为我们向集合中添加元素的时候,有时会将元素添加到集合中的最前面,或者有可能删除最前面的数据,这样就导致其他数据向后移动或者删除时向前移动,所以效率会低。
对于查询ArrayList集合,由于ArrayList集合是数组结构,而数组结构是排列有序的,并且下标是有序增加的,当查询ArrayList集合的时候可以按照排列顺序去查询,或者直接可以通过某个下标去查询,这样就会导致查询速度相对来说会快很多。

2)解释:为什么LinkedList集合增删效率快,而查询速度慢?
LinkedList集合增删效率高是因为底层是链表结构,如果增加或者删除只需要在增加或者删除节点的位置上记住新的节点的地址即可,而其他节点不需要移动,所以速度会快。
而查询遍历由于链表结构的特点,查询只能从头一直遍历到链表的结尾,所以速度会慢。

注意:学习了这么多集合类,在开发中如果不知道使用哪个集合,到底什么时候使用ArrayList集合和LinkedList集合?
1)如果对集合进行查询操作建议使用ArrayList集合;
2)如果对集合进行增删操作建议使用LinkedList集合;

总结:如果实在把握不好使用的时机,建议大家以后在开发中都使用ArrayList集合即可,因为在开发中我们对于集合的操作几乎都是查询操作,很少执行增删操作的。

6、Set接口

6.1、Set接口概述

Collection集合(接口)的接口下面有2个直接的子接口:
 |-----List集合(接口):可以保存重复元素,拥有下标,存储有序,可以存储多个null元素。
      |-----ArrayList类:底层是可变数组,根据下标进行操作,查询效率快,增删效率低。
      |-----LinkedList类:底层是链表,根据链表的头尾进行操作,增删效率快,查询效率低。
 |-----Set集合(接口):不能保存重复元素,没有下标。可以存储null但只能有一个。并且不保证存取的顺序,也就是说对于集合set进行存取操作的时候都没有任何顺序,没有任何规律而言。
      |-----HashSet类
            |-----LinkedHashSet类
      |-----TreeSet类
19.png

说明:
1)Set接口中没有自己的特有函数,所有的函数全部来自于Collection接口。
2)Set集合没有角标,只能通过Iterator迭代器遍历获取集合中的元素,Set集合不能使用ListIterator迭代器,因为ListIterator只是针对List集合特有的迭代器。

6.2、Set接口的练习

需求:存储字符串并遍历。
由于Set是接口不能创建对象,只能创建Set实现类的接口,任何一个都可以,这里就创建HashSet类的对象。

分析和步骤:
1)使用new关键字创建Set接口下的子类HashSet类的对象,并赋值给对象s,类型是Set接口类型;
2)使用对象s调用add()函数向集合中添加字符串数据;
3)循环遍历集合;

package cn.xuexi.set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/*
 * Set集合的练习
 */
public class SetDemo {
    public static void main(String[] args) {
        //创建集合对象
        Set s=new HashSet(); 
        //向Set集合中添加数据
        s.add("aaa");
        s.add("aaa");
        s.add("bbb");
        s.add(null);
        s.add("ccc");
        //遍历集合
        //注意:由于Set集合中不允许包含重复的元素,所以当添加元素的时候,
        //集合对象会把重复的元素删除,不让保存到集合中
        for (Iterator it = s.iterator(); it.hasNext();) {
            //输出数据
            System.out.println(it.next());
        }
    }
}

7、HashSet集合(掌握)

7.1、HashSet类的介绍和特点

20.png

说明:
1)实现了Set接口,具备了Set集合的特性;
2)不保证集合中的迭代顺序(不保证元素存取一致),允许存储null元素;
3)底层使用哈希表结构;

案例:HashSet集合的应用。
分析和步骤:
1)使用new关键字创建HashSet集合类的对象set,类型是HashSet类型;
2)使用集合对象set调用集合中的add()函数向HashSet集合中添加字符串数据;
3)使用对象set调用iterator()函数获取迭代器对象it;
4)使用for循环遍历,通过迭代器对象调用hasNext()和next()函数,并输出获取的数据;

package cn.xuexi.set;
import java.util.HashSet;
import java.util.Iterator;
/*
 * hashSet演示
 */
public class HashSetDemo {
    public static void main(String[] args) {
        //创建HashSet集合对象
        HashSet set=new HashSet();
        //向集合中添加数据
        set.add("JavaSe");
        set.add("JavaSE");
        set.add("JavaEE");
        set.add("AAAA");
        set.add("AAAA");
        set.add("bbbb");
        set.add("bbbb");
        //遍历集合
        for (Iterator it = set.iterator(); it.hasNext();) {
            //输出集合中的数据
            System.out.println(it.next());
        }
    }
}

输出结果:


21.png

通过以上程序输出结果得出结论:
1)HashSet集合不能存储重复的元素;
2)HashSet集合存储元素的顺序不固定;

接下我们要分析为什么HashSet集合存储的数据顺序不固定和为什么不支持存储重复的元素?
答案肯定和HashSet集合的底层哈希表数据结构有关系,所以接下来我们要学习什么是哈希表。

7.2、哈希表介绍(掌握)

哈希表:
它是一个数据结构,底层依赖的是数组,只是不按照数组的下标操作数组中的元素。需要根据数组中存储的元素的哈希值进行元素操作。

哈希表的存储过程:
1)哈希表底层是一个数组,我们必须知道集合中的一个对象元素到底要存储到哈希表中的数组的哪个位置,也就是需要一个下标。
2)哈希表会根据集合中的每个对象元素的内容计算得出一个整数值。由于集合中的对象元素类型是任意的,而现在这里使用的算法必须是任意类型的元素都可以使用的算法。能够让任意类型的对象元素都可以使用的算法肯定在任意类型的对象所属类的父类中,即上帝类Object中,这个算法就是Object类中的hashCode()函数。

结论:要给HashSet集合中保存对象,需要调用对象的hashCode函数。

解释说明:
通过查阅API得知,使用Object的任意子类对象都可以调用Object类中的hashCode()函数并生成任意对象的哈希码值。

22.png

代码演示如下:


23.png

输出结果:
24.png

由以上结果可以看出任意对象调用Object类中的hashCode()函数都会生成一个整数。

3)hashCode算法,得到一个整数,但是这个整数太大了,这个值不能直接作为数组下标的。所以底层还会对这个值结合数组的长度继续计算运行,得到一个在0~数组长度-1之间的整数,这样就可以作为数组的下标了。

问题1:使用hashCode函数生成的一个过大整数是用什么算法将将生成哈希码值变成0~数组长度-1之间的数字呢?
其中一种最简单的算法是可以实现的,举例:假设底层哈希表中数组长度是5,那么下标的范围是0~4,
所以我们这里可以使用生成的哈希码值(过大的整数)对数组长度取余数,我们发现任何数在这里对5取余都是在 0 1 2 3 4 之间,所以这样就可以获取到0~数组长度-1之间的下标了。

问题2:如果数据过多,HashSet底层的数组存储不下,怎么办?
hashSet集合底层数组初始容量是16,如果大小不够,那么会继续新创建一个数组,新数组大小等于原来数组的大小*0.75+原来数组的大小。

4)如果上述做法已经计算出底层数组的下标位置,那么就要判断计算出的下标位置是否已经有元素了:
A.如果下标对应的位置没有元素:直接存储数据;
B.如果下标对应的位置有元素:这时就必须调用对象的equals()函数比较两个对象是否相同:
如果结果是true:相同,直接将要添加的数据丢弃;
如果结果是false:不相同,那直接将数据存储到数组当前空间位置;
但是当前位置已经存在元素了,怎么将后来的数据存储到数组中呢?
这里需要使用类似链表的结构了,在当前位置上在画出来一个空间,然后将当前的对象数据保存到新划出来的空间中,在原来的空间中设置一个引用变量记录着新划分空间的地址,如果后面还有数据要存储当前空间,做法和上述相同。

哈希表如下图所示:

25.png

最终结论:哈希表底层通过hashCode和equals算法结合,来保证对象数据在HashSet集合中不重复唯一,并且存储的顺序不固定。
哈希表:数组+hashCode函数+equals函数,哈希表保证对象唯一需要依赖对象的hashCode和equals方法。

面试题:哈希表如何保证元素唯一?
哈希表保证元素唯一依赖两个方法:hashCode和equals。
哈希表底层其实还是一个数组,元素在存储的时候,会先通过hashCode算法结合数组长度得到一个索引。然后判断该索引位置是否有元素:如果没有,不用调用equals函数,直接存储;如果有,再调用元素的equals方法比较是否相同:相同,直接舍弃;如果不同,也存储。

7.3、HashSet保存自定义对象

案例需求:向HashSet集合中添加自定义对象。

分析和步骤:
1)定义一个Person类,在Person类中定义两个属性name和age,并分别生成toString()、set()、get()方法;
2)在定义一个测试类HashSetDemo,在这个类中的main函数中,使用new关键字创建HashSet集合类的对象set,类型是HashSet类型;
3)使用集合对象set调用集合中的add()函数向HashSet集合中添加Person类的匿名对象,并通过创建Person类的对象调用构造函数给name和age赋值;
4)使用对象set调用iterator()函数获取迭代器对象it;
5)使用for循环遍历,通过迭代器对象调用hasNext()和next()函数,并输出获取的数据;

测试类代码如下:

package cn.xuexi.set;
import java.util.HashSet;
import java.util.Iterator;
/*
 * 使用哈希表存储自定义对象
 */
public class HashSetDemo2 {

    public static void main(String[] args) {
        //创建集合对象
        HashSet set=new HashSet();
        //向集合中添加自定义对象数据
        set.add(new Person("黑旋风",19));
        set.add(new Person("助教",18));
        set.add(new Person("班导",28));
        set.add(new Person("班长",19));
        set.add(new Person("黑旋风",19));
        //循环遍历集合
        for (Iterator it = set.iterator(); it.hasNext();) {
            //输出数据
            System.out.println(it.next());
        }
    }
}

运行结果:

26.png

上述代码有问题,为什么以上程序中的HashSet集合中会出现相同的Person类的对象?

给哈希表中保存自定义对象时,由于每次创建的都是新的Person对象,而每个Person对象都有自己的唯一的内存地址,就是每个对象的内存地址都不相同。并且在给哈希表中存放这些对象的时候每个对象都需要调用自己从Object类中继承到的hashCode函数,而hashCode函数会根据每个对象自己内存地址计算每个对象哈希值,每个对象的内存地址不同,计算出来的哈希值也一定不同,最后就会导致生成的底层的数组的下标也不相同。如果下标不一样,根本就不会去调用equals比较是否是同一个元素,直接就会导致每个对象都可以正常的保存到哈希表中。

自己定义的对象,有自己所属的类,而这个类继承到了Object的hashCode函数,所以导致这个函数是根据对象内存地址计算哈希值,而我们更希望根据对象自己的特有数据(name和age的属性值)来计算哈希值。

问题:那么为什么我们之前向集合中保存String类的字符串对象时没有出现这种现象呢?

因为在String类中已经复写了Object类中的hashCode()和equals()函数,所以存储字符串对象的时候,底层调用的hashCode()和equals()函数根本就不是Object类中的而是String类中已经复写好的函数。

也就是对于String类的对象调用自己的hashCode函数,根本就不是根据字符串对象的地址计算出来的哈希码值,而是根据字符串内容计算出来的。

对于equals函数,也是调用自己的函数,底层根本比较不是 this==obj,而是比较的是字符串对象的内容。

解决上述问题的办法:

所以我们需要重写Object类中的hashCode方法。这样重写完之后就会根据对象特有的数据来计算哈希值了,而不再是根据对象的随机地址生成哈希值了。在这里:根据姓名和年龄计算。

上述办法只重写hashCode函数,仅仅是保证了对象内容相同时计算的数组下标相同。同时即使一个人的姓名和年龄不相等,也有可能计算的下标是相同的。所以,当数组下标相同时,我们接下来还要调用equals函数来比较对象元素的内容。根据我们之前所学的Object类中的equals函数得知,Object类中的equals函数体是比较两个对象的地址值是否相等,那么即使两个对象内容的下标相同,但是equals函数比较的不是两个对象的内容,是地址,也会永远不相等,所以这里也得复写Object类中的equals()函数。这样才能保证当两个对象内容一致不会被保存到HashSet集合中。

所以还得重写equals方法。

手动复写Object类中的hashCode()和equals()函数,代码如下:

27.png

说明:虽然上述手动复写Object类中的hashCode和equals函数可以实现,但是在真实开发中建议最好不要手动去书写,比较麻烦。

建议直接在eclipse中使用快捷键生成的方式,比较简单,开发效率还快。

在Person类中使用快捷键生成复写Object类中的hashCode()和equals()函数:

package cn.xuexi.set;
/*
 * 描述人类
 */
public class Person {
    //属性
    String name;
    int age;
    public Person(String name, int age) {
        this.name = name;
        this.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;
    }
    //复写Object类中的toString()函数
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
    
    /*//复写hashCode函数
    @Override
    public int hashCode() {
        
         * 根据年龄和姓名计算一个int值 this.name.hashCode()表示调用String类中的hashCode函数
         * 如果两个字符串相同,生成的哈希值也会相同 如果两个人的名字一样,则生成哈希值相同
         * this.name.hashCode()+this.age :表示如果两个人的姓名和年龄相同,那么返回得整数值肯定也相同
         
        return this.name.hashCode()+this.age;
    }
    //复写equals函数
    @Override
    public boolean equals(Object obj) {
        Person p=(Person)obj;//强制转换为Person类型的对象
        return this.name.equals(p.name) && this.age==p.age;
    }*/
    
    //复写Object类中的hashCode()函数
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }
    //复写Object类中的equals()函数
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
}

结论:
要求以后给HashSet集合即哈希表中保存对象的时候,要求当前这个对象所属的类必须复写Object类中的hashCode和equals方法。建议最好快捷键生成。

7.4、HashSet总结

HashSet集合存储对象的时候:
1、HashSet集合的底层使用的哈希表结构。那么就要求存放的对象必须具备hashCode功能。由于任何一个类的父类都是Object类,而hashCode函数定义在了Object类中,因此所有的对象都具备hashCode功能。
2、如果我们要把一个对象可以正确的存放在HashSet集合中,这个对象所属的类一般都需要复写hashCode函数。建立本类自己的计算哈希值的方式。
3、如果在HashSet集合中要保证对象唯一,不能仅仅依靠hashCode函数,还要依赖于对象的equals函数,当hashCode函数计算出来的哈希值相同的时候,还要调用equals方法比较2个对象是否相同。
4、要求在向HashSet集合中存储自己定义一个类对象的时候,那么必须在这个自定义类中复写Object类中的hashCode和equals函数。
5、注意当向HashSet集合中存储数据的时候,对象一定会调用hashCode函数计算下标,但是不一定一定会调用equals函数,只有当计算的下标相同时
才会调用equals函数,否则不会调用equals函数来比较两个对象是否相等。

7.5、LinkedHashSet介绍(了解)

28.png

LinkedHashSet集合:它的底层使用的链表+哈希表结构。它和HashSet集合的区别是LinkedHashSet是一个可以保证存取顺序的集合,并且LinkedHashSet集合中的元素也不能重复。
特点:
A:存取有序(底层有一个链接表) 链表记录着存储数据的顺序
B:保证元素的唯一(哈希表) 哈希表是真正存储数据的地方
C:线程不安全,效率高

29.png

说明:LinkedHashSet集合没有自己的特有函数,所有的功能全部继承父类。

分析和步骤:
1)定义一个测试类LinkedHashSetDemo;
2)在这个类中使用new关键字创建LinkedHashSet类的对象set,对象set的类型是LinkedHashSet;
3)使用对象set调用add()函数给集合LinkedHashSet添加字符串常量;
4)使用迭代器类Iterator进行迭代并输出集合中的数据;

package cn.xuexi.set;
import java.util.Iterator;
import java.util.LinkedHashSet;
/*
 * LinkedHashSet集合演示
 */
public class LinkedHashSetDemo {
    public static void main(String[] args) {
        //创建集合对象
        LinkedHashSet set=new LinkedHashSet();
        //向集合中添加数据
        set.add("aaa");
        set.add("bbb");
        set.add("aaa");
        set.add("ccc");
        //遍历集合
        for (Iterator it = set.iterator(); it.hasNext();) {
            //输出集合中的数据
            System.out.println(it.next());
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容