集合框架List、Set和Map详解

集合框架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集合的简单总结,希望对大家有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值