文章目录
旨在复习回顾,很多细碎的知识点请看文档:JDK8文档
一、File类
1、文件路径
正斜杠,又称左斜杠,符号是"/"
;反斜杠,也称右斜杠,符号是"\"
。
在Unix/Linux
中,路径的分隔采用正斜杠"/"
,比如"/home/hutaow"
;而在Windows
中,路径分隔采用反斜杠"",比如"C:\Windows\System"
在Java当中反斜杠代表的是转义:
比如:
制表符(也叫制表位)的功能是在不使用表格的情况下在垂直方向按列对齐文本,就是咱们的Tab键。
- '\'
将双引号转义为真正的双引号
‘\r’
(回车):即将光标回到当前行的行首(而不会换到下一行),之后的输出会把之前的输出覆盖‘\n’
换行,换到当前位置的下一位置,而不会回到行首
2、案例:递归遍历文件
构造方法、方法过多,看文档即可
【案例】列出D:\Code\image
文件夹下的所有的图片:
目录结构,aa,bb里边有一些图片和txt文件
思路:
遍历D:\Code\image
下拿到**过滤后的**路径的路径数组,数组不为空且有文件的情况下,如果是文件夹递归则进去,直到不是文件夹,然后输出文件,否则输出当前目录下的文件!
File[] | listFiles(FilenameFilter filter) | 通过过滤器过滤文件,过滤通过文件名过滤,返回文件数组 |
---|---|---|
package com.io;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
/**
* 列出D:\Code\image文件夹下的所有的图片:
*/
public class Demon01 {
public static void main(String[] args) throws IOException {
String PATH = "d:\\Code\\image";
File file = new File(PATH);// 拿到子文件对象
listAll(file);
}
// 单独列出方法获取目录下的图片(递归遍历,过滤)
public static void listAll(File parent) {
MyFilter myFilter = new MyFilter();
File[] children = parent.listFiles(myFilter);
// 过滤后 children数组中只有本次递归后的文件夹或者图片
for (int i = 0; i < children.length; i++) {
// 如果子文件是个文件夹,则递归调用
if(!children[i].isFile()){
listAll(children[i]);
} else {
System.out.println(children[i].getName());
}
}
}
// 自定义文件过滤类,实现FilenameFilter接口
static class MyFilter implements FilenameFilter {
@Override
public boolean accept(File dir, String name) {
// 注意过滤条件!!! : 这里的dir是当前的目录的file类(父路径),而不是子文件的. name是子文件的名称
// 因此要判断子文件是否存在(子文件是否为文件夹) —————— 则要进到这个文件的目录里边,而不是直接判断dir的
// 及 new File(dir, name) ===>
// File(File parent,String child) 根据指定的父路径对象和文件路径创建一个新的File对象实例
if(name.endsWith(".png") || new File(dir, name).isDirectory()){
return true;
}else {
return false;
}
}
}
}
同理:递归创建,删除文件也类似,都是递归遍历!
二、IO流分类与体系
流的分类
1、 按照流向分
- 输入流: 只能从中读取数据,而不能向其写入数据。
- 输出流:只能向其写入数据,而不能向其读取数据。
其实计算机在读取文件的时候是很麻烦的:
当然系统级别的方法调用我们可以暂时不用考虑。但是我们确确实实看到一个文件在传输过程中经历了很多次的拷贝,IO的性能本来就不是很高,所以后来又有了零拷贝、Nio等技术,这些知识点我们计划在附加课讲解。
2 、按照操作单元划分
- 字节流:是一个字节一个字节的读取或写入
- 字符流:是一个字符一个字符的读取或写入,一个字符就是两个字节,主要用来处理字符。
3、 按照角色划分
- 节点流:直接从/向一个特定的IO设备(如磁盘,网络)读/写数据的流,称为节点流。
- 处理流:“连接”在已存在的流(节点流或处理流)之上通过对数据的处理为程序提供更为强大的读写功能的流。
| 分类 | 字节输入流 |字节输出流|字符输入流|字符输出流|
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流(处理) | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
操作对象 | ObjectInputStream | ObjectOutputStream |
三、流的案例
1、继承结构
InputStream和OutputStream
Reader和Writer
2、流到底怎么使用?
(1)将一个流对象插在一个节点上:
其实通过名字我们就可以很好的理解了:FileInputStream就是怼在文件上的输入流啊!
public abstract class InputStream implements Closeable
InputStream本身是抽象类,我们需要使用它的子类去构造对象:
InputStream inputStream = new FileInputStream(file);
既然是输入流就要一点一点的往内存里读数据!
其实inputStream的方法并不多,关键在于几个read方法,管子已经插上了,接下来就是读了。
// 读一个字节
int read = inputStream.read();
// 一次性读1024个字节到那个内存数组
int read = inputStream.read(new byte[1024]);
// 从第0个字节开始读,读120个字节
int read = inputStream.read(new byte[1024],0,120);
(2)使用read()方法读取</font>
第一种读取方式:单个字节读取——效率不高!
【案例】读取D:\code.txt
package com.io;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class TestInputStream {
public static void main(String[] args) throws IOException {
// 拿到文件字节流对象
InputStream ins = new FileInputStream("d:/code.txt");
int data;
while((data = ins.read()) != -1){
System.out.println(data);
}
}
}
read就是每次读出的字节,直到-1就停止。
使用read(byte[] byte)读取
第二种读取方式:一次读取多个字节,多一个字节数组!
package com.io;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class TestInputStream {
public static void main(String[] args) throws IOException {
// 拿到文件字节流对象
InputStream ins = new FileInputStream("d:/code.txt");
byte[] bf = new byte[3];// 每次读三个字节
int cnt = 0;
while((cnt = ins.read(bf)) != -1){// cnt返回读了多少个字节
System.out.println(cnt);
System.out.println(new String(bf, 0, cnt));
}
}
}
(4)输出流的使用
输出流即写
注: 在定义文件输出流时,有两个参数,第二个如果是true代表追加文件,如果false代表覆盖文件,意思就是如果人家这个文件原来有内容,就覆盖的没了,这一点要注意。
package com.io;
import java.io.*;
public class TestOutnputStream {
public static void main(String[] args) throws IOException {
// true:追加,false覆盖
OutputStream outputStream = new FileOutputStream("d:/code.txt",false);
// 一个一个字节的写入
outputStream.write(97);// a
}
}
原来的内容被a
覆盖
(5)资源的释放
第一种:调用资源.close()
方法,记得抛出异常即可!
public static void main(String[] args) {
// 定义资源
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = new FileInputStream("D:/code/a.txt");
outputStream = new FileOutputStream("D:/code/b.txt",true);
byte[] buf = new byte[3];
int len;
while ((len =inputStream.read(buf)) != -1){
outputStream.write(buf,0,len);
}
} catch (IOException e) {
e.printStackTrace();
// 最终无论如何,都要释放资源
} finally {
// 这里判空非常的关键!!!!!!!
// 如果你是空的,你还要释放吗,本来无异常空的在释放就是错的!!!!!!!
if(inputStream != null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(outputStream != null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
第二种:
以上代码如此繁杂,jdk1.7之后,很多资源类的类都实现了AutoCloseable接口
实现了这个接口的类可以在try中定义资源,并会主动释放资源:
ublic static void main(String[] args) {
try(InputStream inputStream = new FileInputStream("D:/code/a.txt");
OutputStream outputStream= new FileOutputStream("D:/code/b.txt",true)) {
byte[] buf = new byte[3];
int len;
while ((len =inputStream.read(buf)) != -1){
outputStream.write(buf,0,len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
案例:文件拷贝
package com.io;
import java.io.*;
/**
* 文件拷贝:将d盘的code.txt复制到e盘下
* 1.先读d盘的code.txt
* 2.边度边写
* 3.最后释放资源
*/
public class TestInputStream {
public static void main(String[] args) throws IOException {
// 1.读
InputStream inputStream = new FileInputStream("d:/code.txt");
OutputStream outputStream = new FileOutputStream("e:/code.txt");
byte[] bf = new byte[1024];// 1K 1K的读
int len;
while((len = inputStream.read(bf)) != -1){
outputStream.write(bf, 0, len);
}
}
}
案例:字符流读取文件
/**
* 字符流读取文件
*/
@Test
public void testReader() throws IOException {
Reader reader = new FileReader("d:/code.txt");// 拿到文件对象
BufferedReader br = new BufferedReader(reader);// 赋予文件对象读的操作权力
String str;
while((str = br.readLine()) != null){// 一行一行的读
System.out.println(str);
}
reader.close();
br.close();
}
案例:字符流写文件
// 因为有输入,放在主函数中测试
public class TestReader {
public static void main(String[] args) throws IOException {
Writer write = new FileWriter("d:/code.txt",true);// 可追加
BufferedWriter bw = new BufferedWriter(write);
Scanner scan = new Scanner(System.in);
int tmp = 5;
while (tmp > 0){
System.out.print("请输入内容:");
String words = scan.next();
tmp --;
bw.write(words);
bw.newLine();// 换行
bw.flush();
}
}
四、序列化与反序列化
序列化与反序列化:Java中对对象进行读写操作
- 序列化:将对象写入到IO流中,说的简单一点就是将内存模型的对象变成字节数字,可以进行存储和传输。
- 反序列化:从IO流中恢复对象,将存储在磁盘或者从网络接收的数据恢复成对象模型。
- 使用场景:所有可在网络上传输的对象都必须是可序列化的,否则会出错;所有需要保存到磁盘的Java对象都必须是可序列化的。
该对象必须实现Serializable接口,才能被序列化。
1、对象序列化与反序列化
对象代码:
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实现对象的序列化————写入对象
* 要求:序列化类必须实现接口
*/
@Test
public void testObjectOuptStream() throws Exception{
//1. 获取到将要写入的文件对象,并创建序列化对象
OutputStream fos = new FileOutputStream("d:\\code.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
//2. 序列化(写入操作)
Student student = new Student("张三",19);
oos.writeObject(student);
//3. 关闭(自带flush()方法了)
oos.close();
System.out.println("序列化完毕");
}
2、序列化版本号
我们知道,反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?
Java序列化提供了一个``private static final long serialVersionUID` 的序列化版本号,只要版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
public class Student implements Serializable {
//序列化版本号
private static final long serialVersionUID = 1111013L;
private String name;
private int age;
private int money;// 修改
//省略构造方法及get,set
}
如果反序列化使用的版本号与序列化时使用的不一致,反序列化会报InvalidClassException’异常。
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级、代码的修改等因素无法正确反序列化;
不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
什么情况下需要修改serialVersionUID呢:
- 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
- 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号。
IDEA配置该操作可以看看这篇博客:(8条消息) IDEA 配置Serializable的快捷键快速生成serialVersionUID_程序之大道至简的博客-CSDN博客_idea生成serialversionuid快捷键
3、总结
- 所有需要网络传输的对象都需要实现序列化接口。
- 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
- 如果想让某个变量不被序列化,使用transient修饰。
- 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
- 反序列化时必须有序列化对象的class文件。
- 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
- 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。
五、综合案例
写一个程序,能够给一个商品文件进行增、删、改、查。
第一列是编号,第二列是商品名称,第三列是价格。
骨架代码:
import java.util.Scanner;
public class Shop {
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
while (true) {
System.out.println("请选择功能:1、插入新商品 2、删除商品 3、修改商品 4、显示所有商品 5、查询一个商品 6、退出");
int function = scanner.nextInt();
switch (function){
case 1:
insert();
break;
case 2:
delete();
break;
case 3:
update();
break;
case 4:
displayAll();
break;
case 5:
findone();
break;
case 6:
System.exit(-1);
break;
}
}
}
private static void findOne() {
System.out.println("请输入商品编号:");
int id = scanner.nextInt();
// 思路一:一行一行的读,找到为止
// 思路二:全部读到内存,内存里找
}
private static void update() {
System.out.println("请输入商品编号:");
// 全部拿出来,更新后覆盖
}
private static void delete() {
System.out.println("请输入商品编号:");
// 全部拿出来,删除后覆盖
}
private static void insert() {
System.out.println("请输入商品编号:");
// 最简单,直接追加
}
private static class Goods{
private int id;
private String name;
private int price;
public Goods() {
}
public Goods(int id, String name, int price) {
this.id = id;
this.name = name;
this.price = price;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
}
【Code】:
Shop类:
package com.test;
public class Goods{
private int id;
private String name;
private int price;
public Goods() {
}
public Goods(int id, String name, int price) {
this.id = id;
this.name = name;
this.price = price;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
@Override
public String toString() {
return "Goods{" +
"id=" + id +
", name='" + name + '\'' +
", price=" + price +
'}';
}
}
核心代码:
package com.test;
import java.io.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Scanner;
public class Shop {
static final String PATH = "d:\\code.txt";
public static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
while (true) {
System.out.println("请选择功能:1、插入新商品 2、删除商品 3、修改商品 4、显示所有商品 5、查询一个商品 6、退出");
int function = scanner.nextInt();
switch (function){
case 1:
insert();
break;
case 2:
delete();
break;
case 3:
update();
break;
case 4:
displayAll();
break;
case 5:
findOne();
break;
case 6:
System.exit(-1);
break;
}
}
}
/**
* 插入操作:直接写入磁盘即可,可以特判id(主键)是否重复维护程序的合理行
*/
/**
* 1.这里一不留意调了一天的bug:注意插入时若文本为空(没有任何数据要特批,不然死循环)————论idedebug的强大功能!!
* 2.若:刚刚开始你若没在文本时预填有任何数据(即为空),那么一定就要保证你的文本是空的(一个空格\换行都不行)————没完全清空文本,找了一天
* 不然findAllGoods()函数里就会爆异常,读的时候你以为它是空的,本应不会进入到while进行读取,其实不然(存在空白的空格符号),
* 那么就导致读文本不为空,那么就存在三个数据中为空的现在,你把空转为整形或者字符串都是错的!!!
*/
private static void insert() {
boolean flag = true;
Integer id = null;
List<Goods> allGoods = findAllGoods();
while (flag){
System.out.println("请输入你要添加的商品编号:");
id = scanner.nextInt();
// 刚刚开始可能为空
if(allGoods.size() == 0) break;
for (Goods goods : allGoods) {
System.out.println(goods.getId());
flag = (goods.getId() == id);
}
if(flag){
System.out.println("商品已经存在");
}
}
System.out.println("请输入你要添加的商品名称:");
String name = scanner.next();
System.out.println("请输入你要添加的商品的价格:");
int price = scanner.nextInt();
List<Goods> new_list = new ArrayList<>();
new_list.add(new Goods(id, name, price));
writeAll(new_list, true);
}
/**
* 删除操作:由于不能直接在磁盘中删除
* 先将磁盘的数据读出,存到链表
* 在遍历链表找到要删除的id,删掉该节点
* 然后再将经过删除后的链表写回磁盘即可
*/
private static void delete() {
List<Goods> allGoods = findAllGoods();
System.out.println("请输入你想删除的物品编号:");
int id = scanner.nextInt();
Iterator<Goods> iterator = allGoods.iterator();
while (iterator.hasNext()){
Goods goods = iterator.next();
if(goods.getId() == id){
iterator.remove();
}
}
// 写回磁盘
writeAll(allGoods, false);
}
/**
* 更新操作:根据id进行更新,可更新出id外的其他信息(同删除操作)
*/
private static void update() {
List<Goods> allGoods = findAllGoods();
System.out.println("请输入你想更新的物品编号:");
int id = scanner.nextInt();
// 读出
Iterator<Goods> iterator = allGoods.iterator();
// 更新
while (iterator.hasNext()){
Goods goods = iterator.next();
if(goods.getId() == id){
System.out.println("请输入新的物品名称:");
String name = scanner.next();
System.out.println("请输入新的物品价格:");
int price = scanner.nextInt();
goods.setName(name);
goods.setPrice(price);
}
}
// 写回磁盘
writeAll(allGoods, false);
}
/**
* 展示所有商品信息:读取所有信息,打印即可
*/
private static void displayAll() {
for (Goods good : findAllGoods()) {
System.out.println(good.getId() + " " + good.getName() + " " + good.getPrice());
}
}
/**
* 查询操作:根据id在链表中查找即可,然后输出
*/
private static void findOne() {
System.out.println("请输入你想查询的物品编号:");
int id = scanner.nextInt();
for (Goods good : findAllGoods()) {
if(good.getId() == id){
System.out.println(good.getId() + " " + good.getName() + " " + good.getPrice());
}
}
}
/**
* 将链表中的所有商品写到磁盘中(字符流来写, 一行一行的写)
* @param opt
*/
private static void writeAll(List<Goods> list, boolean opt) {
Writer writer = null;
BufferedWriter bw = null;
try {
writer = new FileWriter(Shop.PATH, opt);
bw = new BufferedWriter(writer);
for (Goods goods : list) {
bw.write(goods.getId() + " " + goods.getName() + " " + goods.getPrice());
bw.newLine();
bw.flush();
}
}catch (IOException e){
e.printStackTrace();
}finally {
try {
bw.close();
writer.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
/**
* 从磁盘中读取数据,存到链表中(字符流一行一行的读)
* @return
*/
private static List<Goods> findAllGoods() {
List<Goods> goodList = new ArrayList<>();
Reader reader = null;
BufferedReader bf = null;
try{
reader = new FileReader(Shop.PATH);
bf = new BufferedReader(reader);
String goodsStr;
while((goodsStr = bf.readLine()) != null){
String[] goodsElem = goodsStr.split(" ");
Goods goods = new Goods(
Integer.parseInt(goodsElem[0]),
goodsElem[1],
Integer.parseInt(goodsElem[2]));
goodList.add(goods);
}
}catch (IOException e){
e.printStackTrace();
}finally {
if(bf != null){// ?
try {
bf.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
return goodList;
}
}
总结:报错要善于利用Idea的debug调试工具,十分的关键!!!!