Java8 新特性:Lambda 表达式方法和构造器引用Stream API新时间与日期API注解

Posted 一宿君

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8 新特性:Lambda 表达式方法和构造器引用Stream API新时间与日期API注解相关的知识,希望对你有一定的参考价值。

Java8新特性:Lambda 表达式、方法和构造器引用、Stream API、新时间与日期API、注解

1、Java8新特性

Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本。 Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,它支持函数式编程,新的 javascript 引擎,新的日期 API,新的Stream API 等等。

1.1、主要的新特性:

Java8 新增了非常多的特性,最主要的有以下几个:

  • Lambda 表达式 − Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)。

  • 方法引用 − 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

  • 默认方法 − 默认方法就是一个在接口里面有了一个实现的方法。

  • 新工具 − 新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。

  • Stream API −新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。

  • Date Time API − 加强对日期与时间的处理。

  • Optional 类 − Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。

  • Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。

将新特性 着重点简化一下:

  • 速度更快,改革hashmap及其相关数据结构(HashMap的底层原理和线程安全的替代方案)(重要)
  • 代码更少,增加了新的语法Lambda表达式(重要)
  • 强大的Stream API(重要)
  • 便于并行
  • 最大化减少空指针异常 Optional(重要)

更多的新特性可以参阅官网:What’s New in JDK 8

1.2、编程风格

Java 8 希望有自己的编程风格,并与 Java 7 区别开,以下实例展示了 Java 7 和 Java 8 的编程格式:

运行结果:

我想从上述编码风格上一眼就可以看出,哪个更简洁更直观吧!

2、Lambda 表达式

2.1、Lambda 基础语法

基础语法入门推荐二哥的这篇:Lambda 表达式入门。

Java8中引入了一个新的操作符“->”,该操作符称为箭头操作符或者Lambda操作符,箭头操作符将Lambda表达式分成两部分:

  • 左侧:Lambda表达式的参数列表
  • 右侧:Lambda表达式中所要执行的功能,即Lambda体

语法格式1:无参数,无返回值

() -> System.out.println("Hello Lambda!");

语法格式2:有一个参数,无返回值

(x) -> System.out.println(x);

//若只有一个参数,小括号可以不写
x -> System.out.println(x)


语法格式三:有两个及以上的参数,有返回值,并且Lambda体中有多条语句时,参数列表需必须带小括号,Lambda体必须带

语法格式四:有两个及以上的参数,只需要返回值时,return和大括号都可以省略不写

(x,y) -> Integer.compare(x,y);

语法格式五:Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器通过上下文推断出,数据类型,即类型推断

		(Integer x,Integer y) -> Integer.compare(x,y);
		//可以省略不写:
		(x,y) -> Integer.compare(x,y);

		/**
         * 正常初始化赋值,可以上下文推断
         */
        String[] str = "aaa","bbb","ccc";

        /**
         * 赋值报错,无法根据上下文推断
         */
        String[] str2;
        str2 = "aaa","bbb","ccc";

        /**
         * new ArrayList<>()中的<>无需指定类型,上下文可以推断
         */
        List<String> stringList = new ArrayList<>();
        
		//定义一个方法,参数为Map集合	
		public void show(Map<String,Integer> map)
		/**
         * 引用的时候new HashMap<>()中<>无需指定类型,根据上下文可以指定
         */
        show(new HashMap<>());

2.2、Lambda 表达式需要“函数式接口”的支持

函数式接口:接口中只有一个抽象方法的接口,称为函数式接口。可以使用注解@FunctionalInterface修饰
可以检查是否是函数式接口。

该注解的源码中,有一句英文描述:

Note that instances of functional interfaces can be created with lambda expressions,
method references, or constructor references.

//大致意思如下:
通过 @FunctionalInterface 标记的接口可以使用 Lambda 表达式创建实例。

细心的伙子应该发现了上述JDK默认内置的两个接口 ComparatorConsumerRunnable,都是由注解@FunctionalInterface标注的,这就是标识这三个接口都是函数式接口。

@FunctionalInterface
public interface Comparator<T> 
    int compare(T o1,T o2);


@FunctionalInterface
public interface Consumer<T>
    void accept(T t);
    

@FunctionalInterface
public interface Runnable 
    public abstract void run();

当然我们自己也可以自定义函数式接口,只要由注解@FunctionalInterface标注了,就只能有一个抽象方法:

/**
 * 只能有一个抽象方法,有多个方法就会直接报错
 */
@FunctionalInterface
public interface People<T> 
    void chinese();

    //void american();

具体怎么表现呢?

原来我们创建一个线程并启动它是这样的:

new Thread(new Runnable() 
     @Override
     public void run() 
        System.out.println("一宿君!");
     
).start();

通过 Lambda 表达式只需要下面这样:

new Thread(() -> System.out.println("一宿君!")).start();

效果一模模一样!比起匿名内部类,Lambda 表达式不仅易于理解,更大大简化了必须编写的代码数量。

2.3、Lambda 表达式的作用域范围

示例:

	@Test
    public void test5()
        int limit = 10;
        Runnable r = () -> 
            //这一步会编译报错
            int limit = 5;
            for (int i = 0; i < limit; i++)
                System.out.println(i);
        ;
    


上面这段代码在编译的时候会提示错误:变量 limit 已经定义过了。

和匿名内部类一样,不要在 Lambda 表达式主体内对方法内的局部变量进行修改,否则编译也不会通过:Lambda 表达式中使用的变量必须是 final 的

这个问题发生的原因是因为 Java 规范(§4.12.4)中是这样规定的:

Any local variable, formal parameter, or exception parameter used but not declared in a lambda
expression must either be declared final or be effectively final (§4.12.4),
or a compile-time error occurs where the use is attempted.


//大致的意思就是说,
Lambda 表达式中要用到的,但又未在 Lambda 表达式中声明的变量,
必须声明为 final 或者是 effectively final,否则就会出现编译错误。

关于 finaleffectively final 的区别:

final int a;
a = 1; //初始化赋值不会报错
// a = 2;
// 由于 a 是 final 的,所以不能被重新赋值

int b;
b = 1;
// b 之后不再更改,b 就是 effectively final 常量

int c;
c = 1; // c 先被赋值为 1
c = 2; // 随后又被重新赋值为 2,此时c 就不是 effectively final

明白了 finaleffectively final 的区别后,我们了解到,如果把变量 limit 定义为 final,那就无法在 Lambda 表达式中修改变量的值。那有什么好的解决办法呢?既能让编译器不发出警告,又能修改变量的值。

有三个解决方案:

  • 1)把 limit 变量声明为 static

  • 2)把 limit 变量声明为 AtomicInteger

  • 3)使用数组

方案1:把 limit 变量声明为 static

要想把 limit 变量声明为 static,无论是静态方法,还是普通方法,都必须将 limit 变量放在方法外部,完整的代码示例如下所示。

	static int limit = 10;

    @Test
    public void test6()
        new Thread(() -> 
            limit = 5;
            for (int i = 0; i < limit; i++) 
                System.out.println(i);
            
        ).start();
    
//控制台
	0
	1
	2
	3
	4

方案2:把 limit 变量声明为 AtomicInteger

	/**
     * limit.set():赋值
     * limit.get():获取值
     */
    @Test
    public void test7()
        final AtomicInteger limit = new AtomicInteger(10);
        new Thread(() -> 
            limit.set(5);
            for (int i = 0; i < limit.get(); i++) 
                System.out.println(i);
            
        ).start();
    
//控制台
	0
	1
	2
	3
	4

方案3:使用数组

使用数组的方式略带一些欺骗的性质,在声明数组的时候设置为 final,但更改时,只是修改了数组中的一个元素,并不是整个数组。

	/**
     * 使用数组
     */
    @Test
    public void test8()
        final int [] limits = 10;
        new Thread(() -> 
            limits[0] = 5;
            for (int i = 0; i < limits[0]; i++) 
                System.out.println(i);
            
        ).start();
    
    
//控制台
	0
	1
	2
	3
	4

2.4、Lambda 和 this 关键字

Lambda表达式并不会引入新的作用域,这一点和匿名内部类是不同的,也就是说,Lambda表达式主体内使用的this关键字和其所在的类实例相同

看实例说话:

public class TestLambda2 
    public static void main(String[] args) 
        new TestLambda2().lambda();
    
    
    public void lambda()
        System.out.printf("this = %s%n", this);

        new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.printf("this = %s%n", this);
            
        ).start();


        new Thread(() -> System.out.printf("this = %s%n", this)).start();
    


解释一下:

  • printf(参数以逗号隔开)不是println(参数拼接)
  • %s代表当前位置输出字符串(占位符)
  • %n 代表换行符,也可以使用 \\n 代替,但 %n 是跨平台的。

上述案例包含三部分:

  • 第一部分:单独的this关键字

    //其中 this 为 main() 方法中通过 new 关键字创建的 TestLambda2 对象——new TestLambda2 ()。
    System.out.printf("this = %s%n", this);
    
  • 第二部分:匿名内部类中的 this 关键字

    //其中 this 为 lambda() 方法中通过 new 关键字创建的 Runnable 对象——new Runnable()...。
    new Thread(new Runnable() 
        @Override
        public void run() 
           System.out.printf("this = %s%n", this);
        
    ).start();
    
  • 第三部分:Lambda 表达式中的 this 关键字

    //其中 this 关键字和 第一部分中的相同,都是指new TestLambda2()
    new Thread(() -> System.out.printf("this = %s%n", this)).start();
    

看下最终的输出结果:

this = cn.wbs.lambda.TestLambda2@448139f0
this = cn.wbs.lambda.TestLambda2$1@6690e42a
this = cn.wbs.lambda.TestLambda2@448139f0

和预想的没毛病吧!

2.5、来几个小题练练手

1、调用Collections.sort()方法,通过定制排序比较两个Employee(先按年龄比,年龄相同按姓名比),使用Lambda 作为参数传递。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee 
    private int id;
    private String name;
    private int age;



	/**
     * 1. 调用Collections.sort()方法,通过定制排序比较两个Employee(先按年龄比,年龄相同按姓名比),使用Lambda 作为参数传递。
     */
    List<Employee> employees = Arrays.asList(
            new Employee(1,"张三",28),
            new Employee(2,"李四",8),
            new Employee(3,"王五",38),
            new Employee(4,"赵六",58),
            new Employee(5,"田七",38)
    );


    @Test
    public void test1()
        System.out.println("排序之前:" + employees.toString());

        Collections.sort(employees,(e1,e2) ->
            if(e1.getAge() == e2.getAge())
                return e1.getName().compareTo(e2.getName());
            else 
                return Integer.compare(e1.getAge(),e2.getAge());
            
        );

        System.out.println("排序之后:" + employees.toString());

    

//输出结果
排序之前:[Employee(id=1, name=张三, age=28), Employee(id=2, name=李四, age=8), Employee(id=3, name=王五, age=38), Employee(id=4, name=赵六, age=58), Employee(id=5, name=田七, age=38)]
排序之后:[Employee(id=2, name=李四, age=8), Employee(id=1, name=张三, age=28), Employee(id=3, name=王五, age=38), Employee(id=5, name=田七, age=38), Employee(id=4, name=赵六, age=58)]

2、①声明函数式接口,接口中声明抽象方法,public String getValue(String str);
②声明类TestLambda ,类中编写方法使用接口作为参数,将一个字符串转换成大写,并作为方法的返回值;
③再将一个字符串的第﹖个和第4个索引位置进行截取子串。

@FunctionalInterface
public interface MyLambdaFuntion 
    public String getValue(String str);


	/**
     * 2、①声明函数式接口,接口中声明抽象方法,public String getValue(String str);
     * 	 ②编写一个方法使用上述函数式接口作为参数,将一个字符串转换成大写,并作为方法的返回值;
     * 	 ③再将一个字符串的第﹖个和第4个索引位置进行截取子串。
     */
    public String opration(String str, MyLambdaFuntion mlf)
        return mlf.getValue(str);
    

    @Test
    public void test2()
        System.out.println(opration("abcdef", x -> x.toUpperCase()));
        System.out.println(opration("abcdef", x -> x.substring(2,5)));
    

//输出结果:
ABCDEF
cde

3、①声明一个带两个泛型的函数式接口,泛型类型为<T,R> T为参数类型,R为返回值类型;
②接口中声明对应抽象方法;
③声明一个方法,使用上述函数式接口作为参数,计算两个 long 型参数的和;
④再计算两个 long 型参数的乘积。

@FunctionalInterface
public interface MyDoubleFuntion<T,R> 
    public R getValue(T t1,T t2);



	/**
     * 3、①声明一个带两个泛型的函数式接口,泛型类型为<

以上是关于Java8 新特性:Lambda 表达式方法和构造器引用Stream API新时间与日期API注解的主要内容,如果未能解决你的问题,请参考以下文章

Java8 新特性:Lambda 表达式方法和构造器引用Stream API新时间与日期API注解

java8新特性→方法和构造函数引用:替代Lambda表达式

Java8新特性

java8新特性

Java8新特性:Lambda表达式函数式接口以及方法引用

java8新特性之方法引用和构造器引用