首页 专题 文章 代码 归档
Java 集合基础
2020.03.12 09:42 2020.03.12 11:20

1. 集合概念

什么是集合?

集合可以看作是一种容器,用来存储对象信息。所有集合类都位于java.util包下,但支持多线程的集合类位于java.util.concurrent包下。

为什么要在计算机中引入集合呢?

这是为了便于处理一组类似的数据,那么狠显然,数组就是一个最简单的集合:

public class Main {
    public static void main(String[] args) {
        int[] grades = {90, 89, 60, 88, 78, 96, 69};
        System.out.println(Arrays.toString(grades));
    }
}

那既然有了数组,为什么Java还要提供其他集合来供我们使用呢?

数组特点:

  • 数组初始化后大小不可变;
  • 数组只能按索引顺序存取。

但我们需要各种不同类型的集合类来处理不同的数据:

  • 可变大小的顺序链表;
  • 保证无重复元素的集合;

Java集合关系:

截图-1583974286

如上面的图,Java的集合主要有List ,Set, Map

List , Set继承至Collection接口,Map为独立接口

List下有ArrayListLinkedListVector(已废弃)

Set下有HashSetLinkedHashSetTreeSet

Map下有HashMapLinkedHashMapTreeMapHashtable

2. Collection接口

Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口。

Java集合的设计有几个特点:

  • 一是实现了接口和实现类相分离,例如,有序表的接口是List,具体的实现类有ArrayList,LinkedList等
  • 二是支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素,例如:
List<Integer> list = new ArrayList<>();// 只能放入Integer类型的数据
list.add(12341);
list.add(112341);
list.add(123141);

2.1. 主要方法

前面说了java集合中有两个接口:CollectionMap

Collection下面有两个:List和Set,所以我们需要先知道Collection中定义了哪些可用方法

Modifier and Type Method and Description
boolean add(E e) 确保此集合包含指定的元素(可选操作)。
boolean addAll(Collection c) 将指定集合中的所有元素添加到这个集合(可选操作)。
void clear() 从这个集合中移除所有的元素(可选操作)。
boolean contains(Object o) 返回 true如果集合包含指定元素。
boolean containsAll(Collection c) 返回 true如果这个集合包含指定集合的所有元素。
boolean equals(Object o) 将指定的对象与此集合进行比较,以进行相等性。
int hashCode() 返回此集合的哈希代码值。
boolean isEmpty() 返回 true如果集合不包含任何元素。
Iterator iterator() 返回此集合中的元素的迭代器。
default Stream parallelStream() 返回一个可能并行 Stream与集合的来源。
boolean remove(Object o) 从这个集合中移除指定元素的一个实例,如果它是存在的(可选操作)。
boolean removeAll(Collection c) 删除此集合中包含的所有元素(可选操作)的所有元素(可选操作)。
default boolean removeIf(Predicate filter) 删除满足给定谓词的这个集合的所有元素。
boolean retainAll(Collection c) 仅保留包含在指定集合中的这个集合中的元素(可选操作)。
int size() 返回此集合中的元素的数目。
default Spliterator spliterator() 创建此集合中的元素的Spliterator。
default Stream stream() 返回一个序列 Stream与集合的来源。
Object[] toArray() 返回包含此集合中所有元素的数组。
T[] toArray(T[] a)

所以List等都有以上的方法,初学者我们就是要简单学起;

2.2. List

List特点:有序,可重复

List也是一个接口,继承自Collection,而其下面有:ArrayListLinkedList

ArrayList:

  • 优点: 底层数据结构是数组,查询快,增删慢。
  • 缺点: 线程不安全,效率高

LinkedList:

  • 优点: 底层数据结构是链表,查询慢,增删快。
  • 缺点: 线程不安全,效率高

新建方法:

前面我们学了向上转型,所以一般new实现类,左边的类型都是父类List而不是ArrayListLinkedList,如下:

List<Integer> list = new ArrayList<>();// 只能放入Integer类型的数据
List<String> list2 = new LinkedList<>();

添加方法:

List<Integer> list = new ArrayList<>();// 只能放入Integer类型的数据
list.add(123);
list.add(3534);
list.add(312);
list.add(312);

清空方法:

List<Integer> list = new ArrayList<>();// 只能放入Integer类型的数据
list.add(123);
list.add(3534);
list.add(312);
list.add(312);
System.out.println("清空之前:" + list.toString());
list.clear();
System.out.println("清空之后:" + list.toString());

/*清空之前:[123, 3534, 312, 312]
清空之后:[]*/

判空方法:

List<Integer> list = new ArrayList<>();// 只能放入Integer类型的数据
list.add(123);
list.add(3534);
list.add(312);
list.add(312);
System.out.println("清空之前:" + list.toString());
System.out.println("清空之前(isEmpty):" + list.isEmpty());
list.clear();
System.out.println("清空之后:" + list.toString());
System.out.println("清空之后(isEmpty:" + list.isEmpty());

/*清空之前:[123, 3534, 312, 312]
清空之前(isEmpty):false
清空之后:[]
清空之后(isEmpty:true*/

删除方法:

List<Integer> list = new ArrayList<>();// 只能放入Integer类型的数据
list.add(123);
list.add(3534);
list.add(312);
list.add(312);
System.out.println("删除之前:" + list.toString());
list.remove(Integer.valueOf(312));
System.out.println("删除之后:" + list.toString());
/*删除之前:[123, 3534, 312, 312]
删除之后:[123, 3534, 312]*/

注意:remove方法有两个重载:

  • remove(Object o);
  • remove(int index);

一个时按照索引删除,一个时按照元素删除,但是上面我们list装入的是整型,如果直接输入312,它便以为是按照索引删除,导致报错;

所以我们 使用Integer的一个静态方法valueOf来传入参数;

返回数目:

List<Integer> list = new ArrayList<>();// 只能放入Integer类型的数据
list.add(123);
list.add(3534);
list.add(312);
list.add(312);
System.out.println("删除之前:" + list.toString());
System.out.println("删除之前(size):" + list.size());
list.remove(Integer.valueOf(312));
System.out.println("删除之后:" + list.toString());
System.out.println("删除之后(size):" + list.size());

/*删除之前:[123, 3534, 312, 312]
删除之前(size):4
删除之后:[123, 3534, 312]
删除之后(size):3*/

2.3. Set

Set 无序,唯一

Set下面有:HashSetLinkedHashSetTreeSet

前面说了Set和List都是继承自Collection接口,所以基本方法两者都一样,这里不多说:

private static void test01() {
    Set<Integer> set = new HashSet<>();
    set.add();
    set.remove();
    set.contains();
    set.isEmpty();
    set.size();
}

需要说的是这些"Set"的一些特性(保证元素无序)

HashSet

底层数据结构是哈希表。(无序,唯一)

如何来保证元素唯一性?

依赖两个方法:hashCode()和equals()

HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。

具体过程:

  1. 存储元素时首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,肯定是不同的对象。
  2. hashCode值相同,再比较equals方法。
  3. equals相同,对象相同。(则无需储存)

LinkedHashSet

底层数据结构是链表和哈希表。(FIFO插入有序,唯一)

  1. 由链表保证元素有序
  2. 由哈希表保证元素唯一

LinkedHashSet底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。线程不安全,效率高。

TreeSet

底层数据结构是红黑树。(唯一,有序)

如何保证元素排序的呢?

  • 自然排序
  • 比较器排序 如何保证元素唯一性的呢?
  • 根据比较的返回值是否是0来决定

TreeSet底层数据结构采用红黑树来实现,元素唯一且已经排好序;

唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性。

根据构造方法不同,分为自然排序(无参构造)和比较器排序(有参构造),自然排序要求元素必须实现Compareable接口,并重写里面的compareTo()方法,元素通过比较返回的int值来判断排序序列,返回0说明两个对象相同,不需要存储;

比较器排需要在TreeSet初始化是时候传入一个实现Comparator接口的比较器对象,或者采用匿名内部类的方式new一个Comparator对象,重写里面的compare()方法;

2.4. List与Set简单比较

1、List可重复、有序:

List<Integer> list = new ArrayList<>();
list.add(1231);
list.add(1231);
list.add(13113);
list.add(7912);
list.add(7934);
System.out.println(list.toString());
/*[1231, 1231, 13113, 7912, 7934]*/

按照插入顺序存储,且可重复;

2、Set不可重复,无序:

HashSet

Set<Integer> set = new HashSet<>();
set.add(1231);
set.add(1231);
set.add(8972);
set.add(9082);
set.add(98111);
set.add(8912);
System.out.println(set.toString());
/*[8912, 9082, 8972, 98111, 1231]*/

无序;


LinkedHashSet

Set<Integer> set = new LinkedHashSet<>();
set.add(1231);
set.add(1231);
set.add(8972);
set.add(9082);
set.add(98111);
set.add(8912);
System.out.println(set.toString());
/*[1231, 8972, 9082, 98111, 8912]*/

底层数据结构是链表和哈希表。(FIFO插入有序,唯一)


TreeSet

Set<Integer> set = new TreeSet<>();
set.add(1231);
set.add(1231);
set.add(8972);
set.add(9082);
set.add(99999);
set.add(98111);
set.add(8912);
System.out.println(set.toString());
/*[1231, 8912, 8972, 9082, 98111, 99999]*/

可以看出是通过自然排序比较器排序来排序的,因为是输出结果是按从小到大输出的;

3. Map接口

Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value。

Map接口有四个比较重要的实现类,分别是HashMapLinkedHashMapTreeMapHashTable

TreeMap是有序的,HashMap和HashTable是无序的。

Hashtable的方法是同步的,HashMap的方法不是同步的。这是两者最主要的区别。


Map接口方法:

Modifier and Type Method and Description
void clear() 从这个映射中移除所有的映射(可选操作)。
default V compute(K key, BiFunction remappingFunction) 试图计算出指定键和当前的映射值的映射(或 null如果没有当前映射)。
default V computeIfAbsent(K key, Function mappingFunction) 如果指定的键是不是已经与价值相关的(或映射到 null),尝试使用给定的映射功能,进入到这个Map除非 null计算其价值。
default V computeIfPresent(K key, BiFunction remappingFunction) 如果指定键的值是存在和非空的,尝试计算一个新的映射,给出了键和它当前的映射值。
boolean containsKey(Object key) 返回 true如果这Map包含一个指定的键映射。
boolean containsValue(Object value) 返回 true如果映射到指定的值的一个或多个键。
Set> entrySet() 返回一个 Set视图的映射包含在这个Map。
boolean equals(Object o) 将指定的对象与此映射的相等性进行比较。
default void forEach(BiConsumer action) 在该映射中的每个条目执行给定的操作,直到所有的条目被处理或操作抛出异常。
V get(Object key) 返回指定的键映射的值,或 null如果这个Map不包含的键映射。
default V getOrDefault(Object key, V defaultValue) 返回指定的键映射的值,或 defaultValue如果这个Map不包含的键映射。
int hashCode() 返回此映射的哈希代码值。
boolean isEmpty() 返回 true如果这个Map不包含键值的映射。
Set keySet() 返回一个 Set的关键视图包含在这个Map。
default V merge(K key, V value, BiFunction remappingFunction) 如果指定的键已与值相关联的值或与空值相关联的,则将其与给定的非空值关联。
V put(K key, V value) 将指定的值与此映射中的指定键关联(可选操作)。
void putAll(Map m) 从指定的映射到这个Map(可选操作)复制所有的映射。
default V putIfAbsent(K key, V value) 如果指定的键是不是已经与价值相关的(或映射到 null)将其与给定的值并返回 null,否则返回当前值。
V remove(Object key) 如果存在(可选操作),则从该Map中移除一个键的映射。
default boolean remove(Object key, Object value) 仅当它当前映射到指定的值时,为指定的键移除条目。
default V replace(K key, V value) 仅当它当前映射到某一值时,替换指定的键的条目。
default boolean replace(K key, V oldValue, V newValue) 仅当当前映射到指定的值时,替换指定的键的条目。
default void replaceAll(BiFunction function) 将每个条目的值替换为在该项上调用给定函数的结果,直到所有的条目都被处理或函数抛出异常。
int size() 返回这个映射中的键值映射的数目。
Collection values() 返回一个 Collection视图的值包含在这个Map。

3.1. HashMap

Map 主要用于存储键(key)值(value)对,根据键得到值,因此键不允许重复,但允许值重复

HashMap 是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。

HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null;

HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。

HashMap基于哈希表结构实现的 ,当一个对象被当作键时,必须重写hasCode和equals方法

3.2. LinkedHashMap

LinkedHashMap继承自HashMap,它主要是用链表实现来扩展HashMap类,HashMap中条目是没有顺序的,但是在LinkedHashMap中元素既可以按照它们插入图的顺序排序,也可以按它们最后一次被访问的顺序排序。

3.3. TreeMap

TreeMap基于红黑树数据结构的实现,键值可以使用Comparable或Comparator接口来排序。TreeMap继承自AbstractMap,同时实现了接口NavigableMap,而接口NavigableMap则继承自SortedMap。SortedMap是Map的子接口,使用它可以确保图中的条目是排好序的。

在实际使用中,如果更新图时不需要保持图中元素的顺序,就使用HashMap,如果需要保持图中元素的插入顺序或者访问顺序,就使用LinkedHashMap,如果需要使图按照键值排序,就使用TreeMap。

3.4. Hashtable

Hashtable和前面介绍的HashMap很类似,它也是一个散列表,存储的内容是键值对映射,不同之处在于,Hashtable是继承自Dictionary的,Hashtable中的函数都是同步的,这意味着它也是线程安全的,另外,Hashtable中key和value都不可以为null。

3.5. 简单应用

Map最大的一个特性就是键值对,也即我们可以通过取出相应的值;

比如我需要统计班上同学的数学成绩,肯定需要包括对应成绩的人名:

Map<String, Integer> math = new HashMap<>();
math.put("无道", 90);
math.put("迷思", 70);
math.put("死爱", 88);
System.out.println(math.toString());
/*{迷思=70, 死爱=88, 无道=90}*/

添加方法:

math.put("无道", 90);

Map这里使用putCollection是使用的add

判空:

boolean empty = math.isEmpty();

条目:

int size = math.size();

删除:

Integer r = math.remove("无道");

获取:

Integer grade = math.get("无道");

4. 循环数据

光是存储数据还不行,我们很多时候是需要获取List或者Map里面的数据的;

所以,循环遍历Collection和Map也是需要的;

4.1. 遍历List

方法一:for遍历

List<Integer> list = new ArrayList<>();
list.add(1231);
list.add(1231);
list.add(13113);
list.add(7912);
list.add(7934);

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i).intValue());
}
/*1231
1231
13113
7912
7934
*/

方法二:forEach(增强for循环)

List<Integer> list = new ArrayList<>();
list.add(1231);
list.add(1231);
list.add(13113);
list.add(7912);
list.add(7934);

for (Integer integer : list) {
    System.out.println(integer);
}
/*1231
1231
13113
7912
7934
*/

JDK8又新增了一种Lambda表达式,可以简化:

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

当然,你以为上面已经是最简单了的吗?不是的,还有更简单的:

list.forEach(System.out::println);

方法三:Iterator遍历

Iterator<Integer> iterator = list.iterator();//这是创建一个迭代器
while (iterator.hasNext()) {//hasNext方法,判断是否有下一个数据
    Integer next = iterator.next();
    System.out.println("next = " + next);
}

4.2. 遍历Map

在java中所有的map都实现了Map接口,因此所有的Map(如HashMap, TreeMap, LinkedHashMap, Hashtable等)都可以用以下的方式去遍历。

方法一:在for循环中使用entries实现Map的遍历

Map<String, Integer> math = new HashMap<>();
math.put("无道", 90);
math.put("迷思", 70);
math.put("死爱", 88);

for (Map.Entry<String, Integer> entry : math.entrySet()) {
    String key = entry.getKey();
    Integer val = entry.getValue();
    System.out.println("key:" + key + ",val:" + val);
}
/*key:迷思,val:70
key:死爱,val:88
key:无道,val:90*/

方法二:在for循环中遍历key或者values

一般适用于只需要map中的key或者value时使用,在性能上比使用entrySet较好;

Map<String, Integer> math = new HashMap<>();
math.put("无道", 90);
math.put("迷思", 70);
math.put("死爱", 88);

for (String key : math.keySet()) {
    System.out.print(key + " ");
}
System.out.println();
for (Integer val : math.values()) {
    System.out.println("val = " + val);
}
/*迷思 死爱 无道 
val = 70
val = 88
val = 90*/

方法三:通过Iterator遍历

Map<String, Integer> math = new HashMap<>();
math.put("无道", 90);
math.put("迷思", 70);
math.put("死爱", 88);

Iterator<Map.Entry<String, Integer>> iterator = math.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, Integer> entry = iterator.next();
    String key = entry.getKey();
    int val = entry.getValue();
    System.out.println("key = " + key);
    System.out.println("val = " + val);
}
/*key = 迷思
val = 70
key = 死爱
val = 88
key = 无道
val = 90*/

方法四:通过键找值遍历

这种方式的效率比较低,因为本身从键取值是耗时的操作;

Map<String, Integer> math = new HashMap<>();
math.put("无道", 90);
math.put("迷思", 70);
math.put("死爱", 88);

for (String key : math.keySet()) {
    Integer integer = math.get(key);
    System.out.println("key:" + key + ",val=" + integer);
}
/*key:迷思,val=70
key:死爱,val=88
key:无道,val=90*/
本节阅读完毕! (分享
二维码图片 扫描关注我们哟