集合框架List、Set和Map详解
文章开始让我们先看一下以下内容,对集合框架做个基本的了解。
1、 集合框架概述
(1)什么是集合框架:
尽管这些容器类非常好用,但是却不能集中和统一管理。集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法(底层都对应着某一种数据结构的算法)。
(2)为什么需要集合框架(把集合框架的类和接口都存放在java.util包中):
A、提供功能的复用(java.util包).
B、让程序猿专注于业务开发,而不是数据结构和算法.
(3)常用的集合类:
Set(集):集合中的对象不按特定方式排序,不允许元素重复.
List(列表):集合中的对象按照索引位置排序,允许元素重复.
Map(映射):集合中每一个元素都包含一对key和value对象.不允许key对象重复,值对象可以重复.
2、 Collection
(1) java.util.Collection是一个集合接口,他提供了对集合对象进行基本操作的通用接口方法。Collection接口的意义是为各种具体的集合提供了最大化的统一操作。Collection中具体的方法如下:
3、 List
List是Collection接口的子接口,也是最常用的接口,此接口对Collection接口进行了大量的扩充,里面的内容是允许重复的,会记录先后添加的顺序。
List方法如下:
3.1、Vector
在JDK2之前(在Java的集合框架之前),要存储多个数据,此时存在一个叫Vector类.
Vector类底层其实就是一个Object数组,Vector类中的方法是支持同步(方法使用synchronized修饰)的。
3.1.1、 Vector类存储原理:
(1)表面上把数据存储到Vector对象中,其实底层依然是把数据存储到Object数组中的.
(2)我们发现该数组的元素类型是Object类型,意味着集合中只能存储任意类型的对象.
集合中只能存储对象,不能存储基本数据类型的值.
在Java5之前,必须对基本数据类型手动装箱.
如:v.addElement(Integer.valueOf(123));
从Java5开始支持自动装箱操作,代码.
如:v.addElement(123);其实底层依然是手动装箱.
(3)集合类中存储的对象,都存储的是对象的引用,而不是对象本身.
3.1.2、集合类的常用操作方法:
增加:
boolean add(Object e) 将指定元素添加到此向量的末尾,等价于addElement方法。
void add(int index, Object element) 在此向量的指定位置插入指定的元素。
boolean addAll(Collection c) :把c集合中的元素添加到当前集合对象中.
- 删除:
Object remove(int index) :删除指定索引位置的元素,并返回删除之后的元素.
boolean remove(Object o):删除指定的元素.
boolean removeAll(Collection c):从此集合中移除包含在指定 集合c中的所有元素。
boolean retainAll(Collection c):在此集合中仅保留包含在指定 集合c中的元素,求两个集合的交集。
- 修改:
Object set(int index, Object element) :修改当前集合中指定索引位置的元素.
返回被替换的旧的元素.
- 查询:
int size() :返回当前集合中存储几个元素.
boolean isEmpty():判断当前集合中元素个数是否为0.
Object get(int index):查询指定索引位置的元素.
Object[] toArray():把集合对象转换为Object数组.
3.1.3、Vector类的常用操作方法:
栈(Stack):数据结构的一种,存储特点:Last In First Out. (后进先出)
3.2、ArrayList
ArrayList类是Java集合框架出现之后用来取代Vector类的:二者底层原理都是基于数组的算法,一模一样.
3.2.1、ArrayList和Vector的区别
Vector: 所有的方法都使用了synchronized修饰符. 线程安全但是性能较低. 适用于多线程环境.
ArrayList:所有的方法都没有使用synchronized修饰符.线程不安全但是性能较高.
即使以后在多线程环境下,我们也不使用Vector类,用以下方法可以实现同步:
ArrayList list = Collections.synchronizedList(new ArrayList(…));
3.2.2、ArrayList常用方法
3.3、LinkedList
LinkedList类是双向链表,单向队列,双向队列,栈的实现类:
LinkedList类实现单向队列和双向队列的接口,自身提高了栈操作的方法,链表操作的方法.
3.3.1、LinkedList和ArrayList的区别
链表没有索引的概念,本不应该有索引,但是从Java2开始,存在了集合框架,让LinkedList类作为了List接口的实现类,List中提供了该根据索引查询元素的方法,LinkedList内部类提供了一个变量来当做索引.该方法要少用,因为LinkedList相对ArrayList于不擅长做查询操作. 擅长保存和删除操作.
3.3.2、编写一个双向链表.
package com.it520.collection;
public class LinkDemo {
private Node first;//链表的第一个节点
private Node last;//链表的最后一个节点
private int size;//节点的数量
class Node {
Node prev;//上一个节点对象
Node next;//下一个节点对象
Node ele;//当前节点中储存的数据
}
}
3.3.3、LinkedList的常用方法
在LinkedList类中存在很多方法,但是功能都是相同的.LinkedList表示了多种数据结构的实现,每一种数据结构的操作名字不同
3.4、List总结
3.4.1、Vector类,ArrayList类,LinkedList类的比较
根据Vector类,ArrayList类,LinkedList类他们的特点,我就可以指定规范:遵循该规范的实现类,无论底层算法如何,都必须保证允许元素重复和保证添加先后顺序,我们给该规范起名:List.
Vector类:底层才有数组结构算法,方法都使用了synchronized修饰,线程安全,但是性能相对于ArrayList较低.
ArrayList类: 底层才有数组结构算法,方法没有使用synchronized修饰,线程不安全,性能相对于Vector较高.ArrayList现在机会已经取代了Vector的江湖地位.
为了保证ArrayList的线程安全,List list = Collections.synchronizedList(new ArrayList(…));
LinkedList类:底层才有双向链表结构算法,方法没有使用synchronized修饰,线程不安全.
3.4.2、数组结构算法和双向链表结构算法的性能问题:
数组结构算法: 插入和删除操作速度低,查询和更改较快.
链表结构算法: 插入和删除操作速度快,查询和更改较慢.
3.4.3使用的选择
Vector类打死不用!即使要用选ArrayList类.
如果删除和插入操作频繁,应该选择LinkedList类.
如果查询操作频繁,应该使用ArrayList类.
在开发中使用ArrayList较多,根据具体的需求环境来做选择
4、 Set
Set是Collection子接口,Set只包含从Collection继承的方法,不过Set无法记住添加的顺序,不允许包含重复的元素。当试图添加两个相同元素进Set集合,添加操作失败,add()方法返回false。Set判断两个对象是否相等用equals,而不是使用==。也就是说两个对象equals比较返回true,Set集合是不会接受这个两个对象的。
4.1、HashSet
HashSet是Set接口最常用的实现类,顾名思义,底层才用了哈希表(散列/hash)算法.其底层其实也是一个数组,存在的意义是提供查询速度,插入速度也比较快,但是适用于少量数据的插入操作.
4.1.1、HashSet如何判断两个对象是否相同
当往HashSet集合中添加新的对象的时候,先回判断该对象和集合对象中的hashCode值:
1):不等: 直接把该新的对象存储到hashCode指定的位置.
2):相等: 再继续判断新对象和集合对象中的equals做比较.
hashCode相同,equals为true: 则视为是同一个对象,则不保存在哈希表中.
hashCode相同,equals为false:非常麻烦,存储在之前对象同槽为的链表上(拒绝,操作比较麻烦).
4.1.2、对象的hashCode和equals方法的重要性:
每一个存储到hash表中的对象,都得提供hashCode和equals方法,用来判断是否是同一个对象.
存储在哈希表中的对象,都应该覆盖equals方法和hashCode方法,并且保证equals相等的时候,hashCode也应该相等.
4.2、LinkedHashSet
LinkedHashSet:底层才有哈希表和链表算法.
哈希表:来保证唯一性,.此时就是HashSet,在哈希表中元素没有先后顺序.
链表:来记录元素的先后添加顺序.
4.3、TreeSet
TreeSet集合底层才有红黑树算法,会对存储的元素默认使用自然排序(从小到大).
注意: 必须保证TreeSet集合中的元素对象是相同的数据类型,否则报错.
4.3.1、TreeSet的排序规则:
判断两个对象是否相等的规则:
自然排序: compareTo方法返回0;
定制排序: compare方法返回0;
- 自然排序(从小到大):
TreeSet调用集合元素的compareTo方法来比较元素的大小关系,然后讲集合元素按照升序排列(从小到大).
注意:要求TreeSet集合中元素得实现java.util.Comparable接口.
java.util.Comparable接口:可比较的.
覆盖 public int compareTo(Object o)方法,在该方法中编写比较规则.
在该方法中,比较当前对象(this)和参数对象o做比较(严格上说比较的是对象中的数据,比如按照对象的年龄排序).
this > o: 返回正整数. 1
this < o: 返回负整数. -1
this == o: 返回0. 此时认为两个对象为同一个对象
- 定制排序(从大到小,按照名字的长短来排序):
在TreeSet构造器中传递java.lang.Comparator对象**.并覆盖public int compare(Object o1, Object o2)再编写比较规则. compare方法返回0; **此时认为两个对象为同一个对象;
4.4、Set实现类性能对比
4.4.1、共同的特点:
1):都不允许元素重复.
2):都不是线程安全的类.
解决方案:Set s = Collections.synchronizedSet(Set对象);
4.4.2、HashSet,LinkedHashSet和TreeSet的特点
- HashSet: 不保证元素的先后添加顺序.
底层才有的是哈希表算法,查询效率极高.
判断两个对象是否相等的规则:
1):equals比较为true.
2):hashCode值相同.
要求:存在哈希中的对象元素都得覆盖equals和hashCode方法.
-
LinkedHashSet:
HashSet的子类,底层也采用的是哈希表算法,但是也使用了链表算法来维持元素的先后添加顺序.
判断两个对象是否相等的规则和HashSet相同.
因为需要多使用一个链表俩记录元素的顺序,所以性能相对于HashSet较低.
一般少用, 如果要求一个集合既要保证元素不重复,也需要记录添加先后顺序,才选择使用LinkedHashSet.
- TreeSet
不保证元素的先后添加顺序,但是会对集合中的元素做排序操作.
底层才有红黑树算法(树结构,比较擅长做范围查询).
TreeSet要么才有自然排序,要么定制排序.
自然排序: 要求在TreeSet集合中的对象必须实现java.lang.Comparable接口,并覆盖compareTo方法.
定制排序: 要求在构建TreeSet对象的时候,传入一个比较器对象(必须实现java.lang.Comparator接口). 在比较器中覆盖compare方法,并编写比较规则.
TreeSet判断元素对象重复的规则: compareTo/compare方法是否返回0.如果返回0,则视为是同一个对象.
- HashSet做等值查询效率高,TreeSet做范围查询效率高.
而我们更多的情况,都是做等值查询, 在数据库的索引中做范围查询较多,所以数结构主要用于做索引,用来提高查询效率.
5、 Map
严格上说,Map并不是集合,而是两个集合之间的映射关系(Map接口并没有继承于Collection接口),然而因为Map可以存储数据(每次存储都应该存储A集合中以一个元素(key),B集合中一个元素(value)),我们还是习惯把Map也称之为集合.因为:Map接口并没有继承于Collection接口也没有继承于Iterable接口,所以不能直接对Map使用for-each操作.
5.1、Map的操作方法
package com.it520.collection;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapDemo {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("s01", "david");
map.put("s02", "Bob");
map.put("s03", "lucy");
Set<String> keys = map.keySet();
for(String key:keys) {
System.out.println(key+"->"+map.get(key));
}
System.out.println("==========");
Collection<String> values = map.values();
for(String value : values) {
System.out.println(value);
}
Set<Map.Entry<String , String>> entrys = map.entrySet();
for (Map.Entry<String, String> entry: entrys) {
String k = entry.getKey();
String v = entry.getValue();
System.out.println(k+"->"+v);
}
}
5.2、Map的实现类
-
HashMap:采用哈希表算法, 底层是数组和链表+红黑树,此时Map中的key不会保证添加的先后顺序,key也不允许重复.
key判断重复的标准是: key1和key2是否equals为true,并且hashCode相等.
-
TreeMap:采用红黑树算法,此时Map中的key会按照自然顺序或定制排序进行排序,key也不允许重复.
key判断重复的标准是: compareTo/compare的返回值是否为0.
-
LinkedHashMap: 采用链表和哈希表算法,此时Map中的key会保证先后添加的顺序,key不允许重复. key判断重复的标准和HashMap中的key的标准相同.
-
Hashtable:采用哈希表算法,是HashMap的前身(类似于Vector是ArrayList的前身).在Java的集合框架之前,表示映射关系就使用Hashtable.所有的方法都使用synchronized修饰符,线程安全的,但是性能相对HashMap较低.
-
Properties: Hashtable的子类,此时要求key和value都是String类型.用来加载资源文件
-
ConcurrentHashMap:从JDK1.2起,就有了HashMap,正如前一篇文章所说,HashMap不是线程安全的,因此多线程操作时需要格外小心。它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中,这个目前用的越来越多,至于和他的使用和性能对比可以参考:https://blog.csdn.net/xuefeng0707/article/details/40834595,写的很具体。
在JDK1.5中,伟大的Doug Lea给我们带来了concurrent包,从此Map也有安全的了。
-
一般的,我们定义Map,key都使用不可变的类(String),把key作为value的唯一名称.
HashMap和TreeMap以及LinkedHashMap都是线程不安全的,但是性能较高:
解决方案: Map m = Collections.synchronizedMap(Map对象);
Hashtable类实现线程安全的,但是性能较低
哈希表算法:做等值查询最快.
数结构算法:做范围查询最快–>应用到索引上.
下面举个例子,如何得出一个字符串里面各个字母出现的次数:
public class MapDemo2 {
public static void main(String[] args) {
String str = "dajdhauiabdagyadha";
char[] charArray = str.toCharArray();//将String字符串转换位char数据,其实String的底层就是char[]
Map<Character, Integer> map = new TreeMap<>();//创建一个Map对象用于存放字母和出现次数的Key-value关系
for (char cha : charArray) { //遍历char[]
if(map.containsKey(cha)) {
Integer old = map.get(cha);//如果当前的map中存在cha就取出当前cha在map中的value,及之前出现的次数
old++;
map.put(cha, old);
}else {
map.put(cha, 1);//如果当前的map中不存在cha,就put新的数据,value设置成1
}
}
System.out.println(map);//{a=6, b=1, d=4, g=1, h=2, i=1, j=1, u=1, y=1}
}
}
5.3、HashMap和Hashtable的区别
HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。
-
HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
-
HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
-
由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
5.4、HashMap的底层存储原理
先看一张图,关于HashMap的底层存储原理简介,关于HashMap的底层会在后续详细总结:
put:(key-value)方法是HashMap中最重要的方法,使用HashMap最主要使用的就是put,get两个方法。
1、判断键值对数组table[i]是否为空或者为null,否则执行resize()进行扩容;
2、根据键值key计算hash值得到插入的数组索引 i ,如果table[i] == null ,直接新建节点添加即可,转入6,如果table[i] 不为空,则转向3;
3、判断table[i] 的首个元素是否和key一样,如果相同(hashCode和equals)直接覆盖value,否则转向4;
4、判断table[i] 是否为treeNode,即table[i]是否为红黑树,如果是红黑树,则直接插入键值对,否则转向5;
5、遍历table[i] ,判断链表长度是否大于8,大于8的话把链表转换成红黑树,进行插入操作,否则进行链表插入操作;便利时遇到相同key直接覆盖value;
6、插入成功后,判断实际存在的键值对数量size是否超过了threshold,如果超过,则扩容。
原文链接:https://blog.csdn.net/Yao_shen_yun/article/details/94451122
6、List和Set以及Map的选用
6.1、选用取决于当前业务的需求:
List:单一元素集合.
允许元素重复/记录元素的添加顺序.
Set:单一元素集合.
不允许元素重复/不记录元素的添加顺序.
既要不重复,又要保证先后顺序:LinkedHashSet.
Map: 双元素集合. 如果存储数据的时候,还得给数据其为一个的一个名称,此时考虑使用
6.2、List和Set以及Map之间相互转换问题:
List list = new ArrayList<>();
把List转换为Set:
Set set = new HashSet<>(list);//此时会消除重复的元素.
把Set转换为List:
List list2 = new ArrayList<>(set );
Map不能直接转换为List或Set(但是Map中的方法可以间接转换).
7、 集合的工具类
7.1、Arrays类
在Collection接口中有一个方法叫toArray把集合转换为Object数组.
把集合转换为数组: Object[] arr = 集合对象.toArray();
数组也可以转换为集合(List集合):
List<String> lis = Arrays.asList("A","B","C","D");//[A, B, C, D]
List<Date> lis1 = Arrays.asList(new Date(),new Date());//[Tue Jun 09 22:47:24 CST 2020, Tue Jun 09 22:47:24 CST 2020]
通过Arrays.asList方法得到的List对象的长度是固定的,不能增,也不能减.因为 asList方法返回的ArrayList对象,不是java.util.ArrayList而是Arrays类中的内部类对象.
7.2、Collections类
Collections类:封装了Set,List,Map的操作的工具方法.
获取空集对象(没有元素的集合,注意集合不为null):
HashSet/ArrayList/HashMap都是线程不安全的,在多线程环境下不安全.
在Collections类中有获取线程安全的集合方法:
List list = Collections.synchronizedList(new ArrayList());
8、集合的迭代
8.1、List和Set的迭代
主要是for循环迭代,for-each增强for循环和迭代器三种方法,代码已List集合为例。
public static void main(String[] args) {
List<Integer> lst = new ArrayList<>();
for(int i = 0 ; i < 10; i++) {
lst.add(i);
}
System.out.println(lst);
for(int j = 0 ; j < lst.size();j++) {//for循环迭代
System.out.print(lst.get(j));
}
Iterator<Integer> itr = lst.iterator(); //迭代器
while (itr.hasNext()) {
System.out.print(itr.next());
}
for (Integer ls :lst) { //for-each增强for循环
System.out.println(ls);
}
}
8.2、Map的遍历
主要有以上四种方法:通过键找值遍历(效率低)、在for-each循环中使用entries来遍历、在for-each循环中使用entries来遍历、
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("1", 111);
map.put("2", 222);
map.put("3", 333);
map.put("4", 444);
//通过键找值遍历(效率低)
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println("Key = " + key + ", Value = " + value);
}
//在for-each循环中使用entries来遍历
for(Map.Entry<String, Integer> ent : map.entrySet()) {
System.out.println("Key = " + ent.getKey() + ", Value = " + ent.getValue());
}
//在for-each循环中遍历keys或values。
for(String k: map.keySet()) {
System.out.println("Key = "+k);//遍历键
}
for(Integer v: map.values()) {
System.out.println("Value = "+v);//遍历值
}
//使用Iterator遍历()
Iterator<Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Entry<String, Integer> entry = iterator.next();
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
}
8.3、迭代时删除(并发修改异常)
当需要边迭代集合元素,边删除指定的元素时:此时只能使用迭代器. 而且只能使用迭代器对象的remove方法.
public static void main(String[] args) {
List<String> lst = new ArrayList<>();
lst.add("A");
lst.add("B");
lst.add("C");
lst.add("D");
for(String ls:lst) {
if("B".equals(ls)) {
lst.remove(ls);
}
}
System.out.println(lst);
}
//结果Exception in thread "main" java.util.ConcurrentModificationException“并发修改异常”
选择使用迭代器删除
public static void main(String[] args) {
List<String> lst = new ArrayList<>();
lst.add("A");
lst.add("B");
lst.add("C");
lst.add("D");
Iterator<String> itr = lst.iterator();
while(itr.hasNext()) {
if("B".equals(itr.next())) {
itr.remove();
}
}
System.out.println(lst);//[A, C, D]
}
以上是对List、Set和Map集合的简单总结,希望对大家有所帮助。