dream

一个菜鸟程序员的成长历程

0%

转语言者速学java

接口的默认方法

java的接口里面可以写方法体,这样的话,所有实现了该接口的,都自动具有了该方法而不用实现方法内容。当然了,也可以重写方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface IGuideService {

public default Integer getNum() {
return 1;
}
}

public class GuideService implements IGuideService {


}

public class Main {

public static void main(String [] args) {
IGuideService guideService = new GuideService;
System.out.println(guideService.getNum());
}
}

但是java可以实现多个接口,所以就可能会有二义性的问题,C++解决问题的方法是虚基类,java则是简单粗暴的。

  • 如果是父类中和接口中的方法冲突了。那么选择父类中的方法。
  • 如果是多个接口中的方法冲突了,那么你必须通过覆盖该方法来手动解决冲突。

看一下如果父类中也有getNum方法。创建一个父类返回2.再次执行以后会发现返回2了。

1
2
3
4
5
6
7
8
9
10
11
12
public class BaseGuideService {

public Integer getNum() {
return 2;
}
}

// 继承父类
public class GuideService extends BaseGuideService implements IGuideService {


}

接下来不用父类,然后创建两个接口试试。

1
2
3
4
5
6
7
8
9
10
11
12
public interface IGuideNewService {

public default Integer getNum() {
return 3;
}
}

// 实现两个接口
public class GuideService implements IGuideService,IGuideNewService {


}

这个时候编译就会报错了。

GuideService 从 IGuideService 和 IGuideNewService 中继承了getNum() 的不相关默认值

那如果这两个接口是父子接口呢?这个时候就不会报错了,因为相当于子接口重写了这个方法。所以只存在一个getNum方法了。

我们来看如果是单独的两个接口,那么需要我们手动来解决冲突。重写一下这个方法。

1
2
3
4
5
6
7
8
9
// 实现两个接口
public class GuideService implements IGuideService, IGuideNewService {

@Override
public Integer getNum() {
return 5;
}
}

在重写的时候也可以选择使用两个接口的某一个。

1
2
3
4
5
6
7
8
9
10
11
12
// 实现两个接口
public class GuideService implements IGuideService, IGuideNewService {

@Override
public Integer getNum() {
return IGuideService.super.getNum();

//或者使用另一个
//return IGuideNewService.super.getNum();
}
}

日期时间

在java中的时间,有几个概念

  • 某一个具体的时间点:Instant
  • 持续时间,两个时间点之间的时间:Duration
  • 本地时间,没有时区信息的:LocalDate,LocalTime,LocalDateTime
  • 带时区的时间:ZonedDateTime
  • 处理时区信息时,时间段使用 Period 而不是 Duration
  • 时间格式化处理: DateTimeFormatter
  • 日历计算,比如查找月份的第一天,第二天,第一周,第二周的星期二:TemporalAdjuster

获取当前的一个时间点。

1
Instant.now();

获取一段时间,可以用来求运行速度。start是处理开始的时间点。end是处理完成的时间点。再通过Duration来求出一段时间.

1
2
3
4
5
6
Instant start = Instant.now();
//处理逻辑
Instant end = Instant.now();

Duration time = Duration.between(start,end);
long mills = time.toMills();

获取本地当前时间

1
LocalDateTime time = LocalDateTime.now();

时间格式化

1
2
LocalDateTime time = LocalDateTime.now();
time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

获取带时区的当前时间

1
ZonedDateTime time = ZonedDateTime.now();

想看更多时间处理的,可以参考 LocalDateTime源码解析

stream

stream可以帮我们处理很多东西。比如

方法名 说明
map 循环。可以简单理解为foreach
flatMap 将二维数据展开成一维
filter 过滤数据
distinct 去重
sorted 排序
limit 限制只取n个元素
skip 跳过n个元素

除了这些还可以实现比如把list转换成逗号分隔的字符串。把list经过处理以后转换成set或者map。

还可以变成指定值为key,指定值为value的map。比如id为key,name为值或者map为值的map。

具体的可以参考下面三个文章。

optional

java中的optional可以帮助你避免空指针异常.

具体的可以参考optional并非银弹

函数式编程和lambda

注解

  • @Autowired Spring提供的,基于类型注入的,可以放在setter方法上,变量上,构造函数上
  • @Inject 同@Autowired
  • @Qualifier Spring提供的,基于名称注入的,一般和@Autowired配合使用来通过value参数指定名称
  • @Name 同@Qualifier,给@Inject配合使用
  • @Primary
  • @Resource Java提供的,可以基于类型或名称注入的,可以通过name参数来指定名称,可以放在setter方法上
  • @RequiredArgsConstructor lombok提供的,基于类型注入,通过增加一个构造函数来注入。
  • @Value Spring提供的,注入基本类型的注解,一般用来从配置文件取值。
  • @Component 单纯的说我是一个bean
  • @Service 和上面的一样,不过一般用在service类中,更加语义化
  • @Controller 和上面的一样,一般用在controller类中
  • @Repository 我也是一个bean,一般用在dao数据访问层
  • @Bean 我也是一个bean,导入第三方包里面的注解
  • @Import 导入组件
  • @ImportSelector 返回需要导入的组件的全类名数组
  • @ImportBeanDefinitionRegistrar 手动注册Bean到容器
  • @JsonIgnore json的时候忽略属性
  • @Bean(initMethod=”init”,destoryMethod=”destory”) 设置初始化和销毁方法
  • @PostConstruct:初始化方法
  • @PreDestory:销毁方法
  • BeanPostProcessor:bean的后置处理器,在bean初始化前后进行一些处理工作
  • @Configuration 声明为配置类
  • @ComponentScan 扫描Component
  • @Aspect 声明一个切面
  • @After 在方法执行之后执行(方法上)
  • @Before 在方法执行之前执行(方法上)
  • @Around 在方法执行之前与之后执行(方法上)
  • @PointCut 声明切点
  • @EnableAspectJAutoProxy 启Spring对AspectJ代理的支持
  • @Profile 指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件。
  • @Conditional 通过实现Condition接口,并重写matches方法,从而决定该bean是否被实例化。
  • @EnableAsync 配置类中通过此注解开启对异步任务的支持
  • @Async 在实际执行的bean方法使用该注解来声明其是一个异步任务
  • @EnableScheduling 在配置类上使用,开启计划任务的支持
  • @Scheduled 来申明这是一个任务,包括cron,fixDelay,fixRate等类型
  • @EnableConfigurationProperties:开启对@ConfigurationProperties注解配置Bean的支持;
  • @EnableJpaRepositories:开启对SpringData JPA Repository的支持;
  • @EnableTransactionManagement:开启注解式事务的支持;
  • @EnableCaching:开启注解式的缓存支持;
  • @EnableAspectAutoProxy:开启对AspectJ自动代理的支持;
  • @EnableWebMvc 在配置类中开启Web MVC的配置支持。
  • @RequestMapping 用于映射web请求,包括访问路径和参数。
  • @ResponseBody 支持将返回值放到response内,而不是一个页面,通常用户返回json数据。
  • @RequestBody 允许request的参数在request体中,而不是在直接连接的地址后面。
  • @PathVariable 用于接收路径参数,比如@RequestMapping(“/hello/{name}”)声明的路径,将注解放在参数前,即可获取该值,通常作为Restful的接口实现方法。
  • @RestController 该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。
  • @ControllerAdvice 全局异常处理 全局数据绑定 全局数据预处理
  • @ExceptionHandler 用于全局处理控制器里的异常。
  • @InitBinder 用来设置WebDataBinder,WebDataBinder用来自动绑定前台请求参数到Model中。
  • @ModelAttribute
  • @Transactional
    • name 当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。
    • propagation 事务的传播行为,默认值为 REQUIRED。
      • REQUIRED 如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 )
      • SUPPORTS 如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
      • MANDATORY 如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
      • REQUIRES_NEW 重新创建一个新的事务,如果当前存在事务,暂停当前的事务。
      • NOT_SUPPORTED 以非事务的方式运行,如果当前存在事务,暂停当前的事务。
      • NEVER 以非事务的方式运行,如果当前存在事务,则抛出异常。
      • NESTED 和 REQUIRED 效果一样。
    • isolation 事务的隔离度,默认值采用 DEFAULT
    • timeout 事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
    • read-only 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
    • rollback-for 用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。
    • no-rollback- for 抛出 no-rollback-for 指定的异常类型,不回滚事务。
  • @Schema 表示此类对应的数据库表对应的schema。
  • @JsonFormat 可以方便的把Date类型直接转化为我们想要的模式
  • @Transient 如果一个属性并非数据库表的字段映射,就务必将其标示为@Transient
  • @JsonProperty 可以指定某个属性和json映射的名称
  • @Scope设置类型包括:

设置Spring容器如何新建Bean实例(方法上,得有@Bean)

① Singleton

(单例,一个Spring容器中只有一个bean实例,默认模式),

② Protetype

(每次调用新建一个bean),

③ Request

(web项目中,给每个http request新建一个bean),

④ Session

(web项目中,给每个http session新建一个bean),

⑤ GlobalSession

(给每一个 global http session新建一个Bean实例)

stream

stream的中间态

中间态的主要作用是构建双向链表的中间节点。一个操作对应一个节点。比如map,就会创建一个节点。其中pre指针指向前一个节点也就是头节点。而头节点的next指针指向map节点。

filter操作的时候同样创建一个节点,pre指针指向上一个操作也就是map节点。map节点的next指针指向filter节点。

每个中间态节点中都存储了操作,也就是中间态的时候传入的函数。而数据则全部在头节点中。

比如下面这样:

strem

每个中间态节点其实又分成两种

  • 有状态节点
  • 无状态节点

类图如下:

stream12

中间态节点的几个操作如下:

stream13

我们目前的代码中使用了两个中间态的方法。

  • map
  • filter

map

我们调用的是ReferencePipeline类的map方法。作为中间态方法,需要链式操作,所以返回值当然是一个stream了。接受一个函数作为入参,可以是一个写好的函数,也可以是一个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
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
//参数校验
Objects.requireNonNull(mapper);
//创建一个无状态的对象
//第一个参数是this,也就是刚才初始化好的只有一个头节点的双向链表。
//第二个参数是一个常量 REFERENCE
//第三个参数是 8 & 2 = 1000 & 0010 = 1010 = 10
return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {

//增加了opWrapSink方法,第一个参数是标志 = 95,第二个是sink节点
//这个方法会在结果态的时候调用
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
//创建一个 Sink.ChainedReference类的对象并返回。具体的放在结果态里面讲。
return new Sink.ChainedReference<P_OUT, R>(sink) {
@Override
public void accept(P_OUT u) {
downstream.accept(mapper.apply(u));
}
};
}
};
}

//无状态节点的构造方法
//第一个参数是this,也就是刚才初始化好的只有一个头节点的双向链表。
//第二个参数是一个常量 REFERENCE
//第三个参数是 8 & 2 = 1000 & 0010 = 1010 = 10
StatelessOp(AbstractPipeline<?, E_IN, ?> upstream,
StreamShape inputShape,
int opFlags) {
//调用父类的构造函数
super(upstream, opFlags);
// upstream.getOutputShape() 返回的就是 REFERENCE, inputShape = REFERENCE,
// 所以返回true
assert upstream.getOutputShape() == inputShape;
}

//巧了,他的父类也是ReferencePipeline,所以又来到了它的构造方法,不过和上次那个不是同一个了。
//这个作用是构造一个中间态的节点,加入到双向链表也就是加入到流中。
//第一个参数是 双向链表
//第二个是 10
ReferencePipeline(AbstractPipeline<?, P_IN, ?> upstream, int opFlags) {
//再次调用父类的构造方法
super(upstream, opFlags);
}

//这里同样来到了AbstractPipeline的构造方法。
//这个方法的作用是构造一个中间态的节点。
//第一个参数是上一个节点,这里上一个就是头节点。
//第二个参数是10
AbstractPipeline(AbstractPipeline<?, E_IN, ?> previousStage, int opFlags) {
//判断上一个节点是否连接了消费者,如果连接了就报错。
if (previousStage.linkedOrConsumed)
//抛出非法状态异常
throw new IllegalStateException(MSG_STREAM_LINKED);

//把上一个节点的是否连接消费者 = 1
previousStage.linkedOrConsumed = true;
//上一个节点的 next 指针指向当前节点。
previousStage.nextStage = this;

//当前节点的头指针指向上一个节点
this.previousStage = previousStage;
//10 & 16777407 = 10
this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK;
//组合当前节点的标志和上一个节点的标志生成新的标志 生成的新标志 = 90
this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousStage.combinedFlags);
//当前节点的数据 = 上一个节点的数据 = list
this.sourceStage = previousStage.sourceStage;
//当前节点是否是有状态的节点,这里是false
if (opIsStateful())
sourceStage.sourceAnyStateful = true;
//深度+1 = 1
this.depth = previousStage.depth + 1;
}

//组合当前节点的标志和上一个节点的标志生成新的标志
//第一个参数10
//第二个参数95
static int combineOpFlags(int newStreamOrOpFlags, int prevCombOpFlags) {
// 0x01 or 0x10 nibbles are transformed to 0x11
// 0x00 nibbles remain unchanged
// Then all the bits are flipped
// Then the result is logically or'ed with the operation flags.
// 95 & -16 | 10 = 90
return (prevCombOpFlags & StreamOpFlag.getMask(newStreamOrOpFlags)) | newStreamOrOpFlags;
}

@Override
final StreamShape getOutputShape() {
return StreamShape.REFERENCE;
}

到这里map方法就执行结束了。可以看到这里依然没有真正的执行map方法。只是封装成了一个中间态的节点并加入了双向链表。并将数据list和操作mapper都存入了节点。如下。

stream3.png

filter

filter和map一样,作为中间态的方法。来看一下它的源码。

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
// 同样需要返回一个stream对象。
// 同样接受一个方法或lambda表达式
public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
//参数校验
Objects.requireNonNull(predicate);
//同样创建一个无状态的节点并返回
//第一个参数是this,是包含了头节点和map节点的链表。
//第二个参数是一个常量 REFERENCE
//第三个参数是 128
//具体的代码和map的执行是一样的创建。并没有区别。不再赘述。
return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SIZED) {

//增加了opWrapSink方法,第一个参数是标志 = 90,第二个是sink节点
//这个方法会在结果态的时候调用
Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
//创建一个 Sink.ChainedReference类的对象并返回。具体的放在结果态里面讲。
return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
@Override
public void begin(long size) {
downstream.begin(-1);
}

@Override
public void accept(P_OUT u) {
if (predicate.test(u))
downstream.accept(u);
}
};
}
};
}

当filter完成以后,可以得到下面的数据结构。

stream4.png

其他的中间态方法

总共有9个中间态方法,除了上面的两个还有

  • 无状态
    • flatMap
    • unordered
    • peek
  • 有状态
    • distinct
    • sorted
    • limit
    • skip

flatMap

先来看flatMap这个。他的作用是把给定的二维数组,转化成一维数组。比如

  • 给定输入:[[1],[2],[3],[4,5]]
  • 要求输出:[1,2,3,4,5]

来看应用层代码。首先构建一个二维数组,然后调用flatMap方法,传入Collection::stream方法进行处理元素,最后通过collect变成一个一维的list。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//首先初始化输入列表
List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<String> list3 = new ArrayList<>();
List<String > list4 = new ArrayList<>();
list1.add("1");
list2.add("2");
list3.add("3");
list4.add("4");
list4.add("5");
List<List<String>> list = new ArrayList<>();
list.add(list1);
list.add(list2);
list.add(list3);
list.add(list4);

//开始执行操作
List<String> listT = list.stream().flatMap(Collection::stream).collect(Collectors.toList());

return listT;

看一下具体的执行流程。橙色的是stream的通用执行流程,不管你中间态用哪个方法,这里是不变的,蓝色的是ArrayListSpliterator分割器。红色的执行流程是flatMap的执行流程。

stream14.png

可以看到ArrayListSpliterator先取出第一个元素[1]这个一维数组传递给flatMap,然后flatMap执行了我们传入的Collection::stream方法,该方法我们之前说过是初始化一个stream头节点。也就是再生成了一个stream

重点就是这里了。再次把[1]这个一维数组放入了新的stream里面。然后把结果态节点ReduceOps传递给了新的stream作为新的stream的结果态节点。

这个时候新的stream开始执行ArrayListSpliterator。从而把[1]一维数组进行for循环,取出了其中的1这个元素,然后把1传入了同一个ReduceOps进行处理从而组成了一个结果list->[1]。

把步骤总结如下:

  1. 取出二维数组的第一个一维数组
  2. 把一维数组和结果态节点重新创建一个stream
  3. 执行stream把一维数组的元素循环放入结果态的list

循环二维数组,不断重复上述步骤,就可以把二维数组展开成一维数组了。

源码分析

来看方法的源码,可以看到接受一个方法,返回一个stream,标准的中间态处理。

可以看到参数校验以后就创建了一个无状态节点,节点的具体创建上面说过了。是一样的,不同的就是accpet这个方法。

这个处理逻辑,接受一个参数,这个参数就是上面传入的一维数组,第一次是[1],第二次是[2],第三次是[3],第四次是[4,5]。因为我们传入的方法是 Collection.stream方法,所以会生成一个新的stream并返回给result。

这个时候的result就是一个只有一个头节点的stream。头节点中包含了一维数组[1]。然后判断stream不为空,则调用stream顺序流进行处理,并把collect结果态节点传入。

forEach处理完以后,该accept方法处理完成。回到当前流也就是二维数组的流中。然后取出下一个一维数组[2]再次进入accept方法执行。直到当前二维数组的流处理完成。

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
@Override
public final <R> Stream<R> flatMap(Function<? super P_OUT, ? extends Stream<? extends R>> mapper) {
//参数校验
Objects.requireNonNull(mapper);
// We can do better than this, by polling cancellationRequested when stream is infinite
// 创建无状态节点并返回
return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT | StreamOpFlag.NOT_SIZED) {
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
return new Sink.ChainedReference<P_OUT, R>(sink) {
@Override
// 初始化数据
public void begin(long size) {
downstream.begin(-1);
}

@Override
// 执行逻辑处理
public void accept(P_OUT u) {
// 调用传入的方法进行处理元素 u ,u可能是 [1] [2] [3] [4,5]
// 并将执行结果新的stream赋值给 result
try (Stream<? extends R> result = mapper.apply(u)) {
// We can do better that this too; optimize for depth=0 case and just grab spliterator and forEach it
// 如果执行成功 返回了一个有效的 stream,则调用stream顺序流循环处理 downstream
// 这里的 downstream就是 结果态节点 ReduceOp 也就是 collect 收集方法
if (result != null)
result.sequential().forEach(downstream);
}
}
};
}
};
}

来看一下forEach方法,这个方法很简单。如果不是并行流,那么调用ArrayListSpliteratorforEachRemaining进行处理,传入结果态节点collect。

forEachRemaining这个方法我们很熟悉了,无非就是循环流中的元素并传入sink链处理。要注意的是这个时候的流是上面新创建的流,这个流里面的元素是[1]这个一维数组。而这个流里面是没有中间态节点的,只有一个传入的collect结果态节点组成的sink链。所以这个流处理完以后就会把[1]里面的1这个元素收集到collect结果态节点中。

当这个forEach处理完这个一维数组以后,返回到上面的accept方法中。

1
2
3
4
5
6
7
8
9
10
11
@Override
public void forEach(Consumer<? super E_OUT> action) {
//判断不是并行流
if (!isParallel()) {
//使用ArrayListSpliterator的forEachRemaining方法,传入结果态节点进行处理。
sourceStageSpliterator().forEachRemaining(action);
}
else {
super.forEach(action);
}
}

unordered

这个方法很少使用。主要是不保证流有序,而不是主动打乱流的顺序。直接看当前类中的方法源码吧。

如果有序,直接返回了,如果无序的话会返回一个无状态节点。而这里面并没有accept操作,而是直接返回了传入的sink节点。

比如传入 reduceOp 这个结果态的sink,那就返回这个。如果传入 map 的 sink 就返回这个sink。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public Stream<P_OUT> unordered() {
//判断是否有序,如果无序,直接返回
if (!isOrdered())
return this;

//如果有序,创建一个无状态节点,sink链直接返回。
return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE, StreamOpFlag.NOT_ORDERED) {
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
return sink;
}
};
}

//判断是否有有序的标志
final boolean isOrdered() {
//判断标志位里面是否有ordered这个标志位
return StreamOpFlag.ORDERED.isKnown(combinedFlags);
}

peek

这个方法很少使用。主要是调试的时候使用来查看元素是否经过流,当然了也有其他的用法,如果你能保证它不出错的情况下。

来看一个debug的例子。他将打印每一个流经peek方法的元素,在当前场景下,所有元素都会执行打印操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//首先初始化输入列表
List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<String> list3 = new ArrayList<>();
List<String > list4 = new ArrayList<>();
list1.add("1");
list2.add("3");
list3.add("2");
list4.add("5");
list4.add("4");
List<List<String>> list = new ArrayList<>();
list.add(list1);
list.add(list2);
list.add(list3);
list.add(list4);

//开始执行操作
List<String> listT = list.stream().flatMap(Collection::stream).peek(e -> System.out.println(e)).collect(Collectors.toList());


return listT;

但是如果我们修改一下,比如增加filter方法。这个时候,第二个peek方法,就只有>2的元素会流经了,从而只会打印出>2的元素。

1
2
3
4
//开始执行操作
List<String> listT = list.stream().flatMap(Collection::stream).peek(e -> System.out.println(e)).filter(x -> {
return x > 2;
}).peek(e -> System.out.println(e)).collect(Collectors.toList());

来看一下peek的执行时序图。可以看到第一个元素1执行完filter由于不满足条件所以后面的peek和collect都没有再执行。

而第二个元素3执行完filter以后又执行了peek和collect。所以这就是为什么一般又来debug的原因,因为他不一定执行。

stream15

接下来看一下它的源码.接受一个方法进行处理,返回一个stream。

而真正处理的时候,只是把流中的元素作为参数调用了peek的这个方法,然后不管结果,再次流动。相当于在流中间插入了一个方法。

所以理论上,你可以插入任何方法。但是要小心,如果因为某些原因导致流中的元素没有走到你的peek方法中,可能会产生印象不到的问题。

所以官方也更推荐用来debug。

This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public final Stream<P_OUT> peek(Consumer<? super P_OUT> action) {
//参数校验
Objects.requireNonNull(action);
//创建一个无状态节点并返回
return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
0) {
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
@Override
public void accept(P_OUT u) {
//接受到流中的一个元素
//使用该元素作为参数调用peek传入的方法
action.accept(u);
//调用完以后不管结果如何 继续流动下去
downstream.accept(u);
}
};
}
};
}

distinct

distinct的作用很明显了,是去重

看一下应用层代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//首先初始化输入列表
List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<String> list3 = new ArrayList<>();
List<String > list4 = new ArrayList<>();
list1.add("1");
list2.add("3");
list3.add("3");
list4.add("5");
list4.add("4");
List<List<String>> list = new ArrayList<>();
list.add(list1);
list.add(list2);
list.add(list3);
list.add(list4);

//开始执行操作
List<String> listT = list.stream().flatMap(Collection::stream).peek(e -> System.out.println(e)).distinct().collect(Collectors.toList());

return listT;

接下来看源码。直接返回DistinctOps节点。

1
2
3
4
@Override
public final Stream<P_OUT> distinct() {
return DistinctOps.makeRef(this);
}

看一下makeRef这个方法。

他首先是创建了一个有状态节点stateFulOps对象。接下来分为三种情况

  • 已经是去重的了,不再去重
  • 有序去重
  • 无序去重

先看无序去重,在这个对象的begin初始化中,创建了一个hashSet对象。

accept执行方法中。首先判断了流中的这个元素是否存在hashSet中,如果存在了就不继续沿着sink链执行了。如果不存在,将元素放入hashSet中并继续执行sink链。

通过hashSet来达到去重的一个效果。这将输出1,3,5,4

再来看有序去重,在这个对象的begin初始化中,创建了seenNulllastSeen属性。

  • seenNull: 当前元素是否为null
  • lastSeen: 上一个元素

accept方法中。判断当前元素是null并且seenNull = false,那么设置seenNull = true 且 lastSeen = null 并执行下一个操作。

否则,判断 lastSeen 是null也就是代表当前元素是第一个元素,或者 当前元素不等于上一个元素,那么自然是不重复的,所以执行下一个操作。

如果都不满足说明重复。那么不执行。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
static <T> ReferencePipeline<T, T> makeRef(AbstractPipeline<?, T, ?> upstream) {
//生成有状态节点。传入stream,标志位传入IS_DISTINCT也就是要去重。
return new ReferencePipeline.StatefulOp<T, T>(upstream, StreamShape.REFERENCE,
StreamOpFlag.IS_DISTINCT | StreamOpFlag.NOT_SIZED) {
@Override
Sink<T> opWrapSink(int flags, Sink<T> sink) {
//参数校验
Objects.requireNonNull(sink);

//判断标志位是否有DISTINCT标志
//注意这里的flags是上一个节点的标志位,以我们的代码为例,这个是peek的标志位。所以这里返回false
//这里的判断相当于如果你已经去重了,就不需要在去重了。比如distinct().distinct()。只会去重一次。
if (StreamOpFlag.DISTINCT.isKnown(flags)) {
return sink;
} else if (StreamOpFlag.SORTED.isKnown(flags)) {
//判断是否有序的标志位,也就是判断前面是否已经有序 这里是无序,所以不走这里。
return new Sink.ChainedReference<T, T>(sink) {
boolean seenNull;
T lastSeen;

@Override
public void begin(long size) {
seenNull = false;
lastSeen = null;
downstream.begin(-1);
}

@Override
public void end() {
seenNull = false;
lastSeen = null;
downstream.end();
}

@Override
public void accept(T t) {
//有序的去重比无序简单很多
//因为有序,如果重复那么必然是形如 a a a 这种重复,也就是重复的一定在一起。
if (t == null) {
if (!seenNull) {
seenNull = true;
downstream.accept(lastSeen = null);
}
} else if (lastSeen == null || !t.equals(lastSeen)) {
//所以这里只需要判断lastSeen也就是上一个出现的这个元素是null
//或者当前元素不等于上一个出现的元素那么就是不重复的
//比如 2 1 1,当前元素是第一个1,上一个元素是2,不等于,所以流动下去
//当前元素是第二个1,上一个元素也是1,等于,所以重复,不走这里
downstream.accept(lastSeen = t);
}
}
};
} else {
//如果上一个节点没有 DISTINCT 也没有 SORTED 标志,那么走这里。
return new Sink.ChainedReference<T, T>(sink) {
Set<T> seen;

@Override
public void begin(long size) {
//初始化 hashSet
seen = new HashSet<>();
downstream.begin(-1);
}

@Override
public void end() {
seen = null;
downstream.end();
}

@Override
public void accept(T t) {
// 这里的处理就是创建一个hashSet然后判断这个元素是否在hashSet中存在。
if (!seen.contains(t)) {
//如果不存在就不重复,放入hashSet
seen.add(t);
//继续执行
downstream.accept(t);
}
}
};
}
}
};
}

看一下执行的时序图。

stream16

sorted

sorted作用是按照给定的方法进行排序。其实底层就是调用的List.sort方法或Arrays.sort方法。

如果不传排序方法,将按照自然排序的方法来排序。

看一下应用层代码。

  • 输入 1 3 3 5 4
  • 输出 1 3 3 4 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//首先初始化输入列表
List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<String> list3 = new ArrayList<>();
List<String> list4 = new ArrayList<>();
list1.add("1");
list2.add("3");
list3.add("3");
list4.add("5");
list4.add("4");
List<List<String>> list = new ArrayList<>();
list.add(list1);
list.add(list2);
list.add(list3);
list.add(list4);

//开始执行操作
List<String> listT = list.stream().flatMap(Collection::stream).sorted(String::compareTo).collect(Collectors.toList());

return listT;

接下来看源码。有两个方法,一个是传入给定的排序方法,一个是不传。他们都会通过SortedOps.makeRef创建OfRef对象,这个是StateFulOp的子类。

1
2
3
4
5
6
7
8
9
@Override
public final Stream<P_OUT> sorted(Comparator<? super P_OUT> comparator) {
return SortedOps.makeRef(this, comparator);
}

@Override
public final Stream<P_OUT> sorted() {
return SortedOps.makeRef(this);
}

看一下SortedOps,就是直接创建OfRef对象。

1
2
3
4
5
6
7
8
static <T> Stream<T> makeRef(AbstractPipeline<?, T, ?> upstream) {
return new OfRef<>(upstream);
}

static <T> Stream<T> makeRef(AbstractPipeline<?, T, ?> upstream,
Comparator<? super T> comparator) {
return new OfRef<>(upstream, comparator);
}

看这个类。有两个属性。最终根据是否有SIZED标志位,来决定使用array处理排序,还是array list处理排序。

  • isNaturalSort 是否自然有序
  • comparator 排序比较方法
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
private static final class OfRef<T> extends ReferencePipeline.StatefulOp<T, T> {
/**
* Comparator used for sorting
*/
private final boolean isNaturalSort;
private final Comparator<? super T> comparator;

/**
* Sort using natural order of {@literal <T>} which must be
* {@code Comparable}.
*/
OfRef(AbstractPipeline<?, T, ?> upstream) {
super(upstream, StreamShape.REFERENCE,
StreamOpFlag.IS_ORDERED | StreamOpFlag.IS_SORTED);

// 设置为自然有序
this.isNaturalSort = true;
// Will throw CCE when we try to sort if T is not Comparable
//设置排序方法为自然排序,如果是个不可排序的类型,将抛出异常
@SuppressWarnings("unchecked")
Comparator<? super T> comp = (Comparator<? super T>) Comparator.naturalOrder();
this.comparator = comp;
}

/**
* Sort using the provided comparator.
*
* @param comparator The comparator to be used to evaluate ordering.
*/
OfRef(AbstractPipeline<?, T, ?> upstream, Comparator<? super T> comparator) {
super(upstream, StreamShape.REFERENCE,
StreamOpFlag.IS_ORDERED | StreamOpFlag.NOT_SORTED);
// 非自然排序
this.isNaturalSort = false;
// 将传入的排序方法赋值给排序方法
this.comparator = Objects.requireNonNull(comparator);
}

@Override
public Sink<T> opWrapSink(int flags, Sink<T> sink) {
Objects.requireNonNull(sink);

// If the input is already naturally sorted and this operation
// also naturally sorted then this is a no-op
// 如果已经是有序的并且是自然排序的,就直接返回sink节点
if (StreamOpFlag.SORTED.isKnown(flags) && isNaturalSort)
return sink;
else if (StreamOpFlag.SIZED.isKnown(flags))
// 如果标志位有 StreamOpFlag.SIZED,返回 SizedRefSortingSink 对象
return new SizedRefSortingSink<>(sink, comparator);
else
// 如果没有,返回 RefSortingSink 对象
return new RefSortingSink<>(sink, comparator);
}

看一下array流的sink节点。
有两个属性

  • array 数据
  • offset 大小
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
59
60
61
62
63
64
65
66
67
68
69
70
private static final class SizedRefSortingSink<T> extends AbstractRefSortingSink<T> {
private T[] array;
private int offset;

SizedRefSortingSink(Sink<? super T> sink, Comparator<? super T> comparator) {
// 调用父类的构造器,创建一个 sink 节点
super(sink, comparator);
}

@Override
@SuppressWarnings("unchecked")
public void begin(long size) {
// 在流开始的时候处理 如果流太大,就抛出异常,因为这里只处理有限流
if (size >= Nodes.MAX_ARRAY_SIZE)
throw new IllegalArgumentException(Nodes.BAD_SIZE);

// 创建一个Object的数组。
array = (T[]) new Object[(int) size];
}

@Override
public void end() {
// 当流结束的时候,就是所有元素都流动完成了
// 使用给定的比较器对数组进行排序
Arrays.sort(array, 0, offset, comparator);
// 这个时候流就是有序的流了,接下来在顺着donstream流动
downstream.begin(offset);
// 如果cancellationWasRequested == false
if (!cancellationWasRequested) {
// 每个元素调用 downstream 进行流动处理
for (int i = 0; i < offset; i++)
downstream.accept(array[i]);
}
else {
// 如果cancellationWasRequested == true
// 先调用 downstream.cancellationRequested() 返回 false
// 再执行元素调用 downstream 处理,
for (int i = 0; i < offset && !downstream.cancellationRequested(); i++)
downstream.accept(array[i]);
}
downstream.end();
array = null;
}

@Override
public void accept(T t) {
// 当流元素经过的时候,存储到数组中,且offset++
array[offset++] = t;
}
}

// 父类 包含一个比较器,继承自 Sink.ChainedReference
private static abstract class AbstractRefSortingSink<T> extends Sink.ChainedReference<T, T> {
protected final Comparator<? super T> comparator;
// @@@ could be a lazy final value, if/when support is added
protected boolean cancellationWasRequested;

AbstractRefSortingSink(Sink<? super T> downstream, Comparator<? super T> comparator) {
// 调用父类的构造方法
super(downstream);
// 比较器赋值
this.comparator = comparator;
}

@Override
public final boolean cancellationRequested() {
cancellationWasRequested = true;
return false;
}
}

看一下array list的sink节点

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
private static final class RefSortingSink<T> extends AbstractRefSortingSink<T> {
private ArrayList<T> list;

RefSortingSink(Sink<? super T> sink, Comparator<? super T> comparator) {
// 调用父类的构造方法
super(sink, comparator);
}

@Override
public void begin(long size) {
// 如果流太大就抛出异常
if (size >= Nodes.MAX_ARRAY_SIZE)
throw new IllegalArgumentException(Nodes.BAD_SIZE);

// 使用 array list存储数据
list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
}

@Override
public void end() {
//对array listp排序
list.sort(comparator);
//往下流动
downstream.begin(list.size());
// 如果cancellationWasRequested == false
if (!cancellationWasRequested) {
//循环每个元素进行流动
list.forEach(downstream::accept);
}
else {
// 如果cancellationWasRequested == true
// 先调用 downstream.cancellationRequested() 返回 false
// 再执行元素调用 downstream 处理,
for (T t : list) {
if (downstream.cancellationRequested()) break;
downstream.accept(t);
}
}
downstream.end();
list = null;
}

@Override
public void accept(T t) {
// 把数据存入 array list
list.add(t);
}
}

最后看一下时序图。

steam17

limit

limit的作用是选取多少个元素。常常用在截取流的一部分。

还是以上面的排序代码为例,排序后只取前3个元素,就可以使用limit(3).

1
2
3

List<String> listT = list.stream().flatMap(Collection::stream).sorted(String::compareTo).limit(3).collect(Collectors.toList());

简单看一下源码

1
2
3
4
5
6
7
8
@Override
public final Stream<P_OUT> limit(long maxSize) {
// 如果元素个数小于0,抛出异常
if (maxSize < 0)
throw new IllegalArgumentException(Long.toString(maxSize));
// 通过 SliceOps 创建一个 有状态节点
return SliceOps.makeRef(this, 0, maxSize);
}

看一下创建的节点。这个节点里面包含了一个sink节点。

sink节点中有两个属性

  • n 也就是skip 跳过哪些,这里是0
  • m 也就是limit 取哪些,我们这里是3

首先在初始化的时候限制了接下里节点初始化的大小。

真正处理的时候,如果有跳过的,先进行跳过,没有跳过的,就进行流动,当流动的元素数量达到limit个数量以后,不再流动。

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
59
60
61
62
63
64
65
66
public static <T> Stream<T> makeRef(AbstractPipeline<?, T, ?> upstream,
long skip, long limit) {
// 这里 skip 是0,检查如果小于0就抛出异常
if (skip < 0)
throw new IllegalArgumentException("Skip must be non-negative: " + skip);

// 返回有状态节点
return new ReferencePipeline.StatefulOp<T, T>(upstream, StreamShape.REFERENCE,
flags(limit)) {
@Override
Sink<T> opWrapSink(int flags, Sink<T> sink) {
// 返回sink节点
return new Sink.ChainedReference<T, T>(sink) {
//两个属性
long n = skip;
long m = limit >= 0 ? limit : Long.MAX_VALUE;

@Override
public void begin(long size) {
// 流的初始化 计算大小
// 这里size是流的大小,我们这里是5,skip是0,m是3 calcSize 返回的是 3
// 代表经过limit以后元素只有3个了,所以接下来节点的初始化只需要按3的大小来初始化就行了
downstream.begin(calcSize(size, skip, m));
}

@Override
public void accept(T t) {
// 如果跳过0个
if (n == 0) {
// 要取的元素 > 0个
if (m > 0) {
// 按照顺序把每个元素流动
// m--,当3个元素流动完成以后,m == 0,就不再流动了。
m--;
downstream.accept(t);
}
}
else {
// 如果有要跳过的元素,n--,一直跳过,当全部跳过以后 n == 0 ,就不再跳过了
n--;
}
}

@Override
public boolean cancellationRequested() {
return m == 0 || downstream.cancellationRequested();
}
};
}
};
}


private static int flags(long limit) {
// 返回 NOT_SIZED 标志位 和 IS_SHORT_CIRCUIT 标志位
return StreamOpFlag.NOT_SIZED | ((limit != -1) ? StreamOpFlag.IS_SHORT_CIRCUIT : 0);
}


// 计算大小
private static long calcSize(long size, long skip, long limit) {
// size >=0就 先取 size - skip 和 limit中最小的
// 我们这里 5-0 = 5,和3比,最小的是3,这里的意思就是如果流的大小比limit小,就返回全部流
// 然后 -1 和 5比,取大的,返回5
return size >= 0 ? Math.max(-1, Math.min(size - skip, limit)) : -1;
}

skip

skip就是跳过多少个元素。可以和limit结合起来一起截取流。

还是以上面的代码为例.skip 1 , limit 3就代表取第2个元素到第4个元素。

1
List<String> listT = list.stream().flatMap(Collection::stream).sorted(String::compareTo).skip(1).limit(3).collect(Collectors.toList());

简单看一下源码。如果skip小于0就抛出异常,如果==0就直接返回,相当于不跳过。

创建节点的时候用的和上面limit的是一样的方法。只是参数变成了传skip,不传limit.

1
2
3
4
5
6
7
8
9
10
@Override
public final Stream<P_OUT> skip(long n) {
if (n < 0)
throw new IllegalArgumentException(Long.toString(n));
if (n == 0)
return this;
else
//和limit一样
return SliceOps.makeRef(this, n, -1);
}

总结

好了,到这里为止,就分析完了stream所有的中间操作

从源码的角度分析了他们是如何运行的,都有什么作用。

总结一下。这些操作主要分为两大类

  • 有状态
  • 无状态

作为中间操作,他们的返回类型全部是 stream。

stream

stream是java8新增的非常重要的一个特性。并且非常的常用。它实现了函数式编程。具体函数式编程的概念已经很久了,比如js中的箭头函数。java中也通过stream做出了支持。想深入理解的可以参考cmu的课程15-150或者stanford的CS 95SI

它可以帮助我们方便的处理很多东西。处理分为两种,中间态和结果态。

streamImg

下面是一些中间态操作。他们位于链式操作的中间,当调用他们的时候并没有真正执行。只有当调用结果态的方法的时候才会真正的执行操作,也就是所谓的延迟执行

方法名 说明
map 循环。可以简单理解为foreach
flatMap 将二维数据展开成一维
filter 过滤数据
distinct 去重
sorted 排序
limit 限制只取n个元素
skip 跳过n个元素

stream的初始化

看一个例子,假设我们有一个需求。输出大于5的所有数。

  • 期望输入:2,5,7,1,3,2,8
  • 期望输出:7,8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//首先初始化输入列表
List<String> list = new ArrayList<>();
list.add("2");
list.add("5");
list.add("7");
list.add("1");
list.add("3");
list.add("2");
list.add("8");

//开始执行操作
List<Integer> list2 = list.stream().map(Integer::valueOf).filter(x -> {
return x > 5;
}).collect(Collectors.toList());

//输出
System.out.println(list2);

接下来看一下stream是如何执行的。下面是stream的一个类图。可以看到初始化需要使用到一个接口和6个类。主要分为三大类。

stream8.png

类介绍

第一类是ArrayList类和ArrayListSpliterator类。这两个类是核心类。毕竟我们输入类型是ArrayList,这个就不用说了。

主要在于ArrayListSpliterator这个类,这个类是ArrayList的一个内部类。最主要的操作方法和数据都在里面。看一下几个属性

  • list 我们要进行stream操作的list
  • fence 大小
  • expectedModCount 期望的处理数量
  • index

最主要的循环处理方法同样在这个类里面。处理逻辑全部在forEachRemaining这个方法中。

第二大类是StreamSupport流的支持类,算是一个单独的类,提供了对stream的一些操作方法,比如初始化一个stream。

第三大类是AbstractPipeline抽象类为主的3个类,还有两个是继承自AbstractPipelineReferencePipeline类,主要负责处理引用类的流。和继承ReferencePipelineHead类,实现了双向链表的头节点。他们的主要功能就是构造为流的双向链表数据结构。

stream9.png

执行流程介绍

这几个类的执行时序图如下。

stream7.png

可以清晰的看到,通过Collection类的stream方法调用到了ArrayList的方法然后调用到了ArrayListSpliterator的方法,来初始化了ArrayListSpliterator对象,并存储到流中。

接下来Collection类将初始化好的ArrayListSpliterator对象传递给了StreamSupport类用来初始化stream。

StreamSupport将会创建一个双向链表的头节点。并将ArrayListSpliterator对象放入头节点。初始化以后的流如下图所示:

stream2.png

流介绍

流分为两种,顺序流和并行流。

顺序流

顺序流顾名思义就是按照顺序执行。可以直接的类比为for循环。如下图,如果1,2,3三个元素,进入流以后,依然是1,2,3三个元素。

stream10.png

并行流

并行流是充分的利用现代多核计算机的性能而出的。它可以把流分散到各个进程/线程中去执行。来达到并行执行的效果。如下图,1,2,3三个元素,可能会进入2个流中。

stream11.png

源码分析

list的stream方法调用的是Collection类的stream方法。所以首先来看这个方法,该方法返回一个顺序流,顺序流中包含了list中的所有元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//该方法返回一个顺序流,顺序流中包含了list中的所有元素。
default Stream<E> stream() {
//调用了StreamSupport.stream方法,传入了一个分割迭代器,第二个参数false代表是顺序流,true是平行流。
return StreamSupport.stream(spliterator(), false);
}

//生成一个分割迭代器,该方法是ArrayList类中的方法。
@Override
public Spliterator<E> spliterator() {
//返回一个ArrayListSpliterator类的实例。
//实例中包含4个属性
//list = list
//index = 0
//fence = -1
//expectedModCount = 0
return new ArrayListSpliterator<>(this, 0, -1, 0);
}

接下来来到StreamSupport类的stream方法。通过spliterator来创建一个顺序流。只有当结果态操作开始后,spliterator才会真正开始运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 通过spliterator来创建一个顺序流或者并行流,如果parallel=1就是并行流
* 只有当结果态操作开始后,spliterator才会真正开始运行
* spliterator需要具有不可变性,并发性或延迟绑定性。
* 当前的spliterator就是上面生成的ArrayListSpliterator对象。且包含上面说的4个属性。
**/
public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
//参数校验
Objects.requireNonNull(spliterator);
//返回一个ReferencePipeline.Head类型的头节点
return new ReferencePipeline.Head<>(spliterator,
StreamOpFlag.fromCharacteristics(spliterator),
parallel);
}

在生成Head对象的时候,第二个参数调用了StreamOpFlag类的fromCharacteristics方法。来看一下这个方法。
可以看到有一个判断是否是自然排序的,如果不是自然排序就会走到if里面,也就是不会将spliterator标记为已排序状态。
最终返回结果为80

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
//将spliterator的characteristic bit转换为stream flags
//当前的spliterator就是上面生成的 ArrayListSpliterator 对象。且包含上面说的4个属性。
static int fromCharacteristics(Spliterator<?> spliterator) {
//调用spliterator对象的characteristics方法。具体看下面 返回的characteristics = 16464
int characteristics = spliterator.characteristics();
// 16464和4做按位与,即两个都是1才是1,100 0000 0101 0000 & 100 = 000 0000 0000 0000 = 0说明不满足第一个条件,触发短路,走else
if ((characteristics & Spliterator.SORTED) != 0 && spliterator.getComparator() != null) {
// Do not propagate the SORTED characteristic if it does not correspond
// to a natural sort order
// 如果不是自然排序的,则不传播有序状态。
return characteristics & SPLITERATOR_CHARACTERISTICS_MASK & ~Spliterator.SORTED;
}
else {
//将16464和85做按位与,100 0000 0101 0000 & 0101 0101 = 000 0000 0101 0000 = 80
//返回80
return characteristics & SPLITERATOR_CHARACTERISTICS_MASK;
}
}

//ArrayListSpliterator类的characteristics方法。
//三个数做按位或,即有1就是1,结果是 100 0000 0101 0000 = 16464
public int characteristics() {
//Spliterator.ORDERED = 16 = 1 0000
//Spliterator.SIZED = 64 = 100 0000
//Spliterator.SUBSIZED = 16384 = 100 0000 0000 0000
return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
}

接下来看Head类,它是ReferencePipeline类的一个内部类。主要作用是头节点的一些属性和操作。包括生成头节点等。

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

static class Head<E_IN, E_OUT> extends ReferencePipeline<E_IN, E_OUT> {
//这个是双向链表的头节点。也就是把最先创建的初始流作为头节点。
//三个参数,第一个参数source = ArrayListSpliterator对象
//第二个参数是流的标志,80
//第三个参数是否并行流 0
Head(Spliterator<?> source,
int sourceFlags, boolean parallel) {
//调用了父类的构造函数,就是ReferencePipeline的构造方法。在下面。
super(source, sourceFlags, parallel);
}
}


//ReferencePipeline的构造方法。参数同上,不再描述。
ReferencePipeline(Spliterator<?> source,
int sourceFlags, boolean parallel) {
//再次调用了父类的构造方法。ReferencePipeline的父类是AbstractPipeline类,放在下面了。
super(source, sourceFlags, parallel);
}

//AbstractPipeline类的构造方法。构造一个stream的头节点。
AbstractPipeline(Spliterator<?> source,
int sourceFlags, boolean parallel) {
//头节点的头指针为空。
this.previousStage = null;
//数据 = ArrayListSpliterator对象
this.sourceSpliterator = source;
//头节点的数据 指向自己。
this.sourceStage = this;
//标志位 = 80 & 85 = 80 二进制 0101 0000 & 0101 0101 = 0101 0000 = 80
this.sourceOrOpFlags = sourceFlags & StreamOpFlag.STREAM_MASK;
// The following is an optimization of:
// StreamOpFlag.combineOpFlags(sourceOrOpFlags, StreamOpFlag.INITIAL_OPS_VALUE);
//sourceOrOpFlags << 1 = 160 左移一位就是 * 2
//~160 = -161 按位去反
// -161 & 255 = 95
// combinedFlags = 95
this.combinedFlags = (~(sourceOrOpFlags << 1)) & StreamOpFlag.INITIAL_OPS_VALUE;
// 双向链表深度 = 0 ,因为现在只有头节点。
this.depth = 0;
// 是否并行流 = 0 不是
this.parallel = parallel;
}

list.stream()函数的源码就完了。主要调用了构造函数,构造了一个含有list数据的头节点,头指针指向空,next指针指向自己。完成了流的初始化。当前数据结构如下。

stream2.png

stream的中间态

中间态的具体源码和流程在后面介绍,这里只介绍中间态的作用。

中间态的主要作用是构建双向链表的中间节点。一个操作对应一个节点。比如map,就会创建一个节点。其中pre指针指向前一个节点也就是头节点。而头节点的next指针指向map节点。

filter操作的时候同样创建一个节点,pre指针指向上一个操作也就是map节点。map节点的next指针指向filter节点。

每个中间态节点中都存储了操作,也就是中间态的时候传入的函数。而数据则全部在头节点中。

比如下面这样:

strem

每个中间态节点其实又分成两种

  • 有状态节点
  • 无状态节点

类图如下:

stream12

stream的结果态

结果态的具体源码和流程在后面介绍,这里只介绍结果态的作用。

结果态的主要作用有三个

  1. 构造结果态节点
  2. 构造sink链表
  3. 执行流。

先说第一个,结果态节点是ReduceOp对象。这个结果态节点中包含了一个makeSink方法,用来构建结果态的sink节点。结果态的sink节点是一个ReducingSink对象。

第二个,当结果态节点和结果态的sink节点构造完成以后。接下来会根据之前构建好的双向链表来生成对应的sink链表。

一开始的双向链表我们知道是这样的

strem

而sink链表则是反过来了,根据双向链表从后向前通过pre指针不断向前,把每个节点包裹在sink节点中并通过downstream指针来指向下个节点。这里因为在第一步的时候把源数据取出来了,所以sink中不包含头节点。创建完后如图所示:

strem

第三步才是真正的执行流。根据sink链表来执行,每次把元素传递给第一个sink也就是map操作,当第一个sink节点处理完以后通过downstream流动到下一个sink节点执行。不断通过downsteram流动,直到最后到结果态的sink执行完以后。再次把第二个元素进行流动执行。直至所有元素执行完毕。

总结

因为是通过自顶向下的方式来了解stream。所以这里主要介绍了stream的执行流程和初始化的源码分析。中间态和结果态的源码分析放在了后面。

在执行流程中可以看到。首先创建了一个双向链表,然后在根据双向链表创建了sink链表。最后通过sink链表进行执行流的操作。也可以看出来确实是流动,传播,充满了stream的味道。

从这里也能看出来为何一开始是双向链表而不是单向的,因为要通过pre指针构造sink链表。

但是这里就有一个问题了,为什么不直接用一开始的双向链表,而要在创建一个sink链表呢?

我个人觉得有几个原因:

  1. 因为当前处于结果态节点,想从头流动执行,需要当前指针先指向头节点,所以必须遍历一遍。
  2. 重新构建一个纯净的sink链表,来达到不变性的性质。保持之前的数据和节点等不可变。
  3. 双向链表只负责存储数据和操作。真正的执行通过sink链表来执行,达到单一职责分层清晰
  4. 其中或许还有并行流并发的问题。

JAVA-LocalDateTime时间格式化,转换时间戳和源码分析

LocalDateTime

LocalDateTime作为java8新加的时间类型,也是后面开发中常用的时间类型。

作为时间类型,最关注的点无非是这几个

  • 获取当前时间
  • 获取指定时间
  • 时间格式化
  • 时间转时间戳
  • 时间戳转时间
  • 时间比较
  • 时间加减

这些点通过LocalDateTime来操作,都会比较简单

获取当前时间

只需要now一下就可以获取到当前的时间,还是很方便的。

1
LocalDateTime time = LocalDateTime.now();

再看一下之前的Date

1
Date date = new Date();

获取指定时间

这个有比较多的方式

  • 通过原来的datedateTime类型来生成
  • 通过传年月日时分秒生成
1
LocalDateTime time = LocalDateTime.of(2022,11,30,6,6,6);

原来Date类的方式。比较奇怪,他的年份会+1900,所以2022年就得是122,月份也会+1,所以11月就是10.但是这个方法呢后面会被删除,已经被标记为弃用了,使用Calendar代替。

1
Date date = new Date(122,10,30,6,6,6);

看一下Calendar的使用。这个年份就正常了,是2022,但是月份还是会+1.

1
2
Calendar calendar = Calendar.getInstance();
calendar.set(2022,10,30,6,6,6);

时间格式化

时间格式化都是通过format函数,需要传一个DateTimeFormatter对象进去,我们可以通过DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")来生成自己想要的格式。

DateTimeFormatter类里面也有一些定义好的格式可以直接用,除了下面列出的还有一些其他的,感兴趣可以看一下,不过我觉得都没啥用。

  • ISO_DATE_TIME 2011-12-03T10:15:30
  • ISO_OFFSET_DATE_TIME 2011-12-03T10:15:30+01:00
  • ISO_LOCAL_DATE_TIME 2011-12-03T10:15:30
1
time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

看一下Date的格式化。这个需要借用SimpleDateFormat类来完成格式化。

1
2
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
format.format(date);

时间转时间戳

时间转时间戳分为两种,一种是当你已经有一个LocalDateTime类型的时间了,需要转换成秒或者毫秒的时间戳。

时间转换秒级时间戳

只需要直接用toEpochSecond方法就可以了。

1
2
LocalDateTime time = LocalDateTime.now();
time.toEpochSecond(ZoneOffset.ofHours(8));

Date类型没有办法直接获取秒级时间戳,只能获取毫秒级再转秒。

时间转换毫秒级时间戳

转换毫秒需要先转换成instant对象,然后才能转换成毫秒级时间戳。

1
2
LocalDateTime time = LocalDateTime.now();
time.toInstant(ZoneOffset.ofHours(8)).toEpochMilli();

Date获取毫秒就很简单了。

1
2
Date date = new Date();
date.getTime();

字符串转换成时间戳

时间转时间戳分为两种,除了上面的,还有一种是有一个格式化好的字符串,比如2022-12-18 10:00:00这种,但是这个是字符串并不是时间类型。所以要先转换成LocalDateTime类型,然后就可以转换成时间戳了。

其实就是上面格式化的一种反向操作。使用parse方法就可以了。

1
2
LocalDateTime.parse("2022-12-18 10:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
LocalDateTime.parse("2022-12-18", DateTimeFormatter.ofPattern("yyyy-MM-dd"));

Date类型的字符串转时间戳也是通过SimpleDateFormat类来完成。

1
2
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = format.parse("2022-12-18 10:00:00")

时间戳转时间

那如果我们现在转换成时间戳以后又想转换成时间呢?也可以通过instant对象来做到。

毫秒时间戳转时间

Instant.ofEpochSecond(1671365543834)是将一个毫秒时间戳转换成一个instant对象。在通过ofInstant方法就可以将instant对象转换成LocalDateTime对象了。

1
LocalDateTime.ofInstant(Instant.ofEpochSecond(1671365543834), ZoneOffset.ofHours(8));

Date

1
Date date = new Date(1669759566000L);

秒时间戳转时间

Instant.ofEpochMilli(1671365543)是将一个秒时间戳转换成instant对象。和上面的区别就是使用的是ofEpochMilli方法。

1
LocalDateTime.ofInstant(Instant.ofEpochMilli(1671365543), ZoneOffset.ofHours(8));

Date类不支持秒,只能把秒转成毫秒在转时间戳。

时间比较

通过compareTo方法可以进行时间的一个比较大小。如果大于会返回1,小于返回-1.

1
2
LocalDateTime time = LocalDateTime.now();
time.compareTo(LocalDateTime.now());

Date也是通过compareTo方法进行比较

1
2
Date date = new Date(1669759566000L);
date.compareTo(new Date());

时间加减

如果加上几天,就是plusDays。加几个小时就是plusHours。当然也可以使用plus

1
2
3
4
LocalDateTime time = LocalDateTime.now();
time.plusDays(1);
time.plusHours(1);
time.plus(Period.ofDays(1));

如果减去几天就是minusDays.减去几个小时就是minusHours。当然也可以使用minus

1
2
3
4
LocalDateTime time = LocalDateTime.now();
time.minusDays(1);
time.minusHours(1);
time.minus(Period.ofDays(1));

Date类不支持时间加减,只能通过Calendar类实现。

1
2
3
4
5
6
Date date = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, 1);
//减去
calendar.add(Calendar.DAY_OF_MONTH, -1);

时间格式在入参出参中的使用

入参的时候需要通过JsonFormat注解来指定需要的是字符串类型和对应的时间格式。

1
2
3
4
5
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd")
private LocalDate date;

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
private LocalDateTime time;

出参的时候就很简单了,因为只是返回了一个字符串。

1
private String time;

格式化时间源码分析

格式化的时候这两个年是不一样的,具体的可以看一下源码。我们来找一下。

首先点进去是LocalDateTime这个类里面

1
2
3
4
5
6
7
@Override  // override for Javadoc and performance
public String format(DateTimeFormatter formatter) {
//判断参数是否空
Objects.requireNonNull(formatter, "formatter");
//真正的执行格式化
return formatter.format(this);
}

接下来点进去,看一下怎么执行的,可以看到又调用了formatTo这个函数,说明主要的格式化代码都在这里面。

1
2
3
4
5
6
7
8
public String format(TemporalAccessor temporal) {
//创建了一个32长度的字符串构建器
StringBuilder buf = new StringBuilder(32);
//格式化主要代码
formatTo(temporal, buf);
//转成字符串
return buf.toString();
}

看一下formatTo函数,可以发现主要是调用printerParser这个对象的format方法,那我们这个对象是哪来的呢,是在一开始指定格式化类型的时候来的。不同的格式化类型对应不同的解析器,也就会执行不同的format方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void formatTo(TemporalAccessor temporal, Appendable appendable) {
//判断参数,这里不用管
Objects.requireNonNull(temporal, "temporal");
Objects.requireNonNull(appendable, "appendable");
try {
//创建一个DateTimePrintContext对象
DateTimePrintContext context = new DateTimePrintContext(temporal, this);
//判断,显然我们之前传过来的就是一个StringBuilder
if (appendable instanceof StringBuilder) {
//主要看这个怎么处理 这里有个 printerParser 对象,这个对象是怎么来的呢,其实是上面DateTimeFormatter.ofPattern的时候给创建的。
printerParser.format(context, (StringBuilder) appendable);
} else {
//这里其实就是如果传的不是个StringBuilder,就在创建一个然后执行
// buffer output to avoid writing to appendable in case of error
StringBuilder buf = new StringBuilder(32);
printerParser.format(context, buf);
appendable.append(buf);
}
} catch (IOException ex) {
throw new DateTimeException(ex.getMessage(), ex);
}
}

接下来我们看一下ofPattern这个方法里面是怎样的吧。这里是创建了一个 时间格式化的建造者,然后把我们这个字符串添加进去了。

1
2
3
4
//这里的字符串就是我们传的 yyyy-MM-dd HH:mm:ss
public static DateTimeFormatter ofPattern(String pattern) {
return new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter();
}

看一下appendPattern里面是怎么把字符串加进去的。

1
2
3
4
5
6
7
public DateTimeFormatterBuilder appendPattern(String pattern) {
//忽略
Objects.requireNonNull(pattern, "pattern");
//主要的解析逻辑
parsePattern(pattern);
return this;
}

继续追踪到parsePattern方法里面。这个方法代码比较多,这里只关注我们想知道的。其余的有兴趣的可以看一下。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
private void parsePattern(String pattern) {
//这里给字符串做循环,注意 pattern = yyyy-MM-dd HH:mm:ss 这个字符串。
for (int pos = 0; pos < pattern.length(); pos++) {
//取出字符 比如第一个就是 y 对应的ASCII码就是121
char cur = pattern.charAt(pos);
//这里就是判断是否是大小写字母了,也就是A-Z或者a-z
if ((cur >= 'A' && cur <= 'Z') || (cur >= 'a' && cur <= 'z')) {
//初始化变量 start = 0 pos = 1
int start = pos++;
//这里做一个循环,目的其实就是找出相同的字符有几个,比如y有4个,pos就会变成4
for ( ; pos < pattern.length() && pattern.charAt(pos) == cur; pos++); // short loop
//这里就是算出具体的数量 4 - 0 = 4
int count = pos - start;
// padding 这里忽略 我们这里面没有这个字符
if (cur == 'p') {
int pad = 0;
if (pos < pattern.length()) {
cur = pattern.charAt(pos);
if ((cur >= 'A' && cur <= 'Z') || (cur >= 'a' && cur <= 'z')) {
pad = count;
start = pos++;
for ( ; pos < pattern.length() && pattern.charAt(pos) == cur; pos++); // short loop
count = pos - start;
}
}
if (pad == 0) {
throw new IllegalArgumentException(
"Pad letter 'p' must be followed by valid pad pattern: " + pattern);
}
padNext(pad); // pad and continue parsing
}
//接下来是主要逻辑。
// main rules
//从hashMap里面取出对应的值,这个map放在下面了。y取出来就是 YEAR_OF_ERA
TemporalField field = FIELD_MAP.get(cur);
//判断map里面取出来的是否为空,如果不为空就直接解析,如果为空就接着往下走,看是不是 zvZOXxWwY 这几个,如果都不是就会报错了
if (field != null) {
//我们y是能取出来的,直接解析这里 cur = y, count = 4, field = YEAR_OF_ERA
parseField(cur, count, field);
} else if (cur == 'z') {
if (count > 4) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
} else if (count == 4) {
appendZoneText(TextStyle.FULL);
} else {
appendZoneText(TextStyle.SHORT);
}
} else if (cur == 'V') {
if (count != 2) {
throw new IllegalArgumentException("Pattern letter count must be 2: " + cur);
}
appendZoneId();
} else if (cur == 'Z') {
if (count < 4) {
appendOffset("+HHMM", "+0000");
} else if (count == 4) {
appendLocalizedOffset(TextStyle.FULL);
} else if (count == 5) {
appendOffset("+HH:MM:ss","Z");
} else {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
} else if (cur == 'O') {
if (count == 1) {
appendLocalizedOffset(TextStyle.SHORT);
} else if (count == 4) {
appendLocalizedOffset(TextStyle.FULL);
} else {
throw new IllegalArgumentException("Pattern letter count must be 1 or 4: " + cur);
}
} else if (cur == 'X') {
if (count > 5) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
appendOffset(OffsetIdPrinterParser.PATTERNS[count + (count == 1 ? 0 : 1)], "Z");
} else if (cur == 'x') {
if (count > 5) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
String zero = (count == 1 ? "+00" : (count % 2 == 0 ? "+0000" : "+00:00"));
appendOffset(OffsetIdPrinterParser.PATTERNS[count + (count == 1 ? 0 : 1)], zero);
} else if (cur == 'W') {
// Fields defined by Locale
if (count > 1) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
appendInternal(new WeekBasedFieldPrinterParser(cur, count));
} else if (cur == 'w') {
// Fields defined by Locale
if (count > 2) {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
appendInternal(new WeekBasedFieldPrinterParser(cur, count));
} else if (cur == 'Y') {
// Fields defined by Locale
appendInternal(new WeekBasedFieldPrinterParser(cur, count));
} else {
throw new IllegalArgumentException("Unknown pattern letter: " + cur);
}
pos--;

} else if (cur == '\'') {
// parse literals
int start = pos++;
for ( ; pos < pattern.length(); pos++) {
if (pattern.charAt(pos) == '\'') {
if (pos + 1 < pattern.length() && pattern.charAt(pos + 1) == '\'') {
pos++;
} else {
break; // end of literal
}
}
}
if (pos >= pattern.length()) {
throw new IllegalArgumentException("Pattern ends with an incomplete string literal: " + pattern);
}
String str = pattern.substring(start + 1, pos);
if (str.length() == 0) {
appendLiteral('\'');
} else {
appendLiteral(str.replace("''", "'"));
}

} else if (cur == '[') {
optionalStart();

} else if (cur == ']') {
if (active.parent == null) {
throw new IllegalArgumentException("Pattern invalid as it contains ] without previous [");
}
optionalEnd();

} else if (cur == '{' || cur == '}' || cur == '#') {
throw new IllegalArgumentException("Pattern includes reserved character: '" + cur + "'");
} else {
// - : 这两个符号就会走到这里了
appendLiteral(cur);
}
}
}

看一下通过不同的key取值的map

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
//时代
FIELD_MAP.put('G', ChronoField.ERA); // SDF, LDML (different to both for 1/2 chars)
//这个时代的年份,也就是我们常用的年份yyyy
FIELD_MAP.put('y', ChronoField.YEAR_OF_ERA); // SDF, LDML
//单纯的年份
FIELD_MAP.put('u', ChronoField.YEAR); // LDML (different in SDF)
FIELD_MAP.put('Q', IsoFields.QUARTER_OF_YEAR); // LDML (removed quarter from 310)
FIELD_MAP.put('q', IsoFields.QUARTER_OF_YEAR); // LDML (stand-alone)
//一年里面的月份,也是我们常用的月份 MM
FIELD_MAP.put('M', ChronoField.MONTH_OF_YEAR); // SDF, LDML
FIELD_MAP.put('L', ChronoField.MONTH_OF_YEAR); // SDF, LDML (stand-alone)
//一年里面的天,我们基本不用这个作为日子
FIELD_MAP.put('D', ChronoField.DAY_OF_YEAR); // SDF, LDML
//一个月里面的天,我们常用这个获取多少号
FIELD_MAP.put('d', ChronoField.DAY_OF_MONTH); // SDF, LDML
FIELD_MAP.put('F', ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH); // SDF, LDML
FIELD_MAP.put('E', ChronoField.DAY_OF_WEEK); // SDF, LDML (different to both for 1/2 chars)
FIELD_MAP.put('c', ChronoField.DAY_OF_WEEK); // LDML (stand-alone)
FIELD_MAP.put('e', ChronoField.DAY_OF_WEEK); // LDML (needs localized week number)
FIELD_MAP.put('a', ChronoField.AMPM_OF_DAY); // SDF, LDML
//一天里面的小时,常用的小时 HH
FIELD_MAP.put('H', ChronoField.HOUR_OF_DAY); // SDF, LDML
FIELD_MAP.put('k', ChronoField.CLOCK_HOUR_OF_DAY); // SDF, LDML
FIELD_MAP.put('K', ChronoField.HOUR_OF_AMPM); // SDF, LDML
FIELD_MAP.put('h', ChronoField.CLOCK_HOUR_OF_AMPM); // SDF, LDML
//一个小时里面的分钟,常用的分钟 mm
FIELD_MAP.put('m', ChronoField.MINUTE_OF_HOUR); // SDF, LDML
//一分钟里面的秒数,常用的秒数 ss
FIELD_MAP.put('s', ChronoField.SECOND_OF_MINUTE); // SDF, LDML
//这个大S基本不用,这是秒里面的纳秒
FIELD_MAP.put('S', ChronoField.NANO_OF_SECOND); // LDML (SDF uses milli-of-second number)
FIELD_MAP.put('A', ChronoField.MILLI_OF_DAY); // LDML
FIELD_MAP.put('n', ChronoField.NANO_OF_SECOND); // 310 (proposed for LDML)
FIELD_MAP.put('N', ChronoField.NANO_OF_DAY); // 310 (proposed for LDML)

继续深入,直接解析y的方法parseField。可以看到这个是根据我们格式化的字母执行不同的代码,比如u,y都执行到一个代码块。4个y走到了appendValue方法里面。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
private void parseField(char cur, int count, TemporalField field) {
boolean standalone = false;
switch (cur) {
case 'u':
case 'y':
//判断数量
if (count == 2) {
//yy走这里
appendValueReduced(field, 2, 2, ReducedPrinterParser.BASE_DATE);
} else if (count < 4) {
//y or yyy走这里
appendValue(field, count, 19, SignStyle.NORMAL);
} else {
// yyyy走这里 field = YEAR_OF_ERA count = 4
appendValue(field, count, 19, SignStyle.EXCEEDS_PAD);
}
break;
case 'c':
if (count == 2) {
throw new IllegalArgumentException("Invalid pattern \"cc\"");
}
/*fallthrough*/
case 'L':
case 'q':
standalone = true;
/*fallthrough*/
case 'M':
case 'Q':
case 'E':
case 'e':
switch (count) {
case 1:
case 2:
//两个MM输出月份走到这里
if (cur == 'c' || cur == 'e') {
appendInternal(new WeekBasedFieldPrinterParser(cur, count));
} else if (cur == 'E') {
appendText(field, TextStyle.SHORT);
} else {
if (count == 1) {
appendValue(field);
} else {
//经过判断走到这里
appendValue(field, 2);
}
}
break;
case 3:
appendText(field, standalone ? TextStyle.SHORT_STANDALONE : TextStyle.SHORT);
break;
case 4:
appendText(field, standalone ? TextStyle.FULL_STANDALONE : TextStyle.FULL);
break;
case 5:
appendText(field, standalone ? TextStyle.NARROW_STANDALONE : TextStyle.NARROW);
break;
default:
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'a':
if (count == 1) {
appendText(field, TextStyle.SHORT);
} else {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'G':
switch (count) {
case 1:
case 2:
case 3:
appendText(field, TextStyle.SHORT);
break;
case 4:
appendText(field, TextStyle.FULL);
break;
case 5:
appendText(field, TextStyle.NARROW);
break;
default:
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'S':
appendFraction(NANO_OF_SECOND, count, count, false);
break;
case 'F':
if (count == 1) {
appendValue(field);
} else {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'd':
case 'h':
case 'H':
case 'k':
case 'K':
case 'm':
case 's':
if (count == 1) {
appendValue(field);
} else if (count == 2) {
//可以看到dd HH mm ss也是走到这里,最终也是通过NumberPrinterParser这个对象来格式化
appendValue(field, count);
} else {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
case 'D':
if (count == 1) {
appendValue(field);
} else if (count <= 3) {
appendValue(field, count);
} else {
throw new IllegalArgumentException("Too many pattern letters: " + cur);
}
break;
default:
if (count == 1) {
appendValue(field);
} else {
appendValue(field, count);
}
break;
}
}

看一下appendValue方法。field = YEAR_OF_ERA,minWidth = 4, maxWidth = 19, signStyle = SignStyle.EXCEEDS_PAD。前面是一些判断,重点是创建了一个NumberPrinterParser的对象。最后转换的时候其实就是通过这个对象来转换的。

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 DateTimeFormatterBuilder appendValue(
TemporalField field, int minWidth, int maxWidth, SignStyle signStyle) {
//这里不执行 忽略
if (minWidth == maxWidth && signStyle == SignStyle.NOT_NEGATIVE) {
return appendValue(field, maxWidth);
}
//参数校验
Objects.requireNonNull(field, "field");
Objects.requireNonNull(signStyle, "signStyle");
//一些校验规则
if (minWidth < 1 || minWidth > 19) {
throw new IllegalArgumentException("The minimum width must be from 1 to 19 inclusive but was " + minWidth);
}
if (maxWidth < 1 || maxWidth > 19) {
throw new IllegalArgumentException("The maximum width must be from 1 to 19 inclusive but was " + maxWidth);
}
if (maxWidth < minWidth) {
throw new IllegalArgumentException("The maximum width must exceed or equal the minimum width but " +
maxWidth + " < " + minWidth);
}
//重点是这里,创建了一个 NumberPrinterParser的对象,把参数传进去了。
NumberPrinterParser pp = new NumberPrinterParser(field, minWidth, maxWidth, signStyle);
appendValue(pp);
return this;
}

看一下NumberPrinterParser类。还记得最开始格式化的时候那一段代码printerParser.format(context, (StringBuilder) appendable);吗,实际调用的就是这里。?

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
59
60
61
62
63
64

//构造方法赋值
NumberPrinterParser(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle) {
// validated by caller
this.field = field;
this.minWidth = minWidth;
this.maxWidth = maxWidth;
this.signStyle = signStyle;
this.subsequentWidth = 0;
}

//格式化方法
@Override
public boolean format(DateTimePrintContext context, StringBuilder buf) {
//从context上下文中获取字段 field = YEAR_OF_ERA context实际包含了真正的时间 2022-12-01T00:00:00
Long valueLong = context.getValue(field);
if (valueLong == null) {
return false;
}
//获取到以后 value = 2022
long value = getValue(context, valueLong);
DecimalStyle decimalStyle = context.getDecimalStyle();
String str = (value == Long.MIN_VALUE ? "9223372036854775808" : Long.toString(Math.abs(value)));
if (str.length() > maxWidth) {
throw new DateTimeException("Field " + field +
" cannot be printed as the value " + value +
" exceeds the maximum print width of " + maxWidth);
}
//转换一个格式类型
str = decimalStyle.convertNumberToI18N(str);

//这些条件都不满足,忽略
if (value >= 0) {
switch (signStyle) {
case EXCEEDS_PAD:
if (minWidth < 19 && value >= EXCEED_POINTS[minWidth]) {
buf.append(decimalStyle.getPositiveSign());
}
break;
case ALWAYS:
buf.append(decimalStyle.getPositiveSign());
break;
}
} else {
switch (signStyle) {
case NORMAL:
case EXCEEDS_PAD:
case ALWAYS:
buf.append(decimalStyle.getNegativeSign());
break;
case NOT_NEGATIVE:
throw new DateTimeException("Field " + field +
" cannot be printed as the value " + value +
" cannot be negative according to the SignStyle");
}
}
//填充0 也就是yyyy minWidth = 4就会填充0 MM minWidth = 2如果 1月就会填充01,一个M就不会走到循环填充0
for (int i = 0; i < minWidth - str.length(); i++) {
buf.append(decimalStyle.getZeroDigit());
}
//输出到buf中
buf.append(str);
return true;
}

可以看到上面的代码,但是NumberPrinterParser其实只是解析了yMdHms这些格式的。也可以再看一下M的确认一下。

首先是appendValue这个方法。大差不差,除了传到解析器的参数不一样,没啥区别,其实dd这些也都一样。

1
2
3
4
5
6
7
8
9
10
11
public DateTimeFormatterBuilder appendValue(TemporalField field, int width) {
//参数校验
Objects.requireNonNull(field, "field");
if (width < 1 || width > 19) {
throw new IllegalArgumentException("The width must be from 1 to 19 inclusive but was " + width);
}
//可以发现MM也是用的yyyy这个解析器格式化的,但是后面三个参数不一样
NumberPrinterParser pp = new NumberPrinterParser(field, width, width, SignStyle.NOT_NEGATIVE);
appendValue(pp);
return this;
}

那我们-,:这些格式化符号的输出呢?是通过另外一个解析器,它先是取到char类型的一个字符来判断的时候会走到else里面然后走appendLiteral(cur);这个方法。看一下这个方法里面。这里可以看到主要使用的是 CharLiteralPrinterParser 这个解析器。

1
2
3
4
5
public DateTimeFormatterBuilder appendLiteral(char literal) {
//这里可以看到主要使用的是 CharLiteralPrinterParser 这个解析器
appendInternal(new CharLiteralPrinterParser(literal));
return this;
}

看一下 CharLiteralPrinterParser 这个解析器

1
2
3
4
5
6
7
8
9
10
11
//构造方法
CharLiteralPrinterParser(char literal) {
this.literal = literal;
}

@Override
public boolean format(DateTimePrintContext context, StringBuilder buf) {
//简单粗暴 直接把 - : 这种符号添加到字符串里面
buf.append(literal);
return true;
}

接下来看一下为啥我们刚才上面说的,y代表 YEAR_OF_ERA,为啥就能从2022-12-01里面取到2022呢?这个可以看到我们NumberPrinterParser这个解析器里面主要调用了一个context.getValue(field)方法。

主要是temporal.getLong(field)方法,其实temporal就是我们的日期时间,在我们一开始创建上下文的时候过来的。回忆一下上面的创建。这里的temporal可以再往上一层传过来的,传的其实就是LocalDateTime的对象

new DateTimePrintContext(temporal, this)

1
2
3
4
5
6
7
8
9
10
11
Long getValue(TemporalField field) {
try {
//主要是这里,其实temporal就是我们的日期时间,在我们一开始创建上下文的时候过来的。
return temporal.getLong(field);
} catch (DateTimeException ex) {
if (optional > 0) {
return null;
}
throw ex;
}
}

所以我们再看一下getLong方法。可以看到有一个类型判断,yMdHms这几个类型就会走到if里面,如果是时间的 Hms这几个调用time.getLong方法,yMd日期的调用日期的getLong方法。Y的话就会走到 getFrom 这个方法。而且是通过field调用。

1
2
3
4
5
6
7
8
9
10
11
@Override
public long getLong(TemporalField field) {
//类型判断 yMdHms这几个走的这里面
if (field instanceof ChronoField) {
ChronoField f = (ChronoField) field;
//如果是时间的 Hms这几个调用time.getLong方法,yMd日期的调用日期的getLong方法
return (f.isTimeBased() ? time.getLong(field) : date.getLong(field));
}
//Y走这个方法
return field.getFrom(this);
}

看一下getFrom方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public long getFrom(TemporalAccessor temporal) {
if (rangeUnit == WEEKS) { // day-of-week
return localizedDayOfWeek(temporal);
} else if (rangeUnit == MONTHS) { // week-of-month
return localizedWeekOfMonth(temporal);
} else if (rangeUnit == YEARS) { // week-of-year
return localizedWeekOfYear(temporal);
} else if (rangeUnit == WEEK_BASED_YEARS) {
return localizedWeekOfWeekBasedYear(temporal);
} else if (rangeUnit == FOREVER) {
// YYYY 大写的Y走的是这里
return localizedWeekBasedYear(temporal);
} else {
throw new IllegalStateException("unreachable, rangeUnit: " + rangeUnit + ", this: " + this);
}
}

如果大写的Y格式化就会走下面的函数,主要就是取出年份以后计算周数,如果周数=0就认为是上一年的,年份-1,如果周数大于等于下一年的周数就年份+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
private int localizedWeekBasedYear(TemporalAccessor temporal) {
//获取到这周的第几天 第5天
int dow = localizedDayOfWeek(temporal);
//获取日期中的年份 2021
int year = temporal.get(YEAR);
//获取今年的第几天 2021-12-30 是 364天
int doy = temporal.get(DAY_OF_YEAR);
//这周开始的偏移量 5
int offset = startOfWeekOffset(doy, dow);
//今年的第几周 53周
int week = computeWeek(offset, doy);
//如果这周是0周,就是上一年的,年份就-1
if (week == 0) {
// Day is in end of week of previous year; return the previous year
return year - 1;
} else {
//如果接近年底,使用更高精度的逻辑
//检查 如果年份的日期包含在下一年的部分的星期里面了
// If getting close to end of year, use higher precision logic
// Check if date of year is in partial week associated with next year
//获取一年里面的天数 对象里面包含 最小1天 - 最大365天
ValueRange dayRange = temporal.range(DAY_OF_YEAR);
//获取到年份的长度,也就是365
int yearLen = (int)dayRange.getMaximum();
//下一年的周数 根据下面的计算公式得出 (7 + 5 + 366 - 1) / 7 = 53
//这里为啥是366呢,因为yearLen是今年的天数也就是365 + 1,其实也就是到下一年去了。为的是计算下一年的第一周
int newYearWeek = computeWeek(offset, yearLen + weekDef.getMinimalDaysInFirstWeek());
//比较如果今年的这周大于等于下一年的周 就年份 +1 所以这里格式化就会出错了。
if (week >= newYearWeek) {
return year + 1;
}
}
return year;
}

那么是怎么计算今年的第几周的呢,看一下computeWeek方法。其实就是一个计算公式。

1
2
3
4
5
//offset = 5 , day = 今年的第几天 364 天
private int computeWeek(int offset, int day) {
//计算公式 ( 7 + 5 + (364 - 1)) / 7
return ((7 + offset + (day - 1)) / 7);
}

还有一个问题,就是我们用到了一个周的偏移量,这个偏移量怎么计算的呢,看一下这个方法startOfWeekOffset。以2021-12-30为例,day = 364,dow = 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private int startOfWeekOffset(int day, int dow) {
// offset of first day corresponding to the day of week in first 7 days (zero origin)
//算出上一周 (364 - 5) % 7 = 2
int weekStart = Math.floorMod(day - dow, 7);
// offset = -2
int offset = -weekStart;
//这里 2 + 1 > 1会走进去
if (weekStart + 1 > weekDef.getMinimalDaysInFirstWeek()) {
// The previous week has the minimum days in the current month to be a 'week'
//这里 7 - 2 = 5 返回的就是5
offset = 7 - weekStart;
}
return offset;
}

上面看完了大写的Y,再来看一下小写的y。走的getLong方法。

日期的getLong方法。经过判断后主要看get0这个方法。可以看到这个命名就很随意了。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public long getLong(TemporalField field) {
//这个判断也会走进来
if (field instanceof ChronoField) {
//这两个判断忽略
if (field == EPOCH_DAY) {
return toEpochDay();
}
if (field == PROLEPTIC_MONTH) {
return getProlepticMonth();
}
//走到这里
return get0(field);
}
return field.getFrom(this);
}

看一下日期的get0方法。可以发现了,这里主要处理了这几种类型。我们常用的

  • y也就是YEAR_OF_ERA 处理很简单,判断了一下year >= 1就返回 year。
  • M也就是MONTH_OF_YEAR 处理很简单,返回日期的month.
  • d也就是DAY_OF_MONTH 返回日期的day.

从这里也可以看出我们格式化成YEARERA作为年其实也是可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int get0(TemporalField field) {
switch ((ChronoField) field) {
case DAY_OF_WEEK: return getDayOfWeek().getValue();
case ALIGNED_DAY_OF_WEEK_IN_MONTH: return ((day - 1) % 7) + 1;
case ALIGNED_DAY_OF_WEEK_IN_YEAR: return ((getDayOfYear() - 1) % 7) + 1;
case DAY_OF_MONTH: return day;
case DAY_OF_YEAR: return getDayOfYear();
case EPOCH_DAY: throw new UnsupportedTemporalTypeException("Invalid field 'EpochDay' for get() method, use getLong() instead");
case ALIGNED_WEEK_OF_MONTH: return ((day - 1) / 7) + 1;
case ALIGNED_WEEK_OF_YEAR: return ((getDayOfYear() - 1) / 7) + 1;
case MONTH_OF_YEAR: return month;
case PROLEPTIC_MONTH: throw new UnsupportedTemporalTypeException("Invalid field 'ProlepticMonth' for get() method, use getLong() instead");
case YEAR_OF_ERA: return (year >= 1 ? year : 1 - year);
case YEAR: return year;
case ERA: return (year >= 1 ? 1 : 0);
}
throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
}

看完了日期的处理再看一下时间的吧,其实大同小异了。

时间的getLong方法。同样的经过判断走到get0里面,注意这是时间的getLongget0

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public long getLong(TemporalField field) {
if (field instanceof ChronoField) {
if (field == NANO_OF_DAY) {
return toNanoOfDay();
}
if (field == MICRO_OF_DAY) {
return toNanoOfDay() / 1000;
}
return get0(field);
}
return field.getFrom(this);
}

时间的get0方法。处理的就是这些类型了。主要看我们关注的几个

  • H 也就是 HOUR_OF_DAY, 直接返回时间的 hour
  • m 也就是MINUTE_OF_HOUR,直接返回时间的 minute
  • s 也就是 SECOND_OF_MINUTE, 直接返回时间的 second
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private int get0(TemporalField field) {
switch ((ChronoField) field) {
case NANO_OF_SECOND: return nano;
case NANO_OF_DAY: throw new UnsupportedTemporalTypeException("Invalid field 'NanoOfDay' for get() method, use getLong() instead");
case MICRO_OF_SECOND: return nano / 1000;
case MICRO_OF_DAY: throw new UnsupportedTemporalTypeException("Invalid field 'MicroOfDay' for get() method, use getLong() instead");
case MILLI_OF_SECOND: return nano / 1000_000;
case MILLI_OF_DAY: return (int) (toNanoOfDay() / 1000_000);
case SECOND_OF_MINUTE: return second;
case SECOND_OF_DAY: return toSecondOfDay();
case MINUTE_OF_HOUR: return minute;
case MINUTE_OF_DAY: return hour * 60 + minute;
case HOUR_OF_AMPM: return hour % 12;
case CLOCK_HOUR_OF_AMPM: int ham = hour % 12; return (ham % 12 == 0 ? 12 : ham);
case HOUR_OF_DAY: return hour;
case CLOCK_HOUR_OF_DAY: return (hour == 0 ? 24 : hour);
case AMPM_OF_DAY: return hour / 12;
}
throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
}

总结

好了,到这里我们知道了时间格式的各种使用方法和格式化的源码。

对于不同格式化的区别。总结一下。

  • y 处理简单,只是判断了year > 1 就返回了year。
  • Y 处理较复杂,还判断了周,根据情况对年份+1和-1。某些年份的某些日期会有坑。一定要注意!!!
  • Md Hms处理非常简单,直接返回了日期时间上面对应的数。
  • -: 一些特殊字符,格式化的时候是直接增加到字符串里面的。

下面总结一下源码对应文件和方法的追踪链。感兴趣的可以自己在多翻翻源码。

ofPattern指定格式的调用链

  • DateTimeFormatter.java -> public static DateTimeFormatter ofPattern(String pattern)
    • DateTimeFormatterBuilder.java -> public DateTimeFormatterBuilder appendPattern(String pattern)
    • DateTimeFormatterBuilder.java -> private void parsePattern(String pattern)
    • DateTimeFormatterBuilder.java -> private void parseField(char cur, int count, TemporalField field)
    • DateTimeFormatterBuilder.java -> public DateTimeFormatterBuilder appendValue(TemporalField field, int width)
    • 在这里创建的解析器
    • DateTimeFormatterBuilder.java -> static class NumberPrinterParser implements DateTimePrinterParser
    • DateTimeFormatterBuilder.java -> static final class CharLiteralPrinterParser implements DateTimePrinterParser

format方法调用链

  • LocalDateTime.java -> public String format(DateTimeFormatter formatter)
    • DateTimeFormatter.java -> public String format(TemporalAccessor temporal)
    • DateTimeFormatter.java -> public void formatTo(TemporalAccessor temporal, Appendable appendable)
      • 接下来根据不同的处理解析器进行处理,主要有两个解析器
      • DateTimeFormatterBuilder.java -> static class NumberPrinterParser implements DateTimePrinterParser
      • DateTimeFormatterBuilder.java -> static final class CharLiteralPrinterParser implements DateTimePrinterParser
        • DateTimePrintContext.java -> Long getValue(TemporalField field)
          • LocalDateTime.java -> public long getLong(TemporalField field)
            • 这里日期调日期的 LocalDate.java -> public long getLong(TemporalField field)
            • LocalDate.java -> private int get0(TemporalField field)
            • 时间调时间的 LocalTime.java -> public long getLong(TemporalField field)
            • LocalTime.java -> private int get0(TemporalField field)

spring

AOP切面

spring

IOC控制反转

这里先说一下IOC,再说IOCspring框架中的使用。

IOC的概念

IOC这个缩写有很多意思,比如

  • 智慧城市智能运行中心(IOC)
  • 奥林匹克运动的领导机构

但是呢,我们这里说的是面向对象编程中的一种设计原则。他的全称是Inversion Of Control即控制反转。这里有两个单词控制反转。这两个单词单独拿出来会发现,都缺少主语。比如

  • 谁控制了谁?
  • 什么东西发生反转了呢?

控制

这里说一下第一个问题,谁控制了谁呢?看下面的代码

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

public class main{

public static void main(String[] args) {
Person person = new Person();
person.setName("tony");
System.out.println(person.getName());
}
}

class Person{
private String name;

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}

很显然,在这里,Person类的person对象的一切都控制在main函数里面。main函数创建它,使用它,销毁它。所以在当前上下文中,main控制了person。

反转

上面的写法main函数控制了person对象,这是一种紧耦合的关系,如果person发生了改变,我们就需要改变main。反转是控制权的反转。现在person的控制权在main这里,我们将它反转一下。不在用main控制它。那么我们加一个简单工厂看一下呢。

1
2
3
4
5
6
7
8
9
10
11
12
13

class Factory{
public static Person createPerson() {
return new Person();
}
}

public static void main(String[] args) {
Person person = Factory.createPerson();
person.setName("tony");
System.out.println(person.getName());
}

这里可以看到person的控制权转交给了Factory工厂,而main只有使用权了。当然了作为示例代码,这里的控制只做了创建。

现在,由main控制person改为了Factory控制person。如果person发生了改变,我们只需要改变Factory,而不需要动业务逻辑。

当然了,这种程度的解耦依然不够,因为main还是和Factory有耦合关系,他还控制了Factory。我们可以扩大这个简单工厂,扩大后的简单工厂就不再是工厂了,而叫做容器。我们把所有的控制权都交给容器,让容器控制所有的类,对象。而在使用的时候我们去告诉容器我们要使用哪个对象,让容器给我们提供就可以了。

DI

依赖注入 DI全称Dependency Injection就可以实现我们告诉容器我们需要的对象,然后容器把对象注入给我们的功能。

具体的依赖注入实现方式每个语言,每个框架可能都不一样,这里以spring为例

1
2
3
4
5
6
7
8
9
10
11
public class main{

@Autowired
private Person person;

public static void main(String[] args) {
person.setName("tony");
System.out.println(person.getName());
}
}

可以看到,我们通过@Autowired注解告诉容器,我们需要一个Person类型的对象,然后让容器把这个对象注入到我们的person属性中。这里仅做示例使用,具体情况请以实际开发中为准,实际开发中应使用接口类型。

容器中的实现方式就类似刚才工厂中的,假设你需要Person类型的对象,就new Person返回,当然了,要更加复杂,比如可以根据名称来反射创建对象,可以更好的管理对象的生命周期,可以实现单例对象等等。

spring中的IOC

Spring中的IOC实现也是通过容器,但是怎么把类注入到容器中呢,也就是怎么告诉容器,你需要实例化哪些类呢?有两种方式,一种是XML配置方式,一种是注解方式

XML配置方式

先创建一个XML文件,比如:

touch bean.xml

接下来编辑它。

vim bean.xml

无参数构造方式

重点在这个配置,通过bean这个标签来告诉容器我要把哪些类注册到容器中,其中id就是注册后的唯一标识,我们获取的时候也可以通过指定id来从容器中获取对象。而class是告诉容器,我们具体要注入的类的路径。但是这时候没有指定参数,也就是说类似于Person person = new Person()这样的注册,需要有无参构造器。

1
2
3
4
5
6
7
8
<?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="person" class="com.test.java.Person"></bean>

</beans>

使用的时候需要借助于Spring提供的容器获取,可以看一下效果是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
public class main{

public static void main(String[] args) {
//这里先加载我们的XML配置文件
BeanFactory ac = new ClassPathXmlApplicationContext("bean.xml");
//通过容器获取一个对象,第一个参数也就是配置的id,第二个就是类
Person person = ac.getBean("person", Person.class);
person.setName("tony");
System.out.println(person.getName());
}
}

构造器构造参数方式

同样在上面的XML文件中进行修改,但这次我们需要加上参数,并且告诉容器是通过构造器来构造参数的,而不是set的方式。

我们只需要在原来的bean标签中,加入我们要传给构造器的参数就可以了。

使用constructor-arg标签,name就是参数名,value是对应的值。

1
2
3
4
5
6
7
8
9
10
<?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="person" class="com.test.java.Person">
<constructor-arg name="name" value="tony"></constructor-arg>
</bean>

</beans>

我们还需要改造一下原来的Person类,增加一个构造函数.

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

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

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}

看一下现在的main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
public class main{

public static void main(String[] args) {
//这里先加载我们的XML配置文件
BeanFactory ac = new ClassPathXmlApplicationContext("bean.xml");
//通过容器获取一个对象,第一个参数也就是配置的id,第二个就是类
Person person = ac.getBean("person", Person.class);
// 这里不需要setName了,因为通过构造器注入参数了
// person.setName("tony");
System.out.println(person.getName());
}
}

但是如果我们需要构造一个其他类的对象作为参数该怎么配置呢,毕竟我们总不能在value上面写new Class()吧哈哈。但是我们知道一个bean就是一个对象,那我们可以传一个bean进来就可以了。

来看一下配置方式.不再使用value了,因为value只能传基本类型这些,而其他的对象需要使用ref来传参。ref的值就是其他beanid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="avatar" class="com.test.java.Avatar">
<constructor-arg name="url" value="http://baidu.com"></constructor-arg>
</bean>

<bean id="person" class="com.test.java.Person">
<constructor-arg name="name" value="tony"></constructor-arg>
<constructor-arg name="avatar" ref="avatar"></constructor-arg>
</bean>

</beans>

同样需要改造一下构造函数,增加一个avatar参数。

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
class Avatar{
private String url;
public Avatar(String url) {
this.url = url;
}
}

class Person{
private String name;

private Avatar avatar;

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

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

public String getAvatar() {
return this.avatar.url;
}
}

执行一下main函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class main{

public static void main(String[] args) {
//这里先加载我们的XML配置文件
BeanFactory ac = new ClassPathXmlApplicationContext("bean.xml");
//通过容器获取一个对象,第一个参数也就是配置的id,第二个就是类
Person person = ac.getBean("person", Person.class);
// 这里不需要setName了,因为通过构造器注入参数了
// person.setName("tony");
System.out.println(person.getName());
System.out.println(person.getAvatar());
}
}

set方式传参

除了通过构造器传参,我们还可以写set函数来传参,比如setNamesetAvatar

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
class Avatar{
private String url;
// public Avatar(String url) {
// this.url = url;
// }

//增加set函数
public void setUrl(String url) {
this.url = url;
}
}

class Person{
private String name;

private Avatar avatar;

// public Person(String name, Avatar avatar) {
// this.name = name;
// this.avatar = avatar;
// }

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

public void setAvatar(Avatar avatar) {
this.avatar = avatar;
}

public String getAvatar() {
return this.avatar.url;
}
}

修改XML配置文件,不在使用constructor-arg标签,而是换成property。不过除了标签名变了,其他的属性name,value,ref都是不变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

<?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="avatar" class="com.test.java.Avatar">
<property name="url" value="http://baidu.com"></property>
</bean>

<bean id="person" class="com.test.java.Person">
<property name="name" value="tony"></property>
<property name="avatar" ref="avatar"></property>
</bean>

</beans>

执行一下main函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class main{

public static void main(String[] args) {
//这里先加载我们的XML配置文件
BeanFactory ac = new ClassPathXmlApplicationContext("bean.xml");
//通过容器获取一个对象,第一个参数也就是配置的id,第二个就是类
Person person = ac.getBean("person", Person.class);
// 这里不需要setName了,因为通过构造器注入参数了
// person.setName("tony");
System.out.println(person.getName());
System.out.println(person.getAvatar());
}
}

自动注入

可以通过配置XML文件来使用自动注入,就不需要手动增加<property name="url" ref="avatar"></property><constructor-arg name="avatar" ref="avatar"></constructor-arg>的标签了,只需要配置一个属性autowire就可以了。但是这种的只适用于注入其他的bean

autowire只有两个值,一个是byName,是通过bean的名称进行注入,比如你的属性名是avatar,就会查找id=avatar这个类。还有一个是byType,是通过bean的类型进行注入,比如类型是Avatar,那么就会查找class=Avatar的bean进行注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="avatar" class="com.test.java.Avatar">
<property name="url" value="http://baidu.com"></property>
</bean>

<bean id="person" class="com.test.java.Person" autowire="byName">
<property name="name" value="tony"></property>
<!-- <property name="avatar" ref="avatar"></property> -->
</bean>

</beans>

XML读取外部配置文件

通过XML可以读取外部的配置文件,这样的话像数据库,redis连接这些就可以把host,name,password这些写到外部的配置文件中。

配置文件使用.properties后缀。比如spring.properties

增加一个配置文件spring.properties

1
spring.person.name=tony

修改XML直接从配置中读取person.name来注入。读取的时候还需要在XML中增加context命名空间。并通过context命名空间来读取配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<!-- 增加context命名空间 -->
<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/beans/spring-context.xsd">

<!-- 读取配置文件 -->
<context:property-placeholder location="classpath:spring.properties">

<bean id="avatar" class="com.test.java.Avatar">
<property name="url" value="http://baidu.com"></property>
</bean>

<bean id="person" class="com.test.java.Person" autowire="byName">
<!-- 读取配置文件的值 -->
<property name="name" value="${spring.person.name}"></property>
<!-- <property name="avatar" ref="avatar"></property> -->
</bean>

</beans>

注解方式

注解方式要比XML方式简单的多,其中原理就是不再手动配置,而是通过注解告诉Spring我是一个bean。快来注册我吧。

主要是这4个注解告诉Spring

  • @Component 单纯的说我是一个bean
  • @Service 和上面的一样,不过一般用在service类中,更加语义化
  • @Controller 和上面的一样,一般用在controller类中
  • @Repository 我也是一个bean

接下来我们告诉Spring,你需要扫描出所有带上面注解的类,把他们注册到容器中。这一步需要修改XML文件,需要配置<context:component-scan>标签,并且通过base-package属性告诉Spring我们要扫描哪个目录

1
2
3
4
5
6
7
8
9
10
11

<?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 https://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="com.test.java"></context:component-scan>

</beans>

在类上面增加注解

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

@Component
class Avatar{
private String url;
// public Avatar(String url) {
// this.url = url;
// }

//增加set函数
public void setUrl(String url) {
this.url = url;
}
}

@Component
class Person{
private String name;

private Avatar avatar;

// public Person(String name, Avatar avatar) {
// this.name = name;
// this.avatar = avatar;
// }

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

public void setAvatar(Avatar avatar) {
this.avatar = avatar;
}

public String getAvatar() {
return this.avatar.url;
}
}

还可以自己指定扫描哪些注解,通过context:include-filter标签来指定。type类型写注解,expression指定扫描哪个注解。把标签放在context:component-scan这个里面就可以了。还需要在context:component-scan标签中指定,禁用默认的扫描方式。指定use-default-filters的属性为false.

1
2
3
4
5
6
7
8
9
10
11
<?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 https://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="com.test.java" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component" />
</context:component-scan>

</beans>

还可以排除一些注解不进行扫描,通过context:exclude-filter标签来指定。type同样写注解,expression指定排除的注解。

1
2
3
4
5
6
7
8
9
10
11
<?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 https://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="com.test.java" use-default-filters="false">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Component" />
</context:component-scan>

</beans>

把类注册到容器中以后,我们还需要在使用的时候告诉容器,我们需要从容器中获取这个类,有5个注解

  • @Autowired Spring提供的,基于类型注入的,可以放在setter方法上
  • @Qualifier Spring提供的,基于名称注入的,一般和@Autowired配合使用来通过value参数指定名称
  • @Resource Java提供的,可以基于类型或名称注入的,可以通过name参数来指定名称,可以放在setter方法上
  • @RequiredArgsConstructor lombok提供的,基于类型注入,通过增加一个构造函数来注入。
  • @Value Spring提供的,注入基本类型的注解,一般用来从配置文件取值。

@RequiredArgsConstructor是lombok提供的,兼容性较差,像写单元测试的时候就用不了,它会给你的类增加一个构造方法,而且只会给final类型的属性进行注入。

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

@Component
//增加注解
@RequiredArgsConstructor
class Person{
private String name;

//错误使用,因为没有final
// private Avatar avatar;

//正确使用,加上final
private final Avatar avatar;

// public Person(String name, Avatar avatar) {
// this.name = name;
// this.avatar = avatar;
// }

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

public void setAvatar(Avatar avatar) {
this.avatar = avatar;
}

public String getAvatar() {
return this.avatar.url;
}
}

这个时候可以编译完以后查看.class文件,看到的是这样的

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

@Component
// 注解没有了
// @RequiredArgsConstructor
class Person{
private String name;

//错误使用,因为没有final
// private Avatar avatar;

//正确使用,加上final
private final Avatar avatar;

//增加了一个构造函数
public Person(final Avatar avatar) {
this.avatar = avatar;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

public void setAvatar(Avatar avatar) {
this.avatar = avatar;
}

public String getAvatar() {
return this.avatar.url;
}
}

@Autowired是spring提供的,在spring中不管是写业务还是写单元测试都可以使用,它可以放在要注入的属性上面,也可以放在setter方法上面。使用他的时候不需要final修饰。

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

@Component
class Person{
private String name;

//在需要的属性上面增加这个注解,不需要final修饰
@Autowired
private Avatar avatar;

//错误使用,加上final
// @Autowired
// private final Avatar avatar;

// public Person(String name, Avatar avatar) {
// this.name = name;
// this.avatar = avatar;
// }

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

//也可以加在setter方法上面
@Autowired
public void setAvatar(Avatar avatar) {
this.avatar = avatar;
}

public String getAvatar() {
return this.avatar.url;
}
}

@Qualifier注解配合@Autowired使用,比如我们有一个头像的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

interface IAvatar {
String getUrl();
}

class maleAvatar implements IAvatar{
public String getUrl() {
return "male avatar";
}
}

class femaleAvatar implements IAvatar{
public String getUrl() {
return "female avatar";
}
}

这个时候我们在注入的时候如果只根据IAvatar来注入,容器就不知道我们需要哪个实现类了,所以我们需要指定类名.

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

@Component
class Person{
private String name;

//在需要的属性上面增加这个注解,不需要final修饰
@Autowired
//指定要注入的实现类
@Qualifier(value="maleAvatar")
private IAvatar avatar;

//错误使用,加上final
// @Autowired
// private final Avatar avatar;

// public Person(String name, Avatar avatar) {
// this.name = name;
// this.avatar = avatar;
// }

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

//也可以加在setter方法上面
@Autowired
@Qualifier(value="maleAvatar")
public void setAvatar(IAvatar avatar) {
this.avatar = avatar;
}

public String getAvatar() {
return this.avatar.url;
}
}

@Resource更像是上面两个的合体,并且是由java提供的。也是可以放在属性和setter上面,并且不需要final修饰。

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

@Component
class Person{
private String name;

//在需要的属性上面增加这个注解,不需要final修饰
@Resource
private Avatar avatar;

//错误使用,加上final
// @Resource
// private final Avatar avatar;

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

//也可以加在setter方法上面
@Resource
public void setAvatar(Avatar avatar) {
this.avatar = avatar;
}

public String getAvatar() {
return this.avatar.url;
}
}

同样的,如果我们有多个实现类,需要指定可以通过它的name参数来指定。比如

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

@Component
class Person{
private String name;

//在需要的属性上面增加这个注解,不需要final修饰
@Resource(name="femaleAvatar")
private IAvatar avatar;

//错误使用,加上final
// @Resource
// private final Avatar avatar;

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

//也可以加在setter方法上面
@Resource(name="femaleAvatar")
public void setAvatar(IAvatar avatar) {
this.avatar = avatar;
}

public String getAvatar() {
return this.avatar.url;
}
}

@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
26
27
28
29
30
31
32
33
34
35
36
37
38
@Component
class Person{
//直接注入tony字符串到name中
@Value("tony")
private String name;

//从配置文件中取值
//person:
// name: tony
@Value("${person.name}")
private String nameFromConfig;

//在需要的属性上面增加这个注解,不需要final修饰
@Resource(name="femaleAvatar")
private IAvatar avatar;

//错误使用,加上final
// @Resource
// private final Avatar avatar;

public void setName(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

//也可以加在setter方法上面
@Resource(name="femaleAvatar")
public void setAvatar(IAvatar avatar) {
this.avatar = avatar;
}

public String getAvatar() {
return this.avatar.url;
}
}

高等数学笔记

$y = x^(m/n) 相当于 y^n = x^m$

三角函数

  • sinx,tanx,cotx,cscx是奇函数
  • cosx,secx是偶函数
  • tanx = sinx/cosx
  • cotx = cosx/sinx
  • $cos^2x + sin^2x = 1$
  • $1 + tan^2x = sec^2x$
  • $cot^2x + 1 = csc^2x$
  • $cos^2x = (1 + cosx) / 2$
  • $sin^2x = (1 - cosx) / 2$
1
2
3
4
sin(a+b) = sina cosb + cosa sinb
cos(a+b) = cosa cosb - sina sinb
sin2x = 2sinxcosx
cos2x = cosx^2 - sinx^2

单位圆上

1
2
3
4
5
6
7
sinx = y/r
cosx = x/r
tanx = y/x

余割 cscx = r/y
正割 secx = r/x
余切 cotx = x/y

余弦定理

$$c^2 = a^2 + b^2 -2ab * cosx$$

反函数

  • $sec^-1 X = cos^-1 (1/X)$
  • $csc^-1 X = sin^-1 (1/x)$
  • $cot^-1 X = π/2 - tan^-1X$

对数

  • ln(a * b) = lna + lnb
  • ln(a / b) = lna - lnb
  • lnx^n = n * lnx
  • ln(ⁿ√x)=lnx/n
  • lne = 1
  • ln1 = 0
  • logab = logcb / logca
  • a^x = logaX = e^xlna
  • e^lnx = x
  • lne^x = x
1
2
3
4
5
6
7
8
lnx = 3t+5
e^lnx = e^3t+5
x = e^3t+5

e^2x = 10
ln e^2x = ln 10
2x = ln10
x = 1/2 * ln10

幂函数

  • a^m * a^n = a^m+n
  • a^m / a^n = a^m-n
  • (a^m)^n = a^mn
  • (a^m * a^n)^p = a^mp * b^np

求切线方程

公式 y-y0=m(x-x0),m为斜率,也就是导数。代入点到x0,y0处求方程,比如y=2^x在(0,1)点的切线方程

1
2
3
4
y = 2^x的导数为ln2 * 2^x,x = 0代入为ln2
在点(0,1)处代入 y - 1 = ln2 (x - 0)
y - 1 = ln2x - 0
y - ln2x - 1 = 0

导数

四则运算

1
2
3
4
5
d/dx (a+b) = d/dx a + d/dx b
d/dx (a*b) = d/dxa * b + a * d/dx b
d/dx (a/b) = (d/dxa * b - a * d/dx b) / b^2
d/dx (ca) = c * d/dx a, c为常数
d/dx (1/v) = -v^-2 * d/dx v

链式法则

d/dx f(g(x)) = d/dx f(g) * d/dx g(x)

隐函数微分法

对于不像y=2x这种直接的函数。比如x^2 + y^2 = 1这种函数,可以直接对每一项求导。在使用链式法则就可以得到y的导数

1
2
3
4
5
x^2 + y^2 = 1 对每一项求导后 x^2 = 2x, y^=2y , 1 = 0,因为y是函数,在对y使用链式法则,得:
2x + 2y * d/dx y = 0
2y * d/dx y = -2x
d/dx y = -2x / 2y
d/dx y = -x/y

对数微分法

常见导数

  • 常数导数为0
  • $sinx = cosx$
  • $cosx = -sinx$
  • $tanx = sec^2x$
  • $1/x = -1/x^2$
  • $x^a = a*x^a-1$
  • $a^x = lna(a^x)$
  • $lnx = 1/x$

线性近似

f(x) = f(x) + d/dx f(x) (x - x0)

当x=0时:
f(x) = f(0) + d/dx f(0) * x

lnx的线性近似,当x = 1时:
lnx = ln1 + d/dx ln1 (x - 1)
lnx = 0 + 1 * (x - 1) = x - 1

当x = 0时:

1
2
3
4
5
lnx = ln(1 + x) = 1 + x - 1 = x
sinx = 0 + cos0 * x = x
cosx = 1 + 0 * x = 1
e^x = 1 + 1 * x = 1 + x
(1 + x)^r = 1 + rx

二阶近似

f(x) = f(x) + d/dx f(x) (x - x0) + f(x)’’/2 * (x - x0)^2

当x=0时:
f(x) = f(0) + f(0)’ * x + f(0)’’/2 * x^2

1
2
3
4
5
sinx = x
cosx = 1 - 1/2 * x^2
e^x = 1 + x + 1/2 * x^2
ln(1 + x) = x - 1/2 * x^2
(1 + x)^r = 1 + rx + r(r-1)/2 * x^2

曲线构图

  • if f’ > 0, f 是递增的
  • if f’ < 0, f 是递减的
  • if f’’ > 0, f’ 是递增的
  • if f’’ < 0, f’ 是递减的

if f(x0)’ = 0, 则 x0 为临界点, y0 = f(x0) 为临界点值 。

if f(x0)’’ = 0,则 x0为 拐点。

画图

  • 描点
    • 找出不连续的点
    • 找出最远端的点
    • 找出一些简单的点
  • 求出导数为0的点
    • 标出临界点的值
  • 判断f’在每个区间的正负性
  • 判断f’’的正负性,以判断凹凸性
    • 求出f0’’,算出拐点
  • 组合所有信息

最大最小值

只需要求出临界点,最远端的点和不连续的点就可以找出最大最小值

牛顿迭代法

用来求函数f(x)在x轴上的交点x,对y点做一切线,切线交于X轴的点为X1,求出X1点,并对X1点的y点做切线交于X轴为X2点,不断重复,求出X点

Xn+1 = Xn - f(Xn)/f’(Xn)

x^2 = 5

x = 根号5

X1 = X0 - (X0^2 - 5/2X0)
X1 = X0 - 1/2 * X0 + 5/2X0
X1 = 1/2 * X0 + 5/2X0

X点的误差在
E1 = |X - X1|
E2 = |X - X2|

En = |根号5 - Xn-1|

E2 约等于 E1^2

f’不能太小 f’’不能太大并且X0要在X的附近

中值定理

(f(b) - f(a)) / (b - a) = f(c)’ 要求x在a < x < b 之间可微,在a <= x <= b之间连续

比如:一辆车从北京到上海,在路上,一定有一段时间的速度等于平均速度

如果f’ > 0 则 f 增长
如果f’ < 0 则 f 递减
如果f’ = 0 则 f 是常数

重要不等式

e^x > 1+x

e^x > 1+x+1/2*x^2

微分

y = f(x) 的微分记作 dy = f(x)’dx

下面的例子,求解出来是fx = y + dy,其实就是线性近似 fx = fa + f’(x - a), x - a其实就是dx,f’ * dx就是dy,fa就是y

1
2
3
4
5
6
7
8
9
10
11
12
13
14
例子1
求 64.1的1/3次方
令 y = x的1/3次方
dy = 1/3X^-2/3 * dx
当x=64的时候, y = 64^1/3 = 4
dy = 1/3 * 64^-2/3 * dx
= 1/3 * 1/16 * dx
= 1/48 * dx
如果x=64,求64.1,则dx = 0.1
求 64.1的1/3次方, y = 64的1/3次方,那么
64.1的1/3次方 = y + dy = 4 + (1/48 * dx)
= 4 + (1/48 * 1/10)
= 4 + 1/480
约等于 4.002

反导数(不定积分)

一阶导数微分的解就是函数,二阶导数微分的解就是一阶导数。式子 f’ = f + C

G(x) = 积分 g(x) dx, Gx 就是 gx 的反导数

积分sinx dx = -cosx,因为 -cosX的一阶导数是 sinX所以 积分sinX * dx = -cosX

不定积分的不定就是可以在后面加上一个常数C,也就是

1
Gx = 积分 sinX * dx = -cosX + C也成立

重要积分

  • x^a的不定积分 = (1/a+1 * X ^ a+1) + C 当 a 不等于 - 1时成立,因为a = -1分母为0
  • 1/X的不定积分 = (ln|X|) + C
  • sec2X 的不定积分 = tanX + C
  • 1/根号 1-X^2 的不定积分 = sin-1X + C
  • 1/1+X^2的不定积分 = tan-1X + C

积分换元法

1
2
3
4
5
6
7
8
9
10
例子1
求解 X^3 * (X^4 + 2)^5 * dx 的积分

令 u = x^4 + 2, 则 du = u' + dx = 4x^3 * dx

x^3 * (x^4 + 2)^5 * dx
= u^5 * x^3 * dx
= u^5 * 1/4 * du
= 1/24 * u^6 + C
= 1/24 * (x^4 + 2)^6 + C

提前猜测

例子2

1
2
3
4
5
6
求解 e^6x 的积分

e^6x的导数是 6*e^6x
他的导数乘以 1/6就是 e^6x
所以 积分就是 1/6 * e^6x + C

例子3

1
2
3
4
求 x * e^-x^2的积分

猜测 e^-x^2,求导 = e^-x^2 * -2x
所以 积分 = 1/2 * e^-x^2 + C

例子4

1
2
3
4
5
6
7
8
9
10
求 sinx cosx的积分

猜测 sinX^2, 求导 = 2sinxcosx
所以 积分 = 1/2sinx^2 + C

也可以猜测 cosx^2,求导 = -2sinxcosx
所以 积分 = -1/2cosx^2 + C

两个都成立,两者可以相减 1/2 sinx^2 - (-1/2 cosx^2) = 1/2 所以 两个 C 相差 1/2

高级猜测

例子4 求 (d/dx + x) * y = 0

1
2
3
4
5
6
7
8
9
10
11
12
(d/dx + x) * y = 0
dy/dx + xy = 0
dy/dx = -xy
dy = -xy * dx
dy/y = -x * dx 把y和x各放到一边
积分 dy/y = 积分 -x * dx 对两边同时积分
因为 lny的导数是1/y 所以 积分 dy/y = lny
因为 -x^2/2 的导数是 -x 所以 积分 -x * dx = -x^2/2
则 : lny = -x^2/2 + C y > 0
e^lny = e^-x^2/2 + C 对两边同时取对数
y = A * e^-x^2/2 (A = e^c)

分离变量法

1
2
3
4
5
6
7
dy/dx = f(x) * g(y) = -x * y
dy/g(y) = f(x) * dx
G(y) = 积分 dy/g(y)
F(x) = 积分 f(x) * dx
G(y) = F(x) + C
上面是隐式方程,为了变成显式方程还需要求逆
y = G^-1(F(x) + C)

定积分

几何意义是求函数曲线下的面积

  1. 划分成多个矩形 所有矩形的底边一样长,都是b/n

例子1 y = x^2 的定积分 a = 0, b = n

1
2
3
4
5
6
7
8
9
划分成多个矩形后,第一个矩形的面积 = 底 * 高 = b/n * f(x) = b/n * (b/n)^2
第二个矩形的面积 = 底 * 高 = b/n * f(x) = b/n * f(2 * b/n) = b/n * (2b/n)^2
第n个矩形的面积 = b/n * (nb/n)^2
矩形面积的和 提取公因子 (b/n)^3 * (1^2 + 2^2 + .... + n^2)
(1^2 + 2^2 + .... + n^2) 想成金字塔,其体积最小是 1/3 * n^3 ,体积最大是 1/3 * (n+1)^3
而矩形面积的和 = (b/n)^3 * (1^2 + 2^2 + .... + n^2) = b^3 * (1^2 + 2^2 + .... + n^2) / n^3
1/3 < (1^2 + 2^2 + .... + n^2) / n^3 < 1/3 * (n+1)^3 / n^3
根据夹逼定理,左边的极限是 1/3 右边 1/3 * (n+1)^3 / n^3 = 1/3 * (n+1/n)^3 = 1/3 * (1 + 1/n)^3 当n趋于无穷的极限也是1/3所以中间的极限是1/3
矩形面积的和 = b^3 * (1^2 + 2^2 + .... + n^2) / n^3 = b^3 * 1/3

定积分

  • x^2 = b^3/3
  • x = b^2/2
  • 1 = b

微积分第一基本定理

if F(x)’ = f(x) , than 从a到b f(x) dx的定积分 = F(b) - F(a) = b的积分 - a的积分

例子1 x^2

1
2
从a到b x^2 dx 的定积分 = F(b) - F(a) = b^3/3 - a^3/3
当a = 0,则 b^3/3 - 0/3 = b^3/3

运算法则

  • 积分(fx + gx) = 积分fx + 积分gx
  • 积分(c * fx) = c * 积分fx
  • 如果 a < b < c 则 a到b的积分 + b到c的积分 = a到c的积分
  • a到a的积分 = 0
  • a到b的积分 = -(b到a的积分)
  • 如果 fx <= gx,那么从a到b fx的积分 <= gx的积分 a < b

微积分第二基本定理

if f 是连续的函数,并且 G(x) = 从a到x的积分 f(t) dt, than G(x)’ = f(x)

平均公式

1/(b - a) * 从a到b的积分f(x) dx

加权平均公式

从a到b的积分 f(x) w(x) dx / 从a到b的积分 w(x) dx

圆盘法

先求一个圆盘的体积,也就是 面积 * 高 = πr^2 * dx,然后积分

壳层法

先求竖着的圆柱的体积,绕一圈在展开变成长方体,求体积就是 长 * 宽 * 高 = 圆的周长 * dx * f(x),然后积分

数值积分

三角替换

  • tanx的积分 = ln(cosx) + C
  • secx的积分 = ln(secx + tanx) + C

例题1

求secX的4次方的积分

1
2
3
4
因为 sexX^2 = 1 + tanX^2,所以secx^4 dx的积分 = (1 + tanx^2)sexX^2 dx的积分

令 u = tanx, du = secX^2 dx,则 = (1 + u^2) du的积分 = u + u^3/3 + C
= tanx + tanx^3/3 + C

例题2

1/(x^2根号下1+x^2)的积分

例题3

tan (arc cscx) = 1/ (根号x^2 - 1)

被积函数 三角替换 结果
根号下a^2 - x^2 x = acosx or y = asinx asinx or acosx
根号下a^2 + x^2 x = atanx asecx
根号下x^2 - a^2 x = asecx atanx

例题4

dx/根号x^2+4x

部分分式

如果分子项数 < 分母项数,可以用掩盖法

  1. 对分母因式分解成 x/(x+1)(x-2) 的形式
  2. 设置未知数 变成 A/x+1 和 B/x-2
  3. 掩盖法解A,B,先同乘以一个分母 比如 x+1 则变成 A + B/(x-2) * (x + 1),令x = -1则 B这项为0从而解出A,同理解出B

如果分子项数 >= 分母项数, 用直接除法变成 分子项数 < 分母项数的形式

常用积分

被积函数 结果
lnx的积分 xlnx - x + C
(lnx)^2 x(lnx)^2 - 2(xlnx - x) + C
tanx的积分 ln(cosx) + C
cotx的积分 ln(sinx) + C
secx的积分 ln(secx + tanx) + C

php实现归并排序算法

归并排序算法的复杂度是O(nlogn)。

代码如下,完整代码在github上面,只需要clone下来执行composer install然后执行 php artisan test:mergeSort 就可以看到结果了

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
/**
* 归并排序把数据逐步分解,然后对分解后的数据进行排序,最后合并到一起
*
* @return mixed
*/
public function handle()
{
$this->a = [3,70,4,38,5,6,8,4,7,10,6,10,34,4];
dump($this->a);
$a = $this->mergeSort($this->a, 0, count($this->a));
dd($a);
}

private function mergeSort($a, $lo, $hi) {
if (($hi - $lo) < 2) return [$a[$lo]];
$mi = ($lo + $hi) >> 1;
//把中点左边的进行归并
$b = $this->mergeSort($a, $lo, $mi);
dump('$b:',$b);
//把中点右边的进行归并
$c = $this->mergeSort($a, $mi, $hi);
dump('$c:',$c);
//把所有数据进行排序
return $this->merge($b, $c, $lo,$mi,$hi);
}

/**
* 假设有一个数组$a分成了两个数组[3,4] [2,8]
* 逐一比较,3and2,取出来2然后3and8取出来3然后4and8取出来4,最后取出来8
*
* @param [type] $lo
* @param [type] $mi
* @param [type] $hi
* @return void
*/
private function merge($b, $c, $lo, $mi, $hi) {
$lb = $mi - $lo; //$b数组的边界
$lc = $hi - $mi; //$c数组的边界
$res = [];
//$i表示合并后数组的下标 $ib是b数组的下标 $ic是c数组的下标
for($i = 0,$ib=0,$ic=0;$ib<$lb || $ic < $lc;){
//ib 下标没有越界 && c的数组已经空了也就是$ic >= $lc || 比较两个数组首位的大小 如果b的首元素 < c的首元素,那么取出来b的首元素
if ($ib < $lb && ( $ic >= $lc || $b[$ib] <= $c[$ic])) {
$res[$i++] = $b[$ib++];
}
//k 下标没有越界 && b的数组已经空了也就是$ib >= $lb || 如果c的首元素 < b的首元素,那么取出来c的首元素
if ($ic < $lc && ($ib >= $lb || $b[$ib] > $c[$ic])) {
$res[$i++] = $c[$ic++];
}
}
return $res;
}

归并排序原理

归并排序和快排刚好相反,是先将整个数组左右打散,然后在逐一合并进行排序,最终完成整个数组的排序,排序示意图如下:

mergesort1

首先将整个数组左右打散,变成单个元素,因为单个元素可以被认为是有序的。
对应代码

1
2
3
4
5
6
7
8
if (($hi - $lo) < 2) return [$a[$lo]];
$mi = ($lo + $hi) >> 1;
//把中点左边的进行归并
$b = $this->mergeSort($a, $lo, $mi);
dump('$b:',$b);
//把中点右边的进行归并
$c = $this->mergeSort($a, $mi, $hi);
dump('$c:',$c);

接下来对左右两个有序数组进行排序,假设有一个数组$a分成了两个数组[3,4] [2,8],逐一比较,3and2,取出来2然后3and8取出来3然后4and8取出来4,最后取出来8,对应代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$lb = $mi - $lo; //$b数组的边界
$lc = $hi - $mi; //$c数组的边界
$res = [];
//$i表示合并后数组的下标 $ib是b数组的下标 $ic是c数组的下标
for($i = 0,$ib=0,$ic=0;$ib<$lb || $ic < $lc;){
//ib 下标没有越界 && c的数组已经空了也就是$ic >= $lc || 比较两个数组首位的大小 如果b的首元素 < c的首元素,那么取出来b的首元素
if ($ib < $lb && ( $ic >= $lc || $b[$ib] <= $c[$ic])) {
$res[$i++] = $b[$ib++];
}
//k 下标没有越界 && b的数组已经空了也就是$ib >= $lb || 如果c的首元素 < b的首元素,那么取出来c的首元素
if ($ic < $lc && ($ib >= $lb || $b[$ib] > $c[$ic])) {
$res[$i++] = $c[$ic++];
}
}
return $res;

示意图如下:

mergesort2

无序数组去重算法

无序数组去重算法的复杂度是O(n2)。

代码如下,首先进行外层循环,复杂度O(n),然后查找这个元素之前的元素中有没有重复的,复杂度O(n),如果有就删除,复杂度O(1),没有就下一个元素,复杂度O(1)。加起来复杂度O(n2)。

完整代码在github上面,只需要clone下来执行composer install然后执行 php artisan test:unsortDeduplicate 就可以看到结果了

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
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$a = [4,5,4,3,8,6,6,10,34,10,4];
dump($a);
$i = 1;
$len = count($a);
dump("长度:".$len);
while ($i < $len) { //循环全部数据
//在整个数组中寻找这个值,如果找到了就删除他,如果没找到就下一个
$preIndex = $this->find($i, $a);
if ($preIndex!==false) {unset($a[$preIndex]);}
else $i++;
}
dd($a);
}

private function find($i, array $a) {
$index = 0;
//循环从0到这个下标
while ($index < $i) {
//不存在说明被删除了
if (!array_key_exists($index, $a)) {$index++;continue;}
//如果找到了返回下标
if ($a[$i] == $a[$index]) return $index;
else $index++;
}
return false;
}

有序数组去重算法

有序数组去重算法的复杂度是O(n)。

代码如下,只进行一次循环,复杂度O(n)

完整代码在github上面,只需要clone下来执行composer install然后执行 php artisan test:sortDeduplicate 就可以看到结果了

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
/**
* 因为是有序数组,为了提高去重效率,取一个元素往后一直比对,如果相邻的相等表示是重复的
* 继续往后,直到不相等,也就是遇到一个不重复的为止,将这个不重复的元素移动到该元素的下一个
* 然后用这个不重复的元素为起始,重复上述操作。直到最后
* 删除最后一个不重复元素后面所有的元素
* 因为只循环一遍,复杂度O(n)
*
* @return mixed
*/
public function handle()
{
$a = [3,4,4,4,5,6,6,8,10,10,34];
dump($a);
//分别代表第一个数据,和要比对的数据
$i = 0;$j=0;
$len = count($a);
dump("长度:".$len);
while ((++$j) < $len) { //循环全部数据
//从$i往后寻找,如果相邻的相等表示是重复的,继续往后,直到不相等,也就是遇到一个不重复的为止,将这个不重复的元素移动到该元素的下一个
if($a[$i] != $a[$j]) {
$a[++$i] = $a[$j];
}
}
//截取前面去重过的数据
$a = array_slice($a, 0, ++$i);
dd($a);
}