Java集合篇:集合类介绍

Java集合篇:集合类介绍

Java集合篇:集合类介绍

上面的图展示了整个集合大家族的成员以及他们之间的关系。下面就上面的各个接口、基类做一些简单的介绍(主要介绍各个集合的特点,区别)。

一、Collection接口:

Collection接口是最基本的集合接口,它不提供直接的实现,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。Collection所代表的是一种规则,它所包含的元素都必须遵循一条或者多条规则。如有些允许重复而有些则不能重复、有些必须要按照顺序插入而有些则是散列,有些支持排序但是有些则不支持。

在Java中所有实现了Collection接口的类都必须提供两套标准的构造函数,一个是无参,用于创建一个空的Collection,一个是带有Collection参数的有参构造函数,用于创建一个新的Collection,这个新的Collection与传入进来的Collection具备相同的元素。 

 

二、List接口:

List接口为Collection直接接口。List所代表的是有序的Collection,即它用某种特定的插入顺序来维护元素顺序。用户可以对列表中每个元素的插入位置进行精确地控制,同时可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。

2.1、ArrayList:

(1)ArrayList底层是一个基于数组的非同步实现,是线程不安全的,它的操作基本上都是基于对数组的操作。

(2)允许包括null在内的所有元素,ArrayList擅长于随机访问,所以对元素的查询快,增删慢,在ArrayList的中间插入或删除一个元素,该位置后面的元素都会被移动,查询的时间复杂度为O(1),插入和删除的时间复杂度为O(n)。

(3)初始容量为10,数组扩容时,会将旧数组中的元素重新拷贝一份到新的数组中,每次数组容量增长大约是其容量的1.5倍,这种操作的代价很高。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。

(4)采用了Fail-Fast机制,面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

(5)remove方法会让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便GC。

size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。

2.2、LinkedList:

(1)LinkedList是基于双向链表的非同步实现,所以是线程不安全的;

(2)允许包括null在内的所有元素。

(3)LinkedList不能随机访问,在查找元素的时候,需要从开头或结尾遍历列表(先判断index是在链表的哪一半,然后再去对应区域查找,这样最多只要遍历链表的一半节点即可找到)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。

(4)虽然LinkedList对元素的查询慢,时间复杂度为O(n),但是对元素的增删快,插入或删除元素只需要修改元素的指针,时间复杂度为O(1)

2.3、Vector:

(1)与ArrayList相似,但是Vector是同步的,底层使用synchronized进行加锁。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。

(2)初始容量是10,扩容时,Vector的容量增加capacityIncrement如果没有指定capacityIncrement,则将容量增加为原来的两倍。

2.4、Stack:

(1)Stack继承自Vector,实现一个后进先出的堆栈。所以用法、线程安全什么的跟Vector都差不多,Stack刚创建后是空栈。

(2)Stack还提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。

 

三、Map接口:

Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。同时它也没有继承Collection。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。实现map的有:HashMap、TreeMap、HashTable、Properties、EnumMap。

3.1、HashMap(JDK1.8及以上):

(1)HashMap是基于哈希表的Map接口的非同步实现,允许使用null值和null键,但不保证映射的顺序,它是为快速查询而设计的。

(2)数据结构可以看成数组+链表+红黑树。底层使用数组实现,数组中每一项是个单向链表,即数组和链表的结合体;当链表长度大于一定阈值时,链表转换为红黑树,这样减少链表查询时间。

(3)HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Node对象。HashMap底层采用一个Node[]数组来保存所有的key-value对,当需要存储一个Node对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Node时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Node。

(4)HashMap进行数组扩容需要重新计算扩容后每个元素在数组中的位置,很耗性能。

HashMap的扩容代价非常大,要生成一个新的桶数组,然后要把所有元素都重新Hash落桶一次,几乎等于重新执行了一次所有元素的put。所以如果我们对Map的大小有一个范围的话,可以在构造时给定大小,一般大小设置为:(int) ((float) expectedSize / 0.75F + 1.0F)。

(5)采用了Fail-Fast机制,通过一个modCount值记录修改次数,对HashMap内容的修改都将增加这个值。迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,马上抛出异常。

3.2、Hashtable:

(1)Hashtable是基于哈希表的Map接口的同步实现,使用synchronized实现线程安全,即每次锁住整张表让线程独占。

(2)底层使用数组实现,数组中每一项是个单链表,即数组和链表的结合体,采用链表法解决哈希冲突;

(3)不允许使用null值和null键;

(4)Hashtable在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

3.3、ConcurrentHashMap(JDK1.7版本):

(1)ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同段进行的修改,每个段其实就是一个小的hashtable,它们有各自的锁。只要多个并发发生在不同的段上,它们就可以并发进行。

(2)与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment),ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁,但在HashMap中的实现中,因为允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。

(3)ConcurrentHashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

(4)在JDK1.8之后的版本中,抛弃了Segment锁分段的概念,而是才赢CAS算法,保存并发安全性,同时底层数据结构可以看成“数组+链表+红黑树”。

3.4、TreeMap:

键以某种排序规则排序,内部以red-black(红-黑)树数据结构实现,实现了SortedMap接口。

3.5 LinkedHashMap:

(1)LinkedHashMap继承于HashMap,底层使用哈希表和双向链表来保存所有元素,并且它是非同步,允许使用null值和null键。

(2)基本操作与父类HashMap相似,通过重写HashMap相关方法,重新定义了数组中保存的元素Entry,来实现自己的链接列表特性。该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而构成了双向链接列表。

3.6、WeakHashMap:

WeakHashMap与HashMap的用法基本相同,区别在于:后者的key保留对象的强引用,即只要HashMap对象不被销毁,其对象所有key所引用的对象不会被垃圾回收,HashMap也不会自动删除这些key所对应的键值对对象。但WeakHashMap的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被回收。WeakHashMap中的每个key对象保存了实际对象的弱引用,当回收了该key所对应的实际对象后,WeakHashMap会自动删除该key所对应的键值对。

3.7、IdentifyHashMap:

IdentityHashMap与HashMap基本相似,只是当两个key严格相等时,即key1==key2时,它才认为两个key是相等的 。IdentityHashMap也允许使用null,但不保证键值对之间的顺序。

 

四、Set接口:

Set是一种不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与List一样,它同样运行null的存在但是仅有一个。由于Set接口的特殊性,所有传入Set集合中的元素都必须不同,同时要注意任何可变对象,如果在对集合中元素进行操作时,导致e1.equals(e2)==true,则必定会产生某些问题。实现了Set接口的集合有:EnumSet、HashSet、TreeSet。

3.1、HashSet:

(1)HashSet基于HashMap实现,API也是对HashMap的行为进行了封装。

(2)HashSet堪称查询速度最快的集合,因为其内部是以HashCode来实现的。它内部元素的顺序是由哈希码来决定的,所以它不保证set 的迭代顺序,特别是它不保证该顺序恒久不变,并允许使用null元素。

3.2、LinkedHashSet:

(1)对于LinkedHashSet而言,它继承与HashSet、又基于LinkedHashMap来实现的。

(2)LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同。

3.3、TreeSet:

基于TreeMap,生成一个总是处于排序状态的set,内部以TreeMap来实现。它是使用元素的自然顺序对元素进行排序,或者根据创建Set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。

3.4、EnumSet:

是枚举的专用Set。所有的元素都是枚举类型。

 

五、Queue:

队列,它主要分为两大类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。另一种队列则是双端队列,支持在头、尾两端插入和移除元素,主要包括:ArrayDeque、LinkedBlockingDeque、LinkedList。

 

六、相关问题:

6.1、Set集合是如何保证对象不重复的?

Set集合不允许有重复出现的对象,且最终的判断是根据equals()的。其实原理是这样的:HashSet的底层采用HashMap来存放数据,HashMap的put()方法是这样的:

  public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());//----------1----------
        int i = indexFor(hash, table.length);//-----------2---------
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//-----------3---------
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }//------------------4--------------------
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

当向HashMap中添加元素的时候,首先计算元素的hashcode值,然后根据1处的代码计算出Hashcode的值,再根据2处的代码计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则看3-4的代码,遍历索引为i的链上的元素,如果key重复,则替换并返回oldValue值。

6.2、集合类排序问题:

一种情况是集合类本身自带排序功能,如前面说过的TreeSet、SortedSet、SortedMap等,另一种就是本身不带排序功能,我们通过为需要排序的类实现Comparable或者Comparator接口来实现。

先来看两个例子,一个是实现Comparable的,一个是实现Comparator的,为了方便,我将类都写在了一个文件中。

(1)实现Comparable的:

package com.xtfggef.list.test;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
 
@SuppressWarnings("unchecked")
public class ComparableTest {
	public static void main(String[] args) {
		// User[] users = { new User("egg", 23), new User("niuniu", 22),
		// new User("qing", 28) };
		// Arrays.sort(users);
		// for (User user : users) {
		// System.out.println(user.getName() + " " + user.getAge());
		// }
		List<User> users = new ArrayList<User>();
		users.add(new User("egg", 23));
		users.add(new User("niu", 22));
		users.add(new User("qing", 28));
 
		Collections.sort(users);
		for (User user : users) {
			System.out.println(user.getName() + " " + user.getAge());
		}
	}
}
 
@SuppressWarnings("unchecked")
class User implements Comparable {
	private String name;
	private int age;
 
	public User(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
 
	public String getName() {
		return name;
	}
 
	public void setName(String name) {
		this.name = name;
	}
 
	public int getAge() {
		return age;
	}
 
	public void setAge(int age) {
		this.age = age;
	}
 
	@Override
	public int compareTo(Object o) {
		return this.age - ((User) o).getAge();
	}
}

(2)下面是实现Comparator接口的:

package com.xtfggef.comparator.test;
 
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
 
public class ComparatorTest {
 
	public static void main(String[] args) {
		List<User> users = new ArrayList<User>();
		users.add(new User("egg", 21));
		users.add(new User("niu", 22));
		users.add(new User("gg", 29));
		UserComparator comparator = new UserComparator();
		Collections.sort(users, comparator);
		for (User user : users) {
			System.out.println(user.getUsername() + " " + user.getAge());
		}
	}
 
}
class User {
	private String username;
	private int age;
 
	public User(String username, int age) {
		super();
		this.username = username;
		this.age = age;
	}
 
	public String getUsername() {
		return username;
	}
 
	public void setUsername(String username) {
		this.username = username;
	}
 
	public int getAge() {
		return age;
	}
 
	public void setAge(int age) {
		this.age = age;
	}
}
class UserComparator implements Comparator<User> {
 
	@Override
	public int compare(User user1, User user2) {
		int age1 = user1.getAge();
		int age2 = user2.getAge();
		if (age1 < age2) {
			return 1;
		}
		return 0;
	}
}

通过上面的这两个小例子,我们可以看出,Comparator和Comparable用于不同的场景,实现对对象的比较从而进行排序。

总结为:

相同点:二者都可以实现对象的排序,不论用Arrays的方法还是用Collections的sort()方法。

不同点:

(1)实现Comparable接口的类,似乎是预先知道该类将要进行排序,需要排序的类实现Comparable接口,是一种“静态绑定排序”。

(2)实现Comparator的类不需要,设计者无需事先为需要排序的类实现任何接口。

(3)Comparator接口里有两个抽象方法compare()和equals(),而Comparable接口里只有一个方法:compareTo()。

(4)Comparator接口无需改变排序类的内部,也就是说实现算法和数据分离,是一个良好的设计,是一种“动态绑定排序”。

(5)Comparator接口可以使用多种排序标准,比如升序、降序等。

6.3、使用for循环删除元素陷阱:

先来看看下面这个程序:

public class Test {
 
	public static void main(String[] args) {
		List<String> list = new LinkedList<String>();
		list.add("A");
		list.add("B");
		list.add("C");
		
		for(int i=0; i<list.size(); i++){
			list.remove(i);
		}
		
		for(String item:list){
			System.out.println(item);
		}
	}
}

读者朋友们可以先猜猜这个程序输出什么?按我们的思路,应该是输不出什么,但是执行它,输出的却是:B。这是为什么呢?我们分部分析下这个程序,当地一步remove完后,集合内还剩2个元素,此时i为1,而list.size()的值为2,从0开始的话,i为1时,正好指向第二个元素,也就是说当remove完A后,直接就跳到C,将B漏了。

public class Test {
 
	public static void main(String[] args) {
		List<String> list = new LinkedList<String>();
		list.add("A");
		list.add("B");
		list.add("C");
		
		for(int i=0; i<list.size(); i++){
			list.remove(i);
			i -= 1;//每次删除完后,i减少1
		}
		
		for(String item:list){
			System.out.println(item);
		}
	}
}

 

参考博客:

https://blog.csdn.net/chenssy/article/details/17732841

https://blog.csdn.net/zhangerqing/article/details/8122075

https://blog.csdn.net/qq_25868207/article/details/55259978

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/114669.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • Java 数组转 List 的 4 种方式

    Java 数组转 List 的 4 种方式目录前言【一】最常见方式(未必最佳)【二】数组转为List后,支持增删改查的方式【三】通过集合工具类Collections.addAll()方法(最高效)问题解答总结前言本文介绍Java中数组转为List三种情况的优劣对比,以及应用场景的对比,以及程序员常犯的类型转换错误原因解析。【一】最常见方式(未必最佳)通过Arrays.asList(strArray)方式,将数组转换List后,不能对List增删,只能查改,否则抛异常。关键代码:Listlist=Arrays.asList(

  • SpringBoot源码解析之注解

    SpringBoot源码解析之注解本文的代码基于1.5.9版本启动类中都需要添加@SpringBootApplication注解,该注解中包含了三个重要注解:1、@SpringBootConfiguration标记为springBoot应用,在这个注解中又有@Configuration表示是个注解配置。2、@EnableAutoConfiguration该注解中又包含@AutoConfigurationPackage…

  • raid5什么意思?怎样做raid5?raid5 几块硬盘?

    raid5什么意思?怎样做raid5?raid5 几块硬盘?

    2021年11月15日
  • 什么是DDR3_DDR3 SDRAM

    什么是DDR3_DDR3 SDRAMODT是什么鬼?为什么要用ODT?在很多关于DDR3的博文和介绍中都没有将清楚。在查阅了很多资料并仔细阅读DDR3的官方标准(JESD79-3A)之后,总算有点了头绪,下面来整理整理。1、首先ODT是什么?ODT(On-DieTermination),是从DDR2SDRAM时代开始新增的功能。其允许用户通过读写MR1寄存器,来控制DDR3SDRAM中内部的终端电阻的连接或者断开。在DDR3…

  • pycharm配置pyqt5_python pyqt5教程

    pycharm配置pyqt5_python pyqt5教程pycharm配置PyQt5说明打开配置的位置配置QtDesigner配置PyUIC配置PyRCC说明Program目录:此处直接用的虚拟环境下的文件,也可用安装目录下的文件打开配置的位置File-SettingsTools-ExternalTools-点击+配置QtDesignerProgram:D:\ProgramFiles\Python\virtualenvs\…

  • 那些长短不一的PCI-E插槽都有什么不一样?

    那些长短不一的PCI-E插槽都有什么不一样?https://www.ednchina.com/news/20171121-PCI-E.html时间:2017-11-21目前PCI-E插槽已经成为了主板上的主力扩展插槽,除了显卡会用到PCI-E插槽外,诸如独立声卡、独立网卡、USB3.0/3.1接口扩展卡以及SSD等硬件都可以使用PCI-E插槽。主板上的扩展插槽曾经是多种多样的,例如曾经非常流行…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号