Java新特性

Java7新特性

Java7语法特性

根据JSR 334,Java7添加了数个语法方面的新特性:

1. switch可以消化String

比如:

1
2
3
4
5
6
7
8
9
10
public static void switchString(String s){
switch (s){
case "db": ...
case "wls": ...
case "ibm": ...
case "soa": ...
case "fa": ...
default: ...
}
}

2. 新的整数字面表达方式 - “0b”前缀和”_”连数符,提升程序员的幸福感。

  • a. 表示二进制字面值的前缀, 0b:
    比如以下三个变量的值相同:

    1
    2
    3
    byte b1 = 0b00100001;     // New
    byte b2 = 0x21; // Old
    byte b3 = 33; // Old
  • b. 用下划线连接整数提升其可读性,自身无含义,不可用在数字的起始和末尾:

    1
    long phone_nbr = 021_1111_2222;

3. 简化了泛型对象创建的语法 - “菱形 new”,以下两个语句等价:

1
2
ArrayList<String> al1 = new ArrayList<String>();    // Old
ArrayList<String> al2 = new ArrayList<>(); // New

4. 为所有的reflect操作异常找了个新爸爸 - ReflectOperationException,孩儿们是:

1
2
3
4
5
6
ClassNotFoundException,
IllegalAccessException,
InstantiationException,
InvocationTargetException,
NoSuchFieldException,
NoSuchMethodException

5. catch有了多重捕获功能,也玩起了包养的勾当,以下代码心领神会:

1
2
3
4
5
6
try{
// code
}
catch (SQLException | IOException ex) {
// ...
}

6. 异常精确重抛 - 重抛时自动造型为子类,有点半主动制导武器的style:

1
2
3
4
5
6
7
8
public void test() throws NoSuchMethodException, NoSuchFieldException{    // 子类
try{
// code
}
catch (RelectiveOperationException ex){ // 父类
throws ex;
}
}

7. 发明了try()结构 - Try with Resources,能够自动接住异常并关闭资源(所谓的资源需要利用新的java.lang.AutoCloseable接口),注意以下代码中try后面跟的是”(“不是”{“:

1
2
3
try(BufferedReader br = new BufferedReader(new FileReader("/home/oracle/temp.txt"))){
... br.readLine() ...
}

try-with-resources语句可以带catch,也可以向上例一样一个catch也没有。

Java7 NIO 新方法

整体来说,对IO操作进行了优化,使用起来更加顺手,甚至可以替换apache common-io包。

1.增加java.nio.file.Paths用于目录操作

1
2
3
4
Path path = Paths.get("/home/shenyanchao", "Desktop");
System.out.println(path.toAbsolutePath());
System.out.println(path.getParent());
System.out.println(path.getFileSystem().isOpen());

2.增加java.nio.file.Files工具类来处理文件

1
2
3
4
5
6
7
8
9
10
11
Files.copy(src,dest, StandardCopyOption.COPY_ATTRIBUTES,StandardCopyOption.REPLACE_EXISTING);

Files.move(src,dest,StandardCopyOption.ATOMIC_MOVE);

Files.createLink(src,dest);
Files.createSymbolicLink(src,dest);
Files.deleteIfExists(dest);

Files.readAllLines(src);

Files.createTempFile(src,"aa","bb");

3.目录树遍历

使用FileVisitor来实现访问者模式。

1
2
3
4
preVisitDirectory(T dir, BasicFileAttributes attrs);
visitFile(T dir, BasicFileAttributes attrs);
visitFileFailed(T dir, IOException exc);
postVisitDirectory(T dir, IOException exc);

4.使用WatchService来监控目录,变化请通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
WatchService watchService = FileSystems.getDefault().newWatchService();
Path path = Paths.get("/home/shenyanchao/Documents");
WatchKey watchKey = path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
while (true) {
List<WatchEvent<?>> watchEvents = watchService.take().pollEvents();
for (WatchEvent<?> watchEvent : watchEvents) {
System.out.printf("[%s]文件发生了[%s]事件。%n", watchEvent.context(), watchEvent.kind());
}
boolean valid = watchKey.reset();
if (!valid){
break;
}
}

Java7并发(JSR166Y)

Fork Join框架,大任务分解为小任务

通过ForkJoinPool,ForkJoinTask来实现的。

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 Fibonacci extends RecursiveTask<Integer> {

final int n;

Fibonacci(int n) {
this.n = n;
}

@Override
protected Integer compute() {
if (n <= 1)
return n;
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
f2.fork();
return f1.join() + f2.join();
}

public static void main(String[] args) {
Fibonacci fibonacci = new Fibonacci(4);
System.out.println(fibonacci.compute());
}
}

TransferQueue,ConcurrentLinkedDeque等新类

TransferQueue是一个继承了 BlockingQueue的接口,并且增加若干新的方法。

ThreadLocalRandom用于生成随机数

1
ThreadLocalRandom.current().nextInt (...)

Random是线程安全的,但速度较慢。而这个是快速的,但是速度很快。适用于线程内部的使用。

Java7 client

诸如更新了很多swing显示相关的api.
更好的支持linux fonts

Java7 VM新特性

1.引入Garbage First回收算法

Garbage First简称G1,它的目标是要做到尽量减少GC所导致的应用暂停的时间,让应用达到准实时的效果,同时保持JVM堆空间的利用率。用于替代CMS


参考文档:http://www.slideshare.net/boulderjug/55-things-in-java-7

Java8新特性

1.接口默认方法[接口允许有实现啦]

Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法。

1
2
3
4
5
6
7
interface Formula {
double calculate(int a);

default double sqrt(int a) {
return Math.sqrt(a);
}
}

2.Lambda表达式

据说借鉴了各种动态语言的新特性,比如scala,python
首先看看在老版本的Java中是如何排列字符串的:

1
2
3
4
5
6
7
8
List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});

Java 8 提供了更为简介的语法,lambda表达式:

1
2
3
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});

可以更短:

1
Collections.sort(names, (String a, String b) -> b.compareTo(a));

再短:

1
Collections.sort(names, (a, b) -> b.compareTo(a));

3.@FunctionalInterface

这是新引入的一个注解,用于支持lambda。用FunctionalInterface标识的接口,必须只包含1个抽象方法。否则会编译报错的。因为如果有多个抽象方法,lambda就无法知道对应哪个方法了。

1
2
3
4
5
6
7
8
@FunctionalInterface
interface Converter<F, T> {
T convert(F from);
}

Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted); // 123

例子中,(from) -> Integer.valueOf(from)这个lambda表达式指出了convert方法的具体实现。

(1)方法与构造函数的引用::

Java 8 允许你使用 :: 关键字来传递方法或者构造函数引用,下面的代码展示了如何引用一个静态方法

1
2
3
Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted); // 123

当然也可以引用一个对象的方法:

1
2
3
converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted); // "J"

那么如何引用构造函数呢?

1
2
3
4
5
6
7
8
9
10
11
class Person {
String firstName;
String lastName;

Person() {}

Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}

这是一个pojo类。下面创建一个用于创建person对象的FunctionalInterface:

1
2
3
interface PersonFactory<P extends Person> {
P create(String firstName, String lastName);
}

这里我们使用构造函数引用来将他们关联起来,而不是实现一个完整的工厂:

1
2
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

我们只需要使用 Person::new 来获取Person类构造函数的引用,Java编译器会自动根据PersonFactory.create方法的签名来选择合适的构造函数。

(2)内嵌的Functional Interfaces

上面提到的Comparator接口,为什么可以使用lambda表达式,正是因为在java 8中,被定义为了FunctionalInterface。这些已经存在的接口是通过添加@FunctionalInterface注解来支持的。

另外,java 8 api还提供了丰富的接口。这些接口貌似都是从Google Guava包里得到的提示,命名甚至都是一样的。

  • Predicates

Predicate 接口只有一个参数,返回boolean类型。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非):

1
2
3
4
5
6
7
8
9
10
Predicate<String> predicate = (s) -> s.length() > 0;

predicate.test("foo"); // true
predicate.negate().test("foo"); // false

Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
  • Function

Function 接口有一个参数并且返回一个结果,并附带了一些可以和其他函数组合的默认方法(compose, andThen):

1
2
3
4
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);

backToString.apply("123"); // "123"
  • Supplier

Supplier 接口返回一个给定类型的泛型,和Function接口不同的是该接口不接受任何参数.

1
2
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person
  • Consumer

Consumer在一个输入参数上做一些操作。

1
2
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
  • Comparator

Comparator是早就存在的,不过java 8提供了一些默认方法。

1
2
3
4
5
6
7
Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);

Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");

comparator.compare(p1, p2); // > 0
comparator.reversed().compare(p1, p2); // < 0
  • Optional

Optional其实并不是一个FunctionInterface,而是一个用来避免NullPointerException的工具。

1
2
3
4
5
6
7
Optional<String> optional = Optional.of("bam");

optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"

optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
  • Stream

java.util.stream.Stream代表了能在其上做一系列操作的一串元素。在java 8中,Collections被扩展了。我们可以通过Collections.stream()或者Collections.parallelStream()来创建Stream。
首先,新建一个数组。

1
2
3
4
5
6
7
8
9
List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");

具体使用如下:

1
2
3
4
5
6
7
stringCollection
.stream()
.sorted()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);

// "aaa1", "aaa2"

map()函数可以把每个值转换为另外的值。

1
2
3
4
5
6
7
stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);

// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

另外提供有各种match方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boolean anyStartsWithA =
stringCollection
.stream()
.anyMatch((s) -> s.startsWith("a"));

System.out.println(anyStartsWithA); // true

boolean allStartsWithA =
stringCollection
.stream()
.allMatch((s) -> s.startsWith("a"));

System.out.println(allStartsWithA); // false

boolean noneStartsWithZ =
stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z"));

System.out.println(noneStartsWithZ); // true

count()方法返回的是一个值。

1
2
3
4
5
6
7
long startsWithB =
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();

System.out.println(startsWithB); // 3

reduce则进行了归一化处理:

1
2
3
4
5
6
7
8
Optional<String> reduced =
stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
  • ParallelStream

它是一个并行的。速度要比stream快的多。

  • Map

Map并不支持Stream,不过Java 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
25
26
27
28
29
30
31
32
33
Map<Integer, String> map = new HashMap<>();

for (int i = 0; i < 10; i++) {
map.putIfAbsent(i, "val" + i);
}

map.forEach((id, val) -> System.out.println(val));

map.computeIfPresent(3, (num, val) -> val + num);
map.get(3); // val33

map.computeIfPresent(9, (num, val) -> null);
map.containsKey(9); // false

map.computeIfAbsent(23, num -> "val" + num);
map.containsKey(23); // true

map.computeIfAbsent(3, num -> "bam");
map.get(3); // val33

map.remove(3, "val3");
map.get(3); // val33

map.remove(3, "val33");
map.get(3); // null

map.getOrDefault(42, "not found"); // not found

map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
map.get(9); // val9

map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9); // val9concat

4.时间API

Java8更新了Date API,这个新的api与joda-time类似。

(1)Clock

Clock类提供了访问当前日期和时间的方法,Clock是时区敏感的,可以用来取代 System.currentTimeMillis() 来获取当前的微秒数。某一个特定的时间点也可以使用Instant类来表示,Instant类也可以用来创建老的java.util.Date对象。

1
2
3
4
5
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();

Instant instant = clock.instant();
Date legacyDate = Date.from(instant); // legacy java.util.Date

(2)TimeZones

在新API中时区使用ZoneId来表示。时区可以很方便的使用静态方法of来获取到。 时区定义了到UTS时间的时间差,在Instant时间点对象到本地日期对象之间转换的时候是极其重要的

1
2
3
4
5
6
7
8
9
10
System.out.println(ZoneId.getAvailableZoneIds());
// prints all available timezone ids

ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());

// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]

(3)LocalTime

LocalTime 定义了一个没有时区信息的时间,例如 晚上10点,或者 17:30:15。下面的例子使用前面代码创建的时区创建了两个本地时间。之后比较时间并以小时和分钟为单位计算两个时间的时间差:

1
2
3
4
5
6
7
8
9
10
LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);

System.out.println(now1.isBefore(now2)); // false

long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);

System.out.println(hoursBetween); // -3
System.out.println(minutesBetween); // -239

LocalTime 提供了多种工厂方法来简化对象的创建,包括解析时间字符串

1
2
3
4
5
6
7
8
9
10
LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late); // 23:59:59

DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.GERMAN);

LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime); // 13:37

(4)LocalDate

LocalDate 表示了一个确切的日期,比如 2014-03-11。该对象值是不可变的,用起来和LocalTime基本一致。下面的例子展示了如何给Date对象加减天/月/年。另外要注意的是这些对象是不可变的,操作返回的总是一个新实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);

LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY

DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.GERMAN);

LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas); // 2014-12-24

(5)LocalDateTime

LocalDateTime 同时表示了时间和日期,相当于前两节内容合并到一个对象上了。LocalDateTime和LocalTime还有LocalDate一样,都是不可变的。LocalDateTime提供了一些能访问具体字段的方法。

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
LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);

DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek); // WEDNESDAY

Month month = sylvester.getMonth();
System.out.println(month); // DECEMBER

long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay); // 1439

Instant instant = sylvester
.atZone(ZoneId.systemDefault())
.toInstant();

Date legacyDate = Date.from(instant);
System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014

DateTimeFormatter formatter =
DateTimeFormatter
.ofPattern("MMM dd, yyyy - HH:mm");

LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string); // Nov 03, 2014 - 07:13

5.支持重复注解了,注解也可以用在任何地方了

java8之前,类,属性,方法才有注解,现在几乎任何地方都可以了。

1
2
3
4
new @Interned MyObject();
myString = (@NonNull String) str;

void monitorTemperature() throws @Critical TemperatureException { ... }

之前要实现重复注解需要这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public @interface Authority {
String role();
}

public @interface Authorities {
Authority[] value();
}

public class RepeatAnnotationUseOldVersion {

@Authorities({@Authority(role="Admin"),@Authority(role="Manager")})
public void doSomeThing(){
}
}

而现在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Repeatable(Authorities.class)
public @interface Authority {
String role();
}

public @interface Authorities {
Authority[] value();
}

public class RepeatAnnotationUseNewVersion {
@Authority(role="Admin")
@Authority(role="Manager")
public void doSomeThing(){ }
}

6.Nashorn JavaScript 引擎

简单的说,它是 Rhino 的接替者.升级啦。

7.StampedLock

它是java8在java.util.concurrent.locks新增的一个API。

ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。

然而,如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。

StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量!!

它是java8在java.util.concurrent.locks新增的一个API。

ReentrantReadWriteLock 在沒有任何读写锁时,才可以取得写入锁,这可用于实现了悲观读取(Pessimistic Reading),即如果执行中进行读取时,经常可能有另一执行要写入的需求,为了保持同步,ReentrantReadWriteLock 的读取锁定就可派上用场。

然而,如果读取执行情况很多,写入很少的情况下,使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题,也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。

StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

所谓的乐观读模式,也就是若读的操作很多,写的操作很少的情况下,你可以乐观地认为,写入与读取同时发生几率很少,因此不悲观地使用完全的读取锁定,程序可以查看读取资料之后,是否遭到写入执行的变更,再采取后续的措施(重新读取变更信息,或者抛出异常) ,这一个小小改进,可大幅度提高程序的吞吐量!!

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
45
46
47
48
49
50
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
//下面看看乐观读锁案例
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
double currentX = x, currentY = y; //将两个字段读入本地局部变量
if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
try {
currentX = x; // 将两个字段读入本地局部变量
currentY = y; // 将两个字段读入本地局部变量
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
//下面是悲观读锁案例
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
if (ws != 0L) { //这是确认转为写锁是否成功
stamp = ws; //如果成功 替换票据
x = newX; //进行状态改变
y = newY; //进行状态改变
break;
}
else { //如果不能成功转换为写锁
sl.unlockRead(stamp); //我们显式释放读锁
stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
}
}
} finally {
sl.unlock(stamp); //释放读锁或写锁
}
}
}

参考文档:http://winterbe.com/posts/2014/03/16/java-8-tutorial/

申艳超 wechat
欢迎关注微信订阅号~
您的支持是我原创的动力!