自制笔记 | Java基础——集合(上)(ArrayList、LinkedList、HashSet、TreeSet等)(持续更新...)

本文详细介绍了Java集合框架中的关键概念,如集合与数组的区别、泛型的使用及其优点、单列集合与双列集合的结构、以及ArrayList、LinkedList、Set、HashSet、LinkedHashSet和TreeSet等集合的使用场景、方法和遍历方式。重点讲解了泛型在编译期类型校验和避免异常的优点。

集合概述

集合与数组的区别:

① 长度:

数组的长度是固定的
集合的长度是可变的(自动伸缩)

② 存储类型:

数组可以存基本数据类型,也可以存引用数据类型
集合可以存引用数据类型,但若要存基本数据类型,需要将其转换为对应的包装类

创建集合的对象:

泛型:限定集合中存储数据的类型

以ArrayList示例:

ArrayList<E> list = new ArrayList<>(); //E为引用数据类型或者包装类

打印对象时,不是打印地址值,而是集合中存储的数据内容,在展示的时候会拿[]把所有的数据进行包裹

基本数据类型对应的包装类:

基本数据类型对应的包装类
byteByte
shortShort
charCharacter
intInteger
longLong
floatFloat
doubleDouble
booleanBoolean

泛型

泛型:是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查

泛型的格式:<数据类型>

注意:泛型只能支持引用数据类型

推出泛型的原因:如果没有给集合指定类型,默认认为所有的数据类型都是Object类型,此时可以给集合添加任意的数据类型,带来一个坏处:我们在获取数据的时候,无法使用它的特有行为(多态的缺点)。此时推出了泛型,可以在添加数据的时候就把类型进行统一,而且我们在获取数据的时候,也不用强转

泛型的好处:

① 统一数据类型
② 把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为在编译阶段类型就能确定下来

扩展知识点:Java中的泛型是伪泛型

当数据添加到集合中时,只是在添加前检查了数据的数据类型,但是添加完毕后,集合还是会把这些数据当作Object来处理,只是在往外获取时,会将数据按照泛型来进行类型的强转

代码中的体现:

伪泛型

泛型的细节:

① 泛型中不能写基本数据类型
② 指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型
③ 如果不写泛型,类型默认是Object

泛型可以在很多地方进行定义:

类后面——泛型类
方法上面——泛型方法
接口后面——泛型接口

泛型类:

使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类

格式:

修饰符 class 类名<类型> {
    
}

举例:

public class ArrayList<E> { //创建该类对象时,E就确定类型
    
}

此处E可以理解为变量,但是不是用来记录数据的,而是记录数据的类型,可以写成:T、E、K、V

泛型方法:

方法中形参类型不确定时,有两种方案解决

方案①:使用类名后面定义的泛型(所有方法都可以使用)
方案②:在方法申明上定义自己的泛型(只有本方法能用)

格式:

修饰符<类型> 返回值类型 方法名(类型 变量名) {
    
}

举例:

public<T> void show(T t) {
    
}

此处T可以理解为变量,但是不是用来记录数据的,而是记录数据的类型,可以写成:T、E、K、V

泛型接口:

格式:

修饰符 interface 接口<类型> {
    
}

举例:

public interface List<E> {
    
}

使用带泛型的接口的方式:

① 实现类给出具体类型
② 实现类延续泛型,创建对象时再确定

举例:

方式一:

public class MyArrayList<E> implements List<String> {
    ...
}

//创建对象
MyArrayList list = new MyArrayList();

方式二:

public class MyArrayList<E> implements List<E> {
    ...
}

//创建对象时需要确定类型
MyArrayList<String> list = new MyArrayList<>();

泛型的继承和通配符:

泛型不具备继承性,但是数据具备继承性

//Fu继承于Ye,Zi继承于Fu
ArrayList<Ye> list1 = new ArrayList<>();
ArrayList<Fu> list2 = new ArrayList<>();
ArrayList<Zi> list3 = new ArrayList<>();
ArrayList<Student> list4 = new ArrayList<>();

//method(list1);
//method(list2); 报错
//method(list3); 报错

/* public static void method(ArrayList<Ye> list) {
    ...
} 这段代码会报错,因为泛型不具备继承性*/
ArrayList<Ye> list = new ArrayList<>();
list.add(new Ye());
list.add(new Fu());
list.add(new Zi());
//代码不会报错,因为数据具备继承性

泛型的通配符:

?也表示不确定的类型,它可以进行类型的限定

? extends E:表示可以传递E或者E所有的子类类型
? super E:表示可以传递E或者E所有的父类类型

应用场景:

① 我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口
② 如果类型不确定,但是能知道以后只能传递某个继承体系,就可以使用泛型的通配符

集合体系结构

单列集合:在添加数据时,每次只能添加一个元素
双列集合:在添加数据时,每次添加的是一对数据

单列集合体系结构:

单列集合体系结构

List系列集合:添加的元素是有序(指的是存和取的顺序,和之前的由小到大或由大到小排序不一样)、可重复有索引

Set系列集合:添加的元素是无序不重复无索引

Collection

Collection是单列集合的祖宗接口,它的功能是全部单列集合都可以继承使用的

常用方法:

方法名称说明
public boolean add(E e)把给定的对象添加到当前集合中
public void clear()清空集合中所有的元素
public boolean remove(E e)把给定的对象在当前集合中删除
public boolean contains(Object obj)判断当前集合中是否包含给定的对象
public boolean isEmpty()判断当前集合是否为空
public int size()返回集合中元素的个数 / 集合的长度

add方法细节:

① 如果我们要往List系列集合中添加数据,那么方法永远返回true,因为List系列是允许元素重复的
② 如果我们要往Set系列集合中添加数据,如果当前要添加的元素不存在,方法返回true,表示添加成功;如果当前要添加的元素已经存在,方法返回false,表示添加失败,因为Set系列集合不允许重复

remove方法细节:

① 因为Collection里面定义的是共性的方法,所以此时不能通过索引进行删除,只能通过元素的对象进行删除
② 方法会有一个布尔类型的返回值,删除成功返回true,删除失败返回false

contains方法细节:底层是以来equals方法进行判断是否存在的。所以,如果集合中存储的是自定义对象,也想通过contains方法来判断是否包含,那么在javabean类中,一定要重写equals方法

Collection的遍历方式:

1.迭代器遍历:

迭代器在Java中的类是Iterator,迭代器是集合专用的遍历方式

Collection集合获取迭代器:

方法名称说明
Iterator<E>iterator()返回迭代器对象,默认指向当前集合的0索引

Iterator中的常用方法:

方法名称说明
boolean hasNext()判断当前位置是否有元素,有元素返回true,没有元素返回false
E next()获取当前位置的元素,并将迭代器对象移向下一个位置
Iterator<String> it = list.iterator();
while(it.hasNext()) {
    String str = it.next();
    System.out.println(str);
}

细节:

① 如果迭代器已经遍历完毕,再次调用next方法时,会报错NoSuchElementException
② 迭代器遍历完毕,指针不会复位,如果要继续第二次遍历集合,只能再次创建新的迭代器对象
③ 循环中只能用一次next方法,如果使用多次,可以会发生第一点的报错
④ 迭代器遍历时,不能用集合的方法进行增加或者删除,如果实在要删除,可以用迭代器提供的remove方法进行删除。只有当迭代器遍历结束时,才可以用集合的方法进行增加或者删除(如果使用集合的方法进行增加或删除,由于集合的结构发生了变化,应该去重新获取迭代器,但循环下一次的时候并没有重新获取迭代器,因此会报异常)

2.增强for遍历:

① 增强for的底层就是迭代器,为了简化迭代器的代码书写的
② 它是JDK5之后出现的,其内部原理就是一个Iterator迭代器
③ 只有所有的单列集合数组才能用增强for进行遍历

格式:

for(元素的数据类型 变量名 : 数组或者集合) {
    
}

举例:

for(String s : list) {
    System.out.println(s);
}

细节:修改增强for中的变量,不会改变集合中原本的数据

3.Lambda表达式遍历:

得益于JDK8开始的新技术Lambda表达式,提供了一种更简单、更直接的遍历集合的方式

方法名称说明
default void forEach(Consumer<? super T> action)结合lambda遍历集合

底层原理:也会自己遍历集合,依次得到每一个元素,把得到的每一个元素,传递给下面的accept方法

举例:

Collection<String> coll = new ArrayList<>();
coll.add("zhangsan");
coll.add("lisi");
coll.add("wangwu");
/*
coll.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.println(s);
    }
});
*/
coll.forEach(s -> System.out.println(s));

三种遍历方式的使用场景:

迭代器:在遍历的过程中需要删除元素,就要使用迭代器
增强for、Lambda:仅仅想遍历,那么使用增强for或Lambda表达式

List

List集合的特点:

有序:存和取的元素顺序一致
有索引:可以通过索引操作元素
可重复:存储的元素可以重复

List集合的特有方法:

Collection的方法List都继承了
List集合因为有索引,所以多了很多索引操作的方法

方法名称说明
void add(int index, E element)在此集合中的指定位置插入指定的元素
E remove(int index)删除指定索引处的元素,返回被删除的元素
E set(int index, E element)修改指定索引处的元素,返回被修改的元素
E get(int index)返回指定索引处的元素

add方法细节:原来索引上的元素会依次往后移

remove方法细节:List系列集合中有两个删除方法,一个是直接删除元素,一个是通过索引进行删除,而若出现集合内部的数据元素类型为Integer时,list.remove(1)表示的意思是删除索引为1的元素,而不是删除1这个元素。因为在调用方法的时候,如果方法出现了重载现象,优先调用实参跟形参类型一致的那个方法

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.remove(1); //删除索引为1的元素

List集合的遍历方式:

1.迭代器遍历:

List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
Iterator<String> it = list.iterator();
while(it.hasNext()) {
    String str = it.next();
    System.out.println(str);
}

2.增强for遍历:

for(String s : list) {
    System.out.println(s);
}

3.Lambda表达式遍历:

list.forEach(s -> System.out.println(s));

4.普通for循环遍历:

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

5.列表迭代器遍历:

ListIterator<String> it = list.listIterator(); //获取一个列表迭代器对象
while(it.hasNext()) {
    String str = it.next();
    if("bbb".equals(str)) {
        it.add("qqq"); //[aaa, bbb, qqq, ccc]
    }
}

列表迭代器额外添加了一个方法add,在遍历的过程中,可以添加元素

五种遍历方式对比:

迭代器遍历:在遍历的过程中需要删除元素,请使用迭代器
列表迭代器:在遍历的过程中需要添加元素,请使用列表迭代器
增强for遍历、Lambda表达式:仅仅想遍历,那么使用增强for或Lambda表达式
普通for:如果遍历的时候想操作索引,可以用普通for

ArrayList

ArrayList常用成员方法:

方法名说明
boolean add(E e)添加元素,返回值表示是否添加成功
boolean remove(E e)删除指定元素,返回值表示是否删除成功
E remove(int index)删除指定索引的元素,返回被删除元素
E set(int index, E e)修改指定索引下的元素,返回原来的元素
E get(int index)获取指定索引的元素
int size()集合的长度,也就是集合中元素的个数

LinkedList

底层数据结构是双向链表,查询慢,增删快,但是如果操作的是首尾元素,速度也是极快的

LinkedList本身多了很多直接操作首尾元素的特有API

特有方法说明
public void addFirst(E e)在该列表开头插入指定的元素
public void addLast(E e)将指定的元素追加到此列表的末尾
public E getFirst()返回此列表中的第一个元素
public E getLast()返回此列表中的最后一个元素
public E removeFirst()从此列表中删除并返回第一个元素
public E removeLast()从此列表中删除并返回最后一个元素

Set

Set系列集合添加的元素是无序、不重复、无索引

无序:存取顺序不一致
不重复:可以去除重复
无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素

Set接口中的方法基本上与Collection的API一致

HashSet

HashSet集合底层采取哈希表存储数据

哈希表是一种对于增删改查数据性能都较好的结构

哈希表组成:

JDK8之前:数组 + 链表
JDK8开始:数组 + 链表 + 红黑树

哈希值:

是根据hashCode方法算出来的int类型的整数
该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值

对象的哈希值特点:

如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
在小部分情况,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样(哈希碰撞)

HashSet底层原理:

① 创建一个默认长度16,默认加载因子为0.75(指的是当存入元素数量为数组长度的0.75倍时,数组会扩容),数组名为table
② 根据元素的哈希值跟数组的长度计算出应存入的位置

int index = (数组长度 - 1) & 哈希值;

③ 判断当前位置是否为null,如果是null直接存入
④ 如果位置不为null,表示有元素,则调用equals方法比较属性值
一样:不存;不一样:存入数组,形成链表
JDK8以前:新元素存入数组,老元素挂在新元素下面
JDK8以后:新元素直接挂在老元素下面

JDK8以后:当链表长度超过8,而且数组长度大于等于64时,自动转换为红黑树

如果集合中存储的是自定义对象,必须要重写hashCodeequals方法

HashSet底层

HashSet的三个问题:

① HashSet为什么存和取的顺序不一样?
因为取的时候,是从数组的0索引开始,依次遍历每个位置上的链表,一直遍历到数组最后,而存的时候并非一定是按照数组索引顺序而存入的,是由哈希值计算出的位置决定的

② HashSet为什么没有索引?
因为数组中一个位置上可能有多个元素,因此不好分配索引

③ HashSet是利用什么机制保证去重的?
HashCode方法和equals方法

LinkedHashSet

特点:有序(存储和取出的元素顺序一致)、不重复、无索引

原理:底层数据结构依然是哈希表,只是每个元素又额外地多了一个双向链表的机制记录存储的顺序,遍历的时候是从头指针开始遍历,依次遍历到尾指针

LinkedHashSet底层原理

在以后如果要数据去重,我们默认使用HashSet,如果要求去重且存取有序,才使用LinkedHashSet

TreeSet

特点:不重复、无索引、可排序(按照元素的默认规则(由小到大)进行排序)

原理:TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好

TreeSet集合默认的规则:

对于数值类型:Integer,Double,默认按照从小到大的顺序进行排序
对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序

TreeSet的两种比较方式:

如果是自定义对象,则需要使用以下方式进行排序,否则往TreeSet里添加元素时会报错

方式一:默认排序 / 自然排序:Javabean类实现Comparable接口指定比较规则

需要类实现Comparable接口,重写里面的抽象方法,再指定比较规则

@Override
public int compareTo(Student o) {
    //根据年龄进行排序
    return this.getAge() - o.getAge();
}

/*
this:表示当前要添加的元素
o:表示已经在红黑树存在的元素

返回值:
负数:认为要添加的元素是小的,存左边
正数:认为要添加的元素是大的,存右边
0:认为要添加的元素已经存在,舍弃
*/

方式二:比较器排序:创建TreeSet对象时候,传递比较器Comparator指定规则

使用原则:默认使用第一种,如果第一种不能满足当前需求,就使用第二种

若两种方式同时使用了,是以第二种为准

TreeSet<String> ts = new TreeSet<>(new Comparator<String>) { //创建对象时传递一个比较器对象
    @override
    public int compare(String o1, String o2) {
        //按照长度排序
        int i = o1.length() - o2.length();
        //如果一样长则按照首字母排序
        i = i == 0 ? o1.compareTo(o2) : i;
        return i;
    }
}

使用场景

1.如果想要集合中的元素可重复
ArrayList集合,基于数组的(用的最多

2.如果想要集合中的元素可重复,而且当前的增删操作明显多于查询
LinkedList集合,基于链表的

3.如果想对集合中的元素去重
HashSet集合,基于哈希表的(用的最多

4.如果想对集合中的元素去重,而且保证存取顺序
LinkedHashSet集合,基于哈希表和双向链表,效率低于HashSet

5.如果想对集合中的元素进行排序
第二种为准

TreeSet<String> ts = new TreeSet<>(new Comparator<String>) { //创建对象时传递一个比较器对象
    @override
    public int compare(String o1, String o2) {
        //按照长度排序
        int i = o1.length() - o2.length();
        //如果一样长则按照首字母排序
        i = i == 0 ? o1.compareTo(o2) : i;
        return i;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值