Java 8——十大新特性

Java 8(LTS)是Java历史上一个重大的版本更新,发布于2014年3月18日,有Lambad表达式、Stream、Optional、新的日期API等十种新特性。

ps:重要的特性会尽量细讲,不重要的简单概括。

一、Lambda表达式

Lambad表达式被誉为Java 8的最大的特性,标志着Java向函数式边冲迈出了重要的第一步。

1.1 语法

(parameters)->expression
//或者
(parameters)->{statements:}
  • parameters:参数列表,可以为空或多个参数。

  • ->:操作符,作用是将参数和Lambda主体分开。

  • expression:返回值,或者在主体中执行的单一表达式。

  • {statements}:主体部分,如果有多个操作,就需要这种形式。

看一个简单的Lambda表达式,遍历一个list:

public static void main(String[] args) {
    List<String> strList = ImmutableList.of("hello", "world", "!!!");
    strList.forEach(s-> System.out.println(s));
//    strList.forEach(s->{
//        if("hello".equals(s)){
//            System.out.println(s);
//        }
//    });
}

1.2 实现原理

网上有人说他不是语法糖,但是这个说法并不准确。Lambda表达式不是匿名内部类的语法糖,但它也是一个语法糖,实现的方式主要是依赖了JVM底层提供的Lambda相关api。

我们对上述例子编译一下:

可以看到只有一个文件如果是内部类的话编译后应该会有两个class文件才对,所以他不是内部类的语法糖。

下面我们反编译看下:

public class LambdaTest {
    public static void main(String[] args) {
        ImmutableList strList = ImmutableList.of((Object)"hello", (Object)"world", (Object)"!!!");
        strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());
    }
​
    private static /* synthetic */ void lambda$main$0(String s) {
        System.out.println(s);
    }
}

可以看到,在foreach中,其实调用了LambdaMetafactory的metaFactory方法,这个方法的第五个参数指定了方法的实现(感兴趣的可以去看下源码),这里其实调了一个lambda$main$0方法进行了输出。

这个比较简单,我们换个稍微复杂点的:

public static void main(String[] args) {
    List<String> strList = ImmutableList.of("hello", "world", "!!!");
​
    strList.stream().
            filter(s->s.contains("hello"))
            .collect(Collectors.toList())
            .forEach(s-> System.out.println(s));
}

再反编译一下:

public class LambdaTest {
    public static void main(String[] args) {
        ImmutableList strList = ImmutableList.of((Object)"hello", (Object)"world", (Object)"!!!");
        strList.stream().filter((Predicate<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList()).forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$1(java.lang.String ), (Ljava/lang/String;)V)());
    }
​
    private static /* synthetic */ void lambda$main$1(String s) {
        System.out.println(s);
    }
​
    private static /* synthetic */ boolean lambda$main$0(String s) {
        return s.contains("hello");
    }
}

可以看到两个Lambda表达式分别调用了lambda$main$1lambda$main$0方法。

所以Lambda表达式的实现其实是依赖了一些底层的api,在编译阶段,编译器会把lambda表达式进行解糖,转换成调用内部api的方式。

1.3 使用条件

有两个条件:

  • 必须有相应的函数式接口:也是Java8引入的,下面会介绍到。(第四节)

  • 类型推断机制:也是Java 8优化的类型推断机制,允许编译器根据上下文自动推断Lambda表达式的参数类型,下面也会讲。(第十节)

二、Stream流

Java 8新特性两个最为重要的更新:第一个就是上述的Lambda表达式,另一个则是Stream流。

2.1 相关概念

Stream流是一种对Java集合和表达的高阶抽象,类似于用SQL语句从数据库查询数据的直观方式。Stream API可以极大提高Java程序眼你的生产力,让程序员写出高效率、简洁、优雅的代码。

Stream流具备以下几个特点:

  • 无存储:Stream不是一种数据结构,可以理解为它是某种数据源的视图。数据源:数组、Java容器、I/O channel等。

  • 为函数式编程而生:对Stream的任何修改都不会修改背后的数据源,而是会生成一个新的Stream(线程安全)。

  • 惰性执行:Strean的操作不会立即执行,而是真正需要结果的时候才会执行。

对于Stream流的处理,主要分为三个步骤:流的创建、中间操作以及最终操作。

2.2 流的创建

Java 8 Stream流的创建有很多种,这里就介绍最常用的两种。

2.2.1 已有集合创建

Java 8除了增加了很多Stream相关的类之外,还对集合类自身做了增强:

public static void main(String[] args) {
    List<String> strList = ImmutableList.of("hello", "world", "!!!");
    //方式一:普通流
    Stream<String> stream = strList.stream();
    //方式二:并行流
    Stream<String> stringStream = strList.parallelStream();
}

2.2.2 Straem创建

可以用Stream提供的方法,直接返回一个流:

public static void main(String[] args) {
    Stream<String> stream = Stream.of("hello", "world", "!!!");
}

ps:另外还可以用Stream的builder方法、generate方法或iterate方法,这几种都不常用,感兴趣的可以去试一下。

2.3 中间操作

这些操作允许在Stream上进行一系列的数据处理,常见的操作有fileter、map、limit、skip、sorted、distinst等:

public static void main(String[] args) {
    //1、filter:过滤出符合条件的元素
    List<String> strList = ImmutableList.of("hello", "world", "!!!");
    //过滤出集合中包含“o”的元素,输出结果:hello, world
    strList.stream().filter(s -> s.contains("o")).forEach(System.out::println);
​
    //2、map:映射每个元素到对应的结果
    List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9);
    //求平方数,输出结果:9,1,16,1,25,81
    list.stream().map(i->i*i).forEach(System.out::println);
​
    //3、limit/skip:limit返回Stream的前面n个元素;skip则是跳过前n个元素
    //返回前3个元素,输出结果:3,1,4
    list.stream().limit(3).forEach(System.out::println);
    //跳过前3个元素,输出结果:1,5,9
    list.stream().skip(3).forEach(System.out::println);
​
    //4、sorted:对流进行排序
    //默认升序:113459
    list.stream().sorted().forEach(System.out::println);
​
    //5、distinct:去重
    //输出结果:31459
    list.stream().distinct().forEach(System.out::print);
}

2.4 最终操作

Stream的中间操作得到的结果还是一个Stream,那么如何转为我们需要的类型呢?比如计算出流中元素个数、转为集合等,这就是最终操作实现的。

最终操作会消耗流,也就是说在最终操作之后就不能再使用流了。

常用的有forEach、count以及collect:

public static void main(String[] args) {
    //1、foreach:迭代流中的数据
    List<String> list = Arrays.asList("hello", "world", "!!!");
    //输出结果:hello、world
    list.stream().limit(2).forEach(System.out::println);
​
    //2、count:统计流中元素个数
    //输出结果:2
    long o = list.stream().filter(s -> s.contains("o")).count();
​
    //3、collect:就是一个归约操作,将流中元素汇总结果
    //输出结果:!!!
    list = list.stream().skip(2).collect(Collectors.toList());
}

ps:另外还有匹配相关的api,比如allMatch()、anyMatch()、noneMatch()等,查询相关的min()、max()、findAny等等。

三、Optional

NullPointException是最令我们头疼的异常之一,而Java为了处理这种异常也在一直努力着,比如在Java 8引入了Optional类来减少NEP的发生,Java 14引入了Help NEP来帮助我们更好地排查(这个后面再说)。

举几个常用的例子:

public static void main(String[] args) {
    Student stu = createStu();
    //创建一个Optional对象
    Optional<Student> optional = Optional.ofNullable(stu);
    //用法一:判断是否为空,true不为空,false为空
    if(optional.isPresent()){
        //不为null
        Student student = optional.get();
    }
    //用法二:如果为空则初始化一个Student对象并返回
    Student student = optional.orElse(new Student("李四", 20));
    //用法三:如果student为空,直接抛异常,否则获取Student对象
    Student student1 = optional.orElseThrow(() -> new RuntimeException("Student不存在"));
}
​
public static Student createStu(){
    return new Student("张三",18);
}

四、函数式接口

函数式接口是一个只有一个抽象方法的接口(Single Abstract Method)。Java 8引入的函数式接口主要目的是为了支持Lambda表达式,比如CompletableFuture、Stream类中的方法都有这些函数式接口作为参数。

从本质上说Lambda表达式就是一个函数式接口的实例。

Java 8中新增的函数式接口都在java.util.function包中,我们使用最多的也就四个:

ps:Java 8之前也已经有了大量的函数式接口,比如我们最熟悉的Runnable、Callable、Comparator接口,还有很多很多,这里就不列举了。

五、新的日期API

5.1 为什么要增加新设计新的日期API

在Java 8之前,我们一般用java.util.Date、java.util.Calender或java.text.SimpleDateFormat来处理与时间相关的常见,但是这三个类存在一些问题:

  • 线程不安全。

  • 使用起来比较复杂,通常需要大量的样板代码。

  • 时区处理困难。

  • java.util.Date类是可变的,这意味着我们可以随时修改它,如果一不小心就会导致数据不一致问题。

基于上述原因,Java重新设计了时间日期的API,主要有Instant、LoacalDate、LocalTiem、LoacalDateTime等。

5.2 Instant

Instant表示时间线上的点,不可变。参考的是标准的Java纪元(epoch),即1970-1-1 0:0:0。

public static void main(String[] args) {
    Instant start = Instant.now();
    //逻辑
    Instant end=Instant.now();
    //获取时间差(类似System.currentTimeMillis()功能)
    System.out.println("耗时:"+Duration.between(start,end).toMillis()+"ms");
}

5.3 LocalDate

表示不包含时间信息的日期,不可变。ps:DateTimeFormatter也是Java 8新增的类,不可变。

public static void main(String[] args) {
    //创建当前日期
    LocalDate now = LocalDate.now();
    //创建指定日期的LocalDate对象
    LocalDate of = LocalDate.of(2024, 8, 29);
    //使用DateTimeFormatter解析一个LocalDate对象
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    LocalDate localDate = LocalDate.parse("2024-08-29",dateTimeFormatter);
    //日期计算
    //加2年:2026-8-29
    LocalDate localDate1 = localDate.plusYears(2);
    //加2月
    LocalDate localDate2 = localDate.plusMonths(2);
    //加2天
    LocalDate localDate3 = localDate.plusDays(2);
    //加2周
    LocalDate localDate4 = localDate.plusWeeks(2);
​
    //减2年:2022-8-29
    LocalDate localDate5 = localDate.minusYears(2);
    System.out.println("localDate5 = " + localDate5);
    //...
}

5.4 Period

用于处理时间间隔的类。

public static void main(String[] args) {
    LocalDate start = LocalDate.of(1970, 1, 1);
    LocalDate end = LocalDate.of(2024,8,29);
    Period period = Period.between(start, end);
    //相隔54年7个月零28天
    System.out.println("相隔"+period.getYears()+"年"+period.getMonths()+"个月零"+period.getDays()+"天");
}

5.5 LocalTime

获取不包含日期的时间,也就是一天中的时间。

public static void main(String[] args) {
    //当前时间:21:46:16.462
    LocalTime now = LocalTime.now();
    //创建指定时间:13:14:52.000000001
    LocalTime time = LocalTime.of(13, 14, 52, 1);
    //时间计算
    LocalTime localTime = time.plusHours(1);
    LocalTime localTime1 = time.minusHours(1);
    //...分、秒、纳秒,这里就省略了
    //获取时间信息:时、分、秒..
    int hour = time.getHour();
    //修改时间:14:14:52.000000001,时分秒...
    LocalTime localTime2 = time.withHour(14);
}

5.6 LocalDateTime

日期和时间,这里LocalDate和LocalTime结合起来就是了,方法什么的也都一致,这里就不举例了。

5.7 ZoneDateTime

带时区的日期和时间,因为地球上不同区域的时区是不一样的,比如现在北京是晚上,别的地方可能是早晨,所以为了更好地支持全球化的时间操作,有了ZoneDateTime类,用法和LocalTime几乎一样。

public static void main(String[] args) {
    //获取所有时区(589个时区)
    Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
    //默认时区:上海
    ZonedDateTime now = ZonedDateTime.now();
    //上海时间:2024-08-29 22:10:44
    String format1 = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    //改为纽约时区
    ZonedDateTime zonedDateTime = now.withZoneSameInstant(ZoneId.of("America/New_York"));
    //纽约时间:2024-08-29 10:10:44
    String format = zonedDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

六、异步编程神器:CompletableFuture类

Java 8引入了一种基于Future的编程模型,旨在解决Future的局限性。

本节可以看作者之前的文章:Java并发编程第7讲——CompletableFuture、Future和ForkJoinPool(万字详解)

七、接口默认方法和静态方法

在Java8之前,我们知道接口的变量必须时public static final修饰的,方法必须时public abstract修饰的,这意味着我们在接口中每新增一个方法,对应的实现类就必须重写这个方法,灵活性有点差。

所以Java 8接口引入了默认方法,实现类可以不强制重写默认方法。有点更像抽象类了。

同时Java 8还允许在接口中定义静态方法,可以直接用类名.方法名直接调用。

public interface DefaultInterFace {
    default void defaultMeth(){
        System.out.println("默认方法");
    }
    static void staticMethod(){
        System.out.println("静态方法");
    }
}
​
//测试
@Test
void test() {
    DefaultInterFace defaultInterFace=new DefaultInterFaceImpl();
    //调用默认方法
    defaultInterFace.defaultMeth();//默认方法
    //调用静态方法
    DefaultInterFace.staticMethod();//静态方法
}

八、Base64 API

Base 64编码是一种用64个字符表示二进制的方法,注意它并不是一种加密算法,Base 64常用于在不支持二进制数据的系统间传输二进制数据。

在Java 8之前我们通常需要依赖第三方库Apache Common Code来实现Base64的编码和解码。

这个就简单介绍了,直接看个例子吧:

public static void main(String[] args) {
    String str="橡皮人";
    //编码
    String encode = Base64.getEncoder().encodeToString(str.getBytes());
    System.out.println("encode = " + encode);
    //解码
    byte[] decode = Base64.getDecoder().decode(encode);
    System.out.println("decode = " + new String(decode));
}

ps:上述例子是最常见的类型,适用于大多数常见,另外还有URL和文件名安全、MIME类型的Base64编码,直接调用其对应方法即可。

九、类型注解

在Java 8之前,注解仅能用于方法、类、字段上,Java 8引入了类型注解,扩展了注解的应用范围,可以用在任何地方,比如方法的返回类型、变量的类型等,提供了更丰富的编译时检查。

public static void main(String[] args) {
    //1、泛型
    List<@NonNull String> strList=new ArrayList<>();
    strList.add(null);
    //2、类型转换
    Object obj="橡皮人";
    String str= (@NonNull String) obj;
    //3、局部变量
    @NonNull String s=null;
​
}
​
//4、方法返回值上、抛出的异常类型上
//@NonNull仅为说明异常类型上也可以加注解
public static @Positive int t() {
    //@Positive方法返回一个正数
    return -6;
}
//5、接口上
//@NonNull仅为说明接口上也可以加注解
class Anno implements @NonNull DefaultInterFace{
}

那有什么用呢?这样看不直观,看下下面截图:

它会在编译时,如果不符合注解的语义,会给出提示,从而防止发生未知的错误。

十、类型推断优化

类型推断机制也是慢慢演进而来的,比如Java 5引入了泛型、Java 7引入了"<>"操作符简化泛型实例的创建,而Java 8的优化主要十Lambda表达式。

List<String> list = Arrays.asList("hello", "world", "!!!");
​
List<String> strings = list.stream()
        .filter(s -> s.contains("o"))
        .map(String::toUpperCase)
        .collect(Collectors.toList());

比如上述filter和map的中间操作,就是使用了类型推断机制,每个操作得输出类型自动称为下一个操作得输入类型。

ps:不使用的话,每个操作都得指定类型,比如"s"就得写为"(String s)"。

 End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橡 皮 人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值