本篇文章為大家帶來了關於java的相關知識,其中主要介紹了關於函數式程式設計的相關內容,java在最開始是不支援函數式程式設計的,但是在java8這個大版本裡為了支援函數式編程,java引進了許多重要特性,下面一起來看一下,希望對大家有幫助。
推薦學習:《java影片教學》
Java 在最開始時不支援函數式程式設計的,想來也好理解,因為在Java 中類Class 才是第一等公民,這就導致在Java 中實現程式設計不是件那麼容易的事兒,不過雖然難,但是結果我們也已經知道了,在Java 8 這個大版本裡為了支援函數式編程,Java 引入了許多特重要特性,咱們在前面幾篇文章中,分別學習了其中的Lambda 表達式和Stream API 裡的各種流操作,今天這篇文章我們再來梳理一下Java 內置給我們提供的函數式介面。
本文大綱如下:
Java 根據常用需求場景的用例,抽象化了幾個內建的函數式介面給開發者使用,例如Function
、 Supplier
等等,Stream 中各種操作方法的參數或是回傳值類型往往就是這些內建的函數式介面。
例如Stream 中map 操作方法的參數類型就是Function
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
那為什麼我們在平時使用Stream 操作的map 方法時,從來沒有見過宣告這個類型的參數呢?大家可以回顧我們 Stream API 操作那一篇文章裡使用 map 方法的例子,例如下面這個透過 map 方法把流中的每個元素轉換成大寫的例子。
List<String> list = new ArrayList<String>(); Stream<String> stream = list.stream(); Stream<String> streamMapped = stream.map((value) -> value.toUpperCase());
map 方法的參數直接是一個 Lambada 表達式:
(value) -> value.toUpperCase()
這個Lambda 表達式就是Function
介面的實作。
函數式介面的載體通常是 Lambda 表達式,透過 Lambda 表達式,編譯器會根據 Lambda 表達式的參數和回傳值推斷出其實作的是哪個函數式介面。使用Lambda 表達式實現接口,我們不必像匿名內部類那樣--指明類要實現的接口,所以像Stream 操作中雖然參數或者返回值類型很多都是Java 的內置函數式接口,但是我們並沒有顯示的使用匿名類別實作它們。
雖然Lambda 表達式使用起來很方便,不過這也從側面造成了咋一看到那些Java 內建的函數式介面類型時,我們會有點迷惑「這貨是啥?這貨又是啥?的感覺。
下面我們先說一下函數式程式設計、Java 的函數式介面、Lambda 為什麼只能實作函數式介面這幾個問題,把這些東西搞清楚了再梳理Java 內建提供了哪些函數式接口。
函數式程式設計中包含以下兩個關鍵的概念:
即使我們在寫程式的時候沒有一直遵循所有這些規則,但仍然可以從使用函數式程式設計思想編寫程式中獲益良多。
接下來,我們來看看這兩個關鍵概念再 Java 函數程式設計中的落地。
在函數式程式設計範式中,函數是語言中的第一等公民。這意味著可以創建函數的“實例”,對函數實例的變數引用,就像對字串、Map 或任何其他物件的引用一樣。函數也可以作為參數傳遞給其他函數。
在 Java 中,函數顯然不是第一等公民,類別才是。所以 Java 才引入 Lambda 表達式,這個語法糖從表現層上讓 Java 擁有了函數,讓函數可以當作變數的參考、方法的參數等等。為啥說是從表現層呢?因為實際上在編譯的時候 Java 編譯器還是會把 Lambda 表達式編譯成類別。
函數程式設計中,有個純函數(Pure Function)的概念,如果一個函數滿足下列條件,就是純函數:
下面是一個Java 中的純函數(方法)範例
public class ObjectWithPureFunction{ public int sum(int a, int b) { return a + b; } }
上面這個sum()
方法的傳回值只取決於其輸入參數,而且sum()
是沒有副作用的,它不會在任何地方修改函數以外的任何狀態(變數)。
相反,這裡有一個非純函數的例子:
public class ObjectWithNonPureFunction{ private int value = 0; public int add(int nextValue) { this.value += nextValue; return this.value; } }
add()
方法使用成员变量value
来计算其返回值,并且它还修改了value
成员变量的状态,这代表它有副作用,这两个条件都导致add
方法不是一个纯函数
正如我们看到的,函数式编程并不是解决所有问题的银弹。尤其是“函数是没有副作用的”这个原则就使得在一些场景下很难使用函数式编程,比如要写入数据库的场景,写入数据库就算是一个副作用。所以,我们需要做的是了解函数式编程擅长解决哪些问题,把它用在正确的地方。
Java中的函数式接口在 Lambda 表达式那篇文章里提到过,这里再详细说说。函数式接口是只有一个抽象方法的接口(抽象方法即未实现方法体的方法)。一个 Interface 接口中可以有多个方法,其中默认方法和静态方法都自带实现,但是只要接口中有且仅有一个方法没有被实现,那么这个接口就可以被看做是一个函数式接口。
下面这个接口只定义了一个抽象方法,显然它是一个函数式接口:
public interface MyInterface { public void run(); }
下面这个接口中,定义了多个方法,不过它也是一个函数式接口:
public interface MyInterface2 { public void run(); public default void doIt() { System.out.println("doing it"); } public static void doItStatically() { System.out.println("doing it statically"); } }
因为doIt
方法在接口中定义了默认实现,静态方法也有实现,接口中只有一个抽象方法run
没有提供实现,所以它满足函数式接口的要求。
这里要注意,如果接口中有多个方法没有被实现,那么接口将不再是函数式接口,因此也就没办法用 Java 的 Lambda 表达式实现接口了。
编译器会根据 Lambda 表达式的参数和返回值类型推断出其实现的抽象方法,进而推断出其实现的接口,如果一个接口有多个抽象方法,显然是没办法用 Lambda 表达式实现该接口的。
这里扩充一个标注接口是函数式接口的注解@FunctionalInterface
@FunctionalInterface // 标明接口为函数式接口 public interface MyInterface { public void run(); //抽象方法}
一旦使用了该注解标注接口,Java 的编译器将会强制检查该接口是否满足函数式接口的要求:“确实有且仅有一个抽象方法”,否则将会报错。
需要注意的是,即使不使用该注解,只要一个接口满足函数式接口的要求,那它仍然是一个函数式接口,使用起来都一样。该注解只起到--标记接口指示编译器对其进行检查的作用。
Java 语言内置了一组为常见场景的用例设计的函数式接口,这样我们就不必每次用到Lambda 表达式、Stream 操作时先创建函数式接口了,Java 的接口本身也支持泛型类型,所以基本上 Java 内置的函数式接口就能满足我们平时编程的需求,我自己在开发项目时,印象里很少见过有人自定义函数式接口。
在接下来的部分中,我们详细介绍下 Java 内置为我们提供了的函数式接口。
Function
接口(全限定名:java.util.function.Function)是Java中最核心的函数式接口。 Function
接口表示一个接受单个参数并返回单个值的函数(方法)。以下是 Function 接口定义的:
@FunctionalInterface public interface Function<T, R> { R apply(T t); default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } static <T> Function<T, T> identity() { return t -> t; }
Function
接口本身只包含一个需要实现的抽象方法apply
,其他几个方法都已在接口中提供了实现,这正好符合上面我们讲的函数式接口的定义:“有且仅有一个抽象方法的接口”。
Function 接口中的其他三个方法中compse
、andThen
这两个方法用于函数式编程的组合调用,identity
用于返回调用实体对象本身,我们之前在把对象 List 转换为 Map 的内容中提到过,可以回看前面讲 List 的文章复习。
Function
接口用Java 的类这么实现
public class AddThree implements Function<Long, Long> { @Override public Long apply(Long aLong) { return aLong + 3; } public static void main(String[] args) { Function<Long, Long> adder = new AddThree(); Long result = adder.apply(4L); System.out.println("result = " + result); } }
不过现实中没有这么用的,前面说过 Lambda 表达式是搭配函数式接口使用的,用Lambda表达式实现上Function 接口只需要一行,上面那个例子用 Lambda 实现的形式是:
Function<Long, Long> adder = (value) -> value + 3;Long resultLambda = adder.apply(8L); System.out.println("resultLambda = " + resultLambda);
是不是简洁了很多。后面的接口示例统一用 Lambda 表达式举例,不再用类实现占用太多篇幅。
Function
接口的常见应用是 Stream API 中的 map 操作方法,该方法的参数类型是Function
接口,表示参数是一个“接收一个参数,并返回一个值的函数”。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
所以我们在代码里常会见到这样使用 map 操作:
stream.map((value) -> value.toUpperCase())
Predicate 接口 (全限定名:java.util.function.Predicate)表示一个接收单个参数,并返回布尔值 true 或 false 的函数。以下是 Predicate 功能接口定义:
public interface Predicate<T> { boolean test(T t); }
Predicate 接口里还有几个提供了默认实现的方法,用于支持函数组合等功能,这里不再赘述。 用 Lambda 表达式实现 Predicate 接口的形式如下:
Predicate predicate = (value) -> value != null;
Stream API 中的 filter 过滤操作,接收的就是一个实现了 Predicate 接口的参数。
Stream<T> filter(Predicate<? super T> predicate);
写代码时,会经常见到这样编写的 filter 操作:
Stream<String> longStringsStream = stream.filter((value) -> { // 元素长度大于等于3,返回true,会被保留在 filter 产生的新流中。 return value.length() >= 3; });
Supplier 接口(java.util.function.Supplier),表示提供某种值的函数。其定义如下:
@FunctionalInterfacepublic interface Supplier<T> { T get(); }
Supplier接口也可以被认为是工厂接口,它产生一个泛型结果。与 Function 不同的是,Supplier 不接受参数。
Supplier<Integer> supplier = () -> new Integer((int) (Math.random() * 1000D));
上面这个 Lambda 表达式的 Supplier 实现,用于返回一个新的 Integer 实例,其随机值介于 0 到 1000 之间。
Consumer 接口(java.util.function.Consume)表示一个函数,该函数接收一个参数,但是不返回任何值。
@FunctionalInterfacepublic interface Consumer<T> { void accept(T t); }
Consumer 接口常用于表示:要在一个输入参数上执行的操作,比如下面这个用Lambda 表达式实现的 Consumer,它将作为参数传递给它的value
变量的值打印到System.out
标准输出中。
Consumer<Integer> consumer = (value) -> System.out.println(value);
Stream API 中的 forEach、peek 操作方法的参数就是 Consumer 接口类型的。
Stream<T> peek(Consumer<? super T> action); void forEach(Consumer<? super T> action);
比如,Stream API 中的 forEach 操作,会像下面这样使用 Consume 接口的实现
Stream<String> stream = stringList.stream(); // 下面是Lambda 的简写形式 // 完整形式为:value -> System.out.println(value); stream.forEach(System.out::println);
最后再介绍一下 Optional 接口,Optional 接口并不是一个函数式接口,这里介绍它主要是因为它经常在一些 Stream 操作中出现,作为操作的返回值类型,所以趁着学习函数式编程的契机也学习一下它。
Optional 接口是预防NullPointerException
的好工具,它是一个简单的容器,其值可以是 null 或非 null。比如一个可能返回一个非空结果的方法,方法在有些情况下返回值,有些情况不满足返回条件返回空值,这种情况下使用 Optional 接口作为返回类型,比直接无值时返回 Null 要更安全。
接下来我们看看 Optional 怎么使用:
// of 方法用于构建一个 Optional 容器 Optional<String> optional = Optional.of("bam"); // 判断值是否为空 optional.isPresent(); // true // 取出值,如果不存在直接取会抛出异常 optional.get(); // "bam" // 取值,值为空时返回 orElse 提供的默认值 optional.orElse("fallback"); // "bam" // 如果只存在,执行ifPresent参数中指定的方法 optional.ifPresent((s) -> System.out.println(s.charAt(0)));// "b"
Stream 操作中像 findAny、 findFirst这样的操作方法都会返回一个 Optional 容器,意味着结果 Stream 可能为空,因此没有返回任何元素。我们可以通过 Optional 的 isPresent() 方法检查是否找到了元素。Java 编程那些绕不开的接口 这个子系列的文章已经更新完毕,感兴趣的请持续关注,后面还有更多实用、精彩的内容。
推荐学习:《java视频教程》
以上是深入淺析Java函數式編程的詳細內容。更多資訊請關注PHP中文網其他相關文章!