Java基础篇笔记(一):Java IO

一、File类

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

File类可以使用文件路径字符串来创建File实例,该文件路径字符串既可以是绝对路径,也可以是相对路径。

一旦创建File对象后,就可以调用File对象的方法来访问,File类提供了很多方法来操作文件和目录。下面列出一些常见方法:

1.String getName() 返回此File对象所表示文件对应的文件名。
2.String getPath() 返回此File对象所对应的路径名。
3.String getAbsolutePath() 返回此File对象的绝对路径名。
4.String getParent() 返回此File对象所对应目录的父目录名。
5.boolean renameTo(File newName) 重命名此File对象所对应的文件或目录,如果重命名成功则返回true,否则返回false。
6.long length() 返回文件内容的长度。
7.boolean delete() 删除File对象所对应的文件或路径。
8.static File createTempFile(String prefix,String suffix,File directory) 再directory所指定的目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定的后缀作为文件名。
9.boolean createNewFile() 当此File对象所对应的文件不存在时,该方法将新建一个该File对象所指定的新文件,如果创建成功返回true,否则返回false。

二、理解Java的IO流

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

输入流:只能从中读取数据,不能向其写入数据。主要由InputStream(字节)和Reader(字符)作为基类。
输出流:只能向其写入数据,不能从中读取数据。主要由OutputStream(字节)和Writer(字符)作为基类。
字符流和字节流的用法几乎一样,区别在于字节流和字符流所操作的数据单元不同。字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。

按照流的角色可以把流分为节点流和处理流:可以从/向一个特定的IO设备(如磁盘、网络)读写数据的流,称为节点流,节点流也被称为低级流;处理流则用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读写功能,处理流也被称为高级流。
处理流的功能主要体现在:性能的提高——主要以增加缓冲的方式来提高输入/输出的效率;操作的边界——处理流可能提供了一系列边界的方法来一次输入/输出大批量的内容。

三、字节流和字符流

字节流和字符流的操作方式几乎完全一样,区别只是操作的数据单元不同而已。
在InputStream中包含如下三个方法:

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

实际操作一下:

public class FileInputStreamTest {
    public static void main(String[] args) throws IOException {
        FileInputStream fis=new FileInputStream("D:\\新建文件夹\\src\\FileInputStreamTest.java");
        byte[] bbuf=new byte[1024];
        int hasRead=0;
        while((hasRead=fis.read(bbuf))>0){
            //0即从第0位开始读取
            System.out.println(new String(bbuf,0,hasRead));
        }
        fis.close();/*关闭文件输入流*/
    }
}
输出结果:
上述代码

当然,Reader中也可以有上面三种方法,不过读取的是字符。
对于代码行FileStream fis=new FileInputStream("完整路径...FileInputStreamTest.java");注意是完整路径。
与JDBC编程一样,程序里打开的文件IO资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应当.close();手动关闭文件IO资源。
要注意使用read方法时要先创建一个数组,即“竹筒”,再创建一个hasRead来保存实际读取的字符数。

在OutputStream中包含三个方法:

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

Writer可以用字符串来代替字符数组,故还包含两个方法:

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

实际操作:

public class FileOutputSteramTest {
    public static void main(String[] args){
        try(
                FileInputStream fis=new FileInputStream("D:\\新建文件夹\\src\\FileOutputSteramTest.java");
                FileOutputStream fos=new FileOutputStream("newFile.txt");
                )
        {
            byte[] bbuf=new byte[32];
            int hasRead=0;
            while((hasRead=fis.read(bbuf))>0){
                fos.write(bbuf,0,hasRead);
            }
        }
        catch(IOException ex){
            ex.printStackTrace();
        }
    }
}

运行上述代码,可以看到在当前路径下生成了一个newFile.txt文件,文件中内容为上述完整代码。

四、输入输出流体系

前面的4个基类使用起来有些繁琐,如果希望简化编程,可以使用处理流。处理流可以隐藏底层设备上节点流的差异,并对外提供更加方便地输入/输出方法。使用处理流地典型思路是:使用处理流来包装节点流,程序通过处理流来执行输入/输出功能,让节点流与底层地I/O设备、文件交互。
处理流的优势:对开发人员来说,使用处理流进行输入/输出操作更简单;使用处理流的执行效率更高。

使用PrintStream处理流来包装OutputStream:

public class PrintStreamTest {
    public static void main(String[] args){
        try(
                FileOutputStream fos=new FileOutputStream("D:\\新建文件夹\\text.txt");
                PrintStream ps=new PrintStream(fos);
        )
        {
            ps.println("普通字符串");/*在text.txt中输出普通字符串*/
            ps.println(new PrintStreamTest());/*直接输出对象*/
        }
        catch(IOException e){
            e.printStackTrace();
        }
    }
}

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

输入输出体系中有两个特殊的流与众不同,即PushbackInputStream和PushbackReader,它们都提供了如下三个方法:

void unread(byte[]/char[] buf) 将一个字节/字符数组内容推回缓冲区里,从而允许重复的读取刚刚读取的内容。
void unread(byte[]/char[] b,int off,int len) 将一个字节/字符数组里从off开始,长度为len字节/字符的内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。
void unread(int b) 将一个字节/字符推回到推回缓冲区里,从而允许重复刚刚读取的内容。

这两个推回输入流都带有一个推回缓冲区,当程序调用这两个推回输入流的unread()方法时,系统将会把指定数组的内容推回到该缓冲区里,而推回输入流每次调用read()方法时总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有装满read()所需的数组时才会从原输入流中读取。
当程序创建一个PushbackInputStream和PushbackReader时需要指定推回缓冲区的大小,默认的推回缓冲区的长度为1.如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发 Pushback buffer overflow 的IOException异常。

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

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

下面程序通过重定向标准输出流,将System.out的输出重定向到文件输出,而不是在屏幕上输出:

public class RedirectIn {
    public static void main(String[] args){
        try(FileInputStream fis=new FileInputStream("D:\\复习QAQ\\src\\RedirectIn.java"))
        {
            //重定向标准输入
            System.setIn(fis);
            Scanner sc=new Scanner(System.in);
            //把回车作为分隔符
            sc.useDelimiter("\n");
            while(sc.hasNext()){
                //输出输入项(即该代码)
                System.out.println("内容是:"+sc.next());
            }
        }
        catch (IOException ioe){
            ioe.printStackTrace();
        }
    }
}
//从输出结果看出,程序不再等待键盘输入,而是直接把RedirectIn.java文件的内容输出,即把改文件作为标准输入源。

如果试图让子进程读取程序中的数据,应该用输出流。站在Java程序的角度来考虑,子进程读取Java程序的数据,就是让Java程序把数据输出到子进程中(就像把数据输出到文件中一样,只是显现由子进程节点代替了文件节点),所以应该使用输出流。

RandomAccessFile类详解
RandomAccessFile是Java输入输出流体系中功能最丰富的文件内容访问类,它既可以读取文件内容,也可以向文件输出数据。它支持“随机访问”的方式,程序可以直接跳转到文件的任意地方来读写数据。所以如果只需要访问文件部分内容,使用它是最好的选择。它也可以向已存在的文件后追加内容。不过它也有个最大的局限就是只能读写文件,不能读写其他IO节点。
RandomAccessFile包含了如下两个方法来操作文件记录指针:

long getFilePointer() 返回文件记录指针的当前位置。
void seek(long pos) 将文件记录指针定位到pos位置。

创建RandomAccessFile对象时还需要制定一个mode参数:

“r” 以只读的方式打开指定文件。如果试图对该RandomAccessFile执行写入方法,都将抛出IOException异常。
“rw” 以读、写方式打开指定文件。如果该文件尚不存在,则尝试创建该文件。
“rws” 以读、写方式打开指定文件。相对于”rw”模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
“rwd” 以读写方式打开指定文件。相对于"rw"模式,还要求对文件内容的每个更新都同步写入到底层存储设备。

用该类读取部分数据:

public class RandomAccessFileTest {
    public static void main(String[] args){
        try(
                RandomAccessFile raf=new RandomAccessFile("D:\\新建文件夹\\src\\RandomAccessFileTest.java","r");
                )
        {
            System.out.println("RandomAccessFile的文件指针的初始位置"+raf.getFilePointer());
            raf.seek(300);/*将文件记录指定到pos(此处为300)位置*/
            byte[] bbuf=new byte[1024];/*用于保存实际读取的字节数*/
            int hasRead=0;
            while((hasRead=raf.read(bbuf))>0){
                System.out.println(new String(bbuf,0,hasRead));/*讲字节数组转换成字符串输入*/
            }
        }
        catch (IOException ex){
            ex.printStackTrace();
        }
    }
}

用该类在指定文件后追加内容,为了追加内容,程序应该先讲记录指针移动到文件最后,然后开始向文件中输出内容:

public class AppendContent {
    public static void main(String[] args){
        try(
                RandomAccessFile raf=new RandomAccessFile("out.txt","rw")
                )
        {
            raf.seek(raf.length());/*每次将记录指针移动到文件最后,进行追加*/
            raf.write("追加的内容\r\n".getBytes());
        }
        catch (IOException ex){
            ex.printStackTrace();
        }
    }
}

RandomAccessFile依然不能向文件的指定位置插入内容,如果直接将文件记录指针移动到中间某位置后开始输出,则新输出的内容会覆盖文件中原有的内容。如果需要向指定位置插入内容,程序需要先把插入点后面的内容读入缓冲区,等把需要插入的数据写入文件后,再将缓冲区的内容追加到文件后面。

五、Java9改进后的对象序列化

对象序列化即把一个对象以字节流的形式保存到磁盘中,反序列化即把一个字节流恢复为原来的对象
对象序列化的目标是将对象保存到磁盘中,或者允许在网络中直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流,都可以将这种二进制流恢复为原来的Java对象。
序列化机制允许将实现序列化的Java对象转换成字节序列。序列化指将一个Java对象写入IO流中,与此对应,反序列化则指从IO流中恢复该Java对象。
Java9增强了对象序列化机制,它允许对读入的序列化数据进行过滤,这种过滤可在反序列化之前对数据执行校验,从而提高安全性和健壮性。如果需要让某个对象支持序列化的机制,则必须让它的类是可序列化的(必须实现Serializable或Externalizable)。public class class_name implements java.io.Serializable{...}
所有可能在网络上传输的对象的类都应该是可序列化的,否则程序将会出现异常,比如RMI过程中的参数和返回值;所有需要保存到磁盘里的对象的类都必须可序列化。 通常建议:程序创建的每个JavaBean类都实现Serializable。

序列化的一般步骤:1.创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("路径.txt"));;2.调用ObjectOutputStream对象中的writeObject()方法输出可序列化的对象oos.writeObject(对象名);
下面示范一下如何将一个Person对象写入磁盘文件:

class Person implements java.io.Serializable{
    private String name;
    private int age;
    /*此处没有提供无参数的构造器*/
    public Person(String name,int age){
        System.out.println("有参数的构造器");
        this.name=name;
        this.age=age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public class WriteObject {
    public static void main(String[] args){
        try(ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("object.txt")))
        {
            Person p=new Person("huanweiming",18);
            oos.writeObject(p);
        }
        catch (IOException ex){
            ex.printStackTrace();
        }
    }
}

从二进制流中恢复Java对象反序列化:

public class ReadObject {
    public static void main(String[] args){
    /*将一个文件输入流包装成ObjectInputStream输入流*/
        try(ObjectInputStream ois=new ObjectInputStream(new FileInputStream("object.txt")))
        {
            Person p=(Person)ois.readObject();
            System.out.println("名字为:"+p.getName()+"\n年龄为:"+p.getAge());
        }
        catch(Exception ex){
            ex.printStackTrace();
        }
    }
}

反序列化换读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供该Java对象所属类的class文件,否则将会引发ClassNotFoundException异常。
当一个可序列化类由多个父类(包括直接父类和间接父类),这些父类要么有无参数的构造器,要么也是可序列化的,否则将会抛出InvalidClassException异常。如果父类时不可序列化的,只是带有无参数的构造器,则该父类中定义的成员变量值不会序列化到二进制流中。
如果某个类的成员变量的类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的。

Java序列化机制采用了一种特殊的序列化算法:所有保存到磁盘中的对象都有一个序列化编号;当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机)中被序列化过,系统才会将该对象转换成字节序列并输出;如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。

Java9为ObjectInputStream增加了setObjectInputFilter()和getObjectInputFilter()两个方法第一个方法用于为对象输入流设置过滤器。当程序通过ObjectInputStream反序列化对象时,过滤器的checkInput()方法会被自动激发,用于检查序列化数据是否有效。使用checkInput()方法检查序列化数据时共有3种返回值:Status.REJECTED 拒绝恢复;Status.ALLOWED 允许恢复;Status.UNDECIDED 未决定状态,程序继续执行检察。

当某个对象进行序列化时,系统会自动把该对象的所有实例变量一次进行序列化,如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化;如果被引用的对象的实例变量也引用了其他对象,则被引用的对象也会被序列化,这种情况被称为递归序列化
如果不希望系统将一些实例变量值进行序列化,可以在实例变量前面加一个transien关键字修饰,指定Java序列化时无须理会该实例变量。
使用transien关键字固然很方便,但是被它修饰的实例变量将被完全隔离在序列化机制之外,这将导致在反序列化恢复Java对象时无法取得该实例变量的值。

Java还提供了另一种序列化机制,这种序列化方式完全由程序员决定存储和恢复对象数据,要实现该目标,Java类必须实现Externalizable接口,该接口定义了如下两个方法:

void readExternal(ObjectInput in) 需要序列化的类实现 readExternal()方法来实现反序列化。该方法调用DataInput的方法来恢复基本类型的实际变量值,调用ObjectInput的readObject()方法来恢复引用类型的实际变量值。
void writeExternal(ObjectOutput out) 需要序列化的类实现writeExternal()方法来保存对象的状态。该方法调用DataOutput的方法来保存基本类型的实例变量值,调用ObjectOutput的writeObject()方法来保存引用类型的实例变量值。

当使用Externalizable机制反序列化对象时,程序会先试用public的无参数构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参数构造器。

实现Serializable接口 实现Externalizable接口
系统自动存储必要信息 程序员决定存储哪些信息
Java内建支持,易于实现,只需实现该接口即可,无需任何代码支持 仅仅提供两个空方法,实现该接口必须为两个空方法提供实现
性能略差 性能略好

关于对象序列化小结:
1.对象的类名、实例变量(包括基本类型、数组、对其他对象的引用)都会被序列化;方法、类变量(即static修饰的成员变量)、transient实例变量(也被称为瞬态实例变量)都不会被序列化;
2.实现Serializable接口的类如果需要让某个实例变量不被序列化,则可以在该实例变量前加transient修饰符,当然加static野能达到效果不过static不是这么用的。
3.保证序列化对象的实例变量类型也是可序列化的,否则需要使用transient关键字来修饰该实例变量,不然该类时不可序列化的。
4.反序列化对象时必须有序列化对象的class文件。
5.当通过文件、网络来读取序列化后的对象时,必须按实际写入的顺序读取。

对象序列化的最主要的用处就是在传递,和保存对象的时候,保证对象的完整性和可传递性。
什么时候用序列化:
1.把内存中的对象状态保存到一个文件中或者数据库中;
2.用套接字在网络上传送对象;
3.通过RMI传输对象。

六、NIO

NIO使用了不同的方式来处理输入输出,它采用内存映射文件的方式来处理输入输出,NIO将文件或文件的一段区域映射到内存中,这样就可以向访问内存一样来访问文件了。(NIO面向缓冲区而IO面向字节/字符流)

Channel(通道)和Buffer(缓冲)是NIO的两个核心对象,Channel是对传统的输入输出系统的模拟,在NIO系统中所有的数据都需要通过通道传输;Chanel与传统的InputStream、OutputStream最大的区别在于它提供了一个map()方法,通过该方法可以直接将一块数据映射到内存中。如果说传统的输入输出系统是面向流的处理,那么NIO则是面向块的处理。
Buffer可以被理解为一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先放在Buffer中,而从Channel中读取的数据也必须先放到Buffer中。Buffer可以从Channel中取,也允许使用Channel直接将文件的某块数据映射成Buffer。

对于Buffer的使用
通过使用如下方法来得到一个Buffer对象:static DatatypeBuffer allocate(int capacity) 创建一个容量为capacity的DatatypeBuffer对象。在Buffer中有三个比较重要的概念:容量(capacity)、界限(limit)、位置(position)。

容量(capacity):缓冲区的容量表示该Buffer的最大数据容量。缓冲区的容量不可以为负值,创建后不能改变。
界限(limit):第一个不应该被读出或者写入的缓冲区位置索引。也就是说,位于limit后的数据既不可以被读,也不可以被写。
位置(position):用于指明下一个可以被读出的或者写入的缓冲区位置索引(类似IO中的记录指针)。当使用Buffer从Channel中读取数据时,position的值恰好等于已经读到了多少数据。
这三者满足该关系:0<=mark<=position<=limit<=capacity

Buffer的主要作用就是装入数据,然后输出数据,开始时Buffer的position为0,limit为capacity,程序可以通过put()方法向Buffer中放入一些数据(或者从Channel中获取一些数据),每放入一些数据,Buffer的position相应的向后移动一些位置。当Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position的位置,并将position设置为0,这就使得Buffer的读写指针又移到了开始位置。即Buffer调用flip()方法之后,Buffer为输出数据做好准备;当Buffer输出数据结束后,Buffer调用clear()方法,clear()不是清空数据,而是将position置为0,将limit置为capacity,再次为Buffer中装入数据做准备。

Buffer包含的常用方法:

int capacity() 返回Buffer的capacity大小。
boolean hasRemaining() 判断position和界限limit之间是否还有元素可供处理。
int limit() 返回Buffer的limit的位置。
Buffer limit(int newLt) 重新设置limit的值,并返回一个具有新的limit的缓冲区对象。
Buffer mark() 设置Buffer的mark位置,它只能在0和position之间做mark。
int position() 返回Buffer中position的值。
Buffer position(int newPs) 设置Buffer的position,并返回position被修改后的Buffer对象。
int remaining() 返回position和limit之间的元素个数。
Buffer reset() 将position转到mark所在的位置。
Buffer rewind() 将position设置成0,取消设置的mark。

通过allocate()方法创建的Buffer对象是普通的Buffer,ByteBuffer还提供了一个allocateDirect()方法来创建直接Buffer。直接Buffer的创建成本逼普通Buffer的创建成本高,但直接Buffer的读取效率更高。故直接Buffer只适用于长生存期的Buffer,而且只有ByteBuffer才提供了allocateDirect()方法。

对Channel的使用
Channel类似于传统的流对象,但是于传统的流又有所不同。Channel可以直接将制定文件的部分或全部直接映射成Buffer。程序不能直接访问Channel中的数据,包括读写都不行,Channel只能与Buffer进行交互。即如果要从Channel中取得数据,必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出这些数据;如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channel中。
所有的Channel都不应该通过构造器来直接创建,而是通过传统节点InputStream、OutputStream的getChannel()方法来返回对应的Channel。Channel中最常用的三类方法是map()、read()和write()。

在RandomAccessFile中也包含了一个getChannel()方法,RandomAccessFile返回的FileChannel()是只读的还是读写的,则取决于RandomAccessFile打开文件的模式,如下代码可以复制a.txt的内容并且追加在文件后面:

public class RandomFileChannelTest {
    public static void main(String[] args) throws IOException {
        File f=new File("a.txt");
        try(
                RandomAccessFile raf=new RandomAccessFile(f,"rw");
                FileChannel fc=raf.getChannel()
                )
        {
            ByteBuffer bf=rand.map(FileChannel.MapMode.READ_WRITE,0,f.length());
            fc.position(f.length());/*将Channel的记录指针移动到该Channel的最后,以便让追加数据*/
            fc.write(bf);
        }
    }
}

文件锁:如果多个运行的程序需要并发修改同一个文件时,程序之间需要某种机制来进行通信,使用文件锁可以有效的阻止多个进程并发修改同一个文件。

猜你喜欢

转载自blog.csdn.net/laobanhuanghe/article/details/96349227