写在前
终于完成了!!!!内容不多·就是本人太拖拉!
这里主要介绍文件input,output操作。File类,流对象(分为字节流、字符流)
需要掌握每个流对象的使用方式:打开、读写、关闭
熟练使用API并知道这些API起到的效果
这里涉及到的经典面试题需要注意:
抽象类和接口的区别[4.2]
还需要重点去看的一个是4.2.8中的案例一,重点体会其中的递归·操作!!!
目录
[经典面试题--- 抽象类和接口什么区别?](#经典面试题--- 抽象类和接口什么区别?)
[try with resources](#try with resources)
文件操作--IO
1.认识文件
1.1狭义上的文件
硬盘上的文件和目录(文件夹) 本篇博客记录的就是狭义上的文件。
1.2广义上的文件
泛指计算机中的很多软硬件资源。操作系统中,把很多的硬件设备和软件资源抽象成文件,按照文件的方式统一管理。
2.文件路径
2.1绝对路径
以c: d: 盘符开头的路径
2.2相对路径
以当前所在的目录为基准,以 . 或者 .. 开头,(.有时可以省略),找到指定的路径。【当前所在的目录称为是工作目录,每个程序运行的时候,都有一个工作目录,,在控制台里通过命令操作的时候是特别明显的,后期进化到图形化界面,工作目录就不那么直观了。】
举个例子:如下图,假定当前的工作目录是 f:/Code,定位到111这个目录上就可以表示为
./111 其中./表示为当前的目录
如果工作目录不同,定位到同一个文件,相对路径的写法是不同的。
我们同样希望定位到 111 这里
|-----------------|------------|
| 工作目录 | 相对路径 |
| f:/ | ./Code/111 |
| f:/Code | ./111 |
| f:/Code/222 | ../111 |
| f:/Code/222/bbb | ../../111 |
[几种相对路径的写法]
../表示当前目录的上级目录。
3.文件类型
word;exe;图片;视频;音频;源代码;动态库等等,这些不同的文件整体可以归纳为两类
3.1文本文件
存的是文本,字符串【字符串,由字符构成,每个字符都是通过一个数字来表示的。这个文本文件里存储的数据一定是合法的,都是在指定字符编码的码表之内的数据。】
3.2二进制文件
存的是二进制数据,不一定是字符串【没有任何限制,可以存储任何想要的数据。】
小思考? 当我们有一个文件,如何区分是文本文件还是二进制文件?
直接使用记事本打开,如果乱码了就说明是二进制文件,如果没有乱码就是文本。
4.Java中操作文件
主要有两类操作,第一类是针对文件系统的操作(文件的创建、删除、重命名);第二类是针对文件内容操作(文件的读和写)。
Java中通过java.io.File类来对一个文件(包括目录)进行抽象的描述,有File对象并不代表真实存在该文件。
4.1File类
4.1.1属性
|---------------|---------------|------------------------|
| 修饰符及类型 | 属性 | 说明 |
| static String | pathSeparator | 依赖于系统路径分隔符,String类型的表示 |
| static char | pathSeparator | 依赖于系统的路径分隔符,char类型的表示 |
4.1.2构造方法
|----------------------------------|-------------------------------------------------------------------|
| 签名 | 说明 |
| File(File parent,String child) | 根据父目录+孩子文件路径,创建一个新的File实例 |
| File(String pathname) | 根据文件路径创建一个新的File实例,路径可以是绝对路径或者是相对路径 |
| File(String parent,String child) | 根据父目录+孩子文件路径,创建一个新的File实例,父目录用路径表示 parent表示当前文件所在的目录,child是自身文件名。 |
在new File对象的时候,构造方法参数中,可以指定一个路径,此时File对象就代表这个路径对应的文件。
4.1.3方法【不需要背】
|-------------|---------------------|--------------------------------------|
| 返回修饰符及返回值类型 | 方法签名 | 说明 |
| String | getparent() | 返回File对象父目录文件路径 |
| String | getName() | 返回File对象的纯文件名称 |
| String | getPath() | 返回File对象的文件路径 |
| String | getAbsolutePath() | 返回File对象的绝对路径 |
| String | getCanonicalPath() | 返回File对象的修饰过的绝对路径【绝对路径更加化简的表示方式】 |
| boolean | exists() | 判断File对象描述的文件是否真实存在 |
| boolean | isDirectory() | 判断File对象代表的文件是否是一个目录 |
| boolean | isFile() | 判断File对象代表的文件是否是一个普通文件 |
| boolean | createNewFile() | 根据File对象,自动创建一个空文件。成功创建后返回true |
| boolean | delete() | 根据File对象,删除该文件。成功删除后返回true |
| void | deleteOnExit() | 根据File对象,标注文件将被删除,删除动作会到JVM运行结束后才会进行 |
| String[] | list() | 返回File对象代表的目录下的所有文件名 |
| File[] | listFiles() | 返回File对象代表目录下的所有文件,以File对象表示 |
| boolean | mkdir() | 创建File对象代表的目录 |
| boolean | mkdirs() | 创建File对象代表的目录,如果必要,会创建中间目录 |
| boolean | renameTo(File dest) | 进行文件改名,也可以视为我们平时的剪切、粘贴操作 |
| boolean | canRead() | 判断用户是否对文件有可读权限 |
| boolean | canWrite() | 判断用户是否对文件有可写权限 |
4.1.3代码示例
(1)获取相应的路径
java
public class IOD1 {
public static void main(String[] args) throws IOException {
File file = new File("d:/test.txt");//这里的路径不需要真实的存在、
System.out.println(file.getPath());
System.out.println(file.getName());
System.out.println(file.getParent());
System.out.println(file.getAbsolutePath());
System.out.println(file.getCanonicalPath());
}
}
运行输出结果:
(2)普通文件的创建
java
public class IOD2 {
public static void main(String[] args) throws IOException {
File file = new File("d:/hello.txt");
System.out.println(file.exists());
System.out.println(file.isFile());
System.out.println(file.createNewFile());
System.out.println(file.isDirectory());
System.out.println(file.isFile());
}
}
运行输出结果:
(3)普通文件的删除
java
public class IOD3 {
public static void main(String[] args) {
File file = new File("d:/hello.txt");
System.out.println(file.delete());
}
}
运行输出结果:
延申:关于deleteOnExit 不会立即删除,是在程序运行完关闭之后才进行的删除。
(4)创建目录
需要注意的是:mkdir() mk是make的缩写!
java
public class IOD4 {
public static void main(String[] args) {
File dir = new File("d:/study");
dir.mkdir();
}
}
运行程序后就会在d盘生成一个名字为study的文件夹。
如果我们想创建多级目录。例如创建"d:/test/aaa/bbb",需要使用makedirs。
java
public class IOD4 {
public static void main(String[] args) {
File d = new File("d:/aaa/ddd/m");
d.mkdirs();
}
}
运行后的文件夹显示:
(5)文件重命名
java
public class IOD5 {
public static void main(String[] args) {
File dest = new File("./testAAA");
File file = new File("./test");
dest.renameTo(file);
}
}
创建一个名为testAAA的文件和一个名为test的文件,将testAAA改名为test;
4.2文件内容操作
针对文件内容,使用流对象进行操作。[流对象 可以类比于 水流]
Java标准库的流对象,从类型上,可以分为两个大类,一类是字节流,一类是字符流。
字节流[操作二进制文件]:InputStream OutputStream (FileInputStream FileOutputStream)
字符流[操作文本数据]:Reader Writer(FileReader FileWriter )
这些类的使用方式是非常固定的,核心操作就是四个:
(1)打开文件[构造对象]
(2)关闭文件[close]
(3)读文件[read针对InputStream 和 Reader]
(4)写文件[write针对OutputStream和Writer]
需要注意的是:InputStream OutputStream Reader Writer 都是抽象类,不能直接new
经典面试题--- 抽象类和接口什么区别?
抽象类比接口更抽象,抽象类的功能比接口更多。抽象类和普通的类相比差别不大,只不过抽象类不能new实例,带有抽象方法。抽象类可以有普通方法,也可以有抽象的方法。大部分的东西都是确定的有几个属性有几个方法,大部分都是明确的,只有一小部分是抽象方法。
接口里面都是抽象方法,接口中大部分都是不确定的,有什么属性都不知道,方法也都是抽象方法(不考虑default的情况)
因此,接口提供的信息更少,视为接口比抽象类更抽象。
4.2.1使用字节流来读取文件
(1)完整代码展示:
java
public class IOD6 {
//使用字节流来读取文件
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("f:/test.txt");
while(true){
int b = inputStream.read();
if(b == -1){
break;
}
System.out.println(""+(byte)b);
}
inputStream.close();
}
}
输出结果:
read() 一次读一个字节,但是这里使用的是int类型,原因:因为要表述"读取完毕"这样的情况,看起来是int,其实是byte,把int转为byte进行使用,另外需要判断当前的值是否是-1,是-1说明读取完毕,也就可以结束循环了。
(2)buffer版本代码:
java
public class IOD6 {
//使用字节流来读取文件
public static void main(String[] args) throws IOException {
InputStream inputStream = new FileInputStream("f:/test.txt");
while (true) {
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer);
if (len == -1) {
break;
}
for (int i = 0; i < len; i++) {
System.out.printf("%x\n", buffer[i]);
}
}
inputStream.close();
}
}
一次读1024个字节,并使用十六进制形式打印出来。
buffer理解:
缓冲区,缓和冲突,减少冲击的次数
存在的意义:为了提高IO的操作效率,单词的IO操作,就是要访问硬盘/IO设备,单次操作是比较消耗时间的,如果频繁的进行这样的IO操作,耗时肯定就更多了。
单次IO时间是一定的,如果缩短IO的次数,此时就可以提高程序整体的效率了。
以上第一个版本的代码是一次读一个字节,循环次数就比较高,read次数也很高
第二个版本的代码是一次读1024个字节,循环次数就降低了很多,read次数变少了。
4.2.2使用字节流来写文件
写文件有三个版本:
完整代码实现:
java
public class IOD7 {
//进行写文件
public static void main(String[] args) throws IOException {
OutputStream outputStream = new FileOutputStream("f:/test.txt");
outputStream.write(97);
outputStream.write(98);
outputStream.write(97);
outputStream.write(98);
outputStream.close();
}
}
现在在我们的f盘txt文件夹中的内容是:
当我们运行程序后再次打开txt文件,看到运行结果:
可以看到的是记事本中的内容发生了改变,输入的内容分别对应a和b的ASCII码值。
需要注意的是在outputStream进行写操作的时候总是将文件中的内容进行清空,再进行写入,如果不想清空,需要进行"追加写"操作。
4.2.3怎样理解这里说的读与写?【约定】
4.2.4关于close操作
(1)理解
close,关闭文件,释放资源。
进程在内核里,使用PCB这样的数据结构来表示进程。
一个线程可以对应一个PCB,一个进程可以对应一个PCB也可以对应多个。
PCB有一个重要的属性是文件描述符表。(相当于一个数组,记录了该进程打开了哪些文件) 即使一个进程里面有多个PCB也没关系,这些PCB共用同一个文件描述符表。
(2)如果close操作没写会如何?
如果没有close,对应的表项没有及时释放,虽然Java有GC,GC操作会在回收这个outputStream对象的时候去完成释放操作,但是GC不一定及时,如果不手动释放会导致文件描述符表可能很快被占满(文件描述符表不能自动扩容,是存在上线的),再次打开文件就会打开失败。
close一般来说是要执行的,但是如果程序要自始至终的使用,不关闭的问题也是不大的,随着进程结束,PCB销毁,文件描述符表也被销毁了,对应的资源操作系统就自动回收。如果close后程序立刻结束不执行close也没关系,但是在写代码的过程中还是需要注意这个close,如何确保close被执行到?
(3)确保close被执行?
try-ctach
我们首先可以想到的是使用try-catch执行:
可以但是不够美观,更推荐的一种写法是将变量的定义写到try( )里面,后面将不需要写close语句。
try with resources
没有显式的写close,但是实际上是会执行close的,只要try语句块执行完毕,就可以自动的执行到close,这个语法在Java中被称为是try with resources。
注意:不是随便拿一个对象放到try里面就可以自动释放的,需要满足的要求是实现Closeable接口,才可以。这个接口提供的方法就是close方法。
4.2.5使用字符流读文件
完整代码展示:
java
public class IOD8 {
public static void main(String[] args) {
try(Reader reader = new FileReader("f:/test.txt")) {
while(true){
int ch =reader.read();
if(ch == -1){
break;
}
System.out.println("" +(char)ch);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出结果:
如果输入汉字不能正确读取的话可能是编码的问题,需要改为utf-8编码。
4.2.6使用字符流写文件
(1)完整代码展示:
java
public class IOD9 {
public static void main(String[] args) {
//使用字符流写一个字符串
try(Writer writer = new FileWriter("f:/text.txt")){
writer.write("hello123");
} catch (IOException e) {
e.printStackTrace();
}
}
}
有的时候可以发现写的文件内容没有真的在文件中出现,很大可能是缓冲区的问题。
类似于这样的操作,其实是先写到缓冲区中(缓冲区有很多种形态,自己写的代码中有缓冲区,标准库中也可以有缓冲区,操作系统内核中也存在缓冲区),在写操作执行完后,内容可能在缓冲区中,还没真正的进入硬盘。
close操作,会触发缓冲区的"冲刷"(冲刷操作,就是把缓冲区里的内容写到硬盘中)
(2)flush-冲刷
除了close之外,也可以通过flush方法,也可以起到刷新缓冲区的效果。close操作后就关闭用不了了,flush操作还可以继续使用。
4.2.7Scanner
点进去可以发现:inputStream,输入流对象【字节流】
Scanner不仅仅可以针对键盘的输入进行读取。
java
public class IOD10 {
public static void main(String[] args) {
//Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = new FileInputStream("f:/test.txt")){
Scanner scanner = new Scanner(inputStream);
scanner.next();
}catch (IOException e){
e.printStackTrace();
}
}
}
Scanner的close本质上是要关闭内部包含的这个流对象,此时内部的inputStream对象已经被try()关闭了,里面的这个Scanner不关闭也是没问题的。
4.2.8两个小案例的实现
(1)扫描指定目录
找到名称中包含指定字符的所有普通文件(不包含目录),并且询问是否要删除该文件
完整代码实现:
java
public class IOD11 {
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
//首先让用户输入一个指定搜索的目录
// Scanner scanner = new Scanner(System.in);
System.out.println("请输入要搜索的路径");
String basePath = scanner.next();
//针对用户的输入进行一个简单的判定
File root = new File(basePath);
//判断是不是目录
if(!root.isDirectory()){
//路径不存在或者只是一个普通文件。无法进行搜索
System.out.println("当前输入的目录错误!");
return;
}
//让用户再输入一个要删除的文件名
System.out.println("请输入要删除的文件名");
//此处使用的是next来读字符串而不是nextln!!
//next是读到空白符就算是读完了。空白符包括换行,也可以是制表符,回车符
//nextln仅仅是去找换行符
//在控制台中,nextln是很难控制的
String nameToDelete = scanner.next();
//针对指定的路径进行扫描【递归操作】
//从根目录出发(root)
//先判断一下,当前的这个目录中是否包含要删除的文件,存在即删除,否则继续下一个
//如果当前目录中包含了一些目录,则针对子目录进行递归
scanDir(root,nameToDelete);
}
private static void scanDir(File root, String nameToDelete) {
System.out.println("[scanDir]" +root.getAbsolutePath());
//1.首先列出当前目录下包含的内容
File[] files = root.listFiles(); //看一下当前的目录中有啥,好比文件资源管理器双击目录打开
if(files == null){
//当前root目录下没东西,是一个空目录
//结束继续递归
return;
}
//2.遍历当前的列出结果
for(File f:files){
if(f.isDirectory()){
//如果是目录就进一步递归
scanDir(f,nameToDelete);
}else{
//如果是普通文件判定是否要删除
if(f.getName().contains(nameToDelete)){
System.out.println("确认删除?" +f.getAbsolutePath());
String choice = scanner.next();
if(choice.equals("y") || choice.equals("Y")){
f.delete();
System.out.println("删除成功");
}else{
System.out.println("取消删除操作");
}
}
}
}
}
}
控制台输出结果:
需要注意的是:
这里使用File对象表示列出的结果,结果有多个,使用数组表示。相当于看一下当前的目录中有什么,打开文件资源管理器,文件资源管理器中显示的这些结果,就相当于是listFiles得到的内容。
list方法也有类似的功效,但是list得到的只是String类型的文件名。
(2)文件拷贝
把第一个文件按照字节依次读取,把结果写入到另一个文件中。
需要构建"源路径"和"目标路径"两个路径,对于源路径来说要看源是不是存在,对于目标路径来说要看目标路径是不是不存在。
需要注意一下代码中读一个字节写一个字节的部分
完整代码实现:
java
public class IOD12 {
public static void main(String[] args) {
//输入两个路径
//第一个路径表示拷贝谁(源-src),第二个路径表示需要拷贝到哪里去(目的-dest)
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要拷贝哪个文件?");
String srcPath = scanner.next();
System.out.println("请输入要拷贝到哪个地方?");
String destPath = scanner.next();
//针对路径进行判定
File srcFile = new File(srcPath);
if(!srcFile.isFile()){
//如果源不是一个文件(是个目录或者不存在)
//此时就不做任何操作
System.out.println("当前输入的源路径错误!");
return;
}
File destFile = new File(destPath);
if(destFile.isFile()){
//如果已经存在了,认为也不能拷贝
System.out.println("当前输入的目标路径错误!");
return;
}
//进行拷贝操作
//try()语法支持多个流对象,写几个都可以进行关闭
//多个流对象之间使用分号进行分割就可以
try(InputStream inputStream = new FileInputStream(srcFile);
OutputStream outputStream = new FileOutputStream(destFile)) {
//进行读文件操作
while(true){
//读一个字节,写一个字节!!
int b = inputStream.read();
if(b == -1){
break;
}
outputStream.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
OutputStream在进行写文件的时候,文件不存在就会自动创建。
InputStream不行,文件不存在就会抛异常了。
到这里文件IO部分就大体结束啦!下一部分将开启网络部分的内容~