一、Java集合概述
1、一方面,面向对象的的语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行储存,另一方面,使用Array储存对象方面具有一些弊端,而Java集合就像一个容器,可以动态的把多个对象的引用放入容器中。
2、Java集合类可以用于储存数量不等的多个对象,还可用于保存具有映射关系的关联数组。
Java集合类的继承层次:
![d128bf54820884401aa0d33394e1ac9f.png](https://img-blog.csdnimg.cn/img_convert/d128bf54820884401aa0d33394e1ac9f.png)
二、Collection类
从上面的集合类的继承层次图可以看出,集合由两个基本接口:Collection类和Map类。
1> Collection接口与迭代器
2.1.1 Collection接口
在Java类库中,集合类的基本接口是Collection接口。这个接口有两个基本方法:
public interface Collection<E>{
boolean add(E element);
Iterator<E> iterator();
...
}
- add( )方法用于向集合中添加元素,如果添加元素确实改变了集合就返回true;如果集合没有发生变化就返回false
- iterator( )方法用于返回了一个实现了Iterator接口的对象。可以使用这个迭代器对象依次访问集合中的元素。
补充:Collection中的其他方法
- int size( )
返回一个用于访问集合中各个元素的迭代器
- boolean isEmpty( )
如果集合中没有元素,则返回true
- boolean contains(Object obj)
如果集合中包含了一个与obj对象相等的对象,则返回true
- boolean containAll(Collection<?> other)
如果这个集合元素包含other集合中的所有元素,则返回true
- boolean add(E element) [前面有讲到,此处就省略了]
- boolean addAll(Collection<? extends E> other)
将other集合中的所有元素添加到这个集合中。如果由于这个调用改变了集合,则返回true。否则,false
- boolean remove(Object obj)
从这个集合中删除等于obj的对象。如果有匹配的对象被删除,则返回true。
- boolean removeAll(Collection<?> other)
从这个集合中删除other集合中所存在的所有元素。如果由于这个调用改变了集合,则返回true
- default boolean removeIf(Predicate<? super E> filter)
从这个集合删除filter返回true的所有元素。如果由于这个调用改变了集合,则返回true
- void clear( )
从这个集合中删除所有元素
- boolean retainAll(Collection<?> other)
从这个集合中删除所有与other集合中存在的所有元素。如果由于这个调用改变了集合,返回true。
- Object[ ] toArray( )
返回这个集合中的对象的按钮
- T[ ] toArray(T[ ] arrayToFill)
返回这个集合中的对象的数组。如果arrayToFill足够大,就将集合中的元素填入这个数组中。剩余空间用null填补;否则,分配一个新数组,其成员类型与arrayToFill的成员类型相同,其长度等于集合的大小,并填充集合元素。
2.1.2 Java中的迭代器
2.1.2.1 Iterator接口
Iterator接口包含4个方法
public Interface Iterator<E>{
E next();
boolean hasNext();
void remove();
default void forEachRemaining(Consumer<? super E> action);
}
- next( )方法:返回即将要访问的下一个对象元素。如果已经到达了集合末尾,扔掉用next( )方法的话,将会抛出一个NoSuchElementException。
- hasNext( )方法:如果迭代器对象后面还有可以访问的对象元素,这个方法就返回true。所以在调用next( )方法以前,要调用一个hasNext( )方法来确保是否还有可以访问
有了这两个基础方法,我们就可以介绍迭代器了:
Collection<String> collection = ...;
Iterator<String> iterator = collection.iterator(); //返回一个Collection的迭代器
while(iterator.hasNext()){
String element = iterator.next();
doing something with element...;
}
在Java中,迭代器位于两个元素之间。当调用next( )方法时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
![c0a4d0c1c3244445c37313359137e1cf.png](https://img-blog.csdnimg.cn/img_convert/c0a4d0c1c3244445c37313359137e1cf.png)
- remove( )方法:将会删除上次调用next( )方法时返回的元素。如果调用remove( )方法以前没有调用next( )方法,这将是不合法的,如果这样做,将会抛出一个IllegalStateException异常
Iterator iter = collection.iterator;
//legal
iter.next();
iter.remove();
//Illegal
iter.remove //error
- forEachRemaining( )方法: 可以这样调用forEachRemaining( )方法
iter.forEachRemaining(element -> do something with element);
2.1.2.2 Iterable接口
public interface Iterable<E>{
Iterator<E> iterator();
...
}
可以看出这个接口的作用就是返回一个新的迭代器。
有很多的集合类并没有实现Iterator接口,反而是实现了Iterable接口,这是为什么呢
(一下内容摘自一篇博客:https://www.iteye.com/blog/perfy315-1459201)
为什么一定要实现Iterable接口,为什么不直接实现Iterator接口呢? 看一下JDK中的集合类,比如List一族或者Set一族,都是实现了Iterable接口,但并不直接实现Iterator接口。 仔细想一下这么做是有道理的。
因为Iterator接口的核心方法next()或者hasNext() 是依赖于迭代器的当前迭代位置的。 如果Collection直接实现Iterator接口,势必导致集合对象中包含当前迭代位置的数据(指针)。 当集合在不同方法间被传递时,由于当前迭代位置不可预置,那么next()方法的结果会变成不可预知。 除非再为Iterator接口添加一个reset()方法,用来重置当前迭代位置。 但即时这样,Collection也只能同时存在一个当前迭代位置。 而Iterable则不然,每次调用都会返回一个从头开始计数的迭代器。 多个迭代器是互不干扰的。
2> 具体集合:List类
在Java程序设计语言中,所有的链表都是双向链接的------即每个链接还存放着其前驱的引用。如下图:
![b867190c5eb5c462395a738fbd89a5dc.png](https://img-blog.csdnimg.cn/img_convert/b867190c5eb5c462395a738fbd89a5dc.png)
我们来介绍List类的一些方法:
- ListIterator listIterator( )
返回一个列表迭代器,用来访问列表中的元素
- ListIterator listIterator(int index)
返回一个列表迭代器,用来访问列表的元素,第一次调用这个迭代器的next( )方法会返回给定索引的元素
- void add(int i, E element)
在给定位置添加一个元素
- void addAll(int i, Colection<? extends E> elemens)
将一个集合中的所有元素添加到给定的位置
- E remove(int i)
删除并返回给定位置的元素
- E get(int i)
获取给定位置的元素
- E set(int i, E element)
用一个新元素替换给定位置的元素,并返回原来的那个元素
- int indexOf(Object element)
返回与指定元素相等的元素在列表中第一次出现的位置,如果没有这样的元素将会返回-1
- int lastindexOf(Object element)
返回与指定元素相等的元素在列表中最后一次出现的位置,如果没有这样的元素将会返回-1
2.2.1 链表:LinkedList类
LinkedList类与泛型Collection集合有一个重要的区别。链表是一个有序集合。每个对象的位置十分重要LinkedList.add( )方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表的中间。由于迭代器描述了集合中的位置,所以这种依赖于位置的add( )方法将由迭代器负责
2.2.1.1 LinkedList的一些基本方法
- LinkedList( )
构造一个空链表
- LinkedList(Collection<? extends E> elements)
构造一个链表,并将集合中的所有的元素都添加到这个链表中
- void addFirst(E element)
- void addLast(E element)
将某个元素添加到列表的头部或尾部
- E getFirst( )
- E getLast( )
返回列表头部或尾部的元素
- E removeFirst( )
- E removeLast( )
删除并返回列表头部或尾部的元素
![bd4ddfa3bd6f98c85c78cdbb1379e402.png](https://img-blog.csdnimg.cn/img_convert/bd4ddfa3bd6f98c85c78cdbb1379e402.png)
2.2.1.2 LinkedList类的列表迭代器
LinkedList类除了能获取一个Iterator迭代器,还可以获取一个自己独特的一个列表迭代器-----ListIterator,它是Iterator的子接口
interface ListIterator<E> extends Iterator<E>{
...
}
我们来介绍ListIterator的一些方法
- void add(E element)
在当前位置前添加一个元素
- next( )
- hasNext( ) 这两个方法与Iterator的相同
- E previous( )
返回前一个对象。如果已经达到了列表的头部,就抛出一个NoSuchElementException异常
- boolean hasPrevious( )
当反向迭代列表时,如果还有可以访问的元素,则返回true
这两个方法可以实现反向遍历,使用的方法与next( )方法和hasNext( )方法遍历集合元素一致
- void set(E newElement)
用新元素替换next( )或previous( )访问的上一个元素。如果在上一个next( )或previous( )调用之后列表结构被修改了,将抛出一个IllegalStateException异常
- int nextIndex( )
返回下一次调用next( )方法将返回的元素的索引
- int previousIndex( )
返回下一次调用previous( )方法时将返回的元素的索引
2.2.1.3 链表与其链表迭代器详述
2.2.1.3.1 添加元素示例代码
ListIterator 的add( )方法在迭代器位置之前添加一个新对象。
我们来看一段示例代码:
var staff = new LinkedList<String>();
ListIterator<String> iter = staff.listIterator();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
//以下为越过第一个元素,在第二个元素之前添加“Juliet”
iter.next();
iter.add("Juliet");
如果多次调用add( )方法,将按照提供的次序把元素添加到链表中。它们被一次添加到迭代器的当前位置之前。
2.2.1.3.2 多个迭代器的情形
set( )方法用一个新元素替换调用next( )方法或previous( )方法返回的上一个元素。
例如,下面的代码将用一个新值替换链表的第一个元素:
ListIterator<String> iter = list.listIterator();
String oldValue = iter.next();
iter.set(newValue);
可以想象,如果在某个迭代器修改集合时,另一个迭代器却在遍历这个集合,那么一定会出现混乱。例如,假设一个迭代器向一个元素前面的位置,而另一个迭代器刚刚删除了这个元素,现在前一个迭代器就是无效的,并且不能再使用。
而链表迭代器设计为可以检测到这种修改。如果一个迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的某个方法修改了,就会抛出一个ConcurrentModificationException异常
例如我们考虑下面这段代码:
List<String> list = ...;
ListIterator<String> iter1 = list.listIterator();
ListIterator<String> iter2 = list.listIterator();
iter1.next();
iter1.remove();
iter2.next(); //throw ConcurrentModificationException
由于iter2检测出这个链表从外部修改了,所以iter2.next( )的调用抛出了一个异常
为了避免发生并发(并发为多线程的内容,后面的章节将会讲到)修改异常,请遵循这样一个简单的规则:可以根据需要为一个集合关联多个迭代器,前提是这些迭代器只能读取集合。或者可以再关联一个能同时读写的迭代器
2.2.1.3.3 链表示例代码:
我们来练习以下LinkedList类以及它的迭代器的使用,将会创建两个列表,然后将他们合在一起
package Collection.List;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
/**
* @param
* @author myW_Yh
* @Description
* @return
* @creat 2020-05-15-9:21
*/
public class LinkedList_Test {
public static void main(String[] args) {
var a = new LinkedList<String>();
a.add("Amy");
a.add("Carl");
a.add("Erica");
var b = new LinkedList<String>();
b.add("Bob");
b.add("Doug");
b.add("Frances");
b.add("Gloria");
//merge the words from b into a
ListIterator<String> aIter = a.listIterator();
Iterator<String> bIter = b.iterator();
while(bIter.hasNext()){
if (aIter.hasNext()) aIter.next();
aIter.add(bIter.next());
}
System.out.println(a.toString());
//remove every second word form b
bIter = b.iterator();
while(bIter.hasNext()){
bIter.next();
if (bIter.hasNext()){
bIter.next();
bIter.remove();
}
}
System.out.println(b.toString());
//bulk operation: remove all words in b from a
a.removeAll(b);
System.out.println(a);
}
}
2.2.2 数组列表:ArrayList类
2.2.2.1 LinkedList和ArrayList优势
- LinkedList
- 优势:对于频繁的删除,添加操作很有优势,比起ArrayList减去很大的开销
- ArrayList
- 优势:它是以数组的方式来实现的,数组的特性是可以使用索引的方式来快速定位对象的位置,因此对于快速的随机取得对象的需求,使用ArrayList实现执行效率上会比较好。
2.2.2.2 ArrayList详述
在java中,有两种访问元素的协议:一种是通过迭代器,另一种是通过get( )和set( )方法随机地访问每个元素。后者不适用于链表。所以自然而然地,适用于ArrayLIst类,这个类实现了List接口,并且ArrayList封装了一个动态再分配地对象数组
2.2.2.3 ArrayList方法
List接口中所有方法ArrayList都可以使用
除此之外,ArrayList还可以使用数组地一些方法,例如sort( )等
3>具体集合:set类:不能有相同的元素
链表和数组允许你根据一元指定元素地次序。但是,如果想要查看某个指定的元素,却又不记得它的位置,就需要访问所有元素,直到找到为止。如果集合中包含的元素很多这将会需要很长时间。如果不在意元素的顺序,我们这时候就可以使用set集合。但是它的缺点是无法控制元素出现的次序,是无序的,这些数据结构将按照对自己最方便的方式组织元素。
2.3.1 散列集:hashSet
有一种众所周知的一个数据结构,可以用于快速地超找对象,这就是散列表。散列表为每一个对象计算一个整数,称为散列码。有不同的数据地修电工将产生不同的散列码。它们是由hashcode方法产生的。
2.3.1.1 hashcode( )方法:
在所有类地超类中,就定义着一个默认的hashcoder( )方法,会从对象的存储地址得出散列码。
如果新写的类中重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义hashcode( )方法。
这是因为,equals( )方法必须和hashcode( )方法实现兼容:如果x.equals(y)返回true,那么x.hashcode( ) 与y.hashcode( )返回的hash值应该相等。
我们来看以下hashcode( )方法应该怎样重写实现:
我们假设的是重写前面几章写过的Eployee类:成员变量有String 类型的name,double 类型的salary, Date类的hireDay
public int hashCode(){
return Objects.hash(name,salary,hireDay);
}
(有关于hashCode的底层实现原理与hash算法,笔者由于能力有限,此处不作叙述,感兴趣的可以查查资料)
2.3.1.2 hashSet中的方法
Collection中的方法hashSet都可以使用。
2.3.2 树集:TreeSet
树集是一个有序集合。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,值将自动的按照排序后的顺序呈现。
TreeSet要注意的事项:
a.向TreeSet中添加的元素必须是同一个类型的。
b.可以按照添加进集合中的元素的指定的顺序遍历。像String、包装类等默认按照从小到大的顺序遍历 。
c.当向TreeSet中添加自定义类的对象时,有两种排序方法:自然排序和定制排序。
d.自然排序:要求自定义类实现java.lang.Comparable接口并重写compareTo(Object obj) 在此方法中,指明按照自定义类的哪个属性进行排序。
e.向TreeSet中添加元素时,首先按照compareTo() 进行比较,一旦返回0,虽然仅是两个对象的此属性值相同,但是程序会认为这两个对象是相同的,进而后一个对象就不能进来。
f.定制排序:
- 创建一个实现了Comparator接口的类对象
- 将此对象作为形参传入TreeSet的构造器
- 向TreeSet中添加元素
补充注意:compareTo()【compare()】与hashCode()以及equals() 三者保持一致
2.3.2LinkedHashSet(插入性略低,但是迭代性好)
使用链表维护了一个添加进集合中的顺序,导致当我们遍历LinkedHashSet集合元素时,是按照添加进去的顺序遍历的
4> 队列
2.4.1队列与双端队列
队列(Queue)允许高校地在尾部添加元素,并在头部删除元素。
双端队列(Deque)允许在头部和尾部都高效地添加和删除元素。
它们都不允许在队列中间添加元素
java.util.Queue
- boolean add(E element)
- boolean offer(E element)
如果队列没有满,将给定的元素添加到这个队列的队尾并返回true。如果队列已满,第一个方法抛出一个IllegalStateStateException,而第二个方法返回false
- E remove( )
- E poll( )
假如队列不为空,删除并返回这个队列队头的元素。如果队列是空的,第一个方法抛出NoSuchElementWException,而第二个方法返回null
- E element( )
- E peek( )
如果队列不为空,返回这个队列的队头,但不删除。如果队列空,第一个方法抛出NoSuchElementWException,而第二个方法返回null
java.util.Deque
- void addFirst(E element)
- void addLast(E element)
- boolean offerFirst(E element)
- boolean offerLast(E element)
将给定的对象添加到队列的队头或队尾。如果这个双端队列已满,前两个方法抛出一个IllegalStateStateException,后两个方法返回false
- E removeFirst( )
- E removeLast( )
- E pollFirst( )
- E popllLast( )
如果这个双端队列不为空,删除并返回双端队列队头或队尾的元素。如果这个双端队列为空,前两个方法抛出一个IllegalStateStateException,后两个方法返回false
- E getFirst( )
- E getLast( )
- E peekFirst( )
- E peekLast( )
如果这个双端队列非空,返回双端队列或队尾的元素,但不删除。如果这个双端队列为空,前两个方法抛出一个IllegalStateStateException,后两个方法返回false
2.4.2 无限定双端队列 ArrayDeque
- ArrayDeque( )
- ArrayDeque(int initialCapacity)
用初始容量16或给定的初始容量构造一个无限定双端队列
2.4.3 优先队列
优先队列中的元素可以按照任意的顺序插入,但会按照有序的顺序进行检索。也就是说,无论何时调用remove( ) 方法,总会获得当前优先队列中最小的元素。
优先队列并没有对所有元素进行排序。如果迭代处理这些元素,并不需要对它们进行排序。优先队列使用了一个精巧且高校的数据结构,称为堆。堆是一个可以自己组织的二叉树,其添加和删除操作可以让最小的元素移动到根,而不必花费时间对元素进行排序。
与TreeSet一样,优先队列既可以保存实现了Comparable接口的类对象,也可以保存构造器中提供的Compatator对象
优先队列的典型用法是任务调度。每一个任务都有一个任务级,任务以随即顺序添加到队列中。每当启动一个新的任务时,都将优先级最好的任务从队列中删除。(由于习惯上将1设为“最高”优先级,所以remove( ) 操作会将最小的元素删除
java.util.PriorityQueue
- PriorityQueue( )
- PriorityQueue(int initialCapacity)
构造一个存放Comparable对象的优先队列
- PriorityQueue(int initialCapacity, Comparator<? super E> c)
构造一个优先队列,并使用指定的比较器对元素进行排序
三、映射Map
集是一个集合(与数学中的集合一样),允许快速的查找现有的元素。但是,要查找一个元素,需要所有要查找的那个元素的准确副本。这不是一种常见的查找方式。通常,我们需要知道某些关键信息,希望查找与之关联的元素。映射(Map)数据结构就是为此设计的。
映射用来存放键/值对。如果提供了键,就能找到值。
1> 基本映射操作:增删改查与迭代处理
Java类库提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了Map接口。
3.1.1 创建一个Map对象以及“增”的操作
散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树,散列或者比较函数都只用于键,与键关联的值不进行散列或比较。
那什么是键与值呢?来看一个示例代码:
var staff = new HashMap<String, Employee>();
var harry = new Employee("Harry Hacker");
staff.put("987-96-8896",harry);
...
其中,那一串数字就是一个键,而与之关联的Employee类的harry对象就是值
我们要记住:
- 每当往映射中添加一个对象时,必须提供一个键
- 键必须是唯一的,不能对同一个键存放两个值。如果对同一个键调用两次put( )方法,第二个值就会取代第一个值。实际上,put( )方法将返回与这个键参数关联的上一个值。
其他的一些“增”的方法:
- void putAll(Map<? extends K, ? extends V> entries)
将给定映射中所有映射条目添加到这个映射中
3.1.2 “删”
remove( )方法从映射中删除给定键对应的元素。
3.1.3 “改”
可以用put( )方法来改
- default V putIfAbsent(K key, V value)
如果指定的键尚未与值关联(或映射到 null
), null
其与给定值关联并返回 null
,否则返回当前值。
- default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
在所有映射条目上应用这个函数。将键与非null结果相关联,对于null结果,则将相应的键删除
3.1.4 “查”
3.1.4.1 查集合中的元素的个数
size( )方法返回映射中的元素数
3.1.4.2 根据键来查找值
- get(Object key)
它会获取与键关联的值;返回与键关联的对象,或者如果映射中没有这个对象,则返回null。实现类可以禁止键为null。
- default V getOrDefault(Object key, V defaultValue)
返回指定键映射到的值,如果此映射不包含键的映射,则返回 defaultValue
3.1.4.3 其他的一些“查”的方法
- boolean containsKey(Object key)
如果在映射中已经有这个键,返回true
- boolean containsValue(Object value)
如果映射中已经有这个值,返回true
3.1.5 迭代处理
- 可以用forEach(BiConsumer<? super K, ? super V> action)内嵌套一个lambda表达式来遍历
例如:
java Map<String, Interger> scores = ...; int score = scores.getOrDefault(id,0); scores.forEach((k,v) -> System.out.println("Key= "+ k + " Value= " + v));
其他的迭代处理方法将在下面的映射视图讲到。
2> 映射视图: 键集,值集,键/值对集(entry集)
- key集是用Set存放的,不可重复。
我们可以用Map类中的ketSet( )方法来获取键集:
java HashMap<> map = new HashMap(); Set<> keySet = map.keySet();
- value集是用Collection存放的,可重复的。
我们可以用Map类的values( )方法来获取值集:
Collection<> collection = map.values();
- 一个key-value对,是一个Entry,所有的Entry是用Set存放的,不可重复。(映射条目集的元素是实现了Map.Entry接口的类的对象)
我们可以用entrySet( )来获取entry集:
java Set<> entrySet = map.entrySet();
3.2.1 entry中的方法
java.util.Map.Entry
- K getKey( )
- V getValue( )
返回这个映射条目的键或值
- V setValue(V newValue)
将相关映射中的值改为新值,并返回原来的值。
3.2.2 遍历方法
//遍历key集
Set set = map.keySet();
for(Object obj:set){
System.out.println(obj);
}
//2.遍历value集
Colllection values = map.values();
Iterator i = values.iterator();
while(i.hasNext()){
System.out,println(i.next());
}
//3.如何遍历key - value
//方式一:
Set set1 = map.keySet();
for(Object obj : set1){
System.out,println(obj+map.get(obj))
}
//方式二:
Set set2 = map.entrySet();
for(Object obj:set2){
Map.Entry entry=(Map.Entry)obj;
System.out.println(entry.getKey()+entry.getValue());
System.out.println(entry);
}