MyBatis是一款优秀的 持久层 框架,用于简化JDBC的开发。
MyBatis本是 Apache的一个开源项目iBatis,2010年这个项目由apache迁移到了google code,并且改名为MyBatis 。2013年11月迁移到Github。
在上面我们提到了两个词:一个是持久层,另一个是框架
现在使用Mybatis操作数据库,就是在Mybatis中编写SQL查询代码,发送给数据库执行,数据库执行后返回结果
Mybatis会把数据库执行的查询结果,使用实体类封装起来(一行记录对应一个实体类对象)
通过Mybatis可以很方便的进行数据库的访问操作。但是大家要明白,其实java语言操作数据库呢,只能通过一种方式:使用sun公司提供的 JDBC 规范
Mybatis框架,就是对原始的JDBC程序(Java语言操作关系型数据库的一套API)的封装
本质:
sun公司官方定义的一套操作所有关系型数据库的规范,即接口。
各个数据库厂商去实现这套接口,提供数据库驱动jar包。
我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类。
下面我们看看原始的JDBC程序是如何操作数据库的。操作步骤如下:
在pom.xml文件中已引入MySQL驱动依赖,我们直接编写JDBC代码即可
JDBC具体代码实现:
1 | import com.itheima.pojo.User; |
DriverManager(类):数据库驱动管理类。
作用:
注册驱动
创建java代码和数据库之间的连接,即获取Connection对象
Connection(接口):建立数据库连接的对象
- 作用:用于建立java程序和数据库之间的连接
Statement(接口): 数据库操作对象(执行SQL语句的对象)。
- 作用:用于向数据库发送sql语句
ResultSet(接口):结果集对象(一张虚拟表)
- 作用:sql查询语句的执行结果会封装在ResultSet中
通过上述代码,我们看到直接基于JDBC程序来操作数据库,代码实现非常繁琐,所以在项目开发中,我们很少使用。 在项目开发中,通常会使用Mybatis这类的高级技术来操作数据库,从而简化数据库操作、提高开发效率
原始的JDBC程序,存在以下几点问题:
分析了JDBC的缺点之后,我们再来看一下在mybatis中,是如何解决这些问题的:
数据库连接四要素(驱动、链接、用户名、密码),都配置在springboot默认的配置文件 application.properties中
查询结果的解析及封装,由mybatis自动完成映射封装,我们无需关注
在mybatis中使用了数据库连接池(Springboot默认Hikari追光者)技术,从而避免了频繁的创建连接、销毁连接而带来的资源浪费。
没有使用数据库连接池:
- 客户端执行SQL语句:要先创建一个新的连接对象,然后执行SQL语句,SQL语句执行后又需要关闭连接对象从而释放资源,每次执行SQL时都需要创建连接、销毁链接,这种频繁的重复创建销毁的过程是比较耗费计算机的性能。
允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个
释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏
数据库连接池的好处:
常见的数据库连接池:
现在使用更多的是:Hikari、Druid (性能更优越)
Hikari(追光者) [默认的连接池]
Druid(德鲁伊)
Druid连接池是阿里巴巴开源的数据库连接池项目
功能强大,性能优秀,是Java语言最好的数据库连接池之一
把默认的数据库连接池切换为Druid数据库连接池,只需要完成以下两步操作即可:
参考官方地址:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
1.在pom.xml文件中引入依赖
1 | <dependency> |
2.在配置文件中引入数据库连接配置(2种方式)
1 | 方式1:(datasource后面加druid) |
Lombok是一个实用的Java类库,可以通过简单的注解来简化和消除一些必须有但显得很臃肿的Java代码
通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,并可以自动化生成日志变量,简化java开发、提高效率
注解 | 作用 |
---|---|
@Getter/@Setter | 为所有的属性提供get/set方法 |
@ToString | 为类自动生成易阅读的 toString 方法 |
@EqualsAndHashCode | 为类提供拥有的非静态字段自动重写 equals 方法和 hashCode 方法 |
@Data | 提供了更综合的生成代码功能(@Getter + @Setter + @ToString + @EqualsAndHashCode) 集成了前四个注解 |
@NoArgsConstructor | 为实体类生成无参的构造器方法 |
@AllArgsConstructor | 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。 |
第1步:在pom.xml文件中引入依赖
1 | <!-- 在springboot的父工程中,已经集成了lombok并指定了版本号,故当前引入依赖时不需要指定version --> |
第2步:在实体类上添加注解
1 | import lombok.Data; |
创建位置:必须要和mapper层保持同一个包级别和同名(==同包同名==)
命名规则:要保证mapper的namespacce和mapper同全路径名,四大类语句的id要和方法名一致,resultType要和方法返回值(最里面的)一致
用于判断条件是否成立 –如果条件为true就拼接sql
1.在子元素有内容的情况下插入where子句
2.自动去除子句开头的and/or
1.动态的在行首插入set关键字
2.自动去除额外的逗号
循环的时候批量处理一部分数据
将一些固定的(重复的/相同的)sql提取出来
类似于Java的Switch语句,可以实现选择
生成sql语句的时候,在前后添加自定义的字符串
prefix:加前缀
prefixOverrides:要覆盖的前缀
suffix:加后缀
suffixOverrides:要覆盖的后缀
可以将表达式结果/特定内容绑定到一个变量,这个变量可以在sql语句中使用【类似于参数宏定义】
在Mybatis当中我们可以借助日志,查看到sql语句的执行、执行传递的参数以及执行结果。具体操作如下:
1 | #指定mybatis输出日志的位置, 输出控制台 |
开启日志之后,我们再次运行单元测试,可以看到在控制台中,输出了以下的SQL语句信息:
但是我们发现输出的SQL语句:delete from emp where id = ?,我们输入的参数16并没有在后面拼接,id的值是使用?进行占位。那这种SQL语句我们称为预编译SQL
mybatis中mapper层的写法就是预编译sql,传入参数放入的形式
预编译SQL有两个优势:
性能更高
更安全(防止SQL注入)
如果mapper接口方法形参只有一个普通类型的参数,#{…} 里面的属性名可以随便写
#{}表达式为OGNL表达式
${}表达式为EL表达式
1)相同:都可以获取对象的信息。
(2)#{ }
执行SQL时,会将#{ }替换为?,生成预编译SQL,会自动设置参数值,防止SQL注入
使用时机:参数传递,都使用#{ }
(3)${ }
拼接SQL。直接将参数拼接在SQL语句中,存在SQL注入问题
使用时机:如果对表名、列表进行动态设置时使用【东林微课堂创建每个月赛季表】
(4)#{ } 只能操作跟数据字表字段相关的列值,跟列值无关的只能用${ }
(5)#{ } 底层使用的是PreparedStatement,${ } 底层使用的是 Statement
在SpringBoot进行web程序开发时,它内置了一个核心的Servlet程序 DispatcherServlet,称之为 核心控制器。
DispatcherServlet 负责接收页面发送的请求,然后根据执行的规则,将请求再转发给后面的请求处理器Controller,请求处理器处理完请求之后,最终再由DispatcherServlet给浏览器响应数据。
那将来浏览器发送请求,会携带请求数据,包括:请求行、请求头;请求到达tomcat之后,tomcat会负责解析这些请求数据,然后呢将解析后的请求数据会传递给Servlet程序的HttpServletRequest对象,那也就意味着 HttpServletRequest 对象就可以获取到请求数据。 而Tomcat,还给Servlet程序传递了一个参数 HttpServletResponse,通过这个对象,我们就可以给浏览器设置响应数据 。
那上述所描述的这种浏览器/服务器的架构模式呢,我们称之为:BS架构。
• BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。
那今天呢,我们的课程内容主要就围绕着:请求、响应进行。 今天课程内容,主要包含三个部分:
- 请求
- 响应
- 分层解耦
其实之前就是servlet去写响应和请求的信息,但是后来有了tomcat就可以省略了对于请求三部分和响应三部分的解析,然后对于后端可以使用HttpServletRequest和Response去接受请求和设置响应,但是也很麻烦,最后就是框架的注解解决了。请求就通过判断@RequestBody等注解获取前端信息,然后响应就是@ResponseBody注解返回(Springboot类的RestController是一个组合注解里面就是controller和responsebody解决了这个顾虑)。
之前我们课程中有提到当前最为主流的开发模式:前后端分离
在这种模式下,前端技术人员基于”接口文档”,开发前端程序;后端技术人员也基于”接口文档”,开发后端程序。
由于前后端分离,对我们后端技术人员来讲,在开发过程中,是没有前端页面的,那我们怎么测试自己所开发的程序呢?
方式1:在浏览器中输入地址(只能测试get请求)
方式2:使用专业的接口测试工具(课程中我们使用Postman工具)
简单参数:在向服务器发起请求时,向服务器传递的是一些普通的请求数据。
那么在后端程序中,如何接收传递过来的普通参数数据呢?
我们在这里讲解两种方式:
在原始的Web程序当中,需要通过Servlet中提供的API:HttpServletRequest(请求对象),获取请求的相关信息。比如获取请求参数:
Tomcat接收到http请求时:把请求的相关信息封装到HttpServletRequest对象中
在Controller中,我们要想获取Request对象,可以直接在方法的形参中声明 HttpServletRequest 对象。然后就可以通过该对象来获取请求信息:
1 | //根据指定的参数名获取请求参数的数据值 |
1 |
|
以上这种方式,我们仅做了解。(在以后的开发中不会使用到)
在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。
1 |
|
postman测试( GET 请求):
postman测试( POST请求 ):
结论:不论是GET请求还是POST请求,对于简单参数来讲,只要保证==请求参数名和Controller方法中的形参名保持一致==,就可以获取到请求参数中的数据值。
如果方法形参名称与请求参数名称不一致,controller方法中的形参还能接收到请求参数值吗?
1 |
|
答案:运行没有报错。 controller方法中的username值为:null,age值为20
那么如果我们开发中,遇到了这种请求参数名和controller方法中的形参名不相同,怎么办?
解决方案:可以使用Spring提供的@RequestParam注解完成映射
在方法形参前面加上 @RequestParam 然后通过value属性执行请求参数名,从而完成映射。代码如下:
1 |
|
注意事项:
@RequestParam中的required属性默认为true(默认值也是true),代表该请求参数必须传递,如果不传递将报错
![]()
如果该参数是可选的,可以将required属性设置为false
1
2
3
4
5 "/simpleParam") (
public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age){
System.out.println(username+ ":" + age);
return "OK";
}
在使用简单参数做为数据传递方式时,前端传递了多少个请求参数,后端controller方法中的形参就要书写多少个。如果请求参数比较多,通过上述的方式一个参数一个参数的接收,会比较繁琐。
此时,我们可以考虑将请求参数封装到一个实体类对象中。 要想完成数据封装,需要遵守如下规则:请求参数名与实体类的属性名相同
定义POJO实体类:
1 | public class User { |
Controller方法:
1 |
|
Postman测试:
上面我们讲的呢是简单的实体对象,下面我们在来学习下复杂的实体对象。
复杂实体对象指的是,在实体类中有一个或多个属性,也是实体对象类型的。如下:
复杂实体对象的封装,需要遵守如下规则:
定义POJO实体类:
1 | public class Address { |
1 | public class User { |
Controller方法:
1 |
|
Postman测试:
数组集合参数的使用场景:在HTML的表单中,有一个表单项是支持多选的(复选框),可以提交选择的多个值。
多个值是怎么提交的呢?其实多个值也是一个一个的提交。
后端程序接收上述多个值的方式有两种:
数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数
Controller方法:
1 |
|
Postman测试:
在前端请求时,有两种传递形式:
方式一: xxxxxxxxxx?hobby=game&hobby=java
方式二:xxxxxxxxxxxxx?hobby=game,java
集合参数:请求参数名与形参集合对象名相同且请求参数为多个,@RequestParam 绑定参数关系
默认情况下,请求中参数名相同的多个值,是封装到数组。如果要封装到集合,要使用@RequestParam绑定参数关系
Controller方法:
1 |
|
Postman测试:
方式一: xxxxxxxxxx?hobby=game&hobby=java
方式二:xxxxxxxxxxxxx?hobby=game,java
上述演示的都是一些普通的参数,在一些特殊的需求中,可能会涉及到日期类型数据的封装。比如,如下需求:
因为日期的格式多种多样(如:2022-12-12 10:05:45 、2022/12/12 10:05:45),那么对于日期类型的参数在进行封装的时候,需要通过@DateTimeFormat注解,以及其pattern属性来设置日期的格式。
Controller方法:
1 |
|
Postman测试:
在学习前端技术时,我们有讲到过JSON,而在前后端进行交互时,如果是比较复杂的参数,前后端通过会使用JSON格式的数据进行传输。 (JSON是开发中最常用的前后端数据交互方式)
我们学习JSON格式参数,主要从以下两个方面着手:
Postman发送JSON格式数据:
服务端Controller方法接收JSON格式数据:
传递json格式的参数,在Controller中会使用实体类进行封装。
封装规则:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。
@RequestBody注解:将JSON数据映射到形参的实体类对象中(JSON中的key和实体类中的属性名保持一致)
实体类:Address
1 | public class Address { |
实体类:User
1 | public class User { |
Controller方法:
1 |
|
Postman测试:
传统的开发中请求参数是放在请求体(POST请求)传递或跟在URL后面通过?key=value的形式传递(GET请求)。
在现在的开发中,经常还会直接在请求的URL中传递参数。例如:
1 | http://localhost:8080/user/1 |
上述的这种传递请求参数的形式呢,我们称之为:路径参数。
学习路径参数呢,主要掌握在后端的controller方法中,如何接收路径参数。
路径参数:
Controller方法:
1 |
|
Postman测试:
传递多个路径参数:
Postman:
Controller方法:
1 |
|
前面我们学习过HTTL协议的交互方式:请求响应模式(有请求就有响应)
那么Controller程序呢,除了接收请求外,还可以进行响应。
在我们前面所编写的controller方法中,都已经设置了响应数据。
controller方法中的return的结果,怎么就可以响应给浏览器呢?
答案:使用@ResponseBody注解
@ResponseBody注解:
但是在我们所书写的Controller中,只在类上添加了@RestController注解、方法添加了@RequestMapping注解,并没有使用@ResponseBody注解,怎么给浏览器响应呢?
1 |
|
原因:在类上添加的@RestController注解,是一个组合注解。
@RestController源码:
1 | //元注解(修饰注解的注解) ({ElementType.TYPE}) |
结论:在类上添加@RestController就相当于添加了@ResponseBody注解。
下面我们来测试下响应数据:
1 |
|
在服务端响应了一个对象或者集合,那私前端获取到的数据是什么样子的呢?我们使用postman发送请求来测试下。测试效果如下:
大家有没有发现一个问题,我们在前面所编写的这些Controller方法中,返回值各种各样,没有任何的规范。
如果我们开发一个大型项目,项目中controller方法将成千上万,使用上述方式将造成整个项目难以维护。那在真实的项目开发中是什么样子的呢?
在真实的项目开发中,无论是哪种方法,我们都会定义一个统一的返回结果。方案如下:
前端:只需要按照统一格式的返回结果进行解析(仅一种解析方案),就可以拿到数据。
统一的返回结果使用类来描述,在这个结果中包含:
响应状态码:当前请求是成功,还是失败
状态码信息:给页面的提示信息
返回的数据:给前端响应的数据(字符串、对象、集合)
定义在一个实体类Result来包含以上信息。代码如下:
1 | public class Result { |
改造Controller:
1 |
|
使用Postman测试:
在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一些(单一职责原则)。
单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。
这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。
我们之前开发的程序呢,并不满足单一职责原则。下面我们来分析下之前的程序:
那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:
按照上述的三个组成部分,在我们项目开发中呢,可以将代码分为三层:
基于三层架构的程序执行流程:
思考:按照三层架构的思想,如何要对业务逻辑(Service层)进行变更,会影响到Controller层和Dao层吗?
答案:不会影响。 (程序的扩展性、维护性变得更好了)
我们使用三层架构思想,来改造下之前的程序:
三层架构的好处:
刚才我们学习过程序分层思想了,接下来呢,我们来学习下程序的解耦思想。
解耦:解除耦合。
首先需要了解软件开发涉及到的两个概念:内聚和耦合。
内聚:软件中各个功能模块内部的功能联系。
耦合:衡量软件中各个层/模块之间的依赖、关联的程度。
软件设计原则:高内聚低耦合。
高内聚指的是:一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 “高内聚”。
低耦合指的是:软件中各个层、模块之间的依赖关联程序越低越好。
程序中高内聚的体现:
程序中耦合代码的体现:
高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强。
之前我们在编写代码时,需要什么对象,就直接new一个就可以了。 这种做法呢,层与层之间代码就耦合了,当service层的实现变了之后, 我们还需要修改controller层的代码。
那应该怎么解耦呢?
我们想要实现上述解耦操作,就涉及到Spring中的两个核心概念:
控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器
依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
程序运行时需要某个资源,此时容器就为其提供这个资源。
例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象
IOC容器中创建、管理的对象,称之为:bean对象
上面我们引出了Spring中IOC和DI的基本概念,下面我们就来具体学习下IOC和DI的代码实现。
任务:完成Controller层、Service层、Dao层的代码解耦
第1步:删除Controller层、Service层中new对象的代码
第2步:Service层及Dao层的实现类,交给IOC容器管理
第3步:为Controller及Service注入运行时依赖的对象
前面我们提到IOC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象。
在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component
而Spring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:
@Controller (标注在控制层类上)
@Service (标注在业务层类上)
@Repository (标注在数据访问层类上)
要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
注解 | 说明 | 位置 |
---|---|---|
@Controller | @Component的衍生注解 | 标注在控制器类上 |
@Service | @Component的衍生注解 | 标注在业务类上 |
@Repository | @Component的衍生注解 | 标注在数据访问类上(由于与mybatis整合 –mybatis用的@Mapper) |
@Component | 声明bean的基础注解 | 不清楚属于哪一个层上就用这个 |
在IOC容器中,每一个Bean都有一个属于自己的名字,可以通过注解的value属性指定bean的名字。如果没有指定,默认为类名首字母小写。
注意事项:
- 声明bean的时候,可以通过value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
- 使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。
问题:使用前面学习的四个注解声明的bean,一定会生效吗?
答案:不一定。(原因:bean想要生效,还需要被组件扫描)
推荐做法(如下图):
依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。
默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作) –如果有多个相同类型的就会报错,就要用其他三种注解解决
使用@Primary注解:当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。
使用@Qualifier注解:指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。
使用@Resource注解:是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。
面试题 : @Autowird 与 @Resource的区别
- @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
- @Autowired 默认是按照类型注入,而@Resource是按照名称注入
- @Autowired 如果出现多个类型相同的话会出现异常(所以用其他三种注解解决)
1 | 第一种:TCP和IP协议 |
层名 | 传输的数据 | 作用 | 协议 |
---|---|---|---|
应用层 | 报文 | 为特定应用程序提供数据传输服务 | FTP(文件传输协议) DNS(域名系统) HTTP(超文本传输协议) DHCP(动态主机配置协议) TELNET(远程登录协议) SMTP() POP3() IMAP() |
传输层 | TCP报文段 UDP用户数据包 |
为进程提供通用数据传输服务 | TCP(传输控制协议) UDP(用户数据报协议) |
网络层 | IP数据包 | 为主机提供数据传输服务 | IP(网络互连协议) ARP(地址解析协议) ICMP(网际控制报文协议) IGMP(网际组管理协议) |
数据链路层 | 数据帧 | 为同一链路的主机提供数据传输服务 | PPP协议 |
1.作用:将各种数据包传送给对方
2.两个重要条件:①IP地址:指明了节点被分配到的地址(与MAC地址进行配对,可变) ②MAC地址:指网卡所属的固定地址(基本不变)
1 | 数据结构=数据+算法 |
数据:指的是一种未处理的原始文字,数字,符号、图形
信息:当数据 –处理【算法+数据结构】–> (特定的方式系统地处理、归纳甚至进行分析)
1.1 数值数据: 如0,1,2…,9所组成,可用运算符来进行运算的数据
1.2 字符数据: 如A,B,C,…,+,*等非数值型数据
2.1 **基本数据类型/标量数据类型**: 不能以其他类型来定义的数据类型。【Java中所有基本数据类型】
2.2 **结构数据类型/虚拟数据类型**: 【字符串,数组,指针,列表,文件等】
2.3 **抽象数据类型**: 可以将一种数据类型看成是一种值的集合,以及在这些值上所进行的运算和其所代表的属性所成的集合【堆栈等】
1 | 可执行程序=数据结构+算法【最关键的因素】 |
1 | 为了解决某项工作/某个问题,所需要有限数量的机械性/重复性指令与计算步骤 |
算法的五个条件:
算法的特性 | 内容与说明 |
---|---|
输入 | 0-n个输入,这些输入必须有清楚的描述或定义 |
输出 | 1-n个输出【至少有一个输出结果】 |
明确性 | 每个指令/步骤必须是简洁明确的 |
有限性 | 在有限步骤后一定会结束【不存在无限循环】 |
有效性 | 步骤清楚+可行【能让用户】 |
算法的分类:
描述工具 | 具体描述 | 举例 |
---|---|---|
伪语言 | 不能直接放入计算机执行的语言,一般需要一种特定的预处理器/人工编写转换成真正的计算机语言 | sparks,pascal-like |
表格/图形 | 清晰明了的描述过程 | 数组,树形图,矩阵图 |
流程图 | 图形符号表示法 | 流程图 |
程序设计语言 | 可读性高的高级语言 | Java,python,c# |
1 | 1.一种以概量方式来衡量运行时间 |
1 | 1.一种以概量方式来衡量所需的内存空间 |
1 | 常见的算法时间复杂度: |
四项原则
1 | 1.可读性高:阅读和理解都相当容易 |
有序表可以是空集合/(a1,a2,a3,….an-1,an),存在唯一的第一个元素a1和存在唯一的最后一个元素an。除了最后一个元素都有后继者,除了第一个元素都有先行者。
1.计算线性表的长度n
2.取出线性表中的第i项元素来修改
3.插入一个新元素到第i项,并将原来位置和后面元素都后移
4.删除第i项元素
5.遍历读取线性表元素
6.替换第i项元素
7.复制线性表
8.合并线性表
静态数据结构 | 动态数据结构 | |
---|---|---|
形式 | 密集表 | 链表 |
优点 | 使用连续分配的内存空间来存储有序表元素 设计简单,读取和修改表中任意元素时间固定O(1) |
使用不连续的内存空间来存储 数据插入和删除都方便O(1) O(n)内存分配是在程序执行时才分配,不需要事先声明,充分节省内存 |
缺点 | 删除和加入数据,需要挪动大量数据O(n) | 不能随机读取,必须按顺序遍历找到数据O(n) |
对比项目 | 动态配置 | 静态分配 |
---|---|---|
内存分配 | 运行阶段 | 编译阶段 |
内存释放 | 程序结束前必须释放分配的内存空间,否则会内存”泄露” | 不需要释放,程序结束时自动归还给系统 |
程序运行性能 | 比较低(因为所需内存要执行程序时候才分配) | 比较高(程序编译阶段就分配好需要的内存容量) |
指针遗失 | 可能会存在内存泄露(程序结束前不释放内存就指向新的内存空间,则原本指向的内存空间就无法被释放) | 没有此问题 |
1.插入到第一节点前,成为新的链接头部
2.插入到中间节点部分
Java程序中,所有的对象都有两种类型:编译时类型
和运行时类型
,而很多时候对象的编译时类型和运行时类型不一致
。
1 | Object obj = new String("hello"); |
例如:某些变量或形参的声明类型是Object类型,但是程序却需要调用该对象运行时类型的方法,该方法不是Object中的方法,那么如何解决呢?
解决这个问题,有两种方案:
1.方案1(向下转型):在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可以直接先使用instanceof
运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量即可。
2.方案2(反射):编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。
Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间
借助Reflection API取得任何类的内部信息,能直接操作任意对象的内部属性及方法。
加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。
从内存加载上看反射:
Java反射机制提供的功能:
1 | java.lang.Class:代表一个类 |
优点:
提高了Java程序的灵活性和扩展性,降低了耦合性
,提高自适应
能力
允许程序创建和控制任何类的对象,无需提前硬编码
目标类
缺点:
反射的性能较低
。
反射会模糊
程序内部逻辑,可读性较差
。
要想解剖
一个类,必须先要获取到该类的Class对象。而剖析一个类/用反射解决具体的问题就是使用相关API:
所以,Class对象是反射的根源。
在Object类中定义了以下的方法,此方法将被所有子类继承:
1 | public final Class getClass() |
以上的方法返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称。
对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。
对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。
说明:字符串常量池在JDK6中,存储在方法区;
字符串常量池在JDK7及以后,存储在堆空间。
前提:若已知具体的类,通过类的class属性获取,该方法最为安全可靠,程序性能最高
实例:
1 | Class clazz = String.class; |
前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象
实例:
1 | Class clazz = "www.atguigu.com".getClass(); |
前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException
实例:
1 | Class clazz = Class.forName("java.lang.String"); |
前提:可以用系统类加载对象或自定义加载器对象加载指定路径下的类型
实例:
1 | ClassLoader cl = this.getClass().getClassLoader(); |
再举例:
1 | public class GetClassObject { |
简言之,所有Java类型!
(1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
(2)interface:接口
(3)[]:数组
(4)enum:枚举
(5)annotation:注解@interface
(6)primitive type:基本数据类型
(7)void
举例:
1 | Class c1 = Object.class; |
方法名 | 功能说明 |
---|---|
static Class forName(String name) | 返回指定类名name的Class 对象 |
Object newInstance() | 调用缺省构造函数,返回该Class对象的一个实例 |
getName() | 返回此Class对象表示的实体(类/接口/数组类/基本类型/void)名称 |
Class getSuperClass() | 返回当前Class对象的父类的Class对象 |
Class [] getInterfaces() | 获取当前Class对象的接口 |
ClassLoader getClassLoader() | 返回该类的类加载器 |
Class getSuperclass() | 返回表示此Class所表示的实体的超类的Class |
Constructor[] getConstructors() | 返回一个包含某些Constructor对象的数组 |
Field[] getDeclaredFields() | 返回Field对象的一个数组 |
Method getMethod(String name,Class … paramTypes) | 返回一个Method对象,此对象的形参类型为paramType |
举例:
1 | String str = "test4.Person"; |
类在内存中完整的生命周期:加载–>使用–>卸载。其中加载过程又分为:装载、链接、初始化三个阶段。
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、链接、初始化三个步骤来对该类进行初始化。
如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。
类的加载又分为三个阶段:
(1)装载(Loading)
将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成
(2)链接(Linking)
验证Verify:确保加载的类信息符合JVM规范,例如:以cafebabe开头,没有安全方面的问题。
准备Prepare:正式为类变量(static)分配内存并设置类变量默认初始值
的阶段,这些内存都将在方法区中进行分配。
解析Resolve:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
(3)初始化(Initialization)
执行类构造器<clinit>()方法
的过程。类构造器<clinit>()方法
是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
虚拟机会保证一个类的<clinit>()方法
在多线程环境中被正确加锁和同步。
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)
和自定义类加载器(User-Defined ClassLoader)
。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:
(1)启动类加载器(引导类加载器,Bootstrap ClassLoader)
C/C++语言
实现的,嵌套在JVM内部。获取它的对象时往往返回null(2)扩展类加载器(Extension ClassLoader)
(3)应用程序类加载器(系统类加载器,AppClassLoader)
(4)用户自定义类加载器(了解)
应用隔离
,例如 Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。(1)获取默认的系统类加载器
1 | ClassLoader classloader = ClassLoader.getSystemClassLoader(); |
(2)查看某个类是哪个类加载器加载的
1 | ClassLoader classloader = Class.forName("exer2.ClassloaderDemo").getClassLoader(); |
(3)获取某个类加载器的父加载器
1 | ClassLoader parentClassloader = classloader.getParent(); |
示例代码:
1 | package com.atguigu.loader; |
关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的指定文件的输入流
1 | InputStream in = null; |
举例:
1 | //需要掌握如下的代码 |
有了Class对象,能做什么?
这是反射机制应用最多的地方。创建运行时类的对象有两种方式:
方式1:直接调用Class对象的newInstance()方法
要 求: 1)类必须有一个无参数的构造器。2)类的构造器的访问权限需要足够。
方式2:通过获取构造器对象来进行实例化
方式一的步骤:
1)获取该类型的Class对象 2)调用Class对象的newInstance()方法创建对象
方式二的步骤:
1)通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类型的构造器
2)向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
3)通过Constructor实例化对象。
如果构造器的权限修饰符修饰的范围不可见,也可以调用setAccessible(true)
示例代码:
1 | package com.atguigu.reflect; |
可以获取:包、修饰符、类型名、父类(包括泛型父类)、父接口(包括泛型父接口)、成员(属性、构造器、方法)、注解(类上的、方法上的、属性上的)。
1 | //1.实现的全部接口 |
1 | package com.atguigu.java2; |
1 | package com.atguigu.java2; |
1 | package com.atguigu.java2; |
示例代码获取泛型父类信息:
1 | /* Type: |
public Class<?>[] getClasses():返回所有公共内部类和内部接口。包括从超类继承的公共类和接口成员以及该类声明的公共类和接口成员。
public Class<?>[] getDeclaredClasses():返回 Class 对象的一个数组,这些对象反映声明为此 Class 对象所表示的类的成员的所有类和接口。包括该类所声明的公共、保护、默认(包)访问及私有类和接口,但不包括继承的类和接口。
public Class<?> getDeclaringClass():如果此 Class 对象所表示的类或接口是一个内部类或内部接口,则返回它的外部类或外部接口,否则返回null。
Class<?> getEnclosingClass() :返回某个内部类的外部类
1 |
|
在实际的操作中,取得类的信息的操作代码,并不会经常开发。
一定要熟悉java.lang.reflect包的作用,反射机制。
在反射机制中,可以直接通过Field类操作类中的属性,通过Field类提供的set()和get()方法就可以完成设置和取得属性内容的操作。
(1)获取该类型的Class对象
Class clazz = Class.forName(“包.类名”);
(2)获取属性对象
Field field = clazz.getDeclaredField(“属性名”);
(3)如果属性的权限修饰符不是public,那么需要设置属性可访问
field.setAccessible(true);
(4)创建实例对象:如果操作的是非静态属性,需要创建实例对象
Object obj = clazz.newInstance(); //有公共的无参构造
Object obj = 构造器对象.newInstance(实参…);//通过特定构造器对象创建实例对象
(4)设置指定对象obj上此Field的属性内容
field.set(obj,”属性值”);
如果操作静态变量,那么实例对象可以省略,用null表示
(5)取得指定对象obj上此Field的属性内容
Object value = field.get(obj);
如果操作静态变量,那么实例对象可以省略,用null表示
示例代码:
1 | public class Student { |
1 | package com.atguigu.reflect; |
关于setAccessible方法的使用:
(1)获取该类型的Class对象
Class clazz = Class.forName(“包.类名”);
(2)获取方法对象
Method method = clazz.getDeclaredMethod(“方法名”,方法的形参类型列表);
(3)创建实例对象
Object obj = clazz.newInstance();
(4)调用方法
Object result = method.invoke(obj, 方法的实参值列表);
如果方法的权限修饰符修饰的范围不可见,也可以调用setAccessible(true)
如果方法是静态方法,实例对象也可以省略,用null代替
示例代码:
1 | import org.junit.Test; |
读取user.properties文件中的数据,通过反射完成User类对象的创建及对应方法的调用。
配置文件:user.properties
1 | className:com.atguigu.bean.User |
User.java文件:
1 | package com.atguigu.bean; |
ReflectTest.java文件:
1 | package com.atguigu.java4; |
一个完整的注解应该包含三个部分:
(1)声明
(2)使用
(3)读取
1 | import java.lang.annotation.*; |
1 | import java.lang.annotation.*; |
1 | "t_stu") ( |
自定义注解必须配上注解的信息处理流程才有意义。
我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是RetentionPolicy.RUNTIME。
1 | import java.lang.reflect.Field; |
体会1:
1 | public class ReflectionTest { |
体会2:
1 | public class ReflectionTest { |
体会3:
1 | public class ReflectionTest { |
其中,配置文件【config.properties】存放在当前Module的src下
1 | com.atguigu.java1.Orange |
网络把主机连接起来,而互连网(internet)是把多种不同的网络连接起来,因此互连网是网络的网络。而互联网(Internet)是全球范围的互连网
电路交换
电路交换用于电话通信系统,两个用户要通信之前需要建立一条专用的物理链路,并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路,因此电路交换对线路的利用率很低,往往不到 10%
分组交换
每个分组都有首部和尾部,包含了源地址和目的地址等控制信息,在同一个传输线路上同时传输多个分组互相不会影响,因此在同一条传输线路上允许同时传输多个分组,也就是说分组交换不需要占用传输线路。
在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。
四种时延 | 定义 | 进一步解释 |
---|---|---|
排队时延 | 分组在路由器的输入队列和输出队列中排队等待的时间 | 取决于网络当前的通信量 |
处理时延 | 主机/路由器收到分组时进行处理需要的时间 | 分析首部、从分组中提取数据、差错检验、查找适合的路由 |
传输时延 | 主机/路由器传输数据帧所需要的时间 | ![]() |
传播时延 | 电磁波在信道中传播所需要的时间 | ![]() |
应用层 :为特定应用程序提供数据传输服务,例如 HTTP、DNS 等协议。数据单位为报文。
传输层 :为进程提供通用数据传输服务。由于应用层协议很多,定义通用的传输层协议就可以支持不断增多的应用层协议。
运输层包括两种协议:(TCP 主要提供完整性服务,UDP 主要提供及时性服务。)
1.传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;
2.用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。
网络层 :为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成分组。
数据链路层 :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。
物理层 :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。
层名 | 传输的数据 | 作用 | 协议 |
---|---|---|---|
应用层 | 报文 | 为特定应用程序提供数据传输服务 | FTP(文件传输协议) DNS(域名系统) HTTP(超文本传输协议) DHCP(动态主机配置协议) TELNET(远程登录协议) SMTP() POP3() IMAP() |
传输层 | TCP报文段 UDP用户数据包 |
为进程提供通用数据传输服务 | TCP(传输控制协议) UDP(用户数据报协议) |
网络层 | IP数据包 | 为主机提供数据传输服务 | IP(网络互连协议) ARP(地址解析协议) ICMP(网际控制报文协议) IGMP(网际组管理协议) |
数据链路层 | 数据帧 | 为同一链路的主机提供数据传输服务 | PPP协议 |
物理层 | 数据比特流 | 虑的是怎样在传输媒体上传输数据比特流 |
其中表示层和会话层用途如下:
五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。
其中表示层和会话层用途如下:
五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理
在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。
路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要传输层和应用层。
根据信息在传输线上的传送方向,分为以下三种通信方式:
将网络层传下来的分组添加首部和尾部(向下封装),用于标记帧的开始和结束
帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符。如果数据部分出现转义字符,那么就在转义字符前面再加个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在
目前数据链路层广泛使用循环冗余检验(CRC)来检查比特差错
形式 | 具体细节 | 控制方式 | |
---|---|---|---|
广播信道 | 一对多 | 一个节点发送的数据能够被广播信道上所有的节点接收到 【所以节点在同一个广播信道上发送数据,需要专门的控制方法进行协调,避免发生冲突】 |
1.信道复用技术 2.CSMA/CD协议 |
点对点信道 | 一对一 | 一个节点发送的数据只能被对应节点接收到 【不会发生碰撞,比较简单】 |
PPP协议 |
定义 | 图形 | |
---|---|---|
频分复用 | 所有主机在相同的时间占用不同的频率带宽资源 | ![]() |
时分复用 | 所有主机在不同的时间占用相同的频率带宽资源 | ![]() |
统计时分复用 | 对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后发送 | ![]() |
波分复用 | 光的频分复用 【由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波】 |
![]() |
码分复用 | 当码分复用信道为多个不同地址的用户所共享时 | ![]() |
记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 争用期 。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。
当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 截断二进制指数退避算法 来确定。从离散的整数集合 {0, 1, .., (2k-1)} 中随机取出一个数,记作 r,然后取 r 倍的争用期作为重传等待时间。
互联网用户通常需要连接到某个 ISP 之后才能接入到互联网,PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议
其中星型拓扑结构局域网—-以太网
早期,使用集线器进行连接,集线器是一种物理层设备, 作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线器同时收到两个不同接口的帧,那么就发生了碰撞。
目前,使用交换机替代了集线器,交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。
以太网帧格式:
类型 :标记上层使用的协议;
数据 :长度在 46-1500 之间,如果太小则需要填充;
FCS :帧检验序列,使用的是 CRC 检验方法;
交换机学习交换表内容(存储MAC地址到接口的映射)
由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。
下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B,先查交换表,此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧,主机 B 回应该帧向主机 A 发送数据包时,交换机查找交换表得到主机 A 映射的接口为 1,就发送数据帧到接口 1,同时交换机添加主机 B 到接口 2 的映射
虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。
例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。
使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊接口被设置为干线接口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网
网络层是整个互联网的核心,因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大努力交互的数据报服务
使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络
由两部分组成,网络号和主机号,其中不同分类具有不同的网络号长度,并且是固定的。
IP 地址 ::= {< 网络号 >, < 主机号 >}
通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。
IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >}
使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000,也就是 255.255.192.0。
注意,外部网络看不到子网的存在。
无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。
IP 地址 ::= {< 网络前缀号 >, < 主机号 >}
CIDR 的记法:IP 地址+网络前缀长度,例如 128.14.35.7/20 表示前 20 位为网络前缀。
CIDR 的地址掩码:子网掩码,子网掩码首 1 长度为网络前缀的长度。
一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为 构成超网 。
在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配来确定应该匹配哪一个。
网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。
ARP 实现: IP 地址 –> MAC 地址
每个主机都有一个 ARP 高速缓存(局域网上的各主机和路由器的 IP 地址到 MAC 地址)映射表。
如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。
ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会
ICMP封装在 IP 数据报中,但是不属于高层协议
ICMP 报文分为差错报告报文和询问报文
1.作用:用来测试两台主机之间的连通性
2.Ping 的原理:是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率
具体步骤:
1.Win+R快捷键输入cmd
2.输入ping ip地址
1.作用:用来跟踪一个分组从源点到终点的路径
Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。
由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数 << 本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)
有三个专用地址块:
VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指好像是,而实际上并不是,它有经过公用的互联网。
专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP –> 全球 IP。
在以前,NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把传输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT
功能划分 | 组成 |
---|---|
路由选择 | 内部网关协议 RIP 内部网关协议 OSPF 外部网关协议 BGP |
分组转发 | 交换结构 一组输入端口 一组输出端口 |
路由选择协议都是自适应的,能随着网络通信量和拓扑结构的变化而自适应地进行调整。
互联网可以划分为许多较小的自治系统 AS,一个 AS 可以使用一种和别的 AS 不同的路由选择协议。
可以把路由选择协议划分为两大类:
RIP 是一种基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1。跳数最多为 15,超过 15 表示不可达。
RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。
距离向量算法:
RIP 协议实现简单,开销小。但是 RIP 能使用的最大距离为 15,限制了网络的规模。并且当网络出现故障时,要经过比较长的时间才能将此消息传送到所有路由器。
开放最短路径优先 OSPF,是为了克服 RIP 的缺点而开发出来的。
开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法 SPF。
OSPF 具有以下特点:
所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIP,OSPF 的更新过程收敛的很快。
BGP(Border Gateway Protocol,边界网关协议)
AS 之间的路由选择很困难,主要是由于:
BGP 只能寻找一条比较好的路由,而不是最佳路由。
每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。
网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。传输层提供了进程间的逻辑通信,传输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个传输层实体之间有一条端到端的逻辑通信信道。
传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。
TCP首部格式:
用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。
UDP首部格式:
1 | 假设 A 为客户端,B 为服务器端 |
第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。
1 | 以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 |
第四次挥手是因为客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态(这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文)
其中TIME_WAIT状态:
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:
TCP使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。
一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下:
其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响。
超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下:
其中 RTTd 为偏差的加权平均值。
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复
其中,快恢复和慢开始的区别就是慢开始的时候cwnd变为1重新开始,而快恢复是直接到cwnd=ssthresh,直接进入拥塞避免阶段
DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。
其中,域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。
DNS 可以使用 UDP(大多数情况) / TCP 进行传输,使用的端口号都为 53。这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用 TCP 进行传输:
FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件:
根据数据连接是否是服务器端主动建立 –> FTP有主动模式和被动模式:
DHCP提供了即插即用的连网方式,用户不再需要手动配置 IP 地址(子网掩码,网关IP地址)
DHCP 工作过程如下:
TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。
TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。
一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件协议。
三个部分 | 分类 | 举例 |
---|---|---|
用户代理 | ||
邮件服务器 | ||
邮件协议 | 发送协议和读取协议 | 1.发送协议:SMTP 2.读取协议:POP3和IMAP |
SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。
只要用户从服务器上读取了邮件,就把该邮件删除。
IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。
Java是 Internet 上的语言,它从语言级上提供了对网络应用程序的支持,程序员能够很容易开发常见的网络应用程序。
Java提供的网络类库,可以实现无痛的网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由 JVM 控制。并且 Java 实现了一个跨平台的网络库,程序员面对的是一个统一的网络编程环境
。
B/S架构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有IE、谷歌、火狐等。
两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序。
计算机网络:
把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息、共享硬件、软件、数据信息等资源。
网络编程的目的:直接/间接地通过网络协议与其它计算机实现数据交换,进行通讯。
网络编程中有三个主要的问题:
生活类比:
IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给网络中的一台计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。
IP地址分类方式一:
IPv4
:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d
的形式,以点分十进制
表示,例如192.168.65.100
。其中a、b、c、d都是0~255之间的十进制整数。
这种方式最多可以表示42亿个。其中,30亿都在北美,亚洲4亿,中国2.9亿。2011年初已经用尽。
IP地址 = 网络地址 +主机地址
主机地址:标识特定主机或网络设备
其中,E类用于科研。
IPv6
:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。
为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,共16个字节,写成8个无符号整数,每个整数用四个十六进制位表示,数之间用冒号(:)分开。
比如:ABCD:EF01:2345:6789:ABCD:EF01:2345:6789
,按保守方法估算IPv6实际可分配的地址,整个地球的每平方米面积上仍可 分配1000多个地址,这样就解决了网络地址资源数量不够的问题。2012年6月6日,国际互联网协会举行了世界IPv6启动纪念日,这一天,全球IPv6网络正式启动。多家知名网站,如Google、Facebook和Yahoo等,于当天全球标准时间0点(北京时间8点整)开始永久性支持IPv6访问。2018年6月,三大运营商联合阿里云宣布,将全面对外提供IPv6服务,并计划在2025年前助推中国互联网真正实现“IPv6 Only”。
在IPv6的设计过程中除了一劳永逸地解决了地址短缺问题以外,还考虑了在IPv4中解决不好的其它问题,主要有端到端IP连接、服务质量(QoS)、安全性、多播、移动性、即插即用等。
IP地址分类方式二:
公网地址( 万维网使用)和 私有地址( 局域网使用)。192.168.开头的就是私有地址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用。
常用命令:
1 | ipconfig |
1 | ping 空格 IP地址 |
特殊的IP地址:
127.0.0.1
localhost
Internet上的主机有两种方式表示地址:
域名解析:因为IP地址数字不便于记忆,因此出现了域名。域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS,Domain Name System,域名系统)负责将域名转化成IP地址,这样才能和主机建立连接。
简单理解:
详细理解:
hosts文件
是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。本地DNS解析器缓存
,是否有这个网址映射关系,如果有,直接返回,完成域名解析。本地DNS服务器
,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。缓存
了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)。
不同的进程,设置不同的端口号。
如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败。
通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。
网络通信协议
:在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤、出错控制等做了统一规定,通信双方必须同时遵守才能完成数据交换。新的问题:网络协议涉及内容太多、太复杂。如何解决?
计算机网络通信涉及内容很多,比如指定源地址和目标地址,加密解密,压缩解压缩,差错控制,流量控制,路由控制,如何实现如此复杂的网络协议呢?通信协议分层思想
。
在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一层,而与再下一层不发生关系
。各层互不影响,利于系统的开发和扩展。
这里有两套参考模型
上图中,OSI参考模型:模型过于理想化
,未能在因特网上进行广泛推广。 TCP/IP参考模型(或TCP/IP协议):事实上的国际标准
。
TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),TCP/IP 以其两个主要协议:传输控制协议(TCP)和网络互联协议(IP)而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。是Internet最基本、最广泛的协议。
TCP/IP协议中的四层介绍:
应用层
:应用层决定了向用户提供应用服务时通信的活动。主要协议有:HTTP协议、FTP协议、SNMP(简单网络管理协议)、SMTP(简单邮件传输协议)和POP3(Post Office Protocol 3的简称,即邮局协议的第3个版)等。传输层
:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。TCP(Transmission Control Protocol)协议,即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。UDP(User Datagram Protocol,用户数据报协议):是一个无连接的传输层协议、提供面向事务的简单不可靠的信息传送服务。网络层
:网络层是整个TCP/IP协议的核心,支持网间互连的数据通信。它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。而IP协议是一种非常重要的协议。IP(internet protocal)又称为互联网协议。IP的责任就是把数据从源传送到目的地。它在源地址和目的地址之间传送一种称之为数据包的东西,它还提供对数据大小的重新组装功能,以适应不同网络对包大小的要求。物理+数据链路层
:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。通信的协议还是比较复杂的,java.net
包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。
java.net
包中提供了两种常见的网络协议的支持:
TCP协议:
建立TCP连接
,形成基于字节流的传输数据通道可靠的
重发机制
,当一个通信实体发送一个消息给另一个通信实体后,需要收到另一个通信实体确认信息,如果没有收到另一个通信实体确认信息,则会再次重复刚才发送的消息。大数据量的传输
释放已建立的连接,效率低
UDP协议:
不需要建立连接
不可靠的
64K
内无需释放资源,开销小,通信效率高
TCP生活案例:打电话
UDP生活案例:发送短信、发电报
TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。
1、客户端会随机一个初始序列号seq=x,设置SYN=1 ,表示这是SYN握手报文。然后就可以把这个 SYN 报文发送给服务端了,表示向服务端发起连接,之后客户端处于
同步已发送
状态。2、服务端收到客户端的 SYN 报文后,也随机一个初始序列号(seq=y),设置ack=x+1,表示收到了客户端的x之前的数据,希望客户端下次发送的数据从x+1开始。
设置 SYN=1 和 ACK=1。表示这是一个SYN握手和ACK确认应答报文。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于同步已接收
状态。3、客户端收到服务端报文后,还要向服务端回应最后一个应答报文,将ACK置为 1 ,表示这是一个应答报文
ack=y+1 ,表示收到了服务器的y之前的数据,希望服务器下次发送的数据从y+1开始。
最后把报文发送给服务端,这次报文可以携带数据,之后客户端处于 连接已建立 状态。服务器收到客户端的应答报文后,也进入连接已建立
状态。
完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。
TCP协议中,在发送数据结束后,释放连接时需要经过四次挥手。
让服务器做最后的准备工作
。此时,客户端处于半关闭状态,即表示不再向服务器发送数据了,但是还可以接受数据。会将最后的数据发给客户端
。并告知上层的应用进程不再接收数据。发送一个释放连接的报文
。那么客户端接收后就知道可以正式释放连接了。回复一个彻底断开的报文
。这样服务器收到后才会彻底释放连接。这里客户端,发送完最后的报文后,会等待2MSL,因为有可能服务器没有收到最后的报文,那么服务器迟迟没收到,就会再次给客户端发送释放连接的报文,此时客户端在等待时间范围内接收到,会重新发送最后的报文,并重新计时。如果等待2MSL后,没有收到,那么彻底断开。1、客户端打算断开连接,向服务器发送FIN报文(FIN标记位被设置为1,1表示为FIN,0表示不是),FIN报文中会指定一个序列号,之后客户端进入FIN_WAIT_1状态。也就是客户端发出连接释放报文段(FIN报文),指定序列号seq = u,主动关闭TCP连接,等待服务器的确认。
2、服务器收到连接释放报文段(FIN报文)后,就向客户端发送ACK应答报文,以客户端的FIN报文的序列号 seq+1 作为ACK应答报文段的确认序列号ack = seq+1 = u + 1。接着服务器进入CLOSE_WAIT(等待关闭)状态,此时的TCP处于半关闭状态(下面会说什么是半关闭状态),客户端到服务器的连接释放。客户端收到来自服务器的ACK应答报文段后,进入FIN_WAIT_2状态。
3、服务器也打算断开连接,向客户端发送连接释放(FIN)报文段,之后服务器进入LASK_ACK(最后确认)状态,等待客户端的确认。服务器的连接释放(FIN)报文段的FIN=1,ACK=1,序列号seq=m,确认序列号ack=u+1。
4、客户端收到来自服务器的连接释放(FIN)报文段后,会向服务器发送一个ACK应答报文段,以连接释放(FIN)报文段的确认序号 ack 作为ACK应答报文段的序列号 seq,以连接释放(FIN)报文段的序列号 seq+1作为确认序号ack。
之后客户端进入TIME_WAIT(时间等待)状态,服务器收到ACK应答报文段后,服务器就进入CLOSE(关闭)状态,到此服务器的连接已经完成关闭。客户端处于TIME_WAIT状态时,此时的TCP还未释放掉,需要等待2MSL后,客户端才进入CLOSE状态。
InetAddress类主要表示IP地址,两个子类:Inet4Address、Inet6Address。
InetAddress 类没有提供公共的构造器,而是提供如下几个静态方法来获取InetAddress 实例
InetAddress 提供了如下几个常用的方法
1 | import java.net.InetAddress; |
通信的两端都要有Socket,是两台机器间通信的端点。
Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。
Socket分类:
ServerSocket类的构造方法:
ServerSocket类的常用方法:
Socket类的常用构造方法:
Socket类的常用方法:
注意:先后调用Socket的shutdownInput()和shutdownOutput()方法,仅仅关闭了输入流和输出流,并不等于调用Socket的close()方法。在通信结束后,仍然要调用Scoket的close()方法,因为只有该方法才会释放Socket占用的资源,比如占用的本地端口号等。
DatagramSocket 类的常用方法:
DatagramPacket类的常用方法:
public byte[] getData()
返回数据缓冲区。接收到的或将要发送的数据从缓冲区中的偏移量 offset 处开始,持续 length 长度。public int getLength()
返回将要发送或接收到的数据的长度。Java语言的基于套接字TCP编程分为服务端编程和客户端编程,其通信模型如图所示:
客户端程序包含以下四个基本的步骤 :
服务器端程序包含以下四个基本的 步骤:
例题1:
1 | import org.junit.Test; |
例题2:客户端发送文件给服务端,服务端将文件保存在本地。
1 | package net; |
例题3:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端。并关闭相应的连接。
1 | package net; |
练习1:服务端读取图片并发送给客户端,客户端保存图片到本地
练习2:客户端给服务端发送文本,服务端会将文本转成大写在返回给客户端。
演示单个客户端与服务器单次通信:
需求:客户端连接服务器,连接成功后给服务发送“lalala”,服务器收到消息后,给客户端返回“欢迎登录”,客户端接收消息后,断开连接
1、服务器端示例代码
1 | import java.io.InputStream; |
2、客户端示例代码
1 | import java.io.InputStream; |
演示多个客户端与服务器之间的多次通信:
通常情况下,服务器不应该只接受一个客户端请求,而应该不断地接受来自客户端的所有请求,所以Java程序通常会通过循环,不断地调用ServerSocket的accept()方法。
如果服务器端要“同时”处理多个客户端的请求,因此服务器端需要为每一个客户端单独分配一个线程来处理,否则无法实现“同时”。
咱们之前学习IO流的时候,提到过装饰者设计模式,该设计使得不管底层IO流是怎样的节点流:文件流也好,网络Socket产生的流也好,程序都可以将其包装成处理流,甚至可以多层包装,从而提供更多方便的处理。
案例需求:多个客户端连接服务器,并进行多次通信
1、服务器端示例代码
1 | import java.io.BufferedReader; |
2、客户端示例代码
1 | import java.io.BufferedReader; |
服务端:
1 | import java.io.BufferedReader; |
客户端:
1 | import java.io.IOException; |
客户端:
服务端:
UDP(User Datagram Protocol,用户数据报协议):是一个无连接的传输层协议、提供面向事务的简单不可靠的信息传送服务,类似于短信。
UDP协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送,至于对方是否可以接收到这些数据内容,UDP协议无法控制,因此说,UDP协议是一种不可靠的协议。无连接的好处就是快,省内存空间和流量,因为维护连接需要创建大量的数据结构。UDP会尽最大努力交付数据,但不保证可靠交付,没有TCP的确认机制、重传机制,如果因为网络原因没有传送到对端,UDP也不会给应用层返回错误信息。
UDP协议是面向数据报文的信息传送服务。UDP在发送端没有缓冲区,对于应用层交付下来的报文在添加了首部之后就直接交付于ip层,不会进行合并,也不会进行拆分,而是一次交付一个完整的报文。比如我们要发送100个字节的报文,我们调用一次send()方法就会发送100字节,接收方也需要用receive()方法一次性接收100字节,不能使用循环每次获取10个字节,获取十次这样的做法。
UDP协议没有拥塞控制,所以当网络出现的拥塞不会导致主机发送数据的速率降低。虽然UDP的接收端有缓冲区,但是这个缓冲区只负责接收,并不会保证UDP报文的到达顺序是否和发送的顺序一致。因为网络传输的时候,由于网络拥塞的存在是很大的可能导致先发的报文比后发的报文晚到达。如果此时缓冲区满了,后面到达的报文将直接被丢弃。这个对实时应用来说很重要,比如:视频通话、直播等应用。
因此UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境,数据报大小限制在64K以下。
类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序。
UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证 UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。
DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号。
UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。如同发快递包裹一样。
发送端程序包含以下四个基本的步骤:
接收端程序包含以下四个基本的步骤 :
基于UDP协议的网络编程仍然需要在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送、接收数据报的对象,Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表DatagramSocket发送、接收的数据报。
发送端:
1 | DatagramSocket ds = null; |
接收端:
1 | DatagramSocket ds = null; |
发送端:
1 | package com.atguigu.udp; |
接收端:
1 | package com.atguigu.udp; |
URL(Uniform Resource Locator):统一资源定位符,它表示 Internet 上某一资源的地址。
通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp 站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源。
URL的基本结构由5部分组成:
1 | <传输协议>://<主机名>:<端口号>/<文件名>#片段名?参数列表 |
例如: http://192.168.1.100:8080/helloworld/index.jsp#a?username=shkstart&password=123
为了表示URL,java.net 中实现了类 URL。我们可以通过下面的构造器来初始化一个 URL 对象:
public URL (String spec):通过一个表示URL地址的字符串可以构造一个URL对象。例如:
1 | URL url = new URL("http://www. atguigu.com/"); |
public URL(URL context, String spec):通过基 URL 和相对 URL 构造一个 URL 对象。例如:
1 | URL downloadUrl = new URL(url, “download.html") |
public URL(String protocol, String host, String file); 例如:
1 | URL url = new URL("http", "www.atguigu.com", “download. html"); |
public URL(String protocol, String host, int port, String file); 例如:
1 | URL gamelan = new URL("http", "www.atguigu.com", 80, “download.html"); |
URL类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通常是用 try-catch 语句进行捕获。
一个URL对象生成后,其属性是不能被改变的,但可以通过它给定的方法来获取这些属性:
public String getProtocol( ) 获取该URL的协议名
public String getHost( ) 获取该URL的主机名
public String getPort( ) 获取该URL的端口号
public String getPath( ) 获取该URL的文件路径
public String getFile( ) 获取该URL的文件名
public String getQuery( ) 获取该URL的查询名
1 | import java.net.MalformedURLException; |
URL的方法 openStream():能从网络上读取数据
若希望输出数据,例如向服务器端的 CGI (公共网关接口-Common Gateway Interface-的简称,是用户浏览器和服务器端的应用程序进行连接的接口)程序发送一些数据,则必须先与URL建立连接,然后才能对其进行读写,此时需要使用 URLConnection 。
URLConnection:表示到URL所引用的远程对象的连接。当与一个URL建立连接时,首先要在一个 URL 对象上通过方法 openConnection() 生成对应的 URLConnection 对象。如果连接过程失败,将产生IOException.
String str="http://raw.githubusercontent.com/.images/202311031512799.png";
URL url=new URL(str);
HttpURLConnection urlConnection= (HttpURLConnection) url.openConnection();
1
2
3
4
5
6
7
8
9
10
- 通过URLConnection对象获取的输入流和输出流,即可以与现有的CGI程序进行交互。
- public Object getContent( ) throws IOException
- public int getContentLength( )
- public String getContentType( )
- public long getDate( )
- public long getLastModified( )
- public InputStream getInputStream ( ) throws IOException
- public OutputSteram getOutputStream( )throws IOException
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.io.*;
public class UrlTestDown {
public static void main(String[] args) throws IOException {
//将URL代表的资源下载到本地
//1.获取URL实例
String str="http://raw.githubusercontent.com/Larkkkkkkk/hexo-picture/main/.images/202311031512799.png";
URL url=new URL(str);
//2.建立与服务器端的连接
HttpURLConnection urlConnection= (HttpURLConnection) url.openConnection();
//3.获取输入流(从服务器得到)、创建输出流(存储到本地)
InputStream is = urlConnection.getInputStream();
File file=new File("hahha.png");
FileOutputStream fos=new FileOutputStream(file);
//4.读写数据
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
System.out.println("文件下载完成");
//5.关闭资源
fos.close();
is.close();
urlConnection.disconnect(); //还比较特殊
}
}
public File(String pathname)
:以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储。public File(String parent, String child)
:以parent为父路径,child为子路径创建File对象。public File(File parent, String child)
:根据一个父File对象和子文件路径创建File对象关于路径:
项目目录
的路径,这是一个便捷的路径,开发中经常使用。当前工程project
“当前模块module
“举例:
1 | import java.io.File; |
注意:
无论该路径下是否存在文件或者目录,都不影响File对象的创建。
window的路径分隔符使用“\”,而Java程序中的“\”表示转义字符,所以在Windows中表示路径,需要用“\”。或者直接使用“/”也可以,Java程序支持将“/”当成平台无关的
路径分隔符
。或者直接使用File.separator常量值表示。比如:File file2 = new File(“d:” + File.separator + “atguigu” + File.separator + “info.txt”);
当构造路径是绝对路径时,那么getPath和getAbsolutePath结果一样
当构造路径是相对路径时,那么getAbsolutePath的路径 = user.dir的路径 + 构造路径
public String getAbsolutePath()
:获取绝对路径public String getParent()
:获取上层文件目录路径。若无,返回null如果File对象代表的文件或目录存在,则File对象实例初始化时,就会用硬盘中对应文件或目录的属性信息(例如,时间、类型等)为File对象的属性赋值,否则除了路径和名称,File对象的其他属性将会保留默认值。
举例:
1 |
|
1 |
|
public boolean exists()
:此File表示的文件或目录是否实际存在。public boolean isDirectory()
:此File表示的是否为目录。public boolean isFile()
:此File表示的是否为文件。举例:
1 |
|
如果文件或目录不存在,那么exists()、isFile()和isDirectory()都是返回true
public boolean createNewFile()
:创建文件。若文件存在,则不创建,返回false。public boolean mkdir()
:创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。public boolean mkdirs()
:创建文件目录。如果上层文件目录不存在,一并创建。public boolean delete()
:删除文件或者文件夹举例:
1 | import java.io.File; |
1 | 运行结果: |
API中说明:delete方法,如果此File表示目录,则目录必须为空才能删除。
练习1:利用File构造器,new 一个文件目录file
1) 在其中创建多个文件和目录
2) 编写方法,实现删除file中指定文件的操作
1 | @Test |
练习2:判断指定目录下是否有后缀名为.jpg的文件。如果有,就输出该文件名称
1 | public class FindJPGFileTest { |
练习3:遍历指定目录所有文件名称,包括子文件目录中的文件。
拓展1:并计算指定目录占用空间的大小
拓展2:删除指定文件目录及其下的所有文件
1 | public class ListFilesTest { |
Java程序中,数据的输入/输出操作以“流(stream)
” 的方式进行,可以看做是一种数据的流动。
I/O流中的I/O是Input/Output
的缩写, I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。
输入input
:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。输出output
:将程序(内存)数据输出到磁盘、光盘等存储设备中。java.io
包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法
输入或输出数据。
按数据的流向不同 —-> :输入流和输出流。
其他设备
上读取到内存
中的流。 内存
中写出到其他设备
上的流。按操作数据单位的不同 ** —-> :字节流(8bit)和字符流(16bit)**。
根据IO流的角色不同 —-> :节点流和处理流。
节点流:直接从数据源或目的地读写数据
处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。
小结:图解
(抽象基类) | 输入流 | 输出流 |
---|---|---|
字节流(以字节为单位,读写数据) | InputStream | OutputStream |
字符流(以字符为单位,读写数据) | Reader | Writer |
常用节点流:
常用处理流:
Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。不能操作图片,视频等非文本文件。
常见的文本文件有如下的格式:.txt、.java、.c、.cpp、.py等
注意:.doc、.xls、.ppt这些都不是文本文件。
java.io.Reader
抽象类是表示用于读取字符流的所有类的父类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。
public int read()
: 从输入流读取一个字符。 虽然读取了一个字符,但是会自动提升为int类型。返回该字符的Unicode编码值。如果已经到达流末尾了,则返回-1。public int read(char[] cbuf)
: 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。每次最多读取cbuf.length个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回-1。 public int read(char[] cbuf,int off,int len)
:从输入流中读取一些字符,并将它们存储到字符数组 cbuf中,从cbuf[off]开始的位置存储。每次最多读取len个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回-1。 public void close()
:关闭此流并释放与此流相关联的任何系统资源。 注意:当完成流的操作时,必须调用close()方法,释放系统资源,否则会造成内存泄漏。
java.io.Writer
抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
public void write(int c)
:写出单个字符。public void write(char[] cbuf)
:写出字符数组。 public void write(char[] cbuf, int off, int len)
:写出字符数组的某一部分。off:数组的开始索引;len:写出的字符个数。 public void write(String str)
:写出字符串。 public void write(String str, int off, int len)
:写出字符串的某一部分。off:字符串的开始索引;len:写出的字符个数。public void flush()
:刷新该流的缓冲。 public void close()
:关闭此流。注意:当完成流的操作时,必须调用close()方法,释放系统资源,否则会造成内存泄漏。
java.io.FileReader
类用于读取字符文件,构造时使用系统默认的字符编码和默认字节缓冲区。
FileReader(File file)
: 创建一个新的 FileReader ,给定要读取的File对象。 FileReader(String fileName)
: 创建一个新的 FileReader ,给定要读取的文件的名称。 举例:读取hello.txt文件中的字符数据,并显示在控制台上
1 | public class FileReaderWriterTest { |
不同实现方式的类比:
java.io.FileWriter
类用于写出字符到文件,构造时使用系统默认的字符编码和默认字节缓冲区。
FileWriter(File file)
: 创建一个新的 FileWriter,给定要读取的File对象。 FileWriter(String fileName)
: 创建一个新的 FileWriter,给定要读取的文件的名称。 FileWriter(File file,boolean append)
: 创建一个新的 FileWriter,指明是否在现有文件末尾追加内容。举例:
1 | public class FWWrite { |
1 | ① 因为出现流资源的调用,为了避免内存泄漏,需要使用try-catch-finally处理异常 |
因为内置缓冲区的原因,如果FileWriter不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush()
方法了。
flush()
:刷新缓冲区,流对象可以继续使用。close()
:先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。注意:即便是flush()方法写出了数据,操作的最后还是要调用close方法,释放系统资源。
举例:
1 | public class FWWriteFlush { |
如果我们读取或写出的数据是非文本文件,则Reader、Writer就无能为力了,必须使用字节流。
java.io.InputStream
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
public int read()
: 从输入流读取一个字节。返回读取的字节值。虽然读取了一个字节,但是会自动提升为int类型。如果已经到达流末尾,没有数据可读,则返回-1。 public int read(byte[] b)
: 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。每次最多读取b.length个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1。 public int read(byte[] b,int off,int len)
:从输入流中读取一些字节数,并将它们存储到字节数组 b中,从b[off]开始存储,每次最多读取len个字节 。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1。 public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。 说明:close()方法,当完成流的操作时,必须调用此方法,释放系统资源。
java.io.OutputStream
抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
public void write(int b)
:将指定的字节输出流。虽然参数为int类型四个字节,但是只会保留一个字节的信息写出。public void write(byte[] b)
:将 b.length字节从指定的字节数组写入此输出流。 public void write(byte[] b, int off, int len)
:从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。 public void flush()
:刷新此输出流并强制任何缓冲的输出字节被写出。 public void close()
:关闭此输出流并释放与此流相关联的任何系统资源。 说明:close()方法,当完成流的操作时,必须调用此方法,释放系统资源。
java.io.FileInputStream
类是文件输入流,从文件中读取字节。
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。 FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。 举例:
1 | //read.txt文件中的内容如下: |
读取操作
1 | public class FISRead { |
java.io.FileOutputStream
类是文件输出流,用于将数据写出到文件。
public FileOutputStream(File file)
:创建文件输出流,写出由指定的 File对象表示的文件。 public FileOutputStream(String name)
: 创建文件输出流,指定的名称为写出文件。public FileOutputStream(File file, boolean append)
: 创建文件输出流,指明是否在现有文件末尾追加内容。举例:
1 | import org.junit.Test; |
练习:实现图片加密操作。
提示:
1 | public class FileSecretTest { |
为了提高数据读写的速度
,Java API提供了带缓冲功能的流类:缓冲流。
缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:
BufferedInputStream
,BufferedOutputStream
BufferedReader
,BufferedWriter
缓冲流的基本原理:在创建流对象时,内部会创建一个缓冲区数组(缺省使用8192个字节(8Kb)
的缓冲区),通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
public BufferedInputStream(InputStream in)
:创建一个 新的字节型的缓冲输入流。 public BufferedOutputStream(OutputStream out)
: 创建一个新的字节型的缓冲输出流。代码举例:
1 | // 创建字节缓冲输入流 |
public BufferedReader(Reader in)
:创建一个 新的字符型的缓冲输入流。 public BufferedWriter(Writer out)
: 创建一个新的字符型的缓冲输出流。代码举例:
1 | // 创建字符缓冲输入流 |
查询API,缓冲流读写方法与基本的流是一致的,我们通过复制大文件(375MB),测试它的效率。
1 | //方法1:使用FileInputStream\FileOutputStream实现非文本文件的复制 |
字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。
public String readLine()
: 读一行文字。 public void newLine()
: 写一行行分隔符,由系统属性定义符号。 1 | public class BufferedIOLine { |
说明:
涉及到嵌套的多个流时,如果都显式关闭的话,需要先关闭外层的流,再关闭内层的流
其实在开发中,只需要关闭最外层的流即可,因为在关闭外层流时,内层的流也会被关闭。
练习1:分别使用节点流:FileInputStream、FileOutputStream和缓冲流:BufferedInputStream、BufferedOutputStream实现文本文件/图片/视频文件的复制。并比较二者在数据复制方面的效率。
练习2:
姓氏统计:一个文本文件中存储着北京所有高校在校生的姓名,格式如下:
1 | 每行一个名字,姓与名以空格分隔: |
现在想统计所有的姓氏在文件中出现的次数,请描述一下你的解决方案。
1 | public static void main(String[] args) { |
引入情况1:
使用FileReader
读取项目中的文本文件。由于IDEA设置中针对项目设置了UTF-8编码,当读取Windows系统中创建的文本文件时,如果Windows系统默认的是GBK编码,则读入内存中会出现乱码。
1 | import java.io.FileReader; |
那么如何读取GBK编码的文件呢?
引入情况2:
针对文本文件,现在使用一个字节流进行数据的读入,希望将数据显示在控制台上。此时针对包含中文的文本数据,可能会出现乱码。
作用:转换流是字节与字符间的桥梁!
具体来说:
InputStreamReader
转换流java.io.InputStreamReader
,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。
构造器
InputStreamReader(InputStream in)
: 创建一个使用默认字符集的字符流。 InputStreamReader(InputStream in, String charsetName)
: 创建一个指定字符集的字符流。举例
1 | //使用默认字符集 |
示例代码:
1 | package com.atguigu.transfer; |
OutputStreamWriter
转换流java.io.OutputStreamWriter
,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。
构造器
OutputStreamWriter(OutputStream in)
: 创建一个使用默认字符集的字符流。 OutputStreamWriter(OutputStream in,String charsetName)
: 创建一个指定字符集的字符流。举例:
1 | //使用默认字符集 |
示例代码:
1 | package com.atguigu.transfer; |
计算机中储存的信息都是用二进制数
表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。
字符编码(Character Encoding) : 就是一套自然语言的字符与二进制数之间的对应规则。
编码表:生活中文字和计算机中二进制的对应规则
乱码的情况:按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。
1 | 编码:字符(人能看懂的)--字节(人看不懂的) |
可见,当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。
ASCII字符集 :
英语字符
与二进制位之间的关系,做了统一规定。这被称为ASCII码。128个
字符。比如:空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。ISO-8859-1字符集:
GBxxx字符集:
显示中文
而设计的一套字符集。7000多个简体汉字
,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,这就是常说的”全角”字符,而原来在127号以下的那些符号就叫”半角”字符了。双字节
编码方案,共收录了21003个
汉字,完全兼容GB2312标准,同时支持繁体汉字
以及日韩汉字等。70244个
,采用多字节
编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。Unicode字符集 :
任意语言的任意字符
而设计,也称为统一码、标准万国码。Unicode 将世界上所有的文字用2个字节
统一进行编码,为每个字符设定唯一的二进制编码,以满足跨语言、跨平台进行文本处理的要求。极大的浪费
。区别Unicode和ASCII
?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?不够表示所有字符
。UTF-8字符集:
将数字转换到程序数据
的编码方案。顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。其中,UTF-8 是在互联网上使用最广
的一种 Unicode 的实现方式。变长的编码方式
。它使用1-4个字节为每个字符编码,编码规则:Unicode符号范围 | UTF-8编码方式
1 | (十六进制) | (二进制) |
注意:在中文操作系统上,ANSI(美国国家标准学会、AMERICAN NATIONAL STANDARDS INSTITUTE: ANSI)编码即为GBK;在英文操作系统上,ANSI编码即为ISO-8859-1。
把当前module下的《康师傅的话.txt》字符编码为GBK,复制到电脑桌面目录下的《寄语.txt》,
字符编码为UTF-8。
在当前module下的文本内容:
1 | 六项精进: |
代码:
1 | public class InputStreamReaderDemo { |
如果需要将内存中定义的变量(包括基本数据类型或引用数据类型)保存在文件中,那怎么办呢?
1 | int age = 300; |
Java提供了数据流和对象流来处理这些类型的数据:
数据流:DataOutputStream、DataInputStream
DataOutputStream:允许应用程序将基本数据类型、String类型的变量写入输出流中
DataInputStream:允许应用程序以与机器无关的方式从底层输入流中读取基本数据类型、String类型的变量。
对象流DataInputStream中的方法:
1 | byte readByte() short readShort() |
说明:对象流的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
ObjectOutputStream中的构造器:
public ObjectOutputStream(OutputStream out)
: 创建一个指定的ObjectOutputStream。
1 | FileOutputStream fos = new FileOutputStream("game.dat"); |
ObjectOutputStream中的方法:
public void writeObject(Object obj)
:写出一个obj对象ObjectInputStream中的构造器:
public ObjectInputStream(InputStream in)
: 创建一个指定的ObjectInputStream。
1 | FileInputStream fis = new FileInputStream("game.dat"); |
ObjectInputStream中的方法:
public void readObject(Object obj)
:读入一个obj对象1、何为对象序列化机制?
对象序列化机制
允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。
对象的类型
和对象中存储的属性
等信息。字节序列写出到文件之后,相当于文件中持久保存
了一个对象的信息。 反序列化
。对象的数据
、对象的类型
和对象中存储的数据
信息,都可以用来在内存中创建对象。2、序列化机制的重要性
序列化是 RMI(Remote Method Invoke、远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。
序列化的好处,在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原。
3、实现原理
序列化:用ObjectOutputStream类保存基本类型数据或对象的机制。方法为:
public final void writeObject (Object obj)
: 将指定的对象写出。反序列化:用ObjectInputStream类读取基本类型数据或对象的机制。方法为:
public final Object readObject ()
: 读取一个对象。如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现java.io.Serializable
接口。Serializable
是一个标记接口
,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
。
Serializable
接口transient
关键字修饰。静态(static)变量
的值不会序列化。因为静态变量的值不属于某个对象。举例1:
1 | import org.junit.Test; |
举例2:
1 | import java.io.Serializable; |
1 | import org.junit.Test; |
举例3:如果有多个对象需要序列化,则可以将对象放到集合中,再序列化集合对象即可。
1 | import org.junit.Test; |
问题1:
对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException
异常。
问题2:
当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException
异常。发生这个异常的原因如下:
解决办法:
Serializable
接口给需要序列化的类,提供了一个序列版本号:serialVersionUID
。凡是实现 Serializable接口的类都应该有一个表示序列化版本标识符的静态变量:
1 | static final long serialVersionUID = 234242343243L; //它的值由程序员随意指定即可。 |
自动生成
的。若类的实例变量做了修改,serialVersionUID 可能发生变化
。因此,建议显式声明。1 | import java.io.Serializable; |
面试题:谈谈你对java.io.Serializable接口的理解,我们知道它用于序列化,是空方法接口,还有其它认识吗?
1 | 实现了Serializable接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异。换句话说,可以先在Windows机器上创建一个对象,对其序列化,然后通过网络发给一台Unix机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。 |
练习:
需求说明:
分析:
举例:
从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,直至当输入“e”或者“exit”时,退出程序。
1 | System.out.println("请输入信息(退出输入e或exit):"); |
拓展:
System类中有三个常量对象:System.out、System.in、System.err
查看System类中这三个常量对象的声明:
1 | public final static InputStream in = null; |
奇怪的是,
1 | final声明的常量,表示在Java的语法体系中它们的值是不能修改的,而这三个常量对象的值是由C/C++等系统函数进行初始化和修改值的,所以它们故意没有用大写,也有set方法。 |
1 | public static void setOut(PrintStream out) { |
练习:
Create a program named MyInput.java: Contain the methods for reading int, double, float, boolean, short, byte and String values from the keyboard.
1 | package com.atguigu.java; |
打印流:PrintStream
和PrintWriter
提供了一系列重载的print()和println()方法,用于多种数据类型的输出
PrintStream和PrintWriter的输出不会抛出IOException异常
PrintStream和PrintWriter有自动flush功能
PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。
System.out返回的是PrintStream的实例
构造器
代码举例1
1 | import java.io.FileNotFoundException; |
1 | PrintStream ps = null; |
1 | /* |
1 | public class LogTest { |
构造方法
常用方法:
1 | import org.junit.Test; |
IO技术开发中,代码量很大,而且代码的重复率较高,为此Apache软件基金会,开发了IO技术的工具类commonsIO
,大大简化了IO开发。
Apahce软件基金会属于第三方,(Oracle公司第一方,我们自己第二方,其他都是第三方)我们要使用第三方开发好的工具,需要添加jar包。
在导入commons-io-2.5.jar包之后,内部的API都可以使用。
IOUtils类的使用
1 | - 静态方法:IOUtils.copy(InputStream in,OutputStream out)传递字节流,实现文件复制。 |
1 | public class Test01 { |
1 | - 静态方法:void copyDirectoryToDirectory(File src,File dest):整个目录的复制,自动进行递归遍历 |
1 | public class Test02 { |