08-流

部分内容来自这里

问:Java中有几种类型的流?

在Java使用流的机制进行数据的传送,从文件到内存是输入流,从内存到文件是输出流。

Java中有字节流和字符流。字节流继承于InputStream/OutputStream,字符流继承于Reader/Writer。

在java.io包中还有许多其他的流,低层流与高层流,高层流主要是为了提高性能和使用方便。


图 1

问:字符流与字节流

要把一片二进制数据逐一输出到某个设备中,或者从某个设备中逐一读取一片二进制数据,不管输入输出设备是什么,我们要用统一的方式来完成这些操作,用一种抽象的方式进行描述,这个抽象描述方式即为IO流,对应的抽象类为OutputStream和InputStream,它们都是针对字节进行操作的。

在应用中,经常要将完全是字符的一段文本输出去或读进来,直接用字节流可以吗?计算机中的一切最终都是以二进制的字节形式存在。对于“中国”这些字符,首先需要得到其对应的字节,然后将字节写入到输出流。读取时,首先读到的是字节,可是我们要把它显示为字符,我们需要将字节转换成字符。由于这样的需求很广泛,人家专门提供了字符流的包装类。

底层设备永远只接受字节数据,有时候要将字符串写到底层设备,需要将字符串转成字节再进行写入。字符流是字节流的包装,它直接接受字符串,内部将字符转换成字节,再写入底层设备,这为我们向IO设备写入或读取字符串提供了一点点方便。

在将字符转换为字节时,需要注意编码问题:因为字符串转换成字节数组,其实是将每个字符转换成某种编码的字节形式,读取也是反之的道理。

例1:字节流和字符流的使用

String str="Hello World!";

字节流:

FileOutputStream fos=new FileOutputStream("content.txt");
fos.write(str.getBytes("UTF-8"));
fos.close();

FileInputStream fis = new FileInputStream("content.txt");
byte[] buf = new byte[1024];
int len =fis.read(buf);
String myStr = new String(buf, 0, len, "UTF-8");
System.out.println(myStr);

字符流:

FileWriter fw =new FileWriter("content.txt");
fw.write(str);
fw.close();

FileReader fr =new FileReader("content.txt");
char[] buf = new char[1024];
int len =fr.read(buf);
String myStr = new String(buf, 0, len);
System.out.println(myStr);

例2:编程实现文件拷贝

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class Main {
    // 工具类中的方法都是静态方式访问的,因此将构造器设为私有不允许创建对象
    private Main() {
        throw new AssertionError();
    }

    public static void main(String[] args) {
        fileCopy("file1.txt", "file2.txt");
        //fileCopyNIO("file1.txt", "file2.txt");
    }

    public static void fileCopy(String source, String target) {
        InputStream in = null;
        OutputStream out = null;
        try {
            in = new FileInputStream(source);
            out = new FileOutputStream(target);
            byte[] buffer = new byte[4096];
            int bytesToRead;
            while ((bytesToRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesToRead);
            }
        } catch (IOException e) {
        } finally {
            try {
                if (in != null)
                    in.close();
                if (out != null)
                    out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void fileCopyNIO(String source, String target) {
        FileInputStream in = null;
        FileOutputStream out = null;
        try {
            in = new FileInputStream(source);
            out = new FileOutputStream(target);

            FileChannel inChannel = in.getChannel();
            FileChannel outChannel = out.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(4096);
            while (inChannel.read(buffer) != -1) {
                buffer.flip();
                outChannel.write(buffer);
                buffer.clear();
            }
        } catch (IOException e) {
        } 
    }
}

问:节点流与处理流

流可以分为节点流和处理流:

  • 节点流可以从一个特定的数据源(如文件、内存等)读写数据;

图 1
  • 而处理流则是连接在已存在的节点流或处理流之上,通过对数据的处理为程序提供更强大的读写功能。

图 2

常用的节点流


图 1

常用的处理流


图 2

节点流向处理流转换的实例:

FileInputStream(System.in) -> InputSteamReader -> BufferReader
OutputSteam(System.out) -> PrintStream
FileReader -> BufferedReader
FileWriter -> PrintWriter或bufferWriter

例1:处理流的使用

String str="Hello World!";

PrintWriter pw =new PrintWriter("content.txt", "utf-8");
pw.write(str);
pw.close();

BufferedReader br =new BufferedReader(new InputStreamReader(new FileInputStream("content.txt"), "UTF-8"));
String myStr =br.readLine();
br.close();
System.out.println(myStr);

例2:写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数

/**
 * 统计给定文件中给定字符串的出现次数
 * 
 * @param filename  文件名
 * @param word 字符串
 * @return 字符串在文件中出现的次数
 */
public static int countWordInFile(String filename, String word) {
    BufferedReader br = null;
    int counter = 0;
    try {
        br = new BufferedReader(new FileReader(filename));
        String line = null;
        while ((line = br.readLine()) != null) {
            int index = -1;
            while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) {
                counter++;
                line = line.substring(index + word.length());
            }
        }
    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        try {
            if(br!=null)
                br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return counter;
}

问:IO与NIO

推荐阅读:Java IO与NIO

问:序列化

序列化是为了解决在对对象流进行读写操作时所引发的问题(如果不进行序列化可能会存在数据乱序的问题)。序列化就是一种用来处理对象流的机制,所谓对象流就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。

Java序列化是指将对象转换为字节序列的过程,而反序列化则是将字节序列转换成目标对象的过程。

我们都知道,在进行浏览器访问的时候,我们看到的文本、图片、音频、视频等都是通过二进制序列进行传输的,那么如果我们需要将Java对象进行传输的时候,是不是也应该先将对象进行序列化?答案是肯定的,我们需要先将Java对象进行序列化,然后通过网络、IO进行传输,当到达目的地之后,再进行反序列化获取到我们想要的对象,最后完成通信。

问:序列化的实现

推荐阅读1推荐阅读2

方式一

(1) 定义类实现Serializable接口,并生成一个版本号

import java.io.Serializable;

public class People implements Serializable{
    private static final long serialVersionUID = 1942699735300957387L;

    private transient String id;
    private String name;
    private String age;

    public People(String id, String name, String age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public People(){

    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{id='" + id + "\', name='" + name + "\', age='" + age + "\'}";
    }
}

(2) 使用ObjectOutputStream类中的writeObject()方法将对象进行序列化;使用ObjectInputStream类中的readObject()方法反序列化得到对象

public static  void main(String[] args){
    File file = new File("D:/test.txt");
    People people = new People("12", "齐天大圣", "500");
    try {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
        oos.writeObject(people);
        oos.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

    try {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        People p = (People) ois.readObject();
        System.out.println(p.toString());
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

注意:

  • Serializable接口中并没有提供任何方法,它只是一个标识性接口,标识这个类的对象需要被序列化,javac编译时会进行特殊处理;然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject()方法从流中读取对象。
  • 序列化版本号serialVersionUID的作用是区分我们所编写的类的版本,用于判断反序列化时类的版本是否一致,如果不一致会出现版本不一致异常;
  • 关键字transient用来忽略我们不希望进行序列化的变量,在被反序列化后,transient变量的值被设为初始值,如int型的是0,引用型变量的值是null。

方式二

(1) 定义类实现Externalizable接口,实现接口中的writeExternal()和readExternal()方法实现对象的序列化。Externaliable接口是Serializable的子接口,有着和Serializable接口同样的功能。

import java.io.Externalizable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class People implements Externalizable{
    private transient String id;
    private String name;
    private String age;

    public People(String id, String name, String age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public People(){

    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{id='" + id + "\', name='" + name + "\', age='" + age + "\'}";
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(id);
        out.writeObject(name);
        out.writeObject(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        id = (String) in .readObject();
        name = (String) in.readObject();
        age = (String) in.readObject();
    }
}

(2)

public static  void main(String[] args){
    File file = new File("D:/test.txt");
    People people = new People("12", "齐天大圣", "500");
    try {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
        people.writeExternal(oos);
        oos.close();
    } catch (IOException e) {
        e.printStackTrace();
    }

    try {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        People p = new People();
        p.readExternal(ois);
        System.out.println(p.toString());
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

问:transient关键字的作用?如何将transient修饰符修饰的变量序列化?

当使用transient关键字修饰一个变量时,这个变量将不会参与序列化过程,也就是说它不会在网络操作时被传输,也不会在本地被存储下来,这对于保护一些敏感字段(如密码等)非常有帮助。

  • 变量被transient关键字修饰后,反序列化后不能获取到该变量的值;
  • transient只能修饰变量,不能修饰方法。修饰我们自定义类的变量时,这个类一定要实现Serializable接口;
  • 由于static修饰的变量是属于类的,而序列化是针对对象的,因此static修饰的变量不能被序列化。

例1:当一个类实现了Serializable接口,这个对象的所有字段就可以被自动序列化。在持久化对象时,我们可能不想让一些特殊字段随着网络传输过去,或者在本地被序列化缓存起来,这时我们就可以在这些字段前加上transient关键字修饰,被transient修饰的变量的值将不会包括在序列化的表示中,也就不会被保存下来。这个字段的生命周期仅存在于调用者的内存中。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class UserBean implements Serializable {
    private static final long serialVersionUID = 856780694939330811L;
    private String userName;
    private transient String password; // 此字段不需要被序列化

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

public class Main {
    public static void main(String[] args) {
        // 序列化
        try {
            UserBean bean = new UserBean();
            bean.setUserName("name");
            bean.setPassword("password");
            System.out.println("序列化前--->userName:" + bean.getUserName() + ",password:" + bean.getPassword());

            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:/userbean.txt"));
            oos.writeObject(bean);// 将对象序列化缓存到本地
            oos.flush();
            oos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:/userbean.txt"));
            UserBean bean = (UserBean) ois.readObject();
            ois.close();

            System.out.println("反序列化后获取出的数据--->userName:" + bean.getUserName() + ",password:" + bean.getPassword());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

//输出:

序列化前--->userName:name,password:password
反序列化后获取出的数据--->userName:name,password:null

注意:如果给password加上static关键字修饰,反序列化后依然能够获取到值,但是这个时候的值是JVM内存中对应的static的值,因为static修饰后,它属于类不属于对象,存放在一块单独的区域,直接通过对象也是可以获取到这个值的。

我们可以通过实现Externalizable接口,实现writeExternal()和readExternal()方法,然后再自定义序列化对象。

分享到 评论