Cookie和Session
Cookie和Session是什么
Cookie和Session是两种会话数据的保存方式。其中,Cookie是客户端技术,Session是服务端技术。
两者具体的类封装在(时代变了)javax.servlet.http
jakarta.servlet.http
包中,依赖jakarta.servlet-api
包即可使用jakarta.servlet.http
下的类。
Cookie
存储位置
存储在客户端,一般保存在本地用户目录下的appdata
中。生命周期
默认会话结束后消失(存于内存中)。通过
setMaxAge(int time)
可以设置cookie的有效期(单位是s,存到硬盘):- 默认值为
-1
- 如果设为
0
,立即删除
- 默认值为
缺陷
大小和数量限制:一般每个站点大约能保存20个cookie,大小限制在4kb以内。浏览器一般有300个cookie上限。
数据安全性问题:http请求中的cookie是明文传递的。以存储网站的上次访问时间为例:
1 | public class CookieDemo1 extends HttpServlet { |
Session
服务器会自动给每一个用户(浏览器)创建一个Session对象,一个Session独占一个浏览器,只要浏览器没有关闭,这个Session就存在。
服务器在自动创建Session的时候,会同时生成一个Cookie,存储sessionId:
1 | Cookie cookie = new Cookie("JSESSIONID", sessionId); |
存储位置
保存于服务端。使用场景
- 保存登录用户的信息
- 保存购物车信息
- 其他在整个网站中经常会使用的数据
生命周期
- 有效期30min,可以通过两种方式设置有效期:
setMaxInactiveTnterval(int time)
,单位是s- 在
web.xml
中配置session有效期,单位是min
1
2
3<session-config>
<session-timeout>time</session-timeout>
</session-config>- 还可以通过HttpSession的
invalidate()
方法,手动使session失效。
- 有效期30min,可以通过两种方式设置有效期:
相关方法
Session也能和
ServletContext
一样,实现不同Servlet之间的通信。并且该方式**优于利用ServletContext
**。
1 | String password = request.getParameter("password"); |
Servlet
Servlet是什么
Servlet(Server + Applet),一个Servlet就是一个Java类,并提供 基于请求-响应模式 的Web服务。
要使用servlet需要添加(时代变了)javax.servlet-api
Jar包jakarta.servlet-api
。
Servlet的处理流程
接收客户端的HTTP请求路径及请求内容
根据web.xml(属于web应用程序的一部分,部署描述符),找到请求路径对应的Servlet
1
2
3
4
5
6
7
8
9...
<servlet>
<servlet-name>servlet名称(最好与类名相同)</servlet-name>
<servlet-class>类路径</servlet-calss>
</servlet>
<servlet-mapping>
<servlet-name>与<servlet>元素中的该元素内容相同</servlet-name>
<url-pattern>映射路径</url-pattern>
</servlet-mapping>将请求转发给Servlet对应的service方法(会传递HttpServletRequest对象和HttpServletResponse对象作为方法参数)
service方法根据请求是 get/post/… 转发给 doGet/doPost方法处理
通过HttpResponse对象,将响应返回给客户端
Servlet生命周期
init → service(doGet、doPost…) → destroy
管理Servlet的配置信息
通过ServletConfig
对象获取配置
1 | ... |
每个Servlet支持设置多个<init-param>
,Servlet初始化过程中,<init-param>
参数将被封装到ServletConfig
对象中。在Servlet中,通过调用ServletConfig对象,就可以利用配置信息:
1 | ServletConfig config = this.getServletConfig(); |
通过ServletContext
共享Servlet的配置信息
Servlet容器在启动时,会为每个Web应用创建一个对应的ServletContext
对象,它代表当前的Web应用。在一个Servlet中保存的数据,可以在另一个Servlet中获取。
1 | <context-param> |
<context-param>
是在各个具体的<servlet>
元素之外的,同样可以设置多个<context-param>
。在任意具体的Servlet中,通过调用ServletContext
对象,就可以利用配置信息:
1 | ServletContext context = this.getServletContext(); |
通过ServletContext
的属性(attribute)实现不同Servlet之间的通信
事先不知道,无法预先配置的信息,应该如何共享?(比如将购物车中的商品共享给结算页面)
在具体的Serlvet中,通过ServletContext
对象的属性(attribute)设置希望共享的信息:
1 | ServletContext context = this.getServletContext(); |
读取外部资源配置文件信息
通过ServletContext
的方法读取。
Properties文件
在
java
目录下新建的properties默认不会被打包,需要在
pom.xml
文件中配置如下内容:1
2
3
4
5
6
7
8
9
10<build>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>t</filtering>
</resource>
</build>在
resource
目录下新建的properties默认会被打包到
classes
路径下
都会被打包到
classes
路径下,俗称这个路径为classpath
getResource(“外部资源配置文件的路径”)
1 | ServletContext context = this.getServletContext(); |
getResourceAsStream(“外部资源配置文件的路径”)
1 | public class ServletDemo3 extends HttpServlet { |
getRealPath(“外部资源配置文件的相对路径”)
获取外部资源配置文件的绝对路径
1 | String realPath = context.getRealPath("外部资源配置文件的相对路径"); |
HttpServletRequest类
常见应用
获取前端传递的参数
getParameter()
getParameterValues()
请求转发
请求转发就是将当前的HttpServletRequest
和HttpServletResponse
对象交给指定的web组件处理。对客户端来说,是一次请求,一次响应,浏览器的URL不变。
步骤:
- 获取请求转发对象
即获取RequestDispatcher
类的对象(由Servlet容器创建,封装由路径所标识的服务器资源)。
获取请求转发对象有两种方式:- 通过
HTTPServletRequest
对象获取:
RequestDispatcher rd = request.getRequestDispatcher("绝对路径/相对路径");
- 通过
ServleContext
对象获取:
RequestDispatcher rd = this.getServletContext().getNamedDispatcher("servlet-name");
或者RequestDispatcher rd = this.getServletContext().getRequestDispatcher("绝对路径");
- 通过
- 调用转发对象的
forward(HttpServletRequest req, HttpServletResponse resp)
方法
例子:
1 | public class ServletDemo2 extends HttpServlet { |
HttpServletResponse类
向浏览器发送数据的方法
getOutputStream()
getWriter()
向浏览器发送响应头的方法
1 | void setCharacterEncoding(String var1); |
常见应用
向浏览器输出消息
1 | public class GetServlet extends HttpServlet { |
下载文件
1 | public class FileServlet extends HttpServlet { |
验证码功能
实现方式:
- 前端实现
- 后端实现,需要用到java的图片类,产生一个图片
实现重定向
请求重定向就是通过HttpServletResponse
对象发送给客户端一个新的URL地址,让其重新请求。对客户端来说,是两次请求,两次响应。
请求重定向的方法:resp.sendRedirect("绝对路径");
,其中路径需要写明tomcat设置的application context前缀,可用req.getContextPath()
获取绝对路径前缀。
Maven
Tomcat
Tomcat简介
Tomcat是一个典型的Web应用服务器软件,通过运行Tomcat服务器,我们就可以快速部署我们的Web项目,并交由Tomcat进行管理,我们只需要直接通过浏览器访问我们的项目即可。
安装及配置
当前支持的版本:
Apache Tomcat® - Which Version Do I Want?
Servlet Spec | JSP Spec | EL Spec | Apache Tomcat Version | Supported Java Versions |
---|---|---|---|---|
6.1 | 4.0 | 6.0 | 11.0.x | 17 and later |
6.0 | 3.1 | 5.0 | 10.1.x | 11 and later |
4.0 | 2.3 | 3.0 | 9.0.x | 8 and later |
Apache Tomcat® - Apache Tomcat 10 Software Downloads下载压缩包64-bit Windows zip
,解压到合适的目录文件下,运行bin
目录下的startup.bat
/shutdown.bat
,即可启动/关闭 Tomcat。
conf文件夹
conf文件夹下放置tomcat的配置文件,其中server.xml
是服务器核心配置文件。
连接端口相关设置
主机相关设置
默认端口号为
8080
.1
2
3
4
5
6
7
8
9
10
11<Serveice name="Catalina">
<!-- 默认端口号:8080 -->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<!-- 默认主机名称:localhost 等价于127.0.0.1 -->
<!-- 默认网站应用存放的根目录:webapps -->
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
</Serveice>
环境变量
环境变量的配置可选,真实的开发环境,都是在IDEA中操作。
如有需要,后续补充
部署项目到Tomcat
一般的项目结构:
webapps:Tomcat服务器的web目录
ROOT
项目名
WEB-INF
- classes:java程序
- lib:依赖的jar包
- web.xml:网站的配置文件
index.html:默认的首页
static
- css
- style.css
- js
- img
- css
…
手动
将自己写的网页放到tomcat中网站应用(webapps)文件夹下,就可以访问了。
Git
水杉码园入坑指北
概述
水杉码园使用Go语言,基于Gitea开发。本文用于记录如何入坑开发。
开发机
先用Cisco的VPN登录,再连接开发机。
待完善……
Windows环境下clone代码
水杉码园的代码存储在gitlab上:ShuiShan / Gitea / gitea · GitLab。并且该地址需要拥有项目管理权限的人员(徐柴笛、周成义、李苗进、顾业鸣)进行授权才可访问。
clone代码的操作:
- 安装Git
Clone With HTTPS
- 先在命令行执行
git config --global http.sslVerify false
git clone https://code.kfcoding.com/ShuiShan/Gitea/gitea.git
- 先在命令行执行
Windows下环境搭建
安装go
安装node.js(一定要勾选会附带安装chocolatey的选项)
安装make(
choco install make
)设置npm的国内镜像代理
npm config set registry https://registry.npm.taobao.org
在Clone的项目文件夹下(
\gitea
),执行TAGS="bindata" make build
本地测试,执行
./gitea web
浏览器访问
http://localhost:3000/
登录水杉码园,配置本地数据库信息,记住要在可选设置中设置管理员账号
使用VS Code进行开发
安装git历史提交记录插件 GitLens
课程批量导入学生
- 入口:
/org/:org/teams/new_all_student
导入完成后,课程团队下会生成指定的团队:
团队为成员-仓库的形式
Owners
管理员-管理的仓库
AllStuRead
教师有操作权限,学生只有读权限。用于老师布置作业,分享数据、资料。
AllStuWrite
所有学生都有操作权限
课程中的仓库,除了AllStuWrite和AllStuRead这两个公开仓库之外,每个学生还有自己的个人仓库,目前以学号作为仓库名。仓库描述为student u.Name(学号) u.FullName(用户名)'s repo
。
代码
routes.go
下,m.Post("/teams/new_all_student", bindIgnErr(auth.CreateAllStuTeamForm{}), org.NewAllStuTeamPost)
auth.CreateAllStuTeamForm{}
相当于是前端的表单其中的Studentlist表示的上传的excel文件
这个Post函数将前端的表单传入后端的
NewAllStuTeamPost
函数,细看一下NewAllStuTeamPost方法
首先是通过前端的form获取相应的基本信息
再来我们可以先看一下这个函数最后的部分
GetCache().Set
这里把课程ID(
Organization.ID
)设置了1分钟的缓存再回过头来看上边的代码,如果能在缓存中获取到课程ID的缓存,说明之前一分钟内有过成功导入的记录,就会给出一个不要多次导入的提示并结束导入学生的操作
批量移除选中的仓库
批量删除成员
剑指Offer
数组与矩阵
5. 替换空格
请实现一个函数,把字符串
s
中的每个空格替换成”%20”。示例1:
1
2 输入:s = "We are happy."
输出:"We%20are%20happy."限制:
0 <= s 的长度 <= 10000
在网络编程中,如果URL参数中含有特殊字符,如空格、#等,则可能导致服务器端无法获得正确的参数值。我们需要将这些特殊符号转换成服务器可以识别的字符。转换的规则是在后面跟上ASCII码的两位十六进制的表示。比如空格的ASCII码是32,即十六进制的0x20,因此空格被替换成”%20”。再比如#的ASCII码为35,即十六进制的0x23,它在URL中被替换为”%23”。
如果是在原来的字符串上进行替换,就有可能覆盖修改在该字符串后面的内存。如果是创建新的字符串并在新的字符串上进行替换,那么我们可以自己分配足够多的内存。由于有这样两种不同的解决方案,我们应该向面试官问清楚,让他明确告诉我们他的需求。假设面试官让我们在原来的字符串上进行替换,并且保证输入的字符串后面有足够多的空余内存。
最直观的做法是从头到尾扫描字符串,每次碰到空格时进行替换。这样做,每次碰到空格就需要把空格后面的所有字符都后移2字节。假设字符串的长度是n。对每个空格字符,需要移动后面$O(n)$个字符,因此对于含有$O(n)$个空格字符的字符串而言,总的时间效率是$O(n^2)$。这显然不是一个好的解决方案。
那么,怎么操作能减少移动次数呢?—— 试试把从前向后替换改成从后向前替换。(对于Java来说,String是不可变的,因此,严谨地说,OJ题提供的方法头的参数应该由String改为StringBuffer。)
1 | public String replaceSpace(String s) { |
时间复杂度和空间复杂度都是$O(n)$。
29. 顺时针打印矩阵
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
示例 1:
1
2 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]示例 2:
1
2 输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]限制:
- 0 <= matrix.length <= 100
- 0 <= matrix[i].length <= 100
矩阵不一定是方阵,四个顶点的坐标是临界点。
1 | public int[] spiralOrder(int[][] matrix) { |
50. 第一个只出现一次的字符位置
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
示例:
1
2
3
4
5 s = "abaccdeff"
返回 "b"
s = ""
返回 " "限制:
0 <= s 的长度 <= 50000
统计字符是否仅出现一次,可以用容器存放每个字符是否只出现一次。这个容器可以根据<字符>来存放<是否多次出现>的信息。因此,可以利用哈希表。
1 | public char firstUniqChar(String s) { |
时间复杂度$O(n)$,空间复杂度$O(1)$
栈、队列、堆
9. 用两个栈实现队列
剑指 Offer 09. 用两个栈实现队列 - 力扣(LeetCode)
- 栈是LIFO(后进先出),队列是FIFO(先进先出)。
- 将一个栈专门用于插入整数,另一个栈专门用于删除整数
- Stack类已被Java不推荐使用,LinkedList基于双向链表实现,只能顺序访问,但可以快速插入和删除元素。LinkedList可用作栈、队列和双向队列。
1 | private LinkedList<Integer> list1, list2; |
时间复杂度:$O(n)$(deleteHead()函数在N次队首元素删除操作中总共需完成N个元素的倒序),空间复杂度$O(n)$。
30. 包含min函数的栈
剑指 Offer 30. 包含min函数的栈 - 力扣(LeetCode)
既然是定义栈,那么push和pop功能显然不用再变动,重点在于实现min函数。栈中的最小元素会随着元素的入栈和出栈动态变化,因此需要记录每个状态对应的当前最小元素。可以构造一个辅助栈来实现。
1 | private LinkedList<Integer> stack, minStack; |
31. 栈的压入、弹出序列
剑指 Offer 31. 栈的压入、弹出序列 - 力扣(LeetCode)
一个序列是否为栈的弹出序列的规律:
- 如果当前栈顶的数字是下一个弹出的数字,则弹出;
- 如果当前栈顶的数字不是下一个弹出的数字,把还未入栈的数字压入栈中,直至栈顶的数字是下一个弹出的数字
- 如果所有数字都入栈的过程中,栈顶数字始终不是需要弹出的数字,则不可能是弹出序列
- 栈中数字全部顺利出栈,则为弹出序列
1 | public boolean validateStackSequences(int[] pushed, int[] popped) { |
时间复杂度:$O(n)$(每个元素最多进栈1次,出栈1次,$O(2n)$);空间复杂度:$O(n)$
40. 最小的K个数
剑指 Offer 40. 最小的k个数 - 力扣(LeetCode)
快速排序每次都能将选定的哨兵置于排序完成后的最终位置,当前的哨兵最终位置索引为K时,比它小的K个数将全在左侧。
1 | public int[] getLeastNumbers(int[] arr, int k) { |
- 时间复杂度$O(n)$: 其中n为数组元素数量;对于长度为n的数组执行哨兵划分操作的时间复杂度为$O(N)$;每轮哨兵划分后根据k和i的大小关系选择递归,由于i分布的随机性,则向下递归子数组的平均长度为$\frac{N}{2}$;因此平均情况下,哨兵划分操作一共有$N + \frac{N}{2} + \frac{N}{4} + … + \frac{N}{N} = \frac{N - \frac{1}{2}}{1 - \frac{1}{2}} = 2N - 1$(等比数列求和),即总体时间复杂度为$O(n)$。
- 空间复杂度$O(\log n)$:划分函数的平均递归深度为$O(\log n)$。
41. 数据流中的中位数
剑指 Offer 41. 数据流中的中位数 - 力扣(LeetCode)
通过建立一个大顶堆和一个小顶堆(
PriorityQueue
),将数据流拆分为两部分,根据两个堆顶元素就能得到中位数。添加元素过程中,要保证两个堆的大小平衡
1 | private Queue<Integer> minHeap, maxHeap; |
时间复杂度:
- 查找中位数$O(1)$:获取堆顶元素使用$O(1)$时间;
- 添加数字$O(\log N)$:堆的插入和弹出操作使用$O(\log N)$时间。
空间复杂度:$O(N)$
59.1 滑动窗口的最大值
剑指 Offer 59 - I. 滑动窗口的最大值(单调队列,清晰图解) - 滑动窗口的最大值 - 力扣(LeetCode)
如何在窗口每次滑动时,获取最大值?使用双端队列存储当前滑动窗口中的最大值以及在后续窗口中潜在的最大值。
每轮窗口滑动:
- 如果滑出窗口的数是队首数字,则队首数字也出队(队列的大小必然<=窗口大小)
- 如果新进入窗口的数比队尾的数大,说明队尾的数不可能成为某个窗口的最大值。将队尾元素移除,直到队尾的数字>=新进入窗口的数或队列已空。
- 如果新进入窗口的数比队尾的数小,说明等之前的数滑出窗口后,有可能会成为后续窗口中的最大值,因此,直接加入队尾。
1 | private Deque<Integer> deque; |
时间复杂度:$O(n)$
空间复杂度:$O(n)$
双指针
57.1 和为S的两个数字
剑指 Offer 57. 和为s的两个数字 - 力扣(LeetCode)
递增排序的数组,查找两个数使得和为s——双指针
1 | public int[] twoSum(int[] nums, int target) { |
时间复杂度:$O(n)$
空间复杂度:$O(1)$
57.2 和为s的连续正数序列
剑指 Offer 57 - II. 和为s的连续正数序列 - 力扣(LeetCode)
连续正整数序列 & 求和——双指针
遍历的终点:序列中最小的数 * 2 + 1 > target
双指针可以模拟滑动窗口
滑动窗口的右指针无需向左移动,必然能遍历所有解。
1 | public int[][] findContinuousSequence(int target) { |
时间复杂度:$O(n)$
空间复杂度:$O(n)$
58.1 翻转单词顺序列
剑指 Offer 58 - I. 翻转单词顺序 - 力扣(LeetCode)
1 | public String reverseWords(String s) { |
时间复杂度:$O(n)$
空间复杂度:$O(n)$
58.2 左旋转字符串
剑指 Offer 58 - II. 左旋转字符串 - 力扣(LeetCode)
1 | public String reverseLeftWords(String s, int n) { |
时间复杂度:$O(n)$
空间复杂度:$O(n)$
链表
6. 从尾到头打印链表
剑指 Offer 06. 从尾到头打印链表 - 力扣(LeetCode)
1 | public int[] reversePrint(ListNode head) { |
时间复杂度:$O(n)$
空间复杂度:$O(n)$
18. 删除链表的节点
剑指 Offer 18. 删除链表的节点 - 力扣(LeetCode)
- 无需存储所有节点,找到被删的节点,修改前一个节点的next节点即可——双指针
1 | public ListNode deleteNode(ListNode head, int val) { |
时间复杂度:$O(n)$
空间复杂度:$O(1)$
22. 链表中倒数第K个节点
剑指 Offer 22. 链表中倒数第k个节点 - 力扣(LeetCode)
双指针
1 | public ListNode getKthFromEnd(ListNode head, int k) { |
时间复杂度:$O(n)$
空间复杂度:$O(1)$
23. 链表中环的入口节点
剑指 Offer II 022. 链表中环的入口节点 - 力扣(LeetCode)
方式一:利用HashSet存储ListNode,判断后续遍历的节点是否已经在集合中。时间复杂度:$O(n\log n)$;空间复杂度$O(n)$。
方式二:一快一慢的双指针。
一快一慢的指针,让快的速度是慢的2倍,如果存在环,快的指针将会追上慢的指针。
指针到达相同节点后,如何得知入口节点?
当前指针继续往前走向入口节点的距离 == 头结点到达入口节点的距离
假设环入口节点为$y_1$,相遇所在节点为$z_1$。(头结点到达入口节点的距离为$X$,入口节点到达相遇节点的距离为$Y$,相遇节点到达入口节点的距离为$Z$)
假设快指针 fast 在圈内绕了$N$圈,则总路径长度为$X+NY+(N-1)Z$。$Z$为$N-1 $倍是因为快慢指针最后已经在$z_1$节点相遇了,后面就不需要再走了。
而慢指针slow总路径长度为$X+Y$。
因为快指针是慢指针的两倍,因此$X+NY+(N-1)Z = 2(X+Y)$。
我们要找的是环入口节点$y_1$,也可以看成寻找长度$X$的值,因此我们先将上面的等式转换为:$X=(N-2)Y+(N-1)Z = (N-2)(Y+Z)+Z$。$Y+Z$是圆环的总长度,右边可以看作从相遇点$z_1$开始在圆环中走过$N-2$圈,再走长度为$Z$的长度。可以发现如果让两个指针同时从起点$x_1$和相遇点$z_1$开始,每次只走过一个距离,那么最后他们会在环入口节点相遇。
1 | public ListNode detectCycle(ListNode head) { |
时间复杂度:$O(n)$
空间复杂度:$O(1)$
24. 反转链表
剑指 Offer 24. 反转链表 - 力扣(LeetCode)
1 | public ListNode reverseList(ListNode head) { |
时间复杂度:$O(n)$
空间复杂度:$O(1)$
25. 合并两个排序的链表
剑指 Offer 25. 合并两个排序的链表 - 力扣(LeetCode)
1 | public ListNode mergeTwoLists(ListNode l1, ListNode l2) { |
时间复杂度:$O(n)$
空间复杂度:$O(1)$
35. 复杂链表的复制
剑指 Offer 35. 复杂链表的复制 - 力扣(LeetCode)
- 利用哈希表,构建原链表节点和新链表对应节点的映射关系。
1 | public Node copyRandomList(Node head) { |
时间复杂度:$O(n)$
空间复杂度:$O(n)$
52. 两个链表的第一个公共节点
剑指 Offer 52. 两个链表的第一个公共节点 - 力扣(LeetCode)
1 | public ListNode getIntersectionNode(ListNode headA, ListNode headB) { |
时间复杂度:$O(n+m)$
空间复杂度:$O(1)$
二分查找
11. 旋转数组的最小数字
剑指 Offer 11. 旋转数组的最小数字 - 力扣(LeetCode)
分治
数学
39. 数组中出现次数超过一半的数字
1 | public int majorityElement(int[] nums) { |
时间复杂度:$O(n)$
空间复杂度:$O(1)$
43. 1~n整数中1出现的次数
剑指 Offer 43. 1~n 整数中 1 出现的次数 - 力扣(LeetCode)
1 | public int countDigitOne(int n) { |
I/O详解
概述
Java的I/O大概可以分成以下几类:
- 磁盘操作:File
- 字节操作:InputStream 和 OutputStream
- 字符操作:Reader 和 Writer
- 对象操作:Serializable
- 网络操作:Socket
- 新的输入/输出:NIO
File 类
java.io.File
File类可以用于表示文件和目录路径名,但是不表示文件的内容。
静态成员变量
pathSeparator
:路径分隔符Windows:
;
Linux:
:
separator
:文件名称分隔符Windows:反斜杠
\
Linux:正斜杠
/
为了保证在不同系统中都能顺利执行,操作路径不能写死:
1
"C:" + File.Separator + "develop" + File.Separator + "a" + File.Separator + "a.md"
绝对路径和相对路径
绝对路径:一个完整地址
相对路径:简化的路径,将当前地址作为根目录
构造方法
File(String pathname)
:将给定的路径名字符串转换为抽象路径名来创建一个新File实例。pathname可以以文件结尾,也可以以文件夹结尾;可以是相对路径,也可以是绝对路径;可以存在,也可以不存在。创建File对象,只是把字符串路径封装为File对象,无需考虑路径真假。
File(String parent, String child)
参数把路径分成了两部分,父路径和子路径可以单独书写,使用起来非常灵活。
File(File parent, String child)
父路径是File类型,可以使用File类的方法对路径进行一些操作。
常用方法
获取方法
String getAbsolutePath()
:返回此File的绝对路径名字符串String getPath()
:将此File转换为路径名字符串(创建File时的pathname是什么样,就是什么样)使用
toString
方法有相同的效果(调用了getPath
方法)String getName()
:返回File的构造方法传递的结尾部分(文件或目录的名称)long length()
:返回File表示的文件的大小,以字节为单位文件夹没有大小的概念,因此获取文件夹的大小会返回0;如果构造方法中给出的路径不存在,返回0。
判断方法
boolean exists()
:File表示的文件或者目录当前是否存在boolean isDirectory()
:File对象是否是目录boolean isFile()
:File对象是否是文件
isDirectory()
和isFile()
方法的正确发挥作用的前提是路径已存在,否则都会返回false。可以通过exist()
先做判断:
1 | File f = new File("C:\\download\\xx.torrent"); |
创建和删除方法
boolean createNewFile()
:当且仅当具有该名称的文件不存在时,创建一个新的空文件。boolean delete()
:删除由此File表示的文件或目录。文件夹中有内容,则不会删除;路径不存在,会返回false。
delete方法是直接在硬盘删除文件/文件夹,不走回收站(直接永久删除),要慎用。
boolean mkdir()
:创建由此File表示的目录。boolean mkdirs()
:创建由此File表示的目录,包括任何必需但不存在的父目录(相对于mkdir()
,适用范围更广,推荐)。
遍历目录
String[] list()
:返回一个String数组,表示该File目录中的所有子文件或目录(子目录下的内容不会获取)。File[] listFiles()
:返回一个File数组,表示该File目录中的所有子文件或目录。
如果目录的路径不存在,或者路径不是一个目录,都会抛出空指针异常。
文件搜索
递归地列出一个目录下的所有文件:
1 | public static void listAllFiles(File dir) { |
文件过滤器
java.io.FileFilter
是一个接口,是File的过滤器。该接口的对象可以传递给File类的listFiles(FileFilter)
作为参数。接口中的唯一方法:
boolean accept(File pathname)
:测试指定文件对象是否应该包含在当前File目录中。listFiles方法会调用参数传递的过滤器中的方法accept。
java.io.FilenameFilter
是文件名称的过滤器。接口中的唯一方法:
boolean accept(File dir, String name)
:测试指定文件是否应该包含在某一文件列表中。
两个过滤器接口没有实现类,需要自己写实现类,重写accept方法,自定义过滤规则。
IO 流概述
数据的传输可以看作是一种数据的流动,根据数据的流向,可以分为输入流和输出流。根据数据的类型,可以分为字节流和字符流。
输入流 | 输出流 | |
---|---|---|
字节流 | InputStream | OutputStream |
字符流 | Reader | Writer |
字节流
字节输出流 OutputStream
java.io.OutputStream
是一个抽象类,定义了一些成员方法如下:
void close()
:关闭输出流,并释放相关的系统资源void flush()
:刷新输出流,并强制任何缓冲的输出字节被写出void write(byte[] b)
:将指定的字节数组写入此输出流void write(byte[] b, int off, int len)
:从指定的字节数组写入len字节,从偏移量off开始输出到输出流abstract void write(int b)
:将指定的字节输出流
文件字节输出流 FileOutputStream
构造方法
FileOutputStream(String name)
:创建一个向具有指定名称的文件中写入数据的文件输出流FileOutputStream(File file)
:创建一个向File对象表示的文件中写入数据的文件输出流FileOutputStream(String name, boolean append)
append:追加写开关
true
:创建对象不会覆盖源文件,在末尾追加写数据false
:创建新文件,覆盖源文件(默认)
使用步骤
创建一个FileOutputStream对象,构造方法中传递写入数据的目的地
调用FileOutputStream对象中的write方法,把数据写入到文件中
当1次写多个字节时,写入的字节最后的显示:
- 如果写的第一个字节是正数(0-127),那么显示的时候会查询ASCII表。
- 如果写的第一个字节是负数,则第一个字节会和第二个字节**组成一个中文显示,查询系统默认码表(GBK)**。
释放资源(提高程序的效率)
写入字符的方法
可以使用String类中的方法byte[] getBytes()
,把字符串转换为字节数组(默认编码方式一般为UTF-8)。
1 | FileOutputStream fos = new FileOutputStream("C:\\download\\xx.md"); |
换行写入
通过写换行符完成换行写入:
- windows:
\r\n
- linux:
/n
- mac:
/r
1 | fos.write("\r\n".getBytes()); |
缓冲流有更跨平台的换行写入方式。
字节输入流 InputStream
java.io.InputStream
是一个抽象类,定义了一些成员方法如下:
void close()
: 关闭输入流,并释放相关的系统资源int read()
: 从输入流读取并返回输入的下一个字节返回-1时,表示读取到了eof(end of file)
int read(byte[] b)
: 从输入流读取多个字节,并存储到字节数组b中- 数组b起到了缓冲作用,存储读取到的多个字节,能提高读取效率
- 数组的大小一般定义为1024(1kb)的整数倍
- 返回每次读取的有效字节个数(返回-1时,表示读取到了eof(end of file))
文件字节输入流 FileInputStream
构造方法
FileInputStream(String name)
FileInputStream(File file)
使用步骤
- 创建FileInputStream对象,构造方法中绑定要读取的数据源
- 使用FileInputStream对象中的read方法,读取文件
- 释放资源(提高程序的效率)
1 | FileInputStream fis = new FileInputStream("C:\\download\\xx.md"); |
实现文件复制
1 | public static void copyFile(String src, String dist) throws IOException { |
字符流
字节流读取中文的问题
字节流读取中文字符时,可能不会显示完整的字符,因为一个中文字符可能占用多个字节存储。所以Java提供了一些字符流类,以字符为单位读写数据,专门用于处理文本文件。
Reader 与 Writer
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
InputStreamReader
实现从字节流解码成字符流OutputStreamWriter
实现字符流编码成字节流
字符输出流 Writer
java.io.Writer
抽象类表示用于字符输出流的所有类的超类,可以将字符信息从内存存入磁盘。
void close()
:关闭输出流,并释放相关的系统资源void flush()
:刷新输出流,并强制任何缓冲的输出字符被写出void write(char[] cbuf)
:将指定的字符数组写入此输出流void write(char[] cbuf, int off, int len)
:从指定的字符数组写入len字节,从偏移量off开始输出到输出流void write(String str)
:将字符串写入输出流void write(String str, int off, int len)
:将子字符串写入输出流abstract void write(int b)
:将指定的字符输出流
文件字符输出流 FileWriter
java.io.FileWriterextends OutputStreamWriter extends Writer
构造方法
FileWriter(String name)
:创建一个向具有指定名称的文件中写入数据的文件输出流FileWriter(File file)
:创建一个向File对象表示的文件中写入数据的文件输出流FileWriter(String name, boolean append)
append:追加写开关
true
:创建对象不会覆盖源文件,在末尾追加写数据false
:创建新文件,覆盖源文件(默认)
使用步骤
- 创建一个FileWriter对象,构造方法中传递写入数据的目的地
- 调用FileWriter对象中的write方法,把数据写入到内存缓冲区中(有一个字符转换为字节的过程)
- 使用FileWriter对象中的flush方法,把内存缓冲区中的数据刷新到文件中
- 释放资源(也会执行把内存缓冲区中的数据刷新到文件中的操作,故第3步可以不显式写出来)
1 | FileWriter fw = new FileWriter("C:\\download\\xx.md"); |
换行写入
通过写换行符完成换行写入:
- windows:
\r\n
- linux:
/n
- mac:
/r
1 | fw.writer("\r\n"); |
缓冲流有更跨平台的换行写入方式。
字符输入流 Reader
java.io.Reader
抽象类表示用于字符输入流的所有类的超类,可以读取字符信息到内存中。
void close()
:关闭输入流,并释放相关的系统资源int read()
:从输入流读取一个字符返回-1时,表示读取到了eof(end of file)
int read(char[] cbuf)
:从输入流中读取多个字符,并将它们存储到字符数组cbuf中。- 数组cbuf起到了缓冲作用,存储读取到的多个字符,能提高读取效率
- 数组的大小一般定义为1024(1kb)的整数倍
- 返回每次读取的有效字符个数(返回-1时,表示读取到了eof(end of file))
文件字符输入流 FileReader
java.io.FileReader extends InputStreamReader extends Reader
构造方法
FileReader(String fileName)
FileReader(File file)
使用步骤
- 创建FileReader对象,构造方法中绑定要读取的数据源
- 使用FileReader对象中的read方法,读取文件
- 释放资源(提高程序的效率)
1 | FileReader fr = new FileReader("C:\\download\\xx.md"); |
IO 异常的处理
JDK 7 之前的处理
实际开发中不应该直接抛出异常,而应该使用try...catch...finally
代码块对异常进行处理。
1 | // 提高变量fw的作用域,让finally能使用 |
JDK 7 的处理
JDK 7的新特性:在try后边可以增加一个()
,在括号中可以定义流对象,则这个流对象的作用域就在try中有效,try中的代码执行完毕,会自动把流对象释放,不用写finally。
1 | try (// 1. 创建一个字节输入流对象,构造方法中绑定要读取的数据源 |
属性集 Properties
java.util.Properties
继承于HashTable
,虽然HashTable是一个遗留类,但Properties作为唯一和IO流相结合的集合依然活跃着。Properties表示一个持久的属性集。它使用键值对存储数据,每个键和值都是字符串,因此Properties有一些操作字符串的特有方法:
Object setProperty(String key, String value)
String getProperty(String key)
Set<String> stringPropertyNames()
返回此属性列表中的键集。
1 | // 创建Properties集合对象 |
可以使用Properties集合中的store
方法,把集合中的临时数据持久化写入到硬盘中。
void store(OutputStream out, String comments)
- OutputStream是字节输出流,不能写中文
- comments是注释,用来解释保存的文件的作用。(不能使用中文,会产生乱码,默认是Unicode编码)一般使用空字符串
""
。
void store(Writer writer, String comments)
- Writer是字符输出流,可以写中文
使用步骤:
- 创建Properties集合对象,添加数据
- 创建字节输出流/字符输出流对象
- 使用Properties集合的store方法,将集合中的临时数据持久化写入硬盘
- 释放资源
1 | // 创建Properties集合对象 |
可以使用Properties集合中的load
方法,把硬盘中保存的文件(键值对)读取到集合中使用。
void load(InputStream inStream)
InputStream是字节输入流,不能读取中文
void load(Reader reader)
Reader是字符输入流,可以读取中文
使用步骤:
- 创建Properties集合对象
- 使用Properties集合的load方法,读取存储键值对的文件
- 键值对文件,键与值默认的连接符可以用
=
、空格以及一些其他符号 - 文件中可以使用
#
进行注释 - 文件中的键和值默认都是字符串,无需加引号
- 键值对文件,键与值默认的连接符可以用
- 遍历Properties集合
- 释放资源
1 | // 创建Properties集合对象 |
缓冲流
缓冲流也叫高效流,是对4个基本的FileXxx
流的增强,所以也是4个流。缓冲流的基本原理,是在创建流对象时,创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少IO次数,从而提高读写的效率。
缓冲字节输出流 BufferedOutputStream
java.io.BufferedOutputStream extends OutputStream
构造方法
BufferedOutputStream(OutputStream out)
创建一个缓冲输出流,将数据写入指定的底层输出流
BufferedOutputStream(OutputStream out, int size)
创建一个缓冲输出流,将具有指定缓冲区大小的数据写入指定的底层输出流
使用步骤
- 创建FileOutputSream对象,构造方法中绑定要输出的目的地
- 创建BufferedOutputStream对象,构造方法中传递FileOutputStream对象,提高FileOutputStream对象的效率
- 使用BufferedOutputStream对象的write方法,将数据写入内部缓冲区
- 使用BufferedOutputStream对象的flush方法,将内部缓冲区中的数据刷新到文件中
- 释放资源(也会执行把内存缓冲区中的数据刷新到文件中的操作,故第4步可以不显式写出来)
1 | // 1. |
缓冲字节输入流 BufferedInputStream
java.io.BufferedInputStream extends InputStream
构造方法
BufferedInputStream(InputStream in)
创建一个缓冲输入流,并保存输入流in,以便将来使用
BufferedInputStream(InputStream in, int size)
创建一个缓冲输入流,将具有指定缓冲区大小的数据写入指定的底层输入流
使用步骤
- 创建FileInputSream对象,构造方法中绑定要读取的数据源
- 创建BufferedInputStream对象,构造方法中传递FileInputStream对象,提高FileInputStream对象的效率
- 使用BufferedInputStream对象的read方法,将数据存入缓冲区数组
- 释放资源
1 | // 1. |
实现文件复制
和使用基本字节流进行文件复制相比,效率明显提升(1次读取单个字节或多个字节都是如此)。
1 | long s = System.currentTimeMills(); // 测试程序效率 |
缓冲字符输出流 BufferedWriter
java.io.BufferedWriter extends Writer
构造方法
BufferedWriter(Writer out)
创建一个使用默认输出缓冲区的缓冲字符输出流
BufferedWriter(Writer out, int size)
创建一个使用给定大小缓冲区的缓冲字符输出流
特有的成员方法
void newLine()
:写入一个行分隔符(根据不同的操作系统,获取不同的行分隔符)
使用步骤
- 创建缓冲字符输出流对象,构造方法中传递字符输出流
- 调用缓冲字符输出流的write方法,把数据写入到内存缓冲区中
- 调用缓冲字符输出流的flush方法,把内存缓冲区中的数据刷新到文件中
- 释放资源
1 | BufferedWriter bw = new BufferedWriter(new FileWriter("C:\\download\\xx.md")); |
缓冲字符输入流 BufferedReader
java.io.BufferedReader extends Reader
构造方法
BufferedReader(Reader in)
BufferedReader(Reader in, int size)
特有的成员方法
String ReadLine()
:读取一个文本行(根据不同的操作系统,获取不同的行分隔符)
返回该行内容的字符串,不包含任何行分隔符。如果已到达流末尾,则返回null。
逐行读取文本文件的内容
1 | BufferedReader br = new BufferedReader(new FileReader("C:\\download\\xx.md")); |
编码与解码
编码就是把字符转换为字节,而解码是把字节重新组合成字符。如果编码和解码过程使用不同的编码方式那么就出现了乱码。
字符集 charset
字符集也叫编码表。是一个系统支持的所有字符的集合,包括文字、标点符号、图形符号、数字等。常见的字符集有ASCII、GBK、Unicode等。
GBK编码中,中文字符占2个字节,英文字符占1个字节;
Unicode字符集是为表达任意语言的任意字符而设计,是业界的一种标准。
UTF-8编码中,中文字符占3个字节,英文字符占1个字节;
是电子邮件、网页级其他存储或传送文字的应用中,优先采用的编码。
UTF-16be编码中,中文字符和英文字符都占2个字节。
UTF-16be中的be指的是Big Endian,也就是大端。相应地也有UTF-16le,le指的是Little Endian,也就是小端。
大端模式:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
(符合直观上认为的模式)
小端模式:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
从软件的角度理解端模式:
不同端模式的处理器进行数据传递时必须要考虑端模式的不同。在Socket接口编程中,以下几个函数用于大小端字节序的转换
1
2
3
4 #define ntohs(n) //16位数据类型网络字节顺序到主机字节顺序的转换
#define htons(n) //16位数据类型主机字节顺序到网络字节顺序的转换
#define ntohl(n) //32位数据类型网络字节顺序到主机字节顺序的转换
#define htonl(n) //32位数据类型主机字节顺序到网络字节顺序的转换其中互联网使用的网络字节顺序采用大端模式进行编址,而主机字节顺序根据处理器的不同而不同,如PowerPC处理器使用大端模式,而Pentuim处理器使用小端模式。
Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
String的编码方式
String可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为String。
1 | String str1 = "中文"; |
在调用无参数 getBytes() 方法时,默认的编码方式不是 UTF-16be。双字节编码的好处是可以使用一个 char 存储中文和英文,而将 String 转为 bytes[] 字节数组就不再需要这个好处,因此也就不再需要双字节编码。getBytes() 的默认编码方式与平台有关,一般为 UTF-8。
编码引起的问题
在IDEA中,使用FileReader
读取项目中的文本文件。由于IDEA的设置,都是默认UTF-8编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统默认的是GBK编码,所以会出现乱码。那么该如何读取GBK编码的文件?
转换流
OutputStreamWriter 类
转换流java.io.OutputStreamWriter
是Writer的子类,是从字符流到字节流的桥梁。它读取字符,并使用指定的字符集将其编码为字节。
构造方法:
OutputStreamWriter(OutputStream out)
OutputStreamWriter(OutputStream out, String charsetName)
charsetName用于指定字符集,不区分大小写。
1 | OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("C:\\download\\xx.md"), "GBK"); |
InputStreamReader 类
转换流java.io.InputStreamReader
是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。
构造方法:
InputStreamReader(InputStream in)
InputStreamReader(InputStream in, String charsetName)
:charsetName用于指定字符集,不区分大小写。
1 | InputStreamReader isr = new InputStreamReader(new FileInputStream("C:\\download\\xx.md"), "GBK"); |
转换文件编码
将GBK编码的文本文件,转换为UTF-8编码的文本文件。
- 指定GBK编码的转换流,读取文本文件
- 指定UTF-8编码的转换流,写入文本文件
1 | InputStreamReader isr = new InputStreamReader(new FileInputStream("C:\\download\\xx.md"), "GBK"); |
装饰者模式
以InputStream为例,
InputStream是抽象组件;
FileInputStream是InputStream的子类,属于具体组件,提供了字节流的输入操作;
FilterInputStream属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。
例如BufferedInputStream为FileInputStream提供缓存的功能。
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可:
1
2FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);DataInputStream装饰者提供了对更多数据类型进行输入的操作,比如int、double等基本类型。
序列化
序列化就是将一个对象转换成字节序列,该字节序列包含对象的数据、对象的类型以及对象中存储的属性。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
- 序列化:
void ObjectOutputStream.writeObject()
- 反序列化:
Object ObjectInputStream.readObject()
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
Serializable 接口
序列化的类需要实现Serializable接口,它只是一个标准,没有任何方法需要实现。但如果不去实现它的话,会抛出异常。
1 | public static void main(String[] args) throws IOException, ClassNotFoundException { |
InvalidClassException 异常
当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException
异常。
发生该异常的原因有:
- 该类的序列版本号与从流中读取的类描述符的版本号不一致
- 该类包含未知数据类型
- 该类没有可访问的无参数构造方法
Serializable
接口给需要序列化的类**提供了一个序列版本号serialVersionUID
**,用于验证序列化的对象和对应类是否版本匹配。
解决方案:
无论是否对类的定义进行修改,都不重新生成新的序列号。可序列化的类通过声明static final long serialVersionUID
来显式声明自己的序列号。
1 | public class A implements Serializable { |
IDEA自动生成序列号
Settings - Editor - Inspections - 搜索UID - Serializable class without ‘serialVersionUID’ 右侧勾选,修改Severity为Error。
transient 关键字
transient(瞬态)关键字可以使一些属性不会被序列化。
ArrayList中存储数据的数组elementData
是用transient
修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
序列化集合
当想在文件中保存多个对象的时候,可以把对象存储到一个集合中,对集合进行序列化和反序列化。
1 | ArrayList<A> list = new ArrayList<>(); |
打印流
java.io.PrintStream extends OutputStream
与其他输出流不同,永远不会抛出IOException。
特有的方法:
void print(任意类型的值)
void println(任意类型的值)
构造方法:
PrintStream(File file)
:输出至文件PrintStream(OutputStream out)
:输出至字节输出流PrintStream(String fileName)
:输出至指定的文件路径
注意:
如果使用继承自父类的write方法写数据,那么查看数据的时候会查询编码表97 -> a
;如果使用自己特有的print/println方法写数据,原样输出97 -> 97
。
打印流还可以改变输出语句System.out.print/println
的目的地:
static void System.setOut(PrintStream out)
1 | System.out.println("默认在控制台输出"); |