Lambda表達式支持將代碼塊作為方法的參數,Lambda表達式允許使用更加簡潔的代碼來創建一個只有一個抽象方法的接口(這種接口被稱為函數式接口)的實例。
一、Lambda表達式入門——為了避免匿名內部類的繁瑣
我們前面介紹了Command表達式的例子:
定義一個處理數組元素的接口
package one;
public interface Command
{
//接口里定義的Process方法用於封裝“處理行為”
void process(int element);
}
定義一個處理數組的類
package two;
import one.Command;
public class ProcessArray
{
public void process(int[] target,Command cmd)
{
for(var t:target)
{
cmd.process(t);
}
}
}
1、通過匿名類來調用Commad處理數組
import one.Command;
import two.ProcessArray;
class CommandTest1
{
public static void main(String[] args)
{
var pa=new ProcessArray();
int[] a={1,5,9,7};
pa.process(a,new Command(){
public void process(int element)
{
System.out.println("數組元素的平方:"+element*element);
}
});
}
}
---------- 運行Java捕獲輸出窗 ----------
數組元素的平方:1
數組元素的平方:25
數組元素的平方:81
數組元素的平方:49
輸出完成 (耗時 0 秒) - 正常終止
2、Lambda表達式來簡化匿名內部類對象
import one.Command;
import two.ProcessArray;
class CommandTest2
{
public static void main(String[] args)
{
var pa=new ProcessArray();
int[] a={1,5,9,7};
pa.process(a,(int element)->
System.out.println("數組元素的平方:"+element*element));
}
}
這段代碼代碼與創建匿名內部類時實現的process(int element)方法完全相同,只是不需要new Xxx(){}的繁瑣形式,不需要指出重寫方法的名字,也不需要指出重寫方法的返回值類型,只需要給出重寫方法括號以及括號里的形參列表即可。
3、Lambda語句的組成
Lambda表達式主要用於代替匿名內部類的繁瑣語法。它由三部分組成:
1、形參列表。形參列表允許是省略類型。如果形參列表只有一個參數,甚至連形參列表的圓括號也可以省略。
2、箭頭(->)
3、代碼塊。如果代碼塊只有一條語句允許省略代碼塊的花括號;如果只有一條return語句,甚至可以省略return關鍵字。Lambda表達式需要返回值,而他的代碼塊僅有一條省略了return 語句,Lambda表達式會自動返回這條語句的值。
Lambda表達式的集中簡化形式:
interface Eatable
{
void taste();//public abstract
}
interface Flyable
{
void fly(String weather);
}
interface Addable
{
int add(int a,int b);
}
public class LambdaQs
{
//調用該方法需要Eatable對象
public void eat(Eatable e)
{
System.out.println(e);
e.taste();
}
//調用該方法需要Flyable對象
public void drive(Flyable f)
{
System.out.println("我正在駕駛:"+f);
f.fly("[碧空如洗的晴天]");
}
//調用該方法需要Addable對象
public void test(Addable add)
{
System.out.println("3加5的和為:"+add.add(3,5));
}
public static void main(String[] args)
{
var lq=new LambdaQs();
//Lambda語句只有一條語句,可以省略花括號
lq.eat(()->System.out.println("蘋果味道不錯!"));
//Lambda表達式形參列表只有一個形參,可以省略圓括號
lq.drive(weather->{
System.out.println("今天天氣是"+weather);
System.out.println("直升機平穩飛行");});
//Lambda只有一條語句時,可以省略花括號
//代碼塊只有一條語句,即使該表達式需要返回值,也可以省略return關鍵字
lq.test((a,b)->{return (a+b);});
lq.test((a,b)->a+b);
}
}
---------- 運行Java捕獲輸出窗 ----------
LambdaQs$$Lambda$1/0x0000000801201040@72ea2f77
蘋果味道不錯!
我正在駕駛:LambdaQs$$Lambda$2/0x0000000801201840@eed1f14
今天天氣是[碧空如洗的晴天]
直升機平穩飛行
3加5的和為:8
3加5的和為:8
輸出完成 (耗時 0 秒) - 正常終止
lq.eat()使用不帶形參列表的匿名方法,由於該Lambda表達式只有一條代碼,因此可以省略花括號;
lq.drive()的Lambda表達式的形參列表只有一個形參,因此省略了形參列表的圓括號;
lq.test()的Lambda表達式的代碼塊只有一行語句,這行語句的返回值作為該代碼塊的返回值。
二、lambda表達式與函數式接口
Lambda表達式的類型,也成為“目標類型(target type)”,Lambda表達式的目標類型必須是“函數式接口(functional interface)”。函數式接口代表只包含一個一個抽象方法的接口。函數式接口可以包含多個默認方法、類方法,但只能聲明一個抽象方法。
2.1 匿名內部類和Lambda表達式的適用情況
如果采用匿名內部類語法來創建函數式接口,且只需要實現一個抽象方法,在這種情況下,即可采用Lambda表達式來創建對象,該表示創建出來的對象的目標類型就是函數式接口。
注:
Java 8專門為函數式接口提供了@FunctionalInterface注解,該注解用於方法在接口定義前面,該注解對程序功能沒有任何影響,它用於告訴編譯器執行更嚴格的檢查——檢查該接口必須是函數式接口,否則編譯就會出錯。
下面程序使用匿名內部類:
/*@FunctionalInterface
*A 不是函數接口
* 在 接口 A 中找到多個非覆蓋抽象方法
*/
interface A
{
public void test1();
public void test2();
default void test3()//接口中的默認方法
{
System.out.println("接口A中的默認方法");
}
}
public class 適用匿名內部類
{
public void test(A a)
{
System.out.println("接口A含有兩個抽象方法和一個默認方法,此時適合用匿名內部類");
a.test1();
a.test2();
a.test3();
}
public static void main(String[] args)
{
var p=new 適用匿名內部類();
p.test(new A()
{
public void test1()
{
System.out.println("接口中的抽象方法1");
}
public void test2()
{
System.out.println("接口中的抽象方法2");
}
});
}
}
接口A含有兩個抽象方法和一個默認方法,此時適合用匿名內部類
接口中的抽象方法1
接口中的抽象方法2
接口A中的默認方法
下面定義的接口B只有一個抽象方法,是函數式接口,此時適合用Lambda表達式:
@FunctionalInterface
interface B
{
void test1(String msg);//抽象方法,默認public abstract
default void test2()//接口中的默認方法
{
System.out.println("接口A中的默認方法");
}
}
public class LambdaFor
{
public void test(B b)
{
System.out.println("接口A含有1個抽象方法和一個默認方法,是函數式接口");
b.test1("函數式接口A中的抽象方法");
b.test2();
}
public static void main(String[] args)
{
var p=new LambdaFor();
p.test((msg)->
System.out.println(msg));
}
}
---------- 運行Java捕獲輸出窗 ----------
接口A含有1個抽象方法和一個默認方法,是函數式接口
函數式接口A中的抽象方法
接口A中的默認方法
輸出完成 (耗時 0 秒) - 正常終止
2.2 使用Lambda表達式賦值給對象
用於Lambda表達式的結果就是被當成對象,因此程序中完全可以使用Lambda表達式進行賦值。我們知道接口不能創建實例,接口中只能定義常量,因此接口不存在構造器和初始化塊。接口不能創建實例,但是通過Lambda表達式我們可以創建一個“目標類型”並把它賦值給函數式接口的對象。
例如:
@FunctionalInterface
interface Runnable
{
void printNum();
}
public class RunnableTest
{
public static void main(String[] args)
{
//Runnable接口中只包含一個無參數的構造器方法
//Lambda表達式代表的匿名方法實現了Runnable接口中唯一的無參數方法
//因此下面的方法創建了一個Runnable的對象
Runnable r=()->{
for(int i=0;i<10;i++)
System.out.print(" "+i);
};
r.printNum();
}
}
---------- 運行Java捕獲輸出窗 ----------
0 1 2 3 4 5 6 7 8 9
輸出完成 (耗時 0 秒) - 正常終止
Lambda表達式實現的匿名方法——因此它只能實現特定函數式接口中唯一方法。這意味着Lambda表達式有兩個限制:
1、Lambda表達式的目標類型必須是明確的函數式接口。
2、Lambda表達式只能為函數式接口創建對象。Lambda表達式只能實現一個方法,因此他只能為只有一個抽象方法的接口(函數式接口)創建對象。
關於第一點限制舉例:
@FunctionalInterface
interface A
{
void test();
}
class LambdaLimit1
{
public static void main(String[] args)
{
//Object a=()->{System.out.println("This is a test!");};
//上面代碼將報錯: 不兼容的類型: Object 不是函數接口
//Lambda表達式的目標類型必須是明確的函數式接口
A a=()->{System.out.println("This is a test!");};
a.test();//This is a test!
}
}
從錯誤信息可以看出,Lambda表達式的目標類型必須是明確的函數式接口。上述表達式將Lambda表達式賦給Object變量,編譯器只能確定該表達式的類型為Object,而Object並不是函數式接口。
為了保證Lambda表達式的目標類型是一個明確的函數式接口,常見有三種方式:
1、將Lambda表達式賦值給函數式接口的變量;
//參考上面的完整程序
A a=()->{System.out.println("This is a test!");};
2、將Lambda表達式作為函數接口類型的參數傳給某個方法。
interface A
{
void test(String msg);
}
public class ATest
{
public static void med(A a)
{
System.out.println("主類的非靜態方法");
a.test("我是傳奇");
}
public static void main(String[] args)
{
ATest.med((msg)->System.out.println(msg));
}
}
---------- 運行Java捕獲輸出窗 ----------
主類的非靜態方法
我是傳奇
輸出完成 (耗時 0 秒) - 正常終止
3、使用函數式接口類型對Lambda表達式進行強制轉換。
Object a=(A)()->{System.out.println("This is a test!");};
三、在Lambda表達式中使用var
對與var聲明變量,程序可以使用Lambda表達式進行賦值。但由於var代表需要由編譯器推斷的類型,因此使用Lambda表達式對var表達式定義的變量進行賦值時,必須指明Lambda表達式的目標類型。
例如:
var a=(A)()->{System.out.println("This is a test!");};
如果程序需要對Lambda表達式的形參列表添加注解,此時就不能省略Lambda表達式的形參類型——因為注解只能放在形參類型之前。在Java 11之前,程序必須嚴格聲明Lambda表達式中的每個形參類型,但實際上編譯器完全可以推斷出lambda表達式中每個形參的類型。
例如:下面程序定義了一個Predator接口,該接口中的prey方法的形參使用了@NotNull注解修飾:
@interface NotNull{}
interface Predator
{
void prey(@NotNull String animal);
}
接下來程序打算使用Lambda表達式來實現一個Predator對象。如果Lambda表達式不需要對animal形參使用@NotNull注解,則完全可以省略animal形參注解;但如果希望為animal形參注解,則必須為形參聲明類型,此時可直接使用var來聲明形參類型。
@interface NotNull{}
interface Predator
{
void prey(@NotNull String animal);
}
public class PredatorTest
{
public static void main(String[] args)
{
//使用var聲明lambda表達式的形參類型
//這樣即可為Lambda表達式的形參添加注解
Predator p=(@NotNull var animal)->System.out.println("老鷹在抓"+animal);
p.prey("小雞");
}
}
//老鷹在抓小雞
四、方法引用和構造器引用
Lambda表達式的方法引用和構造器引用都需要兩個英文冒號::。Lambda表達式支持如下幾種引用方式:
| 種類 | 示例 | 說明 | 對應的Lambda表達式 |
|---|---|---|---|
| 引用類方法 | 類名::類方法名 | 函數式接口中被實現的方法的參數全部傳給該類方法作為參數 | (a,b...)->類名.類方法(a,b...) |
| 引用特定對象的實例方法 | 特定對象::示例方法名 | 函數式接口中被實現的方法的參數全部傳給該實例方法作為參數 | (a,b...)->特定對象.實例方法(a,b...) |
| 引用某類對象的實例方法 | 類名::實例方法名 | 函數式接口中被實現的方法的第一個參數作為調用者,后面的參數傳給該方法作為參數 | (a,b,c...)->a.實例方法(b,c...) |
| 引用構造器 | 類名::new | 函數式接口中被實現的方法的全部參數傳給該構造器作為參數 | (a,b...)->new 類名(a,b...) |
4.1 引用類方法
@FunctionalInterface
interface Converter
{
Integer convert(String form);
}
public class ConverterTest
{
public static void main(String[] args)
{
//Lambda表達式只有一條語句,可以省略1花括號:Lambda表達式會把這條代碼的值作為返回值
Converter c=(form)->Integer.parseInt(form);
System.out.println(c.convert("185"));
//下面通過引用類方法來實現相同的功能
Converter cPlus=Integer::valueOf;
System.out.println(cPlus.convert("140"));
}
}
4.2 引用特定對象的實例方法
@FunctionalInterface
interface Converter
{
Integer convert(String form);
}
public class ConverterTest1
{
public static void main(String[] args)
{
//先使用Lambda表達式來創建一個Converter對象
Converter c=form->"fkit.org".indexOf(form);//代碼塊只有一條語句,因此Lambda表達式會把這條代碼的值作為返回值
System.out.println(c.convert("it"));//輸出2
//引用特定對象的特定方法 "fkit.org"是一個String對象
Converter c1="fkit.org"::indexOf;
System.out.println(c1.convert("org"));//輸出5
}
}
對於上面的示例方法引用,也就是說,調用"fkit.org"對象的indexOf()實例方法來實現Converter函數式接口中唯一的抽象方法,當調用Converter接口中的唯一抽象的方法時,調用參數會傳給"fkit.org"對象的indexOf()實例方法。
4.3 引用某類對象的實例方法
先介紹一個函數:public String substring(int beginIndex, int endIndex)返回字符串索引范圍[beginIndex,endIndex)的子字符串。
@FunctionalInterface
interface MyTest
{
String test(String a,int b, int c);
}
class substringTest
{
public static void main(String[] args)
{
MyTest m=(a,b,c)->a.substring(b,c);
System.out.println(m.test("fkjava",1,5));
//引用某類對象的實例方法
MyTest mPlus=String::substring;
System.out.println(mPlus.test("hello world",2,7));//相當於"hello world".substring(2,7)
}
}
---------- 運行Java捕獲輸出窗 ----------
kjav
llo w
輸出完成 (耗時 0 秒) - 正常終止
4.4 引用構造器
JFrame屏幕上window的對象,能夠最大化、最小化、關閉
import java.awt.*;
import javax.swing.*;
@FunctionalInterface
interface YourTest
{
JFrame win(String title);
}
public class MethodRefer
{
public static void main(String[] args)
{
// 下面代碼使用Lambda表達式創建YourTest對象
// YourTest yt = (String a) -> new JFrame(a);
// 構造器引用代替Lambda表達式。
// 函數式接口中被實現方法的全部參數傳給該構造器作為參數。
YourTest yt = JFrame::new;
JFrame jf = yt.win("我的窗口");
System.out.println(jf);
}
}
五、Lambda表達式和匿名內部類的聯系和區別
Lambda表達式與匿名內部類之間存在如下相同點:
1、Lambda表達式與匿名內部類一樣,都可以直接訪問"effectively final"的局部變量,以及外部類的成員變量,包括實例變量和類變量。
2、Lambda表達式創建的對象與匿名內部類生成的對象一樣,都可以直接從接口中繼承的默認方法。
@FunctionalInterface
interface Displayable
{
void display();
default int add(int a,int b)
{
return a+b;
}
}
public class LambdaAndInner
{
private int age=12;
private static String name="fkit.org";
public void test()
{
var book="瘋狂Java講義";
Displayable dis=()->{
//訪問"effictively final"的局部變量
System.out.println("book局部變量為:"+book);
//訪問外部類的實例變量和類變量
System.out.println("外部類的age實例變量:"+age);
System.out.println("外部類的name類變量:"+name);
};
dis.display();
//調用方對從接口繼承add()方法
System.out.println(dis.add(3,5));
}
public static void main(String[] args)
{
var lambda=new LambdaAndInner();
lambda.test();
}
}
---------- 運行Java捕獲輸出窗 ----------
book局部變量為:瘋狂Java講義
外部類的age實例變量:12
外部類的name類變量:fkit.org
8
輸出完成 (耗時 0 秒) - 正常終止
與匿名函數相似的是,由於Lambda表達式訪問了book局部變量,因此該局部變量相當於有一個隱式的final修飾,因此同樣不允許對book局部變量重新賦值。當程序使用了Lambda表達式創建了Displayable對象之后,該對象不僅可調用接口的抽象方法,也可以調用接口中的默認方法,因此同樣不允許對book局部變量重新賦值。
</font color=red>Lambda表達式與匿名內部類的區別:
1、匿名內部類可以為內部類可以為任意接口創建實例;但Lambda表達式只能為函數式創建實例。
2、匿名內部類可以為抽象類乃至普通類創建實例;但Lambda表達式只能為函數式接口創建實例。
3、匿名內部類是實現抽象方法的方法體允許調用接口中定義的默認方法;但Lambda表達式的代碼塊不允許不允許調用接口中的默認方法。
六、使用Lambda表達式調用Arrays的類方法
Arrays類的有些方法需要Comparator、XxxOperator、XxxFunction等接口的實例,這些接口都是函數式編程,因此可以使用Lambda表達式來調用Arrays的方法。
import java.util.Arrays;
public class LambdaArrays
{
public static void main(String[] args)
{
var arr1 = new String[] {"java", "fkava", "fkit", "ios", "android"};
Arrays.parallelSort(arr1, (o1, o2) -> o1.length() - o2.length());
//這行Lambda表達式的目標類型是Comparator,該Comparator指定判斷字符串大小的標准:字符串越長,認為該字符串越大
System.out.println(Arrays.toString(arr1));
var arr2 = new int[] {3, -4, 25, 16, 30, 18};
// left代表數組中前一個所索引處的元素,計算第一個元素時,left為1
// right代表數組中當前索引處的元素
Arrays.parallelPrefix(arr2, (left, right)-> left * right);
//這行Lambda表達式的目標類型是IntBinaryOperator,該對象將會根據前后兩個元素來計算當前元素
System.out.println(Arrays.toString(arr2));
var arr3 = new long[5];
// operand代表正在計算的元素索引
Arrays.parallelSetAll(arr3, operand -> operand * 5);
//這行Lambda表達式的目標類型是IntToLongFunction,該對象將會根據當前索引值計算當前元素的值。
System.out.println(Arrays.toString(arr3));
}
}
---------- 運行Java捕獲輸出窗 ----------
[ios, java, fkit, fkava, android]
[3, -12, -300, -4800, -144000, -2592000]
[0, 5, 10, 15, 20]
輸出完成 (耗時 0 秒) - 正常終止
