《Java核心技术 卷II》笔记——(7)输入/输出流&文件流&序列化

Java的输入和输出通过数据流、序列化和文件系统提供;

输入/输出流?字节流与字符流?

  • 按流向分——
    输入流: 程序可以从中读取数据的流。
    输出流: 程序能向其中写入数据的流。

标准输入输出,文件的操作,网络上的数据流,字符串流,对象流,zip文件流等等,java中将输入输出抽象称为流,就好像水管,将两个容器连接起来。将数据冲外存中读取到内存中的称为输入流,将数据从内存写入外存中的称为输出流。

  • 按数据传输单位分——
    字节流: 以字节为单位传输数据的流,表示以字节为单位从stream中读取或往stream中写入信息,即io包中的inputstream类和outputstream类的派生类。通常用来读取二进制数据,如图象和声音。
    字符流: 以字符为单位传输数据的流,以Unicode字符为导向的stream,表示以Unicode字符为单位从stream中读取或往stream中写入信息,包括Reader/Writer。

常用类

字节流:InputStream/OutputStream(二进制格式操作);
字符流:Reader/Writer;
文件流:FileInputStream / FileOutputStream (字节流)、FileReader / FileWriter(字符流);
对象流:ObjectInputStream / ObjectOutputStream ;(序列化/反序列化);

eg:

1.字符流
int ch = [System.in](http://system.in/).read();
System.out.println(ch);

2.字节流
BufferedReader bfr = new BufferedReader(new InputStreamReader([System.in](http://system.in/)));
System.out.println(bfr.readLine());

3.文件流
(1)BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("test.txt")));
(2)BufferedReader br = new BufferedReader(new FileReader("test.txt"));

Java的输入,我们用到Scanner类,可以用它创建一个对象:Scanner reader=new Scanner([System.in](http://system.in/)); 然后reader对象调用nextBoolean(), nextByte(), nextShort(), nextInt(), nextLong(), nextFloat(), nextDouble()方法来从输入流中获取数据。这些方法在执行时都会阻塞,程序等待用户在输入流中输入enter键(\n)时继续执行;

操作文件?

Files类可以使得普通文件操作变得快捷:
byte[] bytes = Files.readAllBytes(path);

如果想将文件当作字符串读入,可以在调用readAllBytes之后执行:
String content = new String(bytes,charset);

如果希望文件当作行序列读入,可以调用:
List<String> lines = Files.readAllLines(path,charset);

相反,如果希望写出一个字符串到文件中,可以调用:
Files.write(path,content.getBytes(charset));

可以用下面的语句将一个行的集合写出到文件中:
Files.write(path,lines);
这些简便方法适用于处理中等长度的文本文件,如果要处理的文本长度较大,或者是二进制文件,还是应该使用所熟知的输入输出流或者读入器/写出器。

创建新目录可以调用:
Files.createDirectory(path);

可以使用下面的语句创建一个空文件:
Files.createFile(path);
如果文件已存在,那么这个调用就会异常。

将文件从一个位置复制到另一个位置可以直接调用
Files.copy(fromPath,toPath);

移动文件可以调用:
Files.move(fromPath,toPath);
如果目标路径已经存在,那么复制或移动将失败;
eg:

    @Test
    public void fuc_01() throws IOException{ // 测试 创建文件

        File mf = new File("Test_InAndOut_ABC.txt");
        System.out.println(mf.exists());

        if(mf.exists() == false){
            mf.createNewFile();
        }

        // 读文件

        String si = new String(Files.readAllBytes(Paths.get("Test_InAndOut_ABC.txt")), StandardCharsets.UTF_8);//前提是String不是很大
        System.out.println(si);
        List<String> lines = Files.readAllLines(Paths.get("Test_InAndOut_ABC.txt"), StandardCharsets.UTF_8);//读到List里面,按照行存
        StringBuilder sb = new StringBuilder();
        for(String line : lines){
            sb.append(line);
        }
        String fromFile = sb.toString();
        System.out.println(fromFile);

        // 写入文件

        PrintWriter pw = new PrintWriter(new FileWriter("Test_InAndOut_ABC.txt", true)); // true,**意味着为“追加内容”而非覆盖,否则 整个文件原内容都会被覆盖**
        pw.println("B");
        pw.println("结束");
        pw.close();

    }
    @Test
    public void fuc_02() throws IOException{//复制文件/移动文件/删除文件

        Path fromPath = Paths.get("D:\\IDEA\\workspace\\src\\JavaStudy_Test\\Test_InAndOut_ABC.txt");

        // 1."testPath";2."testPath\\Test_InAndOut_ABC_copy.txt"(可重命名文件);3."testPath\\Test_InAndOut_ABC_copy.txt"(可重命名文件);
        Path toPath = Paths.get("testPath\\Test_InAndOut_ABC_copy01.txt");

//        Files.createDirectory(toPath);//若文件夹已存在,则FileAlreadyExistsException;
//        Files.copy(fromPath, toPath);//将文件从一个位置复制到另一个位置可以直接调用,可以在toPath里面重命名该文件
//        Files.move(fromPath,toPath);//移动文件可以调用,如果目标路径已经存在,那么复制或移动将失败;

        Files.deleteIfExists(toPath);//删除文件;
    }

Path类?目录遍历(目录流)?获取文件信息?

下面的Files下的静态方法(传入Path类参数)将返回boolean值,表示检查路径的某个属性的结果:

exists(path);
isHidden(path);
isReadadble(path),isWritable(path),isExecuutable(path);
isRegularFile(path), isDirectory(path), isSymbolicLink(path);
long filesize = Files.size(path); // size方法将返回文件的字节数

静态的Files.list方法会返回一个可以读取目录中各个项的Stream<Path>对象,list方法不会进入子目录。为了处理目录中的所有子目录,需要使用File.walk方法;

有时需要对遍历过程进行更加细粒度的控制。在这种情况下,应该使用File.newDirectoryStream对象,它会产生一个DirectoryStream。注意它不是java.util.stream.Stream的子接口,而是专门用于目录遍历的接口。它是Iterable的子接口,可以在增强的for循环中使用目录流。

try语句块用来确保目录流可以被正确关闭。访问目录中的项并没有具体的顺序。可以用glob模式来过滤文件try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir, "*.java"))

eg:

    @Test
    public void fuc_03() throws IOException {//遍历目录,目录流;

        Path rootPath = Paths.get("testPath");
        Stream<Path> entries_01 = Files.list(rootPath);
        //entries_01.forEach(System.out::println); // 只能当前目录,不能遍历子目录

        Stream<Path> entries_02 = Files.walk(rootPath);
        //entries_02.forEach(System.out::println); // 按顺序遍历当前下所有子目录和文件

        try (DirectoryStream<Path> entries = Files.newDirectoryStream(rootPath)){ // 使用目录流DirectoryStream类,不是Stream而是Iterator的子类,不进入子目录;
            for (Path entry: entries){
                System.out.println(entry);
            }
        }

        //可以用glob模式来过滤文件:
        try (DirectoryStream<Path> entries = Files.newDirectoryStream(rootPath, "*.java"*)){
            for (Path entry: entries){
                System.out.println(entry);
            }
        }
    }

内存映射?

大多数操作系统都可以利用虚拟内存实现来将一个文件或文件的一部分“映射”到内存中。然后,这个文件就可以当作是内存数组一样访问,这比传统的文件操作要快得多。

与随机访问相比,性能提高总是很明显的。另一方面,对于中等尺寸文件的顺序读入则没有必要使用内存映射。

首先,从文件中获得一个通道(channel),,通道是用于磁盘文件的一种抽象,它使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。

FileChannel channel = FileChannel.open(path,options);

然后,通过调用FileChannel类的map方法从这个通道中获得一个ByteBuffer。可以指定想要映射的文件区域与映射模式,支持的模式options有三种:

  • FileChannel.MapMode.READ_ONLY: 所产生的缓冲区是只读的,任何对缓冲区写入的尝试都会导致ReadOnlyBufferedException异常。
  • FileChannel.MapMode.READ_WRTE:所产生的缓冲区是可写的,任何修改都会在某个时刻写回到文件中。注意:其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于操作系统的。
  • FileChannel.MapMode.PRIVATE: 所产生的缓冲区是科协的,但是任何修改过这个缓冲区来说是私有的,不会传播到文件中。

一旦有了缓冲区,就可以使用ByteBuffer类和Buffer超类的方法读写数据了。

缓冲区支持顺序和随机访问数据访问,它有一个可以通过get和put操作来移动的位置。
eg:

    @Test
    public void fuc_04() throws IOException { // 测试内存映射文件,以下为4种读文件的方式;

        Path path = Paths.get("testPath\\Test_InAndOut_ABC.txt");

        try (InputStream in = Files.newInputStream(path)) { // 第1种方式,InputStream字节流
            int c;
            while ((c = in.**read**()) != -1) {
                System.out.println(c);
            }
        }

        try (InputStream in = Files.newInputStream(path)) { // 第2种方式,BufferedReader字符
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String s;
            while ((s = br.readLine()) != null) {
                System.out.println(s);
            }
        }

        try (RandomAccessFile file = new RandomAccessFile(path.toFile(), "r")) { // 第3种方式,随机访问文件RandomAccessFile(只读模式)
            long len = file.length();
            System.out.println("len:" + len);
            String s;
            while ((s = file.readLine()) != null) {
                s = new String(s.getBytes("ISO-8859-1"), "utf-8");// **解决中文乱码**
                System.out.println(s);
            }
        }

        try (FileChannel channel = FileChannel.open(path)) { // 第4种方式,内存映射FileChannel类
            // 创建缓冲区
            ByteBuffer buf = ByteBuffer.allocate(1024);
            // 从通道读取数据到缓冲区
            channel.read(buf);
            // 反转缓冲区(limit设置为position,position设置为0,mark设置为-1)
            buf.flip();
            // 就是判断position和limit之间是否有元素
            while (buf.hasRemaining()) {
                // 按照字节的格式获取数据,注意中文乱码https://aliahhqcheng.iteye.com/blog/1797041;
                System.out.print((char) buf.get());
            }
            // 读完将缓冲区还原(position设置为0,limit设置为capacity,mark设置为-1)
            buf.clear();
        }
    }

文件加锁?

考虑一下多个同时执行的程序需要修改同一个文件的情形,这些程序需要以某种方式进行通信,不然这个文件很容易被损坏。文件锁可以解决问题,它可以控制对文件或文件中某个范围的字节的访问。

假设你的应用程序将用户的偏好存储在一个配置文件中,当用户调用这个应用的两个实例时,这两个实例就有可能会同时希望写这个配置文件。这种情况下,第一个实例应该锁定这个文件,当第二个实例发现这个文件被锁定时,它必须决策是等待直至这个文件解锁,还是直接跳过这个文件写操作。

要锁定一个文件,可以调用FileChannel类的lock和tryLock方法:

FileChannel = FileChannel.open(path);
FileLock lock = channel.lock();

FileLock lock = channel.rtyLock();

第一个调用会阻塞直至可获得锁,而第二个调用将立即返回,要么返回锁,要么在锁不可获得的情况下返回null。这个文件将保持锁定状态,直至这个通道关闭,或者在锁上调用了release方法。

还可以通过下面的调用锁定文件的一部分:

FileLock lock (long start, long size, boolean shared)

FileLock tryLock (long start, long size, boolean shared)

如果shared标志为false,则锁定文件的目的是读写,而如果为true,则这是一个共享锁,它允许多个进程从文件中读入,并阻止任何进程获得独占的锁。并非所有的操作系统都支持共享锁,因此你可能会在请求共享锁的时候得到的是独占的锁。

注意:在某些系统中,文件加锁仅仅是建议性的,如果是一个应用未能得到锁,它仍旧可以向被另一个应用并发锁定的文件执行写操作;在网络文件系统上锁定文件时高度依赖与系统的,因此应尽量避免;

对象流?序列化与反射/Clone?

简单地说,序列化就是将对象的状态存储到特定存储介质中的过程,也就是将对象状态转换为可保持或传输格式的过程,在序列化过程中,会将对象的共有成员、私有成员包括雷明,转换为字节流,然后再把字节流写入数据流,存储到存储介质中,这里说的存储介质通常指的是文件。

使用序列化的意义在于,将对象序列化后,可以将其转换为字节序列,这样字节序列就可以被保存在磁盘上,也可以借助网络进行传输,同时序列化后的对象时二进制状态,这样实现了平台无关性。

序列化机制允许将实现了序列化的Java对象转化为字节序列,这个过程需要借助I/O流实现。Java中只有实现了java.io.Serializable接口的类的对象才能被序列化,Serializable表示可串行的、可序列化的,所有序列化有时也称作串行化。JDK中如String类、包装类、Date类等都实现了Serializable接口;

eg:

package cn.ming;

import java.io.Serializable;

public class Serial_Student<strong> implements Serializable {
    private String name;
    private int age;
    private String gender;
    private transient String password; // transient修饰,被声明为transient的password属性不会被序列化,static对象也不会被序列化

    public Serial_Student(String name, int age, String gender, String password) {
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.password = password;
    }

    public String getName() {...} // getter and setter.
    public String toString() {
        return "姓名为:" + this.getName() + "\n" +
                "年龄为:" + this.getAge() + "\n" +
                "性别为:" + this.getGender() + "\n" +
                "密码为:" + this.getPassword();
    }
}
// test
package cn.ming;

import java.io.*;
import java.util.List;
import java.util.ArrayList;

public class Serial_Test_01 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;

        try {
            // 创建ObjectOutputStream输出流
            oos = new ObjectOutputStream(new FileOutputStream("testPath\\Test_StudentsInfo_ObjStream.txt"));
            Serial_Student stu = new Serial_Student("安娜", 30, "女", "aaaa");
            Serial_Student stu1 = new Serial_Student("李白", 18, "男", "123456");
            List<Serial_Student> list = new ArrayList();
            list.add(stu);
            list.add(stu1);
            System.out.println("write in:");
            list.forEach(System.out::println);
            // 对象序列化,写入输出流
            oos.writeObject(list);
            // 创建ObjectInputStream输入流
            ois = new ObjectInputStream(new FileInputStream("testPath\\Test_StudentsInfo_ObjStream.txt"));
            // 反序列化,强转类型
            List<Serial_Student> list_Students =(ArrayList)ois.readObject();
            System.out.println("read out:");
            // 输出生成后对象信息
            list_Students.forEach(System.out::println);
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            try {
                if (oos != null) {
                    oos.close();
                }
                if (ois != null) {
                    ois.close();
                }
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

使用反序列化获取对象信息?

反序列化就是从特定存储介质中读取数据并重新构成对象的过程。大致分为2步:

1)创建一个对象输入流(ObjectInputStream),它可以板状一个其他类型的输入流,如FileInputStream;

2)通过对象输入流的readObject()方法读取对象,该方法返回一个Object类型的对象,需要进行强制转换成真实的对象类型;

在对象序列化时需要注意

  • 序列化之后保存的是类的信息;
  • 被声明为transient的属性不会被序列化(出于安全考虑),这就是transient关键字的作用;
  • 被声明为static的属性不会被序列化,这个问题可以这么理解,序列化保存的是对象的状态,但是static修饰的变量是属于类的而不是属于变量的,因此序列化的时候不会序列化它;
  • 如果向文件中使用序列化写入多个对象,则反序列化恢复对象时必须按照写入的顺序读取;
  • 如果一个可序列化的类有多级父类,则这些父类要么也是可序列化的,要么有无参数的构造器,否则会抛出异常;
  • 如果一个类的成员中包含其他类的对象,如班级类中包含学生类型的对象,那么要序列化班级类时,则必须保证班级类和学生类都是可序列化的。即当需要序列化某个特定对象时,它的各个成员对象也必须是可序列化的;

将字节流格式的被序列化的对象经过传输,恢复成原来的对象,需要类的信息,会用到反射

关于反射的注意:使用反射虽然会很大程度上提高代码的灵活性,但是不能滥用反射,因为通过反射创建对象时性能稍微要低一些

实际上,只有当程序需要动态创建某个类的对象时才会考虑使用反射,通常在开发通用性比较强的框架、基础平台时可能会大量使用反射,因为很多Java框架中都需要根据配置问价信息来创建Java对象,从配置文件读取的只是某个类的字符串类名,程序需要根据字符串来创建对应的实例,就必须使用反射。

在实际开发中,没有必要使用反射来访问已知类的方法和属性,只有当程序需要动态创建某个类的对象时才会考虑使用。

正则表达式?(需要用的时候去查)

正则表达式可以用来搜索、编辑或处理文本,具体地说,用来匹配字符串的“某一段”,可以对其进行“判断匹配Matcher-返回true/false、删除、替换ReplaceAll、依次查找find...

如何使用?eg:

     boolean isMatch = Pattern.matches(patternStr, content);
     Pattern pattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE);
     Matcher matcher = pattern.matcher(input);
     while (matcher.find()){
              String match = matcher.group();
              System.out.println(match);
     }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值