初识函数式编程与函数式接口(一)
目前大部分的 JAVA8 的教程一上来就给大家将 Lambda 表达式,方法引用,给大家搞得云里雾里,最终导致 JAVA8 学习的不是特别透彻。我们先来了解一下什么时候能用 Lambda 表达式,然后在探究怎么用 Lambda 表达式。
从函数式编程开始
前一章节我们说过,JAVA8 其实是 Java 像其他语言或者一些优秀的框架学习的结果。函数式编程这个概念提出的非常早,有很多语言都是支持函数式编程的。JAVA8 中也对函数式编程做了支持。我们下面要介绍的函数式接口等概念都是围绕函数式编程而生的。
什么是函数式编程?
简单说,
函数式编程
是一种编程范式
(programming paradigm),也就是如何编写程序的方法论。这句话是百度百科中给我们的解答,如果单看这句话,我们可能根本理解不了到底什么是函数式编程
。我们不妨使用我们最熟悉的面向对象编程(
命令式编程
)来类比一下,让大家能对函数式编程有一个简单的概念。命令式编程是针对于计算机硬件的,我们写的每一句话都是一个底层的硬件指令。函数式编程不是我们中的
函数
不是我们平时在 Java 中编写的函数或者方法,它是一种针对于数学的概念,可以将其理解为一个表达式或者公式,或者理解为数据之间的转换关系。我们说 JAVA8 以前不支持函数式编程,那么有哪些具体的体现哪?
- Java 中最重要的部分是类和对象,没有类和对象,我们所要实现的功能也无从谈起。我们所定义的方法或者函数是必须依托于类或者对象来存在的,引用现在比较流行的说法,类和对象是一等公民,方法或者叫函数是二等公民。
- 在 JAVA8 之前,我们无法将一个函数作为参数传递给一个方法,也无法声明一个返回函数的方法。
JAVA8 对函数式编程做了哪些支持?
JAVA8 通过函数式接口和 Lambda 表达式为我们引入了函数式编程的概念,从而使函数在 Java 中也变为了一等公民。
函数式编程有些好处?
在 Java 中,我们所谓的变量都是可以进行状态变化的,比如一个“学生”对象的年龄属性是可以按照我们的需求去更改的。而函数式编程中变量则于 Java 中完全不同,就是数学中定义的变量,变量的值也是不可更改的。函数式编程虽然最终也是被编译成机器指令去运行,但是它从思想方面为我们带来了许多好处,比如,函数的结果不会因为调用的时间和位置的变化而变化,函数的运行是独立于外部环境的。最大的好处就是不可变。因为不可变性,我们在进行多线程操作时就不用额外的加锁,也不用关心各种线程安全问题,非常适合用来处理并发问题。这和 Java 中不可变对象概念相同(Immutable Object)。
我们通过实例来看看 JAVA8 引入函数式编程后到底给我们的编程带来了哪些好处。
我们的项目中经常使用各种条件会对集合中的元素进行过滤。比如我们定义一个学生类,我们可以根据学生的分数来过滤选出一部分学生,通过性别过滤出一部分学生,通过其他条件过滤出一部分学生。
来看看我们最开始的写法:
public static List<Student> selectByMark(List<Student> lists){ List<Student> result = new ArrayList<>(); for(Student s:lists){ if(s.getMark()>60){ result.add(s); } } return result; } public static List<Student> selectBySex(List<Student> lists){ List<Student> result = new ArrayList<>(); for(Student s:lists){ if(s.getSex().equals("Male")){ result.add(s); } } return result; } ...... ....... selectByName()
按照这种方式写我们可能会根据不断变化的需求写出许多像上面一样的代码。这样的代码不仅冗余而且无趣,相当于在浪费程序员的时间。
如果你是一个有些经验的程序员,你肯定不会使用上面的方法去写,而是使用设计模式中的策略模式来实现。
//策略接口,一个过滤器 public interface Filter{ boolean filter(Student s); } //不同的实现类,即对学生的过滤方法 public class MarkFilter implements Filter{ @Override public boolean filter(Student s){ return s.getMark() > 60; } } public class SexFilter implements Filter{ @Override public boolean filter(Student s){ return s.getSex().equals("Male"); } } ......... //给用户提供的静态方法 public static List<Student> select(List<Student> lists, Filter f){ List<Student> result = new ArrayList<>(); for(Student s:lists){ if(f.filter(s)){ lists.add(s); } } return result; }
使用了上面的策略模式后,其实并没有好多少,我们只不过是把原来的静态方法换成了类。而在实际编程过程中我们经常会使用匿名内部类来实现(伪代码),这样写的好处是,我们只定义了一个框架或者行为方式,具体怎么实现交给调用者去实现。我们定义行为,使用者决定细节。
//调用者需要过滤的时候需要写的代码,Filter 不需要实现类,只需要在调用时 new 一个匿名内部类就可以了。 List<Student> lists = new ArrayList<>(); //lists.add(); List<Student> res = select(lists, new Filter() { @Override public boolean filter(Student s) { return s.getMark()>60; } });
最后给出使用函数式编程的方式的解决方案,使用 Lambda 表达式的方式,它比匿名内部类节省更多空间,代码也更整洁,更易于理解(伪代码)。
//重点 List<Student> result = select(lists,m->m.getMark()>60);
原来面向对象编程时,我们的方法是用来处理逻辑的。而现在逻辑或者算法是调用时动态提供的。现在看方法只能看到一种通用或者宏观的逻辑。提供了一种更高层次的抽象化。函数式编程,写的更少,做的更多。
原来我们使用面向对象编程时,当我们的函数写完了之后,函数的功能就已经固定下来了,我们想实现找到偶数就要写一个方法,找到奇数要写一个方法,找到大于5的数字要写一个方法。函数式编程中,方法的实现是一种抽象的概念,具体实现要使用者或者调用端来实现的。
函数式接口
了解了函数式编程的概念之后,我们来看看 JAVA8 为了支持函数式编程做了哪些努力。
首先,在 Java 中函数式编程的体现就是 Lambda 表达式
(下一章节我们会具体介绍,Lambada 表达式
首先它是一个表达式,这就是函数式编程的概念,它就是匿名函数)。
但是 Lambda 表达式在 Java 中不是能随处用的,如果将这个表达式放入到 Java 语言中那?Java 定义了一个承载 Lambda 表达式的接口,因为这个接口是为函数式编程设计的接口,所以叫函数式接口
。
下面我们来帮大家理解一下函数式接口:
回顾匿名内部类:
new Thread(new Runnable() { @Override public void run() { System.out.println("this is a thread"); } }).start();
这是我们比较熟悉的启动一个线程的方法,Thread 类里面接收一个 Runnalbe 接口参数,我们需要定义一个接口实现类传进来,这里直接使用匿名内部类来实现。
这里传入 Runable 接口的意义是什么那?或者说设计者为什么要这样设计那?
这样设计是在启动一个线程时,设计者只希望我们把线程具体要执行的任务传进去就好了,其他的功能都已经封装好了,使用者并不需要关心。
这个接口封装了一个行为,不用传入参数,不用返回参数的行为,行为的具体实现也就是线程具体执行的任务需要调用者传入。这里就引入了一个概念,函数式接口是对某些行为的抽象,一个函数式接口只能有一个行为。
类似的是 Callable 接口,他也是线程的实现方式,它封装的行为是不需要传入参数,但需要一个返回值的行为。
通过 Runnable 接口引出函数式接口:
JAVA8 在这个接口带来的改变是在上面加了一个 FunctionalInterface
函数式接口的注解,这证明 Runnable 已经是一个函数式接口了,里面只有一个不用传入参数,也不用返回参数的抽象方法。
@FunctionalInterface public interface Runnable { /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run(); }
函数式接口的定义:
通过函数式接口的定义我们看不出什么,到底什么是函数式接口,我们可以看源码里面的英文注释,这里我们直接给出定义。
- 函数式接口首先是一个接口。
- 函数式接口必须只有一个抽象函数。
- 除了一个抽象方法之外,函数式接口允许有默认方法和静态方法(接口中的默认方法和静态方法也是 JAVA8 引入的,下面的章节我们会详细讲解)。
- java.lang.Object 的抽象方法是不计算在内的,就是在函数式接口中可以定义许多 java.lang.Object 的抽象方法。
- 如果我们的接口满足上面的条件但是没有
@FunctionalInterface
注解依然会被编译器认作函数式接口。
看完定义之后我们在回头来看 Callable 接口和 Runaable 接口,他们完全满足函数式接口的定义。
@FunctionalInterface public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }
如何创建接口的实例那?
既然是接口,我们在使用的使用一定需要创建接口的实例。函数式接口有三种创建方法,Lambda 表达式
,方法引用
和构造方法
的方式来实现,下面的章节会具体讲解 Lambda 表达式和方法引用的使用。
我们反过来想,Lambda 表达式的作用就是用来创建函数式接口的实例。
**Lambda 表达式的实现:
我们下面的章节会详细讲,大家可以先有一个简单的印象。
//还是上面那个启动一个线程的例子,因为 Runnable 是一个函数式接口,所以我们可以使用 Lambda 表达式来创建接口实例。 new Thread(() -> System.out.println("this is a thread")).start();
方法引用的实现:
这个可能大家理解会非常困难,我们下面的章节会详细讲,大家可以先有一个简单的印象。
//Lambda 表达式的写法 list.forEach(item-> System.out.println(item)); //方法引用,相当于调用 PrintStream 类的 println 方法 list.forEach(System.out::println);
构造方法的实现就是我们平常使用的匿名内部类或者实现接口,这里就不详细介绍了。
通过这一章节,我们已经了解了 JAVA8 引入的函数式编程以及具体怎样实现。并且理清了函数式编程的体现 Lambda 表达式与函数式接口的关系,并且介绍了函数式接口的定义和构建方法,还引出了方法引用这个概念。