1、Collection接口
java中集合类的基本接口是Collection接口。该接口有如下方法:
/*
java.util.Collection<E>
*/
Iterator<E> iterator() //返回一个用于访问集合中每个元素的迭代器
int size() //返回当前储存在集合中的元素个数
boolean isEmpty() //如果集合中没有元素,返回true
boolean containsAll( Collection<?> other ) //如果集合中包含other集合中的所有元素,返回true
boolean contains( Object obj ) //如果集合中包含了一个与obj相等的对象,返回true
boolean add( Object element ) //将一个元素添加到集合中。如果添加成功,返回true
boolean addAll( Collection< ? extends E > other) //将other集合中的所有元素添加到这个集合中。添加成功返回true
boolean remove( Object obj )
boolean removeAll( Collection<?> other)//同理为删除
default boolean removeIf( Predicate< ? super E > filter ) //从这个集合中删除filter返回true的所有元素。如果这个调用改变了当前集合,返回为true
void clear()//从这个集合中删除所有元素
boolean retainAll( Collection<?> other ) //从这个集合中删除所有与other集合中元素不同的元素。如果由于这个调用改变了当前集合,返回true
Object[] toArray() //返回这个集合的对象数组
<T> T[] toArray( T[] arrayToFill ) //返回这个集合的对象数组。如果arrayToFill足够大,就将集合中的元素填入这个数组,剩余的空间补null;否则,重新分配一个数组,其成员类型与arrayToFill的成员类型相同,其长度等于集合的大小,并填充集合元素。
2、迭代器(Iterator接口)
Iterator接口有如下方法:
/*
java.util.Iterator<E>
*/
boolean hasNext() //如果存在可访问的元素,返回true
E next() //返回将要访问的下一个对象。如果已经到达了集合尾部,将抛出NoSuchElementException
void remove() //删除上次访问的对象。这个方法必须紧跟在访问一个元素之后执行。如果上次访问后,集合发生了变化,这个方法将抛出一个IllegalStateException
default void forEachRemaining( Consumer< ? super E > action )
最开始时,迭代器的位置处于集合中首位元素的前面
java中查找元素的唯一方法就是调用next方法,在执行查找操作的同时,迭代器的位置随之向后移动。
java的迭代器是位于两个元素之间的。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
Iterator接口的remove方法将会删除上次调用next方法时返回的元素。所以应该在决定删除某一个元素之前看一下这个元素是否具有实际意义。然而,如果想要删除指定位置上的元素,仍需要越过这个元素。
更重要的是,对next方法和remove方法的调用是具有相互依赖性的。如果调用remove方法之前没有调用next方法是不合法的,将会抛出一个IllegalStateException异常。所以必须要先调用next越过将要删除的元素。
如果想要查看集合中所有的元素,可以申请一个迭代器,并在hasNext返回为true是反复调用next方法:
Collection<String> c=...;
Iterator<String> iter=c.iterator();
while( iter.hasNext() ){
String element =iter.next();
....
}
可以使用for each循环更加方便地实现上述的迭代过程:
for (String element:c) {
...
}
//编译器简单的将for each循环翻译为带有迭代器的循环。原因如下:
/*
for each 循环可以和任意实现了Iterable接口的对象一起工作,该接口只有一个方法,该方法用于返回一个Iterator迭代器:
*/
public interface Iterable<E>{
Iterator<E> iterator();
}
/*
Collection接口扩展了Iterable接口,所以,对于标准类库中的任何集合都可以使用for each循环
*/
在Java SE 8中甚至可以不用写循环,直接调用forEachRemaining方法并提供一个lambda表达式(它会处理一个元素)。该方法将对迭代器的每一个元素调用这个lambda表达式,直到没有元素为止。如:
iterator.forEachRemaining( element -> {... });
//lambda表达式参数为element,花括号是 对该元素进行相应操作的代码块。
3、链表
List接口如下:
/*
java.util.List<E>
*/
ListIterator<E> listIterator() //返回一个列表的迭代器,以便用来访问列表中的元素
ListIterator<E> listIterator(int index) //返回一个迭代器,该迭代器指向索引为index的元素前面的位置
void add( int i,E element) //在给定位置i处添加一个元素
void addAll(int i,Collection< ? extends E > elements )//将某个集合中的所有元素添加到给定位置
E remove( int i )//删除给定位置的元素并返回这个元素
E get(int i)//获取某个位置的元素(强烈不建议使用该方法去遍历整个集合,因为效率低下)
E set(int i ,E element)//用新元素取代给定位置的元素,并返回原来那个元素
int indexOf( Object element )//返回与指定元素相等的元素在列表中第一次出现的位置,如果没有这个元素就返回-1
int lastIndexOf( Object element )//同理是最后一次出现的位置
LinkedList接口如下:
/*
java.util.LinkedList<E>
*/
LinkedList()//构造一个空链表
LinkedList( Collection < ? extends E > element )//构造一个链表,并将集合中的所有元素添加到这个链表中
void addFirst(E element)
void addLast(E element)//将某个元素添加到列表的头部或者尾部
E getFirst()
E getLast()
E removeFirst()
E removeLast()//删除并返回表头部或者尾部的元素
在java中所有的链表实际上都是【双向连接的】(LinkedList类实现了Deque接口),即每个结点还存放着指向前驱结点的引用。链表与上述的泛型集合有很大的区别:链表是一个有序集合。只有对自然有序的集合使用迭代器添加元素才有实际意义。所以Collection使用的迭代器中没有add方法。但是集合类库中提供了Iterator的子接口ListIterator,其中新增了如下方法:
/*
java.util.ListIterator<E>
*/
void add( E newElement )//在迭代器的当前位置添加一个元素(注意迭代器的是位于两个元素之间的),添加后迭代器处于新增元素的后面
void set(E newElement )//用新元素取代next或者previous上次访问的元素。如果在next或者previous上次调用之后列表的结构就被修改了,将抛出一个IllegalStateException异常。
boolean hasPrevious()//当反向迭代列表时,若还有可供访问的元素,则返回true
E previous()//返回前一个对象。如果已经到达了列表的头部,就抛出一个NoSuchElementException异常
int nextIndex()//返回下一次调用next方法时将返回的元素的索引
int previousIndex()//返回下一次调用previous方法时将返回的元素索引
/*
迭代器的效率比get方法的效率高:因为迭代器保持着当前位置的计数值。
例如:返回索引为n的元素:
/*
ListIterator<String> iter=list.listIterator(n);
iter.next();
list.get(n);
//这两种方法都会返回相同的值,但是迭代器的效率要高于get方法。
此外:nextIndex和previousIndex两个方法获取索引的效率非常高!
当给集合附加许多迭代器时,由于有add和remove方法的存在,很可能造成并发修改异常。例如:
List<String> list=...;
ListIterator<String> iter1 = list.listIterator();
ListIterator<String> iter2 = list.listIterator();
//此时两个迭代器将同时指向list列表首个元素的前面
iter1.next();
iter1.remove();//通过iter1迭代器删除list首部元素
iter2.next();//这里抛出异常:ConcurrentModificationException
/*
解释:
iter删除了首部元素,但是iter2仍然指向已被删除的首部元素之前,所以这里的iter2迭代器失去了作用,它是无效的,所以抛出异常
该异常为并发修改异常,由集合跟踪改写操作实现:每个迭代器都维护了一个独立的计数值,在每个迭代器方法的开始出检查自己改写操作的计数值是否与集合的改写操作计数值一致,如果不一致,则抛出ConcurrentModificationException异常(set方法不被视为改写操作)
*/
使用链表唯一的理由是:
尽可能地减少在列表中插入或者删除元素所付出的代价。如果列表中只有几个元素,完全可以使用ArrayList代替。我们应该避免使用以整数索引表示链表中位置的所有方法。如果需要对集合进行随机访问,应该使用数组或者ArrayList。
4、数组列表(ArrayList)
ArrayList类封装了一个动态分配的对象数组。
有两种访问元素的协议:
- 一种是迭代器
- 另一种是get和set方法(数组中很有用,但是不适用于列表)
【注意】ArrayList和Vector
- Vector类的所有方法都是同步的。可以由两个及其以上的线程安全的访问一个Vector对象。但是,如果一个线程访问Vector,代码要在同步操作上花费大量的时间。
- ArrayList类不是线程同步的,多个线程访问时,会线程不安全。
综上所述:在需要使用同步的时候,使用Vector,不需要线程同步的时候使用ArrayList
接口如下:
/*
java.util.ArrayList<E>
*/
ArrayList<E>() //构造一个空数组列表
ArrayList<E>( int initialCapacity ) //构造一个指定容量的空数组列表
boolean add(E obj) //在数组列表尾部添加一个元素obj,返回true
/*
自动扩容:
如果调用add方法时,内部数组已经满了,数组列表会自动创建一个更大的数组,并将所有的对象从小的数组中拷贝到大数组中
*/
int size() //返回存储在数组列表中的当前元素的数量(这个值小于等于数组列表的容量)
void ensureCapacity( int capacity ) //确保数组列表在不重新分配储存空间的情况下就能够保存给定数量的元素
void trimToSize() //将数组列表的储存容量消减到当前尺寸,垃圾回收器将回收多余的储存空间
/*
一旦整理了数组列表的大小,再添加新元素就需要花时间移动储存块,所以应该再确认不会添加任何元素时,再使用该方法。
*/
void set(int index , E obj )//设置数组列表指定的元素值,这个操作将覆盖这个位置的原有内容。
//注意:新增元素的时候,使用add方法不要使用set方法
E get(int index) //获取指定位置的元素值
void add(int index, E obj) //在指定位置添加元素,该方法会向后移动指定位置及其之后的所有元素,将新元素插入到指定位置
E remove(int index) //删除一个元素,将后面的元素向前移动。返回被删除的元素。
数组列表的容量和数组的大小是不同的两个概念:
new ArrayList<>(100) // capacity 是 100,只是拥有保存100个元素的潜力(实际上,重新分配空间的话,可能超过100)
new Manager[100] // size 是100,即已经分配了100的空间,大小固定。
当使用一个原始类型的ArrayList赋予一个类型化的ArrayList时,编译器会提出一个警告,即使对该原始类型的ArrayList进行类型转换也同样会出现警告:
ArrayList<Manager> managers = new ArrayList<>(100); // 出现警告
ArrayList<Manager> managers = (ArrayList<Manager>)new ArrayList(100); // 仍然警告,指出类型有误
/*
原因:
鉴于兼容性的考虑,编译器在对类型转换进行检查之后,如果没有发现违反规则的现象,就将所有的类型化数组列表转换成原始ArrayList对象。
在程序运行时,所有的数组列表都是一样的,即没有虚拟机中的类型参数。因此,类型转换(ArrayList)和(ArrayList<Manager>)将执行相同的运行时检查。
*/
解决方法:
如果能确保不会造成严重的后果,可以使用@SuppressWarnings("unckecked")标注来标记这个标量能够接收类型转换。如:
@SuppressWarnings("unchecked")
ArrayList<String> managers = (ArrayList<String>)new ArrayList();
5、散列集(HashSet)
有几种能够快速查找元素的数据结构,但缺点是无法控制元素出现的次序,他们将按照有利于其操作目的的原则存放数据。散列表可以快速查找所需要的对象,它为每个对象计算一个整数,称为散列码(hash code)。散列码是由对象的实例域产生的一个整数。如果自定义类,就要负责实现这个类的hashCode方法。
注意:自定义的hashCode方法需要与equals方法兼顾(有关hashCode方法具体的讲解,请转至之前的笔记)
散列表用链表数组实现。每个列表被称为桶,要想查找表中对象的位置,就要计算它的散列码,【再与桶的总数取余】,所得结果就是保存这个元素的桶的索引。
如果桶已经被其他对象占满了,则称为【散列冲突】现象。这时需要将新对象和桶中的所有对象进行比较,判断该对象是否已经存在。
- 降低散列表运行性能:
如果选择的散列函数不当,或者有恶意代码试图在散列表中填充多个有相同散列码的值,或者需要插入到散列表中的元素太多,则会产生很多冲突,会大大降低运行性能。
- 提高散列表运行性能:
在 Java SE 8 中,桶满时,会从【链表】变成【平衡二叉树】,这样有助于提高性能。如果想要更多的控制散列表的运行性能,就需要散列码是合理且随机分布的,同时指定一个初始的桶数,这样需要比较的次数就会很少。
桶数是指用于收集具有相同散列值的桶的数目。可以将桶数设置为预计元素个数的75%~150%。最好将桶数设置为一个素数,以防止键的重聚。标准类库中使用的桶数是2的幂,默认值为16(为表大小提供的任何值都将被自动转换为2的下一个幂)。
如果散列表太满就需要【再散列】:即创建一个桶数为原表桶数两倍的新表,将原表中的所有元素插入到这个新表中,并丢弃原表。决定何时进行再散列的是【填装因子】,一般来说将填装因子设置为0.75比较合理。若原表中超过75%的位置已经填入元素,则进行再散列。
利用散列表实现set类型的数据结构——散列集(HashSet)
(set:是没有重复元素的元素集合。其add方法会首先在集中查找需要添加的对象,如果不存在,则将这个对象添加到set集中)
- HashSet:它实现了基于散列表的集。可以使用add方法添加某个元素。另外:其contains方法被重新定义,用来快速查看是否某个元素已经出现在集中,这个方法只需要在某个桶中查找元素,而不需要访问整个集合中的所有元素。
散列表的迭代器会依次访问所有的桶。由于散列将所有的元素分散在整个表的各个位置上,所以元素的访问顺序是无序的。所以只有在不关心集合中元素的顺序时才应该使用HashSet,其方法如下:
/*
java.util.HashSet<E>
*/
HashSet() //构造一个空三列表
HashSet( Collection < ? extends E > elements) //构造一个散列集,将集合中的所有元素添加到这个散列集中。
HashSet( int initialCapacity ) //构造一个空的具有指定容量(桶数)的散列集。
HashSet( int initialCapacity , float loadFactor ) //构造一个具有指定容量和填装因子(0.0~1.0)的空散列集。
/*
java.lang.Object
*/
int hashCode() //返回这个对象的散列码,散列码可以是任何整数,包括负数和正数。equals和hashCode的定义必须兼容。
6、树集(TreeSet)
TreeSet是一个有序集合,对集合进行遍历时,迭代器总是以自动排好序的顺序访问每一个元素。TreeSet内部排序使用的是红黑树。每次将一个元素添加到树中,都会被放到正确的排序位置上。
将一个元素添加到树中,比添加到散列表中慢。但是检查相同的元素时,其速度比数组和链表快很多。若树中包含了n元素,则添加一个新元素需要比较次。
使用树集时,元素之间要求可比较,其必须实现Comparable接口,或者构造集时必须提供一个Comparator。
7、队列、双端队列、优先级队列
- 队列(Queue):先进先出
根据先进先出原则,队列可以有效的在尾部添加一个元素,在头部删除一个元素。其通常有两种实现方式:使用循环数组、是用链表。(接口如下:)
/*
java.util.Queue<E>
*/
boolean add ( E element )
boolean offer ( E element )
/*
添加元素:
如果队列没有满,则将给定的元素添加到这个双端队列的尾部并返回true
如果队列满了,则第一个方法将抛出一个IllegalStateException,第二个方法返回false
*/
E remove()
E poll()
/*
删除元素:
如果队列不为空,删除这个队列的头部元素
如果队列为空,则第一个方法抛出NoSuchElementException,第二个方法返回null
*/
E element()
E peek()
/*
返回元素:
如果队列不为空,返回这个队列头部的元素,但不删除
如果队列为空,第一个方法将抛出NoSuchElementException,第二个方法返回null
*/
- 双端队列(Deque):有两个端头
有效地在头部和尾部同时添加和删除元素。ArrayDeque和LinkedList两个类实现了Deque接口,所以这两个类都提供了双端队列,并且在必要的时候可以增加队列的长度。(后面还会看到有限队列和有限双端队列)
Deque接口如下:
/*
java.util.Deque<E>
*/
void addFirst( E element )
void addLast( E element )
boolean offerFirst( E element )
boolean offerLast( E element )
/*
将给定的对象添加到双端队列的头部或者尾部:
如果队列满了,前两个方法将抛出IllegalStateException,后面两个返回false
*/
E removeFirst()
E removeLast()
E pollFirst()
E pollLast()
/*
删除队列首部或者尾部的元素:
如果队列满了,前两个方法将抛出一个NoSuchElementException,后面两个方法返回null
*/
E getFirst()
E getLast()
E peekFirst()
E peekLast()
/*
获取队列首部或者尾部的元素:
如果队列满了,前两个方法将抛出NoSuchElementException,后面两个返回null
*/
LinedList上文已经解说过,这里只给出ArrayDeque接口的方法:
/*
java.util.ArrayDeque<E>
*/
ArrayDeque() //用初始容量16构造一个无限双端队列
ArrayDeque( int initialCapacity ) //用给定容量构造一个无限双端队列
- 优先级队列(PriorityQueue):使用了数据结构——堆(heap)
堆:是一棵可以自我调整的二叉树,对树执行add和remove操作,可以让最小的元素移动到根,而不必花时间对元素进行排序。
所以每次对优先级队列实行remove操作都会得到队列中的最小元素
与TreeSet一样,一个优先级队列既可以保存实现了Comparable接口的类对象,也可以保存在构造器中提供的Comparator。
优先级队列接口如下:
/*
java.util.PriorityQueue
*/
PriorityQueue()
PriorityQueue( int initialCapacity )//构造一个用于存放Comparable对象的优先级队列
PriorityQueue( int initialCapacity , Comparator < ? super E > c )//构造一个优先级队列,并用指定的比较器对元素进行排序。
8、映射map
(1)基本映射操作:HashMap和TreeMap
HashMap和TreeMap都实现了Map接口。其中,散列映射(HashMap)对键进行散列;树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。与集类似,散列比树快,在不需要按照排列顺序访问键时,就最好使用散列映射。
散列或者比较函数都只能作用于键,与键关联的值不能进行散列或者比较。
接口如下:
/*
java.util.Map< K , V >
*/
V get( Object key) //获取与键对应的值(对象),如果映射中不存在这个对象,则返回null,键可以为null,但是值不可以为null
default V getOrDefault( Object key ,V defaultValue ) //获取与键对应的值,如果不存在,则返回参数所给定的默认值defaultValue
V put( K key ,V value ) //将键对应的值关系插入到映射中,如果这个键已经存在,新的对象将取代这个键对应的旧对象,如果这个键以前没有出现过,则返回null。注意:键可以为null,但是值不能为null
void putAll( Map < ? extends K , ? extends V > entries ) //将给定映射中的所有条目添加到这个映射中。
boolean containsKey( Object key ) //如果映射中已经有这个键,返回true
boolean containsValue( Object value ) //如果映射中已经有这个值,返回true
default void forEach ( BiConsumer < ? super K , ? super V > action ) //对这个映射中的所有键/值对 应用这个动作,一般来说使用lambda表达式,如下:
/*
manager.forEach( (k,v) -> {
System.out.prinlt( " key = " + k + " value = " + v );
})
*/
/*
java.util.HashMap<K, V>
*/
HashMap()
HashMap( int initialCapacity )
HashMap( int initcalCapacity , float loadFactor )
/*
用给定的容量和填装因子(0.0~1.0)构造一个空散列映射。填装因子据欸的那个但列表填充的百分比,一旦超过就需要将其再散列到更大的表中。默认的填装因子为0.75
/*
java.util.TreeMap<K , V>
*/
TreeMap() //为实现Comparable接口的键构造一个空的树映射。
TreeMap( Comparator < ? super K > c ) //构造一个树映射,并使用一个指定的比较器对键进行排序
TreeMap( Map < ? extends K , ? extends > entries ) //构造一个树映射,将某个有序映射中的所有条目添加到树映射中,并使用与给定的有序映射相同的比较器
/*
java.util.SortMap<K , V>
*/
Comparator< ? super K > comparator() //返回对键进行排序的比较器。如果键是使用Comparable接口的compareTo方法进行比较的,则返回null
K firstKey ()
K lastKey ()
//返回映射中最小的元素和最大的元素