20180514

收获

  1. Spark Dataset 中的Row包含array类型时,当使用row读取可用row.getAsList(),当使用UDF读取,必须将UDF的参数声明为Seq,声明为List会报错
  2. 注册完UDF后,除了可以在SQL语句中使用UDF之外,还可以用functions.callUDF;另外也可以用functions.udf返回一个UserDefinedFunction,但是得2.3版本之后支持
  3. 改变Row的内容可以直接使用ds.withColumn;避免写一个bean类,然后将row中所有的值填充bean,假如有几十个列就很繁琐

教训

  1. 匹配搜索关键词应同时转换为小写(浪费数小时)
  2. 不要在分配给executor的方法中使用耗时的操作,比如UDF、map等,特别是初始化读取本地文件等(这应该是刚开始使用Spark时易犯的错误,浪费10分钟)
  3. Dataset.repartition 有多个重载函数,当不输入分区数而直接调用的时候,不是根据spark.sql.shuffle.partitions默认分区,而是调用了repartition(Column… partitionExprs),因为没有输入分区表达式,导致所有数据都传输到一个executor,数据倾斜;还是尽量少使用变长参数(困扰一周末)

一图看懂Java泛型通配符

当使用 <? super MyClass> 的时候,表明未知类的继承结构处于 ObjectMyClass 之间,这时

  • 编译器只能确定任何返回该未知类型的方法,返回的变量都是 Object 的子类,所以返回的类型就确定为 Object,比如 getter 方法
  • 使用该未知类型作为参数的方法,该参数一定是 MyClass 的父类,所以可以传递 MyClass 及其子类进去,比如 setter 方法

而使用 <? extends MyClass> 的时候,未知类型一定是 MyClass 的子类,但向下延伸到无穷尽,无法判断

  • 所以返回未知类型的方法的返回类型有一个上界,就是 MyClass,即返回类型确定为 MyClass
  • 但是使用未知类型的方法,因为向下继承无限延伸,无法判断下界,所以不能使用该方法,比如 setter(可以 set(null))

使用 <?> 的时候,可以当作 <? extends Object>,即上界是 Object,可以使用 getter 方法,不可以使用 setter 方法。

根据上面这些原则,一个简单的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data // lombok,省略了 getter 和 setter
class Holder<T>{
private T t;

public <U extends MyClass> void testSetter(Holder<? super MyClass> holder, U u) {
holder.setT(u); // 可以输入任何 MyClass 及子类的对象
holder.setT(null);
}

public void testGetter1(Holder<? extends MyClass> holder) {
MyClass obj = holder.getT(); // 能确定返回的对象一定是 MyClass 或父类的对象
}

public void testGetter2(Holder<?> holder) {
Object obj = holder.getT(); // 只能确定返回的对象一定是 Object
}
}

class MyClass{}

选择限定通配符时的快速判断方法:

get-put principle:
Use an extends wildcard when you only get values out of a structure, use a super wildcard when you only put values into a structure, and don’t use a wildcard when you do both.

参考:
https://www.ibm.com/developerworks/java/library/j-jtp07018/index.html

使用Python和Java调用Shell脚本时的死锁陷阱

最近有一项需求,要定时判断任务执行条件是否满足并触发 Spark 任务,平时编写 Spark 任务时都是封装为一个 Jar 包,然后采用 Shell 脚本形式传入所需参数执行,考虑到本次判断条件逻辑复杂,只用 Shell 脚本完成不利于开发测试,所以调研使用了 Python 和 Java 分别调用 Spark 脚本的方法。

使用版本为 Python 3.6.4 及 JDK 8

Python

主要使用 subprocess 库。Python 的 API 变动比较频繁,在 3.5 之后新增了 run 方法,这大大降低了使用难度和遇见 Bug 的概率。

1
2
subprocess.run(["ls", "-l"])
subprocess.run(["sh", "/path/to/your/script.sh", "arg1", "arg2"])

为什么说使用 run 方法可以降低遇见 Bug 的概率呢?
在没有 run 方法之前,我们一般调用其他的高级方法,即 Older high-level API,比如 callcheck_all,或者直接创建 Popen 对象。因为默认的输出是 console,这时如果对 API 不熟悉或者没有仔细看 doc,想要等待子进程运行完毕并获取输出,使用了 stdout = PIPE 再加上 wait 的话,当输出内容很多时会导致 Buffer 写满,进程就一直等待读取,形成死锁。在一次将 Spark 的 log 输出到 console 时,就遇到了这种奇怪的现象,下边的脚本可以模拟:

1
2
3
4
# a.sh
for i in {0..9999}; do
echo '***************************************************'
done

1
2
p = subprocess.Popen(['sh', 'a.sh'], stdout=subprocess.PIPE)
p.wait()

call 则在方法内部直接调用了 wait 产生相同的效果。
要避免死锁,则必须在 wait 方法调用之前自行处理掉输入输出,或者使用推荐的 communicate 方法。 communicate 方法是在内部生成了读取线程分别读取 stdout stderr,从而避免了 Buffer 写满。而之前提到的新的 run 方法,就是在内部调用了 communicate

1
stdout, stderr = process.communicate(input, timeout=timeout)

Java

说完了 Python,Java 就简单多了。
Java 一般使用 Runtime.getRuntime().exec() 或者 ProcessBuilder 调用外部脚本:

1
2
3
4
5
6
7
8
Process p = Runtime.getRuntime().exec(new String[]{"ls", "-al"});
Scanner sc = new Scanner(p.getInputStream());
while (sc.hasNextLine()) {
System.out.println(sc.nextLine());
}
// or
Process p = new ProcessBuilder("sh", "a.sh").start();
p.waitFor(); // dead lock

需要注意的是,这里 stream 的方向是相对于主程序的,所以 getInputStream() 就是子进程的输出,而 getOutputStream() 是子进程的输入。

基于同样的 Buffer 原因,假如调用了 waitFor 方法等待子进程执行完毕而没有及时处理输出的话,就会造成死锁。
由于 Java API 很少变动,所以没有像 Python 那样提供新的 run 方法,但是开源社区也给出了自己的方案,如commons exec,或 http://www.baeldung.com/run-shell-command-in-java,或 alvin alexander 给出的方案(虽然不完整)。

1
2
3
4
5
6
7
8
9
10
11
// commons exec,要想获取输出的话,相比 python 来说要复杂一些
CommandLine commandLine = CommandLine.parse("sh a.sh");

ByteArrayOutputStream out = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(out);

Executor executor = new DefaultExecutor();
executor.setStreamHandler(streamHandler);
executor.execute(commandLine);

String output = new String(out.toByteArray());

但其中的思想和 Python 都是统一的,就是在后台开启新线程读取子进程的输出,防止 Buffer 写满。

另一个统一思想的地方就是,都推荐使用数组或 list 将输入的 shell 命令分隔成多段,这样的话就由系统来处理空格等特殊字符问题。

参考:

https://dcreager.net/2009/08/06/subprocess-communicate-drawbacks/
https://alvinalexander.com/java/java-exec-processbuilder-process-1
https://www.javaworld.com/article/2071275/core-java/when-runtime-exec—won-t.html

Spark 中的序列化陷阱

Spark 的代码分为 Driver 端执行的部分和 Executor 端执行的部分,Driver 端分发任务的同时,会通过序列化传送 Executor 需要的对象,由于 Java 序列化的一些特性,初学者在使用时容易碰到一些陷阱。

陷阱1: 没有序列化

最常见的一个错误就是传递的类不可序列化,如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package test;
import ...
/**
* Created by PerfectDay20.
*/
public class Main {
public static void main(String[] args) {
SparkConf conf = new SparkConf().setAppName("test");
JavaSparkContext javaSparkContext = new JavaSparkContext(conf);

JavaRDD<Integer> rdd =
javaSparkContext.parallelize(IntStream.range(1, 10000).boxed().collect(Collectors.toList()), 10);

Util util = new Util();
rdd.map(util::process); // 序列化错误
}

}

class Util implements Serializable{
public int process(int i) {
return i + 1;
}
}

这里的 Util 类没有实现 Serializable 接口,由 Driver 创建实例后,在 map 中传递给各个 Executor,导致序列化失败报错:

1
2
3
4
5
6
7
8
9
10
11
Exception in thread "main" org.apache.spark.SparkException: Task not serializable
at org.apache.spark.util.ClosureCleaner$.ensureSerializable(ClosureCleaner.scala:298)
at org.apache.spark.util.ClosureCleaner$.org$apache$spark$util$ClosureCleaner$$clean(ClosureCleaner.scala:288)
at org.apache.spark.util.ClosureCleaner$.clean(ClosureCleaner.scala:108)
at org.apache.spark.SparkContext.clean(SparkContext.scala:2094)
at org.apache.spark.rdd.RDD$$anonfun$map$1.apply(RDD.scala:370)
...
Caused by: java.io.NotSerializableException: test.Util
Serialization stack:
- object not serializable (class: test.Util, value: [email protected])
...

这种错误根据不同的需求有不同的解决方法:

  1. 最简单的方法就是让Util类可序列化: class Util implements Serializable
  2. 如果是工具类,比如上例,没有必要创建Util实例,直接将process替换为静态方法:public static int process(int i),然后在map方法中:rdd.map(Util::process)
  3. 如果调用的方法比较简单,就不用创建Util类,直接在map中写 lambda 表达式即可:rdd.map( i -> i + 1 );这种方法其实是创建了一个实现Function接口的匿名类,而Function接口的定义是:public interface Function<T1, R> extends Serializable,所以自然就可序列化了
  4. 另外可以在map中创建Util实例,这样的话,每个实例都是在 Executor 端创建的,因为不需要序列化传递,就不存在序列化问题了:
    1
    2
    3
    4
    5
    rdd.map(i->{
    Util util = new Util();
    LOG.info(""+util);
    return util.process(i);
    })

但是这种情况对于每一个i都要创建一个实例,在一些重量级操作,比如创建数据库链接时,可以考虑采用mapPartition,这样如上面的例子,就只需要创建10个Util实例:

1
2
3
4
5
6
rdd.mapPartitions(iterator->{
Util util = new Util();
List<Integer> list = new ArrayList<>();
iterator.forEachRemaining(i -> list.add(util.process(i)));
return list.iterator();
})

陷阱2: 更改静态域导致结果不一致

Java 的序列化结果中,只包括类的实例域部分,静态域在恢复实例时是由本地的 JVM 负责创建的,所以,假如在 Driver 端更改了静态域,而在 Driver 端是看不到的。所以要在 Executor 端使用的静态域,就不要在 Driver端更改,这和Broadcast创建后不要更改的要求是类似的。由于出现这种问题一般不会报异常,只会体现在结果中,所以比较难以发现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package test;
import ...
/**
* Created by PerfectDay20.
*/
public class Main {
private static final Logger LOG = LoggerFactory.getLogger(Main.class);
private static String word = "hello";
public static void main(String[] args) {
SparkConf conf = new SparkConf().setAppName("test");
JavaSparkContext javaSparkContext = new JavaSparkContext(conf);
JavaRDD<Integer> rdd =
javaSparkContext.parallelize(IntStream.range(1, 10000).boxed().collect(Collectors.toList()), 10);
word = "world";
rdd.foreach(i -> LOG.info(word));
}
}

上面的例子中,word初始化为"hello",在 Driver 端的main方法中修改为"world",但该值并没有序列化到 Executor 端,Executor 本地仍然是"hello",输出的 log 结果自然也全都是 "hello"

解决方案:

  1. 最好一次性初始化好静态域,修饰为final ,避免二次更改
  2. 在 Executor 端修改静态域,如
    1
    2
    3
    4
    rdd.foreach(i -> {
    word = "world";
    LOG.info(word);
    });

假如要在 Executor 端使用一个大的对象,比如一个Map,最好的方法还是利用Broadcast

此外,由于多个 task 可能在同一 JVM 中运行,使用静态域可能会导致多线程问题,这也是需要注意的地方。

参考链接:

Spark Code Analysis

Understanding Spark Serialization

Java对象占用内存计算

计算前提

  1. JDK 版本,不同版本的类可能会有变化
  2. 要区分是 32bit 还是 64bit 系统
  3. 是否开启压缩指针(默认开启,指针为 4Byte,否则为 8Byte)
  4. 是否数组,数组对象头多了一个长度值,占 4Byte

计算方法

对象所占内存 = 对象头 + 所有域 + 填充
其中,若域为另一个对象,即非基本类型,则需递归计算

对象头

对象头分为3部分:

  1. mark word:同步状态、GC状态、hashcode 等
  2. klass pointer: 指向本身的类对象
  3. 数组类型的长度
_mark _kclass Array Length
32bit 4 4 4
64bit 8 8 4
64+comp 8 4 4

https://mechanical-sympathy.blogspot.com/2011/07/false-sharing.html
http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

不同状态下的对象头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//  32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/oops/markOop.hpp#l29

对象的域在内存中的顺序:

域的顺序并不是在类中定义的顺序,而是经过了调整;每个对象都是 8Byte 对齐的,不是倍数的话会在最后填充,具体顺序如下:

  1. doubles (8) and longs (8)
  2. ints (4) and floats (4)
  3. shorts (2) and chars (2)
  4. booleans (1) and bytes (1)
  5. references (4/8)
  6. repeat for sub-class fields

https://zeroturnaround.com/rebellabs/dangerous-code-how-to-be-unsafe-with-java-classes-objects-in-memory/5/

不同域的大小

Bytes
boolean 1
byte 1
char 2
short 2
int 4
float 4
long 8
double 8
reference 4

具体例子

64bit 压缩指针 JDK8 中 String s = "abc",对象 s 的大小: 48Bytes

注意:不同版本的 JDK String 类的域不同,比如 JDK6 中有 offsetcount,JDK7 中有 hash32
具体验证可以使用 jol 库:
http://openjdk.java.net/projects/code-tools/jol/

1
2
3
4
5
6
System.out.println(GraphLayout.parseInstance("abc").toPrintable());
==>
[email protected]2ff4acd0d object externals:
ADDRESS SIZE TYPE PATH VALUE
795707020 24 java.lang.String (object)
795707038 24 [C .value [a, b, c]

How to Override Equals in Java and Scala

相信读过 《Effective Java》 的读者都已经知道编写 equals 方法的作用与重要性,基本概念不多做解释,这里就总结一下如何编写正确的 equals 方法。

equals 在 Java 和 Scala 中含义相同,都需要满足以下五个条件:

  1. 自反性
  2. 对称性
  3. 传递性
  4. 一致性
  5. anyObject.equals(null) == false

现在我们有三个问题:

  1. 假如我们只有一个类 Person,如何写?
  2. 假如 Person 类有一个子类 Student,相互不能判断(一定返回 false),如何写?相互可以判断,如何写?
  3. 假如 PersonStudent 可以相互判断,但另一子类 Teacher 只能和同类判断,如何写?

Java

《Effective Java》 中最后推荐的写法步骤是:

  1. 通过 == 判断是否是同一个对象
  2. instanceof 判断是否是正确的类型,注意这里已经包含了 null 的情况,所以不用单独另写
  3. 将对象转换成正确的类型
  4. 对需要判断的域分别进行对比

需要注意,基本类型用 == 判断,例外是 floatFloat.comparedoubleDouble.compare,因为有 NaN 等特殊值存在。

上述第二步中还有另一个变种,是使用 getClass 进行类型判断,这样的话只有类型完全一致才能返回 true,如果只是单一的类还好,要是涉及类之间的继承,则违背了 Liskov Substitution Principle,所以最后书中的结论是:

There is no way to extend an instantiable class and add a value component while preserving the equals contract.

由于现在的 IDE 例如 IntelliJ IDEA 已经可以自动为我们生成 equals 方法,还可以选择是否允许子类判断,是否可为 null 等判断,所以我们就不必手动编写了,但是生成的结果也是符合上面的 4 步的:


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
class Person{
private String name;
private int age;

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) { // 不涉及继承,问题 1 和 问题 2 前半的写法
return false;
}

Person person = (Person) o;

if (age != person.age) {
return false;
}
return name != null ? name.equals(person.name) : person.name == null;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Person)) { // 涉及继承,使得与子类之间也可以判断,问题 2 后半的写法
return false;
}

Person person = (Person) o;

if (age != person.age) {
return false;
}
return name != null ? name.equals(person.name) : person.name == null;
}
}

Scala

scala 中编写的方式大致相同,但是结合其语法,相比似乎又简单又繁琐。
简单是指当没有子类,或和子类判断一定为 false 时(违反LSP),可以这样写:

1
2
3
4
5
6
7
8
class Person(val name: String, val age: Int) { 
override def equals(other: Any): Boolean = other match { // 问题 1 的写法
case that: this.getClass == that.getClass &&
Person => name == that.name &&
age == that.age
case _ => false
}
}

繁琐是指假如这时出现了一个子类 Student 且增加了一个域 sid,假如我们需要两个类可相互判断,则上述方法在判断一个 Person 对象和一个 Student 对象时一定会返回 false

因此《Programming in Scala》中建议采用如下的编写方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person(val name: String, val age: Int) {
def canEqual(other: Any): Boolean = other.isInstanceOf[Person]

override def equals(other: Any): Boolean = other match { // 问题 2 的写法
case that: Person =>
(that canEqual this) &&
name == that.name &&
age == that.age
case _ => false
}
}

class Student(override val name: String, override val age: Int, val sid: Int) extends Person(name, age){
}

上面 canEqual 方法的作用和 Java 代码中判断 instanceof 的作用是一致的,但比 Java 中的判断更加灵活,比如可以限定不同子类与父类的判断关系。

比如有一个 Person 的子类 Teacher,我们希望它只能和 Teacher 类进行判断,与 PersonStudent 判断都返回 false,该如何写呢?一种错误的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Teacher(override val name: String, override val age: Int, val tid: Int) extends Person(name, age){
override def equals(other: Any): Boolean = other match {
case that: Teacher =>
this.getClass == that.getClass &&
name == that.name &&
age == that.age
case _ => false
}
}

val s1 = new Student("z", 1, 2)
val t1 = new Teacher("z", 1, 2)
println(s1 == t1) // true
println(t1 == s1) // false 违反了对称性

正确的写法应该是:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Teacher(override val name: String, override val age: Int, val tid: Int) extends Person(name, age){
override def canEqual(other: Any): Boolean = other.isInstanceOf[Teacher]

override def equals(other: Any): Boolean = other match { // 问题 3 的写法
case that: Teacher =>
super.equals(that) &&
(that canEqual this) &&
name == that.name &&
age == that.age &&
tid == that.tid
case _ => false
}
}

注意只覆盖了 canEqual 方法也会违反对称性。在 Java 中要实现相同的效果,则也需要编写类似的 canEqual 方法,就留给读者自己考虑了。

总之,在编写单个类的 equals 方法时比较简单,当涉及子类继承时,就要多考虑一下了。

另外不要忘记覆盖 hashcode 方法哦。

Split in Java

在 Java 中处理字符串时,split 是一个很常用的操作,但是这一简单的操作,却经常有意想不到的结果,就拿Guava库官方教程中的一个例子来说,",a,,b,".split(",") 的结果是?

1
2
3
4
5
1. "", "a", "", "b", ""
2. null, "a", null, "b", null
3. "a", null, "b"
4. "a", "b"
5. None of the above

正确答案应该是 5,以上都不对;正确结果是 ["", "a", "", "b"]

正是因为 JDK 自带的 split 这种奇怪的现象,其他开源库也都给出了自己的 split 方法,如 Apache Commons Lang 和上文中的 Guava 。

split in JDK8

String 类包含两个 split 重载方法,public String[] split(String regex)public String[] split(String regex, int limit),调用前者就相当于默认 limit = 0,而上面的例子中奇怪的现象就和这个 limit 有关。

JDK 文档中是这么解释的:

  1. limitn 大于 0 时,会返回至多 n 项,最后一项会包含所有未被拆分的部分
  2. n 小于 0 时,会返回所有拆分后的结果
  3. n 等于 0 时,会返回所有拆分后的结果,但是最后跟着的空字符串会被删除

由于使用了单参数的 split 方法,n == 0,于是就产生了如上的结果。关于这一部分的 JDK 中的源码部分如下:

1
2
3
4
5
6
7
// Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}

平常在分析一些具有固定格式的数据时,比如每一行都是 tab 分割的,且有固定列数,那么进行解析时可以使用 s.split("\t", -1) 来进行操作。这样会保存所有的分割项,包含任意部位的空字符串,比如

1
":a::b::".split(":", -1) => ["", "a", "", "b", "", ""]

另外一个需要注意的地方是,split 接收的参数是一个正则表达式,这一点经常容易忽略。比如 "a.b.c".split(".") 的结果是 [],长度为 0 ,因为首先 . 匹配任意字符,所以原字符串中每一个都是分割符,这就产生了 6 个空字符串, 然后 limit 默认为 0 ,从后往前删除空字符串,结果就为空。

split in Commons Lang

JDK 中的方法毕竟还是简单了一些,不能满足我们一些特殊需求,或者说不想使用正则,那么可以使用 Commons Lang 库中的方法。这些 split 方法有以下特点:

  1. 如果没有指定结果个数,都默认输出最多项
  2. 如果没有 PreserveAllTokens 后缀,默认将多个连续分割符视为 1 个,不保留任意位置空字符串
    比如:
    1
    StringUtils.split("::a::b::", ":") => ["a", "b"]

需要注意的是 split(String str, String separatorChars) 方法中第二个参数的意义是每一个字符都被当成分割符,比如:

1
StringUtils.split(":a:b:", "ab") => [":", ":", ":"]

那么假如我想用 "ab" 整体作为分割符呢,可以使用 splitByWholeSeparator 方法:

1
StringUtils.splitByWholeSeparator("abcabc","ab") => ["c", "c"]

但这个方法有一个和其他方法表现不一致的地方,它保留了末尾的空字符串,且只保留一个。

1
2
3
StringUtils.splitByWholeSeparator("abb", "bb") => ["a", ""]
StringUtils.splitByWholeSeparator("bba", "bb") => ["a"]
StringUtils.splitByWholeSeparator("abbbbabbbb", "bb") =>["a", "a", ""]

另外一个我觉得很有用的就是一系列 splitPreserveAllTokens 重载函数了,因为默认输出所有结果,且保留了空字符串。和 JDK 中的 limit = -1 结果一致,但更易读一些。

split in Guava

假如你已经被上面这些特殊情况都绕晕了,不妨试试 Guava 库,它没有提供简单的一系列重载 split 方法,而是提供了一系列的工厂方法,采用链式调用,从而从方法名上就能看出结果,不用苦思冥想到底有没有陷阱。

1
2
3
4
5
6
7
Splitter.on(",")
.trimResults(CharMatcher.is(','))
.omitEmptyStrings()
.limit(2)
.split("a,,,,,b,,,,c,,,")

=> ["a", "b,,,,c"]

除了按照分割符外,还可以按照长度:

1
Splitter.fixedLength(3).split("abcde") => ["abc", "de"]

不像 JDK 和 Commons Lang 中的返回数组,Guava 返回 IterableList,而且这个 Iterable 已经重载了 toString,可以方便地进行打印测试。