目录
一、反射机制
1、含义
2、作用
3、※反射相关的几个类
3.1、Class类(Class对象是反射的基石)
3.2、Class类中相关的方法
3.2.1 (※重要)常用获得类相关的方法
3.2.2 (※重要)常用获得类中属性、变量Field相关的方法
3.2.3 获得类中注解相关的方法
3.2.4(※重要)获得类中构造器相关的方法
3.2.5(※重要)获得类中方法相关的方法
4、使用反射来获取类的信息
4.1 获取Class对象的三种方法
4.2 反射的一系列使用
4.2.1通过反射获取对象的变量信息
4.2.2通过反射获取对象的方法信息
4.2.3通过反射获取对象的构造方法信息
5、反射的优点和缺点
二、枚举
1、背景及定义
2、枚举的使用
2.1、此处是简单的获取枚举对象,直接引用即可
2.2、Enmu常用的方法
2.3、枚举的原理
2.4、有关枚举的values()方法和valueOf()方法
3、枚举的优缺点
4、枚举 和 反射
三、Lambda表达式
1、背景和含义
1.1、Lambda表达式的语法
1.2、函数式接口
2、Lambda表达式的使用
2.1、Lambda表达式的使用
2.2、Lambda表达式的精简
3、变量捕获
3.1、 匿名内部类
3.2、匿名内部类的变量捕获
3.3、Lambda变量捕获
3.4、匿名内部类和Lambda的区别
4、Lambda在集合中的使用
4.1、Collection接口
4.2、List接口
4.3、Map接口
5、总结
一、反射机制
1、含义
Java的反射(reflection)机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,既然能拿到那么,我们就可以修改部分类型信息;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射(reflection)机制。
可以想象在过安检时,传送带上防止我们的行李和物品,经过安检机器后,安检员可以实时动态的看到行李箱里的物品。跟java的反射机制有点类似,在运行中就可以知道一个类、对象中的一切细节(成员方法、成员变量等等的一些列信息),并可以对其进行修改.
2、作用
- 当在类中定义了被private修饰的成员变量、成员方法等,类外就无法再对其进行访问或修改了,只有在那个类内部才可以进行调用、访问或修改的操作。要是我们想在类外访问被私有private修饰的变量或方法时,就可以利用 “反射”机制来获取类中私有的成员,并进行访问或修改等操作!
- 反射最重要的用途就是开发各种通用框架,比如在spring中,我们将所有的类Bean交给spring容器管理,无论是XML配置Bean还是注解配置,当我们从容器中获取Bean来依赖注入时,容器会读取配置,而配置中给的就是类的信息,spring根据这些信息,需要创建那些Bean,spring就动态的创建这些类。
3、※反射相关的几个类
类名 | 用途 |
Class类 【特殊】 | 代表类的实体,在运行的Java应用程序中表示类和接口 |
Field类(字段) | 代表类的成员变量/类的属性 |
Method类(方法) | 代表类的方法 |
Constructor类(构造方法) | 代表类的构造方法 |
3.1、Class类(Class对象是反射的基石)
※没有class对象就没法进行反射!
“ 反射机制 ” 最重要的就是 “运行时”(在程序运行的时候,可以加载、探索编译期间的未知.class文件)
当一个.java程序编译的时候,会生成一个.class二进制文件(字节码文件),此时的JVM就会去解读这个.class文件,之后就会解析创建出一个Class对象(java.lang.Class),这样当程序在运行时,每个java文件就最终变成了Class类对象的一个实例。我们通过Java的反射机制应用到这个实例,就可以去获得甚至去添加改变这个类的属性和动作,使得这个类成为一个动态的类.
换句话说,Java 程序可以加载一个运行时才得知名称的 .class 文件,然后获悉其完整构造,并生成其对象实体、或对其 fields(变量)设值、或调用其 methods(方法)或传参调用constructor构造方法。
3.2、Class类中相关的方法
3.2.1 (※重要)常用获得类相关的方法
方法 | 用途 |
getClassLoader() | 获得类的加载器 |
getDeclaredClasses() | 返回一个类数组,数组中包含该类中所有类和接口类的对象(Declared包括私有private的) |
forName(String className) | 传入类的完整路径名字来获取类的Class对象 |
newInstance() | 创建类的实例 |
getName() | 获得类的完整路径名字(类似com.demo.Reflect) |
3.2.2 (※重要)常用获得类中属性、变量Field相关的方法
方法 | 用途 |
getField(String name) | 根据成员变量名获取该公有的属性对象 |
getFields() | 获取公有的所有属性对象 |
getDeclaredField(String name) | 根据成员变量名获取所有的属性对象(包括private私有的) |
getDeclaredFields() | 获取公有的所有属性对象(包括private私有的) |
3.2.3 获得类中注解相关的方法
方法 | 用途 |
getAnnotation(Class annotationClass) | 返回该类中与参数类型匹配的公有注解对象 |
getAnnotations() | 返回该类所有的公有注解对象 |
getDeclaredAnnotation(Class annotationClass) | 返回该类中与参数类型匹配的所有注解对象 |
getDeclaredAnnotations() | 返回该类所有的注解对象 |
3.2.4(※重要)获得类中构造器相关的方法
方法 | 用途 |
getConstructor(构造方法的参数)----反射中的参数形式为:String.class、int.class等等 | 获得该类中与参数类型匹配的公有构造方法 |
getConstructors() | 获取该类中所有的公有构造方法 |
getDeclaredConstructor(构造方法的参数) | 获得该类中与参数类型匹配的所有构造方法(包括私有private修饰的) |
getDeclaredConstructors() | 获取该类中所有的构造方法(包括私有private修饰的) |
3.2.5(※重要)获得类中方法相关的方法
方法 | 用途 |
getMethod(String name, Class... parameterTypes) | 获得该类某个公有的方法 |
getMethods() | 获得该类所有公有的方法 |
getDeclaredMethod(String name, Class... parameterTypes) | 获得该类某个方法(包括私有的) |
getDeclaredMethods() | 获得该类所有方法(包括私有的) |
根据上述的方法,可以知道,可以利用反射机制在运行时,解析编译的class文件获取Class对象,并进行实例,可以访问类中的私有成员。像上述方法中,带有Declared的方法都可以获取对应的私有成员。
4、使用反射来获取类的信息
首先,要使用反射获取一个类中的所有信息,必须先获取其Class对象,然后通过Class对象的核心方法,达到反射的目的。
此处借助一个Student类来演示:
class Student {
//私有属性name
private String name = "bit";
//公有属性age
public int age = 18;
//不带参数的构造方法
public Student() {
System.out.println("Student()");
}
private Student(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Student(String,name)");
}
private void eat() {
System.out.println("i am eat");
}
public void sleep() {
System.out.println("i am pig");
}
private void function(String str) {
System.out.println(str);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
4.1 获取Class对象的三种方法
第一种方法:是通过实例了的对象,来直接使用类对象的getClass()方法获取Class对象
第二种方法:(适合在编译前就已经明确了要操作的类Class) 调用目标对象类的class()方法
第三种方法:(需要明确对象类的完整路径名字)调用Class类的forName()方法
public static void main(String[] args) {
//获取Class对象的三种方法
//1.通过getClass获取Class对象
Student student = new Student();
Class<?> c1 = student.getClass();
//2..直接通过 类名.class 的方式得到,该方法最为安全可靠,程序性能更高
//这说明任何一个类都有一个隐含的静态成员变量 class
Class<?> c2 = Student.class;
//3.通过 Class 对象的 forName() 静态方法来获取,用的最多,
//但可能抛出 ClassNotFoundException 异常
Class<?> c3 = null;
try {
c3 = Class.forName("Student"); //此处是类的完整路径名字
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
//因为三个Class对象都是同一个,说明对象只有一个,一个类只对应一个Class对象(一个类在 JVM 中只会有一个 Class 实例)
System.out.println(c1.equals(c2));
System.out.println(c2.equals(c3));
System.out.println(c1.equals(c3));
}
4.2 反射的一系列使用
通过反射获取类的方法Method、属性Field、构造方法Constructor(包括私有的,注意需要调用其对应的setAccessible(true)方法来获取访问修饰权限,是获取,不是修改)
注意:所有和反射相关的包都在 import java.lang.reflect 包下面。并且必须要有创建一个Class对象,才可以去通过反射机制去获取类的所有信息
下面演示的是以People类
class People {
//私有属性name
private String name = "bit";
//公有属性age
public int age = 18;
//不带参数的构造方法
public People() {
System.out.println("People()");
}
private People(String name, int age) {
this.name = name;
this.age = age;
System.out.println("People(String,name)");
}
private void eat() {
System.out.println("i am eat");
}
public void sleep() {
System.out.println("i am pig");
}
private void function(String str) {
System.out.println(str);
}
@Override
public String toString() {
return "People{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
4.2.1通过反射获取对象的变量信息
public static void getField(){
try {
//1.通过People类的完整路径名去获取Class对象
Class<?>c = Class.forName("review.reflectdemo2.People");
//此处需要注意的是:调用newInstance()返回的是一个Object类,所以需要强转类型
People people = (People) c.newInstance();
//2.根据变量名获取类中的变量
Field field = c.getDeclaredField("name"); //可以获取任意类型的一个变量(包括private私有修饰的变量)
Field []fields = c.getFields(); //获取所有的公有变量
for(Field s:fields){
//3.获取变量的访问修饰权限
int modifier = s.getModifiers();
//打印变量的访问修饰权限+类型名+变量名
System.out.println(Modifier.toString(modifier)+
" "+ s.getType().getName()+
" "+s.getName());
}
//对私有的变量进行修改,需要先获取访问权限
field.setAccessible(true);
//此处要修改变量信息,调用的是Field类的set方法,参数分别是:(1)指定修改的People对象(2)修改后的值
field.set(people,"Danie");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
4.2.2通过反射获取对象的方法信息
public static void getMethod() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException {
Class<?> c = Class.forName("review.reflectdemo2.People");
People people = (People)c.newInstance();
//1.获取公有的方法(参数是方法名和方法的参数),因为此处的sleep()方法没有参数,所有无需加参数
Method method = c.getMethod("sleep");
//2.获取所有类型的一个方法(此处的function()有一个参数,根据反射的规则,其参数形式是形如-----类型名.class )
Method method1 = c.getDeclaredMethod("function",String.class);
//3.获取所有的公有方法(返回一个Method数组)
Method []methods = c.getMethods();
//获取所有类型的方法(包括私有的)
Method []methods1 = c.getDeclaredMethods();
for(Method temp:methods1){
//获取方法的访问修饰权限
int modifiers = temp.getModifiers();
//获取方法的返回值类型
Class<?> returnType = temp.getReturnType();
//获取方法的所有参数
Parameter[]parameters = temp.getParameters();
System.out.println("\n"+"访问修饰权限为:"+Modifier.toString(modifiers)+
" " + "返回类型为:"+returnType+
" " + "方法名:"+temp.getName());
System.out.print(" 参数为: ");
for(Parameter parameter:parameters){
System.out.println("[类型"+parameter.getType().getName()+
" "+"参数名:"+parameter.getName()+"]");
}
}
}
输出结果如下:
(引用类型获取返回类型会直接返回其完整的路径名)
4.2.3通过反射获取对象的构造方法信息
public static void getConstructor(){
try {
//基本跟上述的变量和方法无异
Class<?> c = Class.forName("review.reflectdemo2.People");
Constructor<?>constructor = c.getDeclaredConstructor(String.class,int.class);
//获取两个参数的构造方法
//可以利用构造方法来直接创建示例
//注意对于私有的构造方法,调用也需要获取访问修饰权限
constructor.setAccessible(true);
People people = (People) constructor.newInstance("Nero",18);
System.out.println(people);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
5、反射的优点和缺点
(1)优点:
- 在运行时状态,对于任意一个类都可以知道这个类中的所有方法和变量,即使时private私有的修饰也可以,也可以进行修改或调用;对于任意一个对象,也可以调用其所有的方法
- 增加程序的灵活性和扩展性,降低耦合性,提高自适应能力。
- 反射已经运用在了很多流行框架如:Struts、Hibernate、Spring 等等。
(2)缺点:
- 使用反射会有效率问题。会导致程序效率降低。毕竟只是单单访问一个私有变量,或者调用一个方法,就得多条代码来实现。
- 反射技术绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂 。
二、枚举
1、背景及定义
枚举类型是java5中新增特性的一部分 / 在JDK1.5以后引入的),它是一种特殊的数据类型。枚举,主要就是将一组常量给组织起来,方便统一管理。
一般我们平时创建常量最常见的方式如下:
public static final int January = 1;
public static final int February = 2;
public static final int March = 3;
public static final int April = 4;
此处定义的常量本身没有问题,但是有一定的缺陷,在类型安全和使用方便性上并没有多少好处,如果存在定义的int类型的值刚好一样的变量存在(就可能把另外一个变量值为1的直接认定为January),可能会混淆,编译器也不会进行检验提示。
现在使用利用枚举类型来定义组织管理常量,就不会出现这样的问题,且可以很直观的观察到常量值,且代码简洁,易管理。
public enum Month {
//此处为一个简单的枚举类
//里面的Januar,February,March,April 每一个是一个枚举对象
January,February,March,April,May,June,July,August,
September,October,November,December;
}
🎈枚举的应用场景:错误状态码(例如404、405 Http的常见状态码等等),消息类型,颜色的划分,状态机等等...
🎈本质:枚举类是 java.lang.Enum 的子类,也就是说,自己自定义的枚举类,就算没有显示的继承 Enum ,但是其默认继承了这个类。
2、枚举的使用
2.1、此处是简单的获取枚举对象,直接引用即可
枚举类型也是可以被Switch语句支持的,使用switch进行条件判断时,条件参数一般只能是整型、字符型(需要注意的是使用在于switch条件进行结合使用时,无需使用Month引用)
2.2、Enmu常用的方法
方法名称 | 描述 |
values() | 以Enum数组的形式返回枚举类的所有对象 |
valueOf() | 根据枚举对象变量名获取枚举实例 |
ordinal() | 获取枚举对象对应的序号(索引位置) |
compareTo() | 比较两个枚举对象的定义时的顺序 |
演示方法:
输出结果:
※ java中枚举实际就是一个类,所以在定义枚举时,可以对其中的枚举对象进行传参创建,那么就需要创建对应的构造方法。【需要注意的是,枚举中的所有构造方法都是默认为“私有的”】
如果把构造方法的访问修饰权限改为其他的(例如下面把构造方法改为公有的),编译器会报错
2.3、枚举的原理
对于我们使用enum关键字创建的枚举类型,在经过编译器编译后,会为我们定义的枚举再生成一个相关的枚举类,该类是继承于java.lang.Enum的。
//查看目录下的java文件
EnumTest.java
//利用javac命令编译EnumTest.java
javac -encoidng utf-8 EnumTest.java
//查看生成的class文件,注意有Month.class和EnumDemo.class 两个
Month.class EnumTest.class EnumTest.java
利用javac编译前面定义的EnumTest.java文件后分别生成了Month.class和EnumTest.class文件,而Month.class就是枚举类型,这也就验证前面所说的使用关键字enum定义枚举类型并编译后,编译器会自动帮助我们生成一个与枚举相关的类。
我们再来看看反编译Month.class文件:
//反编译Month.class
final class Month extends Enum
{
//编译器为我们添加的静态的values()方法
public static Month[] values()
{
return (Month[])$VALUES.clone();
}
//编译器为我们添加的静态的valueOf()方法,注意间接调用了Enum也类的valueOf方法
public static Month valueOf(String s)
{
return (Month)Enum.valueOf(review/Month, s);
}
//私有构造函数
private Month(String s, int i)
{
super(s, i);
}
//前面定义的7种枚举实例
public static final Month January;
public static final Month February;
public static final Month March;
public static final Month April;
public static final Month May;
public static final Month June;
public static final Month July;
public static final Month August;
public static final Month September;
public static final Month October;
public static final Month November;
public static final Month December;
private static final Month $VALUES[];
static
{
//实例化枚举实例
January = new Month("January", 0);
February = new Month("February", 1);
March = new Month("March", 2);
April = new Month("April", 3);
May = new Month("May", 4);
June = new Month("June", 5);
July = new Month("July",6)
August = new Month("August", 7);
September = new Month("September", 8);
October = new Month("August", 9);
November = new Month("August", 10);
December = new Month("August", 11);
$VALUES = (new Month[] {
January,February,March,April,May,June,July,August,September,October,November,December;
});
}
}
从反编译的代码可以看出编译器确实帮助我们生成了一个Month类(注意该类是final类型的,将无法被继承)而且该类继承自java.lang.Enum类,该类是一个抽象类除此之外,编译器还帮助我们生成了12个Month类型的实例对象分别对应枚举中定义的12个月份,这也充分说明了我们前面使用关键字enum定义的Month类型中的每种月份枚举常量也是实实在在的Month实例对象,只不过代表的内容不一样而已。
注意编译器还为我们生成了两个静态方法,分别是values()和valueOf(),稍后会分析它们的用法,到此我们也就明白了,使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象,如上述的January枚举类型对应
public static final Month January;,同时编译器会为该类创建两个方法,分别是values()和valueOf()。
2.4、有关枚举的values()方法和valueOf()方法
🤔虽然自定义的枚举都是继承自Enum的,那么在我们调用枚举的values()方法以及valueOf()等方法时,其实这些方法不存在于我们自定义的枚举类中,而是继承自Enum父类。【此处特殊的唯有values()方法在Enum类中并没有找到,而Enum中的valueOf()方法是两个参数的,咱们调用的是只有一个参数的valueOf()方法。】 但为什么我们还可以调用该方法呢?
虽然 values() 方法并没有在 Enum 类中定义,但是我们却可以调用枚举类的values()方法!
这是因为枚举类在编译时会自动添加一些方法和字段,以便实现枚举类型的相关功能。具体来说,当我们定义一个枚举时,编译器会自动将其转换为一个类,该类继承自 Enum 类。在该类中,编译器会自动添加一些方法和字段,以便实现枚举类型的相关功能。其中,values() 静态方法和valueOIf()就是其中编译器添加的静态方法。
values()方法和valueOf(String name)方法是编译器生成的static方法,因此从前面的分析中,在Enum类中并没出现values()方法,但valueOf()方法还是有出现的,只不过编译器生成的valueOf()方法需传递一个name参数,而Enum自带的静态方法valueOf()则需要传递两个方法【且编译器创建的枚举类中的静态方法valueOf()其实简洁的调用了Enum类中的valueOf()方法】
//可以反编译Class文件来查看,编译器生成的枚举类中的静态valueOf()方法
public static Month valueOf(String name) {
return (Month )Enum.valueOf(Month.class, name);
}
因此values()方法和valueOf(String name)方法都是编译器生成的,我们前面调用的一个参数的valueOf()方法,其实就是编译器提供的静态方法,而不是Enum类中的。而由于values()方法是编译器插入到枚举类中来方便实现枚举功能的,如果把枚举类向上转型为Enum类,那么values()方法就无法被调用了,因为Enum类中并没有values方法
3、枚举的优缺点
【重要】:枚举是不可以被继承的,因为枚举对象的构造方法是默认私有的,继承的类是无法访问调用其构造方法,子类没法帮助父类去构造!
优点:
- 可读性好,枚举常量也更简单安全
- 枚举具有内置的方法(values、valueOf、compareTo、ordinal),代码更简便优雅
缺点:
- 不可继承,无法拓展
- 连反射机制也无法干涉到枚举,无法利用反射机制传参调用其构造方法创建实例
4、枚举 和 反射
虽然前面说了,可以借助反射机制在运行时,获取类中的所有信息(即使是私有的成员也可以访问并进行修改),也可以通过反射获取其构造方法去创建实例对象。但是,对于枚举来说(其构造方法都是私有的),即使时反射也无法获取修改枚举类中的信息。
此处以这段代码为例:
package enumdemo;import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum ReflectEnum {
Black("黑色",0),RED("红色",1),BLUE("蓝色",2);
//因为此处的枚举对象有参数,需要提供对应的构造方法(一定是private)
private String color;
private int origial;
ReflectEnum(String color,int origial){
this.color = color;
this.origial = origial;
}
private static ReflectEnum getValue(int origial){
for(ReflectEnum temp:ReflectEnum.values()){
if(temp.origial == origial){
return temp; }
}
return null; }
public static void createEnumReflect(){
//利用反射机制获取枚举类的构造方法,并创建实例
try {
Class<?>c = Class.forName("enumdemo.ReflectEnum");
//注意传入对应的参数,获得对应的构造方法来构造对象,当前枚举类是提供了两个参数分别是String和int
Constructor<?>constructor = c.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
ReflectEnum reflectEnum = (ReflectEnum) constructor.newInstance("黑色",0);
System.out.println(reflectEnum.color+" "+reflectEnum.origial);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
createEnumReflect();
}
}
虽然代码看似没问题,但是编译运行后就报错了![错误:java.lang.NoSuchMethodException: ReflectEnum.(java.lang.String, int),没有找到对应的构造方法方法]
但此处我们传的是两个参数的构造方法,对应其枚举中的构造方法,并没有错,那问题是出在哪?
从上面的枚举的原理,可以知道,使用Enum关键创建的枚举类,在编译器编译后会创建一个类继承子Enum(也可以说是所有的枚举类都默认继承自java.lang.Enum),而且我们调用枚举类的构造方法,那么子类也得帮助父类构建构造方法。但是我们写的类并没有帮助父类构造,这也不意味着我们需要在构造方法中提供super()方法,枚举相对特殊一点,虽然写的是两个参数,但默认还有两个参数。
下面观察Enum的源码:
这意味着,前面我们定义的枚举类中的构造方法,不仅要提供给他两个参数,而且默认后面还要给父类Enum提供两个参数,那么我们在反射的时候获取构造方法就需要4个参数,前两个代表传给ReflectEnum的构造方法,后两个传给Enum父类中的构造方法。
但是修改之后,还是会报错!从报错信息,可知,报错的代码是34行,newInstance()方法出现问题,通过观察这个方法的源码,可以看到:
枚举在此处直接被过滤掉了,此处的(clazz.getModifiers() & Modifier.ENUM) != 0,用来判断该类是否为一个Enum类,如果是直接抛出异常。
- clazz.getModifiers() 返回的是一个整型值,表示该类的访问修饰符(访问修饰符是一个整型值,其中每个二进制位表示一个访问修饰符,可以使用 Modifier 类中的常量来解析该整型值)
- Modifier.ENUM 是一个表示 enum 访问修饰符的常量,它的值是一个二进制数,表示 enum 访问修饰符对应的二进制位
& 运算符表示按位与运算,如果 clazz.getModifiers() 中包含 enum 访问修饰符,则 clazz.getModifiers() & Modifier.ENUM 的结果不为 0,否则为 0。
因此,clazz.getModifiers() & Modifier.ENUM != 0 表示判断该类是否包含 enum 访问修饰符,即判断该类是否为枚举类型。
因此,我们无法利用反射机制来获取枚举类的实例
三、Lambda表达式
1、背景和含义
Lambda表达式是Java SE 8中一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。Lambda 表达式(Lambda expression)是一个 匿名函数(即没有函数名的函数) ,Lambda表达式基于数学中的 λ演算 得名,。Lambda表达式可以表示闭包(Closure)。
1.1、Lambda表达式的语法
Lambda的基本语法为这两种:
(parameters) -> expression
(parameters) -> { statements; }
其中,Lambda表达式可以分为三部分:
- parameters :表示参数列表(此处的参数是函数式接口中的参数),可以为空或包含一个或多个参数。如果只有一个参数,可以省略小括号。这里的参数类型可以明确的声明也可不声明而由JVM隐含的推断。
- expression 或 { statements; } (方法体):表示函数体,可以是表达式,也可以是一条语句或多条语句的代码块,是函数式接口里方法的实现。
- ->: 可以理解成 “被用于”
例子:
//1.无参有返回值的方法,返回10
() -> 10;
//2.有参有返回值的方法,返回两个参数之和
(a,b) -> a+b;
//3.接受两个int类型的方法,返回两者乘积(如果参数的类型都一样,类型名可以省略)
(int a,int b) -> a*b;
//4.接受一个String类型,并执行打印到控制台的操作(因为只有一条语句,{}可以省略)
(String s) -> {System.out.println(s);};
1.2、函数式接口
当一个接口被@FunctionalInterface注解修饰时,说明这个接口是一个函数式接口,有且仅有一个抽象方法。加了这个注解之后,编译器会帮我们去检查这个接口是否符合函数式接口的规范!
函数式接口:其实说白了就是只存在一个抽象方法的接口
在定义函数式接口时要注意,一个函数式接口有且仅有一个抽象方法(反过来说,只要一个接口只有一个抽象方法,那么这个接口就是一个函数式接口),如果有两个及两个以上的抽象方法存在,且有@FunctionalInterface注解修饰时,编译器检查之后程序就会报错.
诸如此类接口,都是函数式接口(接口中只能存在抽象方法)
但是,在 Java 8 中,引入了 default 和 static 修饰符,允许在接口中定义具有默认实现的方法和静态方法。这样做的目的是为了让接口也能够包含一些具体的实现,而不仅仅是定义规范。
default 修饰的方法是具有默认实现的方法,也可以称为默认方法,它可以在接口中定义方法体,实现类可以选择性地覆盖它。
Java 8 引入 default 和 static 方法是为了增强接口的功能和灵活性,让接口不再是一组纯粹的规范,而是具有一些默认的实现。
2、Lambda表达式的使用
2.1、Lambda表达式的使用
此处以一下几个函数式接口为例,来演示Lambda表达式的使用:
@FunctionalInterface
interface NoParameterNoReturn {
void test();
}
//无返回值一个参数
@FunctionalInterface
interface OneParameterNoReturn {
void test(int a);
}
//无返回值多个参数
@FunctionalInterface
interface MoreParameterNoReturn {
void test(int a, int b);
}
//有返回值无参数@FunctionalInterface
interface NoParameterReturn {
int test();
}
//有返回值一个参数@FunctionalInterface
interface OneParameterReturn {
int test(int a);
}
//有返回值多参数@FunctionalInterface
interface MoreParameterReturn {
int test(int a, int b);
}
我们在上面提到过,Lambda表达式本质是一个匿名函数,其实可以理解为:Lambda就是匿名内部类的简化,实际上是创建了一个类,实现了接口,重写了接口的方法 。
✨匿名内部类:是一种没有名字的内部类,它可以在创建一个对象的时候进行定义,通常用于创建只需要使用一次的类。匿名内部类的定义语法比较特殊,它需要在创建对象的时候定义类的结构,并且在定义的同时实现其方法,因此它通常会比较简短。
//匿名内部类的创建
new 接口名/类名(参数列表) {
// 类的成员变量
// 类的构造方法
// 类的成员方法
};
/*
其中,接口名/类名表示要实现的接口或要继承的类,
参数列表是用于调用构造方法时传递的参数。
花括号中的部分表示类的具体实现,包括成员变量、构造方法和成员方法等。
*/
例如,在Java中使用匿名内部类来实现重写Comparator接口中的compare()方法.此处是创建了优先级队列,以该接口来定义队列的比较规则!
因为Comparator接口是一个函数式接口,所以可以用Lambda表达式来表示里面的抽象方法。而如果使用Lambda表达式来简化上述的代码,结果如下:
//以下演示都是带返回值的Lambda表达式
/*
NoParameterReturn noParameterReturn = ()->{return 10;};
该Lambda表达式表示,()不带参数有返回值的一个方法,返回值是10* */
//优化版本:
NoParameterReturn noParameterReturn = ()->10;
System.out.println(noParameterReturn.test());
//==========================================================
//带一个参数有返回值的方法(如果只是返回一个值,可以连return都省略)
OneParameterReturn oneParameterReturn = a->a;
System.out.println(oneParameterReturn.test(15));
//==========================================================
//带两个参数有返回值的方法
MoreParameterReturn moreParameterReturn = (a,b)->a+b;
System.out.println(moreParameterReturn.test(1,2));
2.2、Lambda表达式的精简
其实从上述代码就可以知道,Lambda表达式可以精简哪些地方。
- 如果参数列表只有一个参数,那么小括号()可以省略。 a -> {return a};
- 如果参数列表的参数都是同一类型,可以省略类型名,且所有参数类型都要省略,不能只省略一部分。 (a,b) -> a+b;
- 如果方法体只有一条语句,那么花括号可以省略掉。 a -> System.out.println(a);
- 如果方法体只有一条语句,且是return语句,可以省略掉return和花括号. a -> a;
3、变量捕获
变量捕获(Variable Capturing)是指在编写嵌套代码块时,内部代码块可以访问外部代码块中定义的变量的过程。在 Java 中,Lambda 表达式和匿名内部类都支持变量捕获。
Lambda 表达式中的变量捕获指的是在 Lambda 表达式中引用的外部变量的处理方式。Lambda 表达式可以访问外部变量,但是需要注意的是,Lambda 表达式中引用的外部变量必须是 final 的,即一旦被赋值之后就不能再被修改。
在 Java 8 之前,匿名内部类中引用外部变量的处理方式是将外部变量转换成内部类的成员变量,但是在 Lambda 表达式中,由于 Lambda 表达式没有成员变量的概念,所以需要使用一种不同的方式来处理外部变量的访问。
Lambda 表达式中引用的外部变量可以分为两种类型:局部变量和对象实例变量。对于局部变量,Lambda 表达式会将其复制一份,并存储在 Lambda 表达式内部,这样即使外部变量被修改,Lambda 表达式内部也不会受到影响。而对于对象实例变量,Lambda 表达式可以直接访问,因为对象实例变量是共享的。
3.1 匿名内部类
匿名内部类:是一种没有名字的内部类,它可以在创建一个对象的时候进行定义,通常用于创建只需要使用一次的类。匿名内部类的定义语法比较特殊,它需要在创建对象的时候定义类的结构,并且在定义的同时实现其方法,因此它通常会比较简短。
此处演示上述出现过的优先级队列,并实现Comparator接口重写其Compare()方法
PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2); }
});
3.2、匿名内部类的变量捕获
与 Lambda 表达式不同的是,匿名内部类中引用的外部变量可以是 final、非 final 或没有显式修饰符的局部变量。如果外部变量没有显式修饰符,则默认为 final。但是需要注意的是,如果在匿名内部类中修改非 final 的局部变量,则编译器会报错。
上述代码中的name和i变量就是被捕获的变量,这个变量不是被final修饰就是非final修饰的,如果是非final修饰的,就要保证其变量值不被修改,要是在内部类中对外部变量进行修改,那么编译器就报错!
3.3、Lambda变量捕获
Lambda变量捕获跟匿名内部类的变量捕获差不多,如果在Lambda方法体中修改了外部变量,编译器一样会报错
3.4匿名内部类和Lambda的区别
匿名内部类 | Lambda |
没有名字的类 | 没有名称的方法(匿名函数) |
可以实现拥有任意方法的接口或者抽象类 | 只能使用在仅有一个抽象方法的接口中 |
可以实例化匿名内部类 | Lambda 表达式无法实例化 |
每当我们创建对象时,内存分配都是按需的 | 它驻留在JVM的永久内存中 |
在匿名内部类内部,“ this”始终是指当前匿名内部类对象,而不是外部对象 | 在Lambda表达式内部,“ this”始终引用当前的外部类对象,即包围类对象 |
如果我们要处理多种方法,这是最佳选择 | 如果我们要处理接口,这是最佳选择 |
匿名类可以具有实例变量和方法局部变量 | Lambda表达式只能具有局部变量 |
在编译时,将生成一个单独的.class文件 | 在编译时,不会生成单独的.class文件。只是将其转换为外部类的私有方法 |
Lambda表达式的变量捕获:
- 1、处理方式
Lambda 表达式中引用的外部变量的处理方式是将其复制一份,并存储在 Lambda 表达式内部
- 2、变量类型
Lambda 表达式中引用的外部变量必须是 final 或 effectively final 的,即一旦被赋值之后就不能再被修改。
匿名内部类的变量捕获:
- 1、处理方式
匿名内部类中引用外部变量的处理方式是将外部变量转换成内部类的成员变量
- 2、变量类型
匿名内部类中引用的外部变量可以是 final、非 final 或没有显式修饰符的局部变量。如果外部变量没有显式修饰符,则默认为 final。
4、Lambda在集合中的使用
为了能够让Lambda和Java的集合类集更好的一起使用,集合当中,也新增了部分接口,以便与Lambda表达式对接。
接口 | 新增的方法 |
Collection | removeIf() spliterator() stream() parallelStream() forEach() |
List | replaceAll() sort() |
Map | getOrDefault() forEach() replaceAll() putIfAbsent() remove() replace() computeIfAbsent() computeIfPresent() compute() merge() |
4.1、Collection接口
forEach()方法的演示:【该方法是在接口Iterable中的,该方法主要是对容器中的每一个元素执行action中的操作,通过重写action中的accept方法】
而下面是forEach()方法在Iterable接口中的原型:
而forEach()方法中调用了Consumer接口accept()方法,而Consumer接口是一个函数式接口
(1)采用匿名内部类的表现形式如下:
(2)用Lambda表达式表示为:
4.2、List接口
sort()方法的演示
List.sort()方法的参数是Comparator比较器接口
该方法时指定一个c的比较规则对容器元素去比较进行排序的。
(1)采用匿名内部类的表现形式如下:
(2)用Lambda表达式表示为:
4.3、Map接口
forEach()方法的演示----HashMap中的forEach()方法如下:
该方法通过entrySet()方法映射获取map中的所有的key-value,然后再传入action中的accept方法执行操作
(1)采用匿名内部类的表现形式如下:
(2)用Lambda表达式表示为:
5、总结
Lambda表达式的优点很明显,在代码层次上来说,使代码变得非常的简洁。缺点也很明显,代码不易读。
优点:
- 1. 代码简洁,相较于传统的匿名内部类,可以减少代码的冗余和嵌套,开发迅速
- 2. 方便函数式编程
- 3. 非常容易进行并行计算(Lambda 表达式可以提高代码的执行效率,因为它们可以使用并行处理,从而利用多核处理器的优势。)
- 4. Java 引入 Lambda,改善了集合操作
缺点:
- 1. 代码可读性变差,像有些相同类型的参数,其省略了类型,就很难直观地看出变量的类型
- 2. 在非并行计算中,很多计算未必有传统的 for 性能要高
- 3. 不容易进行调试,因为它们隐藏了一些细节和实现细节。因此需要在使用时注意,尽可能保持代码的可读性和可维护性。