1. NIO
1.1. 说明:在新的I/O系统当中,我们将主要使用Channel和Buffer来描述我们底层的操作。
1.2. 模型:
1.3. 对Channel进行读写:
/**
* @author cenyongh@mails.gscas.ac.cn
*/
public class CopyFile {
public static void main(String[] args) throws Exception {
String in = args[0];
String out = args[1];
FileInputStream fis = new FileInputStream(in);
FileOutputStream fos = new FileOutputStream(out);
FileChannel inc = fis.getChannel();
FileChannel outc = fos.getChannel();
ByteBuffer bb = ByteBuffer.allocate(1024);
while (true) {
int ret = inc.read(bb);
if (ret == -1) {
break;
}
bb.flip();
outc.write(bb);
bb.clear();
}
}
}
注:我们并没有直接对Channel进行读写,而是通过Buffer来对Channel进行间接操作。这里有两个地方要注意,就是我们在拷贝的过程当中调用了flip()和clear()方法,这两个方法的作用,将在后面讲解。
1.4. 手工填充Buffer
/**
* @author cenyongh@mails.gscas.ac.cn
*/
public class WriteFile {
public static void main(String[] args) throws Exception {
String out = args[0];
String in = args[0];
FileInputStream fin = new FileInputStream(in);
FileOutputStream fout = new FileOutputStream(out);
FileChannel inc = fin.getChannel();
FileChannel outc = fout.getChannel();
ByteBuffer bb = ByteBuffer.allocate(256);
for (int i = 0; i < 256; i++)
bb.put((byte) i);
bb.flip();
outc.write(bb);
bb.clear();
inc.read(bb);
bb.flip();
for (int i = 0; i < bb.limit(); i++) {
System.out.println(bb.get());
}
}
}
注:通过调用Buffer上的put()和get()方法,我们可以手工的往Buffer当中填充数据。
1.5. Buffer的状态量。
Buffer主要使用三个状态量position,limit,capacity来标记底层的状态。其中capacity表征Buffer的最大容量,这个值在Buffer被分配时设定,一般不会随着操作改变。position表征Buffer的当前读写位置,不管是读操作还是写操作,都会导致position的增加。limit表征Buffer的最大可读写位置,limit总是小于或等于capacity。
1.5.1. 结构图:
1.5.2. flip()和clear()操作
flip(){
limit = position;
postion = 0;
}
clear(){
limit = capacity;
position = 0;
}
1.5.3. 例子:
/**
* @author cenyongh@mails.gscas.ac.cn
*/
public class CopyFile {
public static void main(String[] args) throws Exception {
String in = args[0];
String out = args[1];
FileInputStream fis = new FileInputStream(in);
FileOutputStream fos = new FileOutputStream(out);
FileChannel inc = fis.getChannel();
FileChannel outc = fos.getChannel();
ByteBuffer bb = ByteBuffer.allocate(1024);
inc.read(bb);
show(bb, "After read");
bb.flip();
show(bb, "After flip");
outc.write(bb);
show(bb, "After write");
bb.clear();
show(bb, "After clear");
}
public static void show(ByteBuffer bb, String msg) {
System.out.println(msg + " p:" + bb.position() + " l:" + bb.limit()
+ " c:" + bb.capacity());
}
}
输出: After read p:1024 l:1024 c:1024
After flip p:0 l:1024 c:1024
After write p:1024 l:1024 c:1024
After clear p:0 l:1024 c:1024
注:在进行read()操作时,程序将尽量的填充从position到limit之间的空间。在进行write()操作时,
程序将读出从position到limit之间的空间。所以,在调用完read()操作以后,要进行其他操作以前,
我们必须要调用flip()操作,使得position的位置回指到开头;而当调用完write()操作以后,应调
用clear()操作,这一方面使得position回指到开头,同时使得limit到达Buffer最大的容量处。
1.6. 子Buffer
当在Buffer上面调用slice()操作时,将单独划出在[position,limit)之间的一段Buffer作为子Buffer,子Buffer与父Buffer使用相同的空间,但维护各自的状态量。
1.6.1. 结构图:
1.6.2. 例子:
ByteBuffer original = ByteBuffer.allocate( 8 );
original.position( 2 );
original.limit( 6 );
ByteBuffer slice = original.slice();
1.7. 其他类型的Buffer
我们可以把最基本的ByteBuffer包装成其他的CharBuffer,FloatBuffer等。
1.7.1. 结构图:
1.7.2. 例子:
ByteBuffer buffer = ByteBuffer.allocate( size );
FloatBuffer floatBuffer = buffer.asFloatBuffer();
1.8. 在ByteBuffer上的多格式读取
在对ByteBuffer进行读取时,除了可以按照固定间隔的读取方式以外,我们也可以按照变长的方式读取。
1.8.1. 例子:
fch.read( bb );
bb.flip();
byte b0 = bb.get();
short s0 = bb.getShort();
byte b1 = bb.get();
float f0 = bb.getFloat();
1.9. 非阻塞I/O
在实现基于TCP/UDP的聊天服务器时,为了节省资源我们可以使用轮训技术。而为了让服务器不永远阻塞在accept()方法上,我们可以设置一个等待超时值。而通过使用Selector类,我们可以让以上方法更容易,更高效的得到实现。
public class ServerStub implements Runnable{
private Selector selector = null;
private int port = 0;
…
public void run() {
started = true;
try {
selector = Selector.open();
ServerSocketChannel schannel = ServerSocketChannel.open();
schannel.configureBlocking(false);
ServerSocket ssocket = schannel.socket();
ssocket.bind(new InetSocketAddress("127.0.0.1", port));
schannel.register(selector, SelectionKey.OP_ACCEPT);
Set<SelectionKey> sks = null;
int keys = 0;
while (started) {
keys = selector.select();
if (keys > 0) {
sks = selector.selectedKeys();
Iterator<SelectionKey> it = sks.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isReadable()) {
sender = (SocketChannel) key.channel();
String msg = receive(sender);
} else if (key.isAcceptable()) {
SocketChannel sc = schannel.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
} else {
emit("Something Abnormal");
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
…
}
注:Selector的使用,使得服务器能以一种事件响应的方式对客户端的连接进行监听。通过
SelectionKey提供的常量,管道可以注册他说感兴趣的事件,对于ServerSocketChannel他
只能注册OP_ACCEPT事件。当用户调用selector.select()方法时,线程将会被阻塞,直到某
些事件发生了。然后用户判断发生的事件类型并进行对应的操作。这里有几点需要注意,第一
是需要使用Selector的Channel需要设置为非阻塞模式(configureBlocking(false)),第二
是用户需要手工的把已处理的SelectionKey,从集合中移除。
public class ClientStub implements Runnable {
private Selector selector = null;
private SocketChannel channel = null;
private boolean started = false;
…
public void run() {
started = true;
try {
selector = Selector.open();
Selector sel = Selector.open();
channel = SocketChannel.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ
| SelectionKey.OP_CONNECT);
channel.connect(addr);
Set<SelectionKey> sks = null;
int keys = 0;
while (started) {
keys = selector.select();
if (keys > 0) {
sks = selector.selectedKeys();
Iterator<SelectionKey> it = sks.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isReadable()) {
String msg = receive(channel);
} else if (key.isConnectable()) {
channel.finishConnect();
key.interestOps(SelectionKey.OP_READ);
} else {
emit("Something Abnormal");
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
…
}
注:客户端程序的实现与服务器端的基本相似。唯一的一点区别就是,客户端一开始注册了两个事件类型OP_READ和OP_CONNECT,而当客户端连接上服务器以后,他将会收到一个isConnectable的SelectionKey。在这里我们需要先调用finishConnect()方法,然后由于我们不再需要监听连接事件,因此我们需要修改Channel在Selector上的监听事件类型,这需要调用interestOps()操作来完成,其中方法的参数就是我们所需要的新的事件类型,这一步骤非常重要。
1.10. 编码与解码
J2SDK 1.4提供了专门用于进行编/解码的类,CharsetEncoder和CharstDecoder。
public CharBuffer decode(ByteBuffer bb){
Charset c = Charset.forName("gb2312");
CharsetDecoder cd = c.newDecoder();
CharBuffer cb = cd.decode(bb);
return cb;
}
注:编码(CharsetEncoder)的方法与解码的类似。
2. Image I/O
J2SDK 1.4提供了专门用于图片读写的类。ImageIO。如果我们只是想简单的读取或输出图片的话,那么我们可以直接使用ImageIO提供的static方法。而如果我们想对图片的读/写进行更多的控制的话,我们可以使用ImageReader和ImageWriter,以及与图片读写相关的一系列Listener。
public Image readImage(String filename){
BufferedImage bi = ImageIO.read(new File(filename));
return bi;
}
public void writeImage(){
BufferedImage bi = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = bi.createGraphics();
// 绘图操作
ImageIO.write(bi, "jpeg",new File("pic.jpg"));
}
3. Log
J2SDK 1.4提供了专门用于书写日志的类,Logger及其相关的Handler,Filter和Formatter。
3.1. 结构图:
在Logger系统当中,我们需要先获取一个Logger实例,然后通过调用Logger上的日志方法,我们将产生一个LogRecord实例,该实例将会被传送到在Logger上注册的所有Handler内,然后Handler使用他内部的Filter对象,以判断是否要处理该LogRecord记录,如果要处理的话,则把LogRecord传递给Formatter,让他对输出格式进行格式化。
public class Test{
public static void main(String[] args){
Logger log = Logger.getLogger("Test");
StreamHandler sh = new StreamHandler(System.out, new SimpleFormatter());
log.addHandler(sh);
log.info("Hello World");
}
}
输出: 2005-3-12 1:06:25 nick.log.Test main
信息: Hello World
2005-3-12 1:06:25 nick.log.Test main
信息: Hello World
说明:由于Logger机制会递归的调用父类的Logger,因此,这里输出了两份日志记录。
3.2. 自定义Handler,Filter,Formatter
public class TestFormatter extends Formatter {
public String format(LogRecord record) {
return "INFO MESSAGE:" + record.getMessage();
}
}
public class TestFilter implements Filter {
public boolean isLoggable(LogRecord record) {
if (record.getLevel() == Level.INFO)
return true;
else
return false;
}
}
public class TestHandler extends Handler {
public void publish(LogRecord r) {
if (!isLoggable(r))
return;
System.out.println(getFormatter().format(r));
}
public void close() throws SecurityException {}
public void flush() {}
}
public class Test {
public static void main(String[] args) {
Logger log = Logger.getLogger("Test");
log.setLevel(Level.ALL);
log.setUseParentHandlers(false);
TestHandler th = new TestHandler();
th.setFilter(new TestFilter());
th.setFormatter(new TestFormatter());
log.addHandler(th);
log.info("info");
log.fine("fine");
}
}
输出:INFO MESSAGE:info
说明:在主程序里面,我们调用了setUseParentHandlers(false)方法,这样做是为了禁止当前
的Logger调用其父类Logger,默认情况下该值为true。
3.3. 默认Handler及其配置
Log系统提供了五个默认Handler的实现:FileHandler,ConsoleHandler,MemoryHandler,SocketHandler,StreamHandler。通过配置文件,我们可以设定其默认属性。而通过在System.setProperty()方法里面设定“java.util.loggin.config.file”的值,可以指定配置文件的位置,默认情况下系统使用/jre/lib/logging.properties作为配置文件。
FileHandler
ConsoleHandler
MemoryHandler
SocketHandler
StreamHandler
level
y
y
y
Y
Y
filter
y
y
y
Y
Y
formatter
y
y
y
Y
encoding
y
y
y
Y
limit
y
count
y
pattern
y
append
y
size
y
push
y
target
y
host
Y
port
Y
logging.properties的内容:
nick.log.level = WARNING
public class Test {
public static void main(String[] args) {
System.setProperty("java.util.logging.config.file",
"./logging.properties");
Logger log = Logger.getLogger("nick.log");
System.out.println(log.getLevel());
log.setUseParentHandlers(false);
StreamHandler sh = new StreamHandler(System.out, new SimpleFormatter());
log.addHandler(sh);
log.warning("warning");
log.info("info");
log.fine("fine");
}
}
输出: WARNING
2005-3-12 1:05:44 nick.log.Test main
警告: warning
4. 正则表达式
J2SDK 1.4引入了对正则表达式的支持。这主要包括Pattern和Matcher类。
public class Test {
public static void main(String[] args) {
Pattern p = Pattern.compile("\\S+\\s");
String inputString = "well, hey there feller";
Matcher matcher = p.matcher(inputString);
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
String matched = inputString.substring(start, end);
System.out.println(matched);
}
System.out.println("===== Using Group: =====");
matcher.reset();
while (matcher.find()) {
String matched = matcher.group();
System.out.println(matched);
}
}
}
输出: well,
hey
there
===== Using Group: =====
well,
hey
there
说明:Pattern对需要进行识别的模式进行编译,这可以提高之后的识别速度。在使用Pattern
时有一点要特别注意,就是正则表达式单中,大量的使用以“\”开头的符号,所以为了在Pattern
中表示“\S”我们需要写成“\\S”。而当中的加号并不是表示连接,而是表示“1此或多次”
上述程序演示了如何使用一个模式去识别一个字符串,并提取每一个匹配的串。
4.1. 捕获组(Capturing Group)
在Pattern当中的正则表达当中,通过使用括号,我们可以在原来的表达式当中定义子表达式,或者称为Capturing Group。通过Matcher,我们还可以直接提取某一个Capturing Group的内容。
public class Test {
public static void main(String[] args) {
Pattern p = Pattern.compile("\\S+\\s+(\\S+)\\s+\\S+");
String inputString = "hey there feller";
Matcher matcher = p.matcher(inputString);
while (matcher.find()) {
int start = matcher.start(1);
int end = matcher.end(1);
String matched = inputString.substring(start, end);
System.out.println(matched);
}
System.out.println("===== Using Group: =====");
matcher.reset();
while (matcher.find()) {
String matched = matcher.group(1);
System.out.println(matched);
}
}
}
输出: there
===== Using Group: =====
there
说明:Capturing Group的编号是从1开始的。组号为0的组表示整个串。
4.2. 替换
Matcher提供用于替换的方法。一种是简单进行查找替换,使用replaceAll()方法。第二种更加灵活的方式,在使用的时候可以结合Capturing Group。
public class Test {
public static void main(String[] args) {
Pattern p = Pattern.compile("\\S+\\s");
String inputString = "hey there feller";
Matcher matcher = p.matcher(inputString);
String ns = matcher.replaceAll("Hello ");
System.out.println(ns);
}
}
输出:Hello Hello feller
public class Test {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("\\(((\\w|\\s)*)\\)");
String inputString = "These should be (square brackets).(hello)";
StringBuffer sb = new StringBuffer();
Matcher matcher = pattern.matcher(inputString);
while (matcher.find()) {
matcher.appendReplacement(sb, "[$1]");
}
matcher.appendTail(sb);
String newString = sb.toString();
System.out.println(newString);
}
}
输出:These should be [square brackets].[hello]
说明:这种方式的替换,由于加入了Capturing Group。所以比之前的方法更加灵活。在appendReplacement()方法中,我们使用第二个参数的内容,替换匹配的部分。而$X则是用于引用对应的Capturing Group的值。