spring-cloud基础

服务端发现的两种方式

  1. 客户端发现:Eureka
  2. 客户端发现:Nginx, Zookeeper, Kubernetes

redis基础

redis 简介

redis 是高性能键值对数据库,支持键值数据类型:

  • 字符串类型
  • 列表类型
  • 有序集合类
  • 散列类型
  • 集合类型

redis 的应用场景

  • 缓存
  • 任务队列
  • 应用排行榜
  • 网站访问统计
  • 数据过期处理
  • 分布式集群架构中的 session 分离

redis 五种数据类型

  • 字符串(String)
  • 哈希(hash)
  • 字符串列表(list)
  • 字符串集合(set)
  • 有序字符串集合(sorted set)

存储 String

  • 二进制安全的,存入和获取的数据相同。
  • Value 最多可以容纳的数据长度是 512M

redis 基本操作

String 类型

Hash 类型

存入键值对, 将 username:jack 存入 myhash

1
2
hset myhash username jack
hset myhash age 20

一次存入多个键值对

1
hmset myhash2 username rose age 21

根据键获取值

1
hget myhash username

返回

1
"jack"

根据多个键获取值

1
hmget myhash2 username age

返回

1
2
1) "rose"
2) "21"

获取所有键值对

1
hgetall myhash

删除某个键值对

1
hdel myhash2 username age

返回

1
2
3
4
1) "username"
2) "jack"
3) "age"
4) "20"

对某个数字进行增加:将 age 属性递增5。

1
hincrby myhash2 age 5

返回

1
(integer) 26

rose 的 age 属性由 21 增加到 26

判断某个set中是否含有某个键

1
hexists myhash username

获取 hash 中的所有键

1
hkeys myhash

运行结果为:

1
2
1) "username"
2) "age"

获取 hash 中的所有值

1
hvals myhash

运行结果为

1
2
1) "jack"
2) "20"

List 类型

list 中存储数据(从左侧添加,先进去的数据排在最后一位)

1
2
lpush mylist a b c
lpush mylist 1 2 3

从右侧添加 (从右侧添加)

1
2
rpush mylist2 a b c
rpush mylist2 a b c

查看列表 表示从第一个到第六个元素

1
lrange 0 5

运行结果如下

1
2
3
4
5
6
1) "3"
2) "2"
3) "1"
4) "c"
5) "b"
6) "a"
1
lrange mylist2 0 -1

表示从第一个到倒数第一个元素

运行结果如下

1
2
3
4
5
6
1) "a"
2) "b"
3) "c"
4) "1"
5) "2"
6) "3"

左边弹出

1
lpop mylist

运行结果

1
"3"

右边弹出

1
rpop mylist2

运行结果

1
"3"

lpushxrpushx 仅当需要操作的 list 中有值时才进行保存操作

现在 mylist3 的数据如下

1
2
3
4
5
6
7
8
9
10
11
 1) "5"
2) "3"
3) "2"
4) "3"
5) "7"
6) "6"
7) "3"
8) "5"
9) "4"
10) "2"
11) "1"

移除命令 lrem

从左侧开始删除两个3

1
lrem mylist3 2 3

运行后的 mylist3 结果

1
2
3
4
5
6
7
8
9
1) "5"
2) "2"
3) "7"
4) "6"
5) "3"
6) "5"
7) "4"
8) "2"
9) "1"

从后边往前删除两个1

1
lrem mylist3 -2 1

运行后的 mylist3 结果

1
2
3
4
5
6
7
8
1) "5"
2) "2"
3) "7"
4) "6"
5) "3"
6) "5"
7) "4"
8) "2"

删除所有2

1
lrem mylist3 0 2

运行后的 mylist3 结果

1
2
3
4
5
6
1) "5"
2) "7"
3) "6"
4) "3"
5) "5"
6) "4"

给某个指定的角标位置的元素设置值

1
lset mylist 3 haha

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
3) "c"
4) "b"
5) "a"
127.0.0.1:6379> lset mylist 3 haha
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "2"
2) "1"
3) "c"
4) "haha"
5) "a"

新建一个 list

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> lrange mylist4 0 -1
1) "c"
2) "b"
3) "a"
4) "c"
5) "b"
6) "a"
7) "c"
8) "b"
9) "a"

在第一个 b 之前插入 11

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> LINSERT mylist4 before b 11
(integer) 10
127.0.0.1:6379> lrange mylist4 0 -1
1) "c"
2) "11"
3) "b"
4) "a"
5) "c"
6) "b"
7) "a"
8) "c"
9) "b"
10) "a"

在第一个 b 之后插入 22

1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> LINSERT mylist4 after b 22
(integer) 11
127.0.0.1:6379> lrange mylist4 0 -1
1) "c"
2) "11"
3) "b"
4) "22"
5) "a"
6) "c"
7) "b"
8) "a"
9) "c"
10) "b"
11) "a"

创建两个 list

1
2
3
4
5
6
7
8
127.0.0.1:6379> lrange mylist5 0 -1
1) "c"
2) "b"
3) "a"
127.0.0.1:6379> lrange mylist6 0 -1
1) "3"
2) "2"
3) "1"

现在讲 mylist5 右左边弹出,从左侧压入 mylist6

rpoplpush 用于消息队列

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> RPOPLPUSH mylist5 mylist6
"a"
127.0.0.1:6379> lrange mylist5 0 -1
1) "c"
2) "b"
127.0.0.1:6379> lrange mylist6 0 -1
1) "a"
2) "3"
3) "2"
4) "1"

Set 类型

set 类型不允许出现相同的操作,set 通常用作

  1. 跟踪一些唯一性数据
  2. 用户维护数据对象之间的关联关系

创建一个 set 并添加一个元素

1
2
3
sadd myset a
sadd myset 1 2 3
sadd myset 1 2

查看所有元素发现没有重复元素

1
2
3
4
5
127.0.0.1:6379> SMEMBERS myset
1) "3"
2) "1"
3) "a"
4) "2"

查看一个元素是否存在于某个集合

1
2
3
4
127.0.0.1:6379> SISMEMBER myset a
(integer) 1
127.0.0.1:6379> SISMEMBER myset x
(integer) 0

说明 myset 中包含 a,但不包含 x

创建两个 set

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> SMEMBERS mya1
1) "b"
2) "c"
3) "a"
127.0.0.1:6379> SMEMBERS myb1
1) "2"
2) "1"
3) "c"
4) "a"

找出 mya1 中有,但是 myb1 中没有的元素

1
2
127.0.0.1:6379> sdiff mya1 myb1
1) "b"

找出 myb1 中有,但是 mya1 中没有的元素

1
2
3
127.0.0.1:6379> sdiff myb1 mya1
1) "1"
2) "2

sdiff 产生的结果直接保存

1
2
3
4
127.0.0.1:6379> SDIFFSTORE my1 mya1 myb1
(integer) 1
127.0.0.1:6379> SMEMBERS my1
1) "b"

找出 mya1 ,myb1 中都有的元素

1
2
3
127.0.0.1:6379> SINTER mya1 myb1
1) "c"
2) "a"

将上面的结果直接存储

1
2
3
4
5
127.0.0.1:6379> SINTERSTORE my2 mya1 myb1
(integer) 2
127.0.0.1:6379> SMEMBERS my2
1) "a"
2) "c"

列出 mya1myb1 的所有元素

1
2
3
4
5
6
127.0.0.1:6379> SUNION mya1 myb1
1) "b"
2) "c"
3) "2"
4) "a"
5) "1"

将上面的结果直接保存

1
2
3
4
5
6
7
8
127.0.0.1:6379> SUNIONSTORE my3 mya1 myb1
(integer) 5
127.0.0.1:6379> SMEMBERS my3
1) "b"
2) "c"
3) "2"
4) "a"
5) "1"

查看 set 中元素的个数

1
2
127.0.0.1:6379> SCARD mya1
(integer) 3

随机返回 set 中的一个元素

1
2
3
4
5
6
127.0.0.1:6379> SRANDMEMBER mya1
"b"
127.0.0.1:6379> SRANDMEMBER mya1
"a"
127.0.0.1:6379> SRANDMEMBER mya1
"c"

sorted set

sorted set 中每一个元素都有一个分数与之关联,redis 正是通过这些分数来为 set 排序,sorted set 元素是有序的.

可以用于:

  1. 大型游戏的积分排行榜
  2. 构建索引数据

添加元素

1
2
3
4
5
6
7
127.0.0.1:6379> ZADD mysort 70 zs 80 ls 90 ww
(integer) 3
// 如果 set 中已经包将要添加的元素,执行添加命令将覆盖原来的分数
127.0.0.1:6379> ZADD mysort 100 zs
(integer) 0
127.0.0.1:6379> ZADD mysort 60 tom
(integer) 1

展示元素的分数

1
2
127.0.0.1:6379> ZSCORE mysort zs
"100"

移除元素

1
2
127.0.0.1:6379> zrem mysort tom ww
(integer) 2

查看元素的个数

1
2
127.0.0.1:6379> ZCARD mysort
(integer) 2

再添加两个元素

1
2
127.0.0.1:6379> zadd mysort 85 jack 95 rose
(integer) 2

展示所有元素

1
2
3
4
5
127.0.0.1:6379> ZRANGE mysort 0 -1
1) "ls"
2) "jack"
3) "rose"
4) "zs"

展示所有元素和分数

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> ZRANGE mysort 0 -1 withscores
1) "ls"
2) "80"
3) "jack"
4) "85"
5) "rose"
6) "95"
7) "zs"
8) "100"

按分数从大到小排列

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> ZREVRANGE mysort 0 -1 withscores
1) "zs"
2) "100"
3) "rose"
4) "95"
5) "jack"
6) "85"
7) "ls"
8) "80"

删除排序从 0 到 3 的元素

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> ZRANGE mysort 0 -1
1) "bruce"
2) "perter"
3) "clark"
4) "arrow"
5) "wonder"
// 删除了排名 0 到 2 的元素
127.0.0.1:6379> ZREMRANGEBYRANK mysort 0 2
(integer) 3
127.0.0.1:6379> ZRANGE mysort 0 -1
1) "arrow"
2) "wonder"

新增几个元素

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> zrange mysort 0 -1 withscores
1) "arrow"
2) "40"
3) "wonder"
4) "50"
5) "bruce"
6) "60"
7) "clark"
8) "70"
9) "peter"
10) "80"

查找分数在 40 到 70 分区间的前两个元素

1
2
3
4
5
127.0.0.1:6379> ZRANGEBYSCORE mysort 40 70 withscores limit 0 2
1) "arrow"
2) "40"
3) "wonder"
4) "50"

增加分数

1
2
127.0.0.1:6379> ZINCRBY mysort 7 arrow
"47"

查找某个分数段内元素的个数

1
2
127.0.0.1:6379> ZCOUNT mysort 50 80
(integer) 4

redis的keys基本操作

查找所有的 keys

1
2
3
4
5
6
127.0.0.1:6379> keys *
1) "myhash2"
2) "hahaha"
3) "mylist2"
4) "myhashset2"
...

查看以 my 开头的所有的 key

1
2
3
4
127.0.0.1:6379> keys my?
1) "my3"
2) "my2"
3) "my1"

删除 keys

1
2
127.0.0.1:6379> del my1 my2 my3
(integer) 3

判断某个 key 是否存在

1
2
3
4
127.0.0.1:6379> EXISTS hahahs
(integer) 0
127.0.0.1:6379> EXISTS mysort
(integer) 1

重命名key

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> get compnay
"immooc"
127.0.0.1:6379> RENAME compnay
(error) ERR wrong number of arguments for 'rename' command
127.0.0.1:6379> RENAME compnay newcompany
OK
// 重命名之后就获取不到了
127.0.0.1:6379> get compnay
(nil)
// 使用新的名字获取
127.0.0.1:6379> get newcompany
"immooc"

设置超时时间 单位秒

1
2
3
4
5
127.0.0.1:6379> EXPIRE newcompany 1000
(integer) 1
// 查看剩余超时时间
127.0.0.1:6379> ttl newcompany
(integer) 989

查看 key 的类型

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> type newcompany
string
127.0.0.1:6379> type mylist
list
127.0.0.1:6379> type myhash
hash
127.0.0.1:6379> type mysort
zset
127.0.0.1:6379> type myset
set

redis 的事务

一个 redis 实例可以包含 15 个数据库,下标是从 0-15,默认选择第 0 号数据库

切换数据库命令如下

1
2
3
4
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
(empty list or set)

可以看到 1 号数据库中没有保存任何的key

将 0 号数据库中的一个 key 移动到 1 号数据库

1
2
3
4
5
6
127.0.0.1:6379> MOVE mysort 1
(integer) 1
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> keys *
1) "mysort"

multi 相当于开启事务

开启事务后,输入的命令将被保存的队列中

exec 相当于 commit

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> get num
"65"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr num
QUEUED
127.0.0.1:6379> incr num
QUEUED
127.0.0.1:6379> exec
1) (integer) 66
2) (integer) 67
127.0.0.1:6379>

执行之后命令队列中的命令将会被全部执行

discard 相当于 rollerback

redis 持久化概述

redis 之所以效率很高是因为数据都保存在内存中,为了使 redis 重启之后数据不丢失,就需要将 redis 数据保存到硬盘上。

redis 持久化有两种方式:

  • RDB 方式
  • AOF 方式

持久化的使用方式

  • RDB 持久化:定期保存 redis 的快照到硬盘
  • AOF 通过日志记录 redis 的命令,每次重启之后读取这个文件来重现 redis 之前的状态
  • 无持久化:单纯将 redis 作为一个缓存服务
  • 同时使用 RDB 和 AOF

RDB 方式

redis.conf 文件中可以指定保存快照的时机

1
2
3
save 900 1
save 300 10
save 60 10000

AOF 方式

需要在 redis.conf 中打开配置

1
2
3
4
5
6
7
8
9
10
# aof 是否打开 默认是关闭的 需要改为 yes
appendonly yes
appendfilename "appendonly.aof"
# 每次操作都同步日志
# appendfsync always
# 每秒同步记录一次
appendfsync everysec
# 不同步
# appendfsync no

SringBoot Redis 重要注解

Spring-boot 引入 redis 缓存机制,首先需要在启动类上增加一个注解

1
@EnableCaching

controller 的方法上增加 @Cacheable 注解,一般用于查询接口

@Cacheable(cacheNames = "product", key = '#sellerId', condition="#sellerId.length>3", unless = "#result.getCode()!=0")

#sellerId 的写法成为 spel 表达式,获取参数 sellerId的值

sellerId 的字符长度大于 3 ,而且 除了 返回的 code!=0(也就是等于0都缓存)

当数据改变之后表用 @Cacheput 注解修改缓存,通常用于保存或者修改方法

@CachePut

要使用上面两个注解更新缓存 必须保证方法的返回值一致

如果上面两个注解的 key 不填或者为空,则key默认为方法的参数,这就会导致 key不一致的问题(即使方法的返回值一致)

可以把 cacheName 属性移到 类注解 @CacheConfig(cacheNames="product")

@CacheEvict(cacheNames = "product",key="123")

清除缓存

使用上面的缓存必须实现序列化

Java泛型

泛型是从 Java 5 开始增加的对于数据类型的规范和检测机制,使用泛型有如下两个优点

  1. 可以有效地避免 ClassCastException ,在编译期就可以使用泛型发现类型转换错误,或者类型不匹配
  2. 避免强类型转换

一、泛型引入

请看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ShowExample {
public static void main(String[] args) {
List list = new ArrayList();
// 往 list 中添加一个 String
list.add("这是一个String");
// 往 list 中添加一个 整形
list.add(123);
printSomeThing(list);
}
public static void printSomeThing(List list) {
for (int i = 0; i < list.size(); i++) {
// 进行类型转换
String temp = (String) list.get(i);
System.out.println(temp);
}
}
}

运行结果如下

1
2
这是一个String
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

第一行被正确打印,但是第二行的整形却在进行强制类型转换的时候失败了,这个如果发生在程序运行期间,会给整个项目带来安全问题。所以,如果能在编译期间就发现这个问题,会对程序的健壮性有很大的提高。

泛型使用一对尖括号来设置变量的类型,多用在集合框架中。比如,如下代码定义了一个只能存放 String 类型数据的 List

1
List<String> stringList = new ArrayList<String>();

二、泛型类

当类中要操作的引用数据类型不确定时,就需要使用泛型。

JDK 1.5 之前使用 Object 来完成扩展

现在我们需要定义一个工具类用来设置和获取属性值,在没有泛型之前,可以通过下面的方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class GenericClassDemo {
public static void main(String[] args) {
ToolOld toolOld = new ToolOld();
// 通过 Object 暂存数据
toolOld.setObject(new Cat());
// 将数据强制装换回原来的类型
Cat cat = (Cat) toolOld.getObject();
// 如果强制准换的类型于原来的类型不一致,就会出现类型转换错误
// ClassCastException
// Dog dog = (Dog) toolOld.getObject();
}
}
class Cat {
}
class Dog {
}
/**
* 此方法可以用来暂存和获取一个对象
*/
class ToolOld {
private Object object;

public Object getObject() {
return object;
}

public void setObject(Object object) {
this.object = object;
}
}

运行结果没有报错,因为 Object 是所有类的父类,上面的 ToolOld 通过 object 属性暂存数据,当需要获取数据时再通过强制转换,转换成原来的类型。

定义泛型类

泛型类的定义方法如下

1
2
3
4
5
6
7
public class ClassName<泛型标识符> {
private 泛型标识符 属性名称;
// 定义一个参数类型是指定泛型的方法
private void testMethod(T t) {
...
}
}

使用泛型类

上面 Object 的方法可以使用泛型来代替。下面定义了一个泛型类 ToolGeneric,每次创建对象时可以传入类型,在调用 set 方法时,如果类型不匹配,则编译不会通过。

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
public class GenericClassDemo {
public static void main(String[] args) {
// 这里指定了类中只能传入 toolGeneric 类型
ToolGeneric<Cat> toolGeneric = new ToolGeneric<Cat>();
// 如果传入 new Dog() 就会报错
toolGeneric.setT(new Cat());
Cat cat = toolGeneric.getT();
}
}

class Cat {

}
class Dog {

}
// 泛型类 传入的类型用T来标记
class ToolGeneric<T> {
private T t;

public T getT() {
return t;
}
// 调用set 函数时传入的参数类型必须与创建对象时泛型的类型相同
public void setT(T t) {
this.t = t;
}
}

另一个泛型类的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class GenericClassShowCase {
public static void main(String[] args) {
PrintTool<String> printToolStr = new PrintTool<String>();
printToolStr.printSomeThing("你好");
// 因为已经指定 printToolStr 传入的类型为 String 下面的语句不能通过编译
// printToolStr.printSomeThing(12312);

PrintTool<Integer> printToolInt = new PrintTool<Integer>();
printToolInt.printSomeThing(3123412);
// 因为已经指定 printToolStr 传入的类型为 Integer 下面的语句不能通过编译
// printToolInt.printSomeThing("你好");
}
}

class PrintTool<T> {
public void printSomeThing(T t) {
System.out.println(t);
}
// 需要注意的是 静态方法不能引用上面的泛型 T,
// 但是可以定义泛型静态方法
// 'PrintTool.this' cannot be referenced from a static context
/*
public static void showIt(T t) {
System.out.println(t);
}*/
}

运行结果 :

1
2
你好
3123412

上面的泛型类通过每次指定不同的类型,可以打印不同的数据类型。但是每打印一个新的数据类型都需要运行 PrintTool<XXX> printTool = new PrintTool<XXX>() 来创建新的类,这样也不方便,所以有必要使用泛型方法

泛型方法

泛型方法定义时需要将一对尖括号包裹的泛型类型写在函数的返回值之前。

1
2
3
public <T> void getName(T t) {
// TODO
}

下面是一个泛型方法的例子

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 GenericMethodShow {
public static void main(String[] args) {
GenericMethod<String> stringGenericMethod = new GenericMethod<String>();
stringGenericMethod.show("你好");

// 编译不通过,应为 show 方法使用的是类传来的参数类型,
// 所以在本例中只能打印 String 类型的数据
// stringGenericMethod.show(13123);

// show 方法的参数类型在调用时通过传入的参数确定,
// 所以两种类型都可以打印
stringGenericMethod.print("你好");
stringGenericMethod.print(123124);

// 调用静态泛型方法
GenericMethod.showStatic("static");

}
}

class GenericMethod<T> {
// 这是一个普通的方法,只是参数的类型是泛型
public void show(T t) {
System.out.println("show: " + t);
}

// 这是一个泛型方法,这里泛型使用的是 T2,而不是类中的T
public <T2> void print(T2 t) {
System.out.println("print: " + t);
}

// 静态方法可以定义泛型函数,可以使用用户调用时传入的参数的类型 T3
// 但是无法使用本类的泛型 T
public static <T3> void showStatic(T3 t3) {
System.out.println("static: " + t3);
}
}
1
2
3
4
show: 你好
print: 你好
print: 123124
static: static

泛型接口

泛型接口的定义方式为

IterName{
1
2
interface IterName<T>{
}

泛型接口使用时可以在泛型类的定义时传入泛型的类型,也可以在调用方法时通过参数来确定泛型的类型

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 GenericInterfaceShow {
public static void main(String[] args) {
ShowInterImpl1 showInterImpl1 = new ShowInterImpl1();
showInterImpl1.show("哈哈");
// 编译不通过
//showInterImpl1.show(123);

ShowInterImpl2 showInterImpl2 = new ShowInterImpl2();
// 在调用时确定参数类型
showInterImpl2.show("哈哈2");
showInterImpl2.show(1234);
}
}

// 定义一个泛型接口
interface ShowInter<T> {
void show(T t);
}

// 在定义接口类时传入泛型
class ShowInterImpl1 implements ShowInter<String> {
@Override
// 这里的参数类型必须写 String
public void show(String s) {
System.out.println(s);
}
}

// 在调用时定义参数类型
// 方法定义时必须在类名和接口名后都加入泛型
class ShowInterImpl2<T> implements ShowInter<T> {

@Override
public void show(T t) {
System.out.println(t);
}
}

运行结果如下

1
2
3
哈哈
哈哈2
1234

泛型限定

当不知道数据类型时,可以用 ? 作为通配符

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
public class GenericExtends {
public static void main(String[] args) {
ArrayList<Person> personList = new ArrayList<Person>();
personList.add(new Person("aaa"));
personList.add(new Person("bbb"));
personList.add(new Person("ccc"));

ArrayList<Student> studentList = new ArrayList<Student>();
personList.add(new Person("aaa1"));
personList.add(new Person("bbb2"));
personList.add(new Person("ccc3"));

// printData(personList);
// printData(studentList);

ArrayList<String> strings = new ArrayList<String>();
strings.add("a");
strings.add("b");
strings.add("c");

ArrayList<Integer> integers = new ArrayList<Integer>();
integers.add(1);
integers.add(2);
integers.add(3);
printData(strings);
printData(integers);

}

// 使用 ? 作为限定符
public static void printData(ArrayList<?> dataList) {
Iterator<?> iterator = dataList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
// 这里也可以使用T, 这种写法与 printData 的差别不大,区别在于可以通过 t 对变量进行操作
public static <T> void printData2(ArrayList<T> dataList) {
Iterator<T> iterator = dataList.iterator();
while (iterator.hasNext()) {
T t = iterator.next();
System.out.println(t);
}
}
}

运行结果如下

1
2
3
4
5
6
a
b
c
1
2
3

可以看出:使用 ? 可以匹配不同类型的数据

这个例子说明 子父类之间的泛型不能通过多态调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 泛型限定
public class GenericExtends {
public static void main(String[] args) {
ArrayList<Person> personList = new ArrayList<Person>();
personList.add(new Person("aaa"));
personList.add(new Person("bbb"));
personList.add(new Person("ccc"));

ArrayList<Student> studentList = new ArrayList<Student>();
studentList.add(new Student("aaa1"));
studentList.add(new Student("bbb2"));
studentList.add(new Student("ccc3"));
printPerson(personList);
// 下面的代码编译不通过
// 说明 ArrayList<Person> peopleList = new ArrayList<Student>();是不成立的
// 同样 ArrayList<Student> peopleList = new ArrayList<Person>(); 也是不成立的
// printPerson(studentList);
}
public static void printPerson(ArrayList<Person> personList) {
Iterator<Person> iterator = personList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().getName());
}
}
}

class Person {
private String name;

public Person(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

class Student extends Person {
public Student(String name) {
super(name);
}

}

现在我们确实需要使用 printPerson 方法来打印 studentList ,将 printPerson 申明为泛型方法是一个途径:

1
2
3
4
5
6
7
8
public static <T> void printPerson(ArrayList<T> personList) {
Iterator<T> iterator = personList.iterator();
while (iterator.hasNext()) {

System.out.println(((Person) iterator.next()).getName());
}

}

或者使用

1
2
3
4
5
6
7
public static void printPerson(ArrayList<?> personList) {
Iterator<?> iterator = personList.iterator();
while (iterator.hasNext()) {
System.out.println(((Person) iterator.next()).getName());
}

}

但是这两种方法都有局限性:无论 personList 中存储的是什么类型的数据都可以传入。这就存在了风险:就上面的例子来说,如果 personList 里边存储的不是 Person 类(或者子类),就会造成类型转换异常。为了解决这个问题,引入了类型限定。

使用 <? extends Person> 来限定 personList 只能存储 Person 类或者其子类型

下面是一个类型限定的例子

1
2
3
4
5
6
public static void printPersonExtend(ArrayList<? extends Person> personList) {
Iterator<? extends Person> iterator = personList.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next().getName());
}
}

增加一个汽车类 Car

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Car {
private String brandName;

public Car(String brandName) {
this.brandName = brandName;
}

public String getBrandName() {
return brandName;
}

public void setBrandName(String brandName) {
this.brandName = brandName;
}
}

调用打印方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class GenericExtends {
public static void main(String[] args) {
ArrayList<Person> personList = new ArrayList<Person>();
personList.add(new Person("aaa"));
personList.add(new Person("bbb"));
personList.add(new Person("ccc"));

ArrayList<Student> studentList = new ArrayList<Student>();
studentList.add(new Student("aaa1"));
studentList.add(new Student("bbb2"));
studentList.add(new Student("ccc3"));
// 打印 Person
printPersonExtend(personList);
// 因为 Stduent 是 Person 的子类,所以也可以打印
printPersonExtend(studentList);


ArrayList<Car> carList = new ArrayList<Car>();
carList.add(new Car("Benz"));
carList.add(new Car("BMW"));
carList.add(new Car("Porsche"));
// 编译失败,因为 Car 类和 Person 类没有任何关系
// printPersonExtend(carList);

}

public static void printPersonExtend(ArrayList<? extends Person> personList) {
Iterator<? extends Person> iterator = personList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().getName());
}

}

与此同时,使用 <? super Student> 限定只能传入 Student 类及其父类,TreeSet 的一个构造方法就使用如下的限定符:

1
2
3
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<E,Object>(comparator));
}

下面是一个使用 <? super E> 的例子:

定义一个 Worker 类,它同时也是 Person 的子类

1
2
3
4
5
6
class Worker extends Person {

public Worker(String name) {
super(name);
}
}

现在我们用 TreeSet 分别存储 学生 和 工人,并将它们倒序输出

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
public class GenericSuper {
public static void main(String[] args) {
// 创建 studentTreeSet 并用 StudentComparator 指定比较的方法
TreeSet<Student> studentTreeSet = new TreeSet<Student>(new StudentComparator());
studentTreeSet.add(new Student("s--001"));
studentTreeSet.add(new Student("s--002"));
studentTreeSet.add(new Student("s--003"));
Iterator<Student> studentIterator = studentTreeSet.iterator();

while (studentIterator.hasNext()) {
System.out.println(studentIterator.next().getName());
}
// 创建 studentTreeSet 并用 WorkerComparator 指定比较的方法
TreeSet<Worker> workerTreeSet = new TreeSet<Worker>(new WorkerComparator());
workerTreeSet.add(new Worker("w--001"));
workerTreeSet.add(new Worker("w--002"));
workerTreeSet.add(new Worker("w--003"));

Iterator<Worker> workerIterator = workerTreeSet.iterator();
while (workerIterator.hasNext()) {
System.out.println(workerIterator.next().getName());
}
}
}


// 用来比较 Student 对象
class StudentComparator implements Comparator<Student> {

@Override
public int compare(Student s1, Student s2) {
return s2.getName().compareTo(s1.getName());
}
}
// 用来比较 Worker 对象
class WorkerComparator implements Comparator<Worker> {

@Override
public int compare(Worker w1, Worker w2) {
return w2.getName().compareTo(w1.getName());
}
}

输出结果如下

1
2
3
4
5
6
s--003
s--002
s--001
w--003
w--002
w--001

上面两种不同的对象比较,使用了不同的比较器,是否可以将其合并成一个呢?

查看源码 TreeSet 当前的构造函数如下

1
2
3
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<E,Object>(comparator));
}

这个源码说明比较器参数可以传入 E 类型,或者 E 类型的 父类型

现在重写一个通用的比较器

1
2
3
4
5
6
7
8
// 这里指定了类型时 Student 和 Worker 的共同父类 Person 
// 所以,当传入 Student 或者 Worker 时,比较器都是可以使用的
class AllPersonComp implements Comparator<Person> {
@Override
public int compare(Person s1, Person s2) {
return s2.getName().compareTo(s1.getName())
}
}

在定义 TreeSet 时传入上面的比较器

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
public class GenericSuper {
public static void main(String[] args) {
// 构造函数中传入的泛型是Student,后面的参数就可以传入以Student的父类作为泛型的比较器
TreeSet<Student> studentTreeSet = new TreeSet<Student>(new AllPersonComp());
studentTreeSet.add(new Student("s--001"));
studentTreeSet.add(new Student("s--002"));
studentTreeSet.add(new Student("s--003"));
Iterator<Student> studentIterator = studentTreeSet.iterator();

while (studentIterator.hasNext()) {
System.out.println(studentIterator.next().getName());
}


// 构造函数中传入的泛型是Worker,后面的参数就可以传入以Worker的父类作为泛型的比较器
TreeSet<Worker> workerTreeSet = new TreeSet<Worker>(new AllPersonComp());
workerTreeSet.add(new Worker("w--001"));
workerTreeSet.add(new Worker("w--002"));
workerTreeSet.add(new Worker("w--003"));

Iterator<Worker> workerIterator = workerTreeSet.iterator();
while (workerIterator.hasNext()) {
System.out.println(workerIterator.next().getName());
}
}


}

运行结果与上面相同

mysql问题

  1. 提示时区错误

    具体报错如下

    1
    The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone

    有两种解决方法

    a. 在数据库路径的后面增加时区配置选项

    1
    url: jdbc:mysql://localhost:3306/sell?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8

    b. 增加全局配置的选项

    使用下面的语句查看当前的时区配置

    1
    SHOW VARIABLES LIKE '%time_zone%'

    输出结果如下

Variable_name Value
system_time_zone
time_zone SYSTEM

将时区设置为 +8

1
SET GLOBAL time_zone = '+8:00'

git远程分支

推送

当你想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。 本地的分支并不会自动与远程仓库同步 - 你必须显式地推送想要分享的分支。 这样,你就可以把不愿意分享的内容放到私人分支上,而将需要和别人协作的内容推送到公开分支。

如果希望和别人一起在名为 serverfix 的分支上工作,你可以像推送第一个分支那样推送它。 运行 git push (remote) (branch):

1
2
3
4
5
6
7
8
$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
* [new branch] serverfix -> serverfix

这里有些工作被简化了。 Git 自动将 serverfix 分支名字展开为 refs/heads/serverfix:refs/heads/serverfix,那意味着,“推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支。” 我们将会详细学习 Git 内部原理 的 refs/heads/ 部分,但是现在可以先把它放在儿。 你也可以运行 git push origin serverfix:serverfix,它会做同样的事 - 相当于它说,“推送本地的 serverfix 分支,将其作为远程仓库的 serverfix 分支” 可以通过这种格式来推送本地分支到一个命名不相同的远程分支。 如果并不想让远程仓库上的分支叫做 serverfix,可以运行 git push origin serverfix:awesomebranch 来将本地的 serverfix 分支推送到远程仓库上的 awesomebranch 分支。

下一次其他协作者从服务器上抓取数据时,他们会在本地生成一个远程分支 origin/serverfix,指向服务器的 serverfix 分支的引用:

需要特别注意的是 可以通过 git push origin serverfix:awsomeserverfix 命令,将本地的 serverfix 分支的内容,推送到远程awsomeserverfix 分支上。

1
2
3
4
5
6
7
$ git fetch origin
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/schacon/simplegit
* [new branch] serverfix -> origin/serverfix

要特别注意的一点是当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。 换一句话说,这种情况下,不会有一个新的 serverfix 分支 - 只有一个不可以修改的 origin/serverfix 指针。

可以运行 git merge origin/serverfix 将这些工作合并到当前所在的分支。 如果想要在自己的 serverfix 分支上工作,可以将其建立在远程跟踪分支之上:

$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch ‘serverfix’
这会给你一个用于工作的本地分支,并且起点位于 origin/serverfix。

拉取

git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。 它只会获取数据然后让你自己合并。 然而,有一个命令叫作 git pull 在大多数情况下它的含义是一个 git fetch 紧接着一个 git merge 命令。 如果有一个像之前章节中演示的设置好的跟踪分支,不管它是显式地设置还是通过 clonecheckout 命令为你创建的,git pull 都会查找当前分支所跟踪的服务器与分支,从服务器上抓取数据然后尝试合并入那个远程分支。

由于 git pull 的魔法经常令人困惑所以通常单独显式地使用 fetch 与 merge 命令会更好一些。

删除远程分支

1
git push origin --delete serverfix

删除远程仓库的 serverfix 分支

默认跟踪分支

下面的命令将本地的 ccc 分支与远程仓库 originnihao 分支建立跟踪关系

1
git branch --set-upstream-to=origin/nihao ccc

如果不指定后面的 ccc 分支,则将当前所在分支与 origin/nihao 建立跟踪关系

tomcat配置虚拟目录

项目中的文件我们经常会存储在其他的文件夹,而不是数据库,而 tomcat 服务器可以将 文件的相对路径映射为文件的绝对路径

javascript的apply和call方法,以及构造函数

javascript 的每个函数都包含两个非继承而来的方法:apply()call()。这两个方法都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值

apply() 函数

首先,apply() 方法接收两个参数:

  1. 运行函数的作用域 ( this ) 的值。
  2. 参数数组,这个参数可以是 Array 的实例,也可以是 arguments 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function sum(num1, num2) {
return num1 + num2;
}

function callSum1(num1, num2) {
return sum.apply(this, arguments);
}

function callSum2(num1, num2) {
return sum.apply(this,[num1, num2])
}

console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20

上面的例子中, callSum1() 在执行 sum() 函数时传入了 this 作为 this 值(因为是在全局作用域中调用的,所以传入的就是 window 对象) 和 arguments 对象。而 callSum2 同样也调用了 sum() 函数,但它传入的是 this 和一个参数数组。这两个函数都会正常返回结果。

在严格模式下,未指定环境对象而调用函数,则 this 值不会转化为 window ,而是 undefined

call() 函数

call() 方法与 apply() 函数的作用相同,他们区别仅在于接收参数的方式不同。对于 call() 方法而言,第一个参数 this 值没有变化,变化的是其余参数都直接传递给函数。换句话说,在使用 call 方法时,传递给函数的参数必须逐个列举出来。

1
2
3
4
5
6
7
8
cafunction sum(num1, num2) {
return num1 + num2;
}

function callSum(num1, num2) {
return sum.call(this, num1, num2)
}
console.log(callSum(10, 10)); //20

对象的构造函数

下面是 Person 的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name) ;
};
}
var person = Person("Bill Gates", 65, 'Software Enginerr');
var person1 = new Person('梅西', 29, 'football player');


上面第一种调用方法是 javascript 普通函数的调用方法,这里的 Person() 就是一个普通的 javascirpt 函数,调用过程中 thiswindow 对象,这里不会返回任何的对象,最后person 的值是 undefined

按普通函数调用

第二种调用方法主要经历了一下 4 个步骤:


  1. 创建一个新对象,暂时命名为 person 。
  2. 将构造函数的作用域附给了新的对象(如果使用 call 方法来调用,也就是 Person.call(person,'梅西', 29, 'football player' ))
  3. 执行构造函数中的代码。
  4. 返回新的对象。

查看运行过程,我们发现 this 指向新创建的的 person 对象。

按构造函数调用

任何函数,只要通过 new 操作符来调用,那就可以当做构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样

javascript函数调用中的 call 和 apply 方法

每个 javascript 函数都包含两个非继承而来的方法:apply()call() 。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。
首先,apply() 方法接收两个参数:一个是在其中运行函数的的作用域,另一个是参数数组。其中第二个参数可以是 Array 的实例,也可以是 arguments 对象。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function sum(num1, num2) {
return num1 + num2;
}


function callSum1(num1, num2) {
return sum.apply(this, arguments);
}

function callSum2(num1, num2) {
return sum.apply(this,[num1, num2]);
}

console.log(callSum1(10, 10));
console.log(callSum2(10, 10))

vscode使用技巧

1.打开 markdown 实时预览窗口

在命令行中输入
open locked preview to the side

git基础

1. 撤销提交

如果不小心运行了 git add * 把你不想跟踪的文件添加到了版本控制,使用如下命令撤销提交

1
2
3
4
5
6
7
8
9
git reset HEAD <File>
​````

## 2.撤销对文件的修改

如果修改了某个文件,但是想还原使用如下命令

​```bash
git checkout -- <file>

3.查看远程仓库列表

1
git remote

3.添加远程仓库

1
git remote add <shortname> <url>

例如

1
2
3
4
5
6
$ git remote add pb https://github.com/paulboone/ticgit
$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)
pb https://github.com/paulboone/ticgit (fetch)
pb https://github.com/paulboone/ticgit (push)

4.将本地修改提交到远程服务器

1
git push <remote> <branch>

下面的命令的意思是 将本地 master 分支推送到 server 服务器上.

1
git push origin master

5.了解远程仓库的详细信息

1
git remote show <remotename>

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git remote show origin
* remote origin
Fetch URL: git@github.com:lastdetective/myblogsourcecode.git
Push URL: git@github.com:lastdetective/myblogsourcecode.git
HEAD branch: master
Remote branches:
master tracked
smallcomputer tracked
Local branch configured for 'git pull':
master merges with remote master
Local refs configured for 'git push':
master pushes to master (up to date)
smallcomputer pushes to smallcomputer (up to date)

6.远程仓库的移除与重命名

重命名 将远程仓库由 pb 重命名 为 paul

1
git remote rename pb paul

移除paul

1
git remote rm paul

7.直接添加和提交代码

1
git commit -a -m 'made a change'

8.直接新建和转换分支

1
git checkout -b hotfix

9. pull 命令

1
git pull origin master

这里的 master 是指远程的 master 分支

10. push 命令

1
git push origin master

将本地的 master 分支的数据,推送到它对应的 远程分支上去 这个远程分支可能是 awsomemaster

11.git 回退版本

如果当前版本是 HEAD, 那么上一个版本就是 HEAD^,上上一个版本就是 HEAD^^ ,当然往上写100个 ^ 容易写不过来,所以写成 HEAD~100。我们要将当前版本回退到上一个版本,使用如下命令:

1
git reset --hard HEAD^

如果我们知道版本号,还可以使用直接指定版本号

1
git reset --hard s7s6d734

12.查看命令历史

使用如下命令查看命令历史

1
git reflog

结果如下

1
2
3
4
5
6
7
$ git reflog
a993996 (HEAD -> master) HEAD@{0}: reset: moving to a993996
286b286 HEAD@{1}: reset: moving to HEAD^
a993996 (HEAD -> master) HEAD@{2}: commit: sss
286b286 HEAD@{3}: commit: 我再次看看
eee6a49 HEAD@{4}: commit: 让我看看是修改了哪一个
83f7fe8 HEAD@{5}: commit (initial): 初次提交

13.比较命令

不加参数,暂存区有文件未提交(commit)时,比较工作区和暂存区的文件

当暂存区的文件已经提交时,则比较工作区与最近一次提交的版本,如下比较名为 test1 的文件。

1
git diff test1

如果同时你有一个分支叫做 test1 则以下命令

1
git diff test1

则会出现如下的歧义

1
2
3
fatal: ambiguous argument 'test1': both revision and filename
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

这个时候需要制定版本号文件名

1
git diff test1 -- test1

上面的命令比较当前分支工作区的 test1 文件 与 test1 分支上最新版本的 test1 文件

比较工作区与缓存区的文件

1
git diff --cached 文件名

查看工作区和版本库里面最新版本的区别

1
git diff HEAD 文件名

比较工作区与指定 commit-id 的区别

1
git diff commit-id 文件名

比较两个 commit-id 之间的区别

1
git diff [<>]

14.git分支

查看分支

1
git branch 

创建分支

1
git branch <name>

切换分支

1
git checkout <name>

创建+切换分支

1
git checkout -b <name>

删除分支

1
git branch -d <name>

合并分支

1
git merge <name>

15.git的标签

打标签

1
git tag <tagname>

指定标签信息

1
git tag -a <tagname> -m 'bulabula'

查看所有的标签

1
git tag

命令git push origin 可以推送一个本地标签;

命令git push origin –tags可以推送全部未推送过的本地标签;

命令git tag -d 可以删除一个本地标签;

命令git push origin :refs/tags/可以删除一个远程标签。