最近学习了IO流,可能理解的还不是特别好望大家见谅
下面我来叙述一下我对IO流的理解:
IO:对文件进行操作,是in和out,就是输入和输出
本质上是处理设备之间的数据传输,比如,读/写文件,网络通讯等
流:是一个抽象的概念,可以看作是交互的数据像水流一样在数据流通的管道里运输
假设要我们对一个文件进行读写操作时,我们会与数据架起一个通道,数据在通道里流过去~
有点丑,大家理解意思就好哈哈哈
而对于Java来说,IO流实际上是内存和持久化设备的交互,这里的持久化设备更多的应该是硬盘,后面我也就对硬盘来说了。
读:将数据从硬盘读到内存中
写:将数据从内存写到硬盘中
我们可以这样理解,对于内存来说,数据增加了就是读,数据减少了就是写,读是进,写是出
而对于流来说有几个特点:
一:先进先出
二:有顺序存储:按顺序读取和写入数据,不能随机访问数据
三:只能读或者写:对于读写操作一个流只能干一件事,如果需要读和写两个操作,我们就要创建两个流来操作
IO流的分类:
按数据:字节流和字符流
字节流:处理一切数据,二进制,图片,视频,文本等等,处理一切的数据
字符流:只能处理文本文件:.txt文件,不能处理文本文件以外的数据
对于文本来说,一个英文字母是1位字节,中文可能是2字节和3字节,而字符流可以直接区分每个字符,来进行读取。而字节流如果获取的字节数不对,可能出现乱码的清空,比如说:
好这个字,是3字节,如果字节流每次读取1字节,就会造成乱码
字符流是包装流
按流的方向:输出流和输入流
输入流:将数据读入到内存中
输出流:将数据写入到硬盘中
按功能:节点流和包装流
节点流:直接操作数据读写
包装流:基于节点流对其进行功能的拓展和强化
(别处找到的,大家将就看一下)
下面我们来实现一下IO流,如果对文件进行操作:
首先是FileInputStream、FileOutputStream(字节流)
import java.io.*;
public class FileInputStreamText {
public static void main(String[] args) {
File file;
try {
file = new File("C:\\Users\\yizhi\\Desktop\\a.txt");
String content = read(file);
System.out.println(content);
}
catch (IOException e) {
e.printStackTrace();
}
}
private static String read(File file) throws IOException {
//创建读入流
InputStream in = new FileInputStream(file);
//创建缓冲区数组,一字读取3字节
byte[] bytes = new byte[3];
//字符串拼接
StringBuilder string = new StringBuilder();
int length;
//read方法返回读取的数量,length用来接收read返回值,判断是否读入完毕
while ((length = in.read(bytes)) != -1){
//字符串拼接,将byte数组转为字符串
string.append(new String(bytes,0,length));
}
in.close();
return string.toString();
}
}
首先创建一个File对象,里面存储着文件的绝对路径
(File对象大家可以去看看大佬们的讲解,其实就是文件的删除建立等,不是很难,这里我就不详细说了,大家只需要知道这是一个文件对象就好)
下面我们调用read方法,也就是IO流的数据读取
首先需要创建节点流对象,基于上面的图我们可以看到,对于节点流来说
有两个父类抽象类,一个是InputStream(读),另一个是OutputStream(写)
所以这里是多态的体现,父类型指向子类型FileInputStream,实际指向的是文件的读,这个流
将本次的文件传入到流中
下面创建一个数组,充当缓冲区,下面再说一下什么是缓冲区,现在可以看成创建了一个byte数组长度为3
然后构建一个字符串对象,用于字符串的拼接
下面使用while循环进行数据的遍历读取,节点流的read方法返回读取的数量,length用来接收read返回值,判断是否读入完毕,我们可以这样理解,如图:
假设我们本次读取的文本内容时,abcde,当调用read方法时,指针会指向第一个字节a,并将其放入到数组中,然后指针会移动一位指向b,当再次调用read方法时,将b放入数组中,指针再次移动一位。
当数组填满时,会将整个数组返回,然后重复刚刚的过程直到文件中没有数据可读
我们看下结果:
可以看到确实是这样的,每次都会返回一个遍历数据的数组
最后一定要记得,将流关闭
下面我们看一下输出,将数据写到内存的流
public class FileOutputSteamText{
public static void main(String[] args) {
File file;
try {
file = new File("C:\\Users\\yizhi\\Desktop\\a.txt");
write(file);
} catch (IOException e) {
e.printStackTrace();
}
}
private static void write(File file) throws IOException {
//创建写入流 FileOutputStream
OutputStream out = new FileOutputStream(file);
String data = "大家好";
out.write(data.getBytes());
out.close();
}
}
和刚刚读的操作是一样的,只是创建的流不一样了,这次我们创建的是输出节点流,一样的将文件路径传入,本次我们要写入的内容是,大家好,下面调用write方法,将该字符串首先进行byte数组的转换,因为我们要知道,数据本质是二进制,将字符串变成byte数组的过程其实是编码的过程,最后关闭流即可:
我这里就不去讲字符流的相关操作了,过程是一模一样的,只是创建的流不一样。
下面我按照我的理解说一下什么是缓冲操作:
普通流每次读写一个字节,而缓冲流在内存中设置一个缓存区,缓冲区先存储足够的待操作数据后,再与内存或磁盘进行交互。这样,在总数据量不变的情况下,通过提高每次交互的数据量,减少了交互次数。
我们要知道,我们每次读写是和硬盘在交互,交互次数越多,效率越低,所以就有了缓存的概念
举个例子:假如我们现在有一堆砖要从前院运到后院,我们需要一趟搬一块,需要50多个来回,可是我们不会这么做,我们一般是准备一个推车,将砖全部放到推车上,最后一起推到后院即可,只需要一个来回,这个效率是很高的。
而在上面的使用中,数组其实就是充当了缓存区,小推车这个作用
下面我们使用一下BufferedInputStream:
我们看一下源码:
可以看到BufferedInputStream本质上是创建了一个大数组,将数据读取到这个大数组中,而我们自己新建的数组,不再于硬盘交互,而是于BufferedInputStream创建的大数组交互,从这里读取数据
而BufferedInputStream的使用于节点流对象没有什么差别,差别只有是包装类:
import java.io.*;
public class BufferedStreamTest {
public static void main(String[] args) {
File file;
try {
file = new File("C:\\Users\\yizhi\\Desktop\\a.txt");
String content = read(file);
System.out.println(content);
} catch (IOException e) {
e.printStackTrace();
}
}
private static String read(File file) throws IOException {
//创建缓冲流,缓冲流为包装流,需要在里面创建一个节点流使用
InputStream in = new BufferedInputStream(new FileInputStream(file));
byte[] bytes = new byte[3];
StringBuilder string = new StringBuilder();
int length;
while ((length = in.read(bytes)) != -1){
string.append(new String(bytes,0,length));
}
return string.toString();
}
}
我们可以看到使用时没有任何改变的,只有在创建流的时候有点不同,我们需要创建BufferedInputStream流中先创建一个节点对象,这就是装饰器的特点。
我觉得常用的目前常用的流可能就是这几个,比如标准读,标准输出,压缩流,转换流等等其实用法是大差不差的,就是根据情况转换,我目前还没遇见这几个流的使用,大家如果详细了解其他流的使用场景可以去看看别的大佬写的。
下面我说一下我对装饰器的理解
什么是装饰器
也称为包装模式 是指在不改变原有对象的基础之上,将功能附加到对象上
我们来简单举个例子:
还记得多态,接口,抽象类和继承的使用吗,顺便复习一下
比如我们现在有一个宠物猫和一个宠物狗,我们将它们两个抽象成动物,猫和狗都有名字和行为,接口来规定行为,结构如下:
再来墨迹一下,猫狗是真实存在的对象,我们将猫狗共同的特征和行为抽象出来,提取出一个抽象类为动物,并且都实现一个行为,为吃东西。
这个例子很简单,我就不再去着重讲了,下面我们思考一个问题,如果我们现在有一个对狗有一个要求,我需要狗在吃饭之前先刷牙,请问我们有几种方式实现:(我目前掌握的,不是全部的)
第一种:重写狗这个子类的eat方法,在原有的代码上增加功能
第二种:在狗的子类下再创建一个类继承狗,并重写父类狗的eat方法
在解除包装类之前,我可能会选择第二种,因为这样不违背OCP原则,并没有对原有代码进行更改,而是新增了一个类,可是现在我又有一个需求,我需要让猫也刷牙,再吃饭,我们是不是需要再增加一个类,去重写猫的eat方法
我们现在是两个宠物,可以这样做,那么如果我们有100只种类的宠物呢,哈哈哈假设你是动物园的管理员,我们需要重写100个宠物的eat方法,会死人的哈哈哈,简称类爆炸
那么我们这时候需要一个功能,能不能只写一个模块,将对象放进去,对该方法进行统一的重写,是不是就完成了目标,解决了类爆炸的问题,这时候,需要引入一种设计模式:装饰器模式
下面我们回到这个例子,我们想让动物在吃饭之前刷牙,我们只需要将每个动物对象丢入到包装器中。
下面我们来实现一下:
Animal动物类:
public abstract class Animal implements Action{
public String name;
public Animal(String name) {
this.name = name;
}
abstract public void eat();
}
很简单的抽象父类,实现了Action接口,让子类去实现吃的行为
Action接口:
public interface Action {
void eat();
}
不多说了,定义了,吃,这个行为
Cat子类:
public class Cat extends Animal{
public Cat(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(super.name + "在吃鱼");
}
}
继承父类,并实现吃的行为
Dog子类:
public class Dog extends Animal{
public Dog(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(super.name + "在吃骨头");
}
}
没什么区别和猫类,就是变成了吃骨头
测试类:
public class DecoratorText {
public static void main(String[] args) {
Animal cat = new Cat("小猫");
Animal dog = new Dog("小狗");
cat.eat();
dog.eat();
}
}
没什么问题
我们下面来实现如何让它们吃饭之前先刷牙
我们先来了解一下装饰器的结构:
装饰(Decorator)模式中的角色:
抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
我来解释一下我的理解:
抽象构件:就是被扩展的行为或者对象,在这个例子中是action这个接口中,eat这个动作,我们想扩展eat这个东西,在吃之前,刷牙。
具体构件:就是一个具体的对象,谁在吃,在这个例子中是,猫和狗两个具体的对象
抽象装饰:其实本质上也是一个抽象类,继承接口中的行为,指定一个对象去实现具体的内容,本质上还是对,猫和狗的eat进行重写,只不过交给具体装饰去实现
具体装饰:就是对eat功能的扩展实现,在这里对eat方法进行重写
如果有点懵,看下代码:
抽象构件:
public interface Action {
void eat();
}
就是接口中的方法,对其进行扩展
具体构件:
public class Dog extends Animal{
public Dog(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(super.name + "在吃骨头");
}
}
我们需要对该对象的功能进行扩展
抽象装饰器:
public abstract class Decorator implements Action{
public Action action;
public Decorator(Action action) {
this.action = action;
}
@Override
abstract public void eat();
}
我们可以看装饰器类去重写吃这个行为,首先需要在装饰器内创建其实现对象的属性,也就是Action这一接口,并在构造函数中去让其指向传入的对象
Animal dog = new Dog(“小狗”)
dog指向Dog类,这时多态的体现
Action action = dog ----> Action action = new Dog(“小狗”)
看到这里我们应该明白,最终action指向的其实是Dog类
下面是对eat内容的具体实现
下面是 具体装饰:
public class ConcreteDecoratorBrush extends Decorator{
public ConcreteDecoratorBrush(Action action) {
super(action);
}
Animal animal = (Animal) action;
public void before(){
System.out.println(animal.name + "先刷牙");
}
public void after(){
System.out.println("吃饱了");
}
@Override
public void eat() {
before();
action.eat();
after();
}
}
在具体装饰里我们对eat进行了功能扩展,首先执行父类的构造函数,下面对action进行向下转型,如果前面理解了,我们是知道,action指向的是Dog类,那一定是可以转型到Dog的父类Animal的,获取对象的名字,通过父类指向子类获取对象的名称。
然后重写eat方法对其进行扩展,先刷牙,然后吃饭dog.eat,最后吃饱了。
让我们看一下测试程序:
public class DecoratorText {
public static void main(String[] args) {
Animal dog = new Dog("小狗");
dog.eat();
System.out.println("===============");
Decorator decorator1 = new ConcreteDecoratorBrush(dog);
decorator1.eat();
}
}
我们先使用了多态进行了普通的eat执行,然后我们使用了装饰器,将dog对象传入进去,发现功能进行了扩展,如果是猫呢?我们再试一下
public class DecoratorText {
public static void main(String[] args) {
Animal cat = new Cat("小猫");
cat.eat();
System.out.println("===============");
Decorator decorator1 = new ConcreteDecoratorBrush(cat);
decorator1.eat();
}
}
这次我们传入的是猫这个对象,发现同样进行了扩展,所以这里也证明了我们可以对多个对象进行同一种功能的扩展,我们完全没有对原有的代码进行任何的修改,也没有添加过多的子类,就实现了功能的增加,希望大家能理解这种方式,其实最终的本质,还是多态和继承。
下面我再来写一个装饰器,让动物们先喝水再吃饭:
public class ConcreteDecoratorBrush extends Decorator{
public ConcreteDecoratorBrush(Action action) {
super(action);
}
Animal animal = (Animal) action;
public void before(){
System.out.println(animal.name + "先刷牙");
}
public void after(){
System.out.println("吃饱了");
}
@Override
public void eat() {
before();
action.eat();
after();
}
}
测试程序:
public class DecoratorText {
public static void main(String[] args) {
Animal cat = new Cat("小猫");
cat.eat();
System.out.println("===============");
Decorator decorator = new ConcreteDecoratorDrink(cat);
decorator.eat();
}
}
又增加一种行为,本质还是对吃这个动作,进行了功能扩展
好了希望大家能理解装饰器的设计模式,还有IO流的使用,IO流我并没有去详细讲述每种流应该什么时候使用,大家去看看别的大佬写的,很详细,我这里只是讲述我的理解,如果有错误的地方,希望大家的指出!谢谢大家!