黑马程序员——字节流、自定义缓冲区、异常信息处理
1、字节流
字节流的两个基类是InputStream和OutputStream,
相应的缓冲区是BufferedInputStream和BufferedOutputStream
它的操作与字符流类似,可以参与字符流的定义、读取、写入、处理异常的格式
只不过是处理的数据不同,因为对于非字符的数据, 比 如图片、视频、音频文件(例如mp3)等,这些文件只能用字节流 对之进行操作。
如果想在已有的文件中追加数据,怎样呢?依旧在创建对象是:fos =new FileOutputStream(file,true);
字节流的写操作,不用刷新动作,也能写入目的地,这是为什么?
字符流底层用也的字节流,因为涉及编码,比如写一个汉字,它有两个字节,你不能读一个字节就写入,而要等到两个字节都读取完后,才写入,所以必须要有flush机制。而字节流,不涉及编码,可以读一个就写一个而不出现问题,所以不用刷新也能写入。
字节流的写方法代码:
public class OutputStreamDemo {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
File file = new File("d:"+File.separator+"def.txt");
if(!file.exists()){
try {
file.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
//定义一个字符串,因为字节流只能以字节或字节数组的形式读取
String s = "我要进黑马,实现一个跨越";
byte [] by =s.getBytes();
fos.write(by);//不用刷新的
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
if(fos!=null){
try {
fos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
2、字节流的读方法:
但注意:如果字节说过大,会造成内存溢出。
部分代码:fis =newFileInputStream(file);
byte [] ch =newbyte[fis.available()];
fis.read(ch);
System.out.println(newString(ch));
字节流特有的读取方式:available(
方法可以知道文件的所有字节数,以此可以定义一个刚刚好的字节数组,只用读取一次,然后写入来完成文件复制
方法一:按字节读取
public class InputStreamDemo1 {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
File file = new File("d:"+File.separator+"abc.txt");
if(!file.exists()){
try {
file.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
FileInputStream fis = null;
try {
//实例化字节流对象
fis = new FileInputStream(file);
int x = 0;
while((x = fis.read())!=-1){
System.out.println((char)x);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
if(fis!=null){
fis.close();//一定要执行的关闭流,释放资源
}
}}}
方法二:按字节数据读取
部分代码:
try {
fis = new FileInputStream(file);
// int x = 0;
byte [] ch = new byte[1024];//定义一个容量为1024的byte数组
int len =0;
while((len = fis.read(ch))!=-1){
System.out.println(new String(ch,0,len));
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
if(fis!=null){
fis.close();
}
练习1:复制图片(字节文件输入输出流)
注意:字符流也可以复制图片,但是它读取一段字节后就会查表进行编码,如果表中有对应的编码值那么没有问题,
如果没有对应的编码,则会在编码表的未知区域找一个类似的编码进行替代,这时就改变了原来的字节,
导致图片格式不对,复制的图片无法打开。
/*
需求:复制一个图片。
思路:
1.定义一个字节读取流对象和图片关联
2.定义一个字节写入流对象,用于创建和存储图片。
3.通过循环读写,完成数据的存储。
4.关闭资源。
*/
public class CopyPic {
/**
* @param args
*/
public static void main(String[] args) {
long start = System.currentTimeMillis();
copy_pic();
long end = System.currentTimeMillis();
System.out.println(end-start);
}
public static void copy_pic(){
File pic = new File("d:\\我和女友.jpg");
File pic_copy = new File("d:"+File.separator+"我和女友_复件.jpg");
//判断两个文件是否存在;
if(!pic.exists()){
throw new RuntimeException("找不到图片");
}
if(!pic_copy.exists()){
try {
pic_copy.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try{
bis = new BufferedInputStream(new FileInputStream(pic));
bos = new BufferedOutputStream(new FileOutputStream(pic_copy));
byte[] ch = new byte[1024];
int len = 0;
while((len = bis.read(ch))!=-1){//一次读一个字节数组
bos.write(ch, 0, len);
}
}catch(Exception e){
e.printStackTrace();
}finally{//必须关流
if(bis!=null){
try {
bis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(bos!=null){
try {
bos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
练习2:复制mp3文件(使用字节缓冲区)
复制媒体文件要注意的问题:
1、复制MP3因为有可能读取的字节8个1,值为-1,,返回值是int类型,类型提升后结果仍为-1,这会导致数据并未读完,
但myRead()结果却为-1,从而使得复制mp3文件失败。
为避免这种情况,可以采取这种方法,读取的字节类型提升后前24位补的是1,我们让它补0。
怎么补零?
类型提升的过程我们是没法控制的,那么可以将提升后的结果&255,
结果就转化成最后一个字节不变,而前三个字节位都变成了0,
&255后的值作为read方法的返回值。
这其实也是为什么read()方法的返回值是int而不是byte的原因。
2、字节写入流的write(intb)方法,在写入时,有个截取动作,即把最低8位为保留,而把高24位舍弃。
自定义的字节流缓冲:
代码:
class MyBufferedInputStream
{
private InputStream in;
private byte[] buf = new byte[1024*4];
private int pos = 0,count = 0;
MyBufferedInputStream(InputStream in)
{
this.in = in;
}
//一次读一个字节,从缓冲区(字节数组)获取。
public int myRead()throws IOException
{
//通过in对象读取硬盘上数据,并存储buf中。
if(count==0)
{
count = in.read(buf);
if(count<0)
return -1;
pos = 0;
byte b = buf[pos];
count--;
pos++;
return b&255;
}else if(count>0){
byte b = buf[pos];
count--;
pos++;
return b&0xff;
}
return -1;
}
public void myClose()throws IOException{
in.close();
}
}
/*毕老师的讲解:
11111111-111111110000000000101001001010100101010010101001010
byte: -1 ---> int : -1;
00000000 00000000 00000000 11111111 255
11111111 11111111 11111111 11111111
11111111 -->提升了一个int类型 那不还是-1吗?是-1的原因是因为在8个1前面补的是1导致的。
那么我只要在前面补0,即可以保留原字节数据不变,又可以避免-1的出现。
怎么补0呢?
11111111 11111111 11111111 11111111
&00000000 00000000 00000000 11111111
------------------------------------
00000000 00000000 00000000 11111111
0000-0001
1111-1110
000000001
1111-1111 -1/*
结论:
字节流的读一个字节的read方法为什么返回值类型不是byte,而是int。
因为有可能会读到连续8个二进制1的情况,8个二进制1对应的十进制是-1.
那么就会数据还没有读完,就结束的情况。因为我们判断读取结束是通过结尾标记-1来确定的。
所以,为了避免这种情况将读到的字节进行int类型的提升。
并在保留原字节数据的情况前面了补了24个0,变成了int类型的数值。
而在写入数据时,只写该int类型数据的最低8位。
*/
Import ........
public class CopyMp3 {
public static void main(String[] args)throws IOException {//异常全抛出去
// TODO Auto-generated method stub
long start =System.currentTimeMillis();
copyMp3();
long end = System.currentTimeMillis();
Print.sop((end-start)+"毫秒");
}
public static void copyMp3()throws IOException{
BufferedInputStream in =
new BufferedInputStream(new FileInputStream("D:\\突然好想你.mp3"));
BufferedOutputStream out =
new BufferedOutputStream(new FileOutputStream("D:\\突然好想你_copy.mp3"));
int len =0;
while((len = in.read())!=-1){
out.write(len);
}
if(in!=null){
in.close();
}
if(out!=null){
out.close();}
}
public static void copy_2() throwsIOException {
MyBufferedInputStream bufis =
new MyBufferedInputStream(new FileInputStream("突然好想你.mp3"));
BufferedOutputStream bufos =
new BufferedOutputStream(new FileOutputStream("突然好想你_1.mp3"));
int by = 0;
//System.out.println("第一个字节是:"+bufis.myRead());//结果为-1。
int len = 0;
while((len = bufis.myRead())!= -1)
{
bufos.write(len);//write其实在做一个强转动作
//bufos.flush();//这里不要写flush,否则复制速度会大幅降低。
//System.out.println(len);
}
bufis.myClose();
bufos.close();
}
}
5、 读取键盘录入
读取键盘录入。
System.out:对应的是标准输出设备,控制台。
System.in:对应的是标准的输入设备,键盘。
通过键盘录入数据。
当录入一行数据后,就将改行数据打印。
如果录入的数据是over,那么停止录入
import java.io.InputStream;
public class SystemIn {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
// 新建一个键盘输入流
InputStream in = System.in;
//创建一个缓冲区,将数据存入缓冲区中
StringBuilder sb = new StringBuilder();
int ch = 0;
while(true){
ch = in.read();
/*
read()是一个阻塞式的方法,它每次读取一个字节,如果没有数据,它会一直等待。
1. 为什么它读硬盘上的文件文件时,没有阻塞?
因为硬盘上的文件就算是空,为0字节,因为文件有结束标记(windows系统给每个磁盘文件都加有结束标记),会返回-1,而不阻塞。 而在dos命令行窗口下,启动后,既没有数据,也没有结束标记,所以一直阻塞,而“ctrl+c”命令,其实就是输入一个结束标记。
2. 为什么dos命令窗口下,只用我敲回车键后,控制台才打印,而不是我录入一个字符它就打印?
因为,录入的字符,只有在敲回车键后才写入in关联的流里边,这时流里边才有数据,从而被read()方法读取到。
3.为什么我敲入一个字符回车后,却打印出来3个数字?
因为,windows中,敲回车键,就是加入换行符,而windows中的换行符是用\r\n两个字符表示的,所以会有3个。
代码
*/
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);
}
}
}
}
6、转换流
在接下来会讲到指定编码表的问题。
上边读取键盘录入的代码,与BufferedReader的readLine()方法很相似,那么能不能用readLine()来代替?因为System.in是字节流,它要使用字符流缓冲区的方法,这就需要使用到两个转换流:InputStreamReader和OutputStreamWriter。
代码
class TransStreamDemo
{
public static void main(String[] args) throws IOException
{
//获取键盘录入对象。
//InputStream in = System.in;
//将字节流对象转成字符流对象,使用转换流。InputStreamReader
//InputStreamReader isr = new InputStreamReader(in);
//为了提高效率,将字符串进行缓冲区技术高效操作。使用BufferedReader
//BufferedReader bufr = new BufferedReader(isr);
//键盘的最常见写法。
BufferedReader bufr =
new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bufw =
new BufferedWriter(new OutputStreamWriter(System.out));
String line = null;
while((line=bufr.readLine())!=null)
{
if("over".equals(line))
break;
bufw.write(line.toUpperCase());
bufw.newLine();
bufw.flush();
}
bufr.close();
}
}
7、流操作的基本规律
1.明确源和目的。
源:输入流,InputStream和Reader;
目的:输出流,OutputStream和Writer
2.明确操作的数据是否是纯文本。
是:字符流
不是:字节流。
3.当体系明确后,再明确要使用哪个具体的对象。
通过设备来区分:
源设备:内存、硬盘、键盘
目的设备:内存、硬盘、控制台。
此外:
如果需要提高效率,则换用对应的缓冲区。
如果需要指定编码,则换用对应的转换流。
System类中的setIn()、setOut()、setError()方法,可以改变标准输入流、输出流和错误输出流。
8、异常信息的保存
IO操作最头痛的是如何其体系中选择需要的类,这个过程可以通过三个步骤来完成
代码
class ExceptionInfo {
Public static void main(String[] args) throws IOException {
try {
int [] arr = newint[2];
System.out.println(arr[3]); //空指针异常
}
catch (Exception e) {
try {
Date d= new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-DDHH:mm:ss");
String s =sdf.format(d);
//保存异常信息到文件中,可以续写。
PrintStream ps = new PrintStream(new FileOutputStream("exception.log",true));
ps.println(s); //打印异常的日期
System.setOut(ps);
}
catch (IOException ex)
{
throw new RuntimeException("日志文件创建失败");
}
e.printStackTrace(System.out);
}
}
}
异常信息序列化
class SystemInfo {
public static void main(String[]args) throws IOException {
Properties prop = System.getProperties();
System.out.println(prop); //数组的形式打印
prop.list(System.out);//不用像在集合时一样,用遍历集合
//将属性列表输出到指定的输出流
prop.list(new PrintStream("d:"+File.separator+"sysinfo.txt")); //列表的形式打印到sysinfo.txt.
}
}
要点总结:
字符流缓冲区:BufferReader和BufferWriter
a.提高了对数据读写的效率。
b.必须结合流使用。
c.在流的基础上对流的功能进行了增强。
BufferReader字符读取流缓冲区:
该缓冲区提供了一个一次读一行的方法readLine,方便于对文本数据的获取。当返回null时,表示读到文件末尾。
readLine方法返回的时候只返回回车符之前的数据内容。并不返回回车符
BufferWriter
该缓冲区中提供了一个跨平台的换行符。newLine();
1)练习:通过缓冲区复制一个.java文件。
2)练习:Bufferreader的MyreadLine方法
3)装饰设计模式;自定义装饰类;读取行数的LineNumnberReader。MyLineNumberReader的设计
4)字节流
InputStream和OutputStream
5)缓冲字节流
6)拷贝MP3,拷贝图片。
7)自定义字节流的缓冲区
8)读取键盘录入
9)读取转换流
10)写出转换流
11)流操作规律总结。
12)改变标准输入输出设备
---------------ASP.Net+Android+IOS开发、.Net培训、期待与您交流! -----------