【Java】3.0 Java中lambda表达式(上)
【Java】4.0 Java中lambda表达式(下)
7.0 lambda表达式与异常
lambda表达式也可以抛出异常,但是异常必须与函数式接口的抽象方法中的throws子句中列出的异常兼容。如下例所示。
package com.edpeng.game1;
interface DoubleNumericArrayFunc {
// 接口也必须定义抛出自定义异常EmptyArrayException
double func(double[] n) throws EmptyArrayException;
}
@SuppressWarnings("serial")
class EmptyArrayException extends Exception {
EmptyArrayException() {
super("Array Empty");
}
}
public class test1 {
public static void main(String[] args)
throws EmptyArrayException {
double[] values = { 1.0, 2.0, 3.0, 4.0 };
//计算一个double数组的值,但当数组为0时
//抛出自定义异常EmptyArrayException
DoubleNumericArrayFunc average = (n) -> {
double sum = 0;
if (n.length == 0)
throw new EmptyArrayException();
for (int i = 0; i < n.length; i++) {
sum += n[i];
}
return sum / n.length;
};
System.out.println("数组的平均数为: \n"
+ average.func(values));
// 这将导致引发异常
System.out.println("数组的平均数为: \n"
+ average.func(new double[0]));
}
}
执行结果如下:
![](https://img.haomeiwen.com/i16102290/8a6430b8fa126c92.png)
这里需要注意的是,lambda表达式中的参数依旧是“(n)
”,代表的是一个double[ ]数组,而不是“n[ ]
”,lambda表达式能从目标上下文推出是double[ ],所以“(n)
”的类型也是double[ ],没必要写成“n[ ]
”(这么做也不合法)。
但是可以显示的写成(声明)为“double[] n
”,这么做是合法的,但是在本例中这么做不会有什么好处。
8.0 lambda表达式与变量捕获
先看一个简单的例子。
package com.edpeng.game1;
interface MyFunc {
int func(int n);
}
public class test1 {
public static void main(String[] args) {
int num = 10;
MyFunc mylambda = (n) -> {
int v = num + n;
num++;
return v;
};
}
}
编译都无法通过,代码“num++;
”提示报错。
![](https://img.haomeiwen.com/i16102290/cd375ab5adeca9a7.png)
我们可以看到,提示“
在封闭范围内定义的局部变量num必须是fiinal或有效的fiinal。
”
- 可见,lambda表达式可以访问其外层作用域内定义的变量。比如上例可以直接调用
num
。lambda表达式也可以显式或隐式地访问this变量。 - 当,lambda表达式访问其外层作用域内定义的变量时,会产生一种特殊情况,这种情况就叫“
变量捕获
”。此时,lambda表达式智能使用实质上的final局部变量。实质上final的变量是指第一次赋值之后,值便不再发生变化的变量。没必要将这种变量声明为final,不过可以这么做,也不会错。(外层作用域的this参数自动是实质上的final的变量,lambda表达式没有自己的this参数)。所以如下这样也是无法编译运行的:
public static void main(String[] args) {
int num = 10;
num = 52;
MyFunc mylambda = (n) -> {
int v = num + n;
// num++;
return v;
};
}
- lambda表达式不能修改外层作用域内的局部变量, 修改局部变量会移除其实质上的final状态,从而使得捕获该变量变得不合法。lambda表达式内部定义的变量可以随意修改。
9.0 方法引用
有一个重要的特性与lambda表达式相关,叫方法引用
。方法引用提供一种引用而不执行方法的方式。往往两者伴随使用。
9.1 静态方法引用
创建静态方法引用,需要用到以下一般语法:
ClassName::methodName
类名和方法名之间使用双冒号隔开。“::
”是JDK8新增的一个分隔符,专门用于此目的。 如果函数式接口的实现恰好是通过调用一个静态方法来实现,那么就可以使用静态方法引用。
具体演示代码如下:
package com.edpeng.game1;
interface StringFunc {
String func(String n);
}
public class test1 {
static String strReverse(String str) {
String result = "";
int i;
for (i = str.length() - 1; i >= 0; i--) {
result += str.charAt(i);
}
return result;
}
}
class MethodRefDemo {
static String stringOp(StringFunc sf, String s) {
return sf.func(s);
}
public static void main(String[] args) {
String inStr = "lambda表达式加入Java";
String outStr;
outStr = stringOp(test1::strReverse, inStr);
System.out.println("原始字符串为: \n" + inStr);
System.out.println("倒转后的字符串为: \n" + outStr);
}
}
执行结果为:
![](https://img.haomeiwen.com/i16102290/a5bdf779f3a4ec7d.png)
可能有人不太明白静态方法引用这个概念。如下这张表可以简单了解一下:
类型 | 语法 | 对应的lambda表达式 |
---|---|---|
静态方法引用 | 类名::staticMethod | (args) -> 类名.staticMethod(args) |
实例方法引用 | inst::instMethod | (args) -> inst.instMethod(args) |
对象方法引用 | 类名::instMethod | (inst,args) ->类名.instMethod(args) |
构造方法引用 | 类名::new | (args) -> new 类名(args) |
这里只说静态方法,代码演示如下:
public class StaticExample {
/**
* 无参数有返回值
* @return
*/
public static String put() {
System.out.println("put method");
return "hello";
}
/**
* 有参数无返回值
* @param size
*/
public static void con(String size) {
System.out.println("size: " + size);
}
/**
* 带有一个参数一个返回值
* @param str
* @return
*/
public static String toUpperCase(String str) {
return str.toUpperCase();
}
/**
* 带有两个参数一个返回值
* @param s1
* @param s2
* @return
*/
public static Integer len(String s1, String s2) {
return s1.length() + s2.length();
}
public static void main(String[] args) {
/**
* 无参数有返回值
*/
//lambda表达式的写法
Supplier<String> stringSupplier= ()-> StaticExample.put();
System.out.println(stringSupplier.get());
//方法引用
Supplier<String> stringSupplier2 = StaticExample::put;
System.out.println(stringSupplier.get());
/**
* 有参数无返回值
*/
//lambda表达式的写法
Consumer<String> consumer = (x)-> StaticExample.con(x);
consumer.accept("100");
//方法引用
Consumer<String> consumer2= StaticExample::con;
consumer.accept("100");
/**
* 有1个参数有一个返回值
*/
//lambda表达式的写法
Function<String,String> function = (x)-> StaticExample.toUpperCase(x);
System.out.println(function.apply("dada"));
//方法引用的写法
Function<String,String> function1= StaticExample::toUpperCase;
System.out.println(function1.apply("dada"));
/**
* 有两个参数一个返回值
*/
//lambda表达式的写法
BiFunction<String,String,Integer> biFunction= (s1,s2)-> StaticExample.len(s1,s2);
Integer apply = biFunction.apply("dada", "dada");
System.out.println(apply);
//方法引用的写法
BiFunction<String,String,Integer> biFunction2= StaticExample::len;
Integer apply2 = biFunction2.apply("dada", "dada");
System.out.println(apply2);
}
}
这个演示案例抄别个的,虽然啰嗦,但是很完整,思路简单,很好理解。
9.2 实例方法的方法引用
实例方法于9.1中有格式写法的简单介绍。格式为:
objRef::mwthodName
可以看到,很静态方法语法特别类似,不过这里使用对象引用而不是类名。演示代码如下:
package com.edpeng.game1;
interface StringFunc {
String func(String n);
}
class MyStringOps{
String strReverse(String str) {
String resultStr = "";
int i;
for(i= str.length()-1;i>=0;i--) {
resultStr +=str.charAt(i);
}
return resultStr;
}
}
public class test1 {
static String stringOp(StringFunc sf,String s) {
return sf.func(s);
}
public static void main(String[] args) {
String inStr ="lambda表达式加入Java";
String outStr;
MyStringOps stringOps = new MyStringOps();
outStr= stringOp(stringOps::strReverse, inStr);
System.out.println("原始字符串为: \n"+inStr);
System.out.println("倒转后的字符串为: \n"+outStr);
}
}
执行结果为:
![](https://img.haomeiwen.com/i16102290/75943c6848c080b6.png)
上述例子应该不算难理解,下面通过另一个案例代码演示,进一步深入:
package com.edpeng.game1;
interface MyFunc<T> {
boolean func(T v1, T v2);
}
class HighTemp {
private int hTemp;
HighTemp(int ht) {
hTemp = ht;
}
//判断是否两个温度相等
boolean sameTemp(HighTemp ht2) {
return hTemp == ht2.hTemp;
}
//判断是否参数温度是否比指标温度要高
boolean lessThanTemp(HighTemp ht2) {
return hTemp < ht2.hTemp;
}
}
public class test1 {
static <T> int counter(T[] vals, MyFunc<T> f, T v) {
//第一个参数是查询温度列表
//第二个参数是查询内容
//第三个参数是查询指标
int count = 0;
for (int i = 0; i < vals.length; i++) {
if (f.func(vals[i], v)) {
count++;
}
}
return count;
}
public static void main(String[] args) {
int count;
HighTemp[] weekDayHighs = {
new HighTemp(89),new HighTemp(82),
new HighTemp(90),new HighTemp(89),
new HighTemp(91),new HighTemp(84),
new HighTemp(83)
};
HighTemp[] weekDayHighs2 = {
new HighTemp(32),new HighTemp(12),
new HighTemp(24),new HighTemp(19),
new HighTemp(18),new HighTemp(12),
new HighTemp(-1),new HighTemp(13)
};
count= counter(weekDayHighs, HighTemp::sameTemp, new HighTemp(89));
System.out.println(count+"天的温度超过89摄氏度");
count= counter(weekDayHighs2, HighTemp::sameTemp, new HighTemp(12));
System.out.println(count+"天的温度超过12摄氏度");
count = counter(weekDayHighs, HighTemp::lessThanTemp, new HighTemp(89));
System.out.println(count+"天的温度最高不超过89摄氏度");
count = counter(weekDayHighs2, HighTemp::lessThanTemp, new HighTemp(19));
System.out.println(count+"天的温度最高不超过19摄氏度");
}
}
运行结果如下:
![](https://img.haomeiwen.com/i16102290/2ee1042060f359a4.png)
这里存在多重引用,有HighTemp两个实例方法:sameTemp()和lessThanTemp(),作用代码里面说明了,这两个方法都需要一个HighTemp类型的参数,并且都返回布尔结果。因此这两个方法都与MyFunc函数式接口兼容,调用的时候,对象类型可以映射到func()的第一个参数,传递的实参可以映射到func()第二个参数。
我们看实例方法引用的表达式:
HighTemp::sameTemp
被传递给counter方法时,会创建函数式接口MyFunc的一个实例,第一个参数的参数类型就是实例方法的调用对象的类型,也就是HighTemp。第二个参数的类型也是HighTemp,是sameTemp()或者lessThanTemp()方法的参数。
另外,可以通过super引用方法的超类版本,如下所示:
super::name
方法的名称由name指定。
9.3 泛型中的方法引用
意思就是泛型类或者泛型方法中也可以用方法引用。看代码:
package com.edpeng.game1;
interface MyFunc<T> {
int func(T[] vals, T v);
}
class MyArrayOps {
static <T> int countMatching(T[] vals, T v) {
int count = 0;
for (int i = 0; i < vals.length; i++) {
if (vals[i] == v) {
count++;
}
}
return count;
}
}
public class test1 {
static <T> int myOp(MyFunc<T> f,T[] vals,T v) {
return f.func(vals, v);
}
public static void main(String[] args) {
Integer[] vals = {1,2,3,4,2,3,4,4,5};
String[] strs = {"One","Two","Three","Two"};
int count;
count = myOp(MyArrayOps::<Integer>countMatching, vals,4);
System.out.println("vals数列中包含"+count+"个4");
count = myOp(MyArrayOps::<String>countMatching, strs,"Two");
System.out.println("strs数列中包含"+count+"个Two");
}
}
执行结果如下:
![](https://img.haomeiwen.com/i16102290/aecfdfe8bfd7ff57.png)
- 这里首先注意的是一种语法用法:
count = myOp(MyArrayOps::<Integer>countMatching, vals,4);
这里传递的参数类型为Integer,这个东西发生在“::
”之后,这种语法特别建议经常用。但是,对于指定泛型类的情况下,类型参数位于类名的后面,“::
”的前面。
- 代码中,MyArrayOps是非泛型类,但是有泛型方法countMatching()。该方法返回数组中与指定值匹配的元素的个数,这里用泛型意味着可以匹配任何数据类型的数组。
class MyArrayOps {
static <T> int countMatching(T[] vals, T v) {
int count = 0;
for (int i = 0; i < vals.length; i++) {
if (vals[i] == v) {
count++;
}
}
return count;
}
}
之前9.3的案例代码都没有体现使用方法引用机制的真正优势,方法引用和集合框架一起使用时,才是一展拳脚的地方。(之后会开一篇专门介绍集合框架的文章。)
9.5 举一个简单有效的例子。代码作用是使用方法引用来帮助确定集合中最大的元素。
先看完整代码:
package com.edpeng.game1;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
class MyClass {
private int val;
public MyClass(int v) {
val = v;
}
int getVal() {
return val;
}
}
public class test1 {
static int compareMC(MyClass a, MyClass b) {
return a.getVal() - b.getVal();
}
public static void main(String[] args) {
ArrayList<MyClass> aList = new ArrayList<MyClass>();
aList.add(new MyClass(1));
aList.add(new MyClass(4));
aList.add(new MyClass(2));
aList.add(new MyClass(9));
aList.add(new MyClass(3));
aList.add(new MyClass(7));
MyClass maxValObj = Collections.max(aList, test1::compareMC);
System.out.println("最大值为:\n" + maxValObj.getVal());
}
}
执行结果为:
![](https://img.haomeiwen.com/i16102290/8af07c0edbbe5ca5.png)
Java集合框架都在Java.util包里面,提供很多复杂的接口和类层次,功能很多,我们用到的是java.util.Collections包中的max()方法。
MyClass maxValObj = Collections.max(aList, test1::compareMC);
这个方法主要接受两个参数,类型均为比较的对象的类型,如果第一个参数大于第二个参数,该方法返回一个正数,如果两个参数相等,返回0,如果第一个参数小于第二个参数,返回一个负数。
JDK8之前,max()方法中使用用户定义的对象,须首先通过一个类显式实现Comparator<T>接口。JDK8后,可以简单将比较方法的引用传递给max()方法,因为可以自动实现比较器。
在本程序中,MyClass既没有定义自己的比较方法,也没有实现Comparator接口。但是通过调用max()方法,仍然可以获得MyClass对象列表的最大值,这是因为UseMethodRef定义了静态方法compareMC(),他与Comparator 定义的compare()方法兼容,因此没必要显式地实现Comparator接口并创建其实例。
10.0 构造函数引用
10.1普通构造函数(非泛型)。先看代码:
package com.edpeng.game1;
interface MyFunc {
Myclass func(int n);
}
class Myclass {
private int val;
//第一个构造函数
Myclass(int v) {
val = v;
}
//第二个构造函数
Myclass() {
val = 0;
}
int getVal() {
return val;
};
}
public class test1 {
public static void main(String[] args) {
MyFunc myClassCons = Myclass::new;
Myclass mc = myClassCons.func(100);
System.out.println("Myclass中的val为: \n" + mc.getVal());
}
}
执行结果为:
![](https://img.haomeiwen.com/i16102290/bfe7cfe8da9a6280.png)
这里涉及到创造构造函数的引用。语法为:
classname::new
//上述实际代码中为
// Myclass::new
我们主要看几行关键代码:
MyFunc myClassCons = Myclass::new;
- 本程序中,MyFunc的func()方法返回MyClass类型的引用,并且有一个int类型的参数。
- MyClass定义了两个构造函数,第一个有参(int),第二个无参。
- “
Myclass::new
”就是创建了对MyClass构造函数的引用的表达式,然后通过如下语句进行实例化和设置参数(int型):
MyFunc myClassCons = Myclass::new;
Myclass mc = myClassCons.func(100);
10.2 泛型构造函数。其实与非泛型没什么区别。唯一的区别在于可以指定类型参数。如下演示代码所示:
package com.edpeng.game1;
interface MyFunc<T> {
Myclass<T> func(T n);
}
class Myclass<T> {
private T val;
//第一个构造函数
Myclass(T v) {
val = v;
}
//第二个构造函数
Myclass() {
val = null;
}
T getVal() {
return val;
};
}
public class test1 {
public static void main(String[] args) {
MyFunc<Integer> myClassCons = Myclass<Integer>::new;
Myclass<Integer> mc = myClassCons.func(100);
System.out.println("Myclass中的val为: \n" + mc.getVal());
}
}
执行结果和10.1中一样:
![](https://img.haomeiwen.com/i16102290/fa398932bef35b8e.png)
和10.1中代码内容、结构是一样的,把非泛型改成泛型而已。
10.3 像10.1和10.2只是简单说明下可以这么用,但是这么用一点好处也没有。而且两个构造函数却是相同的名字使用起来极易造成错误(很容易用错方法)。
下面这个代码案例将通过一种更加实际的用法,体现构造函数引用的好处:
package com.edpeng.game1;
interface MyFunc<R, T> {
R func(T n);
}
class MyClass<T> {
private T val;
// 第一个构造函数
MyClass(T v) {
val = v;
}
// 第二个构造函数
MyClass() {
val = null;
}
T getVal() {
return val;
};
}
class MyClass2 {
String str;
// 第一个构造函数
MyClass2(String s) {
str = s;
}
// 第二个构造函数
MyClass2() {
str = "";
}
String getVal() {
return str;
}
}
public class test1 {
static <R, T> R myClassFactory(MyFunc<R, T> cons, T v) {
return cons.func(v);
}
public static void main(String[] args) {
MyFunc<MyClass<Double>, Double> myClassCons = MyClass<Double>::new;
MyClass<Double> mClass = myClassFactory(myClassCons, 100.1);
System.out.println("Myclass中的val为: \n" + mClass.getVal());
MyFunc<MyClass2, String> myClassCons2 = MyClass2::new;
MyClass2 mc2 = myClassFactory(myClassCons2, "lambda");
System.out.println("Myclass2中的str为: \n" + mc2.getVal());
}
}
执行结果如下:
![](https://img.haomeiwen.com/i16102290/eddc92a3878a5c87.png)
- 可以看到程序中使用myClassFactory()方法来创建MyClass<Double>和MyClass2类型的对象。
- 本程序中,MyClass是泛型类,而MyClass不是,但是这两个方法都能通过myClassFactory()方法创建,因为这两个类的构造函数都与MyFunc的func()方法兼容。
- 从本例中可以看出,构造函数引用带给了Java强大的能力。
11.0 预定义的函数式接口
11.1 作为最后一个小点,必须有点总结性的东西。
本篇和上一篇所用的案例中,基本实现定义一个自己的函数式接口。作为小案例来说这算是脱裤子放屁——多此一举。但这样目的,是为了演示lambda表达式和函数式接口背后的基本概念。
11.2 在很多时候,我们并不需要自己去定义函数式接口,因为JDK8之后,包含了新包java.util.function,其中提供了一些预定义的函数式接口。
11.3 这里简单介绍下java.util.function包下一些预定于函数式接口
接口 | 用途 |
---|---|
UnaryOperator<T> | 对类型为T的对象应用一元运算,并返回结果,结果类型也是T,包含方法名为apply() |
BinaryOperator<T> | 对类型为T的对象应用操作,并返回结果,结果类型也是T,包含方法名为apply() |
Consumer<T> | 对类型为T的对象应用操作,包含方法名为accept() |
Supplier<T> | 返回类型为T的对象,包含的方法名为get() |
Function<T,R> | 对类型为T的对象应用操作,并返回结果,结果类型为R的对象,包含方法名为apply() |
Predicate<T> | 确定类型为T的对象是否满足某种约束,并返回指出结果的布尔值,包含方法名为test() |
11.4 下面演示通过function接口重写“4.0 块lambda表达式
”中计算阶乘的代码:
package com.edpeng.game1;
import java.util.function.Function;
public class test1 {
public static void main(String[] args) {
Function<Integer, Integer> factorial = (n) -> {
int result = 1;
for (int i = 1; i <= n; i++) {
result = i * result;
}
return result;
};
// 3的阶乘:1*2*3
System.out.println("3的阶乘是:" + factorial.apply(3));
// 5的阶乘:1*2*3*4*5
System.out.println("5的阶乘是:" + factorial.apply(5));
}
}
在4.0中原来的源代码为:
package com.edpeng.game1;
interface Numericfunc {
int func(int n);
}
public class test1 {
public static void main(String[] args) {
Numericfunc numericfunc = (n) -> {
int result = 1;
for (int i = 1; i <= n; i++) {
result = i * result;
}
return result;
};
// 3的阶乘:1*2*3
System.out.println("3的阶乘是:" + numericfunc.func(3));
// 5的阶乘:1*2*3*4*5
System.out.println("5的阶乘是:" + numericfunc.func(5));
}
}
两份代码执行结果是一样的:
![](https://img.haomeiwen.com/i16102290/b35c9dab459d2bf1.png)
END
网友评论