0%

<< Java高级软件工程师知识结构

  1. 掌握Collection和Map的继承体系。
  2. 掌握ArrayList、LinkedList、Vector、Stack、PriorityQueue、HashSet、LinkedHashSet、TreeSet、HashMap、
  3. LinkedHashMap、TreeMap、WeakHashMap、EnumMap、HashTable的特点和实现原理。
    4.掌握CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap的实现原理和适用场景。

Java Collection与Map的继承关系

在程序设计中, 集合可以存储和传递一组数据. 集合虽然比不上数组的查询速度, 但是有更加方便的功能,
如可变长度、键值对、去重复等.
其家族成员有:

Collection是一个接口, 该接口允许添加和查找一个或多个元素、生成迭代器等功能.

List SetQueue分别是继承了Collocation的子接口.
List用于存放可重复可为null的元素的有序集合. 并且可以对元素进行精确地控制, 可根据整数索引访问元素.
Set用于存放不可重复可为null的元素的集合,
Map并没有继承Collection, 是由一系列键值对组成的集合. 在Map中一个key对应一个value, key不能相同.

List接口

实现了List接口的集合主要有 ArrayList LinkedList Vector Stack

ArrayList

特性

  1. 可重复, 可为null: 添加元素是将元素存放到数组中
  2. 有序
  3. 擅长随机访问: 快速检索 增删慢
  4. 非线程同步
    通过Collections.synchronizedList(new ArrayList());转换为线程安全的List

实现原理
动态数组, 底层也是通过单个Java数组实现的, ArrayList根据元素个数动态调整内部数组的长度以达到实现动态数组的效果.
内部数组的初始长度为10, 当添加的元素的个数超出了内部数组的长度时, 调用JNI函数对内部数组实现扩容(为原长度的150%)和复制.
本质上, ArrayList是采用了线性表的结构, 因此, ArrayList具有快速检索的优点也具有增删慢的缺点.

复杂度
添加n个元素需要O(n)时间

优化建议 :
如果确定了插入元素的多少, 最好可以指定初始容量值,
避免过多的进行扩容和复制而浪费时间.

LinkedList

特性

  1. 不能随机访问
  2. 非线程同步
    通过Collections.synchronizedList(new LinkedList());转换为线程安全的List
  3. 善于插入和删除 不善于随机访问

实现原理
双向链表, 可以通过get remove insert方法操作首部和尾部的元素.

复杂度

与ArrayList对比
由于ArrayList是线性表的形式存储的, 需要连续的存储空间. 而LinkedList不需要连续,
因此在存储数据量较大的情况下, 优先选择LinkedList.

Vector

特性

  1. 线程同步
  2. 与ArrayList一样 ???

实现原理
线程安全的动态数组, 内部也是采用单个数组

复杂度

Stack 栈

特性

  1. 后进先出的栈
  2. 提供了除了ArrayList和Vector以外的栈操作方法:
    • push 压入栈
    • pop 出栈
    • peek 得到栈顶
    • empty 测试栈是否为空
    • search 检测一个元素在栈中的位置

实现原理
Vector构建,而非继承自Vector

复杂度

【样例】: 使用栈实现计算器

Set接口

特性

  1. 不可重复
  2. 最多只允许一个null

EnumSet

特性

  1. 枚举专用Set
  2. 不是同步的
    多线程情况下, 最好在创建时完成这一操作, 以防止意外的非同步访问
    Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));
  3. 枚举 set 中所有键都必须来自单个枚举类型, 该枚举类型在创建 set 时显式或隐式地指定.

实现原理

//TODO

复杂度

HashSet

特性

  1. 速度最快的集合
  2. 不能重复,最多一个为null

实现原理
内部存在一个HashMap, 借助于HashCode来实现, 所以不保证元素的顺序

复杂度

LinkedHashSet

特性

  1. 有序

实现原理
内部是LinkedHashMap实现的
LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它 同时使用链表维护元素的次序
当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。

复杂度

TreeSet

特性

  1. 总是处于排序状态的Set(顺序取决于元素的自然顺序或者创建Set时指定的Comparator)
  2. 非线程同步
    多线程情况下,最好在创建时进行, 以防止对 set 的意外非同步访问:
    SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

实现原理
内部由TreeMap(使用红黑树)来实现

复杂度

Map接口

特性

  1. 键值对
  2. Key不能重复

HashMap

特性

  1. 线程不安全
  2. 初始容量设定

实现原理

参考链接: HashMap的工作原理

以哈希表的数据结构实现. 内部存在一个哈希表数组, 每个数组元素又有一组长度不确定的链表.

HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。

HashMap在bucket中存储Map.Entry对象,每个Map.Entry保存有key和value。

get的工作原理

当使用get(key)方法,
首先调用hashing方法,利用key.hashcode计算key所在的bucket,找到相应的bucket后。
然后遍历bucket中的Map.Entry,首先比对key.hashcode值,其次比对(key值或key.equals方法比对两个对象)

1
2
if (e.hash == hash &&  ((k = e.key) == key || (key != null && key.equals(k))))
return e;

在这里,使用了 && 的短路特性: 只要第一个条件不满足,不再比较后面的条件;只有前面的条件满足了,才比较后面的条件。
等价于

1
2
3
4
5
if(e.hash==hash){
if((k = e.key) == key || (key != null && key.equals(k)){
return e;
}
}

key的hashcode相同的情况

因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。当hashcode相同时,还会调用key.equals比对两个key对象是否相同。

负载因子 0.75

“如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?”
默认的负载因子大小为 0.75,也就是说,当一个map填满了 75% 的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。
这个过程叫作 rehashing,因为它调用hash方法找到新的bucket位置。

rehashing 过程

重新调整HashMap大小存在的问题:
当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候, HashMap 并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。

key类型的选择与提速

  • hashing的概念

  • HashMap 中解决碰撞的方法

  • equals()和hashCode()的应用,以及它们在HashMap中的重要性

  • 不可变对象的好处

    使用不可变的、声明作final的对象,并且采用合适的equals()hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用 String,Interger 这样的wrapper类作为键是非常好的选择。而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。

    不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。

    因为获取对象的时候要用到equals()hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

复杂度

LinkedHashMap

特性

  1. 非线程同步
  2. 有序: 可以按照访问顺序或者插入顺序排序

实现原理
底层使用哈希表与双向链表来保存所有元素。其基本操作与父类 HashMap 相似

LinkedHashMap 定义了排序模式 accessOrder,该属性为 boolean 型变量,对于访问顺序,为 true;
对于插入顺序,则为 false。一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。

//TODO 排序模式

TreeMap

特性

  1. 可排序
  2. 不是同步的
    SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

实现原理
红黑树的数据结构, 实现了SortedMap接口

复杂度

应用
TreeMap 常用于在接口参数拼接中,以自动对key排序

WeakHashMap

特性

  1. 当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值

实现原理
使用弱引用作为内部数据的存储方案。 WeakHashMap可以作为简单缓存表的解决方案,
当系统内存不够的时候,垃圾收集器会自动的清除没有在其他任何地方被引用的键值对。

EnumMap

特性

  1. Key必须是Enum
  2. EnumMap的key不允许为null,value可以为null,按照key在enum中的顺序进行保存,非线程安全。

实现原理

HashTable

特性

  1. 线程安全
  2. 性能比HashMap差

实现原理
哈希表
使用 synchronized 锁住所有的读写操作

复杂度

Queue接口

// FIXME 实现原理

队列, 它主要分为两大类:

  • 一类是阻塞式队列, 队列满了以后再插入元素则会抛出异常, 主要包括
    • ArrayBlockQueue
    • PriorityBlockingQueue
    • LinkedBlockingQueue
  • 另一类是双端队列, 支持在头、尾两端插入和移除元素, 主要包括:
    • ArrayDeque
    • LinkedBlockingDeque
    • LinkedList

常见的队列有:

  1. ArrayDeque, (数组双端队列)
  2. PriorityQueue, (优先级队列)
  3. ConcurrentLinkedQueue, (基于链表的并发队列)
  4. DelayQueue, (延期阻塞队列)(阻塞队列实现了BlockingQueue接口)
  5. ArrayBlockingQueue, (基于数组的并发阻塞队列)
  6. LinkedBlockingQueue, (基于链表的FIFO阻塞队列)
  7. LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)
  8. PriorityBlockingQueue, (带优先级的无界阻塞队列)
  9. SynchronousQueue (并发同步阻塞队列)

PriorityQueue

无界优先级队列
特性

  1. 有序:
    • 顺序取决于元素的自然顺序或者创建队列时指定的Comparator
    • 依靠自然顺序的优先级队列还不允许插入不可比较的对象(这样做可能导致 ClassCastException)。
  2. 不允许元素为null
  3. 优先级队列是无界的
    有一个内部容量,控制着用于存储队列元素的数组大小。它通常至少等于队列的大小。
    随着不断向优先级队列添加元素,其容量会自动增加。无需指定容量增加策略的细节。
  4. 非线程安全

PriorityBlockingQueue

无界优先级阻塞队列
特性

  1. 有序: 与PriorityQueue相同
  2. 不允许元素为null
  3. 无界: 资源耗尽时执行add会失败(导致 OutOfMemoryError)
  4. 线程安全

几种特殊的

CopyOnWriteArrayList

ArrayList的一个线程安全的变体, 所有可变操作(addset 等等)都是通过对底层数组进行一次新的复制作为新的内部数组来实现的.

  • 这一般需要很大的开销, 但是当遍历操作的数量大大超过可变操作的数量时, 这种方法可能比其他替代方法更有效.
  • 在不能或不想进行同步遍历, 但又需要从并发线程中排除冲突时, 它也很有用.

“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用. 此数组在迭代器的生存期内不会更改, 因此不可能发生冲突, 并且迭代器保证不会抛出ConcurrentModificationException. 创建迭代器以后, 迭代器就不会反映列表的添加、移除或者更改. 在迭代器上进行的元素更改操作(remove、set 和 add)不受支持. 这些方法将抛出 UnsupportedOperationException.

允许使用所有元素, 包括 null.

内存一致性效果:

当存在其他并发 collection 时, 将对象放入 CopyOnWriteArrayList 之前的线程中的操作 happen-before
随后通过另一线程从 CopyOnWriteArrayList 中访问或移除该元素的操作.

这个类和ArrayList最大的区别就是add(E) 的时候。容器会自动copy一份出来然后再尾部add(E)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

CopyOnWriteArraySet

对其所有操作使用内部 CopyOnWriteArrayList 的 Set. 因此, 它共享以下相同的基本属性:

它最适合于具有以下特征的应用程序:

  • set 大小通常保持很小, 只读操作远多于可变操作, 需要在遍历期间防止线程间的冲突.
  • 它是线程安全的.
  • 因为通常需要复制整个基础数组, 所以可变操作(add、set 和 remove 等等)的开销很大.
  • 迭代器不支持可变 remove 操作.
  • 使用迭代器进行遍历的速度很快, 并且不会与其他线程发生冲突. 在构造迭代器时, 迭代器依赖于不变的数组快照.

示例用法

以下代码使用一个写时复制(copy-on-write)的 set, 以维护在状态更新时执行某项操作的一组 Handler 对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Handler { void handle(); ... }
class X {
private final CopyOnWriteArraySet<Handler> handlers = new CopyOnWriteArraySet<Handler>();
public void addHandler(Handler h) {
handlers.add(h);
}
private long internalState;
private synchronized void changeState() {
internalState = ...;
}
public void update() {
changeState();
for (Handler handler : handlers)
handler.handle();
}
}

ConcurrentHashMap

//FIXME 重写介绍

实现原理

  1. 结构

c

  1. 详情请参考: 探索 ConcurrentHashMap 高并发性的实现机制本地版

线程安全

Collections类中的静态方法

在 Collections类中有多个静态方法,它们可以获取通过同步方法封装非同步集合而得到的集合:

1
2
3
4
5
6
public static Collection synchronizedCollention(Collection c)
public static List synchronizedList(list l)
public static Map synchronizedMap(Map m)
public static Set synchronizedSet(Set s)
public static SortedMap synchronizedSortedMap(SortedMap sm)
public static SortedSet synchronizedSortedSet(SortedSet ss)

这些方法基本上返回具有同步集合方法版本的新类。比如,为了创建多线程安全且由ArrayList支持的List,可以使用如下代码:

1
List list = Collection.synchronizedList(new ArrayList());

注意,ArrayList实例马上封装起来,不存在对未同步化ArrayList的直接引用(即直接封装匿名实例)。这是一种最安全的途径。如果另一个线程要直接引用ArrayList实例,它可以执行非同步修改。

//FIXME 实现原理

CopyOnWrite机制

synchronized机制

ReteenLock机制

ConcurrentHashMap是个特例

异同点

Vector 和 ArrayList

  1. Vector是线程同步的, 所以它也是线程安全的, 而ArrayList是线程异步的, 是不安全的. 如果不考虑到线程的安全因素, 一般用ArrayList效率比较高.
  2. 如果集合中的元素的数目大于目前集合数组的长度时, Vector增长率为目前数组长度的100%, 而ArrayList增长率为目前数组长度的50%. 如果在集合中使用数据量比较大的数据, 用Vector有一定的优势.
  3. 如果查找一个指定位置的数据, Vector和ArrayList使用的时间是相同的, 都是O(1),这个时候使用Vector和ArrayList都可以; 而如果移动一个指定位置的数据花费的时间为O(n-i) n为总长度, 这个时候就应该考虑到使用LinkedList, 因为它移动一个指定位置的数据所花费的时间为O(1), 而查询一个指定位置的数据时花费的时间为O(i).

Arraylist和LinkedList

  1. ArrayList是实现了基于动态数组的数据结构, LinkedList基于链表的数据结构.
  2. 对于随机访问getset, ArrayList优于LinkedList, 因为LinkedList要移动指针.
  3. 对于新增和删除操作addremove, LinkedList比较占优势, 因为ArrayList要移动数据.
    这一点要看实际情况的. 若只对单条数据插入或删除, ArrayList的速度反而优于LinkedList. 但若是批量随机的插入删除数据, LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据, 要移动插入点及之后的所有数据.

HashMap 与 TreeMap

  1. HashMap通过hashcode对其内容进行快速查找, 而TreeMap中所有的元素都保持着某种固定的顺序,
    如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的).
  2. 在Map 中插入、删除和定位元素, HashMap 是最好的选择. 但如果您要按自然顺序或自定义顺序遍历键, 那么TreeMap会更好.
    使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现. 这个TreeMap没有调优选项, 因为该树总处于平衡状态.

HashMap与HashTable的区别

  1. 继承不同
    public class Hashtable extends Dictionary implements Map
    public class HashMap extends AbstractMap implements Map
  2. Hashtable 中的方法是同步的, 而HashMap中的方法在缺省情况下是非同步的. 在多线程并发的环境下, 可以直接使用Hashtable, 但是要使用HashMap的话就要自己增加同步处理了.
  3. Hashtable中, key和value都不允许出现null值, 在HashMap中, null可以作为键, 这样的键只有一个; 可以有一个或多个键所对应的值为null. 当get()方法返回null值时, 即可以表示 HashMap中没有该键,也可以表示该键所对应的值为null. 因此, 在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断.
  4. 两个遍历方式的内部实现上不同.
    Hashtable、HashMap都使用了 Iterator. 而由于历史原因, Hashtable还使用了Enumeration的方式 .
  5. 哈希值的使用不同, HashTable直接使用对象的hashCode. 而HashMap重新计算hash值.
  6. Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式. HashTable中hash数组默认大小是11, 增加的方式是 old*2+1. HashMap中hash数组的默认大小是16, 而且一定是2的指数

对集合的选择

对List的选择

  1. 对于随机查询与迭代遍历操作, 数组比所有的容器都要快. 所以在随机访问中一般使用ArrayList
  2. LinkedList使用双向链表对元素的增加和删除提供了非常好的支持, 而ArrayList执行增加和删除元素需要进行元素位移.
  3. 对于Vector而已, 我们一般都是避免使用.
  4. 将ArrayList当做首选, 毕竟对于集合元素而已我们都是进行遍历, 只有当程序的性能因为List的频繁插入和删除而降低时, 再考虑LinkedList.

对Set的选择

  1. HashSet由于使用HashCode实现, 所以在某种程度上来说它的性能永远比TreeSet要好, 尤其是进行增加和查找操作.
  2. 虽然TreeSet没有HashSet性能好, 但是由于它可以维持元素的排序, 所以它还是存在用武之地的.

对Map的选择

  1. HashMap与HashSet同样, 支持快速查询. 虽然HashTable的速度也不慢, 但是在HashMap面前还是稍微慢了些, 所以HashMap在查询方面可以取代HashTable.
  2. 由于TreeMap需要维持内部元素的顺序, 所以它通常要比HashMap和HashTable慢.

用法

数组

数组复制System.arrayCopy

该方法是个JNI函数, 是在JVM中实现的

1
2
3
4
5
6
7
8
9
/**
*从src的srcPos位置复制数据到dest的destPos位置, 长度为length
*src - 源数组.
*srcPos - 源数组中的起始位置.
*dest - 目标数组.
*destPos - 目标数据中的起始位置.
*length - 要复制的数组元素的数量.
*/
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

Arrays.copyOf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 由U类型复制为T类型?
* original - 要复制的数组
* newLength - 要返回的副本的长度
* newType - 要返回的副本的类型
*/
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
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;
}
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}

Arrays.asList: 将数组转换为ArrayList

1
2
3
public static <T> List<T> asList(T... a) {
return new ArrayList<T>(a);
}

Arrays.asList返回的ArrayList并不是java.util.ArrayList, 只是Arrays的内部类. 该类只提供了一些基本的操作,

  1. size:元素数量
  2. toArray:转换为数组, 实现了数组的浅拷贝.
  3. get:获得指定元素.
  4. contains:是否包含某元素.
    asList返回的是一个长度不可变的列表. 数组是多长, 转换成的列表是多长, 我们是无法通过add、remove来增加或者减少其长度的
    我们经常需要使用到Arrays这个工具的asList()方法将其转换成列表. 方便是方便, 但是有时候会出现莫名其妙的问题. 如下:
1
2
3
4
5
public static void main(String[] args) {
int[] datas = new int[]{1,2,3,4,5};
List list = Arrays.asList(datas);
System.out.println(list.size());
}

输出结果:

1
1

结果是1, 为什么会是1而不是5呢?先注意这个参数: T…a, 这个参数是一个泛型的变长参数, 我们知道 基本数据类型是不可能泛型化的 ,也就是说8个基本数据类型是不可作为泛型参数的, 但是为什么编译器没有报错呢?这是因为数组会当做一个对象来处理, 它是可以泛型的, 所以我们的程序是把一个int型的数组作为了T的类型,所以在转换之后List中就只会存在一个类型为int数组的元素了.
所以我们这样的程序System.out.println(datas.equals(list.get(0)));输出结果肯定是true.
当然如果将int改为Integer, 则长度就会变成5了.

Arrays.fill

使用值填充数组

1
2
3
int[] a=new int[10];
Arrays.fill(a,0);
// 使用0填充数组a

遍历

Map的遍历

Map的遍历,都是需要转换为Collection

1
Map<String, String> map = ...;
  1. 由Map生成Collection, 获取所有的值

    1
    2
    3
    4
    5
    Collection<String> values = map.values();
    Iterator<String> iterator = values.iterator();
    while (iterator.hasNext()) {
    System.out.println(iterator.next());
    }
  2. 由Map.keySet, 遍历key值

    1
    2
    Set<String> keySet = map.keySet();
    Iterator<String> keyIterator = keySet.iterator();
  3. 获取Map.Entry类型的Set

    1
    2
    3
    4
    5
    6
    7
    8
    Set<Map.Entry<String, String>> entrySet = map.entrySet();
    Iterator<Map.Entry<String, String>> entryIterator = entrySet.iterator();
    while (entryIterator.hasNext()) {
    Map.Entry<String, String> entry = entryIterator.next();
    String key = entry.getKey();
    String value = entry.getValue();
    System.out.println(key + "\t" + value);
    }

Collection的遍历方法

  1. Iterator 迭代子

    1
    2
    3
    4
    5
    Collection<String> values2 = map.values();
    Iterator<String> iterator2 = values2.iterator();
    while (iterator2.hasNext()) {
    System.out.println(iterator.next());
    }
  2. foreach

    1
    2
    3
    for (String valueItem : values2) {
    System.out.println(valueItem);
    }
  3. List特有的遍历方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    List<String> list = null;
    assert list != null;
    for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
    }
    // 当然也可以写成
    for (String aList : list) {
    System.out.println(aList);
    }

List遍历方式有三种:

- 下标遍历
- Iterator遍历
- Foreach遍历(最快)

排序

FIXME 排序:集合自带排序 对集合排序

其他

Java中有多少种数据结构, 分别是什么?

- List:是列表, 有下标值, 存储元素可以重复, 遍历元素是有序的.
- Set:是散列集, 无下标值, 存储元素不可重复, 遍历元素时无序的.
- Map:是以键值对存储, 一个key一个value, key不可以重复, value可以重复.
- 数组:指定类型, 固定长度, 元素存储地址是连续的.
- 树:元素以树形结构存储, 只有一个根节点.
- 栈:元素是先进后出, 后进先出.
- 向量:动态数组, 可以存储任何类型元素, 动态长度, 元素存储地址是连续的.
- 队列:元素存储是排列有序的, 一定保证先进的先出, 后进的后出.

修改记录:

  1. HashMap的详细实现原理 重写ConcurrentHashMap介绍 2016-08-20

参考文献:

  1. java提高篇(二十)集合大家族
  2. Java集合类详解
  3. 探索 ConcurrentHashMap 高并发性的实现机制

HashMap

JDK8和JDK7不一样,JDK7中没有红黑树,数组中只挂载链表.
而JDK8中在桶容量大于等于64链表节点数大于等于8的时候转换为红黑树. 当红黑树节点数量小于6时又会转换为链表.

桶容量就是Map元素的总个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 初始化容量,必须要2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 负载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 需要从链表转换为红黑树时,链表节点的最小长度
static final int TREEIFY_THRESHOLD = 8;

// 转换为红黑树时数组的最小容量: 最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;

// resize操作时,红黑树节点个数小于6则转换为链表.
static final int UNTREEIFY_THRESHOLD = 6;

// HashMap阈值,用于判断是否需要扩容(threshold = 容量*loadFactor)
int threshold;

// 负载因子
final float loadFactor;

HashMap解决hash冲突的方法

Hash冲突

由于用于计算的数据是无限的H(key),key属于(-∞,+∞),而映射到区间是有限的,所以肯定会存在两个key:key1,key2,H(key1)=H(key2),这就是hash冲突.

一般的解决Hash冲突方法有: 开放定址法、再哈希法、链地址法(拉链法)、建立公共溢出区.

  • 开放定址法: 再次hash,直到不冲突
  • 换hash算法
  • 将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行. 链表法适用于经常进行插入和删除的情况. HashMap采用的就是链地址法来解决hash冲突. (链表长度大于等于8时转为红黑树)
  • 冲突的元素放入溢出表

Java7的死循环问题

在 JDK 1.8 之前,rehash 的过程中采用头插法转移结点,高并发下,多个线程同时操作一条链表将直接导致闭链,死循环并占满 CPU。

JDK 1.8 以来,对 HashMap 的内部进行了很大的改进,采用数组+链表+红黑树来进行数据的存储。
rehash 的过程也进行了改动,基于复制的算法思想,不直接操作原链,而是定义了两条链表分别完成对原链的结点分离操作,
即使是多线程的情况下也是安全的。

Java8 中的HashMap扩容

而newTab[j + oldCap] = hiHead;这一步,是一个非常巧妙的地方,也是本文分析的重点.

优化点1: 扩容,避免重复计算hash值,还随机

解释

经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,经过rehash之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置.

对应的就是下方的resize的注释.

1
2
3
4
5
6
7
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
* /

看下图可以明白这句话的意思,n为table的长度,
图(a)表示扩容前的key1和key2两种key确定索引位置的示例,
图(b)表示扩容后key1和key2两种key确定索引位置的示例,
其中hash1是key1对应的哈希值(也就是根据key1算出来的hashcode值)与高位与运算的结果.

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,
只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”.

这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的
因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了.

这一块就是JDK1.8新增的优化点.

优化点2: Rehash链表元素不倒置

有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置.

再解释:为什么刚好原位置+原数组长度就会等于新的数组中的位置呢?
要搞明白这个问题首先要清楚

HashMap的数组长度恒定为2的n次方,也就是说只会为2 4 8 16 . . . . . 这种数. 源码中有限制,也就是说即使你创建HashMap的时候是写的

1
Map<String,String> hashMap = new HashMap<>(13);

最后数组长度也会变成16,而不是你的13. 会取与你传入的数最近的一个2的n次方的数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}


static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

那么明确这一点有什么用呢?我们知道2,4,8,16,32所对应的二进制分别为

1
2
3
4
5
2:  0000 0000 0000 0000 0000 0000 0000 0010
4: 0000 0000 0000 0000 0000 0000 0000 0100
8: 0000 0000 0000 0000 0000 0000 0000 1000
16: 0000 0000 0000 0000 0000 0000 0001 0000
32: 0000 0000 0000 0000 0000 0000 0010 0000

而我们知道,0在做位与运算时与任何一个数运算结果都恒为0

1
2
0 & 1 = 0
0 & 0 = 0

故看源码中

1
if ((e.hash & oldCap) == 0)

这一步是否为0只需要看元素的二进制数对应数组长度的二进制数1那个位置是否为0.
假设某个元素的hashcode为52:

而假设某个元素的hashcode为100:

而通过源码可以看出0就还是在原来的位置. 不为0就需要变动位置了,新的位置为元素在原数组的位置+原数组的长度,那么为什么是这样呢?我们接着看
看之前我们先使用JDK1.7中的方式重新进行hash运算
HashMap在运算元素位置的时候使用为 数组长度-1. 也就是15.31这种数15 31 对应的二进制为

1
2
15:0000 0000 0000 0000 0000 0000 0000 1111
31: 0000 0000 0000 0000 0000 0000 0001 1111

这里需要注意的是hashmap中,计算元素位置采用的是length-1,而leng是用来判断元素是否需要更换位置

1
if ((e.hash & oldCap) == 0)  //仅仅是判断元素是否需要换位置

这一步才是计算位置,使用的是length-1.

16扩容后变成32.那么1.7中计算元素的位置方式为 31&52, 31&100.我们把他与扩容前的15&52. 15&100做对比看看

可以看到,由于每次扩容会把原数组的长度*2,那么再二进制上的表现就是多出来一个1,比如元数组16-1二进制为1111,那么扩容后的32-1的二进制就变成了1 1111
而扩容前和扩容后的位置是否一样完全取决于多出来的那一位与key值的hash做按位与运算之后的值值是为0还是1. 为0则新位置与原位置相同,不需要换位置,不为零则需要换位置.

而为什么新的位置 = 原位置 + 原数组长度,是因为每次换的位置只是前面多了一个1而已. 那么新位置的变化的高位进1位. 而每一次高位进1都是在加上原数组长度的过程.


正好1+2=3 3+4=7 7+8=15 . 也就验证了新的位置为原位置+原数组长度.

[参考文献]
————————————————

  1. JDK8之HashMap resize方法详解(深入讲解为什么1.8中扩容后的元素新位置为原位置+原数组长度)
  2. JAVA8对HashMap扩容机制的优化
  3. HashMap中的Hash冲突解决和扩容机制

JVM内存区域划分
    1. 掌握程序计数器、堆、虚拟机栈、本地方法栈、方法区(JAVA8已移除)、元空间(JAVA8新增)的作用及基本原理.
    2. 掌握堆的划分:  新生代(Eden、Survivor1、Survivor2)和老年代的作用及工作原理.
    3. 掌握JVM内存参数设置及调优.
类加载
    1. 掌握类的加载阶段:  加载、链接(验证、准备、解析)、初始化、使用、卸载.
    2. 掌握类加载器分类及其应用:  启动类加载器、扩展类加载器、应用程序类加载器、自定义加载器.

运行时内存结构

Class文件格式

Java字节码和执行引擎

Java内存模型

监控调优

GC算法和垃圾回收

类加载

线程和锁

JVM内存区域划分

java虚拟机运行时数据区
java虚拟机运行时数据区与线程的关系

JVM内存区域可以划分为:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区(Java8已移除)
  • 元空间(Java8新增)

程序计数器

  1. 程序计数器可以看做是当前线程所执行的字节码的行号指示器. 在JVM的概念模型里, 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令.

  2. 由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的, 为了在线程切换后能恢复到正确的执行位置, 每条线程都需要有一个独立的程序计数器, 独立存储, 互不影响. 所以, 程序计数器是线程私有的内存区域.

  3. 如果线程执行的是一个Java方法, 计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程执行的是一个Native方法, 计数器的值为空.

  4. 程序计数器是Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域.

虚拟机栈

  1. Java虚拟机栈描述的是Java方法执行的内存模型:

每个方法执行的同时会创建一个栈帧, 栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息. 每个方法从调用直至执行完成的过程, 就对应着一个栈帧在虚拟机栈中入栈到出栈的过程.

栈帧

  1. Java虚拟机栈是线程私有的, 它的生命周期与线程相同.

  2. 程序员主要关注的stack栈内存, 就是虚拟机栈中局部变量表部分.
    局部变量表存放了编译时期可知的各种基本数据类型对象引用.
    局部变量表所需的内存空间在编译时期完成分配, 当进入一个方法时, 这个方法需要在栈帧中分配多大的局部变量空间是完全确定的, 在方法运行期间不会改变局部变量表的大小.

  3. Java虚拟机规范对这个区域规定了两种异常情况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度, 将抛出StackOverflowError 异常;
  • 如果虚拟机栈可以动态扩展, 如果扩展时无法申请到足够的内存, 就会抛出OutOfMemoryError异常;
    (当前大部分JVM都可以动态扩展, 只不过JVM规范也允许固定长度的虚拟机栈)

栈深度: 每次方法调用, 都会创建一个栈帧, 一个方法调用另一个方法, 栈帧就会深度增加一层

本地方法栈

  1. 本地方法栈与虚拟机栈所发挥的作用是非常相似的, 它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务(也就是字节码), 而本地方法栈为虚拟机使用到的Native方法服务.

  2. Java虚拟机规范对本地方法栈使用的语言、使用方法与数据结构并没有强制规定, 因此可以由虚拟机自由实现. 例如: HotSpot虚拟机直接将本地方法栈和虚拟机栈合二为一.

  3. 同虚拟机栈相同, Java虚拟机规范对这个区域也规定了两种异常情况StackOverflowErrorOutOfMemoryError异常.

  1. Java堆是被所有的线程共享的一块内存区域, 在虚拟机启动时创建.
    Java堆的唯一目的就是存放对象实例, 几乎所有的对象实例都在这里分配内存.
  2. Java堆是垃圾回收器管理的主要区域, 因此也被称为”GC堆”.

从内存回收的角度看, 由于现在收集器基本都采用分代收集算法, 所以Java堆可以细分为: 新生代、老生代;

从内存分配的角度看, 线程共享的Java堆可能划分出多个线程私有的分配缓冲区(TLAB);

不论如何划分, 都与存放的内容无关, 无论哪个区域, 存储的仍然是对象实例.

  1. Java虚拟机规范规定, Java堆可以处于物理上不连续的内存空间中, 只要逻辑上是连续的即可, 就像我们的磁盘空间一样. 在实现上, 既可以是固定大小的, 也可以是可扩展的, 不过当前主流JVM都是按照可扩展来实现的.

  2. Java虚拟机规范规定, 如果在堆上没有内存完成实例分配, 并且堆上也无法再扩展时, 将会抛出OutOfMemoryError异常.

  3. 内存泄露和内存溢出
    Java堆内存的OOM异常是非常常见的异常情况, 重点是根据内存中的对象是否是必要的, 来弄清楚到底是出现了内存泄露(Memory Leak)还是内存溢出(Memory Overflow).

  • 内存泄露: 指程序中一些对象不会被GC所回收, 它始终占用内存, 即被分配的对象引用链可达但已无用. (可用内存减少)
  • 内存溢出: 程序运行过程中无法申请到足够的内存而导致的一种错误. 内存溢出通常发生于OLD段或Perm段垃圾回收后, 仍然无内存空间容纳新的Java对象的情况.
  • 内存泄露是内存溢出的一种诱因, 不是唯一因素.

方法区

  1. 方法区也是被所有的线程共享的一块内存区域. 它用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据.

  2. Java虚拟机规范对方法区的限制非常宽松, 除了和Java堆一样 不需要连续的内存和可以选择固定大小或者可扩展之外, 还可以选择不实现垃圾回收.
    这区域的内存回收目标主要是针对常量池的回收和类型的卸载, 一般而言, 这个区域的内存回收比较难以令人满意, 尤其是类型的回收, 条件相当苛刻, 但是这部分区域的内存回收确实是必要的.

  3. Java虚拟机规范规定, 当方法区无法满足内存分配的需求时, 将抛出OutOfMemoryError异常.

  4. 运行时常量池

运行时常量池是方法区的一部分. Class文件中除了有类的版本、字段、方法、接口等描述信息外, 还有一项信息是常量池, 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后进入方法区的运行时常量池中存放.

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性, Java语言并不要求常量一定只有编译期才能产生, 也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池, 运行期间也可能将新的常量放入池中, 这种特性被开发人员利用比较多的就是String类的intern()方法.

  1. String.intern()

String.intern()是一个Native方法, 它的作用是: 如果字符串常量池中已经包含了一个等于此String对象的字符串, 则返回代表池中这个字符串的String对象;否则, 将此String对象包含的字符串添加到常量池中, 并且返回此字符串的引用.

1
2
3
4
5
6
7
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}

这段代码在JDK1.6中运行, 会得到两个false, 而在JDK1.7中运行, 会得到一个true和一个false. 原因是:

  1. 在JDK1.6中intern()方法会把首次遇到的字符串实例复制到永久代中, 返回的也是永久代中这个字符串实例的引用, 而由StringBuilder创建的字符串实例在Java堆上, 所以必然不是一个引用.
  2. 在JDK1.7中intern()方法不会复制实例, 只是在常量池中记录首次出现的实例引用, 因此intern()返回的引用和由StringBuilder创建的字符串实例是同一个.
  3. str2返回false是因为Java这个字符串在执行StringBuilder("ja").append("va").toString()之前已经出现过, 字符串常量池中已经有它的引用了, 不符合首次出现的原则, 而”计算机软件”这个字符串是首次出现的.

堆的划分

堆的分代划分
堆的分代划分

  1. Young(年轻代)

年轻代分三个区. 一个Eden区, 两个 Survivor区. 大部分对象在Eden区中生成. 当Eden区满时, 还存活的对象将被复制到Survivor区(两个中的一个), 当这个 Survivor区满时, 此区的存活对象将被复制到另外一个Survivor区, 当这个Survivor区也满了的时候, 从第一个Survivor区复制过来的并且此时还存活的对象, 将被复制“年老区(Tenured)”. 需要注意, Survivor的两个区是对称的, 没先后关系, 所以同一个区中可能同时存在从Eden复制过来的对象和从前一个Survivor复制过来的对象, 而复制到年老区的只有从第一个Survivor区过来的对象. 而且, Survivor区总有一个是空的.

  1. Tenured(年老代)

年老代存放从年轻代存活的对象. 一般来说年老代存放的都是生命期较长的对象.

  1. Perm(持久代)

用于存放静态文件, 如Java类、方法等. 持久代对垃圾回收没有显著影响, 但是有些应用可能动态生成或者调用一些Class, 例如Hibernate等, 在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类. 持久代大小通过-XX:MaxPermSize=进行设置.

GC

GC有两种类型: Scavenge GC和Full GC.

  1. Scavenge GC

一般情况下, 当新对象生成, 并且在Eden申请空间失败时, 就好触发Scavenge GC, 堆Eden区域进行GC, 清除非存活对象, 并且把尚且存活的对象移动到Survivor区. 然后整理Survivor的两个区.

  1. Full GC

对整个堆进行整理, 包括Young、Tenured和Perm. Full GC比Scavenge GC要慢, 因此应该尽可能减少Full GC. 有如下原因可能导致Full GC:

  • Tenured被写满
  • Perm域被写满
    • System.gc()被显示调用
    • 上一次GC之后Heap的各域分配策略动态变化

基本回收算法

  • 引用计数(Reference Counting)
    比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
  • 标记-清除(Mark-Sweep)
    此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
  • 复制(Copying)
    此 算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理 正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不过出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍 内存空间。
  • 标记-整理(Mark-Compact)
    此算法结 合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活 对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
  • 增量收集(Incremental Collecting)
    实施垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。
  • 分代(Generational Collecting)
    基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

JVM内存参数设置与调优

常见配置举例

堆大小设置

JVM 中最大堆大小有三方面限制: 相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制. 32位系统 下, 一般限制在1.5G~2G;64为操作系统对内存无限制. 我在Windows Server 2003 系统, 3.5G物理内存, JDK5.0下测试, 最大可设置为1478m.

典型设置:

  • java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
    • -Xmx3550m: 设置JVM最大可用内存为3550M.
    • -Xms3550m: 设置JVM初始内存为3550m. 此值可以设置与-Xmx相同, 以避免每次垃圾回收完成后JVM重新分配内存.
    • -Xmn2g: 设置年轻代大小为2G. 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小. 持久代一般固定大小为64m, 所以增大年轻代后, 将会减小年老代大小. 此值对系统性能影响较大, Sun官方推荐配置为整个堆的3/8.
    • -Xss128k: 设置每个线程的堆栈大小. JDK5.0以后每个线程堆栈大小为1M, 以前每个线程堆栈大小为256K. 更具应用的线程所需内存大小进行调整. 在相同物理内 存下, 减小这个值能生成更多的线程. 但是操作系统对一个进程内的线程数还是有限制的, 不能无限生成, 经验值在3000~5000左右.
  • java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
    • -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代). 设置为4, 则年轻代与年老代所占比值为1: 4, 年轻代占整个堆栈的1/5
    • -XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值. 设置为4, 则两个Survivor区与一个Eden区的比值为2:4, 一个Survivor区占整个年轻代的1/6
    • -XX:MaxPermSize=16m:设置持久代大小为16m.
    • -XX:MaxTenuringThreshold=0: 设置垃圾最大年龄. 如果设置为0的话, 则年轻代对象不经过Survivor区, 直接进入年老代. 对于年老代比较多的应用, 可以提高效率. 如果将此值设置为一个较大值, 则年轻代对象会在Survivor区进行多次复制, 这样可以增加对象再年轻代的存活时间, 增加在年轻代即被回收的概论.

调优总结

  1. 年轻代大小选择

    响应时间优先的应用: 尽可能设大, 直到接近系统的最低响应时间限制(根据实际情况选择). 在此种情况下, 年轻代收集发生的频率也是最小的. 同时, 减少到达年老代的对象.

    吞吐量优先的应用: 尽可能设大, 可能到达Gbit的程度. 因为对响应时间没有要求, 垃圾收集可以并行进行, 一般适合8CPU以上的应用.

  2. 年老代大小选择
    响应时间优先的应用:

    年老代使用并发收集器, 所以其大小需要小心设置, 一般要考虑并发会话率和会话持续时间等一些参数. 如果堆设置小了, 可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了, 则需要较长的收集时间. 最优化的方案, 一般需要参考以下数据获得:

    • 并发垃圾收集信息
    • 持久代并发收集次数
    • 传统GC信息
    • 花在年轻代和年老代回收上的时间比例 减少年轻代和年老代花费的时间, 一般会提高应用的效率

吞吐量优先的应用: 一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代. 原因是, 这样可以尽可能回收掉大部分短期对象, 减少中期的对象, 而年老代尽存放长期存活对象.

  1. 较小堆引起的碎片问题

因为年老代的并发收集器使用标记、清除算法, 所以不会对堆进行压缩. 当收集器回收时, 他会把相邻的空间进行合并, 这样可以分配给较大的对象. 但是, 当堆空间 较小时, 运行一段时间以后, 就会出现“碎片”, 如果并发收集器找不到足够的空间, 那么并发收集器将会停止, 然后使用传统的标记、清除方式进行回收. 如果出 现“碎片”, 可能需要进行如下配置:
- -XX:+UseCMSCompactAtFullCollection: 使用并发收集器时, 开启对年老代的压缩.
- -XX:CMSFullGCsBeforeCompaction=0: 上面配置开启的情况下, 这里设置多少次Full GC后, 对年老代进行压缩

常见配置参数汇总

  1. 堆设置
    • -Xms:初始堆大小
    • -Xmx:最大堆大小
    • -XX:NewSize=n:设置年轻代大小
    • -XX:NewRatio=n:设置年轻代和年老代的比值. 如:为3, 表示年轻代与年老代比值为1: 3, 年轻代占整个年轻代年老代和的1/4
    • -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值. 注意Survivor区有两个. 如: 3, 表示Eden: Survivor=3: 2, 一个Survivor区占整个年轻代的1/5
    • -XX:MaxPermSize=n:设置持久代大小
  2. 收集器设置
    • -XX:+UseSerialGC:设置串行收集器
    • -XX:+UseParallelGC:设置并行收集器
    • -XX:+UseParalledlOldGC:设置并行年老代收集器
    • -XX:+UseConcMarkSweepGC:设置并发收集器
  3. 垃圾回收统计信息
    • -XX:+PrintGC
    • -XX:+Printetails
    • -XX:+PrintGCTimeStamps
    • -Xloggc:filename
  4. 并行收集器设置
    • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数. 并行收集线程数.
    • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
    • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比. 公式为1/(1+n)
  5. 并发收集器设置
    • -XX:+CMSIncrementalMode:设置为增量模式. 适用于单CPU情况.
    • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时, 使用的CPU数. 并行收集线程数.

类加载

类的加载阶段

类加载分为装载、链接、初始化三步.

  1. 装载

通过类的全限定名和ClassLoader加载类, 主要是将指定的.Class文件加载至JVM. 当类被加载以后, 在JVM内部就以“类的全限定名+ClassLoader实例ID”来标明类.

在内存中, ClassLoader实例和类的实例都位于堆中, 它们的类信息都位于方法区.

装载过程采用了一种被称为“双亲委派模型(Parent Delegation Model)”的方式, 当一个ClassLoader要加载类时, 它会先请求它的双亲ClassLoader(其实这里只有两个ClassLoader, 所以称为父ClassLoader可能更容易理解)加载类, 而它的双亲ClassLoader会继续把加载请求提交再上一级的ClassLoader, 直到启动类加载器. 只有其双亲ClassLoader无法加载指定的类时, 它才会自己加载类.

双亲委派模型是JVM的第一道安全防线, 它保证了类的安全加载, 这里同时依赖了类加载器隔离的原理: 不同类加载器加载的类之间是无法直接交互的, 即使是同一个类, 被不同的ClassLoader加载, 它们也无法感知到彼此的存在. 这样即使有恶意的类冒充自己在核心包(例如java.lang)下, 由于它无法被启动类加载器加载, 也造成不了危害.

由此也可见, 如果用户自定义了类加载器, 那就必须自己保障类加载过程中的安全.

  1. 链接

链接的任务是把二进制的类型信息合并到JVM运行时状态中去.
链接分为以下三步:

  • 验证: 校验.Class文件的正确性, 确保该文件是符合规范定义的, 并且适合当前JVM使用.
  • 准备: 为类分配内存, 同时初始化类中的静态变量赋值为默认值.
  • 解析(可选): 主要是把类的常量池中的符号引用解析为直接引用, 这一步可以在用到相应的引用时再解析.
  1. 初始化

初始化类中的静态变量, 并执行类中的static代码、构造函数.

JVM规范严格定义了何时需要对类进行初始化:

  • 通过new关键字、反射、clone、反序列化机制实例化对象时.
  • 调用类的静态方法时.
  • 使用类的静态字段或对其赋值时.
  • 通过反射调用类的方法时.
  • 初始化该类的子类时(初始化子类前其父类必须已经被初始化).
  • JVM启动时被标记为启动类的类(简单理解为具有main方法的类).

类加载器

ClassLoader的分类:

1)BootstrapClassLoader(启动类加载器)
  负责加载$JAVA_HOME中jre/lib/rt.jar里所有的Class,加载System.getProperty(“sun.boot.Class.path”)所指定的路径或jar。
2)ExtensionClassLoader(标准扩展类加载器)
  负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包。
在System.getProperty(“java.ext.dirs”)所指定的路径或jar。
3)AppClassLoader(系统类加载器)
  负责记载Classpath中指定的jar包及目录中Class
4)CustomClassLoader(自定义加载器)
  属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。

类加载器的顺序: 双亲委派

当一个ClassLoader要加载类时,它会先请求它的双亲ClassLoader(其实这里只有两个ClassLoader,所以称为父ClassLoader可能更容易理解)加载类,而它的双亲ClassLoader会继续把加载请求提交再上一级的ClassLoader,直到启动类加载器。只有其双亲ClassLoader无法加载指定的类时,它才会自己加载类。

双亲委派模型是JVM的第一道安全防线,它保证了类的安全加载,这里同时依赖了类加载器隔离的原理:不同类加载器加载的类之间是无法直接交互的,即使是同一个类,被不同的ClassLoader加载,它们也无法感知到彼此的存在。这样即使有恶意的类冒充自己在核心包(例如java.lang)下,由于它无法被启动类加载器加载,也造成不了危害。

由此也可见,如果用户自定义了类加载器,那就必须自己保障类加载过程中的安全。

参考文献

  1. 深入理解JVM 1
  2. JVM调优总结
  3. 浅析Java虚拟机结构与机制
  4. 深入理解JVM03–垃圾收集算法

heap分代的目的是,应对不同生命周期的对象,大部分对象是朝生夕死的,也用一些常量是长期占用内存的,如数据值的枚举
动态调整:

正则表达式

元字符

  1. \w 匹配一个字符,可以是数字、下划线、字母或者汉字
  2. \b 匹配的是位置,它的前一个字符和后一个字符不全是(一个是, 一个不是或不存在) \w
  • \bhi\b 匹配hi单词, 对于history和him都不匹配
  • \bhi\b.*\blucy\b hi单词后面若干字符之后是lucy
  1. . 匹配除了换行符之外的任一字符
    \bhi.lucy\b在hi和lucy中存在一个除了换行符之外的任一字符
  2. * 匹配的是数量 0~若干个
    \bhi\b.*\blucy\b hi单词后面若干个除换行符以外的字符之后是lucy
  3. \s 匹配任意的空白符
  4. \d 匹配数字
    0\d{2}-\d{8} 匹配以0开头,紧接着是2位数字,接着是-,最后是8位数字
    \d+ 匹配多于一个数字
  5. [0123456789]或者[0-9] 匹配数字与\d的意义完全相同
  6. ^ 匹配字符串的开始
  7. $ 匹配字符串的结束
    ^\b{5,12}$ 匹配5到12位的数字

字符转义

使用斜杠\对特殊字符转义,转换为普通字符

重复

代码/语法 说明
* 重复零次或更多次
+ 重复一次或者更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

windows\d+ windows后面紧跟着多于一个数字
^\w+ 以多于一个字符开头

字符类

  1. [aeiou] aeiou中的一个字符

  2. [0-9] 从0到9

  3. [0-9A-Za-z] 完全等同于 \w(如果不考虑中文字符)

  4. [) -] 从右括号、空格和减号中选择一个

    \(?0\d{2}[) -]?\d{8} 首先是一个转义字符\(, 它可能出现0次或1次(?), 然后是一个0, 后面跟着2个数字\d{2},然后是)或-或空格中的一个,它出现1此或不出现(?), 最后是8个数字(\d{8}).

分枝条件

分枝条件: 满足若干条件中的一个就能匹配. 要注意优先级和各条件顺序问题.

** 例1 匹配电话号码
要匹配两种电话号码形式: (0755)12345678 或者 0755-12345678 以及 0751-1234567

\(\d{3,4}\)\d{7,8}|\d{3,4}-\d{7,8}

** 例2: 注意分枝条件各条件的顺序
美国的邮编是有两种形式的: 5位数字 或者用连字号间隔的9位数字, 如12345和12345-1234两种形式.

应该使用\d{5}-\d{4}|\d{5}, 而不能使用\d{5}|\d{5}-\d{4}
也就是说, 如果\d{5}提前匹配了12345之后会返回而丢弃掉后面的四位.

分组

分组可以实现子表达式重复出现.

(\d{1,3}\.){3}\d{1,3} 匹配以点号分割的四组数字, 每组数字是1到3位,如 999.999.999.9991.1.0.0

** IP地址的匹配 **
IP地址分为4段, 每段的数字范围为: 0~255
正则表达式只能匹配字符串的格式, 不支持数值比较, 因此需要用尽可能精简的方式准确列出所有的可能性

((2[0-4]\d|25[0-5]|[0-1]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[0-1]?\d\d?)

解释:

  1. 2[0-4]\d 首位是2,第二位可以为0到4,第三位是任意数字
  2. 25[0-5] 前两位是25,第三位必须是0到5, 因为最大是255
  3. [0-1]?\d\d? 首位是0或1, 也可以首位不存在, 第二位为任何数字, 第三位是任意数字, 也可以不存在, 不存在这种情况下, 就只有前两位.
    为什么不是[0-1]?\d?\d?

反义

对字符类进行反义, 只需要将上面字符类一节的所有的字符转为大写

代码/语法 说明
\W 匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^aeiou] 匹配除了aeiou这几个字母以外的任意字符

后向引用

用于引用上文中匹配到的字符- 给上文中出现的表达式添加组号, 在下文中引用这个组号

代码/语法 说明
(exp) 匹配exp,并捕获文本到自动命名的组里
(?<name>exp) 匹配exp,并捕获文本到名称为name的组里,也可以写成(?’name’exp)
(?:exp) 匹配exp,不捕获匹配的文本,也不给此分组分配组号

分组命名规则为:

  • 分组0对应整个正则表达式
  • 实际上组号分配过程是要从左向右扫描两遍的:第一遍只给未命名组分配,第二遍只给命名组分配--因此所有命名组的组号都大于未命名的组号
  • 你可以使用(?:exp)这样的语法来剥夺一个分组对组号分配的参与权.

例1 自动命名
\b(\w+)\b\s+\1\b 首先匹配一个单词(\w+)并放到分组1中, 在下文中通过\1引用上文中分组1匹配到的字符.
该表达式用于匹配重复单词. 如 hello hello

例2 手动命名
\b(?<name>\w+)\b\s+\k<name>\b 首先匹配一个单词(\w+) 并将这个单词放入name组中, 在下文中引用name分组, 看后面是否再出现
注意反向引用时的格式: \k<name>

例3 不捕获
\b(?:\w+)\b\s+ 不捕获

零宽断言

零宽断言和负向零宽断言用于指示位置, 零宽断言分为两种, 分别指示匹配表达式的前面和后面.

零宽度正预测先行断言

用于指示匹配表达式的字符串的上一个字符位置

\b\w+(?=ing\b) 匹配以ing结尾的单词的前面部分(除了ing以外的部分).
如果查找 I'm singing while you're dancing.时, 它会匹配sing和danc.

零宽度正回顾后发断言

用于指示匹配表达式的字符串的下一个字符位置

(?<=\bre)\w+\b 匹配以re开头的单词的后半部分

((?<=\d)\d{3}+\b)匹配一个数字后面是若干的三个数字的后面部分.
对1234567890进行查找时结果是234567890

负向零宽断言

负向零宽断言, 与上一节中的正向零宽断言相对, 用于匹配不是某个字符或不在某些字符类里的方法

确保某个字符没有出现, 但并不想去匹配它, 就可以使用负向零宽断言

零宽度负预测先行断言(?!exp)

断言此位置的后面不能匹配表达式exp。
例如:\d{3}(?!\d)匹配三位数字,而且这三位数字的后面不能是数字;
\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词。

零宽度负回顾后发断言

同理,可以用(?<!exp),零宽度负回顾后发断言来断言此位置的前面不能匹配表达式exp:
(?<![a-z])\d{7} 匹配前面不是小写字母的七位数字。

请详细分析表达式(?<=<(\w+)>).*(?=<\/\1>),这个表达式最能表现零宽断言的真正用途。

一个更复杂的例子:
(?<=<(\w+)>).*(?=<\/\1>)匹配不包含属性的简单HTML标签内里的内容。
(?<=<(\w+)>)指定了这样的前缀:被尖括号括起来的单词(比如可能是),
然后是.*(任意的字符串),最后是一个后缀(?=</\1>)。注意后缀里的/,
它用到了前面提过的字符转义;\1则是一个反向引用,引用的正是捕获的第一组,
前面的(\w+)匹配的内容,这样如果前缀实际上是的话,后缀就是了。
整个表达式匹配的是之间的内容(再次提醒,不包括前缀和后缀本身)。

注释

通过语法(?#comment)来包含注释

1
2
3
4
5
6
7
(?<=    # 断言要匹配的文本的前缀
<(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)
) # 前缀结束
.* # 匹配任意文本
(?= # 断言要匹配的文本的后缀
<\/\1> # 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签
) # 后缀结束

贪婪与懒惰

代码/语法 说明
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

处理选项

名称 说明
IgnoreCase(忽略大小写) 匹配时不区分大小写。
Multiline(多行模式) 更改^和$的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.)
Singleline(单行模式) 更改.的含义,使它与每一个字符匹配(包括换行符\n)。
IgnorePatternWhitespace(忽略空白) 忽略表达式中的非转义空白并启用由#标记的注释。
ExplicitCapture(显式捕获) 仅捕获已被显式命名的组。

平衡组/递归匹配

用于在嵌套的层次结构中, 判断是否嵌套.

  • (?'group') 把捕获的内容命名为group,并压入堆栈(Stack)
  • (?'-group') 从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败
  • (?(group)yes|no) 如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分
  • (?!) 零宽负向先行断言,由于没有后缀表达式,试图匹配总是失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<                         #最外层的左括号
[^<>]* #最外层的左括号后面的不是括号的内容
(
(
(?'Open'<) #碰到了左括号,在黑板上写一个"Open"
[^<>]* #匹配左括号后面的不是括号的内容
)+
(
(?'-Open'>) #碰到了右括号,擦掉一个"Open"
[^<>]* #匹配右括号后面不是括号的内容
)+
)*
(?(Open)(?!)) #在遇到最外层的右括号前面,判断黑板上还有没有没擦掉的"Open";如果还有,则匹配失败

> #最外层的右括号

我们需要做的是每碰到了左括号,就在压入一个”Open”,每碰到一个右括号,就弹出一个,到了最后就看看堆栈是否为空--如果不为空那就证明左括号比右括号多,那匹配就应该失败。正则表达式引擎会进行回溯(放弃最前面或最后面的一些字符),尽量使整个表达式得到匹配。

其他语法

代码/语法 说明
\a 报警字符(打印它的效果是电脑嘀一声)
\b 通常是单词分界位置,但如果在字符类里使用代表退格
\t 制表符,Tab
\r 回车
\v 竖向制表符
\f 换页符
\n 换行符
\e Escape
\0nn ASCII代码中八进制代码为nn的字符
\xnn ASCII代码中十六进制代码为nn的字符
\unnnn Unicode代码中十六进制代码为nnnn的字符
\cN ASCII控制字符。比如\cC代表Ctrl+C
\A 字符串开头(类似^,但不受处理多行选项的影响)
\Z 字符串结尾或行尾(不受处理多行选项的影响)
\z 字符串结尾(类似$,但不受处理多行选项的影响)
\G 当前搜索的开头
\p{name} Unicode中命名为name的字符类,例如\p{IsGreek}
(?>exp) 贪婪子表达式
(?-exp) 平衡组
(?im-nsx:exp) 在子表达式exp中改变处理选项
(?im-nsx) 为表达式后面的部分改变处理选项
(?(exp)yes no)
(?(exp)yes) 同上,只是使用空表达式作为no
(?(name)yes no)
(?(name)yes) 同上,只是使用空表达式作为no

常用的正则表达式

电子邮箱Email

\w+([-+.]\w+)*@\w+([-.]\w)*\.\w+([-.]\w+)*
Email地址是以@分割, @前面的部分为用户名, 后面为域名.
用户名部分可以包含字母/数字/下划线/-/+/., 但是只能字母/数字/下划线开头.
不能出现+-.连续
而域名部分可以包含字母/数字/下划线/-以及. 也是只能字母/数字/下划线开头.

用户名部分\w+([-+.]\w+)* , 必须是至少一个\w开头,

手机号码

[1-9][3,4,5,7,8]/d{9}

汉字

[\u4e00-\u9fa5]

QQ号码

[1-9]\d{4,}

邮编

[1-9]\d{5}

不含abc的单词

\b((?!abc)\w)+\b

时间(小时:分钟, 24小时制)

((1|0?)[0-9]|2[0-3]):([0-5][0-9])

姓名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String regex="^([(?<cc>a-zA-Z\\u4e00-\\u9fa5)]+)|([a-zA-Z\\u4e00-\\u9fa5]+[a-zA-Z\\u4e00-\\u9fa5. _·]*)$";
String [] tem={
"张大千",
"张.大千",
"zhangdaqian",
"zhang.daqian",
"张大千Quene",
"zhang daqian",
"zhang_daqian",
" 张大千",
".张大千",
"_张大千"
};
for (String s : tem) {
final boolean matches = Pattern.matches(regex, s);
System.out.println(s+":\t"+(matches?"√":"×"));
}
1
2
3
4
5
6
7
8
9
10
张大千:	√
张.大千: √
zhangdaqian: √
zhang.daqian: √
张大千Quene: √
zhang daqian: √
zhang_daqian: √
张大千: ×
.张大千: ×
_张大千: ×

Java中运用

java.util.regex 包主要由三个类所组成:Pattern、Matcher 和 PatternSyntaxException。

  • Pattern 对象表示一个已编译的正则表达式。Pattern 类没有提供公共的构造方法。要构建一个模式,首先必须调用公共的静态 compile 方法,它将返回一个 Pattern 对象。这个方法接受正则表达式作为第一个参数。
  • Matcher 是一个靠着输入的字符串来解析这个模式和完成匹配操作的对象。与 Pattern 相似,Matcher 也没有定义公共的构造方法,需要通过调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。
  • PatternSyntaxException 对象是一个未检查异常,指示了正则表达式中的一个语法错误。

Pattern

1
2
3
4
5
6
7
8
String message="";
// 大小写不敏感
Pattern pattern = Pattern.compile(message, Pattern.CASE_INSENSITIVE );
// 大小写不敏感且使用Unix的行结束符
pattern = Pattern.compile("[az]$", Pattern.MULTILINE | Pattern.UNIX_LINES);
// 使用int变量, 启用Unicode折叠感知和大小写不敏感
final int flags = Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
Pattern pattern = Pattern.compile("aa", flags);

** matches(String, CharSequence) 方法 **

Pattern 类定义了一个方便的 matches 方法,用于快速地检查模式是否表示给定的输入字符串。与使用所有的公共静态方法一样,应该通过它的类名来调用 matches 方法,诸如 Pattern.matches(“\d”,”1”);。这个例子中,方法返回 true,这是由于数字“1”匹配了正则表达式\d。

** split(String) 方法 **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String REGEX = ":";
String INPUT = "one:two:three:four:five";
Pattern p = Pattern.compile(REGEX);
String[] items = p.split(INPUT);
for(String s : items) {
System.out.println(s);
}
REGEX="//d";
INPUT="one9two4three7four1five";
Pattern p = Pattern.compile(REGEX);
String[] items = p.split(INPUT);
for(String s : items) {
System.out.println(s);
}
// 输出结果 one two three four five

** public static String quote(String s)** :返回指定字符串字面模式的字符串。此方法会产生一个字符串,能被用于构建一个与字符串 s 匹配的 Pattern,好像它是一个字面上的模式。输入序列中的元字符和转义序列将没有特殊的意义了。
**  public String toString() ** :返回这个模式的字符串表现形式。这是一个编译过的模式中的正则表达式。

** java.lang.String中等价的方法 **

java.lang.String 通过模拟 java.util.regex.Pattern 行为的几个方法,也可以支持正则表达式。方便起见,下面主要摘录了出现在 API 关键的方法。

  • public boolean matches(String regex):告知字符串是否匹配给定的正则表达式。调用 str.matches(regex)方法所产生的结果与作为表达式的 Pattern.matches(regex, str)的结果是完全一致。
  • public String[] split(String regex, int limit):依照匹配给定的正则表达式来拆分字符串。调用 str.split(regex, n)方法所产生的结果与作为表达式的 Pattern.compile(regex).split(str, n) 的结果完全一致。
  • public String[] split(String regex):依照匹配给定的正则表达式来拆分字符串。这个方法与调用两个参数的 split 方法是相同的,第一个参数使用给定的表达式,第二个参数限制为 0。在结果数组中不包括尾部的空字符串。
  • 还有一个替换方法,把一个 CharSequence 替换成另外一个:
    public String replace(CharSequence target,CharSequence replacement):将字符串中每一个匹配替换匹配字面目标序列的子字符串,替换成指定的字面替换序列。这个替换从字符串的开始处理直至结束,例如,把字符串“aaa”中的“aa”替换成“b”,结果是“ba”,而不是“ab”。

Matcher

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

索引方法

  索引方法(index methods)提供了一些正好在输入字符串中发现匹配的索引值:
  public int start():返回之前匹配的开始索引。
  public int start(int group):返回之前匹配操作中通过给定组所捕获序列的开始索引。
  public int end(): 返回最后匹配字符后的偏移量。
public int end(int group): 返回之前匹配操作中通过给定组所捕获序列的最后字符之后的偏移量。

研究方法

  研究方法(study methods)回顾输入的字符串,并且返回一个用于指示是否找到模式的布尔值。
  public boolean lookingAt(): 尝试从区域开头处开始,输入序列与该模式匹配。
  public boolean find(): 尝试地寻找输入序列中,匹配模式的下一个子序列。
  public boolean find(int start): 重置匹配器,然后从指定的索引处开始,尝试地寻找输入序列中,匹配模式的下一个子序列。
  public boolean matches(): 尝试将整个区域与模式进行匹配

替换方法

  替换方法(replacement methods)用于在输入的字符串中替换文本有用处的方法。
  public Matcher appendReplacement(StringBuffer sb, String replacement):实现非结尾处的增加和替换操作。
  public StringBuffer appendTail(StringBuffer sb):实现结尾处的增加和替换操作。
  public String replaceAll(String replacement):使用给定的替换字符串来替换输入序列中匹配模式的每一个子序列。
  public String replaceFirst(String replacement):使用给定的替换字符串来替换输入序列中匹配模式的第一个子序列。
  public static String quoteReplacement(String s):返回指定字符串的字面值来替换字符串。这个方法会生成一个字符串,用作 Matcher 的 appendReplacement 方法中的字面值替换 s。所产生的字符串将与作为字面值序列的 s 中的字符序列匹配。斜线(\)和美元符号($)将不再有特殊意义了。

** 使用 start 和 end 方法 **

1
2
3
4
5
6
7
8
9
10
11
String REGEX = "\\bdog\\b";
String INPUT = "dog dog dog doggie dogg";
Pattern p = Pattern.compile(REGEX);
Matcher m = p.matcher(INPUT); // 获得匹配器对象
int count = 0;
while (m.find()) {
count++;
System.out.println("Match number " + count);
System.out.println("start(): " + m.start());
System.out.println("end(): " + m.end());
}

** 使用 matches 和 lookingAt 方法 **

1
2
3
4
5
6
7
8
9
10
11
String REGEX = "foo";
String INPUT = "fooooooooooooooooo";
Pattern pattern;
Matcher matcher;
// 初始化
pattern = Pattern.compile(REGEX);
matcher = pattern.matcher(INPUT);
System.out.println("Current REGEX is: " + REGEX);
System.out.println("Current INPUT is: " + INPUT);
System.out.println("lookingAt(): " + matcher.lookingAt());
System.out.println("matches(): " + matcher.matches());

** 使用 replaceFirst(String) 和 replaceAll(String) 方法 **

1
2
3
4
5
6
7
8
String REGEX = "dog";
String INPUT = "The dog says meow. All dogs say meow.";
String REPLACE = "cat";

Pattern p = Pattern.compile(REGEX);
Matcher m = p.matcher(INPUT); // 获得匹配器对象
INPUT = m.replaceAll(REPLACE);
System.out.println(INPUT);

** 使用 appendReplacement(StringBuffer, String) 和
  appendTail(StringBuffer) 方法 **

1
2
3
4
5
6
7
8
9
10
11
String REGEX = "a*b";
static String INPUT = "aabfooaabfooabfoob";
static String REPLACE = "-";
Pattern p = Pattern.compile(REGEX);
Matcher m = p.matcher(INPUT); // 获得匹配器对象
StringBuffer sb = new StringBuffer();
while (m.find()) {
m.appendReplacement(sb, REPLACE);
}
m.appendTail(sb);
System.out.println(sb.toString());

** 在 java.lang.String 中等价的 Matcher 方法 **

为了使用方便,String 类看上去还不错地模仿了 Matcher 的两个方法:

public String replaceFirst(String regex, String replacement):使用给定的替换字符串替换该字符串中匹配了给定正则表达式的第一个子字符串。调用 str.replaceFirst(regex, repl)方法与使用 Pattern.compile(regex).matcher(str).replaceFirst(repl)产生的结果是完全相同的。

public String replaceAll(String regex, String replacement):使用给定的替换字符串替换该字符串中匹配了给定正则表达式的每一个子字符串。调用 str.replaceAll(regex, repl)方法与使用 Pattern.compile(regex).matcher(str).replaceAll(repl)产生的结果是完全相同的。

PatternSyntaxException

PatternSyntaxException 是未检查异常,指示正则表达式模式中的语法错误。PatternSyntaxException 类提供了下面的一些方法,用于确定在什么地方发生了错误:

  • public String getDescription():获得错误描述。
  • public int getIndex():获得错误索引。
  • public String getPattern():获得字符串形式的错误正则表达式。
  • public String getMessage():获得一个多行的字符串,包括语法错误和错误的索引、错误的正则表达式模式,以及模式内可视化的索引指示。
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
Pattern pattern = null;
Matcher matcher = null;

Console console = System.console();
if (console == null) {
System.err.println("No console.");
System.exit(1);
}
while (true) {
try {
pattern = Pattern.compile(console.readLine("%nEnter your regex: "));
matcher = pattern.matcher(console.readLine("Enter input string to search: "));
} catch (PatternSyntaxException pse){
console.format("There is a problem with the regular expression!%n");
console.format("The pattern in question is: %s%n", pse.getPattern());
console.format("The description is: %s%n", pse.getDescription());
console.format("The message is: %s%n", pse.getMessage());
console.format("The index is: %s%n", pse.getIndex());
System.exit(0);
}
boolean found = false;
while (matcher.find()) {
console.format("I found the text \"%s\" starting at " +
"index %d and ending at index %d.%n",
matcher.group(), matcher.start(), matcher.end()
);
found = true;
}
if (!found){
console.format("No match found.%n");
}
}
}

执行结果

1
2
3
4
5
6
7
8
Enter your regex: ?i)
There is a problem with the regular expression!
The pattern in question is: ?i)
The description is: Dangling meta character '?'
The message is: Dangling meta character '?' near index 0
?i)
^
The index is: 0

问题与练习

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
〖问题〗

1. 在 java.util.regex 包中有哪三个公共的类?描述一下它们的作用。
2. 考虑一下字符串“foo”,它的开始索引是多少?结束索引是多少?解释一下这些编号的意思。
3. 普通字符和元字符有什么不同?各给出它们的一个例子。
4. 如何把元字符表现成像普通字符那样?
5. 附有方括号的字符集称为什么?它有什么作用?
6. 这里是三个预定义的字符类:\d、\s和\w。描述一下它们各表示什么?并使用方括号的形式将它们重写。
7. 对于\d、\s和\w,写出两个简单的表达式,匹配它们相反的字符集。
8. 思考正则表达式(dog){3},识别一下其中的两个子表达式。这个表达式会匹配什么字符串?

〖练习〗

1. 使用反向引用写一个表达式,用于匹配一个人的名字,假设这个人的 first 名字与 last 名字是相同的。

【问题答案】

1. 问:在 java.util.regex 包中有哪三个公共的类?描述一下它们的作用。
答:

编译后的 Pattern 实例表示正则表达式。
Matcher 实例是解析模式和靠着输入的字符串完成匹配操作的引擎。
PatternSyntaxException 定义一个未检查异常,指示正则表达式中的语法错误。

2. 问:考虑一下字符串“foo”,它的开始索引是多少?结束索引是多少?解释一下这些编号的意思。

答:字符串中的每一个字符位于其自身的单元格中。索引位置在两个单元格之间。字符串“foo”开始于索引 0,结束于索引 3,即便是这些字符仅占用了 0、1 和 2 号单元格。

3. 问:普通字符和元字符有什么不同?各给出它们的一个例子。

答:正则表达式中的普通字符匹配其本身。元字符是一个特殊的字符,会影响被匹配模式的方式。字母A是一个普通字符。标点符号.是一个元字符,其匹配任意的单字符。

4. 问:如何把元字符表现成像普通字符那样?答:有两种方法:

在元字符前加上反斜线(\);
把元字符置于\Q(开始)\E(结束)的引用表达式中。

5. 问:附有方括号的字符集称为什么?它有什么作用?

答:是一个字符类。通过方括号间的表达式,匹配指定字符类中的任意一个字符。

6. 问:这里是三个预定义的字符类:\d、\s和\w。描述一下它们各表示什么?并使用方括号的形式将它们重写。

答:\d 匹配任意数字[0-9]
  \s 匹配任意空白字符[ \t\n-x0B\f\r ]
  \w 匹配任意单词字符[a-zA-Z_0-9]

7. 问:对于\d、\s和\w,写出两个简单的表达式,匹配它们相反的字符集。

答:\d \D [^\d]
  \s \S [^\s]
  \w \W [^\w]

8. 问:思考正则表达式(dog){3},识别一下其中的两个子表达式。这个表达式会匹配什么字符串?

答:表达式由捕获组(dog)和接着的贪婪量词{3}所组成。它匹配字符串“dogdogdog”。

【练习答案】

1. 练习:使用反向引用写一个表达式,用于匹配一个人的名字,假设这个人的 first 名字与 last 名字是相同的。

解答:([A-Z][a-zA-Z]*)\s\1

使用

替换字符串中的字符串

1
2
3
4
5
6
7
8
String regularExpressionString=...;
Matcher m = Pattern.compile(regularExpressionString, Pattern.CASE_INSENSITIVE).matcher(source);
String result=m.replaceAll(newstring);
System.out.println("使用正则表达式不区分大小写的替换结果"+result);

Matcher m1 = Pattern.compile(regularExpressionString, Pattern.CANON_EQ).matcher(source);
String result1=m1.replaceAll(newstring);
System.out.println("使用正则表达式区分大小写的替换结果"+result1);

参考文献:

  1. 正则表达式30分钟入门教程
  2. java正则表达式语法详解及其使用代码实例

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import java.time.*;
import java.time.temporal.TemporalAdjusters;
import java.util.Date;

public class TimeApiInJava8 {
public void dateToLocalDate() {
final Date date = new Date();
final Instant instant = date.toInstant();
System.out.println(instant);
final ZoneId defaultZoneId = ZoneId.systemDefault();
ZonedDateTime atZone = instant.atZone(defaultZoneId);
final LocalDate localDate = atZone.toLocalDate();
System.out.println(localDate);
LocalDateTime localDateTime = atZone.toLocalDateTime();
System.out.println(localDateTime);
}

public void dateToLocalDateTime() {
Date date = new Date();
Instant instant = date.toInstant();
ZoneId systemDefault = ZoneId.systemDefault();
LocalDateTime.ofInstant(instant, systemDefault);
}

public LocalDateTime longToLocalDateTime(Long time) {
ZoneId systemDefault = ZoneId.systemDefault();
Instant instant = Instant.ofEpochMilli(time);
LocalDateTime localDateTime = instant.atZone(systemDefault).toLocalDateTime();
return localDateTime;
}

public void localDateNow() {
LocalDate now = LocalDate.now();
System.out.println("localDateNow: " + now);
System.out.println("year: " + now.getYear());
System.out.println("month: " + now.getMonthValue());
System.out.println("day: " + now.getDayOfMonth());
}

public void localDateDiff() {
LocalDate nowLocalDate = LocalDate.of(2019, 12, 15);
LocalDate birthLocalDate = LocalDate.of(1986, 2, 14);
long l = nowLocalDate.toEpochDay() - birthLocalDate.toEpochDay();
System.out.println("date diff: " + l);
}

public void firstLastDay() {
LocalDate now = LocalDate.now();
LocalDate firstDayOfMonth = now.with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth());
LocalDate firstDayOfNextYear = now.with(TemporalAdjusters.firstDayOfNextYear());
LocalDate lastDayOfYear = now.with(TemporalAdjusters.lastDayOfYear());
LocalDate firstDayOfYear = now.with(TemporalAdjusters.firstDayOfYear());
System.out.println("firstDayOfMonth: " + firstDayOfMonth);
System.out.println("lastDayOfMonth: " + lastDayOfMonth);
System.out.println("firstDayOfNextYear: " + firstDayOfNextYear);
System.out.println("lastDayOfYear: " + lastDayOfYear);
System.out.println("firstDayOfYear: " + firstDayOfYear);
}

public void getDayOfMonth() {
LocalDate localDate = LocalDate.of(2019, 2, 14);
LocalDate lastDay = localDate.with(TemporalAdjusters.lastDayOfMonth());
int dayOfMonth = lastDay.getDayOfMonth();
System.out.println("day of month: " + dayOfMonth);
}

public void localDateCompareTo() {
LocalDate date1 = LocalDate.of(2018, 9, 20);
LocalDate date2 = LocalDate.of(2018, 9, 21);
System.out.println(date1 + ".compareTo(" + date2 + "): " + date1.compareTo(date2));
System.out.println(date1 + ".compareTo(" + date1 + "): " + date1.compareTo(date1));
System.out.println(date2 + ".compareTo(" + date1 + "): " + date2.compareTo(date1));

}

public void nextMonth() {

LocalDate localDate = LocalDate.of(2018, 2, 12);

// 下一周的该星期
LocalDate nextWeeks1 = localDate.minusWeeks(-1);
System.out.println(nextWeeks1);
// 2018-02-19
LocalDate nextWeeks2 = localDate.plusWeeks(1);
System.out.println(nextWeeks2);

// 获取下个月的这天
LocalDate nextMonth1 = localDate.minusMonths(-1);
System.out.println(nextMonth1);
// 2018-03-12
LocalDate nextMonth2 = localDate.plusMonths(1);
System.out.println(nextMonth2);

// 下个月的1号
LocalDate localDate3 = LocalDate.of(localDate.getYear(), localDate.getMonthValue() + 1, 1);
System.out.println(localDate3);
// 2018-03-01
}

public void localDatePeriod() {
LocalDate date1 = LocalDate.of(2018, 10, 9);
LocalDate date2 = LocalDate.of(2019, 4, 1);
Period period = Period.between(date1, date2);
int years = period.getYears();
int months = period.getMonths();
int days = period.getDays();
System.out.println("years:" + years + ", months:" + months + ", days:" + days);
// years:0, months:5, days:23
}

public void ofEpochSecond() {
Instant now = Instant.now();
System.out.println(now);
// 2019-03-13T06:41:32.865Z

// 去除毫秒
long l = now.toEpochMilli() / 1000;
// 通过秒构建Instant对象
Instant instant = Instant.ofEpochSecond(l);
System.out.println(instant);
// 2019-03-13T06:41:32Z
}

public void stringToLocalDate() {
final String string = "2018-12-07";
LocalDate parse = LocalDate.parse(string);
System.out.println(parse.toString());
// 结果是2018-12-07

}

public void localDateTimeToZonedDateTime() {
final String string = "2018-12-07T09:33:38";
LocalDateTime parse = LocalDateTime.parse(string);
ZonedDateTime z1 = ZonedDateTime.of(parse, ZoneId.of("Asia/Shanghai"));
System.out.println(z1.toString());
// 2018-12-07T09:33:38+08:00[Asia/Shanghai]

ZonedDateTime z2 = ZonedDateTime.of(parse, ZoneId.of("Z"));
System.out.println(z2.toString());
// 2018-12-07T09:33:38Z

ZonedDateTime z3 = ZonedDateTime.of(parse, ZoneId.of("UTC"));
System.out.println(z3.toString());
// 2018-12-07T09:33:38Z[UTC]

ZonedDateTime z4 = ZonedDateTime.of(parse, ZoneId.of("UTC+08:00"));
System.out.println(z4.toString());
// 2018-12-07T09:33:38+08:00[UTC+08:00]

ZonedDateTime z5 = ZonedDateTime.of(parse, ZoneId.of("+08:00"));
System.out.println(z5.toString());
// 2018-12-07T09:33:38+08:00

ZonedDateTime z6 = ZonedDateTime.of(parse, ZoneId.of("+00:00"));
System.out.println(z6.toString());
// 2018-12-07T09:33:38Z

}

public static void main(final String[] args) {
TimeApiInJava8 timeApi = new TimeApiInJava8();
timeApi.dateToLocalDate();
timeApi.dateToLocalDateTime();
System.out.println("long to localDateTime: " + timeApi.longToLocalDateTime(System.currentTimeMillis()));
timeApi.localDateNow();
timeApi.localDateDiff();
timeApi.firstLastDay();
timeApi.getDayOfMonth();
timeApi.localDateCompareTo();
timeApi.nextMonth();
timeApi.localDatePeriod();
timeApi.ofEpochSecond();
timeApi.stringToLocalDate();
timeApi.localDateTimeToZonedDateTime();
}
}

请参考深入理解java异常处理机制

异常是指当程序中某些地方出错时创建的一种特殊的运行时错误对象。Java创建异常对象后,就发送给Java程序,即抛出异常(throwing an exception)。程序捕捉到这个异常后,可以编写相应的异常处理代码进行处理。使用异常处理可以使得程序更加健壮,有助于调试和后期维护。

Throwable的继承体系

Throwable类派生了两个类:Exception类和Error类,其中Error类系统保留,而Exception类供应用程序使用,它下面又派生出几个具体的异常类,都对应着一项具体的运行错误

Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。

异常的工作原理

抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。

捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。

常见的可检查异常(checked)

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

IOException:操作输入流和输出流时可能出现的异常。

EOFException 文件已结束异常

FileNotFoundException 文件未找到异常

常见的非受检异常(unchecked)

运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

  1. java.lang.ArrayIndexOutOfBoundsException 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
  2. java.lang.ArithmeticException 算术条件异常。譬如:整数除零等。
  3. java.lang.NullPointerException 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等
  4. java.lang.ClassNotFoundException 找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
  5. java.lang.NegativeArraySizeException 数组长度为负异常
  6. java.lang.ArrayStoreException 数组中包含不兼容的值抛出的异常
  7. java.lang.SecurityException 安全性异常
  8. java.lang.IllegalArgumentException 非法参数异常

综合实例:

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
65
66
public class TestException {
public TestException() {
}

boolean testEx() throws Exception {
boolean ret = true;
try {
ret = testEx1();
} catch (Exception e) {
System.out.println("testEx, catch exception");
ret = false;
throw e;
} finally {
System.out.println("testEx, finally; return value=" + ret);
return ret;
}
}

boolean testEx1() throws Exception {
boolean ret = true;
try {
ret = testEx2();
if (!ret) {
return false;
}
System.out.println("testEx1, at the end of try");
return ret;
} catch (Exception e) {
System.out.println("testEx1, catch exception");
ret = false;
throw e;
} finally {
System.out.println("testEx1, finally; return value=" + ret);
return ret;
}
}

boolean testEx2() throws Exception {
boolean ret = true;
try {
int b = 12;
int c;
for (int i = 2; i >= -2; i--) {
c = b / i;
System.out.println("i=" + i);
}
return true;
} catch (Exception e) {
System.out.println("testEx2, catch exception");
ret = false;
throw e;
} finally {
System.out.println("testEx2, finally; return value=" + ret);
return ret;
}
}

public static void main(String[] args) {
TestException testException1 = new TestException();
try {
testException1.testEx();
} catch (Exception e) {
e.printStackTrace();
}
}
}

输出结果:

1
2
3
4
5
6
i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, finally; return value=false
testEx, finally; return value=false

最佳实践

  1. 尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常
  2. 不要生吞(swallow)异常
  3. Throw early, catch late 原则
  4. 尽量少用异常,可能带来性能问题
    • try-catch 代码段会产生额外的性能开销,往往会影响 JVM 对代码进行优化,建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
    • Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。

[参考文献]:

  1. Think in Java
  2. 深入理解java异常处理机制

<< Java高级软件工程师知识结构

  1. 掌握InputStream、OutputStream、Reader、Writer的继承体系。
  2. 掌握字节流(FileInputStream、DataInputStream、BufferedInputStream、FileOutputSteam、DataOutputStream、BufferedOutputStream)和 字符流(BufferedReader、InputStreamReader、FileReader、BufferedWriter、OutputStreamWriter、PrintWriter、FileWriter),并熟练运用。
  3. 掌握NIO实现原理及使用方法。

Java IO包括:

JSON

JSON: JavaScript对象表示法(JavaScript Object Notation)
是存储和交换文本信息的语法, 类似于XML,它采用键值对的形式来组织,易于阅读和编写,同时也已于机器解析和生成。JSON是独立于语言的,也就是说不管什么语言,都可以解析json,只需要按照json的规则来执行。

JSON与XML比较

  1. json长度更小
  2. json读写速度更快
  3. 可以通过JavaScript内建的方法直接解析,转换成JavaScript对象,非常方便 。

JSON格式

JavaScript中的解析

要两种方式:evalJSON.parse

在代码中使用eval时和危险的! 特别是用它第三方的JSON数据(可能包含恶意代码)时, 尽可能使用 JSON.parse()方法解析字符串本身, 该方法还可以捕捉JSON中的语法错误。

例子:

1
2
3
var jsondata = '{"staff":[{"name":"洪七","age":70},{"name":"郭靖","age":35},{"name":"黄蓉","age":30}]}'
var jsonobj= eval('('+jsondata+')');
alert(jsonobj.staff[0].name);
1
2
3
var jsondata = '{"staff":[{"name":"洪七","age":70},{"name":"郭靖","age":35},{"name":"黄蓉","age":30}]}'
var jsonobj=JSON.parse(jsondata);
alert(jsonobj.staff[0].name);

[参考文献]:

SQL

SQL(Structured Query Language, 结构化查询语言)可以分为 DDL(data definition language,数据定义语言) TPL(事务处理语言) DML(data manipulation language, 数据操作语言)和 DCL(data control language, 数据控制语言)。

其主要的语句有:

  1. DDL(data definition language,数据定义语言) 创建、删除和更改数据库对象
    1. 创建 删除 修改数据库
      1. creat database
      2. drop database
      3. alter database
    2. 创建 删除 修改数据表
      1. create table
      2. alter table
      3. drop table
    3. 创建 删除索引
      1. create index
      2. drop index
  2. DML(data manipulation language, 数据操作语言) 查询和更新指令都成了 SQL 的 DML 部分
    1. select 从表或试图中检索数据
    2. update 更改表中的数据
    3. delete 从表中删除数据行
    4. insert into 添加数据行到表
  3. DCL(data control language, 数据控制语言)
    1. 用于规定数据库用户的各种权限
      1. grant 将权限或角色授予用户或其他角色
      2. revoke 从用户或数据库角色回收权限
    2. 数据库事务控制
      1. commit 把当前事务所有的更改写入磁盘
      2. rollback 作废上次提交依赖的所有的更改

数据库设计

数据库设计可以分为需求分析、逻辑分析、物理设计和维护优化四个阶段。

数据库的基本名词:

  • 关系:一个关系对应通常所说的一张表
  • 元组:表中的一行即为一个元组
  • 属性:表中的一列即为一个属性;每一个属性都有一个名称,称为属性名。
  • 候选码:表中的某个属性组,它可以唯一确定一个元组。
  • 主码:一个关系有多个候选码,确定其中一个为主码
  • 域:属性的取值范围
  • 分量:元组中的一个属性值。

ER 图

ER 图中各符号的含义:

在这里以订单、用户、商品、供应商以及购物车的关系构建 ER 图

矩形框表示实体, 即图中的订单、用户、商品、供应商以及购物车。
椭圆表示视图的属性, 如用户的用户 ID、用户名、密码、昵称和身份证。椭圆中的文本表示属性的名称,文本带下划线表示为主键。
线段将属性与实体集相互连接,表示属性是实体的。将实体间相互连接,表示实体间的对应关系。
线段的两端标示 1 或 M,表示是一对一、一对多或者多对多的关系。

数据操作异常与数据冗余

数据操作异常是判断数据库设计是否合理的依据。

  • 插入异常: 如果某实体随着另一个实体的存在而存在, 即缺少某个实体时无法表示这个实体,那么这个表就存在插入异常。
  • 更新异常: 如果更改表所对应的某个实体实例的单独属性时,需要将多行更新,那么就说这个表存在更新异常。
  • 删除异常: 如果删除表的某一行来反应某实体实例。失效时导致另一个不同实体实例信息丢失,那么这个表存在删除异常。

数据冗余:
是指相同的数据在多个地方存在, 或者说表中的某个列可以有其他列计算得到,这样就说表中存在着数据冗余。

三范式

第一范式(1NF)

字段具有原子性,不可再分。所有关系型数据库系统都满足第一范式。
数据库表中的字段都是单一属性的,不可再分。例如,姓名字段,其中的姓和名必须作为一个整体,无法区分哪部分是姓,哪部分是名,如果要区分出姓和名,必须设计成两个独立的字段。

这个单一属性是由基本的数据类型所构成的, 如整数,浮点数,字符串等;
换句话说: 第一范式要求数据库中的表都是二维表

如下表中

第二个表格中, 用户信息列又包含姓名 电话两个列。 不符合第一范式的要求

第二范式(2NF)

满足第二范式(2NF)必须先满足第一范式(1NF)。
要求数据库表中的每个实例或行必须可以被惟一地区分。通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主关键字或主键。
第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,第二范式就是非主属性非部分依赖于主关键字。

由于供应商和商品之间是多对多的关系
所以只有使用商品名称供应商名称才可以唯一表示出一件商品。
也就是商品名称和供应商名称是一组组合关键字。
上表的依赖关系为:

  • (商品名称)->(价格、描述、重量、商品有效期)
  • (供应商名称)->(供应商电话)

存在的问题:

  1. 插入异常
  2. 删除异常
  3. 更新异常
  4. 数据冗余

第三范式

必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。
所以第三范式具有如下特征: 1. 每一列只有一个值 2. 每一行都能区分。 3. 每一个表都不包含其他表已经包含的非主关键字信息。

例如,帖子表中只能出现发帖人的 id,而不能出现发帖人的 id,还同时出现发帖人姓名,否则,只要出现同一发帖人 id 的所有记录,它们中的姓名部分都必须严格保持一致,这就是数据冗余。

如果数据表中不存在非关键字段对任一候选字段的传递函数依赖则符合第三范式。

BC 范式(Boyce.Codd 范式)

在第三范式的基础上,数据库表中如果不存在任何字段对任一候选关键字段的传递函数依赖则符合 BC 范式。
也就是说如果是符合关键字,则符合关键字之间也不能存在函数依赖关系。

上表中存在的下列关系不符合 BCNF:

  • (供应商)->(供应商联系人)
  • (供应商联系人)->(供应商)
  • 并且存在数据存在异常及数据冗余

数据库锁

锁的类型有三种:

  • 共享(S)锁:多个事务可封锁一个共享页;任何事务都不能修改该页; 通常是该页被读取完毕,S 锁立即被释放。
  • 排它(X)锁:仅允许一个事务封锁此页;其他任何事务必须等到 X 锁被释放才能对该页进行访问;X 锁一直到事务结束才能被释放。
  • 更新(U)锁:用来预定要对此页施加 X 锁,它允许其他事务读,但不允许再施加 U 锁或 X 锁;当被读取的页将要被更新时,则升级为 X 锁;U 锁一直到事务结束时才能被释放。

SQLServer 中的数据库锁

MySQL 中的数据库锁

数据库锁原理

TODOs

  1. 事务 各个层次的操作
  2. 键 唯一键 主键 外键
  3. MySQL 集群
  4. 事务隔离级别

<< Java高级软件工程师知识结构

  1. 掌握InputStream、OutputStream、Reader、Writer的继承体系.
  2. 掌握字节流(FileInputStream、DataInputStream、BufferedInputStream、FileOutputSteam、DataOutputStream、BufferedOutputStream)和 字符流(BufferedReader、InputStreamReader、FileReader、BufferedWriter、OutputStreamWriter、PrintWriter、FileWriter), 并熟练运用.
  3. 掌握NIO实现原理及使用方法.

Java IO包括:

文后的参考文献 Java NIO浅析 需要重点关注

Doug Lea《scalable IO in Java》
中文解读

引言

新的输入/输出(NIO)库是在JDK 1.4中引入的. NIO弥补了原来的I/O的不足, 它在标准java中提供了高速的、面向块的I/O.
   NIO与 BIO 最重要的区别是数据打包和传输的方式的不同, 原来的 I/O 以流 的方式处理数据, 而 NIO 以块 的方式处理数据.  
   面向流的I/O系统一次一个字节地处理数据. 一个输入流产生一个字节的数据, 一个输出流消费一个字节的数据. 为流式数据创建过滤器非常容易. 链接几个过滤器, 以便每个过滤器只负责单个复杂处理机制的一部分, 这样也是相对简单的. 不利的一面是, 面向流的I/O通常相当慢.  
   NIO与 BIO 有同样的作用和目的, 但是它使用块I/O的处理方式. 每一个操作都在一步中产生或者消费一个数据块. 按块处理数据比按(流式的)字节处理数据要快得多. 但是面向块的I/O缺少一些面向流的I/O所具有的优雅性和简单性.

 

从一个例子开始 
   下面我们从一个简单的使用IO和NIO读取一个文件中的内容为例, 来进入NIO的学习之旅.
   使用IO来读取指定文件中的前1024字节并打印出来:

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
/** 
* 使用IO读取指定文件的前1024个字节的内容.  
@param file 指定文件名称.  
@throws java.io.IOException IO异常.  
*/  
public void ioRead(String file) throws IOException {  
FileInputStream in = new FileInputStream(file);  
byte[] b = new byte[1024];  
in.read(b);  
System.out.println(new String(b));  
}  

/** 
* 使用NIO读取指定文件的前1024个字节的内容.  
@param file 指定文件名称.  
@throws java.io.IOException IO异常.  
*/  
public void nioRead(String file) throws IOException {  
FileInputStream in = new FileInputStream(file);  
FileChannel channel = in.getChannel();  

ByteBuffer buffer = ByteBuffer.allocate(1024);  
channel.read(buffer);  
byte[] b = buffer.array();  
System.out.println(new String(b));  
}  

从上面的例子中可以看出, NIO以 通道Channel缓冲区Buffer 为基础来实现面向块的IO数据处理. 下面将讨论并学习NIO 库的核心概念以及从高级的特性到底层编程细节的几乎所有方面.

核心概念:通道和缓冲区

通道和缓冲区是NIO中的核心对象, 几乎在每一个I/O操作中都要使用它们

  • 通道Channel 是对原I/O包中的流的模拟. 到任何目的地(或来自任何地方)的所有数据都必须通过一个Channel对象.
  • 缓冲区Buffer 实质上是一个容器对象. 发送给一个通道的所有对象都必须首先放到缓冲区中;同样地, 从通道中读取的任何数据都要读到缓冲区中

缓冲区

Buffer是一个容器对象, 它包含一些要写入或者刚读出的数据. 在NIO中加入Buffer对象, 体现了新库与原I/O的一个重要区别. 在面向流的I/O中, 您将数据直接写入或者将数据直接读到Stream对象中;
在NIO库中, 所有数据都是用缓冲区处理的. 在读取数据时, 它是直接读到缓冲区中的. 在写入数据时, 它是写入到缓冲区中的. 任何时候访问NIO中的数据, 您都是将它放到缓冲区中.  
缓冲区实质上是一个数组. 通常它是一个字节数组, 但是也可以使用其他种类的数组. 但是一个缓冲区不仅仅是一个数组. 缓冲区提供了对数据的结构化访问, 而且还可以跟踪系统的读/写进程.  

最常用的缓冲区类型是 ByteBuffer . 一个ByteBuffer可以在其底层字节数组上进行get/set操作(即字节的获取和设置).  
ByteBuffer不是NIO中唯一的缓冲区类型. 事实上, 对于每一种基本Java类型都有一种缓冲区类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

每一个Buffer类都是Buffer接口的一个实例. 除了ByteBuffer, 每一个Buffer类都有完全一样的操作, 只是它们所处理的数据类型不一样. 因为大多数标准I/O操作都使用 ByteBuffer , 所以它具有所有共享的缓冲区操作以及一些特有的操作.

下面的例子使用类型化的缓冲区FloatBuffer的一个应用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 分配一个容量为10的新的 float 缓冲区  
FloatBuffer buffer = FloatBuffer.allocate(10);  
for (int i = 0; i < buffer.capacity(); i++) {  
float f = (float) Math.sin((((float) i) / 10) * (2 * Math.PI));  
buffer.put(f);  
}  
// 反转此缓冲区  
buffer.flip();
// 告知在当前位置和限制之间是否有元素  
while (buffer.hasRemaining()) {  
float f = buffer.get();  
System.out.println(f);  
}

通道

Channel是对原I/O包中的流的模拟, 可以通过它读取和写入数据. 通道就像流, 所有数据都通过Buffer对象来处理. 您永远不会将字节直接写入通道中, 相反, 是将数据写入包含一个或者多个字节的缓冲区. 同样, 您不会直接从通道中读取字节, 而是将数据从通道 读入缓冲区, 再从缓冲区获取这个字节

**通道与流的不同之处在于通道是双向的. ** 而流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类), 而通道可以用于读、写或者同时用于读写. 因为它们是双向的, 所以通道可以比流更好地反映底层操作系统的真实情况. 特别是在UNIX模型中, 底层操作系统通道是双向的.

从理论到实践:NIO中的读和写

读和写是I/O的基本过程. 从一个通道中读取很简单:只需创建一个缓冲区, 然后让通道将数据读到这个缓冲区中. 写入也相当简单:创建一个缓冲区, 用数据填充它, 然后让通道用这些数据来执行写入操作.  

从文件中读取

首先从FileInputStream获取一个FileChannel对象, 然后使用这个通道来读取数据.  

在NIO系统中, 任何时候执行一个读操作, 您都是从通道中读取, 但是您不是直接从通道读取. 因为所有数据最终都驻留在缓冲区中, 所以您是从通道读到缓冲区中

   因此读取文件涉及三个步骤:

(1) 从 FileInputStream 获取 Channel
(2) 创建 Buffer
(3) 将数据从 Channel 读到 Buffer 中

现在, 让我们看一下这个过程.

1
2
3
4
5
6
7
// 第一步是获取通道. 从 FileInputStream 获取通道:
FileInputStream fin = new FileInputStream( "readandshow.txt" );  
FileChannel fc = fin.getChannel();  
// 下一步是创建缓冲区:
ByteBuffer buffer = ByteBuffer.allocate( 1024 );  
// 最后, 需要将数据从通道读到缓冲区中:
fc.read( buffer );  

不需要告诉通道要读多少数据到缓冲区中. 每一个缓冲区都有复杂的内部统计机制, 它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据.

写入文件:

在 NIO 中写入文件类似于从文件中读取.

1
2
3
4
5
6
7
8
9
10
11
// 首先从 FileOutputStream 获取一个通道:
FileOutputStream fout = new FileOutputStream"writesomebytes.txt" );  
FileChannel fc = fout.getChannel();  
// 下一步是创建一个缓冲区并在其中放入一些数据, 这里, 用message来表示一个持有数据的数组.
ByteBuffer buffer = ByteBuffer.allocate( 1024 );  
for (int i=0; i < message.length; ++i) {  
buffer.put( message[i] );  
}  
buffer.flip();  
// 最后一步是从缓冲区写入通道中:
fc.write( buffer );  

读写结合

将一个文件的所有内容拷贝到另一个文件中.

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
/** 
* 将一个文件的所有内容拷贝到另一个文件中. ​
* 执行三个基本操作: 
* 首先创建一个 Buffer, 然后从源文件中将数据读到这个缓冲区中,
* 然后将缓冲区写入目标文件.  
* 程序不断重复 — 读、写、读、写 — 直到源文件结束.  
*  ​
*/  
String infile = "C:\\copy.sql";  
String outfile = "C:\\copy.txt";  

// 获取源文件和目标文件的输入输出流  
FileInputStream fin = new FileInputStream(infile);  
FileOutputStream fout = new FileOutputStream(outfile);  

// 获取输入输出通道  
FileChannel fcin = fin.getChannel();  
FileChannel fcout = fout.getChannel();  

// 创建缓冲区  
ByteBuffer buffer = ByteBuffer.allocate(1024);  

while (true) {  
// clear方法重设缓冲区, 使它可以接受读入的数据  
buffer.clear();  

// 从输入通道中将数据读到缓冲区  
int r = fcin.read(buffer);  

// read方法返回读取的字节数, 可能为零,
// 如果该通道已到达流的末尾, 则返回-1  
if (r == -1) {  
break;  
}  

// flip方法让缓冲区可以将新读入的数据写入另一个通道  
buffer.flip();  

// 从输出通道中将数据写入缓冲区  
fcout.write(buffer);  
}  

缓冲区内部实现

每一个缓冲区都有复杂的内部统计机制, 它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据, 以便我们对缓冲区的操作.

本节介绍两个重要的缓冲区组件:状态变量和访问方法. 虽然NIO的内部统计机制初看起来可能很复杂, 但是您很快就会看到大部分的实际工作都已经替您完成了. 您只需像平时使用字节数组和索引变量一样进行操作即可.

状态变量

状态变量是”内部统计机制”的关键. 每一个读/写操作都会改变缓冲区的状态. 通过记录和跟踪这些变化, 缓冲区就能够管理内部地自己的资源.

每一种Java基本类型的缓冲区都是抽象类Buffer的子类, 从Buffer的源代码中可以发现, 它定义了三个私有属性:

1
2
3
private int position = 0;  
private int limit;  
private int capacity;  

实际上, 这三个属性值可以指定缓冲区在任意时刻的状态和它所包含的数据.
我们知道, 每一个基本类型的缓冲区底层实际上就是一个该类型的数组. 如在ByteBuffer中, 有:

1
final byte[] hb;  

在从通道读取时, 所读取的数据将放被到底层的数组中;同理, 向通道中写入时, 将从底层数组中将数据写入通道. 下面我们来具体介绍这三个变量的作用:

position

position变量跟踪了向缓冲区中写入了多少数据或者从缓冲区中读取了多少数据.
更确切的说, 当您从通道中读取数据到缓冲区中时, 它指示了下一个数据将放到数组的哪一个元素中. 比如, 如果您从通道中读三个字节到缓冲区中, 那么缓冲区的position将会设置为3, 指向数组中第4个元素. 反之, 当您从缓冲区中获取数据进行写通道时, 它指示了下一个数据来自数组的哪一个元素. 比如, 当您从缓冲区写了5个字节到通道中, 那么缓冲区的 position 将被设置为5, 指向数组的第六个元素.

limit

limit变量表明还有多少数据需要取出(在从缓冲区写入通道时), 或者还有多少空间可以放入数据(在从通道读入缓冲区时).
position总是小于或者等于limit.

capacity

capacity变量表明可以储存在缓冲区中的最大数据容量. 实际上, 它指定了底层数组的大小—或者至少是指定了准许我们使用的底层数组的容量.  
limit总是小于或者等于capacity.

举例说明

下面我们就以数据从一个输入通道拷贝到一个输出通道为例, 来详细分析每一个变量, 并说明它们是如何协同工作的:

初始变量: 

我们首先观察一个新创建的缓冲区, 以ByteBuffer为例, 假设缓冲区的大小为8个字节, ByteBuffer初始状态如下:

NIO缓冲区内部实现机制

回想一下 , limit决不能大于capacity, 此例中这两个值都被设置为8. 我们通过将它们指向数组的尾部之后(第8个槽位)来说明这点.

NIO缓冲区内部实现机制

我们再将position设置为0. 表示如果我们读一些数据到缓冲区中, 那么下一个读取的数据就进入 slot 0. 如果我们从缓冲区写一些数据, 从缓冲区读取的下一个字节就来自slot 0. position设置如下所示:

NIO缓冲区内部实现机制

由于缓冲区的最大数据容量capacity不会改变, 所以我们在下面的讨论中可以忽略它.

第一次读取: 
   现在我们可以开始在新创建的缓冲区上进行读/写操作了. 首先从输入通道中读一些数据到缓冲区中. 第一次读取得到三个字节. 它们被放到数组中从position开始的位置, 这时position被设置为0. 读完之后, position就增加到了3, 如下所示, limit没有改变.

NIO缓冲区内部实现机制

第二次读取: 
   在第二次读取时, 我们从输入通道读取另外两个字节到缓冲区中. 这两个字节储存在由position所指定的位置上, position因而增加2, limit没有改变.

NIO缓冲区内部实现机制

flip: 
   现在我们要将数据写到输出通道中. 在这之前, 我们必须调用flip()方法. 其源代码如下:

1
2
3
4
5
6
public final Buffer flip() {  
limit = position;  
position = 0;  
mark = -1;  
return this;  
}  

   这个方法做两件非常重要的事:
   i  将limit设置为当前position.
   ii 将position设置为0.

   上一个图显示了在flip之前缓冲区的情况. 下面是在flip之后的缓冲区:

NIO缓冲区内部实现机制

   我们现在可以将数据从缓冲区写入通道了. position被设置为0, 这意味着我们得到的下一个字节是第一个字节. limit已被设置为原来的position, 这意味着它包括以前读到的所有字节, 并且一个字节也不多.
第一次写入: 
   在第一次写入时, 我们从缓冲区中取四个字节并将它们 写入输出通道. 这使得position增加到4, 而limit不变, 如下所示:

NIO缓冲区内部实现机制

第二次写入: 
   我们只剩下一个字节可写了. limit在我们调用flip()时被设置为5, 并且position不能超过limit. 所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道. 这使得position增加到5, 并保持limit不变, 如下所示:

NIO缓冲区内部实现机制

clear: 
   最后一步是调用缓冲区的clear()方法. 这个方法重设缓冲区以便接收更多的字节. 其源代码如下:

1
2
3
4
5
6
7

public final Buffer clear() {  
position = 0;  
limit = capacity;  
mark = -1;  
return this;  
}  

clear做两种非常重要的事情:

i 将limit设置为与capacity相同.  
ii 设置position为0.  

下图显示了在调用clear()后缓冲区的状态, 此时缓冲区现在可以接收新的数据了.

NIO缓冲区内部实现机制

 

访问方法

程序需要直接处理数据. 例如, 您可能需要将用户数据保存到磁盘. 在这种情况下, 您必须将这些数据直接放入缓冲区, 然后用通道将缓冲区写入磁盘. 或者, 您可能想要从磁盘读取用户数据. 在这种情况下, 您要将数据从通道读到缓冲区中, 然后检查缓冲区中的数据.  
   实际上, 每一个基本类型的缓冲区都为我们提供了直接访问缓冲区中数据的方法, 我们以ByteBuffer为例, 分析如何使用其提供的get()和put()方法直接访问缓冲区中的数据.

get()

   ByteBuffer类中有四个get()方法:

1
2
3
4
byte get();  
ByteBuffer getbyte dst[] );  
ByteBuffer getbyte dst[], int offset, int length );  
byte getint index );

   第一个方法获取单个字节. 第二和第三个方法将一组字节读到一个数组中. 第四个方法从缓冲区中的特定位置获取字节. 那些返回ByteBuffer的方法只是返回调用它们的缓冲区的this值.  
   前三个get()方法是相对的, 而最后一个方法是绝对的. “相对”意味着get()操作服从limit和position值, 更明确地说, 字节是从当前position读取的, 而position在get之后会增加. 另一方面, 一个“绝对”方法会忽略limit和position值, 也不会影响它们. 事实上, 它完全绕过了缓冲区的统计方法.  

上面列出的方法对应于ByteBuffer类. 其他类有等价的get()方法, 这些方法除了不是处理字节外, 其它方面是是完全一样的, 它们处理的是与该缓冲区类相适应的类型.

put()

   ByteBuffer类中有五个put()方法:

1
2
3
4
5
ByteBuffer putbyte b );  
ByteBuffer putbyte src[] );  
ByteBuffer putbyte src[], int offset, int length );  
ByteBuffer put( ByteBuffer src );  
ByteBuffer putint index, byte b );

   第一个方法 写入(put)单个字节. 第二和第三个方法写入来自一个数组的一组字节. 第四个方法将数据从一个给定的源ByteBuffer写入这个ByteBuffer. 第五个方法将字节写入缓冲区中特定的 位置 . 那些返回ByteBuffer的方法只是返回调用它们的缓冲区的this值.  
   与get()方法一样, 我们将把put()方法划分为“相对”或者“绝对”的. 前四个方法是相对的, 而第五个方法是绝对的.  
   上面显示的方法对应于ByteBuffer类. 其他类有等价的put()方法, 这些方法除了不是处理字节之外, 其它方面是完全一样的. 它们处理的是与该缓冲区类相适应的类型.  

类型化的 get() 和 put() 方法

   除了前些小节中描述的get()和put()方法, ByteBuffer还有用于读写不同类型的值的其他方法, 如下所示

 -  getByte()
 -  getChar()
 -  getShort()
 -  getInt()
 -  getLong()
 -  getFloat()
 -  getDouble()
 -  putByte()
 -  putChar()
 -  putShort()
 -  putInt()
 -  putLong()
 -  putFloat()
 -  putDouble()

   事实上, 这其中的每个方法都有两种类型:一种是相对的, 另一种是绝对的. 它们对于读取格式化的二进制数据(如图像文件的头部)很有用.

 

如何使用?

   下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程.

1
2
3
4
5
6
7
8
9
10
while (true) {  
buffer.clear();  
int r = fcin.read( buffer );  

if (r==-1) {  
break;  
}  
buffer.flip();  
fcout.write( buffer );  
}  

   read()和write()调用得到了极大的简化, 因为许多工作细节都由缓冲区完成了. clear()和flip()方法用于让缓冲区在读和写之间切换.

连网和异步IO

连网是学习异步I/O的很好基础, 而异步I/O对于在Java语言中执行任何输入/输出过程的人来说, 无疑都是必须具备的知识. NIO中的连网与NIO中的其他任何操作没有什么不同, 它依赖通道和缓冲区, 而您通常使用InputStream和OutputStream来获得通道.

异步 I/O

   异步I/O是一种“没有阻塞地读写数据”的方法. 通常, 在代码进行read()调用时, 代码会阻塞直至有可供读取的数据. 同样, write()调用将会阻塞直至数据能够写入. 但异步I/O调用不会阻塞. 相反, 您可以注册对特定I/O事件的兴趣:如可读的数据的到达、新的套接字连接等等, 而在发生这样的事件时, 系统将会告诉您.  
   异步I/O的一个优势在于, 它允许您同时根据大量的输入和输出执行I/O. 同步程序常常要求助于轮询, 或者创建许许多多的线程以处理大量的连接. 使用异步I/O, 您可以监听任何数量的通道上的事件, 不用轮询, 也不用额外的线程.
   我们来看一个基于非阻塞I/O的服务器端的处理流程, 它接受网络连接并向它们echo它们可能发送的数据. 在这里假设它能同时监听多个端口, 并处理来自所有这些端口的连接. 下面是其主方法:

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
65
66
67
68
69
70
71
72
73
private void execute () throws IOException {  
// 创建一个新的选择器  
Selector selector = Selector.open();  

// 打开在每个端口上的监听, 并向给定的选择器注册此通道接受客户端连接的I/O事件.   
for (int i = 0; i < ports.length; i++) {  
// 打开服务器套接字通道  
ServerSocketChannel ssc = ServerSocketChannel.open();  
// 设置此通道为非阻塞模式  
ssc.configureBlocking(false);  
// 绑定到特定地址  
ServerSocket ss = ssc.socket();  
InetSocketAddress address = new InetSocketAddress(ports[i]);  
ss.bind(address);  

// 向给定的选择器注册此通道的接受连接事件  
ssc.register(selector, SelectionKey.OP_ACCEPT);  
System.out.println("Going to listen on " + ports[i]);  
}  

while (true) {  
// 这个方法会阻塞, 直到至少有一个已注册的事件发生.   
// 当一个或者更多的事件发生时, 此方法将返回所发生的事件的数量.   
int num = selector.select();  

// 迭代所有的选择键, 以处理特定的I/O事件.   
Set<SelectionKey> selectionKeys = selector.selectedKeys();  
Iterator<SelectionKey> iter = selectionKeys.iterator();  

SocketChannel sc;  
while (iter.hasNext()) {  
SelectionKey key = iter.next();  
if ((key.readyOps() 
& SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {  
// 接受服务器套接字撒很能够传入的新的连接, 并处理接受连接事件.   
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();  
sc = ssc.accept();  
// 将新连接的套接字通道设置为非阻塞模式  
sc.configureBlocking(false);  

// 接受连接后, 在此通道上从新注册读取事件, 以便接收数据.   
sc.register(selector, SelectionKey.OP_READ);  
// 删除处理过的选择键  
iter.remove();  

System.out.println("Got connection from " + sc);  
else if ((key.readyOps() 
& SelectionKey.OP_READ) == SelectionKey.OP_READ) {  
// 处理读取事件, 读取套接字通道中发来的数据.   
sc = (SocketChannel) key.channel();  

// 读取数据  
int bytesEchoed = 0;  
while (true) {  
echoBuffer.clear();  
int r = sc.read(echoBuffer);  

if (r == -1) {  
break;  
}  

echoBuffer.flip();  
sc.write(echoBuffer);  

bytesEchoed += r;  
}  
System.out.println("Echoed " + bytesEchoed + " from " + sc);  
// 删除处理过的选择键  
iter.remove();  
}  
}  
}  
}

3)    Selectors

Selector是异步I/O中的核心对象. Selector就是注册对各种I/O事件的兴趣的地方, 而且当那些事件发生时, 就是这个对象告诉您所发生的事件. 所以, 我们需要做的第一件事就是创建一个Selector:

1
Selector selector = Selector.open();  

然后, 我们将对不同的通道对象调用register()方法, 以便注册我们对这些对象中发生的I/O事件的兴趣. register()的第一个参数就是这个Selector对象.  

4)    打开一个ServerSocketChannel

在服务端为了接收连接, 我们需要一个ServerSocketChannel. 事实上, 我们要监听的每一个端口都需要有一个ServerSocketChannel. 对于每一个端口, 我们打开一个ServerSocketChannel, 如下所示:

1
2
3
4
5
ServerSocketChannel ssc = ServerSocketChannel.open();  
ssc.configureBlocking( false );  
ServerSocket ss = ssc.socket();  
InetSocketAddress address = new InetSocketAddress( ports[i] );  
ss.bind( address );  

第一行创建一个新的ServerSocketChannel, 最后三行将它绑定到给定的端口. 第二行将ServerSocketChannel设置为非阻塞的. 我们必须对每一个要使用的套接字通道调用这个方法, 否则异步I/O就不能工作.

5)    选择键

下一步是将新打开的ServerSocketChannels注册到Selector上. 为此我们使用ServerSocketChannel.register()方法, 如下所示:

1
SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );  

register()方法的第一个参数总是这个Selector. 第二个参数是OP_ACCEPT, 这里它指定我们想要监听accept事件, 也就是在新的连接建立时所发生的事件. 这是适用于ServerSocketChannel的唯一事件类型.  

请注意对register()的调用的返回值. SelectionKey代表这个通道在此Selector上的这个注册. 当某个Selector通知您某个传入事件时, 它是通过提供对应于该事件的SelectionKey来进行的. SelectionKey还可以用于取消通道的注册.
 
6)    内部循环

现在已经注册了我们对一些 I/O 事件的兴趣, 下面将进入主循环. 使用 Selectors 的几乎每个程序都像下面这样使用内部循环:

1
2
3
4
5
6
7
8
9

int num = selector.select();  

Set selectedKeys = selector.selectedKeys();  
Iterator it = selectedKeys.iterator();  
while (it.hasNext()) {  
SelectionKey key = (SelectionKey)it.next();  
// ... 处理I/O事件...  
}  
  • 调用Selectorselect()方法. 这个方法会阻塞, 直到至少有一个已注册的事件发生. 当一个或者更多的事件发生时, select()方法将返回所发生的事件的数量.  
  • 调用SelectorselectedKeys()方法, 它返回发生了事件的SelectionKey对象的一个集合.  
  • 通过迭代SelectionKeys并依次处理每个SelectionKey来处理事件. 对于每一个SelectionKey, 您必须确定发生的是什么I/O事件, 以及这个事件影响哪些I/O对象.

7)    监听新连接

程序执行到这里, 我们仅注册了ServerSocketChannel, 并且仅注册它们“接收”事件. 为确认这一点, 我们对SelectionKey调用readyOps()方法, 并检查发生了什么类型的事件:

1
2
3
if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {  
// ...  
}  

可以肯定地说, readOps()方法告诉我们该事件是新的连接.

8)    接受新的连接

因为我们知道这个服务器套接字上有一个传入连接在等待, 所以可以安全地接受它;也就是说, 不用担心accept()操作会阻塞:

1
2
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();  
SocketChannel sc = ssc.accept();  

下一步是将新连接的SocketChannel配置为非阻塞的. 而且由于接受这个连接的目的是为了读取来自套接字的数据, 所以我们还必须将SocketChannel注册到Selector上, 如下所示:

1
2
sc.configureBlocking( false );  
SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );  

注意我们使用register()的OP_READ参数, 将SocketChannel注册用于“读取”而不是“接受”新连接.

9)    删除处理过的SelectionKey

在处理SelectionKey之后, 我们几乎可以返回主循环了. 但是我们必须首先将处理过的SelectionKey从选定的键集合中删除. 如果我们没有删除处理过的键, 那么它仍然会在主集合中以一个激活的键出现, 这会导致我们尝试再次处理它. 我们调用迭代器的remove()方法来删除处理过的SelectionKey:

1
it.remove();  

现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的I/O事件)了.  

10) 传入的I/O

当来自一个套接字的数据到达时, 它会触发一个I/O事件. 这会导致在主循环中调用Selector.select(), 并返回一个或者多个I/O事件. 这一次, SelectionKey将被标记为OP_READ事件, 如下所示:

1
2
3
4
5
6
else if ((key.readyOps() & SelectionKey.OP_READ)  
== SelectionKey.OP_READ) {  
// Read the data  
SocketChannel sc = (SocketChannel)key.channel();  
// ...  
}  

与以前一样, 我们取得发生I/O事件的通道并处理它. 在本例中, 由于这是一个echo server, 我们只希望从套接字中读取数据并马上将它发送回去. 关于这个过程的细节, 请参见附件中的源代码 (MultiPortEcho.java).  

11)    回到主循环

每次返回主循环, 我们都要调用select的Selector()方法, 并取得一组SelectionKey. 每个键代表一个I/O事件. 我们处理事件, 从选定的键集中删除SelectionKey, 然后返回主循环的顶部.

说明: 这个程序有点过于简单, 因为它的目的只是展示异步I/O所涉及的技术. 在现实的应用程序中, 您需要通过将通道从Selector中删除来处理关闭的通道. 而且您可能要使用多个线程. 这个程序可以仅使用一个线程, 因为它只是一个演示, 但是在现实场景中, 创建一个线程池来负责I/O事件处理中的耗时部分会更有意义.

缓冲区更多内容

比如缓冲区分配、包装和分片. 我们还会讨论NIO带给Java平台的一些新功能. 我们将学如何创建不同类型的缓冲区以达到不同的目的, 如可保护数据不被修改的“只读缓冲区”, 和直接映射到底层操作系统缓冲区的“直接缓冲区”, 以及如何在 NIO 中创建内存映射文件.

   1) 缓冲区分配和包装

在能够读和写之前, 必须有一个缓冲区. 要创建缓冲区, 您必须“分配”它. 我们使用静态方法allocate()来分配缓冲区:

1
ByteBuffer buffer = ByteBuffer.allocate( 1024 );  

   allocate()方法分配一个具有指定大小的底层数组, 并将它包装到一个缓冲区对象中, 在本例中是一个ByteBuffer.
   您还可以将一个现有的数组转换为缓冲区, 如下所示:

1
2
byte array[] = new byte[1024];  
ByteBuffer buffer = ByteBuffer.wrap( array );  

   本例使用了wrap()方法将一个数组包装为缓冲区. 必须非常小心地进行这类操作. 一旦完成包装, 底层数据就可以通过缓冲区或者直接访问.
 
2) 缓冲区分片 

   slice()方法根据现有的缓冲区创建一个子缓冲区. 也就是说, 它创建一个新的缓冲区, 新缓冲区与原来的缓冲区的一部分共享数据.
 
   使用例子可以最好地说明这点. 让我们首先创建一个长度为10的ByteBuffer:

1
ByteBuffer buffer = ByteBuffer.allocate( 10 );  

   然后使用数据来填充这个缓冲区, 在第n个槽中放入数字n:

1
2
3
for (int i=0; i<buffer.capacity(); ++i) {  
buffer.put( (byte)i );  
}  

   现在我们对这个缓冲区“分片”, 以创建一个包含槽3到槽6的子缓冲区. 在某种意义上, 子缓冲区就像原来的缓冲区中的一个窗口 .

   窗口的起始和结束位置通过设置position和limit值来指定, 然后调用Buffer的slice()方法进行分片:

1
2
3
buffer.position( 3 );  
buffer.limit( 7 );  
ByteBuffer slice = buffer.slice();  

   该“片段”是缓冲区的子缓冲区. 不过, “片段”和“缓冲区”共享同一个底层数据数组, 我们在下一节将会看到这一点.

  1. 缓冲区片份和数据共享

 

   我们已经创建了原缓冲区的子缓冲区, 并且已经知道缓冲区和子缓冲区共享同一个底层数据数组. 让我们看看这意味着什么.  

   我们遍历子缓冲区, 将每一个元素乘以11来改变它. 例如, 5会变成55.

1
2
3
4
5
for (int i=0; i<slice.capacity(); ++i) {  
byte b = slice.get( i );  
b *= 11;  
slice.put( i, b );  
}  

   最后, 再看一下原缓冲区中的内容:

1
2
3
4
5
buffer.position( 0 );  
buffer.limit( buffer.capacity() );  
while ( buffer.remaining() > 0 ) {  
System.out.println( buffer.get() );  
}  

   结果表明只有在子缓冲区窗口中的元素被改变了: 

1
2
3
4
5
6
7
8
9
10
0
1
2
33
44
55
66
7
8
9

   缓冲区片对于促进抽象非常有帮助. 可以编写自己的函数处理整个缓冲区, 而且如果想要将这个过程应用于子缓冲区上, 您只需取主缓冲区的一个片, 并将它传递给您的函数. 这比编写自己的函数来取额外的参数以指定要对缓冲区的哪一部分进行操作更容易.  

  1. 只读缓冲区

只读缓冲区的含义已经很直白了:您可以读取它们, 但是不能向它们写入. 可以通过调用缓冲区的asReadOnlyBuffer()方法, 来将任何常规缓冲区转换为只读缓冲区, 这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据), 只不过它是只读的.  

   只读缓冲区对于保护数据很有用. 在将缓冲区传递给某个对象的方法时, 您无法知道这个方法是否会修改缓冲区中的数据. 创建一个只读的缓冲区可以保证该缓冲区不会被修改. 不能将只读的缓冲区转换为可写的缓冲区.  

  1. 直接和间接缓冲区

 

   另一种有用的ByteBuffer是直接缓冲区. “直接缓冲区”是为加快I/O速度, 而以一种特殊的方式分配其内存的缓冲区. 实际上, 直接缓冲区的准确定义是与实现相关的.

Sun的文档是这样描述直接缓冲区的: 给定一个直接字节缓冲区, Java虚拟机将尽最大努力直接对它执行本机I/O操作. 也就是说, 它会在每一次调用底层操作系统的本机I/O操作之前(或之后), 尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据).  

附件中, 您可以在例子程序FastCopyFile.java中看到直接缓冲区的实际应用, 这个程序是CopyFile.java的另一个版本, 它使用了直接缓冲区以提高速度. 还可以用内存映射文件创建直接缓冲区.  

  1. 内存映射文件I/O

   内存映射文件I/O是一种读和写文件数据的方法, 它可以比常规的基于流或者基于通道的I/O快得多.  

内存映射文件I/O是通过使文件中的数据神奇般地出现为内存数组的内容来完成的. 这其初听起来似乎不过就是将整个文件读到内存中, 但是事实上并不是这样. 一般来说, 只有文件中实际读取或者写入的部分才会送入(或者映射)到内存中.  

内存映射并不真的神奇或者多么不寻常. 现代操作系统一般根据需要将文件的部分映射为内存的部分, 从而实现文件系统. Java内存映射机制不过是在底层操作系统中可以采用这种机制时, 提供了对该机制的访问

尽管创建内存映射文件相当简单, 但是向它写入可能是危险的. 仅只是改变数组的单个元素这样的简单操作, 就可能会直接修改磁盘上的文件. 修改数据与将 数据保存到磁盘是没有分开的.

  1. 将文件映射到内存

了解内存映射的最好方法是使用例子. 在下面的例子中, 我们要将一个FileChannel (它的全部或者部分)映射到内存中. 为此我们将使用FileChannel.map()方法. 下面代码行将文件的前1024个字节映射到内存中:

1
MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 01024 );  

map()方法返回一个MappedByteBuffer, 它是ByteBuffer的子类. 因此, 您可以像使用其他任何ByteBuffer 一样使用新映射的缓冲区, 操作系统会在需要时负责执行行映射.

  1. 分散和聚集

 

  1. 概述:

分散/聚集I/O是使用多个而不是单个缓冲区来保存数据的读写方法.  
一个分散的读取就像一个常规通道读取, 只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中. 同样地, 一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据. 分散/聚集I/O对于将数据流划分为单独的部分很有用, 这有助于实现复杂的数据格式.

  1. 分散/聚集 I/O:

通道可以有选择地实现两个新的接口:ScatteringByteChannel和GatheringByteChannel. 一个 ScatteringByteChannel是一个具有两个附加读方法的通道:

1
2
long read( ByteBuffer[] dsts );  
long read( ByteBuffer[] dsts, int offset, int length );

这些long read()方法很像标准的read方法, 只不过它们不是取单个缓冲区而是取一个缓冲区数组.  

在“分散读取”中, 通道依次填充每个缓冲区. 填满一个缓冲区后, 它就开始填充下一个. 在某种意义上, 缓冲区数组就像一个大缓冲区.  

  1. 分散/聚集的应用:

 

分散/聚集I/O对于将数据划分为几个部分很有用. 例如, 您可能在编写一个使用消息对象的网络应用程序, 每一个消息被划分为固定长度的头部和固定长度的正文. 您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区. 当您将它们放入一个数组中并使用分散读取来向它们读入消息时, 头部和正文将整齐地划分到这 两个缓冲区中.  

我们从缓冲区所得到的方便性对于缓冲区数组同样有效. 因为每一个缓冲区都跟踪自己还可以接受多少数据, 所以分散读取会自动找到有空间接受数据的第一个缓冲区. 在这个缓冲区填满后, 它就会移动到下一个缓冲区.  

  1. 聚集写入:

 

聚集写入类似于分散读取, 只不过是用来写入. 它也有接受缓冲区数组的方法:

1
2
long write( ByteBuffer[] srcs );  
long write( ByteBuffer[] srcs, int offset, int length );

聚集写对于把一组单独的缓冲区中组成单个数据流很有用. 为了与上面的消息例子保持一致, 您可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流, 以便跨越网络传输消息.  
从附件的例子程序 UseScatterGather.java 中可以看到分散读取和聚集写入的实际应用.

文件锁定

  1. 概述:

 

文件锁定初看起来可能让人迷惑. 它似乎指的是防止程序或者用户访问特定文件. 事实上, 文件锁就像常规的Java对象锁, 它们是“劝告式”的(advisory)锁. 它们不阻止任何形式的数据访问, 相反, 它们通过锁的共享和获取来允许系统的不同部分相互协调. 您可以锁定整个文件或者文件的一部分. 如果您获取一个排它锁, 那么其他人就不能获得同一个文件或者文件的一部分上的锁. 如果您获得一个共享锁, 那么其他人可以获得同一个文件或者文件一部分上的共享锁, 但是不能获得排它锁. 文件锁定并不总是出于保护数据的目的. 例如, 您可能临时锁定一个文件以保证特定 的写操作成为原子的, 而不会有其他程序的干扰.  大多数操作系统提供了文件系统锁, 但是它们并不都是采用同样的方式. 有些实现提供了共享锁, 而另一些仅提供了排它锁. 事实上, 有些实现使得文件的锁定部分不可访问, 尽管大多数实现不是这样的.  在这里, 我们将学习如何在 NIO 中执行简单的文件锁过程, 还将探讨一些保证被锁定的文件尽可能可移植的方法.  

  1. 锁定文件:

 

要获取文件的一部分上的锁, 您要调用一个打开的FileChannel上的lock()方法. 注意, 如果要获取一个排它锁, 您必须以写方式打开文件.

1
2
3
RandomAccessFile raf = new RandomAccessFile"usefilelocks.txt""rw" );  
FileChannel fc = raf.getChannel();  
FileLock lock = fc.lock( start, end, false );  

   在拥有锁之后, 您可以执行需要的任何敏感操作, 然后再释放锁:

1
lock.release();  

   在释放锁后, 尝试获得锁的其他任何程序都有机会获得它.  
   本附件的例子程序UseFileLocks.java必须与它自己并行运行. 这个程序获取一个文件上的锁, 持有三秒钟, 然后释放它. 如果同时运行这个程序的多个实例, 您会看到每个实例依次获得锁.  

  1. 文件锁定和可移植性:

文件锁定可能是一个复杂的操作, 特别是考虑到 不同的操作系统是以不同的方式实现锁这一事实. 下面的指导原则将帮助您尽可能保持代码的可移植性: 

i 只使用排它锁.  

ii 将所有的锁视为劝告式的(advisory).  

字符集

  1. 概述:

 

根据Sun的文档, 一个Charset是“十六位Unicode字符序列与字节序列之间的一个命名的映射”. 实际上, 一个Charset允许您以尽可能最具可移植性的方式读写字符序列.  

Java语言被定义为基于Unicode. 然而在实际上, 许多人编写代码时都假设一个字符在磁盘上或者在网络流中用一个字节表示. 这种假设在许多情况下成立, 但是并不是在所有情况下都成立, 而且随着计算机变得对Unicode越来越友好, 这个假设就日益变得不能成立了.  

在这里, 我们将看一下如何使用Charsets以适合现代文本格式的方式处理文本数据. 这里将使用的示例程序相当简单, 不过, 它触及了使用Charset的所有关键方面:为给定的字符编码创建Charset, 以及使用该Charset解码和编码文本数据.  

  1. 编码/解码:

 

要读和写文本, 我们要分别使用CharsetDecoder和CharsetEncoder. 将它们称为“编码器”和“解码器” 是有道理的. 一个字符不再表示一个特定的位模式, 而是表示字符系统中的一个实体. 因此, 由某个实际的位模式表示的字符必须以某种特定的编码来表示.  

CharsetDecoder用于将逐位表示的一串字符转换为具体的char值. 同样, 一个CharsetEncoder用于将字符转换回位.  

  1. 处理文本的正确方式:

 

现在我们将分析这个例子程序UseCharsets.java. 这个程序非常简单:它从一个文件中读取一些文本, 并将该文本写入另一个文件. 但是它把该数据当作文本数据, 并使用CharBuffer来将该数句读入一个CharsetDecoder中. 同样, 它使用CharsetEncoder来写回该数据.  

我们将假设字符以ISO-8859-1(Latin1)字符集(这是ASCII的标准扩展)的形式储存在磁盘上. 尽管我们必须为使用Unicode做好准备, 但是也必须认识到不同的文件是以不同的格式储存的, 而ASCII无疑是非常普遍的一种格式. 事实上, 每种Java实现都要求对以下字符编码提供完全的支持: 

   US-ASCII 
   ISO-8859-1 
   UTF-8 
   UTF-16BE 
   UTF-16LE 
   UTF-16 

  1. 示例程序:

 

   在打开相应的文件、将输入数据读入名为inputData的ByteBuffer之后, 我们的程序必须创建ISO-8859-1字符集的一个实例:

1
Charset latin1 = Charset.forName( "ISO-8859-1" );  

   然后, 创建一个解码器(用于读取)和一个编码器 (用于写入):

1
2
CharsetDecoder decoder = latin1.newDecoder();  
CharsetEncoder encoder = latin1.newEncoder();

   为了将字节数据解码为一组字符, 我们把ByteBuffer传递给CharsetDecoder, 结果得到一个CharBuffer:

1
CharBuffer cb = decoder.decode( inputData );  

   如果想要处理字符, 我们可以在程序的此处进行. 但是我们只想无改变地将它写回, 所以没有什么要做的.  

   要写回数据, 我们必须使用CharsetEncoder将它转换回字节:

1
ByteBuffer outputData = encoder.encode( cb );  

   在转换完成之后, 我们就可以将数据写到文件中了.

Java NIO bug

JDK 原生 NIO 程序的问题

JDK 原生也有一套网络应用程序 API,但是存在一系列问题,主要如下:

NIO 的类库和 API 繁杂,使用麻烦。你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。

需要具备其他的额外技能做铺垫。例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的 NIO 程序。

可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。

NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。

JDK NIO 的 Bug。例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。

官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本解决。


[参考文献]