侧边栏壁纸
博主头像
ZHD的小窝博主等级

行动起来,活在当下

  • 累计撰写 79 篇文章
  • 累计创建 53 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

JDK 8 的新特性(LTS)

江南的风
2015-12-17 / 0 评论 / 0 点赞 / 22 阅读 / 19568 字 / 正在检测是否收录...

Java 8 的新特性

1. Lambda表达式和函数式接口

参考链接:https://openjdk.org/jeps/126

Lambda 表达式是一个匿名函数(指的是没有函数名的函数),直接对应于其中的 Lambda 抽象,Lambda 表达式可以表示闭包。我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码作为实参),也可以理解为函数式编程,将一个函数作为参数进行传递。Lambda 表达式允许把函数作为一个方法的参数,Lambda 表达式的基本语法如下:

(parameters) -> expression 或 (parameters) -> {statements;}

例子:

// 使用 Lambda 表达式计算两个数的和
MathOperation addition = (a, b) -> a + b;

// 调用 Lambda 表达式
int result = addition.operation(5, 3);
System.out.println("5 + 3 = " + result);

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// 使用 Lambda 表达式遍历列表
names.forEach(name -> System.out.println(name));

// 传统的匿名内部类
Runnable runnable1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World!");
    }
};

// 使用 Lambda 表达式
Runnable runnable2 = () -> System.out.println("Hello World!");

Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。函数式接口是一种特殊类型的接口,它仅包含一个抽象方法(可隐式地转换为Lambda表达式)。java.lang.Runnablejava.util.concurrent.Callable是这类接口的典型示例。

然而,函数式接口的脆弱性在于其定义的严格性:一旦接口中增加了额外的方法,它就会失去函数式接口的特性,并可能导致编译错误。为了增强代码的健壮性和明确性,Java 8 引入了@FunctionalInterface注解,专门用于标记那些设计为函数式接口的接口。这个注解不仅帮助开发者快速识别函数式接口,还能在接口不再符合函数式接口定义时提供编译时检查。

下面是一个简单的函数式接口定义示例,使用了@FunctionalInterface注解:

@FunctionalInterface  
interface SimpleFunction {  
    void performAction();  
}

这样,任何尝试向SimpleFunction接口中添加额外方法的尝试都会立即被编译器捕获,从而防止了因违反函数式接口规则而导致的潜在问题。

2. 接口默认方法和静态方法

参考链接:https://openjdk.org/jeps/126

Java 8 新增了接口的默认方法和静态方法。简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。我们只需在方法名前面加个 default 关键字即可实现默认方法。

默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要实现,接口提供的默认方法会被接口的实现类继承或者重写(非必须)。

// 默认方法
public interface Singleton{
    default String getName(){
        return "默认方法";
    }
}

public static class SingletonImpl implements Singleton{
    // 可以重写getName方法
}

// 静态方法
public interface SingletonFactory{
    // Interfaces now allow static methods
    static Singleton getInstance(Supplier<Singleton> supplier) {
        return supplier.get();
    }
}

// 使用例子
public static void main( String[] args ) {
    Singleton singleton = SingletonFactory.getInstance( SingletonImpl::new );
    System.out.println( singleton.getName() );
}

3. 方法引用

参考链接:https://openjdk.org/jeps/160

方法引用通过方法的名字来指向一个方法。方法引用可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 ::

下面,我们在 Car 类中定义了 4 个方法作为例子来区分 Java 中 4 种不同方法的引用。

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
 
public class Car {
    //Supplier是jdk1.8的接口,这里和lamda一起使用了
    public static Car create(final Supplier<Car> supplier) {
        return supplier.get();
    }
 
    public static void collide(final Car car) {
        System.out.println("Collided " + car.toString());
    }
 
    public void follow(final Car another) {
        System.out.println("Following the " + another.toString());
    }
 
    public void repair() {
        System.out.println("Repaired " + this.toString());
    }
}
// 1.构造器引用:它的语法是Class::new,或者更一般的Class< T >::new
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );

// 2.静态方法引用:它的语法是Class::static_method
cars.forEach( Car::collide );

// 3.特定类的任意对象的方法引用:它的语法是Class::method,注意,这个方法没有定义入参
cars.forEach( Car::repair );

// 特定对象的方法引用:它的语法是instance::method
final Car police = Car.create( Car::new );
cars.forEach( police::follow );

4. 重复注解

参考链接:https://openjdk.org/jeps/120

自从Java 5版本引入了注解功能后,这一特性迅速流行开来,广泛应用于各种编程框架和项目之中。然而,早期注解存在一个显著限制:相同位置不能重复使用同一注解。为了突破这一限制,Java 8引入了重复注解的概念,允许在相同位置多次应用同一注解,极大地增强了注解的灵活性和表达能力。

在Java 8中,通过@Repeatable注解来定义可重复的注解,这并非直接对Java语言层面的修改,而是编译器采用的一种技术手段,其底层实现机制保持不变。以下是一个简单的示例来说明如何使用@Repeatable注解:

// 首先,定义一个容器注解来存储多次使用的注解实例  
@interface MyAnnotations {  
    MyAnnotation[] value();  
}  
  
// 使用@Repeatable注解标记MyAnnotation为可重复注解,并指定其容器注解为MyAnnotations  
@Repeatable(MyAnnotations.class)  
@interface MyAnnotation {  
    String value();  
}  
  
// 现在可以在同一个地方多次使用MyAnnotation注解了  
@MyAnnotation(value = "Hello")  
@MyAnnotation(value = "World")  
public class MyClass {  
    // 类体  
}  
  
// 编译器会将上述用法转换成使用容器注解的形式,但代码编写时无需关心这一转换

上述代码展示了如何使用@Repeatable注解定义一个可重复的注解MyAnnotation,并通过容器注解MyAnnotations来存储这些重复的注解实例。这样,开发者就可以在同一个位置多次使用MyAnnotation注解,而无需担心之前的限制。需要注意的是,虽然@Repeatable提供了这种便利,但其背后的实现细节(如编译器如何转换代码)对大多数开发者来说是透明的。

5. 拓宽了注解的应用场景

参考链接:https://openjdk.org/jeps/104

Java 8显著扩展了注解的应用范围,使得注解现在几乎可以应用于编程语言的各个元素之上。无论是局部变量、接口类型、超类声明、接口实现类,还是函数的异常定义,注解都能发挥其作用。以下是一些具体的应用示例,展示了这种广泛的适用性:

  • 局部变量:注解可以直接附加在局部变量上,为局部变量提供额外的元数据信息。

  • 接口类型:在接口定义上使用注解,可以为接口添加特定的说明或配置信息。

  • 超类(父类):注解也可以应用于类的继承声明中,即可以标记一个类是如何扩展自另一个类的。

  • 接口实现类:在实现接口时,注解可以用来标记实现类或与实现相关的特定配置。

  • 函数异常定义:在方法签名中定义异常时,也可以使用注解来提供关于这些异常的额外信息或处理指示。

这些例子展示了Java 8中注解功能的强大和灵活性,使得开发者能够以更加丰富的方式利用注解来增强代码的可读性、可维护性和可扩展性。

6. 在运行时获得Java程序中方法的参数名称

参考链接:https://openjdk.org/jeps/118

Java 8之前,为了在运行时获取Java方法中参数的名称,经验丰富的Java开发者不得不依赖像Paranamer这样的第三方库。不过,Java 8正式将这一功能标准化,从两个层面提供了支持:在语言层面,通过反射API新增的Parameter.getName()方法,可以直接访问方法参数的名称;在字节码层面,新的javac编译器增加了-parameters选项,以保留并生成包含参数名信息的字节码。这样,开发者就能更方便地在运行时获取到方法参数的名称了。

例如:

package com.javacodegeeks.java8.parameter.names;
 
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
 
public class ParameterNames {
    public static void main(String[] args) throws Exception {
        Method method = ParameterNames.class.getMethod("main", String[].class );
        for( final Parameter parameter: method.getParameters() ) {
            System.out.println( "Parameter: " + parameter.getName() );
        }
    }
}

在Java 8中这个特性是默认关闭的,因此如果不带 -parameters 参数编译上述代码并运行,则会输出:Parameter: arg0

如果带 -parameters 参数,则会输出(正确的结果):Parameter: args

7. Stream API

参考链接:https://www.javacodegeeks.com/2014/05/the-effects-of-programming-with-java-8-streams-on-algorithm-performance.html

Java 8 引入了流(Stream)这一新概念,它提供了一种声明式的方式来处理数据集合。流API的设计灵感源自SQL查询,为Java集合操作提供了高级抽象,让数据处理更加直观。

通过使用流,Java程序员能够显著提升开发效率,编写出既高效又简洁的代码。在流的处理过程中,数据元素被视作一个流动的序列,在管道内传输,并在各个节点上经历如筛选、排序、聚合等中间操作。最终,这些操作通过终端操作汇总成处理结果。

简而言之,流API让Java集合的操作变得像编写SQL查询一样直观,从而实现了代码的优雅与效率的双重提升。流主本身并不存储元素,流要是用来计算的。

+--------------------+       +------+   +------+   +---+   +-------+
| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
+--------------------+       +------+   +------+   +---+   +-------+

以上的流程转换为 Java 代码为:

List<Integer> transactionsIds = 
widgets.stream()
             .filter(b -> b.getColor() == RED)
             .sorted((x,y) -> x.getWeight() - y.getWeight())
             .mapToInt(Widget::getWeight)
             .sum();

Java 8 提供了streamparallelStream两种流:

  • stream() − 为集合创建串行流

  • parallelStream() − 为集合创建并行流

parallelStream与Stream的区别在于parallelStream开启了多线程的处理方式,parallelStream采用了Fork/Join线程池,在我的另一篇文章里详细讲了fork/join线程池的原理和使用

用例:

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// filter
strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
// forEach
strings.stream().forEach(System.out::println);
// limit
strings.stream().limit(10).forEach(System.out::println);
// sorted
strings.stream().sorted().forEach(System.out::println);
// map
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
// 获取对应的平方数
numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());
// Collectors
strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
// 统计
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的数 : " + stats.getMax());
System.out.println("列表中最小的数 : " + stats.getMin());
System.out.println("所有数之和 : " + stats.getSum());
System.out.println("平均数 : " + stats.getAverage());

// 并行(parallel)程序
long count = strings.parallelStream().filter(string -> string.isEmpty()).count();

其中Collectors 是一个非常有用的工具类,它提供了各种方法来将 Stream 中的元素收集到一个集合中,或者进行其他类型的聚合操作。以下是一些常见的 Collectors 方法及其用法:

// 收集到列表(List)
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());

// 收集到集合(Set)
Set<String> set = Stream.of("a", "b", "c").collect(Collectors.toSet());

// 收集到特定类型的集合
ArrayList<String> arrayList = Stream.of("a", "b", "c").collect(Collectors.toCollection(ArrayList::new));

// 收集到映射(Map)
Map<String, Integer> map = Stream.of("a", "b", "c").collect(Collectors.toMap(s -> s, s -> s.length()));

// 分组(Grouping)
Map<Integer, List<String>> groupedByLength = Stream.of("a", "bb", "ccc").collect(Collectors.groupingBy(String::length));

// 分区(Partitioning)
Map<Boolean, List<String>> partitionedByLength = Stream.of("a", "bb", "ccc").collect(Collectors.partitioningBy(s -> s.length() > 1));

// 连接字符串(Joining)
String joined = Stream.of("a", "b", "c").collect(Collectors.joining(", "));

// 计算总和(Summing)
int sum = Stream.of(1, 2, 3).collect(Collectors.summingInt(Integer::intValue));

// 计算平均值(Averaging)
double average = Stream.of(1, 2, 3).collect(Collectors.averagingInt(Integer::intValue));

// 计算统计信息(Summarizing)
IntSummaryStatistics stats = Stream.of(1, 2, 3).collect(Collectors.summarizingInt(Integer::intValue));

8. Optional

参考链接:https://docs.oracle.com/javase/8/docs/api/

Java应用中,空值异常(NullPointerException)是一个频繁出现的问题。为了解决这一问题,避免代码中遍布繁琐的null检查,从而在编写时保持代码的整洁性,Google Guava在Java 8之前率先引入了Optional类。这个类允许开发者以更优雅的方式处理可能为null的值。

随后,Java 8官方也采纳了这一思想,将Optional作为标准库的一部分,使得所有Java开发者都能利用这一特性。Optional的核心功能是提供一种容器,用于安全地持有或不持有某个类型的值(T),而无需直接使用null。它提供了一系列方法,帮助开发者在不进行显式null检查的情况下,安全地访问和操作这些值。例如,通过使用Optional,开发者可以更加清晰地表达意图,减少空指针异常的风险。

例如:

Optional<String> fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );        
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) ); 
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
  • Optional.of(T t) : 创建一个 Optional 实例

  • Optional.empty() : 创建一个空的 Optional 实例

  • Optional.ofNullable(T t):若 t 不为 null,创建 Optional 实例,否则创建空实例

  • isPresent() : 判断是否包含值

  • orElse(T t) : 如果调用对象包含值,返回该值,否则返回t

  • orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值

  • map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()

  • flatMap(Function mapper):与 map 类似,要求返回值必须是Optional

9. 新时间日期API

参考链接:https://jcp.org/en/jsr/detail?id=310

在旧版Java中,日期时间处理API饱受诟病,主要问题包括:

  1. 线程安全性缺失java.util.Date及其相关日期类均设计为可变类型,不具备线程安全特性,这成为了Java日期处理中的一个显著缺陷。

  2. 设计不一致与冗余:Java的日期时间类设计显得杂乱无章,分散在java.utiljava.sql以及java.text等多个包中,缺乏统一性和一致性。尤为突出的是,java.util.Datejava.sql.Date虽然名称相同,但功能各异(前者包含日期和时间,后者仅含日期),且将日期类置于java.sql包中显得不合逻辑,这样的设计无疑增加了使用的复杂性和混淆。

  3. 时区与国际化支持不足:原始的日期类缺乏对时区处理的直接支持,也未充分考虑国际化需求。尽管后来引入了java.util.Calendarjava.util.TimeZone来试图弥补这一不足,但这些类同样继承了旧有API的诸多问题,未能从根本上解决问题。

Java 8通过发布新的Date-Time API (JSR 310)来解决以上日期与时间的问题,新的Date-Time API具有:不可变性(线程安全)、概念清晰便于理解、更好的时区支持、强大的时间间隔和持续时间处理能力、灵活的日期时间格式化与解析、更好的性能和兼容性等优势。

例子:

//当前时间
LocalDateTime now = LocalDateTime.now();

// 设置时间
LocalDateTime of = LocalDateTime.of(2020, 2, 01, 5, 10, 53);

// 计算:加/减两年
LocalDateTime of2 = of.plusYears(2);
LocalDateTime of3 = of.minusYears(2);

// 获取年月日等值
of.getYear() | of.getMonthValue() | of.getDayOfMonth()

//获取秒数时间戳(10位)|(13位)
now.toEpochSecond(ZoneOffset.of("+8")) | now.toEpochSecond(ZoneOffset.of("+8")).toEpochMilli()

// 时间间隔
Instant i1 = Instant.now();
TimeUnit.SECONDS.sleep(1);
Instant i2 = Instant.now();
long l = Duration.between(i1, i2).toMillis();
System.out.println(l);

// 时间格式化
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String format = dtf.format(now);

// Date 转为 LocalDateTime
Date date = new Date();
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());

...

10. Base64

参考链接:https://www.javacodegeeks.com/2014/04/base64-in-java-8-its-not-too-late-to-join-in-the-fun.html

对Base64编码的支持已经被加入到Java 8官方库中,这样不需要使用第三方库就可以进行Base64编码。Base64工具类提供了一套静态方法获取下面三种BASE64编解码器:

  • 基本:输出被映射到一组字符A-Za-z0-9+/,编码不添加任何行标,输出的解码仅支持A-Za-z0-9+/。

  • URL:输出映射到一组字符A-Za-z0-9+_,输出是URL和文件。

  • MIME:输出映射到MIME友好格式。输出每行不超过76字符,并且使用'\r'并跟随'\n'作为分割。编码输出最后没有行分割。

例如:

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Base64Example {
    public static void main(String[] args) {
        // Original text to be encoded
        final String text = "Base64 finally in Java 8!";

        // Encoding the text to Base64
        final String encoded = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8));
        System.out.println("Encoded: " + encoded);

        // Decoding the Base64 encoded string back to the original text
        final String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
        System.out.println("Decoded: " + decoded);
    }
}

ps:什么是MIME编码格式,我的另一篇文章有详细介绍

11. 并行数组

参考链接:https://openjdk.org/jeps/103

JDK1.8增加了对数组并行处理的方法(parallelXxx),使用JSR 166 Fork/Join并行公共池提供并行数组排序的数组。

Arrays.parallelSort 是 Java 8 引入的一个方法,用于对数组进行并行排序。这个方法直接作用于数组,而不需要先将其转换为流(Stream)。它利用了多核处理器的能力来加速排序过程,但请注意,并行排序并不总是比传统的顺序排序更快,特别是在数据量较小或数组已经接近排序完成的情况下。

例如:

// 定义一个未排序的整数数组  
int[] numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};  
  
// 打印原始数组  
System.out.println("原始数组: " + Arrays.toString(numbers));  

// 使用Arrays.parallelSort方法对数组进行并行排序  
Arrays.parallelSort(numbers);  

// 打印排序后的数组  
System.out.println("排序后的数组: " + Arrays.toString(numbers));

12. JVM 变化

使用 MetaspaceJEP 122)代替持久代(PermGen space)。

在JVM参数方面,使用 -XX:MetaSpaceSize-XX:MaxMetaspaceSize 代替原来的 -XX:PermSize-XX:MaxPermSize

0

评论区