Java中的集合框架

391 阅读8分钟

一、Java中的集合框架

1.为什么使用集合框架

image-20230813104505198.png

2.集合框架的继承关系(简略)

graph BT
ArrayList-->List
LinkedList-->List
HashSet-->Set
TreeSet-->Set
List-->Collection
Set-->Collection
graph BT
HashMap-->Map
TreeMap-->Map

List是可重复且有序的,Set是不可重复且无序的

二、List

1.List类型的特点

Java中的List是一种接口类型,它定义了一种有序的集合数据结构。List中的元素按照插入的顺序存储,并且可以通过索引来访问元素。以下是List类型的主要特点:

  1. 有序集合:List中的元素按照插入的顺序进行排序,因此元素的顺序是有意义的。可以通过索引来访问元素,索引从0开始。
  2. 可以包含重复元素:List允许包含重复元素,即多个元素可以具有相同的值。
  3. 动态性:List是动态的,可以在运行时添加、删除或修改元素。这使得List非常灵活,可以适应程序运行时的变化需求。
  4. 易于使用:List提供了一组简单而强大的方法来操作数据,例如add、get、remove等。这使得开发人员可以轻松地使用List来处理数据。
  5. 可扩展性:List接口具有良好的扩展性,可以通过实现List接口来创建自己的List实现。这使得List可以根据需求进行扩展和定制,以满足特定的应用程序需求。

2.ArrayList案例和LinkedList案例

ArrayList:

public class ArrayListTest {
    public static void main(String[] args) {
        //声明的时候最好采用多态的形式,也就是以父接口的形式去声明
        //增加元素
        List list=new ArrayList();
        list.add("西瓜");
        list.add("苹果");
        list.add("草莓");
        list.add("西瓜");
        //修改元素
        list.set(1,"面包");//下标从0开始,此处将‘苹果’修改成了‘面包’
        //删除元素
        list.remove(1);//remove可以直接通过对象删除,也可以通过下标删除
        //查询元素
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

LinkedList:

public class LinkedListTest {

    public static void main(String[] args) {
        List list=new LinkedList();
        list.add("西瓜");
        list.add("苹果");
        list.add("草莓");
        list.add("西瓜");
        //修改元素
        list.set(1,"面包");
        //删除元素
        list.remove(1);
        //查询元素
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

3.ArrayList底层原理和LinkedList底层原理

  • ArrayList底层是由==数组==实现的

    其默认长度为10

image-20230813121440991.png

将elementData这个存储ArrayList元素的数组作为缓冲区

image-20230813122149819.png

通过ensureCapacity()方法进行动态扩容

image-20230813121706278.png

  • LinkedList底层是由双向链表实现的

image-20230813131607880.png

其初始化时不需要申请内存空间

4.ArrayList和LinkedList如何选择

ArrayList可以根据索引直接访问元素,因此可以快速随机访问元素。在添加或删除元素时,需要移动后面的所有元素,因此这种操作的效率较低。如果需要高效的随机访问元素,并且添加或删除元素的频率较低,那么应该选择ArrayList。

LinkedList无法进行随机访问元素,但是添加或删除元素时只需要修改指针,因此它的效率相对较高。如果需要高效的添加或删除元素,并且对访问元素的顺序并没有特别的要求,那么应该选择LinkedList。

在开发时,主要的业务场景是追加和遍历,所以使用ArrayList的频率更高。

5.List存放复杂的数据类型

先定义一个Dog复杂数据类型

public class Dog {
    private String name;
    private String type;
	//此处忽略了全参数构造方法和get,set方法
    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", type='" + type + '\'' +
                '}';
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Dog dog = (Dog) o;
        return Objects.equals(name, dog.name) && Objects.equals(type, dog.type);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, type);
    }
}

将其存入ArrayList中

public class ArrayListTest {
    public static void main(String[] args) {
        List list=new ArrayList();
        list.add(new Dog("a","a"));
        list.add(new Dog("b","b"));
        list.add(new Dog("c","c"));
        System.out.println("共有"+list.size()+"条狗");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
        //判断list中是否包含某个对象,或者移除对象都需要重写equals方法
        System.out.println(list.contains(new Dog("a", "a")));
        System.out.println(list.remove(new Dog("a", "a")));
    }
}

contain包含,remove移除,传入的对象都需要重写equals()方法,否则操作失败返回false

6.LinkedList独有的方法

因为LinkedList是链表实现,所以有一些方法针对其实现

方法名说明
void addFirst(Object o)在列表首部添加元素
void addLast(Object o)在列表尾部添加元素
Object getFirst()返回列表的第一个元素
Object getLast()返回列表的最后一个元素
Object removeFirst()删除并返回列表中的第一个元素
Object removeLast()删除并返回列表中的最后一个元素

三、Set

1.Set类型的特点

  1. 无序的:Set中的元素没有特定的顺序。添加到Set的元素不会保持原来的顺序。
  2. 无重复的元素:Set的一个重要特性是它不允许重复元素。如果尝试添加一个已经存在的元素,Set将不会接受它。
  3. 线程不安全:Set接口的实现类(例如HashSet和TreeSet)不是线程安全的。如果你在多线程环境中使用,需要额外的同步措施。
  4. 不允许null元素:Set不允许包含null元素。
  5. 没有父/子关系:与List不同,Set没有子列表的概念。
  6. 基于Map实现:大多数Set的实现(如HashSet和LinkedHashSet)是基于Map实现的。这意味着添加和删除操作通常具有O(1)的复杂度,而查找操作具有O(n)的复杂度(在最佳情况下,如在TreeSet中,查找操作具有O(log n)的复杂度)。
  7. 用于存储单例:由于Set不允许重复元素,所以它是存储单例的理想结构。
  8. 支持泛型:从Java 5开始,Set接口的实现类(如HashSet和TreeSet)支持泛型,这使得在编译时检查类型错误成为可能。

2.HashSet案例

HashSet

public class HashSetTest {

    public static void main(String[] args) {
        Set set = new HashSet();
        set.add("苹果");
        set.add("苹果");
        set.add("香蕉");
        set.add("椰子");
        set.add("桃子");
        //因为set是无序的,因为没有下标,所以只能使用增强型for循环
        for (Object o : set) {
            System.out.println(o);
        }

    }
}
/**
运行结果:不可重复,且无序的
桃子
苹果
香蕉
椰子
*/

3.Set的去重原理

先创建一个复杂类型Dog

public class Dog {
    private String name;
    private String type;
	//省略了get和set方法以及构造函数
    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", type='" + type + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Dog dog = (Dog) o;
        return Objects.equals(name, dog.name) && Objects.equals(type, dog.type);
    }
}

HashSet

public class HashSetTest {
    public static void main(String[] args) {
        Set set = new HashSet();
        set.add(new Dog("a","a"));
        //与第一条相同
        set.add(new Dog("a","a"));
        set.add(new Dog("b","b"));
        set.add(new Dog("c","c"));
        set.add(new Dog("d","d"));
        set.add(new Dog("e","e"));
        //因为set是无序的,因为没有下标,所以只能使用增强型for循环
        for (Object o : set) {
            System.out.println((Dog)o);
        }

    }
}
/*
运行结果:
Dog{name='b', type='b'}
Dog{name='e', type='e'}
Dog{name='a', type='a'}
Dog{name='d', type='d'}
Dog{name='a', type='a'}
Dog{name='c', type='c'}
**/

此时,复杂类型并没有被去重!!!

所以,此时需要在Dog类中重写hashCode方法

    @Override
    public int hashCode() {
        super.hashCode();
        return Objects.hash(name, type);
    }

再执行,成功去重

Dog{name='a', type='a'}
Dog{name='b', type='b'}
Dog{name='c', type='c'}
Dog{name='d', type='d'}
Dog{name='e', type='e'}

Set去重的原理主要基于对象的hashCode()和equals()方法。当添加元素到Set时,首先会计算元素的hashCode,然后在Set内部查找具有相同hashCode的元素。如果找到了具有相同hashCode的元素,那么就会调用equals()方法来比较这个元素和Set内部存储的元素是否相同。如果equals()返回true,那么就会忽略新添加的元素,实现去重,简而言之,Set去重一定要重写hashCode()和equals()方法

image-20230815144107970.png


image-20230815150647906.png

4.TreeSet算法依赖于一个比较接口

代码案例

public class TreeSetTest {

    public static void main(String[] args) {
        Set set = new TreeSet();
        set.add("a");
        set.add("a");
        set.add("b");
        set.add("c");
        set.add("d");
        set.add("e");
        for (Object o:set){
            System.out.println(o);
        }
    }
}

/**
添加字符串已经去重
a
b
c
d
e
*/

当添加的是对象时

public class TreeSetTest {

    public static void main(String[] args) {
        Set set = new TreeSet();
        set.add(new Dog("a","a"));
        set.add(new Dog("a","a"));
        set.add(new Dog("b","b"));
        set.add(new Dog("c","c"));
        set.add(new Dog("d","d"));
        set.add(new Dog("e","e"));
        for (Object o : set) {
            System.out.println((Dog)o);
        }
    }
}

报错

image-20230815160759948.png

出现这个问题是因为,TreeSet在添加遍历复杂类型时,必须实现一个比较接口,Comparable,同时重写compareTo(T o)方法。

public class Dog implements Comparable<Dog>{
    private String name;
    private String type;
    
    //省略了get和set方法已经全参构造函数

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                ", type='" + type + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Dog dog = (Dog) o;
        return Objects.equals(name, dog.name) && Objects.equals(type, dog.type);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, type);
    }

    @Override
    public int compareTo(Dog o) {
        return name.compareTo(o.name)+type.compareTo(o.type);
    }
}

此时再运行,没有报错

Dog{name='a', type='a'}
Dog{name='b', type='b'}
Dog{name='c', type='c'}
Dog{name='d', type='d'}
Dog{name='e', type='e'}

为什么直接添加字符串不需要实现Comparable接口

原因:String类型中已经自动实现了Comparable了。

image-20230815165449031.png

四、Map

1.Map类型的特点

Java中的Map是一种接口类型,它定义了一种将键映射到值的数据结构。Map中的元素是通过键值对来存储和访问的。以下是Map类型的主要特点:

  1. 键的唯一性:在Map中,每个键只能对应一个值。如果尝试使用相同的键插入多个值,那么后续的插入操作将覆盖先前的值。
  2. 键和值的存储:Map中的键和值可以是任何类型的对象,包括自定义的对象类型。键和值的存储是相互独立的,即键的类型与值的类型可以是不同的类型。
  3. 键的有序性:在Java中,Map的内部实现有多种方式,如HashMap、LinkedHashMap等。不同的实现方式对键的存储顺序可能会有所不同。例如,HashMap不保证键的顺序,而LinkedHashMap则按照键的插入顺序进行排序。
  4. Map的遍历:可以通过迭代器(Iterator)来遍历Map中的所有元素。遍历时,元素的顺序可能与插入顺序不同,具体取决于Map的实现方式。
  5. Map的操作:Map提供了一些常用的操作方法,如put(key, value)用于插入或更新元素,get(key)用于根据键获取对应的值,remove(key)用于删除指定键对应的元素等。
  6. Map的实现类:Java提供了多个Map的实现类,如HashMap、LinkedHashMap、TreeMap等。这些实现类具有不同的特点和用途,可以根据实际需求选择合适的实现方式。

2.HashMap基本实现原理

  1. 数据存储:HashMap内部采用数组+链表(Java 8后为数组+树)的方式存储数据。数组是HashMap的基础,每个数组元素就是一个Node对象,包含三个部分:key、value和next指针。
  2. 散列映射:当我们将键值对存入HashMap时,首先会对键进行散列(hash)操作,得到一个散列值,这个散列值对应数组的某个位置,然后将该键值对存入该位置的Node中。
  3. 解决冲突:由于不同的键可能会得到相同的散列值,因此可能会产生冲突。HashMap通过链表的方式解决这个问题。当两个或多个键得到相同的散列值时,它们会被存放在同一个链表中。在查询时,如果根据key得到的散列值相同,就会在对应的链表中按照next指针顺序查找。
  4. 树的构建(Java 8及以后版本): 当链表的长度大于一定阈值(默认为8)时,会将链表转换为红黑树(一种自平衡的二叉搜索树)。树的节点数总是大于等于链表的节点数,所以查找、插入、删除在树上的操作时间复杂度是O(log n)。
  5. 树的合并(Java 8及以后版本): 当红黑树的节点数小于等于6时,会将红黑树转回为链表。

3.HashMap案例

public class HashMapTest {
    public static void main(String[] args) {
        //双列,键值结构
        //key不可重复,value可重复
        Map hashMap = new HashMap<>();
        hashMap.put("a", "注意看这个");
        hashMap.put("b", "桃子");
        hashMap.put("c", "梨");
        hashMap.put("d", "芒果");
        hashMap.put("a", "被修改了");//修改
        System.out.println("长度为:"+hashMap.size());
        //拿到hashMap中所有的key
        Set set = hashMap.keySet();
        for (Object o : set) {
            System.out.println(o+":"+hashMap.get(o));
        }
    }
}

/**
长度为:4
a:被修改了
b:桃子
c:梨
d:芒果
*/

因为Set是无序的,取到的key也是无序的,所以打印出来也是无序的

4.Map常用方法

image-20230816135405902.png

代码示例:

public class HashMapTest {
    public static void main(String[] args) {
        Map hashMap = new HashMap<>();
        hashMap.put("a", "注意看这个");
        hashMap.put("b", "桃子");
        hashMap.put("c", "梨");
        hashMap.put("d", "芒果");
        hashMap.put("a", "被修改了");
        System.out.println("长度为:"+hashMap.size());//获取长度
        System.out.println(hashMap.get("a"));//通过key取value
        System.out.println(hashMap.containsKey("b"));//查询hashMap中是否存在某个键
        System.out.println(hashMap.remove("c"));//移除键,返回对应的value
        System.out.println(hashMap.keySet());//返回一个存有key的set
        System.out.println(hashMap.values());//返回一个存有value的collection
        hashMap.clear();//清空hashMap
        System.out.println("长度为:"+hashMap.size());
    }
}
/**
长度为:4
被修改了
true
梨
[a, b, d]
[被修改了, 桃子, 芒果]
长度为:0
*/

5.泛型在集合中的应用

为什么使用泛型

在上述代码例子中,key和value可以添加任意数据类型的值,会出现一些类型不匹配和强制类型转换的问题。

使用泛型还可以提高代码的灵活性和可重用性。假设我们有一个程序需要处理不同类型的数据,如果使用Object类型的键和值,则需要使用强制类型转换来获取具体类型的对象。而使用泛型,我们可以在定义List,Set,Map集合时指定具体的类型,使得代码更加清晰和易于维护。

代码示例(以List举例,未使用泛型):

public class GenericTest {
    public static void main(String[] args) {
        List list=new ArrayList();
        list.add(new Dog("a","a"));
        list.add(1);
        list.add("test");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}
/**
Dog{name='a', type='a'}
1
test
*/

代码可读性极低,当数据量极大时,需要取list中Dog类型的数据进行操作,需要先使用到instanceof关键字判断是否为Dog类型,然后再进行操作

泛型入门

public class GenericTest<T> {
    T a;

    public void show(T a) {
        System.out.println(a);
    }

    public static void main(String[] args) {
        GenericTest objectGenericTest = new GenericTest();
        objectGenericTest.show("a");
        objectGenericTest.show(123);
    }
}

使用泛型可以传入不同数据类型的数据,也可以指定传入的数据类型。

指定传入的数据类型

public class GenericTest<T> {
    T a;

    public void show(T a) {
        System.out.println(a);
    }

    public static void main(String[] args) {
        GenericTest<String> objectGenericTest = new GenericTest<String>();
        objectGenericTest.show("a");
    }
}

指定传入的数据类型不能是基础数据类型,比如指定传入整型,就只能使用int类型的包装类型Integer

在Map中使用泛型

在代码中插入一条非String类型的数据

image-20230816180556484.png

所以,在使用泛型时申明了传入的数据类型,便只能传入对应的类型。

image-20230816180625776.png

提示:尖括号可以在一些版本下简写

Map<String, String> hashMap = new HashMap<String, String>();//后面的那个<>可以省略
Map<String, String> hashMap = new HashMap();//可以写成这样,但是只能是jdk6以上的版本才能省略

补充

迭代器Iterator

**增强型for循环(语法糖)**的底层都是靠迭代器实现,其原理大致为:

  1. 对于数组:增强for循环会隐式地创建一个ArrayIterator,该ArrayIterator负责遍历数组并返回数组的每个元素。对于基本类型的数组,这个ArrayIterator是类型特定的,即每个类型都会有一个对应的ArrayIterator类。
  2. 对于集合(Collection):增强for循环会隐式地创建一个迭代器,该迭代器会遍历集合并返回集合的每个元素。对于实现了Iterable接口的集合类(如List,Set等),增强for循环可以直接使用Iterable接口的iterator方法获取一个迭代器。
graph LR
HashSet-->|extends|AbstractSet-->|extends|AbstractCollection-->|implements|Collection-->|extends|Iterable

image-20230816193343149.png

Iterator使用案例

public class IteratorTest {
    public static void main(String[] args) {
        Set<Integer> set = new HashSet<Integer>();
        set.add(1);set.add(2);set.add(3);set.add(4);
        Iterator<Integer> iterator = set.iterator();
        while (iterator.hasNext()) {//判断是否存在下一个值
            System.out.println(iterator.next());//获取迭代器中的下一个元素
        }
    }
}
/*
1
2
3
4
**/

Collections集合框架工具类

public class CollectionsUtilTest {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i <= 54; i++) {
            list.add(i);//生成了一个0到54的列表
        }
        System.out.println(list);//列表初始状态
        Collections.addAll(list,55,56);//追加元素
        System.out.println(list);
        Collections.reverse(list);//将整个列表反向
        System.out.println(list);
        Collections.shuffle(list);//类似洗牌的操作,将整个列表打乱
        System.out.println(list);
    }
}

本节主要介绍了三个工具方法,主要用于列表中的操作,更多方法可以查看官方文档,搜索Collections。