引入lambda
以下都是自己的理解奥,有什么不正确的欢迎指出(●'◡'●),我们要用一个东西,首先的了解为什么用?给我们带来了什么优势?
从book书籍里面筛选名称为“zx”的书籍,我们按照之前的写法:
/**
* 筛选名称
* @param books
* @return
*/
public static List<Book> filterName(List<Book> books){
List<Book> resultBook = new ArrayList<>();
for(Book book:books){
if(book.getAuther().equals("zx")){ //满足名称的条件
resultBook.add(book);
}
}
return resultBook;
}
现在我们要筛选动态的名称,我们就把名称作为参数传递给方法
public static List<Book> filterName(List<Book> books,String bookName){
List<Book> resultBook = new ArrayList<>();
for(Book book:books){
if(bookName.equals(book.getAuther())){ //如果传过来的名称等于 满足条件
resultBook.add(book);
}
}
return resultBook;
}
再比如,现在用户又要添加价格筛选在100元以上的书籍,我们又要修改我们的方法,再把价格作为参数传递过去;
public static List<Book> filterName(List<Book> books,String bookName,Integer price){
List<Book> resultBook = new ArrayList<>();
for(Book book:books){
if(bookName.equals(book.getAuther()) && book.getPrice() > price){ //如果传过来的名称等于 满足条件
resultBook.add(book);
}
}
return resultBook;
}
如果到后面还有筛选条件那我们继续这样添加筛选条件吗?如果筛选组合更更复杂了呢?在如果筛选组合条件变了呢?
那我们怎么针对这种情况做出相应的处理呢?通过上面我们可以发现并且这些函数仅仅是筛选条件的部分不一样,其余部分都是相同的模板代码(遍历集合),这个时候我们就可以将行为进行 参数化处理,让函数仅保留模板代码,而把筛选条件抽离出来当做参数传递进来(也就是说:我们把我们的筛选条件-也就是筛选名称体重-作为参数传过去,这样我们只需要实现接口,就可以完成各种筛选的功能);
在这里,“我们根据名称/价格等筛选条件”就是一个行为,我们把这个行为作为参数传入相应的函数就被称为:行为参数化
在 java 8th 之前,我们通过匿名内部类来实现:
/**
* 1.定义一个过滤的接口 后续想要实现什么过滤方法实现就好了,随便怎么组合都可以 看实际的运用
*/
interface FilterBook{
boolean test(Book book);
}
public static void main(String[] args) {
List<Book> books = Arrays.asList(new Book("zx",100),new Book("chy",200),new Book("zx",150));
//2. 实现具体的筛选名称跟价格 条件
FilterBook filterBook = new FilterBook() {
@Override
public boolean test(Book book) {
if(book.getAuther().equals("zx") && book.getPrice() > 100){
return true;
}
return false;
}
};
//3.调用:在具体调用的地方设置筛选条件,并将上面定义的条件作为参数传递到方法中:
filterByMenthod(books,filterBook);
}
/**
* 根据匿名内部类来筛选 将具体的筛选传递
* @param books
* @return
*/
public static List<Book> filterByMenthod(List<Book> books,FilterBook filterBook){
List<Book> resultBook = new ArrayList<>();
for(Book book:books){
if(filterBook.test(book)){ //调用FilterBook接口的test方法 注意具体的方法实现就看你怎么定义的这个方法实现,所以不同的筛选组合传递过来即可
resultBook.add(book);
}
}
return resultBook;
}
上面的
用的是匿名内部类实现,这样的设计在 jdk 内部也经常采用,比如java.util.Comparator,java.util.concurrent.Callable等,使用这类接口的时候,我们都可以在具体调用的地方用匿名类指定函数的具体执行逻辑,
下面我们用Java 8 lambda的方式来写:非常简洁、方便、同时行为参数让我们的程序极大的增强了可扩展性。
//java8
//过滤名称跟价格
filterByMenthod(books,b->b.getAuther().equals("zx") && b.getPrice() > 100);
//只想过滤名称
filterByMenthod(books,(b)->b.getAuther().equals("zx"));
(注意:这里为什么可以直接传入lambda表达式呢?因为我们定义的接口FilterBook有且仅有一个抽象方法,符合函数式接口就可以使用lambda表达式,下面会详细介绍:函数式接口)
知道了lambda给我们带来了好处,我们就来了解lambda!
了解lambda
语法:
(parameters) -> expression 或 (parameters) ->{ statements; }
特征:
可选类型声明:不需要声明参数类型,编译器可以统一识别参数值
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号
可选的大括号:如果主体包含了一个语句,就不需要使用大括号
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值
实例: 需要注意 lambda 表达式隐含了 return 关键字,所以在单个的表达式中,我们无需显式的写 return 关键字,但是当表达式是一个语句集合的时候则需要显式添加 return 关键字,并用花括号{ } 将多个表达式包围起来,下面看几个例子:
public static void main(String[] args) {
//1.不需要参数
Example1 example1 = () -> 5;
//2.接收一个参数。有一个返回值 一个参数的时候 () 可以省略
Example2 example2 = a->2*a;
Example2 example21 = (a) -> 2*a;
//3.接收2个参数。返回差值
Example3 example3 = (a,b) -> a-b;
//4.接收2个参数,打印值,没有返回值
Example4 example4 = (a,b)->System.out.println("我是字符串"+a+"与数值:"+b);
//当表达式是一个语句集合的时候则需要显式添加 return 关键字,并用花括号{ } 将多个表达式包起来
Example4 example41 = (a,b)->{
if(b > 10){
System.out.println(a);
}
};
}
interface Example1{
int test();
}
interface Example2{
int test(int a);
}
interface Example3{
int test(int a,int b);
}
interface Example4{
void test(String b,int a);
}
通过上面的接口我们可以发现:我们定义的接口都只有一个抽象方法!也称为:函数式接口
lambda 表达式的使用需要借助于 函数式接口,也就是说只有函数式接口出现地方,我们才可以将其用 lambda 表达式进行简化。那么什么是函数接口?
基于函数式接口使用 lambda 表达式
函数接口的定义如下:仅含有一个抽象方法的接口
注意:按照上面的定义如果一个接口如果声明了两个或两个以上的方法就不叫函数式接口,需要注意一点的是 java 8th 为接口的定义引入了默认的方法,我们可以用
default
关键字在接口中定义具备方法体的方法,这个在后面的文章中专门讲解,如果一个接口存在多个默认方法,但是仍然仅含有一个抽象方法,那么这个接口也符合函数式接口的定义。
当我们添加default方法时,是可以使用lambda表达式的,没有影响的
当我们添加第二个抽象方法的时候,上面我们使用的lambda表达式就会报错
@FunctionalInterface注解
用于标记该接口是一个函数式接口,不过该注解是可选的,当添加了该注解之后,编译器会限制了该接口只允许有一个抽象方法,否则报错,所以推荐为函数式接口添加该注解。
当我们使用@FunctionalInterface注解时,添加default方法是没有影响的
当我们使用@FunctionalInterface注解时,再次添加一个抽象方法的时候就会发现改注解会报错!
最后最后:
Java8 已经为我们添加了很多函数式接口,(下面是从百家号作者:BaiduSpring 转载过来的,)
jdk 自带的函数式接口
jdk 为 lambda 表达式已经内置了丰富的函数式接口,如下表所示(仅列出部分):
其中最典型的三个接口是Predicate<T>、Consumer<T>,以及Function<T, R>,其余接口几乎都是对这三个接口的定制化,下面就这三个接口举例说明其用处,针对接口中提供的逻辑操作默认方法,留到后面介绍接口的 default 方法时再进行说明。
Predicate<T>
Predicate 的功能类似于上面的 AppleFilter,利用我们在外部设定的条件对于传入的参数进行校验并返回验证通过与否,下面利用 Predicate 对 List 集合的元素进行过滤:
上述方法的逻辑是遍历集合中的元素,通过 Predicate 对集合元素进行验证,并将验证不过的元素从集合中移除。我们可以利用上面的函数式接口筛选整数集合中的偶数:
Consumer<T>
Consumer 提供了一个 accept 抽象函数,该函数接收参数并依据传递的行为应用传递的参数值,下面利用 Consumer 遍历字符串集合并转换成小写进行打印:
利用上面的函数式接口,遍历字符串集合并以小写形式打印输出:
Function<T, R>
Funcation 执行转换操作,输入类型 T 的数据,返回 R 类型的结果,下面利用 Function 对字符串集合转换成整型集合,并忽略掉不是数值型的字符:
下面利用上面的函数式接口,将一个封装字符串的集合转换成整型集合,忽略不是数值形式的字符串:
2.2.3 一些需要注意的事情
类型推断
在编码过程中,有时候可能会疑惑我们的调用代码会具体匹配哪个函数式接口,实际上编译器会根据参数、返回类型、异常类型(如果存在)等因素做正确的判定。在具体调用时,一些时候可以省略参数的类型以进一步简化代码:
局部变量
上面所有例子中使用的变量都是 lambda 表达式的主体参数,我们也可以在 lambda 中使用实例变量、静态变量,以及局部变量,如下代码为在 lambda 表达式中使用局部变量:
上述示例我们在 lambda 中使用了局部变量 weight,不过在 lambda 中使用局部变量还是有很多限制,学习初期 IDE 可能经常会提示我们
Variable used in lambda expression should be final or effectively final
的错误,即要求在 lambda 表达式中使用的变量必须 显式声明为 final 或事实上的 final 类型。
为什么要限制我们直接使用外部的局部变量呢?主要原因在于内存模型,我们都知道实例变量在堆上分配的,而局部变量在栈上进行分配,lambda 表达式运行在一个独立的线程中,了解 JVM 的同学应该都知道栈内存是线程私有的,所以局部变量也属于线程私有,如果肆意的允许 lambda 表达式引用局部变量,可能会存在局部变量以及所属的线程被回收,而 lambda 表达式所在的线程却无从知晓,这个时候去访问就会出现错误,之所以允许引用事实上的 final(没有被声明为 final,但是实际中不存在更改变量值的逻辑),是因为对于该变量操作的是变量副本,因为变量值不会被更改,所以这份副本始终有效。这一限制可能会让刚刚开始接触函数式编程的同学不太适应,需要慢慢的转变思维方式。
实际上在 java 8th 之前,我们在方法中使用内部类时就已经遇到了这样的限制,因为生命周期的限制 JVM 采用复制的策略将局部变量复制一份到内部类中,但是这样会带来多个线程中数据不一致的问题,于是衍生了禁止修改内部类引用的外部局部变量这一简单、粗暴的策略,只不过在 8th 之前必须要求这部分变量采用 final 修饰,但是 8th 开始放宽了这一限制,只要求所引用变量是 “事实上” 的 final 类型即可。
三. 方法引用
方法引用可以更近一步的简化代码,有时候这种简化让代码看上去更加直观,先看一个例子:
方法引用通过 ::将方法隶属和方法自身连接起来,主要分为三类:
静态方法
参数的实例方法
外部的实例方法