JAVA-数据结构与算法

1.数据结构与算法

1.1 数据结构的定义

1
数据结构=数据+算法

1.1.1 数据与信息

  • 数据:指的是一种未处理的原始文字,数字,符号、图形

  • 信息:当数据 –处理【算法+数据结构】–> (特定的方式系统地处理、归纳甚至进行分析)

1.1.2 数据的分类

  • 1.按照计算机中所存储和使用的对象:

​ 1.1 数值数据: 如0,1,2…,9所组成,可用运算符来进行运算的数据
​ 1.2 字符数据: 如A,B,C,…,+,*等非数值型数据

  • 2.按照数据在计算机程序设计语言中的存在层次:
2.1 **基本数据类型/标量数据类型**:  不能以其他类型来定义的数据类型。【Java中所有基本数据类型】
2.2 **结构数据类型/虚拟数据类型**:  【字符串,数组,指针,列表,文件等】
2.3 **抽象数据类型**: 可以将一种数据类型看成是一种值的集合,以及在这些值上所进行的运算和其所代表的属性所成的集合【堆栈等】

1.2 算法

1
可执行程序=数据结构+算法【最关键的因素】

1.2.1 算法的定义

1
为了解决某项工作/某个问题,所需要有限数量的机械性/重复性指令与计算步骤
  • 算法的五个条件:

    算法的特性 内容与说明
    输入 0-n个输入,这些输入必须有清楚的描述或定义
    输出 1-n个输出【至少有一个输出结果】
    明确性 每个指令/步骤必须是简洁明确的
    有限性 在有限步骤后一定会结束【不存在无限循环】
    有效性 步骤清楚+可行【能让用户】
  • 算法的分类:

    描述工具 具体描述 举例
    伪语言 不能直接放入计算机执行的语言,一般需要一种特定的预处理器/人工编写转换成真正的计算机语言 sparks,pascal-like
    表格/图形 清晰明了的描述过程 数组,树形图,矩阵图
    流程图 图形符号表示法 流程图
    程序设计语言 可读性高的高级语言 Java,python,c#

1.3 算法性能分析

1.3.1 时间复杂度

1
2
3
1.一种以概量方式来衡量运行时间
2. 在一个完全理想状态下的计算机中,我们定义T(n)来表示程序执行所要花费的时间
3. Big-oh:程序的运行时间/最大运行时间是时间复杂度的衡量标准

1.3.2 空间复杂度

1
2
3
4
5
1.一种以概量方式来衡量所需的内存空间

1.固定空间内存:基本程序代码,常量,变量
2.变动空间内存:随程序进行时而改变大小的使用空间,例如引用类型变量

1.3.3 Big-oh(衡量时间复杂度)

1
2
常见的算法时间复杂度:
O(1) < O(log2N) < O(N) < O(Nlog2N) < O(N^2) < O(N^3) <O(2^N) <O(N!)

image-20231108144058821

1.4 常见算法

1.4.1 分治法

1.4.2 递归法

1.4.3 贪心法

1.4.4 动态规划法

1.4.5 迭代法

1.4.6 枚举法

1.5程序设计简介

1.5.1 程序开发流程

  • 四项原则

    1
    2
    3
    4
    1.可读性高:阅读和理解都相当容易
    2.平均成本低:成本考虑不局限于编码的成本,还包括执行,编译,维护,学习,调试和日后更新等成本
    3.可靠性高:所编写出来的程序代码稳定性高,不容易出现边际错误
    4.可编写性高:对于针对需求所编写的程序相对于容易

2.数组结构

2.1 线性表

  • 线性表的定义:

​ 有序表可以是空集合/(a1,a2,a3,….an-1,an),存在唯一的第一个元素a1和存在唯一的最后一个元素an。除了最后一个元素都有后继者,除了第一个元素都有先行者。

  • 线性表的运算:

​ 1.计算线性表的长度n

2.取出线性表中的第i项元素来修改

​ 3.插入一个新元素到第i项,并将原来位置和后面元素都后移

​ 4.删除第i项元素

5.遍历读取线性表元素

6.替换第i项元素

​ 7.复制线性表

​ 8.合并线性表

  • 线性表的存储方式:
静态数据结构 动态数据结构
形式 密集表 链表
优点 使用连续分配的内存空间来存储有序表元素
设计简单,读取和修改表中任意元素时间固定O(1)
使用不连续的内存空间来存储
数据插入和删除都方便O(1)
O(n)内存分配是在程序执行时才分配,不需要事先声明,充分节省内存
缺点 删除和加入数据,需要挪动大量数据O(n) 不能随机读取,必须按顺序遍历找到数据O(n)

2.2 数组

2.2.1 一维数组

image-20231108151519477

2.2.2 二维数组

image-20231108151658260

2.3 矩阵

3.链表

  • 许多相同数据类型的数据项,按照特定顺序排列而成的线性表(2.1定义的)

3.1 动态分配内存

对比项目 动态配置 静态分配
内存分配 运行阶段 编译阶段
内存释放 程序结束前必须释放分配的内存空间,否则会内存”泄露” 不需要释放,程序结束时自动归还给系统
程序运行性能 比较低(因为所需内存要执行程序时候才分配) 比较高(程序编译阶段就分配好需要的内存容量)
指针遗失 可能会存在内存泄露(程序结束前不释放内存就指向新的内存空间,则原本指向的内存空间就无法被释放) 没有此问题

3.2 单向链表

3.2.1 单链表定义

image-20231108154542232

image-20231108154727896

3.2.2 建立单链表

image-20231108160130306

3.2.3 单链表删除节点

  • 1.删除链表的第一个节点

image-20231108161827402

  • 2.删除链表的中间节点

image-20231108161836010

  • 3.删除链表的最后一个节点

image-20231108161855648

3.2.4 单链表插入节点

  • 1.插入到第一个节点前面

image-20231108163419693

  • 2.插入到中间位置

image-20231108163433463

  • 3.插入到最后一个节点后面

image-20231108163456678

3.2.5 单链表反转

3.3 环形链表

3.3.1 环形链表定义

image-20231108164158939

3.3.2 环形链表插入新节点

  • 1.插入到第一节点前,成为新的链接头部

    image-20231108165636708

  • 2.插入到中间节点部分

    image-20231108165650399

3.3.3 环形链表删除节点

  • 1.删除第一个节点

image-20231108165658190

  • 2.删除中间节点部分

image-20231108165708563

3.3.4 环形链表串联

image-20231108165906135

3.4 双向链表

3.4.1 双向链表定义

image-20231108170323488

3.4.2 双向链表插入节点

  • 1.插入到双向链表的第一个节点前

image-20231108193519482

  • 2.插入到双向链表的末尾

  • 3.插入到双向链表的中间位置

image-20231108193541287

3.4.3 双向链表删除节点

  • 1.删除双向链表的第一个节点

image-20231108193552713

  • 2.删除双向链表的最后一个节点

  • 3.删除双向链表的中间位置

4.堆栈

4.1 堆栈

JAVA-反射

第17章_反射机制

本章专题与脉络

第3阶段:Java高级应用-第17章


1. 反射(Reflection)的概念

1.1 反射出现的背景

Java程序中,所有的对象都有两种类型:编译时类型运行时类型,而很多时候对象的编译时类型和运行时类型不一致

1
2
Object obj = new String("hello");   
obj.getClass();

例如:某些变量或形参的声明类型是Object类型,但是程序却需要调用该对象运行时类型的方法,该方法不是Object中的方法,那么如何解决呢?

解决这个问题,有两种方案:

1.方案1(向下转型):在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可以直接先使用instanceof运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量即可。

2.方案2(反射):编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。

1.2 反射概述

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间借助Reflection API取得任何类的内部信息,能直接操作任意对象的内部属性及方法

加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。

image-20220417161529285

从内存加载上看反射:

image-20220524101448625

1.3 Java反射机制研究及应用

Java反射机制提供的功能:

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时获取泛型信息
  • 在运行时调用任意一个对象的成员变量和方法
  • 在运行时处理注解
  • 生成动态代理

1.4 反射相关的主要API

1
2
3
4
5
java.lang.Class:代表一个类
java.lang.reflect.Method:代表类的方法
java.lang.reflect.Field:代表类的成员变量
java.lang.reflect.Constructor:代表类的构造器
… …

1.5 反射的优缺点

优点:

  • 提高了Java程序的灵活性和扩展性,降低了耦合性,提高自适应能力

  • 允许程序创建和控制任何类的对象,无需提前硬编码目标类

缺点:

  • 反射的性能较低

    • 反射机制主要应用在对灵活性和扩展性要求很高的系统框架上
  • 反射会模糊程序内部逻辑,可读性较差

2. 理解Class类并获取Class实例

要想解剖一个类,必须先要获取到该类的Class对象。而剖析一个类/用反射解决具体的问题就是使用相关API:

  • java.lang.Class
  • java.lang.reflect.*

所以,Class对象是反射的根源

2.1 理解Class

2.1.1 理论上

在Object类中定义了以下的方法,此方法将被所有子类继承

1
public final Class getClass()

以上的方法返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称。

image-20220417162559217

对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。
对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。

  • Class本身也是一个类
  • Class 对象只能由系统建立对象
  • 一个加载的类在 JVM 中只会有一个Class实例
  • 一个Class对象对应的是一个加载到JVM中的一个.class文件
  • 每个类的实例都会记得自己是由哪个 Class 实例所生成
  • 通过Class可以完整地得到一个类中的所有被加载的结构
  • Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象

2.1.2 内存结构上

image-20220514180100176

说明:字符串常量池在JDK6中,存储在方法区;
字符串常量池在JDK7及以后,存储在堆空间。

2.2 获取Class类的实例(四种方法)

2.2.1:要求编译期间已知类型

前提:若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高

实例:

1
Class clazz = String.class;

2.2.2:获取对象的运行时类型

前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象

实例:

1
Class clazz = "www.atguigu.com".getClass();

2.2.3:可以获取编译期间未知的类型

前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException

实例:

1
Class clazz = Class.forName("java.lang.String");

2.2.4:其他方式(不做要求)

前提:可以用系统类加载对象或自定义加载器对象加载指定路径下的类型

实例:

1
2
ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass("类的全类名");

再举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class GetClassObject {
@Test
public void test1() throws ClassNotFoundException {
//1.调用运行时类的静态属性:class
Class clazz1= User.class;
System.out.println(clazz1);

//2.调用运行时类的对象的方法: 对象.getClass()
User u1=new User();
Class clazz2=u1.getClass();

//3.调用Class的静态方法:forName(String className)
String className="code.User"; //全类名
Class<?> clazz3=Class.forName(className);

//4.使用类的加载器方式
Class clazz4=ClassLoader.getSystemClassLoader().loadClass("code.User");
}
}

2.3 哪些类型可以有Class对象

简言之,所有Java类型!

(1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
(2)interface:接口
(3)[]:数组
(4)enum:枚举
(5)annotation:注解@interface
(6)primitive type:基本数据类型
(7)void

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class c1 = Object.class;
Class c2 = Comparable.class;
Class c3 = String[].class;
Class c4 = int[][].class;
Class c5 = ElementType.class;
Class c6 = Override.class;
Class c7 = int.class;
Class c8 = void.class;
Class c9 = Class.class;
//测试是否是一个Class
int[] a = new int[10];
int[] b = new int[100];
Class c10 = a.getClass();
Class c11 = b.getClass();
// 只要元素类型与维度一样,就是同一个Class
System.out.println(c10 == c11);

2.4 Class类的常用方法

方法名 功能说明
static Class forName(String name) 返回指定类名name的Class 对象
Object newInstance() 调用缺省构造函数,返回该Class对象的一个实例
getName() 返回此Class对象表示的实体(类/接口/数组类/基本类型/void)名称
Class getSuperClass() 返回当前Class对象的父类的Class对象
Class [] getInterfaces() 获取当前Class对象的接口
ClassLoader getClassLoader() 返回该类的类加载器
Class getSuperclass() 返回表示此Class所表示的实体的超类的Class
Constructor[] getConstructors() 返回一个包含某些Constructor对象的数组
Field[] getDeclaredFields() 返回Field对象的一个数组
Method getMethod(String name,Class … paramTypes) 返回一个Method对象,此对象的形参类型为paramType

举例:

1
2
3
4
5
6
7
8
9
10
11
String str = "test4.Person";
Class clazz = Class.forName(str);

Object obj = clazz.newInstance();

Field field = clazz.getField("name");
field.set(obj, "Peter");
Object name = field.get(obj);
System.out.println(name);

//注:test4.Person是test4包下的Person类

3. 类的加载与ClassLoader的理解

3.1 类的生命周期

类在内存中完整的生命周期:加载–>使用–>卸载。其中加载过程又分为:装载、链接、初始化三个阶段。

image-20220417173459849

3.2 类的加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、链接、初始化三个步骤来对该类进行初始化。

如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。

image-20220417171411631

类的加载又分为三个阶段:

(1)装载(Loading)

将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成

(2)链接(Linking)

  • 验证Verify:确保加载的类信息符合JVM规范,例如:以cafebabe开头,没有安全方面的问题。

  • 准备Prepare:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。

  • 解析Resolve:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

(3)初始化(Initialization)

  • 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。

  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。

  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

3.3 类加载器(classloader)

image-20220417173647473

3.3.1 类加载器的作用

将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。

类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

image-20220417173616344

3.3.2 类加载器的分类(JDK8为例)

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)自定义类加载器(User-Defined ClassLoader)

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:

image-20220417173819953

(1)启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。获取它的对象时往往返回null
  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。
  • 并不继承自java.lang.ClassLoader,没有父加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。

(2)扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 继承于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

image-20220417174032702

(3)应用程序类加载器(系统类加载器,AppClassLoader)

  • java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 继承于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性 java.class.path 指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器。
  • 它是用户自定义类加载器的默认父加载器
  • 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器

(4)用户自定义类加载器(了解)

  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
  • 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。
  • 同时,自定义加载器能够实现应用隔离,例如 Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
  • 自定义类加载器通常需要继承于ClassLoader。

3.3.3 查看某个类的类加载器对象

(1)获取默认的系统类加载器

1
ClassLoader classloader = ClassLoader.getSystemClassLoader();

(2)查看某个类是哪个类加载器加载的

1
2
3
4
ClassLoader classloader = Class.forName("exer2.ClassloaderDemo").getClassLoader();

//如果是根加载器加载的类,则会得到null
ClassLoader classloader1 = Class.forName("java.lang.Object").getClassLoader();

(3)获取某个类加载器的父加载器

1
ClassLoader parentClassloader = classloader.getParent();

示例代码:

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
package com.atguigu.loader;

import org.junit.Test;

public class TestClassLoader {
@Test
public void test01(){
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("systemClassLoader = " + systemClassLoader);
}

@Test
public void test02()throws Exception{
ClassLoader c1 = String.class.getClassLoader();
System.out.println("加载String类的类加载器:" + c1);

ClassLoader c2 = Class.forName("sun.util.resources.cldr.zh.TimeZoneNames_zh").getClassLoader();
System.out.println("加载sun.util.resources.cldr.zh.TimeZoneNames_zh类的类加载器:" + c2);

ClassLoader c3 = TestClassLoader.class.getClassLoader();
System.out.println("加载当前类的类加载器:" + c3);
}

@Test
public void test03(){
ClassLoader c1 = TestClassLoader.class.getClassLoader();
System.out.println("加载当前类的类加载器c1=" + c1);

ClassLoader c2 = c1.getParent();
System.out.println("c1.parent = " + c2);

ClassLoader c3 = c2.getParent();
System.out.println("c2.parent = " + c3);

}
}

3.3.4 使用ClassLoader获取流

关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的指定文件的输入流

1
2
3
InputStream in = null;
in = this.getClass().getClassLoader().getResourceAsStream("exer2\\test.properties");
System.out.println(in);

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//需要掌握如下的代码
@Test
public void test5() throws IOException {
Properties pros = new Properties();
//方式1:此时默认的相对路径是当前的module
// FileInputStream is = new FileInputStream("info.properties");
// FileInputStream is = new FileInputStream("src//info1.properties");

//方式2:使用类的加载器
//此时默认的相对路径是当前module的src目录
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("info1.properties");


pros.load(is);

//获取配置文件中的信息
String name = pros.getProperty("name");
String password = pros.getProperty("password");
System.out.println("name = " + name + ", password = " + password);
}

4. 反射的基本应用

有了Class对象,能做什么?

4.1 应用1:创建运行时类的对象

这是反射机制应用最多的地方。创建运行时类的对象有两种方式:

方式1:直接调用Class对象的newInstance()方法

要 求: 1)类必须有一个无参数的构造器。2)类的构造器的访问权限需要足够。

方式2:通过获取构造器对象来进行实例化

方式一的步骤:

1)获取该类型的Class对象 2)调用Class对象的newInstance()方法创建对象

方式二的步骤:

1)通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类型的构造器
2)向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
3)通过Constructor实例化对象。

如果构造器的权限修饰符修饰的范围不可见,也可以调用setAccessible(true)

示例代码:

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
package com.atguigu.reflect;

import org.junit.Test;

import java.lang.reflect.Constructor;

public class TestCreateObject {
@Test
public void test1() throws Exception{
// AtGuiguClass obj = new AtGuiguClass();//编译期间无法创建

Class<?> clazz = Class.forName("com.atguigu.ext.demo.AtGuiguClass");
//clazz代表com.atguigu.ext.demo.AtGuiguClass类型
//clazz.newInstance()创建的就是AtGuiguClass的对象
Object obj = clazz.newInstance();
System.out.println(obj);
}

@Test
public void test2()throws Exception{
Class<?> clazz = Class.forName("com.atguigu.ext.demo.AtGuiguDemo");
//java.lang.InstantiationException: com.atguigu.ext.demo.AtGuiguDemo
//Caused by: java.lang.NoSuchMethodException: com.atguigu.ext.demo.AtGuiguDemo.<init>()
//即说明AtGuiguDemo没有无参构造,就没有无参实例初始化方法<init>
Object stu = clazz.newInstance();
System.out.println(stu);
}

@Test
public void test3()throws Exception{
//(1)获取Class对象
Class<?> clazz = Class.forName("com.atguigu.ext.demo.AtGuiguDemo");
/*
* 获取AtGuiguDemo类型中的有参构造
* 如果构造器有多个,我们通常是根据形参【类型】列表来获取指定的一个构造器的
* 例如:public AtGuiguDemo(String title, int num)
*/
//(2)获取构造器对象
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);

//(3)创建实例对象
// T newInstance(Object... initargs) 这个Object...是在创建对象时,给有参构造的实参列表
Object obj = constructor.newInstance("尚硅谷",2022);
System.out.println(obj);
}
}

4.2 应用2:获取运行时类的完整结构

可以获取:包、修饰符、类型名、父类(包括泛型父类)、父接口(包括泛型父接口)、成员(属性、构造器、方法)、注解(类上的、方法上的、属性上的)。

4.2.1 相关API

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
//1.实现的全部接口
public Class<?>[] getInterfaces()
//确定此对象所表示的类或接口实现的接口。

//2.所继承的父类
public Class<? Super T> getSuperclass()
//返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的 Class。

//3.全部的构造器
public Constructor<T>[] getConstructors()
//返回此 Class 对象所表示的类的所有public构造方法。
public Constructor<T>[] getDeclaredConstructors()
//返回此 Class 对象表示的类声明的所有构造方法。

//Constructor类中:
//取得修饰符:
public int getModifiers();
//取得方法名称:
public String getName();
//取得参数的类型:
public Class<?>[] getParameterTypes();

//4.全部的方法
public Method[] getDeclaredMethods()
//返回此Class对象所表示的类或接口的全部方法
public Method[] getMethods()
//返回此Class对象所表示的类或接口的public的方法

//Method类中:
public Class<?> getReturnType()
//取得全部的返回值
public Class<?>[] getParameterTypes()
//取得全部的参数
public int getModifiers()
//取得修饰符
public Class<?>[] getExceptionTypes()
//取得异常信息

//5.全部的Field
public Field[] getFields()
//返回此Class对象所表示的类或接口的public的Field。
public Field[] getDeclaredFields()
//返回此Class对象所表示的类或接口的全部Field。

//Field方法中:
public int getModifiers()
//以整数形式返回此Field的修饰符
public Class<?> getType()
//得到Field的属性类型
public String getName()
//返回Field的名称。

//6. Annotation相关
get Annotation(Class<T> annotationClass)
getDeclaredAnnotations()

//7.泛型相关
//获取父类泛型类型:
Type getGenericSuperclass()
//泛型类型:ParameterizedType
//获取实际的泛型类型参数数组:
getActualTypeArguments()

//8.类所在的包
Package getPackage()

4.2.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
package com.atguigu.java2;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import org.junit.Test;

import com.atguigu.java1.Person;

public class FieldTest {

@Test
public void test1(){

Class clazz = Person.class;
//getFields():获取到运行时类本身及其所有的父类中声明为public权限的属性
// Field[] fields = clazz.getFields();
//
// for(Field f : fields){
// System.out.println(f);
// }

//getDeclaredFields():获取当前运行时类中声明的所有属性
Field[] declaredFields = clazz.getDeclaredFields();
for(Field f : declaredFields){
System.out.println(f);
}
}

//权限修饰符 变量类型 变量名
@Test
public void test2(){
Class clazz = Person.class;
Field[] declaredFields = clazz.getDeclaredFields();
for(Field f : declaredFields){
//1.权限修饰符
/*
* 0x是十六进制
* PUBLIC = 0x00000001; 1 1
* PRIVATE = 0x00000002; 2 10
* PROTECTED = 0x00000004; 4 100
* STATIC = 0x00000008; 8 1000
* FINAL = 0x00000010; 16 10000
* ...
*
* 设计的理念,就是用二进制的某一位是1,来代表一种修饰符,整个二进制中只有一位是1,其余都是0
*
* mod = 17 0x00000011
* if ((mod & PUBLIC) != 0) 说明修饰符中有public
* if ((mod & FINAL) != 0) 说明修饰符中有final
*/
int modifier = f.getModifiers();
System.out.print(Modifier.toString(modifier) + "\t");

// //2.数据类型
Class type = f.getType();
System.out.print(type.getName() + "\t");
//
// //3.变量名
String fName = f.getName();
System.out.print(fName);
//
System.out.println();
}
}
}

4.2.3 获取所有的方法及相关细节

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
package com.atguigu.java2;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.junit.Test;

import com.atguigu.java1.Person;

public class MethodTest {

@Test
public void test1() {

Class clazz = Person.class;
// getMethods():获取到运行时类本身及其所有的父类中声明为public权限的方法
// Method[] methods = clazz.getMethods();
//
// for(Method m : methods){
// System.out.println(m);
// }

// getDeclaredMethods():获取当前运行时类中声明的所有方法
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
System.out.println(m);
}
//
}

// 注解信息
// 权限修饰符 返回值类型 方法名(形参类型1 参数1,形参类型2 参数2,...) throws 异常类型1,...{}
@Test
public void test2() {
Class clazz = Person.class;
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
// 1.获取方法声明的注解
Annotation[] annos = m.getAnnotations();
for (Annotation a : annos) {
System.out.println(a);
}

// 2.权限修饰符
System.out.print(Modifier.toString(m.getModifiers()) + "\t");

// 3.返回值类型
System.out.print(m.getReturnType().getName() + "\t");

// 4.方法名
System.out.print(m.getName());
System.out.print("(");
// 5.形参列表
Class[] parameterTypes = m.getParameterTypes();
if (!(parameterTypes == null && parameterTypes.length == 0)) {
for (int i = 0; i < parameterTypes.length; i++) {

if (i == parameterTypes.length - 1) {
System.out.print(parameterTypes[i].getName() + " args_" + i);
break;
}

System.out.print(parameterTypes[i].getName() + " args_" + i + ",");
}
}

System.out.print(")");

// 6.抛出的异常
Class[] exceptionTypes = m.getExceptionTypes();
if (exceptionTypes.length > 0) {
System.out.print("throws ");
for (int i = 0; i < exceptionTypes.length; i++) {
if (i == exceptionTypes.length - 1) {
System.out.print(exceptionTypes[i].getName());
break;
}
System.out.print(exceptionTypes[i].getName() + ",");
}
}
System.out.println();
}
}
}

4.2.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
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
package com.atguigu.java2;

import com.atguigu.java1.Person;
import org.junit.Test;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
* @author 尚硅谷-宋红康
* @create 2020 下午 2:47
*/
public class OtherTest {

/*
获取当前类中的所有的构造器
*/
@Test
public void test1(){
Class clazz = Person.class;
Constructor[] cons = clazz.getDeclaredConstructors();
for(Constructor c :cons){
System.out.println(c);
}
}
/*
获取运行时类的父类
*/
@Test
public void test2(){
Class clazz = Person.class;
Class superclass = clazz.getSuperclass();
System.out.println(superclass);//class com.atguigu.java1.Creature
}
/*
获取运行时类的所在的包
*/
@Test
public void test3(){
Class clazz = Person.class;
Package pack = clazz.getPackage();
System.out.println(pack);

}
/*
获取运行时类的注解
*/
@Test
public void test4(){
Class clazz = Person.class;
Annotation[] annos = clazz.getAnnotations();
for (Annotation anno : annos) {

System.out.println(anno);
}

}

/*
获取运行时类所实现的接口
*/
@Test
public void test5(){
Class clazz = Person.class;
Class[] interfaces = clazz.getInterfaces();
for (Class anInterface : interfaces) {

System.out.println(anInterface);
}

}
/*
获取运行时类的带泛型的父类
*/
@Test
public void test6(){
Class clazz = Person.class;
Type genericSuperclass = clazz.getGenericSuperclass();
System.out.println(genericSuperclass);//com.atguigu.java1.Creature<java.lang.String>
}
}

4.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
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
/* Type:
* (1)Class
* (2)ParameterizedType
* 例如:Father<String,Integer>
* ArrayList<String>
* (3)TypeVariable
* 例如:T,U,E,K,V
* (4)WildcardType
* 例如:
* ArrayList<?>
* ArrayList<? super 下限>
* ArrayList<? extends 上限>
* (5)GenericArrayType
* 例如:T[]
*
*/
public class TestGeneric {
public static void main(String[] args) {
//需求:在运行时,获取Son类型的泛型父类的泛型实参<String,Integer>

//(1)还是先获取Class对象
Class clazz = Son.class;//四种形式任意一种都可以

//(2)获取泛型父类
// Class sc = clazz.getSuperclass();
// System.out.println(sc);
/*
* getSuperclass()只能得到父类名,无法得到父类的泛型实参列表
*/
Type type = clazz.getGenericSuperclass();

// Father<String,Integer>属于ParameterizedType
ParameterizedType pt = (ParameterizedType) type;

//(3)获取泛型父类的泛型实参列表
Type[] typeArray = pt.getActualTypeArguments();
for (Type type2 : typeArray) {
System.out.println(type2);
}
}
}
//泛型形参:<T,U>
class Father<T,U>{

}
//泛型实参:<String,Integer>
class Son extends Father<String,Integer>{

}

4.2.6 获取内部类或外部类信息(选讲)

public Class<?>[] getClasses():返回所有公共内部类和内部接口。包括从超类继承的公共类和接口成员以及该类声明的公共类和接口成员。

public Class<?>[] getDeclaredClasses():返回 Class 对象的一个数组,这些对象反映声明为此 Class 对象所表示的类的成员的所有类和接口。包括该类所声明的公共、保护、默认(包)访问及私有类和接口,但不包括继承的类和接口。

public Class<?> getDeclaringClass():如果此 Class 对象所表示的类或接口是一个内部类或内部接口,则返回它的外部类或外部接口,否则返回null。

Class<?> getEnclosingClass() :返回某个内部类的外部类

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test5(){
Class<?> clazz = Map.class;
Class<?>[] inners = clazz.getDeclaredClasses();
for (Class<?> inner : inners) {
System.out.println(inner);
}

Class<?> ec = Map.Entry.class;
Class<?> outer = ec.getDeclaringClass();
System.out.println(outer);
}

4.2.7 小 结

  1. 在实际的操作中,取得类的信息的操作代码,并不会经常开发。

  2. 一定要熟悉java.lang.reflect包的作用,反射机制。

4.3 应用3:调用运行时类的指定结构

4.3.1 调用指定的属性

在反射机制中,可以直接通过Field类操作类中的属性,通过Field类提供的set()和get()方法就可以完成设置和取得属性内容的操作。

(1)获取该类型的Class对象

Class clazz = Class.forName(“包.类名”);

(2)获取属性对象

Field field = clazz.getDeclaredField(“属性名”);

(3)如果属性的权限修饰符不是public,那么需要设置属性可访问

field.setAccessible(true);

(4)创建实例对象:如果操作的是非静态属性,需要创建实例对象

Object obj = clazz.newInstance(); //有公共的无参构造

Object obj = 构造器对象.newInstance(实参…);//通过特定构造器对象创建实例对象

(4)设置指定对象obj上此Field的属性内容

field.set(obj,”属性值”);

如果操作静态变量,那么实例对象可以省略,用null表示

(5)取得指定对象obj上此Field的属性内容

Object value = field.get(obj);

如果操作静态变量,那么实例对象可以省略,用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
28
public class Student {
private int id;
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
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
package com.atguigu.reflect;

import java.lang.reflect.Field;

public class TestField {
public static void main(String[] args)throws Exception {
//1、获取Student的Class对象
Class clazz = Class.forName("com.atguigu.reflect.Student");

//2、获取属性对象,例如:id属性
Field idField = clazz.getDeclaredField("id");

//3、如果id是私有的等在当前类中不可访问access的,我们需要做如下操作
idField.setAccessible(true);

//4、创建实例对象,即,创建Student对象
Object stu = clazz.newInstance();

//5、获取属性值
/*
* 以前:int 变量= 学生对象.getId()
* 现在:Object id属性对象.get(学生对象)
*/
Object value = idField.get(stu);
System.out.println("id = "+ value);

//6、设置属性值
/*
* 以前:学生对象.setId(值)
* 现在:id属性对象.set(学生对象,值)
*/
idField.set(stu, 2);

value = idField.get(stu);
System.out.println("id = "+ value);
}
}

关于setAccessible方法的使用:

  • Method和Field、Constructor对象都有setAccessible()方法。
  • setAccessible启动和禁用访问安全检查的开关。
  • 参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。
    • 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。
    • 使得原本无法访问的私有成员也可以访问
  • 参数值为false则指示反射的对象应该实施Java语言访问检查。

4.3.2 调用指定的方法

image-20220417181700813

(1)获取该类型的Class对象

Class clazz = Class.forName(“包.类名”);

(2)获取方法对象

Method method = clazz.getDeclaredMethod(“方法名”,方法的形参类型列表);

(3)创建实例对象

Object obj = clazz.newInstance();

(4)调用方法

Object result = method.invoke(obj, 方法的实参值列表);

如果方法的权限修饰符修饰的范围不可见,也可以调用setAccessible(true)

如果方法是静态方法,实例对象也可以省略,用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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import org.junit.Test;
import java.lang.reflect.Method;
public class TestMethod {
@Test
public void test()throws Exception {
// 1、获取Student的Class对象
Class<?> clazz = Class.forName("com.atguigu.reflect.Student");

//2、获取方法对象
/*
* 在一个类中,唯一定位到一个方法,需要:(1)方法名(2)形参列表,因为方法可能重载
*
* 例如:void setName(String name)
*/
Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);

//3、创建实例对象
Object stu = clazz.newInstance();

//4、调用方法
/*
* 以前:学生对象.setName(值)
* 现在:方法对象.invoke(学生对象,值)
*/
Object setNameMethodReturnValue = setNameMethod.invoke(stu, "张三");

System.out.println("stu = " + stu);
//setName方法返回值类型void,没有返回值,所以setNameMethodReturnValue为null
System.out.println("setNameMethodReturnValue = " + setNameMethodReturnValue);

Method getNameMethod = clazz.getDeclaredMethod("getName");
Object getNameMethodReturnValue = getNameMethod.invoke(stu);
//getName方法返回值类型String,有返回值,getNameMethod.invoke的返回值就是getName方法的返回值
System.out.println("getNameMethodReturnValue = " + getNameMethodReturnValue);//张三
}

@Test
public void test02()throws Exception{
Class<?> clazz = Class.forName("com.atguigu.ext.demo.AtGuiguClass");
Method printInfoMethod = clazz.getMethod("printInfo", String.class);
//printInfo方法是静态方法
printInfoMethod.invoke(null,"尚硅谷");
}
}

4.3.3 练习

读取user.properties文件中的数据,通过反射完成User类对象的创建及对应方法的调用。

配置文件:user.properties

1
2
className:com.atguigu.bean.User
methodName:show

User.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.atguigu.bean;

/**
* @author 尚硅谷-宋红康
* @create 18:41
*/
public class User {
private String name;

public User() {
}

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

public void show(){
System.out.println("我是一个脉脉平台的用户");
}
}

ReflectTest.java文件:

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
package com.atguigu.java4;

import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Properties;

/**
* @author 尚硅谷-宋红康
* @create 18:43
*/
public class ReflectTest {
@Test
public void test() throws Exception {
//1.创建Properties对象
Properties pro = new Properties();

//2.加载配置文件,转换为一个集合
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
InputStream is = classLoader.getResourceAsStream("user.properties");
pro.load(is);

//3.获取配置文件中定义的数据
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");

//4.加载该类进内存
Class clazz = Class.forName(className);

//5.创建对象
Object instance = clazz.newInstance();

//6.获取方法对象
Method showMethod = clazz.getMethod(methodName);

//7.执行方法
showMethod.invoke(instance);
}
}

5. 应用4:读取注解信息

一个完整的注解应该包含三个部分:
(1)声明
(2)使用
(3)读取

5.1 声明自定义注解

1
2
3
4
5
6
7
import java.lang.annotation.*;
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
1
2
3
4
5
6
7
8
import java.lang.annotation.*;
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String columnName();
String columnType();
}
  • 自定义注解可以通过四个元注解@Retention,@Target,@Inherited,@Documented,分别说明它的声明周期,使用位置,是否被继承,是否被生成到API文档中。
  • Annotation 的成员在 Annotation 定义中以无参数有返回值的抽象方法的形式来声明,我们又称为配置参数。返回值类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组
  • 可以使用 default 关键字为抽象方法指定默认返回值
  • 如果定义的注解含有抽象方法,那么使用时必须指定返回值,除非它有默认值。格式是“方法名 = 返回值”,如果只有一个抽象方法需要赋值,且方法名为value,可以省略“value=”,所以如果注解只有一个抽象方法成员,建议使用方法名value。

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
@Table("t_stu")
public class Student {
@Column(columnName = "sid",columnType = "int")
private int id;
@Column(columnName = "sname",columnType = "varchar(20)")
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

5.3 读取和处理自定义注解

自定义注解必须配上注解的信息处理流程才有意义。

我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是RetentionPolicy.RUNTIME。

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
import java.lang.reflect.Field;
public class TestAnnotation {
public static void main(String[] args) {
Class studentClass = Student.class;
Table tableAnnotation = (Table) studentClass.getAnnotation(Table.class);
String tableName = "";
if(tableAnnotation != null){
tableName = tableAnnotation.value();
}

Field[] declaredFields = studentClass.getDeclaredFields();
String[] columns = new String[declaredFields.length];
int index = 0;
for (Field declaredField : declaredFields) {
Column column = declaredField.getAnnotation(Column.class);
if(column!= null) {
columns[index++] = column.columnName();
}
}

String sql = "select ";
for (int i=0; i<index; i++) {
sql += columns[i];
if(i<index-1){
sql += ",";
}
}
sql += " from " + tableName;
System.out.println("sql = " + sql);
}
}

6. 体会反射的动态性

体会1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ReflectionTest {
//体会反射的动态性:动态的创建给定字符串对应的类的对象
public <T> T getInstance(String className) throws Exception {
Class clazz = Class.forName(className);
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
return (T) constructor.newInstance();
}

@Test
public void test1() throws Exception {
String className = "com.atguigu.java1.Person";
Person p1 = getInstance(className);
System.out.println(p1);
}
}

体会2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ReflectionTest {
//体会反射的动态性:动态的创建指定字符串对应类的对象,并调用指定的方法
public Object invoke(String className,String methodName) throws Exception {
Class clazz = Class.forName(className);
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
//动态的创建指定字符串对应类的对象
Object obj = constructor.newInstance();
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
return method.invoke(obj);
}

@Test
public void test2() throws Exception {
String info = (String) invoke("com.atguigu.java1.Person", "show");

System.out.println("返回值为:" + info);

}
}

体会3:

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
public class ReflectionTest {
@Test
public void test1() throws Exception {
//1.加载配置文件,并获取指定的fruitName值
Properties pros = new Properties();
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("config.properties");
pros.load(is);
String fruitStr = pros.getProperty("fruitName");
//2.创建指定全类名对应类的实例
Class clazz = Class.forName(fruitStr);
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Fruit fruit = (Fruit) constructor.newInstance();
//3. 调用相关方法,进行测试
Juicer juicer = new Juicer();
juicer.run(fruit);
}
}

interface Fruit {
public void squeeze();
}

class Apple implements Fruit {
public void squeeze() {
System.out.println("榨出一杯苹果汁儿");
}
}

class Orange implements Fruit {
public void squeeze() {
System.out.println("榨出一杯桔子汁儿");
}
}

class Juicer {
public void run(Fruit f) {
f.squeeze();
}
}

其中,配置文件【config.properties】存放在当前Module的src下

1
com.atguigu.java1.Orange

计算机网络

1.计算机网络概述

1.1 网络的网络

网络把主机连接起来,而互连网(internet)是把多种不同的网络连接起来,因此互连网是网络的网络。而互联网(Internet)是全球范围的互连网

image-20231113202315139

1.2 主机之间的通信方式

1.2.1 客户-服务器(C/S)

image-20231113202618225

1.2.2 对等(P2P)

image-20231113202641140

1.3 电路交换和分组交换

  • 电路交换
    电路交换用于电话通信系统,两个用户要通信之前需要建立一条专用的物理链路,并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路,因此电路交换对线路的利用率很低,往往不到 10%

  • 分组交换

    每个分组都有首部和尾部,包含了源地址和目的地址等控制信息,在同一个传输线路上同时传输多个分组互相不会影响,因此在同一条传输线路上允许同时传输多个分组,也就是说分组交换不需要占用传输线路。

    在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。

1.4 四种时延

四种时延 定义 进一步解释
排队时延 分组在路由器的输入队列和输出队列中排队等待的时间 取决于网络当前的通信量
处理时延 主机/路由器收到分组时进行处理需要的时间 分析首部、从分组中提取数据、差错检验、查找适合的路由
传输时延 主机/路由器传输数据帧所需要的时间 image-20231113203459904,其中l代表数据帧长度,v代表传输速率
传播时延 电磁波在信道中传播所需要的时间 image-20231113203506221,其中l代表信道长度,v代表电磁波在信道上传播速度

1.5 计算机网络体系结构

image-20231113210802196

1.5.1 五层协议

  • 应用层 :为特定应用程序提供数据传输服务,例如 HTTP、DNS 等协议。数据单位为报文。

  • 传输层 :为进程提供通用数据传输服务。由于应用层协议很多,定义通用的传输层协议就可以支持不断增多的应用层协议。
    运输层包括两种协议:(TCP 主要提供完整性服务,UDP 主要提供及时性服务。)

    1.传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;
    2.用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。

  • 网络层 :为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成分组。

  • 数据链路层 :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。

  • 物理层 :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。

层名 传输的数据 作用 协议
应用层 报文 为特定应用程序提供数据传输服务 FTP(文件传输协议)
DNS(域名系统)
HTTP(超文本传输协议)
DHCP(动态主机配置协议)
TELNET(远程登录协议)
SMTP()
POP3()
IMAP()
传输层 TCP报文段
UDP用户数据包
为进程提供通用数据传输服务 TCP(传输控制协议)
UDP(用户数据报协议)
网络层 IP数据包 为主机提供数据传输服务 IP(网络互连协议)
ARP(地址解析协议)
ICMP(网际控制报文协议)
IGMP(网际组管理协议)
数据链路层 数据帧 为同一链路的主机提供数据传输服务 PPP协议
物理层 数据比特流 虑的是怎样在传输媒体上传输数据比特流

1.5.2 OSI

其中表示层和会话层用途如下:

  • 表示层 :数据压缩、加密以及数据描述,这使得应用程序不必关心在各台主机中数据内部格式不同的问题。
  • 会话层 :建立及管理会话。

五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。

1.5.3 TCP/IP

其中表示层和会话层用途如下:

  • 表示层 :数据压缩、加密以及数据描述,这使得应用程序不必关心在各台主机中数据内部格式不同的问题。
  • 会话层 :建立及管理会话。

五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理
image-20231113211635898

1.5.4 数据在各层的传递过程

在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。

路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要传输层和应用层。

image-20231113211742825

2.物理层

2.1 通信方式

根据信息在传输线上的传送方向,分为以下三种通信方式:

  • 单工通信:单向传输
  • 半双工通信:双向交替传输
  • 全双工通信:双向同时传输

2.2 带通调制

  • 带通调制:数字信号(离散的信号)–> 模拟信号(连续的信号)

image-20231113212021243

3.数据链路层

3.1 三大基本问题

3.1.1 封装成帧

将网络层传下来的分组添加首部和尾部(向下封装),用于标记帧的开始和结束

image-20231113212755618

3.1.2 透明传输

帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符。如果数据部分出现转义字符,那么就在转义字符前面再加个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在

image-20231114151432788

3.1.3 差错检测

目前数据链路层广泛使用循环冗余检验(CRC)来检查比特差错

image-20231114151524213

3.2 信道分类

形式 具体细节 控制方式
广播信道 一对多 一个节点发送的数据能够被广播信道上所有的节点接收到
【所以节点在同一个广播信道上发送数据,需要专门的控制方法进行协调,避免发生冲突】
1.信道复用技术
2.CSMA/CD协议
点对点信道 一对一 一个节点发送的数据只能被对应节点接收到
【不会发生碰撞,比较简单】
PPP协议

3.3 广播信道的两种控制方式

3.3.1 信道复用技术

定义 图形
频分复用 所有主机在相同的时间占用不同的频率带宽资源
时分复用 所有主机在不同的时间占用相同的频率带宽资源
统计时分复用 对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后发送 image-20231114112248695
波分复用 光的频分复用
【由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波】
image-20231114151752256
码分复用 当码分复用信道为多个不同地址的用户所共享时 image-20231114151740381

3.3.2 CSMA/CD协议

  • 多点接入 :说明这是总线型网络,许多主机以多点的方式连接到总线上。
  • 载波监听 :每个主机都必须不停地监听信道。在发送前,如果监听到信道正在使用,就必须等待。
  • 碰撞检测 :在发送中,如果监听到信道已有其它主机正在发送数据,就表示发生了碰撞。虽然每个主机在发送数据之前都已经监听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。

记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 争用期 。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。

当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 截断二进制指数退避算法 来确定。从离散的整数集合 {0, 1, .., (2k-1)} 中随机取出一个数,记作 r,然后取 r 倍的争用期作为重传等待时间。

image-20231114151947497

3.4 点对点信道的控制方式

3.4.1 PPP协议

互联网用户通常需要连接到某个 ISP 之后才能接入到互联网,PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议

image-20231114152043721

  • F 字段为帧的定界符
  • A 和 C 字段暂时没有意义
  • 信息部分的长度不超过 1500
  • FCS 字段是使用 CRC 的检验序列

image-20231114152610748

3.5 MAC地址

  1. MAC地址=链路层地址(一台主机拥有多少个网络适配器就有多少个 MAC 地址)
  2. MAC地址用于唯一标识网络适配器(网卡)
  3. MAC地址长度为6字节(48位)

3.6 局域网

  1. 局域网是一种典型的广播信道,主要特点是网络为一个单位所拥有,且地理范围和站点数目均有限
  2. 局域网技术分类:①以太网(主要市场) ②令牌环网 ③FDDI ④ATM
  3. 局域网按照网络拓扑结构分类:

image-20231114154248028

3.6.1 以太网

其中星型拓扑结构局域网—-以太网

​ 早期,使用集线器进行连接,集线器是一种物理层设备, 作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线器同时收到两个不同接口的帧,那么就发生了碰撞。

​ 目前,使用交换机替代了集线器,交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。

​ 以太网帧格式:

  • 类型 :标记上层使用的协议;

  • 数据 :长度在 46-1500 之间,如果太小则需要填充;

  • FCS :帧检验序列,使用的是 CRC 检验方法;

    image-20231114154944820

3.7 交换机

交换机学习交换表内容(存储MAC地址到接口的映射)

由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。

下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B,先查交换表,此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧,主机 B 回应该帧向主机 A 发送数据包时,交换机查找交换表得到主机 A 映射的接口为 1,就发送数据帧到接口 1,同时交换机添加主机 B 到接口 2 的映射

image-20231114155237140

3.8 虚拟局域网

虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。

例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。

使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊接口被设置为干线接口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网

image-20231114155531585

4.网络层

网络层是整个互联网的核心,因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大努力交互的数据报服务

使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络

image-20231114161253487

4.1 IP协议

4.1.1 IP数据包格式

image-20231114162301605

  • 版本 : 有 4(IPv4)和 6(IPv6)两个值;
  • 首部长度 : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为固定部分长度为 20 字节,因此该值最小为 5。如果可选字段的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。
  • 区分服务 : 用来获得更好的服务,一般情况下不使用。
  • 总长度 : 包括首部长度和数据部分长度。
  • 生存时间 :TTL,它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当 TTL 为 0 时就丢弃数据报。
  • 协议 :指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。
  • 首部检验和 :因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。
  • 标识 : 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。
  • 片偏移 : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节

4.1.2 IP地址编址方式

4.1.2.1 分类

由两部分组成,网络号和主机号,其中不同分类具有不同的网络号长度,并且是固定的。

IP 地址 ::= {< 网络号 >, < 主机号 >}

image-20231114162924758

4.1.2.2 子网划分

通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。

IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >}

使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000,也就是 255.255.192.0。

注意,外部网络看不到子网的存在。

4.1.2.3 不分类

无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。

IP 地址 ::= {< 网络前缀号 >, < 主机号 >}

CIDR 的记法:IP 地址+网络前缀长度,例如 128.14.35.7/20 表示前 20 位为网络前缀。

CIDR 的地址掩码:子网掩码,子网掩码首 1 长度为网络前缀的长度。

一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为 构成超网

在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配来确定应该匹配哪一个。

4.2 ARP 地址解析协议

网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。

image-20231114164147781

ARP 实现: IP 地址 –> MAC 地址

image-20231114164245388

每个主机都有一个 ARP 高速缓存(局域网上的各主机和路由器的 IP 地址到 MAC 地址)映射表。

如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。

image-20231114164438285

4.3 ICMP 网际控制报文协议

ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会
ICMP封装在 IP 数据报中,但是不属于高层协议

image-20231114164804741

ICMP 报文分为差错报告报文询问报文

image-20231114164842498

4.3.1 Ping

1.作用:用来测试两台主机之间的连通性

2.Ping 的原理:是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率

​ 具体步骤:

​ 1.Win+R快捷键输入cmd

image-20231114164926127

​ 2.输入ping ip地址

image-20231114165031993

4.3.2 Traceroute

1.作用:用来跟踪一个分组从源点到终点的路径

Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。

  • 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,当 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文;
  • 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。
  • 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。
  • 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。

4.4 虚拟专用网 VPN

由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数 << 本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)

有三个专用地址块:

  • 10.0.0.0 ~ 10.255.255.255
  • 172.16.0.0 ~ 172.31.255.255
  • 192.168.0.0 ~ 192.168.255.255

VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指好像是,而实际上并不是,它有经过公用的互联网。

image-20231114170015323

4.5 网络地址转换 NAT

专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP –> 全球 IP

在以前,NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把传输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT

image-20231114170139455

4.6 路由器

4.6.1 路由器的结构

功能划分 组成
路由选择 内部网关协议 RIP
内部网关协议 OSPF
外部网关协议 BGP
分组转发 交换结构
一组输入端口
一组输出端口

image-20231114170620304

4.6.2 路由选择-三大协议

路由选择协议都是自适应的,能随着网络通信量和拓扑结构的变化而自适应地进行调整。

互联网可以划分为许多较小的自治系统 AS,一个 AS 可以使用一种和别的 AS 不同的路由选择协议。

可以把路由选择协议划分为两大类:

  • 自治系统内部的路由选择:RIP 和 OSPF
  • 自治系统间的路由选择:BGP

4.6.2.1 内部网关协议 RIP

RIP 是一种基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1。跳数最多为 15,超过 15 表示不可达。

RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。

距离向量算法:

  • 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X,并把所有的距离字段加 1;
  • 对修改后的 RIP 报文中的每一个项目,进行以下步骤:
  • 若原来的路由表中没有目的网络 N,则把该项目添加到路由表中;
  • 否则:若下一跳路由器地址是 X,则把收到的项目替换原来路由表中的项目;否则:若收到的项目中的距离 d 小于路由表中的距离,则进行更新(例如原始路由表项为 Net2, 5, P,新表项为 Net2, 4, X,则更新);否则什么也不做。
  • 若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。

RIP 协议实现简单,开销小。但是 RIP 能使用的最大距离为 15,限制了网络的规模。并且当网络出现故障时,要经过比较长的时间才能将此消息传送到所有路由器。

4.6.2.2 内部网关协议 OSPF

开放最短路径优先 OSPF,是为了克服 RIP 的缺点而开发出来的。

开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法 SPF。

OSPF 具有以下特点:

  • 向本自治系统中的所有路由器发送信息,这种方法是洪泛法。
  • 发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距离、时延、带宽等来表示。
  • 只有当链路状态发生变化时,路由器才会发送信息。

所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIP,OSPF 的更新过程收敛的很快。

4.6.2.3 外部网关协议 BGP

BGP(Border Gateway Protocol,边界网关协议)

AS 之间的路由选择很困难,主要是由于:

  • 互联网规模很大;
  • 各个 AS 内部使用不同的路由选择协议,无法准确定义路径的度量;
  • AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。

BGP 只能寻找一条比较好的路由,而不是最佳路由。

每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。

image-20231114171053334

4.6.3 分组转发-具体流程

  • 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。
  • 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付;
  • 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器;
  • 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器;
  • 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器;
  • 报告转发分组出错

image-20231114170850874

5.传输层

网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。传输层提供了进程间的逻辑通信,传输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个传输层实体之间有一条端到端的逻辑通信信道。

5.1 TCP 传输控制协议

  • 传输控制协议 TCP(Transmission Control Protocol)是面向连接的提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。

  • TCP首部格式:

image-20231114172409619

5.2 UDP 用户数据报协议

  • 用户数据报协议 UDP(User Datagram Protocol)是无连接的尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。

  • UDP首部格式:

image-20231114172115587

5.3 TCP三次握手

image-20231114173125064

1
2
3
4
5
6
假设 A 为客户端,B 为服务器端
1.首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。
2.第一次握手: A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。
3.第二次握手: B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。
4.第三次握手: A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。
5.最终结果: B 收到 A 的确认后,连接建立。

第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接

客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。

5.4 TCP四次挥手

image-20231114194236979

1
2
3
4
5
6
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。
A 发送连接释放报文,FIN=1。
B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。
当 B 不再需要连接时,发送连接释放报文,FIN=1。
A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。
B 收到 A 的确认后释放连接。

第四次挥手是因为客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态(这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文)

其中TIME_WAIT状态:

客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:

  • 确保A给B发送的最后一个确认报文B收到了。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。
  • 为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。

5.5 TCP三大特性

5.5.1 可靠传输

TCP使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。

一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下:

image-20231114195423466

其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响。

超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下:

image-20231114195705243
其中 RTTd 为偏差的加权平均值。

5.5.1.1 TCP滑动窗口

窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。

发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。

接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。

image-20231114195654719

5.5.2 流量控制

流量控制是为了控制发送方发送速率保证接收方来得及接收

接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

5.5.3 拥塞控制

如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度

image-20231114200011897

TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复

  • 慢开始和拥塞避免:

image-20231114201842276

  • 快恢复:

其中,快恢复和慢开始的区别就是慢开始的时候cwnd变为1重新开始,而快恢复是直接到cwnd=ssthresh,直接进入拥塞避免阶段

image-20231114202529909

  • 快重传:

image-20231114202931293

6.应用层

6.1 DNS 域名系统

DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。

其中,域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。

image-20231114203419598

DNS 可以使用 UDP(大多数情况) / TCP 进行传输,使用的端口号都为 53。这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用 TCP 进行传输:

  • 如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。
  • 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。

6.2 FTP 文件传输协议

FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件:

  • 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。
  • 数据连接:用来传送一个文件数据。

根据数据连接是否是服务器端主动建立 –> FTP有主动模式和被动模式:

  • 主动模式:服务器端主动建立数据连接,– 服务器端的端口号为 20,客户端的端口号随机,但是必须大于 1024(因为 0~1023 是熟知端口号)

image-20231114204132043

  • 被动模式:客户端主动建立数据连接,其中客户端的端口号由客户端自己指定,服务器端的端口号随机。

image-20231114204258334

6.3 DHCP 动态主机配置协议

DHCP提供了即插即用的连网方式,用户不再需要手动配置 IP 地址(子网掩码,网关IP地址)

DHCP 工作过程如下:

  1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。
  2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。
  3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。
  4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。

image-20231114204556894

6.4 TELNET 远程登录协议

TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。

TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。

6.5 电子邮件协议

一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件协议。

三个部分 分类 举例
用户代理
邮件服务器
邮件协议 发送协议和读取协议 1.发送协议:SMTP
2.读取协议:POP3和IMAP

image-20231114205849947

6.5.1 SMTP

SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。

image-20231114210026119

6.5.2 POP3

只要用户从服务器上读取了邮件,就把该邮件删除

6.5.3 IMAP

IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件

7.常用端口

image-20231114210346161

8.Web页面请求过程

image-20231114210841984

  • 1.浏览器会对url进行解析:从而生成发送给web服务器的请求信息
  • 2.DNS域名解析出IP地址:本地DNS将该域名和对应的IP地址写入自身缓存,然后将解析的IP地址返回给客户端
  • 3.浏览器和服务器进行三次握手:建立连接
  • 4.浏览器向服务器端发生HTTP请求报文:第一步对 URL 进行解析之后,浏览器确定了 Web 服务器和文件名,接下来就是根据这些信息来生成 HTTP 请求消息了,此时则可以发送HTTP请求报文
  • 5.服务器端向浏览器发送HTTP响应报文和首页HTML文件:
  • 6.浏览器渲染HTML页面:
  • 7.浏览器和服务器进行四次挥手:断开连接

JAVA-网络编程

第16章_网络编程

本章专题与脉络

第3阶段:Java高级应用-第16章

1. 网络编程概述

Java是 Internet 上的语言,它从语言级上提供了对网络应用程序的支持,程序员能够很容易开发常见的网络应用程序。

Java提供的网络类库,可以实现无痛的网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由 JVM 控制。并且 Java 实现了一个跨平台的网络库,程序员面对的是一个统一的网络编程环境

1.1 软件架构

  • C/S架构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、美团app、360安全卫士等软件。

B/S架构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有IE、谷歌、火狐等。

两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序。

1.2 网络基础

  • 计算机网络:
    把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息、共享硬件、软件、数据信息等资源。

  • 网络编程的目的:直接/间接地通过网络协议与其它计算机实现数据交换,进行通讯。

  • 网络编程中有三个主要的问题:

    • 问题1:如何准确地定位网络上一台或多台主机(去看病如何找到你?) —使用IP地址(找到你)
    • 问题2:如何定位主机上的特定的应用(找到你之后如何判断是哪里有病?) —使用端口号(定位你身上特定的位置)
    • 问题3:找到主机后,如何可靠、高效地进行数据传输(确定病因如何用药?) —规范网络通信协议(可靠、高效地进行数据传输)

2. 网络通信要素

2.1 如何实现网络中的主机互相通信

  • 通信双方地址
    • IP
    • 端口号
  • 一定的规则:不同的硬件、操作系统之间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为协议,即网络通信协议。

生活类比:

image-20220503144420721 image-20220503144450241

2.2 通信要素一:IP地址和域名

2.2.1 IP地址

IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给网络中的一台计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。

IP地址分类方式一:

  • IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,以点分十进制表示,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数。

    image-20220503145929568
    • 这种方式最多可以表示42亿个。其中,30亿都在北美,亚洲4亿,中国2.9亿。2011年初已经用尽。

    • IP地址 = 网络地址 +主机地址

      • 网络地址:标识计算机或网络设备所在的网段
    • 主机地址:标识特定主机或网络设备

      20220518_101503

      其中,E类用于科研。

  • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。

    ​ 为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,共16个字节,写成8个无符号整数,每个整数用四个十六进制位表示,数之间用冒号(:)分开
    ​ 比如:ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,按保守方法估算IPv6实际可分配的地址,整个地球的每平方米面积上仍可 分配1000多个地址,这样就解决了网络地址资源数量不够的问题。2012年6月6日,国际互联网协会举行了世界IPv6启动纪念日,这一天,全球IPv6网络正式启动。多家知名网站,如Google、Facebook和Yahoo等,于当天全球标准时间0点(北京时间8点整)开始永久性支持IPv6访问。2018年6月,三大运营商联合阿里云宣布,将全面对外提供IPv6服务,并计划在2025年前助推中国互联网真正实现“IPv6 Only”。

    在IPv6的设计过程中除了一劳永逸地解决了地址短缺问题以外,还考虑了在IPv4中解决不好的其它问题,主要有端到端IP连接、服务质量(QoS)、安全性、多播、移动性、即插即用等。

IP地址分类方式二:

公网地址( 万维网使用)和 私有地址( 局域网使用)。192.168.开头的就是私有地址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用。

常用命令:

  • 查看本机IP地址,在控制台输入:
1
ipconfig
  • 检查网络是否连通,在控制台输入:
1
2
ping 空格 IP地址
ping 220.181.57.216

特殊的IP地址:

  • 本地回环地址(hostAddress):127.0.0.1
  • 主机名(hostName):localhost

2.2.2 域名

Internet上的主机有两种方式表示地址:

  • 域名(hostName):www.atguigu.com
  • IP 地址(hostAddress):202.108.35.210

域名解析:因为IP地址数字不便于记忆,因此出现了域名。域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS,Domain Name System,域名系统)负责将域名转化成IP地址,这样才能和主机建立连接。

简单理解:

image-20220415003729013

详细理解:

image-20220131125934581

  1. 在浏览器中输入www . qq .com 域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。
  2. 如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
  3. 如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/IP参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
  4. 如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。
  5. 如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至13台根DNS,根DNS服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(http://qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找(http://qq.com)域服务器,重复上面的动作,进行查询,直至找到www.qq.com主机。
  6. 如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管是本地DNS服务器用是是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机。

2.3 通信要素二:端口号

网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?

如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)。

不同的进程,设置不同的端口号。

  • 端口号:用两个字节表示的整数,它的取值范围是0~65535
    • 公认端口:0~1023。被预先定义的服务通信占用,如:HTTP(80),FTP(21),Telnet(23)
    • 注册端口:1024~49151。分配给用户进程或应用程序。如:Tomcat(8080),MySQL(3306),Oracle(1521)。
    • 动态/ 私有端口:49152~65535。

如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。

image-20220415004400166

2.4 通信要素三:网络通信协议

通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。

  • 网络通信协议:在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤、出错控制等做了统一规定,通信双方必须同时遵守才能完成数据交换。

新的问题:网络协议涉及内容太多、太复杂。如何解决?

计算机网络通信涉及内容很多,比如指定源地址和目标地址,加密解密,压缩解压缩,差错控制,流量控制,路由控制,如何实现如此复杂的网络协议呢?通信协议分层思想

在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一层,而与再下一层不发生关系。各层互不影响,利于系统的开发和扩展。

这里有两套参考模型

  • OSI参考模型:模型过于理想化,未能在因特网上进行广泛推广
  • TCP/IP参考模型(或TCP/IP协议):事实上的国际标准。

上图中,OSI参考模型:模型过于理想化,未能在因特网上进行广泛推广。 TCP/IP参考模型(或TCP/IP协议):事实上的国际标准

  • TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),TCP/IP 以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。是Internet最基本、最广泛的协议。

    image-20220512234904047

TCP/IP协议中的四层介绍:

  • 应用层:应用层决定了向用户提供应用服务时通信的活动。主要协议有:HTTP协议、FTP协议、SNMP(简单网络管理协议)、SMTP(简单邮件传输协议)和POP3(Post Office Protocol 3的简称,即邮局协议的第3个版)等。
  • 传输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。TCP(Transmission Control Protocol)协议,即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。UDP(User Datagram Protocol,用户数据报协议):是一个无连接的传输层协议、提供面向事务的简单不可靠的信息传送服务。
  • 网络层:网络层是整个TCP/IP协议的核心,支持网间互连的数据通信。它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。而IP协议是一种非常重要的协议。IP(internet protocal)又称为互联网协议。IP的责任就是把数据从源传送到目的地。它在源地址和目的地址之间传送一种称之为数据包的东西,它还提供对数据大小的重新组装功能,以适应不同网络对包大小的要求。
  • 物理+数据链路层:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。

image-20220514172435398

2. 谈传输层协议:TCP与UDP协议

通信的协议还是比较复杂的,java.net 包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。

java.net 包中提供了两种常见的网络协议的支持:

  • UDP:用户数据报协议(User Datagram Protocol)。
  • TCP:传输控制协议 (Transmission Control Protocol)。

2.1 TCP协议与UDP协议

TCP协议:

  • TCP协议进行通信的两个应用进程:客户端、服务端。
  • 使用TCP协议前,须先建立TCP连接,形成基于字节流的传输数据通道
  • 传输前,采用“三次握手”方式,点对点通信,是可靠的
    • TCP协议使用重发机制,当一个通信实体发送一个消息给另一个通信实体后,需要收到另一个通信实体确认信息,如果没有收到另一个通信实体确认信息,则会再次重复刚才发送的消息。
  • 在连接中可进行大数据量的传输
  • 传输完毕,需释放已建立的连接,效率低

UDP协议:

  • UDP协议进行通信的两个应用进程:发送端、接收端。
  • 将数据、源、目的封装成数据包(传输的基本单位),不需要建立连接
  • 发送不管对方是否准备好,接收方收到也不确认,不能保证数据的完整性,故是不可靠的
  • 每个数据报的大小限制在64K
  • 发送数据结束时无需释放资源,开销小,通信效率高
  • 适用场景:音频、视频和普通数据的传输。例如视频会议

TCP生活案例:打电话

UDP生活案例:发送短信、发电报

2.2 三次握手

TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。

  • 第一次握手,客户端向服务器端发起TCP连接的请求
  • 第二次握手,服务器端发送针对客户端TCP连接请求的确认
  • 第三次握手,客户端发送确认的确认
image-20220415010105484

1、客户端会随机一个初始序列号seq=x,设置SYN=1 ,表示这是SYN握手报文。然后就可以把这个 SYN 报文发送给服务端了,表示向服务端发起连接,之后客户端处于同步已发送状态。

2、服务端收到客户端的 SYN 报文后,也随机一个初始序列号(seq=y),设置ack=x+1,表示收到了客户端的x之前的数据,希望客户端下次发送的数据从x+1开始。
设置 SYN=1 和 ACK=1。表示这是一个SYN握手和ACK确认应答报文。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于同步已接收状态。

3、客户端收到服务端报文后,还要向服务端回应最后一个应答报文,将ACK置为 1 ,表示这是一个应答报文
ack=y+1 ,表示收到了服务器的y之前的数据,希望服务器下次发送的数据从y+1开始。
最后把报文发送给服务端,这次报文可以携带数据,之后客户端处于 连接已建立 状态。服务器收到客户端的应答报文后,也进入连接已建立状态。

完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。

2.3 四次挥手

TCP协议中,在发送数据结束后,释放连接时需要经过四次挥手。

  • 第一次挥手:客户端向服务器端提出结束连接,让服务器做最后的准备工作。此时,客户端处于半关闭状态,即表示不再向服务器发送数据了,但是还可以接受数据。
  • 第二次挥手:服务器接收到客户端释放连接的请求后,会将最后的数据发给客户端。并告知上层的应用进程不再接收数据。
  • 第三次挥手:服务器发送完数据后,会给客户端发送一个释放连接的报文。那么客户端接收后就知道可以正式释放连接了。
  • 第四次挥手:客户端接收到服务器最后的释放连接报文后,要回复一个彻底断开的报文。这样服务器收到后才会彻底释放连接。这里客户端,发送完最后的报文后,会等待2MSL,因为有可能服务器没有收到最后的报文,那么服务器迟迟没收到,就会再次给客户端发送释放连接的报文,此时客户端在等待时间范围内接收到,会重新发送最后的报文,并重新计时。如果等待2MSL后,没有收到,那么彻底断开。
image-20220415010205891

1、客户端打算断开连接,向服务器发送FIN报文(FIN标记位被设置为1,1表示为FIN,0表示不是),FIN报文中会指定一个序列号,之后客户端进入FIN_WAIT_1状态。也就是客户端发出连接释放报文段(FIN报文),指定序列号seq = u,主动关闭TCP连接,等待服务器的确认。

2、服务器收到连接释放报文段(FIN报文)后,就向客户端发送ACK应答报文,以客户端的FIN报文的序列号 seq+1 作为ACK应答报文段的确认序列号ack = seq+1 = u + 1。接着服务器进入CLOSE_WAIT(等待关闭)状态,此时的TCP处于半关闭状态(下面会说什么是半关闭状态),客户端到服务器的连接释放。客户端收到来自服务器的ACK应答报文段后,进入FIN_WAIT_2状态。

3、服务器也打算断开连接,向客户端发送连接释放(FIN)报文段,之后服务器进入LASK_ACK(最后确认)状态,等待客户端的确认。服务器的连接释放(FIN)报文段的FIN=1,ACK=1,序列号seq=m,确认序列号ack=u+1。

4、客户端收到来自服务器的连接释放(FIN)报文段后,会向服务器发送一个ACK应答报文段,以连接释放(FIN)报文段的确认序号 ack 作为ACK应答报文段的序列号 seq,以连接释放(FIN)报文段的序列号 seq+1作为确认序号ack。

之后客户端进入TIME_WAIT(时间等待)状态,服务器收到ACK应答报文段后,服务器就进入CLOSE(关闭)状态,到此服务器的连接已经完成关闭。客户端处于TIME_WAIT状态时,此时的TCP还未释放掉,需要等待2MSL后,客户端才进入CLOSE状态。

3. 网络编程API

img

3.1 InetAddress类

InetAddress类主要表示IP地址,两个子类:Inet4Address、Inet6Address。

InetAddress 类没有提供公共的构造器,而是提供如下几个静态方法来获取InetAddress 实例

  • public static InetAddress getLocalHost()
  • public static InetAddress getByName(String host)
  • public static InetAddress getByAddress(byte[] addr)

InetAddress 提供了如下几个常用的方法

  • public String getHostAddress() :返回 IP 地址字符串(以文本表现形式)
  • public String getHostName() :获取此 IP 地址的主机名
  • public boolean isReachable(int timeout):测试是否可以达到该地址
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
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetAddressTest {
public static void main(String[] args) throws UnknownHostException {
//1.实例化 获取指定ip对应的InetAddress的实例
// getByName()
InetAddress inet1 = InetAddress.getByName("192.168.23.31");
System.out.println("指定ip的信息: "+inet1);
InetAddress inet2 = InetAddress.getByName("www.xt7910.top"); //域名解析出ip为185.199.111.153
System.out.println("指定域名的信息: "+inet2);

//2.实例化 获取本地ip对应的InetAddress的实例
// getLocalHost() 本机地址
InetAddress localHost1 = InetAddress.getLocalHost();
System.out.println("本机名/IP地址: "+localHost1); //larkkkkkkk/10.171.87.85
InetAddress localHost2 = InetAddress.getByName("127.0.0.1");//本地回路地址:127.0.0.1
System.out.println("本地回路地址: "+localHost2);
System.out.println("-----------------------------------------------------");

//方法
System.out.println("我的域名信息: "+inet2.getHostName()); //打印域名
System.out.println("我的域名ip地址: "+inet2.getHostAddress()); //打印地址
}
}
最终输出:
指定ip的信息: /192.168.23.31
指定域名的信息: www.xt7910.top/185.199.111.153
本机名/IP地址: larkkkkkkk/10.171.87.85
本地回路地址: /127.0.0.1
-----------------------------------------------------
我的域名信息: www.xt7910.top
我的域名ip地址: 185.199.111.153

1564024137998

3.2 Socket类

  • 具有唯一标识的IP地址+端口号 –组合–> 构成唯一能识别的标识符套接字(Socket)
  • 利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。网络通信其实就是Socket间的通信。
  • 通信的两端都要有Socket,是两台机器间通信的端点。

  • Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。

  • 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。

  • Socket分类:

    • 流套接字(stream socket):使用TCP提供可依赖的字节流服务
      • ServerSocket:此类实现TCP服务器套接字。服务器套接字等待请求通过网络传入。
      • Socket:此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
    • 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务
      • DatagramSocket:此类表示用来发送和接收UDP数据报包的套接字。

3.3 Socket相关类API

3.3.1 ServerSocket类

ServerSocket类的构造方法:

  • ServerSocket(int port) :创建绑定到特定端口的服务器套接字。

ServerSocket类的常用方法:

  • Socket accept():侦听并接受到此套接字的连接。

3.3.2 Socket类

Socket类的常用构造方法

  • public Socket(InetAddress address,int port):创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
  • public Socket(String host,int port):创建一个流套接字并将其连接到指定主机上的指定端口号。

Socket类的常用方法

  • public InputStream getInputStream():返回此套接字的输入流,可以用于接收消息
  • public OutputStream getOutputStream():返回此套接字的输出流,可以用于发送消息
  • public InetAddress getInetAddress():此套接字连接到的远程 IP 地址;如果套接字是未连接的,则返回 null。
  • public InetAddress getLocalAddress():获取套接字绑定的本地地址。
  • public int getPort():此套接字连接到的远程端口号;如果尚未连接套接字,则返回 0。
  • public int getLocalPort():返回此套接字绑定到的本地端口。如果尚未绑定套接字,则返回 -1。
  • public void close():关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使用(即无法重新连接或重新绑定)。需要创建新的套接字对象。 关闭此套接字也将会关闭该套接字的 InputStream 和 OutputStream。
  • public void shutdownInput():如果在套接字上调用 shutdownInput() 后从套接字输入流读取内容,则流将返回 EOF(文件结束符)。 即不能在从此套接字的输入流中接收任何数据。
  • public void shutdownOutput():禁用此套接字的输出流。对于 TCP 套接字,任何以前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。 如果在套接字上调用 shutdownOutput() 后写入套接字输出流,则该流将抛出 IOException。 即不能通过此套接字的输出流发送任何数据。

注意:先后调用Socket的shutdownInput()和shutdownOutput()方法,仅仅关闭了输入流和输出流,并不等于调用Socket的close()方法。在通信结束后,仍然要调用Scoket的close()方法,因为只有该方法才会释放Socket占用的资源,比如占用的本地端口号等。

3.3.3 DatagramSocket类

DatagramSocket 类的常用方法:

  • public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被绑定到通配符地址,IP 地址由内核来选择。
  • public DatagramSocket(int port,InetAddress laddr)创建数据报套接字,将其绑定到指定的本地地址。本地端口必须在 0 到 65535 之间(包括两者)。如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地址,IP 地址由内核选择。
  • public void close()关闭此数据报套接字。
  • public void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。
  • public void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时,DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的长度长,该信息将被截短。
  • public InetAddress getLocalAddress()获取套接字绑定的本地地址。
  • public int getLocalPort()返回此套接字绑定的本地主机上的端口号。
  • public InetAddress getInetAddress()返回此套接字连接的地址。如果套接字未连接,则返回 null。
  • public int getPort()返回此套接字的端口。如果套接字未连接,则返回 -1。

3.3.4 DatagramPacket类

DatagramPacket类的常用方法:

  • public DatagramPacket(byte[] buf,int length)构造 DatagramPacket,用来接收长度为 length 的数据包。 length 参数必须小于等于 buf.length。
  • public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。length 参数必须小于等于 buf.length。
  • public InetAddress getAddress()返回某台机器的 IP 地址,此数据报将要发往该机器或者是从该机器接收到的。
  • public int getPort()返回某台远程主机的端口号,此数据报将要发往该主机或者是从该主机接收到的。
  • public byte[] getData()返回数据缓冲区。接收到的或将要发送的数据从缓冲区中的偏移量 offset 处开始,持续 length 长度。
  • public int getLength()返回将要发送或接收到的数据的长度。

4. TCP网络编程

4.1 通信模型

Java语言的基于套接字TCP编程分为服务端编程和客户端编程,其通信模型如图所示:

image-20220514172833216

4.2 开发步骤

客户端程序包含以下四个基本的步骤 :

  • 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
  • 打开连接到 Socket 的输入/ 出流: 使用 getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输
  • 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线路。
  • 关闭 Socket :断开客户端到服务器的连接,释放线路

服务器端程序包含以下四个基本的 步骤:

  • 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。
  • 调用 accept() :监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。
  • 调用 该Socket 类对象的 getOutputStream() 和 getInputStream () :获取输出流和输入流,开始网络数据的发送和接收。
  • 关闭Socket 对象:客户端访问结束,关闭通信套接字。

4.3 例题与练习

例题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
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPTest1 {
//客户端 ---发送端
@Test
public void client() throws IOException {
//1.创建一个Socket
InetAddress inetAddress=InetAddress.getByName("10.171.87.85");
Socket socket=new Socket(inetAddress,8080);
//2.发送数据
OutputStream outputStream = socket.getOutputStream();
outputStream.write("你好我是客户端".getBytes());
//3.关闭Socket
socket.close();
outputStream.close();
}

//服务端 ---接收端
@Test
public void server() throws IOException {
//1.创建一个ServerSocket
ServerSocket serverSocket=new ServerSocket(8080);
//2.调用accept(),接收客户端的Socket
Socket socket = serverSocket.accept(); //阻塞式方法
System.out.println(socket.getInetAddress().getHostName()+"给我发送的请求");
System.out.println("服务器端已开启");
//3.接收数据
InputStream inputStream = socket.getInputStream();
byte[] buffer=new byte[1024];
int len;
ByteArrayOutputStream baos=new ByteArrayOutputStream(); //使用一个字符数组-输出流 内部维护一个byte[]数组
while((len=inputStream.read(buffer))!=-1){
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
System.out.println("数据已经接收完毕");
//4.关闭Socket和ServerSocket
serverSocket.close();
socket.close();
inputStream.close();
}

}

image-20231103151202673

例题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
package net;
import org.junit.Test;
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPTest2 {
//客户端 ---发送端
@Test
public void client() throws IOException {
//1.创建一个Socket
//指明对方(服务器端)的ip地址和端口号
InetAddress inetAddress=InetAddress.getByName("10.171.87.85");
Socket socket=new Socket(inetAddress,8080);
//2.创建File类对象,创建FileInputStream类实例
File file=new File("E:\\javacode\\test\\src\\net\\playgirl.jpg");
FileInputStream fis = new FileInputStream(file);
//3.通过socket获得输出流
OutputStream outputStream = socket.getOutputStream();
//4.读写数据
byte[] buffer = new byte[1024];
int len;
while((len = fis.read(buffer)) != -1){
outputStream.write(buffer,0,len);
}
System.out.println("数据发送完毕");
//5.关闭Socket和相关流
socket.close();
outputStream.close();
fis.close();
}

//服务端 ---接收端
@Test
public void server() throws IOException {
//1.创建一个ServerSocket
ServerSocket serverSocket=new ServerSocket(8080);
//2.调用accept(),接收客户端的Socket
Socket socket = serverSocket.accept(); //阻塞式方法
System.out.println(socket.getInetAddress().getHostName()+"给我发送的请求");
System.out.println("服务器端已开启");
//3.通过socket获取一个输入流
InputStream inputStream = socket.getInputStream();
//4.创建File类对象,创建FileOutStream对象
File file = new File("E:\\javacode\\test\\src\\net\\girlclient.jpg");
FileOutputStream fos = new FileOutputStream(file);
//5.读写过程
byte[] buffer = new byte[1024];
int len;
while((len = inputStream.read(buffer)) != -1){
fos.write(buffer,0,len);
}
System.out.println("数据接收完毕");
//6.关闭Socket和ServerSocket还有相关流
serverSocket.close();
socket.close();
inputStream.close();
fos.close();
}
}
image-20231103153058152

例题3:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端。并关闭相应的连接。

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
package net;
import org.junit.Test;
import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPTest3 {
//客户端 ---发送端
@Test
public void client() throws IOException {
//1.创建一个Socket
//指明对方(服务器端)的ip地址和端口号
InetAddress inetAddress=InetAddress.getByName("10.171.87.85");
Socket socket=new Socket(inetAddress,8080);
//2.创建File类对象,创建FileInputStream类实例
File file=new File("E:\\javacode\\test\\src\\net\\playgirl.jpg");
FileInputStream fis = new FileInputStream(file);
//3.通过socket获得输出流
OutputStream outputStream = socket.getOutputStream();
//4.读写数据
byte[] buffer = new byte[1024];
int len;
while((len = fis.read(buffer)) != -1){
outputStream.write(buffer,0,len);
}
System.out.println("client数据发送完毕");
//客户端表明不再继续发送数据
socket.shutdownOutput(); //不加的话可能认为还没发送完毕

//4.新增 接收客户端返回的数据
InputStream inputStream = socket.getInputStream();
ByteArrayOutputStream baos=new ByteArrayOutputStream(); //防止中文乱码
byte[] buffer1 = new byte[5];
int len1;
while((len1 = inputStream.read(buffer1)) != -1){
baos.write(buffer1,0,len1);
}
System.out.println("sever接受服务端的信息"+baos);
//5.关闭Socket和相关流
socket.close();
outputStream.close();
fis.close();
inputStream.close();
baos.close();
}

//服务端 ---接收端
@Test
public void server() throws IOException {
//1.创建一个ServerSocket
ServerSocket serverSocket=new ServerSocket(8080);
//2.调用accept(),接收客户端的Socket
Socket socket = serverSocket.accept(); //阻塞式方法
System.out.println(socket.getInetAddress().getHostName()+"给我发送的请求");
System.out.println("服务器端已开启");
//3.通过socket获取一个输入流
InputStream inputStream = socket.getInputStream();
//4.创建File类对象,创建FileOutStream对象
File file = new File("E:\\javacode\\test\\src\\net\\girlclient.jpg");
FileOutputStream fos = new FileOutputStream(file);
//5.读写过程
byte[] buffer = new byte[1024];
int len;
while((len = inputStream.read(buffer)) != -1){
fos.write(buffer,0,len);
}
System.out.println("server数据接收完毕");

//5.新增 返回数据给客户端
OutputStream outputStream = socket.getOutputStream();
outputStream.write("client谢谢你收到了".getBytes());

//6.关闭Socket和ServerSocket还有相关流
serverSocket.close();
socket.close();
inputStream.close();
fos.close();
outputStream.close();
}

}

image-20231103160126679

练习1:服务端读取图片并发送给客户端,客户端保存图片到本地

练习2:客户端给服务端发送文本,服务端会将文本转成大写在返回给客户端。

演示单个客户端与服务器单次通信:

需求:客户端连接服务器,连接成功后给服务发送“lalala”,服务器收到消息后,给客户端返回“欢迎登录”,客户端接收消息后,断开连接

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
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {

public static void main(String[] args)throws Exception {
//1、准备一个ServerSocket对象,并绑定8888端口
ServerSocket server = new ServerSocket(8888);
System.out.println("等待连接....");

//2、在8888端口监听客户端的连接,该方法是个阻塞的方法,如果没有客户端连接,将一直等待
Socket socket = server.accept();
InetAddress inetAddress = socket.getInetAddress();
System.out.println(inetAddress.getHostAddress() + "客户端连接成功!!");

//3、获取输入流,用来接收该客户端发送给服务器的数据
InputStream input = socket.getInputStream();
//接收数据
byte[] data = new byte[1024];
StringBuilder s = new StringBuilder();
int len;
while ((len = input.read(data)) != -1) {
s.append(new String(data, 0, len));
}
System.out.println(inetAddress.getHostAddress() + "客户端发送的消息是:" + s);

//4、获取输出流,用来发送数据给该客户端
OutputStream out = socket.getOutputStream();
//发送数据
out.write("欢迎登录".getBytes());
out.flush();

//5、关闭socket,不再与该客户端通信
//socket关闭,意味着InputStream和OutputStream也关闭了
socket.close();

//6、如果不再接收任何客户端通信,可以关闭ServerSocket
server.close();
}
}

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
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws Exception {
// 1、准备Socket,连接服务器,需要指定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8888);

// 2、获取输出流,用来发送数据给服务器
OutputStream out = socket.getOutputStream();
// 发送数据
out.write("lalala".getBytes());
//会在流末尾写入一个“流的末尾”标记,对方才能读到-1,否则对方的读取方法会一致阻塞
socket.shutdownOutput();

//3、获取输入流,用来接收服务器发送给该客户端的数据
InputStream input = socket.getInputStream();
// 接收数据
byte[] data = new byte[1024];
StringBuilder s = new StringBuilder();
int len;
while ((len = input.read(data)) != -1) {
s.append(new String(data, 0, len));
}
System.out.println("服务器返回的消息是:" + s);

//4、关闭socket,不再与服务器通信,即断开与服务器的连接
//socket关闭,意味着InputStream和OutputStream也关闭了
socket.close();
}
}

演示多个客户端与服务器之间的多次通信:

通常情况下,服务器不应该只接受一个客户端请求,而应该不断地接受来自客户端的所有请求,所以Java程序通常会通过循环,不断地调用ServerSocket的accept()方法。

如果服务器端要“同时”处理多个客户端的请求,因此服务器端需要为每一个客户端单独分配一个线程来处理,否则无法实现“同时”。

咱们之前学习IO流的时候,提到过装饰者设计模式,该设计使得不管底层IO流是怎样的节点流:文件流也好,网络Socket产生的流也好,程序都可以将其包装成处理流,甚至可以多层包装,从而提供更多方便的处理。

案例需求:多个客户端连接服务器,并进行多次通信

  • 每一个客户端连接成功后,从键盘输入英文单词或中国成语,并发送给服务器
  • 服务器收到客户端的消息后,把词语“反转”后返回给客户端
  • 客户端接收服务器返回的“词语”,打印显示
  • 当客户端输入“stop”时断开与服务器的连接
  • 多个客户端可以同时给服务器发送“词语”,服务器可以“同时”处理多个客户端的请求
image-20220514173031651

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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
// 1、准备一个ServerSocket
ServerSocket server = new ServerSocket(8888);
System.out.println("等待连接...");

int count = 0;
while(true){
// 2、监听一个客户端的连接
Socket socket = server.accept();
System.out.println("第" + ++count + "个客户端"+socket.getInetAddress().getHostAddress()+"连接成功!!");

ClientHandlerThread ct = new ClientHandlerThread(socket);
ct.start();
}

//这里没有关闭server,永远监听
}
static class ClientHandlerThread extends Thread{
private Socket socket;
private String ip;

public ClientHandlerThread(Socket socket) {
super();
this.socket = socket;
ip = socket.getInetAddress().getHostAddress();
}

public void run(){
try{
//(1)获取输入流,用来接收该客户端发送给服务器的数据
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//(2)获取输出流,用来发送数据给该客户端
PrintStream ps = new PrintStream(socket.getOutputStream());
String str;
// (3)接收数据
while ((str = br.readLine()) != null) {
//(4)反转
StringBuilder word = new StringBuilder(str);
word.reverse();

//(5)返回给客户端
ps.println(word);
}
System.out.println("客户端" + ip+"正常退出");
}catch(Exception e){
System.out.println("客户端" + ip+"意外退出");
}finally{
try {
//(6)断开连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

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
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class Client {
public static void main(String[] args) throws Exception {
// 1、准备Socket,连接服务器,需要指定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8888);

// 2、获取输出流,用来发送数据给服务器
OutputStream out = socket.getOutputStream();
PrintStream ps = new PrintStream(out);

// 3、获取输入流,用来接收服务器发送给该客户端的数据
InputStream input = socket.getInputStream();
BufferedReader br;
if(args!= null && args.length>0) {
String encoding = args[0];
br = new BufferedReader(new InputStreamReader(input,encoding));
}else{
br = new BufferedReader(new InputStreamReader(input));
}

Scanner scanner = new Scanner(System.in);
while(true){
System.out.println("输入发送给服务器的单词或成语:");
String message = scanner.nextLine();
if(message.equals("stop")){
socket.shutdownOutput();
break;
}

// 4、 发送数据
ps.println(message);
// 接收数据
String feedback = br.readLine();
System.out.println("从服务器收到的反馈是:" + feedback);
}

//5、关闭socket,断开与服务器的连接
scanner.close();
socket.close();
}
}

4.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
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
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

public class TestChatServer {
//这个集合用来存储所有在线的客户端
static ArrayList<Socket> online = new ArrayList<Socket>();

public static void main(String[] args)throws Exception {
//1、启动服务器,绑定端口号
ServerSocket server = new ServerSocket(8989);

//2、接收n多的客户端同时连接
while(true){
Socket accept = server.accept();

online.add(accept);//把新连接的客户端添加到online列表中

MessageHandler mh = new MessageHandler(accept);
mh.start();//
}
}

static class MessageHandler extends Thread{
private Socket socket;
private String ip;

public MessageHandler(Socket socket) {
super();
this.socket = socket;
}

public void run(){
try {
ip = socket.getInetAddress().getHostAddress();

//插入:给其他客户端转发“我上线了”
sendToOther(ip+"上线了");

//(1)接收该客户端的发送的消息
InputStream input = socket.getInputStream();
InputStreamReader reader = new InputStreamReader(input);
BufferedReader br = new BufferedReader(reader);

String str;
while((str = br.readLine())!=null){
//(2)给其他在线客户端转发
sendToOther(ip+":"+str);
}

sendToOther(ip+"下线了");
} catch (IOException e) {
try {
sendToOther(ip+"掉线了");
} catch (IOException e1) {
e1.printStackTrace();
}
}finally{
//从在线人员中移除我
online.remove(socket);
}
}

//封装一个方法:给其他客户端转发xxx消息
public void sendToOther(String message) throws IOException{
//遍历所有的在线客户端,一一转发
for (Socket on : online) {
OutputStream every = on.getOutputStream();
//为什么用PrintStream?目的用它的println方法,按行打印
PrintStream ps = new PrintStream(every);

ps.println(message);
}
}
}
}

客户端:

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
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;

public class TestChatClient {
public static void main(String[] args)throws Exception {
//1、连接服务器
Socket socket = new Socket("127.0.0.1",8989);

//2、开启两个线程
//(1)一个线程负责看别人聊,即接收服务器转发的消息
Receive receive = new Receive(socket);
receive.start();

//(2)一个线程负责发送自己的话
Send send = new Send(socket);
send.start();

send.join();//等我发送线程结束了,才结束整个程序

socket.close();
}
}
class Send extends Thread{
private Socket socket;

public Send(Socket socket) {
super();
this.socket = socket;
}

public void run(){
try {
OutputStream outputStream = socket.getOutputStream();
//按行打印
PrintStream ps = new PrintStream(outputStream);

Scanner input = new Scanner(System.in);

//从键盘不断的输入自己的话,给服务器发送,由服务器给其他人转发
while(true){
System.out.print("自己的话:");
String str = input.nextLine();
if("bye".equals(str)){
break;
}
ps.println(str);
}

input.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}
class Receive extends Thread{
private Socket socket;

public Receive(Socket socket) {
super();
this.socket = socket;
}

public void run(){
try {
InputStream inputStream = socket.getInputStream();
Scanner input = new Scanner(inputStream);

while(input.hasNextLine()){
String line = input.nextLine();
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

4.5 理解客户端、服务端

  • 客户端:

    • 自定义
    • 浏览器(browser — server)
  • 服务端:

    • 自定义
    • Tomcat服务器

5. UDP网络编程

UDP(User Datagram Protocol,用户数据报协议):是一个无连接的传输层协议、提供面向事务的简单不可靠的信息传送服务,类似于短信。

5.1 通信模型

UDP协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送,至于对方是否可以接收到这些数据内容,UDP协议无法控制,因此说,UDP协议是一种不可靠的协议。无连接的好处就是快,省内存空间和流量,因为维护连接需要创建大量的数据结构。UDP会尽最大努力交付数据,但不保证可靠交付,没有TCP的确认机制、重传机制,如果因为网络原因没有传送到对端,UDP也不会给应用层返回错误信息。

UDP协议是面向数据报文的信息传送服务。UDP在发送端没有缓冲区,对于应用层交付下来的报文在添加了首部之后就直接交付于ip层,不会进行合并,也不会进行拆分,而是一次交付一个完整的报文。比如我们要发送100个字节的报文,我们调用一次send()方法就会发送100字节,接收方也需要用receive()方法一次性接收100字节,不能使用循环每次获取10个字节,获取十次这样的做法。

UDP协议没有拥塞控制,所以当网络出现的拥塞不会导致主机发送数据的速率降低。虽然UDP的接收端有缓冲区,但是这个缓冲区只负责接收,并不会保证UDP报文的到达顺序是否和发送的顺序一致。因为网络传输的时候,由于网络拥塞的存在是很大的可能导致先发的报文比后发的报文晚到达。如果此时缓冲区满了,后面到达的报文将直接被丢弃。这个对实时应用来说很重要,比如:视频通话、直播等应用。

因此UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境,数据报大小限制在64K以下。

image-20220131144712460

类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序。

UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证 UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。

DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号。

UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。如同发快递包裹一样。

5.2 开发步骤

发送端程序包含以下四个基本的步骤:

  • 创建DatagramSocket :默认使用系统随机分配端口号。
  • 创建DatagramPacket:将要发送的数据用字节数组表示,并指定要发送的数据长度,接收方的IP地址和端口号。
  • 调用 该DatagramSocket 类对象的 send方法 :发送数据报DatagramPacket对象。
  • 关闭DatagramSocket 对象:发送端程序结束,关闭通信套接字。

接收端程序包含以下四个基本的步骤 :

  • 创建DatagramSocket :指定监听的端口号。
  • 创建DatagramPacket:指定接收数据用的字节数组,起到临时数据缓冲区的效果,并指定最大可以接收的数据长度。
  • 调用 该DatagramSocket 类对象的receive方法 :接收数据报DatagramPacket对象。。
  • 关闭DatagramSocket :接收端程序结束,关闭通信套接字。

5.3 演示发送和接收消息

基于UDP协议的网络编程仍然需要在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送、接收数据报的对象,Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表DatagramSocket发送、接收的数据报。

举例1:

发送端:

1
2
3
4
5
6
7
8
9
10
11
12
DatagramSocket ds = null;
try {
ds = new DatagramSocket();
byte[] by = "hello,atguigu.com".getBytes();
DatagramPacket dp = new DatagramPacket(by, 0, by.length, InetAddress.getByName("127.0.0.1"), 10000);
ds.send(dp);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (ds != null)
ds.close();
}

接收端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DatagramSocket ds = null;
try {
ds = new DatagramSocket(10000);
byte[] by = new byte[1024*64];
DatagramPacket dp = new DatagramPacket(by, by.length);
ds.receive(dp);
String str = new String(dp.getData(), 0, dp.getLength());
System.out.println(str + "--" + dp.getAddress());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (ds != null)
ds.close();
}

举例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
package com.atguigu.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;

public class Send {

public static void main(String[] args)throws Exception {
// 1、建立发送端的DatagramSocket
DatagramSocket ds = new DatagramSocket();

//要发送的数据
ArrayList<String> all = new ArrayList<String>();
all.add("尚硅谷让天下没有难学的技术!");
all.add("学高端前沿的IT技术来尚硅谷!");
all.add("尚硅谷让你的梦想变得更具体!");
all.add("尚硅谷让你的努力更有价值!");

//接收方的IP地址
InetAddress ip = InetAddress.getByName("127.0.0.1");
//接收方的监听端口号
int port = 9999;
//发送多个数据报
for (int i = 0; i < all.size(); i++) {
// 2、建立数据包DatagramPacket
byte[] data = all.get(i).getBytes();
DatagramPacket dp = new DatagramPacket(data, 0, data.length, ip, port);
// 3、调用Socket的发送方法
ds.send(dp);
}

// 4、关闭Socket
ds.close();
}
}

接收端:

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
package com.atguigu.udp;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class Receive {

public static void main(String[] args) throws Exception {
// 1、建立接收端的DatagramSocket,需要指定本端的监听端口号
DatagramSocket ds = new DatagramSocket(9999);

//一直监听数据
while(true){
//2、建立数据包DatagramPacket
byte[] buffer = new byte[1024*64];
DatagramPacket dp = new DatagramPacket(buffer,buffer.length);

//3、调用Socket的接收方法
ds.receive(dp);

//4、拆封数据
String str = new String(dp.getData(),0,dp.getLength());
System.out.println(str);
}

// ds.close();
}
}

6. URL编程

6.1 URL类

  • URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址。

  • 通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp 站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源。

  • URL的基本结构由5部分组成:

1
<传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表
  • 例如: http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123

    • 片段名:即锚点,例如看小说,直接定位到章节
    • 参数列表格式:参数名=参数值&参数名=参数值….
  • 为了表示URL,java.net 中实现了类 URL。我们可以通过下面的构造器来初始化一个 URL 对象:

    • public URL (String spec):通过一个表示URL地址的字符串可以构造一个URL对象。例如:

      1
      URL url = new URL("http://www. atguigu.com/");
    • public URL(URL context, String spec):通过基 URL 和相对 URL 构造一个 URL 对象。例如:

      1
      URL downloadUrl = new URL(url, “download.html")

      public URL(String protocol, String host, String file); 例如:

      1
      URL url = new URL("http", "www.atguigu.com", “download. html");
    • public URL(String protocol, String host, int port, String file); 例如:

      1
      URL gamelan = new URL("http", "www.atguigu.com", 80, “download.html");
  • URL类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通常是用 try-catch 语句进行捕获。

image-20220415013546074

6.2 URL类常用方法

一个URL对象生成后,其属性是不能被改变的,但可以通过它给定的方法来获取这些属性:

  • public String getProtocol( ) 获取该URL的协议名

  • public String getHost( ) 获取该URL的主机名

  • public String getPort( ) 获取该URL的端口号

  • public String getPath( ) 获取该URL的文件路径

  • public String getFile( ) 获取该URL的文件名

  • public String getQuery( ) 获取该URL的查询名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.net.MalformedURLException;
import java.net.URL;
public class UrlTest {
public static void main(String[] args) throws MalformedURLException {
String str="http://192.168.21.107:8080/examples/abcd.jpg?name=Tom";
URL url=new URL(str);
System.out.println("协议:"+url.getProtocol());
System.out.println("主机号:"+url.getHost());
System.out.println("端口号:"+url.getPort());
System.out.println("文件路径:"+url.getPath());
System.out.println("文件名:"+url.getFile());
System.out.println("参数列表:"+url.getQuery());
}
}

image-20231103162911038

6.3 针对HTTP协议的URLConnection类

  • URL的方法 openStream():能从网络上读取数据

  • 若希望输出数据,例如向服务器端的 CGI (公共网关接口-Common Gateway Interface-的简称,是用户浏览器和服务器端的应用程序进行连接的接口)程序发送一些数据,则必须先与URL建立连接,然后才能对其进行读写,此时需要使用 URLConnection 。

  • URLConnection:表示到URL所引用的远程对象的连接。当与一个URL建立连接时,首先要在一个 URL 对象上通过方法 openConnection() 生成对应的 URLConnection 对象。如果连接过程失败,将产生IOException.

    • String str="http://raw.githubusercontent.com/.images/202311031512799.png";
      URL url=new URL(str);
      HttpURLConnection urlConnection= (HttpURLConnection) url.openConnection();
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      - 通过URLConnection对象获取的输入流和输出流,即可以与现有的CGI程序进行交互。

      - public Object getContent( ) throws IOException
      - public int getContentLength( )
      - public String getContentType( )
      - public long getDate( )
      - public long getLastModified( )
      - public InputStream getInputStream ( ) throws IOException
      - public OutputSteram getOutputStream( )throws IOException
      import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.io.*; public class UrlTestDown { public static void main(String[] args) throws IOException { //将URL代表的资源下载到本地 //1.获取URL实例 String str="http://raw.githubusercontent.com/Larkkkkkkk/hexo-picture/main/.images/202311031512799.png"; URL url=new URL(str); //2.建立与服务器端的连接 HttpURLConnection urlConnection= (HttpURLConnection) url.openConnection(); //3.获取输入流(从服务器得到)、创建输出流(存储到本地) InputStream is = urlConnection.getInputStream(); File file=new File("hahha.png"); FileOutputStream fos=new FileOutputStream(file); //4.读写数据 byte[] buffer = new byte[1024]; int len; while ((len = is.read(buffer)) != -1) { fos.write(buffer, 0, len); } System.out.println("文件下载完成"); //5.关闭资源 fos.close(); is.close(); urlConnection.disconnect(); //还比较特殊 } }

    image-20231103164404714

6.4 小结

  • 位于网络中的计算机具有唯一的IP地址,这样不同的主机可以互相区分。
  • 客户端-服务器是一种最常见的网络应用程序模型。服务器是一个为其客户端提供某种特定服务的硬件或软件。客户机是一个用户应用程序,用于访问某台服务器提供的服务。端口号是对一个服务的访问场所,它用于区分同一物理计算机上的多个服务。套接字用于连接客户端和服务器,客户端和服务器之间的每个通信会话使用一个不同的套接字。TCP协议用于实现面向连接的会话。
  • Java 中有关网络方面的功能都定义在 java.net 程序包中。Java 用 InetAddress 对象表示 IP 地址,该对象里有两个字段:主机名(String) 和 IP 地址(int)。
  • 类 Socket 和 ServerSocket 实现了基于TCP协议的客户端-服务器程序。Socket是客户端和服务器之间的一个连接,连接创建的细节被隐藏了。这个连接提供了一个安全的数据传输通道,这是因为 TCP 协议可以解决数据在传送过程中的丢失、损坏、重复、乱序以及网络拥挤等问题,它保证数据可靠的传送。
  • 类 URL 和 URLConnection 提供了最高级网络应用。URL 的网络资源的位置来同一表示 Internet 上各种网络资源。通过URL对象可以创建当前应用程序和 URL 表示的网络资源之间的连接,这样当前程序就可以读取网络资源数据,或者把自己的数据传送到网络上去。

JAVA-File类和IO流

第15章_File类与IO流

本章专题与脉络

第3阶段:Java高级应用-第15章

1. java.io.File类的使用

1.1 概述

  • File类及本章下的各种流,都定义在java.io包下。
  • 一个File对象 —-代表—-> 硬盘或网络中可能存在的一个文件/文件目录(俗称文件夹),与平台无关。(体会万事万物皆对象)
  • File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。
    • File对象可以作为参数传递给流的构造器。
  • 想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录。

1.2 构造器

  • public File(String pathname) :以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储。
  • public File(String parent, String child) :以parent为父路径,child为子路径创建File对象。
  • public File(File parent, String child) :根据一个父File对象和子文件路径创建File对象

关于路径:

  • 绝对路径:从盘符开始的路径,这是一个完整的路径。
  • 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。
    • IDEA中,main中的文件的相对路径,是相对于”当前工程project
    • IDEA中,单元测试方法中的文件的相对路径,是相对于”当前模块module

举例:

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
import java.io.File;
public class FileObjectTest {
@Test
public void test(){
//1.File(String pathname)
File file1 = new File("D:\\javacode\\test\\abc");
System.out.println(file1.getAbsoluteFile()); // D:\javacode\test\abc

//2.File(String parent,String child) 以parent为父路径,child为子路径创建File对象
//参数1:一定是一个文件目录
//参数2:可以是一个文件,也可以是一个文件目录
String parent = "D:\\aaa";
String child = "bbb.txt";
File file2 = new File(parent, child);
System.out.println(file2.getAbsoluteFile()); // D:\aaa\bbb.txt

//3.File(File parent,String child) 根据一个父File对象和子文件路径创建File对象
// 参数1:一定是一个文件目录
//参数2:可以是一个文件,也可以是一个文件目录
File parentDir = new File("D:\\aaa");
String childFile = "bbb.txt";
File file3 = new File(parentDir, childFile);
System.out.println(file3.getAbsoluteFile()); // D:\aaa\bbb.txt
}

@Test
public void test01() throws IOException{
File f1 = new File("d:\\atguigu\\javase\\HelloIO.java"); //绝对路径
System.out.println("文件/目录的名称:" + f1.getName());
System.out.println("文件/目录的构造路径名:" + f1.getPath());
System.out.println("文件/目录的绝对路径名:" + f1.getAbsolutePath());
System.out.println("文件/目录的父目录名:" + f1.getParent());
}
@Test
public void test02()throws IOException{
File f2 = new File("/HelloIO.java");//绝对路径,从根路径开始
System.out.println("文件/目录的名称:" + f2.getName());
System.out.println("文件/目录的构造路径名:" + f2.getPath());
System.out.println("文件/目录的绝对路径名:" + f2.getAbsolutePath());
System.out.println("文件/目录的父目录名:" + f2.getParent());
}

@Test
public void test03() throws IOException {
File f3 = new File("HelloIO.java");//相对路径
System.out.println("user.dir =" + System.getProperty("user.dir"));
System.out.println("文件/目录的名称:" + f3.getName());
System.out.println("文件/目录的构造路径名:" + f3.getPath());
System.out.println("文件/目录的绝对路径名:" + f3.getAbsolutePath());
System.out.println("文件/目录的父目录名:" + f3.getParent());
}
@Test
public void test04() throws IOException{
File f5 = new File("HelloIO.java");//相对路径
System.out.println("user.dir =" + System.getProperty("user.dir"));
System.out.println("文件/目录的名称:" + f5.getName());
System.out.println("文件/目录的构造路径名:" + f5.getPath());
System.out.println("文件/目录的绝对路径名:" + f5.getAbsolutePath());
System.out.println("文件/目录的父目录名:" + f5.getParent());
}
}

注意:

  1. 无论该路径下是否存在文件或者目录,都不影响File对象的创建。

  2. window的路径分隔符使用“\”,而Java程序中的“\”表示转义字符,所以在Windows中表示路径,需要用“\”。或者直接使用“/”也可以,Java程序支持将“/”当成平台无关的路径分隔符。或者直接使用File.separator常量值表示。比如:

    File file2 = new File(“d:” + File.separator + “atguigu” + File.separator + “info.txt”);

  3. 当构造路径是绝对路径时,那么getPath和getAbsolutePath结果一样

    当构造路径是相对路径时,那么getAbsolutePath的路径 = user.dir的路径 + 构造路径

1.3 常用方法

1、获取文件和目录基本信息

  • public String getName() :获取名称
  • public String getPath() :获取路径
  • public String getAbsolutePath():获取绝对路径
  • public File getAbsoluteFile():获取绝对路径表示的文件
  • public String getParent():获取上层文件目录路径。若无,返回null
  • public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
  • public long lastModified() :获取最后一次的修改时间,毫秒值

如果File对象代表的文件或目录存在,则File对象实例初始化时,就会用硬盘中对应文件或目录的属性信息(例如,时间、类型等)为File对象的属性赋值,否则除了路径和名称,File对象的其他属性将会保留默认值。

image-20220412215446368

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test2(){
//文件存在
File file=new File("E:\\宋亚翔-学业奖材料\\宋亚翔.docx");
System.out.println("文件名称:"+file.getName());
System.out.println("文件路径:"+file.getPath());
System.out.println("文件绝对路径:"+file.getAbsoluteFile());
System.out.println("绝对路径的文件:"+file.getAbsoluteFile());
System.out.println("上层文件目录路径:"+file.getParent());
System.out.println("文件大小(字节数):"+file.length());
System.out.println("足迹后一次修改时间(毫秒值):"+file.lastModified());
System.out.println("-----------------------------------------------------------------");
//文件不存在
File file1=new File("E:\\宋亚翔-学业奖材料\\李四.docx");
System.out.println("文件名称:"+file1.getName());
System.out.println("文件路径:"+file1.getPath());
System.out.println("文件绝对路径:"+file1.getAbsoluteFile());
System.out.println("绝对路径的文件:"+file1.getAbsoluteFile());
System.out.println("上层文件目录路径:"+file1.getParent());
//其他属性保留默认值
System.out.println("文件大小(字节数):"+file1.length());
System.out.println("足迹后一次修改时间(毫秒值):"+file1.lastModified());
}

image-20231101103403951

2、列出目录的下一级

  • public String[] list() :返回一个String数组,表示该File目录中的所有子文件或目录。
  • public File[] listFiles() :返回一个File数组,表示该File目录中的所有的子文件或目录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test3(){
//文件存在
File file=new File("E:\\宋亚翔-学业奖材料");
String[] strs=file.list();
for(String temp:strs){
System.out.println(temp);
}
System.out.println("--------------------------------------");
File[] files = file.listFiles();
for(File temp:files){
System.out.println(temp);
}
}

image-20231101110256535

3、File类的重命名功能

  • public boolean renameTo(File dest):把文件重命名为指定的文件路径。

4、判断功能的方法

  • public boolean exists() :此File表示的文件或目录是否实际存在。
  • public boolean isDirectory() :此File表示的是否为目录。
  • public boolean isFile() :此File表示的是否为文件。
  • public boolean canRead() :判断是否可读
  • public boolean canWrite() :判断是否可写
  • public boolean isHidden() :判断是否隐藏

举例:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test4(){
//文件存在
File file=new File("E:\\宋亚翔-学业奖材料\\宋亚翔.docx");
System.out.println("文件是否存在:"+file.exists());
System.out.println("file是否为目录:"+file.isDirectory());
System.out.println("file是否为文件:"+file.isFile());
System.out.println("file是否可读:"+file.canRead());
System.out.println("file是否可写:"+file.canWrite());
System.out.println("file是否隐藏:"+file.isHidden());
}

image-20231101110745077

如果文件或目录不存在,那么exists()、isFile()和isDirectory()都是返回true

5、创建、删除功能

  • public boolean createNewFile() :创建文件。若文件存在,则不创建,返回false。
  • public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
  • public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建。
  • public boolean delete() :删除文件或者文件夹
    删除注意事项:① Java中的删除不走回收站。② 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。

举例:

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
import java.io.File;
import java.io.IOException;
public class FileCreateDelete {
public static void main(String[] args) throws IOException {
// 文件的创建
File f = new File("aaa.txt");
System.out.println("aaa.txt是否存在:"+f.exists());
System.out.println("aaa.txt是否创建:"+f.createNewFile());
System.out.println("aaa.txt是否存在:"+f.exists());

// 目录的创建
File f2= new File("newDir");
System.out.println("newDir是否存在:"+f2.exists());
System.out.println("newDir是否创建:"+f2.mkdir());
System.out.println("newDir是否存在:"+f2.exists());

// 创建一级目录
File f3= new File("newDira\\newDirb");
System.out.println("newDira\\newDirb创建:" + f3.mkdir());
File f4= new File("newDir\\newDirb");
System.out.println("newDir\\newDirb创建:" + f4.mkdir());
// 创建多级目录
File f5= new File("newDira\\newDirb");
System.out.println("newDira\\newDirb创建:" + f5.mkdirs());

// 文件的删除
System.out.println("aaa.txt删除:" + f.delete());

// 目录的删除 delete方法,如果此File表示目录,则目录必须为空才能删除
System.out.println("newDir删除:" + f2.delete());
System.out.println("newDir\\newDirb删除:" + f4.delete());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
运行结果:
aaa.txt是否存在:false
aaa.txt是否创建:true
aaa.txt是否存在:true
newDir是否存在:false
newDir是否创建:true
newDir是否存在:true
newDira\newDirb创建:false
newDir\newDirb创建:true
newDira\newDirb创建:true
aaa.txt删除:true
newDir删除:false
newDir\newDirb删除:true

API中说明:delete方法,如果此File表示目录,则目录必须为空才能删除。

1.4 练习

练习1:利用File构造器,new 一个文件目录file

​ 1) 在其中创建多个文件和目录

2) 编写方法,实现删除file中指定文件的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test6() throws IOException {
File file=new File("F:\\test1");
File file1=new File("F:\\test1\\abc.txt");
File file2=new File("F:\\test1\\aaa.txt");
File file3=new File("F:\\test1\\test1mulu");

//创建一个文件目录
System.out.println(file.mkdir()); //创建一个test1目录
//创建多个文件和目录
System.out.println(file1.createNewFile()); //新增abc.txt文件
System.out.println(file2.createNewFile()); //新增aaa.txt文件
System.out.println(file3.mkdir()); //新增test1mulu文件
//删除file中指定文件
System.out.println(file1.delete()); //删除abc.txt文件
}

image-20231101143332206

练习2:判断指定目录下是否有后缀名为.jpg的文件。如果有,就输出该文件名称

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
public class FindJPGFileTest {
//方法1:
@Test
public void test1(){
File srcFile = new File("d:\\code");

String[] fileNames = srcFile.list(); //将code目录下所有文件转为字符串数组
for(String fileName : fileNames){
if(fileName.endsWith(".jpg")){ //查看是否以.jpg文件名结束
System.out.println(fileName);
}
}
}
//方法2:
@Test
public void test2(){
File srcFile = new File("d:\\code");

File[] listFiles = srcFile.listFiles();
for(File file : listFiles){
if(file.getName().endsWith(".jpg")){
System.out.println(file.getAbsolutePath());
}
}
}
//方法3:
/*
* File类提供了两个文件过滤器方法
* public String[] list(FilenameFilter filter)
* public File[] listFiles(FileFilter filter)

*/
@Test
public void test3(){
File srcFile = new File("d:\\code");

File[] subFiles = srcFile.listFiles(new FilenameFilter() {

@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jpg");
}
});

for(File file : subFiles){
System.out.println(file.getAbsolutePath());
}
}

}

练习3:遍历指定目录所有文件名称,包括子文件目录中的文件。

拓展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
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
public class ListFilesTest {
//练习3:(方式1)
public static void printSubFile(File dir) {
// 打印目录的子文件
File[] subfiles = dir.listFiles();

for (File f : subfiles) {
if (f.isDirectory()) {// 文件目录
printSubFile(f);
} else {// 文件
System.out.println(f.getAbsolutePath());
}

}
}

// //练习3:(方式2)
public void listAllSubFiles(File file) {
if (file.isFile()) {
System.out.println(file);
} else {
File[] all = file.listFiles();
// 如果all[i]是文件,直接打印
// 如果all[i]是目录,接着再获取它的下一级
for (File f : all) {
listAllSubFiles(f);// 递归调用:自己调用自己就叫递归
}
}
}
@Test
public void testListAllFiles(){
// 1.创建目录对象
File dir = new File("E:\\teach\\01_javaSE\\_尚硅谷Java编程语言\\3_软件");

// 2.打印目录的子文件
printSubFile(dir);
}

// 拓展1:求指定目录所在空间的大小
public long getDirectorySize(File file) {
// file是文件,那么直接返回file.length()
// file是目录,把它的下一级的所有file大小加起来就是它的总大小
long size = 0;
if (file.isFile()) {
size = file.length();
} else {
File[] all = file.listFiles();// 获取file的下一级
// 累加all[i]的大小
for (File f : all) {
size += getDirectorySize(f);// f的大小;
}
}
return size;
}

// 拓展2:删除指定的目录
public void deleteDirectory(File file) {
// 如果file是文件,直接delete
// 如果file是目录,先把它的下一级干掉,然后删除自己
if (file.isDirectory()) {
File[] all = file.listFiles();
// 循环删除的是file的下一级
for (File f : all) {// f代表file的每一个下级
deleteDirectory(f);
}
}
// 删除自己
file.delete();
}

}

2. IO流原理及流的分类

image-20220412224132724

2.1 Java IO原理

  • Java程序中,数据的输入/输出操作以“流(stream)” 的方式进行,可以看做是一种数据的流动。

    image-20220503123117300
  • I/O流中的I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。

    • 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
    • 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中。

image-20220412224700133

2.2 流的分类

java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

  • 数据的流向不同 —-> :输入流输出流

    • 输入流 :把数据从其他设备上读取到内存中的流。
      • 以InputStream、Reader结尾
    • 输出流 :把数据从内存 中写出到其他设备上的流。
      • 以OutputStream、Writer结尾
  • 操作数据单位的不同 ** —-> :字节流(8bit)字符流(16bit)**。

    • 字节流 :以字节为单位,读写数据的流。
      • 以InputStream、OutputStream结尾
    • 字符流 :以字符为单位,读写数据的流。
      • 以Reader、Writer结尾
  • 根据IO流的角色不同 —-> :节点流处理流

    • 节点流:直接从数据源或目的地读写数据

      image-20220412230745170

    • 处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。

      image-20220412230751461

小结:图解

image-20220412225253349

2.3 流的API

  • Java的IO流共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的。
(抽象基类) 输入流 输出流
字节流(以字节为单位,读写数据) InputStream OutputStream
字符流(以字符为单位,读写数据) Reader Writer
  • 由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。

image-20220412230501953

常用节点流:  

  • 文件流: FileInputStream、FileOutputStrean、FileReader、FileWriter
  • 字节/字符数组流: ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter
    • 对数组进行处理的节点流(对应的不再是文件,而是内存中的一个数组)。

常用处理流:

  • 缓冲流:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
    • 作用:增加缓冲功能,避免频繁读写硬盘,进而提升读写效率。
  • 转换流:InputStreamReader、OutputStreamReader
    • 作用:实现字节流和字符流之间的转换。
  • 对象流:ObjectInputStream、ObjectOutputStream
    • 作用:提供直接读写Java对象功能

3. 节点流之一:FileReader\FileWriter

3.1 Reader与Writer

Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。不能操作图片,视频等非文本文件。

常见的文本文件有如下的格式:.txt、.java、.c、.cpp、.py等

注意:.doc、.xls、.ppt这些都不是文本文件。

3.1.1 字符输入流:Reader

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

  • public int read(): 从输入流读取一个字符。 虽然读取了一个字符,但是会自动提升为int类型。返回该字符的Unicode编码值。如果已经到达流末尾了,则返回-1。
  • public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。每次最多读取cbuf.length个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回-1。
  • public int read(char[] cbuf,int off,int len):从输入流中读取一些字符,并将它们存储到字符数组 cbuf中,从cbuf[off]开始的位置存储。每次最多读取len个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回-1。
  • public void close() :关闭此流并释放与此流相关联的任何系统资源。

注意:当完成流的操作时,必须调用close()方法,释放系统资源,否则会造成内存泄漏。

3.1.2 字符输出流:Writer

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

  • public void write(int c) :写出单个字符。
  • public void write(char[] cbuf):写出字符数组。
  • public void write(char[] cbuf, int off, int len):写出字符数组的某一部分。off:数组的开始索引;len:写出的字符个数。
  • public void write(String str):写出字符串。
  • public void write(String str, int off, int len) :写出字符串的某一部分。off:字符串的开始索引;len:写出的字符个数。
  • public void flush():刷新该流的缓冲。
  • public void close() :关闭此流。

注意:当完成流的操作时,必须调用close()方法,释放系统资源,否则会造成内存泄漏。

3.2 FileReader 与 FileWriter

3.2.1 FileReader

java.io.FileReader类用于读取字符文件,构造时使用系统默认的字符编码和默认字节缓冲区。

  • FileReader(File file): 创建一个新的 FileReader ,给定要读取的File对象。
  • FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称。

举例:读取hello.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
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
public class FileReaderWriterTest {

//实现方式1
@Test
public void test1() throws IOException {
//1. 创建File类的对象,对应着物理磁盘上的某个文件
File file = new File("hello.txt");
//2. 创建FileReader流对象,将File类的对象作为参数传递到FileReader的构造器中
FileReader fr = new FileReader(file);
//3. 通过相关流的方法,读取文件中的数据
// int data = fr.read(); //每调用一次读取一个字符
// while (data != -1) {
// System.out.print((char) data);
// data = fr.read();
// }
int data;
while ((data = fr.read()) != -1) {
System.out.print((char) data);
}

//4. 关闭相关的流资源,避免出现内存泄漏
fr.close();

}

//实现方式2:在方式1的基础上改进,使用try-catch-finally处理异常。保证流是可以关闭的
@Test
public void test2() {
FileReader fr = null;
try {
//1. 创建File类的对象,对应着物理磁盘上的某个文件
File file = new File("hello.txt");
//2. 创建FileReader流对象,将File类的对象作为参数传递到FileReader的构造器中
fr = new FileReader(file);
//3. 通过相关流的方法,读取文件中的数据
/*
* read():每次从对接的文件中读取一个字符。并将此字符返回。
* 如果返回值为-1,则表示文件到了末尾,可以不再读取。
* */
// int data = fr.read();
// while(data != -1){
// System.out.print((char)data);
// data = fr.read();
// }

int data;
while ((data = fr.read()) != -1) {
System.out.println((char) data);
}

} catch (IOException e) {
e.printStackTrace();
} finally {
//4. 关闭相关的流资源,避免出现内存泄漏
try {
if (fr != null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

//实现方式3:调用read(char[] cbuf),每次从文件中读取多个字符
@Test
public void test3() {
FileReader fr = null;
try {
//1. 创建File类的对象,对应着物理磁盘上的某个文件
File file = new File("hello.txt");
//2. 创建FileReader流对象,将File类的对象作为参数传递到FileReader的构造器中
fr = new FileReader(file);
//3. 通过相关流的方法,读取文件中的数据
char[] cbuf = new char[5];
/*
* read(char[] cbuf) : 每次将文件中的数据读入到cbuf数组中,并返回读入到数组中的
* 字符的个数。
* */
int len; //记录每次读入的字符的个数
while ((len = fr.read(cbuf)) != -1) {
//处理char[]数组即可
//错误:
// for(int i = 0;i < cbuf.length;i++){
// System.out.print(cbuf[i]);
// }
//错误:
// String str = new String(cbuf);
// System.out.print(str);
//正确:
// for(int i = 0;i < len;i++){
// System.out.print(cbuf[i]);
// }
//正确:
String str = new String(cbuf, 0, len);
System.out.print(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4. 关闭相关的流资源,避免出现内存泄漏
try {
if (fr != null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

不同实现方式的类比:

image-20220518095907714

3.2.2 FileWriter

java.io.FileWriter类用于写出字符到文件,构造时使用系统默认的字符编码和默认字节缓冲区。

  • FileWriter(File file): 创建一个新的 FileWriter,给定要读取的File对象。
  • FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。
  • FileWriter(File file,boolean append): 创建一个新的 FileWriter,指明是否在现有文件末尾追加内容。

举例:

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
public class FWWrite {
//注意:应该使用try-catch-finally处理异常。这里出于方便阅读代码,使用了throws的方式
@Test
public void test01()throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter(new File("fw.txt"));
// 写出数据
fw.write(97); // 写出第1个字符
fw.write('b'); // 写出第2个字符
fw.write('C'); // 写出第3个字符
fw.write(30000); // 写出第4个字符,中文编码表中30000对应一个汉字。

//关闭资源
fw.close();
}
//注意:应该使用try-catch-finally处理异常。这里出于方便阅读代码,使用了throws的方式
@Test
public void test02()throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter(new File("fw.txt"));
// 字符串转换为字节数组
char[] chars = "尚硅谷".toCharArray();

// 写出字符数组
fw.write(chars); // 尚硅谷

// 写出从索引1开始,2个字符。
fw.write(chars,1,2); // 硅谷

// 关闭资源
fw.close();
}
//注意:应该使用try-catch-finally处理异常。这里出于方便阅读代码,使用了throws的方式
@Test
public void test03()throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("fw.txt");
// 字符串
String msg = "尚硅谷";

// 写出字符数组
fw.write(msg); //尚硅谷

// 写出从索引1开始,2个字符。
fw.write(msg,1,2); // 硅谷

// 关闭资源
fw.close();
}

@Test
public void test04(){
FileWriter fw = null;
try {
//1. 创建File的对象
File file = new File("personinfo.txt");
//2. 创建FileWriter的对象,将File对象作为参数传递到FileWriter的构造器中
//如果输出的文件已存在,则会对现有的文件进行覆盖
fw = new FileWriter(file);
// fw = new FileWriter(file,false);
//如果输出的文件已存在,则会在现有的文件末尾写入数据
// fw = new FileWriter(file,true);

//3. 调用相关的方法,实现数据的写出操作
//write(String str) / write(char[] cbuf)
fw.write("I love you,");
fw.write("you love him.");
fw.write("so sad".toCharArray());
} catch (IOException e) {
e.printStackTrace();
} finally {
//4. 关闭资源,避免内存泄漏
try {
if (fw != null)
fw.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

3.2.3 小结

1
2
3
4
5
6
7
① 因为出现流资源的调用,为了避免内存泄漏,需要使用try-catch-finally处理异常
② 对于输入流来说,File类的对象必须在物理磁盘上存在,否则执行就会报FileNotFoundException。如果传入的是一个目录,则会报IOException异常。
对于输出流来说,File类的对象是可以不存在的。
> 如果File类的对象不存在,则可以在输出的过程中,自动创建File类的对象
> 如果File类的对象存在,
> 如果调用FileWriter(File file)或FileWriter(File file,false),输出时会新建File文件覆盖已有的文件
> 如果调用FileWriter(File file,true)构造器,则在现有的文件末尾追加写出内容。

3.3 关于flush(刷新)

因为内置缓冲区的原因,如果FileWriter不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush() 方法了。

  • flush() :刷新缓冲区,流对象可以继续使用。
  • close():先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。

注意:即便是flush()方法写出了数据,操作的最后还是要调用close方法,释放系统资源。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FWWriteFlush {
//注意:应该使用try-catch-finally处理异常。这里出于方便阅读代码,使用了throws的方式
@Test
public void test() throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("fw.txt");
// 写出数据,通过flush
fw.write('刷'); // 写出第1个字符
fw.flush();
fw.write('新'); // 继续写出第2个字符,写出成功
fw.flush();

// 写出数据,通过close
fw.write('关'); // 写出第1个字符
fw.close();
fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed
fw.close();
}
}

4. 节点流之二:FileInputStream\FileOutputStream

如果我们读取或写出的数据是非文本文件,则Reader、Writer就无能为力了,必须使用字节流。

4.1 InputStream和OutputStream

4.1.1 字节输入流:InputStream

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

  • public int read(): 从输入流读取一个字节。返回读取的字节值。虽然读取了一个字节,但是会自动提升为int类型。如果已经到达流末尾,没有数据可读,则返回-1。
  • public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。每次最多读取b.length个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1。
  • public int read(byte[] b,int off,int len):从输入流中读取一些字节数,并将它们存储到字节数组 b中,从b[off]开始存储,每次最多读取len个字节 。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1。
  • public void close() :关闭此输入流并释放与此流相关联的任何系统资源。

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

4.1.2 字节输出流:OutputStream

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

  • public void write(int b) :将指定的字节输出流。虽然参数为int类型四个字节,但是只会保留一个字节的信息写出。
  • public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
  • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。

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

4.2 FileInputStream 与 FileOutputStream

4.2.1 FileInputStream

java.io.FileInputStream类是文件输入流,从文件中读取字节。

  • FileInputStream(File file): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。
  • FileInputStream(String name): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。

举例:

1
2
//read.txt文件中的内容如下:
abcde

读取操作

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
public class FISRead {
//注意:应该使用try-catch-finally处理异常。这里出于方便阅读代码,使用了throws的方式
@Test
public void test() throws IOException {
// 使用文件名称创建流对象
FileInputStream fis = new FileInputStream("read.txt");
// 读取数据,返回一个字节
int read = fis.read();
System.out.println((char) read);
read = fis.read();
System.out.println((char) read);
read = fis.read();
System.out.println((char) read);
read = fis.read();
System.out.println((char) read);
read = fis.read();
System.out.println((char) read);
// 读取到末尾,返回-1
read = fis.read();
System.out.println(read);
// 关闭资源
fis.close();
/*
文件内容:abcde
输出结果:
a
b
c
d
e
-1
*/
}

@Test
public void test02()throws IOException{
// 使用文件名称创建流对象
FileInputStream fis = new FileInputStream("read.txt");
// 定义变量,保存数据
int b;
// 循环读取
while ((b = fis.read())!=-1) {
System.out.println((char)b);
}
// 关闭资源
fis.close();
}

@Test
public void test03()throws IOException{
// 使用文件名称创建流对象.
FileInputStream fis = new FileInputStream("read.txt"); // 文件中为abcde
// 定义变量,作为有效个数
int len;
// 定义字节数组,作为装字节数据的容器
byte[] b = new byte[2];
// 循环读取
while (( len= fis.read(b))!=-1) {
// 每次读取后,把数组变成字符串打印
System.out.println(new String(b));
}
// 关闭资源
fis.close();
/*
输出结果:
ab
cd
ed
最后错误数据`d`,是由于最后一次读取时,只读取一个字节`e`,数组中,
上次读取的数据没有被完全替换,所以要通过`len` ,获取有效的字节
*/
}

@Test
public void test04()throws IOException{
// 使用文件名称创建流对象.
FileInputStream fis = new FileInputStream("read.txt"); // 文件中为abcde
// 定义变量,作为有效个数
int len;
// 定义字节数组,作为装字节数据的容器
byte[] b = new byte[2];
// 循环读取
while (( len= fis.read(b))!=-1) {
// 每次读取后,把数组的有效字节部分,变成字符串打印
System.out.println(new String(b,0,len));// len 每次读取的有效字节个数
}
// 关闭资源
fis.close();
/*
输出结果:
ab
cd
e
*/
}
}

4.2.2 FileOutputStream

java.io.FileOutputStream类是文件输出流,用于将数据写出到文件。

  • public FileOutputStream(File file):创建文件输出流,写出由指定的 File对象表示的文件。
  • public FileOutputStream(String name): 创建文件输出流,指定的名称为写出文件。
  • public FileOutputStream(File file, boolean append): 创建文件输出流,指明是否在现有文件末尾追加内容。

举例:

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
import org.junit.Test;
import java.io.FileOutputStream;
import java.io.IOException;
public class FOSWrite {
//注意:应该使用try-catch-finally处理异常。这里出于方便阅读代码,使用了throws的方式
@Test
public void test01() throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 写出数据
fos.write(97); // 写出第1个字节
fos.write(98); // 写出第2个字节
fos.write(99); // 写出第3个字节
// 关闭资源
fos.close();
/* 输出结果:abc*/
}

@Test
public void test02()throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
// 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
fos.write(b,2,2);
// 关闭资源
fos.close();
}
//这段程序如果多运行几次,每次都会在原来文件末尾追加abcde
@Test
public void test03()throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt",true);
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
fos.write(b);
// 关闭资源
fos.close();
}

//使用FileInputStream\FileOutputStream,实现对文件的复制
@Test
public void test05() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//1. 造文件-造流
//复制图片:成功
// fis = new FileInputStream(new File("pony.jpg"));
// fos = new FileOutputStream(new File("pony_copy1.jpg"));
//复制文本文件:成功
fis = new FileInputStream(new File("hello.txt"));
fos = new FileOutputStream(new File("hello1.txt"));

//2. 复制操作(读、写)
byte[] buffer = new byte[1024];
int len;//每次读入到buffer中字节的个数
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
System.out.println("复制成功");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//3. 关闭资源
try {
if (fos != null)
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
if (fis != null)
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

}
}

4.3 练习

练习:实现图片加密操作。

提示:

image-20220413002723838

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
public class FileSecretTest {
//图片的加密
@Test
public void test1(){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
File file1 = new File("pony.jpg");
File file2 = new File("pony_secret.jpg");
fis = new FileInputStream(file1);
fos = new FileOutputStream(file2);
//方式2:每次读入一个字节数组,效率高
int len;
byte[] buffer = new byte[1024];
while((len = fis.read(buffer)) != -1){
for(int i = 0;i < len;i++){
buffer[i] = (byte) (buffer[i] ^ 5);
}
fos.write(buffer,0,len);
}
System.out.println("加密成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

//图片的解密
@Test
public void test2(){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
File file1 = new File("pony_secret.jpg");
File file2 = new File("pony_unsecret.jpg");
fis = new FileInputStream(file1);
fos = new FileOutputStream(file2);
//方式2:每次读入一个字节数组,效率高
int len;
byte[] buffer = new byte[1024];
while((len = fis.read(buffer)) != -1){
for(int i = 0;i < len;i++){
buffer[i] = (byte) (buffer[i] ^ 5);
}
fos.write(buffer,0,len);
}
System.out.println("解密成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

5. 处理流之一:缓冲流

  • 为了提高数据读写的速度,Java API提供了带缓冲功能的流类:缓冲流。

  • 缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:

    • 字节缓冲流BufferedInputStreamBufferedOutputStream
    • 字符缓冲流BufferedReaderBufferedWriter
  • 缓冲流的基本原理:在创建流对象时,内部会创建一个缓冲区数组(缺省使用8192个字节(8Kb)的缓冲区),通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

image-20220413002314063

image-20220514183413011

5.1 构造器

  • public BufferedInputStream(InputStream in) :创建一个 新的字节型的缓冲输入流。
  • public BufferedOutputStream(OutputStream out): 创建一个新的字节型的缓冲输出流。

代码举例:

1
2
3
4
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("abc.jpg"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("abc_copy.jpg"));
  • public BufferedReader(Reader in) :创建一个 新的字符型的缓冲输入流。
  • public BufferedWriter(Writer out): 创建一个新的字符型的缓冲输出流。

代码举例:

1
2
3
4
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));

5.2 效率测试

查询API,缓冲流读写方法与基本的流是一致的,我们通过复制大文件(375MB),测试它的效率。

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
//方法1:使用FileInputStream\FileOutputStream实现非文本文件的复制
public void copyFileWithFileStream(String srcPath,String destPath){
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//1. 造文件-造流
fis = new FileInputStream(new File(srcPath));
fos = new FileOutputStream(new File(destPath));

//2. 复制操作(读、写)
byte[] buffer = new byte[100];
int len;//每次读入到buffer中字节的个数
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
System.out.println("复制成功");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//3. 关闭资源
try {
if (fos != null)
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
if (fis != null)
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

@Test
public void test1(){
String srcPath = "C:\\Users\\shkstart\\Desktop\\01-复习.mp4";
String destPath = "C:\\Users\\shkstart\\Desktop\\01-复习2.mp4";

long start = System.currentTimeMillis();

copyFileWithFileStream(srcPath,destPath);

long end = System.currentTimeMillis();

System.out.println("花费的时间为:" + (end - start));//7677毫秒

}

//方法2:使用BufferedInputStream\BufferedOuputStream实现非文本文件的复制
public void copyFileWithBufferedStream(String srcPath,String destPath){
FileInputStream fis = null;
FileOutputStream fos = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
//1. 造文件
File srcFile = new File(srcPath);
File destFile = new File(destPath);
//2. 造流
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);

bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);

//3. 读写操作
int len;
byte[] buffer = new byte[100];
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
System.out.println("复制成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
//4. 关闭资源(如果有多个流,我们需要先关闭外面的流,再关闭内部的流)
try {
if (bos != null)
bos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
if (bis != null)
bis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}

}
}
@Test
public void test2(){
String srcPath = "C:\\Users\\shkstart\\Desktop\\01-复习.mp4";
String destPath = "C:\\Users\\shkstart\\Desktop\\01-复习2.mp4";

long start = System.currentTimeMillis();

copyFileWithBufferedStream(srcPath,destPath);

long end = System.currentTimeMillis();

System.out.println("花费的时间为:" + (end - start));//415毫秒

}

5.3 字符缓冲流特有方法

字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。

  • BufferedReader:public String readLine(): 读一行文字。
  • BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号。
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
public class BufferedIOLine {
@Test
public void testReadLine()throws IOException {
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("in.txt"));
// 定义字符串,保存读取的一行文字
String line;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
System.out.println(line);
}
// 释放资源
br.close();
}

@Test
public void testNewLine()throws IOException{
// 创建流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
// 写出数据
bw.write("尚");
// 写出换行
bw.newLine();
bw.write("硅");
bw.newLine();
bw.write("谷");
bw.newLine();
// 释放资源
bw.close();
}
}

说明:

  1. 涉及到嵌套的多个流时,如果都显式关闭的话,需要先关闭外层的流,再关闭内层的流

  2. 其实在开发中,只需要关闭最外层的流即可,因为在关闭外层流时,内层的流也会被关闭。

5.4 练习

练习1:分别使用节点流:FileInputStream、FileOutputStream和缓冲流:BufferedInputStream、BufferedOutputStream实现文本文件/图片/视频文件的复制。并比较二者在数据复制方面的效率。

练习2:

姓氏统计:一个文本文件中存储着北京所有高校在校生的姓名,格式如下:

1
2
3
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(new File("e:/name.txt")));
String value = null; // 临时接收文件中的字符串变量
StringBuffer buffer = new StringBuffer();
flag:
while ((value = br.readLine()) != null) { // 开始读取文件中的字符
char[] c = value.toCharArray();
for (int i = 0; i < c.length; i++) {
if (c[i] != ' ') {
buffer.append(String.valueOf(c[i]));
} else {
if (map.containsKey(buffer.toString())) {
int count = map.get(buffer.toString());
map.put(buffer.toString(), count + 1);
} else {
map.put(buffer.toString(), 1);
}
buffer.delete(0, buffer.length());
continue flag;
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Set<Map.Entry<String, Integer>> set = map.entrySet();
Iterator<Map.Entry<String, Integer>> it = set.iterator();
while (it.hasNext()) {
Map.Entry<String, Integer> end = (Map.Entry<String, Integer>) it.next();
System.out.println(end);
}

}

6. 处理流之二:转换流

6.1 问题引入

引入情况1:

使用FileReader 读取项目中的文本文件。由于IDEA设置中针对项目设置了UTF-8编码,当读取Windows系统中创建的文本文件时,如果Windows系统默认的是GBK编码,则读入内存中会出现乱码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.FileReader;
import java.io.IOException;
public class Problem {
public static void main(String[] args) throws IOException {
FileReader fileReader = new FileReader("E:\\File_GBK.txt");
int data;
while ((data = fileReader.read()) != -1) {
System.out.print((char)data);
}
fileReader.close();
}
}

输出结果:
���

那么如何读取GBK编码的文件呢?

引入情况2:

针对文本文件,现在使用一个字节流进行数据的读入,希望将数据显示在控制台上。此时针对包含中文的文本数据,可能会出现乱码。

6.2 转换流的理解

作用:转换流是字节与字符间的桥梁!

具体来说:

image-20220412231533768

6.3 InputStreamReader 与 OutputStreamWriter

  • InputStreamReader

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

    • 构造器

      • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
      • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。
    • 举例

      1
      2
      3
      4
      //使用默认字符集
      InputStreamReader isr1 = new InputStreamReader(new FileInputStream("in.txt"));
      //使用指定字符集
      InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");
    • 示例代码:

      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
      package com.atguigu.transfer;

      import java.io.FileInputStream;
      import java.io.IOException;
      import java.io.InputStreamReader;

      public class InputStreamReaderDemo {
      public static void main(String[] args) throws IOException {
      // 定义文件路径,文件为gbk编码
      String fileName = "E:\\file_gbk.txt";
      //方式1:
      // 创建流对象,默认UTF8编码
      InputStreamReader isr1 = new InputStreamReader(new FileInputStream(fileName));
      // 定义变量,保存字符
      int charData;
      // 使用默认编码字符流读取,乱码
      while ((charData = isr1.read()) != -1) {
      System.out.print((char)charData); // ��Һ�
      }
      isr1.close();

      //方式2:
      // 创建流对象,指定GBK编码
      InputStreamReader isr2 = new InputStreamReader(new FileInputStream(fileName) , "GBK");
      // 使用指定编码字符流读取,正常解析
      while ((charData = isr2.read()) != -1) {
      System.out.print((char)charData);// 大家好
      }
      isr2.close();
      }
      }
  • OutputStreamWriter

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

    • 构造器

      • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
      • OutputStreamWriter(OutputStream in,String charsetName): 创建一个指定字符集的字符流。
    • 举例:

      1
      2
      3
      4
      //使用默认字符集
      OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt"));
      //使用指定的字符集
      OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK");
    • 示例代码:

      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
      package com.atguigu.transfer;

      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.io.OutputStreamWriter;

      public class OutputStreamWriterDemo {
      public static void main(String[] args) throws IOException {
      // 定义文件路径
      String FileName = "E:\\out_utf8.txt";
      // 创建流对象,默认UTF8编码
      OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName));
      // 写出数据
      osw.write("你好"); // 保存为6个字节
      osw.close();

      // 定义文件路径
      String FileName2 = "E:\\out_gbk.txt";
      // 创建流对象,指定GBK编码
      OutputStreamWriter osw2 = new OutputStreamWriter(new
      FileOutputStream(FileName2),"GBK");
      // 写出数据
      osw2.write("你好");// 保存为4个字节
      osw2.close();
      }
      }

6.4 字符编码和字符集

6.4.1 编码与解码

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码

字符编码(Character Encoding) : 就是一套自然语言的字符与二进制数之间的对应规则。

编码表:生活中文字和计算机中二进制的对应规则

乱码的情况:按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

1
2
3
编码:字符(人能看懂的)--字节(人看不懂的)

解码:字节(人看不懂的)-->字符(人能看懂的)

6.4.2 字符集

  • 字符集Charset:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。
  • 计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。

可见,当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。

  • ASCII字符集

    • ASCII码(American Standard Code for Information Interchange,美国信息交换标准代码):上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码。
    • ASCII码用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。
    • 基本的ASCII字符集,使用7位(bits)表示一个字符(最前面的1位统一规定为0),共128个字符。比如:空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。
    • 缺点:不能表示所有字符。
  • ISO-8859-1字符集

    • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰语、德语、意大利语、葡萄牙语等
    • ISO-8859-1使用单字节编码,兼容ASCII编码。
  • GBxxx字符集

    • GB就是国标的意思,是为了显示中文而设计的一套字符集。
    • GB2312:简体中文码表。一个小于127的字符的意义与原来相同,即向下兼容ASCII码。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,这就是常说的”全角”字符,而原来在127号以下的那些符号就叫”半角”字符了。
    • GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。
    • GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。
  • Unicode字符集

    • Unicode编码为表达任意语言的任意字符而设计,也称为统一码、标准万国码。Unicode 将世界上所有的文字用2个字节统一进行编码,为每个字符设定唯一的二进制编码,以满足跨语言、跨平台进行文本处理的要求。
    • Unicode 的缺点:这里有三个问题:
      • 第一,英文字母只用一个字节表示就够了,如果用更多的字节存储是极大的浪费
      • 第二,如何才能区别Unicode和ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?
      • 第三,如果和GBK等双字节编码方式一样,用最高位是1或0表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符
    • Unicode在很长一段时间内无法推广,直到互联网的出现,为解决Unicode如何在网络上传输的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现。具体来说,有三种编码方案,UTF-8、UTF-16和UTF-32。
  • UTF-8字符集

    • Unicode是字符集,UTF-8、UTF-16、UTF-32是三种将数字转换到程序数据的编码方案。顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。其中,UTF-8 是在互联网上使用最广的一种 Unicode 的实现方式。
    • 互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。UTF-8 是一种变长的编码方式。它使用1-4个字节为每个字符编码,编码规则:
      1. 128个US-ASCII字符,只需一个字节编码。
      2. 拉丁文等字符,需要二个字节编码。
      3. 大部分常用字(含中文),使用三个字节编码。
      4. 其他极少使用的Unicode辅助字符,使用四字节编码。
  • 举例

Unicode符号范围 | UTF-8编码方式

1
2
3
4
5
6
7
8
9
10
11
(十六进制)           | (二进制)

————————————————————|—–—–—–—–—–—–—–—–—–—–—–—–—–—–

0000 0000-0000 007F | 0xxxxxxx(兼容原来的ASCII)

0000 0080-0000 07FF | 110xxxxx 10xxxxxx

0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx

0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

image-20220525164636164

  • 小结

字符集

注意:在中文操作系统上,ANSI(美国国家标准学会、AMERICAN NATIONAL STANDARDS INSTITUTE: ANSI)编码即为GBK;在英文操作系统上,ANSI编码即为ISO-8859-1。

6.5 练习

把当前module下的《康师傅的话.txt》字符编码为GBK,复制到电脑桌面目录下的《寄语.txt》,
字符编码为UTF-8。

在当前module下的文本内容:

1
2
3
4
5
6
7
六项精进:
(一)付出不亚于任何人的努力
(二)要谦虚,不要骄傲
(三)要每天反省
(四)活着,就要感谢
(五)积善行、思利他
(六)不要有感性的烦恼

代码:

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
public class InputStreamReaderDemo {

@Test
public void test() {
InputStreamReader isr = null;
OutputStreamWriter osw = null;
try {
isr = new InputStreamReader(new FileInputStream("康师傅的话.txt"),"gbk");
osw = new OutputStreamWriter(new FileOutputStream("C:\\Users\\shkstart\\Desktop\\寄语.txt"),"utf-8");

char[] cbuf = new char[1024];
int len;
while ((len = isr.read(cbuf)) != -1) {
osw.write(cbuf, 0, len);
osw.flush();
}
System.out.println("文件复制完成");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (isr != null)
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (osw != null)
osw.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}

}

7. 处理流之三/四:数据流、对象流

7.1 数据流与对象流说明

如果需要将内存中定义的变量(包括基本数据类型或引用数据类型)保存在文件中,那怎么办呢?

1
2
3
4
5
6
7
8
int age = 300;
char gender = '男';
int energy = 5000;
double price = 75.5;
boolean relive = true;

String name = "巫师";
Student stu = new Student("张三",23,89);

Java提供了数据流和对象流来处理这些类型的数据:

  • 数据流:DataOutputStream、DataInputStream

    • DataOutputStream:允许应用程序将基本数据类型、String类型的变量写入输出流中

    • DataInputStream:允许应用程序以与机器无关的方式从底层输入流中读取基本数据类型、String类型的变量。

  • 对象流DataInputStream中的方法:

1
2
3
4
5
byte readByte()                short readShort()
int readInt() long readLong()
float readFloat() double readDouble()
char readChar() boolean readBoolean()
String readUTF() void readFully(byte[] b)
  • 对象流DataOutputStream中的方法:将上述的方法的read改为相应的write即可。
  • 数据流的弊端:只支持Java基本数据类型和字符串的读写,而不支持其它Java对象的类型。而ObjectOutputStream和ObjectInputStream既支持Java基本数据类型的数据读写,又支持Java对象的读写,所以重点介绍对象流ObjectOutputStream和ObjectInputStream。
  • 对象流:ObjectOutputStream、ObjectInputStream
    • ObjectOutputStream:将 Java 基本数据类型和对象写入字节输出流中。通过在流中使用文件可以实现Java各种基本数据类型的数据以及对象的持久存储。
    • ObjectInputStream:ObjectInputStream 对以前使用 ObjectOutputStream 写出的基本数据类型的数据和对象进行读入操作,保存在内存中。

说明:对象流的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。

7.2 对象流API

ObjectOutputStream中的构造器:

public ObjectOutputStream(OutputStream out): 创建一个指定的ObjectOutputStream。

1
2
FileOutputStream fos = new FileOutputStream("game.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);

ObjectOutputStream中的方法:

  • public void writeBoolean(boolean val):写出一个 boolean 值。
  • public void writeByte(int val):写出一个8位字节
  • public void writeShort(int val):写出一个16位的 short 值
  • public void writeChar(int val):写出一个16位的 char 值
  • public void writeInt(int val):写出一个32位的 int 值
  • public void writeLong(long val):写出一个64位的 long 值
  • public void writeFloat(float val):写出一个32位的 float 值。
  • public void writeDouble(double val):写出一个64位的 double 值
  • public void writeUTF(String str):将表示长度信息的两个字节写入输出流,后跟字符串 s 中每个字符的 UTF-8 修改版表示形式。根据字符的值,将字符串 s 中每个字符转换成一个字节、两个字节或三个字节的字节组。注意,将 String 作为基本数据写入流中与将它作为 Object 写入流中明显不同。 如果 s 为 null,则抛出 NullPointerException。
  • public void writeObject(Object obj):写出一个obj对象
  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源

ObjectInputStream中的构造器:

public ObjectInputStream(InputStream in): 创建一个指定的ObjectInputStream。

1
2
FileInputStream fis = new FileInputStream("game.dat");
ObjectInputStream ois = new ObjectInputStream(fis);

ObjectInputStream中的方法:

  • public boolean readBoolean():读取一个 boolean 值
  • public byte readByte():读取一个 8 位的字节
  • public short readShort():读取一个 16 位的 short 值
  • public char readChar():读取一个 16 位的 char 值
  • public int readInt():读取一个 32 位的 int 值
  • public long readLong():读取一个 64 位的 long 值
  • public float readFloat():读取一个 32 位的 float 值
  • public double readDouble():读取一个 64 位的 double 值
  • public String readUTF():读取 UTF-8 修改版格式的 String
  • public void readObject(Object obj):读入一个obj对象
  • public void close() :关闭此输入流并释放与此流相关联的任何系统资源

7.3 认识对象序列化机制

1、何为对象序列化机制?

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。

  • 序列化过程:用一个字节序列可以表示一个对象,该字节序列包含该对象的类型对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
  • 反序列化过程:该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化对象的数据对象的类型对象中存储的数据信息,都可以用来在内存中创建对象。

2、序列化机制的重要性

序列化是 RMI(Remote Method Invoke、远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。

序列化的好处,在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原。

3、实现原理

  • 序列化:用ObjectOutputStream类保存基本类型数据或对象的机制。方法为:

    • public final void writeObject (Object obj) : 将指定的对象写出。
  • 反序列化:用ObjectInputStream类读取基本类型数据或对象的机制。方法为:

    • public final Object readObject () : 读取一个对象。

7.4 如何实现序列化机制

如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现java.io.Serializable 接口。Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException

  • 如果对象的某个属性也是引用数据类型,那么如果该属性也要序列化的话,也要实现Serializable 接口
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。
  • 静态(static)变量的值不会序列化。因为静态变量的值不属于某个对象。

举例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
import org.junit.Test;
import java.io.*;
public class ReadWriteDataOfAnyType {
@Test
public void save() throws IOException {
String name = "巫师";
int age = 300;
char gender = '男';
int energy = 5000;
double price = 75.5;
boolean relive = true;

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("game.dat"));
oos.writeUTF(name);
oos.writeInt(age);
oos.writeChar(gender);
oos.writeInt(energy);
oos.writeDouble(price);
oos.writeBoolean(relive);
oos.close();
}
@Test
public void reload()throws IOException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("game.dat"));
String name = ois.readUTF();
int age = ois.readInt();
char gender = ois.readChar();
int energy = ois.readInt();
double price = ois.readDouble();
boolean relive = ois.readBoolean();

System.out.println(name+"," + age + "," + gender + "," + energy + "," + price + "," + relive);

ois.close();
}
}

举例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
import java.io.Serializable;
public class Employee implements Serializable {
//static final long serialVersionUID = 23234234234L;
public static String company; //static修饰的类变量,不会被序列化
public String name;
public String address;
public transient int age; // transient瞬态修饰成员,不会被序列化

public Employee(String name, String address, int age) {
this.name = name;
this.address = address;
this.age = age;
}

public static String getCompany() {
return company;
}

public static void setCompany(String company) {
Employee.company = company;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
", age=" + age +
", company=" + company +
'}';
}
}
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
import org.junit.Test;
import java.io.*;
public class ReadWriteObject {
@Test
public void save() throws IOException {
Employee.setCompany("尚硅谷");
Employee e = new Employee("小谷姐姐", "宏福苑", 23);
// 创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employee.dat"));
// 写出对象
oos.writeObject(e);
// 释放资源
oos.close();
System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年龄没有被序列化。
}

@Test
public void reload() throws IOException, ClassNotFoundException {
// 创建反序列化流
FileInputStream fis = new FileInputStream("employee.dat");
ObjectInputStream ois = new ObjectInputStream(fis);
// 读取一个对象
Employee e = (Employee) ois.readObject();
// 释放资源
ois.close();
fis.close();

System.out.println(e);
}
}

举例3:如果有多个对象需要序列化,则可以将对象放到集合中,再序列化集合对象即可。

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
import org.junit.Test;
import java.io.*;
import java.util.ArrayList;
public class ReadWriteCollection {
@Test
public void save() throws IOException {
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee("张三", "宏福苑", 23));
list.add(new Employee("李四", "白庙", 24));
list.add(new Employee("王五", "平西府", 25));
// 创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employees.dat"));
// 写出对象
oos.writeObject(list);
// 释放资源
oos.close();
}

@Test
public void reload() throws IOException, ClassNotFoundException {
// 创建反序列化流
FileInputStream fis = new FileInputStream("employees.dat");
ObjectInputStream ois = new ObjectInputStream(fis);
// 读取一个对象
ArrayList<Employee> list = (ArrayList<Employee>) ois.readObject();
// 释放资源
ois.close();
fis.close();

System.out.println(list);
}
}

7.5 反序列化失败问题

问题1:

对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException 异常。

问题2:

当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。发生这个异常的原因如下:

  • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
  • 该类包含未知数据类型

解决办法:

Serializable 接口给需要序列化的类,提供了一个序列版本号:serialVersionUID 。凡是实现 Serializable接口的类都应该有一个表示序列化版本标识符的静态变量:

1
static final long serialVersionUID = 234242343243L; //它的值由程序员随意指定即可。
  • serialVersionUID用来表明类的不同版本间的兼容性。简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
  • 如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。因此,建议显式声明。
  • 如果声明了serialVersionUID,即使在序列化完成之后修改了类导致类重新编译,则原来的数据也能正常反序列化,只是新增的字段值是默认值而已。
1
2
3
4
5
6
import java.io.Serializable;
public class Employee implements Serializable {
private static final long serialVersionUID = 1324234L; //增加serialVersionUID

//其它结构:略
}

7.6 面试题&练习

面试题:谈谈你对java.io.Serializable接口的理解,我们知道它用于序列化,是空方法接口,还有其它认识吗?

1
2
3
实现了Serializable接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异。换句话说,可以先在Windows机器上创建一个对象,对其序列化,然后通过网络发给一台Unix机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。

由于大部分作为参数的类如String、Integer等都实现了java.io.Serializable的接口,也可以利用多态的性质,作为参数使接口更灵活。

练习:

  • 需求说明:

    • 网上购物时某用户填写订单,订单内容为产品列表,保存在“save.bin”中。
    • 运行时,如果不存在“save.bin”,则进行新订单录入,如果存在,则显示并计算客户所需付款。
  • 分析:

    • 编写Save()方法保存对象到“save.bin”
    • 编写Load()方法获得对象,计算客户所需付款
image-20220503123603551

8. 其他流的使用

8.1 标准输入、输出流

  • System.in和System.out分别代表了系统标准的输入和输出设备
  • 默认输入设备是:键盘,输出设备是:显示器
  • System.in的类型是InputStream
  • System.out的类型是PrintStream,其是OutputStream的子类FilterOutputStream 的子类
  • 重定向:通过System类的setIn,setOut方法对默认设备进行改变。
    • public static void setIn(InputStream in)
    • public static void setOut(PrintStream out)

举例:

从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,直至当输入“e”或者“exit”时,退出程序。

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
System.out.println("请输入信息(退出输入e或exit):");
// 把"标准"输入流(键盘输入)这个字节流包装成字符流,再包装成缓冲流
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = null;
try {
while ((s = br.readLine()) != null) { // 读取用户输入的一行数据 --> 阻塞程序
if ("e".equalsIgnoreCase(s) || "exit".equalsIgnoreCase(s)) {
System.out.println("安全退出!!");
break;
}
// 将读取到的整行字符串转成大写输出
System.out.println("-->:" + s.toUpperCase());
System.out.println("继续输入信息");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close(); // 关闭过滤流时,会自动关闭它包装的底层节点流
}
} catch (IOException e) {
e.printStackTrace();
}
}

拓展:

System类中有三个常量对象:System.out、System.in、System.err

查看System类中这三个常量对象的声明:

1
2
3
public final static InputStream in = null;
public final static PrintStream out = null;
public final static PrintStream err = null;

奇怪的是,

  • 这三个常量对象有final声明,但是却初始化为null。final声明的常量一旦赋值就不能修改,那么null不会空指针异常吗?
  • 这三个常量对象为什么要小写?final声明的常量按照命名规范不是应该大写吗?
  • 这三个常量的对象有set方法?final声明的常量不是不能修改值吗?set方法是如何修改它们的值的?
1
final声明的常量,表示在Java的语法体系中它们的值是不能修改的,而这三个常量对象的值是由C/C++等系统函数进行初始化和修改值的,所以它们故意没有用大写,也有set方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void setOut(PrintStream out) {
checkIO();
setOut0(out);
}
public static void setErr(PrintStream err) {
checkIO();
setErr0(err);
}
public static void setIn(InputStream in) {
checkIO();
setIn0(in);
}
private static void checkIO() {
SecurityManager sm = getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setIO"));
}
}
private static native void setIn0(InputStream in);
private static native void setOut0(PrintStream out);
private static native void setErr0(PrintStream err);

练习:

Create a program named MyInput.java: Contain the methods for reading int, double, float, boolean, short, byte and String values from the keyboard.

image-20220412232254819
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
package com.atguigu.java;
// MyInput.java: Contain the methods for reading int, double, float, boolean, short, byte and
// string values from the keyboard

import java.io.*;

public class MyInput {
// Read a string from the keyboard
public static String readString() {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

// Declare and initialize the string
String string = "";

// Get the string from the keyboard
try {
string = br.readLine();

} catch (IOException ex) {
System.out.println(ex);
}

// Return the string obtained from the keyboard
return string;
}

// Read an int value from the keyboard
public static int readInt() {
return Integer.parseInt(readString());
}

// Read a double value from the keyboard
public static double readDouble() {
return Double.parseDouble(readString());
}

// Read a byte value from the keyboard
public static double readByte() {
return Byte.parseByte(readString());
}

// Read a short value from the keyboard
public static double readShort() {
return Short.parseShort(readString());
}

// Read a long value from the keyboard
public static double readLong() {
return Long.parseLong(readString());
}

// Read a float value from the keyboard
public static double readFloat() {
return Float.parseFloat(readString());
}
}

8.2 打印流

  • 实现将基本数据类型的数据格式转化为字符串输出。
  • 打印流:PrintStreamPrintWriter

    • 提供了一系列重载的print()和println()方法,用于多种数据类型的输出

      image-20220131021502089

      image-20220131021528397

    • PrintStream和PrintWriter的输出不会抛出IOException异常

    • PrintStream和PrintWriter有自动flush功能

    • PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。

    • System.out返回的是PrintStream的实例

  • 构造器

    • PrintStream(File file) :创建具有指定文件且不带自动行刷新的新打印流。
    • PrintStream(File file, String csn):创建具有指定文件名称和字符集且不带自动行刷新的新打印流。
    • PrintStream(OutputStream out) :创建新的打印流。
    • PrintStream(OutputStream out, boolean autoFlush):创建新的打印流。 autoFlush如果为 true,则每当写入 byte 数组、调用其中一个 println 方法或写入换行符或字节 (‘\n’) 时都会刷新输出缓冲区。
    • PrintStream(OutputStream out, boolean autoFlush, String encoding) :创建新的打印流。
    • PrintStream(String fileName):创建具有指定文件名称且不带自动行刷新的新打印流。
    • PrintStream(String fileName, String csn) :创建具有指定文件名称和字符集且不带自动行刷新的新打印流。
  • 代码举例1

1
2
3
4
5
6
7
8
9
10
11
import java.io.FileNotFoundException;
import java.io.PrintStream;
public class TestPrintStream {
public static void main(String[] args) throws FileNotFoundException {
PrintStream ps = new PrintStream("io.txt");
ps.println("hello");
ps.println(1);
ps.println(1.5);
ps.close();
}
}
  • 代码举例2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(new File("D:\\IO\\text.txt"));
// 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
ps = new PrintStream(fos, true);
if (ps != null) {// 把标准输出流(控制台输出)改成文件
System.setOut(ps);
}
for (int i = 0; i <= 255; i++) { // 输出ASCII字符
System.out.print((char) i);
if (i % 50 == 0) { // 每50个数据一行
System.out.println(); // 换行
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ps != null) {
ps.close();
}
}
  • 代码举例3:自定义一个日志工具
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 Logger {
/*
记录日志的方法。
*/
public static void log(String msg) {
try {
// 指向一个日志文件
PrintStream out = new PrintStream(new FileOutputStream("log.txt", true));
// 改变输出方向
System.setOut(out);
// 日期当前时间
Date nowTime = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(nowTime);

System.out.println(strTime + ": " + msg);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
public class LogTest {
public static void main(String[] args) {
//测试工具类是否好用
Logger.log("调用了System类的gc()方法,建议启动垃圾回收");
Logger.log("调用了TeamView的addMember()方法");
Logger.log("用户尝试进行登录,验证失败");
}
}

8.3 Scanner类

构造方法

  • Scanner(File source) :构造一个新的 Scanner,它生成的值是从指定文件扫描的。
  • Scanner(File source, String charsetName) :构造一个新的 Scanner,它生成的值是从指定文件扫描的。
  • Scanner(InputStream source) :构造一个新的 Scanner,它生成的值是从指定的输入流扫描的。
  • Scanner(InputStream source, String charsetName) :构造一个新的 Scanner,它生成的值是从指定的输入流扫描的。

常用方法:

  • boolean hasNextXxx(): 通过nextXxx(),此扫描器输入信息中的下一个标记可以解释为默认基数中的一个 Xxx 值,则返回 true。
  • Xxx nextXxx(): 将输入信息的下一个标记扫描为一个Xxx
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
import org.junit.Test;
import java.io.*;
import java.util.Scanner;
public class TestScanner {
@Test
public void test01() throws IOException {
Scanner input = new Scanner(System.in);
PrintStream ps = new PrintStream("1.txt");
while(true){
System.out.print("请输入一个单词:");
String str = input.nextLine();
if("stop".equals(str)){
break;
}
ps.println(str);
}
input.close();
ps.close();
}

@Test
public void test2() throws IOException {
Scanner input = new Scanner(new FileInputStream("1.txt"));
while(input.hasNextLine()){
String str = input.nextLine();
System.out.println(str);
}
input.close();
}
}

9. apache-common包的使用

9.1 介绍

IO技术开发中,代码量很大,而且代码的重复率较高,为此Apache软件基金会,开发了IO技术的工具类commonsIO,大大简化了IO开发。

Apahce软件基金会属于第三方,(Oracle公司第一方,我们自己第二方,其他都是第三方)我们要使用第三方开发好的工具,需要添加jar包。

9.2 导包及举例

  • 在导入commons-io-2.5.jar包之后,内部的API都可以使用。

    image-20220416004246436

  • IOUtils类的使用

1
2
- 静态方法:IOUtils.copy(InputStream in,OutputStream out)传递字节流,实现文件复制。
- 静态方法:IOUtils.closeQuietly(任意流对象)悄悄的释放资源,自动处理close()方法抛出的异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test01 {
public static void main(String[] args)throws Exception {
//- 静态方法:IOUtils.copy(InputStream in,OutputStream out)传递字节流,实现文件复制。
IOUtils.copy(new FileInputStream("E:\\Idea\\io\\1.jpg"),new FileOutputStream("E:\\Idea\\io\\file\\柳岩.jpg"));
//- 静态方法:IOUtils.closeQuietly(任意流对象)悄悄的释放资源,自动处理close()方法抛出的异常。
/* FileWriter fw = null;
try {
fw = new FileWriter("day21\\io\\writer.txt");
fw.write("hahah");
} catch (IOException e) {
e.printStackTrace();
}finally {
IOUtils.closeQuietly(fw);
}*/
}
}
  • FileUtils类的使用
1
2
3
4
5
6
7
8
- 静态方法:void copyDirectoryToDirectory(File src,File dest):整个目录的复制,自动进行递归遍历
参数:
src:要复制的文件夹路径
dest:要将文件夹粘贴到哪里去

- 静态方法:void writeStringToFile(File file,String content):将内容content写入到file中
- 静态方法:String readFileToString(File file):读取文件内容,并返回一个String
- 静态方法:void copyFile(File srcFile,File destFile):文件复制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test02 {
public static void main(String[] args) {
try {
//- 静态方法:void copyDirectoryToDirectory(File src,File dest);
FileUtils.copyDirectoryToDirectory(new File("E:\\Idea\\io\\aa"),new File("E:\\Idea\\io\\file"));

//- 静态方法:writeStringToFile(File file,String str)
FileUtils.writeStringToFile(new File("day21\\io\\commons.txt"),"柳岩你好");

//- 静态方法:String readFileToString(File file)
String s = FileUtils.readFileToString(new File("day21\\io\\commons.txt"));
System.out.println(s);
//- 静态方法:void copyFile(File srcFile,File destFile)
FileUtils.copyFile(new File("io\\yangm.png"),new File("io\\yangm2.png"));
System.out.println("复制成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}

JAVA-数据结构与集合源码

第14章_数据结构与集合源码

本章专题与脉络

第3阶段:Java高级应用-第14章

1. 数据结构剖析

我们举一个形象的例子来理解数据结构的作用:

image-20220412011531879

战场:程序运行所需的软件、硬件环境

敌人:项目或模块的功能需求

指挥官:编写程序的程序员

士兵和装备:一行一行的代码

战术和策略:数据结构

image-20220412011555025

上图:没有战术,打仗事倍功半

image-20220412011600845

上图:有战术,打仗事半功倍

总结:简单来说,数据结构,就是一种程序设计优化的方法论,研究数据的逻辑结构物理结构以及它们之间相互关系,并对这种结构定义相应的运算目的是加快程序的执行速度、减少内存占用的空间。

具体研究对象如下:

1.1 研究对象一:数据间逻辑关系

数据的逻辑结构指反映数据元素之间的逻辑关系,而与数据的存储无关,是独立于计算机的。

  • 集合结构:数据结构中的元素之间除了“同属一个集合” 的相互关系外,别无其他关系。集合元素之间没有逻辑关系。
  • 线性结构:数据结构中的元素存在一对一的相互关系。比如:排队。结构中必须存在唯一的首元素和唯一的尾元素。体现为:一维数组、链表、栈、队列
  • 树形结构:数据结构中的元素存在一对多的相互关系。比如:家谱、文件系统、组织架构
  • 图形结构:数据结构中的元素存在多对多的相互关系。比如:全国铁路网、地铁图
image-20220824011022664

1.2 研究对象二:数据的存储结构(或物理结构)

数据的物理结构/存储结构:包括数据元素的表示关系的表示。数据的存储结构是逻辑结构用计算机语言的实现,它依赖于计算机语言。

结构1:顺序结构

  • 顺序结构就是使用一组连续的存储单元依次存储逻辑上相邻的各个元素。

  • 优点: 只需要申请存放数据本身的内存空间即可,支持下标访问,也可以实现随机访问。

  • 缺点: 必须静态分配连续空间,内存空间的利用率比较低。插入或删除可能需要移动大量元素,效率比较低

image-20220521100746910

结构2:链式结构

  • 不使用连续的存储空间存放结构的元素,而是为每一个元素构造一个节点。节点中除了存放数据本身以外,还需要存放指向下一个节点的指针。
  • 优点:不采用连续的存储空间导致内存空间利用率比较高,克服顺序存储结构中预知元素个数的缺点。插入或删除元素时,不需要移动大量的元素。
  • 缺点:需要额外的空间来表达数据之间的逻辑关系,不支持下标访问和随机访问。

image-20220521103734742

结构3:索引结构

  • 除建立存储节点信息外,还建立附加的索引表来记录每个元素节点的地址。索引表由若干索引项组成。索引项的一般形式是:(关键字,地址)。

  • 优点:用节点的索引号来确定结点存储地址,检索速度快。

  • 缺点: 增加了附加的索引表,会占用较多的存储空间。在增加和删除数据时要修改索引表,因而会花费较多的时间。

    <img src=”F:/JAVA%25E8%25B7%25Aimage-20220521115200921

结构4:散列结构

  • 根据元素的关键字直接计算出该元素的存储地址,又称为Hash存储。
  • 优点:检索、增加和删除结点的操作都很快。
  • 缺点:不支持排序,一般比用线性表存储需要更多的空间,并且记录的关键字不能重复。
image-20220521115734571

1.3 研究对象三:运算结构

施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。

  • 分配资源,建立结构,释放资源
  • 插入和删除
  • 获取和遍历
  • 修改和排序

1.4 小结

数据结构

2. 一维数组

2.1 数组的特点

  • 在Java中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型。
1
2
3
4
5
//只声明了类型和长度
数据类型[] 数组名称 = new 数据类型[数组长度];

//声明了类型,初始化赋值,大小由元素个数决定
数据类型[] 数组名称 = {数组元素1,数组元素2,......}

例如:整型数组

1563432676234

例如:对象数组

1563432696340
  • 物理结构特点:
    • 申请内存:一次申请一大段连续的空间,一旦申请到了,内存就固定了。
    • 不能动态扩展(初始化给大了,浪费;给小了,不够用),插入快,删除和查找慢。
    • 存储特点:所有数据存储在这个连续的空间中,数组中的每一个元素都是一个具体的数据(或对象),所有数据都紧密排布,不能有间隔。
  • 具体的,如下图:
数据结构-一维数组

2.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
package com.atguigu01.overview.array;

/**
* @author 尚硅谷-宋红康
* @create 14:39
*/
class Array {
private Object[] elementData;

private int size;

public Array(int capacity){
elementData = new Object[capacity];
}

/**
* 添加元素
* @param value
*/
public void add(Object value){
if(size >= elementData.length){
throw new RuntimeException("数组已满,不可添加");
}
elementData[size] = value;
size++;
}

/**
* 查询元素value在数组中的索引位置
* @param value
* @return
*/
public int find(Object value){
for (int i = 0; i < size; i++) {
if(elementData[i].equals(value)){
return i;
}
}
return -1;
}

/**
* 从当前数组中移除首次出现的value元素
* @param value
* @return
*/
public boolean delete(Object value){
int index = find(value);
if(index == -1){
return false;
}

for(int i = index;i < size - 1;i++){
elementData[i] = elementData[i + 1];
}
elementData[size - 1] = null;
size--;
return true;
}

/**
* 将数组中首次出现的oldValue替换为newValue
* @param oldValue
* @param newValue
* @return
*/
public boolean update(Object oldValue,Object newValue){
int index = find(oldValue);
if(index == -1){
return false;
}
elementData[index] = newValue;
return true;

}

/**
* 遍历数组中所有数据
*/
public void print(){
System.out.print("{");
for (int i = 0; i < size; i++) {
if(i == size - 1){
System.out.println(elementData[i] + "}");
break;
}
System.out.print(elementData[i] + ",");
}
}
}

//测试类
public class ArrayTest {
public static void main(String[] args) {
Array arr = new Array(10);

arr.add(123);
arr.add("AA");
arr.add(345);
arr.add(345);
arr.add("BB");

arr.delete(345);

arr.update(345,444);

arr.print();
}
}

3. 链表

3.1 链表的特点

  • 逻辑结构:线性结构
  • 物理结构:不要求连续的存储空间
  • 存储特点:链表由一系列结点node(链表中每一个元素称为结点)组成,结点可以在代码执行过程中动态创建。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域

image-20220511113744772

  • 常见的链表结构有如下的形式:

1563448858180

数据结构-链表

3.2 自定义链表

3.2.1 自定义单向链表

image-20221028195106363

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
单链表中的节点。
节点是单向链表中基本的单元。
每一个节点Node都有两个属性:
一个属性:是存储的数据。
另一个属性:是下一个节点的内存地址。
*/
public class Node {

// 存储的数据
Object data;

// 下一个节点的内存地址
Node next;

public Node(){

}

public Node(Object data, Node next){
this.data = data;
this.next = next;
}
}
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
/*
链表类(单向链表)
*/
public class Link<E> {

// 头节点
Node header;

private int size = 0;

public int size(){
return size;
}

// 向链表中添加元素的方法(向末尾添加)
public void add(E data){
//public void add(Object data){
// 创建一个新的节点对象
// 让之前单链表的末尾节点next指向新节点对象。
// 有可能这个元素是第一个,也可能是第二个,也可能是第三个。
if(header == null){
// 说明还没有节点。
// new一个新的节点对象,作为头节点对象。
// 这个时候的头节点既是一个头节点,又是一个末尾节点。
header = new Node(data, null);
}else {
// 说明头不是空!
// 头节点已经存在了!
// 找出当前末尾节点,让当前末尾节点的next是新节点。
Node currentLastNode = findLast(header);
currentLastNode.next = new Node(data, null);
}
size++;
}

/**
* 专门查找末尾节点的方法。
*/
private Node findLast(Node node) {
if(node.next == null) {
// 如果一个节点的next是null
// 说明这个节点就是末尾节点。
return node;
}
// 程序能够到这里说明:node不是末尾节点。
return findLast(node.next); // 递归算法!
}

/*// 删除链表中某个数据的方法
public void remove(Object obj){
//略
}

// 修改链表中某个数据的方法
public void modify(Object newObj){
//略
}

// 查找链表中某个元素的方法。
public int find(Object obj){
//略
}*/
}

3.2.2 自定义双向链表

image-20220514165707977

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
双向链表中的节点。
*/
public class Node<E> {
Node prev;
E data;
Node next;

Node(Node prev, E data, Node next) {
this.prev = prev;
this.data = data;
this.next = next;
}
}
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
/**
* 链表类(双向链表)
* @author 尚硅谷-宋红康
* @create 15:05
*/
public class MyLinkedList<E> implements Iterable<E>{
private Node first; //链表的首元素
private Node last; //链表的尾元素
private int total;

public void add(E e){
Node newNode = new Node(last, e, null);

if(first == null){
first = newNode;
}else{
last.next = newNode;
}
last = newNode;
total++;
}

public int size(){
return total;
}

public void delete(Object obj){
Node find = findNode(obj);
if(find != null){
if(find.prev != null){
find.prev.next = find.next;
}else{
first = find.next;
}
if(find.next != null){
find.next.prev = find.prev;
}else{
last = find.prev;
}

find.prev = null;
find.next = null;
find.data = null;

total--;
}
}

private Node findNode(Object obj){
Node node = first;
Node find = null;

if(obj == null){
while(node != null){
if(node.data == null){
find = node;
break;
}
node = node.next;
}
}else{
while(node != null){
if(obj.equals(node.data)){
find = node;
break;
}
node = node.next;
}
}
return find;
}

public boolean contains(Object obj){
return findNode(obj) != null;
}

public void update(E old, E value){
Node find = findNode(old);
if(find != null){
find.data = value;
}
}

@Override
public Iterator<E> iterator() {
return new Itr();
}

private class Itr implements Iterator<E>{
private Node<E> node = first;

@Override
public boolean hasNext() {
return node!=null;
}

@Override
public E next() {
E value = node.data;
node = node.next;
return value;
}
}
}

自定义双链表测试:

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
package com.atguigu.list;

public class MyLinkedListTest {
public static void main(String[] args) {
MyLinkedList<String> my = new MyLinkedList<>();
my.add("hello");
my.add("world");
my.add(null);
my.add(null);
my.add("java");
my.add("java");
my.add("atguigu");

System.out.println("一共有:" + my.size());
System.out.println("所有元素:");
for (String s : my) {
System.out.println(s);
}
System.out.println("-------------------------------------");
System.out.println("查找java,null,haha的结果:");
System.out.println(my.contains("java"));
System.out.println(my.contains(null));
System.out.println(my.contains("haha"));

System.out.println("-------------------------------------");
System.out.println("替换java,null后:");
my.update("java","JAVA");
my.update(null,"songhk");
System.out.println("所有元素:");
for (String s : my) {
System.out.println(s);
}
System.out.println("-------------------------------------");
System.out.println("删除hello,JAVA,null,atguigu后:");
my.delete("hello");
my.delete("JAVA");
my.delete(null);
my.delete("atguigu");
System.out.println("所有元素:");
for (String s : my) {
System.out.println(s);
}
}
}

4. 栈

4.1 栈的特点

  • 栈(Stack)又称为堆栈或堆叠,是限制仅在表的一端进行插入和删除运算的线性表。

  • 栈按照先进后出(FILO,first in last out)的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶。每次删除(退栈)的总是删除当前栈中最后插入(进栈)的元素,而最先插入的是被放在栈的底部,要到最后才能删除。

  • 核心类库中的栈结构有Stack和LinkedList。

    • Stack就是顺序栈,它是Vector的子类。
    • LinkedList是链式栈。
  • 体现栈结构的操作方法:

    • peek()方法:查看栈顶元素,不弹出
    • pop()方法:弹出栈
    • push(E e)方法:压入栈
  • 时间复杂度:

    • 索引: O(n)
    • 搜索: O(n)
    • 插入: O(1)
    • 移除: O(1)
  • 图示:

image-20220826010258638 数据结构-栈

4.2 Stack使用举例

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
/**
* @author 尚硅谷-宋红康
* @create 15:44
*/
public class TestStack {
/*
* 测试Stack
* */
@Test
public void test1(){
Stack<Integer> list = new Stack<>();
list.push(1);
list.push(2);
list.push(3);

System.out.println("list = " + list);

System.out.println("list.peek()=" + list.peek());
System.out.println("list.peek()=" + list.peek());
System.out.println("list.peek()=" + list.peek());

/*
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());//java.util.NoSuchElementException
*/

while(!list.empty()){
System.out.println("list.pop() =" + list.pop());
}
}

/*
* 测试LinkedList
* */
@Test
public void test2(){
LinkedList<Integer> list = new LinkedList<>();
list.push(1);
list.push(2);
list.push(3);

System.out.println("list = " + list);

System.out.println("list.peek()=" + list.peek());
System.out.println("list.peek()=" + list.peek());
System.out.println("list.peek()=" + list.peek());

/*
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());
System.out.println("list.pop() =" + list.pop());//java.util.NoSuchElementException
*/
while(!list.isEmpty()){
System.out.println("list.pop() =" + list.pop());
}
}
}

4.3 自定义栈

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
public class MyStack {
// 向栈当中存储元素,我们这里使用一维数组模拟。存到栈中,就表示存储到数组中。
// 为什么选择Object类型数组?因为这个栈可以存储java中的任何引用类型的数据
private Object[] elements;

// 栈帧,永远指向栈顶部元素
// 那么这个默认初始值应该是多少。注意:最初的栈是空的,一个元素都没有。
//private int index = 0; // 如果index采用0,表示栈帧指向了顶部元素的上方。
//private int index = -1; // 如果index采用-1,表示栈帧指向了顶部元素。
private int index;

/**
* 无参数构造方法。默认初始化栈容量10.
*/
public MyStack() {
// 一维数组动态初始化
// 默认初始化容量是10.
this.elements = new Object[10];
// 给index初始化
this.index = -1;
}

/**
* 压栈的方法
* @param obj 被压入的元素
*/
public void push(Object obj) throws Exception {
if(index >= elements.length - 1){
//方式1:
//System.out.println("压栈失败,栈已满!");
//return;
//方式2:
throw new Exception("压栈失败,栈已满!");
}
// 程序能够走到这里,说明栈没满
// 向栈中加1个元素,栈帧向上移动一个位置。
index++;
elements[index] = obj;
System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}

/**
* 弹栈的方法,从数组中往外取元素。每取出一个元素,栈帧向下移动一位。
* @return
*/
public Object pop() throws Exception {
if (index < 0) {
//方式1:
//System.out.println("弹栈失败,栈已空!");
//return;
//方式2:
throw new Exception("弹栈失败,栈已空!");
}
// 程序能够执行到此处说明栈没有空。
Object obj = elements[index];
System.out.print("弹栈" + obj + "元素成功,");
elements[index] = null;
// 栈帧向下移动一位。
index--;
return obj;
}

// set和get也许用不上,但是你必须写上,这是规矩。你使用IDEA生成就行了。
// 封装:第一步:属性私有化,第二步:对外提供set和get方法。
public Object[] getElements() {
return elements;
}

public void setElements(Object[] elements) {
this.elements = elements;
}

public int getIndex() {
return index;
}

public void setIndex(int index) {
this.index = index;
}
}

5. 队列

  • 队列(Queue)是只允许在一端进行插入,而在另一端进行删除的运算受限的线性表。
  • 队列是逻辑结构,其物理结构可以是数组,也可以是链表。
  • 队列的修改原则:队列的修改是依先进先出(FIFO)的原则进行的。新来的成员总是加入队尾(即不允许”加塞”),每次离开的成员总是队列头上的(不允许中途离队),即当前”最老的”成员离队。

  • 图示:

image-20220826010241172

数据结构-队列

6. 树与二叉树

6.1 树的理解

image-20220521111904272

专有名词解释:

结点:树中的数据元素都称之为结点

根节点:最上面的结点称之为根,一颗树只有一个根且由根发展而来,从另外一个角度来说,每个结点都可以认为是其子树的根

父节点:结点的上层结点,如图中,结点K的父节点是E、结点L的父节点是G

子节点:节点的下层结点,如图中,节点E的子节点是K节点、节点G的子节点是L节点

兄弟节点:具有相同父节点的结点称为兄弟节点,图中F、G、H互为兄弟节点

结点的度数:每个结点所拥有的子树的个数称之为结点的度,如结点B的度为3

树叶:度数为0的结点,也叫作终端结点,图中D、K、F、L、H、I、J都是树叶

非终端节点(或分支节点):树叶以外的节点,或度数不为0的节点。图中根、A、B、C、E、G都是

树的深度(或高度):树中结点的最大层次数,图中树的深度为4

结点的层数:从根节点到树中某结点所经路径上的分支树称为该结点的层数,根节点的层数规定为1,其余结点的层数等于其父亲结点的层数+1

同代:在同一棵树中具有相同层数的节点

6.2 二叉树的基本概念

二叉树(Binary tree)是树形结构的一个重要类型。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。许多实际问题抽象出来的数据结构往往是二叉树形式,二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。

1563449427345

6.3 二叉树的遍历

  • 前序遍历:中左右(根左右)

    即先访问根结点,再前序遍历左子树,最后再前序遍历右子 树。前序遍历运算访问二叉树各结点是以根、左、右的顺序进行访问的。

  • 中序遍历:左中右(左根右)

    即先中前序遍历左子树,然后再访问根结点,最后再中序遍 历右子树。中序遍历运算访问二叉树各结点是以左、根、右的顺序进行访问的。

  • 后序遍历:左右中(左右根)

    即先后序遍历左子树,然后再后序遍历右子树,最后访问根 结点。后序遍历运算访问二叉树各结点是以左、右、根的顺序进行访问的。

1574575739236

前序遍历:ABDHIECFG

中序遍历:HDIBEAFCG

后序遍历:HIDEBFGCA

6.4 经典二叉树

image-20220521153016348

1、满二叉树: 除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。 第n层的结点数是2的n-1次方,总的结点个数是2的n次方-1

1574575163883

2、完全二叉树: 叶结点只能出现在最底层的两层,且最底层叶结点均处于次底层叶结点的左侧。

1574575180247

3、二叉排序/查找/搜索树:即为BST (binary search/sort tree)。满足如下性质:
(1)若它的左子树不为空,则左子树上所有结点的值均小于它的根节点的值;
(2)若它的右子树上所有结点的值均大于它的根节点的值;
(3)它的左、右子树也分别为二叉排序/查找/搜索树。

image-20220521145208018

对二叉查找树进行中序遍历,得到有序集合。便于检索。

4、平衡二叉树:(Self-balancing binary search tree,AVL)首先是二叉排序树,此外具有以下性质:
(1)它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
(2)并且左右两个子树也都是一棵平衡二叉树
(3)不要求非叶节点都有两个子结点

平衡二叉树的目的是为了减少二叉查找树的层次,提高查找速度。平衡二叉树的常用实现有红黑树、AVL、替罪羊树、Treap、伸展树等。

image-20220521150151219

6、红黑树:即Red-Black Tree。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,它是在 1972 年由 Rudolf Bayer 发明的。红黑树是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的:它可以在 O(log n)时间内做查找,插入和删除, 这里的 n 是树中元素的数目。

红黑树的特性:

  • 每个节点是红色或者黑色

  • 根节点是黑色

  • 每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点)

  • 每个红色节点的两个子节点都是黑色的。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(确保没有一条路径会比其他路径长出2倍)

当我们插入或删除节点时,可能会破坏已有的红黑树,使得它不满足以上5个要求,那么此时就需要进行处理,使得它继续满足以上的5个要求:

1、recolor :将某个节点变红或变黑

2、rotation :将红黑树某些结点分支进行旋转(左旋或右旋)

image-20221208212053079

红黑树可以通过红色节点和黑色节点尽可能的保证二叉树的平衡。主要是用它来存储有序的数据,它的时间复杂度是O(logN),效率非常之高。

6.5 二叉树及其结点的表示

普通二叉树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BinaryTree<E>{
private TreeNode root; //二叉树的根结点
private int total;//结点总个数

private class TreeNode{
//至少有以下几个部分
TreeNode parent;
TreeNode left;
E data;
TreeNode right;

public TreeNode(TreeNode parent, TreeNode left, E data, TreeNode right) {
this.parent = parent;
this.left = left;
this.data = data;
this.right = right;
}
}
}

TreeMap红黑树:

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 TreeMap<K,V> {
private transient Entry<K,V> root;
private transient int size = 0;

static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;

/**
* Make a new cell with given key, value, and parent, and with
* {@code null} child links, and BLACK color.
*/
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
}
}

7. List接口分析

7.1 List接口特点

  • List集合所有的元素是以一种线性方式进行存储的,例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
  • 它是一个元素存取有序的集合。即元素的存入顺序和取出顺序有保证。
  • 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
  • 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素。

1563549818689

注意:

List集合关心元素是否有序,而不关心是否重复,请大家记住这个原则。例如“张三”可以领取两个号。

  • List接口的主要实现类
    • ArrayList:动态数组
    • Vector:动态数组
    • LinkedList:双向链表
    • Stack:栈

7.2 动态数组ArrayList与Vector

Java的List接口的实现类中有两个动态数组的实现:ArrayList 和 Vector。

7.2.1 ArrayList与Vector的区别

它们的底层物理结构都是数组,我们称为动态数组。

  • ArrayList是新版的动态数组,线程不安全,效率高,Vector是旧版的动态数组,线程安全,效率低。
  • 动态数组的扩容机制不同,ArrayList默认扩容为原来的1.5倍,Vector默认扩容增加为原来的2倍。
  • 数组的初始化容量,如果在构建ArrayList与Vector的集合对象时,没有显式指定初始化容量,那么Vector的内部数组的初始容量默认为10,而ArrayList在JDK 6.0 及之前的版本也是10,JDK8.0 之后的版本ArrayList初始化为长度为0的空数组,之后在添加第一个元素时,再创建长度为10的数组。原因:
    • 用的时候,再创建数组,避免浪费。因为很多方法的返回值是ArrayList类型,需要返回一个ArrayList的对象,例如:后期从数据库查询对象的方法,返回值很多就是ArrayList。有可能你要查询的数据不存在,要么返回null,要么返回一个没有元素的ArrayList对象。

7.2.2 ArrayList部分源码分析

JDK1.7.0_07中:

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
//属性
private transient Object[] elementData; //存储底层数组元素
private int size; //记录数组中存储的元素的个数

//构造器
public ArrayList() {
this(10); //指定初始容量为10
}

public ArrayList(int initialCapacity) {
super();
//检查初始容量的合法性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
//数组初始化为长度为initialCapacity的数组
this.elementData = new Object[initialCapacity];
}

//方法:add()相关方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); //查看当前数组是否够多存一个元素
elementData[size++] = e; //将元素e添加到elementData数组中
return true;
}

private void ensureCapacityInternal(int minCapacity) {
modCount++;
// 如果if条件满足,则进行数组的扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //当前数组容量
int newCapacity = oldCapacity + (oldCapacity >> 1); //新数组容量是旧数组容量的1.5倍
if (newCapacity - minCapacity < 0) //判断旧数组的1.5倍是否够
newCapacity = minCapacity;
//判断旧数组的1.5倍是否超过最大数组限制
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//复制一个新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}

//方法:remove()相关方法
public E remove(int index) {
rangeCheck(index); //判断index是否在有效的范围内

modCount++; //修改次数加1
//取出[index]位置的元素,[index]位置的元素就是要被删除的元素,用于最后返回被删除的元素
E oldValue = elementData(index);

int numMoved = size - index - 1; //确定要移动的次数
//如果需要移动元素,就用System.arraycopy移动元素
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
//将elementData[size-1]位置置空,让GC回收空间,元素个数减少
elementData[--size] = null;

return oldValue;
}

private void rangeCheck(int index) {
if (index >= size) //index不合法的情况
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

E elementData(int index) { //返回指定位置的元素
return (E) elementData[index];
}

//方法:set()方法相关
public E set(int index, E element) {
rangeCheck(index); //检验index是否合法

//取出[index]位置的元素,[index]位置的元素就是要被替换的元素,用于最后返回被替换的元素
E oldValue = elementData(index);
//用element替换[index]位置的元素
elementData[index] = element;
return oldValue;
}

//方法:get()相关方法
public E get(int index) {
rangeCheck(index); //检验index是否合法

return elementData(index); //返回[index]位置的元素
}

//方法:indexOf()
public int indexOf(Object o) {
//分为o是否为空两种情况
if (o == null) {
//从前往后找
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

//方法:lastIndexOf()
public int lastIndexOf(Object o) {
//分为o是否为空两种情况
if (o == null) {
//从后往前找
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}

jdk1.8.0_271中:

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
//属性
transient Object[] elementData;
private int size;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//构造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; //初始化为空数组
}

//方法:add()相关方法
public boolean add(E e) {
//查看当前数组是否够多存一个元素
ensureCapacityInternal(size + 1); // Increments modCount!!
//存入新元素到[size]位置,然后size自增1
elementData[size++] = e;
return true;
}

private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果当前数组还是空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//那么minCapacity取DEFAULT_CAPACITY与minCapacity的最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

//查看是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //修改次数加1

//如果需要的最小容量比当前数组的长度大,即当前数组不够存,就扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //当前数组容量
int newCapacity = oldCapacity + (oldCapacity >> 1); //新数组容量是旧数组容量的1.5倍
//看旧数组的1.5倍是否够
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//看旧数组的1.5倍是否超过最大数组限制
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//复制一个新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}

7.2.3 ArrayList相关方法图示

  • ArrayList采用数组作为底层实现
image-20221029112037297
  • ArrayList自动扩容过程

  • ArrayList的add(E e)方法

  • ArrayList的add(int index,E e)方法
image-20221029112157007

7.2.4 Vector部分源码分析

jdk1.8.0_271中:

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
//属性
protected Object[] elementData;
protected int elementCount;

//构造器
public Vector() {
this(10); //指定初始容量initialCapacity为10
}

public Vector(int initialCapacity) {
this(initialCapacity, 0); //指定capacityIncrement增量为0
}

public Vector(int initialCapacity, int capacityIncrement) {
super();
//判断了形参初始容量initialCapacity的合法性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
//创建了一个Object[]类型的数组
this.elementData = new Object[initialCapacity];
//增量,默认是0,如果是0,后面就按照2倍增加,如果不是0,后面就按照你指定的增量进行增量
this.capacityIncrement = capacityIncrement;
}

//方法:add()相关方法
//synchronized意味着线程安全的
public synchronized boolean add(E e) {
modCount++;
//看是否需要扩容
ensureCapacityHelper(elementCount + 1);
//把新的元素存入[elementCount],存入后,elementCount元素的个数增1
elementData[elementCount++] = e;
return true;
}

private void ensureCapacityHelper(int minCapacity) {
//看是否超过了当前数组的容量
if (minCapacity - elementData.length > 0)
grow(minCapacity); //扩容
}

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //获取目前数组的长度
//如果capacityIncrement增量是0,新容量 = oldCapacity的2倍
//如果capacityIncrement增量是不是0,新容量 = oldCapacity + capacityIncrement增量;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
//如果按照上面计算的新容量还不够,就按照你指定的需要的最小容量来扩容minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新容量超过了最大数组限制,那么单独处理
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//把旧数组中的数据复制到新数组中,新数组的长度为newCapacity
elementData = Arrays.copyOf(elementData, newCapacity);
}

//方法:remove()相关方法
public boolean remove(Object o) {
return removeElement(o);
}
public synchronized boolean removeElement(Object obj) {
modCount++;
//查找obj在当前Vector中的下标
int i = indexOf(obj);
//如果i>=0,说明存在,删除[i]位置的元素
if (i >= 0) {
removeElementAt(i);
return true;
}
return false;
}

//方法:indexOf()
public int indexOf(Object o) {
return indexOf(o, 0);
}
public synchronized int indexOf(Object o, int index) {
if (o == null) {//要查找的元素是null值
for (int i = index ; i < elementCount ; i++)
if (elementData[i]==null)//如果是null值,用==null判断
return i;
} else {//要查找的元素是非null值
for (int i = index ; i < elementCount ; i++)
if (o.equals(elementData[i]))//如果是非null值,用equals判断
return i;
}
return -1;
}

//方法:removeElementAt()
public synchronized void removeElementAt(int index) {
modCount++;
//判断下标的合法性
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}

//j是要移动的元素的个数
int j = elementCount - index - 1;
//如果需要移动元素,就调用System.arraycopy进行移动
if (j > 0) {
//把index+1位置以及后面的元素往前移动
//index+1的位置的元素移动到index位置,依次类推
//一共移动j个
System.arraycopy(elementData, index + 1, elementData, index, j);
}
//元素的总个数减少
elementCount--;
//将elementData[elementCount]这个位置置空,用来添加新元素,位置的元素等着被GC回收
elementData[elementCount] = null; /* to let gc do its work */
}

7.3 链表LinkedList

Java中有双链表的实现:LinkedList,它是List接口的实现类。

LinkedList是一个双向链表,如图所示:

image-20220514165707977

7.3.1 链表与动态数组的区别

动态数组底层的物理结构是数组,因此根据索引访问的效率 非常高。但是非末尾位置的插入和删除效率不高,因为涉及到移动元素。另外添加操作时涉及到扩容问题,就会增加时空消耗。

链表底层的物理结构是链表,因此根据索引访问的效率不高,即查找元素慢。但是插入和删除不需要移动元素,只需要修改前后元素的指向关系即可,所以插入、删除元素快。而且链表的添加不会涉及到扩容问题。

7.3.2 LinkedList源码分析

jdk1.8.0_271中:

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
//属性
transient Node<E> first; //记录第一个结点的位置
transient Node<E> last; //记录当前链表的尾元素
transient int size = 0; //记录最后一个结点的位置

//构造器
public LinkedList() {
}

//方法:add()相关方法
public boolean add(E e) {
linkLast(e); //默认把新元素链接到链表尾部
return true;
}

void linkLast(E e) {
final Node<E> l = last; //用 l 记录原来的最后一个结点
//创建新结点
final Node<E> newNode = new Node<>(l, e, null); //新节点链接到前一个节点 新节点没有链接到后一个节点
//现在的新结点是最后一个结点了
last = newNode;
//如果l==null,说明原来的链表是空的
if (l == null)
//那么新结点同时也是第一个结点
first = newNode;
else
//否则把新结点链接到原来的最后一个结点的next中
l.next = newNode;
//元素个数增加
size++;
//修改次数增加
modCount++;
}

//其中,Node类定义如下
private static class Node<E> {
E item; //元素数据
Node<E> next; //下一个结点
Node<E> prev; //前一个结点

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
//方法:获取get()相关方法
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

//方法:插入add()相关方法
public void add(int index, E element) {
checkPositionIndex(index);//检查index范围

if (index == size)//如果index==size,连接到当前链表的尾部
linkLast(element);
else
linkBefore(element, node(index));
}

Node<E> node(int index) {
// assert isElementIndex(index);
/*
index < (size >> 1)采用二分思想,先将index与长度size的一半比较,如果index<size/2,就只从位置0
往后遍历到位置index处,而如果index>size/2,就只从位置size往前遍历到位置index处。这样可以减少一部
分不必要的遍历。
*/
//如果index<size/2,就从前往后找目标结点
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {//否则从后往前找目标结点
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

//把新结点插入到[index]位置的结点succ前面
void linkBefore(E e, Node<E> succ) {//succ是[index]位置对应的结点
// assert succ != null;
final Node<E> pred = succ.prev; //[index]位置的前一个结点

//新结点的prev是原来[index]位置的前一个结点
//新结点的next是原来[index]位置的结点
final Node<E> newNode = new Node<>(pred, e, succ);

//[index]位置对应的结点的prev指向新结点
succ.prev = newNode;

//如果原来[index]位置对应的结点是第一个结点,那么现在新结点是第一个结点
if (pred == null)
first = newNode;
else
pred.next = newNode;//原来[index]位置的前一个结点的next指向新结点
size++;
modCount++;
}

//方法:remove()相关方法
public boolean remove(Object o) {
//分o是否为空两种情况
if (o == null) {
//找到o对应的结点x
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);//删除x结点
return true;
}
}
} else {
//找到o对应的结点x
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);//删除x结点
return true;
}
}
}
return false;
}
E unlink(Node<E> x) {//x是要被删除的结点
// assert x != null;
final E element = x.item;//被删除结点的数据
final Node<E> next = x.next;//被删除结点的下一个结点
final Node<E> prev = x.prev;//被删除结点的上一个结点

//如果被删除结点的前面没有结点,说明被删除结点是第一个结点
if (prev == null) {
//那么被删除结点的下一个结点变为第一个结点
first = next;
} else {//被删除结点不是第一个结点
//被删除结点的上一个结点的next指向被删除结点的下一个结点
prev.next = next;
//断开被删除结点与上一个结点的链接
x.prev = null;//使得GC回收
}

//如果被删除结点的后面没有结点,说明被删除结点是最后一个结点
if (next == null) {
//那么被删除结点的上一个结点变为最后一个结点
last = prev;
} else {//被删除结点不是最后一个结点
//被删除结点的下一个结点的prev执行被删除结点的上一个结点
next.prev = prev;
//断开被删除结点与下一个结点的连接
x.next = null;//使得GC回收
}
//把被删除结点的数据也置空,使得GC回收
x.item = null;
//元素个数减少
size--;
//修改次数增加
modCount++;
//返回被删除结点的数据
return element;
}

public E remove(int index) { //index是要删除元素的索引位置
checkElementIndex(index);
return unlink(node(index));
}

7.3.3 LinkedList相关方法图示

  • 只有1个元素的LinkedList
image-20221029134437888
  • 包含4个元素的LinkedList
image-20221029134534198
  • add(E e)方法
image-20221029135013377
  • add(int index,E e)方法
image-20221029135045120
  • remove(Object obj)方法
image-20221029134721089
  • remove(int index)方法
image-20221029134807613

8. Map接口分析

8.1 哈希表的物理结构

HashMap和Hashtable底层都是哈希表(也称散列表),其中维护了一个长度为2^幂次方的Entry类型的数组table,数组的每一个索引位置被称为一个桶(bucket),你添加的映射关系(key,value)最终都被封装为一个Map.Entry类型的对象,放到某个table[index]桶中。

使用数组的目的是查询和添加的效率高,可以根据索引直接定位到某个table[index]。

image-20221029144811305

8.2 HashMap中数据添加过程

8.2.1 JDK7中过程分析

1
2
3
// 在底层创建了长度为16的Entry[] table的数组
HashMap map = new HashMap();
map.put(key1,value1);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
分析过程如下:
将(key1,value1)添加到当前hashmap的对象中。首先会调用key1所在类的hashCode()方法,计算key1的哈希值1,
此哈希值1再经过某种运算(hash()),得到哈希值2。此哈希值2再经过某种运算(indexFor()),确定在底层table数组中的索引位置i。
(1)如果数组索引为i上的数据为空,则(key1,value1)直接添加成功 ------位置1
(2)如果数组索引为i上的数据不为空,有(key2,value2),则需要进一步判断:
判断key1的哈希值2与key2的哈希值是否相同:
(3) 如果哈希值不同,则(key1,value1)直接添加成功 ------位置2
如果哈希值相同,则需要继续调用key1所在类的equals()方法,将key2放入equals()形参进行判断
(4) equals方法返回false : 则(key1,value1)直接添加成功 ------位置3
equals方法返回true : 默认情况下,value1会覆盖value2。

位置1:直接将(key1,value1)以Entry对象的方式存放到table数组索引i的位置。
位置2、位置3:(key1,value1) 与现有的元素以链表的方式存储在table数组索引i的位置,新添加的元素指向旧添加的元素。

...
在不断的添加的情况下,满足如下条件的情况下,会进行扩容:
if ((size >= threshold) && (null != table[bucketIndex])) :
默认情况下,当要添加的元素个数超过12(即:数组的长度 * loadFactor得到的结果)时,就要考虑扩容。
1
2
3
4
5
6
7
8
9
10
补充:jdk7源码中定义的:
static class Entry<K,V> implements Map.Entry<K,V>
map.get(key1);
① 计算key1的hash值,用这个方法hash(key1)
② 找index = table.length-1 & hash;
③ 如果table[index]不为空,那么就挨个比较哪个Entry的key与它相同,就返回它的value
map.remove(key1);
① 计算key1的hash值,用这个方法hash(key1)
② 找index = table.length-1 & hash;
③ 如果table[index]不为空,那么就挨个比较哪个Entry的key与它相同,就删除它,把它前面的Entry的next的值修改为被删除Entry的next

image-20231031195051986

8.2.2 JDK8中过程分析

下面说明是JDK8相较于JDK7的不同之处:

1
2
3
4
5
6
①使用HashMap()的构造器创建对象时,并没有在底层初始化长度为16的table数组。
②jdk8中添加的key,value封装到了HashMap.Node类的对象中。而非jdk7中的HashMap.Entry。
③jdk8中新增的元素所在的索引位置如果有其他元素。在经过一系列判断后,如果能添加,则是旧的元素指向新的元素。而非jdk7中的新的元素指向旧的元素。“七上八下”
④jdk7时底层的数据结构是:数组+单向链表。 而jdk8时,底层的数据结构是:数组+单向链表+红黑树。
红黑树出现的时机:当某个索引位置i上的链表的长度达到8,且数组的长度超过64时,此索引位置上的元素要从单向链表改为红黑树。
如果索引i位置是红黑树的结构,当不断删除元素的情况下,当前索引i位置上的元素的个数低于6时,要从红黑树改为单向链表。

8.3 HashMap源码剖析

8.3.1 JDK1.7.0_07中源码

image-20220514190849626

1、Entry

key-value被封装为HashMap.Entry类型,而这个类型实现了Map.Entry接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HashMap<K,V>{
transient Entry<K,V>[] table;

static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash; //使用key得到的哈希值2进行赋值。

/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//略
}
}
2、属性
1
2
3
4
5
6
7
8
9
10
11
12
//table数组的默认初始化长度
static final int DEFAULT_INITIAL_CAPACITY = 16; // default_initial_capacity=16
//哈希表
transient Entry<K,V>[] table; //table数组默认大小为16
//哈希表中key-value的个数
transient int size;
//临界值、阈值(扩容的临界值)
int threshold;
//加载因子
final float loadFactor;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f; // default_load_factor=0.75
3、构造器
1
2
3
4
5
public HashMap() {
//DEFAULT_INITIAL_CAPACITY:默认初始容量16
//DEFAULT_LOAD_FACTOR:默认加载因子0.75
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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 HashMap(int initialCapacity, float loadFactor) {
//校验initialCapacity合法性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//校验initialCapacity合法性
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//校验loadFactor合法性
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

//计算得到table数组的长度(保证capacity是2的整次幂)
int capacity = 1;
//通过此循环,得到capacity的最终值,此最终值决定了Entry数组的长度。此时的capacity一定是2的整数倍
while (capacity < initialCapacity)
//保证数组长度一致是2的整次幂
capacity <<= 1; //通过不停的左移相当于放大2倍 --直到capacity=16
//加载因子,初始化为0.75
this.loadFactor = loadFactor;
// threshold 初始为默认容量
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化table数组
table = new Entry[capacity]; //初始化table大小为16
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
4、put()方法
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
public V put(K key, V value) {
//如果key是null,单独处理,存储到table[0]中,如果有另一个key为null,value覆盖
if (key == null)
return putForNullKey(value);
//对key的hashCode进行干扰,算出一个hash值
/*
hashCode值 xxxxxxxxxx
table.length-1 000001111

hashCode值 xxxxxxxxxx 无符号右移几位和原来的hashCode值做^运算,使得hashCode高位二进制值参与计算,
也发挥作用,降低index冲突的概率。
*/
int hash = hash(key);
//计算新的映射关系应该存到table[i]位置,
//i = hash & table.length-1,可以保证i在[0,table.length-1]范围内
int i = indexFor(hash, table.length);
//检查table[i]下面有没有key与我新的映射关系的key重复,如果重复替换value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果hash不相同,就不进入if语句,那样就一直e=e.next然后调用addEntry()最终使用头插法 --情况2
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //左边false那就情况2 左边true就判断后面的就是情况3
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; //如果put是修改操作,会返回原有旧的value值。
}
}
//如果当前索引位置i没有元素 e==null --情况1
modCount++;
//添加新的映射关系
addEntry(hash, key, value, i); //将key,value封装为一个Entry对象,并将此对象保存在索引i位置。
return null; //如果put是添加操作,会返回null.
}

其中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//如果key是null,直接存入[0]的位置
private V putForNullKey(V value) {
//判断是否有重复的key,如果有重复的,就替换value
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//把新的映射关系存入[0]的位置,而且key的hash值用0表示
addEntry(0, null, value, 0);
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}

h ^= k.hashCode();

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
1
2
3
static int indexFor(int h, int length) {
return h & (length-1); //相当于用哈希值2&15 那只考虑最低四位&1111 效率要比整体取模要高很多 -->得到0-15的值
}
1
2
3
4
5
6
7
8
9
10
11
12
13
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断是否需要库容
//扩容:(1)size达到阈值(2)table[i]正好非空
if ((size >= threshold) && (null != table[bucketIndex])) {
//table扩容为原来的2倍,并且扩容后,会重新调整所有key-value的存储位置
resize(2 * table.length);
//新的key-value的hash和index也会重新计算
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length); //重新计算索引位置
}
//存入table中
createEntry(hash, key, value, bucketIndex);
}
1
2
3
4
5
6
7
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex]; //将现有位置的元素取出来给e
//原来table[i]下面的映射关系作为新的映射关系next
table[bucketIndex] = new Entry<>(hash, key, value, e); //现有位置存放新元素 新的元素指向原来的元素e 【头插法】
//个数增加
size++;
}

8.3.2 JDK1.8.0_271中源码

1、Node

key-value被封装为HashMap.Node类型或HashMap.TreeNode类型,它俩都直接或间接的实现了Map.Entry接口。

存储到table数组的可能是Node结点对象,也可能是TreeNode结点对象,它们也是Map.Entry接口的实现类。即table[index]下的映射关系可能串起来一个链表或一棵红黑树。

image-20220514190904009

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
public class HashMap<K,V>{
transient Node<K,V>[] table;

//Node类
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// 其它结构:略
}

//TreeNode类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red; //是红结点还是黑结点
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}

//....
}
2、属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量 1 << 30
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //默认树化阈值8,当链表的长度达到这个值后,要考虑树化
static final int UNTREEIFY_THRESHOLD = 6;//默认反树化阈值6,当树中结点的个数达到此阈值后,要考虑变为链表

//当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
//当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量64

transient Node<K,V>[] table; //数组
transient int size; //记录有效映射关系的对数,也是Entry对象的个数
int threshold; //阈值,当size达到阈值时,考虑扩容
final float loadFactor; //加载因子,影响扩容的频率
3、构造器
1
2
3
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted (其他字段都是默认值)
}
4、put()方法
1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

其中,

1
2
3
4
5
6
7
8
9
10
11
12
13
static final int hash(Object key) {
int h;
//如果key是null,hash是0
//如果key非null,用key的hashCode值 与 key的hashCode值高16进行异或
// 即就是用key的hashCode值高16位与低16位进行了异或的干扰运算

/*
index = hash & table.length-1
如果用key的原始的hashCode值 与 table.length-1 进行按位与,那么基本上高16没机会用上。
这样就会增加冲突的概率,为了降低冲突的概率,把高16位加入到hash信息中。
*/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
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
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; //数组
Node<K,V> p; //一个结点
int n, i; //n是数组的长度 i是下标

//tab和table等价
//如果table是空的
if ((tab = table) == null || (n = tab.length) == 0){
n = (tab = resize()).length;
/*
tab = resize();
n = tab.length;*/
/*
如果table是空的,resize()完成了①创建了一个长度为16的数组②threshold = 12
n = 16
*/
}
//i = (n - 1) & hash ,下标 = 数组长度-1 & hash
//p = tab[i] 第1个结点
//if(p==null) 条件满足的话说明 table[i]还没有元素
if ((p = tab[i = (n - 1) & hash]) == null){
//把新的映射关系直接放入table[i]
tab[i] = newNode(hash, key, value, null);
//newNode()方法就创建了一个Node类型的新结点,新结点的next是null
}else {
Node<K,V> e; K k;
//p是table[i]中第一个结点
//if(table[i]的第一个结点与新的映射关系的key重复)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//用e记录这个table[i]的第一个结点
else if (p instanceof TreeNode){ //如果table[i]第一个结点是一个树结点
//单独处理树结点
//如果树结点中,有key重复的,就返回那个重复的结点用e接收,即e!=null
//如果树结点中,没有key重复的,就把新结点放到树中,并且返回null,即e=null
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
}else {
//table[i]的第一个结点不是树结点,也与新的映射关系的key不重复
//binCount记录了table[i]下面的结点的个数
for (int binCount = 0; ; ++binCount) {
//如果p的下一个结点是空的,说明当前的p是最后一个结点
if ((e = p.next) == null) {
//把新的结点连接到table[i]的最后
p.next = newNode(hash, key, value, null);
//如果binCount>=8-1,达到7个时
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//要么扩容,要么树化
treeifyBin(tab, hash);
break;
}
//如果key重复了,就跳出for循环,此时e结点记录的就是那个key重复的结点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;//下一次循环,e=p.next,就类似于e=e.next,往链表下移动
}
}
//如果这个e不是null,说明有key重复,就考虑替换原来的value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //什么也没干
return oldValue;
}
}
++modCount;

//元素个数增加
//size达到阈值
if (++size > threshold)
resize(); //一旦扩容,重新调整所有映射关系的位置
afterNodeInsertion(evict); //什么也没干
return 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
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
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //oldTab原来的table
//oldCap:原来数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr:原来的阈值
int oldThr = threshold;//最开始threshold是0

//newCap,新容量
//newThr:新阈值
int newCap, newThr = 0;
if (oldCap > 0) { //说明原来不是空数组
if (oldCap >= MAXIMUM_CAPACITY) { //是否达到数组最大限制
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//newCap = 旧的容量*2 ,新容量<最大数组容量限制
//新容量:32,64,...
//oldCap >= 初始容量16
//新阈值重新算 = 24,48 ....
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; //新容量是默认初始化容量16
//新阈值= 默认的加载因子 * 默认的初始化容量 = 0.75*16 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; //阈值赋值为新阈值12,24.。。。
//创建了一个新数组,长度为newCap,16,32,64.。。
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) { //原来不是空数组
//把原来的table中映射关系,倒腾到新的table中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {//e是table下面的结点
oldTab[j] = null; //把旧的table[j]位置清空
if (e.next == null) //如果是最后一个结点
newTab[e.hash & (newCap - 1)] = e; //重新计算e的在新table中的存储位置,然后放入
else if (e instanceof TreeNode) //如果e是树结点
//把原来的树拆解,放到新的table
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//把原来table[i]下面的整个链表,重新挪到了新的table中
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
1
2
3
4
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
//创建一个新结点
return new Node<>(hash, key, value, next);
}
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
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index;
Node<K,V> e;
//MIN_TREEIFY_CAPACITY:最小树化容量64
//如果table是空的,或者 table的长度没有达到64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();//先扩容
else if ((e = tab[index = (n - 1) & hash]) != null) {
//用e记录table[index]的结点的地址
TreeNode<K,V> hd = null, tl = null;
/*
do...while,把table[index]链表的Node结点变为TreeNode类型的结点
*/
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;//hd记录根结点
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);

//如果table[index]下面不是空
if ((tab[index] = hd) != null)
hd.treeify(tab);//将table[index]下面的链表进行树化
}
}

小结:
image-20220524142524796

8.4 LinkedHashMap源码剖析

8.4.1 源码

内部定义的Entry如下:

1
2
3
4
5
6
7
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;

Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}

LinkedHashMap重写了HashMap中的newNode()方法:

1
2
3
4
5
6
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
1
2
3
4
5
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);
return p;
}

8.4.2 图示

image-20221029145708224

9. Set接口分析

9.1 Set集合与Map集合的关系

Set的内部实现其实是一个Map,Set中的元素,存储在HashMap的key中。即HashSet的内部实现是一个HashMap,TreeSet的内部实现是一个TreeMap,LinkedHashSet的内部实现是一个LinkedHashMap。

9.2 源码剖析

HashSet源码:

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
//构造器
public HashSet() {
map = new HashMap<>();
}

public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}

public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}

//这个构造器是给子类LinkedHashSet调用的
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

//add()方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//其中,
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();

//iterator()方法:
public Iterator<E> iterator() {
return map.keySet().iterator();
}

LinkedHashSet源码:

1
2
3
4
5
6
7
8
9
10
//构造器
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);//调用HashSet的某个构造器
}
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);//调用HashSet的某个构造器
}

TreeSet源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public TreeSet() {
this(new TreeMap<E,Object>());
}

TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
//其中,
private transient NavigableMap<E,Object> m;

//add()方法:
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
//其中,
private static final Object PRESENT = new Object();

10. 【拓展】HashMap的相关问题

1、说说你理解的哈希算法

hash算法是一种可以从任何数据中提取出其“指纹”的数据摘要算法,它将任意大小的数据映射到一个固定大小的序列上,这个序列被称为hash code、数据摘要或者指纹。比较出名的hash算法有MD5、SHA。hash是具有唯一性且不可逆的,唯一性是指相同的“对象”产生的hash code永远是一样的。

1563797150134

2、Entry中的hash属性为什么不直接使用key的hashCode()返回值呢?

不管是JDK1.7还是JDK1.8中,都不是直接用key的hashCode值直接与table.length-1计算求下标的,而是先对key的hashCode值进行了一个运算,JDK1.7和JDK1.8关于hash()的实现代码不一样,但是不管怎么样都是为了提高hash code值与 (table.length-1)的按位与完的结果,尽量的均匀分布。

image-20220514190454633

JDK1.7:

1
2
3
4
5
6
7
8
9
10
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}

h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

JDK1.8:

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

虽然算法不同,但是思路都是将hashCode值的高位二进制与低位二进制值进行了异或,然高位二进制参与到index的计算中。

为什么要hashCode值的二进制的高位参与到index计算呢?

因为一个HashMap的table数组一般不会特别大,至少在不断扩容之前,那么table.length-1的大部分高位都是0,直接用hashCode和table.length-1进行&运算的话,就会导致总是只有最低的几位是有效的,那么就算你的hashCode()实现的再好也难以避免发生碰撞,这时让高位参与进来的意义就体现出来了。它对hashcode的低位添加了随机性并且混合了高位的部分特征,显著减少了碰撞冲突的发生。

3、HashMap是如何决定某个key-value存在哪个桶的呢?

因为hash值是一个整数,而数组的长度也是一个整数,有两种思路:

①hash 值 % table.length会得到一个[0,table.length-1]范围的值,正好是下标范围,但是用%运算效率没有位运算符&高。

②hash 值 & (table.length-1),任何数 & (table.length-1)的结果也一定在[0, table.length-1]范围。

1563800372286

JDK1.7:

1
2
3
4
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1); //此处h就是hash
}

JDK1.8:

1
2
3
4
5
6
7
8
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // i = (n - 1) & hash
tab[i] = newNode(hash, key, value, null);
//....省略大量代码
}

4、为什么要保持table数组一直是2的n次幂呢?

因为如果数组的长度为2的n次幂,那么table.length-1的二进制就是一个高位全是0,低位全是1的数字,这样才能保证每一个下标位置都有机会被用到。

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
hashCode值是   ?
table.length是10
table.length-19

? ????????
9 00001001
&_____________
00000000 [0]
00000001 [1]
00001000 [8]
00001001 [9]
一定[0]~[9]

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hashCode值是   ?
table.length是16
table.length-115

? ????????
15 00001111
&_____________
00000000 [0]
00000001 [1]
00000010 [2]
00000011 [3]
...
00001111 [15]
范围是[0,15],一定在[0,table.length-1]范围内

5、解决[index]冲突问题

虽然从设计hashCode()到上面HashMap的hash()函数,都尽量减少冲突,但是仍然存在两个不同的对象返回的hashCode值相同,或者hashCode值就算不同,通过hash()函数计算后,得到的index也会存在大量的相同,因此key分布完全均匀的情况是不存在的。那么发生碰撞冲突时怎么办?

JDK1.8之间使用:数组+链表的结构。

1563802656661

JDK1.8之后使用:数组+链表/红黑树的结构。

1563802665708

即hash相同或hash&(table.lengt-1)的值相同,那么就存入同一个“桶”table[index]中,使用链表或红黑树连接起来。

6、为什么JDK1.8会出现红黑树和链表共存呢?

因为当冲突比较严重时,table[index]下面的链表就会很长,那么会导致查找效率大大降低,而如果此时选用二叉树可以大大提高查询效率。

但是二叉树的结构又过于复杂,占用内存也较多,如果结点个数比较少的时候,那么选择链表反而更简单。所以会出现红黑树和链表共存。

7、加载因子的值大小有什么关系?

如果太大,threshold就会很大,那么如果冲突比较严重的话,就会导致table[index]下面的结点个数很多,影响效率。

如果太小,threshold就会很小,那么数组扩容的频率就会提高,数组的使用率也会降低,那么会造成空间的浪费。

8、什么时候树化?什么时候反树化?

1
2
3
static final int TREEIFY_THRESHOLD = 8;//树化阈值
static final int UNTREEIFY_THRESHOLD = 6;//反树化阈值
static final int MIN_TREEIFY_CAPACITY = 64;//最小树化容量
  • 当某table[index]下的链表的结点个数达到8,并且table.length>=64,那么如果新Entry对象还添加到该table[index]中,那么就会将table[index]的链表进行树化。

  • 当某table[index]下的红黑树结点个数少于6个,此时,

    • 当继续删除table[index]下的树结点,最后这个根结点的左右结点有null,或根结点的左结点的左结点为null,会反树化
    • 当重新添加新的映射关系到map中,导致了map重新扩容了,这个时候如果table[index]下面还是小于等于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
package com.atguigu.map;

public class MyKey{
int num;

public MyKey(int num) {
super();
this.num = num;
}

@Override
public int hashCode() {
if(num<=20){
return 1;
}else{
final int prime = 31;
int result = 1;
result = prime * result + num;
return result;
}
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MyKey other = (MyKey) obj;
if (num != other.num)
return false;
return true;
}

}
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
import org.junit.Test;
import java.util.HashMap;
public class TestHashMapMyKey {
@Test
public void test1(){
//这里为了演示的效果,我们造一个特殊的类,这个类的hashCode()方法返回固定值1
//因为这样就可以造成冲突问题,使得它们都存到table[1]中
HashMap<MyKey, String> map = new HashMap<>();
for (int i = 1; i <= 11; i++) {
map.put(new MyKey(i), "value"+i);//树化演示
}
}
@Test
public void test2(){
HashMap<MyKey, String> map = new HashMap<>();
for (int i = 1; i <= 11; i++) {
map.put(new MyKey(i), "value"+i);
}
for (int i = 1; i <=11; i++) {
map.remove(new MyKey(i));//反树化演示
}
}
@Test
public void test3(){
HashMap<MyKey, String> map = new HashMap<>();
for (int i = 1; i <= 11; i++) {
map.put(new MyKey(i), "value"+i);
}

for (int i = 1; i <=5; i++) {
map.remove(new MyKey(i));
}//table[1]下剩余6个结点

for (int i = 21; i <= 100; i++) {
map.put(new MyKey(i), "value"+i);//添加到扩容时,反树化
}
}
}

9、key-value中的key是否可以修改?

key-value存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的key-value,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。

这个规则也同样适用于LinkedHashMap、HashSet、LinkedHashSet、Hashtable等所有散列存储结构的集合。

10、JDK1.7中HashMap的循环链表是怎么回事?如何解决?

HashMap的循环引用问题

避免HashMap发生死循环的常用解决方案:

  • 多线程环境下,使用线程安全的ConcurrentHashMap替代HashMap,推荐
  • 多线程环境下,使用synchronized或Lock加锁,但会影响性能,不推荐
  • 多线程环境下,使用线程安全的Hashtable替代,性能低,不推荐

HashMap死循环只会发生在JDK1.7版本中,主要原因:头插法+链表+多线程并发+扩容。

在JDK1.8中,HashMap改用尾插法,解决了链表死循环的问题。

JAVA-泛型

第13章_泛型(Generic)

本章专题与脉络

第3阶段:Java高级应用-第13章

1. 泛型概述

1.1 生活中的例子

  • 举例1:中药店,每个抽屉外面贴着标签

image-20220411000757577

  • 举例2:超市购物架上很多瓶子,每个瓶子装的是什么,有标签
image-20220918230534965
  • 举例3:家庭厨房中:就像垃圾分类一样,只有对应的垃圾才能扔进去
image-20220514191533296

Java中的泛型,就类似于上述场景中的标签

1.2 泛型的引入

在Java中,在声明方法时如果有未知的数据参与,这些未知数据在调用方法时才能确定,那么我们把这样的数据通过形参表示。

在方法体中,用这个形参名来代表那个未知的数据,而调用者在调用时,对应的传入实参就可以了。

1563414367674

因此,JDK1.5设计了泛型概念,泛型 == 类型参数,这个类型参数在声明它的类、接口或方法中,代表未知的某种通用类型。

举例1:

集合类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK5.0之前只能把元素类型设计为Object,JDK5.0时Java引入了“参数化类型(Parameterized type)”的概念,允许我们在创建集合时指定集合元素的类型。比如:List<String>,这表明该List只能保存字符串类型的对象。

使用集合存储数据时,除了元素的类型不确定,其他部分是确定的(例如关于这个元素如何保存,如何管理等)。

举例2:

java.lang.Comparable接口和java.util.Comparator接口,是用于比较对象大小的接口。这两个接口只是限定了当一个对象大于另一个对象时返回正整数,小于返回负整数,等于返回0,但是并不确定是什么类型的对象比较大小。JDK5.0之前只能用Object类型表示,使用时既麻烦又不安全,因此 JDK5.0 给它们增加了泛型。

image-20220923154058074

image-20220923154426871

其中<T>就是类型参数,即泛型。

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值或参数的类型。这个类型参数将在使用时(例如,继承或实现这个接口、创建对象或调用方法时)确定(即传入实际的类型参数,也称为类型实参)。

2. 使用泛型举例

自从JDK5.0引入泛型的概念之后,对之前核心类库中的API做了很大的修改,例如:JDK5.0改写了集合框架中的全部接口和类、java.lang.Comparable接口、java.util.Comparator接口、Class类等。为这些接口、类增加了泛型支持,从而可以在声明变量、创建对象时传入类型实参

2.1 集合中使用泛型

2.1.1 举例

集合中没有使用泛型时:

image-20220411001522636

集合中使用泛型时:

image-20220411001549747

Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。即,把不安全的因素在编译期间就排除了,而不是运行期;既然通过了编译,那么类型一定是符合要求的,就避免了类型转换。

同时,代码更加简洁、健壮。

把一个集合中的内容限制为一个特定的数据类型,这就是generic背后的核心思想。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//泛型在List中的使用
@Test
public void test1(){
//举例:将学生成绩保存在ArrayList中
//标准写法:
//ArrayList<Integer> list = new ArrayList<Integer>();
//jdk7的新特性:类型推断
ArrayList<Integer> list = new ArrayList<>();

list.add(56); //自动装箱
list.add(76);
list.add(88);
list.add(89);
//当添加非Integer类型数据时,编译不通过
//list.add("Tom");//编译报错
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
//不需要强转,直接可以获取添加时的元素的数据类型
Integer score = iterator.next();
System.out.println(score);
}
}

举例:

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
//泛型在Map中的使用
@Test
public void test2(){
HashMap<String,Integer> map = new HashMap<>();

map.put("Tom",67);
map.put("Jim",56);
map.put("Rose",88);
//编译不通过
// map.put(67,"Jack");

//遍历key集
Set<String> keySet = map.keySet();
for(String str:keySet){
System.out.println(str);
}

//遍历value集
Collection<Integer> values = map.values();
Iterator<Integer> iterator = values.iterator();
while(iterator.hasNext()){
Integer value = iterator.next();
System.out.println(value);
}

//遍历entry集
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator1 = entrySet.iterator();
while(iterator1.hasNext()){
Map.Entry<String, Integer> entry = iterator1.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + ":" + value);
}

}

2.1.2 练习

练习1:

1
2
3
4
5
(1)创建一个ArrayList集合对象,并指定泛型为<Integer>
(2)添加5个[0,100)以内的整数到集合中
(3)使用foreach遍历输出5个整数
(4)使用集合的removeIf方法删除偶数,为Predicate接口指定泛型<Ineteger>
(5)再使用Iterator迭代器输出剩下的元素,为Iterator接口指定泛型<Integer>
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
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;
import java.util.function.Predicate;
public class Test2 {
public static void main(String[] args) {
//(1)创建一个ArrayList集合对象,并指定泛型为<Integer>
ArrayList<Integer> list=new ArrayList<Integer>();
//(2)添加5个[0,100)以内的随机整数到集合中
Random r=new Random();
list.add(r.nextInt(100));
list.add(r.nextInt(100));
list.add(r.nextInt(100));
list.add(r.nextInt(100));
list.add(r.nextInt(100));
System.out.println(list);
//(3)使用foreach遍历输出5个整数
for(Integer temp:list){
System.out.println(temp);
}
//(4)使用集合的removeIf方法删除偶数,为Predicate接口指定泛型<Ineteger>
list.removeIf(new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
if(integer%2==0){
return true;
}
return false;
}
});
System.out.println("删除偶数之后的list:"+list);
//(5)再使用Iterator迭代器输出剩下的元素,为Iterator接口指定泛型<Integer>
Iterator iterator=list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}

image-20231029145834736

练习2:编写一个简单的同学通迅录

需求说明:

  • 查询所有通讯录的同学信息。
  • 输入姓名,根据姓名查询指定同学信息。如果该姓名不存在,输出提示信息。
  • 添加同学,姓名重复的不能添加。
  • 根据学员姓名删除学员。
  • 按姓名排序查询学员。

分析:

  • 使用HashMap<K,V>存储同学信息,使用同学姓名做key,同学对象做value。
  • 同学对象包含的属性有:姓名、年龄、住址、爱好等。

2.2 比较器中使用泛型

2.2.1 举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Circle{
private double radius;
public Circle(double radius) {
super();
this.radius = radius;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
@Override
public String toString() {
return "Circle [radius=" + radius + "]";
}
}

使用泛型之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Comparator;
class CircleComparator implements Comparator{
@Override
public int compare(Object o1, Object o2) {
//强制类型转换
Circle c1 = (Circle) o1;
Circle c2 = (Circle) o2;
return Double.compare(c1.getRadius(), c2.getRadius());
}
}
//测试:
public class TestNoGeneric {
public static void main(String[] args) {
CircleComparator com = new CircleComparator();
System.out.println(com.compare(new Circle(1), new Circle(2)));

System.out.println(com.compare("圆1", "圆2"));//运行时异常:ClassCastException
}
}

使用泛型之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Comparator;
class CircleComparator1 implements Comparator<Circle> {

@Override
public int compare(Circle o1, Circle o2) {
//不再需要强制类型转换,代码更简洁
return Double.compare(o1.getRadius(), o2.getRadius());
}
}

//测试类
public class TestHasGeneric {
public static void main(String[] args) {
CircleComparator1 com = new CircleComparator1();
System.out.println(com.compare(new Circle(1), new Circle(2)));

//System.out.println(com.compare("圆1", "圆2"));
//编译错误,因为"圆1", "圆2"不是Circle类型,是String类型,编译器提前报错,
//而不是冒着风险在运行时再报错。
}
}

2.2.2 练习

(1)声明矩形类Rectangle,包含属性长和宽,属性私有化,提供有参构造、get/set方法、重写toString方法,提供求面积和周长的方法。

(2)矩形类Rectangle实现java.lang.Comparable接口,并指定泛型为,重写int compareTo(T t)方法,按照矩形面积比较大小,面积相等的,按照周长比较大小。

(3)在测试类中,创建Rectangle数组,并创建5个矩形对象

(4)调用Arrays的sort方法,给矩形数组排序,并显示排序前后的结果。

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
package com.atguigu.genericclass.use;

public class Rectangle implements Comparable<Rectangle>{
private double length;
private double width;

public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}

public double getLength() {
return length;
}

public void setLength(double length) {
this.length = length;
}

public double getWidth() {
return width;
}

public void setWidth(double width) {
this.width = width;
}
//获取面积
public double area(){
return length * width;
}
//获取周长
public double perimeter(){
return 2 * (length + width);
}

@Override
public String toString() {
return "Rectangle{" +
"length=" + length +
", width=" + width +
",area =" + area() +
",perimeter = " + perimeter() +
'}';
}

@Override
public int compareTo(Rectangle o) {
int compare = Double.compare(area(), o.area());
return compare != 0 ? compare : Double.compare(perimeter(),o.perimeter());
}
}
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
package com.atguigu.genericclass.use;

import java.util.Arrays;

public class TestRectangle {
public static void main(String[] args) {
Rectangle[] arr = new Rectangle[4];
arr[0] = new Rectangle(6,2);
arr[1] = new Rectangle(4,3);
arr[2] = new Rectangle(12,1);
arr[3] = new Rectangle(5,4);

System.out.println("排序之前:");
for (Rectangle rectangle : arr) {
System.out.println(rectangle);
}

Arrays.sort(arr);

System.out.println("排序之后:");
for (Rectangle rectangle : arr) {
System.out.println(rectangle);
}
}
}

2.3 相关使用说明

  • 在创建集合对象的时候,可以指明泛型的类型。

    具体格式为:List list = new ArrayList();

  • JDK7.0时,有新特性,可以简写为:

    List list = new ArrayList<>(); //类型推断

  • 泛型,也称为泛型参数,即参数的类型,只能使用引用数据类型进行赋值。(不能使用基本数据类型,可以使用包装类替换)

  • 集合声明时,声明泛型参数。在使用集合时,可以具体指明泛型的类型。一旦指明,类或接口内部,凡是使用泛型参数的位置,都指定为具体的参数类型。如果没有指明的话,看做是Object类型。

3. 自定义泛型结构

3.1 泛型的基础说明

1、<类型>这种语法形式就叫泛型。

  • <类型>的形式我们称为类型参数,这里的”类型”习惯上使用T表示,是Type的缩写。即:

  • :代表未知的数据类型,我们可以指定为等。

    • 类比方法的参数的概念,我们把,称为类型形参,将称为类型实参,有助于我们理解泛型
  • 这里的T,可以替换成K,V等任意字母。

2、在哪里可以声明类型变量<T>

  • 声明类或接口时,在类名或接口名后面声明泛型类型,我们把这样的类或接口称为泛型类泛型接口
1
2
3
4
5
6
7
8
9
10
11
12
【修饰符】 class 类名<类型变量列表> 【extends 父类】 【implements 接口们】{

}
【修饰符】 interface 接口名<类型变量列表> 【implements 接口们】{

}

//例如:
public class ArrayList<E>
public interface Map<K,V>{
....
}
  • 声明方法时,在【修饰符】与返回值类型之间声明类型变量,我们把声明了类型变量的方法,称为泛型方法。
1
2
3
4
5
6
7
8
[修饰符] <类型变量列表> 返回值类型 方法名([形参列表])[throws 异常列表]{
//...
}

//例如:java.util.Arrays类中的
public static <T> List<T> asList(T... a){
....
}

3.2 自定义泛型类或泛型接口

当我们在类或接口中定义某个成员时,该成员的相关类型是不确定的,而这个类型需要在使用这个类或接口时才可以确定,那么我们可以使用泛型类、泛型接口。

3.2.1 说明

① 我们在声明完自定义泛型类以后,可以在类的内部(比如:属性、方法、构造器中)使用类的泛型。

② 我们在创建自定义泛型类的对象时,可以指明泛型参数类型。一旦指明,内部凡是使用类的泛型参数的位置,都具体化为指定的类的泛型类型。

③ 如果在创建自定义泛型类的对象时,没有指明泛型参数类型,那么泛型将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。

  • 经验:泛型要使用一路都用。要不用,一路都不要用。

④ 泛型的指定中必须使用引用数据类型。不能使用基本数据类型,此时只能使用包装类替换。

⑤ 除创建泛型类对象外,子类继承泛型类时、实现类实现泛型接口时,也可以确定泛型结构中的泛型参数。

如果我们在给泛型类提供子类时,子类也不确定泛型的类型,则可以继续使用泛型参数。

我们还可以在现有的父类的泛型参数的基础上,新增泛型参数。

3.2.2 注意

① 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>

② JDK7.0 开始,泛型的简化操作:ArrayList flist = new ArrayList<>();

③ 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。

④ 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];

​ 参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。

⑤ 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,但不可以在静态方法中使用类的泛型。

⑥ 异常类不能是带泛型的。

3.2.2 举例

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person<T> {
// 使用T类型定义变量
private T info;
// 使用T类型定义一般方法
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = info;
}
// 使用T类型定义构造器
public Person() {
}
public Person(T info) {
this.info = info;
}
}

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Father<T1, T2> {
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son1 extends Father {// 等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2 extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2> extends Father<Integer, T2> {
}

举例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Father<T1, T2> {
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son<A, B> extends Father{//等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2<A, B> extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2, A, B> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2, A, B> extends Father<Integer, T2> {
}

3.2.3 练习

练习1:

声明一个学生类,该学生包含姓名、成绩,而此时学生的成绩类型不确定,为什么呢,因为,语文老师希望成绩是“优秀”、“良好”、“及格”、“不及格”,数学老师希望成绩是89.5, 65.0,英语老师希望成绩是’A’,’B’,’C’,’D’,’E’。那么我们在设计这个学生类时,就可以使用泛型。

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 Student<E> {  //其实把E当做一个真实的类思考的话 其实和普通类一样!!
private String name;
private E grade;

public Student(String name, E grade) {
this.name = name;
this.grade = grade;
}

public Student() {
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public E getGrade() {
return grade;
}

public void setGrade(E grade) {
this.grade = grade;
}

@Override
public String toString() {
return "Student{" +"name='" + name + '\'' +", grade=" + grade +'}';
}

}

public class StudentTest {
public static void main(String[] args) {
//语文老师
Student<String> s1=new Student<String>("宋亚翔","优秀"); //其实可以Student s1=new Student("xx","xx");
Student s2=new Student<>("宋亚翔","优秀");
Student s3=new Student<>("宋亚翔","优秀");
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
//数学老师
Student<Double> s11=new Student<Double>("李四",88.8);
Student<Double> s12=new Student<Double>("李四",68.5);
Student<Double> s13=new Student<Double>("李四",65.0);
System.out.println(s11);
System.out.println(s12);
System.out.println(s13);
//英语老师
Student<Character> s21=new Student<Character>("李四",'A');
Student<Character> s22=new Student<Character>("李四",'B');
Student<Character> s23=new Student<Character>("李四",'C');
System.out.println(s21);
System.out.println(s22);
System.out.println(s23);
}
}
最终输出:
Student{name='宋亚翔', grade=优秀}
Student{name='宋亚翔', grade=优秀}
Student{name='宋亚翔', grade=优秀}
Student{name='李四', grade=88.8}
Student{name='李四', grade=68.5}
Student{name='李四', grade=65.0}
Student{name='李四', grade=A}
Student{name='李四', grade=B}
Student{name='李四', grade=C}

练习2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
定义个泛型类 DAO<T>,在其中定义一个Map 成员变量,Map 的键为 String 类型,值为 T 类型。

分别创建以下方法:
public void save(String id,T entity): 保存 T 类型的对象到 Map 成员变量中
public T get(String id):从 map 中获取 id 对应的对象
public void update(String id,T entity):替换 map 中key为id的内容,改为 entity 对象
public List<T> list():返回 map 中存放的所有 T 对象
public void delete(String id):删除指定 id 对象

定义一个 User 类:
该类包含:private成员变量(int类型) id,age;(String 类型)name。

定义一个测试类:
创建 DAO 类的对象, 分别调用其 save、get、update、list、delete 方法来操作 User 对象,
使用 Junit 单元测试类进行测试。

代码实现:

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
public class DAO<T> {
private Map<String,T> map=new HashMap<String,T>();;

//保存 T 类型的对象到 Map 成员变量中
public void save(String id,T entity){
if(!map.containsKey(id)){
map.put(id,entity);
}

}
//从 map 中获取 id 对应的对象
public T get(String id){
return map.get(id);
}
//替换 map 中key为id的内容,改为 entity 对象
public void update(String id,T entity){
if(map.containsKey(id)){
map.put(id,entity);
}
}
//返回 map 中存放的所有 T 对象
public List<T> list(){
Collection<T> values = map.values();
ArrayList<T> list = new ArrayList<>(values);
return list;
}
//删除指定 id 对象
public void delete(String id){
map.remove(id);
}
}
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
import java.util.Objects;
public class User {
private int id;
private int age;
private String name;

public User() {
}

public User(int id, int age, String name) {
this.id = id;
this.age = age;
this.name = name;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "User{" +"id=" + id +", age=" + age +", name='" + name + '\'' +'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return id == user.id && age == user.age && Objects.equals(name, user.name);
}

@Override
public int hashCode() {
return Objects.hash(id, age, name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.junit.Test;
import java.util.List;
public class DAOTest {
public static void main(String[] args) {
//DAO传入的是一个User类
DAO<User> dao = new DAO<>();
//set方法 new User传入
dao.save("1001",new User(1,34,"曹操"));
dao.save("1002",new User(2,33,"刘备"));
dao.save("1003",new User(3,24,"孙权"));

dao.update("1002",new User(2,23,"刘禅"));

dao.delete("1003");

List<User> list = dao.list();
for(User u : list){
System.out.println(u);
}
}
}

image-20231029201142321

3.3 自定义泛型方法

如果我们定义类、接口时没有使用<泛型参数>,但是某个方法形参类型不确定时,这个方法可以单独定义<泛型参数>。

3.3.1 说明

  • 泛型方法的格式:
1
2
3
[访问权限]  <泛型>  返回值类型  方法名([泛型标识 参数名称])  [抛出的异常]{

}
  • 方法,也可以被泛型化,与其所在的类是否是泛型类没有关系。
  • 泛型方法中的泛型参数在方法被调用时确定。
  • 泛型方法可以根据需要,声明为static的。

3.3.2 举例

举例1:

1
2
3
4
5
6
public class DAO {
public <E> E get(int id, E e) {
E result = null;
return result;
}
}

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o);
}
}

public static void main(String[] args) {
Object[] ao = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(ao, co);

String[] sa = new String[20];
Collection<String> cs = new ArrayList<>();
fromArrayToCollection(sa, cs);

Collection<Double> cd = new ArrayList<>();
// 下面代码中T是Double类,但sa是String类型,编译错误。
// fromArrayToCollection(sa, cd);
// 下面代码中T是Object类型,sa是String类型,可以赋值成功。
fromArrayToCollection(sa, co);
}

举例3:

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
class MyArrays {
public static <T> void sort(T[] arr){
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < arr.length-i; j++) {
if(((Comparable<T>)arr[j]).compareTo(arr[j+1])>0){
T temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}

public class MyArraysTest {
public static void main(String[] args) {
int[] arr = {3,2,5,1,4};
// MyArrays.sort(arr);//错误的,因为int[]不是对象数组

String[] strings = {"hello","java","song"};
MyArrays.sort(strings);
System.out.println(Arrays.toString(strings));

Circle[] circles = {new Circle(2.0),new Circle(1.2),new Circle(3.0)};
MyArrays.sort(circles); //编译通过,运行报错,因为Circle没有实现Comparable接口
}
}

3.3.3 练习

练习1: 泛型方法

编写一个泛型方法,实现任意引用类型数组指定位置元素交换。

public static void method1( E[] e,int a,int b)

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
import org.junit.Test;
public class MethodTest1 {
//使用泛型方法
public static <T> void swap(T[] arr,int a,int b){
T temp=arr[a];
arr[a]=arr[b];
arr[b]=temp;
}

@Test
public void test(){
Integer[] arr=new Integer[]{123,423,54,7655,3132,234,12,12,32};
System.out.println("交换前:");
for(Integer temp:arr){
System.out.print(temp+" ");
}
System.out.println();
//调用方法
swap(arr,1,4);
System.out.println("交换后:");
for(Integer temp:arr){
System.out.print(temp+" ");
}
}
}

image-20231029201755987

练习2: 泛型方法

编写一个泛型方法,接收一个任意引用类型的数组,并反转数组中的所有元素

public static void method2( E[] e)

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
import org.junit.Test;
public class MethodTest1 {
public static <T> void swapAll(T[] arr){
int left=0;
int right=arr.length-1;
while(left<=right){
T temp=arr[left];
arr[left]=arr[right];
arr[right]=temp;
left++;
right--;
}
}
@Test
public void test(){
Integer[] arr=new Integer[]{123,423,54,7655,3132,234,12,12,32};
System.out.println("交换前:");
for(Integer temp:arr){
System.out.print(temp+" ");
}
System.out.println();
//调用方法
swapAll(arr);
System.out.println("反转所有元素:");
for(Integer temp:arr){
System.out.print(temp+" ");
}
System.out.println();
}
}

image-20231029202242605

4. 泛型在继承上的体现

如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G并不是G的子类型!

比如:String是Object的子类,但是List并不是List的子类。

image-20220411003422259
1
2
3
4
5
6
7
8
9
10
11
12
13
public void testGenericAndSubClass() {
Person[] persons = null;
Man[] mans = null;
//Person[] 是 Man[] 的父类
persons = mans;

Person p = mans[0];

// 在泛型的集合上
List<Person> personList = null;
List<Man> manList = null;
//personList = manList;(报错)
}

思考:对比如下两段代码有何不同:

片段1:

1
2
3
4
5
6
public void printCollection(Collection c) {
Iterator i = c.iterator();
for (int k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}

片段2:

1
2
3
4
5
public void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}

5. 通配符的使用

当我们声明一个变量/形参时,这个变量/形参的类型是一个泛型类或泛型接口,例如:Comparator类型,但是我们仍然无法确定这个泛型类或泛型接口的类型变量的具体类型,此时我们考虑使用类型通配符 ? 。

5.1 通配符的理解

使用类型通配符:?

比如:List<?>Map<?,?>

List<?>List<String>List<Object>等各种泛型List的父类。

5.2 通配符的读与写

写操作:

将任意元素加入到其中不是类型安全的:

1
2
3
Collection<?> c = new ArrayList<String>();

c.add(new Object()); // 编译时错误

因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。

唯一可以插入的元素是null,因为它是所有引用类型的默认值。

读操作:

另一方面,读取List<?>的对象list中的元素时,永远是安全的,因为不管 list 的真实类型是什么,它包含的都是Object。

举例1:

1
2
3
4
5
6
7
public class TestWildcard {
public static void m4(Collection<?> coll){
for (Object o : coll) {
System.out.println(o);
}
}
}

举例2:

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) {
List<?> list = null;
list = new ArrayList<String>();
list = new ArrayList<Double>();
// list.add(3);//编译不通过
list.add(null);

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
l1.add("尚硅谷");
l2.add(15);
read(l1);
read(l2);
}

public static void read(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}

5.3 使用注意点

注意点1:编译错误:不能用在泛型方法声明上,返回值类型前面<>不能使用?

1
2
public static <?> void test(ArrayList<?> list){
}

注意点2:编译错误:不能用在泛型类的声明上

1
2
class GenericTypeClass<?>{
}

注意点3:编译错误:不能用在创建对象上,右边属于创建集合对象

1
ArrayList<?> list2 = new ArrayList<?>();

5.4 有限制的通配符

  • <?>

    • 允许所有泛型的引用调用
  • 通配符指定上限:<? extends 类/接口 >

    • 使用时指定的类型必须是继承某个类,或者实现某个接口,即<=
  • 通配符指定下限:<? super 类/接口 >

    • 使用时指定的类型必须是操作的类或接口,或者是操作的类的父类或接口的父接口,即>=
  • 说明:

    1
    2
    3
    4
    5
    6
    7
    8
    <? extends Number>     //(无穷小 , Number]
    //只允许泛型为Number及Number子类的引用调用

    <? super Number> //[Number , 无穷大)
    //只允许泛型为Number及Number父类的引用调用

    <? extends Comparable>
    //只允许泛型为实现Comparable接口的实现类的引用调用
  • 举例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
    @Test
    public void test3(){
    List<? extends Father> list=null; // ?≤ Father
    List<Object> list1=null;
    List<Father> list2=null;
    List<Son> list3=null;
    //list=list1;
    list=list2;
    list=list3;
    }

    @Test
    public void test3_1(){
    List<? extends Father> list=null; // ?≤ Father
    List<Father> list1=new ArrayList<>();
    list1.add(new Father());
    list=list1;
    //读取数据
    Father father = list.get(0); //最大是Father 那我就赋值Father
    //写入数据
    list.add(null);
    list.add(new Father()); //属于是有上限,但是还是不知道?是谁 有可能总有左边的类型比你当前类型的小,那就跟强制类型一样,但是不知道强制类型给谁
    list.add(new Son()); //属于是有上限,但是还是不知道?是谁 有可能总有左边的类型比你当前类型的小,那就跟强制类型一样,但是不知道强制类型给谁

    }

    @Test
    public void test4(){
    List<? super Father> list=null; // ?≥ Father
    List<Object> list1=null;
    List<Father> list2=null;
    List<Son> list3=null;
    list=list1;
    list=list2;
    //list=list3;
    }

    @Test
    public void test4_1(){
    List<? super Father> list=null; // ?≥ Father
    List<Father> list1=new ArrayList<>();
    list1.add(new Father());
    list=list1;
    //读取数据
    Object object = list.get(0); //最大是Object 那我就赋值Object
    //写入数据
    list.add(null);
    list.add(new Object()); //属于有下限 但是如果你写Object 永远可能有左边比你小的,那么就跟强制类型一样,但是不知道强制类型给谁
    list.add(new Father()); //属于是有下限 最高就写自己,可以让左边的所有子类写他(多态)
    list.add(new Son()); //属于是有下限 子类可以写进父类(儿子可以跟父亲借钱,父亲不可以跟儿子借钱)

    }

    image-20231030093305716

  • 举例2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public static void main(String[] args) {
    Collection<Integer> list1 = new ArrayList<Integer>();
    Collection<String> list2 = new ArrayList<String>();
    Collection<Number> list3 = new ArrayList<Number>();
    Collection<Object> list4 = new ArrayList<Object>();

    getElement1(list1);
    getElement1(list2);//报错
    getElement1(list3);
    getElement1(list4);//报错

    getElement2(list1);//报错
    getElement2(list2);//报错
    getElement2(list3);
    getElement2(list4);

    }
    // 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
    public static void getElement1(Collection<? extends Number> coll){}
    // 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
    public static void getElement2(Collection<? super Number> coll){}
  • 举例3:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public static void printCollection1(Collection<? extends Person> coll) {
    //Iterator只能用Iterator<?>或Iterator<? extends Person>.why?
    Iterator<?> iterator = coll.iterator();
    while (iterator.hasNext()) {
    Person per = iterator.next();
    System.out.println(per);
    }
    }

    public static void printCollection2(Collection<? super Person> coll) {
    //Iterator只能用Iterator<?>或Iterator<? super Person>.why?
    Iterator<?> iterator = coll.iterator();
    while (iterator.hasNext()) {
    Object obj = iterator.next();
    System.out.println(obj);
    }
    }

举例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
28
29
30
31
32
33
34
35
36
37
38
39
40
@Test
public void test1(){
//List<Object> list1 = null;
List<Person> list2 = new ArrayList<Person>();
//List<Student> list3 = null;

List<? extends Person> list4 = null;

list2.add(new Person());
list4 = list2;

//读取:可以读
Person p1 = list4.get(0);

//写入:除了null之外,不能写入
list4.add(null);
// list4.add(new Person());
// list4.add(new Student());

}

@Test
public void test2(){
//List<Object> list1 = null;
List<Person> list2 = new ArrayList<Person>();
//List<Student> list3 = null;

List<? super Person> list5 = null;
list2.add(new Person());

list5 = list2;

//读取:可以实现
Object obj = list5.get(0);

//写入:可以写入Person及Person子类的对象
list5.add(new Person());
list5.add(new Student());

}

5.5 泛型应用举例

举例1:泛型嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
HashMap<String, ArrayList<Citizen>> map = new HashMap<String, ArrayList<Citizen>>();
ArrayList<Citizen> list = new ArrayList<Citizen>();
list.add(new Citizen("赵又廷"));
list.add(new Citizen("高圆圆"));
list.add(new Citizen("瑞亚"));
map.put("赵又廷", list);
//对map的entrySet集合进行迭代器遍历
Set<Entry<String, ArrayList<Citizen>>> entrySet = map.entrySet();
Iterator<Entry<String, ArrayList<Citizen>>> iterator = entrySet.iterator();
while (iterator.hasNext()) {
Entry<String, ArrayList<Citizen>> entry = iterator.next();
String key = entry.getKey();
ArrayList<Citizen> value = entry.getValue();
System.out.println("户主:" + key);
System.out.println("家庭成员:" + value);
}
}

举例2:个人信息设计

用户在设计类的时候往往会使用类的关联关系,例如,一个人中可以定义一个信息的属性,但是一个人可能有各种各样的信息(如联系方式、基本信息等),所以此信息属性的类型就可以通过泛型进行声明,然后只要设计相应的信息类即可。

image-20220411004301224
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
interface Info{		// 只有此接口的子类才是表示人的信息
}
class Contact implements Info{ // 表示联系方式
private String address ; // 联系地址
private String telephone ; // 联系方式
private String zipcode ; // 邮政编码
public Contact(String address,String telephone,String zipcode){
this.address = address;
this.telephone = telephone;
this.zipcode = zipcode;
}
public void setAddress(String address){
this.address = address ;
}
public void setTelephone(String telephone){
this.telephone = telephone ;
}
public void setZipcode(String zipcode){
this.zipcode = zipcode;
}
public String getAddress(){
return this.address ;
}
public String getTelephone(){
return this.telephone ;
}
public String getZipcode(){
return this.zipcode;
}
@Override
public String toString() {
return "Contact [address=" + address + ", telephone=" + telephone
+ ", zipcode=" + zipcode + "]";
}
}
//实现Info接口
class Introduction implements Info{
private String name ; // 姓名
private String sex ; // 性别
private int age ; // 年龄
public Introduction(String name,String sex,int age){
this.name = name;
this.sex = sex;
this.age = age;
}
public void setName(String name){
this.name = name ;
}
public void setSex(String sex){
this.sex = sex ;
}
public void setAge(int age){
this.age = age ;
}
public String getName(){
return this.name ;
}
public String getSex(){
return this.sex ;
}
public int getAge(){
return this.age ;
}
@Override
public String toString() {
return "Introduction [name=" + name + ", sex=" + sex + ", age=" + age
+ "]";
}
}
class Person<T extends Info>{
private T info ;
public Person(T info){ // 通过构造器设置信息属性内容
this.info = info;
}
public void setInfo(T info){
this.info = info ;
}
public T getInfo(){
return info ;
}
@Override
public String toString() {
return "Person [info=" + info + "]";
}

}
public class GenericPerson{
public static void main(String args[]){
Person<Contact> per = null ; // 声明Person对象
per = new Person<Contact>(new Contact("北京市","01088888888","102206")) ;
System.out.println(per);

Person<Introduction> per2 = null ; // 声明Person对象
per2 = new Person<Introduction>(new Introduction("李雷","男",24));
System.out.println(per2) ;
}
}

JAVA-集合框架

第12章_集合框架

本章专题与脉络

第3阶段:Java高级应用-第12章

1. 集合框架概述

1.1 生活中的容器

image-20220523190743146

1.2 数组的特点与弊端

  • 一方面,面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。
  • 另一方面,使用数组存储对象方面具有一些弊端,而Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中。
  • 数组在内存存储方面的特点
    • 数组初始化以后,长度就确定了。
    • 数组中的添加的元素是依次紧密排列的,有序的,可以重复的。
    • 数组声明的类型,就决定了进行元素初始化时的类型。不是此类型的变量,就不能添加。
    • 可以存储基本数据类型值,也可以存储引用数据类型的变量
  • 数组在存储数据方面的弊端
    • 数组初始化以后,长度就不可变了,不便于扩展
    • 数组中提供的属性和方法少,不便于进行添加、删除、插入、获取元素个数等操作,且效率不高。
    • 数组存储数据的特点单一,只能存储有序的、可以重复的数据
  • Java 集合框架中的类可以用于存储多个对象,还可用于保存具有映射关系的关联数组。

1.3 Java集合框架体系

Java 集合可分为 Collection 和 Map 两大体系:

  • Collection接口:用于存储一个一个的数据,也称单列数据集合

    • List子接口:用来存储有序的、可以重复的数据(主要用来替换数组,”动态”数组)
      • 实现类:ArrayList(主要实现类)、LinkedList、Vector
  • Set子接口:用来存储无序的、不可重复的数据(类似于高中讲的”集合”)

    • 实现类:HashSet(主要实现类)、LinkedHashSet、TreeSet
  • Map接口:用于存储具有映射关系“key-value对”的集合,即一对一对的数据,也称双列数据集合。(类似于高中的函数、映射。(x1,y1),(x2,y2) —> y = f(x) )

    • HashMap(主要实现类)、LinkedHashMap、TreeMap、Hashtable、Properties
  • JDK提供的集合API位于java.util包内

  • 图示:集合框架全图

  • 简图1:Collection接口继承树
image-20220407203244029
  • 简图2:Map接口继承树
image-20220407203412665

1.4 集合的使用场景

第12章_集合的使用场景

2. Collection接口及方法

  • JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)去实现。
  • Collection 接口是 List和Set接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 集合。方法如下:

2.1 添加

(1)add(E obj):添加元素对象到当前集合中
(2)addAll(Collection other):添加other集合中的所有元素对象到当前集合中,即this = this ∪ other

注意:add和addAll的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test(){
Collection coll=new ArrayList<>();
//添加
//1.add()
coll.add("AA");
coll.add(123); //自动装箱为包装类
coll.add("桑鬼谷");
coll.add("尚硅谷");
coll.add(new Object());
System.out.println(coll); //[AA, 123, 桑鬼谷, 尚硅谷, java.lang.Object@77f03bb1]
//2.addAll(Collection x)
Collection coll1=new ArrayList();
coll1.add("BB");
coll1.add(456);
//将coll1里面的所有元素一次性加入到coll
coll.addAll(coll1);
System.out.println(coll); //[AA, 123, 桑鬼谷, 尚硅谷, java.lang.Object@77f03bb1, BB, 456]
}

注意:coll.addAll(other);与coll.add(other);

2.2 判断

(3)int size():获取当前集合中实际存储的元素个数
(4)boolean isEmpty():判断当前集合是否为空集合
(5)boolean contains(Object obj):判断当前集合中是否存在一个与obj对象equals返回true的元素
(6)boolean containsAll(Collection coll):判断coll集合中的元素是否在当前集合中都存在。即coll集合是否是当前集合的“子集”
(7)boolean equals(Object obj):判断当前集合与obj是否相等

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
@Test
public void test1(){
Collection coll=new ArrayList<>();
coll.add("AA");
coll.add(123); //自动装箱为包装类
coll.add("桑鬼谷");
coll.add("尚硅谷");
coll.add(new Object());
System.out.println(coll); //[AA, 123, 桑鬼谷, 尚硅谷, java.lang.Object@77f03bb1]
Collection coll1=new ArrayList();
coll1.add("AA");
coll1.add(123);
System.out.println(coll1);
//判断
//(3)int size():获取当前集合中实际存储的元素个数
System.out.println(coll.size()); //5
//(4)boolean isEmpty():判断当前集合是否为空集合
System.out.println(coll.isEmpty()); //false

//比较的是值是否相等!!!!!
//(5)boolean contains(Object obj):判断当前集合中是否存在一个与obj对象equals返回true的元素
System.out.println(coll.contains(coll1)); //false 就是coll1把两个元素当成一个元素了 所以匹配不到
System.out.println(coll.contains("AA")); //true
//(6)boolean containsAll(Collection coll1):判断coll1集合中的元素是否在当前集合中都存在。即coll1集合是否是当前集合coll的“子集”
System.out.println(coll.containsAll(coll1)); //true
//(7)boolean equals(Object obj):判断当前集合与obj是否相等
System.out.println(coll.equals(coll1));
}

2.3 删除

(8)void clear():清空集合元素
(9) boolean remove(Object obj) :从当前集合中删除第一个找到的与obj对象equals返回true的元素。
(10)boolean removeAll(Collection coll):从当前集合中删除所有与coll集合中相同的元素。即this = this - this ∩ coll
(11)boolean retainAll(Collection coll):从当前集合中删除两个集合中不同的元素,使得当前集合仅保留与coll集合中的元素相同的元素,即当前集合中仅保留两个集合的交集,即this = this ∩ coll;

注意几种删除方法的区别

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
@Test
public void test2(){
Collection coll=new ArrayList<>();
coll.add("AA");
coll.add(123); //自动装箱为包装类
coll.add("桑鬼谷");
coll.add("尚硅谷");
coll.add(new Object());
System.out.println(coll); //[AA, 123, 桑鬼谷, 尚硅谷, java.lang.Object@77f03bb1]
Collection coll1=new ArrayList();
coll1.add("AA");
coll1.add(123);
coll1.add("尚硅谷");
System.out.println(coll1); //[AA, 123]
System.out.println("-----------------------------------------");
//删除
//(9) boolean remove(Object obj) :从当前集合中删除第一个找到的与obj对象equals返回true的元素。
System.out.println(coll.remove("AAA")); //false
System.out.println(coll.remove("AA")); //true
System.out.println(coll); //[123, 桑鬼谷, 尚硅谷, java.lang.Object@77f03bb1]
System.out.println("-----------------------------------------");
//(10)boolean removeAll(Collection coll1):从当前集合中删除所有与coll1集合中相同的元素。即this = this - this ∩ coll1 ---删除两个集合的交集
System.out.println(coll.removeAll(coll1)); //true
System.out.println(coll); //[桑鬼谷, java.lang.Object@77f03bb1]
System.out.println("-----------------------------------------");
//(11)boolean retainAll(Collection coll1):从当前集合中删除两个集合中不同的元素,使得当前集合仅保留与coll1集合中的元素相同的元素,即当前集合中仅保留两个集合的交集,即this = this ∩ coll1; ---保留两个集合的交集
System.out.println(coll.retainAll(coll1)); //true
System.out.println(coll); //[]
//(8)void clear():清空集合元素 ---底层一个一个=null清空
coll1.clear();
System.out.println(coll1.size()); //0
System.out.println("-----------------------------------------");
}

2.4 其它

(12)Object[] toArray():返回包含当前集合中所有元素的数组
(13)hashCode():获取集合对象的哈希值
(14)iterator():返回迭代器对象,用于集合遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test3(){
Collection coll=new ArrayList<>();
coll.add("AA");
coll.add(123); //自动装箱为包装类
coll.add("桑鬼谷");
coll.add("尚硅谷");
coll.add(new Object());
System.out.println(coll); //[AA, 123, 桑鬼谷, 尚硅谷, java.lang.Object@77f03bb1]
Collection coll1=new ArrayList();
coll1.add("AA");
coll1.add(123);
System.out.println(coll1);
System.out.println("-----------------------------------------");
//其他
//(12)Object[] toArray():返回包含当前集合中所有元素的数组
System.out.println(Arrays.toString(coll.toArray())); //[AA, 123, 桑鬼谷, 尚硅谷, java.lang.Object@77f03bb1]
//(13)hashCode():获取集合对象的哈希值
System.out.println(coll.hashCode()); //514809589
//(14)iterator():返回迭代器对象,用于集合遍历
System.out.println(coll.iterator()); //java.util.ArrayList$Itr@326de728
}

2.5 数组和集合的转换

(15)toArray():集合 —> 数组
(16)Arrays.asList(Object …objs):数组 —> 集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void test4(){
Collection coll=new ArrayList<>();
coll.add("AA");
coll.add(123); //自动装箱为包装类
coll.add("桑鬼谷");
coll.add("尚硅谷");
coll.add(new Object());
System.out.println(coll); //[AA, 123, 桑鬼谷, 尚硅谷, java.lang.Object@77f03bb1]
//集合 ---> 数组
Object[] arr1=coll.toArray();
//1.Arrays.toString()输出
System.out.println(Arrays.toString(arr1)); //[AA, 123, 桑鬼谷, 尚硅谷, java.lang.Object@77f03bb1]
//2.for循环遍历输出
for (int i = 0; i < arr1.length; i++) {
System.out.print(arr1[i]+" "); //AA 123 桑鬼谷 尚硅谷 java.lang.Object@77f03bb1
}
System.out.println();
//数组 ---> 集合
Integer[] arr = new Integer[]{1,2,3};
List list= Arrays.asList(arr);
System.out.println(list); //[1, 2, 3]
}

3. Iterator(迭代器)接口

3.1 Iterator接口

  • 在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.IteratorIterator接口也是Java集合中的一员,但它与CollectionMap接口有所不同。
    • Collection接口与Map接口主要用于存储元素
    • Iterator,被称为迭代器接口,本身并不提供存储对象的能力,主要用于遍历Collection中的元素
  • Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。

    • public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的。
    • 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
  • Iterator接口的常用方法如下:

    • public E next():返回迭代的下一个元素。
    • public boolean hasNext():如果仍有元素可以迭代,则返回 true。
  • 注意:在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常

举例:

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
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class TestIterator {
@Test
public void test01(){
Collection coll = new ArrayList();
coll.add("小李广");
coll.add("扫地僧");
coll.add("石破天");
Iterator iterator = coll.iterator();
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next());
System.out.println(iterator.next()); //报NoSuchElementException异常
}

@Test
public void test02(){
Collection coll = new ArrayList();
coll.add("小李广");
coll.add("扫地僧");
coll.add("石破天");

//正确方法
Iterator iterator = coll.iterator();//获取迭代器对象
while(iterator.hasNext()) {//判断是否还有元素可迭代
System.out.println(iterator.next());//取出下一个元素
}
}
}

3.2 迭代器的执行原理

Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,接下来通过一个图例来演示Iterator对象迭代元素的过程:

image-20220407235130988

使用Iterator迭代器删除元素:java.util.Iterator迭代器中有一个方法:void remove() ;

1
2
3
4
5
6
7
Iterator iter = coll.iterator();//回到起点
while(iter.hasNext()){
Object obj = iter.next();
if(obj.equals("Tom")){
iter.remove();
}
}

注意:

  • Iterator可以删除集合的元素,但是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法。

  • 如果还未调用next()或在上一次调用 next() 方法之后已经调用了 remove() 方法,再调用remove()都会报IllegalStateException。

  • Collection已经有remove(xx)方法了,为什么Iterator迭代器还要提供删除方法呢?因为迭代器的remove()可以按指定的条件进行删除。

例如:要删除以下集合元素中的偶数

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
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class TestIteratorRemove {
@Test
public void test01(){
Collection coll = new ArrayList();
coll.add(1);
coll.add(2);
coll.add(3);
coll.add(4);
coll.add(5);
coll.add(6);

Iterator iterator = coll.iterator();
while(iterator.hasNext()){
Integer element = (Integer) iterator.next();
if(element % 2 == 0){
iterator.remove();
}
}
System.out.println(coll);
}
}

在JDK8.0时,Collection接口有了removeIf 方法,即可以根据条件删除。(第18章中再讲)

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
package com.atguigu.collection;

import org.junit.Test;

import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Predicate;

public class TestCollectionRemoveIf {
@Test
public void test01(){
Collection coll = new ArrayList();
coll.add("小李广");
coll.add("扫地僧");
coll.add("石破天");
coll.add("佛地魔");
System.out.println("coll = " + coll);

coll.removeIf(new Predicate() {
@Override
public boolean test(Object o) {
String str = (String) o;
return str.contains("地");
}
});
System.out.println("删除包含\"地\"字的元素之后coll = " + coll);
}
}

3.3 foreach循环

  • foreach循环(也称增强for循环)是 JDK5.0 中定义的一个高级for循环,专门用来遍历数组和集合的。
  • foreach循环的语法格式:
1
2
3
4
for(元素的数据类型 局部变量 : Collection集合或数组){ 
//操作局部变量的输出操作
}
//这里局部变量就是一个临时变量,自己命名就可以
  • 举例:
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
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
public class TestForeach {
@Test
public void test01(){
Collection coll = new ArrayList();
coll.add("小李广");
coll.add("扫地僧");
coll.add("石破天");
//foreach循环其实就是使用Iterator迭代器来完成元素的遍历的。
for (Object o : coll) {
System.out.println(o);
}
}
@Test
public void test02(){
int[] nums = {1,2,3,4,5};
for (int num : nums) {
System.out.println(num);
}
System.out.println("-----------------");
String[] names = {"张三","李四","王五"};
for (String name : names) {
System.out.println(name);
}
}
}
  • 对于集合的遍历,增强for的内部原理其实是个Iterator迭代器。如下图。

image-20220128010114124

  • 它用于遍历Collection和数组。通常只进行遍历元素,不要在遍历的过程中对集合元素进行增删操作。
    • 练习:判断输出结果为何?
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 ForTest {
public static void main(String[] args) {
String[] str = new String[5];
for (String myStr : str) {
myStr = "atguigu";
System.out.println(myStr);
}
for (int i = 0; i < str.length; i++) {
System.out.println(str[i]); //str没变化都是null
}
}
}
最终输出:
atguigu
atguigu
atguigu
atguigu
atguigu
null
null
null
null
null

4. Collection子接口1:List

4.1 List接口特点

  • 鉴于Java中数组用来存储数据的局限性,我们通常使用java.util.List替代数组

  • List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。

    • 举例:List集合存储数据,就像银行门口客服,给每一个来办理业务的客户分配序号:第一个来的是“张三”,客服给他分配的是0;第二个来的是“李四”,客服给他分配的1;以此类推,最后一个序号应该是“总人数-1”。

1563549818689

  • JDK API中List接口的实现类常用的有:ArrayListLinkedListVector

4.2 List接口方法

List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。

  • 插入元素

    • void add(int index, Object ele):在index位置插入ele元素
    • boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
  • 获取元素

    • Object get(int index):获取指定index位置的元素
    • List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
  • 获取元素索引

    • int indexOf(Object obj):返回obj在集合中首次出现的位置
    • int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
  • 删除和替换元素

    • Object remove(int index):移除指定index位置的元素,并返回此元素

    • Object set(int index, Object ele):设置指定index位置的元素为ele

举例:

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
import java.util.ArrayList;
import java.util.List;
public class TestListMethod {
public static void main(String[] args) {
// 创建List集合对象
List<String> list = new ArrayList<String>();

// 往 尾部添加 指定元素
list.add("图图");
list.add("小美");
list.add("不高兴");

System.out.println(list);
// add(int index,String s) 往指定位置添加
list.add(1,"没头脑");

System.out.println(list);
// String remove(int index) 删除指定位置元素 返回被删除元素
// 删除索引位置为2的元素
System.out.println("删除索引位置为2的元素");
System.out.println(list.remove(2));

System.out.println(list);

// String set(int index,String s)
// 在指定位置 进行 元素替代(改)
// 修改指定位置元素
list.set(0, "三毛");
System.out.println(list);

// String get(int index) 获取指定位置元素
// 跟size() 方法一起用 来 遍历的
for(int i = 0;i<list.size();i++){
System.out.println(list.get(i));
}
//还可以使用增强for
for (String string : list) {
System.out.println(string);
}
}
}

注意:在JavaSE中List名称的类型有两个,一个是java.util.List集合接口,一个是java.awt.List图形界面的组件,别导错包了。

4.3 List接口主要实现类:ArrayList

  • ArrayList 是 List 接口的主要实现类

  • 本质上,ArrayList是对象引用的一个”变长”数组

  • Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是 Vector 实例。 Arrays.asList(…) 返回值是一个固定长度的 List 集合

    image-20220408210743342

4.4 List的实现类之二:LinkedList

  • 对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高。这是由底层采用链表(双向链表)结构存储数据决定的。

image-20220408225615829

  • 特有方法:
    • void addFirst(Object obj)
    • void addLast(Object obj)
    • Object getFirst()
    • Object getLast()
    • Object removeFirst()
    • Object removeLast()

4.5 List的实现类之三:Vector

  • Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
  • 在各种List中,最好把ArrayList作为默认选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。
  • 特有方法:
    • void addElement(Object obj)
    • void insertElementAt(Object obj,int index)
    • void setElementAt(Object obj,int index)
    • void removeElement(Object obj)
    • void removeAllElements()

4.6 练习

面试题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class Test1 {
@Test
public void testListRemove() {
//创建一个arraylist实现类对象
//多态
List list = new ArrayList();
//添加集合元素
list.add(1);
list.add(2);
list.add(3);
System.out.println(list); //[1,2,3]
updateList(list); //调用updateList方法
System.out.println(list);//[1,2]
}

private static void updateList(List list) {
//移除下标为2的值
list.remove(2); //list=[1,2]
}
}

练习1:

  • 定义学生类,属性为姓名、年龄,提供必要的getter、setter方法,构造器,toString(),equals()方法。
  • 使用ArrayList集合,保存录入的多个学生对象。
  • 循环录入的方式,1:继续录入,0:结束录入。
  • 录入结束后,用foreach遍历集合。
  • 代码实现,效果如图所示:

    1559890098509

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.ArrayList;
import java.util.Iterator;
public class StudentTest {
public static void main(String[] args) {
ArrayList list=new ArrayList();
list.add(new Student("宋亚翔",12));
list.add(new Student("宋亚翔2",121));
list.add(new Student("宋亚翔3",11222));
list.add(new Student("宋亚翔4",13122));
list.add(new Student("宋亚翔5",112));
list.add(new Student("宋亚翔6",1122));
//增强for循环
for(Object stu:list){
System.out.println(stu);
}
System.out.println();
//迭代器
Iterator iterator=list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
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
import java.util.Objects;
public class Student {
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student student)) return false;
return getAge() == student.getAge() && Objects.equals(getName(), student.getName());
}

@Override
public String toString() {
return "Student{" +"name='" + name + '\'' +", age=" + age +'}';
}

}

image-20231025152940101

练习2:

​ 1、请定义方法public static int listTest(Collection list,String s)统计集合中指定元素出现的次数

​ 2、创建集合,集合存放随机生成的30个小写字母

​ 3、用listTest统计,a、b、c、x元素的出现次数

​ 4、效果如下

1559896150606

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
import java.util.ArrayList;
import java.util.Collection;
import java.util.Random;
public class Test2 {
public static void main(String[] args) {
ArrayList list=new ArrayList();
Random random=new Random();
for (int i = 0; i < 30; i++) {
int temp= random.nextInt(26); //获取0-26范围的整数
char tempchar= (char) ('a'+temp); //在a基础上加0-26 因为char会自动类型转换为int,要char就要强转
list.add(tempchar);
}
System.out.println("随机小写字母为:");
System.out.println(list);
System.out.println("a"+":"+listTest(list,"a"));
System.out.println("b"+":"+listTest(list,"b"));
System.out.println("c"+":"+listTest(list,"c"));
System.out.println("x"+":"+listTest(list,"x"));
}
//统计集合中指定元素出现的次数
public static int listTest(Collection list,String s){
//s转为字符
char[] arr=s.toCharArray();
int sum=0;
for(Object temp:list){
if(arr[0]==(char)temp){ //arr[0]就是要判断的字符 依次和list元素匹配
sum++;
}
}
return sum;
}

}

image-20231025154852384

练习3:KTV点歌系统

描述

分别使用ArrayList和LinkedList集合,编写一个KTV点歌系统的程序。在程序中:

  • 指令1代表添加歌曲
  • 指令2代表将所选歌曲置顶
  • 指令3代表将所选歌曲提前一位
  • 指令4代表退出该系统

要求根据用户输入的指令和歌曲名展现歌曲列表。例如输入指令1,输入歌曲名”爱你一万年”,则输出“当前歌曲列表:[爱你一万年]”。

提示

  • 为了指引用户操作,首先要将各个指令所表示的含义打印到控制台

    1
    2
    3
    4
    5
    System.out.println("-------------欢迎来到点歌系统------------");
    System.out.println("1.添加歌曲至列表");
    System.out.println("2.将歌曲置顶");
    System.out.println("3.将歌曲前移一位");
    System.out.println("4.退出");
  • 程序中需要创建一个集合作为歌曲列表,并向其添加一部分歌曲

  • 通过ArrayList或LinkedList集合定义的方法操作歌曲列表

代码

  • 使用ArrayList集合模拟点歌系统的实现代码,如下所示:

    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
    /**
    * @author 尚硅谷-宋红康
    * @create 20:26
    */
    public class KTVByArrayList {
    private static ArrayList musicList = new ArrayList();// 创建歌曲列表
    private static Scanner sc = new Scanner(System.in);

    public static void main(String[] args) {
    addMusicList();// 添加一部分歌曲至歌曲列表
    boolean flag = true;
    while (flag) {
    System.out.println("当前歌曲列表:" + musicList);
    System.out.println("-------------欢迎来到点歌系统------------");
    System.out.println("1.添加歌曲至列表");
    System.out.println("2.将歌曲置顶");
    System.out.println("3.将歌曲前移一位");
    System.out.println("4.退出");
    System.out.print("请输入操作序号:");
    int key = sc.nextInt();// //接收键盘输入的功能选项序号
    // 执行序号对应的功能
    switch (key) {
    case 1:// 添加歌曲至列表
    addMusic();
    break;
    case 2:// 将歌曲置顶
    setTop();
    break;
    case 3:// 将歌曲前移一位
    setBefore();
    break;
    case 4:// 退出
    System.out.println("----------------退出---------------");
    System.out.println("您已退出系统");
    flag = false;
    break;
    default:
    System.out.println("----------------------------------");
    System.out.println("功能选择有误,请输入正确的功能序号!");
    break;
    }

    }
    }

    // 初始时添加歌曲名称
    private static void addMusicList() {
    musicList.add("本草纲目");
    musicList.add("你是我的眼");
    musicList.add("老男孩");
    musicList.add("白月光与朱砂痣");
    musicList.add("不谓侠");
    musicList.add("爱你");
    }

    // 执行添加歌曲
    private static void addMusic() {
    System.out.print("请输入要添加的歌曲名称:");
    String musicName = sc.next();// 获取键盘输入内容
    musicList.add(musicName);// 添加歌曲到列表的最后
    System.out.println("已添加歌曲:" + musicName);
    }

    // 执行将歌曲置顶
    private static void setTop() {
    System.out.print("请输入要置顶的歌曲名称:");
    String musicName = sc.next();// 获取键盘输入内容
    int musicIndex = musicList.indexOf(musicName);// 查找指定歌曲位置
    if (musicIndex < 0) {// 判断输入歌曲是否存在
    System.out.println("当前列表中没有输入的歌曲!");
    }else if(musicIndex == 0){
    System.out.println("当前歌曲默认已置顶!");
    }else {
    musicList.remove(musicName);// 移除指定的歌曲
    musicList.add(0, musicName);// 将指定的歌曲放到第一位
    System.out.println("已将歌曲《" + musicName + "》置顶");
    }
    }

    // 执行将歌曲置前一位
    private static void setBefore() {
    System.out.print("请输入要置前的歌曲名称:");
    String musicName = sc.next();// 获取键盘输入内容
    int musicIndex = musicList.indexOf(musicName);// 查找指定歌曲位置
    if (musicIndex < 0) {// 判断输入歌曲是否存在
    System.out.println("当前列表中没有输入的歌曲!");
    } else if (musicIndex == 0) {// 判断歌曲是否已在第一位
    System.out.println("当前歌曲已在最顶部!");
    } else {
    musicList.remove(musicName);// 移除指定的歌曲
    musicList.add(musicIndex - 1, musicName);// 将指定的歌曲放到前一位
    System.out.println("已将歌曲《" + musicName + "》置前一位");
    }
    }
    }

5. Collection子接口2:Set

5.1 Set接口概述

  • Set接口是Collection的子接口,Set接口相较于Collection接口没有提供额外的方法【list接口有自己的增删查改】
  • Set 集合存储不相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
  • Set集合支持的遍历方式和Collection集合一样:foreach和Iterator。
  • Set的常用实现类有:HashSet、TreeSet、LinkedHashSet。

5.2 Set主要实现类:HashSet

5.2.1 HashSet概述

  • HashSet 是 Set 接口的主要实现类,大多数时候使用 Set 集合时都使用这个实现类。

  • HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存储、查找、删除性能。

  • HashSet 具有以下特点

    • 不能保证元素的排列顺序【假设用取余法,那本身存放位置相同(都是放在下标为1),如果冲突的话可能存放在其他位置,】
    • HashSet 不是线程安全的
    • 集合元素可以是 null
  • HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法得到的哈希值相等,并且两个对象的 equals()方法返回值为true。

  • 对于存放在Set容器中的对象,对应的类一定要重写hashCode()和equals(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。

  • HashSet集合中元素的无序性,不等同于随机性。这里的无序性与元素的添加位置有关。具体来说:我们在添加每一个元素到数组中时,具体的存储位置是由元素的hashCode()调用后返回的hash值决定的。导致在数组中每个元素不是依次紧密存放的,表现出一定的无序性。

5.2.2 HashSet中添加元素的过程:

  • 第1步:当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法得到该对象的 hashCode值,然后根据 hashCode值,通过某个散列函数决定该对象在 HashSet 底层数组中的存储位置。

  • 第2步:如果要在数组中存储的位置上没有元素,则直接添加成功。

  • 第3步:如果要在数组中存储的位置上有元素,则继续比较:

    • 如果两个元素的hashCode值不相等,则添加成功;
    • 如果两个元素的hashCode()值相等,则会继续调用equals()方法:
      • 如果equals()方法结果为false,则添加成功。
      • 如果equals()方法结果为true,则添加失败。

    第2步添加成功,元素会保存在底层数组中。

    第3步两种添加成功的操作,由于该底层数组的位置已经有元素了,则会通过链表的方式继续链接,存储。

举例:

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
import java.util.Objects;
public class MyDate {
private int year;
private int month;
private int day;

public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyDate myDate = (MyDate) o;
return year == myDate.year &&
month == myDate.month &&
day == myDate.day;
}

@Override
public int hashCode() {
return Objects.hash(year, month, day);
}

@Override
public String toString() {
return "MyDate{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
}
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
import org.junit.Test;
import java.util.HashSet;
public class TestHashSet {
@Test
public void test01(){
HashSet set = new HashSet();
set.add("张三");
set.add("张三");
set.add("李四");
set.add("王五");
set.add("王五");
set.add("赵六");
System.out.println("set = " + set);//不允许重复,无序
}

@Test
public void test02(){
HashSet set = new HashSet();
set.add(new MyDate(2021,1,1));
set.add(new MyDate(2021,1,1));
set.add(new MyDate(2022,2,4));
set.add(new MyDate(2022,2,4));
System.out.println("set = " + set);//不允许重复,无序
}
}

5.2.3 重写 hashCode() 方法的基本原则

  • 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
  • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
  • 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

注意:如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功。

5.2.4 重写equals()方法的基本原则

  • 重写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。

  • 推荐:开发中直接调用Eclipse/IDEA里的快捷键自动重写equals()和hashCode()方法即可。

    • 为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?
    1
    2
    3
    4
    5
    6
    7
    首先,选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突)

    其次,31只占用5bits,相乘造成数据溢出的概率较小。

    再次,31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)

    最后,31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有1来整除!(减少冲突)

5.2.5 练习

练习1:在List内去除重复数字值,要求尽量简单

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
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
public class Test1 {
public static void main(String[] args) {
List list=new ArrayList();
list.add(123);
list.add(123);
list.add(123);
list.add(123);
list.add(123);
list.add(123);
list.add(1234234);
list=delete(list);
System.out.println(list); //[123,1234234]
}
//去重方法
public static List delete(List list){
ArrayList arrayList=new ArrayList();
HashSet set=new HashSet();
//增强for循环
for(Object temp:list){
set.add(temp); //set自带不能存储相同元素的功能
}
return new ArrayList(set); //使用ArrayList构造器创建
}
}

练习2:获取随机数

编写一个程序,获取10个1至20的随机数,要求随机数不能重复。并把最终的随机数输出到控制台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.HashSet;
import java.util.Random;
public class Test2 {
public static void main(String[] args) {
//如果使用ArrayList的话就用for循环
//如果使用HashSet的话就要考虑每次的不一定存进去,就要使用while
HashSet set=new HashSet();
Random random=new Random();
//生成10个
while(set.size()<10){
int temp=random.nextInt(20); //生成随机数
set.add(temp);
}
System.out.println(set); //[17, 1, 3, 19, 8, 10, 12, 13, 14, 15]
}
}

练习3:去重

使用Scanner从键盘读取一行输入,去掉其中重复字符,打印出不同的那些字符。比如:aaaabbbcccddd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.HashSet;
import java.util.Scanner;
public class Test3 {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
//输入一行字符串
String str=input.next();
//转为字符数组(一位一位判断)
char[] arr=str.toCharArray();
//去重
HashSet set=new HashSet();
//增强for循环进行添加
for(Object temp:arr){
set.add(temp);
}
System.out.println(set);
}
}

练习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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.HashSet;
public class Test4 {
public static void main(String[] args) {
//存储不重复的元素
HashSet set = new HashSet();
Person p1 = new Person("AA", 1001);
Person p2 = new Person("BB", 1002); //其中Person类中重写了hashCode()和equal()方法 -->啥都相同也可添加!
set.add(p1);
set.add(p2);
System.out.println("set里的元素:"+set);
System.out.println("---------------------------------------------------------------------------------------------");
//将元素p1更改姓名
p1.name = "CC";
System.out.println("set里的元素更改信息之后:"+set);
System.out.println("---------------------------------------------------------------------------------------------");
//移除元素
set.remove(p1);
System.out.println("set移除完元素如下:"+set);
System.out.println("---------------------------------------------------------------------------------------------");
//添加元素
set.add(new Person("CC", 1001));
System.out.println("set里添加元素:"+set);
System.out.println("---------------------------------------------------------------------------------------------");
//添加元素
set.add(new Person("AA", 1001));
System.out.println("set里添加元素:"+set);
System.out.println("---------------------------------------------------------------------------------------------");
}
}
最终输出:
set里的元素:[Person{name='BB', age=1002}, Person{name='AA', age=1001}]
---------------------------------------------------------------------------------------------
set里的元素更改信息之后:[Person{name='BB', age=1002}, Person{name='CC', age=1001}]
---------------------------------------------------------------------------------------------
set移除完元素如下:[Person{name='BB', age=1002}, Person{name='CC', age=1001}]
---------------------------------------------------------------------------------------------
set里添加元素:[Person{name='BB', age=1002}, Person{name='CC', age=1001}, Person{name='CC', age=1001}]
---------------------------------------------------------------------------------------------
Person equals()...
set里添加元素:[Person{name='BB', age=1002}, Person{name='CC', age=1001}, Person{name='CC', age=1001}, Person{name='AA', age=1001}]
---------------------------------------------------------------------------------------------

5.3 Set实现类之二:LinkedHashSet

  • LinkedHashSet 是 HashSet 的子类,不允许集合元素重复。

  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以添加顺序保存的。

  • LinkedHashSet插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。

image-20220408235936404

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.junit.Test;
import java.util.LinkedHashSet;
public class TestLinkedHashSet {
@Test
public void test01(){
LinkedHashSet set = new LinkedHashSet();
set.add("张三");
set.add("张三");
set.add("李四");
set.add("王五");
set.add("王五");
set.add("赵六");
System.out.println("set = " + set);//不允许重复,体现添加顺序 set = [张三, 李四, 王五, 赵六]
}
}

5.4 Set实现类之三:TreeSet

5.4.1 TreeSet概述

  • TreeSet 是 SortedSet 接口的实现类,TreeSet 可以按照添加的元素的指定的属性的大小顺序进行遍历
  • TreeSet底层使用红黑树结构存储数据
  • 新增的方法如下: (了解)
    • Comparator comparator()
    • Object first()
    • Object last()
    • Object lower(Object e)
    • Object higher(Object e)
    • SortedSet subSet(fromElement, toElement)
    • SortedSet headSet(toElement)
    • SortedSet tailSet(fromElement)
  • TreeSet特点:不允许重复、实现排序(自然排序或定制排序)
  • TreeSet 两种排序方法:自然排序定制排序。默认情况下,TreeSet 采用自然排序。
    • 自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列。
      • 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。
      • 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。
    • 定制排序:如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。
      • 利用int compare(T o1,T o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
      • 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
  • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类的对象
  • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 或compare(Object o1,Object o2)方法比较返回值。返回值为0,则认为两个对象相等。

5.4.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
import org.junit.Test;
import java.util.Iterator;
import java.util.TreeSet;
public class TreeSetTest {
/*
* 自然排序:针对String类的对象
* */
@Test
public void test1(){
TreeSet set = new TreeSet();

set.add("MM");
set.add("CC");
set.add("AA");
set.add("DD");
set.add("ZZ");
//set.add(123); //报ClassCastException的异常

Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
/*
* 自然排序:针对User类的对象
* */
@Test
public void test2(){
TreeSet set = new TreeSet();

set.add(new User("Tom",12));
set.add(new User("Rose",23));
set.add(new User("Jerry",2));
set.add(new User("Eric",18));
set.add(new User("Tommy",44));
set.add(new User("Jim",23));
set.add(new User("Maria",18));
//set.add("Tom");

Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}

System.out.println(set.contains(new User("Jack", 23))); //true
}
}

其中,User类定义如下:

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
public class User implements Comparable{
String name;
int age;

public User() {
}

public User(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
/*
举例:按照age从小到大的顺序排列,如果age相同,则按照name从大到小的顺序排列
* */
public int compareTo(Object o) {
if(this == o){
return 0;
}

if(o instanceof User){
User user = (User)o;
int value = this.age - user.age;
if(value != 0){
return value;
}
return -this.name.compareTo(user.name);
}
throw new RuntimeException("输入的类型不匹配");
}
}

举例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
/*
* 定制排序
* */
@Test
public void test3(){
//按照User的姓名的从小到大的顺序排列
Comparator comparator = new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;

return u1.name.compareTo(u2.name);
}
throw new RuntimeException("输入的类型不匹配");
}
};
TreeSet set = new TreeSet(comparator);

set.add(new User("Tom",12));
set.add(new User("Rose",23));
set.add(new User("Jerry",2));
set.add(new User("Eric",18));
set.add(new User("Tommy",44));
set.add(new User("Jim",23));
set.add(new User("Maria",18));
//set.add(new User("Maria",28));

Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}

5.4.3 练习

练习1:在一个List集合中存储了多个无大小顺序并且有重复的字符串,定义一个方法,让其有序(从小到大排序),并且不能去除重复元素。

提示:考查ArrayList、TreeSet

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
import java.util.ArrayList;
import java.util.Comparator;
import java.util.TreeSet;
public class Test11 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("ccc");
list.add("ccc");
list.add("aaa");
list.add("aaa");
list.add("bbb");
list.add("ddd");
list.add("ddd");
System.out.println("排序前:"+list);
sort(list);
System.out.println("排序后:"+list);
}

public static void sort(ArrayList list){
TreeSet set=new TreeSet(new Comparator() { //实现Comparator接口,重写compare方法,然后传入构造器内
@Override
public int compare(Object o1, Object o2) {
String s1 = (String)o1;
String s2 = (String)o2;
int num = s1.compareTo(s2); // 调用字符串重写的compareTo()方法
return num == 0 ? 1 : num; // 如果内容一样返回一个不为0的数字即可
}
});
set.addAll(list); // 将list集合中的所有元素添加到set中
list.clear(); // 清空list
list.addAll(set); // 将set中排序并保留重复的结果返回到list中
}

}
最终输出:
排序前:[ccc, ccc, aaa, aaa, bbb, ddd, ddd]
排序后:[aaa, aaa, bbb, ccc, ccc, ddd, ddd]

练习2:TreeSet的自然排序和定制排序

  1. 定义一个Employee类。
    该类包含:private成员变量name,age,birthday,其中 birthday 为 MyDate 类的对象;
    并为每一个属性定义 getter, setter 方法;
    并重写 toString 方法输出 name, age, birthday

  2. MyDate类包含:
    private成员变量year,month,day;并为每一个属性定义 getter, setter 方法;

  3. 创建该类的 5 个对象,并把这些对象放入 TreeSet 集合中(下一章:TreeSet 需使用泛型来定义)

  4. 分别按以下两种方式对集合中的元素进行排序,并遍历输出:

    1). 使Employee 实现 Comparable 接口,并按 name 排序[Employee类就要实现Comparable接口]
    2). 创建 TreeSet 时传入 Comparator对象,按生日日期的先后排序。[MyDate类就要实现Comparable接口]

代码实现:

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
public class Employee implements Comparable{  //实现自然排序  必须实现Comparable接口
private String name;
private int age;
private MyDate birthday;
public Employee() {

}

public Employee(String name, int age, MyDate birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}

@Override
public String toString() {
return "Employee{" +"name='" + name + '\'' +", age=" + age +", birthday=" + birthday +'}';
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public MyDate getBirthday() {
return birthday;
}

public void setBirthday(MyDate birthday) {
this.birthday = birthday;
}

@Override
public int compareTo(Object o) {
if(o==this){
return 0;
}
if(o instanceof Employee){
Employee temp=(Employee) o;
return this.getName().compareTo(temp.getName()); //根据年龄排序
}
throw new RuntimeException("传入的类型不匹配");
}
}
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
public class MyDate implements Comparable{  //实现自然排序  必须实现Comparable接口
private int year;
private int month;
private int day;

public MyDate() {
}

public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public int getMonth() {
return month;
}

public void setMonth(int month) {
this.month = month;
}

public int getDay() {
return day;
}

public void setDay(int day) {
this.day = day;
}

@Override
public String toString() {
return "MyDate{" +"year=" + year +", month=" + month +", day=" + day +'}';
}

@Override
public int compareTo(Object o) {
if(this == o){
return 0;
}
if(o instanceof MyDate){
MyDate myDate = (MyDate) o;
int yearDistance = this.getYear() - myDate.getYear();
if(yearDistance != 0){
return yearDistance;
}
int monthDistance = this.getMonth() - myDate.getMonth();
if(monthDistance != 0){
return monthDistance;
}
return this.getDay() - myDate.getDay();
}
throw new RuntimeException("输入的类型不匹配");
}
}
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
import org.junit.Test;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
public class EmployeeTest {
//自然排序 --Employee实现Comparable接口 重写CompareTo方法
@Test
public void test1() {
TreeSet set = new TreeSet();
Employee e1 = new Employee("Tom", 23, new MyDate(1999, 7, 9));
Employee e2 = new Employee("Rose", 43, new MyDate(1999, 7, 19));
Employee e3 = new Employee("Jack", 54, new MyDate(1998, 12, 21));
Employee e4 = new Employee("Jerry", 12, new MyDate(2002, 4, 21));
Employee e5 = new Employee("Tony", 22, new MyDate(2001, 9, 12));
set.add(e1);
set.add(e2);
set.add(e3);
set.add(e4);
set.add(e5);
//遍历
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
//输出: 根据名字
//Employee{name='Jack', age=54, birthday=MyDate{year=1998, month=12, day=21}}
//Employee{name='Jerry', age=12, birthday=MyDate{year=2002, month=4, day=21}}
//Employee{name='Rose', age=43, birthday=MyDate{year=1999, month=7, day=19}}
//Employee{name='Tom', age=23, birthday=MyDate{year=1999, month=7, day=9}}
//Employee{name='Tony', age=22, birthday=MyDate{year=2001, month=9, day=12}}
}


//定制排序 --匿名内部类方式实现Comparator接口 重写compare方法
@Test
public void test2(){
TreeSet set = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof Employee && o2 instanceof Employee){
Employee e1 = (Employee) o1;
Employee e2 = (Employee) o2;
//对比两个employee的生日的大小
MyDate birth1 = e1.getBirthday();
MyDate birth2 = e2.getBirthday();
//方式2:
return birth1.compareTo(birth2);
}
throw new RuntimeException("输入的类型不匹配");
}
});
Employee e1 = new Employee("Tom", 23, new MyDate(1999, 7, 9));
Employee e2 = new Employee("Rose", 43, new MyDate(1999, 7, 19));
Employee e3 = new Employee("Jack", 54, new MyDate(1998, 12, 21));
Employee e4 = new Employee("Jerry", 12, new MyDate(2002, 4, 21));
Employee e5 = new Employee("Tony", 22, new MyDate(2001, 9, 12));
set.add(e1);
set.add(e2);
set.add(e3);
set.add(e4);
set.add(e5);
//遍历
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
//输出: 根据生日
//Employee{name='Jack', age=54, birthday=MyDate{year=1998, month=12, day=21}}
//Employee{name='Tom', age=23, birthday=MyDate{year=1999, month=7, day=9}}
//Employee{name='Rose', age=43, birthday=MyDate{year=1999, month=7, day=19}}
//Employee{name='Tony', age=22, birthday=MyDate{year=2001, month=9, day=12}}
//Employee{name='Jerry', age=12, birthday=MyDate{year=2002, month=4, day=21}}
}

}

6. Map接口

现实生活与开发中,我们常会看到这样的一类集合:用户ID与账户信息、学生姓名与考试成绩、IP地址与主机名等,这种一一对应的关系,就称作映射
Java提供了专门的集合框架用来存储这种映射关系的对象,即java.util.Map接口

6.1 Map接口概述

  • Map与Collection并列存在。用于保存具有映射关系的数据:key-value

    • Collection集合称为单列集合,元素是孤立存在的(理解为单身
    • Map集合称为双列集合,元素是成对存在的(理解为夫妻)
  • Map 中的 key 和 value 可以是任何引用类型的数据。但常用String类作为Map的“键”。

  • Map接口的常用实现类:HashMapLinkedHashMapTreeMap和``Properties。其中,HashMap是 Map 接口使用频率最高`的实现类。

    image-20220409001015034

6.2 Map中key-value特点

这里主要以HashMap为例说明。HashMap中存储的key、value的特点如下:

image-20220409001213720
  • Map 中的 key用Set来存放不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法

    image-20220514190412763
  • key 和 value 之间存在单向一对一关系,通过指定 key 总能找到唯一的、确定的 value【根据key存放value】,不同key对应的value可以重复。value所在的类要重写equals()方法。

  • key和value构成一个entry。所有的entry彼此之间是无序的不可重复的

  • 总结:
    image-20231027103310700

6.2 Map接口的常用方法

  • 添加、修改操作:
    • Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
    • void putAll(Map m):将m中的所有key-value对存放到当前map中
  • 删除操作:
    • Object remove(Object key):移除指定key的key-value对,并返回value
    • void clear():清空当前map中的所有数据
  • 元素查询的操作:
    • Object get(Object key):获取指定key对应的value
    • boolean containsKey(Object key):是否包含指定的key
    • boolean containsValue(Object value):是否包含指定的value
    • int size():返回map中key-value对的个数
    • boolean isEmpty():判断当前map是否为空
    • boolean equals(Object obj):判断当前map和参数对象obj是否相等
  • 元视图操作的方法:
    • Set keySet():返回所有key构成的Set集合
    • Collection values():返回所有value构成的Collection集合
    • Set entrySet():返回所有key-value对构成的Set集合

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.HashMap;
public class TestMapMethod {
public static void main(String[] args) {
//创建 map对象
HashMap map = new HashMap();

//添加元素到集合
map.put("黄晓明", "杨颖");
map.put("李晨", "李小璐");
map.put("李晨", "范冰冰");
map.put("邓超", "孙俪");
System.out.println(map);

//删除指定的key-value
System.out.println(map.remove("黄晓明"));
System.out.println(map);

//查询指定key对应的value
System.out.println(map.get("邓超"));
System.out.println(map.get("黄晓明"));

}
}

举例:

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) {
HashMap map = new HashMap();
map.put("许仙", "白娘子");
map.put("董永", "七仙女");
map.put("牛郎", "织女");
map.put("许仙", "小青");

System.out.println("所有的key:");
Set keySet = map.keySet();
for (Object key : keySet) {
System.out.println(key);
}

System.out.println("所有的value:");
Collection values = map.values();
for (Object value : values) {
System.out.println(value);
}

System.out.println("所有的映射关系:");
Set entrySet = map.entrySet();
for (Object mapping : entrySet) {
//System.out.println(entry);
Map.Entry entry = (Map.Entry) mapping;
System.out.println(entry.getKey() + "->" + entry.getValue());
}
}

6.3 Map的主要实现类:HashMap

6.3.1 HashMap概述

  • HashMap是 Map 接口使用频率最高的实现类。
  • HashMap是线程不安全的。允许添加 null 键和 null 值。
  • 存储数据采用的哈希表结构,底层使用一维数组+单向链表+红黑树进行key-value数据的存储。与HashSet一样,元素的存取顺序不能保证一致。
  • HashMap 判断两个key相等的标准是:两个 key 的hashCode值相等,通过 equals() 方法返回 true。
  • HashMap 判断两个value相等的标准是:两个 value 通过 equals() 方法返回 true。

6.3.2 练习

练习1:添加你喜欢的歌手以及你喜欢他唱过的歌曲

例如:

image-20220914190805362

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 SingerTest1 {
public static void main(String[] args) {

//创建一个HashMap用于保存歌手和其歌曲集
HashMap singers = new HashMap();

//声明一组key,value
String singer1 = "周杰伦";
ArrayList songs1 = new ArrayList();
songs1.add("双节棍");
songs1.add("本草纲目");
songs1.add("夜曲");
songs1.add("稻香");
//添加到map中
singers.put(singer1,songs1);

//声明一组key,value
String singer2 = "陈奕迅";
List songs2 = Arrays.asList("浮夸", "十年", "红玫瑰", "好久不见", "孤勇者");
//添加到map中
singers.put(singer2,songs2);

//遍历map
Set entrySet = singers.entrySet();
for(Object obj : entrySet){
Map.Entry entry = (Map.Entry)obj;
String singer = (String) entry.getKey();
List songs = (List) entry.getValue();

System.out.println("歌手:" + singer);
System.out.println("歌曲有:" + songs);
}

}
}
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
//方式2:改为HashSet实现
public class SingerTest2 {
@Test
public void test1() {

Singer singer1 = new Singer("周杰伦");
Singer singer2 = new Singer("陈奕迅");

Song song1 = new Song("双节棍");
Song song2 = new Song("本草纲目");
Song song3 = new Song("夜曲");
Song song4 = new Song("浮夸");
Song song5 = new Song("十年");
Song song6 = new Song("孤勇者");

HashSet h1 = new HashSet();// 放歌手一的歌曲
h1.add(song1);
h1.add(song2);
h1.add(song3);

HashSet h2 = new HashSet();// 放歌手二的歌曲
h2.add(song4);
h2.add(song5);
h2.add(song6);

HashMap hashMap = new HashMap();// 放歌手和他对应的歌曲
hashMap.put(singer1, h1);
hashMap.put(singer2, h2);

for (Object obj : hashMap.keySet()) {
System.out.println(obj + "=" + hashMap.get(obj));
}

}
}

//歌曲
public class Song implements Comparable{
private String songName;//歌名

public Song() {
super();
}

public Song(String songName) {
super();
this.songName = songName;
}

public String getSongName() {
return songName;
}

public void setSongName(String songName) {
this.songName = songName;
}

@Override
public String toString() {
return "《" + songName + "》";
}

@Override
public int compareTo(Object o) {
if(o == this){
return 0;
}
if(o instanceof Song){
Song song = (Song)o;
return songName.compareTo(song.getSongName());
}
return 0;
}


}
//歌手
public class Singer implements Comparable{
private String name;
private Song song;

public Singer() {
super();
}

public Singer(String name) {
super();
this.name = name;

}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Song getSong() {
return song;
}

public void setSong(Song song) {
this.song = song;
}

@Override
public String toString() {
return name;
}

@Override
public int compareTo(Object o) {
if(o == this){
return 0;
}
if(o instanceof Singer){
Singer singer = (Singer)o;
return name.compareTo(singer.getName());
}
return 0;
}
}

练习2:二级联动

将省份和城市的名称保存在集合中,当用户选择省份以后,二级联动,显示对应省份的地级市供用户选择。

效果演示:

img

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
class CityMap{

public static Map model = new HashMap(); //使用HashMap()

static {
model.put("北京", new String[] {"北京"});
model.put("上海", new String[] {"上海"});
model.put("天津", new String[] {"天津"});
model.put("重庆", new String[] {"重庆"});
model.put("黑龙江", new String[] {"哈尔滨","齐齐哈尔","牡丹江","大庆","伊春","双鸭山","绥化"});
model.put("吉林", new String[] {"长春","延边","吉林","白山","白城","四平","松原"});
model.put("河北", new String[] {"石家庄","张家口","邯郸","邢台","唐山","保定","秦皇岛"});
}

}

public class ProvinceTest {
public static void main(String[] args) {

Set keySet = CityMap.model.keySet();
for(Object s : keySet) {
System.out.print(s + "\t");
}
System.out.println();
System.out.println("请选择你所在的省份:");
Scanner scan = new Scanner(System.in);
String province = scan.next();

String[] citys = (String[])CityMap.model.get(province);
for(String city : citys) {
System.out.print(city + "\t");
}
System.out.println();
System.out.println("请选择你所在的城市:");
String city = scan.next();

System.out.println("信息登记完毕");
}

}

练习3:WordCount统计

需求:统计字符串中每个字符出现的次数

String str = “aaaabbbcccccccccc”;

提示:

char[] arr = str.toCharArray(); //将字符串转换成字符数组

HashMap hm = new HashMap(); //创建双列集合存储键和值,键放字符,值放次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.HashMap;
public class WordCount {
public static void main(String[] args) {
String str="aaaabbbcccccccccc";
//统计每个字符串出现的次数 最终结果是类似于:a:0 b:1 c:2
//因为有映射关系 ---> 使用HashMap类
HashMap map=new HashMap();
//字符串转为字符数组
char[] arr=str.toCharArray();
for(Object temp:arr){
//如果不存在就key设置为temp,value设置1
if(!map.containsKey(temp)){
map.put(temp,1);
}else{
//如何获取oldvalue
//方法一: 通过map.put返回原有value
//方法二: 通过get(KEY)获取对应的值 map.put(temp,map.get(temp));
int oldvalue= (int) map.put(temp,1); //只能通过先设置一次获取到原来的value 然后下一次是认真的+1
map.put(temp,oldvalue+1); //如果存在就value+1
}
}
System.out.println(map);
}
}

6.4 Map实现类之二:LinkedHashMap

  • LinkedHashMap 是 HashMap 的子类
  • 存储数据采用的哈希表结构+链表结构,在HashMap存储结构的基础上,使用了一对双向链表记录添加元素的先后顺序,可以保证遍历元素时,与添加的顺序一致。
  • 通过哈希表结构可以保证键的唯一、不重复,需要键所在类重写hashCode()方法、equals()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestLinkedHashMap {
public static void main(String[] args) {
LinkedHashMap map = new LinkedHashMap();
map.put("王五", 13000.0);
map.put("张三", 10000.0);
//key相同,新的value会覆盖原来的value
//因为String重写了hashCode和equals方法
map.put("张三", 12000.0);
map.put("李四", 14000.0);
//HashMap支持key和value为null值
String name = null;
Double salary = null;
map.put(name, salary);

Set entrySet = map.entrySet();
for (Object obj : entrySet) {
Map.Entry entry = (Map.Entry)obj;
System.out.println(entry);
}
}
}

6.5 Map实现类之三:TreeMap

  • TreeMap存储 key-value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 key-value 对处于有序状态
  • TreeSet底层使用红黑树结构存储数据
  • TreeMap 的 Key 的排序:
    • 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
    • 定制排序:创建 TreeMap 时,构造器传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口
  • TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
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
public class TestTreeMap {
/*
* 自然排序举例
* */
@Test
public void test1(){
TreeMap map = new TreeMap();

map.put("CC",45);
map.put("MM",78);
map.put("DD",56);
map.put("GG",89);
map.put("JJ",99);

Set entrySet = map.entrySet();
for(Object entry : entrySet){
System.out.println(entry);
}

}

/*
* 定制排序
*
* */
@Test
public void test2(){
//按照User的姓名的从小到大的顺序排列

TreeMap map = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;

return u1.name.compareTo(u2.name);
}
throw new RuntimeException("输入的类型不匹配");
}
});

map.put(new User("Tom",12),67);
map.put(new User("Rose",23),"87");
map.put(new User("Jerry",2),88);
map.put(new User("Eric",18),45);
map.put(new User("Tommy",44),77);
map.put(new User("Jim",23),88);
map.put(new User("Maria",18),34);

Set entrySet = map.entrySet();
for(Object entry : entrySet){
System.out.println(entry);
}
}
}

class User implements Comparable{
String name;
int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}

public User() {
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
/*
举例:按照age从小到大的顺序排列,如果age相同,则按照name从大到小的顺序排列
* */
@Override
public int compareTo(Object o) {
if(this == o){
return 0;
}

if(o instanceof User){
User user = (User)o;
int value = this.age - user.age;
if(value != 0){
return value;
}
return -this.name.compareTo(user.name);
}
throw new RuntimeException("输入的类型不匹配");
}
}

6.6 Map实现类之四:Hashtable

  • Hashtable是Map接口的古老实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的。
  • Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构(数组+单向链表),查询速度快。
  • 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
  • Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
  • 与HashMap不同,Hashtable 不允许使用 null 作为 key 或 value。

面试题:Hashtable和HashMap的区别

1
2
3
4
5
6
7
8
9
10
HashMap:底层是一个哈希表(jdk7:数组+链表;jdk8:数组+链表+红黑树),是一个线程不安全的集合,执行效率高
Hashtable:底层也是一个哈希表(数组+链表),是一个线程安全的集合,执行效率低

HashMap集合:可以存储null的键、null的值
Hashtable集合,不能存储null的键、null的值

Hashtable和Vector集合一样,在jdk1.2版本之后被更先进的集合(HashMap,ArrayList)取代了。所以HashMap是Map的主要实现类,Hashtable是Map的古老实现类。

Hashtable的子类Properties(配置文件)依然活跃在历史舞台
Properties集合是一个唯一和IO流相结合的集合

6.7 Map实现类之五:Properties

  • Properties 类是 Hashtable 的子类,该对象用于处理属性文件

  • 由于属性文件里的 key、value 都是字符串类型,所以 Properties 中要求 key 和 value 都是字符串类型

  • 存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test01() {
Properties properties = System.getProperties();
String fileEncoding = properties.getProperty("file.encoding");//当前源文件字符编码
System.out.println("fileEncoding = " + fileEncoding);
}
@Test
public void test02() {
Properties properties = new Properties();
properties.setProperty("user","songhk");
properties.setProperty("password","123456");
System.out.println(properties);
}

@Test
public void test03() throws IOException {
Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String user = pros.getProperty("user");
System.out.println(user);
}

7. Collections工具类

Arrays是一个操作数组的工具类

Collections 是一个操作 Set、List 和 Map 等集合的工具类。

7.1 常用方法(static方法 )

Collections 中提供了一系列静态方法对集合元素进行排序查询修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法(均为static方法):

- 排序

  • reverse(List):反转 List 中元素的顺序
  • shuffle(List):对 List 集合元素进行随机排序
  • sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
  • sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
  • swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
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
@Test
public void test1(){
ArrayList list=new ArrayList();
list.add(123);
list.add(12);
list.add(2);
list.add(4414);
System.out.println("初始的list:"+list); //初始的list:[123, 12, 2, 4414]
Collections.reverse(list);
System.out.println("reverse之后:"+list); // 反转顺序 ---- reverse之后:[4414, 2, 12, 123]
Collections.shuffle(list);
System.out.println("shuffle之后:"+list); //很随机 ---- shuffle之后:[2, 4414, 12, 123]
Collections.sort(list);
System.out.println("sort之后:"+list); // 自然顺序(升序) ---- sort之后:[2, 12, 123, 4414]
Collections.sort(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
int temp1=(int)o1;
int temp2=(int)o2;
return -(temp1-temp2); //实现倒序
}
});
System.out.println("定制排序之后:"+list); //定制排序之后:[2, 12, 123, 4414]
Collections.swap(list,0,2);
System.out.println("将0和2位置的元素互换位置:"+list); // 将0和2位置的元素互换位置: ---- swap之后:[12, 123, 4414, 2]
}

- 查找

  • Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  • Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
  • Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
  • Object min(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最小元素
  • int binarySearch(List list,T key)在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且必须是可比较大小的,即支持自然排序的。而且集合也事先必须是有序的,否则结果不确定。
  • int binarySearch(List list,T key,Comparator c)在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且集合也事先必须是按照c比较器规则进行排序过的,否则结果不确定。
  • int frequency(Collection c,Object o):返回指定集合中指定元素的出现次数
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
@Test
public void test2(){
ArrayList list=new ArrayList();
list.add(123);
list.add(12);
list.add(2);
list.add(4414);
System.out.println("初始的list:"+list); //初始的list:[123, 12, 2, 4414]
//寻找最大值
System.out.println("max(list)得出的最大值:"+Collections.max(list)); //max(list)得出的最大值:4414
System.out.println("max(list,Comparator)得出的最大值:"+Collections.max(list, new Comparator() { //max(list,Comparator)得出的最大值:4414
@Override
public int compare(Object o1, Object o2) {
int temp1=(int)o1;
int temp2=(int)o2;
return (temp1-temp2); //实现倒序
}
}));
//寻找最小值
System.out.println("min(list)得出的最小值:"+Collections.min(list)); //min(list)得出的最小值:2
System.out.println("min(list,Comparator)得出的最小值:"+Collections.min(list, new Comparator() { //min(list,Comparator)得出的最小值:2
@Override
public int compare(Object o1, Object o2) {
int temp1=(int)o1;
int temp2=(int)o2;
return (temp1-temp2); //实现倒序
}
}));
//查找某个元素的下标
//1.自然排序
Collections.sort(list);
System.out.println("排序之后的list:"+list); // 排序之后的list:[2, 12, 123, 4414]
//查找某个元素的下标
System.out.println(Collections.binarySearch(list,12)); //下标为1
//2.定制排序
System.out.println(Collections.binarySearch(list,12, new Comparator() { //下标为1
@Override
public int compare(Object o1, Object o2) {
int temp1=(int)o1;
int temp2=(int)o2;
return -(temp1-temp2); //实现倒序
}
}));
//查找指定元素的出现次数
System.out.println(Collections.frequency(list,412)); //412元素出现的次数为0
}

- 复制、替换

  • void copy(List dest,List src):将src中的内容复制到dest中
  • boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
  • 提供了多个unmodifiableXxx()方法,该方法返回指定 Xxx的不可修改的视图。
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
@Test
public void test3(){
ArrayList list=new ArrayList();
list.add(123);
list.add(12);
list.add(2);
list.add(4414);
System.out.println("初始的list:"+list); //初始的list:[123, 12, 2, 4414]
//复制
ArrayList list1=new ArrayList();
list1.add("AA");
list1.add("BA");
list1.add("CCC");
Collections.copy(list,list1);
System.out.println("复制之后的list:"+list); //复制之后的list:[AA, BA, CCC, 4414]
}
------------------------------------------------------------------------------------------------------------
@Test
public void test4(){
ArrayList list=new ArrayList();
list.add(123);
list.add(123);
list.add(2);
list.add(4414);
System.out.println("初始的list:"+list); //初始的list:[123, 123, 2, 4414]
//替换
Collections.replaceAll(list,123,"CC"); //替换之后的list:[CC, CC, 2, 4414]
System.out.println("替换之后的list:"+list);
}

- 添加

  • boolean addAll(Collection c,T… elements)将所有指定元素添加到指定 collection 中。
1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test5(){
ArrayList list=new ArrayList();
list.add(123);
list.add(12);
list.add(2);
list.add(4414);
System.out.println("初始的list:"+list); //初始的list:[123, 12, 2, 4414]
//添加
Collections.addAll(list,"CC","asd","ewr");
System.out.println("添加之后的list:"+list); //添加之后的list:[123, 12, 2, 4414, CC, asd, ewr]
}

- 同步

  • Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题:

image-20220409003002526

7.2 练习

练习1:

请从键盘随机输入10个整数保存到List中,并按倒序、从大到小的顺序显示出来

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
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class CollectionTest1 {
public static void main(String[] args) {
ArrayList list=new ArrayList();
list.add(12);
list.add(345);
list.add(323);
list.add(1233);
list.add(3);
list.add(123);
list.add(232);
list.add(122);
list.add(167);
list.add(789);
System.out.println("初始的list:"+list); // 初始的list:[12, 345, 323, 1233, 3, 123, 232, 122, 167, 789]
//排序的话 -- 默认排序(升序不可以) 那就只能实现Comparator接口进行定制排序
Collections.sort(list, new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
int temp1=(int)o1;
int temp2=(int)o2;
return -(temp1-temp2); //倒序
}
});
System.out.println("倒序之后的list:"+list); // 倒序之后的list:[1233, 789, 345, 323, 232, 167, 123, 122, 12, 3]
}
}

image-20231028105328967

练习2:模拟斗地主洗牌和发牌,牌没有排序

效果演示:

image-20220409011625061

代码示例:

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
package MapTest;
import java.util.ArrayList;
import java.util.Collections;
public class CollectionTest2 {
public static void main(String[] args) {
String[] num = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};
String[] color = {"方片","梅花","红桃","黑桃"};
ArrayList<String> poker = new ArrayList<>();
//1. 生成54张扑克牌
for (String s1 : color) {
for (String s2 : num) {
poker.add(s1.concat(" " + s2));
}
}
poker.add("小王");
poker.add("大王");
//2.洗牌
Collections.shuffle(poker);
//3.发牌
ArrayList tomCards = new ArrayList();
ArrayList jerryCards = new ArrayList();
ArrayList meCards = new ArrayList();
ArrayList lastCards = new ArrayList();
for(int i=0;i<17;i++){
tomCards.add(poker.get(i));
}
for(int i=18;i<34;i++){
jerryCards.add(poker.get(i));
}
for(int i=35;i<51;i++){
meCards.add(poker.get(i));
}
for(int i=51;i<54;i++){
lastCards.add(poker.get(i));
}

//4. 看牌
System.out.println("Tom:\n" + tomCards);
System.out.println("Jerry:\n" + jerryCards);
System.out.println("me:\n" + meCards);
System.out.println("底牌:\n" + lastCards);
}
}

image-20231028110109452

练习3:模拟斗地主洗牌和发牌并对牌进行排序的代码实现。

image-20220915002714578

提示:考查HashMap、TreeSet、ArrayList、Collections

代码示例:

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 PokerTest1 {
public static void main(String[] args) {
String[] num = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};
String[] color = {"方片", "梅花", "红桃", "黑桃"};
HashMap map = new HashMap(); // 存储索引和扑克牌
ArrayList list = new ArrayList(); // 存储索引
int index = 0; // 索引的开始值
for (String s1 : num) {
for (String s2 : color) {
map.put(index, s2.concat(s1)); // 将索引和扑克牌添加到HashMap中
list.add(index); // 将索引添加到ArrayList集合中
index++;
}
}
map.put(index, "小王");
list.add(index);
index++;
map.put(index, "大王");
list.add(index);
// 洗牌
Collections.shuffle(list);
// 发牌 ---保证每个人是TreeSet就行
TreeSet Tom = new TreeSet();
TreeSet Jerry = new TreeSet();
TreeSet me = new TreeSet();
TreeSet lastCards = new TreeSet();

for (int i = 0; i < list.size(); i++) {
if (i >= list.size() - 3) {
lastCards.add(list.get(i)); // 将list集合中的索引添加到TreeSet集合中会自动排序
} else if (i % 3 == 0) {
Tom.add(list.get(i));
} else if (i % 3 == 1) {
Jerry.add(list.get(i));
} else {
me.add(list.get(i));
}
}

// 看牌
lookPoker("Tom", Tom, map);
lookPoker("Jerry", Jerry, map);
lookPoker("康师傅", me, map);
lookPoker("底牌", lastCards, map);

}

public static void lookPoker(String name, TreeSet ts, HashMap map) {
System.out.println(name + "的牌是:");
for (Object index : ts) {
System.out.print(map.get(index) + " ");
}

System.out.println();
}
}

picgo

引入前提

1
2
3
1.之前传入博客经常因为图片本地可见,但是上传之后不可见
2.因为路径导致经常更改图片位置
---》 因此网上搜索可以使用picgo软件将图片上传到github仓库实现

1.配置Github仓库

1.1 创建仓库

image-20231021205806036

  • 其他步骤看参考步骤5里面的就行

2.配置picgo软件

image-20231021210145557

3.配置typora

image-20231021210037894

4.如何上传

直接复制图片只是他会自动上传,然后就可以看到地址直接是github仓库的路径

image-20231021210202896

5.参考链接

,