Java学习笔记(二十二):常用IO流及Properties
数据输入输出流
该流属于字节流。有一个规律,一般以Stream结尾的流都是字节流。
该流的特点在于,与之前的字节流写入读取都是通过字节,该流可以直接写入和读取基本数据类型。
数据输入流: DataInputStream(InputStream in)
数据输出流: DataOutputStream(OutputStream out)
public class Mytest {
public static void main(String[] args) throws IOException {
writeData();
//读取数据:怎么写的,就怎么读取,顺序不要乱
DataInputStream in = new DataInputStream(new FileInputStream("a.txt"));
int i = in.readInt();
System.out.println(i);
boolean b = in.readBoolean();
System.out.println(b);
double v = in.readDouble();
System.out.println(v);
char c = in.readChar();
System.out.println(c);
String s = in.readUTF();
System.out.println(s);
in.close();
}
private static void writeData() throws IOException {
//写入基本数据类型
DataOutputStream dos = new DataOutputStream(new FileOutputStream("a.txt"));
dos.writeInt(1000);
dos.writeBoolean(true);
dos.writeDouble(3.14);
dos.writeChar('你');
dos.writeUTF("呵呵");
dos.close();
}
}
如果改变顺序,那么结果并不会报异常,只是读取的数据是错误的。改变顺序:
public class Mytest {
public static void main(String[] args) throws IOException {
writeData();
//改变读写中的顺序,先读Int再读Boolean
DataInputStream in = new DataInputStream(new FileInputStream("a.txt"));
int i = in.readInt();
System.out.println(i);
boolean b = in.readBoolean();
System.out.println(b);
in.close();
}
private static void writeData() throws IOException {
//写入基本数据类型
DataOutputStream dos = new DataOutputStream(new FileOutputStream("a.txt"));
dos.writeBoolean(true);
dos.writeInt(1000);
dos.close();
}
}
结果输出boolean值为false,Int值为16777219。
内存操作流
该流不直接去关联文件,只是在内存中进行读写。
a:操作字节数组
ByteArrayOutputStream(byte[] buf):其中的数据被写入一个 byte 数组充当缓冲区,缓冲区会随着数据的不断写入而自动增长。
ByteArrayInputStream(byte[] buf):包含一个内部缓冲区,该缓冲区包含从流中读取的字节。内部计数器跟踪 read 方法要提供的下一个字节。
此流关闭无效,所以无需关闭。
//下面的例子是使用内存操作流将两首歌合并成一首歌
//其思路是将File文件读取到内存,再从内存中读出,复制到另外一个路径下
public class MyTest {
public static void main(String[] args) throws IOException {
//把多首歌合并成一首歌
FileInputStream in2 = new FileInputStream("许巍 - 蓝莲花.mp3");
FileInputStream in1 = new FileInputStream("许巍 - 曾经的你.mp3");
ArrayList<FileInputStream> list = new ArrayList<>();
list.add(in1);
list.add(in2);
//遍历集合,读取两首歌的字节数据,并把两首歌的字节数据,放到内存操作流所维护的字节数组中
//ByteArrayOutputStream在内存中维护了一个字节数组,将歌曲的字节数据读入内存
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] bytes=new byte[1024*8];
int len=0;
for (FileInputStream fin : list) {
while ((len=fin.read(bytes))!=-1){
bos.write(bytes,0,len);
}
fin.close();
}
//取出两首歌的字节数据
byte[] allBytes = bos.toByteArray();
//把两首歌的字节数据写到一个文件中
//ByteArrayInputStream在内存中维护了一个内部字节数组,将歌曲的字节数据读入内存
ByteArrayInputStream bis = new ByteArrayInputStream(allBytes);
FileOutputStream out = new FileOutputStream("C:\\Users\\ShenMouMou\\Desktop\\许巍歌曲大连唱.mp3");
byte[] bytes2 = new byte[1024 * 8];
int len2 = 0;
while ((len2 = bis.read(bytes2)) != -1) {
out.write(bytes2, 0, len2);
}
out.close();
System.out.println("合并完成");
}
}
b:操作字符数组
CharArrayWrite(char[] buf)
CharArrayReader(char[] buf)
c:操作字符串
StringWriter(String s)
StringReader(String s)
打印流
特点:
-
打印流只能操作目的地,不能操作数据源,即不能进行读取数据;
-
可以操作任意数据类型的数据,调用print() 方法可以写任意数据类型;
-
如果我们启用自动刷新,那么在调用println、printf 或 format 方法中的一个方法的时候,会完成自动刷新。
通过以下构造创建对象 能够启动自动刷新 然后调用println、printf 或 format 方法中的一个方法的时候,会完成自动刷新
- public PrintWriter(OutputStream out, boolean autoFlush) 启动 自动刷新
- public PrintWriter(Writer out, boolean autoFlush) 启动自动刷新
-
这个流可以直接对文件进行操作(可以直接操作文件的流: 就是构造方法的参数可以传递文件或者文件路径)
public class MyTest {
public static void main(String[] args) throws FileNotFoundException {
//打印流:就是单个的,只能往出写数据,
//打印流:字符打印流。字节打印流
//字符打印流
//PrintWriter
/* PrintWriter(File file)
使用指定文件创建不具有自动行刷新的新 PrintWriter。
PrintWriter(String fileName)
创建具有指定文件名称且不带自动行刷新的新 PrintWriter。
*/
PrintWriter writer = new PrintWriter(new File("b.txt"));
writer.write("abc");
writer.write("\r\n");
//写成并换行
writer.println("aaaaaaaaaaaa");
writer.println(200);
writer.println(true);
writer.print("aaaa2222");
writer.write("\r\n");
writer.print("aaaa2222");
writer.flush();
writer.close();
}
}
//开启自动刷新:
public class MyTest2 {
public static void main(String[] args) throws IOException {
/* PrintWriter(OutputStream out, boolean autoFlush)
通过现有的 OutputStream 创建新的 PrintWriter。
PrintWriter(Writer out, boolean autoFlush)
创建新 PrintWriter。*/
//参数2:true开启自动刷新
/* 如果启用了自动刷新,则只有在调用 println、printf 或 format
的其中一个方法时才可能完成此操作,而不是每当正好输出换行符时才完成。*/
PrintWriter writer = new PrintWriter(new FileWriter("c.txt"), true);
writer.print("abc555555");
writer.println("cccc");
writer.println("cccc");
writer.println("cccc");
writer.println("cccc");
writer.println("cccc");
writer.close();
}
}
//打印流复制文本文件
public class MyTest4 {
public static void main(String[] args) throws IOException {
//我们使用 BufferedReader和 PrintWriter配合来复制文本文件
BufferedReader reader = new BufferedReader(new FileReader("Mytest.java"));
//开启自动刷新
PrintWriter writer = new PrintWriter(new FileWriter("copy.txt"),true);
String line=null;
while ((line=reader.readLine())!=null){
writer.println(line);
}
reader.close();
writer.close();
}
}
标准输入输出流
在System这个类中存在两个静态的成员变量:
-
public static final InputStream in: 标准输入流, 对应的设备是键盘;
-
public static final PrintStream out: 标准输出流 , 对应的设备就是显示器;
System.in的类型是InputStream。
System.out的类型是PrintStream,是OutputStream的孙子类,FilterOutputStream 的子类。新的实现键盘录入的方法:
public class MyTest34 {
public static void main(String[] args) throws IOException {
//public static final InputStream in“标准”输入流。
// 此流已打开并准备提供输入数据。通常,此流对应于键盘输入或者由主机环境或用户指定的另一个输入源。
// Scanner scanner = new Scanner(System.in);
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.println("请输入数据");
String s = reader.readLine();
//自己定义一个结束标记
if ("886".equals(s)) {
break;
}
System.out.println(s);
}
}
}
随机访问流
RandomAccessFile类不属于流,是Object类的子类。但它融合了InputStream和OutputStream的功能。支持对随机访问文件的读取和写入。
RandomAccessFile的父类是Object , 这个流对象可以用来读取数据也可以用来写数据,可以操作任意数据类型的数据。
随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针。输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。
我们可以通过getFilePointer方法获取文件指针,并且可以通过seek方法设置文件指针。
RandomAccessFile(String name, String mode)
创建从中读取和向其中写入(可选)的随机访问文件流,该文件具有指定名称。mode是指读写的模式:只读、只写、读写。
public class MyTest {
public static void main(String[] args) throws IOException {
writeData();
RandomAccessFile read = new RandomAccessFile("f.txt", "rw");
//怎么写的就怎么读取,顺序不要乱
int i = read.readInt();
System.out.println(i);
//获取文件指针的位置
long filePointer = read.getFilePointer();
System.out.println("文件指针的位置"+filePointer);//读取到4位置,因为一个Int变量占4个
boolean b = read.readBoolean();
System.out.println(b);
filePointer = read.getFilePointer();
System.out.println("文件指针的位置" + filePointer);//读取到5位置,因为一个Boolean变量占1个
double v = read.readDouble();
System.out.println(v);
filePointer = read.getFilePointer();
System.out.println("文件指针的位置" + filePointer);//读取到13位置,因为一个Double变量占8个
String s = read.readUTF();
System.out.println(s);
filePointer = read.getFilePointer();
System.out.println("文件指针的位置" + filePointer);//读取到21位置,因为一个汉字占3个字节,但是使用UTF-8编码会多写两个字节
//我们可以使用seek方法移动文件指针
read.seek(0);
int i1 = read.readInt();
System.out.println(i1);
read.seek(13); //设置文件指针的位置
String s1 = read.readUTF();
System.out.println(s1);
read.close();
}
private static void writeData() throws IOException {
//rw 可读可写
RandomAccessFile write = new RandomAccessFile("f.txt", "rw");
write.writeInt(100);
write.writeBoolean(true);
write.writeDouble(3.14);
//会多写两个字节
write.writeUTF("你好");
write.close();
}
}
模拟下载文件:
public class MyTest2 {
public static void main(String[] args) throws IOException {
//复制文件,可以暂停继续
//下载:断点下载
RandomAccessFile read = new RandomAccessFile("许巍 - 蓝莲花.mp3","rw");
File file = new File("C:\\Users\\Desktop\\许巍 - 蓝莲花.mp3");
RandomAccessFile write = new RandomAccessFile(file, "rw");
//严谨性判断,如果上次没复制完的文件,不存在了,就从头开始复制
/* if(!file.exists()||file.length()<上次记录的字节数){
//把文件指针设置为0 从头开始读
read.seek(0);
//把文件指针设置为0 从头开始写
write.seek(0);
}else{
//从配置文件中读取上次断开的位置
read.seek(3001);
//从配置文件中读取上次断开的位置
write.seek(3301);
}*/
int by=0;
int i=1;
try {
while ((by=read.read())!=-1){
//模拟一个异常,或模拟用户暂停
if(i++>3000){
System.out.println(1/0);
}
write.write(by);
}
} catch (Exception e) {
//当复制或下载文件过程当中遇到了异常或用户手动暂停,我们就把文件当前的指针位置,存到一个配置文件中,
//为了用户下次继续复制或下载时,把这个位置读取回去,从这个位置开始继续读写。
long filePointer = read.getFilePointer();
PrintWriter printWriter = new PrintWriter("point.txt");
printWriter.println(filePointer);
printWriter.flush();
printWriter.close();
e.printStackTrace();
}
}
}
序列化流和反序列化流
序列化(ObjectOutputStream)就是把对象通过流的方式存储到文件中。注意:此对象要重写Serializable 接口才能被序列化。
反序列化(ObjectInputStream)就是把文件中存储的对象以流的方式还原成对象。
像Serializable、Cloneable这样一个接口中如果没有方法,那么这样的接口我们将其称之为标记接口(用来给类打标记的,相当于猪肉身上盖个章)。
一个对象可以被序列化的前提是这个对象对应的类必须实现Serializable接口。
ObjectOutputStream(OutputStream out):创建写入指定 OutputStream 的 ObjectOutputStream。
ObjectInputStream(InputStream in):创建从指定 InputStream 读取的 ObjectInputStream。
-------------------------------------------------------
public class MyTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
writeData();
readData();
}
private static void readData() throws IOException, ClassNotFoundException {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("student.txt"));
Object obj = in.readObject();
Student stu = (Student) obj;
System.out.println(stu);
}
private static void writeData() throws IOException {
Student student = new Student("张三", 23);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student.txt"));
out.writeObject(student);
out.close();
}
}
//Student类
public class Student implements Serializable{
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
现在有一个情况,就是使用ObjectOutputStream将Student对象保存之后,如果我们修改了Student类中的参数,比如将private int age改成public int age,此时再通过ObjectInputStream读取对象,此时会出现异常。这种现象被称为黄色警告线问题。
出现这种问题的原因是在完成序列化以后,序列化文件中还存在一个标记,然后在进行反序列化的时候会验证这个标记和序列化前的标记是否一致。如果一致就正常进行反序列化;如果不一致就报错了。而现在我们把这个类做了修改,将相当于更改了标记,从而导致这两个标记不一致,因此就报错了。
那么该如何解决这个问题呢?其实只要让这个两个标记一致,就不会报错了。
要让这两个标记一致,就需要确定UID。确定UID的方法有两种:
- 下载serialVersionUID插件,再使用快捷键alt+Insert,就可以生成出来;
- 复制粘贴private static final long serialVersionUID,并赋给其一个随机的Long类型的值即可。
当对象中有我们不想进行序列化的变量时,我们可以使用transient关键字。如:transient private int age。此时再进行序列化时age不会进行序列化,读取的时候age读出的时默认值0。
如果我们要序列化多个对象,如果一个个去存储的话,那么读取时会碰到一个问题:假如我只要读取第三个对象,但是读取时必定会从第一个对象开始一个一个读取,直到第三个,这样就很不方便。
为了解决这个问题,我们再存储对象的时候可以将多个对象放入一个容器中,再把容器序列化。这样读取的时候可以根据索引读取所需要的对象。如:
public class MyTest3 {
public static void main(String[] args) throws Exception{
Student student1 = new Student("王五", 25, '男');
Student student2 = new Student("赵六", 26, '女');
Student student3 = new Student("李四", 24, '男');
ArrayList<Student> list = new ArrayList<>();
list.add(student1);
list.add(student2);
list.add(student3);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("obj.txt"));
//如果我们要序列化多个对象,我们一个个去存这个对象。我我们可以把这多个对象,放到一个容器中,把容器序列化到文件中
out.writeObject(list);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("obj.txt"));
ArrayList<Student> myList = (ArrayList) in.readObject();
Student student = myList.get(3);
System.out.println(student);
in.close();
//序列化流和反序列化流,可以进行对象的深克隆。
}
}
Properties属性集合
Properties 继承自Hashtable, 是一个双列集合,表示了一个持久的属性集,可保存在流中或从流中加载。我们经常使用这个集合来读取配置文件。
Properties 规定了键和值都是String类型,因此不能指定泛型。
特殊功能:
public Object setProperty(String key,String value)
public String getProperty(String key)
public Set<String> stringPropertyNames()
假如我们需要将一个双列集合的键值存入文本文件中,使用Properties集合最为便利。方法如下:
public class MyTest2 {
public static void main(String[] args) throws IOException {
Properties properties = new Properties();
properties.setProperty("username", "张三");
properties.setProperty("password", "123456");
properties.setProperty("email", "westos@163.com");
//我们把属性集合中的键值对数据存到配置文件中
properties.store(new FileWriter("yonghu.properties"),null);
}
}
Properties属性集合读取配置文件时,要求配置文件中的数据键和值以=拼接。一般属性配置文件后缀名会以.Properties结果。具体方法:
public class MyTest3 {
public static void main(String[] args) throws IOException {
Properties properties = new Properties();
//把配置文件中的键值对数据读取到属性集合中
properties.load(new FileInputStream("user.properties"));
System.out.println(properties);
System.out.println(properties.getProperty("username"));
String hooby = properties.getProperty("hobby");
System.out.println(hooby);
}
}
SequenceInputStream
SequenceInputStream 表示其他输入流的逻辑串联。它从输入流的有序集合开始,并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的文件末尾为止。
简单来说就是串联多个输入流。
构造方法:
SequenceInputStream(InputStream s1, InputStream s2):通过记住这两个参数来初始化新创建的 SequenceInputStream(将按顺序读取这两个参数,先读取 s1,然后读取 s2),以提供从此 SequenceInputStream 读取的字节。
SequenceInputStream(Enumeration<? extends InputStream> e):通过记住参数来初始化新创建的 SequenceInputStream,该参数必须是生成运行时类型为 InputStream 对象的 Enumeration 型参数。
构造方法1应用实例:
public class MyTest {
public static void main(String[] args) throws IOException {
/*
案例需求:
将a.txt和b.txt两个文本文件的内容合并到c.txt*/
FileInputStream in1 = new FileInputStream("user.properties");
FileInputStream in2 = new FileInputStream("yonghu.properties");
SequenceInputStream in= new SequenceInputStream(in1, in2);
FileOutputStream out = new FileOutputStream("all.properties");
int len=0;
byte[] bytes = new byte[1024];
while ((len=in.read(bytes))!=-1){
out.write(bytes,0,len);
}
in.close();//in1和in2由in维护,只需要关闭in流就可以了
out.close();
}
}
构造方法2应用实例:
public class MyTest3 {
public static void main(String[] args) throws IOException {
FileInputStream in1 = new FileInputStream("user.properties");
FileInputStream in2 = new FileInputStream("yonghu.properties");
FileInputStream in3 = new FileInputStream("yonghu2.properties");
//把多个输入流,放到集合vector中
Vector<FileInputStream> vector = new Vector<>();
vector.add(in1);
vector.add(in2);
vector.add(in3);
//获取一个迭代器
Enumeration<FileInputStream> elements = vector.elements();
//然后把迭代器传给序列流,他就会去迭代集合中的流
SequenceInputStream in = new SequenceInputStream(elements);
FileOutputStream out = new FileOutputStream("all3.properties");
int len = 0;
byte[] bytes = new byte[1024];
while ((len = in.read(bytes)) != -1) {
out.write(bytes, 0, len);
}
in.close();
out.close();
}
}