大数据学习笔记——Java篇之IO

2023-06-25,,

IO学习笔记整理

1. File类

1.1 File对象的三种创建方式:

File对象是一个抽象的概念,只有被创建出来之后,文件或文件夹才会真正存在

注意:File对象想要创建成功,它的目录必须存在!

import java.io.File;

/*
演示三种创建File对象的方式
*/
public class FileDemo {
public static void main(String[] args) throws Exception {
//指定完整路径名的字符串
File f1 = new File("e:/test/a.txt");
//指定父路径名以及子路径名的字符串
File f2 = new File("e:/test", "b.txt");
//指定父路径的File对象以及子路径名的字符串
File f3 = new File(new File("e:/test"), "c.txt"); f1.createNewFile();
f2.createNewFile();
f3.createNewFile(); }
}

如果使用的是相对路径(即不以盘符开头),那么默认的是当前项目的路径

1.2 mkdir和createNewFile方法

mkdir和createNewFile两个方法的区别,前者看成文件夹,哪怕写上了后缀,后者看成文件

import java.io.File;

/*
演示mkdir和createNewFile的区别
注:如果出现同名的文件以及文件夹,File会首先创建文件!
*/
public class FileDemo2 {
public static void main(String[] args) throws Exception {
File file1 = new File("e:/test/a.txt");
File file2 = new File("e:/test/aaa.txt");
//创建一个文本文件
file1.createNewFile();
//创建一个名为aaa.txt的文件夹
file2.mkdir();
}
}

在调用delete方法的时候要注意要删除的文件夹不能包含内容,否则就会报错

1.3 renameTo方法

renameTo方法如果在不同路径,就相当于剪切加重命名

import java.io.File;

/*
演示rename方法以及剪切的效果
*/
public class FileDemo3 {
public static void main(String[] args) {
File srcFile = new File("e:/test/a.txt");
File destFile = new File("e:/test/b.txt");
//如果源文件和目标文件拥有相同的父目录,那么就是改名操作
//srcFile.renameTo(destFile);
//如果两者的父目录不相同,那么就相当于剪切并改名
File destFile2 = new File("e:/test1/b.txt");
srcFile.renameTo(destFile2);
}
}

1.4 length方法的说明

关于length()方法,如果是文件的话返回文件的字节数,而如果是文件夹的话,是不确定的

import java.io.File;

/*
关于length方法的说明
*/
public class FileDemo4 {
public static void main(String[] args) {
//如果是一个文件的话,length方法即返回这个文件的字节数
File f1 = new File("e:/test/a.txt");
System.out.println(f1.length());
//如果是文件夹的话,返回值是不确定的,不一定是0,有时会是别的数字
File f2 = new File("e:/test");
System.out.println(f2.length());
}
}

1.5 listFiles方法

listFiles方法的优点是由于返回的是一个File类型的数组,而File本身有各种各样的方法,因此非常灵活,可以实现各种操作

import java.io.File;
import java.io.FilenameFilter; /*
演示listFiles方法,实现找到一个文件夹下一级子目录的所有txt文件,并把文件名打印出来
*/
public class FileDemo5 {
public static void main(String[] args) {
//常规方法
File f = new File("e:/test");
File[] files = f.listFiles();
for (File file : files) {
if(file.getName().endsWith(".txt")){
System.out.println(file.getName());
}
}
System.out.println("<---------------------->");
//使用FileNameFilter过滤出a.txt这个文件
//采用匿名内部类方式
File[] files1 = f.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return new File(dir,name).getName().equals("a.txt");
}
});
for (File file : files1) {
System.out.println(file.getName());
}
}
}

1.6 递归操作(查找以及删除)

递归地查找某一类文件

import java.io.File;

/*
演示递归地查找jpg文件并将文件名打印出来
*/
public class FileRecursiveDemo1 {
public static void main(String[] args) {
findJPG(new File("e:"));
}
public static void findJPG(File file){
File[] files = file.listFiles();
for (File f : files) {
if(f.isDirectory()){
findJPG(f);
}
else{
if(f != null && f.getName().toUpperCase().endsWith("JPG")){
System.out.println(f.getName());
}
}
}
}
}

递归删除文件夹

import java.io.File;

/*
演示递归删除文件夹以及文件夹下所有内容
*/
public class FileRecursiceDemo2 {
public static void main(String[] args) {
deleteDir(new File("e:/test"));
}
public static void deleteDir(File f){
File[] files = f.listFiles();
for (File file : files) {
if(file.isDirectory()){
deleteDir(file);
}else{
file.delete();
}
}
//文件都删除干净后,最后再把文件夹删除即可
f.delete();
}
}

2. 输入流和输出流

2.1 java IO框架中各种流的继承及实现图:

2.2 带异常处理的拷贝程序

基本思路:在try块中执行基本业务逻辑,catch中捕捉异常,记住使用Exception兜底,最后将关流操作放在finally中执行

import java.io.*;

/*
演示带完整异常处理的文件拷贝程序
*/
public class CopyFileDemo {
public static void main(String[] args) {
BufferedReader br = null;
BufferedWriter bw = null;
try {
br = new BufferedReader(new FileReader("e:/test/a.txt"));
bw = new BufferedWriter(new FileWriter("e:/test/b.txt"));
String line = null;
while((line = br.readLine()) != null){
bw.write(line);
bw.newLine();
}
} catch (FileNotFoundException e) {
System.out.println("File Not Found !!!");
} catch (IOException e){
System.out.println("IO Exception !!!");
} catch (Exception e){
System.out.println("Unknown Exception !!!");
}finally {
if(br != null){
try {
br.close();
} catch (IOException e) {
}
}
if(bw != null){
try {
bw.close();
} catch (IOException e) {
}
}
}
}
}

新版try-catch语句,可以将需要关闭的流使用try(){}形式放在小括号内,这样就不用再写finally块就能自动关流了

import java.io.*;

/*
演示带完整异常处理的文件拷贝程序升级版
*/
public class CopyFileDemo2 {
public static void main(String[] args) {
try (
BufferedReader br = new BufferedReader(new FileReader("e:/test/a.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("e:/test/b.txt"));
){
String line = null;
while((line = br.readLine()) != null){
bw.write(line);
bw.newLine(); }
} catch (FileNotFoundException e) {
System.out.println("File Not Found !!!");
} catch (IOException e){
System.out.println("IO Exception !!!");
} catch (Exception e){
System.out.println("Unknown Exception");
}
}
}

2.3 三种方式触发流的关闭

import java.io.BufferedOutputStream;
import java.io.FileOutputStream; /*
演示三种方式触发流的关闭
*/
public class StreamCloseDemo {
public static void main(String[] args) throws Exception {
//test1();
//test2();
test3();
} public static void test1() throws Exception {
//使用close方法关闭流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("e:/test/a.txt"));
bos.write("hello world".getBytes());
bos.close();
}
public static void test2() throws Exception{
//手动刷新
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("e:/test/a.txt"));
bos.write("hello world".getBytes());
bos.flush();
}
public static void test3() throws Exception{
//当写出的字节数大于等于8192的时候,就会自动触发关流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("e:/test/a.txt"));
bos.write(new byte[8192]);
}
}

对于第三种方式,对比图如下:

当写出的字节数为8191的时候,文件字节大小为0字节,说明并未关流

而当写出字节数为8192的时候,文件字节正好为8KB,说明写出成功!

2.4 数据

由于后期在用到hadoop中的时候会涉及到ComparableWritable接口的重写方面的知识,因此这里需要关注,特别关注writeInt方法的底层实现!可以查看这些方法的底层源码!

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream; /*
演示数据流的基本方法:
1.数据流具有丰富的API可以读写各种各样的数据类型而不用每次都调用getBytes()方法
2.注意,若用数据流将数据写出到文本文件,则会出现乱码,解决方案是再用read方法读回来
*/
public class DataInputOutputStreamDemo {
public static void main(String[] args) throws Exception {
DataOutputStream dos = new DataOutputStream(new FileOutputStream("e:/test/a.txt"));
dos.writeBoolean(true);
dos.writeByte(97);
dos.writeChar('b');
dos.writeInt(10);
dos.writeDouble(3.14);
dos.writeUTF("hello world");
//然后用相对应的方法读回来
DataInputStream dis = new DataInputStream(new FileInputStream("e:/test/a.txt"));
System.out.println(dis.readBoolean());
System.out.println(dis.readByte());
System.out.println(dis.readChar());
System.out.println(dis.readInt());
System.out.println(dis.readDouble());
System.out.println(dis.readUTF());
}
}

WriteInt和ReadInt的源码分析:

  // writeInt
    public final void writeInt(int v) throws IOException {
out.write((v >>> 24) & 0xFF);
out.write((v >>> 16) & 0xFF);
out.write((v >>> 8) & 0xFF);
out.write((v >>> 0) & 0xFF);
incCount(4);
}

查看了writeInt方法的实现原理,可知,它底层是通过将一个int类型的数据通过无符号右移操作从高位到低位一个一个拿出8位来实现的,而与0xFF进行与运算则保证了,除了这8位,其他位全部都是0,而readInt()方法则正好相反,将所有的获得的4个数字再进行左移操作,再加到一起去即可,具体代码如下:

public final int readInt() throws IOException {
int ch1 = in.read();
int ch2 = in.read();
int ch3 = in.read();
int ch4 = in.read();
if ((ch1 | ch2 | ch3 | ch4) < 0)
throw new EOFException();
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
}

知道了底层原理之后,自己也可以写字节数组和int值相互转换的方法:

/*
写两个将字节数组与int值相互转换的方法
*/
public class MyUtils { public static void main(String[] args) {
//进行测试
int b = -256;
byte[] bytes = int2Bytes(b);
System.out.println(bytes2Int(bytes));
} public static byte[] int2Bytes(int i){
byte[] bytes = new byte[4];
bytes[0] = (byte) (i >> 24);
bytes[1] = (byte) (i >> 16);
bytes[2] = (byte) (i >> 8);
bytes[3] = (byte) (i >> 0);
return bytes;
} public static int bytes2Int(byte[] bytes){
int i1 = bytes[0] & 0xFF << 24;
int i2 = bytes[1] & 0xFF << 16;
int i3 = bytes[2] & 0xFF << 8;
int i4 = bytes[3] & 0xFF << 0;
return i1 + i2 + i3 + i4;
}
}

2.5 打印流

打印流是单向的,只有输出方法,没有读取方法,但是有其特殊的打印方法,并且经过设置可以实现自动关流

import java.io.FileOutputStream;
import java.io.PrintWriter; /*
演示打印流的基本操作
*/
public class PrintWriterDemo {
public static void main(String[] args) throws Exception {
//设置成可以自动关流的那种,根据文档可知,自动关流只对println等方法有效
PrintWriter pw = new PrintWriter(new FileOutputStream("e:/test/a.txt"),true);
//使用自带的特殊方法
pw.println(10);
pw.println(3.14);
pw.println("hello world");
}
}

2.6 内存流

本质上不涉及到文件的IO,是用数组来实现的,写的时候会写出到一个无名的数组,当调用toByteArray方法时可以返回字节数组

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; /*
演示内存流的基本方法
*/
public class ByteArrayInputOutputStreamDemo {
public static void main(String[] args) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write("hello world".getBytes());
byte[] bytes = baos.toByteArray();
System.out.println(new String(bytes));
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
int len = 0;
byte[] buf = new byte[1024];
len = bais.read(buf);
System.out.println(new String(buf,0,len)); }
}

2.7 RandomAccessFile(随机访问文件)

import java.io.RandomAccessFile;

/*
演示随机访问文件类:
1.该类相当于是一个巨大的byte数组,即可以读文件又可以写文件
2.具有seek方法可以随便移动指针
3.有只读模式以及正常模式
*/
public class RandomAccessFileDemo {
public static void main(String[] args) throws Exception {
//指定的模式使得它既可以读,又可以写
RandomAccessFile raf = new RandomAccessFile("e:/test/a.txt", "rw");
raf.writeByte(98);
raf.writeLong(100L);
raf.writeUTF("hello world");
//试验:将指针移动至第9个字节处,查看是否可以读到hello world字符串
raf.seek(9L);
System.out.println(raf.readUTF());
}
}

2.8 Properties类(配置文件)

此类并不是IO,但是和IO关系很紧密,查看源码可知它是继承自HashTable的,因此可以把它当成map使用

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Properties; /*
演示一个简单的配置文件的读取以及更新等操作
*/
public class PropertiesDemo {
public static void main(String[] args) throws Exception {
//先用一个输入流读取已经存在了的配置文件
FileInputStream fis = new FileInputStream("e:/test/prop.txt");
Properties prop = new Properties();
prop.load(fis);
System.out.println(prop.getProperty("name"));
//进行更新操作,如果不存在,那就是添加
prop.setProperty("id","0001");
//更新的内容还是输出到原来文件中去
//如果不想写上comments的话,可以写null,但是绝对不可以出现中文,否则就会出现乱码
prop.store(new FileOutputStream("e:/test/prop.txt"),null);
}
}

2.9 对象输出 / 输入流 (ObjectOutputStream / ObjectInputStream)

对象输出流使得对象,这样一个具有立体结构类型的数据能够永久化地保存在磁盘上去,(这一过程就叫做持久化,在之后的大数据学习过程中会反复接触到),这样就使得对象可以脱离程序而存在,该过程就叫做“序列化”,而"反序列化“则正好相反,将一个已经保存在磁盘上的对象再反过来读到内存中

想要序列化一个对象,必须具备这样一个前提,那就是该对象要实现Serializable接口,该接口十分特殊,方法体内没有任何代码,是一个标记性接口,相当于在告诉用户,如果对象要想序列化,就得实现这个接口,不实现它,那就不能用!

public interface Serializable {
}

判断末尾的方法:之前的都要一个结束符,-1或是null,而Object却没有,因此,判断结束条件的方法不一样

需要注意的是:对象流和之前接触到的流判断是否达到文件末尾的方法不一样,由于对象流在写出到文件时,并不会在末尾添加-1或null这样的结束符,因此通过原来的方式判断是否达到文件末尾将不再适用,会抛出EOFException,解决方案是将所有的对象全部放在一个集合中,在反序列化时一次性读取整个集合即可,演示代码如下所示:

/*
演示对象流
演示解决EOFException的方案
*/ import java.io.*;
import java.util.ArrayList; class Employee implements Serializable {
private int id;
private String name; public Employee() {
} public Employee(int id, String name) {
this.id = id;
this.name = name;
} 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;
} @Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
} public class ObjectStreamDemo {
public static void main(String[] args) throws Exception {
//serialize();
deSerialize();
}
//序列化方法
public static void serialize() throws Exception {
//创建100个员工对象将它们放到集合中并持久化保存起来
ArrayList<Object> list = new ArrayList<>();
for(int i = 1; i <= 100; i++){
Employee emp = new Employee();
emp.setId(i);
emp.setName("tom" + i);
list.add(emp);
}
//对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("e:/test/oos.txt"));
//使用该输出流特有的方法writeObject()
oos.writeObject(list);
oos.close();
}
//反序列化方法
public static void deSerialize() throws Exception{
//一次性读取一整个集合,这样就避免了EOFException异常的问题
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:/test/oos.txt"));
Object obj = ois.readObject();
ArrayList<Object> list = (ArrayList<Object>) obj;
for (Object o : list) {
System.out.println((Employee) o);
}
}
}

3. IO的应用

3.1 切割小文件 & 合并小文件

  /**
* 给定需要进行切割的大文件的文件路径,需要存放小文件的目标文件夹,以及每个小文件的大小
*
* @param srcFilePath 源文件的路径
* @param destDir 目标文件夹
* @param size 每个小文件的大小
*/
public static void splitFile(String srcFilePath, String destDir, long size){
//先进行判断,只有源文件的大小大于等于小文件大小时,才进行切割
File srcFile = new File(srcFilePath);
long srcFileLen = srcFile.length();
if(srcFileLen < size){
System.out.println("Small file's size is larger than source file's size!");
}else{
//判断能够把文件切割成几份
int count = (int) (srcFileLen % size == 0 ? srcFileLen / size : srcFileLen / size + 1);
//遍历count变量,如果是最后一个小文件的话,重新计算文件长度
long len = size;
for(int i = 0; i < count; i++){
if(srcFileLen % size != 0 && i == count - 1){
len = srcFileLen % size;
}
copyFile(srcFile,destDir,i,i * size,len);
}
}
} /**
* 将切割好的文件进行合并,合并到另一个目录下
*
* @param srcDir 源文件夹
* @param destDir 目标文件夹
*/
public static void mergeFile(String srcFilePath, String srcDir, String destDir){
String srcFileName = new File(srcFilePath).getName();
FileOutputStream fos = null;
try {
fos = new FileOutputStream(new File(destDir,srcFileName));
File[] files = new File(srcDir).listFiles();
for (File file : files) {
FileInputStream fis = new FileInputStream(file);
byte[] buf = new byte[1024 * 8];
int len = 0;
while((len = fis.read(buf)) != -1){
fos.write(buf,0,len);
}
fis.close();
} } catch (FileNotFoundException e) {
System.out.println("File Not Found!");
} catch (Exception e){
System.out.println("Unknown Exception!");
} finally {
if(fos != null){
try {
fos.close();
} catch (IOException e) {
}
}
}
} /**
*
* 单独将拷贝文件方法抽取出来
*
* @param srcFile 源文件
* @param destDir 目标文件夹
* @param i 小文件索引
* @param offset 被切割文件的偏移量,即从哪个字节开始切割
* @param len 需要拷贝的小文件的长度
*/
public static void copyFile(File srcFile, String destDir, int i, long offset, long len){
RandomAccessFile raf = null;
FileOutputStream fos = null;
try {
raf = new RandomAccessFile(srcFile,"rw");
raf.seek(offset);
byte[] buf = new byte[(int) len];
//一次性把数组读满
raf.read(buf);
fos = new FileOutputStream(new File(destDir, srcFile.getName() + "_" + i));
fos.write(buf); } catch (FileNotFoundException e) {
System.out.println("File Not Found!");
} catch (Exception e){
System.out.println("Unknown Exception!");
} finally {
if(raf != null){
try {
raf.close();
} catch (IOException e) {
}
}
if(fos != null){
try {
fos.close();
} catch (IOException e) {
}
}
}
}
}

3.2 归档文件 & 解档文件

简单说明:文件归档过后若想要解档,则必须要知道文件名是什么,以及文件长度是多少,因此,在进行归档操作时,必须写入这些控制字符,然后在解档时先对这些控制字符进行解析,解析完之后才能真正地进行文件的写出

/**
* 指定源文件夹,将文件夹下的所有文件夹归档至目标文件夹去
*
* @param srcDir 源文件夹
* @param destDir 目标文件夹
*/
public static void archiveFile(String srcDir, String destDir){
FileOutputStream fos = null;
try {
fos = new FileOutputStream(new File(destDir, "archive.dat"));
//列出这个文件夹下的文件
File[] files = new File(srcDir).listFiles();
for (File file : files) {
if(file.isFile()){
//先写文件名的长度,一个字节存放足够
int fileNameLen = file.getName().length();
fos.write(fileNameLen);
//然后写真实的文件名
fos.write(file.getName().getBytes());
//写四个字节的文件内容长度,自己手写一个工具类
fos.write(int2Bytes((int) file.length()));
//开始正式拷贝真实的文件
FileInputStream fis = new FileInputStream(file);
byte[] buf = new byte[1024 * 8];
int len = 0;
while((len = fis.read(buf)) != -1){
fos.write(buf,0,len);
}
fis.close();
}
}
} catch (FileNotFoundException e) {
System.out.println("File Not Found!");
} catch (Exception e){
System.out.println("Unknown Exception!");
} finally {
//关闭资源
if(fos != null){
try {
fos.close();
} catch (IOException e) {
}
}
}
} /**
* 将归好档的文件进行解档操作至目标文件夹中去
*
* @param srcDir 已经归好档的文件所在的文件夹
* @param destDir 需要解档到的目标文件夹
*
*/
public static void unarchiveFile(String srcDir, String destDir) {
FileInputStream fis = null;
try {
fis = new FileInputStream(new File(srcDir,"archive.dat"));
//先用read方法读一个字节读出文件名长度的字节
int b = 0;
while((b = fis.read()) != -1){
byte[] fileNameBytes = new byte[b];
//读取b长度字节数的文件名数组
fis.read(fileNameBytes);
//解析出文件名
String fileName = new String(fileNameBytes);
//读取4个字节的byte数组并将其还原回int值表示文件的真实长度
byte[] fileLenBytes = new byte[4];
fis.read(fileLenBytes);
int fileLen = bytes2Int(fileLenBytes);
//读取fileLen长度的文件真实数据
byte[] fileBytes = new byte[fileLen];
fis.read(fileBytes);
FileOutputStream fos = new FileOutputStream(new File(destDir, fileName));
fos.write(fileBytes);
fos.close();
}
} catch (FileNotFoundException e) {
//System.out.println("File Not Found!");
} catch (Exception e){
System.out.println("Unknown Exception!");
} finally {
if(fis != null){
try {
fis.close();
} catch (IOException e) {
}
}
}
} /**
* 将一个整型数转换成字节数组
*
* @param i 传入的int值
*/
public static byte[] int2Bytes(int i){
byte[] bytes = new byte[4];
bytes[0] = (byte) (i >> 24);
bytes[1] = (byte) (i >> 16);
bytes[2] = (byte) (i >> 8);
bytes[3] = (byte) (i >> 0);
return bytes;
} /**
* 将一个字节数组还原成int值
*
* @param bytes 长度为4的字节数组
*
*/
public static int bytes2Int(byte[] bytes){
int i1 = (bytes[0] & 255) << 24;
int i2 = (bytes[1] & 255) << 16;
int i3 = (bytes[2] & 255) << 8;
int i4 = (bytes[3] & 255) << 0;
return i1 + i2 + i3 + i4;
}

大数据学习笔记——Java篇之IO的相关教程结束。

《大数据学习笔记——Java篇之IO.doc》

下载本文的Word格式文档,以方便收藏与打印。