Java 8 为 Java 语言、编译器、类库、开发工具与 JVM( Java 虚拟机)带来了大量新特性。
上一篇了解了最重要的 Stream 和 Lambda 表达式(或者说闭包,虽然不是很恰当),或者可以理解为 Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据。
这篇就补全剩下的部分,默认方法啦、方法引用(双冒号运算符)、新的类库,当然这些也是不全的,我认为经常用的就这些了,全部的新特性可以见参考的链接。
接口的默认方法与静态方法
Java 8 用默认方法与静态方法这两个新概念来扩展接口的声明。
默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求。相反,如果接口定义了默认方法,那么必须提供一个所谓的默认实现,这样所有的接口实现者将会默认继承它(如果有必要的话,可以覆盖这个默认实现)。
1 | private interface Defaulable { |
Java 8带来的另一个有趣的特性是接口可以声明(并且可以提供实现)静态方法。
1 | private interface DefaulableFactory { |
在JVM中,默认方法的实现是非常高效的,并且通过字节码指令为方法调用提供了支持。默认方法允许继续使用现有的 Java 接口,而同时能够保障正常的编译过程。
这方面好的例子是大量的方法被添加到 java.util.Collection
接口中去:stream(),parallelStream(),forEach(),removeIf(),……
尽管默认方法非常强大,但是在使用默认方法时我们需要小心注意一个地方:在声明一个默认方法前,请仔细思考是不是真的有必要使用默认方法,因为默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误。
为什么要有默认方法
在 java 8 之前,接口与其实现类之间的 耦合度 太高了(tightly coupled),当需要为一个接口添加方法时,所有的实现类都必须随之修改。默认方法解决了这个问题,它可以为接口添加新的方法,而不会破坏已有的接口的实现。这在 lambda 表达式作为 java 8 语言的重要特性而出现之际,为升级旧接口且保持向后兼容(backward compatibility)提供了途径。
这个 forEach 方法是 jdk 1.8 新增的接口默认方法,正是因为有了默认方法的引入,才不会因为 Iterable 接口中添加了 forEach 方法就需要修改所有 Iterable 接口的实现类。
关于继承
和其它方法一样,接口默认方法也可以被继承。
1 | interface InterfaceA { |
接口默认方法的继承分三种情况(分别对应上面的 InterfaceB
接口、InterfaceC
接口和 InterfaceD
接口):
- 不覆写默认方法,直接从父接口中获取方法的默认实现。
- 覆写默认方法,这跟类与类之间的覆写规则相类似。
- 覆写默认方法并将它重新声明为抽象方法,这样新接口的子类必须再次覆写并实现这个抽象方法。
然后来考虑下多继承的问题,是的,默认方法在接口里,接口可以继承,接口可以多实现,那么自然就带来了默认方法多继承的问题;但是 Java 使用的是单继承、多实现的机制,为的是避免多继承带来的调用歧义的问题。当接口的子类同时拥有具有相同签名的方法时,就需要考虑一种解决冲突的方案。
1 | interface InterfaceA { |
在 ClassB 类中,它实现的 InterfaceB 接口和 InterfaceC 接口中都存在相同签名的 foo 方法,需要手动解决冲突。覆写存在歧义的方法,并可以使用 InterfaceName.super.methodName();
的方式手动调用需要的接口默认方法。
下面来看特殊情况:接口继承行为发生冲突时的解决规则。
比如,出现了下面的这种情况:
1 | interface InterfaceA { |
因为 InterfaceB 接口继承了 InterfaceA 接口,那么 InterfaceB 接口一定包含了所有 InterfaceA 接口中的字段方法,因此一个同时实现了 InterfaceA 接口和 InterfaceB 接口的类与一个只实现了 InterfaceB 接口的类完全等价。
这很好理解,就相当于 class SimpleDateFormat extends DateFormat
与 class SimpleDateFormat extends DateFormat
, Object 等价(如果允许多继承)。
而覆写意味着对父类方法的屏蔽,这也是 Override 的设计意图之一。因此在实现了 InterfaceB 接口的类中无法访问已被覆写的 InterfaceA 接口中的 foo 方法。
这是当接口继承行为发生冲突时的规则之一,即 被其它类型所覆盖的方法会被忽略。
如果想要调用 InterfaceA 接口中的 foo 方法,只能通过自定义一个新的接口同样继承 InterfaceA 接口并显示地覆写 foo 方法,在方法中使用 InterfaceA.super.foo();
调用 InterfaceA 接口的 foo 方法,最后让实现类同时实现 InterfaceB 接口和自定义的新接口,代码如下:
1 | interface InterfaceA { |
注意! 虽然 InterfaceC 接口的 foo 方法只是调用了一下父接口的默认实现方法,但是这个覆写 不能省略,否则 InterfaceC 接口中继承自 InterfaceA 接口的隐式的 foo 方法同样会被认为是被 InterfaceB 接口覆写了而被屏蔽,会导致调用 InterfaceC.super.foo()
时出错。
通过这个例子,应该注意到在使用一个默认方法前,一定要考虑它是否真的需要。因为 默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误。滥用默认方法可能给代码带来意想不到、莫名其妙的错误。
接口与抽象类
当接口继承行为发生冲突时的另一个规则是,类的方法声明优先于接口默认方法,无论该方法是具体的还是抽象的。
1 | interface InterfaceA { |
ClassA 类中并不需要手动覆写 bar 方法,因为优先考虑到 ClassA 类继承了的 AbstractClassA 抽象类中存在对 bar 方法的实现,同样的因为 AbstractClassA 抽象类中的 foo 方法是抽象的,所以在 ClassA 类中必须实现 foo 方法。
虽然 Java 8 的接口的默认方法就像抽象类,能提供方法的实现,但是他们俩仍然是 不可相互代替的:
- 接口可以被类多实现(被其他接口多继承),抽象类只能被单继承。
- 接口中没有
this
指针,没有构造函数,不能拥有实例字段(实例变量)或实例方法,无法保存 状态(state),抽象方法中可以。 - 抽象类不能在 java 8 的 lambda 表达式中使用。
- 从设计理念上,接口反映的是 “like-a” 关系,抽象类反映的是 “is-a” 关系。
顺便复习了下接口和抽象类的知识点~~
其他
补充下其他的知识点:
default
关键字只能在接口中使用(以及用在switch
语句的default
分支),不能用在抽象类中。- 接口默认方法不能覆写
Object
类的equals
、hashCode
和toString
方法。 - 接口中的静态方法必须是
public
的,public
修饰符可以省略,static
修饰符不能省略。 - 即使使用了 java 8 的环境,一些 IDE 仍然可能在一些代码的实时编译提示时出现异常的提示(例如无法发现 java 8 的语法错误),因此不要过度依赖 IDE。
方法引用
其实就是上篇所说的双冒号操作,不知道还有没有印象,即 目标引用::方法
,下面就来看看具体的几种用法。
方法引用提供了非常有用的语法,可以直接引用已有 Java 类或对象的方法或构造器。与 lambda 联合使用(一般是不能独立使用的),方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
下面来看看 Java 支持的这四种不同的方法引用:
1 | public static class Car { |
这四类可以定义为:
- 类名::new
- 类名::静态方法名
- 类名::实例方法名
这种方法引用有些特殊之处:当使用这种方式时,一定是 lambda 表达式所接收的第一个参数来调用实例方法,如果lambda表达式接收多个参数,其余的参数作为方法的参数传递进去。
参考:http://sfau.lt/b5ZD16 - 对象::实例方法名
下面就来解释下上面例子里的四种方式,说的都是在本例的情况下。
第一种方法引用是构造器引用,它的语法是 Class::new
,或者更一般的 Class< T >::new
。new 不就是调用构造函数嘛~请注意构造器没有参数。
第二种方法引用是静态方法引用,它的语法是 Class::static_method
,请注意这个方法接受一个 Car 类型的参数。
第三种方法引用是特定类的任意对象的方法引用,它的语法是 Class::method
。请注意,这个方法没有参数,并且是非静态。
最后,第四种方法引用是特定对象的方法引用,它的语法是 instance::method
。请注意,这个方法接受一个 Car 类型的参数
类库新特性
Java 8 通过增加大量新类,扩展已有类的功能的方式来改善对并发编程、函数式编程、日期/时间相关操作以及其他更多方面的支持。
Optional
到目前为止,臭名昭著的空指针异常是导致 Java 应用程序失败的最常见原因。以前,为了解决空指针异常,Google 公司著名的 Guava 项目引入了 Optional 类,Guava 通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。
受到 Google Guava 的启发,Optional 类已经成为 Java 8 类库的一部分。
Optional 实际上是个容器:它可以保存类型 T 的值,或者仅仅保存 null。Optional 提供很多有用的方法,这样我们就不用显式进行空值检测。在 Javadoc 中的描述翻译过来就是:
这是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回 true,调用 get() 方法会返回该对象。
下面就来看看它的几个方法(在前面说 stream 的时候大量使用了 Optional ):
- of
为非 null 的值创建一个 Optional。
of 方法通过工厂方法创建 Optional 类。需要注意的是,创建对象时传入的参数不能为 null。如果传入参数为 null,则抛出 NPE。 - ofNullable
为指定的值创建一个 Optional,如果指定的值为 null,则返回一个空的 Optional。 - empty
此方法用于创建一个没有值的 Optional 对象;如果对 emptyOpt 变量调用 isPresent() 方法会返回 false,调用 get() 方法抛出 NullPointerException 异常。 - isPresent
如果值存在返回 true,否则返回 false。 - ifPresent
如果 Optional 实例有值则为其调用 consumer(比如 lambda 表达式),否则不做处理 - get
如果 Optional 有值则将其返回,否则抛出 NoSuchElementException。 - orElse
如果有值则将其返回,否则返回指定的其它值(默认值)。empty.orElse("There is no value present!");
- orElseGet
orElseGet 与 orElse 方法类似,区别在于得到的默认值。
orElse 方法将传入的字符串作为默认值,orElseGet 方法可以接受 Supplier 接口的实现用来生成默认值。empty.orElseGet(() -> "Default Value");
- orElseThrow
如果有值则将其返回,否则抛出 supplier 接口创建的异常。
在 orElseThrow 中我们可以传入一个 lambda 表达式或方法,如果值不存在来抛出异常。empty.orElseThrow(ValueAbsentException::new);
- map
如果有值,则对其执行调用 mapping 函数得到返回值。
如果返回值不为 null,则创建包含 mapping 返回值的 Optional 作为 map 方法返回值,否则返回空 Optional。Optional<String> upperName = name.map((value) -> value.toUpperCase());
- flatMap
如果有值,为其执行 mapping 函数返回 Optional 类型返回值,否则返回空 Optional。
flatMap 与 map(Funtion)方法类似,区别在于 flatMap 中的 mapper 返回值必须是 Optional。
调用结束时,flatMap 不会对结果用 Optional 封装。upperName = name.flatMap((value) -> Optional.of(value.toUpperCase()));
- filter
如果有值并且满足断言条件返回包含该值的 Optional,否则返回空 Optional。
对于 filter 函数我们应该传入实现了 Predicate 接口的 lambda 表达式。Optional<String> longName = name.filter((value) -> value.length() > 6);
要理解 ifPresent 方法,首先需要了解 Consumer 类。
简答地说,Consumer 类包含一个抽象方法。该抽象方法对传入的值进行处理,但没有返回值。
Java8 支持不用接口直接通过 lambda 表达式传入参数。
在 Java 9 中,对 Optional 还进行了增强,多加了几个方法,感兴趣的可以去:http://sfau.lt/b5KDt8
最后通过一个例子来综合的展示下:
1 | public class OptionalDemo { |
Java 8 提倡函数式编程,新增的许多 API 都可以用函数式编程表示,Optional
类也是其中之一。这里有几条关于Optional
使用的建议:
- 尽量避免在程序中直接调用
Optional
对象的get()
和isPresent()
方法(活用 orElse 系列); - 避免使用
Optional
类型声明实体类的属性;
第一条建议中直接调用get()
方法是很危险的做法,如果Optional
的值为空,那么毫无疑问会抛出 NPE 异常,而为了调用get()
方法而使用isPresent()
方法作为空值检查,这种做法与传统的用 if 语句块做空值检查没有任何区别。
第二条建议避免使用 Optional 作为实体类的属性,它在设计的时候就没有考虑过用来作为类的属性,如果你查看 Optional 的源代码,你会发现它没有实现 java.io.Serializable
接口,这在某些情况下是很重要的(比如你的项目中使用了某些序列化框架),使用了 Optional 作为实体类的属性,意味着他们不能被序列化。
1 | User user = ... |
当你很确定一个对象不可能为 null 的时候,应该使用 of()
方法,否则,尽可能使用 ofNullable()
方法
新的时间和日期API
Java 8 另一个新增的重要特性就是引入了新的时间和日期 API,它们被包含在 java.time
包中。借助新的时间和日期 API 可以以更简洁的方法处理时间和日期。
在 Java 8 之前,所有关于时间和日期的 API 都存在各种使用方面的缺陷,主要有:
- Java 的
java.util.Date
和java.util.Calendar
类易用性差,不支持时区,并且是可变的,也就意味着他们都不是线程安全的; - 用于格式化日期的类
DateFormat
被放在java.text
包中,它是一个抽象类,所以我们需要实例化一个 SimpleDateFormat 对象来处理日期格式化,并且 DateFormat 也是非线程安全,这意味着如果你在多线程程序中调用同一个 DateFormat 对象,会得到意想不到的结果。 - 对日期的计算方式繁琐,而且容易出错,因为月份是从0开始的,这意味着从
Calendar
中获取的月份需要加一才能表示当前月份。
由于以上这些问题,出现了一些三方的日期处理框架,例如 Joda-Time,data4j 等开源项目。
但是,Java 需要一套标准的用于处理时间和日期的框架,于是 Java 8 中引入了新的日期 API。新的日期 API 是 JSR-310 规范的实现,Joda-Time 框架的作者正是 JSR-310 的规范的倡导者,所以能从 Java 8 的日期 API 中看到很多 Joda-Time 的特性。
常用的几个类就是 LocalDate, LocalTime, LocalDateTime, Instant, Period, Duration 等.
下面通过几个示例代码来快速学会使用新版的日期时间 API:
1 | // 获取当前日期 |
上面所用的 API 大部分都是不可变的,也就是说是线程安全的,可放心食用!
Instant 用于表示一个时间戳,它与我们常使用的 System.currentTimeMillis()
有些类似,不过 Instant 可以精确到纳秒(Nano-Second),System.currentTimeMillis()
方法只精确到毫秒(Milli-Second)。
类似的还有 Duration、Period 它们通过 between 方法来确定一段时间。
Base64的API
在 JDK1.6 之前,JDK 核心类一直没有 Base64 的实现类,有人建议用 Sun/Oracle JDK 里面的 sun.misc.BASE64Encoder
和 sun.misc.BASE64Decoder
,使用它们的优点就是不需要依赖第三方类库,缺点就是可能在未来版本会被删除(用 maven 编译会发出警告),而且性能不佳,性能测试见最后的参考链接。
JDK1.6 中添加了另一个 Base64 的实现,javax.xml.bind.DatatypeConverter
两个静态方法 parseBase64Binary 和 printBase64Binary,隐藏在 javax.xml.bind
包下面,不被很多开发者知道。
在 Java 8 在 java.util
包下面实现了 BASE64 编解码 API,而且性能不俗,API 也简单易懂,下面展示下这个类的使用例子。
1 | /* Basic编码:是标准的BASE64编码,用于处理常规的需求 */ |
之前我们常用的第三方工具有:
Apache Commons Codec library 里面的 org.apache.commons.codec.binary.Base64
;
Google Guava 库里面的 com.google.common.io.BaseEncoding.base64()
这个静态方法;net.iharder.Base64
,这个 jar 包就一个类;
号称 Base64 编码速度最快的 MigBase64,而且是 10 年前的实现.
关于他们之间的性能测试去参考里面的最后一个链接查看,总之,用 Java 8 自带的就足足够了!
JVM新特性
PermGen 空间被移除了,取而代之的是 Metaspace(JEP 122)。
JVM 选项 -XX:PermSize
与 -XX:MaxPermSize
分别被 -XX:MetaSpaceSize
与 -XX:MaxMetaspaceSize
所代替。
参考
http://ebnbin.com/2015/12/20/java-8-default-methods/
http://www.importnew.com/11908.html
http://www.importnew.com/6675.html
http://www.importnew.com/15637.html
https://lw900925.github.io/java/java8-newtime-api.html
http://www.importnew.com/14961.html
评论框加载失败,无法访问 Disqus
你可能需要魔法上网~~