持有对象
- 其实这章就是告诉我们如何使用容器。除了数组外,Java还提供了一套相当完整的容器类来解决这个问题。其中最基本的类型是List、Set、Queue和Map。这些对象类型也称为集合类,但由于Java的类库中使用了Collection这个名字来指代该类库的一个特殊子集,所以也被称为容器。
- 下面是关于容器的关系图:
泛型和类型安全的容器
- 这一小节主要讲了在使用List和Map这些容器的时候,加上泛型能预防一些类型转换的错误。并且get的时候也不必加上强转的代码。总之好处多多,因为我平常就是这么做的,我也就不记录了。
基本概念
- Java容器类类库的用途是保存对象,并将其化为两个不同的概念:
- Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,而Set不能有重复元素。Queue按照排队规则来确定对象产生的顺序。
- Map。一组成对的键值对对象,允许你使用键来查找值。映射表允许我们使用另一个对象来查找某个对象,它也被称为关联数组,或被称为字典,是一个强大的编程工具。
添加一组元素
- 在java.util包中的Arrays和Collections类中有很多实用方法。下面会举例说明Arrays.asList()、Collections.addAll()和Collection.addAll()的用法。
import java.util.*;
public class Test {
public static void main(String args[]) {
Integer[] i = new Integer[] {1, 2, 3, 4, 5};
Collection c = new ArrayList<>();//向上转型
Collection c1 = new ArrayList<>();
c.addAll(c1);//这种是直接将另外一个Collection加入到旧Collection中。
c.addAll(Arrays.asList(i));//这种是直接将另外一个Collection加入到旧Collection中。
c.addAll(Arrays.asList(1, 2, 3, 4, 5));//这种是直接将另外一个Collection加入到旧Collection中。
Collections.addAll(c, i);
//Collections.addAll(c, Arrays.asList(i));//这种方式是添加一个数组进去,不要混淆
Collections.addAll(c, 1, 2, 3, 4, 5);
//要注意通过Arrays.asList(i)转换而来的List由于底层是个固定大小的数组是不能修改大小的。
//看过源码可得知,此处的返回类型其实是Arrays中的一个嵌套类,它的数组使用了final修饰词
List<Integer> ints = Arrays.asList(i);
ints.set(0, 50);//可以修改 [50, 2, 3, 4, 5]
System.out.println(ints);//由于toString已经被重写了,可以直接打印。
//ints.add(60);//不能更改大小,add或remove
//java.lang.UnsupportedOperationException
//此外还可以通过构造方法创建一个浅拷贝List
ArrayList c2 = new ArrayList<StringBuffer>();
c2.add(new StringBuffer("hello"));
List<StringBuffer> c3 = new ArrayList<>(c2);//浅拷贝
System.out.println(c3);
c3.get(0).append(" world");
System.out.println(c2);//hello world
}
}
- 下面是关于asList()产生的类型的一些要注意的地方:
import java.util.*;
class O {}
class A extends O {}
class B extends O {}
class A1 extends A {}
class A2 extends A {}
class B1 extends B {}
class B2 extends B {}
public class Test {
public static void main(String args[]) {
List<O> ol0 = Arrays.asList(new A(), new B());
List<O> ol1 = Arrays.asList(new A1(), new B1());
List<A> ol2 = Arrays.asList(new A1(), new A2());
//Type mismatch: cannot convert from List<A> to List<O>
//List<O> ol3 = Arrays.asList(new A1(), new A2());
List<O> ol4 = new ArrayList<O>();
//ol4.addAll(new A1(), new A2());//观察源码可知此处是extends T
Collections.addAll(ol4, new A1(), new A2());//观察源码可知此处是super T
List<O> ol5 = Arrays.<O>asList(new A1(), new A2());//显式类型参数说明
}
}
List
- List接口在Collection的基础上添加了大量的方法,使得可以在List的中间插入和移除元素。有三种常用类型的List:
- ArrayList,底层是用数组存放元素,所以随机访问元素比较快,但是由于插入和移除元素时需要考虑数组大小和扩容的问题,所以在插入和移除元素时速度较慢。初始大小为10,扩容方式为扩展到1.5倍,还不够就直接扩容到最大下标值。
- LinkedList,底层是用一个内部类(Node)的对象包含元素实体和前后元素引用的形式实现的。所以相对于ArrayList,它的插入和移除元素速度较快,但是随机访问元素速度较慢(只能通过第一个元素循环获得下一个元素的位置)。
- Vector,底层也是用数组存放元素,和ArrayList大同小异( Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe)),接下来不会进行过多分析。大家可以看看这篇文章。初始大小为10,默认扩容为自身2倍,也有传入每次扩容大小的构造器。不过现在已经过时了,不再推荐使用这个类。
- 下面我总结了一下List中的方法,记录是为了过一遍API,真的要使用可以去查看API(E指泛型):
- boolean add(E e);void add(int index, E e);前者调用了后者在末尾插入元素,后者是在指定位置插入元素,使该元素的下标为index。如果该元素后面还有元素,则要依次下标+1(其实是通过System.arraycopy()生成新数组实现)。这里如果是ArrayList也可以看出效率不高。
- boolean addAll(Collection< ? extends E> c);boolean addAll(int index, Collection< ? extends E> c); 和上面类似,c的顺序和迭代器顺序相同。比如插入的是List那就相当于保持原样。
- void clear(),移除所有元素。
- boolean contains(Object o);boolean containsAll(Collection< ?> c) ,ArrayList和LinkedList都是通过indexOf()方法判断是否包含对象,而indexOf()方法则是通过对象的equals()方法判断两个对象是否相等。这也意味着一般情况下两个属性完全相同的对象也不会返回false(除非你重写equals()),但是两个实体相同的String对象则会返回true。
- boolean equals(Object o) 这个方法是继承自AbstractList,如果o不是List的实现类型返回false,否则对每个元素按下标进行equals()比较,如果是两个null也被认为是符合的。
- E get(int index)这个方法在ArrayList和LinkedList中的实现有所不同。在LinkedList中会先检查下标是否越界,然后判断下标的位置。如果在前半部分,则通过firstNode往后查询,否则通过lastNode往前查询。而在ArrayList就没有那么麻烦了,除了同样需要检查是否越界以外,ArrayList只需通过下标返回数组中相应的元素就行了。
- int hashCode() 这个方法是继承自AbstractList的,简单来说就是对每个元素的hashCode()进行公式累加(有兴趣可以看看源码),因为其为int类型,所以可能出现越界返回负值。
- int indexOf(Object o);int lastIndexOf(Object o); 这个方法比较的方式是调用对象的equals()方法。所以如果是普通的对象则是用==去比较。但是类似String类型,就可能重写这个方法。另外还可以传入null,将会返回从左往右第一个为null的下标。后者则是从右到左
- boolean isEmpty() 如果List为空返回true。
- Iterator< E> iterator() 返回一个迭代器,下一小节细讲。
- ListIterator< E> listIterator(); ListIterator< E> listIterator(int index);返回一个列表迭代器,下一小节细讲。
- E remove(int index);boolean remove(Object o);这个不多说了,前者返回类型是E,后者如果没有该元素返回false。这两个方法在ArrayList和LinkedList中的实现有所不同。前者需要重新生成一个数组,后者只需要修改一下引用就可以了。
- boolean removeAll(Collection< ?> c) ;boolean retainAll(Collection< ?> c) 前者是移除c中的元素,后者是保留c中的元素。有意思的是在源码中,ArrayList对这两个方法其实使用了同一个私有方法实现,只是传入的boolean值有所不同。算法大致是这样的。拿remove举例,先对原来的Collection进行遍历,如果c不包含该元素,则将该元素设置为原Collection的第0个下标,依次类推。如果中间出现异常的话(我也不知道会出现什么异常),则认为还未遍历的元素都符合并通过System.arraycopy加入原Collection。最后再将后面都置null(见下面代码1)。而LinkedList则是则是直接继承AbstractCollection中的方法,直接利用Iterator的remove()实现。
- E set(int index, E element) ;用指定元素替换列表中指定位置的元素,具体实现的话ArrayList就是直接换,LinkedList需要先定位,然后将内部类的item置换成element就行了。
- int size() 返回列表中的元素数。
- List< E> subList(int fromIndex, int toIndex) 返回列表中指定的 fromIndex(包括 )和 toIndex(不包括)之间的部分视图。
- Object[] toArray(); < T> T[] toArray(T[] a) 前者没有什么好说的,后者的话用法见代码2。
//代码1 remove complement为false retainAll为true
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;//这个值有其他用处 此处可以不用关注
size = w;
modified = true;
}
}
return modified;
}
//代码2 ArrayList中 toArray(T[] t)的实现 LinkedList大同小异省略
public <T> T[] toArray(T[] paramArrayOfT)
{
if (paramArrayOfT.length < this.size) {
return (Object[])Arrays.copyOf(this.elementData, this.size, paramArrayOfT.getClass());
}
System.arraycopy(this.elementData, 0, paramArrayOfT, 0, this.size);
if (paramArrayOfT.length > this.size) {
paramArrayOfT[this.size] = null;
}
return paramArrayOfT;
}
-----------------------------以下是测试代码:
import java.util.*;
public class Test {
public static void main(String args[]) {
String s1 = new String("123");
ArrayList<String> l = new ArrayList<String>();
l.add(s1);
l.add(s1);
l.add(s1);
String[] i = new String[0];
String[] copyi = l.toArray(i);
String[] copyi1 = l.toArray(new String[0]);
//String[] copyi2 = (String[]) l.toArray();//ClassCastException
Object[] copyi3 = l.toArray();
System.out.println(Arrays.toString(copyi));
System.out.println(Arrays.toString(copyi1));
System.out.println(Arrays.toString(copyi3));
System.out.println(Arrays.toString(i));
}
}
--------------------运行结果
[123, 123, 123]
[123, 123, 123]
[123, 123, 123]
[]//这里也能反映Java调用方法是传值而不是传递引用。
- 1
List接口中的方法已经简单说明了。当然ArrayList和LinkedList中还有很多额外的方法,此处就不展开了,平常开发中使用List的方法差不多就够了。不过还是需要知道他们实现的区别。有兴趣可以去看源码。
迭代器
- 迭代器是一种设计模式。迭代器是一个对象,它的工作是遍历并选择序列中的对象,而客户端程序员不必知道或关心该序列底层的结构。此外,迭代器通常被称为轻量级对象:创建它的代价小。所以限制也比较多(因为还是借用其底层结构实现了相应的接口方法),例如Java的Iterator只能单向移动。Collection和Map接口都是继承自Iterable接口。这个接口有一个 Iterator< T> iterator();方法。所以我们可以对Collection和Map的实例通过iterator()方法获得一个迭代器对象。
- Iterator接口一共有三个方法:
- boolean hasNext();
- E next();
- void remove();
- 不同类型的实现方式有所不同,这里主要讲一下ArrayList的实现方式,来看一下源代码:
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
- 在ArrayList中是以一个私有内部类的形式,返回一个实现了Iterator接口的对象。构造方式非常简单,几乎不需要什么代价。在这个内部类中一共有三个属性和三个方法。前面两个看注释也就懂了,expectedModCount 这个属性主要是用来判断在迭代过程中,原集合是否大小进行了修改。这个我没有仔细去解读过,modCount是AbstractList的一个属性:
protected transient int modCount = 0;
用法大致就是在add和remove的时候自增。三个方法的作用我觉得一目了然,就不再多说了。 - ListIterator是Iterator的一个子类型。前面我们已经提到List接口中有两个方法是返回这个类型的。相对于Iterator,由于它是针对List对象,所以它的方法也更加丰富。由于其允许双向移动,所以ListIterator< E> listIterator(int index) 允许我们传入一个下标创建一个ListIterator对象。我们来看看ArrayList中的源代码:
public ListIterator<E> listIterator() {
return new ListItr(0);
}
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
/**
* An optimized version of AbstractList.ListItr
*/
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
- 这代码多看几遍就明白了,这里也不想过多解释。新增方法有add(E e),set(E e),previous(),previousIndex(),nextIndex()(感觉是为了对称才出的),hasPrevious()
LinkedList
- LinkedList除了实现List接口外还实现了Deque接口,这个接口继承了Queue接口,所以LinkedList还实现了可以使其用作栈、队列或双端队列的方法。这些方法中有些彼此之间只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用。例如getFirst()和element()完全一样,都是返回列表中首元素,如果列表为空,则抛出NoSuchElementException。而peek()方法也是返回首元素,只不过在列表为空的情况下会返回null。
- removeFirst()和remove()也是一样的,都是移除首个元素并返回,不存在抛出NoSuchElementException。poll()稍有差异,在列表为空的时候返回null。addLast()和add()都是在尾部添加元素,而addFirst()是在首部添加元素。removeLast()是移除并返回最后一个元素。
Stack
- 栈通常指后进先出的容器。LinkedList具有能够直接实现栈的所有功能的方法,因此LinkedList可以直接作为栈来使用。下面是组合LinkedList创建的一个Stack类。
class MyStack<T> {
private LinkedList<T> storage = new LinkedList<T>();
public void push(T e) {storage.addFirst(e);}
public T peek() {return storage.getFirst();}
public T pop() {return storage.removeFirst();}
public boolean empty() {return storage.isEmpty();}
public String toString() {return storage.toString();}
}
- 1
- Stack没有被设计成接口,可能是因为Java1.0中的设计欠佳的java.util.Stack类占用了这个名字。尽管已经有了java.util.Stack,但是LinkedList可以产生更好的Stack(但是LinkedList不是线程安全的)。java.util.Stack设计欠佳是因为底层使用的是数组而不是链表,而Stack需要频繁进栈出栈,所以使用链表更加合理。
import java.util.*;
public class Test {
public static void main(String[] args) {
List<Integer> data1 = new ArrayList<>();
for (int i=0; i<10; i++) {
data1.add(i);
}
List<Integer> data2 = new ArrayList<>();
for (int i=0; i>-10; i--) {
data2.add(i);
}
//使用SubStack始终不会报错,因为在用toString()打印容器的时候,该方法是加锁了的(继承自Vector)
//StackInterface<Integer> s1 = new SubStack<Integer>();
//使用MyStack则偶尔会报错,因为toString()过程中,可以对stack进行修改,导致modCount变化或者其他错误。
StackInterface<Integer> s1 = new MyStack<Integer>();
PushThread<Integer> pt1 = new PushThread(s1, data1);
PushThread<Integer> pt2 = new PushThread(s1, data2);
pt1.start();
pt2.start();
}
}
//为了代码共用 我创建了一个接口
interface StackInterface<T> {
public T push(T e);
public T peek();
public T pop();
public boolean empty();
public String toString();
}
//继承Stack的线程安全的栈
class SubStack<T> extends Stack<T> implements StackInterface<T> {}
//组合LinkedList的线程不安全的栈
class MyStack<T> implements StackInterface<T> {
private LinkedList<T> storage = new LinkedList<T>();
public T push(T e) {storage.addFirst(e);return e;}
public T peek() {return storage.getFirst();}
public T pop() {return storage.removeFirst();}
public boolean empty() {return storage.isEmpty();}
public String toString() {return storage.toString();}
}
class PushThread<T> extends Thread {
StackInterface<T> si;
List<T> pushList;
public PushThread(StackInterface<T> si, List<T> pushList) {
this.si = si;
this.pushList = pushList;
}
@Override
public void run() {
for (int i = 0; i < pushList.size(); i++) {
T t = si.push(pushList.get(i));
System.out.println("目前: " + si);
}
}
}
class PopThread<T> extends Thread {
StackInterface<T> si;
int popSize;
public PopThread(StackInterface<T> si, int popSize) {
this.si = si;
this.popSize = popSize;
}
@Override
public void run() {
for (int i = 0; i < popSize; i++) {
T t = si.pop();
System.out.println("目前: " + si + " pop:" + t);
}
}
}
Set
- 这里要开始讲另外一个与List接口平起平坐的Set接口。不保存重复的元素是Set的最大特点。Set中最被常使用的是测试归属性,你可以很容易地询问某个对象是否在某个Set中。所以查找就成为了Set中最重要的操作,因此通常会选择HashSet作为实现,它专门对快速查找做了优化。
- 其实看了源码你会发现,Set接口的定义居然和Collection一模一样,虽然List接口也是继承Collection的,但是List至少对Collection有一些扩展,这个Set同学就很过分了,完全就是照抄,那为什么需要定义一个Set接口呢?其实这里只不过是体现了继承和多态的思想。多定义一个Set接口,以此表名该接口具有的特殊行为。下面先简单介绍一下Set接口。一共有三个类实现了该接口:
- HashSet,使用了散列(后面章节介绍),保存顺序与插入顺序无关,查询速度优秀。它借助了HashMap实现了数据的存储。
- TreeSet,将元素存储于红-黑树结构中,保存顺序与插入顺序无关,但是它会按照一定规则自动对元素进行排序(构造方法能够传入比较器,关于比较器后期会进行说明)。它借助了TreeMap实现了数据的存储。
- LinkedHashSet,也使用了散列,保存的顺序与插入顺序相同,同时查询速度也不赖。它借助了LinkedHashMap实现了数据的存储。
- 这位Set同学不仅照抄Collection,连下面的实现都是借助了Map,真的是上梁不正下梁歪。等说到Map了再仔细说说Hash和Tree是如何实现的吧。下面是一个简单的应用Set的例子。
public class Test {
public static void main(String[] args) {
Set<Integer> s1 = new HashSet<Integer>();
Set<Integer> s2 = new TreeSet<Integer>();
Set<Integer> s3 = new LinkedHashSet<Integer>();
test(s1);
test(s2);
test(s3);
}
public static void test(Set s) {
s.add(5);
s.add(4);
s.add(1);
s.add(1);//能够去重
s.add(2);
s.add(3);
for (int i=6; i<30; i++) {
s.add(i);//元素太少,无法反应HashSet的无序性
}
System.out.println(s);
}
}
-----------------执行结果:
HashSet:后面元素位置不会保持顺序
TreeSet:按照从小到大排序
LinkedHashSet:与插入顺序相同
- 1
Map
- 其他就不废话,接下来主要说说HashMap、LinkedHashMap、TreeMap是如何实现的。关于Map此处不再展开讨论方法的使用方式,只是简单说一下原理。
HashMap
- HashMap中有一个default属性Entry< K, V>[] table以此存放元素,初始默认大小为16,阈值为16*0.75=12。当元素大于阈值时会进行扩容,每次扩大2倍。扩容的同时也会对元素进行重排列。这个Entry是一个内部类,4个属性是K key,V value,int hash,Entry next.这个hash值就是通过散列函数计算得来的,而next就是一种链表的应用。
- 这个table属性的长度就是HashMap的最大长度,接下来说明为什么HashMap的优势是查找速度。当你根据一个key要插入一个value时,它会根据key的hash值(int型)映射到这个table对应的下标。然后生成对应的Entry放入table中,如果多个key对应了同个下标,那么table只会保存最后一个Entry,而之前的Entry则以next属性的形式去维持。这样当我们根据一个key值要去获得value时,就不必遍历整个Map了,我们可以根据hash值得知相应的table下标,再用next去遍历这个链表。
LinkedHashMap
- 这个类继承于HashMap,它有一个新的属性Entry header;这里的Entry同样也继承了HashMap中的Entry,扩展了Entry before和Entry after;header的功能也就是维持一个插入顺序的链表。当每次插入新的元素时,LinkedHashMap就会使该元素成为header元素,然后使之前的header元素成为新header的after值。所以就是在HashMap的基础上再多维持一个链表而已。而生成迭代器时,迭代器的方法会借助这个属性。
TreeMap
- TreeMap的实现借助了红黑树,具体它是如何维持整棵树的顺序,以及add和remove后树的调整这里不再展开。简单来说,TreeMap中维护了一个root属性,该属性是该树的根节点。当我们要查找一个key时,会根据key的大小不断遍历节点的左子树或右子树(小左右大)。它的查找最多次数即是树的层数,可见其查询速度也是优秀的(TreeSet的查找速度比LinkedList快,比HashSet慢)。它的最大的特点是有序。我们可以通过后序遍历获得一个按照key排序的EntrySet。
Queue
- 队列是一个先进先出的容器。LinkedList实现了Queue接口,提供了支持队列行为的方法。这部分就不重新复述了。接下来讲一下Queue的一个实现类。
PriorityQueue
- 优先级队列声明下一个弹出元素是最需要的元素(具有最高的优先级)。当你在PriorityQueue上调用offer()方法来插入一个对象时,这个对象会在队列中被排序,由此确保当你调用peek(),poll()或remove()方法时,获得的元素将是队列中优先级最高的元素。
- 当我读完定义时,我就想到是否可以借助TreeSet的有序性实现PriorityQueue。
import java.util.Comparator;
import java.util.Queue;
import java.util.TreeSet;
class TestObj {
public int pri;
private String key;
public TestObj(int pri, String key) {
this.pri = pri;
this.key = key;
}
public String toString() {
return key;
}
}
public class MyPriQueue<E> extends TreeSet<E> implements Queue<E> {
public static void main(String args[]) {
Queue<TestObj> queue = new MyPriQueue<TestObj>(
new Comparator<TestObj>() {
@Override
public int compare(TestObj o1, TestObj o2) {
return o1.pri - o2.pri;
}
}
);
//缺点就是不能放两个相同的key
queue.offer(new TestObj(5, "5"));
queue.offer(new TestObj(4, "4"));
queue.offer(new TestObj(3, "3"));
System.out.println(queue.poll());
queue.offer(new TestObj(6, "6"));
System.out.println(queue.poll());
}
public MyPriQueue() {
}
public MyPriQueue(Comparator<? super E> c) {
super(c);
}
public boolean offer(E e) {
return add(e);
}
public E poll() {
return pollFirst();
}
public E element() {
return first();
}
public E peek() {
return first();
}
public E remove() {
return remove();
}
}
- JDK已经实现了PriorityQueue,他存放元素使用的是Object[],并且还有一个 Comparator< ? super E> comparator。因为像Integer、String这些都实现了Comparator接口,比较方式就是其默认的比较方式,PriorityQueue的行为除了在add的时候通过比较器进行比较获得插入的位置以外,其余的和ArrayList类似。不过它获取插入的位置的算法特别有意思,并不是简单的维持整个数组有序,它只保持了局部有序,这种方式优化了出队入队的速度。
//paramInt 为当前size paramE为要插入的元素
private void siftUpUsingComparator(int paramInt, E paramE)
{
while (paramInt > 0)
{
int i = paramInt - 1 >>> 1;
Object localObject = this.queue[i];
if (this.comparator.compare(paramE, localObject) >= 0) {
break;
}
this.queue[paramInt] = localObject;
paramInt = i;
}
this.queue[paramInt] = paramE;
}
- 通过这个方法可以看出,当要插入一个元素时,它会将其与当前数组的(-1 /2)位置进行比较,如此并维护了局部方向上的有序性。而当我们将元素出队列时,会调用下列方法:
//paramInt 为0 paramE为queue[size-1]即最后一个元素
private void siftDownUsingComparator(int paramInt, E paramE)
{
int i = this.size >>> 1;
while (paramInt < i)
{
int j = (paramInt << 1) + 1;
Object localObject = this.queue[j];
int k = j + 1;
if ((k < this.size) && (this.comparator.compare(localObject, this.queue[k]) > 0)) {
localObject = this.queue[(j = k)];
}
if (this.comparator.compare(paramE, localObject) <= 0) {
break;
}
this.queue[paramInt] = localObject;
paramInt = j;
}
this.queue[paramInt] = paramE;
}
- 将元素从0到(*2+1和 *2+2)中的更小的值调换,首先0位置已经是空的了。 根据之前的插入规则,我们可以得出 0 *2+1和 0 *2+2 将是剩余元素中的最小的2个值。设置好0的位置以后,我们还需要将后面的位置也设置好。这个算法有兴趣可以研究一下。
Foreach和迭代器
- 我们知道数组和实现Collection的接口类都是直接能够使用foreach语法的,最重要的原因是Collection实现了Iterable接口。以下是我做的一个测试类:
import java.util.*;
public class Test {
public static void main(String args[]) {
Test2<Integer> t = new Test2<Integer>(10);
t.add(1);
t.add(2);
t.add(4);
t.add(8);
for (Integer i:t) {
System.out.println(i);
}
}
}
class Test2<T> implements Iterable<T> {
private final Object[] t;
private final int length;
private int size;
public Test2(int size) {
length = size;
t = new Object[size];
}
public void add(T obj) {
t[size++] = obj;
}
public Iterator<T> iterator() {
return new Iterator<T>() {
private int index = 0;
public boolean hasNext() {
return index < length && t[index] != null;
}
public T next() {
return (T) t[index++];
}
public void remove() {
//暂时不实现这个方法
}
};
}
}
小结
- 其实只有四种容器:Map、List、Set和Queue,他们各有两到三个实现版本。看到此处希望你可以再次回味一下这四个接口的用法。