月光下的影子

Only the stronger survives in this world.


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

装饰者模式

发表于 2017-08-24 | 分类于 设计模式

The Decorator pattern is a more flexible alternative to subclassing. The Decorator class implements the same interface as the target and uses aggregation to “decorate” calls to thetarget. Using the Decorator pattern it is possible to change the behavior of the class during runtime.

装饰者模式(decorator)有时又被称为包装者模式(Wrapper)。该模式可以动态的、透明的给对象赋予某些额外的功能。
我们常用的BufferedInputStream就是InputStream的一个装饰者,或者称之为包装类(Wrapper),通过这样我们可以给我们的输入流提供了额外的缓存的功能。
装饰者Decorator和目标的类Target实现同一个接口,使用装饰者模式可以在运行时改变类的行为。

装饰者模式的意图

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

上面这句话的意思是说装饰者模式的意图是给一个对象动态的添加一些功能。相较于子类化来说,装饰者模式更加的灵活可变。

装饰者模式适用的场景

  • 用于给对象动态的、透明的添加某些功能。同时又不会影响到其他的对象。
  • 当使用继承扩展一个对象是不合适的时候(继承最好是”IS-A”)的关系。
  • 有的类是不能继承的,但是你又希望在使用的时候增强它的功能。

具体实例

这个装饰者模式的例子共有4个关键的类。Troll巨魔接口, SimpleTroll 普通巨魔, TrollDecorator 巨魔装饰者以及 ClubbedTroll 棍棒巨魔。类之间的继承关系如图所示:
装饰者模式类图
从图中可以看到,SimpleTroll和装饰者TrollDecorator都实现了 Troll 接口。TrollDecorator 包含了一个 Troll 的引用,并对其中的方法使用该引用进行执行。ClubbedTroll 继承了 TrollDecorator ,它也是个装饰器,并对其中的一些方法进行了增强。具体代码如下:
Troll 接口:

1
2
3
4
5
6
7
8
9
public interface Troll {

void attack();

int getAttackPower();

void fleeBattle();

}

装饰者TrollDecorator:

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
/**
* TrollDecorator是一个装饰者,持有了一个被装饰者的引用
* 它会拦截对被装饰者的调用,并将调用委托为被装饰者执行
*/
public class TrollDecorator implements Troll {

private Troll decorated;

public TrollDecorator(Troll decorated) {
this.decorated = decorated;
}

@Override
public void attack() {
decorated.attack();
}

@Override
public int getAttackPower() {
return decorated.getAttackPower();
}

@Override
public void fleeBattle() {
decorated.fleeBattle();
}
}

目标类也就是被装饰者SimpleTroll:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
*目标类也就是被装饰者SimpleTroll
*/
public class SimpleTroll implements Troll {

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

@Override
public void attack() {
LOGGER.info("The troll tries to grab you!");
}

@Override
public int getAttackPower() {
return 10;
}

@Override
public void fleeBattle() {
LOGGER.info("The troll shrieks in horror and runs away!");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 装饰者,继承自TrollDecorator
* 并对attack和getAttackPower方法进行了扩展
*/
public class ClubbedTroll extends TrollDecorator {

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

public ClubbedTroll(Troll decorated) {
super(decorated);
}

@Override
public void attack() {
super.attack();
LOGGER.info("The troll swings at you with a club!");
}

@Override
public int getAttackPower() {
return super.getAttackPower() + 10;
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class App {
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
// simple troll
LOGGER.info("A simple looking troll approaches.");
Troll troll = new SimpleTroll();
troll.attack();
troll.fleeBattle();
LOGGER.info("Simple troll power {}.\n", troll.getAttackPower());

// change the behavior of the simple troll by adding a decorator
LOGGER.info("A troll with huge club surprises you.");
Troll clubbed = new ClubbedTroll(troll);
clubbed.attack();
clubbed.fleeBattle();
LOGGER.info("Clubbed troll power {}.\n", clubbed.getAttackPower());
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11:03:44.314 [main] INFO com.iluwatar.decorator.App - A simple looking troll approaches.
11:03:44.317 [main] INFO com.iluwatar.decorator.SimpleTroll - The troll tries to grab you!
11:03:44.317 [main] INFO com.iluwatar.decorator.SimpleTroll - The troll shrieks in horror and runs away!
11:03:44.317 [main] INFO com.iluwatar.decorator.App - Simple troll power 10.

11:03:44.319 [main] INFO com.iluwatar.decorator.App - A troll with huge club surprises you.
11:03:44.320 [main] INFO com.iluwatar.decorator.SimpleTroll - The troll tries to grab you!
11:03:44.320 [main] INFO com.iluwatar.decorator.ClubbedTroll - The troll swings at you with a club!
11:03:44.320 [main] INFO com.iluwatar.decorator.SimpleTroll - The troll shrieks in horror and runs away!
11:03:44.320 [main] INFO com.iluwatar.decorator.App - Clubbed troll power 20.

总结

装饰者模式可以透明的给一个对象增加功能,并不改变对象的使用方法,在现实中使用的也是比较多的。如我们经常用的 BufferedInputStream 就是一个装饰者,它给 InputStream 类增加了缓存的功能。

1
2
InputStream is = new FileInputStream("path");
BufferedInputStream bis = new BufferedInputStream(is);

参考资料

  • Decorator

观察者模式

发表于 2017-08-23 | 分类于 设计模式

首先来看看最简单的观察者模式,使用java.util包实现的观察者模式。使用另个类,一个是java.util.Observer接口以及java.util.Observable来实现。(除了直接使用java提供的现成的观察者类以外,我们也可以自己实现观察者模式)直接上代码:

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
/**
* 观察者A
*/
class ObserverA implements Observer{
@Override
public void update(Observable o, Object arg)
{
//arg为接收到的消息,收到消息直接print
System.out.println("A received:" + arg);
}
}
/**
* 观察者B
*/
class ObserverB implements Observer{
@Override
public void update(Observable o, Object arg)
{
System.out.println("B received:" + arg);
}
}
/**
* 主题类,也就是被观察的类
* 直接继承自Observable接口
*/
public class Subject extends Observable
{

{
//添加观察者
addObserver(new ObserverA());
addObserver(new ObserverB());
//设置状态已经改变
setChanged();
}
public static void main(String[] args)
{
Subject subject = new Subject();
subject.notifyObservers("hello");
}
}

运行结果:

1
2
B received: hello
A received: hello

上面的代码很简单,Subject类实现了Observable接口,表明了它是一个主题(被观察者)。然后,它本身持有了两个观察者的引用 ObserverA 和 ObserverB 。当Subject 类的状态改变时,观察者将会调用update()方法来进行相应的操作。
注意,Subject 类在通知观察者的时候,只知道这些是观察者(实现了 Observer )接口,但是并不知道是哪个观察者。同时,它也不知道观察者的具体实现,这样就实现了主题和观察者之间的松耦合。下面详细介绍观察者模式的使用场景以及实现。

观察者模式

观察者模式的设计意图

定义对象之间的一对多依赖关系,这样当一个对象改变状态时,所有的依赖项都会自动得到通知和更新。

使用场景

在以下情况,可以考虑使用观察者模式:

  • 当一个问题的抽象后有两个主要操作A和B,一个依赖于另一个。将这些操作封装在单独的对象中,可以可以独立地进行更改和重用。
  • 当对一个对象的状态改变时,需要更改其他对象,但是不知道需要更改哪些对象。
  • 希望实现两个对象之间的松耦合。

在上面的描述中,最典型的使用场景就是一个对象的改变,需要将其他对象也进行相应的操作(主题通知观察者)。

具体实例

天气 Weather的改变将会导致其他物种的改变,半兽人 Orcs和霍比特人Hobbits对于天气的改变将会做出不同的反应。在这个实例中,天气就是一个主题,也就是被观察者;半兽人和霍比特人就是观察者,他们会随着天气的变化来相应的变化。
类之间的关系图如下图所示:
观察者之间的类图
WeatherObserver接口定义了一个update()方法。两个观察者Hobbits和Orcs实现了该接口。主题类Weather中有增加、删除、通知观察者的方法,当timePasses方法执行时,将会通知已经注册的观察者;WeatherType是一个天气的枚举类。具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
/**
*
* 观察者接口
*
*/
public interface WeatherObserver {

void update(WeatherType currentWeather);

}

两个观察者Hobbits和Orcs。

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
/**
*
* Hobbits
*
*/
public class Hobbits implements WeatherObserver {

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

@Override
public void update(WeatherType currentWeather) {
switch (currentWeather) {
case COLD:
LOGGER.info("The hobbits are shivering in the cold weather.");
break;
case RAINY:
LOGGER.info("The hobbits look for cover from the rain.");
break;
case SUNNY:
LOGGER.info("The happy hobbits bade in the warm sun.");
break;
case WINDY:
LOGGER.info("The hobbits hold their hats tightly in the windy weather.");
break;
default:
break;
}
}
}
/**
*
* Orcs
*
*/
public class Orcs implements WeatherObserver {

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

@Override
public void update(WeatherType currentWeather) {
switch (currentWeather) {
case COLD:
LOGGER.info("The orcs are freezing cold.");
break;
case RAINY:
LOGGER.info("The orcs are dripping wet.");
break;
case SUNNY:
LOGGER.info("The sun hurts the orcs' eyes.");
break;
case WINDY:
LOGGER.info("The orc smell almost vanishes in the wind.");
break;
default:
break;
}
}
}

下面是主题类Weather的定义:

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
/**
*
* 天气的枚举
*
*/
public enum WeatherType {

SUNNY, RAINY, WINDY, COLD;

@Override
public String toString() {
return this.name().toLowerCase();
}
}
/**
*
* Weather can be observed by implementing {@link WeatherObserver} interface and registering as
* listener.
*
*/
public class Weather {

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

private WeatherType currentWeather;
private List<WeatherObserver> observers;

public Weather() {
observers = new ArrayList<>();
currentWeather = WeatherType.SUNNY;
}

public void addObserver(WeatherObserver obs) {
observers.add(obs);
}

public void removeObserver(WeatherObserver obs) {
observers.remove(obs);
}

/**
* Makes time pass for weather
*/
public void timePasses() {
WeatherType[] enumValues = WeatherType.values();
currentWeather = enumValues[(currentWeather.ordinal() + 1) % enumValues.length];
LOGGER.info("The weather changed to {}.", currentWeather);
notifyObservers();
}

private void notifyObservers() {
for (WeatherObserver obs : observers) {
obs.update(currentWeather);
}
}
}

测试类的代码:

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

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

/**
* Program entry point
*
* @param args command line args
*/
public static void main(String[] args) {

//1. 定义了一个主题
Weather weather = new Weather();
//2. 添加两个观察者
weather.addObserver(new Orcs());
weather.addObserver(new Hobbits());

//3. 当主题改变时看观察者的变化
weather.timePasses();
weather.timePasses();
weather.timePasses();
weather.timePasses();

}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
17:27:44.181 [main] INFO com.iluwatar.observer.Weather - The weather changed to rainy.
17:27:44.185 [main] INFO com.iluwatar.observer.Orcs - The orcs are dripping wet.
17:27:44.186 [main] INFO com.iluwatar.observer.Hobbits - The hobbits look for cover from the rain.
17:27:44.186 [main] INFO com.iluwatar.observer.Weather - The weather changed to windy.
17:27:44.186 [main] INFO com.iluwatar.observer.Orcs - The orc smell almost vanishes in the wind.
17:27:44.186 [main] INFO com.iluwatar.observer.Hobbits - The hobbits hold their hats tightly in the windy weather.
17:27:44.186 [main] INFO com.iluwatar.observer.Weather - The weather changed to cold.
17:27:44.186 [main] INFO com.iluwatar.observer.Orcs - The orcs are freezing cold.
17:27:44.186 [main] INFO com.iluwatar.observer.Hobbits - The hobbits are shivering in the cold weather.
17:27:44.186 [main] INFO com.iluwatar.observer.Weather - The weather changed to sunny.
17:27:44.186 [main] INFO com.iluwatar.observer.Orcs - The sun hurts the orcs' eyes.
17:27:44.186 [main] INFO com.iluwatar.observer.Hobbits - The happy hobbits bade in the warm sun.

JVM垃圾收集器

发表于 2017-08-22 | 分类于 java , JVM

垃圾收集器就是将垃圾回收算法进行了具体的实现,Java虚拟机规范中对于垃圾收集器并没有做任何规定,因此不同的厂商可以自由的实现自己的垃圾收集器。
下图展示的为HotSpot虚拟机的垃圾收集器,如果两个收集器之间有连线,则表示两者可以配合使用,反之,则不能配合使用。
HotSpot虚拟机的垃圾收集器
从图中可以看出,Serial、ParNew、Parallel Scavenge三个收集器负责收集新生代;CMS、Serial Old、Parallel Old负责老年代的垃圾收集;G1收集器既可以收集老年代又可以收集新生代。

Serial收集器

该收集器采用“单线程”、串行的方式进行垃圾收集。它这个”单线程”不止是说只是用一条线程来进行工作,最重要的是它进行垃圾收集时,它会暂停掉其他所有的工作线程,直到收集工作结束。新生代采用复制算法,老年代采用标记整理算法。
Serial收集器的优点是简单和高效,但是其在收集时会”Stop The World!”。
Serial收集器工作示意图:
Serial收集器工作示意图

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了多线程以外,该收集器与串行收集器相比并没有太多的创新之处,但是它是Server模式下首选的新生代收集器。从图中可以看出,目前除了Serial收集器以外,只有ParNew可以和CMS收集器一起工作,CMS收集器负责老年代的垃圾收集。ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC 选项来强制指定它。
ParNew收集器运行示意图:
ParNew收集器运行示意图
垃圾收集中,并行与并发的含义:

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

Parallel Scavenge收集器

该收集器关注的点与CMS不同。CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间;Parallel Scavenge收集器关注的是吞吐量的可控。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间短更适合与用户交互的程序(CMS),而吞吐量高,则可以更好的利用CPU资源,适合于在后台运算,而不需要太多的交互的任务。(Parallel Scavenge)
在使用时Parallel Scavenge有几个参数可以设置:

  • -XX:MaxGCPauseMillis。用来设置垃圾回收停顿间隔,单位是毫秒。
  • -XX:GCTimeRatio。直接设置吞吐量,是一个大于0小于100的整数,吞吐量的概念在上面已经介绍过了。
  • -XX:+UseAdaptiveSizePolicy。使用自适应的调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。该策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

上面三个收集器都是新生代的收集器,下面这将会介绍老年代的垃圾收集器。

Serial Old收集器

穿行收集器的老年代版本,使用单线程进行垃圾收集了。在进行垃圾收集时也需要暂停所有用户线程。
Serial Old收集器的运行示意图如图所示:
Serial Old老年代的运行示意图

Parallel Old收集器

该收集器可以与Parallel Scavenge收集器一起工作,在注重“吞吐量优先”以及CPU资源敏感的场合,优先考虑使用Parallel Scavenge加Parallel Old收集器。
Parallel Old收集器的工作流如图所示:
Parallel Old收集器的工作流程图

CMS收集器

CMS(Concurrent Mark Sweep)收集器的目标是缩短用户线程的停顿时间,在注重用户交互的服务器端,可以采用该收集器来进行老年代的收集。
该收集器是基于“标记-清除”算法来实现的,总共分为四个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)
    初始标记、重新标记两个步骤仍然需要暂停所有用户线程。初始标记仅仅标记GC Roots能够直接关联的对象,速度很快;并发标记阶段是进行GC RootsTracing的过程;重新标记是标记那些由于用户程序继续运行而产生变化的那部分对象的标记记录。
    由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
    CMS收集器运行示意图
    CMS收集器的缺点:
  • CMS收集器对CPU资源非常敏感,由于与用户线程并发运行,因此也降低了系统的吞吐量。
  • CMS收集器无法收集浮动垃圾(Floating Garbage),浮动垃圾指的就是由于CMS并发清理阶段用户线程还在运行着,这部分产生的来及只能等到下次GC在进行收集。
  • 由于使用的是“标记-清除”算法,在进行清除后可能会产生的大量的内存碎片,从而导致无法进行大对象的分配。

G1收集器

G1收集器的特点如下:

  • 并行与并发。G1能充分利用多CPU、 多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集。与其他收集器一样,分代概念在G1中依然得以保留。 虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、 熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合。与CMS的“标记—清除”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。 这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
    G1收集器主要包括初始标记(Initial Marking)、并发标记(Concurrent Marking)、最终标记(Final Marking)、筛选回收(Live Data Counting and Evacuation)。
    G1收集器的工作流程图:
    G1收集器的工作流程图

总结

本文主要介绍了JVM中的几种垃圾收集器的工作方式和原理,分别对于老年代和新生代进行收集。其中,新生代的垃圾收集器包括Serial 、ParNew 、Parallel Scavenge三种,老年代的垃圾收集器主要有Serial Old、Parallel Old以及CMS收集器。CMS收集器更注重的是降低用户线程的停顿时间,而Parallel Scavenge收集器更注重的是吞吐量(用户线程的运行时间/运行总时间)。最后,对G1收集器进行了介绍,描述了该收集器的一些特点。以后会对这些垃圾收集器进行实验分析,敬请期待!

参考资料

  • 深入理解java虚拟机
  • JVM 垃圾回收器工作原理及使用实例介绍

JVM的垃圾回收机制

发表于 2017-08-22 | 分类于 java , JVM

JVM垃圾回收算法概述

Java 语言的一大特点就是可以进行自动垃圾回收处理,而无需开发人员过于关注系统资源,例如内存资源的释放情况。自动垃圾收集虽然大大减轻了开发人员的工作量,但是也增加了软件系统的负担。
Java在进行垃圾回收之前需要考虑两个问题,即:

  1. 哪些内存需要回收?
  2. 如何回收?
    其中,第一个问题描述的是如何判断对象已经不需要了,死了,可以回收了。第二个问题则解决了采用哪种垃圾回收算法来回收。下面详细的介绍上述3个问题的解决方法。

    如何判断对象可以进行回收?

    判断对象”已死”通常有引用计数法和可达性分析算法两种方法。

    引用计数法

    引用计数器的实现很简单,对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1,当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,则对象 A 就不可能再被使用。
    可以看出,引用计数法很简单,但是存在一种“循环引用”的问题。例如:有对象 A 和对象 B,对象 A 中含有对象 B 的引用,对象 B 中含有对象 A 的引用。此时,对象 A 和对象 B 的引用计数器都不为 0。但是在系统中却不存在任何第 3 个对象引用他们。也就是说,A 和 B 是应该被回收的垃圾对象,但由于垃圾对象间相互引用,从而使垃圾回收器无法识别,引起内存泄漏。

    可达性分析算法

    在主流的商用程序语言(Java、 C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。 这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
    可作为GC Roots的对象有:
    • 虚拟机栈(栈中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象

      对方法区(永久代)的回收

      永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。 对于常量回收,只要这个常量没有被任何引用,那么就可以进行垃圾回收。要判断一个类是否是无用的类的条件则要苛刻的多,满足以下条件:
    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
    • 加载该类的ClassLoader已经被回收。
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

      垃圾回收算法

      标记-清除算法

      该算法分为两个阶段,即标记和清除。通过可达性分析算法,标记的阶段就是找出可以回收的对象。清除就是将这些区域重新可以使用。
      该算法有两个不足:
    • 效率问题。标记和清除的效率都不高。
    • 空间问题。进行标记-清除算法后,将会产生不连续的内存碎片,可能导致无法分配大对象而又进行垃圾收集。
      算法的描述如图所示:
      标记清除算法图

      复制算法

      为了解决效率问题,该算法将内存区域划分为两个相同的部分,记为A和B。一次只使用其中的一半,在需要对A进行垃圾收集时,则将A中存活的对象复制到区域B,然后对A进行清空即可。
      该算法不会产生碎片,实现简单,运行高效。但是该算法使得可用的内存下降为一半,代价太高了。
      算法的描述如图所示:
      复制算法图
      现在的虚拟机通常采用该算法进行新生代的垃圾收集,然而并不是按照1:1的比例来进行内存划分,而是将内存划分为一块较大的Eden区和两个较小的Survivor区域(如A和B),每次进行内存分配只在Eden区和Survivor A区进行。当进行垃圾回收时,将上述两个区域中存活的对象全部复制到Survivor B区域,最后,对这两个区域进行清空。
      复制算法存在一种内存担保的现象,指的是当Eden和Survivor A区域存货的对象太多,Survivor B中放不下了,那么这些对象将直接进入老年代。

      标记-整理算法

      复制算法在对象存活率比较高的情况下,效率降低,因此复制算法不适用于老年代的垃圾收集。
      根据老年代的特点,标记-整理算法在对存活的对象进行标记后,不是将对象进行清除,而是将对象有序的向一端移动,然后清理掉边界以外的内存。
      算法的描述图如下:
      标记整理图

      分代收集算法

      分代收集指的是根据Java中不同区域中的对象的生存时间不同,将对象分为新生代和老年代。针对不同的区域,来使用不同的收集算法。例如:新生代每次存活的对象很少,那么就可以采用复制算法来对该区域进行垃圾回收;老年代每次收集时存活的对象较多,那么就可以采用标记-清除或者标记-整理算法来进行收集。

总结

本文介绍了Java中的垃圾收集机制以及常见的垃圾收集算法。Java的垃圾收集机制是Java的一个重要特性,用户无需手动释放内存,Java的GC机制能够满足大多数场景对于内存分配的要求。同时,本文在介绍了Java垃圾回收需要解决的问题,即如何判断对象已经可以回收以及如何进行回收(垃圾回收算法)。

JVM中的内存区域

发表于 2017-08-22 | 分类于 java , JVM

JVM运行时的数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。 这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。 根据《Java虚拟机规范(JavaSE 7版)》 的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图所示:
jvm运行时数据区
从图中可以看出方法区、堆由所有的线程共享;虚拟机栈、本地方法栈、程序计数器是线程隔离的。

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。 在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、 循环、 跳转、 异常处理、 线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。 因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、 操作数栈、 动态链接、 方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
人们平时所说的栈指的就是虚拟机栈,局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在此块区域有两种异常情况:

  • StackOverflowError异常。如果线程请求的栈深度大于虚拟机所允许的深度。
  • OutOfMemoryError异常。如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。 与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

Java堆

  • 对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
  • 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 目前也有栈上分配的一些技术。
  • 从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、 From Survivor空间、 To Survivor空间等。
  • 从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。 不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
  • Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。 在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。
  • 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

    方法区

    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据。 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
    Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。 相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的。

    运行时常量池

    运行时常量池(Runtime Constant Pool)是方法区的一部分。 Class文件中除了有类的版本、 字段、 方法、 接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
    Java虚拟机对Class文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、 装载和执行,但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。 不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
    运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

    直接内存(NIO分配)

    直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。 但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
    显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。 服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

    Java对象的创建

  • 首先,虚拟机遇到一条new指令时,将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、 解析和初始化过。 如果没有,那必须先执行相应的类加载过程。
  • 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。分配内存的方式有指针碰撞和空闲列表两种方式。
  • 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、 如何才能找到类的元数据信息、 对象的哈希码、 对象的GC分代年龄等信息。
  • 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始——<init>方法还没有执行,所有的字段都还为零。执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

    参考资料

  • 深入理解java虚拟机

查询当前的数据库中的表名称

发表于 2017-08-22 | 分类于 数据库

前段时间做项目,需要获取到不同的数据库中,当前登录的用户的表的名称。数据库类型包括MySQL、Oracle、Hive数据库,这三种数据库获取当前表名称的方法不同,总结如下。

Hive获取当前用户的表名称

  1. 使用show tables

    1
    show tables like '*name*'; -- 显示当前数据库中所有表的名称
  2. 由于Hive的元数据是存储在mysql中,可以从元数据库中获取到表的相关信息。获取表的信息主要是从dbs和tbls两个表中获取。两个表中的字段如下图所示:
    dbs表中的字段
    tbls表中的字段

两张表通过db_id这个字段进行连接,可以得到每个库中有那些表:

1
select b.tbl_name tableName from dbs a join tbls b on(a.db_id = b.db_id) where a.name = ?

mysql获取当前库的表名称

  1. mysql也可以用show tables的方式
  2. 从information_schema.tables这张表中获取。
    1
    2
    select table_name tableName from information_schema.tables s where s.table_schema = database()
    -- database()函数为获取当前所用的数据库。

oracle获取当前用户的表名称

oracle没有库的概念,oracle的用户相当于库。也就是一个用户下可以有那些表。可以使用如下的sql语句查询:

1
select table_name tableName from user_tables

参考资料

  • hive表信息查询:查看表结构、表操作等
  • [一起学Hive]之十四-Hive的元数据表结构详解

java动态代理总结

发表于 2017-08-21 | 分类于 java

动态代理介绍

什么是java的动态代理机制?

Java 动态代理机制的出现,使得 Java 开发人员不用手工编写代理类,只要简单地指定一组接口及委托类对象,便能动态地获得代理类。代理类会负责将所有的方法调用分派到委托对象上反射执行,在分派执行的过程中,开发人员还可以按需调整委托类对象及其功能,这是一套非常灵活有弹性的代理框架。

通过Java的动态代理,我们可以使用一个代理对象来增强一个目标对象的功能。例如Spring的AOP就是基于动态代理实现的,通过动态代理,我们可以在每个方法之前或者之后进行额外的处理,最常见的有事务管理、日志记录等功能。

设计模式之代理模式?

代理是一种常用的设计模式,其目的就是为需要代理的对象提供一个代理,以控制对某个对象的访问。代理类可以为目标类进行预处理、过滤转发等操作。

如上图所示,代理模式的Client向Subject对象发送一个请求,代理类ProxySubject将替代RealSubject进行处理。可以看出ProxySubject除了doSomething()方法之外还可以doOtherThing(),在不需要修改类的源码时增强了类的功能。
通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。

java动态代理的实现

相关的类和接口

要了解 Java 动态代理的机制,首先需要了解以下相关的类或接口。

  • java.lang.reflect.Proxy:这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 方法 1: 该方法用于获取指定代理对象所关联的调用处理器
    static InvocationHandler getInvocationHandler(Object proxy)

    // 方法 2:该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象
    static Class getProxyClass(ClassLoader loader, Class[] interfaces)

    // 方法 3:该方法用于判断指定类对象是否是一个动态代理类
    static boolean isProxyClass(Class cl)

    // 方法 4:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
    static Object newProxyInstance(ClassLoader loader, Class[] interfaces,
    InvocationHandler h)
  • java.lang.reflect.InvocationHandler:这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对目标类的代理访问。

    1
    2
    3
    4
    5
    // 该方法负责集中处理动态代理类上的所有方法调用。
    // 第一个参数既是代理类实例,
    // 第二个参数是被调用的方法对象,第三个参数是调用参数。
    // 调用处理器根据这三个参数进行预处理或分派到委托类实例上执行
    Object invoke(Object proxy, Method method, Object[] args)

每次生成动态代理类对象时,都需要指定一个实现了InvocationHandler调用处理器对象。

  • java.lang.ClassLoader:这是类装载器类,负责将类的字节码装载到 Java 虚拟机(JVM)中并为其定义类对象,然后该类才能被使用。Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中。

java动态代理的步骤以及特点

Java动态代理的步骤

  1. 通过实现 InvocationHandler 接口创建自己的调用处理器;
  2. 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
  3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
  4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
    代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用
    InvocationHandler handler = new InvocationHandlerImpl(..);

    // 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象
    Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... });

    // 通过反射从生成的类对象获得构造函数对象
    Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class });

    // 通过构造函数对象创建动态代理类实例
    Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });

实际使用过程更加简单,因为 Proxy 的静态方法 newProxyInstance 已经为我们封装了步骤 2 到步骤 4 的过程,所以简化后的过程如下

1
2
3
4
5
6
7
// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
InvocationHandler handler = new InvocationHandlerImpl(..);

// 通过 Proxy 直接创建动态代理类实例
Interface proxy = (Interface)Proxy.newProxyInstance( classLoader,
new Class[] { Interface.class },
handler );

Java动态代理类本身的特点

  1. 包:如果所代理的接口都是 public 的,那么它将被定义在顶层包(即包路径为空);如果所代理的接口中有非 public 的接口(因为接口不能被定义为 protect 或 private,所以除 public 之外就是默认的 package 访问级别),那么它将被定义在该接口所在包,这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;
  2. 类修饰符:该代理类具有 final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;
  3. 类名:格式是“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类.
  4. 生成的代理类的继承关系
    生成的代理类的继承关系
    从图中可以看出,所有的代理类都继承自Proxy基类,并实现了代理的接口。

代理类实例的一些特点

  1. 每个实例都会关联一个调用处理器对象,可以通过 Proxy 提供的静态方法 getInvocationHandler 去获得代理类实例的调用处理器对象。在代理类实例上调用其代理的接口中所声明的方法时,这些方法最终都会由调用处理器的 invoke 方法执行。
  2. 值得注意的是,代理类的根类 java.lang.Object 中有三个方法也同样会被分派到调用处理器的 invoke 方法执行,它们是 hashCode,equals 和 toString,可能的原因有:一是因为这些方法为 public 且非 final 类型,能够被代理类覆盖;二是因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行。
  3. 当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。

异常处理的特点

从调用处理器接口声明的方法中可以看到理论上它能够抛出任何类型的异常,因为所有的异常都继承于 Throwable 接口,但事实是否如此呢?答案是否定的,原因是我们必须遵守一个继承原则:即子类覆盖父类或实现父接口的方法时,抛出的异常必须在原方法支持的异常列表之内。所以虽然调用处理器理论上讲能够,但实际上往往受限制,除非父接口中的方法支持抛 Throwable 异常。那么如果在 invoke 方法中的确产生了接口方法声明中不支持的异常,那将如何呢?放心,Java 动态代理类已经为我们设计好了解决方法:它将会抛出 UndeclaredThrowableException 异常。这个异常是一个 RuntimeException 类型,所以不会引起编译错误。通过该异常的 getCause 方法,还可以获得原来那个不受支持的异常对象,以便于错误诊断。

动态代理的实现分析

首先,咱们来看看Proxy的构造方法。Proxy类有两个构造方法,一个无参的私有构造方法,以及一个protect的参数为一个InvocationHandler的构造方法:

1
2
3
4
5
// 由于 Proxy 内部从不直接调用构造函数,所以 private 类型意味着禁止任何调用
private Proxy() {}

// 由于 Proxy 内部从不直接调用构造函数,所以 protected 意味着只有子类可以调用
protected Proxy(InvocationHandler h) {this.h = h;}

再来看看Proxy对象最核心的创建代理对象的Proxy.newInstance(ClassLoader classLoader, Class<?>[] interfaces, InvocationHandler h )方法:

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
/**
* 返回指定接口的代理类的实例将方法调用分派给指定的调用处理程序。
* @param loader 定义代理类的classloader
* @param interfaces 代理类实现的接口
* @param h 该代理类指定的InvocationHandler
* @return 通过给定的invocationHandler,返回一个代理实例。这个代理实例是由
* classoader来定义,并且实现了指定的接口
*/
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
//首先校验InvocationHandler是否为null,如果为null则抛出npe
Objects.requireNonNull(h);

//获取要实现的接口
final Class<?>[] intfs = interfaces.clone();
//系统安全检查
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}

/*
* 查找或生成指定的代理类的Class对象。
*/
Class<?> cl = getProxyClass0(loader, intfs);

/*
*通过指定的InvocationHandler来构造代理类
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
//获取到代理类的构造函数,
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
// 并通过构造函数来生成代理类的实例
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}

java动态代理存在的问题

诚然,Proxy 已经设计得非常优美,但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持 interface 代理的桎梏,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫 Proxy。Java 的继承机制注定了这些动态代理类们无法实现对 class 的动态代理,原因是多继承在 Java 中本质上就行不通。
有很多条理由,人们可以否定对 class 代理的必要性,但是同样有一些理由,相信支持 class 动态代理会更美好。接口和类的划分,本就不是很明显,只是到了 Java 中才变得如此的细化。如果只从方法的声明及是否被定义来考量,有一种两者的混合体,它的名字叫抽象类。实现对抽象类的动态代理,相信也有其内在的价值。此外,还有一些历史遗留的类,它们将因为没有实现任何接口而从此与动态代理永世无缘。如此种种,不得不说是一个小小的遗憾。

编程原则

发表于 2017-08-18 | 分类于 设计模式

原始链接principle

每一个程序员都应该理解并使用编程原则以及设计模式,并能从中受益。

通用原则

KISS(Keep It Simple Stupid)

大多数系统在保持简单而不是复杂的情况下运行得最好。

为什么这么做

  • 代码越少,编写的时间就越少,bug也就越少,并且更容易修改。
  • Simplicity is the ultimate sophistication.(简化是最终的高雅?)
  • 完美并不是说系统达到了无法添加的程度,而是指达到了没有任何可以继续简化的程度。

    参考资料

  • KISS Principle
  • Keep It Simple Stupid (KISS)

    YAGNI(You aren’t gonna need it)

    不要实现没有必要实现的东西。

为什么这么做

  • 为了完成以后需要来完成的功能付出努力,意味着你会减低为当前迭代周期付出的努力。
  • 可能会导致代码膨胀,软件将会变得越来越复杂。

    如何做

  • 只实现当前系统中必须实现的东西而不是那些你所能遇见到的将来可能会需要来实现的,那些东西以后再实现。

    参考资料

  • http://c2.com/xp/YouArentGonnaNeedIt.html
  • http://www.xprogramming.com/Practices/PracNotNeed.html
  • http://en.wikipedia.org/wiki/You_ain’t_gonna_need_it

做最简单的事(Do The Simplest Thing That Could Possibly Work)

为什么这么做

  • 为这个问题寻找最简单的解决方案,而不是将他复杂化。这样我们可以回归到问题的本身。

    如何做

  • 问你自己:“解决这个问题最简单的方案是什么?“

    参考资料

  • Do The Simplest Thing That Could Possibly Work

关注点分离(Separation of Concerns)

关注点分离是一种设计原则,用于将计算机程序分离成不同的部分,这样每个部分都处理一个单独的关注点。例如,应用程序的业务逻辑是一个令人关注的问题,而用户界面则是另一个关注点。更改用户界面不需要对业务逻辑进行更改,反之亦然。
Edsger W. Dijkstra

It is what I sometimes have called “the separation of concerns”, which, even if not perfectly possible, is yet the only available technique for effective ordering of one’s thoughts, that I know of. This is what I mean by “focusing one’s attention upon some aspect”: it does not mean ignoring the other aspects, it is just doing justice to the fact that from this aspect’s point of view, the other is irrelevant.

为什么这么做

  • 简化软件应用程序的开发和维护
  • 当关注点分离时,单独的部分可以被重用,也可以独立地开发和更新。

    如何做

  • 将程序功能分解成多个模块,尽可能少地重叠。但是也要注意别分的太散,不然将带来更大的复杂度。

    参考资料

  • Separation of Concerns

    保持代码干燥,尽量减少重复代码(Keep Things DRY)

    程序中的每一个部分都必须在一个系统中是单一的、明确的、权威的表示。程序中的每一项重要功能都应该只在一个地方实现。在不同的代码片段中执行相似的功能时,将他们重合的部分提取出来。

    为什么这么做

  • 重复(无意的或有目的的重复)会导致代码难以维护、分解以及逻辑上的矛盾。
  • 对系统中的任何单个功能进行修改时,不需要对其他逻辑无关的功能代码进行更改。
  • 此外,逻辑上相关的代码的改变都是可预测以及一致的,因此需要保持同步。

    如何做

  • 将业务规则、长表达式、if语句、数学公式、元数据等放在一个地方。
  • 确定系统中所使用的单一的、确定性的知识源,然后使用该源生成该知识的可应用实例(代码、文档、测试等)
  • 应用 Rule of three原则).

    参考资料

  • Dont Repeat Yourself

    写代码的时候考虑代码的可维护性(Code For The Maintainer)

    为什么这么做

  • 到目前为止,维护是任何项目中最昂贵的阶段。

    如何做

  • 成为一个代码的维护者。
  • 把维护你的代码的人想象成一个知道你住处的精神病患者,如果你的代码写的不好随时都可能过来干掉你。
  • 确定系统中所使用的单一的、确定性的知识源,然后使用该源生成该知识的可应用实例(代码、文档、测试等)
  • 良好的编写代码和注释,如果一个新手拿起代码,他们就会乐于阅读和学习。
  • Don’t make me think.
  • Use the Principle of Least Astonishment

    参考资料

  • http://c2.com/cgi/wiki?CodeForTheMaintainer
  • http://blog.codinghorror.com/the-noble-art-of-maintenance-programming/

避免过早优化(Avoid Premature Optimization)

Donald Knuth:

程序员浪费了大量的时间去思考,或者担心他们的程序的非关键部分的速度。而当考虑到调试和维护时,这些效率的尝试实际上会产生强烈的负面影响。我们应该忘记小的效率,大约97%的时间:过早的优化是所有邪恶的根源。然而,我们不应在那关键的3%中放弃我们的机会。

当然,理解什么不是“过早”是至关重要的

为什么这么做

  • 在开发的早期,影响系统性能的问题并不会浮现的很清楚。
  • 在优化之后,代码可能更难读懂和维护。

    如何做

  • Make It Work Make It Right Make It Fast
  • 只有在你真正需要的时候才去优化代码,并且在你明白系统的瓶颈后才去优化他。

    参考资料

  • http://en.wikipedia.org/wiki/Program_optimization
  • http://c2.com/cgi/wiki?PrematureOptimization

    重构代码时需要考虑的问题——童子军原则(Boy-Scout Rule)

    为什么这么做

  • 当对现有代码库进行更改时,有可能会降低代码的质量。

    如何做

  • 确保每次提交都不会降低代码的质量。
  • 当看到一些代码不像它应该的那样清晰,应该设法修复它。

    参考资料

  • http://martinfowler.com/bliki/OpportunisticRefactoring.html

    模块与模块(类与类)之间的设计原则(Inter-Module/Class)

    最小化代码的耦合程度(Minimise Coupling)

    模块/组件之间的耦合是它们相互依赖的程度;低耦合是更好的。换句话说,耦合就是在修改模块A后模块B”中断“的概率。

    为什么这么做

  • 一个模块的更改通常会导致其他模块的更改产生连锁反应。
  • 模块的组合可能需要更多的努力和/或时间,因为模块之间的依赖性增加了。
  • 由于必须包含依赖模块,一个特定的模块可能更难复用和测试。
  • 开发人员可能害怕更改代码,因为他们不确定可能会带来什么影响。

    如何做

  • 消除、最小化和减少必要的关系的复杂性。
  • 通过隐藏实现的细节,耦合程度就降低了。
  • 应用迪米特法则(迪米特法则)。

    参考资料

  • Coupling)
  • Coupling And Cohesion

(迪米特法则)Law of Demeter

不要和陌生人说话(Don’t talk to strangers)

为什么

  • 这样做会增加了耦合的程度
  • 这样做会暴露太多的实现细节

    如何做

  • 限制一个对象中的方法,让它只能访问:
    • 这个对象本身
    • 这个方法的参数
    • 在这个方法中创建的对象
    • 这个对象的属性和域。

      参考资料

  • 迪米特法则
  • The Law of Demeter Is Not A Dot Counting Exercise

组合优先用于继承(Composition Over Inheritance)

原因

  • 降低类之间的耦合
  • 使用继承,子类可以轻松地做出假设,并打破里氏替换原则(LSP)。

    如何做

  • 对LSP(可替代性)进行测试,以决定什么时候继承。
  • 当两个类之间的关系是”has a”或者”uses a”时使用组合,当时”is a”时才使用继承。

    参考资料

  • Favor Composition Over Inheritance

    正交原则(Orthogonality)

    正交性的基本概念是,在概念上不相关的事物在系统中也应该是不相关的。
    正交性与简单性有关:设计越正交,异常就越少。这使得用编程语言学习、阅读和编写程序变得更加容易。正交特征的意义是独立于上下文的;关键的参数是对称性和一致性。

(鲁棒性原则) Robustness Principle

Be conservative in what you do, be liberal in what you accept from others

原因

  • 为了能够改进服务,您需要确保服务提供者能够做出更改以支持新需求,同时对现有客户造成最小的破坏。

    如何做

  • 向其他机器发送命令或数据的代码(或在同一台机器上的其他程序)应该完全符合规范,但是接收输入的服务应该接受不符合条件的输入,只要其含义是明确的。(在输入不符合规范时,也能够进行处理)。

    控制反转(Inversion of Control)

    控制反转是一种设计原则,他能够在一个框架中,将自定义的功能交由一个容器来控制。控制反转使得可重用代码和解决特定问题的代码可以同时使用,并让他们可以在一个应用中进行协作,降低了系统的耦合。

控制反转好处

  • 控制反转是用来提高系统的模块化,并使得系统更容易拓展。
  • 将任务的执行和实现进行了分离。
  • 将一个模块集中在它所实现的任务上。
  • 将模块从假设中解放出来,不依赖其他系统是如何做的,而是依赖于合同。
  • 防止更换模块产生的副作用。

    控制反转实现方法

  • 使用工厂模式( Service Locator pattern)
  • 使用服务定位器模式( Service Locator pattern)
  • 使用依赖注入(Dependency Injection)
  • 使用上下文查找(Dependency Injection)
  • 使用模板方法模式(Template Method pattern)
  • 使用策略模式(Strategy pattern)

    参考资料

  • Inversion of Control in Wikipedia
  • Inversion of Control Containers and the Dependency Injection pattern

    模块/类的设计原则(Module/Class)

    高内聚(Maximise Cohesion)

    Cohesion of a single module/component is the degree to which its responsibilities form a meaningful unit; higher cohesion is better.

高内聚的缺点

  • 增加了理解模块的难度。
  • 系统维护的难度增加了,因为域的逻辑变化会影响多个模块,并且一个模块的更改可能会需要相关模块的更改。
  • 重用模块的难度增加,因为大多数应用程序不需要模块提供的随机操作集。

    实现高内聚

  • 将相关联的功能聚合在一起,比如说一个class中。

    里氏替换原则(Liskov Substitution Principle)

    Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

java中的多态,父类引用指向子类对象。

参考资料

  • Liskov substitution principle
  • Liskov Substitution Principle

    开闭原则(Open/Closed Principle)

    类在设计时应该对继承开放,并且对修改封闭。这样的类可以在不修改源代码的情况下修改其行为。

    好处

  • 通过对当前的代码改变最小,提高了系统的可维护性和稳定性。

    如何做

  • 编写可以扩展的类(而不是可以修改的类)。
  • 只暴露需要更改的移动部分,隐藏其他所有内容。

    参考资料

  • Open Closed Principle
  • The Open Closed Principle

    单一职责原则(Single Responsibility Principle)

    一个类永远不应该有超过一个理由去改变。
    每个类都应该有一个单独的职责,而这个职责应该完全由类来封装,而且只有这一个职责、

    好处

  • 提高可维护性:只要修改一个模块或者类。

    如何做

  • 使用Curly’s Law.

    参考资料

  • Single responsibility principle

    隐藏具体的实现细节(Hide Implementation Details)

    一个软件模块通过提供一个接口来隐藏信息(即实现细节),而不泄漏任何不必要的信息。

    好处

  • 当实现更改时,客户端所使用的是接口,不需要更改。(多态)

    如何做

  • 最小化类和成员的作用域。
  • 不要公开公开成员数据。
  • 避免将私有实现细节放入类的接口中。
  • 减少耦合,以隐藏更多的实现细节。

    参考资料

  • Information hiding

    Curly’s Law

    参考资料

  • Curly’s Law: Do One Thing
  • The Rule of One or Curly’s Law

    封装变化(Encapsulate What Changes)

    一个好的设计可以识别那些最有可能改变的点,并将它们封装在API背后。当预期的更改发生时,修改将保留在本地。

    好处

  • 当发生更改时,最小化所需的修改

    如何做

  • 封装API背后的变化,而提供不变的访问方式。
  • 尽可能的将不同的改变分发到这个模块本身,让其本身负责。

    参考资料

  • Encapsulate the Concept that Varies
  • Encapsulate What Varies
  • Information Hiding

    接口分割原则(Interface Segregation Principle)

    将一些方法比较多的接口拆分成多个小的接口。接口应该更依赖于调用它的代码,而不是实现它的代码。

    原则描述

    • 如果一个类实现了不需要的方法,调用者就需要知道该类的方法实现。例如,如果一个类实现了一个方法,但是只是简单地抛出,那么调用者将需要知道这个方法实际上不应该被调用。HiveStatement实现了Statement接口,然而现在Hive现在并不打算支持其中的一些方法,只是简单的throw Exception了。

      如何做

  • 避免过于庞大的接口。类不应该实现除了它的职责的方法。

    参考文献

  • Interface segregation principle

    命令查询分离(Command Query Separation)

    命令查询分离原则声明,每个方法都应该要么是执行操作的命令,要么是将数据返回给调用者的查询,而不是两者都执行。
    有了这个原则,程序员就可以更加自信地编写代码了。查询方法可以在任何地方和任何顺序使用,因为它们不会改变状态。有了命令,你必须更加小心。

    如何做

  • 将每个方法作为一个查询或一个命令来实现。
  • 将命名约定应用于方法名,这意味着该方法是一个查询还是一个命令。

    参考文献

  • Command Query Separation in Wikipedia
  • Command Query Separation by Martin Fowler

从源码层面看MyBatis的缓存机制

发表于 2017-08-17 | 分类于 数据库

MyBatis中的缓存

MyBatis 中主要包括一级缓存和二级缓存。一级缓存指的是 session 级别的缓存,MyBatis 每次创建一个数据库连接,则会产生一个数据库访问的 sqlSession ,在这一次会话中执行了两次相同的数据库查询的话,MyBatis在第二次查询的时候会使用之前缓存的数据,从而提高查询效率;二级缓存指的是应用级别的缓存,开启缓存后,每一个 Mapper 下的查询都会使用缓存数据,二级缓存可以使用自定义实现,也可以使用第三方提供的缓存,如 EHCache 等。

一级缓存

一级缓存的实现

MyBatis 在进行一次查询时,主要包括如下几个步骤:

  1. 使用SqlSessionFactoryBuilder从 XML 中读取配置信息,构造一个 SqlSessionFactory 对象。
  2. SqlSessionFactory 对象调用 openSession 方法,开启一次数据库会话。
  3. session 中持有了一个 Executor 的引用用来执行数据库操作。

在上述描述中,查询缓存是否命中是在 executor 执行 sql 时进行的。每个executor 中都包含一个PerpetualCache的缓存,该缓存使用一个简单的HashMap 来实现。在进行查询时首先会查询缓存有没有命中,如果命中,则直接返回结果;否则,从数据库中进行查询并放到缓存中。
Executor在执行查询时的代码如下:

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
 @Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//创建缓存的key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
//调用下面的query方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//omitted...
List<E> list;
try {
queryStack++;
//首先从缓存中取数据
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 从数据库中查询,并返回
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
//omitted...
}

缓存的key的创建使用的是createCacheKey方法,该方法的实现代码如下:

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
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
//添加MappedStatement的id
cacheKey.update(ms.getId());
//添加rowBound信息,分页时用的
cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
////添加rowBound信息,分页时用的
cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
//添加sql信息
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
//添加查询的参数信息
cacheKey.update(value);
}
}
//如果Configuration的环境不为空,则把环境的id也加上
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;

从代码中可以看出,缓存的key由以下几部分组成。

  • MappedStatement对象的id。
  • 分页信息RowBound。
  • sql 语句。
  • sql语句中传递的参数
  • 环境名environment
    最后生成的key大约长这样:
    1
    -1994665822:3056142341:com.yrb.mybatis.mapper.BookStoreMapper.getBookNames:0:2147483647:select book_name from my_test.book_store where id = ?:4:development

一级缓存的使用和生命周期

一级缓存的使用

一级缓存默认开启,不过可以进行手动清除;MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:

  1. 传入的 statementId
  2. 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示);
  3. 这次查询所产生的最终要传递给JDBC Java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql())
  4. 传递给 java.sql.Statement 要设置的参数值
    3、4两条MyBatis最本质的要求就是 : 调用JDBC的时候,传入的SQL语句要完全相同,传递给JDBC的参数值也要完全相同。
    根据一级缓存的特性,在使用的过程中,我认为应该注意:
  5. 对于数据变化频率很大,并且需要高时效准确性的数据要求,我们使用SqlSession 查询的时候,要控制好SqlSession 的生存时间,SqlSession 的生存时间越长,它其中缓存的数据有可能就越旧,从而造成和真实数据库的误差;同时对于这种情况,用户也可以手动地适时清空 SqlSession 中的缓存;
  6. 对于只执行、并且频繁执行大范围的 select操作的 SqlSession 对象,SqlSession 对象的生存时间不应过长。

一级缓存的生命周期

一级缓存在 session 中创建,因此它的生命周期和session 的生命周期一致。随 session而生,随 session而死。同时注意,在执行更新操作时,也会清空该session的一级缓存。

二级缓存

二级缓存可以看下《深入理解mybatis原理》 MyBatis的二级缓存的设计原理。主要讲解了二级缓存的配置使用以及原理。需要注意的:

  • 缓存的使用顺序二级缓存 -> 一级缓存-> 数据库。
  • 缓存策略有 FIFO、LRU、Scheduled(指定时间清空)。
  • 二级缓存的作用域:以Mapper区分,通常一个Mapper一个Cache,也可以多个Mapper公用一个Cache对象。
  • 二级缓存的实现有三种选择:
    • MyBatis自身提供的缓存实现;
    • 用户自定义的Cache接口实现(实现Cache接口,并在<cache />中指明)
    • 跟第三方内存缓存库的集成(如Ehcache等);

总结

MyBatis可以采用一级缓存和二级缓存;一级缓存不需要配置,默认开启,但是在一些实时性要求较高的应用可能需要手动清空缓存。二级缓存指的是应用级别的缓存,在每一个Mapper都对应一个Cache对象,也可以多个Mapper共有一个;在实际项目中,我们使用的是用Redis来实现自己的缓存。

参考资料

  • 《深入理解mybatis原理》 MyBatis的一级缓存实现详解 及使用注意事项
  • MyBatis Java API
  • 《深入理解mybatis原理》 MyBatis的二级缓存的设计原理

策略模式

发表于 2017-08-16 | 分类于 设计模式

模式定义

策略模式是一种软件设计模式,它可以在运行时动态的选择算法的行为。

例如:现在我们要完成一件事情,可以有多重策略,如策略A、策略B、策略C等。我们的需求是想要在不同的情况下选择使用不同的策略。在Java中,我们可以使用多态来实现这个功能。策略模式还体现了面向接口而不是面向实现来编程的原则。

示例说明

如下图所示,我们需要完成一个屠龙的任务。屠龙者(DragonSlayer)可以有三种策略来完成这个任务,分别为ProjectileStrategy、MeleeStrategy、SpellStrategy。DragonSlayer可以在运行时动态的选择这三种策略来完成屠龙的任务。
类之间的关系图如下所示:

策略模式的类的关系图
可以看出,这三种策略都实现了DragonSlayingStrategy这个接口,这个接口定义了一个execute的方法。

代码说明

1
2
3
4
5
6
/**
* 策略的接口
*/
public interface DragonSlayingStrategy {
void execute();
}

MeleeStrategy:

1
2
3
4
5
6
7
8
9
10
public class MeleeStrategy implements DragonSlayingStrategy {

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

@Override
public void execute() {
//用你的神剑,你切断了龙的头!
LOGGER.info("With your Excalibur you sever the dragon's head!");
}
}

ProjectileStrategy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
*
* Projectile strategy.
*
*/
public class ProjectileStrategy implements DragonSlayingStrategy {

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

@Override
public void execute() {
//你用神奇的弩向龙射击,它倒在地上!
LOGGER.info("You shoot the dragon with the magical crossbow and it falls dead on the ground!");
}
}

SpellStrategy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
*
* Spell strategy.
*
*/
public class SpellStrategy implements DragonSlayingStrategy {

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

@Override
public void execute() {
//你施放了瓦解的魔咒,龙在一堆尘埃中蒸发了!
LOGGER.info("You cast the spell of disintegration and the dragon vaporizes in a pile of dust!");
}
}

下面来看看如何使用这三个策略,DragonSlayer 这个类通过组合的方式在自己的内部持有了一个策略。注意的是,该策略是一个接口,而没有具体指定是哪一个策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DragonSlayer {
//持有了一个策略,并通过构造函数来初始化
private DragonSlayingStrategy strategy;

public DragonSlayer(DragonSlayingStrategy strategy) {
this.strategy = strategy;
}

//改变策略
public void changeStrategy(DragonSlayingStrategy strategy) {
this.strategy = strategy;
}
//战斗吧, 哈哈!
public void goToBattle() {
strategy.execute();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args) {
// GoF Strategy pattern
LOGGER.info("Green dragon spotted ahead!");
DragonSlayer dragonSlayer = new DragonSlayer(new MeleeStrategy());
dragonSlayer.goToBattle();
LOGGER.info("Red dragon emerges.");
dragonSlayer.changeStrategy(new ProjectileStrategy());
dragonSlayer.goToBattle();
LOGGER.info("Black dragon lands before you.");
dragonSlayer.changeStrategy(new SpellStrategy());
dragonSlayer.goToBattle();

// 注意,在新的java8中,使用lambda表达式可以更方便的实现策略模式
//减少了代码量
LOGGER.info("Green dragon spotted ahead!");
dragonSlayer = new DragonSlayer(
() -> LOGGER.info("With your Excalibur you severe the dragon's head!"));
dragonSlayer.goToBattle();
LOGGER.info("Red dragon emerges.");
dragonSlayer.changeStrategy(() -> LOGGER.info(
"You shoot the dragon with the magical crossbow and it falls dead on the ground!"));
dragonSlayer.goToBattle();
LOGGER.info("Black dragon lands before you.");
dragonSlayer.changeStrategy(() -> LOGGER.info(
"You cast the spell of disintegration and the dragon vaporizes in a pile of dust!"));
dragonSlayer.goToBattle();
}
}

输出:

1
2
3
4
5
6
11:02:32.567 [main] INFO com.iluwatar.strategy.App - Green dragon spotted ahead!
11:02:32.616 [main] INFO com.iluwatar.strategy.App - With your Excalibur you severe the dragon's head!
11:02:32.616 [main] INFO com.iluwatar.strategy.App - Red dragon emerges.
11:02:32.616 [main] INFO com.iluwatar.strategy.App - You shoot the dragon with the magical crossbow and it falls dead on the ground!
11:02:32.616 [main] INFO com.iluwatar.strategy.App - Black dragon lands before you.
11:02:32.616 [main] INFO com.iluwatar.strategy.App - You cast the spell of disintegration and the dragon vaporizes in a pile of dust!

12
Robin Yang

Robin Yang

Only the stronger survive in the world.

20 日志
5 分类
11 标签
RSS
© 2020 Robin Yang
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4