24.Java 1.8新特性

目录


Java专栏目录(点击进入…)



接口的默认方法

Java 8允许给接口添加一个非抽象的方法实现,只需要使用default关键字即可,这个特征又叫做扩展方法

interface Formula {

	double calculate(int a);

	default double sqrt(int a) {
		return Math.sqrt(a);
	}
	
}

Formula接口在拥有calculate方法之外同时还定义了sqrt方法,实现了Formula接口的子类只需要实现一个calculate方法,默认方法sqrt将在子类上可以直接使用


Lambda表达式

从Java 8出现以来lambda是最重要的特性之一,它可以用简洁流畅的代码完成一个功能。 很长一段时间Java被吐槽是冗余和缺乏函数式编程能力的语言,随着函数式编程的流行Java 8种也引入了这种编程风格;可以取代大部分的匿名内部类,写出更优雅的Java代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构

JDK也提供了大量的内置函数式接口供使用,使得Lambda表达式的运用更加方便、高效

什么是Lambda?

lambda表达式是一段可以传递的代码,核心思想是将面向对象中的传递数据变成传递行为

为什么要使用Lambda表达式?

“lambda表达式”是一段可以传递的代码,因此它可以被执行一次或多次。在学习语法(甚至包括一些奇怪的术语)之前,先回顾一下之前在Java中一直使用的相似的代码块。

当要在另一个独立线程中执行一些逻辑时,通常会将代码放在一个实现Runnable接口的类的run方法中

class Worker implements Runnable {

	public void run(){
		for (int i=0;i<1000;i++)
			doWork();
		}
	}

	// ....
}

当希望执行这段代码时,会构造一个worker类的实例。然后将该实例提交到一个线程池中,或者简单点,直接启动一个新的线程:

Worker w = new Worker();
new Thread(w).start();

这段代码的关键在于,run方法中包含了你希望在另一个线程中执行的代码。考虑一下用一个自定义的比较器来进行排序。如果希望按照字符串的长度而不是默认的字典顺序来排序,那么可以将一个Comparator对象传递给sort方法:

class LengthComparator implements Comparator<String> {
	public int compare(String first,String second){
		return Integer.compare(first.length(),second.length());
	}
}
Arrays.sort(strings,new LengthComparator());

sort方法会一直调用compare方法,对顺序不对的元素进行重新排序,直到数组完全有序为止。你给sort方法传递了一段需要比较元素的代码片段,而该代码会被整合到排序逻辑中,而你可能并不关心如何在那里实现。

按钮回调是另外一个会延迟执行的例子
将回调操作放到一个实现了监听器接口的类的某个方法中,然后构造一个实例,并将实例注册到按钮上。在这种情况下,许多开发人员都会使用“匿名类的匿名实例”的方法:

button.setOnAction(new EventHandler<ActionEvent>() {
	public void handle(ActionEvent event){
		System.out.println("Thanks for clicking!");
	}
});

这里的关键是代码处于handle方法中。该代码会在按钮被点击时执行。


Lambda的作用?

所有的Lambda的类型都是一个接口,而Lambda表达式本身,也就是“那段代码”,需要是这个接口的实现

Lambda表达式本身就是一个接口的实现


Lambda表达式的语法

以排序为例。传递代码来检查某个字符串的长度是否小于另一个字符串的长度,如下所示:

Integer.compare(first.length(),second.length())

first和second是什么呢?它们都是字符串。Java是一个强类型的语言,因此我们必须同时指定类型。

(String first,String second) -> Integer.compare(first.length(),second.length())

这就是你见到的第一个“lambda表达式”。这个表达式不仅是一个简单的代码块,还指定了必须传递给代码的所有变量。

为什么要叫这个名字呢?许多年前,在计算机出现之前,有位名叫Alonzo Church的逻辑学家,他想要证明什么样的数学函数是可以有效计算的。(奇怪的是,当时已经存在了许多已知的函数,但是没有人知道怎样去计算它们的值。)他使用希腊字母的lambda(λ)来标记参数。如果他懂Java API的话,他应该会写下如下代码:

λfirst.second.Integer.compare(first.length(),second.length())

Java中lambda表达式的格式:参数、箭头->,以及一个表达式。如果负责计算的代码无法用一个表达式表示,那么可以用编写方法的方式来编写:即用{}包裹代码并明确使用return语句,例如:

(string first, string second) -> {
	if(first.length()<second.length())
		return -1;
	else if(first.length()>second.length())
		return 1;
	else
		return 0;
}

如果lambda表达式没有参数,你仍可以提供一对空的小括号,如同不含参数的方法那样:

() -> { 
	for (int i=0;i<1000;i++)	doWork();
}

如果一个lambda表达式的参数类型是可以被推导的,那么就可以省略它们的类型,例如:

Comparator<String> comp =
	// 同(String first,String second)一样
	(first,second) -> Integer.compare(first.length(),second.length());

这里,编译器会推导出first和second必须是字符串,因为lambda表达式被赋给了一个字符串比较器(将会在下一节详细讲解该赋值过程)。

如果某个方法只含有一个参数,并且该参数的类型可以被推导出来,你甚至可以省略小括号:

EventHandler<ActionEvent>listener = 
	event -> System.out.println("Thanks for clicking!"); 
	// 无须(event)->或(ActionEvent event)->
	(final String name)->...
	(@NonNull String na    e)->...

注意:可以像对待方法参数一样向lambda表达式的参数添加注解或者final修饰符。

永远不需要为一个lambda表达式执行返回类型,它总是会从上下文中被推导出来。例如,表达式:

(String first,String second)->Integer.compare(first.length(),second.length())

可以被使用在期望结果类型为int的上下文中。

注意:在lambda表达式中,只在某些分支中返回值(其他分支没有返回值)是不合法的。例如,(int x)->{ if(x>=0)return 1; }是不合法的。


Lambda表达式语法

()  ->  {}
语法字符描述
()用来描述参数列表
->为 lambda运算符,读作(goes to)
{}用来描述方法体

使用Lambda对接口的要求

虽然使用Lambda表达式可以对某些接口进行简单的实现,但并不是所有的接口都可以使用Lambda表达式来实现。Lambda规定接口中只能有一个需要被实现的方法,也就是只有一个抽象方法;不是规定接口中只能有一个方法,被default修饰的方法会有默认实现,不是必须被实现的方法,所以不影响Lambda表达式的使用


函数式接口

Lambda表达式是如何在Java的类型系统中表示的呢?

每一个Lambda表达式都对应一个类型,通常是接口类型。而“函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的Lambda表达式都会被匹配到这个抽象方法。因为默认方法不算抽象方法,所以也可以给函数式接口添加默认方法。

@FunctionalInterface
可以将Lambda表达式当作任意只包含一个抽象方法的接口类型,确保接口一定达到这个要求,只需要给接口添加@FunctionalInterface注解,@FunctionalInterface修饰函数式接口,要求接口中的抽象方法只有一个,编译器如果发现标注了这个注解的接口有多于一个抽象方法的时候会报错的

@FunctionalInterface
interface Converter<F, T> {
	T convert(F from);
}

Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted);    // 123

需要注意如果@FunctionalInterface如果没有指定,上面的代码也是对的,因为它只有一个需要实现的抽象方法

Lambda作用域

在Lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量

Lambda访问局部变量

可以直接在Lambda表达式中访问外层的局部变量

final int num = 1;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2);     // 3

但是和匿名对象不同的是,这里的变量num可以不用声明为final,该代码同样正确

int num = 1;
Converter<Integer, String> stringConverter = (from) -> String.valueOf(from + num);
stringConverter.convert(2);     // 3

不过这里的num必须不可被后面的代码修改(即隐性的具有final的语义),例如下面的就无法编译:

int num = 1;
Converter<Integer, String> stringConverter = (from) -> {
	String.valueOf(from + num);
	num = 3;
};

在lambda表达式中试图修改num同样是不允许的

Lambda访问对象字段与静态变量

和本地变量不同的是,Lambda对于实例字段、静态变量是即可读又可写。该行为和匿名对象是一致的

class Lambda4 {

    static int outerStaticNum;
    int outerNum;

    void testScopes() {
        Converter<Integer, String> stringConverter1 = (from) -> {
            outerNum = 23;
            return String.valueOf(from);
        };

        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = 72;
            return String.valueOf(from);
		};
    }
}

无法访问接口的默认方法

Lambda表达式中是无法访问到接口的默认方法,将无法编译

JDK 1.8 API包含了很多内建的函数式接口,在老Java中常用到的比如Comparator或者Runnable接口,这些接口都增加了@FunctionalInterface注解以便能用在Lambda上

匿名内部类

Runnable r = new Runnable() {
    @Override
    public void run() {  
    	// 实现函数化接口的抽象方法run
        System.out.println("do something.");      
    }
};

与在实现类中实现抽象方法相似,实现后可以直接runnable.run()调用函数化接口->只有一个抽象方法

Lambda表达式实现

Runnable runnable = () -> {   
	// run方法的表达式 () 是方法参数, 没有就空着
 	System.out.println("do something.");  
};

匿名内部类

匿名内部类也就是没有名字的内部类,正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写

使用匿名内部类还有个前提条件:必须继承一个父类实现一个接口

不具有抽象和静态属性。并且不能派生出子类;匿名内部类只能访问外部的静态变量和final修饰的常量

对于实例字段、静态变量是即可读又可写|实例字段:必须先由其所在的类构造一个实例出来。对象.实例字段
作用:内部类通过该访问路径可以进行内部类内部和外部的数据交互,一般与final结合使用比较多

new Runnable(){}其实不是实例化Runnable接口来的,实际上一种内部类的一种简写
①首先构造了一个“implements Runnable”的无名local内部类(方法内的内部类)
②然后构造了这个无名local内部类的一个实例
③然后用Runnable来表示这个无名local内部类的type(OO多态)

package com.cph.Thread;

public class Text2 {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
		Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println("i am new Runnable(){xxx}");
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }
    
}

上面这段代码编译后你会看到其实是编译了两个类来的
在这里插入图片描述

其中Text2$1就是无名local内部内类,这个也就很好地解释了为什么在main()方法中new Runnable(){xxx}里面的调用main()方法中的变量的时候要用final关键字来修饰。 上面只是借助new Runnable(){xxx}特例来说明接口在方法中new的机制,对其他接口同理可得

匿名内部类(Anonymous Inner Class),在创建实例的同时给出类的定义,所有这些在一个表达式中完成


方法与构造函数引用

Java 8允许使用“ :: ”关键字来传递方法或者构造函数引用

如何引用一个静态方法,也可以引用一个对象的方法

Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted);   // 123

converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"

构造函数是如何使用“ :: ”关键字来引用

首先定义一个包含多个构造函数的简单类

class Person {

    String firstName;
    String lastName;
    
    Person() {}
    
    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
}

接下来指定一个用来创建Person对象的对象工厂接口

interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}

这里使用构造函数引用来将他们关联起来,而不是实现一个完整的工厂

PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

只需要使用Person::new来获取Person类构造函数的引用,Java编译器会自动根据PersonFactory.create方法的签名来选择合适的构造函数

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

未禾

您的支持是我最宝贵的财富!

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

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

打赏作者

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

抵扣说明:

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

余额充值