Java学习总结之Java IO系统(一)

概述

java.io 包几乎包含了所有操作输入、输出需要的类。所有这些流类代表了输入源和输出目标。java.io 包中的流支持很多种格式,比如:基本类型、对象、本地化字符集等等。一个流可以理解为一个数据的序列。输入流表示从一个源读取数据,输出流表示向一个目标写数据。Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。

Java 的 I/O 大概可以分成以下几类:

  • 磁盘操作:File
  • 字节操作:InputStream 和 OutputStream
  • 字符操作:Reader 和 Writer
  • 对象操作:Serializable
  • 网络操作:Socket
  • 新的输入/输出:NIO

File

Java中IO操作有相应步骤,以文件操作为例,主要操作流程如下:
1.使用File类打开一个文件
2.通过字节流或字符流的子类,指定输出的位置
3.进行读/写操作
4.关闭输入/输出

那么我们先来介绍一下File类
Java文件类在java.io包中,它以抽象的方式代表文件名和目录路径名。该类主要用于获取文件和目录的属性,文件和目录的创建、查找、删除、重命名等,但不能进行文件的读写操作
File对象代表磁盘中实际存在的文件和目录。通过以下构造方法创建一个File对象。

通过给定的父抽象路径名子路径名字符串创建一个新的File实例。
File(File parent, String child)

通过将给定路径名字符串转换成抽象路径名来创建一个新 File 实例。
File(String pathname)

根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
File(String parent, String child)

通过将给定的 file: URI 转换成一个抽象路径名来创建一个新的 File 实例。
File(URI uri)

注意:
1.在各个操作系统中,路径的分隔符是不一样的,例如:Windows中使用反斜杠:"\",Linux|Unix中使用正斜杠:"/"。在使用反斜杠时要写成"\\"的形式,因为反斜杠要进行转义。如果要让Java保持可移植性,应该使用File类的静态常量File.pathSeparator。
2.构建一个File实例并不会在机器上创建一个文件。不管文件是否存在,都可以创建任意文件名的File实例。可以调用File实例上的exists()方法来判断这个文件是否存在。通过后续的学习我们会知道,当把一个输出流绑定到一个不存在的File实例上时,会自动在机器上创建该文件,如果文件已经存在,把输出流绑定到该文件上则会覆盖该文件,但这些都不是在创建File实例时进行的。

创建File对象成功后,可以使用以下列表中的方法操作文件。

File1.png

File2.png
File3.png
File4.png

下面给出一个使用File类的实例:

import java.io.File;
public class DirList {
   public static void main(String args[]) {
      String dirname = "/java";
      File f1 = new File(dirname);
      if (f1.isDirectory()) {
         System.out.println( "Directory of " + dirname);
         String s[] = f1.list();
         for (int i=0; i < s.length; i++) {
            File f = new File(dirname + "/" + s[i]);
            if (f.isDirectory()) {
               System.out.println(s[i] + " is a directory");
            } else {
               System.out.println(s[i] + " is a file");
            }
         }
      } else {
         System.out.println(dirname + " is not a directory");
    }
  }
}

小贴士:lastModified()方法返回的是从时间戳(1970年1月1日0时0分0秒)到当前的毫秒数,返回值类型是long,可以用Date类对它进行包装使其更易读。

Java中的目录

创建目录:
File类中有两个方法可以用来创建文件夹:

  • mkdir( )方法创建一个文件夹,成功则返回true,失败则返回false。失败表明File对象指定的路径已经存在,或者由于整个路径还不存在,该文件夹不能被创建。
  • mkdirs()方法创建一个文件夹和它的所有父文件夹。
    下面的例子创建 "/tmp/user/java/bin"文件夹:
import java.io.File;
 
public class CreateDir {
  public static void main(String args[]) {
    String dirname = "/tmp/user/java/bin";
    File d = new File(dirname);
    // 现在创建目录
    d.mkdirs();
  }
}

mkdirs是递归创建文件夹,允许在创建某文件夹时其父文件夹不存在,从而一同创建;mkdir必须满足路径上的父文件夹全都存在
注意: Java 在 UNIX 和 Windows 自动按约定分辨文件路径分隔符。如果你在 Windows 版本的 Java 中使用分隔符 (/) ,路径依然能够被正确解析。
读取目录:
一个目录其实就是一个 File 对象,它包含其他文件和文件夹。
如果创建一个 File 对象并且它是一个目录,那么调用 isDirectory() 方法会返回 true。
可以通过调用该对象上的 list() 方法,来提取它包含的文件和文件夹的列表。
下面展示的例子说明如何使用 list() 方法来检查一个文件夹中包含的内容:

import java.io.File;
 
public class DirList {
  public static void main(String args[]) {
    String dirname = "/tmp";
    File f1 = new File(dirname);
    if (f1.isDirectory()) {
      System.out.println( "目录 " + dirname);
      String s[] = f1.list();
      for (int i=0; i < s.length; i++) {
        File f = new File(dirname + "/" + s[i]);
        if (f.isDirectory()) {
          System.out.println(s[i] + " 是一个目录");
        } else {
          System.out.println(s[i] + " 是一个文件");
        }
      }
    } else {
      System.out.println(dirname + " 不是一个目录");
    }
  }
}

删除目录或文件:
删除文件可以使用 java.io.File.delete() 方法。
以下代码会删除目录/tmp/java/,即便目录不为空。
测试目录结构:

/tmp/java/
|-- 1.log
|-- test

deleteFolder是一个递归函数,类似于DFS思想

import java.io.File;
 
public class DeleteFileDemo {
  public static void main(String args[]) {
      // 这里修改为自己的测试目录
    File folder = new File("/tmp/java/");
    deleteFolder(folder);
  }
 
  //删除文件及目录
  public static void deleteFolder(File folder) {
    File[] files = folder.listFiles();
        if(files!=null) { 
            for(File f: files) {
                if(f.isDirectory()) {
                    deleteFolder(f);
                } else {
                    f.delete();
                }
            }
        }
        folder.delete();
    }
}

RandomAccessFile

RandomAccessFile不同于File,它提供了对文件内容的访问,可以读写文件且支持随机访问文件的任意位置
RandomAccessFile读写用到文件指针,它的初始位置为0,可以用getFilePointer()方法获取文件指针的位置。下面是RandomAccessFile常用的方法。

RandomAccessFile.png

public int read(int x) throws IOException 方法只读取一个字节,也就是x的低八位。


import java.io.File ;
import java.io.RandomAccessFile ;
public class RandomAccessFileDemo01{
    // 所有的异常直接抛出,程序中不再进行处理
    public static void main(String args[]) throws Exception{
        File f = new File("d:" + File.separator + "test.txt") ; // 指定要操作的文件
        RandomAccessFile rdf = null ;       // 声明RandomAccessFile类的对象
        rdf = new RandomAccessFile(f,"rw") ;// 读写模式,如果文件不存在,会自动创建
        String name = null ;
        int age = 0 ;
        name = "zhangsan" ;         // 字符串长度为8
        age = 30 ;                  // 数字的长度为4
        rdf.writeBytes(name) ;      // 将姓名写入文件之中
        rdf.writeInt(age) ;         // 将年龄写入文件之中
        name = "lisi    " ;         // 字符串长度为8
        age = 31 ;                  // 数字的长度为4
        rdf.writeBytes(name) ;      // 将姓名写入文件之中
        rdf.writeInt(age) ;         // 将年龄写入文件之中
        name = "wangwu  " ;         // 字符串长度为8
        age = 32 ;                  // 数字的长度为4
        rdf.writeBytes(name) ;      // 将姓名写入文件之中
        rdf.writeInt(age) ;         // 将年龄写入文件之中
        rdf.close() ;               // 关闭
    }
};

写完之后,开始读取数据。写的时候可以将一个字符串写入,读的时候需要一个个的以字节的形式读取出来。


import java.io.File ;
import java.io.RandomAccessFile ;
public class RandomAccessFileDemo02{
    // 所有的异常直接抛出,程序中不再进行处理
    public static void main(String args[]) throws Exception{
        File f = new File("d:" + File.separator + "test.txt") ; // 指定要操作的文件
        RandomAccessFile rdf = null ;       // 声明RandomAccessFile类的对象
        rdf = new RandomAccessFile(f,"r") ;// 以只读的方式打开文件
        String name = null ;
        int age = 0 ;
        byte b[] = new byte[8] ;    // 开辟byte数组
        // 读取第二个人的信息,意味着要空出第一个人的信息
        rdf.skipBytes(12) ;     // 跳过第一个人的信息
        for(int i=0;i<b.length;i++){
            b[i] = rdf.readByte() ; // 读取一个字节
        }
        name = new String(b) ;  // 将读取出来的byte数组变为字符串
        age = rdf.readInt() ;   // 读取数字
        System.out.println("第二个人的信息 --> 姓名:" + name + ";年龄:" + age) ;
        // 读取第一个人的信息
        rdf.seek(0) ;   // 指针回到文件的开头
        for(int i=0;i<b.length;i++){
            b[i] = rdf.readByte() ; // 读取一个字节
        }
        name = new String(b) ;  // 将读取出来的byte数组变为字符串
        age = rdf.readInt() ;   // 读取数字
        System.out.println("第一个人的信息 --> 姓名:" + name + ";年龄:" + age) ;
        rdf.skipBytes(12) ; // 跳过第二个人的信息
        for(int i=0;i<b.length;i++){
            b[i] = rdf.readByte() ; // 读取一个字节
        }
        name = new String(b) ;  // 将读取出来的byte数组变为字符串
        age = rdf.readInt() ;   // 读取数字
        System.out.println("第三个人的信息 --> 姓名:" + name + ";年龄:" + age) ;
        rdf.close() ;               // 关闭
    }
};

结果如下:

result.png

在Java程序中所有的数据都是以的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成。程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。流涉及的领域很广:标准输入输出,文件的操作,网络上的数据流,字符串流,对象流,zip文件流等等。

Stream.png

流具有方向性,至于是输入流还是输出流则是一个相对的概念,一般以程序为参考,如果数据的流向是程序至设备,我们成为输出流,反之我们称为输入流。
可以将流想象成一个“水流管道”,水流就在这管道中形成了,自然就出现了方向的概念。

Information.jpg

先上一个Java IO流类层次图,如前所述,一个流被定义为一个数据序列。输入流用于从源读取数据,输出流用于向目标写数据:

JavaIO流类层次图.png

是不是被吓到了?没关系,我们将通过一个个例子来学习这些功能。

IO流分类

1.按操作数据类型分:字符流和字节流

编码与解码:编码就是把字符转换为字节,而解码是把字节重新组合成字符。
如果编码和解码过程使用不同的编码方式那么就出现了乱码。

  • GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
  • UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
  • UTF-16be 编码中,中文字符和英文字符都占 2 个字节。

UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。

字符流:Java中的字符流处理的最基本的单元是2字节的Unicode码元(char),它通常用来处理文本数据,如字符、字符数组或字符串等。所谓Unicode码元,也就是一个Unicode代码单元,范围是0x0000~0xFFFF。在以上范围内的每个数字都与一个字符相对应,Java中的String类型默认就把字符以Unicode规则编码而后存储在内存中。然而与存储在内存中不同,存储在磁盘上的数据通常有着各种各样的编码方式。使用不同的编码方式,相同的字符会有不同的二进制表示。实际上字符流是这样工作的:

  • 输出字符流:把要写入文件的字符序列(实际上是Unicode码元序列)转为指定编码方式下的字节序列,然后再写入到文件中。
  • 输入字符流:把要读取的字节序列按指定编码方式解码为相应字符序列(实际上是Unicode码元序列从)从而可以存在内存中。

也就是说,所有的文件在硬盘或在传输时都是以字节的方式进行的,包括图片等都是按字节的方式存储的,而字符是只有在内存中才会形成。

字节流:Java中的字节流处理的最基本单位为单个字节(byte),它通常用来处理二进制数据,如果要得到字节对应的字符需要强制类型转换。

两者比较:
1.字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,所以它对多国语言支持性较好,如果要操作中文数据等,用字符流。
2.字符流只用来处理文本数据,字节流还可以用来处理媒体数据,如视频、音频、图片等。
3.字符流的两个抽象基类为Reader和Writer,字节流的两个抽象基类为InputStream和OutputStream。它们的具体子类名以基类名为后缀进行扩展。
4.字节流在操作的时候不会用到缓冲区(内存),是直接对文件本身操作的,而字符流在操作的时候使用缓冲区。

Compare.jpg

以向一个文件输出"Hello world!"为例,我们分别使用字节流和字符流进行输出,且在使用完之后都不关闭流。

使用字节流不关闭执行:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;


public class IOPractice {

    public static void main(String[] args) throws IOException {
        // 第1步:使用File类找到一个文件    
             File f = new File("/home/xiejunyu/"+
             "桌面/text.txt");   
        // 第2步:通过子类实例化父类对象     
             OutputStream out = new FileOutputStream(f); 
        // 通过对象多态性进行实例化    
        // 第3步:进行写操作    
             String str = "Hello World!";      
        // 准备一个字符串    
             byte b[] = str.getBytes();          
        // 字符串转byte数组    
             out.write(b);                      
        // 将内容输出    
         // 第4步:关闭输出流    
            // out.close();                  
        // 此时没有关闭    
                }    
}
1.png

此时没有关闭字节流操作,但是文件中也依然存在了输出的内容,证明字节流是直接操作文件本身的。

使用字符流不关闭执行:

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;


public class IOPractice {

    public static void main(String[] args) throws IOException {
         // 第1步:使用File类找到一个文件    
        File f = new File("/home/xiejunyu/桌面/test.txt");
        // 第2步:通过子类实例化父类对象            
        Writer  out = new FileWriter(f);            
        // 第3步:进行写操作    
        String str = "Hello World!";      
        // 准备一个字符串    
        out.write(str);                    
        // 将内容输出    
        // 第4步:关闭输出流    
        // out.close();
        // 此时没有关闭    
    }    
}
2.png

程序运行后会发现文件中没有任何内容,这是因为字符流操作时使用了缓冲区,而在关闭字符流时会强制性地将缓冲区中的内容进行输出,但是如果程序没有关闭字符流,缓冲区中的内容是无法输出的,所以得出结论:字符流使用了缓冲区,而字节流没有使用缓冲区。如果想让缓冲区中的内容输出,要么关闭流强制刷新缓冲区,要么调用flush方法冲刷缓冲区。可以简单地把缓冲区理解为一段特殊的内存。某些情况下,如果一个程序频繁地操作一个资源(如文件或数据库),则性能会很低,此时为了提升性能,就可以将一部分数据暂时读入到内存的一块区域之中,以后直接从此区域中读取数据即可,因为读取内存速度会比较快,这样可以提升程序的性能。
在字符流的操作中,所有的字符都是在内存中形成的,在输出前会将所有的内容暂时保存在内存之中,所以使用了缓冲区暂存数据。

建议:
1.虽然不关闭字节流不影响数据的输出,且后续JVM会自动回收这部分内存,但还是建议在使用完任何流对象之后关闭流。
2.使用流对象都要声明或抛出IOException
3.在创建一个文件时,如果目录下有同名文件将被覆盖
4.在写文件时,如果文件不存在,会在创建输出流对象并绑定文件时自动创建文件,不必使用File的exists方法提前检测
4.在读取文件时,必须使用File的exists方法提前检测来保证该文件已存在,否则抛出FileNotFoundException

2.按流向分:输入流和输出流

输入流:程序从输入流读取数据源。数据源包括外界(键盘、文件、网络等),即是将数据源读入到程序的通信通道。输入流主要包括两个抽象基类:InputStream(字节输入流)和Reader(字符输入流)及其扩展的具体子类。
输出流:程序向输出流写入数据。将程序中的数据输出到外界(显示器、打印机、文件、网络等)的通信通道。输出流主要包括两个抽象基类:OutputStream(字节输出流)和Writer(字符输出流)及其扩展的具体子类。

3.按功能分:节点流和处理流

按照流是否直接与特定的地方(如磁盘、内存、设备等)相连,分为节点流和处理流两类。
节点流:程序用于直接操作目标设备所对应的类叫节点流。(低级流)
处理流:程序通过一个间接流类去调用节点流类,以达到更加灵活方便地读写各种类型的数据,这个间接流类就是处理流。处理流可以看成是对已存在的流进行连接和封装的流。(高级流)

注意:在使用到处理流对流进行连接和封装时,读写完毕只需关闭处理流,不用关闭节点流。处理流关闭的时候,会调用其处理的节点流的关闭方法。如果将节点流关闭以后再关闭处理流,会抛出IO异常。

(1) 节点流
节点流.png
  • File 文件流。对文件进行读、写操作:FileReader、FileWriter、FileInputStream、FileOutputStream。
  • Memory 流。
    向内存数组读写数据: CharArrayReader与 CharArrayWriter、ByteArrayInputStream与ByteArrayOutputStream。
    向内存字符串读写数据:StringReader、StringWriter、StringBufferInputStream。
  • Pipe管道流:实现管道的输入和输出(进程间通信): PipedReader与PipedWriter、PipedInputStream与PipedOutputStream。
节点流示意图.png
(1) 处理流
处理流.png
  • Buffering缓冲流:在读入或写出时,对数据进行缓存,以减少I/O的次数:BufferedReader与BufferedWriter、BufferedInputStream与BufferedOutputStream。
  • Filtering 滤流:在数据进行读或写时进行过滤:FilterReader与FilterWriter、FilterInputStream与FilterOutputStream。
  • Converting between Bytes and Characters 转换流:按照一定的编码/解码标准将字节流转换为字符流,或进行反向转换(Stream到Reader):InputStreamReader、OutputStreamWriter。
  • Object Serialization 对象流 :ObjectInputStream、ObjectOutputStream。
  • DataConversion数据流:按基本数据类型读、写(处理的数据是Java的基本类型):DataInputStream、DataOutputStream 。
  • Counting计数流:在读入数据时对行记数 :LineNumberReader、LineNumberInputStream。
  • Peeking Ahead预读流: 通过缓存机制,进行预读 :PushbackReader、PushbackInputStream。
  • Printing打印流: 包含方便的打印方法 :PrintWriter、PrintStream。
处理流示意图.png

读取控制台输入

在Java中,从控制台输入有三种方法:

1.使用标准输入流对象System.in

System.in是System中内置的InputStream类对象,它的read方法一次只读入一个字节数据,返回0 ~ 255的一个byte值,一般用来读取一个字符,需要强制类型转换为char类型,而我们通常要取得一个字符串或一组数字,故这种方法不常用。下面给出这种方法的一个例子:

public class CharTest{
public static void main(String[] args) {
         try{   
         System.out.print("Enter a Char:");   
         char i = (char)System.in.read();   
         System.out.println("Yout Enter Char is:" + i);           }   
         catch(IOException e){   
            e.printStackTrace();   
         }   
    }
}

使用这种方法必须提供try-catch块或者在main方法首部声明IOException异常,因为System.in是一个流对象

2.使用Scanner类

Scanner类功能十分强大,可以读入字符串、整数、浮点数、布尔类型值等等。下面是例子:

public class ScannerTest{
public static void main(String[] args){
    Scanner sc = new Scanner(System.in);   
    System.out.println("ScannerTest, Please Enter Name:");   
    String name = sc.nextLine();  //读取字符串型输入   
    System.out.println("ScannerTest, Please Enter Age:");   
    int age = sc.nextInt();    //读取整型输入   
    System.out.println("ScannerTest, Please Enter Salary:");   
    float salary = sc.nextFloat(); //读取float型输入   
    System.out.println("Your Information is as below:");   
    System.out.println("Name:" + name +"\n" + "Age:"+age 
    + "\n"+"Salary:"+salary);   
    }
 }   

注意:
1.用nextXXX()读入XXX类型的数据,XXX可以是除了char外的所有基本数据类型,还可以是BigInteger或BigDecimal,其中凡是整型类型的数据还可以指定radix(进制),可以用next()和nextLine()读取一个字符串或一行字符
2.next()和nextLine()的区别:
next()

  • 一定要读取到有效字符后才可以结束输入。
  • 对输入有效字符之前遇到的空白,next() 方法会自动将其去掉。
  • 只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。
  • next() 不能得到带有空格的字符串,除非用useDelimeter方法修改分隔符。

nextLine()

  • 以Enter为结束符,也就是说 nextLine()方法返回的是输入回车之前的所有字符。
  • 可以获得空白。

3.可以用循环配合hasNextXXX方法判断输入是否继续
4.Scanner类没有直接提供读取一个字符的方法,如果要读取一个字符,有三种方法,一是读入一个字符串后取字符串的第一个字符,二是使用System.in的read方法,三是使用字符流读入

更多Scanner的用法之前已经在Java学习总结之Java基本程序设计结构中总结过了,不再赘述。

3.使用BufferedReader对象

可以把 System.in 包装在一个 BufferedReader 对象中来创建一个字符流
下面是创建 BufferedReader 的基本语法:

BufferedReader br = new BufferedReader(new 
                      InputStreamReader(System.in));

其中,System.in是一个InputStream对象(字节流),使用InputStreamReader作为桥梁,将字节流转换为字符流,然后再使用BufferedReader进行进一步包装。
BufferedReader 对象创建后,我们便可以使用 read() 方法从控制台读取一个字符(读入一个用0~65535之间的整数表示的字符,需要强制类型转换为char类型,如果已到达流末尾,则返回 -1),或者用 readLine() 方法读取一个字符串。下面是例子:

public static void main(String[] args){
//必须要处理java.io.IOException异常
  BufferedReader br = new BufferedReader(new InputStreamReader
  (System.in ));
  //java.io.InputStreamReader继承了Reader类
  String read = null;
  System.out.print("输入数据:");
  try {
   read = br.readLine();
  } catch (IOException e) {
   e.printStackTrace();
  }
  System.out.println("输入数据:"+read);
 }

下面的程序示范了用 read() 方法从控制台不断读取字符直到用户输入 "q"。

// 使用 BufferedReader 在控制台读取字符
 
import java.io.*;
 
public class BRRead {
  public static void main(String args[]) throws IOException
  {
    char c;
    // 使用 System.in 创建 BufferedReader 
    BufferedReader br = new BufferedReader(new 
                       InputStreamReader(System.in));
    System.out.println("输入字符, 按下 'q' 键退出。");
    // 读取字符
    do {
       c = (char) br.read();
       System.out.println(c);
    } while(c != 'q');
  }
}

下面的程序读取和显示字符行直到你输入了单词"end"。

// 使用 BufferedReader 在控制台读取字符
import java.io.*;
public class BRReadLines {
  public static void main(String args[]) throws IOException
  {
    // 使用 System.in 创建 BufferedReader 
    BufferedReader br = new BufferedReader(new
                            InputStreamReader(System.in));
    String str;
    System.out.println("Enter lines of text.");
    System.out.println("Enter 'end' to quit.");
    do {
       str = br.readLine();
       System.out.println(str);
    } while(!str.equals("end"));
  }
}

在ACM等算法竞赛中,我们常常也会使用Java,在输入数据时有以下几点注意:
1.hasXXX等价于C++中读到文件末尾(EOF)
2.使用BufferedReader输入会比Scanner输入快十倍左右!

控制台输出

控制台的输出由 print() 和 println() 完成。这些方法都由类 PrintStream 定义,System.out 是该类的一个对象。
PrintStream 继承了 OutputStream类,并且实现了方法 write()。这样,write() 也可以用来往控制台写操作。
PrintStream 定义 write() 的最简单格式如下所示:
void write(int byteval)该方法将 byteval 的低八位字节写到流中,即System.out的write方法一次只能写一个字节(类比System.in的read方法一次只能读取一个字节)。
下面的例子用 write() 把字符 "A" 和紧跟着的换行符输出到屏幕:

import java.io.*;
 
// 演示 System.out.write().
public class WriteDemo {
   public static void main(String args[]) {
      int b; 
      b = 'A';//向上类型转换
      System.out.write(b);
      System.out.write('\n');
   }
}

注意:write() 方法不经常使用,因为 print() 和 println() 方法用起来更为方便。

字节流(OutputStream、InputStream)

字节流主要是操作byte类型的数据,以byte数组为准,主要操作类是OutputStream、InputStream。
由于文件读写最为常见,我们先讨论两个重要的字节流 FileInputStream(文件输入流) 和 FileOutputStream(文件输出流),分别是抽象类InputStream和OutputStream的具体子类:

FileInputStream
该流用于从文件读取数据,它的对象可以用关键字 new 来创建。
有多种构造方法可用来创建对象。
可以使用字符串类型的文件名来创建一个输入流对象来读取文件:

InputStream f = new FileInputStream("C:/java/hello");

也可以使用一个文件对象来创建一个输入流对象来读取文件。我们首先得使用 File() 方法来创建一个文件对象:

File f = new File("C:/java/hello");
InputStream in = new FileInputStream(f);

创建了InputStream对象,就可以使用下面的方法来读取流或者进行其他的流操作。


InputStream.png

下面是一个例子:

public static void main(String[] args) throws IOException{
    InputStream f  = new FileInputStream
    ("/home/xiejunyu/桌面/test.txt");
    int c = 0;
    while((c =  f.read()) != -1) 
    //这里也可以先用available方法得到可读的字节数
    System.out.println((char)c);
}

当我们需要创建一个byte[]来保存读取的字节时,如果数组太小,无法完整读入数据,如果太大又会造成内存浪费。可以使用File类的length方法得到文件的数据字节数,从而有效确定byte数组的大小。

public static void main(String[] args) {
        // 创建一个FileInputStream对象
        try {
            FileInputStream fis = new FileInputStream("/home/xiejunyu/桌面/test.txt");
            byte[] b=new byte[100];
            fis.read(b,0,5); 
            /*把字节从文件读入b数组,从b数组的0位置开始存放,
            读取5个字节*/
            System.out.println(new String(b));
            fis.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

注意: 每调用一次read方法,当前读取在文件中的位置就会向后移动一个字节或者移动byte[]的长度(read的两个重载方法),已经到文件末尾会返回-1,可以通过read方法返回-1判断是否读到文件末尾,也可以使用available方法返回下一次可以不受阻塞读取的字节数来读取。FileInputStream不支持mark和reset方法进行重复读取。BufferedInputStream支持此操作。

FileOutputStream
该类用来创建一个文件并向文件中写数据。
如果该流在打开文件进行输出前,目标文件不存在,那么该流会创建该文件。
有两个构造方法可以用来创建 FileOutputStream 对象。
使用字符串类型的文件名来创建一个输出流对象:

OutputStream f = new FileOutputStream("C:/java/hello")

也可以使用一个文件对象来创建一个输出流来写文件。我们首先得使用File()方法来创建一个文件对象:

File f = new File("C:/java/hello");
OutputStream f = new FileOutputStream(f);

之前的所有操作中,如果重新执行程序,则肯定会覆盖文件中的已有内容,那么此时就可以通过FileOutputStream向文件中追加内容,FileOutputStream的另外一个构造方法:

public FileOutputStream(File file,boolean append) 

在构造方法中,如果将append的值设置为true,则表示在文件的末尾追加内容。程序代码如下:

File f = new File("C:/java/hello");
OutputStream f = new FileOutputStream(f,true);

创建OutputStream 对象完成后,就可以使用下面的方法来写入流或者进行其他的流操作。

FileOutputStream.png

当有一个字符串时,可以用getBytes方法转为byte数组用于字节流的输出。

下面是一个演示 InputStream 和 OutputStream 用法的例子:

import java.io.*;
 
public class FileStreamTest{
  public static void main(String args[]){
    try{
      byte bWrite[] = "ABC".getBytes();
      OutputStream os = new FileOutputStream("/home/xiejunyu/桌面/test.txt");
      for(int x=0; x < bWrite.length ; x++){
      os.write(bWrite[x] ); // writes the bytes
    }
    os.close();
 
    InputStream is = new FileInputStream("/home/xiejunyu/桌面/test.txt");
    int size = is.available();
 
    for(int i=0; i< size; i++){
      System.out.print((char)is.read() + "  ");
    }
      is.close();
    }catch(IOException e){
      System.out.print("Exception");
    }  
  }
}

上面的程序首先创建文件test.txt,并把给定的数字以二进制形式写进该文件,同时输出到控制台上。
以上代码由于是二进制写入,可能存在乱码,你可以使用以下代码实例来解决乱码问题:

import java.io.*;
 
public class fileStreamTest2{
  public static void main(String[] args) throws IOException {
    
    File f = new File("a.txt");
    FileOutputStream fop = new FileOutputStream(f);
    // 构建FileOutputStream对象,文件不存在会自动新建;如果存在会覆盖原文件
    
    OutputStreamWriter writer = new OutputStreamWriter(fop, "UTF-8");
    // 构建OutputStreamWriter对象,参数可以指定编码,默认为操作系统默认编码,windows上是gbk
    
    writer.append("中文输入");
    // 写入到缓冲区
    
    writer.append("\r\n");
    //换行
    
    writer.append("English");
    // 刷新缓冲区,写入到文件,如果下面已经没有写入的内容了,直接close也会写入
    
    writer.close();
    //关闭写入流,同时会把缓冲区内容写入文件,所以上面的注释掉
    
    fop.close();
    // 关闭输出流,释放系统资源
 
    FileInputStream fip = new FileInputStream(f);
    // 构建FileInputStream对象
    
    InputStreamReader reader = new InputStreamReader(fip, "UTF-8");
    // 构建InputStreamReader对象,编码与写入相同
 
    StringBuffer sb = new StringBuffer();
    while (reader.ready()) {
      sb.append((char) reader.read());
      // 转成char加到StringBuffer对象中
    }
    System.out.println(sb.toString());
    reader.close();
    // 关闭读取流
    
    fip.close();
    // 关闭输入流,释放系统资源
 
  }
}

以上例子证明:在对多国语言的支持上,字符流表现更优,此时应使用字符流而不是字节流。

还可以用InputStream和OutputStream配合进行文件的复制,即读取原件数据,写入副本文件。
复制有两种实现方式:
实现一:将源文件中的内容全部读取进来,之后一次性的写入到目标文件
实现二:边读边写

在实际开发中建议使用边读边写的方式,代码如下:

public static void main(String[] args) {
        // 文件拷贝
        try {
            FileInputStream fis=new FileInputStream("happy.gif");
            FileOutputStream fos=new FileOutputStream("happycopy.gif");
            int n=0;
            byte[] b=new byte[1024];
            while((n=fis.read(b))!=-1){ 
            /*循环读取,每次1024个字节,最后一次可能不满1024。
            后面的字节覆盖前面的字节,不必担心数组溢出。*/
                fos.write(b,0,n); //n是实际读取到的字节数,如果写fos.write(b),会造成最后一次数组未满的情况也写1024个字节,从而造成副本比原件略大
            }
            fis.close();
            fos.close();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }catch(IOException e){
            e.printStackTrace();
        }
    }

实际上边读边写也分为三种方式:
1.批量拷贝(循环读取,每次读入一个byte数组)
2.缓冲拷贝(使用缓冲流)
3.批量+缓冲拷贝(循环批量读取到字节数组中,然后使用缓冲输出流写入到文件)

第三种方式是最快的。

注意:InputStream的int read()方法读取一个字节,并用这个字节填充整型的低八位并返回,OutputStream的void write(int x)写入x的低八位,如果要写入一个int,需要移位并写4次。读写基本数据类型建议使用DataInputStream和DataOutputStream。

后续内容见 Java学习总结之Java IO系统(二)

推荐阅读更多精彩内容