Java中常用日志框架

日志框架的演化

  • 1996 年初,欧洲安全电子市场 (EU SEMPER) 项目组决定编写它自己的程序跟踪 API(Tracing API)。经过不断的完善,这个 API 终于成为一个十分受欢迎的 Java 日志软件包,即 Log4j。后来 Log4j 成为 Apache 基金会项目中的一员。
  • Log4j 发布后近乎成了 Java 社区的日志标准。据说 Apache 基金会还曾经建议 sun 引入 Log4j 到 java 的标准库中,但 Sun 拒绝了。
  • 2002年 Java1.4 发布,Sun 推出了自己的日志库 JUL(Java Util Logging),其实现基本模仿了 Log4j 的实现。在 JUL 出来以前,log4j 就已经成为一项成熟的技术,使得 log4j 在选择上占据了一定的优势。
  • 2005年,Apache 推出了 JCL(Jakarta Commons Logging),JCL只是定义了一套日志接口(其内部也提供一个Simple Log的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用 Commons Logging 的接口,底层实现可以是log4j,也可以是Java Util Logging。
  • 2006年,Ceki Gülcü (Log4j最初由他编写) 开发了 SLF4J(The Simple Logging Facade for Java) 作为 JCL(Jakarta Commons Logging) 的更可靠替代方案。之后又开发了 Logback 框架 (Slf4j 的实现),并回瑞典创建了 QOS 公司,QOS 官网上是这样描述 Logback 的:The Generic,Reliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)。
  • 之后,Java 日志领域被划分为两大阵营:Commons Logging 阵营和 SLF4J 阵营。 Commons Logging 在 Apache 大树的笼罩下,有很大的用户基数。但有证据表明,形式正在发生变化。2013 年底有人分析了 GitHub 上30000个项目,统计出了最流行的100个 Libraries,可以看出 slf4j 的发展趋势更好
  • 2012-07 Apache 重写了 log4j 1.x,成立了新的项目 Log4j2。Log4j2 具有 Logback 的所有特性。

现在,SLF4J + Logback 是 Spring Boot 的默认日志框架。

日志框架的分类

日志框架分为日志门面框架日志实现框架,那什么是日志门面框架日志实现框架

  • 日志门面框架指的是:无具体实现的抽象门面框架,如:Commons Logging、SLF4J
  • 日志实现框架:具体实现的框架,如:Log4j,Log4j2,Logback,Jul(java.util.logging)

日志门面框架

日志门面:是门面模式的一个典型的应用,门面模式,也称外观模式。日志门面框架就使一套提供了日志相关功能的接口而无具体实现的框架,其调用具体的实现框架来进行日志记录。也就是说日志门面天然的兼容日志实现框架。

在java 生态中国日志门面框架主要有两个 Commons Logging 和 SLF4J

Commons Logging

common-logging 是 apache 提供的一个通用的日志接口, 在 common-logging 中,有一个 Simple logger 的简单实现,但是它功能很弱,所以使用 common-logging,通常都是配合着 log4j 来使用。

SLF4J

SLF4J(Simple Logging Facade for Java),是一套包装 Logging 框架的界面程式,以外观模式实现。可以在软件部署的时候决定要使用的 Logging 框架,目前主要支援的有Java Logging API、Log4j及logback等框架。以MIT 授权方式发布。SLF4J 的作者就是 Log4j和Logback 的作者 Ceki Gülcü。

日志实现框架

Log4j

最早的日志实现框架,早期由 Ceki Gülcü 开发,现在则是Apache软件基金会的一个项目。

JUL(Java Util Logging)

Java 1.4 以来的官方日志实现,主要参考自 Log4j

Logback

Log4j 的早期作者 Ceki Gülcü 开发的新一代日志框架

Log4j2

Apache Log4j2 是 Apache Log4j 的升级产品,并且不兼容 Log4j,主要参考自 Logback

框架选择

  • 成本考虑:Logback 的所有文档是全面免费提供,Apache Log4J 只提供部分免费文档,其余部分需要付费购买。
  • 资源开销:Commons Logging 相比较与 SLF4J 开销更高.
  • 性能:Logback 相比 Log4j、Log4j2 拥有更好的性能。Logback声称:某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高。这个操作在Logback中需要3纳秒,而在Log4J中则需要30纳秒。LogBack创建记录器(logger)的速度也更快:13毫秒,而在Log4J中需要23毫秒。更重要的是,它获取已存在的记录器只需94纳秒,而Log4J需要2234纳秒,时间减少到了1/23。跟JUL相比的性能提高也是显著的。

SLF4J + Logback 成为了实际开发中的最佳选择。


SpringBoot项目中配置SLF4J+Logback

引入依赖

要开始使用 Logback,首先需要将logback-classic依赖项添加到类路径中。让我们用 Maven 来做到这一点:

1
2
3
4
5
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>

引入这个单一依赖项就足够了,因为它将间接引入logback-coreslf4j-api这两个依赖项。

如果没有定义自定义配置,Logback 会自行提供简单的自动配置。默认情况下,这可确保日志语句以 DEBUG 级别打印到控制台。

因此,您现在可以获得*Logger*实例并开始使用默认的基本配置写入日志消息。

首先,您可以使用slf4j LoggerFactory类创建Logger

1
private final static Logger LOGGER = LoggerFactory.getLogger(LogbackTestApplication.class);

接下来,您可以简单地使用你想要的日志级别并打印日志:

1
LOGGER.info("LogbackTestApplication running...");

Logback配置文件

要为 Logback 创建配置,您可以使用 XML 以及 Groovy。只要您遵守命名约定,系统就会自动选择并使用配置。

您可以选择三个有效的标准文件名:

  • logback-test.xml
  • logback.groovy
  • logback.xml

这里值得注意的是,Logback 将按照这个确切的顺序搜索这些文件。

展望未来,本教程中的示例将依赖于基于 XMLlogback.xml文件。

让我们看看默认配置的基本配置是什么样的:

1
2
3
4
5
6
7
8
9
10
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>

此配置定义了一个ConsoleAppender。控制台上的日志消息将使用<pattern>中配置的样式显示在打印控制台。

1
2023-08-19 15:57:13.086  INFO 72780 --- [           main] c.g.logbacktest.LogbackTestApplication   : LogbackTestApplication running...

Logback 配置文件结构

Logback 配置文件的基本结构如下:

如上,根节点为<configuration>元素,包含零个或多个<appender>元素,然后是零个或多个<logger>元素,然后是最多一个<root>元素

loggerroot 作为日志的记录器,把它关联到应用的对应的 context 上后,主要用于存放日志对象,也可以定义日志类型、级别。

appender 主要用于指定日志输出的目的地,目的地可以是控制台、文件、远程套接字服务器、 MySQLPostreSQLOracle 和其他数据库、 JMS 和远程 UNIX Syslog 守护进程等。

根节点<configuration>

根节点 <configuration> 常用的属性包含scanscanPerioddebug三个属性。

  1. scan:当此属性设置为 true 时,如果配置文件发生改变,会自动重新加载配置文件,默认值为 true.
  2. scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当 scan 为 true 时,此属性生效。默认的时间间隔为 1 分钟.
  3. debug:当此属性设置为 true 时,将打印出 logback 内部日志信息,实时查看 logback 运行状态。默认值为 false.
1
2
3
<configuration scan="true" scanPeriod="60 second" debug="false">  
<!-- 其他配置省略-->
</configuration>

注意 scanPeriod 值的写法,数值英文空格时间单位 。 如果未指定时间单位,则默认时间单位为毫秒

变量 <property>

自定义变量

<configuration> 中,可以通过<property> 来定义一个变量,属性 name 是变量的名称,属性 value 是变量的值。

1
2
3
4
<configuration>
<property name="LOG_PATTERN" value="%date %level [%thread] %logger{10} [%file : %line] %msg%n" />
...
</configuration>

使用自定义变量

通过 ${变量名} 来引用变量

1
2
3
4
5
6
7
8
9
10
<configuration>
<property name="LOG_PATTERN" value="%date %level [%thread] %logger{10} [%file : %line] %msg%n" />

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
...
</configuration>

使用配置文件中的属性

1
2
<!-- 读取spring.application.name中的属性来生成日志文件名 -->
<springProperty scope="context" name="logName" source="spring.application.name" defaultValue="localhost.log"/>

<springProperty> 标签允许我们从Spring中显示属性,Environment 以便在Logback中使用。如果你想将读取application.properties文件中的值,这将非常有用。

1
2
<!-- 读取spring.application.name中的属性来生成日志文件名 -->
<springProperty scope="context" name="logName" source="spring.application.name" defaultValue="localhost.log"/>

日志输出器 <appender>

<appender> (日志输出器),用于将日志按照一定的格式输出到控制台、文件、数据库等地方,logger(日志记录器) 需要使用 appender(日志输出器) 将记录器中的日志输出。

appender 有两个必填属性

  • nameappender 的名称,任意填写,不要重名就行
  • calss:某个具体的 appender 的完全类名,它是 appender 的具体实现,Logback 自带常用的几个 appender
    • ch.qos.logback.core.ConsoleAppender :将日志输出到控制台的 appender
    • ch.qos.logback.core.rolling.RollingFileAppender :将日志输出到文件,并按照条件切换输出到新建文件(滚动输出,自动切割)

<encoder> 负责将事件(日志)转换为字节数组,并将该字节数组写出为 OutputStream

<encoder><appender> 的子节点,在 <encoder> 节点中,最重要的是配置 <pattern>,它是用来定义日志输出格式。

1
2
3
4
5
6
<appender name="" class="">
<encoder>
<pattern></pattern>
<charset></charset>
</encoder>
</appender>

<pattern>

<pattern>用于定义日志的输出格式,通过 Logback 中的转换说明符(Conversion specifier)(其实就是一些预定义变量),可以方便的组合出我们想要的日志格式

在这里我只列举出常用的转换说明符(Conversion specifier),更多请参考官方文档

日期、时间

假设系统时间是 2006-10-20 14:06:49,812

Conversion Pattern Result
%d 2006-10-20 14:06:49,812
%date 2006-10-20 14:06:49,812
%date{ISO8601} 2006-10-20 14:06:49,812
%date{HH:mm:ss.SSS} 14:06:49.812
%date{yyyy-MM-dd HH:mm:ss.SSS, Asia/Shanghai} 2006-10-20 14:06:49,812
日志记录器

输出日志记录事件源处的记录器名称,下表提供了缩写算法的实际应用示例。

转换说明符 记录器名称 输出结果
%logger mainPackage.sub.sample.Bar mainPackage.sub.sample.Bar
%logger{0} mainPackage.sub.sample.Bar Bar
%logger{5} mainPackage.sub.sample.Bar m.s.s.Bar
%logger{10} mainPackage.sub.sample.Bar m.s.s.Bar
%logger{15} mainPackage.sub.sample.Bar m.s.sample.Bar
%logger{16} mainPackage.sub.sample.Bar m.sub.sample.Bar
%logger{26} mainPackage.sub.sample.Bar mainPackage.sub.sample.Bar
类名、方法名、行号
Conversion Pattern Result
%C、%class 输出log的类的完整类名,获取类名的效率一般,只建议在需要时使用
%M 、%method 输出log的方法
%L 、%line 输出log的行号。获取行号的效率一般,只建议在需要时使用
线程
Conversion Pattern Result
%t 、%thread 输出日志的线程
消息请求级别
Conversion Pattern Result
%p、%le、 %level 消息请求级别
日志信息
Conversion Pattern Result
%m 、%msg 、%message 日志信息
换行
Conversion Pattern Result
%n 换行

组合示例:

1
2
3
4
5
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{yyyy-MM-dd HH:mm:ss.SSS, Asia/Shanghai} %red(%-5level) --- [%thread] %cyan(%class.%method/%line) : %msg%n</pattern>
</encoder>
</appender>
颜色

对于控制台的输出,Logback 支持填充颜色,支持的颜色如下

  • %black()
  • %red()
  • %green()
  • %yellow()
  • %blue()
  • %magenta()
  • %cyan()
  • %white()
  • %gray()
  • %boldRed()
  • %boldGreen()
  • %boldYellow()
  • %boldBlue()
  • %boldMagenta()
  • %boldCyan()
  • %boldWhite()
  • %highlight()

使用方法是用颜色的代码把消息包起来,比如想将日志级别设置成红色 %red(%-5level)

设置字符集 <charset>

<charset><encoder> 的子节点,用于设置输出字符集

1
2
3
4
5
6
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date{yyyy-MM-dd HH:mm:ss.SSS, Asia/Shanghai} %red(%-5level) --- [%thread] %cyan(%class.%method/%line) : %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

ConsoleAppender

ch.qos.logback.core.ConsoleAppenderLogback 自带的 appender,用于将日志输出到控制台。

对应控制台,只需要重点配置 <encoder>节点

1
2
3
4
5
6
7
8
9
10
11
12
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date %red(%-5level) --- [%thread] %cyan(%class.%method/%line) : %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

RollingFileAppender

ch.qos.logback.core.rolling.RollingFileAppenderLogback 自带的 Appender,用于将日志输出到文件,并按照条件切换输出到新建文件(滚动输出,自动切割)

属性 类型 说明
file String 文件名,如果文件不存在,则创建。注意文件路径
append boolean true 在文件末尾追加,false 覆盖,默认 true
encoder Encoder 编码器,设置日志输出格式和编码
rollingPolicy RollingPolicy 滚动策略,描述该如何滚动
triggeringPolicy TriggeringPolicy 触发策略,什么条件下触发滚动

要使用 RollingFileAppender RollingPolicy (滚动策略)和 TriggeringPolicy(触发策略)必不可少。但是,如果 RollingPolicy 实现了 TriggeringPolicy 接口,则仅需要指定 RollingPolicy

rollingPolicy(滚动策略) 里面有一个属性叫 fileNamePattern,它和 file 属性一样都表示日志文件名。

滚动策略 —— RollingPolicy

RollingPolicy 负责日志文件的移动和重命名的过程。
Logback 提供了多种 RollingPolicy 的实现

  • TimeBasedRollingPolicy :按时间滚动,比如按天或按月
  • SizeAndTimeBasedRollingPolicy :按时间和大小滚动,比如说在按天滚动的基础上设置大小,防止某天的日志文件过大!

TimeBasedRollingPolicy

TimeBasedRollingPolicy可能是最受欢迎的滚动政策。它定义了基于时间的展期策略,例如按天或按月。

ch.qos.logback.core.rolling.TimeBasedRollingPolicy 滚动策略同时也实现了 TriggeringPolicy(触发策略)

因此配置好 TimeBasedRollingPolicy 后,就不需要配置 TriggeringPolicy

TimeBasedRollingPolicy的配置包含一个必选的fileNamePattern属性和几个可选属性。

属性名 类型 描述
fileNamePattern String fileNamePattern 是最为复杂也最为重要的属性,同时也是必填项,将在下文中详细介绍
maxHistory Int 可选的 maxHistory 属性控制要保留的归档文件的最大数量,异步删除旧文件。 例如,如果您指定每月滚动,并将 maxHistory 设置为 6,则将保留近 6 个月的存档文件,并删除超过 6 个月的文件。 请注意,当旧的存档日志文件被删除时,为日志文件存档而创建的任何文件夹都将根据需要删除。

将 maxHistory 设置为0会禁用存档删除。 默认情况下,maxHistory 设置为0,即默认情况下不删除存档。
totalSizeCap Int 可选的totalSizeCap属性控制所有归档文件的总大小,在 maxHistory 限制的基础上,进一步限制 所有存档文件的总大小。当超过总大小上限时,最旧的存档将被异步删除。totalSizeCap 依赖于 maxHistory ,如果没有 maxHistory ,单独设置 totalSizeCap 是不生效的。

TotalSizeCap 属性可以通过在数值后分别添加 KB、MB 和 GB 来指定为字节、千字节、兆字节或千兆字节的单位。 例如,5000000、5000KB、5MB 和 2GB 都是有效值,前三个是等效的。 没有后缀的数值以字节为单位。

默认情况下,totalSizeCap 设置为零,这意味着没有总大小上限。
cleanHistoryOnStart boolean 如果设置为 true,则存档文件将在appender启动时删除。 默认情况下,此属性设置为 false。

存档删除通常在滚动期间执行。 但是,某些应用程序的生存时间可能不够长,无法触发滚动。 因此,对于此类短暂的应用程序,存档删除可能永远没有机会执行。 通过将 cleanHistoryOnStart 设置为 true,存档删除将在appender启动时执行。

fileNamePattern 属性

fileNamePattern 的值包含文件名以及支持绝对和相对路径和日期转换符——%d

% d转换符可以包含由 java.text.SimpleDateFormat类指定的日期和时间模式。如果省略日期和时间模式,则 采用默认模式yyyy-MM-dd 。滚动周期是根据fileNamePattern的值推断出来的。

%d 表示系统当前的时间,默认格式为 %d{yyyy-MM-dd},通过 %d{日期格式} 来自定义。

fileNamePattern 属性内或日期和时间模式内任意位置的 正斜杠“/”或反斜杠“\”字符将被解释为目录分隔符。

时区

%d 默认采用主机时区,也可以自定义%d{yyyy-MM-dd,UTC+8}

多个%d

可以指定多个 %d 转换符,但只有其中一个可以是主要的,即用于推断滚动周期。

多个 %d转换符允许您以不同于滚动周期的文件夹结构来组织存档文件。 例如,下面显示的文件名模式按年和月组织日志文件夹,但每天午夜滚动更新日志文件。

1
/var/log/%d{yyyy/MM, aux}/myapplication.%d{yyyy-MM-dd}.log

自动文件压缩
TimeBasedRollingPolicy支持自动文件压缩。如果fileNamePattern选项的值 以.gz或.zip结尾,则启用此功能。

fileNamePattern Rollover schedule Example
/wombat/foo.%d.gz 每日滚动(午夜),自动对存档文件进行 GZIP 压缩。 如果没有设置,2009 年 11 月 23 日期间,日志将输出到文件 /wombat/foo.2009-11-23。但是,在午夜该文件将被压缩为 /wombat/foo.2009-11-23.gz。11 月 24 日,日志记录输出将定向到 /wombat/folder/foo.2009-11-24,以此类推。
如果设置/wombat/foo.txt,2009 年 11 月 23 日期间,日志输出将转到文件/wombat/foo.txt。午夜该文件将被压缩并重命名为 /wombat/foo.2009-11-23.gz。并将创建一个新的 /wombat/foo.txt文件,并将11月24日期间的日志记录到该文件。在11月24日午夜,/wombat/foo.txt将被压缩并重命名为/wombat/foo.2009-11-24.gz

fileNamePattern有双重用途。首先,logback通过研究模式 计算请求的滚动周期。其次,它计算每个存档文件的名称。请注意,两种不同的模式可以指定相同的周期。模式yyyy-MM和 yyyy@MM都指定每月滚动,尽管生成的存档文件将带有不同的名称。

通过设置<file>属性,您可以分离活动日志文件的位置和归档日志文件的位置。日志输出将定位到文件属性指定的文件中 。由此可见,活动日志文件的名称不会随时间而改变。但是,如果您选择忽略file属性,则将根据fileNamePattern的值在每个周期重新计算活动文件名称。通过保留<file>属性未设置,您可以避免在滚动期间存在引用日志文件的外部文件句柄时发生的 文件重命名错误。

由于各种技术原因,滚动不是时钟驱动的,而是取决于日志记录事件的到达。例如,在 2002 年 3 月 8 日,假设 fileNamePattern设置为yyyy-MM-dd (每日滚动),则午夜后第一个事件的到达将触发滚动。如果在午夜后 23 分 47 秒期间没有记录事件,则滚动实际上将发生在 3 月 9 日上午 00:23’47,而不是上午 0:00。因此,根据事件的到达率,可能会以一定的延迟触发滚动。然而,无论延迟如何,滚动算法都已知是正确的,从某种意义上说,在特定时间段内生成的所有日志记录事件都将输出到界定该时间段的正确文件中。

SizeAndTimeBasedRollingPolicy

有时您可能希望基本上按日期归档文件,但同时限制每个日志文件的大小,特别是日志文件处理工具对大小有着明确的限制。为了满足这一要求,logback 附带了 SizeAndTimeBasedRollingPolicy.

请注意,TimeBasedRollingPolicy已经允许限制归档日志文件的组合大小。如果您只想限制日志归档的组合大小,那么 TimeBasedRollingPolicy上面描述的和设置totalSizeCap属性应该足够了。

maxFileSize 属性表示单个归档日志文件的大小,单位有 KBMBGB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<configuration>
<appender name="SizeAndTimeFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app1/app1.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/app1/backup/app1_%d_%i.zip</fileNamePattern>
<!--单个日志文件最大10MB-->
<maxFileSize>10MB</maxFileSize>
<!--删除n个滚动周期之前的日志文件(最多保留前n个滚动周期的历史记录)件-->
<maxHistory>60</maxHistory>
<!--在 maxHistory 限制的基础上,进一步限制所有存档文件的总大小-->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%date %-5level --- [%thread] %class.%method/%line : %msg%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="SizeAndTimeFile" />
</root>
</configuration>

%i 是一个滚动周期内的归档序列号,从0开始

注意:maxHistory 是表了 n 个滚动周期的日志文件,而不是 n 个的日志文件,在设置了 maxFileSize 后,一个滚动周期内可能有多个日志文件。


日志记录器 <logger>

1
2
3
<logger name="" level="" additivity="" >
<appender-ref ref="" />
</logger>

<logger> 有三个属性

  • name : 包名或类名。即,将该记录器作用于哪个类或包下。必填
  • level : 该记录器的级别,低于该级别的日志消息不记录。可选级别从小到大为 TRACE、DEBUG、INFO、WARN、ERROR、ALL、OFF(不区分大小写)。选填,不填则默认从父记录器继承level
  • additivity : 是否追加父 Logger 的输出源(appender),默认为true,选填。如果只想输出到自己的输出源(appender),需要设置为 false

logger 还可以包含 0 或多个 <appender-ref> 元素, 将记录的日志使用 appender 进行输出

level 的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date %level [%thread] %logger{10} [%file : %line] %msg%n</pattern>
</encoder>
</appender>

<logger name="com.wqlm.boot.user.controller.UserController" ></logger>
<logger name="com.wqlm.boot.user.controller" ></logger>

<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

如上,"com.wqlm.boot.user.controller.UserController"logger 没有定义 level,因此它会继承父 logger 的 level。

那它的父 logger 是谁呢? 其实就是下面的 "com.wqlm.boot.user.controller"logger!

Logback 是根据包或类的层次结构来确定 logger 的父子关系的

"com.wqlm.boot.user.controller"logger 也没有定义 level,因此它也会继承它的父 loggerlevel。但是,在该 xml 中,已经没有比它层次还“浅”的 logger 了,因此,它的父 loggerroot。也就是说它会继承 root loggerlevel,也就是 info

additivity 的追加策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date %level [%thread] %logger{10} [%file : %line] %msg%n</pattern>
</encoder>
</appender>

<logger name="com.wqlm.boot.user.controller.UserController" ></logger>
<logger name="com.wqlm.boot.user.controller" ></logger>

<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>

如上,"com.wqlm.boot.user.controller.UserController"logger 没有设置 additivity,因此 additivity 采用默认值 true,即,追加它父 logger 中的 appender 到自己的 logger 中。

在看它的父 logger —— "com.wqlm.boot.user.controller" loggeradditivity 也采用默认值,因此,它也会追加它父 logger 中的 appender 到自己的 logger 中。
在该 xml"com.wqlm.boot.user.controller" logger 的父 loggerroot logger!,因此,它会追加 名为 STDOUT 的 appender 到自己的 logger

刚才是从上层往下层走,直到找到显示标注 appender="false"loggerroot logger 才停止追加。
现在要从下层往上层走,把层层追加的 appender 往上传,直到遇到显示标注 appender="false"logger 才停止

因此,"com.wqlm.boot.user.controller.UserController"logger 中,也有一个名为 STDOUT 的 appender,这个 appender 是从它的父 logger 中来的

所以,一般情况下,只需要给 root logger 添加 appender 即可!!!其他 logger 都会直接或间接获取到 root loggerappender

根记录器 <root>

1
2
3
<root level="">
<appender-ref ref="" />
</root>

<root>元素配置根记录器。它支持单一属性,即level属性。它不允许任何其他属性,因为可加性标志不适用于根记录器。此外,由于根记录器已被命名为“ROOT”,因此它也不允许使用名称属性。level 属性的值可以是不区分大小写的字符串 TRACEDEBUGINFOWARNERRORALLOFF 之一。请注意,根记录器的级别不能设置为 INHERITEDNULL

与元素<logger>类似,<root>元素可以包含零个或多个 <appender-ref>元素;这样引用的每个日志记录器都会添加到根记录器中。请注意,与 log4j 不同,logback-classic在配置根记录器时 不会关闭或删除任何先前引用的日志记录器。


Filter

Filter:过滤器,过滤满足条件或者不满足条件的日志
允许组合使用,可配置多个过滤器有很多,只介绍两种常用的LevelFilterThresholdFilter

名称 值说明 说明 备注
LevelFilter 级别过滤器 精确匹配指定的日志级别
ThresholdFilter 阈值过滤 匹配等于或高于指定级别

使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>

<!-- deny all events with a level below INFO, that is TRACE and DEBUG -->
<!-- 拒绝所有级别低于 INFO 的, 也就是 TRACE 和 DEBUG -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>

LevelFilter属性如下:

onMatchonMismatch的取值类型有三种:

  • DENY:拒绝,不再执行后续的过滤器
  • NEUTRAL:中性,继续执行后面的过滤器
  • ACCEPT:接受,记录日志,不再执行后续过滤器

日志有效级别

记录器可以被分配级别。在类中定义 ch.qos.logback.classic.Level了可能的级别集(TRACEDEBUGINFOWARNERROR)。

如果给定的记录器没有分配级别,那么它会从其最接近的祖先那里继承一个已分配级别的级别。

为了确保所有记录器最终都能继承级别,根记录器始终具有指定的级别。默认情况下,该级别为 DEBUG

根据定义,打印方法决定日志记录请求的级别。例如,如果L是一个记录器实例,则该语句L.info("..")INFO 级别的日志记录语句。

如果日志请求的级别高于或等于其记录器的有效级别,则称该日志请求已启用。否则,该请求被称为被禁用

基本选择规则:

如果p >= q,则启用 向有效级别q的记录器发出级别p的日志打印请求。

这条规则是 logback 的核心。它假设级别按如下顺序排序: TRACE < DEBUG < INFO < WARN < ERROR

以更形象的方式,以下是选择规则的工作原理。在下表中,垂直标头显示日志记录请求的级别,由p指定,而水平标头显示记录器的有效级别,由q指定。行(级别请求)和列(有效级别)的交集是由基本选择规则产生的布尔值。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import ch.qos.logback.classic.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
....

// get a logger instance named "com.foo". Let us further assume that the
// logger is of type ch.qos.logback.classic.Logger so that we can
// set its level
ch.qos.logback.classic.Logger logger =
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foo");
//set its Level to INFO. The setLevel() method requires a logback logger
logger.setLevel(Level. INFO);

Logger barlogger = LoggerFactory.getLogger("com.foo.Bar");

// This request is enabled, because WARN >= INFO
logger.warn("Low fuel level.");

// This request is disabled, because DEBUG < INFO.
logger.debug("Starting search for nearest gas station.");

// The logger instance barlogger, named "com.foo.Bar",
// will inherit its level from the logger named
// "com.foo" Thus, the following request is enabled
// because INFO >= INFO.
barlogger.info("Located nearest gas station.");

// This request is disabled, because DEBUG < INFO.
barlogger.debug("Exiting gas station search");

注释

  1. 单行注释 - 以#和空格开头的部分

    1
    # print('hello,world')
  2. 多行注释 - 三个引号开头,三个引号结尾

    1
    2
    3
    """
    print('hello,world')
    """

变量和类型

变量命名

在Python中,变量命名需要遵循以下这些必须遵守硬性规则和强烈建议遵守的非硬性规则。

  • 硬性规则:
    • 变量名由字母(广义的Unicode字符,不包括特殊字符)、数字和下划线构成,数字不能开头。
    • 大小写敏感(大写的a和小写的A是两个不同的变量)。
    • 不要跟关键字(有特殊含义的单词,后面会讲到)和系统保留字(如函数、模块等的名字)冲突。
  • PEP 8要求:
    • 用小写字母拼写,多个单词用下划线连接。
    • 受保护的实例属性用单个下划线开头(后面会讲到)。
    • 私有的实例属性用两个下划线开头(后面会讲到)。

类型

计算机中的变量是实际存在的数据或者说是存储器中存储数据的一块内存空间,变量的值可以被读取和修改,这是所有计算和控制的基础。计算机能处理的数据有很多种类型,除了数值之外还可以处理文本、图形、音频、视频等各种各样的数据,那么不同的数据就需要定义不同的存储类型。Python中的数据类型很多,而且也允许我们自定义新的数据类型(这一点在后面会讲到),我们先介绍几种常用的数据类型。

  • 整型:Python中可以处理任意大小的整数(Python 2.x中有intlong两种类型的整数,但这种区分对Python来说意义不大,因此在Python 3.x中整数只有int这一种了),而且支持二进制(如0b100,换算成十进制是4)、八进制(如0o100,换算成十进制是64)、十进制(100)和十六进制(0x100,换算成十进制是256)的表示法。
  • 浮点型:浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,浮点数除了数学写法(如123.456)之外还支持科学计数法(如1.23456e2)。
  • 字符串型:字符串是以单引号或双引号括起来的任意文本,比如'hello'"hello",字符串还有原始字符串表示法、字节字符串表示法、Unicode字符串表示法,而且可以书写成多行的形式(用三个单引号或三个双引号开头,三个单引号或三个双引号结尾)。
  • 布尔型:布尔值只有TrueFalse两种值,要么是True,要么是False,在Python中,可以直接用TrueFalse表示布尔值(请注意大小写),也可以通过布尔运算计算出来(例如3 < 5会产生布尔值True,而2 == 1会产生布尔值False)。
  • 复数型:形如3+5j,跟数学上的复数表示一样,唯一不同的是虚部的i换成了j。实际上,这个类型并不常用,大家了解一下就可以了。

在Python中可以使用type函数对变量的类型进行检查。

1
2
3
4
5
6
7
8
9
10
a = 100
b = 12.345
c = 1 + 5j
d = 'hello, world'
e = True
print(type(a)) # <class 'int'>
print(type(b)) # <class 'float'>
print(type(c)) # <class 'complex'>
print(type(d)) # <class 'str'>
print(type(e)) # <class 'bool'>

类型转换

可以使用Python中内置的函数对变量类型进行转换。

  • int():将一个数值或字符串转换成整数,可以指定进制。
  • float():将一个字符串转换成浮点数。
  • str():将指定的对象转换成字符串形式,可以指定编码。
  • chr():将整数转换成该编码对应的字符串(一个字符)。
  • ord():将字符串(一个字符)转换成对应的编码(整数)。

格式化输出

下面的代码通过键盘输入两个整数来实现对两个整数的算术运算。

1
2
3
4
5
6
7
8
9
a = int(input('a = '))
b = int(input('b = '))
print('%d + %d = %d' % (a, b, a + b))
print('%d - %d = %d' % (a, b, a - b))
print('%d * %d = %d' % (a, b, a * b))
print('%d / %d = %f' % (a, b, a / b))
print('%d // %d = %d' % (a, b, a // b))
print('%d %% %d = %d' % (a, b, a % b))
print('%d ** %d = %d' % (a, b, a ** b))

说明:上面的print函数中输出的字符串使用了占位符语法,其中%d是整数的占位符,%f是小数的占位符,%%表示百分号(因为百分号代表了占位符,所以带占位符的字符串中要表示百分号必须写成%%),字符串之后的%后面跟的变量值会替换掉占位符然后输出到终端中,运行上面的程序,看看程序执行结果就明白啦。

除了这种格式化字符串的方式外,还可以用下面的方式来格式化字符串

1
2
3
a1 = float(input('a1 = '))
b1 = float(input('a1 = '))
print(f'{a1} + {b1} = {a1 + b1}')

分支结构

if语句的使用

虽然都是用 if 关键词定义判断,但与C,Java等语言不同,Python不使用 {}if 语句控制的区域包含起来。Python使用的是缩进方法。同时,也不需要用 ()将判断条件括起来。

一个完整的 if结构通常如下所示(注意:条件后的 :是必须要的,缩进值需要一样):

1
2
3
4
5
6
7
if <condition 1>:
<statement 1>
<statement 2>
elif <condition 2>:
<statements>
else:
<statements>
1
2
3
4
5
6
7
8
x = 0
if x > 0:
print "x is positive"
elif x == 0:
print "x is zero"
else:
print "x is negative"
x is zero

elif的个数没有限制,可以是1个或者多个,也可以没有。

else 最多只有1个,也可以没有。

可以使用 andor, not等关键词结合多个判断条件:

1
2
3
x = 10
y = -5
x > 0 and y < 0
1
True
1
not x > 0
1
False
1
x < 0 or y < 0
1
True

这里使用这个简单的例子,假如想判断一个年份是不是闰年,按照闰年的定义,这里只需要判断这个年份是不是能被4整除,但是不能被100整除,或者正好被400整除:

1
2
3
4
5
6
7
8
9
year = 1900
if year % 400 == 0:
print "This is a leap year!"
# 两个条件都满足才执行
elif year % 4 == 0 and year % 100 != 0:
print "This is a leap year!"
else:
print "This is not a leap year."
This is not a leap year.

循环结构

循环结构就是程序中控制某条或某些指令重复执行的结构。在Python中构造循环结构有两种做法,一种是for-in循环,一种是while循环。

for-in循环

1
2
for <variable> in <sequence>:
<indented block of code>

如果明确的知道循环执行的次数或者要对一个容器进行迭代(后面会讲到),那么我们推荐使用for-in循环,例如下面代码中计算1~100求和的结果

1
2
3
4
sum = 0
for x in range(101):
sum += x
print(sum)

需要说明的是上面代码中的range(1, 101)可以用来构造一个从1到100的范围,当我们把这样一个范围放到for-in循环中,就可以通过前面的循环变量x依次取出从1到100的整数。当然,range的用法非常灵活,下面给出了一个例子:

  • range(101):可以用来产生0到100范围的整数,需要注意的是取不到101。
  • range(1, 101):可以用来产生1到100范围的整数,相当于前面是闭区间后面是开区间。
  • range(1, 101, 2):可以用来产生1到100的奇数,其中2是步长,即每次数值递增的值。
  • range(100, 0, -2):可以用来产生100到1的偶数,其中-2是步长,即每次数字递减的值。

知道了这一点,我们可以用下面的代码来实现1~100之间的偶数求和。

1
2
3
4
sum = 0
for x in range(2, 101, 2):
sum += x
print(sum)

while循环

while 循环

1
2
while <condition>:
<statesments>

Python会循环执行<statesments>,直到<condition>不满足为止。

例如,计算数字01000000的和:

1
2
3
4
5
6
i = 0
total = 0
while i < 1000000:
total += i
i += 1
print total
1
499999500000

continue 语句

遇到 continue 的时候,程序会返回到循环的最开始重新执行。

例如在循环中忽略一些特定的值:

1
2
3
4
5
6
values = [7, 6, 4, 7, 19, 2, 1]
for i in values:
if i % 2 != 0:
# 忽略奇数
continue
print i/2
1
2
3
3
2
1

break 语句

遇到 break 的时候,程序会跳出循环,不管循环条件是不是满足

else语句

if 一样, whilefor 循环后面也可以跟着 else 语句,不过要和break 一起连用。

  • 当循环正常结束时,循环条件不满足, else 被执行;
  • 当循环被 break 结束时,循环条件仍然满足, else 不执行。

不执行:

1
2
3
4
5
6
7
values = [7, 6, 4, 7, 19, 2, 1]
for x in values:
if x <= 10:
print 'Found:', x
break
else:
print 'All values greater than 10'
1
Found: 7

执行:

1
2
3
4
5
6
7
values = [11, 12, 13, 100]
for x in values:
if x <= 10:
print 'Found:', x
break
else:
print 'All values greater than 10'
1
All values greater than 10

容器型数据类型

容器型数据类型包括:

类型 例子
列表(list) [1, 1.2, 'hello']
元组(Tuple) (10, 11, 12, 13, 14)
集合(set) {1, 2, 3, 1}
字典(dict) {'dogs': 5, 'pigs': 3}
Numpy数组 array([1, 2, 3])

列表

Python中,列表是一个有序的序列。

列表用一对 [] 生成,中间的元素用 ,隔开,其中的元素不需要是同一类型,同时列表的长度也不固定。

1
2
3
l = [1, 2.0, 'hello']
print l
[1, 2.0, 'hello']

空列表可以用 [] 或者 list() 生成:

1
2
empty_list = []
empty_list
1
[]
1
2
empty_list = list()
empty_list
1
[]

列表操作

与字符串类似,列表也支持以下的操作:

长度

len 查看列表长度:

1
len(l)

加法和乘法

列表加法,相当于将两个列表按顺序连接:

1
2
3
a = [1, 2, 3]
b = [3.2, 'hello']
a + b
1
[1, 2, 3, 3.2, 'hello']

列表与整数相乘,相当于将列表重复相加:

1
l * 2
1
[1, 2.0, 'hello', 1, 2.0, 'hello']

索引和分片

列表和字符串一样可以通过索引和分片来查看它的元素。

索引:

1
2
a = [10, 11, 12, 13, 14]
a[0]
1
10

反向索引:

1
a[-1]
1
14

分片:

1
a[2:-1]
1
[12, 13]

与字符串不同的是,列表可以通过索引和分片来修改。

对于字符串,如果我们通过索引或者分片来修改,Python会报错:

1
2
3
4
5
6
7
8
9
10
11
s = "hello world"
# 把开头的 h 改成大写
s[0] = 'H'
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-10-844622ced67a> in <module>()
1 s = "hello world"
2 # 把开头的 h 改成大写
----> 3 s[0] = 'H'

TypeError: 'str' object does not support item assignment

而这种操作对于列表来说是可以的:

1
2
3
a = [10, 11, 12, 13, 14]
a[0] = 100
print(a)
1
[100, 11, 12, 13, 14]

这种赋值也适用于分片,例如,将列表的第2,3两个元素换掉:

1
2
a[1:3] = [1, 2]
print(a)
1
[100, 1, 2, 13, 14]

事实上,对于连续的分片(即步长为 1 ),Python采用的是整段替换的方法,两者的元素个数并不需要相同,例如,将 [11,12] 替换为 [1,2,3,4]

1
2
3
4
a = [10, 11, 12, 13, 14]
a[1:3] = [1, 2, 3, 4]
print a
[10, 1, 2, 3, 4, 13, 14]

这意味着,可以用这种方法来删除列表中一个连续的分片:

1
2
3
4
5
6
a = [10, 1, 2, 11, 12]
print a[1:3]
a[1:3] = []
print a
[1, 2]
[10, 11, 12]

对于不连续(间隔step不为1)的片段进行修改时,两者的元素数目必须一致:

1
2
3
a = [10, 11, 12, 13, 14]
a[::2] = [1, 2, 3]
a
1
[1, 11, 2, 13, 3]

否则会报错:

1
2
3
4
5
6
7
a[::2] = []
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-16-7b6c4e43a9fa> in <module>()
----> 1 a[::2] = []

ValueError: attempt to assign sequence of size 0 to extended slice of size 3

添加或删除元素

向列表添加单个元素

l.append(ob) 将元素 ob 添加到列表 l 的最后。

1
2
3
4
a = [10, 11, 12]
a.append(11)
print(a)
[10, 11, 12, 11]

append每次只添加一个元素,并不会因为这个元素是序列而将其展开:

1
2
3
a.append([11, 12])
print(a)
[10, 11, 12, 11, [11, 12]]

向列表添加序列

l.extend(lst) 将序列 lst 的元素依次添加到列表 l 的最后,作用相当于 l += lst

1
2
3
a = [10, 11, 12, 11]
a.extend([1, 2])
print(a)
1
[10, 11, 12, 11, 1, 2]

插入元素

l.insert(idx, ob) 在索引 idx 处插入ob ,之后的元素依次后移。

1
2
3
4
a = [10, 11, 12, 13, 11]
# 在索引 3 插入 'a'
a.insert(3, 'a')
print(a)
1
[10, 11, 12, 'a', 13, 11]

删除元素

Python提供了删除列表中元素的方法del

删除列表中的第一个元素:

1
2
3
4
a = [1002, 'a', 'b', 'c']
del a[0]
print(a)
['a', 'b', 'c']

删除第2到最后一个元素:

1
2
3
a = [1002, 'a', 'b', 'c']
del a[1:]
print(a)
1
[1002]

删除间隔的元素:

1
2
3
a = ['a', 1, 'b', 2, 'c']
del a[::2]
print(a)
1
[1, 2]

移除元素

l.remove(ob) 会将列表中第一个出现的 ob 删除,如果 ob 不在 l 中会报错。

1
2
3
4
5
a = [10, 11, 12, 13, 11]
# 移除了第一个 11
a.remove(11)
print a
[10, 12, 13, 11]

弹出元素

l.pop(idx) 会将索引 idx 处的元素删除,并返回这个元素。

1
2
a = [10, 11, 12, 13, 11]
a.pop(2)
1
12

列表遍历

1
2
3
4
5
6
7
8
9
10
list1 = [1, 3, 5, 7, 9]
# 通过循环用下标遍历列表元素
for index in range(len(list1)):
print(list1[index])
# 通过for循环遍历列表元素
for elem in list1:
print(elem)
# 通过enumerate函数处理列表之后再遍历可以同时获得元素索引和值
for index, elem in enumerate(list1):
print(index, elem)

列表方法

测试从属关系

in 来看某个元素是否在某个序列(不仅仅是列表)中,用not in来判断是否不在某个序列中。

1
2
3
4
5
a = [10, 11, 12, 13, 14]
print(10 in a)
print(10 not in a)
True
False

也可以作用于字符串:

1
2
3
4
5
s = 'hello world'
print('he' in s)
print('world' not in s)
True
False

列表中可以包含各种对象,甚至可以包含列表:

1
2
a = [10, 'eleven', [12, 13]]
a[2]
1
[12, 13]

a[2]是列表,可以对它再进行索引:

1
a[2][1]
1
13

列表中某个元素个数count

l.count(ob) 返回列表中元素 ob 出现的次数。

1
2
a = [11, 12, 13, 12, 11]
a.count(11)
1
2

列表中某个元素位置index

l.index(ob) 返回列表中元素 ob 第一次出现的索引位置,如果 ob 不在 l 中会报错。

1
a.index(12)
1
1

排序

l.sort() 会将列表中的元素按照一定的规则排序:

1
2
3
4
a = [10, 1, 11, 13, 11, 2]
a.sort()
print(a)
[1, 2, 10, 11, 11, 13]

如果不想改变原来列表中的值,可以使用 sorted 函数:

1
2
3
4
5
6
a = [10, 1, 11, 13, 11, 2]
b = sorted(a)
print(a)
print(b)
[10, 1, 11, 13, 11, 2]
[1, 2, 10, 11, 11, 13]

列表反向

l.reverse() 会将列表中的元素从后向前排列。

1
2
3
4
a = [1, 2, 3, 4, 5, 6]
a.reverse()
print(a)
[6, 5, 4, 3, 2, 1]

如果不想改变原来列表中的值,可以使用这样的方法:

1
2
3
4
5
6
a = [1, 2, 3, 4, 5, 6]
b = a[::-1]
print(a)
print(b)
[1, 2, 3, 4, 5, 6]
[6, 5, 4, 3, 2, 1]

元组

与列表相似,元组Tuple也是个有序序列,但是元组是不可变的,用()生成。

1
2
t = (10, 11, 12, 13, 14)
t
1
(10, 11, 12, 13, 14)

可以索引,切片:

1
t[0]
1
10
1
t[1:3]
1
(11, 12)

但是元组是不可变的:

1
2
# 会报错
t[0] = 1
1
2
3
4
5
6
7
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-4-da6c1cabf0b0> in <module>()
1 # 会报错
----> 2 t[0] = 1

TypeError: 'tuple' object does not support item assignment

单个元素的元组生成

由于()在表达式中被应用,只含有单个元素的元组容易和表达式混淆,所以采用下列方式定义只有一个元素的元组:

1
2
3
4
5
a = (10,)
print(a)
print(type(a))
(10,)
<type 'tuple'>
1
2
3
a = (10)
print(type(a))
<type 'int'>

将列表转换为元组:

1
2
a = [10, 11, 12, 13, 14]
tuple(a)
1
(10, 11, 12, 13, 14)
1
2
# 获取元组中的元素
print(a[0])
1
10

元组方法

由于元组是不可变的,所以只能有一些不可变的方法,例如计算元素个数 count 和元素位置 index ,用法与列表一样。

1
a.count(10)
1
1
1
a.index(12)
1
2

字符串

Python中可以使用一对单引号’’或者双引号””生成字符串。

1
2
3
s = "hello, world"
print s
hello, world
1
2
3
s = 'hello world'
print s
hello world
1
2
3
4
hello,
world!
hello,
python!

Python 用一对 """ 或者 ''' 来生成多行字符串:

1
2
3
4
5
6
7
strs = '''
hello,
world!
hello,
python!
'''
print(strs)

可以在字符串中使用(反斜杠)来表示转义,也就是说\后面的字符不再是它原来的意义,例如:\n不是代表反斜杠和字符n,而是表示换行;而\t也不是代表反斜杠和字符t,而是表示制表符。所以如果想在字符串中表示'要写成\',同理想表示\要写成\\。可以运行下面的代码看看会输出什么。

1
2
3
s1 = '\'hello, world!\''
s2 = '\n\\hello, world!\\\n'
print(s1, s2, end='')
1
2
'hello, world!' 
\hello, world!\

我们可以通过在字符串的最前面加上字母r来表示原始字符

1
2
3
s1 = r'\'hello, world!\''
s2 = r'\n\\hello, world!\\\n'
print(s1, s2, end='')
1
\'hello, world!\' \n\\hello, world!\\\n

格式化字符串还有更为简洁的书写方式,就是在字符串前加上字母f,我们可以使用下面的语法糖来简化上面的代码。

1
2
a, b = 5, 10
print(f'{a} * {b} = {a * b}')
1
5 * 10 = 50

简单操作

加法:

1
2
s = 'hello ' + 'world'
print(s)
1
'hello world'

字符串与数字相乘:

1
"echo" * 3
1
'echoechoecho'

字符串长度:

1
len(s)

使用innot in来判断一个字符串是否包含另外一个字符串(成员运算)

1
2
3
s = 'hello,world'
print('hello' in s) //True
print('good' in s) //False

索引和切片

我们也可以用[][:]运算符从字符串取出某个字符或某些字符(切片运算)

1
2
3
4
5
6
7
8
9
10
s = 'hello,world'
# 从字符串中取出指定位置的字符(下标运算)
print(s[2]) # l
# 字符串切片(从指定的开始索引到指定的结束索引)
print(s[2:5]) # llo
print(s[2:]) # llo,world
print(s[2::2]) # lowrd
print(s[::2]) # hlowrd
print(s[::-1]) # dlrow,olleh
print(s[-3:-1]) # rl

字符串的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 第一种方式,for in
strs = 'hello,world'
for s in strs:
print(s)

// 第二种方式,内置函数range()或xrange()
for i in range(len(strs)):
print(strs[i])

// 第三种方式,内置函数enumerate()
for index, str in enumerate(strs):
print(index,str)

// 第四种方式,内置函数iter()
for str in iter(strs):
print(str)

字符串方法

大小写转换

s.upper()方法返回一个将s中的字母全部大写的新字符串。

s.lower()方法返回一个将s中的字母全部小写的新字符串。

1
"hello world".upper()
1
'HELLO WORLD'

这两种方法也不会改变原来s的值:

1
2
3
s = "HELLO WORLD"
print(s.lower())
print(s)
1
2
hello world
HELLO WORLD

首字母大写

s.capitalize()方法返回一个将s中首字母大写的新字符串。

1
2
s = 'hello,world'
print(s.capitalize()) //Hello,world

s.title()方法返回一个将s中每个首字母大写的新字符串。

1
2
print(s.title())
Hello,World

str类型是不可变类型

查找子串

s.index()从左到右查找子串,可以指定起始查找位置,默认是0

s.rindex() 则是从右向左查找

1
2
3
4
s = 'hello,world'
print(s.index('l'))
print(s.index('l',3))
print(s.rindex('l'))
1
2
3
2
3
9

如果查找不到则会程序报错

1
print(s.index('re'))
1
2
3
4
Traceback (most recent call last):
File "/Users/mac7/PycharmProjects/pythonBasic/helloworld.py", line 114, in <module>
print(s.index('re'))
ValueError: substring not found

s.find() 查找不到则会返回-1

1
2
print(s.find('l'))
print(s.find('oo'))
1
2
2
-1

格式化输出

1
2
3
4
5
6
7
8
9
10
11
12
13
# 将字符串以指定的宽度居中并在两侧填充指定的字符
print(s.center(50, '*'))
# 将字符串以指定的宽度靠右放置左侧填充指定的字符
print(s.rjust(50, '-'))
# 将字符串以指定的宽度靠左放置右侧填充指定的字符
print(s.ljust(50, '-'))
# 0填充,使得字符串的长度达到指定的长度
n = '12345'
print(n.zfill(10))
# The original string is returned if width is less than or equal to len(s).
# 如果指定的长度小于等于原始的长度,则返回原始的字符串
n = '12345'
print(n.zfill(5)) // 12345
1
2
3
4
*******************hello,world********************
---------------------------------------hello,world
hello,world---------------------------------------
0000012345

格式化字符串

Python用字符串的format()方法来格式化字符串。

具体用法如下,字符串中花括号 {} 的部分会被format传入的参数替代,传入的值可以是字符串,也可以是数字或者别的对象。

1
2
a, b = 5, 10
print('{} * {} = {}'.format(a, b, a * b))
1
5 * 10 = 50

可以用数字指定传入参数的相对位置:

1
'{2} {1} {0}'.format('a', 'b', 'c')
1
'c b a'

还可以指定传入参数的名称:

1
'{color} {n} {x}'.format(n=10, x=1.5, color='blue')

可以在一起混用:

1
'{color} {0} {x} {1}'.format(10, 'foo', x = 1.5, color='blue')
1
'blue 10 1.5 foo'

Python 3.6以后,格式化字符串还有更为简洁的书写方式,就是在字符串前加上字母f,我们可以使用下面的语法糖来简化上面的代码。

1
2
a, b = 5, 10
print(f'{a} * {b} = {a * b}')

去除多余空格

s.strip()返回一个将s两端的多余空格除去的新字符串。

s.lstrip()返回一个将s开头的多余空格除去的新字符串。

s.rstrip()返回一个将s结尾的多余空格除去的新字符串。

1
2
s = "  hello world   "
s.strip()
1
'hello world'

s的值依然不会变化:

1
print(s)
1
'  hello world   '
1
s.lstrip()
1
'hello world   '
1
s.rstrip()
1
'  hello world'

分割与连接

s.split()将s按照空格(包括多个空格,制表符\t,换行符\n等)分割,并返回所有分割得到的字符串。

1
2
3
4
line = "1 2 3 4  5"
numbers = line.split()
print numbers
['1', '2', '3', '4', '5'

s.split(sep)以给定的sep为分隔符对s进行分割。

1
2
3
4
line = "1,2,3,4,5"
numbers = line.split(',')
print numbers
['1', '2', '3', '4', '5']

源码定义如下:

1
2
3
4
5
6
7
8
9
10
11
def split(self, *args, **kwargs): # real signature unknown
"""
Return a list of the words in the string, using sep as the delimiter string.

sep 参数一:指定的分割字符
The delimiter according which to split the string.
None (the default value) means split according to any whitespace,
and discard empty strings from the result.
maxsplit 参数二:最大允许分割数量,不指定则默认-1
Maximum number of splits to do.
-1 (the default value) means no limit.
1
2
3
line = "1,2,3,4,5,6"
numbers = line.split(",",maxsplit=3)
print(numbers)
1
['1', '2', '3', '4,5,6']

s.rsplit()则是从右向左拆分

1
2
s = "i love you"
print(s.rsplit(" ", 1))
1
['i love', 'you']

与分割相反,s.join(str_sequence)的作用是以s为连接符将字符串序列str_sequence中的元素连接起来,并返回连接后得到的新字符串:

1
2
3
4
strList = ["i","love","you"]
s = ' '
joinStr = s.join(strList)
print(joinStr)
1
i love you

集合

之前看到的列表和字符串都是一种有序序列,而集合 set 是一种无序的序列。

因为集合是无序的,所以当集合中存在两个同样的元素的时候,Python只会保存其中的一个(唯一性);同时为了确保其中不包含同样的元素,集合中放入的元素只能是不可变的对象(确定性)。

定义集合

可以用set()函数来显示的生成空集合:

1
2
a = set()
type(a)
1
set

也可以使用一个列表来初始化一个集合:

1
2
a = set([1, 2, 3, 1])
print(a)
1
{1, 2, 3}

集合会自动去除重复元素 1

可以看到,集合中的元素是用大括号{}包含起来的,这意味着可以用{}的形式来创建集合:

1
2
a = {1, 2, 3, 1}
print(a)
1
{1, 2, 3}

但是创建空集合的时候只能用set来创建,因为在Python中{}创建的是一个空的字典:

1
2
s = {}
type(s)
1
dict

成员运算

集合的成员变量在效率上是远远高于列表运算

1
2
3
set = {1,2,3,4,5}
print(1 in set)
print(7 not in set)

集合操作

假设有这样两个集合:

1
2
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

两个集合的并,返回包含两个集合所有元素的集合(去除重复)。

可以用方法 a.union(b) 或者操作 a | b 实现。

1
a.union(b)
1
{1, 2, 3, 4, 5, 6}
1
b.union(a)
1
{1, 2, 3, 4, 5, 6}
1
a | b
1
{1, 2, 3, 4, 5, 6}

两个集合的交,返回包含两个集合共有元素的集合。

可以用方法 a.intersection(b) 或者操作 a & b 实现。

1
a.intersection(b)
1
{3, 4}
1
b.intersection(a)
1
{3, 4}
1
a & b
1
{3, 4}
1
2
print(a & b)
set([3, 4])

注意:一般使用print打印set的结果与表示方法并不一致。

ab 的差集,返回只在 a 不在 b 的元素组成的集合。

可以用方法 a.difference(b) 或者操作 a - b 实现。

1
a.difference(b)
1
{1, 2}
1
a - b
1
{1, 2}

注意,a - bb - a并不一样,b - a 返回的是返回 b 不在 a 的元素组成的集合:

1
b.difference(a)
1
{5, 6}
1
b - a 
1
{5, 6}

对称差

ab 的对称差集,返回在 a 或在 b 中,但是不同时在 ab 中的元素组成的集合。

可以用方法 a.symmetric_difference(b) 或者操作 a ^ b 实现(异或操作符)。

1
a.symmetric_difference(b)
1
{1, 2, 5, 6}
1
b.symmetric_difference(a)
1
{1, 2, 5, 6}
1
a ^ b
1
{1, 2, 5, 6}

包含关系

假设现在有这样两个集合:

1
2
a = {1, 2, 3}
b = {1, 2}

要判断 b 是不是 a 的子集,可以用 b.issubset(a) 方法,或者更简单的用操作 b <= a

1
b.issubset(a)
1
True
1
b <= a
1
True

与之对应,也可以用 a.issuperset(b) 或者 a >= b 来判断:

1
a.issuperset(b)
1
True
1
a >= b
1
True

方法只能用来测试子集,但是操作符可以用来判断真子集:

1
a <= a
1
True

自己不是自己的真子集:

1
a < a
1
False

集合方法

add 方法向集合添加单个元素

跟列表的 append 方法类似,用来向集合添加单个元素。

1
s.add(a)

将元素 a 加入集合 s 中。

1
2
3
t = {1, 2, 3}
t.add(5)
print(t)
1
{1, 2, 3, 5}

如果添加的是已有元素,集合不改变:

1
2
t.add(3)
print(t)
1
{1, 2, 3, 5}
update 方法向集合添加多个元素

跟列表的extend方法类似,用来向集合添加多个元素。

1
s.update(seq)

seq中的元素添加到s中。

1
2
t.update([5, 6, 7])
print(t)
1
{1, 2, 3, 5, 6, 7}
remove 方法移除单个元素
1
s.remove(ob)

从集合s中移除元素ob,如果不存在会报错。

1
2
t.remove(1)
print(t)
1
{2, 3, 5, 6, 7}
1
2
3
4
5
6
7
t.remove(10)
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-31-3bc25c5e1ff4> in <module>()
----> 1 t.remove(10)

KeyError: 10
pop方法弹出元素

由于集合没有顺序,不能像列表一样按照位置弹出元素,所以pop 方法删除并返回集合中任意一个元素,如果集合中没有元素会报错。

1
t.pop()
1
{3, 5, 6, 7}
1
2
print(t)
set([3, 5, 6, 7])
1
2
3
4
5
6
7
8
9
10
11
s = set()
# 报错
s.pop()
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-34-9f9e06c962e6> in <module>()
1 s = set()
2 # 报错
----> 3 s.pop()

KeyError: 'pop from an empty set'
discard 方法

作用与 remove 一样,但是当元素在集合中不存在的时候不会报错。

1
2
t.discard(3)
print(t)
1
{5, 6, 7}

不存在的元素不会报错:

1
2
t.discard(20)
print(t)
1
{5, 6, 7}
difference_update方法
1
a.difference_update(b)

从a中去除所有属于b的元素:

清空集合
1
t.clear()

字典

字典 dictionary ,在一些编程语言中也称为 hashmap ,是一种由键值对组成的数据结构。

顾名思义,我们把键想象成字典中的单词,值想象成词对应的定义,那么——

一个词可以对应一个或者多个定义,但是这些定义只能通过这个词来进行查询。

初始化字典

1
2
3
4
5
6
# 创建字典的字面量语法
b = {'one': 'this is number 1', 'two': 'this is number 2'}
# 创建字典的构造器语法
items1 = dict(one=1, two=2, three=3, four=4)
# 创建字典的推导式语法
items3 = {num: num ** 2 for num in range(1, 10)}

Python 使用 {} 或者 dict() 来创建一个空的字典:

1
2
a = {}
type(a)
1
dict
1
2
a = dict()
type(a)
1
dict

基本操作

有了dict之后,可以用索引键值的方法向其中添加元素,也可以通过索引来查看元素的值:

插入键值

1
2
3
a["one"] = "this is number 1"
a["two"] = "this is number 2"
print(a)
1
{'one': 'this is number 1', 'two': 'this is number 2'}

查看键值

1
a['one']
1
'this is number 1'

更新键值

1
2
a["one"] = "this is number 1, too"
a
1
{'one': 'this is number 1, too', 'two': 'this is number 2'}

遍历

1
2
3
4
5
6
7
8
9
10
11
12
# 对字典中所有键值对进行遍历
dic = dict(one=1, two=2, three=3, four=4)
for key in dic:
print(f'{key}: {dic[key]}')

# 遍历字典中所以的值
for value in dic.values():
print(value)

# 遍历键值对
for key, value in dic.items():
print(key, value)

字典没有顺序

当我们 print 一个字典时,Python并不一定按照插入键值的先后顺序进行显示,因为字典中的键本身不一定是有序的。

1
2
print a
{'two': 'this is number 2', 'one': 'this is number 1, too'}
1
2
print b
{'two': 'this is number 2', 'one': 'this is number 1'}

因此,Python中不能用支持用数字索引按顺序查看字典中的值,而且数字本身也有可能成为键值,这样会引起混淆:

1
2
3
4
5
6
7
8
9
# 会报错
a[0]
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-9-cc39af2a359c> in <module>()
1 # 会报错
----> 2 a[0]

KeyError: 0

使用 dict 初始化字典

除了通常的定义方式,还可以通过 dict() 转化来生成字典:

1
2
3
4
5
6
7
inventory = dict(
[('foozelator', 123),
('frombicator', 18),
('spatzleblock', 34),
('snitzelhogen', 23)
])
inventory
1
{'foozelator': 123, 'frombicator': 18, 'snitzelhogen': 23, 'spatzleblock': 34}

利用索引直接更新键值对:

1
2
inventory['frombicator'] += 1
inventory
1
{'foozelator': 123, 'frombicator': 19, 'snitzelhogen': 23, 'spatzleblock': 34}

适合做键的类型

在不可变类型中,整数字符串是字典中最常用的类型;而浮点数通常不推荐用来做键,原因如下:

1
2
3
4
5
6
7
8
9
10
11
12
data = {}
data[1.1 + 2.2] = 6.6
# 会报错
data[3.3]
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-16-a48e87d01daa> in <module>()
2 data[1.1 + 2.2] = 6.6
3 # 会报错
----> 4 data[3.3]

KeyError: 3.3

事实上,观察data的值就会发现,这个错误是由浮点数的精度问题所引起的:

1
data
1
{3.3000000000000003: 6.6}

有时候,也可以使用元组作为键值,例如,可以用元组做键来表示从第一个城市飞往第二个城市航班数的多少:

1
2
3
4
connections = {}
connections[('New York', 'Seattle')] = 100
connections[('Austin', 'New York')] = 200
connections[('New York', 'Austin')] = 400

元组是有序的,因此 ('New York', 'Austin')('Austin', 'New York') 是两个不同的键:

1
2
3
4
print connections[('Austin', 'New York')]
print connections[('New York', 'Austin')]
200
400

字典方法

get 方法

之前已经见过,用索引可以找到一个键对应的值,但是当字典中没有这个键的时候,Python会报错,这时候可以使用字典的 get 方法来处理这种情况,其用法如下:

1
d.get(key, default = None)

返回字典中键 key 对应的值,如果没有这个键,返回 default 指定的值(默认是 None )。

1
2
3
a = {}
a["one"] = "this is number 1"
a["two"] = "this is number 2"

索引不存在的键值会报错:

1
2
3
4
5
6
7
a["three"]
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-22-8a5f2913f00e> in <module>()
----> 1 a["three"]

KeyError: 'three'

改用get方法:

1
2
print a.get("three")
None

指定默认值参数:

1
a.get("three", "undefined")
1
'undefined'

pop 方法删除元素

pop() 方法可以用来弹出字典中某个键对应的值,同时也可以指定默认参数:

1
d.pop(key, default = None)

删除并返回字典中键 key 对应的值,如果没有这个键,返回 default 指定的值(默认是 None )。

1
print(a)
1
{'one': 'this is number 1', 'two': 'this is number 2'}

弹出并返回值:

1
print(a.pop("two"))
1
'this is number 2'
1
print(a)
1
{'one': 'this is number 1'}

弹出不存在的键值:

1
a.pop("two", 'not exist')
1
'not exist'

与列表一样,del 函数可以用来删除字典中特定的键值对,例如:

1
2
del a["one"]
print(a)
1
{}

可以用popItem() 方法删除最后一个元素

update方法更新字典

之前已经知道,可以通过索引来插入、修改单个键值对,但是如果想对多个键值对进行操作,这种方法就显得比较麻烦,好在有 update 方法:

1
d.update(newd)

将字典newd中的内容更新到d中去。

1
2
3
4
5
6
person = {}
person['first'] = "Jmes"
person['last'] = "Maxwell"
person['born'] = 1831
print person
{'born': 1831, 'last': 'Maxwell', 'first': 'Jmes'}

把’first’改成’James’,同时插入’middle’的值’Clerk’:

1
2
3
4
person_modifications = {'first': 'James', 'middle': 'Clerk'}
person.update(person_modifications)
print person
{'middle': 'Clerk', 'born': 1831, 'last': 'Maxwell', 'first': 'James'}

类似的方法还有a.setdefault()

If key is in the dictionary, return its value.
If not, insert key with a value of default and return default. default defaults to None.

in查询字典中是否有该键

1
barn = {'cows': 1, 'dogs': 5, 'cats': 3}

in 可以用来判断字典中是否有某个特定的键:

1
'chickens' in barn
1
False
1
'cows' in barn
1
True

字典键值互换

1
2
3
a = {'one': 1, 'two': 2, 'three': 3}
b = dict(zip(a.values(),a.keys()))
print(b)
1
{1: 'one', 2: 'two', 3: 'three'}

函数

定义函数

函数function ,通常接受输入参数,并有返回值。

它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。

1
2
3
4
def add(x, y):
"""Add two numbers"""
a = x + y
return a

函数通常有一下几个特征:

  • 使用 def 关键词来定义一个函数。
  • def 后面是函数的名称,括号中是函数的参数,不同的参数用 , 隔开, def foo(): 的形式是必须要有的,参数可以为空;
  • 使用缩进来划分函数的内容;
  • docstring""" 包含的字符串,用来解释函数的用途,可省略;
  • return 返回特定的值,如果省略,返回 None

函数的文档注释:

1
2
3
4
5
6
7
8
9
10
def add(x, y):
"""
Add two numbers

:param x: 需要求和的参数一
:param y: 需要求和的参数二
:return: 求和的结果
"""
a = x + y
return a

效果如下:

使用函数

使用函数时,只需要将参数换成特定的值传给函数。

Python并没有限定参数的类型,因此可以使用不同的参数类型:

1
2
print add(2, 3) # 5
print add('foo', 'bar') # foobar

在这个例子中,如果传入的两个参数不可以相加,那么Python会将报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
print add(2, "foo")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-3-6f8dcf7eb280> in <module>()
----> 1 print add(2, "foo")

<ipython-input-1-e831943cfaf2> in add(x, y)
1 def add(x, y):
2 """Add two numbers"""
----> 3 a = x + y
4 return a

TypeError: unsupported operand type(s) for +: 'int' and 'str'

如果传入的参数数目与实际不符合,也会报错:

1
2
3
4
5
6
7
print add(1, 2, 3)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-4-ed7bae31fc7d> in <module>()
----> 1 print add(1, 2, 3)

TypeError: add() takes exactly 2 arguments (3 given)
1
2
3
4
5
6
7
print add(1)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-5-a954233d3b0d> in <module>()
----> 1 print add(1)

TypeError: add() takes exactly 2 arguments (1 given)

传入参数时,Python提供了两种选项,第一种是上面使用的按照位置传入参数,另一种则是使用关键词模式,显式地指定参数的值:

1
2
3
4
print add(x=2, y=3)
print add(y="foo", x="bar")
5
barfoo

可以混合这两种模式:

1
2
print add(2, y=3)
5

设定参数默认值

可以在函数定义的时候给参数设定默认值,例如:

1
2
def quad(x, a=1, b=0, c=0):
return a*x**2 + b*x + c

可以省略有默认值的参数:

1
2
print quad(2.0)
4.0

可以修改参数的默认值:

1
2
print quad(2.0, b=3)
10.0
1
2
print quad(2.0, 2, c=4)
12.0

这里混合了位置和指定两种参数传入方式,第二个2是传给 a 的。

注意,在使用混合语法时,要注意不能给同一个值赋值多次,否则会报错,例如:

1
2
3
4
5
6
7
print quad(2.0, 2, a=2)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-12-101d0c090bbb> in <module>()
----> 1 print quad(2.0, 2, a=2)

TypeError: quad() got multiple values for keyword argument 'a'

作用域

A scope is a textual region of a Python program where a namespace is directly accessible. “Directly accessible” here means that an unqualified reference to a name attempts to find the name in the namespace.

在一个 python 程序中,直接访问一个变量,会从内到外依次访问所有的作用域直到找到,否则会报未定义的错误。

变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称。Python 的作用域一共有4种,分别是:

有四种作用域:

  • L(Local):最内层,包含局部变量,比如一个函数/方法内部。
  • E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。
  • G(Global):当前脚本的最外层,比如当前模块的全局变量。
  • B(Built-in): 包含了内建的变量/关键字等,最后被搜索。

规则顺序: L –> E –> G –> B

在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内置中找。

1
2
3
4
5
g_count = 0  # 全局作用域
def outer():
o_count = 1 # 闭包函数外的函数中
def inner():
i_count = 2 # 局部作用域

内置作用域是通过一个名为 builtin 的标准模块来实现的,但是这个变量名自身并没有放入内置作用域内,所以必须导入这个文件才能够使用它。在Python3.0中,可以使用以下的代码来查看到底预定义了哪些变量:

1
2
>>> import builtins
>>> dir(builtins)

Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问,如下代码:

global 和 nonlocal关键字

当内部作用域想修改外部作用域的变量时,就要用到 global 和 nonlocal 关键字了。

global声明使用全局变量或者将一个局部变量放到全局变量中

1
2
3
4
5
6
7
8
num = 1
def fun1():
global num # 需要使用 global 关键字声明
print(num)
num = 123
print(num)
fun1()
print(num)
1
2
3
1
123
123

nonlocal声明使用嵌套作用域变量(不使用局部变量)

1
2
3
4
5
6
7
8
9
def outer():
num = 10

def inner():
nonlocal num # nonlocal关键字声明
num = 100
print(num)
inner()
print(num)
1
2
100
100

用模块管理函数

对于任何一种编程语言来说,给变量、函数这样的标识符起名字都是一个让人头疼的问题,因为我们会遇到命名冲突这种尴尬的情况。最简单的场景就是在同一个.py文件中定义了两个同名函数,由于Python没有函数重载的概念,那么后面的定义会覆盖之前的定义,也就意味着两个函数同名函数实际上只有一个是存在的。

1
2
3
4
5
6
7
8
9
10
def foo():
print('hello, world!')


def foo():
print('goodbye, world!')


# 下面的代码会输出什么呢?
foo()

当然上面的这种情况我们很容易就能避免,但是如果项目是由多人协作进行团队开发的时候,团队中可能有多个程序员都定义了名为foo的函数,那么怎么解决这种命名冲突呢?答案其实很简单,Python中每个文件就代表了一个模块(module),我们在不同的模块中可以有同名的函数,在使用函数的时候我们通过import关键字导入指定的模块就可以区分到底要使用的是哪个模块中的foo函数,代码如下所示。

module1.py

1
2
def foo():
print('hello, world!')

module2.py

1
2
def foo():
print('goodbye, world!')

test.py

1
2
3
4
5
6
7
8
9
10
11
from module1 import foo

# 输出hello, world!

foo()

from module2 import foo

# 输出goodbye, world!

foo()

也可以按照如下所示的方式来区分到底要使用哪一个foo函数。

test.py

1
2
3
4
5
import module1 as m1
import module2 as m2

m1.foo()
m2.foo()

但是如果将代码写成了下面的样子,那么程序中调用的是最后导入的那个foo,因为后导入的foo覆盖了之前导入的foo

test.py

1
2
3
4
5
from module1 import foo
from module2 import foo

# 输出goodbye, world!
foo()

test.py

1
2
3
4
5
6
from module2 import foo
from module1 import foo

# 输出hello, world!

foo()

需要说明的是,如果我们导入的模块除了定义函数之外还有可以执行代码,那么Python解释器在导入这个模块时就会执行这些代码,事实上我们可能并不希望如此,因此如果我们在模块中编写了执行代码,最好是将这些执行代码放入如下所示的条件中,这样的话除非直接运行该模块,if条件下的这些代码是不会执行的,因为只有直接执行的模块的名字才是”main“。

module3.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def foo():
pass


def bar():
pass


# __name__是Python中一个隐含的变量它代表了模块的名字
# 只有被Python解释器直接执行的模块的名字才是__main__
if __name__ == '__main__':
print('call foo()')
foo()
print('call bar()')
bar()

test.py

1
2
3
import module3

# 导入module3时 不会执行模块中if条件成立时的代码 因为模块的名字是module3而不是__main

接收不定参数

使用如下方法,可以使函数接受不定数目的参数:

1
2
3
4
5
def add(x, *args):
total = x
for arg in args:
total += arg
return total

这里,*args 表示参数数目不定,可以看成一个元组,把第一个参数后面的参数当作元组中的元素。

1
2
3
4
print(add(1, 2, 3, 4))
print(add(1, 2))
10
3

关键字参数

这样定义的函数不能使用关键词传入参数,要使用关键词,可以这样:

1
2
3
4
5
6
def add(x, **kwargs):
total = x
for arg, value in kwargs.items():
print("adding ", arg)
total += value
return total

这里, **kwargs 表示参数数目不定,相当于一个字典,关键词和值对应于键值对。

1
print(add(10, y=11, z=12, w=13))
1
2
3
4
adding  y
adding z
adding w
46

再看这个例子,可以接收任意数目的位置参数和键值对参数:

1
2
3
4
5
def foo(*args, **kwargs):
print(args, kwargs)

foo(2, 3, x='bar', z=10)
(2, 3) {'x': 'bar', 'z': 10}

不过要按顺序传入参数,先传入位置参数 args ,在传入关键词参数 kwargs

命名关键字参数

如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。
这种方式定义的函数如下:

1
2
3
4
def person(name, age, *, city, job)
print(name, age, city, job)
# 调用
person('city', 22, city='Beijing', job='IT')

输出:

1
yjc 22 Beijing IT

和关键字参数**kw不同,命名关键字参数需要一个特殊的分隔符**后面的参数被视为命名关键字参数
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。

返回多个值

函数可以返回多个值:

1
2
3
4
5
6
7
8
9
10
from math import atan2

def to_polar(x, y):
r = (x**2 + y**2) ** 0.5
theta = atan2(y, x)
return r, theta

r, theta = to_polar(3, 4)
print(r, theta)
5.0 0.927295218002

事实上,Python将返回的两个值变成了元组:

1
2
print(to_polar(3, 4))
(5.0, 0.9272952180016122)

因为这个元组中有两个值,所以可以使用

1
r, theta = to_polar(3, 4)

给两个值赋值。

列表也有相似的功能:

1
2
a, b, c = [1, 2, 3]
print(a, b, c) #1 2 3

事实上,不仅仅返回值可以用元组表示,也可以将参数用元组以这种方式传入:

1
2
3
4
5
6
7
def add(x, y):
"""Add two numbers"""
a = x + y
return a

z = (2, 3)
print add(*z) #5

这里的*必不可少。

事实上,还可以通过字典传入参数来执行函数:

1
2
3
4
5
6
7
def add(x, y):
"""Add two numbers"""
a = x + y
return a

w = {'x': 2, 'y': 3}
print add(**w) # 5

map 方法生成序列

可以通过 map 的方式利用函数来生成序列:

1
2
3
4
5
def sqr(x): 
return x ** 2

a = [2,3,4]
print map(sqr, a)

map方法返回一个迭代器:

1
[4, 9, 16]
1
print(list(map(sqr, a))) # [4, 9, 16]

其用法为:

1
map(aFun, aSeq)

将函数 aFun 应用到序列 aSeq 上的每一个元素上,返回一个迭代器,不管这个序列原来是什么类型。

事实上,根据函数参数的多少,map 可以接受多组序列,将其对应的元素作为参数传入函数:

1
2
3
4
5
6
7
def add(x, y):
return x + y


a = (2,3,4)
b = [10,5,3]
print(list(map(add, a, b))) # [12, 8, 7]

一等函数

Python中的函数是一等函数

  • 函数可以作为函数的参数
  • 函数可以作为函数的返回值
  • 函数可以赋值给变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def calculate(init_value, fn, *args, **kwargs):
total = init_value
for arg in args:
total = fn(total, arg)
for kwarg in kwargs.values():
total = fn(total, kwarg)
return total

# 加
def add(a, b):
return a + b

# 乘
def multipication(a, b):
return a * b


print(calculate(0, add, 1, 2, 3, 4))
print(calculate(1, multipication, 1, 2, 3, 4))
1
2
10
24

Lambda函数

关键字lambda表示匿名函数,冒号前面的表示函数参数。

匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。

用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

1
2
print(calculate(0, lambda a, b: a + b, 1, 2, 3, 4))
print(calculate(1, lambda a, b: a * b, 1, 2, 3, 4))
1
2
10
24

模块和包

模块

Python会将所有 .py 结尾的文件认定为Python代码文件,在 Python 中,一个 .py 文件就称之为一个模块(Module)。考虑下面的脚本 ex1.py

1
2
3
4
5
6
7
8
9
10
11
12
%%writefile ex1.py

PI = 3.1416

def sum(lst):
tot = lst[0]
for value in lst[1:]:
tot = tot + value
return tot

w = [0, 1, 2, 3]
print(sum(w), PI)

可以执行它:

1
2
%run ex1.py
6 3.1416

这个脚本可以当作一个模块,可以使用import 关键词加载并执行它(这里要求ex1.py在当前工作目录):

1
2
import ex1
6 3.1416
1
print(ex1)
1
<module 'ex1' from 'ex1.py'>

在导入时,Python会执行一遍模块中的所有内容。

ex1.py 中所有的变量都被载入了当前环境中,不过要使用

1
ex1.变量名

的方法来查看或者修改这些变量:

1
print(ex1.PI) # 3.1416
1
2
3
ex1.PI = 3.141592653
print(ex1.PI)
3.141592653

还可以用

1
ex1.函数名

调用模块里面的函数:

1
2
print(ex1.sum([2, 3, 4]))
9

为了提高效率,Python只会载入模块一次,已经载入的模块再次载入时,Python并不会真正执行载入操作,哪怕模块的内容已经改变。

例如,这里重新导入 ex1 时,并不会执行 ex1.py 中的 print 语句:

1
import ex1

需要重新导入模块时,可以使用reload强制重新载入它,例如:

1
2
reload(ex1)
6 3.1416
1
<module 'ex1' from 'ex1.pyc'>

删除之前生成的文件:

1
2
import os
os.remove('ex1.py')

__name__ 属性

有时候我们想将一个 .py 文件既当作脚本,又能当作模块用,这个时候可以使用 __name__ 这个属性。

只有当文件被当作脚本执行的时候,__name__的值才会是 __main__,所以我们可以:

ex2.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PI = 3.1416

def sum(lst):
""" Sum the values in a list
"""
tot = 0
for value in lst:
tot = tot + value
return tot

def add(x, y):
" Add two values."
a = x + y
return a

def test():
w = [0,1,2,3]
assert(sum(w) == 6)
print 'test passed.'

if __name__ == '__main__':
test()
Writing ex2.py

运行文件:

1
2
%run ex2.py
test passed.

当作模块导入, test() 不会执行:

1
import ex2

但是可以使用其中的变量:

1
ex2.PI
1
3.1416

使用别名:

1
2
import ex2 as e2
e2.PI
1
3.1416

其他导入方法

可以从模块中导入变量:

1
from ex2 import add, PI

使用 from 后,可以直接使用addPI

1
print(add(2, 3))
1
5

或者使用 * 导入所有变量:

1
2
from ex2 import *
add(3, 4.5)
1
7.5

这种导入方法不是很提倡,因为如果你不确定导入的都有哪些,可能覆盖一些已有的函数。

删除文件:

1
2
import os
os.remove('ex2.py')

假设我们有这样的一个文件夹:

foo/

  • __init__.py
  • bar.py (defines func)
  • baz.py (defines zap)

这意味着 foo 是一个包,我们可以这样导入其中的内容:

1
2
from foo.bar import func
from foo.baz import zap

barbaz 都是 foo 文件夹下的 .py 文件。

导入包要求:

  • 文件夹 fooPython的搜索路径中
  • __init__.py 表示 foo 是一个包,它可以是个空文件。

解决命名冲突

方式一:导入函数的时候,对函数进行别名

1
2
3
4
5
6
from foo.person import eat as person_eat
from foo.animal import eat as animal_eat


person_eat()
animal_eat()

方式二:使用完全限定名(qualified name)–>[包名.]模块名.函数名

1
2
3
4
5
6
import foo.person
import foo.animal


foo.person.eat()
foo.animal.eat()

常用的标准库

  • re 正则表达式
  • copy 复制
  • math, cmath 数学
  • decimal, fraction
  • sqlite3 数据库
  • os, os.path 文件系统
  • gzip, bz2, zipfile, tarfile 压缩文件
  • csv, netrc 各种文件格式
  • xml
  • htmllib
  • ftplib, socket
  • cmd 命令行
  • pdb
  • profile, cProfile, timeit
  • collections, heapq, bisect 数据结构
  • mmap
  • threading, Queue 并行
  • multiprocessing
  • subprocess
  • pickle, cPickle
  • struct

PYTHONPATH设置

Python的搜索路径可以通过环境变量PYTHONPATH设置,环境变量的设置方法依操作系统的不同而不同,具体方法可以网上搜索。


面向对象

Python 就是一门面向对象的语言,

如果你学过 Java ,就知道 Java 的编程思想就是:万事万物皆对象。Python 也不例外,在解决实际问题的过程中,可以把构成问题事务分解成各个对象。

面向对象都有两个基本的概念,分别是类和对象。

用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。

  • 对象

通过类定义的数据结构实例

面向对象的三大特性

面向对象的编程语言,也有三大特性,继承,多态和封装性。

  • 继承

即一个派生类(derived class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。

例如:一个 Dog 类型的对象派生自 Animal 类,这是模拟”是一个(is-a)”关系(例图,Dog 是一个 Animal )。

  • 多态

它是指对不同类型的变量进行相同的操作,它会根据对象(或类)类型的不同而表现出不同的行为。

  • 封装性

“封装”就是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体(即类);封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,一特定的访问权限来使用类的成员。

定义类

在Python中可以使用class关键字定义类,类的命名采用驼峰命名法,然后在类中通过之前学习过的函数来定义方法,这样就可以将对象的动态特征描述出来,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Student(object):

# __init__是一个特殊方法用于在创建对象时进行初始化操作
# 通过这个方法我们可以为学生对象绑定name和age两个属性
def __init__(self, name, age):
self.name = name
self.age = age

def eat(self):
print(f'{self.name}正在吃饭.')

def study(self):
print(f'{self.name}正在学习.')

# PEP 8要求标识符的名字用全小写多个单词用下划线连接
# 但是部分程序员和公司更倾向于使用驼峰命名法(驼峰标识)
def watch_movie(self):
if self.age < 18:
print('%s只能观看《熊出没》.' % self.name)
else:
print('%s正在观看岛国爱情大电影.' % self.name)

创建和使用对象

1
2
3
s = Student('张三', 18)
s.eat()
s.study()
1
2
张三正在吃饭.
张三正在学习.

静态方法和类方法

Python 的类方法和静态方法很相似,它们都推荐使用类来调用(其实也可使用对象来调用)。

类方法和静态方法的区别在于,Python会自动绑定类方法的第一个参数,类方法的第一个参数(通常建议参数名为 cls)会自动绑定到类本身;但对于静态方法则不会自动绑定。

使用 @classmethod 修饰的方法就是类方法;使用 @staticmethod 修饰的方法就是静态方法。

1
2
3
4
5
6
7
8
9
class Bird:
# classmethod修饰的方法是类方法
@classmethod
def fly (cls):
print('类方法fly: ', cls)
# staticmethod修饰的方法是静态方法
@staticmethod
def info (p):
print('静态方法info: ', p)

调用类方法,Bird类会自动绑定到第一个参数

1
2
Bird.fly()  
# 类方法fly: <class '__main__.Bird'>

调用静态方法,不会自动绑定,因此程序必须手动绑定第一个参数

1
2
Bird.info('crazyit')
# 静态方法info: crazyit

创建Bird对象

1
2
b = Bird()
# 类方法fly: <class '__main__.Bird'>

使用对象调用fly()类方法,其实依然还是使用类调用, 因此第一个参数依然被自动绑定到Bird类

1
2
b.fly()
# 静态方法info: fkit

使用对象调用info()静态方法,其实依然还是使用类调用,因此程序必须为第一个参数执行绑定

1
2
b.info('fkit')
# 静态方法info: fkit

继承

首先我们来看下类的继承的基本语法:

1
2
3
4
5
6
class ClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>

在定义类的时候,可以在括号里写继承的类,如果不用继承类的时候,也要写继承 object 类,因为在 Python 中 object 类是一切类的父类。

当然上面的是单继承,Python 也是支持多继承的,具体的语法如下:

1
2
3
4
5
6
class ClassName(Base1,Base2,Base3):
<statement-1>
.
.
.
<statement-N>

多继承有一点需要注意的:若是父类中有相同的方法名,而在子类使用时未指定,python 在圆括号中父类的顺序,从左至右搜索 , 即方法在子类中未找到时,从左到右查找父类中是否包含方法。

继承的子类的好处:

  • 会继承父类的属性和方法
  • 可以自己定义,覆盖父类的属性和方法

定义父类

1
2
3
4
5
6
7
8
9
10
11
class Person:

def __init__(self, name, gender):
self.name = name
self.gender = gender

def eat(self):
print(f'{self.name}正在吃饭')

def play(self):
print(f'{self.name}正在玩')

继承父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student(Person):

def __init__(self, name, gender, grade):
super().__init__(name, gender)
self.grade = grade

def study(self):
print(f'{self.name}正在学习')


s = Student("张三", True, 80)
s.eat()
s.play()
s.study()
1
2
3
张三正在吃饭
张三正在玩
张三正在学习

子类的类型判断

对于 class 的继承关系来说,有些时候我们需要判断 class 的类型,该怎么办呢?

可以使用 isinstance() 函数,

一个例子就能看懂 isinstance() 函数的用法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class User1(object):
pass


class User2(User1):
pass


class User3(User2):
pass


if __name__ == '__main__':
user1 = User1()
user2 = User2()
user3 = User3()
# isinstance()就可以告诉我们,一个对象是否是某种类型
print(isinstance(user3, User2))
print(isinstance(user3, User1))
print(isinstance(user3, User3))
# 基本类型也可以用isinstance()判断
print(isinstance('两点水', str))
print(isinstance(347073565, int))
print(isinstance(347073565, str))

输出的结果如下:

1
2
3
4
5
6
True
True
True
True
True
False

文件和异常

在Python中实现文件的读写操作其实非常简单,通过Python内置的open函数,我们可以指定文件名、操作模式、编码信息等来获得操作文件的对象,接下来就可以对文件进行读写操作了。这里所说的操作模式是指要打开什么样的文件(字符文件还是二进制文件)以及做什么样的操作(读、写还是追加),具体的如下表所示。

读写文本文件

读取文本文件时,需要在使用open函数时指定好带路径的文件名(可以使用相对路径或绝对路径)并将文件模式设置为'r'(如果不指定,默认值也是'r'),然后通过encoding参数指定编码(如果不指定,默认值是None,那么在读取文件时使用的是操作系统默认的编码),如果不能保证保存文件时使用的编码方式与encoding参数指定的编码方式是一致的,那么就可能因无法解码字符而导致读取失败。下面的例子演示了如何读取一个纯文本文件。

1
2
3
4
5
6
7
8
def main():
f = open('未选择的路.txt', 'r', encoding='utf-8')
print(f.read())
f.close()


if __name__ == '__main__':
main()

请注意上面的代码,如果open函数指定的文件并不存在或者无法打开,那么将引发异常状况导致程序崩溃。为了让代码有一定的健壮性和容错性,我们可以使用Python的异常机制对可能在运行时发生状况的代码进行适当的处理,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def main():
f = None
try:
f = open('未选择的路.txt', 'r', encoding='utf-8')
print(f.read())
except FileNotFoundError:
print('无法打开指定的文件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取文件时解码错误!')
finally:
if f:
f.close()


if __name__ == '__main__':
main()

在Python中,我们可以将那些在运行时可能会出现状况的代码放在try代码块中,在try代码块的后面可以跟上一个或多个except来捕获可能出现的异常状况。例如在上面读取文件的过程中,文件找不到会引发FileNotFoundError,指定了未知的编码会引发LookupError,而如果读取文件时无法按指定方式解码会引发UnicodeDecodeError,我们在try后面跟上了三个except分别处理这三种不同的异常状况。最后我们使用finally代码块来关闭打开的文件,释放掉程序中获取的外部资源,由于finally块的代码不论程序正常还是异常都会执行到(甚至是调用了sys模块的exit函数退出Python环境,finally块都会被执行,因为exit函数实质上是引发了SystemExit异常),因此我们通常把finally块称为“总是执行代码块”,它最适合用来做释放外部资源的操作。如果不愿意在finally代码块中关闭文件对象释放资源,也可以使用上下文语法,通过with关键字指定文件对象的上下文环境并在离开上下文环境时自动释放文件资源,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def main():
try:
with open('未选择的路.txt', 'r', encoding='utf-8') as f:
print(f.read())
except FileNotFoundError:
print('无法打开指定的文件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取文件时解码错误!')


if __name__ == '__main__':
main()

写文件

使用 w 模式时,如果文件不存在会被创建,如果文件已经存在, w 模式会覆盖之前写的所有内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def main():
file = open('venv/resource/望江南·超然台作.txt', mode='w', encoding='utf-8')
try:
file.write('春未老\r\n')
file.write('风细柳斜斜\r\n')
file.write('试上超然台上望\r\n')
file.write('半壕春水一城花\r\n')
file.write('烟雨暗千家\r\n')
file.write('寒食后\r\n')
file.write('酒醒却咨嗟\r\n')
file.write('休对故人思故国\r\n')
file.write('且将新火试新茶\r\n')
file.write('诗酒趁年华\r\n')
finally:
file.close()


if __name__ == '__main__':
main()

除了写入模式,还有追加模式 a ,追加模式不会覆盖之前已经写入的内容,而是在之后继续写入:

1
2
3
4
5
6
7
8
9
10
def main():
file = open('venv/resource/望江南·超然台作.txt', mode='a', encoding='utf-8')
try:
file.write('end\r\n')
finally:
file.close()


if __name__ == '__main__':
main()

二进制文件

读写二进制文件

知道了如何读写文本文件要读写二进制文件也就很简单了,下面的代码实现了复制图片文件的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def main():
try:
with open('guido.jpg', 'rb') as fs1:
data = fs1.read()
print(type(data)) # <class 'bytes'>
with open('吉多.jpg', 'wb') as fs2:
fs2.write(data)
except FileNotFoundError as e:
print('指定的文件无法打开.')
except IOError as e:
print('读写文件时出现错误.')
print('程序执行结束.')


if __name__ == '__main__':
main()

什么是正则表达式?

在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。

很可能你使用过Windows/Dos下用于文件查找的通配符(wildcard),也就是*?。如果你想查找某个目录下的所有的Word文档的话,你会搜索*.doc。在这里,会被解释成任意的字符串。和通配符类似,正则表达式也是用来进行文本匹配的工具,只不过比起通配符,它能更精确地描述你的需求——当然,代价就是更复杂——比如你可以编写一个正则表达式,用来查找所有以0开头,后面跟着2-3个数字,然后是一个连字号“-”,最后是7或8位数字的字符串(像010-123456780376-7654321)。

入门

学习正则表达式的最好方法是从例子开始,理解例子之后再自己对例子进行修改,实验。下面给出了不少简单的例子,并对它们作了详细的说明。

假设你在一篇英文小说里查找hi,你可以使用正则表达式hi

这几乎是最简单的正则表达式了,它可以精确匹配这样的字符串:由两个字符组成,前一个字符是h,后一个是i。通常,处理正则表达式的工具会提供一个忽略大小写的选项,如果选中了这个选项,它可以匹配hi,HI,Hi,hI这四种情况中的任意一种。

不幸的是,很多单词里包含hi这两个连续的字符,比如him,history,high等等。用hi来查找的话,这里边的hi也会被找出来。如果要精确地查找hi这个单词的话,我们应该使用\bhi\b

\b是正则表达式规定的一个特殊代码(好吧,某些人叫它元字符,metacharacter),代表着单词的开头或结尾,也就是单词的分界处。虽然通常英文的单词是由空格,标点符号或者换行来分隔的,但是\b并不匹配这些单词分隔字符中的任何一个,它只匹配一个位置

假如你要找的是hi后面不远处跟着一个Lucy,你应该用\bhi\b.*\bLucy\b

这里,.是另一个元字符,匹配除了换行符以外的任意字符。*同样是元字符,不过它代表的不是字符,也不是位置,而是数量——它指定前边的内容可以连续重复使用任意次以使整个表达式得到匹配。因此,.\连在一起就意味着任意数量的不包含换行的字符。现在\bhi\b.*\bLucy\b的意思就很明显了:先是一个单词hi,然后是任意个任意字符(但不能是换行),最后是Lucy这个单词。

换行符就是'\n',ASCII编码为10(十六进制0x0A)的字符。

如果同时使用其它元字符,我们就能构造出功能更强大的正则表达式。比如下面这个例子:

0\d\d-\d\d\d\d\d\d\d\d匹配这样的字符串:以0开头,然后是两个数字,然后是一个连字号“-”,最后是8个数字(也就是中国的电话号码。当然,这个例子只能匹配区号为3位的情形)。

这里的\d是个新的元字符,匹配一位数字(0,或1,或2,或……)-不是元字符,只匹配它本身——连字符(或者减号,或者中横线,或者随你怎么称呼它)。

为了避免那么多烦人的重复,我们也可以这样写这个表达式:0\d{2}-\d{8}。这里\d后面的{2}({8})的意思是前面\d必须连续重复匹配2次(8次)。

注解:

  1. 如果需要更精确的说法,\b匹配这样的位置:它的前一个字符和后一个字符不全是(一个是,一个不是或不存在)\w
  2. 换行符就是'\n',ASCII编码为10(十六进制0x0A)的字符。

元字符

现在你已经知道几个很有用的元字符了,如\b,.,*,还有\d.正则表达式里还有更多的元字符,比如\s匹配任意的空白符,包括空格,制表符(Tab),换行符,中文全角空格等。\w匹配字母或数字或下划线或汉字等。

对中文/汉字的特殊处理是由.Net提供的正则表达式引擎支持的,其它环境下的具体情况请查看相关文档。

下面来看看更多的例子:

\ba\w*\b匹配以字母a开头的单词——先是某个单词开始处(\b),然后是字母a,然后是任意数量的字母或数字(\w),最后是单词结束处(\b)。

\d+匹配1个或更多连续的数字。这里的+是和*类似的元字符,不同的是*匹配重复任意次(可能是0次),而+则匹配重复1次或更多次。

\b\w{6}\b 匹配刚好6个字符的单词。

代码 说明
. 匹配除换行符以外的任意字符
\w 匹配字母或数字或下划线
\s 匹配任意的空白符
\d 匹配数字
\b 匹配单词的开始或结束
^ 匹配字符串的开始
$ 匹配字符串的结束

元字符^(和数字6在同一个键位上的符号)和$都匹配一个位置,这和\b有点类似。^匹配你要用来查找的字符串的开头,$匹配结尾。这两个代码在验证输入的内容时非常有用,比如一个网站如果要求你填写的QQ号必须为5位到12位数字时,可以使用:^\d{5,12}$

这里的{5,12}和前面介绍过的{2}是类似的,只不过{2}匹配只能不多不少重复2次,{5,12}则是重复的次数不能少于5次,不能多于12次,否则都不匹配。

因为使用了^$,所以输入的整个字符串都要用来和\d{5,12}来匹配,也就是说整个输入必须是5到12个数字,因此如果输入的QQ号能匹配这个正则表达式的话,那就符合要求了。

和忽略大小写的选项类似,有些正则表达式处理工具还有一个处理多行的选项。如果选中了这个选项,^$的意义就变成了匹配行的开始处和结束处。

注解:

  1. 对中文/汉字的特殊处理是由.Net提供的正则表达式引擎支持的,其它环境下的具体情况请查看相关文档。
  2. 好吧,现在我们说说正则表达式里的单词是什么意思吧:就是不少于一个的连续的\w。不错,这与学习英文时要背的成千上万个同名的东西的确关系不大 :)

字符转义

有时候在我们查找 . ,或者*,或者一些元字符本身的话,会有一些问题出现,因为这些元字符已经变成别的意思了,所以我们没有办法指定这些元字符。出现这种情况,我们就得使用\来取消这些字符的特殊意义。所以,我们应该使用\.\*。当然,要查找\本身,你也得用\\.

例如:deerchao.net匹配deerchao.netC:\Windows匹配C:\Windows

重复

你已经看过了前面的*+{2}{5,12}这几个匹配重复的方式了。下面是正则表达式中所有的限定符(指定数量的代码,例如*,{5,12}等):

代码/语法 说明
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

下面是一些使用重复的例子:

Windows\d+匹配Windows后面跟1个或更多数字

^\w+匹配一行的第一个单词(或整个字符串的第一个单词,具体匹配哪个意思得看选项设置)

正则表达式之字符类

如果想要查找数字,字母或数字,空白是很简单的,因为对应这些字符集合的元字符是已经存在的了,不过要想匹配像元音字母a,e,i,o,u这些没有预先定义元字符的字符集合该怎么做呢?

有个很简单的方法,只需要在方括号里列出它们就可以了,像[aeiou]就匹配任何一个英文元音字母,[.?!]匹配标点符号(.或?或!)。

我们也可以轻松地指定一个字符范围,像[0-9]代表的含意与\d就是完全一致的:一位数字;同理[a-z0-9A-Z_]也完全等同于\w(如果只考虑英文的话)。

下面是一个更复杂的表达式:(?0\d{2}[) -]?\d{8}

这个表达式可以匹配几种格式的电话号码,像(010)88886666,或022-22334455,或02912345678等。我们对它进行一些分析吧:首先是一个转义字符(,它能出现0次或1次(?),然后是一个0,后面跟着2个数字(\d{2}),然后是)-空格中的一个,它出现1次或不出现(?),最后是8个数字(\d{8})。

注解: ()也是元字符,后面的分组节里会提到,所以在这里需要使用转义。

正则表达式的分枝条件

但是在上节内容介绍的那个表达式也可以用来匹配类似010)12345678或*(022-87654321*这样的错误的格式。怎么办?要想解决这个问题,我们可以使用分枝条件。正则表达式里的分枝条件指的是有几种规则,只要满足这些规则中的任意一种,都会被当成匹配,因此我们要使用|把这些规则分开。为了更好的理解上述内容,我们来看看几个例子:

0\d{2}-\d{8}|0\d{3}-\d{7}这个表达式能匹配两种以连字号分隔的电话号码:一种是三位区号,8位本地号(如010-12345678),一种是4位区号,7位本地号(0376-2233445)。

(0\d{2})[- ]?\d{8}|0\d{2}[- ]?\d{8}这个表达式匹配3位区号的电话号码,其中区号可以用小括号括起来,也可以不用,区号与本地号间可以用连字号或空格间隔,也可以没有间隔。你可以试试用分枝条件把这个表达式扩展成也支持4位区号的。

\d{5}-\d{4}|\d{5}这个表达式用于匹配美国的邮政编码。美国邮编的规则是5位数字,或者用连字号间隔的9位数字。之所以要给出这个例子是因为它能说明一个问题:使用分枝条件时,要注意各个条件的顺序。如果你把它改成\d{5}|\d{5}-\d{4}的话,那么就只会匹配5位的邮编(以及9位邮编的前5位)。原因是匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。

正则表达式之分组

在上述章节中我们已经知道直接在字符后面加上限定符就可以重复单个字符,那么多个字符的重复又该如何实现呢?你可以使用小括号来指定子表达式(也称为分组),然后对于这个子表达式的重复次数你就可以自行规定了,子表达式也可以进行一些其他的操作,这个在后面会进行介绍。

(\d{1,3}.){3}\d{1,3}是一个简单的IP地址匹配表达式。要理解这个表达式,请按下列顺序分析它:\d{1,3}匹配1到3位的数字,(\d{1,3}.){3}匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次,最后再加上一个一到三位的数字(\d{1,3})

不幸的是,它也将匹配256.300.888.999这种不可能存在的IP地址。如果能使用算术比较的话,或许能简单地解决这个问题,但是正则表达式中并不提供关于数学的任何功能,所以只能使用冗长的分组,选择,字符类来描述一个正确的IP地址:((2[0-4]\d|25[0-5]|[01]?\d\d?).){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)

理解这个表达式的关键是理解2[0-4]\d|25[0-5]|[01]?\d\d?,这里我就不细说了,你自己应该能分析得出来它的意义。

注解:

  1. IP地址中每个数字都不能大于255. 经常有人问我,01.02.03.04 这样前面带有0的数字, 是不是正确的IP地址呢? 答案是: 是的, IP 地址里的数字可以包含有前导 0 (leading zeroes).

正则表达式之反义

有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到反义

代码/语法 说明
\W 匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了x以外的任意字符
[^aeiou] 匹配除了aeiou这几个字母以外的任意字符

例子:\S+匹配不包含空白符的字符串。

]+>匹配用尖括号括起来的以a开头的字符串。

怎么查找不是某个字符或不在某个字符类里的字符的方法(反义)我们之前已经提过了。但是如果我们的目的不是去匹配某个字符,而是只想要该字符是否出现过,怎么办?例如,如果我们想要查找的单词中出现了字母q,但是字母q的后面跟着的不是字母u的话,我们可以尝试:

\b\w*q[^u]\w*\b匹配包含后面不是字母u的字母q的单词。但是如果多做测试(或者你思维足够敏锐,直接就观察出来了),你会发现,如果q出现在单词的结尾的话,像Iraq,Benq,这个表达式就会出错。这是因为[^u]总要匹配一个字符,所以如果q是单词的最后一个字符的话,后面的[^u]将会匹配q后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的\w*\b将会匹配下一个单词,于是\b\w*q[^u]\w*\b就能匹配整个Iraq fighting负向零宽断言能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,我们可以这样来解决这个题:\b\w*q(?!u)\w*\b

零宽度负预测先行断言(?!exp),断言此位置的后面不能匹配表达式exp。例如:\d{3}(?!\d)匹配三位数字,而且这三位数字的后面不能是数字;\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词。

同理,我们可以用(?<!exp),零宽度负回顾后发断言来断言此位置的前面不能匹配表达式exp:(?<![a-z])\d{7}匹配前面不是小写字母的七位数字。

一个更复杂的例子:(?<=<(\w+)>).*(?=)匹配不包含属性的简单HTML标签内里的内容。(?<=<(\w+)>)指定了这样的前缀:被尖括号括起来的单词(比如可能是),然后是.*(任意的字符串),最后是一个后缀(?=)。注意后缀里的/,它用到了前面提过的字符转义;\1则是一个反向引用,引用的正是捕获的第一组,前面的(\w+)匹配的内容,这样如果前缀实际上是的话,后缀就是了。整个表达式匹配的是之间的内容(再次提醒,不包括前缀和后缀本身)。

注解:

  • 请详细分析表达式(?<=<(\w+)>).*(?=),这个表达式最能表现零宽断言的真正用途。

正则表达式之注释

小括号还有另一种用途,就是通过语法(?#comment)来包含注释。例如:2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)

如果要包含注释,最好是启用“忽略模式里的空白符”选项,因为这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。

例如,我们可以前面的一个表达式写成这样:

(?<= # 断言要匹配的文本的前缀

<(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)

) # 前缀结束

.* # 匹配任意文本

(?= # 断言要匹配的文本的后缀

# 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签

) # 后缀结束

正则表达式之贪婪与懒惰

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。以这个表达式为例:a.*b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧:

a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。

代码/语法 说明
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

注解:

  • 为什么第一个匹配是aab(第一到第三个字符)而不是ab(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权——The match that begins earliest wins。

正则表达式之处理选项

以前介绍的忽略大小写、处理多行等选项可以用来改变处理正则表达式的方式,接下来要来介绍下载.Net中常用到的正则表达式的选项:

名称 说明
IgnoreCase(忽略大小写) 匹配时不区分大小写。
Multiline(多行模式) 更改^$的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.)
Singleline(单行模式) 更改.的含义,使它与每一个字符匹配(包括换行符\n)。
IgnorePatternWhitespace(忽略空白) 忽略表达式中的非转义空白并启用由#标记的注释。
ExplicitCapture(显式捕获) 仅捕获已被显式命名的组。

一个经常被问到的问题是:是不是只能同时使用多行模式和单行模式中的一种?答案是:不是。这两个选项之间没有任何关系,除了它们的名字比较相似(以至于让人感到疑惑)以外。

注解:

  • 在C#中,你可以使用Regex(String, RegexOptions)构造函数来设置正则表达式的处理选项。如:Regex regex = new Regex(@”\ba\w{6}\b”, RegexOptions.IgnoreCase);

正则表达式补充

到此为止,我们已经介绍了有关于正则表达式的大量元素,但是那么复杂的正则表达式,肯定还有一些元素还未提到,作者把那些未被提到的元素整合成如下的列表,其中还包含了语法和简单的说明。如果你觉得列表中的介绍不够详细的话,你也可以根据自己的需要利用更多的资源去学习它们。如果你的电脑中装了MSDN Library,你也能够在里面查到.net下正则表达式的详细文档,如果你没有安装MSDN Library,可以查看关于正则表达式语言元素的MSDN在线文档

代码/语法 说明
\a 报警字符(打印它的效果是电脑嘀一声)
\b 通常是单词分界位置,但如果在字符类里使用代表退格
\t 制表符,Tab
\r 回车
\v 竖向制表符
\f 换页符
\n 换行符
\e Escape
\0nn ASCII代码中八进制代码为nn的字符
\xnn ASCII代码中十六进制代码为nn的字符
\unnnn Unicode代码中十六进制代码为nnnn的字符
\cN ASCII控制字符。比如\cC代表Ctrl+C
\A 字符串开头(类似^,但不受处理多行选项的影响)
\Z 字符串结尾或行尾(不受处理多行选项的影响)
\z 字符串结尾(类似$,但不受处理多行选项的影响)
\G 当前搜索的开头
\p{name} Unicode中命名为name的字符类,例如\p{IsGreek}
(?>exp) 贪婪子表达式
(?-exp) 平衡组
(?im-nsx:exp) 在子表达式exp中改变处理选项
(?im-nsx) 为表达式后面的部分改变处理选项
(?(exp)yes|no) 把exp当作零宽正向先行断言,如果在这个位置能匹配,使用yes作为此组的表达式;否则使用no
(?(exp)yes) 同上,只是使用空表达式作为no
(?(name)yes|no) 如果命名为name的组捕获到了内容,使用yes作为表达式;否则使用no
(?(name)yes) 同上,只是使用空表达式作为no

常用正则表达式示例

使用正则表达式匹配数值范围

由于正则表达式处理文本而不是数字,因此匹配给定范围内的数字需要格外小心。你不能只写[ 0-2 55 ]来匹配 0 到 255 之间的数字。虽然是有效的正则表达式,但它匹配完全不同的东西。[ 0-2 55 ]是具有三个元素的字符类:字符范围 0-2、字符 5 和字符 5(再次)。该字符类匹配单个数字 0、1、2 或 5,就像[ 0125 ]一样。

由于正则表达式适用于文本,因此正则表达式引擎将0视为单个字符,将255视为三个字符。要匹配从 0 到 255 的所有字符,我们需要一个匹配 1 到 3 个字符的正则表达式。

正则表达式[ 0-9 ]匹配 0 到 9 的一位数。[ 1-9 ] [ 0-9 ]匹配 10 到 99 的两位数。这很容易。

匹配三位数字有点复杂,因为我们需要排除数字 256 到 999。1 [ 0-9 ] [ 0-9 ]处理100 到 199。2 [ 0-4 ] [ 0-9 ]匹配 200 到 249。最后,25 [ 0-5 ]添加 250 到 255。

如您所见,您需要将数字范围拆分为具有相同位数的范围,并且每个范围都允许每个数字具有相同的变化。在我们示例中的 3 位范围内,以 1 开头的数字允许后面的两位数字使用所有 10 位数字,而以 2 开头的数字则限制后面允许的数字。

使用交替将所有这些放在一起,我们得到:[ 0-9 ] | [ 1-9 ] [ 0-9 ] | 1 [ 0-9 ] [ 0-9 ] | 2 [ 0-4 ] [ 0-9 ] | 25 [ 0-5 ]。这匹配了我们想要的数字,但有一点需要注意:正则表达式搜索通常允许部分匹配,因此我们的正则表达式将匹配123中的12345。有两种解决方案。

搜索数值范围

如果您在较大的文档或输入字符串中搜索这些数字,请使用单词边界来要求在任何有效匹配之前和之后都需要一个非单词字符(或根本没有字符)。然后正则表达式变为\b ( [ 0-9 ] | [ 1-9 ] [ 0-9 ] | 1 [ 0-9 ] [ 0-9 ] | 2 [ 0-4 ] [ 0-9 ] | 25 [ 0-5 ] ) \b。由于交替运算符的优先级最低,因此需要括号将备选方案组合在一起。这样,正则表达式引擎将尝试匹配第一个单词边界,然后尝试所有备选方案,然后尝试匹配它匹配的数字之后的第二个单词边界。正则表达式引擎将所有字母数字字符以及下划线视为单词字符。

验证数值范围

如果您使用正则表达式验证输入,您可能需要检查整个输入是否包含有效数字。为此,请将单词边界替换为锚点以匹配字符串的开头和结尾:^ ( [ 0-9 ] | [ 1-9 ] [ 0-9 ] | 1 [ 0-9 ] [ 0-9 ] | 2 [ 0-4 ] [ 0-9 ] | 25 [ 0-5 ] )美元

以下是您可能想要匹配的一些更常见的范围:

  • 000..255:^ ([ 01 ] [ 0-9 ] [ 0-9 ] | 2 [ 0-4 ] [ 0-9 ] | 25 [ 0-5 ] )$
  • 0 或 000..255: ^ ( [ 01 ] ? [ 0-9 ] ? [ 0-9 ] | 2 [ 0-4 ] [ 0-9 ] | 25 [ 0-5 ] ) $
  • 0 或 000..127: ^ ( 0 ? [ 0-9 ] ? [ 0-9 ] | 1 [ 01 ] [ 0-9 ] | 12 [ 0-7 ] ) $
  • 0..999:^ ([ 0-9 ] | [ 1-9 ] [ 0-9 ] | [ 1-9 ] [ 0-9 ] [ 0-9 ] )$
  • 000..999: ^ [ 0-9 ] {3} $
  • 0 或 000..999: ^ [ 0-9 ] {1,3} $
  • 1..999:^ ([ 1-9 ] | [ 1-9 ] [ 0-9 ] | [ 1-9 ] [ 0-9 ] [ 0-9 ] )$
  • 001..999: ^ ( 00 [ 1-9 ] | 0 [ 1-9 ] [ 0-9 ] | [ 1-9 ] [ 0-9 ] [ 0-9 ] ) $
  • 1 或 001..999: ^ ( 0 {0,2} [ 1-9 ] | 0 ? [ 1-9 ] [ 0-9 ] | [ 1-9 ] [ 0-9 ] [ 0-9 ] ) $
  • 0 或 00..59: ^ [ 0-5 ] ? [ 0-9 ] $
  • 0 或 000..366: ^ ( [ 012 ] ? [ 0-9 ] ? [ 0-9 ] | 3 [ 0-5 ] [ 0-9 ] | 36 [ 0-6 ] ) $

用正则表达式匹配浮点数

此示例说明如何避免不熟悉正则表达式的人经常犯的常见错误。例如,我们将尝试构建一个可以匹配任何浮点数的正则表达式。我们的正则表达式还应该匹配未给出整数部分的整数和浮点数。我们不会尝试将数字与指数相匹配,例如 1.5e8(科学计数法中的 1.5 亿)。

乍一看,以下正则表达式似乎可以解决问题:[ -+ ] ?[ 0-9 ] * \。? [ 0-9 ] *。这将浮点数定义为可选符号,后跟可选的数字系列(整数部分),后跟可选的点,然后是另一个可选的数字系列(小数部分)。

用单词拼写正则表达式很明显:这个正则表达式中的所有内容都是可选的。此正则表达式将单独的符号或单独的点视为有效的浮点数。事实上,它甚至将空字符串视为有效的浮点数。如果您尝试使用此正则表达式在文件中查找浮点数,您将在字符串中没有出现浮点数的每个位置获得零长度匹配。

不转义也是一个常见的错误。未转义的点匹配任何字符,包括点。如果我们没有转义点,那么4.44X4都将被视为浮点数。

在创建正则表达式时,考虑它不应该匹配的内容比它应该匹配的内容更重要。上面的正则表达式确实匹配正确的浮点数,因为正则表达式引擎是greedy。但它也匹配了许多我们不想要的东西,我们必须排除这些东西。

这是一个更好的尝试:[ -+ ] ? ( [ 0-9 ] * \。[ 0-9 ] + | [ 0-9 ] + )。此正则表达式匹配一个可选符号,该符号后跟零个多个数字,后跟一个点和一个或多个数字(带有可选整数部分的浮点数),或者后跟一个或多个数字(整数) .

这是一个更好的定义。任何匹配项都必须包含至少一位数字。没有办法绕过[ 0 - 9 ] +部分。我们已经成功排除了我们不想要的匹配项:那些没有数字的匹配项。

我们可以将这个正则表达式优化为:[ -+ ] ? [ 0-9 ] * \。? [ 0-9 ] +

如果您还想将数字与指数匹配,您可以使用:[ -+ ] ? [ 0-9 ] * \。? [ 0-9 ] + ([ eE ] [ -+ ] ?[ 0-9 ] + )?. 请注意我是如何通过将整个指数部分组合在一起来使整个指数部分成为可选的,而不是使指数中的每个元素都是可选的。

最后,如果要验证特定字符串是否包含浮点数,而不是在较长的文本中查找浮点数,则必须锚定正则表达式:^ [ -+ ] ? [ 0-9 ] * \。? [ 0-9 ] + $^ [ -+ ] ?[ 0-9 ] * \。? [ 0-9 ] + ( [ eE ] [ -+ ] ? [ 0-9 ] +) ? $ . 您可以在RegexBuddy 的库中找到这些正则表达式的其他变体。

如何查找或验证电子邮件地址

我收到最多反馈的正则表达式,更不用说“bug”报告了,就是你可以在这个网站的主页上找到的那个:\b [ AZ 0-9 ._%+ - ] + @ [ AZ 0 -9 。- ] + \。[ AZ ] {2,} \b。我声称,这个正则表达式匹配任何电子邮件地址。我收到的大多数反馈都通过显示一个与此正则表达式不匹配的电子邮件地址来驳斥这种说法。通常,“错误”报告还包括使正则表达式“完美”的建议。

正如我在下面解释的那样,只有当人们接受我对有效电子邮件地址到底是什么以及不是什么的定义时,我的主张才成立。如果您想使用不同的定义,则必须调整正则表达式。匹配一个有效的电子邮件地址是一个完美的例子,它表明(1)在编写正则表达式之前,您必须确切地知道您要匹配的内容,以及不匹配的内容;(2) 在准确和实用之间经常需要权衡取舍。

我上面的正则表达式的优点是它匹配了当今使用的 99% 的电子邮件地址。它匹配的所有电子邮件地址都可以被 99% 的所有电子邮件软件处理。如果您正在寻找快速解决方案,您只需阅读下一段。如果您想了解所有权衡并获得大量可供选择的替代方案,请继续阅读。

如果要使用上面的正则表达式,有两点需要了解。首先,长正则表达式很难很好地格式化段落。所以我没有将az包含在三个字符类中的任何一个中。此正则表达式旨在与您的正则表达式引擎的“不区分大小写”选项打开时使用。(你会惊讶于我收到了多少“错误”报告。)其次,上面的正则表达式是用单词边界分隔的,这使它适合从文件或更大的文本块中提取电子邮件地址。如果要检查用户是否输入了有效的电子邮件地址,请将单词边界替换为字符串开头和字符串结尾的锚点,如下所示:^ [ AZ 0-9 ._%+- ] + @ [ AZ 0-9 。- ] + \。[ AZ ] {2,} $

上一段也适用于以下所有示例。您可能需要将单词边界更改为字符串开始/结束锚点,反之亦然。而且您必须打开不区分大小写的匹配选项。

如何查找或验证 IP 地址

匹配 IP 地址是在正则表达式复杂性和准确性之间进行权衡的另一个很好的例子。\b \d {1,3} \. \d {1,3} \. \d {1,3} \. \d {1,3} \b将匹配任何 IP 地址就好了。但也会匹配999.999.999.999,就好像它是一个有效的 IP 地址一样。如果您的正则表达式支持 Unicode,它甚至可以匹配١٢٣.१२३.೧೨೩.๑๒๓。这是否是一个问题取决于您打算应用正则表达式的文件或数据。

Stream简介

我们先来看看Java里面是怎么定义Stream的:

A sequence of elements supporting sequential and parallel aggregate operations.

我们来解读一下上面的那句话:

  1. Stream是元素的集合,这点让Stream看起来用些类似Iterator;
  2. 可以支持顺序和并行的对原Stream进行汇聚的操作;

Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

Stream有哪些特点:

  • 元素的序列:与集合一样可以访问里面的元素,集合讲的是数据,而流讲的是操作,比如:filter、map
  • : 流也需要又一个提供数据的源,顺序和生成时的顺序一致
  • 数据的操作:流支持类似于数据库的操作,支持顺序或者并行处理数据;上面的例子用流来实现会更加的简洁
1
2
3
4
5
public List<Integer> getGt5Data() {
return Stream.of(1, 7, 3, 8, 2, 4, 9)
.filter(num -> num > 5)
.collect(toList());
}
  • 流水线操作:很多流的方法本身也会返回一个流,这样可以把多个操作连接起来,形成流水线操作

  • 内部迭代:与以往的迭代不同,流使用的内部迭代,用户只需要专注于数据处理

  • 只能遍历一次: 遍历完成之后我们的流就已经消费完了,再次遍历的话会抛出异常


使用Stream

Java8中的Stream定义了很多方法,基本可以把他们分为两类:中间操作、终端操作;要使用一个流一般都需要三个操作:

  1. 定义一个数据源
  2. 定义中间操作形成流水线
  3. 定义终端操作,执行流水线,生成计算结果

操作符

什么是操作符呢?操作符就是对数据进行的一种处理工作,一道加工程序;就好像工厂的工人对流水线上的产品进行一道加工程序一样。

Stream的操作符大体上分为两种:中间操作符终止操作符

中间操作符

对于数据流来说,中间操作符在执行制定处理程序后,数据流依然可以传递给下一级的操作符。

中间操作符包含8种(排除了parallel,sequential,这两个操作并不涉及到对数据流的加工操作):

  1. map(mapToInt,mapToLong,mapToDouble) 转换操作符,把比如A->B,这里默认提供了转int,long,double的操作符。
  2. flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 变成 2,3,4 也就是从原来的一个数据变成了3个数据,这里默认提供了拍平成int,long,double的操作符。
  3. limit限流操作,比如数据流中有10个 我只要出前3个就可以使用。
  4. distint去重操作,对重复元素去重,底层使用了equals方法。
  5. filter过滤操作,把不想要的数据过滤。
  6. peek挑出操作,如果想对数据进行某些操作,如:读取、编辑修改等。
  7. skip跳过操作,跳过某些元素。
  8. sorted(unordered)排序操作,对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。

终止操作符

数据经过中间加工操作,就轮到终止操作符上场了;终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。

  1. collect收集操作,将所有数据收集起来,这个操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以说Stream 的核心在于Collectors。
  2. count统计操作,统计最终的数据个数。
  3. findFirstfindAny查找操作,查找第一个、查找任何一个 返回的类型为Optional。
  4. noneMatchallMatchanyMatch匹配操作,数据流中是否存在符合条件的元素 返回值为bool 值。
  5. minmax最值操作,需要自定义比较器,返回数据流中最大最小的值。
  6. reduce 约操作,将整个数据流的值规约为一个值,count、min、max底层就是使用reduce。
  7. forEachforEachOrdered遍历操作,这里就是对最终的数据进行消费了。
  8. toArray数组操作,将数据流的元素转换成数组。

这里只介绍了Stream,并没有涉及到IntStreamLongStreamDoubleStream,这三个流实现了一些特有的操作符,这里不做介绍。


创建流Stream

Stream.of()

通过Stream类中的静态方法 of()

1
2
Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9);
stream.forEach(p -> System.out.println(p));

Stream.of(array)

1
2
Stream<Integer> stream = Stream.of( new Integer[]{1,2,3,4,5,6,7,8,9} );
stream.forEach(p -> System.out.println(p));

List.stream()

通过Collection 系列集合提供的串行流:stream()并行流: paralleStream()

1
2
3
4
5
6
7
8
List<Integer> list = new ArrayList<Integer>();

for(int i = 1; i< 10; i++){
list.add(i);
}

Stream<Integer> stream = list.stream();
stream.forEach(p -> System.out.println(p));

Stream.generate() or Stream.iterate()

使用Stream类的静态方法 iterate 创建无限流
iterate方法:

Stream<T> iterate(final T seed, final UnaryOperator<T> f)

参数 seed 起始值,UnaryOperator 函数式接口 继承Function<T,T> 此时参数类型符合返回值类型一致

1
2
Stream<Integer> stream4 = Stream.iterate(0, (x) -> x + 2);
stream4.limit(5).forEach(System.out::println);

使用Stream类的静态方法 generate创建无限流

generate方法参数为Supplier<T> 供给型接口

1
2
3
4
Stream<Integer> randomNumbers = Stream
.generate(() -> (new Random()).nextInt(100));

randomNumbers.limit(20).forEach(System.out::println);

Stream of String chars or tokens

1
2
IntStream stream = "12345_abcdefg".chars();
stream.forEach(p -> System.out.println(p));

Stream中间操作

中间操作会返回另外一个流,这样可以让多个操作连接起来形成一个流水线的操作,只要不触发终端操作,那么这个中间操作都不会实际执行。

注意点

  • 若只有中间操作,则不会执行
  • 只有终止操作执行后,所有的中间操作一次执行,此时就称为延迟加载或者惰性求值
1
2
3
4
5
6
//取age>30的Stu元素
//若只有中间操作,则不会执行
Stream<Stu> stuStream = stuList.stream().filter((i) -> {
System.out.println("验证是否是延迟加载");
return i.getAge() > 40;
});
此时只有中间操作,无终止操作,无结果,控制台无输出。

在继续之前,让我们先构建一个List字符串。我们将在此列表上构建我们的示例,以便于关联和理解。

1
2
3
4
5
6
7
8
9
List<String> memberNames = new ArrayList<>();
memberNames.add("Amitabh");
memberNames.add("Shekhar");
memberNames.add("Aman");
memberNames.add("Rahul");
memberNames.add("Shahrukh");
memberNames.add("Salman");
memberNames.add("Yana");
memberNames.add("Lokesh");

filter()

filter()方法接受一个Predicate来过滤流的所有元素。此操作是中间操作,它使我们能够对结果调用另一个流操作(例如forEach())。

Lambda表达式写法:

1
2
memberNames.stream().filter((s) -> s.startsWith("A"))
.forEach(System.out::println);

非Lambda表达式写法:

1
2
3
memberNames.stream().filter((s) -> {
return s.startsWith("A");
}).forEach(System.out::println);

程序输出:

1
2
Amitabh
Aman

map()

map()中间操作的流中的每个元素转换成经由给定功能的另一个对象。

以下示例将每个字符串转换为大写字符串。但我们也可以使用map()将对象转换为另一种类型。

1
2
3
memberNames.stream().filter((s) -> s.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);

程序输出:

1
2
AMITABH
AMAN

flatmap()

flatMap()是两步过程,即map() + Flattening。它有助于转换Collection<Collection<T>>Collection<T>

  • flatmap 作用就是将元素拍平拍扁 ,将拍扁的元素重新组成Stream,并将这些Stream 串行合并成一条Stream
1
2
3
4
5
6
7
8
9
10
11
12
flatMap() 示例
List<Integer> list1 = Arrays.asList(1,2,3);
List<Integer> list2 = Arrays.asList(4,5,6);
List<Integer> list3 = Arrays.asList(7,8,9);

List<List<Integer>> listOfLists = Arrays.asList(list1, list2, list3);

List<Integer> listOfAllIntegers = listOfLists.stream()
.flatMap(x -> x.stream())
.collect(Collectors.toList());

System.out.println(listOfAllIntegers); //[1, 2, 3, 4, 5, 6, 7, 8, 9]

sorted()

对Stream的排序通过sorted进行,它比数组的排序更强之处在于你可以首先对Stream进行各类map、filter、limit、skip甚至distinct来减少元素数量后再排序,这能帮助程序明显缩短执行时间。

sorted()方法是返回流的排序视图的中间操作。除非我们传递自定义Comparator ,否则流中的元素按自然顺序排序。

自然排序

1
2
3
4
List<String> stringList = Arrays.asList("aa", "bb", "dd", "cc","哈哈","啊");
//根据String类中Comparable方式进行默认排序,即compare to()方法
stringList.stream()
.sorted().forEach(System.out::println);

程序输出:

1
2
3
4
5
6
aa
bb
cc
dd
哈哈

指定排序

1
sorted(Comparator com)
  • 根据实现Comparator接口的指定方法进行排序
1
2
3
4
5
6
7
8
9
stuList.stream().sorted(
(a,b) ->{
if (a.getAge().equals(b.getAge())){
return a.getName().compareTo(b.getName());
}else{
return a.getAge().compareTo(b.getAge());
}
}
).forEach(System.out::println);

limit()

语法

1
Stream<T> limit(long maxSize)

这里maxSize应该限制流的元素数量;并且方法返回值是一个新的,Stream由从原始流中挑选的元素组成。

示例 1:Java 程序从无限的偶数流中获取前 10 个偶数

1
2
3
4
5
Stream<Integer> evenNumInfiniteStream = Stream.iterate(0, n -> n + 2);

List<Integer> newList = evenNumInfiniteStream.limit(10)
.collect(Collectors.toList());
System.out.println(newList);

程序输出。

1
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

skip()

1
2
方法语法
Stream<T> skip(long n)

返回一个跳过前n个元素的流,若流中元素不足n个,则返回一个空流

如果为负,该方法可能会抛出IllegalArgumentExceptionn

示例:从无限的偶数流中跳过前 5 个偶数,然后将接下来的 10 个偶数收集到一个新的流中。

1
2
3
4
5
6
7
Stream<Integer> evenNumInfiniteStream = Stream.iterate(0, n -> n + 2);

List<Integer> newList = evenNumInfiniteStream
.skip(5)
.limit(10)
.collect(Collectors.toList());
System.out.println(newList);

程序输出

1
[10, 12, 14, 16, 18, 20, 22, 24, 26, 28]

distinct()

distinct()去重原理为通过流所生成元素的hashCode()equals()来去除重复元素

字符串或包装类型去重

很容易从一个字符串集合或包装类型集合中找到重复的元素,因为这些类型实现了equals()方法

示例:使用 Stream 遍历所有String元素并使用终端操作将不同的String元素收集到另一个元素中。List Stream.collect()

1
2
3
4
5
6
7
8
9
10
// 查找所有不同的字符串
Collection<String> list = Arrays.asList("A", "B", "C", "D", "A", "B", "C");

// Get collection without duplicate i.e. distinct only
List<String> distinctElements = list.stream()
.distinct()
.collect(Collectors.toList());

//Let's verify distinct elements
System.out.println(distinctElements);

程序输出:

1
[A, B, C, D]

重写equals() 方法和hashCode() 方法的复杂类型去重

让我们为我们的示例创建一个 Person 类。它有三个领域:idfnamelname。如果两个人相同,则他们id是相等的。

不要忘记覆盖该类型的equals()hashCode()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class Person {
private Integer id;
private String fname;
private String lname;

public Person(Integer id, String fname, String lname) {
super();
this.id = id;
this.fname = fname;
this.lname = lname;
}

public Integer getId() {
return id;
}

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

public String getFname() {
return fname;
}

public void setFname(String fname) {
this.fname = fname;
}

public String getLname() {
return lname;
}

public void setLname(String lname) {
this.lname = lname;
}

@Override
public int hashCode() {
return Objects.hash(id);
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
return Objects.equals(id, other.id);
}

@Override
public String toString() {
return "Person [id=" + id + ", fname=" + fname + ", lname=" + lname + "]";
}
}

我们测试一下代码。我们创建List. 然后我们将使用该Stream.distinct()方法查找具有唯一性的 Person 类的所有实例id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Java 程序通过 id 查找不同的人
Person lokeshOne = new Person(1, "Lokesh", "Gupta");
Person lokeshTwo = new Person(1, "Lokesh", "Gupta");
Person lokeshThree = new Person(1, "Lokesh", "Gupta");
Person brianOne = new Person(2, "Brian", "Clooney");
Person brianTwo = new Person(2, "Brian", "Clooney");
Person alex = new Person(3, "Alex", "Kolen");

//Add some random persons
Collection<Person> list = Arrays.asList(alex, brianOne, brianTwo, lokeshOne, lokeshTwo, lokeshThree);

// Get distinct people by id
List<Person> distinctElements = list.stream()
.distinct()
.collect( Collectors.toList() );

// Let's verify distinct people
System.out.println( distinctElements );
1
2
3
4
5
6
输出
[
Person [id=1, fname=Lokesh, lname=Gupta],
Person [id=2, fname=Brian, lname=Clooney],
Person [id=3, fname=Alex, lname=Kolen]
]

自定义的复杂类型去重

有时,我们希望根据自定义逻辑找到不同的项目。

例如,我们可能需要找到所有可能有不同id但全名相同的人。在这种情况下,我们必须根据Personfnamelname字段检查相等性。

Java 没有任何用于在使用提供的用户函数比较对象时查找不同对象的本机 API。因此,我们将创建自己的实用程序函数,然后使用它。

distinctByKey()

distinctByKey()函数使用一个ConcurrentHashMap实例来确定是否存在具有相同值的现有键——其中键是从函数引用中获取的。

此函数的参数是一个lambda 表达式,用于生成映射键以进行比较。

1
2
3
4
5
按类字段查找不同的实用函数public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) 
{
Map<Object, Boolean> map = new ConcurrentHashMap<>();
return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

我们可以将任何字段 getter 方法作为方法参数传递,这将导致字段值充当映射的键。

示例:

检查我们distinctByKey(p -> p.getFname() + " " + p.getLname())filter()方法中的使用方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java 程序按姓名查找不同的人Person lokeshOne = new Person(1, "Lokesh", "Gupta");
Person lokeshTwo = new Person(2, "Lokesh", "Gupta");
Person lokeshThree = new Person(3, "Lokesh", "Gupta");
Person brianOne = new Person(4, "Brian", "Clooney");
Person brianTwo = new Person(5, "Brian", "Clooney");
Person alex = new Person(6, "Alex", "Kolen");

//Add some random persons
Collection<Person> list = Arrays.asList(alex, brianOne, brianTwo, lokeshOne, lokeshTwo, lokeshThree);

// Get distinct objects by key
List<Person> distinctElements = list.stream()
.filter( distinctByKey(p -> p.getFname() + " " + p.getLname()) )
.collect( Collectors.toList() );

// Again verify distinct people
System.out.println( distinctElements );

程序输出:

1
2
3
4
5
输出[
Person [id=1, fname=Lokesh, lname=Gupta],
Person [id=4, fname=Brian, lname=Clooney],
Person [id=6, fname=Alex, lname=Kolen]
]

peek()

peek:如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,没有返回值。

peek()存在主要是为了支持调试

我们看下debug用途的使用:

1
2
3
4
5
Stream.of("one", "two", "three","four").filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());

上面的例子输出:

1
2
3
4
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR

上面的例子我们输出了stream的中间值,方便我们的调试。

为什么只作为debug使用呢?我们再看一个例子:

1
Stream.of("one", "two", "three","four").peek(u -> u.toUpperCase()).forEach(System.out::println);

上面的例子我们使用peek将element转换成为upper case。然后输出:

1
2
3
4
one
two
three
four

可以看到stream中的元素并没有被转换成大写格式。

再看一个map的对比:

1
Stream.of("one", "two", "three","four").map(u -> u.toUpperCase()).forEach(System.out::println);

输出:

1
2
3
4
ONE
TWO
THREE
FOUR

可以看到map是真正的对元素进行了转换。


Stream终止操作

foreach()

Stream forEach(Consumer action)对流的每个元素执行一个操作。Stream forEach(Consumer action) 是一个终端操作,即它可以遍历流以产生结果或副作用。

1
memberNames.forEach(System.out::println);

方法语法

forEach()方法的语法如下:

1
void forEach(Consumer<? super T> action)

Consumer是一个功能接口action表示要对 Stream 中的每个元素执行的非干扰操作

特点:

  • forEach()方法是终端操作。这意味着它不再返回Stream
  • 执行forEach()后,认为流管道已消耗,不能再使用 Stream。
  • 如果我们需要再次遍历相同的数据源(支持 Stream 的集合),我们必须返回数据源以获取新的流。
  • 对于并行流,该forEach()操作不保证流中元素的顺序,因为这样做会牺牲并行性的好处。
  • 使用 Lambda 运算符:在stream().forEach() 中,使用了 lambda,因此不允许对循环外的变量进行操作。只能对相关集合进行操作。
1
2
3
4
5
6
7
8
9
10
11
12
List<String> arr1 = new ArrayList<String>();
int count = 0;
arr1.add("Geeks");
arr1.add("For");
arr1.add("Geeks");
arr1.stream().forEach(s -> {
// this will cause an error
count++;

// print all elements
System.out.print(s);
});

示例:

1
list.stream().forEach( System.out::println );

forEachOrdered()

同forEach()

  • forEachOrdered 适用用于并行流的情况下进行迭代,能保证迭代的有序性

示例:

1
2
3
List<Integer> list = Arrays.asList(2, 4, 6, 8, 10);
list.stream().parallel().forEach( System.out::println ); //1
list.stream().parallel().forEachOrdered( System.out::println ); //2

In the above example, both statements guarantee that output will be 2, 4, 6, 8, 10.

1
2
3
4
5
6
7
8
9
10
11
12
//forEach()
6
10
8
4
2
//forEachOrdered()
2
4
6
8
10

toArray()

toArray()方法返回一个包含给定流元素的数组。这是终端操作

示例 :将字符串流转换为字符串数组

1
2
3
4
5
6
将字符串流转换为数组
Stream<String> tokenStream = Arrays.asList("A", "B", "C", "D").stream(); //stream

String[] tokenArray = tokenStream.toArray(String[]::new); //array

System.out.println(Arrays.toString(tokenArray));

程序输出

1
[A, B, C, D]

reduce()

很多时候,我们需要执行将流归约为单个结果值的操作,例如,最大值、最小值、总和、乘积等。reduce是组合所有元素的重复过程。

reduce操作将二元运算符应用于流中的每个元素,其中运算符的第一个参数是前一个reduce操作的返回值,第二个参数是当前流元素。

Optional<T> reduce(BinaryOperator<T> accumulator):第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。
示例 1:获取最长的字符串

1
2
3
4
List<String> words = Arrays.asList("GFG", "Geeks", "for","GeeksQuiz", "GeeksforGeeks");
Optional<String> longestString = words.stream().reduce((word1, word2) -> word1.length() > word2.length() ? word1 : word2);

longestString.ifPresent(System.out::println);

示例 3:求和

1
2
3
List<Integer> array = Arrays.asList(-2, 0, 4, 6, 8);
int sum = array.stream().reduce(0,(element1, element2) -> element1 + element2);
System.out.println("The sum of all elements is " + sum);

collect()

collect() 流式传输到 List

collect()方法用于从stream接收元素并将它们存储在集合中。

1
2
3
4
5
List<String> memNamesInUppercase = memberNames.stream().sorted()
.map(String::toUpperCase)
.collect(Collectors.toList());

System.out.print(memNamesInUppercase);

程序输出:

1
[AMAN, AMITABH, LOKESH, RAHUL, SALMAN, SHAHRUKH, SHEKHAR, YANA]

collect() 流式传输到 Set

我们可以使用Collectors.toSet()将流元素收集到一个新的 Set 中。

1
2
3
4
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

Set<Integer> oddNumbers = numbers.parallelStream().filter(x -> x % 2 != 0).collect(Collectors.toSet());
System.out.println(oddNumbers); // [1, 3, 5]

collect() 流式传输到 Map

我们可以使用Collectors.toMap()函数将流元素收集到Map。此方法接受映射键的两个参数和 Map 中的相应值。

1
2
3
4
5
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

Map<Integer, String> mapOddNumbers = numbers.parallelStream().filter(x -> x % 2 != 0)
.collect(Collectors.toMap(Function.identity(), x -> String.valueOf(x)));
System.out.println(mapOddNumbers); // {1=1, 3=3, 5=5}

Collectors joining()

我们可以使用 Collectors join() 方法来获取一个 Collector,它将输入流 CharSequence 元素按照遇到的顺序连接起来。我们可以使用它来连接字符串流、StringBuffer或 StringBuilder。

1
2
3
4
5
6
7
8
9
10
11
String value = Stream.of("a", "b", "c").collect(Collectors.joining());
// "abc"

String valueCSV = Stream.of("a", "b", "c").collect(Collectors.joining(","));
// "a,b,c"

String valueCSVLikeArray = Stream.of("a", "b", "c").collect(Collectors.joining(",", "{", "}"));
// "{a,b,c}"

String valueObject = Stream.of("1", new StringBuffer("2"), new StringBuilder("3")).collect(Collectors.joining());
// "123"

Collectors类中的常用方法

  • counting-统计数量
  • averagingDouble-求平均值并转换成Double类型
  • averagingInt-求平均值并转换成Int类型
  • averagingLong-求平均值并转换成Long类型
  • summingDouble-求和并转换成Double类型
  • summingInt-求和并转换成Int类型
  • summingLong-求和并转换成Long类型
  • maxBy-根据函数条件求最大值
  • groupingBy-分组
  • partitioningBy-分区
  • joining-连接字符串

示例 求年龄的平均值

1
2
3
Double ageAve = stuList.stream()
.collect(Collectors.averagingDouble(Stu::getAge));
System.out.println(ageAve);//37.0

求年龄之和

1
2
3
Double ageSum = stuList.stream()
.collect(Collectors.summingDouble(Stu::getAge));
System.out.println(ageSum);//296.0

根据年龄找出最大年龄值的stu对象

1
2
3
4
5
//根据年龄找出最大年龄值的stu对象
Optional<Stu> stuOptional = stuList.stream()
.collect(Collectors.maxBy((a, b) -> Double.compare(a.getAge(), b.getAge())));
System.out.println(stuOptional.get());//Stu{id=5, name='dd', age=52}

min()

返回流中的最小值

  • 这是终端操作。所以这个方法执行后就不能使用stream了。

  • 根据提供的Comparator回此流的最小元素。

  • 这是流缩减的一个特例,缩减到只有一个元素。

  • 该方法返回一个描述此流的最小元素的Optional,Optional如果流为空,则返回一个空。

示例:查找最小数字

1
2
3
4
5
List<Integer> list = Arrays.asList(2, 4, 1, 3, 7, 5, 9, 6, 8);

Optional<Integer> minNumber = list.stream().min((i, j) -> i.compareTo(j));

System.out.println(minNumber.get());

程序输出。

1
1

max()

返回流中的最大值

  • 这是终端操作。所以这个方法执行后就不能使用stream了。

  • 根据提供的Comparator回此流的最大元素。

  • 这是流缩减的一个特例,缩减到只有一个元素。

  • 该方法返回一个描述此流的最大元素的Optional,Optional如果流为空,则返回一个空。

示例:查找最大数字

1
2
3
4
5
List<Integer> list = Arrays.asList(2, 4, 1, 3, 7, 5, 9, 6, 8);

Optional<Integer> maxNumber = list.stream().max((i, j) -> i.compareTo(j));

System.out.println(maxNumber.get());

程序输出。

1
9

count()

返回流中元素的总个数

示例 1:计算 List 中元素个数

1
2
long count = Stream.of("how","to","do","in","java").count();
System.out.printf("There are %d words in the stream %n", count);

示例 2:使用 Collectors.counting() 计算元素数量

1
2
long count = Stream.of("how","to","do","in","java").collect(Collectors.counting());
System.out.printf("There are %d words in the stream %n", count);

anyMatch()

接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false

  • 这是一个短路终端操作。
  • 它返回此流的任何元素是否与提供的谓词匹配。
  • 如果不需要确定结果,则可能不会评估所有元素的谓词。true一旦遇到第一个匹配元素,方法就会返回。
  • 如果流为空,则false返回并且不评估谓词。
  • allMatch() 和 anyMatch() 之间区别在于,如果流中的任何元素与给定的谓词匹配,则anyMatch()返回true。使用 时allMatch(),所有元素必须匹配给定的谓词。

示例 1:检查流是否包含特定元素

1
2
3
Stream<String> stream = Stream.of("one", "two", "three", "four");
boolean match = stream.anyMatch(s -> s.contains("four"));
System.out.println(match); //true

anyMatch() 与 contains() 的区别

对于集合List来说,anyMatch()和contains()没有区别

anyMatch()在某些需要检查流中是否至少有一个元素的情况下,该方法很有用。

较短的版本list.contains()也做同样的事情,可以代替使用。

allMatch()

allMatch-检查是否匹配所有元素

示例 1:Stream.allMatch()检查所有流元素是否不包含任何数字字符

1
2
3
4
Stream<String> stream = Stream.of("one", "two", "three", "four");
Predicate<String> containsDigit = s -> s.contains("\\d+") == false;
boolean match = stream.allMatch(containsDigit);
System.out.println(match); //true

noneMatch()

句法

1
boolean noneMatch(Predicate<? super T> predicate)

noneMatch()回报:

  • true – 如果流中没有元素匹配给定的谓词,或者流为空。

  • false – 如果至少一个元素与给定的谓词匹配。

  • 它与方法allMatch() 完全相反。

示例 1:Stream.noneMatch()检查 Stream 中的元素是否包含任何数字/数字字符

1
2
3
4
5
Stream<String> stream = Stream.of("one", "two", "three", "four");

boolean match = stream.noneMatch( s -> s.contains("\\d+") );

System.out.println(match); //true

findFirst()

1
Optional<T> findFirst()

此方法返回一个Optional描述此流第一个匹配元素

1
2
3
Optional<Customer> optional = allCustomers.stream()
.filter(customer -> customer.getAge() > 20)
.findFirst();

findAny()

findAny-返回当前流中的任意一个元素

1
2
3
4
5
6
7
//从集合中随便找个age>30的Stu对象  可以使用串行流stream,也可以使用parallelStream 并行流
Optional<Stu> any = stuList.parallelStream()
.filter((e) -> e.getAge() > 30).findAny();
System.out.println(any.get());//Stu{id=4, name='cc', age=42}
Optional<Stu> any1 = stuList.stream()
.filter((e) -> e.getAge() > 30).findAny();
System.out.println(any1.get());//Stu{id=3, name='bb', age=32}

findFirst() 与 findAny()

在非并行流中,大多数情况下两者都可能返回流的第一个元素,但不提供此行为的任何保证。findAny()

用于findAny()在更快的时间内从任何并行流中获取任何元素。否则,我们总是可以findFirst()在大多数情况下使用。


Stream使用示例

示例1:查找流的最后一个元素

Stream.reduce() API

reduce()方法对流的元素使用归约技术。在这种情况下,它将选择流的两个元素并选择后者。这将一直持续到所有元素都用尽为止。

在归约过程结束时,我们将得到流的最后一个元素。

1
2
3
4
5
6
7
// 使用 reduce() 方法获取最后一个元素
Stream<Integer> stream = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9).stream();

Integer lastElement = stream.reduce((first, second) -> second)
.orElse(-1);

System.out.println(lastElement); // Prints 9

如果流本身是空的,则lastElement为-1。

示例2:查找和计算重复项并删除这些重复项

Stream.distinct() - 删除重复项

删除重复的字符串

distinct()方法根据对象的equals()方法检查对象是否相等

1
2
3
4
5
List<String> list = Arrays.asList("A", "B", "C", "D", "A", "B", "C");

List<String> distinctItems = list.stream().distinct().collect(Collectors.toList());

System.out.println(distinctItems); // [A, B, C, D]

删除重复的自定义对象

可以使用相同的语法从List 中删除重复的对象。为此,我们需要非常小心对象的equals()hashCode()方法,因为它将决定对象是重复的还是唯一的。

考虑下面的例子,如果两个Person实例具有相同的id值,则它们被认为是相等的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Person 
{
public Person(Integer id, String fname, String lname) {
super();
this.id = id;
this.fname = fname;
this.lname = lname;
}

private Integer id;
private String fname;
private String lname;

//Getters and Setters are hidden for brevity

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
return Objects.equals(id, other.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}

@Override
public String toString() {
return "Person [id=" + id + ", fname=" + fname + ", lname=" + lname + "]";
}
}

删除重复的Person对象

1
2
3
4
5
6
7
8
9
10
Person p1 = new Person(1,"zhang","san");
Person p2 = new Person(2,"li","si");
Person p3 = new Person(3,"wang","wu");
Person p4 = new Person(1,"zhang","san");
Person p5 = new Person(2,"li","si");
Collection<Person> list = Arrays.asList(p1, p2, p3, p4, p5);

List<Person> distinctElements = list.stream().distinct().collect( Collectors.toList() );
System.out.println(distinctElements);
//[Person [id=1, fname=zhang, lname=san], Person [id=2, fname=li, lname=si], Person [id=3, fname=wang, lname=wu]]

Collectors.toSet() - 删除重复项

另一种简单且非常有用的方法是将所有元素存储在Set. 根据定义,集合仅存储不同的元素。请注意,Set通过使用*equals()*方法比较对象来存储不同的项目。

1
2
3
4
5
6
ArrayList<Integer> numbersList
= new ArrayList<>(Arrays.asList(1, 1, 2, 3, 3, 3, 4, 5, 6, 6, 6, 7, 8));

Set<Integer> setWithoutDuplicates = numbersList.stream().collect(Collectors.toSet());

System.out.println(setWithoutDuplicates);

程序输出:

1
[1, 2, 3, 4, 5, 6, 7, 8]

Collectors.toMap() - 计算重复项

有时,我们有兴趣找出哪些元素是重复的,以及它们在原始列表中出现的次数。我们可以使用 一个Map来存储这些信息。

我们必须遍历列表,将元素作为 Map 键,并将其所有出现在 Map 值中。

1
2
3
4
5
6
7
8
// ArrayList with duplicate elements
ArrayList<Integer> numbersList
= new ArrayList<>(Arrays.asList(1, 1, 2, 3, 3, 3, 4, 5, 6, 6, 6, 7, 8));

Map<Integer, Long> elementCountMap = numbersList.stream()
.collect(Collectors.toMap(Function.identity(), v -> 1L, Long::sum));

System.out.println(elementCountMap);

程序输出:

1
{1=2, 2=1, 3=3, 4=1, 5=1, 6=3, 7=1, 8=1}

去重

1
2
3
4
5
6
7
8
9
10
11
12
Person p1 = new Person(1,"zhang","san");
Person p2 = new Person(2,"li","si");
Person p3 = new Person(3,"wang","wu");
Person p4 = new Person(1,"zhang","san");
Person p5 = new Person(2,"li","si");
Collection<Person> list = Arrays.asList(p1, p2, p3, p4, p5);

// key值重复时取第一个
// map: key:Person::getId,value:person -> person,重复key处理(oldPerson, newPerson) -> oldPerson
Map<Integer, Person> distinctElements = list.stream().distinct().collect(Collectors.toMap(Person::getId, person -> person, (oldPerson, newPerson) -> oldPerson));
System.out.println(distinctElements);
// {1=Person [id=1, fname=zhang, lname=san], 2=Person [id=2, fname=li, lname=si], 3=Person [id=3, fname=wang, lname=wu]}

示例3: 图解Stream处理过程

1
2
3
4
5
6
List<Integer> transactionsIds = 
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.sorted(comparing(Transaction::getValue).reversed())
.map(Transaction::getId)
.collect(toList());

视图

常见的数据库对象

对象 描述
表(TABLE) 表是存储数据的逻辑单元,以行和列的形式存在,列就是字段,行就是记录
数据字典 就是系统表,存放数据库相关信息的表。系统表的数据通常由数据库系统维护,程序员通常不应该修改,只可查看
约束(CONSTRAINT) 执行数据校验的规则,用于保证数据完整性的规则
视图(VIEW) 一个或者多个数据表里的数据的逻辑显示,视图并不存储数据
索引(INDEX) 用于提高查询性能,相当于书的目录
存储过程(PROCEDURE) 用于完成一次完整的业务处理,没有返回值,但可通过传出参数将多个值传给调用环境
存储函数(FUNCTION) 用于完成一次特定的计算,具有一个返回值
触发器(TRIGGER) 相当于一个事件监听器,当数据库发生特定事件后,触发器被触发,完成相应的处理

视图概述

视图是从一个或几个基本表(或视图)导出的表。他与基本表不同,是一个虚表。数据库只存放视图的定义,而不存放视图对应的数据,这些数据仍存放在原来的基本表中。以基本表的数据发生变化,从视图中查询出的数据也随之改变了,从这个意义上讲,视图就像一个窗口。透过他可以看到数据库中自己感兴趣的数据及变化。

视图的特点

  • 视图是一种虚拟表,本身是不具有数据的,占用很少的内存空间,它是 SQL 中的一个重要概念。

  • 视图的创建和删除只影响视图本身,不影响对应的基表。但是当对视图中的数据进行增加、删除和修改操作时,数据表中的数据会相应地发生变化,反之亦然。

  • 向视图提供数据内容的语句为 SELECT 语句, 可以将视图理解为存储起来的SELECT 语句

    • 在数据库中,视图不会保存数据,数据真正保存在数据表中。当对视图中的数据进行增加、删除和修改操作时,数据表中的数据会相应地发生变化;反之亦然。
  • 视图,是向用户提供基表数据的另一种表现形式。通常情况下,小型项目的数据库可以不使用视图,但是在大型项目中,以及数据表比较复杂的情况下,视图的价值就凸显出来了,它可以帮助我们把经常查询的结果集放到虚拟表中,提升使用效率。理解和使用起来都非常方便。

创建视图

1
2
3
CREATE VIEW 视图名称 [(字段列表)]
AS 查询语句
[WITH [CASCADED|LOCAL] CHECK OPTION]

创建单表视图

举例:

1
2
3
4
5
CREATE VIEW empvu80
AS
SELECT employee_id, last_name, salary
FROM employees
WHERE department_id = 80;

查询视图:

1
2
SELECT *
FROM salvu80;

创建多表联合视图

举例:

1
2
3
4
5
CREATE VIEW emp_dept
AS
SELECT ename,dname
FROM t_employee LEFT JOIN t_department
ON t_employee.did = t_department.did;
  • 利用视图对数据进行格式化

我们经常需要输出某个格式的内容,比如我们想输出员工姓名和对应的部门名,对应格式为 emp_name(department_name),就可以使用视图来完成数据格式化的操作:

1
2
3
4
5
CREATE VIEW emp_depart
AS
SELECT CONCAT(last_name,'(',department_name,')') AS emp_dept
FROM employees e JOIN departments d
WHERE e.department_id = d.department_id

查看视图

语法1:查看数据库的表对象、视图对象

1
SHOW TABLES;

语法2:查看视图的结构

1
DESC / DESCRIBE 视图名称;

语法3:查看视图的属性信息

1
2
# 查看视图信息(显示数据表的存储引擎、版本、数据行数和数据大小等)
SHOW TABLE STATUS LIKE '视图名称'\G

执行结果显示,注释Comment为VIEW,说明该表为视图,其他的信息为NULL,说明这是一个虚表。

语法4:查看视图的详细定义信息

1
SHOW CREATE VIEW 视图名称;

更新视图的数据

一般情况

MySQL支持使用INSERTUPDATEDELETE语句对视图中的数据进行插入、更新和删除操作。对视图的操作转换为对基本表的更新操作。

举例:UPDATE操作

1
2
3
UPDATE IS_Student
SET Sname = '刘晨'
WHERE Sno = '202210101'

转换后的更新语句为:

1
2
3
UPDATE Student
SET Sname = '刘晨'
WHERE Sno = '202210101' AND Sdept = 'IS'

不可更新的视图

要使视图可更新,视图中的行和底层基本表中的行之间必须存在一对一的关系。另外当视图定义出现如下情况时,视图不支持更新操作:

  • 在定义视图的时候指定了“ALGORITHM = TEMPTABLE”,视图将不支持INSERT和DELETE操作;
  • 视图中不包含基表中所有被定义为非空又未指定默认值的列,视图将不支持INSERT操作;
  • 在定义视图的SELECT语句中使用了JOIN联合查询,视图将不支持INSERT和DELETE操作;
  • 在定义视图的SELECT语句后的字段列表中使用了数学表达式子查询,视图将不支持INSERT,也不支持UPDATE使用了数学表达式、子查询的字段值;
  • 在定义视图的SELECT语句后的字段列表中使用DISTINCT聚合函数GROUP BYHAVINGUNION等,视图将不支持INSERT、UPDATE、DELETE;
  • 在定义视图的SELECT语句中包含了子查询,而子查询中引用了FROM后面的表,视图将不支持INSERT、UPDATE、DELETE;
  • 视图定义基于一个不可更新视图
  • 常量视图。

虽然可以更新视图数据,但总的来说,视图作为虚拟表,主要用于方便查询,不建议更新视图的数据。对视图数据的更改,都是通过对实际数据表里数据的操作来完成的。

修改、删除视图

修改视图

方式1:使用CREATE OR REPLACE VIEW 子句修改视图

1
2
3
4
5
6
CREATE OR REPLACE VIEW empvu80
(id_number, name, sal, department_id)
AS
SELECT employee_id, first_name || ' ' || last_name, salary, department_id
FROM employees
WHERE department_id = 80;

说明:CREATE VIEW 子句中各列的别名应和子查询中各列相对应。

方式2:ALTER VIEW

修改视图的语法是:

1
2
3
ALTER VIEW 视图名称 
AS
查询语句

删除视图

  • 删除视图只是删除视图的定义,并不会删除基表的数据。

  • 删除视图的语法是:

    1
    DROP VIEW IF EXISTS 视图名称;
    1
    DROP VIEW IF EXISTS 视图名称1,视图名称2,视图名称3,...;
  • 举例:

    1
    DROP VIEW empvu80;
  • 说明:基于视图a、b创建了新的视图c,如果将视图a或者视图b删除,会导致视图c的查询失败。这样的视图c需要手动删除或修改,否则影响使用。


存储过程与存储函数

存储过程概述

含义:存储过程的英文是 Stored Procedure。它的思想很简单,就是一组经过预先编译的 SQL 语句的封装。

执行过程:存储过程预先存储在 MySQL 服务器上,需要执行的时候,客户端只需要向服务器端发出调用存储过程的命令,服务器端就可以把预先存储好的这一系列 SQL 语句全部执行。

好处

1、简化操作,提高了sql语句的重用性,减少了开发程序员的压力
2、减少操作过程中的失误,提高效率
3、减少网络传输量(客户端不需要把所有的 SQL 语句通过网络发给服务器)
4、减少了 SQL 语句暴露在网上的风险,也提高了数据查询的安全性

和视图、函数的对比

它和视图有着同样的优点,清晰、安全,还可以减少网络传输量。不过它和视图不同,视图是虚拟表,通常不对底层数据表直接操作,而存储过程是程序化的 SQL,可以直接操作底层数据表,相比于面向集合的操作方式,能够实现一些更复杂的数据处理。

一旦存储过程被创建出来,使用它就像使用函数一样简单,我们直接通过调用存储过程名即可。相较于函数,存储过程是没有返回值的

分类

存储过程的参数类型可以是IN、OUT和INOUT。根据这点分类如下:

1、没有参数(无参数无返回)
2、仅仅带 IN 类型(有参数无返回)
3、仅仅带 OUT 类型(无参数有返回)
4、既带 IN 又带 OUT(有参数有返回)
5、带 INOUT(有参数有返回)

注意:IN、OUT、INOUT 都可以在一个存储过程中带多个。

创建存储过程

语法:

1
2
3
4
5
6
CREATE PROCEDURE 存储过程名(IN|OUT|INOUT 参数名 参数类型,...)
[characteristics ...]
BEGIN
存储过程体

END

说明:

1、参数前面的符号的意思

  • IN:当前参数为输入参数,也就是表示入参;

    存储过程只是读取这个参数的值。如果没有定义参数种类,默认就是 IN,表示输入参数。

  • OUT:当前参数为输出参数,也就是表示出参;

    执行完成之后,调用这个存储过程的客户端或者应用程序就可以读取这个参数返回的值了。

  • INOUT:当前参数既可以为输入参数,也可以为输出参数。

2、形参类型可以是 MySQL数据库中的任意类型。

3、characteristics 表示创建存储过程时指定的对存储过程的约束条件,其取值信息如下:

1
2
3
4
5
LANGUAGE SQL
| [NOT] DETERMINISTIC
| { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
| COMMENT 'string'
  • LANGUAGE SQL:说明存储过程执行体是由SQL语句组成的,当前系统支持的语言为SQL。
  • [NOT] DETERMINISTIC:指明存储过程执行的结果是否确定。DETERMINISTIC表示结果是确定的。每次执行存储过程时,相同的输入会得到相同的输出。NOT DETERMINISTIC表示结果是不确定的,相同的输入可能得到不同的输出。如果没有指定任意一个值,默认为NOT DETERMINISTIC。
  • { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }:指明子程序使用SQL语句的限制。
    • CONTAINS SQL表示当前存储过程的子程序包含SQL语句,但是并不包含读写数据的SQL语句;
    • NO SQL表示当前存储过程的子程序中不包含任何SQL语句;
    • READS SQL DATA表示当前存储过程的子程序中包含读数据的SQL语句;
    • MODIFIES SQL DATA表示当前存储过程的子程序中包含写数据的SQL语句。
    • 默认情况下,系统会指定为CONTAINS SQL。
  • SQL SECURITY { DEFINER | INVOKER }:执行当前存储过程的权限,即指明哪些用户能够执行当前存储过程。
    • DEFINER表示只有当前存储过程的创建者或者定义者才能执行当前存储过程;
    • INVOKER表示拥有当前存储过程的访问权限的用户能够执行当前存储过程。
    • 如果没有设置相关的值,则MySQL默认指定值为DEFINER。
  • COMMENT 'string':注释信息,可以用来描述存储过程。

4、存储过程体中可以有多条 SQL 语句,如果仅仅一条SQL 语句,则可以省略 BEGIN 和 END

编写存储过程并不是一件简单的事情,可能存储过程中需要复杂的 SQL 语句。

1
2
3
4
1. BEGIN…END:BEGIN…END 中间包含了多个语句,每个语句都以(;)号为结束符。
2. DECLARE:DECLARE 用来声明变量,使用的位置在于 BEGIN…END 语句中间,而且需要在其他语句使用之前进行变量的声明。
3. SET:赋值语句,用于对变量进行赋值。
4. SELECT… INTO:把从数据表中查询的结果存放到变量中,也就是为变量赋值。

5、需要设置新的结束标记

1
DELIMITER 新的结束标记

因为MySQL默认的语句结束符号为分号;。为了避免与存储过程中SQL语句结束符相冲突,需要使用DELIMITER改变存储过程的结束符。

比如:“DELIMITER //”语句的作用是将MySQL的结束符设置为//,并以“END //”结束存储过程。存储过程定义完毕之后再使用“DELIMITER ;”恢复默认结束符。DELIMITER也可以指定其他符号作为结束符。

当使用DELIMITER命令时,应该避免使用反斜杠\字符,因为反斜线是MySQL的转义字符。

示例:

1
2
3
4
5
6
7
8
9
DELIMITER $

CREATE PROCEDURE 存储过程名(IN|OUT|INOUT 参数名 参数类型,...)
[characteristics ...]
BEGIN
sql语句1;
sql语句2;

END $

代码举例

举例1:创建存储过程select_all_data(),查看 emps 表的所有数据

1
2
3
4
5
6
7
8
9
DELIMITER $

CREATE PROCEDURE select_all_data()
BEGIN
SELECT * FROM emps;

END $

DELIMITER ;

举例2:创建存储过程avg_employee_salary(),返回所有员工的平均工资

1
2
3
4
5
6
7
8
DELIMITER //

CREATE PROCEDURE avg_employee_salary ()
BEGIN
SELECT AVG(salary) AS avg_salary FROM emps;
END //

DELIMITER ;

举例3:创建存储过程show_max_salary(),用来查看“emps”表的最高薪资值。

1
2
3
4
5
6
7
8
9
10
11
CREATE PROCEDURE show_max_salary()
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT '查看最高薪资'
BEGIN
SELECT MAX(salary) FROM emps;
END //

DELIMITER ;

举例4:创建存储过程show_min_salary(),查看“emps”表的最低薪资值。并将最低薪资通过OUT参数“ms”输出

1
2
3
4
5
6
7
8
DELIMITER //

CREATE PROCEDURE show_min_salary(OUT ms DOUBLE)
BEGIN
SELECT MIN(salary) INTO ms FROM emps;
END //

DELIMITER ;

举例5:创建存储过程show_someone_salary(),查看“emps”表的某个员工的薪资,并用IN参数empname输入员工姓名。

1
2
3
4
5
6
7
8
DELIMITER //

CREATE PROCEDURE show_someone_salary(IN empname VARCHAR(20))
BEGIN
SELECT salary FROM emps WHERE ename = empname;
END //

DELIMITER ;

举例6:创建存储过程show_someone_salary2(),查看“emps”表的某个员工的薪资,并用IN参数empname输入员工姓名,用OUT参数empsalary输出员工薪资。

1
2
3
4
5
6
7
8
DELIMITER //

CREATE PROCEDURE show_someone_salary2(IN empname VARCHAR(20),OUT empsalary DOUBLE)
BEGIN
SELECT salary INTO empsalary FROM emps WHERE ename = empname;
END //

DELIMITER ;

举例7:创建存储过程show_mgr_name(),查询某个员工领导的姓名,并用INOUT参数“empname”输入员工姓名,输出领导的姓名。

1
2
3
4
5
6
7
8
9
DELIMITER //

CREATE PROCEDURE show_mgr_name(INOUT empname VARCHAR(20))
BEGIN
SELECT ename INTO empname FROM emps
WHERE eid = (SELECT MID FROM emps WHERE ename=empname);
END //

DELIMITER ;

调用存储过程

调用格式

存储过程有多种调用方法。存储过程必须使用CALL语句调用,并且存储过程和数据库相关,如果要执行其他数据库中的存储过程,需要指定数据库名称,例如CALL dbname.procname。

1
CALL 存储过程名(实参列表)

格式:

1、调用in模式的参数:

1
CALL sp1('值');

2、调用out模式的参数:

1
2
3
SET @name;
CALL sp1(@name);
SELECT @name;

3、调用inout模式的参数:

1
2
3
SET @name=值;
CALL sp1(@name);
SELECT @name;

代码举例

1
2
3
4
5
6
7
8
9
DELIMITER //

CREATE PROCEDURE CountProc(IN sid INT,OUT num INT)
BEGIN
SELECT COUNT(*) INTO num FROM fruits
WHERE s_id = sid;
END //

DELIMITER ;

调用存储过程:

1
2
mysql> CALL CountProc (101, @num);
Query OK, 1 row affected (0.00 sec)

查看返回结果:

1
mysql> SELECT @num;

该存储过程返回了指定 s_id=101 的水果商提供的水果种类,返回值存储在num变量中,使用SELECT查看,返回结果为3。

存储函数

函数(function)为一命名的存储程序,可带参数,并返回一计算值。函数和过程的结构类似,但必须有一个return子句,用于返回函数值。函数说明要指定函数名、结果值的类型,以及参数类型等。

语法分析

学过的函数:LENGTH、SUBSTR、CONCAT等

语法格式:

1
2
3
4
5
6
7
CREATE FUNCTION 函数名(参数名 参数类型,...) 
RETURNS 返回值类型
[characteristics ...]
BEGIN
函数体 #函数体中肯定有 RETURN 语句

END

说明:

1、参数列表:指定参数为IN、OUT或INOUT只对PROCEDURE是合法的,FUNCTION中总是默认为IN参数。

2、RETURNS type 语句表示函数返回数据的类型;

RETURNS子句只能对FUNCTION做指定,对函数而言这是强制的。它用来指定函数的返回类型,而且函数体必须包含一个RETURN value语句。

3、characteristic 创建函数时指定的对函数的约束。取值与创建存储过程时相同,这里不再赘述。

4、函数体也可以用BEGIN…END来表示SQL代码的开始和结束。如果函数体只有一条语句,也可以省略BEGIN…END。

调用存储函数

在MySQL中,存储函数的使用方法与MySQL内部函数的使用方法是一样的。换言之,用户自己定义的存储函数与MySQL内部函数是一个性质的。区别在于,存储函数是用户自己定义的,而内部函数是MySQL的开发者定义的。

1
SELECT 函数名(实参列表)

代码举例

举例1:

创建存储函数,名称为email_by_name(),参数定义为空,该函数查询Abel的email,并返回,数据类型为字符串型。

1
2
3
4
5
6
7
8
9
10
11
DELIMITER //

CREATE FUNCTION email_by_name()
RETURNS VARCHAR(25)
DETERMINISTIC
CONTAINS SQL
BEGIN
RETURN (SELECT email FROM employees WHERE last_name = 'Abel');
END //

DELIMITER ;

调用:

1
SELECT email_by_name();

举例2:

创建存储函数,名称为email_by_id(),参数传入emp_id,该函数查询emp_id的email,并返回,数据类型为字符串型。

1
2
3
4
5
6
7
8
9
10
11
DELIMITER //

CREATE FUNCTION email_by_id(emp_id INT)
RETURNS VARCHAR(25)
DETERMINISTIC
CONTAINS SQL
BEGIN
RETURN (SELECT email FROM employees WHERE employee_id = emp_id);
END //

DELIMITER ;

调用:

1
2
SET @emp_id = 102;
SELECT email_by_id(102);

举例3:

创建存储函数count_by_id(),参数传入dept_id,该函数查询dept_id部门的员工人数,并返回,数据类型为整型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DELIMITER //

CREATE FUNCTION count_by_id(dept_id INT)
RETURNS INT
LANGUAGE SQL
NOT DETERMINISTIC
READS SQL DATA
SQL SECURITY DEFINER
COMMENT '查询部门平均工资'
BEGIN
RETURN (SELECT COUNT(*) FROM employees WHERE department_id = dept_id);

END //

DELIMITER ;

调用:

1
2
SET @dept_id = 50;
SELECT count_by_id(@dept_id);

注意:

若在创建存储函数中报错“you might want to use the less safe log_bin_trust_function_creators variable”,有两种处理方法:

  • 方式1:加上必要的函数特性“[NOT] DETERMINISTIC”和“{CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA}”
  • 方式2:
1
mysql> SET GLOBAL log_bin_trust_function_creators = 1;

对比存储函数和存储过程

关键字 调用语法 返回值 应用场景
存储过程 PROCEDURE CALL 存储过程() 理解为有0个或多个 一般用于更新
存储函数 FUNCTION SELECT 函数() 只能是一个 一般用于查询结果为一个值并返回时

此外,存储函数可以放在查询语句中使用,存储过程不行。反之,存储过程的功能更加强大,包括能够执行对表的操作(比如创建表,删除表等)和事务操作,这些功能是存储函数不具备的。

存储过程和函数的查看、修改、删除

查看

创建完之后,怎么知道我们创建的存储过程、存储函数是否成功了呢?

MySQL存储了存储过程和函数的状态信息,用户可以使用SHOW STATUS语句或SHOW CREATE语句来查看,也可直接从系统的information_schema数据库中查询。这里介绍3种方法。

1. 使用SHOW CREATE语句查看存储过程和函数的创建信息

基本语法结构如下:

1
SHOW CREATE {PROCEDURE | FUNCTION} 存储过程名或函数名

举例:

1
SHOW CREATE FUNCTION test_db.CountProc \G

2. 使用SHOW STATUS语句查看存储过程和函数的状态信息

基本语法结构如下:

1
SHOW {PROCEDURE | FUNCTION} STATUS [LIKE 'pattern']

这个语句返回子程序的特征,如数据库、名字、类型、创建者及创建和修改日期。

[LIKE ‘pattern’]:匹配存储过程或函数的名称,可以省略。当省略不写时,会列出MySQL数据库中存在的所有存储过程或函数的信息。
举例:SHOW STATUS语句示例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> SHOW PROCEDURE STATUS LIKE 'SELECT%' \G 
*************************** 1. row ***************************
Db: test_db
Name: SelectAllData
Type: PROCEDURE
Definer: root@localhost
Modified: 2021-10-16 15:55:07
Created: 2021-10-16 15:55:07
Security_type: DEFINER
Comment:
character_set_client: utf8mb4
collation_connection: utf8mb4_general_ci
Database Collation: utf8mb4_general_ci
1 row in set (0.00 sec)

3. 从information_schema.Routines表中查看存储过程和函数的信息

MySQL中存储过程和函数的信息存储在information_schema数据库下的Routines表中。可以通过查询该表的记录来查询存储过程和函数的信息。其基本语法形式如下:

1
2
SELECT * FROM information_schema.Routines
WHERE ROUTINE_NAME='存储过程或函数的名' [AND ROUTINE_TYPE = {'PROCEDURE|FUNCTION'}];

说明:如果在MySQL数据库中存在存储过程和函数名称相同的情况,最好指定ROUTINE_TYPE查询条件来指明查询的是存储过程还是函数。

举例:从Routines表中查询名称为CountProc的存储函数的信息,代码如下:

1
2
SELECT * FROM information_schema.Routines
WHERE ROUTINE_NAME='count_by_id' AND ROUTINE_TYPE = 'FUNCTION' \G

修改

修改存储过程或函数,不影响存储过程或函数功能,只是修改相关特性。使用ALTER语句实现。

1
ALTER {PROCEDURE | FUNCTION} 存储过程或函数的名 [characteristic ...]

其中,characteristic指定存储过程或函数的特性,其取值信息与创建存储过程、函数时的取值信息略有不同。

1
2
3
{ CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
| SQL SECURITY { DEFINER | INVOKER }
| COMMENT 'string'
  • CONTAINS SQL,表示子程序包含SQL语句,但不包含读或写数据的语句。
  • NO SQL,表示子程序中不包含SQL语句。
  • READS SQL DATA,表示子程序中包含读数据的语句。
  • MODIFIES SQL DATA,表示子程序中包含写数据的语句。
  • SQL SECURITY { DEFINER | INVOKER },指明谁有权限来执行。
    • DEFINER,表示只有定义者自己才能够执行。
    • INVOKER,表示调用者可以执行。
  • COMMENT 'string',表示注释信息。

修改存储过程使用ALTER PROCEDURE语句,修改存储函数使用ALTER FUNCTION语句。但是,这两个语句的结构是一样的,语句中的所有参数也是一样的。

举例1:

修改存储过程CountProc的定义。将读写权限改为MODIFIES SQL DATA,并指明调用者可以执行,代码如下:

1
2
3
ALTER PROCEDURE CountProc
MODIFIES SQL DATA
SQL SECURITY INVOKER ;

查询修改后的信息:

1
2
3
SELECT specific_name,sql_data_access,security_type
FROM information_schema.`ROUTINES`
WHERE routine_name = 'CountProc' AND routine_type = 'PROCEDURE';

结果显示,存储过程修改成功。从查询的结果可以看出,访问数据的权限(SQL_DATA_ ACCESS)已经变成MODIFIES SQL DATA,安全类型(SECURITY_TYPE)已经变成INVOKER。

举例2:

修改存储函数CountProc的定义。将读写权限改为READS SQL DATA,并加上注释信息“FIND NAME”,代码如下:

1
2
3
ALTER FUNCTION CountProc
READS SQL DATA
COMMENT 'FIND NAME' ;

存储函数修改成功。从查询的结果可以看出,访问数据的权限(SQL_DATA_ACCESS)已经变成READS SQL DATA,函数注释(ROUTINE_COMMENT)已经变成FIND NAME。

删除

删除存储过程和函数,可以使用DROP语句,其语法结构如下:

1
DROP {PROCEDURE | FUNCTION} [IF EXISTS] 存储过程或函数的名

IF EXISTS:如果程序或函数不存储,它可以防止发生错误,产生一个用SHOW WARNINGS查看的警告。

举例:

1
DROP PROCEDURE CountProc;
1
DROP FUNCTION CountProc;

变量

在MySQL数据库的存储过程和函数中,可以使用变量来存储查询或计算的中间结果数据,或者输出最终的结果数据。

在 MySQL 数据库中,变量分为系统变量以及用户自定义变量

系统变量

变量由系统定义,不是用户定义,属于服务器层面。启动MySQL服务,生成MySQL服务实例期间,MySQL将为MySQL服务器内存中的系统变量赋值,这些系统变量定义了当前MySQL服务实例的属性、特征。这些系统变量的值要么是编译MySQL时参数的默认值,要么是配置文件(例如my.ini等)中的参数值。大家可以通过网址 https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html 查看MySQL文档的系统变量。

系统变量分为全局系统变量(需要添加global 关键字)以及会话系统变量(需要添加 session 关键字),有时也把全局系统变量简称为全局变量,有时也把会话系统变量称为local变量。如果不写,默认会话级别。静态变量(在 MySQL 服务实例运行期间它们的值不能使用 set 动态修改)属于特殊的全局系统变量。

每一个MySQL客户机成功连接MySQL服务器后,都会产生与之对应的会话。会话期间,MySQL服务实例会在MySQL服务器内存中生成与该会话对应的会话系统变量,这些会话系统变量的初始值是全局系统变量值的复制。

  • 全局系统变量针对于所有会话(连接)有效,但重启之后失效
  • 会话系统变量仅针对于当前会话(连接)有效。会话期间,当前会话对某个会话系统变量值的修改,不会影响其他会话同一个会话系统变量的值。
  • 会话1对某个全局系统变量值的修改会导致会话2中同一个全局系统变量值的修改。

在MySQL中有些系统变量只能是全局的,例如 max_connections 用于限制服务器的最大连接数;有些系统变量作用域既可以是全局又可以是会话,例如 character_set_client 用于设置客户端的字符集;有些系统变量的作用域只能是当前会话,例如 pseudo_thread_id 用于标记当前会话的 MySQL 连接 ID。

查看系统变量

  • 查看所有或部分系统变量
1
2
3
4
5
6
7
#查看所有全局变量
SHOW GLOBAL VARIABLES;

#查看所有会话变量
SHOW SESSION VARIABLES;

SHOW VARIABLES;
1
2
3
4
5
#查看满足条件的部分系统变量。
SHOW GLOBAL VARIABLES LIKE '%标识符%';

#查看满足条件的部分会话变量
SHOW SESSION VARIABLES LIKE '%标识符%';

举例:

1
SHOW GLOBAL VARIABLES LIKE 'admin_%';
  • 查看指定系统变量

作为 MySQL 编码规范,MySQL 中的系统变量以两个“@”开头,其中“@@global”仅用于标记全局系统变量,“@@session”仅用于标记会话系统变量。“@@”首先标记会话系统变量,如果会话系统变量不存在,则标记全局系统变量。

1
2
3
4
5
6
7
#查看指定的系统变量的值
SELECT @@global.变量名;

#查看指定的会话变量的值
SELECT @@session.变量名;
#或者
SELECT @@变量名;
  • 修改系统变量的值

有些时候,数据库管理员需要修改系统变量的默认值,以便修改当前会话或者MySQL服务实例的属性、特征。具体方法:

方式1:修改MySQL配置文件,继而修改MySQL系统变量的值(该方法需要重启MySQL服务)

方式2:在MySQL服务运行期间,使用“set”命令重新设置系统变量的值

1
2
3
4
5
6
7
8
9
10
11
12
#为某个系统变量赋值
#方式1:
SET @@global.变量名=变量值;
#方式2:
SET GLOBAL 变量名=变量值;


#为某个会话变量赋值
#方式1:
SET @@session.变量名=变量值;
#方式2:
SET SESSION 变量名=变量值;

举例:

1
2
SELECT @@global.autocommit;
SET GLOBAL autocommit=0;
1
2
SELECT @@session.tx_isolation;
SET @@session.tx_isolation='read-uncommitted';
1
2
SET GLOBAL max_connections = 1000;
SELECT @@global.max_connections;

用户变量

用户变量是用户自己定义的,作为 MySQL 编码规范,MySQL 中的用户变量以一个“@”开头。根据作用范围不同,又分为会话用户变量局部变量

  • 会话用户变量:作用域和会话变量一样,只对当前连接会话有效。

  • 局部变量:只在 BEGIN 和 END 语句块中有效。局部变量只能在存储过程和函数中使用。

会话用户变量

  • 变量的定义
1
2
3
4
5
6
7
8
#方式1:“=”或“:=”
SET @用户变量 = 值;
SET @用户变量 := 值;

#方式2:“:=” 或 INTO关键字
SELECT @用户变量 := 表达式 [FROM 等子句];
SELECT 表达式 INTO @用户变量 [FROM 等子句];

  • 查看用户变量的值 (查看、比较、运算等)
1
SELECT @用户变量
  • 举例
1
2
3
SET @a = 1;

SELECT @a;
1
2
3
SELECT @num := COUNT(*) FROM employees;

SELECT @num;
1
2
3
SELECT AVG(salary) INTO @avgsalary FROM employees;

SELECT @avgsalary;
1
SELECT @big;  #查看某个未声明的变量时,将得到NULL值

局部变量

定义:可以使用DECLARE语句定义一个局部变量

作用域:仅仅在定义它的 BEGIN … END 中有效

位置:只能放在 BEGIN … END 中,而且只能放在第一句

1
2
3
4
5
6
7
8
9
10
11
12
13
BEGIN
#声明局部变量
DECLARE 变量名1 变量数据类型 [DEFAULT 变量默认值];
DECLARE 变量名2,变量名3,... 变量数据类型 [DEFAULT 变量默认值];

#为局部变量赋值
SET 变量名1 = 值;
SELECT 值 INTO 变量名2 [FROM 子句];

#查看局部变量的值
SELECT 变量1,变量2,变量3;
END

1.定义变量

1
DECLARE 变量名 类型 [default 值];  # 如果没有DEFAULT子句,初始值为NULL

举例:

1
DECLARE myparam INT DEFAULT 100;

2.变量赋值

方式1:一般用于赋简单的值

1
2
SET 变量名=值;
SET 变量名:=值;

方式2:一般用于赋表中的字段值

1
SELECT 字段名或表达式 INTO 变量名 FROM 表;

3.使用变量(查看、比较、运算等)

1
SELECT 局部变量名;

举例1:声明局部变量,并分别赋值为employees表中employee_id为102的last_name和salary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DELIMITER //

CREATE PROCEDURE set_value()
BEGIN
DECLARE emp_name VARCHAR(25);
DECLARE sal DOUBLE(10,2);

SELECT last_name,salary INTO emp_name,sal
FROM employees
WHERE employee_id = 102;

SELECT emp_name,sal;
END //

DELIMITER ;

举例2:声明两个变量,求和并打印 (分别使用会话用户变量、局部变量的方式实现)

1
2
3
4
5
6
#方式1:使用用户变量
SET @m=1;
SET @n=1;
SET @sum=@m+@n;

SELECT @sum;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#方式2:使用局部变量
DELIMITER //

CREATE PROCEDURE add_value()
BEGIN
#局部变量
DECLARE m INT DEFAULT 1;
DECLARE n INT DEFAULT 3;
DECLARE SUM INT;

SET SUM = m+n;
SELECT SUM;
END //

DELIMITER ;

举例3:创建存储过程“different_salary”查询某员工和他领导的薪资差距,并用IN参数emp_id接收员工id,用OUT参数dif_salary输出薪资差距结果。

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
#声明
DELIMITER //

CREATE PROCEDURE different_salary(IN emp_id INT,OUT dif_salary DOUBLE)
BEGIN
#声明局部变量
DECLARE emp_sal,mgr_sal DOUBLE DEFAULT 0.0;
DECLARE mgr_id INT;

SELECT salary INTO emp_sal FROM employees WHERE employee_id = emp_id;
SELECT manager_id INTO mgr_id FROM employees WHERE employee_id = emp_id;
SELECT salary INTO mgr_sal FROM employees WHERE employee_id = mgr_id;
SET dif_salary = mgr_sal - emp_sal;

END //

DELIMITER ;

#调用
SET @emp_id = 102;
CALL different_salary(@emp_id,@diff_sal);


#查看
SELECT @diff_sal;

对比会话用户变量与局部变量

作用域 定义位置 语法
会话用户变量 当前会话 会话的任何地方,加@符号,不用指定类型
局部变量 定义它的BEGIN END中 BEGIN END的第一句话,一般不用加@,需要指定类型

定义条件与处理程序

定义条件是事先定义程序执行过程中可能遇到的问题,处理程序定义了在遇到问题时应当采取的处理方式,并且保证存储过程或函数在遇到警告或错误时能继续执行。这样可以增强存储程序处理问题的能力,避免程序异常停止运行。

说明:定义条件和处理程序在存储过程、存储函数中都是支持的。

案例分析

案例分析:创建一个名称为“UpdateDataNoCondition”的存储过程。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
DELIMITER //

CREATE PROCEDURE UpdateDataNoCondition()
BEGIN
SET @x = 1;
UPDATE employees SET email = NULL WHERE last_name = 'Abel';
SET @x = 2;
UPDATE employees SET email = 'aabbel' WHERE last_name = 'Abel';
SET @x = 3;
END //

DELIMITER ;

调用存储过程:

1
2
3
4
5
6
7
8
9
10
11
mysql> CALL UpdateDataNoCondition();
ERROR 1048 (23000): Column 'email' cannot be null

mysql> SELECT @x;
+------+
| @x |
+------+
| 1 |
+------+
1 row in set (0.00 sec)

可以看到,此时@x变量的值为1。结合创建存储过程的SQL语句代码可以得出:在存储过程中未定义条件和处理程序,且当存储过程中执行的SQL语句报错时,MySQL数据库会抛出错误,并退出当前SQL逻辑,不再向下继续执行。

定义条件

定义条件就是给MySQL中的错误码命名,这有助于存储的程序代码更清晰。它将一个错误名字指定的错误条件关联起来。这个名字可以随后被用在定义处理程序的DECLARE HANDLER语句中。

定义条件使用DECLARE语句,语法格式如下:

1
DECLARE 错误名称 CONDITION FOR 错误码(或错误条件)

错误码的说明:

  • MySQL_error_codesqlstate_value都可以表示MySQL的错误。
    • MySQL_error_code是数值类型错误代码。
    • sqlstate_value是长度为5的字符串类型错误代码。
  • 例如,在ERROR 1418 (HY000)中,1418是MySQL_error_code,’HY000’是sqlstate_value。
  • 例如,在ERROR 1142(42000)中,1142是MySQL_error_code,’42000’是sqlstate_value。

举例1:定义“Field_Not_Be_NULL”错误名与MySQL中违反非空约束的错误类型是“ERROR 1048 (23000)”对应。

1
2
3
4
5
#使用MySQL_error_code
DECLARE Field_Not_Be_NULL CONDITION FOR 1048;

#使用sqlstate_value
DECLARE Field_Not_Be_NULL CONDITION FOR SQLSTATE '23000';

举例2:定义”ERROR 1148(42000)”错误,名称为command_not_allowed。

1
2
3
4
5
#使用MySQL_error_code
DECLARE command_not_allowed CONDITION FOR 1148;

#使用sqlstate_value
DECLARE command_not_allowed CONDITION FOR SQLSTATE '42000';

定义处理程序

可以为SQL执行过程中发生的某种类型的错误定义特殊的处理程序。定义处理程序时,使用DECLARE语句的语法如下:

1
DECLARE 处理方式 HANDLER FOR 错误类型 处理语句
  • 处理方式:处理方式有3个取值:CONTINUE、EXIT、UNDO。
    • CONTINUE:表示遇到错误不处理,继续执行。
    • EXIT:表示遇到错误马上退出。
    • UNDO:表示遇到错误后撤回之前的操作。MySQL中暂时不支持这样的操作。
  • 错误类型(即条件)可以有如下取值:
    • SQLSTATE '字符串错误码':表示长度为5的sqlstate_value类型的错误代码;
    • MySQL_error_code:匹配数值类型错误代码;
    • 错误名称:表示DECLARE … CONDITION定义的错误条件名称。
    • SQLWARNING:匹配所有以01开头的SQLSTATE错误代码;
    • NOT FOUND:匹配所有以02开头的SQLSTATE错误代码;
    • SQLEXCEPTION:匹配所有没有被SQLWARNING或NOT FOUND捕获的SQLSTATE错误代码;
  • 处理语句:如果出现上述条件之一,则采用对应的处理方式,并执行指定的处理语句。语句可以是像“SET 变量 = 值”这样的简单语句,也可以是使用BEGIN ... END编写的复合语句。

定义处理程序的几种方式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#方法1:捕获sqlstate_value
DECLARE CONTINUE HANDLER FOR SQLSTATE '42S02' SET @info = 'NO_SUCH_TABLE';

#方法2:捕获mysql_error_value
DECLARE CONTINUE HANDLER FOR 1146 SET @info = 'NO_SUCH_TABLE';

#方法3:先定义条件,再调用
DECLARE no_such_table CONDITION FOR 1146;
DECLARE CONTINUE HANDLER FOR NO_SUCH_TABLE SET @info = 'NO_SUCH_TABLE';

#方法4:使用SQLWARNING
DECLARE EXIT HANDLER FOR SQLWARNING SET @info = 'ERROR';

#方法5:使用NOT FOUND
DECLARE EXIT HANDLER FOR NOT FOUND SET @info = 'NO_SUCH_TABLE';

#方法6:使用SQLEXCEPTION
DECLARE EXIT HANDLER FOR SQLEXCEPTION SET @info = 'ERROR';

案例解决

在存储过程中,定义处理程序,捕获sqlstate_value值,当遇到MySQL_error_code值为1048时,执行CONTINUE操作,并且将@proc_value的值设置为-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DELIMITER //

CREATE PROCEDURE UpdateDataNoCondition()
BEGIN
#定义处理程序
DECLARE CONTINUE HANDLER FOR 1048 SET @proc_value = -1;

SET @x = 1;
UPDATE employees SET email = NULL WHERE last_name = 'Abel';
SET @x = 2;
UPDATE employees SET email = 'aabbel' WHERE last_name = 'Abel';
SET @x = 3;
END //

DELIMITER ;

调用过程:

1
2
3
4
5
6
7
8
9
10
11
mysql> CALL UpdateDataWithCondition();
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT @x,@proc_value;
+------+-------------+
| @x | @proc_value |
+------+-------------+
| 3 | -1 |
+------+-------------+
1 row in set (0.00 sec)

举例:

创建一个名称为“InsertDataWithCondition”的存储过程,代码如下。

在存储过程中,定义处理程序,捕获sqlstate_value值,当遇到sqlstate_value值为23000时,执行EXIT操作,并且将@proc_value的值设置为-1。

1
2
3
4
5
6
7
#准备工作
CREATE TABLE departments
AS
SELECT * FROM atguigudb.`departments`;

ALTER TABLE departments
ADD CONSTRAINT uk_dept_name UNIQUE(department_id);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DELIMITER //

CREATE PROCEDURE InsertDataWithCondition()
BEGIN
DECLARE duplicate_entry CONDITION FOR SQLSTATE '23000' ;
DECLARE EXIT HANDLER FOR duplicate_entry SET @proc_value = -1;

SET @x = 1;
INSERT INTO departments(department_name) VALUES('测试');
SET @x = 2;
INSERT INTO departments(department_name) VALUES('测试');
SET @x = 3;
END //

DELIMITER ;

调用存储过程:

1
2
3
4
5
6
7
8
9
10
11
mysql> CALL InsertDataWithCondition();
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT @x,@proc_value;
+------+-------------+
| @x | @proc_value |
+------+-------------+
| 2 | -1 |
+------+-------------+
1 row in set (0.00 sec)


流程控制

解决复杂问题不可能通过一个 SQL 语句完成,我们需要执行多个 SQL 操作。流程控制语句的作用就是控制存储过程中 SQL 语句的执行顺序,是我们完成复杂操作必不可少的一部分。只要是执行的程序,流程就分为三大类:

  • 顺序结构:程序从上往下依次执行
  • 分支结构:程序按条件进行选择执行,从两条或多条路径中选择一条执行
  • 循环结构:程序满足一定条件下,重复执行一组语句

针对于MySQL 的流程控制语句主要有 3 类。注意:只能用于存储程序。

  • 条件判断语句:IF 语句和 CASE 语句
  • 循环语句:LOOP、WHILE 和 REPEAT 语句
  • 跳转语句:ITERATE 和 LEAVE 语句

分支结构之 IF

  • IF 语句的语法结构是:
1
2
3
4
IF 表达式1 THEN 操作1
[ELSEIF 表达式2 THEN 操作2]……
[ELSE 操作N]
END IF

根据表达式的结果为TRUE或FALSE执行相应的语句。

  • 举例1:

    1
    2
    3
    4
    5
    IF val IS NULL 
    THEN SELECT 'val is null';
    ELSE SELECT 'val is not null';

    END IF;
  • 举例2:声明存储过程“update_salary_by_eid1”,定义IN参数emp_id,输入员工编号。判断该员工薪资如果低于8000元并且入职时间超过5年,就涨薪500元;否则就不变。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    DELIMITER //

    CREATE PROCEDURE update_salary_by_eid1(IN emp_id INT)
    BEGIN
    DECLARE emp_salary DOUBLE;
    DECLARE hire_year DOUBLE;

    SELECT salary INTO emp_salary FROM employees WHERE employee_id = emp_id;

    SELECT DATEDIFF(CURDATE(),hire_date)/365 INTO hire_year
    FROM employees WHERE employee_id = emp_id;

    IF emp_salary < 8000 AND hire_year > 5
    THEN UPDATE employees SET salary = salary + 500 WHERE employee_id = emp_id;
    END IF;
    END //


    DELIMITER ;
  • 举例3:声明存储过程“update_salary_by_eid2”,定义IN参数emp_id,输入员工编号。判断该员工薪资如果低于9000元并且入职时间超过5年,就涨薪500元;否则就涨薪100元。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    DELIMITER //

    CREATE PROCEDURE update_salary_by_eid2(IN emp_id INT)
    BEGIN
    DECLARE emp_salary DOUBLE;
    DECLARE hire_year DOUBLE;

    SELECT salary INTO emp_salary FROM employees WHERE employee_id = emp_id;

    SELECT DATEDIFF(CURDATE(),hire_date)/365 INTO hire_year
    FROM employees WHERE employee_id = emp_id;

    IF emp_salary < 8000 AND hire_year > 5
    THEN UPDATE employees SET salary = salary + 500 WHERE employee_id = emp_id;
    ELSE
    UPDATE employees SET salary = salary + 100 WHERE employee_id = emp_id;
    END IF;
    END //


    DELIMITER ;
  • 举例4:声明存储过程“update_salary_by_eid3”,定义IN参数emp_id,输入员工编号。判断该员工薪资如果低于9000元,就更新薪资为9000元;薪资如果大于等于9000元且低于10000的,但是奖金比例为NULL的,就更新奖金比例为0.01;其他的涨薪100元。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    DELIMITER //

    CREATE PROCEDURE update_salary_by_eid3(IN emp_id INT)
    BEGIN
    DECLARE emp_salary DOUBLE;
    DECLARE bonus DECIMAL(3,2);

    SELECT salary INTO emp_salary FROM employees WHERE employee_id = emp_id;
    SELECT commission_pct INTO bonus FROM employees WHERE employee_id = emp_id;

    IF emp_salary < 9000
    THEN UPDATE employees SET salary = 9000 WHERE employee_id = emp_id;
    ELSEIF emp_salary < 10000 AND bonus IS NULL
    THEN UPDATE employees SET commission_pct = 0.01 WHERE employee_id = emp_id;
    ELSE
    UPDATE employees SET salary = salary + 100 WHERE employee_id = emp_id;
    END IF;
    END //

    DELIMITER ;

分支结构之 CASE

CASE 语句的语法结构1:

1
2
3
4
5
6
7
#情况一:类似于switch
CASE 表达式
WHEN 值1 THEN 结果1或语句1(如果是语句,需要加分号)
WHEN 值2 THEN 结果2或语句2(如果是语句,需要加分号)
...
ELSE 结果n或语句n(如果是语句,需要加分号)
END [case](如果是放在begin end中需要加上case,如果放在select后面不需要)

CASE 语句的语法结构2:

1
2
3
4
5
6
7
#情况二:类似于多重if
CASE
WHEN 条件1 THEN 结果1或语句1(如果是语句,需要加分号)
WHEN 条件2 THEN 结果2或语句2(如果是语句,需要加分号)
...
ELSE 结果n或语句n(如果是语句,需要加分号)
END [case](如果是放在begin end中需要加上case,如果放在select后面不需要)
  • 举例1:

使用CASE流程控制语句的第1种格式,判断val值等于1、等于2,或者两者都不等。

1
2
3
4
5
CASE val
   WHEN 1 THEN SELECT 'val is 1';
   WHEN 2 THEN SELECT 'val is 2';
   ELSE SELECT 'val is not 1 or 2';
END CASE;
  • 举例2:

使用CASE流程控制语句的第2种格式,判断val是否为空、小于0、大于0或者等于0。

1
2
3
4
5
6
CASE
WHEN val IS NULL THEN SELECT 'val is null';
WHEN val < 0 THEN SELECT 'val is less than 0';
WHEN val > 0 THEN SELECT 'val is greater than 0';
ELSE SELECT 'val is 0';
END CASE;
  • 举例3:声明存储过程“update_salary_by_eid4”,定义IN参数emp_id,输入员工编号。判断该员工薪资如果低于9000元,就更新薪资为9000元;薪资大于等于9000元且低于10000的,但是奖金比例为NULL的,就更新奖金比例为0.01;其他的涨薪100元。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DELIMITER //

CREATE PROCEDURE update_salary_by_eid4(IN emp_id INT)
BEGIN
DECLARE emp_sal DOUBLE;
DECLARE bonus DECIMAL(3,2);

SELECT salary INTO emp_sal FROM employees WHERE employee_id = emp_id;
SELECT commission_pct INTO bonus FROM employees WHERE employee_id = emp_id;

CASE
WHEN emp_sal<9000
THEN UPDATE employees SET salary=9000 WHERE employee_id = emp_id;
WHEN emp_sal<10000 AND bonus IS NULL
THEN UPDATE employees SET commission_pct=0.01 WHERE employee_id = emp_id;
ELSE
UPDATE employees SET salary=salary+100 WHERE employee_id = emp_id;
END CASE;
END //

DELIMITER ;
  • 举例4:声明存储过程update_salary_by_eid5,定义IN参数emp_id,输入员工编号。判断该员工的入职年限,如果是0年,薪资涨50;如果是1年,薪资涨100;如果是2年,薪资涨200;如果是3年,薪资涨300;如果是4年,薪资涨400;其他的涨薪500。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DELIMITER //

CREATE PROCEDURE update_salary_by_eid5(IN emp_id INT)
BEGIN
DECLARE emp_sal DOUBLE;
DECLARE hire_year DOUBLE;

SELECT salary INTO emp_sal FROM employees WHERE employee_id = emp_id;

SELECT ROUND(DATEDIFF(CURDATE(),hire_date)/365) INTO hire_year FROM employees WHERE employee_id = emp_id;

CASE hire_year
WHEN 0 THEN UPDATE employees SET salary=salary+50 WHERE employee_id = emp_id;
WHEN 1 THEN UPDATE employees SET salary=salary+100 WHERE employee_id = emp_id;
WHEN 2 THEN UPDATE employees SET salary=salary+200 WHERE employee_id = emp_id;
WHEN 3 THEN UPDATE employees SET salary=salary+300 WHERE employee_id = emp_id;
WHEN 4 THEN UPDATE employees SET salary=salary+400 WHERE employee_id = emp_id;
ELSE UPDATE employees SET salary=salary+500 WHERE employee_id = emp_id;
END CASE;
END //

DELIMITER ;

循环结构之LOOP

LOOP循环语句用来重复执行某些语句。LOOP内的语句一直重复执行直到循环被退出(使用LEAVE子句),跳出循环过程。

LOOP语句的基本格式如下:

1
2
3
[loop_label:] LOOP
循环执行的语句
END LOOP [loop_label]

其中,loop_label表示LOOP语句的标注名称,该参数可以省略。

举例1:

使用LOOP语句进行循环操作,id值小于10时将重复执行循环过程。

1
2
3
4
5
6
7
DECLARE id INT DEFAULT 0;
add_loop:LOOP
SET id = id +1;
IF id >= 10 THEN LEAVE add_loop;
END IF;

END LOOP add_loop;

举例2:当市场环境变好时,公司为了奖励大家,决定给大家涨工资。声明存储过程“update_salary_loop()”,声明OUT参数num,输出循环次数。存储过程中实现循环给大家涨薪,薪资涨为原来的1.1倍。直到全公司的平均薪资达到12000结束。并统计循环次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DELIMITER //

CREATE PROCEDURE update_salary_loop(OUT num INT)
BEGIN
DECLARE avg_salary DOUBLE;
DECLARE loop_count INT DEFAULT 0;

SELECT AVG(salary) INTO avg_salary FROM employees;

label_loop:LOOP
IF avg_salary >= 12000 THEN LEAVE label_loop;
END IF;

UPDATE employees SET salary = salary * 1.1;
SET loop_count = loop_count + 1;
SELECT AVG(salary) INTO avg_salary FROM employees;
END LOOP label_loop;

SET num = loop_count;

END //

DELIMITER ;

循环结构之WHILE

WHILE语句创建一个带条件判断的循环过程。WHILE在执行语句执行时,先对指定的表达式进行判断,如果为真,就执行循环内的语句,否则退出循环。WHILE语句的基本格式如下:

1
2
3
[while_label:] WHILE 循环条件  DO
循环体
END WHILE [while_label];

while_label为WHILE语句的标注名称;如果循环条件结果为真,WHILE语句内的语句或语句群被执行,直至循环条件为假,退出循环。

举例1:

WHILE语句示例,i值小于10时,将重复执行循环过程,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DELIMITER //

CREATE PROCEDURE test_while()
BEGIN
DECLARE i INT DEFAULT 0;

WHILE i < 10 DO
SET i = i + 1;
END WHILE;

SELECT i;
END //

DELIMITER ;
#调用
CALL test_while();

举例2:市场环境不好时,公司为了渡过难关,决定暂时降低大家的薪资。声明存储过程“update_salary_while()”,声明OUT参数num,输出循环次数。存储过程中实现循环给大家降薪,薪资降为原来的90%。直到全公司的平均薪资达到5000结束。并统计循环次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DELIMITER //

CREATE PROCEDURE update_salary_while(OUT num INT)
BEGIN
DECLARE avg_sal DOUBLE ;
DECLARE while_count INT DEFAULT 0;

SELECT AVG(salary) INTO avg_sal FROM employees;

WHILE avg_sal > 5000 DO
UPDATE employees SET salary = salary * 0.9;

SET while_count = while_count + 1;

SELECT AVG(salary) INTO avg_sal FROM employees;
END WHILE;

SET num = while_count;

END //

DELIMITER ;

循环结构之REPEAT

REPEAT语句创建一个带条件判断的循环过程。与WHILE循环不同的是,REPEAT 循环首先会执行一次循环,然后在 UNTIL 中进行表达式的判断,如果满足条件就退出,即 END REPEAT;如果条件不满足,则会就继续执行循环,直到满足退出条件为止。

REPEAT语句的基本格式如下:

1
2
3
4
[repeat_label:] REPEAT
    循环体的语句
UNTIL 结束循环的条件表达式
END REPEAT [repeat_label]

repeat_label为REPEAT语句的标注名称,该参数可以省略;REPEAT语句内的语句或语句群被重复,直至expr_condition为真。

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DELIMITER //

CREATE PROCEDURE test_repeat()
BEGIN
DECLARE i INT DEFAULT 0;

REPEAT
SET i = i + 1;
UNTIL i >= 10
END REPEAT;

SELECT i;
END //

DELIMITER ;

举例2:当市场环境变好时,公司为了奖励大家,决定给大家涨工资。声明存储过程“update_salary_repeat()”,声明OUT参数num,输出循环次数。存储过程中实现循环给大家涨薪,薪资涨为原来的1.15倍。直到全公司的平均薪资达到13000结束。并统计循环次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DELIMITER //

CREATE PROCEDURE update_salary_repeat(OUT num INT)
BEGIN
DECLARE avg_sal DOUBLE ;
DECLARE repeat_count INT DEFAULT 0;

SELECT AVG(salary) INTO avg_sal FROM employees;

REPEAT
UPDATE employees SET salary = salary * 1.15;

SET repeat_count = repeat_count + 1;

SELECT AVG(salary) INTO avg_sal FROM employees;
UNTIL avg_sal >= 13000
END REPEAT;

SET num = repeat_count;

END //

DELIMITER ;

对比三种循环结构:

1、这三种循环都可以省略名称,但如果循环中添加了循环控制语句(LEAVE或ITERATE)则必须添加名称。
2、
LOOP:一般用于实现简单的”死”循环
WHILE:先判断后执行
REPEAT:先执行后判断,无条件至少执行一次

跳转语句之LEAVE语句

LEAVE语句:可以用在循环语句内,或者以 BEGIN 和 END 包裹起来的程序体内,表示跳出循环或者跳出程序体的操作。如果你有面向过程的编程语言的使用经验,你可以把 LEAVE 理解为 break。

基本格式如下:

1
LEAVE 标记名

其中,label参数表示循环的标志。LEAVE和BEGIN … END或循环一起被使用。

举例1:创建存储过程 “leave_begin()”,声明INT类型的IN参数num。给BEGIN…END加标记名,并在BEGIN…END中使用IF语句判断num参数的值。

  • 如果num<=0,则使用LEAVE语句退出BEGIN…END;
  • 如果num=1,则查询“employees”表的平均薪资;
  • 如果num=2,则查询“employees”表的最低薪资;
  • 如果num>2,则查询“employees”表的最高薪资。

IF语句结束后查询“employees”表的总人数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DELIMITER //

CREATE PROCEDURE leave_begin(IN num INT)

begin_label: BEGIN
IF num<=0
THEN LEAVE begin_label;
ELSEIF num=1
THEN SELECT AVG(salary) FROM employees;
ELSEIF num=2
THEN SELECT MIN(salary) FROM employees;
ELSE
SELECT MAX(salary) FROM employees;
END IF;

SELECT COUNT(*) FROM employees;
END //


DELIMITER ;

举例2:

当市场环境不好时,公司为了渡过难关,决定暂时降低大家的薪资。声明存储过程“leave_while()”,声明OUT参数num,输出循环次数,存储过程中使用WHILE循环给大家降低薪资为原来薪资的90%,直到全公司的平均薪资小于等于10000,并统计循环次数。

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
DELIMITER //
CREATE PROCEDURE leave_while(OUT num INT)

BEGIN
#
DECLARE avg_sal DOUBLE;#记录平均工资
DECLARE while_count INT DEFAULT 0; #记录循环次数

SELECT AVG(salary) INTO avg_sal FROM employees; #① 初始化条件

while_label:WHILE TRUE DO #② 循环条件

#③ 循环体
IF avg_sal <= 10000 THEN
LEAVE while_label;
END IF;

UPDATE employees SET salary = salary * 0.9;
SET while_count = while_count + 1;

#④ 迭代条件
SELECT AVG(salary) INTO avg_sal FROM employees;

END WHILE;

#赋值
SET num = while_count;

END //

DELIMITER ;

跳转语句之ITERATE语句

ITERATE语句:只能用在循环语句(LOOP、REPEAT和WHILE语句)内,表示重新开始循环,将执行顺序转到语句段开头处。如果你有面向过程的编程语言的使用经验,你可以把 ITERATE 理解为 continue,意思为“再次循环”。

语句基本格式如下:

1
ITERATE label

label参数表示循环的标志。ITERATE语句必须跟在循环标志前面。

举例: 定义局部变量num,初始值为0。循环结构中执行num + 1操作。

  • 如果num < 10,则继续执行循环;
  • 如果num > 15,则退出循环结构;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DELIMITER //

CREATE PROCEDURE test_iterate()

BEGIN
DECLARE num INT DEFAULT 0;

my_loop:LOOP
SET num = num + 1;

IF num < 10
THEN ITERATE my_loop;
ELSEIF num > 15
THEN LEAVE my_loop;
END IF;

SELECT '尚硅谷:让天下没有难学的技术';

END LOOP my_loop;

END //

DELIMITER ;

游标

使用游标步骤

游标必须在声明处理程序之前被声明,并且变量和条件还必须在声明游标或处理程序之前被声明。

如果我们想要使用游标,一般需要经历四个步骤。不同的 DBMS 中,使用游标的语法可能略有不同。

第一步,声明游标

在MySQL中,使用DECLARE关键字来声明游标,其语法的基本形式如下:

1
DECLARE cursor_name CURSOR FOR select_statement; 

这个语法适用于 MySQL,SQL Server,DB2 和 MariaDB。如果是用 Oracle 或者 PostgreSQL,需要写成:

1
DECLARE cursor_name CURSOR IS select_statement;

要使用 SELECT 语句来获取数据结果集,而此时还没有开始遍历数据,这里 select_statement 代表的是 SELECT 语句,返回一个用于创建游标的结果集。

比如:

1
2
DECLARE cur_emp CURSOR FOR 
SELECT employee_id,salary FROM employees;
1
2
DECLARE cursor_fruit CURSOR FOR 
SELECT f_name, f_price FROM fruits ;

第二步,打开游标

打开游标的语法如下:

1
OPEN cursor_name

当我们定义好游标之后,如果想要使用游标,必须先打开游标。打开游标的时候 SELECT 语句的查询结果集就会送到游标工作区,为后面游标的逐条读取结果集中的记录做准备。

1
OPEN cur_emp ;

第三步,使用游标(从游标中取得数据)

语法如下:

1
FETCH cursor_name INTO var_name [, var_name] ...

这句的作用是使用 cursor_name 这个游标来读取当前行,并且将数据保存到 var_name 这个变量中,游标指针指到下一行。如果游标读取的数据行有多个列名,则在 INTO 关键字后面赋值给多个变量名即可。

注意:var_name必须在声明游标之前就定义好。

1
FETCH cur_emp INTO emp_id, emp_sal ;

注意:游标的查询结果集中的字段数,必须跟 INTO 后面的变量数一致,否则,在存储过程执行的时候,MySQL 会提示错误。

第四步,关闭游标

1
CLOSE cursor_name

有 OPEN 就会有 CLOSE,也就是打开和关闭游标。当我们使用完游标后需要关闭掉该游标。因为游标会占用系统资源,如果不及时关闭,游标会一直保持到存储过程结束,影响系统运行的效率。而关闭游标的操作,会释放游标占用的系统资源。

关闭游标之后,我们就不能再检索查询结果中的数据行,如果需要检索只能再次打开游标。

1
CLOSE cur_emp;

举例

创建存储过程“get_count_by_limit_total_salary()”,声明IN参数 limit_total_salary,DOUBLE类型;声明OUT参数total_count,INT类型。函数的功能可以实现累加薪资最高的几个员工的薪资值,直到薪资总和达到limit_total_salary参数的值,返回累加的人数给total_count。

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
DELIMITER //

CREATE PROCEDURE get_count_by_limit_total_salary(IN limit_total_salary DOUBLE,OUT total_count INT)

BEGIN
DECLARE sum_salary DOUBLE DEFAULT 0; #记录累加的总工资
DECLARE cursor_salary DOUBLE DEFAULT 0; #记录某一个工资值
DECLARE emp_count INT DEFAULT 0; #记录循环个数
#定义游标
DECLARE emp_cursor CURSOR FOR SELECT salary FROM employees ORDER BY salary DESC;
#打开游标
OPEN emp_cursor;

REPEAT
#使用游标(从游标中获取数据)
FETCH emp_cursor INTO cursor_salary;

SET sum_salary = sum_salary + cursor_salary;
SET emp_count = emp_count + 1;

UNTIL sum_salary >= limit_total_salary
END REPEAT;

SET total_count = emp_count;
#关闭游标
CLOSE emp_cursor;

END //

DELIMITER ;

小结

游标是 MySQL 的一个重要的功能,为逐条读取结果集中的数据,提供了完美的解决方案。跟在应用层面实现相同的功能相比,游标可以在存储程序中使用,效率高,程序也更加简洁。

但同时也会带来一些性能问题,比如在使用游标的过程中,会对数据行进行加锁,这样在业务并发量大的时候,不仅会影响业务之间的效率,还会消耗系统资源,造成内存不足,这是因为游标是在内存中进行的处理。

建议:养成用完之后就关闭的习惯,这样才能提高系统的整体效率。

聚合函数

  • 什么是聚合函数

聚合函数作用于一组数据,并对一组数据返回一个值。

  • 聚合函数类型

    • AVG()
    • SUM()
    • MAX()
    • MIN()
    • COUNT()
  • 聚合函数不能嵌套调用。比如不能出现类似AVG(SUM(字段名称))形式的调用。

AVG和SUM函数

可以对数值型数据使用AVGSUM函数。

1
2
3
4
5
6
7
SELECT AVG(salary), MAX(salary),MIN(salary), SUM(salary)
FROM employees
WHERE job_id LIKE '%REP%';

AVg(salary) MAx(salary) min(salary) Sum(salary)
----------- ----------- ----------- -------------
8272.727273 11500.00 6000.00 273000.00

MIN和MAX函数

可以对任意数据类型的数据使用MINMAX函数。

1
2
3
4
5
6
SELECT MIN(hire_date), MAX(hire_date)
FROM employees;

min(hire_date) max(hire_date)
-------------- ----------------
1987-06-17 2000-04-21

COUNT函数

  • COUNT(*)返回表中记录总数,适用于任意数据类型
1
2
3
SELECT COUNT(*)
FROM employees
WHERE department_id = 50;
  • COUNT(expr)返回expr不为空的记录总数。
1
2
3
SELECT COUNT(commission_pct)
FROM employees
WHERE department_id = 50;

问题:用count(*),count(1),count(列名)谁好呢?

其实,对于MyISAM引擎的表是没有区别的。这种引擎内部有一计数器在维护着行数。

Innodb引擎的表用count(*),count(1)直接读行数,复杂度是O(n),因为innodb真的要去数一遍。但好于具体的count(列名)。

GROUP BY

GROUP BY子句将查询结果按某一列或多列的值分组,值相等的为一组。对查询结果分组的目的是为了细化聚合函数的作用对象。如果未对查询结果分组,聚合函数将作用于整个查询结果。

在SELECT列表中所有未包含在组函数中的列都应该包含在 GROUP BY子句中

1
2
3
SELECT   department_id, AVG(salary)
FROM employees
GROUP BY department_id ;

在SELECT列表中所有未包含在组函数中的列都应该包含在 GROUP BY子句中

GROUP BY中使用WITH ROLLUP

使用WITH ROLLUP关键字之后,在所有查询出的分组记录之后增加一条记录,该记录计算查询出的所有记录的总和,即统计记录数量。

1
2
3
4
SELECT department_id,AVG(salary)
FROM employees
WHERE department_id > 80
GROUP BY department_id WITH ROLLUP;

注意:

当使用ROLLUP时,不能同时使用ORDER BY子句进行结果排序,即ROLLUP和ORDER BY是互相排斥的。

HAVING

过滤分组HAVING子句

  1. 行已经被分组。
  2. 使用了聚合函数。
  3. 满足HAVING 子句中条件的分组将被显示。
  4. HAVING不能单独使用,必须要跟GROUP BY一起使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT   department_id, MAX(salary)
FROM employees
GROUP BY department_id
HAVING MAX(salary)>10000 ;

department_id MAX(salary)
------------- -------------
20 13000.00
30 11000.00
80 14000.00
90 24000.00
100 12000.00
110 12000.00
  • 非法使用聚合函数 : 不能在 WHERE 子句中使用聚合函数。如下:
1
2
3
4
SELECT   department_id, AVG(salary)
FROM employees
WHERE AVG(salary) > 8000
GROUP BY department_id;

ERROR at line 3: ORA-00934. group function is not allowed here

WHERE和HAVING的对比

区别1:WHERE 可以直接使用表中的字段作为筛选条件,但不能使用分组中的计算函数作为筛选条件;HAVING 必须要与 GROUP BY 配合使用,可以把分组计算的函数和分组字段作为筛选条件。

这决定了,在需要对数据进行分组统计的时候,HAVING可以完成WHERE不能完成的任务。这是因为,在查询语法结构中,WHEREGROUP BY之前,所以无法对分组结果进行筛选。HAVINGGROUP BY之后,可以使用分组字段和分组中的计算函数,对分组的结果集进行筛选,这个功能是WHERE无法完成的。另外,WHERE排除的记录不再包括在分组中。

区别2:如果需要通过连接从关联表中获取需要的数据,WHERE 是先筛选后连接,而 HAVING 是先连接后筛选。 这一点,就决定了在关联查询中,WHEREHAVING更高效。因为WHERE可以先筛选,用一个筛选后的较小数据集和关联表进行连接,这样占用的资源比较少,执行效率也比较高。HAVING 则需要先把结果集准备好,也就是用未被筛选的数据集进行关联,然后对这个大的数据集进行筛选,这样占用的资源就比较多,执行效率也较低。

小结如下:

优点 缺点
WHERE 先筛选数据再关联,执行效率高 不能使用分组中的计算函数进行筛选
HAVING 可以使用分组中的计算函数 在最后的结果集中进行筛选,执行效率较低

开发中的选择:

WHEREHAVING也不是互相排斥的,我们可以在一个查询里面同时使用WHEREHAVING。包含分组统计函数的条件用HAVING,普通条件用WHERE。这样,我们就既利用了WHERE条件的高效快速,又发挥了HAVING可以使用包含分组统计函数的查询条件的优点。当数据量特别大的时候,运行效率会有很大的差别。


SELECT的执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT [ALL | DISTINCT] < 目标列表达式 > [,< 目标列表达式 >]...
FROM < 表名或试图名 > [,< 表名或试图名 >]...
[ WHERE < 条件表达式 > ]
[ GROUP BY < 列名1 > [ HAVING < 条件表达式 > ] ]
[ ORDER BY < 列名2 > [ ASC | DESC ]];


#其中:
#(1)from:从哪些表中筛选
#(2)on:关联多表查询时,去除笛卡尔积
#(3)where:从表中筛选的条件
#(4)group by:分组依据
#(5)having:在统计结果中再次筛选
#(6)order by:排序
#(7)limit:分页

SELECT执行顺序

你需要记住 SELECT 查询时的两个顺序:

1. 关键字的顺序是不能颠倒的:

1
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT...

2.SELECT 语句的执行顺序(在 MySQL 和 Oracle 中,SELECT 执行顺序基本相同):

1
FROM -> WHERE -> GROUP BY -> HAVING -> SELECT 的字段 -> DISTINCT -> ORDER BY -> LIMIT

比如你写了一个 SQL 语句,那么它的关键字顺序和执行顺序是下面这样的:

1
2
3
4
5
6
7
SELECT DISTINCT player_id, player_name, count(*) as num # 顺序 5
FROM player JOIN team ON player.team_id = team.team_id # 顺序 1
WHERE height > 1.80 # 顺序 2
GROUP BY player.team_id # 顺序 3
HAVING num > 2 # 顺序 4
ORDER BY num DESC # 顺序 6
LIMIT 2 # 顺序 7

在 SELECT 语句执行这些步骤的时候,每个步骤都会产生一个虚拟表,然后将这个虚拟表传入下一个步骤中作为输入。需要注意的是,这些步骤隐含在 SQL 的执行过程中,对于我们来说是不可见的。

SQL 的执行原理

SELECT是先执行FROM这一步的。在这个阶段,如果是多张表联查,还会经历下面的几个步骤:

  1. 首先先通过CROSS JOIN求笛卡尔积,相当于得到虚拟表 vt(virtual table)1-1;
  2. 通过ON进行筛选,在虚拟表 vt1-1 的基础上进行筛选,得到虚拟表 vt1-2;
  3. 添加外部行。如果我们使用的是左连接、右链接或者全连接,就会涉及到外部行,也就是在虚拟表 vt1-2 的基础上增加外部行,得到虚拟表 vt1-3。

当然如果我们操作的是两张以上的表,还会重复上面的步骤,直到所有表都被处理完为止。这个过程得到是我们的原始数据。

当我们拿到了查询数据表的原始数据,也就是最终的虚拟表 vt1,就可以在此基础上再进行 WHERE 阶段。在这个阶段中,会根据 vt1 表的结果进行筛选过滤,得到虚拟表 vt2

然后进入第三步和第四步,也就是 GROUPHAVING阶段。在这个阶段中,实际上是在虚拟表 vt2 的基础上进行分组和分组过滤,得到中间的虚拟表 vt3vt4

当我们完成了条件筛选部分之后,就可以筛选表中提取的字段,也就是进入到 SELECTDISTINCT 阶段。

首先在SELECT阶段会提取想要的字段,然后在DISTINCT阶段过滤掉重复的行,分别得到中间的虚拟表 vt5-1vt5-2

当我们提取了想要的字段数据之后,就可以按照指定的字段进行排序,也就是 ORDER BY阶段,得到虚拟表 vt6

最后在 vt6 的基础上,取出指定行的记录,也就是LIMIT阶段,得到最终的结果,对应的是虚拟表 vt7

当然我们在写 SELECT 语句的时候,不一定存在所有的关键字,相应的阶段就会省略。

同时因为 SQL 是一门类似英语的结构化查询语言,所以我们在写 SELECT 语句的时候,还要注意相应的关键字顺序,所谓底层运行的原理,就是我们刚才讲到的执行顺序。


子查询

子查询指一个查询语句嵌套在另一个查询语句内部的查询,这个特性从MySQL 4.1开始引入。

  • 子查询(内查询)在主查询之前一次执行完成。
  • 子查询的结果被主查询(外查询)使用 。
  • 注意事项
    • 子查询要包含在括号内
    • 将子查询放在比较条件的右侧
    • 单行操作符对应单行子查询,多行操作符对应多行子查询

单行子查询

单行比较操作符

操作符 含义
= equal to
> greater than
>= greater than or equal to
< less than
<= less than or equal to
<> not equal to

题目:查询工资大于149号员工工资的员工的信息

1
2
3
4
5
6
7
SELECT last_name 
FROM employees
WHERE salary > (
SELECT salary
FROM employees
WHERE employee id = 149
)

多行子查询

  • 也称为集合比较子查询
  • 内查询返回多行
  • 使用多行比较操作符

多行比较操作符

操作符 含义
IN 等于列表中的任意一个
ANY 需要和单行比较操作符一起使用,和子查询返回的某一个值比较
ALL 需要和单行比较操作符一起使用,和子查询返回的所有值比较
SOME 实际上是ANY的别名,作用相同,一般常使用ANY

题目:查询平均工资最低的部门id

1
2
3
4
5
6
7
8
SELECT department_id
FROM employees
GROUP BY department_id
HAVING AVG(salary) <= ALL (
SELECT AVG(salary) avg_sal
FROM employees
GROUP BY department_id
)

相关子查询

相关子查询执行流程

如果子查询的执行依赖于外部查询,通常情况下都是因为子查询中的表用到了外部的表,并进行了条件关联,因此每执行一次外部查询,子查询都要重新计算一次,这样的子查询就称之为关联子查询

相关子查询按照一行接一行的顺序执行,主查询的每一行都执行一次子查询。

题目:找出每个学生超过他选修课程平均成绩的课程号

1
2
3
4
5
6
7
SELECT Sno, Cno
FROM SC x
WHERE Grade >= (
SELECT AVG(Grade)
FROM SC y
WHERE y.Sno = x.Sno
)

EXISTS 与 NOT EXISTS关键字

EXISTS代表存在量词。带有EXISTS谓词的子查询不返回任何数据,只产生逻辑真值true或逻辑假值false

使用存在量词EXISTS后,若内存查询结果非空,则外层的WHERE子句返回真值,否则返回假值。

NOT EXISTS关键字表示如果不存在某种条件,则返回TRUE,否则返回FALSE

题目:查询departments表中,不存在于employees表中的部门的department_id和department_name

1
2
3
4
5
SELECT department_id, department_name
FROM departments d
WHERE NOT EXISTS (SELECT 'X'
FROM employees
WHERE department_id = d.department_id);

创建和管理表

标识符命名规则

  • 数据库名、表名不得超过30个字符,变量名限制为29个
  • 必须只能包含 A–Z, a–z, 0–9, _共63个字符
  • 数据库名、表名、字段名等对象名中间不要包含空格
  • 同一个MySQL软件中,数据库不能同名;同一个库中,表不能重名;同一个表中,字段不能重名
  • 必须保证你的字段没有和保留字、数据库系统或常用方法冲突。如果坚持使用,请在SQL语句中使用`(着重号)引起来
  • 保持字段名和类型的一致性:在命名字段并为其指定数据类型的时候一定要保证一致性,假如数据类型在一个表里是整数,那在另一个表里可就别变成字符型了

MySQL中的数据类型

类型 类型举例
整数类型 TINYINT、SMALLINT、MEDIUMINT、**INT(或INTEGER)**、BIGINT
浮点类型 FLOAT、DOUBLE
定点数类型 DECIMAL
位类型 BIT
日期时间类型 YEAR、TIME、DATE、DATETIME、TIMESTAMP
文本字符串类型 CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT
枚举类型 ENUM
集合类型 SET
二进制字符串类型 BINARY、VARBINARY、TINYBLOB、BLOB、MEDIUMBLOB、LONGBLOB
JSON类型 JSON对象、JSON数组
空间数据类型 单值:GEOMETRY、POINT、LINESTRING、POLYGON;
集合:MULTIPOINT、MULTILINESTRING、MULTIPOLYGON、GEOMETRYCOLLECTION

其中,常用的几类类型介绍如下:

数据类型 描述
INT 从-2^31到2^31-1的整型数据。存储大小为 4个字节
CHAR(size) 定长字符数据。若未指定,默认为1个字符,最大长度255
VARCHAR(size) 可变长字符数据,根据字符串实际长度保存,必须指定长度
FLOAT(M,D) 单精度,占用4个字节,M=整数位+小数位,D=小数位。 D<=M<=255,0<=D<=30,默认M+D<=6
DOUBLE(M,D) 双精度,占用8个字节,D<=M<=255,0<=D<=30,默认M+D<=15
DECIMAL(M,D) 高精度小数,占用M+2个字节,D<=M<=65,0<=D<=30,最大取值范围与DOUBLE相同。
DATE 日期型数据,格式’YYYY-MM-DD’
BLOB 二进制形式的长文本数据,最大可达4G
TEXT 长文本数据,最大可达4G

创建和管理数据库

创建数据库

  • 方式1:创建数据库
1
CREATE DATABASE 数据库名; 
  • 方式2:创建数据库并指定字符集
1
CREATE DATABASE 数据库名 CHARACTER SET 字符集;
  • 方式3:判断数据库是否已经存在,不存在则创建数据库(推荐
1
CREATE DATABASE IF NOT EXISTS 数据库名; 

如果MySQL中已经存在相关的数据库,则忽略创建语句,不再创建数据库。

注意:DATABASE 不能改名。一些可视化工具可以改名,它是建新库,把所有表复制到新库,再删旧库完成的。

修改数据库

  • 更改数据库字符集
1
ALTER DATABASE 数据库名 CHARACTER SET 字符集;  #比如:gbk、utf8等

删除数据库

  • 方式1:删除指定的数据库
1
DROP DATABASE 数据库名;
  • 方式2:删除指定的数据库(推荐
1
DROP DATABASE IF EXISTS 数据库名;

创建表

创建方式1

  • 必须具备:
    • CREATE TABLE权限
    • 存储空间
  • 语法格式:
1
2
3
4
5
6
7
CREATE TABLE [IF NOT EXISTS] 表名(
字段1, 数据类型 [约束条件] [默认值],
字段2, 数据类型 [约束条件] [默认值],
字段3, 数据类型 [约束条件] [默认值],
……
[表约束条件]
);

加上了IF NOT EXISTS关键字,则表示:如果当前数据库中不存在要创建的数据表,则创建数据表;如果当前数据库中已经存在要创建的数据表,则忽略建表语句,不再创建数据表。

  • 必须指定:

    • 表名
    • 列名(或字段名),数据类型,长度
  • 可选指定:

    • 约束条件
    • 默认值
  • 创建表举例:

1
2
3
4
5
6
7
8
CREATE TABLE dept(
-- int类型,自增
deptno INT(2) AUTO_INCREMENT,
dname VARCHAR(14),
loc VARCHAR(13),
-- 主键
PRIMARY KEY (deptno)
);DESCRIBE dept;

创建方式2 基于查询创建

1
2
3
4
5
CREATE TABLE dept80
AS
SELECT employee_id, last_name, salary*12 ANNSAL, hire_date
FROM employees
WHERE department_id = 80;

查看数据表结构

在MySQL中创建好数据表之后,可以查看数据表的结构。MySQL支持使用DESCRIBE/DESC语句查看数据表结构,也支持使用SHOW CREATE TABLE语句查看数据表结构。

语法格式如下:

1
SHOW CREATE TABLE 表名\G

使用SHOW CREATE TABLE语句不仅可以查看表创建时的详细语句,还可以查看存储引擎和字符编码。

修改表

修改表指的是修改数据库中已经存在的数据表的结构。

使用 ALTER TABLE 语句可以实现:

  • 向已有的表中添加列

  • 修改现有表中的列

  • 删除现有表中的列

  • 重命名现有表中的列

追加一个列

语法格式如下:

1
ALTER TABLE 表名 ADD 【COLUMN】 字段名 字段类型 【FIRST|AFTER 字段名】;

举例:

1
2
ALTER TABLE dept80 
ADD job_id varchar(15);

修改一个列

  • 可以修改列的数据类型,长度、默认值和位置

  • 修改字段数据类型、长度、默认值、位置的语法格式如下:

1
ALTER TABLE 表名 MODIFY 【COLUMN】 字段名1 字段类型 【DEFAULT 默认值】【FIRST|AFTER 字段名2】;
  • 举例:
1
2
ALTER TABLE	dept80
MODIFY last_name VARCHAR(30);
1
2
ALTER TABLE	dept80
MODIFY salary double(9,2) default 1000;
  • 对默认值的修改只影响今后对表的修改

重命名一个列

使用 CHANGE old_column new_column dataType子句重命名列。语法格式如下:

1
ALTER TABLE 表名 CHANGE 【column】 列名 新列名 新数据类型;

举例:

1
2
ALTER TABLE  dept80
CHANGE department_name dept_name varchar(15);

删除一个列

删除表中某个字段的语法格式如下:

1
ALTER TABLE 表名 DROP 【COLUMN】字段名

举例:

1
2
ALTER TABLE  dept80
DROP COLUMN job_id;

重命名表

  • 方式一:使用RENAME
1
2
RENAME TABLE emp
TO myemp;
  • 方式二:
1
2
ALTER table dept
RENAME [TO] detail_dept; -- [TO]可以省略
  • 必须是对象的拥有者

删除表

  • 在MySQL中,当一张数据表没有与其他任何数据表形成关联关系时,可以将当前数据表直接删除。

  • 数据和结构都被删除

  • 所有正在运行的相关事务被提交

  • 所有相关索引被删除

  • 语法格式:

1
DROP TABLE [IF EXISTS] 数据表1 [, 数据表2, …, 数据表n];

IF EXISTS的含义为:如果当前数据库中存在相应的数据表,则删除数据表;如果当前数据库中不存在相应的数据表,则忽略删除语句,不再执行删除数据表的操作。

  • 举例:
1
DROP TABLE dept80;
  • DROP TABLE语句不能回滚

清空表

  • TRUNCATE TABLE语句:

    • 删除表中所有的数据
    • 释放表的存储空间
  • 举例:

1
TRUNCATE TABLE detail_dept;
  • TRUNCATE语句不能回滚,而使用DELETE语句删除数据,可以回滚

  • 对比:

1
2
3
4
5
6
7
8
9
10
SET autocommit = FALSE;

DELETE FROM emp2;
#TRUNCATE TABLE emp2;

SELECT * FROM emp2;

ROLLBACK;

SELECT * FROM emp2;

阿里开发规范:

【参考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE 无事务且不触发 TRIGGER,有可能造成事故,故不建议在开发代码中使用此语句。

说明:TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。

阿里巴巴《Java开发手册》之MySQL字段命名

  • 强制】表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。

    • 正例:aliyun_admin,rdc_config,level3_name
    • 反例:AliyunAdmin,rdcConfig,level_3_name
  • 强制】禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。

  • 强制】表必备三字段:id, gmt_create, gmt_modified。

    • 说明:其中 id 必为主键,类型为BIGINT UNSIGNED、单表时自增、步长为 1。gmt_create, gmt_modified 的类型均为 DATETIME 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新
  • 强制】表的命名最好是遵循 “业务名称_表的作用”。

    • 正例:alipay_task 、 force_project、 trade_config
  • 强制】库名与应用名称尽量一致。

  • 参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。

    • 正例:无符号值可以避免误存负数,且扩大了表示范围。
对象 年龄区间 类型 字节 表示范围
150岁之内 tinyint unsigned 1 无符号值:0到255
数百岁 smallint unsigned 2 无符号值:0到65535
恐龙化石 数千万年 int unsigned 4 无符号值:0到约43亿
太阳 约50亿年 bigint unsigned 8 无符号值:0到约10的19次方

拓展2:如何理解清空表、删除表等操作需谨慎?!

表删除操作将把表的定义和表中的数据一起删除,并且MySQL在执行删除操作时,不会有任何的确认信息提示,因此执行删除操时应当慎重。在删除表前,最好对表中的数据进行备份,这样当操作失误时可以对数据进行恢复,以免造成无法挽回的后果。

同样的,在使用ALTER TABLE进行表的基本修改操作时,在执行操作过程之前,也应该确保对数据进行完整的备份,因为数据库的改变是无法撤销的,如果添加了一个不需要的字段,可以将其删除;相同的,如果删除了一个需要的列,该列下面的所有数据都将会丢失。

拓展3:MySQL8新特性—DDL的原子化

在MySQL 8.0版本中,InnoDB表的DDL支持事务完整性,即DDL操作要么成功要么回滚。DDL操作回滚日志写入到data dictionary数据字典表mysql.innodb_ddl_log(该表是隐藏的表,通过show tables无法看到)中,用于回滚操作。通过设置参数,可将DDL操作日志打印输出到MySQL错误日志中。

分别在MySQL 5.7版本和MySQL 8.0版本中创建数据库和数据表,结果如下:

1
2
3
4
5
6
7
8
9
10
CREATE DATABASE mytest;

USE mytest;

CREATE TABLE book1(
book_id INT ,
book_name VARCHAR(255)
);

SHOW TABLES;

(1)在MySQL 5.7版本中,测试步骤如下:
删除数据表book1和数据表book2,结果如下:

1
2
mysql> DROP TABLE book1,book2;
ERROR 1051 (42S02): Unknown table 'mytest.book2'

再次查询数据库中的数据表名称,结果如下:

1
2
mysql> SHOW TABLES;
Empty set (0.00 sec)

从结果可以看出,虽然删除操作时报错了,但是仍然删除了数据表book1。

(2)在MySQL 8.0版本中,测试步骤如下:
删除数据表book1和数据表book2,结果如下:

1
2
mysql> DROP TABLE book1,book2;
ERROR 1051 (42S02): Unknown table 'mytest.book2'

再次查询数据库中的数据表名称,结果如下:

1
2
3
4
5
6
7
mysql> show tables;
+------------------+
| Tables_in_mytest |
+------------------+
| book1 |
+------------------+
1 row in set (0.00 sec)

从结果可以看出,数据表book1并没有被删除。


数据处理之增删改

插入数据

方式1:VALUES的方式添加

使用这种语法一次只能向表中插入一条数据。

情况1:为表的所有字段按默认顺序插入数据

1
2
INSERT INTO 表名
VALUES (value1,value2,....);

举例:

1
2
INSERT INTO departments
VALUES (70, 'Pub', 100, 1700);

情况2:为表的指定字段插入数据

1
2
INSERT INTO 表名(column1 [, column2, …, columnn]) 
VALUES (value1 [,value2, …, valuen]);

为表的指定字段插入数据,就是在INSERT语句中只向部分字段中插入值,而其他字段的值为表定义时的默认值。

举例:

1
2
INSERT INTO departments(department_id, department_name)
VALUES (80, 'IT');

情况3:同时插入多条记录

INSERT语句可以同时向数据表中插入多条记录,插入时指定多个值列表,每个值列表之间用逗号分隔开,基本语法格式如下:

1
2
3
4
5
6
INSERT INTO table_name 
VALUES
(value1 [,value2, …, valuen]),
(value1 [,value2, …, valuen]),
……
(value1 [,value2, …, valuen]);

或者

1
2
3
4
5
6
INSERT INTO table_name(column1 [, column2, …, columnn]) 
VALUES
(value1 [,value2, …, valuen]),
(value1 [,value2, …, valuen]),
……
(value1 [,value2, …, valuen]);

举例:

1
2
3
4
5
6
mysql> INSERT INTO emp(emp_id,emp_name)
-> VALUES (1001,'shkstart'),
-> (1002,'atguigu'),
-> (1003,'Tom');
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0

使用INSERT同时插入多条记录时,MySQL会返回一些在执行单行插入时没有的额外信息,这些信息的含义如下:
● Records:表明插入的记录条数。
● Duplicates:表明插入时被忽略的记录,原因可能是这些记录包含了重复的主键值。
● Warnings:表明有问题的数据值,例如发生数据类型转换。

一个同时插入多行记录的INSERT语句等同于多个单行插入的INSERT语句,但是多行的INSERT语句在处理过程中效率更高。因为MySQL执行单条INSERT语句插入多行数据比使用多条INSERT语句快,所以在插入多条记录时最好选择使用单条INSERT语句的方式插入。

小结:

  • VALUES也可以写成VALUE,但是VALUES是标准写法。
  • 字符和日期型数据应包含在单引号中。

方式2:将查询结果插入到表中

INSERT还可以将SELECT语句查询的结果插入到表中,此时不需要把每一条记录的值一个一个输入,只需要使用一条INSERT语句和一条SELECT语句组成的组合语句即可快速地从一个或多个表中向一个表中插入多行。

基本语法格式如下:

1
2
3
4
5
6
INSERT INTO 目标表名
(tar_column1 [, tar_column2, …, tar_columnn])
SELECT
(src_column1 [, src_column2, …, src_columnn])
FROM 源表名
[WHERE condition]
  • INSERT语句中加入子查询。
  • 不必书写 VALUES 子句。
  • 子查询中的值列表应与INSERT子句中的列名对应。

举例:

1
2
3
4
INSERT INTO sales_reps(id, name, salary, commission_pct)
SELECT employee_id, last_name, salary, commission_pct
FROM employees
WHERE job_id LIKE '%REP%';

更新数据

  • 使用UPDATE语句更新数据。语法如下:
1
2
3
UPDATE table_name
SET column1=value1, column2=value2, … , column=valuen
[WHERE condition]
  • 可以一次更新多条数据。

  • 如果需要回滚数据,需要保证在DML前,进行设置:SET AUTOCOMMIT = FALSE;

  • 使用 WHERE 子句指定需要更新的数据。

1
2
3
UPDATE employees
SET department_id = 70
WHERE employee_id = 113;
  • 如果省略WHERE子句,则表中的所有数据都将被更新。
1
2
UPDATE 	copy_emp
SET department_id = 110;

删除数据

  • 使用DELETE语句从表中删除数据
1
DELETE FROM table_name [WHERE <condition>];

table_name指定要执行删除操作的表;[WHERE <condition>]为可选参数,指定删除条件,如果没有WHERE子句,DELETE语句将删除表中的所有记录。

  • 使用 WHERE 子句删除指定的记录。
1
2
DELETE FROM departments
WHERE department_name = 'Finance';
  • 如果省略 WHERE 子句,则表中的全部数据将被删除
1
DELETE FROM  copy_emp;

MySQL中的数据类型

类型 类型举例
整数类型 TINYINT、SMALLINT、MEDIUMINT、INT(或INTEGER)、BIGINT
浮点类型 FLOAT、DOUBLE
定点数类型 DECIMAL
位类型 BIT
日期时间类型 YEAR、TIME、DATE、DATETIME、TIMESTAMP
文本字符串类型 CHAR、VARCHAR、TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT
枚举类型 ENUM
集合类型 SET
二进制字符串类型 BINARY、VARBINARY、TINYBLOB、BLOB、MEDIUMBLOB、LONGBLOB
JSON类型 JSON对象、JSON数组
空间数据类型 单值类型:GEOMETRY、POINT、LINESTRING、POLYGON;
集合类型:MULTIPOINT、MULTILINESTRING、MULTIPOLYGON、GEOMETRYCOLLECTION

常见数据类型的属性,如下:

MySQL关键字 含义
NULL 数据列可包含NULL值
NOT NULL 数据列不允许包含NULL值
DEFAULT 默认值
PRIMARY KEY 主键
AUTO_INCREMENT 自动递增,适用于整数类型
UNSIGNED 无符号
CHARACTER SET name 指定一个字符集

整数类型

类型介绍

整数类型一共有 5 种,包括 TINYINT、SMALLINT、MEDIUMINT、INT(INTEGER)和 BIGINT。

它们的区别如下表所示:

整数类型 字节 有符号数取值范围 无符号数取值范围
TINYINT 1 -128~127 0~255
SMALLINT 2 -32768~32767 0~65535
MEDIUMINT 3 -8388608~8388607 0~16777215
INT、INTEGER 4 -2147483648~2147483647 0~4294967295
BIGINT 8 -9223372036854775808~9223372036854775807 0~18446744073709551615

适用场景

TINYINT:一般用于枚举数据,比如系统设定取值范围很小且固定的场景。

SMALLINT:可以用于较小范围的统计数据,比如统计工厂的固定资产库存数量等。

MEDIUMINT:用于较大整数的计算,比如车站每日的客流量等。

INTINTEGER:取值范围足够大,一般情况下不用考虑超限问题,用得最多。比如商品编号。

BIGINT:只有当你处理特别巨大的整数时才会用到。比如双十一的交易量、大型门户网站点击量、证券公司衍生产品持仓等。

如何选择?

在评估用哪种整数类型的时候,你需要考虑存储空间可靠性的平衡问题:一方 面,用占用字节数少的整数类型可以节省存储空间;另一方面,要是为了节省存储空间, 使用的整数类型取值范围太小,一旦遇到超出取值范围的情况,就可能引起系统错误,影响可靠性。

举个例子,商品编号采用的数据类型是INT。原因就在于,客户门店中流通的商品种类较多,而且,每天都有旧商品下架,新商品上架,这样不断迭代,日积月累。

如果使用SMALLINT类型,虽然占用字节数比INT类型的整数少,但是却不能保证数据不会超出范围 65535。相反,使用 INT,就能确保有足够大的取值范围,不用担心数据超出范围影响可靠性的问题。

你要注意的是,在实际工作中,系统故障产生的成本远远超过增加几个字段存储空间所产生的成本。因此,我建议你首先确保数据不会超过取值范围,在这个前提之下,再去考虑如何节省存储空间。

浮点类型

类型介绍

浮点数和定点数类型的特点是可以处理小数,你可以把整数看成小数的一个特例。因此,浮点数和定点数的使用场景,比整数大多了。 MySQL支持的浮点数类型,分别是FLOATDOUBLEREAL

  • FLOAT表示单精度浮点数;

  • DOUBLE表示双精度浮点数;

  • REAL默认就是DOUBLE。如果你把 SQL 模式设定为启用“REAL_AS_FLOAT”,那 么,MySQL 就认为 REAL 是 FLOAT。如果要启用“REAL_AS_FLOAT”,可以通过以下 SQL 语句实现:

    1
    SET sql_mode = “REAL_AS_FLOAT”;

问题1:FLOAT 和 DOUBLE 这两种数据类型的区别是啥呢?

FLOAT占用字节数少,取值范围小;DOUBLE占用字节数多,取值范围也大。

问题2:为什么浮点数类型的无符号数取值范围,只相当于有符号数取值范围的一半,也就是只相当于有符号数取值范围大于等于零的部分呢?

MySQL 存储浮点数的格式为:符号(S)尾数(M)阶码(E)。因此,无论有没有符号,MySQL 的浮点数都会存储表示符号的部分。因此, 所谓的无符号数取值范围,其实就是有符号数取值范围大于等于零的部分。

精度误差说明

浮点数类型有个缺陷,就是不精准。下面我来重点解释一下为什么 MySQL 的浮点数不够精准。比如,我们设计一个表,有f1这个字段,插入值分别为0.47,0.44,0.19,我们期待的运行结果是:0.47 + 0.44 + 0.19 = 1.1。而使用sum之后查询:

1
2
3
4
5
6
CREATE TABLE test_double2(
f1 DOUBLE
);

INSERT INTO test_double2
VALUES(0.47),(0.44),(0.19);
1
2
3
4
5
6
7
8
mysql> SELECT SUM(f1)
-> FROM test_double2;
+--------------------+
| SUM(f1) |
+--------------------+
| 1.0999999999999999 |
+--------------------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
mysql> SELECT SUM(f1) = 1.1,1.1 = 1.1
-> FROM test_double2;
+---------------+-----------+
| SUM(f1) = 1.1 | 1.1 = 1.1 |
+---------------+-----------+
| 0 | 1 |
+---------------+-----------+
1 row in set (0.00 sec)

查询结果是 1.0999999999999999。看到了吗?虽然误差很小,但确实有误差。 你也可以尝试把数据类型改成 FLOAT,然后运行求和查询,得到的是, 1.0999999940395355。显然,误差更大了。

那么,为什么会存在这样的误差呢?问题还是出在 MySQL 对浮点类型数据的存储方式上。

MySQL 用 4 个字节存储 FLOAT 类型数据,用 8 个字节来存储 DOUBLE 类型数据。无论哪个,都是采用二进制的方式来进行存储的。比如 9.625,用二进制来表达,就是 1001.101,或者表达成 1.001101×2^3。如果尾数不是 0 或 5(比如 9.624),你就无法用一个二进制数来精确表达。进而,就只好在取值允许的范围内进行四舍五入。

在编程中,如果用到浮点数,要特别注意误差问题,因为浮点数是不准确的,所以我们要避免使用“=”来判断两个数是否相等。同时,在一些对精确度要求较高的项目中,千万不要使用浮点数,不然会导致结果错误,甚至是造成不可挽回的损失。那么,MySQL 有没有精准的数据类型呢?当然有,这就是定点数类型:DECIMAL

定点数类型

类型介绍

  • MySQL中的定点数类型只有 DECIMAL 一种类型。

    数据类型 字节数 含义
    DECIMAL(M,D),DEC,NUMERIC M+2字节 有效范围由M和D决定

    使用DECIMAL(M,D)的方式表示高精度小数。其中,M被称为精度,D被称为标度。0<=M<=65,0<=D<=30,D<M。例如,定义DECIMAL(5, 2)的类型,表示该列取值范围是-999.99~999.99。

  • DECIMAL(M,D)的最大取值范围与DOUBLE类型一样,但是有效的数据范围是由M和D决定的。DECIMAL 的存储空间并不是固定的,由精度值M决定,总共占用的存储空间为M+2个字节。也就是说,在一些对精度要求不高的场景下,比起占用同样字节长度的定点数,浮点数表达的数值范围可以更大一些。

  • 定点数在MySQL内部是以字符串的形式进行存储,这就决定了它一定是精准的。

  • 当DECIMAL类型不指定精度和标度时,其默认为DECIMAL(10,0)。当数据的精度超出了定点数类型的精度范围时,则MySQL同样会进行四舍五入处理。

  • 浮点数 vs 定点数

    • 浮点数相对于定点数的优点是在长度一定的情况下,浮点类型取值范围大,但是不精准,适用于需要取值范围大,又可以容忍微小误差的科学计算场景(比如计算化学、分子建模、流体动力学等)
    • 定点数类型取值范围相对小,但是精准,没有误差,适合于对精度要求极高的场景 (比如涉及金额计算的场景)
  • 举例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    CREATE TABLE test_decimal1(
    f1 DECIMAL,
    f2 DECIMAL(5,2)
    );

    DESC test_decimal1;

    INSERT INTO test_decimal1(f1,f2)
    VALUES(123.123,123.456);

    #Out of range value for column 'f2' at row 1
    INSERT INTO test_decimal1(f2)
    VALUES(1234.34);
    1
    2
    3
    4
    5
    6
    7
    mysql> SELECT * FROM test_decimal1;
    +------+--------+
    | f1 | f2 |
    +------+--------+
    | 123 | 123.46 |
    +------+--------+
    1 row in set (0.00 sec)
  • 举例

    我们运行下面的语句,把test_double2表中字段“f1”的数据类型修改为 DECIMAL(5,2):

    1
    2
    ALTER TABLE test_double2
    MODIFY f1 DECIMAL(5,2);

    然后,我们再一次运行求和语句:

    1
    2
    3
    4
    5
    6
    7
    8
    mysql> SELECT SUM(f1)
    -> FROM test_double2;
    +---------+
    | SUM(f1) |
    +---------+
    | 1.10 |
    +---------+
    1 row in set (0.00 sec)
    1
    2
    3
    4
    5
    6
    7
    8
    mysql> SELECT SUM(f1) = 1.1
    -> FROM test_double2;
    +---------------+
    | SUM(f1) = 1.1 |
    +---------------+
    | 1 |
    +---------------+
    1 row in set (0.00 sec)

开发中经验

“由于 DECIMAL 数据类型的精准性,在我们的项目中,除了极少数(比如商品编号)用到整数类型外,其他的数值都用的是 DECIMAL,原因就是这个项目所处的零售行业,要求精准,一分钱也不能差。 ” ——来自某项目经理

BIT

BIT类型中存储的是二进制值,类似010110。

二进制字符串类型 长度 长度范围 占用空间
BIT(M) M 1 <= M <= 64 约为(M + 7)/8个字节

BIT类型,如果没有指定(M),默认是1位。这个1位,表示只能存1位的二进制值。这里(M)是表示二进制的位数,位数最小值为1,最大值为64。

日期与时间类型

日期与时间是重要的信息,在我们的系统中,几乎所有的数据表都用得到。原因是客户需要知道数据的时间标签,从而进行数据查询、统计和处理。

MySQL有多种表示日期和时间的数据类型,不同的版本可能有所差异,MySQL8.0版本支持的日期和时间类型主要有:YEAR类型、TIME类型、DATE类型、DATETIME类型和TIMESTAMP类型。

  • YEAR类型通常用来表示年
  • DATE类型通常用来表示年、月、日
  • TIME类型通常用来表示时、分、秒
  • DATETIME类型通常用来表示年、月、日、时、分、秒
  • TIMESTAMP类型通常用来表示带时区的年、月、日、时、分、秒
类型 名称 字节 日期格式 最小值 最大值
YEAR 1 YYYY或YY 1901 2155
TIME 时间 3 HH:MM:SS -838:59:59 838:59:59
DATE 日期 3 YYYY-MM-DD 1000-01-01 9999-12-03
DATETIME 日期时间 8 YYYY-MM-DD HH:MM:SS 1000-01-01 00:00:00 9999-12-31 23:59:59
TIMESTAMP 日期时间 4 YYYY-MM-DD HH:MM:SS 1970-01-01 00:00:00 UTC 2038-01-19 03:14:07UTC

可以看到,不同数据类型表示的时间内容不同、取值范围不同,而且占用的字节数也不一样,你要根据实际需要灵活选取。

为什么时间类型 TIME 的取值范围不是 -23:59:59~23:59:59 呢?原因是 MySQL 设计的 TIME 类型,不光表示一天之内的时间,而且可以用来表示一个时间间隔,这个时间间隔可以超过 24 小时。

开发中经验

用得最多的日期时间类型,就是DATETIME。虽然 MySQL 也支持 YEAR(年)、 TIME(时间)、DATE(日期),以及 TIMESTAMP 类型,但是在实际项目中,尽量用DATETIME类型。因为这个数据类型包括了完整的日期和时间信息,取值范围也最大,使用起来比较方便。毕竟,如果日期时间信息分散在好几个字段,很不容易记,而且查询的时候,SQL 语句也会更加复杂。

此外,一般存注册时间、商品发布时间等,不建议使用DATETIME存储,而是使用时间戳,因为DATETIME虽然直观,但不便于计算。

1
2
3
4
5
6
7
mysql> SELECT UNIX_TIMESTAMP();
+------------------+
| UNIX_TIMESTAMP() |
+------------------+
| 1635932762 |
+------------------+
1 row in set (0.00 sec)

文本字符串类型

在实际的项目中,我们还经常遇到一种数据,就是字符串数据。

MySQL中,文本字符串总体上分为CHARVARCHARTINYTEXTTEXTMEDIUMTEXTLONGTEXTENUMSET等类型。

文本字符串类型 值的长度 长度范围 占用的存储空间
CHAR(M) M 0 <= M <= 255 M个子节
VARCHAR(M) M 0 <= M <= 65535 M+l个字节
TINYTEXT L 0 <= L <= 255 L+2个字节
TEXT L 0 <= L <= 65535 L+2个字节
MEDIUMTEXT L 0 <= L <= 16777215 L+3个字节
LONGTEXT L 0 <= L <= 4294967295 L+4个字节
ENUM L 1 <= L <= 65535 1或2个字节
SET L 0 <= L <= 64 1,2,3,4或8个字节

CHAR与VARCHAR类型

CHARVARCHAR类型都可以存储比较短的字符串。

字符串(文本)类型 特点 长度 长度范围 占用的存储空间
CHAR(M) 固定长度 M 0 <= M <= 255 M个字节
VARCHAR(M) 可变长度 M 0 <= M <= 65535 (实际长度 + 1) 个字节

CHAR类型:

  • CHAR(M)类型一般需要预先定义字符串长度。如果不指定(M),则表示长度默认是1个字符
  • 如果保存时,数据的实际长度比CHAR类型声明的长度小,则会在右侧填充空格以达到指定的长度。当MySQL检索CHAR类型的数据时,CHAR类型的字段会去除尾部的空格。
  • 定义CHAR类型字段时,声明的字段长度即为CHAR类型字段所占的存储空间的字节数。

VARCHAR类型:

  • VARCHAR(M) 定义时,必须指定长度M,否则报错。
  • MySQL4.0版本以下,varchar(20):指的是20字节,如果存放UTF8汉字时,只能存6个(每个汉字3字节) ;MySQL5.0版本以上,varchar(20):指的是20字符。
  • 检索VARCHAR类型的字段数据时,会保留数据尾部的空格。VARCHAR类型的字段所占用的存储空间为字符串实际长度加1个字节。

哪些情况使用 CHAR 或 VARCHAR 更好

类型 特点 空间上 时间上 适用场景
CHAR(M) 固定长度 浪费存储空间 效率高 存储不大,速度要求高
VARCHAR(M) 可变长度 节省存储空间 效率低 非CHAR的情况

情况1:存储很短的信息。比如门牌号码101,201……这样很短的信息应该用char,因为varchar还要占个byte用于存储信息长度,本来打算节约存储的,结果得不偿失。

情况2:固定长度的。比如使用uuid作为主键,那用char应该更合适。因为他固定长度,varchar动态根据长度的特性就消失了,而且还要占个长度信息。

情况3:十分频繁改变的column。因为varchar每次存储都要有额外的计算,得到长度等工作,如果一个非常频繁改变的,那就要有很多的精力用于计算,而这些对于char来说是不需要的。

情况4:具体存储引擎中的情况:

  • MyISAM 数据存储引擎和数据列:MyISAM数据表,最好使用固定长度(CHAR)的数据列代替可变长度(VARCHAR)的数据列。这样使得整个表静态化,从而使数据检索更快,用空间换时间。

  • MEMORY 存储引擎和数据列:MEMORY数据表目前都使用固定长度的数据行存储,因此无论使用CHAR或VARCHAR列都没有关系,两者都是作为CHAR类型处理的。

  • InnoDB存储引擎,建议使用VARCHAR类型。因为对于InnoDB数据表,内部的行存储格式并没有区分固定长度和可变长度列(所有数据行都使用指向数据列值的头指针),而且主要影响性能的因素是数据行使用的存储总量,由于char平均占用的空间多于varchar,所以除了简短并且固定长度的,其他考虑varchar。这样节省空间,对磁盘I/O和数据存储总量比较好。

TEXT类型

在MySQL中,TEXT用来保存文本类型的字符串,总共包含4种类型,分别为TINYTEXTTEXTMEDIUMTEXTLONGTEXT类型。

在向TEXT类型的字段保存和查询数据时,系统自动按照实际长度存储,不需要预先定义长度。这一点和 VARCHAR类型相同。

每种TEXT类型保存的数据长度和所占用的存储空间不同,如下:

文本字符串类型 特点 长度 长度范围 占用的存储空间
TINYTEXT 小文本、可变长度 L 0 <= L <= 255 L + 2 个字节
TEXT 文本、可变长度 L 0 <= L <= 65535 L + 2 个字节
MEDIUMTEXT 中等文本、可变长度 L 0 <= L <= 16777215 L + 3 个字节
LONGTEXT 大文本、可变长度 L 0 <= L<= 4294967295(相当于4GB) L + 4 个字节

由于实际存储的长度不确定,MySQL 不允许 TEXT 类型的字段做主键。遇到这种情况,你只能采用 CHAR(M),或者 VARCHAR(M)。

开发中经验:

TEXT文本类型,可以存比较大的文本段,搜索速度稍慢,因此如果不是特别大的内容,建议使用CHAR,VARCHAR来代替。还有TEXT类型不用加默认值,加了也没用。而且text和blob类型的数据删除后容易导致“空洞”,使得文件碎片比较多,所以频繁使用的表不建议包含TEXT类型字段,建议单独分出去,单独用一个表。

ENUM类型

ENUM类型也叫作枚举类型,ENUM类型的取值范围需要在定义字段时进行指定。设置字段值时,ENUM类型只允许从成员中选取单个值,不能一次选取多个值。

其所需要的存储空间由定义ENUM类型时指定的成员个数决定。

文本字符串类型 长度 长度范围 占用的存储空间
ENUM L 1 <= L <= 65535 1或2个字节
  • 当ENUM类型包含1~255个成员时,需要1个字节的存储空间;

  • 当ENUM类型包含256~65535个成员时,需要2个字节的存储空间。

  • ENUM类型的成员个数的上限为65535个。

举例:

创建表如下:

1
2
3
CREATE TABLE test_enum(
season ENUM('春','夏','秋','冬','unknow')
);

SET类型

SET表示一个字符串对象,可以包含0个或多个成员,但成员个数的上限为64。设置字段值时,可以取取值范围内的 0 个或多个值。

当SET类型包含的成员个数不同时,其所占用的存储空间也是不同的,具体如下:

成员个数范围(L表示实际成员个数) 占用的存储空间
1 <= L <= 8 1个字节
9 <= L <= 16 2个字节
17 <= L <= 24 3个字节
25 <= L <= 32 4个字节
33 <= L <= 64 8个字节

SET类型在存储数据时成员个数越多,其占用的存储空间越大。注意:SET类型在选取成员时,可以一次选择多个成员,这一点与ENUM类型不同。

举例:

创建表:

1
2
3
CREATE TABLE test_set(
s SET ('A', 'B', 'C')
);

向表中插入数据:

1
2
3
4
5
6
7
8
9
10
INSERT INTO test_set (s) VALUES ('A'), ('A,B');

#插入重复的SET类型成员时,MySQL会自动删除重复的成员
INSERT INTO test_set (s) VALUES ('A,B,C,A');

#向SET类型的字段插入SET成员中不存在的值时,MySQL会抛出错误。
INSERT INTO test_set (s) VALUES ('A,B,C,D');

SELECT *
FROM test_set;

二进制字符串类型

MySQL中的二进制字符串类型主要存储一些二进制数据,比如可以存储图片、音频和视频等二进制数据。

MySQL中支持的二进制字符串类型主要包括BINARYVARBINARYTINYBLOBBLOBMEDIUMBLOBLONGBLOB类型。

BINARY与VARBINARY类型

BINARYVARBINARY类似于CHAR和VARCHAR,只是它们存储的是二进制字符串。

BINARY (M)为固定长度的二进制字符串,M表示最多能存储的字节数,取值范围是0~255个字符。如果未指定(M),表示只能存储1个字节。例如BINARY (8),表示最多能存储8个字节,如果字段值不足(M)个字节,将在右边填充’\0’以补齐指定长度。

VARBINARY (M)为可变长度的二进制字符串,M表示最多能存储的字节数,总字节数不能超过行的字节长度限制65535,另外还要考虑额外字节开销,VARBINARY类型的数据除了存储数据本身外,还需要1或2个字节来存储数据的字节数。VARBINARY类型必须指定(M),否则报错。

二进制字符串类型 特点 值的长度 占用空间
BINARY(M) 固定长度 M (0 <= M <= 255) M个字节
VARBINARY(M) 可变长度 M(0 <= M <= 65535) M+1个字节

BLOB类型

BLOB是一个二进制大对象,可以容纳可变数量的数据。

MySQL中的BLOB类型包括TINYBLOB、BLOB、MEDIUMBLOB和LONGBLOB 4种类型,它们可容纳值的最大长度不同。可以存储一个二进制的大对象,比如图片音频视频等。

需要注意的是,在实际工作中,往往不会在MySQL数据库中使用BLOB类型存储大对象数据,通常会将图片、音频和视频文件存储到服务器的磁盘上,并将图片、音频和视频的访问路径存储到MySQL中。

二进制字符串类型 值的长度 长度范围 占用空间
TINYBLOB L 0 <= L <= 255 L + 1 个字节
BLOB L 0 <= L <= 65535(相当于64KB) L + 2 个字节
MEDIUMBLOB L 0 <= L <= 16777215 (相当于16MB) L + 3 个字节
LONGBLOB L 0 <= L <= 4294967295(相当于4GB) L + 4 个字节

举例:

1
2
3
4
CREATE TABLE test_blob1(
id INT,
img MEDIUMBLOB
);

TEXT和BLOB的使用注意事项:

在使用textblob字段类型时要注意以下几点,以便更好的发挥数据库的性能。

① BLOB和TEXT值也会引起自己的一些问题,特别是执行了大量的删除或更新操作的时候。删除这种值会在数据表中留下很大的”空洞“,以后填入这些”空洞”的记录可能长度不同。为了提高性能,建议定期使用 OPTIMIZE TABLE 功能对这类表进行碎片整理

② 如果需要对大文本字段进行模糊查询,MySQL 提供了前缀索引。但是仍然要在不必要的时候避免检索大型的BLOB或TEXT值。例如,SELECT * 查询就不是很好的想法,除非你能够确定作为约束条件的WHERE子句只会找到所需要的数据行。否则,你可能毫无目的地在网络上传输大量的值。

③ 把BLOB或TEXT列分离到单独的表中。在某些环境中,如果把这些数据列移动到第二张数据表中,可以让你把原数据表中的数据列转换为固定长度的数据行格式,那么它就是有意义的。这会减少主表中的碎片,使你得到固定长度数据行的性能优势。它还使你在主数据表上运行 SELECT * 查询的时候不会通过网络传输大量的BLOB或TEXT值。

JSON 类型

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。它易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。JSON 可以将 JavaScript 对象中表示的一组数据转换为字符串,然后就可以在网络或者程序之间轻松地传递这个字符串,并在需要的时候将它还原为各编程语言所支持的数据格式。

在MySQL 5.7中,就已经支持JSON数据类型。在MySQL 8.x版本中,JSON类型提供了可以进行自动验证的JSON文档和优化的存储结构,使得在MySQL中存储和读取JSON类型的数据更加方便和高效。
创建数据表,表中包含一个JSON类型的字段 js 。

1
2
3
4
CREATE TABLE test_json(
js json

);

向表中插入JSON数据。

1
2
INSERT INTO test_json (js) 
VALUES ('{"name":"songhk", "age":18, "address":{"province":"beijing", "city":"beijing"}}');

当需要检索JSON类型的字段中数据的某个具体值时,可以使用“->”和“->>”符号。

1
2
3
4
5
6
7
8
mysql> SELECT js -> '$.name' AS NAME,js -> '$.age' AS age ,js -> '$.address.province' AS province, js -> '$.address.city' AS city
-> FROM test_json;
+----------+------+-----------+-----------+
| NAME | age | province | city |
+----------+------+-----------+-----------+
| "songhk" | 18 | "beijing" | "beijing" |
+----------+------+-----------+-----------+
1 row in set (0.00 sec)

通过“->”和“->>”符号,从JSON字段中正确查询出了指定的JSON数据的值。

数据类型选择建议

在定义数据类型时,如果确定是整数,就用INT; 如果是小数,一定用定点数类型 DECIMAL(M,D); 如果是日期与时间,就用DATETIME

这样做的好处是,首先确保你的系统不会因为数据类型定义出错。不过,凡事都是有两面的,可靠性好,并不意味着高效。比如,TEXT 虽然使用方便,但是效率不如 CHAR(M) 和 VARCHAR(M)。

关于字符串的选择,建议参考如下阿里巴巴的《Java开发手册》规范:

阿里巴巴《Java开发手册》之MySQL数据库:

  • 任何字段如果为非负数,必须是UNSIGNED
  • 强制】小数类型为DECIMAL,禁止使用FLOATDOUBLE
    • 说明:在存储的时候,FLOATDOUBLE都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过DECIMAL的范围,建议将数据拆成整数和小数并分开存储。
  • 强制】如果存储的字符串长度几乎相等,使用CHAR定长字符串类型。
  • 强制VARCHAR是可变长字符串,不预先分配存储空间,长度不要超过 5000。如果存储长度大于此值,定义字段类型为TEXT,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

约束

为什么需要约束

数据完整性(Data Integrity)是指数据的精确性(Accuracy)和可靠性(Reliability)。它是防止数据库中存在不符合语义规定的数据和防止因错误信息的输入输出造成无效操作或错误信息而提出的。

为了保证数据的完整性,SQL规范以约束的方式对表数据进行额外的条件限制。从以下四个方面考虑:

  • 实体完整性(Entity Integrity):例如,同一个表中,不能存在两条完全相同无法区分的记录
  • 域完整性(Domain Integrity):例如:年龄范围0-120,性别范围“男/女”
  • 引用完整性(Referential Integrity):例如:员工所在部门,在部门表中要能找到这个部门
  • 用户自定义完整性(User-defined Integrity):例如:用户名唯一、密码不能为空等,本部门经理的工资不得高于本部门职工的平均工资的5倍。

什么是约束

约束是表级的强制规定。

可以在创建表时规定约束(通过 CREATE TABLE 语句),或者在表创建之后通过 ALTER TABLE 语句规定约束

约束的分类

  • 根据约束数据列的限制,约束可分为:
    • 单列约束:每个约束只约束一列
    • 多列约束:每个约束可约束多列数据
  • 根据约束的作用范围,约束可分为:
    • 列级约束:只能作用在一个列上,跟在列的定义后面
    • 表级约束:可以作用在多个列上,不与列一起,而是单独定义
位置 支持的约束类型 是否可以起约束名
列级约束: 列的后面 语法都支持,但外键没有效果 不可以
表级约束: 所有列的下面 默认和非空不支持,其他支持 可以(主键没有效果)
  • 根据约束起的作用,约束可分为:
    • NOT NULL 非空约束,规定某个字段不能为空
    • UNIQUE 唯一约束规定某个字段在整个表中是唯一的
    • PRIMARY KEY 主键(非空且唯一)约束
    • FOREIGN KEY 外键约束
    • CHECK 检查约束
    • DEFAULT 默认值约束

注意: MySQL不支持check约束,但可以使用check约束,而没有任何效果

  • 查看某个表已有的约束
1
2
3
4
#information_schema数据库名(系统库)
#table_constraints表名称(专门存储各个表的约束)
SELECT * FROM information_schema.table_constraints
WHERE table_name = '表名称';

非空约束

限定某个字段/某列的值不允许为空

特点

  • 默认,所有的类型的值都可以是NULL,包括INTFLOAT等数据类型

  • 非空约束只能出现在表对象的列上,只能某个列单独限定非空,不能组合非空

  • 一个表可以有很多列都分别限定了非空

  • 空字符串''不等于NULL0也不等于NULL

添加非空约束

(1)建表时

1
2
3
4
5
CREATE TABLE 表名称(
字段名 数据类型,
字段名 数据类型 NOT NULL,
字段名 数据类型 NOT NULL
);

举例:

1
2
3
4
5
CREATE TABLE emp(
id INT(10) NOT NULL,
NAME VARCHAR(20) NOT NULL,
sex CHAR NULL
);

(2)建表后

1
alter table 表名称 modify 字段名 数据类型 not null;

举例:

1
2
ALTER TABLE emp
MODIFY sex VARCHAR(30) NOT NULL;
1
alter table student modify sname varchar(20) not null;

删除非空约束

1
2
3
4
5
alter table 表名称 modify 字段名 数据类型 NULL;#去掉not null,相当于修改某个非注解字段,该字段允许为空



alter table 表名称 modify 字段名 数据类型;#去掉not null,相当于修改某个非注解字段,该字段允许为空

举例:

1
2
ALTER TABLE emp
MODIFY sex VARCHAR(30) NULL;
1
2
ALTER TABLE emp
MODIFY NAME VARCHAR(15) DEFAULT 'abc' NULL;

唯一性约束

用来限制某个字段/某列的值不能重复。

特点

  • 同一个表可以有多个唯一约束。
  • 唯一约束可以是某一个列的值唯一,也可以多个列组合的值唯一。
  • 唯一性约束允许列值为空。
  • 在创建唯一约束的时候,如果不给唯一约束命名,就默认和列名相同。
  • MySQL会给唯一约束的列上默认创建一个唯一索引。

添加唯一约束

(1)建表时

1
2
3
4
5
6
7
8
9
10
11
12
create table 表名称(
字段名 数据类型,
字段名 数据类型 unique,
字段名 数据类型 unique key,
字段名 数据类型
);
create table 表名称(
字段名 数据类型,
字段名 数据类型,
字段名 数据类型,
[constraint 约束名] unique key(字段名)
);

举例:

1
2
3
4
5
6
create table student(
sid int,
sname varchar(20),
tel char(11) unique,
cardid char(18) unique key
);
1
2
3
4
5
6
7
CREATE TABLE USER(
id INT NOT NULL,
NAME VARCHAR(25),
PASSWORD VARCHAR(16),
-- 使用表级约束语法
CONSTRAINT uk_name_pwd UNIQUE(NAME,PASSWORD)
);

表示用户名和密码组合不能重复

(2)建表后指定唯一键约束

1
2
3
#字段列表中如果是一个字段,表示该列的值唯一。如果是两个或更多个字段,那么复合唯一,即多个字段的组合是唯一的
#方式1:
alter table 表名称 add unique key(字段列表);
1
2
#方式2:
alter table 表名称 modify 字段名 字段类型 unique;

举例:

1
2
ALTER TABLE USER 
ADD UNIQUE(NAME,PASSWORD);
1
2
ALTER TABLE USER 
ADD CONSTRAINT uk_name_pwd UNIQUE(NAME,PASSWORD);
1
2
ALTER TABLE USER 
MODIFY NAME VARCHAR(20) UNIQUE;

举例:

1
2
3
4
5
6
create table student(
sid int primary key,
sname varchar(20),
tel char(11) ,
cardid char(18)
);
1
2
alter table student add unique key(tel);
alter table student add unique key(cardid);

关于复合唯一约束

1
2
3
4
5
6
create table 表名称(
字段名 数据类型,
字段名 数据类型,
字段名 数据类型,
unique key(字段列表) #字段列表中写的是多个字段名,多个字段名用逗号分隔,表示那么是复合唯一,即多个字段的组合是唯一的
);

删除唯一约束

  • 添加唯一性约束的列上也会自动创建唯一索引。
  • 删除唯一约束只能通过删除唯一索引的方式删除。
  • 删除时需要指定唯一索引名,唯一索引名就和唯一约束名一样。
  • 如果创建唯一约束时未指定名称,如果是单列,就默认和列名相同;如果是组合列,那么默认和()中排在第一个的列名相同。也可以自定义唯一性约束名。
1
SELECT * FROM information_schema.table_constraints WHERE table_name = '表名'; #查看都有哪些约束
1
2
ALTER TABLE USER 
DROP INDEX uk_name_pwd;

注意:可以通过 show index from 表名称; 查看表的索引


PRIMARY KEY 约束

用来唯一标识表中的一行记录。

特点

  • 主键约束相当于唯一约束+非空约束的组合,主键约束列不允许重复,也不允许出现空值。

  • 一个表最多只能有一个主键约束,建立主键约束可以在列级别创建,也可以在表级别上创建。

  • 主键约束对应着表中的一列或者多列(复合主键)

  • 如果是多列组合的复合主键约束,那么这些列都不允许为空值,并且组合的值不允许重复。

  • MySQL的主键名总是PRIMARY,就算自己命名了主键约束名也没用。

  • 当创建主键约束时,系统默认会在所在的列或列组合上建立对应的主键索引(能够根据主键查询的,就根据主键查询,效率更高)。如果删除主键约束了,主键约束对应的索引就自动删除了。

  • 需要注意的一点是,不要修改主键字段的值。因为主键是数据记录的唯一标识,如果修改了主键的值,就有可能会破坏数据的完整性。

添加主键约束

1)建表时指定主键约束

1
2
3
4
5
6
7
8
9
10
11
create table 表名称(
字段名 数据类型 primary key, #列级模式
字段名 数据类型,
字段名 数据类型
);
create table 表名称(
字段名 数据类型,
字段名 数据类型,
字段名 数据类型,
[constraint 约束名] primary key(字段名) #表级模式
);

举例:

1
2
3
4
create table temp(
id int primary key,
name varchar(20)
);

再举例:

  • 列级约束
1
2
3
4
CREATE TABLE emp4(
id INT PRIMARY KEY AUTO_INCREMENT ,
NAME VARCHAR(20)
);
  • 表级约束
1
2
3
4
5
6
CREATE TABLE emp5(
id INT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(20),
pwd VARCHAR(15),
CONSTRAINT emp5_id_pk PRIMARY KEY(id)
);

(2)建表后增加主键约束

1
ALTER TABLE 表名称 ADD PRIMARY KEY(字段列表); #字段列表可以是一个字段,也可以是多个字段,如果是多个字段的话,是复合主键
1
ALTER TABLE student ADD PRIMARY KEY (sid);
1
ALTER TABLE emp5 ADD PRIMARY KEY(NAME,pwd);

删除主键约束

1
alter table 表名称 drop primary key;

举例:

1
ALTER TABLE student DROP PRIMARY KEY;
1
ALTER TABLE emp5 DROP PRIMARY KEY;

说明:删除主键约束,不需要指定主键名,因为一个表只有一个主键,删除主键约束后,非空还存在。


自增列:AUTO_INCREMENT

某个字段的值自增

特点和要求

(1)一个表最多只能有一个自增长列

(2)当需要产生唯一标识符或顺序值时,可设置自增长

(3)自增长列约束的列必须是键列(主键列,唯一键列)

(4)自增约束的列的数据类型必须是整数类型

(5)如果自增列指定了 0 和 null,会在当前最大值的基础上自增;如果自增列手动指定了具体值,直接赋值为具体值。

如何指定自增约束

(1)建表时

1
2
3
4
5
6
7
8
9
10
11
12
create table 表名称(
字段名 数据类型 primary key auto_increment,
字段名 数据类型 unique key not null,
字段名 数据类型 unique key,
字段名 数据类型 not null default 默认值,
);
create table 表名称(
字段名 数据类型 default 默认值 ,
字段名 数据类型 unique key auto_increment,
字段名 数据类型 not null default 默认值,,
primary key(字段名)
);
1
2
3
4
create table employee(
eid int primary key auto_increment,
ename varchar(20)
);
1
2
3
4
5
6
7
8
mysql> desc employee;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| eid | int(11) | NO | PRI | NULL | auto_increment |
| ename | varchar(20) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

(2)建表后

1
alter table 表名称 modify 字段名 数据类型 auto_increment;

例如:

1
2
3
4
create table employee(
eid int primary key ,
ename varchar(20)
);
1
alter table employee modify eid int auto_increment;
1
2
3
4
5
6
7
8
mysql> desc employee;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| eid | int(11) | NO | PRI | NULL | auto_increment |
| ename | varchar(20) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)

如何删除自增约束

1
2
3
#alter table 表名称 modify 字段名 数据类型 auto_increment;#给这个字段增加自增约束

alter table 表名称 modify 字段名 数据类型; #去掉auto_increment相当于删除
1
alter table employee modify eid int;
1
2
3
4
5
6
7
8
mysql> desc employee;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| eid | int(11) | NO | PRI | NULL | |
| ename | varchar(20) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

FOREIGN KEY 约束

限定某个表的某个字段的引用完整性。

比如:员工表的员工所在部门的选择,必须在部门表能找到对应的部分。

主表和从表/父表和子表

主表(父表):被引用的表,被参考的表

从表(子表):引用别人的表,参考别人的表

例如:员工表的员工所在部门这个字段的值要参考部门表:部门表是主表,员工表是从表。

例如:学生表、课程表、选课表:选课表的学生和课程要分别参考学生表和课程表,学生表和课程表是主表,选课表是从表。

特点

(1)从表的外键列,必须引用/参考主表的主键或唯一约束的列

​ 为什么?因为被依赖/被参考的值必须是唯一的

(2)在创建外键约束时,如果不给外键约束命名,默认名不是列名,而是自动产生一个外键名(例如 student_ibfk_1;),也可以指定外键约束名。

(3)创建(CREATE)表时就指定外键约束的话,先创建主表,再创建从表

(4)删表时,先删从表(或先删除外键约束),再删除主表

(5)当主表的记录被从表参照时,主表的记录将不允许删除,如果要删除数据,需要先删除从表中依赖该记录的数据,然后才可以删除主表的数据

(6)在“从表”中指定外键约束,并且一个表可以建立多个外键约束

(7)从表的外键列与主表被参照的列名字可以不相同,但是数据类型必须一样,逻辑意义一致。如果类型不一样,创建子表时,就会出现错误“ERROR 1005 (HY000): Can’t create table’database.tablename’(errno: 150)”。

​ 例如:都是表示部门编号,都是int类型。

(8)当创建外键约束时,系统默认会在所在的列上建立对应的普通索引。但是索引名是外键的约束名。(根据外键查询效率很高)

(9)删除外键约束后,必须手动删除对应的索引

添加外键约束

(1)建表时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
create table 主表名称(
字段1 数据类型 primary key,
字段2 数据类型
);

create table 从表名称(
字段1 数据类型 primary key,
字段2 数据类型,
[CONSTRAINT <外键约束名称>] FOREIGN KEY(从表的某个字段) references 主表名(被参考字段)
);
#(从表的某个字段)的数据类型必须与主表名(被参考字段)的数据类型一致,逻辑意义也一样
#(从表的某个字段)的字段名可以与主表名(被参考字段)的字段名一样,也可以不一样

-- FOREIGN KEY: 在表级指定子表中的列
-- REFERENCES: 标示在父表中的列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
create table dept( #主表
did int primary key, #部门编号
dname varchar(50) #部门名称
);

create table emp(#从表
eid int primary key, #员工编号
ename varchar(5), #员工姓名
deptid int, #员工所在的部门
foreign key (deptid) references dept(did) #在从表中指定外键约束
#emp表的deptid和和dept表的did的数据类型一致,意义都是表示部门的编号
);

说明:
(1)主表dept必须先创建成功,然后才能创建emp表,指定外键成功。
(2)删除表时,先删除从表emp,再删除主表dept

(2)建表后

一般情况下,表与表的关联都是提前设计好了的,因此,会在创建表的时候就把外键约束定义好。不过,如果需要修改表的设计(比如添加新的字段,增加新的关联关系),但没有预先定义外键约束,那么,就要用修改表的方式来补充定义。

格式:

1
ALTER TABLE 从表名 ADD [CONSTRAINT 约束名] FOREIGN KEY (从表的字段) REFERENCES 主表名(被引用字段) [on update xx][on delete xx];

举例:

1
2
ALTER TABLE emp1
ADD [CONSTRAINT emp_dept_id_fk] FOREIGN KEY(dept_id) REFERENCES dept(dept_id);

举例:

1
2
3
4
5
6
7
8
9
10
11
create table dept(
did int primary key, #部门编号
dname varchar(50) #部门名称
);

create table emp(
eid int primary key, #员工编号
ename varchar(5), #员工姓名
deptid int #员工所在的部门
);
#这两个表创建时,没有指定外键的话,那么创建顺序是随意
1
alter table emp add foreign key (deptid) references dept(did);

总结:约束关系是针对双方的

  • 添加了外键约束后,主表的修改和删除数据受约束

  • 添加了外键约束后,从表的添加和修改数据受约束

  • 在从表上建立外键,要求主表必须存在

  • 删除主表时,要求从表从表先删除,或将从表中外键引用该主表的关系先删除

约束等级

  • Cascade方式:在父表上update/delete记录时,同步update/delete掉子表的匹配记录

  • Set null方式:在父表上update/delete记录时,将子表上匹配记录的列设为null,但是要注意子表的外键列不能为not null

  • No action方式:如果子表中有匹配的记录,则不允许对父表对应候选键进行update/delete操作

  • Restrict方式:同no action, 都是立即检查外键约束

  • Set default方式(在可视化工具SQLyog中可能显示空白):父表有变更时,子表将外键列设置成一个默认的值,但Innodb不能识别

如果没有指定等级,就相当于Restrict方式。

对于外键约束,最好是采用: ON UPDATE CASCADE ON DELETE RESTRICT 的方式。

1
2
3
4
5
6
7
create table emp(
eid int primary key, #员工编号
ename varchar(5), #员工姓名
deptid int, #员工所在的部门
foreign key (deptid) references dept(did) on update cascade on delete set null
#把修改操作设置为级联修改等级,把删除操作设置为set null等级
);

建和不建外键约束有什么区别?

答:建外键约束,你的操作(创建表、删除表、添加、修改、删除)会受到限制,从语法层面受到限制。例如:在员工表中不可能添加一个员工信息,它的部门的值在部门表中找不到。

不建外键约束,你的操作(创建表、删除表、添加、修改、删除)不受限制,要保证数据的引用完整性,只能依靠程序员的自觉,或者是在Java程序中进行限定。例如:在员工表中,可以添加一个员工的信息,它的部门指定为一个完全不存在的部门。

那么建和不建外键约束和查询有没有关系?

在 MySQL 里,外键约束是有成本的,需要消耗系统资源。对于大并发的 SQL 操作,有可能会不适合。比如大型网站的中央数据库,可能会因为外键约束的系统开销而变得非常慢。所以, MySQL 允许你不使用系统自带的外键约束,在应用层面完成检查数据一致性的逻辑。也就是说,即使你不用外键约束,也要想办法通过应用层面的附加逻辑,来实现外键约束的功能,确保数据的一致性。

阿里开发规范

【强制】不得使用外键与级联,一切外键概念必须在应用层解决。

说明:(概念解释)学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度


CHECK 约束

检查某个字段的值是否符号xx要求,一般指的是值的范围

说明:MySQL 5.7 不支持

MySQL5.7 可以使用check约束,但check约束对数据验证没有任何作用。添加数据时,没有任何错误或警告

但是MySQL 8.0中可以使用check约束了

DEFAULT约束

给某个字段/某列指定默认值,一旦设置默认值,在插入数据时,如果此字段没有显式赋值,则赋值为默认值。

给字段加默认值

1)建表时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
create table 表名称(
字段名 数据类型 primary key,
字段名 数据类型 unique key not null,
字段名 数据类型 unique key,
字段名 数据类型 not null default 默认值,
);
create table 表名称(
字段名 数据类型 default 默认值 ,
字段名 数据类型 not null default 默认值,
字段名 数据类型 not null default 默认值,
primary key(字段名),
unique key(字段名)
);

说明:默认值约束一般不在唯一键和主键列上加
1
2
3
4
5
6
create table employee(
eid int primary key,
ename varchar(20) not null,
gender char default '男',
tel char(11) not null default '' #默认是空字符串
);

(2)建表后

1
2
3
4
5
alter table 表名称 modify 字段名 数据类型 default 默认值;

#如果这个字段原来有非空约束,你还保留非空约束,那么在加默认值约束时,还得保留非空约束,否则非空约束就被删除了
#同理,在给某个字段加非空约束也一样,如果这个字段原来有默认值约束,你想保留,也要在modify语句中保留默认值约束,否则就删除了
alter table 表名称 modify 字段名 数据类型 default 默认值 not null;
1
2
alter table employee modify gender char default '男';  #给gender字段增加默认值约束
alter table employee modify tel char(11) default ''; #给tel字段增加默认值约束
1
alter table employee modify tel char(11) default ''  not null;#给tel字段增加默认值约束,并保留非空约束

如何删除默认值约束

1
2
3
alter table 表名称 modify 字段名 数据类型 ;#删除默认值约束,也不保留非空约束

alter table 表名称 modify 字段名 数据类型 not null; #删除默认值约束,保留非空约束
1
2
alter table employee modify gender char; #删除gender字段默认值约束,如果有非空约束,也一并删除
alter table employee modify tel char(11) not null;#删除tel字段默认值约束,保留非空约束

SQL概述

SQL 分类

SQL语言在功能上主要分为如下3大类:

DDL(Data Definition Languages、数据定义语言),这些语句定义了不同的数据库、表、视图、索 引等数据库对象,还可以用来创建、删除、修改数据库和数据表的结构。

主要的语句关键字包括 CREATEDROPALTER 等。
DML(Data Manipulation Language、数据操作语言),用于添加、删除、更新和查询数据库记 录,并检查数据完整性。

主要的语句关键字包括 INSERTDELETEUPDATESELECT 等。

SELECT是SQL语言的基础,最为重要。
DCL(Data Control Language、数据控制语言) ,用于定义数据库、表、字段、用户的访问权限和 安全级别。

主要的语句关键字包括 GRANTREVOKECOMMITROLLBACKSAVEPOINT 等。


DCL(Data Control Language、数据控制语言) 

注意:

SELECT是SQL语言的基础,最为重要。


SQL大小写规范 (建议遵守)

  • MySQL 在Windows 环境下是大小写不敏感的

  • MySQL 在Linux 环境下是大小写敏感的

    ​ 数据库名、表名、表的别名、变量名是严格区分大小写的

​ 关键字、函数名、列名(或字段名)、列的别名(字段的别名) 是忽略大小写的。

  • 推荐采用统一的书写规范:

​ 数据库名、表名、表别名、字段名、字段别名等都小写 SQL 关键字、函数名、绑定变量等都大写

注释

可以使用如下格式的注释结构

1
2
3
单行注释: #注释文字(MySQL特有的方式) 
单行注释: -- 注释文字(--后面必须包含一个空格。)
多行注释: /* 注释文字 */

命名规则

  • 数据库、表名不得超过30个字符,变量名限制为29个
  • 必须只能包含A–Z, a–z, 0–9, _63个字符
  • 数据库名、表名、字段名等对象名中间不要包含空格
  • 同一个MySQL软件中,数据库不能同名;同一个库中,表不能重名;同一个表中,字段不能重名
  • 必须保证你的字段没有和保留字、数据库系统或常用方法冲突。如果坚持使用,请在SQL语句中使 用`(着重号)引起来
  • 保持字段名和类型的一致性,在命名字段并为其指定数据类型的时候一定要保证一致性。假如数据 类型在一个表里是整数,那在另一个表里可就别变成字符型了

举例:

1
2
3
4
5
#以下两句是一样的,不区分大小写 show databases;
SHOW DATABASES;
#创建表格
#create table student info(...); #表名错误,因为表名有空格 create table student_info(...);
#其中order使用``飘号,因为order和系统关键字或系统函数名等预定义标识符重名了

数据查询

SELECT

SELECT语句一般格式:

1
2
3
4
5
SELECT [ALL | DISTINCT] < 目标列表达式 > [,< 目标列表达式 >]...
FROM < 表名或试图名 > [,< 表名或试图名 >]...
[ WHERE < 条件表达式 > ]
[ GROUP BY < 列名1 > [ HAVING < 条件表达式 > ] ]
[ ORDER BY < 列名2 > [ ASC | DESC ]];

列的别名

方式一

1
2
SELECT last_name lname
FROM employees;

方式二

1
2
SELECT last_name AS lname
FROM employees;

方式三

列的别名可以使用一对""引起来,不要使用''

1
2
SELECT last_name "lname"
FROM employees;

去除重复行

1
2
SELECT DISTINCT Sno
FROM SC;

查询经过计算的值

1
2
SELECT Sname NAME,2021-Sage BIRTHDAY
FROM Student;

查询时添加常量列

1
2
SELECT first_name, '湖北' AS province
FROM employees;

显示表结构

使用DESCRIBEDESC命令,表示表结构。

1
2
3
DESCRIBE employees;

DESC employees;

运算符

算术运算符

比较运算符

比较运算符用来对表达式左边的操作数和右边的操作数进行比较,比较的结果为真则返回1,比较的结果为假则返回0,其他情况则返回NULL

比较运算符经常被用来作为SELECT查询语句的条件来使用,返回符合条件的结果记录。

安全等于运算符

安全等于运算符<=>与等于运算符=的作用是相似的,唯一的区别是<=>可以用来对NULL进行判断。在两个操作数均为NULL时,其返回值为1,而不为NULL;当一个操作数为NULL时,其返回值为0,而不为NULL。

此外,还有非符号类型的运算符:

非符号类型的运算符


逻辑运算符

逻辑运算符主要用来判断表达式的真假,在MySQL中,逻辑运算符的返回结果为10或者NULL

MySQL中支持4种逻辑运算符如下:

逻辑非运算符

逻辑非(NOT!)运算符表示当给定的值为0时返回1;当给定的值为非0值时返回0;当给定的值为NULL时,返回NULL。

逻辑与运算符

逻辑与(AND&&)运算符是当给定的所有值均为非0值,并且都不为NULL时,返回1;当给定的一个值或者多个值为0时则返回0;否则返回NULL。

逻辑或运算符

逻辑或(OR||)运算符是当给定的值都不为NULL,并且任何一个值为非0值时,则返回1,否则返回0;当一个值为NULL,并且另一个值为非0值时,返回1,否则返回NULL;当两个值都为NULL时,返回NULL。

注意:OR可以和AND一起使用,但是在使用时要注意两者的优先级,由于AND的优先级高于OR,因此先对AND两边的操作数进行操作,再与OR中的操作数结合。

逻辑异或运算符

逻辑异或(XOR)运算符是当给定的值中任意一个值为NULL时,则返回NULL;如果两个非NULL的值都是0或者都不等于0时,则返回0;如果一个值为0,另一个值不为0时,则返回1。

位运算符

位运算符是在二进制数上进行计算的运算符。位运算符会先将操作数变成二进制数,然后进行位运算,最后将计算结果从二进制变回十进制数。

MySQL支持的位运算符如下:

  1. 按位与运算符 按位与(&)运算符将给定值对应的二进制数逐位进行逻辑与运算。当给定值对应的二 进制位的数值都为1时,则该位返回1,否则返回0。

    例子:

    ​ 1的二进制数为0001,10的二进制数为1010,所以1 & 10的结果为0000,对应的十进制数为0。20的二进制 数为10100,30的二进制数为11110,所以20 & 30的结果为10100,对应的十进制数为20。

  2. 按位或运算符 按位或(|)运算符将给定的值对应的二进制数逐位进行逻辑或运算。当给定值对应的 二进制位的数值有一个或两个为1时,则该位返回1,否则返回0。

    例子:

    ​ 1的二进制数为0001,10的二进制数为1010,所以1 | 10的结果为1011,对应的十进制数为11。20的二进 制数为10100,30的二进制数为11110,所以20 | 30的结果为11110,对应的十进制数为30。

  3. 按位异或运算符^”是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。

  4. 按位取反运算符 按位取反(~)运算符将给定的值的二进制数逐位进行取反操作,即将1变为0,将0变 为1。

  5. 按位右移运算符 按位右移(>>)运算符将给定的值的二进制数的所有位右移指定的位数。右移指定的 位数后,右边低位的数值被移出并丢弃,左边高位空出的位置用0补齐。

    例子:

    ​ 1的二进制数为0000 0001,右移2位为0000 0000,对应的十进制数为0。4的二进制数为0000 0100,右移2 位为0000 0001,对应的十进制数为1。

  6. 按位左移运算符 按位左移(<<)运算符将给定的值的二进制数的所有位左移指定的位数。左移指定的 位数后,左边高位的数值被移出并丢弃,右边低位空出的位置用0补齐。

    例子:

    ​ 1的二进制数为0000 0001,左移两位为0000 0100,对应的十进制数为4。4的二进制数为0000 0100,左移 两位为0001 0000,对应的十进制数为16。

运算符优先级

数字编号越大,优先级越高,优先级高的运算符先进行计算。


排序与分页

排序

使用 ORDER BY 子句排序

  • ASC(ascend): 升序
  • DESC(descend): **降序
    ** ORDER BY 子句在SELECT语句的结尾。
1
2
3
SELECT   last_name, job_id, department_id, hire_date
FROM employees
ORDER BY hire_date DESC ;

分页

分页原理 所谓分页显示,就是将数据库中的结果集,一段一段显示出来需要的条件。 MySQL中使用 LIMIT 实现分页
格式:

1
LIMIT [位置偏移量,] 行数
  • 分页显式公式:(当前页数-1)每页条数,每页条数
1
2
SELECT * FROM table 
LIMIT(PageNo - 1)*PageSize,PageSize;

举例

1
2
3
4
5
6
7
8
9
 --前10条记录:
SELECT * FROM 表名 LIMIT 0,10; 或者
SELECT * FROM 表名 LIMIT 10;

--第11至20条记录:
SELECT * FROM 表名 LIMIT 10,10;

--第21至30条记录:
SELECT * FROM 表名 LIMIT 20,10;

注意:LIMIT 子句必须放在整个SELECT语句的最后!

“LIMIT条目数”等价于”LIMIT 0, 条目数”

多表查询,也称为关联查询,指两个或更多个表一起完成查询操作。

前提条件:这些一起查询的表之间是有关系的(一对一、一对多),它们之间一定是有关联字段,这个关联字段可能建立了外键,也可能没有建立外键。比如:员工表和部门表,这两个表依靠“部门编号”进行关联。

多表查询

多表查询,也称为关联查询,指两个或更多个表一起完成查询操作。

前提条件:这些一起查询的表之间是有关系的(一对一、一对多),它们之间一定是有关联字段,这个关联字段可能建立了外键,也可能没有建立外键。比如:员工表和部门表,这两个表依靠“部门编号”进行关联。

SQL92中,笛卡尔积也称为交叉连接,英文是 CROSS JOIN。在 SQL99 中也是使用CROSS JOIN表示交叉连接。它的作用就是可以把任意表进行连接,即使这两张表不相关。在MySQL中如下情况会出现笛卡尔积:

1
2
3
4
5
#查询员工姓名和所在部门名称
SELECT last_name,department_name FROM employees,departments;
SELECT last_name,department_name FROM employees CROSS JOIN departments;
SELECT last_name,department_name FROM employees INNER JOIN departments;
SELECT last_name,department_name FROM employees JOIN departments;
  • 笛卡尔积的错误会在下面条件下产生

    • 省略多个表的连接条件(或关联条件)
    • 连接条件(或关联条件)无效
    • 所有表中的所有行互相连接
  • 为了避免笛卡尔积, 可以在 WHERE 加入有效的连接条件。

  • 加入连接条件后,查询语法:

    1
    2
    3
    SELECT	table1.column, table2.column
    FROM table1, table2
    WHERE table1.column1 = table2.column2; #连接条件
    • 在 WHERE子句中写入连接条件。
  • 正确写法:

    1
    2
    3
    4
    #案例:查询员工的姓名及其部门名称
    SELECT last_name, department_name
    FROM employees, departments
    WHERE employees.department_id = departments.department_id;
  • 在表中有相同列时,在列名之前加上表名前缀。

等值连接与非等值连接

非等值连接

1
2
3
SELECT e.last_name, e.salary, j.grade_level
FROM employees e, job_grades j
WHERE e.salary BETWEEN j.lowest_sal AND j.highest_sal;

自连接与非自连接

当table1和table2本质上是同一张表,只是用取别名的方式虚拟成两张表以代表不同的意义。然后两个表再进行内连接,外连接等查询。

1
2
3
SELECT FIRST.Cno, SECOND.Cpno
FROM Course FIRST, Course SECOND
WHERE FIRST.Cpno = SECOND.Cno

内连接与外连接

  • 内连接: 合并具有同一列的两个以上的表的行, 结果集中不包含一个表与另一个表不匹配的行

  • 外连接: 两个表在连接过程中除了返回满足连接条件的行以外还返回左(或右)表中不满足条件的行 ,这种连接称为左(或右) 外连接。没有匹配的行时, 结果表中相应的列为空(NULL)。

  • 如果是左外连接,则连接条件中左边的表也称为主表,右边的表称为从表

    如果是右外连接,则连接条件中右边的表也称为主表,左边的表称为从表

内连接(INNER JOIN)

  • 语法:
1
2
3
4
SELECT 字段列表
FROM A表 INNER JOIN B表
ON 关联条件
WHERE 等其他子句;

例子:

1
2
3
4
SELECT e.employee_id, e.last_name, e.department_id, 
d.department_id, d.location_id
FROM employees e JOIN departments d
ON (e.department_id = d.department_id);

外连接(OUTER JOIN)的实现

左外连接(LEFT OUTER JOIN)

  • 语法:
1
2
3
4
5
#实现查询结果是A
SELECT 字段列表
FROM A表 LEFT JOIN B表
ON 关联条件
WHERE 等其他子句;
  • 举例:
1
2
3
4
SELECT e.last_name, e.department_id, d.department_name
FROM employees e
LEFT OUTER JOIN departments d
ON (e.department_id = d.department_id) ;
右外连接(RIGHT OUTER JOIN)

  • 语法:
1
2
3
4
5
#实现查询结果是B
SELECT 字段列表
FROM A表 RIGHT JOIN B表
ON 关联条件
WHERE 等其他子句;
  • 举例:
1
2
3
4
SELECT e.last_name, e.department_id, d.department_name
FROM employees e
RIGHT OUTER JOIN departments d
ON (e.department_id = d.department_id) ;
满外连接(FULL OUTER JOIN)
  • 满外连接的结果 = 左右表匹配的数据 + 左表没有匹配到的数据 + 右表没有匹配到的数据。
  • SQL99是支持满外连接的。使用FULL JOIN 或 FULL OUTER JOIN来实现。
  • 需要注意的是,MySQL不支持FULL JOIN,但是可以用 LEFT JOIN UNION RIGHT join代替。

UNION的使用

合并查询结果
利用UNION关键字,可以给出多条SELECT语句,并将它们的结果组合成单个结果集。合并时,两个表对应的列数和数据类型必须相同,并且相互对应。各个SELECT语句之间使用UNION或UNION ALL关键字分隔。

语法格式:

1
2
3
SELECT column,... FROM table1
UNION [ALL]
SELECT column,... FROM table2

UNION操作符返回两个查询的结果集的并集,去除重复记录。

UNION ALL操作符返回两个查询的结果集的并集。对于两个结果集的重复部分,不去重。

注意:执行UNION ALL语句时所需要的资源比UNION语句少。如果明确知道合并数据后的结果数据不存在重复数据,或者不需要去除重复的数据,则尽量使用UNION ALL语句,以提高数据查询的效率。

SQL99语法新特性

SQL99 在 SQL92 的基础上提供了一些特殊语法,比如NATURAL JOIN用来表示自然连接。

我们可以把自然连接理解为 SQL92 中的等值连接。它会帮你自动查询两张连接表中所有相同的字段,然后进行等值连接。

在SQL92标准中:

1
2
3
4
SELECT employee_id,last_name,department_name
FROM employees e JOIN departments d
ON e.`department_id` = d.`department_id`
AND e.`manager_id` = d.`manager_id`;

在 SQL99 中你可以写成:

1
2
SELECT employee_id,last_name,department_name
FROM employees e NATURAL JOIN departments d;

USING连接

当我们进行连接的时候,SQL99还支持使用 USING 指定数据表里的同名字段进行等值连接。但是只能配合JOIN一起使用。比如:

1
2
3
SELECT employee_id,last_name,department_name
FROM employees e JOIN departments d
USING (department_id);

你能看出与自然连接NATURAL JOIN不同的是,USING指定了具体的相同的字段名称,你需要在USING的括号()中填入要指定的同名字段。同时使用JOIN...USING可以简化JOIN ON的等值连接。它与下面的 SQL 查询结果是相同的:

1
2
3
SELECT employee_id,last_name,department_name
FROM employees e ,departments d
WHERE e.department_id = d.department_id;

单行函数

MySQL的内置函数及分类

MySQL提供的内置函数从实现的功能角度可以分为数值函数、字符串函数、日期和时间函数、流程控制函数、加密与解密函数、获取MySQL信息函数、聚合函数等。这里,我将这些丰富的内置函数再分为两类:单行函数聚合函数(或分组函数)

数值函数

基本函数

函数 用法
ABS(x) 返回x的绝对值
SIGN(X) 返回X的符号。正数返回1,负数返回-1,0返回0
PI() 返回圆周率的值
CEIL(x)CEILING(x) 返回大于或等于某个值的最小整数
FLOOR(x) 返回小于或等于某个值的最大整数
LEAST(e1,e2,e3…) 返回列表中的最小值
GREATEST(e1,e2,e3…) 返回列表中的最大值
MOD(x,y) 返回X除以Y后的余数
RAND() 返回0~1的随机值
RAND(x) 返回0~1的随机值,其中x的值用作种子值,相同的X值会产生相同的随机数
ROUND(x) 返回一个对x的值进行四舍五入后,最接近于X的整数
ROUND(x,y) 返回一个对x的值进行四舍五入后最接近X的值,并保留到小数点后面Y位
TRUNCATE(x,y) 返回数字x截断为y位小数的结果
SQRT(x) 返回x的平方根。当X的值为负数时,返回NULL

举例:

1
2
3
SELECT ABS(-123),ABS(32),SIGN(-23),SIGN(43),PI(),CEIL(32.32),CEILING(-43.23),FLOOR(32.32),
FLOOR(-43.23),MOD(12,5)
FROM DUAL;

角度与弧度互换函数

函数 用法
RADIANS(x) 将角度转化为弧度,其中,参数x为角度值
DEGREES(x) 将弧度转化为角度,其中,参数x为弧度值
1
2
SELECT RADIANS(30),RADIANS(60),RADIANS(90),DEGREES(2*PI()),DEGREES(RADIANS(90))
FROM DUAL;

三角函数

函数 用法
SIN(x) 返回x的正弦值,其中,参数x为弧度值
ASIN(x) 返回x的反正弦值,即获取正弦为x的值。如果x的值不在-1到1之间,则返回NULL
COS(x) 返回x的余弦值,其中,参数x为弧度值
ACOS(x) 返回x的反余弦值,即获取余弦为x的值。如果x的值不在-1到1之间,则返回NULL
TAN(x) 返回x的正切值,其中,参数x为弧度值
ATAN(x) 返回x的反正切值,即返回正切值为x的值
ATAN2(m,n) 返回两个参数的反正切值
COT(x) 返回x的余切值,其中,X为弧度值

举例:

ATAN2(M,N)函数返回两个参数的反正切值。
与ATAN(X)函数相比,ATAN2(M,N)需要两个参数,例如有两个点point(x1,y1)和point(x2,y2),使用ATAN(X)函数计算反正切值为ATAN((y2-y1)/(x2-x1)),使用ATAN2(M,N)计算反正切值则为ATAN2(y2-y1,x2-x1)。由使用方式可以看出,当x2-x1等于0时,ATAN(X)函数会报错,而ATAN2(M,N)函数则仍然可以计算。

ATAN2(M,N)函数的使用示例如下:

1
2
SELECT SIN(RADIANS(30)),DEGREES(ASIN(1)),TAN(RADIANS(45)),DEGREES(ATAN(1)),DEGREES(ATAN2(1,1))
FROM DUAL;

指数与对数

函数 用法
POW(x,y),POWER(X,Y) 返回x的y次方
EXP(X) 返回e的X次方,其中e是一个常数,2.718281828459045
LN(X),LOG(X) 返回以e为底的X的对数,当X <= 0 时,返回的结果为NULL
LOG10(X) 返回以10为底的X的对数,当X <= 0 时,返回的结果为NULL
LOG2(X) 返回以2为底的X的对数,当X <= 0 时,返回NULL
1
2
3
4
5
6
7
8
mysql> SELECT POW(2,5),POWER(2,4),EXP(2),LN(10),LOG10(10),LOG2(4)
-> FROM DUAL;
+----------+------------+------------------+-------------------+-----------+---------+
| POW(2,5) | POWER(2,4) | EXP(2) | LN(10) | LOG10(10) | LOG2(4) |
+----------+------------+------------------+-------------------+-----------+---------+
| 32 | 16 | 7.38905609893065 | 2.302585092994046 | 1 | 2 |
+----------+------------+------------------+-------------------+-----------+---------+
1 row in set (0.00 sec)

进制间的转换

函数 用法
BIN(x) 返回x的二进制编码
HEX(x) 返回x的十六进制编码
OCT(x) 返回x的八进制编码
CONV(x,f1,f2) 返回f1进制数变成f2进制数
1
2
3
4
5
6
7
8
mysql> SELECT BIN(10),HEX(10),OCT(10),CONV(10,2,8)
-> FROM DUAL;
+---------+---------+---------+--------------+
| BIN(10) | HEX(10) | OCT(10) | CONV(10,2,8) |
+---------+---------+---------+--------------+
| 1010 | A | 12 | 2 |
+---------+---------+---------+--------------+
1 row in set (0.00 sec)

字符串函数

函数 用法
ASCII(S) 返回字符串S中的第一个字符的ASCII码值
CHAR_LENGTH(s) 返回字符串s的字符数。作用与CHARACTER_LENGTH(s)相同
LENGTH(s) 返回字符串s的字节数,和字符集有关
CONCAT(s1,s2,……,sn) 连接s1,s2,……,sn为一个字符串
CONCAT_WS(x, s1,s2,……,sn) 同CONCAT(s1,s2,…)函数,但是每个字符串之间要加上x
INSERT(str, idx, len, replacestr) 将字符串str从第idx位置开始,len个字符长的子串替换为字符串replacestr
REPLACE(str, a, b) 用字符串b替换字符串str中所有出现的字符串a
UPPER(s) 或 UCASE(s) 将字符串s的所有字母转成大写字母
LOWER(s) 或LCASE(s) 将字符串s的所有字母转成小写字母
LEFT(str,n) 返回字符串str最左边的n个字符
RIGHT(str,n) 返回字符串str最右边的n个字符
LPAD(str, len, pad) 用字符串pad对str最左边进行填充,直到str的长度为len个字符
RPAD(str ,len, pad) 用字符串pad对str最右边进行填充,直到str的长度为len个字符
LTRIM(s) 去掉字符串s左侧的空格
RTRIM(s) 去掉字符串s右侧的空格
TRIM(s) 去掉字符串s开始与结尾的空格
TRIM(s1 FROM s) 去掉字符串s开始与结尾的s1
TRIM(LEADING s1 FROM s) 去掉字符串s开始处的s1
TRIM(TRAILING s1 FROM s) 去掉字符串s结尾处的s1
REPEAT(str, n) 返回str重复n次的结果
SPACE(n) 返回n个空格
STRCMP(s1,s2) 比较字符串s1,s2的ASCII码值的大小
SUBSTR(s,index,len) 返回从字符串s的index位置其len个字符,作用与SUBSTRING(s,n,len)、MID(s,n,len)相同
LOCATE(substr,str) 返回字符串substr在字符串str中首次出现的位置,作用于POSITION(substr IN str)、INSTR(str,substr)相同。未找到,返回0
ELT(m,s1,s2,…,sn) 返回指定位置的字符串,如果m=1,则返回s1,如果m=2,则返回s2,如果m=n,则返回sn
FIELD(s,s1,s2,…,sn) 返回字符串s在字符串列表中第一次出现的位置
FIND_IN_SET(s1,s2) 返回字符串s1在字符串s2中出现的位置。其中,字符串s2是一个以逗号分隔的字符串
REVERSE(s) 返回s反转后的字符串
NULLIF(value1,value2) 比较两个字符串,如果value1与value2相等,则返回NULL,否则返回value1

注意:MySQL中,字符串的位置是从1开始的。

举例:

1
2
3
4
5
6
7
8
mysql> SELECT FIELD('mm','hello','msm','amma'),FIND_IN_SET('mm','hello,mm,amma')
-> FROM DUAL;
+----------------------------------+-----------------------------------+
| FIELD('mm','hello','msm','amma') | FIND_IN_SET('mm','hello,mm,amma') |
+----------------------------------+-----------------------------------+
| 0 | 2 |
+----------------------------------+-----------------------------------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7
mysql> SELECT NULLIF('mysql','mysql'),NULLIF('mysql', '');
+-------------------------+---------------------+
| NULLIF('mysql','mysql') | NULLIF('mysql', '') |
+-------------------------+---------------------+
| NULL | mysql |
+-------------------------+---------------------+
1 row in set (0.00 sec)

日期与时间戳的转换

函数 用法
UNIX_TIMESTAMP() 以UNIX时间戳的形式返回当前时间。SELECT UNIX_TIMESTAMP() ->1634348884
UNIX_TIMESTAMP(date) 将时间date以UNIX时间戳的形式返回。
FROM_UNIXTIME(timestamp) 将UNIX时间戳的时间转换为普通格式的时间

举例:

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
mysql> SELECT UNIX_TIMESTAMP(now());
+-----------------------+
| UNIX_TIMESTAMP(now()) |
+-----------------------+
| 1576380910 |
+-----------------------+
1 row in set (0.01 sec)

mysql> SELECT UNIX_TIMESTAMP(CURDATE());
+---------------------------+
| UNIX_TIMESTAMP(CURDATE()) |
+---------------------------+
| 1576339200 |
+---------------------------+
1 row in set (0.00 sec)

mysql> SELECT UNIX_TIMESTAMP(CURTIME());
+---------------------------+
| UNIX_TIMESTAMP(CURTIME()) |
+---------------------------+
| 1576380969 |
+---------------------------+
1 row in set (0.00 sec)

mysql> SELECT UNIX_TIMESTAMP('2011-11-11 11:11:11')
+---------------------------------------+
| UNIX_TIMESTAMP('2011-11-11 11:11:11') |
+---------------------------------------+
| 1320981071 |
+---------------------------------------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7
mysql> SELECT FROM_UNIXTIME(1576380910);
+---------------------------+
| FROM_UNIXTIME(1576380910) |
+---------------------------+
| 2019-12-15 11:35:10 |
+---------------------------+
1 row in set (0.00 sec)

获取月份、星期、星期数、天数等函数

函数 用法
YEAR(date) / MONTH(date) / DAY(date) 返回具体的日期值
HOUR(time) / MINUTE(time) / SECOND(time) 返回具体的时间值
MONTHNAME(date) 返回月份:January,…
DAYNAME(date) 返回星期几:MONDAY,TUESDAY…..SUNDAY
WEEKDAY(date) 返回周几,注意,周1是0,周2是1,。。。周日是6
QUARTER(date) 返回日期对应的季度,范围为1~4
WEEK(date) , WEEKOFYEAR(date) 返回一年中的第几周
DAYOFYEAR(date) 返回日期是一年中的第几天
DAYOFMONTH(date) 返回日期位于所在月份的第几天
DAYOFWEEK(date) 返回周几,注意:周日是1,周一是2,。。。周六是7

举例:

1
2
3
SELECT YEAR(CURDATE()),MONTH(CURDATE()),DAY(CURDATE()),
HOUR(CURTIME()),MINUTE(NOW()),SECOND(SYSDATE())
FROM DUAL;

日期的操作函数

函数 用法
EXTRACT(type FROM date) 返回指定日期中特定的部分,type指定返回的值

EXTRACT(type FROM date)函数中type的取值与含义:

type取值 含义
MICROSECOND 返回毫秒数
SECOND 返回秒数
MINUTE 返回分钟数
HOUR 返回小时数
DAY 返回大数
WEEK 返回口期在一年中的第儿个星期
MONTH 返回日期在一年中的第几个月
QUARTER 返回日期在一年中的第几个季度
YEAR 返回日期的年份
SECOND MICROSECOND 返回秒和毫秒值
MINUTE MICROSECOND 返回分钟和毫秒值
MINUTE SECOND 返回分钟和秒值
HOUR MICROSECOND 返回小时和毫秒值
HOUR SECOND 返回小时和秒值
HOUR MINUTE 返回小时和分钟值
DAY MICROSECOND 返回天和毫秒值
DAY SECOND 返回天和秒值
DAY MINUTE 返回天和分钟值
DAY HOUR 返回天和小时
YEAR MONTH 返回年和月
1
2
3
SELECT EXTRACT(MINUTE FROM NOW()),EXTRACT( WEEK FROM NOW()),
EXTRACT( QUARTER FROM NOW()),EXTRACT( MINUTE_SECOND FROM NOW())
FROM DUAL;

时间和秒钟转换的函数

函数 用法
TIME_TO_SEC(time) 将 time 转化为秒并返回结果值。转化的公式为:小时*3600+分钟*60+秒
SEC_TO_TIME(seconds) 将 seconds 描述转化为包含小时、分钟和秒的时间

举例:

1
2
3
4
5
6
7
mysql> SELECT TIME_TO_SEC(NOW());
+--------------------+
| TIME_TO_SEC(NOW()) |
+--------------------+
| 78774 |
+--------------------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7
mysql> SELECT SEC_TO_TIME(78774);
+--------------------+
| SEC_TO_TIME(78774) |
+--------------------+
| 21:52:54 |
+--------------------+
1 row in set (0.12 sec)

计算日期和时间的函数

第1组:

函数 用法
DATE_ADD(datetime, INTERVAL expr type),ADDDATE(date,INTERVAL expr type) 返回与给定日期时间相差INTERVAL时间段的日期时间
DATE_SUB(date,INTERVAL expr type),SUBDATE(date,INTERVAL expr type) 返回与date相差INTERVAL时间间隔的日期

上述函数中type的取值:

间隔类型 含义
HOUR 小时
MINUTE 分钟
SECOND
YEAR
MONTH
DAY
YEAR MONTH 年和月
DAY HOUR 日和小时
DAY MINUTE 日和分钟
DAY SECOND 日和秒
HOUR MINUTE 小时和分钟
HOUR SECOND 小时和秒
MINUTE SECOND 分钟和秒

举例:

1
2
3
4
5
6
SELECT DATE_ADD(NOW(), INTERVAL 1 DAY) AS col1,DATE_ADD('2021-10-21 23:32:12',INTERVAL 1 SECOND) AS col2,
ADDDATE('2021-10-21 23:32:12',INTERVAL 1 SECOND) AS col3,
DATE_ADD('2021-10-21 23:32:12',INTERVAL '1_1' MINUTE_SECOND) AS col4,
DATE_ADD(NOW(), INTERVAL -1 YEAR) AS col5, #可以是负数
DATE_ADD(NOW(), INTERVAL '1_1' YEAR_MONTH) AS col6 #需要单引号
FROM DUAL;
1
2
3
4
SELECT DATE_SUB('2021-01-21',INTERVAL 31 DAY) AS col1,
SUBDATE('2021-01-21',INTERVAL 31 DAY) AS col2,
DATE_SUB('2021-01-21 02:01:01',INTERVAL '1 1' DAY_HOUR) AS col3
FROM DUAL;

第2组:

函数 用法
ADDTIME(time1,time2) 返回time1加上time2的时间。当time2为一个数字时,代表的是,可以为负数
SUBTIME(time1,time2) 返回time1减去time2后的时间。当time2为一个数字时,代表的是,可以为负数
DATEDIFF(date1,date2) 返回date1 - date2的日期间隔天数
TIMEDIFF(time1, time2) 返回time1 - time2的时间间隔
FROM_DAYS(N) 返回从0000年1月1日起,N天以后的日期
TO_DAYS(date) 返回日期date距离0000年1月1日的天数
LAST_DAY(date) 返回date所在月份的最后一天的日期
MAKEDATE(year,n) 针对给定年份与所在年份中的天数返回一个日期
MAKETIME(hour,minute,second) 将给定的小时、分钟和秒组合成时间并返回
PERIOD_ADD(time,n) 返回time加上n后的时间

举例:

1
2
3
4
SELECT ADDTIME(NOW(),20),SUBTIME(NOW(),30),SUBTIME(NOW(),'1:1:3'),DATEDIFF(NOW(),'2021-10-01'),
TIMEDIFF(NOW(),'2021-10-25 22:10:10'),FROM_DAYS(366),TO_DAYS('0000-12-25'),
LAST_DAY(NOW()),MAKEDATE(YEAR(NOW()),12),MAKETIME(10,21,23),PERIOD_ADD(20200101010101,10)
FROM DUAL;

日期的格式化与解析

函数 用法
DATE_FORMAT(date,fmt) 按照字符串fmt格式化日期date值
TIME_FORMAT(time,fmt) 按照字符串fmt格式化时间time值
GET_FORMAT(date_type,format_type) 返回日期字符串的显示格式
STR_TO_DATE(str, fmt) 按照字符串fmt对str进行解析,解析为一个日期

上述非GET_FORMAT函数中fmt参数常用的格式符:

格式符 说明 格式符 说明
%Y 4位数字表示年份 %y 表示两位数字表示年份
%M 月名表示月份(January,….) %m 两位数字表示月份(01,02,03。。。)
%b 缩写的月名(Jan.,Feb.,….) %c 数字表示月份(1,2,3,…)
%D 英文后缀表示月中的天数(1st,2nd,3rd,…) %d 两位数字表示月中的天数(01,02…)
%e 数字形式表示月中的天数(1,2,3,4,5…..)
%H 两位数字表示小数,24小时制(01,02..) %h和%I 两位数字表示小时,12小时制(01,02..)
%k 数字形式的小时,24小时制(1,2,3) %l 数字形式表示小时,12小时制(1,2,3,4….)
%i 两位数字表示分钟(00,01,02) %S和%s 两位数字表示秒(00,01,02…)
%W 一周中的星期名称(Sunday…) %a 一周中的星期缩写(Sun.,Mon.,Tues.,..)
%w 以数字表示周中的天数(0=Sunday,1=Monday….)
%j 以3位数字表示年中的天数(001,002…) %U 以数字表示年中的第几周,(1,2,3。。)其中Sunday为周中第一天
%u 以数字表示年中的第几周,(1,2,3。。)其中Monday为周中第一天
%T 24小时制 %r 12小时制
%p AM或PM %% 表示%

GET_FORMAT函数中date_type和format_type参数取值如下:

日期类型 格式化类型 返回的格式化字符串
DATE USA %m.%d.%Y
DATE JIS %Y-%m-%d
DATE ISO %Y-%m-%d
DATE EUR %d.%m.%Y
DATE INTERNAL %Y%m%d
TIME USA %h:%i:%s %p
TIME JIS %H:%i:%s
TIME ISO %H:%i:%s
TIME EUR %H.%i.%s
TIME INTERNAL %H%i%s
DATETIME USA %Y-%m-%d %H.%i.%s
DATETIME JIS %Y-%m-%d %H:%i:%s
DATETIME ISO %Y-%m-%d %H:%i:%s
DATETIME EUR %Y-%m-%d %I-L%i.%s
DATETIME INTERNAL %Y%m%d%H%i%s

举例:

1
2
3
4
5
6
7
mysql> SELECT DATE_FORMAT(NOW(), '%H:%i:%s');
+--------------------------------+
| DATE_FORMAT(NOW(), '%H:%i:%s') |
+--------------------------------+
| 22:57:34 |
+--------------------------------+
1 row in set (0.00 sec)
1
2
3
4
5
6
7
8
SELECT STR_TO_DATE('09/01/2009','%m/%d/%Y')
FROM DUAL;

SELECT STR_TO_DATE('20140422154706','%Y%m%d%H%i%s')
FROM DUAL;

SELECT STR_TO_DATE('2014-04-22 15:47:06','%Y-%m-%d %H:%i:%s')
FROM DUAL;
1
2
3
4
5
6
7
8
9
10
mysql> SELECT GET_FORMAT(DATE, 'USA');
+-------------------------+
| GET_FORMAT(DATE, 'USA') |
+-------------------------+
| %m.%d.%Y |
+-------------------------+
1 row in set (0.00 sec)

SELECT DATE_FORMAT(NOW(),GET_FORMAT(DATE,'USA')),
FROM DUAL;
1
2
3
4
5
6
7
mysql> SELECT STR_TO_DATE('2020-01-01 00:00:00','%Y-%m-%d'); 
+-----------------------------------------------+
| STR_TO_DATE('2020-01-01 00:00:00','%Y-%m-%d') |
+-----------------------------------------------+
| 2020-01-01 |
+-----------------------------------------------+
1 row in set, 1 warning (0.00 sec)

流程控制函数

流程处理函数可以根据不同的条件,执行不同的处理流程,可以在SQL语句中实现不同的条件选择。MySQL中的流程处理函数主要包括IF()IFNULL()CASE()函数。

函数 用法
IF(value,value1,value2) 如果value的值为TRUE,返回value1,否则返回value2
IFNULL(value1, value2) 如果value1不为NULL,返回value1,否则返回value2
CASE WHEN 条件1 THEN 结果1 WHEN 条件2 THEN 结果2 …. [ELSE resultn] END 相当于Java的if…else if…else…
CASE expr WHEN 常量值1 THEN 值1 WHEN 常量值1 THEN 值1 …. [ELSE 值n] END 相当于Java的switch…case…
1
2
SELECT IF(1 > 0,'正确','错误')    
->正确
1
2
SELECT IFNULL(null,'Hello Word')
->Hello Word
1
2
3
4
5
6
7
8
SELECT CASE 
  WHEN 1 > 0
  THEN '1 > 0'
  WHEN 2 > 0
  THEN '2 > 0'
  ELSE '3 > 0'
  END
->1 > 0
1
2
3
4
SELECT CASE 1 
  WHEN 1 THEN '我是1'
  WHEN 2 THEN '我是2'
ELSE '你是谁'
1
2
3
4
5
SELECT employee_id,salary, CASE WHEN salary>=15000 THEN '高薪' 
WHEN salary>=10000 THEN '潜力股'
WHEN salary>=8000 THEN '屌丝'
ELSE '草根' END "描述"
FROM employees;
1
2
3
4
5
6
SELECT oid,`status`, CASE `status` WHEN 1 THEN '未付款' 
WHEN 2 THEN '已付款'
WHEN 3 THEN '已发货'
WHEN 4 THEN '确认收货'
ELSE '无效订单' END
FROM t_order;
1
2
3
4
5
6
SELECT last_name, job_id, salary,
CASE job_id WHEN 'IT_PROG' THEN 1.10*salary
WHEN 'ST_CLERK' THEN 1.15*salary
WHEN 'SA_REP' THEN 1.20*salary
ELSE salary END "REVISED_SALARY"
FROM employees;

加密与解密函数

加密与解密函数主要用于对数据库中的数据进行加密和解密处理,以防止数据被他人窃取。这些函数在保证数据库安全时非常有用。

函数 用法
PASSWORD(str) 返回字符串str的加密版本,41位长的字符串。加密结果不可逆,常用于用户的密码加密
MD5(str) 返回字符串str的md5加密后的值,也是一种加密方式。若参数为NULL,则会返回NULL
SHA(str) 从原明文密码str计算并返回加密后的密码字符串,当参数为NULL时,返回NULL。SHA加密算法比MD5更加安全
ENCODE(value,password_seed) 返回使用password_seed作为加密密码加密value
DECODE(value,password_seed) 返回使用password_seed作为加密密码解密value

可以看到,ENCODE(value,password_seed)函数与DECODE(value,password_seed)函数互为反函数。

举例:

1
2
3
4
5
6
7
mysql> SELECT PASSWORD('mysql'), PASSWORD(NULL);
+-------------------------------------------+----------------+
| PASSWORD('mysql') | PASSWORD(NULL) |
+-------------------------------------------+----------------+
| *E74858DB86EBA20BC33D0AECAE8A8108C56B17FA | |
+-------------------------------------------+----------------+
1 row in set, 1 warning (0.00 sec)
1
2
SELECT md5('123')
->202cb962ac59075b964b07152d234b70
1
2
SELECT SHA('Tom123')
->c7c506980abc31cc390a2438c90861d0f1216d50
1
2
3
4
5
6
7
mysql> SELECT ENCODE('mysql', 'mysql');
+--------------------------+
| ENCODE('mysql', 'mysql') |
+--------------------------+
| íg ¼ ìÉ |
+--------------------------+
1 row in set, 1 warning (0.01 sec)
1
2
3
4
5
6
7
mysql> SELECT DECODE(ENCODE('mysql','mysql'),'mysql');
+-----------------------------------------+
| DECODE(ENCODE('mysql','mysql'),'mysql') |
+-----------------------------------------+
| mysql |
+-----------------------------------------+
1 row in set, 2 warnings (0.00 sec)

MySQL信息函数

MySQL中内置了一些可以查询MySQL信息的函数,这些函数主要用于帮助数据库开发或运维人员更好地对数据库进行维护工作。

函数 用法
VERSION() 返回当前MySQL的版本号
CONNECTION_ID() 返回当前MySQL服务器的连接数
DATABASE(),SCHEMA() 返回MySQL命令行当前所在的数据库
USER(),CURRENT_USER()、SYSTEM_USER(),SESSION_USER() 返回当前连接MySQL的用户名,返回结果格式为“主机名@用户名”
CHARSET(value) 返回字符串value自变量的字符集
COLLATION(value) 返回字符串value的比较规则

举例:

1
2
3
4
5
6
mysql> SELECT USER(), CURRENT_USER(), SYSTEM_USER(),SESSION_USER();
+----------------+----------------+----------------+----------------+
| USER() | CURRENT_USER() | SYSTEM_USER() | SESSION_USER() |
+----------------+----------------+----------------+----------------+
| root@localhost | root@localhost | root@localhost | root@localhost |
+----------------+----------------+----------------+----------------+

其他函数

MySQL中有些函数无法对其进行具体的分类,但是这些函数在MySQL的开发和运维过程中也是不容忽视的。

函数 用法
FORMAT(value,n) 返回对数字value进行格式化后的结果数据。n表示四舍五入后保留到小数点后n位
CONV(value,from,to) 将value的值进行不同进制之间的转换
INET_ATON(ipvalue) 将以点分隔的IP地址转化为一个数字
INET_NTOA(value) 将数字形式的IP地址转化为以点分隔的IP地址
BENCHMARK(n,expr) 将表达式expr重复执行n次。用于测试MySQL处理expr表达式所耗费的时间
CONVERT(value USING char_code) 将value所使用的字符编码修改为char_code

简介

在Java世界里面,面向对象还是主流思想,对于习惯了面向对象编程的开发者来说,抽象的概念并不陌生。面向对象编程是对数据进行抽象,而函数式编程是对行为进行抽象。现实世界中,数据和行为并存,程序也是如此,因此这两种编程方式我们都得学。

这种新的抽象方式还有其他好处。很多人不总是在编写性能优先的代码,对于这些人来说,函数式编程带来的好处尤为明显。程序员能编写出更容易阅读的代码——这种代码更多地表达了业务逻辑,而不是从机制上如何实现。易读的代码也易于维护、更可靠、更不容易出错。

在写回调函数和事件处理器时,程序员不必再纠缠于匿名内部类的冗繁和可读性,函数式编程让事件处理系统变得更加简单。能将函数方便地传递也让编写惰性代码变得容易,只有在真正需要的时候,才初始化变量的值。

Java Lambda表达式的一个重要用法是简化某些匿名内部类Anonymous Classes)的写法。实际上Lambda表达式并不仅仅是匿名内部类的语法糖,JVM内部是通过invokedynamic指令来实现Lambda表达式的。

Lambda表达式(也称闭包),是Java8中最受期待和欢迎的新特性之一。Lambda表达式本质是一个匿名函数,但是它并不是匿名类的语法糖,它让 Java 开始走向函数式编程,其实现原理区别于一般的匿名类中的匿名函数。在Java语法层面Lambda表达式允许函数作为一个方法的参数(函数作为参数传递到方法中),或者把代码看成数据。Lambda表达式可以简化函数式接口的使用。函数式接口就是一个只有一个抽象方法的普通接口,像这样的接口就可以使用Lambda表达式来简化代码的编写。

Lambda表达式

Lambda 表达式为 Java 添加了缺失的函数式编程特点,使我们能将函数当做一等公民看待。尽管不完全正确,我们很快就会见识到 Lambda 与闭包的不同之处,但是又无限地接近闭包。在支持一类函数的语言中,Lambda 表达式的类型将是函数。但是,在 Java 中,Lambda 表达式是对象,他们必须依附于一类特别的对象类型——函数式接口(functional interface)。函数式接口在java中是指:有且仅有一个抽象方法的接口只有函数式接口,才可以转换为lambda表达式函数式接口可以显式的被@FunctionalInterface所表示,当被标识的接口不满足规定时,编译器会提示报错。我们会在后文详细介绍函数式接口。

在认识Lambda表达式基础语法之前,先来看一段用两种方式创建线程的代码

1
2
3
4
5
6
7
8
9
10
11
12
// 创建线程
// 匿名类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello!");
}
}).start();

// Lambda 表达式
new Thread(() -> System.out.println("Hello!")).start();

Lambda 表达式的基础语法:Java8中引入了一个新的操作符 ->该操作符称为箭头操作符或 Lambda 操作符

箭头操作符将 Lambda 表达式拆分成两部分:

左侧:Lambda 表达式的参数列表

右侧:Lambda 表达式中所需实现的功能, 即 Lambda 体

Java 中的 Lambda 表达式通常使用(argument) -> (body)语法书写,例如:

1
2
3
(arg1, arg2...) -> { body }

(type1 arg1, type2 arg2...) -> { body }

以下是一些 Lambda 表达式的例子:

1
2
3
4
5
6
7
8
9
(int a, int b) -> {  return a + b; }

() -> System.out.println("Hello World");

(String s) -> { System.out.println(s); }

() -> 42

() -> { return 3.1415 };

什么是函数式接口

在 Java 中,Marker(标记)类型的接口是一种没有方法或属性声明的接口,简单地说,marker 接口是空接口。相似地,函数式接口是只包含一个抽象方法声明的接口。

java.lang.Runnable 就是一种函数式接口,在 Runnable 接口中只声明了一个方法void run(),相似地,ActionListener 接口也是一种函数式接口,我们使用匿名内部类来实例化函数式接口的对象,有了 Lambda 表达式,这一方式可以得到简化。

每个 Lambda 表达式都能隐式地赋值给函数式接口,例如,我们可以通过 Lambda 表达式创建 Runnable 接口的引用。

1
Runnable r = () -> System.out.println("hello world");

当不指明函数式接口时,编译器会自动解释这种转化:

1
2
3
new Thread(
() -> System.out.println("hello world")
).start();

因此,在上面的代码中,编译器会自动推断:根据线程类的构造函数签名 public Thread(Runnable r) { },将该 Lambda 表达式赋给 Runnable 接口。

以下是一些 Lambda 表达式及其函数式接口:

1
2
3
4
5
Consumer<Integer>  c = (int x) -> { System.out.println(x) };

BiConsumer<Integer, String> b = (Integer x, String y) -> System.out.println(x + " : " + y);

Predicate<String> p = (String s) -> { s == null };

@FunctionalInterface 是 Java 8 新加入的一种接口,用于指明该接口类型声明是根据 Java 语言规范定义的函数式接口。Java 8 还声明了一些 Lambda 表达式可以使用的函数式接口,当你注释的接口不是有效的函数式接口时,可以使用 @FunctionalInterface 解决编译层面的错误。

以下是一种自定义的函数式接口: @FunctionalInterface public interface WorkerInterface {

1
2
3
   public void doSomeWork();

}

根据定义,函数式接口只能有一个抽象方法,如果你尝试添加第二个抽象方法,将抛出编译时错误。例如:

1
2
3
4
5
6
7
8
@FunctionalInterface
public interface WorkerInterface {

public void doSomeWork();

public void doSomeMoreWork();

}

错误:

1
2
3
Unexpected @FunctionalInterface annotation 
@FunctionalInterface ^ WorkerInterface is not a functional interface multiple
non-overriding abstract methods found in interface WorkerInterface 1 error

函数式接口定义好后,我们可以在 API 中使用它,同时利用 Lambda 表达式。例如:

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
 //定义一个函数式接口
@FunctionalInterface
public interface WorkerInterface {

public void doSomeWork();

}


public class WorkerInterfaceTest {

public static void execute(WorkerInterface worker) {
worker.doSomeWork();
}

public static void main(String [] args) {

//invoke doSomeWork using Annonymous class
execute(new WorkerInterface() {
@Override
public void doSomeWork() {
System.out.println("Worker invoked using Anonymous class");
}
});

//invoke doSomeWork using Lambda expression
execute( () -> System.out.println("Worker invoked using Lambda expression") );
}

}

输出:

1
2
Worker invoked using Anonymous class 
Worker invoked using Lambda expression

这上面的例子里,我们创建了自定义的函数式接口并与 Lambda 表达式一起使用。execute() 方法现在可以将 Lambda 表达式作为参数。

Lambda 表达式的结构

让我们了解一下 Lambda 表达式的结构。

  • 一个 Lambda 表达式可以有零个多个参数
  • 参数的类型既可以明确声明,也可以根据上下文来推断。例如:(int a)(a)效果相同
  • 当只有一个参数,且其类型可推导时,圆括号()可省略。例如:a -> return a*a
  • 当参数大于一个时,所有参数需包含在圆括号内,参数之间用逗号相隔。例如:(a, b)(int a, int b)(String a, int b, float c)
  • 空圆括号代表没有参数。例如:() -> 42
  • Lambda 表达式的主体可包含零条多条语句
  • 如果 Lambda 表达式的主体只有一条语句,花括号{}可省略,如果Lambda体不加{}就不用写return
  • 如果 Lambda 表达式的主体包含一条以上语句,则表达式必须包含在花括号{}中(形成代码块)Lambda体加上{}就需要添加return。匿名函数的返回类型与代码块的返回类型一致,若没有返回则为空

Lambda表达式和内部类区别一

读过上一篇之后,相信对Lambda表达式的语法以及基本原理有了一定了解。对于编写代码,有这些知识已经够用。本文将进一步区分Lambda表达式和匿名内部类在JVM层面的区别。

经过第一篇的的介绍,我们看到Lambda表达式似乎只是为了简化匿名内部类书写,这看起来仅仅通过语法糖在编译阶段把所有的Lambda表达式替换成匿名内部类就可以了。但实时并非如此。在JVM层面,Lambda表达式和匿名内部类有着明显的差别。

匿名内部类实现

匿名内部类仍然是一个类,只是不需要程序员显示指定类名,编译器会自动为该类取名。因此如果有如下形式的代码,编译之后将会产生两个class文件:

1
2
3
4
5
6
7
8
9
10
public class MainAnonymousClass {
public static void main(String[] args) {
new Thread(new Runnable(){
@Override
public void run(){
System.out.println("Anonymous Class Thread run()");
}
}).start();;
}
}

编译之后文件分布如下,两个class文件分别是主类和匿名内部类产生的:

进一步分析主类MainAnonymousClass.class的字节码,可发现其创建了匿名内部类的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// javap -c MainAnonymousClass.class
public class MainAnonymousClass {
...
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class MainAnonymousClass$1 /*创建内部类对象*/
7: dup
8: invokespecial #4 // Method MainAnonymousClass$1."<init>":()V
11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: invokevirtual #6 // Method java/lang/Thread.start:()V
17: return
}

Lambda表达式实现

Lambda表达式通过invokedynamic指令实现,书写Lambda表达式不会产生新的类。如果有如下代码,编译之后只有一个class文件:

1
2
3
4
5
6
7
public class MainLambda {
public static void main(String[] args) {
new Thread(
() -> System.out.println("Lambda Thread run()")
).start();;
}
}

编译之后的结果:

通过javap反编译命名,我们更能看出Lambda表达式内部表示的不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// javap -c -p MainLambda.class
public class MainLambda {
...
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; /*使用invokedynamic指令调用*/
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return

private static void lambda$main$0(); /*Lambda表达式被封装成主类的私有方法*/
Code:
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String Lambda Thread run()
5: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

反编译之后我们发现Lambda表达式被封装成了主类的一个私有方法,并通过invokedynamic指令进行调用。

它没有创建将包装 Lambda 函数的新对象,而是使用Java 7 中添加的新invokeDynamic指令将此调用站点动态链接到实际的 Lambda 函数。

看到 Java,最“严格”的现代语言现在如何使用动态链接来支持其新的 Lambda 表达式,真是令人着迷。这也是一种有效的方法,因为不需要额外的类加载和编译——Lambda 方法只是我们类中的另一个私有方法。


Lambda表达式和内部类区别二

尽管在实际开发中基本上用 lambda 表达式替换了内部类,但这两个概念在一个重要方面是不同的:作用域。

既然Lambda表达式不是内部类的简写,那么Lambda内部的this引用也就跟内部类对象没什么关系了。在Lambda表达式中this的意义跟在表达式外部完全一样。因此下列代码将输出两遍Hello Hoolee,而不是两个引用地址。

1
2
3
4
5
6
7
8
9
public class Hello {
Runnable r1 = () -> { System.out.println(this); };
Runnable r2 = () -> { System.out.println(toString()); };
public static void main(String[] args) {
new Hello().r1.run();
new Hello().r2.run();
}
public String toString() { return "Hello Hoolee"; }
}

而对于匿名类,关键词 this 解读为匿名类对象。

1
2
3
4
5
6
7
new Thread(new Runnable() {
String name = "zhangsan";
@Override
public void run() {
System.out.println(this.name); //zhangsan
}
}).start();

Lambda and Collections

我们先从最熟悉的*Java集合框架(Java Collections Framework, JCF)*开始说起。

为引入Lambda表达式,Java8新增了java.util.funcion包,里面包含常用的函数接口,这是Lambda表达式的基础,Java集合框架也新增部分接口,以便与Lambda表达式对接。

首先回顾一下Java集合框架的接口继承结构:

上图中绿色标注的接口类,表示在Java8中加入了新的接口方法,当然由于继承关系,他们相应的子类也都会继承这些新方法。下表详细列举了这些方法。

接口名 Java8新加入的方法
Collection removeIf() spliterator() stream() parallelStream() forEach()
List replaceAll() sort()
Map getOrDefault() forEach() replaceAll() putIfAbsent() remove() replace() computeIfAbsent() computeIfPresent() compute() merge()

这些新加入的方法大部分要用到java.util.function包下的接口,这意味着这些方法大部分都跟Lambda表达式相关。我们将逐一学习这些方法。

Collection中的新方法

如上所示,接口CollectionList新加入了一些方法,我们以List的子类ArrayList为例来说明。了解Java7ArrayList实现原理,将有助于理解下文。

forEach()

该方法的签名为void forEach(Consumer action),作用是对容器中的每个元素执行action指定的动作,其中Consumer是个函数接口,里面只有一个待实现方法void accept(T t)(后面我们会看到,这个方法叫什么根本不重要,你甚至不需要记忆它的名字)。

需求:假设有一个字符串列表,需要打印出其中所有长度大于3的字符串.

Java7及以前我们可以用增强的for循环实现:

1
2
3
4
5
6
// 使用曾强for循环迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(String str : list){
if(str.length()>3)
System.out.println(str);
}

现在使用forEach()方法结合匿名内部类,可以这样实现:

1
2
3
4
5
6
7
8
9
// 使用forEach()结合匿名内部类迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach(new Consumer<String>(){
@Override
public void accept(String str){
if(str.length()>3)
System.out.println(str);
}
});

上述代码调用forEach()方法,并使用匿名内部类实现Comsumer接口。到目前为止我们没看到这种设计有什么好处,但是不要忘记Lambda表达式,使用Lambda表达式实现如下:

1
2
3
4
5
6
// 使用forEach()结合Lambda表达式迭代
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.forEach( str -> {
if(str.length()>3)
System.out.println(str);
});

上述代码给forEach()方法传入一个Lambda表达式,我们不需要知道accept()方法,也不需要知道Consumer接口,类型推导帮我们做了一切。

removeIf()

该方法签名为boolean removeIf(Predicate filter),作用是删除容器中所有满足filter指定条件的元素,其中Predicate是一个函数接口,里面只有一个待实现方法boolean test(T t),同样的这个方法的名字根本不重要,因为用的时候不需要书写这个名字。

需求:假设有一个字符串列表,需要删除其中所有长度大于3的字符串。

我们知道如果需要在迭代过程冲对容器进行删除操作必须使用迭代器,否则会抛出ConcurrentModificationException,所以上述任务传统的写法是:

1
2
3
4
5
6
7
// 使用迭代器删除列表元素
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Iterator<String> it = list.iterator();
while(it.hasNext()){
if(it.next().length()>3) // 删除长度大于3的元素
it.remove();
}

现在使用removeIf()方法结合匿名内部类,我们可是这样实现:

1
2
3
4
5
6
7
8
// 使用removeIf()结合匿名名内部类实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(new Predicate<String>(){ // 删除长度大于3的元素
@Override
public boolean test(String str){
return str.length()>3;
}
});

上述代码使用removeIf()方法,并使用匿名内部类实现Precicate接口。相信你已经想到用Lambda表达式该怎么写了:

1
2
3
// 使用removeIf()结合Lambda表达式实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.removeIf(str -> str.length()>3); // 删除长度大于3的元素

使用Lambda表达式不需要记忆Predicate接口名,也不需要记忆test()方法名,只需要知道此处需要一个返回布尔类型的Lambda表达式就行了。

replaceAll()

该方法签名为void replaceAll(UnaryOperator<E> operator),作用是对每个元素执行operator指定的操作,并用操作结果来替换原来的元素。其中UnaryOperator是一个函数接口,里面只有一个待实现函数T apply(T t)

需求:假设有一个字符串列表,将其中所有长度大于3的元素转换成大写,其余元素不变。

Java7及之前似乎没有优雅的办法:

1
2
3
4
5
6
7
// 使用下标实现元素替换
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
for(int i=0; i<list.size(); i++){
String str = list.get(i);
if(str.length()>3)
list.set(i, str.toUpperCase());
}

使用replaceAll()方法结合匿名内部类可以实现如下:

1
2
3
4
5
6
7
8
9
10
// 使用匿名内部类实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(new UnaryOperator<String>(){
@Override
public String apply(String str){
if(str.length()>3)
return str.toUpperCase();
return str;
}
});

上述代码调用replaceAll()方法,并使用匿名内部类实现UnaryOperator接口。我们知道可以用更为简洁的Lambda表达式实现:

1
2
3
4
5
6
7
// 使用Lambda表达式实现
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.replaceAll(str -> {
if(str.length()>3)
return str.toUpperCase();
return str;
});

sort()

该方法定义在List接口中,方法签名为void sort(Comparator c),该方法根据c指定的比较规则对容器元素进行排序Comparator接口我们并不陌生,其中有一个方法int compare(T o1, T o2)需要实现,显然该接口是个函数接口。

需求:假设有一个字符串列表,按照字符串长度增序对元素排序。

由于Java7以及之前sort()方法在Collections工具类中,所以代码要这样写:

1
2
3
4
5
6
7
8
// Collections.sort()方法
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
Collections.sort(list, new Comparator<String>(){
@Override
public int compare(String str1, String str2){
return str1.length()-str2.length();
}
});

现在可以直接使用List.sort()方法,结合Lambda表达式,可以这样写:

1
2
3
// List.sort()方法结合Lambda表达式
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length()-str2.length());

spliterator()

方法签名为Spliterator<E> spliterator(),该方法返回容器的可拆分迭代器。从名字来看该方法跟iterator()方法有点像,我们知道iterator()是用来迭代容器的,Spliterator也有类似作用,但二者有如下不同:

  1. Spliterator既可以像iterator()那样逐个迭代,也可以批量迭代。批量迭代可以降低迭代的开销。
  2. Spliterator是可拆分的,一个Spliterator可以通过调用Spliterator<T> trySplit()方法来尝试分成两个。一个是this,另一个是新返回的那个,这两个迭代器代表的元素没有重叠。

可通过(多次)调用Spliterator.trySplit()方法来分解负载,以便多线程处理。

stream()和parallelStream()

stream()parallelStream()分别返回该容器的Stream视图表示,不同之处在于parallelStream()返回并行的stream()stream()是Java函数式编程的核心类,我们会在后面章节中学习。


Map中的新方法

相比Collection,Map中加入了更多的方法,我们以HashMap为例来逐一探秘。了解[Java7HashMap`实现原理](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/6-HashSet and HashMap.md),将有助于理解下文。

forEach()

该方法签名为void forEach(BiConsumer action),作用是Map中的每个映射执行action指定的操作,其中BiConsumer是一个函数接口,里面有一个待实现方法void accept(T t, U u)BinConsumer接口名字和accept()方法名字都不重要,请不要记忆他们。

需求:假设有一个数字到对应英文单词的Map,请输出Map中的所有映射关系.

Java7以及之前经典的代码如下:

1
2
3
4
5
6
7
8
// Java7以及之前迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
System.out.println(entry.getKey() + "=" + entry.getValue());
}

使用Map.forEach()方法,结合匿名内部类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
// 使用forEach()结合匿名内部类迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach(new BiConsumer<Integer, String>(){
@Override
public void accept(Integer k, String v){
System.out.println(k + "=" + v);
}
});

上述代码调用forEach()方法,并使用匿名内部类实现BiConsumer接口。当然,实际场景中没人使用匿名内部类写法,因为有Lambda表达式:

1
2
3
4
5
6
7
// 使用forEach()结合Lambda表达式迭代Map
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
}

getOrDefault()

该方法跟Lambda表达式没关系,但是很有用。方法签名为V getOrDefault(Object key, V defaultValue),作用是按照给定的key查询Map中对应的value,如果没有找到则返回defaultValue。使用该方法程序员可以省去查询指定键值是否存在的麻烦.

需求;假设有一个数字到对应英文单词的Map,输出4对应的英文单词,如果不存在则输出NoValue

1
2
3
4
5
6
7
8
9
10
11
12
13
// 查询Map中指定的值,不存在时使用默认值
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
// Java7以及之前做法
if(map.containsKey(4)){ // 1
System.out.println(map.get(4));
}else{
System.out.println("NoValue");
}
// Java8使用Map.getOrDefault()
System.out.println(map.getOrDefault(4, "NoValue")); // 2

putIfAbsent()

该方法跟Lambda表达式没关系,但是很有用。方法签名为V putIfAbsent(K key, V value),作用是只有在不存在key值的映射或映射值为null,才将value指定的值放入到Map中,否则不对Map做更改.该方法将条件判断和赋值合二为一,使用起来更加方便.

remove()

我们都知道Map中有一个remove(Object key)方法,来根据指定key值删除Map中的映射关系;Java8新增了remove(Object key, Object value)方法,只有在当前Map中**key正好映射到value时**才删除该映射,否则什么也不做.

replace()

在Java7及以前,要想替换Map中的映射关系可通过put(K key, V value)方法实现,该方法总是会用新值替换原来的值.为了更精确的控制替换行为,Java8在Map中加入了两个replace()方法,分别如下:

  • replace(K key, V value),只有在当前Map中**key的映射存在时**才用value去替换原来的值,否则什么也不做.
  • replace(K key, V oldValue, V newValue),只有在当前Map中**key的映射存在且等于oldValue时**才用newValue去替换原来的值,否则什么也不做.

replaceAll()

该方法签名为replaceAll(BiFunction function),作用是对Map中的每个映射执行function指定的操作,并用function的执行结果替换原来的value,其中BiFunction是一个函数接口,里面有一个待实现方法R apply(T t, U u).不要被如此多的函数接口吓到,因为使用的时候根本不需要知道他们的名字.

需求:假设有一个数字到对应英文单词的Map,请将原来映射关系中的单词都转换成大写.

Java7以及之前经典的代码如下:

1
2
3
4
5
6
7
8
// Java7以及之前替换所有Map中所有映射关系
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
for(Map.Entry<Integer, String> entry : map.entrySet()){
entry.setValue(entry.getValue().toUpperCase());
}

使用replaceAll()方法结合匿名内部类,实现如下:

1
2
3
4
5
6
7
8
9
10
11
// 使用replaceAll()结合匿名内部类实现
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll(new BiFunction<Integer, String, String>(){
@Override
public String apply(Integer k, String v){
return v.toUpperCase();
}
});

上述代码调用replaceAll()方法,并使用匿名内部类实现BiFunction接口。更进一步的,使用Lambda表达式实现如下:

1
2
3
4
5
6
// 使用replaceAll()结合Lambda表达式实现
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());

简洁到让人难以置信.

merge()

该方法签名为merge(K key, V value, BiFunction remappingFunction),作用是:

  1. 如果Mapkey对应的映射不存在或者为null,则将value(不能是null)关联到key上;
  2. 否则执行remappingFunction,如果执行结果非null则用该结果跟key关联,否则在Map中删除key的映射.

参数中BiFunction函数接口前面已经介绍过,里面有一个待实现方法R apply(T t, U u)

merge()方法虽然语义有些复杂,但该方法的用方式很明确,一个比较常见的场景是将新的错误信息拼接到原来的信息上,比如:

1
map.merge(key, newMsg, (v1, v2) -> v1+v2);

compute()

该方法签名为compute(K key, BiFunction remappingFunction),作用是把remappingFunction的计算结果关联到key上,如果计算结果为null,则在Map中删除key的映射.

要实现上述merge()方法中错误信息拼接的例子,使用compute()代码如下:

1
map.compute(key, (k,v) -> v==null ? newMsg : v.concat(newMsg));

computeIfAbsent()

该方法签名为V computeIfAbsent(K key, Function mappingFunction),作用是:只有在当前Map不存在key值的映射或映射值为null,才调用mappingFunction,并在mappingFunction执行结果非null时,将结果跟key关联.

Function是一个函数接口,里面有一个待实现方法R apply(T t)

computeIfAbsent()常用来对Map的某个key值建立初始化映射.比如我们要实现一个多值映射,Map的定义可能是Map>,要向Map中放入新值,可通过如下代码实现:

1
2
3
4
5
6
7
8
9
10
11
Map<Integer, Set<String>> map = new HashMap<>();
// Java7及以前的实现方式
if(map.containsKey(1)){
map.get(1).add("one");
}else{
Set<String> valueSet = new HashSet<String>();
valueSet.add("one");
map.put(1, valueSet);
}
// Java8的实现方式
map.computeIfAbsent(1, v -> new HashSet<String>()).add("yi");

使用computeIfAbsent()将条件判断和添加操作合二为一,使代码更加简洁.

computeIfPresent()

该方法签名为V computeIfPresent(K key, BiFunction remappingFunction),作用跟computeIfAbsent()相反,即,只有在当前Map存在key值的映射且非null,才调用remappingFunction,如果remappingFunction执行结果为null,则删除key的映射,否则使用该结果替换key原来的映射.

这个函数的功能跟如下代码是等效的:

1
2
3
4
5
6
7
8
9
10
11
// Java7及以前跟computeIfPresent()等效的代码
if (map.get(key) != null) {
V oldValue = map.get(key);
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null)
map.put(key, newValue);
else
map.remove(key);
return newValue;
}
return null;
  1. Java8为容器新增一些有用的方法,这些方法有些是为完善原有功能,有些是为引入函数式编程,学习和使用这些方法有助于我们写出更加简洁有效的代码.
  2. 函数接口虽然很多,但绝大多数时候我们根本不需要知道它们的名字,书写Lambda表达式时类型推断帮我们做了一切.

方法引用

方法引用(Method Reference)是用来直接访问类或者实例已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。

当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。

作用

  • 方法引用的唯一用途是支持Lambda的简写。
  • 方法引用提高了代码的可读性,也使逻辑更加清晰。

组成

  • 使用::操作符将方法名和对象或类的名字分隔开。::是域操作符(也可以称作定界符、分隔符)。
方法引用 等价的Lambda表达式
String::valueOf x -> String.valueOf(x)
Object::toString x -> x.toString()
x::toString () -> x.toString()
ArrayList::new () -> new ArrayList<>()

方法引用的类型归结如下:

类型 例子
静态方法引用 ClassName::methodName
指定对象实例方法引用 instanceRef::methodName
特定类型任意对象方法引用 ContainingType::methodName
超类方法引用 supper::methodName
构造器方法引用 ClassName::new
数组构造器方法引用 TypeName[]::new

可见其基本形式是:方法容器::方法名称或者关键字

举一些基本的使用例子:

静态方法引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StaticMethodRef {

public static void main(String[] args) {
Function<String, Integer> function = StaticMethodRef::staticMethod;
// 等同于
// Function<String, Integer> function1 = (String s) -> StaticMethodRef.staticMethod(s);
Integer result = function.apply("10086");
// 10086
System.out.println(result);
}

public static Integer staticMethod(String value) {
return Integer.parseInt(value);
}

}

指定对象实例方法引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ParticularInstanceRef {

public Integer refMethod(String value) {
return Integer.parseInt(value);
}

public static void main(String[] args) {
ParticularInstanceRef ref = new ParticularInstanceRef();
Function<String, Integer> function = ref::refMethod;
// 等同于
// Function<String,Integer> function1 = (String s) -> ref.refMethod(s);
Integer result = function.apply("10086");
// 10086
System.out.println(result);
}
}

特定类型任意对象方法引用

1
2
3
4
5
6
String[] stringArray = {"C", "a", "B"};
Arrays.sort(stringArray, String::compareToIgnoreCase);
// 等同于
// Arrays.sort(stringArray, (String s1, String s2) -> s1.compareToIgnoreCase(s2));
// [a, B, C]
System.out.println(Arrays.toString(stringArray));

超类方法引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SupperRef {

public static void main(String[] args) throws Exception {
Sub sub = new Sub();
// 10086
System.out.println(sub.refMethod("10086"));
}

private static class Supper {
private Integer supperRefMethod(String value) {
return Integer.parseInt(value);
}
}

private static class Sub extends Supper {
private Integer refMethod(String value) {
Function<String, Integer> function = super::supperRefMethod;
// 等同于
// Function<String,Integer> function1 = (String s) -> super.supperRefMethod(s);
return function.apply(value);
}
}

}

构造器方法引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ConstructorRef {

public static void main(String[] args) {
Function<String, Person> function = Person::new;
// 等同于
// Function<String,Person> function1 = (String s) -> new Person(s);
Person person = function.apply("thinkwon");
// doge
System.out.println(person.getName());
}

private static class Person {

private final String name;

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

public String getName() {
return name;
}
}

}

数组构造器方法引用

1
2
3
4
5
6
7
Function<Integer, Integer[]> function = Integer[]::new;
// 等同于
// Function<Integer, Integer[]> function1 = (Integer i) -> new Integer[i];
Integer[] array = function.apply(10);
// [null, null, null, null, null, null, null, null, null, null]
System.out.println(Arrays.toString(array));


Filter & Predicate

常规用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(args[]){
List languages = Arrays.asList("Java", "Scala", "C++", "Haskell", "Lisp");

System.out.println("Languages which starts with J :");
filter(languages, (str)->str.startsWith("J"));

System.out.println("Languages which ends with a ");
filter(languages, (str)->str.endsWith("a"));

System.out.println("Print all languages :");
filter(languages, (str)->true);

System.out.println("Print no language : ");
filter(languages, (str)->false);

System.out.println("Print language whose length greater than 4:");
filter(languages, (str)->str.length() > 4);
}

public static void filter(List names, Predicate condition) {
names.stream().filter((name) -> (condition.test(name))).forEach((name) -> {
System.out.println(name + " ");
});
}

多个Predicate组合filter

1
2
3
4
5
6
7
// 可以用and()、or()和xor()逻辑函数来合并Predicate,
// 例如要找到所有以J开始,长度为四个字母的名字,你可以合并两个Predicate并传入
Predicate<String> startsWithJ = (n) -> n.startsWith("J");
Predicate<String> fourLetterLong = (n) -> n.length() == 4;
names.stream()
.filter(startsWithJ.and(fourLetterLong))
.forEach((n) -> System.out.print("nName, which starts with 'J' and four letter long is : " + n));

Spring Bean命名策略

概述

当我们有多个相同类型的实现时,命名Spring bean非常有用。这是因为如果我们的 bean 没有唯一的名称,Spring 将不明确注入 bean。

通过控制 bean 的命名,我们可以告诉 Spring 我们要将哪个 bean 注入目标对象。

在本文中,我们将讨论 Spring bean 命名策略,并探讨如何为单一类型的 bean 赋予多个名称。

默认 Bean 命名策略

Spring为创建 bean提供了多个注解。我们可以在不同级别使用这些注解。例如,我们可以在 bean 类上放置一些注解,而在创建 bean 的方法上放置其他注解。

首先,让我们看看 Spring 的默认命名策略。当我们只指定注解而没有任何值时,Spring 如何命名我们的 bean?

类级注解

让我们从在类级别使用的注解的默认命名策略开始。为了给 bean 命名,Spring 使用类名并将第一个字母转换为小写

我们来看一个例子:

1
2
3
@Service
public class LoggingService {
}

在这里,Spring 为LoggingService类创建了一个 bean,并使用名称loggingService注册它。

这个相同的默认命名策略适用于用于创建 Spring bean 的所有类级注解,例如@Component@Service@Controller

方法级注解

Spring 提供了诸如@Bean@Qualifier之类的注解,用于在方法上创建 bean 。

让我们看一个例子来理解@Bean注解的默认命名策略:

1
2
3
4
5
6
7
@Configuration
public class AuditConfiguration {
@Bean
public AuditService audit() {
return new AuditService();
}
}

在这个配置类中,Spring在名称“ audit ”下注册了一个AuditService类型的 bean,因为当我们在方法上使用@Bean注解时,Spring 使用方法名称作为 bean 名称。

我们也可以在方法上使用@Qualifier注解。

Bean 的自定义命名

当我们需要在同一个 Spring 上下文中创建多个相同类型的 bean 时,我们可以为 bean 指定自定义名称并使用这些名称引用它们。

那么,让我们看看如何为我们的 Spring bean 指定一个自定义名称:

类级注解

1
2
3
@Component("myBean")
public class MyCustomComponent {
}

这一次,Spring 将创建名为“ myBean ”的MyCustomComponent类型的 bean 。

由于我们明确地为 bean 指定名称,Spring 将使用此名称,然后可以使用该名称来引用或访问 bean。

@Component(“myBean”)类似,我们可以使用@Service(“myService”)@Controller(“myController”)@Bean(“myCustomBean”)等其他注解指定名称,然后Spring 会注册具有给定名称的那个 bean。

方法级注解

正如我们之前看到的,@Bean注解是在方法级别应用的,默认情况下,Spring 使用方法名称作为 bean 名称。

这个默认的 bean 名称可以被覆盖——我们可以使用@Bean注解指定值:

1
2
3
4
5
6
7
@Configuration
public class MyConfiguration {
@Bean("beanComponent")
public MyCustomComponent myComponent() {
return new MyCustomComponent();
}
}

在这种情况下,当我们想要获取一个MyCustomComponent类型的 bean 时,我们可以使用名称“ beanComponent ”来引用这个 bean 。

Spring @Bean注解通常在配置类方法中声明。它可以通过直接调用来引用同一个类中的其他@Bean方法。

总结


Spring ApplicationContext

ApplicationContext接口

Spring 框架的主要特性之一是IoC(控制反转)容器。在Spring IoC容器负责管理应用程序的对象。它使用依赖注入来实现控制反转。

BeanFactoryApplicationContext 接口代表 Spring IoC 容器。在这里,BeanFactory是访问 Spring 容器的根接口。它提供了管理 bean 的基本功能。

另一方面,ApplicationContextBeanFactory的子接口。因此,它提供了BeanFactory所有功能

此外,它还提供了 更多特定于企业的功能ApplicationContext的重要特性是解析消息、支持国际化、发布事件和应用层特定上下文。这就是我们将其用作默认 Spring 容器的原因。

在容器中配置Bean

正如我们所知,ApplicationContext的主要工作是管理 bean。

因此,应用程序必须向ApplicationContext容器提供 bean 配置。一个 Spring bean 配置由一个或多个 bean 定义组成。此外,Spring 支持不同的 bean 配置方式。

基于 Java 的配置

首先,我们将从基于 Java 的配置开始,因为它是最新和最受欢迎的 bean 配置方式。它从 Spring 3.0 开始可用。

Java 配置通常在@Configuration类中使用带有 @Bean 注解的方法。方法上的@Bean注解表明该方法创建了一个 Spring bean。此外,用@Configuration注解的类表示它包含Spring bean 配置。

现在让我们创建一个配置类来将我们的AccountService类定义为 Spring bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class AccountConfig {

@Bean
public AccountService accountService() {
return new AccountService(accountRepository());
}

@Bean
public AccountRepository accountRepository() {
return new AccountRepository();
}
}

基于注解的配置

Spring 2.5 引入了基于注解的配置,作为在 Java 中启用 bean 配置的第一步。

在这种方法中,我们首先通过XML配置启用基于注解的配置。然后我们在 Java 类、方法、构造函数或字段上使用一组注解来配置 bean。这些注解的一些示例是@Component@Controller@Service@Repository@Autowired@Qualifier

值得注意的是,我们也将这些注解用于基于 Java 的配置。另外值得一提的是,Spring 会在每个版本中不断为这些注解添加更多功能。

现在让我们看一个这个配置的简单例子。

首先,我们将创建 XML 配置user-bean-config.xml以启用注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>
<context:component-scan base-package="com.baeldung.applicationcontext"/>

</beans>

在这里,该注解的配置标签启用基于注解的映射。该组件扫描标签也告诉Spring到哪里寻找注解类。

其次,我们将创建UserService类并使用@Component注解将其定义为 Spring bean :

1
2
3
4
@Component
public class UserService {
// user service code
}

基于 XML 的配置

最后,让我们看一下基于 XML 的配置。这是在 Spring 中配置 bean 的传统方式。

显然,在这种方法中,我们在一个 XML 配置文件中完成所有bean 映射

因此,让我们创建一个 XML 配置文件account-bean-config.xml,并为我们的AccountService类定义 bean :

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="accountService" class="com.baeldung.applicationcontext.AccountService">
<constructor-arg name="accountRepository" ref="accountRepository" />
</bean>

<bean id="accountRepository" class="com.baeldung.applicationcontext.AccountRepository" />
</beans>

ApplicationContext 的类型

Spring 提供了适合不同需求的不同类型的ApplicationContext容器。这些是ApplicationContext接口的实现。那么让我们来看看ApplicationContext 的一些常见类型。

AnnotationConfigApplicationContext

首先我们来看一下Spring 3.0中引入的AnnotationConfigApplicationContext类。它可以采取与注解的类@Configuration@Component,和JSR-330的元数据作为输入。从Java的配置类中加载上下文定义。

因此,让我们看一个使用AnnotationConfigApplicationContext容器和基于 Java 的配置的简单示例:

1
2
ApplicationContext context = new AnnotationConfigApplicationContext(AccountConfig.class);
AccountService accountService = context.getBean(AccountService.class);

AnnotationConfigWebApplicationContext

适用于 Web 应用下xml文件中加载上下文,使用注解方式加载上下文

当我们 在web.xml文件中配置 Spring 的ContextLoaderListener servlet侦听器或Spring MVC DispatcherServlet时,我们可能会使用这个类。

此外,从 Spring 3.0 开始,我们还可以通过编程方式配置这个应用程序上下文容器。我们需要做的就是实现WebApplicationInitializer接口:

1
2
3
4
5
6
7
8
9
10
public class MyWebApplicationInitializer implements WebApplicationInitializer {

public void onStartup(ServletContext container) throws ServletException {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AccountConfig.class);
context.setServletContext(container);

// servlet configuration
}
}

XmlWebApplicationContext

如果我们在 Web 应用程序中使用基于 XML 的配置,我们可以使用XmlWebApplicationContext类。

事实上,配置这个容器就像AnnotationConfigWebApplicationContext类,也就是说我们可以在web.xml配置它, 或者实现WebApplicationInitializer接口:

1
2
3
4
5
6
7
8
9
10
public class MyXmlWebApplicationInitializer implements WebApplicationInitializer {

public void onStartup(ServletContext container) throws ServletException {
XmlWebApplicationContext context = new XmlWebApplicationContext();
context.setConfigLocation("/WEB-INF/spring/applicationContext.xml");
context.setServletContext(container);

// Servlet configuration
}
}

FileSystemXMLApplicationContext

我们使用FileSystemXMLApplicationContext从文件系统或 URL加载基于 XML 的 Spring 配置文件。当我们需要以编程方式加载ApplicationContext时,这个类很有用。一般来说,测试工具和独立应用程序是一些可能的用例。

例如,让我们看看如何创建这个 Spring 容器并为基于 XML 的配置加载 bean:

1
2
3
4
String path = "C:/myProject/src/main/resources/applicationcontext/account-bean-config.xml";

ApplicationContext context = new FileSystemXmlApplicationContext(path);
AccountService accountService = context.getBean("accountService", AccountService.class);

ClassPathXmlApplicationContext

如果我们想从 classpath 加载 XML 配置文件,我们可以使用ClassPathXmlApplicationContext类。与FileSystemXMLApplicationContext类似它对于测试工具以及嵌入在 JAR 中的应用程序上下文很有用。

那么让我们看一个使用这个类的例子:

1
2
ApplicationContext context = new ClassPathXmlApplicationContext("applicationcontext/account-bean-config.xml");
AccountService accountService = context.getBean("accountService", AccountService.class);

BeanFactory 和 ApplicationContext 的区别

Spring 框架带有两个 IOC 容器—— BeanFactoryApplicationContext。该Bean工厂是IOC容器的最基本的版本,以及ApplicationContext的扩展的功能Bean工厂

我们将通过实际示例了解这两个 IOC 容器之间的显着差异。

ApplicationContext的带有先进的功能,包括正朝着企业应用面向好几个,而Bean工厂仅预装了基本功能。因此,通常建议使用ApplicationContext的,并且只有当内存消耗是至关重要的时候,我们才应该使用Bean工厂

@ComponentScan

在这篇文章中,我们将看到@ComponentScan注解可用的不同类型的过滤器选项 。

@ ComponentScan过滤器

默认情况下,使用@Component@Repository@Service@Controller注解的被注册为Spring beans。对于使用@Component注解的自定义注解的类也是如此。我们可以通过使用@ComponentScan注解的includeFiltersexcludeFilters参数 来扩展此行为。

ComponentScan.Filter有五种类型的过滤器:

  • ANNOTATION 按照注解过滤
  • ASSIGNABLE_TYPE 按照给定的类型
  • ASPECTJ 使用ASPECTJ表达式
  • REGEX 正则表达式
  • CUSTOM 自定义规则

FilterType.ANNOTATION

例如,假设我们有一个@Animal注解:

1
2
3
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Animal { }

现在,让我们定义一个 使用*@AnimalElephant*类:

1
2
@Animal
public class Elephant { }

最后,让我们使用FilterType.ANNOTATION告诉 Spring 扫描 @Animal 注解的类:扫描仪很好地拾取了我们的Elephant

1
2
3
4
@Configuration
@ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,
classes = Animal.class))
public class ComponentScanAnnotationFilterApp { }

FilterType.ASSIGNABLE_TYPE

首先,让我们声明Animal 接口:

1
public interface Animal { }

再一次,让我们声明我们的Elephant类,这次实现Animal接口

1
public class Elephant implements Animal { }

让我们声明我们的Cat类也实现了Animal:

1
public class Cat implements Animal { }

现在,让我们使用ASSIGNABLE_TYPE来引导 Spring 扫描Animal实现类:

1
2
3
4
@Configuration
@ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
classes = Animal.class))
public class ComponentScanAssignableTypeFilterApp { }

CatElephant 都被扫描到。

FilterType.REGEX

再一次,让我们声明我们的Elephant类。这次没有实现任何接口或使用任何注解进行注解

1
public class Elephant { }

让我们再声明一个类Cat

1
public class Cat { }

现在,让我们声明Loin类:

1
public class Loin { }

让我们使用FilterTypeREGEX指示 Spring 扫描与正则表达式.*[nt]匹配的类我们的正则表达式计算所有包含nt 的内容:

1
2
3
4
@Configuration
@ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.REGEX,
pattern = ".*[nt]"))
public class ComponentScanRegexFilterApp { }

这次在我们的测试中,我们将看到 Spring 扫描Elephant,而不是Lion

FilterType.ASPECTJ

对于这个用例,我们可以重用与上一节相同的三个类。

让我们使用FilterType.ASPECTJ来指示 Spring 扫描与我们的AspectJ表达式匹配的类:

1
2
3
4
5
6
@Configuration
@ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ASPECTJ,
pattern = "com.baeldung.componentscan.filter.aspectj.* "
+ "&& !(com.baeldung.componentscan.filter.aspectj.L* "
+ "|| com.baeldung.componentscan.filter.aspectj.C*)"))
public class ComponentScanAspectJFilterApp { }

虽然有点复杂,但我们这里的逻辑希望 bean 的类名中既不以“L”也不以“C”开头,因此我们又得到了Elephant s


@Qualifier

Autowire 消除歧义的需要

当需要自动注入特定精确的Bean时,@Autowire是一种很好的方式,尽管它很有用,但在某些用例中,仅此注解不足以让 Spring 了解要注入哪个 bean。

默认情况下,Spring 按类型解析自动装配的条目。

如果容器中有多个相同类型的 bean,则框架将抛出NoUniqueBeanDefinitionException, 表明有多个 bean 可用于自动装配。

让我们想象这样一种情况,其中 Spring 存在两个可能的候选者作为 bean 协作者注入给定实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component("fooFormatter")
public class FooFormatter implements Formatter {

public String format() {
return "foo";
}
}

@Component("barFormatter")
public class BarFormatter implements Formatter {

public String format() {
return "bar";
}
}

@Component
public class FooService {

@Autowired
private Formatter formatter;
}

如果我们尝试将FooService加载到我们的上下文中,Spring 框架将抛出一个NoUniqueBeanDefinitionException。这是因为Spring 不知道要注入哪个 bean。为了避免这个问题,有几种解决方案;该@Qualifier注解就是其中之一。

@ Qualifier注解

通过使用@Qualifier注解,我们可以消除需要注入哪个bean的问题

让我们回顾一下之前的示例,看看我们如何通过包含*@Qualifier*注解来指示我们要使用哪个 bean 来解决问题:

1
2
3
4
5
6
public class FooService {

@Autowired
@Qualifier("fooFormatter")
private Formatter formatter;
}

通过包含*@Qualifier注解,连同我们想要使用的具体实现的名称,在这个例子中为Foo,*我们可以避免当 Spring 找到多个相同类型的 bean 时产生歧义。

我们需要考虑到要使用的限定符名称是@Component注解中声明的名称。

请注意,我们也可以在Formatter实现类上使用@Qualifier注解,而不是在它们的@Component注解中指定名称,以获得相同的效果:

1
2
3
4
5
6
7
8
9
10
11
@Component
@Qualifier("fooFormatter")
public class FooFormatter implements Formatter {
//...
}

@Component
@Qualifier("barFormatter")
public class BarFormatter implements Formatter {
//...
}

@Qualifier VS @Primary

还有另一个名为@Primary的注解 ,当依赖注入存在歧义时,我们可以使用它来决定注入哪个 bean。

当存在多个相同类型的 bean 时,此注解定义了一个首选项。除非另有说明,否则将使用与@Primary注解关联的 bean 。

让我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class Config {

@Bean
public Employee johnEmployee() {
return new Employee("John");
}

@Bean
@Primary
public Employee tonyEmployee() {
return new Employee("Tony");
}
}

在此示例中,两种方法都返回相同的Employee类型。Spring 将注入的 bean 是tonyEmployee方法返回的bean。这是因为它包含@Primary注解。当我们想要指定默认情况下应该注入哪个特定类型的 bean时,此注解很有用。

如果我们在某个注入时需要另一个 bean,我们需要特别指出它。我们可以通过*@Qualifier注解来做到这一点。例如,我们可以通过使用@Qualifier注解来指定我们要使用johnEmployee*方法返回的 bean 。

值得注意的是,如果@Qualifier和@Primary注解都存在,那么@Qualifier注解将具有优先权。基本上,@Primary定义了一个默认值,而@Qualifier则非常具体。


理解 Spring 中的 getBean()

我们将介绍BeanFactory.getBean()方法的不同变体。

简而言之,正如该方法的名称所暗示的那样,它负责从 Spring 容器中检索 bean 实例

Spring Beans 设置

首先,让我们定义几个 Spring bean 进行测试。我们可以通过多种方式为 Spring 容器提供 bean 定义,但在我们的示例中,我们将使用基于注解的 Java 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
class AnnotationConfig {

@Bean(name = {"tiger", "kitty"})
@Scope(value = "prototype")
Tiger getTiger(String name) {
return new Tiger(name);
}

@Bean(name = "lion")
Lion getLion() {
return new Lion("Hardcoded lion name");
}

interface Animal {}
}

我们已经创建了两个 bean。Lion具有默认的单例范围。Tiger明确设置为原型范围。此外,请注意,我们为将在进一步请求中使用的每个 bean 定义了名称。

getBean() API

BeanFactory提供了getBean()方法的五种不同签名

按名称检索 Bean

让我们看看如何使用名称检索Lion bean 实例:

1
2
3
Object lion = context.getBean("lion");

assertEquals(Lion.class, lion.getClass());

在这个变体中,我们提供一个名称,作为回报,如果应用程序上下文中存在具有给定名称的 bean ,我们将获得一个Object 类的实例。否则,如果 bean 查找失败,则此实现和所有其他实现都将抛出NoSuchBeanDefinitionException

主要的缺点是在检索 bean 后,我们必须将其强制转换为所需的类型如果返回的 bean 的类型与我们预期的不同,这可能会产生另一个异常。

按名称和类型检索 Bean

这里我们需要指定请求的 bean 的名称和类型:

1
Lion lion = context.getBean("lion", Lion.class);

与前一种方法相比,这种方法更安全,因为我们可以立即获取有关类型不匹配的信息。

按类型检索 Bean

使用getBean()的第三个变体, 仅指定 bean 类型就足够了:

1
Lion lion = context.getBean(Lion.class);

在这种情况下,我们需要特别注意一个潜在的模棱两可的结果

在上面的示例中,由于LionTiger 都实现了Animal接口,因此仅指定类型不足以明确确定结果。因此,我们得到一个NoUniqueBeanDefinitionException

使用构造函数参数按名称检索 Bean

除了 bean 名称,我们还可以传递构造函数参数:

1
Tiger tiger = (Tiger) context.getBean("tiger", "Siberian");

这个方法有点不同,因为它只适用于具有原型作用域的 bean

在单例的情况下,我们将得到一个BeanDefinitionStoreException

因为原型 bean 每次从应用程序容器请求时都会返回一个新创建的实例,所以我们可以在调用getBean()即时提供构造函数参数

1
2
3
4
5
Tiger tiger = (Tiger) context.getBean("tiger", "Siberian");
Tiger secondTiger = (Tiger) context.getBean("tiger", "Striped");

assertEquals("Siberian", tiger.getName());
assertEquals("Striped", secondTiger.getName());

正如我们所见,根据我们在请求 bean 时指定为第二个参数的内容,每个Tiger获得不同的名称。

使用构造函数参数按类型检索 Bean

这个方法类似于最后一个,但我们需要传递类型而不是名称作为第一个参数:

1
2
3
Tiger tiger = context.getBean(Tiger.class, "Shere Khan");

assertEquals("Shere Khan", tiger.getName());

与使用构造函数参数按名称检索 bean 类似,此方法仅适用于具有原型范围的 bean

使用注意事项

尽管在BeanFactory接口中定义了getBean()方法,但它最常通过ApplicationContext访问通常,我们不想在我们的程序中直接使用getBean()方法。

Bean 应该由容器管理。如果我们想使用其中之一,我们应该依赖依赖注入而不是直接调用ApplicationContext.getBean() 这样,我们就可以避免将应用程序逻辑与框架相关的细节混合在一起。


@RequestParam

简单地说,我们可以使用@RequestParam从请求中提取查询参数、表单参数甚至文件。

一个简单的映射

假设我们有一个端点/api/foos,它接受一个名为id的查询参数 :

1
2
3
4
5
@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam String id) {
return "ID: " + id;
}

在这个例子中,我们使用@RequestParam来提取id查询参数。

一个简单的 GET 请求将调用getFoos

1
2
3
http://localhost:8080/api/foos?id=abc
----
ID: abc

接下来,让我们看看注解的属性:name、 value、required和defaultValue。

指定请求参数名称

幸运的是,我们可以使用name属性配置@RequestParam名称

1
2
3
4
5
@PostMapping("/api/foos")
@ResponseBody
public String addFoo(@RequestParam(name = "id") String fooId, @RequestParam String name) {
return "ID: " + fooId + " Name: " + name;
}

我们也可以做 @RequestParam(value = “id”)或者只是@RequestParam(“id”)

可选的请求参数

默认用@RequestParam注解的方法参数 是必须的。

这意味着如果请求中不存在该参数,我们将收到错误消息:

1
2
3
4
GET /api/foos HTTP/1.1
-----
400 Bad Request
Required String parameter 'id' is not present

使用required 属性将@RequestParam配置为可选:

1
2
3
4
5
@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam(required = false) String id) {
return "ID: " + id;
}

在这种情况下,两者:

1
2
3
http://localhost:8080/api/foos?id=abc
----
ID: abc

1
2
3
http://localhost:8080/api/foos
----
ID: null

将正确调用该方法。

当未指定参数时,方法参数绑定到null。

请求参数的默认值

我们还可以 使用defaultValue属性为@RequestParam设置默认值:

1
2
3
4
5
@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam(defaultValue = "test") String id) {
return "ID: " + id;
}

这就像required=false, 因为用户不再需要提供参数

1
2
3
http://localhost:8080/api/foos
----
ID: test

尽管如此,我们仍然可以提供它:

1
2
3
http://localhost:8080/api/foos?id=abc
----
ID: abc

请注意,当我们设置 defaultValue 属性时, required确实设置为false

映射所有参数

我们也可以有多个参数,而无需定义它们的名称或计数,只需使用Map

1
2
3
4
5
@PostMapping("/api/foos")
@ResponseBody
public String updateFoos(@RequestParam Map<String,String> allParams) {
return "Parameters are " + allParams.entrySet();
}

然后将反映发送的任何参数:

1
2
3
curl -X POST -F 'name=abc' -F 'id=123' http://localhost:8080/api/foos
-----
Parameters are {[name=abc], [id=123]}

映射多值参数

单个@RequestParam可以有多个值:

1
2
3
4
5
@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam List<String> id) {
return "IDs are " + id;
}

Spring MVC 将映射一个逗号分隔的 id 参数

1
2
3
http://localhost:8080/api/foos?id=1,2,3
----
IDs are [1,2,3]

或单独的id参数列表

1
2
3
http://localhost:8080/api/foos?id=1&id=2
----
IDs are [1,2]

@Component vs @Repository 和 @Service 在 Spring 中区别

在大多数典型的应用程序中,我们有不同的层,如数据访问、表示、服务、业务等。

此外,在每一层中,我们都有不同的 bean。为了自动检测这些 bean,Spring 使用类路径扫描注释

然后它在ApplicationContext 中注册每个 bean 。

以下是其中一些注释的快速概览:

  • @Component是任何 Spring 管理的组件的通用构造型。
  • @Service在服务层注释类。
  • @Repository在持久层注释类,它将充当数据库存储库。

有什么不同?

这些刻板印象之间的主要区别在于它们用于不同的分类。当我们为自动检测注释一个类时,我们应该使用相应的构造型。

现在让我们更详细地了解它们。

@Component

我们可以在整个应用程序中使用 @Component 将 bean 标记为 Spring 的托管组件@Component是任何 Spring 管理的组件的通用构造型。Spring 只会使用@Component获取和注册 bean ,一般不会查找@Service@Repository

它们在ApplicationContext中注册,因为它们用@Component作为元注解:

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
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {

/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
*/
@AliasFor(annotation = Component.class)
String value() default "";

}


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {

/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
*/
@AliasFor(annotation = Component.class)
String value() default "";

}

@Service 和 @Repository是@Component的特例,相当于给@Component 起的别名。它们在技术上是相同的,但我们将它们用于不同的目的。

@Repository

@Repository的工作是捕获特定于持久性的异常并将它们作为 Spring 的统一未检查异常之一重新抛出

为此,Spring 提供了PersistenceExceptionTranslationPostProcessor,我们需要将其添加到我们的应用程序上下文中(如果我们使用 Spring Boot,则已包含):

1
2
<bean class=
"org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

这个 bean 后处理器向任何用*@Repository*注释的 bean 添加了一个顾问

@Service

我们用 @Service 标记 bean 以表明它们持有业务逻辑。除了在服务层使用之外,这个注解没有任何其他特殊用途。


Spring 中基于 XML 的注入

让我们从在pom.xml 中添加 Spring 的库依赖开始:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>

依赖注入——概述

依赖注入是一种技术,其中对象的依赖项由外部容器提供。

假设我们有一个依赖于实际处理业务逻辑的服务的应用程序类:

1
2
3
4
public class IndexApp {
private IService service;
// standard constructors/getters/setters
}

现在让我们说IService是一个接口:

1
2
3
public interface IService {
public String serve();
}

这个接口可以有多个实现。

让我们快速看一下一个潜在的实现:

1
2
3
4
5
6
public class IndexService implements IService {
@Override
public String serve() {
return "Hello World";
}
}

这里,IndexApp是一个依赖于名为IService的低级组件的高级组件。

从本质上讲,我们将IndexAppIService的特定实现分离,该实现可能因各种因素而异。

依赖注入

使用属性注入

让我们看看如何使用基于 XML 的配置将依赖项连接在一起:

1
2
3
4
5
6
7
8
<bean 
id="indexService"
class="com.baeldung.di.spring.IndexService" />
<bean
id="indexApp"
class="com.baeldung.di.spring.IndexApp" >
<property name="service" ref="indexService" />
</bean>

可以看出,我们正在创建一个IndexService实例并为其分配一个 id。默认情况下,bean 是单例。此外,我们正在创建IndexApp的实例。

在这个 bean 中,我们使用 setter 方法注入另一个 bean。

使用构造函数注入

我们可以使用构造函数注入依赖项,而不是通过 setter 方法注入 bean:

1
2
3
4
5
<bean 
id="indexApp"
class="com.baeldung.di.spring.IndexApp">
<constructor-arg ref="indexService" />
</bean>

使用静态工厂

我们也可以注入一个由工厂返回的 bean。让我们创建一个简单的工厂,根据提供的数字返回IService的实例:

1
2
3
4
5
public class StaticServiceFactory {
public static IService getNumber(int number) {
// ...
}
}

现在让我们看看我们如何使用上述实现使用基于 XML 的配置将 bean 注入到IndexApp 中

1
2
3
4
5
6
7
8
9
<bean id="messageService"
class="com.baeldung.di.spring.StaticServiceFactory"
factory-method="getService">
<constructor-arg value="1" />
</bean>

<bean id="indexApp" class="com.baeldung.di.spring.IndexApp">
<property name="service" ref="messageService" />
</bean>

在上面的例子中,我们使用factory-method调用静态getService方法来创建一个带有 id messageService的 bean ,我们将其注入IndexApp。

使用工厂方法

让我们考虑一个实例工厂,它根据提供的数字返回一个IService实例。这一次,该方法不是静态的:

1
2
3
4
5
public class InstanceServiceFactory {
public IService getNumber(int number) {
// ...
}
}

现在让我们看看我们如何使用上面的实现来使用 XML 配置将 bean 注入到IndexApp 中

1
2
3
4
5
6
7
8
9
10
<bean id="indexServiceFactory" 
class="com.baeldung.di.spring.InstanceServiceFactory" />
<bean id="messageService"
class="com.baeldung.di.spring.InstanceServiceFactory"
factory-method="getService" factory-bean="indexServiceFactory">
<constructor-arg value="1" />
</bean>
<bean id="indexApp" class="com.baeldung.di.spring.IndexApp">
<property name="service" ref="messageService" />
</bean>

在上面的示例中,我们使用factory-method调用InstanceServiceFactory 实例上的getService方法来创建一个带有 id messageService的 bean ,我们将其注入IndexApp

访问配置的bean:

这是我们如何访问配置的bean:

1
2
3
4
5
6
@Test
public void whenGetBeans_returnsBean() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("...");
IndexApp indexApp = applicationContext.getBean("indexApp", IndexApp.class);
assertNotNull(indexApp);
}

使用带有默认值的 Spring @Value

字符串默认值

让我们看一下为String属性设置默认值的基本语法:

1
2
@Value("${some.key:my default value}")
private String stringWithDefaultValue;

如果some.key无法解析,stringWithDefaultValue将设置为my default value的默认值。

同样,我们可以设置一个零长度的字符串作为默认值:

1
2
@Value("${some.key:})"
private String stringWithBlankDefaultValue;

基本类型默认值

要为基本类型(例如booleanint )设置默认值,我们使用文字值:

1
2
3
4
@Value("${some.key:true}")
private boolean booleanWithDefaultValue;
@Value("${some.key:42}")
private int intWithDefaultValue;

如果我们愿意,我们可以通过将类型更改为BooleanInteger来使用原始包装器。

数组

我们还可以将逗号分隔的值列表注入数组:

1
2
3
4
5
@Value("${some.key:one,two,three}")
private String[] stringArrayWithDefaults;

@Value("${some.key:1,2,3}")
private int[] intArrayWithDefaults;

在上面的第一个示例中,值onetwothree作为默认值注入到stringArrayWithDefaults 中。

使用 SpEL 的高级示例

我们还可以使用 SpEL 表达式来获取值。

如果我们有一个名为priority的系统属性那么它的值将应用于该字段:

1
2
@Value("#{systemProperties['priority']}")
private String spelValue;

如果我们还没有定义系统属性,那么将分配值。

为了防止这种情况,我们可以在 SpEL 表达式中提供一个默认值。如果未定义系统属性,我们会为该字段获取一些默认值:

1
2
@Value("#{systemProperties['unknown'] ?: 'some default'}")
private String spelSomeDefault;

此外,我们可以使用来自其他 bean 的字段值。假设我们有一个名为someBean的 bean ,其字段someValue等于10。然后,将10分配给该字段:

1
2
@Value("#{someBean.someValue}")
private Integer someBeanValue;

我们可以操作属性来获取值列表,这里是字符串值 A、B 和 C 的列表:

1
2
@Value("#{'${listOfValues}'.split(',')}")
private List<String> valuesList;

@Value(“${}”)和@Value(“#{}”)的区别

  • @Value("${}")主要获取的是配置文件 application.yml /application.proterties中的配置信息。
  • @Value("#{}") 表示SpEl表达式通常用来获取bean的属性,或者调用bean的某个方法或属性。当然还有可以表示常量

在下面的示例中,我们希望将some.system.key设置为系统属性,如果未设置,我们希望使用我的默认系统属性值 作为默认值:

1
2
@Value("#{systemProperties['some.key'] ?: 'my default system property value'}")
private String spelWithDefaultValue;

@Autowired vs @Resource vs @Inject 的区别

为了实现依赖注入 DI 而引入,Java 提供javax.annotation.Resource , javax.inject.Inject 注解,Spring 框架提供了 org.springframework.beans.factory.annotation.Autowired。依赖注入(Denpendency Injection,DI), 控制反转(Inversion of Control, IoC),主要的目的是去除代码耦合。

具体解释

Annotation Package Source
@Autowired org.springframework.beans.factory.annotation.Autowire Spring
@Resource javax.annotation.Resource Java
@Inject javax.inject.Inject Java 需额外依赖

@Autowired: Spring 特有的注解,@Autowired 通过类型来注入,比如通过类的类型,或者类的接口来注解 field 或者 constructor。为了防止在项目中实现同一个接口,或者一系列子类,可以使用 @Qualifier 注解来避免歧义。默认情况下 bean 的名字就是 qualifier 的值。 尽管你可以按照约定通过名字来使用 @Autowired 注解,@Autowired 根本上还是类型驱动的注入,并且附带可选的语义上的 qualifiers.

@Inject: 该注解基于 JSR-330, @Inject 注解是 Spring @Autowired 注解的代替品。所以使用 Spring 独有的 @Autowired 注解时,可以考虑选择使用 @Inject. @Autowired 和 @Inject 的不同之处在于是否有required属性,@Inject 没有 required 属性,因此在找不到合适的依赖对象时 inject 会失败,而 @Autowired 可以使用 required=false 来允许 null 注入。

使用 @Inject 需要添加如下依赖:

1
2
3
4
5
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>

Advantage of @Inject annotation is that rather than inject a reference directly, you could ask @Inject to inject a Provider. The Provider interface enables, among other things, lazy injection of bean references and injection of multiple instances of a bean. In case we have few implementation of an interface or a subclass we can narrow down the selection using the @Named annotation to avoid ambiguity. @Named annotation works much like Spring’s @Qualifier

@Resource: JDK 1.6 支持注解,JSR-250 引入。@Resource 和 @Autowired @Inject 类似,最主要的区别在于寻找存在的 Bean 注入的路径不同。@Resource寻找的优先顺序为

  • 1)优先通过名字 (by name)
  • 2)其次是类型 (by type)
  • 3)再次是 qualifier(by qualifier)

@Autowired and @Inject 寻找的顺序为

  1. 通过类型寻找
  2. 通过 qualifier
  3. 最后通过名字寻找

@Resource 如果没有指定 name 属性,当注解标注在 field 上,默认取字段名称作为 bean 名称寻找依赖对象;当标注在属性 setter 方法上,默认取属性名作为 bean 名称寻找依赖。如果没有指定 name 属性,并且按照默认名称找不到依赖对象时,回退到类型装配。


@Autowired

从 Spring 2.5 开始,该框架引入了注解驱动的依赖注入。此功能的主要注释是@Autowired 它允许 Spring 解析并将协作 bean 注入到我们的 bean 中。

启用@Autowired注解

Spring 框架支持自动依赖注入。换句话说,通过在 Spring 配置文件中声明所有 bean 依赖项,Spring 容器可以自动装配协作 bean 之间的关系。这称为Spring bean 自动装配

要在我们的应用程序中使用基于 Java 的配置,让我们启用注解驱动注入 来加载我们的 Spring 配置:

1
2
3
@Configuration
@ComponentScan("com.baeldung.autowire.sample")
public class AppConfig {}

或者,注解主要用于激活 Spring XML 文件中的依赖注入注解。

此外,Spring Boot 引入了@SpringBootApplication注解。这个单一的注解等效于使用@Configuration@EnableAutoConfiguration@ComponentScan

让我们在应用程序的主类中使用这个注解:

1
2
3
4
5
6
@SpringBootApplication
class VehicleFactoryApplication {
public static void main(String[] args) {
SpringApplication.run(VehicleFactoryApplication.class, args);
}
}

因此,当我们运行这个 Spring Boot 应用程序时,它会自动扫描当前包及其子包中的组件。因此,它将在 Spring 的应用程序上下文中注册它们,并允许我们使用@Autowired注入 bean 。

使用@Autowired

启用注解注入后,我们可以在属性、设置器和构造函数上使用自动装配

@Autowired用在属性上

让我们看看如何使用@Autowired注释属性。这消除了对 getter 和 setter 的需要。

首先,让我们定义一个fooFormatter bean:

1
2
3
4
5
6
@Component("fooFormatter")
public class FooFormatter {
public String format() {
return "foo";
}
}

然后,我们将在字段定义上使用*@Autowired将此 bean 注入FooService* bean :

1
2
3
4
5
@Component
public class FooService {
@Autowired
private FooFormatter fooFormatter;
}

其结果是,Spring注入fooFormatterFooService接口被创建。

@Autowired在 Setter 上

现在让我们尝试在 setter 方法上添加 @Autowired 注释。

在以下示例中,在创建FooService时使用FooFormatter实例调用 setter 方法:

1
2
3
4
5
6
7
public class FooService {
private FooFormatter fooFormatter;
@Autowired
public void setFooFormatter(FooFormatter fooFormatter) {
this.fooFormatter = fooFormatter;
}
}

@Autowired在构造函数上

最后,让我们在构造函数上使用 @Autowired 。

我们将看到Spring 注入了一个FooFormatter实例作为FooService构造函数的参数:

1
2
3
4
5
6
7
public class FooService {
private FooFormatter fooFormatter;
@Autowired
public FooService(FooFormatter fooFormatter) {
this.fooFormatter = fooFormatter;
}
}

@Autowired 可选注入

在构建 bean 时,@ Autowired依赖项应该可用。否则,如果 Spring 无法解析用于连接的 bean,它将抛出异常

因此,它会阻止 Spring 容器成功启动,但以下形式除外:

1
2
3
4
5
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No qualifying bean of type [com.autowire.sample.FooDAO] found for dependency:
expected at least 1 bean which qualifies as autowire candidate for this dependency.
Dependency annotations:
{@org.springframework.beans.factory.annotation.Autowired(required=true)}

为了解决这个问题,我们需要required解决

1
2
3
4
public class FooService {
@Autowired(required = false)
private FooDAO dataAccessor;
}

@PropertySource注解

@PropertySource注解是Spring 3.1开始引入的配置类注解。通过@PropertySource注解将properties配置文件中的值存储到Spring的Environment中,Environment接口提供方法去读取配置文件中的值,参数是properties文件中定义的key值。也可以使用@Value注解用${}占位符注入属性

@PropertySource注解的源代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.io.support.PropertySourceFactory;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {
String name() default "";
String[] value();
boolean ignoreResourceNotFound() default false;
String encoding() default "";
Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;
}

@PropertySource的源码可以看出,我们可以通过@PropertySource注解指定多个properties文件,可以使用如下形式进行指定。

1
@PropertySource(value={"classpath:xxx.properties", "classpath:yyy.properties"})

细心的读者可以看到,在@PropertySource注解类的上面标注了如下的注解信息。

1
@Repeatable(PropertySources.class)

看到这里,小伙伴们是不是有种恍然大悟的感觉呢?没错,我们也可以使用@PropertySources注解来指定properties配置文件。

通过注解注册一个配置文件

我们可以将此注解与@Configuration注解结合使用:

1
2
3
4
5
@Configuration
@PropertySource("classpath:foo.properties")
public class PropertiesWithJavaConfig {
//...
}

另一种注册新属性文件的非常有用的方法是使用占位符,它允许我们在运行时动态选择正确的文件

1
2
3
4
@PropertySource({ 
"classpath:persistence-${envTarget:mysql}.properties"
})
...

定义多个属性位置

根据 Java 8 约定,@PropertySource注释是可重复的。因此,如果我们使用 Java 8 或更高版本,我们可以使用此注释来定义多个属性位置:

1
2
3
4
5
@PropertySource("classpath:foo.properties")
@PropertySource("classpath:bar.properties")
public class PropertiesWithJavaConfig {
//...
}

当然,我们也可以使用@PropertySources注解,指定一个@PropertySource数组。这适用于任何受支持的 Java 版本,而不仅仅是 Java 8 或更高版本:

1
2
3
4
5
6
7
@PropertySources({
@PropertySource("classpath:foo.properties"),
@PropertySource("classpath:bar.properties")
})
public class PropertiesWithJavaConfig {
//...
}

在任何一种情况下,值得注意的是,在发生属性名称冲突的情况下,最后一个源读取优先。

使用/注入属性

使用@Value注释注入属性很简单:

1
2
@Value( "${jdbc.url}" )
private String jdbcUrl;

我们还可以为属性指定一个默认值:

1
2
@Value( "${jdbc.url:aDefaultUrl}" )
private String jdbcUrl;

Spring 3.1 中添加的新PropertySourcesPlaceholderConfigurer解析了 bean 定义属性值和@Value注释中的${…} 占位符。

最后,我们可以 使用Environment API获取属性的值

1
2
3
4
@Autowired
private Environment env;
...
dataSource.setUrl(env.getProperty("jdbc.url"));

Spring Boot 的属性

在我们进入更高级的属性配置选项之前,让我们花点时间看看 Spring Boot 中的新属性支持。

一般来说,与标准 Spring 相比这种新支持涉及的配置更少,这当然是 Boot 的主要目标之一。

application.properties:默认属性文件

Boot 将其典型的约定应用于属性文件的配置方法。这意味着我们可以简单地将application.properties文件放在我们的src/main/resources 目录中,它将被自动检测。然后我们可以像往常一样从它注入任何加载的属性。

因此,通过使用此默认文件,我们不必显式注册PropertySource ,甚至不必提供属性文件的路径。

2.4.0版本开始,Spring Boot 支持使用多文档属性文件,类似于YAML的设计:

1
2
3
baeldung.customProperty=defaultValue
#---
baeldung.customProperty=overriddenValue

请注意,对于属性文件,三个破折号符号前面有一个注释字符 ( # )。

特定于环境的属性文件

如果我们需要针对不同的环境,Boot 中有一个内置的机制。

我们可以简单地在src/main/resources目录下定义一个application-environment.properties文件,然后设置一个具有相同环境名称的 Spring 配置文件。

例如,如果我们定义一个“暂存”环境,这意味着我们必须定义一个暂存配置文件,然后是application-staging.properties

此 env 文件将被加载并优先于默认属性文件。注意,默认文件还是会被加载,只是当发生属性冲突时,特定于环境的属性文件优先。替代方案:YAML 文件

Spring 还支持 YAML 文件。

所有相同的命名规则都适用于特定于测试、特定于环境和默认属性文件。唯一的区别是文件扩展名和对我们类路径上的SnakeYAML库的依赖。

YAML 特别适合分层属性存储;以下属性文件:

1
2
3
4
database.url=jdbc:postgresql:/localhost:5432/instance
database.username=foo
database.password=bar
secret: foo

与以下 YAML 文件同义:

1
2
3
4
5
database:
url: jdbc:postgresql:/localhost:5432/instance
username: foo
password: bar
secret: foo

还值得一提的是,YAML 文件不支持*@PropertySource*注解,所以如果我们需要使用这个注解,它会限制我们使用属性文件。

另一个值得注意的地方是,在 2.4.0 版本中,Spring Boot 改变了从多文档 YAML 文件加载属性的方式。以前,它们的添加顺序基于配置文件激活顺序。然而,在新版本中,框架遵循我们之前为*.properties*文件指出的相同排序规则;在文件中声明较低的属性将简单地覆盖那些较高的属性。

此外,在此版本中,配置文件无法再从配置文件特定文档中激活,从而使结果更清晰、更可预测。

批量更改文件名称

对我而言这个功能最好用的是批量更改时替换功能,在截了一堆图需要改名字的时候就可以派上用场。

聚焦Spotlight

“聚焦”可以帮助您快速找到 Mac 上的 App、文稿和其他文件。借助 Siri 建议,您还可以获取最新新闻、体育赛事比分、天气状况等信息。“聚焦”甚至可以为您进行计算和转换。

搜索内容

  1. 在 Mac 上,点按菜单栏中的“聚焦”图标 img(如果显示),或者按下 Command-空格键或按下键盘上功能键行中的 聚焦键(如果可用)。

    您可以将“聚焦”窗口拖到桌面上的任意位置。

    【提示】如果“聚焦”图标未显示在菜单栏中,请使用“程序坞与菜单栏”偏好设置进行添加。

  2. 在搜索栏中,键入要查找的内容,键入时结果会随之出现。

    “聚焦”首先列出最常点选,点按一个最常点选以进行预览或打开。“聚焦”还会建议搜索的各种变体;您可以在“聚焦”或网上查看这些结果。

    “聚焦”窗口顶部显示了搜索栏中的搜索文本,下方是结果。

  3. 在结果中,执行以下任一项操作:

    • 显示预览:按下 Tab 键。根据结果中所选的项目类型,您可以与之交互。例如,选择 App 以查看最近打开文件的列表,然后点按以打开文件。或者点按“播放”按钮来聆听歌曲。

在“聚焦”中进行计算和转换

您可以在“聚焦”搜索栏中输入数学表达式、金额、温度或单位,搜索栏中会立即显示转换或计算结果。

“聚焦”窗口,显示搜索栏中从码到米的转换。左侧是搜索结果列表。右侧预览中显示更多转换。

  • 计算:输入数学表达式,如 956*23.94 或 2020/15。
  • 货币转换:输入金额,如 100 美元、100 日元或“300 克朗转换成欧元”。
  • 温度转换:输入温度,如 98.8F、32C 或“340K 转换成华氏度”。
  • 单位转换:输入度量单位,例如 25 磅、54 码、23 英石或“32 英尺转换成米”。

【提示】按下 Tab 键以在预览区域中显示更多转换。

快速添加emoji表情

按住键盘Control+Command+空格键,此时你的输入栏中,就会跳出所有表情了。

快速锁屏

只需按住control+command+Q,此时你的MacBook就会马上锁屏


向后删除

文本编辑时按 Fn + Delete 键可向后删除内容


切换应用程序

按住command键再按tab键,可从左往右切换已打开的应用程序;command+shift+tab,可从右往左切换


触控板手势

Mac触控板强大无比,掌握触控板使用技能,对提高学习工作效率大有帮助,打开Mac系统偏好设置中的触控板,选择相应手势,右边会有动画教程。

还有一个比较特殊的手势,三指拖动,需在系统偏好设置下的辅助功能中设置。


设置文件默认打开方式

不同格式的文件有不同的默认打开方式,比如视频文件会默认用QuickTime Player打开,如果你想更改系统默认的打开方式,按照如下步骤设置即可,

单击你想要改变默认打开方式的文件,右键单击选择显示简介,或者按快捷键Command+i 显示简介;

在打开方式的下拉菜单中选择你需要的默认打开方式,点击全部更改,在弹出的窗口中选择继续即可。


自带截屏

  • cmd+shift+3:对整个屏幕进行截图;

  • cmd+shift+4:对自行选择的区域进行截图;

  • cmd+shift+4+space(空格键):对选定的某个应用程序界面窗口进行截图。


Command(或 Cmd)⌘键

以下是常用的12个快捷键:

  • Command + X: 剪切
  • Command + C: 拷贝
  • Command + V: 原格式粘贴
  • Command + Shift +Option +V: 合并格式粘贴
  • Command + Z: 撤销上一个操作(Shift + Command + Z: 反向执行撤销命令)
  • Command + W: 关闭但不退出应用
  • Command + Q: 退出应用
  • Command + Option + Esc: 强制退出应用
  • Command + Tab: 切换应用
  • Command + 空格: “聚焦”查找
  • Command + Shift + 5(MacOS Mojave系统): 截屏或录屏(其他MacOS: Command + Shift +3: 捕捉整个屏幕;Command + Shift +4: 自选截屏区域)
  • Command + Control + 空格: 添加emoji

Command键除了上面 提到的常用组合快捷键外,还有很多其他功能,这里介绍四个关于Command的隐藏功能

  • 按住Command,可以选择并移动菜单栏图标

  • 在“聚焦”中搜索文件,按住Command就可以显示文件路径,按住Command打开文件可以打开所在的文件夹

Option(或 Alt)⌥键

  • 按住Option,可以同时关闭同个程序的多个窗口

  • 按住Option,单击右侧翻页条任何位置,就可以快速跳到指定位置

  • 『移动文件』CMD+C 复制文件,在目标目录 CMD+Option+V,就把原文件移动过来了,相当于剪切粘贴。

  • 按住Option+拖到文件,可快速复制一份原文件

0%