如何使用Javac/Jar/Java工具对源代码进行编译打包执行

图片来自pixabay.com的qimono会员

Javac/Jar/Java是JDK的发布包中带的三个基本工具,用于对Java源码进行编译打包运行,本文将介绍这几个工具的使用方法。

1. 代码和演示环境

代码地址:https://gitee.com/pphh/blog/tree/master/171221_java_compile_run/
演示环境:Windows 7 SP1, JDK 9.0.1

下面的所有命令演示都是在目录./171221_java_compile_run/中进行,文件的目录结构如下,

+ Simple.java
+ src
  - App.java
  + common
    - java/com/pphh/demo/Logger.java
  + main
    - java/com/pphh/demo/AppPkg.java
+ src2
  - AppLogger.java
+ src3
  - AppJ7.java
  - AppJ8.java
+ target
  - app.jar
  - common.jar
  + classes

2. Java源码编译

在开发完Java代码后,首先需要对源码进行编译。JDK中带的编译工具为Javac。

javac的命令格式为,

javac [options] [sourcefiles]

其中:
options 命令行选项
sourcefiles 待编译的代码文件

a) 简单的编译

只需要输入代码的文件地址即可

javac ./Simple.java
java Simple

b) 指定编译后的输出目录

使用-d选项,指定编译后的class文件输入目录

javac -d ./target/classes Simple.java
javac -d ./target/classes ./src/main/java/com/pphh/demo/AppPkg.java

c) 编译某个目录下的多个代码文件

使用-sourcepath选项,指定一个代码目录,然后通过通配符*.java获取指定目录下的所有代码文件进行编译,

set src_dir=./src/common/java/com/pphh/demo/
javac -d ./target/classes -sourcepath %src_dir% %src_dir%/*.java

上述命令会将指定目录下Logger.java文件进行编译,并按照package(com.pphh.demo)放置编译好的class文件,具体位置为,
./target/classes/com/pphh/demo/Logger.class

d) 有依赖的编译

可以通过-cp选项,指定依赖的class/Jar文件或所在目录。
例如程序代码src2/AppLogger.java有对前一步产生的Logger.class有依赖,其编译命令为,

javac -cp ./target/classes -d ./target/classes src2/AppLogger.java

上述命令指定了在依赖文件目录为/target/classes,编译代码后,将在目录./target/classes中产生一个AppLogger.class文件。

e) CLASSPATH

在编译的时候,Javac会从如下几个地方查找依赖class文件,

  1. 环境变量CLASSPATH定义的依赖文件和目录
  2. 当前执行命令的目录
  3. 通过-cp/-classpath指定的依赖文件和目录

在指定依赖的时候,可以通过上述几个方法定义依赖的class文件。

f) 编译时指定源码和目标代码的Java版本

可以通过-source/-target选项指定。

javac -d ./target/classes ./src3/AppJ7.java -source 7 -target 7
javac -d ./target/classes ./src3/AppJ8.java -source 8 -target 8

在JDK 9中Javac支持的Java版本包括6, 7, 8, 9,JDK9不再支持-source 5之前的版本。

需要注意的是,源发行版必须高于目标发行版,换句话说Java 7语言级别的代码,可以编译成Java 7/8/9语言级别的目标class文件,但不能编译成Java 6语言级别目标代码。

3. class文件打包

对代码编译之后,为了方便发布和使用,需要使用工具jar对这些class文件进行打包成Jar包。Jar是Java Archives的缩写,意思为Java归档文件。

jar的命令格式为,

jar [options] [ [--release VERSION] [-C dir] files] ...

其中:
options 命令行选项
--release VERSION创建多版本的Jar包,这是JDK9新功能
-C DIR 更改目录路径

a) 简单的打包命令

在打包前,先切换到class文件目录,然后执行打包命令,

cd ./target/classes
jar cvf ../common.jar ./

上述命令将./target/classes目录下所有文件打好包为common.jar,并放置在上一层目录中,也就是./target/下面。

将common.jar放置在classes目录之外,这是为了区分class和jar包文件,防止在下一次运行命令时,将刚刚生成的common.jar也打入下一个包中,形成循环。

b) 打包时更改目录路径到根目录

很多时候我们希望直接指定class的目录路径,但是我们不要使用如下的命令,

jar cvf ./target/common.jar ./target/classes(不推荐此命令,请使用-C选项)

因为上面的命令没有更改目录结构。如果通过解压工具解压common.jar包,会发现里面classes文件目录结构是不对的,classes文件并没有放置在根目录,而且放置在target/classes/中,这个是不对的。

这个时候要使用-C选项,更改目录路径到根目录

jar -cvf ./target/common.jar -C ./target/classes/ .

c) 设置主程序

每一个Jar包都可以设置一个运行主程序,当使用Java -jar命令运行Jar包时,会自动执行这个主程序。

设置主程序要使用-e选项,

jar -cvfe ./target/common1.jar Simple -C ./target/classes/ .
jar -cvfe ./target/common2.jar AppLogger -C ./target/classes/ .
jar -cvfe ./target/common3.jar com.pphh.demo.AppPkg -C ./target/classes/ .

上面的命令将Jar包中的主程序分别设置为Simple\AppLogger\com.pphh.demo.AppPkg,包的运行见后面。

d) 选择指定的class文件打包

除了指定一个class目录,我们还可以指定某些class文件打成Jar包。

下面将指定的一个App.class打入app.jar包中,

jar -cvf ./target/app.jar -C ./target/classes/ Simple.class

下面将一个Logger.class归档为一个日志类库包中,

jar -cvf ./target/logger.jar -C ./target/classes/ ./com/pphh/demo/Logger.class

4. 执行Java程序

在将源码编译成可执行的class文件后,接下来就可以使用java工具来启动运行。

java的命令格式为,

java [options] mainclass [args...]

其中:
options 命令行选项
mainclass 主程序类文件
args 命令行参数

a) 简单的编译运行

javac Simple.java
java Simple

b) 指定class文件或目录
javac -d ./target/classes Simple.java
java -classpath ./target/classes Simple
java -classpath "./target/classes/Simple.class;" Simple
c) 运行一个在指定package的运行程序

javac -d ./target/classes ./src/main/java/com/pphh/demo/AppPkg.java
java -classpath ./target/classes com.pphh.demo.AppPkg

d) 运行一个已配置主程序的Jar包

java -jar ./target/common1.jar
java -jar ./target/common2.jar
java -jar ./target/common3.jar

e) 运行一个未配置主程序的Jar包,通过命令指定运行的主程序
java -classpath "./target/common.jar" Simple
java -classpath "./target/common.jar" AppLogger
java -classpath "./target/common.jar" com.pphh.demo.AppPkg

5. 参考文献

  1. Java 9 SE技术文档:Javac工具命令
  2. Java 9 SE技术文档:Jar工具命令
  3. Java 9 SE技术文档:Java工具命令
  4. Java 7 SE技术文档:Javac工具命令
  5. Java 7 SE技术文档:Jar工具命令
  6. Java 7 SE技术文档:Java工具命令

Java对象的类型转换和属性复制

1. 问题的源由

在J2EE项目开发中,会涉及很多领域模型对象,例如,

  • VO (View Object) 视图对象,也叫展示对象,用于前端页面渲染所需要的数据
  • DTO (Data Transfer Object) 数据传输对象,一般用于Service和Manager向外传输数据
  • PO (Persistent Object) 持久化对象,一般和数据库表结构会形成一一映射关系,通过DAO层向上传输数据源对象

更多领域分层模型,及其所在的应用分层,可见阿里巴巴Java开发手册中的工程规约定义。

这些VO/DTO/PO都是POJO对象,每个属性都有getter/setter方法的定义,其数据转换链条大概为,

数据库DB 》PO 》DTO 》VO

可以看到,在项目中不同的应用分层DB/DAO/Service/Manager/Web,经常会需要把一个数据对象转换为其它类型的数据对象,转换的同时复制部分对象属性,有时部分属性需要从多个数据对象获取进行组装。这些数据的转换有一个共同的需求,那就是从一个源对象变为目标对象

源对象 Source s 》目标对象 Target t

接下来我们就需要讨论这个转换过程。

2. 单个对象的转换

1)简单的Java对象转换,使用BeanUtils.copyProperties

使用Spring都知道Spring Beans中提供了一个BeanUtils,可以用于对象的属性复制,其使用方法为,

    Target t = new Target();
    BeanUtils.copyProperties(s, t);
    BeanUtils.copyProperties(s, t, 'password');

其中,第二个copyProperties方法中第三个参数是告知复制过程中忽略复制'password'这个属性。

2)单个对象的转换,指定目标类

BeanUtils.copyProperties不提供目标对象的创建,因此每次都需要new出一个目标对象。为了能够省去这一步,可以使用下面的方法。

实现代码

public static <S, T> T convert(S s, Class<T> clazz) {
  T t = null;
  
  try {
    t = clazz.newInstance();
    BeanUtils.copyProperties(s, t);
  } catch (Exception e) {
    e.printStackTrace();
  }
  
  return t;
}

上述方法将对象的生成封装起来返回。

使用样例

Target t = convert(s, Target.class);

使用中直接指定目标类,代码更加简洁易懂。

3. List/Set集合之间的转换

在很多时候,我们不仅处理一个对象的转换,而是面对集合的对象,这时需要能够对集合中的对象进行批量处理转换。

1)List集合转换,将列表中每一个实体转换为目标类

实现代码

public static <S, T> List<T> convert(Iterable<S> iterable, Class<T> clazz) {
  return StreamSupport.stream(iterable.spliterator(), false)
                      .map(s -> convert(s, clazz))
                      .collect(Collectors.toList());
}

使用样例

List<Target> targets = convert(srcCollection, Target.class);
2)List集合转换,提供一个mapper转换方法,将列表中每一个实体对象转换为目标类

实现代码

public static <S, T> List<T> convert(Iterable<S> iterable, 
           Function<? super S, ? extends T> mapper) {
  return StreamSupport.stream(iterable.spliterator(), false)
                      .map(mapper)
                      .collect(Collectors.toList());
}

使用样例

List<Target> targets = convert(srcCollection, mapper);

其中mapper的定义为Function<? super S, ? extends T>,即该函数需要一个源对象作为输入参数,返回一个目标对象

3)List集合转换,转换为指定的List/Set容器类,集合中的实体对象不变

实现代码

public static <S, U extends Collection<S>> U collect(Iterable<S> iterable, CollectionFactory<S, U> factory) {
  U collection = factory.createCollection();
  iterable.forEach(collection::add);
  return collection;
}

其中CollectionFactory为一个集合类工厂。

使用样例

Iterable<Integer> iterable = IntStream.range(0, 5).boxed().collect(Collectors.toList());
ArrayList<Integer> arrayList = collect(iterable, ArrayList::new);
HashSet<Integer> hashSet = collect(iterable, HashSet::new);
LinkedList<Integer> linkedList = collect(iterable, LinkedList::new);

上述将一个Collection集合转换为ArrayList/HashSet/LinkedList,集合中的整型变量对象保持不变。

4)List集合转换,转换为指定的List/Set容器类,并将集合中的实体对象转换为目标类

实现代码

public static <S, U extends Collection<T>, T> U collect(Iterable<S> iterable, Class<T> clazz, CollectionFactory<T, U> factory) {
  Iterable<T> list = convert(iterable, clazz);
  U collection = factory.createCollection();
  list.forEach(collection::add);
  return collection;
}

使用样例

Iterable<UserEntity> users = UserDao.SingleInstance.findAll();
ArrayList<UserVO> arrayList = collect(users, UserVO.class, ArrayList::new);
HashSet<UserVO> hashSet = collect(users, UserVO.class, HashSet::new);
LinkedList<UserVO> linkedList = collect(users, UserVO.class, LinkedList::new);

上述将一个Collection集合转换为ArrayList/HashSet/LinkedList,同时集合中的UserEntity变量对象转换为了UserVO。

4. List/Set/Map之间的转换

Java中Map的处理效率比List/Set高效不少,有些时候需要在List/Set/Map之间进行转换。

1)List集合转换,转换为Map集合,提供一个keyGenerator生成key,实体对象保存为value

实现代码

public static <S> Map<String, S> map(Iterable<S> iterable, Function<? super S, String> keyGenerator) {
  return StreamSupport.stream(iterable.spliterator(), false)
         .collect(Collectors.toMap(keyGenerator, Function.identity()));
}

使用样例

List<UserEntity> users = UserDao.SingleInstance.findAll();
Map<String, UserEntity> result = map(users, UserEntity::getName);

上述样例中由UserEntity::getName来生成对象的键值,其必须为保证唯一。

2)Map集合转换,转换为List集合,实体对象保持不变

实现代码

public static <K, S, U extends Collection<S>> U collect(Map<K, S> map, CollectionFactory<S, U> factory) {
  U collection = factory.createCollection();
  map.values().iterator().forEachRemaining(collection::add);
  return collection;
}

使用样例

Map<String, UserEntity> users;
List<UserEntity> list1 = collect(users, ArrayList::new);
Set<UserEntity> set = collect(users, HashSet::new);
List<UserEntity> list2 = collect(users, LinkedList::new);
Collection collection = users.values();

上述将一个用户的Map集合转换为List集合,可以看到一个简单获得List集合的方法就是使用map.values()方法。

3)Map集合转换,转换为指定的Map容器类

实现代码

public static <K, S, T, U extends Map<K, T>> U map(Map<K, S> srcMap, Class<T> clazz,  MapFactory<K, T, U> factory) {
  U u = factory.createMap();
  srcMap.entrySet()
        .forEach(ksEntry -> u.put(ksEntry.getKey(),
                          convert(ksEntry.getValue(), clazz)));
  return u;
}

使用样例

Map<String, UserEntity> users;
Map<String, UserVO> m1 = map(users, UserVO.class, HashMap<String, UserVO>::new);
Map<String, UserVO> m2 = map(users, UserVO.class, LinkedHashMap<String, UserVO>::new);
Hashtable<String, UserVO> m3 = map(users, UserVO.class, Hashtable<String, UserVO>::new);

上述将一个用户的Map集合转换为List集合,可以看到一个简单获得List集合的方法就是使用map.values()方法。

5. 源代码

演示代码仓库地址:https://gitee.com/pphh/blog,可以通过如下git clone命令获取仓库代码,

git clone git@gitee.com:pphh/blog.git

本篇博文的程序代码样例在文件路径171201_java_object_copy\171201_java_object_copy_convert中,请使用JDK8及后续版本编译并运行项目代码。

6. 参考资料

  • POJO阿里巴巴Java开发手册(2017版本):应用分层和领域模型PO/DTO/VO
  • 百度百科:POJO简单Java对象

Java Stream简介和使用参考

1. 什么是Java Stream

Java Stream是Java 8引入的一个新的集合操作功能类,提供数据的流式处理,能够对Java集合数据实现类似大数据领域中map-reduce的数据处理方式,为集合对象进行各种高效的聚合和批量处理操作提供便利。Java Stream是Java语言在集合类Array/Collection/Map之后又一项集合功能扩展。

Java Stream提供串行和并行的数据处理模式,会在流式操作前后步骤中进行优化处理,可以在并发模式中充分利用多核处理器,实现高效的数据处理过程。

Java Stream不同于其它Java集合类(例如Array/Collection/Map等等),有其独自的特点,

  • 没有数据存储,Java Stream关注的是算法和计算,提供的是数据处理管道(pipeline)。
  • 数据流,数据源转换成Stream数据对象之后,Stream数据处理产生新的Stream数据对象,如此循环直到产生数据结果,整个处理的过程中不改变数据源。
  • 延后处理(Laziness-seeking),很多Stream操作,比如filter/map/duplicate removal,能够将多次操作优化合并后执行,延后处理提供了这种优化执行的机会,详情见下文。
  • 数据源可以是无限的,只要处理过程存在终止条件(例如limit/findFirst)。
  • 一次性消费,在一个Stream操作流水线中,数据源中每一个数据元素只被消费处理一次。如果想再消费,需重新构建一个新的Stream对象。

2. 简单的使用

下面是一个使用Java Stream的样例,使用了Stream的过滤-映射-聚合(filter-map-reduce)操作,

Integer sum = IntStream.range(0,10)
                       .filter(n -> n%2 == 0)
                       .map(n -> n*2)
                       .reduce(0, (x, y) -> x+y);

在这个样例中,首先拿到 [0,9] 十个数字的整形数据集合,然后过滤出偶数,然后对每个偶数乘2,最后算出总和为40。

3. Java Steam的使用演示

Java Stream各种操作方法演示代码请见这里,供使用时参考。请先通过如下git clone命令下载代码仓库,

git clone git@gitee.com:pphh/blog.git

演示代码放在文件夹171124_java_stream中,请打开演示代码项目,运行主程序。

里面包括如下几个部分的演示,

  1. 演示简单的stream集合处理(SimpleStream)
    • 在SimpleStream的演示方法中,通过filter-map-reduce操作数组,获取偶数数列,相加出总和。
  2. 演示Stream的构建操作(StreamBuilder.demo)
    • 通过Java数组生成Stream对象
    • 通过Java Collection集合类生成Stream对象
    • 通过Stream静态工厂类生成Stream对象
  3. 演示Stream的各种中间操作方法:StreamOpertions
    • Stream.filter:在数列中找到偶数,挑出字符串长度大于5的用户名
    • Stream.map:对用户名进行字符串大写处理,对多个集合合并处理
    • Stream.forEach:对集合元素一一执行指定动作,forEach是一个结束操作,只能在最后执行,并只被执行一次。
    • Stream.peek:对集合元素一一执行指定动作,和forEach不一样之处,peek是一个中间操作,可以执行多次。
    • Stream.reduce:聚合操作,获取偶数并取总和
    • Stream.skip:获取数列,跳过数组的前三个数据元素
    • Stream.limit:获取数列,指定获取数组的前面三个数据元素
    • Stream.sort:对集合数据元素进行排序
    • Stream.sort and Stream.limit:演示limit和sort一起工作后,执行流水线的变化,注意有了limit之后,sort只对限定的数据集合进行排序。该演示可以看到Java Stream对中间操作的优化。
    • Stream.min and Stream.max:获取数列中的最大值和最小值
    • Stream.distinct:获取新数列,去除重复值
    • Stream.allMatch, Stream.anyMatch, Stream.noneMatch:判断数列中是否所有数字大于0,有数字大于0,没有任何数字大于0
    • Stream.iterate:获取等差数列
    • Stream.Collectors:对数列进行分组
  4. 演示Stream的串并行处理:StreamParallel
    • 当没有指定并行处理时,数列中的数据在主线程中一一执行peek操作。
    • 若指定了并行处理时,数列中的数据在不同线程中执行peek操作,执行顺序不定。

    如下为Stream串并行处理的演示输出,可以看到前者一一执行,后者有不同线程执行,顺序也不定,
    [20171124 22:16:09-970][main] 演示:串行执行数据流式处理
    [20171124 22:16:09-971][main] 0
    [20171124 22:16:09-971][main] 1
    [20171124 22:16:09-971][main] 2
    [20171124 22:16:09-971][main] 3
    [20171124 22:16:09-971][main] 4
    [20171124 22:16:09-971][main] 5
    [20171124 22:16:09-972][main] 6
    [20171124 22:16:09-972][main] 7
    [20171124 22:16:09-972][main] 8
    [20171124 22:16:09-972][main] 9
    [20171124 22:16:09-972][main] 演示:并行执行数据流式处理
    [20171124 22:16:09-975][main] 9
    [20171124 22:16:09-975][main] 8
    [20171124 22:16:09-976][main] 7
    [20171124 22:16:09-976][ForkJoinPool.commonPool-worker-2] 4
    [20171124 22:16:09-976][ForkJoinPool.commonPool-worker-2] 3
    [20171124 22:16:09-976][ForkJoinPool.commonPool-worker-2] 2
    [20171124 22:16:09-976][ForkJoinPool.commonPool-worker-2] 1
    [20171124 22:16:09-976][ForkJoinPool.commonPool-worker-2] 0
    [20171124 22:16:09-977][ForkJoinPool.commonPool-worker-2] 5
    [20171124 22:16:09-977][main] 6

4. Stream的生成

Stream对象主要有如下几种获取方式

  • Java Collection类
    • java.util.Collection.stream()
    • java.util.Collection.parallelStream()
  • Java Arrays类
    • Java.util.Arrays.stream()
  • 调用Java Stream中的静态工厂方法
    • java.util.stream.Stream.of(Object[])
    • java.util.stream.Stream.iterate()
    • java.util.stream.IntStream.range()
  • 自定义生成
    • java.util.StreamSupport
  • 其它
    • Random.ints()
    • BitSet.stream()

5. Stream操作流水线和操作方法分类

有了Stream集合数据,我们接下来就可以调用Stream提供的操作方法,对数据进行流式处理,这种流式处理也被称为操作流水线(Stream Pipeline)。

在了解操作流式线定义之前首先需要了解下操作方法,Java Stream类中提供每一种操作方法都可划分为如下两大类型,

  1. 中间操作(Intermediate Operation):会产生另外一个Stream对象
  2. 结束操作(Terminal Operation):产生结果

一个Stream流水线通常由一个数据源,N个中间操作,一个结束操作组成,

Stream Pipeline = Source + N * Intermediate operations + Terminal Operation

其中N>=0。

需要注意的是,所有的中间操作都是延后执行(Lazy Operation),换句话说,当Stream对象调用一个中间操作方法时,其操作不会立即执行,而是只有当Stream对象调用了结束操作方法时,才开始执行整个操作流水线,按顺序依次执行各种intermediate和terminal操作。

Stream Lazy Intermediate Operation这种特性能够让数据的处理效率得到提升,Java Stream可以分析中间操作的算法,进行相应的操作合并,或者根据结束条件,一旦有满足结束条件,则提前结束流水线执行。例如,在上述的filter-map-reduce样例中,可以让filtering\mapping\summing三次操作放入同一次循环中完成。再比如在一组数据集合中找出一个小于零的数字,那么只要找到第一小于零的数字则可以提前结束整个流水线操作。

中间操作的延后执行对于无限的数据集合是非常必要的。Stream在面对无限的数据集合时,必须要定义一个结束操作条件,一旦该条件满足了,则流水线执行结束。

中间操作可以分为无状态和有状态的操作,

  1. Stateless operation是指当前中间操作不会给后续操作带来状态,每个数据元素可以独立地执行后续操作并获取结果,各个数据元素在执行后续操作时互不影响,没有相互关联关系,filter/map就是一种stateless operations。
  2. Stateful operations是指只有所有的数据元素都执行完当前中间操作,才能获取到执行结果。Sort/Distinct就是一种有状态的操作,前者对数据集合进行排序,后者对数据进行重复值去除。

操作的状态有无对Stream流水线的并发执行有影响,如果是有状态的,在并发执行过程中会产生大量的数据需要缓存,而如果是无状态的,则数据的缓存量会大大减少。

还有一种是短路操作(short-circuiting),是指对一个无限的对象集合可能在有限的时间内产生一个有限的集合,短路操作能够提前结束对无限对象集合的流水线执行,比如limit/anyMatch操作,前者获取指定个数的对象集合,后者查询是否有匹配的数据元素。中间操作和结束操作都可能是short-circuiting。

下表列出java.util.stream.Stream中各操作方法所具有的属性分类,标识Y表明该方法具有当前属性,

Stream Operations Stateless Stateful Intermediate Terminal Short-Circuit
filter Y Y
map Y Y
mapToInt Y Y
mapToLong Y Y
mapToDouble Y Y
flatMap Y Y
flatMapToLong Y Y
flatMapToDouble Y Y
peek Y Y
distinct Y Y
sorted Y Y
skip Y Y
forEach Y
forEachOrdered Y
toArray Y
reduce Y
collect Y
min Y
max Y
count Y
anyMatch Y Y
allMatch Y Y
noneMatch Y Y
findFirst Y Y
findAny Y Y
limit Y Y Y

6. 参考资料