黑马程序员Java学习笔记 - 下部(从基础到源码)

十三、集合

集合:提供一种存储空间可变的存储模型,存储的数据容量可以发生改变。

集合相对于数组的优势

  1. 长度可变
  2. 添加数据的时候不需要考虑索引,默认将数据添加到末尾

集合体系结构(重点)

集合只能存引用数据类型,如果要存基本数据类型,需要存对应的包装类。

image-20250531162614721

建议先学ArrayList再学其他,因为所有东西都是基于ArrayList举例的。

1.泛型

由于泛型和集合紧密相关,所以在学习集合前,需要先了解泛型。

泛型就是数据类型的泛指,可以泛指任何引用数据类型,将来可以被任何引用数据类型替代。

泛型的定义格式

泛型是用一对尖括号<>包裹的内容,尖括号内可以是任意字符。

  • <类型>: 指定一种类型的格式。尖括号里面可以任意书写,一般只写一个字母。例如: <E><T>等。
  • <类型1,类型2…>:指定多种类型的格式,多种类型之间用逗号隔开。例如: <E,T><K,V>等。

1.1 泛型类

在编写类时,如果不确定类型(如成员变量的类型),那么这个类就可以定义成泛型类。

格式

1
2
3
4
类名<类型>

类名<类型1,类型2…>
Lobster AIjava运行123

1.2 泛型方法

方法中形参类型不确定时就可以使用泛型方法。

方案一、使用类名后面定义的泛型(这个泛型所有方法都能用)

方案二、在方法声明上定义自己的泛型(这个泛型只有此方法可以用)

格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
方案一:
class 类名<T>{
修饰符 T|指定类型 方法名(T|指定类型 变量名){

}
}
方案二:
修饰符<类型> 返回值类型 方法名(类型 变量名){

}

class Student<T>{
public String show (T param){
return "OK";
}
}
public<T> void show(T param){

}
Lobster AIjava运行12345678910111213141516171819

1.3 泛型接口

格式

1
2
3
4
5
6
7
8
修饰符 interface 接口名<类型>{

}

public interface List<E>{

}
Lobster AIjava运行1234567

方式一、实现类给出具体类型

1
2
3
4
public class MyList implements List<String>{

}
Lobster AIjava运行123

方式二、实现类延续泛型,创建对象时再确定

1
2
3
4
5
6
public class MyList<E> implements List<E>{

}

MyList<String> ml=new MyList<>;
Lobster AIjava运行12345

1.4 泛型的通配符

泛型不具备继承性,但是数据具备继承性

1
2
3
4
5
6
7
8
9
10
11
12
//Fu是Ye的子类
ArrayList<Ye> list1;
ArrayList<Fu> list2;

method(list2) //出错,因为泛型不具备继承性
list1.add(new Fu());
method(list1) //不会报错,因为数据具备继承性

public static void method(ArrayList<Ye> list){

}
Lobster AIjava运行1234567891011

通配符格式

? extends E:表示可以传递E或者E的所有子类类型

? super E:表示可以传递E或E的所有父类类型

1
2
3
4
5
6
7
8
9
10
11
//Fu是Ye的子类
ArrayList<Ye> list1;
ArrayList<Fu> list2;

method(list2) //不会出错,因为表示可以Ye的子类类型Fu
method(new Fu()) //不会报错,因为数据具备继承性

public static void method(? extends Ye list){

}
Lobster AIjava运行12345678910

<?><T>区别

  • 单独的<?>表示可以通配任意类型,但是将来不能被其他数据类型替换,只起到通配的作用。
  • <T>将来必须被引用类型替换,可以被<?>替换。
  • <?>用于泛型类型的使用(如方法参数),不能用于泛型类和泛型方法的定义。
  • <T>既可以用于泛型类型的使用(如方法参数),也可以用于泛型类和泛型方法的定义。
1
2
3
4
5
6
7
8
9
10
11
12
// 泛型方法:通过<T>声明类型变量,可在方法中使用T
public static <T> void copy(List<T> src, List<T> dest) {
dest.addAll(src);
}

// 通配符方法:使用?表示未知类型,无法操作具体类型
public static void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
}
Lobster AIjava运行1234567891011

案例:数组扩容

由于数组的长度本身是不可变的,如果想要进行数组扩容,就需要重新准备一个更大的数组,把原数组的数据拷贝进来,最后把原来数组的空间释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ArrayUtils {
/**
* 扩容数组并释放原数组空间
* @param original 原数组
* @param newCapacity 新容量
* @param <T> 数组类型
* @return 扩容后的新数组
* @throws IllegalArgumentException 如果新容量小于原数组长度
*/
public static <T> T[] resizeArray(T[] original, int newCapacity) {
//新数组容量必须大于原数组长度
if (newCapacity < original.length) {
throw new IllegalArgumentException("New capacity must be larger than original array size");
}

// 创建新数组并复制元素
T[] newArray = Arrays.copyOf(original, newCapacity);

//返回扩容后的数组
return newArray;
}

public static void main(String[] args) {
// 示例:扩容Integer数组
Integer[] arr = {1, 2, 3};
System.out.println("原数组: " + Arrays.toString(arr));

arr = resizeArray(arr, 5); // 扩容数组,由于arr重新赋值后原数组空间就没有变量指向它,所以JVM自动释放空间
System.out.println("扩容后数组: " + Arrays.toString(arr));
}
}
Lobster AIjava运行12345678910111213141516171819202122232425262728293031

2.单列集合

2.1 Collection集合

2.1.1 概述和使用
  • Collection集合概述
    • 是单例集合的顶层接口,它表示一组对象,这些对象也称为Collection的元素。
    • JDK 不提供此接口的任何直接实现。它提供更具体的子接口(如Set和List)实现。
  • 创建Collection集合的对象
    • 多态的方式。
    • 具体的实现类ArrayList。
  • 常用方法
方法名 说明
boolean add(E e) 添加元素
boolean remove(Object o) 从集合中移除指定的元素
boolean removeIf(Predicate<? super E> filter) 根据条件进行移除
void clear() 清空集合中的元素
boolean contains(Object o) 判断集合中是否存在指定的元素
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中元素的个数

removeIf(Predicate<? super E> filter)

1
2
3
4
5
6
7
8
Collection<Person> people = new ArrayList<>();
people.add(new Person("Alice", 20));
people.add(new Person("Bob", 15));
people.add(new Person("Charlie", 18));

// 移除未成年人
people.removeIf(person -> person.getAge() < 18);
Lobster AIjava运行1234567
2.1.2 Collection集合的遍历

迭代器遍历

  • 迭代器介绍

    • 迭代器:集合的专用遍历方式。
    • Iterator<E> iterator():返回此集合中元素的迭代器,通过集合对象的iterator()方法得到。
  • Iterator中的常用方法

    • boolean hasNext():判断当前位置是否有元素可以被取出。
    • E next():获取当前位置的元素,将迭代器对象移向下一个索引位置。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
       		Collection<String> c = new ArrayList<>();     
    //获取迭代器
    Iterator<String> it = c.iterator();

    //用while循环改进元素的判断和获取
    while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
    //获取完毕后,删除集合中的元素
    it.remove();
    }
    //循环结束后,指针不会复位,如果继续使用hasNext,会出现NoSuchElementException异常
    //如果还想要遍历,就需要重新获取迭代器对象
    Lobster AIjava运行12345678910111213
  • 迭代器中删除的方法

    • void remove():删除迭代器对象当前指向的元素(原集合数据发生改变)。

细节

  • 如果当前位置没有元素,还要强行获取,会报NoSuchElementException
  • 迭代器遍历完毕,指针不会复位。
  • 循环中只能用一次next方法(如果用两次,会出现一些问题)。
  • 迭代器遍历时,不能用集合的方法增加或删除元素。

增强for遍历

  • 介绍

    • 它是JDK5之后出现的,其内部原理是一个Iterator迭代器。
    • 实现Iterable接口的类才可以使用迭代器和增强for(不用理会,集合底层已经实现了)。
    • 简化数组和Collection集合的遍历。
  • 格式

    1
    2
    3
    4
    for(集合/数组中元素的数据类型 变量名 :  集合/数组名) {
    // 已经将当前遍历到的元素封装到变量中了,直接使用变量即可
    }
    Lobster AIjava运行123
    1
    2
    3
    4
    5
    6
    7
    8
    		//1.数据类型一定是集合或者数组中元素的类型
    //2.str仅仅是一个变量名而已,在循环的过程中,依次表示集合或者数组中的每一个元素
    //3.改变str的值不会改变list集合的值
    //4.list就是要遍历的集合或者数组
    for(String str : list){
    System.out.println(str);
    }
    Lobster AIjava运行1234567

lambda表达式遍历

利用forEach方法,再结合lambda表达式的方式进行遍历。

  • default void forEach(Consumer<? super T> action);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
		//1.创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("zhangsan");
coll.add("lisi");
coll.add("wangwu");

//2.利用匿名内部类的形式
//底层原理:
//其实也会自己遍历集合,依次得到每一个元素
//把得到的每一个元素,传递给下面的accept方法
//s依次表示集合中的每一个数据
coll.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});

//lambda表达式
coll.forEach(s -> System.out.println(s));
Lobster AIjava运行1234567891011121314151617181920

2.2 List集合

2.2.1 List集合

特点

  • 存取有序:存和取的顺序是一致的
  • 可以重复:可以存放重复的数据
  • 有索引:用户可以根据索引获取数据,或操作指定索引处的数据

List集合的特有方法

方法名 描述
void add(int index,E element) 在此集合中的指定位置插入指定的元素
E remove(int index) 删除指定索引处的元素,返回被删除的元素
E set(int index,E element) 修改指定索引处的元素,返回被修改的元素
E get(int index) 返回指定索引处的元素

list能够继承collection类中的方法,所以collection类中的方法在list中也能用。如remove(Object o)方法删除指定的元素。

List集合的五种遍历方式

  1. 迭代器
  2. 列表迭代器
  3. 增强for
  4. Lambda表达式
  5. 普通for循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public static void main(String[] args) {
//创建集合并添加元素
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");

//1.迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String str = it.next();
System.out.println(str);
}


//2.增强for
for (String s : list) {
System.out.println(s);
}

//3.Lambda表达式
list.forEach(s -> System.out.println(s));


//4.普通for循环
//size方法跟get方法还有循环结合的方式,利用索引获取到集合中的每一个元素
for (int i = 0; i < list.size(); i++) {
//i:依次表示集合中的每一个索引
String s = list.get(i);
System.out.println(s);
}

// 5.列表迭代器
//获取一个列表迭代器的对象,里面的指针默认也是指向0索引的
//额外添加了一个方法:在遍历的过程中,可以添加元素
ListIterator<String> itlist = list.listIterator();
while (itlist.hasNext()) {
String str = itlist.next();
if ("bbb".equals(str)) {
itlist.add("qqq");
}
}
System.out.println(list); //[aaa, bbb, qqq, ccc]
}
Lobster AIjava运行1234567891011121314151617181920212223242526272829303132333435363738394041424344

注意点

如果List集合的泛型是Integer,那么调用remove(1)方法是会存在一个问题,是删除指定的元素1还是删除指定索引的元素?

答:如果Java中在调用方法的时候,如果方法出现了重载现象,会优先调用实参跟形参类型一致的那个方法。所以是删除指定索引上的元素。

2.2.2 ArrayList集合
  • List接口的实现类
  • 特点:长度可变,只能存储引用数据类型。
  • 泛型的使用用于约束集合中存储元素的数据类型
  • 打印ArrayList对象打印的不是地址值,而是集合中存储数据内容,在展示的时候会拿[]把所有的数据进行包裹
  • 底层是数组结构实现,查询快、增删慢

ArrayList类常用方法

  • 构造方法:
方法名 说明
public ArrayList() 创建一个空的集合对象
  • 成员方法:
方法名 说明
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() 返回集合中的元素的个数
2.2.3 LinkedList集合
  • List接口的实现类
  • 特点:长度可变,只能存储引用数据类型。
  • 泛型的使用用于约束集合中存储元素的数据类型
  • 打印ArrayList对象打印的不是地址值,而是集合中存储数据内容,在展示的时候会拿[]把所有的数据进行包裹
  • 底层是链表结构实现,查询慢、增删快

特有方法

方法名 说明
public void addFirst(E e) 在该列表开头插入指定的元素
public void addLast(E e) 将指定的元素追加到此列表的末尾
public E getFirst() 返回此列表中的第一个元素
public E getLast() 返回此列表中的最后一个元素
public E removeFirst() 从此列表中删除并返回第一个元素
public E removeLast() 从此列表中删除并返回最后一个元素
2.2.4 源码分析
ArrayList源码分析

核心步骤:

  1. 创建ArrayList对象的时候,他在底层先创建了一个长度为0的数组。

    数组名字:elementDate,定义变量size。

    size这个变量有两层含义:
    ①:元素的个数,也就是集合的长度
    ②:下一个元素的存入位置

  2. 添加元素,添加完毕后,size++

扩容时机:

  1. 当存满时候,会创建一个新的数组,新数组的长度,是原来的1.5倍,也就是长度为15。再把所有的元素,全拷贝到新数组中。
  2. 如果一次添加多个元素,1.5倍放不下,那么新创建数组的长度以实际为准。
  3. 如果扩容后的数组也满了,会继续按照上述规则扩容。
  • 添加一个元素时的扩容

image-20250531184617505

  • 添加多个元素时的扩容

image-20250531184724677

LinkedList源码分析

底层是双向链表结构。

核心步骤如下:

  1. 刚开始创建的时候,底层创建了两个变量:一个记录头结点first,一个记录尾结点last,默认为null
  2. 添加第一个元素时,底层创建一个结点对象,first和last都记录这个结点的地址值
  3. 添加第二个元素时,底层创建一个结点对象,第一个结点会记录第二个结点的地址值,last会记录新结点的地址值

image-20250531184852757

迭代器源码分析

迭代器遍历相关的三个方法:

  • Iterator<E> iterator():获取一个迭代器对象。
  • boolean hasNext():断当前指向的位置是否有元素。
  • E next() :取当前指向的元素并移动指针。

image-20250531185122111

2.3 Set集合

2.3.1 Set集合
  • 存取是否有序取决于实现类。
  • 不可以存储重复元素。
  • 没有索引:不能使用普通for循环遍历。

特有成员方法

1
2
3
4
5
6
7
8
9
10
11
12
<T> T[] toArray(T[] a) //返回一个包含此 set 集合中所有元素的数组;返回数组的类型是指定数组的类型。
如果指定的数组a能容纳该set集合中的所有元素,则它将在其中返回。
否则,将分配一个具有指定数组的运行时类型和此set大小的新数组。

例如:
Set<String> fruits = new HashSet<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
String[] array3 = fruits.toArray(new String[0]); //建议传入空数组
System.out.println("Array3: " + Arrays.toString(array3)); //Array3: [Apple, Banana, Cherry]
Lobster AIjava运行1234567891011

Set能够继承collection类中的方法,所以collection类中的方法在Set中也能用。如remove(Object o)方法删除指定的元素。

2.3.2 Hashset集合

特点

  • Set接口的实现类。
  • 底层数据结构是哈希表。
  • 存取无序:存和取的顺序不一样。
  • 不可以存储重复元素。
  • 没有索引,不能使用普通for循环遍历。

哈希值

  • 哈希值是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值
  • 如何获取哈希值:Object类中的public int hashCode():返回对象的哈希码值
  • 哈希值的特点
    • 同一个对象多次调用hashCode()方法返回的哈希值是相同的
    • 默认情况下,不同对象的哈希值是不同的(地址值不同)。而重写hashCode()方法,可以实现让不同对象的哈希值相同

哈希表的结构

  • JDK8以前

创建一个默认长度16,默认加载因子0.75的数组,数组名为table。

根据元素的哈希值跟数组的长度计算出应存入的位置:

1
2
int index = (数组长度 - 1) & 哈希值;
Lobster AIjava运行1

新元素存入数组,老元素挂在新元素下面:数组 + 链表

image-20250531191140255

  • JDK8以后

新元素直接挂在老元素的下面:

  • 节点个数少于等于8个:数组 + 链表
  • 节点个数多于8个:数组 + 红黑树

image-20250531191403154

HashSet集合存储自定义类型元素,要想实现元素的唯一,要求必须重写hashCode方法和equals方法

2.3.3 LinkedHashSet集合

特点

  • Set接口的实现类。
  • 底层数据结构依然是哈希表,只是每个元素又多了一个双向链表用来记录存储的顺序。
  • 存取有序:存和取的顺序一致(原因就在于双向链表)。
  • 不重复:不可以存储重复元素。
  • 无索引:不能使用普通for循环遍历。

image-20250531192110708

2.3.4 TreeSet集合

特点

  • Set接口的实现类。
  • 底层使用红黑树来管理元素。
  • 不可以存储重复元素。
  • 没有索引:不能使用普通for循环遍历。
  • 可以将元素按照规则进行排序
    • public TreeSet():根据其元素的自然排序进行排序
    • public TreeSet(Comparator comparator) :根据指定的比较器进行排序

自然排序Comparable的使用

TreeSet集合默认的规则:

  • 对于基本数据类型:Integer,Double,默认按照从小到大进行升序排序。
  • 对于字符、字符串类型:按照字符在ASCII码表中的数字升序排序。
  • 对于引用数据类型,如果不使用带参构造集合对象,就必须实现Comparable接口并重写compareTo(T o)方法。
  • 案例需求
    • 存储学生对象并遍历,创建TreeSet集合使用无参构造方法
    • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
  • 实现步骤
    1. 使用空参构造创建TreeSet集合
      • 用TreeSet集合存储自定义对象,无参构造方法使用的是自然排序对元素进行排序的
    2. 自定义的Student类实现Comparable接口
      • 自然排序,就是让元素所属的类实现Comparable接口,重写compareTo(T o)方法
    3. 重写接口中的compareTo方法
      • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Student implements Comparable<Student>{
private String name;
private int age;
...
//this:表示当前要添加的元素
//o:表示已经在红黑树存在的元素
//返回值:
//负数:认为要添加的元素是小的,放左边
//正数:认为要添加的元素是大的,放右边
//0:认为要添加的元素已存在,舍弃。
//this-o代表升序
//o-this代表降序
@Override
public int compareTo(Student o) {
//按照对象的年龄进行排序
//主要判断条件: 按照年龄从小到大排序
int result = this.age - o.age;
//次要判断条件: 年龄相同时,按照姓名的字母顺序排序
result = result == 0 ? this.name.compareTo(o.getName()) : result;
return result;
}
}
Lobster AIjava运行12345678910111213141516171819202122

比较器排序Comparator的使用

  • 案例需求
    • 存储老师对象并遍历,创建TreeSet集合使用带参构造方法
    • 要求:按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
  • 实现步骤
    • 用TreeSet集合存储自定义对象,带参构造方法使用的是比较器排序对元素进行排序的
    • 比较器排序,就是让集合构造方法接收Comparator的实现类对象,重写compare(T o1,T o2)方法
    • 重写方法时,一定要注意排序规则必须按照要求的主要条件和次要条件来写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
		//创建集合对象
TreeSet<Teacher> ts = new TreeSet<>(new Comparator<Teacher>() {
@Override
public int compare(Teacher o1, Teacher o2) {
//o1表示现在要存入的那个元素
//o2表示已经存入到集合中的元素

//主要条件
int result = o1.getAge() - o2.getAge();
//次要条件
result = result == 0 ? o1.getName().compareTo(o2.getName()) : result;
return result;
}
});
//创建老师对象
...
//把老师添加到集合
...
//遍历集合
...
Lobster AIjava运行1234567891011121314151617181920

两种比较方式总结

  • 两种比较方式小结
    • 自然排序:自定义类实现Comparable接口,重写compareTo方法,根据返回值进行排序。
    • 比较器排序:创建TreeSet对象的时候传递Comparator的实现类对象,重写compare方法,根据返回值进行排序。
    • 在使用的时候,默认使用自然排序,当自然排序不满足现在的需求时,必须使用比较器排序。
  • 两种方式中关于返回值的规则
    • 如果返回值为负数,表示当前存入的元素是较小值,存左边。
    • 如果返回值为0,表示当前存入的元素跟集合中元素重复了,不存。
    • 如果返回值为正数,表示当前存入的元素是较大值,存右边。

3.双列集合

3.1 Map集合

双列集合:把两个元素当成一个集合元素的集合,比如商品和价格,一件商品对应一个价格,商品就是键,价格就是值。

Map接口是双列集合的顶层接口,不能直接创建它的对象,但是可以常见它的实现类对象。

1
2
interface Map<K,V>  K:键的类型;V:值的类型
Lobster AIjava运行1

双列集合的特点

  • 一次需要存一对数据,分别是键和值。
  • 键不能重复,值可以重复。
  • 每一个键对应着一个值。
  • 键+值这个整体称为“键值对”或“Entry对象”。
3.1.1 Map的常见API

Map是双列集合的顶层接口,它的功能全部双列集合都可以继承使用。

方法名 说明
V put(K key,V value) 添加元素
V remove(Object key) 根据键删除键值对元素
void clear() 移除所有的键值对元素
boolean containsKey(Object key) 判断集合是否包含指定的键
boolean containsValue(Object value) 判断集合是否包含指定的值
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中键值对的个数

put方法的细节

  1. 在添加元素时,如果键不存在,方法直接把键值对对象添加到map集合中,方法返回null。
  2. 如果键存在,那么会把原有的键值对对象覆盖,并把被覆盖的值返回。
3.1.2 Map集合的获取功能
方法名 说明
V get(Object key) 根据键获取值
Set<K> keySet() 获取所有键的集合
Collection<V> values() 获取所有值的集合
Set<Map.Entry<K,V>> entrySet() 获取所有键值对对象的集合
3.1.3 Map集合的遍历

通过键找值

  • 获取所有键的集合。用keySet()方法实现。
  • 遍历键的集合,获取到每一个键。
  • 根据键去找值。用get(Object key)方法实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public static void main(String[] args) {
//创建集合对象
Map<String, String> map = new HashMap<String, String>();

//添加元素
map.put("张无忌", "赵敏");
map.put("郭靖", "黄蓉");
map.put("杨过", "小龙女");

//获取所有键的集合。用keySet()方法实现
Set<String> keySet = map.keySet();
//遍历键的集合,获取到每一个键。用增强for实现
for (String key : keySet) {
//根据键去找值。用get(Object key)方法实现
String value = map.get(key);
System.out.println(key + "," + value);
}
}
Lobster AIjava运行123456789101112131415161718

通过键值对对象进行遍历

  • 获取所有键值对对象的集合
    • Set<Map.Entry<K,V>> entrySet():获取所有键值对对象的集合。
  • 遍历键值对对象的集合,得到每一个键值对对象Map.Entry
  • 根据键值对对象获取键和值
    • 用getKey()得到键。
    • 用getValue()得到值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public static void main(String[] args) {
//创建集合对象
Map<String, String> map = new HashMap<String, String>();

//添加元素
map.put("张无忌", "赵敏");
map.put("郭靖", "黄蓉");
map.put("杨过", "小龙女");

//获取所有键值对对象的集合
Set<Map.Entry<String, String>> entrySet = map.entrySet();
//遍历键值对对象的集合,得到每一个键值对对象
for (Map.Entry<String, String> me : entrySet) {
//根据键值对对象获取键和值
String key = me.getKey();
String value = me.getValue();
System.out.println(key + "," + value);
}
}
Lobster AIjava运行12345678910111213141516171819

利用Lambda表达式遍历

方法名称 说明
default void forEach(BiConsumer<? super K, ? super V> action) 结合lambda遍历Map集合
  • 底层就是利用第二种方式进行遍历,依次得到每一个键的值。
  • 再调用accept方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public static void main(String[] args) {
//创建集合对象
Map<String, String> map = new HashMap<String, String>();

//添加元素
map.put("张无忌", "赵敏");
map.put("郭靖", "黄蓉");
map.put("杨过", "小龙女");

map.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value) {
System.out.println(key + "=" + value);
}
})
//简化后
map.forEach((key, value) -> System.out.println(key + "=" + value));
}
Lobster AIjava运行123456789101112131415161718

3.2 HashMap集合

3.2.1 HashMap的特点和细节
  • HashMap是Map接口的一个实现类。
  • 特点都是由键决定的:无序、不重复、无索引。
  • HashMap跟HashSet底层原理是一样的,都是哈希表结构。
  • 依赖hashCode方法和equals方法保证键的唯一
    • 如果键要存储的是自定义对象,需要重写hashCode和equals方法。
3.2.2 HashMap源码分析

image-20250531220342399

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
1.看源码之前需要了解的一些内容

Node<K,V>[] table 哈希表结构中数组的名字

DEFAULT_INITIAL_CAPACITY: 数组默认长度16

DEFAULT_LOAD_FACTOR: 默认加载因子0.75



HashMap里面每一个对象包含以下内容:
1.1 链表中的键值对对象
包含:
int hash; //键的哈希值
final K key; //键
V value; //值
Node<K,V> next; //下一个节点的地址值


1.2 红黑树中的键值对对象
包含:
int hash; //键的哈希值
final K key; //键
V value; //值
TreeNode<K,V> parent; //父节点的地址值
TreeNode<K,V> left; //左子节点的地址值
TreeNode<K,V> right; //右子节点的地址值
boolean red; //节点的颜色



2.添加元素
HashMap<String,Integer> hm = new HashMap<>();
hm.put("aaa" , 111);
hm.put("bbb" , 222);
hm.put("ccc" , 333);
hm.put("ddd" , 444);
hm.put("eee" , 555);

添加元素的时候至少考虑三种情况:
2.1数组位置为null
2.2数组位置不为null,键不重复,挂在下面形成链表或者红黑树
2.3数组位置不为null,键重复,元素覆盖



//参数一:键
//参数二:值

//返回值:被覆盖元素的值,如果没有覆盖,返回null
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}


//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留
// true,表示老元素的值保留,不会覆盖
// false,表示老元素的值不保留,会进行覆盖
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//定义一个局部变量,用来记录哈希表中数组的地址值。
Node<K,V>[] tab;

//临时的第三方变量,用来记录键值对对象的地址值
Node<K,V> p;

//表示当前数组的长度
int n;

//表示索引
int i;

//把哈希表中数组的地址值,赋值给局部变量tab
tab = table;

if (tab == null || (n = tab.length) == 0){
//1.如果当前是第一次添加数据,底层会创建一个默认长度为16,加载因子为0.75的数组
//2.如果不是第一次添加数据,会看数组中的元素是否达到了扩容的条件
//如果没有达到扩容条件,底层不会做任何操作
//如果达到了扩容条件,底层会把数组扩容为原先的两倍,并把数据全部转移到新的哈希表中
tab = resize();
//表示把当前数组的长度赋值给n
n = tab.length;
}

//拿着数组的长度跟键的哈希值进行计算,计算出当前键值对对象,在数组中应存入的位置
i = (n - 1) & hash;//index
//获取数组中对应元素的数据
p = tab[i];


if (p == null){
//底层会创建一个键值对对象,直接放到数组当中
tab[i] = newNode(hash, key, value, null);
}else {
Node<K,V> e;
K k;

//等号的左边:数组中键值对的哈希值
//等号的右边:当前要添加键值对的哈希值
//如果键不一样,此时返回false
//如果键一样,返回true
boolean b1 = p.hash == hash;

if (b1 && ((k = p.key) == key || (key != null && key.equals(k)))){
e = p;
} else if (p instanceof TreeNode){
//判断数组中获取出来的键值对是不是红黑树中的节点
//如果是,则调用方法putTreeVal,把当前的节点按照红黑树的规则添加到树当中。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
} else {
//如果从数组中获取出来的键值对不是红黑树中的节点
//表示此时下面挂的是链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//此时就会创建一个新的节点,挂在下面形成链表
p.next = newNode(hash, key, value, null);
//判断当前链表长度是否超过8,如果超过8,就会调用方法treeifyBin
//treeifyBin方法的底层还会继续判断
//判断数组的长度是否大于等于64
//如果同时满足这两个条件,就会把这个链表转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//e: 0x0044 ddd 444
//要添加的元素: 0x0055 ddd 555
//如果哈希值一样,就会调用equals方法比较内部的属性值是否相同
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
break;
}

p = e;
}
}

//如果e为null,表示当前不需要覆盖任何元素
//如果e不为null,表示当前的键是一样的,值会被覆盖
//e:0x0044 ddd 555
//要添加的元素: 0x0055 ddd 555
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null){

//等号的右边:当前要添加的值
//等号的左边:0x0044的值
e.value = value;
}
afterNodeAccess(e);
return oldValue;
}
}

//threshold:记录的就是数组的长度 * 0.75,哈希表的扩容时机 16 * 0.75 = 12
if (++size > threshold){
resize();
}

//表示当前没有覆盖任何元素,返回null
return null;
}
Lobster AIjava运行123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170

3.3 LinkedHashMap集合

  • LinkedHashMap是Map接口的一个实现类。
  • 特点由键决定:有序(由双向链表保证)、不重复、无索引。
  • 原理:底层依然是哈希表,只是每个键值对元素又额外多了一个双向链表记录顺序。
  • 依赖hashCode方法和equals方法保证键的唯一
    • 如果键要存储的是自定义对象,需要重写hashCode和equals方法。

3.4 TreeMap集合

3.4.1 TreeMap集合概述和特点
  • TreeMap是Map接口的一个实现类。
  • TreeMap底层是红黑树结构。
  • 由键决定特性:不重复、无索引、可排序。
  • 依赖hashCode方法和equals方法保证键的唯一
    • 如果键要存储的是自定义对象,需要重写hashCode和equals方法。
  • 可排序:对键进行排序
    • 默认按键的大小升序排序,也可以自己定义键的排序规则。
3.4.2 TreeMap的两种排序规则

实现Comparable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Student implements Comparable<Student>{
private String name;
private int age;
...
@Override
public int compareTo(Student o) {
//按照年龄进行排序
int result = o.getAge() - this.getAge();
//次要条件,按照姓名排序。
result = result == 0 ? o.getName().compareTo(this.getName()) : result;
return result;
}
}
Lobster AIjava运行12345678910111213

创建TreeMap对象时给出排序规则

1
2
3
4
5
6
7
8
9
10
11
TreeMap<Integer,String> tm = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//o1:当前要添加的元素
//o2:表示已经在红黑树中存在的元素
return o2 - o1;
}
});
//简化后
TreeMap<Integer,String> tm = new TreeMap<>((o1, o2) -> o2 - o1);
Lobster AIjava运行12345678910
3.4.3 TreeMap源码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
1.TreeMap中每一个节点的内部属性
K key; //键
V value; //值
Entry<K,V> left; //左子节点
Entry<K,V> right; //右子节点
Entry<K,V> parent; //父节点
boolean color; //节点的颜色




2.TreeMap类中中要知道的一些成员变量
public class TreeMap<K,V>{

//比较器对象
private final Comparator<? super K> comparator;

//根节点
private transient Entry<K,V> root;

//集合的长度
private transient int size = 0;



3.空参构造
//空参构造就是没有传递比较器对象
public TreeMap() {
comparator = null;
}



4.带参构造
//带参构造就是传递了比较器对象。
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}


5.添加元素
public V put(K key, V value) {
return put(key, value, true);
}

参数一:键
参数二:值
参数三:当键重复的时候,是否需要覆盖值
true:覆盖
false:不覆盖

private V put(K key, V value, boolean replaceOld) {
//获取根节点的地址值,赋值给局部变量t
Entry<K,V> t = root;
//判断根节点是否为null
//如果为null,表示当前是第一次添加,会把当前要添加的元素,当做根节点
//如果不为null,表示当前不是第一次添加,跳过这个判断继续执行下面的代码
if (t == null) {
//方法的底层,会创建一个Entry对象,把他当做根节点
addEntryToEmptyMap(key, value);
//表示此时没有覆盖任何的元素
return null;
}
//表示两个元素的键比较之后的结果
int cmp;
//表示当前要添加节点的父节点
Entry<K,V> parent;

//表示当前的比较规则
//如果我们是采取默认的自然排序,那么此时comparator记录的是null,cpr记录的也是null
//如果我们是采取比较去排序方式,那么此时comparator记录的是就是比较器
Comparator<? super K> cpr = comparator;
//表示判断当前是否有比较器对象
//如果传递了比较器对象,就执行if里面的代码,此时以比较器的规则为准
//如果没有传递比较器对象,就执行else里面的代码,此时以自然排序的规则为准
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else {
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
} else {
//把键进行强转,强转成Comparable类型的
//要求:键必须要实现Comparable接口,如果没有实现这个接口
//此时在强转的时候,就会报错。
Comparable<? super K> k = (Comparable<? super K>) key;
do {
//把根节点当做当前节点的父节点
parent = t;
//调用compareTo方法,比较根节点和当前要添加节点的大小关系
cmp = k.compareTo(t.key);

if (cmp < 0)
//如果比较的结果为负数
//那么继续到根节点的左边去找
t = t.left;
else if (cmp > 0)
//如果比较的结果为正数
//那么继续到根节点的右边去找
t = t.right;
else {
//如果比较的结果为0,会覆盖
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
}
//就会把当前节点按照指定的规则进行添加
addEntry(key, value, parent, cmp < 0);
return null;
}



private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
Entry<K,V> e = new Entry<>(key, value, parent);
if (addToLeft)
parent.left = e;
else
parent.right = e;
//添加完毕之后,需要按照红黑树的规则进行调整
fixAfterInsertion(e);
size++;
modCount++;
}



private void fixAfterInsertion(Entry<K,V> x) {
//因为红黑树的节点默认就是红色的
x.color = RED;

//按照红黑规则进行调整

//parentOf:获取x的父节点
//parentOf(parentOf(x)):获取x的爷爷节点
//leftOf:获取左子节点
while (x != null && x != root && x.parent.color == RED) {


//判断当前节点的父节点是爷爷节点的左子节点还是右子节点
//目的:为了获取当前节点的叔叔节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//表示当前节点的父节点是爷爷节点的左子节点
//那么下面就可以用rightOf获取到当前节点的叔叔节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
//叔叔节点为红色的处理方案

//把父节点设置为黑色
setColor(parentOf(x), BLACK);
//把叔叔节点设置为黑色
setColor(y, BLACK);
//把爷爷节点设置为红色
setColor(parentOf(parentOf(x)), RED);

//把爷爷节点设置为当前节点
x = parentOf(parentOf(x));
} else {

//叔叔节点为黑色的处理方案


//表示判断当前节点是否为父节点的右子节点
if (x == rightOf(parentOf(x))) {

//表示当前节点是父节点的右子节点
x = parentOf(x);
//左旋
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
//表示当前节点的父节点是爷爷节点的右子节点
//那么下面就可以用leftOf获取到当前节点的叔叔节点
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}

//把根节点设置为黑色
root.color = BLACK;
}


1.TreeMap添加元素的时候,键是不需要重写hashCode和equals方法?

2.在HashMap的底层,默认是利用哈希值的大小关系来创建红黑树的,所以,HashMap的键不需要实现Compareable接口或者传递比较器对象。

3.TreeMap和HashMap谁的效率更高?
如果是最坏情况,添加了8个元素,这8个元素形成了链表,此时TreeMap的效率要更高,但是这种情况出现的几率非常的少。
一般而言,还是HashMap的效率要更高。

4.在Map集合中,如果键重复了,不会覆盖的put方法:
思想:代码中的逻辑都有两面性,如果我们只知道了其中的A面,而且代码中还发现了有变量可以控制两面性的发生,那么该逻辑一定会有B面。
习惯:
boolean类型的变量控制,一般只有AB两面,因为boolean只有两个值
int类型的变量控制,一般至少有三面,因为int可以取多个值。
第一种方法:V putIfAbsent(K key, V value),仅当键不存在时,才会将键值对插入Map,返回Map中该键原本关联的值(若原本不存在则返回null)。
第二种方法:自定义Map实现,继承现有Map类并重写put()方法,但要注意线程安全问题。

5.三种双列集合的使用建议:
默认:HashMap(效率最高)
如果要保证存取有序:LinkedHashMap
如果要进行排序:TreeMap
Lobster AIjava运行123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234

4.Collections类

4.1 可变参数

由于Collections类工具类需要使用到可变参数,所以我们先讲解可变参数。

JDK1.5之后,如果我们定义一个方法需要接受多个参数(不确定个数),并且多个参数类型一致,我们可以对其简化。

格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
修饰符 返回值类型 方法名(参数类型... 形参名){  }

例如:
public static void main(String[] args) {
int sum = getSum(6, 7, 2, 12, 2121);
System.out.println(sum);
}

public static int getSum(int... arr) {
int sum = 0;
for (int a : arr) {
sum += a;
}
return sum;
}
Lobster AIjava运行123456789101112131415
  • 方法的形参个数是可以变化的:0,1,2,…
  • 底层就是一个数组,只不过不需要自己创建而已。
  • 在方法的形参中最多只能写一个可变参数。
  • 在形参中,如果出现了可变参数以外的其他形参,可变参数一定要写在最后。

4.2 Collections工具类

java.utils.Collections是集合工具类,用来对单列集合进行操作。

方法名称 说明
public static <T> boolean addAll(Collection<T> c, T… elements) 批量添加元素到集合c中
public static void shuffle(List<?> list) 随机打乱List集合元素的顺序
public static <T> void sort(List<T> list) 排序,默认升序排列
public static <T> void sort(List<T> list, Comparator<T> c) 根据指定的规则进行排序
public static <T> int binarySearch (List<T> list, T key) 以二分查找法查找元素
public static <T> void copy(List<T> dest, List<T> src) 拷贝集合中的元素
public static <T> int fill (List<T> list, T obj) 使用指定的元素填充集合
public static <T> void max/min(Collection<T> coll) 根据默认的自然排序获取最大/小值
public static <T> void swap(List<?> list, int i, int j) 交换集合中指定位置的元素

sort(List<T> list, Comparator<T> c)

如果是自定义对象,需要重写Comparable接口compareTo方法指定规则。

binarySearch

返回要查找元素key在集合list中的索引,如果元素不存在,会返回-(应插入点索引+1),由返回值得到元素应插入点索引的公式-(返回值+1)

copy

把src中的元素拷贝到dest中,如果src的长度 > dest的长度,方法会报错。

max/min

求指定规则的最大值或者最小值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// String中默认是按照字母的abcdefg顺序进行排列的
// 现在我要求最长的字符串
// 默认的规则无法满足,可以自己指定规则
// 求指定规则的最大值或者最小值
ArrayList<String> list7 = new ArrayList<>();
Collections.addAll(list7, "a","aa","aaa","aaaa");
System.out.println(Collections.max(list7, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}));
//简化后
System.out.println(Collections.max(list7, (o1, o2) -> o1.length() - o2.length()));
Lobster AIjava运行1234567891011121314

5.不可变集合

5.1 不可变集合概述

特点

  • 长度不可变:不能增加和删除元素
  • 内容不可变:不能修改元素

使用场景

  • 某个数据不能被修改,把它防御性地拷贝到不可变集合中是个很好的实践。
  • 当集合对象被不可信的库调用时,不可变形式是安全的。

简单理解:不想让别人修改集合中的内容

不可变集合分类

  • 不可变的list集合
  • 不可变的set集合
  • 不可变的map集合

5.2 创建不可变集合的方式

方法名称 说明
static <E> List<E> of(E…elements) 创建一个具有指定元素的List集合对象
static <E> Set<E> of(E…elements) 创建一个具有指定元素的Set集合对象
static <K,V> Map<K,V> of(E…elements) 创建一个具有指定元素的Map集合对象

List和Set不可变集合

当获取一个不可变的Set集合时,里面的参数一定要保证唯一性,不然会报IllegalArgumentException异常。

1
2
3
4
5
6
//一旦创建完毕之后,是无法进行修改的,在下面的代码中,只能进行查询操作
List<String> list = List.of("张三", "李四", "王五", "赵六");

//一旦创建完毕之后,是无法进行修改的,在下面的代码中,只能进行查询操作
Set<String> set = Set.of("张三", "李四", "王五", "赵六");
Lobster AIjava运行12345

static <K,V> Map<K,V> of(E…elements)

  • 键是不能重复的。
  • Map里面的of方法,参数是有上限的,最多只能传递20个参数,10个键值对。
1
2
3
//每两个为一对,第一个为键,第二个为值,如 张三==南京、李四==北京、王五==上海 等等。
Map<String, String> map = Map.of("张三", "南京", "李四", "北京", "王五", "上海", "赵六", "广州", "孙七", "深圳", "周八", "杭州", "吴九", "宁波", "郑十", "苏州", "刘一", "无锡", "陈二", "嘉兴");
Lobster AIjava运行12
  • 如果我们要传递多个键值对对象,数量大于10个,要使用Map集合中的ofEntries或copyof方法,否则会报错:

static <K,V> Map<K,V> ofEntries(Entry<? extends K, ? extends V>…entries)

  • 根据传递的若干Map.Entry对象返回Map不可变集合。

static <K,V> Map<K,V> copyOf(Map<? extends K,? extends V> map)

  • JDK10以后出现,根据给定的Map对象返回Map不可变集合。
  • 只对 map 本身进行拷贝,不会对 map 中的键和值进行深拷贝,而且不允许map中有null键或值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//1.创建一个普通的Map集合
HashMap<String, String> hm = new HashMap<>();
hm.put("张三", "南京");
hm.put("李四", "北京");
hm.put("王五", "上海");
hm.put("赵六", "北京");
hm.put("孙七", "深圳");
hm.put("周八", "杭州");
hm.put("吴九", "宁波");
hm.put("郑十", "苏州");
hm.put("刘一", "无锡");
hm.put("陈二", "嘉兴");
hm.put("aaa", "111");

//获取一个不可变的集合
//获取到所有的键值对对象(Entry对象)
Set<Map.Entry<String, String>> entries = hm.entrySet();
//把entries变成一个数组
Map.Entry[] arr = entries.toArray(new Map.Entry[0]);
//不可变的map集合
Map map = Map.ofEntries(arr);

//以上过程可以简化,如下:
Map<Object, Object> map = Map.ofEntries(hm.entrySet().toArray(new Map.Entry[0])); //使用ofEntries方法

Map<String, String> map = Map.copyOf(hm); //使用copyOf方法
Lobster AIjava运行1234567891011121314151617181920212223242526

十四、GUI图形化界面

1.JFrame类

构造方法

1
2
JFrame() //构造一个初始时不可见的新窗体
Lobster AIjava运行1

成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setsize(int width,int height) //设置界面的宽和高
setTitle(String title) //设置界面标题
setAlwaysOnTop(boolean alwaysOnTop) //设置页面是否置顶
setLocationRelativeTo(null) //设置页面居中
setDefaultCloseOperation(int operation) //设置窗口关闭模式:
0:不执行任何操作
1:仅隐藏窗口,不释放资源,虚拟机仍然运行
2:直到关闭所有窗口才结束虚拟机释放资源
3:关闭任意一个窗口就会结束虚拟机
Container getContentPane() //获得窗口的隐藏容器,JFrame对象默认有一个居中的隐藏容器存放添加的对象
setLayout(null) //取消隐藏容器的默认居中布局
setJMenuBar(JMenuBar menubar) //为界面添加菜单
setVisible(boolean b) //设置页面是否显示出来
setIconImage(Image image) //设置任务栏图标,image可以通过Toolkit.getDefaultToolkit().getImage(String filename)指定,filename可以是相对路径或绝对路径
setResizable(boolean resizable) //设置此窗体是否可由用户调整大小
setBackground(Color c) //设置页面背景颜色,例如参数Color.red为红色
Lobster AIjava运行12345678910111213141516

getContentPane()隐藏容器的方法

1
2
3
4
5
removeAll() //清空隐藏容器的所有组件
repaint() //更新容器内的组件
add(Component comp) //为隐藏容器添加元素,可以是JButton、JLabel、JTextField、JPasswordField。先添加的元素显示在上方,后添加的元素显示在下方
setComponentZOrder(Component comp, int index) //将指定组件移动到容器中指定的 z 顺序索引,顺序索引从0开始,低的显示在顶层
Lobster AIjava运行1234

案例:随机显示若干窗口到屏幕上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//text:窗口中显示的内容
public static void showJFram(String text){
//获取电脑屏幕尺寸
Toolkit toolkit = Toolkit.getDefaultToolkit();
Dimension screenSize = toolkit.getScreenSize();
int screenWidth = screenSize.width;
int screenHeight = screenSize.height;
//设置页面宽度和高度
int width = 450, height = 400;
Random rand = new Random();
//循环显示若干窗口
for (int i = 0; i < 250; i++) {
//创建JFrame窗口
JFrame frame = new JFrame();
//设置窗口大小和位置
frame.setBounds(rand.nextInt(screenWidth - width + 1), rand.nextInt(screenHeight - height + 1), width, height);
//设置窗口关闭模式
frame.setDefaultCloseOperation(0);
//设置窗口置顶
frame.setAlwaysOnTop(true);
//创建JLabel对象并添加文本
JLabel label = new JLabel(text);
//设置label内文本居中
label.setHorizontalAlignment(SwingConstants.CENTER);
//设置字体样式
label.setFont(new Font("Serif", Font.BOLD, 24));
// 使用GridBagLayout将JLabel居中
frame.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.anchor = GridBagConstraints.CENTER;
//把label添加到界面中
frame.add(label, gbc);
//让界面显示出来
frame.setVisible(true);
}
}
Lobster AIjava运行1234567891011121314151617181920212223242526272829303132333435363738

2.菜单条目

2.1 JMenuItem条目

构造方法

1
2
JMenuItem(String text) //构造一个提示信息是text的条目对象
Lobster AIjava运行1

2.2 JMenu选项

构造方法

1
2
JMenu(String s) //构造一个提示信息是s的选项对象
Lobster AIjava运行1

成员方法

1
2
add(JMenuItem menuItem) //为选项对象添加条目,参数还可以传递JMenu对象为选项对象添加子选项
Lobster AIjava运行1

2.3 JMenuBar菜单类

构造方法

1
2
JMenuBar() //构造一个菜单对象
Lobster AIjava运行1

成员方法

1
2
add(JMenu c) //为菜单对象添加选项
Lobster AIjava运行1

3.ImageIcon图像类

构造方法

1
2
3
ImageIcon() //创建一个未初始化的图像类
ImageIcon(String filename) //根据filename路径构造一个图像类,filename可以是相对路径或绝对路径,idea默认相对路径的起点是项目文件夹,注意:文件名之间用'//'代表'/'。
Lobster AIjava运行12

ImageIcon不能添加事件监听,常常和Jbutton或JLabel结合用来添加事件监听。

4.JLabel类

JLabel 对象可以显示文本、图像或同时显示二者,可以把它理解为一个容器容纳对象。

构造方法

1
2
3
4
JLabel() //创建无图像并且其标题为空字符串的 JLabel
JLabel(String text) //创建具有指定文本的 JLabel 实例
JLabel(Icon image) //创建具有指定图像的 JLabel 实例
Lobster AIjava运行123

成员方法

1
2
3
4
5
6
7
8
9
10
11
12
13
setBounds(int x, int y, int width, int height) //移动组件并调整其大小。由 x 和 y 指定左上角的新位置,由 width 和 height 指定新的大小。
setBorder(Border border) //为JLabel容器添加边框:
BevelBorder:斜面边框
参数: 0:凸起;1:凹陷
setText(String text) //定义此组件将要显示的单行文本
getText() //返回该标签所显示的文本字符串
setForeground(Color fg) //指定字体颜色,如Color.white表示白色
setFont(Font font) //将字体对象应用到JLabel对象
Point getLocation() //获取组件的位置对象Point
Point对象名.x、Point对象名.y分别可以得到Point对象的横纵坐标
setLocation(int x, int y) //根据x,y设置组件的位置
setLocation(Point p) //根据位置对象设置组件的位置
Lobster AIjava运行123456789101112

5.JDialog弹窗对象

构造方法

1
2
JDialog() //创建一个没有内容的弹窗对象
Lobster AIjava运行1

成员方法

1
2
3
4
5
6
7
getContentPane() //获得默认的隐藏容器,JDialog也像JFrame一样具有居中的默认隐藏容器存放添加的对象
setsize(int width,int height) //设置弹窗的宽和高
setAlwaysOnTop(boolean alwaysOnTop) //设置弹窗是否置顶
setLocationRelativeTo(null) //设置弹窗居中
setModal(boolean modal) //设置弹窗是否不关闭无法进行其他操作
setVisible(boolean b) //设置弹窗是否显示出来,建议放在最后写
Lobster AIjava运行123456

6.JButton按钮

构造方法

1
2
JButton() //创建不带任何内容的按钮
Lobster AIjava运行1

成员方法

1
2
3
4
setIcon(Icon defaultIcon) //设置按钮的默认显示图标,可以传递ImageIcon对象
setBorderPainted(boolean b) //设置按钮边框是否显示,默认有边框并且显示(true)
setContentAreaFilled(boolean b) //设置按钮背景是否显示,默认有背景颜色并且显示(true)
Lobster AIjava运行123

7.JTextField文本输入框

构造方法

1
2
JTextField() //构造一个新的文本输入框
Lobster AIjava运行1

成员方法

1
2
3
4
5
6
7
8
9
setBounds(int x, int y, int width, int height) //移动输入框并调整其大小。由 x 和 y 指定左上角的新位置,由 width 和 height 指定新的大小。
String getText() //返回文本输入内输入的内容
setEditable(boolean b) //设置输入框用户是否可以编辑
setVisible(boolean aFlag) //设置输入框是否显示
setHorizontalAlignment(int alignment) //设置文本框水平居中方式
JTextField.LEFT //左对齐
JTextField.CENTER //居中对齐
JTextField.RIGHT //右对齐
Lobster AIjava运行12345678

8.JPasswordField密码输入框

构造方法

1
2
JPasswordField() //创建一个新的密码输入框
Lobster AIjava运行1

成员方法

1
2
3
4
5
6
7
8
9
10
setBounds(int x, int y, int width, int height) //移动输入框并调整其大小。由 x 和 y 指定左上角的新位置,由 width 和 height 指定新的大小。
String getText() //返回密码输入框输入的内容
setEchoChar(char c) //设置此 JPasswordField 的回显字符,传递(char) 0表示显示明文,传递'*'表示显示'*'样式的密文
setEditable(boolean b) //设置输入框用户是否可以编辑
setVisible(boolean aFlag) //设置输入框是否显示
setHorizontalAlignment(int alignment) //设置文本框水平居中方式
JTextField.LEFT //左对齐
JTextField.CENTER //居中对齐
JTextField.RIGHT //右对齐
Lobster AIjava运行123456789

9.Font字体类

构造方法

1
2
Font(String name, int style, int size) //创建指定样式的字体对象:name指定字体风格(宋体等等),null表示使用默认字体;style指定字体样式,0-普通样式 1-加粗 2-斜体;size指定字体大小
Lobster AIjava运行1

10.监听事件

10.1 ActionListener行为监听

鼠标单击或键入空格时,触发行为监听。

可以用对象名.addActionListener为对象添加行为监听,参数常常传递this表示给当前调用者添加行为监听。

ActionEvent相关成员方法

1
2
Object getSource() //获取操作源对象
Lobster AIjava运行1

抽象方法

1
2
public void actionPerformed(ActionEvent e) //需要重写指定行为监听触发事件
Lobster AIjava运行1

10.2 KeyListener键盘监听

可以用对象名.KeyListener为对象添加键盘监听,参数常常传递this表示给当前调用者添加键盘监听。

KeyEvent相关成员方法

1
2
int getKeyCode() //返回操作的键盘上按键的整数代码
Lobster AIjava运行1

抽象方法

1
2
3
4
public void keyTyped(KeyEvent e) //键入某个键时调用此方法
public void keyPressed(KeyEvent e) //按下某个键时调用此方法
public void keyReleased(KeyEvent e) //松开某个键时调用此方法
Lobster AIjava运行123

10.3 MouseListener鼠标监听

可以用对象名.MouseListener为对象添加鼠标监听,参数常常传递this表示给当前调用者添加鼠标监听。

MouseEvent相关成员方法

1
2
Object getSource() //获取操作源对象
Lobster AIjava运行1

抽象方法

1
2
3
4
5
6
public void mouseClicked(MouseEvent e) //鼠标按键在组件上单击(按下并释放)时调用
public void mousePressed(MouseEvent e) //鼠标按键在组件上按下时调用
public void mouseReleased(MouseEvent e) //鼠标按钮在组件上释放时调用
public void mouseEntered(MouseEvent e) //鼠标进入到组件上时调用
public void mouseExited(MouseEvent e) //鼠标离开组件时调用
Lobster AIjava运行12345

11.综合案例-拼图游戏

参考资料中的拼图游戏模块。只需要把puzzlegame文件夹导入成模块就可以了。

十五、Stream流

Stream流可以理解成是一个流水线,可以把原来的一系列数据经过流水线的处理变成我们想要的数据,比如过滤掉奇数、删选字符串长度等。

  • 中间方法:操作完成后还能进行其他操作。
  • 终结方法:操作完成后不能进行其他操作,是Stream上的最后一个方法,一个Stream流只能有一个终结方法。

1.Stream流的获取

获取方式 方法名 说明
单列集合 default Stream<E> stream() Collections中的默认非静态方法
双列集合 把Map转成Set集合,间接的生成流
数组 static <T> Stream<T> stream(T[] array) Arrays工具类中的静态方法
一对零散数据 static <T> Stream<T> of(T…values) Stream接口中的静态方法

单列集合、数组、零散数据获取stream流

如果数组是基本数据类型,使用Arrays.stream(…)获取的stream流是特化流,这里不介绍。

1
2
3
4
5
6
7
8
9
10
11
12
//Collection体系的集合可以使用默认方法stream()生成流
List<String> list = new ArrayList<String>();
Stream<String> listStream = list.stream();

//数组可以通过Arrays中的静态方法stream生成流
String[] strArray = {"hello","world","java"};
Stream<String> strArrayStream = Arrays.stream(strArray);

//同种数据类型的多个数据可以通过Stream接口的静态方法of(T... values)生成流
Stream<String> strArrayStream2 = Stream.of("hello", "world", "java");
Stream<Integer> intStream = Stream.of(10, 20, 30);
Lobster AIjava运行1234567891011
  • Stream接口中的of方法的细节
    • 方法的形参是可变参数,可以传递一些零散的同类型的数据,也可以传递数组。
    • 传递的数组必须是引用数据类型,如果传递基本数据类型,会把整个数组当做一个元素(数组地址),放到Stream中。

双列集合获取Stream的方式

  • 通过keySet获取所有键的集合调用Collections中的stream方法
  • 通过values获取所有值的集合调用Collections中的stream方法
  • 通过entrySet获取所有键值对对象的集合调用Collections中的stream方法
1
2
3
4
5
Map<String,Integer> map = new HashMap<String, Integer>();
Stream<String> keyStream = map.keySet().stream();
Stream<Integer> valueStream = map.values().stream();
Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();
Lobster AIjava运行1234

2.Stream流中间操作方法

方法名 说明
Stream<T> filter(Predicate predicate) 用于对流中的数据进行过滤
Stream<T> limit(long maxSize) 获取前几个元素
Stream<T> skip(long n) 跳过前几个元素
Stream<T> distinct() 元素去重,依赖HashCode和equals方法
static <T> Stream<T> concat(Stream a, Stream b) 合并a和b两个流为一个流
Stream<R> map(Function<T, R> mapper) 转换流中的数据类型
  • 用中间Stream类型变量接受Stream流,这个流只能使用一次,否则系统报错,建议使用链式编程
  • 修改Stream流中的数据,不会影响原来集合或数组中的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//1. Stream<T> filter(Predicate predicate):过滤
//Predivate<T>泛型接口是函数式接口,方法 public boolean test(T t):对给定的参数进行判断,返回一个布尔值
ArrayList<String> list = new ArrayList<>();
list.add("张三丰");
list.add("张无忌");
list.add("张翠山");
list.add("王二麻子");
list.add("张良");
list.add("谢广坤");
list.stream().filter(
new Predicate<String>() {
@Override
//返回值为true保留数据,返回值为false舍弃数据
public boolean test(String s) {
boolean result = s.startsWith("张");
return result;
}
}).forEach(s-> System.out.println(s));
//Lambda简化
list.stream().filter(s ->s.startsWith("张")).forEach(s-> System.out.println(s));


//2. limit:截取;skip:跳过
//需求1:取前3个数据在控制台输出
list.stream().limit(3).forEach(s-> System.out.println(s));
//需求2:跳过3个元素,把剩下的元素在控制台输出
list.stream().skip(3).forEach(s-> System.out.println(s));


//3. distinct:去重;Stream:合并流
distinct依赖HashCode和equals方法,如果去重的元素是自定义类型,要重写HashCode和equals方法
//合并两个流并且去重
Stream.concat(list.stream().limit(4),list.stream().skip(2)).distinct().forEach(s-> System.out.println(s));


//4. Stream`<R>` map(Function<T, R> mapper):转换流中数据的数据类型
Function<T,R>泛型接口是函数式接口,T表示流中原本的数据类型,R表示要转成之后的类型
方法 public R apply(String T) 的形参T依次表示流里面的每一个数据,返回值R表示转换之后的数据
//需求:只获取里面的年龄并进行打印
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-15", "周芷若-14", "赵敏-13", "张强-20", "张三丰-100", "张翠山-40");
//当map方法执行完毕之后,流上的数据就变成了整数
list.stream().map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
String[] arr = s.split("-");
String ageString = arr[1];
int age = Integer.parseInt(ageString);
return age;
}
}).forEach(s-> System.out.println(s));
//Lambda简化
list.stream().map(s-> Integer.parseInt(s.split("-")[1])).forEach(s-> System.out.println(s));
Lobster AIjava运行1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253

3.Stream流终结操作方法

方法名 说明
void forEach(Consumer action) 遍历
long count() 统计流中的元素数
Object[] toArray() 收集流中的数据,放到数组中
A[] toArray(IntFunction<A[]> generator) 收集流中的数据,放到数组中
R collect(Collector collector) 收集流中的数据,放到集合中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//1. forEach:遍历
//Consumer<T>泛型接口是函数式接口,T表示流中的数据类型
//方法 public void accept(T t)对每一个流中的数据t进行操作(如打印)
ArrayList<String> list = new ArrayList<>();
list.add("张三丰");
list.add("张无忌");
list.add("张翠山");
list.add("王二麻子");
list.add("张良");
list.add("谢广坤");
list.stream().forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
//Lambda简化
list.stream().forEach(s->System.out.println(s));

//2.count:统计
long count = list.stream().count();
System.out.println(count);

//3.toArray():收集流中数据放到Object数组中
Object[] arr1 = list.stream().toArray();
System.out.println(Arrays.toString(arr1));
Lobster AIjava运行1234567891011121314151617181920212223242526

toArray(IntFunction<A[]> generator)收集流中数据放到数组中

IntFunction<R>泛型接口是函数式接口,R表示具体类型的数组。

方法:R apply(int value)中的value表示流中数据的个数,要跟数组长度一致,返回值为具体类型的数组,方法体创建数组

  • toArray形参负责创建一个指定类型的数组。
  • 然后toArray底层会依次获得流里的每个元素放到数组中。
1
2
3
4
5
6
7
8
9
String[] arr = list.stream().toArray(new IntFunction<String[]>() {
@Override
public String[] apply(int value) {
return new String[value];
}
});
//Lambda简化
String[] arr2 = list.stream().toArray(value -> new String[value]);
Lobster AIjava运行12345678

R collect(Collector collector)收集流中的数据到集合中

工具类Collectors提供了具体的收集方式:

方法名 说明
public static <T> Collector toList() 把元素收集到List集合中
public static <T> Collector toSet() 把元素收集到Set集合中
public static Collector toMap(Function keyMapper,Function valueMapper) 把元素收集到Map集合中

1.收集到List和Set集合中

1
2
3
4
5
6
7
8
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-男-15", "周芷若-女-14", "赵敏-女-13", "张强-男-20", "张三丰-男-100", "张翠山-男-40", "张良-男-35", "王二麻子-男-37", "谢广坤-男-41");
//收集所有男性
List<String> newList1 = list.stream().filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toList());
Set<String> newList2 = list.stream().filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toSet());
Lobster AIjava运行1234567

2.收集到Map集合中

  • 参数1表示键的生成规则,参数2表示值的生成规则。
  • Function<T,R>接口中T表示流中每一个数据的类型,R表示Map集合中键或值的数据类型,方法public R applay(T t)的形参t表示流中的每个数据,R表示已经生成的键或值的类型。

注意:如果我们要收集到Map集合当中,键不能重复,否则会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//键:姓名。 值:年龄
Map<String, Integer> map = list.stream().filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(new Function<String, String>() {
@Override
public String apply(String s) {
return s.split("-")[0];
}
},new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.parseInt(s.split("-")[2]);
}
}));
//Lambda简化
Map<String, Integer> map2 = list.stream().filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(s -> s.split("-")[0], s -> Integer.parseInt(s.split("-")[2])));
Lobster AIjava运行12345678910111213141516

十六、方法引用

方法引用就是把已有的方法拿过来用,当做函数式接口中抽象方法的方法体。

1.条件和引用方式

1.1 条件

  1. 引用处需要是函数式接口。
  2. 被引用的方法需要已经存在(不存在怎么引用)。
  3. 被引用方法的形参和返回值需要跟抽象方法的形参和返回值类型保持一致。
  4. 被引用方法的功能需要满足当前的要求。

1.2 引用方式

方法引用符::: 该符号为引用运算符,而它所在的表达式被称为方法引用。

引用方式

1
2
类名::方法名(形参列表)
Lobster AIjava运行1
1.2.1 引用类的静态方法
1
2
3
4
5
6
7
8
类名::静态方法

例如:
//Lambda写法,形参:String类型;返回值:int类型;功能:解析字符串为int整数
useConverter(s -> Integer.parseInt(s));
//引用类方法,Integer中的静态方法parseInt和上述三个要求一致
useConverter(Integer::parseInt);
Lobster AIjava运行1234567
1.2.2 引用类中的成员方法

引用本类或父类中的成员方法时,不能再静态方法中使用,因为静态方法中没有this和super关键字。

引用本类中的成员方法

1
2
this::成员方法
Lobster AIjava运行1

引用父类中的成员方法

1
2
super::成员方法
Lobster AIjava运行1

引用其他类中的成员方法

1
2
3
4
5
6
7
8
9
对象名::成员方法

例如:
//Lambda简化写法,形参:Printer类型;返回值:void;功能:打印s.toUpperCase()
usePrinter(s -> System.out.println(s.toUpperCase()));
//引用对象的实例方法,PrintString类中也有一个成员方法printUpper和上述三个要求一致
PrintString ps = new PrintString();
usePrinter(ps::printUpper);
Lobster AIjava运行12345678
  • 技巧:forEach遍历中的方法引用
1
2
3
4
5
List<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
//System.out底层是PrintStream的对象,通过对象引用结合forEach方法可以方便的打印集合里的每一个数据
list.forEach(System.out::println);
Lobster AIjava运行1234
1.2.3 类名引用成员方法
1
2
类名::成员方法
Lobster AIjava运行1

要求

  1. 需要有函数式接口
  2. 被引用的方法必须已经存在
  3. 被引用方法的形参,需要跟抽象方法的第二个形参到最后一个形参保持一致,返回值需要保持一致。
  4. 被引用方法的功能需要满足当前的需求

抽象方法的形参

  • 第一个参数:第一个参数一般都表示流里面的每一个数据;同时还表示被引用方法的调用者,即决定了可以引用哪些类中的方法在Stream流当中。
  • 第二个参数到最后一个参数:跟被引用方法的形参保持一致,如果没有第二个参数,说明被引用的方法需要是无参的成员方法。

局限性

不能引用所有类中的成员方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@FunctionalInterface
public interface MyString {
String mySubString(String s,int x,int y);
}

public class MyStringDemo {
public static void main(String[] args) {
//Lambda简化写法,形参:第一个是String类型,意味着方法引用只能引用String类里的方法
//第二个形参和第三个形参是int类型
//返回值:String类型
//功能:截取字符串s的x索引到y - 1索引
useMyString((s,x,y) -> s.substring(x,y));

//String类中的substring只有两个int参数,对应到x、y,返回值和功能也一致
useMyString(String::substring);

}

private static void useMyString(MyString my) {//MyString是函数式接口
String s = my.mySubString("HelloWorld", 2, 5);
System.out.println(s);
}
}
Lobster AIjava运行1234567891011121314151617181920212223
1.2.4 引用构造方法
1
2
3
4
5
6
7
8
类名::new

例如:
//Lambda简化写法,形参:String类型和int类型;返回值:void;功能:创建Student对象
useStudentBuilder((name,age) -> new Student(name,age));
//引用构造器,Student类中有一个构造方法和上述三个要求一致
useStudentBuilder(Student::new);
Lobster AIjava运行1234567

引用数组的构造方法

1
2
数组类型[]::new
Lobster AIjava运行1
  • 创建一个指定类型的数组
  • 数组的类型,需要跟流中数据的类型保持一致。
1
2
3
ArrayList<Integer> list = new ArrayList<>();
Integer[] arr2 = list.stream().toArray(Integer[]::new);
Lobster AIjava运行12

十七、异常

异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行。

异常的作用

  1. 用来查询bug的关键参考信息。
  2. 作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况。

1.异常体系

Java中,万物皆对象,所以某个异常本质上也是一个对象。

异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Errorjava.lang.Exception,平常所说的异常指java.lang.Exception

image-20250601144324717

Throwable体系

  • Error:系统级别的错误(属于严重错误),系统出错会把这些错误封装成Error对象,是给Sun公司自己用。
  • Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。

Exception体系

  • RuntimeException:运行时异常,RuntimeException和它的子类。编译阶段不会出现异常提醒,运行时出现的异常(如索引越界)
  • 其他异类:编译时异常,编译阶段就会出现的异常提醒(如日期解析异常)

Exception是异常体系的最上层父类

异常的分类

  1. 编译时异常:除了RuntimeException和它的子类,其他都是编译时异常,编译时就需要处理,否则无法运行。
  2. 运行时异常:除了RuntimeException和它的所有子类,都是运行时异常,编译阶段不会报错,一般是由于参数传递错误带来的问题。

2.异常的处理方式

2.1 JVM默认的处理方式

打印异常信息,结束JVM的执行。

image-20250601145935410

2.2 捕获异常try…catch

特点:用在方法调用处,可以让程序继续往下执行,不会停止。

1
2
3
4
5
6
7
try{
编写可能会出现异常的代码
}catch(异常类型 e){
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
Lobster AIjava运行123456

try:该代码块中编写可能产生异常的代码。

catch:用来进行某种异常的捕获:判断异常类型是否是否匹配,匹配就把异常对象赋值给e,然后执行处理异常的代码,否则跳过catch继续执行代码(try里的剩下代码不会执行了)。

注意:try和catch都不能单独使用,必须连用。

一次捕获多次处理方式

1
2
3
4
5
6
7
8
9
10
try{
编写可能会出现异常的代码
}catch(异常类型A e){ 当try中出现A类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}catch(异常类型B e){ 当try中出现B类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}...
Lobster AIjava运行123456789

这种方式的要求:

  1. 多个catch中的异常不能相同。
  2. 若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。

try…catch的细节

  1. 如果try中的代码没有出现异常,会跳过catch继续执行。
  2. 如果try中遇到多个异常,,我们一般会写多个catch与之对应,父类需要写在子类下方。
  3. 如果try中的异常没有被捕获,最终还会交给虚拟机进行处理,此时相当于try…catch白写了。
  4. 如果在try块中遇到第一个异常,程序会立即跳转到对应的catch块进行匹配,而不会继续执行try块中剩余的代码。

2.3 finally代码块

在try…catch语句中,如果try中的语句遇到了某个异常,会直接跳转到对应的catch,剩下的代码并不会执行,这可能会导致某些资源被使用了但是没有被释放,例如以后学到的IO流,这时可以使用try...catch...finally语句,把释放资源代码写到finally代码块中,这时不管try内是否有异常,最终都会执行finally中的代码释放资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
try{
编写可能会出现异常的代码
}catch(异常类型A e){ 当try中出现A类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}catch(异常类型B e){ 当try中出现B类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}...{
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}finally{
释放资源代码或其他一定要被执行的代码
}

例如:
public static int division(int x, int y) {
int result;
try {
result = x / y;
} catch (ArithmeticException e) {
throw new ArithmeticException();
} finally {
System.out.println("释放资源代码或其他一定要被执行的代码");
}
return result;
}
Lobster AIjava运行123456789101112131415161718192021222324252627

finally方法会在try…catch语句执行完毕后立即执行,之后才返回result。

当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。

2.4 抛出异常throw

throw

throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

1
2
3
4
5
throw new 异常类名(参数);

例如:
throw new NullPointerException("要访问的arr数组不存在");
Lobster AIjava运行1234
  • 写在方法内,结束方法。
  • 手动抛出异常对象,交给调用者。
  • 方法下面的代码不会再执行。

throws

关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)。

1
2
3
4
5
6
7
8
9
10
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }

例如:
public static double division(double x, double y) {
if (y == 0) {
throw new ArithmeticException("除数不能为0");
}
return x / y;
}
Lobster AIjava运行123456789
  • 写在方法定义处,表示声明一个异常。
  • 告诉调用者,使用本方法可能会出现哪些异常。
  • 编译时异常必须要写,而运行时异常可以不写。

一般throw和throws会结合使用,throws用来声明方法可能出现的异常给调用者,throw用来抛出这个异常给调用者。

3.Throwable中的常用方法

  • public void printStackTrace():打印异常的详细信息,在底层是利用System.err.println进行输出,仅仅打印信息,不会停止程序的执行。

    包含了异常的类型、异常的原因 和 异常出现的位置,在开发和调试阶段,都得使用printStackTrace。

  • public String getMessage():获取发生异常的原因。

    示给用户的时候,就提示错误原因。

  • public String toString():获取异常的类型和异常描述信息(不用)。

System.err.println表示把信息以红色字体输出到控制台,基本不用。

4.自定义异常

虽然Java提供了各种异常供我们使用,但是可能我们遇到的异常系统并没有提供,如账号不存在、密码错误等,这是就需要自定义异常。

自定义异常的步骤:

  1. 定义异常类:类名建议使用异常功能Exception格式,例如LoginException登录时异常。
  2. 写继承关系:
    • 编译时异常:继承java.lang.Exception
    • 运行时异常:继承java.lang.RuntimeException
  3. 书写空参构造和带参构造。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LoginException extends Exception {
/**
* 空参构造
*/
public LoginException() {
}

/**
* @param message 表示异常提示
*/
public LoginException(String message) {
super(message);
}
}
Lobster AIjava运行1234567891011121314

5.异常的注意事项

  • 运行时异常被抛出可以不处理。即不捕获也不声明抛出。
  • 如果父类抛出了多个异常,子类覆盖父类方法时,只能抛出相同的异常或者是他的子集。
  • 父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出。
  • 当多异常处理时,捕获处理,前边的类不能是后边类的父类。
  • 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收。

十八、IO流

1.File类

java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

下面讲解时的目录指文件夹,文件指具有后缀名的具体文件(如test.docx)。

1.1 构造方法

构造方法 说明
public File(String pathname) 通过绝对或相对路径名字符串创建 File对象
public File(String parent, String child) 从父路径名字符串和子路径名字符串创建新的 File实例
public File(File parent, String child) 从父抽象路径名和子路径名字符串创建新的 File实例
  1. 一个File对象代表硬盘中实际存在的一个文件或者目录。
  2. 无论该路径下是否存在文件或者目录,都不影响File对象的创建。
  3. 父路径名:路径名去掉最后的一层路径就是这个路径的父路径名,如C:\Program Files\MySQL的父路径名是C:\Program Files

补充(绝对路径和相对路径)

绝对路径:从根目录开始的路径,Windows中的根目录就是盘符,Linux中根目录是/。例如C:\Program Files\MySQL\MySQL Server 8.0就是Windows中的绝对路径。

相对路径:在idea中,相对路径是从项目所在目录开始的,比如新建一个project-test项目到D:\project中,此时相对路径test-mutil\src\HelloWorld.java就可以定位到文件D:\project\project-test\test-mutil\src\HelloWorld.java

注意:在Java的字符串中\表示转义字符,如果想要表示路径,需要用\\表示\,例如C:\\Program Files\\MySQL\\MySQL Server 8.0

1.2 判断功能的成员方法

方法名称 说明
public boolean exists() 此File表示的文件或目录是否实际存在
public boolean isDirectory() 此File表示的是否为文件夹(目录)
public boolean isFile() 此File表示的是否为文件

如果此File表示的文件或目录不存在,那么isDirectory和isFile都会返回false,不会报异常。

1.3 获取功能的成员方法

方法名称 说明
public long length() 返回由此File表示的文件的大小(字节数量)
public String getAbsolutePath() 返回此File的绝对路径名字符串
public String getPath() 返回定义此File对象时使用的路径
public String getName() 返回由此File表示的文件或目录的名称,带后缀
public long lastModified() 返回文件的最后修改时间毫秒值

public long length()

这个方法无法获取文件夹的大小,如果要获取一个文件夹的大小,需要把这个文件夹里的所有文件大小加在一起。

public String getName()

对于文件,返回文件名+后缀名,对于文件夹,返回文件夹的名字。

1.4 创建删除功能的成员方法

方法名称 说明
public boolean createNewFile() 创建一个新的空文件
public boolean mkdir() 创建单级文件夹
public boolean mkdirs() 创建单级或多级文件夹
public boolean delete() 删除文件、空文件夹

public boolean createNewFile()

  • 如果当前路径表示的文件是不存在的,则创建成功,返回true。
  • 如果当前路径表示的文件是存在的,则创建失败,返回false。
  • 如果父级路径是不存在的,那么会出现异常IOException。
  • createNewFile只能创建文件,不能创建文件夹,如果路径中不包含后缀名,就创建一个没有后缀名的文件。

public boolean mkdir()

  • 如果当前路径已经存在,就创建失败,返回false。
  • mkdir只能创建单级文件夹,无法创建多级文件夹。

public boolean delete()

  • 如果删除的是文件或空文件夹,则直接删除文件,不经过回收站。
  • 如果删除的是有内容的文件夹,会删除失败。

1.5 获取和遍历的成员方法

方法名称 说明
public File[] listFiles() 获取当前路径下所有内容放到数组中
public static File[] listRoots() 获取所有可用的文件系统根(盘符)
public String[] list() 获取当前路径下所有内容(仅获取名字)
public String[] list(FilenameFilter filter) 利用文件名过滤器获取当前路径下所有内容
public File[] listFiles(FileFilter filter) 利用文件名过滤器获取当前路径下所有内容
public File[] listFiles(FilenameFilter filter) 利用文件名过滤器获取当前路径下所有内容

public File[] listFiles()

  • 当调用者File表示的路径是文件或不存在时,返回null。
  • 当调用者File表示的路径是一个空文件夹时,返回长度为0的数组。
  • 当调用者File表示的路径是一个有内容的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏文件。
  • 当调用者File表示的路径是一个需要权限的文件夹时,返回null。

public String[] list(FilenameFilter filter)

FilenameFilter是一个函数式接口,方法public boolean accept(File dir, String name)的形参dir表示表示父级路径,name表示子级路径,返回值表示当前路径是否保留。

public File[] listFiles(FileFilter filter)

FileFilter 是一个函数接口,方法public boolean accept(File pathname)的形参pathname表示完整路径,返回值表示当前路径是否保留。

1.6 案例

扫描目录

递归扫描目录下的所有文件和文件夹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void scanDirectory(String src){
File srcFile = new File(src);
//得到目录下的所有内容
File[] files = srcFile.listFiles();
//如果没有内容,直接结束遍历
if(files == null){
return;
}
//遍历目录下的内容
for (File file : files) {
if(file.isFile()){
//如果是文件,直接打印绝对路径
System.out.println(file.getAbsoluteFile());
}else {
//如果是目录,递归遍历
System.out.println(file.getAbsoluteFile());
scanDirectory(file.getPath());
}
}
}
Lobster AIjava运行1234567891011121314151617181920

删除文件夹或文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void delete(String src){
File srcFile = new File(src);
//如果文件或文件夹不存在,打印提示信息
if(!srcFile.exists()){
System.err.println("文件或文件夹不存在");
return;
}
//如果是文件,就直接删除
if(srcFile.isFile()){
srcFile.delete();
return;
}
//遍历文件夹内所有内容
File[] files = srcFile.listFiles();
for (File file : files) {
if(file.isFile()){
//如果是文件,删除
file.delete();
}else {
//如果是文件夹,就递归
delete(file.getPath());
}
}
//2.再删除自己
srcFile.delete();
}
Lobster AIjava运行1234567891011121314151617181920212223242526

统计文件或文件夹大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static long getSize(String src) {
File srcFile = new File(src);
//如果文件或文件夹不存在,打印提示信息
if (!srcFile.exists()) {
System.err.println("文件或文件夹不存在");
System.exit(1);
}
//如果是文件,直接返回大小
if (srcFile.isFile()) {
return srcFile.length();
}
long size = 0; //大小统计
//遍历文件夹
File[] files = srcFile.listFiles();
if (files != null && files.length != 0) {
for (File file : files) {
if (file.isFile()) {
//如果是文件,累加大小
size += file.length();
} else {
//如果是文件夹,递归统计
size += getSize(file.getPath());
}
}
}
return size;
}
Lobster AIjava运行123456789101112131415161718192021222324252627

2.IO流

IO流是和内存打交道的,可以进行内存的读和写。

I/O操作主要是指使用java.io包下的内容,进行数据的输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。

2.1 分类和IO流体系

根据数据的流向分为:输入流输出流

  • 输入流 :把数据从其他设备上读取到内存中的流。
  • 输出流 :把数据从内存 中写出到其他设备上的流。

格局数据的类型分为:字节流字符流

  • 字节流 :以字节为单位,读写数据的流,可以操作所有类型的文件。
  • 字符流 :以字符为单位,读写数据的流,只能操作纯文本文件。

能用操作系统自带记事本打开并且能读懂的文件是纯文本文件,如txt、md、…ml、lrc文件等。

img

2.2 字节流

2.2.1 字节输出流OutputStream

java.io.OutputStream 抽象类是字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
  • public void write(byte[] b):将 b.length 字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len) :从指定字节数组 b 的索引 off 开始输出 len 个字节到此输出流。
  • public abstract void write(int b) :将指定的字节输出到此输出流。

close方法:当完成流的操作时,必须调用此方法,释放系统资源。

2.2.2 文件输出流FileOutputStream

java.io.FileOutputStream 类是字节输出流OutputStream的一个子类,用于将数据写出到指定文件。

构造方法

FileOutputStream(File file):通过File对象创建一个字节输出流的对象。

FileOutputStream(String name):通过String对象记录的路径创建一个字节输出流的对象。

  • 如果文件不存在会创建一个新的文件,但是要保证父类路径是存在的。
  • 如果文件已经存在,会清空文件。

写出字节数据到文件中

写出字节:write(int b) 方法,每次可以写出一个字节数据,写到本地文件上的数据是 b 所对应ASCII上的字符。

写出字节数组:write(byte[] b),每次可以写出数组中的全部数据。

写出指定长度字节数组:write(byte[] b, int off, int len) ,每次写出从off索引开始,len个字节。

  • 换行写

write默认写入是依次紧接往后书写,不会换行,如果要换行,需要手动添加

  1. Windows系统里,换行符是 回车+换行 ,即\r\n

  2. Linux系统里,换行符是 换行 ,即\n

  3. Mac系统里,换行符是 回车 ,即\r。从 Mac OS X开始与Linux统一。

  • 续写

每次程序运行,创建输出流对象,都会清空目标文件中的数据。

public FileOutputStream(File file, boolean append): 创建文件输出流以写入由指定的 File对象表示的文件。

public FileOutputStream(String name, boolean append): 创建文件输出流以指定的名称写入文件。

参数中都需要传入一个boolean类型的值,true 表示追加数据,false 表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了。

2.2.3 字节输入流InputStream

java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

  • public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
  • public abstract int read(): 从输入流读取数据的下一个字节。
  • public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b 中。

close方法:当完成流的操作时,必须调用此方法,释放系统资源。

2.2.4 文件输入流FileInputStream

java.io.FileInputStream 类是字节输入流InputStream的一个子类,用于写入字节到指定文件中。

构造方法

FileInputStream(File file): 通过打开与实际文件的连接来创建一个 FileInputStream 。

FileInputStream(String name): 通过打开与实际文件的连接来创建一个 FileInputStream 。

  • 如果文件不存在,则直接报错。

从文件中读取字节数据

public abstract int read(): 一次读取一个字节,读取出来的是ASCII上对应的数字,如果读到文档末尾,返回-1。

public int read(byte[] b): 一次读取一个字节数组,每次读取尽可能把数组填满,返回值表示本次读取到多少个字节数据。

  • read方法底层维护一个指针,初始指向文件第一个字节,没调用一次read方法,指针就向后移动一个字节,如果一次读取多个字节,读取几个就移动几次,这个指针始终指向下一次需要读取的字节。
2.2.5 案例:图片复制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Copy {
public static void main(String[] args) throws IOException {
// 1.创建流对象
// 1.1 指定数据源
FileInputStream fis = new FileInputStream("D:\\test.jpg");
// 1.2 指定目的地
FileOutputStream fos = new FileOutputStream("test_copy.jpg");

// 2.读写数据
// 2.1 定义数组
byte[] b = new byte[1024];
// 2.2 定义长度
int len;
// 2.3 循环读取
while ((len = fis.read(b))!=-1) {
// 2.4 写出数据
fos.write(b, 0 , len);
}

// 3.关闭资源
fos.close();
fis.close();
}
}

2.3 字符编码规则

  • 在计算机中,任意数据都是以二进制形式存储。
  • 计算机中最小的存储单元是字节。
  • ASCII字符集中,一个英文占一个字节。
2.3.1 常见编码集

GBK字符集

  • GBK完全兼容ASCII字符集
  • 一个英文占一个字节,二进制第一位一定是0,不足8位前面补0
  • 一个中文占两个字节,二进制高位字节的第一位一定是1,转成十进制后是一个负数

Unicode字符集

img

  • 最常用的编码规则是UTF-8编码规则:用1~4位字节保存。
  • 一个英文占一个字节,二进制第一位是0,转成十进制是正数
  • 一个中文占三个字节,二进制第一位是1,每一个字节转成十进制是负数
2.3.2 乱码问题

乱码的原因分析

  1. 原因1:读取数据时未读完整个汉字。
  2. 原因2:编码和解码时的方式不一样。

乱码避免

  1. 不要使用字节流读取文本文件。
  2. 编码解码时使用同一个码表,同一个编码方式。
2.3.3 Java中的编码和解码

编码方法(String类中的方法)

  • public byte[] getBytes():使用默认方法进行编码。
  • public byte[] getBytes(String charsetName):使用指定方式进行解码。

解码方法(String类中的方法)

  • String(byte[] bytes):使用默认方式进行解码。
  • String(byte[] bytes, String charsetName):使用指定方式进行解码。

2.4 字符流

特点

  1. 输入流:一次读一个字节,遇到中文时,一次读多个字节。
  2. 输出流:底层会把数据按照指定编码进行编码,变成字节再写到文件中。

使用场景:对于纯文本文件进行读写操作。

2.4.1 字符输入流Reader

java.io.Reader抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

  • public void close() :关闭此流并释放与此流相关联的任何系统资源。
  • public int read(): 从输入流读取一个字符。
  • public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中。
2.4.2 FileReader类

java.io.FileReader 类是字符输入流Reader的一个子类,用于从纯文本文件中读取字符。构造时使用系统默认的字符编码和默认字节缓冲区。

Windows系统的中文编码默认是GBK编码表;idea中是UTF-8编码表。

字节缓冲区:一个字节数组,用来临时存储字节数据。是FileReader自带的。

构造方法

FileReader(File file):创建字符输入流关联本地文件。

FileReader(String fileName):创建字符输入流关联本地文件。

从纯文本文件中读取数据

1
int read()`方法,每次可以读取一个字符的数据,提升为int类型,读取到文件末尾,返回`-1
  • 按字节进行读取,遇到中文,一次读多个字节,读取后解码,返回一个整数。
  • 返回值也表示在字符集上的数字。
1
int read(char[] cbuf)`方法,每次读取 b 的长度个字符到数组中,返回读取到的有效字符个数,读取到末尾时,返回`-1
  • 读取数据、解码、强转三步合并了。
2.4.3 字符输出流Writer

java.io.Writer 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • void write(int c): 写入单个字符。
  • void write(char[] cbuf) :写入字符数组。
  • abstract void write(char[] cbuf, int off, int len) :写入字符数组 cbuf 的 off 索引开始 len 个字符。
  • void write(String str) :写入字符串。
  • void write(String str, int off, int len) :写入字符数组 cbuf 的 off 索引开始 len 个字符。
  • void flush() :刷新该流的缓冲。
  • void close() :关闭此流,但要先刷新它。
2.4.4 FileWriter类

java.io.FileWriter 类是字符输出流Writer的一个子类,用于写出字符到纯文本文件。构造时使用系统默认的字符编码和默认字节缓冲区。

构造方法

FileWriter(File file): 创建字符输出流关联本地文件。

FileWriter(String fileName): 创建字符输出流关联本地文件。

FileWriter(File file, boolean append):创建字符输出流关联本地文件,并指定是否续写。

FileWriter(String pathname, boolean append):创建字符输出流关联本地文件,并指定是否续写。

  • 如果父级路径正确且文件不存在,会创建一个新的文件。
  • 如果文件已存在会清空文件。

写出数据到纯文本文件中

  • void write(int c):写出 c 索引在字符集上对应的字符。
  • void write(String str):写出一个字符串。
  • void write(String str, int off, int len):写出一个字符串的一部分。
  • void write(char[] cbuf):写出一个字符数组。
  • void write(char[] cbuf, int off, int len):写出字符数组的一部分。
2.4.5 源码解析

字符输入流底层源码分析

  1. 创建字符输入流对象

    底层:关联文件,并创建长度为8192个字节长度的数组作为缓存区。

  2. 读取数据

    底层: 判断缓冲区中是否有数据可以获取

    缓冲区没有数据:就从文件中获取数据装到缓冲区中,每次尽可能装满缓冲区

    如果文件中也没有数据了,返回-1

    缓冲区有数据:就从缓冲区中获取

    空参的read方法,一次获取一个字节,遇到中文一次读多个字节,把字节解码成十进制返回

    带参的read方法,把读取字节、解码、强转三步合并了,强转之后的字符放到数组中

字符输出流底层源码分析

在进行输出时,先创建长度为8192个字节长度的数组作为缓存区,当发生以下三种情况才清空缓冲区并把内容输出到文件中:

  1. 情况一:缓冲区装满了

    缓冲区刚好装满不会输出,再往缓冲流添加才会输出

  2. 情况二:手动刷新,即flush

    public void flush():清空缓冲区并把缓冲区的数据输出到文件中,此时还可以往文件中写出数据

  3. 情况三:释放资源/关流,即close

2.5 IO异常处理

image-20250601190140158

JDK7以前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("D:\\itheima\\movie.mp4");
fos = new FileOutputStream("myio\\copy.mp4");
//2.拷贝
int len;
byte[] bytes = new byte[1024 * 1024 * 5];
while((len = fis.read(bytes)) != -1){
fos.write(bytes,0,len);
}
} catch (IOException e) {
//e.printStackTrace();
} finally {
//3.释放资源
if(fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

JDK7

1
2
3
4
5
6
7
8
9
10
11
12
//只有实现了AutoCloseable接口的类,才能在小括号中创建对象。
try (FileInputStream fis = new FileInputStream("D:\\itheima\\movie.mp4");
FileOutputStream fos = new FileOutputStream("myio\\copy.mp4")) {
//2.拷贝
int len;
byte[] bytes = new byte[1024 * 1024 * 5];
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}

JDK9以后

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建流对象
FileInputStream fis = new FileInputStream("D:\\itheima\\movie.mp4");
FileOutputStream fos = new FileOutputStream("myio\\copy.mp4");
try (fis;fos) {
//2.拷贝
int len;
byte[] bytes = new byte[1024 * 1024 * 5];
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}

2.6 综合练习

拷贝文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
* 作用:拷贝文件夹
* 参数一:数据源
* 参数二:目的地
*
* */
private static void copydir(File src, File dest) throws IOException {
dest.mkdirs();
//递归
//1.进入数据源
File[] files = src.listFiles();
//2.遍历数组
for (File file : files) {
if (file.isFile()) {
//3.判断文件,拷贝
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(new File(dest, file.getName()));
byte[] bytes = new byte[1024];
int len;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
fos.close();
fis.close();
} else {
//4.判断文件夹,递归
copydir(file, new File(dest, file.getName()));
}
}
}

文件加密和解密

1
2
3
4
5
6
7
8
9
10
11
12
//使用异或运算进行文件加密和解密
public static void encryptionAndReduction(File src, File dest) throws IOException {
FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest);
int b;
while ((b = fis.read()) != -1) {
fos.write(b ^ 2);
}
//4.释放资源
fos.close();
fis.close();
}

2.7 缓冲流

缓冲流,也叫高效流,是对4个基本的FileXxx 流的增强,所以也是4个流,按照数据类型分类:

  • 字节缓冲流BufferedInputStreamBufferedOutputStream
  • 字符缓冲流BufferedReaderBufferedWriter

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

2.7.1 字节缓冲流

构造方法

public BufferedInputStream(InputStream in) :把基本流包装成缓冲流,提高读取数据的性能

public BufferedOutputStream(OutputStream out): 把基本流包装成缓冲流,提高读取数据的性能

  • **原理:**底层自带了长度为8192个字节的缓冲区提高性能,输入流和和输出流会分别创建一个缓冲区,两个缓冲区不一样

img

成员方法

与普通字节输入流和普通字节输出流一致。

2.7.2 字符缓冲流

构造方法

public BufferedReader(Reader in) :创建一个 新的缓冲输入流。

public BufferedWriter(Writer out): 创建一个新的缓冲输出流。

  • **原理:**底层自带了长度为8192个字符的缓冲区,输入流和和输出流会分别创建一个缓冲区,两个缓冲区不一样。
  • 由于普通字符流已经有自带的缓冲区,所以字符缓冲流提速不明显。

成员方法

和普通字符输入流和普通字符输出流一致,但是有两个特有的方法:

BufferedReader:public String readLine():读一行文字(一行末尾的换行符读取不到),读取到结束返回null。

BufferedWriter:public void newLine():写一个行分隔符,由系统属性定义符号。

2.7.3 练习:文本排序

将下列文本信息排序(每一个序号为一行,只是显示不下所以是多行):

1
2
3
4
5
6
7
8
9
10
3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
9.今当远离,临表涕零,不知所言。
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。
Lobster AIjava运行123456789

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Demo05Test {
public static void main(String[] args) throws IOException {
//1.创建ArrayList集合,泛型使用String
ArrayList<String> list = new ArrayList<>();
//2.创建BufferedReader对象,构造方法中传递FileReader对象
BufferedReader br = new BufferedReader(new FileReader("10_IO\\in.txt"));
//3.创建BufferedWriter对象,构造方法中传递FileWriter对象
BufferedWriter bw = new BufferedWriter(new FileWriter("10_IO\\out.txt"));
//4.使用BufferedReader对象中的方法readLine,以行的方式读取文本
String line;
while((line = br.readLine())!=null){
//5.把读取到的文本存储到ArrayList集合中
list.add(line);
}
//6.使用Collections集合工具类中的方法sort,对集合中的元素按照自定义规则排序
Collections.sort(list, new Comparator<String>() {
/*
o1-o2:升序
o2-o1:降序
*/
@Override
public int compare(String o1, String o2) {
//依次比较集合中两个元素的首字母,升序排序
return o1.charAt(0)-o2.charAt(0);
}
});
//7.遍历ArrayList集合,获取每一个元素
for (String s : list) {
//8.使用BufferedWriter对象中的方法wirte,把遍历得到的元素写入到文本中(内存缓冲区中)
bw.write(s);
//9.写换行
bw.newLine();
}
//10.释放资源
bw.close();
br.close();
}
}
Lobster AIjava运行1234567891011121314151617181920212223242526272829303132333435363738

2.8 转换流

如果两个文件的编码方式不一样,使用普通流进行读取写入交互时就会出现乱码问题,这时可以使用转换流,在读和写的时候分别进行编码调整。

image-20250601192813265

2.8.1 InputStreamReader类

转换流java.io.InputStreamReader是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

InputStreamReader(InputStream in):创建一个使用默认字符集的字符流。

InputStreamReader(InputStream in, String charsetName):创建一个指定字符集的字符流。

成员方法

可以使用字符输入流Reader中的所有方法,如read、colse。

2.8.2 OutputStreamWriter类

转换流java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

OutputStreamWriter(OutputStream in):创建一个使用默认字符集的字符流。

OutputStreamWriter(OutputStream in, String charsetName):创建一个指定字符集的字符流

2.8.3 转换流的使用

指定字符集读写(被淘汰)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
		//1.创建对象并指定字符编码
InputStreamReader isr = new InputStreamReader(new FileInputStream("myio\\gbkfile.txt"),"GBK");
//2.读取数据
int ch;
while ((ch = isr.read()) != -1){
System.out.print((char)ch);
}
//3.释放资源
isr.close();


//JDK11后被FileReader替代
FileReader fr = new FileReader("myio\\gbkfile.txt", Charset.forName("GBK"));
//2.读取数据
int ch;
while ((ch = fr.read()) != -1){
System.out.print((char)ch);
}
//3.释放资源
fr.close();
Lobster AIjava运行1234567891011121314151617181920

字节流想要使用字符流中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
		//1.创建转换流的对象
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("myio\\b.txt"),"GBK");
//2.写出数据
osw.write("你好你好");
//3.释放资源
osw.close();


//JDK11后被FileWiter替代
FileWriter fw = new FileWriter("myio\\c.txt", Charset.forName("GBK"));
fw.write("你好你好");
fw.close();
Lobster AIjava运行123456789101112

爬取网站的html内容

参考十一、正则表达式5.2 网络数据爬取

练习:转换文件编码

将GBK编码的文本文件,转换为UTF-8编码的文本文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TransDemo {
public static void main(String[] args) {
// 1.定义文件路径
String srcFile = "file_gbk.txt";
String destFile = "file_utf8.txt";
// 2.创建流对象
// 2.1 转换输入流,指定GBK编码
InputStreamReader isr = new InputStreamReader(new FileInputStream(srcFile) , "GBK");
// 2.2 转换输出流,默认utf8编码
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(destFile));
// 3.读写数据
// 3.1 定义数组
char[] cbuf = new char[1024];
// 3.2 定义长度
int len;
// 3.3 循环读取
while ((len = isr.read(cbuf))!=-1) {
// 循环写出
osw.write(cbuf,0,len);
}
// 4.释放资源
osw.close();
isr.close();
}
}
Lobster AIjava运行12345678910111213141516171819202122232425

2.9 序列化流和反序列化流

在开发中,一个用户就是一个对象,如果把对象写到本地文件,明文写会非常不安全,别人可以修改数据,这时可以使用序列化流和反序列化流。

序列化:把对象的属性、方法数据类型等数据转换成一个字节序列写出到本地文件中存储。

反序列化:把对象的属性、方法数据类型等数据从本地文件的字节序列中读取出来使用。

2.9.1 ObjectOutputStream序列化流

java.io.ObjectOutputStream 类,将Java对象的原始数据写到文件,实现对象的持久存储。

构造方法

public ObjectOutputStream(OutputStream out) : 把基本流包装成序列化流。

成员方法

public final void writeObject (Object obj):将对象序列化写出到ser或obj文件中去。

  • 使用序列化流将对象写到文件时,需要让JavaBean类实现Serializable接口,否则,会出现NotSerializableException异常

Serializable接口没有任何抽象方法,是标记性接口,被Serializable接口标记的类时可以被序列化的。

2.9.2 ObjectInputStream反序列化流

构造方法

public ObjectInputStream(InputStream in) :把基本流变成反序列化流。

成员方法

public final Object readObject ():把序列化到本地文件中的对象读取到程序中。

  • 序列化流写到文件中的数据是不能修改的,一旦修改就无法再次读回来了。
2.9.3 序列化流和反序列化流细节
  1. 序列化对象后,修改了JavaBean类,再次反序列化,会出现InvalidClassException异常,解决方案是使用语句:private static final long serialVersionUID = 版本号L为类添加一个版本号serialVersionUID
  2. 如果一个对象中的某个成员变量的值不想被序列化,可以为这个成员变量添加transient关键字修饰,该关键字标识的变量不参与序列化,反序列化读取后是默认值。
  3. 对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常。

idea自动生成serialVersionUID:

image-20250601200036007

2.9.4 案例:序列化集合

将存有多个自定义对象的集合序列化操作,保存到list.txt文件中,并进行反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SerTest {
public static void main(String[] args) throws Exception {
ArrayList<Student> arrayList = new ArrayList<>();
arrayList.add(new Student("老王", "laow"));
arrayList.add(new Student("老张", "laoz"));
arrayList.add(new Student("老李", "laol"));
// 序列化操作
serializ(arrayList);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.txt"));
// 读取对象,强转为ArrayList类型
ArrayList<Student> list = (ArrayList<Student>)ois.readObject();

for (int i = 0; i < list.size(); i++ ){
Student s = list.get(i);
System.out.println(s.getName()+"--"+ s.getPwd());
}
}

private static void serializ(ArrayList<Student> arrayList) throws Exception {
// 创建 序列化流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
// 写出对象
oos.writeObject(arrayList);
// 释放资源
oos.close();
}
}
Lobster AIjava运行12345678910111213141516171819202122232425262728

2.10 打印流

特点:

  1. 打印流只操作文件目的地,不操作数据源。
  2. 特有的写出方法,可以实现数据原样输出。
  3. 特有的写出方法,可以实现自动刷新,自动换行。
2.10.1 PrintStream字节打印流

构造方法

public PrintStream(OutputStream/File/String):关联字节输出流/文件/文件路径。

public PrintStream(String fileName, Charset charset):指定字符编码。

public PrintStream(OutputStreamout, boolean autoFlush):自动刷新。

public PrintStream(OutputStream out, boolean autoFlush, String encoding):指定字符编码且自动刷新。

  • 字节流底层没有缓冲区,开不开自动刷新都一样。

成员方法

public void write(int b):常规方法:规则跟之前一样,将指定的字节写出。

public void println(Xxx xx):特有方法:打印任意数据,自动刷新,自动换行。

public void print(Xxx xx):特有方法:打印任意数据,不换行。

public void printf(String format, Object... args):特有方法:带有占位符的打印语句,不换行。

拓展

System.out就是PrintStream类型的,只不过它的流向是系统规定的,打印在控制台上。可以通过System.setOut(PrintStream ps)改变它的流向。

2.10.2 PrintWriter字符打印流

构造方法

public PrintWriter(Write/File/String):关联字节输出流/文件/文件路径。

public PrintWriter(String fileName, Charset charset):指定字符编码。

public PrintWriter(Write w, boolean autoFlush):自动刷新。

public PrintWriter(Write out, boolean autoFlush, String encoding):指定字符编码且自动刷新。

  • 字符流底层有缓冲区,想要自动刷新需要开启。

成员方法

public void write(int b):常规方法:规则跟之前一样,将指定的字节写出。

public void println(Xxx xx):特有方法:打印任意数据,自动刷新,自动换行。

public void print(Xxx xx):特有方法:打印任意数据,不换行。

public void printf(String format, Object... args):特有方法:带有占位符的打印语句,不换行。

2.11 解压缩流和压缩流

Java的解压缩流和压缩流都只能操作zip压缩文件。

2.11.1 ZipInputStream解压缩流

压缩包里的每一个文件或文件夹是一个ZipEntry对象,解压的本质就是把每一个ZipEntry按照层级拷贝到本地的另一个文件夹中。

构造方法

ZipInputStream(InputStream in):将字节输入流对象包装成解压缩流对象。

成员方法

ZipEntry getNextEntry():读取下一个 ZIP 文件条目并将流定位到该条目数据的开始处,返回下一个文件条目,如果不存在更多条目则返回null。

read方法:和基本输入流一致。

void closeEntry():关闭当前 ZIP 条目并定位流以读取下一个条目,使用read读取文件后必须用closeEntry退出当前文件,否则再调用getNextEntry方法读取压缩包条目会出现异常。

void close():关流并释放资源。

ZipEntry成员方法

boolean isDirectory():判断当前对象是否是文件夹(目录),如果是就返回true,不是就返回false。

String toString():返回 ZIP 条目的字符串表示形式。

解压zip文件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    /**
* @param src 要解压的zip压缩包
* @param dest 解压到哪里
* @throws IOException
*/
public static void unZip(File src, File dest) throws IOException {
//创建解压缩流
ZipInputStream zip = new ZipInputStream(new FileInputStream(src));
//创建ZipEntry对象表示压缩包里的每一个文件或目录
ZipEntry entry;
//循环遍历压缩包里的每一个文件或目录
while ((entry = zip.getNextEntry()) != null) {
if (entry.isDirectory()) {
//如果是目录就创建目录
new File(dest, entry.toString()).mkdirs();
} else {
//如果是文件就拷贝文件
FileOutputStream fos = new FileOutputStream(new File(dest, entry.toString()));
int len;
byte[] buffer = new byte[1024 * 1024];
while ((len = zip.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.close();
zip.closeEntry();
}
}
//关流释放资源
zip.close();
}
Lobster AIjava运行123456789101112131415161718192021222324252627282930
2.11.2 ZipOutputStream压缩流

压缩包里的每一个文件或文件夹是一个ZipEntry对象,压缩的本质是把每一个(文件/文件夹)看成ZipEntry对象放到压缩包中。

构造方法

ZipOutputStream(OutputStream out):将字节输出流对象封装成压缩流对象。

成员方法

void putNextEntry(ZipEntry e):把ZipEntry对象写入到当前流指定的位置,并将流定位到条目数据的开始处。

write方法:和字节输出流的write方法一致。

void closeEntry():关闭当前 ZIP 条目并定位流以读取下一个条目,使用read读取文件后必须用closeEntry退出当前文件,否则再调用putNextEntry方法读取压缩包条目会出现异常。

void close():关流并释放资源。

ZipEntry构造方法

ZipEntry(String name):使用指定名称创建新的 ZIP 条目,如果name是一个多级文件夹,就创建一个多级文件夹的 ZIP 条目。

压缩文件夹代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 	 /**
* 获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中
* @param src 要压缩的文件
* @param zos 压缩目的地zip流
* @param name 压缩包内部的路径
* @throws IOException
*/
public static void toZip(File src, ZipOutputStream zos, String name) throws IOException {
//1.进入src文件夹
File[] files = src.listFiles();
//2.遍历数组
for (File file : files) {
if (file.isFile()) {
//3.判断-文件,变成ZipEntry对象,放入到压缩包当中
ZipEntry entry = new ZipEntry(name + "\\" + file.getName());//aaa\\no1\\a.txt
zos.putNextEntry(entry);
//读取文件中的数据,写到压缩包
FileInputStream fis = new FileInputStream(file);
int b;
while ((b = fis.read()) != -1) {
zos.write(b);
}
fis.close();
zos.closeEntry();
} else {
//4.判断-文件夹,递归
toZip(file, zos, name + "\\" + file.getName());
}
}
}
Lobster AIjava运行123456789101112131415161718192021222324252627282930
  • 调用示例:toZip(src,zos,src.getName());

3.工具包

3.1 Commons-io工具包

Commons是apache开源基金组织提供的工具包,里面有很多帮助我们提高开发效率的API。

其中,Commons-io是apache开源基金组织提供的一组有关IO操作的开源工具包。

使用方式

1,在项目下新建lib文件夹。

2,把资料中的第三方jar包粘贴到文件夹中。

3,右键点击add as a library。

  • 工具类和相关方法参考资料中的相关文档。

3.2 hutool工具包

hutool有IO流和其他各种类型的工具类,用来提高开发效率。

使用方式

1,新建lib文件夹。

2,把资料中的第三方jar包粘贴到文件夹中。

3,右键点击add as a library。

4.Properties配置文件

4.1 概述

配置文件

  • 可以把软件的设置永久化存储。
  • 如果要修改参数,不需要改动代码,直接修改配置文件就可以了。

Properties配置文件

key=value的形式存储数据,如name=itcast。多行的话不使用分隔符,每行都是key=value的形式。

  1. 以properties作为文件后缀名。
  2. Properties是一个双列集合,属于Map的分支,具有Map集合的所有特点和方法。
  3. Properties的键和值一般都用String类型。
  4. 还有一些特有的方法,可以把集合中的数据,按照键值对的形式写到配置文件中,也可以把配置文件中的数据读取到集合当中。

4.2 Properties类

java.util.Properties类是操作properties文件的工具类,使用时不需要导包。

4.2.1 构造方法

public Properties():创建一个空的属性列表。

public Properties(Properties defaults):创建一个带有指定默认值的属性列表。

1
2
3
4
5
Properties props = new Properties();
Properties defaultProps = new Properties();
defaultProps.setProperty("user", "admin");
Properties propsWithDefaults = new Properties(defaultProps);
Lobster AIjava运行1234
4.2.2 常用方法

设置和获取属性

  • void setProperty(String key, String value):设置属性键值对。
  • String getProperty(String key):获取指定键对应的值。
  • String getProperty(String key, String defaultValue):获取指定键对应的值,如果键不存在,则返回默认值。

加载和保存属性

  • void load(InputStream inStream):从输入流中加载属性列表。
  • void load(Reader reader):从字符流中加载属性列表。
  • void store(OutputStream out, String comments):将属性列表保存到输出流,并添加注释。
  • void store(Writer writer, String comments):将属性列表保存到字符流,并添加注释。

遍历属性

Set<String> stringPropertyNames():返回所有属性键的集合。

Set<Map.Entry<Object,Object>> entrySet():返回所有属性键值对的集合。

其他方法

void list(PrintStream out):将属性列表打印到指定的输出流。

void list(PrintWriter out):将属性列表打印到指定的字符输出流。

void clear():清空所有属性。

  • list方法和store方法的区别
    • list方法只是把属性列表打印到输出流,并不会输出到文件中,在调试时使用;
    • store方法即把属性列表打印到输出流,还会输出到文件中,在保存配置时使用。
  • 技巧props.list(System.out);可以把属性列表打印到控制台。

配置文件被流清空的解决办法

当我们每次使用Properties写数据到配置文件中时,由于输出流的创建,总是自动清空配置文件,导致数据丢失。可以在每次写数据之前,先加载配置文件中的所有属性,再进行写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class PropertiesUpdateExample {
public static void main(String[] args) {
Properties props = new Properties();
File propertiesFile = new File("config.properties");

try {
// 1. 先加载原文件的所有属性(如果文件存在)
if (propertiesFile.exists()) {
try (FileInputStream fis = new FileInputStream(propertiesFile)) {
props.load(fis);
}
}

// 2. 修改或添加属性(不会影响原文件)
props.setProperty("age", "31"); // 更新现有键
props.setProperty("city", "Beijing"); // 添加新键

// 3. 一次性写入(覆盖原文件,但保留未修改的属性)
try (FileOutputStream fos = new FileOutputStream(propertiesFile)) {
props.store(fos, "Updated properties");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Lobster AIjava运行1234567891011121314151617181920212223242526

由于程序可能出现各种异常,导致写入到一半程序非正常停止,数据丢失,解决办法是每次先把配置属性拷贝到新创建的临时文件中,更新完再删除临时文件(临时文件通过File类进行创建和删除)。

十九、多线程

进程:正在运行的程序,如微信。

线程:是操作系统能够进行运算的最小单位,他被包含在进程当中,是进程中的实际运作单位。如小程序、聊天等

  • 简单理解:应用软件中相互独立,可以同时运行的功能。

多线程:多个线程同时运行或交替运行就是多线程,如聊天和小程序可以同时使用。

并行:在同一时刻,有多个指令在多个CPU上同时执行。

并发:在同一时刻,有多个指令在单个CPU上交替执行。

1.多线程的实现方式

1.1 继承Thread类

  1. 定义一个类继承Thread类
  2. 重写run()方法
    • void run():在线程开启后,此方法将自动被调用执行。
  3. 创建子类的对象,并启动线程
    • void start():使此线程开始执行,Java虚拟机会调用run方法()。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyThread extends Thread {
@Override
public void run() {
...
}
}
//void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.start();
my2.start();
}

1.2 实现Runnable接口

  1. 定义一个类实现Runnable接口
  2. 重写run()方法
  3. 创建类的对象
  4. 创建Thread类的对象,并启动线程
    • Thread(Runnable target):分配一个新的Thread对象。
    • Thread(Runnable target, String name):分配一个新的Thread对象,并为线程起别名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyRunnable implements Runnable {
@Override
public void run() {
...
}
}
public static void main(String[] args) {
MyRunnable my = new MyRunnable();
Thread t1 = new Thread(my,"线程1");
Thread t2 = new Thread(my,"线程2");
//等效于
//Thread t1 = new Thread(my), t2 = new Thread(my);
//t1.setName("线程1");
//t2.setName("线程2");
t1.start();
t2.start();
}

1.3 实现Callable接口

  1. 定义一个类实现Callable<V>接口
  2. 重写call()方法(带返回值,表示多线程运行的结果)
    • V call():计算结果,如果无法计算结果,则抛出一个异常。
  3. 创建实现Callable的类的对象(表示多线程要执行的任务)
  4. 创建FutureTask的对象(作用管理多线程运行的结果)
    • FutureTask(Callable<V> callable):创建一个 FutureTask,一旦运行就执行给定的 Callable。
    • V get():如有必要,等待计算完成,然后获取其结果。
  5. 创建Thread类的对象,并启动线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
...
return "答应";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//表示多线程要执行的任务
MyCallable mc = new MyCallable();
//Thread t1 = new Thread(mc);
//管理多线程运行的结果
FutureTask<String> ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft);
//开启线程
t1.start();
//获取线程执行的结果
String s = ft.get();
System.out.println(s);
}

1.4 三种实现方式的对比

  • 实现Runnable、Callable接口
    • 好处:扩展性强,实现该接口的同时还可以继承其他的类。
    • 缺点:编程相对复杂,不能直接使用Thread类中的方法。
  • 继承Thread类
    • 好处:编程比较简单,可以直接使用Thread类中的方法。
    • 缺点:可以扩展性较差,不能再继承其他的类。

三种方式的选择

  • 如果只是简单的线程,并不需要继承其他类或需要使用到Thread类成员方法,选择继承Thread类。
  • 如果需要继承其他类,而且不需要Thread类成员方法,选择实现Runnable接口。
  • 如果需要线程返回一个结果,选择实现Callable接口。

在Java中,一个线程对应一个栈,如main线程就是一个栈,有几个线程就有几个栈。

2.Thread类成员方法

1、String getName():返回此线程的名称

2、void setName(String name):设置线程的名字(构造方法也能设置名字)

  • 如果没有给线程设置名字,线程默认名字是Thread-X(X序号,从0开始)。

3、Thread currentThread():获取当前正在执行的线程对象

  • 虚拟机执行后,会自动执行多条线程,我们写的所有代码都运行在main线程中。

4、static void sleep(long millis):让线程休息指定的时间,单位为毫秒

5、final int getPriority():返回此线程的优先级

6、final void setPriority(int newPriority):更改此线程的优先级

  • 线程默认优先级是5;线程优先级的范围是:1-10

分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片。

抢占式调度模型:随机选择一个线程使用CPU,优先级高的线程获取的 CPU 的概率更大一些。

Java使用的是抢占式调度模型。

随机性:假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的。

7、void setDaemon(boolean on):将此线程标记为守护线程(备胎线程)

  • 当其他线程执行完毕后,守护线程会陆续结束,但不会立刻结束。
  • 当运行的线程都是守护线程时,Java虚拟机将退出。

8、static void yield():出让线程/礼让线程

  • 写在run方法内,出让当前线程执行权,重新抢夺执行权。
  • 可以让结果更加均匀一点儿。

9、static void join():插入线程/插队线程

  • 把线程插入到当前线程之前执行。

3.线程的声明周期

image-20250601234546048

4.数据安全问题

4.1 安全问题出现的条件

  • 是多线程环境
  • 有共享数据(可以是静态成员)
  • 有多条语句操作共享数据

4.2 解决多线程安全问题

4.2.1 同步代码块

把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。

1
2
3
4
synchronized(锁) { 
多条语句操作共享数据的代码
}
Lobster AIjava运行123
  • 特点一:锁默认打开,有一个线程进去了,锁自动关闭
  • 特点二:里面的代码全部执行完毕,线程出来,锁自动打开
  • 锁可以是任意对象,但一定要是唯一的,比如用static修饰的对象
  • 锁对象一般是当前类的字节码文件对象,即当前类名.class
4.2.2 同步方法

就是把synchronized关键字加到方法上。

1
2
3
4
修饰符 synchronized 返回值类型 方法名(方法参数) { 
方法体;
}
Lobster AIjava运行123
  • 特点一:同步方法就是锁住方法里所有的代码
  • 特点二:锁对象不能自己指定(系统会帮忙指定)
    • 非静态方法:this
    • 静态方法:当前类的字节码文件对象

StringBuffer类

  1. 线程安全的可变字符序列。一个类似于 StringBuilder 的字符串缓冲区,可将字符串缓冲区安全地用于多个线程。
  2. 管理字符串的类,构造方法和成员方法和 StringBuilder 类一样。
  3. StringBuilder 的实例用于多个线程是不安全的。如果需要这样的同步,则建议使用StringBuffer。
4.2.3 Lock锁

Lock锁相较于同步代码块和同步方法可以动态的加锁和释放锁。

Lock是一个接口,不能直接创建其对象,可以用它的实现类ReentrantLock来实例化。

成员方法:

  • void lock():获得锁
  • void unlock():释放锁

**注意:**创建的Lock对象是实例化对象时,要加上static表示锁唯一,不然会出现安全性问题。

4.2.4 死锁

线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。

发生条件:

  1. 资源有限
  2. 同步嵌套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();

new Thread(()->{
while(true){
synchronized (objA){
//线程一
synchronized (objB){
System.out.println("小康同学正在走路");
}
}
}
}).start();

new Thread(()->{
while(true){
synchronized (objB){
//线程二
synchronized (objA){
System.out.println("小薇同学正在走路");
}
}
}
}).start();
}
Lobster AIjava运行1234567891011121314151617181920212223242526

**避免:**程序中不要出现锁的嵌套。

5.生产者消费者

5.1 概述

生产者消费者模式是一个十分经典的多线程协作的模式。

生产者消费者问题,主要是包含了两类线程:

一类是生产者线程用于生产数据

一类是消费者线程用于消费数据

同时,使用共享数据区解决生产者和消费者的关系。

方法名 说明
void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify() 唤醒正在等待对象监视器的单个线程
void notifyAll() 唤醒正在等待对象监视器的所有线程

5.2 代码实现

厨师是生产者,顾客是消费者,桌子是中间共享数据区,桌子上有汉堡包顾客吃,桌子上没汉堡包厨师做,顾客最多吃10个。

5.2.1 用三个类实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class Desk {
//定义标记表示桌子上是否有汉堡包
public static boolean flag = false;
//汉堡包的总数量,最多能吃10个
public static int count = 10;
//锁对象
public static final Object lock = new Object();
}

public class Cooker extends Thread {
//生产者。
//1.判断是否有包子,决定当前线程是否执行
//2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子
//3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子
@Override
public void run() {
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
if(!Desk.flag){
//生产
System.out.println("厨师正在生产汉堡包");
Desk.flag = true;
Desk.lock.notifyAll();
}else{
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}

public class Foodie extends Thread {
@Override
public void run() {
//消费者
//1.判断是否有包子,决定当前线程是否执行
//2.如果没有包子,就进入等待状态,如果有包子,就消费包子
//3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
if(Desk.flag){
//有
System.out.println("吃货在吃汉堡包");
Desk.flag = false;
Desk.lock.notifyAll();
Desk.count--;
}else{
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

}
}

public class Demo {
public static void main(String[] args) {
//测试类,运行线程
Foodie f = new Foodie();
Cooker c = new Cooker();
f.start();
c.start();

}
}
Lobster AIjava运行123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
5.2.2 阻塞队列实现

image-20250602002724490

常见BlockingQueue<E>实现类:

  • ArrayBlockingQueue:底层是数组,有界。
  • LinkedBlockingQueue:底层是链表,无界。但不是真正的无界,最大为int的最大值。

常见BlockingQueue<E>核心方法:

  • void put(E e):将指定元素插入此队列中,如果放不进去会阻塞。
  • E take():获取并移除此队列的头部,取不到会阻塞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class Cooker extends Thread {

private ArrayBlockingQueue<String> bd;

public Cooker(ArrayBlockingQueue<String> bd) {
this.bd = bd;
}
@Override
public void run() {
//生产者
//1.构造方法中接收一个阻塞队列对象
//2.在run方法中循环向阻塞队列中添加包子
//3.打印添加结果
while (true) {
try {
bd.put("汉堡包");
System.out.println("厨师放入一个汉堡包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class Foodie extends Thread {
private ArrayBlockingQueue<String> bd;

public Foodie(ArrayBlockingQueue<String> bd) {
this.bd = bd;
}

@Override
public void run() {
//消费者
//1.构造方法中接收一个阻塞队列对象
//2.在run方法中循环获取阻塞队列中的包子
//3.打印获取结果
while (true) {
try {
String take = bd.take();
System.out.println("吃货将" + take + "拿出来吃了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
}

public class Demo {
public static void main(String[] args) {
//测试类
//1.创建阻塞队列对象
//2.创建生产者线程和消费者线程对象,构造方法中传入阻塞队列对象
//3.分别开启两个线程
ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);

Foodie f = new Foodie(bd);
Cooker c = new Cooker(bd);

f.start();
c.start();
}
}
Lobster AIjava运行12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364

6.线程的状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。

线程总共有6种状态,分别是:

  • 新建状态(NEW):创建线程对象。
  • 就绪状态(RUNNABLE):start方法。
  • 阻塞状态(BLOCKED):无法获取锁对象。
  • 等待状态(WAITING):wait方法。
  • 计时等待(TIMED_WAITING):sleep方法。
  • 结束状态(TERMINATED):全部代码运行完毕。

image-20250602003411212

7.线程池

传统的线程使用时需要创建,用完就消失,再次使用必须重新创建。线程池可以存放线程,保证线程不会消失。

7.1 线程池主要核心原理

  1. 创建一个池子,池子中是空的。
  2. 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给线程池,下次再次提交任务时,不需要创建新的线程,直接复用已有的线程即可。
  3. 如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务会排队等待。

7.2 默认线程池

7.2.1 创建线程池

使用Executors中所提供的静态方法来创建线程池

  • static ExecutorService newCachedThreadPool() :创建一个默认的无上限的线程池。
  • static ExecutorService newFixedThreadPool(int nThreads):创建一个指定最多线程数量的线程池。
7.2.2 提交任务

Future<?> submit(Runnable task):提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功完成时将会返回 null。

7.3.3 关闭线程池

所有任务执行完毕,关闭线程池(可以不关)

  • void shutdown():调用 shutdown() 后,线程池实例仍然存在,但处于关闭流程中,不再接受新任务。
7.3.4 代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyRunnable implements Runnable{
@Override
public void run() {
...
}
}
public static void main(String[] args) throws InterruptedException {
//1.获取线程池对象
ExecutorService pool1 = Executors.newFixedThreadPool(3);
//2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
//3.销毁线程池,可以不销毁
pool1.shutdown();
}
Lobster AIjava运行123456789101112131415

7.3 创建自定义线程池

自定义线程池ThreadPoolExecutor由核心线程、临时线程和阻塞队列构成:

  1. 创建一个空的池子。
  2. 有任务提交时,会创建线程去执行任务,执行完毕归还线程。
7.3.1 四种执行任务情况

image-20250602005536441

  1. 提交任务时,如果线程池中有相应线程,就调用线程池中的线程。

  2. 如果没有可以用的线程,创建核心线程去执行任务,任务有几个创建几个核心线程。

  3. 核心线程满时,再次提交任务会排队。

  4. 当队列也满时,再次提交任务会创建临时线程去执行任务(队列中的线程先提交,不一定先执行)。

  5. 当核心线程、队列、临时线程都满时,再次提交任务会触发任务拒绝策略。

    线程池最多可执行的任务数 = 队列容量 + 最大线程数(核心线程数+临时线程数)

7.3.2 构造方法
1
2
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
Lobster AIjava运行1

参数详解

假如有一个饭店,只能有三个核心员工,三个临时员工,如果顾客数量>核心员工数量,就会排队,如果队列也满了,就会招聘临时员工,临时员工空闲一段时间会被开除。这个饭店和参数的关系如下:

image-20250602152613037

例如:

1
2
3
4
5
6
7
8
9
10
11
12
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
5,
2,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
pool.submit(new MyRunnable()),
pool.submit(new MyRunnable()),
pool.shutdown();
Lobster AIjava运行1234567891011
7.3.3 任务拒绝策略

对应第7个参数。

1
2
3
4
5
ThreadPoolExecutor.AbortPolicy: 		    丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。
Lobster AIjava运行1234
7.3.4 线程池大小选择

线程池大小就是第二个参数最大线程数。

CPU密集型运算:运算占比多的程序,读取文件、访问操作系统相对较少。

I/O密集型运算:读取文件、访问操作系统占比多的程序,运算相对较少;大部分项目都属于这种。

最大并行数:例如 4核8线程 的电脑就是8,可以通过Runtime.getRuntime().availableProcessors()获取。

image-20250602010526637

8.多线程综合案例

抢红包

100块,分成了3个包,现在有5个人去抢,并且每个人保底抢到1分钱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class MyThread extends Thread{

//共享数据
//100块,分成了3个包
static double money = 100;
static int count = 3;

//最小的中奖金额
static final double MIN = 0.01;

@Override
public void run() {
//同步代码块
synchronized (MyThread.class){
if(count == 0){
//判断,共享数据是否到了末尾(已经到末尾)
System.out.println(getName() + "没有抢到红包!");
}else{
//判断,共享数据是否到了末尾(没有到末尾)
//定义一个变量,表示中奖的金额
double prize = 0;
if(count == 1){
//表示此时是最后一个红包
//就无需随机,剩余所有的钱都是中奖金额
prize = money;
}else{
//表示第一次,第二次(随机)
Random r = new Random();
//100 元 3个包
//第一个红包:99.98
//100 - (3-1) * 0.01
double bounds = money - (count - 1) * MIN;
prize = r.nextDouble(bounds);
if(prize < MIN){
prize = MIN;
}
}
//从money当中,去掉当前中奖的金额
money = money - prize;
//红包的个数-1
count--;
//本次红包的信息进行打印
System.out.println(getName() + "抢到了" + prize + "元");
}
}
}
}
public class Test {
public static void main(String[] args) {
/*
微信中的抢红包也用到了多线程。
假设:100块,分成了3个包,现在有5个人去抢。
其中,红包是共享数据。
5个人是5条线程。
打印结果如下:
XXX抢到了XXX元
XXX抢到了XXX元
XXX抢到了XXX元
XXX没抢到
XXX没抢到
*/

//创建线程的对象
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
MyThread t4 = new MyThread();
MyThread t5 = new MyThread();

//给线程设置名字
t1.setName("小A");
t2.setName("小QQ");
t3.setName("小哈哈");
t4.setName("小诗诗");
t5.setName("小丹丹");

//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
Lobster AIjava运行123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384

二十、网络编程

在网络通信协议下,不同计算机上运行的程序,可以进行数据传输。如微信聊天就是不同的手机进行消息传输。

使用java.net包下的技术开发网络编程程序。

1.基本概念

1.1 CS/BS架构

BS架构:

就是浏览访问网站,由浏览器发送请求从服务器获取数据,资源存放在服务端。

  1. 不需要开发客户端,只需要开发服务器。
  2. 用户不需要下载,打开浏览器就可直接使用。
  3. 如果应用过大,用户体验受到影响。

CS架构:

就是第三方软件,数据由客户端发送请求到服务器,资源存放在客户端。

  1. 画面可以做的特别精美。
  2. 需要开发客户端,也需要开发服务端。
  3. 用户需要下载和更新的时候太麻烦。

1.2 网络编程三要素

1.2.1 IP地址

设备在网络中的地址,是唯一的标识。相当于一台主机的身份证号。

IPv4

给每个连接在网络上的主机分配一个32bit(4字节)地址,是普遍使用的类型,使用时采用点分十进制表示法,即每一个字节为一组,每组写成十进制的形式,中间使用符号“.”分隔不同的字节。

分类:公网地址(万维网使用)和私有地址(局域网使用)

  • **192.168.**开头的是私有地址,范围是192.168.0.0~192.168.255.255,专为组织机构内部使用。

127.0.0.1:是回送地址,代表本机的地址,一般用来测试使用,每台计算机都有一个127.0.0.1 IP地址。

DOS常用命令:

  • ipconfig:查看本机IP地址
  • ping IP地址:检查网络是否连通

IPv4在2019年11月26日就不够用了。

IPv6

为了解决IPv4不够用而存在,还未普及,采用冒分十六进制表示法,即128位地址长度,每16个字节一组,分成8组十六进制数,每组中间使用”:”分隔不同字节。

1.2.2 端口号

应用程序在设备中唯一的标识。同一台主机可以有多个应用程序,这些应用程序由端口号唯一确定。

用两个字节表示的整数,它的取值范围是065535。其中,01023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号,我们自己使用1024以上的端口就可以了。

  • 一个端口号只能被一个应用程序使用
1.2.3 协议

数据在网络中传输的规则。不同主机的相同应用程序、同一主机的不同应用程序要想通信,必须遵循同一个协议(规则)。

常见的协议有UDP、TCP、http、https、ftp,我们学习网络编程使用UDP和TCP。

连接:就是两个程序通信前征用的网络线路。例如两个人通话前要先占用一个电话线路,其他人在这个线路未释放前不能使用。

UDP协议

用户数据报协议,是面向无连接通信协议

  • 速度快,有大小限制,一次最多发送64K,数据不安全,易丢失数据。
  • 损失小部分数据影响不大时使用,如音频、视频、普通数据的传输等。

TCP协议

传输控制协议,是面向连接通信协议

  • 速度慢,没有大小限制,数据安全。
  • 不能损失数据时使用,如上传文件、下载文件、浏览网页等。

2.InetAddress

Java中,万物皆对象,所以一个Internet协议地址(即IP地址)也是一个对象,这个对象就是InetAddress类的对象。

方法名 说明
static InetAddress getByName(String host) 确定主机名称的IP地址。主机名称host可以是机器名称,也可以是IP地址
String getHostName() 获取此IP地址的主机名
String getHostAddress() 返回文本显示中的IP地址字符串

3.UDP通信程序

UDP协议是一种不可靠的网络协议,它在通信的两端各建立一个Socket对象,但是这两个Socket只是发送,接收数据的对象,因此对于基于UDP协议的通信双方而言,没有所谓的客户端和服务器的概念。

3.1 UDP发送数据

3.1.1 构造方法
方法名 说明
DatagramSocket() 所有可用的端口中随机一个进行使用
DatagramSocket(int port) 指定端口号进行绑定
DatagramPacket(byte[] buf,int len,InetAddress add,int port) 创建数据包,发送长度为len的数据包到指定主机的指定端口
3.1.2 成员方法
方法名 说明
void send(DatagramPacket p) 发送数据报包
void close() 关闭数据报套接字
void receive(DatagramPacket p) 从此套接字接受数据报包
3.1.3 代码实现
  • 创建发送端的DatagramSocket对象
  • 创建数据,并把数据打包(DatagramePacket)
  • 发送数据
  • 关闭发送端,释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
		//1.创建DatagramSocket对象(快递公司)
DatagramSocket ds = new DatagramSocket();

//2.打包数据
String str = "你好威啊!!!";
byte[] bytes = str.getBytes();//要发送的字节数据
InetAddress address = InetAddress.getByName("127.0.0.1");//通过哪台主机发送
int port = 10086;//接受端端口号
//取bytes数组的前bytes.length个字节发送到address主机port端口
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);

//3.发送数据
ds.send(dp); //通过快递公司ds发送dp包裹

//4.释放资源
ds.close();
Lobster AIjava运行12345678910111213141516

3.2 UDP接受数据

3.2.1 构造方法
方法名 说明
DatagramPacket(byte[] buf, int len) 创建一个DatagramPacket用于接收长度为len的数据包
3.2.2 成员方法
方法名 说明
byte[] getData() 返回数据缓冲区
int getLength() 返回要发送的数据的长度或接收的数据的长度
3.2.3 代码实现
  • 创建接收端的DatagramSocket对象(必须指定端口)
  • 接收打包好的数据(DatagramPacket)
  • 解析数据包
  • 关闭接收端,释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
		//1.创建Socket类(快递公司)
//绑定的端口一定要跟发送的端口保持一致
DatagramSocket ds = new DatagramSocket(10086);

//2.接收数据包
byte[] bytes = new byte[1024];
//接受bytes.length个字节放到bytes数组中
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);
ds.receive(dp); //该方法是阻塞的,程序会死等,直到接受到发送端发送的数据才继续

//3.解析数据包
byte[] data = dp.getData();//发送方发送的数据在这个数组中
int len = dp.getLength();//发送方发送的数据的长度
InetAddress address = dp.getAddress();//发送方的IP地址对象
int port = dp.getPort();//发送方发送数据的端口号
System.out.println(new String(data, 0, len))//解析接受到的数据为字符串

//4.释放资源
ds.close();
Lobster AIjava运行12345678910111213141516171819

3.3 UDP的三种通信方式

3.3.1 单播

一个主机和一个主机进行通信。

以前的代码就是单播。

3.3.2 组播

一个主机同时和局域网内一组主机通信。

组播地址:224.0.0.0239.255.255.255,其中,224.0.0.0224.0.0.255为预留的组播地址。

代码实现

  • 发送端
    1. 创建发送端的MulticastSocket对象
    2. 创建数据,并把数据打包(DatagramPacket)
    3. 调用MulticastSocket对象的方法发送数据
    4. 释放资源
  • 接收端(创建多个接收端,这些接收端绑定同一个组播地址)
    1. 创建接收端MulticastSocket对象
    2. 把当前计算机绑定到组播地址中
    3. 创建DatagramPacket数据包对象
    4. 接收数据
    5. 解析数据包
    6. 释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
发送端

//创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket();

// 创建DatagramPacket对象
String s = "你好,你好!";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("224.0.0.1");
int port = 10000;
DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, address, port);

// 调用MulticastSocket发送数据方法发送数据
ms.send(datagramPacket);

// 释放资源
ms.close();

接收端(这里只写一个,其他类似)

//1. 创建MulticastSocket对象
MulticastSocket ms = new MulticastSocket(10000);

//2. 将将当前本机,添加到224.0.0.1的这一组当中
InetAddress address = InetAddress.getByName("224.0.0.1");
ms.joinGroup(address);

//3. 创建DatagramPacket数据包对象
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);

//4. 接收数据
ms.receive(dp);

//5. 解析数据
byte[] data = dp.getData();
int len = dp.getLength();
String ip = dp.getAddress().getHostAddress();
String name = dp.getAddress().getHostName();

System.out.println("ip为:"+ip +",主机名为:"+name +"的人,发送了数据:"+new String(data,0,len));

//6. 释放资源
ms.close();
Lobster AIjava运行1234567891011121314151617181920212223242526272829303132333435363738394041424344
3.3.3 广播

一个主机同时和局域网内所有主机通信。

广播地址:255.255.255.255

代码实现

和组播一样,只是组播地址变成了广播地址,而且不需要把接收端计算机绑定到组播地址中:

  • 发送端
    1. 创建发送端Socket对象(DatagramSocket)
    2. 创建存储数据的箱子,将广播地址封装进去
    3. 发送数据
    4. 释放资源
  • 接收端
    1. 创建接收端的Socket对象(DatagramSocket)
    2. 创建一个数据包,用于接收数据
    3. 调用DatagramSocket对象的方法接收数据
    4. 解析数据包,并把数据在控制台显示
    5. 关闭接收端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 发送端
public class ClientDemo {
public static void main(String[] args) throws IOException {
// 1. 创建发送端Socket对象(DatagramSocket)
DatagramSocket ds = new DatagramSocket();
// 2. 创建存储数据的箱子,将广播地址封装进去
String s = "广播 hello";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("255.255.255.255");
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
// 3. 发送数据
ds.send(dp);
// 4. 释放资源
ds.close();
}
}

// 接收端
public class ServerDemo {
public static void main(String[] args) throws IOException {
// 1. 创建接收端的Socket对象(DatagramSocket)
DatagramSocket ds = new DatagramSocket(10000);
// 2. 创建一个数据包,用于接收数据
DatagramPacket dp = new DatagramPacket(new byte[1024],1024);
// 3. 调用DatagramSocket对象的方法接收数据
ds.receive(dp);
// 4. 解析数据包,并把数据在控制台显示
byte[] data = dp.getData();
int length = dp.getLength();
System.out.println(new String(data,0,length));
// 5. 关闭接收端
ds.close();
}
}
Lobster AIjava运行1234567891011121314151617181920212223242526272829303132333435

4.TCP通信程序

TCP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket对象,通信前要确保连接已经建立,通过Socket产生IO流来进行网络通信。

4.1 客户端(发送数据)

4.1.1 构造方法
方法名 说明
Socket(InetAddress address,int port) 创建流套接字并将其连接到指定IP指定端口号
Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号
4.1.2 成员方法
方法名 说明
OutputStream getOutputStream() 返回此套接字的输出流
void shutdownOutput() 禁用此套接字的输出流,连接通道仍然存在

如果调用shutdownInput方法后继续在套接字输出流上写入内容,则该流将抛出 IOException异常。

4.1.3 代码实现
  1. 创建客户端的Socket对象与指定服务器相连接
    • 在创建对象的同时会连接服务端,如果连接不上会出现异常。
  2. 获取输出流,写数据
  3. 释放资源
1
2
3
4
5
6
7
8
9
10
11
12
//1.创建Socket对象
Socket socket = new Socket("127.0.0.1",10000);

//2.可以从连接通道中获取输出流
OutputStream os = socket.getOutputStream();
//写出数据
os.write("aaa".getBytes());

//3.释放资源
os.close();
socket.close();
Lobster AIjava运行1234567891011

4.2 服务器(接收数据)

4.2.1 构造方法
方法名 说明
ServletSocket(int port) 创建绑定到指定端口的服务器套接字
4.2.2 成员方法
方法名 说明
Socket accept() 监听要连接到此的套接字并接受它
InputStream getInputStream() 返回此套接字的输入流
void shutdownInput() 将此套接字的输入流置于“流的末尾”,连接通道仍然存在

如果调用shutdownInput方法后继续从套接字输入流读取内容,则流将返回 EOF(文件结束符)。

4.2.3 代码实现
  1. 创建服务器端的Socket对象(ServerSocket)
  2. 监听客户端连接,返回一个Socket的对象
    • accept方法是阻塞的,作用就是等待客户端连接。
  3. 获取输入流,读取数据
    • read方法也是阻塞的。
  4. 释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1.创建对象ServerSocker
ServerSocket ss = new ServerSocket(10000);

//2.监听客户端的链接
Socket socket = ss.accept();

//3.从连接通道中获取输入流读取数据
InputStream is = socket.getInputStream();
int b;
while ((b = is.read()) != -1){
System.out.println((char) b);
}

//4.释放资源
socket.close();
ss.close();
Lobster AIjava运行12345678910111213141516

4.3 细节

  1. 必须先执行服务端再执行客户端,否则客户端会因连接不上服务器而出现异常。
  2. 客户端创建对象并连接服务器,此时是通过三次握手协议,保证跟服务器之间的连接。

image-20250602173130783

  1. 客户端在关流的时候,还多了一个往服务器写结束标记的动作。
  2. 最后一步断开连接,通过四次挥手协议保证连接终止。

image-20250602173230060

5.实用案例

5.1 多发多收

  • 客户端:多次发送数据
  • 服务器:接收多次接收数据,并打印
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//客户端:多次发送数据
public class Client {
public static void main(String[] args) throws IOException {
//1. 创建Socket对象并连接服务端
Socket socket = new Socket("127.0.0.1",10000);

//2.写出数据
Scanner sc = new Scanner(System.in);
OutputStream os = socket.getOutputStream();

while (true) {
System.out.println("请输入您要发送的信息");
String str = sc.nextLine();
if("886".equals(str)){
break;
}
os.write(str.getBytes());
}
//3.释放资源
socket.close();
}
}

//服务端:接收多次接收数据,并打印
public class Server {
public static void main(String[] args) throws IOException {
//1.创建对象绑定10000端口
ServerSocket ss = new ServerSocket(10000);

//2.等待客户端来连接
Socket socket = ss.accept();

//3.读取数据
InputStreamReader isr = new InputStreamReader(socket.getInputStream());
int b;
while ((b = isr.read()) != -1){
System.out.print((char)b);
}

//4.释放资源
socket.close();
ss.close();
}
}
Lobster AIjava运行1234567891011121314151617181920212223242526272829303132333435363738394041424344

5.2 接收并反馈

  • 客户端:发送数据,接受服务器反馈
  • 服务器:收到消息后给出反馈

思路分析:

  • 客户端创建对象,使用输出流输出数据
  • 服务端创建对象,使用输入流接受数据
  • 服务端使用输出流给出反馈数据
  • 客户端使用输入流接受反馈数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 客户端
public class ClientDemo {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",10000);

OutputStream os = socket.getOutputStream();
os.write("hello".getBytes());
// os.close();如果在这里关流,会导致整个socket都无法使用
socket.shutdownOutput();//仅仅关闭输出流.并写一个结束标记,对socket没有任何影响

BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
while((line = br.readLine())!=null){
System.out.println(line);
}
br.close();
os.close();
socket.close();
}
}
// 服务器
public class ServerDemo {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10000);

Socket accept = ss.accept();

InputStream is = accept.getInputStream();
int b;
while((b = is.read())!=-1){
System.out.println((char) b);
}

System.out.println("看看我执行了吗?");

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(accept.getOutputStream()));
bw.write("你谁啊?");
bw.newLine();
bw.flush();

bw.close();
is.close();
accept.close();
ss.close();
}
}
Lobster AIjava运行12345678910111213141516171819202122232425262728293031323334353637383940414243444546

5.3 文件上传

UUID

Java提供的一个类,可以生成一个唯一的字符串,由于上传的文件名可能重复,所以服务端使用UUID生成一个唯一的文件名上传。

  • 客户端:将本地文件上传到服务器。接收服务器的反馈。
  • 服务器:接收客户端上传的文件,上传完毕之后给出反馈。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//客户端:将本地文件上传到服务器。接收服务器的反馈
public class Client {
public static void main(String[] args) throws IOException {
//客户端:将本地文件上传到服务器。接收服务器的反馈。
//服务器:接收客户端上传的文件,上传完毕之后给出反馈。


//1. 创建Socket对象,并连接服务器
Socket socket = new Socket("127.0.0.1",10000);

//2.读取本地文件中的数据,并写到服务器当中
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("mysocketnet\\clientdir\\a.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}

//往服务器写出结束标记
socket.shutdownOutput();


//3.接收服务器的回写数据
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = br.readLine();
System.out.println(line);


//4.释放资源
socket.close();

}
}

//服务器:接收客户端上传的文件,上传完毕之后给出反馈
public class Server {
public static void main(String[] args) throws IOException {
//客户端:将本地文件上传到服务器。接收服务器的反馈。
//服务器:接收客户端上传的文件,上传完毕之后给出反馈。


//1.创建对象并绑定端口
ServerSocket ss = new ServerSocket(10000);

//2.等待客户端来连接
Socket socket = ss.accept();

//3.读取数据并保存到本地文件中
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
String name = UUID.randomUUID().toString().replace("-", "");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("mysocketnet\\serverdir\\" + name + ".jpg"));
int len;
byte[] bytes = new byte[1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.close();
//4.回写数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("上传成功");
bw.newLine();
bw.flush();

//5.释放资源
socket.close();
ss.close();
}
}
Lobster AIjava运行123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869

5.4 网络编程和多线程结合使用

对于文件上传,服务器只能处理一个客户端请求,接收完一个图片之后,服务器就关闭了。

优化方案一:

使用循环

弊端:

第一个用户正在上传数据,第二个用户就来访问了,此时第二个用户是无法成功上传的。

所以,使用多线程改进

优化方案二:

每来一个用户,就开启多线程处理。

下面还结合了线程池的思想进行了优化,得到最终代码:

  • 客户端:将本地文件上传到服务器。接收服务器的反馈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Client {
public static void main(String[] args) throws IOException {
//1. 创建Socket对象,并连接服务器
Socket socket = new Socket("127.0.0.1",10000);

//2.读取本地文件中的数据,并写到服务器当中
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("mysocketnet\\clientdir\\a.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1){
bos.write(bytes,0,len);
}

//往服务器写出结束标记
socket.shutdownOutput();

//3.接收服务器的回写数据
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = br.readLine();
System.out.println(line);

//4.释放资源
socket.close();
}
}
Lobster AIjava运行1234567891011121314151617181920212223242526
  • 服务器:接收客户端上传的文件,上传完毕之后给出反馈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Server {
public static void main(String[] args) throws IOException {
//创建线程池对象
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量
16,//线程池总大小
60,//空闲时间
TimeUnit.SECONDS,//空闲时间(单位)
new ArrayBlockingQueue<>(2),//队列
Executors.defaultThreadFactory(),//线程工厂,让线程池如何创建线程对象
new ThreadPoolExecutor.AbortPolicy()//阻塞队列
);

//1.创建对象并绑定端口
ServerSocket ss = new ServerSocket(10000);

while (true) {
//2.等待客户端来连接
Socket socket = ss.accept();
//开启一条线程
//一个用户就对应服务端的一条线程
//new Thread(new MyRunnable(socket)).start();
pool.submit(new MyRunnable(socket));
}
}
}
Lobster AIjava运行1234567891011121314151617181920212223242526
  • 创建线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class MyRunnable implements Runnable{

Socket socket;

public MyRunnable(Socket socket){
this.socket = socket;
}

@Override
public void run() {
try {
//3.读取数据并保存到本地文件中
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
String name = UUID.randomUUID().toString().replace("-", "");
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("mysocketnet\\serverdir\\" + name + ".jpg"));
int len;
byte[] bytes = new byte[1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bos.close();
//4.回写数据
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bw.write("上传成功");
bw.newLine();
bw.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
//5.释放资源
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Lobster AIjava运行12345678910111213141516171819202122232425262728293031323334353637383940

二十一、反射

反射是允许对成员变量、成员方法和构造方法的信息进行编程访问。可以把它理解成外挂,通过它可以跳过权限修饰符直接修改原对象的信息,所以Java中有一个专有名词:暴力反射。

1.获取字节码文件对象

字节码文件:Java中的类经过编译后可以得到一个class后缀的文件,这个在HelloWorld案例中就可以看到。由于idea会自动帮我们编译,所以开发中感受不到class文件的存在。

字节码文件对象是属于类Class的对象,有三种获取方式:

  1. 源代码阶段:编写Java代码的阶段,程序员使用文本编辑器或集成开发环境(IDE)编写Java源代码文件
    • 使用Class.forName("类的全类名")
  2. 加载阶段:当Java程序运行时,JVM(Java虚拟机)需要将字节码文件加载到内存中
    • 使用类名.class
  3. 运行阶段:程序实际执行的阶段,JVM解释或编译执行字节码
    • 使用对象.getClass()
    • 同一个类的不同对象获取到的是同一个字节码文件对象。

2.获取构造方法

类的构造方法对象是属于Constructor类的对象:

Class类中的方法 说明
Constructor<?>[] getConstructors() 获得所有公共构造方法对象
Constructor<?>[] getDeclaredConstructors() 获得所有的构造方法对象
Constructor<T> getConstructor(Class<?>… parameterTypes) 获取单个公共构造方法对象
Constructor<T> getDeclaredConstructor(Class<?>… parameterTypes) 获取单个构造方法对象

Constructor类中用于创建对象的方法:

方法名 说明
T newInstance(Object… initargs) 根据指定的构造方法创建对象
setAccessible(boolean flag) 设置为true表示取消访问检查
int getModifiers() 获取此构造方法的权限修饰符

对于私有构造方法,如果要强行创建对象,必须先用setAccessible方法取消访问检查

3.获取成员变量

类的成员变量对象是属于Field类的对象:

Class类中的方法 说明
Field[] getFields() 返回所有公共成员变量对象的数组
Field[] getDeclaredFields() 返回所有成员变量对象的数组
Field getField(String name) 返回单个公共成员变量对象
Field getDeclaredField(String name) 返回单个成员变量对象

Field类中用于创建对象的方法:

方法名 说明
void set(Object obj, Object value) 给指定成员变量赋值
Object get(Object obj) 获取指定成员变量的值
String getName() 获取指定成员变量的变量名

4.获取成员方法

类的成员方法对象是属于Method类的对象:

方法名 说明
Method[] getMethods() 返回所有公共成员方法对象的数组
Method[] getDeclaredMethods() 返回所有成员方法对象的数组
Method getMethod(String name, Class<?>… parameterTypes) 返回单个公共成员方法对象
Method getDeclaredMethod(String name, Class<?>… parameterTypes) 返回单个成员方法对象

Method类中用于调用成员方法的方法:

Object invoke(Object obj, Object...args):运行方法

  • 参数一:用obj对象调用该方法
  • 参数二:调用方法的传递参数(如果没有就不写)
  • 返回值:方法的返回值(如果没有就不接收)

5.使用案例

1.反射和配置文件结合动态获取

需求: 利用反射根据文件中的不同类名和方法名,创建不同的对象并调用方法。

分析:

①通过Properties加载配置文件

②得到类名和方法名

③通过类名反射得到Class对象

④通过Class对象创建一个对象

⑤通过Class对象得到方法

⑥调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ReflectDemo9 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//1.读取配置文件的信息
Properties prop = new Properties();
FileInputStream fis = new FileInputStream("day14-code\\prop.properties");
prop.load(fis);
fis.close();
System.out.println(prop);

String classname = prop.get("classname") + "";
String methodname = prop.get("methodname") + "";

//2.获取字节码文件对象
Class clazz = Class.forName(classname);

//3.要先创建这个类的对象
Constructor con = clazz.getDeclaredConstructor();
con.setAccessible(true);
Object o = con.newInstance();
System.out.println(o);

//4.获取方法的对象
Method method = clazz.getDeclaredMethod(methodname);
method.setAccessible(true);

//5.运行方法
method.invoke(o);


}
}

配置文件中的信息:
classname=com.itheima.a02reflectdemo1.Student
methodname=sleep

2.利用反射保存对象中的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class MyReflectDemo {
public static void main(String[] args) throws IllegalAccessException, IOException {
/*
对于任意一个对象,都可以把对象所有的字段名和值,保存到文件中去
*/
Student s = new Student("小A",23,'女',167.5,"睡觉");
Teacher t = new Teacher("播妞",10000);
saveObject(s);
}

//把对象里面所有的成员变量名和值保存到本地文件中
public static void saveObject(Object obj) throws IllegalAccessException, IOException {
//1.获取字节码文件的对象
Class clazz = obj.getClass();
//2. 创建IO流
BufferedWriter bw = new BufferedWriter(new FileWriter("myreflect\\a.txt"));
//3. 获取所有的成员变量
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
//获取成员变量的名字
String name = field.getName();
//获取成员变量的值
Object value = field.get(obj);
//写出数据
bw.write(name + "=" + value);
bw.newLine();
}
bw.close();
}
}

public class Student {
private String name;
private int age;
private char gender;
private double height;
private String hobby;

...

public String toString() {
return "Student{name = " + name + ", age = " + age + ", gender = " + gender + ", height = " + height + ", hobby = " + hobby + "}";
}
}

public class Teacher {
private String name;
private double salary;

...

public String toString() {
return "Teacher{name = " + name + ", salary = " + salary + "}";
}
}

二十二、动态代理

动态代理可以无无侵入式的给方法增强功能,即如果想要给项目增加功能,通过动态代理可以在不动项目源码的情况下实现增加功能。

代理需要通过接口保证代理的样子,后面的对象和代理需要同时实现同一个接口,接口中就是被代理的所有方法。

1.动态代理三要素

  1. 真正干活的对象
  2. 代理对象
  3. 利用代理调用方法

java.lang.refect.Proxy类:提供了为对象产生代理的方法:

1
2
3
4
5
6
7
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

参数一:用于指定用哪个类加载器,去加载生成的代理类。

参数二:用于指定接口,这些接口用于指定生成什么样的代理,也就是有哪些方法。

参数三:用来指定生成的代理对象要干什么事情。

2.代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
明星:要代理的对象

public class BigStar implements Star {
private String name;

public BigStar() {
}

public BigStar(String name) {
this.name = name;
}

//唱歌
@Override
public String sing(String name){
System.out.println(this.name + "正在唱" + name);
return "谢谢";
}

//跳舞
@Override
public void dance(){
System.out.println(this.name + "正在跳舞");
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

public String toString() {
return "BigStar{name = " + name + "}";
}
}

中间接口

public interface Star {
//把所有想要被代理的方法定义在接口当中
//唱歌
public abstract String sing(String name);
//跳舞
public abstract void dance();
}

代理类:用于生成明星的代理人对象

public class ProxyUtil {
//方法作用:根据传递的明星实例为其创建一个代理
//形参:被代理的明星
//返回值:给经纪人创建的代理人对象
public static Star createProxy(BigStar bigStar){
Star star = (Star) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),//参数一:用于指定用哪个类加载器,去加载生成的代理类
new Class[]{Star.class},//参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法;接口可以有 多个,多个接口间用逗号分隔,表示代理多个明星
//参数三:用来指定生成的代理对象要干什么事情
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
* 参数一:代理的对象
* 参数二:要运行的方法 sing
* 参数三:调用sing方法时,传递的实参
* */
if("sing".equals(method.getName())){
System.out.println("准备话筒,收钱");
}else if("dance".equals(method.getName())){
System.out.println("准备场地,收钱");
}
//去找大明星开始唱歌或者跳舞
//代码的表现形式:调用大明星里面唱歌或者跳舞的方法
return method.invoke(bigStar,args);
}
}
);
return star;
}
}

测试类

public class Test {
public static void main(String[] args) {
/*
需求:
外面的人想要大明星唱一首歌
1. 获取代理的对象
代理对象 = ProxyUtil.createProxy(大明星的对象);
2. 再调用代理的唱歌方法
代理对象.唱歌的方法("只因你太美");
*/
//1. 获取代理的对象
BigStar bigStar = new BigStar("鸡哥");
Star proxy = ProxyUtil.createProxy(bigStar);

//2. 调用唱歌的方法
String result = proxy.sing("只因你太美");
System.out.println(result);
}
}

动态代理添加拦截方法

动态代理还可以设置拦截方法,实现方式是在代理类重写的invoke里添加拦截条件,当拦截条件生效,返回null。

比如,在这个故事中,经济人作为代理,如果别人让邀请大明星去唱歌,打篮球,经纪人就增强功能。但是如果别人让大明星去扫厕所,经纪人就要拦截,不会去调用大明星的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ProxyUtil {
public static Star createProxy(BigStar bigStar){
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
Star star = (Star) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{Star.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("cleanWC".equals(method.getName())){
System.out.println("拦截,不调用大明星的方法");
return null;
}
//如果是其他方法,正常执行 这是反射调用成员方法的句子
return method.invoke(bigStar,args);
}
}
);
return star;
}
}