0%

1. 组提交介绍

1.1 什么是组提交

  Binary Log Group Commit 即二进制日志组提交。这是 MySQL5.6 版本中引进的一个新的特性。为什么需要引进这个特性呢?我们知道当我们把 MySQL 的 binlog 开启的时候,MySQL 会将每个事务的操作都记录到 binlog 中,方便我们使用 binlog 来完成复制或者恢复操作。可是需要调用 fsync() 才能将缓存中被更改的 binlog 真正的写到磁盘上,保证数据的持久化。但是这是一个从内存写到磁盘的过程,I/O 比较慢。如果每次事务提交都执行一遍 fsync() 将 binlog 持久化落盘到磁盘的话,效率很低。于是就想,能不能等几个事务的 binlog 一起调用一次 fsync(),一次性落盘。减少 fsync() 的次数,从而提高效率。这就是二进制日志组提交的概念。

2.两阶段提交

2.1 为什么需要二阶段提交

  我们知道在 MySQL 中不仅仅有 binlog,还有 redo log 和 undo log 。binlog 用来记录每个事务的操作信息,redo 是在数据库宕机恢复时使用,用来恢复数据库数据,undo 用来回滚还未被提交的数据。binlog 是在数据库 Server 层产生的,即它会记录所有存储引擎中事务的操作,而 redo 是 InnoDB 存储引擎特有的日志。

  在事务提交的时候,我们需要先写入二进制日志,再写 InnoDB 存储引擎的 redo。并且要求二进制日志和 redo 要么都写,要么都不写。不然可能会出现这样的情况:在主从复制的环境下,master 提交了一个事务,先写了二进制日志,但是在要写 InnoDB 存储引擎的时候,数据库发生了宕机,此时 binlog 又已经被 slave 接收到了,slave 会执行这个事务,但是实际 master 上并没有这个事务。这就会导致主从数据的不一致。所以我们引入了二阶段提交来解决这个问题,即将写 binlog 操作个 InnoDB 提交操作通过事务变成原子的。

2.2 什么是二阶段提交

  所谓的二阶段提交就是,我在事务提交的时候,确保先将 binlog 写入,然后再到数据引擎层提交,并且这两个操作是原子的。在 MySQL 中用内部的 XA 事务来完成,即将这两个操作包装成一个事务的概念。

  上图表示了二阶段提交的过程。当一个会话中的某一事务 COMMIT 的时候,进去二阶段提交的过程。首先数据库先去协调 Server 层和 Engine,询问是否都可以开始写日志,这个过程就是图中的的 prepare 阶段。协调好两层之间的关系,Server 层和 Engine 层都表示可以写日志,这时候进入下一个过程。

  第二个过程就是写 binlog 的过程,先把 binlog 写到内存中,然后调用 fsync() 将 binlog 从内存写到磁盘上。

  第三个过程就是在存储引擎层提交的过程,将真实修改的数据提交到数据库中。当这一步完成才最终返回给会话一个 COMMIT 成功的信号。

  这整个过程就是二阶段提交的过程,如果在 fsync() 之前数据库 crash 了,重启之后数据将会被回滚,若在 fsync() 之后 crash,则会进行重做操作。通过二阶段提交的方式就保证了存储引擎与二进制日志保持一致

  

3.三阶段提交

3.1 为什么需要三阶段提交

  上面的二阶段提交是针对单一事务提交时候的操作顺序,下面我们来看看当多个事务并发的时候会是什么样的一个情况。

  现在有T1、T2、T3 三个事务需要执行,从图中可以看到数据在 fsync() 之前,三个事务已经写入到了 binlog 中,通过 fsync() 操作将 binlog 刷到磁盘。之后先是 T2 COMMIT,将数据更改更新到存储引擎层,接着是 T3 COMMIT,将数据更新到存储引擎层。这时候我们做了一个热备份的操作,有多种方式进行数据库的热备份,比如:XtraBackup等。这时候就会发生错误。会发生什么错误,我们需要先了解一下 XtraBackup 等热备工具的备份原理。

  XtraBackup备份原理:直接拷贝数据库文件,并且记录下当前二进制日志中已经提交的最后一个事务标记。在新的数据库实例上完成 recovery 操作。

  了解完备份原理之后,我们就可以想到上述情况下做热备会出现什么情况。因为 T2、T3 已经提交,所以备份的时候会记录下 T3 是最后一个提交的事务,会认为 T3 之前的事务都是已经提交的,由于是直接拷贝数据库文件,可以看到 T1 事务的数据还没有提交到存储引擎层,所以备份数据中还并没有 T1 的数据。如果新的数据库是用来做主从复制的话,change master to 会指向二进制日志中 T3 的位置,从 T3 事务开始往后进行复制,这样一来 T1 事务的数据就这样没了。产生这个问题的主要原因就是:事务写入二进制日志的顺序与事务在存储引擎层提交的顺序不一致

  为了解决这个问题,MySQL 引入了 prepare_commit_mutext 的机制,当事务提交的时候,需要先获得 prepare_commit_mutext 这个锁。有了这个锁就可以保证事务写入二进制日志的顺序与事务在存储引擎层提交的顺序一致。

  但是这样一来,从图中我们也可以看到,原先是并发的事务,又变成了串行的,效率又变低了。只要是问题,必然存在解决方法。于是三阶段提交就出现了。

3.2 什么是三阶段提交

  三阶段提交,顾名思义有三个阶段: Flush 阶段、sync 阶段、commit 阶段。分别对应的就是二进制日志写内存的阶段、二进制日志刷盘的阶段、事务提交到存储引擎层的阶段。

  每个阶段都有 leader、follower 两种角色。当一个事务进入三个阶段中的某一个阶段,如果发现这个阶段中的队列为空,那么这个事务就会成为 leader 的角色,之后进入同一阶段的事务,发现这个阶段的队列中已经有事务存在了,那就变成 follower 角色。leader 角色的任务是安排当前阶段队列中的事务按顺序执行,并且带领队列中所有的事务进入下一个阶段。当 leader 带领队列中的事务进入下一阶段的时候,如果发现下一阶段中已经有事务存在(即下一阶段已有 leader 存在),新来的 leader 自动变成 follower 角色。

  三阶段提交在每个阶段都控制了事务的顺序,从而也就控制了事务执行的整体顺序。解决了 prepare_commit_mutex 锁导致的问题,事务可以并发的执行。

参考资料

  1. MySQL并发复制系列一:binlog组提交
  2. MySQL并发复制系列二:多线程复制
  3. Binary Log Group Commit in MySQL 5.6
  4. 《MySQL技术内幕》

博客地址:https://win-man.github.io/
公众号:欢迎关注

1. 装饰器简介

  首先,@func_decorator 长成这个样子的东西,在 Python 中被称为装饰器(Decorator)。第一眼看到它,我还以为又遇到了 Java 中的注解(Annotation),有一种很熟悉的感觉。仔细了解下来,发现装饰器与注解有点类似又有点不同。

  装饰器是用来修饰方法、函数或者类的,其作用是在原先的基础上完成一些额外的功能,利用装饰器来达到在不影响原先代码的情况下增加功能的需求。相比较而言,注解在 Java 中是可以用来修饰方法、函数、类和变量的,且其作用仅仅类似于注释的作用,当注解和 Java 中的反射机制结合在一起才完成了更多的功能。

  下面还是如何使用装饰器吧。

2. 装饰器的分类

  装饰器可以用于修饰函数和类,并且装饰器还分带参数和不带参数两种,所以我们分别来讲这几种不同的装饰器该如何编写,使用。

2.1 修饰函数的装饰器

2.1.1 不带参数的装饰器

  假定我们之前有这么一个函数:

1
2
def func_test():
print 'This is func_test() running....'

  现在,有这么一个需求过来:要求在这个函数运行开始前和运行结束后记录一下。之前的我肯定二话不说,操起键盘,上来就改成:

1
2
3
4
def func_test():
print '%s() start.' %(func_test.__name__)
print 'This is func_test() running....'
print '%s() finish.' %(func_test.__name__)

  但是如果我用装饰器我可以怎么写呢。

  首先,使用装饰器之前需要先定义装饰器,Python 定义装饰器和定义函数式一样的:

1
2
3
4
5
6
def func_decorator(func):
def wrapper(*args,**kw):
print '%s() start.' %(func.__name__)
func(*args,**kw)
print '%s() finish.' %(func.__name__)
return wrapper

  然后怎么使用这和装饰器呢,很简单:

1
2
3
4
5
6
@func_decorator
def func_test():
print 'This is func_test() running....'

if __name__ == '__main__':
func_test()

  就是在原先的基础上增加了一行代码,做到的效果和我原先更改两行代码的效果是一样的。

  在回过头来看一下 func_decorator 的代码,其实不难理解,因为 Python 一切都是对象,所以装饰器,把函数 func_test 当成一个参数传入一个新的函数中,在新的函数 wrapper 中增加我们所需要的功能,最后将这个经过装饰的函数返回,使其替代原先的函数。

2.1.2 带参数的装饰器

  有了不带参数的装饰器的基础,带参数的装饰器就比较容易理解了。我们看到装饰器的定义和函数的定义没有什么区别,我们可以向函数传递参数,那么我们也可以向装饰器传递参数。假定我们现在还是原来那个函数:

1
2
def func_test():
print 'This is func_test() running....'

  新的需求是:输出函数使用对象。

  我们可以定义这样一个装饰器:

1
2
3
4
5
6
7
def func_decorator_with_args(user):
def decorator(func):
def wrapper(*args,**kw):
print 'The user of func is %s' %(user)
return func(*args,**kw)
return wrapper
return decorator

  然后使用这个装饰器:

1
2
3
4
5
6
@func_decorator_with_args('sg')
def func_test():
print 'This is func_test() running....'

if __name__ == '__main__':
func_test()

  结果如下:

  给定义带参数的装饰器的时候也可以向函数一样指定默认值。

2.2 修饰类的装饰器

  看过用来修饰函数的装饰器,我们再来看看修饰类的装饰器。修饰函数的装饰器是将原函数包装成一个新的函数并且返回,修饰类的装饰器也是一样的道理,是将原先的类包装成一个新的类并且返回。

  假设我们有这么一个类:

1
2
3
4
5
6
class People(object):
def __init__(self,age,name):
self.age = age
self.name = name
def show(self):
print 'Age:%d Name:%s' %(self.age,self.name)

  需求是在每次调用 show() 函数的时候,记录这个函数一共被调用了几次。

  我们定义了这么一个装饰器,并使用这个装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def class_decorator(a):
class NewClass(object):
def __init__(self,age,name):
self.show_count = 0
self.wrapped = a(age,name)
def show(self):
self.show_count += 1
print 'Call show() %d ' %(self.show_count)
self.wrapped.show()
return NewClass

@class_decorator
class People(object):
def __init__(self,age,name):
self.age = age
self.name = name
def show(self):
print 'Age:%d Name:%s' %(self.age,self.name)

if __name__ == '__main__':
p = People(22,'sg')
p.show()
p.show()

  可以得到结果:

  可以看到 class_decorator 这装饰器是创建了一个 NewClass 来替代被装饰的类,比较重要的点就是,调用被装饰类的 init() 方法来获得被装饰类的一个对象,并用这个对象完成对原先方法的调用。

3. 装饰器的用途

  装饰器的作用和设计模式中的装饰模式吻合,在尽量保持原代码不动的情况下增加程序的功能。但是从我目前的感觉来说,Pyhton 的装饰器用于修饰函数还是可以的,但是用于修饰类的装饰器,我觉得并不方便,因为装饰器的代码完全需要依赖于原先的类的定义,我定义一个装饰器,我还不如重新定义一个类来继承原类。

  总结来说,我知道装饰器该怎么用了,但是用在哪,怎么用好装饰器,我任然需要学习。

博客地址:https://win-man.github.io/
公众号:欢迎关注

1.前言

  

2.环境

操作系统: CentOS6.5
MySQL:mysql-5.6.34

3.二进制包方式安装 MySQL

3.1 卸载 Linux 自带的 MySQL

Linux 系统自带了 MySQL 数据库,但是对于我们来说版本低了点,所以我们还是自己重新安装一遍 MySQL,为防止冲突,我们需要将原先版本的 MySQL 卸载。

  1. 查看 MySQL 及相关的 RPM 包
1
rpm -qa | grep -i mysql

  1. 卸载 MySQL
1
yum -y remove mysql-5.1.71-1.el6
  1. 查看是否卸载

3.2 安装 MySQL

  1. 下载 MySQL

通过 wget 命令下载 MySQL 的 tar.gz 文件

  1. 解压 tar.gz 文件
1
2
cd /usr/local
tar zxvf {下载文件所在路径}.tar.gz
  1. 重名文件夹
1
mv mysql-5.6.34-linux-glibc2.5-i686/ mysql
  1. 更改 mysql 文件夹属性
1
2
3
4
5
6
7
8
9
10
11
cd mysql/
groupadd mysql
useradd -g mysql mysql
chown -R mysql .
chgrp -R mysql .
```

5. 编译安装 MySQL

```c
scripts/mysql_install_db --user=mysql --basedir=/usr/local/mysql/ --datadir=/usr/local/mysql/data/
  1. 配置 my.cnf 文件

从 mysql 安装目录下拷贝一份 my.cnf 文件到 /etc/ 目录下,如果原先 /etc/ 存在 my.cnf 文件,则覆盖。

[mysqld]中添加:

1
2
3
4
basedir = /usr/local/mysql
datadir = /usr/local/mysql/data
port = 3306
server_id = 1
  1. 启动 MySQL 服务
1
2
3
cp support-files/mysql.server /etc/init.d/mysqld
ln -s /usr/local/mysql/bin/mysql /usr/bin
service mysqld start
  1. 验证 MySQL 服务是否开启
1
ps -ef | grep mysqld

4.配置主从环境

4.1 准备两台 Linux 环境服务器

分别在两台服务器上按上述步骤安装 MySQL。

4.2 配置主库

  1. 修改主库所在服务器上的 my.cnf 文件
1
vi /etc/my.cnf

在 my.cnf 文件中添加

1
log_bin = master_mysql_bin

该参数配置表示开启 MySQL 的二进制日志。

  1. 重启 MySQL 服务
1
service mysqld restart
  1. 查看 master 节点状态
1
mysql> show master status;

记录下红色标记处的信息,配置 slave 节点时需要。

  1. 主库中增加复制账号
1
mysql> grant replication slave on *.* to 'repl'@'192.168.222.%' identified by 'replsafe';

4.3 配置从库

  1. 修改从库所在服务器上的 my.cnf 文件
1
vi /etc/my.cnf

在 my.cnf 文件中添加

1
2
log_bin = slave_mysql_bin
relay_log = mysql_relay_bin

修改从库上的二进制日志和中继日志的命名规则

  1. 重启 MySQL 服务
1
service mysqld restart
  1. 连接到主库
1
2
3
4
5
6
7
8
mysql> change master to
-> master_host='192.168.222.131',
-> master_port=3306,
-> master_user='repl',
-> master_password='replsafe',
-> master_log_file='master_mysql_bin.000001',
-> master_log_pos=120;
Query OK, 0 rows affected, 2 warnings (0.11 sec)
  1. 开启复制
1
start slave;
  1. 查看从库复制状态
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
mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.222.131
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: master_mysql_bin.000001
Read_Master_Log_Pos: 330
Relay_Log_File: mysql_relay_bin.000002
Relay_Log_Pos: 500
Relay_Master_Log_File: master_mysql_bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 330
Relay_Log_Space: 673
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 131
Master_UUID: 249f7c7a-9bc8-11e6-8aaf-000c29b971bb
Master_Info_File: /usr/local/mysql/data/master.info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set:
Executed_Gtid_Set:
Auto_Position: 0
1 row in set (0.00 sec)

通过 show slave status\G 显示出来的内容如上,则表示主备环境搭建成功。

  1. 验证复制功能

在主库上创建一个数据库,创建一张表,并想表中插入数据,查看从库能否有相同数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> create database master_slave;
Query OK, 1 row affected (0.00 sec)

mysql> use master_slave;
Database changed
mysql> create table tb1(id int);
Query OK, 0 rows affected (0.14 sec)

mysql> insert into tb1 values(1);
Query OK, 1 row affected (0.03 sec)

mysql> select * from tb1;
+------+
| id |
+------+
| 1 |
+------+
1 row in set (0.00 sec)

查看从库上是否有相同数据

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
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| master_slave |
| mysql |
| performance_schema |
| test |
+--------------------+
5 rows in set (0.00 sec)

mysql> use master_slave;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> show tables;
+------------------------+
| Tables_in_master_slave |
+------------------------+
| tb1 |
+------------------------+
1 row in set (0.00 sec)

mysql> select * from tb1;
+------+
| id |
+------+
| 1 |
+------+
1 row in set (0.00 sec)

验证复制功能成功。

5.可能遇到的问题

问题一:
在启动复制功能时,查看 slave 节点状态发现:

原因:主库服务器上防火墙开启导致,从库连接不上主库

解决方案:在主库服务器上关闭防火墙 service iptables stop

博客地址:https://win-man.github.io/
公众号:欢迎关注

  学习 Java 的过程中,我们已经接触到了各种各样的流:输入流、输出流、文件输入流、文件输出流等等,但是在 Java8 中又提出了一个流的概念,这篇文章就来讲讲 Java8 中的流,看看它与以前了解的流究竟有什么关系。

1. 什么是流

  在 《Java8实战》 这本书中,流的定义是

从支持数据处理操作的源生成的元素序列

  按照我的理解,流就是一种在 Java8 中新提出的数据集合。这个新的数据集合提供一系列的 API ,可以方便我们对集合进行数据的筛选、转换等操作。

2. 流之初体验

阅读应该是人生活中就像阳光、空气和水一样自然而然的存在。

  我就拿书作为例子来讲讲 Java8 中的流。首先,我们先来看看流到底是怎么样的,以及流的作用。定义一个 Book 的实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Book {
//书名
private String name;
//作者
private String author;
//价格
private double price;
//出版社
private String publish;
//出版年份
private int publishYear;

public Book(String name,String author,double price,String publish,int publishYear){
this.name = name;
this.author = author;
this.price = price;
this.publish = publish;
this.publishYear = publishYear;
}
/*省略set和get方法*/
}

  假使我们现在有很多本书,给我们的要求是找出这些书中所有在 2000 年以前出版的书的名字。这个任务看起来一点都不难啊,老司机一下子就能写好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<Book> books = Arrays.asList(
new Book("书名一", "作者甲", 19.8, "一号出版社", 1995),
new Book("书名二", "作者乙", 99.8, "一号出版社", 2016),
new Book("书名三", "作者乙", 9.9, "一号出版社", 1994),
new Book("书名四", "作者甲", 21.3, "一号出版社", 1998),
new Book("书名五", "作者乙", 30.2, "一号出版社", 1999),
new Book("书名六", "作者丙", 15.7, "二号出版社", 2000),
new Book("书名七", "作者甲", 49.0, "二号出版社", 2007),
new Book("书名八", "作者丁", 72.0, "二号出版社", 2012),
new Book("书名九", "作者丙", 98.0, "二号出版社", 2015),
new Book("书名十", "作者丁", 100.0, "二号出版社", 2014)
);
List<String> bookNames = new ArrayList<>();
for (Book book : books) {
if (book.getPublishYear() < 2000) {
bookNames.add(book.getName());
}
}

  但是使用流的写法可以更简便,一句话就能搞定:

1
2
3
4
List<String> bookNamesNew = books.stream()
.filter(b -> b.getPublishYear() < 2000)
.map(Book::getName)
.collect(toList());

  这里用到了 Lambda 写法,不会的可以参考之前的文章。这个任务比较简单,所以并不能很好说明流的好处,但是这只是为了简单的介绍一下流可以做什么,现在大家应该已经有一个初步的印象了。等了解完流其他的功能之后,更加能体会到流的好用之处。

3. 流之再体验

  使用流来处理问题的时候,一般有三个步骤:

  1. 获取流,像上面的 books.stream() 返回的是一个 Stream<T> 的流对象
  2. 执行中间操作对数据进行处理,上面的 fliter()map() 都是对流中的数据进行处理操作
  3. 执行终端操作获得预期的返回结果,collect(toList()) 根据我们的预期返回 List 类型的数据

  Java8 中的流有一个很重要的特性就是并行处理,对于数据的处理不是一个步骤一个步骤地处理,而是像流水线一样处理,这个并行处理并不需要我们处理,Java 内部就会帮我们实现。只需要更改一下获取流对象的方法就行,将stream() 方法改为 parallelStream() 方法即可。上面代码中的 filter()map()collect() 这几个操作其实是并行处理的。这就省去了我们代码优化啊、实现多线程啊一系列工作。

3.1 中间操作

  中间操作是对流对象进行处理的操作,中间操作会返回一个流对象,这样多个中间操作可以连起来,形成流水线。下面介绍一下主要的一些中间操作:

3.1.1 筛选操作

源码定义

1
Stream<T> filter(Predicate<? super T> predicate);

通过传入一个 Predicate 类型的 Lambda 表达式,对流对象中的数据进行筛选。

写法示例

1
2
3
List<Book> books1 = books.stream()
.filter(b -> b.getPublishYear() < 2000)
.collect(toList());

3.1.2 去重操作

源码定义

1
Stream<T> distinct();

对流对象中的数据进行去重,其判断对象是否一样调用的是 equals() 方法。

写法示例:

1
2
3
4
List<Book> books2 = books.stream()
.filter(b -> b.getPublishYear() < 2000)
.distinct()
.collect(toList());

3.1.3 限制返回个数

源码定义

1
Stream<T> limit(long maxSize);

对流操作返回的结果中的个数进行限制,按顺序返回前 maxSize 个元素,若元素个数不足 maxSize 则返回所有元素。

写法示例:

1
2
3
4
List<Book> books3 = books.stream()
.filter(b -> b.getPublishYear() < 2000)
.limit(3)
.collect(toList());

3.1.4 跳过操作

源码定义:

1
Stream<T> skip(long n);

将流对象中前 n 个元素去除,并返回一个流对象,如果元素个数不足 n ,则返回一个数据为空的一个流对象。

写法示例:

1
2
3
4
List<Book> books4 = books.stream()
.filter(b -> b.getPublishYear() < 2000)
.skip(3)
.collect(toList());

3.1.5 映射操作

源码定义:

1
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

通过传入一个 Function 类型的函数式接口,完成流对象中元素的类型转换功能。

写法示例:

1
2
3
List<String> books5 = books.stream()
.map(Book::getName)
.collect(toList());

3.1.6 排序操作

源码定义

1
2
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

有默认排序和自定义排序,自定义排序的话,传入一个 Comparator 类型的函数式接口,按照自己预期的情况进行排序,返回排序之后的流对象。

写法示例:

1
2
3
List<Book> books6 = books.stream()
.sorted()
.collect(toList());

3.2 终端操作

  终端操作将流对象转换成我们所期望的结果,终端操作是流水线上的最后一个操作,一般终端操作都不会像中间操作一样返回一个流对象。因为流对象像迭代器一样只能遍历一次,所以当流对象遇到终端操作之后,整个流水线就结束了。

3.2.1 返回集合

源码定义

1
2
3
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);

通过传入参数来确定返回的结果类型,像之前一直用的 collect(toList()) 就是说将对象流转换成 List 类型的集合,toList() 是在 java.util.stream.Collectors 中定义好的, Java8 已经为我们准备好了一些常用的转换规则,这可以在 java.util.stream.Collectors 中找到。

写法示例

1
2
3
List<Book> books1 = books.stream()
.filter(b -> b.getPublishYear() < 2000)
.collect(toList());

3.2.2 查找是否至少存在一个

源码定义:

1
boolean anyMatch(Predicate<? super T> predicate);

判断流对象中是否存在满足 Predicate 类型的元素,如果存在则返回 true,否则返回 false

写法示例:

1
2
boolean books7 = books.stream()
.anyMatch(b -> b.getPublishYear() < 2000);

3.2.3 查找第一个元素

源码定义

1
2
3
4
5
6
7
8
9
10
11
Optional<T> findFirst();
```

返回流对象中第一个元素。

**写法示例**:

``` java
Book books8 = books.stream()
.filter(b -> b.getPublishYear() < 2000)
.findFirst().get();

3.2.4 返回元素个数

源码定义

1
long count();

返回流对象中元素的个数。

写法示例

1
2
3
long books9 = books.stream()
.filter(b -> b.getPublishYear() < 2000)
.count();

4. 小结

  其实这篇文章就只是简单的介绍了一下 Java8 中流的内容,本来想把 Java8 中定义的关于流的 API都讲一下的,但是发现这个量有点大,所以就挑了一些比较有代表性的中间操作终端操作讲一下,更多的内容还是得靠自己发掘了。

博客地址:https://win-man.github.io/
公众号:欢迎关注

  Java8 中引入的 Lambda 表达式已经能在某些方面很大程度上简化我们的代码了,但是如果问有没有更加优雅的码代码方式呢,答案是肯定的。Java8 中出现了一个新的功能:方法引用

方法引用:仅仅调用特定方法的 Lambda 的一种快捷写法。

  方法引用,让我们以更加简洁,语义化的方式去码写我们的代码。

如何将 Lambda 表达式转换为方法引用

  比方说我现在有一个 List<String> 类型的列表,里面存了一些内容:

1
2
3
4
5
6
List<String> numStrings = new ArrayList<>();
numStrings.add("123");
numStrings.add("234");
numStrings.add("345");
numStrings.add("456");
numStrings.add("567");

  下面我们从对这个 List 进行一些操作来说明一下方法引用的三种类型。

一、指向静态方法的方法引用

  比如说我想将这个 List 中所有的字符转转换成 int 类型的,用 Lambda 应该如何完成呢。我们先确定我们需要参数化的行为就是:对字符串进行某种特定的操作,然后我们直接使用 Java 内置的 Function 函数式接口来完成任务。让我们来调用一下函数式接口:

1
2
3
4
5
6
7
public static List<Integer> strToInt(List<String> strs, Function<String,Integer> f){
List<Integer> result = new ArrayList<>();
for(String s:strs){
result.add( f.apply(s));
}
return result;
}

  接着将 Lambda 表达式传进去就好了:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args){
List<String> numStrings = new ArrayList<>();
numStrings.add("123");
numStrings.add("234");
numStrings.add("345");
numStrings.add("456");
numStrings.add("567");
List<Integer> ints = strToInt(numStrings,(String s) -> Integer.parseInt(s));
System.out.println(ints);
}

  这就是 Lambda 的写法,如何转换成方法引用呢。看到我们其实是调用了 Integer.parseInt() 这个静态方法。对于这种指向静态方法的 Lambda 表达式转换规则如下:

Lambda: (args) -> ClassName.staticMethod(args)

方法引用: ClassName::staticMethod

  所以 (String s) -> Integer.parseInt(s) 可以改写为 Integer::parseInt,方法引用会自动完成传参等工作。

二、指向任意类型实例方法的方法引用

 改写规则:

Lambda: (arg0,rest) -> arg0.instanceMethod(rest)

方法引用:ClassName::instanceMethod /ClassName是arg0的类型/

如果对上面的 List 进行处理,得到一个存放原先 List 中每个字符串长度的 List 的话。Lambda 写法如:List<Integer> ints = strToInt(numStrings,(String s) -> s.length());;参照改写规则,我们可以改写为: List<Integer> ints = strToInt(numStrings,String::length);

三、指向现有对象的实例方法的方法引用

   改写规则:

Lambda:(args) -> expr.instanceMethod(args)

方法引用:expr::instanceMethod

  如果对上面的 List 进行处理,得到一个存放原先 List 中每个字符串反转之后的 List 的话。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
31
32
public class FunctionReference {
public static List<Integer> strToInt(List<String> strs, Function<String,Integer> f){
List<Integer> result = new ArrayList<>();
for(String s:strs){
result.add( f.apply(s));
}
return result;
}
public static void main(String[] args){
List<String> numStrings = new ArrayList<>();
numStrings.add("123");
numStrings.add("234");
numStrings.add("345");
numStrings.add("456");
numStrings.add("567");
StrClass sc = new StrClass();
List<Integer> ints = strToInt(numStrings,(String s) -> sc.reverseInt(s));
System.out.println(ints);
}
}
class StrClass{
public int reverseInt(String s){
int n = Integer.parseInt(s);
int result = 0;
while(n > 0){
result *= 10;
result += (n % 10);
n /= 10;
}
return result;
}
}

  根据改写规则,可以将 (String s) -> sc.reverseInt(s) 改写为 sc::reverseInt

  总结来说的话,就是看自己的 Lambda 表达式符合哪种改写规则的话,那就可以将 Lambda 表达式改写为方法引用的方式,但是不是每一个 Lambda 表达式都可以转变成方法引用的写法的。个人觉得将 Lambda 转换成方法引用的写法并不是很符合我的习惯(可能是看到::就想起了C++中的命名空间吧)。

构造函数的方法引用

  对于一个类的构造函数,我们也可以用类名加上关键字 new 来创建这个构造函数的方法引用。我们先定义一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ParamterClass{
public int a;
public int b;
public int c;
public ParamterClass(){
this.a = 0;
this.b = 0;
this.c = 0;
}
public ParamterClass(int a){
this.a = a;
this.b = 0;
this.c = 0;
}
public ParamterClass(int a,int b){
this.a = a;
this.b = b;
this.c = 0;
}
}

  这个类有三个构造方法,分别是一个无参构造函数,一个参数的构造函数,两个参数的构造函数。然后我们看一下该如何根据需要调用不同的构造函数呢:

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
public class ConstructReference {

public static List<ParamterClass> noParamterCreator(Supplier<ParamterClass> s){
List<ParamterClass> result = new ArrayList<>();
for(int i = 0;i < 10;i++){
result.add(s.get());
}
return result;
}

public static List<ParamterClass> oneParamterCreator(Function<Integer,ParamterClass> f){
List<ParamterClass> result = new ArrayList<>();
for(int i = 0;i < 10;i++){
result.add(f.apply(i));
}
return result;
}

public static List<ParamterClass> twoParamterCreator(BiFunction<Integer,Integer,ParamterClass> b){
List<ParamterClass> result = new ArrayList<>();
for(int i = 0;i < 10;i++){
result.add(b.apply(i,i+1));
}
return result;
}

public static void main(String[] args){
List<ParamterClass> list1 = noParamterCreator(ParamterClass::new);
List<ParamterClass> list2 = oneParamterCreator(ParamterClass::new);
List<ParamterClass> list3 = twoParamterCreator(ParamterClass::new);
for(ParamterClass p :list1){
System.out.println(String.format("List1:a->%d b->%d c->%d",p.a,p.b,p.c));
}
for(ParamterClass p :list2){
System.out.println(String.format("List2:a->%d b->%d c->%d",p.a,p.b,p.c));
}
for(ParamterClass p :list3){
System.out.println(String.format("List3:a->%d b->%d c->%d",p.a,p.b,p.c));
}
}
}

  用 Supplier 函数式接口调用无参构造函数,Function 函数式接口调用一个参数的构造函数,BiFunction 函数式接口调用两个参数的构造函数。其实方法引用的写法都是一样的 ParamterClass::new,实际上调用不同的构造函数式通过不同的函数式接口来实现的。

总结

  方法引用的出现,是为了更加简洁的运用 Lambda 表达式,让我们的代码更加清晰明了。做一个优雅写代码的码农。

博客地址:https://win-man.github.io/
公众号:欢迎关注

  从1998年 Java 发布以来,Java 版本从1.1到目前的 Java8,Java一直在升级,在增加新功能。这也就很好的解释了为什么 Java 在编程语言排行榜上一直占据前排位置。不断的创新才是保持优势的正确方式。Java8 在2014年被发布,提供了更多的编程工具和概念,让开发人员以更加简洁、易维护的方式去解决问题。Lambda 表达式作为 Java8 中一个重要的特性,是不可不学的一部分。

1. 为什么需要 Lambda 表达式

1.1 怎么解决点外卖的问题

  都说人生有三大难题:早饭吃什么?午饭吃什么?晚饭吃什么?每当到了
饭点,在决定点哪家外卖的时候,都像是在作出人生抉择。我们就从点外卖这件事上来讲一讲为什么需要引入 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
public class Store {
private String name; //商店名称
private double avePrice; //平均价格
private int aveDeliveryTime; //平均送餐时间
private String type; //商店类型

public Store(){

}

public Store(String name,double avePrice,int aveDeliveryTime,String type){
this.name = name;
this.avePrice = avePrice;
this.aveDeliveryTime = aveDeliveryTime;
this.type = type;
}

@Override
public String toString() {
return String.format("店铺名称:%s 平均价格:%d 平均送餐时间:%d 店铺类型:%s",
this.name,this.avePrice,this.aveDeliveryTime,this.type);
}
/*省略get和set方法*/
}

  接着,我们开始定外卖,现在到月底了,又没钱了,点外卖也不挑什么了,就找最便宜的,省钱是硬道理。找最便宜的外卖店应该怎么找呢,很自然的就是对店铺的价格进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args){
List<Store> stores = new ArrayList<Store>();
stores.add(new Store("店铺一",20,10,"快餐"));
stores.add(new Store("店铺二",30,20,"烧烤"));
stores.add(new Store("店铺三",10,20,"披萨"));
stores.add(new Store("店铺四",15,30,"炸鸡"));
stores.add(new Store("店铺五",45,20,"甜品"));
Collections.sort(stores, new Comparator<Store>() {
public int compare(Store o1, Store o2) {
return Double.compare(o1.getAvePrice(),o2.getAvePrice());
}
});
}

  可以看到这样的确解决了问题,但是当我们有新的需求的时候,比如说我想找出送餐时间最短的店铺,或者是我只想找出卖快餐的店铺时,这个代码就变得不适用了,我们需要更改代码来实现新的功能。这个还是简单的情况,真实开发中情况更加复杂,显然每次遇到新的需要就大改代码这样很不优雅,该怎么解决这个问题。熟悉设计模式的朋友很快就会想到,这很符合策略模式的情况,我们每次只是更改不同的选择店铺策略去选择店铺。

1.2 用策略模式来解决点外卖的问题

  下面我们重新用策略模式来完成我们挑选外卖店铺的任务。我们将挑选店铺的策略抽象出来:

1
2
3
public interface Selector {
public List<Store> select(List<Store> stroes);
}

  然后实现这个选择:

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
//将店铺按照价格排序
class PriceSelector implements Selector{
public List<Store> select(List<Store> stroes) {
Collections.sort(stroes, new Comparator<Store>() {
public int compare(Store o1, Store o2) {
return Double.compare(o1.getAvePrice(),o2.getAvePrice());
}
});
return stroes;
}
}
//将店铺按照送餐时间排序
class TimeSelector implements Selector{
public List<Store> select(List<Store> stroes) {
Collections.sort(stroes, new Comparator<Store>() {
public int compare(Store o1, Store o2) {
return o1.getAveDeliveryTime() - o2.getAveDeliveryTime();
}
});
return stroes;
}
}
//选择所有的快餐店
class TypeSelector implements Selector{
public List<Store> select(List<Store> stroes) {
List<Store> result = new ArrayList<Store>();
for(Store store:stroes){
if("快餐".equals(store.getType())){
result.add(store);
}
}
return result;
}
}

  之后我们就可以同传递不同的选择方式来选择外卖店:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static List<Store> selectStore(List<Store> stores,Selector s){
return s.select(stores);
}

public static void main(String[] args){
List<Store> stores = new ArrayList<Store>();
stores.add(new Store("店铺一",20,10,"快餐"));
stores.add(new Store("店铺二",30,20,"烧烤"));
stores.add(new Store("店铺三",10,20,"披萨"));
stores.add(new Store("店铺四",15,30,"炸鸡"));
stores.add(new Store("店铺五",45,20,"甜品"));
Selector s = new TypeSelector();
stores = selectStore(stores,s);
for (Store store : stores){
System.out.println(store.toString());
}
}

  这样一来看上去貌似是解决了我们的问题,当有新的选择方式的时候,我们只需要实现一个新的选择类,用户端基本上不用改变任何代码。但是仔细一考虑,当选择方式越来越多的时候,每次都需要重新实现新的类,而且仔细观察发现每个选择类中真正不同的只有 select() 方法中的代码而已。

1.3 Lambda 在点外卖为题中的应用

  有什么办法可以让我们不用每次都写那么繁琐的代码呢,这个时候就需要 Lambda 表达式登场了。等了这么久,终于写到文章的主题了(我的文章就是这么又臭又长)。为什么 Lambda 表达式可以简化我们的代码呢,因为 Lambda 提出的是一个行为参数化的概念,首先行为就是我们上面讲到的 select() 方法中的代码,这是选择的具体行为,参数化就是向方法中传递参数一样。行为参数化就是定义了一种新的编程方式,让我们可以将代码像参数一样传递到方法中使用。

  看一下我们使用 Lambda 表达式完成店铺选择的功能是怎么样的:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args){
List<Store> stores = new ArrayList<Store>();
stores.add(new Store("店铺一",20,10,"快餐"));
stores.add(new Store("店铺二",30,20,"烧烤"));
stores.add(new Store("店铺三",10,20,"披萨"));
stores.add(new Store("店铺四",15,30,"炸鸡"));
stores.add(new Store("店铺五",45,20,"甜品"));
stores.sort((Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice()));
for (Store store : stores){
System.out.println(store.toString());
}
}

  stores.sort((Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice())); 就这么一行代码代替了我们之前那么多的代码,可以看到 Lambda 表达式可以使得我们的程序变得多么简洁吧。 (Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice()) 这行跟表达式一样的东西就这么传递给了 sort() 方法。这就是所谓的行为参数化。

2. 什么是 Lambda 表达式

Lambda 表达式可以理解为简洁地表示可传递的匿名函数的一种方式:他没有名称,但他有参数列表、函数主体、返回类型,还有一个可以抛出的异常列表。

  Lambda 表达式由三个部分组成:参数列表、箭头、Lambda 主体。具体的形式表现为 (parameters) -> expression(parameters) -> {statements;}

  可以看到我们之前的 (Store o1,Store o2) -> Double.compare(o1.getAvePrice(),o2.getAvePrice())(parameters) -> expression 的形式,(Store o1,Store o2) 是参数列表 ,Double.compare(o1.getAvePrice(),o2.getAvePrice()) 是表达式。当 Lambda 主体中有多条语句的时候,就需要符合 (parameters) -> {statements;} 的形式了(注意{})。

3. 怎么使用 Lambda 表达式

  在介绍 Lambda 表达式的使用之前,先提出一个概念:函数式接口。函数式接口定义:只定义一个抽象方法的接口。为什么需要这个概念呢,在 Java 中,”123” 是 String 类型; 12.3 是 Double 类型;true 是 boolean 类型…那 Lambda 这个实现行为参数化的新规范是什么类型呢,为了更符合我们的思维方式,Java8 给 Lambda 归为函数式接口类型。这样,我们能以我们更加理解的方式去学习 Lambda。

3.1 明确行为参数化

  这其实就是一个去同存异的步骤,从上面的代码中我们可以发现,真正不同的地方就是选择店铺的策略上,所以我们需要做的就是将 select() 的行为参数化。

3.2 定义函数式接口来传递行为

  知道我们需要参数化的行为之后,我们需要定义一个函数式接口,以便于我们之后传递 Lambda 表达式。

1
2
3
4
@FunctionalInterface
public interface StoreSelect {
boolean select(Store store);
}

  我们将对店铺的判断逻辑抽取出来,将其进行行为参数化。@FunctionalInterface 这个注解并不是必须的,但是为了将函数式接口与其他普通的接口区分开来,最好加上这个注解。

3.3 执行行为

  定义完函数式接口之后,我们需要定义一个方法来接受 Lambda 表达式的参数,不然我们应该从哪边传入 Lambda 参数呢。

1
2
3
4
5
6
7
8
9
public static List<Store> selectStore(List<Store> stores,StoreSelect s){
List<Store> result = new ArrayList<>();
for(Store store:stores){
if(s.select(store)){
result.add(store);
}
}
return result;
}

  可以看到我们调用了 StoreSelect 接口中的 select() 方法,但是我们并没有哪边实现过这个接口,这就需要我们在下一步中传入 Lambda 表达式来实现这个接口。

3.4 使用 Lambda

  传入 Lambda 表达式,有种类似于用匿名方式实现接口:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args){
List<Store> stores = new ArrayList<Store>();
stores.add(new Store("店铺一",20,10,"快餐"));
stores.add(new Store("店铺二",30,20,"烧烤"));
stores.add(new Store("店铺三",10,20,"披萨"));
stores.add(new Store("店铺四",15,30,"炸鸡"));
stores.add(new Store("店铺五",45,20,"甜品"));
stores = selectStore(stores,(Store store) -> store.getAvePrice() <= 20.0);
for (Store store : stores){
System.out.println(store.toString());
}
}

  使用了一个 (Store store) -> store.getAvePrice() <= 20.0 Lambda 表达式,可以看到在传入 Lambda表达式的时候,并不能乱写,我们 Lambda 表达式中的参数列表必须与函数式接口的方法的参数列表一致,Lambda 主体中的返回结果必须与函数式接口中的方法的返回类型一致。Java 程序在编译的过程中会检查我们的代码是否满足这些要求,不然编译不会通过。
  Java8 提出了一个新的概念:方法引用,这是一种 Lambda 表达式的简略写法。具体可以参照这篇文章,我在这就不啰嗦了。

3.5 使用 Java8 为我们准备的函数式接口

  如果每次都使用 Lambda 表达式,都需要完成上面的这些步骤,会觉得很麻烦,人都是有惰性的。所以 Java 已经为我们准备好了许多内置的函数式接口,当我们需要函数式接口的时候,只需要寻找一下有没有我们需要的,如果找不到再去自己定义。

  Java内置函数式接口

4. 使用复合 Lambda 表达式

  上面的过程中,我们每次使用 Lambda 表达式的时候传递进的只是一个逻辑,当需要的逻辑更加复杂,简单的一句话已经不能描述清楚的时候,应该怎么办,多定义几个函数式接口?并不需要,我们可以对 Lambda 表达式进行复合使用,就像 if 语句中出传入的条件一样,可以传入多个。

  在介绍如何复合使用 Lambda 表达式之前,我们先来升华一下函数式接口这个定义,之前的定义是函数式接口中只有一个抽象方法。Java 中引入一个叫默认方法的机制,让我们可以在接口中实现方法。只需要在方法前加上 default 关键字就能为方法提供实现了。为什么要讲这个呢,因为默认方法为我们复合使用 Lambda 表达式提供了可能。

  Java 中内置的函数式接口中的 ComparatorFunctionPredicate 都提供了允许进行表达式复合的方法,这些方法都是默认方法,已经为我们提供了实现。

Function<T,R> T -> R
Predicate T -> boolean

4.1 比较器复合

  我们可以通过使用静态方法 Comparator.comparing,根据提取用于比较的 Function 来返回一个 Comparator。Comparator.comparing((Store store) -> store.getAvePrice()) 这样就可以获得一个根据店铺平均价格进行排序的 Comparator。如果我们想逆序排序,Compator 函数式接口中定义了一个 reversed() 函数,这可以返回一个逆序排序的 Comparator,类似于:

1
Comparator.comparing((Store store) -> store.getAvePrice()).reversed();

  当两家店铺的价格一样的时候,我们希望根据送餐时间进行排序又该怎么办呢,Comparator 很贴心的又准备了一个 thenComparing() 方法,用法如下:

1
2
3
Comparator.comparing((Store store) -> store.getAvePrice())
.reversed()
.thenComparing((Store store) -> store.getAveDeliveryTime());

4.2 谓词复合

  谓词接口 Predicate 提供了三个方法:andornegate,分别代表与、或、非。

  首先,定义一个 Predicate 接口,选择出所有的快餐店:

1
Predicate<Store> typeStores = (Store store) -> "快餐".equals(store.getType());

  那要找出所有的非快餐店呢:

1
Predicate<Store> notTypeStores = typeStores.negate();

  找出非快餐店中,价格低于20的呢:

1
2
Predicate<Store> priceAndTypeStores = typeStores.negate()
.and((Store store) -> store.getAvePrice() < 20);

  找出非快餐店中,价格低于20或者送餐时间低于30的呢:

1
2
3
Predicate<Store> priceAndTypeStores1 = typeStores.negate()
.and((Store store) -> store.getAvePrice() < 20)
.or((Store store) -> store.getAveDeliveryTime() < 30);

4.3 函数复合

  函数复合和数学中的函数复合基本上概念是一致的,提供了 andThencompose 两个方法:

1
2
Function<String,String> f1 = (String s) -> s.trim();
Function<String,String> f2 = (String s) -> s.substring(4);

  f1.andThen(f2) 表示 f2(f1(s))f1.compose(f2) 表示 f1(f2(s))

5. 小结

  啰啰嗦嗦的讲 Lambda 的基本概念讲完了,最后只想表明一个观点:实践出真知。光看懂 Lambda 表达式的概念,不在实际中使用,这一切都是空谈,Lambda 的出现就是为了简化我们的代码,将它用到平时的代码中去才能说是真正掌握了知识。

博客地址:https://win-man.github.io/
公众号:欢迎关注

  在进行 Java 开发的过程中,我们一定会看到像 @Override@SuppressWarnings 这种既不像是代码又不像是注释的东西。老师在上课过程中也没提到过(如果没记错。但是这东西从第一次看到它,我就接受了它是合理存在的设定,从来没想过为什么要出现,有什么作用。后来才知道这叫注解,作用也不只是看上去的装饰作用。

注解介绍

注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。

  Java 内置了三种注解供我们使用:

  • @Override,表示当前的方法定义将覆盖超类中的方法。
  • @Deprecated,表示被标记的这个方法或类不再建议使用,如果程序中仍然使用了这个方法或者类,编译器将会发出警告信息。
  • @SuppressWarnings,表示忽略编译器发出的警告。

自定义一个注解

  除了使用 Java 自带的三种内置注解,我们也可以根据自己的实际需要来自定义注解,怎么定义一个注解呢。定义一个注解和定义一个接口,定义一个类是一样的。先上一个例子:

1
2
3
4
5
6
7
8
9
10
11
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Example {
public int id();
public String name() default "there is something";
}

  这样子就定义了一个最简单的注解,通过 @Example 来使用这个注解。接下来,我们一点一点的看定义一个注解需要哪些部分。

命名注解

  首先,public @interface Example 这是定义一个注解,注解的名称为 Example@interface 是定义注解的关键字,和定义一个类或者一个接口的时候使用的 class, interface 关键字是一样的。

注解元素

  接着看 public int id();public String name() 这是在定义注解中的元素,这和定义类的时候定义类中的属性是一个道理,用于定义这个注解中拥有哪些元素。定义注解的时候可以使用的元素类型包括:

  • 所有基本类型 (int,float,boolean 等)
  • String
  • Class
  • enum
  • Annotation

  所以注解中使用的元素类型基本上没有限定,都是可以使用的,最后一个 Annotation 说明注解也可以作为元素的类型,注解之间可以进行嵌套。

  读者一定注意到例子中的注解元素一个 id 后面没有跟 default 内容,一个注解元素 name 后面跟了 default 内容。default 用于指定这个元素的默认值,如果没有指定默认值,表示这个元素在使用的时候是一定需要被赋值的,否则是错误的。在设置默认值上这一点注解和类和接口有一点不同,类和接口中通过 = 赋值来设定默认值,对于没有默认值的,在使用过程中会自动给予一个默认值。

元注解

  接下来是看 @Target(ElementType.METHOD) , @Target 注解是 Java 提供的四种用于定义注解的元注解之一。@Target 表示该注解可以使用在什么地方,ElementType 是一个 enum 类型的数据,其中包括:

  • CONSTRUCTOR:说明这个注解使用于构造器声明
  • FIELD: 说明这个注解用于域声明(包括 enum )
  • LOCAL_VARIABLE: 说明这个注解用于局部变量声明
  • METHOD: 说明这个注解用于方法声明
  • PACKAGE: 说明这个注解用于包声明
  • PARAMETER: 说明这个注解用于参数声明
  • TYPE: 说明这个注解用于类、接口或 enum 声明

  再看 @Retention(RetentionPolicy.RUNTIME) 这是指定需要在什么级别保存该注解信息。RetentionPolicy 包括:

  • SOURCE 注解只在源文件中被保留,在编译的时候会被丢弃
  • CLASS 注解在 class 文件中可用,但会被 VM 丢弃
  • RUNTIME 注解在 VM 运行期间也存在

  另外还有两个元注解用于定义注解,@Documented 表示将此注解包含在 Javadoc 中,Inherited 表示允许子类继承父类中的注解。

编写一个注解解释器

  上面只是定义了一个注解,现在我们来看一下注解应该怎么使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ExampleTest {

@Example(id = 1)
public void test1(){
System.out.println("this is test1()");
}

@Example(id = 2, name = "test2")
public void test2(){
System.out.println("this is test2()");
}

@Example(id = 3, name = "example name")
public void test3(){
System.out.println("this is test3()");
}

public void test4(){
System.out.println("this is test4()");
}
}

  到这一步,我们好像还是看不出来注解究竟有什么用,好像就是跟注释一样啊。别急,要让注解真正发挥作用,我们需要写一个注解处理器,注解处理器才是真正让注解起作用的部分。

  编写注解处理器的时候需要用到一些关于反射机制方面的内容,不过是属于比较简单的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ExampleHandler {
public static void examplePrint(Class<?> cl){
for(Method m : cl.getDeclaredMethods()){
Example e = m.getAnnotation(Example.class);
if(e != null){
System.out.println("This example id: "+e.id()+" , name: "+e.name());
}
}
}

public static void main(String args[]){
ExampleHandler.examplePrint(ExampleTest.class);
}
}

  可以看到主要是用了反射中的 getDeclaredMethods()getAnnotation 方法, examplePrint() 方法的作用就是对类中的所有方法进行遍历,看是否有 @Example 这个注解,如果存在这个注解,那么输出这个注解的 idname 值。

  输出结果为:

  可以看到有 @Example 注解的方法都输出了相应的内容,没有对 name 进行赋值的注解也输出了默认的内容,没有 @Example 注解的部分并没有相应的输出。

  以上,就是关于 Java 注解入门的一些基础知识,深入的话还有好多内容可以挖。

博客地址:https://win-man.github.io/
公众号:欢迎关注

  在用jquery对页面元素进行操作的时候,经常会用到 append() 函数来对页面元素进行添加,有时候还需要对新添加的元素绑定新的事件,但是如果像普通绑定事件函数一样对新添加的元素进行事件绑定的话,会失败,事件监听不起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="button-test">
<button id="add-button" class="btn btn-primary">增加按钮</button>
</div>
<script>
TestData={}
TestUtil = {
init:function(){
$('#add-button').on('click',function () {
TestUtil.addButtonFunc();
})
$('.select').on('click',function () {
alert('clicked');
});
},
addButtonFunc:function () {
var innerHtml = '<button class="select btn btn-primary">点击按钮</button>';
$('#button-test').append(innerHtml);
}
}

$(function(){
TestUtil.init();
})
</script>

这个时候需要使用新的方式来进行事件绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="button-test">
<button id="add-button" class="btn btn-primary">增加按钮</button>
</div>
<script>
TestData={}
TestUtil = {
init:function(){
$('#add-button').on('click',function () {
TestUtil.addButtonFunc();
})
$('#button-test').on('click','.select',function () {
alert('success clicked');
})
},
addButtonFunc:function () {
var innerHtml = '<button class="select btn btn-primary">点击按钮</button>';
$('#button-test').append(innerHtml);
}
}

$(function(){
TestUtil.init();
})
</script>
1
2
3
$('#button-test').on('click','.select',function () {
alert('success clicked');
})

  其中,$(‘#button-test’)是一直存在的页面元素,click是要绑定的事件类型,’.select’是选择器,在’#button-test’的范围内进行选择,function是绑定的事件

博客地址:https://win-man.github.io/
公众号:欢迎关注

添加jar包

  添加freemarker的jar,还需要额外添加spring-content-support的jar包,不然会报错。

  然后再Spring的配置文件中添加对freemarker的配置

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 配置freeMarker的模板路径 -->  
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="WEB-INF/ftl/" />
<property name="defaultEncoding" value="UTF-8" />
</bean>
<!-- freemarker视图解析器 -->
<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<property name="suffix" value=".html" />
<property name="contentType" value="text/html;charset=UTF-8" />
<!-- 此变量值为pageContext.request, 页面使用方法:rc.contextPath -->
<property name="requestContextAttribute" value="rc" />
</bean>

  这样就配置好了对freemarker的支持。

  做一下测试,写一个User类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.my.springmvc.bean;

public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}

}

  一个FreeMarkerController类:

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
package com.my.springmvc.controller;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.my.springmvc.bean.User;

@Controller
@RequestMapping("/home")
public class FreeMarkerController {

@RequestMapping("/index")
public ModelAndView Add(HttpServletRequest request,HttpServletResponse response){
User user = new User();
user.setUsername("sg");
user.setPassword("1234");
List<User> users = new ArrayList<User>();
users.add(user);

ModelAndView mv = new ModelAndView();
mv.setViewName("index");
mv.addObject("users",users);
return mv;
}
}

然后再WEB-INF/ftl目录下创建一个index.html文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>another</title>
</head>
<body>
<#list users as user>
username : ${user.username}<br/>
password : ${user.password}
</#list>
</body>
</html>

  结果:
这里写图片描述

博客地址:https://win-man.github.io/
公众号:欢迎关注

  《怒火攻心》——2017.10.12

  《星际穿越》——2017.10.11

  《山2》——2017.10.9

  《达拉斯买家俱乐部》——2017.10.5

  《出租车司机》——2017.10.3

  《末代皇帝》——2017.9.24

  《闪光少女》——2017.9.24

  《战狼》——2017.9.10

  《敦刻尔克》——2017.9.3

  《二十二》——2017.8.14

  《战狼2》——2017.8.13

  《猜火车2》——2017.7.16

  《边境杀手》——2017.6.8

  《疾速特攻》——2017.6.4

  《提着心,吊着胆》——2017.6.2

  《爱乐之城》——2017.5.7

  《摔跤吧爸爸》——2017.5.6

  《斯诺登》——2017.4.16

  《降临》——2017.4.4

  《邻家大间谍》——2017.3.3

  《海边的曼彻斯特》——2017.3.2

  《生化危机:终章》——2017.2.26

  《罗曼蒂克消亡史》——2017.2.26

  《血战钢锯岭》-2017.2.15

  《地雷区》——2017.2.10

  《鸟人》——2017.2.5

  《乘风破浪》——2017.2.1

  《天空之眼》——2017.1.9

  《地球脉动第一季》——2016.12.18

  《长城》——2016.12.17

  《你的名字》——2016.12.04

  《青木瓜之味》——2016.11.30

  《大空头》——2016.11.12

  《六弄咖啡馆》——2016.11.5

  《追凶者也》——2016.11.5

  《Begin Again》——2016.11.3

  《剪刀手爱德华》——2016.10.22

  《湄公河行动》——2016.10.21

  《窃听风暴》——2016.10.20

  《完美陌生人》——2016.10.16

  《从你的全世界路过》——2016.10.15

  《树大招风》——2016.10.7

  《使徒行者》——2016.10.6

  《天堂电影院》——2016.10.6

  《爆裂鼓手》——2016.10.5

  《阳光姐妹淘》——2016.10.5

  《小姐》——2016.10.5

  《辩护人》——2016.10.4

  《一个叫欧维的男人决定去死》——2016.10.4

  《素媛》——2016.9.30

  《超脱》——2016.9.24

  《战争之王》——2016.9.18

  《V字仇杀队》——2016.9.16

  《釜山行》——2016.9.14

  《追梦赤子心》——2016.9.6

  《竞相灭绝》——2016.9.2

  《我被爸爸绑架了》——2016.9.1

  《年轻气盛》——2016.8.27

  《Me Before You》——2016.8.27

  《神秘代码》——2016.8.26

  《惊天魔盗团2》——2016.8.24

  《小萝莉的猴神大叔》—— 2016.8.20

  《裁缝》—— 2016.5.31

  《白日梦想家》—— 2016.5.18

  《飞鹰艾迪》

  《垫底辣妹》

  《海街日记》

  《海上钢琴师》

  《华尔街之狼》

  《荒野猎人》

  《极盗者》

  《控方证人》

  《老炮儿》

  《美国派1》

  《美国派2》

  《熔炉》

  《如晴天,似雨天》

  《神探夏洛克》

  《死亡诗社》

  《岁月神偷》

  《万物生长》

  《王牌特工》

  《我的青春期》

  《我的少女时代》

  《西西里的美丽传说》

  《我们结婚吧》

  《夏洛特烦恼》

  《小森林·冬春篇》

  《小森林·夏秋篇》

  《心迷宫》

  《野马》

  《烈日灼心》

  《东京审判》