IO模型与NIO基础二
抽象基类之二
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 操作基于事件和回调机制,可以简单理解为,
应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。