Java 8 简介
Java 8 (又称 jdk8) 是 Java 开发的主要版本,Oracle 公司于 2014 年 3 月 18 日发布 Java 8.
新特性
- Lambda 表达式
- 方法引用
- 默认方法
- 新工具:Nashorn引擎 jjs、 类依赖分析器jdeps
- Stream API
- Date Time API
- Optional 类
- Nashorn, JavaScript 引擎
Lambda 表达式
使用 Lambda 表达式需要注意的地方
- Lambda 表达式主要用来定义行内执行的方法类型接口(例如,一个简单方法接口)。
- Lambda 表达式免去了使用匿名方法的麻烦,并且给予 Java 简单但是强大的函数化的编程能力。
- lambda 表达式只能引用标记了 final 的外层局部变量。
- lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)。
- 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
方法引用
什么是方法引用
- 方法引用通过方法的名字来指向一个方法。
- 方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
- 方法引用使用一对冒号 :: 。
不同方法的引用
- 构造器引用
- 静态方法引用
- 特定类的任意对象的方法引用
- 特定对象的方法引用
实例
// 方法引用
public class PracticeTest {
@Test
public void test() {
// 构造器引用
final Car car = Car.create(Car::new);
final List<Car> cars = new ArrayList<>(Collections.singletonList(car));
// 静态方法引用
cars.forEach(Car::collide);
// 特定类的任意对象的方法引用
cars.forEach(Car::repair);
// 特定对象的方法引用
final Car police = Car.create(Car::new);
cars.forEach(police::follow);
}
}
class Car {
public static Car create(final Supplier<Car> supplier) {
return supplier.get();
}
public static void collide(final Car car) {
System.out.println("Collide " + car.toString());
}
public void follow(final Car anthor) {
System.out.println("Follow the " + anthor.toString());
}
public void repair() {
System.out.println("Repaird " + this.toString());
}
}
函数式接口
什么是函数式接口
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口
函数式接口可以被隐式转换为 lambda 表达式。
JDK 1.8 新增加的函数接口:
- java.util.function
实例
public class PracticeTest {
@Test
public void test() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
eval(list, n -> n % 2 == 0);
}
public static void eval(List<Integer> list, Predicate<Integer> predicate) {
for (Integer n : list){
if (predicate.test(n)){
System.out.println(n + " ");
}
}
}
}
默认方法
什么是默认方法
简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。
我们只需在方法名前面加个 default 关键字即可实现默认方法。
为什么要有这个特性?
首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的 java 8 之前的集合框架没有 foreach 方法,通常能想到的解决办法是在 JDK 里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。
实例
public class PracticeTest implements Vehicle, FourWheeler {
@Test
public void test() {
this.print();
}
@Override
public void print() {
System.out.println("我是一辆四轮汽车");
Vehicle.super.print();
// 静态默认方法
Vehicle.blowHorn();
FourWheeler.super.print();
}
}
interface Vehicle {
default void print() {
System.out.println("我是第一辆车");
}
static void blowHorn() {
System.out.println("按喇叭");
}
}
interface FourWheeler {
default void print() {
System.out.println("我是一辆四轮车");
}
}
Stream 流
什么是 Stream?
Stream(流)是一个来自数据源的元素队列并支持聚合操作
- 元素是特定类型的对象,形成一个队列。 Java 中的 Stream 并不会存储元素,而是按需计算。
- 数据源 流的来源。 可以是集合,数组,I/O channel, 产生器 generator 等。
- 聚合操作 类似 SQL 语句一样的操作, 比如 filter, map, reduce, find, match, sorted 等。
和以前的 Collection 操作不同, Stream 操作还有两个基础的特征:
- Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行 (laziness) 和短路 (short-circuiting)。
- 内部迭代: 以前对集合遍历都是通过 Iterator 或者 For-Each 的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream 提供了内部迭代的方式, 通过访问者模式 (Visitor) 实现。
生成流
在 Java 8 中, 集合接口有两个方法来生成流:
- Stream 为集合创建串行流
- parallelStream 为集合创建并行流
实例
public class PracticeTest {
@Test
public void test() {
// 计算空字符串
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
long count = strings.stream().filter(String::isEmpty).count();
count = strings.stream().filter(i -> i.length() == 3).count();
List<String> stringList = strings.stream().filter(i -> !i.isEmpty()).collect(Collectors.toList());
String s = strings.stream().filter(i -> !i.isEmpty()).collect(Collectors.joining(","));
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
List<Integer> numberCollect = numbers.stream().map(i -> i * i).distinct().collect(Collectors.toList());
List<Integer> integers = Arrays.asList(1,2,13,4,15,6,17,8,19);
IntSummaryStatistics intSummaryStatistics = integers.stream().mapToInt(i -> i).summaryStatistics();
intSummaryStatistics.getMax();
intSummaryStatistics.getMin();
intSummaryStatistics.getSum();
intSummaryStatistics.getAverage();
Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);
}
}
Optional 类
引入 Optional 类的目的是什么?
Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回 true,调用 get() 方法会返回该对象。
Optional 是个容器:它可以保存类型 T 的值,或者仅仅保存 null。Optional 提供很多有用的方法,这样我们就不用显式进行空值检测。
Optional 类的引入很好的解决空指针异常。
建议
- 不建议将 Optional 作为参数,容易造成空指针和误解,这和 Optional 的目的相违背,如果是想传递某个调用,请使 Supplier.
- 不建议将 Optional 作为属性,非要用建议使用 guava 包的 Optional 类。
实例
**public class PracticeTest {
@Test
public void test() {
Integer value1 = null;
Integer value2 = 10;
System.out.println(this.sum(value1, value2));
}
public Integer sum(Integer a, Integer b) {
// 如果为非空,返回 Optional 描述的指定值,否则返回空的 Optional。
Optional<Integer> ao = Optional.ofNullable(a);
Optional<Integer> bo = Optional.ofNullable(b);
// Optional.isPresent - 判断值是否存在
System.out.println("第一个参数值存在:" + ao.isPresent());
System.out.println("第二个参数值存在:" + bo.isPresent());
// Optional.orElse - 如果值存在,返回它,否则返回默认值
Integer value1 = ao.orElse(0);
Integer value2 = bo.orElse(0);
//Optional.get - 获取值,值需要存在
return value1 + value2;
}
}**
Nashorn JavaScript
N a s h o r n J a v a S c r i p t E n g i n e 在 J a v a 15 已经不可用了。 \color{red}{Nashorn JavaScript Engine 在 Java 15 已经不可用了。} NashornJavaScriptEngine在Java15已经不可用了。
日期时间 API
在旧版的 Java 中,日期时间 API 存在诸多问题,其中有:
- 非线程安全 − java.util.Date 是非线程安全的,所有的日期类都是可变的,这是 Java 日期类最大的问题之一。
- 设计很差 − Java 的日期/时间类的定义并不一致,在 java.util 和 java.sql 的包中都有日期类,此外用于格式化和解析的类在 java.text 包中定义。java.util.Date 同时包含日期和时间,而 java.sql.Date 仅包含日期,将其纳入 java.sql 包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
- 时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此 Java 引入了 java.util.Calendar 和 java.util.TimeZone 类,但他们同样存在上述所有的问题。
Java 8 在 java.time 包下提供了很多新的 API。以下为两个比较重要的 API:
- Local(本地) − 简化了日期时间的处理,没有时区的问题。
- Zoned(时区) − 通过指定的时区处理日期时间。
新的 java.time 包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。
本地化日期时间 API
LocalDate/LocalTime 和 LocalDateTime 类可以在处理时区不是必须的情况。
实例
public class PracticeTest {
@Test
public void test() {
// 获取当前的日期时间
LocalDateTime currentTime = LocalDateTime.now();
System.out.println("当前时间:" + currentTime);
LocalDate date1 = currentTime.toLocalDate();
System.out.println("date1:" + date1);
Month month = currentTime.getMonth();
int day = currentTime.getDayOfMonth();
int second = currentTime.getSecond();
System.out.println("月:" + month + ",日:" + day + ",秒:" + second);
LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012);
System.out.println("date2:" + date2);
LocalDate date3 = LocalDate.of(2014, Month.DECEMBER, 12);
System.out.println("date3:" + date3);
LocalTime date4 = LocalTime.of(22, 15);
System.out.println("date4:" + date4);
// 解析字符串
LocalTime date5 = LocalTime.parse("20:15:30");
System.out.println("date5:" + date5);
LocalDateTime date6 = LocalDateTime.parse("2012-07-10 14:05:45", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println("date6:" + date6);
}
}
使用时区的日期时间API
需要考虑到时区,就可以使用时区的日期时间 API
实例
public class PracticeTest {
@Test
public void test() {
// 获取当前的日期时间
ZonedDateTime date1 = ZonedDateTime.parse("2015-12-03T10:15:30+05:30[Asia/Shanghai]");
System.out.println("date1:" + date1);
ZoneId id = ZoneId.of("Europe/Paris");
System.out.println("ZoneId:" + id);
ZoneId currentZone = ZoneId.systemDefault();
System.out.println("当前时区:" + currentZone);
}
}
Base64
在 Java 8 中,Base64 编码已经成为 Java 类库的标准。
Java 8 内置了 Base64 编码的编码器和解码器。
Base64 工具类提供了一套静态方法获取下面三种 BASE64 编解码器:
- 基本:输出被映射到一组字符 A-Za-z0-9+/,编码不添加任何行标,输出的解码仅支持A-Za-z0-9+/。
- URL:输出映射到一组字符 A-Za-z0-9+_,输出是URL和文件。
- MIME:输出隐射到MIME友好格式。输出每行不超过76字符,并且使用’\r’并跟随’\n’作为分割。编码输出最后没有行分割。
实例
public class PracticeTest {
@Test
public void test() {
// 获取当前的日期时间
String base64encodeString = Base64.getEncoder().encodeToString("runoob?java8".getBytes(StandardCharsets.UTF_8));
System.out.println("Base64 编码字符串(基本):" + base64encodeString);
byte[] base64decodeBytes = Base64.getDecoder().decode(base64encodeString);
System.out.println("原始字符串: " + new String(base64decodeBytes));
base64encodeString = Base64.getUrlEncoder().encodeToString("runoob?java8".getBytes(StandardCharsets.UTF_8));
System.out.println("Base64 编码字符串 (URL):" + base64encodeString);
// StringBuilder 是线程不安全的,不能同步访问
// 使用 StringBuffer 时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新对象
// 由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder。
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10; i++) {
stringBuilder.append(UUID.randomUUID().toString());
}
byte[] mimeBytes = stringBuilder.toString().getBytes(StandardCharsets.UTF_8);
String mimeEncodeString = Base64.getMimeEncoder().encodeToString(mimeBytes);
System.out.println("Base64 编码字符串(mime):" + mimeEncodeString);
}
}