JSP概述

JSP(Java Server Pages)是一种动态网页开发技术。它使用JSP标签在HTML网页中插入Java代码。JSP是一种Java Servlet主要用于实现Java Web应用程序的用户界面部分

JSP原理

阅读全文 »

Cookie和Session是什么

Cookie和Session是两种会话数据的保存方式。其中,Cookie是客户端技术,Session是服务端技术。

两者具体的类封装在javax.servlet.http(时代变了)jakarta.servlet.http包中,依赖jakarta.servlet-api包即可使用jakarta.servlet.http下的类。

  1. 存储位置
    存储在客户端,一般保存在本地用户目录下的appdata

  2. 生命周期
    默认会话结束后消失(存于内存中)。

    通过setMaxAge(int time)可以设置cookie的有效期(单位是s,存到硬盘):

    • 默认值为-1
    • 如果设为0,立即删除
  3. 缺陷
    大小和数量限制:一般每个站点大约能保存20个cookie,大小限制在4kb以内。浏览器一般有300个cookie上限。
    数据安全性问题:http请求中的cookie是明文传递的。

  4. 以存储网站的上次访问时间为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class CookieDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 解决中文乱码
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html");

PrintWriter out = resp.getWriter();

// 从客户端获取Cookie
Cookie[] cookies = req.getCookies();

// 判断cookies是否存在
if (cookies != null) {
out.write("你上次访问的时间是:");

for (Cookie cookie : cookies) {
if (cookie.getName().equals("lastLoginTime")) {
long lastLoginTime = Long.parseLong(cookie.getValue());
Date date = new Date(lastLoginTime);
SimpleDateFormat dateFormat = (SimpleDateFormat) DateFormat.getDateTimeInstance();
out.write(dateFormat.format(date));
break;
}
}
} else {
out.write("这是你第一次访问本站");
}

// 服务器给客户端响应一个cookie
Cookie cookie = new Cookie("lastLoginTime", Long.toString(System.currentTimeMillis()));
// 设置cookie过期时间,以秒为单位
cookie.setMaxAge(24 * 60 * 60);
resp.addCookie(cookie);
}
}

Session

服务器会自动每一个用户(浏览器)创建一个Session对象,一个Session独占一个浏览器,只要浏览器没有关闭,这个Session就存在。

服务器在自动创建Session的时候,会同时生成一个Cookie,存储sessionId

1
2
Cookie cookie = new Cookie("JSESSIONID", sessionId);
resp.addCookie(cookie);
  1. 存储位置
    保存于服务端。

  2. 使用场景

    • 保存登录用户的信息
    • 保存购物车信息
    • 其他在整个网站中经常会使用的数据
  3. 生命周期

    • 有效期30min,可以通过两种方式设置有效期:
      • setMaxInactiveTnterval(int time),单位是s
      • web.xml中配置session有效期,单位是min
    1
    2
    3
    <session-config>
    <session-timeout>time</session-timeout>
    </session-config>
    • 还可以通过HttpSession的invalidate()方法,手动使session失效
  4. 相关方法

    Session也能和ServletContext一样,实现不同Servlet之间的通信。并且该方式**优于利用ServletContext**。

1
2
3
4
5
6
7
8
9
10
11
String password = request.getParameter("password");
HttpSession session = request.getSession();

String name = (String)session.getAttribute("userName");
session.setMaxInactiveTnterval(30 * 60);

session.setAttribute("password", password);

session.removeAttribute("password");

session.invalidate();

Servlet是什么

Servlet(Server + Applet),一个Servlet就是一个Java类,并提供 基于请求-响应模式 的Web服务

要使用servlet需要添加javax.servlet-apiJar包(时代变了)jakarta.servlet-api


Servlet的处理流程

  1. 接收客户端的HTTP请求路径及请求内容

  2. 根据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>
  3. 将请求转发给Servlet对应的service方法(会传递HttpServletRequest对象和HttpServletResponse对象作为方法参数)

  4. service方法根据请求是 get/post/… 转发给 doGet/doPost方法处理

  5. 通过HttpResponse对象,将响应返回给客户端


Servlet生命周期

init → service(doGet、doPost…) → destroy


管理Servlet的配置信息

通过ServletConfig对象获取配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
<servlet>
<init-param>
<param-name>参数名1</param-name>
<param-value>参数值1</param-value>
</init-param>
...
<servlet-name>servlet名称(最好与类名相同)</servlet-name>
<servlet-class>类名</servlet-calss>
</servlet>
<servlet-mapping>
<servlet-name>与<servlet>元素中的该元素内容相同</servlet-name>
<url-pattern>映射路径</url-pattern>
</servlet-mapping>

每个Servlet支持设置多个<init-param>,Servlet初始化过程中,<init-param>参数将被封装到ServletConfig对象中。在Servlet中,通过调用ServletConfig对象,就可以利用配置信息:

1
2
ServletConfig config = this.getServletConfig();
String value1 = config.getParameter("参数名1");//根据查找参数名,获取参数值

通过ServletContext共享Servlet的配置信息

Servlet容器在启动时,会为每个Web应用创建一个对应的ServletContext对象,它代表当前的Web应用。在一个Servlet中保存的数据,可以在另一个Servlet中获取。

1
2
3
4
5
<context-param>
<param-name>参数名1</param-name>
<param-value>参数值1</param-value>
</context-param>
...

<context-param>是在各个具体的<servlet>元素之外的,同样可以设置多个<context-param>。在任意具体的Servlet中,通过调用ServletContext对象,就可以利用配置信息:

1
2
ServletContext context = this.getServletContext();
String globalValue1 = context.getInitParameter("参数名");

通过ServletContext的属性(attribute)实现不同Servlet之间的通信

事先不知道,无法预先配置的信息,应该如何共享?(比如将购物车中的商品共享给结算页面)

在具体的Serlvet中,通过ServletContext对象的属性(attribute)设置希望共享的信息:

1
2
3
4
ServletContext context = this.getServletContext();
context.setAttribute("属性名","属性值");
String attribute = (String) context.getAttribute("属性名");//getAttribute的返回值是Object类型
context.removeAttribute("属性名");

读取外部资源配置文件信息

通过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
2
3
4
5
6
7
ServletContext context = this.getServletContext();
URL url = context.getResource("外部资源配置文件的相对路径");
InputStream in = url.openStream();
/**
利用编写的一个工具类,读取属性值
String property1 = GeneralUtil.getProperty("属性名", in);
*/

getResourceAsStream(“外部资源配置文件的路径”)

1
2
3
4
5
6
7
8
9
10
11
12
public class ServletDemo3 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 开头的"/"表示当前web项目,具体为target下的servlet-02-1.0-SNAPSHOT目录 (目录名根据pom.xml生成)
// 参数s 为相对路径
InputStream is = this.getServletContext().getResourceAsStream("/WEB-INF/classes/db.properties");
Properties prop = new Properties();
prop.load(is);
String username = prop.getProperty("username");
String passwd = prop.getProperty("passwd");
resp.getWriter().print(username + ": " + passwd);
}

getRealPath(“外部资源配置文件的相对路径”)

获取外部资源配置文件的绝对路径

1
String realPath = context.getRealPath("外部资源配置文件的相对路径");

HttpServletRequest类

常见应用

获取前端传递的参数

  1. getParameter()
  2. getParameterValues()

请求转发

请求转发就是将当前的HttpServletRequestHttpServletResponse对象交给指定的web组件处理。对客户端来说,是一次请求,一次响应,浏览器的URL不变。

步骤:

  1. 获取请求转发对象
    即获取RequestDispatcher类的对象(由Servlet容器创建,封装由路径所标识的服务器资源)。
    获取请求转发对象有两种方式:
    • 通过HTTPServletRequest对象获取:
      RequestDispatcher rd = request.getRequestDispatcher("绝对路径/相对路径");
    • 通过ServleContext对象获取:
      RequestDispatcher rd = this.getServletContext().getNamedDispatcher("servlet-name"); 或者RequestDispatcher rd = this.getServletContext().getRequestDispatcher("绝对路径");
  2. 调用转发对象的forward(HttpServletRequest req, HttpServletResponse resp)方法

例子:

1
2
3
4
5
6
7
8
9
10
public class ServletDemo2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext context = this.getServletContext();
// 路径"/getInitParam"是在web.xml文件中配置的<servlet-mapping>的<url-pattern>
// 开头的"/"表示当前web项目,已涵盖tomcat设置的application context部分
// forward 实现请求转发
context.getRequestDispatcher("/getInitParam").forward(req, resp);
}
}

HttpServletResponse类

向浏览器发送数据的方法

  1. getOutputStream()
  2. getWriter()

向浏览器发送响应头的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void setCharacterEncoding(String var1);

void setContentLength(int var1);

void setContentLengthLong(long var1);

void setContentType(String var1);

void setDateHeader(String var1, long var2);

void addDateHeader(String var1, long var2);

void setHeader(String var1, String var2);

void addHeader(String var1, String var2);

void setIntHeader(String var1, int var2);

void addIntHeader(String var1, int var2);

常见应用

向浏览器输出消息

1
2
3
4
5
6
7
8
9
10
11
12
13
public class GetServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

ServletContext context = this.getServletContext();
String username = (String) context.getAttribute("username");

// 设置响应内容的类型
resp.setContentType("text/html");
//resp.setCharacterEncoding("utf-8");
resp.getWriter().print("用户名:" + username);
}
}

下载文件

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 FileServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 获取下载文件的路径
String realPath = "C:\\Users\\Hunter\\IdeaProjects\\javaweb-02-servlet\\response\\src\\main\\resources\\20180422 卡特迈国家公园和自然保护区里午睡的灰熊幼崽,阿拉斯加州 1920x1080.jpg";
System.out.println("下载文件的路径:" + realPath);

// 2. 下载的文件名
String fileName = realPath.substring(realPath.lastIndexOf("\\") + 1);
// 3. 让浏览器能够支持下载,支持中文文件名编码
resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8"));
// 4. 获取下载文件的输入流
FileInputStream fis = new FileInputStream(realPath);
// 5. 创建缓冲输入流
BufferedInputStream bis = new BufferedInputStream(fis);
// 6. 获取Outputstream对象
ServletOutputStream sos = resp.getOutputStream();
// 7. 将FileOutputStream流写入到缓冲区数组,使用OutputStream将缓冲区中的数据输出到客户端
byte[] bytes = new byte[1024]; // 存储每次读取的数据
int len = 0; // 记录每次读取的字节个数
while ((len = bis.read(bytes)) != -1) {
sos.write(bytes, 0, len);
}
bis.close(); // 关闭缓冲流,基本流也会被自动关闭
sos.close();
}
}

验证码功能

实现方式:

  1. 前端实现
  2. 后端实现,需要用到java的图片类,产生一个图片

实现重定向

请求重定向就是通过HttpServletResponse对象发送给客户端一个新的URL地址,让其重新请求。对客户端来说,是两次请求,两次响应

请求重定向的方法:resp.sendRedirect("绝对路径");,其中路径需要写明tomcat设置的application context前缀,可用req.getContextPath()获取绝对路径前缀。

Maven是什么

Maven是一个免费、开源的,用Java编写的项目构建和依赖管理工具

Maven的特点

  • 默认的目录结构
  • 第三方依赖管理
    通过配置pom.xml文件,从远程仓库下载相关Jar包。解决了依赖维护问题
  • 提供了一致的项目构建管理方式
    生命周期。减少了构建工作量
  • 插件式架构,大量的可重用插件
  • 方便地集成于IDE

MAVEN的安装及配置

  1. 官网下载压缩包,解压到合适文件目录下。

  2. 配置环境变量 - 系统变量

    • M2_HOME = 文件目录
    • 添加%M2_HOME%/bin到Path
  3. 配置maven的阿里云镜像

    配置方式见阿里云仓库服务,修改maven配置文件conf\settings.xml即可

    1
    2
    3
    4
    5
    6
    <mirror>
    <id>aliyunmaven</id>
    <mirrorOf>*</mirrorOf>
    <name>阿里云公共仓库</name>
    <url>https://maven.aliyun.com/repository/public</url>
    </mirror>
  4. 建立本地仓库

    本地仓库也是在conf\settings.xml中配置,默认地址为${user.home}/.m2/repository

    1
    2
    3
    4
    5
    6
    <!-- localRepository
    | The path to the local repository maven will use to store artifacts.
    |
    | Default: ${user.home}/.m2/repository
    <localRepository>/path/to/local/repo</localRepository>
    -->

基本操作命令

命令 作用
mvn archetype:generate 使用模板生成项目
mvn compile 编译源代码
mvn test 单元测试
mvn package 打包项目,Java项目→Jar;Web项目→War
mvn deploy 部署,把打包后的项目上传发布到远程仓库
mvn site 生成项目相关的站点、在线文档
mvn clean 清理,删除构建的Jar包、class文件等
mvn install 把打包的目录拷到本地仓库

在IDEA中使用Maven

创建一个Maven项目

方式一:Create from archetype

是一种后续省心的创建方式。

image-20220111011500763

创建后,目前默认产生的src/main/webapp/WEB-INF/web.xml文件的内容比较老旧,如下:

1
2
3
4
5
6
7
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>

需要更新为如下较新的版本:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
</web-app>

另外,需要在src/main目录下创建两个目录:javaresources。2021.3.2版本的IDEA在main目录上右键 - New - Directory,就会默认出现这两个目录名,选中创建即可。


方式二:不勾选Create from archetype

原生态一点,直接打包好的项目可能会缺少部分文件,需要手动添加才能保证项目的顺利运行。

  1. 创建完成后,右键项目文件夹 - Add Framwork Support…

    Add Framwork Support...

  2. 勾选Web Application - Versions:4.0 - Create web.xml - OK

    Web Application

  3. 会在项目文件夹下,生成一个web文件夹

    可以在web/WEB-INF/web.xml中看到[方式一](#方式一:Create from archetype)更新的内容。

  4. pom.xml中添加了需要的依赖之后,需要手动添加依赖的jar包到WEB-INF/lib文件夹下

    • File - Project Structure - Project Settings - Artifacts - 选择目标web应用 - Output Layout - Avaliable Elements

      可以看到项目,项目下是由依赖关系添加的Jar包

      Avaliable Elements

    • 双击这些Jar包,它们就会自动添加到左边的WBE-INF/lib目录下(首次添加时会自动创建lib目录),OK关闭窗口即可

      添加Jar包到

只有这样,最后打包的项目中才有依赖的Jar包,项目才能正常运行。

参考:Idea使用maven创建项目常见错误和注意点


选择自定义的maven配置文件

File - Settings - 搜索maven:

  • Maven home path:选择使用的maven的路径

    • Bundled(Maven 3)是IDEA捆绑的Maven
    • 也可选择自己下载的Maven
  • User settings file:用户配置文件

    IDEA默认的maven配置文件为${user.home}/.m2/settings.xml。上文MAVEN的安装及配置中提到的为了快速下载依赖而配置的maven的阿里云镜像想要继续生效,有3种方式

    1. 修改${user.home}/.m2/settings.xml(默认文件,是最省心的方式)
    2. IDEA中修改maven配置文件的路径,更改为maven目录下的conf\settings.xml
    3. 在便于管理的目录下,自定义一个maven配置的xml文件(私以为${user.home}/.m2/目录下就很好)

pom.xml

pom(project object model, 项目对象模型),是Maven的核心配置文件。

  • groupIdartifactIdversion构成项目的唯一标识(坐标),不能重复。

Maven的生命周期

maven有三套独立的生命周期

  1. clean(删除构建的Jar包、class文件等)
    pre-clean → clean → post-clean
  2. site(生成站点文档)
    pre-site → site → post-site → site-deploy
  3. default
    1. validate (验证项目是否正确,所需信息是否有缺失)
    2. process-resources(将资源文件复制到项目目录下,之后打包用)
    3. compile (编译源代码)
    4. test (单元测试)
    5. package (打包)
    6. install (把打包的项目拷到本地仓库下)
    7. deploy (把打包的项目上传到远程仓库)

Maven的问题

由于Maven的约定大于配置,可能会遇到配置文件无法导出或无法生效的问题

解决方案:<build>中配置<resource>节点,防止资源导出失败的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>**/*.properties</exclude>
<exclude>**/*.xml</exclude>
</excludes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>

Tomcat简介

市场上有很多支持JSPServlet开发的Web服务器,Tomcat就是其中之一,并且是免费的。


安装及配置

官网下载压缩包,解压到合适的目录文件下,运行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

手动

将自己写的网页放到tomcat中网站应用(webapps)文件夹下,就可以访问了。

版本控制工具

版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。版本控制能方便查看更改历史,备份以及恢复以前的版本,保证多人协作不出问题。

Git对待数据的方式,更像是把数据看作是对小型文件系统的一组快照。每次提交更新或者在Git中保存项目状态时,它主要对当时的全部文件制作一个快照保存这个快照的索引。为了高效,如果文件没有修改,Git不再重新存储该文件,而只是保留一个链接指向之前存储的文件,Git对待数据更像是一个快照流


安装Git和TortoiseGit

TortoiseGit

TortoiseGit提供了Git的图形化操作界面,Git工作区的目录和文件的图标符加了标识版本控制状态的图像,可以非常直观地看到哪些文件被更改了需要提交。通过对右键菜单的扩展,可以非常方便操作Git版本库。

通过设置,选择Git提供的ssh客户端,这样**在下载ssh协议的代码仓库时,命令行与图形界面都可以使用同一套公钥和密钥。


Git基本配置

3种配置等级

  1. 系统配置:git config --system

    所有用户都有效,存放在git的安装目录下:%Git%/etc/gitconfig

  2. 用户配置:git config --global

    只对当前用户有效,存放在用户目录下:~/.gitconfig

  3. 仓库配置:git config --local

​ 只对当前项目有效,存放在项目目录下:.git/config


配置个人身份

1
2
git config --global user.name "Hunter"
git config --global user.email hspecial@163.com

这个配置信息会在Git仓库提交的修改信息中体现和Git服务器认证使用的密码或者公钥密码无关


文本换行符配置

这一节的内容仍然是给我造成困惑的部分,感觉从参考资料来看,跨平台的项目开发应该要自动转换换行符。但是在我目前有限的开发经验来看,似乎并没有对项目造成影响,怀疑可能是IDE完成了相应的工作。

默认的配置状态为:core.autocrlf=true,暂且不去作修改,待日后有了深刻的理解再来完善本节内容。


编码配置

  • 中文编码支持

    • git config --global gui.encoding utf-8

      图形化界面采用的编码

    • git config --global i18n.commitencoding utf-8

      git commit log存储时,采用的编码

    • git config --global i18n.logoutputencoding utf-8

      查看git log时,显示所采用的编码

  • 显示路径中的中文

    git config --global core.quotepath false


与服务器的认证配置

http/https协议认证

添加HTTPS证书信任:git config http.sslverify false


SSH协议认证

使用公钥认证,无需输入密码,加密传输,操作便利又保证安全性。


配置过程
  1. 生成公钥

    在bash命令行中输入如下命令:

    1
    ssh-keygen -t rsa -C "hspecial@163.com"

    一路回车,就能在用户目录下~/.ssh中,生成公钥id_rsa.pub

  2. 添加公钥id_rsa.pub中的内容到代码平台


新增公钥

出于某些情况,你可能要在另外的平台或者另外的项目,使用不同的邮箱来生成公钥,以便和代码平台的账号相匹配

  1. 生成公钥

    提示Enter file in which to save the key(输入要保存密钥的文件,即id_rsa文件)”时,输入密钥文件的保存位置。为了和用户配置等级的密钥区分,我选择在默认公钥的目录下创建对应的平台或项目名的文件夹。输入保存密钥的文件绝对路径即可。

  2. 添加公钥id_rsa.pub中的内容到代码平台


Git基本命令

Git版本控制下的工程区域

  1. 暂存区 stage

    一般存放在工程根目录.git/index文件中,因此也可以把暂存区叫做索引。

  2. 工作区 working directory

    日常工作的代码文件或者文档所在的文件夹。

  3. 版本库 Repository

    .git文件夹就是Git的版本库,也可以叫本地仓库


Git版本控制下的三种文件状态

  1. 已修改 modified

    修改了某个文件,但还没有提交保存

  2. 已暂存 staged

    把已修改的文件放在下次提交时要保存的清单中

  3. 已提交 committed

    文件已经被安全地保存在本地数据库中


常用命令

工程准备

  • git init:本地目录下新建git项目仓库

  • git clone [URL]复制远端工程到本地

    如果所在的项目Git服务器已支持git-lfs对二进制文件进行了区别管理,那么复制工程的时候**务必使用git lfs clone [URL]**,否则克隆操作无法下载到工程中的二进制文件,工程内容不完整。


查看修改

  • git diff:查看工作区的修改内容(当前索引与上次提交/任意两个节点(分支)之间的差异)

    • git diff --cached:当前索引与上次提交之间的差异

    添加--name-status参数,可以只看文件列表

  • git status:查看工作区和暂存区的文件状态

    该命令能看到修改的git文件是否已被暂存,新增的文件是否纳入了git版本库的管理。

    • Untracked files:新建但未被跟踪的文件
    • Changes not staged for commit:修改但未被暂存的文件
    • Changes to be committed:已修改并已暂存的文件

文件修改后提交推送

  • git add新增文件到暂存区

    把文件添加到暂存区,是提交修改文件之前的必要操作。如果文件已经被git追踪,即曾经提交过,在Git的早期版本中,需要git add再提交;在较新的版本中,无需git add即可提交

  • git rm删除文件到暂存区

    执行git rm后,通过git status查看时,会有deleted: xxx的提醒。之后进行提交,对应的文件就不再受git工程的管理直接从硬盘中删除文件,然后对该文件执行git commit,git会自动将删除的文件从索引中移除,效果相同

  • git mv移动文件到暂存区

    相当于mv操作的基础上,用git将操作传入了暂存区。

  • git commit提交更改的文件

    将暂存区中的文件改动提交到本地的版本库,一般需要附带提交描述信息,所以常见的用法是:

    git commit file_name -m "commit message"

    如果要一次性提交所有在暂存区改动的文件到版本库,可以执行git commit -am "commit message"

  • git push:将本地版本库的分支推送到远端对应的分支

    常见的推送命令格式:git push origin branch_name:new_branch_name

    new_branch_name是推送成功后,在远端服务器上的分支名:new_branch_name可以不写,远端分支名就和本地分支名相同。


查看日志

  • git log:查看当前分支上的提交日志

    git log默认按提交时间的由近到远列出所有的历史提交日志,每个日志基本包含:

    1. 提交节点
    2. 作者信息
    3. 提交时间
    4. 提交说明
    • --name-status

      能列出涉及改动的具体文件


分支管理

  • git branch:列出所有本地分支

    如果想查看远端服务器上的分支,-r即可,返回的分支名带origin前缀,即表示在远端。如果想查看远端和本地的所有分支,-a即可。

  • 新建分支

    默认基于当前节点创建分支

    • git checkout -b new_branch_name 新建后会自动切换到新分支
    • git branch new_branch_name 新建后不会切换到新分支
  • 删除分支

    • git branch -d branch_name

    • git branch -D branch_name 大写的D表示强制删除

      当目标分支上包含未合并的改动,则需要通过强制删除来操作。

    • git branch -d -r branch_name 删除服务器上的远程分支

      删除后还需要推送到服务器git push origin : branch_name

  • 切换分支

    • git checkout branch_name

    • git checkout -f branch_name

      如果当前分支工作区存在修改未提交的文件,与目的分支上的内容冲突,会导致切换失败,则需要使用git checkout -f进行强制切换

    checkout的对象可以是分支,也可以是某个提交节点或者节点下的某个文件

  • 从远端更新分支

    • git pull origin remote_branch:local_branch

      从远端服务器中获取某个分支的更新,再**与本地指定的分支进行自动合并(merge)**,如果远程指定的分支与本地指定的分支相同,则可以直接执行git pull origin remote_branch

    • git fetch origin remote_branch:local_branch

      git pull不同,git fetch在获取到更新后,不会自动合并,而是留给用户一个操作空间,确认git fetch内容符合预期后,再决定是否手动合并节点


分支合并

合并目标分支内容到当前分支,以下两种方式都可以达到目的

  • git merge branch_name

    git会将指定的分支与当前分支进行比较,找出二者最近的一个共同节点base,之后将指定分支在base之后分离的节点合并到当前分支上实际上是分支之间,差异提交的节点的合并

  • git rebase branch_name

但是二者的实现机制和对合并后节点造成的影响有很大差异,有者各自的风险


撤销操作

  • git reset commit_id

    可将工作区内容回退到历史提交节点

  • git checkout .

    回退本地所有未提交的修改。这是一条有风险的命令,不给用户任何确认机会。它会取消本地工作区的修改,用暂存区的所有文件直接覆盖本地文件,达到回退内容的目的

    如果仅仅想回退某个文件的未提交改动,可以使用git checkout -filename;如果想将工作区回退到某个提交版本,可以使用git checkout commit_id

概述

水杉码园使用Go语言,基于Gitea开发。本文用于记录如何入坑开发。


开发机

先用Cisco的VPN登录,再连接开发机。

待完善……


Windows环境下clone代码

水杉码园的代码存储在gitlab上:ShuiShan / Gitea / gitea · GitLab。并且该地址需要拥有项目管理权限的人员(徐柴笛、周成义、李苗进、顾业鸣)进行授权才可访问。

clone代码的操作:

  1. 安装Git
  2. Clone With HTTPS
    1. 先在命令行执行git config --global http.sslVerify false
    2. git clone https://code.kfcoding.com/ShuiShan/Gitea/gitea.git

Windows下环境搭建

  1. 安装go

  2. 安装node.js(一定要勾选会附带安装chocolatey的选项)

  3. 安装make(choco install make

  4. 设置npm的国内镜像代理npm config set registry https://registry.npm.taobao.org

  5. Clone的项目文件夹下(\gitea),执行TAGS="bindata" make build

  6. 本地测试,执行./gitea web

  7. 浏览器访问http://localhost:3000/

  8. 登录水杉码园,配置本地数据库信息,记住要在可选设置中设置管理员账号

    管理员账号设置

node版本如何升级 - 知乎


使用VS Code进行开发


课程批量导入学生

  • 入口:/org/:org/teams/new_all_student

导入完成后,课程团队下会生成指定的团队:

团队为成员-仓库的形式

  • Owners

    管理员-管理的仓库

  • AllStuRead

    教师有操作权限,学生只有读权限。用于老师布置作业,分享数据、资料。

  • AllStuWrite

    所有学生都有操作权限

课程中的仓库,除了AllStuWrite和AllStuRead这两个公开仓库之外,每个学生还有自己的个人仓库,目前以学号作为仓库名。仓库描述为student u.Name(学号) u.FullName(用户名)'s repo


代码

  1. routes.go下,m.Post("/teams/new_all_student", bindIgnErr(auth.CreateAllStuTeamForm{}), org.NewAllStuTeamPost)

    • auth.CreateAllStuTeamForm{}相当于是前端的表单

      其中的Studentlist表示的上传的excel文件

    这个Post函数将前端的表单传入后端的NewAllStuTeamPost函数,

  2. 细看一下NewAllStuTeamPost方法

    • 首先是通过前端的form获取相应的基本信息

    • 再来我们可以先看一下这个函数最后的部分GetCache().Set

      这里把课程ID(Organization.ID)设置了1分钟的缓存

    • 再回过头来看上边的代码,如果能在缓存中获取到课程ID的缓存,说明之前一分钟内有过成功导入的记录,就会给出一个不要多次导入的提示并结束导入学生的操作


批量移除选中的仓库


批量删除成员

数组与矩阵

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String replaceSpace(String s) {
StringBuffer str = new StringBuffer(s); //将String转换为StringBuffer(可变长度)
int P1 = str.length() - 1; //模拟指针
for (int i = 0; i <= P1; i++)
if (str.charAt(i) == ' ')
str.append(" "); //出现一个空格,就在字符串末尾添加2个任意字符

int P2 = str.length() - 1; // 模拟指针,初始指向 将空格替换后的最后一位
while (P1 >= 0 && P2 > P1) { //从后往前遍历,能保证原来的字符串不会因为替换字符而改变
char c = str.charAt(P1--); // 获取原始字符串的最后一位字符
if (c == ' ') {
str.setCharAt(P2--, '0');
str.setCharAt(P2--, '2');
str.setCharAt(P2--, '%');
} else {
str.setCharAt(P2--, c);
}
}
return str.toString();
}

时间复杂度和空间复杂度都是$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
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 int[] spiralOrder(int[][] matrix) {
if (matrix.length == 0) { // 矩阵为空
return new int[0];
}

int left = 0;
int right = matrix[0].length - 1;
int top = 0;
int bottom = matrix.length - 1;
int[] result = new int[(right + 1) * (bottom + 1)];
int index = 0;

while(true) {
for (int i = left; i <= right; i++) // 遍历top行
result[index++] = matrix[top][i];
if (++top > bottom) // 没有新的行,则遍历完成
break;

for (int i = top; i <= bottom; i++)
result[index++] = matrix[i][right];
if (--right < left) // 没有新的列
break;

for (int i = right; i >= left; i--)
result[index++] = matrix[bottom][i];
if (--bottom < top) // 没有新的行
break;

for (int i = bottom; i >= top; i--)
result[index++] = matrix[i][left];
if (++left > right) // 没有新的列
break;
}
return result;
}

50. 第一个只出现一次的字符位置

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例:

1
2
3
4
5
s = "abaccdeff"
返回 "b"

s = ""
返回 " "

限制:

0 <= s 的长度 <= 50000

统计字符是否仅出现一次,可以用容器存放每个字符是否只出现一次。这个容器可以根据<字符>来存放<是否多次出现>的信息。因此,可以利用哈希表

1
2
3
4
5
6
7
8
9
10
11
12
public char firstUniqChar(String s) {
HashMap<Character, Boolean> hm = new HashMap(); // 用布尔值表示是否只有一个字符,比用Integer统计出现次数节约空间
char[] s2c = s.toCharArray();
for(char c : s2c) {
hm.put(c, !hm.containsKey(c)); // 如果已有该字符,false;否则true
}
for(char c : s2c) { // 按字符顺序遍历,能返回第一个只出现一次的字符
if(hm.get(c) == true)
return c;
}
return ' ';
}

时间复杂度$O(n)$​,空间复杂度$O(1)$​


栈、队列、堆

9. 用两个栈实现队列

剑指 Offer 09. 用两个栈实现队列 - 力扣(LeetCode)

  • 栈是LIFO(后进先出),队列是FIFO(先进先出)。
  • 将一个栈专门用于插入整数,另一个栈专门用于删除整数
  • Stack类已被Java不推荐使用LinkedList基于双向链表实现,只能顺序访问,但可以快速插入和删除元素。LinkedList可用作栈、队列和双向队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private LinkedList<Integer> list1, list2;

public CQueue() {
list1 = new LinkedList<>();
list2 = new LinkedList<>();
}

public void appendTail(int value) {
list1.addLast(value); // 加到链表尾部
}

public int deleteHead() {
if (!list2.isEmpty()) { //list2中的元素都在list1之前进入栈中
return list2.removeLast();
}
if (list1.isEmpty()) { //模拟的队列中没有元素了
return -1;
}

while (!list1.isEmpty()) { //将list1中已存在的元素都移入list2中
list2.addLast(list1.removeLast());
}
return list2.removeLast();
}

时间复杂度:$O(n)$(deleteHead()函数在N次队首元素删除操作中总共需完成N个元素的倒序),空间复杂度$O(n)$。


30. 包含min函数的栈

剑指 Offer 30. 包含min函数的栈 - 力扣(LeetCode)

既然是定义栈,那么push和pop功能显然不用再变动,重点在于实现min函数。栈中的最小元素会随着元素的入栈和出栈动态变化,因此需要记录每个状态对应的当前最小元素。可以构造一个辅助栈来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private LinkedList<Integer> stack, minStack;

public MinStack() {
stack = new LinkedList<>();
minStack = new LinkedList<>();
}

public void push(int x) { //插入元素,同时记录当前最小的元素
stack.addLast(x);
minStack.addLast(minStack.isEmpty() ? x : Math.min(minStack.getLast(), x));
}

public void pop() { // 同步更新最小值
stack.removeLast();
minStack.removeLast();
}

public int top() {
return stack.getLast();
}

public int min() {
return minStack.getLast();
}

31. 栈的压入、弹出序列

剑指 Offer 31. 栈的压入、弹出序列 - 力扣(LeetCode)

一个序列是否为栈的弹出序列的规律:

  1. 如果当前栈顶的数字是下一个弹出的数字,则弹出;
  2. 如果当前栈顶的数字不是下一个弹出的数字,把还未入栈的数字压入栈中,直至栈顶的数字是下一个弹出的数字
  3. 如果所有数字都入栈的过程中,栈顶数字始终不是需要弹出的数字,则不可能是弹出序列
  4. 栈中数字全部顺利出栈,则为弹出序列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean validateStackSequences(int[] pushed, int[] popped) {
if(pushed.length == 0)
return true;
LinkedList<Integer> stack = new LinkedList<> ();
int i = 0;
for(int num : pushed) { // 遍历入栈顺序数组
stack.addFirst(num); // 入栈
while(!stack.isEmpty() && stack.getFirst() == popped[i]) { // 当前栈不为空,且 栈顶元素为出栈元素时
stack.removeFirst(); // 出栈
i++;
}
}
return stack.isEmpty(); // 如果都顺利出栈,则说明弹出序列是正确的
}

时间复杂度:$O(n)$(每个元素最多进栈1次,出栈1次,$O(2n)$)​;空间复杂度:$O(n)$


40. 最小的K个数

剑指 Offer 40. 最小的k个数 - 力扣(LeetCode)

快速排序每次都能将选定的哨兵置于排序完成后的最终位置,当前的哨兵最终位置索引为K时,比它小的K个数将全在左侧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public int[] getLeastNumbers(int[] arr, int k) {
if (k >= arr.length)
return arr; //不限制输出顺序
return quickSort(arr, k, 0, arr.length - 1);
}

private int[] quickSort(int[] arr, int k, int left, int right) {
int i = left, j = right;
while (i < j) { // 分别从左右开始,直至"相遇";默认取第一个元素为哨兵(arr[left])
while (i < j && arr[j] >= arr[left]) { // 从右往左,找到比哨兵小的数(且保证左右未相遇)
j--;
}
while (i < j && arr[i] <= arr[left]) { // 从左往右,找到比哨兵大的数
i++;
}
swap(arr, i, j); // 交换位置(比哨兵小的放在左边,大的放在右边)
}
swap(arr, i, left); // 将哨兵置于最终位置i
// 【递归判断】优化时间复杂度的关键
if (i > k) { // 确认最终位置的哨兵,其左侧的数 > k,比最终需要的数多,继续递归
return quickSort(arr, k, left, i - 1);
}
if (i < k) {
return quickSort(arr, k, i + 1, right);
}
return Arrays.copyOf(arr, k); //哨兵左侧的数 = k 个,复制数组左侧的k个数,无需再进行完整的排序
}

private void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
  • 时间复杂度$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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Queue<Integer> minHeap, maxHeap;

public MedianFinder() {
minHeap = new PriorityQueue<>(); // 默认构造小顶堆,保存较大的一半
maxHeap = new PriorityQueue<>((x, y) -> (y - x)); //lambda表达式,大顶堆,保存较小的一半
}

public void addNum(int num) {
if (minHeap.size() == maxHeap.size()) { // 添加新的整数后,会是奇数个数值
maxHeap.offer(num);
minHeap.offer(maxHeap.poll()); //取出最大堆中最大的数放入最小堆中 --> 中位数在最小堆的堆顶
} else { // 添加新的整数后,会是偶数个数值(奇数个数值时,最小堆的数值更多)
minHeap.offer(num);
maxHeap.offer(minHeap.poll()); // 取出最小堆中的最大数放入最大堆
}
}

public double findMedian() {
return minHeap.size() == maxHeap.size() ? (minHeap.peek() + maxHeap.peek()) / 2.0 : minHeap.peek();
}

时间复杂度:

  • 查找中位数$O(1)$:获取堆顶元素使用$O(1)$时间;
  • 添加数字$O(\log N)$:堆的插入和弹出操作使用$O(\log N)$时间。

空间复杂度:$O(N)$


59.1 滑动窗口的最大值

剑指 Offer 59 - I. 滑动窗口的最大值(单调队列,清晰图解) - 滑动窗口的最大值 - 力扣(LeetCode)

如何在窗口每次滑动时,获取最大值?使用双端队列存储当前滑动窗口中的最大值以及在后续窗口中潜在的最大值

每轮窗口滑动:

  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
private Deque<Integer> deque;
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0) {
return new int[0];
}
int[] result = new int[nums.length - k + 1];
int index = 0;
deque = new LinkedList<>(); //双端队列,存储每个窗口中的最大值
// 先形成一个窗口
for(int i = 0; i < k; i++) {
while(!deque.isEmpty() && deque.getLast() < nums[i]) { // 队列不为空,且队尾元素 < 当前进入窗口的数字
deque.removeLast();
}
deque.addLast(nums[i]);
}
result[index++] = deque.getFirst(); // 第一个窗口中的最大值入队
// 窗口形成后
for(int i = k; i < nums.length; i++) {
if(nums[i - k] == deque.getFirst()) // 如果滑出窗口的数是队首数字
deque.removeFirst(); // 队首数字也出队
while(!deque.isEmpty() && deque.getLast() < nums[i]) { // 队列不为空,且队尾元素 < 当前进入窗口的数字
deque.removeLast();
}
deque.addLast(nums[i]);
result[index++] = deque.getFirst();
}
return result;
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


双指针

57.1 和为S的两个数字

剑指 Offer 57. 和为s的两个数字 - 力扣(LeetCode)

递增排序的数组,查找两个数使得和为s——双指针

1
2
3
4
5
6
7
8
9
10
11
12
13
public int[] twoSum(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left < right) {
if(nums[left] + nums[right] == target) {
return new int[] {nums[left], nums[right]};
} else if (nums[left] + nums[right] < target) { //需要更大的数
left++;
} else (nums[left] + nums[right] > target) { //需要更小的数
right--;
}
}
return new int[0];
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$


57.2 和为s的连续正数序列

剑指 Offer 57 - II. 和为s的连续正数序列 - 力扣(LeetCode)

连续正整数序列 & 求和——双指针

  • 遍历的终点:序列中最小的数 * 2 + 1 > target

  • 双指针可以模拟滑动窗口

    滑动窗口的右指针无需向左移动,必然能遍历所有解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int[][] findContinuousSequence(int target) {
int left = 1, right = 2;
List<int[]> list = new ArrayList<>();
int sum = 3; // 初始滑动窗口中数字的和
while (left <= target / 2) {
if (sum == target) {
int[] result = new int[right - left + 1];
for(int i = 0; i < right - left + 1; i++) {
result[i] = left + i;
}
list.add(result);
}
if (sum < target) { // 向右移动右指针
sum += ++right;
} else { // 连续正数的和 >= target
sum -= left++; //向右移动左指针
}
}
return list.toArray(new int[0][]); //参数为 返回的数组类型(存储数组的数组),不指定则为Object
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


58.1 翻转单词顺序列

剑指 Offer 58 - I. 翻转单词顺序 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String reverseWords(String s) {
s = s.trim(); // 去除首尾空格
StringBuilder sb = new StringBuilder();
int i = s.length() - 1;
int j = i;
while (i >= 0) {
while(i >= 0 && s.charAt(i) != ' ') { //找到句中空格或遍历结束
i--;
}
sb.append(s.substring(i+1, j + 1) + ' ');
while(i >= 0 && s.charAt(i) == ' ') { //跳过多余的空格
i--;
}
j = i;
}
return sb.toString().trim();
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


58.2 左旋转字符串

剑指 Offer 58 - II. 左旋转字符串 - 力扣(LeetCode)

1
2
3
4
5
6
public String reverseLeftWords(String s, int n) {
StringBuilder sb = new StringBuilder();
sb.append(s.substring(n, s.length()));
sb.append(s.substring(0, n));
return sb.toString();
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


链表

6. 从尾到头打印链表

剑指 Offer 06. 从尾到头打印链表 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
public int[] reversePrint(ListNode head) {
List<Integer> list = new ArrayList<>();
while(head != null) {
list.add(head.val);
head = head.next;
}
int[] result = new int[list.size()];
for(int i = result.length - 1; i >= 0; i--) {
result[i] = list.get(result.length - 1 - i);
}
return result;
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


18. 删除链表的节点

剑指 Offer 18. 删除链表的节点 - 力扣(LeetCode)

  • 无需存储所有节点,找到被删的节点,修改前一个节点的next节点即可——双指针
1
2
3
4
5
6
7
8
9
10
11
12
13
public ListNode deleteNode(ListNode head, int val) {
if(head.val == val) { // 头结点即为被删的节点
return head.next;
}
ListNode pre = head;
ListNode cur = pre.next;
while(cur.val != val) {
pre = cur;
cur = cur.next;
}
pre.next = cur.next;
return head;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$​


22. 链表中倒数第K个节点

剑指 Offer 22. 链表中倒数第k个节点 - 力扣(LeetCode)

双指针

1
2
3
4
5
6
7
8
9
10
11
12
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode right = head;
ListNode left = head;
for(int i = 0; i < k; i++) { //只要让第二个指针落后于第一个指针k位,遍历完成时,即能得到倒数第k个节点
right = right.next;
}
while(right != null) {
right = right.next;
left = left.next;
}
return left;
}

时间复杂度:$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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public ListNode detectCycle(ListNode head) {
ListNode fast = head; // 创建一快一慢的指针,快的速度是慢的2倍
ListNode slow = head;
while(fast != null && slow != null) {
fast = fast.next.next; // 一次前进两个节点
slow = slow.next; // 一次前进一个节点
if(fast == slow) { // 快的指针追上了慢的指针,说明存在环
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
return null;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$​


24. 反转链表

剑指 Offer 24. 反转链表 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ListNode reverseList(ListNode head) {
ListNode cur = head; // 1
if(cur == null) {
return null;
}
ListNode nextNode = cur.next; // 2
head.next = null;
ListNode preNode = head; // 1
while(nextNode != null) {
cur = nextNode; // 2
nextNode = nextNode.next; // 3
cur.next = preNode; // 1
preNode = cur;
}
return cur;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$


25. 合并两个排序的链表

剑指 Offer 25. 合并两个排序的链表 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode(1); // 创建一个任意的节点
ListNode cur = head;
while(l1 != null && l2 != null) {
if(l1.val <= l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = l1 != null ? l1 : l2;
return head.next;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$


35. 复杂链表的复制

剑指 Offer 35. 复杂链表的复制 - 力扣(LeetCode)

  • 利用哈希表,构建原链表节点新链表对应节点映射关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Node copyRandomList(Node head) {
Map<Node, Node> hm = new HashMap<>();
Node cur = head;
while(cur != null) { // 构建节点的映射
hm.put(cur, new Node(cur.val));
cur = cur.next;
}
cur = head;
while(cur != null) {
hm.get(cur).next = hm.get(cur.next); // 只有这样,才能获得新地址下的Node
hm.get(cur).random = hm.get(cur.random);
cur = cur.next;
}
return hm.get(head);
}

时间复杂度:$O(n)$

空间复杂度:$O(n)$


52. 两个链表的第一个公共节点

剑指 Offer 52. 两个链表的第一个公共节点 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int step1 = 0, step2 = 0;
ListNode A = headA, B = headB;
while(A != null) {
step1++;
A = A.next;
}
while(B != null) {
step2++;
B = B.next;
}
int step = step1 - step2;
A = headA;
B = headB;
if(step < 0) { // 第一个链表比较短
while(step != 0) {
B = B.next;
step++;
}
} else if(step > 0) { // 第一个链表比较长
while (step != 0) {
A = A.next;
step--;
}
}
while (A != B) {
A = A.next;
B = B.next;
}
return A;
}

时间复杂度:$O(n+m)$

空间复杂度:$O(1)$


二分查找

11. 旋转数组的最小数字

剑指 Offer 11. 旋转数组的最小数字 - 力扣(LeetCode)


分治


数学

39. 数组中出现次数超过一半的数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int majorityElement(int[] nums) {
int target = nums[0], times = 1;
for(int i = 1; i < nums.length; i++) {
if (target != nums[i]) {
times--;
} else {
times++;
}
if (times == 0) {
target = nums[i];
times = 1;
}
}
return target;
}

时间复杂度:$O(n)$

空间复杂度:$O(1)$​


43. 1~n整数中1出现的次数

剑指 Offer 43. 1~n 整数中 1 出现的次数 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int countDigitOne(int n) {
int digit = 1, res = 0;
int high = n / 10, cur = n % 10, low = 0; // 高位,当前位,低位;从个位开始统计
while(high != 0 || cur != 0) {
if(cur == 0) { // 当前位最大为0
res += high * digit; // 该位出现1的次数,由高位决定
} else if(cur == 1) { //当前位最大为1
res += high * digit + low + 1; // 该位出现1的次数 由高位和低位决定
} else { // 当前位最大 > 1
res += (high + 1) * digit;
}
low += cur * digit;
cur = high % 10;
high /= 10;
digit *= 10;
}
return res;
}

概述

Java的I/O大概可以分成以下几类:

  • 磁盘操作:File
  • 字节操作:InputStream 和 OutputStream
  • 字符操作:Reader 和 Writer
  • 对象操作:Serializable
  • 网络操作:Socket
  • 新的输入/输出:NIO

File 类

java.io.FileFile类可以用于表示文件和目录路径名,但是不表示文件的内容


静态成员变量

  • 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
2
3
4
5
6
File f = new File("C:\\download\\xx.torrent");

// 如果路径不存在,没有必要进行后续的判断
if(f.exists()) {
System.out.println(f.isFile());
}

创建和删除方法

  • boolean createNewFile():当且仅当具有该名称的文件不存在时,创建一个新的空文件。

  • boolean delete():删除由此File表示的文件或目录。

    文件夹中有内容,则不会删除;路径不存在,会返回false。

    delete方法是直接在硬盘删除文件/文件夹,不走回收站(直接永久删除),要慎用

  • boolean mkdir():创建由此File表示的目录。

  • boolean mkdirs():创建由此File表示的目录包括任何必需但不存在的父目录(相对于mkdir()适用范围更广,推荐)。


遍历目录

  • String[] list():返回一个String数组,表示该File目录中的所有子文件或目录(子目录下的内容不会获取)。
  • File[] listFiles():返回一个File数组,表示该File目录中的所有子文件或目录。

如果目录的路径不存在,或者路径不是一个目录,都会抛出空指针异常


文件搜索

递归地列出一个目录下的所有文件:

1
2
3
4
5
6
7
8
9
10
11
12
public static void listAllFiles(File dir) {
if (dir == null || !dir.exists()) {
return;
}
if (dir.isFile()) {
System.out.println(dir.getName());
return;
}
for (File file : dir.listFiles()) {
listAllFiles(file);
}
}

文件过滤器

  • 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:创建新文件,覆盖源文件(默认

使用步骤
  1. 创建一个FileOutputStream对象,构造方法中传递写入数据的目的地

  2. 调用FileOutputStream对象中的write方法,把数据写入到文件中

    1次写多个字节时,写入的字节最后的显示

    • 如果写的第一个字节是正数(0-127),那么显示的时候会查询ASCII表
    • 如果写的第一个字节是负数,则第一个字节会和第二个字节**组成一个中文显示,查询系统默认码表(GBK)**。
  3. 释放资源(提高程序的效率)


写入字符的方法

可以使用String类中的方法byte[] getBytes()把字符串转换为字节数组(默认编码方式一般为UTF-8)。

1
2
3
4
5
6
7
8
9
FileOutputStream fos = new FileOutputStream("C:\\download\\xx.md");
byte[] bytes = "Hellow World!".getBytes();

System.out.println(Arrays.toString(bytes)); // 可以看到字符串转换成字节数组后,再将字节数组转换为字符串形式
System.out.println(new String(bytes)); // 可以看到字符串转换成字节数组后,再转换为字符串

fos.write(bytes);

fos.close();

换行写入

通过写换行符完成换行写入:

  • 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)

使用步骤
  1. 创建FileInputStream对象,构造方法中绑定要读取的数据源
  2. 使用FileInputStream对象中的read方法,读取文件
  3. 释放资源(提高程序的效率)
1
2
3
4
5
6
7
8
FileInputStream fis = new FileInputStream("C:\\download\\xx.md");

int len; // 存储读取的字节
while ((len = fis.read()) != -1) {
System.out.print((char)len);
}

fis.close();

实现文件复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void copyFile(String src, String dist) throws IOException {
FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dist);

byte[] buffer = new byte[20 * 1024];
int cnt;

// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof,即文件尾
while ((cnt = fis.read(buffer)) != -1) { // 该read方法返回每次读取的有效字节个数
fos.write(buffer, 0, cnt);
}

fis.close();
fos.close();
}

字符流

字节流读取中文的问题

字节流读取中文字符时,可能不会显示完整的字符,因为一个中文字符可能占用多个字节存储。所以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:创建新文件,覆盖源文件(默认

使用步骤
  1. 创建一个FileWriter对象,构造方法中传递写入数据的目的地
  2. 调用FileWriter对象中的write方法,把数据写入到内存缓冲区中(有一个字符转换为字节的过程
  3. 使用FileWriter对象中的flush方法把内存缓冲区中的数据刷新到文件中
  4. 释放资源(也会执行把内存缓冲区中的数据刷新到文件中的操作,故第3步可以不显式写出来)
1
2
3
4
5
6
FileWriter fw = new FileWriter("C:\\download\\xx.md");

fw.write(97); // 不会直接写入文件,而是写入内存缓冲区
fw.flush();

fw.close();

换行写入

通过写换行符完成换行写入:

  • 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)

使用步骤
  1. 创建FileReader对象,构造方法中绑定要读取的数据源
  2. 使用FileReader对象中的read方法,读取文件
  3. 释放资源(提高程序的效率)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FileReader fr = new FileReader("C:\\download\\xx.md");


int len; // 存储读取的【字符】
while ((len = fr.read()) != -1) { // 一次读取一个字符
System.out.print((char)len);
}


// 一次读取多个字符
char[] chars = new char[2 * 1024];
len = 0; // 记录有效字符个数
while((len = fr.read(chars) != -1)) {
/*
String类的构造方法
String(char[] value) 把字符数组转换为字符串
String(char[] value, int offset, int count) 把字符数组的一个
*/
System.out.println(new String(chars, 0, len));
}

fr.close();

IO 异常的处理

JDK 7 之前的处理

实际开发中不应该直接抛出异常,而应该使用try...catch...finally代码块对异常进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 提高变量fw的作用域,让finally能使用
FileWriter fw = null; // 必须初始化,避免try中发生异常时,fw还没有被赋值,导致finally中的代码无法正常执行。
try {
// 可能会产生异常的代码
fw = new FileWriter("C:\\download\\xx.md", true);
for (int i = 0; i < 10; i++) {
fw.write("Hellow world!" + i + "\r\n");
}
fw.close();
} catch (IOException e) {
// 异常的处理逻辑
System.,out.println(e);
} finally {
// 资源释放
// 如果创建对象FileWriter对象失败,fw为null,null无法调用方法,会抛出空指针异常,需要增加一个判断
if(fw != null) {
try {
fw.close(); // close方法本身也声明会抛出异常对象,因此也要处理
} catch (IOException e) {
e.printStackTrace();
}
}
}

JDK 7 的处理

JDK 7的新特性:在try后边可以增加一个()在括号中可以定义流对象,则这个流对象的作用域就在try中有效,try中的代码执行完毕,会自动把流对象释放不用写finally

1
2
3
4
5
6
7
8
9
10
11
12
try (// 1. 创建一个字节输入流对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("C:\\download\\xx.md");
// 2. 创建一个字节输出流对象,构造方法中绑定要写入的文件
FileOutputStream fos = new FileOutputStream("D:\\download\\1.jpg")) {

int len = 0;
while((len = fis.read()) != 1) {
fos.write(len);
}
} catch (IOException e) {
System.out.println(e);
}

属性集 Properties

java.util.Properties继承于HashTable,虽然HashTable是一个遗留类,但Properties作为唯一和IO流相结合的集合依然活跃着。Properties表示一个持久的属性集。它使用键值对存储数据,每个键和值都是字符串,因此Properties有一些操作字符串的特有方法

  • Object setProperty(String key, String value)

  • String getProperty(String key)

  • Set<String> stringPropertyNames()

    返回此属性列表中的键集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建Properties集合对象
Properties prop = new Properties();

// 使用setProperty往集合中添加数据
prop.setProperty("邓肯", "21");
prop.setProperty("吉诺比利", "20");
prop.setProperty("吉诺比利", "9");

// 把Properties集合中的键取出,存储到一个Set集合中
Set<String> set = prop.stringPropertyNames();

// 遍历Set集合,获取每个键对应的值
for (String key : set) {
String value = prop.getProperty(key);
System.out.println(key + "=" + value);
}

可以使用Properties集合中的store方法,把集合中的临时数据持久化写入到硬盘中

  • void store(OutputStream out, String comments)
    • OutputStream是字节输出流,不能写中文
    • comments是注释,用来解释保存的文件的作用。(不能使用中文,会产生乱码,默认是Unicode编码)一般使用空字符串""
  • void store(Writer writer, String comments)
    • Writer是字符输出流,可以写中文

使用步骤:

  1. 创建Properties集合对象,添加数据
  2. 创建字节输出流/字符输出流对象
  3. 使用Properties集合的store方法,将集合中的临时数据持久化写入硬盘
  4. 释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建Properties集合对象
Properties prop = new Properties();

// 使用setProperty往集合中添加数据
prop.setProperty("邓肯", "21");
prop.setProperty("吉诺比利", "20");
prop.setProperty("吉诺比利", "9");

FileWriter fw = new FileWriter("C:\\download\\xx.md");

prop.store(fw, "save data");

fw.close();

可以使用Properties集合中的load方法,把硬盘中保存的文件(键值对)读取到集合中使用

  • void load(InputStream inStream)

    InputStream是字节输入流,不能读取中文

  • void load(Reader reader)

    Reader是字符输入流,可以读取中文

使用步骤:

  1. 创建Properties集合对象
  2. 使用Properties集合的load方法,读取存储键值对的文件
    • 键值对文件,键与值默认的连接符可以用=、空格以及一些其他符号
    • 文件中可以使用#进行注释
    • 文件中的键和值默认都是字符串,无需加引号
  3. 遍历Properties集合
  4. 释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建Properties集合对象
Properties prop = new Properties();

// 读取键值对文件
prop.load(new FileReader("C:\\download\\xx.md"));

// 遍历集合
for (String key : set) {
String value = prop.getProperty(key);
System.out.println(key + "=" + value);
}

fw.close();

缓冲流

缓冲流也叫高效流,是对4个基本的FileXxx流的增强,所以也是4个流。缓冲流的基本原理,是在创建流对象时,创建一个内置的默认大小的缓冲区数组通过缓冲区读写,减少IO次数,从而提高读写的效率。


缓冲字节输出流 BufferedOutputStream

java.io.BufferedOutputStream extends OutputStream

构造方法

  • BufferedOutputStream(OutputStream out)

    创建一个缓冲输出流,将数据写入指定的底层输出流

  • BufferedOutputStream(OutputStream out, int size)

    创建一个缓冲输出流,将具有指定缓冲区大小的数据写入指定的底层输出流


使用步骤

  1. 创建FileOutputSream对象,构造方法中绑定要输出的目的地
  2. 创建BufferedOutputStream对象,构造方法中传递FileOutputStream对象提高FileOutputStream对象的效率
  3. 使用BufferedOutputStream对象的write方法,将数据写入内部缓冲区
  4. 使用BufferedOutputStream对象的flush方法,将内部缓冲区中的数据刷新到文件中
  5. 释放资源(也会执行把内存缓冲区中的数据刷新到文件中的操作,故第4步可以不显式写出来)
1
2
3
4
5
6
7
8
9
10
11
12
// 1.
FileOutputStream fos = new FileOutputStream("C:\\download\\xx.md");

// 2.
BufferedOutputStream bos = new BufferedOutputStream(fos);

// 3.
bos.write("把数据写入到内部缓冲区中".getBytes());

bos.flush();

bos.close(); // 关闭缓冲流,基本流也会被自动关闭

缓冲字节输入流 BufferedInputStream

java.io.BufferedInputStream extends InputStream

构造方法

  • BufferedInputStream(InputStream in)

    创建一个缓冲输入流,并保存输入流in,以便将来使用

  • BufferedInputStream(InputStream in, int size)

    创建一个缓冲输入流,将具有指定缓冲区大小的数据写入指定的底层输入流


使用步骤

  1. 创建FileInputSream对象,构造方法中绑定要读取的数据源
  2. 创建BufferedInputStream对象,构造方法中传递FileInputStream对象提高FileInputStream对象的效率
  3. 使用BufferedInputStream对象的read方法,将数据存入缓冲区数组
  4. 释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1.
FileInputStream fis = new FileInputStream("C:\\download\\xx.md");

// 2.
BufferedInputStream bis = new BufferedInputStream(fis);

// 3.
byte[] bytes = new byte[1024]; // 存储每次读取的数据
int len = 0; // 记录每次读取的字节个数
while((len = bis.read(bytes)) != -1) {
System.out.println(new String(bytes, 0 ,len));
}

bis.close(); // 关闭缓冲流,基本流也会被自动关闭

实现文件复制

使用基本字节流进行文件复制相比,效率明显提升1次读取单个字节或多个字节都是如此)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
long s = System.currentTimeMills(); // 测试程序效率

BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\download\\xx.md"));

BufferedOutStream bos = new BufferedOutputStream(new FileOutputStream("C:\\download\\yy.md"));

byte[] bytes = new byte[1024]; // 存储每次读取的数据
int len = 0; // 记录每次读取的字节个数
while((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}

bis.close();
bos.close();

long e = System.currentTimeMills(); // 测试程序效率
System.out.println("程序共耗时:" + (e - s) + "毫秒");

缓冲字符输出流 BufferedWriter

java.io.BufferedWriter extends Writer

构造方法

  • BufferedWriter(Writer out)

    创建一个使用默认输出缓冲区的缓冲字符输出流

  • BufferedWriter(Writer out, int size)

    创建一个使用给定大小缓冲区的缓冲字符输出流


特有的成员方法

void newLine()写入一个行分隔符根据不同的操作系统,获取不同的行分隔符


使用步骤

  1. 创建缓冲字符输出流对象,构造方法中传递字符输出流
  2. 调用缓冲字符输出流的write方法,把数据写入到内存缓冲区中
  3. 调用缓冲字符输出流的flush方法,把内存缓冲区中的数据刷新到文件中
  4. 释放资源
1
2
3
4
5
6
7
8
9
BufferedWriter bw = new BufferedWriter(new FileWriter("C:\\download\\xx.md"));

for(int i = 0; i < 10; i++) {
bw.write("机智医生生活");
bw.newLine(); // 换行(根据不同的操作系统,获取不同的行分隔符)
}

bw.flush();
bw.close(); // 关闭缓冲流,基本流也会被自动关闭

缓冲字符输入流 BufferedReader

java.io.BufferedReader extends Reader

构造方法

  • BufferedReader(Reader in)
  • BufferedReader(Reader in, int size)

特有的成员方法

String ReadLine()读取一个文本行根据不同的操作系统,获取不同的行分隔符

​ 返回该行内容的字符串,不包含任何行分隔符。如果已到达流末尾,则返回null


逐行读取文本文件的内容

1
2
3
4
5
6
7
8
BufferedReader br = new BufferedReader(new FileReader("C:\\download\\xx.md"));

String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}

br.close();

编码与解码

编码就是把字符转换为字节,而解码是把字节重新组合成字符。如果编码和解码过程使用不同的编码方式那么就出现了乱码


字符集 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
2
3
4
String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);

在调用无参数 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
2
3
4
5
6
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("C:\\download\\xx.md"), "GBK");

osw.write("机智医生生活"); // 不会直接写入文件,而是写入内存缓冲区(编码过程)
osw.flush();

osw.close();

InputStreamReader 类

转换流java.io.InputStreamReader是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符

构造方法

  • InputStreamReader(InputStream in)

  • InputStreamReader(InputStream in, String charsetName)

    charsetName用于指定字符集不区分大小写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
InputStreamReader isr = new InputStreamReader(new FileInputStream("C:\\download\\xx.md"), "GBK");

int len = 0;
while((len = isr.read() != -1)) {
System.out.println((char)len);
}

// 一次读取多个字符
char[] chars = new char[2 * 1024];
len = 0; // 记录有效字符个数
while((len = isr.read(chars) != -1)) {
System.out.println(new String(chars, 0, len));
}
isr.close();

转换文件编码

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

  1. 指定GBK编码的转换流,读取文本文件
  2. 指定UTF-8编码的转换流,写入文本文件
1
2
3
4
5
6
7
8
InputStreamReader isr = new InputStreamReader(new FileInputStream("C:\\download\\xx.md"), "GBK");

OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("C:\\download\\yy.md"), "UTF-8");

int len = 0;
while((len = isr.read() != -1)) {
osw.write(len);
}

装饰者模式

以InputStream为例,

  • InputStream是抽象组件;

  • FileInputStream是InputStream的子类,属于具体组件,提供了字节流的输入操作;

  • FilterInputStream属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能

    • 例如BufferedInputStream为FileInputStream提供缓存的功能。

      实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可:

      1
      2
      FileInputStream fileInputStream = new FileInputStream(filePath);
      BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
    • DataInputStream装饰者提供了对更多数据类型进行输入的操作,比如int、double等基本类型。


序列化

序列化就是将一个对象转换成字节序列,该字节序列包含对象的数据、对象的类型以及对象中存储的属性。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息

  • 序列化:void ObjectOutputStream.writeObject()
  • 反序列化:Object ObjectInputStream.readObject()

不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。


Serializable 接口

序列化的类需要实现Serializable接口,它只是一个标准,没有任何方法需要实现。但如果不去实现它的话,会抛出异常。

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 static void main(String[] args) throws IOException, ClassNotFoundException {

A a1 = new A(123, "abc");
String objectFile = "C:\\download\\a1.md";

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(objectFile));
oos.writeObject(a1);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(objectFile));
A a2 = (A) ois.readObject(); // 类型转换
ois.close();
System.out.println(a2);
}

private static class A implements Serializable {

private int x;
private String y;

A(int x, String y) {
this.x = x;
this.y = y;
}

@Override
public String toString() {
return "x = " + x + " " + "y = " + y;
}
}

InvalidClassException 异常

当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。

发生该异常的原因有:

  1. 该类的序列版本号与从流中读取的类描述符的版本号不一致
  2. 该类包含未知数据类型
  3. 该类没有可访问的无参数构造方法

Serializable接口给需要序列化的类**提供了一个序列版本号serialVersionUID**,用于验证序列化的对象和对应类是否版本匹配。

解决方案:

无论是否对类的定义进行修改,都不重新生成新的序列号。可序列化的类通过声明static final long serialVersionUID显式声明自己的序列号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class A implements Serializable {

private static final long serialVersionUID = 20L; //显式声明序列号

private int x;
private String y;

A(int x, String y) {
this.x = x;
this.y = y;
}

@Override
public String toString() {
return "x = " + x + " " + "y = " + y;
}
}

IDEA自动生成序列号

Settings - Editor - Inspections - 搜索UID - Serializable class without ‘serialVersionUID’ 右侧勾选,修改Severity为Error。

image-20230920225509841


transient 关键字

transient(瞬态)关键字可以使一些属性不会被序列化

ArrayList中存储数据的数组elementData是用transient修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据


序列化集合

当想在文件中保存多个对象的时候,可以把对象存储到一个集合中,对集合进行序列化和反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ArrayList<A> list = new ArrayList<>();

list.add(new A(20, "吉诺比利"));
list.add(new A(21, "邓肯"));

// 创建序列化流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\download\\Spurs.md"));

// 对集合进行序列化
oos.writeObject(list);

oos.close();

// 创建反序列化流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\download\\Spurs.md"));

// 读取文件中保存的集合
ArrayList<A> list2 = (ArrayList<A>)ois.readObject();

for (A a : list2) {
System.out.println(a);
}

ois.close();

打印流

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
2
3
4
5
6
7
8
9
System.out.println("默认在控制台输出");

PrintStream ps = new PrintStream("C:\\download\\Spurs.md");

System.setOut(ps); //更改输出语句的目的地为打印流的目的地

System.out.println("在打印流的目的地中输出");

ps.close();
0%