[08][01][01] I/O流--基本概念

I/O 是什么

I/O 指的是输入/输出(Input/Output),在 Java 中 I/O 操作主要是指使用 Java 进行输入和输出操作

I/O 分类

IO可以按照数据处理方式分为以下两类

  • 流 I/O:传输过程是以字节流形式进行的,这样的设备是不需要缓冲机制的,简单易用但效率较低
  • 块 I/O:把数据打包成块进行传输,传输基本单位为块,传输过程中需要缓冲区(buffer)支持,读写也是以块作为基本单位,效率很高但编程比较复杂

数据流

数据流是一串连续不断的数据的集合,数据写入程序可以是一段、一段地向数据流管道中写入数据,这些数据段会按先后顺序形成一个长的数据流

对数据读取程序来说,看不到数据流在写入时的分段情况,每次可以读取其中的任意长度的数据,但只能先读取前面的数据后,再读取后面的数据

不管写入时是将数据分多次写入,还是作为一个整体一次写入,读取时的效果都是完全一样的

数据流可以分为两类(以内存为判断标准)

  • 输入流:从外界读取数据写入内存
  • 输出流:从内存中读取数据到外界

I/O 体系结构

整个 java.io 包中最重要的就是五个类和一个接口

  • 五个类:File、OutputStream、InputStream、Writer、Reader
  • 一个接口:Serializable

I/O结构体系

在学习过重很容易混淆,分不清是字节流还是字符流,看每个类的最后这个单词,如果是 Stream 的话就是字节流,如果是 Reader/Writer 的话就是字符流

Java 中字符是采用 Unicode 标准,一个字符是16位,即一个字符使用两个字节来表示,Java 中引入了处理字符的流

I/O 流分类

  • 文件流
  • 管道流
  • 字节/字符数组流
  • Buffered 缓冲流
  • 转化流
  • 数据流
  • 打印流
  • 对象流
  • 序列化流
  • 非流类

I/O流分类

文件流

  • FileInputStream(字节输入流)
  • FileOutputStream(字节输出流)
  • FileReader(字符输入流)
  • FileWriter(字符输出流)

管道流

  • PipedInputStream(字节输入流)
  • PipedOutStream(字节输出流)
  • PipedReader(字符输入流)
  • PipedWriter(字符输出流)

PipedInputStream的一个实例要和PipedOutputStream的一个实例共同使用,共同完成管道的读取写入操作,主要用于线程操作

字节/字符数组流

  • ByteArrayInputStream
  • ByteArrayOutputStream
  • CharArrayReader
  • CharArrayWriter

在内存中开辟了一个字节或字符数组

Buffered 缓冲流

  • BufferedInputStream
  • BufferedOutputStream
  • BufferedReader
  • BufferedWriter

是带缓冲区的处理流,缓冲区的作用的主要目的是:避免每次和硬盘打交道,提高数据访问的效率

转化流

  • InputStreamReader
  • OutputStreamWriter

把字节转化成字符

数据流

  • DataInputStream
  • DataOutputStream

因为平时若是我们输出一个8个字节的long类型或4个字节的float类型,那怎么办呢?
可以一个字节一个字节输出,也可以把转换成字符串输出,但是这样转换费时间,若是直接输出该多好啊,因此这个数据流就解决了我们输出数据类型的困难

数据流可以直接输出float类型或long类型,提高了数据读写的效率

打印流

  • PrintStream
  • PrintWriter

一般是打印到控制台,可以进行控制打印的地方

对象流

  • ObjectInputStream
  • ObjectOutputStream

把封装的对象直接输出,而不是一个个在转换成字符串再输出

序列化流

  • SequenceInputStream

对象序列化:把对象直接转换成二进制,写入介质中

非流类

  • RandomAccessFile 从文件的任意位置进行存取(输入输出)操作
  • File 提供了描述文件和目录的操作与管理方法,主要用于命名文件、查询文件属性和处理文件目录

InputStream

InputStream:为字节输入流,它本身为一个抽象类,必须依靠其子类实现各种功能,是所有字节输入流类的超类,继承自 InputStream 的流都是向内存中输入数据,且数据单位为字节(8bit)

常用方法

// 读取一个 byte 的数据,返回值是高位补0的 int 类型值。若返回值=-1说明没有读取到任何字节读取工作结束
abstract int read()
// 读取 b.length 个字节的数据放到b数组中,返回值是读取的字节数
int read(byte b[]) 
// 读取 len 个字节的数据,存放到偏移量为off的b数组中,返回值是读取的字节数
int read(byte b[], int off, int len)
// 返回输入流中可以读取的字节数。注意:若输入阻塞,当前线程将被挂起,如果 InputStream 对象调用这个方法的话
//它只会返回0,这个方法必须由继承 InputStream 类的子类对象调用才有用
int available() 
// 忽略输入流中的 n 个字节,返回值是实际忽略的字节数, 跳过一些字节来读取
long skip(long n)   
// 我们在使用完后,必须对我们打开的流进行关闭
int close()

源码解读

/**
 * 读取 len 个字节的数据,存放到偏移量为off的b数组中,返回值是读取的字节数
 */
public int read(byte b[], int off, int len) throws IOException {
    if (b == null) {
        throw new NullPointerException();
    } else if (off < 0 || len < 0 || len > b.length - off) {
        // 如果b[].length - 偏移量off < 读取字节数 len,抛出异常
        // 如果读取字节数大于 b[] 从偏移量off到最后的元素个数,意味着无法把数据全部装入 b[]
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return 0;
    }
    // 父类调用子类的 read() 实现
    int c = read();
    if (c == -1) {
        return -1;
    }
    b[off] = (byte)c;

    int i = 1;
    try {
        // 依次循环从输入流中读取一个字节存储到 b[] 中
        for (; i < len ; i++) {
            c = read();
            if (c == -1)
                break;
            }
            b[off + i] = (byte)c;
        }
    } catch (IOException ee) {
    }
    return i;
}

OutputStream

OutputStream:为字节输出流,它本身为一个抽象类,必须依靠其子类实现各种功能,是所有字节输出流类的超类,继承自 OutputStream 的流都是从内存中输出数据,且数据单位为字节(8bit)

常用方法

// 输出一个字节, 抽象方法,子类实现
void write(int b)
// 将字节数组 b[] 写入输出流
void write(byte b[])
// 将字节数组 b[],从 off 位置开始,把 len 个字节写入输出流
write(byte b[], int off, int len)
// 强制把缓冲区内容输出,刷空输出流
flush()
// 关闭
close()

源码解读

/**
 * 输出字节到字节数组 b[],从 off 位置开始,数量为 len
 */
public void write(byte b[], int off, int len) throws IOException {
    if (b == null) {
        throw new NullPointerException();
    } else if ((off < 0) || (off > b.length) || (len < 0) ||
               ((off + len) > b.length) || ((off + len) < 0)) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return;
    }
    // 遍历一个个调用子类实现的 write(int b) 方法,输出字节
    for (int i = 0 ; i < len ; i++) {
        write(b[off + i]);
    }
}

Reader

用于读取字符流的抽象类

常用方法

// 将字符流存入缓冲区 target,返回实际存入缓冲区的数量,返回 -1 表示已存满
int read(java.nio.CharBuffer target)
// 读取一个字符存入字符流,返回值为读取的字符
int read()
// 读取一系列字符存入数组 cbuf[] 中,返回值为实际读取的字符的数量
int read(char cbuf[])
// 读取 len 个字符,从数组 cbuf[] 的下标 off 处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现
abstract int read(char cbuf[],int off,int len)
// 跳过指定的字符数 n,返回实际跳过的字符数
long skip(long n)
// 判断这个字节流是否能被读
boolean ready()
// 标记字符位置
void mark(int readAheadLimit)
// 重新回到标记的字符
void reset()
// 关闭字符输入流
abstract void close()

源码解读

/**
 * 此方法可以把字符输入流的数据保存到缓冲区中,返回实际存进缓冲区的数量,如果为-1表示已经到底了
 */
public int read(java.nio.CharBuffer target) throws IOException {
    // 获得缓冲区还可以缓冲字符的长度
    int len = target.remaining();
    char[] cbuf = new char[len];
    // 调用 read(char cbuf[], int off, int len)方法,读取 len 个字符到字符数组 cbuf,偏移量 off,返回实际读取字符的数量 n
    int n = read(cbuf, 0, len);
    //如果有读取,就把读取到的字符存进字符缓冲区中
    if (n > 0)
        target.put(cbuf, 0, n);
    return n;
}


/**
 * 跳过指定的字符数 n,返回实际跳过的字符数
 */
public long skip(long n) throws IOException {
	if (n < 0L)
		throw new IllegalArgumentException("skip value is negative");
	// nn 为一次跳过的字符数,不能大于maxSkipBufferSize。
	int nn = (int) Math.min(n, maxSkipBufferSize);
    // 做同步锁,防止多线程访问的时候出现奇怪的逻辑现象
	synchronized (lock) {
		if ((skipBuffer == null) || (skipBuffer.length < nn))
			//就会重新创建一个nn长度的字符数组
			skipBuffer = new char[nn];
		long r = n;//用于储存还需要跳过的字符数
		while (r > 0) {
			//注意这里的(int)Math.min(r, nn),和前面的(int) Math.min(n, maxSkipBufferSize)搭配。
			int nc = read(skipBuffer, 0, (int)Math.min(r, nn));
			//如果输入流已经到底了,直接跳出循环
			if (nc == -1)
				break;
			//减去已经跳过的字符数
			r -= nc;
		}
		//需要跳过的字符数-还需跳过的字符数=实际跳过的字符数。
		return n - r;
	}
}

Writer

写入字符流的抽象类

常用方法

// 将整型值 c 写入输出流
void write(int c)
// 将字符数组 cbuf[] 写入输出流 
void write(char cbuf[]) 
// 将字符数组 cbuf[] 中的从索引为 off 的位置处开始的 len 个字符写入输出流(子类实现)
abstract void write(char cbuf[],int off,int len)
// 将字符串 str 中的字符写入输出流
void write(String str)
// 将字符串str 中从索引off开始处的len个字符写入输出流
void write(String str,int off,int len)
// 刷空输出流,并输出所有被缓存的字节
abstract void flush( )
// 关闭流
abstract void close()

源码解读

/**
 * 将整型 c 写入输出流
 */
public void write(int c) throws IOException {
    synchronized (lock) {
        if (writeBuffer == null){
            writeBuffer = new char[WRITE_BUFFER_SIZE];
        }
        writeBuffer[0] = (char) c;
        write(writeBuffer, 0, 1);
    }
}

/**
 * 将字符串str 中从索引off开始处的len个字符写入输出流
 */
public void write(String str, int off, int len) throws IOException {
    synchronized (lock) {
        char cbuf[];
        // 判断写入元素个数 len 与 缓冲 buffer 大小,以较大的值为 buffer 容量
        if (len <= WRITE_BUFFER_SIZE) {
            if (writeBuffer == null) {
                writeBuffer = new char[WRITE_BUFFER_SIZE];
            }
            cbuf = writeBuffer;
        } else {    // Don't permanently allocate very large buffers.
            cbuf = new char[len];
        }
        // 调用 String 的 getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) 方法
        // 此方法是把字符串的起始位置 srcBegin 到结束位置 srcEnd这个区间内的字符串,拷贝到字符数组中,并从偏移量 dstBegin 位置开始存储
        str.getChars(off, (off + len), cbuf, 0);
        // 把字符数组写入输出流
        write(cbuf, 0, len);
    }
}

/**
 * 将指定的字符序列追加到输出流
 */
public Writer append(CharSequence csq) throws IOException {
    if (csq == null)
        write("null");
    else
        // 最终调用的是 write(String str, int off, int len)
        write(csq.toString());
    return this;
}

public Writer append(CharSequence csq, int start, int end) throws IOException {
    CharSequence cs = (csq == null ? "null" : csq);
    // CharSequence 的 subSequence(int start, int end) 方法是获取从起始位置 start 到结束位置 end之间的 CharSequence
    write(cs.subSequence(start, end).toString());
    return this;
}

CharSequence是一个描述字符串结构的接口,在这个接口里面一般发现有三种常用的子类:String, StringBuffer, StirngBuilder

File

IO流操作中大部分都是对文件的操作,所以Java就提供了File类供我们来操作文件

File: 文件和目录(文件夹)路径名的抽象表示形式

常用方法

/**
 * 构造方法
 **/
 
// 根据一个路径得到File对象
File(String pathname)
// 根据一个目录和一个子文件/目录得到File对象
File(String parent, String child)
// 根据一个父File对象和一个子文件/目录得到File对象
File(File parent, String child)

/**
 * 创建功能
 **/

// 创建文件 如果存在这样的文件,就不创建了
boolean createNewFile()
// 创建文件夹 如果存在这样的文件夹,就不创建了
boolean mkdir()
// 创建文件夹,如果父文件夹不存在,会帮你创建出来  
boolean mkdirs()

/**
 * 删除功能
 **/

// 它可以删除文件和文件夹(目录),但要删除一个文件夹时,请注意该文件夹内不能包含文件或者文件夹
boolean delete()

/**
 * 重命名功能
 **/

// 如果路径名相同,就是改名,如果路径名不同,就是改名并剪切
boolean renameTo(File dest)
                
/**
 * 判断功能
 **/

// 判断是否是目录
boolean isDirectory()
// 判断是否是文件
boolean isFile()
// 判断是否存在
boolean exists()
// 判断是否可读
boolean canRead()
// 判断是否可写
boolean canWrite()
// 判断是否隐藏
boolean isHidden()

/**
 * 获取功能
 **/

// 获取绝对路径
String getAbsolutePath()
// 获取相对路径
String getPath()
// 获取名称
String getName()
// 获取长度。字节数
long length()
// 获取最后一次的修改时间,毫秒值
long lastModified()

/**
 * 高级获取功能
 **/

// 获取指定目录下的所有文件或者文件夹的名称数组
String[] list()
// 获取指定目录下的所有文件或者文件夹的File数组
File[] listFiles()

/**
 * 过滤器功能
 **/

String[] list(FilenameFilter filter)
File[] listFiles(FilenameFilter filter)

源码解读

再看 File 类源码之前需要先学习 FileSystem
FileSystem是一个文件创建的抽象类,jdk中是 UnixFileSystem 继承此类,并做了接口的实现,在 File 类中是以参数名 fs 存在

File类的四个构造方法

/**
 * 根据一个路径得到File对象
 **/
public File(String pathname) {
    if (pathname == null) {
        throw new NullPointerException();
    }
    // 校验路径的合法性
    this.path = fs.normalize(pathname);
    // 判断文件路径是否以 / 符号开始
    this.prefixLength = fs.prefixLength(this.path);
}

/**
 * 根据一个目录和一个子文件/目录得到 File 对象
 **/ 
public File(String parent, String child) {
    if (child == null) {
        throw new NullPointerException();
    }
    if (parent != null) {
        if (parent.equals("")) {
            this.path = fs.resolve(fs.getDefaultParent(),
                                   fs.normalize(child));
        } else {
            this.path = fs.resolve(fs.normalize(parent),
                                   fs.normalize(child));
        }
    } else {
        this.path = fs.normalize(child);
    }
    this.prefixLength = fs.prefixLength(this.path);
}


/**
 * 根据一个父 File 对象和一个子文件/目录得到 File 对象
 **/ 
public File(File parent, String child) {
    if (child == null) {
        throw new NullPointerException();
    }
    if (parent != null) {
        if (parent.path.equals("")) {
            this.path = fs.resolve(fs.getDefaultParent(),
                                   fs.normalize(child));
        } else {
            this.path = fs.resolve(parent.path,
                                   fs.normalize(child));
        }
    } else {
        this.path = fs.normalize(child);
    }
    this.prefixLength = fs.prefixLength(this.path);
}

/**
 * 根据 URI 得到 File 对象
 **/ 
public File(URI uri) {

    // Check our many preconditions
    if (!uri.isAbsolute())
        throw new IllegalArgumentException("URI is not absolute");
    if (uri.isOpaque())
        throw new IllegalArgumentException("URI is not hierarchical");
    String scheme = uri.getScheme();
    if ((scheme == null) || !scheme.equalsIgnoreCase("file"))
        throw new IllegalArgumentException("URI scheme is not \"file\"");
    if (uri.getAuthority() != null)
        throw new IllegalArgumentException("URI has an authority component");
    if (uri.getFragment() != null)
        throw new IllegalArgumentException("URI has a fragment component");
    if (uri.getQuery() != null)
        throw new IllegalArgumentException("URI has a query component");
    String p = uri.getPath();
    if (p.equals(""))
        throw new IllegalArgumentException("URI path component is empty");

    // Okay, now initialize
    p = fs.fromURIPath(p);
    if (File.separatorChar != '/')
        p = p.replace('/', File.separatorChar);
    this.path = fs.normalize(p);
    this.prefixLength = fs.prefixLength(this.path);
}
发布了29 篇原创文章 · 获赞 10 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/csharpqiuqiu/article/details/104808701