0%

前言

Optional 是 Java8 引入的一个重要特性,它是一个容器,里面装着一个可能为空可能不为空的对象。在它出现之前,为避免空指针异常我们可能会这样编码:

1
2
3
4
5
6
7
8
9
10
11
12
public String getLastFour(Employee employee) {
if(employee != null) {
Address address = employee.getPrimaryAddress();
if(address != null) {
ZipCode zip = address.getZipCode();
if(zip != null) {
return zip.getLastFour()
}
}
}
throw new FMLException("Missing data");
}

显然代码嵌套层次很深不够整洁。而用 Optional 将值包裹起来后,我们可以不再关注于值会不会为 null,会不会抛空指针,而将注意力集中在对数据的操作。并且 Optional 提供了 mapflatMapfilter 等方法让我们可以进行函数式风格的编码。那么上诉代码可以如此改写:

1
2
3
4
5
6
public String getLastFour(Optional<Employee> employee) {
return employee.flatMap(employee -> employee.getPrimaryAddress())
.flatMap(address -> address.getZipCode())
.flatMap(zip -> zip.getLastFour())
.orElseThrow(() -> new FMLException("Missing data"));
}

更确切的说,Optional 是一个 Monad 容器。那什么是 Monad 呢?可以参考下面这段话: Think of monads as an object that wraps a value and allows us to apply a set of transformations on that value and get it back out with all the transformations applied. 简单的说,Monad 是一个包裹了一个值的容器(值可以是单个对象也可以是集合),允许我们对该值进行一系列的转换(函数操作)后返回给m我们期望的值。也就是说,Monad 封装了接收函数作为入参的一些方法(filter、map、flatMap 等)。

那么现在,让我们从源码开始一步一步揭开它的神秘面纱。

工厂方法

Optional 使用私有化的构造函数和单例模式提供了值为空的单例,暴露的静态工厂方法为 Optional.empty()。该种设计感觉和 Java 设计模式中的空对象模式有异曲同工之妙,也是避免空指针的核心所在,后面我们将会提到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>();

private Optional() {
this.value = null;
}

public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
阅读全文 »

函数接口

在函数式编程中,纯函数的定义是:

  1. 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关。
  2. 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

为使 Java 支持函数式编程,函数接口Functional Interfaces)正是 Java 8 引入的一个核心概念。函数接口要求接口中只能定义个唯一一个抽象方法。同时,引入一个新的注解: @FunctionalInterface,作为函数接口的标记。

Java 8 提供了四大类函数接口:谓词 Predicate函数 Function提供者 Supplier消费者 Consumer。以及衍生的函数接口,比如 BiFunction<T, U, R> 支持两个入参的函数等等,它们都定义在 java.util.function 包下。

Lambda 表达式

函数式接口的重要属性是:我们能够使用 Lambda 实例化它们。Lambda 表达式的引入给开发者带来了不少优点:在 Java 8 之前,匿名内部类,监听器和事件处理器的使用都显得很冗长,代码可读性很差,Lambda 表达式的应用则使代码变得更加紧凑,可读性增强;Lambda 表达式使并行操作大集合变得很方便,可以充分发挥多核 CPU 的优势,更易于为多核处理器编写代码。

Lambda 表达式由三个部分组成:第一部分为一个括号内用逗号分隔的函数入参;第二部分为箭头符号 ->;第三部分为方法体,可以是表达式和代码块。语法如下:

  • (parameters) ‑> expression 方法体为表达式,该表达式的值作为返回值返回。

  • (parameters) ‑> { statements; } 方法体为代码块,必须用 {} 包裹起来,若该函数需要返回值则需要 return

用 Lambda 表达式替换匿名内部类的写法如下。再进一步,可以使用方法引用Method Reference)简写

阅读全文 »

Preface

单例模式为GOF设计模式中的一种,当我们创建的对象需要昂贵的资源,且该对象又被频繁使用,同时,这个对象的状态是共享的,为了避免资源的浪费以及加载资源的时间,我们往往会引入单例模式。因此,要实现单例模式,其核心就是要在已有该类的实例前提下,阻止其他开发人员再创建类的新实例

在Java语言中,通常提供了如下几种单例模式的实现方式。

懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingletonDemo {
private static SingletonDemo instance;

private SingletonDemo() {
}

public static SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
}

通过提供一个静态的对象 instance,利用 private 权限的构造方法和 getInstance() 方法来给予访问者一个单例。缺点是,没有考虑到线程安全,可能存在多个访问者同时访问,并同时构造了多个对象的问题。之所以叫做懒汉模式,主要是因为此种方法可以非常明显的 lazy loading。针对懒汉模式线程不安全的问题,我们自然想到了,在 getInstance() 方法前加锁,于是就有了第二种实现。

线程安全的懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingletonDemo {
private static SingletonDemo instance;

private SingletonDemo() {
}

public static synchronized SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
}

然而并发其实是一种特殊情况,大多时候这个锁占用的额外资源都浪费了,这种打补丁方式写出来的结构效率很低。

饿汉模式

阅读全文 »

目标

编写一个 Java 的爬虫程序,并行爬取小说网站上的小说。代码已上传至 GitHub。Python 编写的爬虫请见50 行代码实现一个并发的 Python 爬虫程序。本文主要关注:

  • 爬虫程序思路
  • 使用 Gradle 构建项目时遇到的问题以及解决办法
  • 使用 JDK8 的特性如何简便地进行并行化开发

爬虫思路

先说一说自己写这个爬虫时的思路,也算是解决问题的入手点。主要的解析类为 HtmlParser ,负责解析 Html。其实这个程序就是通过 Html 源码解析小说名 Title、小说所含所有章节的 Url、小说每一章的内容,最后将内容按顺序写入文件。

Title

解析小说章节目录网址 Html 源码的 <h1>...</h1> 标签中的值

1
2
3
4
5
6
7
8
// 章节目录网址
http://www.biquge.cm/9/9422/

// html
<h1>大道朝天</h1>

// after parse
大道朝天

Chapter URIs

解析小说章节目录网址 Html 源码的 <div id="list">...</div> 中包含的 <a href="...">

阅读全文 »

Preface

最近我在项目中单独编码了加载配置的模块,并提供给团队里的其他开发者使用。在设计 API 时有些心得体会,遂有了此博客记录下来。

项目结构

截取了部分项目结构,大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// config module

├── ConfigFactory.java
├── YamlConverter.java
└── core
   ├── AbstractConfig.java
   ├── Config.java
   ├── ConfigFile.java
   ├── ConfigLoader.java
   ├── ...
   └── exceptions
├── ConfigFileNotFoundException.java
├── UnsupportedFileTypeException.java
└── ...

ConfigFactoryYamlConverter 都是对外提供 API 的类,故放在最外层目录下,另外有个 core 的文件夹,存放间接使用到的类。类似于操作系统和用户 UI 界面,内核 core 封装你的实现,不暴露给用户,可以非常复杂。外层的 API 提供给用户使用,应该尽量满足不变的原则,即隔离变化,为了避免升级后用户需要改动代码。

易用性

API 应该保持简单易用,尽量可以望文知义。由于现在的 IDE 都有智能感知,会自动提示可调用的方法,一个设计优秀的 API 应该方法名即能显示用途,再不济,用户跟进方法阅读注释或者源码也应知道如何使用。能避免用户必须查阅文档才知道如何使用的应当尽量避免

提高易用性可以从如下几各方面:

静态方法

阅读全文 »