java函数的编写-编写函数把华氏温度转换为摄氏温度,
出品 | CSDN博客
本文将介绍Java 8中新增的Lambda表达式,包括Lambda表达式的常见用法和方法引用的用法,分析Lambda表达式的原理,最后总结Lambda表达式的优缺点。
概述
Java 8引入的Lambda表达式的主要作用是简化部分匿名内部类的编写。
能够使用Lambda表达式的一个重要基础是必须有相应的函数式接口。 所谓函数式接口,是指内部只有一个抽象方法的接口。
lambda 表达式的另一个基础是类型推断机制。 给定足够的上下文信息,编译器可以在不显式命名的情况下推断参数列表的类型。
常见用法
2.1 无参数函数的简写
无参函数就是没有参数的函数,比如Runnable接口的run方法,定义如下:
@FunctionalInterface
public interface Runnable {
public abstract void run;
}
在Java 7及更早的版本中,我们一般可以这样使用:
new Thread(new Runnable {
@Override
public void run {
System.out.println("Hello");
System.out.println("Jimmy");
}
}).start;
从Java 8开始,无参函数的匿名内部类可以简写如下:
-> {
执行语句
}
这样接口名和函数名就可以省略了。 那么,上面的例子可以简写为:
new Thread( -> {
System.out.println("Hello");
System.out.println("Jimmy");
}).start;
当只有一条语句时,我们也可以将代码块简写,格式如下:
-> 表达式
注意这里用的是表达式,不是语句,也就是说不需要在末尾加分号。
那么,当上面例子中只有一条语句执行时,可以简写为:
new Thread( -> System.out.println("Hello")).start;
2.2 单参数函数的简写
单参数函数是只有一个参数的函数。 例如View内部的OnClickListener接口的onClick(View v)方法定义如下:
public interface OnClickListener {
/**
* Called when a view has been clicked.
*
* @param v The view that was clicked.
*/
void onClick(View v);
}
在Java 7及更早的版本中,我们通常可能会这样使用:
view.setOnClickListener(new View.OnClickListener {
@Override
public void onClick(View v) {
v.setVisibility(View.GONE);
}
});
从Java 8开始,单参数函数的匿名内部类可以简写如下:
([类名 ]变量名) -> {
执行语句
}
类名可以省略,因为Lambda表达式可以自己推断。 那么上面的例子可以简写为:
view.setOnClickListener((View v) -> {
v.setVisibility(View.GONE);
});
view.setOnClickListener((v) -> {
v.setVisibility(View.GONE);
});
单参数函数甚至可以去掉括号,官方推荐使用这种方法:
变量名 -> {
执行语句
}
那么,上面的例子可以简写为:
view.setOnClickListener(v -> {
v.setVisibility(View.GONE);
});
当只有一条语句时,代码块仍然可以简写,格式如下:
([类名 ]变量名) -> 表达式
类名和括号仍然可以省略,如下:
变量名 -> 表达式
那么,上面的例子可以进一步简写为:
view.setOnClickListener(v -> v.setVisibility(View.GONE));
2.3 多参数函数的简写
多参数函数是具有两个或多个参数的函数。 例如Comparator接口的compare(T o1, To o2)方法有两个参数,定义如下:
@FunctionalInterface
public interface Comparator
{ int compare(T o1, T o2);
}
在Java 7及更早的版本中,当我们对一个集合进行排序时,通常可以这样写:
List
list = Arrays.asList(1, 2, 3); Collections.sort(list, new Comparator
{ @Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
从Java 8开始,多参数函数的匿名内部类可以简写如下:
([类名1 ]变量名1, [类名2 ]变量名2[, ...]) -> {
执行语句
}
相同的类名可以省略,所以上面的例子可以简写为:
Collections.sort(list, (Integer o1, Integer o2) -> {
return o1.compareTo(o2);
});
Collections.sort(list, (o1, o2) -> {
return o1.compareTo(o2);
});
当只有一条语句时,代码块仍然可以简写,格式如下:
([类名1 ]变量名1, [类名2 ]变量名2[, ...]) -> 表达式
这时候类名也可以省略,但是括号不能省略。 如果这条语句需要返回一个值,那么return关键字就不用写了。
因此,上面的例子可以进一步简写为:
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
最后,这个例子也可以简写如下:
Collections.sort(list, Integer::compareTo);
咦,这是什么功能? 这就是我们接下来要讲的:方法引用。
方法参考
方法引用也是一种语法糖,可用于简化开发。
我们在使用Lambda表达式的时候,如果“->”右边要执行的表达式只是调用一个类已有的方法,那么可以用“方法引用”代替Lambda表达式。
方法引用可分为4类:
下面分别介绍这四类。
3.1 引用静态方法
当我们要执行的表达式是调用某个类的静态方法,而这个静态方法的参数列表对应接口中抽象函数的参数列表时,我们可以使用引用静态方法的格式。
如果 Lambda 表达式符合以下格式:
([变量1, 变量2, ...]) -> 类名.静态方法名([变量1, 变量2, ...])
我们可以简写如下:
类名::静态方法名
注意静态方法名后不需要加括号或参数,因为编译器可以推断出来。 下面我们继续以2.3节中的例子进行说明。
首先创建一个工具类,代码如下:
public class Utils {
public static int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
}
注意比较函数的参数和Comparable接口的比较函数的参数是一一对应的。 那么一个通用的Lambda表达式可以这样写:
Collections.sort(list, (o1, o2) -> Utils.compare(o1, o2));
如果你使用方法引用,你可以像这样缩写它:
Collections.sort(list, Utils::compare);
3.2 引用对象的方法
当我们要执行的表达式是调用一个对象的方法,而这个方法的参数列表对应接口中抽象函数的参数列表时,我们可以使用引用该对象的方法的格式。
如果 Lambda 表达式符合以下格式:
([变量1, 变量2, ...]) -> 对象引用.方法名([变量1, 变量2, ...])
我们可以简写如下:
对象引用::方法名
下面我们继续以2.3节中的例子进行说明。 首先创建一个类,代码如下:
public class MyClass {
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
}
当我们创建这个类的对象java函数的编写,并在Lambda表达式中使用该对象的方法时,一般可以这样写:
MyClass myClass = new MyClass;
Collections.sort(list, (o1, o2) -> myClass.compare(o1, o2));
注意这里函数的参数也是一一对应的,所以方法引用的方式可以简写如下:
MyClass myClass = new MyClass;
Collections.sort(list, myClass::compare);
另外,当我们要执行的表达式是调用Lambda表达式所在类的方法时,也可以使用如下格式:
this::方法名
比如我在Lambda表达式所在的类中添加如下方法:
private int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
在Lambda表达式中使用该方法时,一般可以这样写:
Collections.sort(list, (o1, o2) -> compare(o1, o2));
如果你使用方法引用,你可以像这样缩写它:
Collections.sort(list, this::compare);
3.3 引用类的方法
引用类的方法所采用的相应参数形式与上述两种略有不同。 如果Lambda表达式的“->”右边要执行的表达式是被调用的“->”左边第一个参数的实例方法,第二个参数(或者没有参数)对应给实例方法当实例方法的参数列表时可以使用此方法。
这可能有点令人困惑,假设我们的 Lambda 表达式符合以下格式:
(变量1[, 变量2, ...]) -> 变量1.实例方法([变量2, ...])
那么我们的代码可以简写为:
变量1对应的类名::实例方法名
还是用2.3节的例子,当我们使用的Lambda表达式是这样的:
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
根据上面的说法,可以简写为:
Collections.sort(list, Integer::compareTo);
3.4 引用构造函数
当我们要执行的表达式是创建一个新的对象,而对象的构造方法的参数列表与接口中函数的参数列表相对应时,我们可以使用“引用构造方法”的格式。
假设我们的 Lambda 表达式符合以下格式:
([变量1, 变量2, ...]) -> new 类名([变量1, 变量2, ...])
我们可以简写如下:
类名::new
我们举个例子来说明。 Java 8引入了Function接口,也就是函数式接口。 部分代码如下:
@FunctionalInterface
public interface Function
{ /**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
// 省略部分代码
}
我们使用这个接口来实现一个函数来创建一个指定大小的ArrayList。 一般我们可以这样做:
Function
function = new Function { @Override
public ArrayList apply(Integer n) {
return new ArrayList(n);
}
};
List list = function.apply(10);
使用 Lambda 表达式,我们通常可以这样写:
Function
function = n -> new ArrayList(n);
使用“引用构造函数”,我们可以将其缩写为:
Function
function = ArrayList::new;
自定义功能接口
自定义函数接口很容易,写一个只有一个抽象方法的接口即可,示例代码:
@FunctionalInterface
public interface MyInterface
{ void function(T t);
}
上面代码中的@FunctionalInterface是可选的,但是有了这个注解,编译器会帮你检查接口是否符合函数式接口规范。 就像添加@Override注解会检查函数是否被重写一样。
实现原理
经过上面的介绍,我们看到Lambda表达式只是为了简化匿名内部类的写法。 似乎在编译阶段将所有的Lambda表达式替换为匿名内部类就足够了。 但这种情况并非如此。 在JVM层面,Lambda表达式和匿名内部类其实有着明显的区别。
5.1 匿名内部类的实现
匿名内部类仍然是一个类,但我们不需要显式指定类名,编译器会自动为类命名。 例如,有如下形式的代码:
public class LambdaTest {
public static void main(String[] args) {
new Thread(new Runnable {
@Override
public void run {
System.out.println("Hello World");
}
}).start;
}
}
编译后会生成两个class文件:
LambdaTest.class
LambdaTest$1.class
使用javap -c LambdaTest.class进一步分析LambdaTest.class的字节码,部分结果如下:
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class com/example/myapplication/lambda/LambdaTest$1
7: dup
8: invokespecial #4 // Method com/example/myapplication/lambda/LambdaTest$1."
":V 11: invokespecial #5 // Method java/lang/Thread."
":(Ljava/lang/Runnable;)V 14: invokevirtual #6 // Method java/lang/Thread.start:V
17: return
可以发现匿名内部类的对象是在第4行:new #3创建的。
5.2 Lambda表达式的实现
接下来,我们将使用Lambda表达式来实现上面的示例代码,代码如下:
public class LambdaTest {
public static void main(String[] args) {
new Thread( -> System.out.println("Hello World")).start;
}
}
此时编译后只会生成一个文件LambdaTest.class。 我们看一下通过javap反编译文件的结果:
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."
":(Ljava/lang/Runnable;)V 12: invokevirtual #5 // Method java/lang/Thread.start:V
15: return
从上面的结果我们发现java函数的编写,Lambda表达式被封装到主类的私有方法中,通过invokedynamic指令调用。
因此,我们可以得出结论,Lambda表达式是通过invokedynamic指令实现的,编写Lambda表达式并不会产生新的类。
由于Lambda表达式不会创建匿名内部类,因此在Lambda表达式中使用this关键字时,指向外部类的引用。
的优点和缺点
优势:
缺点: