文章目录
- 一、字符集
- 常见字符集
- 编码、解码操作
- 二、IO流
- FileInputStream
- FileOutputStream
- FileReader
- FileWriter
- 常见问题
一、字符集
常见字符集
我们的计算机底层是不可以直接存储字符的,计算机中底层只能存储二进制(0、1),同时二进制是可以转换成十进制的。
结论:计算机底层是可以表示十进制编号。计算机可以给人类字符进行编号存储,这套编号规则就是字符集。
ASCll字符集: ASCll(American Standard Code for Information Interchange),美国信息交换标准代码,包括了数字、英文、符号。
ASCll使用1个字节存储一个字符,一个字节是8位,一共可以表示128个字符信息,对于英文,数字是够用的。
GBK:windows系统默认的码表。兼容ASCll码表,也包含了几万个汉字,并支持繁体汉字以及部分日韩文字。
注意:GBK是中国的码表,一个中文以两个字节的形式存储。但不包含世界上所有国家的文字。
Unicode码表:unicode(统一码,万国码)是计算机科学领域里的一项业界字符编码标准,容纳世界上大多数国家的所有常见的文字和符号。
由于Unicode会先通过UTF-8、UTF-16、以及UTF-32编码成二进制在存储到计算机中,最常见的是UTF-8.
注意:
1.Unicode是万国码,以UTF-8编码后一个中文一般以三个字节的形式存储。
2.UTF-8也兼容ASCll编码表
3.我们正常情况下应该使用UTF-8的字符集编码
4.编码前和编码后的字符集需要保持一致,否则会出现乱码
5.英文和数字在任何编码中都不会乱码。
这里可能有的同学会问了,编码的时候又是怎么知道你是汉字去读取三个字节,知道你是字符去读一个字节。
因为我们中文的第一个字节和英文的第一个字节是有一定的存储规律的,比如我们中文的第一个字节表示为负数,英文的第一个字节表示为正数。
编码、解码操作
String编码:
方法 | 作用 |
---|---|
byte[] getBytes() | 使用平台默认字符集将String编码为字节,存储到数组中 |
byte[] getBytes(String charsetName) | 使用指定字符集将String编码为字节,存储到字节数组中 |
String解码:
方法 | 作用 |
---|---|
String(byte[] bytes) | 使用平台默认编码来解码字节数组来构造String |
String(byte[] bytes,String charsetName) | 通过指定字符集解码指定字节数组来构造String |
public static void main(String[] args) {
//1.编码: 把字符串转为字节(使用默认的编码)
String name = "abc进大厂";
byte[] bytes = name.getBytes(); //使用默认字符集编码(UTF-8)
System.out.println(bytes.length);
System.out.println(Arrays.toString(bytes));
}
我们可以发现英文的都是正的,汉字是负的。
public static void main(String[] args) throws UnsupportedEncodingException {
//1.编码: 把字符串转为字节(使用指定的编码)
String name = "abc进大厂";
byte[] bytes = name.getBytes("GBK"); //使用指定字符集编码(GBK)
System.out.println(bytes.length);
System.out.println(Arrays.toString(bytes));
}
由于我们GBK中一个汉字占两个字节,所以我们编码后的数组长度为9.
public static void main(String[] args) throws UnsupportedEncodingException {
//1.编码: 把字符串转为字节(使用指定的编码)
String name = "abc进大厂";
byte[] bytes = name.getBytes("GBK"); //使用指定字符集编码(GBK)
System.out.println(bytes.length);
System.out.println(Arrays.toString(bytes));
//2.解码: 把字节转换为对应的字符形式(使用默认的编码)
String res = new String(bytes);
System.out.println(res);
}
由于我们编码使用的是GBK,解码使用的是UTF-8,所以我们这里的中文乱码了。
当我们统一了编码和解码方式之后,乱码的情况消失了。
二、IO流
IO流也称为输入,输出流,就是用来读写数据的。
I:表示input,数据从硬盘文件读入到内存的过程,称之输入(读)
O: 表示output,数据从内存写入硬盘文件的过程,称之输出(写)
我们java标准库的流对象,从类型上,可以分为两大类:
但是我们这里类都是抽象类,是不能直接用的,于是我们提供了操作文件的实现类
虽然这里的类很多,但是具有非常强的规律性,核心就是四个操作:
1.打开文件(构造对象)
2.关闭文件(close)
3.读文件(read),只针对 InputStream / Reader
4.写文件(write),只针对OutputStream / Writer
FileInputStream
方法 | 作用 |
---|---|
FileInputStream(File file) | 利用 File 构造文件输入流 |
FileInputStream(String name) | 利用文件路径构造文件输入流 |
public static void main(String[] args) throws FileNotFoundException {
//创建一个文件字节输入流管道与源文件接通
InputStream inputStream = new FileInputStream("./data.txt");
}
在这里我们可以直接传路径,因为会帮我们去new 一个 File对象
我们有三种读取数据的方法:
无参数:一次读取一个字节
一个参数:我们把读取到的内容填充到字节数组当中(“输出型参数”)
三个参数:指定范围,只往字节数组填充一部分
在这里我们准备一个data文件。
public static void main(String[] args) throws IOException {
//创建一个文件字节输入流管道与源文件接通
InputStream inputStream = new FileInputStream("./data.txt");
//按一次读一个字节的方式读取
int a1 = inputStream.read();
System.out.println((char)a1);
int a2 = inputStream.read();
System.out.println((char)a2);
int a3 = inputStream.read();
System.out.println((char)a3);
int a4 = inputStream.read();
System.out.println(a4);
}
这里我们可以发现,当我们文件的数据被读完之后,再去读就会返回-1,我们来看看方法。
我们这里read一次读取的是一个字节,按理说用byte接收就可以了,但实际上是用int,因为byte除了要表示0 -> 255(-128 -> 127),还有一个特殊情况,就是表示文件读取结束要返回-1.
但很明显,我们这样的读效率不高,因为我们不知道文件有多少数据,所以我们采用循环的方式来读。
public static void main(String[] args) throws IOException {
//创建一个文件字节输入流管道与源文件接通
InputStream inputStream = new FileInputStream("./data.txt");
int a;
while ((a = inputStream.read()) != -1) {
System.out.print((char)a);
}
}
当我们文件中有中文时,我们用字节输入流就会出现乱码,因为中文在UTF-8是按照3个字节存储的,而我们这里是一个一个字节读取的。
我们一次读一个字节可能出现什么问题?
1.性能较慢
2.读取中文字符输出无法避免乱码问题
于是我们有更搞笑的读取方法,每次读取一个字节数组的数据。我们输入管道,我们read()方法就相当于一次取一滴,read(byte[] byte)相当于我们用桶接,而且我们可以指定桶的大小。
public static void main(String[] args) throws IOException {
//创建一个文件字节输入流管道与源文件接通
InputStream inputStream = new FileInputStream("./data.txt");
//定义一个字节数组读取数据
byte[] buffer = new byte[1024]; //1KB
}
我们的字节数组一般指定为1024大小,也就是1KB,但是为了更好的带大家学习,我这里先不指定为1024.
文件数据
public static void main(String[] args) throws IOException {
//创建一个文件字节输入流管道与源文件接通
InputStream inputStream = new FileInputStream("./data.txt");
//定义一个字节数组读取数据
byte[] buffer = new byte[3]; //1KB
int len = inputStream.read(buffer);//返回读取了几个字节
System.out.println("读取了 " +len +"字节");
String rs = new String(buffer);
System.out.println(rs);
len = inputStream.read(buffer);//返回读取了几个字节
System.out.println("读取了 " +len +"字节");
rs = new String(buffer);
System.out.println(rs);
len = inputStream.read(buffer);//返回读取了几个字节
System.out.println("读取了 " +len +"字节");
rs = new String(buffer);
System.out.println(rs);
}
我们大家思考,为什么我们第三桶水,只读了2个字节,单打印出来了三个。
当我们读取第二桶水的时候,buffer = {a,b,c}。
当我们读取第三桶水的时候,只读了两个字节,于是只替换掉了buffer的前两个数据,buffer = {b,c,c}。
当我们再去读取的时候,就会返回-1,这时候我们不能去打印桶,因为桶里面仍然有数据,那我们该怎样去避免这样的情况呢?
方法 | 作用 |
---|---|
String(Byte[] byte,int offset,int length |
我们在new String的时候,可以使用它的这个构造方法。
这样我们就不会打印桶中的无关数据了,但是我们这样效率还是太低,我们改进为循环的方式。
public static void main(String[] args) throws IOException {
//创建一个文件字节输入流管道与源文件接通
InputStream inputStream = new FileInputStream("./data.txt");
//定义一个字节数组读取数据
byte[] buffer = new byte[3]; //1KB
int len; //记录读取的字节数
while((len = inputStream.read(buffer)) != -1) {
System.out.print(new String(buffer,0,len));
}
}
我们一次读一个字节数组可能出现什么问题?
1.性能得到了提升
2.读取中文字符输出无法避免乱码问题
那我们如何使用字节输入流解决中文乱码问题?
定义一个与文件一样大的字节数组,一次性读取完文件的全部字节。但是如果文件过大,字节数组可能会引起内存溢出。我们官方也有提供API。
方法 | 作用 |
---|---|
readAllBytes() | 一次读取文件的全部字节 |
FileOutputStream
我们刚刚介绍了从硬盘读取到内存的文件字节输入流,现在介绍从内存写入到硬盘上的字节输出流。
方法 | 作用 |
---|---|
FileOutputStream(File file) | 创建字节输出流与源文件对象接通 |
FileOutputStream(File file,boolean append) | 创建字节输出流与源文件对象接通,可追加数据 |
FileOutputStream(String filepath) | 创建字节输出流与源文件路径接通 |
FileOutputStream((String filepath),boolean append) | 创建字节输出流与源文件路径接通,可追加数据 |
我们在每次打开文件进行写数据的时候,默认是清空文件然后写,我们传入true之后可以在文件里追加数据写。
同样为我们提供了三个写数据的方法:
无参数:一次写一个字节
一个参数:写一个字节数组
三个参数:写字节数组的一部分
方法 | 作用 |
---|---|
flush() | 刷新流,还可以继续写数据 |
close() | 关闭流,释放资源,再关闭之前先刷新流。一旦关闭,就不能写数据 |
我们在写数据时,可能不能实时写到硬盘,为了避免这种情况发生,我们在每次写完数据都刷新一下。
public static void main(String[] args) throws IOException {
//创建一个文件字节输出流管道与源文件接通
OutputStream outputStream = new FileOutputStream("./data.txt");
//一次写一个字节
outputStream.write('a');
outputStream.write('b');
outputStream.write('c');
outputStream.close();
}
public static void main(String[] args) throws IOException {
//创建一个文件字节输出流管道与源文件接通
OutputStream outputStream = new FileOutputStream("./data.txt");
//一次写一个字节数组
byte[] bytes = {'a',98,99,100};
outputStream.write(bytes);
outputStream.close();
}
那我们如何写中文到硬盘呢?
public static void main(String[] args) throws IOException {
//创建一个文件字节输出流管道与源文件接通
OutputStream outputStream = new FileOutputStream("./data.txt");
//一次写一个字节数组
byte[] bytes = "我爱你中国".getBytes();
outputStream.write(bytes);
outputStream.close();
}
我们可以先将字符串转为字节数组再写。
有同学可以发现,我们写数据都是写在一行了太紧凑了,那么如何换行呢?
//写换行
outputStream.write("\r\n".getBytes());
肯定有同学疑惑,不是\n就可以实现换行,为什么还要加回车\r,\n确实在windows可以达到换行操作,但是在linux可能就不太行了,使用\r\n比较稳妥。
我们现在每次写都会清空文件之前的数据,让我们来实现一下可以直接在文件后面追加写的。
我们只需要在创建流对象的时候,第二个参数传true即可。
FileReader
字符输入流:我们刚才所介绍的字节流,对于操作中文数据不太友好,我们现在来介绍一个非常针对中文操作的流对象|
方法 | 作用 |
---|---|
FileReader(File file) | 创建字符输人流与源文件对象接通 |
FileReader(File file,Charset charset) | 创建字符输人流与源文件对象接通,指定字符集 |
FileReader(String fileName) | 创建字符输人流与源文件路径接通 |
FileReader(String fileName,Charset charset) | 创建字符输人流与源文件路径接通,指定字符集 |
同样为我们一些读数据的方法,但是是针对字符来操作的。
我们准备好数据
public static void main(String[] args) throws IOException {
//创建一个文件字符流管道与文件接通
Reader reader = new FileReader("./data.txt");
int len;
while((len = reader.read()) != -1) {
System.out.print((char)len);
}
}
如果文件数据过多时我们一个字符一个字符读会很慢,应该一次读一个字符数组。
public static void main(String[] args) throws IOException {
//创建一个文件字符流管道与文件接通
Reader reader = new FileReader("./data.txt");
char[] buffer = new char[1024];
int len;
while ((len = reader.read(buffer)) != -1) {
String str = new String(buffer,0,len);
System.out.print(str);
}
}
FileWriter
文件字符输出流
方法 | 作用 |
---|---|
FileWriter(File file) | 创建字符输出流管道与源文件对象接通 |
FileWriter(File file,boolean append) | 创建字符输出流管道与源文件接通,可追加数据 |
FileWriter(String filepath) | 创建字符输出流管道与源文件路径接通 |
FileWriter(String filepath,boolean append) | 创建字符输出流管道与源文件路径接通,可追加数据 |
public static void main(String[] args) throws IOException {
//创建一个文件字符流管道与文件接通
Writer writer = new FileWriter("./data.txt");
writer.write(97);//写一个字符
writer.write("\r\n");//换行
writer.write("中国人");//写一个字符串
writer.write("\r\n");//换行
writer.write("China".toCharArray());//写一个字符数组
writer.write("\r\n");//换行
writer.write("陕西老铁666",0,4);//写字符串的一部分
writer.write("\r\n");//换行
writer.write("油泼面嘹咋咧".toCharArray(),0,3);//写字符数组的一部分
writer.write("\r\n");//换行
writer.close();
}
如果想实现追加数据,我们可以在构造方法的第二个参数传true.
常见问题
close方法代表关闭文件,这也是一个重要的方法。
进程,在操作系统内核,使用PCB这样的数据结构来标识进程,一个线程对应一个PCB,一个进程可以对应一个PCB,也可以对应多个,我们PCB里有一个和文件息息相关的属性:文件描述符(用来记录该进程打开了那些文件。
我们每次打开一个文件就会占一个文件描述符表的一个位置,当我们关闭文件时,就会把文件描述符表的该位置释放。
如果我们没有close操作,会造成什么后果?
首先我们文件描述符表是有大小的,存在上限,虽然我们java有GC,但是GC操作可能不太及时,那么我们不手动close就可能将文件描述符表吃满,导致后面我们打开文件时,就会打开失败。
但是如果我们的程序中从始至终都在使用文件对象,那么我们不关闭也没事,因为当我们进程结束,PCB就会销毁,伴随着文件描述符也会销毁,对应的资源也会被操作系统自动回收。
给大家介绍一种关闭资源的方式,我们称为try with resources
我们将资源定义到try()括号里,这里虽然没有显示的写close,但会自动的执行close。
但不是说任何对象放到try()里都会自动释放
必须实现Closeable接口,该接口提供了close方法。