Java IO流之字节流【一】
1.IO 概述
生活中,你肯定经历过这样的场景。当你编辑一个文本文件,忘记了 ctrl+s ,可能文件就白白编辑了。
当你电脑上插入一个U盘,可以把一个视频,拷贝到你的电脑硬盘里。那么数据都是存储在哪些设备呢?内存、硬盘、外接设备(移动硬盘,U盘)等等。
数据的传输可以看做是数据的流动,按照流动的方向,以内存为基准,分为输入流input 和输出流output ,即流向内存是输入流,流出内存是输出流。
Java的IO模型设计采用了Decorator(装饰者模式),按功能划分流,并且可以动态装配。
举例:应当组合使用 FileInputStream 和 BufferedInputStream,以实现具有缓冲功能的文件输入流。
2.IO流的分类
- 根据数据的流向分为:输入流 和 输出流。
- 输入流 :把数据从其他设备上读取到内存中的流。
- 输出流 :把数据从内存中写出到其他设备上的流。
- 根据数据的类型分为:字节流 和 字符流。
- 字节流 :以字节为单位,读写数据的流。
- 字符流 :以字符为单位,读写数据的流。Java中的字符是Unicode编码,一个字符占用两个字节
- 根据功能分为:节点流 和 处理流
- 节点流:向一个特定的地方(节点)读写数据。如
FileInputStream
。 - 处理流:是对一个已存在的流进行封装。如
BufferedInputStream
。处理流的构造方法需要一个流对象做参数。一个流对象可以被其他流多次封装。
流的分类 | 输入流 | 输出流 |
---|---|---|
字节流 | InputStream(字节输入流) | OutputStream(字节输出流) |
字符流 | Reader(字符输入流) | Writer(字符输出流) |
2.2 IO流类图结构
3.字节流
在计算机中,一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存的,都是一个个字节。
所以,字节流可以传输任意文件数据。
在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
字节流类图结构:
3.1 字节
- 字节(Byte)是计算机中表述存储容量的计量单位,也表示编程语言中的数据类型和语言字符。
- 一个字节存储8位无符号数,储存的数值范围为0-255。
字节与比特
- 数据存储是以“字节”(Byte)为单位,数据传输大多是以“位”(bit,又名“比特”)为单位,一个位就代表一个0或1(即二进制),每8个位(bit,简写为b)组成一个字节(Byte,简写为B),是最小一级的信息单位 [4] 。
Java基本数据类型
类型 | 字节数 | 位数 | 范围 |
---|---|---|---|
byte | 1 | 8位 | 最小值是 -128(-2^7); 最大值是 127(2^7-1) |
short | 2 | 16位 | 最小值是 -32768(-2^15); 最大值是 32767(2^15 - 1) |
int | 4 | 32位 | 最小值是 -2,147,483,648(-2^31); 最大值是 2,147,483,647(2^31 - 1) |
long | 8 | 64位 | 最小值是 -2^63; 最大值是 2^63 -1 |
float | 4 | 32位 | 单精度、-2^128 ~ +2^128,也即-3.40E+38 ~ +3.40E+38 |
double | 8 | 64位 | 双精度、-2^1024 ~ +2^1024,也即-1.79E+308 ~ +1.79E+308。 |
char | 2 | 16 位 | 最小值是 \u0000(即为0);最大值是 \uffff(即为65,535) |
boolean | 1 | 8 | 只有两个取值:true(00000001) 和 false(00000000) |
3.2 【OutputStream】字节输出流
java.io.OutputStream
是一个抽象类,是表示字节输出流的所有类的超类,能够将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
java.io.OutputStream
的方法如下:
public void close() :关闭此输出流并释放与此流有关的所有系统资源
public void flush() :刷新此输出流并强制写出所有缓冲的输出字节。
public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。该方法等同于write(byte[], 0, b.length);
public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从索引 off开始的len 个字节写入此输出流。该方法底层调用的是write(int b)。
public abstract void write(int b) :将指定的字节写入此输出流。(要写入的字节是参数 b 的8个低位。b 的 24 个高位将被忽略。)
3.2.1 FileOutputStream 【文件字节输出流】
作用: 将数据以字节的形式写入到文件中。
构造方法:
public FileOutputStream(File file) : 相当于FileOutputStream(File file,false) 。
public FileOutputStream(File file,boolean append) : 创建一个向File对象表示的文件中写入数据的文件输出流.如果第二个参数为true,则表示追加写入到文件末尾,若为false,则是覆盖写入。
public FileOutputStream(String name) :相当于FileOutputStream(String name,false) 。
public FileOutputStream(String name,boolean append) :创建一个向具有指定名称的文件中写入数据的输出文件流。如果第二个参数为true,则表示追加写入到文件末尾,若为false,则是覆盖写入。
public FileOutputStream(FileDescriptor fdObj) : 创建一个向指定文件描述符处写入数据的输出文件流,该文件描述符表示一个到文件系统中的某个实际文件的现有连接。
构造方法源码注释:
public FileOutputStream(File file) throws FileNotFoundException {
this(file, false);
}
public FileOutputStream(File file, boolean append)
throws FileNotFoundException
{
// 获取构造方法中传入的路径名
String name = (file != null ? file.getPath() : null);
// 获取系统安全管理器
SecurityManager security = System.getSecurityManager();
if (security != null) {
// 验证是否有写权限
security.checkWrite(name);
}
if (name == null) {
throw new NullPointerException();
}
// 验证文件路径是否无效
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
// 创建一个文件描述符类
this.fd = new FileDescriptor();
// 增加一个可关闭的标签,以便跟踪
fd.attach(this);
this.append = append;
this.path = name;
/** 打开指定名称的文件以进行覆盖写入或追加写入。
* append :false 覆盖写入
* append :true 追加写入
*/
open(name, append);
}
// 打开指定名称的文件以进行覆盖写入或追加写入。
private void open(String name, boolean append) throws FileNotFoundException {
open0(name, append);
}
// native方法,调用系统底层。
private native void open0(String name, boolean append) throws FileNotFoundException;
public FileOutputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null, false);
}
public FileOutputStream(String name, boolean append)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, append);
}
public FileOutputStream(FileDescriptor fdObj) {
// 获取系统安全管理器
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
// 验证是否有写入权限
security.checkWrite(fdObj);
}
this.fd = fdObj;
this.append = false;
this.path = null;
fd.attach(this);
}
通过源码,可以看出,这几个构造方法真正调用的其实都是 public FileOutputStream(File file, boolean append)
这个构造方法。
写入数据的步骤:
java程序-->jvm(java虚拟机)-->OS(操作系统)-->OS调用写数据的方法-->把数据写入到文件中
简单示例:
package com.hanyxx.io;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @author layman
*/
public class Demo01 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("layman.txt");
// 写入单个字节
fos.write(65);
/**
* 一次性写入多个字节
* 如果第一个字节是正数(0~127),那么显示的时候会查询ASCII表
* 如果第一个字节是负数,那么第一个字节会和第二个字节,组成一个中文显示。使用系统默认码表(GBK)。
*/
byte[] bytes = {-65,-66,67,-68,69};
//fos.write(bytes);
/**
* write(byte b[], int off, int len):写入字节数组的一部分
* off :开始写入的数组索引
* len :写入的字节个数
*/
//fos.write(bytes,1,2);
fos.close();
}
}
注意:
- 写数据的时候,会把整数65转为二进制,也就是01000001。
- 任意的文本编辑器(记事本,nodepad++…,Sublime Text在打开文件的时候,都会去查询编码表,把字节转化为字符进行显示。)
- 0~127 ,查询ASCII码表,如果是其他值,则查询系统默认码表,如果是中文,则查询GBK。
- 所以虽然写入了65,实际上是个A (要不起!)
换行符:
windows系统: \r\n linux系统: \r mac系统: \n
3.3 InputStream【字节输入流】
java.io.InputStream
是一个抽象类,是表示字节输入流的所有类的超类,能够将指定的字节信息写入内存。它定义了字节输入流的基本共性功能方法。
java.io.InputStream
的方法如下:
public int available():返回此输入流下一个方法调用可以不受阻塞地从此输入流读取(或跳过)的估计字节数。此方法应该由子类重写
public void close() :关闭此输入流并释放与其关联的所有系统资源。
public void mark(int readlimit):在此输入流中标记当前位置。
public boolean markSupported():测试此输入流是否支持 mark 和 reset 方法。
public abstract int read():从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果读到流末尾,返回 -1。
public int read(byte[] b):从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。。该方法等同于 read(b, 0, b.length)
public int read(byte[] b, int off, int len):将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len 个字节,但读取的字节也可能小于该值。
public void reset():将此流重新定位到最后一次对此输入流调用 mark 方法时的位置。
public long skip(long n):跳过和丢弃此输入流中的 n 个字节。
3.3.1 FileInputStream【文件字节输入流】
作用: 将数据以字节的形式读取到内存中。
构造方法:
public FileInputStream(File file):通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由File对象 指定。创建一个新 FileDescriptor 对象来表示此文件连接。
public FileInputStream(FileDescriptor fdObj):过使用文件描述符 fdObj 创建一个 FileInputStream,该文件描述符表示到文件系统中某个实际文件的现有连接。
public FileInputStream(String name):通过打开与实际文件的连接来创建一个 FileInputStream,该文件通过文件路径名 name 指定。创建一个新 FileDescriptor 对象来表示此文件连接。
构造方法源码:
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
// 获取系统安全管理器
SecurityManager security = System.getSecurityManager();
if (security != null) {
// 验证是否有读权限
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
// 验证文件路径是否无效
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
// 跟踪
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
}
public FileInputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);
}
fd = fdObj;
path = null;
fd.attach(this);
}
通过源码,可以看出,构造方法底层真正调用的其实都是 public FileInputStream(File file)
这个构造方法。
读取字节的源码:
// 从流中读取一个字节
public int read() throws IOException {
return read0();
}
// 调用系统底层native方法
private native int read0() throws IOException;
// 读取字节数组
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}
// 读取一部分字节数组
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len);
}
// 调用系统底层native方法
private native int readBytes(byte b[], int off, int len) throws IOException;
简单演示:
package com.hanyxx.io;
import java.io.FileInputStream;
import java.io.IOException;
/**
* @author layman
*/
public class Demo02 {
static FileInputStream fis;
public static void main(String[] args) throws IOException {
read01();
read02();
read03();
}
// 循环读取单个字节
private static void read01() throws IOException {
fis = new FileInputStream("layman.txt");
int len;
while((len = fis.read()) != -1){
// 将读取到的字节转为字符,并打印输出
System.out.print((char)len);
}
System.out.println();
fis.close();
}
/**
* int read(byte[] b):从输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。
*/
private static void read02() throws IOException {
fis = new FileInputStream("layman.txt");
// fis.available():获取流中能够读取到的有效字节个数
byte[] bytes = new byte[fis.available()];
int length = fis.read(bytes);
fis.close();
System.out.println("------------------");
System.out.println("读取到的字节个数: " + length);
System.out.println(new String(bytes));
}
/**
* int read(byte[] b, int off, int len) :从此输入流中将最多 len 个字节的数据读入一个 byte 数组中。off:偏移量,不是数组索引
*/
private static void read03() throws IOException {
fis = new FileInputStream("layman.txt");
byte[] bytes = new byte[fis.available()];
int len = fis.read(bytes,2,3);
fis.close();
System.out.println("------------------");
System.out.println("读取到的字节个数: " + len);
System.out.println(new String(bytes));
}
}
运行结果:
BIGHUGE
------------------
读取到的字节个数: 7
BIGHUGE
------------------
读取到的字节个数: 3
BIGBIGHUGE
读取到的字节个数: 7
BIGHUGE
读取到的字节个数: 3
BIG
简单案例(图片复制):
将D:\food.jpg复制为D:\food_copy.jpg
package com.hanyxx.io;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 图片复制
* @author layman
* @date 2021/3/7
*/
public class Demo03 {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
FileInputStream fis = new FileInputStream("D:\\food.jpg");
FileOutputStream fos = new FileOutputStream("D:\\food_copy.jpg");
int len;
//单字节写入
/*while((len = fis.read()) != -1){
fos.write(len);
}*/
// 使用数组缓冲流读取字节(1KB)
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes))!=-1) {
fos.write(bytes, 0 , len);
}
//流的关闭原则:先开后关,后开先关。
fos.close();
fis.close();
long end = System.currentTimeMillis();
System.out.println("复制耗时:" + (end-start) + "毫秒");
}
}