鲜活的Java 8 --- Java 8 进化之路

最近在学习java8的新特性。 首先先来安装java8。 Java8 在 win7下很容易安装,但是官方不支持win XP. windows Xp java8无法安装。如果在xp下安装会报这样的错误:

无法定位程序输入点 RegDeleteKeyExA 于动态链接库 ADVAPI32.dll


如果非要在32位的xp系统,可以先下载java8的jdk的exe文件。然后使用7zip进行解压。再解压tools.zip文件。最后你能得到下面的目录结构:


在这个目录下执行如下的命令:

1
FOR /R %f IN (*.pack) DO "d:\java8\bin\unpack200.exe" -r -v "%f" "%~pf%~nf.jar"

注意d:\java8\bin\unpack200.exe 这个。这个是你的解压路径中的unpack200.exe。上面的命令就是使用解压中的unpack200.exe将整个解压路径中的.pack文件转换为.jar文件. 转换完之后,d:\java8这个路径就已经是JAVA_HOME路径。

下面就是IDE也能使用java1.8的新特性。下载一个新版本的eclipse。另外如果是eclipse kepler 可以在eclipse market中下载一个java8的插件"Eclipse Java Development Tools Patch with Java 8 support(for Kepler SR2)"。 然后在tab中windows->Pererfence->java->Installed JREs => “add…”=> Standard VM => JRE home选择之前的那个解压路径。剩下的内容eclipse会帮你填充。然后一路点击确认就可以了。


然后将execute Environments 选中 javaSE-1.8, 然后选中 Java8 ,点击确定。


新建立一个工程。然后将Java Compiler选中 1.8的版本。

然后新建一个interface。编写如下代码。如果IDE没有报错。那么eclipse IDE就已经可以使用java8的新特性了。

1
2
3
4
5
6
7

public interface TestDefault {
default void pirnt(){
System.out.println();
}
}

下面的图里没有编译错误

对于java8, 网上说了55条新特性。但详细看来,最有用的也就那么几条。下面罗列一下:

  1. 消除持久代. 再也没有OutOfMemoryError. PermGen Space 这样的错误了。持久代(Interned 字符串,类元数据和类静态变量)合并入Heap Space或者direct buffer. 以后可能更多的会OutOfMemoryError: Heap space.
  2. Annotation更加强大。可以在函数参数中加入@notnull 这样的标记,来判断参数是否为null. 另外还有可以用来消除public,private 这些关键字。
  3. 加密方面,AES使用了CPU指令,可以更快加解密。用SHA-224代替SHA-1.支持了64位windows的PKCS#11,加解密AES,RSA支持更多的PKCS。
  4. 加入JS 引擎。 可以运行js脚本。这对node.js, webview的使用意义更大
  5. Http URL, 支持Http GET, POST 操作,这个是要在未来代替HttpClient吗?
  6. 使用平衡树处理HashMap中的冲突,对于频繁的冲突会将List替换成平衡树。这样做会提高HashMap的插入速度。
  7. Base64终于可以名正言顺的使用。java.util.Base64.Encoder 和java.util.Base64.Decoder.而不要使用过去的sun.misc.BASE64Encoder等。
  8. 并发的支持。增加适用于频繁更新但非频繁读取的原子变量。ConcurrentHashMap更新,Fork-Join框架更新。Arrays.sort有了并发的sort => Arrays.parallelSort,可以很好的利用多核的资源。据说性能在数据均衡的情况下最少提升了30%。
  9. lamda表达式引入,接口中的default function以及stream

对于以上几点主要说说第8和第9点。对于第8点做了个小实验验证了一下并发情况下的排序性能提升:

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

public class TestJava8 {
public static void main(String[] args) {
int count =10000000;
int[] k = new int[count];
Random r = new Random();
for (int j= 0; j < 5; j++) {
for (int i = 0;i<count; i++) {
k[i] = r.nextInt();
}
int [] g = Arrays.copyOf(k, k.length);
int [] t = Arrays.copyOf(k, k.length);
long start = System.currentTimeMillis();
Arrays.parallelSort(g);

System.out.println(System.currentTimeMillis() - start+":parallel:");

start = System.currentTimeMillis();
Arrays.sort(t);
System.out.println(System.currentTimeMillis() - start+":serial:");
}

}
}

在公司里的伪双核的机器上跑。反复几次,当如也有可能期间夹杂了gc()的影响。结果基本是这样的:

1
2
3
4
5
6
7
8
9
10
11
12

1398:parallel:
1373:serial:
1224:parallel:
1287:serial:
1261:parallel:
1274:serial:
1304:parallel:
1314:serial:
1234:parallel:
1480:serial:

从结果上看,基本上并发还是需要使用在数据量较大的情况下。但是如果数据量大是否能使用内存排序还是个问题。对于parallelSort,Java8还是使用的Fork-Join的模式。对于小数据量效果并不佳,因为开启线程还是要费些时间的。我个人认为java8 对于Arrays.sort()这个接口,还是默默的做并发/非并发判断比较好。毕竟多核都对java程序员隐藏的话,并发sort()啥的最好也能对java程序员隐藏。否则又要在上面包一层先判断一下数据量大小,然后再执行sort() or parallelSort().  另外官方所说的数据均衡的情况下最少提升了30%。这个意思是说官方的测试在比较均衡的数据集跑出来的,但是一般在生产环境中的数据集是不均衡的。

java8 最大的亮点就是lamda表达式。可以使用java进行函数式编程。这样的好处是什么呢?

  1. 如果lamda表达式只对变量进行只读操作,那一定是threadSafe. 非只读的情况需要特殊判读是否是threadSafe.
  2. 如果是threadSafe 一定可以并发执行
  3. 从参数推出结果,是针对API接口编程。(这点有些虚)
  4. 返回的结果也可以是函数

一段代码这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

// 求一个List中的最大值
public static void main(String[] args) {
List<Integer> k = new ArrayList<Integer>();
k.add(1);
k.add(2);
k.add(3);
k.add(8);
k.add(11);
k.add(20);
k.add(50);
int max = Integer.MIN_VALUE;
for (int j : k) {
if (max < j) {
max = j;
}
}
System.out.println(max);
}

假如这段代码中查找最大值成了性能瓶颈,那么需要将这段代码改为并发操作。想想就不容易。那现在使用lamda表达式这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 求一个链表中的最大值
public static void main(String[] args) {

List<Integer> k = new ArrayList<Integer>();
k.add(1);
k.add(2);
k.add(3);
k.add(8);
k.add(11);
k.add(20);
k.add(50);
Optional<Integer> max = k.stream().max((a,b) -> {return a-b;});
System.out.println(max);
}

想变成并发操作这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 求一个链表中的最大值
public static void main(String[] args) {

List<Integer> k = new ArrayList<Integer>();
k.add(1);
k.add(2);
k.add(3);
k.add(8);
k.add(11);
k.add(20);
k.add(50);
Optional<Integer> max = k.parallelStream().max((a,b) -> {return a-b;});
System.out.println(max);
}

很简单吧。就是将k.stream() 改成 k.parallelStream() 而这个之所以简单是基于这样几点。

  1. 编写ThreadSafe的函数或者lamda表达式, 本身就很容易变为多线程操作。

  2. Stream方式处理数据。

  3. 传入的参数数据只关系上一步的结果,而不关系其他数据在本步所做操作产生的中间数据。

  4. Fork-Join提供了很好的多线程框架

这里八卦一下Java8 为什么做了除了lamda表达式之外的一些事情。

  1. interface中的default function接口。 原因是lamda表达式是为了并发做的。那么并发包concurrent中已经有了很多接口,希望和过去的版本兼容,减少迁移的工作量。如果interface中新增了接口,那么使用jdk8以前的代码就会有编译错误。要解决这个编译错误,要么抽象出一个abstract class实现接口,并加入缺省的实现。要么在以前的所有的实现中都加入这个接口。所以听听就觉得这次迁移是个大的工作量。那default function就诞生了。为了不写abstract class, 并且使用jdk5,jdk6,jdk7的代码可以方便迁移到jdk8中。default function 这样使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14

public class TestF {
public interface TestDefault {
default void print(){
System.out.println("ew2we");

}
}
public static void main(String[] args) {
// 写个匿名类
new TestDefault(){}.print();
}
}

另外值得注意的一点就是:如果有两个interface实现了同名的default function. 那编译会报错。

  1. SAM (single abstract method) 也就是这个Annotation @FunctionalInterface.如果带上这个Annotation,那么这个接口仅且只有一个未实现的方法。当然可以有多个已经实现的default方法。这个是为了lamda表达式做的。lamda表达式是形如: (param list)-> {code statements;},基本上可以看作是一个函数。所以要求仅且只有一个函数方法没有实现就是为了代码好看。@FunctionalInterface 写法如下:
1
2
3
4
5
6
7
8
9
10
11

@FunctionalInterface
public interface DefaultInterface {
default void hello() {
System.out.println("hello");
}
default void zzz() {
System.out.println("zzz");
}
void z(); // 只有一个未实现的函数
}
  1. Stream 是为了更好的使用Fork-Join框架。Stream就是pipeline. 对于只关心输入和输出的API来说。编写Stream方式的代码非常容易实现并发。其实就是把一组相似的数据丢到一个池中,然后线程池针对这些数据开始做事情。在执行完毕之前,需要join所有的线程,将最终结果放入到最终返回的池中并返回。而流式的操作最简单的就是调用方便。可以理解为以前所说的链式调用。但是对于这种编写代码的Style,还是尽量将每次调用写在不同的行中,这样会方便调试。下面是Stream的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

public static void main(String[] args) {

List<Integer> k = new ArrayList<Integer>();
k.add(1);
k.add(2);
k.add(3);
k.add(8);
k.add(11);
k.add(20);
k.add(50);
// 找到list中大于2的三个数,不要求顺序,最后打印出来
k.parallelStream().filter((a)->{return a>2;}).unordered().limit(3).forEach((a) -> {System.out.println(a);});
// 获取大于1的list元素中的最大值。如果list中的元素都不大于1,那么返回Optional.empty
Optional<Integer> max = k.parallelStream().filter((a)->{return a>1;}).max((a,b) -> {return a-b;});
System.out.println(max);

}

这里简单介绍一下lamda表达式中语法。 这个lamda表达式就是实现了一个抽象方法。所以需要注意那个抽象方法到底是不是void返回,这关系到你的表达式中是否有return.lamda的语法是前面是参数列表,0个参数或者多于一个参数时必须使用圆括号括起。如果只有一个参数,那么可以省略圆括号。然后是一个箭头->。再后面是函数体。函数体需要使用花括号括起,如果只有一条语句,可以省略花括号。下面的lamda表达式都是对的。

1
2
3
4
5
6

a -> return a+1;
(a) -> return a+1;
(a) -> {return a+1;}
(a,b) -> {int c = a+b; return c>>1;}
()->System.out.println("empty");

另外注意一下final 变量和this指针。

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class TestLamba {

public static class Score {
int score;
public List<Integer> findLarger(List<Integer> scores) {
List<Integer> tmp = new ArrayList<Integer>();
//注意这里的this是 Score Object的this
scores.stream().filter((a) -> {return a > this.score;}).forEach((a)->{tmp.add(a);});
return tmp;
}
public static List<Integer> findLarger(List<Integer> scores, Score myScore) {
List<Integer> tmp = new ArrayList<Integer>();
//myScore = new Score(); compile wrong
//这里的myScore在语义上是final的, 但是myScore中的域可以变化
//myScore.score = 3; 这个是正确的,放在lamda表达式中也正确。
//所以对于myScore而言lamda表达式并不是threadSafe
scores.stream().filter((a) -> {return a > myScore.score;}).forEach((a)->{tmp.add(a);});
return tmp;
}
}

public static void main(String[] args) {
Arrays.asList().forEach((a)->System.out.println("I'm "+a));
List<Integer> k = new ArrayList<Integer>();
k.add(1);
k.add(2);
k.add(3);
k.add(8);
k.add(11);
k.add(20);
k.add(50);
Score s = new Score();
s.score = 11;
s.findLarger(k).forEach(a->{System.out.println(a);});
System.out.println("dsdsd");
Score.findLarger(k, s).forEach(a->{System.out.println(a);});
}
}

参数里面没有final变量,java8在语法上并不要求外部引用参数是final,但是语义上还是final的,这个类似匿名内部类。另外this指针一定是外层类的指针。lamda表达式是没有this的。现在java的jdk已经可能非常容易的使用多核资源。所以非常期待java以后的jdk能够利用多机器的资源例如现在的map/reduce.这样写程序就越来越简单了。看起来就是这样一个趋势~~~继续进化吧~