- Stream
- 方法引用
Stream流
JDK1.8引入的新特性。用于解决已有集合类库既有的一些弊端(繁琐的操作)。依赖于Lambda表达式。
从集合中获取需要的元素时,传统做法需要穷举、多次遍历集合。
如果希望对集合中的元素按多个条件进行筛选过滤:
-
将集合A中的元素根据条件1过滤拿到子集合B
-
再将子集合B根据条件2过滤拿到子集合C
使用Steam流的优秀写法
//需要字符串中包含数字1的元素取出 Stream<String> stream = list.stream();//相当于原集合A //Stream<T> filter(Predicate<? super T> predicate) Stream<String> stream2 = stream.filter(str -> str.contains("1"));//相当于子集合B //将元素长度不超过6个的元素取出 Stream<String> stream3 = stream2.filter(str -> str.length()<=6);//相当于子集合C //forEach()进行遍历 //forEach(Consumer<? super T> action) 需要借助Consumer中的accept(T t)方法 steam3.forEach(str -> System.out.println(str));//遍历打印输出集合C 可以使用链式调用优化代码 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流提供了内部迭代的方式,这个流可以直接调用遍历的方法。forEach()
备注:Stream流其实是一个集合元素的函数模型,并不是集合,也不是数据结构。其本身并不存储任何元素(或地址值)。
Steam流是一个来自数据源的元素队列:
元素是特定类型的对象,形成一个队列。Java当中的Stream流并不会存储元素,而是按需计算
数据源是流的来源,可以是集合也可以是数组等容器。
获取一个流对象
java.util.stream.Stream<T>
是JDK1.8引入的新特性,较为常用的接口(并不是函数式接口)
获取一个流对象,有以下常见的操作:
- 所有的Collection集合都可以通过stream()这个默认方法来获取
- Stream接口中含有一个静态方法 of()也可以获取对应的流对象
//把集合转换为Stream流
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();
HashMap<String, Integer> mao = new HashMap<String, Integer>();
Set<String> keySet = mao.keySet();
Stream<String> stream2 = keySet.stream();//key
Collection<Integer> values = mao.values();
Stream<Integer> stream3 = values.stream();//value
Set<Entry<String,Integer>> entrySet = mao.entrySet();
Stream<Entry<String, Integer>> stream4 = entrySet.stream();//entry
//数组转换Steam
Stream<Integer> stream5 = Stream.of(1,2,3,4,5,6);
//可变参数可以是一个数组
String []arr = {"a","b","c","d"};
Stream<String> stream6 = Stream.of(arr);
Stream中的常用方法
两个大类:延迟方法 / 终结方法
延迟方法:返回值类型都是Stream接口自身,支持链式操作
终结方法:返回值类型不是Steam接口本身,因此不能进行链式操作。count方法和forEach方法
forEach方法
void forEach(Consumer<T> consumer);//借助于该函数式接口中的方法accept方法
//Consumer<T> 是一个消费型接口,用来消费一个指定泛型的数据
public class TestforEach{
public static void main(String args []){
//获取一个数据源
Stream<String> stream = Stream.of("abc","aaa","abd","ddd");
//转换操作, 获取想要的结果
stream.forEach(str ->{
if(str.conntains("a")){
System.out.println(str); //abc aaa abd
}
});
}
}
过滤: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就会将集合或数组(数据源)的元素保留;如果条件不满足,filter方法会丢弃该元素
//1.准备数据源
//获取该数据源
String[] arr = {"小孙","小赵","小王","小张","大牛","狗蛋"};
//2.数据转换
//使用Stream流中的方法filter
Stream<String> stream1 = Stream.of(arr);
//3.筛选
stream1.filter(name->name.contains("小")).forEach(name->System.out.println(name));
映射:map
如果你需要将流中的数据映射到另外一个流中,可以使用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类型的数据,这种转换的动作,我们称之为"映射"
//1.准备一个数据源并获取 将字符串的整数转换为int类型的整数
Stream<String> stream = Stream.of("123","234","456","897");
//2.数据转换 apply(T t)
Stream<Integer> stream2 = stream.map(str -> Integer.valueOf(str));
//3.遍历
stream2.forEach(num -> System.out.println(num));
统计个数:count
可以像Collection集合当中的size()方法一样,统计流中的元素个数。通过count方法来实现
long count();//返回此流中元素的数量
该方法返回一个long类型的值代表流中的元素个数(区别于int size())
Stream<Integer> stream = Stream.of(1,2,3,4,5,6);
long count = stream.count();
System.out.println(count);//6
取用流中前几个元素 :limit
limit()方法可以对流中的数据进行限制 --->截取操作,需要一个参数,设定取用流中前max个数。
Stream<T> limit(long maxSize)
返回由此流的元素组成的流,截断长度不超过maxSize
参数是long类型的,截取的长度不能超过流中最大元素个数,否则不进行操作。
Stream<Integer> stream = Stream.of(12,13,14,15,17,88);
//截取流中前五个元素
Stream<Integer> stream2 = stream.limit(5);
//查看
System.out.println(stream2.count);//5
跳过前几个元素:skip
如果你想要跳过前几个元素,取用后几个元素,请使用skip
Stream<T> skip(long n)//在丢弃流的第一个n元素后,返回由此流的其余元素组成的流。
//如果此流包含少于n元素,则将返回空流,长度为0
String []arr = {"123","112","abc","fdh","455"};
Stream<String> stream = Stream.of(arr);
//跳过前三个元素
Stream<String> stream2 = stream.skip(3);
//System.out.println(stream2.count()); //2
stream2.forEach(str -> System.out.println(str)); //fdh 455
组合:concat
如果有两个流并且希望合成为一个流,可以使用concat()静态方法
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) :
//创建一个延迟连接的流,其元素是第一个流的所有元素,后跟第二个流的所有元素
Stream<Integer> stream = Stream.of(52,94,37,45,64,66);
Stream<Integer> stream1 = Stream.of(12,13,14,15,17,88);
Stream<Integer> stream2 = Stream.concat(stream,stream1);
//把两个流合为一个流
方法引用 -> 优化Lambda冗余现象
使用Lambda表达式的时候,传递的是一段解决问题的代码,给什么参数做什么操作。
Lambda冗余场景:
比如,需要打印一个文本内容
//函数式接口
@FunctionalInterface
public interface Printable {
//定义唯一的抽象方法
public abstract void print(String str);
}
/*测试类 定义一个静态方法,静态方法传入函数式接口作为参数,函数式接口有唯一的抽象方法print(),通常我们使用Lambda表达式实现以上需求。
经观察,对字符串进行打印输出的操作方案中,明明已经有了现成的执行方案,System.out对象中有一个println方法,所以引出了方法引用的概念---直接通过对象名引用方法
PrintStream out = System.out;
printString(out::println);
*/
public class MethodReference {
//定义一个静态方法,参数传入函数式接口Printable
public static void printString(Printable p) {
p.print("Hello World!!!!");
}
public static void main(String []args) {
printString( (String str) ->{
System.out.println(str.toUpperCase());//传统Lambda表达式写法
//new MethodReference02().printUppercase(str);//调用对象中的方法
});
PrintStream out = System.out;
//通过对象来引用对应的成员方法
printString(out::println);
/*
* 使用方法引用优化Lambda
* 前提: 对象必须是已经存在的
* 成员方法已经存在
* 所以可以使用对象名来引用成员方法
*
*/
MethodReference02 method = new MethodReference02();
printString(method::printUppercase);
}
}
//提供方法的类
public class MethodReference02 {
//定义一个成员方法,传递一个字符串,把字符串转换为大写输出
public void printUppercase(String str) {
System.out.println(str.toUpperCase());
}
}
注意:其中的双冒号 ': :'就是方法引用,JDK1.8新特性
方法引用符号
双冒号::也被归置为引用运算符
方法引用的使用场景:
- 通过对象名来引用成员方法
- 通过类名引用静态方法
比如: java.lang,Math
全部都是静态方法
(方法内容都被封装到了其他类中)
Lambda表达式写法: d -> Math.abs(d)
方法引用写法:Math ::abs
通过super来引用成员方法
如果存在继承关系,当Lambda中需要使用super调用时,也可以使用方法引用来优化Lambda表达式。super::成员方法
通过this来引用成员方法
this指代当前对象,如果需要引用的方法就是本类当中的成员方法,那么可以使用this::成员方法
类的构造器引用
由于构造器的名称与类名完全相同,所以构造器的引用使用类名称::new的格式表示
数组的构造器引用
数组也是Object子类对象,所以同样具有构造器,只不过语法稍微有些特殊。数组数据类型 [ ]::new
为什么能够如此引用?
可推导即可省略,在Lambda中,无需指定参数类型,无需指定重写的形式 ---> 它们都可以被推导出来,所以就可以省略掉。能够使用方法引用,同样也是可以根据上下文环境进行推导出来的
函数式接口是Lambda的基础,而方法引用是Lambda的优化产品