Java输入输出

Java输入输出学习

File类

File类是java.io包下代表与平台无关的文件和目录,也就是说,如果希望在程序中操作文件和目录,都可以通过File类来完成。值得指出的是,不管是文件还是目录都是使用File来操作的,File能新建、删除、重命名文件和目录,File不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。

访问文件和目录

File类可以使用文件路径字符串来创建File实例,该文件路径字符串既可以是绝对路径,也可以是相对路径。在默认情况下,系统总是依据用户的工作路径来解释相对路径,这个路径由系统属性“user.dir”指定,通常也就是运行Java虚拟机时所在的路径。

  1. 访问文件名相关方法

    String getName():返回此File对象所表示的文件名或路径名(如果是路径,则返回最后一级子路径名)。
    String getPath():返回此File对象所对应的路径名。
    File getAbsoluteFile():返回此File对象所对应的绝对路径所对应的File对象。
    String getAbsolutePath():返回此File对象所对应的绝对路径名。
    String getParent():返回此File对象所对应目录(最后一级子目录)的父目录名。
    boolean renameTo(File newName):重命名此File对象所对应的文件或目录,如果重命名成功,则返回true;否则返回false。
    
  2. 文件检测相关方法

    boolean exists():判断File对象所对应的文件或目录是否存在。
    boolean canWrite():判断File对象所对应的文件和目录是否可写。
    boolean canRead():判断File对象所对应的文件和目录是否可读。
    boolean isFile():判断File对象所对应的是否是文件,而不是目录。
    boolean isDirectory():判断File对象所对应的是否是目录,而不是文件。
    boolean isAbsolute():判断File对象所对应的文件或目录是否是绝对路径。该方法消除了不同平台的差异,可以直接判断File对象是否为绝对路径。在UNIX/Linux/BSD等系统上,如果路径名开头是一条斜线(/),则表明该File对象对应一个绝对路径;在Windows等系统上,如果路径开头是盘符,则说明它是一个绝对路径。
    
  3. 获取文件信息

    long lastModified():返回文件的最后修改时间。
    long length():返回文件内容的长度。
    
  4. 文件操作相关方法

    boolean createNewFile():当此File对象所对应的文件不存在时,该方法将新建一个该File对象所指定的新文件,如果创建成功则返回true;否则返回false。
    boolean delete():删除File对象所对应的文件或路径。
    static File createTempFile(String prefix, String suffix):在默认的临时文件目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名。这是一个静态方法,可以直接通过File类来调用。prefix参数必须至少是3个字节长。建议前缀使用一个短的、有意义的字符串,比如"hjb"或"mail"。suffix参数可以为null,在这种情况下,将使用默认的后缀“.tmp”。
    static File createTempFile(String prefix, String suffix, File directory) :在directory所指定的目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名。这是一个静态方法,可以直接通过File类来调用。
    void deleteOnExit():注册一个删除钩子,指定当Java虚拟机退出时,删除File对象所对应的文件和目录。
    
  5. 目录操作相关方法

    boolean mkdir():试图创建一个File对象所对应的目录,如果创建成功,则返回true;否则返回false。调用该方法时File对象必须对应一个路径,而不是一个文件。
    String[] list():列出File对象的所有子文件名和路径名,返回String数组。
    File[] listFiles():列出File对象的所有子文件和路径,返回File数组。
    static File[] listRoots():列出系统所有的根路径。这是一个静态方法,可以直接通过File类来调用。上面详细列出了File类的常用方法,下面程序以几个简单方法来测试一下File类的功能。
    

示例:

 public static void main(String[] args) throws IOException {
        File file=new File(".");//以当前路径创建File对象
        System.out.println(file.getName());//获取文件名,输出一点
        System.out.println(file.getParent());//获取相对路径父路径可能出错,输出null
        System.out.println(file.getAbsoluteFile());//获取绝对路径
        //在当前路径下创建临时文件tmpFile.
        File tmpFile=File.createTempFile("aaa",".txt",file);
        tmpFile.deleteOnExit();//当JVM退出时删除该文件
        File newFile=new File(System.currentTimeMillis()+"");//以系统当前时间为文件名创建新文件
        System.out.println("newFile是否存在"+newFile.exists());
        newFile.createNewFile();//以newFile对象来创建一个文件
        newFile.mkdir();//以newFile对象来创建一个目录,因为newFile已经存在,所以返回false
        String [] fileList=file.list();//列出当前路径下所有文件和路径
        System.out.println("--------------------当前路径下的文件和路径--------------");
        for(String fileName:fileList){
            System.out.println(fileName);
        }
        //listRoot()静态方法列出所有的磁盘根路径
        System.out.println("---------------------系统所有根路径---------------");
        File [] roots=File.listRoots();
        for (File root: roots
             ) {
            System.out.println(root);
        }
    }

运行上面程序,可以看到程序列出当前路径的所有文件和路径时,列出了程序创建的临时文件,但程序运行结束后,aaa.txt临时文件并不存在,因为程序指定虚拟机退出时自动删除该文件。

当使用相对路径的File对象来获取父路径时可能引起错误,因为该方法返回将File对象所对应的目录名、文件名里最后一个子目录名、子文件名删除后的结果

注意

Windows的路径分隔符使用反斜线(),而Java程序中的反斜线表示转义字符,所以如果需要在Windows的路径下包括反斜线,则应该使用两条反斜线,如F:\abc\test.txt,或者直接使用斜线(/)也可以,Java程序支持将斜线当成平台无关的路径分隔符。

文件过滤器

在File类的list()方法中可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。这里的FilenameFilter接口和javax.swing.filechooser包下的FileFilter抽象类的功能非常相似,可以把FileFilter当成FilenameFilter的实现类,但可能Sun在设计它们时产生了一些小小遗漏,所以没有让FileFilter实现FilenameFilter接口。

FilenameFilter接口里包含了一个accept(File dir, String name)方法,该方法将依次对指定File的所有子目录或者文件进行迭代,如果该方法返回true,则list()方法会列出该子目录或者文件。

public static void main(String[] args)  {
    File file =new File(".");
    String nameList=file.list(new MyFilenameFilter());
    for(String name:nameList){
         System.out.println(name);
    }
}
class MyFilenameFilter implements FilenameFilter{
    public boolean accept(File dir,String name){
        return name.endWith(".java")||new File(name).isDirectory();
    }
}

当前路径下所有的*.java文件以及文件夹被列出.

注意

这种用法是一个典型的Command设计模式,因为File的list()方法需要一个判断规则:判断哪些文件应该被列出——这段判断规则需要一个代码块,但目前的JDK版本不支持直接向方法传入代码块,所以Java使用了FilenameFilter的accept()方法来封装该代码块。也就是说,所传入的MyFilenameFilter对象的作用就是传入accept()方法的方法体,该方法体指定哪些文件应该被列出。

理解IO流

Java的IO流是实现输入/输出的基础,它可以方便地实现数据的输入/输出操作,在Java中把不同的输入/输出源(键盘、文件、网络连接等)抽象表述为“流”(stream),通过流的方式允许Java程序使用相同的方式来访问不同的输入/输出源。stream是从起源(source)到接收(sink)的有序数据。

流的分类

  1. 输入和输出流

    输入流:只能从中读取数据,而不能向其写入数据。

    输出流:只能向其写入数据,而不能从中读取数据。

  2. 字节和字符流

    字节流和字符流的用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同——字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。字节流主要由InputStream和OutputStream作为基类,而字符流则主要由Reader和Writer作为基类。

  3. 节点流和处理流

    可以从/向一个特定的IO设备(如磁盘、网络)读/写数据的流,称为节点流,节点流也被称为低级流(Low Level Stream)

    处理流则用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。处理流也被称为高级流。当使用处理流进行输入/输出时,程序并不会直接连接到实际的数据源,没有和实际的输入/输出节点连接。使用处理流的一个明显好处是,只要使用相同的处理流,程序就可以采用完全相同的输入/输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据源也相应地发生变化。

注意

实际上,Java使用处理流来包装节点流是一种典型的装饰器设计模式,通过使用处理流来包装不同的节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入/输出功能。因此处理流也被称为包装流。

Java的IO流共涉及40多个类,这些类看上去芜杂而凌乱,但实际上非常规则,而且彼此之间存在非常紧密的联系。Java的IO流的40多个类都是从如下4个抽象基类派生的。

InputStream/Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流。

OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。

字节流和字符流

InputStream和Reader

InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例来执行输入,但它们将成为所有输入流的模板,所以它们的方法是所有输入流都可使用的方法。

在InputStream里包含如下3个方法:

int read():从输入流中读取单个字节,返回所读取的字节数据(字节数据可直接转换为int类型)。
int read(byte[] b):从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数。
int read(byte[] b, int off, int len):从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入数组b中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数。

在Reader里包含如下3个方法:

int read():从输入流中读取单个字符(相当于从图15.5所示的水管中取出一滴水),返回所读取的字符数据(字符数据可直接转换为int类型)。
int read(char[] cbuf):从输入流中最多读取cbuf.length个字符的数据,并将其存储在字符数组cbuf中,返回实际读取的字符数。
int read(char[] cbuf, int off, int len):从输入流中最多读取len个字符的数据,并将其存储在字符数组cbuf中,放入数组cbuf中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字符数。

InputStream和Reader都是抽象类,本身不能创建实例,但它们分别有一个用于读取文件的输入流:FileInputStream和FileReader,它们都是节点流——会直接和指定文件关联。

程序如何判断到了最后呢?直到read(char[] cbuf)或read(byte[] b)方法返回-1,即表明到了输入流的结束点。

使用FileInputStream来读取:

public static void main(String[] args) throws IOException {
   FileInputStream fis =new FileInputStream("E:\temp\hello.txt");
   //创建1024字节的数组
   byte [] buff=new byte[1024];
   int hasRead=0;//保存实际读取的字节数
   while((hasRode=fis.read(buff))>0){
       //取出字节,将字节数组转换成字符串输入
       System.out.println(new String (buff,0,hasRead));
   }
   //关闭文件输入流
   fis.close();
}

使用FileReader来读取文件:

public static void main(String[] args)  {
    FileReader fileReader=new FileReader("E:\temp\hello.txt");
    char [] cbuff =new char[32];
    int hasRead=0;//保存实际读取的字节数
    while((hasRead=fileReader.read(cbuff))>0){
        //取出字节,将字节数组转换成字符串输入
        System.out.println(new String (cbuff,0,hasRead));
    }
    //关闭文件输入流
    fis.close();
}

这两个程序并没有太大的不同,程序只是将字符数组的长度改为32,这意味着程序需要多次调用read()方法才可以完全读取输入流的全部数据。

InputStream和Reader还支持如下几个方法来移动记录指针:

 void mark(int readAheadLimit):在记录指针当前位置记录一个标记(mark)。
 boolean markSupported():判断此输入流是否支持mark()操作,即是否支持记录标记。
 void reset():将此流的记录指针重新定位到上一次记录标记(mark)的位置。
 long skip(long n):记录指针向前移动n个字节/字符。

OutputStream和Writer

OutputStream和Writer也非常相似,它们采用如图15.6所示的模型来执行输出,两个流都提供了如下3个方法:

void write(int c):将指定的字节/字符输出到输出流中,其中c既可以代表字节,也可以代表字符。
void write(byte[]/char[] buf):将字节数组/字符数组中的数据输出到指定输出流中。
void write(byte[]/char[] buf, int off, int len):将字节数组/字符数组中从off位置开始,长度为len的字节/字符输出到输出流中。

因为字符流直接以字符作为操作单位,所以Writer可以用字符串来代替字符数组,即以String对象作为参数。Writer里还包含如下两个方法:

void write(String str):将str字符串里包含的字符输出到指定输出流中。
void write(String str, int off, int len):将str字符串里从off位置开始,长度为len的字符输出到指定输出流中。

下面程序使用FileInputStream来执行输入,并使用FileOutputStream来执行输出,用以实现复制文件的功能:

public static void main(String[] args) throws IOException {
       FileInputStream fileInputStream=new FileInputStream("E:\temp\hello.txt");
       FileOutputStream fileOutputStream=new FileOutputStream("E:\temp\newFile.txt");
       byte [] bytes=new byte[32];
       int hasRead=0;
       while ((hasRead=fileInputStream.read(bytes))>0){
           fileOutputStream.write(bytes,0,hasRead);
       }
        fileInputStream.close();
       fileOutputStream.close();
    }

运行上面程序,将看到系统路径下多了一个文件:newFile.txt,该文件的内容和hello.txt文件的内容完全相同。

如果希望直接输出字符串内容,则使用Writer会有更好的效果,如下程序所示:

FileWriter fileWriter=new FileWriter("E:\temp\poem.txt");
      fileWriter.write("锦瑟 - 李商隐
");
      fileWriter.write("锦瑟无端五十弦,一弦一柱思华年

");
      fileWriter.write("庄生晓梦迷蝴蝶,望帝春心托杜鹃

");
      fileWriter.write("沧海明月珠有泪,蓝天日暖玉生烟

");
      fileWriter.write("此情可待成追忆,只是当时已惘然

");
      fileWriter.close();

将会在当前目录下输出一个poem.txt文件,文件内容就是程序中输出的内容。

输入输出流

处理流

处理流可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入/输出方法,让程序员只需关心高级流的操作。因此,我们使用处理流时的典型思路是,使用处理流来包装节点流,程序通过处理流来执行输入/输出功能,让节点流与底层的I/O设备、文件交互。

关于使用处理流的优势,归纳起来就是2点:①对开发人员来说,使用处理流进行输入/输出操作更简单;②使用处理流的执行效率更高。

示例:使用PrintStream处理流来包装OutputStream,使用处理流后的输出流在输出时将更加方便:

public static void main(String[] args) throws IOException {
     FileOutputStream fileOutputStream=new FileOutputStream("E:\temp\newFile.txt");
     PrintStream printStream=new PrintStream(fileOutputStream);
     //使用PrintStream执行输出
        printStream.println("字符串");
        printStream.println(new test());
        printStream.close();
    }

从前面的代码可以看出,程序使用处理流非常简单,通常只需要在创建处理流时传入一个节点流作为构造器参数即可,这样创建的处理流就是包装了该节点流的处理流。

在使用处理流包装了底层节点流之后,关闭输入/输出流资源时,只要关闭最上层的处理流即可。关闭最上层的处理流时,系统会自动关闭被该处理流包装的节点流。

输入输出流体系

通常来说,我们认为字节流的功能比字符流的功能强大,因为计算机里所有的数据都是二进制的,而字节流可以处理所有的二进制文件——但问题是,如果使用字节流来处理文本文件,则需要使用合适的方式把这些字节转换成字符,这就增加了编程的复杂度。所以通常有一个规则:如果进行输入/输出的内容是文本内容,则应该考虑使用字符流;如果进行输入/输出的内容是二进制内容,则应该考虑使用字节流。

注意

我们常常会把计算机的文件分为文本文件和二进制文件两大类——所有能用记事本打开并看到其中字符内容的文件称为文本文件,反之则称为二进制文件。但实质是,计算机里的所有文件都是二进制文件,文本文件只是二进制文件的一种特例,当二进制文件里的内容恰好能被正常解析成字符时,则该二进制文件就变成了文本文件。更甚至于,即使是正常的文本文件,如果打开该文件时强制使用了“错误”的字符集,例如使用notepad++打开刚刚生成的poem.txt文件时指定使用UTF-8字符集,如图15.9所示,则将看到打开的poem.txt文件内容变成了乱码。因此,如果希望看到正常的文本文件内容,则必须在打开文件时与保存文件时使用相同的字符集(Windows下简体中文默认使用GBK字符集,而Linux下简体中文默认使用UTF-8字符集)。

转换流

输入/输出流体系中还提供了两个转换流,这两个转换流用于实现将字节流转换成字符流,其中InputStreamReader将字节输入流转换成字符输入流,OutputStreamWriter将字节输出流转换成字符输出流。

下面以获取键盘输入为例来介绍转换流的用法。Java使用System.in代表标准输入,即键盘输入,但这个标准输入流是InputStream类的实例,使用不太方便,而且键盘输入内容都是文本内容,所以可以使用InputStreamReader将其转换成字符输入流,普通的Reader读取输入内容时依然不太方便,我们可以将普通的Reader再次包装成BufferedReader,利用BufferedReader的readLine()方法可以一次读取一行内容。如下程序所示:

public static void main(String[] args) throws IOException {
        InputStreamReader reader=new InputStreamReader(System.in);
        BufferedReader bufferedReader=new BufferedReader(reader);
        String buffer=null;
        while ((buffer=bufferedReader.readLine())!=null){
            if(buffer.equals("exit")){
                System.exit(1);
            }
            System.out.println("输入内容为:"+buffer);
        }
    }

上面程序中的粗体字代码负责将System.in包装成BufferedReader,BufferedReader流具有缓冲功能,它可以一次读取一行文本——以换行符为标志,如果它没有读到换行符,则程序阻塞,等到读到换行符为止。运行上面程序可以发现这个特征,当我们在控制台执行输入时,只有按下回车键,程序才会打印出刚刚输入的内容。

重定向标准输入输出

Java的标准输入/输出分别通过System.in和System.out来代表,在默认情况下它们分别代表键盘和显示器,当程序通过System.in来获取输入时,实际上是从键盘读取输入;当程序试图通过System.out执行输出时,程序总是输出到屏幕。

在System类里提供了如下3个重定向标准输入/输出的方法:

static void setErr(PrintStream err):重定向 “标准”错误输出流。
static void setIn(InputStream in):重定向“标准”输入流。
static void setOut(PrintStream out):重定向 “标准”输出流。

示例:

PrintStream ps=new PrintStream(new FileOutputStream("temp.txt"));
System.setOut(ps);
System.out.println("普通字符串");
System.out.println(new test());

上面程序中的粗体字代码创建了一个PrintStream输出流,并将系统的标准输出重定向到该PrintStream输出流。运行上面程序时将看不到任何输出——这意味着标准输出不再输出到屏幕,而是输出到out.txt文件,运行结束后,打开系统当前路径下的out.txt文件,即可看到文件里的内容,正好与程序中的输出一致。

对象序列化

对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流(无论是从磁盘中获取的,还是通过网络获取的),都可以将这种二进制流恢复成原来的Java对象。

序列化的意义

对象的序列化(Serialize)指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则指从IO流中恢复该Java对象。如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的(serializable)。为了让某个类是可序列化的,该类必须实现如下两个接口之一:

Serializable

Externalizable

Java的很多类已经实现了Serializable,该接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的。

所有可能在网络上传输的对象的类都应该是可序列化的,否则程序将会出现异常,比如RMI(Remote Method Invoke,即远程方法调用,是JavaEE的基础)过程中的参数和返回值;所有需要保存到磁盘里的对象的类都必须可序列化,比如Web应用中需要保存到HttpSession或ServletContext属性的Java对象。

因为序列化是RMI过程的参数和返回值都必须实现的机制,而RMI又是Java EE技术的基础——所有的分布式应用常常需要跨平台、跨网络,所以要求所有传递的参数、返回值必须实现序列化。因此序列化机制是Java EE平台的基础。通常建议:程序创建的每个JavaBean类都实现

NIO

新IO和传统的IO有相同的目的,都是用于进行输入/输出,但新IO使用了不同的方式来处理输入/输出,新IO采用内存映射文件的方式来处理输入/输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念),通过这种方式来进行输入/输出比传统的输入/输出要快得多。

Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象,Channel是对传统的输入/输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输;Channel与传统的InputStream、OutputStream最大的区别在于它提供了一个map()方法,通过该map()方法可以直接将“一块数据”映射到内存中。如果说传统的输入/输出系统是面向流的处理,则新IO则是面向块的处理。

NIO.2

Java 7对原有的NIO进行了重大改进,改进主要包括如下两方面的内容。

提供了全面的文件IO和文件系统访问支持。

基于异步Channel的IO。

第一个改进表现为Java7新增的java.nio.file包及各个子包;第二个改进表现为Java7在java.nio.channels包下增加了多个以Asynchronous开头的Channel接口和类。Java7把这种改进称为NIO.2

作者:Loserfromlazy
本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。
原文地址:https://www.cnblogs.com/yhr520/p/12703617.html