API Design with Java 8

2017 Mar 5

如果返回值可能为null,那么使用Optional类型。 尽量不要把Optional做为接口参数,这会使得接口调用变得很笨拙

public Optional<String> getComment() {
    return Optional.ofNullable(comment);
}

不要使用数组做为返回值或者参数。 使用数组做为返回值时,为了防止调用者修改source数组,常常要先拷贝source数组再返回,效率不高(比如Enum::values())。 使用数组做为参数时,其它线程可能会同时在修改数组。 如果需要强调返回的集合是不可修改时,可以考虑使用Stream做为返回类型。

public Stream<String> comments() {
    return Stream.of(comments);
}

可以考虑增加静态的接口方法来创建接口的实现类。 相比Point point = new PointImpl(1,2);而言,Point point = Point.of(1,2);隐藏了具体的实现类。

考虑使用lambda,而不是继承,来个性化对象的行为。

//不要这样
Reader reader = new AbstractReader() {
    @Override
    public void handleError(IOException ioe) {
        ioe. printStackTrace();
    }
};
//better to expose a static method or a builder in the Reader interface that takes a Consumer<IOException> and applies it to an internal generic ReaderImpl
Reader reader = Reader.builder()
    .withErrorHandler(IOException::printStackTrace)
    .build();

给Functional Interface加上@FunctionalInterface,保证接口只有一个抽象方法。

如果接口要求参数不为null,考虑使用Objects.requireNonNull()做参数验证,可以提前抛出异常。 不要过分考虑带来的性能损失,JVM会优化掉不必要的检查。

public void addToSegment(Segment segment, Point point) {
    Objects.requireNonNull(segment); //可以更早地抛出异常
    Objects.requireNonNull(point);
    segment.add(point);
}

更多详见原文。

Spock,又一种Java测试框架

2017 Mar 3

Spock是用Groovy语言实现的测试框架,可以测试Groovy代码和Java代码。

比JUnit简洁,

class Math extends Specification {
    def "maximum of two numbers"(int a, int b, int c) {
        expect:
        Math.max(a, b) == c

        where:
        a | b | c
        1 | 3 | 3   //passes
        7 | 4 | 4   //fails
        0 | 0 | 0   //passes
    }
}

原生地支持Mocking和Stubbing,不再依赖Mockito之类的mock框架,

def "should send messages to all subscribers"() {
    when:
    publisher.send("hello")

    then:
    1 * subscriber.receive("hello") //subsriber should call receive with "hello" once.
    1 * subscriber2.receive("hello")
}
subscriber.receive(_) >>> ["ok", "error", "error", "ok"]

支持BDD(Behavioral Driven Development),

given: //data initialization goes here (includes creating mocks)
when: //invoke your test subject here and assign it to a variable
then: //assert data here
cleanup: //optional
where: //optional:provide parametrized data (tables or pipes) 

更好的错误提示,

maximum of two numbers   FAILED

Condition not satisfied:

Math.max(a, b) == c
	|    |  |  |  |
	|    7  0  |  7
	42         false

Spock使用了JUnit的runner infrastructure,所以也支持code coverage等报告。

其它参考:

An introduction to Spock
So Why is Spock Such a Big Deal?
Comparing Spock and Junit

String.intern()

2017 Mar 2

通过String.intern(),值相同的字符串们在JVM里可以只占用一个String对象,从而减少内存的消耗。 另外程序使用过的String literal和String类型的常量也会被intern。

“intern”在这里是“拘留”的意思,

n. 实习生,实习医师
vt. 拘留,软禁
vi. 作实习医师

String.intern("foo")就是把字符串“foo”“拘留”在String类内部的一个pool里。

String pool实际上是一个固定大小的哈希表。 在Java 7之后,String pool占用的是堆(heap)内存。

因为哈希表是固定大小的,不会自动扩容,所以大小的设置很重要。 如果相比于要intern的字符串数目,哈希表的大小设置得过小的话,哈希表的性能就可能退化成线性搜索。

# 在已经intern了1000000个字符串的情况下,再intern 10000个字符串
# pool的大小是60013,会有很多collision,哈希表性能退化
time = 1.913 sec
# pool的大小是100003
time = 0.012 sec

和普通的HashMap不同的是,String pool里的字符串如果没有地方引用,可以被垃圾回收。 从这个方面看,String pool的行为和WeakHashMap<String, WeakReference<String>>差不多。 但是WeakHashMap<String, WeakReference<String>>消耗的内存是String pool的实现的5倍。

Java7u40之后String pool的默认大小是60013。 可以通过-XX:StringTableSize=N选项,给pool指定合适的大小。 考虑到哈希表的性能,pool的大小最好是质数,比如60013。 同时为了避免冲突(collision),pool的大小可以设成比“要intern的字符串数目X2”大一点的质数。

Java 6的时代,String pool放在PermGen区,所以intern太多字符串会导致PermGen区out of memory。

另外,选项-XX:+PrintStringTableStatistics可以打印出String pool的使用情况。 选项-XX:+PrintFlagsFinal可以显示String pool的大小。

Java的finally是否一定会执行?

2017 Feb 2

try {
  return 1; 
} catch (Exception ex) {
  //...
} finally {
  //...
  return 2;
}

类似“try里的return语句会不会使得finally块被跳过”或者“到底是return 1还是return 2”之类的问题常常会在面试时被问到。 这篇Oracle的文档给出了答案,

The finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return, continue, or break.

意思是return之类的关键字不会使得finally块的执行被跳过。

除非在执行try时JVM退出了(比如调用了System.exit());或者执行try的线程被中断或者杀死了。

If the JVM exits while the try or catch code is being executed, then the finally block may not execute. Likewise, if the thread executing the try or catch code is interrupted or killed, the finally block may not execute even though the application as a whole continues.

用removeIf和Lambda更快更简洁地删除元素

2017 Jan 19

Java 8的Collection接口提供了一个新的删除元素的方法,boolean removeIf(Predicate<? super E> filter)。 它可以接受一个Lambda做为参数。

用这个方法可以写出更简洁的代码。 用一行代码,items.removeIf(i -> isDeletable(i));,就可以替代下面的代码。

for (Iterator it = items.iterator(); it.hasNext();) {
  if (isDeletable(it.next())) {
    it.remove(); 
  } 
}

另外,对于ArrayList而言,使用removeIf比传统的iterator方式更快。 传统的iterator方式,每删除一个元素E都要把E之后的所有元素往前移动一位。 如果ArrayList的大部分元素都要被删除,那么时间复杂度会是O(n^2)级别。 (实际上,Collection接口提供的默认removeIf实现就是用iterator方式来做删除的。) ArrayList类override了removeIf方法,提供了更高效的实现。 新的实现先用一个BitSet在一趟循环中标记哪些元素需要删除(O(n)复杂度),然后再在一趟循环中移动要保留的元素(O(n)复杂度)。

Guava中的Utility Object Pattern

2016 Dec 20

在一篇Guava的presentation中,Google提到了一种叫“Utility Object pattern”的模式。

return Joiner.on(", ").skipNulls().join("one", null, "two", "three");
             |-- 1 ---|-- 2 ------|-- 3 ----------------------------|

return Splitter.on("|").omitEmptyStrings().split("|Harry||Ron|||Hermione  ||");
               |-- 1 --|-- 2 -------------|-- 3 ------------------------------|

通过1得到一个初始的、默认的utility对象,通过2可以得到utility对象的一个“变种”, 再通过3最后求值。

这种API看起来更简洁。用户不用知道不同变种对应的utility类。

实现上,2中的方法,比如skipNulls(),会返回一个新对象,或者修改对象的属性并返回它。

  public Joiner skipNulls() {
    return new Joiner(this) {//通过传入的this可以“继承”上一个Utility对象的某些特性
      @Override
      public <A extends Appendable> A appendTo(A appendable, Iterator<?> parts) throws IOException {
      //...
      }
      //...
    }
  }
  public Splitter omitEmptyStrings() {
    //直接构造相应的Utility对象
    return new Splitter(strategy, true, trimmer, limit);
  }

ComparisonChain是另一个有趣的例子。

Comparator<Person> comparator = new Comparator<Person>() {
  public int compare(Person p1, Person p2) { 
    return ComparisonChain.start().compare(p1.exp, p2.exp).compare(p1.age, p2.age).result();
  }
};  

使用ComparisonChain时,不会创建新的ComparisonChain对象。 ComparisonChain也支持“short circuit(短路)”。 当p1.expp2.exp的比较有结果时(不等于0),就不会再去比较age

Tomcat里的各种Class Loader

2016 Dec 18

这篇文档介绍了Tomcat的class loader hierarchy和加载类时目录的搜索次序。 Tomcat的各个web app会共用一些class loader,也有属于各自的class loader(为了隔离)。

      Bootstrap
          |
       System
          |
       Common
       /     \
  Webapp1   Webapp2 ...

当WebappX的class loader加载一个类时,会先在WebappX的目录里找类定义 (这和普通Java程序的class loader parent-delegation model略有不同)。 但是也有例外,比如加载JRE的基础类时还是会先委托父class loader去加载。

Tomcat的类加载行为也是可以配置的(通过<Loader delegate="true"/>)。

因为每个web app都有自己的class loader,要防止class loader的泄漏

Tomcat的文档还是比较详尽的,没事可以多看看。

ThreadLocal简介

2016 Dec 3

在单线程的程序里,可以通过static变量或者singleton对象的方式定义全局的、共享的变量。 在多线程的程序里,如果想定义在一个线程内部共享的变量,可以考虑使用ThreadLocal

ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread

一般来说,ThreadLocal对象会被声明成类的私有静态成员,比如下面这个例子来自ThreadLocal的javadoc

 public class ThreadId {
     // Atomic integer containing the next thread ID to be assigned
     private static final AtomicInteger nextId = new AtomicInteger(0);

     // Thread local variable containing each thread's ID
     private static final ThreadLocal<Integer> threadId =
         new ThreadLocal<Integer>() {
             @Override protected Integer initialValue() {
                 // 让不同的线程有不同的初始值
                 return nextId.getAndIncrement();
         }
     };

     // 不同的线程调用threadId.get()会返回不同的值,每个线程都有其各种的拷贝
     public static int get() {
         return threadId.get();
     }
 }
ThreadLocal的实现

每个线程对应的Thread对象里存在着一个map(所以不同线程的map相互隔离,没有竞争),

// 摘自Thread类的定义
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap的实现是一个hash表,定义如下,

static class ThreadLocalMap {
	static class Entry extends WeakReference<ThreadLocal<?>> {
		/** The value associated with this ThreadLocal. */
		// ThreadLocal对象关联的对象
		Object value;
		
		Entry(ThreadLocal<?> k, Object v) {
			super(k);
			value = v;
		}		
	}
	
	private Entry[] table;
}      

map中的key由ThreadLocal对象们担当,value则是ThreadLocal对象所关联的对象。

下面是ThreadLocal::get()的伪代码(真实的实现考虑了threadLocals的lazy initialization),

public class ThreadLocal<T> {
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap.Entry e = t.threadLocals.get(this);
        if (e == null) {
            t.threadLocals.set(this, this.initialValue());
        }
        return (T)e.value;
    }
} 
ThreadLocal和内存泄漏

Java生态圈工具和技术的2016年度报告

2016 Nov 29

关于Java生态圈中的工具和技术的2016年度调查报告。 报告基于大约两千多份问卷,问卷时间为2016年三、四月份。 报告一共分为三个部分,除了上面的链接,还有Pivoting dataTrends and Historical data

一些有意思的点:

  • 三分之一的受调查者采用了微服务架构。
  • 采用了微服务架构的受调查者中,60%的人觉得微服务架构并没有让开发变得更简单🤔。
  • IntelliJ超过了Eclipse成为了最受欢迎的IDE。
  • MongoDB是最受Java开发者欢迎的NoSQL数据库。
  • SVN仍然占据着23%的份额😨。
  • VisualVM是最受欢迎的profiler(38%),其次是JProfiler(16%);超过三分之一不做profiling🤔。
  • 采用了微服务架构的开发者中接近二分之一使用Spring Boot做为web框架。

要不要使用ORM框架

2016 Nov 26

到底应不应该在项目中使用ORM(Object-relational mapping)框架?

来自Martin Fowler的一篇文章,OrmHate。文章的观点是,

关系型数据库和内存对象的相互映射本来就是一个很难的问题: 因为两者属于两种不同的建模方式,而且还要考虑两者的数据一致性(特别是在并发访问/修改的情况下)。 一些开发者对ORM框架有着不实际的期待——他们只想处理(灵活的)内存对象,期待ORM框架能(完美)处理对数据库的操作。 实际上,如果使用了关系型数据库,那么就应该在开发中考虑关系型数据库带来的影响。 如果ORM框架能解决80%的问题,那么使用它就是值得的。 另外,如果不需要双向的映射(bi-directional mapping),或者NoSQL数据库适用于业务场景,可以考虑不用ORM框架。

另一篇文章,ORM Haters Don’t Get It,更细节一些。文章的观点是,

即使不使用现成的ORM框架,也会在项目中创造出一个功能类似ORM框架的东西。 使用ORM还有一些额外的好处,比如方便切换数据库、现成的缓存实现等。 需要防止错误地使用ORM框架(需要熟悉ORM框架,ORM不是一个傻瓜式框架)。 另外ORM框架的session管理一般比较复杂,想要配置好需要一定的经验。

一些想法,

  • 考虑一下NoSQL是否能满足业务需求。
  • 考虑一下轻量级的ORM框架,比如MyBatis,能否满足要求。
  • 使用ORM框架时,考虑显式地使用native query(有时候ORM框架生成的SQL可能并不理想)
  • 开发过程中打印出ORM框架所执行的SQL语句,确认ORM框架的行为符合性能要求和预期。比如通过执行的SQL语句可以很容易地发现N+1问题

使用ORM框架(比如Ruby on Rails的ActiveRecord)的另外一个好处是可以快速地进行原型开发。