字节流与字符流读写

一、字节流读写

单位字节 (byte),即 8 位二进制数。

读取方式:它不管你读的是什么文件(是图片、视频还是文本),它都把内容当成一串原始的二进制数据来搬运。

你的代码inputStream.read(bytes) 读取的就是纯粹的字节。如果你用 System.out.printf("0x%02X", bytes[i]) 打印,看到的是内存中的真实二进制数据。

适用:所有类型的文件(万能)。

使用 Java 字节流读取任何二进制数据

法一:

public class Demo7_1 {
    public static void main(String[] args) throws IOException {
        //选择合适的 InputStream 子类(如 FileInputStream),并指定数据源。
        InputStream inputStream = new FileInputStream("./1.txt");

        //循环读取数据
        while(true) {
            int data = inputStream.read();
            if(data == -1) {
                break;
            }

            //以 16 进制格式化打印读取数据
            System.out.printf("0x%X\n",data);
        }
        
        关闭流(释放资源)
        inputStream.close();
    }
}

以上代码就实现了从 1.txt 文件中读取字节数据的功能,我们来刨析一下:

1、为什么要 使用 InputStream inputStream = new FileInputStream("./1.txt");

“左边写父类(或接口),右边写具体的实现类” —— 这是写出高质量、可扩展 Java 代码的基本功。

(1)、语法层面:向上转型(Upcasting)

选择合适的 InputStream 子类(如 FileInputStream),并指定数据源。 InputStream inputStream = new FileInputStream("./1.txt"); 是一种典型的 “父类引用指向子类对象” 的写法,体现了 Java 中 多态(Polymorphism) 和 面向抽象编程 的核心思想FileInputStream 是 InputStream 的子类。将子类对象赋值给父类类型的变量,称为 向上转型。这在 Java 中是自动且安全的,不需要强制类型转换。

// 向上转型(隐式)
InputStream inputStream = new FileInputStream("..."); // ✅ 合法

// 向下转型(需显式强转,且有风险)
FileInputStream fis = (FileInputStream) inputStream; // ⚠️ 需确保类型匹配

(2)、设计层面:面向接口/抽象类编程

虽然 InputStream 是一个 抽象类(不是接口),但它扮演了“通用输入流”的角色。这种写法的好处是:

  • 代码更通用、灵活

你后续调用的方法(如 read()close())都是通过 InputStream 定义的通用接口进行的。

这意味着:将来可以轻松替换数据源,而无需修改使用逻辑。

// 今天读文件
InputStream in = new FileInputStream("data.txt");

// 明天读网络
InputStream in = new SocketInputStream(socket);

// 后天读内存
InputStream in = new ByteArrayInputStream(bytes);

→ 只要右边换一个 InputStream 的实现类,左边和后续代码完全不用动

  • 解耦与可维护性高

你的业务逻辑只依赖于“能读字节”这个能力(InputStream 抽象),而不关心底层是文件、网络还是内存。这符合 “依赖倒置原则”(DIP)。

  • 便于组合与装饰(Decorator 模式)

Java I/O 流大量使用装饰器模式,这种写法天然支持:

InputStream in = new BufferedInputStream(
                    new FileInputStream("large.txt")
                 );

这里 BufferedInputStream 包装了 FileInputStream,但对外仍表现为 InputStream,使用方式不变

2、int data = inputStream.read();

是 Java 字节流(InputStream)中最基础、最核心的单字节读取操作。它的作用是从输入流中读取下一个字节的数据,并以 int 类型返回。

(1)、返回值含义

⚠️ 注意:虽然读的是 byte(有符号,-128~127),但返回 int 是为了能用 -1 表示结束,同时避免负数字节(如 0xFF = -1)与结束标志混淆。

(2)为什么返回 int 而不是 byte

如果返回 byte,那么当读到字节值为 -1(即 0xFF)时,程序无法区分这是“有效数据”还是“流结束”。使用 int 后:有效字节被提升为 0~255 的正整数;-1 专用于表示“结束”,无歧义。

3、关闭字节流

inputStream.close(); 是 Java I/O 操作中释放系统资源的关键一步。它的作用是关闭输入流,并释放与该流关联的所有底层系统资源(如文件句柄、网络连接、内存缓冲区等)。

为什么必须调用 close()

  1. 防止资源泄漏(Resource Leak)

    • 每打开一个文件,操作系统会分配一个 文件描述符(File Descriptor)。

    • 如果不关闭流,这些描述符会一直被占用,直到程序退出。

    • 操作系统对单个进程能打开的文件数有限制(如 Linux 默认 1024),资源耗尽会导致程序崩溃(抛出 IOException: Too many open files)。

  2. 确保数据完整性(对输出流更重要)

    • 虽然 InputStream 主要是读取,但某些流(如带缓冲的 BufferedInputStream)可能在内部维护状态,及时关闭有助于清理。

  3. 符合“谁打开,谁关闭”原则

    • 良好的编程习惯:打开资源后,必须显式或隐式地关闭它。

法二

public class Demo7_2 {
    public static void main(String[] args) throws IOException {
        InputStream inputStream = new FileInputStream("./1.txt");

        //通过 read 读取数据,一次就读一个字节数组
        while(true) {
            //长度随便定,无所谓
            byte[] bytes = new byte[1024];

            //read 方法会尽可能的把参数的数组填满
            //返回值表示实际读取到的字节数
            int n = inputStream.read(bytes);
            if(n == -1) {
                break;
            }
            for (int i = 0; i < n; i++) {
                System.out.printf("0x%X\n",bytes[i]);
            }
        }

        //若上面代码出现异常,close() 可能执行不到
        inputStream.close();
    }
}

法二与法一的区别是,这使用字节流逐块读取文件并以十六进制形式打印每个字节

4、int n = inputStream.read(bytes);

是 Java 字节流(InputStream)中“批量读取”数据的标准写法,属于 I/O 编程中最核心、最常用的模式之一。

  • 参数byte[] b —— 一个字节数组,作为缓冲区(buffer),用于接收读取到的数据。
  • 返回值int —— 表示实际读取到的字节数(不是数组长度!)。

✅ 所以 int n = inputStream.read(bytes); 的意思是:
“尝试从输入流中读取最多 bytes.length 个字节,存入 bytes 数组,并把实际读到的字节数赋给变量 n。”

✅ 具体过程(以文件大小 > 1024 字节为例)

假设文件总长度为 2500 字节,你使用 byte[1024] 缓冲区:

循环次数调用 read(bytes) 后实际返回值 n读取的字节范围流内部指针位置
第 1 次读取前 1024 字节n = 1024字节 0 ~ 1023指向第 1024 字节(即下一次从 1024 开始)
第 2 次读取接下来的 1024 字节n = 1024字节 1024 ~ 2047指向第 2048 字节
第 3 次读取剩余 452 字节n = 452字节 2048 ~ 2499指向文件末尾(EOF)
第 4 次尝试再读n = -1无数据已到末尾,返回 -1

法三:

public class Demo7_3 {
    public static void main(String[] args) throws IOException {
        InputStream inputStream = null;
        try {
            inputStream = new FileInputStream("./1.txt");
            //通过 read 读取数据,一次就读一个字节数组
            while(true) {
                //长度随便定,无所谓
                byte[] bytes = new byte[1024];

                //read 方法会尽可能的把参数的数组填满
                //返回值表示实际读取到的字节数
                int n = inputStream.read(bytes);
                if(n == -1) {
                    break;
                }
                for (int i = 0; i < n; i++) {
                    System.out.printf("0x%X\n",bytes[i]);
                }
            }

        } finally {
            //关闭文件
            inputStream.close();
        }
    }
}

法三保证了inputStream.close() 被执行

5、保证inputStream.close() 被执行

在法二中,虽然方法声明了 throws IOException,但 read() 或 printf() 在循环中仍可能抛出异常。一旦异常抛出,程序会直接跳转到调用栈上层,跳过 close() 语句。结果:文件句柄未释放 → 资源泄漏(Resource Leak)。

法四:

public class Demo8 {
    // try with resource
    public static void main(String[] args) {
        try(InputStream inputStream = new FileInputStream("./1.txt")) {
            while(true) {
                byte[] bytes = new byte[1024];
                int n = inputStream.read(bytes);
                if(n == -1) {
                    break;
                }
                for (int i = 0; i < n; i++) {
                    System.out.printf("0x%X\n",bytes[i]);
                }
            }
            // close 不必写了
            //会在 try 结束的时候,自动调用 close()
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

6、使用 try-with-resources 确保资源释放

try-with-resources 是 Java 7 引入的一种自动资源管理(Automatic Resource Management, ARM) 语法,用于确保每个打开的资源在使用完毕后都能被正确关闭,即使发生异常也不会泄漏资源。

“任何实现了 Closeable / AutoCloseable 的资源,都必须用 try-with-resources 管理!

✅ 基本语法

try (ResourceType resource = new ResourceType(...)) {
    // 使用 resource 的代码
} catch (ExceptionType e) {
    // 处理异常
}
// resource 在此处自动关闭(无论是否抛出异常)

前提:资源类必须实现 java.lang.AutoCloseable 接口(Closeable 是其子接口)。
Java I/O 中的所有流(如 FileInputStreamFileReader)都实现了该接口。

7、3 种 read 方法

1、int read()

  • 读取单个字节(8 位)。

  • 返回该字节的 无符号整数值(0 ~ 255)。

  • 如果已到达流末尾(EOF),返回 -1

2、int read(byte[] b)

  • 尝试读取 最多 b.length 个字节 到数组 b 中。

  • 返回 实际读取的字节数(0 ≤ n ≤ b.length)。

  • 如果已到流末尾,返回 -1

 3. int read(byte[] b, int off, int len)

  • 从输入流中读取 最多 len 个字节,存入数组 b 的 从索引 off 开始的位置。

  • 返回 实际读取的字节数。

  • 流结束时返回 -1

使用 Java 字节流写任何二进制数据

法一:

public class Demo9_1 {
    public static void main(String[] args) {
        //如果没有 true 会把原来的内容清空再写
        try(OutputStream outputStream = new FileOutputStream("./1.txt")) {

            // 97 十进制,表示 a
            outputStream.write(97);
            outputStream.write(98);
            outputStream.write(99);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

法二:

public class Demo9_2 {
    public static void main(String[] args) {
        //加了 true, 实现了追加写的作业,原来的内容不消失
        try(OutputStream outputStream = new FileOutputStream("./1.txt",true)) {

            // 97 十进制,表示 a
            outputStream.write(97);
            outputStream.write(98);
            outputStream.write(99);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

法三:

public class Demo9_3 {
    public static void main(String[] args) {
        try(OutputStream outputStream = new FileOutputStream("./1.txt",true)) {
            byte[] bytes = {97,98,99};
            outputStream.write(bytes);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

法四:

ublic class Demo9_4 {
    public static void main(String[] args) {
        try (OutputStream outputStream = new FileOutputStream("./1.txt",true)) {

            byte[] bytes = {97,98,99,100,101,102,103,104,105,106};
            outputStream.write(bytes,2,3);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

二、字符流读写

单位字符 (char),即 16 位 Unicode 字符。

读取方式:它专门用于读取文本。它在底层其实也是读取字节,但它会根据指定的编码规则(如 UTF-8、GBK),自动将“字节”翻译成“人类可读的字符”。

适用:纯文本文件(.txt, .java, .html 等)。

使用字符流读取数据

public class Demo10 {
    public static void main(String[] args) {
        try(Reader reader = new FileReader("./1.txt")) {
            //使用 read 方法读取数据
//            while(true) {
//                int data = reader.read();
//                if(data == -1) {
//                    break;
//                }
//                char c = (char)data;
//                System.out.println(c);
//            }

            while(true) {
                char[] chars = new char[1024];
                int n = reader.read(chars);
                if(n == -1) {
                    break;
                }
                for (int i = 0; i < n; i++) {
                    System.out.println(chars[i]);
                }
            }
        } catch(IOException e) {
            e.printStackTrace();
        }
    }
}

1. int read()

 这是最基本的方法,用于逐个字符读取。

2. int read(char[] cbuf) 

此方法通过字符数组进行批量读取,能显著提升读取效率。

3. int read(char[] cbuf, int off, int len) 

这是最灵活的重载方法,允许将字符读入数组的指定部分。

使用字符流写数据

public class Demo11 {
    public static void main(String[] args) {
        try(Writer writer = new FileWriter("./1.txt",true)) {

            writer.write('我');
            writer.write('真');
            writer.write('美');
            char[] chars = {'我','真','美'};
            writer.write(chars);

            String s = "你也挺美的";
            writer.write(s);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1、void write(int c)

  • 功能:写入单个字符。参数c是一个表示字符的整数(0-65535,对应Unicode字符)。

  • 示例

    writer.write(65); // 写入字符 'A'
    writer.write('H'); // 直接写入字符 'H'

2、void write(char[] cbuf)

  • 功能:写入整个字符数组cbuf中的所有字符。
  • 示例
    char[] chars = {'H', 'e', 'l', 'l', 'o'};
    writer.write(chars);

3、void write(char[] cbuf, int off, int len)

  • 功能:写入字符数组cbuf中从索引off开始的len个字符。这是对字符数组进行部分写入的精确控制方法。
  • 示例
    char[] chars = {'H', 'e', 'l', 'l', 'o'};
    writer.write(chars, 0, 3); // 只写入 "Hel"

4、void write(String str)

  • 功能:写入整个字符串str
  • 示例
    writer.write("Hello, World!");

5、void write(String str, int off, int len)

  • 功能:写入字符串str中从索引off开始的len个字符。这是对字符串进行部分写入的精确控制方法。
  • 参数
    • str:要写入的字符串。
    • startingIndex (off):获取字符部分的起始索引。
    • lengthOfstring (len):要写入的字符串长度。
  • 示例
    String str = "GeeksForGeeks";
    writer.write(str, 0, 5); // 写入 "Geeks"[11](@ref)

三、字节流与字符流的区别

假设文件里存的是中文字符 “中”

  • 在磁盘上(UTF-8编码):它实际上占用了 3 个字节,数据可能是 0xE4 0xB8 0xAD

  • 字节流读取

    • 它会读出 3 个独立的字节:E4B8AD

    • 如果你直接把这些字节转成字符串打印,可能会显示乱码(比如 ),因为它不知道这 3 个字节合起来才表示一个“中”字。

  • 字符流读取

    • 它会根据编码规则(UTF-8),把这 3 个字节自动拼装、翻译,最终给你返回一个字符 '中'

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值