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集合关系:
如上面的图,Java的集合主要有List
,Set
, Map
List , Set继承至Collection
接口,Map
为独立接口
List下有ArrayList
,LinkedList
,Vector
(已废弃)
Set下有HashSet
,LinkedHashSet
,TreeSet
Map下有HashMap
,LinkedHashMap
, TreeMap
,Hashtable
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集合中有两个接口:Collection
和Map
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,而其下面有:ArrayList
和LinkedList
ArrayList:
- 优点: 底层数据结构是数组,查询快,增删慢。
- 缺点: 线程不安全,效率高
LinkedList:
- 优点: 底层数据结构是链表,查询慢,增删快。
- 缺点: 线程不安全,效率高
新建方法:
前面我们学了向上转型,所以一般new
实现类,左边的类型都是父类List
而不是ArrayList
或LinkedList
,如下:
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下面有:HashSet
、LinkedHashSet
、TreeSet
;
前面说了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()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
具体过程:
- 存储元素时首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,肯定是不同的对象。
- hashCode值相同,再比较equals方法。
- equals相同,对象相同。(则无需储存)
LinkedHashSet
底层数据结构是链表和哈希表。(FIFO插入有序,唯一)
- 由链表保证元素有序
- 由哈希表保证元素唯一
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接口有四个比较重要的实现类,分别是HashMap
、LinkedHashMap
、TreeMap
和HashTable
。
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这里使用put
而Collection
是使用的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*/