【备战秋招】IO

File对象

在计算机系统中,文件是非常重要的存储方式。Java的标准库java.io提供了File对象来操作文件和目录。

要构造一个File对象,需要传入文件路径:

File f = new File("C:\\Windows\\notepad.exe");

构造File对象时,既可以传入绝对路径,也可以传入相对路径

Windows平台使用\作为路径分隔(Java字符串中需要用\\表示一个),Linux平台使用/作为路径分隔符.

  • 相对路径
    .表示当前目录..表示上级目录
// 绝对路径是C:\Docs\sub\javac
File f1 = new File("sub\\javac"); 
// 绝对路径是C:\Docs\sub\javac
File f3 = new File(".\\sub\\javac"); 
// 绝对路径是C:\sub\javac
File f3 = new File("..\\sub\\javac");
  • 绝对路径
File f = new File("C:\\Windows\\notepad.exe");

getPath()返回构造方法传入的路径

getAbsolutePath()返回绝对路径
C:\Users\ThinkPad\IdeaProjects\Algorithm…
getCanonicalPath()它和绝对路径类似,但是返回的是规范路径。
C:\Users\ThinkPad\IdeaProjects

File.separator根据当前平台打印" \ “或” / "

文件和目录

File对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。

isFile()判断该File对象是否是一个已存在的文件

isDirectory()判断该File对象是否是一个已存在的文件

canRead()是否可读

canWrite()是否可写

canExecute()是否可执行

length()文件字节大小

创建和删除文件

File file = new File("/path/to/file");
if (file.createNewFile()) {
    // 文件创建成功:
    // TODO:
    if (file.delete()) {
        // 删除文件成功:
    }
}

createTempFile() 来创建一个临时文件
deleteOnExit() 在JVM退出时自动删除该文件

遍历文件和目录

list()listFiles()列出目录下的文件和子目录名.
listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录:

File f = new File("C:\\Windows");
File[] f1 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
    public boolean accept(File dir, String name) {
        return name.endsWith(".exe"); // 返回true表示接受该文件
    }
});

boolean mkdir():创建当前File对象表示的目录;
boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来
boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功

path对象

 public static void main(String[] args) throws IOException {
     // 构造一个Path对象
     Path p1 = Paths.get(".", "project", "study"); 
     // 转换为绝对路径
     Path p2 = p1.toAbsolutePath(); 
     // 转换为规范路径
     Path p3 = p2.normalize(); 
     // 转换为File对象
     File f = p3.toFile(); 
     // 可以直接遍历Path
     for (Path p : Paths.get("..").toAbsolutePath()) {
         System.out.println("  " + p);
     }
 }

InputStream

InputStream就是Java标准库提供的最基本的输入流.InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类

public abstract int read() throws IOException;

这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回 -1表示不能继续读取 了。

 FileInputStream fis = new FileInputStream("src/readme.txt");
 while (true){
     int n = fis.read();
     if (n == -1){
         break;
     }
     System.out.println(n);
 }
 fis.close(); 

我们需要用try … finally来保证InputStream在无论是否发生IO错误的时候都能够正确地关闭:

public void readFile() throws IOException {
    InputStream input = null;
    try {
        input = new FileInputStream("src/readme.txt");
        int n;
        while ((n = input.read()) != -1) { // 利用while同时读取并判断
            System.out.println(n);
        }
    } finally {
        if (input != null) { 
            input.close(); 
        }
    }
} 

用try … finally来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource) 的语法,只需要编写try语句,让编译器自动为我们关闭资源。

public void readFile() throws IOException {
    try (InputStream input = new FileInputStream("src/readme.txt")) {
        int n;
        while ((n = input.read()) != -1) {
            System.out.println(n);
        }
    } // 编译器在此自动为我们写入finally并调用close()
} 

缓冲

在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:

  • int read(byte[] b):读取若干字节并填充到byte[]数组返回读取的字节数
  • int read(byte[] b, int off, int len):指定byte[]数组的偏移量最大填充数
    利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回 -1,表示没有更多的数据了。

阻塞

在调用InputStreamread() 方法读取数据时,我们说read()方法是阻塞(Blocking)的。

int n;
// 必须等待read()方法返回才能执行下一行代码
n = input.read(); 
int m = n;

执行到第二行代码时,必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间

读取classpath资源

从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把default.properties文件放到classpath中,就不用关心它的实际存放路径。

try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
    if (input != null) {
        // TODO:
    }
}

如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:

Properties props = new Properties();
props.load(inputStreamFromClassPath("/default.properties"));
props.load(inputStreamFromFile("./conf.properties"));

ByteArrayInputStream
ByteArrayInputStream实际上是把一个byte[] 数组在内存中变成一个InputStream,虽然实际应用不多,但测试的时候,可以用它来构造一个InputStream

byte[] data = { 72, 101, 108, 108, 111, 33 };
InputStream input = new ByteArrayInputStream(data);

ZipInputStream
要创建一个ZipInputStream,通常是传入一个FileInputStream作为数据源,然后,循环调用getNextEntry(),直到返回null,表示zip流结束。一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,我们就用read()方法不断读取,直到返回-1:

try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
    ZipEntry entry = null;
    while ((entry = zip.getNextEntry()) != null) {
        String name = entry.getName();
        if (!entry.isDirectory()) {
            int n;
            while ((n = zip.read()) != -1) {
                ...
            }
        }
    }
}

OutputStream

InputStream相反,OutputStream是Java标准库提供的最基本的输出流。
OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b).和InputStream一样,OutputStream的write() 方法也是阻塞的。

public abstract void write(int b) throws IOException;

要注意的是,虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分(相当于b & 0xff)。

close()关闭输出流,以便释放系统资源
flush()向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。

  OutputStream os = new FileOutputStream("src/a.txt");
  os.write("hello".getBytes("UTF-8"));
  os.write(72);
  os.close();

Filter模式(装饰器模式)

在这里插入图片描述
为了解决依赖继承会导致子类数量失控的问题,JDK首先将InputStream分为两大类:

一类是直接提供数据的基础InputStream,例如:

  • FileInputStream
  • ByteArrayInputStream
  • ServletInputStream

一类是提供额外附加功能的InputStream,例如:

  • BufferedInputStream
  • DigestInputStream
  • CipherInputStream
//数据来源自文件
InputStream file = new FileInputStream("test.gz");
//提供缓冲的功能来提高读取的效率
InputStream buffered = new BufferedInputStream(file);
//直接读取解压缩的内容
InputStream gzip = new GZIPInputStream(buffered);

无论我们包装多少次,得到的对象始终是InputStream
在这里插入图片描述

序列化

序列化后可以把byte[] 保存到文件中,或者把byte[] 通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:

public interface Serializable {
      //标记接口
}
 public static void main(String[] args) throws IOException {
     ByteArrayOutputStream buffer = new ByteArrayOutputStream();
     ObjectOutputStream output = new ObjectOutputStream(buffer);
     
     output.writeInt(12345);
     output.writeUTF("hello");
     output.writeObject(Double.valueOf(123.456));
     
     System.out.println(Arrays.toString(buffer.toByteArray()));
 }

反序列化

ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取Java对象:

try (ObjectInputStream input = new ObjectInputStream(...)) {
    int n = input.readInt();
    String s = input.readUTF();
    Double d = (Double) input.readObject();
}

readObject() 可能抛出的异常有:

  • ClassNotFoundException没有找到对应的Class
    对于ClassNotFoundException,这种情况常见于一台电脑上的Java程序把一个Java对象,例如,Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化

  • InvalidClassExceptionClass不匹配
    对于InvalidClassException,这种情况常见于序列化的Person对象定义了一个int类型的age字段,但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。

Java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”

public class Person implements Serializable {
    private static final long serialVersionUID = 2709425275741743919L;
}

反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。 实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String) 的内容,而不存储任何与代码相关的信息。

Reader

Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取:

InputStreamReader
字节流,以byte为单位字符流,以char为单位
读取字节(-1,0~255)读取字符(-1,0~65535)
读到字节数组:int read(byte[] b)读到字符数组:int read(char[] c)

java.io.Reader是所有字符输入流的超类,它最主要的方法是:

public int read() throws IOException;
//一次性读取若干字符
public int read(char[] c) throws IOException

FileReader

public void readFile() throws IOException {
    // 创建一个FileReader对象:
    Reader reader = new FileReader("src/readme.txt",StandardCharsets.UTF_8); // 字符编码是???
        char[] buffer = new char[1000];
        int n;
        while ((n = reader.read(buffer)) != -1) {
            System.out.println("read " + n + " chars.");
        }
    reader.close(); // 关闭流
}

如果文件中包含中文,就会出现乱码,因为FileReader默认的编码与系统相关,例如,Windows系统的默认编码可能是GBK,打开一个UTF-8编码的文本文件就会出现乱码。

CharArrayReader

try (Reader reader = new CharArrayReader("Hello".toCharArray())) 
{
}

StringReader

try (Reader reader = new StringReader("Hello")) {

}

InputStreamReader

除了特殊的CharArrayReader和StringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。

// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");

Writer

Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。

OutputStreamWriter
字节流,以byte为单位字符流,以char为单位
写入字节(0~255):void write(int b)写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b)写入字符数组:void write(char[] c)
无对应方法写入String:void write(String s)

Writer是所有字符输出流的超类,它提供的方法主要有:

  • 写入一个字符(0~65535):void write(int c)
  • 写入字符数组的所有字符:void write(char[] c)
  • 写入String表示的所有字符:void write(String s)

PrintStream

PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:

  • 写入int:print(int)
  • 写入boolean:print(boolean)
  • 写入String:print(String)
  • 写入Object:print(Object),实际上相当于print(object.toString())

以及对应的一组println()方法,它会自动加上换行符。

System.out是系统默认提供的PrintStream
System.err是系统默认提供的标准错误输出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值