java8新特性之lambda表达式
1.什么是lambda表达式?为什么用它?
lambda表达式又称闭包。它是推动java8发布的最重要新特性。lambda允许把函数作为方法的参数(函数作为参数传递进方法中)。使用lambda可以使代码更加简洁。
例如:我们要创建一个线程输出一句话
不用lambda:
我们需要写一个类实现Runnable接口,然后实现他的run方法
//实现Runnable接口的类
public class RunTemp implements Runnable{
@Override
public void run() {
System.out.println("我是实现的Runnable方法");
}
}
//使用
RunTemp runTemp = new RunTemp();
new Thread(runTemp).start();
结果:
使用lambda:
new Thread(()-> System.out.println("lambdaYYDS")).start();
结果:
一对比我们就可以看出,lambda为我们节省了好多代码。但实现的功能却是相同的。
2. 为什么Java需要lambda表达式?
lambda表达式为java提供了缺失的函数式编程特点。使我们能将函数当作一等公民来看待。在一个支持函数的语言中,lambda表达式的类型应该是函数,但是在Java中它是一种对象,它必须依附于一种特殊的对象类型:函数式接口(functional interface)
3. lambda表达式的语法
lambda表达式在Java语言中引入了一个新的操作符“->”,该操作符被称为lambda操作符或者箭头操作符。它将lambda表达式分为了两部分:
左侧:lambda所需参数
右侧:lambda要执行的操作。
(type param1,type param2,...) -> {body}
(param1,param2,...) -> {body}
//例如:
(String param1,Interge param2,...) -> {return param1+param2;}
以下是lambda表达式的重要特性:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但无参数或者多参数需要定义圆括号
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指明表达式返回了一个数值。
举例:
- 无参,无返回值,lambda体只有一句话:
public class Test1 {
public static void main(String[] args) {
Runnable runnable = ()->{ System.out.println("无参,无返回值,一句话"); };
runnable.run();
}
}
//一语句可以省略大括号:
public static void main(String[] args) {
Runnable runnable = ()-> System.out.println("无参,无返回值,一句话。可以省略大括号");
runnable.run();
}
- 一参,无返回值,lambda体只有一句话:
public static void main(String[] args) {
Consumer<String> consumer = (t)->{System.out.println("一参:"+t+",无返回值,一句话"); };
consumer.accept("我是参数");
}
//一参可以省略小括号:
public static void main(String[] args) {
Consumer<String> consumer = t->{ System.out.println("一参:"+t+",无返回值,一句话。一参可以省略小括号"); };
consumer.accept("我是参数");
}
- 两个参数,一条语句,有返回值
public static void main(String[] args) {
Comparator<Integer> comparable = (a, b)->{return Integer.compare(a,b);};
System.out.println(comparable.compare(3,4));
}
/**
* 一条语句有返回值时,return和大括号都可不写
**/
public static void main(String[] args) {
Comparator<Integer> comparable = (a, b)-> Integer.compare(a,b);
System.out.println(comparable.compare(3,4));
}
- 两个参数,多条语句,有返回值
public static void main(String[] args) {
Comparator<Integer> comparable = (a, b)->{
System.out.println("我是另一条语句");
return Integer.compare(a,b);
};
System.out.println(comparable.compare(3,4));
}
4.函数式接口
4.1 什么是函数式接口
只包含一个抽象方法的接口就是函数式接口。我们可以通过lambda表达式来创建该接口的实现对象。我们可以在任意函数式接口上使用@Functionallnterface注解,这样做可以用于检测它是否是一个函数式接口,同时javadoc也会包含一条声明,说明这个接口是一个函数式接口。
4.2 自定义函数式接口
/**
* 自定义函数式接口
* @author wangdawei
*/
@FunctionalInterface
public interface WorkerInterface {
/**
* 一个抽象方法
*/
public void doSomeWork();
}
//使用
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.show(()-> System.out.println("我是最简单的lambda表达式"),"你号");
}
//自定义一个方法,将函数式接口作为参数
private void show(WorkerInterface worker,String str) {
System.out.println(str);
worker.doSomeWork();
}
}
4.3 内置函数式接口
四大内置函数式接口:
使用示例:
1 Consumer:
//使用
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.markMoney(10,(x)-> System.out.println("今天花费了"+x+"元钱"));
}
/**
* Consumer<T>
*/
private void markMoney(Integer money, Consumer<Integer> consumer){
consumer.accept(money);
}
}
//结果:今天花费了10元钱
2 Supplier:
public class Test {
public static void main(String[] args) {
Test test = new Test();
List<Integer> list = test.addNumInList(10,()->(int)(Math.random()*100));
list.forEach(t-> System.out.print(t+","));
}
/**
* Supplier<T>
*/
private List<Integer> addNumInList(int size, Supplier<Integer> supplier){
List<Integer> list=new ArrayList<>();
for (int i = 0; i < size; i++) {
list.add(supplier.get());
}
return list;
}
}
//结果:92,40,77,48,95,86,40,51,52,27,
3 Function<T,R>:
public class Test {
public static void main(String[] args) {
Test test = new Test();
Function<Integer,String > function = (b)->{
System.out.println("Function");
int data = b*b+b;
return "b*b+b的结果是:"+data;
};
function.apply(11);
}
}
//结果:
//Function
//b*b+b的结果是:132
4 Predicate<T>:
//判断数字大小
public class Test {
public static void main(String[] args) {
int data = 11;
Predicate<Integer> predicate = (a)->{
if (a>10){
return true;
}
return false;
};
if (predicate.test(data)){
System.out.println("data大于10");
}else{
System.out.println("data小于等于10");
}
}
}
//结果:data大于10
其它接口:
5. 方法引用
当要传递给Lambda体的操作,已经有实现的方法了,就可以使用方法引用!(实现抽象方法的参数列表,必须与方法引用的参数列表一致,方法的返回值也必须一致,即方法的签名一致)。可以理解为方法引用是Lambda表达式的另外一种表现形式。
语法:使用操作符"::"将对象或类与方法名分隔开。
使用方法:
- 对象::实例方法名
- 类::静态方法名
- 类::实例方法名
- 类::new
- type[]::new
5.1. 对象::实例方法名
public class Test {
public static void main(String[] args) {
PrintStream stream = System.out;
Consumer<String> consumer = stream::println;
consumer.accept("对象::实例方法名");
}
}
//结果:对象::实例方法名
5.2. 类::静态方法名
public class Test {
public static void main(String[] args) {
PrintStream stream = System.out;
Consumer<String> consumer = Test::show;
consumer.accept("类::静态方法名");
}
/**
* 方法引用
*/
static void show(String s){
System.out.println(s);
}
}
//结果:类::静态方法名
5.3. 类::实例方法名
public class Test {
public static void main(String[] args) {
BiPredicate<String,String> biPredicate = String::equals;
biPredicate.test("t","t");
}
}
注意:此方式有要求。
第一点:接口方法的参数比引用方法的参数多一个
第二点:接口方法的第一个参数恰巧是调用引用方法的对象(其引用方法所在类或其父类的实例)
以上面的例子为例:
BiPredicate<String,String> biPredicate = String::equals;
//首先,BigPredicate接口中的test方法规定要传两个参数:第一个参数规定第一个参数必须是String类型的实例,第二个参数是String类型参数。
biPredicate.test("t","t");
//第一个参数”t“是String类型的实例,流程可以理解为:"第一个参数".(test/equals)(第二个参数)
//这也是为什么第一个参数必须是String类型的实例的原因。如果不是那就无法调用.equals方法。
例二:
public class Test {
public static void main(String[] args) {
BiPredicate<MyFinalClass,String> biPredicate = MyFinalClass::showString;
biPredicate.test(new MyFinalClass(),"例二结果输出");
}
}
public class MyFinalClass{
public Boolean showString(Object s){
System.out.println("给你展示:"+s);
return true;
}
}
//BiPredicate<MyFinalClass,String> 规定:第一个参数必须是“MyFinalClass”类型的实例。第二个参数是String类型的实例。
5.4. 类::new
public class Test {
public static void main(String[] args) {
//无参
Supplier<Entity> supplier = Entity::new;
supplier.get();
//一参
Function<String,Entity> function = Entity::new;
System.out.println(function.apply("王大伟").getName());
//两参
BiFunction<String,String,Entity> biPredicate = Entity::new;
Entity entity = biPredicate.apply("1234232","王大伟");
System.out.println("id:"+entity.getId()+";name:"+entity.getName());
//三参
NewEntity newEntity = Entity::new;
Entity entity1 = newEntity.newEntity("372929","王大伟",20);
System.out.println("id:"+entity.getId()+";name:"+entity.getName()+";age:"+entity1.getAge());
}
}
/**
* 自定义一个函数式接口
**/
@FunctionalInterface
public interface NewEntity {
/**
* 三个参数的初始化
* @param id
* @param name
* @param age
* @return
*/
public Entity newEntity(String id,String name,Integer age);
}
public class Entity {
private String id;
private String name;
private Integer age;
public Entity() {
}
public Entity(String name) {
this.name = name;
}
public Entity(String id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
public Entity(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
5.5 数组引用
数组引用格式:type[]:new
示例:
@Test
public void test02(){
Function<Integer,String[]> function=String[]::new;
String[] apply = function.apply(10);
System.out.println(apply.length);//结果:10
}
6 lambda表达式的作用域
Lambda表达式可以看作式匿名内部类实例化的对象,Lambda表达式对变量的访问限制和匿名内部类一样,因此Lambda表达式可以访问局部变量,局部引用,静态变量,实例变量。
6.1 访问局部变量
在Lambda表达式中规定只能引用标记了final的外层局部变量。我们不能在lambda内部修改定义在域外的局部变量,否则会编译错误。
例如:
public class Test {
public static void main(String[] args) {
//声明局部变量
int t = 0;
WorkerInterface workerInterface = (a,b)->{
System.out.println("a:"+a+"b:"+b);
//修改局部变量
t = t+1;
System.out.println("t:"+t);
};
workerInterface.doSomeWork(3,4);
}
}
/**
* 自定义函数式接口
* @author wangdawei
*/
@FunctionalInterface
public interface WorkerInterface {
/**
* 一个抽象方法
*/
public void doSomeWork(int a,int b);
}
特殊情况下,局部变量也可以不用声明为final,但是必须保证它不可被后面的代码修改,(即隐形的具有final语义)
例:以上面代码举例:
public class Test {
public static void main(String[] args) {
//声明局部变量
//final int t = 0;
int t = 0;
WorkerInterface workerInterface = (a,b)->{
System.out.println("a:"+a+"b:"+b);
//修改局部变量
int c = t+1;
System.out.println("c:"+c);
};
workerInterface.doSomeWork(3,4);
System.out.println("我这里可不敢改被lambda用过的局部变量");
}
}
上面代码就可以成功了。
6.2 访问局部引用,静态变量,实例变量
Lambda表达式不限制访问局部引用变量,静态变量,实例变量。
例如:
//访问局部引用变量。
public class Test {
public static void main(String[] args) {
int t = 0;
//创建一个局部引用变量
List<String> list = new ArrayList<>();
list.add("增加了一个数据。");
WorkerInterface workerInterface = (a,b)->{
//获取了一个局部引用变量,
System.out.println(list.get(0));
//我往里面加一个数据
list.add("我往里面加一个数据");
};
workerInterface.doSomeWork(3,4);
list.add("我再往里面加一个数据,也不会报错");
}
}
//访问静态变量
public class Test {
static String staticStr = "静态变量";
public static void main(String[] args) {
WorkerInterface staticWorker = (a,b)->{
System.out.println("看好了,我要改静态变量:"+staticStr);
staticStr = staticStr+";;我改了,看见了吗?";
System.out.println(staticStr);
};
staticWorker.doSomeWork(1,1);
}
}
//结果:看好了,我要改静态变量:静态变量
//静态变量;;我改了,看见了吗?
//访问实例变量
public class Test {
static String staticStr = "静态变量";
String instanceStr = "实例变量";
public static void main(String[] args) {
Test test = new Test();
WorkerInterface instanceWorker = (a,b)->{
System.out.println("看我改实例变量:"+test.instanceStr);
test.instanceStr = test.instanceStr+",我改了";
System.out.println(test.instanceStr);
};
}
}
//结果:
//看我改实例变量:实例变量
//实例变量,我改了
6.3 Lambda表达式访问局部变量做限制的原因。
因为实例变量存在堆中,而局部变量是在栈上分配,存在于虚拟机栈的局部变量表中,Lambda表达式(匿名类)有可能会在另一个线程中执行。如果在线程中要直接访问一个局部变量,可能线程执行时该局部变量已经被销毁了,而final类型的局部变量在Lambda表达式(匿名类)中其实是局部变量的拷贝。
基于上述,对于引用类型的局部变量,因为Java是值传递,又因为引用类型的指向内容是保存在堆中,是线程共享的,因此Lambda表达式中可以修改引用类型的局部变量的内容,而不能修改该变量的引用。
对于基本数据类型的变量,在Lambda表达式中只是获取到该变量的副本,且局部变量是线程私有的。因此无法知道其它线程对该变量的修改。如果该变量不做final修饰,会造成数据不同步的问题。
但是实例变量,静态变量不做限制,因为他两个保存在堆中(Java8之后),而堆是线程共享的。在Lambda表达式内部是可以知道实例变量,静态变量的变化。
参考:
Lambda表达式超详细总结
【Java 8系列】Lambda 表达式,一看就废
Lambda表达式使用局部变量的限制
lambda表达式——类名::实例方法