设计模式 - 观察者模式

设计模式 - 观察者模式


本文转载自屈定’s Blog

观察者模式

观察者模式描述的是一种一对多的关系,这里的一可能是某个状态发生变化,也可能是某一个事件产生.举个例子,针对订单付款,这一事件产生后可能需要经过很多个处理步骤,比如积分,入库,消费排行榜之类的操作,这些操作之间并没有任何关联性甚至可以并行处理,那么就可以理解为订单付款与处理步骤之间的一对多关系.
还有一个特点就是单向依赖,处理步骤对于订单付款是单向依赖的,比如有订单付款,才能有处理步骤.但是反之就不依赖,订单付款对于有没有处理步骤是不关心的,这一点在下文实战中会详细讲解.

观察者模式结构

观察者模式主要结构如下:
img

  • Subject: 负责事件产生后通知到具体观察者的角色,所谓的通知实际上是循环调用其所持有的观察者接口
  • Observer: 负责对事件的处理,该接口可以很好的做到任务分离,每一个不同的任务都是其一个实现子类,互相不关心对方,很好的描述了业务上的关系.

那么本质什么?用一个Subject来聚合所有的Observer,那么调用者只需要关心对应的Subject就可以.
为什么可以这样? 因为Observer之间没有任何关系,只是单纯的做自己要做的事情,也并不需要返回值之类的东西.

观察者模式的实战案例

如笔者一开始描述的需求,再订单付款完成之后执行一些处理步骤,具体如下:

  1. 如果是虚拟产品订单,那么就发放虚拟产品
  2. 如果是会员订单那么就开通会员
  3. 根据付款金额增加积分
  4. 如果有消费排行榜活动则更新用户金额.
  5. …..

这种是开发中很常见的付款后一些对应的操作需求,并且随着活动之类的增加后续还很容易增加其他的处理步骤需求,对于观察者模式其符合以下两点特征:

  1. 订单付款完成这一事件对应对个处理步骤,典型一对多关系
  2. 处理步骤之间并无关联性,每一个都是独立的处理

观察者模式设计

上述用观察模式可以设计出如下结构:
img

OrderPaidHandlerObserver
其是观察者需要实现的接口,主要功能是判断是不是自己可以处理,可以处理的话就处理,其子类各司其职,比如IntegralOrderService是处理积分相关的观察者,VipOrderService则是处理会员相关的Service.

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface OrderPaidHandlerObserver {
/**
* 是否支持处理该消息
* @param paidMsg 消息体
* @return true 支持
*/
boolean supportHandler(OrderPaidMsgDTO paidMsg);
/**
* 处理方法
* @param paidMsg 消息体
*/
void handler(OrderPaidMsgDTO paidMsg);
}

OrderPaidHandlerSubject
其是负责通知所有观察者的接口,实现了该接口就有了通知观察者的义务.

1
2
3
4
5
6
7
public interface OrderPaidHandlerSubject {
/**
* 提醒所有的观察者
* @param paidMsg 付款消息
*/
void notifyObservers(OrderPaidMsgDTO paidMsg);
}

OrderCompositeService
其是OrderPaidHandlerSubject的实现类,主要实现负责通知所有观察者的逻辑,所谓的通知本质上就是调用自己所持有的观察者对象,那么当订单付款事件产生后OrderCompositeService只需要调用下notifyObservers()方法就可以完成通知,完成所有的处理步骤.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OrderCompositeService implements OrderPaidHandlerSubject {

private List<OrderPaidHandlerObserver> observers;

@Override
public void notifyObservers(OrderPaidMsgDTO paidMsg) {
// 所谓的通知本质上就是调用对应观察者的方法
for (OrderPaidHandlerObserver observer : observers) {
try {
if (observer.supportHandler(paidMsg)) {
observer.handler(paidMsg);
}
} catch (Exception e) {
log.error("OrderPaidHandlerObserver fail", e);
}
}
}
...
}

使用Spring管理观察者

OrderCompositeService作为Subject来说,其持有了全部的Observer,那么如果利用IOC把Observer全部注入进该类中,那么当下次新增加一个Observer实现类时就不需要改这边的任何代码,完完全全的解耦.
思路是利用IOC管理所有的观察者,当Spring容器启动完毕后获取所有的观察者,添加到对应的observers集合中,具体做法就是让OrderCompositeService实现ApplicationListener<ContextRefreshedEvent>接口,Spring在启动完毕后会发出通知,在该接口中利用BeanFactoryUtils初始化所需要的观察者集合.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 容器初始化完毕后所执行的事件
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
initPaidObserver(event);
}
/**
* 初始化订单付款所要执行的事件
*/
private void initPaidObserver(ContextRefreshedEvent event) {
// 从Spring容器中取出所有的观察者
Map<String, OrderPaidHandlerObserver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(event.getApplicationContext(), OrderPaidHandlerObserver.class, true, false);
// 实例化观察者集合
this.observers = Collections.unmodifiableList(Lists.newArrayList(matchingBeans.values()));
if (this.observers.size() <= 0) {
throw new IllegalStateException("OrderPaidHandlerObserver not found");
} else {
log.info("initPaidObserver finish, observer is {}", matchingBeans.keySet());
}
}

这样做的好处就是把观察者的实例化与Subject解耦,对于观察者只需要知道自己一旦实现了观察者接口,那么就一定会有相应的Subject通知自己就足够了.

观察者的 “感兴趣” 粒度

在观察者模式中Observer会像Subject注册自己,那么当Subject对应多个事件时怎么处理呢?
1.Subject管理多组Observer
Subject中存放着多组Observer,当一个事件触发时只会通知其中一组.这样做法个人感觉是比较合理的.缺点是管理不方便,对于Subject来说要管理多组,对应的removeOvserver或者addObserver就会比较麻烦了,此时可以依赖IOC等工具完成这个过程.
2.Observer多角色
这种方案下,对于一个Subject他管理的只有一组Observer,但是Observer本身要承担多个责任,并且对自己不感兴趣的责任要留空方法处理.Observer可能只对一件事情感兴趣却不得不实现一堆空方法,不符合最少知道原则.Java的AWT就是这种设计.
3.使用弱类型参数
JDK自带的Observer就是类似的形式,其使用Object作为观察者参数,当接收到消息时需要用instance判断是否是自己感兴趣的事件,然后才执行逻辑,当事件很少的话这种方式是比较合适的,事件多的话则对一堆事件要分开处理,依然很麻烦.Eclipse的SWT是这种设计.

Filter 链

在写JavaWeb的时候我们经常会遇到一个概念就是 Filter 链,比如对于路由拦截,以及文件目录拦截都有一个Filter 和Filter链,那么他们底层具体到底是怎么工作的呢?

听到Filter 链这个名词我们觉得一个 Filter 链应该可以进行链式调用,来判断我们的条件是否成立,也就是类似于下面的规则:

1
filterChain.doFilter(pattern).doFilter(pattern)

这样想是没错,那每次就需要返回下一个 Filter 这么想是没错,可是我们怎么知道下一次 Filter 是谁呢?

链表! 对滴,这时候链表就起作用了,我们直接采用了 LinkedList 就能把这些 Filter 串起来。然后根据当前的 Filter 找出下一个 Filter 。

实际上在某些框架之中不是这么实现了的,但是基本的思路已经非常对了。例如这里是 SpringSecurity 中的 Filter 的简单实现:

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
import java.util.LinkedList;
import java.util.List;

public class Test1 {
interface Filter{
public void doFilter(FilterChain filterChain);
}

static class MyFilter implements Filter {
@Override
public void doFilter(FilterChain filterChain) {
System.out.println("filter invoke");
filterChain.doFilter();
}
}

interface FilterChain{
public void doFilter();
}

static class MyFilterChain implements FilterChain{
List<Filter> filters;
int pos = 0;

MyFilterChain(List<Filter> filters) {
this.filters = filters;
}

@Override
public void doFilter() {
if (pos == filters.size()) {
System.out.println("end");
return;
}
Filter filter = filters.get(pos++);
filter.doFilter(this);
}
}

public static void main(String[] args) {
LinkedList<Filter> list = new LinkedList<>();
list.add(new MyFilter());
list.add(new MyFilter());
list.add(new MyFilter());
new MyFilterChain(list).doFilter();
}
}

Java 新特性之 Stream

Stream

使用这个方法创建一个 Stream 对象。

1
new ArrayList<>().stream()

Filter

过滤器,里面传递一个函数,这个函数的返回结果如果为 true 则保留这个元素,否则的话丢弃这个元素。

1
2
3
4
stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);

Foreach

遍历,消费。

1
2
3
4
stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);

Map

这个功能也是遍历,但是他是有返回值的,而上面的 Foreach 是没有返回值的,仅仅是单纯的消费。而且 Foreach 不能够链式调用,因为没有返回值,但是 Map 没问题。

1
2
3
4
5
stringCollection
.stream()
.map(String::toUpperCase)
.sorted(Comparator.reverseOrder())
.forEach(System.out::println);

Sorted

这个方法是用来排序的,里面传递的函数就是一个比较器,也可以不传递参数,使用默认的就好。

1
2
3
4
5
stringCollection
.stream()
.sorted(( x, y)-> y.length()-x.length())
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);

Match

根据在给定的 stream 对象中是否含有指定内容返回 true 或者 false 。

具体的有:

  • allMatch
  • anyMatch
  • noneMatch
1
2
3
4
5
6
7
8
9
10
11
boolean anyStartsWithA = stringCollection
.stream()
.anyMatch((s) -> s.startsWith("a"));

boolean allStartsWithA = stringCollection
.stream()
.allMatch((s) -> s.startsWith("a"));

boolean noneStartsWithZ = stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z"));

count

计算集合中的元素的个数。

1
2
3
4
long startsWithB = stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();

reduce

这个函数就是类似于斐波那契数列,每次传递的参数是上一次的结果和从集合中取出的新元素。第一次默认取出了第一个元素和第二个元素。

简单的例子就是,第一次取出 0,1 第二次取出 第一次reduce的结果作为第一个参数,取出 2 作为第二个参数,以此类推。

1
2
3
4
5
Optional<String> reduced =
stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2);

parallelStream

并行的 steam 流,可以进行并行处理,这样会效率更高。在使用stream.foreach时这个遍历没有线程安全问题,但是使用parallelStream就会有线程安全问题,所有在parallelStream里面使用的外部变量,比如集合一定要使用线程安全集合,不然就会引发多线程安全问题。如果说需要保证安全性需要使用 reduce 和 collect,不过这个用起来超级麻烦!!!

1
long count = values.parallelStream().sorted().count();

IntStream.range(a,b)

可以直接生成 从 a 到 b 的整数这里还是遵循编程语言的大多数约定,那就是含头不含尾。

1
2
IntStream.range(0, 10)
.forEach(System.out::println);

输出的结果是

1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

new Random().ints()

获取一系列的随机值,这个接口出来的数据是连续不断的,所以需要用limit来限制一下。

1
new Random().ints().limit(10).forEach(System.out::println);

Supplier

1
2
Supplier<String> stringSupplier=String::new;
stringSupplier.get();

该接口就一个抽象方法get方法,不用传入任何参数,直接返回一个泛型T的实例.就如同无参构造一样

Consumer

1. accept方法

​ 该函数式接口的唯一的抽象方法,接收一个参数,没有返回值.

2. andThen方法

​ 在执行完调用者方法后再执行传入参数的方法.

1
2
3
4
5
6
7
8
9
10
11
12
public class ConsumerTest {
public static void main(String[] args) {
Consumer<Integer> consumer = (x) -> {
int num = x * 2;
System.out.println(num);
};
Consumer<Integer> consumer1 = (x) -> {
int num = x * 3;
System.out.println(num);
};
consumer.andThen(consumer1).accept(10);
}

先执行了 consumer.accept(10) 然后执行了 consumer1.accept(10)

ifPresent

针对一个optional 如果有值的话就执行否则不执行。

1
2
3
4
5
6
7
8
9
10
IntStream
.builder()
.add(1)
.add(3)
.add(5)
.add(7)
.add(11)
.build()
.average()
.ifPresent(System.out::println);

average 执行结果就是一个 optional

Collect

他有两种调用方式

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

<R, A> R collect(Collector<? super T, A, R> collector);

下面主要介绍一下这两种方式的使用方法:

1. 函数

第一种调用方式的接口如下

1
2
3
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
  • supplier 这个参数就是提供一个容器,可以看到最后 collect 操作的结果是一个 R 类型变量,而 supplier 接口最后需要返回的也是一个 R 类型的变量,所以说这里返回的是收集元素的容器。
  • accumulator 参数,看到这个函数的定义是传入一个 R 容器,后面则是 T 类型的元素,需要将这个 T 放到 R 容器中,即这一步是用来将元素添加到容器中的操作。
  • conbiner 这个参数是两个容器,即当出现多个容器的时候容器如何进行聚合。

一个简单的例子:

1
2
3
String concat = stringStream.collect(StringBuilder::new, StringBuilder::append,StringBuilder::append).toString();
//等价于上面,这样看起来应该更加清晰
String concat = stringStream.collect(() -> new StringBuilder(),(l, x) -> l.append(x), (r1, r2) -> r1.append(r2)).toString();

2. Collector 接口

第二种方案是更高级的用法采用了 Collector 接口:

1
<R, A> R collect(Collector<? super T, A, R> collector);

可以看到他返回的还是一个 R 类型的变量,也就是容器。

Collector接口是使得collect操作强大的终极武器,对于绝大部分操作可以分解为旗下主要步骤,提供初始容器->加入元素到容器->并发下多容器聚合->对聚合后结果进行操作

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
static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
private final Supplier<A> supplier;
private final BiConsumer<A, T> accumulator;
private final BinaryOperator<A> combiner;
private final Function<A, R> finisher;
private final Set<Characteristics> characteristics;

CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Function<A,R> finisher,
Set<Characteristics> characteristics) {
this.supplier = supplier;
this.accumulator = accumulator;
this.combiner = combiner;
this.finisher = finisher;
this.characteristics = characteristics;
}

CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Set<Characteristics> characteristics) {
this(supplier, accumulator, combiner, castingIdentity(), characteristics);
}

@Override
public BiConsumer<A, T> accumulator() {
return accumulator;
}

@Override
public Supplier<A> supplier() {
return supplier;
}

@Override
public BinaryOperator<A> combiner() {
return combiner;
}

@Override
public Function<A, R> finisher() {
return finisher;
}

@Override
public Set<Characteristics> characteristics() {
return characteristics;
}
}

可以看到我们可以直接 new CollectorImpl 然后将这些函数传入,另外还有一种简单的方式就是 使用 Collector.of()依然可以直接传入函数。和 new CollectorImpl 是等价的。

3. 工具函数

1. toList()

容器: ArrayList::new
加入容器操作: List::add
多容器合并: left.addAll(right); return left;

1
2
3
4
5
6
public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}

2.joining()

容器: StringBuilder::new
加入容器操作: StringBuilder::append
多容器合并: r1.append(r2); return r1;
聚合后的结果操作: StringBuilder::toString

1
2
3
4
5
6
public static Collector<CharSequence, ?, String> joining() {
return new CollectorImpl<CharSequence, StringBuilder, String>(
StringBuilder::new, StringBuilder::append,
(r1, r2) -> { r1.append(r2); return r1; },
StringBuilder::toString, CH_NOID);
}

3.groupingBy()

roupingBytoMap的一种高级方式,弥补了toMap对值无法提供多元化的收集操作,比如对于返回Map<T,List<E>>这样的形式toMap就不是那么顺手,那么groupingBy的重点就是对Key和Value值的处理封装.分析如下代码,其中classifier是对key值的处理,mapFactory则是指定Map的容器具体类型,downstream为对Value的收集操作.

1
2
3
4
5
6
public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T, A, D> downstream) {
.......
}

一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//原生形式
Lists.<Person>newArrayList().stream()
.collect(() -> new HashMap<Integer,List<Person>>(),
(h, x) -> {
List<Person> value = h.getOrDefault(x.getType(), Lists.newArrayList());
value.add(x);
h.put(x.getType(), value);
},
HashMap::putAll
);
//groupBy形式
Lists.<Person>newArrayList().stream()
.collect(Collectors.groupingBy(Person::getType, HashMap::new, Collectors.toList()));
//因为对值有了操作,因此我可以更加灵活的对值进行转换
Lists.<Person>newArrayList().stream()
.collect(Collectors.groupingBy(Person::getType, HashMap::new, Collectors.mapping(Person::getName,Collectors.toSet())));
// 还有一种比较简单的使用方式 只需要传递一个参数按照key来划分
Map<Integer, List<Person>> personsByAge = persons
.stream()
.collect(Collectors.groupingBy(p -> p.age));

4.reducing()

reducing是针对单个值的收集,其返回结果不是集合家族的类型,而是单一的实体类T
容器: boxSupplier(identity),这里包裹用的是一个长度为1的Object[]数组,至于原因自然是不可变类型的锅
加入容器操作: a[0] = op.apply(a[0], t)
多容器合并: a[0] = op.apply(a[0], b[0]); return a;
聚合后的结果操作: 结果自然是Object[0]所包裹的数据a -> a[0]
优化操作状态字段: CH_NOID

1
2
3
4
5
6
7
8
9
public static <T> Collector<T, ?, T>
reducing(T identity, BinaryOperator<T> op) {
return new CollectorImpl<>(
boxSupplier(identity),
(a, t) -> { a[0] = op.apply(a[0], t); },
(a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },
a -> a[0],
CH_NOID);
}

简单来说这个地方做的事情和 reduce 是一样的,第一个 id 传入的就是 reduce 的初始值,只是他把它包装成一个 长度为1的数组了。

1
2
3
4
5
6
7
8
9
10
11
//原生操作
final Integer[] integers = Lists.newArrayList(1, 2, 3, 4, 5)
.stream()
.collect(() -> new Integer[]{0}, (a, x) -> a[0] += x, (a1, a2) -> a1[0] += a2[0]);
//reducing操作
final Integer collect = Lists.newArrayList(1, 2, 3, 4, 5)
.stream()
.collect(Collectors.reducing(0, Integer::sum));
//当然Stream也提供了reduce操作
final Integer collect = Lists.newArrayList(1, 2, 3, 4, 5)
.stream().reduce(0, Integer::sum)

Java 新特性之 Time

日期类 LocalDate

能够简单的获取当天的日期,并且可以方便的对日期进行加减。

他是通过静态方法或者 from/of 等方法创建对象的。

这个类不存储时区,所以他没有时区的概念,如果需要时区的话需要使用 ZonedDateTime

这个类只能进行日期的相关操作,没有具体的时间。

下面介绍常用的几个方法

atTime

生成一个带有时间的日期,返回结果是 LocalDateTime 所以很明显这个就是带有时间的日期类。

1
2
LocalDateTime time = LocalDate.now().atTime(12, 3, 22,232);
System.out.println(time); //2019-05-10T12:03:22.000000232

compareTo()

比较两个时间返回的值是 int ,大于 0 则是比较者大,否则是被比较者大,为0相等。

format()

格式化时间,需要传递一个时间的 formatter 类型为 [DateTimeFormatter]

parse()

同上第一个参数是解析的字符串,第二个就是传入formatter

plus/minus

对日期进行加减,第一个参数传入的是 int 第二个就是单位,单位使用 ChronoUnit 枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class LocalDate1 {

public static void main(String[] args) {
LocalDate today = LocalDate.now();
// 添加时间
LocalDate i22day = today.plusDays(22);
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
// 减去时间
LocalDate s22day = today.minus(22, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);

System.out.println(today);
System.out.println(tomorrow);
System.out.println(yesterday);
System.out.println(i22day);
System.out.println(s22day);

LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY

DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.GERMAN);

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/mm/dd hh:mm:ss");
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
// System.out.println(LocalDate.parse("2014/02/01 12:03:22", dateTimeFormatter));
System.out.println(xmas); // 2014-12-24


System.out.println(LocalDate.now().atStartOfDay());

LocalDateTime time = LocalDate.now().atTime(12, 3, 22,232);
System.out.println(time);
}

时间 LocalDateTime

这个和上面类似是一个时间类,不仅仅有日期还是时间。

atZone

产生带有时区日期

format

格式化时间

of

从一个时间创建对象 of(int year, int month, int dayOfMonth, int hour, int minute, int second)

parse

解析时间 **parse**([CharSequence]text, [DateTimeFormatter]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class LocalDateTime1 {

public static void main(String[] args) {

LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);

DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek); // WEDNESDAY

Month month = sylvester.getMonth();
System.out.println(month); // DECEMBER

long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay); // 1439

Instant instant = sylvester
.atZone(ZoneId.systemDefault())
.toInstant();

Date legacyDate = Date.from(instant);
System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014


DateTimeFormatter formatter =
DateTimeFormatter
.ofPattern("MMM dd, yyyy - HH:mm");

LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = parsed.format(formatter);
System.out.println(string); // Nov 03, 2014 - 07:13
}

}

本地时间 LocalTime

这个类是上面的LocaDateTime中的 Time 部分,也就是只具有时间没有日期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class LocalDate1 {

public static void main(String[] args) {
LocalDate today = LocalDate.now();
// 添加时间
LocalDate i22day = today.plusDays(22);
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
// 减去时间
LocalDate s22day = today.minus(22, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);

System.out.println(today);
System.out.println(tomorrow);
System.out.println(yesterday);
System.out.println(i22day);
System.out.println(s22day);

LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY

DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.GERMAN);

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/mm/dd hh:mm:ss");
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
// System.out.println(LocalDate.parse("2014/02/01 12:03:22", dateTimeFormatter));
System.out.println(xmas); // 2014-12-24


System.out.println(LocalDate.now().atStartOfDay());

LocalDateTime time = LocalDate.now().atTime(12, 3, 22,232);
System.out.println(time);
}

时钟 Clock

这个类可以获取当前的时间戳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Clock1 {
public static void main(String[] args) {
Clock millis = Clock.tickMillis(ZoneOffset.UTC);
Clock seconds = Clock.tickSeconds(ZoneOffset.UTC);
Clock minutes = Clock.tickMinutes(ZoneOffset.UTC);
// 精确到毫秒的时间戳
System.out.println(millis.millis());
// 精确到秒的时间戳
System.out.println(seconds.millis());
// 精确到分钟的时间戳
System.out.println(minutes.millis());
// 精确到毫秒的时间
System.out.println(millis.instant());
// 精确到秒的时间
System.out.println(seconds.instant());
// 精确到分钟的时间
System.out.println(minutes.instant());
}
}

上面的输出结果是:

1
2
3
4
5
6
1557567339397
1557567339000
1557567300000
2019-05-11T09:35:39.397Z
2019-05-11T09:35:39Z
2019-05-11T09:35:00Z

Map初始化小技巧

再看Clock类中的 ZoneId 接口的时候发现了这个接口中使用了一个Map,然对Map做了初始化。一般初始化必须使用 put 放数据,这里采用了 ofEntries 来初始化多组数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static final Map<String, String> SHORT_IDS = Map.ofEntries(
entry("ACT", "Australia/Darwin"),
entry("AET", "Australia/Sydney"),
entry("AGT", "America/Argentina/Buenos_Aires"),
entry("ART", "Africa/Cairo"),
entry("AST", "America/Anchorage"),
entry("BET", "America/Sao_Paulo"),
entry("BST", "Asia/Dhaka"),
entry("CAT", "Africa/Harare"),
entry("CNT", "America/St_Johns"),
entry("CST", "America/Chicago"),
entry("CTT", "Asia/Shanghai"),
entry("EAT", "Africa/Addis_Ababa"),
entry("ECT", "Europe/Paris"),
entry("IET", "America/Indiana/Indianapolis"),
entry("IST", "Asia/Kolkata"),
entry("JST", "Asia/Tokyo"),
entry("MIT", "Pacific/Apia"),
entry("NET", "Asia/Yerevan"),
entry("NST", "Pacific/Auckland"),
entry("PLT", "Asia/Karachi"),
entry("PNT", "America/Phoenix"),
entry("PRT", "America/Puerto_Rico"),
entry("PST", "America/Los_Angeles"),
entry("SST", "Pacific/Guadalcanal"),
entry("VST", "Asia/Ho_Chi_Minh"),
entry("EST", "-05:00"),
entry("MST", "-07:00"),
entry("HST", "-10:00")
);

扬帆起航

扬帆起航

其实很早就想写这篇文章,只是一直不知道从何说起。或者说,没能腾出一片完整的时间,给自己的大学生活写一个完整的回忆录。其实细细想来,这段时光更像是平静的水面,偶然也有湍急之处泛着白色的浪花。

开始

​ 和大多数人一样,以萌新的身份来到大学,看着即将成为自己学长学姐的那群人,一时不知道从哪里开始了解这个陌生而又神秘的校园。好,美好的大学生活就先从图书馆开始。良好的习惯是成功一半,也得要先熟悉熟悉自己以后学习的地方。

​ 摸索了挺久,才找到了计算机学科的阅览室。怎么说呢,被一大堆《Photoshop 从入门到精通》、《21 天精通 C 语言》、《JAVA语言精粹》吸引,幻想着自己真的能21天完成菜鸟到码神的逆袭。

​ 好吧… 想多了,不过,我还是爱上了这个地方。接下来的一学期,就是扎根图书馆,进行名词式学习了。之所以称之为名词式学习,是真的觉得自己的学习,没有正确的方式。一股脑的恨不得把没听过的东西都去学一遍,没见过的语言都想办法 『21天精通』 。于是,我成了一个彻底的 “宅男” ,只不过宅的是图书馆。

​ 所以还是想说,要在正确的路上做正确的事。这时候,一个好的引导者就显得尤为重要。他会带着你,踩尽量少的坑,走弯路更少的路。这个人,可以是你的老师,你的学长学姐,偶然发现的你们班里的大神。记得,找最适合自己的人,走最适合自己的路。

LAMP - 光

​ 庆幸的是,我的野路子并没有持续太久,我找到了方向——加入实验室。这就是我想说的另一点了,加入实验室。任何时候都不要说 “等我在多学点,再怎么怎么样”。要知道,实验室本身就是,可以更好的更快的学习新东西并快速实践的地方。

​ 我在大一下学期就进入了网络中心,也是在这里下定了决心一门心思的去做开发,不再一味的折腾 Linux 。(PS:如果不是想要看内核源码或者做系统运维,就不用花大量时间折腾 Linux 软件了,时间上比较不划算)

​ 加入了正规军,也接受了更正规的训练。我更加清楚,作为一名计算机专业的学生,我以后要走的是一条什么样的路,要成为一个怎么样的人。我们的团队叫做 “LMAP” ,取自我们开发使用的技术栈,同时也有 “光” 的含义!在这里,我度过了我的整个大学时光。

​ 已经说不清楚,那些曾经帮助过我的老师和学长们,是我所尊敬的师长,还是陪我一起成长的家人了。我们一起撸过凌晨两点的小院烧烤,也一起看过凌晨四点钟的西大,我想在这里特别感谢帮助过我的 『杨建丰』老师,以及 『金刚』 学长, 『文科』 学长!

嘉木

​ 我想说一件,我大学里很骄傲的事。也许没什么人听过,但确确实实是我大学里很珍贵的财富。事情源于大二上学期,在一个学长的组织下我们准备建设西北大学自己的 PT 站点。奈何出师未捷身先死,本来组建的团队还没开始开发就解散了。我很感谢20岁的我,可以那么勇敢得,去一个人做我现在可能有顾虑的事。

​ 项目基于开源的 “NexusPHP 项目” 。其实关于这个项目的案例很多,但是资料很少,很多高校采用了这套代码,并且进行了魔改,但是遗憾的是并没有开源。怀着雄心壮志,我一个人上路了。我想把它做好,然后开源出去,哪怕困难重重(事实证明遇到的困恼是重重重重重)。

​ 项目从九月中旬一直到十二月份,出了三个测试版本。这段时间基本除了上课时间我都是坐在电脑前面赶项目。由于 NexusPHP 是 2006 年开发的,当时的开发团队采用了比较原始的前后端夹杂的方式,代码基本没有可维护性而言,大部分还是 function block 基本不存在 OO 设计,HTML 和 CSS 基本全部嵌在 PHP 中,一个文件 1000 - 3000 行,甚至还有 15000 行的。那段时间真的是日常崩溃 ,不知道因为改代码电脑死机多少次,又熬了多少次夜。经常为了一个问题和功能,想到睡不着觉,有时候做梦都在写代码,想要看着自己的成果,早点问世。

​ 十二月份,第一个上线的版本终于确定了,第一周的注册量只有 500 。不过,当时上线的时间确实也是选择的不对,那时候快要期末考试,大家一门心思复习,没什么时间关注别的东西。第二年又对界面进行了重构,基本是全新的 UI。之后,我们也想了很多办法去提高用户量。我们组织了一些宣传活动有推出了嘉木的纪念文化衫,还想了很多办法降低用户使用站点的门槛,但是最终用户活跃度始终稳定在几百人,并没有达到预期。

​ 最终,站点只运行了一年,由于多方面的问题被关停。这次的失败也让我明白,一个产品的孵化并不简简单单的依靠技术,产品的运营和团队管理是上层建筑。没有好的运营和管理,再好的产品也没有办法发挥价值。

​ 另外比较庆幸的是,这次之后,我确实发现自己的技术得到了质的飞跃——毕竟 10 多万行代码的编码经验和调试上的经验比看书来的要真切的多,也算是真切的体会到 “纸上得来终觉浅,绝知此事要躬行” 。

迷茫

​ 其实在嘉木比较稳定运行期间,就没有太多额外的开发工作了。那段时间除了处理实验室老师交代的任务之外,就全身心的投入到 ThinkPHPOneThink 的源码。其实源码看了会很容易忘掉,所以不仅仅是看,看完以后我都会凭着印象和思路再敲一遍,实现框架和系统的核心部分。

​ 我在阅读读 OneThink 源码时就明显感觉到了自己和巨佬们之间的差距。虽然说 OneThink 源码不是那种 “Bible 级别” ,但官方对 TP 的运用确实称的上炉火纯青。一边阅读,一边发现原来ThinkPHP 可以这么写,原来那么多特性是可以这么使用的,原来一个简单的模块需要这么多代码保证高扩展性,健壮性,和稳定性。

​ 大二快结束时,我心里却越来越觉得慌张。自己只熟悉 PHP 一门语言,其他的只能算是入门,这样还怎么成为**『全栈工程师』 **?还怎么愉快的写 Bug ?加上那段敏感时间里,实验室学长们也都签了不错的工作,有去 BAT 的,也有去其他知名互联网公司的。他们的离开让我觉得到伤感和不舍。偶尔还会思考,实验室的下一批成员会是什么样的,我们会不会相处的很好,已经成为学长的我,能不能像我的学长们那样,给别人提供正确的引导和帮助?

选择

​ 空闲期的时间总是过得异常的快,转眼自己就已经大三了。突然发现,热爱了这么久的计算机,捣鼓着写了两年的代码,自己却没有一门能够拿得出手的技术。按照自己的规划,我是希望能够找一个不错的实习的。打开招聘网站却发现很多公司并没有 PHP 职位。当时的我,除了PHP就只会写前端了,还只能勉强算是个半吊子前端。我明确地,感受到了自己的压力和慌张。

​ 想了段时间,我觉得剩下的一年里,学一门纯正的面向对象语言吧!可是具体要猛攻哪一门呢? Java 在大一的有学过一段时间,但是很久不写,很多基本语法都已经不记得了。C++ 吧,当时确实用功学了半年,高级特性,内存管理,STL ,Boost 也都有好好看,只是后来也没好好用它写过项目。

​ 其实这个问题我纠结了很久。也许很多人说 “其实不论什么编程语言只要你精通了一门,如果你能融汇贯通其实其他的语言也一样”。其实我是很抵触这种说法的,简单对于 C++ 和 Java 来说,就算你敢说你熟悉 C++ 我觉得短时间熟悉 Java 语法,能使用 Java 做开发是没问题的。就像我在大一的很大一段时间内是使用的以前编写 C++ 的思维去 Java 写的,这样不仅不能表现出这个语言的特性和优雅,反而会觉得使用起来有些蹩脚。就像一个长时间写驱动和系统软件的人去接手一个大型的 Web 项目,我想他很可能会写出垃圾代码来。

​ 经过综合各方面的原因,我选择了Java。我知道,当我选择了一条路,另一条路上的风景便与我无关。一旦选择,我便不再摇摆不定。之后,全身心的学习Java是我每天都习以为常的事了。偶尔想想自己真的在面向‘’对象‘’编程,生活倒也怡然自得。

转折

​ 良好的基础决定了成功的一半。当时的我,把 《疯狂 Java 讲义》 和 《 JDK 1.8 API 》 里面重要章节 Demo 都敲了一遍,把语言基础、集合框架、OO、IO、反射、多线程、Servlet、JSP、Hibernate … 理解的异常透彻,日常觉得自己是一个 “人工语法分析器” 。

​ 可是当我掌握这些东西以后,我发现我好像一直停留在使用的阶段。这是一种有力气没出事的难受。后来,我开始思考,为什么我不去深入到底层探一探究竟?JDK 里的 集合框架并发工具包IO 究竟是如何实现的?Java 虚拟机是怎么运行起来的?JVM 的内存、GC、类加载、性能优化是怎么做到的?那些主流框架是如何设计的?DB 作为一个很重要的部分,我又该如何优化、如何做运维呢?这样的思考让我恐慌,却也促进了我的进步。我开始从另一个角度学习了。

​ 回想那段时间,内心还是会燃气一股热血。那是知识的输入和输出都到达最大化的一个阶段。基本两天我就可以刷掉一本 200 - 400 页的书,然后整理笔记。记不起来的、感觉模糊的、牵扯到其他不理解的继续看书,找博客,弄清楚之后把自己的理解写在笔记上。有时候一天就坐在那敲一篇几万字的读书笔记。其实说不清痛苦还是快乐 …

​ 当时差不多看了 30 多本经典书籍,也算是形成了比较完整的知识结构。那种高强度的训练让我把从大一学到的各种计算机专业知识都串起来了,好像突然打通了任督二脉,总能通过一个知识点发散出很多很多相关的知识。算得上是,真正跨入了开发的门槛。

四月

​ 2018 年刚开学我就泡在图书馆看书了。看以前写的笔记,整理博客,重新思考之前刷过的错题。我发现不论是以前学习的东西还是以前做的项目都还有很大的提升空间。对于以前看的书做的笔记,我又重新做了一遍,发现自己又有了很多新的理解。对于项目也会思考以前为什么那么实现,现在让我实现会不会有更好的实现方式呢?剩下的就是刷题了,LeetCode 、牛客,熟悉一些常用的算法,为找实习做准备。

​ 三月尾,我试着投了一些简历。四月初就开始陆陆续续面试了。说到面试,其实第一次面试大家都会紧张的,但是不要让过度的紧张让自己看起来应接不暇就可以了。之后的面试就会慢慢放松下来。其实面试,也就是一个双向的沟通学习,展示自己的过程,本没有太恐怖。找准定位,变现自己,就做到了能到到的最好了。

​ 后面就是 腾讯、阿里、百度 、携程的面试了,基本这几家面试官都很和善的,一路也算是比较顺利,只是当时腾讯最后一个主管面的时候直接给了五道算法题,还必须用 C 写,然后下载一个 gcc + CLion 基本时间过去一大半了,最后只有十多分钟做了两道题,然后就没有然后了…

​ 这里面印象比较好的就是阿里和携程了。阿里问的技术点都很深入,主要是因为问的点都偏向实际应用,我个人又更加喜欢这种关于实际开发的问题,另外也是我并不擅长算法。携程面试官则是对应聘者很热情,虽然当时面试场地没有腾讯那么大,但是面试官很 nice 的给我提了很多建议。

​ 最后我还是去了阿里,毕竟那里是 Javaer 的天堂,也是我学Java最初的心愿了。

实习

​ 阿里的师兄和师姐们都非常的 nice ,并且 对于新人的培养非常的用心。虽然感觉他们也特别忙碌,但师兄始终都很关心我,并给我找了很多资料让我熟悉业务,还抽出自己的空闲时间给我讲业务逻辑和项目结构。虽然在短时间内不能完全弄清楚公司业务,但是师兄的讲解还是让我受益颇多!

​ 另外,在阿里的实习也让我再一次发现自己真的很渺小。我会发现原来还有这么多自己不知道的框架、系统和工具。于是我又开始宅了,只不过这一次,我宅的地方变成了公司。我每天熟悉系统的业务模型和组内维护的系统代码,还要安排时间学习集团内的新技术。忙碌而紧张的学习甚至让我来不及为自己的进步开心,只是在内心深处觉得自己应该在多学一点,在努力一点。不仅仅是为了对得起自己最初的心愿,也是为了对得起那些默默支持着我的,默默帮助着我的人。

前行

​ 大四的时间过得异常的快,对于我来说,还没有好好感受过感受大学的美好就要匆匆说再见了。虽然我总是会在与别人交谈时避免谈到毕业的问题,可是自己心里清楚,自己留下了多少的遗憾。那些没有用心欣赏的玉兰花,没有好好感受过的西操场的风,以及那些来不及参与的一次次的同学聚会。

​ 大四的生活也没有我曾经以为的那么空闲。我还是写着代码,改着论文,过着平凡而又规律的生活。也许有人会觉得,只有奋斗的大学不会遗憾吗?会的,当然会。可是,没有奋斗的大学可能就不只是遗憾了,而是悔恨。也许是习惯了忙碌而充实的生活,也许是自己骨子里无趣又枯燥的灵魂,总之,我想我还是会继续这般生活——全力以赴,问心无愧。

扬帆起航

扬帆起航

其实很早就想写这篇文章,只是一直不知道从何说起。或者说,没能腾出一片完整的时间,给自己的大学生活写一个完整的回忆录。其实细细想来,这段时光更像是平静的水面,偶然也有湍急之处泛着白色的浪花。

开始

​ 和大多数人一样,以萌新的身份来到大学,看着即将成为自己学长学姐的那群人,一时不知道从哪里开始了解这个陌生而又神秘的校园。好,美好的大学生活就先从图书馆开始。良好的习惯是成功一半,也得要先熟悉熟悉自己以后学习的地方。

​ 摸索了挺久,才找到了计算机学科的阅览室。怎么说呢,被一大堆《Photoshop 从入门到精通》、《21 天精通 C 语言》、《JAVA语言精粹》吸引,幻想着自己真的能21天完成菜鸟到码神的逆袭。

​ 好吧… 想多了,不过,我还是爱上了这个地方。接下来的一学期,就是扎根图书馆,进行名词式学习了。之所以称之为名词式学习,是真的觉得自己的学习,没有正确的方式。一股脑的恨不得把没听过的东西都去学一遍,没见过的语言都想办法 『21天精通』 。于是,我成了一个彻底的 “宅男” ,只不过宅的是图书馆。

​ 所以还是想说,要在正确的路上做正确的事。这时候,一个好的引导者就显得尤为重要。他会带着你,踩尽量少的坑,走弯路更少的路。这个人,可以是你的老师,你的学长学姐,偶然发现的你们班里的大神。记得,找最适合自己的人,走最适合自己的路。

LAMP - 光

​ 庆幸的是,我的野路子并没有持续太久,我找到了方向——加入实验室。这就是我想说的另一点了,加入实验室。任何时候都不要说 “等我在多学点,再怎么怎么样”。要知道,实验室本身就是,可以更好的更快的学习新东西并快速实践的地方。

​ 我在大一下学期就进入了网络中心,也是在这里下定了决心一门心思的去做开发,不再一味的折腾 Linux 。(PS:如果不是想要看内核源码或者做系统运维,就不用花大量时间折腾 Linux 软件了,时间上比较不划算)

​ 加入了正规军,也接受了更正规的训练。我更加清楚,作为一名计算机专业的学生,我以后要走的是一条什么样的路,要成为一个怎么样的人。我们的团队叫做 “LMAP” ,取自我们开发使用的技术栈,同时也有 “光” 的含义!在这里,我度过了我的整个大学时光。

​ 已经说不清楚,那些曾经帮助过我的老师和学长们,是我所尊敬的师长,还是陪我一起成长的家人了。我们一起撸过凌晨两点的小院烧烤,也一起看过凌晨四点钟的西大,我想在这里特别感谢帮助过我的 『杨建丰』老师,以及 『金刚』 学长, 『文科』 学长!

嘉木

​ 我想说一件,我大学里很骄傲的事。也许没什么人听过,但确确实实是我大学里很珍贵的财富。事情源于大二上学期,在一个学长的组织下我们准备建设西北大学自己的 PT 站点。奈何出师未捷身先死,本来组建的团队还没开始开发就解散了。我很感谢20岁的我,可以那么勇敢得,去一个人做我现在可能有顾虑的事。

​ 项目基于开源的 “NexusPHP 项目” 。其实关于这个项目的案例很多,但是资料很少,很多高校采用了这套代码,并且进行了魔改,但是遗憾的是并没有开源。怀着雄心壮志,我一个人上路了。我想把它做好,然后开源出去,哪怕困难重重(事实证明遇到的困恼是重重重重重)。

​ 项目从九月中旬一直到十二月份,出了三个测试版本。这段时间基本除了上课时间我都是坐在电脑前面赶项目。由于 NexusPHP 是 2006 年开发的,当时的开发团队采用了比较原始的前后端夹杂的方式,代码基本没有可维护性而言,大部分还是 function block 基本不存在 OO 设计,HTML 和 CSS 基本全部嵌在 PHP 中,一个文件 1000 - 3000 行,甚至还有 15000 行的。那段时间真的是日常崩溃 ,不知道因为改代码电脑死机多少次,又熬了多少次夜。经常为了一个问题和功能,想到睡不着觉,有时候做梦都在写代码,想要看着自己的成果,早点问世。

​ 十二月份,第一个上线的版本终于确定了,第一周的注册量只有 500 。不过,当时上线的时间确实也是选择的不对,那时候快要期末考试,大家一门心思复习,没什么时间关注别的东西。第二年又对界面进行了重构,基本是全新的 UI。之后,我们也想了很多办法去提高用户量。我们组织了一些宣传活动有推出了嘉木的纪念文化衫,还想了很多办法降低用户使用站点的门槛,但是最终用户活跃度始终稳定在几百人,并没有达到预期。

​ 最终,站点只运行了一年,由于多方面的问题被关停。这次的失败也让我明白,一个产品的孵化并不简简单单的依靠技术,产品的运营和团队管理是上层建筑。没有好的运营和管理,再好的产品也没有办法发挥价值。

​ 另外比较庆幸的是,这次之后,我确实发现自己的技术得到了质的飞跃——毕竟 10 多万行代码的编码经验和调试上的经验比看书来的要真切的多,也算是真切的体会到 “纸上得来终觉浅,绝知此事要躬行” 。

迷茫

​ 其实在嘉木比较稳定运行期间,就没有太多额外的开发工作了。那段时间除了处理实验室老师交代的任务之外,就全身心的投入到 ThinkPHPOneThink 的源码。其实源码看了会很容易忘掉,所以不仅仅是看,看完以后我都会凭着印象和思路再敲一遍,实现框架和系统的核心部分。

​ 我在阅读读 OneThink 源码时就明显感觉到了自己和巨佬们之间的差距。虽然说 OneThink 源码不是那种 “Bible 级别” ,但官方对 TP 的运用确实称的上炉火纯青。一边阅读,一边发现原来ThinkPHP 可以这么写,原来那么多特性是可以这么使用的,原来一个简单的模块需要这么多代码保证高扩展性,健壮性,和稳定性。

​ 大二快结束时,我心里却越来越觉得慌张。自己只熟悉 PHP 一门语言,其他的只能算是入门,这样还怎么成为**『全栈工程师』 **?还怎么愉快的写 Bug ?加上那段敏感时间里,实验室学长们也都签了不错的工作,有去 BAT 的,也有去其他知名互联网公司的。他们的离开让我觉得到伤感和不舍。偶尔还会思考,实验室的下一批成员会是什么样的,我们会不会相处的很好,已经成为学长的我,能不能像我的学长们那样,给别人提供正确的引导和帮助?

选择

​ 空闲期的时间总是过得异常的快,转眼自己就已经大三了。突然发现,热爱了这么久的计算机,捣鼓着写了两年的代码,自己却没有一门能够拿得出手的技术。按照自己的规划,我是希望能够找一个不错的实习的。打开招聘网站却发现很多公司并没有 PHP 职位。当时的我,除了PHP就只会写前端了,还只能勉强算是个半吊子前端。我明确地,感受到了自己的压力和慌张。

​ 想了段时间,我觉得剩下的一年里,学一门纯正的面向对象语言吧!可是具体要猛攻哪一门呢? Java 在大一的有学过一段时间,但是很久不写,很多基本语法都已经不记得了。C++ 吧,当时确实用功学了半年,高级特性,内存管理,STL ,Boost 也都有好好看,只是后来也没好好用它写过项目。

​ 其实这个问题我纠结了很久。也许很多人说 “其实不论什么编程语言只要你精通了一门,如果你能融汇贯通其实其他的语言也一样”。其实我是很抵触这种说法的,简单对于 C++ 和 Java 来说,就算你敢说你熟悉 C++ 我觉得短时间熟悉 Java 语法,能使用 Java 做开发是没问题的。就像我在大一的很大一段时间内是使用的以前编写 C++ 的思维去 Java 写的,这样不仅不能表现出这个语言的特性和优雅,反而会觉得使用起来有些蹩脚。就像一个长时间写驱动和系统软件的人去接手一个大型的 Web 项目,我想他很可能会写出垃圾代码来。

​ 经过综合各方面的原因,我选择了Java。我知道,当我选择了一条路,另一条路上的风景便与我无关。一旦选择,我便不再摇摆不定。之后,全身心的学习Java是我每天都习以为常的事了。偶尔想想自己真的在面向‘’对象‘’编程,生活倒也怡然自得。

转折

​ 良好的基础决定了成功的一半。当时的我,把 《疯狂 Java 讲义》 和 《 JDK 1.8 API 》 里面重要章节 Demo 都敲了一遍,把语言基础、集合框架、OO、IO、反射、多线程、Servlet、JSP、Hibernate … 理解的异常透彻,日常觉得自己是一个 “人工语法分析器” 。

​ 可是当我掌握这些东西以后,我发现我好像一直停留在使用的阶段。这是一种有力气没出事的难受。后来,我开始思考,为什么我不去深入到底层探一探究竟?JDK 里的 集合框架并发工具包IO 究竟是如何实现的?Java 虚拟机是怎么运行起来的?JVM 的内存、GC、类加载、性能优化是怎么做到的?那些主流框架是如何设计的?DB 作为一个很重要的部分,我又该如何优化、如何做运维呢?这样的思考让我恐慌,却也促进了我的进步。我开始从另一个角度学习了。

​ 回想那段时间,内心还是会燃气一股热血。那是知识的输入和输出都到达最大化的一个阶段。基本两天我就可以刷掉一本 200 - 400 页的书,然后整理笔记。记不起来的、感觉模糊的、牵扯到其他不理解的继续看书,找博客,弄清楚之后把自己的理解写在笔记上。有时候一天就坐在那敲一篇几万字的读书笔记。其实说不清痛苦还是快乐 …

​ 当时差不多看了 30 多本经典书籍,也算是形成了比较完整的知识结构。那种高强度的训练让我把从大一学到的各种计算机专业知识都串起来了,好像突然打通了任督二脉,总能通过一个知识点发散出很多很多相关的知识。算得上是,真正跨入了开发的门槛。

四月

​ 2018 年刚开学我就泡在图书馆看书了。看以前写的笔记,整理博客,重新思考之前刷过的错题。我发现不论是以前学习的东西还是以前做的项目都还有很大的提升空间。对于以前看的书做的笔记,我又重新做了一遍,发现自己又有了很多新的理解。对于项目也会思考以前为什么那么实现,现在让我实现会不会有更好的实现方式呢?剩下的就是刷题了,LeetCode 、牛客,熟悉一些常用的算法,为找实习做准备。

​ 三月尾,我试着投了一些简历。四月初就开始陆陆续续面试了。说到面试,其实第一次面试大家都会紧张的,但是不要让过度的紧张让自己看起来应接不暇就可以了。之后的面试就会慢慢放松下来。其实面试,也就是一个双向的沟通学习,展示自己的过程,本没有太恐怖。找准定位,变现自己,就做到了能到到的最好了。

​ 后面就是 腾讯、阿里、百度 、携程的面试了,基本这几家面试官都很和善的,一路也算是比较顺利,只是当时腾讯最后一个主管面的时候直接给了五道算法题,还必须用 C 写,然后下载一个 gcc + CLion 基本时间过去一大半了,最后只有十多分钟做了两道题,然后就没有然后了…

​ 这里面印象比较好的就是阿里和携程了。阿里问的技术点都很深入,主要是因为问的点都偏向实际应用,我个人又更加喜欢这种关于实际开发的问题,另外也是我并不擅长算法。携程面试官则是对应聘者很热情,虽然当时面试场地没有腾讯那么大,但是面试官很 nice 的给我提了很多建议。

​ 最后我还是去了阿里,毕竟那里是 Javaer 的天堂,也是我学Java最初的心愿了。

实习

​ 阿里的师兄和师姐们都非常的 nice ,并且 对于新人的培养非常的用心。虽然感觉他们也特别忙碌,但师兄始终都很关心我,并给我找了很多资料让我熟悉业务,还抽出自己的空闲时间给我讲业务逻辑和项目结构。虽然在短时间内不能完全弄清楚公司业务,但是师兄的讲解还是让我受益颇多!

​ 另外,在阿里的实习也让我再一次发现自己真的很渺小。我会发现原来还有这么多自己不知道的框架、系统和工具。于是我又开始宅了,只不过这一次,我宅的地方变成了公司。我每天熟悉系统的业务模型和组内维护的系统代码,还要安排时间学习集团内的新技术。忙碌而紧张的学习甚至让我来不及为自己的进步开心,只是在内心深处觉得自己应该在多学一点,在努力一点。不仅仅是为了对得起自己最初的心愿,也是为了对得起那些默默支持着我的,默默帮助着我的人。

前行

​ 大四的时间过得异常的快,对于我来说,还没有好好感受过感受大学的美好就要匆匆说再见了。虽然我总是会在与别人交谈时避免谈到毕业的问题,可是自己心里清楚,自己留下了多少的遗憾。那些没有用心欣赏的玉兰花,没有好好感受过的西操场的风,以及那些来不及参与的一次次的同学聚会。

​ 大四的生活也没有我曾经以为的那么空闲。我还是写着代码,改着论文,过着平凡而又规律的生活。也许有人会觉得,只有奋斗的大学不会遗憾吗?会的,当然会。可是,没有奋斗的大学可能就不只是遗憾了,而是悔恨。也许是习惯了忙碌而充实的生活,也许是自己骨子里无趣又枯燥的灵魂,总之,我想我还是会继续这般生活——全力以赴,问心无愧。

WebStrom 使用技巧

1. React live template

  1. state this.state
  2. sst this.setState
  3. rpc 生成组件类
  4. rsfp 生成组件函数
  5. rrc 具有redux的组件类
  6. rpt propTypes
  7. ren render 方法
  8. ptsr string required
  9. pts string 类型
  10. pto object 类型
  11. ptor object required
  12. ptn/ptnr ptf/ptfr ptb/ptbf pta/ptar number / function / bool /array
  13. props this.props
  14. est this.state={}
  15. cdm componentDidMount

React 实战

1.目录

2.依赖安装

1
2
3
4
5
cnpm install antd-mobile -S 
//组件按需加载 第二个是用来做react脚手架的二次配置的,因为我们看不到Webpack的配置文件了 https://www.cnblogs.com/xiaohuochai/p/8491055.html
cnpm install --save-dev babel-plugin-import react-app-rewired@2.0.2-next.0 //注意指定版本号否则不兼容
cnpm install --save-dev less@2.7.3 less-loader

3.配置项目

1.修改index添加触摸事件

1
2
3
4
5
6
7
8
9
10
11
12
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<script src="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js"></script>
<script>
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
if(!window.Promise) {
document.writeln('<script src="https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"'+'>'+'<'+'/'+'script>');
}
</script>

2.添加 antd 配置文件 config-overrides.js

注意这个文件不是在 src 下面而是在整个项目的与 package,json 同级的目录。定义加载配置的 js 模块 !

1
2
3
4
5
6
const {injectBabelPlugin} = require('react-app-rewired');
module.exports = function override(config, env) {
config = injectBabelPlugin(['import', {libraryName: 'antd-mobile', style: 'css'}],
config);
return config;
}

修改配置: package.json

1
2
3
4
5
6
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",
"eject": "react-scripts eject"
}

这是关于自定义配置的内容 Antd配置

MangoDB 入门

1. 设置为windows启动项

1
mongod -dbpath "C:\Program Files\MongoDB\Server\4.0\data" -logpath "C:\Program Files\MongoDB\Server\4.0\log\MongoDB.log" -install -serviceName "MongoDB"

2.基本概念

  1. 数据库(database):数据的仓库可以在仓库中存放集合
  2. 集合(collection):集合是数组可以在集合中存放文档
  3. 文档(document):存储操作的都是文档,文档是数据库中最小的单位。

在MongoDB中,数据库和集合都不需要手动创建,当我们创建文档时,如果文档所在的集合或数据库不存在会自动创建数据库和集合.MongoDB的文档的属性值也可以是一个文档,当一个文档的属性值是一个文档时,我们称这个文档叫做 内嵌文档

1
db.users.update({username:"sunwukong"},{$set:{hobby:{cities:["beijing","shanghai","shenzhen"] , movies:["sanguo","hero"]}}});

3.基本指令

1. 显示数据库

1
2
show dbs
show databases

2.进入到指定的数据库中

1
use 数据库名

​ 如果我们use了一个不存在的数据库他也不会报错而是在我们第一次创建文档的时候创建这个数据库,在MongoDB中所有的数据库和集合都不需要我们去创建的 。

3.当前所处的数据库

1
db

注意一下 db 就类似于 this 只想当前的上下文,我们在后面的 crud 操作可以直接使用数据库名也可以在进入到数据库之后直接使用 db

4.显示数据库中所有的集合

1
show collections

5.数据库的CRUD(增删改查)的操作

1.向数据库中插入文档

1
db.<collection>.insert()  向集合中插入一个或多个文档

当我们向集合中插入文档时,如果没有给文档指定_id属性,则数据库会自动为文档添加 _id 该属性用来作为文档的唯一标识 , _id我们可以自己指定,如果我们指定了数据库就不会在添加了,如果自己指定_id 也必须确保它的唯一性

1
2
db.collection.insertOne()  插入一个文档对象
db.collection.insertMany() 插入多个文档对象

上面的是在3.2 版本中新加的操作,为了语义更加的明确。

例子:向test数据库中的,stus集合中插入一个新的学生对象
{name:”孙悟空”,age:18,gender:”男”}

1
2
3
4
5
6
7
8
9
10
db.stus.insert({name:"孙悟空",age:18,gender:"男"})
test.stus.insert({name:“lwne”,age:12,gender:“女”})
use test;
db.stus.insert({name:"lwen",age:12})
db.stus.insert([
{name:"lwen",age:12},
{name:"lwen",age:12},
{name:"lwen",age:12}
])
db.stus.find()

2.查询

1
db.collection.find()

find()用来查询集合中所有符合条件的文档,find()可以接收一个对象作为条件参数
{} 空对象或者{ }表示查询集合中所有的文档
{属性:值} 查询属性是指定值的文档

find()返回的是一个数组

1
db.collection.findOne()

用来查询集合中符合条件的第一个文档 ,findOne()返回的是一个文档对象

1
db.collection.find({}).count() 

查询所有结果的数量

MongoDB支持直接通过内嵌文档的属性进行查询,如果要查询内嵌文档则可以通过.的形式来匹配
如果要通过内嵌文档来对文档进行查询,此时属性名必须使用引号

1
db.users.find({'hobby.movies':"hero"});

逻辑条件查询:

19.查询numbers中num大于5000的文档

1
2
3
4
5
6
7
db.numbers.find({num:{$gt:500}}); 大于500
db.numbers.find({num:{$eq:500}}); 等于500
db.numbers.find().limit(10); 查看numbers集合中的前10条数据
db.numbers.find(); 开发时,我们绝对不会执行不带条件的查询
db.numbers.find().skip(10).limit(10);
db.emp.find({sal:{$lt:2000 , $gt:1000}}); 查询工资在1000-2000之间的员工
db.emp.find({$or:[{sal:{$lt:1000}} , {sal:{$gt:2500}}]}); 查询工资小于1000或大于2500的员工

skip()用于跳过指定数量的数据

1
skip((页码-1) * 每页显示的条数).limit(每页显示的条数);  数据分页
1
2
3
4
5
db.stus.find({_id:"hello"});
db.stus.find({age:16 , name:"白骨精"});
db.stus.find({age:28});
db.stus.findOne({age:28});
db.stus.find({}).count();

3.修改

db.collection.update(查询条件,新对象)

update()默认情况下会使用新对象来替换旧的对象

如果需要修改指定的属性,而不是替换需要使用“修改操作符”来完成修改
$set 可以用来修改文档中的指定属性
$unset 可以用来删除文档的指定属性

$push 用于向数组中添加一个新的元素
$addToSet 向数组中添加一个新元素 , 如果数组中已经存在了该元素,则不会添加

1
2
db.users.update({username:"tangseng"},{$push:{"hobby.movies":"Interstellar"}});
db.users.update({username:"tangseng"},{$addToSet:{"hobby.movies":"Interstellar"}});

update()默认只会修改一个

1
db.collection.updateMany()

同时修改多个符合条件的文档

1
db.collection.updateOne()

修改一个符合条件的文档

1
db.collection.replaceOne()

替换一个文档

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
//替换
db.stus.update({name:"沙和尚"},{age:28});

db.stus.update(
{"_id" : ObjectId("59c219689410bc1dbecc0709")},
{$set:{
gender:"男",
address:"流沙河"
}}
)

db.stus.update(
{"_id" : ObjectId("59c219689410bc1dbecc0709")},
{$unset:{
address:1
}}
)

db.stus.updateMany(
{"name" : "猪八戒"},
{
$set:{
address:"猪老庄"
}
}
);

db.stus.update(
{"name" : "猪八戒"},

{
$set:{
address:"呵呵呵"
}
} ,
{
multi:true
}
)

4.删除

1
db.collection.remove()

删除一个或多个,可以第二个参数传递一个true,则只会删除一个

如果传递一个空对象作为参数,则会删除所有的

1
2
3
4
db.collection.deleteOne()
db.collection.deleteMany()
db.collection.drop() 删除集合
db.dropDatabase() 删除数据库

一般数据库中的数据都不会删除,所以删除的方法很少调用,一般会在数据中添加一个字段,来表示数据是否被删除

6.排序投影

查询文档时,默认情况是按照_id的值进行排列(升序),sort()可以用来指定文档的排序的规则,sort()需要传递一个对象来指定排序规则 1表示升序 -1表示降序

1
2
//limit skip sort 可以以任意的顺序进行调用
db.emp.find({}).sort({sal:1,empno:-1});

在查询时,可以在第二个参数的位置来设置查询结果的 投影

1
db.emp.find({},{ename:1 , _id:0 , sal:1});

4. mongoose

1. 连接数据库

  1. 下载安装Mongoose

​ npm i mongoose –save

  1. 在项目中引入mongoose

​ var mongoose = require(“mongoose”);

  1. 连接MongoDB数据库

    mongoose.connect(‘mongodb://数据库的ip地址:端口号/数据库名’, { useMongoClient: true});如果端口号是默认端口号(27017) 则可以省略不写

  2. 断开数据库连接(一般不需要调用)

    MongoDB数据库,一般情况下,只需要连接一次,连接一次以后,除非项目停止服务器关闭,否则连接一般不会断开mongoose.disconnect()

  3. 监听MongoDB数据库的连接状态

    在mongoose对象中,有一个属性叫做connection,该对象表示的就是数据库连接.通过监视该对象的状态,可以来监听数据库的连接与断开

    1. 数据库连接成功的事件

​ mongoose.connection.once(“open”,function(){})}

​ 2. 数据库断开的事件

​ mongoose.connection.once(“close”,function(){});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//引入
var mongoose = require("mongoose");
//连接数据库
mongoose.connect("mongodb://127.0.0.1/mongoose_test" , { useMongoClient: true});

mongoose.connection.once("open",function(){
console.log("数据库连接成功~~~");
});

mongoose.connection.once("close",function(){
console.log("数据库连接已经断开~~~");
});

//断开数据库连接
mongoose.disconnect();

2. Schema

这个东西可以看做我们的关系数据库的表的约束。比如哪些字段必须有,而且他的值是什么类型,默认值是什么,这样我们在语言中就不会出现类型转换错误的情况,并且在mongoose中也会帮我们去做转换。

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
var mongoose = require("mongoose");
mongoose.connect("mongodb://127.0.0.1/mongoose_test",{useMongoClient:true});
mongoose.connection.once("open",function () {
console.log("数据库连接成功~~~");
});


//将mongoose.Schema 赋值给一个变量
var Schema = mongoose.Schema;

//创建Schema(模式)对象
var stuSchema = new Schema({

name:String,
age:Number,
gender:{
type:String,
default:"female"
},
address:String

});

//通过Schema来创建Model
//Model代表的是数据库中的集合,通过Model才能对数据库进行操作
//mongoose.model(modelName, schema):
//modelName 就是要映射的集合名 mongoose会自动将集合名变成复数
var StuModel = mongoose.model("student" , stuSchema);

//向数据库中插入一个文档
//StuModel.create(doc, function(err){});
StuModel.create({
name:"白骨精",
age:16,
address:"白骨洞"
},function (err) {
if(!err){
console.log("插入成功~~~");
}
});

3. Model

有了Model,我们就可以来对数据库进行增删改查的操作了

​ Model.create(doc(s), [callback]) 用来创建一个或多个文档并添加到数据库中

参数:

​ doc(s) 可以是一个文档对象,也可以是一个文档对象的数组

​ callback 当操作完成以后调用的回调函数

查询的:

1
2
3
Model.find(conditions, [projection], [options], [callback])  查询所有符合条件的文档 总会返回一个数组
Model.findById(id, [projection], [options], [callback]) 根据文档的id属性查询文档
Model.findOne([conditions], [projection], [options], [callback]) 查询符合条件的第一个文档 总和返回一个具体的文档对象

conditions 查询的条件

projection 投影 需要获取到的字段,两种方式

  1. {name:1,_id:0}
  2. “name -_id”

options 查询选项(skip limit)

​ skip:3 , limit:1}

callback 回调函数,查询结果会通过回调函数返回

​ 回调函数必须传,如果不传回调函数,压根不会查询

使用Model进行增删改查

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
StuModel.find({name:"唐僧"},function (err , docs) {
if(!err){
console.log(docs);
}
});
StuModel.find({},{name:1 , _id:0},function (err , docs) {
if(!err){
console.log(docs);
}
});
StuModel.find({},"name age -_id", {skip:3 , limit:1} , function (err , docs) {
if(!err){
console.log(docs);
}
});

StuModel.findOne({} , function (err , doc) {
if(!err){
console.log(doc);
}
});
StuModel.findById("59c4c3cf4e5483191467d392" , function (err , doc) {
if(!err){
//console.log(doc);
//通过find()查询的结果,返回的对象,就是Document,文档对象
//Document对象是Model的实例
console.log(doc instanceof StuModel);
}
});StuModel.create([
{
name:"沙和尚",
age:38,
gender:"male",
address:"流沙河"
}

],function (err) {
if(!err){
console.log(arguments);
}
});


/*
修改
Model.update(conditions, doc, [options], [callback])
Model.updateMany(conditions, doc, [options], [callback])
Model.updateOne(conditions, doc, [options], [callback])
- 用来修改一个或多个文档
- 参数:
conditions 查询条件
doc 修改后的对象
options 配置参数
callback 回调函数
Model.replaceOne(conditions, doc, [options], [callback])
* */

StuModel.updateOne({name:"唐僧"},{$set:{age:20}},function (err) {
if(!err){
console.log("修改成功");
}
});


/*
删除:
Model.remove(conditions, [callback])
Model.deleteOne(conditions, [callback])
Model.deleteMany(conditions, [callback])
*/
StuModel.remove({name:"白骨精"},function (err) {
if(!err){
console.log("删除成功~~");
}
});


Model.count(conditions, [callback])
- 统计文档的数量的

StuModel.count({},function (err , count) {
if(!err){
console.log(count);
}
});

4.document

​ Document 和 集合中的文档一一对应 , Document是Model的实例,通过Model查询到结果都是Document

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
var mongoose = require("mongoose");
mongoose.connect("mongodb://127.0.0.1/mongoose_test",{useMongoClient:true});
mongoose.connection.once("open",function () {
console.log("数据库连接成功~~~");
});

var Schema = mongoose.Schema;

var stuSchema = new Schema({

name:String,
age:Number,
gender:{
type:String,
default:"female"
},
address:String

});

var StuModel = mongoose.model("student" , stuSchema);
/*
Document 和 集合中的文档一一对应 , Document是Model的实例
通过Model查询到结果都是Document
*/

//创建一个Document
var stu = new StuModel({
name:"奔波霸",
age:48,
gender:"male",
address:"碧波潭"
});

/*
document的方法
Model#save([options], [fn])
*/
/*
stu.save(function (err) {
if(!err){
console.log("保存成功~~~");
}
});*/

StuModel.findOne({},function (err , doc) {
if(!err){
/*
update(update,[options],[callback])
- 修改对象
remove([callback])
- 删除对象

*/
//console.log(doc);
/*doc.update({$set:{age:28}},function (err) {
if(!err){
console.log("修改成功~~~");
}
});*/

/*doc.age = 18;
doc.save();*/

/*doc.remove(function (err) {
if(!err){
console.log("大师兄再见~~~");
}
});*/


/*
get(name)
- 获取文档中的指定属性值
set(name , value)
- 设置文档的指定的属性值
id
- 获取文档的_id属性值
toJSON() ******
- 转换为一个JSON对象

toObject()
- 将Document对象转换为一个普通的JS对象
转换为普通的js对象以后,注意所有的Document对象的方法或属性都不能使用了

*/
//console.log(doc.get("age"));
//console.log(doc.age);

//doc.set("name","猪小小");
//doc.name = "hahaha";

//console.log(doc._id);
//var j = doc.toJSON();
//console.log(j);

//var o = doc.toObject();

//console.log(o);

doc = doc.toObject();

delete doc.address;

console.log(doc._id);

}
});