一、文件的编码
//获取字符串编码
public static void main(String[] args) {
String s="慕课ABC"; //UTF-8
//把字符串转换为字节数组(十进制)
byte[] byte1=s.getBytes(); //参数可以指定编码,不指定就是项目默认编码
byte[] byte2=s.getBytes("gbk"); //中文两个字节,英文一个字节
byte[] byte3=s.getBytes("utf-16be"); //中文、英文都是两个字节
for (byte b:byte1
) {
//把字节(转换为了int)转换为十六进制
//这里的结果只取了int(32位)的最后八位(一字节)
System.out.print(Integer.toHexString(b & 0xff)+" ");
}
}
关于 byte[] b & 0xff:
第一次遇到这个,也是有点懵,就记录一下。
先说一下
Integer.toHexString()
,这个方法是将int数转换为16进制数,而int是四个字节的,也就是说它的参数得是int类型的数然后是
0xff
,这是一个16进制数,但是它的类型为int,也就是四字节b & 0xff
也就是一个8位 按位与 32位的数,所以8位的数需要补上24个0
最终的计算结果也就是一个32位 四字节 的int类型数了,所以能被Integer.toHexString()
接受
知道了流程,那么为什么能取到最后八位呢?
对于正数来说都是一样的,主要是负数的结果:
计算机中负数是用补码表示的,那么补得24个0,经过补码结果就变成了24个1了
-118 (补上24个0后)
原码: 00000000 00000000 00000000 10001010
补码:11111111 11111111 11111111 11110110
那么在和 0xff 按位与之后
11111111 11111111 11111111 11110110 (补码)
&
00000000 00000000 00000000 11111111 (补码)
=
00000000 00000000 00000000 11110110 (补码)
可以看到最后八位和前面的数一致,因此对于负数来说,相当于只取了最后八位
以下是慕课ABC
的字节数列(我使用了UTF-8编码,所以汉字是三个字节)
//将字节数列转换为字符串
String s="慕课ABC"; //默认UTF-8
byte[] byte1=s.getBytes();
//将字节数列转换为字符串时,不指定编码就使用项目默认编码
String s1=new String(byte1,"gbk"); //这样就会乱码
System.out.println(s1);
二、File类常用API介绍
java.io.File类 用于表示文件(目录)
File类只能用于表示文件(目录)的信息(名称、大小等),不能用于文件的访问
public static void main(String[] args) throws Exception {
File file = new File("C:\\Users\\shitian\\Desktop\\作业");
if(!file.exists()){
file.mkdir(); //创建目录
}
else{
file.delete();
}
//是否是一个文件
System.out.println(file.isFile());
//是否是一个目录,是的话返回true,不是目录或者目录不存在返回false
System.out.println(file.isDirectory());
}
File file1 = new File("C:\\Users\\shitian\\Desktop\\作业\\1.txt");
if(!file1.exists()){
file1.createNewFile(); //创建文件
}
else{
file1.delete();
}
File file2 = new File("C:\\Users\\shitian\\Desktop","作业\\1.txt"); //构造函数的另一种用法
if(!file2.exists()){
file2.createNewFile();
}
else{
file2.delete();
}
File file = new File("C:\\Users\\shitian\\Desktop\\作业\\1.txt");
System.out.println(file); //直接输出完整路径 C:\Users\shitian\Desktop\作业\1.txt
System.out.println(file.getName()); //输出当前文件名称 1.txt
System.out.println(file.getParent()); //输出父目录 C:\Users\shitian\Desktop\作业
三、遍历目录
//工具类 Test.java
public class Test {
//一些File类的常用操作,比如过滤、遍历等
public static void listDirectory(File dir) throws Exception{
if(!dir.exists()){
throw new IllegalArgumentException("目录:"+dir+"不存在");
}
if(!dir.isDirectory()){
throw new IllegalArgumentException(dir+" 不是目录");
}
String[] string=dir.list(); //list()方法 返回字符串数组(列出当前目录下的子目录和文件)
for (String string1:string
) {
System.out.println(string1);
}
}
/**
* File类提供的返回File对象的方法 listFiles() ,用于输出子目录中的文件
* @param dir
*/
public static void listAllDirdectory(File dir){
File[] file = dir.listFiles(); //返回目录中所有文件或目录的File对象
for (File file1: file
) {
System.out.println(file1.getName()); //一个File对象输出的是完整路径,所以使用getName()
if(file1.isDirectory()){
listAllDirdectory(file1); //递归调用
}
}
}
}
//ClassDemo1.java
public class ClassDemo1 {
public static void main(String[] args) throws Exception {
Test.listDirectory(new File("C:\\Users\\shitian\\Desktop\\作业")); //就能输出
}
}
四、RandomAccessFile
java提供的的对文件内容的访问,既可以读文件,也可以写文件
并且支持随机访问文件,可以访问文件的任意位置
(1)java文件模型:
在硬盘上的文件是 byte byte byte存储的,是数据的集合
(2)打开文件:
有两种模式 “rw”读写 “r”只读
RandomAccessFile raf = new RandomAccessFile(file,”rw”);
方法内部存在文件指针,打开文件时指针在开头 pointer = 0;
(3)读方法
int b = raf.read(); —> 只读一个字节,然后转换为int
(4)写方法
raf.write(int); —> 只写一个字节(后8位),同时指针指向下一个位置,准备再次写入
(5)文件读写完成后一定要关闭,否则会出现意想不到的错误。(Oracle官方说明)
public static void main(String[] args) throws Exception{
File file = new File("file");
if(!file.exists()){
file.mkdir();
}
File file2 = new File(file,"file2.dat");
if(!file2.exists())
file2.createNewFile();
RandomAccessFile raf = new RandomAccessFile(file2,"rw");
//指针的位置
System.out.println(raf.getFilePointer());
raf.write('A'); //java中 char是两个字节
System.out.println("写入A时指针的位置: "+raf.getFilePointer());
raf.write('B');
System.out.println("写入B时指针的位置: "+raf.getFilePointer());
int i = 0x7fffffff;
//write方法每次只能写一个字节,所以需要写入四次才能写入一个int
raf.write(i >>> 24);
raf.write(i >>> 16);
raf.write(i >>> 8);
raf.write(i);
System.out.println(raf.getFilePointer());
String s = "一";
byte[] byte1 = s.getBytes("gbk");
raf.write(byte1); //可以直接写入一个数组
System.out.println(raf.getFilePointer());
System.out.println(raf.length()); //每次打开文件,指针总在开头,所以写入覆盖前面的内容,而不会覆盖后面的内容
//读文件,将指针移到头部
raf.seek(0);
//一次性读取,把文件中的内容都读到字节数组中
byte[] buf = new byte[(int)raf.length()];
raf.read(buf); //默认只读一个字节,带上一个字节数组参数,可以都写入数组
System.out.println(Arrays.toString(buf)); //将数组变为字符串
String string = new String(buf,"gbk"); //原来写入怎么编码的,输出就怎么编码(但是由于是对整个数组进行编码,原来的中文可能会变成乱码)
System.out.println(string);
for (byte byte2: buf
) {
System.out.println(Integer.toHexString(byte2 & 0xff)); //获取int后8位
}
raf.close();
}
注意:打开文件时,指针都位于起始位置0,写入时都会覆盖原来的内容
五、字节流和字符流
1、字节流
1)InputStream、OutputStream
InputStream抽象了应用程序读取数据的方式
OutputStream抽象了应用程序写出数据的方式
2)EOF = End 读到-1就是结尾
3)输入流基本方法
int b = in.read(); 读取一个字节,无符号填充到int的低八位,其余补0,-1是EOF
in.read(byte[] buf); 读取数据,直接填充到一个字节数组中
in.read(byte[] buf, int start, int size); 读取数据到字节数组buf, 从buf的start的位置存放size长度的字节
4)输出流基本方法
out.write(int b); 写出一个byte到流,写的是b的低八位
out.write(byte[] buf); 将buf字节数组都写入到流
out.write(byte[] buf, int start, int size); 字节数组buf,从buf的start位置开始写入size长度的字节到流
5)FileInputStream —> 具体实现了在文件上读取数据
/**
*工具类
*读取指定文件内容,按照16进制输出到控制台
* 并且每输出十个byte就换行
* @param FileName
* @throws Exception
*/
public static void printHex(String FileName) throws Exception{
//把文件作为字节流进行读操作
FileInputStream in = new FileInputStream(FileName);
int b;
int i=1;
while((b=in.read())!=-1){
if(b<=0xf){
//少于两位16进制前面补0
System.out.print('0');
}
System.out.print(Integer.toHexString(b)+" "); //将整型b转换为16进制字符串
if(i++%10==0){
System.out.println();
}
}
in.close();
}
public static void printHexByByteArray(String FileName)throws IOException{
FileInputStream in = new FileInputStream(FileName);
byte[] buf = new byte[20*1024];
//从in中批量读取字节,放到buf字节数组中,从第0个位置,最多读取buf长度的字节数
//返回的是读到的字节数,因为字节数组有可能读不满
int bytes = in.read(buf,0,buf.length);
int j=1;
for(int i=0; i<bytes; i++){
if(buf[i]<= 0xf && buf[i]>=0){ //有可能遇到负数
System.out.print("0");
}
System.out.print(Integer.toHexString(buf[i] & 0xff)+" ");
if(j++%10==0){
System.out.println();
}
}
in.close();
}
6)FileOutputStream 实现了向文件中写入byte数据的方法
public static void main(String[] args) throws Exception {
//如果文件不存在,则直接创建,如果存在,删除后创建
//存在一个boolean append 参数,默认为false,若设置为true,则文件若存在,则在其中追加内容
FileOutputStream out = new FileOutputStream("file/file2.dat");
out.write('A'); //写出了A的低八位
out.write('B');
int a = 10; //write()一次只能写入一个字节,需要写四次
out.write(a >>> 24);
out.write(a >>> 16);
out.write(a >>> 8);
out.write(a);
byte[] gbk = "中国".getBytes("gbk");
out.write(gbk);
out.close();
printHex("file/file2.dat");
}
public static void copyFile(File srcFile,File destFile)throws IOException{
if(!srcFile.exists()){
throw new IllegalArgumentException("文件:"+srcFile+"不存在");
}
if(!destFile.isFile()){
throw new IllegalArgumentException(destFile+"不是文件");
}
FileInputStream in = new FileInputStream(srcFile);
FileOutputStream out = new FileOutputStream(destFile);
byte[] bytes = new byte[8*1024];
int b;
//批量读取到字节数组bytes,读完返回读取字节的个数
/**
* 这里一个问题:while不是死循环吗?b是一个不为-1的数
* 解答:在while体中,read读到结尾时,还是会返回-1
*/
while((b=in.read(bytes,0,bytes.length))!=-1){
out.write(bytes,0,b);
out.flush(); //最好加上
}
out.close();
in.close();
}
7)DataOutputStream/DataInputStream
对“流”的扩展,可以更加方面的读取 int、long、字符等类型数据
DataOutputStream
writeInt()/writeDouble()/writeUTF()
DataInputStream
readInut()/readUTF()/readDouble()
public static void main(String[] args) throws Exception {
DataOutputStream dos = new DataOutputStream(new FileOutputStream("file/file2.dat"));
dos.writeInt(10);
dos.writeInt(-10);
//采用UTF-8编码写出,三个字节
dos.writeUTF("中国");
//采用UTF-16编码写出,两个字节
dos.writeChars("中国");
dos.writeChar('a');
dos.close();
}
public static void main(String[] args) throws Exception {
String file = "file/file2.dat";
Test.printHex(file);
DataInputStream dis = new DataInputStream(new FileInputStream(file));
int i = dis.readInt();
System.out.println(i);
i = dis.readInt();
System.out.println(i);
String s1=dis.readUTF();
System.out.println(s1);
//读取两个字节,因为java是双字节编码
//前面使用UTF-16写入中文(双字节),readChar()也就能读取一个中文(两个字节)
char c = dis.readChar();
System.out.println(c);
c=dis.readChar();
System.out.println(c);
c=dis.readChar();
System.out.println(c);
dis.close();
}
8)BufferedInputStream/BufferedOutputStream
这两个流为IO提供了带缓冲区的操作,一般打开文件进行写入或读取操作时,都会加上缓冲,这种流模式提高了IO的性能
从应用程序把输入方法文件中,相当于将一缸水转移到另外一缸水:
FileOutputStream —> write()方法 相当于一滴一滴的把水“转移”过去
DataOutputStream —> writeXxx()方法 相当于一瓢一瓢的把水“转移”过去,会方便一点
BufferedOutputStream —> write()方法 更方便,相当于一瓢一瓢的把水先放入一个桶中(缓冲区),然后再从桶中倒入到缸中
public static void copyFileByBuffer(File srcFile,File destFile)throws IOException{
if(!srcFile.exists()){
throw new IllegalArgumentException("文件:"+srcFile+"不存在");
}
if(!destFile.isFile()){
throw new IllegalArgumentException(destFile+"不是文件");
}
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));
long start = System.currentTimeMillis();
int c;
while((c=bis.read())!=-1){
bos.write(c);
bos.flush(); //刷新缓冲区
}
long end = System.currentTimeMillis();
System.out.println(end-start);
bis.close();
bos.close();
}
9)最后比较了三种方式读取文件的速度
- 利用FileInputStream/FileOutputStream,实现单字节复制文件
- 利用BufferedInputStream/BufferedOutputStream,实现利用缓冲区复制文件
- 利用FileInputStream/FileOutputStream,实现字节批量复制文件(先放到字节数组)
public static void main(String[] args) throws Exception {
//缓冲区复制文件需要 12680毫秒
Test.copyFileByBuffer(new File("C:\\Users\\shitian\\Desktop\\作业\\创建于管理数据库.pptx"),new File("C:\\Users\\shitian\\Desktop\\作业\\1.pptx"));
//单字节复制文件需要 13365毫秒
Test.copyFileByByte(new File("C:\\Users\\shitian\\Desktop\\作业\\创建于管理数据库.pptx"),new File("C:\\Users\\shitian\\Desktop\\作业\\2.pptx"));
//批量复制文件只需要 8毫秒
Test.copyFile(new File("C:\\Users\\shitian\\Desktop\\作业\\创建于管理数据库.pptx"),new File("C:\\Users\\shitian\\Desktop\\作业\\3.pptx"));
单字节传输 < 缓冲区传输 < 批量传输
“不过这差的也太多了qwq”
2、字符流
1)编码问题
2)认识文本和文本文件
java的文本(char)是16位无符号整数,是字符的Unicode编码(双字节编码)
文件是byte byte byte … 的数据序列
文本文件是文本(char)序列按照某种编码方案(utf-8、utf-16be、gbk)序列化为byte的存储结果
3)字符流(Reader Writer)
操作的大部分是文本文件
字符的处理,一次处理一个字符
字符的底层仍然是基本的字节序列
字符流的基本实现:
InputStreamReader 完成byte流解析为char流,按照编码解析
OutputStreamWriter 提供char流到byte流,按照编码处理
/**
* 字符流,批量读取、写入数据
* @throws IOException
*/
public static void charReader()throws IOException{
FileInputStream fis = new FileInputStream("file/file.dat");
InputStreamReader isr = new InputStreamReader(fis); //可以设置编码,默认项目编码
FileOutputStream out = new FileOutputStream("file/file2.dat");
OutputStreamWriter osw = new OutputStreamWriter(out);
// int c;
// while((c=isr.read())!=-1){ //单字符读取
// System.out.print((char) c);
// }
char[] buffer = new char[8*1024];
int c;
//批量读取,放入buffer字符数组,从第0个位置开始放,最多放.length个
//返回读到字符的个数
while((c=isr.read(buffer,0,buffer.length))!=-1){
String s = new String(buffer,0,c);
System.out.print(s);
osw.write(buffer,0,c);
osw.flush();
}
isr.close();
osw.close();
}
4)字符流(FileWriter FileReader)
public static void charFileReader()throws IOException{
FileReader fr = new FileReader("file/file.dat");
FileWriter fw = new FileWriter("file/file2.dat");//不提供编码参数,不同编码使用StreamWriter
//想要追加写入文件内容,添加一个boolean参数,true为追加,默认为false
//FileWriter fw = new FileWriter("file/file.dat",true);
char[] buffer =new char[8*1024];
int c;
while((c=fr.read(buffer,0,buffer.length))!=-1){
String s= new String(buffer,0,c);
System.out.print(s);
fw.write(buffer,0,c);
fw.flush();
}
fw.close();
fr.close();
}
5)字符流的过滤器
对字符流加过滤,使得字符流有更强大的功能
- BufferedReader—> 一次读一行
- BufferedWriter/PrintWriter —> 写一行
public static void charBufferedReader()throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("file/file.dat")));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("file/file2.dat")));
//PrintWriter pw = new PrintWriter("file/file.dat"); //使用相对简单
String line;
while((line=br.readLine())!=null){
System.out.println(line);//一次读一行不识别换行符(需要我们主动换行)
bw.write(line);//.append()方法追加内容
bw.newLine(); //读一行后换行
bw.flush();
//pw.println(line); //可以直接换行
//pw.flush();
}
bw.close();
br.close();
//pw.close();
}
六、反序列化
1、反序列化的基本操作
对象的序列化就是将Object转换为byte序列,反之叫对象的反序列化
1)序列化流(ObjectOutputStream),是过滤流 —— WriteObject
反序列化流(ObjectInputStream)—— ReadObject
2)序列化接口(Serializable)
对象必须实现序列化接口,才能进行序列化,否则会出现异常
这个接口没有任何方法,只是一个标准
3)transient 关键字,修饰一个元素之后,该元素不会进行jvm默认的序列化,但是可以自己完成这个元素的序列化
以及两个方法签名:(参考 ArrayList 源码中的两个方法签名http://www.hollischuang.com/archives/1140)
private void writeObject(java.io.ObjectOutputStream s) //方法签名,可以自己自定义序列化的过程
throws java.io.IOException {}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {}
问题:为什么类中定义了方法签名,就能自定义序列化的过程?
解答:
在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。
如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。
用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
简而言之,自己类中定义的这些方法签名,序列化时jvm就试图调用这些方法,从而进行自定义操作
public static void main(String[] args) throws Exception {
//1、对象序列化
// String file = "file/file.dat";
//
// ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
//
// ClassDemo2 stu = new ClassDemo2("jack",99,"202001");
// oos.writeObject(stu);
// oos.flush();
// oos.close();
//2、对象反序列化
String file2 = "file/file.dat"; //对该文件内容进行反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file2));
ClassDemo2 stu2 = (ClassDemo2) ois.readObject(); //强制类型转换
System.out.println(stu2.toString()); //ClassDemo2{name='jack', score=99, num='202001'}
ois.close();
}
//类中自己定义的方法签名
private void writeObject(java.io.ObjectOutputStream s) //方法签名,可以自己自定义序列化的过程
throws java.io.IOException {
s.defaultWriteObject(); //把虚拟机能默认序列化的元素进行序列化
s.writeObject(num); //自己完成序列化的操作
}
private void readObject(java.io.ObjectInputStream s) //方法签名
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject(); //把jvm默认能反序列化的元素进行反序列化
this.num=(String) s.readObject(); //强制类型转换,因为num为String类型
}
2、序列化中子父类构造函数问题
一个类实现了序列化接口,其子类都能进行序列化操作
对象被序列化时,会调用该类及其父类的构造方法
对象被反序列化时,如果父类实现了序列化接口,则不会调用构造方法;反之,父类没有实现,则会调用
public static void main(String[] args) throws Exception {
//反序列化类
// ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file/file.dat"));
// Foo2 foo2 = new Foo2();
// oos.writeObject(foo2); //反序列化
// oos.flush();
// oos.close();
//反序列化时,是否递归调用构造函数
// ObjectInputStream ois = new ObjectInputStream(new FileInputStream("file/file.dat"));
// Foo2 foo2 = (Foo2)ois.readObject();
// System.out.println(foo2);
// ois.close();
// ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file/file.dat"));
// Bar3 bar3 = new Bar3();
// oos.writeObject(bar3); //反序列化
// oos.flush();
// oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("file/file.dat"));
Bar3 bar3 = (Bar3)ois.readObject();
System.out.println(bar3);
ois.close();
}
}
/**
* 一个类实现了序列化接口,那么其子类都可以进行序列化
*/
class Foo implements Serializable{
public Foo(){
System.out.println("foo");
}
}
class Foo2 extends Foo{
public Foo2() {
System.out.println("foo2");
}
}
class Bar{
public Bar() {
System.out.println("bar");
}
}
class Bar2 extends Bar implements Serializable{ //子类实现序列化接口,父类没有
public Bar2() {
System.out.println("bar2");
}
}
class Bar3 extends Bar2{
public Bar3() {
System.out.println("bar3");
}
}
- 本文链接:http://siii0.github.io/java-IO%E6%B5%81/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。