BufferedReader
中的readLine()
方法无论是读取一行还是读取多个字符。其实最终都是在硬盘上一个个读取。所以最终使用的还是read()
方法一次读取一个的方法。
class MyBufferedReader {
private FileReader r;
public MyBufferedReader(FileReader r) {
this.r = r;
}
public String myReadLine() throws IOException {
StringBuilder sb = new StringBuilder(); // 临时容器
int ch = 0;
while ((ch = r.read()) != -1) {
// windows操作系统的换行是\r\n
if (ch == '\r') // 当读取到\r的时候继续向下读取
continue;
if (ch == '\n') // 当读取到\n的时候将临时容器中的内容返回
return sb.toString();
else
sb.append((char) ch);
}
if (sb.length() != 0)
return sb.toString(); // 最后一行中可能没有\n
return null; // 读到行结尾的时候返回空
}
public void myClose() throws IOException {
r.close();
}
}
public class Test {
public static void main(String[] args) throws IOException {
MyBufferedReader mbr = new MyBufferedReader(new FileReader("res/deamon.txt"));
String line = null;
while ((line = mbr.myReadLine()) != null) {
System.out.println(line);
}
mbr.myClose();
}
}
以上的myReadLine()
其实是对原有的read()
方法的增强,这实际上是一种装饰者设计模式。
当想要对已有的对象的功能进行增强的时候,可以定义类将已有的类传入,基于已有对象的功能并提供加强功能,自定义的该类就称为装饰类。装饰类通常会通过构造方法接收被装饰的对象,并基于被装饰的对象提供增强的功能。装饰类和被装饰类往往在同一个继承体系中例如下面的例子:
class Pserson{
public void eat(){
System.out.println("吃饭!");
}
}
class SuperPerson{
private Pserson pserson;
public SuperPerson(Pserson pserson) {
this.pserson = pserson;
}
public void superEat(){
System.out.println("开胃酒。");
pserson.eat();
System.out.println("甜点。");
System.out.println("来一根。");
}
}
public class Test {
public static void main(String[] args){
SuperPerson superPerson = new SuperPerson(new Pserson());
superPerson.superEat();
}
}
在以上的例子中我们完全可以让SuperPerson
这个类继承Person
类,覆写它的eat()
方法达到我们所需要的效果。装饰模式相对于继承提高了灵活性,避免了继承体系的臃肿,而且降低了类之间的耦合——从继承结构变成了组合结构。
class MyLineNumberReader{
private Reader reader;
private int lineNumber; // 行号
public MyLineNumberReader(Reader reader) {
this.reader = reader;
}
public int getLineNumber() {
return lineNumber;
}
public void setLineNumber(int lineNumber) {
this.lineNumber = lineNumber;
}
public String myReadLine() throws IOException{
lineNumber++;
StringBuilder sb = new StringBuilder();
int ch = 0;
while ((ch = reader.read())!=-1) {
if(ch == '\r')
continue;
if(ch == '\n')
return sb.toString();
else
sb.append((char)ch);
}
if(sb.length()!=0)
return sb.toString();
return null;
}
public void myClose() throws IOException{
reader.close();
}
}
public class Test {
public static void main(String[] args) throws IOException{
MyLineNumberReader mlnr = new MyLineNumberReader(new FileReader("res/deamon.txt"));
String line = null;
mlnr.setLineNumber(100);
while ((line = mlnr.myReadLine())!=null) {
System.out.println(mlnr.getLineNumber() + ":" +line);
}
mlnr.myClose();
}
}
在以上的代码中我们实现了自己的LineNumberReader
,但是发现以上类中的myReadLine()
方法和我们自己写的MyBufferedReader
中的myReadLine()
方法类似。因此我们可以模仿LineNumberReader
那样继承BufferedReader
而使我们自己的类继承MyBufferedReader
,从而以上的代码简化为如下:
class MyBufferedReader {
private Reader reader;
public MyBufferedReader(Reader reader) {
this.reader = reader;
}
public String myReadLine() throws IOException {
int ch = 0;
StringBuilder sb = new StringBuilder();
while ((ch = reader.read()) != -1) {
if (ch == '\r')
continue;
if (ch == '\n')
return sb.toString();
else
sb.append((char) ch);
}
if (sb.length() != 0)
return sb.toString();
return null;
}
public void myClose() throws IOException {
reader.close();
}
}
class MyLineNumberReader extends MyBufferedReader {
public MyLineNumberReader(Reader reader) {
super(reader);
}
private int lineNumber; // 行号
public int getLineNumber() {
return lineNumber;
}
public void setLineNumber(int lineNumber) {
this.lineNumber = lineNumber;
}
public String myReadLine() throws IOException {
lineNumber++;
return super.myReadLine();
}
public void myClose() throws IOException {
super.myClose();
}
}
class MyBufferedInputStream{
private InputStream is;
private byte[] buf = new byte[1024]; // 缓冲区
private int pos = 0,count = 0; // 指针和计数器
public MyBufferedInputStream(InputStream is) {
this.is = is;
}
/**
* 一次读取一个字节,从缓冲区(字节数组)中获取
* @throws IOException
*/
public int myRead() throws IOException{
// 通过is读取数据存放到buf中
if(count == 0){
count = is.read(buf);
if(count < 0)
return -1;
pos = 0; // 指针归零
// 取数据
byte b = buf[pos];
count--;
pos++;
return b & 0xFF;
}else if(count > 0){
// 取数据
byte b = buf[pos];
count--;
pos++;
return b & 0xFF;
}
return -1;
}
public void myClose() throws IOException{
is.close();
}
}
public class Test {
public static void main(String[] args) throws IOException {
MyBufferedInputStream mbis = new MyBufferedInputStream(new FileInputStream("E:/tmp/a.avi"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("E:/tmp/b.avi"));
int temp = 0;
while ((temp = mbis.myRead()) != -1) {
bos.write(temp);
}
bos.close();
}
}
Q1:为什么读取的时候返回的是int类型而不是byte类型?
A1:因为我们读取二进制数据的时候可能会遇到1111 1111
这种情况,如果以byte来判断的话会因为是-1而停止读取认为到达了文件末尾,所以需要将byte类型进行提升为int类型(和0xFF进行与运算)。
Q2:既然读取的是int类型,那么我们为什么读取后写入的数据量不是原来数据的4倍?(即:我们每次读取一个byte,但是我们每次写入的却是一个int)
A2:与Q1类似,write(int b)
方法内部将int强制转化成了byte类型。
读取键盘录入,录入一行数据后显示录入的数据的大写形式,当输入over的时候停止录入。
public class Test {
public static void main(String[] args) throws IOException{
BufferedInputStream bis = new BufferedInputStream(System.in);
StringBuilder sb = new StringBuilder();
while (true) {
int ch = bis.read();
if(ch == '\r')
continue;
if(ch == '\n'){
String s = sb.toString();
if("over".equals(s)){
break;
}
System.out.println(s.toUpperCase());
sb.delete(0, sb.length()); // 清空缓冲区
}else {
sb.append((char)ch);
}
}
}
}
发现以上的代码和自定义BufferedReader
中的myReadLine()
方法类似。但是现在的问题是System.in
是字节流重点内容,而readLine()
方法是BufferedReader
中的方法,是字符流,所以此时我们需要用到转换流(InputStreamReader
是Reader的子类,所以需要在构造的时候指定一个字节输入流)。
以下是键盘录入的最常见的写法:
public class Test {
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in,"utf-8"));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out,"gbk"));
String line = null;
while ((line = br.readLine())!=null) {
if("over".equals(line))
break;
bw.write(line.toUpperCase());
bw.newLine();
bw.flush();
}
br.close();
bw.close();
}
}
以上程序中的目的可以使用PrintWriter pw = new PrintWriter(System.out)
来代替。
System.in
和System.out
可以使用setIn()
和setOut
进行重定向。例如:
System.setIn(new FileInputStream("res/deamon.txt")); // 重定向System.in和System.out
System.setOut(new PrintStream("res/out.txt"));
BufferedReader br = new BufferedReader(new InputStreamReader(System.in,"utf-8"));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out,"utf-8"));
String line = null;
while ((line = br.readLine())!=null) {
if("over".equals(line))
break;
bw.write(line.toUpperCase());
bw.newLine();
bw.flush();
}
br.close();
bw.close();
基于以上的特点我们可以自定义自己的出错日志文件(一般上使用的是log4j):
String s = null;
try {
System.out.println(s.length());
} catch (Exception e) {
PrintStream pw = new PrintStream(new FileOutputStream("res/exception.log"),true);
pw.println(new SimpleDateFormat("yyyy年MM月dd HH:mm:ss.SSS").format(new Date()));
e.printStackTrace(pw);
}
我们还可以将系统信息保存到文件中。
Properties props = System.getProperties();
props.list(new PrintStream("res/sysinfo.txt"));
Properties
文件是HashTable
的子类。
// 设置和获取属性
Properties prop = new Properties();
prop.setProperty("zhangsan", "30");
prop.setProperty("lisi", "20");
prop.setProperty("王五", 10 + "");
prop.setProperty("王五", 40 + ""); // 上面的属性被覆盖了
System.out.println(prop); // 打印属性文件中的全部内容
prop.list(System.out); // 列出属性列表到指定输出流
System.out.println(prop.getProperty("王五")); // 取得特定键对应的值
for (String key : prop.stringPropertyNames()) {
System.out.println(key + "--->" + prop.getProperty(key));
}
System.out.println("-------------------------------------");
// 将文件data.properties中的内容加载到内存
prop = new Properties();
prop.load(new FileReader("res/data.properties"));
prop.list(System.out);
// 将属性文件中的内容持久化到文件(属性文件和xml两种格式)
prop.store(new FileOutputStream("info.properties"), "Student information");
prop.storeToXML(new FileOutputStream("info.xml"), "学生信息", "utf-8");
基于属性文件,我们可以设计如下的程序记录某个程序的运行次数,如果使用次数已到给出注册提示。
思考:肯定需要一个程序计数器,该计数器必须存放在外存,因为程序一旦运行结束计数器就消失了。所以需要一种机制:当程序启动的时候从持久层获取计数器,程序启动后对计数器进行加1操作,重新将计数器持久化。
public class Test {
public static final int COUNT_OF_PROGRAM = 5;
public static final String REG_INFO = "使用次数已到,请注册!\n请输入注册码:";
public static final String REG_SUCCESS = "注册成功。";
public static final String REG_FAILURE = "注册码错误!";
public static final String REG_CODE = "admin";
public static void main(String[] args) throws IOException{
Properties prop = new Properties();
File file = new File("res/count.properties");
if(!file.exists())
file.createNewFile(); // 首次运行程序的时候产生配置文件
int count = 0;
prop.load(new FileReader(file)); // 加载配置文件
String value = prop.getProperty("time", 0 + "");
count = Integer.parseInt(value) + 1;
if(count >= COUNT_OF_PROGRAM){
System.out.println(REG_INFO);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String key = scanner.nextLine();
if(key.equalsIgnoreCase(REG_CODE)){
System.out.println(REG_SUCCESS);
break;
}else {
System.out.println(REG_FAILURE);
scanner = new Scanner(System.in);
}
}
}
prop.setProperty("time", count + "");
prop.store(new FileWriter(file), "程序计数器,使用次数到达的时候提示注册");
}
}
合并流SequenceInputStream
用于将多个源合并成一个源。例如有以下的3个文件1.txt、2.txt、3.txt三个文件,3个文件中存放的分别的若干个1、若干个2、若干个3。现在将1.txt、2.txt、3.txt三个文件合并成一个文件out.txt。
Vector vector = new Vector(); // 通过Vector获取Enumeration
vector.add(new FileInputStream("res/1.txt"));
vector.add(new FileInputStream("res/2.txt"));
vector.add(new FileInputStream("res/3.txt"));
SequenceInputStream sis = new SequenceInputStream(vector.elements()); // 将多个源合并成一个源
FileOutputStream fos = new FileOutputStream("res/out.txt");
byte[] buf = new byte[1024];
int len = 0;
while ((len = sis.read(buf))!=-1) {
fos.write(buf, 0, len);
}
fos.close();
sis.close();
文件的切割和合并
// 切割文件
FileInputStream fis = new FileInputStream("res/a.avi");
FileOutputStream fos = null;
byte[] buf = new byte[1024*1024*4]; // 创建4M的缓冲区
int len = 0;
int count = 0;
while ((len = fis.read(buf))!=-1) {
fos = new FileOutputStream("res/a.avi.part" + (++count));
fos.write(buf, 0, len);
fos.close();
}
fis.close();
// 合并文件
ArrayList fiss = new ArrayList();
for(int i = 1; i <= 5;i++){
fiss.add(new FileInputStream("res/a.avi.part" + i));
}
// 将List转化成Enumeration需要通过List的迭代器
final Iterator iterator = fiss.iterator(); // 匿名内部类访问局部变量需要final修饰
Enumeration enumeration = new Enumeration() {
@Override
public boolean hasMoreElements() {
return iterator.hasNext();
}
@Override
public FileInputStream nextElement() {
return iterator.next();
}
};
SequenceInputStream sis = new SequenceInputStream(enumeration);
fos = new FileOutputStream("res/b.avi");
buf = new byte[1024];
len = 0;
while ((len = sis.read(buf))!=-1) {
fos.write(buf, 0, len);
}
fos.close();
sis.close();
输入和输出可以直接进行连接,通常通过线程结合使用。
在读取键盘录入的时候我们知道它是一个阻塞式方法——我们可以理解为另一个线程处于等待状态,当读取到数据的时候另一个线程就被唤醒了。注意:在使用管道流的时候两个管道必须进行连接。
/**
*
* @ClassName: Read
* @Description: 在构造的时候传入一个管道输入流,从管道中读取数据
*
*/
class Read implements Runnable{
private BufferedInputStream bis;
public Read(PipedInputStream pis) {
bis = new BufferedInputStream(pis);
}
@Override
public void run() {
byte[] buf = new byte[1024];
int len = 0;
try {
System.out.println("读取前,没有数据.\t【阻塞中】");
while ((len = bis.read(buf)) != -1) {
System.out.println(new String(buf, 0, len));
}
System.out.println("数据读取完成.\t【阻塞解除】");
bis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
*
* @ClassName: Write
* @Description: 构造时传入一个管道输入流,向管道中不断写入随机数
*
*/
class Write implements Runnable{
private BufferedOutputStream bos;
private static Random random = new Random();
public Write(PipedOutputStream pos) {
bos = new BufferedOutputStream(pos);
}
@Override
public void run() {
try {
while (true) {
System.out.println("2s后开始写入数据.\t【LOADING TO WRITE】");
Thread.sleep(2000);
bos.write((random.nextLong()+"").getBytes());
System.out.println("数据写入完成.\t【WRITE DONE】");
bos.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Test {
public static void main(String[] args) throws IOException{
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();
pis.connect(pos); // 两个管道进行连接
new Thread(new Read(pis)).start();
new Thread(new Write(pos)).start();
}
}