当前位置: 首页>编程语言>正文

Java 8—Java 10 特性详解(中)

类型注解

什么是类型注解

注解大家都知道,从java5开始加入这一特性,发展到现在已然是遍地开花,在很多框架中得到了广泛的使用,用来简化程序中的配置。那充满争议的类型注解究竟是什么 复杂还是便捷

1.在java 8之前,注解只能是在声明的地方所使用,比如类,方法,属性;

2.java 8里面,注解可以应用在任何地方,比如:

创建类实例

new @Interned MyObject();

类型映射

myString = (@NonNull String) str;

implements 语句中

class UnmodifiableList<T> implements @Readonly List<@Readonly T> { … }

throw exception声明

void monitorTemperature() throws @Critical TemperatureException { … }

需要注意的是,类型注解只是语法而不是语义,并不会影响java的编译时间,加载时间,以及运行时间,也就是说,编译成class文件的时候并不包含类型注解。

类型注解的作用

先看看下面代码

Collections.emptyList().add("One");

int i=Integer.parseInt("hello");

System.console().readLine();

上面的代码编译是通过的,但运行是会分别报UnsupportedOperationException;NumberFormatException;NullPointerException异常,这些都是runtime error;

类型注解被用来支持在Java的程序中做强类型检查。配合插件式的check framework,可以在编译的时候检测出runtime error,以提高代码质量。这就是类型注解的作用了。

check framework是第三方工具,配合Java的类型注解效果就是1+1>2。它可以嵌入到javac编译器里面,可以配合ant和maven使用, 地址是http://types.cs.washington.edu/checker-framework/。check framework可以找到类型注解出现的地方并检查,举个简单的例子:

import checkers.nullness.quals.*;

public class GetStarted {

void sample() {

@NonNull Object ref = new Object();

}

}

使用javac编译上面的类

javac -processor checkers.nullness.NullnessChecker GetStarted.java

编译是通过,但如果修改成

@NonNull Object ref = null;

再次编译,则出现

GetStarted.java:5:

incompatible types.

found : @Nullable <nulltype>

required: @NonNull Object

@NonNull Object ref = null;

^1 error

类型注解向下兼容的解决方案

如果你不想使用类型注解检测出来错误,则不需要processor,直接javac GetStarted.java是可以编译通过的,这是在java 8 with Type Annotation Support版本里面可以,但java 5,6,7版本都不行,因为javac编译器不知道@NonNull是什么东西,但check framework 有个向下兼容的解决方案,就是将类型注解nonnull用/**/注释起来,比如上面例子修改为

import checkers.nullness.quals.*;

public class GetStarted {

void sample() {

/*@NonNull*/ Object ref = null;

}

}

这样javac编译器就会忽略掉注释块,但用check framework里面的javac编译器同样能够检测出nonnull错误。通过类型注解+check framework我们可以看到,现在runtime error可以在编译时候就能找到。

关于JSR 308

JSR 308想要解决在Java 1.5注解中出现的两个问题:

在句法上对注解的限制: 只能把注解写在声明的地方

类型系统在语义上的限制: 类型系统还做不到预防所有的bug

JSR 308 通过如下方法解决上述两个问题:

对Java语言的句法进行扩充,允许注解出现在更多的位置上。包括: 方法接收器(method receivers,译注: 例public int size() @Readonly { … }),泛型参数,数组,类型转换,类型测试,对象创建,类型参数绑定,类继承和throws子句。其实就是类型注解,现在是java 8的一个特性

通过引入可插拔的类型系统(pluggable type systems)能够创建功能更强大的注解处理器。类型检查器对带有类型限定注解的源码进行分析,一旦发现不匹配等错误之处就会产生警告信息。其实就是check framework

对JSR308,有人反对,觉得更复杂更静态了,比如

@NotEmpty List<@NonNull String> strings = new ArrayList<@NonNull String>()>

换成动态语言为

var strings = ["one", "two"];

有人赞成,说到底,代码才是“最根本”的文档。代码中包含的注解清楚表明了代码编写者的意图。当没有及时更新或者有遗漏的时候,恰恰是注解中包含的意图信息,最容易在其他文档中被丢失。而且将运行时的错误转到编译阶段,不但可以加速开发进程,还可以节省测试时检查bug的时间。

总结

并不是人人都喜欢这个特性,特别是动态语言比较流行的今天,所幸,java 8并不强求大家使用这个特性,反对的人可以不使用这一特性,而对代码质量有些要求比较高的人或公司可以采用JSR 308,毕竟代码才是“最基本”的文档,这句话我是赞同的。虽然代码会增多,但可以使你的代码更具有表达意义。对这个特性有何看法,大家各抒己见。。。。

重复注解

什么是重复注解

允许在同一申明类型(类,属性,或方法)的多次使用同一个注解

JDK8之前

java 8之前也有重复使用注解的解决方案,但可读性不是很好,比如下面的代码:

public @interface Authority {

Stringrole();

}

public @interface Authorities {

Authority[] value();

}

public classRepeatAnnotationUseOldVersion{

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

publicvoiddoSomeThing(){

}

}

由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解。

Jdk8重复注解

我们再来看看java 8里面的做法

@Repeatable(Authorities.class)

public@interfaceAuthority

{

Stringrole();

}

public @interface Authorities {

Authority[] value();

}

public classRepeatAnnotationUseNewVersion{

@Authority(role="Admin")

@Authority(role="Manager")

publicvoiddoSomeThing(){ }

}

不同的地方是,创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解。从上面例子看出,java 8里面做法更适合常规的思维,可读性强一点

总结

JEP120没有太多内容,是一个小特性,仅仅是为了提高代码可读性。这次java 8对注解做了2个方面的改进(JEP 104,JEP120),相信注解会比以前使用得更加频繁了。

类型推断

简单理解泛型

泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。通俗点将就是“类型的变量”。这种类型变量可以用在类、接口和方法的创建中。

理解Java泛型最简单的方法是把它看成一种便捷语法,能节省你某些Java类型转换(casting)上的操作:

List<Apple> box = new ArrayList<Apple>();box.add(new Apple());Apple apple =box.get(0);

上面的代码自身已表达的很清楚: box是一个装有Apple对象的List。get方法返回一个Apple对象实例,这个过程不需要进行类型转换。没有泛型,上面的代码需要写成这样:

Apple apple = (Apple)box.get(0);

泛型的尴尬

泛型的最大优点是提供了程序的类型安全同时可以向后兼容,但也有尴尬的地方,就是每次定义时都要写明泛型的类型,这样显示指定不仅感觉有些冗长,最主要是很多程序员不熟悉泛型,因此很多时候不能够给出正确的类型参数,现在通过编译器自动推断泛型的参数类型,能够减少这样的情况,并提高代码可读性。

java7的泛型类型推断改进

在以前的版本中使用泛型类型,需要在声明并赋值的时候,两侧都加上泛型类型。例如:

Map<String, String> myMap = new HashMap<String, String>();

你可能觉得:老子在声明变量的的时候已经指明了参数类型,为毛还要在初始化对象时再指定 幸好,在Java SE 7中,这种方式得以改进,现在你可以使用如下语句进行声明并赋值:

Map myMap = new HashMap<>(); //注意后面的"<>"

在这条语句中,编译器会根据变量声明时的泛型类型自动推断出实例化HashMap时的泛型类型。再次提醒一定要注意new HashMap后面的“<>”,只有加上这个“<>”才表示是自动类型推断,否则就是非泛型类型的HashMap,并且在使用编译器编译源代码时会给出一个警告提示。

但是: Java SE 7在创建泛型实例时的类型推断是有限制的: 只有构造器的参数化类型在上下文中被显著的声明了,才可以使用类型推断,否则不行。例如: 下面的例子在java 7无法正确编译(但现在在java8里面可以编译,因为根据方法参数来自动推断泛型的类型):

List list =newArrayList<>();

list.add("A");// 由于addAll期望获得Collection<extends String>类型的参数,因此下面的语句无法通过

list.addAll(newArrayList<>());

Java8的泛型类型推断改进

java8里面泛型的目标类型推断主要2个:

1.支持通过方法上下文推断泛型目标类型

2.支持在方法调用链路当中,泛型类型推断传递到最后一个方法

让我们看看官网的例子

classList{

staticListnil(){ ... };

staticListcons(Z head, List<Z> tail){ ... };

Ehead(){ ... }

}

根据JEP101的特性,我们在调用上面方法的时候可以这样写

//通过方法赋值的目标参数来自动推断泛型的类型

List l = List.nil();

//而不是显示的指定类型

//List<String> l = List.<String>nil();

//通过前面方法参数类型推断泛型的类型

List.cons(42, List.nil());

//而不是显示的指定类型

//List.cons(42, List.<Integer>nil());

总结

以上是JEP101的特性内容了,Java作为静态语言的代表者,可以说类型系统相当丰富。导致类型间互相转换的问题困扰着每个java程序员,通过编译器自动推断类型的东西可以稍微缓解一下类型转换太复杂的问题。虽然说是小进步,但对于我们天天写代码的程序员,肯定能带来巨大的作用,至少心情更愉悦了。

JRE 精简

JRE精简好处

更小的Java环境需要更少的计算资源。

一个较小的运行时环境可以更好的优化性能和启动时间。

消除未使用的代码从安全的角度总是好的。

这些打包的应用程序可以下载速度更快。

概念

紧凑的JRE分3种,分别是compact1、compact2、compact3,他们的关系是compact1<compact2<compact3,他们包含的API如下图所示

使用javac根据profile编译应用程序

javac –bootclasspath, or javac –profile

如果不符合compact的api,则报错。

$ javac -profile compact2 Test.java

Test.java:7: error: ThreadMXBean is not available in profile'compact2'

ThreadMXBean bean = ManagementFactory.getThreadMXBean();

^

Test.java:7: error: ManagementFactory is not available in profile'compact2'

ThreadMXBean bean = ManagementFactory.getThreadMXBean();

^

2errors

Java 8—Java 10 特性详解(中),第1张

使用工具开发的效果

Java 8—Java 10 特性详解(中),第2张

JPEDS工具使用

java8新增一个工具,用来分析应用程序所依赖的profile,有三个参数比较常用 -p,-v,-r

import java.util.Set;

import java.util.HashSet;

public classDeps{

publicstaticvoidmain(String[] args){

System.out.println(Math.random());

Set<String> set = new HashSet<>();

}

}

************** PROFILE ********************

jdeps -P Deps.class

Deps.class -> /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre/lib/rt.jar

<unnamed> (Deps.class)

-> java.iocompact1

-> java.langcompact1

-> java.utilcompact1

************** VERBOSE ********************

jdeps -v Deps.class

Deps.class -> /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre/lib/rt.jar

Deps (Deps.class)

-> java.io.PrintStream

-> java.lang.Math

-> java.lang.Object

-> java.lang.String

-> java.lang.System

-> java.util.HashSet

************** RECURSIVE ********************

jdeps -R Deps.class

Deps.class -> /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre/lib/rt.jar

<unnamed> (Deps.class)

-> java.io

-> java.lang

-> java.util

/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre/lib/jce.jar -> /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre/lib/rt.jar

javax.crypto (jce.jar)

-> java.io

-> java.lang

-> java.lang.reflect

-> java.net

-> java.nio

-> java.security

-> java.security.cert

-> java.security.spec

-> java.util

-> java.util.concurrent

-> java.util.jar

-> java.util.regex

-> java.util.zip

-> javax.security.auth

-> sun.security.jca JDK internal API (rt.jar)

-> sun.security.utilJDK internal API (rt.jar)

-> sun.security.validator JDK internal API (rt.jar)

javax.crypto.interfaces (jce.jar)

-> java.lang

-> java.math

-> java.security

javax.crypto.spec (jce.jar)

-> java.lang

-> java.math

-> java.security.spec

-> java.util

/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre/lib/rt.jar -> /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre/lib/jce.jar

java.security (rt.jar)

-> javax.crypto JDK internal API (jce.jar)

sun.security.util (rt.jar)

-> javax.crypto JDK internal API (jce.jar)

-> javax.crypto.interfacesJDK internal API (jce.jar)

-> javax.crypto.specJDK internal API (jce.jar)

在linux上构建profile

$ hg clone http://hg.openjdk.java.net/jdk8/jdk8/

$ cd jdk8

$ make images profiles :

# Finishedprofiles(build time00:00:27)

----- Build times -------

Start 2013-03-17 14:47:35

End 2013-03-17 14:58:26

00:00:25 corba

00:00:15 demos

00:01:50 hotspot

00:00:24 images

00:00:21 jaxp

00:00:31 jaxws

00:05:37 jdk

00:00:43 langtools

00:00:18 nashorn

00:00:27 profiles

00:10:51 TOTAL

-------------------------

Finished buildingJava(TM)fortarget 'images profiles'

$ cd images

$ ls -d *image

j2re-compact1-image j2re-compact2-image j2re-compact3-image j2re-image j2sdk-image

编译后compact大致的占用空间

Java 8—Java 10 特性详解(中),第3张

总结

如今,物联网正风行一时。我们看到大量不同的设备在市场上出现,每一种的更新速度都越来越快。java需要一个占用资源少的JRE运行环境,紧凑的JRE特性的出现,希望能带来以后的物联网的发展,甚至还是会有大量的java应用程序出现在物联网上面。目前oracle也发布了针对raspberry pi的JRE了。

另外该特性也是为java9的模块化项目做准备,模块化特性是javaer所期待的特性。他是解决业务系统复杂度的一个利器,当然OSGI也是相当的出色。但osgi对于新学者来说未免太复杂了。

LocalDate/LocalDateTime

Java8之前的Date有哪些槽点

Tiago Fernandez做过一次投票,选举最烂的JAVA API,排第一的EJB2.X,第二的就是日期API。

槽点一

最开始的时候,Date既要承载日期信息,又要做日期之间的转换,还要做不同日期格式的显示,职责较繁杂(不懂单一职责,你妈妈知道吗 纯属恶搞~哈哈)

后来从JDK 1.1 开始,这三项职责分开了:

使用Calendar类实现日期和时间字段之间转换;

使用DateFormat类来格式化和分析日期字符串;

而Date只用来承载日期和时间信息。

原有Date中的相应方法已废弃。不过,无论是Date,还是Calendar,都用着太不方便了,这是API没有设计好的地方。

槽点二

坑爹的year和month

Date date =newDate(2012,1,1);

System.out.println(date);

输出Thu Feb0100:00:00CST3912

观察输出结果,year是2012+1900,而month,月份参数我不是给了1吗 怎么输出二月(Feb)了

应该曾有人告诉你,如果你要设置日期,应该使用 java.util.Calendar,像这样…

Calendar calendar = Calendar.getInstance();

calendar.set(2013, 8, 2);

这样写又不对了,calendar的month也是从0开始的,表达8月份应该用7这个数字,要么就干脆用枚举

calendar.set(2013, Calendar.AUGUST,2);

注意上面的代码,Calendar年份的传值不需要减去1900(当然月份的定义和Date还是一样),这种不一致真是让人抓狂!

有些人可能知道,Calendar相关的API是IBM捐出去的,所以才导致不一致。

槽点三

java.util.Date与java.util.Calendar中的所有属性都是可变的

下面的代码,计算两个日期之间的天数….

public static void main(String[] args){

Calendar birth = Calendar.getInstance();

birth.set(1975, Calendar.MAY,26);

Calendar now = Calendar.getInstance();

System.out.println(daysBetween(birth, now));

System.out.println(daysBetween(birth, now));// 显示 0

}

public static long daysBetween(Calendar begin, Calendar end){

long daysBetween =0;

while(begin.before(end)) {

begin.add(Calendar.DAY_OF_MONTH,1);

daysBetween++;

}

return daysBetween;

}

daysBetween有点问题,如果连续计算两个Date实例的话,第二次会取得0,因为Calendar状态是可变的,考虑到重复计算的场合,最好复制一个新的Calendar

public static long daysBetween(Calendar begin, Calendar end){

Calendar calendar = (Calendar) begin.clone();// 复制

longdaysBetween =0;

while(calendar.before(end)) {

calendar.add(Calendar.DAY_OF_MONTH,1);

daysBetween++;

}

returndaysBetween;

}

槽点四

SimpleDateTimeFormat是非线程安全的。

Java8时间和日期

类概览

Java 8仍然延用了ISO的日历体系,并且与它的前辈们不同,java.time包中的类是不可变且线程安全的。新的时间及日期API位于java.time包中,下面是里面的一些关键的类:

Instant——它代表的是时间戳

LocalDate——不包含具体时间的日期,比如2014-01-14。它可以用来存储生日,周年纪念日,入职日期等。

LocalTime——它代表的是不含日期的时间

LocalDateTime——它包含了日期及时间,不过还是没有偏移信息或者说时区。

ZonedDateTime——这是一个包含时区的完整的日期时间,偏移量是以UTC/格林威治时间为基准的。

新的库还增加了ZoneOffset及Zoned,可以为时区提供更好的支持。有了新的DateTimeFormatter之后日期的解析及格式化也变得焕然一新了。

方法概览

该包的API提供了大量相关的方法,这些方法一般有一致的方法前缀:

of: 静态工厂方法。

parse: 静态工厂方法,关注于解析。

get: 获取某些东西的值。

is: 检查某些东西的是否是true。

with: 不可变的setter等价物。

plus: 加一些量到某个对象。

minus: 从某个对象减去一些量。

to: 转换到另一个类型。

at: 把这个对象与另一个对象组合起来,例如: date.atTime(time)。

一些例子

public classTimeIntroduction{

public static voidtestClock()throwsInterruptedException{

//时钟提供给我们用于访问某个特定 时区的 瞬时时间、日期 和 时间的。

Clock c1 = Clock.systemUTC(); //系统默认UTC时钟(当前瞬时时间 System.currentTimeMillis())

System.out.println(c1.millis()); //每次调用将返回当前瞬时时间(UTC)

Clock c2 = Clock.systemDefaultZone(); //系统默认时区时钟(当前瞬时时间)

Clock c31 = Clock.system(ZoneId.of("Europe/Paris")); //巴黎时区

System.out.println(c31.millis()); //每次调用将返回当前瞬时时间(UTC)

Clock c32 = Clock.system(ZoneId.of("Asia/Shanghai"));//上海时区

System.out.println(c32.millis());//每次调用将返回当前瞬时时间(UTC)

Clock c4 = Clock.fixed(Instant.now(), ZoneId.of("Asia/Shanghai"));//固定上海时区时钟

System.out.println(c4.millis());

Thread.sleep(1000);

System.out.println(c4.millis()); //不变 即时钟时钟在那一个点不动

Clock c5 = Clock.offset(c1, Duration.ofSeconds(2)); //相对于系统默认时钟两秒的时钟

System.out.println(c1.millis());

System.out.println(c5.millis());

}

public static voidtestInstant(){

//瞬时时间 相当于以前的System.currentTimeMillis()

Instant instant1 = Instant.now();

System.out.println(instant1.getEpochSecond());//精确到秒 得到相对于1970-01-01 00:00:00 UTC的一个时间

System.out.println(instant1.toEpochMilli()); //精确到毫秒

Clock clock1 = Clock.systemUTC(); //获取系统UTC默认时钟

Instant instant2 = Instant.now(clock1);//得到时钟的瞬时时间

System.out.println(instant2.toEpochMilli());

Clock clock2 = Clock.fixed(instant1, ZoneId.systemDefault()); //固定瞬时时间时钟

Instant instant3 = Instant.now(clock2);//得到时钟的瞬时时间

System.out.println(instant3.toEpochMilli());//equals instant1

}

public static void testLocalDateTime(){

//使用默认时区时钟瞬时时间创建 Clock.systemDefaultZone() -->即相对于 ZoneId.systemDefault()默认时区

LocalDateTime now = LocalDateTime.now();

System.out.println(now);

//自定义时区

LocalDateTime now2 = LocalDateTime.now(ZoneId.of("Europe/Paris"));

System.out.println(now2);//会以相应的时区显示日期

//自定义时钟

Clock clock = Clock.system(ZoneId.of("Asia/Dhaka"));

LocalDateTime now3 = LocalDateTime.now(clock);

System.out.println(now3);//会以相应的时区显示日期

//不需要写什么相对时间 如java.util.Date 年是相对于1900 月是从0开始

//2013-12-31 23:59

LocalDateTime d1 = LocalDateTime.of(2013, 12, 31, 23, 59);

//年月日 时分秒 纳秒

LocalDateTime d2 = LocalDateTime.of(2013, 12, 31, 23, 59, 59, 11);

//使用瞬时时间 + 时区

Instant instant = Instant.now();

LocalDateTime d3 = LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());

System.out.println(d3);

//解析String--->LocalDateTime

LocalDateTime d4 = LocalDateTime.parse("2013-12-31T23:59");

System.out.println(d4);

LocalDateTime d5 = LocalDateTime.parse("2013-12-31T23:59:59.999");//999毫秒 等价于999000000纳秒

System.out.println(d5);

//使用DateTimeFormatter API 解析 和 格式化

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");

LocalDateTime d6 = LocalDateTime.parse("2013/12/31 23:59:59", formatter);

System.out.println(formatter.format(d6));

//时间获取

System.out.println(d6.getYear());

System.out.println(d6.getMonth());

System.out.println(d6.getDayOfYear());

System.out.println(d6.getDayOfMonth());

System.out.println(d6.getDayOfWeek());

System.out.println(d6.getHour());

System.out.println(d6.getMinute());

System.out.println(d6.getSecond());

System.out.println(d6.getNano());

//时间增减

LocalDateTime d7 = d6.minusDays(1);

LocalDateTime d8 = d7.plus(1, IsoFields.QUARTER_YEARS);

//LocalDate 即年月日 无时分秒

//LocalTime即时分秒 无年月日

//API和LocalDateTime类似就不演示了

// 两个日期是否相等

System.out.println(d1.equals(d2));

// MonthDay - 用来检查生日

LocalDate dateOfBirth = LocalDate.of(2010, 01, 14);

MonthDay birthday = MonthDay.of(dateOfBirth.getMonth(), dateOfBirth.getDayOfMonth());

MonthDay currentMonthDay = MonthDay.from(today);

System.out.println(currentMonthDay.equals(birthday));

// YearMonth - 用来检查信用卡过期

YearMonth currentYearMonth = YearMonth.now(); System.out.printf("Days in month year %s: %d%n", currentYearMonth, currentYearMonth.lengthOfMonth());

YearMonth creditCardExpiry = YearMonth.of(2018, Month.FEBRUARY);

System.out.printf("Your credit card expires on %s %n", creditCardExpiry);

// 判断闰年 - LocalDate类有一个isLeapYear()的方法

System.out.println(dateOfBirth.isLeapYear());

}

public static void testZonedDateTime(){

//即带有时区的date-time 存储纳秒、时区和时差(避免与本地date-time歧义)。

//API和LocalDateTime类似,只是多了时差(如2013-12-20T10:35:50.711+08:00[Asia/Shanghai])

ZonedDateTime now = ZonedDateTime.now();

System.out.println(now);

ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("Europe/Paris"));

System.out.println(now2);

//其他的用法也是类似的 就不介绍了

ZonedDateTime z1 = ZonedDateTime.parse("2013-12-31T23:59:59Z[Europe/Paris]");

System.out.println(z1);

}

public static void testDuration(){

//表示两个瞬时时间的时间段

Duration d1 = Duration.between(Instant.ofEpochMilli(System.currentTimeMillis() - 12323123), Instant.now());

//得到相应的时差

System.out.println(d1.toDays());

System.out.println(d1.toHours());

System.out.println(d1.toMinutes());

System.out.println(d1.toMillis());

System.out.println(d1.toNanos());

//1天时差 类似的还有如ofHours()

Duration d2 = Duration.ofDays(1);

System.out.println(d2.toDays());

}

public static void testChronology(){

//提供对java.util.Calendar的替换,提供对年历系统的支持

Chronology c = HijrahChronology.INSTANCE;

ChronoLocalDateTime d = c.localDateTime(LocalDateTime.now());

System.out.println(d);

}

/**

* 新旧日期转换

*/

public static void testNewOldDateConversion(){

Instant instant=new Date().toInstant();

Date date=Date.from(instant);

System.out.println(instant);

System.out.println(date);

}

public static void main(String[] args)throwsInterruptedException{

testClock();

testInstant();

testLocalDateTime();

testZonedDateTime();

testDuration();

testChronology();

testNewOldDateConversion();

}

}

其它语言时间

日期与时间处理API,在各种语言中,可能都只是个不起眼的API,如果你没有较复杂的时间处理需求,可能只是利用日期与时间处理API取得系统时间,简单做些显示罢了,然而如果认真看待日期与时间,其复杂程度可能会远超过你的想象,天文、地理、历史、政治、文化等因素,都会影响到你对时间的处理。所以在处理时间上,最好选用JSR310(如果你用java8的话就实现310了),或者Joda-Time。

不止是java面临时间处理的尴尬,其他语言同样也遇到过类似的问题,比如

Arrow: Python 中更好的日期与时间处理库

Moment.js: JavaScript 中的日期库

Noda-Time: .NET 阵营的 Joda-Time 的复制

总结

看完了这些例子后,我相信你已经对Java 8这套新的时间日期API有了一定的了解了。现在我们来回顾下关于这个新的API的一些关键的要素。

它提供了javax.time.ZoneId用来处理时区。

它提供了LocalDate与LocalTime类 Java 8中新的时间与日期API中的所有类都是不可变且线程安全的,这与之前的Date与Calendar API中的恰好相反,那里面像java.util.Date以及SimpleDateFormat这些关键的类都不是线程安全的。

新的时间与日期API中很重要的一点是它定义清楚了基本的时间与日期的概念,比方说,瞬时时间,持续时间,日期,时间,时区以及时间段。它们都是基于ISO日历体系的。

每个Java开发人员都应该至少了解这套新的API中的这五个类: Instant 它代表的是时间戳,比如2014-01-14T02:20:13.592Z,这可以从java.time.Clock类中获取,像这样: Instant current = Clock.system(ZoneId.of(“Asia/Tokyo”)).instant(); LocalDate 它表示的是不带时间的日期,比如2014-01-14。它可以用来存储生日,周年纪念日,入职日期等。LocalTime – 它表示的是不带日期的时间 LocalDateTime – 它包含了时间与日期,不过没有带时区的偏移量 ZonedDateTime – 这是一个带时区的完整时间,它根据UTC/格林威治时间来进行时区调整

这个库的主包是java.time,里面包含了代表日期,时间,瞬时以及持续时间的类。它有两个子package,一个是java.time.foramt,这个是什么用途就很明显了,还有一个是java.time.temporal,它能从更低层面对各个字段进行访问。

时区指的是地球上共享同一标准时间的地区。每个时区都有一个唯一标识符,同时还有一个地区/城市(Asia/Tokyo)的格式以及从格林威治时间开始的一个偏移时间。比如说,东京的偏移时间就是+09:00。OffsetDateTime类实际上包含了LocalDateTime与ZoneOffset。它用来表示一个包含格林威治时间偏移量(+/-小时: 分,比如+06:00或者 -08: 00)的完整的日期(年月日)及时间(时分秒,纳秒)。DateTimeFormatter类用于在Java中进行日期的格式化与解析。与SimpleDateFormat不同,它是不可变且线程安全的,如果需要的话,可以赋值给一个静态变量。DateTimeFormatter类提供了许多预定义的格式器,你也可以自定义自己想要的格式。当然了,根据约定,它还有一个parse()方法是用于将字符串转换成日期的,如果转换期间出现任何错误,它会抛出DateTimeParseException异常。类似的,DateFormatter类也有一个用于格式化日期的format()方法,它出错的话则会抛出DateTimeException异常。

再说一句,“MMM d yyyy”与“MMm dd yyyy”这两个日期格式也略有不同,前者能识别出”Jan 2 2014″与”Jan 14 2014″这两个串,而后者如果传进来的是”Jan 2 2014″则会报错,因为它期望月份处传进来的是两个字符。为了解决这个问题,在天为个位数的情况下,你得在前面补0,比如”Jan 2 2014″应该改为”Jan 02 2014″。

JavaFX 2.0

JavaFX历史

跟java在服务器端和web端成绩相比,桌面一直是java的软肋,于是Sun公司在2008年推出JavaFX,弥补桌面软件的缺陷,请看下图JavaFX一路走过来的改进。

Java 8—Java 10 特性详解(中),第4张

从上图看出,一开始推出时候,开发者需使用一种名为JavaFX Script的静态的、声明式的编程语言来开发JavaFX应用程序。因为JavaFX Script将会被编译为Java bytecode,程序员可以使用Java代码代替。

JavaFX 2.0之后的版本摒弃了JavaFX Script语言,而作为一个Java API来使用。因此使用JavaFX平台实现的应用程序将直接通过标准Java代码来实现。

JavaFX 2.0 包含非常丰富的 UI 控件、图形和多媒体特性用于简化可视化应用的开发,WebView可直接在应用中嵌入网页;另外 2.0 版本允许使用 FXML 进行 UI 定义,这是一个脚本化基于 XML 的标识语言。

从JDK 7u6开始,JavaFx就与JDK捆绑在一起了,JavaFX团队称,下一个版本将是8.0,目前所有的工作都已经围绕8.0库进行。这是因为JavaFX将捆绑在Java 8中,因此该团队决定跳过几个版本号,迎头赶上Java 8。

JavaFx8的新特性

全新现代主题: Modena

新的Modena主题来替换原来的Caspian主题。不过在Application的start()方法中,可以通过setUserAgentStylesheet(STYLESHEET_CASPIAN)来继续使用Caspian主题。

参考http://fxexperience.com/2013/03/modena-theme-update/

JavaFX 3D

在JavaFX8中提供了3D图像处理API,包括Shape3D (Box, Cylinder, MeshView, Sphere子类),SubScene, Material, PickResult, LightBase (AmbientLight 和PointLight子类),SceneAntialiasing等。Camera类也得到了更新。从JavaDoc中可以找到更多信息。

富文本

强化了富文本的支持

TreeTableView

日期控件DatePicker

增加日期控件

用于 CSS 结构的公共 API

CSS 样式设置是 JavaFX 的一项主要特性

CSS 已专门在私有 API 中实现(com.sun.javafx.css 软件包)

多种工具(例如 Scene Builder)需要 CSS 公共 API

开发人员将能够定义自定义 CSS 样式

WebView 增强功能

Nashorn JavaScript 引擎

https://blogs.oracle.com/nashorn/entry/open_for_business

WebSocket http://javafx-jira.kenai.com/browse/RT-14947

Web Workers http://javafx-jira.kenai.com/browse/RT-9782

JavaFX Scene Builder 2.0

可视化工具,加速JavaFX图形界面的开发,下载地址

JavaFX Scene Builder如同NetBeans一般,通过拖拽的方式配置界面,待完成界面之後,保存为FXML格式文件,此文件以XML描述物件配置,再交由JavaFX程式处理,因此可減少直接以JavaFX编写界面的困難度。

JavaFX Scene Builder 2.0新增JavaFX Theme预览功能,菜单「Preview」→「JavaFX Theme」选择不同的主題,包括:

Modena (FX8).

Modena Touch (FX8).

Modena High Contrast – Black on White (FX8).

Modena High Contrast – White on Black (FX8).

Modena High Contrast – Yellow on Black (FX8).

Caspian (FX2).

Caspian Embedded (FX2).

Caspian Embedded QVGA (FX2).

JavaFX 8开发2048游戏

2048虽然不像前段时间那么火了,但个人还是非常喜欢玩2048,空闲时间都忍不住来一发,感谢 Gabriele Cirulli 发明了这了不起 (并且会上瘾)的2048游戏,因为是用MIT协议开源出来,各种语言版本的2048游戏横空出世,下图是用JavaFX 8来开发的一款2048。

所用到的技术

Lambda expressions

Stream API

JavaFX 8

JavaFX CSS basics

JavaFX animationsfx2048相关类的说明

Game2048,游戏主类

GameManager,包含游戏界面布局(Board)以及Grid的操作(GridOperator)

Board,包含labels ,分数,grid ,Tile

Tile,游戏中的数字块

GridOperator,Grid操作类

Location,Direction 位置帮助类

RecordManager,SessionManager,纪录游戏分数,会话类

总结

比起AWT和SWING,JavaFX的优势很明显,各大主流IDE已经支持JavaFX的开发了,最佳的工具莫过于NetBeans,且随着lambda带来的好处,JavaFX的事件处理简洁了不少,以前需要写匿名函数类。另外JavaFX开源以来,JavaFX的生态环境也越来越活跃了,包括各种教程,嵌入式尝试,还有一些开源项目,比如: ControlsFX,JRebirth,DataFX Flow,mvvmFX,TestFX 等等。还有JavaFX是可以运行在Android和ios上面,这个很赞!

好了,总结到这里也差不多了,在RIA平台上面,有HTML5、Flex和微软的Sliverlight,JavaFX能否表现优秀,在于大家的各位,只要我们多用JavaFX,那么JavaFX也会越来越优秀,任何语言都是这样, THE END .

PermGen移除

PermGen space简单介绍

PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,说说为什么会内存益出: 这一部分用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和和存放Instance的Heap区域不同,所以如果你的APP会LOAD很多CLASS的话,就很可能出现PermGen space错误。这种错误常见在web服务器对JSP进行pre compile的时候。

JVM 种类有很多,比如 Oralce-Sun Hotspot, Oralce JRockit, IBM J9, Taobao JVM(淘宝好样的!)等等。当然武林盟主是Hotspot了,这个毫无争议。需要注意的是,PermGen space是Oracle-Sun Hotspot才有,JRockit以及J9是没有这个区域。

元空间(MetaSpace)一种新的内存空间诞生

JDK8 HotSpot JVM 将移除永久区,使用本地内存来存储类元数据信息并称之为: 元空间(Metaspace);这与Oracle JRockit 和IBM JVM’s很相似,如下图所示。

Java 8—Java 10 特性详解(中),第5张

这意味着不会再有java.lang.OutOfMemoryError: PermGen问题,也不再需要你进行调优及监控内存空间的使用……但请等等,这么说还为时过早。在默认情况下,这些改变是透明的,接下来我们的展示将使你知道仍然要关注类元数据内存的占用。请一定要牢记,这个新特性也不能神奇地消除类和类加载器导致的内存泄漏。

java8中metaspace总结如下:

PermGen 空间的状况

这部分内存空间将全部移除。

JVM的参数: PermSize 和 MaxPermSize 会被忽略并给出警告(如果在启用时设置了这两个参数)。

Metaspace 内存分配模型

大部分类元数据都在本地内存中分配。

用于描述类元数据的“klasses”已经被移除。

Metaspace 容量

默认情况下,类元数据只受可用的本地内存限制(容量取决于是32位或是64位操作系统的可用虚拟内存大小)。

新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整。

Metaspace 垃圾回收

对于僵死的类及类加载器的垃圾回收将在元数据使用达到“MaxMetaspaceSize”参数的设定值时进行。

适时地监控和调整元空间对于减小垃圾回收频率和减少延时是很有必要的。持续的元空间垃圾回收说明,可能存在类、类加载器导致的内存泄漏或是大小设置不合适。

Java 堆内存的影响

一些杂项数据已经移到Java堆空间中。升级到JDK8之后,会发现Java堆 空间有所增长。

Metaspace 监控

元空间的使用情况可以从HotSpot1.8的详细GC日志输出中得到。

Jstat 和 JVisualVM两个工具,在使用b75版本进行测试时,已经更新了,但是还是能看到老的PermGen空间的出现。

前面已经从理论上充分说明,下面让我们通过“泄漏”程序进行新内存空间的观察……

PermGen vs. Metaspace 运行时比较

为了更好地理解Metaspace内存空间的运行时行为,

将进行以下几种场景的测试:

使用JDK1.7运行Java程序,监控并耗尽默认设定的85MB大小的PermGen内存空间。

使用JDK1.8运行Java程序,监控新Metaspace内存空间的动态增长和垃圾回收过程。

使用JDK1.8运行Java程序,模拟耗尽通过“MaxMetaspaceSize”参数设定的128MB大小的Metaspace内存空间。

首先建立了一个模拟PermGen OOM的代码

public class ClassA{

public void method(String name){

// do nothing

}

}

上面是一个简单的ClassA,把他编译成class字节码放到D: /classes下面,测试代码中用URLClassLoader来加载此类型上面类编译成class

/**

* 模拟PermGen OOM

*@authorbenhail

*/

public class OOMTest{

public static void main(String[] args){

try {

//准备url

URL url = new File("D:/classes").toURI().toURL();

URL[] urls = {url};

//获取有关类型加载的JMX接口

ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();

//用于缓存类加载器

List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();

while (true) {

//加载类型并缓存类加载器实例

ClassLoader classLoader = new URLClassLoader(urls);

classLoaders.add(classLoader);

classLoader.loadClass("ClassA");

//显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)

System.out.println("total: " + loadingBean.getTotalLoadedClassCount());

System.out.println("active: " + loadingBean.getLoadedClassCount());

System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());

}

} catch (Exception e) {

e.printStackTrace();

}

}

}

虚拟机器参数设置如下: -verbose -verbose:gc

设置-verbose参数是为了获取类型加载和卸载的信息

设置-verbose:gc是为了获取垃圾收集的相关信息

JDK 1.7 @64-bit – PermGen 耗尽测试

Java1.7的PermGen默认空间为85 MB(或者可以通过-XX:MaxPermSize=XXXm指定)

Java 8—Java 10 特性详解(中),第6张

可以从上面的JVisualVM的截图看出: 当加载超过6万个类之后,PermGen被耗尽。我们也能通过程序和GC的输出观察耗尽的过程。

程序输出(摘取了部分)

......

[Loaded ClassA from file:/D:/classes/]

total: 64887

active: 64887

unloaded: 0

[GC 245041K->213978K(536768K), 0.0597188 secs]

[Full GC 213978K->211425K(644992K), 0.6456638 secs]

[GC 211425K->211425K(656448K), 0.0086696 secs]

[Full GC 211425K->211411K(731008K), 0.6924754 secs]

[GC 211411K->211411K(726528K), 0.0088992 secs]

...............

java.lang.OutOfMemoryError: PermGen space

JDK 1.8 @64-bit – Metaspace大小动态调整测试

Java的Metaspace空间: 不受限制 (默认)

Java 8—Java 10 特性详解(中),第7张

从上面的截图可以看到,JVM Metaspace进行了动态扩展,本地内存的使用由20MB增长到646MB,以满足程序中不断增长的类数据内存占用需求。我们也能观察到JVM的垃圾回收事件—试图销毁僵死的类或类加载器对象。但是,由于我们程序的泄漏,JVM别无选择只能动态扩展Metaspace内存空间。程序加载超过10万个类,而没有出现OOM事件。

JDK 1.8 @64-bit – Metaspace 受限测试

Java的Metaspace空间: 128MB(-XX:MaxMetaspaceSize=128m)

Java 8—Java 10 特性详解(中),第8张

可以从上面的JVisualVM的截图看出: 当加载超过2万个类之后,Metaspace被耗尽;与JDK1.7运行时非常相似。我们也能通过程序和GC的输出观察耗尽的过程。另一个有趣的现象是,保留的原生内存占用量是设定的最大大小两倍之多。这可能表明,如果可能的话,可微调元空间容量大小策略,来避免本地内存的浪费。

从Java程序的输出中看到如下异常。

[Loaded ClassA from file:/D:/classes/]

total: 21393

active: 21393

unloaded: 0

[GC (Metadata GC Threshold) 64306K->57010K(111616K), 0.0145502 secs]

[FullGC(Metadata GC Threshold)57010K->56810K(122368K), 0.1068084 secs]

java.lang.OutOfMemoryError: Metaspace

在设置了MaxMetaspaceSize的情况下,该空间的内存仍然会耗尽,进而引发“java.lang.OutOfMemoryError: Metadata space”错误。因为类加载器的泄漏仍然存在,而通常Java又不希望无限制地消耗本机内存,因此设置一个类似于MaxPermSize的限制看起来也是合理的。

总结

之前不管是不是需要,JVM都会吃掉那块空间……如果设置得太小,JVM会死掉;如果设置得太大,这块内存就被JVM浪费了。理论上说,现在你完全可以不关注这个,因为JVM会在运行时自动调校为“合适的大小”;

提高Full GC的性能,在Full GC期间,Metadata到Metadata pointers之间不需要扫描了,别小看这几纳秒时间;

隐患就是如果程序存在内存泄露,像OOMTest那样,不停的扩展metaspace的空间,会导致机器的内存不足,所以还是要有必要的调试和监控。

StampedLock

synchronized

在java5之前,实现同步主要是使用synchronized。它是Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

有四种不同的同步块:

实例方法

静态方法

实例方法中的同步块

静态方法中的同步块

大家对此应该不陌生,所以不多讲了,以下是代码示例

synchronized(this)

// do operation

}

小结: 在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,性能上也有所提升。

Lock

rwlock.writeLock().lock();

try {

// do operation

} finally {

rwlock.writeLock().unlock();

}

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

Lock是一个接口,核心方法是lock(),unlock(),tryLock(),实现类有ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock;

ReentrantReadWriteLock, ReentrantLock 和synchronized锁都有相同的内存语义。

与synchronized不同的是,Lock完全用Java写成,在java这个层面是无关JVM实现的。Lock提供更灵活的锁机制,很多synchronized 没有提供的许多特性,比如锁投票,定时锁等候和中断锁等候,但因为lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中

下面是Lock的一个代码示例

class Point{

private double x, y;

private final StampedLock sl = new StampedLock();

void move(doubledeltaX,doubledeltaY){ // an exclusively locked method

long stamp = sl.writeLock();

try {

x += deltaX;

y += deltaY;

} finally {

sl.unlockWrite(stamp);

}

}

//下面看看乐观读锁案例

double distance FromOrigin(){ // 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 move IfAtOrigin(doublenewX,doublenewY){ // 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); //释放读锁或写锁

}

}

}

小结: 比synchronized更灵活、更具可伸缩性的锁定机制,但不管怎么说还是synchronized代码要更容易书写些

StampedLock

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

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

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

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

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

下面是java doc提供的StampedLock一个例子

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); //释放读锁或写锁

}

}

}

小结:

StampedLock要比ReentrantReadWriteLock更加廉价,也就是消耗比较小。

StampedLock与ReadWriteLock性能对比

是和ReadWritLock相比,在一个线程情况下,是读速度其4倍左右,写是1倍。

下图是六个线程情况下,读性能是其几十倍,写性能也是近10倍左右:

Java 8—Java 10 特性详解(中),第9张

总结

synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定;

ReentrantLock、ReentrantReadWriteLock,、StampedLock都是对象层面的锁定,要保证锁定一定会被释放,就必须将unLock()放到finally{}中;

StampedLock 对吞吐量有巨大的改进,特别是在读线程越来越多的场景下;

StampedLock有一个复杂的API,对于加锁操作,很容易误用其他方法;

当只有少量竞争者的时候,synchronized是一个很好的通用的锁实现;

当线程增长能够预估,ReentrantLock是一个很好的通用的锁实现;

StampedLock 可以说是Lock的一个很好的补充,吞吐量以及性能上的提升足以打动很多人了,但并不是说要替代之前Lock的东西,毕竟他还是有些应用场景的,起码API比StampedLock容易入手。

其它更新

处理数值

Java8添加了对无符号数的额外支持。Java中的数值总是有符号的,例如,让我们来观察Integer:

int可表示最多2 ** 32个数。Java中的数值默认为有符号的,所以最后一个二进制数字表示符号(0为正数,1为负数)。所以从十进制的0开始,最大的有符号正整数为2 ** 31 - 1。

你可以通过Integer.MAX_VALUE来访问它:

System.out.println(Integer.MAX_VALUE);// 2147483647

System.out.println(Integer.MAX_VALUE + 1);// -2147483648

Java8添加了解析无符号整数的支持,让我们看看它如何工作:

long maxUnsignedInt = (1l << 32) - 1;

String string = String.valueOf(maxUnsignedInt);

int unsignedInt = Integer.parseUnsignedInt(string, 10);

String string2 = Integer.toUnsignedString(unsignedInt, 10);

就像你看到的那样,现在可以将最大的无符号数2 ** 32 - 1解析为整数。而且你也可以将这个数值转换回无符号数的字符串表示。

这在之前不可能使用parseInt完成,就像这个例子展示的那样:

try {

Integer.parseInt(string, 10);

}

catch (NumberFormatException e) {

System.err.println("could not parse signed int of " + maxUnsignedInt);

}

这个数值不可解析为有符号整数,因为它超出了最大范围2 ** 31 - 1。算术运算

Math工具类新增了一些方法来处理数值溢出。这是什么意思呢 我们已经看到了所有数值类型都有最大值。所以当算术运算的结果不能被它的大小装下时,会发生什么呢

System.out.println(Integer.MAX_VALUE);// 2147483647

System.out.println(Integer.MAX_VALUE + 1);// -2147483648

就像你看到的那样,发生了整数溢出,这通常是我们不愿意看到的。

Java8添加了严格数学运算的支持来解决这个问题。Math扩展了一些方法,它们全部以exact结尾,例如addExact。当运算结果不能被数值类型装下时,这些方法通过抛出ArithmeticException异常来合理地处理溢出。

try {

Math.addExact(Integer.MAX_VALUE, 1);

}

catch (ArithmeticException e) {

System.err.println(e.getMessage());

// => integer overflow

}

当尝试通过toIntExact将长整数转换为整数时,可能会抛出同样的异常:

try {

Math.toIntExact(Long.MAX_VALUE);

}

catch (ArithmeticException e) {

System.err.println(e.getMessage());

// => integer overflow

}

处理文件

Files工具类首次在Java7中引入,作为NIO的一部分。JDK8 API添加了一些额外的方法,它们可以将文件用于函数式数据流。让我们深入探索一些代码示例。列出文件

Files.list方法将指定目录的所有路径转换为数据流,便于我们在文件系统的内容上使用类似filter和sorted的流操作。

try (Stream<Path> stream = Files.list(Paths.get(""))) {

String joined = stream

.map(String::valueOf)

.filter(path -> !path.startsWith("."))

.sorted()

.collect(Collectors.joining("; "));

System.out.println("List: " + joined);

}

上面的例子列出了当前工作目录的所有文件,之后将每个路径都映射为它的字符串表示。之后结果被过滤、排序,最后连接为一个字符串。如果你还不熟悉函数式数据流,你应该阅读我的Java8数据流教程。

你可能已经注意到,数据流的创建包装在try-with语句中。数据流实现了AutoCloseable,并且这里我们需要显式关闭数据流,因为它基于IO操作。

返回的数据流是DirectoryStream的封装。如果需要及时处理文件资源,就应该使用try-with结构来确保在流式操作完成后,数据流的close方法被调用。

查找文件

下面的例子演示了如何查找在目录及其子目录下的文件:

Path start = Paths.get("");

int maxDepth = 5;

try (Stream<Path> stream = Files.find(start, maxDepth, (path, attr) ->

String.valueOf(path).endsWith(".js"))) {

String joined = stream

.sorted()

.map(String::valueOf)

.collect(Collectors.joining("; "));

System.out.println("Found: " + joined);

}

find方法接受三个参数: 目录路径start是起始点,maxDepth定义了最大搜索深度。第三个参数是一个匹配谓词,定义了搜索的逻辑。上面的例子中,我们搜索了所有JavaScirpt文件(以.js结尾的文件名)。

我们可以使用Files.walk方法来完成相同的行为。这个方法会遍历每个文件,而不需要传递搜索谓词。

Path start = Paths.get("");

int maxDepth = 5;

try (Stream<Path> stream = Files.walk(start, maxDepth)) {

String joined = stream

.map(String::valueOf)

.filter(path -> path.endsWith(".js"))

.sorted()

.collect(Collectors.joining("; "));

System.out.println("walk(): " + joined);

}

这个例子中,我们使用了流式操作filter来完成和上个例子相同的行为。

读写文件

将文本文件读到内存,以及向文本文件写入字符串在Java 8 中是简单的任务。不需要再去摆弄读写器了。Files.readAllLines从指定的文件把所有行读进字符串列表中。你可以简单地修改这个列表,并且将它通过Files.write写到另一个文件中:

List<String> lines = Files.readAllLines(Paths.get("res/nashorn1.js"));

lines.add("print('foobar');");

Files.write(Paths.get("res/nashorn1-modified.js"), lines);

要注意这些方法对内存并不十分高效,因为整个文件都会读进内存。文件越大,所用的堆区也就越大。

你可以使用Files.lines方法来作为内存高效的替代。这个方法读取每一行,并使用函数式数据流来对其流式处理,而不是一次性把所有行都读进内存。

try (Stream<String> stream = Files.lines(Paths.get("res/nashorn1.js"))) {

stream

.filter(line -> line.contains("print"))

.map(String::trim)

.forEach(System.out::println);

}

如果你需要更多的精细控制,你需要构造一个新的BufferedReader来代替:

Path path = Paths.get("res/nashorn1.js");

try (BufferedReader reader = Files.newBufferedReader(path)) {

System.out.println(reader.readLine());

}

或者,你需要写入文件时,简单地构造一个BufferedWriter来代替:

Path path = Paths.get("res/output.js");

try (BufferedWriter writer = Files.newBufferedWriter(path)) {

writer.write("print('Hello World');");

}

BufferedReader也可以访问函数式数据流。lines方法在它所有行上面构建数据流:

Path path = Paths.get("res/nashorn1.js");

try (BufferedReader reader = Files.newBufferedReader(path)) {

long countPrints = reader

.lines()

.filter(line -> line.contains("print"))

.count();

System.out.println(countPrints);

}

目前为止你可以看到Java8提供了三个简单的方法来读取文本文件的每一行,使文件处理更加便捷。

不幸的是你需要显式使用try-with语句来关闭文件流,这会使示例代码有些凌乱。我期待函数式数据流可以在调用类似count和collect时可以自动关闭,因为你不能在相同数据流上调用终止操作两次。

java.util.Random

在Java8中java.util.Random类的一个非常明显的变化就是新增了返回随机数流(random Stream of numbers)的一些方法。

下面的代码是创建一个无穷尽的double类型的数字流,这些数字在0(包括0)和1(不包含1)之间。

Random random = new Random();

DoubleStream doubleStream = random.doubles();

下面的代码是创建一个无穷尽的int类型的数字流,这些数字在0(包括0)和100(不包括100)之间。

Random random = new Random();

IntStream intStream = random.ints(0, 100);

那么这些无穷尽的数字流用来做什么呢 接下来,我通过一些案例来分析。记住,这些无穷大的数字流只能通过某种方式被截断(limited)。

示例1: 创建10个随机的整数流并打印出来:

intStream.limit(10).forEach(System.out::println);

示例2: 创建100个随机整数:

List<Integer> randomBetween0And99 = intStream

.limit(100)

.boxed()

.collect(Collectors.toList());

对于高斯伪随机数(gaussian pseudo-random values)来说,random.doubles()方法所创建的流不能等价于高斯伪随机数,然而,如果用java8所提供的功能是非常容易实现的。

Random random = new Random();

DoubleStream gaussianStream = Stream.generate(random::nextGaussian).mapToDouble(e -> e);

这里,我使用了Stream.generate api,并传入Supplier 类的对象作为参数,这个对象是通过调用Random类中的方法 nextGaussian()创建另一个高斯伪随机数。

接下来,我们来对double类型的伪随机数流和double类型的高斯伪随机数流做一个更加有意思的事情,那就是获得两个流的随机数的分配情况。预期的结果是: double类型的伪随机数是均匀的分配的,而double类型的高斯伪随机数应该是正态分布的。

通过下面的代码,我生成了一百万个伪随机数,这是通过java8提供的api实现的:

Random random = new Random();

DoubleStream doubleStream = random.doubles(-1.0, 1.0);

LinkedHashMap<Range, Integer> rangeCountMap = doubleStream.limit(1000000)

.boxed()

.map(Ranges::of)

.collect(Ranges::emptyRangeCountMap, (m, e) -> m.put(e, m.get(e) + 1), Ranges::mergeRangeCountMaps);

rangeCountMap.forEach((k, v) -> System.out.println(k.from() + "\t" + v));

代码的运行结果如下:

-149730

-0.949931

-0.850057

-0.750060

-0.649963

-0.550159

-0.449921

-0.349962

-0.250231

-0.149658

0 50177

0.1 49861

0.2 49947

0.3 50157

0.4 50414

0.5 50006

0.6 50038

0.7 49962

0.8 50071

0.9 49695

为了类比,我们再生成一百万个高斯伪随机数:

Random random = new Random();

DoubleStream gaussianStream = Stream.generate(random::nextGaussian).mapToDouble(e -> e);

LinkedHashMap<Range, Integer> gaussianRangeCountMap =

gaussianStream

.filter(e -> (e >= -1.0 && e < 1.0))

.limit(1000000)

.boxed()

.map(Ranges::of)

.collect(Ranges::emptyRangeCountMap, (m, e) -> m.put(e, m.get(e) + 1), Ranges::mergeRangeCountMaps);

gaussianRangeCountMap.forEach((k, v) -> System.out.println(k.from() + "\t" + v));

上面代码输出的结果恰恰与我们预期结果相吻合,即: double类型的伪随机数是均匀的分配的,而double类型的高斯伪随机数应该是正态分布的。

附: 完整代码可点击这里获取 https://gist.github.com/bijukunjummen/8129250

译文链接: http://www.importnew.com/9672.html


https://www.xamrdz.com/lan/5pn1848561.html

相关文章: