【JavaWeb】HTML、CSS

Web开发

Web介绍

Web:全球广域网,也称为万维网(www World Wide Web),能够通过浏览器访问的网站。

Web 网站的工作流程

image-20240412145130187

整个流程如下:

  1. 浏览器先向前端服务器请求前端资源,也就是我们所说的网页
  2. 浏览器再向后台服务器发起请求,获取数据
  3. 浏览器将得到的后台数据填充到网页上,然后展示给用户去看。

网站的开发模式

**Web开发=开发网站~**主要有2种:前端台分离和混合开发,目前流行的是前后端分离开发模式。

前后台分离:(目前企业开发的主流,市场占有率70%以上)这种开发模式的特点如下

  • 前端人员开发前端程序,前端程序单独部署到前端服务器上

  • 后端人员开开发后端程序,后端程序单独部署到后端服务器上

网站开发技术

前端

技术 描述
HTML 用于构建网站的基础结构的
css 用于美化页面的,作用和化妆或者整容作用一样
JavaScript 实现网页和用户的交互
Vue 主要用于将数据填充到html页面上的
Element 主要提供了一些非常美观的组件
Nginx 一款web服务器软件,可以用于部署我们的前端工程

后端

技术 描述
Maven 一款java中用于管理项目的软件
Mysql 最常用的一款数据库软件之一
SpringBoot spring家族的产品,当前最为主流的项目开发技术。
Mybatis 用于操作数据库的框架

前端开发

整体认知

  1. 网页有哪些部分组成 ?
    • ​ 文字、图片、音频、视频、超链接、表格等等。
  2. 我们看到的网页,背后的本质是什么 ?
    • 程序员写的前端代码 (备注:在前后端分离的开发模式中。)
  3. 前端的代码是如何转换成用户眼中的网页的 ?
    • 通过浏览器转化(解析和渲染)成用户看到的网页。
    • 浏览器中对代码进行解析和渲染的部分,称为 浏览器内核

Web标准

即网页标准,一系列,大部分由W3C( World Wide Web Consortium,万维网联盟)负责制定。由三个组成部分:

  • HTML:负责网页的结构(页面元素和内容)。
  • CSS:负责网页的表现(页面元素的外观、位置等页面样式,如:颜色、大小等)。
  • JavaScript:负责网页的行为(交互效果)。

HTML & CSS

快速入门

什么是HTML ?

**HTML: **HyperText Markup Language,超文本标记语言。

  • 超文本:超越了文本的限制,比普通文本更强大。除了文字信息,还可以定义图片、音频、视频等内容。

  • 标记语言:由标签构成的语言

    • HTML标签都是预定义好的。例如:使用 <h1> 标签展示标题,使用<a>展示超链接,使用<img>展示图片,<video>展示视频。
    • HTML代码直接在浏览器中运行,HTML标签由浏览器解析。

html的固定基本结构

1
2
3
4
5
6
7
8
9
<html>
<head>
<title>HTML 快速入门</title>
</head>
<body>
<h1>Hello HTML</h1>
<img src="1.jpg"/>
</body>
</html>

<html>是根标签,<head>和<body>是子标签,<head>中的字标签<title>是用来定义网页的标题的,里面定义的内容会显示在浏览器网页的标题位置。 <body> 中编写的内容,就网页中显示的核心内容。

HTML中的标签特点

  • HTML标签不区分大小写
  • HTML标签的属性值,采用单引号、双引号都可以
  • HTML语法相对比较松散 (建议大家编写HTML标签的时候尽量严谨一些)

什么是CSS ?

CSS: Cascading Style Sheet,层叠样式表,用于控制页面的样式(表现)。

基础标签 & 样式

标题

【标题排版】

图片标签:<img>

  • src:指定图像的url(绝对路径 / 相对路径)
  • width:图像的宽度(像素 / 相对于父元素的百分比)
  • height:图像的高度(像素 / 相对于父元素的百分比)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
A. 图片标签: <img>

B. 常见属性:
src: 指定图像的url (可以指定 绝对路径 , 也可以指定 相对路径)
width: 图像的宽度 (像素 / 百分比 , 相对于父元素的百分比)
height: 图像的高度 (像素 / 百分比 , 相对于父元素的百分比)

备注: 一般width 和 height 我们只会指定一个,另外一个会自动的等比例缩放。

C. 路径书写方式:
绝对路径:
1. 绝对磁盘路径: C:\Users\Administrator\Desktop\HTML\img\news_logo.png
<img src="C:\Users\Administrator\Desktop\HTML\img\news_logo.png">

2. 绝对网络路径: https://i2.sinaimg.cn/dy/deco/2012/0613/yocc20120613img01/news_logo.png
<img src="https://i2.sinaimg.cn/dy/deco/2012/0613/yocc20120613img01/news_logo.png">

相对路径:
./ : 当前目录 , ./ 可以省略的
../: 上一级目录

标题标签h系列

  • 标题标签:<h1> - <h6>
  • 水平线标签:<hr>图片
1
2
3
4
5
6
7
8
9
10
A. 标题标签: <h1> - <h6>

<h1>111111111111</h1>
<h2>111111111111</h2>
<h3>111111111111</h3>
<h4>111111111111</h4>
<h5>111111111111</h5>
<h6>111111111111</h6>

B. 效果 : h1为一级标题,字体也是最大的 ; h6为六级标题,字体是最小的。

注意:HTML标签都是预定义好的,不能自己随意定义。

[补充]<span>标签

  • <span> 是一个在开发网页时大量会用到的没有语义的布局标签
  • 特点:一行可以显示多个(组合行内元素),宽度和高度默认由内容撑开

[补充]路径选择

  1. (1)绝对路径:绝对磁盘路径(D:/xxxx)、绝对网络路径(https://xxxx)
  2. (2)相对路径:从当前文件开始查找。 (./ : 当前目录, ../ : 上级目录)

【标题样式】

CSS引入方式:

  • (1)行内样式:写在标签的style属性中(不推荐),属性值是css属性键值对。
1
<h1 style="xxx: xxx; xxx: xxx;">中国新闻网</h1>
  • (2)内嵌样式:写在style标签中(可以写在页面任何位置,但通常约定写在head标签中),在标签内部定义css样式。
1
2
3
4
5
6
<style>
  h1 {
     xxx: xxx;
     xxx: xxx;
  }
</style>
  • (3)外联样式:写在一个单独的.css文件中(需要通过 link 标签在网页中引入,通过href属性引入外部css文件。)
1
2
3
4
5
6
h1 {
  xxx: xxx;
   xxx: xxx;
}

<link rel="stylesheet" href="css/news.css">

上述3种引入方式,企业开发的使用情况如下:

  1. 内联样式会出现大量的代码冗余,不方便后期的维护,所以不常用。
  2. 内部样式,通过定义css选择器,让样式作用于当前页面的指定的标签上。
  3. 外部样式,html和css实现了完全的分离,企业开发常用方式。
[补充]CSS属性-颜色设置方式
表示方式 表示含义 取值
关键字 预定义的颜色名 red、green、blue…
rgb表示法 红绿蓝三原色,每项取值范围:0-255 rgb(0,0,0)、rgb(255,255,255)、rgb(255,0,0)
十六进制表示法 #开头,将数字转换成十六进制表示 #000000、#ff0000、#cccccc,简写:#000、#ccc

颜色属性:color: 设置文本内容的颜色

[补充]CSS选择器:来选取需要设置样式的元素(标签)

image-20240412170049481

(1)元素(标签)选择器:

  • 选择器的名字必须是标签的名字
  • 作用:选择器中的样式会作用于所有同名的标签上
1
2
3
元素名称 {
css样式名:css样式值;
}

例子如下:

1
2
3
div{
color: red;
}

(2)id选择器:

  • 选择器的名字前面需要加上#
  • 作用:选择器中的样式会作用于指定id的标签上,而且有且只有一个标签(由于id是唯一的)
1
2
3
#id属性值 {
css样式名:css样式值;
}

例子如下:

1
2
3
#did {
color: blue;
}

(3)类选择器:

  • 选择器的名字前面需要加上 .
  • 作用:选择器中的样式会作用于所有class的属性值和该名字一样的标签上,可以是多个
1
2
3
.class属性值 {
css样式名:css样式值;
}

例子如下:

1
2
3
.cls{
color: green;
}

[补充]css属性

  • font-size:字体大小 (注意:记得加px)

【超链接】

标签:

1
<a href="..." target="...">央视网</a>

属性:

  • href:指定资源访问的url
  • target:指定在何处打开资源链接
    • _self:默认值,在当前页面打开
    • _blank:在空白页面打开

[补充]CSS属性

  • text-decoration:规定添加到文本的修饰,none表示定义标准的文本。
  • color:定义文本的颜色。

正文

【正文排版】

  1. 视频标签:<video>
    • src:规定视频的url
    • controls:显示播放控件
    • width:播放器的宽度
    • height:播放器的高度
  2. 音频标签:<audio>
    • src:规定音频的url
    • controls:显示播放控件
  3. 段落标签:换行~<br> , 段落:<p>
显示结果 描述 占位符
空格 &nbsp;
< 小于号 &lt;
> 大于号 &gt;
& 和号 &amp;
引号 &quot;
撇号 &apos;

​ 4. 文本加粗标签: <b> / <strong>

效果 标签 标签(强调)
加粗 b strong
倾斜 i em
下划线 u ins
删除线 s del
[补充]CSS属性-文本排版样式
  • line-height:设置行高
  • text-indent:定义第一个行内容的缩进
  • text-align:规定元素中的文本的水平对齐方式

【页面布局】

盒子模型
  • 盒子:页面中所有的元素(标签),都可以看做是一个 盒子,由盒子将页面中的元素包含在一个矩形区域内,通过盒子的视角更方便的进行页面布局。
  • 盒子模型组成:内容区域(content)、内边距区域(padding)、边框区域(border)、外边距区域(margin)。

image-20240412154004604

布局标签

实际开发网页中,会大量频繁的使用 div 和 span 这两个没有语义的布局标签。

  • 标签:<div> <span>
  • 特点:
    • div标签:
      • 一行只显示一个(独占一行)
      • 宽度默认是父元素的宽度,高度默认由内容撑开
      • 可以设置宽高(width、height)
    • span标签:
      • 一行可以显示多个
      • 宽度和高度默认由内容撑开
      • 不可以设置宽高(width、height)
[补充]CSS盒子模型
  • 组成:内容(content)、内边距(padding)、边框(border)、外边距(margin)
[补充]CSS属性
  • width:设置宽度
  • height:设置高度
  • border:设置边框的属性,如:1px solid #000;
  • padding:内边距
  • margin:外边距
  • 注意:如果只需要设置某一个方位的边框、内边距、外边距,可以在属性名后加上 – 位置,如:padding-top、padding-left、padding-right …
布局实现
  1. 需要将body中的新闻标题部分、正文部分使用一个 div 布局标签将其包裹起来,方便通过css设置内容占用的宽度,比如:65%。
  2. 通过css为该div设置外边距,左右的外边距分别为:17.5%,上下外边距靠边展示即可,为:0%。

表格、表单标签

表格标签

场景:在网页中以表格(行、列)形式整齐展示数据,如:班级表。
标签:

标签 描述 属性/备注
<table> 定义表格整体,可以包裹多个 <tr> border:规定表格边框的宽度
width:规定表格的宽度
cellspacing: 规定单元之间的空间。
<tr> 表格的行,可以包裹多个 <td>
<td> 表格单元格(普通),可以包裹内容 如果是表头单元格,可以替换为 <th>

表单标签

场景:在网页中主要负责数据采集功能,如 注册、登录等数据采集。

标签:<form>

  • 表单项:不同类型的 input 元素、下拉列表、文本域等。
    • <input>:定义表单项,通过type属性控制输入形式
    • <select>:定义下拉列表,<option>定义列表项
    • <textarea>:定义文本域
  • 属性:
    • action:规定当提交表单时向何处发送表单数据,URL
    • method:规定用于发送表单数据的方式。GET、POST
      • GET:表单数据是拼接在url后面的, 如: xxxxxxxxxxx?username=Tom&age=12,url中能携带的表单数据大小是有限制的。
      • POST: 表单数据是在请求体(消息体)中携带的,大小没有限制。

注意事项

  • 表单中的所有表单项,要想能够正常的采集数据,在提交的时候能提交到服务端,表单项必须指定name属性。 否则,无法提交该表单项。
1
用户名: <input type="text" name="username">

表单标签-表单项<input>

type取值 描述 形式
text 默认值,定义单行的输入字段 image-20240412160140233
password 定义密码字段 image-20240412160146970
radio 定义单选按钮 image-20240412160153981
checkbox 定义复选框 image-20240412160200603
file 定义文件上传按钮 image-20240412160208339
date/time/datetime-local 定义日期/时间/日期时间 image-20240412160216061
number 定义数字输入框 image-20240412160227021
email 定义邮件输入框 image-20240412160235837
hidden 定义隐藏域
submit / reset / button 定义提交按钮 / 重置按钮 / 可点击按钮 image-20240412160242687

文档查阅

文档地址: https://www.w3school.com.cn/index.html

【Java基础】【java.lang.reflect】Junit单元测试、反射

单元测试

快速入门

单元测试

  • 单元测试就是针对最小的功能单元(方法),编写测试代码对该功能进行正确性测试

目前测试方法是怎么进行的? 存在什么问题 ?

  • 只能编写main方法,并在main方法中再去调用其他方法进行测试。
  • 使用起来很不灵活,无法实现自动化测试。
  • 无法得到测试的报告,需要程序员自己去观察测试是否成功。

Junit单元测试框架

  • JUnit是使用Java语言实现的单元测试框架,它是第三方公司开源出来的,很多开发工具已经集成了Junit框架,比如IDEA。

优点

  • 编写的测试代码很灵活,可以指某个测试方法执行测试,也支持一键完成自动化测试
  • 不需要程序员去分析测试的结果,会自动生成测试报告出来。
  • 提供了更强大的测试能力。

需求

  • 某个系统,有多个业务方法,请使用Junit框架完成对这些方法的单元测试。

具体步骤

  1. 将Junit框架的jar包导入到项目中(注意:IDEA集成了Junit框架,不需要我们自己手工导入了)。
  2. 编写测试类、测试类方法(注意:测试方法必须是公共的,无参数,无返回值的非静态方法)。
  3. 【灵魂】必须在测试方法上使用@Test注解(标注该方法是一个测试方法)。(在报错的地方使用alt+enter,导入JUnit4。这个是经典版本,但是版本5可读性更好。)
  4. 在测试方法中,编写程序调用被测试的方法即可。(测试某个方法直接右键该方法启动测试。测试全部方法,可以选择类或者模块启动。)
  5. 选中测试方法,右键选择“JUnit运行” ,如果测试通过则是绿色;如果测试失败,则是红色

先准备一个类,假设写了一个StringUtil工具类,代码如下

1
2
3
4
5
public class StringUtil{
public static void printNumber(String name){
System.out.println("名字长度:"+name.length());
}
}

接下来,写一个测试类,测试StringUtil工具类中的方法能否正常使用。

1
2
3
4
5
6
7
public class StringUtilTest{
@Test
public void testPrintNumber(){
StringUtil.printNumber("admin");
StringUtil.printNumber(null);
}
}

写完代码之后,我们会发现测试方法左边,会有一个绿色的三角形按钮。点击这个按钮,就可以运行测试方法。

单元测试断言

断言:意思是程序员可以预测程序的运行结果,检查程序的运行结果是否与预期一致。**

我们在StringUtil类中新增一个测试方法

1
2
3
4
5
6
public static int getMaxIndex(String data){
if(data == null){
return -1;
}
return data.length();
}

接下来,我们在StringUtilTest类中写一个测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StringUtilTest{
@Test
public void testGetMaxIndex(){
int index1 = StringUtil.getMaxIndex(null);
System.out.println(index1);

int index2 = StringUtil.getMaxIndex("admin");
System.out.println(index2);

//断言机制:预测index2的结果
Assert.assertEquals("方法内部有Bug",4,index2);
}
}

运行测试方法,结果预期值与实际值不一致。

Junit框架的常见注解

常见注解(Junit 4.xxxx版本)

  1. @Test。测试方法。
  2. @Before。用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次。
  3. @After。用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次。
  4. @BeforeClass。用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次。
  5. @AfterClass。用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次。

特点如下

1
2
3
4
1.@BeforeClass标记的方法,执行在所有方法之前
2.@AfterCalss标记的方法,执行在所有方法之后
3.@Before标记的方法,执行在每一个@Test方法之前
4.@After标记的方法,执行在每一个@Test方法之后

常见注解(Junit 5.xxxx版本)

  1. @Test。测试方法。
  2. @BeforeEach。用来修饰一个实例方法,该方法会在每一个测试方法执行之前执行一次。
  3. @AfterEach。用来修饰一个实例方法,该方法会在每一个测试方法执行之后执行一次。
  4. @BeforeAll。用来修饰一个静态方法,该方法会在所有测试方法之前只执行一次。
  5. @AfterAll。用来修饰一个静态方法,该方法会在所有测试方法之后只执行一次。
  • 开始执行的方法:初始化资源。
  • 执行完之后的方法:释放资源。

作用?应用场景?

假设我想在每个测试方法中使用Socket对象,并且用完之后,需要把Socket关闭。代码就可以按照下面的结构来设计

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 StringUtilTest{
private static Socket socket;
@Before
public void test1(){
System.out.println("--> test1 Before 执行了");
}
@BeforeClass
public static void test11(){
System.out.println("--> test11 BeforeClass 执行了");
//初始化Socket对象
socket = new Socket();
}
@After
public void test2(){
System.out.println("--> test2 After 执行了");
}
@AfterCalss
public static void test22(){
System.out.println("--> test22 AfterCalss 执行了");
//关闭Socket
socket.close();
}
}

反射

认识反射Reflection

【核心思想】得到编译以后的class文件对象。

  • 反射指的是允许以编程方式访问已加载类的成分(成员变量、方法、构造器等)
  • 在java.lang.reflect包中。
  • 主要是用来做框架的。好理解一点,我们在写代码的时候,IDEA给我们提示的可以调用的方法。

反射学什么?

  1. 加载类,获取类的字节码:Class对象。
    1. 反射第一步:获取类:Class。
    2. 获取类的构造器:Constructor。
    3. 获取类的成员变量:Field。
    4. 获取类的成员方法:Method。
  2. 获取类的构造器:Constructor对象。
  3. 获取类的成员变量:Field对象。
  4. 获取类的成员方法:Method对象。

之后再看反射的应用场景。

获取类

反射第一步,获取Class类的对象。(3种方式。)

  1. Class c1 = 类名.class。
  2. 调用Class提供的方法:public static Class.forName(String package)。
  3. Object提供的方法:public Class getClass()。

image-20240410171810088

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test1Class{
public static void main(String[] args){
Class c1 = Student.class;
System.out.println(c1.getName()); //获取全类名
System.out.println(c1.getSimpleName()); //获取简单类名

Class c2 = Class.forName("com.itheima.d2_reflect.Student");
System.out.println(c1 == c2); //true

Student s = new Student();
Class c3 = s.getClass();
System.out.println(c2 == c3); //true
}
}

获取类的构造器

Class类提供的几个方法

  1. Constructor<?>[] getConstructors()。返回所有构造器对象的数组(只能拿public的)。
  2. Constructor<?>[] getDeclaredConstructors()。返回所有构造器对象的数组,存在就能拿到
  3. Constructor getConstructor(Class<?>… parameterTypes)。返回单个构造器对象(只能拿public的)。
  4. Constructor getDeclaredConstructor(Class<?>… parameterTypes)。返回单个构造器对象,存在就能拿到。

方便记忆:

1
2
3
4
get:获取
Declared: 有这个单词表示可以获取任意一个,没有这个单词表示只能获取一个public修饰的
Constructor: 构造方法的意思
后缀s: 表示可以获取多个,没有后缀s只能获取一个

假设现在有一个Cat类,里面有几个构造方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Cat{
private String name;
private int age;

public Cat(){

}

private Cat(String name, int age){

}
}

接下来,我们写一个测试方法,来测试获取类中所有的构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test2Constructor(){
@Test
public void testGetConstructors(){
//1、反射第一步:必须先得到这个类的Class对象
Class c = Cat.class;

//2、获取类的全部构造器
Constructor[] constructors = c.getDeclaredConstructors();
//3、遍历数组中的每一个构造器对象。
for(Constructor constructor: constructors){
System.out.println(constructor.getName()+"---> 参数个数:"+constructor.getParameterCount());
}
}
}

刚才的是获取Cat类中所有的构造器,接下来试一试单个构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test2Constructor(){
@Test
public void testGetConstructor(){
//1、反射第一步:必须先得到这个类的Class对象
Class c = Cat.class;

//2、获取类public修饰的空参数构造器
Constructor constructor1 = c.getConstructor();
System.out.println(constructor1.getName()+"---> 参数个数:"+constructor1.getParameterCount());

//3、获取private修饰的有两个参数的构造器,第一个参数String类型,第二个参数int类型
Constructor constructor2 =
c.getDeclaredConstructor(String.class,int.class);

System.out.println(constructor2.getName()+"---> 参数个数:"+constructor1.getParameterCount());

}
}

获取类构造器的作用

初始化对象返回。注意:这两个方法时属于Constructor的,需要用Constructor对象来调用。

  1. T newInstance(Object… initargs)。根据指定的构造器创建对象。(反射后的构造器依然是创建对象的。)
  2. public void setAccessible(boolean flag)。设置为true,表示取消访问检查,进行暴力反射。(–>反射会破坏封装性,私有的也可以执行了。)

代码演示

constructor1和constructor2分别表示Cat类中的两个构造器。现在我要把这两个构造器执行起来

由于构造器是private修饰的,先需要调用setAccessible(true) 表示禁止检查访问控制,然后再调用newInstance(实参列表) 就可以执行构造器,完成对象的初始化了。

代码如下:为了看到构造器真的执行, 故意在两个构造器中分别加了两个打印语句

1668579315586

注意:上面这种代码风格,看起来没什么必要,可以先稍微记一记,以后学习框架的时候有用。

获取类的成员变量&使用

Class类中提供的获取成员变量的方法

  1. Field[] getFields()。返回所有成员变量对象的数组(只能拿public的)。
  2. Field[] getDeclaredFields()。返回所有成员变量对象的数组存在就能拿到
  3. Field getField(String name)。返回单个成员变量对象(只能拿public的)。
  4. Field getDeclaredField(String name)。返回单个成员变量对象,存在就能拿到

记忆规则

1
2
3
4
get:获取
Declared: 有这个单词表示可以获取任意一个,没有这个单词表示只能获取一个public修饰的
Field: 成员变量的意思
后缀s: 表示可以获取多个,没有后缀s只能获取一个

获取成员变量的作用

依然是在某个对象中取值、赋值

Field类中用于取值、赋值的方法

  1. void set(Object obj, Object value)。赋值。
  2. Object get(Object obj)。获取值。
  3. setAccessible(boolean)。如果某成员变量是非public的,需要打开权限(暴力反射),然后再取值、赋值。

代码演示

  • 设有一个Cat类它有若干个成员变量,用Class类提供 的方法将成员变量的对象获取出来。

1668579681294

执行完上面的代码之后,我们可以看到控制台上打印输出了,每一个成员变量的名称和它的类型。

1668579804732

  • 获取到成员变量的对象之后该如何使用呢?

在Filed类中提供给给成员变量赋值和获取值的方法:

  1. void set(object obj,Object value)。赋值。
  2. object get(object obj)。取值。
  3. public voidsetAccessible(boolean flag)。设置为true,表示禁止检查访问控制(暴力反射)。

再次强调一下设置值、获取值的方法时Filed类的需要用Filed类的对象来调用,而且不管是设置值、还是获取值,都需要依赖于该变量所属的对象。代码如下

1668580288930

获取类的成员方法

使用反射技术获取方法对象并使用

  • 反射的第一步是先得到类对象,然后从类对象中获取类的成分对象。

Class类中用于获取成员方法的方法

  1. Method[] getMethods()。返回所有成员方法对象的数组(只能拿public的)。
  2. Method[] getDeclaredMethods()。返回所有成员方法对象的数组存在就能拿到
  3. Method getMethod(String name, Class<?>… parameterTypes)。返回单个成员方法对象(只能拿public的)。
  4. Method getDeclaredMethod(String name, Class<?>… parameterTypes)。返回单个成员方法对象,存在就能拿到

使用反射技术获取方法对象并使用

  • 获取成员方法的作用依然是在某个对象中进行执行此方法

Method类中用于触发执行的方法

  1. Object invoke(Object obj, Object… args)。
    • 参数一:用obj对象调用该方法
    • 参数二:调用方法的传递的参数(如果没有就不写)
    • 返回值:方法的返回值(如果没有就不写)
  2. setAccessible(boolean)。如果某成员方法是非public的,需要打开权限(暴力反射),然后再触发执行。

run()方法和eat(String 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
public class Test3Method{
public static void main(String[] args){
//1、反射第一步:先获取到Class对象
Class c = Cat.class;

//2、获取类中的全部成员方法
Method[] methods = c.getDecalaredMethods();

//3、遍历这个数组中的每一个方法对象
for(Method method : methods){
System.out.println(method.getName()+"-->"+method.getParameterCount()+"-->"+method.getReturnType());
}

System.out.println("-----------------------");
//4、获取private修饰的run方法,得到Method对象
Method run = c.getDecalaredMethod("run");
//执行run方法,在执行前需要取消权限检查
Cat cat = new Cat();
run.setAccessible(true);
Object rs1 = run.invoke(cat);
System.out.println(rs1)

//5、获取private 修饰的eat(String name)方法,得到Method对象
Method eat = c.getDeclaredMethod("eat",String.class);
eat.setAccessible(true);
Object rs2 = eat.invoke(cat,"鱼儿");
System.out.println(rs2)
}
}

作用、应用场景

作用

  1. 基本作用:得到一个类的全部成分然后操作;
  2. 破坏封装性;
  3. 最重要的用途:适合做Java的框架,基本上,主流的框架都会基于反射设计出一些通用的功能。

一个简易的框架

image-20240410220915153

步骤

1
2
3
4
5
6
7
8
9
1.先写好两个类,一个Student类和Teacher类
2.写一个ObjectFrame类代表框本架
在ObjectFrame类中定义一个saveObject(Object obj)方法,用于将任意对象存到文件中去
参数:Object obj: 就表示要存入文件中的对象

3.编写方法内部的代码,往文件中存储对象的属性名和属性值
1)参数obj对象中有哪些属性,属性名是什么实现值是什么,中有对象自己最清楚。
2)接着就通过反射获取类的成员变量信息了(变量名、变量值)
3)把变量名和变量值写到文件中去

写一个ObjectFrame表示自己设计的框架,代码如下图所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ObjectFrame{
public static void saveObject(Object obj) throws Exception{
PrintStream ps =
new PrintStream(new FileOutputStream("模块名\\src\\data.txt",true));
//1)参数obj对象中有哪些属性,属性名是什么实现值是什么,中有对象自己最清楚。
//2)接着就通过反射获取类的成员变量信息了(变量名、变量值)
Class c = obj.getClass(); //获取字节码
ps.println("---------"+class.getSimpleName()+"---------");

Field[] fields = c.getDeclaredFields(); //获取所有成员变量
//3)把变量名和变量值写到文件中去
for(Field field : fields){
String name = field.getName();
Object value = field.get(obj)+"";
ps.println(name);
}
ps.close();
}
}

使用自己设计的框架,往文件中写入Student对象的信息和Teacher对象的信息。

先准备好Student类和Teacher类

1
2
3
4
5
6
7
public class Student{
private String name;
private int age;
private char sex;
private double height;
private String hobby;
}
1
2
3
4
public class Teacher{
private String name;
private double salary;
}

创建一个测试类,在测试中类创建一个Student对象,创建一个Teacher对象,用ObjectFrame的方法把这两个对象所有的属性名和属性值写到文件中去。

1
2
3
4
5
6
7
8
9
10
public class Test5Frame{
@Test
public void save() throws Exception{
Student s1 = new Student("黑马吴彦祖",45, '男', 185.3, "篮球,冰球,阅读");
Teacher s2 = new Teacher("播妞",999.9);

ObjectFrame.save(s1);
ObjectFrame.save(s2);
}
}

注解

认识注解&定义注解

  • Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
  • Java 语言中的类、构造器、方法、成员变量、参数等都可以被注解进行标注。

自定义注解 — 格式

自定义注解就是自己做一个注解来使用。

1
2
3
public @interface 注解名称 {
public 属性类型 属性名() default 默认值 ;
}
  • 属性类型:Java支持的数据类型基本上都支持。

注解的作用

  • 对Java中类、方法、成员变量做标记,然后进行特殊处理,至于到底做何种处理由业务需求来决定。
  • 例如:JUnit框架中,标记了注解@Test的方法就可以被当成测试方法执行,而没有标记的就不能当成测试方法执行。

特殊属性

  • value属性,如果只有一个value属性的情况下,使用value属性的时候可以省略value名称不写!!
  • 但是如果有多个属性, 且多个属性没有默认值,那么value名称是不能省略的。

比如:现在我们自定义一个MyTest注解

1
2
3
4
5
public @interface MyTest{
String aaa();
boolean bbb() default true; //default true 表示默认值为true,使用时可以不赋值。
String[] ccc();
}

定义好MyTest注解之后,我们可以使用MyTest注解在类上、方法上等位置做标记。注意使用注解时需要加@符号,如下

1
2
3
4
5
6
7
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{
@MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"})
public void test1(){

}
}

注意:注解的属性名如何是value的话,并且只有value没有默认值,使用注解时value名称可以省略。比如现在重新定义一个MyTest2注解

1
2
3
4
public @interface MyTest2{
String value(); //特殊属性
int age() default 10;
}

定义好MyTest2注解后,再将@MyTest2标记在类上,此时value属性名可以省略,代码如下

1
2
3
4
5
6
7
8
@MyTest2("孙悟空") //等价于 @MyTest2(value="孙悟空")
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{
@MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"})
public void test1(){

}
}

注解的本质是接口

想要搞清楚注解本质是什么东西,我们可以把注解的字节码进行反编译,使用XJad工具进行反编译。经过对MyTest1注解字节码反编译我们会发现:

1
2
3
4
1.MyTest1注解本质上是接口,每一个注解接口都继承子Annotation接口
2.MyTest1注解中的属性本质上是抽象方法
3.@MyTest1实际上是作为MyTest接口的实现类对象
4.@MyTest1(aaa="孙悟空",bbb=false,ccc={"Python","前端","Java"})里面的属性值,可以通过调用aaa()、bbb()、ccc()方法获取到。 【别着急,继续往下看,再解析注解时会用到】

元注解

元注解:注解注解的注解。

元注解有两个:

  • @Target: 约束自定义注解只能在哪些地方使用。
  • @Retention:申明注解的生命周期。

1669605746113

@Target注解和@Retention注解

1
2
@Target是用来声明注解只能用在那些位置,比如:类上、方法上、成员变量上等
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期

1669605786028

@Target元注解的使用:

比如定义一个MyTest3注解,并添加@Target注解用来声明MyTest3的使用位置

1
2
3
4
@Target(ElementType.TYPE)	//声明@MyTest3注解只能用在类上
public @interface MyTest3{

}

接下来,把@MyTest3用来类上观察是否有错,再把@MyTest3用在方法上、变量上再观察是否有错。

如果我们定义MyTest3注解时,使用@Target注解属性值写成下面样子

1
2
3
4
5
//声明@MyTest3注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface MyTest3{

}

再观察,@MyTest用在类上、方法上、变量上是否有错。

@Retetion元注解的使用

定义MyTest3注解时,给MyTest3注解添加@Retetion注解来声明MyTest3注解保留的时期

1
2
3
4
5
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期
@Retetion(RetetionPloicy.SOURCE): 注解保留到源代码时期、字节码中就没有了
@Retetion(RetetionPloicy.CLASS): 注解保留到字节码中、运行时注解就没有了
@Retetion(RetetionPloicy.RUNTIME):注解保留到运行时期
【自己写代码时,比较常用的是保留到运行时期】
1
2
3
4
5
6
7
//声明@MyTest3注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
//控制使用了@MyTest3注解的代码中,@MyTest3保留到运行时期
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest3{

}

解析注解

注解的操作中经常需要进行解析,注解的解析就是判断是否存在注解,存在注解就解析出内容。

  • 可以通过反射技术把类上、方法上、变量上的注解对象获取出来,然后通过调用方法就可以获取注解上的属性值了。

解析注解套路如下

1
2
3
4
1.如果注解在类上,先获取类的字节码对象,再获取类上的注解
2.如果注解在方法上,先获取方法对象,再获取方法上的注解
3.如果注解在成员变量上,先获取成员变量对象,再获取变量上的注解
总之:注解在谁身上,就先获取谁,再用谁获取谁身上的注解

Class 、Method、Field,Constructor、都实现了AnnotatedElement接口,它们都拥有解析注解的能力。

  1. public Annotation[]getDeclaredAnnotations()。获取当前对象上面的注解。
  2. public TgetDeclaredAnnotation(ClassannotationClass)。获取指定的注解对象。
  3. public boolean isAnnotationPresent(class annotationclass)。判断当前对象上是否存在某个注解。

案例需求

  1. 定义注解MyTest4,要求如
    • 包含属性:String value()。
    • 包含属性:double aaa(),默认值为 100。
    • 包含属性:Stringllbbb()。
    • 限制注解使用的位置:类和成员方法上。
    • 指定注解的有效范围:一直到运行时。
  2. 定义一个类叫:Demo,在类中定义一个test1方法,并在该类和其方法上使用MyTest4注解。
  3. 定义AnnotationTest3测试类,解析Demo类中的全部注解。

代码:

① 先定义一个MyTest4注解

1
2
3
4
5
6
7
8
9
//声明@MyTest4注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
//控制使用了@MyTest4注解的代码中,@MyTest4保留到运行时期
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest4{
String value();
double aaa() default 100;
String[] bbb();
}

② 定义有一个类Demo

1
2
3
4
5
6
7
@MyTest4(value="蜘蛛侠",aaa=99.9, bbb={"至尊宝","黑马"})
public class Demo{
@MyTest4(value="孙悟空",aaa=199.9, bbb={"紫霞","牛夫人"})
public void test1(){

}
}

③ 写一个测试类AnnotationTest3解析Demo类上的MyTest4注解

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
public class AnnotationTest3{
@Test
public void parseClass(){
//1.先获取Class对象
Class c = Demo.class;

//2.解析Demo类上的注解
if(c.isAnnotationPresent(MyTest4.class)){
//获取类上的MyTest4注解
MyTest4 myTest4 = (MyTest4)c.getDeclaredAnnotation(MyTest4.class);
//获取MyTests4注解的属性值
System.out.println(myTest4.value());
System.out.println(myTest4.aaa());
System.out.println(myTest4.bbb());
}
}

@Test
public void parseMethods(){
//1.先获取Class对象
Class c = Demo.class;

//2.解析Demo类中test1方法上的注解MyTest4注解
Method m = c.getDeclaredMethod("test1");
if(m.isAnnotationPresent(MyTest4.class)){
//获取方法上的MyTest4注解
MyTest4 myTest4 = (MyTest4)m.getDeclaredAnnotation(MyTest4.class);
//获取MyTests4注解的属性值
System.out.println(myTest4.value());
System.out.println(myTest4.aaa());
System.out.println(myTest4.bbb());
}
}
}

注解的应用场景

注解是用来写框架的,比如现在我们要模拟Junit写一个测试框架,要求有@MyTest注解的方法可以被框架执行,没有@MyTest注解的方法不能被框架执行。

第一步:先定义一个MyTest注解

1
2
3
4
5
@Target(ElementType.METHOD)	
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest{

}

第二步:写一个测试类AnnotationTest4,在类中定义几个被@MyTest注解标记的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AnnotationTest4{
@MyTest
public void test1(){
System.out.println("=====test1====");
}

@MyTest
public void test2(){
System.out.println("=====test2====");
}


public void test3(){
System.out.println("=====test2====");
}

public static void main(String[] args){
AnnotationTest4 a = new AnnotationTest4();

//1.先获取Class对象
Class c = AnnotationTest4.class;

//2.解析AnnotationTest4类中所有的方法对象
Method[] methods = c.getDeclaredMethods();
for(Method m: methods){
//3.判断方法上是否有MyTest注解,有就执行该方法
if(m.isAnnotationPresent(MyTest.class)){
m.invoke(a);
}
}
}
}

动态代理

代理思想就是被代理者没有能力,或者不愿意去完成某件事情,需要找个人(代理)代替自己去完成这件事。

动态代理主要是对被代理对象的行为进行代理。是一个对象。

1669619922161

先把有唱歌和跳舞功能的接口,和实现接口的大明星类定义出来:

1669620054411

动态代理的开发步骤

  1. 必须定义接口,里面定义一些行为,用来约束被代理对象和代理对象都要完成的事情
  2. 定义一个实现类实现接口,这个实现类的对象代表被代理的对象
  3. 定义一个测试类,在里面创建被代理对象,然后为其创建一个代理对象返回。(重点)
  4. 代理对象中,需要模拟收首付款,真正触发被代理对象的行为,然后接收尾款操作。
  5. 通过返回的代理对象进行方法的调用,观察动态代理的执行流程。

如何创建代理对象

  • Java中代理的代表类是:java.lang.reflect.Proxy,它提供了一个静态方法,用于为被代理对象,产生一个代理对象返回。
1
2
3
4
5
6
7
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,
InvocationHandler h)

为被代理对象返回一个代理对象。
参数一:类加载器 加载代理类,产生代理对象。,。
参数二:真实业务对象的接口。(被代理的方法交给代理对象)
参数三:代理的核心处理程序。

写一个为BigStar生成动态代理对象的工具类。这里需要用Java为开发者提供的一个生成代理对象的类叫Proxy类

通过Proxy类的newInstance(…)方法可以为实现了同一接口的类生成代理对象。 调用方法时需要传递三个参数

1669620794550

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ProxyUtil {
public static Star createProxy(BigStar bigStar){
/* newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
参数1:用于指定一个类加载器
参数2:指定生成的代理长什么样子,也就是有哪些方法
参数3:用来指定生成的代理对象要干什么事情
*/
// Star starProxy = ProxyUtil.createProxy(s);
// starProxy.sing("好日子") starProxy.dance()
Star starProxy = (Star) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
new Class[]{Star.class}, new InvocationHandler() {
@Override // 回调方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 代理对象要做的事情,会在这里写代码
if(method.getName().equals("sing")){
System.out.println("准备话筒,收钱20万");
}else if(method.getName().equals("dance")){
System.out.println("准备场地,收钱1000万");
}
return method.invoke(bigStar, args);
}
});
return starProxy;
}
}

调用我们写好的ProxyUtil工具类,为BigStar对象生成代理对象

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
BigStar s = new BigStar("杨超越");
Star starProxy = ProxyUtil.createProxy(s);

String rs = starProxy.sing("好日子");
System.out.println(rs);

starProxy.dance();
}
}

通过代理对象调用方法,执行流程?

  • 先走向代理。
  • 代理中可以真正触发被代理对象的方法执行。
  • 回到代理中,由代理负责返回结果给调用者。

案例

需求

  • 模拟某企业用户管理业务,需包含用户登录,用户删除,用户查询功能,并要统计每个功能的耗时。

分析

  • 定义一个UserService表示用户业务接口,规定必须完成用户登录,用户删除,用户查询功能。
  • 定义一个实现类UserServiceImpl实现UserService,并完成相关功能,且统计每个功能的耗时。
  • 定义测试类,创建实现类对象,调用方法。

现有如下代码

1
2
3
4
5
6
7
8
9
10
11
/**
* 用户业务接口
*/
public interface UserService {
// 登录功能
void login(String loginName,String passWord) throws Exception;
// 删除用户
void deleteUsers() throws Exception;
// 查询用户,返回数组的形式。
String[] selectUsers() throws Exception;
}

UserService接口的实现类,每一个方法中要计算方法运行时间:

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 UserServiceImpl implements UserService{
@Override
public void login(String loginName, String passWord) throws Exception {
long time1 = System.currentTimeMillis();
if("admin".equals(loginName) && "123456".equals(passWord)){
System.out.println("您登录成功,欢迎光临本系统~");
}else {
System.out.println("您登录失败,用户名或密码错误~");
}
Thread.sleep(1000);
long time2 = System.currentTimeMillis();
System.out.println("login方法耗时:"+(time2-time1));
}

@Override
public void deleteUsers() throws Exception{
long time1 = System.currentTimeMillis();
System.out.println("成功删除了1万个用户~");
Thread.sleep(1500);
long time2 = System.currentTimeMillis();
System.out.println("deleteUsers方法耗时:"+(time2-time1));
}

@Override
public String[] selectUsers() throws Exception{
long time1 = System.currentTimeMillis();
System.out.println("查询出了3个用户");
String[] names = {"张全蛋", "李二狗", "牛爱花"};
Thread.sleep(500);
long time2 = System.currentTimeMillis();
System.out.println("selectUsers方法耗时:"+(time2-time1));
return names;
}
}

本案例存在哪些问题?

  • 答:业务对象的的每个方法都要进行性能统计,存在大量重复的代码。
  • 每一个方法中计算耗时的代码都是重复的,这些重复的代码并不属于UserSerivce的主要业务代码。接下来打算把计算每一个方法的耗时操作交给代理对象来做。

优化的关键步骤

  1. 必须有接口,实现类要实现接口(代理通常是基于接口实现的)。
  2. 创建一个实现类的对象,该对象为业务对象,紧接着为业务对象做一个代理对象。

先在UserService类中把计算耗时的代码删除,代码如下

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 class UserServiceImpl implements UserService{
@Override
public void login(String loginName, String passWord) throws Exception {
if("admin".equals(loginName) && "123456".equals(passWord)){
System.out.println("您登录成功,欢迎光临本系统~");
}else {
System.out.println("您登录失败,用户名或密码错误~");
}
Thread.sleep(1000);
}

@Override
public void deleteUsers() throws Exception{
System.out.println("成功删除了1万个用户~");
Thread.sleep(1500);
}

@Override
public String[] selectUsers() throws Exception{

System.out.println("查询出了3个用户");
String[] names = {"张全蛋", "李二狗", "牛爱花"};
Thread.sleep(500);

return names;
}
}

然后为UserService生成一个动态代理对象,在动态代理中调用目标方法,在调用目标方法之前和之后记录毫秒值,并计算方法运行的时间。代码如下

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 class ProxyUtil {
public static UserService createProxy(UserService userService){
UserService userServiceProxy
= (UserService) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{UserService.class},
new InvocationHandler() {
@Override
public Object invoke( Object proxy,
Method method,
Object[] args) throws Throwable { if(
method.getName().equals("login") || method.getName().equals("deleteUsers")||
method.getName().equals("selectUsers")){
//方法运行前记录毫秒值
long startTime = System.currentTimeMillis();
//执行方法
Object rs = method.invoke(userService, args);
//执行方法后记录毫秒值
long endTime = System.currentTimeMillis();

System.out.println(method.getName() + "方法执行耗时:" + (endTime - startTime)/ 1000.0 + "s");
return rs;
}else {
Object rs = method.invoke(userService, args);
return rs; }
} });
//返回代理对象
return userServiceProxy;
}
}

在测试类中为UserService创建代理对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 目标:使用动态代理解决实际问题,并掌握使用代理的好处。
*/
public class Test {
public static void main(String[] args) throws Exception{
// 1、创建用户业务对象。
UserService userService = ProxyUtil.createProxy(new UserServiceImpl());

// 2、调用用户业务的功能。
userService.login("admin", "123456");
System.out.println("----------------------------------");

userService.deleteUsers();
System.out.println("----------------------------------");

String[] names = userService.selectUsers();
System.out.println("查询到的用户是:" + Arrays.toString(names));
System.out.println("----------------------------------");

}
}

动态代理对象的执行流程如下图所示,每次用代理对象调用方法时,都会执行InvocationHandler中的invoke方法。

1669622605490

动态代理的优点

  • 可以在不改变方法源码的情况下,实现对方法功能的增强,提高了代码的复用。
  • 简化了编程工作、提高了开发效率,同时提高了软件系统的可扩展性。
  • 可以为被代理对象的所有方法做代理。
  • 非常的灵活,支持任意接口类型的实现类对象做代理,也可以直接为接本身做代理。

【Java基础】【java.lang】多线程

概述

线程

  • 线程(Thread)是一个程序内部的一条执行流程。
  • 程序中如果只有一条执行流程,那这个程序就是单线程的程序。

多线程

  • 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
  • 例如:买票系统、百度网盘下载、消息通信、淘宝、京东系统都离不开多线程技术。

创建方法

Java是通过java.lang.Thread 类的对象来代表线程的。

注意main方法由一条默认的主线负责执行。在main里面启动的线程称为子线程。

方式1:继承Thread类

步骤

  1. 定义一个子类MyThread继承线程类java.lang.Thread。
  2. 重写run()方法。
  3. 创建MyThread类的对象。
  4. 调用线程对象的start()方法启动线程(启动后还是执行run方法的)。

优缺点

  • 优点:编码简单。
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展

注意事项

  1. 启动线程必须是调用start方法,不是调用run方法。
    • 如果直接调用run方法就不认为是一条线程启动了,而是把Thread当做一个普通对象,此时run方法中的执行的代码会成为主线程的一部分。
  2. 不要把主线程任务放在启动子线程之前(否则先跑完主线程才启动子线程)。
    • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
    • 只有调用start方法才是启动一个新的线程执行。
    • 这样主线程一直是先跑完的,相当于是一个单线程的效果了。
1
2
3
4
5
6
7
8
9
10
public class MyThread extends Thread{
// 2、必须重写Thread类的run方法
@Override
public void run() {
// 描述线程的执行任务。
for (int i = 1; i <= 5; i++) {
System.out.println("子线程MyThread输出:" + i);
}
}
}

再定义一个测试类,在测试类中创建MyThread线程对象,并启动线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadTest1 {
// main方法是由一条默认的主线程负责执行。
public static void main(String[] args) {
// 3、创建MyThread线程类的对象代表一个线程
Thread t = new MyThread();
// 4、启动线程(自动执行run方法的)
t.start();

for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
}

MyThread和main线程在相互抢夺CPU的执行权(注意:哪一个线程先执行,哪一个线程后执行,目前我们是无法控制的,每次输出结果都会不一样)。

方式2:实现Runnable接口

步骤

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。

  2. 创建MyRunnable任务对象。

  3. 把MyRunnable任务对象交给Thread线程对象处理。

    public Thread(Runnable target)。封装Runnable对象成为线程对象。

  4. 调用线程对象的start()方法启动线程。

优缺点

  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
  • 缺点:需要多一个Runnable对象。

先准备一个Runnable接口的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 1、定义一个任务类,实现Runnable接口
*/
public class MyRunnable implements Runnable{
// 2、重写runnable的run方法
@Override
public void run() {
// 线程要执行的任务。
for (int i = 1; i <= 5; i++) {
System.out.println("子线程输出 ===》" + i);
}
}
}

再写一个测试类,在测试类中创建线程对象,并执行线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadTest2 {
public static void main(String[] args) {
// 3、创建任务对象。
Runnable target = new MyRunnable();
// 4、把任务对象交给一个线程对象处理。
// public Thread(Runnable target)
new Thread(target).start();

for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出 ===》" + i);
}
}
}

匿名内部类写法(简化代码风格)

  1. 可以创建Runnable的匿名内部类形式(任务对象)。
  2. 再交给Thread线程对象。
  3. 再调用线程对象的start()启动线程。
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 ThreadTest2_2 {
public static void main(String[] args) {
// 1、直接创建Runnable接口的匿名内部类形式(任务对象)
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程1输出:" + i);
}
}
};
new Thread(target).start();

// 简化形式1:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程2输出:" + i);
}
}
}).start();

// 简化形式2:
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程3输出:" + i);
}
}).start();

for (int i = 1; i <= 5; i++) {
System.out.println("主线程main输出:" + i);
}
}
}

方式3:实现Callable接口

原因:

  • 前面两种的问题:假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。

解决方案:

  • JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)。
  • 这种方式最大的优点:可以返回线程执行完毕后的结果。

步骤

  1. 创建任务对象
    • 定义一个类实现Callable接口重写call方法,封装要做的事情,和要返回的数据。
    • 把Callable类型的对象封装成FutureTask(线程任务对象)
  2. 把线程任务对象交给Thread对象。
  3. 调用Thread对象的start方法启动线程。
  4. 线程执行完毕后,通过FutureTask对象的的get方法去获取线程任务执行的结果。

未来任务对象的作用?

  1. 是一个任务对象,实现了Runnable对象。
  2. 可以在线程执行完毕后,通过FutureTask对象的的get方法去获取线程任务执行的结果。

先准备一个Callable接口的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 1、让子类继承Thread线程类。
*/
public class MyThread extends Thread{
// 2、必须重写Thread类的run方法
@Override
public void run() {
// 描述线程的执行任务。
for (int i = 1; i <= 5; i++) {
System.out.println("子线程MyThread输出:" + i);
}
}
}

再定义一个测试类,在测试类中创建线程并启动线程,还要获取返回结果:

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 ThreadTest3 {
public static void main(String[] args) throws Exception {
// 3、创建一个Callable的对象
Callable<String> call = new MyCallable(100);
// 4、把Callable的对象封装成一个FutureTask对象(任务对象)
// 未来任务对象的作用?
// 1、是一个任务对象,实现了Runnable对象.
// 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
FutureTask<String> f1 = new FutureTask<>(call);
// 5、把任务对象交给一个Thread对象
new Thread(f1).start();


Callable<String> call2 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call2);
new Thread(f2).start();


// 6、获取线程执行完毕后返回的结果。
// 注意:如果执行到这儿,假如上面的线程还没有执行完毕
// 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
String rs = f1.get();
System.out.println(rs);

String rs2 = f2.get();
System.out.println(rs2);
}
}

FutureTask的API

  • 构造器:public FutureTask<>(Callable call)。把Callable对象封装成FutureTask对象。
  • 方法:public V get() throws Exception。获取线程执行call方法返回的结果。

优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果
  • 缺点:编码复杂一点。

3种方法比较

方式 优点 缺点
继承Thread类 编程比较简单,可以直接使用Thread类中的方法 扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类。 编程相对复杂,不能返回线程执行的结果
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 编程相对复杂

Thread的常用方法

常用方法

  1. public void run()。线程的任务方法。
  2. public void start()。线程的任务方法。
  3. public String getName()。获取当前线程的名称,线程名称默认是Thread-索引。
  4. public void setName(String name)。为线程设置名称
  5. public static Thread currentThread()。获取当前执行的线程对象
  6. public static void sleep(long time)。让当前执行的线程休眠多少毫秒后,再继续执行。
  7. public final void join()…。让调用当前这个方法的线程先执行完

构造器

  1. public Thread(String name)。可以为当前线程指定名称。
  2. public Thread(Runnable target)。封装Runnable对象成为线程对象。
  3. public Thread(Runnable target, String name)。封装Runnable对象成为线程对象,并指定线程名称。

其他

Thread类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,用到再说。

线程安全

什么是线程安全问题

  • 多个线程,同时访问同一个共享资源,并存在修改该资源的时候,可能会出现业务安全问题。

取钱的线程安全问题

  • 场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?

用程序模拟线程安全问题

先定义一个共享的账户类

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
public class Account {
private String cardId; // 卡号
private double money; // 余额。

public Account() {
}

public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}

// 小明 小红同时过来的
public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
}

public String getCardId() {
return cardId;
}

public void setCardId(String cardId) {
this.cardId = cardId;
}

public double getMoney() {
return money;
}

public void setMoney(double money) {
this.money = money;
}
}

再定义一个是取钱的线程类

1
2
3
4
5
6
7
8
9
10
11
12
public class DrawThread extends Thread{
private Account acc;
public DrawThread(Account acc, String name){
super(name);
this.acc = acc;
}
@Override
public void run() {
// 取钱(小明,小红)
acc.drawMoney(100000);
}
}

最后,再写一个测试类,在测试类中创建两个线程对象

1
2
3
4
5
6
7
8
9
public class ThreadTest {
public static void main(String[] args) {
// 1、创建一个账户对象,代表两个人的共享账户。
Account acc = new Account("ICBC-110", 100000);
// 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万。
new DrawThread(acc, "小明").start(); // 小明
new DrawThread(acc, "小红").start(); // 小红
}
}

运行程序,会发现两个人都取了10万块钱,余额为-10万了。

线程同步

认识线程同步

  • 线程同步:解决线程安全问题的方案。
  • 思想:让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

常见方案

  • 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

方式1:同步代码块

  • 作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
1
2
3
4
5
6
7
8
synchronized(同步锁) {
访问共享资源的核心代码
}
//快捷键:ctrl+alt+enter,选择第9个
//同步锁例如可以写“黑马”,因为它在常量中只有一份
//但是有个问题,就是小明来取锁住了自己家的账户,其他家的人也取不了钱了……(锁的对象太大!影响无关线程的执行)
//所以同步锁可以用“this”
//拓展:假设遇到多个线程调用静态方法,官方建议使用“类名.class”作为锁
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
  • 同步锁的注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

只需要修改DrawThread类中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 小明 小红线程同时过来的
public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
// this正好代表共享资源!
synchronized (this) {
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
}
}

【锁对象如何选择的问题】

1
2
3
1.建议把共享资源作为锁对象, 不要将随便无关的对象当做锁对象
2.对于实例方法,建议使用this作为锁对象
3.对于静态方法,建议把类的字节码(类名.class)当做锁对象

方式2:同步方法

  • 作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
  • 如果方法是实例方法:同步方法默认用this作为的锁对象。
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

同步方法

1
2
3
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}

同步代码块好还是同步方法好?

  • 范围上:同步代码块锁的范围更小,同步方法锁的范围更大。(锁的范围越小,性能越好~)
  • 可读性:同步方法更好。(计算机性能上去了,前一个差别不大~)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 同步方法
public synchronized void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
// 1、判断余额是否足够
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
}

方式3:Lock锁

  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

构造器

  1. public ReentrantLock()。获得Lock锁的实现类对象。

常用方法

  1. void lock()。获得锁。
  2. void unlock()。释放锁。

格式

1
2
3
4
5
6
1.首先在成员变量位子,需要创建一个Lock接口的实现类对象(这个对象就是锁对象)
private final Lock lk = new ReentrantLock();
2.在需要上锁的地方加入下面的代码
lk.lock(); // 加锁
//...中间是被锁住的代码...
lk.unlock(); // 解锁

使用Lock锁改写前面DrawThread中取钱的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建了一个锁对象
private final Lock lk = new ReentrantLock();

public void drawMoney(double money) {
// 先搞清楚是谁来取钱?
String name = Thread.currentThread().getName();
try {
lk.lock(); // 加锁
// 1、判断余额是否足够
if(this.money >= money){
System.out.println(name + "来取钱" + money + "成功!");
this.money -= money;
System.out.println(name + "来取钱后,余额剩余:" + this.money);
}else {
System.out.println(name + "来取钱:余额不足~");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lk.unlock(); // 解锁
}
}
}

记得用try-catch-finally来保证一定会解锁!

线程通信[了解]

当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。

线程通信的常见模型(生产者与消费者模型)

  • 生产者线程负责生产数据。
  • 消费者线程负责消费生产者生产的数据。
  • 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!

Object类的等待和唤醒方法:

  1. void wait()。让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法。
  2. void notify()。唤醒正在等待的单个线程。
  3. void notifyAll()。唤醒正在等待的所有线程。

注意

  • 上述方法应该使用当前同步锁对象进行调用。

分析一下完成这个案例的思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1.先确定在这个案例中,什么是共享数据?
答:这里案例中桌子是共享数据,因为厨师和顾客都需要对桌子上的包子进行操作。

2.再确定有那几条线程?哪个是生产者,哪个是消费者?
答:厨师是生产者线程,3条生产者线程;
顾客是消费者线程,2条消费者线程

3.什么时候将哪一个线程设置为什么状态
生产者线程(厨师)放包子:
1)先判断是否有包子
2)没有包子时,厨师开始做包子, 做完之后把别人唤醒,然后让自己等待
3)有包子时,不做包子了,直接唤醒别人、然后让自己等待

消费者线程(顾客)吃包子:
1)先判断是否有包子
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
public class Desk {
private List<String> list = new ArrayList<>();

// 放1个包子的方法
// 厨师1 厨师2 厨师3
public synchronized void put() {
try {
String name = Thread.currentThread().getName();
// 判断是否有包子。
if(list.size() == 0){
list.add(name + "做的肉包子");
System.out.println(name + "做了一个肉包子~~");
Thread.sleep(2000);

// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
}else {
// 有包子了,不做了。
// 唤醒别人, 等待自己
this.notifyAll();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}

// 吃货1 吃货2
public synchronized void get() {
try {
String name = Thread.currentThread().getName();
if(list.size() == 1){
// 有包子,吃了
System.out.println(name + "吃了:" + list.get(0));
list.clear();
Thread.sleep(1000);
this.notifyAll();
this.wait();
}else {
// 没有包子
this.notifyAll();
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

再写测试类,在测试类中,创建3个厨师线程对象,再创建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
public class ThreadTest {
public static void main(String[] args) {
// 需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
// 2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。
Desk desk = new Desk();

// 创建3个生产者线程(3个厨师)
new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师1").start();

new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师2").start();

new Thread(() -> {
while (true) {
desk.put();
}
}, "厨师3").start();

// 创建2个消费者线程(2个吃货)
new Thread(() -> {
while (true) {
desk.get();
}
}, "吃货1").start();

new Thread(() -> {
while (true) {
desk.get();
}
}, "吃货2").start();
}
}

线程池

认识线程池

线程池就是一个可以复用线程的技术。

不使用线程池的问题

  • 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的, 而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能

线程池:正在进行的称为工作线程WorkThread,等待执行的任务在任务队列(WorkQueue)。这些任务对象必须要实现Runnable或者Callable对象。

如何创建线程池?

JDK 5.0起提供了代表线程池的接口:ExecutorService

如何得到线程池对象?

  1. 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
  2. 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

ThreadPoolExecutor构造器【重要】

1
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 
  1. 参数一:corePoolSize : 指定线程池的核心线程的数量。
  2. 参数二:maximumPoolSize:指定线程池的最大线程数量。(一般要大于corePoolSize,多出来的就是临时线程。)
  3. 参数三:keepAliveTime :指定临时线程的存活时间。
  4. 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)。
  5. 参数五:workQueue:指定线程池的任务队列。
  6. 参数六:threadFactory:指定线程池的线程工厂。(负责创建线程。)
  7. 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)。

理解

image-20240409162613513

用构造器创建线程池对象的代码

1
2
3
4
5
6
7
8
9
ExecutorService pool = new ThreadPoolExecutor(
3, //核心线程数有3个
5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
TimeUnit.SECONDS,//时间单位(秒)
new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
Executors.defaultThreadFactory(), //用于创建线程的工厂对象
new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);

注意事项

核心线程–>任务队列–>临时线程–>until占满最大线程–>拒绝新任务。

(1)临时线程什么时候创建?

  • 新任务提交时,发现核心线程都在忙任务队列满了、并且还可以创建临时线程,此时会创建临时线程。

(2)什么时候开始拒绝新的任务?

  • 核心线程和临时线程都在忙任务队列也满了新任务过来时才会开始拒绝任务。

程池执行的任务可以有2种:

  1. Runnable任务;
  2. callable任务。

线程池处理Runnable任务

ExecutorService常用方法

  1. void execute(Runnable command)。执行 Runnable 任务。
  2. Future submit(Callable task)。执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果。
  3. void shutdown()。等全部任务执行完毕后,再关闭线程池!(关闭线程池的2种方法:1.点红方块停止,2.调用本方法。注意,执行了含有线程池的程序之后,线程池不停止是正常现象!)
  4. List shutdownNow()。立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务。

新任务拒绝策略(参数七:handler)

  1. ThreadPoolExecutor.AbortPolicy。丢弃任务并抛出。RejectedExecutionException异常。是默认的策略。
  2. ThreadPoolExecutor.DiscardPolicy。丢弃任务,但是不抛出异常,这是不推荐的做法。
  3. ThreadPoolExecutor.DiscardOldestPolicy。抛弃队列中等待最久的任务, 然后把当前任务加入队列中。
  4. ThreadPoolExecutor.CallerRunsPolicy。由主线程负责调用任务的run()方法从而绕过线程池直接执行

代码

先准备一个线程任务类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyRunnable implements Runnable{
@Override
public void run() {
// 任务是干啥的?
System.out.println(Thread.currentThread().getName() + " ==> 输出666~~");
//为了模拟线程一直在执行,这里睡久一点
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

下面是执行Runnable任务的代码,注意阅读注释,对照着前面的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
ExecutorService pool = new ThreadPoolExecutor(
3, //核心线程数有3个
5, //最大线程数有5个。 临时线程数=最大线程数-核心线程数=5-3=2
8, //临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
TimeUnit.SECONDS,//时间单位(秒)
new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待
Executors.defaultThreadFactory(), //用于创建线程的工厂对象
new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);

Runnable target = new MyRunnable();
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
//下面4个任务在任务队列里排队
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);

//下面2个任务,会被临时线程的创建时机了
pool.execute(target);
pool.execute(target);
// 到了新任务的拒绝时机了!
pool.execute(target);

执行上面的代码,结果输出如下

1668067745116

线程池处理Callable任务

Callable任务与Runnable任务最大的不同:执行完毕后可以返回结果。

上一小节提到的submit()方法。

先准备一个Callable线程任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}

// 2、重写call方法
@Override
public String call() throws Exception {
// 描述线程的任务,返回线程执行返回后的结果。
// 需求:求1-n的和返回。
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;
}
}

再准备一个测试类,在测试类中创建线程池,并执行callable任务。

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
public class ThreadPoolTest2 {
public static void main(String[] args) throws Exception {
// 1、通过ThreadPoolExecutor创建一个线程池对象。
ExecutorService pool = new ThreadPoolExecutor(
3,
5,
8,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());

// 2、使用线程处理Callable任务。
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(200));
Future<String> f3 = pool.submit(new MyCallable(300));
Future<String> f4 = pool.submit(new MyCallable(400));

// 3、执行完Callable任务后,需要获取返回结果。
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}

执行后,结果如下图所示

1668067964048

说明线程2复用了。

Executors工具类实现线程池

是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。

常用方法:

  1. public static ExecutorService newFixedThreadPool(int nThreads)。创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
  2. public static ExecutorService newSingleThreadExecutor()。创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。(不会死掉!)
  3. public static ExecutorService newCachedThreadPool()。线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。
  4. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)。创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

注意 :这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象

【核心线程数量配置多少?】行业通用—>

  • 计算密集型的任务:核心线程数量 = CPU核数 + 1;
  • IO密集型的任务:核心线程数量 = CPU核数 * 2;

【Executors使用可能存在的陷阱】

  • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。(小的系统也最好不要用,容易留安全隐患。)
  • image-20240409192641654

其他细节知识:并发、并行

进程

  • 正在运行的程序(软件)就是一个独立的进程。
  • 线程属于进程的,一个进程中可以同时运行很多个线程
  • 进程中的多个线程其实是并发和并行执行的。

并发

  • 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限为了保证全部线程都能往前执行CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

并行

  • 在同一个时刻上,同时有多个线程在被CPU调度执行

【多线程到底是怎么执行的?】

并发和并行同时进行的!

  • 并发:CPU分时轮询的执行线程。
  • 并行:同一个时刻同时在执行。

其他细节知识:线程的生命周期

线程的生命周期

  • 也就是线程从生到死的过程中,经历的各种状态及状态转换。
  • 理解线程这些状态有利于提升并发编程的理解能力。

Java线程的状态

  • Java总共定义了6种状态。
  • 6种状态都定义在Thread类的内部枚举类中。(Thread.State可以调出来。)
1
2
3
4
5
6
7
8
9
10
11
12
public class Thread{
...
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
...
}
线程状态 说明
NEW(新建) 线程刚被创建,但是并未启动。
Runnable(可运行) 线程已经调用了start(),等待CPU调度
Blocked(锁阻塞) 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。
Waiting(无限等待) 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
Timed Waiting(计时等待) 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

线程的6种状态互相转换

image-20240409194225380

【Java基础】【java.net】网络编程

网络编程概述

基本的通信架构

基本的通信架构有2种形式:CS架构( Client客户端/Server服务端 ) 、 BS架构(Browser浏览器/Server服务端)。

无论是CS架构,还是BS架构的软件都必须依赖网络编程!

CS架构

例如微信、IDEA。

Client客户端

  • 需要程序员开发。
  • 用户需要安装。

Server服务端

  • 需要程序员开发实现。

BS架构

例如浏览器、网页。

Browser浏览器

  • 不需要程序员开发实现。
  • 用户需要安装浏览器。

Server服务端

  • 需要程序员开发实现。

网络编程三要素

  1. IP地址:设备在网络中的地址,是唯一的标识。
  2. 端口号:应用程序在设备中唯一的标识。
  3. 协议:连接和数据在网络中传输的规则。

image-20240409203035483

IP地址

设备在网络中的地址,是唯一的标识。

  • IP(Internet Protocol):全称“互联网协议地址”,是分配给上网设备的唯一标志
  • IP地址有两种形式:IPv4、IPv6。
    • IPv4地址是用32bit(4Bytes)来表示,分成4段表示,使用点分十进制表示法来表示。
    • IPv6地址是用共128bit来表示,分成8段表示,使用冒分十六进制表示法来表示。即,每段每四位编码成一个十六进制位表示, 数之间用冒号(:)分开。

IP域名:

例如,https://www.baidu.com/。本机先去DNS服务器查,查到ip地址并返回,如果查不到,就去运营商服务器那里去查。

image-20240410104003448

公网IP、内网IP:

  • 公网IP:是可以连接互联网的IP地址;内网IP:也叫局域网IP,只能组织机构内部使用
  • 192.168. 开头的就是常见的局域网地址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用。

特殊IP地址:

  • 127.0.0.1、localhost:代表本机IP,只会寻找当前所在的主机。

IP常用命令:

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

在Java中的代表类:

  • 代表IP地址。

InetAddress的常用方法:

  1. public static InetAddress getLocalHost()。获取本机IP,会以一个InetAddress的对象返回
  2. public static InetAddress getByName(String host)。根据ip地址或者域名返回一个InetAdress对象
  3. public String getHostName()。获取该ip地址对象对应的主机名
  4. public String getHostAddress()。获取该ip地址对象中的ip地址信息
  5. public boolean isReachable(int timeout)。在指定毫秒内,判断主机与该ip对应的主机是否能连通
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class InetAddressTest {
public static void main(String[] args) throws Exception {
// 1、获取本机IP地址对象的
InetAddress ip1 = InetAddress.getLocalHost();
System.out.println(ip1.getHostName());
System.out.println(ip1.getHostAddress());

// 2、获取指定IP或者域名的IP地址对象。
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());

// ping www.baidu.com
System.out.println(ip2.isReachable(6000));
}
}

端口号

应用程序设备唯一的标识。标记正在计算机设备上运行的应用程序的,被规定为一个 16 位的二进制,范围是 0~65535。

分类

  • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21)
  • 注册端口:1024~49151,分配给用户进程或某些应用程序。(注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。)
  • 动态端口:49152到65535,之所以称为动态端口,是因为它 一般不固定分配某种进程,而是动态分配。

协议

连接和数据在网络中传输的规则。网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。(为了让全球所有的上网设备都能够互联。)

开放式网络互联标准:OSI网络参考模型

  • OSI网络参考模型:全球网络互联标准。
  • TCP/IP网络模型:事实上的国际标准。

image-20240410105817922

传输层的2个通信协议

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

UDP协议

  • 特点:无连接、不可靠通信
  • 不事先建立连接,数据按照包发,一包数据包含:自己的IP、程序端口,目的地IP、程序端口和数据限制在64KB内)等。
  • 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的 。(但是通信效率高,用于语音通话、视频直播。)

TCP协议

  • 特点:面向连接、可靠通信
  • TCP的最终目的:要保证在不可靠的信道上实现可靠的传输。
  • TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接。(通信效率相对不高,用于网页、文件下载、支付。)
  • 可以进行大数据量的传输。

TCP协议:三次握手建立可靠连接

  • 可靠连接:确定通信双方,收发消息都是正常无问题的!(全双工)

image-20240410110231817

  • 传输数据会进行确认,以保证数据传输的可靠性。

TCP协议:四次握手断开连接

  • 目的:确保双方数据的收发都已经完成!

image-20240410110434580

UDP通信代码(入门案例)

  • 特点:无连接、不可靠通信。
  • 不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了。(类似于拿盘子扔韭菜……)
  • Java提供了一个java.net.DatagramSocket类来实现UDP通信。

DatagramSocket: 用于创建客户端、服务端

构造器

  1. public DatagramSocket()。创建客户端的Socket对象, 系统会随机分配一个端口号。
  2. public DatagramSocket(int port)。创建服务端的Socket对象, 并指定端口号。

方法

  1. public void send(DatagramPacket dp)。发送数据包。
  2. public void receive(DatagramPacket p)。使用数据包接收数据。

DatagramPacket:创建数据包

构造器

  1. public DatagramPacket(byte[] buf, int length, InetAddress address, int port)。创建发出去的数据包对象
  2. public DatagramPacket(byte[] buf, int length)。创建用来接收数据的数据包。

方法

  1. public int getLength()。获取数据包,实际接收到的字节个数。

使用UDP通信实现:发送消息、接收消息

客户端实现步骤

  1. 创建DatagramSocket对象(客户端对象) –> 扔韭菜的人。
  2. 创建DatagramPacket对象封装需要发送的数据(数据包对象) –> 韭菜盘子。
  3. 使用DatagramSocket对象的send方法,传入DatagramPacket对象 –> 开始抛出韭菜。
  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
/**
* 目标:完成UDP通信快速入门:实现1发1收。
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建客户端对象(发韭菜出去的人)
DatagramSocket socket = new DatagramSocket(7777);

// 2、创建数据包对象封装要发出去的数据(创建一个韭菜盘子)
/* public DatagramPacket(byte buf[], int length,
InetAddress address, int port)
参数一:封装要发出去的数据。
参数二:发送出去的数据大小(字节个数)
参数三:服务端的IP地址(找到服务端主机)
参数四:服务端程序的端口。
*/
byte[] bytes = "我是快乐的客户端,我爱你abc".getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length
, InetAddress.getLocalHost(), 6666);

// 3、开始正式发送这个数据包的数据出去了
socket.send(packet);

System.out.println("客户端数据发送完毕~~~");
socket.close(); // 释放资源!
}
}

服务端实现步骤

  1. 创建DatagramSocket对象并指定端口(服务端对象) –> 接韭菜的人。
  2. 创建DatagramPacket对象接收数据(数据包对象) –> 韭菜盘子。
  3. 使用DatagramSocket对象的receive方法,传入DatagramPacket对象 –> 开始接收韭菜。
  4. 释放资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1、创建一个服务端对象(创建一个接韭菜的人) 注册端口
DatagramSocket socket = new DatagramSocket(6666);

// 2、创建一个数据包对象,用于接收数据的(创建一个韭菜盘子)
byte[] buffer = new byte[1024 * 64]; // 64KB.
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

// 3、开始正式使用数据包来接收客户端发来的数据
socket.receive(packet);

// 4、从字节数组中,把接收到的数据直接打印出来
// 接收多少就倒出多少
// 获取本次数据包接收了多少数据。
int len = packet.getLength();

String rs = new String(buffer, 0 , len);
System.out.println(rs);

System.out.println(packet.getAddress().getHostAddress());
System.out.println(packet.getPort());

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

UDP通信代码(多发多收)

客户端可以反复发送数据

客户端实现步骤

  • 创建DatagramSocket对象(发送端对象) –> 扔韭菜的人。
  • 使用while死循环不断的接收用户的数据输入,如果用户输入的exit则退出程序。
  • 如果用户输入的不是exit, 把数据封装成DatagramPacket –> 韭菜盘子。
  • 使用DatagramSocket对象的send方法将数据包对象进行发送 –> 开始抛出韭菜。
  • 释放资源。
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
/**
* 目标:完成UDP通信快速入门:实现客户端反复的发。
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建客户端对象(发韭菜出去的人)
DatagramSocket socket = new DatagramSocket();

// 2、创建数据包对象封装要发出去的数据(创建一个韭菜盘子)
/* public DatagramPacket(byte buf[], int length,
InetAddress address, int port)
参数一:封装要发出去的数据。
参数二:发送出去的数据大小(字节个数)
参数三:服务端的IP地址(找到服务端主机)
参数四:服务端程序的端口。
*/
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();

// 一旦发现用户输入的exit命令,就退出客户端
if("exit".equals(msg)){
System.out.println("欢迎下次光临!退出成功!");
socket.close(); // 释放资源
break; // 跳出死循环
}

byte[] bytes = msg.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length
, InetAddress.getLocalHost(), 6666);

// 3、开始正式发送这个数据包的数据出去了
socket.send(packet);
}
}
}

服务端实现步骤

  • 创建DatagramSocket对象并指定端口(接收端对象)–> 接韭菜的人。
  • 创建DatagramPacket对象接收数据(数据包对象) –> 韭菜盘子。
  • 使用DatagramSocket对象的receive方法传入DatagramPacket对象。
  • 使用while死循环不断的进行第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
/**
* 目标:完成UDP通信快速入门-服务端反复的收
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1、创建一个服务端对象(创建一个接韭菜的人) 注册端口
DatagramSocket socket = new DatagramSocket(6666);

// 2、创建一个数据包对象,用于接收数据的(创建一个韭菜盘子)
byte[] buffer = new byte[1024 * 64]; // 64KB.
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

while (true) {
// 3、开始正式使用数据包来接收客户端发来的数据
socket.receive(packet);

// 4、从字节数组中,把接收到的数据直接打印出来
// 接收多少就倒出多少
// 获取本次数据包接收了多少数据。
int len = packet.getLength();

String rs = new String(buffer, 0 , len);
System.out.println(rs);

System.out.println(packet.getAddress().getHostAddress());
System.out.println(packet.getPort());
System.out.println("--------------------------------------");
}
}
}

提问:UDP的接收端为什么可以接收很多发送端的消息?

  • 接收端只负责接收数据包,无所谓是哪个发送端的数据包。

TCP通信(一发一收)

  • 特点:面向连接、可靠通信。
  • 通信双方事先会采用“三次握手”方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。
  • Java提供了一个java.net.Socket类来实现TCP通信。

TCP通信之-客户端开发

  • 客户端程序就是通过java.net包下的Socket类来实现的。

构造器

  1. public Socket(String host , int port)。根据指定的服务器ip、端口号请求与服务端建立连接,连接通过,就获得了客户端socket。

常用方法

  1. public OutputStream getOutputStream()。获得字节输出流对象。(发)
  2. public InputStream getInputStream()。获得字节输入流对象。(收)

客户端发送消息

  1. 创建客户端的Socket对象,请求与服务端的连接。
  2. 使用socket对象调用getOutputStream()方法得到字节输出流。(注意用的是字节流,所以使用的时候可能需要包装一下。)
  3. 使用字节输出流完成数据的发送。
  4. 释放资源:关闭socket管道。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 目标:完成TCP通信快速入门-客户端开发:实现1发1收。
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket对象,并同时请求与服务端程序的连接。
Socket socket = new Socket("127.0.0.1", 8888);

// 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
OutputStream os = socket.getOutputStream();

// 3、把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);

// 4、开始写数据出去了
dos.writeUTF("在一起,好吗?");
dos.close();

socket.close(); // 释放连接资源
}
}

TCP通信-服务端程序的开发

  • 服务端是通过java.net包下的ServerSocket类来实现的。

构造器

  1. public ServerSocket(int port)。为服务端程序注册端口。

常用方法

  1. public Socket accept()。阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端这边的Socket对象。

实现步骤

  1. 创建ServerSocket对象,注册服务端端口。
  2. 调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象。
  3. 通过Socket对象调用getInputStream()方法得到字节输入流、完成数据的接收。
  4. 释放资源:关闭socket管道。
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
/**
* 目标:完成TCP通信快速入门-服务端开发:实现1发1收。
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8888);

// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();

// 3、从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();

// 4、把原始的字节输入流包装成数据输入流
DataInputStream dis = new DataInputStream(is);

// 5、使用数据输入流读取客户端发送过来的消息
String rs = dis.readUTF();
System.out.println(rs);
// 其实我们也可以获取客户端的IP地址
System.out.println(socket.getRemoteSocketAddress());

dis.close();
socket.close();
}
}

TCP通信(多发多收)

  • 客户端使用死循环,让用户不断输入消息。
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
/**
* 目标:完成TCP通信快速入门-客户端开发:实现客户端可以反复的发消息出去
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket对象,并同时请求与服务端程序的连接。
Socket socket = new Socket("127.0.0.1", 8888);

// 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
OutputStream os = socket.getOutputStream();

// 3、把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);

Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();

// 一旦用户输入了exit,就退出客户端程序
if("exit".equals(msg)){
System.out.println("欢迎您下次光临!退出成功!");
dos.close();
socket.close();
break;
}

// 4、开始写数据出去了
dos.writeUTF(msg);
dos.flush();
}
}
}
  • 服务端也使用死循环,控制服务端收完消息,继续等待接收下一个消息。
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
/**
* 目标:完成TCP通信快速入门-服务端开发:实现服务端反复发消息
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8888);

// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();

// 3、从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();

// 4、把原始的字节输入流包装成数据输入流
DataInputStream dis = new DataInputStream(is);

while (true) {
try {
// 5、使用数据输入流读取客户端发送过来的消息
String rs = dis.readUTF();
System.out.println(rs);
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "离线了!");
dis.close();
socket.close();
break;
}
}
}
}

TCP通信(多线程改进)

目前我们开发的服务端程序,可否支持与多个客户端同时通信 ?

  • 不可以的。因为服务端现在只有一个主线程,只能处理一个客户端的消息。

image-20240410150207421

本次是如何实现服务端同时接收多个客户端的消息的?

  • 主线程定义了循环负责接收客户端Socket管道连接
  • 接收到一个Socket通信管道分配一个独立的线程负责处理它。

首先,我们需要写一个服务端的读取数据的线程类,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true){
try {
String msg = dis.readUTF();
System.out.println(msg);

} catch (Exception e) {
System.out.println("有人下线了:" + socket.getRemoteSocketAddress());
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

接下来,再改写服务端的主程序代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 目标:完成TCP通信快速入门-服务端开发:要求实现与多个客户端同时通信。
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8888);

while (true) {
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();

System.out.println("有人上线了:" + socket.getRemoteSocketAddress());

// 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
new ServerReaderThread(socket).start();
}
}
}

拓展案例-【群聊的实现】-思路

客户端和客户端是不能直接通信的。全群聊的效果必须要有服务端在中间做中转。

  • 客户端 —> 服务端。
  • 客户端 —> 多个客户端。

image-20240410150623261

  • 是指一个客户端把消息发出去,其他在线的全部客户端都可以收到消息。
  • 需要用到端口转发的设计思想
  • 服务端需要把在线的Socket管道存储起来,一旦收到一个消息要推送给其他管道。

可以在服务端创建一个存储Socket的集合,每当一个客户端连接服务端,就可以把客户端Socket存储起来;当一个客户端给服务端发消息时,再遍历集合通过每个Socket将消息再转发给其他客户端。

然后改造服务端代码

由于服务端读取数据是在线程类中完成的,所以我们改SerReaderThread类就可以了。服务端的主程序不用改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true){
try {
String msg = dis.readUTF();
System.out.println(msg);
// 把这个消息分发给全部客户端进行接收。
sendMsgToAll(msg);
} catch (Exception e) {
System.out.println("有人下线了:" + socket.getRemoteSocketAddress());
Server.onLineSockets.remove(socket);
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

private void sendMsgToAll(String msg) throws IOException {
// 发送给全部在线的socket管道接收。
for (Socket onLineSocket : Server.onLineSockets) {
OutputStream os = onLineSocket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF(msg);
dos.flush();
}
}
}

BS架构程序(简易版)

案例:

要求从浏览器中访问服务器,并立即让服务器响应一个很简单的网页给浏览器展示,网页内容就是“黑马程序员666”。

BS架构的基本原理

  • 客户端使用浏览器发起请求(不需要开发客户端)。
  • 注意:服务器必须给浏览器响应HTTP协议规定的数据格式,否则浏览器不识别返回的数据。
  • http://服务器IP:服务器端口,例如,http://127.0.0.1:8080。

image-20240410151156205

HTTP协议规定

必须满足如下形式:

image-20240410151235576

注意:数据是由多行组成的,必须按照规定的格式来写。

先写一个线程类,用于按照HTTP协议的格式返回数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
// 立即响应一个网页内容:“黑马程序员”给浏览器展示。
try {
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=UTF-8");
ps.println(); // 必须换行
ps.println("<div style='color:red;font-size:120px;text-align:center'>黑马程序员666<div>");
ps.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

再写服务端的主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 目标:完成TCP通信快速入门-服务端开发:要求实现与多个客户端同时通信。
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8080);

while (true) {
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();

System.out.println("有人上线了:" + socket.getRemoteSocketAddress());

// 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
new ServerReaderThread(socket).start();
}
}
}

拓展

每次请求都开一个新线程,到底好不好?

  • 高并发时,容易宕机!
  • 可以使用线程池进行优化。

image-20240410151505454

为了避免服务端创建太多的线程,可以把服务端用线程池改进,提高服务端的性能。

先写一个给浏览器响应数据的线程任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
// 立即响应一个网页内容:“黑马程序员”给浏览器展示。
try {
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=UTF-8");
ps.println(); // 必须换行
ps.println("<div style='color:red;font-size:120px;text-align:center'>黑马程序员666<div>");
ps.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

再改写服务端的主程序,使用ThreadPoolExecutor创建一个线程池,每次接收到一个Socket就往线程池中提交任务就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8080);

// 创建出一个线程池,负责处理通信管道的任务。
ThreadPoolExecutor pool = new ThreadPoolExecutor(16 * 2, 16 * 2, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8) , Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());

while (true) {
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();

// 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
pool.execute(new ServerReaderRunnable(socket));
}
}
}

【LeeCode】刷题记录.md

  • 作者:力扣官方题解
  • 来源:力扣(LeetCode)

LeeCode热题100

49、字母异位词分组(中)

https://leetcode.cn/problems/group-anagrams/solutions/520469/zi-mu-yi-wei-ci-fen-zu-by-leetcode-solut-gyoc/

题面

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

为什么是哈希表相关的题?

思路:

  • 当把单词中所有字母按照字母顺序表排列时,字母异位词的排序后的单词是相同的。
  • 可以使用相同点作为一组字母异位词的标志,使用哈希表存储每一组字母异位词。
  • 哈希表的为一组字母异位词的标志,哈希表的一组字母异位词列表
  • 具体做法:遍历每个字符串,对于每个字符串,得到该字符串所在的一组字母异位词的标志,将当前字符串加入该组字母异位词的列表中。遍历全部字符串之后,哈希表中的每个键值对即为一组字母异位词。

方法1:字母排序

  1. 构造单词的字符排序,作为键。
  2. 将单词加入散列表。
  3. 返回答案。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
char[] array = str.toCharArray();
Arrays.sort(array);
String key = new String(array);
List<String> list = map.getOrDefault(key, new ArrayList<String>());
list.add(str);
map.put(key, list);
}
return new ArrayList<List<String>>(map.values());
}
}

复习

  • char[] toCharArray() 。将此字符串转换为新的字符数组
  • getOrDefault。HashMap的一个方法,返回指定键映射到的值,如果此映射不包含键的映射,则返回 defaultValue 。
  • 向list中新增元素用add方法。
  • 向哈希表中新增元素用put方法,同时传入键和值。

复杂度分析

image-20240408211642300

方法2:计数

  • 互为字母异位词的两个字符串包含的字母相同,因此两个字符串中的相同字母出现的次数一定是相同的,故可以将每个字母出现的次数使用字符串表示,作为哈希表的键
  • 字符串只包含小写字母,因此对于每个字符串,可以使用长度为 26 的数组记录每个字母出现的次数。
  • 首先统计字符的出现顺序,然后构造键,把具有相通特征的字符串的单词们放在一组,最后返回结果。
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
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
int[] counts = new int[26];
int length = str.length();
for (int i = 0; i < length; i++) {
counts[str.charAt(i) - 'a']++;
}
// 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串,作为哈希表的键
StringBuffer sb = new StringBuffer(); //可变字符串对象
for (int i = 0; i < 26; i++) {
if (counts[i] != 0) {
sb.append((char) ('a' + i));
sb.append(counts[i]);
}
}
String key = sb.toString();
List<String> list = map.getOrDefault(key, new ArrayList<String>());
list.add(str);
map.put(key, list);
}
return new ArrayList<List<String>>(map.values());
}
}

复杂度分析

image-20240408211900938

面试要点

  • 通过分析,能否意识到单词和键的映射关系。
  • 利用散列表高效储存结果。
  • 数据结构和常见的库函数。

【Java基础】特殊文本文件、日志技术

特殊文件概述

认识

  • 普通文件.txt。
  • 属性文件.properties。
  • XML文件.xml。

为什么要用特殊文件?

  • 存储多个用户的用户名和密码,txt不可以解析,但是properties、xml可以。
  • 存储多个用户的用户名、密码、家乡和性别,txt、properties不可以解析,xml可以。
  • 可以存储有关系的数据,作为系统的配置文件作为信息进行传输

我们主要学什么?

  • 了解它们的特点、作用。
  • 学习使用程序读取它们里面的数据。
  • 学习使用程序把数据存储到这些文件里。(用得不多。)

日志技术

把程序运行的信息,记录到文件中,方便程序员定位bug、并了解程序的执行情况等。

特殊文件:properties属性文件

  • 是一个Map集合键值对集合),但是我们一般不会当集合使用
  • 核心作用:Properties是用来代表属性文件的,通过Properties可以读写属性文件里的键值对。

(读)构造器

  1. public Properties()。用于构建Properties集合对象(空容器)。

(读)常用方法

  1. public void load(InputStream is)。通过字节输入流,读取属性文件里的键值对数据。
  2. public void load(Reader reader)。通过字符输入流,读取属性文件里的键值对数据。
  3. public String getProperty(String key)。根据键获取值(其实就是get方法的效果)。
  4. public Set stringPropertyNames()。获取全部键的集合(其实就是ketSet方法的效果)。

读取步骤

1
2
3
1、创建一个Properties的对象出来(键值对集合,空容器)
2、调用load(字符输入流/字节输入流)方法,开始加载属性文件中的键值对数据到properties对象中去
3、调用getProperty(键)方法,根据键取值

代码

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
/**
* 目标:掌握使用Properties类读取属性文件中的键值对信息。
*/
public class PropertiesTest1 {
public static void main(String[] args) throws Exception {
// 1、创建一个Properties的对象出来(键值对集合,空容器)
Properties properties = new Properties();
System.out.println(properties);

// 2、开始加载属性文件中的键值对数据到properties对象中去
properties.load(new FileReader("properties-xml-log-app\\src\\users.properties"));
System.out.println(properties);

// 3、根据键取值
System.out.println(properties.getProperty("赵敏"));
System.out.println(properties.getProperty("张无忌"));

// 4、遍历全部的键和值。
//获取键的集合
Set<String> keys = properties.stringPropertyNames();
for (String key : keys) {
//再根据键获取值
String value = properties.getProperty(key);
System.out.println(key + "---->" + value);
}

properties.forEach((k, v) -> {
System.out.println(k + "---->" + v);
});
}
}

(写)构造器

  1. public Properties()。用于构建Properties集合对象(空容器)。

(写)常用方法

  1. public Object setProperty(String key, String value)。保存键值对数据到Properties对象中去。
  2. public void store(OutputStream os, String comments)。把键值对数据,通过字节输出流写出到属性文件里去。注意这里需要写评论信息。
  3. public void store(Writer w, String comments)。把键值对数据,通过字符输出流写出到属性文件里去。

步骤

1
2
3
4
5
1、先准备一个.properties属性文件,按照格式写几个键值对
1、创建Properties对象出来,
2、调用setProperty存储一些键值对数据
3、调用store(字符输出流/字节输出流, 注释),将Properties集合中的键和值写到文件中
注意:第二个参数是注释,必须得加;

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PropertiesTest2 {
public static void main(String[] args) throws Exception {
// 1、创建Properties对象出来,先用它存储一些键值对数据
Properties properties = new Properties();
properties.setProperty("张无忌", "minmin");
properties.setProperty("殷素素", "cuishan");
properties.setProperty("张翠山", "susu");

// 2、把properties对象中的键值对数据存入到属性文件中去
properties.store(new FileWriter("properties-xml-log-app/src/users2.properties")
, "i saved many users!");

}
}

特殊文件:XML文件

概述

是什么?

  • XML( 全称EXtensible Markup Language, 可扩展标记语言 )。
  • 本质是一种数据的格式,可以用来存储复杂的数据结构,和数据关系。

特点

  • XML中的“<标签名>” 称为一个标签或一个元素,一般是成对出现的。
  • XML中的标签名可以自己定义(可扩展),但必须要正确的嵌套
  • XML中只能有一个根标签
  • XML中的标签可以有属性
  • 如果一个文件中放置的是XML格式的数据,这个文件就是XML文件,后缀一般要写成**.xml**。

XML的创建

  • 就是创建一个XML类型的文件,要求文件的后缀必须使用xml,如hello_world.xml。

语法规则

  • (1)XML文件的后缀名为:xml,文档声明(抬头)必须是第一行
1
2
3
<?xml version="1.0" encoding="UTF-8" > //自动
version:XML默认的版本号码、该属性是必须存在
encoding:本XML文件的编码
  • (2)XML中可以定义注释信息:

  • (3)根标签有且只能有一个。下面可以写子标签,子标签里面可以存属性,例如id=”1”。再往下可以继续写子标签。

  • (4)标签必须承成对出现,又开始有结束:

  • (5)必须能够正确的嵌套。

  • XML中书写“<”、“&”等,可能会出现冲突,导致报错,此时可以用如下特殊字符替代。注意,XML可以在浏览器中直接渲染,在IDEA里面右键-open in expolrer(将这些特殊字符转换完成)。

1
2
3
4
5
6
&lt;    <  小于
&gt; > 大于
&amp; & 和号
&apos; ' 单引号
&quot; " 引号
例如:<data> 3 &lt; 2 &amp;&amp; 5 &gt; 4 </data>
  • XML中可以写一个叫CDATA的特殊数据区: 里面的内容可以随便写
1
2
3
4
5
<data1>
<![CDATA[
3 < 2 && 5 > 4
]]>
</data1>

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 注释:以上抬头声明必须放在第一行,必须有 -->
<!-- 根标签只能有一个 -->
<users>
<user id="1" desc="第一个用户">
<name>张无忌</name>
<sex></sex>
<地址>光明顶</地址>
<password>minmin</password>
</user>
<people>很多人</people>
<user id="2">
<name>敏敏</name>
<sex></sex>
<地址>光明顶</地址>
<password>wuji</password>
</user>
</users>

作用和应用场景

  • 本质是一种数据格式,可以存储复杂的数据结构,和数据关系。
  • 应用场景:经常用来做为系统的配置文件;或者作为一种特殊的数据结构,在网络中进行传输(现在用得不多但是也能作为一种方案)。

读取XML文件中的数据

又叫,解析XML文件。使用程序读取XML文件中的数据。

  • 注意:程序员并不需要自己写原始的IO流代码来解析XML,难度较大!也相当繁琐!
  • 其实,有很多开源的,好用的,解析XML的框架,最知名的是:Dom4j(第三方研发的)。

Dom4j下载

  1. 下载Dom4j框架,官网下载。
  2. 在项目中创建一个文件夹:lib。
  3. 将dom4j-2.1.3.jar文件复制到 lib 文件夹。
  4. 在jar文件上点右键,选择 Add as Library -> 点击OK。
  5. 在类中导包使用。

DOM4J解析XML文件的思想:文档对象模型

思想

  1. 首先用SAXReader解析器把文件内容一下子加载到内存中,变成一个Document(整个文档)对象。
  2. 通过Document对象就可以获取Element对象:元素(标签)。
  3. Element又可以获取Attribute:属性、子元素、文本。
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
<?xml version="1.0" encoding="UTF-8" ?>
<users>
<user id="1">
<name>张无忌</name>
<sex></sex>
<地址>光明顶</地址>
<password>minmin</password>
<data> 3 &lt; 2 &amp;&amp; 5 > 4 </data>
<data1>
<![CDATA[
3 < 2 && 5 > 4
]]>
</data1>
</user>
<user id="2">
<name>敏敏</name>
<sex></sex>
<地址>光明顶</地址>
<password>wuji</password>
<data> 3 &lt; 2 &amp;&amp; 5 > 4 </data>
<data1>
<![CDATA[
3 < 2 && 5 > 4
]]>
</data1>
</user>
</users>

构造器

  1. public SAXReader()。构建Dom4J的解析器对象。Dom4j提供的解析器,可以认为是代表整个Dom4j框架。
  2. public Document read(String url)。把XML文件读成Document对象。
  3. public Document read(InputStream is)。通过字节输入流读取XML文件。

常用方法

  1. Element getRootElement()。获得根元素对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Dom4JTest1 {
public static void main(String[] args) throws Exception {
// 1、创建一个Dom4J框架提供的解析器对象
SAXReader saxReader = new SAXReader();

// 2、使用saxReader对象把需要解析的XML文件读成一个Document对象。
Document document =
saxReader.read("properties-xml-log-app\\src\\helloworld.xml");

// 3、从文档对象中解析XML文件的全部数据了
Element root = document.getRootElement(); //获取了根元素
System.out.println(root.getName());
}
}

Element提供的方法

注意,只能从根元素一级一级向下解析

  1. public String getName()。得到元素名字。
  2. public List elements()。得到当前元素下所有子元素。
  3. public List elements(String name)。得到当前元素下指定名字的子元素返回集合。
  4. public Element element(String name)。得到当前元素下指定名字的子元素,如果有很多名字相同的返回第一个
  5. public String attributeValue(String name)。通过属性名直接得到属性值。
  6. public String elementText(子元素名)。得到指定名称的子元素的文本。
  7. public String getText()。得到文本。
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 Dom4JTest1 {
public static void main(String[] args) throws Exception {
// 1、创建一个Dom4J框架提供的解析器对象
SAXReader saxReader = new SAXReader();

// 2、使用saxReader对象把需要解析的XML文件读成一个Document对象。
Document document =
saxReader.read("properties-xml-log-app\\src\\helloworld.xml");

// 3、从文档对象中解析XML文件的全部数据了
Element root = document.getRootElement();
System.out.println(root.getName());

// 4、获取根元素下的全部一级子元素。
// List<Element> elements = root.elements();
List<Element> elements = root.elements("user");
for (Element element : elements) {
System.out.println(element.getName());
}

// 5、获取当前元素下的某个子元素。
Element people = root.element("people");
System.out.println(people.getText());

// 如果下面有很多子元素user,默认获取第一个。
Element user = root.element("user");
System.out.println(user.elementText("name"));

// 6、获取元素的属性信息呢?
System.out.println(user.attributeValue("id"));
Attribute id = user.attribute("id");
System.out.println(id.getName());
System.out.println(id.getValue());

List<Attribute> attributes = user.attributes();
for (Attribute attribute : attributes) {
System.out.println(attribute.getName() + "=" + attribute.getValue());
}

// 7、如何获取全部的文本内容:获取当前元素下的子元素文本值
System.out.println(user.elementText("name"));
System.out.println(user.elementText("地址"));
System.out.println(user.elementTextTrim("地址")); // 取出文本去除前后空格
System.out.println(user.elementText("password"));

Element data = user.element("data");
System.out.println(data.getText());
System.out.println(data.getTextTrim()); // 取出文本去除前后空格
}
}

如何往XML文件中写入数据?

  • DOM4J也提供了往XML文件中写标签的方法,但是用起来比较麻烦,不好维护。这里不建议使用。
  • 我们自己使用StringBuilder按照标签的格式拼接,然后再使用BufferedWriter写到XML文件中去就可以了。更加灵活。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Dom4JTest2 {
public static void main(String[] args) {
// 1、使用一个StringBuilder对象来拼接XML格式的数据。
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\r\n");
sb.append("<book>\r\n");
sb.append("\t<name>").append("从入门到跑路").append("</name>\r\n");
sb.append("\t<author>").append("dlei").append("</author>\r\n");
sb.append("\t<price>").append(999.99).append("</price>\r\n");
sb.append("</book>");

try (
BufferedWriter bw = new BufferedWriter(new FileWriter("properties-xml-log-app/src/book.xml"));
){
bw.write(sb.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}

补充知识:约束XML文件的编写[了解]

限制XML文件中的标签或者属性,只能按照规定的格式写。

专门用来限制xml书写格式的文档,比如:限制标签、属性应该怎么写。有两种约束技术:

  1. DTD约束文档。(能够约束具体的数据类型。)
  2. Schame约束文档。(能够约束具体的数据类型。)

案例:利用DTD约束文档,约束一个XML文件的编写。(了解)

1
2
3
1.编写DTD约束文档,后缀必须是.dtd
2.在需要编写的XML文件中导入该DTD约束文档(<!DOCTYPE 书架 SYSTEM "data.dtd">)
3.然后XML文件,就必须按照DTD约束文档指定的格式进行编写,否则报错!

代码(了解)

1
2
3
4
5
<!ELEMENT 书架 (书+)>
<!ELEMENT 书 (书名,作者,售价)>
<!ELEMENT 书名 (#PCDATA)>
<!ELEMENT 作者 (#PCDATA)>
<!ELEMENT 售价 (#PCDATA)>

案例:利用schema约束文档,约束一个XML文件的编写。(了解)

步骤(了解)

1
2
3
1.编写schema约束文档,后缀必须是.xsd,具体的形式到代码中观看。
2.在需要编写的XML文件中导入该schema约束文档
3.按照约束内容编写XML文件的标签。

日志技术

概述

场景引入

  • 希望系统能记住某些数据是被谁操作的,比如被谁删除了?
  • 想分析用户浏览系统的具体情况,以便挖掘用户的具体喜好?
  • 当系统在开发中或者上线后出现了bug,崩溃了,该通过什么去分析、定位bug?

日志

  • 好比生活中的日记,可以记录你生活中的点点滴滴。
  • 程序中的日志,通常就是一个文件,里面记录的是程序运行过程中的各种信息

输出语句的弊端

  • 日志会展示在控制台。
  • 不能更方便的将日志记录到其他的位置(文件,数据库)。
  • 想取消日志,需要修改源代码才可以完成。

日志技术

  • 可以将系统执行的信息,方便的记录到指定的位置(控制台、文件中、数据库中)。
  • 可以随时以开关的形式控制日志的启停,无需侵入到源代码中去进行修改。
输出语句 日志技术
输出位置 输出到控制台 可以将日志信息写入到文件或者数据库中
取消日志 需要修改代码,灵活性比较差 不需要修改代码,灵活性比较好
多线程 性能较差 性能较好

体系结构

日志框架

  1. JUL(java.util.loggiing)。
  2. Log4j。
  3. Logback。
  4. 其他实现。

日志接口

为了减轻负担,设置接口让大家都遵守。

  1. Commons Logging (JCL)。
  2. Simple Logging Facade for Java (SLF4J)。
  • 日志框架:牛人或者第三方公司已经做好的实现代码,后来者直接可以拿去使用。
  • 日志接口:设计日志框架的一套标准,日志框架需要实现这些接口。
  • 注意1:因为对Commons Logging接口不满意,有人就搞了SLF4J;因为对Log4j的性能不满意,有人就搞了Logback。
  • 注意2:Logback是基于slf4j的日志规范实现的框架

Logback日志框架

网站:https://logback.qos.ch/index.html

Logback日志框架有以下几个模块:

  1. logback-core。基础模块,是其他两个模块依赖的基础(必须有)。
  2. logback-classic。功能模块,完整实现了slf4j API的模块(必须有)。
  3. logback-access。与 Tomcat 和 Jetty 等 Servlet 容器集成,以提供 HTTP 访问日志的功能(可选,以后再接触)。

想使用Logback日志框架,至少需要在项目中整合如下三个模块:

  1. slf4j-api。日志接口。
  2. logback-core。基础模块。
  3. logback-classic。功能模块。

Logback入门

需求:使用Logback日志框架,纪录系统的运行信息。

实现步骤

1
2
3
4
5
1.导入Logback框架到项目中去。
2.将Logback框架的核心配置文件logback.xml直接拷贝到src目录下(必须是src下)。
3.创建Logback框架提供的Logger对象,然后用Logger对象调用其提供的方法就可以记录系统的日志信息。
创建一个日志记录日对象:
public static final Logger LOGGER = LoggerFactory.getLogger(“类名");

具体步骤

1
2
3
4
1.在项目下新建文件夹lib,导入Logback的相关jar包到该文件夹下,并添加到项目库中去。
2.必须将Logback的核心配置文件logback.xml直接拷贝到src目录下。
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
public class LogBackTest {
// 创建一个Logger日志对象
public static final Logger LOGGER = LoggerFactory.getLogger("LogBackTest");

public static void main(String[] args) {
//while (true) {
try {
LOGGER.info("chu法方法开始执行~~~");
chu(10, 0);
LOGGER.info("chu法方法执行成功~~~");
} catch (Exception e) {
LOGGER.error("chu法方法执行失败了,出现了bug~~~");
}
//}
}

public static void chu(int a, int b){
LOGGER.debug("参数a:" + a);
LOGGER.debug("参数b:" + b);
int c = a / b;
LOGGER.info("结果是:" + c);
}
}

Logback.xml日志配置文件

核心配置文件logback.xml,对Logback日志框架进行控制的。在记录日志时会读取配置文件中的配置信息,从而记录日志的形式。

具体可以做哪些配置

1
2
3
4
5
6
7
8
9
1. 可以配置日志输出的位置是文件、还是控制台。
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
2. 可以配置日志输出的格式。
3. 还可以配置日志关闭和开启、以及哪些日志输出哪些日志不输出。// 设置为 “ALL”或“OFF”。//设置只输出到配置文件或者控制台,可以删掉其中一行。
<root level="ALL">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE" />
</root>
  • 如下图所示,控制日志往文件中输出,还是往控制台输出**

1668045955362

  • 如下图所示,控制打开和关闭日志

1668046078574

  • 如下图所示,控制日志的输出的格式

    日志格式是由一些特殊的符号组成,可以根据需要删减不想看到的部分。比如不想看到线程名那就不要[%thread]。但是不建议同学们更改这些格式,因为这些都是日志很基本的信息。

1668046216355

Logback设置日志级别【重要】

认识日志级别

日志级别指的是日志信息的类型,日志都会分级别,常见的日志级别如下(优先级依次升高):

日志级别 说明
trace 追踪,指明程序运行轨迹
debug 调试,实际应用中一般将其作为最低级别,而 trace 则很少使用
info 输出重要的运行信息,数据连接、网络连接、IO操作等等,使用较多
warn 警告信息,可能会发生问题,使用较多
error 错误信息, 使用较多

日志级别的作用

用于控制系统中哪些日志级别是可以输出的

如何设置日志级别

可以在配置文件中书写日志级别:

1
2
3
4
<root level=“info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE" />
</root>

只有日志的级别是大于或等于核心配置文件配置的日志级别,才会被记录,否则不记录。

【Java基础】java.io包下的常用API-io流

File类

之前学习的变量、数组、对象和集合都是内存中的数据容器,它们记住的数据在断电或者程序终止时会丢失。

如果想长期保存数据,应该怎么做?

  • 文件是非常重要的存储方式,在计算机硬盘中。
  • 即便断电,或者程序终止了,存储在硬盘文件中的数据也不会丢失。

(1)File类

File是java.io.包下的类, File类的对象,用于代表当前操作系统的文件(可以是文件、或文件夹)。

常见功能

  • 获取文件信息(大小,文件名,修改时间)。
  • 创建文件/文件夹。
  • 删除文件/文件夹。
  • 判断文件的类型。
  • ……

注意:File类只能对文件本身进行操作,不能读写文件里面存储的数据

(2)IO流

用于读写数据的(可以读写文件,或网络中的数据…)。

创建对象

  1. public File(String pathname)。根据文件路径创建文件对象。(最重要!)
  2. public File(String parent, String child)。根据父路径和子路径名字创建文件对象。
  3. public File(File parent, String child)。根据父路径对应文件对象和子路径名字创建文件对象。
  • File对象既可以代表文件、也可以代表文件夹
  • File封装的对象仅仅是一个路径名,这个路径可以是存在的,也允许是不存在的。

路径分隔符

  1. /(推荐
  2. \\
  3. File.separator

绝对路径

  • 从盘符开始。
  • File file1 = new File(“D:\itheima\a.txt”);

相对路径(开发规范

  • 不带盘符,默认直接到当前工程下的目录寻找文件。
  • File file3 = new File(“模块名\a.txt”);

常用方法1:判断文件类型、获取文件信息

  1. public boolean exists()。判断当前文件对象,对应的文件路径是否存在,存在返回true。
  2. public boolean isFile()。判断当前文件对象指代的是否是文件,是文件返回true,反之。
  3. public boolean isDirectory()。判断当前文件对象指代的是否是文件夹,是文件夹返回true,反之。
  4. public String getName()。获取文件的名称(包含后缀)。
  5. public long length()。获取文件的大小,返回字节个数。(然后可以转换成SimpleDateFormat并用format格式化。)
  6. public long lastModified()。获取文件的最后修改时间。
  7. public String getPath()。获取创建文件对象时,使用的路径。
  8. public String getAbsolutePath()。获取绝对路径。

常用方法2:创建文件、删除文件

创建文件

  1. public boolean createNewFile()。创建一个新的空的文件。(程序有可能担心我们乱写盘符,因此报异常,直接alt+enter抛出去就行。)
  2. public boolean mkdir()。只能创建一级文件夹。
  3. public boolean mkdirs()。可以创建多级文件夹。(强大!)

删除文件

  1. public boolean delete()。删除文件、空文件夹。

注意:delete方法默认只能删除文件和空文件夹,删除后的文件不会进入回收站

常用方法3:遍历文件夹

  1. public String[] list()。获取当前目录下所有的”一级文件名称“到一个字符串数组中去返回。
  2. public File[] listFiles()。获取当前目录下所有的”一级文件对象“到一个文件对象数组中去返回(重点)。

注意事项:

  1. 当主调是文件,或者路径不存在时,返回null。
  2. 当主调是空文件夹时,返回一个长度为0的数组。
  3. 当主调是一个有内容的文件夹时,将里面所有一级文件和文件夹的路径放在File数组中返回。
  4. 当主调是一个文件夹,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏文件。
  5. 当主调是一个文件夹,但是没有权限访问该文件夹时,返回null。

前置:方法递归

为了实现文件的多级操作。

是什么?

  • 递归是一种算法,在程序设计语言中广泛应用。
  • 从形式上说:方法调用自身的形式称为方法递归( recursion)。

形式

  • 直接递归:方法自己调用自己。(比较常见。)
  • 间接递归:方法调用其他方法,其他方法又回调方法自己。

注意

  • 递归如果没有控制好终止,会出现递归死循环,导致栈内存溢出错误
  • 递归调用的特点是:一层一层调用,再一层一层往回返。

递归算法三要素

以求阶乘为例。

  1. 递归的公式: f(n) = f(n-1) * n;
  2. 递归的终结点:f(1) 。
  3. 递归的方向必须走向终结点。

递归文件搜索

莫得公式,怎么办?

案例:在D:\\判断下搜索QQ.exe这个文件,然后直接输出。

1
2
3
4
5
6
1.先调用文件夹的listFiles方法,获取文件夹的一级内容,得到一个数组
2.然后再遍历数组,获取数组中的File对象
3.因为File对象可能是文件也可能是文件夹,所以接下来就需要判断
判断File对象如果是文件,就获取文件名,如果文件名是`QQ.exe`则打印,否则不打印
判断File对象如果是文件夹,就递归执行1,2,3步骤
所以:把12,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
/**
* 目标:掌握文件搜索的实现。
*/
public class RecursionTest3 {
public static void main(String[] args) throws Exception {
searchFile(new File("D:/") , "QQ.exe");
}

/**
* 去目录下搜索某个文件
* @param dir 目录
* @param fileName 要搜索的文件名称
*/
public static void searchFile(File dir, String fileName) throws Exception {
// 1、把非法的情况都拦截住
if(dir == null || !dir.exists() || dir.isFile()){
return; // 代表无法搜索
}

// 2、dir不是null,存在,一定是目录对象。
// 获取当前目录下的全部一级文件对象。
File[] files = dir.listFiles();

// 3、判断当前目录下是否存在一级文件对象,以及是否可以拿到一级文件对象。
if(files != null && files.length > 0){
// 4、遍历全部一级文件对象。
for (File f : files) {
// 5、判断文件是否是文件,还是文件夹
if(f.isFile()){
// 是文件,判断这个文件名是否是我们要找的
if(f.getName().contains(fileName)){
System.out.println("找到了:" + f.getAbsolutePath());
Runtime runtime = Runtime.getRuntime();
runtime.exec(f.getAbsolutePath());
}
}else {
// 是文件夹,继续重复这个过程(递归)
searchFile(f, fileName);
}
}
}
}
}

案例:删除一个非空文件夹。

1
2
1、File默认不可以删除非空文件夹
2、我们需要遍历文件夹,先删除里面的内容,再删除自己。

代码:

得空补上。

前置:字符集

常见字符集

标准ASCII字符集

  • ASCII(American Standard Code for Information Interchange): 美国信息交换标准代码,包括了英文、数字、符号等。
  • 标准ASCII使用1个字节存储一个字符,0~127作为码点,首位是0,总共可表示128个字符,对美国佬来说完全够用。

GBK(汉字内码扩展规范,国标)

  • 汉字编码字符集,包含了2万多个汉字等字符,GBK中一个中文字符编码成两个字节的形式存储。
  • 注意:GBK兼容了ASCII字符集。GBK规定,汉字的第一个字节的第一位必须是1。

Unicode字符集(统一码,也叫万国码)

  • Unicode是国际组织制定的,可以容纳世界上所有文字、符号的字符集。
  • 提供多重编码方案。最早是UTF-32,4个字节表示一个字符,但是占内存太大,通信效率变低。

UTF-8

  • 是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节。
  • 英文字符、数字等只占1个字节(兼容标准ASCII编码),汉字字符占用3个字节。
  • 注意:技术人员在开发时都应该使用UTF-8编码
  • UTF-8编码方式(二进制)
    • 0xxxxxxx (ASCII码)
    • 110xxxxx 10xxxxxx
    • 1110xxxx 10xxxxxx 10xxxxxx
    • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

注意事项

  • 注意1:字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码
  • 注意2:英文、数字一般不会乱码,因为很多字符集都兼容了ASCII编码。

字符集的编码、解码操作

String类下的方法和构造器。

编码:把字符按照指定字符集编码成字节。

  1. byte[] getBytes()。使用平台的默认字符集将该String编码为一系列字节,将结果存储到新的字节数组中。
  2. byte[] getBytes(String charsetName)。使用指定的字符集将该 String编码为一系列字节,将结果存储到新的字节数组中 。

解码:把字节按照指定字符集解码成字符。

  1. String(byte[] bytes)。通过使用平台的默认字符集解码指定的字节数组来构造新的String。
  2. String(byte[] bytes, String charsetName)。通过指定的字符集解码指定的字节数组来构造新的String。

IO流(字节流)

IO流概述:输入输出流,用来读写数据的。

  • I指Input,称为输入流:负责把数据读到内存中去。
  • O指Output,称为输出流:负责写数据出去。

怎么学?

  1. 先搞清楚IO流的分类、体系。
  2. 再挨个学习每个IO流的作用、用法。

分类

  • 按照流的方向,分为输入流、输出流。
  • 按照数据的最小单位,分为字节流(适合操作所有类型的文件)、字符流(只适合操作纯文本文件)。
  • –>所以一共是4大基础流:
    1. 字节输入流。以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流。
    2. 字节输出流。以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流。
    3. 字符输入流。以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流。
    4. 字符输出流。以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中去的流。

IO流的体系

  • java.io包下:
    • 字节流–>字节输入流(InputStream)、字节输出流(OutputStream);
    • 字符流–>字符输入流(Reader)、字符输出流(Writer)。
  • 上述都是抽象类。然后对应有实现类,是在前面加个File:
    • 字节流–>字节输入流(FileInputStream)、字节输出流(FileOutputStream);
    • 字符流–>字符输入流(FileReader)、字符输出流(FileWriter)。

FileInputStream文件字节输入流

以内存为基准,可以把磁盘文件中的数据以字节的形式读入到内存中去。

构造器

  1. public FileInputStream(File file)。创建字节输入流管道与源文件接通。
  2. public FileInputStream(String pathname)。创建字节输入流管道与源文件接通。(是1的简化写法,也是推荐写法,系统里帮忙创建了File类。)

常用方法

  1. public int read()。每次读取一个字节返回,如果发现没有数据可读会返回-1。返回值表示当前这一次读取的字节个数。
  2. public int read(byte[] buffer)。每次用一个字节数组去读取数据,返回字节数组读取了多少个字节,如果发现没有数据可读会返回-1。

FileInputStream读取一个字节

使用FileInputStream读取文件中的字节数据,步骤如下:

1
2
3
1.创建FileInputStream文件字节输入流管道,与源文件接通。
2.调用read()方法开始读取文件的字节数据。
3.调用close()方法释放资源

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 目标:掌握文件字节输入流,每次读取一个字节。
*/
public class FileInputStreamTest1 {
public static void main(String[] args) throws Exception {
// 1、创建文件字节输入流管道,与源文件接通。
InputStream is = new FileInputStream(("file-io-app\\src\\itheima01.txt"));

// 2、开始读取文件的字节数据。
// public int read():每次读取一个字节返回,如果没有数据了,返回-1.
int b; // 用于记住读取的字节。
while ((b = is.read()) != -1){
System.out.print((char) b);
}

//3、流使用完毕之后,必须关闭!释放系统资源!
is.close();
}
}

存在的问题:

  1. 这种方法读取数据的性能很差(从系统资源里面调)。(开发规范:尽量减少从硬件内存中读取数据的频次。)
  2. 读取汉字会乱码。一个中文在UTF-8编码方案中是占3个字节,采用一次读取一个字节的方式,读一个字节就相当于读了1/3个汉字,此时将这个字节转换为字符,会有乱码。

FileInputStream读取多个字节

为了提高效率,我们可以使用另一个read(byte[] bytes)的重载方法,可以一次读取多个字节,至于一次读多少个字节,就在于你传递的数组有多大。

使用FileInputStream一次读取多个字节的步骤如下:

1
2
3
1.创建FileInputStream文件字节输入流管道,与源文件接通。
2.调用read(byte[] bytes)方法开始读取文件的字节数据。
3.调用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
/**
* 目标:掌握使用FileInputStream每次读取多个字节。
*/
public class FileInputStreamTest2 {
public static void main(String[] args) throws Exception {
// 1、创建一个字节输入流对象代表字节输入流管道与源文件接通。
InputStream is = new FileInputStream("file-io-app\\src\\itheima02.txt");

// 2、开始读取文件中的字节数据:每次读取多个字节。
// public int read(byte b[]) throws IOException
// 每次读取多个字节到字节数组中去,返回读取的字节数量,读取完毕会返回-1.

// 3、使用循环改造。
byte[] buffer = new byte[3];
int len; // 记住每次读取了多少个字节。 abc 66
while ((len = is.read(buffer)) != -1){
// 注意:读取多少,倒出多少。(否则填不满的位置还是存放之前的内容)
String rs = new String(buffer, 0 , len);
System.out.print(rs);
}
// 性能得到了明显的提升!!
// 这种方案也不能避免读取汉字输出乱码的问题!!

is.close(); // 关闭流
}
}

存在的问题:

  1. 使用FileInputStream每次读取多个字节,读取性能得到了提升。
  2. 但读取汉字输出还是会乱码。

使用字节流读取中文,如何保证输出不乱码,怎么解决?

  • 方式1:自己定义一个字节数组与被读取的文件大小一样大,然后使用该字节数组,一次读完文件的全部字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1、一次性读取完文件的全部字节到一个字节数组中去。
// 创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("file-io-app\\src\\itheima03.txt");

// 2、准备一个字节数组,大小与文件的大小正好一样大。
File f = new File("file-io-app\\src\\itheima03.txt");
long size = f.length();
byte[] buffer = new byte[(int) size];

int len = is.read(buffer);
System.out.println(new String(buffer));

//3、关闭流
is.close();
  • 方式2:Java官方为InputStream提供了如下方法,可以直接把文件的全部字节读取到一个字节数组中返回。///// public byte[] readAllBytes() throws IOException。直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回。
1
2
3
4
5
6
7
8
9
10
// 1、一次性读取完文件的全部字节到一个字节数组中去。
// 创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("file-io-app\\src\\itheima03.txt");

//2、调用方法读取所有字节,返回一个存储所有字节的字节数组。
byte[] buffer = is.readAllBytes();
System.out.println(new String(buffer));

//3、关闭流
is.close();

注意:一次读取所有字节虽然可以解决乱码问题,但是文件不能过大,如果文件过大,可能导致内存溢出。

—>读写文本内容更适合用字符流。字节流适合做数据的转移,例如,文件复制等。

FileOutputStream文件字节输出流

作用:以内存为基准,把内存中的数据以字节的形式写出到文件中去。

构造器

  1. public FileOutputStream(File file)。创建字节输出流管道与源文件对象接通。
  2. public FileOutputStream(String filepath)。创建字节输出流管道与源文件路径接通。
  3. public FileOutputStream(File file,boolean append)。创建字节输出流管道与源文件对象接通,可追加数据
  4. public FileOutputStream(String filepath,boolean append)。创建字节输出流管道与源文件路径接通,可追加数据

常用方法

  1. public void write(int a)。写一个字节出去。
  2. public void write(byte[] buffer)。写一个字节数组出去。
  3. public void write(byte[] buffer , int pos , int len)。写一个字节数组的一部分出去。
  4. public void close() throws IOException。关闭流。

FileOutputStream往文件中写数据的步骤如下:

1
2
3
1.创建FileOutputStream文件字节输出流管道,与目标文件接通。
2.调用wirte()方法往文件中写数据
3.调用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
29
30
/**
* 目标:掌握文件字节输出流FileOutputStream的使用。
*/
public class FileOutputStreamTest4 {
public static void main(String[] args) throws Exception {
// 1、创建一个字节输出流管道与目标文件接通。
// 覆盖管道:覆盖之前的数据
// OutputStream os =
// new FileOutputStream("file-io-app/src/itheima04out.txt");

// 追加数据的管道
OutputStream os =
new FileOutputStream("file-io-app/src/itheima04out.txt", true);

// 2、开始写字节数据出去了
os.write(97); // 97就是一个字节,代表a
os.write('b'); // 'b'也是一个字节
// os.write('磊'); // [ooo] 默认只能写出去一个字节

byte[] bytes = "我爱你中国abc".getBytes();
os.write(bytes);

os.write(bytes, 0, 15);

// 换行符
os.write("\r\n".getBytes());

os.close(); // 关闭流
}
}

案例:字节流复制文件

需求:要复制一张图片,从磁盘D:/resource/meinv.png的一个位置,复制到C:/data/meinv.png位置。

思路:源路径–(创建字节输入流管道)->内存(字节数组)–(创建字节输出流管道)->新路径。

1
2
3
1.需要创建一个FileInputStream流与源文件接通,创建FileOutputStream与目标文件接通
2.然后创建一个数组,使用FileInputStream每次读取一个字节数组的数据,存如数组中
3.然后再使用FileOutputStream把字节数组中的有效元素,写入到目标文件中

代码:

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
/**
* 目标:使用字节流完成对文件的复制操作。
*/
public class CopyTest5 {
public static void main(String[] args) throws Exception {
// 需求:复制照片。
// 1、创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("D:/resource/meinv.png");
// 2、创建一个字节输出流管道与目标文件接通。
OutputStream os = new FileOutputStream("C:/data/meinv.png");

System.out.println(10 / 0);
// 3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024]; // 1KB.
// 4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
int len; // 记住每次读取了多少个字节。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}

os.close();
is.close();
System.out.println("复制完成!!");
}
}

总结:字节流非常适合做一切文件的复制操作。 任何文件的底层都是字节,字节流做复制,是一字不漏的转移完全部字节,只要复制后的文件格式一致就没问题!

IO流资源释放

如果前面的操作出现问题,那就没有机会执行close()方法了。

try-catch-finally

  • finally代码区的特点:无论try中的程序是正常执行了,还是出现了异常,最后都一定会执行finally区,除非JVM终止。(注意千万不要在finally里面返回数据,不然就是一场空。)
  • 作用:一般用于在程序执行完成后进行资源的释放操作(专业级做法)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Test2 {
public static void main(String[] args) {
InputStream is = null;
OutputStream os = null;
try {
System.out.println(10 / 0);
// 1、创建一个字节输入流管道与源文件接通
is = new FileInputStream("file-io-app\\src\\itheima03.txt");
// 2、创建一个字节输出流管道与目标文件接通。
os = new FileOutputStream("file-io-app\\src\\itheima03copy.txt");

System.out.println(10 / 0);

// 3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024]; // 1KB.
// 4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
int len; // 记住每次读取了多少个字节。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
System.out.println("复制完成!!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 释放资源的操作
try {
if(os != null) os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if(is != null) is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

try-with-resource

JDK7以后的资源释放。

格式如下:

1
2
3
4
5
6
7
try(资源对象1; 资源对象2;){
使用资源的代码
}catch(异常类 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
/**
* 目标:掌握释放资源的方式:try-with-resource
*/
public class Test3 {
public static void main(String[] args) {
try (
// 1、创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("D:/resource/meinv.png");
// 2、创建一个字节输出流管道与目标文件接通。
OutputStream os = new FileOutputStream("C:/data/meinv.png");
){
// 3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024]; // 1KB.
// 4、从字节输入流中读取字节数据,写出去到字节输出流中。读多少写出去多少。
int len; // 记住每次读取了多少个字节。
while ((len = is.read(buffer)) != -1){
os.write(buffer, 0, len);
}
System.out.println(conn);
System.out.println("复制完成!!");

} catch (Exception e) {
e.printStackTrace();
}
}
}
  • 注意小括号里面只能放置资源变量(流对象)。
  • 什么是资源?资源都是会实现AutoCloseable接口的,都会有一个close方法。
  • 资源放到小括号里面后,用完会被自动调用其close方法完成资源的释放操作。

IO流(字符流)

  • 字节流:适合复制文件等,不适合读写文本文件。
  • 字符流:适合读写文本文件内容。

作用:以内存为基准,可以把文件中的数据以字符的形式读入到内存中去。

FileReader文件字符输入流

构造器

  1. public FileReader(File file)。创建字符输入流管道与源文件接通。
  2. public FileReader(String pathname)。创建字符输入流管道与源文件接通。

常用方法

  1. public int read()。每次读取一个字符返回,如果发现没有数据可读会返回-1。
  2. public int read(char[] buffer)。每次用一个字符数组去读取数据,返回字符数组读取了多少个字符,如果发现没有数据可读会返回-1。

FileReader读取文件的步骤如下:

1
2
3
1.创建FileReader对象与要读取的源文件接通
2.调用read()方法读取文件中的字符
3.调用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
/**
* 目标:掌握文件字符输入流。
*/
public class FileReaderTest1 {
public static void main(String[] args) {
try (
// 1、创建一个文件字符输入流管道与源文件接通
Reader fr = new FileReader("io-app2\\src\\itheima01.txt");
){
// 2、一个字符一个字符的读(性能较差)
// int c; // 记住每次读取的字符编号。
// while ((c = fr.read()) != -1){
// System.out.print((char) c);
// }
// 每次读取一个字符的形式,性能肯定是比较差的。

// 3、每次读取多个字符。(性能是比较不错的!)
char[] buffer = new char[3];
int len; // 记住每次读取了多少个字符。
while ((len = fr.read(buffer)) != -1){
// 读取多少倒出多少
System.out.print(new String(buffer, 0, len));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

FileWriter文件字符输出流

作用:以内存为基准,把内存中的数据以字符的形式写出到文件中去。

构造器

  1. public FileWriter(File file)。创建字节输出流管道与源文件对象接通。
  2. public FileWriter(String filepath)。创建字节输出流管道与源文件路径接通。
  3. public FileWriter(File file,boolean append)。创建字节输出流管道与源文件对象接通,可追加数据。
  4. public FileWriter(String filepath,boolean append)。创建字节输出流管道与源文件路径接通,可追加数据。

常用方法

  1. void write(int c)。写一个字符。
  2. void write(String str)。写一个字符串。
  3. void write(String str, int off, int len)。写一个字符串的一部分。
  4. void write(char[] cbuf)。写入一个字符数组。
  5. void write(char[] cbuf, int off, int len)。写入字符数组的一部分。

步骤如下:

1
2
3
1.创建FileWirter对象与要读取的目标文件接通
2.调用write(字符数据/字符数组/字符串)方法读取文件中的字符
3.调用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
29
30
31
32
33
34
35
36
37
38
39
/**
* 目标:掌握文件字符输出流:写字符数据出去
*/
public class FileWriterTest2 {
public static void main(String[] args) {
try (
// 0、创建一个文件字符输出流管道与目标文件接通。
// 覆盖管道
// Writer fw = new FileWriter("io-app2/src/itheima02out.txt");
// 追加数据的管道
Writer fw = new FileWriter("io-app2/src/itheima02out.txt", true);
){
// 1、public void write(int c):写一个字符出去
fw.write('a');
fw.write(97);
//fw.write('磊'); // 写一个字符出去
fw.write("\r\n"); // 换行

// 2、public void write(String c)写一个字符串出去
fw.write("我爱你中国abc");
fw.write("\r\n");

// 3、public void write(String c ,int pos ,int len):写字符串的一部分出去
fw.write("我爱你中国abc", 0, 5);
fw.write("\r\n");

// 4、public void write(char[] buffer):写一个字符数组出去
char[] buffer = {'黑', '马', 'a', 'b', 'c'};
fw.write(buffer);
fw.write("\r\n");

// 5、public void write(char[] buffer ,int pos ,int len):写字符数组的一部分出去
fw.write(buffer, 0, 2);
fw.write("\r\n");
} catch (Exception e) {
e.printStackTrace();
}
}
}

注意事项

字符输出流写出数据后,必须刷新流,或者关闭流,写出去的数据才能生效。

  1. public void flush() throws IOException。刷新流,就是将内存中缓存的数据立即写到文件中去生效!刷新流之后数据流可以继续使用。(数据在内存中装满了会自动刷新写进文件然后继续接。)
  2. public void close() throws IOException。关闭流的操作,包含了刷新!关闭之后就不可以继续用了。

总结

  • 字节流适合做一切文件数据的拷贝(音视频,文本);字节流不适合读取中文内容输出。
  • 字符流适合做文本文件的操作(读,写)。

缓冲流

更新(原因/区别)

  • 前面学的以File开头的实现类称为原始流/低级流。
  • 以Buffered开头称为包装流/处理流。对原始流进行包装,以提高原始流读写数据的性能。

字节缓冲流

原理

  • 字节缓冲输入流自带了8KB缓冲池;字节缓冲输出流也自带了8KB缓冲池。

构造器

  1. public BufferedInputStream(InputStream is)。把低级的字节输入流包装成一个高级的缓冲字节输入流,从而提高读数据的性能。
  2. public BufferedOutputStream(OutputStream os)。把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能。

功能上并无很大变化,性能提升。

字符缓冲流

原理:

  • 自带8K(8192)的字符缓冲池,可以提高字符输入流、输出流读取字符数据的性能。

构造器:

  1. public BufferedReader(Reader r)。把低级的字符输入流包装成字符缓冲输入流管道,从而提高字符输入流读字符数据的性能。
  2. public String readLine()。(新增,按照行读取。)读取一行数据返回,如果没有数据可读了,会返回null。
  3. public BufferedWriter(Writer r)。把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流写数据的性能。
  4. public void newLine()。(新增,换行。)

案例:拷贝出师表并恢复顺序。

原始流、缓冲流的性能分析[重点]

测试用例:

  • 分别使用原始的字节流,以及字节缓冲流复制一个很大视频。

测试步骤:

  1. 使用低级的字节流按照一个一个字节的形式复制文件。
  2. 使用低级的字节流按照字节数组的形式复制文件。
  3. 使用高级的缓冲字节流按照一个一个字节的形式复制文件。
  4. 使用高级的缓冲字节流按照字节数组的形式复制文件。
  • 默认情况下,采用一次复制1024个字节,缓冲流完胜。
  • 一次读取8192个字节时,低级流和缓冲流性能相当。相差的那几毫秒可以忽略不计。
  • 数组越大性能越高,低级流和缓冲流性能相当。相差的那几秒可以忽略不计。
  • 数组大到一定程度,性能已经提高了多少了,甚至缓冲流的性能还没有低级流高。

结论:推荐使用哪种方式提高字节流读写数据的性能?

  • 缓冲流的性能不一定比低级流高,其实低级流自己加一个数组,性能其实是不差。只不过缓冲流帮你加了一个相对而言大小比较合理的数组 。
  • 建议使用字节缓冲输入流、字节缓冲输出流,结合字节数组的方式,目前来看是性能最优的组合。

IO流-转换流

转换流:可以将字节流转换为字符流,并且可以指定编码方案。

  • 解决不同编码时,字符流读取文本内容乱码的问题。
  • 解决思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了

原因:

  • FileReader默认只能读取UTF-8编码格式的文件。如果使用FileReader读取GBK格式的文件,可能存在乱码,因为FileReader它遇到汉字默认是按照3个字节来读取的,而GBK格式的文件一个汉字是占2个字节,这样就会导致乱码。
  • 如果代码编码和被读取的文本文件的编码是不一致的,使用字符流读取文本文件时就会出现乱码!

InputStreamReader类

  • InputStreamzhuan转换为Reader,是Reader的子类,也算是字符输入流。
  • 不能单独使用,内部需要封装一个InputStream的子类对象,再指定一个编码表,如果不指定编码表,默认会按照UTF-8形式进行转换。

常用方法:

  1. public InputStreamReader(InputStream is)。把原始的字节输入流,按照代码默认编码转成字符输入流(与直接用FileReader的效果一样)。
  2. public InputStreamReader(InputStream is ,String charset)。把原始的字节输入流,按照指定字符集编码转成字符输入流(重点)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class InputStreamReaderTest2 {
public static void main(String[] args) {
try (
// 1、得到文件的原始字节流(GBK的字节流形式)
InputStream is = new FileInputStream("io-app2/src/itheima06.txt");
// 2、把原始的字节输入流按照指定的字符集编码转换成字符输入流
Reader isr = new InputStreamReader(is, "GBK");
// 3、把字符输入流包装成缓冲字符输入流
BufferedReader br = new BufferedReader(isr);
){
String line;
while ((line = br.readLine()) != null){
System.out.println(line);
}


} catch (Exception e) {
e.printStackTrace();
}
}
}

OutputStreamWriter类

如何控制写出去的字符使用的字符集编码?

  1. 调用String提供的getBytes方法。
  2. 用字符输出转换流实现。
  • OutputStream转换为Writer,是Writer的子类,算是字符输出流。
  • 不能单独使用,内部需要封装一个OutputStream的子类对象,再指定一个编码表,如果不指定编码表,默认会按照UTF-8形式进行转换。
  • 作用:可以控制写出去的字符使用什么字符集编码。
  • 解决思路:获取字节输出流,再按照指定的字符集编码将其转换成字符输出流,以后写出去的字符就会用该字符集编码了。

常用方法:

  1. public OutputStreamWriter(OutputStream os)。可以把原始的字节输出流,按照代码默认编码转换成字符输出流。
  2. public OutputStreamWriter(OutputStream os,String charset)。可以把原始的字节输出流,按照指定编码转换成字符输出流(重点)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OutputStreamWriterTest3 {
public static void main(String[] args) {
// 指定写出去的字符编码。
try (
// 1、创建一个文件字节输出流
OutputStream os = new FileOutputStream("io-app2/src/itheima07out.txt");
// 2、把原始的字节输出流,按照指定的字符集编码转换成字符输出转换流。
Writer osw = new OutputStreamWriter(os, "GBK");
// 3、把字符输出流包装成缓冲字符输出流
BufferedWriter bw = new BufferedWriter(osw);
){
bw.write("我是中国人abc");
bw.write("我爱你中国123");

} catch (Exception e) {
e.printStackTrace();
}
}
}

IO流-打印流

PrintStream/PrintWriter(打印流)

  • 作用:打印流可以实现更方便、更高效的打印数据出去,能实现打印啥出去就是啥出去。
  • 优势:能上都是使用方便,性能高效(核心优势)。

PrintStream构造器

继承自字节输出流OutputStream,支持写字节。

  1. public PrintStream(OutputStream/File/String)。打印流直接通向字节输出流/文件/文件路径。
  2. public PrintStream(String fileName, Charset charset)。可以指定写出去的字符编码。
  3. public PrintStream(OutputStream out, boolean autoFlush)。可以指定实现自动刷新。
  4. public PrintStream(OutputStream out, boolean autoFlush, String encoding)。可以指定实现自动刷新,并可指定字符的编码。

PrintStream常用方法

  1. public void println(Xxx xx)。打印任意类型的数据出去。
  2. public void write(int/byte[]/byte[]一部分)。可以支持写字节数据出去。

PrintWriter构造器

继承自字符输出流Writer,支持写字符。

  1. public PrintWriter(OutputStream/Writer/File/String)。打印流直接通向字节输出流/文件/文件路径。
  2. public PrintWriter(String fileName, Charset charset)。可以指定写出去的字符编码。
  3. public PrintWriter(OutputStream out/Writer, boolean autoFlush)。可以指定实现自动刷新。
  4. public PrintWriter(OutputStream out, boolean autoFlush, String encoding)。可以指定实现自动刷新,并可指定字符的编码。

PrintWriter常用方法

  1. public void println(Xxx xx)。打印任意类型的数据出去。
  2. public void write(int/String/char[]/..)。可以支持写字符数据出去。

PrintStream和PrintWriter的区别

  • 打印数据的功能上是一模一样的:都是使用方便,性能高效(核心优势)。
  • PrintStream继承自字节输出流OutputStream,因此支持写字节数据的方法。
  • PrintWriter继承自字符输出流Writer,因此支持写字符数据出去。

注意:高级流方法不能直接追加true来控制,需要先包装成低级流。

1
new PrintWriterStream(new FileOutputStream("path",true))

输出语句的重定向。

  • 打印流的一种应用。
  • 可以把输出语句的打印位置改到某个文件中去。
  • System.out.println(“老骥伏枥”)这个语句的out实际上是帮我们创建了一个打印对象,然后默认打印到控制台上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PrintTest2 {
public static void main(String[] args) {
System.out.println("老骥伏枥");
System.out.println("志在千里");

try ( PrintStream ps = new PrintStream("io-app2/src/itheima09.txt"); ){
// 把系统默认的打印流对象改成自己设置的打印流
System.setOut(ps);//定向到这里了

System.out.println("烈士暮年");
System.out.println("壮心不已");
} catch (Exception e) {
e.printStackTrace();
}
}
}

IO流-数据流

需求:把数据和数据的类型一并写到文件中去,读取的时候也将数据和数据类型一并读出来。有两个:DataInputStream和DataOutputStream。

DataOutputStream类

允许把数据和其类型一并写出去。

构造器

  1. public DataOutputStream(OutputStream out)。创建新数据输出流包装基础的字节输出流

常用方法

  1. public final void writeByte(int v) throws IOException。将byte类型的数据写入基础的字节输出流。
  2. public final void writeInt(int v) throws IOException。将int类型的数据写入基础的字节输出流。
  3. public final void writeDouble(Double v) throws IOException。将double类型的数据写入基础的字节输出流。
  4. public final void writeUTF(String str) throws IOException。将字符串数据以UTF-8编码成字节写入基础的字节输出流。
  5. public void write(int/byte[]/byte[]一部分)。支持写字节数据出去。

DataInputStream类

用于读取数据输出流写出去的数据。

构造器

  1. public DataInputStream(InputStream is)。创建新数据输入流包装基础的字节输入流

常用方法

  1. Public final byte readByte() throws IOException。读取字节数据返回。
  2. public final int readInt() throws IOException。读取int类型的数据返回。
  3. public final double readDouble() throws IOException。读取double类型的数据返回。
  4. public final String readUTF() throws IOException。读取字符串数(UTF-8)据返回。
  5. public int readInt()/read(byte[])。支持读字节数据进来。

注意:二者需要搭配使用,存进去的是什么类型,读取就要怎么读,而且存进去的数据并不是为了读者直接来看的。(在通信中非常方便。)

IO流-序列化流

序列化

  • 对象序列化:把Java对象写入到文件中去。
  • 对象反序列化:把文件里的Java对象读出来。

ObjectOutputStream类

可以把Java对象进行序列化:把Java对象存入到文件中去。

构造器

  1. public ObjectOutputStream(OutputStream out)。创建对象字节输出流,包装基础的字节输出流

常用方法

  1. public final void writeObject(Object o) throws IOException。把对象写出去。

注意:对象如果要参与序列化,必须实现序列化接口(java.io.Serializable)!相当于一个标记,虽然接口里面什么都没有,但是虚拟机需要。

ObjectInputStream类

可以把Java对象进行反序列化:把存储在文件中的Java对象读入到内存中来。

构造器

  1. public ObjectInputStream(InputStream is)。创建对象字节输入流,包装基础的字节输入流

常用方法

  1. public final Object readObject()。把存储在文件中的Java对象读出来。

注意:如果成员变量中加transient修饰符,这个成员变量将不参与序列化。

如果要一次系列化多个对象,怎么做?

  • 用一个ArrayList集合存储多个对象,然后直接对集合进行序列化即可。
  • 注意:ArrayList集合已经实现了序列化接口!

补充知识: IO框架

什么是框架

  • 解决某类问题,编写的一套类、接口等,可以理解成一个半成品,大多框架都是第三方研发的。
  • 好处:在框架的基础上开发,可以得到优秀的软件架构,并能提高开发效率
  • 框架的形式:一般是把类、接口等编译成class形式,再压缩成一个.jar结尾的文件发行出去。

什么是IO框架

  • 封装了Java提供的对文件、数据进行操作的代码,对外提供了更简单的方式来对文件进行操作,对数据进行读写等。

Commons-io框架

Commons-io是apache开源基金组织提供的一组有关IO操作的小框架,目的是提高IO流的开发效率。

FileUtils类提供的部分方法展示

  1. public static void copyFile(File srcFile, File destFile)。复制文件。
  2. public static void copyDirectory(File srcDir, File destDir)。复制文件夹。
  3. public static void deleteDirectory(File directory)。删除文件夹。
  4. public static String readFileToString(File file, String encoding)。读数据。
  5. public static void writeStringToFile(File file, String data, String charname, boolean append)。写数据。

IOUtils类提供的部分方法展示

  1. public static int copy(InputStream inputStream, OutputStream outputStream)。复制文件。
  2. public static int copy(Reader reader, Writer writer)。复制文件。
  3. public static void write(String data, OutputStream output, String charsetName)。写数据。

使用指北

  1. 下载:Commons IO – Download Apache Commons IO Binary的zip包。
  2. 复制“commons-io-2.11.0.jar”包。
  3. 项目文件夹邮件,新建Directory,取名“lib”。粘贴。
  4. 右键lib文件夹,选择“Add as library”。
  5. 可以使用了。

最后,其实Java原生自己从1.7也提供了File类的copy、readString等方法来做,但是功能上还没有第三方框架强大。

【Java基础】集合(Map、Stream流)

Map集合

需要存储一一对应的数据时,就可以考虑使用Map集合来做。

  • Map集合称为双列集合,格式:{key1=value1 , key2=value2 , key3=value3 , …}, 一次需要存一对数据做为一个元素。
  • Map集合的每个元素“key=value”称为一个键值对/键值对对象/Entry对象,Map集合也被叫做“键值对集合”。
  • Map集合的所有键是不允许重复的,但值可以重复,键和值是一一对应的,每一个键只能找到自己对应的值。

Map集合体系

Map<K,V>是接口,下面有很多实现类:

  1. HashMap<K , V>。
  2. LinkedHashMap<K , V>。(是1的子类。)
  3. TreeMap<K , V>。

特点:

  1. Map系列集合的特点都是由键决定的,值只是一个附属品,值是不做要求的。
  2. HashMap(由键决定特点): 无序、不重复、无索引; (用的最多)。
  3. LinkedHashMap (由键决定特点):由键决定的特点:有序、不重复、无索引。
  4. TreeMap (由键决定特点):按照(键的)大小默认升序排序、不重复、无索引。
1
2
//经典代码
Map<String, Integer> map = new HashMap<>(); //同样需要指定具体类。

Map常用方法

  1. public V put(K key,V value)。添加元素。
  2. public int size()。获取集合的大小。
  3. public void clear()。清空集合。
  4. public boolean isEmpty()。判断集合是否为空,为空返回true , 反之。
  5. public V get(Object key)。根据键获取对应值
  6. public V remove(Object key)。根据键删除整个元素。
  7. public boolean containsKey(Object key)。判断是否包含某个键。(是精确匹配。)
  8. public boolean containsValue(Object value)。判断是否包含某个值。(值的类型一定要注意。)
  9. public Set keySet()。获取全部键的集合。(会放到一个Set里面再返回。)
  10. public Collection values()。获取Map集合的全部值。(会放到一个Collection里面再返回,因为Set里面不允许有重复的值。)
  11. putAll()。把其他Map集合的数据倒入到自己集合中来。

Map遍历方式

  1. 键找值。先获取Map集合全部的键,再通过遍历键来找值。
  2. 键值对。把“键值对“看成一个整体进行遍历(难度较大)。
  3. Labmda。JDK 1.8开始之后的新技术(非常的简单)。

键找值

  1. public Set keySet()。获取所有键的集合。
  2. public V get(Object key)。根据键获取其对应的值。
1
2
3
4
5
6
7
8
9
10
// 1、获取Map集合的全部键
Set<String> keys = map.keySet();
// System.out.println(keys);
// [蜘蛛精, 牛魔王, 至尊宝, 紫霞]
// key
// 2、遍历全部的键,根据键获取其对应的值
for (String key : keys) {
// 根据键获取对应的值
double value = map.get(key);
System.out.println(key + "=====>" + value);

键值对

使用增强for遍历时,元素类型无法确定。所以Java提供另一个方法:

  • Set<Map.Entry<K, V>> entrySet()。获取所有“键值对”的集合。作为一个Set集合返回了,然后每个元素就是键值对类型,自然也就可以遍历了。

Map.Entry提供的方法:

  1. K getKey()。获取键。
  2. V getValue()。获取值。
1
2
3
4
5
6
// 1、调用Map集合提供entrySet方法,把Map集合转换成键值对类型的Set集合
Set<Map.Entry<String, Double>> entries = map.entrySet(); //ctrl+alt+v
for (Map.Entry<String, Double> entry : entries) {
String key = entry.getKey();
double value = entry.getValue();
System.out.println(key + "---->" + value);

Labmda(简单,推荐)

JDK 1.8开始。需要用到下面的方法:

  • default void forEach(BiConsumer<? super K, ? super V> action)。结合lambda遍历Map集合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//遍历map集合,传递Lambda表达式
map.forEach(( k, v) -> {
System.out.println(k + "---->" + v);
});

//来看看原来的形式
//遍历map集合,传递匿名内部类(BiConsumer是一个接口)
map.forEach(new BiConsumer<String, Double>() {
@Override
public void accept(String k, Double v) {
System.out.println(k + "---->" + v);
}
});
//forEach内部其实是利用了第2种遍历方法(键值对)

(1)HashMap

HashMap(由键决定特点): 无序、不重复、无索引; (用的最多)。

底层原理

  • 和HashSet的底层原理一样,基于哈希表实现。实际上:原来学的Set系列集合的底层就是基于Map实现的,只是Set集合中的元素只要键数据,不要值数据而已。
  • JDK8之前,哈希表 = 数组+链表;JDK8开始,哈希表 = 数组+链表+红黑树;哈希表是一种增删改查数据,性能都较好的数据结构。但是它是无序,不能重复,没有索引支持的(由键决定特点)。
  • HashMap的键依赖hashCode方法和equals方法保证键的唯一
  • 如果键存储的是自定义类型的对象,可以通过重写hashCode和equals方法,这样可以保证多个对象内容一样时,HashMap集合就能认为是重复的。

(2)LinkedHashMap

LinkedHashMap (由键决定特点):由键决定的特点:有序、不重复、无索引。

底层原理

  • 底层数据结构依然是基于哈希表实现的,只是每个键值对元素又额外的多了一个双链表的机制记录元素顺序(保证有序)。实际上:原来学习的LinkedHashSet集合的底层原理就是LinkedHashMap。

(3)TreeMap

TreeMap (由键决定特点):按照(键的)大小默认升序排序、不重复、无索引。

底层原理

  • 特点:不重复、无索引、可排序(按照键的大小默认升序排序,只能对键排序)。
  • 原理:TreeMap跟TreeSet集合的底层原理是一样的,都是基于红黑树实现的排序。
  • TreeMap集合同样也支持两种方式来指定排序规则。
    • 让类实现Comparable接口,重写conpareTo()比较规则。
    • TreeMap集合有一个有参数构造器,支持创建Comparator比较器对象,以便用来指定比较规则。

补充知识:集合的嵌套

集合中的元素又是一个集合。

Stream流

JDK8开始最大的改变之一。(一共是两个:1.Lambda表达式,2.Stream流。)

什么是Stream?

  • 也叫Stream流,是Jdk8开始新增的一套API (java.util.stream.*),可以用于操作集合或者数组的数据。
  • 优势: Stream流大量的结合了Lambda的语法风格来编程,提供了一种更加强大,更加简单的方式操作集合或者数组中的数据,代码更简洁,可读性更好。

案例

有一个List集合,元素有"张三丰","张无忌","周芷若","赵敏","张强",找出姓张,且是3个字的名字,存入到一个新集合中去。

1
2
3
List<String> names = new ArrayList<>();
Collections.addAll(names, "张三丰","张无忌","周芷若","赵敏","张强");
System.out.println(names);

传统方式:

1
2
3
4
5
6
7
8
// 找出姓张,且是3个字的名字,存入到一个新集合中去。
List<String> list = new ArrayList<>();
for (String name : names) {
if(name.startsWith("张") && name.length() == 3){
list.add(name);
}
}
System.out.println(list);

Stream流:(支持链式编程。)

1
2
List<String> list2 = names.stream().filter(s -> s.startsWith("张")).filter(a -> a.length()==3).collect(Collectors.toList());
System.out.println(list2);

两次filter筛选,然后collect收集。

使用步骤

  1. 数据源。–>获取其Stream流(理解为流水线,能与数据源建立联系)。
  2. 中间方法。调用流水线的各种方法对数据进行处理和计算。例如,过滤、排序、去重等。
  3. 获取结果。便利、统计、收集到一个新的集合中并返回。
1
2
3
4
5
主要掌握下面四点:
1、如何获取List集合的Stream流?
2、如何获取Set集合的Stream流?
3、如何获取Map集合的Stream流?
4、如何获取数组的Stream流?

常用方法

获取Stream流

如何获取集合的Stream流?

  • Collection类。default Stream stream()。获取当前集合对象的Stream流。

如何获取数组的Stream流?

  • Arrays类。public static Stream stream(T[] array)。获取当前数组的Stream流。
  • Stream类。public static Stream of(T… values)。获取当前接收数据的Stream流。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 目标:掌握Stream流的创建。
*/
public class StreamTest2 {
public static void main(String[] args) {
// 1、如何获取List集合的Stream流?
List<String> names = new ArrayList<>();
Collections.addAll(names, "张三丰","张无忌","周芷若","赵敏","张强");
Stream<String> stream = names.stream();

// 2、如何获取Set集合的Stream流?
Set<String> set = new HashSet<>();
Collections.addAll(set, "刘德华","张曼玉","蜘蛛精","马德","德玛西亚");
Stream<String> stream1 = set.stream();
stream1.filter(s -> s.contains("德")).forEach(s -> System.out.println(s));

// 3、如何获取Map集合的Stream流?
Map<String, Double> map = new HashMap<>();
map.put("古力娜扎", 172.3);
map.put("迪丽热巴", 168.3);
map.put("马尔扎哈", 166.3);
map.put("卡尔扎巴", 168.3);

Set<String> keys = map.keySet();
Stream<String> ks = keys.stream();

Collection<Double> values = map.values();
Stream<Double> vs = values.stream();

Set<Map.Entry<String, Double>> entries = map.entrySet();
Stream<Map.Entry<String, Double>> kvs = entries.stream();
kvs.filter(e -> e.getKey().contains("巴"))
.forEach(e -> System.out.println(e.getKey()+ "-->" + e.getValue()));

// 4、如何获取数组的Stream流?
String[] names2 = {"张翠山", "东方不败", "唐大山", "独孤求败"};
Stream<String> s1 = Arrays.stream(names2);
Stream<String> s2 = Stream.of(names2);
}
}

中间方法

中间方法指的是调用完成后会返回新的Stream流,可以继续使用(支持链式编程)。(因此支持链式编程。)

常用方法

  1. Stream filter(Predicate<? super T> predicate)。用于对流中的数据进行过滤。
  2. Stream sorted()。对元素进行升序排序。
  3. Stream sorted(Comparator<? super T> comparator)。对元素进行升序排序。
  4. Stream limit(long maxSize)。获取前几个元素。
  5. Stream skip(long n)。跳过前几个元素。
  6. Stream distinct()。去除流中重复的元素。
  7. Stream map(Function<? super T,? extends R> mapper)。对元素进行加工,并返回对应的新流。
  8. static Stream concat(Stream a, Stream b)。合并a和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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 目标:掌握Stream流提供的常见中间方法。
*/
public class StreamTest3 {
public static void main(String[] args) {
List<Double> scores = new ArrayList<>();
Collections.addAll(scores, 88.5, 100.0, 60.0, 99.0, 9.5, 99.6, 25.0);
// 需求1:找出成绩大于等于60分的数据,并升序后,再输出。
scores.stream().filter(s -> s >= 60).sorted().forEach(s -> System.out.println(s));

List<Student> students = new ArrayList<>();
Student s1 = new Student("蜘蛛精", 26, 172.5);
Student s2 = new Student("蜘蛛精", 26, 172.5);
Student s3 = new Student("紫霞", 23, 167.6);
Student s4 = new Student("白晶晶", 25, 169.0);
Student s5 = new Student("牛魔王", 35, 183.3);
Student s6 = new Student("牛夫人", 34, 168.5);
Collections.addAll(students, s1, s2, s3, s4, s5, s6);
// 需求2:找出年龄大于等于23,且年龄小于等于30岁的学生,并按照年龄降序输出.
students.stream().filter(s -> s.getAge() >= 23 && s.getAge() <= 30)
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
.forEach(s -> System.out.println(s));

// 需求3:取出身高最高的前3名学生,并输出。
students.stream().sorted((o1, o2) -> Double.compare(o2.getHeight(), o1.getHeight()))
.limit(3).forEach(System.out::println);
System.out.println("-----------------------------------------------");

// 需求4:取出身高倒数的2名学生,并输出。 s1 s2 s3 s4 s5 s6
students.stream().sorted((o1, o2) -> Double.compare(o2.getHeight(), o1.getHeight()))
.skip(students.size() - 2).forEach(System.out::println);

// 需求5:找出身高超过168的学生叫什么名字,要求去除重复的名字,再输出。
students.stream().filter(s -> s.getHeight() > 168).map(Student::getName)
.distinct().forEach(System.out::println);

// distinct去重复,自定义类型的对象(希望内容一样就认为重复,重写hashCode,equals)
students.stream().filter(s -> s.getHeight() > 168)
.distinct().forEach(System.out::println);

Stream<String> st1 = Stream.of("张三", "李四");
Stream<String> st2 = Stream.of("张三2", "李四2", "王五");
Stream<String> allSt = Stream.concat(st1, st2); //如果合并的两个类型不一样,需要用Object来接。
allSt.forEach(System.out::println);
}
}

终结方法

终结方法指的是调用完成后,不会返回新Stream了,没法继续使用流了。

常用方法

  1. void forEach(Consumer action)。对此流运算后的元素执行遍历。
  2. long count()。统计此流运算后的元素个数。
  3. Optional max(Comparator<? super T> comparator)。获取此流运算后的最大值元素。
  4. Optional min(Comparator<? super T> comparator)。获取此流运算后的最小值元素。
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
/**
* 目标:Stream流的终结方法
*/
public class StreamTest4 {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
Student s1 = new Student("蜘蛛精", 26, 172.5);
Student s2 = new Student("蜘蛛精", 26, 172.5);
Student s3 = new Student("紫霞", 23, 167.6);
Student s4 = new Student("白晶晶", 25, 169.0);
Student s5 = new Student("牛魔王", 35, 183.3);
Student s6 = new Student("牛夫人", 34, 168.5);
Collections.addAll(students, s1, s2, s3, s4, s5, s6);
// 需求1:请计算出身高超过168的学生有几人。
long size = students.stream().filter(s -> s.getHeight() > 168).count();
System.out.println(size);

// 需求2:请找出身高最高的学生对象,并输出。
Student s = students.stream().max((o1, o2) -> Double.compare(o1.getHeight(), o2.getHeight())).get();
System.out.println(s);

// 需求3:请找出身高最矮的学生对象,并输出。
Student ss = students.stream().min((o1, o2) -> Double.compare(o1.getHeight(), o2.getHeight())).get();
System.out.println(ss);

// 需求4:请找出身高超过170的学生对象,并放到一个新集合中去返回。
// 流只能收集一次。
List<Student> students1 = students.stream().filter(a -> a.getHeight() > 170).collect(Collectors.toList());
System.out.println(students1);

Set<Student> students2 = students.stream().filter(a -> a.getHeight() > 170).collect(Collectors.toSet());
System.out.println(students2);

// 需求5:请找出身高超过170的学生对象,并把学生对象的名字和身高,存入到一个Map集合返回。
Map<String, Double> map =
students.stream().filter(a -> a.getHeight() > 170)
.distinct().collect(Collectors.toMap(a -> a.getName(), a -> a.getHeight()));
System.out.println(map);

// Object[] arr = students.stream().filter(a -> a.getHeight() > 170).toArray();
Student[] arr = students.stream().filter(a -> a.getHeight() > 170).toArray(len -> new Student[len]);
System.out.println(Arrays.toString(arr));
}
}

其他方法

  • 收集Stream流。就是把Stream流操作后的结果转回到集合或者数组中去返回。
  • Stream流:方便操作集合/数组的手段; 集合/数组:才是开发中的目的。
  1. R collect(Collector collector)。把流处理后的结果收集到一个指定的集合中去。
  2. Object[] toArray()。把流处理后的结果收集到一个数组中去。

collect具体的收集方式

  1. public static Collector toList()。把元素收集到List集合中。
  2. public static Collector toSet()。把元素收集到Set集合中。注意Set会去重
  3. public static Collector toMap(Function keyMapper , Function valueMapper)。把元素收集到Map集合中。注意需要在收集的时候指定键和值。而且它不能帮我们去重,因此可能报错,要自己在中间加一个distict()方法。

注意,流只能收集一次!不可以先用一个Steam对象接住Steam流,然后做两次收集操作。

【资源帖】学习Java和算法

Java教程

学习路线【黑马程序员】

  1. Java简版基础教程:https://www.bilibili.com/video/BV1Cv411372m/
    • 书:《Java核心技术 1》
    • 书:《Head First Java》
  2. Java Web框架:https://www.bilibili.com/video/BV1m84y1w7Tb/
  3. 单体项目开发:
  4. 微服务:
  5. 企业级项目实战(选择学习):
  6. 面试专题:

精进指南

资源帖

LeeCode刷题指南

官网

力扣 (LeetCode) 全球极客挚爱的技术成长平台

刷题指北

peach买个共享会员账号看考察频次。

资源帖

推荐书单

  • 《剑指offer》。

【伴行青年】如何写一份受欢迎的校招简历

常见问题

  1. 过度包装设计。减弱主要信息能量,华而不实。
  2. 篇幅过长。
  3. 求职定位不明。
  4. 实践经历描述不当。
  5. 一份简历闯天下。
    • JD:工作职责、工作胜任力。
  6. 使用表格式简历。
  7. 啰啰嗦嗦重点不突出。
  8. 不该讲的乱讲。例如,创业、离职原因、到岗时间、离婚、错误检讨、薪资条件。

优秀简历的特征

版面设计

简洁大方、布局清晰、模板分界。

简历结构

结构完整、详略得当、易于阅读。

内容呈现

逻辑清晰、优势突出、数据支撑。

人岗匹配

有的放矢、贴近岗位JD、天生我才。

效果

  1. 脱颖而出、入得法眼。
  2. 顺畅读完,越读越喜欢。
  3. 打动人心,不如见一面。
  4. 为面试好印象做好铺垫。(面试官其实是根据初印象,步步求证是否确实是需要的人。)

简历的完整结构“2+2”

通用的(非本专业/技术岗):

  1. 基本信息:略写。7%。
  2. 自我评价:较详。20%。
  3. 工作经历:详写。(大力气。)70%。
  4. 学历、证书、技能:略写。3%。

基本信息

  • 姓名+求职意向+性别+年龄。(政治面貌:国企央企等写,外企不写,民企无所谓。)
  • 联系方式:城市、电话、微信、邮箱。(不用写太多。城市可以写XX(意向城市)。联系方式三个必有一,推荐电话。)
  • 个人照片:彩色、正面头像、有精气神。(匹配行业。)

自我评价/教育背景

社招:

  • 工作背景。例如,年份+领域/行业+擅长/熟悉/掌握。
  • 优势能力。四条分号隔开。专业软件可以写。
  • 职业素养。

校招:

  • 起止时段:学校、专业、学历、学位。
  • 主修课程。
  • 奖学金可以写。

工作经历/实习经历/项目经历

社招:

  • 工作时段。(可以有总分,总的在某个公司,分的是不同岗位。最好是倒叙。)
  • 工作职责。(前3-5个。)
  • 工作业绩。(为了醒目,可以换个标志,比如五角星。一定要有数据,没数据也不要乱讲。)
  • 工作获奖。(要有含金量的,行业、省市级以上,发明专利等。)

校招:

  • 起止时段、公司、岗位。
  • 工作职责、价值、奖项。

其他佐证

  • 学历背景:学校、专业。
  • 语言能力:语种、级别。(只是针对某些需要语言能力的岗位。其他:听说读写能力流利,可作为工作语言。)
  • 专业技能:证书、级别。(例如岗位资格证。)

校招和社招的区别

  • 教育背景前置/后置。
  • 自我评价的有无。(复盘能力。)

优秀简历写作心法

人岗匹配!!!

  1. 职场的本质是价值交换。(以终为始。)
  2. 见字如面,格式细节很重要,大小标题和逻辑关系。
  3. 凤头猪肚豹尾。自我评价漂亮客观,工作经历饱满有结果,其他佐证简短有力。
  4. 工作经历倒叙。写清楚总分,闭环表达,数据支撑。
  5. 工作年限5年以下,请用一张A4纸完成。
  6. 如果经历比较少,根据一段经历可以多挖掘,例如,“1+3+6+x”主轴。

如何准备

  1. 确定自己身份:校招/社招,确定目标岗位,了解岗位JD。
  2. 准备模板,通读三遍。准备素材,多多益善。(所有经历都可以准备。)
  3. 现有骨架,再填充。时间倒叙,先粗后细。
  4. 先写草稿,反复打磨。