对于Java I/O来说,I意味着Input(输入),O意味着Output(输出)。读书写作并非易事,而创建一个好的I/O系统更是一项艰难的任务。

古人云:“读书破万卷,下笔如有神”。也就是说,只有大量的阅读,写作的时候才能风生水起——写作意味着输出(我的知识传播给他人),而读书意味着输入(从他人的知识中汲取营养)。

01、数据流之字节与字符

Java所有的I/O机制都是基于数据流进行的输入输出。数据流可分为两种:

1)字节流,未经加工的原始二进制数据,最小的数据单元是字节

2)字符流,经过一定编码处理后符合某种格式规定的数据,最小的数据单元是字符——占用两个字节。

OutputStreamInputStream用来处理字节流;WriterReader用来处理字符流;OutputStreamWriter可以把OutputStream转换为WriterInputStreamReader可以把InputStream转换为Reader

Java的设计者为此设计了众多的类,见下图。

看到这么多类,你一定感觉头晕目眩。反正我已经看得不耐烦了。搞这么多类,看起来头真的大——这也从侧面说明实际的应用场景各有各的不同——你也完全不用担心,因为实际项目当中,根本就不可能全用到(我就没用过SequenceOutputStream)。

我建议你在学习的时候要掌握一种“挑三拣四”的能力——学习自己感兴趣的、必须掌握的、对能力有所提升的知识。切不可囫囵吞枣,强迫自己什么都学。什么都学,最后的结果可能是什么都不会。

字符流是基于字节流的,因此,我们先来学习一下字节流的两个最基础的类——OutputStreamInputStream,它们是必须要掌握的。

1)OutputStream

OutputStream提供了4个非常有用的方法,如下。

  • public void write(byte b[]):将数组b中的字节写到输出流。
  • public void write(byte b[], int off, int len):将数组b的从偏移量off开始的len个字节写到输出流。
  • public void flush() : 将数据缓冲区中数据全部输出,并清空缓冲区。
  • public void close() : 关闭输出流并释放与流相关的系统资源。

其子类ByteArrayOutputStreamBufferedOuputStream最为常用(File相关类放在下个小节)。

①、ByteArrayOutputStream通常用于在内存中创建一个字节数组缓冲区,数据被“临时”放在此缓冲区中,并不会输出到文件或者网络套接字中——就好像一个中转站,负责把输入流中的数据读入到内存缓冲区中,你可以调用它的toByteArray()方法来获取字节数组。

来看下例。

public static byte[] readBytes(InputStream in, long length) throws IOException {
    ByteArrayOutputStream bo = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int read = 0;
    while (read < length) {
        int cur = in.read(buffer, 0, (int) Math.min(1024, length - read));
        if (cur < 0) {
            break;
        }
        read += cur;
        bo.write(buffer, 0, cur);
    }
    return bo.toByteArray();
}

ByteArrayOutputStream的责任就是把InputStream中的字节流“一字不差”的读出来——这个工具方法很重要,很重要,很重要——可以解决粘包的问题。

②、BufferedOuputStream实现了一个缓冲输出流,可以将很多小的数据缓存为一个大块的数据,然后一次性地输出到文件或者网络套接字中——这里的“缓冲”和ByteArrayOutputStream的“缓冲”有着很大的不同——前者是为了下一次的一次性输出,后者就是单纯的为了缓冲,不存在输出。

来看下例。

protected void write(byte[] data) throws IOException {
    out.write(intToByte(data.length));
    out.write(data);
    out.flush();
    out.close();
} public static byte[] intToByte(int num)
{
    byte[] data = new byte[4];
    for (int i = 0; i < data.length; i++)
    {
        data[3-i] = (byte)(num % 256);
        num = num / 256;
    }
    return data;
}

使用BufferedOuputStream的时候,一定要记得调用flush()方法将数据从缓冲区中全部输出。使用完毕后,调用close()方法关闭输出流,释放与流相关的系统资源。

2)InputStream

InputStream也提供了4个非常有用的方法,如下。

  • public int read(byte b[]):读取b.length个字节的数据放到数组b中,返回值是读取的字节数。
  • public int read(byte b[], int off, int len):从输入流中最多读取len个字节的数据,存放到偏移量为off的数组b中。
  • public int available():返回输入流中可以读取的字节数。
  • public int close() :使用完后,对打开的流进行关闭。

其子类BufferedInputStream(缓冲输入流)最为常用,效率最高(当我们不确定读入的是大数据还是小数据)。

无缓冲流上的每个读取请求通常会导致对操作系统的调用以读取所请求的字节数——进行系统调用的开销非常大。但缓冲输入流就不一样了,它通过对内部缓冲区执行(例如)高达8k字节的大量读取,然后针对缓冲区的大小再分配字节来减少系统调用的开销——性能会提高很多。

使用示例如下。

先来看一个辅助方法byteToInt,把字节转换成int。

public static int byteToInt(byte[] b) {
    int num = 0;
    for (int i = 0; i < b.length; i++) {
        num*=256;
        num+=(b[i]+256)%256;
    }
    return num;
}

再来看如何从输入流中,根据指定的长度contentLength来读取数据。readBytes()方法在之前已经提到过。

BufferedInputStream in = new BufferedInputStream(socket.getInputStream());

byte[] tmpByte = new byte[4];
// 读取四个字节判断消息长度
in.read(tmpByte, 0, 4);
// 将byte转为int
int contentLength = byteToInt(tmpByte); byte[] buf = null;
if (contentLength > in.available()) {
    // 之前提到的方法
    buf = readBytes(in, contentLength);
} else {
    buf = new byte[contentLength];
    in.read(buf, 0, contentLength);     // 发生粘包了
    if (in.available() > 0) {
    }
}

我敢保证,只要你搞懂了字节流,字符流也就不在话下——所以,我们在此略过字符流。

02、File类

前面我们了解到,数据有两种格式:字节与字符。那么这些数据从哪里来,又存往何处呢?

一个主要的方式就是从物理磁盘上进行读取和存储,磁盘的唯一最小描述就是文件。也就是说上层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器交互的一个最小单元。

在Java中,通常用File类来操作文件。当然了,File不止是文件,它也是文件夹(目录)。File类保存了文件或目录的各种元数据信息(文件名、文件长度、最后修改时间、是否可读、当前文件的路径名等等)。

通过File类以及文件输入输出流(FileInputStreamFileOutputStream),可以轻松地创建、删除、复制文件或者目录。

这里,我提供给你一个实用的文件工具类——FileUtils。

/**
 * 文件操作工具类
 * 实现文件的创建、删除、复制、压缩、解压以及目录的创建、删除、复制、压缩解压等功能
 */
public class FileUtils extends org.apache.commons.io.FileUtils {
    /**
     * 复制单个文件
     * @param srcFileName 待复制的文件名
     * @param descFileName 目标文件名
     * @param coverlay 如果目标文件已存在,是否覆盖
     * @return 如果复制成功,则返回true,否则返回false
     */
    public static boolean copyFileCover(String srcFileName,
            String descFileName, boolean coverlay) {
        File srcFile = new File(srcFileName);
        // 判断源文件是否存在
        if (!srcFile.exists()) {
            logger.debug("复制文件失败,源文件 " + srcFileName + " 不存在!");
            return false;
        }
        // 判断源文件是否是合法的文件
        else if (!srcFile.isFile()) {
            logger.debug("复制文件失败," + srcFileName + " 不是一个文件!");
            return false;
        }
        File descFile = new File(descFileName);
        // 判断目标文件是否存在
        if (descFile.exists()) {
            // 如果目标文件存在,并且允许覆盖
            if (coverlay) {
                logger.debug("目标文件已存在,准备删除!");
                if (!FileUtils.delFile(descFileName)) {
                    logger.debug("删除目标文件 " + descFileName + " 失败!");
                    return false;
                }
            } else {
                logger.debug("复制文件失败,目标文件 " + descFileName + " 已存在!");
                return false;
            }
        } else {
            if (!descFile.getParentFile().exists()) {
                // 如果目标文件所在的目录不存在,则创建目录
                logger.debug("目标文件所在的目录不存在,创建目录!");
                // 创建目标文件所在的目录
                if (!descFile.getParentFile().mkdirs()) {
                    logger.debug("创建目标文件所在的目录失败!");
                    return false;
                }
            }
        }
    }
}

限于篇幅,就不贴太多代码了,需要的话找我(微信:qing_gee)要。

  • public static boolean createFile(String descFileName):创建文件。
  • public static boolean createDirectory(String descDirName):创建目录。
  • public static boolean copyFile(String srcFileName, String descFileName):复制文件。
  • public static boolean copyDirectory(String srcDirName, String descDirName):复制目录。
  • public static boolean deleteFile(String fileName):删除文件。
  • public static boolean deleteDirectory(String dirName):删除目录。
  • public static void writeToFile(String fileName, String content, boolean append):向文件中写入内容。

03、网络套接字——Socket

虽然网络套接字(Socket)并不在java.io包下,但它和输入输出流密切相关。FileSocket是两组主要的数据传输方式。

Socket是描述计算机之间完成相互通信的一种抽象。可以把 Socket比作为两个城市之间的交通工具,有了交通工具(高铁、汽车),就可以在城市之间来回穿梭了。交通工具有多种,每种交通工具也有相应的交通规则。Socket也一样,也有多种。大部分情况下,我们使用的都是基于TCP/IP的套接字——一种稳定的通信协议。

假设主机A是客户端,主机B是服务器端。客户端要与服务器端通信,客户端首先要创建一个Socket实例,操作系统将为这个Socket实例分配一个没有被使用的本地端口号,并创建一个套接字数据结构,直到这个连接关闭。

示例如下。

Socket socket = new Socket(serverIp, serverPort);
BufferedInputStream in = new BufferedInputStream(socket.getInputStream());
BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream());

与之对应的,服务端需要创建一个ServerSocket实例,之后调用accept()方法进入阻塞状态,等待客户端的请求。当一个新的请求到来时,将为这个连接创建一个新的套接字数据结构。

示例如下。

ServerSocket server = new ServerSocket(port);
Socket socket = server.accept();
InputStream in = new BufferedInputStream(socket.getInputStream());
OutputStream out = new BufferedOutputStream(socket.getOutputStream());

Socket一旦打通,就可以通过InputStreamOutputStream进行数据传输了。

04、压缩

Java I/O 支持压缩格式的数据流。在Socket通信中,我常用GZIPOutputStreamGZIPInputStream来对数据流进行简单地压缩和解压。

压缩的好处就在于能够减小网络传输中数据的体积。代码如下。

/**
 * 压缩解压
 */
public class CompressionUtil {
    public static byte[] compress(byte[] data) throws IOException {         ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] result = null;
        GZIPOutputStream zos = new GZIPOutputStream(bos);
        zos.write(data);
        return result;
    }     public static byte[] deCompress(byte[] in) throws IOException {         ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        GZIPInputStream inStream = new GZIPInputStream(new ByteArrayInputStream(in));         byte[] buf = new byte[1024];
        while (true) {
            try {
                int size = inStream.read(buf);
                if (size <= 0)
                    break;
                outStream.write(buf, 0, size);
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }         return outStream.toByteArray();
    }
}

上一篇:Java注解(Annotation):请不要小看我!

下一篇:Java:并发不易,先学会用

“挑三拣四”地学一学Java I/O的更多相关文章

  1. 从零开始学 Java - Spring 集成 Memcached 缓存配置(二)

    Memcached 客户端选择 上一篇文章 从零开始学 Java - Spring 集成 Memcached 缓存配置(一)中我们讲到这篇要谈客户端的选择,在 Java 中一般常用的有三个: Memc ...

  2. 从零开始学 Java - Spring 集成 ActiveMQ 配置(一)

    你家小区下面有没有快递柜 近两年来,我们收取快递的方式好像变了,变得我们其实并不需要见到快递小哥也能拿到自己的快递了.对,我说的就是类似快递柜.菜鸟驿站这类的代收点的出现,把我们原来快递小哥必须拿着快 ...

  3. 从零开始学 Java - Spring 集成 Memcached 缓存配置(一)

    硬盘和内存的作用是什么 硬盘的作用毫无疑问我们大家都清楚,不就是用来存储数据文件的么?如照片.视频.各种文档或等等,肯定也有你喜欢的某位岛国老师的动作片,这个时候无论我们电脑是否关机重启它们永远在那里 ...

  4. 从零开始学 Java - 我放弃了 .NET ?

    这不是一篇引起战争的文章 毫无疑问,我之前是一名在微软温暖怀抱下干了近三年的 .NET 开发者,为什么要牛(sha)X一样去搞 Java 呢?因为我喜欢 iOS 阿!哈哈,开个玩笑.其实,开始学 Ja ...

  5. 从零开始学 Java - Spring 集成 ActiveMQ 配置(二)

    从上一篇开始说起 上一篇从零开始学 Java - Spring 集成 ActiveMQ 配置(一)文章中讲了我关于消息队列的思考过程,现在这一篇会讲到 ActivMQ 与 Spring 框架的整合配置 ...

  6. 从零开始学 Java - 利用 Nginx 负载均衡实现 Web 服务器更新不影响访问

    还记得那些美妙的夜晚吗 你洗洗打算看一个小电影就睡了,这个时候突然想起来今天晚上是服务器更新的日子,你要在凌晨时分去把最新的代码更新到服务器,以保证明天大家一觉醒来打开网站,发现昨天的 Bug 都不见 ...

  7. 从零开始学 Java - log4j 项目中的详细配置

    你还会用笔来写字么 我是不怎么会了,有时候老是拿起笔之后不知道这个字怎么写,这时候就会拿起手机去打出来:有时候还会写出来这个字之后越看越不像,这时候就开始怀疑自己的能力了:有时候写出来了一大堆字之后, ...

  8. 从零开始学 Java - Spring 支持 CORS 请求踩的坑

    谁没掉进过几个大坑 记得好久之前,总能时不时在某个地方看到一些标语,往往都是上面一个伟人的头像,然后不管是不是他说的话,下面总是有看起来很政治正确且没卵用的屁话,我活到目前为止,最令我笑的肚子痛得是下 ...

  9. 从零开始学 Java - Spring MVC 实现跨域资源 CORS 请求

    论职业的重要性 问:为什么所有家长都希望自己的孩子成为公务员? 答:体面.有权.有钱又悠闲. 问:为什么所有家长都希望自己的孩子成为律师或医生? 答:体面.有钱.有技能. 问:为什么所有家长都不怎么知 ...

随机推荐

  1. phantomjs Can not connect to the Service phantomjs错误

    尝试方法一: 打开hosts文件配置 cat /etc/hosts 添加 127.0.0.1 localhost 重新运行 尝试方法二: 1,抛开服务,直接调用phantomjs定位问题 由于我是从服 ...

  2. 高效开发 Web 单页应用解决方案

    于 2017 年初,有在 Github 建立并维护一个项目:Vue Boilerplate Template,欲成就一款开箱即用 Vue + Webpack 的脚手架模版:其目标与宗旨是:根据以往经验 ...

  3. CSS float:right 在IE浏览器下换行

    在换行的html标签内加如下样式 style="right: 0px; position: absolute;"

  4. 2个byte类型数据相加(转型问题的分析)

    转自https://blog.csdn.net/alinshen/article/details/53571857 今天看到网上有网友问到关于final修饰的面试题目,题目如下: <span s ...

  5. 转[@SuppressWarnings详解]

    背景知识:       从JDK5开始提供名为Annotation(注释)的功能,它被定义为JSR-175规范.注释是以“@注释名”在代码中存在的,还可以添加一些参数 值,例如:@SuppressWa ...

  6. nsqlookupd.go

    )     }     l.Lock()     l.httpListener = httpListener     l.Unlock()     httpServer := newHTTPServe ...

  7. 【二分贪心】Bzoj3969 [WF2013] Low Power

    Description 有n个机器,每个机器有2个芯片,每个芯片可以放k个电池. 每个芯片能量是k个电池的能量的最小值. 两个芯片的能量之差越小,这个机器就工作的越好. 现在有2nk个电池,已知它们的 ...

  8. 【爆料】-《西悉尼大学毕业证书》UWS一模一样原件

    ☞西悉尼大学毕业证书[微/Q:865121257◆WeChat:CC6669834]UC毕业证书/联系人Alice[查看点击百度快照查看][留信网学历认证&博士&硕士&海归&a ...

  9. APP版本更新通知流程测试要点

    一.APP版本更新通知流程图如下: 二.测试注意点: 1.Android更新直接下载APK,IOS引导至APP Store更新页面: 强制更新------只有"立即更新" 1.一般 ...

  10. LaTeX 公式编辑

    推荐一篇关于LaTeX的文档:<一份不太简短的LATEX 2ε介绍> 1. 常用数学公式符号: 求期望 $\mathbb{E}$ \mathbb{E} 正负无穷 $+\infty$,$-\ ...