今天的任务:
- Map遍历的3种方式能独立写出来,知道区别
- 掌握File工具类的API
- 掌握迭代遍历文件夹中所有内容的代码的写法,能独立写出来
- 输出目标文件夹中所有的内容
- 统计目标文件夹的大小
- 掌握I/O流的基本概念,能够独立写出读取文件内容并输出到控制台的代码
复习
集合
- 保存批量数据(对象)的工具
- Collection接口体系
- List:有序可重复
- 新添加的元素会放到集合的最末端
- ArrayList
- LinkedList
- Set:无序不可重复
- HashSet:底层基于HashMap保存数据,所添加的元素作为底层Map的key进行存储,所有的value都是同一个静态常量(Object对象)
- hash表
- TreeSet:底层基于TreeMap保存数据,所添加的元素作为底层Map的key进行存储,所有的value都是同一个静态常量(Object对象)
- 排序二叉树-红黑树
- HashSet:底层基于HashMap保存数据,所添加的元素作为底层Map的key进行存储,所有的value都是同一个静态常量(Object对象)
- List:有序可重复
- Map接口体系
- 保存的都是键值对形式的数据
- 不允许出现重复的键,和一个键对多个值的映射
- API:
- map.put(key, value);
- value = map.get(key);
- 常用实现类
- HashMap:
- HashMap存储原理
- 将一个键值对保存到HashMap中时,首先调用key的hashCode方法,获取它的hash值
- 然后使用hash值和底层数组长度-1进行 & 运算,得到的结果作为该元素保存的下标
- 判断底层数组下标位置是否有元素,如果没有,创建一个Node对象,封装key和value,然后保存到数组该位置
- 如果数组目标位置有元素,使用要添加的key和该位置所有已存在的Node的key进行比较(equals方法)
- 如果有相等的,则使用新的value覆盖该位置的value
- 如果都不相等,创建一个Node对象,封装key和value,在数组该位置形成链表,新创建的Node对象放在链首,原有的对象向后移动
- 如果某个链表上的节点个数超过了8个,会自动转变成红黑树结构(JDK1.8)
- 如果一个对象要作为key保存到HashMap中,该对象的类必须重写hashCode和equals方法
- HashMap的扩容机制
- 底层数组的初始化长度为16,默认的加载因子是0.75,当保存的元素大于长度*加载因子,则触发扩容机制
- 首先新建一个数组,长度是原数组的2倍
- 遍历原数组中的所有Node,基于hash算法重新计算它们在新数组中的下标,然后将Node添加到新数组中
- HashMap底层数组长度一定是2的幂
- HashMap存储原理
- HashMap:
遍历Map集合
-
先获取所有的键再获取所有的值
-
直接获取所有的值并遍历
-
先获取所有的键值对再获取键和值(优先)
public class Test1 {
public static void main(String[] args) {
Student s1=new Student(1,"乔峰",35);
Student s2=new Student(2,"段誉",18);
Student s3=new Student(3,"虚竹",20);
Student s4=new Student(4,"慕容复",32);
Map<Integer,Student> map=new HashMap<>();
map.put(s1.getId(),s1);
map.put(s2.getId(),s2);
map.put(s3.getId(),s3);
map.put(s4.getId(),s4);
// 遍历Map的方式一
// map.keySet(); 返回所有key组成的set集合
// Set<Integer> set1=map.keySet();
// 需要遍历Map2次-第一次拿所有的key,第二次用key拿value
for(Integer key : map.keySet()){
System.out.println("key="+key+",value="+map.get(key));
}
// map.values(); 返回所有value组成的集合
// 只有value,没有key
for(Student s:map.values()){
System.out.println("value="+s);
}
// map.entrySet(); 返回所有entry组成的集合 - 推荐用法
// 这里遍历了一次Map,拿到了所有的键值对
Set<Entry<Integer,Student>> set2=map.entrySet();
// 这里的遍历,和Map没有关系了
for(Entry<Integer,Student> entry:set2){
System.out.println("key="+entry.getKey()+",value="+entry.getValue());
}
}
}
public class Test2 {
public static void main(String[] args) {
Student s1 = new Student(1,"乔峰",35);
Map<Student,String> map = new HashMap<>();
map.put(s1,s1.getName());
System.out.println("name="+map.get(s1)); // "乔峰"
s1.setAge(36);
System.out.println("name="+map.get(s1)); // null-说明map中没有这个键值对
// 原因:
// 1. Student重写了hashCode方法,标准逻辑是基于所有属性的值计算一个int的结果
// 2. 如果Student的任意属性值发生了变化,则hashCode值会发生变化
// 3. 最初将student作为key保存时,是基于旧的hashCode计算的数组下标并存放
// 4. 后面这次查询,是基于新的hashCode计算的数组下标并进行查找
// 5. 两次映射的下标不同,在新位置找不到旧的键值对,所以结果为null
// 如何规避这个问题:
// 要求Map中的Key的类型最好使用 Java中的不可变类
}
}
public class Test3 {
public static void main(String[] args) {
Student s1=new Student(1,"乔峰",35);
Student s2=new Student(2,"段誉",18);
Student s3=new Student(3,"虚竹",20);
Student s4=new Student(4,"慕容复",32);
// Map<Integer,Student> map=new HashMap<>(); // key是无序的
Map<Integer,Student> map=new LinkedHashMap<>(); // key是有序的
map.put(s3.getId(),s3);
map.put(s1.getId(),s1);
map.put(s4.getId(),s4);
map.put(s2.getId(),s2);
for(Map.Entry<Integer,Student> entry:map.entrySet()){
System.out.println("key="+entry.getKey()+",value="+entry.getValue());
}
}
}
public class Test4 {
public static void main(String[] args) {
Student s1=new Student(1,"乔峰",35);
Student s2=new Student(2,"段誉",18);
Student s3=new Student(3,"虚竹",20);
Student s4=new Student(4,"慕容复",32);
Map<Integer,Student> map=new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1; // 自定义的比较逻辑
}
});
// 按照Key进行排序
map.put(s3.getAge(),s3);
map.put(s1.getAge(),s1);
map.put(s4.getAge(),s4);
map.put(s2.getAge(),s2);
for(Map.Entry<Integer,Student> entry:map.entrySet()){
System.out.println("key="+entry.getKey()+",value="+entry.getValue());
}
// 需求:年龄从大到小排序
}
}
绝对路径和相对路径
绝对路径:从一个固定点到目标点的路径,在文件系统中,一般指从文件系统的根路径到目标文件/文件夹的路径
- 例如:
- windows:c:\user\admin\1.txt
- linux:\user\admin\1.txt
- 特点:
- 不论你当前在哪个文件或文件夹下,目标文件的绝对路径都是一致的
- 例如:从故宫到达内教育大厦的路径,与你当前所在的位置无关的
相对路径:从当前位置到目标点的路径,在文件系统中,一般指从当前文件夹到目标文件的路径
- 例如:
- windows:
- 例1:
- 当前文件夹:c:\user\
- 目标文件夹:c:\user\admin\1.txt
- 相对路径:admin\1.txt
- 例2:
- 当前文件夹:c:\user\admin
- 目标文件夹:c:\1.txt
- 相对路径:…/…/1.txt (…/表示返回上级目录,./表示的是当前目录)
- 例1:
- windows:
- 特点:
- 如果当前所在位置发生了变化,则相对路径发生变化
- 例如:我在达内教育大厦,搜索到故宫的路径,路径1。如果我现在在天坛,再搜索到故宫的路径,路径2,两个路径不同的。
问:绝对路径好还是相对路径好?
- 绝对路径优点:准确,在任何地方都通用
- 绝对路径的缺点:
- 可能会很长很复杂
- 如果大环境改变,则绝对路径可能会生效
- 相对路径的优点:
- 可以比较简短
- 不受大环境改变的影响,只要2个文件的相对位置不变,则路径不受影响
- 相对路径的缺点:
- 如果2个文件的相对位置发生变化,则路径失效
- 可能不适用于其他文件
File工具类
-
可以使用File对象来封装一个文件或目录的信息,包括文件(或目录)名称、所在路径、文件长度等等。
-
File类提供了一些处理和获取文件信息的方法。
-
还可基于File对象来打开一个文件进行读写操作。
-
在Java技术中,目录也被看作是一种文件。因此,无论文件和目录,都可以使用File类来统一处理。
-
File对象可封装指定文件或目录的信息。例如:
-
创建一个封装了名为"myfile.txt"文件的File对象:
File myFile = new File(“myfile.txt”);
-
创建一个封装了名为" MyDocs "目录的File对象:
File myDir = new File(“MyDocs”);
-
-
值得注意的是,File对象只封装文件或目录信息,它本身并不会打开文件甚至访问文件的内容。
-
关于文件名称的方法:
- String getName():返回文件(或目录)名称
- String getPath():返回文件(或目录)的所在路径
- String getAbsolutePath():返回文件(或目录)所在的绝对路径
- String getParent():返回文件(或目录)所在的父目录
- boolean renameTo(File newName):重命名文件(或目录)
-
关于目录方法:
- boolean mkdir():创建相应的目录
- 该方法仅能够创建1级目录,如果路径中存在多级未创建的目录,则该方法返回false,创建失败
- boolean mkdirs():一次性创建多级目录
- String[] list():返回目录下的文件列表
- File[] listFiles():返回目录下的文件列表,以File数组形式
- boolean mkdir():创建相应的目录
-
关于文件信息的方法:
- long lastModified():返回文件(或目录)的最后修改时间
- long length():返回文件(或目录)的长度
- boolean delete():删除文件(或目录)
- 不能直接删除非空的文件夹,需要先删除文件夹中所有内容,才能删除文件夹
-
关于文件测试的方法:
- boolean exists() :指示文件(或目录)是否存在
- 当使用new File(“path”)创建File对象时,该path对应的路径下不一定存在目标文件或文件夹,这并不影响File对象的创建
- 一个File对象既可以表示已经存在在硬盘上的文件或文件夹,也可以表示即将创建的文件或文件夹
- boolean isFile() :指示该对象所封装的是否为文件
- boolean isDirectory() :指示该对象所封装的是否为文件
- boolean exists() :指示文件(或目录)是否存在
public class TestFile {
public static void main(String[] args) {
File file = new File("/JavaSE/TestFile.java");
System.out.println("Name = " + file.getName());
System.out.println("Parent = " + file.getParent());
System.out.println("Path = " + file.getPath());
System.out.println("isDirectory = " + file.isDirectory());
System.out.println("isFile = " + file.isFile());
System.out.println("lastModified = " + file.lastModified());
System.out.println("length = " + file.length());
System.out.println("---------------------------");
File dir = new File("test");
dir.mkdir();
File curDir = new File("./");
String[] strs = curDir.list();
for (String str : strs) {
System.out.println(str);
}
}
}
I/O流
I/O流与文件操作
I/O 流的概念
-
I/O是Input/Output的缩写, I/O技术是非常实用的技术,如读/写文件,网络通讯等等。
-
流(Stream)是指从源节点到目标节点的数据流动。
-
源节点和目标节点可以是文件、网络、内存、键盘、显示器等等。
-
源节点的数据流称为输入流(用来读取数据)。
-
目标节点的数据流称为输出流(用来写入数据)。
流的分类
-
I/O流类库位于java.io包中。
-
从传输数据的角度,I/O流又可分为字节流和字符流
-
InputStream和OutputStream是字节流的基类, Reader和Writer是字符流的基类。
-
I/O流在文件读写、网络通讯、内存数据交换等方面被广泛应用。
读文件访问
- 无论是文本文件还是二进制文件,当需要读取文件数据时,需要完成以下步骤:
-
使用文件输入流打开指定文件:
- 对于文本文件,应使用字符输入流FileReader流
- 对于二进制文件,应使用字节输入流FileInputStream流
-
读取文件数据
-
关闭输入流
写文件访问
- 无论是文本文件还是二进制文件,当需要将数据写入文件时,需要完成以下步骤:
-
使用文件输出流打开指定文件:
- 对于文本文件,应使用字符输出流FileWriter流
- 对于二进制文件,应使用字节输出流FileOutputStream流
-
将数据写入文件
-
关闭输出流
-
在打开一个现有文件的输出流以准备写入数据时,有两种方式可供选择:
- 以清空方式打开
- 以添加方式打开
import java.io.*;
public class TestFileReader {
public static void main(String[] args) {
FileReader fr = null;
try {
fr = new FileReader(args[0]);
char[] buf = new char[100];
int n = fr.read(buf);
while (n != -1) { //-1表示到达文件尾部
for (int i = 0; i < n; i++ ){
System.out.print(buf[i]);
}
n = fr.read(buf);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
}
}
}
}
}
文本文件中的<回车>-<换行>序列
- 在Windows系统中,文本文件每行结尾都有两个不可见的特殊字符表示该行结束。
- 这两个字符为<回车>符(ASCII值为13)和<换行>符(ASCII值10 ),称为<回车>-<换行>序列。
- 在Unix系统中,文本文件每行结尾只有<换行>符。
- 在Java语言中, <回车>符用’\r’表示,<换行>符用’\n’表示。
- System.out.println语句,就是在输出一行内容后,继续输出<回车>-<换行>序列,从显示效果上使光标移动下一行开始。
练习
-
编写程序TextFile.java,在main方法中,读取TextFile.java文本文件,并将文件内容输出到屏幕上。
-
选做:改进该程序,读取文件内容后,在每行开始加上行号,再连同内容一并输出到屏幕上。
提示:可将读出的char数组转换为StringBuilder,然后在字符串中搜索“\n”,并在其之后插入行号即可。
import java.io.*;
public class TestFileWriter {
public static void main(String[] args) {
String[] text = {"这是第1行\r\n",
"这是一个TestWriter示例程序\r\n",
"需要用FileWriter打开文件\r\n",
"访问结束后需要关闭文件\r\n"};
FileWriter fw = null;
try {
fw = new FileWriter(args[0]);
char[] buf;
for(String str : text){
buf = str.toCharArray();
for (int i = 0; i < str.length(); i++ ){
System.out.print(buf[i]);
}
fw.write(buf, 0, str.length());
//或 fw.write(str, 0, str.length());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
}
}
}
}
}
练习:
-
编写程序FileCopy.java,在main方法中,将FileCopy.java复制为FileCopy.java.bak文件;
-
查看FileCopy.java.bak文件的内容,验证复制是否正确。
字符流的包装与链接
-
通常很少使用单个流对象,而是将一系列的流以包装的形式链接起来处理数据。
-
包装可以在不改变被包装流的前提下,获得更强的流处理功能。
-
典型的字符输入流/输出流的链接如下:
public class TestBufferedReader {
public static void main(String[] args) {
FileReader fr = null;
BufferedReader br = null;
try {
fr = new FileReader(args[0]);
br = new BufferedReader(fr);
String line = br.readLine();
while (line != null) {
System.out.println(line);
line = br.readLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
}
}
}
}
}
练习:
- 改写程序TextFile.java,使用Buffered包装形式读取TextFile.java文本文件,为每行加上行号,再连同内容一并输出到屏幕上。
import java.io.*;
public class TestFileWriter {
public static void main(String[] args) {
String[] text = {"这是第1行\r\n",
"这是一个TestWriter示例程序\r\n",
"需要用FileWriter打开文件\r\n",
"访问结束后需要关闭文件\r\n"};
FileWriter fw = null;
try {
fw = new FileWriter(args[0]);
char[] buf;
for(String str : text){
buf = str.toCharArray();
for (int i = 0; i < str.length(); i++ ){
System.out.print(buf[i]);
}
fw.write(buf, 0, str.length());
//或 fw.write(str, 0, str.length());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
}
}
}
}
}
练习:
-
改写程序FileCopy.java,使用Buffered包装形式,将FileCopy.java复制为FileCopy.java.bak文件;
-
查看FileCopy.java.bak文件的内容,验证复制是否正确。
字节流的包装与链接
-
与字符流一样,字节流也可以实现包装和链接,并且可以因此带来更为强大的功能,例如读写基本类型数据、读写对象等。
-
典型的字节输入流/输出流的链接如下:
public class TestOutputStream {
public static void main(String[] args) {
int num = 56789;
float f = 12345.56f;
boolean flag = true;
String str = "the quick fox jumps over the lazy dog.";
FileOutputStream fos = null;
BufferedOutputStream bos = null;
ObjectOutputStream oos = null;
try {
fos = new FileOutputStream(args[0]);
bos = new BufferedOutputStream(fos);
oos = new ObjectOutputStream(bos);
oos.writeInt(num);
oos.writeFloat(f);
oos.writeBoolean(flag);
oos.writeUTF(str);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
}
}
}
}
}
public class TestInputStream {
public static void main(String[] args) {
FileInputStream fis = null;
ObjectInputStream ois = null;
try {
fis = new FileInputStream(args[0]);
ois = new ObjectInputStream(fis);
int num = ois.readInt();
float f = ois.readFloat();
boolean flag = ois.readBoolean();
String str = ois.readUTF();
System.out.println(num);
System.out.println(f);
System.out.println(flag);
System.out.println(str);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
}
}
}
}
}
练习:
-
编写程序,在main方法中,随机生成0-100的整数50个,将这50个数以二进制形式写到文件中;
-
编写另一个程序,在main方法中,将前面所生成的50个数从文件中读取出来并打印。
转换流
- OutputStreamWriter:将字符流转成字节流
- InputStreamReader:把字节流转成字符流