文章目录
第11章、Java IO编程
11.1 文件操作类:File
要完成文件自身的操作(如:创建、删除等),可以使用java.io.File类完成。
-
java.io.File类常用操作方法:
public File(String pathname) // 构造,传递完整文件操作路径 public File(File parent, String child) // 构造,设置父路径与子文件路径 public boolean createNewFile() throws IOException // 普通,创建新文件 public boolean exists() // 普通,判断给定路径是否存在 public boolean delete() // 普通,删除指定路径的文件 public File getParenFile() // 普通,取得当前路径的父路径 public boolean mkdirs() // 普通,创建多级目录 public long length() // 普通,取得文件大小,一字节为单位返回 public boolean isFile() // 普通,判断给定路径是否是文件 public boolean isDirectory() // 普通,判断给定路径是否是目录 public long lastModified() // 普通,取得文件的最后一修改日期时间 public String[] list() // 普通,列出指定目录中的全部内容 public File[] listFiles() // 普通,列出所有的路径以File类对象包装
可以发现File类中提供的方法并不涉及文件的具体内容,只针对文件本身的操作。
-
路径分隔符问题,windows系统中使用“\\”(本质为“\”)作为分隔符。而Linux系统中路径分隔符为“/”。而在Java中,使用java.io.file类中提供的路径分隔符常量:public static final String separator;该常量会在不同的操作系统中转换为适合该操作系统的路径分隔符。举例:
File file = new File("d:" + File.separator + "test.txt");
-
创建带路径的文件:创建文件前判断父路径是否存在,若存在,直接利用createNewFile()方法创建,若父路径不存在,则先用mkdirs()创建父路径,在创建文件。
package com.yootk.demo; import java.io.File; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 File file = new File("d:" + File.separator + "demo" + File.separator + "hello" + File.separator + "yootk" + File.separator + "test.txt"); // 设置文件的路径 if (!file.getParentFile().exists()) { // 现在父路径不存在 file.getParentFile().mkdirs(); // 创建父路径 } System.out.println(file.createNewFile()); // 创建新文件 } }
-
取得文件或目录信息:
package com.yootk.demo; import java.io.File; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.Date; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 File file = new File("d:" + File.separator + "my.jpg"); // 设置文件的路径 if (file.exists()) { System.out.println("是否是文件:" + (file.isFile())); System.out.println("是否是目录:" + (file.isDirectory())); // 文件大小是按照字节单位返回的数字,所以需要将字节单元转换为兆(M)的单元 // 但是考虑到小数点问题,所以使用BigDecimal处理 System.out.println("文件大小:" + (new BigDecimal((double) file.length() / 1024 / 1024) .divide(new BigDecimal(1), 2, BigDecimal.ROUND_HALF_UP)) + "M"); // 返回的日期是以long的形式返回,可以利用SimpleDateFormat进行格式化操作 System.out.println("上次修改时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") .format(new Date(file.lastModified()))); } } } /* 程序执行结果: 是否是文件:true 是否是目录:false 文件大小:0.17M 上次修改时间:2021-04-02 15:10:30 */
-
列出目录信息:
package com.yootk.demo; import java.io.File; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 File file = new File("c:" + File.separator); if (file.isDirectory()) { // 判断当前路径是否为目录 File result[] = file.listFiles() ; for (int x = 0; x < result.length; x++) { System.out.println(result[x]); // 调用toString() } } } }
11.2 字节流与字符流
使用java.io.File类虽然可以操作文件,但不能操作文件内容。想要进行文件内容的操作,就必须依靠流的概念完成。流又被分为输入输出流,而输入与输出流失一种相对的概念,关键要看参考点,比如对于键盘是输出流的信号对于主机来说是输入流。
-
数据流被分为两种:
-
字节流:InputStream(输入字节流)、OutputStream(输出字节流)
-
字符流:Reader(输入字符流)、Writer(输出字符流)
-
-
在java.io包中,四个操作流的类(InputStream、OutputStream、Reader、Writer)全部都属于抽象类,所以使用这些类时,一定要通过其子类对象向上转型来进行抽象类对象的实例化操作。
-
流的基本操作形式:
- 第一步:通过File类定义一个要操作文件的路径
- 第二步:通过字节流或字符流的子类对象为父类实例化对象
- 第三步:进行数据的读(输入)、写(输出)操作;
- 第四步:数据流都属于资源操作,资源操作最后一定要用close()方法关闭。
字节输出流:OutputStream
OutputStream类
-
java.io.OutputStream类常用方法:
public void close() throws IOException // 普通,关闭字节输出流 public void flush() throws IOException // 普通,强制刷新清空字节流缓冲区 public abstract void write(int b) throws IOException // 普通,输出单个字节 public void write(byte[] b) throws IOException // 普通,输出全部字节数组数据 public void write(byte[] b, int off,int len) throws IOException // 普通,输出部分字节数组数据,off:byte起始位置
-
java.io.OutputStream类定义:
public abstract class OutputStream extends Object implements Closeable, Flushable
可以发现OutputStream类实现了Closeable接口,而Closeable接口定义如下:
public interface Closeable extends AutoCloseable { public void close() throws IOException; }
可以发现,Closeable接口继承了AutoCloseable接口,而AutoCloseable接口(自动关闭接口)定义如下:
public interface AutoCloseable { public void close() throws IOException; }
可以发现OutputStream类、Closeable接口和AutoCloseable接口都定义有close()方法,这是因为OutputStream类为JDK1.0定的,Closeable接口为JDK1.5定义的,AutoCloseable接口为JDK1.7定义的。
-
在以往的开发中,都需要用户手动调用close()关闭资源,但实际开发中,很多开发者会忘记关闭资源,从而导致其他线程无法打开资源进行操作。所以在Java中提供了一种新的异常处理格式:
try(AutoCloseable接口子类 对象 = new AutoCloseable接口子类名称()) { 调用方法(可能会出现异常); } catch(异常类型 对象) { 异常处理 } ...[finally { 异常处理统一出口; }]
只要使用此类格式,在操作完成后用户就没必要再去调用close()方法了,系统会自动调用close()方法关闭资源。
-
自动执行close()操作:
package com.yootk.demo; class Net implements AutoCloseable { @Override public void close() throws Exception { System.out.println("*** 网络资源自动关闭,释放资源。"); } public void info() throws Exception { // 假设有异常抛出 System.out.println("*** 欢迎访问:www.Google.com"); } } public class TestDemo { public static void main(String[] args) { try (Net n = new Net()){ n.info(); } catch (Exception e) { e.printStackTrace(); } } } /* 程序执行结果: *** 欢迎访问:www.Google.com *** 网络资源自动关闭,释放资源。 */
手动调用close()关闭资源和使用自动执行close()操作都是可以的。
FileOutputStream类
OutputStream类本身是个抽象类,所以需要一个子类来实例化对象。如果要进行文件操作,则可以使用FileOutputStream子类完成操作。
-
java.io.FileOutputStream类常用方法:
public FileOutputStream(File file) throws FileNotFoundException // 构造,将内容输出到指定路径,如果文件已存在,则使用新的内容覆盖新内容 public FileOutputStream(File file, boolean append) throws FileNotFoundException // 构造,如果将boolean型参数设为true,表示追加新内容到文件中
由于输出操作主要由OutputStream类为主,所以对于FileOutputStream类只需关注其两个构造方法即可。
-
OutputStream类与FileOutputStream类继承关系:
-
文件内容输出:
package com.yootk.demo; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; public class TestDemo { public static void main(String[] args) throws Exception { // 直接抛出 // 1.定义要输出文件的路径 File file = new File("d:" + File.separator + "demo" + File.separator + "mldn.txt"); if (!file.getParentFile().exists()) { // 若文件目录不存在应该首先创建目录 file.getParentFile().mkdirs(); // 创建目录 } // 2.应该使用OutputStream和其子类进行对象的实例化,此时目录存在,文件还不存在 OutputStream output = new FileOutputStream(file); String str1 = "更多课程资源请访问:www.yootk.com。"; // 字节输出流需要使用byte类型,需要将String类对象变为字节数组 String str2 = "java学习" byte data1[] = str1.getBytes(); // 将字符串变为字节数组 byte data2[] = str2.getBytes(); output.write(data1); // 3.输出内容 for (byte b:data2) { output.write(b); } output.close(); // 4.资源操作的最后一定要进行关闭 } } // 文件中的类容: 更多课程资源请访问:www.yootk.com。java学习
在利用OutputStream向文件输入信息时,如果文件不存在,则会自动创建(不需要用户手动调用File类的createNewFile()方法。)
字节输入流:InputStream
InputStream类
在程序中如果进行文件数据的读取操作,可以使用java.io.InputStream类完成。
-
java.io.InputStream类常用方法:
public void close() throws IOException // 普通,关闭字节输出流 public int read() throws IOException // 普通,读取单个字节,返回读取到的字节内容,如果没有内容,则返回-1 public int read(byte[] b) throws IOException // 普通,将数据读取到字节数组中,同时返回读取长度,读到结尾则返回-1 public int read(byte[] b, int off,int len) throws IOException // 普通,将部分数据读取到字节数组中,同时返回读取长度,读到结尾则返回-1
注意:read(byte[] b)中byte[]数组的长度是提前决定好的,也就是说byte[]数组的长度是能读取的最大长度。
-
java.io.InputStream类的定义:
public abstract class InputStream extends Object implements Closeable
由定义可知,InputStream类实现了Closeable接口,也就是说可以利用自动关闭的异常处理结构实现自动释放资源。
FileInputStream类
InputStream类本身是个抽象类,所以需要一个子类来实例化对象。如果要进行文件操作,则可以使用FileInputStream子类完成操作。
-
java.io.FileInputStream类的构造方法:
public FileInputStream(File file) throws FileNotFoundException // 构造,设置要读取文件数据的路径
-
数据读取操作:
package com.yootk.demo; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class TestDemo { public static void main(String[] args) throws Exception { // 直接抛出 File file = new File("d:" + File.separator + "demo" + File.separator + "mldn.txt"); // 1.定义要输出文件的路径 if (file.exists()) { // 需要判断文件是否存在后才可以进行读取 InputStream input = new FileInputStream(file) ; // 2.使用InputStream进行读取 byte data [] = new byte [1024] ; // 准备出一个1024的数组 int len = input.read(data) ; // 3.进行数据读取,将内容保存到字节数组中 input.close(); // 4.关闭输入流 System.out.println(new String(data,0,len)); // 将读取出来的字节数组数据变为字符串进行输出 } } } // 程序执行结果: 更多课程资源请访问:www.yootk.com。java学习
-
采用while循环读取:
package com.yootk.demo; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class TestDemo { public static void main(String[] args) throws Exception { // 直接抛出 File file = new File("d:" + File.separator + "demo" + File.separator + "mldn.txt"); // 1.定义要输出文件的路径 if (file.exists()) { // 需要判断文件是否存在后才可以进行读取 InputStream input = new FileInputStream(file) ; // 2.使用InputStream进行读取 byte data [] = new byte [1024] ; // 准备出一个1024的数组 int foot = 0 ; // 表示字节数组的操作脚标 int temp = 0 ; // 表示接收每次读取的字节数据 // 第一部分:(temp = input.read()),表示将read()方法读取的字节内容给temp变量 // 第二部分:(temp = input.read()) != -1,判断读取的temp内容是否是-1 while((temp = input.read()) != -1) { // 3.读取数据 data[foot ++] = (byte) temp ; // 有内容进行保存 } input.close(); // 4.关闭输入流 System.out.println(new String(data,0,foot)); } } }
-
InputStream类与FileInputStream类继承关系:
字符输出流:Writer
Writer类
java.io.Writer类时从JDK1.1之后增加的,利用Writer类可以实现字符数组(包含字符串)的输出。
-
java.io.Writer类常用方法:
public void close() throws IOException // 普通,关闭字节输出流 public void flush() throws IOException // 普通,强制刷新清空字符流缓冲区 public Writer append(CharSequence csq) throws IOException // 普通,追加数据 public void write(String str) throws IOException // 普通,输出字符串数据 public void write(char[] cbuf) throws IOException // 普通,输出字符数组数据
Writer类中直接提供输出字符串数据的方法,这样就没有必要将字符串转为字节数组再输出了。
注意:在使用Writer类输出之后,一定要调用close()方法关闭输出流,否则无法正常输出。
-
java.io.Writer类的定义:
public abstract class Writer extends Object implements Appendable, Closeable, Flushable
从定义可以发现,Writer类不仅实现了Closeable,和Flushable接口,还实现了Appendable接口。
Appendable接口定义如下:
public interface Appendable { public Appendable append(cahr c) throws IOException; public Appendable append(CharSequence csq) throws IOException; public Appendable append(CharSequence csq, int start, int end) throws IOException; }
-
Writer类的继承关系图:
FileWriter类
Writer类本身是个抽象类,所以需要一个子类来实例化对象。如果要进行文件操作,则可以使用FileWriter子类完成操作。
-
java.io.FileWriter类构造方法:
public FileWriter(File file) throws IOException // 构造,设置输出文件 public FileWriter(File file, boolean append) throws IOException // 构造,设置输出文件以及是否进行数据追加
-
使用Writer实现内容输出:
package com.yootk.demo; import java.io.File; import java.io.FileWriter; import java.io.Writer; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 File file = new File("d:" + File.separator + "demo" + File.separator + "mldn.txt"); // 1.定义要输出文件的路径 if (!file.getParentFile().exists()) { // 判断目录是否存在 file.getParentFile().mkdirs(); // 创建文件目录 } Writer out = new FileWriter(file); // 2.实例化了Writer类的对象 String str = "更多课程请访问:www.yootk.com。"; // 定义输出内容 out.write(str); // 3.输出字符串数据 out.write("Java学习"); out.close(); // 4.关闭输出流 } } // 文件中的类容: 更多课程资源请访问:www.yootk.com。Java学习
字符输入流:Reader
Reader类
java.io.Reader类是实现字符数据输入的操作类,在金乡数据读取时可以直接使用字符数据进行操作。
-
java.io.Reader类的常用方法:
public void close() throws IOException // 普通,关闭字节输入流 public int read() throws IOException // 普通,读取单个字符或字节 public int read(char[] cbuf) throws IOException // 普通,读取数据到字符数组中,返回读取长度 public long skip(long n) throws IOException // 普通,跳过字节长度
在Writer类中有直接输出字符串的操作,而Reader类中没有直接返回字符串的操作;这是因为如果Writer如果提供直接全部读取数据的方法,而所读取的文件非常大,就有可能造成内存溢出问题。
-
java.io.Reader类的定义:
public abstract class Reader extends Object implements Readable, Closeable
其中,Readable接口的定义:
public interface Readable { public int read(CharBuffer cb) throws IOException; }
在Readable接口中定义了read()方法,可以将数据保存再CharBuffer对象中,也就是说利用此类就可以代替字符数组的操作。
-
Reader类的继承关系:
FileReader类
Reader类本身是个抽象类,所以需要一个子类来实例化对象。如果要进行文件操作,则可以使用FileReader子类完成操作。
-
java.io.FileReader类的构造方法:
public FileReader(File file) throws FileNotFoundException // 构造,定义要读取的文件路径
-
使用Reader读取数据:
package com.yootk.demo; import java.io.File; import java.io.FileReader; import java.io.Reader; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 File file = new File("d:" + File.separator + "demo" + File.separator + "mldn.txt"); // 1.定义要输出文件的路径 if (file.exists()) { Reader in = new FileReader(file) ; // 2.为Reader类对象实例化 char data [] = new char [1024] ; // 开辟字符数组,接收读取数据 int len = in.read(data) ; // 3.进行数据读取 in.close(); // 4.关闭输入流 System.out.println(new String(data, 0, len)); } } } // 程序执行结果: 更多课程资源请访问:www.yootk.com。Java学习
字节流与字符流的区别
-
在文件操作中,字节流与字符流最大的区别是:
- 字节流直接与终端文件进行数据交互,使用OutputStream输出数据时,即使最后没有调用close()方法关闭输出流,内容也可以正常输出。
- 字符流需要将数据经过缓冲区处理才与终端文件数据交互,在使用Writer输出数据时,如果执行到最后不调用close方法关闭输出流,就表示在缓冲区处理的内容不会被强制性清空,所以就不会输出数据。
-
如果有特殊情况不能关闭字符输出流,可以使用flush()方法强制清空缓冲区,这样就能正常输出了。
-
使用flush()强制清空字符流缓冲区:
package com.yootk.demo; import java.io.File; import java.io.FileWriter; import java.io.Writer; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 File file = new File("d:" + File.separator + "demo" + File.separator + "mldn.txt"); // 1.定义要输出文件的路径 if (!file.getParentFile().exists()) { // 判断目录是否存在 file.getParentFile().mkdirs(); // 创建文件目录 } Writer out = new FileWriter(file); // 2.实例化了Writer类的对象 String str = "更多课程请访问:www.yootk.com"; // 定义输出内容 out.write(str); // 3.输出字符串数据 out.flush(); // 强制刷新缓冲区 } }
-
实际开发中流的选用原则:在开发中,如果要处理中文时优先考虑字符流,如果没有中文问题,建议使用字节流。
11.3 转换流
虽然字节流和字符流是两种不同的数据流操作,但这两种流彼此间可以通过InputStreamReader类和OutputStreamWriter类实现相互转换的。
-
java.io.InputStreamReader类:
-
定义:
public class InputStreamReader extends Reader
-
构造方法:
public InputStreamReader(InputStream in)
从上可知,InputStreamReader类的构造方法中接收的是InputStream类的对象,而InputStreamReader类又继承了Reader类,所以InputStreamReader类对象可以向上转型为Reader类对象。从而完成从InputStream类对象到Reader的转型。
-
-
java.io.OutputStreamWriter类:
-
定义:
public class OutputStreamWriter extends Writer
-
构造方法:
public OutputStreamWriter(OutputStream out)
从上可知,OutputStreamWriter类的构造方法中接收的是OutputStream类的对象,而OutputStreamWriter类又继承了Writer类,所以OutputStreamWriter类对象可以向上转型为Writer类对象。从而完成从OutputStreamWriter类对象到Writer的转型。
-
-
实现输出流转换:
package com.yootk.demo; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 File file = new File("d:" + File.separator + "demo" + File.separator + "mldn.txt"); // 1.定义要输出文件的路径 if (!file.getParentFile().exists()) { // 判断父路径是否存在 file.getParentFile().mkdirs() ; // 创建父路径 } OutputStream output = new FileOutputStream(file) ; // 字节流 // 将OutputStream类对象传递给OutputStreamWriter类的构造方法,而后向上转型为Writer Writer out = new OutputStreamWriter(output) ; out.write("更多课程请访问:www.yootk.com"); // Writer类的方法 out.flush(); out.close(); } }
本程序利用OutputStreamWriter类将字节输出流转换为字符输出流,这样就可以方便地实现字符数据的输出。
-
文件操作类的继承结构:
通过继承关系可以发现,FileWriter与FileReader都是转换流OutputStreamWriter和InputStreamReader的子类,也就证明所有的要读取的文件数据都是字节数据,所有的字符都是在内存中处理后得到的。
11.4 案例:文件复制
-
现在要求实现一个文件的复制操作,在复制的过程中利用初始化参数设置复制的原路径与目标路径,同时在本程序执行时可以复制任何文件,例如:图片、视频、文本等。
-
对于此程序的要求,首先必须确认要使用何种数据流进行操作。由于程序要求可以复制任意类型的文件,所以很明显必须利用字节流(InputStream、OutputStream)类完成。
-
而具体的复制操作实现,有以下两种做法。
- 做法一:将所有的文件内容先一次性读取到程序中,再一次性输出。这种实现方式有一个缺陷:如果要读取的文件量过大,就会造成程序的崩溃;
- 做法二:采用边读取边输出的操作万式,每次从源文件输入流中读取部分数据,而后将这部分数据交给输出流输出,这样的做法不会占用较大内存空间,但是会适当损耗一些时间(可以通过限制文件大小来避免此类问题)。
-
做法二具体实现流程:
-
文件复制实现代码:
package com.yootk.demo; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; public class CopyDemo { public static void main(String[] args) throws Exception { long start = System.currentTimeMillis() ; // 取得复制开始的时间 if (args.length != 2) { // 初始化参数不足2位 System.out.println("命令执行错误!"); System.exit(1); // 程序退出执行 } // 如果输入参数正确,应该进行源文件有效性的验证 File inFile = new File(args[0]) ; // 第一个为源文件路径 if (!inFile.exists()) { // 源文件不存在 System.out.println("源文件不存在,请确认执行路径。"); System.exit(1); // 程序退出 } // 如果此时源文件正确,就需要定义输出文件,同时要考虑到输出文件有目录 File outFile = new File(args[1]) ; if (!outFile.getParentFile().exists()) { // 输出文件路径不存在 outFile.getParentFile().mkdirs() ; // 创建目录 } // 实现文件内容的复制,分别定义输出流与输入流对象 InputStream input = new FileInputStream(inFile) ; OutputStream output = new FileOutputStream(outFile) ; int temp = 0 ; // 保存每次读取的数据长度 byte data [] = new byte [1024] ; // 每次读取1024个字节 // 将每次读取进来的数据保存在字节数组里面,并且返回读取的个数 while((temp = input.read(data)) != -1) { // 循环读取数据 output.write(data, 0, temp); // 输出数组 } input.close(); // 关闭输入流 output.close(); // 关闭输出流 long end = System.currentTimeMillis() ; // 取得操作结束时间 System.out.println("复制所花费的时间:" + (end - start)); } }
11.5 字符编码
计算机中所有信息都是由二进制数据组成的,因此所有能够描述出的中文文字都是经过处理后的结果。在计算机世界中,所有语言文字都会使用编码来进行描述。例如:最常见的编码是ASCII码。
-
实际工作中最为常见的4种编码:
- GBK、GB2312:中文的国标编码,其中GBK包含简体中文与繁体中文两种,而GB2312只含简体中文。
- ISO8859-1:是国籍编码,可以描述任何文字信息(中文需要转码)
- UNICODE:是十六进制编码,但是在传递字符信息时会造成传输的数据较大;
- UTF编码(Unicode Transformtion Format):是一种UNICODE的可变长度编码,常见的编码为UTF-8编码
-
在项目开发中往往会以UTF-8编码为主。
-
获得当前系统中的环境属性中的文件编码:
package com.yootk.demo; public class TestDemo { public static void main(String[] args) throws Exception { System.getProperties().list(System.out); // 列出全部系统属性 } } /* 程序执行结果: ... file.encoding=UTF-8 ... */
本程序的功能是列出全部的系统属性,其中会发现一个”file.encoding“的属性名称,此属性定义的是文件的默认编码,可以发现这里是:file.encoding=UTF-8。因此当程序向文件输出信息时,文件就会使用UTF-8编码,而文件内容也应该使用UTF-8编码,此处如果强行修改为其他编码,就会出现乱码。
-
程序乱码:
package com.yootk.demo; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; public class TestDemo { public static void main(String[] args) throws Exception { File file = new File("D:" + File.separator + "mldn.txt"); OutputStream output = new FileOutputStream(file); // 强制改变文字的编码,此操作可以通过String类的getBytes()方法实现 output.write("更多课程请访问:www.yootk.com".getBytes("ISO8859-1")); output.close(); } } // 文件内容为: ????????www.yootk.com
11.6 内存流
在流的操作在除了进行文件的输入和输出操作外,还可以针对内存进行同样的操作,假设一种应用需要进行IO操作,但是又不希望在磁盘上产生一些文件时,就可以将内存当做一个临时文件进行操作。
-
在Java中,针对内存操作提供了以下两组类:
- 字节内存流:ByteArrayInputSTream(内存字节输入流)、ByteArrayOutputStream(字节内存输出流)
- 字符内存流:CharArrayReader(字符内存输入流)、CharArrayWriter(字符内存输出流)
-
两种内存流的继承关系图:
-
字节内存流构造:
-
ByteArrayInputSTream类构造:
public ByteArrayInputSTream(byte[] buf); // 构造,创建字节输入内存流对象
-
ByteArrayOutputStream类构造:
public ByteArrayOutputStream(); // 构造,创建字节输出内存流对象
注意与FileInputSTream、FileOutputSTream、FileReader、FileWriter类的构造方法做对比。(这些构造都需要接收File类对象)
-
-
案例:使用内存流实现一个小写字母转大写字母的操作,要求:
- 不使用String类中提供的toUpperCase()方法。
- 而使用Character包装类中的:
- public static char toLowerCase(char ch)
- public static char toLowerCase(int codePoint) // 利用字母编码转换
- public static char toUpperCase(char ch)
- public static char toUpperCase(int codePoint) // 利用字母编码转换
-
具体操作代码:
package com.yootk.demo; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 String str = "www.yootk.com & www.MLDN.cn"; // 要求被转换的字符串 // 本次将通过内存操作流实现转换,先将数据保存在内存流里面,再从里面取出每一个数据 // 将所有要读取的数据设置到内存输入流中,本次利用向上转型为InputStream类实例化 InputStream input = new ByteArrayInputStream(str.getBytes()); // 为了能够将所有的内存流数据取出,可以使用ByteArrayOutputStream OutputStream output = new ByteArrayOutputStream(); int temp = 0; // 读取每一个字节数据 // 经过此次循环后,所有的数据都将保存在内存输出流对象中 while ((temp = input.read()) != -1) { // 每次读取一个数据 // 将读取进来的数据转换为大写字母,利用Character.toUpperCase()可以保证只转换字母 output.write(Character.toUpperCase(temp)); // 字节输出流 } System.out.println(output); // 调用toString()方法 input.close(); // 关闭输入流 output.close(); // 关闭输出流 } }
-
ByteArrayOutputStream类中特有操作:toByteArray()方法
public byte[] toByteArray() // 将内存输出流中的数据转为字节数组形式
此方法可以将所有暂时保存在内存输出流中的字节数据全部以字节数组的形式返回,利用这个方法就可以实现多个文件的合并读取操作。
-
实现多个文件的合并读取操作
package com.yootk.demo; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class TestDemo { public static void main(String[] args) throws Exception { // 异常简化处理 File fileA = new File("D:" + File.separator + "infoa.txt"); // 文件路径 File fileB = new File("D:" + File.separator + "infob.txt"); // 文件路径 InputStream inputA = new FileInputStream(fileA); // 字节输入流 InputStream inputB = new FileInputStream(fileB); // 字节输入流 ByteArrayOutputStream output = new ByteArrayOutputStream(); // 内存输出流 int temp = 0; // 每次读取一个字节 while ((temp = inputA.read()) != -1) { // 循环读取数据 output.write(temp); // 将数据保存到输出流 } while ((temp = inputB.read()) != -1) { // 循环读取数据 output.write(temp); // 将数据保存到输出流 } // 现在所有的内容都保存在了内存输出流里面,所有的内容变为字节数组取出 byte data[] = output.toByteArray(); // 取出全部数据 output.close(); // 关闭输出流 inputA.close(); // 关闭输入流 inputB.close(); // 关闭输入流 System.out.println(new String(data)); // 字节转换为字符串输出 } }
11.7 打印流
在java.io.OutputStream类是Java中最核心的输出操作,但这也会存在一些问题,即所有的输出数据必须为字节(byte)类型,也就是说其他数据类型必须先转为字节类型才能进行输出。为解决这个问题,Java在java.io中又提供了一组打印流(字节打印流:PrintStream,字符打印流:PrintWriter)。
打印流设计思想
-
打印流实现原理如图所示:
从图中可以看出,打印流核心思想就是首先包装一个OutputStream类的实例化对象,然后在打印流的内部自动完成其他数据到字节数组的转换操作。也就是说OutputStream的本质功能没有变。
-
以上这种没有改变原本类的本质,但操作形式变得更加多样化,也更加方便用户使用,这样的设计模式就属于装饰设计模式
-
定义打印流工具类:
package com.yootk.demo; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; class PrintUtil { // 实现专门的输出操作功能 private OutputStream output ; // 输出只能依靠OutputStream /** * 输出流的输出目标要通过构造方法传递 * @param output */ public PrintUtil(OutputStream output) { this.output = output ; } public void print(int x) { // 输出int型数据 this.print(String.valueOf(x)); // 调用本类字符串的输出方法 } public void print(String x) { try { // 采用OutputStream类中定义的方法,将字符串转变为字节数组后输出 this.output.write(x.getBytes()); } catch (IOException e) { e.printStackTrace(); } } public void print(double x) { // 输出double型数据 this.print(String.valueOf(x)); } public void println(int x) { // 输出数据后换行 this.println(String.valueOf(x)); } public void println(String x) { // 输出数据后换行 this.print(x.concat("\n")); } public void println(double x) { this.println(String.valueOf(x)); } public void close() { // 输出流关闭 try { this.output.close(); } catch (IOException e) { e.printStackTrace(); } } } public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 PrintUtil pu = new PrintUtil(new FileOutputStream(new File("d:" + File.separator + "yootk.txt"))); pu.print("优拓教育:"); pu.println("www.yootk.com"); pu.println(1 + 1); pu.println(1.1 + 1.1); pu.close(); } }
打印流
-
在java.io包中提供了两个数据的打印流操作类:
- PrintStream:字节打印流
- PrintWriter:字符打印流
-
以java.io.PrintStream类为例,其继承关系如图:
PrintStream类与PrintWriter类在使用上完全一样,方法功能也一样。
-
java.io.PrintStream类常用方法:
public PrintStream(OutputStream out) // 构造,通过已有的OutputStream对象确定输出目标 public void print(数据类型 参数) // 普通,可输出常见的各种数据类型 public void println(数据类型 参数) // 普通,可输出常见的各种数据类型,并追加一个换行。
PrintStream类的改进
-
从JDK1.5开始,Java为PrintStream类增加了格式化输出的支持方法:
public PrintStream printf(String format, Object... args)
利用此方法,可以使用像C语言那样的数据标记实现内容填充,常见的输出标记:整数(%d)、字符串(%s)、小数(%m.nf)、字符(%c)等。
-
格式化输出:
package com.yootk.demo; import java.io.File; import java.io.FileOutputStream; import java.io.PrintStream; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 String name = "李兴华"; int age = 19; double score = 59.95891023456; PrintStream pu = new PrintStream(new FileOutputStream(new File("d:" + File.separator + "yootk.txt"))); pu.printf("姓名:%s,年龄:%d,成绩:%5.2f", name, age, score); pu.close(); } } // 文件内容:姓名:李兴华,年龄:19,成绩:59.96
-
String.format()方法:格式化字符串
public static String format(String format, Object... args)
package com.yootk.demo; public class TestDemo { public static void main(String[] args) throws Exception { String name = "李兴华"; int age = 19; double score = 59.95891023456; String str = String.format("姓名:%s,年龄:%d,成绩:%5.2f", name, age, score); System.out.println(str); } }
11.8 System类对IO的支持
-
在System类中除了我们常用的System.out.println()和System.out.print()外,还定义了3个与IO有关的对象常量:
public static final PrintStream err; // 常量,显示器上错误输出 public static final PrintStream out; // 常量,显示器上信息输出 public static final inputStream in; // 常量,键盘数据输入
err与out两个对象的类型都属于PrintStream类型,in对象类型属于InputStream,System.out.println()方法从本质上来讲就是调用了System类中的out常量,由于此常量类型为 PrintStream,所以可以继续调用 PrintStream类中的 print()或 println()方法。也就证明,Java的任何输出操作实际上都是IO操作。
错误输出:System.err
-
System.err是PrintStream类对象,此对象专门负责进行错误信息的输出:
package com.yootk.demo; public class TestDemo { public static void main(String[] args) throws Exception { try { Integer.parseInt("abc"); // 此处一定会发生异常 } catch (Exception e) { System.err.println(e); // 错误输出 } } } // 程序执行结果: java.lang.NumberFormatException: For input string: "abc"
信息输出:System.out
System.out是进行屏幕输出的操作对象,是一个PrintStream类实例化对象,并且由JVM负责该对象的实例化。
系统输入:System.in
在Java中没有提供可以直接使用键盘输入的操作,而要实现此类操作必须采用IO处理的形式完成,操作的核心就是利用InputStream类的实例化对象System.in
完成。
-
实现键盘的数据输入:
package com.yootk.demo; import java.io.InputStream; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 byte data[] = new byte[1024]; // 开辟空间接收数据 System.out.print("请输入数据:"); // 信息提示,此处没有换行 int len = System.in.read(data); // 读取数据并返回长度 System.out.println("输入数据为:" + new String(data, 0, len)); } }
本程序虽然实现了键盘输入操作,但如果读取的内容超过1024个字节,就会出现漏读问题。
-
改进输入操作设计:
package com.yootk.demo; import java.io.InputStream; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出异常 InputStream input = System.in ; StringBuffer buf = new StringBuffer(); // 接收输入数据 System.out.print("请输入数据:"); // 提示信息 int temp = 0; // 接收每次读取数据长度 while ((temp = input.read()) != -1) { // 判断是否有输入数据 if (temp == '\n') { // 判断是否为回车符 break; // 停止接收 } buf.append((char) temp); // 保存读取数据 } System.out.println("输入数据为:" + buf); // 输出内容 } }
注意:当输入中文时会引起乱码,因为一个中文需要2个字节保存,而每次只读一个会把一个中文拆成两半。要想解决此问题,需要使用字符缓冲输入流完成。
11.9 字符缓冲流:BufferedReader
为了可以进行完整数据的输入操作,最好的做法是采用缓冲区的方式对输入的数据进行暂存,而后程序可以利用输入流一次性读取内容。
-
缓冲流原理:
-
java.io包中提供了以下两种缓冲流:
- 字符缓冲流:BufferedReader、BufferedWriter
- 字节缓冲流:BufferedInputStream、BufferedOutputStream
-
上面4种缓冲流最重要的是字符输入缓冲流:BufferedReader类
-
java.io.BufferedReader类继承关系为:
从继承关系可以看出,BufferedReader类是Reader类的子类
-
java.io.BufferedReader类常用方法:
public BufferedReader(Reader in) // 构造,设置字符输入流 public String readLine() throws IOEception // 普通,读取一行数据,默认以“\n”为分隔符
由于InputStreamReader类是Reader类的子类,且BufferedReader类构造需要接受Reader类,所以也可以用BufferedReader构造接收InputStreamReader类对象,又因为InputStreamReader类构造接收InputStream类对象,而System.in是InputStream类对象。
- 所以:BufferedReader buf = new BufferedReader(new InputStreamReader(System.in))
-
键盘数据输入的标准格式:
package com.yootk.demo; import java.io.BufferedReader; import java.io.InputStreamReader; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 // System.in是InputStream类对象,BufferedReader的构造方法里面需要接收Reader类对象 // 利用InputStreamReader将字节流对象变为字符流对象 BufferedReader buf = new BufferedReader(new InputStreamReader(System.in)); System.out.print("请输入数据:"); String str = buf.readLine(); // 以回车作为换行 System.out.println("输入的内容:" + str); } }
-
为什么不使用BufferedInputStream?
因为字符流方便处理中文,并且支持String返回。
-
判断输入内容:
package com.yootk.demo; import java.io.BufferedReader; import java.io.InputStreamReader; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 BufferedReader buf = new BufferedReader(new InputStreamReader(System.in)); boolean flag = true; // 编写一个循环的逻辑 while (flag) { // 判断标志位 System.out.print("请输入年龄:"); // 提示信息 String str = buf.readLine(); // 以回车作为换行 if (str.matches("\\d{1,3}")) { // 输入数据由数字组成 System.out.println("年龄是:" + Integer.parseInt(str)); flag = false ; // 退出循环 } else { System.out.println("年龄输入错误,应该由数字所组成。"); } } } }
11.10 扫描流:Scanner
-
Scanner产生的背景:在使用java.io.BufferedReader进行键盘的输入操作时会有以下两个问题
- 它读取的字符只能以字符串返回:public String readLine() throws IOEception
- 所有的分隔符都是固定的
所以从JDK1.5开始新增了Scanner类,它可以简化了键盘数据输入。
-
java.util.Scanner类的继承关系:
Scanner类没有定义在java.io包中,而是定义在了java.util包中,说明此类为工具类。
-
java.util.Scanner类的常用方法:
public Scanner(InputStream source) // 构造,接收InputStream输入流对象,此为输入源 public boolean hasNext() // 普通,判断是否有数据输入 public String next() // 普通,取出输入数据,以String形式返回 public boolean hasNextXxx() // 普通,判断是否为指定数据类型数据 public Xxx nextXxx() // 普通,取出指定数据类型的数据 public boolean hasNext(String 正则表达式) // 普通,判断输入数据是否满足该正则表达式 public String next(String 正则表达式) // 普通,取出满足指定正则表达式的数据 public Scanner useDelimiter(String pattern) // 普通,设置读取的分隔符
-
利用Scanner实现键盘数据输入:
package com.yootk.demo; import java.util.Scanner; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 Scanner scan = new Scanner(System.in); // 准备接收键盘输入数据 System.out.print("请输入内容:"); // 提示信息 if (scan.hasNext()) { // 是否有输入数据 System.out.println("输入内容:" + scan.next()); // 存在内容则输出 } scan.close(); } }
-
输入一个double数字:
package com.yootk.demo; import java.util.Scanner; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 Scanner scan = new Scanner(System.in); // 准备接收键盘输入数据 System.out.print("请输入成绩:"); if (scan.hasNextDouble()) { // 表示输入的是一个小数 double score = scan.nextDouble(); // 省略了转型 System.out.println("输入内容:" + score); } else { // 表示输入的不是一个小数 System.out.println("输入的不是数字,错误!"); } scan.close(); } } /* 程序执行结果: 请输入成绩:59 输入内容:59.0 错误输入: 请输入成绩:a72.2 输入的不是数字,错误! */
-
正则验证:
package com.yootk.demo; import java.util.Scanner; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 Scanner scan = new Scanner(System.in) ; // 准备接收键盘输入数据 System.out.print("请输入生日:"); // 提示文字 if (scan.hasNext("\\d{4}-\\d{2}-\\d{2}")) { // 正则验证 String bir = scan.next("\\d{4}-\\d{2}-\\d{2}") ; // 接收数据 System.out.println("输入内容:" + bir); } else { // 数据格式错误 System.out.println("输入的生日格式错误!"); } scan.close(); } }
-
读取文件:
因为Scanner类的构造接收的是InputStream对象,所以依然可以设置一个文件的数据流。考虑到文件本身会存在多行内容,所以需要考虑使用useDelimiter()方法设置读取分隔符(默认是空字符,例如:空格或换行)
package com.yootk.demo; import java.io.File; import java.io.FileInputStream; import java.util.Scanner; public class TestDemo { public static void main(String[] args) throws Exception { // 此处直接抛出 Scanner scan = new Scanner(new FileInputStream(new File("D:" + File.separator + "yootk.txt"))); // 设置读取的文件输入流 scan.useDelimiter("\n"); // 设置读取的分隔符 while (scan.hasNext()) { // 循环读取 System.out.println(scan.next()); // 直接输出读取数据 } scan.close(); } }
-
对于文本数据(非二进制数据)的输入输出问题,输出时使用打印流PrintSream,输入时使用扫描流Scanner(或字符缓冲输入流BufferedReader)。
11.11 对象序列化
程序运行中创建的对象会在程序结束后清除,如果希望对象能在JVM进程结束后依然保留下来,可以采用对象序列化的方式进行处理。
序列化接口:Serializable
对象序列化的本质实际上就是将内存中保存的对象数据转化为二进制数据流进行传输的操作。但要求序列化的对象所在类一定要实现java.io.Serializable接口。该接口中没有任何操作方法,因为它是一种标识接口,表示一种能力。
实现序列化和反序列化
-
实现了Serializable接口后不代表就可以进行对象的序列化操作。还需要以下两个类的支持:
- 序列化操作类:java.io.ObjectOutputStream类,将对象序列化为指定格式的二进制数据。
- 反序列化操作类:java.io.ObjectInputStream类,将序列化的二进制对象信息转换回对象内容。
-
java.io.ObjectOutputStream类的常用方法:
public ObjectOutputStream(OutputStream out) throws IOException // 构造,指定对象序列化的输出流 public final void writeObject(Object obj) throws IOException // 普通,序列化对象
-
java.io.ObjectInputStream类的常用方法:
public ObjectInputStream(InputStream out) throws IOException // 构造,指定对象反序列化的输入流 public final void readObject(Object obj) throws IOException, ClassNotFoundException // 普通,从输入流中读取对象
-
因为ObjectOutputStream类和ObjectInputStream类中的writeObject()方法和readObject()方法接收的对象都是Object类型的,所以说这两个类可以序列化和反序列化Java中的所以数据类型。(Object可以接收所有引用类型,将基本数据类型自动装箱为包装类对象后接收)
-
序列化对象:
package com.yootk.demo; import java.io.File; import java.io.Serializable; import java.io.FileOutputStream; import java.io.ObjectOutputStream; @SuppressWarnings("serial") // 压制序列化版本号警告信息 class Book implements Serializable { // 此类对象可以被序列化 private String title; private double price; public Book(String title, double price) { this.title = title; this.price = price; } @Override public String toString() { return "书名:" + this.title + ",价格:" + this.price; } } public class TestDemo { public static void main(String[] args) throws Exception { ser(); } public static void ser() throws Exception { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream( new File("D:" + File.separator + "book.ser"))); oos.writeObject(new Book("Java开发实战经典", 79.8)); // 序列化对象 oos.close(); } }
本程序将Book类的一个对象序列化后保存在文件中。
-
对象的反序列化:
package com.yootk.demo; import java.io.File; import java.io.FileInputStream; import java.io.ObjectInputStream; public class TestDemo { public static void main(String[] args) throws Exception { dser(); } public static void dser() throws Exception { ObjectInputStream ois = new ObjectInputStream( new FileInputStream(new File("D:" + File.separator + "book.ser"))); Object obj = ois.readObject() ; // 反序列化对象 Book book = (Book) obj ; // 转型 System.out.println(book); ois.close(); } } // 程序执行结果: 书名:Java开发实战经典,价格:79.8
transient关键字
在类中,通过使用transient关键字声明的属性不会被序列化。
-
定义不需要被序列化的属性:
package com.yootk.demo; import java.io.Serializable; @SuppressWarnings("serial") class Book implements Serializable { // 此类对象可以被序列化 private transient String title; // 此属性无法被序列化 private double price; public Book(String title, double price) { this.title = title; this.price = price; } @Override public String toString() { return "书名:" + this.title + ",价格:" + this.price; } }
本章小结
-
在Java中使用File类表示文件本身,可以直接使用此类完成文件的各种操作,如创建、删除等。
-
File类本身只是操作文件,不涉及内容。
-
在使用File类操作时路径的分隔符时使用:File.separator。
-
输入输出流主要分为字节流(OutputStream、InputStream)和字符流(Writer、Reader)两种,但是在传输中以字节流操作较多,字符流在操作时使用缓冲区,而字节流没有使用缓冲区。
-
字节流或字符流都是以抽象类的形式定义的,根据其使用的子类不同,输入或输出的位置也不同。
-
在IO包中可以使用OutputStreamWriter和 InputStreamReader完成字符流与字节流之间的转换操作。
-
使用ByteArrayInputStream 和 ByteArrayOutputStream可以对内存进行输入输出操作。
-
在IO中输出时最好使用打印流(PrintStream、PrintWriter ),这样可以方便地输出各种类型的数据。
-
System类提供了3个支持IO操作的常量:out、err、in。
- System.out:对应显示器的标准输出;
- System.err:对应错误打印,一般此信息不希望给用户看到;
- System.in:对应标准的键盘输入。
-
BufferedReader可以直接从缓冲区中读取数据。
-
使用Scanner类可以方便地进行输入流操作。
-
造成字符乱码的根本原因就是程序编码与本地编码的不统一。
-
对象序列化可以将内存中的对象转化为二进制数据,但对象所在的类必须实现 Serializable 接口。
-
一个类中的属性如果使用transient 关键字声明,则此属性的内容将不会被序列化。
-
对象的输入输出主要使用ObjectInputStream和ObjectOutputStream两个类完成。