目录
I/O操作
在所有的编程语言当中,I/O操作都是绕不过去的。其中I叫做“输入”--Input,O叫做“输出”--Output。
流模型
在Java的I/O设计当中,把输入输出设计成了所谓“流模型”。流模型描述的是,所有的数据传递都是在“数据源”与“目的地”之间,建立一个管道,然后数据像流水一样通过这跟管道进行传递。
所以在操作Java的I/0操作时,我们都必须要考虑好3个要点:源是谁?目的地是谁?管道是什么管道?
I/O流的分类
从传递方向上分为:输入流和输出流
从管道粗细上分为:字节流和字符流
由上面的两种分类组合成了I/O流当中的4大父类:
输入字节流:InputStream
输出字节流:OutputStream
输入字符流:Reader
输出字符流:Writer
这4大父类是4个抽象类,他们定义了所有流操作的统一的方法外观,然后让各自的子类去实现自己的内部细节。我们真正要用是用子类。
Java在设计子类的名称的时候,循序了一个很任性化的设计方案,那就是子类名很明确的表示了这个类的使用场景。
比如:FileInputStream -- 数据源是File,目的地是程序,管道是字节
FileReader -- 数据源是File,目的地是程序,管道是字符
FileOutputStream -- 数据源是程序,目的地是文件,管道是字节
FileWriter -- 数据源是程序,目的地是文件,管道是字符
I/O流的操作步骤
1、选择流类型
通过传递的数据类型选择
如果是文本数据,选择字符流,
如果是二进制数据,选择字节流。
通过传递的方向选择输入还是输出
最后通过另一个端点到底是谁,选择合适的子类流。
2、产生流对象 -- new出来
3、操作流对象 -- read 或 write
4、关闭流(重要)
提示:一旦涉及到输入输出操作,一定会有各种编译时异常需要我们提前处理。
文件拷贝
try-with-resource
在所有的I/O操作中,最讨厌的一件事情就是,我们需要再catch后面加上finally代码,然后固定的执行流的关闭操作。用了几个流就要关几个,每次调用它们的close方法的时候又有try-catch语句。
在JDK7当中,提出了新的语法"try-with-resource"。它的效果是当我们在try块中操作需要关闭的资源(不仅仅是针对I/O流操作,包括以后各位要常用的数据库链接等等)的时候,可以自己去调用close行为。
语法:
try( 产生资源对象--new动作 ){
使用资源对象
}catch(异常){
异常处理
}
与普通的try-catch-finally在使用的变化点就两个:
1、try后面多打上一对(),里面new资源对象;
2、不用写资源的关闭动作了。
当我们需要用到多个要求最终关闭的资源,那么只需要在try的()里面new出它们多个,中间用";"分隔。
try(new出第一个资源;
new出第二个资源;
......){
}catch(异常){
异常处理
}
自定义的资源类如何使用try-with-resource
要想让我们自定义的资源类同样能够享有try-with-resource的待遇,那么我们必须让这个类实现一个java.lang.AutoCloseable的接口。 该接口只提供了一个方法:close方法,供实现类重写。同时,这个接口不仅仅只是提供统一的关闭行为外观,还起到了标识接口的作用。
public class MyRs implements AutoCloseable{
@Override
public void close(){
}
}
通过实际的测试,我们发现,这个close行为在try块当中执行的时候不管是否发生异常,都一定会被执行,其效果与卸载finally当中是一样的。
try-with-resource的本质 其实它就是一个语法糖,通过编译后的class文件,我们反编译回来看代码,发现编译后其实没有try-with-resource的语法了。 这说明这种语法仅存在于源代码文件当中,编译后它还是普通的try-catch语句,然后在try的最后和catch的最后都有一句close的代码。
注意
1、如果在try的()内有多个资源,那么这些资源的关闭顺序应该是从后往前的;
2、如果我们使用了多个资源,且这多个资源有对接的效果。那么要注意,最好是给每个资源单独new出来,不要直接使用拼接new对象的方式。(参见下一个小结)如果采用拼接new的方式,那么只有第一根管道会被关闭,后面接的那根会被漏掉;所以最好给单独申明。
3、try-with-resource当中如果发生异常,那么是先执行close动作,然后再执行catch块当中的代码。而传统的try-catch-finally,如果发生异常,是先执行catch块当中的代码,然后再关闭close动作。
4、try-with-resource的语法当中,还是允许在最后添加finally块。只不过在这个finally块当中,我们不用再进行关闭资源的动作了,可以根据需要书写其他的“必须要执行的动作”。
I/O操作 示例:
public static void main(String[] args) {
String poem = """
床前明月光,
疑是地上霜。
""";
System.out.println(poem);
writeFile(poem);
readFile();
copyFile();
}
public static void writeFile(String str){
FileWriter fw = null;
try {
fw = new FileWriter("poem.txt");
fw.write(str);
} catch (IOException e) {
e.printStackTrace();
}finally{
if(fw != null){
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void readFile() {
try(FileReader fr = new FileReader("poem.txt")) {
int in = 0;
while ((in = fr.read()) != -1) {
char c = (char) in;
System.out.print(c);
}
}catch (IOException ioe){
ioe.printStackTrace();
}
}
public static void copyFile(){
try(FileInputStream fis = new FileInputStream("old/最炫民族风.mp4");
FileOutputStream fos = new FileOutputStream("data/最炫民族风.mp4");
Scanner scan = new Scanner(System.in)){
//每次读取一个字节数组,然后再把这个字节数组里面的数据写出去
byte[] b = new byte[1024];
int length = 0;
while( (length = fis.read(b)) != -1 ){
fos.write(b,0,length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
对象的序列化和反序列化
我们可以使用Properties来对数据进行持久化操作。但是这种方式操作数据的虽然持久,但是在一定场景中任然存在问题。
1、麻烦。因为在程序当中,我们往往操作的都是对象,但是在properties文件当中,数据是字符串。所以,我们不得不加上大量的代码完成这两种类型之间的转换;
2、不安全。properties文件是文本文件,意味着只要打开它,就能看懂它。
所以,“对象序列化和反序列”的技术。它指的是:
1、对象序列化 --- 把内存中的Java对象以二进制流的形式输出出去。
2、对象反序列化 --- 把输入进来的对象二进制流直接生成为内存中的一个对象。
注意
1、无论是序列化还是反序列化的概念中,都跟文件没有关系。在它们的概念里面,一个是把对象用对象二进制流输出,但没有说输出到哪里去;一个是把输入的对象二进制流转为一个对象,也没有说这个输入流是哪里来的。
2、对象的反序列化 是 Java当中,各位同学学到的第二种产生对象的方式。
序列化API -- ObjectOutputStream
流的三种分类:
1、按方向:输入流 输出流;
2、按传递单位:字节流 字符流 ;
3、按功能: 节点流 操作流。
节点流:如FileInputStream,它控制了输入字节流(InputStream)的数据源是文件。 FileOutputStream,它控制了输出字节流(OutputStream)的目的地是文件。因此这种管道以及具备了I/O流操作所需要的全部三要素,因此可以直接使用。
操作流:ObjectOutputStream,它的作用是进行对象序列化,也就是说它的任务是把对象转成二进制流输出,但是没有指定输出到哪里去。那么,也就是说它只是完成了一个转换操作,但还没有确定传输的另一个节点,所以不能单独使用。所以,它被称之为“操作流”。
而操作流是不能够单独直接使用的,所以必须要至少接一个节点流。 因此,我们看到ObjectOutputStream没有提供公共无参构造,而是一个公共带参构造,要求至少还要传递一个节点流进去进行流的对接。
反序列化API -- ObjectInputStream
它的操作与对象序列化类似,也是两个流的对接动作。ObjectInputStream只是一个操作流,只负责把对象流转成对象这个操作,而没有控制对象流从哪里来的。所以要对接一个输入的节点流,控制哪个节点作为数据源读数据。
注意:
1、如果一个类要参与序列化,那么它应该实现Serializable的接口;并且,它所有的属性都要实现这个接口;JDK自带的常用类,基本上都已经实现了这个接口。
2、JavaBean的书写规范中,除了: 必须要有公共无参构造; 必须为私有属性提供符合命名规范的get/set方法; 应该实现Serializable接口。
3、如果有批量数据需要参与序列化,那么就把它们放到一个集合类当中去,然后序列化整个集合对象。JCF当中提供的集合类,自己本身也都实现了Serializable接口。
4、序列化以后,不要去修改类的代码,如果修改了,那么反序列化回来的时候,会认为两个的版本的类型不一致了,会报错。
序列化/反序列化 示例:
public static void main(String[] args) {
HashMap<Integer, Person> allPerson = new HashMap<>();
allPerson.put(9527,new Person("小明",25));
allPerson.put(9528,new Person("大张",28));
testObjectOutputStream(allPerson);
// Person p = testObjectInputStream();
// System.out.println(p.getName() + " " + p.getAge());
}
//序列化 -- 把对象转成二进制流输出
public static void testObjectOutputStream(HashMap<Integer,Person> p){
try( FileOutputStream fos = new FileOutputStream("data/person.data");
ObjectOutputStream oos = new ObjectOutputStream(fos)){
/*
p对象会先进过oos,转为二进制流;
然后再通过对接fos,输出到文件当中去。
*/
oos.writeObject(p);
} catch (IOException e) {
e.printStackTrace();
}
}
//反序列化 -- 把输入的对象二进制流转成对象
public static Person testObjectInputStream(){
Person p = null;
try( FileInputStream fis = new FileInputStream("data/person.data");
ObjectInputStream ois = new ObjectInputStream(fis)){
p = (Person)ois.readObject();
}catch(IOException io){
io.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return p;
}
文件类File
示例:
public class TestFile {
public static void main(String[] args) {
File f = new File("data");
boolean flag = f.exists();//判断文件是否存在
if(flag){
flag = f.isDirectory();//判断是否是目录
flag = f.isFile();//判断是否是文件
if(flag){
String str = f.getPath();//获取文件相对路径
str = f.getAbsolutePath();//获取文件绝对路径
str = f.getName();//获取文件名
long l = f.length();//获取文件的大小--字节
long mills = f.lastModified();//获取文件上次修改时间
f.deleteOnExit();//文件删除 -- 先判断
f.isHidden();//获取该文件是否是隐藏文件
System.out.println(mills);
}else{
String[] subFileNames = f.list();//罗列该目录下的所有子文件(包括子目录)的名字
for(String sub : subFileNames){
System.out.println(sub);
}
File[] subFiles = f.listFiles();//罗列该目录下所有子文件(包括子目录)的File对象
for(File sub : subFiles){
System.out.println(sub.getName() + " " + sub.length() + " "
+ (sub.isFile()?"文件":"目录"));
}
}
}else{
try {
f.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}