目录
文件/节点集合缓冲流/处理流之一的课后练习一(实现图片加密码与图片输出的解密)
文件/节点集合缓冲流/处理流之一的课后练习二(使用map统计文本上每个字符出现次数,并把map中的数据写入文件)
(了解)NIO.2中Path、Paths、Files类的使用
Files常用静态方法(类似于Collections):用于判断、操作内容、操作文件和目录
了解最早以前的只有英文数字字符、控制代码、空格与二进制之间的关系——ASCII码
File类
File类概述
java.io.File
类:文件和文件目录路径的抽象表示形式,与平台无关- File 能新建、删除、重命名文件和目录,但File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。
- 想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录。
- File对象可以作为参数传递给流的构造器
- File类的一个对象,代表一个文件或一个文件目录(俗称:文件夹)
- File类声明在java.io包下
- 创建file类的实例
- File(String filePath):以filePath为路径创建File对象,可以是绝对路径或者相对路径
- File(String parentPath,String childPath):以parentPath为父路径,childPath为子路径创建File对象。相当于两个路径拼接在一起
- File(File parentFile,String childPath):根据一个父File对象和子文件路径创建File对象。也是把两个路径拼接在一起
- 这里的filePath就是路径,路径有又分为相对路径和绝对路径,还有路径分隔符
- 相对路径:相较于某个路径下,指明的路径。下面以IDEA做声明
-
如果你在Main方法中使用,那么相对路径就是你Project工程的路径
- 如果你在测试类且不在main方法中,而是在@Test注解下的方法中,那么相对路径路径就是你当前的Module模块,而不是Project工程(但是Eplice只认Project工程,并没有模块这种说法,因此相对路径都是Project工程)
-
- 绝对路径:包含盘符在内的文件或文件目录的路径
- 路径分隔符
- windows和DOS用:\\ #这里需要双反斜杠的原因是因为在window层面是单反斜杠,但是在java层面单反斜杠需要转义才能表示,因此在java中需要在前面加多一个反斜杠做为转义作用,后面那个反斜杠才是单反斜杠的意思
- Linux和URL用:/
- Java程序支持跨平台运行,因此路径分隔符要慎用。如果你在window系统测试无误后要上线部署到linux系统时,路径及路径分隔符就要做出相应的修改!!!!!!!!!!!!
- 为了解决这个隐患,File类提供了一个常量:
public static final String separator
根据操作系统,动态的提供分隔符。- File file1= new File("d:\\Work\\info.txt");
#window下的写法 - File file3= new File("d:/Work");
#Linux下的写法 - File file2= new File("d:"+ File.separator+ "Work"+ File.separator+ "info.txt");
#使用File类的separator常量代替分隔符,file.separator 值的第一个字符。在 UNIX 系统上,此字段的值为 ‘/’;在 Microsoft Windows 系统上,它为 ‘\’。使得代码更加通用
- File file1= new File("d:\\Work\\info.txt");
- 相对路径:相较于某个路径下,指明的路径。下面以IDEA做声明
代码演示File类入门案例
package javase15;
import org.junit.Test;
import java.io.File;
public class IO1 {
//创建file对象
@Test
public void test1(){
File file1 = new File("hello.txt");//没有盘符,相当于相对路径.在Test方法中,指向当前Module模块中
File file2 = new File("F:\\java\\Work2\\JavaSenior\\day08\\num.txt");//有盘符,是绝对路径
//public static final String separator常量的使用,可以代替路径分隔符使用,使得代码更加通用
File file3 = new File("F:"+File.separator+"java"+File.separator+"Work2"+File.separator+"JavaSenior"+File.separator+"day08"+File.separator+"num.txt");
//这里并没有使用file对象进行操作,只是把file对象指向的路径进行输出
System.out.println(file1);//hello.txt
System.out.println(file2);//F:\java\Work2\JavaSenior\day08\num.txt
System.out.println(file3);//F:\java\Work2\JavaSenior\day08\num.txt
//构造器2:
File file4 = new File("D:\\workspace_idea1","JavaSenior");//这里表示根目录下D:\workspace_idea1的名为JavaSenior的文件目录
System.out.println(file4);//D:\workspace_idea1\JavaSenior
// //构造器3:
File file5 = new File(file4,"hi.txt");//传入目录对象,在目录对象的基础上为相对路径,指向新的目录或文件
System.out.println(file5);//D:\workspace_idea1\JavaSenior\hi.txt
}
}
File类常用方法
- 获取
- public String getAbsolutePath():获取绝对路径
- public String getPath() :获取file对象路径
- public String getName() :获取文件或者目录名称
- public String getParent():参考file对象,获取上层文件目录路径。若无,返回null
- public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
- public long lastModified() :获取最后一次的修改时间,毫秒值
- 如下的两个方法适用于文件目录,且前提条件是文件目录都存在:
- public String[] list() :获取指定目录下的所有文件或者文件目录的名称,返回一个String数组
- public File[] listFiles() :获取指定目录下的所有文件或者文件目录,转换成File类型对象,存放到一个File数组并返回
- 移动
- public boolean renameTo(File dest):把文件或者文件目录重复名,或者把文件或者文件目录移动到指定目录上,首先确保引用该方法的file对象指向的文件或者文件目录是在硬盘中真实存在的,然后移动到一个在硬盘中不存在的File类型的dest对象中,否则该方法不会生效,
例如:
file1.renameTo(file2)为例:要想保证返回true,需要file1在硬盘中是存在的,且file2不能在硬盘中存在。
- public boolean renameTo(File dest):把文件或者文件目录重复名,或者把文件或者文件目录移动到指定目录上,首先确保引用该方法的file对象指向的文件或者文件目录是在硬盘中真实存在的,然后移动到一个在硬盘中不存在的File类型的dest对象中,否则该方法不会生效,
- 判断
- public boolean isDirectory():判断是否是文件目录
- public boolean isFile() :判断是否是文件
- public boolean exists() :判断是否存在
- public boolean canRead() :判断是否可读
- public boolean canWrite() :判断是否可写
- public boolean isHidden() :判断是否隐藏
- (创建)硬盘中对应的文件或文件目录
- public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false
- public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
- public boolean mkdirs() :创建文件目录。如果此文件目录存在,就不创建了。如果上层文件目录不存在,一并创建
- (删除)删除磁盘中的文件或文件目录
- public boolean delete():删除文件或者文件夹,注意!!!!Java中的删除不走回收站,delete()方法删除文件目录要求文件夹里面没有文件及子文件目录
代码演示File常用方法
package javase15;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.Date;
public class IO2 {
//获取
@Test
public void test1() {
File file = new File("Hello.txt");
File file2 = new File("F:\\java\\Work2\\JavaSenior\\day08\\num.txt");
System.out.println(file.getAbsolutePath());//E:\JavaSE\Project\Hello.txt,获取文件的根目录
System.out.println(file.getPath());//Hello.txt,获取file对象路径
System.out.println(file2.getPath());//F:\java\Work2\JavaSenior\day08\num.txt,获取file对象路径
System.out.println(file.getName());//Hello.txt,获取文件或者目录名称
System.out.println(file2.getName());//num.txt.txt,获取文件或者目录名称
System.out.println(file.getParent());//null,参考file对象,获取上层文件目录路径。若无,返回null
System.out.println(file2.getParent());//F:\java\Work2\JavaSenior\day08,参考file对象,获取上层文件目录路径。若无,返回null
System.out.println(file.length());//0,获取文件长度(即:字节数)
System.out.println(file2.length());//0,获取文件长度(即:字节数)
System.out.println(new Date(file.lastModified()));//Thu Jan 01 08:00:00 CST 1970,获取最后一次的修改时间,毫秒值,通过Date构造器转换成特定日期格式
}
//移动
@Test
public void test2() {
File file = new File("F:\\java\\Work2\\JavaSenior");//补充:window的目录和文件名是不区分大小写的,在java设置路径时可以不用那么在意大小写
//如下的两个方法适用于文件目录,且前提条件是文件目录都存在!!!!!因此需要在file对象指向的路径设置好我们的文件或文件目录
String[] list = file.list();//获取指定目录下的所有文件或者文件目录的名称String类型数组
for (String s : list) {
System.out.println(s);
}
/*
* 新建文件夹
* 新建文本文档.txt
* */
System.out.println("********************");
File[] files = file.listFiles();//获取指定目录下的所有文件或者文件目录,转换成File类型对象,存放到一个File数组并返回
for (File f : files) {
System.out.println(f);
}
/*
* F:\java\Work2\Javasenior\新建文件夹
* F:\java\Work2\Javasenior\新建文本文档.txt
* */
}
/**
* File类的重命名功能
* <p>
* public boolean renameTo(File dest):把文件重命名为指定的文件路径
* 比如:file1.renameTo(file2)为例:
* 要想保证返回true,需要file1在硬盘中是存在的,且file2不能在硬盘中存在。
*/
@Test
public void test3() {
File file1 = new File("F:\\java\\Work2\\JavaSenior\\新建文本文档.txt");
File file2 = new File("F:\\java\\Work2\\新建文本文档.txt");
File file3 = new File("F:\\java\\Work2\\JavaSenior\\新建文件夹");
File file4 = new File("F:\\java\\Work2\\新建文件夹");
boolean renameTo1 = file1.renameTo(file2);
System.out.println(renameTo1);
boolean renameTo2 = file3.renameTo(file4);
System.out.println(renameTo2);
}
//判断
@Test
public void test4() {
File file1 = new File("F:\\java\\Work2\\JavaSenior\\新建文本文档.txt");
System.out.println(file1.isDirectory());//false,是否为文件目录
System.out.println(file1.isFile());//true,是否为文件
System.out.println(file1.exists());//true,文件或者目录是否存在
System.out.println(file1.canRead());//true,是否可读
System.out.println(file1.canWrite());//true,是否可写入
System.out.println(file1.isHidden());//false,是否隐藏
}
//操作文件或文件目录
@Test
public void test5() throws IOException {
//文件或者文件目录的删除或者文件的创建
File file1 = new File("F:\\java\\Work2\\JavaSenior\\新建文本文档.txt");
if (!file1.exists()) {//如果文件不存在则需要
file1.createNewFile();//文件的创建
System.out.println("创建成功");
} else {//如果文件存在则进行
file1.delete();//文件删除
System.out.println("删除成功");
}
//文件目录的创建
File file2 = new File("F:\\java\\Work2\\JavaSenior\\新建文件夹2");
boolean b1 = file2.mkdir();//如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
// boolean b1 = file2.mkdirs();//创建文件目录。如果此文件目录存在,就不创建了。如果上层文件目录不存在,一并创建
if (b1 == true) {
System.out.println("文件目录创建成功");
} else {
System.out.println("文件创建失败");
}
//删除文件目录,delete()方法删除文件目录要求文件夹里面没有文件及子文件目录
boolean b2 = file2.delete();
if (b2) {
System.out.println("文件目录删除成功");
} else {
System.out.println("文件目录删除失败,请检查删除文件目录下是否有文件和子文件目录");
}
}
}
File课后练习
package javase15;
import org.junit.Test;
import java.io.File;
import java.io.FilenameFilter;
public class IO3 {
//课后练习1,判断指定目录下是否有后缀名为.jpg的文件,如果有,就输出该文件名称
//方法1
@Test
public void test1() {
File file1 = new File("F:\\java\\Work2\\JavaSenior\\新建文件夹");
String[] list1 = file1.list();
for (String s : list1) {
if (s.endsWith(".jpg")) System.out.println(s);//测试文件名字符串是否以".jpg结尾",然后打印文件名
}
/*
* photo(2).jpg
* photo.jpg
* */
}
//方法2
@Test
public void test2() {
File file1 = new File("F:\\java\\Work2\\JavaSenior\\新建文件夹");
File[] files = file1.listFiles();//获取指定目录下的所有文件或者文件目录,转换成File类型对象,存放到一个File数组并返回
for (File f : files) {
if (f.getName().endsWith(".jpg")) System.out.println(f.getName());
}
/*
* photo(2).jpg
* photo.jpg
* */
}
//方法3,使用File类提供的过滤器public String[] list(FilenameFilter filter)或者public File[] listFiles(FileFilter filter)
@Test
public void test3() {
//public String[] list(FilenameFilter filter)
File file1 = new File("F:\\java\\Work2\\JavaSenior\\新建文件夹");
String[] list = file1.list(new FilenameFilter() {//条件筛选,迭代所有文件,如果它里面的重写的accept方法返回了true,就表示符合,该文件名会被添加String数组中
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jpg");//如果返回了true,这个
}
});
for (String s : list) {
System.out.println(s);
}
/*
* photo(2).jpg
* photo.jpg
* */
//public File[] listFiles(FileFilter filter)
File srcFile = new File("F:\\java\\Work2\\JavaSenior\\新建文件夹");
File[] subFiles = srcFile.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jpg");
}
});
for (File file : subFiles) {
System.out.println(file.getAbsolutePath());
}
/*
F:\java\Work2\JavaSenior\新建文件夹\photo(2).jpg
F:\java\Work2\JavaSenior\新建文件夹\photo.jpg
*/
}
//删除指定文件目录及其下的所有文件
@Test
public void test4() {
File file = new File("F:\\java\\Work2\\JavaSenior\\新建文件夹");
IO3.deleteDirectory(file);
}
public static void deleteDirectory(File file) {
// 如果file是文件,直接执行delete()方法
// 如果file是目录,先把它的下一级干掉,然后删除自己
if (file.isDirectory()) {
File[] all = file.listFiles();
// 循环删除的是file的下一级
for (File f : all) {// f代表file的每一个下级
deleteDirectory(f);
}
}
// 最后删除清除干净的文件夹
file.delete();
}
}
IO
IO原理
- I/O是Input/Output的缩写,I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件(内存与磁盘的交互),网络通讯(设备之间通过网络交互数据)等。
- Java程序中,对于数据的输入/输出操作以“流(stream)”的方式进行。所以在java程序中称之为IO流
- java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。例如:读取数据会有"read"、“input”,写入数据会有"write"、“output”这些关键字
- 输入input:因为我们是程序员,编写程序要站在程序的角度,读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
- 输出output:因为我们是程序员,编写程序要站在程序的角度,将程序(内存)数据输出到磁盘、光盘等存储设备中。
流的分类
-
按操作数据单位不同划分为:字节流(8 bit,也就是一个字节,例如01010101),字符流(16 bit,也就是两个字节,一个char)
- 按数据流的流向不同分为:输入流,输出流,以java程序为参考系
- 按流的角色功能的不同分为:节点流直接操作数据读写的流,比如
FileInputStream
,处理流对一个已存在的流的链接和封装,通过对数据进行处理为程序提供功能强大、灵活的读写功能,例如BufferedInputStream
(缓冲字节流)
IO流体系(IO流抽象基类的子类及其作用)
- Java的IO流共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的。
- 由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。
- 下图说明:蓝色框都是最基本的流,区分可以看最后面的关键字“Input”、“Output”、“Reader”、“Writer”
文件流(节点流)
FileReader读入数据的基本操作
FileReader读取文件步骤:
- 建立一个流对象,将已存在的一个文件加载进字符流。如果文件不存在会抛出异常
FileReader fr= new FileReader(new File(“Test.txt”));
- 创建一个临时存放数据的char数组。
char[] ch= new char[1024];
- 调用流对象将流中的数据读取到数组中,如果数组有数据,就会进行覆盖,如果最后一轮读取数据时数据总数没有数组长,数组前面的数据可以被覆盖,但后面的数据是上一轮的数据,也可以使用read()方法,只读取一个字符
fr.read(ch);
- 关闭流对象,垃圾回收机制只回收JVM堆内存里的对象空间,对于其他物理粘结,比如数据库连接、输入流输出流、Socket连接无能为力。在Java代码编写的过程可能会发出警告Resorce leak:xxx is never closed.提示垃圾过多系统无法存储其他的资源,内存泄漏。
fr.close();
FileReader代码演示
package javase15;
import org.junit.Test;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class IO4 {
//FileReader的read()方法读取一个字符
@Test
public void test1() /*throws IOException*/ {
//创建一个File对象,指向的文件路径是一个真实存在的文件,并且文件里面有内容
File file = new File("F:\\java\\Work2\\JavaSenior\\新建文本文档.txt");
// //提供文件字符流
// FileReader fr = new FileReader(file);//抛出异常FileNotFoundException,文件可能找不到
//使用read()从首段开始读取文件里的下一个字符,该字符是一个int类型数字,可以根据Unicode编码表强转成char类型字符,如果达到文件末尾,就返回-1
//方法1
// int data = fr.read();//抛出比刚才还要大的异常IOException
// while (data != -1) {
// System.out.println(data+"-->"+(char)data);
// data = fr.read();
// }
// /*
// 20013-->中
// 22269-->国
// */
// //关流
// fr.close();
//(重新写一遍)提供文件字符流,重写的原因是因为配合下面方法二的try catch方法,确保创建的流对象创建了最终一定要被释放
FileReader fr = null;
try {
fr = new FileReader(file);//抛出异常FileNotFoundException
//方法2
int data;
while ((data = fr.read()) != -1) {
System.out.println(data + "-->" + (char) data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr != null) {
try {
fr.close();//因为关闭流自己也有异常,因此需要再抛出异常
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//FileReader的read(char[] cbuf)方法调用流对象将流中的数据读取到数组中,如果数组有数据,就会进行覆盖,提高效率,不用每次只去磁盘中读取一个字符
@Test
public void test2() {
FileReader fr = null;
try {
//创建一个File对象,指向的文件路径是一个真实存在的文件,并且文件里面有内容
File file = new File("F:\\java\\Work2\\JavaSenior\\新建文本文档.txt");
//FileReader流的实例化
fr = new FileReader(file);//文件可能找不到的异常
//读入操作,一次读取多个字符
char[] cbuf = new char[5];//返回每次读入cbuf数组中的字符的个数。如果达到文件末尾,返回-1
// fr.read(cbuf);
int len;//记录读取了多少个
while ((len = fr.read(cbuf)) != -1) {
//方式一:
//错误的写法,如果最后一轮读取数据时数据总数没有数组长,数组前面的数据可以被覆盖,但后面的数据是上一轮的数据
// for(int i = 0;i < cbuf.length;i++){
// System.out.print(cbuf[i]);//遍历读入数组的所有字符
// }
//正确的写法
// for (int i = 0; i < len; i++) {
// System.out.print(cbuf[i]);
// }
//方式二:
//错误的写法,如果最后一轮读取数据时数据总数没有数组长,数组前面的数据可以被覆盖,但后面的数据是上一轮的数据
// String str = new String(cbuf);//
// System.out.print(str);
//正确的写法
String str = new String(cbuf, 0, len);//从什么开始到什么结束,将char抓换成String类型字符串
System.out.print(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关流
if(fr != null){
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileWriter写出数据的操作
FileWriter写出数据步骤:
- 创建流对象,建立数据存放文件,File对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件。
- 如果流使用的构造器是:FileWriter(file,false) / FileWriter(file):对原有文件的覆盖
- 如果流使用的构造器是:FileWriter(file,true):不会对原有文件覆盖,而是在原有文件基础上追加内容
-
FileWriter fw = new FileWriter(new File(“Test.txt”));
- 调用流对象的写入方法write,将数据写入流,数据可以是String/int/char[]
fw.write(“atguigu-songhongkang”);
- 关闭流资源,并将在内存中的输出流中的数据清空传入到文件中。写入的数据的流不关闭是没写不进去的,如果写入的数据是覆盖原有的数据,那么写入的数据和被覆盖的数据都会丢失。
因为覆盖的写入流执行顺序:创建FileWriter类型的流对象,文件原有的数据被清空-->通过流写入数据,也就是使用write(),此时的数据还在流中-->关闭流,流中数据清空到文件中,至此,才是通过流覆盖式写入文件的结束。因此要关好输出流再使用输入流fw.close();
代码演示FileWriter写入数据
package javase15;
import org.junit.Test;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class IO5 {
@Test
public void test1() {
FileWriter fw = null;
FileReader fileReader = null;
File file = null;
try {
//提供File类的对象,指明写出到的文件,文件可以不存在,不存在的文件会在添加内容的时候自动创建一个
file = new File("F:\\java\\Work2\\JavaSenior\\新建文本文档.txt");
//提供FileWriter的对象,用于数据的写出
fw = new FileWriter(file, false);//流使用的构造器是:FileWriter(file,false) / FileWriter(file):对原有文件的覆盖
//写入数据
fw.write("I have a dream!\n");//"\n"表示换行,(强调)java中"\\"表示的是"\"
fw.write("you need to have a dream!\n");
//模拟错误,得出如果代码出错不关输出流会导致写入的数据和被覆盖的数据都会丢失
// int i = 1/0;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fw.close();//写入数据的流不关闭是写不进去的,如果写入的数据是覆盖原有的数据,那么写入的数据和被覆盖的数据都会丢失,这里也是为了防止原有数据丢失
try {
//关好输出流再使用输入流读取数据,看看数据是否被写入,且新写入的数据有覆盖原有文件的数据
fileReader = new FileReader(file);
char[] c1 = new char[10];
int len;
while ((len = fileReader.read(c1)) != -1) {
for (int i = 0; i < len; i++) {
System.out.print(c1[i]);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fileReader != null) fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
代码演示使用FileReader和FileWriter实现文本文件的复制
提示:字符流只能用于文本文件操作。对于非文本文件的操作只能使用字节流处理,因为字符流底层是整数型数据,可以通过强转成char,而不是字节流的byte字节存储二进制数据
package javase15;
import org.junit.Test;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class IO6 {
@Test
public void test1() {
FileReader fr = null;
FileWriter fw = null;
try {
//创建两个file对象指向两个文件,将文件1.txt的内容复制粘贴到文件2.txt中
File srcFile = new File("F:\\java\\Work2\\JavaSenior\\文件1.txt");
File srcFile2 = new File("F:\\java\\Work2\\JavaSenior\\文件2.txt");
//创建输入流和输出流的对象
fr = new FileReader(srcFile);
fw = new FileWriter(srcFile2);
//数据的读入和写出操作
char[] cbuf = new char[5];
int len;//记录每次读入到cbuf数组中的字符的个数
while((len = fr.read(cbuf)) != -1){
//每次写出len个字符
fw.write(cbuf,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关流方法1
// try {
// if(fw != null)
// fw.close();
// } catch (IOException e) {
// e.printStackTrace();
// }finally{
// try {
// if(fr != null)
// fr.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
//关流方法二:
try {
if(fw!=null)fw.close();//关闭输出流
} catch (IOException e) {//此时的关流异常IOException已经被当前的try catch原地处理了,因此下面的try catch也能继续执行
e.printStackTrace();
}
try {
if(fr!=null)fr.close();//关闭输入流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
FileInputStream的使用
- 对于文本文件(.txt,.java,.c,.cpp),使用字符流处理,字符流只能用于文本文件操作。对于非文本文件的操作只能使用字节流处理,因为字符流底层是整数型数据,char用的是Unicode字符集,可以通过强转成char。而不是像字节流的byte字节数组存储二进制数据
- 对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,...),使用字节流处理,因为字符涉及编码问题,所以不推荐使用byte来读取二进制数据,但是字节流可以用来复制文本文件和非文本文件,因为没有用String构造器读取,而是直接复制到另外一个文件中,就不会出现乱码。
代码演示不推荐使用字节流FileInputStream来读取文本文档的原因:"因为容易出现乱码"
package javase15;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class IO7 {
//使用字节流FileInputStream处理文本文件,可能出现乱码
@Test
public void test1() {
FileInputStream fis = null;
try {
File file = new File("F:\\java\\Work2\\JavaSenior\\新建文本文档.txt");
fis = new FileInputStream(file);
//3.读数据
byte[] buffer = new byte[5];//每一次通过输入流从磁盘中读取5个byte存放五个字节的数组中,此文档使用的编码格式是UTF-8,英文是一个字节,中文是三个字节
int len;//记录每次读取的字节的个数
while((len = fis.read(buffer)) != -1){//通过输入流从磁盘中读取5个byte存放五个字节的数组中
String str = new String(buffer,0,len);//通过String构造器结合idea设置setting设置的UTF-8编码集来将数组里的数据转换成字符串
System.out.print(str);//打印转换好的字符串
/*
* 中��人
* I have a dream!
* you need to have a dream!
*
* 出现乱码原因:所要读取的文档编码格式是UTF-8,也就是说英文是一个字节,中文是三个字节,一次只能读取5个字节,那么意味着读完"中"这三个字节后,再读
* "国"的时候,第一次数组只读到"国"的两个字节,然后通过String构造器结合idea设置setting设置的UTF-8编码集来将数组里的数据转换成字符串,
* 只有"中转换成功,因为第一次数组有完整的编译成"中"的三个字节,而"国"只有两个字节,国则编码错误,出现"�".同理!第二数组只读到"国"的后一个字节,
* 而"人"读到三个字节,还有一个字节就是留给"回车换行符",然后通过String构造器结合idea设置setting设置的UTF-8编码集来将数组里的数据转换成字符串,
* 只有"人"和"回车换行符"转换成功,而"国"只有一个字节,国则编码错误,出现"�"*/
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(fis != null) {
//4.关闭资源
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
使用FileInputStream和FileOutputStream复制文件的方法测试
提示:对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,...),使用字节流处理,因为字符涉及编码问题,所以不推荐使用byte来读取字符,但是字节流可以用来复制文本文件和非文本文件,因为没有用String构造器读取,而是直接复制到另外一个文件中。
package javase15;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class IO8 {
@Test
public void test1(){
long start = System.currentTimeMillis();
//可以通过字节流复制文本和非文本文件
// String srcPath = "F:\\java\\Work2\\JavaSenior\\1.jpg";
// String destPath = ""F:\\java\\Work2\\JavaSenior\\2.jpg"";
String srcPath = "F:\\java\\Work2\\JavaSenior\\新建文本文档(1).txt";
String destPath = "F:\\java\\Work2\\JavaSenior\\新建文本文档(2).txt";
copyFile(srcPath,destPath);
long end = System.currentTimeMillis();
System.out.println("复制操作花费的时间为:" + (end - start));//文本文档的速度是1,图片的速度是60,视频是618
}
//设置通过字节流复制所有文件的方法
public void copyFile(String srcPath,String destPath){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//
File srcFile = new File(srcPath);
File destFile = new File(destPath);
//
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
//复制的过程
byte[] buffer = new byte[1024];//写一个相对较大的字节数组来存放每次从磁盘里面读取的字节个数,提高复制文件性能
int len;
while((len = fis.read(buffer)) != -1){
fos.write(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fos != null){
//
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
缓冲流(处理流之一)
- 在实际开发中是不会用那几个最基本的文件流,而是使用缓冲流。为了提高数据读写的速度,Java API提供了带缓冲功能的流类,在使用这些流类时,会创建一个内部缓冲区数组,缺省使用8192个字节(8Kb)/字符的缓冲区,因此缓冲流又称为处理流,在原有的节点流基础上再制造一层流
- BufferedInputStream/BufferedOutputStream
-
BufferedReader/BufferedWriter
- BufferedInputStream/BufferedOutputStream
-
缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:
- BufferedInputStream和BufferedOutputStream
- BufferedReader和BufferedWriter
- 所有的缓冲流都没有任何的读取/写入等操作文件能力,这里都需要对应的输入流和输出流节点流(例如
FileInputStream
)来提供对应的能力。因此在创建缓冲流流对象时,需要传入对应的节点流的输入流对象和输出流对象。底层就是提供了一个默认大小的缓冲数组,用于提高效率
使用缓冲数组之后,程序在运行的大部分时间内都是内存和内存直接的数据交互过程。内存直接的操作效率是比较高的。因为降低了CPU通过内存操作硬盘的次数 -
关闭流的顺序和打开流的顺序相反。只要关闭最外层流(也就是处理流之一的缓冲流)即可,关闭最外层流也会相应关闭内层节点流
-
关闭缓冲流,都会首先释放对应的缓冲数组空间,并且关闭对应的字符输入流和字符输出流。关闭后不能再写出。
-
当读取数据时,在BufferedInputStream底层中有一个默认容量为8KB的byte类型缓冲数组。fill()方法是一个操作核心,,利用缓冲,fill方法,可以极大的降低CPU通过内存访问硬盘的次数。同时程序操作的数据是在内存中进行交互的,具体步骤如下:
-
补充说明:fill()是私密方法,只能内部调用
-
-
从硬盘中读取数据,读取的数据容量和缓冲数组容量一致。
-
所有的read方法,都是从缓冲数组中读取数据
-
每一次读取数据之前,都会检查缓冲区内是否有数据,如果没有,fill()方法执行,填充数据
-
-
向流中写入字节时,不会直接写到文件,在BufferedOutputStream类对象,默认有一个8KB的byte类型缓冲数组,步骤如下:
-
flush()方法的使用:手动将buffer中内容写入文件并清空
-
-
数据写入文件时并不是直接保存到文件中,而是保存在内存8KB字节缓冲数组中
-
如果8KB空间填满,会直接flush缓冲区,数据保存到硬盘中,同时清空整个缓冲区。
-
在BufferedOutputStream关闭时,首先会调用flush方法,保存数据到文件,清空缓冲区,并且规划缓冲区占用内存,同时关闭缓冲流使用的字节输出流
- 字符缓冲输入流,底层有一个8192个元素的缓冲字符数组,而且使用fill方法从硬盘中读取数据填充缓冲数组。
字符缓冲输出流,底层有一个8192个元素的缓冲字符数组,使用flush方法将缓冲数组中的内容写入到硬盘当中
BufferedReader和BufferedWriter
代码演示缓冲流(字节型)实现非文本文件的复制及缓冲流与节点流读写速度对比(结果是缓冲流速度快)
package javase15;
import org.junit.Test;
import java.io.*;
public class IO9 {
@Test
public void test1() {
long start = System.currentTimeMillis();
String srcPath = "F:\\java\\Work2\\JavaSenior\\book1.flv";
String destPath = "F:\\java\\Work2\\JavaSenior\\book2.flv";
copyFileWithBuffered(srcPath,destPath);
long end = System.currentTimeMillis();
System.out.println("复制操作花费的时间为:" + (end - start));//视频复制速度速度从618-->176,越大的视频文件效果越明显
}
//实现文件复制的方法
public void copyFileWithBuffered(String srcPath,String destPath){
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
//1.造文件
File srcFile = new File(srcPath);
File destFile = new File(destPath);
//2.造流
//2.1 造节点流
FileInputStream fis = new FileInputStream((srcFile));
FileOutputStream fos = new FileOutputStream(destFile);
//2.2 造缓冲流(处理流),参数为节点流,使用BufferedInputStream构造器创建了一个8KB==8*1024Byte缓冲区
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
//3.复制的细节:读取、写入
byte[] buffer = new byte[1024];//创建一个1024字节的容器来将二进制数据读取BufferedInput的8*1204字节的缓冲区中数据
int len;
while((len = bis.read(buffer)) != -1){//循环中每次读操作都是从输入流缓冲区中获取数据,缓冲区没有数据再调用fill()填充
bos.write(buffer,0,len);//复制操作,每次写操作都是写进输出流的缓冲区,缓冲区填满底层就会调用flush()刷新缓冲区,并把缓冲区所有数据写入文件中
// bos.flush();//底层自动调用刷新缓冲区,因此这里可以不用调用flush方法,也可以手动将buffer中内容写入文件并清空缓冲区
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.资源关闭
//要求:先关闭外层的流,再关闭内层的流,也就是先关处理流,再关节点流,但是...
if(bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bis != null){
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,因此我们可以省略关闭内层的流.
// fos.close();
// fis.close();
}
}
}
BufferedReader和BufferedWriter
缓冲流(字符型)实现文本文件的复制
package javase15;
import org.junit.Test;
import java.io.*;
public class IO10 {
@Test
public void test1() {
BufferedReader br = null;
BufferedWriter bw = null;
try {
//创建文件和相应的流
br = new BufferedReader(new FileReader(new File("F:\\java\\Work2\\JavaSenior\\文件1")));
bw = new BufferedWriter(new FileWriter(new File("F:\\java\\Work2\\JavaSenior\\文件2")));
//读写操作
//方式一:使用char[]数组
// char[] cbuf = new char[1024];
// int len;
// while((len = br.read(cbuf)) != -1){//每次调用read(char[])方法能从输入流缓存中读取1024个字符存入到cbuf的char类型数组中
// bw.write(cbuf,0,len);//然后每次调用write(char[],int,int)把数组中的数据存入到输出流的缓存中
// // bw.flush();//手动清空输出流缓存写入到数据库中
// }
//方式二:使用String
String data;
while ((data = br.readLine()) != null) {//readLine()方法一次读取输入流缓存中的一行的数据存入到String类型的data中
//换行方法一:
// bw.write(data + "\n");//data中不包含换行符,因此可以手动添加
//换行方法二:
bw.write(data);//data中不包含换行符
bw.newLine();//提供换行的操作,当行内容会进行换行操作
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
if (bw != null) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
文件/节点集合缓冲流/处理流之一的课后练习一(实现图片加密码与图片输出的解密)
效果说明:加密后的图片大小一样,只是每个字节具体的值不一样而已.且因为字节加密了所以图片打不开
package javase15;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class IO11 {
public static void imageHide(String photoPath,String secretPhotoPath){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(photoPath);
fos = new FileOutputStream(secretPhotoPath);
byte[] buffer = new byte[20];
int len;
while ((len = fis.read(buffer)) != -1) {
//对字节数逐一加密
//错误方式,使用高效for循环,原因:这里只是将buffer数组里面的字节取了出来赋予给变量b,这里只修改了变量b,因此不会对数组的值有任何影响
// for(byte b : buffer){
// b ^= 5;
// }
//正确方式,应该使用索引来修改buffer数组里面的字节
for (int i = 0; i < len; i++) {
buffer[i] = (byte) (buffer[i] ^ 5);//异或运算,这次调用会加密,下次调用就是解密,这次是解密
}
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//图片解密
public static void puzzlePhoto(String secretPhotoPath,String photoPath){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(secretPhotoPath);
fos = new FileOutputStream(photoPath);//false或者不写就是覆盖文件
byte[] buffer = new byte[20];
int len;
while ((len = fis.read(buffer)) != -1) {
//字节数组进行修改
//错误的
// for(byte b : buffer){
// b = (byte) (b ^ 5);
// }
//正确的
for (int i = 0; i < len; i++) {
buffer[i] = (byte) (buffer[i] ^ 5);//异或运算,这次调用会加密,下次调用就是解密,这次是解密
}
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
文件/节点集合缓冲流/处理流之一的课后练习二(使用map统计文本上每个字符出现次数,并把map中的数据写入文件)
package javase15;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class IO12 {
public void wordCount(String filePath,String countWordPath) {
FileReader fr = null;
BufferedWriter bw = null;
try {
//创建Map集合
Map<Character, Integer> map = new HashMap<Character, Integer>();
//遍历每一个字符,每一个字符出现的次数放到map中
fr = new FileReader(filePath);
int c = 0;
while ((c = fr.read()) != -1) {
//int 还原 char
char ch = (char) c;
// 判断char是否在map中第一次出现
if (map.get(ch) == null) {
map.put(ch, 1);//如果是就添加该字符为key,value为int类型的1
} else {
map.put(ch, map.get(ch) + 1);//如果不是就覆盖原有的key,并给value的int类型数值+1
}
}
//把map中数据存在countWordPath,作为统计文件输出
bw = new BufferedWriter(new FileWriter("countWordPath"));
//遍历map,通过switch来判断写入数据
Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
for (Map.Entry<Character, Integer> entry : entrySet) {
switch (entry.getKey()) {
case ' ':
bw.write("空格=" + entry.getValue());
break;
case '\t'://\t表示tab 键字符
bw.write("tab键=" + entry.getValue());
break;
case '\r'://
bw.write("回车=" + entry.getValue());
break;
case '\n'://
bw.write("换行=" + entry.getValue());
break;
default:
bw.write(entry.getKey() + "=" + entry.getValue());
break;
}
bw.newLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关流
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bw != null) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
转换流(处理流之一)
- 转换流提供了在字节流和字符流之间的转换
- 在输入的时候把输入字节流转换成输入字符流,并设置特定的编码格式解码文件内容,在输出时会把输出字符流转换成输出字节流,并设置指定的编码格式编码文件内容存放到文件中
- Java API提供了两个转换流:
- InputStreamReader:将InputStream转换为Reader
- 实现将字节的输入流按指定字符集转换为字符的输入流。
- 解码:字节、字节数组 --->字符数组、字符串
- 需要和InputStream“套接”。
- 构造器
- public InputSreamReader(InputStreamin,StringcharsetName) #指定解码的字符集,具体使用哪个字符集,取决于文件dbcp.txt保存时使用的字符集
- public InputStreamReader(InputStreamin) #使用系统默认的字符集,在idea的settings中查看
- 如:Reader isr= new InputStreamReader(System.in,”gbk”); #这里的意思是将文件指定路径
- OutputStreamWriter:将Writer转换为OutputStream
- 实现将字符的输出流按指定字符集转换为字节的输出流。
- 编码:字符数组、字符串 ---> 字节、字节数组
- 需要和OutputStream“套接”。
- 构造器
- public OutputStreamWriter(OutputStreamout)
- public OutputSreamWriter(OutputStreamout,StringcharsetName)
- InputStreamReader:将InputStream转换为Reader
- 字节流中的数据都是字符时,转成字符流操作更高效。
- 很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能。
代码演示InputStreamReader和OutputStreamWriter
package javase15;
import org.junit.Test;
import java.io.*;
public class IO13 {
@Test
public void test1() {
InputStreamReader isr = null;//指定解码的字符集为UTF-8,具体使用哪个字符集,取决于文件dbcp.txt保存时使用的字符集
try {
FileInputStream fis = new FileInputStream("F:\\java\\Work2\\JavaSenior\\文件1.txt");
// InputStreamReader isr = new InputStreamReader(fis);//使用系统默认的字符集,在idea的settings中查看
isr = new InputStreamReader(fis, "UTF-8");
char[] cbuf = new char[20];
int len;
while ((len = isr.read(cbuf)) != -1) {
String str = new String(cbuf, 0, len);
System.out.print(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (isr != null) isr.close();//关闭处理流也会把对应的节点流关闭节点流给关闭
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Test
public void test2() {
InputStreamReader isr = null;
OutputStreamWriter osw = null;
try {
//造文件、造流
File file1 = new File("F:\\java\\Work2\\JavaSenior\\文件1.txt");
File file2 = new File("F:\\java\\Work2\\JavaSenior\\文件2.txt");
FileInputStream fis = new FileInputStream(file1);
FileOutputStream fos = new FileOutputStream(file2);
//转换流,将字节流转换成字符流
isr = new InputStreamReader(fis, "utf-8");
osw = new OutputStreamWriter(fos, "gbk");//此时输出的文件编码格式变成了gbk,也就是ANSI,解码要注意
//读写过程
char[] cbuf = new char[20];
int len;
while ((len = isr.read(cbuf)) != -1) {
osw.write(cbuf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
try {
if (isr != null)
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (osw != null)
osw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
标准输入、输出流(处理流之一)
说明:接收键盘输入和控制台显示
- System.in和System.out分别代表了系统标准的输入和输出设备(流)
- 默认输入设备是:键盘,输出设备是:显示器。也就是说“System.in”表示键盘输入
- 属性System.in的数据类型是InputStream
- 属性System.out的数据类型是PrintStream,其是OutputStream的子类FilterOutputStream的子类
- 重定向:通过System类的setIn,setOut方法对默认设备进行改变。
- public static void setIn(InputStream in)
- public static void setOut(PrintStream out)
package javase15;
import org.junit.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
public class IO15 {
//方法一:使用Scanner实现,调用next()返回一个字符串,本质还是System.in
//提示:在idea中使用@Test测试方法中是无法通过System.in控制台中进行输入操作,因此需要在main方法中使用
public static void main(String[] args) {
String input = null;
Scanner scanner = new Scanner(System.in);//“System.in”表示键盘输入
while(true){
System.out.println("请输入字符串:");
input = scanner.next();
if(input != "e" || input != "exit")break;
input = input.toUpperCase();
System.out.println(input);
}
}
//方法二:使用System.in实现。System.in ---> 转换流 ---> BufferedReader的readLine()
//提示:在idea中使用@Test测试方法中是无法通过System.in控制台中进行输入操作,因此需要在main方法中使用
@Test
public void test1(){
BufferedReader br = null;
try {
//单层处理流
InputStreamReader isr = new InputStreamReader(System.in);//使用转换流把字节输入流转换成字符输入流
//双层处理流
br = new BufferedReader(isr);
while (true) {
System.out.println("请输入字符串:");
String data = br.readLine();//只读取一行数据,只要一回车这行数据就被该变量data接收了
if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {//把字符写在前面更好的控制确定的数据源
System.out.println("程序结束");
break;
}
String upperCase = data.toUpperCase();//转换成大写的字符串
System.out.println(upperCase);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
打印流
- 实现将基本数据类型的数据格式转化为字符串输出
- 打印流:PrintStream和PrintWriter
- 提供了一系列重载的print()和println()方法,用于多种数据类型的输出
- PrintStream和PrintWriter的输出不会抛出IOException异常
- PrintStream和PrintWriter有自动flush功能
- PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用PrintWriter 类。
- System.out返回的是PrintStream的实例,这也是我们能够使用System.out.printIn()/System.out.print()重载方法的原因
package javase15;
import org.junit.Test;
import java.io.*;
public class IO20 {
//PrintStream字节打印流,并修改标准输出流默认的输出方法
@Test
public void test2() {
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(new File("F:\\java\\Work2\\JavaSenior\\新建文本文件.txt"));
// 创建打印输出流,true设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
ps = new PrintStream(fos, true);
if (ps != null) {
System.setOut(ps);// 把标准输出流默认的控制台输出改成输出到指定文件文件
}
for (int i = 0; i <= 255; i++) { // 输出所有的ASCII字符到指定文件中
System.out.print((char) i);
if (i % 50 == 0) { // 每50个数据一行
System.out.println(); // 换行,并刷新缓冲区
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ps != null) {
ps.close();
}
}
}
//字符打印流,在需要写入字符而不是写入字节的情况下,应该使用PrintWriter类。
@Test
public void test1(){
PrintWriter ps = null;
try {
FileOutputStream fos = new FileOutputStream(new File("F:\\java\\Work2\\JavaSenior\\新建文本文件.txt"));
// 创建打印输出流,true设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
ps = new PrintWriter(fos, true);
for (int i = 0; i <= 255; i++) { // 输出所有的ASCII字符到指定文件中
ps.print((char) i);
if (i % 50 == 0) { // 每50个数据一行
ps.println(); // 换行,并刷新缓冲区
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ps != null) {
ps.close();
}
}
}
}
数据流(处理流之一)
- 为了方便地操作Java语言的基本数据类型和String的数据读取和写入,可以使用数据流。
- 数据流有两个类:(用于读取和写出基本数据类型、String类的数据)
- DataInputStream和DataOutputStream
- 分别“套接”在InputStream和OutputStream子类的流上
- DataInputStream中的方法
boolean readBoolean() byte readByte() char readChar() float readFloat() double readDouble() short readShort() long readLong() int readInt() String readUTF() void readFully(byte[s] b)
- DataOutputStream中的方法
- 将上述的方法的read改为相应的write即可。
package javase15;
import org.junit.Test;
import java.io.*;
public class IO16 {
@Test
public void test1() {
DataOutputStream dos = null;
DataInputStream dis = null;
//将内存中的字符串、基本数据类型的变量写出到文件中
try {
dos = new DataOutputStream(new FileOutputStream("F:\\java\\Work2\\JavaSenior\\新建文本文件.txt"));
//输出操作,查看输出结果不是直接打开文本文件,而是使用数据流的输入流查看
dos.writeUTF("刘刚");//通过UTF编译器将"刘刚"字符串写到指定文件新建文本文件.txt中,下面一样
dos.flush();//刷新操作,将内存中的数据写入文件
dos.writeInt(23);
dos.flush();
dos.writeBoolean(true);
dos.flush();
//将文件中存储的基本数据类型变量和字符串读取到内存中,保存在变量中
//注意:读取不同类型的数据的顺序要与当初写入文件时,保存的数据的顺序一致!
dis = new DataInputStream(new FileInputStream("F:\\java\\Work2\\JavaSenior\\新建文本文件.txt"));
String name = dis.readUTF();
int age = dis.readInt();
boolean isMale = dis.readBoolean();
System.out.println("name = " + name);
System.out.println("age = " + age);
System.out.println("isMale = " + isMale);
} catch (IOException e) {
e.printStackTrace();
} finally {
//关流
if (dos != null) {
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (dis != null) {
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
对象流(处理流之一)
对象序列化机制的理解
- ObjectInputStream和OjbectOutputSteam
- 作用:用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源(例如电脑磁盘)中,也能把对象从数据源中还原回来。
- 序列化:用ObjectOutputStream类保存基本类型数据或对象的机制
- 反序列化:用ObjectInputStream类读取基本类型数据或对象的机制
- ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量
- 对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点(序列化过程)。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象(反序列化过程)
- 序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原
- 序列化是RMI(Remote Method Invoke –远程方法调用)过程的参数和返回值都必须实现的机制,而RMI 是JavaEE的基础。因此序列化机制是JavaEE平台的基础
- 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。否则,会抛出NotSerializableException异常,通过源码查看String已经实现了序列化
- Serializable
- Externalizable
- 提示:如果某个类的属性不是基本数据类型或String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的Field 的类也不能序列化,会报错
代码演示序列化和反序列化Java基本数据类型或String 类型
package javase15;
import org.junit.Test;
import java.io.*;
public class IO17 {
/**
* 序列化过程:将内存中的java对象保存到磁盘中或通过网络传输出去
* 使用ObjectOutputStream实现
*/
@Test
public void test(){
ObjectOutputStream oos = null;
try {
//创造流,这个文件和数据流一样,是不能直接打开来看的,要通过输入流来查看
oos = new ObjectOutputStream(new FileOutputStream("F:\\java\\Work2\\JavaSenior\\新建文本文件.data"));
//制造对象,并通过writeObject方式将Object类型及其子类对象按顺序序列化到指定文件中
oos.writeObject(new String("秦始皇陵欢迎你"));
oos.writeObject(new Integer(123));
//刷新操作
oos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(oos != null){
//3.关闭流
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 反序列化:将磁盘文件中的对象还原为内存中的一个java对象
* 使用ObjectInputStream来实现
*/
@Test
public void test2(){
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("F:\\java\\Work2\\JavaSenior\\新建文本文件.data"));
//读取对象,每次调用readObject()都会按序列化的顺序,也就是先进先出的顺序读取指定文件的一个对象
Object obj1 = ois.readObject();
Object obj2 = ois.readObject();
System.out.println(obj1);//秦始皇陵欢迎你
System.out.println(obj2);//123
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if(ois != null){
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
自定义类实现序列化与反序列化操作
-
若某个类实现了
Serializable
接口,该接口为标识接口,标识该类的对象就是可序列化的 -
类实现接口后还要在实现类中添加一个必要属性static final long serialVersionUID = 42L,表示该类的序列号(自定义异常的时候提及到),权限修饰符可以public或private
-
序列号是为了序列化和反序列化时标识你是具体的哪个类
- 强调:如果某个类的属性不是基本数据类型或String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的Field 的类也不能序列化,会报错
代码演示自定义类
package javase15;
import org.junit.Test;
import java.io.*;
public class IO18 {
/**
* 序列化过程:将内存中的java对象保存到磁盘中或通过网络传输出去
* 使用ObjectOutputStream实现
*/
@Test
public void test(){
ObjectOutputStream oos = null;
try {
//创造流,这个文件和数据流一样,是不能直接打开来看的,要通过输入流来查看
oos = new ObjectOutputStream(new FileOutputStream("F:\\java\\Work2\\JavaSenior\\新建文本文件.data"));
//制造对象,并通过writeObject方式将Object类型及其子类对象按顺序序列化到指定文件中
oos.writeObject(new Person("小明",18 ));
oos.writeObject(new Person("小红",19 ));
//刷新操作
oos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(oos != null){
//3.关闭流
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 反序列化:将磁盘文件中的对象还原为内存中的一个java对象
* 使用ObjectInputStream来实现
*/
@Test
public void test2(){
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("F:\\java\\Work2\\JavaSenior\\新建文本文件.data"));
//读取对象,每次调用readObject()都会按序列化的顺序,也就是先进先出的顺序读取指定文件的一个对象
Object obj1 = ois.readObject();
Object obj2 = ois.readObject();
System.out.println(obj1);//Person{name='小明', age=18}
System.out.println(obj2);//Person{name='小红', age=19}
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if(ois != null){
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
class Person implements Serializable {//如果某个类的属性不是基本数据类型或String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型的Field 的类也不能序列化
public static final long serialVersionUID = 475463534532L;
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", 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;
}
}
serialVersionUID的序列号的补充说明
- 凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:
- private static final long serialVersionUID;
- serialVersionUID用来表明类的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容(是否是指定类的指定版本)。
- 如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID可能发生变化。故建议,显式声明。
- 简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,哪怕是你的类发生了改变,反序列化时只是值丢失或者值为默认而已。否则就会出现序列化版本不一致的InvalidCastException异常。
随机存取文件流
- RandomAccessFile 声明在java.io包下,但直接继承于java.lang.Object类。并且它实现了DataInput、DataOutput这两个接口,也就意味着该类即是输出流又是输入流,可以读也可以写,通过RandomAccessFile构造器的mode参数设定它的功能范围(下面会提到)。
- RandomAccessFile 类支持“随机访问” 的方式,程序可以直接跳到文件的任意地方来读、写文件
- 支持对部分内容或者指定位置进行读写操作,因为seek方法可以控制指针
- 可以向已存在的文件内容进行覆盖从头到尾覆盖,如果没覆盖完之前的内容也会被保留
- 如果不存在的文件输出是会自动创建
- 不推荐对文件内容进行操作,底层是字节流,因为中文占用3个字节,在中文中插入的时候会有逻辑判断异常且乱码
- RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile类对象可以自由移动记录指针:
- long getFilePointer():获取文件记录指针的当前位置
- void seek(long pos):以字节为单位,将指针移动到文件内容指定下标位置pos,然后在其之前开始进行操作
- 构造器
- public RandomAccessFile(Filefile, Stringmode)
- public RandomAccessFile(Stringname, Stringmode)
- 创建RandomAccessFile类实例需要指定一个mode 参数,该参数指定RandomAccessFile的访问模式:
- r: 以只读方式打开
- rw:打开以便读取和写入
- rwd:打开以便读取和写入;同步文件内容的更新
- rws:打开以便读取和写入;同步文件内容和元数据的更新
- 如果模式为只读r。则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常。
如果模式为rw读写。如果文件不存在则会去创建文件,如果存在则不会创建。
jdk1.6中,如果模式为rw读写模式,数据不会立即写到硬盘中,而rwd,数据会立即被写入硬盘,如果写数据过程中发生异常,rwd模式中已经被write的数据会被保存到硬盘,而rw模式则会全部丢失。
package javase15;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class IO19 {
//复制文件
@Test
public void test() {
RandomAccessFile raf1 = null;
RandomAccessFile raf2 = null;
try {
//创建RandomAccessFile类实例需要指定一个mode 参数,该参数指定RandomAccessFile的访问模式
raf1 = new RandomAccessFile(new File("F:\\java\\Work2\\JavaSenior\\文件1.png"), "r");//只读模式
raf2 = new RandomAccessFile(new File("F:\\java\\Work2\\JavaSenior\\文件2.png"), "rw");//读写,非同步更新模式
//文件赋值,将已存在的文件1.png复制成文件2.png
byte[] buffer = new byte[1024];
int len;
while ((len = raf1.read(buffer)) != -1) {
raf2.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (raf1 != null) {
try {
raf1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (raf2 != null) {
try {
raf2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//覆盖内容
@Test
public void test2() throws IOException {
RandomAccessFile raf1 = new RandomAccessFile("F:\\java\\Work2\\JavaSenior\\hello.txt", "rw");
raf1.write("xyz".getBytes());//默认可以向已存在的文件内容进行覆盖从头到尾覆盖,如果没覆盖完之前的内容也会被保留,如果不存在的文件输出是会自动创建
raf1.close();
}
//在文件指定位置插入并且覆盖内容,还是不推荐,底层是字节流,因为中文占用3个字节,在中文中插入的时候会有逻辑判断异常且乱码
@Test
public void test3() throws IOException {
RandomAccessFile raf1 = new RandomAccessFile("F:\\java\\Work2\\JavaSenior\\hello.txt", "rw");
raf1.seek(3);//在所有字节下标为3之前开始插入内容,将指针移动到文件内容指定下标位置pos,然后在其之前开始进行操作
raf1.write("xyz".getBytes());//覆盖操作
raf1.close();
}
//使用RandomAccessFile实现数据的插入效果
@Test
public void test4() throws IOException {
RandomAccessFile raf1 = new RandomAccessFile("F:\\java\\Work2\\JavaSenior\\hello.txt", "rw");
raf1.seek(3);//将指针调到角标为3的位置
//保存指针3后面的所有数据到StringBuilder中,但是这种方式对于字节流来说容易乱码,因为在UTF-8中,一个中文字符占用3个字节,会出现中文字符被分割
// StringBuilder builder = new StringBuilder((int) new File("F:\\java\\Work2\\JavaSenior\\hello.txt").length());
// byte[] buffer = new byte[20];
// int len;
// while((len = raf1.read(buffer)) != -1){
// builder.append(new String(buffer,0,len)) ;
// }
//(尽量防止乱码,虽然还是会出现乱码,因为指针是以字节为单位的)将StringBuilder替换为ByteArrayOutputStream
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[10];
int len;
while ((len = raf1.read(buffer)) != -1) {
//把所有字节写到ByteArrayOutputStream类型变量baos中,当字节不够的时候会不断扩容,直到将要读取的所有数据存到baos为止,就不会像String一样分开转换字符串保存,减少乱码现象
baos.write(buffer, 0, len);
}
//调回指针,写入“xyz”
raf1.seek(3);
raf1.write("xyz".getBytes());
//将ByteArrayOutputStream类型变量baos中的数据写入到文件中,乱码问题就解决了
raf1.write(baos.toString().getBytes());
raf1.close();
}
}
(了解)NIO.2中Path、Paths、Files类的使用
- Java NIO (New IO,Non-Blocking IO)是从Java 1.4版本开始引入的一套新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的(IO是面向流的)、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
- Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
|-----java.nio.channels.Channel
|-----FileChannel:处理本地文件
|-----SocketChannel:TCP网络编程的客户端的Channel
|-----ServerSocketChannel:TCP网络编程的服务器端的Channel
|-----DatagramChannel:UDP网络编程中发送端和接收端的Channel
- 随着JDK 7 的发布,Java对NIO进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为NIO.2。因为NIO 提供的一些功能,NIO已经成为文件处理中越来越重要的部分。
- 早期的Java只提供了一个File类来访问文件系统,但File类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出错时仅返回失败或者false,并不会提供异常信息。
- NIO. 2为了弥补这种不足,引入了Path接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path可以看成是File类的升级版本,实际引用的资源也可以不存在。
- 在以前IO操作都是这样写的:
import java.io.File; File file = new File(“index.html”);
-
但在Java7 中,我们可以这样写:
import java.nio.file.Path; import java.nio.file.Paths; Path path = Paths.get(“index.html”);
- 同时,NIO.2在java.nio.file包下还提供了Files、Paths工具类,Files包含了大量静态的工具方法来操作文件;Paths则包含了两个返回Path的静态工厂方法。
- Paths类提供的静态get()方法用来获取Path对象:
- static Pathget(String first, String … more) : 用于将多个字符串串连成路径
- static Path get(URI uri): 返回指定uri对应的Path路径
path常用方法
Files常用静态方法(类似于Collections):用于判断、操作内容、操作文件和目录
代码演示Path和Files常用方法
package javase15;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class IO21 {
//演示path常用方法
@Test
public void test1(){
Path path1 = Paths.get("F:\\java\\Work2\\JavaSenior\\hello.txt");//类似于new FIle(String filePath),创建一个path对象
System.out.println(path1.toString());//F:\java\Work2\JavaSenior\hello.txt,返回调用Path对象的字符串表示形式
System.out.println(path1.startsWith("F:\\java\\Work2\\JavaSenior"));//true,判断是否以path路径开始
System.out.println(path1.endsWith("F:\\java\\Work2\\JavaSenior\\hello.txt"));//true.判断是否以path路径结束
System.out.println(path1.isAbsolute());//true,判断是否为绝对路径
System.out.println(path1.getParent());//F:\java\Work2\JavaSenior,返回Path对象的所在的根目录路径,数据类型为Path
System.out.println(path1.getRoot());//F:\,返回文件所在的盘符(返回Path对象的根路径,不是根目录),数据类型为Path
System.out.println(path1.getFileName());//获取与Path对象关联的文件完整名(文件名.后缀),数据类型为Path
System.out.println(path1.getNameCount());//4,返回Path对象根路径后面元素的数量
System.out.println(path1.getName(0));//java,指定索引返回Path对象根路径后面元素的名称,数据类型为Path
System.out.println(path1.equals(path1.toAbsolutePath()));//true,作为绝对路径返回调用Path对象
System.out.println(path1.resolve(Paths.get("E:\\")));//E:\,合并两个路径,返回合并后路径对应的Path对象
File file1 = path1.toFile();//将Path对象转换成File类对象返回
Path path2 = file1.toPath();//将File类对象转换成Path对象
System.out.println(file1.getClass());//class java.io.File
System.out.println(path2.getClass());//class java.io.File
}
//Files常用静态方法,是Path对象的工具API.类似于Collections
@Test
public void test2() throws IOException {
Path path1 = Paths.get("F:\\java\\Work2\\JavaSenior", "hello.txt");
//判断
System.out.println(Files.exists(path1));//true,判断文件是否存在
System.out.println(Files.isDirectory(path1));//false,判断是否为目录
System.out.println(Files.isRegularFile(path1));//true.判断是否为文件
System.out.println(Files.isReadable(path1));//true,判断是否可读
System.out.println(Files.isWritable(path1));//true,判断文件是否可写
System.out.println(Files.notExists(path1));//false,判断文件是否不存在
//操作不明,暂时不了解
//操作
// Path copyPath = Files.copy(path1, Paths.get("F:\\java\\Work2\\JavaSenior", "hello1.txt"));//文件复制,并返回复制文件对应的Path对象,如果文件存在则会抛出FileAlreadyExistsException异常
Path path2 = Files.createDirectories(Paths.get("F:\\java\\Work2\\JavaSenior", "新建文件夹2"));//创建一个目录,并返回复制文件对应的Path对象,文件已经存在也不会抛出异常
// Path path3 = Files.createFile(Paths.get("F:\\java\\Work2\\JavaSenior", "hello2.txt"));//创建一个目录,并返回复制文件对应的Path对象,文件已经存在会抛出FileAlreadyExistsException异常
// Files.delete(Paths.get("F:\\java\\Work2\\新建文件夹"));//删除一个文件或者文件目录,如果不存在则删除失败异常NoSuchFileException,如果文件非空也会删除失败异常DirectoryNotEmptyException
// Files.deleteIfExists(Paths.get("F:\\java\\Work2\\新建文件夹22"));//删除文件如果文件存在的话才执行删除,因此不会有删除失败异常,如果文件非空也会删除失败异常DirectoryNotEmptyException
// Files.move(Paths.get("F:\\java\\Work2\\新建文件夹"),Paths.get("F:\\java\\Work2\\JavaSenior\\移动文件夹"));//移动文件,如果移动的文件有同名则存在则会抛出异常FileAlreadyExistsException
//查询
System.out.println(Files.size(Paths.get("F:\\java\\Work2\\JavaSenior", "hello1.txt")));//返回文件大小,以字节为单位
}
}
字符编码
编码表由来
计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。这就是编码表。
常见编码表
- ASCII:美国标准信息交换码。
用一个字节的7位可以表示。最后一位数用来表示符号,7位(bit)有127种情况 - ISO8859-1:拉丁码表。欧洲码表
用一个字节的八位表示 - GB2312:中国的中文编码表。
最多两个字节编码所有字符 - GBK:中国的中文编码表升级,融合了更多的中文文字符号。
最多两个字节编码 - Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。
所有的文字都用两个字节来表示。 - UTF-8:变长的编码方式,
可用1-4个字节来表示一个字符。
编码表问题
- Unicode不完美,这里就有三个问题,
- 我们已经知道,英文字母只用一个字节表示就够了
- 第二个问题是如何才能区别Unicode和ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?
- 如果和GBK等双字节编码方式一样,用最高位是1或0表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符。Unicode在很长一段时间内无法推广,直到互联网的出现。
- 面向传输的众多UTF(UCS Transfer Format)标准出现了,顾名思义,**UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。**这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
- Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的Unicode编码是UTF-8和UTF-16。UTF-8和ANSI用的比较多
UTF-8与Unicode互转具体流程
了解最早以前的只有英文数字字符、控制代码、空格与二进制之间的关系——ASCII码
在计算机内部,所有数据都是使用二进制存储。每一个二进制都代表一位(bit),这一位数有0和1两种状态,在ASCII中是由一个字节表示,也就是8位。最前面的数统一规定为0,那么ASCII编码操作的位数只有7位数,是一个7位的编码标准。因此只能规定128种不同的字符编码,在十进制中为0~127,其中包括26个小写字母、26个大写字母、10个数字、32个符号、33个控制代码(不能打印)和一个空格。
例如:空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。缺点就是:因为太少只有128种,不能代表所有字符,也就是不满足世界互联网的语言需求。
了解万能编码——Unicode码
乱码:世界上存在着多种编码方式,同一串二进制数字可以被解释成不同的符号。因此,想要保存一个文本文件,必须选择编码方式。反而言之,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。
Unicode:是一种编码方式,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,使用Unicode 没有乱码的问题。
Unicode 的缺点:Unicode 只规定了所有语言的二进制代码,却没有规定这个二进制代码应该如何存储,因此计算机无法区别Unicode 和ASCII。例如计算机无法区分两个字节一起表示一个符号还是分别表示两个符号。另外,我们知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用两个字节表示,那么每个英文字母前都必然有一个字节是0,这对于存储空间来说是极大的浪费。此时UTF-8代码诞生了
了解UTF-8 码
UTF-8 是在互联网上使用最广的一种Unicode 的实现方式。
UTF-8 是一种变长的编码方式。它可以使用1-6 个字节表示一个符号,根据不同的符号而变化字节长度。(在标准UTF-8编码中,超出基本多语言范围(BMP-Basic Multilingual Plane)的字符被编码为4字节格式,但是在修正的UTF[编码中,他们由代理编码对(surrogatepairs)表示,然后这些代理编码对在序列中分别重新编码。结果标准UTF-8编码中需要4个字节的字符,在修正后的UTF-8编码中将需要6个字节)
UTF-8的编码规则(参考如下黑色背景图):
对于单字节的UTF-8编码,该字节的最高位为0,其余7位用来对字符进行编码(等同于ASCII码)。
对于多字节的UTF-8编码,如果编码包含n 个字节,那么第一个字节的前n位为1,第一个字节的第n+1 位为0,该字节的剩余各位用来对字符进行编码。在第一个字节之后的所有的字节,都是最高两位为"10",其余6位用来对字符进行编码。
UTF-8与Unicode互转流程(重点)
存储过程
描述:
- 在文本框输入"中"一个字符,然后选择UTF-8编码方式保存,此时该文件就是用UTF-8编码方式保存的
- 使用UTF-8解码再次打开文件时发现编码无异常,“中”字也正常显示
- 如果用了不符合的编码格式解码打开文件就会乱码,例图使用ANSI编码解码打开UTF-8编码格式的文件
- "中"字符在Unicode编码表十进制是20013,Unicode表中对应的是"\u4e2d",也就是十六进制为4E2D,二进制为100 1110 0010 1101
package javase15; import org.junit.Test; import java.io.File; public class IO14 { @Test public void test1(){ int i = (int)'中';//字符串根据Unicode转十进制 System.out.println(i);//20013 String s1 = Integer.toHexString(i);//十进制转十六进制 System.out.println(s1);//4e2d String s2 = Integer.toBinaryString(i);//十进制转二进制 System.out.println(s2);//100 1110 0010 1101 } }
- 然后再将十六进制的4E2D转换成"100 1110 0010 1101"的二进制数,其中
- 4的二进制是100
- E是15,转成二进制就是1110
- 2的二进制是0010
- D是14,转换成二进制就是1101
- 如果按照Unicode编码方式存放,此时的二进制"100 1110 0010 1101"会加一个0,变成两个字节"01001110 00101101"存在计算机中,但是这样存入后读取时计算机编码时不知道怎么读,回到之前提出的疑问:"计算机无法区分两个字节一起表示一个符号还是分别表示两个符号"
- 因此我们引入UTF-8完善Unicode存储,使得既可以读又可以写,下面我们查看UTF-8的在不同的Unicode的十六进制值的范围内的存储区别,来选择应该用第几行的方式存储。我们的十六进制“4E2D”明显是属于800-FFFF之间的,因此
- 我们的"4E2D"就选择第三行的方式存储,也就是用三个字节存储我们的Unicode.然后把两个字节的数从左往右填入到第三行的xxxx....中,造出的"11100160 10111000 10101101"就是我们通过UTF-8的编码方式将“中”字存储到磁盘的三个字节
读取过程
描述:
- 通过UTF-8的格式读取数据,先通过开头的"1"的位数从左到右取出几个字节
- 取出UTF-8的XXXX的代替数,也就是Unicode的二进制数"0100111000101101"
- 把二进制数"01001110 00101101"翻译成16进制的数"4E2D"
- 将二进制数前加上"/u",也就是"/u"+"4E2D"组成"/u4E2D"
- 对照Unicode表对照翻译就是"中"字符
Idea使用第三方jar包方法
- 在项目中新建一个libs包,专门存放第三方的jar包
- 将第三方jar包放到libs包中
- 右键选择Add as Library
- 此时jar包已经导入,API可以通过import引入到java文件并使用