Java集合Collcetion、List、Set、ArrayList详细使用,底层原理

目录

一、集合概述和分类

Collection

Collection集合的常用方法

Collection遍历方式

 迭代器遍历集合

增强for遍历集合

forEach遍历集合

遍历集合案例

二、List系列集合

List集合的常用方法

List集合的遍历方式

ArrayList底层原理

LinkedList底层原理

LinkedList集合的应用场景

三、Set系列集合

认识Set集合的特点

HashSet集合底层原理

HashSet去重原理

LinkedHashSet底层原理

TreeSet集合

三、总结Collection集合

并发修改异常


一、集合概述和分类

        除了ArrayList集合,Java还提供了很多种其他的集合,如下图所示:

        一类是单列集合元素是一个一个的,另一类是双列集合元素是一对一对的。

Collection

        Collection是单列集合的根接口,Collection接口下面又有两个子接口List接口、Set接口,List和Set下面分别有不同的实现类,如下图所示:

        上图中各种集合的特点如下图所示:

        代码验证,各种集合的特点:

//简单确认一下Collection集合的特点
ArrayList<String> list = new ArrayList<>(); //存取顺序一致,可以重复,有索引
list.add("java1");
list.add("java2");
list.add("java1");
list.add("java2");
System.out.println(list); //[java1, java2, java1, java2] 

HashSet<String> list = new HashSet<>(); //存取顺序不一致,不重复,无索引
list.add("java1");
list.add("java2");
list.add("java1");
list.add("java2");
list.add("java3");
System.out.println(list); //[java3, java2, java1] 

Collection集合的常用方法

        下面是Collection集合的一些常用方法,这些方法所有Collection实现类都可以使用。 这里以创建ArrayList为例,来演示:

Collection<String> c = new ArrayList<>();
//1.public boolean add(E e): 添加元素到集合
c.add("java1");
c.add("java1");
c.add("java2");
c.add("java2");
c.add("java3");
System.out.println(c); //打印: [java1, java1, java2, java2, java3]

//2.public int size(): 获取集合的大小
System.out.println(c.size()); //5

//3.public boolean contains(Object obj): 判断集合中是否包含某个元素
System.out.println(c.contains("java1")); //true
System.out.println(c.contains("Java1")); //false

//4.pubilc boolean remove(E e): 删除某个元素,如果有多个重复元素只能删除第一个
System.out.println(c.remove("java1")); //true
System.out.println(c); //打印: [java1,java2, java2, java3]

//5.public void clear(): 清空集合的元素
c.clear(); 
System.out.println(c); //打印:[]

//6.public boolean isEmpty(): 判断集合是否为空 是空返回true 反之返回false
System.out.println(c.isEmpty()); //true

//7.public Object[] toArray(): 把集合转换为数组
Object[] array = c.toArray();
System.out.println(Arrays.toString(array)); //[java1,java2, java2, java3]

//8.如果想把集合转换为指定类型的数组,可以使用下面的代码
String[] array1 = c.toArray(new String[c.size()]);
System.out.println(Arrays.toString(array1)); //[java1,java2, java2, java3]

//9.还可以把一个集合中的元素,添加到另一个集合中
Collection<String> c1 = new ArrayList<>();
c1.add("java1");
c1.add("java2");
Collection<String> c2 = new ArrayList<>();
c2.add("java3");
c2.add("java4");
c1.addAll(c2); //把c2集合中的全部元素,添加到c1集合中去
System.out.println(c1); //[java1, java2, java3, java4]

        最后,总结一下Collection集合的常用功能有哪些,ArrayList、LinkedList、HashSet、LinkedHashSet、TreeSet集合都可以调用下面的方法。

Collection遍历方式

        接下来学习一下Collection集合的遍历方式。之前学习过的遍历方式,只能遍历List集合,不能遍历Set集合,因为以前的普通for循环遍历需要索引,只有List集合有索引,而Set集合没有索引。

        所以需要有一种通用的遍历方式,能够遍历所有集合。

迭代器遍历集合

        迭代器就是一种集合的通用遍历方式。

代码写法如下:

Collection<String> c = new ArrayList<>();
c.add("赵敏");
c.add("小昭");
c.add("素素");
c.add("灭绝");
System.out.println(c); //[赵敏, 小昭, 素素, 灭绝]

//第一步:先获取迭代器对象
//解释:Iterator就是迭代器对象,用于遍历集合的工具)
Iterator<String> it = c.iterator();

//第二步:用于判断当前位置是否有元素可以获取
//解释:hasNext()方法返回true,说明有元素可以获取;反之没有
while(it.hasNext()){
    //第三步:获取当前位置的元素,然后自动指向下一个元素.
    String e = it.next();
    System.out.println(s);
}

        迭代器代码的原理如下:

  • 当调用iterator()方法获取迭代器时,当前指向第一个元素

  • hasNext()方法则判断这个位置是否有元素,如果有则返回true,进入循环

  • 调用next()方法获取元素,并将当月元素指向下一个位置,

  • 等下次循环时,则获取下一个元素,依此内推

         最后总结一下,使用迭代器遍历集合用到哪些方法:

增强for遍历集合

        上述学习了迭代器遍历集合,但是这个代码其实还有一种更加简化的写法,叫做增强for循环。

格式如下:

        需要注意的是,增强for循环不光可以遍历集合,还可以遍历数组。接下来用代码演示一下:

Collection<String> c = new ArrayList<>();
c.add("赵敏");
c.add("小昭");
c.add("素素");
c.add("灭绝");

//1.使用增强for遍历集合
for(String s: c){
    System.out.println(s); 
}

//2.再尝试使用增强for遍历数组
String[] arr = {"迪丽热巴", "古力娜扎", "稀奇哈哈"};
for(String name: arr){
    System.out.println(name);
}

forEach遍历集合

        在JDK8版本以后还提供了一个forEach方法也可以遍历集合,如果下图所示:

        我们发现forEach方法的参数是一个Consumer接口,而Consumer是一个函数式接口,所以可以传递Lambda表达式

Collection<String> c = new ArrayList<>();
c.add("赵敏");
c.add("小昭");
c.add("素素");
c.add("灭绝");

//调用forEach方法
//由于参数是一个Consumer接口,所以可以传递匿名内部类
c.forEach(new Consumer<String>{
    @Override
    public void accept(String s){
        System.out.println(s);
    }
});


//也可以使用lambda表达式对匿名内部类进行简化
c.forEach(s->System.out.println(s)); //[赵敏, 小昭, 素素, 灭绝]

遍历集合案例

        以下是一个案例,在集合中存储自定义的对象,并遍历。具体要求如下:

        首先,我们得写一个电影类,用来描述每一步电影应该有哪些信息。

public class Movie{
    private String name; //电影名称
    private double score; //评分
    private String actor; //演员
    //无参数构造方法
    public Movie(){}
    //全参数构造方法
    public Movie(String name, double score, String actor){
        this.name=name;
        this.score=score;
        this.actor=actor;
    }
    //...get、set、toString()方法自己补上..
}

        接着,再创建一个测试类,完成上面的需求

public class Test{
    public static void main(String[] args){
        Collection<Movie> movies = new ArrayList<>();
        movies.add(new MOvie("《肖申克的救赎》", 9.7, "罗宾斯"));
        movies.add(new MOvie("《霸王别姬》", 9.6, "张国荣、张丰毅"));
        movies.add(new MOvie("《阿甘正传》", 9.5, "汤姆汉克斯"));
        
        for(Movie movie : movies){
            System.out.println("电影名:" + movie.getName());
            System.out.println("评分:" + movie.getScore());
            System.out.println("主演:" + movie.getActor());
        }
    }
}

        以上代码的内存原理如下图所示:当往集合中存对象时,实际上存储的是对象的地址值

二、List系列集合

        前面已经把Collection通用的功能学习完了,接下来学习Collection下面的一个子体系List集合。如下图所示:

List集合的常用方法

        List集合是有索引的,所以多了一些有索引操作的方法,如下图所示:

        接下来,用代码演示一下这几个方法的效果

//1.创建一个ArrayList集合对象(有序、有索引、可以重复)
List<String> list = new ArrayList<>();
list.add("蜘蛛精");
list.add("至尊宝");
list.add("至尊宝");
list.add("牛夫人"); 
System.out.println(list); //[蜘蛛精, 至尊宝, 至尊宝, 牛夫人]

//2.public void add(int index, E element): 在某个索引位置插入元素
list.add(2, "紫霞仙子");
System.out.println(list); //[蜘蛛精, 至尊宝, 紫霞仙子, 至尊宝, 牛夫人]

//3.public E remove(int index): 根据索引删除元素, 返回被删除的元素
System.out.println(list.remove(2)); //紫霞仙子
System.out.println(list);//[蜘蛛精, 至尊宝, 至尊宝, 牛夫人]

//4.public E get(int index): 返回集合中指定位置的元素
System.out.println(list.get(3));

//5.public E set(int index, E e): 修改索引位置处的元素,修改后,会返回原数据
System.out.println(list.set(3,"牛魔王")); //牛夫人
System.out.println(list); //[蜘蛛精, 至尊宝, 至尊宝, 牛魔王]

List集合的遍历方式

        List集合相比于前面的Collection多了一种可以通过索引遍历的方式,所以List集合遍历方式一共有四种:

  • 普通for循环(只因为List有索引)

  • 迭代器

  • 增强for

  • Lambda表达式

List<String> list = new ArrayList<>();
list.add("蜘蛛精");
list.add("至尊宝");
list.add("糖宝宝");

//1.普通for循环
for(int i = 0; i< list.size(); i++){
    //i = 0, 1, 2
    String e = list.get(i);
    System.out.println(e);
}

//2.增强for遍历
for(String s : list){
    System.out.println(s);
}

//3.迭代器遍历
Iterator<String> it = list.iterator();
while(it.hasNext()){
    String s = it.next();
    System.out.println(s);
}

//4.lambda表达式遍历
list.forEach(s->System.out.println(s));

ArrayList底层原理

        ArrayList集合底层是基于数组结构实现的,也就是说当你往集合容器中存储元素时,底层本质上是往数组中存储元素。 特点如下:

        我们知道数组的长度是固定的,但是集合的长度是可变的,这是怎么做到的呢?原理如下:

        数组扩容,并不是在原数组上扩容(原数组是不可以扩容的),底层是创建一个新数组,然后把原数组中的元素全部复制到新数组中去。

LinkedList底层原理

        LinkedList底层是链表结构,链表结构是由一个一个的节点组成,一个节点由数据值、下一个元素的地址组成。如下图所示

        假如,现在要在B节点和D节点中间插入一个元素,只需要把B节点指向D节点的地址断掉,重新指向新的节点地址就可以了。如下图所示:

        假如,现在想要把D节点删除,只需要让C节点指向E节点的地址,然后把D节点指向E节点的地址断掉。此时D节点就会变成垃圾,会把垃圾回收器清理掉。

        上面的链表是单向链表,它的方向是从头节点指向尾节点的,只能从左往右查找元素,这样查询效率比较慢;还有一种链表叫做双向链表,不光可以从做往右找,还可以从右往左找。如下图所示:

        LinkedList集合是基于双向链表实现了,所以相对于ArrayList新增了一些可以针对头尾进行操作的方法,如下图示所示:

LinkedList集合的应用场景

        LInkedList集合有什么用?

        可以用它来设计栈结构、队列结构。

        先来认识一下队列结构,队列结构你可以认为是一个上端开口,下端也开口的管子的形状。元素从上端入队列,从下端出队列。

        入队列可以调用LinkedList集合的addLast方法,出队列可以调用removeFirst()方法。

//1.创建一个队列:先进先出、后进后出
LinkedList<String> queue = new LinkedList<>();
//入对列
queue.addLast("第1号人");
queue.addLast("第2号人");
queue.addLast("第3号人");
queue.addLast("第4号人");
System.out.println(queue);

//出队列
System.out.println(queue.removeFirst());	//第4号人
System.out.println(queue.removeFirst());	//第3号人
System.out.println(queue.removeFirst());	//第2号人
System.out.println(queue.removeFirst());	//第1号人

        接下来,我们再用LinkedList集合来模拟一下栈结构的效果。还是先来认识一下栈结构长什么样。栈结构可以看做是一个上端开头,下端闭口的水杯的形状。

        元素永远是上端进,也从上端出,先进入的元素会压在最底下,所以栈结构的特点是先进后出,后进先出

        有没有感觉栈结构很像,手枪的子弹夹呀!!第一个压进入的子弹在最底下,最后一个才能打出来,最后一个压进入的子弹在最顶上,第一个打出来。

        用LinkedList来模拟下栈结构,代码如下:

//1.创建一个栈对象
LinkedList<String> stack = new ArrayList<>();
//压栈(push) 等价于 addFirst()
stack.push("第1颗子弹");
stack.push("第2颗子弹");
stack.push("第3颗子弹");
stack.push("第4颗子弹");
System.out.println(stack); //[第4颗子弹, 第3颗子弹, 第2颗子弹,第1颗子弹]

//弹栈(pop) 等价于 removeFirst()
System.out.println(statck.pop()); //第4颗子弹
System.out.println(statck.pop()); //第3颗子弹
System.out.println(statck.pop()); //第2颗子弹
System.out.println(statck.pop()); //第1颗子弹

//弹栈完了,集合中就没有元素了
System.out.println(list); //[]

三、Set系列集合

认识Set集合的特点

        Set集合是属于Collection体系下的另一个分支,它的特点如下图所示:

        用代码简单演示一下,每一种Set集合的特点。

//Set<Integer> set = new HashSet<>();	//无序、无索引、不重复
//Set<Integer> set = new LinkedHashSet<>(); //有序、无索引、不重复
Set<Integer> set = new TreeSet<>(); //可排序(升序)、无索引、不重复
set.add(666);
set.add(555);
set.add(555);
set.add(888);
set.add(888);
set.add(777);
set.add(777);
System.out.println(set); //[555, 666, 777, 888]

HashSet集合底层原理

        为了更加透彻的理解HashSet为什么可以去重,来看一下它的底层原理。

        HashSet集合底层是基于哈希表实现的,哈希表根据JDK版本的不同,也是有点区别的

  • JDK8以前:哈希表 = 数组+链表

  • JDK8以后:哈希表 = 数组+链表+红黑树

        我们发现往HashSet集合中存储元素时,底层调用了元素的两个方法:一个是hashCode方法获取元素的hashCode值(哈希值);另一个是调用了元素的equals方法,用来比较新添加的元素和集合中已有的元素是否相同。

  • 只有新添加元素的hashCode值和集合中以后元素的hashCode值相同、新添加的元素调用equals方法和集合中已有元素比较结果为true, 才认为元素重复。

  • 如果hashCode值相同,equals比较不同,则以链表的形式连接在数组的同一个索引为位置(如上图所示)

        在JDK8开始后,为了提高性能,当链表的长度超过8时,就会把链表转换为红黑树,如下图所示:

HashSet去重原理

        前面学习了HashSet存储元素的原理,依赖于两个方法:一个是hashCode方法用来确定在底层数组中存储的位置,另一个是用equals方法判断新添加的元素是否和集合中已有的元素相同。

        要想保证在HashSet集合中没有重复元素,我们需要重写元素类的hashCode和equals方法。比如以下面的Student类为例,假设把Student类的对象作为HashSet集合的元素,想要让学生的姓名和年龄相同,就认为元素重复。

public class Student{
    private String name; //姓名
    private int age; //年龄
    private double height; //身高
 
    //无参数构造方法
    public Student(){}
    //全参数构造方法
    public Student(String name, int age, double height){
        this.name=name;
        this.age=age;
        this.height=height;
    }
    //...get、set、toString()方法自己补上..
    
    //按快捷键生成hashCode和equals方法
    //alt+insert 选择 hashCode and equals
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        if (Double.compare(student.height, height) != 0) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }

    @Override
    public int hashCode() {
        int result;
        long temp;
        result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        temp = Double.doubleToLongBits(height);
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }
}

        接着,写一个测试类,往HashSet集合中存储Student对象。

public class Test{
    public static void main(String[] args){
        Set<Student> students = new HashSet<>();
        Student s1 = new Student("至尊宝",20, 169.6);
        Student s2 = new Student("蜘蛛精",23, 169.6);
        Student s3 = new Student("蜘蛛精",23, 169.6);
        Student s4 = new Student("牛魔王",48, 169.6);
        
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        
        for(Student s : students){
            System.out.println(s);
        }
    }
}

        打印结果如下,发现存了两个蜘蛛精,当时实际打印出来只有一个,而且是无序的。

Student{name='牛魔王', age=48, height=169.6}
Student{name='至尊宝', age=20, height=169.6}
Student{name='蜘蛛精', age=23, height=169.6}

LinkedHashSet底层原理

        接下来,再学习一个HashSet的子类LinkedHashSet类。LinkedHashSet它底层采用的是也是哈希表结构,只不过额外新增了一个双向链表来维护元素的存取顺序。如下下图所示:

        每次添加元素,就和上一个元素用双向链表连接一下。第一个添加的元素是双向链表的头节点,最后一个添加的元素是双向链表的尾节点。

        把上个案例中的集合改成LinkedList集合,观察效果怎样:

public class Test{
    public static void main(String[] args){
        Set<Student> students = new LinkedHashSet<>();
        Student s1 = new Student("至尊宝",20, 169.6);
        Student s2 = new Student("蜘蛛精",23, 169.6);
        Student s3 = new Student("蜘蛛精",23, 169.6);
        Student s4 = new Student("牛魔王",48, 169.6);
        
        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        
        for(Student s : students){
            System.out.println(s);
        }
    }
}

        打印结果如下

Student{name='至尊宝', age=20, height=169.6}
Student{name='蜘蛛精', age=23, height=169.6}
Student{name='牛魔王', age=48, height=169.6}

TreeSet集合

        TreeSet集合的特点是可以对元素进行排序,但是必须指定元素的排序规则。

        如果往集合中存储String类型的元素,或者Integer类型的元素,它们本身就具备排序规则,所以直接就可以排序。

Set<Integer> set1= new TreeSet<>();
set1.add(8);
set1.add(6);
set1.add(4);
set1.add(3);
set1.add(7);
set1.add(1);
set1.add(5);
set1.add(2);
System.out.println(set1); //[1,2,3,4,5,6,7,8]

Set<Integer> set2= new TreeSet<>();
set2.add("a");
set2.add("c");
set2.add("e");
set2.add("b");
set2.add("d");
set2.add("f");
set2.add("g");
System.out.println(set1); //[a,b,c,d,e,f,g]

        如果往TreeSet集合中存储自定义类型的元素,比如说Student类型,则需要我们自己指定排序规则,否则会出现异常。

//创建TreeSet集合,元素为Student类型
Set<Student> students = new TreeSet<>();

//创建4个Student对象
Student s1 = new Student("至尊宝",20, 169.6);
Student s2 = new Student("紫霞",23, 169.8);
Student s3 = new Student("蜘蛛精",23, 169.6);
Student s4 = new Student("牛魔王",48, 169.6);

//添加Studnet对象到集合
students.add(s1);
students.add(s2);
students.add(s3);
students.add(s4);
System.out.println(students); 

        此时运行代码,会直接报错。原因是TreeSet不知道按照什么条件对Student对象来排序。

        我们想要告诉TreeSet集合按照指定的规则排序,有两种办法:

第一种:让元素的类实现Comparable接口,重写compareTo方法

//第一步:先让Student类,实现Comparable接口
//注意:Student类的对象是作为TreeSet集合的元素的
public class Student implements Comparable<Student>{
    private String name;
    private int age;
    private double height;
	//无参数构造方法
    public Student(){}
    //全参数构造方法
    public Student(String name, int age, double height){
        this.name=name;
        this.age=age;
        this.height=height;
    }
    //...get、set、toString()方法自己补上..
    
    //第二步:重写compareTo方法
    //按照年龄进行比较,只需要在方法中让this.age和o.age相减就可以。
    /*
    原理:
    在往TreeSet集合中添加元素时,add方法底层会调用compareTo方法,根据该方法的
    结果是正数、负数、还是零,决定元素放在后面、前面还是不存。
    */
    @Override
    public int compareTo(Student o) {
        //this:表示将要添加进去的Student对象
        //o: 表示集合中已有的Student对象
        return this.age-o.age;
    }
}

        此时,再运行测试类,结果如下

Student{name='至尊宝', age=20, height=169.6}
Student{name='紫霞', age=20, height=169.8}
Student{name='蜘蛛精', age=23, height=169.6}
Student{name='牛魔王', age=48, height=169.6}

第二种:在创建TreeSet集合时,通过构造方法传递Compartor比较器对象

//创建TreeSet集合时,传递比较器对象排序
/*
原理:当调用add方法时,底层会先用比较器,根据Comparator的compare方是正数、负数、还是零,决定谁在后,谁在前,谁不存。
*/
//下面代码中是按照学生的年龄升序排序
Set<Student> students = new TreeSet<>(new Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2){
        //需求:按照学生的身高排序
        return Double.compare(o1,o2); 
    }
});

//创建4个Student对象
Student s1 = new Student("至尊宝",20, 169.6);
Student s2 = new Student("紫霞",23, 169.8);
Student s3 = new Student("蜘蛛精",23, 169.6);
Student s4 = new Student("牛魔王",48, 169.6);

//添加Studnet对象到集合
students.add(s1);
students.add(s2);
students.add(s3);
students.add(s4);
System.out.println(students); 

三、总结Collection集合

        将所有的Collection集合总结一下,要求掌握每一种集合的特点,以及他们的体系结构。

并发修改异常

        补充说明一下,那就是在使用迭代器遍历集合时,可能存在并发修改异常。先把这个异常用代码演示出来,再解释一下为什么会有这个异常产生:

List<String> list = new ArrayList<>();
list.add("王麻子");
list.add("小李子");
list.add("李爱花");
list.add("张全蛋");
list.add("晓李");
list.add("李玉刚");
System.out.println(list); // [王麻子, 小李子, 李爱花, 张全蛋, 晓李, 李玉刚]

//需求:找出集合中带"李"字的姓名,并从集合中删除
Iterator<String> it = list.iterator();
while(it.hasNext()){
    String name = it.next();
    if(name.contains("李")){
        list.remove(name);
    }
}
System.out.println(list);

        运行上面的代码,会出现下面的异常。这就是并发修改异常

        为什么会出现这个异常呢?那是因为迭代器遍历机制,规定迭代器遍历集合的同时,不允许集合自己去增删元素,否则就会出现这个异常。

        怎么解决这个问题呢?不使用集合的删除方法,而是使用迭代器的删除方法,代码如下:

List<String> list = new ArrayList<>();
list.add("王麻子");
list.add("小李子");
list.add("李爱花");
list.add("张全蛋");
list.add("晓李");
list.add("李玉刚");
System.out.println(list); // [王麻子, 小李子, 李爱花, 张全蛋, 晓李, 李玉刚]

//需求:找出集合中带"李"字的姓名,并从集合中删除
Iterator<String> it = list.iterator();
while(it.hasNext()){
    String name = it.next();
    if(name.contains("李")){
        //list.remove(name);
        it.remove(); //当前迭代器指向谁,就删除谁
    }
}
System.out.println(list);

        

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值