Java 高级
集合
数组一经定义,长度且无法改变,所以不能添加和删除数据,在某些应用场景中我们必须对数据进行增删改查。所以就需要用到集合。集合是一种 可变 数据存储空间,数据容量可以发生改变,解决了数组的缺陷
List
ArrayList
集合长度可变原理
- 当创建
ArrayList
集合容器时,底层会存在一个长度为10
的空数组 - 如果长度超过了
10
则自动创建一个新的数组容器并扩容原数组的1.5
倍 - 将旧数组的数据自动拷贝到新数组中
- Java 垃圾回收机制会自动把旧数组删除
集合与数组分别的应用场景:当存储的元素个数固定不变时选择使用数组,否则使用集合
初始化一些数据
import java.util.*;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
System.out.println(list); // [1, 2, 3]
}
}
增删改查
方法
方法名 | 说明 |
---|---|
public boolean add(要添加的元素) | 将指定的元素追加到此集合的末尾 |
public boolean remove(要删除的元素) | 删除指定元素,返回值表示是否删除成功 |
public E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
public E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 |
public E get(int index) | 返回指定索引处的元素 |
public int size() | 返回集合中的元素的个数 |
public void clear() | 清空数组中所有的数据 |
public boolean isEmpty() | 判断数据是否为空,有数据就返回true,反之false |
public void allAdd() | 将指定集合中的所有元素添加到另一个集合中 |
代码示例
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
// <String>是泛型,用于约束集合中的数据类型
ArrayList<String> list = new ArrayList<String>();
// 添加
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
list.add("ee");
list.add("ff");
System.out.println(list); // [aa, bb, cc, dd, ee, ff]
// 删除
list.remove("aa"); // 根据元素删除
list.remove(1); // 根据元素下标删除
System.out.println(list); // [bb, dd, ee, ff]
// 修改
list.set(1, "aa");
System.out.println(list); // [bb, aa, ee, ff]
// 获取
System.out.println(list.get(1)); // aa
// 长度
System.out.println(list.size()); // 4
}
}
注意: 集合不能存储基本数据类型如:int
、double
、boolean
等
封装查询数据方法
// Main.java
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<User> list = new ArrayList<User>();
User u1 = new User("heima001", "zhangsan", "123456");
User u2 = new User("heima002", "lisi", "1234");
User u3 = new User("heima003", "wangwu", "1234qwer");
list.add(u1);
list.add(u2);
list.add(u3);
// 通过ID获取数据
String data = getIndex("heima002", list);
System.out.println(data);
}
// 封装查询数据方法
public static String getIndex(String id, ArrayList list) {
byte len = (byte) list.size();
for (byte i = 0; i < len; i++) {
User item = (User) list.get(i);
if (id.equals(item.id)) {
return String.format("%s %s %s", item.id, item.username, item.password);
}
}
return "没有找到该数据~";
}
}
// User.java
public class User {
String id;
String username;
String password;
public User(String id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
}
遍历方式
import java.util.ArrayList;
import java.util.Iterator;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
// 传统写法
for(int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println("--------------------");
// 增强版for循环写法
for (String s : list) {
System.out.println(s);
}
System.out.println("--------------------");
// 使用forEach写法
list.forEach(new Consumer<String>() {
// 该方法在每次遍历集合时会把当前的元素传递给accept方法然后执行
@Override
public void accept(String s) {
System.out.println(s);
}
});
System.out.println("--------------------");
// 使用Lambda表达式简化forEach写法
list.forEach(s -> System.out.println(s));
System.out.println("--------------------");
// 使用迭代器写法
Iterator<String> it = list.iterator();
while(it.hasNext()) {
String s = it.next();
System.out.println(s);
}
}
}
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<User> list = new ArrayList<>();
list.add(new User("张三", 30));
list.add(new User("李四", 10));
list.add(new User("王五", 20));
// 查询list数组中为张三的元素并删除;
list.removeIf(user -> user.getName().equals("张三"));
// 对list数组中的每个元素的年龄加10;
list.forEach((user -> user.setAge(user.getAge() + 10)));
System.out.println(list);
}
}
扩展: 批量给集合添加初始数据
public class Main {
public static void main(String[] args) {
// 方法一
List<Integer> l1 = new ArrayList<>();
l1.addAll(Arrays.asList(1, 2, 3));
System.out.println(l1);
// 方法二
List<Integer> l2 = new ArrayList<>(Arrays.asList(1, 2, 3));
System.out.println(l2);
// 方法三
List<Integer> l3 = new ArrayList<>();
Collections.addAll(l3, 1, 2, 3);
System.out.println(l3);
}
}
LinkedList
LinkedList
和 ArrayList
是 Java
集合框架中的两种不同的列表实现方式。它们在以下几个方面有所不同:
- 存储结构:
ArrayList
底层使用数组实现,而LinkedList
使用双向链表实现。 - 插入和删除操作: 插入和删除元素时,
ArrayList
需要移动元素来保持连续性,而LinkedList
只需要调整指针的指向,效率更高。 - 随机访问:
ArrayList
可以通过索引来随机访问列表中的元素,时间复杂度为O(1);而LinkedList
需要从头节点开始遍历,时间复杂度为O(n)。
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
LinkedList<Object> list = new LinkedList<>();
// 添加
list.add("a");
list.add("b");
list.add("c");
// 在指定位置插入元素
list.add(1, "d");
// 查询
System.out.println(list); // [a, d, b, c]
// 修改
list.set(0, "e"); // 修改指定位置的元素
System.out.println(list); // [e, d, b, c]
// 删除
list.remove(); // 默认删除第一个元素
list.remove("c"); // 删除指定元素
list.remove(1); // 删除指定位置的元素
// 查询
System.out.println(list); // [d]
}
}
特有的方法
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
LinkedList<Object> list = new LinkedList<>();
list.add("a");
list.add("b");
list.add("c");
System.out.println(list);
list.addFirst("d"); // 在头部添加元素
list.addLast("e"); // 在尾部添加元素
System.out.println(list); // [d, a, b, c, e]
System.out.println(list.getFirst()); // 获取第一个数据:d
System.out.println(list.getLast()); // 获取第二个数据:e
}
}
基于以上差异,两者在不同的场景下有不同的应用:
ArrayList
适用于频繁进行随机访问以及插入和删除操作较少的场景。由于其支持随机访问的特性,适用于需要根据索引进行快速查找和访问元素的场景。LinkedList
适用于频繁进行插入和删除操作的场景。由于其插入和删除操作的高效性,适用于需要频繁在列表中间插入或删除元素的场景。
需要注意的是 LinkedList
相比 ArrayList
会占用更多的内存空间,因为每个节点都需要维护前后节点的引用。因此,在选择使用哪种列表实现时,要根据具体场景需求进行权衡和选择。
总结:
ArrayList
集合 底层是数组结构实现,查询快、增删慢
LinkedList
集合 底层是链表结构实现,查询慢、增删快
Collections
注意 Collections
并不是集合,它比 Collection
多了一个 s
,一般后缀为 s
的类很多都是工具类。这里的Collections是用来操作 Collection
的工具类。它提供了一些好用的静态方法
import java.util.Collections;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
// 批量添加数据
Collections.addAll(list,"c", "b", "a");
System.out.println(list); // [c, b, a]
// 随机打乱排序
Collections.shuffle(list);
System.out.println(list); // [b, c, a]
// 数据排序
Collections.sort(list);
System.out.println(list); // [a, b, c]
}
}
上面我们往集合中存储的元素要么是 Stirng
类型,要么是 Integer
类型,他们本来就有一种自然顺序所以可以直接排序
但如果我们往 ArrayList
集合中存储 Student
对象,这个时候就需要对 ArrayList
集合进行自定义比较规则
方法一
import java.util.Collections;
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("蜘蛛精", 23, 169.7));
list.add(new Student("紫霞", 22, 169.8));
list.add(new Student("紫霞", 22, 169.8));
list.add(new Student("至尊宝", 26, 169.5));
Collections.sort(list);
}
}
// 继承Comparable<Student>接口,重写对应的排序方法
public class Student implements Comparable<Student> {
private String name;
private int age;
private double height;
public Student(String name, int age, double height){
this.name = name;
this.age = age;
this.height = height;
}
// 重写compareTo方法
@Override
public int compareTo(Student o){
return this.getAge() - o.getAge();
}
// get / set方法...
}
方法二
import java.util.Collections;
import java.util.ArrayList;
import java.util.Comparator;
public class Main {
public static void main(String[] args) {
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("蜘蛛精", 23, 169.7));
list.add(new Student("紫霞", 22, 169.8));
list.add(new Student("紫霞", 22, 169.8));
list.add(new Student("至尊宝", 26, 169.5));
System.out.println(list);
// 使用匿名内部类实现重写compare方法
Collections.sort(list, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();
}
});
// 通过lambda表达式实现简化上述写法
Collections.sort(list, (o1, o2) -> o1.getAge() - o2.getAge());
// 推荐这么写
list.sort((o1, o2) -> o1.getAge() - o2.getAge());
System.out.println(list);
}
}
Collections 常见的操作
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 添加数据
list.add("aaa");
// 批量添加数据
Collections.addAll(list, "bbb", "ccc", "ddd", "eee");
System.out.println(list); // [aaa, bbb, ccc, ddd, eee]
// 随机打乱集合数据
Collections.shuffle(list);
System.out.println(list); // [aaa, ddd, ccc, eee, bbb]
// 排序:从小到大
Collections.sort(list);
System.out.println(list); // [aaa, bbb, ccc, ddd, eee]
// 排序:从大到小
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
// return a.compareTo(b); // 升序:[aaa, bbb, ccc, ddd, eee]
return b.compareTo(a); // 降序:[eee, ddd, ccc, bbb, aaa]
}
});
System.out.println(list); // [eee, ddd, ccc, bbb, aaa]
// 简写:从大到小
Collections.sort(list, (a, b) -> b.compareTo(a));
System.out.println(list); // [eee, ddd, ccc, bbb, aaa]
// 从大到小排序
list.sort((a, b) -> a.compareTo(b));
System.out.println(list); // [aaa, bbb, ccc, ddd, eee]
// 将指定的数组转换为不可改变的数组并返回
List<String> newList = Collections.unmodifiableList(list);
System.out.println(newList); // [aaa, bbb, ccc, ddd, eee]
// newList.add("fff"); // 报错:不能添加、修改、删除
list.add("fff"); // 允许
System.out.println(list); // [aaa, bbb, ccc, ddd, eee, fff]
}
}
Set
Set
集合是一种无序的集合,它不保留元素的插入顺序,并且不允许重复元素。这意味着当你通过 Set
集合访问元素时,你不能依赖于元素的顺序。如果你需要有序的集合,可以考虑使用 List
接口的实现类,例如 ArrayList
或 LinkedList
HashSet
如果 HashSet
集合中有重复的数据,那么会自动去重
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<String> list = new HashSet<>();
list.add("张三");
list.add("李四");
list.add("张三");
list.add("王五");
for(String item : list) {
System.out.println(item);
}
// 李四
// 张三
// 王五
}
}
HashMap
HashMap
是一种键值对映射的集合,它不允许重复的键。当你向 HashMap
中插入相同的键时,新的键值对会覆盖旧的键值对。
增删改查
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, List> list = new HashMap<>();
// 新增
list.put("1", List.of(1, 2));
list.put("2", List.of(3, 4));
list.put("3", List.of(5, 6));
// 删除:返回值是被删除的值
System.out.println(list.remove("2")); // [3, 4]
// 修改:返回值是被修改之前的值
System.out.println(list.put("1", List.of(1, 2, 3))); // [1, 2]
// 查询
System.out.println(list); // {1=[1, 2], 3=[5, 6]}
}
}
虽然值可以重复,但键必须是唯一的。因此,HashMap
中的键是无重复的,但值可以重复。
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, String> list = new HashMap<>();
// 添加数据
list.put("jack", "111");
list.put("rose", "222");
list.put("tom", "333");
// 获取数据
System.out.println(list.get("jack")); // 111
// 获取map的所有key
Set<String> keys = list.keySet();
System.out.println(keys); // [jack, rose, tom]
// 获取map的所有value
Collection<String> values = list.values();
System.out.println(values); // [111, 222, 333]
System.out.println("--------------------");
// 增强for循环遍历键
for (String k : keys) {
System.out.println(k + " : " + list.get(k));
}
System.out.println("--------------------");
// 迭代器遍历键
Iterator<String> it = keys.iterator();
while (it.hasNext()) {
String k = it.next();
System.out.println(k);
}
System.out.println("--------------------");
// lambda遍历键
keys.forEach(k -> System.out.println(k));
}
}
entrySet
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, String> list = new HashMap<>();
// 添加数据
list.put("jack", "111");
list.put("rose", "222");
list.put("tom", "333");
// 获取所有的键值对
System.out.println(list.entrySet());
// [tom=333, rose=222, jack=111]
// 通过增强for循环遍历
for (Map.Entry<String, String> entry : list.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
System.out.println("------------------------");
// 通过迭代器遍历
Iterator<Map.Entry<String, String>> it = list.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println(entry.getKey() + " : " + entry.getValue());
}
System.out.println("------------------------");
// 通过lambda表达式遍历
list.entrySet().forEach(entry -> System.out.println(entry.getKey() + " : " + entry.getValue()));
}
}
复杂应用场景
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, List<Student>> list = new HashMap<>();
List<Student> l1 = new ArrayList<>();
l1.add(new Student("张三", 13));
l1.add(new Student("李四", 23));
List<Student> l2 = new ArrayList<>();
l2.add(new Student("王五", 33));
List<Student> l3 = new ArrayList<>();
l3.add(new Student("张亮", 25));
list.put("A", l1);
list.put("B", l2);
list.put("C", l3);
for (Map.Entry<String, List<Student>> e : list.entrySet()) {
System.out.println(e.getKey());
// 如果数组有值就遍历
if (!e.getValue().isEmpty()) {
for (Student item : e.getValue()) {
System.out.println(item.name + " " + item.age);
}
}
}
}
}
Stream
假如有一个 List
集合,元素有 "张三丰","张无忌","周芷若","赵敏","张强"
,找出姓张,且是 3
个字的名字,存入到一个新集合中去。
定义数据
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<User> list = new ArrayList<>();
list.add(new User(1, "张三", 18, "上海"));
list.add(new User(2, "王五", 16, "上海"));
list.add(new User(3, "李四", 20, "上海"));
list.add(new User(4, "张雷", 22, "北京"));
list.add(new User(5, "张超", 15, "深圳"));
list.add(new User(6, "李雷", 24, "北京"));
list.add(new User(7, "王爷", 21, "上海"));
list.add(new User(8, "张三丰", 18, "广州"));
list.add(new User(9, "赵六", 16, "广州"));
list.add(new User(10, "赵无极", 26, "深圳"));
}
}
初体验
传统写法:
list.forEach(s -> {
if (s.name.startsWith("张") && s.name.length() == 3) {
System.out.println(s.name);
// 张三丰
// 张无忌
}
});
Stream写法:
list.stream().filter(item -> item.name.startsWith("张") && item.name.length() == 3).forEach(item -> System.out.println(item.name));
// 张三丰
// 张无忌
forEach
遍历所有的数据
list.stream().forEach(item -> System.out.println(item.name));
// 张三
// 王五
// 李四
// ...
sorted
根据年龄对数据进行排序
// 根据年龄从小到大排序
list.stream().sorted((a, b) -> a.age - b.age).forEach(item -> System.out.println(item.name));
// 根据年龄从大到小排序
list.stream().sorted((a, b) -> b.age - a.age).forEach(item -> System.out.println(item.name));
filter
返回过滤后的数据
// 过滤出姓张且名为3个字的用户数据
list.stream().filter(item -> item.name.startsWith("张") && item.name.length() == 3).forEach(item -> System.out.println(item.name));
// 张三丰
// 张无忌
// 查找id为5的数据
list.stream().filter(item -> item.id == 5).forEach(item -> System.out.println(item.name));
// 张超
map
返回被处理后的数据
// 给每个用户名称之前拼接数据并返回
List<String> s = list.stream().map(item -> "姓名:" + item.name).toList();
System.out.println(s);
// [姓名:张三, 姓名:王五, 姓名:李四, ...]
// 给所有用户数据的年龄增加10岁并返回
List<Integer> n = list.stream().map(item -> item.age + 10).toList();
System.out.println(n);
// [28, 26, 30, 32, 25, 34, 31, 28, 26, 36]
peek
对数据进行处理
// 对所有用户的年龄增加10岁
List<User> data = list.stream().peek(item -> item.age += 10).toList();
for (User item : data) {
System.out.printf("姓名:%s 年龄:%d\n", item.name, item.age);
// 姓名:张三 年龄:28
// 姓名:王五 年龄:26
// 姓名:李四 年龄:30
// ...
}
疑惑点: map
与 peek
的区别是什么?
答: map
用于对流中的元素进行转换,生成一个新的流,而peek
用于在流处理过程中查看元素,对其进行操作,但不改变元素的值。
limit
获取指定个数的数据
// 只获取前三个数据
list.stream().limit(3).forEach(item -> System.out.println(item.name));
skip
从哪个位置开始获取数据
// 从第八个获取到最后一个数据
list.stream().skip(8).forEach(item -> System.out.println(item.name));
// 赵六
// 张无忌
// 从第三个开始,获取4个数据
list.stream().skip(2).limit(4).forEach(item -> System.out.println(item.name));
// 李四
// 张雷
// 张超
// 李雷
模拟分页查询
// 每页显示多少个数据
int page = 3;
for (int i = 1; i <= 4; i++) { // 第几页
// 当前索引
int startIndex = (i - 1) * page;
list.stream().skip(startIndex).limit(page).forEach(item -> System.out.println(item.name));
System.out.println("--------------");
}
toList
// 将stream转换为集合
// List<User> data = list.stream().toList();
List<User> data = list.stream().collect(Collectors.toList()); // 推荐方式
count
统计数量
// 统计姓张的用户数量
long n = list.stream().filter(item -> item.name.startsWith("张")).count();
System.out.printf("姓张的有:%d个人", n);
// 姓张的有:5个人
max / min
返回最大值与最小值的数据
// 求出最大的年龄
int max = list.stream().max((a, b) -> Integer.compare(a.age, b.age)).get().age;
System.out.printf("最大的年龄为:%d", max);
// 最大的年龄为:26
// 找出年龄最大与最小的用户
User max = list.stream().max((a, b) -> Integer.compare(a.age, b.age)).get();
System.out.printf("年龄最大的用户是:%s 他的年龄为:%d", max.name, max.age);
// 年龄最大的用户是:张无忌 他的年龄为:26
User min = list.stream().min((a, b) -> Integer.compare(a.age, b.age)).get();
System.out.printf("年龄最大的用户是:%s 他的年龄为:%d", min.name, min.age);
// 年龄最小的用户是:张超 他的年龄为:15
distinct
去重
File
exists
判断指定的目录或文件是否存在,存在就返回 true
反之 false
import java.io.File;
public class Main {
public static void main(String[] args) {
File f1 = new File("src");
File f2 = new File("src/1.txt");
// 既可以判断目录,也可以判断文件是否存在
System.out.println(f1.exists()); // true
System.out.println(f2.exists()); // true
}
}
isFile
判断指定的文件是否存在,存在就返回 true
反之 false
import java.io.File;
public class Main {
public static void main(String[] args) {
File f1 = new File("src");
File f2 = new File("src/1.txt");
// 不能判断目录,只能判断文件是否存在
System.out.println(f1.isFile()); // false
System.out.println(f2.isFile()); // true
}
}
isDirectory
判断是否是文件夹,是就返回 true
反之 false
import java.io.File;
public class Main {
public static void main(String[] args) {
File f1 = new File("src");
File f2 = new File("src/1.txt");
// 是文件夹
System.out.println(f1.isDirectory()); // true
// 不是文件夹
System.out.println(f2.isDirectory()); // false
}
}
getName
获取文件的名称
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("src/1.txt");
System.out.println(f.getName()); // 1.txt
}
}
length
获取文件的大小,返回字节个数
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("src/1.txt");
System.out.println(f.length()); // 12
}
}
lastModified
获取文件上一次被修改的时间
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("src/1.txt");
System.out.println(f.lastModified());
// 1696926150928
}
}
getPath
获取创建文件对象时的路径 src/1.txt
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("src/1.txt");
// 字符串
System.out.println(f.getPath()); // src\1.txt
// File对象
System.out.println(f); // src\1.txt
}
}
getAbsolutePath
获取文件的绝对路径
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("src/1.txt");
System.out.println(f.getAbsolutePath());
// C:\Users\33111\IdeaProjects\liuyuyang\src\1.txt
}
}
getParentFile
获取指定文件的所有父级目录
import java.io.File;
public class Main {
public static void main(String[] args) {
File file = new File("a/b/c/d/e/f/g.txt");
System.out.println(file.getParentFile());
// a\b\c\d\e\f
}
}
createNewFile
如果创建成功返回 true
,反之 false
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("src/2.txt");
System.out.println(f.createNewFile());
}
}
注意: createNewFile
只能创建文件,不能创建目录
mkdir
与 createNewFile
使用相似,不同的是该方法只能创建目录
import java.io.File;
public class Main {
public static void main(String[] args) {
// 创建单个目录
File f1 = new File("src/aaa");
System.out.println(f1.mkdirs());
// 创建多级目录
File f2 = new File("src/aaa/bbb");
System.out.println(f2.mkdirs());
}
}
delete
如果删除成功返回 true
,反之 false
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("src/1.txt");
System.out.println(f.delete());
}
}
list
通过该方法可以将指定目录下的所有目录以及文件以字符串数组形式获取
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("src");
String[] files = f.list();
for (String file : files) {
System.out.println(file);
// aaa
// Err.java
// Main.java
}
}
}
listFiles
通过该方法可以将指定目录下的所有目录以及文件以 File
对象数组形式获取
import java.io.File;
public class Main {
public static void main(String[] args) {
File f = new File("src");
File[] files = f.listFiles();
for (File item : files) {
System.out.println(item.getName() + " ~ " + item);
// a.txt ~ src\a.txt
// abc ~ src\abc
// Anno.java ~ src\Anno.java
// Main.java ~ src\Main.java
// User.java ~ src\User.java
}
}
}
使用 listFiles 方法时的注意事项:
- 当主调是文件,或者路径不存在时,返回
null
- 当主调是空文件夹时,返回一个长度为
0
的数组 - 当主调是一个有内容的文件夹时,将里面所有一级文件和文件夹的路径放在
File
数组中返回 - 当主调是一个文件夹,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在
File
数组中返回,包含隐藏文件 - 当主调是一个文件夹,但是没有权限访问该文件夹时,返回
null
练习
练习一: 过滤出指定目录中的所有以 .java
作为后缀的文件
import java.io.File;
import java.io.FileFilter;
public class Main {
public static void main(String[] args) {
File f = new File("src");
// 过滤文件
File[] files = f.listFiles(new FileFilter() {
@Override
public boolean accept(File data) {
// 只要返回值为true就返回哪个数据
// 返回以.java后缀的所有文件
return data.toString().endsWith(".java");
}
});
for (File item : files) {
System.out.println(item);
}
}
}
练习二: 遍历指定文件目录中所有的目录,不含文件
import java.io.File;
public class Main {
public static void main(String[] args) {
File dir = new File("src/abc");
System.out.println(dir); // src/abc
// 遍历abc文件目录下所有的目录
listAllFiles(dir, "");
}
public static void listAllFiles(File dir, String tab) {
File[] files = dir.listFiles();
for (File item : files) {
// 只打印文件目录
if (item.isDirectory()) {
System.out.println(tab + item.getName());
// 通过递归实现无限遍历,直到遍历完毕为止
listAllFiles(item, tab + "\t");
// a
// aa
// bb
// b
// bb
// c
}
}
}
}
练习三: 查询指定目录中是否存在 .avi
格式的文件,如果有就返回 true
反之 false
import java.io.File;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
File file = new File("src");
System.out.println(haveAVI(file));
}
// 查询指定目录中是否存在.avi格式的文件,如果有就返回true 反之false
public static boolean haveAVI(File file) {
File[] list = file.listFiles();
for (File item : list) {
if (file.isFile() && item.getName().endsWith(".avi")) {
return true;
}
}
return false;
}
}
练习四: 使用代码创建目录 a/b/c/d/e/f/g.txt
import java.io.File;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
File file = new File("a/b/c/d/e/f/g.txt");
// 判断父级目录是否存在
if(!file.getParentFile().exists()){
// 如果父级目录不存在就创建
file.getParentFile().mkdirs();
}
// 创建文件g.txt
file.createNewFile();
}
}
疑惑点: list
方法与 listFiles
方法都可以获取到所有的目录、文件列表,那么他们的区别是什么?
答: list
获取的是文件的名称,而 listFiles
获取的是文件对象
IO
字节流
FileOutputStream
操作本地文件的字节输出流,可以吧程序中的数据写到本地文件中
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("src/a.txt");
// FileOutputStream fos = new FileOutputStream(new File("src/a.txt"));
// 将数据97写入到a.txt:97对应的Asllc码值为a,所以在a.txt文件中的内容为a
fos.write(97);
// 然后又将b写入到文件中,此时内容为:ab
fos.write(98);
// 释放资源
fos.close();
}
}
批量将数据写入到 a.txt
文件中:abcde
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("src/a.txt");
// 批量将数据写入到a.txt文件中:abcde
byte[] list = {97, 98, 99, 100, 101};
fos.write(list);
// 释放资源
fos.close();
}
}
将数据转换为字节并写入数据
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("src/a.txt");
// 将数据转换为字节
byte[] bytes = "你好呀".getBytes();
System.out.println(Arrays.toString(bytes));
// [-28, -67, -96, -27, -91, -67, -27, -111, -128]
// 一键写入数据
fos.write(bytes);
fos.close();
}
}
将该数组从第二个开始往后取 3
个数据批量写入到 a.txt
文件中:cde
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("src/a.txt");
// 将该数组从第二个开始往后取3个数据批量写入到a.txt文件中:cde
byte[] list = {97, 98, 99, 100, 101};
fos.write(list, 2, 3);
// 释放资源
fos.close();
}
}
使用细节
① 创建字节输出流对象
参数可以是字符串路径,也可以是File对象
如果文件不存在 会创建一个新的文件,但必须保证父级路径是存在的
如果文件已经存在,则会清空文件
② 写入数据
write
方法的参数为整数类型,但实际上写到本地文件中的是整数在 ASCll
上对应的字符
③ 释放资源
每次使用完留之后都要释放资源
write
方法的参数为整数类型,但实际上写到本地文件中的是整数在 ASCll
上对应的字符
换行 & 续写
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
// 如果第二个参数为true则表示支持续写,默认情况下为false表示不续写,每次写入的内容会覆盖之前的内容
FileOutputStream fos = new FileOutputStream("src/a.txt", true);
// 数据内容
String content1 = "Hello";
// 将内容转换为byte格式
byte[] content1Bytes = content1.getBytes();
System.out.println(Arrays.toString(content1Bytes)); // [72, 101, 108, 108, 111]
// 将转换后的byte对应的字符写入到文件内容中
fos.write(content1Bytes);
// 换行:为了兼容性,我们通常使用"\r\n"来表示换行
String wrap = "\r\n";
byte[] wrapBytes = wrap.getBytes();
fos.write(wrapBytes);
String content2 = "World";
byte[] content2Bytes = content2.getBytes();
fos.write(content2Bytes);
// 释放资源
fos.close();
// 此时a.txt文件的内容为:
// Hello
// World
}
}
FileInputStream
操作本地文件的字节输入流,将本地文件中的数据读取到程序中
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fos = new FileInputStream("src/a.txt");
// 每次调用read方法都会获取a.txt文件中的一个数据
System.out.println(fos.read()); // 97 -> a
System.out.println(fos.read()); // 98 -> b
System.out.println(fos.read()); // 99 -> c
System.out.println(fos.read()); // 100 -> d
System.out.println(fos.read()); // 101 -> e
// 如果没有数据了就返回-1
System.out.println(fos.read()); // -1
// 释放资源
fos.close();
}
}
使用细节
① 创建字节输入流对象
如果文件不存在就直接报错
② 写入数据
- 一次性只能读一个字节,读出来的是数据在
ASCll
上对应的数字 - 读取到文件末尾后,
read
方法就会返回-1
③ 释放资源
每次使用完留之后都要释放资源
通过循环批量获取数据
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fos = new FileInputStream("src/a.txt");
int b;
// 循环读取数据
while ((b = fos.read()) != -1) {
System.out.printf("%d -> %c | ", b, (char) b);
// 97 -> a | 98 -> b | 99 -> c | 100 -> d | 101 -> e |
}
// 释放资源
fos.close();
}
}
一次性读取多个数据
import java.io.FileInputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("src/a.txt");
byte[] bytes = new byte[5];
int len;
while ((len = fis.read(bytes)) != -1) {
System.out.print(len + " - "); // 每次读取的个数
System.out.println(new String(bytes, 0, len)); // 每次读取的数据
// 5 - abcde
// 2 - fg
// System.out.write(bytes, 0, len);
}
fos.close();
}
}
拷贝文件
将 mac.jpg
这张图片拷贝一张
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("mac.jpg");
FileOutputStream fos = new FileOutputStream("macCory.jpg");
// 记录开始时间
long start = System.currentTimeMillis();
System.out.println("拷贝中 请稍后~");
int len;
while ((len = fis.read()) != -1) {
fos.write(len);
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.printf("拷贝成功 总用时:%d毫秒", end - start);
// 拷贝成功 总用时:7407毫秒
fis.close();
fos.close();
}
}
优化拷贝时间
上面拷贝文件因为一次只能拷贝一个数据,所以导致非常缓慢,所以我们可以这样做,使每次可以拷贝多个数据从而优化拷贝文件的时间
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("src/a.txt");
byte[] bytes = new byte[2];
// bytes数组中存储的个数
// System.out.println(fis.read(bytes)); // 2
fis.read(bytes);
System.out.println(new String(bytes)); // ab
fis.read(bytes);
System.out.println(new String(bytes)); // cd
fis.read(bytes);
// 因为最后一个数据是e,e将cd的c覆盖之后就没有数据了
// 而d并没有被覆盖。所以此时就是:ed
System.out.println(new String(bytes)); // ed
fis.read(bytes);
System.out.println(new String(bytes)); // ed
fos.close();
}
}
上面代码存在一些问题,可以发现有一个多余的数据。所以这并不是最佳的解决方法
我们可以这么做来解决上面的问题
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("src/a.txt");
byte[] bytes = new byte[2];
// bytes数组中存储的个数
System.out.println(fis.read(bytes)); // 2
// 读取2个数据
int l1 = fis.read(bytes);
System.out.println(l1); // 2
System.out.println(new String(bytes, 0, l1)); // ab
// 读取2个数据
int l2 = fis.read(bytes);
System.out.println(l2); // 2
System.out.println(new String(bytes, 0, l2)); // cd
// 读取1个数据
int l3 = fis.read(bytes);
System.out.println(l3); // 1
System.out.println(new String(bytes, 0, l3)); // e
fos.close();
}
}
循环读取数据
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("src/a.txt");
byte[] bytes = new byte[2];
int len;
while ((len = fis.read(bytes)) != -1) {
System.out.print(new String(bytes, 0, len));
}
fos.close();
}
}
下面是文件拷贝缓慢的解决方案: 默认一次只能读取一个数据,我们可以设置为一次读取 5
个数据,这样拷贝的时间就快了好几倍
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("mac.jpg");
FileOutputStream fos = new FileOutputStream("macCory.jpg");
long start = System.currentTimeMillis();
System.out.println("拷贝中 请稍后~");
int len;
byte[] bytes = new byte[1024 * 1024 * 5];
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
long end = System.currentTimeMillis();
System.out.printf("拷贝成功 总用时:%d毫秒", end - start);
// 拷贝成功 总用时:4毫秒
fis.close();
fos.close();
}
}
经过这样的优化后,文件拷贝的速度快了几千倍
疑惑点: new byte[]
括号中的值越大,读取就越快。那为什么不能写成 99999999999999
呢,岂步更快?
byte[] bytes = new byte[1024 * 1024 * 5];
答: 将这个值设置得过大可能会浪费内存资源。原因是,在读取文件时,内存中只能容纳有限数量的数据。如果一次性读取的数据超过内存的限制,就会导致内存溢出的情况发生,程序可能会因此崩溃。
所以我们一般根据文件的大小来决定,比如一个 10M
的视频,那么就写 1024 * 1024 * 10
相当于一次读取 10M
的数据,这样做最合适
编码 & 解码
默认为操作系统编码,而国内 window
系统编码为 utf-8
所以编码与解码都为 utf-8
格式,也可以指定为其他格式
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws UnsupportedEncodingException {
System.out.println("------编码------");
String s1 = "爱你哟";
// 不传参就是系统默认的编码格式,国内window操作系统默认编码为UTF-8
byte[] list1 = s1.getBytes();
System.out.println(Arrays.toString(list1));
// [-25, -120, -79, -28, -67, -96, -27, -109, -97]
// 爱:-25, -120, -79
// 你:-28, -67, -96
// 呦:-27, -109, -97
String s2 = "爱你哟";
// 指定为GBK编码
byte[] list2 = s2.getBytes("GBK");
System.out.println(Arrays.toString(list2));
// [-80, -82, -60, -29, -45, -76]
// 爱:-80, -82
// 你:-60, -29
// 呦:-45, -76
System.out.println("------解码------");
// 默认是UTF-8格式解码
System.out.println(new String(list1)); // 爱你哟
// 不传值相当于使用默认UTF-8,而这个是GBK数据,所以解码时候会乱码
System.out.println(new String(list2)); // ����Ӵ
// 指定为GBK格式解码
System.out.println(new String(list2, "GBK")); // 爱你哟
fos.close();
}
}
根据上述代码可以看出,UTF-8
编码一次性读取 3 个字节,而 GBK
一次读取 2 个字节
字符流
FineReader
字符输入流用来将文件中的字符数据读取到程序中来,他比 FileInputStream
更适合读取中文数据
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("src/a.txt");
int ch;
while ((ch = fr.read()) != -1) {
System.out.printf("%d %s | ", ch, (char) ch);
// 20320 你 | 22909 好 | 32 | 74 J | 97 a | 118 v | 97 a |
}
fr.close();
}
}
疑惑点: FileInputStream
与 FineReader
都可以实现读取数据,那么他们的区别是什么?
答: FileReader
与 FileInputStream
都可以用来读取文件中的中文数据。然而,它们确实有一些区别。
FileReader
是用来读取字符流的,它是 InputStreamReader
类的子类,并且使用默认的字符编码。它将文件中的字节流解码成字符流,因此适用于读取文本文件。对于读取文本文件中的中文数据,使用 FileReader
是一个比较方便的选择。
FileInputStream
是用来读取字节流的。它可以读取任何类型的文件,包括文本文件和二进制文件。读取文本文件时,需要在读取后进行字符编码处理才能正确地解析中文数据。
一次性读取多个数据
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("src/a.txt");
char[] chars = new char[2];
int len;
while ((len = fr.read(chars)) != -1) {
// 把数组中的数据变成字符串再打印
System.out.print(new String(chars, 0, len));
// 你好 Java
}
fr.close();
}
}
FileUtils
commons-io
有 apache
第三方组织开源项目,对使用者提供 jar
包
步骤1:拷贝jar包
步骤2:启用jar包(将jar包应用到项目中)
基本使用
import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
public class Main {
public static void main(String[] args) throws IOException {
// ----------拷贝文件----------
File A_File = new File("src/a.txt");
File B_File = new File("src/b.txt");
// 将a.txt的内容拷贝给b.txt
FileUtils.copyFile(A_File, B_File);
// ----------拷贝文件夹----------
File A_Dir = new File("src/a");
File B_Dir = new File("src/b");
// 将a文件夹里面的所有东西全部拷贝给b文件夹
FileUtils.copyDirectory(A_Dir, B_Dir);
// ----------删除文件----------
FileUtils.deleteQuietly(new File("src/b.txt"));
// ----------删除文件夹----------
FileUtils.forceDelete(new File("src/a.txt")); // 删除的文件不存在会报错
FileUtils.deleteDirectory(new File("src/b")); // 不会报错
// ----------读写文件----------
File file = new File("src/a.txt");
// 写入数据
FileUtils.writeStringToFile(file, "你好呀!", "utf8");
// 读取数据
String data = FileUtils.readFileToString(file, "utf8");
System.out.println(data);
}
}
过滤目录
参数二表示需要过滤的文件后缀名,不指定可以写 null
参数三表示是否深度过滤,如果为 true
表示过滤所有子目录,反之只过滤同级中的文件
例子: 过滤出 src/a
目录下所有以 .java
作为后缀的文件
System.out.println(FileUtils.listFiles(new File("src/a"), new String[]{"java"}, true));
// [src\a\c\ddd.java, src\a\hello.java]
反射
public class User {
public String name = "zs";
private int age = 18;
public User(){
System.out.println("User~");
}
public User(int i){
System.out.println("User" + i);
}
public String info() {
System.out.println("info");
return "aaa";
}
private int info(int i, String s) {
System.out.println("info" + i + s);
return 123;
}
}
通过反射调用类的构造方法
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
// 获取class
Class clazz = Class.forName("User");
// 获取构造方法
Constructor cons1 = clazz.getConstructor();
Constructor cons2 = clazz.getConstructor(int.class);
// 调用无参构造
cons1.newInstance(); // User~
// 调用有参构造方法
cons2.newInstance(123); // User123
}
}
通过反射获取、修改类的属性
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
// 获取class
Class clazz = Class.forName("User");
// 获取实例
Object obj = clazz.newInstance();
// 获取属性
// Field nameAttribute = clazz.getField("name");
Field nameAttribute = clazz.getDeclaredField("name");
Field ageAttribute = clazz.getDeclaredField("age");
// 访问公有属性
System.out.println(nameAttribute.get(obj)); // zs
// 访问私有属性
ageAttribute.setAccessible(true); // 设置为可访问的
System.out.println(ageAttribute.get(obj)); // 18
// 修改属性
nameAttribute.set(obj, "ls");
ageAttribute.set(obj, 20);
System.out.println(nameAttribute.get(obj)); // ls
System.out.println(ageAttribute.get(obj)); // 20
}
}
注意: getField
只能获取 public
修饰的属性,不能获取私有属性。而 getDeclaredField
都可以获取
通过反射调用类的公共属性和方法
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// 获取class
Class clazz = Class.forName("User");
// 获取实例
Object obj = clazz.newInstance();
// 获得User类的info方法
Method infoMethod = clazz.getDeclaredMethod("info");
// 通过invoke调用类的info方法
Object result = infoMethod.invoke(obj); // info
// 打印该方法的返回值
System.out.println(result); // aaa
}
}
通过反射调用类的私有属性和方法
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// 获取class
Class clazz = Class.forName("User");
// 获取实例
Object obj = clazz.newInstance();
// 获得方法User类的info私有方法
Method infoMethod = clazz.getDeclaredMethod("info", int.class, String.class);
// 将私有方法设置为可访问的
infoMethod.setAccessible(true);
// 通过invoke调用类的info方法
Object result = infoMethod.invoke(obj, 100, "_sss"); // 100_sss
// 打印该方法的返回值
System.out.println(result); // 123
}
}
注解
基本使用
public @interface Anno {
public String value() default "";
public String name() default "";
public int age() default 0;
}
@Anno(name = "ls", age = 20)
public class User {
// @Anno()
// @Anno("")
@Anno(name = "ww")
public String name = "zs";
@Anno(age = 20)
public void info() {
System.out.println("info");
}
}
元注解
@Retention
自定义声明周期、保留时间
属性 | 含义 |
---|---|
RetentionPolicy.SOURCE | 源码,自定义注解只能在源码中使用,字节码中没有。提供给编译器使用 |
RetentionPolicy.CLASS | 字节码,自定义注解在源码、字节码中使用。运行后没有。提供运行器使用 |
RetentionPolicy.RUNTIME | 运行时,自定义注解在源码、字节码、运行时都可以使用。开发中常见 |
举个栗子: 获取某个方法被注解的数据
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Anno {
public String name() default "";
public int age() default 0;
}
public class User {
@Anno(name = "zs", age = 20)
public void info() {
System.out.println("info");
}
public void show() {
System.out.println("show");
}
}
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("User");
Method infoMethod = clazz.getMethod("info");
// 获取info方法上的Anno注解对象
Anno anno = (Anno) infoMethod.getAnnotation(Anno.class);
System.out.println(anno); // @Anno(name="zs", age=20)
System.out.println(anno.name()); // zs
System.out.println(anno.age()); // 20
}
}
@Target
用于确定 自定义注解
使用位置(类、属性、构造、方法等)
属性 | 含义 |
---|---|
ANNOTATION_TYPE | 可以用于注解类型的声明 |
CONSTRUCTOR | 可以用于构造方法的声明 |
FIELD | 可以用于字段(成员变量)的声明 |
LOCAL_VARIABLE | 可以用于局部变量的声明 |
METHOD | 可以用于方法的声明 |
MODULE | 可以用于模块的声明(Java 9+) |
PACKAGE | 可以用于包的声明 |
PARAMETER | 可以用于方法参数的声明 |
TYPE | 可以用于类、接口、枚举等类型的声明 |
如果不写 @Taget
则表示在任何位置都可以使用该注解
举个栗子: 执行某个类中所有被注解的方法
// 表示只能被方法使用
@Target({ElementType.METHOD})
// 表示可以通过反射来获取被注解的属性或方法
@Retention(RetentionPolicy.RUNTIME)
public @interface Anno {
}
public class User {
@Anno
public void info() {
System.out.println("info");
}
public void show() {
System.out.println("show");
}
}
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// 获取User类
Class clazz = Class.forName("User");
// 获取类实例
Object obj = clazz.newInstance();
// 获取指定类中所有的方法
Method[] allMethods = clazz.getMethods();
for (Method method : allMethods) {
// 判断当前方法是否被Anno注解
boolean r = method.isAnnotationPresent(Anno.class);
// 如果被注解就调用
if (r) method.invoke(obj);
}
}
}
多线程
启动一个多线程:定义一个类继承于 Thread
,然后重写 run
方法,创建子类对象并启动线程
// 定义一个类继承于Thread
public class MyThread extends Thread {
// 重写run方法
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(getName() + i);
}
}
}
public class Main {
public static void main(String[] args) {
// 创建子类对象
MyThread mt = new MyThread();
// 启动线程
mt.start();
}
}
public class Main {
public static void main(String[] args) {
MyThread mt1 = new MyThread();
mt1.setName("线程A:");
mt1.start();
MyThread mt2 = new MyThread();
mt2.setName("线程B:");
mt2.start();
}
}