Java I/O 扩展

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Java I/O 扩展标签: Java基础NIO Java 的NIO(新IO)和传统的IO有着相同的目的: 输入 输出 .

Java I/O 扩展

标签: Java基础


NIO

Java 的NIO(新IO)和传统的IO有着相同的目的: 输入 输出 .但是NIO使用了不同的方式来处理IO,NIO利用内存映射文件(此处文件的含义可以参考Unix的名言一切皆文件)来处理IO, NIO将文件或文件的一段区域映射到内存中(类似于操作系统的虚拟内存),这样就可以像访问内存一样来访问文件了.

ChannelBuffer是NIO中的两个核心概念:

  • Channel是对传统的IO系统的模拟,在NIO系统中所有的数据都需要通过Channel传输;Channel与传统的InputStream OutputStream 最大的区别在于它提供了一个map()方法,可以直接将一块数据映射到内存中.如果说传统的IO系统是面向流的处理, 则NIO则是面向的处理;
  • Buffer可以被理解成一个容器, 他的本质是一个数组; Buffer作为Channel与程序的中间层, 存入到Channel中的所有对象都必须首先放到Buffer中(Buffer -> Channel), 而从Channel中读取的数据也必须先放到Buffer中(Channel -> Buffer).

Buffer

从原理来看, java.nio.ByteBuffer就像一个数组,他可以保存多个类型相同的数据.Buffer只是一个抽象类,对应每种基本数据类型(boolean除外)都有相应的Buffer类: CharBuffer ShortBuffer ByteBuffer等.

这些Buffer除了ByteBuffer之外, 都采用相同或相似的方法来管理数据, 只是各自管理的数据类型不同而已.这些Buffer类都没有提供构造器, 可以通过如下方法来得到一个Buffer对象.

// Allocates a new buffer.
static XxxBuffer allocate(int capacity);

其中ByteBuffer还有一个子类MappedByteBuffer,它表示Channel将磁盘文件全部映射到内存中后得到的结果, 通常MappedByteBufferChannelmap()方法返回.

Buffer中的几个概念:

  • capacity: 该Buffer的最大数据容量;
  • limit: 第一个不应该被读出/写入的缓冲区索引;
  • position: 指明下一个可以被读出/写入的缓冲区索引;
  • mark: Buffer允许直接将position定位到该mark处.

0 <= mark <= position <= limit <= capacity

Buffer中常用的方法:

方法 解释
int capacity() Returns this buffer’s capacity.
int remaining() Returns the number of elements between the current position and the limit.
int limit() Returns this buffer’s limit.
int position() Returns this buffer’s position.
Buffer position(int newPosition) Sets this buffer’s position.
Buffer reset() Resets this buffer’s position to the previously-marked position.
Buffer clear() Clears this buffer.(并不是真的清空, 而是为下一次插入数据做好准备
Buffer flip() Flips this buffer.(将数据封存,为读取数据做好准备)

除了这些在Buffer基类中存在的方法之外, Buffer的所有子类还提供了两个重要的方法:

  • put() : 向Buffer中放入数据
  • get() : 从Buffer中取数据

当使用put/get方法放入/取出数据时, Buffer既支持单个数据的访问, 也支持(以数组为参数)批量数据的访问.而且当使用put/get方法访问Buffer的数据时, 也可分为相对和绝对两种:

  • 相对 : 从Buffer的当前position处开始读取/写入数据, position按处理元素个数后移.
  • 绝对 : 直接根据索引读取/写入数据, position不变.
/**
 * @author jifang
 * @since 16/1/9下午8:31.
 */
public class BufferTest {

    @Test
    public void client() {
        ByteBuffer buffer = ByteBuffer.allocate(64);
        displayBufferInfo(buffer, "init");

        buffer.put((byte) 'a');
        buffer.put((byte) 'b');
        buffer.put((byte) 'c');
        displayBufferInfo(buffer, "after put");

        buffer.flip();
        displayBufferInfo(buffer, "after flip");
        System.out.println((char) buffer.get());
        displayBufferInfo(buffer, "after a get");

        buffer.clear();
        displayBufferInfo(buffer, "after clear");
        // 依然可以访问到数据
        System.out.println((char) buffer.get(2));
    }

    private void displayBufferInfo(Buffer buffer, String msg) {
        System.out.println("---------" + msg + "-----------");
        System.out.println("position: " + buffer.position());
        System.out.println("limit: " + buffer.limit());
        System.out.println("capacity: " + buffer.capacity());
    }
}

通过allocate()方法创建的Buffer对象是普通Buffer, ByteBuffer还提供了一个allocateDirect()方法来创建DirectByteBuffer. DirectByteBuffer的创建成本比普通Buffer要高, 但DirectByteBuffer的读取效率也会更高.所以DirectByteBuffer适用于生存期比较长的Buffer.
只有ByteBuffer才提供了allocateDirect(int capacity)方法, 所以只能在ByteBuffer级别上创建DirectByteBuffer, 如果希望使用其他类型, 则可以将Buffer转换成其他类型的Buffer.


Channel

像上面这样使用Buffer感觉是完全没有诱惑力的(就一个数组嘛,还整得这么麻烦⊙﹏⊙b).其实Buffer真正的强大之处在于与Channel的结合,从Channel中直接映射一块内存进来,而没有必要一一的get/put.

java.nio.channels.Channel类似于传统的流对象, 但与传统的流对象有以下两个区别:

  • Channel可以直接将指定文件的部分或者全部映射成Buffer
  • 程序不能直接访问Channel中的数据, 必须要经过Buffer作为中间层.

Java为Channel接口提供了FileChannel DatagramChannel Pipe.SinkChannel Pipe.SourceChannel SelectableChannel
SocketChannel ServerSocketChannel. 所有的Channel都不应该通过构造器来直接创建, 而是通过传统的InputStream OutputStreamgetChannel()方法来返回对应的Channel, 当然不同的节点流获得的Channel不一样. 例如, FileInputStream FileOutputStream 返回的是FileChannel, PipedInputStream PipedOutputStream 返回的是Pipe.SourceChannel Pipe.SinkChannel;

Channel中最常用的三个方法是MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) read() write(), 其中map()用于将Channel对应的部分或全部数据映射成ByteBuffer, 而read/write有一系列的重载形式, 用于从Buffer中读写数据.

/**
 * @author jifang
 * @since 16/1/9下午10:55.
 */
public class ChannelTest {
    private CharsetDecoder decoder = Charset.forName("utf-8").newDecoder();

    @Test
    public void client() throws IOException {
        try (FileChannel inChannel = new FileInputStream("save.txt").getChannel();
             FileChannel outChannel = new FileOutputStream("attach.txt").getChannel()) {
            MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0,
                    new File("save.txt").length());
            displayBufferInfo(buffer, "init buffer");

            // 将Buffer内容一次写入另一文件的Channel
            outChannel.write(buffer);
            buffer.flip();
            // 解码CharBuffer之后输出
            System.out.println(decoder.decode(buffer));
        }
    }

    // ...
}

Charset

Java从1.4开始提供了java.nio.charset.Charset来处理字节序列和字符序列(字符串)之间的转换, 该类包含了用于创建解码器和编码器的方法, 需要注意的是, Charset类是不可变类.

Charset提供了availableCharsets()静态方法来获取当前JDK所支持的所有字符集.

/**
 * @author jifang
 * @since 16/1/10下午4:32.
 */
public class CharsetLearn {

    @Test
    public void testGetAllCharsets() {
        SortedMap<String, Charset> charsetMap = Charset.availableCharsets();
        for (Map.Entry<String, Charset> charset : charsetMap.entrySet()) {
            System.out.println(charset.getKey() + " aliases -> " + charset.getValue().aliases() + " chaset -> " + charset.getValue());
        }
    }
}

执行上面代码可以看到每个字符集都有一些字符串别名(比如UTF-8还有unicode-1-1-utf-8 UTF8的别名), 一旦知道了字符串的别名之后, 程序就可以调用Charset的forName()方法来创建对应的Charset对象:

@Test
public void testGetCharset() {
    Charset utf8 = Charset.forName("UTF-8");
    Charset unicode11 = Charset.forName("unicode-1-1-utf-8");
    System.out.println(utf8.name());
    System.out.println(unicode11.name());
    System.out.println(unicode11 == utf8);
}

在Java 1.7 之后, JDK又提供了一个工具类StandardCharsets, 里面提供了一些静态属性来表示标准的常用字符集:

@Test
public void testGetCharset() {
    // 使用UTF-8属性
    Charset utf8 = StandardCharsets.UTF_8;
    Charset unicode11 = Charset.forName("unicode-1-1-utf-8");
    System.out.println(utf8.name());
    System.out.println(unicode11.name());
    System.out.println(unicode11 == utf8);
}

获得了Charset对象之后,就可以使用decode()/encode()方法来对ByteBuffer CharBuffer进行编码/解码了

方法 功能
ByteBuffer encode(CharBuffer cb) Convenience method that encodes Unicode characters into bytes in this charset.
ByteBuffer encode(String str) Convenience method that encodes a string into bytes in this charset.
CharBuffer decode(ByteBuffer bb) Convenience method that decodes bytes in this charset into Unicode characters.

或者也可以通过Charset对象的newDecoder() newEncoder() 来获取CharsetDecoder解码器和CharsetEncoder编码器来完成更加灵活的编码/解码操作(他们肯定也提供了encodedecode方法).

@Test
public void testDecodeEncode() throws IOException {
    File inFile = new File("save.txt");
    FileChannel in = new FileInputStream(inFile).getChannel();

    MappedByteBuffer byteBuffer = in.map(FileChannel.MapMode.READ_ONLY, 0, inFile.length());
    // Charset utf8 = Charset.forName("UTF-8");
    Charset utf8 = StandardCharsets.UTF_8;

    // 解码
    // CharBuffer charBuffer = utf8.decode(byteBuffer);
    CharBuffer charBuffer = utf8.newDecoder().decode(byteBuffer);
    System.out.println(charBuffer);

    // 编码
    // ByteBuffer encoded = utf8.encode(charBuffer);
    ByteBuffer encoded = utf8.newEncoder().encode(charBuffer);
    byte[] bytes = new byte[(int) inFile.length()];
    encoded.get(bytes);
    for (int i = 0; i < bytes.length; ++i) {
        System.out.print(bytes[i]);
    }
    System.out.println();

}

String类里面也提供了一个getBytes(String charset)方法来使用指定的字符集将字符串转换成字节序列.


使用WatchService监控文件变化

在以前的Java版本中,如果程序需要监控文件系统的变化,则可以考虑启动一条后台线程,这条后台线程每隔一段时间去遍历一次指定目录的文件,如果发现此次遍历的结果与上次不同,则认为文件发生了变化. 但在后来的NIO.2中,Path类提供了register方法来监听文件系统的变化.

WatchKey    register(WatchService watcher, WatchEvent.Kind<?>... events);
WatchKey    register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers);

其实是Path实现了Watchable接口, registerWatchable提供的方法.

  • WatchService代表一个文件系统监听服务, 它负责监听Path目录下的文件变化.而WatchService是一个接口, 需要由FileSystem的实例来创建, 我们往往这样获取一个WatchService
WatchService service = FileSystems.getDefault().newWatchService();

一旦register方法完成注册之后, 接下来就可调用WatchService的如下方法来获取被监听的目录的文件变化事件:

方法 释义
WatchKey poll() Retrieves and removes the next watch key, or null if none are present.
WatchKey poll(long timeout, TimeUnit unit) Retrieves and removes the next watch key, waiting if necessary up to the specified wait time if none are yet present.
WatchKey take() Retrieves and removes next watch key, waiting if none are yet present.
  • 获取到WatchKey之后, 就可调用其方法来查看到底发生了什么事件, 得到WatchEvent
方法 释义
List<WatchEvent<?>> pollEvents() Retrieves and removes all pending events for this watch key, returning a List of the events that were retrieved.
boolean reset() Resets this watch key.
  • WatchEvent
方法 释义
T context() Returns the context for the event.
int count() Returns the event count.
WatchEvent.Kind<T> kind() Returns the event kind.
/**
 * @author jifang
 * @since 16/1/10下午8:00.
 */
public class ChangeWatcher {

    public static void main(String[] args) {
        watch("/Users/jifang/");
    }

    public static void watch(String directory) {
        try {
            WatchService service = FileSystems.getDefault().newWatchService();
            Paths.get(directory).register(service,
                    StandardWatchEventKinds.ENTRY_CREATE,
                    StandardWatchEventKinds.ENTRY_DELETE,
                    StandardWatchEventKinds.ENTRY_MODIFY);
            while (true) {
                WatchKey key = service.take();
                for (WatchEvent event : key.pollEvents()) {
                    System.out.println(event.context() + " 文件发生了 " + event.kind() + " 事件!");
                }

                if (!key.reset()) {
                    break;
                }
            }
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

通过使用WatchService, 可以非常优雅的监控指定目录下的文件变化, 至于文件发生变化后的处理, 就取决于业务需求了, 比如我们可以做一个日志分析器, 定时去扫描日志目录, 查看日志大小是否改变, 当发生改变时候, 就扫描发生改变的部分, 如果发现日志中有异常产生(比如有Exception/Timeout类似的关键字存在), 就把这段异常信息截取下来, 发邮件/短信给管理员.


Guava IO

  • 平时开发中常用的IO框架有Apache的commons-io和Google Guava的IO模块; 不过Apache的commons-io包比较老,更新比较缓慢(最新的包还是2012年的); 而Guava则更新相对频繁, 最近刚刚发布了19.0版本, 因此在这儿仅介绍Guava对Java IO的扩展.
  • 使用Guava需要在pom.xml中添加如下依赖:
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>19.0</version>
</dependency>

最近我在写一个网页图片抓取工具时, 最开始使用的是Java的URL.openConnection() + IOStream操作来实现, 代码非常繁琐且性能不高(详细代码可类似参考java 使用URL来读取网页内容). 而使用了Guava之后几行代码就搞定了网页的下载功能:

public static String getHtml(String url) {
    if (StringUtils.isBlank(url)) {
        return null;
    }
    try {
        return Resources.toString(new URL(url), StandardCharsets.UTF_8);
    } catch (IOException e) {
        LOGGER.error("getHtml error url = {}", url, e);
        throw new RuntimeException(e);
    }
}

代码清晰多了.

  • 还可以使用Resources类的readLines(URL url, Charset charset, LineProcessor<T> callback)方法来实现只抓取特定的网页内容的功能:
public static List<String> processUrl(String url, final String regexp) {
    try {
        return Resources.readLines(new URL(url), StandardCharsets.UTF_8, new LineProcessor<List<String>>() {

            private Pattern pattern = Pattern.compile(regexp);
            private List<String> strings = new ArrayList<>();

            @Override
            public boolean processLine(String line) throws IOException {
                Matcher matcher = pattern.matcher(line);
                while (matcher.find()) {
                    strings.add(matcher.group());
                }
                return true;
            }

            @Override
            public List<String> getResult() {
                return strings;
            }
        });
    } catch (IOException e) {
        LOGGER.error("processUrl error, url = {}, regexp = {}", url, regexp, e);
        throw new RuntimeException(e);
    }
}

而性能的话, 我记得有这么一句话来评论STL的

STL性能可能不是最高的, 但绝对不是最差的!

我认为这句话同样适用于Guava; 在Guava IO中, 有三类操作是比较常用的:

  • 对Java传统的IO操作的简化;
  • Guava对的支持;
  • Guava Files Resources对文件/资源的支持;

Java IO 简化

  • 在Guava中,用InputStream/OutputStream Readable/Appendable来对应Java中的字节流和字符流(Writer实现了Appendable接口,Reader实现了Readable接口).并用com.google.common.io.ByteStreamscom.google.common.io.CharStreams来提供对传统IO的支持.

这两个类中, 实现了很多static方法来简化Java IO操作,如:

  • static long copy(Readable/InputStream from, Appendable/OutputStream to)
  • static byte[] toByteArray(InputStream in)
  • static int read(InputStream in, byte[] b, int off, int len)
  • static ByteArrayDataInput newDataInput(byte[] bytes, int start)
  • static String toString(Readable r)
/**
 * 一行代码读取文件内容
 *
 * @throws IOException
 */
@Test
public void getFileContent() throws IOException {
    FileReader reader = new FileReader("save.txt");
    System.out.println(CharStreams.toString(reader));
}

关于ByteStreamsCharStreams的详细介绍请参考Guava文档


Guava源与汇

  • Guava提出源与汇的概念以避免总是直接跟流打交道.
  • 源与汇是指某个你知道如何从中打开流的资源,如File或URL.
  • 源是可读的,汇是可写的.

Guava的源有 ByteSourceCharSource; 汇有ByteSink CharSink

  • 源与汇的好处是它们提供了一组通用的操作(如:一旦你把数据源包装成了ByteSource,无论它原先的类型是什么,你都得到了一组按字节操作的方法). 其实就源与汇就类似于Java IO中的InputStream/OutputStream, Reader/Writer. 只要能够获取到他们或者他们的子类, 就可以使用他们提供的操作, 不管底层实现如何.
/**
 * @author jifang
 * @since 16/1/11下午4:39.
 */
public class SourceSinkTest {

    @Test
    public void fileSinkSource() throws IOException {
        File file = new File("save.txt");
        CharSink sink = Files.asCharSink(file, StandardCharsets.UTF_8);
        sink.write("- 你好吗?\n- 我很好.");

        CharSource source = Files.asCharSource(file, StandardCharsets.UTF_8);
        System.out.println(source.read());
    }

    @Test
    public void netSource() throws IOException {
        CharSource source = Resources.asCharSource(new URL("http://www.sun.com"), StandardCharsets.UTF_8);
        System.out.println(source.readFirstLine());
    }
}

获取源与汇

  • 获取字节源与汇的常用方法有:
字节源 字节汇
Files.asByteSource(File) Files.asByteSink(File file, FileWriteMode... modes)
Resources.asByteSource(URL url) -
ByteSource.wrap(byte[] b) -
ByteSource.concat(ByteSource... sources) -
  • 获取字符源与汇的常用方法有:
字符源 字符汇
Files.asCharSource(File file, Charset charset) Files.asCharSink(File file, Charset charset, FileWriteMode... modes)
Resources.asCharSource(URL url, Charset charset) -
CharSource.wrap(CharSequence charSequence) -
CharSource.concat(CharSource... sources) -
ByteSource.asCharSource(Charset charset) ByteSink.asCharSink(Charset charset)

使用源与汇

  • 这四个源与汇提供通用的方法进行读/写, 用法与Java IO类似,但比Java IO流会更加简单方便(如CharSource可以一次性将源中的数据全部读出String read(), 也可以将源中的数据一次拷贝到Writer或汇中long copyTo(CharSink/Appendable to))
@Test
public void saveHtmlFileChar() throws IOException {
    CharSource source = Resources.asCharSource(new URL("http://www.google.com"), StandardCharsets.UTF_8);
    source.copyTo(Files.asCharSink(new File("save1.html"), StandardCharsets.UTF_8));
}

@Test
public void saveHtmlFileByte() throws IOException {
    ByteSource source = Resources.asByteSource(new URL("http://www.google.com"));
    //source.copyTo(new FileOutputStream("save2.html"));
    source.copyTo(Files.asByteSink(new File("save2.html")));
}

其他详细用法请参考Guava文档


Files与Resources

  • 上面看到了使用FilesResourcesURLFile转换成ByteSourceCharSource的用法,其实这两个类还提供了很多方法来简化IO, 详细请参考Guava文档

  • Resources常用方法

Resources 方法 释义
static void copy(URL from, OutputStream to) Copies all bytes from a URL to an output stream.
static URL getResource(String resourceName) Returns a URL pointing to resourceName if the resource is found using the context class loader.
static List<String> readLines(URL url, Charset charset) Reads all of the lines from a URL.
static <T> T readLines(URL url, Charset charset, LineProcessor<T> callback) Streams lines from a URL, stopping when our callback returns false, or we have read all of the lines.
static byte[] toByteArray(URL url) Reads all bytes from a URL into a byte array.
static String toString(URL url, Charset charset) Reads all characters from a URL into a String, using the given character set.
  • Files常用方法
Files 方法 释义
static void append(CharSequence from, File to, Charset charset) Appends a character sequence (such as a string) to a file using the given character set.
static void copy(File from, Charset charset, Appendable to) Copies all characters from a file to an appendable object, using the given character set.
static void copy(File from, File to) Copies all the bytes from one file to another.
static void copy(File from, OutputStream to) Copies all bytes from a file to an output stream.
static File createTempDir() Atomically creates a new directory somewhere beneath the system’s temporary directory (as defined by the java.io.tmpdir system property), and returns its name.
static MappedByteBuffer map(File file, FileChannel.MapMode mode, long size) Maps a file in to memory as per FileChannel.map(java.nio.channels.FileChannel.MapMode, long, long) using the requested FileChannel.MapMode.
static void move(File from, File to) Moves a file from one path to another.
static <T> T readBytes(File file, ByteProcessor<T> processor) Process the bytes of a file.
static String readFirstLine(File file, Charset charset) Reads the first line from a file.
static List<String> readLines(File file, Charset charset) Reads all of the lines from a file.
static <T> T readLines(File file, Charset charset, LineProcessor<T> callback) Streams lines from a File, stopping when our callback returns false, or we have read all of the lines.
static byte[] toByteArray(File file) Reads all bytes from a file into a byte array.
static String toString(File file, Charset charset) Reads all characters from a file into a String, using the given character set.
static void touch(File file) Creates an empty file or updates the last updated timestamp on the same as the unix command of the same name.
static void write(byte[] from, File to) Overwrites a file with the contents of a byte array.
static void write(CharSequence from, File to, Charset charset) Writes a character sequence (such as a string) to a file using the given character set.

参考:
Google Guava官方教程(中文版)
Google Guava官方文档
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
6月前
|
存储 监控 Java
Java输入输出:什么是NIO(New I/O)?
Java输入输出:什么是NIO(New I/O)?
62 1
|
11天前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
5月前
|
存储 缓存 Java
Java中的缓冲流提升I/O性能,通过内存缓冲区减少对硬件访问
【6月更文挑战第22天】Java中的缓冲流提升I/O性能,通过内存缓冲区减少对硬件访问。`BufferedInputStream`和`BufferedOutputStream`用于字节流,缓存数据批量读写。`BufferedReader`和`BufferedWriter`处理字符流,支持按行操作。使用后务必关闭流。
64 3
|
6月前
|
存储 监控 Java
深入探索Java语言的NIO(New I/O)技术
深入探索Java语言的NIO(New I/O)技术
|
4月前
|
存储 缓存 Oracle
可能是最漂亮的Java I/O流详解
大家有什么思路吗?评论区一起讨论讨论。我需要使用 Java 逐行读取大约 5-6 GB 的大型文本文件。我怎样才能快速完成此操作?最高赞的回答是叫Peter Lawrey的老哥回答的。大家好,我是南哥。一个Java学习与进阶的领路人,今天指南的是Java I/O流,跟着南哥我们一起在Java之路上成长。本文收录在我开源的《Java学习进阶指南》中,涵盖了想要学习Java、成为更好的Java选手都在偷偷看的核心知识、面试重点。
121 1
可能是最漂亮的Java I/O流详解
|
4月前
|
Java Linux
Java演进问题之1:1线程模型对于I/O密集型任务如何解决
Java演进问题之1:1线程模型对于I/O密集型任务如何解决
|
4月前
|
Java API 开发者
Java中的文件I/O操作详解
Java中的文件I/O操作详解
|
5月前
|
Java 视频直播 数据库连接
Java I/O 模型详解:BIO、NIO 与 AIO 的特性与应用
Java I/O 模型详解:BIO、NIO 与 AIO 的特性与应用
66 2
|
5月前
|
存储 网络协议 Java
Java I/O 详解:基础、文件操作与 NIO 实践
Java I/O 详解:基础、文件操作与 NIO 实践
52 1
|
4月前
|
Java 数据库
Java面试题:请解释Java中的输入输出(I/O)流?详细说明应用场景
Java面试题:请解释Java中的输入输出(I/O)流?详细说明应用场景
33 0