Set 是不包含重复元素的 Collection。更确切地讲,Set 不包含满足 e1.equals(e2) 的元素对 e1 和 e2,并且最多包含一个 null 元素。正如其名称所暗示的,此接口模仿了数学上的 set 抽象。声明的方法都是直接继承自父接口Collection的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface Set<E> extends Collection<E> {

int size();

boolean isEmpty();

boolean contains(Object o);

Iterator<E> iterator();

Object[] toArray();

<T> T[] toArray(T[] a);

boolean add(E e);

boolean remove(Object o);

boolean containsAll(Collection<?> c);

boolean addAll(Collection<? extends E> c);

boolean retainAll(Collection<?> c);

boolean removeAll(Collection<?> c);

void clear();

boolean equals(Object o);

int hashCode();

@Override
default Spliterator<E> spliterator() { // ... }
}

SortedSet 里面的元素一定是有序的。保证迭代器按照元素递增顺序遍历的集合,可以按照元素的自然顺序(参见 Comparable)进行排序,或者按照创建有序集合时提供的 Comparator进行排序。要采用此排序,还要提供一些其他操作。插入有序集合的所有元素都必须实现 Comparable 接口(或者被指定的 Comparator 所接受)。另外,所有这些元素都必须是可相互比较的,比如像 e1.compareTo(e2)
(或 comparator.compare(e1, e2))对于有序集合中的任意元素 e1 和 e2 都不能抛出ClassCastException。试图违反这些限制将导致违反规则的方法或者构造方法调用抛出 ClassCastException。
注意,如果有序集合正确实现了 Set 接口,则有序集合所保持的顺序(无论是否明确提供了比较器)
都必须保持相等一致性。这也是因为 Set 接口是按照 equals 操作定义的,但有序集合使用它的 compareTo(或 compare)方法对所有元素进行比较,
因此从有序集合的观点来看,此方法认为相等的两个元素就是相等的。
即使顺序没有保持相等一致性,有序集合的行为仍然是 定义良好的,
只不过没有遵守 Set 接口的常规协定。
所有通用有序集合实现类都应该提供 4 个“标准”构造方法:
1) void(不带参数)构造方法,创建空的有序集合,按照元素的自然顺序排序。
2) 带有一个 Comparator 类型参数的构造方法,创建一个空的有序集合,根据指定的比较器排序。
3) 带有一个 Collection 类型参数的构造方法,创建一个元素与参数相同的有序集合,按照元素的自然顺序排序。
4) 带有一个 SortedSet 类型参数的构造方法,创建一个新的有序集合,元素及排序方法与输入的有序集合相同。
除了 JDK 实现(TreeSet 类)遵循此建议外,无法保证强制实施此建议(因为接口不能包含构造方法)。

声明的主要接口

Public Methods
abstract Comparator<? super E>comparator()
Returns the comparator used to compare elements in this SortedSet.
返回与此有序集合关联的比较器,如果使用元素的自然顺序,则返回null。
abstract Efirst()
Returns the first element in this SortedSet.
返回此有序集合中当前第一个(最小的)元素。
abstract SortedSet<E>headSet(E end)
Returns a SortedSet which contains elements less than the end element.
用一个SortedSet,返回此有序集合中小于end的所有元素。
abstract Elast()
Returns the last element in this SortedSet.
返回此有序集合中最后一个(最大的)元素
abstract SortedSet<E>subSet(E start,E end)
Returns a SortedSet which contains elements greater or equal to the start element but less than the end element.
返回此有序集合的部分元素,元素范围从fromElement(包括)到toElement(不包括)。
abstract SortedSet<E>tailSet(E start)
Returns a SortedSet which contains elements greater or equal to the start element.
返回此有序集合的部分元素,其元素大于或等于fromElement。


上一篇已经讲解了List接口的一些内容,现在继续看一下Collection的另一个子接口Queue,以及Queue的子接口Deque。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Queue<E> extends Collection<E> {

// 继承自Collection
boolean add(E e); // 在队列中插入特定元素

boolean offer(E e); // 在队列中插入特定元素

E remove(); // 删除并取得队列头元素

E poll(); // 删除并取得队列头元素

E element(); // 从头部取得元素但不删除

E peek(); // 从头部取得元素但不删除
}

声明的这些方法中,只有Collection是继承自Collection接口的。

  • add与offer的不同之处在于:在容量受限的队列中,offer方法比add方法更好,因为add在插入失败时仅仅抛出异常
  • remove与poll的不同之处在于:如果队列为空,remove将抛出异常,poll返回null
  • element与peek的不同之处在于:如果队列为空,element将抛出异常,peek返回null

Deque是双端队列(double ended queue)的缩写,大部分Deque用于不固定元素数量的情形,当然也支持容量受限的情形。和Queue接口一样,Deque的插入、删除以及获取元素的操作都有两种形式,当操作失败时,前一种形式抛出异常,后一种形式返回特殊值(null或者false)。后一种形式的插入操作用于容量受限的Deque实现,而在大多数情况中,插入操作不会失败。

Deque方法总结
首元素 (头) 尾元素 (尾)
抛出异常特殊值抛出异常特殊值
插入void addFirst(E e)boolean offerFirst(E e)void addLast(E e)boolean offerLast(E e)
删除E removeFirst()E pollFirst()E removeLast()E pollLast()
获取E getFirst()E peekFirst()E getLast()E peekLast()

Deque用做队列时,表现为FIFO(先入先出),从队尾进入,从队首删除,与Queue中的一些方法等价:

Queue和Deque方法比较
Queue方法等价的Deque方法
boolean add(E e)void addLast(E e)
boolean offer(E e)boolean offerLast(E e)
E remove()E removeFirst()
E poll()E pollFirst()
E element()E getFirst()
E peek()E peekFirst()

用做栈时,表现为LIFO(后入先出),入栈和出栈都是从队首,与Stack中的一些方法等价:

Deque和Stack方法比较
Stack方法等价的Deque方法
E push(E e)void addFirst(E e)
E pop()E removeFirst()
E peek()E peekFirst()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface Deque<E> extends Queue<E> {

// Deque方法总结中提到的12个方法

boolean removeFirstOccurrence(Object o); // 删除第一个出现的特定元素,没有该元素不发生改变

boolean removeLastOccurrence(Object o); // 删除最后一个出现的特定元素,没有该元素不发生改变

// 继承自Queue的6个方法

// *** Stack methods ***
void push(E e); // 入栈

E pop(); // 出栈

// *** Collection methods ***
// 继承自Collection的remove、contain、size、iterator方法

Iterator<E> descendingIterator(); // 返回反向迭代器,和iterator相反,实现从两端访问元素
}

Deque接口提供了两个删除内部元素的方法,removeFirstOccurrence、removeLastOccurrence,可以避免只能从首或尾操作的麻烦。与List不同,Deque不支持通过索引的方式访问元素。


接着上一篇的内容,看过了Collection以后,再看一下继承了它的三个子接口List、Set、Queue
几种常用的数据结构如ArrayList,LinkedList等,都实现了List接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface List<E> extends Collection<E> {

// ... 省略已经在父接口中声明的方法

boolean addAll(int index, Collection<? extends E> c); // 在List的特定位置插入某集合的全部元素

default void replaceAll(UnaryOperator<E> operator) // 替换列表中的每个数据

default void sort(Comparator<? super E> c) // 使用比较器对列表排序

E get(int index); // 得到列表指定位置的数据

E set(int index, E element); // 替换列表中指定位置的数据

void add(int index, E element); // 在列表的指定位置插入数据

E remove(int index); // 删除列表中指定位置的数据或第一次出现的指定数据

int indexOf(Object o); // 得到指定数据在列表中第一次出现的位置,如果列表中不包含指定数据,返回-1

int lastIndexOf(Object o); // 返回指定数据在列表中最后一次出现的位置

ListIterator<E> listIterator(); // 返回列表的迭代器

ListIterator<E> listIterator(int index); // 返回列表指定位置的迭代器

List<E> subList(int fromIndex, int toIndex); // 返回列表两个位置之间的数据组成的列表

@Override
default Spliterator<E> spliterator() // 创建一个列表的分片迭代器
}

首先是一个接口继承的问题,List接口中声明了很多Collection接口中已经声明过的方法,例如size,isEmpty等,没有用@Override做注解,在这里我想为什么已经在Collection中已经声明了size方法还要在子接口List中再次声明呢?
我的理解是,采用这样的设计方法,将层次明确,比如一个List的实现需要size方法时指的就是这个List接口的size,后面的实现类中,明确的实现了List接口中定义的size,而非Collection接口中的size。
其次是几个在JDK1.8中加入的几个default方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

// 将每个元素加1
list.replaceAll(new UnaryOperator<Integer>() {
@Override
public Integer apply(Integer integer) {
return integer + 1;
}
});

// 使用定义的比较器对list进行排序
list.sort(new Comparator<String>() {
public int compare(String x, String y) {
return x.compareTo(y);
}
});

Spliterator方法要细说就不是这个系列所要讲述的内容了。Spliterator(splitable iterator可分割迭代器)接口是Java为了并行遍历数据源中的元素而设计的迭代器,这个可以类比最早Java提供的顺序遍历迭代器Iterator,但一个是顺序遍历,一个是并行遍历。从最早Java提供顺序遍历迭代器Iterator时,那个时候还是单核时代,但现在多核时代下,顺序遍历已经不能满足需求了,如何把多个任务分配到不同核上并行执行,才是能最大发挥多核的能力,于是有了Spliterator。以后再开一篇JDK8的讲解系列。


学习各种集合容器最好先从基本接口入手,之前在《Java集合框架学习(1)——概览》中已经梳理清楚了各个类和接口的脉络,现在就从最基本的Collection来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface Collection<E> extends Iterable<E> {

int size(); // 返回集合中元素数量,超过最大数量后返回Integer.MAX_VALUE

boolean isEmpty(); // 集合是否为空

boolean contains(Object o); // 集合是否包含特定元素

Iterator<E> iterator(); // 返回该集合的迭代器

Object[] toArray(); // 返回一个包含集合中所有元素的数组

// 泛型方法用类型变量声明,在方法的修饰符后,返回类型的前面
<T> T[] toArray(T[] a); // 同toArray()

boolean add(E e); // 向集合中添加元素,返回是否添加成功

boolean remove(Object o); // 删除集合中的特定元素,返回是否删除成功

boolean containsAll(Collection<?> c); // 是否包含特定集合中的全部元素(是否为子集)

boolean addAll(Collection<? extends E> c); // 是否成功添加特定集合中的全部元素

boolean removeAll(Collection<?> c); // 是否删除特定集合中的全部元素

boolean retainAll(Collection<?> c); // 是否包含特定集合中的部分元素(是否有交集)

void clear(); // 删除集合中所有元素

// ...省略了几个default方法和Object方法
}

这样来看基本的集合操作就一目了然,需要注意的是toArray方法,有两个重构的方法,下面来看一些细节:

1
2
3
4
List<Integer> list = new ArrayList<Integer>();   
list.add(1);
Integer[] i1 = (Integer[]) list.toArray(); // 造型异常
Integer[] i2 = (Integer[]) list.toArray(new Integer[0]); // Ok

运行时会报java.lang.ClassCastException。看了一下ArrayList中toArray的实现,在不带参数的toArray方法里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 不带参数的toArray
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}

// 带参数的toArray
public <T> T[] toArray(T[] a) {
if (a.length < size)
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}

// Arrays.copyOf
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}

// copyOf
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class) // 主要是这里
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

采用无参的toArray,在三目运算中直接返回Object数组,此时进行转型是不安全的下溯造型(Downcasting),会产生ClassCastException,这也就是上述问题的原因了。
采用有参的toArray,newType.getCommponentType()返回数组内容的类型,根据该类型构造对应类型的数组copy,于是不会有问题。
Collection继承了Iterable接口,接下来再看一下这个接口:

1
2
3
4
5
6
7
package java.lang;
public interface Iterable<T> {

Iterator<T> iterator(); // 返回一个迭代器

// ...省略了几个default方法
}

文档中是这样说的,只要实现了这个接口,就可以使用for-each循环来遍历目标对象。需要注意的是,Iterable这个接口比较特殊,不在java.util中。

下一篇来学习一下继承了Collection接口的三个基本接口——List、Set、Queue。


在上一篇学习了一些ArrayList基本的API及其实现后,看一些细节性的东西,其中这个modCount变量值得注意。

1
2
3
4
5
6
7
8
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

// ...

protected transient int modCount = 0;

// ...
}

modCount是定义在AbstractList中的成员变量

  • protected,说明该变量只可以在同包中的任何类、不同包中的任何当前类的子类中所访问。即不同包中的任何不是该类的子类不可访问
  • transient,说明串行化时被忽略

在ArrayList中与add和remove相关的操作,都会对该变量进行修改,例如remove方法中用到的fastRemove

1
2
3
4
5
6
7
8
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}

那么这个变量的作用是什么呢?ArrayList的内部类Itr实现了迭代器,其中定义了成员变量expectedModCount,在checkForComodification方法中对expectedModCount是否与ModCount相同进行了检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

可想而知,在使用迭代器遍历ArrayList时,如果使用ArrayList的add、remove方法,导致modCount改变,迭代器在检查时发现与期待的expectModCount不同,会抛出ConcurrentModificationException异常,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(2);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer integer = iterator.next(); // 检查抛出异常
if(integer==2)
list.remove(integer);
}
}
}

那么如果在迭代过程中只是将ArrayList的remove方法换成在迭代器中定义的remove方法就对了吗?

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(2);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer integer = iterator.next();
if(integer==2)
iterator.remove(); //注意这个地方
}
}
}

在单线程环境下确实如此,可当有多个线程对ArrayList进行操作时,线程1进行remove操作后改变了modCount值,但由于modCount不是volatile变量,线程2可能会看到原来的modCount,也可能看到新的modCount,当发现与本线程的expectModCount不同时,仍然会抛出异常。
即使换成Vector容器,可Vector也是继承自AbstractList,仍然会有问题。因此一般有2种解决办法:

  • 在使用iterator迭代的时候使用synchronized或者Lock进行同步;
  • 使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。


第一章环境搭建,第二章示范了如何使用书中所开发的Smart Framework。核心是第三章的框架实现,几个重要类的关系如下图所示:

"framework"

工具类中,核心的就是ClassUtil和ReflectionUtil了。
助手类中

  • HelperLoader负责加载几个助手类,可以视为入口

  • ClassHelper提供ClassSet,包括所有类的Set或者用特定注释的类的Set。例如:用Controller注释的类的集合ControllerSet

  • BeanHelper根据ClassSet中的每个Class,生成对应实例,提供BeanMap<Class<?>, Object>

  • IocHelper根据BeanMap得到每个Bean中用Inject注释的域类型及其object,再用Reflection动态设置该值(实现依赖注入)

  • ControllerHelper负责在静态块中根据ControllerSet得到Controller中的Action方法,解析request方法和路径,提供ActionMap<Request,Handler>,将请求映射到对应的控制器


ArrayList是Java集合框架里很常用的一个数据结构,底层用数组实现数据存储,所以基本上都是对数据的操作。
"ArrayList"

一些成员变量

1
2
3
4
5
6
7
8
// 默认数组容量
private static final int DEFAULT_CAPACITY = 10;

// 底层数组
transient Object[] elementData;

// ArrayList包含的元素数量
private int size;

其中,transient是Java的关键字。Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。

构造函数

ArrayList提供了三个构造函数

  • ArrayList(int initialCapacity):构造一个具有指定初始容量的空列表。
  • ArrayList():默认构造函数,默认为空,DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}。
  • ArrayList(Collection<? extends E> c):构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
    this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
    this.elementData = EMPTY_ELEMENTDATA;
    } else {
    throw new IllegalArgumentException("Illegal Capacity: "+
    initialCapacity);
    }
    }

    public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
    elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
    // replace with empty array.
    this.elementData = EMPTY_ELEMENTDATA;
    }
    }

在声明一个ArrayList时

  • ArrayList alist = new ArrayList() 用到了ArrayList的特性

  • List list = new ArrayList() 通用性更强,可直接替换为其他List接口的实现

    主要API

  • size(),元素数量,复杂度O(1)

    1
    2
    3
    public int size() {
    return size;
    }
  • isEmpty(),判断是否为空,复杂度O(1)

    1
    2
    3
    public boolean isEmpty() {
    return size == 0;
    }
  • contains(),判断是否包括特定元素,复杂度O(n)

    1
    2
    3
    public boolean contains(Object o) {
    return indexOf(o) >= 0;
    }

主要是indexOf(Object o)方法,实现了O(n)复杂度的线性查找,遍历数组,若找到则返回索引i,最坏情况遍历到最后才找到。因为ArrayList是顺序存储,如果知道存储顺序的话,可以选择用lastIndexOf(Object o)倒序遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i] == null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
  • get(int index),得到指定位置的元素,复杂度O(1)

    1
    2
    3
    4
    5
    public E get(int index) {
    rangeCheck(index); // 检查索引越界情况

    return elementData(index);
    }
  • set(int index, E element),替换指定位置的元素,复杂度O(1)

    1
    2
    3
    4
    5
    6
    7
    public E set(int index, E element) {
    rangeCheck(index); // 检查索引越界情况

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
    }
  • add(E e),在末尾添加元素,复杂度O(1)
    ensureCapacityInternal()方法判断是否需要扩容,grow()负责扩容操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public boolean add(E e) {
    ensureCapacityInternal(size + 1); // Increments modCount!!
    elementData[size++] = e;
    return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
    }

    private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 所需最小容量大于当前数组容量,扩容
    if (minCapacity - elementData.length > 0)
    grow(minCapacity);
    }

    private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // 扩容为原来的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 扩容后仍不够
    if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
    }

"grow"

  • add(int index, E element),在指定位置添加元素,复杂度O(n)
    需要将该位置后的所有元素向后移动一位,复杂度取决于数组规模

    1
    2
    3
    4
    5
    6
    7
    8
    public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1); // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
    size - index);
    size++;
    }
  • remove(int index) 删除并取得指定位置的元素,复杂度O(n)
    需要将该位置后的所有元素向前移动一位,复杂度取决于数组规模

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1; // 需要向前移动一位的元素长度
    if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
    numMoved);
    elementData[--size] = null; // 清除该位置的引用,让GC起作用

    return oldValue;
    }

关于Java GC这里需要特别说明一下,有了垃圾收集器并不意味着一定不会有内存泄漏。对象能否被GC的依据是是否还有引用指向它,上面代码中如果不手动赋 null 值,除非对应的位置被其他元素覆盖,否则原来的对象就一直不会被回收。

  • remove(Object o) 删除一个元素,复杂度O(n)
    需要遍历数组查找第一个满足条件的元素,然后把后面的元素向前前进一位

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public boolean remove(Object o) {
    if (o == null) {
    for (int index = 0; index < size; index++)
    if (elementData[index] == null) {
    fastRemove(index);
    return true;
    }
    } else {
    for (int index = 0; index < size; index++)
    if (o.equals(elementData[index])) {
    fastRemove(index);
    return true;
    }
    }
    return false;
    }
  • addAll()
    一次性添加多个元素,根据位置不同也有两个把本,一个是在末尾添加的 addAll(Collection<? extends E> c) 方法,一个是从指定位置开始插入的 addAll(int index, Collection<? extends E> c) 方法。跟 add() 方法类似,在插入之前也需要进行空间检查,如果需要则自动扩容;如果从指定位置插入,也会存在移动元素的情况。 addAll() 的时间复杂度不仅跟插入元素的多少有关,也跟插入的位置相关。

  • removeAll(Collection<?> c)
    删除指定collection中包含的所有元素,即求两个集合的差集,A-B。

  • retainAll(Collection<?> c)
    保留指定collection中包含的所有元素,即求两个集合的交集,A∩B。


Iterator

default修饰符是Java8中新的概念,在Java8发布时,需要Java在现有实现架构的下能往接口里增加新方法。引入Default方法,可以在优化接口的同时,避免跟现有实现架构的兼容问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package java.util;

import java.util.function.Consumer;

public interface Iterator<E> {

// 是否有下一元素
boolean hasNext();

// 返回下一元素
E next();

// Default方法是指,在接口内部包含了一些默认的方法实现
// 也就是接口中可以包含方法体,这打破了Java之前版本对接口的语法限制
// 从而使得接口在进行扩展的时候,不会破坏与接口相关的实现类代码。
default void remove() {
throw new UnsupportedOperationException("remove");
}

default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}

ListIterator

ListIterator继承自Iterator,除了父接口原有的方法hasNext()next()外,还申明了新的查询操作hasPrevious()previous()nextIndex()previousIndex(),以及修改操作remove()set()add()。相关操作见下图
"ListIterator"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package java.util;
public interface ListIterator<E> extends Iterator<E> {
// Query Operations

// 继承自父类
boolean hasNext();

// 继承自父类
E next();

// 是否有前一元素
boolean hasPrevious();

// 返回前一元素
E previous();

// 返回当前iterator的下标
int nextIndex();

// 返回当前iterator的前一下标,如果当前下标为0,前一下标为-1
int previousIndex();

// Modification Operations

// 删除元素
// remove当前元素必须先next(),否则remove的是前一元素
// 若iterator下标为0,直接remove抛出IllegalStateException异常
void remove();

// 替换元素
// 同remove,set当前元素前必须先next,否则set的是前一元素
// 若iterator下标为0,直接set抛出IllegalStateException异常
void set(E e);

// 插入元素,在该iterator的位置add
void add(E e);
}


从源码入手学习Java集合框架,选用的JDK版本是1.8.0_111。首先当然是理清java.util下的各个集合类与接口的关系。整理了一下,如下图所示,关系应该比较清晰了吧。
"Collection"
当然经历了几个JDK版本的变化,集合框架也越来越庞大,最基本的东西却是没怎么变。
核心是以collection为父类的接口以AbstractCollection为父类的抽象类,从这两部分入手,顺藤摸瓜,就可以理清整个框架的脉络,然后结合各个具体集合的实现来温习学习过的数据结构和算法。
比较奇怪的一点是Iterable接口是在java.lang中,而不是在java.util中。为什么Collection不直接继承Iterator呢?有这么一种说法:
JDK的作者之所以采用Iterator设计模式来设计,因为如果Collection直接从Iterator继承,那么Collection的实现类必须直接实现hasNext, next, remove方法。这么做有以下缺点:

  • 这么做会造成代码混乱,迭代代码与Collection本身实现代码混淆在一起,造成阅读困难,而且有方法重复,比如remove,不能做到迭代与本身实现分离。
  • 在Collection实现类中必须包含当前cursor指针,在并发时处理相当尴尬。
  • 访问接口不统一,Collection从Iterable继承的话,在迭代时只需拿到其Iterator(内部类实现),用统一的对象迭代,而且多个迭代器可以做到互不干扰。

从接口看,整体可以分为两大类:Collection和Map。因为Map表示的是关联式容器。接下来,将从比较独立的Iterator来进行分析。

引自《深入分析Java中的中文编码问题》


Java Web 涉及到的编码

对于使用中文来说,有 I/O 的地方就会涉及到编码,前面已经提到了 I/O 操作会引起编码,而大部分 I/O 引起的乱码都是网络 I/O,因为现在几乎所有的应用程序都涉及到网络操作,而数据经过网络传输都是以字节为单位的,所以所有的数据都必须能够被序列化为字节。在 Java 中数据被序列化必须继承 Serializable 接口。
原文中,作者提出这这样一个问题,我们是否认真考虑过一段文本它的实际大小应该怎么计算,他曾经碰到过一个问题:就是要想办法压缩 Cookie 大小,减少网络传输量,当时有选择不同的压缩算法,发现压缩后字符数是减少了,但是并没有减少字节数。所谓的压缩只是将多个单字节字符通过编码转变成一个多字节字符。减少的是 String.length(),而并没有减少最终的字节数。例如将“ab”两个字符通过某种编码转变成一个奇怪的字符,虽然字符数从两个变成一个,但是如果采用 UTF-8 编码这个奇怪的字符最后经过编码可能又会变成三个或更多的字节。同样的道理比如整型数字 1234567 如果当成字符来存储,采用 UTF-8 来编码占用 7 个 byte,采用 UTF-16 编码将会占用 14 个 byte,但是把它当成 int 型数字来存储只需要 4 个 byte 来存储。所以看一段文本的大小,看字符本身的长度是没有意义的,即使是一样的字符采用不同的编码最终存储的大小也会不同,所以从字符到字节一定要看编码类型。
另外一个问题,我们是否考虑过,当我们在电脑中某个文本编辑器里输入某个汉字时,它到底是怎么表示的?我们知道,计算机里所有的信息都是以 01 表示的,那么一个汉字,它到底是多少个 0 和 1 呢?我们能够看到的汉字都是以字符形式出现的,例如在 Java 中“淘宝”两个字符,它在计算机中的数值 10 进制是 28120 和 23453,16 进制是 6bd8 和 5d9d,也就是这两个字符是由这两个数字唯一表示的。Java 中一个 char 是 16 个 bit 相当于两个字节,所以两个汉字用 char 表示在内存中占用相当于四个字节的空间。
这两个问题搞清楚后,再来看一下 Java Web 中那些地方可能会存在编码转换?
用户从浏览器端发起一个 HTTP 请求,需要存在编码的地方是 URL、Cookie、Parameter。服务器端接受到 HTTP 请求后要解析 HTTP 协议,其中 URI、Cookie 和 POST 表单参数需要解码,服务器端可能还需要读取数据库中的数据,本地或网络中其它地方的文本文件,这些数据都可能存在编码问题,当 Servlet 处理完所有请求的数据后,需要将这些数据再编码通过 Socket 发送到用户请求的浏览器里,再经过浏览器解码成为文本。这些过程如下图所示:
"HTTP请求"
如上图所示一次 HTTP 请求设计到很多地方需要编解码,它们编解码的规则是什么?下面将会重点阐述一下:


URL的编解码

用户提交一个 URL,这个 URL 中可能存在中文,因此需要编码,如何对这个 URL 进行编码?根据什么规则来编码?有如何来解码?如下图一个 URL:
"URL"
上图中以 Tomcat 作为 Servlet Engine 为例,它们分别对应到下面这些配置文件中:
Port 对应在 Tomcat 的 中配置,而 Context Path 在 中配置,Servlet Path 在 Web 应用的 web.xml 中的

1
2
3
4
<servlet-mapping> 
<servlet-name>junshanExample</servlet-name>
<url-pattern>/servlets/servlet/*</url-pattern>
</servlet-mapping>

中配置,PathInfo 是我们请求的具体的 Servlet,QueryString 是要传递的参数,注意这里是在浏览器里直接输入 URL 所以是通过 Get 方法请求的,如果是 POST 方法请求的话,QueryString 将通过表单方式提交到服务器端,这个将在后面再介绍。
上图中 PathInfo 和 QueryString 出现了中文,当我们在浏览器中直接输入这个 URL 时,在浏览器端和服务端会如何编码和解析这个 URL 呢?为了验证浏览器是怎么编码 URL 的我们选择 FireFox 浏览器并通过 HTTPFox 插件观察我们请求的 URL 的实际的内容,以下是 URL:

HTTP://localhost:8080/examples/servlets/servlet/ 君山 ?author= 君山

在中文 FireFox3.6.12 的测试结果
"Test"
君山的编码结果分别是:e5 90 9b e5 b1 b1,be fd c9 bd,查阅上一届的编码可知,PathInfo 是 UTF-8 编码而 QueryString 是经过 GBK 编码,至于为什么会有“%”?查阅 URL 的编码规范 RFC3986 可知浏览器编码 URL 是将非 ASCII 字符按照某种编码格式编码成 16 进制数字然后将每个 16 进制表示的字节前加上“%”,所以最终的 URL 就成了上图的格式了。
默认情况下中文 IE 最终的编码结果也是一样的,不过 IE 浏览器可以修改 URL 的编码格式在选项 -> 高级 -> 国际里面的发送 UTF-8 URL 选项可以取消。
从上面测试结果可知浏览器对 PathInfo 和 QueryString 的编码是不一样的,不同浏览器对 PathInfo 也可能不一样,这就对服务器的解码造成很大的困难,下面我们以 Tomcat 为例看一下,Tomcat 接受到这个 URL 是如何解码的。
解析请求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,这个方法把传过来的 URL 的 byte[] 设置到 org.apache.coyote.Request 的相应的属性中。这里的 URL 仍然是 byte 格式,转成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
protected void convertURI(MessageBytes uri, Request request) 
throws Exception {
ByteChunk bc = uri.getByteChunk();
int length = bc.getLength();
CharChunk cc = uri.getCharChunk();
cc.allocate(length, -1);
String enc = connector.getURIEncoding();
if (enc != null) {
B2CConverter conv = request.getURIConverter();
try {
if (conv == null) {
conv = new B2CConverter(enc);
request.setURIConverter(conv);
}
} catch (IOException e) {...}
if (conv != null) {
try {
conv.convert(bc, cc, cc.getBuffer().length -
cc.getEnd());
uri.setChars(cc.getBuffer(), cc.getStart(),
cc.getLength());
return;
} catch (IOException e) {...}
}
}
// Default encoding: fast conversion
byte[] bbuf = bc.getBuffer();
char[] cbuf = cc.getBuffer();
int start = bc.getStart();
for (int i = 0; i < length; i++) {
cbuf[i] = (char) (bbuf[i + start] & 0xff);
}
uri.setChars(cbuf, 0, length);
}

从上面的代码中可以知道对 URL 的 URI 部分进行解码的字符集是在 connector 的 中定义的,如果没有定义,那么将以默认编码 ISO-8859-1 解析。所以如果有中文 URL 时最好把 URIEncoding 设置成 UTF-8 编码。
QueryString 又如何解析? GET 方式 HTTP 请求的 QueryString 与 POST 方式 HTTP 请求的表单参数都是作为 Parameters 保存,都是通过 request.getParameter 获取参数值。对它们的解码是在 request.getParameter 方法第一次被调用时进行的。request.getParameter 方法被调用时将会调用 org.apache.catalina.connector.Request 的 parseParameters 方法。这个方法将会对 GET 和 POST 方式传递的参数进行解码,但是它们的解码字符集有可能不一样。POST 表单的解码将在后面介绍,QueryString 的解码字符集是在哪定义的呢?它本身是通过 HTTP 的 Header 传到服务端的,并且也在 URL 中,是否和 URI 的解码字符集一样呢?从前面浏览器对 PathInfo 和 QueryString 的编码采取不同的编码格式不同可以猜测到解码字符集肯定也不会是一致的。的确是这样 QueryString 的解码字符集要么是 Header 中 ContentType 中定义的 Charset 要么就是默认的 ISO-8859-1,要使用 ContentType 中定义的编码就要设置 connector 的 中的 useBodyEncodingForURI 设置为 true。这个配置项的名字有点让人产生混淆,它并不是对整个 URI 都采用 BodyEncoding 进行解码而仅仅是对 QueryString 使用 BodyEncoding 解码,这一点还要特别注意。
从上面的 URL 编码和解码过程来看,比较复杂,而且编码和解码并不是我们在应用程序中能完全控制的,所以在我们的应用程序中应该尽量避免在 URL 中使用非 ASCII 字符,不然很可能会碰到乱码问题,当然在我们的服务器端最好设置 中的 URIEncoding 和 useBodyEncodingForURI 两个参数。


HTTP Header 的编解码

当客户端发起一个 HTTP 请求除了上面的 URL 外还可能会在 Header 中传递其它参数如 Cookie、redirectPath 等,这些用户设置的值很可能也会存在编码问题,Tomcat 对它们又是怎么解码的呢?
对 Header 中的项进行解码也是在调用 request.getHeader 是进行的,如果请求的 Header 项没有解码则调用 MessageBytes 的 toString 方法,这个方法将从 byte 到 char 的转化使用的默认编码也是 ISO-8859-1,而我们也不能设置 Header 的其它解码格式,所以如果你设置 Header 中有非 ASCII 字符解码肯定会有乱码。
我们在添加 Header 时也是同样的道理,不要在 Header 中传递非 ASCII 字符,如果一定要传递的话,我们可以先将这些字符用 org.apache.catalina.util.URLEncoder 编码然后再添加到 Header 中,这样在浏览器到服务器的传递过程中就不会丢失信息了,如果我们要访问这些项时再按照相应的字符集解码就好了。


POST 表单的编解码

在前面提到了 POST 表单提交的参数的解码是在第一次调用 request.getParameter 发生的,POST 表单参数传递方式与 QueryString 不同,它是通过 HTTP 的 BODY 传递到服务端的。当我们在页面上点击 submit 按钮时浏览器首先将根据 ContentType 的 Charset 编码格式对表单填的参数进行编码然后提交到服务器端,在服务器端同样也是用 ContentType 中字符集进行解码。所以通过 POST 表单提交的参数一般不会出现问题,而且这个字符集编码是我们自己设置的,可以通过 request.setCharacterEncoding(charset) 来设置。
另外针对 multipart/form-data 类型的参数,也就是上传的文件编码同样也是使用 ContentType 定义的字符集编码,值得注意的地方是上传文件是用字节流的方式传输到服务器的本地临时目录,这个过程并没有涉及到字符编码,而真正编码是在将文件内容添加到 parameters 中,如果用这个编码不能编码时将会用默认编码 ISO-8859-1 来编码。


HTTP BODY 的编解码

当用户请求的资源已经成功获取后,这些内容将通过 Response 返回给客户端浏览器,这个过程先要经过编码再到浏览器进行解码。这个过程的编解码字符集可以通过 response.setCharacterEncoding 来设置,它将会覆盖 request.getCharacterEncoding 的值,并且通过 Header 的 Content-Type 返回客户端,浏览器接受到返回的 socket 流时将通过 Content-Type 的 charset 来解码,如果返回的 HTTP Header 中 Content-Type 没有设置 charset,那么浏览器将根据 Html 的 中的 charset 来解码。如果也没有定义的话,那么浏览器将使用默认的编码来解码。


其它需要编码的地方

除了 URL 和参数编码问题外,在服务端还有很多地方可能存在编码,如可能需要读取 xml、velocity 模版引擎、JSP 或者从数据库读取数据等。
xml 文件可以通过设置头来制定编码格式

1
<?xml version="1.0" encoding="UTF-8"?>

Velocity 模版设置编码格式:

1
services.VelocityService.input.encoding=UTF-8

JSP 设置编码格式:

1
<%@page contentType="text/html; charset=UTF-8"%>

访问数据库都是通过客户端 JDBC 驱动来完成,用 JDBC 来存取数据要和数据的内置编码保持一致,可以通过设置 JDBC URL 来制定如 MySQL:

1
url="jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK"

常见问题分析

在了解了 Java Web 中可能需要编码的地方后,下面看一下,当我们碰到一些乱码时,应该怎么处理这些问题?出现乱码问题唯一的原因都是在 char 到 byte 或 byte 到 char 转换中编码和解码的字符集不一致导致的,由于往往一次操作涉及到多次编解码,所以出现乱码时很难查找到底是哪个环节出现了问题,下面就几种常见的现象进行分析。

  • 中文变成了看不懂的字符
      例如,字符串“淘!我喜欢!”变成了“Ì Ô £ ¡Î Ò Ï²»¶ £ ¡”编码过程如下图所示:
    "1"
      字符串在解码时所用的字符集与编码字符集不一致导致汉字变成了看不懂的乱码,而且是一个汉字字符变成两个乱码字符。
  • 一个汉字变成一个问号
      例如,字符串“淘!我喜欢!”变成了“??????”编码过程如下图所示
    "2"
      将中文和中文符号经过不支持中文的 ISO-8859-1 编码后,所有字符变成了“?”,这是因为用 ISO-8859-1 进行编解码时遇到不在码值范围内的字符时统一用 3f 表示,这也就是通常所说的“黑洞”,所有 ISO-8859-1 不认识的字符都变成了“?”。
  • 一个汉字变成两个问号
      例如,字符串“淘!我喜欢!”变成了“????????????”编码过程如下图所示
    "3"
      这种情况比较复杂,中文经过多次编码,但是其中有一次编码或者解码不对仍然会出现中文字符变成“?”现象,出现这种情况要仔细查看中间的编码环节,找出出现编码错误的地方。
  • 一种不正常的正确编码
      还有一种情况是在我们通过 request.getParameter 获取参数值时,当我们直接调用String value = request.getParameter(name);
      会出现乱码,但是如果用下面的方式String value = String(request.getParameter(name).getBytes("ISO-8859-1"), "GBK");
      解析时取得的 value 会是正确的汉字字符,这种情况是怎么造成的呢?看下如所示:
    "4"
      这种情况是这样的,ISO-8859-1 字符集的编码范围是 0000-00FF,正好和一个字节的编码范围相对应。这种特性保证了使用 ISO-8859-1 进行编码和解码可以保持编码数值“不变”。虽然中文字符在经过网络传输时,被错误地“拆”成了两个欧洲字符,但由于输出时也是用 ISO-8859-1,结果被“拆”开的中文字的两半又被合并在一起,从而又刚好组成了一个正确的汉字。虽然最终能取得正确的汉字,但是还是不建议用这种不正常的方式取得参数值,因为这中间增加了一次额外的编码与解码,这种情况出现乱码时因为 Tomcat 的配置文件中 useBodyEncodingForURI 配置项没有设置为”true”,从而造成第一次解析式用 ISO-8859-1 来解析才造成乱码的。

总结

  本文首先总结了几种常见编码格式的区别,然后介绍了支持中文的几种编码格式,并比较了它们的使用场景。接着介绍了 Java 那些地方会涉及到编码问题,已经 Java 中如何对编码的支持。并以网络 I/O 为例重点介绍了 HTTP 请求中的存在编码的地方,以及 Tomcat 对 HTTP 协议的解析,最后分析了我们平常遇到的乱码问题出现的原因。
  综上所述,要解决中文问题,首先要搞清楚哪些地方会引起字符到字节的编码以及字节到字符的解码,最常见的地方就是读取会存储数据到磁盘,或者数据要经过网络传输。然后针对这些地方搞清楚操作这些数据的框架的或系统是如何控制编码的,正确设置编码格式,避免使用软件默认的或者是操作系统平台默认的编码格式。

参考资料

《Unicode 编码规范》,详细描述了 Unicode 如何编码。
《ISO-8859-1 编码》,详细介绍了 ISO-8859-1 的一些细节。
《RFC3986 规范》,详细描述了 URL 编码规范。
《HTTP 协议》,W3C 关于 HTTP 协议的详细描述。
《Tomcat 系统架构与设计模式》,了解 Tomcat 中容器的体系结构,基本的工作原理,以及 Tomcat 中使用的经典的设计模式介绍。
《Servlet 工作原理解析》,以 Tomcat 为例了解 Servlet 容器是如何工作的。
developerWorks Java 技术专区