1、课程名称:Java IO操作_2
2、知识点
2.1、上次课程的主要知识点
1、 File类,直接与文件本身操作有关
2、 字节流:InputStream、OutputStream,是直接与输出位置操作,中间不使用缓冲区
3、 字符流:Reader、Writer,是通过缓冲区进行操作的
4、 字节和字符的转换流:InputStreamReader、OutputStreamWriter
5、 打印流:PrintStream、PrintWriter
|- 打印流属于装饰设计模式,将原本的功能进行扩充
|- 在JDK 1.5之后提供了输出格式化的操作
6、 对象序列化:将一个对象变为二进制byte流的形式,对象所在的类必须实现Serializable接口,使用ObjectOutputStream和ObjectInputStream完成序列化和反序列化的操作,使用transient关键字可以让某一个属性不被序列化。
2.2、本次预计讲解的知识点
1、 内存操作流:输入、输出的位置都在内存中完成的
2、 管道操作流:两个线程对象之间的通讯
3、 缓冲输入流:BufferedReader
4、 System类对IO的支持
5、 字符编码问题
6、 JDK 1.5新支持的IO操作类:Scanner类
3、具体内容
3.1、内存操作流
之前已经学习过了文件的操作流,输入输出都是从文件中进行的,那么如果现在希望产生一些临时的文件,但这些文件又不想在硬盘中直接生成,那么此时就可以使用内存输入、输出流完成。
内存输入流使用ByteArrayInputStream,是InputStream的直接子类,此类的常用方法如下:
No. 方法名称 类型 描述
1 public ByteArrayInputStream(byte[] buf) 构造 将内容输入到内存之中
现在的操作位置是,所有的内容都向内存中输出,但是对于内存而言属于数据的输入。所以使用ByteArrayInputStream完成内存数据的输入功能。
内存输出流使用ByteArrayOutputStream,是OutputStream的子类,此类的常用方法如下:
No. 方法名称 类型 描述
1 public String toString() 普通 取出全部的内容,由内存中取出
实际上对于ByteArrayOutputStream来讲,基本的操作方法还是以OutputStream为主的,还是写的操作,只是这个时候的写是指由内存向程序中输出。
范例:完成一个字母大写变为小写的功能,使用内存流完成
package org.bytearraydemo;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class CharChangeDemo {
public static void main(String[] args) throws Exception {
String info = "HELLOWORLD";// 定义字符串由大写字母组成
// 所有的内容向内存中输入
InputStream input = new ByteArrayInputStream(info.getBytes());
// 所有的内存中的内容需要通过ByteArrayOutputStream输出
OutputStream out = new ByteArrayOutputStream();
int temp = 0;
while ((temp = input.read()) != -1) {// 读取内存中的内容
// 将大写字母变为小写字母
char c = Character.toLowerCase((char) temp);
out.write(c); // 从内存中输出,所有的内容保存在ByteArrayOutputStream中
}
input.close();
out.close();
String change = out.toString(); // 取出输出的内容
System.out.println(change);
}
}
一定要清楚的明白,内存操作流是以内存为操作中断的,InputStream表示向内存中输入,OutputStream表示从内存中输出。
3.2、System类对IO的支持(重点)
在System类中提供了以下的几个常量:
No. 名称 类型 描述
1 public static final PrintStream out 常量 对应标准输出,显示器
2 public static final PrintStream err 常量 对应错误输出,显示器
3 public static final InputStream in 常量 对应标准输入,键盘
实际上现在存在了以上的三个常量,但是发现out和err都是PrintStream类型的对象,表示打印流。
3.4.1、System.out
System.out表示的是标准的屏幕输出,本身属于PrintStream类型的常量。
package org.systemdemo;
import java.io.OutputStream;
public class SystemOutDemo01 {
public static void main(String[] args) throws Exception {
OutputStream out = System.out; // 使用OutputStream接收System.out
out.write("hello".getBytes());
out.close();
}
}
通过此程序可以明白这样一个问题,实际上在整个IO包中,OutputStream是一个操作的标准,那个子类为其实例化就具备了何种输出能力。
3.4.2、System.err
System.err表示错误输出。
package org.systemdemo;
public class SystemErrDemo01 {
public static void main(String[] args) {
try {
int x = Integer.parseInt("hello");
} catch (NumberFormatException e) {
System.out.println(e);
System.err.println(e);
}
}
}
程序运行结果:
java.lang.NumberFormatException: For input string: “hello”
java.lang.NumberFormatException: For input string: “hello”
以上的错误信息都是一样的,只是在Eclipse中如果是使用System.err进行输出的话,则字体的颜色会变成红色。
提问?
以上的两个操作到底有那些区别?
实际上以上的操作从最终的运行结果来看是没有任何区别的,只能从使用概念上区分:
• System.out是希望用户看到的信息,可以通过一些手段进行输出重定向
• System.err是不希望用户看到的信息,往往在后台输出
3.4.3、System.in
System.in对应着键盘的输入操作,在各个语言中都有键盘输入功能,那么在Java中没有纯粹的输入操作,只能通过System.in完成输入。
package org.systemdemo;
import java.io.InputStream;
public class SystemInDemo01 {
public static void main(String[] args) throws Exception {
InputStream input = System.in; // 此时表示从键盘输入
byte b[] = new byte[1024];// 接收键盘输入
System.out.print("请输入内容:");
int len = input.read(b); // 所有内容输入到byte数组之中
System.out.println("输入的内容是:" + new String(b, 0, len));
}
}
此时完成了输入内容的操作,但是以上的操作存在问题:
• 如果字节数组开辟的空间太小的话,则根本就无法读完,而且中国字会出现乱码。而且此程序中有长度限制。
如果要想解决,则干脆不指定长度,用户随意输入。
package org.systemdemo;
import java.io.InputStream;
public class SystemInDemo02 {
public static void main(String[] args) throws Exception {
InputStream input = System.in; // 此时表示从键盘输入
int temp = 0;
StringBuffer buf = new StringBuffer();
System.out.print("请输入内容:");
while ((temp = input.read()) != -1) {
if (temp == '\n') {
break;// 退出循环
}
buf.append((char) temp);
}
System.out.println("输入的内容是:" + buf);
}
}
以上的操作代码确实没有长度限制了,但是所有的内容发现只要输入中文就会出现乱码,因为是一个个字节的操作,所以最好的输入方法,就是将所有的内容一次性输入完成之后在内存中保存,从程序中一次性将内容全部读取出来,而不是分着读取,如果要想完成这个操作,则只能使用BufferedReader类完成,此类随后会讲解。
3.4.4、输入、输出重定向
对于System.in、System.out、System.err来讲本身可以对输入和输出进行重定向的操作。使用的方法如下:
No. 方法名称 类型 描述
1 public static void setOut(PrintStream out) 普通 修改输出的位置
2 public static void setErr(PrintStream err) 普通 修改错误输出的位置,但是,一般不更改
3 public static void setIn(InputStream in) 普通 修改输入的位置
范例:修改System.out的位置
• 将System.out的输出修改为文件输出
package org.systemdemo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintStream;
public class SystemOutDemo02 {
public static void main(String[] args) throws Exception {
PrintStream ps = new PrintStream(new FileOutputStream(new File("D:"
+ File.separator + "test.txt")));
System.setOut(ps);// 修改了输出位置
System.out.println("Hello World!!!");
}
}
所以,在开发中如果希望一些错误可以让用户看到的话,则可以使用System.setOut()修改输出位置,但是一般情况下是不会使用setErr()修改错误的输出位置,因为这样的信息是不希望用户可以看到的。
范例:修改System.in的位置
• 将System.in的输入修改为从文件中读取
package org.systemdemo;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class SystemInDemo03 {
public static void main(String[] args) throws Exception {
InputStream input = new FileInputStream(new File("D:" + File.separator
+ "test.txt"));
System.setIn(input);
byte b[] = new byte[1024];
int len = System.in.read(b); // 读取内容
System.out.println(new String(b, 0, len));
}
}
此时,内容将从文件之中进行读取的操作。
3.5、缓冲区读取(重点)
在使用System.in进行键盘输入的时候会造成输入的乱码,所以最好将输入的内容一次性读取进来,要想实现这样的功能就可以使用BufferedReader类完成。
No. 方法名称 类型 描述
1 public BufferedReader(Reader in) 构造 接收Reader类的实例
2 public String readLine() throws IOException 普通 读取一行数据
实际上现在就可以将System.in放入到缓冲区之中,但是在缓冲区中需要的构造方法的参数类型是Reader的子类,而System.in是InputStream的子类,则此时如果要想使用,则必须将字节输入流变为字符输入流,使用InputStreamReader类。
package org.buffereddemo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class BufferedReaderDemo01 {
public static void main(String[] args) {
BufferedReader buf = new BufferedReader(
new InputStreamReader(System.in));
System.out.print("请输入内容:");
String str = null;
try {
str = buf.readLine();// 输入数据
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("输入的内容是:" + str);
}
}
此时,不管输入任何的内容都不会出现问题,而且没有长度限制了,所以以上的操作是键盘输入数据的标准格式。
3.6、习题
根据显示要求,完成以下的一个菜单:
====== Xxx 系统 ========
[1]、增加数据
[2]、查看数据
[3]、修改数据
[4]、删除数据
[0]、退出系统
在之前菜单程序的基础之上,加入以下的功能:
• 完成一个单人的信息管理程序,使用Person类即可
• 可以增加数据、修改数据、删除数据、查看数据
• 可以将信息保存在文件之中
如果要想完成以上的功能,则可以将程序分为两个部分:
• 第一个部分进行菜单的显示功能操作
• 第二部分加入具体的功能操作
所以在编写程序的时候一定要注意一个原则,类的设计原则:
• 一个类只完成一个具体的功能,例如,程序中的菜单就是完成菜单功能,文件操作就是完成对象的保存及读取操作,菜单中增加菜单操作类,这样以后扩充的时候可以不用修改菜单。
• 类设计原则,完成具体的独立的各个功能,之后某一个局部的修改不影响其他位置的程序执行。
3.7、Scanner类(理解)
Scanner类是在JDK 1.5之后加入到Java类库中的,但是此类并不是io包类的内容,而是java.util包中的内容。
此类的常用方法如下:
No. 方法名称 类型 描述
1 public Scanner(File source) throws FileNotFoundException 构造 从文件中读取内容
2 public Scanner(InputStream source) 构造 从输入流中读取内容
3 public boolean hasNext() 普通 判断是否有下一个输入内容
4 public String next() 普通 取出内容
5 public boolean hasNext(Pattern pattern) 普通 设置输入的正则验证,是在取之前使用
6 public String next(Pattern pattern) 普通 对输入的内容进行验证,是在取的时候使用
7 public Scanner useDelimiter(String pattern) 普通 设置读取的分隔符
范例:使用Scanner类接收键盘的输入内容
package org.scannerdemo;
import java.util.Scanner;
public class ScannerDemo01 {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in); // 表示键盘输入
System.out.print("请输入内容:");
String str = scan.next();
System.out.println("输入的内容是:" + str);
}
}
使用以上的方式输入非常的方便。而且可以使用以上的方法方便的验证输入的是否是整数。
范例:验证输入的是整数
package org.scannerdemo;
import java.util.Scanner;
public class ScannerDemo02 {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in); // 表示键盘输入
System.out.print("请输入内容:");
if (scan.hasNextInt()) { // 判断输入的是否是整数
int x = scan.nextInt(); // 接收整数
System.out.println("输入的是数字,x = " + x);
} else {
System.out.println("输入的不是数字!");
}
}
}
但是,以上的操作中并不能输入日期类型的数据。如果要想进行是否是日期的验证,则必须手工编写正则表达式。
package org.scannerdemo;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;
public class ScannerDemo03 {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in); // 表示键盘输入
System.out.print("请输入内容:");
if (scan.hasNext("\\d{4}-\\d{2}-\\d{2}")) { // 判断输入的是否是日期类型 1992-12-21
String str = scan.next();
Date date = null;
try {
date = new SimpleDateFormat("yyyy-MM-dd").parse(str);
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println("输入的是数字,x = " + date);
} else {
System.out.println("输入的不是日期!");
}
}
}
现在使用Scanner类将“work.txt”的文件读取出来。
但是,此文件之中,可以发现,包含了很多的空格或换行。
范例:读取文件
package org.scannerdemo;
import java.io.File;
import java.io.FileInputStream;
import java.util.Scanner;
public class ScannerDemo04 {
public static void main(String[] args) throws Exception {
Scanner scan = new Scanner(new FileInputStream(new File("D:"
+ File.separator + "work.txt"))); // 表示键盘输入
scan.useDelimiter("\n"); // 换行作为分隔符
while (scan.hasNext()) {
System.out.println(scan.next());
}
}
}
所以,在使用Scanner类的时候一定要设置好分隔符。
3.8、字符编码问题(理解)
在程序中如果没有处理好字符的编码,则就有可能出现乱码问题。但是如果要进行编码的话,则首先应该了解一下常用的字符编码是什么:
编码:
在计算机世界里,任何的文字都是以指定的编码方式存在的,在JAVA程序的开发中最常见的是以下的几种编码:
ISO8859-1、GBK/GB2312、unicode、UTF。
iso8859-1:编码属于单字节编码,最多只能表示0——255的字符范围,主要在英文上应用
GBK/GB2312:中文的国际编码,专门用来表示汉字,是双字节编码
unicode:java中就是使用此编码方式,也是最标准的一种编码,是使用16进制表示的编码。但此编码不兼容iso8859-1编码。
UTF:由于unicode不支持iso8859-1编码,而且容易占用更多的空间,而且对于英文母也需要使用两个字节编码,这样使用unicode不便于传输和储存,因此产生了utf编码,utf编码兼容了iso8859-1编码,也可以用来表示所有语言字符,不过utf是不定长编码,每个字符的长度从1——6个字节不等,一般在中文网页中使用此编码,因为这样可以节省空间。
以后在开发中比较常见的编码就是GBK、ISO 8859-1、UTF-8编码。
那么如果在本机环境中不想造成乱码出现的话,则必须了解本机的编码是什么,此时就可以使用以下的代码完成:
package org.chardemo;
public class CharDemo01 {
public static void main(String[] args) {
System.out.println(System.getProperties().getProperty("file.encoding"));
}
}
因为现在本机属于中文环境,所以编码使用的是GBK,那么问,如果现在程序中使用了ISO8859-1编码,肯定会出现乱码。
package org.chardemo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
public class CharDemo02 {
public static void main(String[] args) throws Exception {
OutputStream out = new FileOutputStream(new File("D:" + File.separator
+ "temp.txt"));
out.write("中国,你好!".getBytes("ISO8859-1"));// 转变编码
out.close();
}
}
程序中的乱码造成的根本原因:程序使用的编码与本机的编码不统一造成的,在网络通讯中,发送方和接收方的编码不统一也会造成乱码。
3.9、装饰者模式Decorator(重点)
引出问题:
Central Perk的名字因为《老友记》而享誉全球,他们的分店几乎开遍世界各地。他们发展的实在是太快了,所以他们此时正在急于实现一套由计算机管理的自动化记账系统。
经过研究了他们的需求以后,开发者设计了如下图的类结构:
Beverage是所有饮料的基类;cost()是抽象方法,所有子类都需要定义它们自己的cost()实现来返回特定饮料的价钱;description变量也是在子类里赋值的,表示特定饮料的描述信息,getDescription()方法可以返回这个描述;
除了咖啡以外,Central Perk还提供丰富的调味品,比如:炼乳、巧克力、砂糖、牛奶等,而且这些调味品也是要单独按份收费的,所以调味品也是订单系统中重要的一部分。
于是,考虑到调味品的管理,开发者又有了下面这样的类结构:
所以下面我们将拜访一下今天的主角—装饰者模式,看看她能给我们带来什么惊喜吧!
意图:
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。该模式以对客户端透明的方式扩展对象的功能。
适用环境
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
处理那些可以撤消的职责。
当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。
装饰者模式的类图
Component(被装饰对象基类)
定义对象的接口,可以给这些对象动态增加职责;
ConcreteComponent(具体被装饰对象)
定义具体的对象,Decorator可以给它增加额外的职责;
Decorator(装饰者抽象类)
维护一个指向Component实例的引用,并且定义了与Component一致的接口;
ConcreteDecorator(具体装饰者)
具体的装饰对象,给内部持有的具体被装饰对象增加具体的职责;
涉及角色
抽象构件角色:定义一个抽象接口,来规范准备附加功能的类。
具体构件角色:将要被附加功能的类,实现抽象构件角色接口。
抽象装饰者角色:持有对具体构件角色的引用并定义与抽象构件角色一致的接口。
具体装饰角色:实现抽象装饰者角色,负责为具体构件添加额外功能。
实现:
OO原则:动态地将责任附加到对象上。想要扩展功能,装饰者提供有别于继承的另一种选择。
要点:
1、继承属于扩展形式之一,但不见得是达到弹性设计的最佳方案。
2、在我们的设计中,应该允许行为可以被扩展,而不须修改现有的代码。
3、组合和委托可用于在运行时动态地加上新的行为。
4、除了继承,装饰者模式也可以让我们扩展行为。
5、装饰者模式意味着一群装饰者类,这些类用来包装具体组件。
6、装饰者类反映出被装饰的组件类型(实际上,他们具有相同的类型,都经过接口或继承实现)。
7、装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。
8、你可以有无所数个装饰者包装一个组件。
9、装饰者一般对组建的客户是透明的,除非客户程序依赖于组件的具体类型。
4、总结
1、 内存操作流
2、 System类对IO的三个支持
3、 了解字符乱码的产生
4、 只要输出操作就使用PrintStream,只要是输入操作,可以使用BufferedReader或Scanner
5、 通过代码反复的熟悉类的设计
6、 理解装饰者设计模式的原理及具体实现,应用
5、作业
1、 编写Java程序,输入3个整数,并求出三个整数的最大值、最小值。
2、 从键盘输入文件的内容和要保存的文件名称,之后根据输入的名称创建文件,并将内容保存到文件之中。
3、 从键盘传入多个字符串到程序中,并将它们按逆序输出在屏幕上。
4、 从键盘输入以下的数据:
• TOM : 89 | JACK : 90 | TONY:95
|- 数据格式:姓名 : 成绩 | 姓名 : 成绩 | 姓名 : 成绩
• 对输入的内容按年龄进行排序,并将排序结果按照成绩由高到低排序
5、 将以上的内容进行扩展,可以将全部输入的信息保存在文件之中,可以添加信息,并可以显示全部的数据。
6、 编写程序,程序运行后,根据屏幕提示输入一个数字字符串,输入后统计有多少个偶数数字和奇数数字。
7、 完成系统登陆程序,从命令行输入用户名和密码,如果没有输入用户名和密码,则提示输入用户名和密码,如果输入了用户名但是没有输入密码,则提示用户输入密码,之后判断用户名是否是“admin”,密码是否是“hello”,如果正确,则提示登陆成功,如果错误,显示登陆失败的信息,用户再次输入用户名和密码,连续三次输入错误之后系统退出。
8、 完成文件拷贝操作,在程序运行后提示输入源文件路径,之后再输入目标文件路径。
9、 编写程序,程序运行时输入目录名称,并把该目录下的所有文件名后缀修改为“.txt”。
10、选举程序
A、功能描述:
有一个班采用民主投票方法推选班长,班长候选人共4位,每个人姓名及代号分别为 张三 1,李四 2,王五 3,刘六 4。程序操作员将每张选票上所填的代号(1、2、3、或4)循环输入电脑,输入数字0结束输入,然后将所有候选人的得票情况显示出来,并显示最终当选者的信息。
B、具体要求如下:
(1)、要求用面向对象方法,编写候选人类Candidate,将候选人姓名、代号和票数保存到类Candidate(候选人类)中,并实现相应的getXXX 和 setXXX方法。
(2)、编写主程序class OneTest(请考试学员统一使用主类名称:OneTest)
(3)、输入数据之前,显示出各位候选人的代号及姓名:(提示:建立一个候选人类型数组)如下图所示。
(4)、循环执行接收键盘输入的班长候选人代号,直到输入的数字为0,结束选票的输入工作,如下图所示
(5)、在接收每次输入的选票后要求验证该选票是否有效,即:如果输入的数不是0,1,2,3,4这5个数字之一,或者输入一串字母(捕捉异常),应显示出错误提示信息:此选票无效,请输入正确的候选人代号!并继续等待输入。
(6)、输入结束后显示所有候选人的得票情况,如图所示
(7)、输出最终当选者的相关信息,如图所示。
C、参考图示:
1:张三【0票】
2:李四【0票】
3:王五【0票】
4:刘六【0票】
请输入班长候选人代号(数字0结束):1
请输入班长候选人代号(数字0结束):1
请输入班长候选人代号(数字0结束):1
请输入班长候选人代号(数字0结束):2
请输入班长候选人代号(数字0结束):3
请输入班长候选人代号(数字0结束):4
请输入班长候选人代号(数字0结束):5
此选票无效,请输入正确的候选人代号!
请输入班长候选人代号(数字0结束):hello
此选票无效,请输入正确的候选人代号!
请输入班长候选人代号(数字0结束):0
1:张三【4票】
2:李四【1票】
3:王五【1票】
4:刘六【1票】
投票最终结果:张三同学,最后以4票当选班长!
11、 使用装饰者模式实现模拟QQ秀功能,每个QQ用户可以使用XX公司专为QQ秀提供的各种道具,但用户为此需要支付相应的道具费用。
QQ秀道具有:西装(10个Q币)、休闲装(8个 Q币)、腰带(3个Q币)、帽子(5个Q币)、手表(2个Q币)