jdk8 新特性真的只有流和lambda表达式吗?一篇文章帮你分析透彻

Java 进阶 解读源码 专栏收录该内容
2 篇文章 0 订阅

大部分小伙伴都知道jdk8新特性有lamda表达式和流,其实除此之外,还有许多小的点 , 你能够知道别人不知道的点,那么这就是你面试中的亮点,了解到熟练差距可能就在这里, 同时总结一下 作为自己的一个知识积累也是不错的.

正文

1. 接口默认实现和静态方法

也就是说在1.8之后可以在接口中提供默认的实现,而静态方法与默认实现的区别在于调用方式不一样,而且默认方法可以被子类重写覆盖

引入默认实现,那么在Java中多实现的原则,出现多个默认方法冲突,应该在子类中重写,解决冲突

默认实现在源码中也得到了体现,如:

  • java.lang.Iterable#forEach
  • java.util.Collection#stream
  • java.util.Map#forEach

2. java.util.Optional 类

NPE(NullPointerException)问题是我们常生产的bug,也是让我们头疼的问题,有调用就可能产生,java.util.Optional 类则是用来规范解决这一问题的类,同时在现在的框架如,JPA框架也支持提供Optional返回

测试代码如下:

	    
	    public void testNPE(){
            Object o = null;
        	if (o==null){
            	System.out.println("xxx为空,非法数据");
            	return ;
        	}
        	 Optional optional   =
                Optional.ofNullable(o);
	        // 查看为空
	        if (!optional.isPresent()){
	            System.out.println("xxx为空,非法数据");
	        }
        }

2.1 Optional 类方法解析

除此之外,其他方法解析入下:

  1. 三个静态方法创建
    • Optional.empty()
    • Optional.of() 如果传入对象为空直接抛NPE
    • Optional.ofNullable() 为空返回空 不抛异常
  2. 实例方法-获取
    • boolean optional.isPresent() 返回是否 不为空
    • T get() 获得内部元素 为空抛异常
    • T orElse(T) 获得内部元素 为空 使用传入默认值
    • T orElseGet(S) 获得内部元素 为空使用 生产者接口 生产默认值
    • Optional filter(Predicate<? super T> predicate) 筛选元素,传入筛选接口
    • Optional flatMap(Function<? super T, Optional> mapper) 扁平化映射,获取内部的元素映射结果

3. Stream流

Java 8 中的 Stream 是对集合(Collection , Map ) 对象功能的增强,它专注于对集合对象进行各种便利、高效的操作,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。

Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以java中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。而和迭代器又不同的是,Stream 可以并行化操作(这里并行化操作使用的是ForkJoinPool),迭代器只能命令式地、串行化操作

3.1 流分类

  • 有限流 , 一般是集合或元素之类生产的流,有限个元素
  • 无限流 , 一般是提供一个生产者接口,源源不断生产,无限个元素

3.2 流创建

  1. 从集合创建流

    • collection.stream
    • collection.parallelStream
  2. 静态方法创建

    • Stream.of(array); 数组创建
    • Stream.generate(Supplier) 生产者接口创建 无限流
    • Stream iterate(final T seed, final UnaryOperator f) 迭代接口创建,提供一个起始值和后续操作 无限流
    • Pattern.compile().splitAsStream() 根据正则分隔产生流
    • IntStream range(int startInclusive, int endExclusive) 根据开始和结束生成流

3.3 流中间操作

流在未消费/未做终结操作之前可以执行中间操作,以达到业务需求的目的,如去重,取数,映射,排序,过滤等,以下是常用的大部分操作

  1. 去重 distinct()
  2. 排序 .sorted() 可以传入指定的比较顺序接口
  3. 限制个数 .limit() 限制元素个数 , 可以通过这个由无限流生产有限个元素
  4. 过滤 .filter() 传入指定的过滤接口
  5. 映射 flatXXX() ,这种通常是复杂元素流中提取内部元素流操作 传入对应取数逻辑的接口
  6. 统计 .count()
  7. 取数
    • 获取第一位 .findFirst()
    • 获取任意一位 .findAny()
    • 取最大 .max()
    • 取最小 .min()

3.4 流终结操作

在得到我们需要的元素流之后,我们需要调用终结操作,以获取/消费目标流中元素,常见有,打印,转换数组,转换集合对象,经过终结操作之后,无法再操作此流.

  1. 消费类 .forEach() .forEachOrdered() 排序后操作
  2. 转换数组 .toArray()
  3. 转换集合对象
    - .collect(Collectors.toSet())
    - .collect(Collectors.toList())
    - .collect(Collectors.toMap()) / .collect(Collectors.toConcurrentMap()) 此类需要提供 kv 的映射关系

注意:
这里toMap 如果出现重复Key 会报错的,因此需要提前去重
同时使用的是merge , 如果出现空的Key 那可就直接NPE 了哦

4. lambda 表达式

lambda 表达式 其实就是语法糖,目的就是简化代码,让代码更加优雅.在编译过程中会转换成实际内部类/方法调用等实现,在编译期已经替换因此不会影响代码执行效率,可以放心使用

4.1 基础:简化方法

我们直接使用代码+注释从一个普通内部类的参数到语句到返回值,到最后的各种静态方法,实例方法的简化介绍

4.1.1 无参数简化

第一个例子很重要!


    public void testContext() {

        // 无参数
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(1111);
            }
        };
		// 这就是最基本的表达式
		//  首先,Java 编译器 具备类型推断功能 , 可以知道这个表达式是实现Runnable接口 ,那么类型可以省略
		// 同样 方法只有一个 方法名自然也可以省略 
		// () 表示 无参数
		// -> 后面接方法体 
		// System.out.println(1111); 是 实际的方法体 
        runnable = ()-> System.out.println(1111); 
        // 本来完全版应该加上大括号 {System.out.println(1111);} 
        // 但是因为是单条语句,所以你懂得,省略了
	}

4.1.2 含参数,含返回值




    public void testMuti() {
		// 这是过滤 kv 的一个接口实现
        BiPredicate<String, String> biPredicate = new BiPredicate<String, String>() {
            @Override
            public boolean test(String s, String s2) {
                return s.equals(s2);
            }
        };
		// 根据 上面原理可以知道
		// 首先 类型可以省略
		// 只保留方法 
	    // 中间加上箭头
        biPredicate = (String s, String s2) -> {
            return s.equals(s2);
        };
    	// 除此之外 方法只有一条 那么大括号可以省略,同理 返回值 是不是也可以省略 (其实这里如果省略大括号,不省略return 会报错)
    	biPredicate = (String s, String s2) -> s.equals(s2);
		// 上面我们分析,编译器可以得知接口信息,那么类型是不是可以从左边推测出来
        biPredicate = (s, s2) -> s.equals(s2);
		// 最后就得到了最终版本
		// 附: 这中间的命名 可以随便命名 ,与方法体一致即可
    
    }
    

4.2 基础引用方法

除了上述的内部类可以使用lambda简化方法,还有一部分也可以使用lambda表达式引用实例与实例方法,类与静态方法
有三种:

  1. 外部实例::方法
  2. 类::静态方法
  3. 内部实例::实例方法
  4. 构造器引用

如下,使用的前提时,参数个数能够对应方法,如果不能推断则不能使用会报编译期错误


 	
	public void testLamda() {
		// 常规内部类
		IntConsumer intConsumer = new IntConsumer() {
			@Override
			public void accept(int value) {
				System.out.println(value);
			}
		};
		// 外部实例::方法 
		intConsumer = System.out::println;
		
		// 类::静态方法
		Stream.generate(Math::random);

		// 内部实例::实例方法
		class OB {
			private double d;

			public OB(double d) {
				this.d = d;
			}

			public void print() {
				System.out.println(d);
			}
		}
		// 内部实例::实例方法
		Stream.generate(() -> new OB(Math.random())).limit(10).forEach(OB::print);
		// 构造器引用
		Stream.generate(Object::new);
		
	}
	

5. 函数接口

jdk8中引入函数式接口,也是为了补充Java中函数编程的不足,函数接口可以使语义更清晰,也符合单一职责原则,可以使代码中,易变与不变分离开,与lambda表达式可以很好的结合

如果接口中只有一个抽象方法,称为函数式接口,,可以使用一个注解 @FunctionalInterface,可以检查接口是否是函数式接口,同时jdk8提供了4个核心函数以及其他的一些函数接口

5.1 4个核心函数接口

  • Consumer 消费型 对传入T 操作,消费 返回void
  • Supplier 供给型接口 无参 返回一个T对象 , 如上述的 Stream.generate() 传入就是供给接口
  • Function<T,R> 函数型接口 传入一个参数 返回任意一种类型 , 可以衍生各种运算等操作
  • Predicate 断言型接口 传入一个参数 返回布尔值 ,流中常用来过滤元素

5.2 其他衍生的函数式接口

  • BiConsumer<T, U> 针对 两个参数的消费接口 ,在Map类的forEach中
  • UnaryOperator 一元操作,接收一个参数 运算操作之后 返回同样的参数
  • BiFunction<T, U, R> 二元操作 , 接收两个参数 返回任意一种类型
  • BiPredicate<T, U> 两个参数的断言型接口 传入两个参数 返回布尔值

5.3 其他衍生的函数式接口的工具类

  • java.util.Comparator#comparing(java.util.function.Function<? super T,? extends U>) 排序接口
  • java.util.Comparator#nullsFirst / nullsLast 空值特殊处理
  • java.util.Comparator#reversed 逆序

虽然Comparator 在1.2 就已经出了,但是 1.8加入了几个新的接口是比较常用的,配合lambda 表达式 十分简洁,但是这里面还有两个小坑:

  1. 排序时出现空值会报错,需要在后面加入空值特殊处理的接口,如:
// 空值直接报错
dto.stream()   
.sorted(Comparator.comparing(DTO::getCreate_time).reversed())
.collect(Collectors.toList());

dto.stream()
.sorted(Comparator.comparing(DTO::getCreate_time,Comparator.nullsLast(String::compareTo))
.reversed())
.collect(Collectors.toList());
  1. 排序后逆序会对前面的全部逆序
// 比如我想先按创建时间逆序,再按发布时间逆序 , 正常思维都是这样写的
dto.stream()
.sorted(Comparator.comparing(DTO::getCreate_time
,Comparator.nullsLast(String::compareTo)).reversed()
.thenComparing(DTO::getPublish_date).reversed())
.collect(Collectors.toList());
// 这个时候实际上是 创建时间顺序,发布时间逆序,因为前面的逆序被后面的逆序抵消了,应该这样写
dto.stream()
.sorted(Comparator.comparing(DTO::getCreate_time
,Comparator.nullsLast(String::compareTo))
.thenComparing(DTO::getPublish_date).reversed())
.collect(Collectors.toList());

6. 日期时间新api

jdk8 日期时间 新 api,主要类 有以下

  • LocalDate 日期
  • LocalTime 时间
  • LocalDateTime 日期时间 上面两个的综合
  • DateTimeFormatter 格式化
  • Instant 时间戳
  • Duration 时间间隔
  • Period 日期间隔
  • TemporalAdjuster 时间校正器 TemporalAdjusters 工具类

除此之外,还有时区等操作类 使用较少就不介绍

6.1 日期时间创建


 /***
     *  创建时间 一般就只有三种需求
     *  当前时间 ,
     *  指定时间 ,
     *  现有时间字符串 ,
     */
    public void testCreate(){
        // 当前时间
        LocalDate localDate = LocalDate.now();
        LocalTime localTime = LocalTime.now();
        LocalDateTime localDateTime = LocalDateTime.now();
        LocalDateTime localDateTime1 = LocalDateTime.of(localDate, localTime);

        // 指定时间
        LocalDateTime of = LocalDateTime.of(2020, 6, 7, 12, 22, 11, 11);

        // 指定字符串
        LocalDateTime parse = LocalDateTime.parse("2020-06-07 12:22:11:11", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SS"));

        System.out.println(localDate);
        System.out.println(localTime);
        System.out.println(localDateTime);
        System.out.println(localDateTime1);
        System.out.println(of);
        System.out.println(parse);
    }


6.2 修改



    /***
     *  业务中时间操作就是计算业务时间有没有超时 ,
     *  常规也是把业务时间加减之后对比另一个业务时间或者当前时间
     *  还有一些舍弃精度操作
     *  还有计算 时间间隔
     *
     *
     *  时间操作修改,不用以前的计算加减 , 或者新建一堆的Calender
     *  在LocalDateTime(其他两个一样,下面就按这个讨论) 中有提供加减方法,需要注意的是
     *      加减之后会形成一个新的对象 得到呃数值是在新的对象中,对于原对象是没有改变的
     *
     *
     */
    public void testUpdate() throws InterruptedException {

        LocalDateTime localDateTime = LocalDateTime.now();
        System.out.println(localDateTime);

        // 加减时间
        LocalDateTime localDateTime1 = localDateTime
                .plusYears(1)
                .plusMonths(1)
                .plusDays(1)
                .plusHours(-1).plusMinutes(1)
                .plusSeconds(1)
                .plusNanos(2);

        System.out.println(localDateTime1);



        // 对比 负数小 正数大
        System.out.println(localDateTime.compareTo(localDateTime1));
        // 当然也可以使用after before equal
        boolean after = localDateTime.isAfter(localDateTime1);
        boolean before = localDateTime.isBefore(localDateTime1);
        boolean equal = localDateTime.isEqual(localDateTime1);

        // 舍弃精度 保留到秒
        LocalDateTime localDateTime2 = localDateTime1.truncatedTo(ChronoUnit.SECONDS);
        System.out.println(localDateTime2);

        Instant start = Instant.now();

        TimeUnit.SECONDS.sleep(2);

        Instant end = Instant.now();
        // 得到间隔
        Duration between = Duration.between(start, end);
        // 显示
        System.out.println(between.getNano());
        System.out.println(between.getSeconds());
        System.out.println(between.toMillis());




    }

6.3 获取特定时间与格式化



    /***
     *  实际业务中 获取特殊的 年月分秒 还是比较少的
     *  获取年月分秒也是为了比较
     *  还有就是一些特殊的格式化操作
     *  同样使用LocalDateTime 演示
     *
     */
    public void testGet(){
        LocalDateTime localDateTime = LocalDateTime.now();

        int year = localDateTime.getYear();

        Month month = localDateTime.getMonth();
        int monthValue = localDateTime.getMonthValue();
        int dayOfYear = localDateTime.getDayOfYear();
        int dayOfMonth = localDateTime.getDayOfMonth();
        DayOfWeek dayOfWeek = localDateTime.getDayOfWeek();
        int dayOfWeekValue = dayOfWeek.getValue();
        int hour = localDateTime.getHour();
        int minute = localDateTime.getMinute();
        int second = localDateTime.getSecond();
        int nano = localDateTime.getNano();

        System.out.println(localDateTime);
        System.out.println(year);
        System.out.println(monthValue);
        System.out.println(dayOfYear);
        System.out.println(dayOfMonth);
        System.out.println(dayOfWeekValue);
        System.out.println(hour);
        System.out.println(minute);
        System.out.println(second);
        System.out.println(nano);

        // 格式化采用的是 格式化类
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SS");
        System.out.println(dateTimeFormatter.format(localDateTime));

        // 针对不同的实例 不存在的值会抛出 UnsupportedTemporalTypeException
        LocalDate localDate = LocalDate.now();
        System.out.println(dateTimeFormatter.format(localDate));

    }


6.4 特殊时间需求:时间校准



    /***
     *  时间校正器 , 比如特定时期,下个周末,下个发薪日期
     */
    public void testJusters(){
        LocalDate localDate  = LocalDate.now();
        TemporalAdjuster temporalAdjuster = TemporalAdjusters.firstDayOfMonth();

        LocalDate with = localDate.with(temporalAdjuster);
        System.out.println(localDate);
        System.out.println(with);
        // 下个周末
        System.out.println(localDate.with(TemporalAdjusters.next(DayOfWeek.SATURDAY)));
    }


7. 源码改造HashMap CurrentHashMap

HashMap 底层采用了 红黑树 而不是传统的全部链表,制定了链表元素大于8则树化,小于6则反树化,优化了hash碰撞下的查询性能

详细请看,我发布的 HashMap源码解读

CurrentHashMap 底层采用CAS(读)+synchronized(写) +数组+链表+红黑树 , 而不是原来segment的分段锁,segment继承自ReentrantLock . 锁的粒度也从原来对需要进行数据操作的Segment加锁,调整为对每个数组元素加锁(Node)。链表转化为红黑树则与HashMap一样为了查询性能

有机会补上对CurrentHashMap的源码解读

8. 重复注解与类型注解

8.1 重复注解

在之前使用重复注解都需要使用注解容器,如

// 权限注解
public @interface Auth {
     String role(); //具体权限
}

// 权限注解数组,可以有多个权限注解
public @interface Auths {
    Auth[] value(); 
}
 
 // 使用权限注解数组 包含多个权限
@Auths({@Auth(role="user"), @Auth(role="admin")})
public void del(){
}
 

现在重复注解,直接这样使用


// 权限注解 指定可重复注解,并且指明重复之后属于哪个注解类
@Repeatable(Auths.class)
public @interface Auth {
     String role(); //具体权限
}

// 权限注解数组,可以有多个权限注解
public @interface Auths {
    Auth[] value(); 
}
 // 使用权限注解数组 包含多个权限
@Auths({@Auth(role="user"), @Auth(role="admin")})
public void del(){
}

其中关键的就是 @Repeatable 注解 , 用于 重复注解上,在使用反射获取注解类时可以通过Auth 获取得到注解数组 , 也可以通过 Auths 获得注解数组

8.2 类型注解

首先你得知道如何写注解,一般创建注解类需要指明注解类的作用域@Target以及注解类的存在周期@Retention

  • @Target作用域决定了注解可以加在哪里
  • 存在周期决定了在哪个时期注解生效,或者说存在

这里直接贴出 作用域的枚举类型

/**
 * @author  Joshua Bloch
 * @since 1.5
 * @jls 9.6.4.1 @Target
 * @jls 4.1 The Kinds of Types and Values
 */
public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

可以看到 注解是1.5就存在了,而在最后面多了两个TYPE_PARAMETER TYPE_USE since 1.8 ,这就是类型注解的主角了

  • TYPE_PARAMETER:表示该注解能写在类型参数的声明语句中。 类型参数声明如: <T><T extends Person>
  • TYPE_USE:表示注解可以再任何用到类型的地方使用,比如允许在如下位置使用:
    1. 创建对象(用 new 关键字创建)
    2. 类型转换
    3. 使用 implements 实现接口
    4. 使用 throws 声明抛出异常

演示代码

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
@interface MyNotNull{ }

// 定义类时使用
// 3.在implements时使用
@MyNotNull
public class TypeAnnotationTest implements Serializable {
    // 4.在throws时使用
    public static void main(String [] args) throws @MyNotNull FileNotFoundException {
        Object  obj = "MyNotNull";
        // 2. 使用强制类型转换时使用
        String str = (@MyNotNull String) obj;
        // 1.创建对象时使用
        Object test= new (@MyNotNull) String("MyNotNull");   
    } 
    //  <T>  泛型中使用
    public void foo(List<@MyNotNull String> list) {
    }
}

总结

可以看到,重大变化还是蛮大的,所以在简历中写精通jdk8不要只知道lambda表达式和流,其他的点也是可以说出来的,同时在日常代码中也是可以用起来,让代码更简洁优雅

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:数字20 设计师:CSDN官方博客 返回首页

打赏作者

木秀林

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值