函数式编程
函数式接口
概念:
函数式接口在Java指的是:有且仅有一个抽象方法的接口就称为函数式接口。
函数式接口,适用于函数式编程,在Java当中的函数式编程体现在Lambda表达式,所以函数式接口就是用来服务Lambda表达式,只有确保接口当中有且仅有一个抽象方法,Java中的Lambda才能顺利进行推导。
备注:“语法糖"是指使用更加便利方便,但是原理不变的代码语法。就比如遍历集合时使用for-each语法,其实底层使用的是迭代器,这便是"语法糖”。
格式:
只有确保接口当中有且仅有一个抽象方法即可:
修饰符 interface InterfaceName{
//只能定义一个抽象方法
public abstract 返回值类型 方法名称(参数列表);
//可以定义其他的非抽象的方法
}
示例:
public interface FunctionInterfaceOne {
public abstract void show01();
public default void show02(){
....
}
//void show03(); 有且仅有一个抽象方法,才称为函数式接口
}
@FunctionalInterface注解
与@Override
注解作用类似,Java 8 中专门为函数式接口引入的一个新注解@FunctionalInterface
,该注解主要定义在接口上,一旦使用该注解,编译器将会强制检查该接口是不是一个函数式接口,该接口中是不是有且仅有一个抽象方法,如果不是,编译报错。
@FunctionalInterface
public interface FunctionInterfaceOne {
//定义一个抽象的方法
void method();
//void show();
public default void show02(){}
}
自定义函数式接口的用途
对于自定义的函数式接口,一般用于方法的参数和返回值上。
函数式编程
能够兼顾Java的面向对象的特性基础上,通过Lambda表达式与方法引用,为开发者打开函数式编程的大门。
Lambda表达式的延迟加载
有些场景的代码运行执行后,结果不一定会被使用到,从而造成性能的浪费。而Lambda表达式是延迟执行的,正好可以解决此问题,提升性能。
@FunctionalInterface
public interface BuildLogMessage {
//定义有且只有一个抽象方法
String buildMessage();
}
public class Demo01Logger {
public static void showLog(int level,BuildLogMessage log){
if (level <= 3){
System.out.println(log.buildMessage());
}
}
public static void main(String[] args) {
//定义一些日志信息
String massage1 = "执行Mysql操作";
String massage2 = "执行Java.exe操作";
String massage3 = "执行tomcat.exe操作";
//调用showLog方法,参数是一个函数式接口,可以使用Lambda表达式
showLog(2,()->{
return massage1 + massage2 + massage3;
});
}
}
备注:实际上使用内部类也可以达到这样的效果,只是将代码操作延迟到另外一个对象当中通过调用方法来完成。后面的代码执行取决于前面的条件的判断结果
使用Lanbda作为方法的参数和返回值
在Java当中,Lambda表达式是作为匿名内部类的替代品,如果一个方法的参数是一个函数式接口类型,那么可以使用Lambda表达式进行代替。
java.lang.Runnable
接口,就是一个函数式接口,
代码如下:
public class Demo01Lambda {
//定义一个方法,开启线程的方法
public static void staticThread(Runnable r) {
new Thread(r).start();
}
public static void main(String[] args) {
startThread(()->{
System.out.println("开启一个新线程,线程任务被执行");
});
//优化Lambda
startThread(()->System.out.println("开启一个新线程,线程任务被执行"));
}
}
如果一个方法的返回值类型是一个函数式接口,那么我们可以直接使用一个Lambda表达式。
java.util.Comparator
接口是一个函数式接口
代码如下:
public class Demo2Lambda {
//定义一个方法,方法的返回值是一个函数式接口类型Comparator
public static Comparator<String> createComparator(){
//返回值是一个函数式接口
return new Comparator() {
@Override
public int compare(String o1,String o2) {
//自定义比较的规则,升序/降序
return o1.length() - o2.length();//升序
}
}
//使用Lambda表达式 字符串长度进行升序
return (o1,o2) -> o1.length() - o2.length();
}
public static void main(String[] args){
String[] strs = {"aaa","a","abcdefg","gggg"};
Arrays.sort(strs,createComparator());
System.out.println(Arrars.toString(strs));//a,aaa,gggg,abcdefg
}
}
常用的函数式接口
JDK提供了大量常用的函数式接口,丰富Lambda表达式的使用场景。它们主要在java.util.function
包中被提供。
Supplier接口
java.util.function.Supplier<T>
接口,该接口有且仅有一个无参的方法,
T get()
用来获取一个泛型参数指定类型的对象数据。由于该接口是一个函数式接口,所以我们可以使用Lambda表达式来操作它。
Supplier< T>接口被称之为生产型接口,指定接口的泛型是什么类型,那么接口中的get()方法就会生产什么类型的数据。
public class Demo01Supplier {
//定义一个方法,方法的参数传递一个Supplier<T>接口,泛型指定为Integer,get方法会返回一个int
public static int getNum(Supplier<Integer> sup){
return sup.get();
}
public static void main(String[] args) {
int[] arr = {10,20,30,40,50};
int maxNum = getNum(() -> {
//求出最大值
int max = arr[0];
for (int i : arr) {
if (max <= i) {
max = i;
}
}
return max;
});
System.out.println(maxNum);
}
}
Consumer接口
java.util.function.Consumer< T>
接口刚好和Supplier接口相反,它不是用来生产一个数据,而是消费一个数据。数据的类型有泛型来指定。
accept(T t)方法
意思就是消费一个指定泛型的数据
代码如下:
//定义一个方法,方法的参数传递一个Consumer<T>接口类型,传递一个字符串变量
public static void consumer(String str,Consumer<String> con) {
//使用消费型接口对象,消费传递的字符串
con.accept(str);
}
public static void main(String[] args){
//来调用消费方法consumer
consumer("abcdefg", name->{
//把里面的abcdefg字符串改为大写
String str = name.toUpperCase();
String s = new StringBuffer(str).reverse().toString();
System.out.println(s);
});
}
默认方法:andThen()
如果一个方法的参数和返回值都是Consumer类型,那么就可以实现这样的效果:消费数据的时候,首先做一个消费的操作,在做一个消费的操作,实现组合,可以通过Consumer接口当中的默认方法andThen来实现。
public class Demo03ConsumerAndThen {
//定义一个方法,方法的参数传递一个字符串和两个Consumer接口,Consumer接口的泛型指定为字符串
public static <con2> void consumers(String str, Consumer<String> con1,Consumer<String> con2){
/* con1.accept(str);
con2.accept(str); */
con1.andThen(con2).accept(str);
//规则 con1连接con2,先执行con1消费数据,在执行con2消费数据.
}
public static void main(String[] args){
//由于consumers方法的参数Consumer接口是一个函数式接口,可以使用Lambda表达式
consumers("Java31-中国最棒-123456",(name1)->{
//消费规则
//截取传入的字符串
String sub = name1.substring(0, 6);
System.out.println(sub);
},(name2)->{
String[] strs = name2.split("-");
System.out.println(Arrays.toString(strs));
});
}
}
通过查看源码得知:andThen方法不允许传入一个null对象,否则会抛出空指针异常。
要想把两次消费动作连接起来,需要传入两个Consumer接口,通过andThen()
方法实现一步一步的执行消费动作。
练习:
定义一个字符串数组,存储每一个人的信息 如:“张三,20,郑州市”,存储5个人的信息,
使用Consumer接口,按照指定的格式进行打印输出:姓名:张三;年龄:20,地址:郑州市,
要求将打印姓名的动作作为第一个Consumer接口的规则
将打印年龄的动作作为第二个接口的规则,打印地址的动作作为第三个接口的规则
public class People {
public static void consumer(String[] arr, Consumer<String> name, Consumer<String> age, Consumer<String> address) {
for (String str : arr) {
name.andThen(age).andThen(address).accept(str);
}
}
public static void main(String[] args) {
String[] arr = {"张三,20,郑州市", "李四,22,南阳市", "小丽,18,开封市", "小王,21,郑州市", "王2五,25,郑州市"};
/* consumer(arr, (name) -> {
String[] split = name.split(",");
System.out.print("姓名:" + split[0] + ";");
}, (age) -> {
String[] split = age.split(",");
System.out.print("年龄:" + split[1] + ";");
}, (address) -> {
String[] split = address.split(",");
System.out.println("地址:" + split[2]);
});*/
consumer(arr, name -> System.out.print("姓名:" + name.split(",")[0] + ";"),
age -> System.out.print("年龄:" + age.split(",")[1] + ";"),
address -> System.out.println("地址:" + address.split(",")[0] + ";"));
}
}
Stream流
在Java1.8中,由于Lambda表达式这种函数式编程,JDK引入了一个全新的概念:Stream流
。用于解决已有集合类库的一些弊端的。
传统从集合中获取需要的元素
public class Demo01Stream {
public static void main(String[] args){
//构建一个集合
List<String> list = new ArrayList<String>();
list.add("abc123");
list.add("aaa22");
list.add("bcd125");
list.add("abcd120");
list.add("bbb230");
//需要字符串中包含数字1的元素取出来
List<String> list2 = new ArrayList<>();
for(String str : list){
if (str.contains("1")) {
list2.add(str);
}
}
//需要集合当中字符串长度不能超过6个的元素
List<String> list3 = new ArrayList<>();
for(String str : list2){
if(str.length() <= 6){
list3.add(str);
}
}
//遍历查看最终想要的元素集合
for(String str : list3){
System.out.println(str);
}
}
}
当我们需要对集合当中的元素进行操作的时候,总是需要对集合循环遍历,再次循环遍历…
将集合A中根据条件拿到子集和B,再将子集和B根据条件二筛选子集和C
它只是用来找到你需要元素的一种方式,并不是目的。目的想要取出想要的元素并且循环展示打印出来。以往的方式就是每次循环都需要从头开始遍历,下一次还要从头遍历
Java 1.8可以使用Lambda表达式的衍生物Stream
流来优化你集合遍历的方式。
Stream流的更优写法
public class Demo01Stream {
public static void main(String[] args){
//构建一个集合
List<String> list = new ArrayList<String>();
list.add("abc123");
list.add("aaa22");
list.add("bcd125");
list.add("abcd120");
list.add("bbb230");
//获取Stream流
Stream<String> stream = list.stream();
//需要字符串中包含数字1的元素取出来
Stream stream02 = stream.filter(str -> str.contains("1"));
//需要集合当中字符串长度不能超过6个的元素
Stream stream03 = stream02.filter(str -> str.length() <= 6);
//遍历查看最终想要的集合元素 forEach(Consumer<? super T> action)
//借助于Consumer中的accept(T t) 打印输出
stream03.forEach(str -> System.out.println(str));
//优化
list.stream().filter(str -> str.contains("1"))
.filter(str -> str.length() <=6)
.forEach(str -> System.out.println(str));
}
}
对此流的每个元素执行操作
一般我们把流式思想称之为"生产流水线"
流式思想概述:
整体来看,流式思想类似于工厂中的"生产流水线"。
当需要对多个元素进行操作的时候,尤其是多步操作,考虑到性能以及便利性。首先需要考虑一个"模型"步骤方案。然后按照设计的步骤方案去执行。
比如你对某种类型的商品进行操作,你需要进行过滤,映射,跳过,计数等操作,这也是我们对集合中的元素操作的步骤,这一套步骤我们称之为一种处理方法,而方案就是一种"函数模型"。
方案中操作的每一个步骤,我们称之为一个"流",调用指定的API方法,从一个流中转换为另一个流。
都有对应的API方法,filter,map,skip,count都是对函数模型进行操作。
当我们使用一个流的时候,通常需要包含三个基本步骤:
①:获取一个数据源;
②:数据转换;
③:执行操作获取想要的操作结果。
每次转换原有的Stream对象,返回一个新的Stream对象。这样我们可以像链条一样进行操作。
Stream流和以往的Collection集合有所不同。Stream操作有两个基础的特征:
- 中间操作都会返回流对象本身,这样多个操作就可以串联成一个管道,如同流式风格,对中间的操作进行优化,比如可以进行延迟执行和短路。
- 内部迭代:以前对集合遍历都是通过迭代器(iterator)或增强for循环,显示的在集合外部进行迭代,这叫外部迭代。Stream流提供了内部迭代的方法,这个流可以直接调用遍历的方法。
- Stream流其实是一个集合元素的函数模型,它并不是集合,也不是数据结构。其本身并不存储任何元素(地址值)
Stream流是一个来自数据源的元素队列:
- 元素式特定类型的对象,形成一个队列。Java当中的Stream并不会存储元素,而是按需计算。
- 数据源:流的来源,可以使集合,也可以是数组等容器。
获取流对象
java.util.stream.Stream< T >
是JDK1.8引入的新特性,较为常用的接口(本身并不是函数式接口)
获取一个流对象,由以下常用方法
-
所有的Collection集合都可以通过
stream()
默认方法来获取 -
Stream接口里面还有静态方法
of()
也可以获取stream流
方法 | |
---|---|
Stream< T > | stream() |
static Stream< T > | of(T t) of(T ...values) |
根据Collection集合获取流对象
只要是Collection集合的实现类或者子类都可以使用stream()方法获取流对象
import java.util.*;
import java.util.stream.Stream;
public class Demo01GetStream {
public static void main(String[] args) {
//把集合转换为Stream流
List<Object> list = new ArrayList<>();
Stream<Object> stream1 = list.stream();
//
HashSet<Integer> set = new HashSet<>();
Stream<Integer> stream2 = set.stream();
//
HashMap<String,String> map = new HashMap<>();
Set<String> keySet = map.keySet();
Stream<String> Stream3 = keySet.stream();
//
Collection<String> values = map.values();
Stream<String> stream4 = values.stream();
//entry(键与值的映射)
Set<Map.Entry<String, String>> entries = map.entrySet();
Stream<Map.Entry<String, String>> stream5 = entries.stream();
//把数组抓换为stream流
Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5, 6);
stream6.filter(num -> num>3).filter(num -> num % 2 == 0).forEach(num -> System.out.println(num));
//可变参数是一个数组
String[] arr = {"a","b","c","d"};
Stream<String> stream7 = Stream.of(arr);
}
}
Stream流属于管道流,每次只能被消费一次,第一个Stream流调用完毕后,数据就会被转移到下一个Stream上,而这时第一个Stream流已经使用完毕,就会关闭了,不能再次调用,如果强制调用,程序就会抛出:java.lang.IllegalStateException: stream has already been operated upon or closed
Stream流中的常用方法
流模型中的操作很多,大致上可以把其中的API方法分为两部分:
- 延迟方法:返回值类型都是Stream流接口本身,因此可以支持链式操作
- 终结方法:返回值就不是Stream流接口本身,因此不能再进行链式操作。比如:count()方法和forEach()方法
返回值 | 方法 |
---|---|
void | forEach(Consumer< T > consumer) :借助于该函数式接口中的方法accept()方法,Consumer< T >是一个消费性接口,用来消费一个指定泛型的数据(遍历流元素) |
Stream< T > | filter(Predicate<? super T> predicate) : 返回由此给定谓词匹配的此流的元素组成的流(返回包含指定元素的流) |
< R > Stream< R > | map(Function<? super T,? extends R> mapper) : 返回由给定函数应用于此流的元素的结果组成的流。(可以将当前流中的T数据转换成另外一种R类型的数据) |
long | count() : 返回此流中的元素数 |
Stream< T > | limit(long maxSize) 返回由此流的元素组成的流,截短长度不能超过maxSize(截取流,返回取得流中的前几个元素的流) |
Stream< T > | skip(long n) 在丢弃流的第n元素后,返回由该流的n元素组成的流。(跳过前几个元素,取用后几个元素) |
Stream< T > | concat(Stream<? extends T> a, Stream<? extends T> b ) 创建一个懒惰连接的流,其元素是第一个流的所有元素,后跟第二个流的所有元素。(把两个流合并成一个) |
forEach()方法
public class TestForEach {
public static void main(String[] args) {
//1.获取一个数据源
Stream<String> stream = Stream.of("aa","abb","ccc","ddd");
//2.转换数据
//3.执行操作获取想要的结果
stream.forEach(str -> {
if (str.contains("a")){
System.out.println(str);
}
});
}
}
//展示结构 aa abb
过滤:filter()
可以通过filter()方法将一个流转换成另外一个子集流
Stream<T> filter(Predicate<? super T> predicate) 返回由此给定微词匹配的此流的元素组成的流
//借助于Predicate函数式接口当中的抽象方法test(T t) 对数据进行过滤
该方法接收一个函数式接口Predicate,可以使用Lambda表达式进行条件的筛选。
Predicate接口
java.util.stream.Predicate
函数式接口,其中唯一的抽象方法
boolean test(T t)
该方法返回布尔类型值,代表指定的条件是否满足,如果条件满足返回true,那么Stream流的方法filter()将集合或数组其中的元素保留下来,如果不满足返回false,将元素过滤
public static void main(String[] args) {
//1.准备一个数据源
//获取该数据源
String[] arr = {"小孙","小王","小赵","老王","涂少","老刘"};
//2.数据转换
//使用Stream流中的方法filter,对姓涂的过滤掉
Stream<String> stream = Stream.of(arr);
Stream<String> stream2 = stream.filter(name -> !name.contains("涂"));
Stream<String> stream3 = stream2.filter(name -> name.startsWith("小"));
stream3.forEach(System.out::println);
/*stream.filter(name -> !name.contains("涂"))
.filter(name -> name.startsWith("小"))
.forEach(name-> System.out.println(name));*/
}
映射:map()
如果你需要将流中的数据映射到另一个流中,
< R > Stream< R > map(Function<? super T,? extends R> mapper) : 返回由给定函数应用于此流的元素的结果组成的流。
该方法接收一个函数式接口Function作为方法参数:可以将当前流中的T数据转换成另外一种R类型的数据。
Function接口
java.util.stream.Function
函数式接口。
R apply(T t)
可以将一种T类型的数据转换成R类型的数据,那么这种转换的动作,我们称为映射
public static void main(String[] args) {
//1.准备一个数据源
//获取数据源
//把String字符串的整数-->int类型的整数
Stream<String> stream = Stream.of("123", "124", "125", "126");
//2.数据转换 把字符串类型的数据转化成int类型的数据, 由于Function式一个函数式接口
//apply(T t)
//Integer.valueOf() 字符转换为Integer类型
Stream<Integer> stream2 = stream.map(str -> Integer.valueOf(str));
//遍历
stream2.forEach(num -> System.out.println(num));
}
统计个数:count()
可以向Collection集合当中的size()一样,统计流中的元素个数,通过count()方法来实现
//返回此流中的元素数
long count();
该方法返回一个long类型的值代表流中的元素个数(区别于size()返回值int值)
public static void main(String[] args){
Stream<Integer> stream = Stream.of(1,2,3,4,5,6);
//统计个数
long count = stream.count();
System.out.println(count);//6
}
取用流中前几个:limit()
limit()方法可以对流中的数据进行限制—>截取操作,需要一个参数,设定取用流中前几个数
limit(long maxSize) 返回由此流的元素组成的流,截短长度不能超过maxSize
参数是一个long类型的,截取的长度不能超过流中最大元素个数:否则不进行操作。
public static void main(String[] args) {
//准备一个数据源
//获取数据源
Stream<Integer> stream = Stream.of(12,13,14,15,16,20,30);
//想要截取流中的前五个元素
Stream<Integer> stream2 = stream.count(5);
//查看流中的元素个数
System.out.println(stream2.count());//5
}
跳过前几个:skip()
如果你希望跳过前几个元素,取用后几个元素,可以使用skip()方法
skip(long n) 在丢弃流的第一个n元素后,返回由该流的n元素组成的流。
如果流中的个数小于n,将会得到一个长度为0的空流,反之流中的个数大于n则会跳过前n个元素
public static void main(String[] args) {
String[] source = {"123","124","125","126","abc","abd","ase"};
Stream<String> stream = Stream.of(source);
//跳过前3个元素
Stream<String> stream2 = stream.skip(4);
/*long count = stream2.count();
System.out.println(count);*/
stream2.forEach(str -> System.out.println(str));
}
组合:concat()
如果由两个流,希望合并成一个流,那么可以使用concat()静态方法
concat(Stream<? extends T> a, Stream<? extends T> b) 创建一个懒惰连接的流,其元素是第一个流的所有元素,后跟第二个流的所有元素。
public static void main(String[] args){
//准备连个数据源
//获取两次数据源
Stream<Integer> stream1 = Stream.of(12,13,14,15);
Stream<Integer> stream2 = Stream.of(1,2,3,4,5,6);
//把两个流合并成一个流
Stream<Integer> stream3 = Stream.concat(stream1,stream2);
//展示结构
stream3.forEach(str -> System.out.println(str));
}