Java_IO字节流上(16)

字节流
字节流

ByteArrayInputStream 和 ByteArrayOutputStream

ByteArrayInputStream 字节数组输入流,内部创建一个字节数组缓冲区来接收字节数据,并定义了索引来标记 read 方法读取下一个字节的位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Test {
  public static void main(String[] args) {
    //对应字符串为abcde
    byte[] byteArr = {0x61, 0x62, 0x63, 0x64, 0x65};
    //内部创建一个字节数组来接收byteArr
    ByteArrayInputStream bais = new ByteArrayInputStream(byteArr);
    //读取2个字节
    for (int i = 0; i < 2; i++) {
      if (bais.available() >= 0) {
        // 读取字节流的下一个字节
        int tmp = bais.read();
        System.out.println(Integer.toHexString(tmp)); //61 62
      }
    }
    //标记下一个读取的位置,之前已经读了2个字节,所以下一个读取的是第3个字节
    //ByteArrayInputStream的mark方法没有marklimit限制的功能,传参无所谓
    //mark与reset配套的,reset会将字节流中下一个读取位置重置为mark标记的位置
    bais.mark(0);
    // 跳过1个字节,所以下一个读取的是第4个字节即0x64
    bais.skip(1);
    //读取两个字节保存到buf中,此时读取了0x64和0x65
    byte[] buf = new byte[2];
    bais.read(buf, 0, 2);
    //将buf转换为String
    String str = new String(buf);
    System.out.println(str); //de
    //将下一个读取位置重置到之前的标记处,即第三个节点0x63
    bais.reset();
    bais.read(buf, 0, 2);
    String str2 = new String(buf);
    System.out.println(str2); //cd
  }
}

ByteArrayOutputStream 字节数组输出流,写入的数据保存在其内部的字节数组缓冲区,缓冲区默认大小 32 个字节,可以自动扩容,可通过 toByteArray 和 toString 方法获取数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test {
  public static void main(String[] args) {
    //对应字符串为abcde
    byte[] byteArr = {0x61, 0x62, 0x63, 0x64, 0x65};
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    //将byteArr数组中从索引1开始的后2个字节写入到baos中
    baos.write(byteArr, 1, 2);
    System.out.println(baos); //bc
    //查看缓冲区数组长度
    System.out.println(baos.size()); //2
    //变回byte[]数组
    byte[] buf = baos.toByteArray();
    String str = new String(buf);
    System.out.println(str);  //bc
    //将baos写到另一个输出流中
    ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
    try {
      baos.writeTo(baos2);
      System.out.println(baos2); //bc
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

PipedInputStream 和 PipedOutputStream

Piped 管道流,可用于线程之间的通信,PipedInputStream 必须与 PipedOutputStream 配套使用,大致流程为,连接了管道后,线程 A 向 PipedOutputStream 写入数据,PipieOutputStream 会调用 PipedInputStream 的 receive 方法将数据写到 PipedInputStream 的缓冲区,线程 B 再通过 PipedInputStream 从缓冲区读取数据,PipedInputStream 的默认缓冲区大小为 1024 字节

PipedInputStream 每次 read 前都会判断缓存区是否有数据,如果没有数据则让出当前的锁让写先执行,当写入的数据超出了缓冲区大小后,会调用 notifyAll(),再 wait(1000),目的是等待缓冲区的数据读出,即每隔 1000ms 检查一下缓冲区是否有剩余空间

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Test {
  public static void main(String[] args) {
    Sender t1 = new Sender();
    Receiver t2 = new Receiver();
    PipedOutputStream out = t1.getOutputStream();
    PipedInputStream in = t2.getInputStream();
    try {
      //管道连接,两种连接方式等效
      //out.connect(in);
      in.connect(out);
      //使线程开始执行
      t1.start();
      t2.start();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//发送线程
class Sender extends Thread {
  private PipedOutputStream out = new PipedOutputStream();
  public PipedOutputStream getOutputStream() {
    return out;
  }
  @Override
  public void run() {
    //writeShortMessage();
    writeLongMessage();
  }
  //向管道输出流写入短数据
  private void writeShortMessage() {
    String strInfo = "short message" ;
    try {
      out.write(strInfo.getBytes());
      out.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  //向管道输出流写入长数据
  private void writeLongMessage() {
    StringBuilder sb = new StringBuilder();
    //1020个字节
    for (int i = 0; i < 102; i++) {
      sb.append("0123456789");
    }
    //26个字节
    sb.append("abcdefghijklmnopqrstuvwxyz");
    //str总长度为1046个字节
    String str = sb.toString();
    try {
      //写入管道输出流1046个字节
      out.write(str.getBytes());
      out.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//接收线程
class Receiver extends Thread {
  private PipedInputStream in = new PipedInputStream();
  public PipedInputStream getInputStream() {
    return in;
  }
  @Override
  public void run() {
    //readMessageOnce() ;
    readMessageContinued();
  }

  //从管道输入流中读取一次数据
  public void readMessageOnce() {
    byte[] buf = new byte[1024];
    try {
      int len = in.read(buf);
      System.out.println(new String(buf,0,len));
      in.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  //从管道输入流读取数据,大于1024个字节时就停止读取
  public void readMessageContinued() {
    while(true) {
      byte[] buf = new byte[1024];
      try {
        int len = in.read(buf);
        System.out.println(new String(buf,0,len));
        //读满1024则继续读,小于1024则跳出循环
        if (len < 1024) break;
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    try {
      in.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

FileDescriptor 和 File

用户程序访问文件需要通过内核,而内核抽象了文件描述符来管理文件,Java 对在内核层面的文件描述符进行抽象提供了 FileDescriptor 类,但不能直接用来进行 I/O 操作,还需要创建对应的流对象,通过流对象才能进行 I/O 操作

例如,FileDescriptor 类中定义了标准输出 out 的文件描述符对象,但进行 I/O 操作的还是 out 对应的 FileOutputStream

1
2
3
4
5
6
7
8
public class Test {
  public static void main(String[] args) throws IOException {
    //通过out对象创建对应的FileOutputStream来进行输出
    FileOutputStream os = new FileOutputStream(FileDescriptor.out);
    os.write('a'); //a
    os.close();
  }
}

FileDescriptor 与系统内核的文件描述符关联,属于底层操作,对于标准输入、输出、错误,Java 提供了封装的对象可直接使用

1
2
3
System.out.print("a");
System.err.print("err");
Scanner scan = new Scanner(System.in);

Java 还提供了对文件进行抽象的 File 类用来屏蔽文件描述符,File 类直接继承于 Object 类,并且实现了 Serializable 和 Comparable 接口,即 File 支持序列化且可以比较大小,下面代码即通过 File 绑定 file.txt 并创建了对应的流对象,虽然用户不能自己设置文件描述符,但可以从已创建的流对象中获取,然后可以通过获取到的文件描述符对象再创建出对应的流对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Test {
  public static void main(String[] args) {
    try {
      //通过File创建FileOutputStream对象
      File file = new File("file.txt");
      FileOutputStream out1 = new FileOutputStream(file);
      //获取file.txt对应的文件描述符
      FileDescriptor fd = out1.getFD();
      //通过文件描述符创建FileOutputStream对象
      FileOutputStream out2 = new FileOutputStream(fd);
      out1.write('A');
      out2.write('a');
      out1.close();
      out2.close();
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
}

File 只是文件及其路径的抽象,对文件进行读写还是需要 FileDescriptor,查看 FileOutputStream 的构造函数可见到,通过 File 构建的方法会 new 一个 FileDescriptor 对象,File 只是用来绑定文件路径

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//新建目录
public class Test {
  public static void main(String[] args) {
    //根据相对路径新建单层目录
    File dir1 = new File("dir");
    dir1.mkdir();
    //根据绝对路径新建单层目录
    File dir2 = new File("D:/dir");
    dir2.mkdir();
    //新建子目录,需要父目录dir已存在
    File sub1 = new File("dir", "sub1");
    sub1.mkdir();
    File sub2 = new File(dir1, "sub2");
    sub2.mkdir();
    //新建多层目录
    File sub3 = new File("dir/sub3");
    sub3.mkdirs();
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//新建文件名
public class Test {
  public static void main(String[] args) throws IOException {
    File dir = new File("dir");
    dir.mkdir();
    //创建的文件所在的目录要保证已存在
    File file1 = new File(dir, "file1.txt");
    file1.createNewFile();
    File file2 = new File("dir", "file2.txt");
    file2.createNewFile();
    //绝对路径创建文件
    new File("D:/dir").mkdir();
    File file3 = new File("D:/dir/file4.txt");
    file3.createNewFile();
  }
}

FileInputStream 和 FileOutputStream

FileInputStream 文件输入流,可以从文件读取字节,FileOutputStream 文件输出流,可以将数据写入 File 或 FileDescriptor 的输出流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Test {
  public static void main(String[] args) {
    testWrite();
    testRead();
  }
  private static void testWrite() {
    try {
      //关联文件如果不存在则自动创建
      File file = new File("file.txt");
      //默认新建模式,即清空文件原内容后再重新写入
      FileOutputStream out1 = new FileOutputStream(file);
      byte[] byteArr1 = {0x61, 0x62};
      out1.write(byteArr1); //可以直接写入一个字节数组
      out1.write(997); //997是int类型,但会自动转换为byte写入文件
      out1.close();
      //开启追加模式,即在文件原来的内容后面继续写入
      FileOutputStream out2 = new FileOutputStream(file, true);
      byte[] byteArr2 = {0x64, 0x65, 0x66};
      out2.write(byteArr2);
      out2.close();
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
  private static void testRead() {
    try {
      File file = new File("file.txt");
      FileInputStream in1 = new FileInputStream(file);
      //FileInputStream in2 = new FileInputStream("file.txt");
      //FileDescriptor fd = in2.getFD();
      //FileInputStream in3 = new FileInputStream(fd);
      char c1 = (char) in1.read();
      System.out.println(c1); //a
      //跳过1个字节
      in1.skip(1);
      byte[] buf = new byte[4];
      in1.read(buf, 0, buf.length);
      System.out.println(new String(buf)); //cdef
      System.out.println(in1.read()); //-1 表示文件的结束EOF
      //可通过while读完整个文件
      //while (in1.read() != -1) {}
      in1.close();
      //in2.close();
      //in3.close();
    } catch(IOException e) {
      e.printStackTrace();
    }
  }
}

read 读取的是一个字节但返回的是 int,因为如果读到 11111111,若返回 byte 则为 -1,导致读取停止,所以可以提升为默认的 int 防止读取中断

FileInputStream 内部没有缓冲区,可以使用带有缓冲区的 BufferedInputStream


以上内容是玉山整理的笔记,如有错误还请指出