@User:woBuShiKeDaYa
@Date:2022/4/15
@Completion:false
泛型与集合
定义泛型类/泛型接口
-
定义泛型类
public static class Stu<T>{ private T t; public Stu(T t) { this.t = t; } public void show(){ System.out.println(t); } }
-
定义泛型接口
public interface InterfaceDemo1<E>{
void show(E e);
}
我们比较常见的一个泛型接口就是:
public interface List<E> extends Collection<E>{}
List接口继承了Collection接口,Collection是集合层次结构中的根接口。
集合
Collection(接口)
-
基本操作
方法名 说明 boolean add(E e) 添加元素 boolean remove(Object o) 从集合中移除指定得元素 boolean removeIf(Object o) 根据条件进行删除 void clear() 清空集合 boolean contains(Object o) 判断集合中是否存在指定的元素 boolean isEmpty() 判断集合是否为空 int size() 集合的长度,也就是集合中元素的个数 -
容器的特点
Java容器里只能放对象,对于基本类型(int, long, float, double等),需要将其包装成对象类型后(Integer, Long, Float, Double等)才能放到容器里。很多时候拆包装和解包装能够自动完成。这虽然会导致额外的性能和空间开销,所以我们在提高程序性能时,在具体需求上灵活使用包装类和基本数据类型,避免因为自动装箱和自动拆箱导致的额外性能消耗,尽管会造成额外的性能消耗,但简化了设计和编程。
List(接口)
List接口是一个有序的集合,它允许我们按顺序存储和访问元素。它扩展了集合接口。
ArrayList(具体实现类)
ArrayList_UML图
ArrayList概述
- ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。
- ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。也可以使用Vector。
- ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输;实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问;实现了Cloneable接口,能被克隆。
tips:序列化
Java序列化是指把Java对象转换为有序字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程,也就是,实现了Serializable接口,可以在流中输出这个数组时输出这个数组的字节序列。然后反序列化之后就由可以得到一个一样的数组。transient关键字可以使该关键字修饰的内容不被序列化
Fail-Fast机制
当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出ConcurrentModificationException异常。fail-fast机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅用于检测bug。
- 结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组大小,仅仅只是设置元素的值不算结构发生变化。
- 在进行序列化或者迭代操作时,需要比较操作前后modCount是否改变,如果改变了需要抛出ConcurrentModificationException,modCount用来记录ArrayList结构发生变化的次数。
ArrayList有以下特点:
- ArrayList基于数组方式实现,无容量的限制(会扩容)
- 添加元素时可能要扩容(所以最好预判一下,最好不要发生扩容,因为会调用Arrays.copyOf()把原数组整个复制到新数组中,这个操作代价是非常高的),删除元素时不会减少容量(若希望减少容量可以使用trimToSize()这个方法可以既删除容量也删除元素,但是代价很高)。
- 线程不安全
- add(int index, E element):添加元素到数组中指定位置的时候,需要将该位置及其后边所有的元素都整块向后复制一位
- get(int index):获取指定位置上的元素时,可以通过索引直接获取(O(1))
- remove(Object o)需要遍历数组
- remove(int index)不需要遍历数组,只需判断index是否符合条件即可,效率比remove(Object o)高
- contains(E)需要遍历数组
LinkList(具体实现类)
LinkList_UML
- LinkedList 继承了 AbstractSequentialList 类。
- LinkedList 实现了 List 接口,可进行列表的相关操作。
- LinkedList 实现了 Deque 接口,可作为队列使用。
- LinkedList 实现了 Cloneable 接口,可实现克隆。
- LinkedList 实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输
- LinkList是一个双向链表
Tips:AbstractSequentialList类
AbstractSequentialList 继承自 AbstractList,是 LinkedList 的父类,是 List 接口 的简化版实现。
简化在哪儿呢?简化在 AbstractSequentialList 只支持按次序访问,而不像 AbstractList 那样支持随机访问。
其实也就是让LinkList支持顺序迭代器遍历。因为AbstractSequentialList 只支持迭代器按顺序访问。
具体方法使用和ArrayList差不多,特殊的实现了Deque接口,所以可以当作队列进行使用,有一些特殊得方法,比如说在链表头部插入元素的方法public boolean offerFirst(E e)
以及等等队列、堆栈相关得操作也在LinkList中做了具体的方法实现。但Java中对于堆栈是有具体的实现类Satck
。
特别要注意的是,区别于ArrayList,LinkList的插入和删除效率较高,而ArrayList的查找效率较高,所以在不同的使用场景,可以对这两个具体的实现做出相应的使用。
Vector(具体实现类)
Vector和ArrayList类似,但是还是存在一定的区别(否则这两个留一个就行了为啥要单独区分)
- Vector是同步访问的,存在同步锁(后续多线程同步问题会说到),也就是Vector是线程安全的
- Vector 包含了许多传统的方法,这些方法不属于集合框架。也就是除了从List实现的一些方法外,它有一些自己独特的方法
Queue(接口)
这是一个队列的一个顶层接口,定义了队列的基本操作。
LinkList实现了Deque接口,代表LinkList可以当作一个双端队列进行使用。
- LinkedBlockingQueue的容量是没有上限的(说的不准确,在不指定时容量为Integer.MAX_VALUE,不要然的话在put时怎么会受阻呢),但是也可以选择指定其最大容量,它是基于链表的队列,此队列按 FIFO(先进先出)排序元素。
- ArrayBlockingQueue在构造时需要指定容量, 并可以选择是否需要公平性,如果公平参数被设置true,等待时间最长的线程会优先得到处理(其实就是通过将ReentrantLock设置为true来 达到这种公平性的:即等待时间最长的线程会先操作)。通常,公平性会使你在性能上付出代价,只有在的确非常需要的时候再使用它。它是基于数组的阻塞循环队 列,此队列按 FIFO(先进先出)原则对元素进行排序。
- PriorityBlockingQueue是一个带优先级的 队列,而不是先进先出队列。元素按优先级顺序被移除,该队列也没有上限(看了一下源码,PriorityBlockingQueue是对 PriorityQueue的再次包装,是基于堆数据结构的,而PriorityQueue是没有容量限制的,与ArrayList一样,所以在优先阻塞 队列上put时是不会受阻的。虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError),但是如果队列为空,那么取元素的操作take就会阻塞,所以它的检索操作take是受阻的。另外,往入该队列中的元 素要具有比较能力。
- DelayQueue(基于PriorityQueue来实现的)是一个存放Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满,poll就以移除这个元素了。此队列不允许使用 null 元素。
Set(接口)
UML
如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set。也就是说,Set用于存储不重复的值。
Set接口并不保证有序,而SortedSet接口则保证元素是有序的。
TreeSet实现了SortedSet接口,但是Sorted接口则继承了Set。
HashSet(具体实现类)
UML
图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XVGCCPPG-1650014884790)(/picture/HashSet_1.PNG)]
构造方法
HashSet(int initialCapacity)
构造一个新的空集;支持HashMap实例具有指定的初始容量和默认加载因子 (0.75)。
HashSet(int initialCapacity, float loadFactor)
构造一个新的空集;后备HashMap实例具有指定的初始容量和指定的负载因子
加载因子是自动扩容时的一个参数,HashSet默认的容量值为16,在数组容量到达16*0.75=12的时候,就会自动扩容,扩容成原来大小的两倍。这里要注意:为了避免过多的自动扩容带来的额外负载,最好在初始化的时候指定一个相对合理的容量值
因为HashSet是西安了Set,所以HashSet中的元素是不能重复的,我们来看看如下这个例子:
public class HashSetDemo1 {
public static void main(String[] args) {
HashSet<Stu> Stus = new HashSet<>();
HashSet<String> strings = new HashSet<>();
Stu stu1 = new Stu("1","张三",20);
Stu stu2 = new Stu("1","张三",20);
String s1 = new String("add");
String s2 = new String("add");
strings.add("yuyanjia");
strings.add("yuyanjia");
strings.add(s1);
strings.add(s2);
System.out.println(strings);
Stus.add(stu1);
Stus.add(stu2);
System.out.println(Stus);
}
}
结果如下:
[add, yuyanjia]
[Stu{no=‘1’, name=‘张三’, age=20}, Stu{no=‘1’, name=‘张三’, age=20}]
为什么会出现这样的结果呢?这就是因为HashSet底层的判断原理。
在底层HashSet调用了HashMap的put(K key, V value)方法:在向HashMap中添加元素时,先判断key的hashCode值是否相同,如果相同,则调用equals()
、==
进行判断,若相同则覆盖原有元素;如果不同,则直接向Map中添加元素;也就是说HashMap添加元素要保证键和值不相同,而判断的开始是先比较两者的哈希值是否相同,如果哈希值不相同则再用equals
或者==
进行比较。
在我自己写的Stu类中,我们虽然重写了equals方法,但是没有进行重写hashCode()方法。因为两个Stu都是new出来的对象,所以系统自己的哈希值往往是通过地址值进行计算的,所以这两个Stu对象的哈希值自然也不相同,所以HashSet在进行判断时,也会判定这两个元素不是相同的元素,都会进行储存。而String类冲洗了hashCode和equals方法,所以无论是在常量池的字符串,还是在堆区new出来的字符串,都会进行正确判断是否一样,所以在HashSet中只被储存了一次。
LinkedHashSet(具体实现类)
首先:LinkedHashSet是线程安全的
UML
HashSet内部的基本部分是一个数组,牵引链表,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DXhHQU5L-1650014884791)(/picture/LinkedHashSet.PNG)]
LinkedHashSet 和 HashSet的区别:
- LinkedHashSet的默认容量和HashSet的默认容量一样,都为16.加载因子都为0.75
- LinkedHashSet和HashSet都实现Set接口。 但是,它们之间存在一些差异。
- LinkedHashSet在内部维护一个链表。因此,它保持其元素的插入顺序。
- LinkedHashSet类比HashSet需要更多的存储空间。这是因为LinkedHashSet在内部维护链表。
- LinkedHashSet的性能比HashSet慢。这是因为LinkedHashSet中存在链表。
但是,对于数据量庞大且增删很频繁的操作,LinkedHashSet不偿是一种很好的选择。
TreeSet(具体实现类)
TreeSet集合特点
- 不包含重复元素的集合
- 没有带索引的方法
- 可以将元素按照规则进行排序
注意:如果想要使用TreeSet集合,必须要指定排序的规则
如何排序?有两种方法,分别是比较器排序,和自然排序
我们先来介绍自然排序:
public interface Comparable<T>
:
此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法。
Map(接口)
HashMap(具体实现类)
HashTable(具体实现类)
这个类和HashMap类似,但是它是线程安全的。
TreeMap(具体实现类)
何排序?有两种方法,分别是比较器排序,和自然排序
我们先来介绍自然排序:
public interface Comparable<T>
:
此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法。
Map(接口)
HashMap(具体实现类)
HashTable(具体实现类)
这个类和HashMap类似,但是它是线程安全的。
TreeMap(具体实现类)
@Precautions:笔记引用了多篇文章,仅做参考。