Java Netty 学习(一)- IO学习笔记

版权声明:本博客所有的原创文章,转载请注明出处,作者皆保留版权。 https://blog.csdn.net/anLA_/article/details/79517767

一直对IO比较半懂不懂,乘着闲暇时间系统梳理一遍IO知识,为以后学习做好铺垫。

什么是IO?

即input output,在Java中,流是一个核心的概念。
流从概念上来说是一个连续的数据流。你既可以从流中读取数据,也可以往流中写数据。
流与数据源或者数据流向的媒介相关联。在Java IO中流既可以是字节流(以字节为单位进行读写),也可以是字符流(以字符为单位进行读写)。注意区分字节和字符,字符是字节的上层单位,即字符由4(或其他)字节大小组成。

InputStream, OutputStream, Reader 和Writer

这四个是IO流中的基类,InputString和OutputStream是对于字节流的输入输出,而Reader和Writer则是对于字符流的操作,
需要InputStream或者Reader从数据源读取数据,需要OutputStream或者Writer将数据写入到目标媒介中。
注意此处的方向问题,要站在电脑基础上来思维,Input和Reader是从外部读入电脑,而Output和Writer则是向外处写文件。

用途总览

说过上面四个是基类,那么上面四个类里面定义的都是些大体对字节或者字符的操作,为了提升便利性,jdk为我们又定义众多子类来处理不同类型的文件:
文件访问、网络访问、内存缓存访问、线程内部通信(管道)、缓冲、过滤、解析、读写文本 (Readers / Writers)、读写基本类型数据 (long, int etc.)、读写对象。

具体的看如下表格:

Type Byte Based Input Byte Based Output Character Based Input Character Based Output
Basic InputStream OutputStream Reader/InputStreamReader Writer/OutputStreamWriter
Arrays ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
Files FileInputStream/RandomAccessFile FileOutputStream/RandomAccessFile FileReader FileWriter
Pipes PipedInputStream PipedOutputStream PipedReader PipedWriter
Buffering BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
Filtering FilterInputStream FilterOutputStream FilterReader FilterWriter
Parsing PushbackInputStream/StreamTokenizer PushbackReader/LineNumberReader
Strings StringReader StringWriter
Data DataInputStream DataOutputStream
Data - Formatted PrintStream PrintWriter
Objects ObjectInputStream ObjectOutputStream
Utilities SequenceInputStream

下面从文件开始一点点了解用途。

文件

在Java应用程序中,文件是一种常用的数据源或者存储数据的媒介,我们可以从文件中读取数据,或者写入数据到文件中。

  1. 通过IO顺序读写文件
    可以通过Java将一个文件数据读入到java流中,可以根据是二进制还是文本文件选择使用FileInputStream还是FileReader。关于二进制还是文本文件,可以将文本文件看作一个特殊的二进制文件,他们二者只是解释方式不同,具体可看:
    https://www.zhihu.com/question/19971994
    这两个类允许你从文件开始到文件末尾一次读取一个字节或者字符,或者将读取到的字节写入到字节数组或者字符数组。你不必一次性读取整个文件,相反你可以按顺序地读取文件中的字节和字符。
    同样,对于写文件,可以根据你要写入的数据是二进制型数据还是字符型数据选用FileOutputStream或者FileWriter。可以一次写入一个字节或者字符到文件中,也可以直接写入一个字节数组或者字符数据。数据按照写入的顺序存储在文件当中。

  2. 随机存取文件
    可以通过RandomAccessFile对文件进行随机存取,随机存取并不意味着你可以在真正随机的位置进行读写操作,它只是意味着你可以跳过文件中某些部分进行操作,并且支持同时读写,不要求特定的存取顺序。这使得RandomAccessFile可以覆盖一个文件的某些部分、或者追加内容到它的末尾、或者删除它的某些内容,当然它也可以从文件的任何位置开始读取文件。

  3. 文件和目录信息的读取
    可以由File知道文件的大小和属性,对于目录来说也是一样的,比如你需要获取某个目录下的文件列表。通过File类可以获取文件和目录的信息。

管道

Java IO中的管道为运行在同一个JVM中的两个线程提供了通信的能力。所以管道也可以作为数据源以及目标媒介。

你不能利用管道与不同的JVM中的线程通信(不同的进程)。在概念上,Java的管道不同于Unix/Linux系统中的管道。在Unix/Linux中,运行在不同地址空间的两个进程可以通过管道通信。
在Java中,通信的双方应该是运行在同一进程中的不同线程。

一个线程通过PipedOutputStream写入的数据可以被另一个线程通过相关联的PipedInputStream读取出来。
看下面一个简单例子:

    public static void main(String[] args) throws IOException {
        final PipedOutputStream pos = new PipedOutputStream();
        final PipedInputStream pis = new PipedInputStream(pos);

        Thread t1 = new Thread(new Runnable() {
            public void run() {
                try {
                    pos.write("hello world?".getBytes());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (pos != null) {
                        try {
                            pos.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            public void run() {
                try {
                    int data = pis.read();
                    while (data != -1) {
                        System.out.print((char) data);
                        data = pis.read();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (pis != null) {
                        try {
                            pis.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(t1);
        executor.execute(t2);
    }

当然对于上述pis和pos的read方法是阻塞式的,只有当有值才会读取并且输出。所以如果尝试在同一个线程里面调用read()方法和write()方法会导致流阻塞,可能会导致线程死锁。

网络

当两个进程建立起网络连接后,也可以把数据传输抽象成IO流的输入输出。
即利用InputStream读取数据,利用OutputStream写入数据。换句话来说,Java网络API用来在不同进程之间建立网络连接,而Java IO则用来在建立了连接之后的进程之间交换数据。

字节数组和字符数组

主要就是围绕ByteArrayInputStream和ByteArrayOutputStream来讲。
在Java中,常用字节和字符数组在应用中临时存储数据。而这些数组又是通常的数据读取来源或者写入目的地。如果你需要在程序运行时需要大量读取文件里的内容,那么你也可以把一个文件加载到数组中。当然你可以通过直接指定索引来读取这些数组。
写了个简单例子,助于理解:

    private static void read() throws FileNotFoundException, IOException{
        File file = new File("/home/anla7856/workspace/io.examples/pom.xml");
        byte[] data = new byte[1024];
        int length = new FileInputStream(file).read(data);
        System.out.println(length);
        InputStream is = new ByteArrayInputStream(data);
        String content = new String(data, "UTF-8");    //输出的则为pom文件内容
        System.out.println(content);
        int temp = is.read();
        while(temp != -1){
            System.out.print(temp);             //这里输出的是字节符号
            temp = is.read();
        }
        is.close();
    }

    private static void write() throws UnsupportedEncodingException, IOException {
        File file = new File("/home/anla7856/workspace/io.examples/pom1.xml");
        FileOutputStream fos = new FileOutputStream(file);
        OutputStream output = new BufferedOutputStream(fos);
        output.write("This text is converted to bytes".getBytes("utf-8"));
        output.close();
        fos.close();
    }

System.in, System.out, System.err

  • System.in是一个典型的连接控制台程序和键盘输入的InputStream流。通常当数据通过命令行参数或者配置文件传递给命令行Java程序的时候,比如在刷oj时候,会使用到这个和Scanner来辅助输入。
  • System.out是一个PrintStream流。System.out一般会把你写到其中的数据输出到控制台上。System.out通常仅用在类似命令行工具的控制台程序上。
  • System.err是一个PrintStream流。System.err与System.out的运行方式类似,但它更多的是用于打印错误文本。一些类似Eclipse的程序,为了让错误信息更加显眼,会将错误信息以红色文本的形式通过System.err输出到控制台上。

替换系统流:
可以替换这几个系统流,也就是可以讲他们直接输出到文件中:

OutputStream output = new FileOutputStream("/home/anla7856/workspace/io.examples/log.txt");
PrintStream printOut = new PrintStream(output);
System.setOut(printOut);

现在所有的System.out都将重定向到/home/anla7856/workspace/io.examples/log.txt文件中。请记住,务必在JVM关闭之前冲刷System.out,即调用flush(),确保System.out把数据输出到了文件中。

Java IO流是既可以从中读取,也可以写入到其中的数据流。
流和数组不一样,不能通过索引读写数据。在流中,你也不能像数组那样前后移动读取数据,除非使用RandomAccessFile 处理文件。流仅仅只是一个连续的数据流。
某些类似PushbackInputStream 流的实现允许你将数据重新推回到流中,以便重新读取。然而你只能把有限的数据推回流中,并且你不能像操作数组那样随意读取数据。流中的数据只能够顺序访问。
如下例子:

PushbackInputStream input = new PushbackInputStream(
                            new FileInputStream("/home/anla7856/workspace/io.examples/input.txt"));
int data = input.read();
input.unread(data);

或者,你也能限制回退长度:

int pushbackLimit = 8;
PushbackInputStream input = new PushbackInputStream(
                                new FileInputStream("/home/anla7856/workspace/io.examples/input.txt"),
                                pushbackLimit);

Java IO流通常是基于字节或者基于字符的。

字节流通常以“stream”命名,比如InputStream和OutputStream。除了DataInputStream 和DataOutputStream 还能够读写int, long, float和double类型的值以外,其他流在一个操作时间内只能读取或者写入一个原始字节。

字符流通常以“Reader”或者“Writer”命名。字符流能够读写字符(比如Latin1或者Unicode字符)。

组合流:
你可以将流整合起来以便实现更高级的输入和输出操作。
比如,一次读取一个字节是很慢的,所以可以从磁盘中一次读取一大块数据,然后从读到的数据块中获取字节。为了实现缓冲,可以把InputStream包装到BufferedInputStream中。

InputStream input = new BufferedInputStream(
                    new FileInputStream("/home/anla7856/workspace/io.examples/input.txt"));

缓冲只是通过流整合实现的其中一个效果。你可以把InputStream包装到PushbackInputStream中,之后可以将读取过的数据推回到流中重新读取,在解析过程中有时候这样做很方便,将不同的流整合到一个链中,可以实现更多种高级操作。

Reader And Writer

前面说过,InputStream和OutputStream是基于字节的,而Reader和Writer除了基于字符。
例如在上面写过一个例子,stream读取的是字节流,而Reader和Writer读取的,则是字符,可以根据不同的编码读取数据。
而不用想上面的,读取byte[]后,再由String去转码。

Reader

Reader类是Java IO中所有Reader的基类。子类包括BufferedReader,PushbackReader,InputStreamReader,StringReader和其他Reader。
例如下面:

private static void reader() throws IOException{
    Reader read = new FileReader("/home/anla7856/workspace/io.examples/pom.xml");
    int data = read.read();
    while(data != -1){
        System.out.print((char)data);     //读出的是字符,所以通过char可以强行转化得出字符
        data = read.read();
    }
}


private static void reader1() throws IOException{
    //用utf-8解码
    Reader read = new InputStreamReader(
            new FileInputStream(
            new File("/home/anla7856/workspace/io.examples/pom.xml")),"UTF-8");
    int data = read.read();
    while(data != -1){
        System.out.print((char)data);
        data = read.read();
    }
}

上面两个输出都是一样的。
InputStream的read()方法返回一个字节,意味着这个返回值的范围在0到255之间(当达到流末尾时,返回-1),
Reader的read()方法返回一个字符,意味着这个返回值的范围在0到65535之间(当达到流末尾时,同样返回-1)。想要获得正常输出,需要char转化。
这并不意味着Reade只会从数据源中一次读取2个字节,Reader会根据文本的编码,一次读取一个或者多个字节。

Writer

Writer类是Java IO中所有Writer的基类。子类包括BufferedWriter和PrintWriter等等。
看一个例子:

private static void writer() throws IOException{
    Writer writer = new FileWriter("/home/anla7856/workspace/io.examples/output.txt"); 
    writer.write("Hello World Writer");    //不需要转码
    writer.close();
}

最好使用Writer的子类,不需要直接使用Writer,因为子类的实现更加明确,更能表现你的意图。常用子类包括OutputStreamWriter,CharArrayWriter,FileWriter等。
Writer的write(int c)方法,会将传入参数的低16位写入到Writer中,忽略高16位的数据。,因为只有0~65535大小

并发IO

有时候你可能需要并发地处理输入和输出。换句话说,你可能有超过一个线程处理输入和产生输出。比如,你有一个程序需要处理磁盘上的大量文件,这个任务可以通过并发操作提高性能。又比如,你有一个web服务器或者聊天服务器,接收许多连接和请求,这些任务都可以通过并发获得性能的提升。

如果你需要并发处理IO,这里有几个问题可能需要注意一下:

  • 在同一时刻不能有多个线程同时从InputStream或者Reader中读取数据,也不能同时往OutputStream或者Writer里写数据。你没有办法保证每个线程读取多少数据,以及多个线程写数据时的顺序。
  • 如果线程之间能够保证操作的顺序,即你能够才想到线程之间尽管有并发,但是仍然能够正确完成自己的工作,就像集合里面的Spliterator那样,使用分区域的完成。

InputStream和OutputStream

InputStream:
InputStream用于读取基于字节的数据,一次读取一个字节,例子就不提了,上面有个类似的,Java7之后,在捕获一场方面,
出现了一种新的方式:“try-with-resource”结构
如下:

private static void printFileJava7() throws IOException {
    try(  FileInputStream     input         = new FileInputStream("file.txt");
          BufferedInputStream bufferedInput = new BufferedInputStream(input)
    ) {
        int data = bufferedInput.read();
        while(data != -1){
            System.out.print((char) data);
    data = bufferedInput.read();
        }
    }
}

主要方法:

  • read():一次读取一个字节,大小在0~255,可以用char强制转化后输出,InputStream的子类可能会包含read()方法的替代方法。比如,DataInputStream允许你利用readBoolean(),readDouble()等方法读取Java基本类型变量int,long,float,double和boolean。
  • read(byte[]):一次读取byte[]大小的字节到byte里面,相似的还有int read(byte, int offset, int length) 方法,一次性读取一个字节数组的方式,比一次性读取一个字节的方式快的多,所以,尽可能使用这两个方法代替read()方法。

OutputStream:
OutputStream类是Java IO API中所有输出流的基类。子类包括BufferedOutputStream,FileOutputStream等等。
看一些主要方法:

  1. write(byte) :
    write(byte)方法用于把单个字节写入到输出流中。OutputStream的write(byte)方法将一个包含了待写入数据的int变量作为参数进行写入。只有int类型的第一个字节会被写入,其余位会被忽略。(即写入低8位,忽略高24位)。
    OutputStream的子类可能会包含write()方法的替代方法。比如,DataOutputStream允许你利用writeBoolean(),writeDouble()等方法将基本类型int,long,float,double,boolean等变量写入。

  2. write(byte[]):
    OutputStream同样包含了将字节数据中全部或者部分数据写入到输出流中的方法,分别是write(byte[])和write(byte[], int offset, int length)。

  3. flush():
    通过调用flush()方法,可以把缓冲区内的数据刷新到磁盘(或者网络,以及其他任何形式的目标媒介)中。

  4. close():
    当你结束数据写入时,需要关闭OutputStream。通过调用close()可以达到这一点。因为OutputStream的各种write()方法可能会抛出IO异常,所以你需要把调用close()的关闭操作方在finally块中执行。

RandomAccessFile

RandomAccessFile允许你来回读写文件,也可以替换文件中的某些部分。FileInputStream和FileOutputStream没有这样的功能。

看下面一个例子:

private static void seek() throws IOException{
    RandomAccessFile file = new RandomAccessFile("/home/anla7856/workspace/io.examples/pom.xml", "rw");
    file.seek(200);
    long pointer = file.getFilePointer();
    file.close();
    System.out.println(pointer);
}

BufferedInputStream和BufferedOutputStream

这两个BufferedStream可以提供一个缓冲区,能提高IO的读取速度,你可以一次读取一大块的数据,而不需要每次从网络或者磁盘中一次读取一个字节。特别是在访问大量磁盘数据时,缓冲通常会让IO快上许多。
用法:

InputStream input = new BufferedInputStream(new FileInputStream("c:\\data\\input-file.txt"));

默认大小为8kb:

private static int DEFAULT_BUFFER_SIZE = 8192;

当然,你也可以自己更改:

InputStream input = new BufferedInputStream(new FileInputStream("c:\\data\\input-file.txt"), 8 * 1024);

然后通过InputStream类似的方法进行操作。
BufferedOutputStream和BufferedInputStream用法上类似,注意读取数据时候,要使用flush方法清空缓冲区数据。

DataInputStream和DataOutputStream

这两个类可以帮助我们写入或者读出Java类型的数据,而不是操作byte或者byte[]。
例如下面例子:

private static void readData() throws IOException{
    DataInputStream input = new DataInputStream(
                new FileInputStream("/home/anla7856/workspace/io.examples/pom.xml"));
    int data = input.readInt();
    System.out.println(data);
}

当你要读取的数据中包含了int,long,float,double这样的基本类型变量时,DataInputStream可以很方便地处理这些数据。

DataOutputStream与DataInputStream类似。

序列化与ObjectInputStream、ObjectOutputStream

利用上面两个类,我们可以把一个类序列化到一个目的数据源(例如文件),然后需要时后再将他们读取出来。
看下面例子:

private static void writeObject() throws FileNotFoundException, IOException{
    Dog dog = new Dog("tom",18);
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("/home/anla7856/workspace/io.examples/data.xml")));
    oos.writeObject(dog);
    oos.close();
}


private static void readObject() throws FileNotFoundException, IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/home/anla7856/workspace/io.examples/data.xml"));
    Dog dog = (Dog) ois.readObject();
    System.out.println(dog);
}

ObjectInputStream和ObjectOutputStream还有许多read和write方法,比如readInt、writeLong等等。
在你序列化和反序列化一个对象之前,该对象的类必须实现了java.io.Serializable接口。

InputStreamReader和OutputStreamWriter

这两个类是用来讲reader,writer和inputStream,outputStream相互转化的类。
看如下一个例子:

private static void reader1() throws IOException{
    Reader read = new InputStreamReader(new FileInputStream(new File("/home/anla7856/workspace/io.examples/pom.xml")),"UTF-8");
    int data = read.read();
    while(data != -1){
        System.out.print((char)data);
        data = read.read();
    }
}

注意在int(4字节)和char(2字节)之间有精度问题,但是

char aChar = (char) data; //这里不会造成数据丢失,因为返回的int类型变量data只有低16位有数据,高16位没有数据

OutputStreamWriter类似~

其他IO类

下面介绍一些其他io类:

SequenceInputStream

SequenceInputStream把一个或者多个InputStream整合起来,形成一个逻辑连贯的输入流。当读取SequenceInputStream时,会先从第一个输入流中读取,完成之后再从第二个输入流读取,以此推类。
具体使用:

InputStream input1 = new FileInputStream("/home/anla7856/workspace/io.examples/file1.txt");
InputStream input2 = new FileInputStream("/home/anla7856/workspace/io.examples/file2.txt");
InputStream combined = new SequenceInputStream(input1, input2);

PrintStream

PrintStream允许你把格式化数据写入到底层OutputStream中。比如,写入格式化成文本的int,long以及其他原始数据类型到输出流中,而非它们的字节数据。
下面看一个例子:

private static void printStream(){
    String s = "printfStream";
    OutputStream os = System.out;
    PrintStream output = new PrintStream(os);
    output.print(true);
    output.printf(Locale.UK, "Text + data: %s$", s);
    output.print((float) 123.456);
    output.close();
}

例如上面,获得控制台OutputStream,并且能够格式化输出,注意这一行:

output.printf(Locale.UK, "Text + data: %s$", s);

对于占位符,需要名字一致。

LineNumberReader

LineNumberReader是记录了已读取数据行号的BufferedReader。默认情况下,行号从0开始,当LineNumberReader读取到行终止符时,行号会递增(换行\n,回车\r,或者换行回车\n\r都是行终止符)。

你可以通过getLineNumber()方法获取当前行号,通过setLineNumber()方法设置当前行数(译者注:setLineNumber()仅仅改变LineNumberReader内的记录行号的变量值,不会改变当前流的读取位置。流的读取依然是顺序进行,意味着你不能通过setLineNumber()实现流的跳跃读取)。

看一段代码例子:

LineNumberReader lineNumberReader = 
    new LineNumberReader(new FileReader("/home/anla7856/workspace/io.examples/input.txt"));

int data = lineNumberReader.read();
while(data != -1){
    char dataChar = (char) data;
    data = lineNumberReader.read();
    int lineNumber = lineNumberReader.getLineNumber();
}
lineNumberReader.close();

这个类能用来干嘛呢?
能够帮我们很容易的定位到出错的位置,以行数来决定。

StreamTokenizer

StreamTokenizer可以帮助我们干嘛呢?
简单来说,就是它可以分词(英文),并且可以区分词语属性从而输出,看一段例子:

    private static void streamTokenizer() throws IOException{
        StreamTokenizer streamTokenizer = new StreamTokenizer(
                new StringReader("Mary had 1 little lamb..."));
        while(streamTokenizer.nextToken() != StreamTokenizer.TT_EOF){
            if(streamTokenizer.ttype == StreamTokenizer.TT_WORD) {
                System.out.println(streamTokenizer.sval); //sval 如果读取到的符号是字符串类型,该变量的值就是读取到的字符串的值
            } else if(streamTokenizer.ttype == StreamTokenizer.TT_NUMBER) {
                System.out.println(streamTokenizer.nval); //nval 如果读取到的符号是数字类型,该变量的值就是读取到的数字的值
            } else if(streamTokenizer.ttype == StreamTokenizer.TT_EOL) {
                System.out.println();
            }
        }
    }

以及它的输出:

Mary
had
1.0
little
lamb...

StreamTokenizer可以识别标示符,数字,引用的字符串,和多种注释类型。你也可以指定何种字符解释成空格、注释的开始以及结束等。在StreamTokenizer开始解析之前,所有的功能都可以进行配置。

StringReader和StringWriter

StringReader能够将原始字符串转换成Reader,而StringWriter能够以字符串的形式从Writer中获取写入到其中数据。
例如以下StringReader:

String input = "Input String... ";
StringReader stringReader = new StringReader(input);
int data = stringReader.read();
while(data != -1) {
  doSomethingWithData(data);
  data = stringReader.read();
}
stringReader.close();

而对于StringWriter:

StringWriter stringWriter = new StringWriter();
stringWriter.write("This is a text");
String       data       = stringWriter.toString();
StringBuffer dataBuffer = stringWriter.getBuffer();
stringWriter.close();

参考资料

  1. http://ifeve.com/java-io/
  2. http://tutorials.jenkov.com/java-io/index.html

猜你喜欢

转载自blog.csdn.net/anLA_/article/details/79517767