Java基础IO流
流
流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。IO流最终要以对象来体现,对象都存在IO包中。
IO流的分类
根据处理数据类型的不同分为:字符流和字节流
根据数据流向不同分为:输入流和输出流
注意:流的操作只有两种:读和写。
流的体系因为功能不同,但是有共性内容,不断抽取,形成继承体系。该体系一共有四个基类,而且都是抽象类。
字节流:InputStream OutputStream
字符流:Reader Writer
在这四个系统中,它们的子类,都有一个共性特点:子类名后缀都是父类名,前缀名都是这个子类的功能名称。
Java流类图结构
字节流
InputStream 和 OutputStream 是两个 abstact 类,对于字节为导向的 stream 都扩展这两个基类。
InputStream:是表示字节输入流的所有类的超类。
OutputStream:此抽象类是表示输出字节流的所有类的超类。
处理字节数据的流对象。设备上的数据无论是图片或者dvd,文字,它们都以二进制存储的。二进制的最终都是以一个8位为数据单元进行体现,所以计算机中的最小数据单元就是字节。意味着,字节流可以处理设备上的所有数据,所以字节流一样可以处理字符数据。
- 向文件中写入字符串
-
1234567891011121314151617
package
heimablog;
/**
* 字节流
* 向文件中写入字符串
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"D:"
+File.separator+
"hello.txt"
;
File f=
new
File(fileName);
OutputStream out =
new
FileOutputStream(f);
String str=
"你好"
;
byte
[] b=str.getBytes();
out.write(b);
out.close();
}
}
-
- 向文件中追加新内容
-
1234567891011121314151617181920
package
heimablog;
/**
* 字节流
* 向文件中追加新内容:
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"D:"
+File.separator+
"hello.txt"
;
File f=
new
File(fileName);
OutputStream out =
new
FileOutputStream(f,
true
);
String str=
"Rollen"
;
//String str="\r\nRollen"; 可以换行
byte
[] b=str.getBytes();
for
(
int
i =
0
; i < b.length; i++) {
out.write(b[i]);
}
out.close();
}
}
-
- 读取文件内容
-
123456789101112
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"D:"
+File.separator+
"hello.txt"
;
File f=
new
File(fileName);
InputStream in=
new
FileInputStream(f);
byte
[] b=
new
byte
[
1024
];
in.read(b);
in.close();
System.out.println(
new
String(b));
}
}
- 例子读取出来会有大量的空格,我们可以利用in.read(b);的返回值来设计程序。如下:
12345678910111213
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"D:"
+File.separator+
"hello.txt"
;
File f=
new
File(fileName);
InputStream in=
new
FileInputStream(f);
byte
[] b=
new
byte
[
1024
];
int
len=in.read(b);
in.close();
System.out.println(
"读入长度为:"
+len);
System.out.println(
new
String(b,
0
,len));
}
}
-
观察上面的例子可以看出,我们预先申请了一个指定大小的空间,但是有时候这个空间可能太小,有时候可能太大,我们需要准确的大小,这样节省空间,那么我们可以这样干:
1234567891011121314151617/**
* 字节流
* 读文件内容,节省空间
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"D:"
+File.separator+
"hello.txt"
;
File f=
new
File(fileName);
InputStream in=
new
FileInputStream(f);
byte
[] b=
new
byte
[(
int
)f.length()];
in.read(b);
System.out.println(
"文件长度为:"
+f.length());
in.close();
System.out.println(
new
String(b));
}
}
- 将上面的例子改为一个一个读:
123456789101112131415161718
/**
* 字节流
* 读文件内容,节省空间
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"D:"
+File.separator+
"hello.txt"
;
File f=
new
File(fileName);
InputStream in=
new
FileInputStream(f);
byte
[] b=
new
byte
[(
int
)f.length()];
for
(
int
i =
0
; i < b.length; i++) {
b[i]=(
byte
)in.read();
}
in.close();
System.out.println(
new
String(b));
}
}
- 上面的几个例子都是在知道文件的内容多大,然后才展开的,有时候我们不知道文件有多大,这种情况下,我们需要判断是否独到文件的末尾:
1234567891011121314151617181920
/**
* 字节流
*读文件
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"D:"
+File.separator+
"hello.txt"
;
File f=
new
File(fileName);
InputStream in=
new
FileInputStream(f);
byte
[] b=
new
byte
[
1024
];
int
count =
0
;
int
temp=
0
;
while
((temp=in.read())!=(-
1
)){
b[count++]=(
byte
)temp;
}
in.close();
System.out.println(
new
String(b));
}
}
-
字符流
Reader:用于读取字符流的抽象类。子类必须实现的方法只有 read(char[], int, int) 和 close()。
Writer:写入字符流的抽象类。子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。
那么为什么要有字符流呢?因为字符每个国家都不一样,所以涉及到了字符编码问题,那么GBK编码的中文用unicode编码解析是有问题的,所以需要获取中文字节数据的同时+ 指定的编码表才可以解析正确数据。为了方便于文字的解析,所以将字节流和编码表封装成对象,这个对象就是字符流。只要操作字符数据,优先考虑使用字符流体系。
- 向文件中写入数据
-
123456789101112131415
/**
* 字符流
* 写入数据
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"D:"
+File.separator+
"hello.txt"
;
File f=
new
File(fileName);
Writer out =
new
FileWriter(f);
String str=
"hello"
;
out.write(str);
out.close();
}
}
这个例子和之前的例子没什么区别,只是你可以直接输入字符串,而不需要你将字符串转化为字节数组。
当你如果想在文件中追加内容的时候,可以使用将上面的声明out的哪一行换为:
Writer out =new FileWriter(f,true);
这样,当你运行程序的时候,会发现文件内容变为:hellohello如果想在文件中换行的话,需要使用“\r\n”
比如将str变为String str="\r\nhello";这样文件追加的str的内容就会换行了。
- 从文件中读内容:
1234567891011121314151617
/**
* 字符流
* 从文件中读出内容
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"D:"
+File.separator+
"hello.txt"
;
File f=
new
File(fileName);
char
[] ch=
new
char
[
100
];
Reader read=
new
FileReader(f);
int
count=read.read(ch);
read.close();
System.out.println(
"读入的长度为:"
+count);
System.out.println(
"内容为"
+
new
String(ch,
0
,count));
}
}
-
当然最好采用循环读取的方式,因为我们有时候不知道文件到底有多大:
1234567891011121314151617181920/**
* 字符流
* 从文件中读出内容
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"D:"
+File.separator+
"hello.txt"
;
File f=
new
File(fileName);
char
[] ch=
new
char
[
100
];
Reader read=
new
FileReader(f);
int
temp=
0
;
int
count=
0
;
while
((temp=read.read())!=(-
1
)){
ch[count++]=(
char
)temp;
}
read.close();
System.out.println(
"内容为"
+
new
String(ch,
0
,count));
}
}
-
BufferedWriter
BufferedWriter是给字符输出流提高效率用的,那就意味着,缓冲区对象建立时,必须要先有流对象。明确要提高具体的流对象的效率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | package heimablog; import java.io.BufferedWriter; import java.io.FileWriter; class ShowTest { public static void main(String[] args) throws Exception { FileWriter fw = new FileWriter( "bufdemo.txt" ); BufferedWriter bufw = new BufferedWriter(fw); // 让缓冲区和指定流相关联。 for ( int x = 0 ; x < 4 ; x++) { bufw.write(x + "abc" ); bufw.newLine(); // 写入一个换行符,这个换行符可以依据平台的不同写入不同的换行符。 bufw.flush(); // 对缓冲区进行刷新,可以让数据到目的地中。 } bufw.close(); // 关闭缓冲区,其实就是在关闭具体的流。 } } |
BufferedReader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | package heimablog; import java.io.BufferedReader; import java.io.FileReader; class ShowTest { public static void main(String[] args) throws Exception { FileReader fr = new FileReader( "bufdemo.txt" ); BufferedReader bufr = new BufferedReader(fr); String line = null ; while ((line=bufr.readLine())!= null ){ //readLine方法返回的时候是不带换行符的。 System.out.println(line); } bufr.close(); } } |
记住:
读取键盘录入:BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
输出到控制台:BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));
字节流和字符流的区别
实际上字节流在操作的时候本身是不会用到缓冲区的,是文件本身的直接操作的,但是字符流在操作的时候是会用到缓冲区的,是通过缓冲区来操作文件的。如果将上面的字节流和字符流的程序的最后一行关闭文件的代码注释掉,然后运行程序看看。你就会发现使用字节流的话,文件中已经存在内容,但是使用字符流的时候,文件中还是没有内容的,这个时候就要刷新缓冲区。
使用字节流好还是字符流好呢?
答案是字节流。首先因为硬盘上的所有文件都是以字节的形式进行传输或者保存的,包括图片等内容。但是字符只是在内存中才会形成的,所以在开发中,字节流使用广泛。
因为功能的不同,流的体系中提供N多的对象。那么开始时,到底该用哪个对象更为合适呢?这就需要明确流的操作规律。
流的操作规律
- 明确源和目的。
数据源:就是需要读取,可以使用两个体系:InputStream、Reader;
数据汇:就是需要写入,可以使用两个体系:OutputStream、Writer;
- 操作的数据是否是纯文本数据?
如果是:数据源:Reader 数据汇:Writer
如果不是:数据源:InputStream 数据汇:OutputStream
- 虽然确定了一个体系,但是该体系中有太多的对象,到底用哪个呢?明确操作的数据设备。
数据源对应的设备:硬盘(File),内存(数组),键盘(System.in)
数据汇对应的设备:硬盘(File),内存(数组),控制台(System.out)。
- 需要在基本操作上附加其他功能吗?比如缓冲。【如果需要就进行装饰】
转换流特有功能:转换流可以将字节转成字符,原因在于,将获取到的字节通过查编码表获取到指定对应字符。
转换流的最强功能就是基于 字节流 + 编码表 。没有转换,没有字符流。
发现转换流有一个子类就是操作文件的字符流对象:
InputStreamReader
|--FileReader
OutputStreamWriter
|--FileWrier
想要操作文本文件,必须要进行编码转换,而编码转换动作转换流都完成了。所以操作文件的流对象只要继承自转换流就可以读取一个字符了。
但是子类有一个局限性,就是子类中使用的编码是固定的,是本机默认的编码表,对于简体中文版的系统默认码表是GBK。
1 2 3 | FileReader fr = new FileReader( "a.txt" ); InputStreamReader isr = new InputStreamReader( new FileInputStream( "a.txt" ), "gbk" ); |
以上两句代码功能一致,
- 如果仅仅使用平台默认码表,就使用FileReader fr = new FileReader("a.txt"); //因为简化。
- 如果需要制定码表,必须用转换流。
转换流 = 字节流+编码表。
转换流的子类File = 字节流 + 默认编码表。
凡是操作设备上的文本数据,涉及编码转换,必须使用转换流。
OutputStreramWriter 和InputStreamReader类
OutputStreramWriter将输出的字符流转化为字节流
InputStreamReader将输入的字节流转换为字符流
但是不管如何操作,最后都是以字节的形式保存在文件中的。
-
将字节输出流转化为字符输出流
12345678910111213/**
* 将字节输出流转化为字符输出流
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"d:"
+File.separator+
"hello.txt"
;
File file=
new
File(fileName);
Writer out=
new
OutputStreamWriter(
new
FileOutputStream(file));
out.write(
"hello"
);
out.close();
}
}
-
将字节输入流变为字符输入流
123456789101112131415/**
* 将字节输入流变为字符输入流
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String fileName=
"d:"
+File.separator+
"hello.txt"
;
File file=
new
File(fileName);
Reader read=
new
InputStreamReader(
new
FileInputStream(file));
char
[] b=
new
char
[
100
];
int
len=read.read(b);
System.out.println(
new
String(b,
0
,len));
read.close();
}
}
-
前面列举的输出输入都是以文件进行的,现在我们以内容为输出输入目的地,使用内存操作流:
ByteArrayInputStream 主要将内容写入内存
ByteArrayOutputStream 主要将内容从内存输出
使用内存操作流将一个大写字母转化为小写字母:
1234567891011121314151617181920/**
* 使用内存操作流将一个大写字母转化为小写字母
* */
import
java.io.*;
class
hello{
public
static
void
main(String[] args)
throws
IOException {
String str=
"ROLLENHOLT"
;
ByteArrayInputStream input=
new
ByteArrayInputStream(str.getBytes());
ByteArrayOutputStream output=
new
ByteArrayOutputStream();
int
temp=
0
;
while
((temp=input.read())!=-
1
){
char
ch=(
char
)temp;
output.write(Character.toLowerCase(ch));
}
String outStr=output.toString();
input.close();
output.close();
System.out.println(outStr);
}
}
内容操作流一般使用来生成一些临时信息采用的,这样可以避免删除的麻烦。
File类
将文件系统中的文件和文件夹封装成了对象。提供了更多的属性和行为可以对这些文件和文件夹进行操作。
这些是流对象办不到的,因为流只操作数据。
File类常见方法
1:创建
boolean createNewFile():在指定目录下创建文件,如果该文件已存在,则不创建。
而对操作文件的输出流而言,输出流对象已建立,就会创建文件,如果文件已存在,会覆盖。除非续写。
boolean mkdir():创建此抽象路径名指定的目录。
boolean mkdirs():创建多级目录。
2:删除
boolean delete():删除此抽象路径名表示的文件或目录。
void deleteOnExit():在虚拟机退出时删除。
注意:在删除文件夹时,必须保证这个文件夹中没有任何内容,才可以将该文件夹用delete删除。
window的删除动作,是从里往外删。注意:java删除文件不走回收站。要慎用。
3:获取
long length():获取文件大小。
String getName():返回由此抽象路径名表示的文件或目录的名称。
String getPath():将此抽象路径名转换为一个路径名字符串。
String getAbsolutePath():返回此抽象路径名的绝对路径名字符串。
String getParent():返回此抽象路径名父目录的抽象路径名,如果此路径名没有指定父目录,则返回 null。
long lastModified():返回此抽象路径名表示的文件最后一次被修改的时间。
File.pathSeparator:返回当前系统默认的路径分隔符,windows默认为 “;”。
File.Separator:返回当前系统默认的目录分隔符,windows默认为 “\”。
4:判断
boolean exists():判断文件或者文件夹是否存在。
boolean isDirectory():测试此抽象路径名表示的文件是否是一个目录。
boolean isFile():测试此抽象路径名表示的文件是否是一个标准文件。
boolean isHidden():测试此抽象路径名指定的文件是否是一个隐藏文件。
boolean isAbsolute():测试此抽象路径名是否为绝对路径名。
5:重命名
boolean renameTo(File dest):可以实现移动的效果。剪切+重命名。
String[] list():列出指定目录下的当前的文件和文件夹的名称。包含隐藏文件。
如果调用list方法的File 对象中封装的是一个文件,那么list方法返回数组为null。如果封装的对象不存在也会返回null。只有封装的对象存在并且是文件夹时,这个方法才有效。
创建一个新文件
1 2 3 4 5 6 7 8 9 10 11 | import java.io.*; class hello{ public static void main(String[] args) { File f= new File( "D:\\hello.txt" ); try { f.createNewFile(); } catch (Exception e) { e.printStackTrace(); } } } |
File类的两个常量
1 2 | System.out.println(File.separator); // \ System.out.println(File.pathSeparator); // ; |
删除一个文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** * 删除一个文件 * */ import java.io.*; class hello{ public static void main(String[] args) { String fileName= "D:" +File.separator+ "hello.txt" ; File f= new File(fileName); if (f.exists()){ f.delete(); } else { System.out.println( "文件不存在" ); } } } |
创建一个文件夹
1 2 3 4 5 6 7 8 9 10 11 | /** * 创建一个文件夹 * */ import java.io.*; class hello{ public static void main(String[] args) { String fileName= "D:" +File.separator+ "hello" ; File f= new File(fileName); f.mkdir(); } } |
文件的复制
采用DOS命令:copy d:\hello.txt d:\rollen.txt
使用程序来复制文件的基本思路还是从一个文件中读入内容,边读边写入另一个文件,就是这么简单:
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 | /** * 文件的复制 * */ import java.io.*; class hello{ public static void main(String[] args) throws IOException { if (args.length!= 2 ){ System.out.println( "命令行参数输入有误,请检查" ); System.exit( 1 ); } File file1= new File(args[ 0 ]); File file2= new File(args[ 1 ]); if (!file1.exists()){ System.out.println( "被复制的文件不存在" ); System.exit( 1 ); } InputStream input= new FileInputStream(file1); OutputStream output= new FileOutputStream(file2); if ((input!= null )&&(output!= null )){ int temp= 0 ; while ((temp=input.read())!=(- 1 )){ output.write(temp); } } input.close(); output.close(); } } |
然后在命令行下面
javac hello.java
java hello d:\hello.txt d:\rollen.txt
现在你就会在d盘看到rollen.txt了。
列出指定目录的全部文件(包括隐藏文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /** * 使用list列出指定目录的全部文件 * */ import java.io.*; class hello{ public static void main(String[] args) { String fileName= "D:" +File.separator; File f= new File(fileName); String[] str=f.list(); for ( int i = 0 ; i < str.length; i++) { System.out.println(str[i]); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * 使用listFiles列出指定目录的全部文件 * listFiles输出的是完整路径 * */ import java.io.*; class hello{ public static void main(String[] args) { String fileName= "D:" +File.separator; File f= new File(fileName); File[] str=f.listFiles(); for ( int i = 0 ; i < str.length; i++) { System.out.println(str[i]); } } } |
判断一个指定的路径是否为目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * 使用isDirectory判断一个指定的路径是否为目录 * */ import java.io.*; class hello{ public static void main(String[] args) { String fileName= "D:" +File.separator; File f= new File(fileName); if (f.isDirectory()){ System.out.println( "YES" ); } else { System.out.println( "NO" ); } } } |
搜索指定目录的全部内容
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 | /** * 列出指定目录的全部内容 * */ import java.io.*; class hello{ public static void main(String[] args) { String fileName= "D:" +File.separator; File f= new File(fileName); print(f); } public static void print(File f){ if (f!= null ){ if (f.isDirectory()){ File[] fileArray=f.listFiles(); if (fileArray!= null ){ for ( int i = 0 ; i < fileArray.length; i++) { //递归调用 print(fileArray[i]); } } } else { System.out.println(f); } } } } |
使用RandomAccessFile写入文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /** * 使用RandomAccessFile写入文件 * */ import java.io.*; class hello{ public static void main(String[] args) throws IOException { String fileName= "D:" +File.separator+ "hello.txt" ; File f= new File(fileName); RandomAccessFile demo= new RandomAccessFile(f, "rw" ); demo.writeBytes( "asdsad" ); demo.writeInt( 12 ); demo.writeBoolean( true ); demo.writeChar( 'A' ); demo.writeFloat( 1 .21f); demo.writeDouble( 12.123 ); demo.close(); } } |
Java.io.outputstream.PrintStream:打印流
- 提供了更多的功能,比如打印方法。可以直接打印任意类型的数据。
- 它有一个自动刷新机制,创建该对象,指定参数,对于指定方法可以自动刷新。
- 它使用的本机默认的字符编码.
- 该流的print方法不抛出IOException。
该对象的构造函数:
- PrintStream(File file) :创建具有指定文件且不带自动行刷新的新打印流。
- PrintStream(File file, String csn) :创建具有指定文件名称和字符集且不带自动行刷新的新打印流。
- PrintStream(OutputStream out) :创建新的打印流。
- PrintStream(OutputStream out, boolean autoFlush) :创建新的打印流。
- PrintStream(OutputStream out, boolean autoFlush, String encoding) :创建新的打印流。
- PrintStream(String fileName) :创建具有指定文件名称且不带自动行刷新的新打印流。
- PrintStream(String fileName, String csn)
PrintStream可以操作目的:1:File对象。2:字符串路径。3:字节输出流。
前两个都JDK1.5版本才出现。而且在操作文本文件时,可指定字符编码了。
当目的是一个字节输出流时,如果使用的println方法,可以在printStream对象上加入一个true参数。这样对于println方法可以进行自动的刷新,而不是等待缓冲区满了再刷新。最终print方法都将具体的数据转成字符串,而且都对IO异常进行了内部处理。
既然操作的数据都转成了字符串,那么使用PrintWriter更好一些。因为PrintWrite是字符流的子类,可以直接操作字符数据,同时也可以指定具体的编码。
PrintWriter:具备了PrintStream的特点同时,还有自身特点:
该对象的目的地有四个:1:File对象。2:字符串路径。3:字节输出流。4:字符输出流。【开发时尽量使用PrintWriter】
方法中直接操作文件的第二参数是编码表。
直接操作输出流的,第二参数是自动刷新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 读取键盘录入将数据转成大写显示在控制台. BufferedReader bufr = new BufferedReader( new InputStreamReader( System.in)); // 源:键盘输入 // 目的:把数据写到文件中,还想自动刷新。 PrintWriter out = new PrintWriter( new FileWriter( "out.txt" ), true ); // 设置true后自动刷新 String line = null ; while ((line = bufr.readLine()) != null ) { if ( "over" .equals(line)) break ; out.println(line.toUpperCase()); // 转大写输出 } // 注意:System.in,System.out这两个标准的输入输出流,在jvm启动时已经存在了。随时可以使用。当jvm结束了,这两个流就结束了。但是,当使用了显示的close方法关闭时,这两个流在提前结束了。 out.close(); bufr.close(); |
管道流
管道流主要可以进行两个线程之间的通信。
PipedOutputStream 管道输出流
PipedInputStream 管道输入流
-
验证管道流
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273/**
* 验证管道流
* */
import
java.io.*;
/**
* 消息发送类
* */
class
Send
implements
Runnable{
private
PipedOutputStream out=
null
;
public
Send() {
out=
new
PipedOutputStream();
}
public
PipedOutputStream getOut(){
return
this
.out;
}
public
void
run(){
String message=
"hello , Rollen"
;
try
{
out.write(message.getBytes());
}
catch
(Exception e) {
e.printStackTrace();
}
try
{
out.close();
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
/**
* 接受消息类
* */
class
Recive
implements
Runnable{
private
PipedInputStream input=
null
;
public
Recive(){
this
.input=
new
PipedInputStream();
}
public
PipedInputStream getInput(){
return
this
.input;
}
public
void
run(){
byte
[] b=
new
byte
[
1000
];
int
len=
0
;
try
{
len=
this
.input.read(b);
}
catch
(Exception e) {
e.printStackTrace();
}
try
{
input.close();
}
catch
(Exception e) {
e.printStackTrace();
}
System.out.println(
"接受的内容为 "
+(
new
String(b,
0
,len)));
}
}
/**
* 测试类
* */
class
hello{
public
static
void
main(String[] args)
throws
IOException {
Send send=
new
Send();
Recive recive=
new
Recive();
try
{
//管道连接
send.getOut().connect(recive.getInput());
}
catch
(Exception e) {
e.printStackTrace();
}
new
Thread(send).start();
new
Thread(recive).start();
}
}
-
打印流
1234567891011121314/**
* 使用PrintStream进行输出
* */
import
java.io.*;
class
hello {
public
static
void
main(String[] args)
throws
IOException {
PrintStream print =
new
PrintStream(
new
FileOutputStream(
new
File(
"d:"
+ File.separator +
"hello.txt"
)));
print.println(
true
);
print.println(
"Rollen"
);
print.close();
}
}
- 当然也可以格式化输出
123456789101112131415
/**
* 使用PrintStream进行输出
* 并进行格式化
* */
import
java.io.*;
class
hello {
public
static
void
main(String[] args)
throws
IOException {
PrintStream print =
new
PrintStream(
new
FileOutputStream(
new
File(
"d:"
+ File.separator +
"hello.txt"
)));
String name=
"Rollen"
;
int
age=
20
;
print.printf(
"姓名:%s. 年龄:%d."
,name,age);
print.close();
}
}
- 使用OutputStream向屏幕上输出内容
12345678910111213141516171819
/**
* 使用OutputStream向屏幕上输出内容
* */
import
java.io.*;
class
hello {
public
static
void
main(String[] args)
throws
IOException {
OutputStream out=System.out;
try
{
out.write(
"hello"
.getBytes());
}
catch
(Exception e) {
e.printStackTrace();
}
try
{
out.close();
}
catch
(Exception e) {
e.printStackTrace();
}
}
}
-
输入输出重定向
123456789101112131415161718192021import
java.io.File;
import
java.io.FileNotFoundException;
import
java.io.FileOutputStream;
import
java.io.PrintStream;
/**
* 为System.out.println()重定向输出
* */
public
class
systemDemo{
public
static
void
main(String[] args){
// 此刻直接输出到屏幕
System.out.println(
"hello"
);
File file =
new
File(
"d:"
+ File.separator +
"hello.txt"
);
try
{
System.setOut(
new
PrintStream(
new
FileOutputStream(file)));
}
catch
(FileNotFoundException e){
e.printStackTrace();
}
System.out.println(
"iadanac"
);
}
}
【运行结果】:eclipse的控制台输出的是hello。然后当我们查看d盘下面的hello.txt文件的时候,会在里面看到:iadanac!
1234567891011121314151617181920import
java.io.File;
import
java.io.FileNotFoundException;
import
java.io.FileOutputStream;
import
java.io.PrintStream;
/**
* System.err重定向 这个例子也提示我们可以使用这种方法保存错误信息
* */
public
class
systemErr{
public
static
void
main(String[] args){
File file =
new
File(
"d:"
+ File.separator +
"hello.txt"
);
System.err.println(
"这些在控制台输出"
);
try
{
System.setErr(
new
PrintStream(
new
FileOutputStream(file)));
}
catch
(FileNotFoundException e){
e.printStackTrace();
}
System.err.println(
"这些在文件中才能看到哦!"
);
}
}
【运行结果】:你会在eclipse的控制台看到红色的输出:“这些在控制台输出”,然后在d盘下面的hello.txt中会看到:这些在文件中才能看到哦!
123456789101112131415161718192021222324252627282930import
java.io.File;
import
java.io.FileInputStream;
import
java.io.FileNotFoundException;
import
java.io.IOException;
/**
* System.in重定向
* */
public
class
systemIn{
public
static
void
main(String[] args){
File file =
new
File(
"d:"
+ File.separator +
"hello.txt"
);
if
(!file.exists()){
return
;
}
else
{
try
{
System.setIn(
new
FileInputStream(file));
}
catch
(FileNotFoundException e){
e.printStackTrace();
}
byte
[] bytes =
new
byte
[
1024
];
int
len =
0
;
try
{
len = System.in.read(bytes);
}
catch
(IOException e){
e.printStackTrace();
}
System.out.println(
"读入的内容为:"
+
new
String(bytes,
0
, len));
}
}
}
【运行结果】:前提是我的d盘下面的hello.txt中的内容是:“这些文件中的内容哦!”,然后运行程序,输出的结果为:读入的内容为:这些文件中的内容哦!
BufferedReader
注意: BufferedReader只能接受字符流的缓冲区,因为每一个中文需要占据两个字节,所以需要将System.in这个字节输入流变为字符输入流,采用:
1 2 | BufferedReader buf = new BufferedReader( new InputStreamReader(System.in)); |
下面给一个实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; /** * 使用缓冲区从键盘上读入内容 * */ public class BufferedReaderDemo{ public static void main(String[] args){ BufferedReader buf = new BufferedReader( new InputStreamReader(System.in)); String str = null ; System.out.println( "请输入内容" ); try { str = buf.readLine(); } catch (IOException e){ e.printStackTrace(); } System.out.println( "你输入的内容是:" + str); } } |
Scanner类
其实我们比较常用的是采用Scanner类来进行数据输入,下面来给一个Scanner的例子吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import java.util.Scanner; /** * Scanner的小例子,从键盘读数据 * */ public class ScannerDemo{ public static void main(String[] args){ Scanner sca = new Scanner(System.in); // 读一个整数 int temp = sca.nextInt(); System.out.println(temp); //读取浮点数 float flo=sca.nextFloat(); System.out.println(flo); //读取字符 } } |
其实Scanner可以接受任何的输入流,下面给一个使用Scanner类从文件中读出内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import java.io.File; import java.io.FileNotFoundException; import java.util.Scanner; /** * Scanner的小例子,从文件中读内容 * */ public class ScannerDemo{ public static void main(String[] args){ File file = new File( "d:" + File.separator + "hello.txt" ); Scanner sca = null ; try { sca = new Scanner(file); } catch (FileNotFoundException e){ e.printStackTrace(); } String str = sca.next(); System.out.println( "从文件中读取的内容是:" + str); } } |
数据操作流DataOutputStream、DataInputStream类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; public class DataOutputStreamDemo{ public static void main(String[] args) throws IOException{ File file = new File( "d:" + File.separator + "hello.txt" ); char [] ch = { 'A' , 'B' , 'C' }; DataOutputStream out = null ; out = new DataOutputStream( new FileOutputStream(file)); for ( char temp : ch){ out.writeChar(temp); } out.close(); } } |
现在我们在上面例子的基础上,使用DataInputStream读出内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class DataOutputStreamDemo{ public static void main(String[] args) throws IOException{ File file = new File( "d:" + File.separator + "hello.txt" ); DataInputStream input = new DataInputStream( new FileInputStream(file)); char [] ch = new char [ 10 ]; int count = 0 ; char temp; while ((temp = input.readChar()) != 'C' ){ ch[count++] = temp; } System.out.println(ch); } } |
合并流 SequenceInputStream
SequenceInputStream主要用来将2个流合并在一起,比如将两个txt中的内容合并为另外一个txt。下面给出一个实例:
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 | import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.SequenceInputStream; /** * 将两个文本文件合并为另外一个文本文件 * */ public class SequenceInputStreamDemo{ public static void main(String[] args) throws IOException{ File file1 = new File( "d:" + File.separator + "hello1.txt" ); File file2 = new File( "d:" + File.separator + "hello2.txt" ); File file3 = new File( "d:" + File.separator + "hello.txt" ); InputStream input1 = new FileInputStream(file1); InputStream input2 = new FileInputStream(file2); OutputStream output = new FileOutputStream(file3); // 合并流 SequenceInputStream sis = new SequenceInputStream(input1, input2); int temp = 0 ; while ((temp = sis.read()) != - 1 ){ output.write(temp); } input1.close(); input2.close(); output.close(); sis.close(); } } |
文件压缩 ZipOutputStream类
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 | import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class ZipOutputStreamDemo1{ public static void main(String[] args) throws IOException{ File file = new File( "d:" + File.separator + "hello.txt" ); File zipFile = new File( "d:" + File.separator + "hello.zip" ); InputStream input = new FileInputStream(file); ZipOutputStream zipOut = new ZipOutputStream( new FileOutputStream( zipFile)); zipOut.putNextEntry( new ZipEntry(file.getName())); // 设置注释 zipOut.setComment( "hello" ); int temp = 0 ; while ((temp = input.read()) != - 1 ){ zipOut.write(temp); } input.close(); zipOut.close(); } } |
上面的这个例子测试的是压缩单个文件,下面的们来看看如何压缩多个文件:
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 | import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * 一次性压缩多个文件 * */ public class ZipOutputStreamDemo2{ public static void main(String[] args) throws IOException{ // 要被压缩的文件夹 File file = new File( "d:" + File.separator + "temp" ); File zipFile = new File( "d:" + File.separator + "zipFile.zip" ); InputStream input = null ; ZipOutputStream zipOut = new ZipOutputStream( new FileOutputStream( zipFile)); zipOut.setComment( "hello" ); if (file.isDirectory()){ File[] files = file.listFiles(); for ( int i = 0 ; i < files.length; ++i){ input = new FileInputStream(files[i]); zipOut.putNextEntry( new ZipEntry(file.getName() + File.separator + files[i].getName())); int temp = 0 ; while ((temp = input.read()) != - 1 ){ zipOut.write(temp); } input.close(); } } zipOut.close(); } } |
大家自然想到,既然能压缩,自然能解压缩,在谈解压缩之前,我们会用到一个ZipFile类,先给一个这个例子吧。java中的每一个压缩文件都是可以使用ZipFile来进行表示的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import java.io.File; import java.io.IOException; import java.util.zip.ZipFile; /** * ZipFile演示 * */ public class ZipFileDemo{ public static void main(String[] args) throws IOException{ File file = new File( "d:" + File.separator + "hello.zip" ); ZipFile zipFile = new ZipFile(file); System.out.println( "压缩文件的名称为:" + zipFile.getName()); } } |
来看看如何加压缩文件了,和之前一样,先让我们来解压单个压缩文件(也就是压缩文件中只有一个文件的情况),我们采用前面的例子产生的压缩文件hello.zip
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 | import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * 解压缩文件(压缩文件中只有一个文件的情况) * */ public class ZipFileDemo2{ public static void main(String[] args) throws IOException{ File file = new File( "d:" + File.separator + "hello.zip" ); File outFile = new File( "d:" + File.separator + "unZipFile.txt" ); ZipFile zipFile = new ZipFile(file); ZipEntry entry = zipFile.getEntry( "hello.txt" ); InputStream input = zipFile.getInputStream(entry); OutputStream output = new FileOutputStream(outFile); int temp = 0 ; while ((temp = input.read()) != - 1 ){ output.write(temp); } input.close(); output.close(); }<br>} |
ZipInputStream类【来解压一个压缩文件中包含多个文件】
当我们需要解压缩多个文件的时候,ZipEntry就无法使用了,如果想操作更加复杂的压缩文件,我们就必须使用ZipInputStream类
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 | import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; /** * 解压缩一个压缩文件中包含多个文件的情况 * */ public class ZipFileDemo3{ public static void main(String[] args) throws IOException{ File file = new File( "d:" + File.separator + "zipFile.zip" ); File outFile = null ; ZipFile zipFile = new ZipFile(file); ZipInputStream zipInput = new ZipInputStream( new FileInputStream(file)); ZipEntry entry = null ; InputStream input = null ; OutputStream output = null ; while ((entry = zipInput.getNextEntry()) != null ){ System.out.println( "解压缩" + entry.getName() + "文件" ); outFile = new File( "d:" + File.separator + entry.getName()); if (!outFile.getParentFile().exists()){ outFile.getParentFile().mkdir(); } if (!outFile.exists()){ outFile.createNewFile(); } input = zipFile.getInputStream(entry); output = new FileOutputStream(outFile); int temp = 0 ; while ((temp = input.read()) != - 1 ){ output.write(temp); } input.close(); output.close(); } } } |
PushBackInputStream回退流
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 | import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.PushbackInputStream; /** * 回退流操作 * */ public class PushBackInputStreamDemo{ public static void main(String[] args) throws IOException{ String str = "hello,rollenholt" ; PushbackInputStream push = null ; ByteArrayInputStream bat = null ; bat = new ByteArrayInputStream(str.getBytes()); push = new PushbackInputStream(bat); int temp = 0 ; while ((temp = push.read()) != - 1 ){ if (temp == ',' ){ push.unread(temp); temp = push.read(); System.out.print( "(回退" + ( char ) temp + ") " ); } else { System.out.print(( char ) temp); } } } } |
1 2 3 4 5 6 7 8 | /** * 取得本地的默认编码 * */ public class CharSetDemo{ public static void main(String[] args){ System.out.println( "系统默认编码为:" + System.getProperty( "file.encoding" )); } } |
乱码的产生:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; /** * 乱码的产生 * */ public class CharSetDemo2{ public static void main(String[] args) throws IOException{ File file = new File( "d:" + File.separator + "hello.txt" ); OutputStream out = new FileOutputStream(file); byte [] bytes = "你好" .getBytes( "ISO8859-1" ); out.write(bytes); out.close(); } } |
一般情况下产生乱码,都是由于编码不一致的问题。
对象的序列化
对象序列化就是把一个对象变为二进制数据流的一种方法。
一个类要想被序列化,就行必须实现java.io.Serializable接口。虽然这个接口中没有任何方法,就如同之前的cloneable接口一样。实现了这个接口之后,就表示这个类具有被序列化的能力。
先让我们实现一个具有序列化能力的类吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import java.io.*; /** * 实现具有序列化能力的类 * */ public class SerializableDemo implements Serializable{ public SerializableDemo(){ } public SerializableDemo(String name, int age){ this .name=name; this .age=age; } @Override public String toString(){ return "姓名:" +name+ " 年龄:" +age; } private String name; private int age; } |
这个类就具有实现序列化能力,在继续将序列化之前,先看一下ObjectInputStream和ObjectOutputStream这两个类
当我们查看产生的hello.txt的时候,看到的是乱码,因为是二进制文件。
虽然我们不能直接查看里面的内容,但是我们可以使用ObjectInputStream类查看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import java.io.File; import java.io.FileInputStream; import java.io.ObjectInputStream; /** * ObjectInputStream示范 * */ public class ObjectInputStreamDemo{ public static void main(String[] args) throws Exception{ File file = new File( "d:" + File.separator + "hello.txt" ); ObjectInputStream input = new ObjectInputStream( new FileInputStream( file)); Object obj = input.readObject(); input.close(); System.out.println(obj); } } |
到底序列化什么内容呢?其实只有属性会被序列化。
Externalizable接口
被Serializable接口声明的类的对象的属性都将被序列化,但是如果想自定义序列化的内容的时候,就需要实现Externalizable接口。
当一个类要使用Externalizable这个接口的时候,这个类中必须要有一个无参的构造函数,如果没有的话,在构造的时候会产生异常,这是因为在反序列话的时候会默认调用无参的构造函数。
现在我们来演示一下序列化和反序列话:
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 | package IO; 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 ExternalizableDemo{ public static void main(String[] args) throws Exception{ ser(); // 序列化 dser(); // 反序列话 } public static void ser() throws Exception{ File file = new File( "d:" + File.separator + "hello.txt" ); ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream( file)); out.writeObject( new Person( "rollen" , 20 )); out.close(); } public static void dser() throws Exception{ File file = new File( "d:" + File.separator + "hello.txt" ); ObjectInputStream input = new ObjectInputStream( new FileInputStream( file)); Object obj = input.readObject(); input.close(); System.out.println(obj); } } class Person implements Externalizable{ public Person(){ } public Person(String name, int age){ this .name = name; this .age = age; } @Override public String toString(){ return "姓名:" + name + " 年龄:" + age; } // 复写这个方法,根据需要可以保存的属性或者具体内容,在序列化的时候使用 @Override public void writeExternal(ObjectOutput out) throws IOException{ out.writeObject( this .name); out.writeInt(age); } // 复写这个方法,根据需要读取内容 反序列话的时候需要 @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException{ this .name = (String) in.readObject(); this .age = in.readInt(); } private String name; private int age; } |
【运行结果】:
姓名:rollen 年龄:20
本例中,我们将全部的属性都保留了下来,
Serializable接口实现的操作其实是吧一个对象中的全部属性进行序列化,当然也可以使用我们上使用是Externalizable接口以实现部分属性的序列化,但是这样的操作比较麻烦,
当我们使用Serializable接口实现序列化操作的时候,如果一个对象的某一个属性不想被序列化保存下来,那么我们可以使用transient关键字进行说明:
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 | package IO; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 序列化和反序列化的操作 * */ public class serDemo{ public static void main(String[] args) throws Exception{ ser(); // 序列化 dser(); // 反序列话 } public static void ser() throws Exception{ File file = new File( "d:" + File.separator + "hello.txt" ); ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream( file)); out.writeObject( new Person1( "rollen" , 20 )); out.close(); } public static void dser() throws Exception{ File file = new File( "d:" + File.separator + "hello.txt" ); ObjectInputStream input = new ObjectInputStream( new FileInputStream( file)); Object obj = input.readObject(); input.close(); System.out.println(obj); } } class Person1 implements Serializable{ public Person1(){ } public Person1(String name, int age){ this .name = name; this .age = age; } @Override public String toString(){ return "姓名:" + name + " 年龄:" + age; } // 注意这里 private transient String name; private int age; } |
【运行结果】:
姓名:null 年龄:20
最后在给一个序列化一组对象的例子吧:
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 | import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 序列化一组对象 * */ public class SerDemo1{ public static void main(String[] args) throws Exception{ Student[] stu = { new Student( "hello" , 20 ), new Student( "world" , 30 ), new Student( "rollen" , 40 ) }; ser(stu); Object[] obj = dser(); for ( int i = 0 ; i < obj.length; ++i){ Student s = (Student) obj[i]; System.out.println(s); } } // 序列化 public static void ser(Object[] obj) throws Exception{ File file = new File( "d:" + File.separator + "hello.txt" ); ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream( file)); out.writeObject(obj); out.close(); } // 反序列化 public static Object[] dser() throws Exception{ File file = new File( "d:" + File.separator + "hello.txt" ); ObjectInputStream input = new ObjectInputStream( new FileInputStream( file)); Object[] obj = (Object[]) input.readObject(); input.close(); return obj; } } class Student implements Serializable{ public Student(){ } public Student(String name, int age){ this .name = name; this .age = age; } @Override public String toString(){ return "姓名: " + name + " 年龄:" + age; } private String name; private int age; } |
【运行结果】:
姓名: hello 年龄:20
姓名: world 年龄:30
姓名: rollen 年龄:40
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· 趁着过年的时候手搓了一个低代码框架
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· 乌龟冬眠箱湿度监控系统和AI辅助建议功能的实现