Java基础——IO流、NIO

2019-10-21  本文已影响0人  So_ProbuING

Java I/O流

Java的IO通过Java.io包下的类和接口来支持,在Java.io包下主要输入、输出两种IO流,每种字符流则以字符来处理输入、输出操作。

Java的IO流使用了一种装饰器设计模式。

File类

File类是Java.io包下代表与平台无关的文件和目录。File类的实例代表文件对象。File能新建、删除、重命名文件和目录,File不能访问文件内容本身

访问文件和目录

File类可以使用文件路径字符串来创建File实例,该文件路径字符串可以是绝对路径也可以是相对路径。

File API

访问文件名相关的方法
文件检测相关
获取常规文件信息
文件操作相关
目录操作相关

文件过滤器

File类的list()方法中可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。

FilenameFilter接口里包含了一个accept(File dir,String name)方法,该方法将依次对指定File的所有子目录或文件进行迭代。如果该方法返回true,则list()方法会列出该子目录或者文件

public class Test {
    public static void main(String[] args) {
        File file = new File("/Users/wangxin/ideaProject");
        String[] fileList = file.list((f, name) -> {
            return name.endsWith(".java") || f.isDirectory();
        });
        for (String s : fileList) {
            System.out.println(s);
        }
    }
}

filenameFilter接口中有accept()方法,实现accept()方法就是指定自己的规则,指定哪些文件应该由list()方法列出

Java的IO流

输入流和输出流

字节流和字符流的概述

字节流主要由InputStream和OutputStream作为基类

字符流主要由Reader和Writer作为基类

字节流和字符流处理的输入/输出单位不同。输入流使用隐式的记录指针来表示当前正准备从哪个地方开始读取,当程序向OutputStream或Writer里输出一个或多个水滴后,记录指针自动向后移动。

InputStream和Reader

InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例。

InputStream

Reader

OutputStream和Writer

Writer

处理流

处理流可以可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入/输出方法

我们可以使用处理流来包装节点流,程序通过处理流来执行输入/输出功能,让节点流与底层的I/O设备

转换流

Java的输入/输出体系中提供了两个转换流,转换流用于实现将字节流转换成字符流。

推回输入流

PushbackInputStream和PushbackReader

推回输入流都带哟一个推回缓冲区,当程序调用这两个推回输入流的unread()时,系统会把指定数组的内容推回到该缓冲区里,而推回输入流每次调用read()时 会先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,还没有装满read()所需的数组时才会从原输入流中读取

如果推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发IOException异常

重定向标准输入/输出

Java的标准输入/输出分别通过System.in和System.out来代表,默认情况下他们分别代表键盘和显示器。

在System类里提供了如下三个重定向标准输入/输出的方法

Java虚拟机读写其他进程的数据

使用Runtime对象的exec()可以产生一个Process对象,Process对象代表由该Java程序启动的子进程,Process类提供了用于让程序和其子进程进行通信的方法

public static void main(String[] args) {
        try {
            final Process process = Runtime.getRuntime().exec("javac");
            final BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line = null;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

RandomAccessFile

RandomAccessFile是Java输入/输出体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。RandomAccessFile支持“随机访问”的方式,程序可以直接跳转到文件的任意地方来读写数据

RandomAccessFile可以自由访问文件的任意位置,如果只需要访问文件部分内容,而不是把文件从头读到尾

RandomAccessFile允许自由定位文件记录指针,RandomAccessFile可以不从开始的地方开始输出,因此RandomAccessFile可以向已存在的文件后追加内容

RandomAccessFile只能读写文件,不能读写其他IO节点

RandomAccessFile读写文件的过程

RandomAccessFile对象包含了一个记录指针,用以标识当前读写处的位置

当程序新创建一个RandomAccessFile对象,该对象的文件记录指针位于文件头(0处),当读/写了n个字节后,文件记录指针将会向后移动n个字节。除此之外,RandomAccessFile可以自由移动该记录指针。既可以向前移动也可以向后移动

RandomAccessFile可读可写,所以它既包含了完全类似于InputStream的三个read(),用法完全一样。也包含了完全类似于OutputStream的三个writer(),用法完全一样。

除此之外,RandomAccessFile还包含了一系列的readXxx()和writeXxx()方法

对于随机访问的理解

随机访问:可以自由访问文件的任意地方,任意访问

RandomAccessFile API

RandomAccessFile类有两个构造器

除此之外,还需要指定一个mode参数

 public static void main(String[] args) {
        try {
            insertFileContent("/Users/wangxin/Documents/test.md",100,"插入的数据,*******¥¥¥¥¥¥");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void insertFileContent(String fileName, long pos, String insertContent) throws IOException {
        //创建临时文件输入输出流 用于存放到缓冲区
        final File tmp = File.createTempFile("tmp", null);
        final FileOutputStream fos = new FileOutputStream(tmp);
        final FileInputStream fis = new FileInputStream(tmp);
        final RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
        //设置文件指针
        accessFile.seek(pos);
        //读取文件
        byte[] buffer = new byte[1024];
        int readCount = 0;
        while ((readCount = accessFile.read(buffer)) != -1) {
            //写入到文件输出流中
            fos.write(buffer, 0, readCount);
        }
        accessFile.seek(pos);
        //写入要追加的内容
        accessFile.write(insertContent.getBytes());
        while ((readCount = fis.read(buffer)) != -1) {
            //写回到文件中
            accessFile.write(buffer,0,readCount);
        }

    }

Java9改进的对象序列化

对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列号机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点

Java9增强了对象序列化机制,它允许对读入的序列化数据进行过滤。

序列化的前提

如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的。为了让某个类是可序列化的,必须实现

Serializable接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的

序列化的步骤

  1. 创建一个ObjectOutputStream 这是一个输出流的处理流,所以必须建立在其他流的基础上
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
  1. 调用ObjectOutputStream对象的writeObject()方法输出可序列化对象
oos.writeObject(obj);

反序列化的步骤

  1. 创建一个ObjectInputStream输入流,这个输入流是一个处理流,处理流也必须建立在其他节点流的基础上
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
  1. 调用readObject()方法读取流中对象,返回一个Object类型的Java对象

序列号

Java9增加的过滤功能

Java9为ObjectInputStream增加了setObjectInputFilter()、getObjectInputFilter()两个方法。

自定义序列化

可以使用transient关键字修饰实例变量,表示实例变量的类型是不可序列化的。

NIO

Java中传统的IO流都是阻塞式,而且传统的输入流、输出流都是通过字节的移动来处理的,

在JDK1.4开始,Java提供了一系列改进的输入/输出处理的新功能,称之为NIO

NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中,这样就可以达到像访问内存一样来访问文件

io包

Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象

Channel

Channel是对传统的输入/输出系统的模拟,在NIO中所有的数据都需要通过通道传输

Channel与传统的InputStream、OutputStream最大的区别在于它提供了一个map()方法,通过map()方法可以直接将"一块数据"映射到内存中

Buffer

Buffer可以被理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,从Channel中读取的数据也必须先放到Buffer中。

创建Buffer

Buffer都没有提供构造器,使用Buffer的静态方法来得到一个Buffer对象,Buffer最常用的子类是:ByteBuffer、CharBuffer

使用Buffer

Buffer是一个抽象类,最常用的子类是ByteBuffer,可以在底层字节数组上进行get/set操作

基本所有的数据类型(boolean)都有相对应的Buffer类

Buffer中有三个重要的概念:容量(capacity)、界限(limit)、位置(position)

0《mark《position《limit《capacity

工作过程

Buffer的主要作用就是装入数据,然后输出数据。开始时Buffer的position为0,limit为capacity。程序可以通过put() 向Buffer中放入一些数据,或者从Channel中获取一些数据,每放入一些数据,Buffer的position相应地向后移动一些位置

Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在的位置,并将position的位置设置为0,丢弃标记这样就将Buffer的读写指针又移到了开始位置。调用flip()方法之后,Buffer为输出数据做好准备,Buffer输出数据结束后,Buffer调用clear()方法,clear不清空Buffer的数据,仅仅将position置为0,将limit置为capacity,丢弃mark标记 这样为再次向Buffer中装入数据做好准备。

buffer调用flip()后后面的数据不可访问,避免读取数据出现Null值

buffer调用clear()后position设置为0后后面的空间又可以访问

存入数据和放入数据 put get方法 ,使用put()和get()方法 放入、取出数据,Buffer即支持对单个数据的访问,也支持对批量数据的访问

当使用put()和get(),分为相对和绝对两种

    //创建Buffer对象
        final CharBuffer charBuffer = CharBuffer.allocate(20);
        //打印buffer信息
        System.out.println("buffer的capacity" + charBuffer.capacity());
        System.out.println("buffer的limit" + charBuffer.limit());
        System.out.println("buffer的position" + charBuffer.position());
        //存入三个数据
        charBuffer.put("a");
        charBuffer.put("b");
        charBuffer.put("c");
        //存入数据后查看position
        System.out.println("存入数据后position" + charBuffer.position());
        //调用flip()方法 准备输出数据
        charBuffer.flip();
        System.out.println("flip()后position" + charBuffer.position());
        System.out.println("flip()后limit" + charBuffer.limit());
        //取出第一个元素
        System.out.println("取出的第一个元素" + charBuffer.get() + "取出第一个元素后position" + charBuffer.position());
        //clear
        charBuffer.clear();
        System.out.println("clear后position" + charBuffer.position());
        System.out.println("clear后limit" + charBuffer.limit());
        //执行clear后,查看数据
        System.out.println("clear后查看buffer中的数据" + charBuffer.get(2));

使用Channel

Channel类似于传统的流对象,但与传统的流对象有两个主要区别

构建Channel

所有的Channel都不应该通过构造方法创建,而是通过传统的节点InputStream、OutputStream的getChannel()方法来返回对应的Channel

Channel中最常用的三类方法:

FileChannel fileChannel = new FileInputStream(f).getChannel();
FileChannel fileOutChannel = new FileOutputStream(f).getChannel();

FileInputStream创建的Channel 只能读取文件

FileOutStream创建的Channel只能写入文件

RandomAccessFile创建的Channel是否能读写取决于RandomAccessFile打开文件的模式

使用Channel Buffer复制a.txt文件,追加在该文件后面

public static void main(String[] args) {
        //构建File
        final File file = new File("/Users/wangxin/Documents/test.txt");
        try (RandomAccessFile raf = new RandomAccessFile(file, "rw");
             //获取Channel
             final FileChannel fileChannel = raf.getChannel()
        ) {
            final MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
            //移动文件指针
            fileChannel.position(file.length());
            //输出所有数据
            fileChannel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

使用Channel Buffer使用数组读取文件,并打印

public static void main(String[] args) {
        //创建文件输入流
        try (FileInputStream fis = new FileInputStream(filePath);
             //获取channel
             final FileChannel channel = fis.getChannel();
        ) {
            //创建Buffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //从channel读取数据到buffer中
            while ((channel.read(byteBuffer)) != -1) {
                //写入数据到文件中
                //锁定缓冲区
                byteBuffer.flip();
                //创建Charset对象
                final Charset charset = StandardCharsets.UTF_8;
                //创建解码器对象
                final CharsetDecoder charsetDecoder = charset.newDecoder();
                //将bytebuffer的内容转码
                //输出数据
                final CharBuffer cbuff = charsetDecoder.decode(byteBuffer);
                System.out.println(cbuff);
                //清空指针与范围
                byteBuffer.clear();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

字符集和Charset

编码Encode和解码Decode

计算机底层是没有文本文件、图片之分的,只是记录每个文件的二进制序列,当需要保存文本文件时,程序必须先把文件中的每个字符翻译成二进制序列,当需要读取文本文件时,程序必须把二进制序列转换为一个个的字符

Java默认使用Unicode字符集,但是很多操作系统并不使用Unicode,那么从系统中读取数据到Java程序时,就可能出现乱码的问题

JDK1.4提供了Charset来处理字节序列和字符序列之间的转换关系,该类包含了用于创建解码器和编码器的方法,还提供了获取Charset所支持字符集的方法,Charset类是不可变的

Java7中新增了一个StandardCharsets类,该类包含了各种平台的编码集的Charset对象

文件锁

如果多个运行的程序需要并发修改同一个文件,程序之间需要某种机制来进行通信。使用文件锁可以有效地组织多个进程并发修改同一个文件

在NIO中,Java提供了FileLock来支持文件锁定功能,在FileChannel中提供的lock()/tryLock()方法可以获得文件锁FileLock对象,从而锁定文件

lock() 试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞

tryLock()尝试锁定文件,将直接返回而不是阻塞,如果获取到了文件锁则直接返回文件锁,否则将返回null

shared为true表示该锁是一个共享锁,它将允许多个进程来读取文件。但组织其他进程获得对该文件的排他所

shared为false时,表明该锁是一个排他锁,它将锁住对该文件的读写。程序可以调用FileLock的isShared来判断它获得的锁是否为共享锁

处理完文件后通过FileLock的release()来释放文件锁

Java7的NIO.2

Java7对原有的NIO进行了重大改进

Path、Paths和Files核心API

为了解决早期的Java提供的File类来访问文件系统时的性能不高问题NIO.2引入了Path接口,Path接口代表一个平台无关的平台路径

 public static void main(String[] args) {
        //以当前路径创建Path对象
        final Path path = Paths.get(".");
        System.out.println("path中路径的数量" + path.getNameCount());
        //获取Path的根路径
        System.out.println("path的根路径" + path.getRoot());
        //获取Path的绝对路径
        final Path absolutePath = path.toAbsolutePath();
        System.out.println(absolutePath);
        //以多个String来构建path对象
        final Path path1 = Paths.get("h", "e", "l");
        System.out.println(path1);// -> h/e/l
    }

FIles

Files是一个操作文件的工具类

static String filePath = "/Users/wangxin/Documents/test.txt";

    public static void main(String[] args) {
        //使用Files复制文件
        try {
            Files.copy(Paths.get(filePath), new FileOutputStream("./copy.txt"));
            //是否为隐藏文件
            System.out.println("file是否为隐藏文件" + Files.isHidden(Paths.get(filePath)));
            //一次性读取文件的所有行 并存储到集合中
            final List<String> fileLines = Files.readAllLines(Paths.get(filePath));
            fileLines.forEach(System.out::println);
            //判断指定文件的大小
            System.out.println("Files文件的大小为" + Files.size(Paths.get(filePath)));
            //将指定list写入文件中
            final ArrayList<String> poem = new ArrayList<>();
            poem.add("添加文字1");
            poem.add("添加文字2");
            poem.add("添加文字3");
            Files.write(Paths.get(filePath), poem, StandardCharsets.UTF_8);
            poem.forEach(s -> {
                System.out.println(s);
            });
            //列出指定的目录下的所有文件
            Files.list(Paths.get(".")).forEach(path -> {
                System.out.println(path);
            });
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

使用FileVisitor遍历文件和目录

在以前的Java版本中,如果要遍历指定目录下的所有文件和子目录,只能使用递归进行遍历。不仅复杂,而且灵活性也不高

现在可以使用Files工具类来遍历指定文件和子目录

FileVisitor代表一个文件访问器,walkFileTree()方法会自动遍历start路径下的所有文件和子目录,遍历文件和子目录都会触发FileVisitor中相应的方法

FileVisitResult是一个枚举类

在使用时,可以使用SimpleFileVisitor(FIleVisitor)的实现类,来实现文件访问器

 public static void main(String[] args) {
        //遍历当前文件目录下的文件和子目录
        try {
            Files.walkFileTree(Paths.get("."), new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    System.out.println("正在访问" + file);
                    if (file.endsWith(".java")) {
                        System.out.println("已经找到.java文件");
                        //终止后续访问
                        return FileVisitResult.TERMINATE;
                    }
                    //执行后续访问
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    System.out.println("当前访问目录" + dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }

WatchService监控文件的变化

在以前的Java版本中,如果程序需要监控文件的变化,需要很繁琐的操作

在NIO.2的Path类提供了监听的方法来监听文件系统的变化

 public static void main(String[] args) {
        //要监控的目录路径
        String watchFile = "/Users/wangxin/Documents";
        //获取系统默认的WatchService
        try {
            final FileSystem fileSystem = FileSystems.getDefault();
            final WatchService watchService = fileSystem.newWatchService();
            Paths.get(watchFile)
                    .register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
            while (true) {
                //获取下一个变化的事件
                final WatchKey watchKey = watchService.take();
                for (WatchEvent<?> event : watchKey.pollEvents()) {
                    System.out.println(event.context() + "文件发生了" + event.kind() + "事件");
                }
                //重设key
                final boolean reset = watchKey.reset();
                if (!reset) {
                    break;
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

上一篇 下一篇

猜你喜欢

热点阅读