Java 标准 I/O 流编程一览笔录

Java标准I/O知识体系图:

1、I/O是什么?

I/O 是Input/Output(输入、输出)的简称,输入流可以理解为向内存输入,输出流是从内存输出。

2、流

流是一个连续的数据流,可以从流中读取数据,也可以往流中写数据。流与数据源,或数据源流向的媒介相关联。

在Java IO流中,流可以是字节流,也可以是字符流。

3、Java I/O 用途与对应的流一览

注:粗体为节点流。蓝色为转换流(字节流转为字符流)。

4、流的处理

流分节点流和处理流两种。

节点流:可以从或向一个特定的地方(节点)读写数据。如FileInputStream、FileReader。

处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader.处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接

5、文件访问

(1)读取文件

如果你需要在不同端使用读取文件,你可以根据你要读的文件是二进制文件还是文本文件,或者根据你要处理的数据是准备采取字节方式还是字符方式,决定使用 FileInputStream 或者 FileReader。两者支持你从文件开头开始到文件结尾读取一个字节或者字符,也可以将读取的多个字节或字符,写入到内存的字节数组或字符数组。

单字节读取文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void readFileAsByte() throws IOException {
         String filepath = "file.bin" ;
         java.io.InputStream is = null ;
         try {
             is = new FileInputStream(filepath);
             int data = - 1 ;
             while ((data = is.read()) != - 1 ) { // -1 表示读取到达文件结尾
                 //操作数据
                 System.out.print(( byte )data + " " );
             }
         } finally {
             if (is != null ) {
                 is.close(); // 关闭流
             }
         }
     }

字节数组读取文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void readFileAsByteArray() throws IOException {
     String filepath = "file.bin" ;
     java.io.InputStream is = null ;
     try {
         is = new BufferedInputStream( new FileInputStream(filepath)); // 组装BufferedInputStream流,加入缓冲能力
         byte [] data = new byte [ 256 ];
         int len = - 1 ;
         while ((len = is.read(data)) != - 1 ) { // -1 表示读取到达文件结尾
             //操作数据
             for ( int i = 0 ; i < len; i++) {
                 System.out.print(data[i] + " " );
             }
         }
     } finally {
         if (is != null ) {
             is.close(); // 关闭流
         }
     }
}

单字符读取文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void readFileAsChar() throws IOException {
     String filepath = "file.txt" ;
     java.io.Reader r = null ;
     try {
         r = new FileReader(filepath);
         int data = - 1 ;
         while ((data = r.read()) != - 1 ) { // -1 表示读取到达文件结尾
             //操作数据
             System.out.print(( char ) data);
         }
     } finally {
         if (r != null ) {
             r.close(); // 关闭流
         }
     }
}

字符数组读取文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void readFileAsCharArray() throws IOException {
     String filepath = "file.txt" ;
     java.io.Reader r = null ;
     try {
         r = new BufferedReader( new FileReader(filepath)); // 组装BufferedReader流,加入缓冲能力
         char [] data = new char [ 256 ];
         int len = - 1 ;
         while ((len = r.read(data)) != - 1 ) { // -1 表示读取到达文件结尾
             //操作数据
             for ( int i = 0 ; i < len; i++) {
                 System.out.print(data[i]);
             }
         }
     } finally {
         if (r != null ) {
             r.close(); // 关闭流
         }
     }
}

(2)写入文件

与读取文件类似:

如果你需要在不同端使用写入文件,你可以根据你要写的文件是二进制文件还是文本文件,或者根据你要处理的数据是准备采取字节方式还是字符方式,决定使用 FileOutputStream 或者 FileWriter。两者支持你可以一次写入一个字节或者字符到文件中,也可以直接写入一个字节数组或者字符数据。数据按照写入的顺序存储在文件当中。

单字节写入文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void writeFileAsByte() throws IOException {
     String filepath = "file.bin" ;
     java.io.OutputStream os = null ;
     try {
         os = new FileOutputStream(filepath);
         os.write( '1' );
         os.write( '2' );
         os.write( '3' );
         os.write( '4' );
         os.flush(); // 把缓冲区内的数据刷新到磁盘
         
     } finally {
         if (os != null ) {
             os.close(); // 关闭流
         }
     }
}

字节数组写入文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void writeFileAsByteArray() throws IOException {
         String filepath = "file.bin" ;
         java.io.OutputStream os = null ;
         try {
             os = new BufferedOutputStream( new FileOutputStream(filepath));
             // 模拟
             byte [] data = new byte [ 256 ];
             new Random().nextBytes(data);
             
             os.write(data);
             os.flush(); // 把缓冲区内的数据刷新到磁盘
         } finally {
             if (os != null ) {
                 os.close(); // 关闭流
             }
         }
     }

单字符写入文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void writeFileAsChar() throws IOException {
     String filepath = "file.txt" ;
     java.io.Writer w = null ;
     try {
         w = new FileWriter(filepath);
         w.write( '1' );
         w.write( '2' );
         w.write( '3' );
         w.write( '4' );
         w.flush(); // 把缓冲区内的数据刷新到磁盘
         
     } finally {
         if (w != null ) {
             w.close(); // 关闭流
         }
     }
}

字符数组写入文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void writeFileAsCharArray() throws IOException {
         String filepath = "file.txt" ;
         java.io.Writer w = null ;
         try {
             w = new BufferedWriter( new FileWriter(filepath)); // 组装BufferedWriter流,加入缓冲能力
             // 模拟
             char [] data = new char [ 256 ];
             String f = "0123456789abcdefghijklmnopqrstuvwxyz" ;
             Random rd = new Random();
             for ( int i = 0 ; i < data.length; i++) {
                 data[i] = f.charAt(rd.nextInt(f.length()));
             }
             w.write(data);
             w.flush(); // 把缓冲区内的数据刷新到磁盘
         } finally {
             if (w != null ) {
                 w.close(); // 关闭流
             }
         }
     }

(3)随机访问文件

如果你需要不按特定的存取顺序,随意读取或者写入文件,可以考虑RandomAccessFile。

void seek(long pos) 设置到此文件开头测量到的文件指针偏移量,在该位置发生下一个读取或写入操作。

简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) throws IOException {
         RandomAccessFile file = null ;
         try {
             file = new java.io.RandomAccessFile( "file.bin" , "rw" );
             file.seek( 0 );
             
             file.writeChar( '1' );
             file.seek( 0 );
             System.out.println(file.readChar());
             
             /**
              * 读取
              */
             int data = - 1 ;
             while ((data = file.read()) != - 1 ) { // -1 表示读取到达文件结尾
                 //操作数据
                 System.out.print(( byte )data + " " );
             }
             
         } finally {
             if (file != null ) {
                 file.close(); // 关闭流
             }
         }
     }

6、管道(线程内存)

管道为同一JVM中运行的线程提供基于内存的通信机制。但是你不能利用管道在不同的JVM中的线程间通信。

在概念上,Java的管道不同于Unix/Linux系统中的管道。在Unix/Linux中,运行在不同地址空间的两个进程可以通过管道通信。在Java中,通信的双方应该是运行在同一进程中的不同线程。当然除了管道之外,一个JVM中不同线程之间还有许多通信的方式。实际上,线程在大多数情况下会传递完整的对象信息而非原始的字节数据。但是,如果你需要在线程之间传递字节数据,Java IO的管道是一个不错的选择。

当使用两个相关联的管道流时,务必将它们分配给不同的线程。read()方法和write()方法调用时会导致流阻塞,这意味着如果你尝试在一个线程中同时进行读和写,可能会导致线程死锁。

简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
static class Input implements Runnable {
         private final PipedInputStream inputStream = new PipedInputStream();
         public Input() {
         }
         public PipedInputStream getInputStream() {
             return inputStream;
         }
         @Override
         public void run() {
             try {
                 byte [] buf = new byte [ 1024 ];
                 int len = - 1 ;
                 System.out.println( "管道读取准备。" );
                 StringBuffer result = new StringBuffer();
                 while ((len = inputStream.read(buf)) > 0 ) {
                     //System.out.println(new String(buf, 0, len));
                     result.append( new String(buf, 0 , len));
                 }
                 System.out.println( "管道读取结果:" + result.toString());
             } catch (IOException e) {
                 e.printStackTrace();
             } finally {
                 try {
                     if (inputStream != null )
                         inputStream.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
     }
     static class Output implements Runnable {
         private final PipedOutputStream outputStream = new PipedOutputStream();
         public Output() {
         }
         public PipedOutputStream getOutputStream() {
             return outputStream;
         }
         @Override
         public void run() {
             try {
                 System.out.println( "管道写出准备。" );
                 StringBuilder sb = new StringBuilder();
                 // 模拟 通过for循环写入2050个字节
                 for ( int i = 0 ; i < 201 ; i++) {
                     sb.append( "0123456789" );
                     if (i > 0 && (i % 10 == 0 )) {
                         sb.append( "\r\n" );
                     }
                 }
                 String str = sb.toString();
                 outputStream.write(str.getBytes());
                 System.out.println( "管道写出完成。" );
             } catch (IOException e) {
                 e.printStackTrace();
             } finally {
                 try {
                     if (outputStream != null )
                         outputStream.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
     }
     public static void main(String[] args) throws IOException {
         Input input = new Input();
         Output output = new Output();
         /**
          * 将“管道输入流”和“管道输出流”关联起来。
          */
         //input.getInputStream().connect(output.getOutputStream());// 与下面一行等价
         output.getOutputStream().connect(input.getInputStream());
         new Thread(input).start();
         new Thread(output).start();
     }

7、序列化与ObjectInputStream、ObjectOutputStream

使用ObjectInputStream、ObjectOutputStream读取或写入对象,首先该对象必须实现Serializable接口,使得能够序列化和反序列化。

简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@SuppressWarnings ( "unused" )
     public static void main(String[] args) throws IOException {
         class A implements java.io.Serializable {
             private static final long serialVersionUID = -9115696482036699559L;
             private int i = 1 ;
             private float f = 3 ;
             private String s = "风策信" ;
             public A() {
                 super ();
             }
             public A( int i, float f, String s) {
                 super ();
                 this .i = i;
                 this .f = f;
                 this .s = s;
             }
             @Override
             public String toString() {
                 StringBuilder builder = new StringBuilder();
                 builder.append( "A [i=" ).append(i).append( ", f=" ).append(f).append( ", s=" ).append(s).append( "]" );
                 return builder.toString();
             }
         }
         class B implements java.io.Serializable {
             private static final long serialVersionUID = 6124575321340728225L;
             private long i = 2 ;
             private double f = 4 ;
             private String str = "风策信" ;
             public B() {
                 super ();
             }
             public B( long i, double f, String str) {
                 super ();
                 this .i = i;
                 this .f = f;
                 this .str = str;
             }
             @Override
             public String toString() {
                 StringBuilder builder = new StringBuilder();
                 builder.append( "B [i=" ).append(i).append( ", f=" ).append(f).append( ", str=" ).append(str).append( "]" );
                 return builder.toString();
             }
         }
         A a = new A( 1 , 3 , "a" );
         B b = new B( 2 , 4 , "b" );
         //System.out.println(a);
         //System.out.println(b);
         ObjectOutputStream oos = null ;
         try {
             oos = new ObjectOutputStream( new FileOutputStream( "object.data.bin" ));
             oos.writeObject(a);
             oos.writeObject(b);
             oos.flush(); // 把缓冲区内的数据刷新到磁盘
         } finally {
             if (oos != null )
                 oos.close();
         }
         ObjectInputStream ois = null ;
         try {
             ois = new ObjectInputStream( new FileInputStream( "object.data.bin" ));
             A a1 = (A) ois.readObject();
             B b1 = (B) ois.readObject();
             System.out.println(a1);
             System.out.println(b1);
         } catch (ClassNotFoundException e) {
             e.printStackTrace();
         } finally {
             if (ois != null )
                 ois.close();
         }
     }

8、回推流:PushbackInputStream与PushbackReader

PushbackInputStream/PushbackReader 用于解析InputStream/Reader内的数据,允许你读取字节/字符后,回推(pushback)到流中,而不破坏流。

PushbackInputStream类具有以下构造函数:

1
2
PushbackInputStream(InputStream inputStream)
PushbackInputStream(InputStream inputStream, int numBytes)

第一种形式创建的流对象允许将一个字节返回到输入流; 第二种形式创建的流对象具有一个长度为numBytes的回推缓存,从而允许将多个字节回推到输入流中。

提供了unread()方法,如下所示:

1
2
3
void unread( int b)
void unread( byte [] buffer)
void unread( byte [] buffer, int offset, int numBytes)

第一种形式回推b的低字节,这会使得后续的read()调用会把这个字节再次读取出来。第二种形式回推buffer中的字节。第三种形式回推buffer中从offset开始的numBytes个字节。当回推缓存已满时,如果试图回推字节,就会抛出IOException异常。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public static void main(String[] args) throws IOException {
         String filepath = "file.bin" ;
         java.io.OutputStream os = null ;
         try {
             os = new FileOutputStream(filepath);
             os.write( '#' );
             os.write( new byte []{ 'a' , 'b' , 'c' , 'd' });
             os.flush(); // 把缓冲区内的数据刷新到磁盘
         } finally {
             if (os != null ) {
                 os.close(); // 关闭流
             }
         }
         /**
          * 回推(pushback)
          */
         PushbackInputStream pis = null ;
         try {
             //pis = new PushbackInputStream(new FileInputStream(filepath));
             pis = new PushbackInputStream( new FileInputStream(filepath), 3 );
             int len = - 1 ;
             byte [] bytes = new byte [ 2 ];
             while ((len = pis.read(bytes)) != - 1 ) {
                 if ( 'b' == bytes[ 0 ]) {
                     //pis.unread('U');
                     //pis.unread(bytes);
                     pis.unread( new byte []{ '1' , '2' , '3' });
                 }
                 for ( int i = 0 ; i < len; i++) {
                     System.out.print((( char ) bytes[i]));
                 }
             }
             System.out.println();
         } finally {
             if (pis != null )
                 pis.close();
         }
         /**
          * 会发现PushbackInputStream并没有改变目标介质的数据,不破坏流
          */
         try {
             pis = new PushbackInputStream( new FileInputStream(filepath));
             int len = - 1 ;
             byte [] bytes = new byte [ 2 ];
             while ((len = pis.read(bytes)) != - 1 ) {
                 for ( int i = 0 ; i < len; i++) {
                     System.out.print((( char ) bytes[i]));
                 }
             }
         } finally {
             if (pis != null )
                 pis.close();
         }
     }

注:PushbackInputStream对象会使得InputStream对象(用于创建PushbackInputStream对象)的mark()或reset()方法无效。对于准备使用mark()或reset()方法的任何流来说,都应当使用markSupported()方法进行检查。

9、行数记录:LineNumberInputStream与LineNumberReader

LineNumberInputStream与LineNumberReader提供跟踪行号的附加功能。行是以回车符 (‘\r’)、换行符 (‘\n’) 或回车符后面紧跟换行符结尾的字节序列。在所有这三种情况下,都以单个换行符形式返回行终止字符。 行号以 0 开头,并在 read 返回换行符时递增 1。

使用getLineNumber()可以获取当前读取所在行数。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void main(String[] args) throws IOException {
         String filepath = "file.txt" ;
         java.io.Writer w = null ;
         try {
             w = new FileWriter(filepath);
             w.write( "百世山河任凋换,一生意气未改迁。愿从劫火投身去,重自寒灰飞赤鸾。\r\n" );
             w.write( "沧海桑田新几度,月明还照旧容颜。琴心剑魄今何在,留见星虹贯九天。 \n" );
             w.write( "冰轮腾转下西楼,永夜初晗凝碧天。长路寻仙三山外,道心自在红尘间。 \n" );
             w.write( "何来慧剑破心茧,再把貂裘换酒钱。回望天涯携手处,踏歌重访白云间。\n" );
             w.write( "何以飘零去,何以少团栾,何以别离久,何以不得安? \n" );
             w.flush(); // 把缓冲区内的数据刷新到磁盘
         } finally {
             if (w != null ) {
                 w.close(); // 关闭流
             }
         }
         /**
          * LineNumberReader
          */
         LineNumberReader lnr = null ;
         try {
             lnr = new LineNumberReader( new FileReader(filepath));
             int len = - 1 ;
             char [] chars = new char [ 2 ];
             //int lastLineNumber = -1;
             while ((len = lnr.read(chars)) != - 1 ) {
                 for ( int i = 0 ; i < len; i++) {
                     System.out.print((( char ) chars[i]));
                 }
                 /*int lineNumber = lnr.getLineNumber();
                 if (lineNumber != lastLineNumber) {
                     System.out.println("---------------行数:" + lineNumber);
                     lastLineNumber = lineNumber;
                 }*/
             }
             int lineNumber = lnr.getLineNumber();
             System.out.println( "行数:" + lineNumber);
             System.out.println();
         } finally {
             if (lnr != null )
                 lnr.close();
         }
     }

10、StreamTokenizer的使用

StreamTokenizer定义了几种基本的常量用于标识解析过程:TT_EOF(流结尾)、TT_EOL(行结尾)、TT_NUMBER(数字符号, 0 1 2 3 4 5 6 7 8 9 . -都属于数字语法)、TT_WORD(一个单词)。
ttype 在调用 nextToken 方法之后,此字段将包含刚读取的标记的类型。
nval 如果当前标记是一个数字,则此字段将包含该数字的值。
sval 如果当前标记是一个文字标记,则此字段包含一个给出该文字标记的字符的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws IOException {
     StreamTokenizer tokenizer = new StreamTokenizer( new StringReader( "Sven had 7 shining ring..." ));
     while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) { // 流末尾
         if (tokenizer.ttype == StreamTokenizer.TT_WORD) {
             System.out.println(tokenizer.sval);
         } else if (tokenizer.ttype == StreamTokenizer.TT_NUMBER) {
             System.out.println(tokenizer.nval);
         } else if (tokenizer.ttype == StreamTokenizer.TT_EOL) { // 行末尾
             System.out.println();
         }
     }
     //System.out.println(tokenizer.lineno());
}

基本方法介绍一下:

nextToken() – 从此标记生成器的输入流中解析下一个标记。

(1)标记注释

commenChar(int ch) – 指定某个字符为注释字符,此字符之后直到行结尾都被stream tokenizer忽略。

slashSlashComments(boolean flag) – 如果为true,则/*与*/之间的都被认为是注释,反之,不是。

slashStartComments(boolean flag) – 如果为true,则//之后到行结尾的所有都被认为是注释,反之,不是。

(2)基本语义

eolIsSignificant(boolean flag) – 决定一个行结束符是否被当作一个基本的符号处理,如果是true,则被当作一个基本符号,不当作普通的分隔符,如果是false,则保持原义,即当作普通的分隔符。

lowerCaseMode(boolean flag) – 决定是否读取一个单词时是否转变成小写。

parseNumbers() – 当stream tokenizer遭遇到一个单词为双精度的浮点数时,会把它当作一个数字,而不是一个单词。

resetSyntax() – 重置语法表使所有的字符都被认为是“ordinary”。

(3)指定字符语义

ordinaryChar(int ch) – 指定字符在这个tokenizer中保持原义,即只会把当前字符认为普通的字符,不会有其他的语义。
ordinaryChars(int low, int hi) – 指定范围内的字符保持语义,同上

whitespaceChars(int low, int hi) – 字符low与hi之间的所有字符都被当作为空格符,即被认识为tokenzier的分隔符。
wordChars(int low, int hi) – 字符low与hi之间的所有字符都被当作为单词的要素。一个单词是由一个单词要素后面跟着0个或者更多个单词要素或者数字要素。 

11、合并流SequenceInputStream

SequenceInputStream会将与之相连接的流集组合成一个输入流并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的末尾为止。 合并流的作用是将多个源合并合一个源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public static void main(String[] args) throws IOException {
         String filepath1 = "file1.txt" ;
         String filepath2 = "file2.txt" ;
         java.io.Writer w = null ;
         try {
             w = new FileWriter(filepath1);
             w.write( "百世山河任凋换,一生意气未改迁。愿从劫火投身去,重自寒灰飞赤鸾。\r\n" );
             w.write( "沧海桑田新几度,月明还照旧容颜。琴心剑魄今何在,留见星虹贯九天。 \n" );
             w.write( "冰轮腾转下西楼,永夜初晗凝碧天。长路寻仙三山外,道心自在红尘间。 \n" );
             w.write( "何来慧剑破心茧,再把貂裘换酒钱。回望天涯携手处,踏歌重访白云间。\n" );
             w.flush(); // 把缓冲区内的数据刷新到磁盘
         } finally {
             if (w != null ) {
                 w.close(); // 关闭流
             }
         }
         try {
             w = new FileWriter(filepath2);
             w.write( "何以飘零去,何以少团栾,何以别离久,何以不得安? " );
             w.flush(); // 把缓冲区内的数据刷新到磁盘
         } finally {
             if (w != null ) {
                 w.close(); // 关闭流
             }
         }
         java.io.Reader r = null ;
         try {
             Vector<InputStream> v = new Vector<InputStream>( 2 );
             InputStream s1 = new FileInputStream(filepath1);
             InputStream s2 = new FileInputStream(filepath2);
             v.addElement(s1);
             v.addElement(s2);
             r = new BufferedReader( new InputStreamReader( new SequenceInputStream(v.elements())));
             
             char [] data = new char [ 256 ];
             int len = - 1 ;
             while ((len = r.read(data)) != - 1 ) { // -1 表示读取到达文件结尾
                 //操作数据
                 for ( int i = 0 ; i < len; i++) {
                     System.out.print(data[i]);
                 }
             }
         } finally {
             if (r != null ) {
                 r.close(); // 关闭流
             }
         }
     }

本文转载来源:http://www.importnew.com/26644.html

更多Demo:https://git.oschina.net/svenaugustus/MyJavaIOLab

猜你喜欢

转载自blog.csdn.net/dhklsl/article/details/78201660