抽象基类之二
FilterInputStream
FilterInputStream 的作用是用来“封装其它的输入流,并为它们提供额外的功能”。
它的常用的子类有BufferedInputStream和DataInputStream。
(1) BufferedInputStream
的作用就是为“输入流提供缓冲功能,以及mark()和reset()功能”。
- InputStream和Reader提供的一些移动指针的方法:
- void mark(int readlimit ); 在记录指针当前位置记录一个标记(mark)。
- boolean markSupported(); 判断此输入流是否支持mark()操作,即是否支持记录标记。
- void reset(); 将此流的记录指针重新定位到上一次记录标记(mark)的位置。
- long skip(long n); 记录指针向前移动n个字节/字符。
readlimit 参数给出当前输入流在标记位置变为非法前允许读取的字节数。
这句话的意思是说:mark就像书签一样,用于标记,以后再调用reset时就可以再回到这个mark过的地方。mark方法有个参数,通过这个整型参数,告诉系统,希望在读出多少个字符之前,这个mark保持有效。
比如说mark(10),那么在read()10个以内的字符时,reset()操作指针可以回到标记的地方,然后重新读取已经读过的数据,如果已经读取的数据超过10个,那reset()操作后,就不能正确读取以前的数据了,mark()打标记已经失效,reset()会报错。
但实际的运行情况却和JAVA文档中的描述并不完全相符。 有时候在BufferedInputStream类中调用mark(int readlimit)方法后,即使读取超过readlimit字节的数据,mark标记仍可能有效,仍然能正确调用reset方法重置。
事实上,mark在JAVA中的实现是和缓冲区相关的。只要缓冲区够大,mark后读取的数据没有超出缓冲区的大小,mark标记就不会失效。如果不够大,mark后又读取了大量的数据,导致缓冲区更新,原来标记的位置自然找不到了。
因此,mark后读取多少字节才失效,并不完全由readlimit参数确定,也和BufferedInputStream类的缓冲区大小有关。 如果BufferedInputStream类的缓冲区大小大于readlimit,在mark以后只有读取超过缓冲区大小的数据,mark标记才会失效。
public class test1 {
public static void main(String[] args) throws IOException {
byte[] b=new byte[] {1,2,3,4,5};
//把数组转为数组输入流
ByteArrayInputStream bais = new ByteArrayInputStream(b);
//进行一次封装,封装时指定缓冲区大小,先指定2个字节大小
BufferedInputStream bis = new BufferedInputStream(bais,2);
//先读出第一个字节数据出来,指针会指向第二个字节即2上面
System.out.println(bis.read()); // 1
//现在指针在2上,打一个标记
bis.mark(1);
//按官方文档来说,读第一个数据出来后,标记会失效,reset()方法会报错,事实上不会报错,经过测试,是缓冲区bis读三个数据时,
//大小缓冲区大小,缓冲区装不下了,标记才有用。
System.out.println(bis.read()); //2
System.out.println(bis.read()); //3
bis.reset();
System.out.println(bis.read()); //2
}
}
----------------------------------------------------------------------------
1
2
3
2
当连续读三个数据时,缓冲区(2个字节大小)装不下,标记才会失效
public class test1 {
public static void main(String[] args) throws IOException {
byte[] b=new byte[] {1,2,3,4,5};
//把数组转为数组输入流
ByteArrayInputStream bais = new ByteArrayInputStream(b);
//进行一次封装,封装时指定缓冲区大小,先指定2个字节大小
BufferedInputStream bis = new BufferedInputStream(bais,2);
//先读出第一个字节数据出来,指针会指向第二个字节即2上面
System.out.println(bis.read()); // 1
//现在指针在2上,打一个标记
bis.mark(1);
//按官方文档来说,读第一个数据出来后,标记会失效,reset()方法会报错,事实上不会报错,经过测试,是缓冲区bis读三个数据时,
//大小缓冲区大小,缓冲区装不下了,标记才有用。
System.out.println(bis.read()); //2
System.out.println(bis.read()); //3
System.out.println(bis.read()); //4
bis.reset();
System.out.println(bis.read()); 没有数据打印,并报错了
}
}
----------------------------------------------------------------------------
1
2
3
4
Exception in thread "main" java.io.IOException: Resetting to invalid mark
at java.io.BufferedInputStream.reset(Unknown Source)
at cn.ybzy.io.filter.test1.main(test1.java:33)
不设标记,不重设定位正常读没问题
public class test1 {
public static void main(String[] args) throws IOException {
byte[] b=new byte[] {1,2,3,4,5};
//把数组转为数组输入流
ByteArrayInputStream bais = new ByteArrayInputStream(b);
//进行一次封装,封装时指定缓冲区大小,先指定2个字节大小
BufferedInputStream bis = new BufferedInputStream(bais,2);
System.out.println(bis.read()); // 1
System.out.println(bis.read()); //2
System.out.println(bis.read()); //3
System.out.println(bis.read()); //4
System.out.println(bis.read()); //5
}
}
(2) DataInputStream
是用来装饰其它输入流,它“允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型”。
应用程序可以使用DataOutputStream(数据输出流)写入由DataInputStream(数据输入流)读取的数据。
FilterOutputStream
FilterOutputStream 的作用是用来“封装其它的输出流,并为它们提供额外的功能”。
它主要包括BufferedOutputStream, DataOutputStream和PrintStream。
(1) BufferedOutputStream的作用就是为“输出流提供缓冲功能”。
(2) DataOutputStream 是用来装饰其它输出流,将DataOutputStream和DataInputStream输入流配合使用,
“允许应用程序以与机器无关方式从底层输入流中读写基本 Java 数据类型”。
打印流PrintStream
PrintStream是用来装饰其它输出流。它能为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。
打印流提供了非常方便的打印功能,可以打印任何类型的数据信息,例如:小数,整数,字符串。
之前打印信息需要使用OutputStream但是这样,所有数据输出会非常麻烦,PrintStream可以把OutputStream类重新包装了一下,使之输出更方便。
public class PrintStreamTest {
public static void main(String[] args) throws FileNotFoundException {
FileOutputStream fos =new FileOutputStream("c:\\c.txt");
PrintStream print=new PrintStream(fos);
print.print("xiongshaowen");
print.print(123);
print.print(12.3);
print.print(new Object());
print.close();
}
}
格式化输出:
JAVA对PrintStream功能进行了扩充,增加了格式化输出功能。直接使用Print即可。但是输出的时候需要指定输出的数据类型。
public class PrintStreamTest {
public static void main(String[] args) throws FileNotFoundException {
FileOutputStream fos =new FileOutputStream("c:\\c.txt");
PrintStream ps=new PrintStream(fos);
/*print.print("xiongshaowen");
print.print(123);
print.print(12.3);
print.println(new Object());
print.close();*/
//格式打印
int i = 10;
String s="打印流";
float f = 15.5f;
ps.printf("整数 %d,字符串 %s,浮点数 %f",i,s,f);
ps.println();
ps.printf("整数 [%d],字符串 %s,浮点数 '%f'",i,s,f);
ps.close();
}
}
推回输入流的使用
通常我们使用输入流的时候,我们读取输入流是顺序读取,当读取的不是我们想要的怎么办,又不能放回去,虽然我们可以使用程序做其他的处理来解决,但是Java提供了推回输入流来解决这个问题,
推回输入流可以做这样子的事情:将已经读取出来的字节或字符数组内容推回到推回缓冲区里面,
从而允许重复读取刚刚读取的我们不想要的东西之前内容
注意:
当程序创建一个推回输入流时需要指定推回缓冲区的大小,默认的推回缓冲区长度为一,
如果程序推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发Pushback buffer overflow 异常。
程序举例:
假如C盘下有一个aa.txt文件,内容如下:我现在只想读取aaa前面的内容,后面的我不想要。
public class TuiHuistream {
public static void main(String[] args) throws IOException {
//1文件流,2字符流 3输入流
Reader reader = new FileReader("C:\\aa.txt");
PushbackReader pr = new PushbackReader(reader,1024);
//从输入流中读取数据
char[] cs = new char[5];
int hasReadCount = 0;
String sumString="";
int count=0; //计数器,看看下面读了多少次数据
while((hasReadCount=pr.read(cs))!=-1) {
String curString = new String(cs,0,hasReadCount);
sumString =sumString+curString;
count++;
int aaaIndex = sumString.indexOf("aaa"); //这里我们以'aaa’为标记,推回aaa之前的内容到缓冲区中
if(aaaIndex>-1) {
pr.unread(sumString.toCharArray()); //把所有内容推到缓冲区中(cs)
//重新把我想要的内容(即aaa之前的内容)读出来
if(aaaIndex >5) {
cs = new char[aaaIndex];//扩容缓冲区
}
pr.read(cs,0,cs.length);
System.out.println("我想要的内容为:"+new String(cs));
break;
}else {
System.out.println(new String(cs));
}
}
System.out.println("一共读了多少次:"+count+"次");
pr.close();
}
}
------------------------------------------------------------------------------------------------------------------------------
ccccc
ccccc
ccccc
cc
c
cccca
我想要的内容为:ccccccccccccccccc
ccccc
一共读了多少次:6次
//注释掉pr.unread(sumString.toCharArray());结果为
ccccc
ccccc
ccccc
cc
c
cccca
我想要的内容为:ttttttttttttttttttttttt
一共读了多少次:6次
数据输出输入流
数据流DataInputStream和DataOutputStream的使用
DataOutputStream数据输出流允许应用程序将基本Java数据类型写到基础输出流中,而DataInputStream数据输入流允许应用程序以机器无关的方式从底层输入流中读取基本的Java类型.
DataInputStream的内部方法:
read(byte b[])---从数据输入流读取数据存储到字节数组b中.
read(byte b[],int off,in len)---从数据输入流中读取数据存储到数组b里面,位置从off开始,长度为len个字节.
readFully(byte b[])---从数据输入流中循环读取b.length个字节到数组b中.
readFully(byte b[],int off,in len )---从数据输入流中循环读取len个字节到字节数组b中.从b的off位置开始放
skipBytes(int b)---跳过n个字节.
readBoolean()---从数据输入流读取布尔类型的值.
readByte()---从数据输入流中读取一个字节.
readUnsignedByte()---从数据输入流中读取一个无符号的字节,返回值转换成int类型.
readShort()---从数据输入流读取一个short类型数据.
readUnsignedShort()---从数据输入流读取一个无符号的short类型数据.
readChar()---从数据输入流中读取一个字符数据
readInt()---从数据输入流中读取一个int类型数据.
readLong()---从数据输入流中读取一个long类型的数据.
readFloat()---从数据输入流中读取一个float类型的数据.
readDouble()---从数据输入流中读取一个double类型的数据.
readUTF()---从数据输入流中读取用UTF-8格式编码的UniCode字符格式的字符串.
DataOutputStream的内部方法
intCount(int value)---数据输出流增加的字节数.
write(int b)---将int类型的b写到数据输出流中.
write(byte b[],int off, int len)---将字节数组b中off位置开始,len个长度写到数据输出流中.
flush()---刷新数据输出流.
writeBoolean()---将布尔类型的数据写到数据输出流中,底层是转化成一个字节写到基础输出流中.
writeByte(int v)---将一个字节写到数据输出流中(实际是基础输出流).
writeShort(int v)---将一个short类型的数据写到数据输出流中,底层将v转换2个字节写到基础输出流中.
writeChar(int v)---将一个charl类型的数据写到数据输出流中,底层是将v转换成2个字节写到基础输出流中.
writeInt(int v)---将一个int类型的数据写到数据输出流中,底层将4个字节写到基础输出流中.
writeLong(long v)---将一个long类型的数据写到数据输出流中,底层将8个字节写到基础输出流中.
writeFloat(flloat v)---将一个float类型的数据写到数据输出流中,底层会将float转换成int类型,写到基础输出流中.
writeDouble(double v)---将一个double类型的数据写到数据输出流中,底层会将double转换成long类型,写到基础输出流中.
writeBytes(String s)---将字符串按照字节顺序写到基础输出流中.
writeChars(String s)---将字符串按照字符顺序写到基础输出流中.
writeUTF(String str)---以机器无关的方式使用utf-8编码方式将字符串写到基础输出流中.
size()---写到数据输出流中的字节数.
例:c盘下 java.txt文件,我们输出内容到里面,再打印到控制台真实内容,文件中的数据为二进制,所以看不出来是什么。
public class DataStream {
public static void main(String[] args) throws IOException {
DataOutputStream dos = new DataOutputStream(new FileOutputStream("c:\\java.txt"));
dos.writeUTF("αα熊少文");
dos.writeInt(1234567);
dos.writeBoolean(true);
dos.writeShort((short)123);
dos.writeLong((long)456);
dos.writeDouble(99.98);
DataInputStream dis = new DataInputStream(new FileInputStream("c:\\java.txt"));
System.out.println(dis.readUTF());
System.out.println(dis.readInt());
System.out.println(dis.readBoolean());
System.out.println(dis.readShort());
System.out.println(dis.readLong());
System.out.println(dis.readDouble());
dis.close();
dos.close();
}
}
--------------------------------------------------------------------------------
αα熊少文
1234567
true
123
456
99.98
重定向标准输入或输出
Java的标准输入/输出分别通过System.in和System.out来代表,默认情况下它们分别代表键盘和显示器,
当程序通过System.in来获取输入时,实际上是从键盘读取输入,当程序试图通过System.out执行输出时,程序总是输出到屏幕。
System类里提供三个重定向标准输入/输出的方法:
static void setErr(PrintStream err):重定向“标准”错误输出流
static void setIn(InputStream in):重定向“标准”输入流
static void setOut(PrintStream out):重定向“标准”输出流
例:标准输出流重定向
public class System_in_out_x {
public static void main(String[] args) {
PrintStream ps=null;
try {
FileOutputStream fos = new FileOutputStream("c:\\eee.txt");
//打印到文件中
ps = new PrintStream(fos);
System.setOut(ps);
System.out.println("关联了关联文件,这里的打印信息,不会输出到控制台,而是输出到文件中了!");
}catch(FileNotFoundException e) {
e.printStackTrace();
}finally {
if(ps!=null)
ps.close(); //内存以外的流一定要关闭,关闭顶层流即可。
}
}
}
例:标准输入流重定向
InputStream in=null;
Scanner scanner=null;
try {
in = new FileInputStream("c:\\fff.txt");
System.setIn(in);
scanner = new Scanner(System.in);
scanner.useDelimiter("\n"); //分隔符为换行符
while(scanner.hasNext()) {
System.out.println(scanner.next());
}
}catch(FileNotFoundException e) {
e.printStackTrace();
}finally {
if(scanner!=null)
scanner.close(); //内存以外的流一定要关闭,关闭顶层流即可。
}
----------------------------------------------------------------------------
熊少文
xiongshaowen
xuhuifeng
Java程序对子进程进行读写
使用Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生一个Process对象,
Process对象代表由该Java程序启动的子进程。
Process类提供了3个方法,用于让程序和其子进程通信
InputStream getErrorStream():获取子进程的错误流。
InputStream getInputSteeam():获取子进程的输入流。
OutputStream getOutputStream():获取子进程的输出流。
例:获取javac命令的错误提示信息
public class RunnSubproess {
public static void main(String[] args) throws IOException {
Process process=Runtime.getRuntime().exec("javac");
//从p里,获取到错误信息流,返回InputStream字节输入流,为了方便读取,我们要转换流转换成字符输入流
InputStreamReader reader =new InputStreamReader(process.getErrorStream(),"GBK"); //GBK,WINDOWS中的编码格式
//进一步再封装成缓冲流,因为字符缓冲流中有一个方法,可以一次性读一行,十分方便
BufferedReader br=new BufferedReader(reader);
String buff = null;
while((buff=br.readLine())!=null) {
System.out.println(buff);
}
if(br!=null)
br.close();
}
}
----------------------------------------------------------------------------
用法: javac <options> <source files>
其中, 可能的选项包括:
-g 生成所有调试信息
-g:none 不生成任何调试信息
-g:{lines,vars,source} 只生成某些调试信息
-nowarn 不生成任何警告
-verbose 输出有关编译器正在执行的操作的消息
-deprecation 输出使用已过时的 API 的源位置
-classpath <路径> 指定查找用户类文件和注释处理程序的位置
-cp <路径> 指定查找用户类文件和注释处理程序的位置
-sourcepath <路径> 指定查找输入源文件的位置
-bootclasspath <路径> 覆盖引导类文件的位置
-extdirs <目录> 覆盖所安装扩展的位置
-endorseddirs <目录> 覆盖签名的标准路径的位置
-proc:{none,only} 控制是否执行注释处理和/或编译。
-processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
-processorpath <路径> 指定查找注释处理程序的位置
-parameters 生成元数据以用于方法参数的反射
-d <目录> 指定放置生成的类文件的位置
-s <目录> 指定放置生成的源文件的位置
-h <目录> 指定放置生成的本机标头文件的位置
-implicit:{none,class} 指定是否为隐式引用文件生成类文件
-encoding <编码> 指定源文件使用的字符编码
-source <发行版> 提供与指定发行版的源兼容性
-target <发行版> 生成特定 VM 版本的类文件
-profile <配置文件> 请确保使用的 API 在指定的配置文件中可用
-version 版本信息
-help 输出标准选项的提要
-A关键字[=值] 传递给注释处理程序的选项
-X 输出非标准选项的提要
-J<标记> 直接将 <标记> 传递给运行时系统
-Werror 出现警告时终止编译
@<文件名> 从文件读取选项和文件名
例:向子进程传递信息,子进程接收信息并保存到指定的文件中。
这里我不在开发工具中执行编译,在cmd中执行javac xxx.java编译java文件
错误: 编码GBK的不可映射字符
处理:这是windows中cmd执行javac xxx.java 编译的文件要保存为ANSI文件格式(GBK),而utf-8文件中又有中文,所以不可执行,
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.Scanner;
public class RunnSubproess {
public static void main(String[] args) throws IOException {
//调用子进程,向子进程写信息,子进程把信息保存到一个指定的文件中,这里我再定义一个类,写一个main方法,该main方法是帽本例中调用执行。
PrintStream ps =null;
try {
Process p = Runtime.getRuntime().exec("java ReceiveInfo"); //执行 java ReceiveInfo命令
OutputStream out =p.getOutputStream();
ps=new PrintStream(out); //封成打印流,方便输出
ps.println("普通字符串!");
ps.println(123456);
ps.println(new RunnSubproess());
}catch (IOException e) {
e.printStackTrace();
}finally {
if(ps!=null)
ps.close();
}
}
}
class ReceiveInfo{
public static void main(String[] args) {
PrintStream ps =null;
try {
OutputStream out=new FileOutputStream("c:\\runtime.txt");
ps=new PrintStream(out);
Scanner sc = new Scanner(System.in); //这里的System.in不是键盘输入,而是父进程的输入流,当然了它绝大多数情况下是键盘输入的
sc.useDelimiter("\n"); //以换行回车符为分隔符读一行数据
while(sc.hasNext()) { //hasNext()会阻塞线程,一直判断
ps.print(sc.next());
}
}catch(FileNotFoundException ex) {
ex.printStackTrace();
}finally {
if(ps!=null)
ps.close();
}
}
}
编译执行主类
对象序列化
序列化的含义和意义
对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流,都可以将这种二进制流恢复成原来的Java对象。
在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。
如果需要让某个对象支持序列化机制,方式有二:
序列化方式一: 实现Serializable接口,通过序列化流
实现Serializable接口,通过ObjectOutputStream和ObjectInputStream将对象序列化和反序列化。Serializable接口是标记接口,是个空接口,用于标识该类可以被序列化。
例:方式一实现Person对象序列化,保存到磁盘的一个文件中。
public class Test1 {
public static void main(String[] args) {
ObjectOutputStream oout = null;
ObjectInputStream oin =null;
try {
OutputStream out =new FileOutputStream("C:\\xiong\\aaa.obj");
oout = new ObjectOutputStream(out);
Person person1=new Person("熊少文",42,"男");
oout.writeObject(person1);
InputStream in =new FileInputStream("c:\\xiong\\aaa.obj");
oin = new ObjectInputStream(in);
Person person=(Person)oin.readObject();
System.out.println(person);
System.out.println(person.getName());
}catch(Exception e) {
e.printStackTrace();
}finally {
try {
if(oout!=null)
oout.close();
if(oin!=null)
oin.close();
}catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
class Person implements Serializable{
private String name;
private int age;
private String sex;
public Person() {
}
public Person(String name, int age, String sex) {
super();
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", sex=" + sex + "]";
}
}
------------------------------------------------------------------
Person [name=熊少文, age=42, sex=男]
熊少文
进一步分析:
当重新读取被保存的Person对象时,并没有调用Person的任何构造器,看起来就像是直接使用字节将Person对象还原出来的。当Person对象被保存到person.out文件后,可以在其它地方去读取该文件以还原对象,但必须确保该读取程序的 CLASSPATH 中包含有 Person.class
哪怕在读取Person对象时并没有显示地使用Person类,如上例所示,我们把写入注释掉,再到class类路径中删除Person.class,则读取会抛出 ClassNotFoundException。
对象引用的序列化
当程序序列化一个Teacher对象时,如果该Teacher对象持有一个Person对象的引用,为了在反序列化时可以正常恢复该Teacher对象,程序会顺带将该Person对象也进行序列化,所以Person类也必须是可序列化的,否则Teacher类将不可序列化。
Transient 关键字
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。Transient 关键字只能用于修饰Field,不可修饰Java程序中的其他成分。
序列化方式二: 实现Externalizable接口,重写writeExternal和readExternal方法
Externalizable接口继承了Serializable接口
,替我们封装了两个方法,一个用于序列化,一个用于反序列化。
这种方式是将属性序列化,注意这种方式transient修饰词将失去作用,也就是说被transient修饰的属性,只要你在writeExternal方法中序列化了该属性,照样也会得到序列化。
public class Test2 {
public static void main(String[] args) {
ObjectOutputStream oout = null;
ObjectInputStream oin =null;
try {
OutputStream out =new FileOutputStream("C:\\xiong\\aaa.obj");
oout = new ObjectOutputStream(out);
PersonMan person=new PersonMan("熊少文",42);
oout.writeObject(person);
InputStream in =new FileInputStream("c:\\xiong\\aaa.obj");
oin = new ObjectInputStream(in);
PersonMan person1=(PersonMan)oin.readObject();
System.out.println(person1);
System.out.println(person1.name);
}catch(Exception e) {
e.printStackTrace();
}finally {
try {
if(oout!=null)
oout.close();
if(oin!=null)
oin.close();
}catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
class PersonMan implements Externalizable{
private static final long serialVersionUID=-59555555L;
public String name; //字段要用public修鉓,前面例子中的序列化字段可用private修鉓
public int age;
public PersonMan() {
System.out.println("无参构造方法,反序列化时会被调用!");
}
public PersonMan(String name,int age) {
this.name = name ;
this.age = age;
}
@Override
public String toString() {
return "PersonMan [name=" + name + ", age=" + age + "]";
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
//该方法中,设置要反序列化的字段
name = (String)in.readObject();
age = in.readInt();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//该方法中设置以序列化的字段
out.writeObject(name);
out.writeInt(age);
}
}
Externalizable 继承于 Serializable,当使用该接口时,序列化的细节需要由程序员去完成。
如上所示的代码,由于实现的writeExternal()与readExternal()方法未作任何处理,
那么该序列化行为将不会保存/读取任何一个字段。这也就是为什么输出结果中所有字段的值均为空。
另外,使用 Externalizable 接口进行序列化时,读取对象会调用被序列化类的无参构造器去创建一个新的对象,
然后再将被保存对象的字段的值分别填充到新对象中,这就是为什么在此次序列化过程中Person类的无参构造器会被调用。
由于这个原因,实现 Externalizable 接口的类必须要提供一个无参构造器,且它的访问权限为public。
对上述Person类做进一步的修改,使其能够对name与age字段进行序列化,但忽略 gender 字段:
每个枚举类型都会默认继承类java.lang.Enum,而Enum类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。
readResolve()方法——单例模式的反序列化
上面的例子中,反序列化时,调用无参构造方法,再把数据流中的数据填充进去,从而整个序序列化,反序列化过程中,创建了两个对象。如果在单例模式下,这整个过程中创建的两个对象不相等。但属性值是一样的。怎么要相等呢。这就在单例中重写readResolve()方法。
例: 以序列化方式一中的Person类为基础,重写该类,使该类为单模式类,即所有过程中,只创建一个对象。
public class SingleTest {
public static void main(String[] args) {
ObjectOutputStream oout = null;
ObjectInputStream oin =null;
try {
OutputStream out =new FileOutputStream("C:\\xiong\\aaa.obj");
oout = new ObjectOutputStream(out);
SinglePerson person1=SinglePerson.getInstance("熊少文", 42, "男");
oout.writeObject(person1);
InputStream in =new FileInputStream("c:\\xiong\\aaa.obj");
oin = new ObjectInputStream(in);
SinglePerson person=(SinglePerson)oin.readObject();
System.out.println(person);
System.out.println(person.getName());
System.out.println("person1==person::"+(person1==person));
}catch(Exception e) {
e.printStackTrace();
}finally {
try {
if(oout!=null)
oout.close();
if(oin!=null)
oin.close();
}catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
class SinglePerson implements Serializable{
private String name;
private int age;
private String sex;
private static SinglePerson person = null;
private SinglePerson() { //私有无参构造方法,外界调用不了
System.out.println("无参构造方法,被调用了!");
}
private SinglePerson(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public static SinglePerson getInstance(String name,int age,String sex) {
if(person==null) {
person = new SinglePerson(name,age,sex);
}
return person;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", sex=" + sex + "]";
}
//
public Object readResolve() {
return person;
}
}
------------------------------------------------------------------------
Person [name=熊少文, age=42, sex=男]
熊少文
person1==person::true
Java序列化对象版本号
在上面的一些例中,如果没有显示添加版本号,当序列化后,我们修改对象的字段(添加删除。。。)后,再去反序列化(此时不要序列化),这时,会产生两个版本号,就会发生异常。
例:处理问题,添加版本号
通过大量实验,我是这样认为的,可以自已定义版本号,更可以让工具自动添加版本号
eclipse中,点对象类名,会有提示添加序列化版本号
IDEA中,settings----code style---inspections--选中Serializable class without 'serialVersionUID' 再 alt+enter,自动添加版本号。
class Person implements Serializable{
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String sex;
...
}
序列化的安全性
服务器端给客户端发送序列化对象数据,序列化二进制格式的数据写在文档中,并且完全可逆。
一抓包就能就看到类是什么样子,以及它包含什么内容。如果对象中有一些数据是敏感的,比如密码字符串等,则要对字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
比如可以通过使用 writeObject 和 readObject 实现密码加密和签名管理,Java提供了更好的方式。
对整个对象进行加密和签名
最简单的是将它放在一个 javax.crypto.SealedObject 和/或 java.security.SignedObject 包装器中。
两者都是可序列化的,所以将对象包装在 SealedObject 中,可以围绕原对象创建一种 “包装盒”。
必须有对称密钥才能解密,而且密钥必须单独管理。
解决上面的问题,使用SealedObject
KeyGenerator对象介绍:
keyGenerator对象位于javax.crypto包下
jdk 1.6 doc介绍:KeyGenerator 此类提供(对称加密算法:AES,DES 等等)密钥生成器的功能
获得keyGenerator:
一般是通过此类的静态方法getInstance()方法获得,
此类的全局变量都为私有变量,因此不讨论
方法:
getAlgorithm();获得算法名称
getInstance();通过指定算法,亦可指定提供者来构造KeyGenerator对象,有多个重载方法
getProvider();返回此算法实现的提供商
init(SecureRandom sRandoom);用于初始化KeyGenerator对象,通过随机源的方式
init(int size);通过指定生成秘钥的大小,来初始化的方式
init(AlgorithmParameterSpec params);通过指定参数集来初始化
init(AlgorithmParameterSpec params,SecureRandom sRandoom);通过指定参数集和随机数源的方式生成
init(int arg0, SecureRandom arg1);通过指定大小和随机源的方式产生
generatorKey();生成秘钥 // 返回SecertKey对象
支持的算法有:
AES
ARCFOUR
Blowfish
DES //DES算法为密码体制中的对称密码体制,又被称为美国数据加密标准,是1972年美国IBM公司研制的对称密码体制加密算法。
DESede //DESede是由DES对称加密算法改进后的一种对称加密算法。使用 168 位的密钥对资料进行三次加密的一种机制;
HmacMD5
HmacSHA1,HmacSHA256,HmacSHA384,HmacSHA512
RC2
Cipher为加密和解密提供密码功能。
获得Cipher:
一般是通过此类的静态方法getInstance()方法获得,
Cipher的工作模式设置在init里,工作模式有如下
public static final int ENCRYPT_MODE 用于将 Cipher 初始化为加密模式的常量。
public static final int DECRYPT_MODE 用于将 Cipher 初始化为解密模式的常量。
public static final int WRAP_MODE 用于将 Cipher 初始化为密钥包装模式的常量。
public static final int UNWRAP_MODE 用于将 Cipher 初始化为密钥解包模式的常量。
public static final int PUBLIC_KEY 用于表示要解包的密钥为“公钥”的常量。
public static final int PRIVATE_KEY 用于表示要解包的密钥为“私钥”的常量。
public static final int SECRET_KEY 用于表示要解包的密钥为“秘密密钥”的常量。
例:
public class Security {
public static void main(String[] args) {
ObjectOutputStream oout = null;
ObjectInputStream oin =null;
ObjectOutputStream objoutkey=null;
ObjectInputStream oinKey=null; //获取密钥用
try {
//加密序列化
OutputStream out =new FileOutputStream("C:\\xiong\\security.obj");
oout = new ObjectOutputStream(out);
Person person1=new Person("熊少文",42,"男");
KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede");//密钥生成器,DESede是算法
SecretKey encryptKey = keyGenerator.generateKey(); //生产一把密钥
Cipher cipher = Cipher.getInstance("DESede"); //加密对象
cipher.init(Cipher.ENCRYPT_MODE, encryptKey); //加密
SealedObject so =new SealedObject(person1, cipher); //封装加密的对象
oout.writeObject(so); //序列化对象,这个对象是加密了的对象
OutputStream outKey = new FileOutputStream("c:\\xiong\\keyfile"); //保存密钥,让别人用
objoutkey = new ObjectOutputStream(outKey);
objoutkey.writeObject(encryptKey); //序列化密钥,保存密钥到文件中(这个要悄悄的进行)
//反序列化,
InputStream in =new FileInputStream("c:\\xiong\\security.obj");
oin = new ObjectInputStream(in);
SealedObject sors = (SealedObject)oin.readObject();
InputStream getKeyin= new FileInputStream("c:\\xiong\\keyfile");
oinKey = new ObjectInputStream(getKeyin);
SecretKey openkey = (SecretKey)oinKey.readObject();
Person person2=(Person)sors.getObject(openkey);
System.out.println(person2);
}catch(Exception e) {
e.printStackTrace();
}finally {
try {
if(oout!=null)
oout.close();
if(oin!=null)
oin.close();
if(objoutkey!=null)
objoutkey.close();
if(oinKey!=null)
oinKey.close();
}catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
class Person implements Serializable{
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String sex;
private String address;
public Person() {
}
public Person(String name, int age, String sex) {
super();
this.name = name;
this.age = age;
this.sex = sex;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + ", sex=" + sex + "]";
}
}
使用SignedObject,解决序列化安全问题:
public class Security2 {
public static void main(String[] args) {
ObjectOutputStream oout = null;
ObjectInputStream oin =null;
ObjectOutputStream objoutkey=null;
ObjectInputStream oinKey=null;
try {
//加密序列化
OutputStream out =new FileOutputStream("C:\\xiong\\security2.obj");
oout = new ObjectOutputStream(out);
Person person1=new Person("熊少文",42,"男");
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); //DSA是签名算法
keyPairGenerator.initialize(1024); //密钥长度
KeyPair keyPair=keyPairGenerator.generateKeyPair();//此生成密钥一对
PrivateKey privateKey = keyPair.getPrivate(); //获取私钥
PublicKey publicKey = keyPair.getPublic(); //获取公钥
objoutkey=new ObjectOutputStream(new FileOutputStream("c:\\xiong\\publickey"));
objoutkey.writeObject(publicKey);
Signature signature = Signature.getInstance("DSA"); //签名对象
SignedObject so = new SignedObject(person1,privateKey,signature);
oout.writeObject(so);
//解密反序列化
InputStream in=new FileInputStream("c:\\xiong\\security2.obj");
oin = new ObjectInputStream(in);
SignedObject so2=(SignedObject)oin.readObject();
oinKey=new ObjectInputStream(new FileInputStream("c:\\xiong\\publickey"));
PublicKey publicKey2=(PublicKey)oinKey.readObject();
Signature verifysignature = Signature.getInstance("DSA");
if(so2.verify(publicKey2, verifysignature)) {
//内容没有被攥改
Person person2=(Person)so2.getObject();
System.out.println(person2);
}else {
System.out.println("内容被攥改了,不读!");
}
}catch(Exception e) {
e.printStackTrace();
}finally {
try {
if(oout!=null)
oout.close();
if(oin!=null)
oin.close();
if(objoutkey!=null)
objoutkey.close();
if(oinKey!=null)
oinKey.close();
}catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
要知道序列化的是什么样儿的对象(成员)
序列化并不保存静态变量
通过序列化操作,可以实现对任何可 Serializable 对象的深度复制(deep copy),
这意味着复制的是整个对象的关系网,而不仅仅是基本对象及其引用
如果父类没有实现Serializable接口,但其子类实现了此接口,那么这个子类是可以序列化的,
但是在反序列化的过程中会调用父类的无参构造函数,所以在其直接父类(注意是直接父类)中必须有一个无参的构造函数。
反序列化后,何时不是同一个对象
只要将对象序列化到单一流中,就可以恢复出与我们写出时一样的对象网,而且只要在同一流中,对象都是同一个。
否则,反序列化后的对象地址和原对象地址不同,只是内容相同
如果将一个对象序列化入某文件,那么之后又对这个对象进行修改,然后再把修改的对象重新写入该文件,
那么修改无效,文件保存的序列化的对象仍然是最原始的。
这是因为,序列化输出过程跟踪了写入流的对象,而试图将同一个对象写入流时,
并不会导致该对象被复制,而只是将一个句柄写入流,该句柄指向流中相同对象的第一个对象出现的位置。
为了避免这种情况,在后续的 writeObject() 之前调用 out.reset() 方法,这个方法的作用是清除流中保存的写入对象的记录。
nio
同步、异步、阻塞和非阻塞
1)同步(Synchronization)和异步(Asynchronous)的方式:
同步和异步都是基于应用程序所在操作系统处理IO事件所采用的方式,
比如同步:是应用程序要直接参与IO读写的操作。
异步:所有的IO读写交给搡作系统去处理,应用程序只需要等待通知。
举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,
如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,
等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。
然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。
2)阻塞(Block)和非租塞(NonBlock):
阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,
当数据没有准备的时候阻塞:往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否則一直等待在那里。
非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。
如果数据已经准备好,也直接返回。
还是上面的例子,你打电话问书店老板有没有《分布式系统》这本书,
你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,
如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了,
当然你也要偶尔过几分钟check一下老板有没有返回结果。
在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关
BIO、NIO、AIO的概述
首先,传统的 java.io包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。
交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,
线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
java.io包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。
尤其是在网络编程中,瓶颈体现的非常明显!
很多时候,人们也把 java.net下面提供的部分网络 API,
比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。
第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,
可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。
第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,
应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。
一、IO流(同步、阻塞),有Block-IO,有的资料上你可以看到BIO其实就是我们前面学的很爽的IO模型
二、NIO(同步、非阻塞)
BIO和NIO的工作模式对比:
BIO工作模式:每读一行,要用一个线程,当在网络编程中,无法想象的困难。
分析:一旦reader.readLine()方法返回,你就可以确定整行已经被读取,readLine()阻塞直到一整行都被读取
NIO工作模式:
NIO包三个核心组件
1)Channel
2)Buffer
3)Selector
1.由一个专门的线程(Selector)来处理所有的IO事件,并负责分发。
2.事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
3.线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。
核心组件的细节:
通道Channel
通道是一个对象,通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。
我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区,然后把缓冲区里的数据写入通道。
同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
在NIO中,提供了多种通道对象,而所有的通道对象都实现了Channel接口。
JAVA NIO中的一些主要Channel的实现:
FileChannel 连接到文件的通道
DatagramChannel 连接到UDP包的通道 (下门课网络编程与NIO进阶)
SocketChannel 连接到TCP网络套接字的通道(下门课网络编程与NIO进阶)
ServerSocketChannel 监听新进来的TCP连接的通道(下门课网络编程与NIO进阶)
FileChannel
Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。
FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下。
打开FileChannel
在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,
需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例
。
下面是通过RandomAccessFile打开FileChannel的示例:
RandomAccessFile aFile = new RandomAccessFile(“c:\xiong\fc.txt”, “rw”);
FileChannel inChannel = aFile.getChannel();
public class FileChannelTest {
public static void main(String[] args) throws IOException {
//实现NIO的用法
//学习的是FileChannel,使用这,首先我们要获取到FileChannel对象实例
//先实现读操作
FileChannel fc=null;
int size=0;
try {
RandomAccessFile raf = new RandomAccessFile("c:\\xiong\\fc.txt", "rw");
fc=raf.getChannel(); //还可通过InputStream、OutputStream获取实例,后面再讲
//Nio处理数据的七本模式,必须要有缓冲区
ByteBuffer buffer= ByteBuffer.allocate(48);
//读取数据,就是把Channel里数据写入缓冲区
int hasRead = fc.read(buffer);
System.out.println(hasRead);
while(hasRead!=-1) { //没读完Channel中的数据,继续读
buffer.flip(); //在这里理解为,写模式改为读模式
while(buffer.hasRemaining()) { //判断缓冲区还没有数据,有的话,从缓冲区读数据,打印到控制台
System.out.print((char)buffer.get()); //读得是字节码,中文会乱码,一次读一个字节
size++;
}
//System.out.println(" "+size+","+hasRead);
buffer.clear();
hasRead = fc.read(buffer);
System.out.println(" "+hasRead);
}
}catch(Exception e) {
e.printStackTrace();
}finally {
if(fc!=null)
fc.close();
}
}
}
----------------------------------------------------------------------------------------
48
xiongshaowenfkjsdlfjslfjsad;fsadklfkjsllfslkfslk 48
fsldalksdflkfsdlkjfklfsdlasfdasdf
xiongshaowenf 48
kjsdlfjslfjsad;fsadklfkjsllfslkfslkfsldalksdflkf 48
sdlkjfklfsdlasfdasdf
xiongshaowenfkjsdlfjslfjsa 48
d;fsadklfkjsllfslkfslkfsldalksdflkfsdlkjfklfsdla 48
owenfkjsdlfjslfjsad;fsadklfkjsllfslkfslkfsldalks 29
dflkfsdlkjfklfsdlasfdasdf
-1
例:向文件通道中写入数据
public class FileChannelWrite {
public static void main(String[] args) {
//道先我定义一个字符串str,这个就是我要写入通道的内容
String str = "fsdafjasldfkjsaldsfsadlfjasd;fasd;fsadflasdflsadfsda";
//获取文件通道
FileChannel fc=null;
try {
FileOutputStream fout = new FileOutputStream("c:\\xiong\\channelwrite.txt");
fc=fout.getChannel();
//创建缓冲区,一定要的
ByteBuffer buffer =ByteBuffer.allocate(1024);
byte[] data = str.getBytes();
System.out.println("看看data字节数组中的内容:"+Arrays.toString(data)); //全部是数字
for(int i=0;i<data.length;i++) {
buffer.put(data[i]);
}
buffer.flip(); //把缓冲区的定入模式,转为读模式,主要是循环要用
while(buffer.hasRemaining()) {
fc.write(buffer);
}
}catch(Exception e) {
e.printStackTrace();
}finally {
if(fc!=null) {
try {
fc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
注意FileChannel.write()是在while循环中调用的
。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。
关闭FileChannel
用完FileChannel后必须将其关闭。如:
channel.close();
FileChannel的position方法
有时可能需要在FileChannel的某个特定位置进行数据的读/写操作。
可以通过调用position()方法获取FileChannel的当前位置。
也可以通过调用position(long pos)方法设置FileChannel的当前位置。
这里有两个例子:
long pos = channel.position();
channel.position(pos +123);
如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。
缓冲区Buffer
缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO中,所有数据都是用缓冲区处理的。
在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;
任何时候访问 NIO 中的数据,都是将它放到缓冲区中。
而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。
在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,
对于Java中的基本类型,基本都有一个具体Buffer类型与之相对应,它们之间的继承关系如下图所示:
缓冲区基础api
理论上,Buffer是经过包装后的数组,而Buffer提供了一组通用api对这个数组进行操作。
1.1、属性:
容量(Capacity)
缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
上界(Limit)
缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。
位置(Position)
下一个要被读或写的元素的索引。位置会自动由相应的get( )和put( )函数更新,即加1。
标记(Mark)
一个备忘位置。调用mark( )来设定mark = postion。
调用reset( )设定position = mark。
标记在设定前是默认值是-1。
这四个属性之间总是遵循以下关系: mark <= position <= limit <= capacity 让我们来看看这些属性在实际应用中的一些例子。-1不指向任何东西。
新创建一个Buffer的初始情况
2、缓冲区API
public final int capacity( ) :返回缓冲区的容量
public final int position( ) :返回缓冲区的位置
public final Buffer position (int newPosition):设置缓冲区的位置
public final int limit( ) :返回缓冲区的上界
public final Buffer limit (int newLimit) :设置缓冲区的上界
public final Buffer mark( ) :设置缓冲区的标记
public final Buffer reset( ) :将缓冲区的位置重置为标记的位置
public final Buffer clear( ) :清除缓冲区
public final Buffer flip( ) :反转缓冲区(如:前面的写转读)
public final Buffer rewind( ) :重绕缓冲区,即positon初始化为0
public final int remaining( ) :返回当为位置与上界之间的元素数
public final boolean hasRemaining( ) :缓存的位置与上界之间是否还有元素
public abstract boolean isReadOnly( ):缓冲区是否为只读缓冲区
public class BufferApiTest {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(9);
System.out.println("新创建buffer时,初始情况: ");
System.out.println("容量:"+buffer.capacity());
System.out.println("上界:"+buffer.limit());
System.out.println("位置:"+buffer.position());
System.out.println("位置移到第五个时:");
buffer.position(5);
System.out.println("位置:"+buffer.position());
System.out.println("在第五位置,打个标记,再移到第八时,再再reset()后,定到标志位置时:");
buffer.mark();
buffer.position(8);
buffer.reset(); //定到标记位置
System.out.println("位置:"+buffer.position());
}
}
------------------------------------
新创建buffer时,初始情况:
容量:9
上界:9
位置:0
位置移到第五个时:
位置:5
在第五位置,打个标记,再移到第八时,再再reset()后,定到标志位置时:
位置:5
1.3、存取API
public abstract byte get( ):获取缓冲区当前位置上的字节,然后增加位置;
public abstract byte get (int index):获取指定索引中的字节;
public abstract ByteBuffer put (byte b):将字节写入当前位置的缓冲区中,并增加位置;
public abstract ByteBuffer put (int index, byte b):将自己写入给定位置的缓冲区中;
1.4、翻转,flip( )函数
void flip( ):将一个能够继续添加数据元素的填充状态的缓冲区翻转成一个准备读出元素的释放状态。
翻转前:
翻转后:
flip()源码如下:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
Rewind()函数与flip()相似,但不影响上界属性。
Rewind()源码如下:
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
1.5、释放-get()、clear()、hasRemaining()和remaining()函数
如果接收到一个在别处被填满的缓冲区,检索内容之前需要将其翻转。
例如,如果一个通道的read()操作完成,要查看被通道放入缓冲区内的数据,那么您需要在调用get()之前翻转缓冲区。
布尔函数hasRemaining()会在释放缓冲区时判断是否已经达到缓冲区的上界。
以下是一种将数据元素从缓冲区释放到一个数组的方法。
for (int i = 0; buffer.hasRemaining( ), i++) {
myByteArray [i] = buffer.get( );
}
作为选择,remaining()函数将告知您从当前位置到上界还剩余的元素数目。
for (int i = 0; buffer.remaining( )>0, i++) {
myByteArray [i] = buffer.get( );
}
缓冲区并不是多线程安全的。
如果您想以多线程同时存取特定的缓冲区,您需要在存取缓冲区之前进行同步。
一旦缓冲区对象完成填充并释放,它就可以被重新使用了。
clear()函数将缓冲区重置为空状态。
它并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设回0。
1.6 、压缩,compact()函数,应用的位置,在clear()的地方,需要的时候,替换clear()方法来用的。
有时,只想从缓冲区中释放一部分数据,而不是全部,然后重新填充。
为了实现这一点,未读的数据元素需要下移至第一个元素位置,即索引为0。
尽管重复这样做会效率低下,但这有时非常必要,而API对此提供了一个compact()函数。这一缓冲区工具在复制数据时要比使用get()和put()函数高效得多。所以当需要时,可以使用compact()。
1.7、标记,mark( )和reset( )函数
标记,使缓冲区能够记住一个位置并在之后将其返回。
缓冲区的标记在mark( )函数被调用之前是未定义的,调用时标记被设为当前位置的值。
reset( )函数将位置设为当前的标记值。
如果标记值未定义,调用reset( )将导致InvalidMarkException异常。
一些缓冲区函数会抛弃已经设定的标记(rewind( ),clear( ),以及flip( )总是抛弃标记)。
如果新设定的值比当前的标记小,调用limit(索引参数)或position(索引参数)会抛弃标记。
1.8、比较,equals()和compareTo( )函数
有时候比较两个缓冲区所包含的数据是很有必要的。
所有的缓冲区都提供了一个常规的equals( )函数用以测试两个缓冲区的是否相等,以及一个compareTo( )函数用以比较缓冲区哪个大,哪个小。
public boolean equals (Object ob)
public int compareTo (Object ob)
两个缓冲区被认为相等的充要条件是:
1.两个对象类型相同。包含不同数据类型的buffer永远不会相等,而且buffer绝不会等于非buffer对象。
2.两个对象都剩余同样数量的元素。Buffer的容量不需要相同,而且缓冲区中剩余数据的索引也不必相同,即 limit-positon 相等。
但每个缓冲区中剩余元素的数目(从位置到上界)必须相同。
3.在每个缓冲区中应被get()函数返回的剩余数据元素序列必须一致。
如果不满足以上任意条件,就会返回false。
相等的缓冲区:
不相等缓冲区
缓冲区也支持用compareTo( )函数以词典顺序进行比较。
A.compareTo(B),A小于B,A等于B,A大于B分别返回一个负整数,0和正整数。即A的limit-position与B的limit-position大小比较,如果差相同,则比较position位置,position位置大的就大,若上两者都相同,再比较内容了。
与equals( )相似,compareTo( )不允许不同对象间进行比较。
但compareTo( )更为严格:如果您传递一个类型错误的对象,它会抛出ClassCastException异常,但equals( )只会返回false。
比较是针对每个缓冲区内剩余数据进行的,与它们在equals( )中的方式相同,直到不相等的元素被发现或者到达缓冲区的上界。
如果一个缓冲区在不相等元素发现前已经被耗尽,较短的缓冲区被认为是小于较长的缓冲区。
public static void main(String[] args) {
ByteBuffer buffer1 = ByteBuffer.allocate(9);
ByteBuffer buffer2 = ByteBuffer.allocate(9);
System.out.println("初始时一定相等:"+buffer1.equals(buffer2));
buffer1.put("123".getBytes());
System.out.println("buffer1写入123后,相等吗?:"+buffer1.equals(buffer2));
buffer2.put("456".getBytes());
System.out.println("buffer2写入456后,相等吗?:"+buffer1.equals(buffer2));
//上行代码,竞然返回是true,为什么呢,这是因为两个缓冲区的大小相等,即两者的limit到position之间的内容是0值一样的
//我们要flip()一下,才可比较内容是否相等
buffer1.flip();
buffer2.flip();
System.out.println("buffer1.filp()buffer2.flip()后,相等吗?:"+buffer1.equals(buffer2));
System.out.println("buffer1比buffer2大小,返回值:若负数表示前者大,0相等,正后者大:::::"+buffer1.compareTo(buffer2));
}
------------------------------------------------------------------
初始时一定相等:true
buffer1写入123后,相等吗?:false
buffer2写入456后,相等吗?:true
buffer1.filp()buffer2.flip()后,相等吗?:false
buffer1比buffer2大小,返回值:若负数表示前者大,0相等,正后者大:::::-3
1.9、批量移动,get(带数组参数)和put(带数组参数/字符串)
buffer API提供了向缓冲区内外批量移动数据元素的函数:不推荐用
public CharBuffer get (char [] dst) :
public CharBuffer get (char [] dst, int offset, int length)
public final CharBuffer put (char[] src)
public CharBuffer put (char [] src, int offset, int length)
public CharBuffer put (CharBuffer src)
public final CharBuffer put (String src)
public CharBuffer put (String src, int start, int end)
有两种形式的get(xxx)可供从缓冲区到数组进行的数据复制使用。
第一种形式只将一个数组作为参数,将一个缓冲区释放到给定的数组。
第二种形式使用offset和length参数来指定目标数组的子区间。
批量的用get方法释放缓冲区里的数据可以能会发生异常:
如果所要求的数量的数据不能被传送,那么不会有数据被传递,缓冲区的状态保持不变,
同时抛出BufferUnderflowException异常。
换个说法:如果缓冲区中的数据不够完全填满数组,你会得到一个BufferUnderflowException异常。
这意味着如果你想将一个小型缓冲区传入一个大型数组,你需要明确地指定缓冲区中剩余的数据长度。
缓冲区的创建
public static CharBuffer allocate (int capacity):创建容量为capacity的缓冲区
public static CharBuffer wrap (char [] array) :将array数组包装成缓冲区
public static CharBuffer wrap (char [] array, int offset, int length) :将array包装为缓冲区,position=offset,limit=lenth
public final boolean hasArray( ) :判断缓冲区底层实现是否为数组
public final char [] array( ) :返回缓冲中的数组
public final int arrayOffset( ) :返回缓冲区数据在数组中存储的开始位置的偏移量;
新的缓冲区是由分配或包装两种操作方式创建的。
分配操作创建一个缓冲区对象并分配一个容量大小的私有的空间来储存数据元素。
包装操作创建一个缓冲区对象,但是不分配任何空间来储存数据元素。
它使用所提供的数组作为存储空间来储存缓冲区中的数据元素。
要分配一个容量为100个char变量的Charbuffer:
CharBuffer charBuffer = CharBuffer.allocate (100);
这段代码隐含地从堆空间中分配了一个char型数组作为备份存储器来储存100个char变量。
如果您想提供您自己的数组用做缓冲区的备份存储器,请调用wrap()函数:
char [] myArray = new char [100];
CharBuffer charbuffer = CharBuffer.wrap (myArray);
这段代码构造了一个新的缓冲区对象,但数据元素会存在于数组中。这意味着通过调用put()函数造成的对缓冲区的改动会直接影响这个数组,而且对这个数组的任何改动也会对这个缓冲区对象可见。
带有offset和length作为参数的wrap()函数版本则会构造一个按照提供的offset和length参数值初始化位置和上界的缓冲区。
间接缓冲区
通过allocate()或者wrap()函数创建的缓冲区通常都是间接的。
间接的缓冲区使用备份数组,像我们之前讨论的,可以通过上面列出的API函数获得对这些数组的存取权。
Boolean型函数hasArray()告诉您这个缓冲区是否有一个可存取的备份数组。
如果这个函数的返回true,array()函数会返回这个缓冲区对象所使用的数组存储空间的引用。
如果hasArray()函数返回false,不要调用array()函数或者arrayOffset()函数。
如果您这样做了您会得到一个UnsupportedOperationException异常。
如果一个缓冲区是只读的,它的备份数组将会是超出上界的,即使一个数组对象被提供给wrap()函数。
调用array()函数或者arrayOffset()会抛出一个ReadOnlyBufferException异常,来阻止您得到存取权来修改只读缓冲区的内容。
如果您通过其它的方式获得了对备份数组的存取权限,对这个数组的修改也会直接影响到这个只读缓冲区。
最后一个函数,arrayOffset(),返回缓冲区数据在数组中存储的开始位置的偏移量。
如果您使用了带有三个参数的版本的wrap()函数来创建一个缓冲区,就是截取原本数组中的一部分来做缓冲区的存储空间,
这个数组偏移量和缓冲区容量值会告诉您数组中哪些元素是被缓冲区使用的。
视图缓冲区
缓冲区不限于能管理数组中作为缓冲区的数据元素,它们也能管理其他缓冲区中的数据元素。不管用什么方式创建的缓冲区,系统都会分一个数组与之对应。
当一个管理其他缓冲区所包含的数据元素的缓冲区被创建时,这个缓冲区被称为视图缓冲区。
Duplicate():
Duplicate()函数创建了一个与原始缓冲区相似的新缓冲区,相当于复制。两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的位置,上界和标记属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。
这一副本缓冲区具有与原始缓冲区同样的数据视图。
如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。
asReadOnlyBuffer():
asReadOnlyBuffer() 函数来生成一个只读的视图缓冲区。
这与duplicate() 相同,除了这个新的缓冲区不允许使用 put(),并且其 isReadOnly() 函数将会返回true。
如果一个只读的缓冲区与一个可写的缓冲区共享数据,或者有包装好的备份数组,
那么对这个可写的缓冲区或直接对这个数组的改变将反映在所有关联的缓冲区上,包括只读缓冲区。
slice():
分割缓冲区与复制相似,但 slice() 创建一个从原始缓冲区的当前 position 开始的新缓冲区,
并且其容量是原始缓冲区的剩余元素数量(limit - position)。
这个新缓冲区与原始缓冲区共享一段数据元素子序列。分割出来的缓冲区也会