• Stream流、方法引用


    Stream

      JDK 1.8引入的新特性。用于解决已有集合类库既有的一些弊端。依赖于Lambda 表达式

    传统从集合中获取需要的元素

    代码如下:

    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<String>();// abc123 bcd125 abcd120
            for (String str: list) {
                if (str.contains("1")) {
                    list2.add(str);
                }
            }
            
            // 需要集合当中字符串长度不能超过6个的元素取出来
            List<String> list3 = new ArrayList<String>();
            for (String str: list2) {
                  if( str.length() <= 6) {
                      list3.add(str);
                  }
            }
            
            // 遍历查看最终想要的元素集合
    		for(String str: list3) {
                System.out.println(str);
            }
        }
    }
    

    经过观察需要对源集合中的元素进行不断的循环,而且每次循环都需要从头到尾进行遍历

    如果希望对集合中的元素进行筛选过滤:

    1. 将集合A中根据条件一过滤拿到子集合B;

    2. 再将子集合B根据条件二过滤筛选为子集合C。

    Stream流的更优写法

    借助于Stream流对象中的API方法:

    public class TestStream {
         public static void main(String[] args) {
            // 构建一个集合  of("abc123","aaa22")
            List<String> list = new ArrayList<String>();
            list.add("abc123");
            list.add("aaa22");
            list.add("bcd125");
            list.add("abcd120");
            list.add("bbb230");
     		
           // 需要字符串中包含数字1的元素取出来
          Stream<String> stream = list.stream();// 源集合A
          // Stream<T> filter(Predicate<? super T> predicate) 返回由与此给定谓词匹配的此流的元素组成的流。 借助于它的方法 booolean test(T t)
         Stream<String> stream02 = stream.filter(str -> str.contains("1"));  //子集合B
          // 需要集合当中字符串长度不能超过6个的元素取出来
    	 Stream<String> stream03 = stream02.filter(str -> str.length() <= 6); // 子集合C
         // void forEach(Consumer<? super T> action)对此流的每个元素执行操作。
         // 借助于Consumer中的accept(T t)  打印输出
         stream03.forEach(str -> System.out.println(str));// 遍历子集合C 打印输出集合中的每个元素
         // 再次优化
         list.stream().filer(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> 是JDK 1.8引入的新特性,较为常用的接口(本身并不是函数式接口)

    获取一个流对象,有以下常见的操作:

    • 所有的Collection集合都可以通过stream()默认方法来获取。

    • Stream接口里面含有一个静态方法of也可以获取对应的流对象。

    根据Collection集合或者of方法获取流对象

    只要是Collection集合的实现类或者子接口都可以调用stream默认方法获取流对象

    代码如下:

    public static void main(String[] args) {
    		// 把集合转换为Stream流
    		List<String> list = new ArrayList<>();
    		Stream<String> stream1 = list.stream();
    		
    		HashSet<Integer> set = new HashSet<>();
    		Stream<Integer> stream2 = set.stream();
    		
    		HashMap<String, String> map = new HashMap<>();
    		// map中的key存储到一个set中
    		Set<String> keySet = map.keySet();
    		Stream<String> stream3 = keySet.stream();
    		
    		// 把map中的value值存储到一个Collection集合中
    		Collection<String> values = map.values();
    		Stream<String> stream4 = values.stream();
    		
    		// 把map中的key和value值一起存储到entry(键与值的映射)中
    		Set<Entry<String,String>> entrySet = map.entrySet();
    		Stream<Entry<String, String>> stream5 = entrySet.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流中的常用方法

    流模型中的操作很多,大致上可以把其中的api方法分成两部分:

    • 延迟方法:返回值类型都是Stream接口自身,因此可以支持链式操作。

    • 终结方法:返回值就不是Stream接口自身,因此不能再进行链式操作。比如:count方法和forEach方法

    forEach方法
     void forEach(Consumer<T> consumer);// 借助于该函数式接口中的方法accept方法
    // Consumer<T> 是一个消费型接口 用来消费一个指定泛型的数据。
    

    代码如下:

    public class TestForEach {
        public static void main(String[] args) {
            // 1. 获取一个数据源
          Stream<String> stream =  Stream.of("abc","aaa","abd","bcd","ddd");
            // 2. 转换数据
            // 3. 执行操作获取想要的结果
            stream.forEach(str -> {
                if (str.contains("a")){
                    System.out.println(str);
                }
            });
        } 
    }
    // 展示的结果
    abc aaa abd
    Gof-23
    算法
    垃圾回收器GC   
    LocalDate和LocalDateTime    
    
    过滤: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,那么filter方法会舍弃该元素。

    代码如下:

    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(name ->System.out.println(name));
    		stream2.filter(name -> !name.contains("少")).forEach(name ->System.out.println(name));
    		 
    		/* Stream流属于管道流,每次只能被消费一次
    		 * 第一个Stream流调用完毕后,数据就会被转换到下一个Stream上
    		 * 而这时第一个Stream流已经使用完毕,就会关闭了。
    		 * 所以第一个Stream就不能再调用方法了。
    		 * 如果你强制调用方法,程序就会抛出非法状态异常
    		 * java.lang.IllegalStateException: stream has already been operated upon or closed
    		 * stream.filter(name -> !name.contains("涂"))
    		      .filter(name -> name.startsWith("小"))
    		      .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类型的数据,那么这种转换的动作,我们称之为"映射"。    
    

    代码如下:

    public static void main(String[] args) {
    		// 1. 准备一个数据源
    		// 获取数据源
    		// 把String字符串的整数-->int类型的整数
    		Stream<String> stream = Stream.of("123","124","125","126","120");
    		// 2. 数据转换 把字符串类型的数据转换成int类型的数据 由于Function是一个函数式接口,所以可以使用Lambda表达式
    		// apply(T t) 
    //		Stream<Integer> stream2 = stream.map(str -> Integer.valueOf(str));
    		Stream<Integer> stream2 = stream.map((String str) -> {
    			return Integer.valueOf(str);
    		});
    		// 遍历
    		stream2.forEach(num -> System.out.println(num));
    	}
    
    统计个数:count

    可以像Collection集合当中的size()一样,统计流中的元素个数,通过count方法来实现。

    // 返回此流中的元素数。 
    long count();
    

    该方法返回一个long类型的值代表流中的元素个数(区别于size()返回值int值)

    代码如下:

    public class Demo01Count {
        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()方法可以对流中的数据进行限制--->截取操作,需要一个参数max,设定取用流中前max个数。

    Stream<T> limit(long maxSize)  //返回由此流的元素组成的流,截短长度不能超过 maxSize 。  
    

    参数是一个long类型的,截取的长度不能超过流中最大元素个数;否则不进行操作。

    代码如下:

    public class Demo02Limit {
        public static void main(String[] args){
            // 准备一个数据源
            // 获取数据源
           Stream<Integer> stream =  Stream.of(12,13,14,15,16,20,30);
           // 想要截取流中的前五个元素
           Stream<Integer> stream02  = stream.limit(5);
           // 查看流中的元素个数
            System.out.println(stream02.count());// 5
        }   
    }
    
    跳过前几个:skip

    如果你希望跳过前几个元素,取用后几个元素,可以使用skip方法来实现。

    Stream<T> skip(long n) // 在丢弃流的第一个 n元素后,返回由该流的 n元素组成的流。  
    

    如果流中的当前个数小于n,你将会得到一个长度为0的空流;反之流中的个数大于n,则会跳过前n个元素。

    代码如下:

    public static void main(String[] args) {
    		// 1.
    		String[] source = {"123","124","125","126","abc","abd","abe"};
    		Stream<String> stream = Stream.of(source);
    		// 2. 跳过前3个元素
    		Stream<String> stream2 = stream.skip(source.length+1);// 空流
    		// 3. 
    		//stream2.forEach(str -> System.out.println(str));// abc abd abe
    		long count = stream2.count();
    		System.out.println(count);// 0
    	}
    
    组合:concat

    如果有两个流,希望合并成一个流,那么可以使用concat静态方法

    // 创建一个懒惰连接的流,其元素是第一个流的所有元素,后跟第二个流的所有元素。 
    static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) 
    

    代码如下:

    public class Demo04Concat {
        public static void main(String[] args){
            // 准备二个数据源
            // 获取两次数据源
           Stream<Integer> stream   =   Stream.of(12,13,14,15,16,20,30);
           Stream<Integer> stream02 =   Stream.of(1,2,3,4,5,6,7);
           // 把两个流合并成一个流
           Stream<Integer> stream03 =   Stream.concat(stream,stream02); 
           stream03.forEach(num -> System.out.print(num + " "));
           //展示结果: 12,13,14,15,16,20,30,1,2,3,4,5,6,7  
        }   
    }
    

    方法引用-->优化Lambda冗余现象

    在使用Lambda表达式的时候,实际我们传递的是一段解决问题的代码,给什么参数做什么操作。

    Lambda冗余的场景

    比如们想要打印一个文本内容。

    // 准备一个函数式接口
    @FunctionalInterface
    public interface Printable {
    	// 定义唯一的抽象方法
       public abstract void print(String str);	
    }
    

    准备一个测试类

     public class Demo01Method {
        // 定义一个静态的方法,方法的参数传递一个函数式接口
    	public static void printString(Printable p) {
    		p.print("Hello World");
    	}
    	
    	public static void main(String[] args) {
    		
    		// 传统的Lambda表达式写法
    		printString((String str) -> {
    			 System.out.println(str.toUpperCase());// Hello World ---->HELLO WORLD
    			/*Demo02Method method = new Demo02Method();
    			method.pringUpperCaseString(str);// HELLO WORLD	
    */		});
    		// 打印流对象已经确定
    		PrintStream printStream =  System.out;
    		// 通过对象来引用对应的成员方法
    		printString(printStream::println);
    		
    		/*
    		 * 使用方法引用优化Lambda
    		 * 对象已经是存在的method
    		 * 成员方法也是已经存在的pringUpperCaseString
    		 * 所以我们就可以使用对象名来来引用成员方法
    		 * 
    		 */
    		//首先必须是对象已经存在
    		Demo02Method method = new Demo02Method();
    		printString(method::pringUpperCaseString);// HELLO WORLD
    	}   
     }    
    

    在测试类中,定义一个静态方法,静态方法传递一个函数式接口Printable,函数式接口当中定义了唯一一个抽象方法print,这个print方法接收一个字符串参数,目的就是为了打印接收的字符串参数。通常我们可以使用Lambda表达式来说实现以上需求,

    但是经过我们观察,对字符串进行控制台打印输出的操作方案中,明明已经有了现成的执行方案,System.out对象中有一个方法println(String str),所以我们可以直接通过对象名来引用该方法println来实现在控制台打印输出字符串内容。

    // 打印流对象已经确定
    PrintStream printStream =  System.out;
    // 通过对象来引用对应的成员方法
    printString(printStream::println);
    

    注意:其中的双冒号::写法,被称之为“方法引用”,两个冒号是一种新语法。

    方法引用符号

    双冒号:: 也被归置为引用运算符,

    使用方法引用的使用场景

    通过对象名引用成员方法
    // 先准备一个类,类中需要定义一个成员方法
    public class Demo02Method {
    	// 定义一个成员方法,传递一个字符串,把字符串转换为大写输出
    	public void pringUpperCaseString(String str) {
    		System.out.println(str.toUpperCase());
    	}
    }
    // 准备一个函数式接口
    @FunctionalInterface
    public interface Printable {
    	// 定义唯一的抽象方法
       public abstract void print(String str);	
    }
    // 准备测试类
    public class Demo01Method {
    
    	// 定义一个静态的方法,方法的参数传递一个函数式接口
    	public static void printString(Printable p) {
    		p.print("Hello World");
    	}
    	
    	public static void main(String[] args) {
    		/*
    		 * 使用方法引用优化Lambda
    		 * 对象已经是存在的method
    		 * 成员方法也是已经存在的pringUpperCaseString
    		 * 所以我们就可以使用对象名来引用成员方法
    		 */
    		//首先必须是对象已经存在
    		Demo02Method method = new Demo02Method();
    		printString(method::pringUpperCaseString);// HELLO WORLD
    	}
    }
    
    通过类名引用静态方法

    比如:java.lang.Math 类中存放的都是静态方法

    // 定义一个函数式接口
    @FunctionalInterface
    public interface Demo01MathStaticMethod {
    	// 定义一个抽象方法
    	double calculateAbs(double d);
    }
    
    // 定义一个测试类
    public class Dem02MethodStatic {
    
    	// 定义一个静态方法,该方法中传递一个函数式接口,再次传递一个浮点数
    	public static double calc(double d,Demo01MathStaticMethod math) {
    		return math.calculateAbs(d);
    	}
    	
    	public static void main(String[] args) {
    		// 传统的Lambda表达式写法
    		double num = calc(-3.14, (d) -> {
    			return Math.abs(d);
    		});
    		System.out.println(num);// 3.14
    		
    		/*
    		 * 使用方法引用进行优化Lambda
    		 * 首先类名已经确定的
    		 * 类中定义的静态方法是已经确定的
    		 * 使用类名引用类中的静态方法
    		 */
    		double d = calc(-3.14, Math::abs);
    		System.out.println(d);// 3.14
    	}
    }
    

    备注:

    ​ Lambda表达式写法:d -> Math.abs(d)

    ​ 方法引用写法: Math::abs

    这两种写法是等价的。

    通过super来引用成员方法

    如果存在继承关系,当Lambda中需要使用super调用时,也可以使用方法引用来优化Lambda表达式。

    // 定义一个父类
    public class Animal {
    	// 定义一个成员方法 交流的方法
    	public void talk() {
    		System.out.println("hello 我是一只动物!");
    	}
    }
    // 定义一个函数式接口
    @FunctionalInterface
    public interface Meet {
    	// 定义一个抽象方法  见面的方法
    	void meet();
    }
    // 定义一个子类
    public class Cat extends Animal{
    
    	@Override
    	public void talk() {
    		System.out.println("hello 我是一只猫!");
    	}
    
    	// 定义一个方法 方法的参数传递一个函数式接口Meet
    	public void meet(Meet m){
    		m.meet();
    	}
    	
    	// 定义一个成员方法 沟通的方法
    	public void commun() {
    		// 传统的Lambda表达式写法
    		meet(() -> {
    			// 创建父类的对象
    			// 调用父类的方法
    			Animal animal = new Animal();
    			animal.talk();
    		});
    		// 使用父类当中的方法 直接用super来调用
    		meet(() -> super.talk());
    		
    		/*
    		 * 使用super关键字来引用成员方法
    		 * super已经存在的
    		 * 父类当中的成员方法talk已经存在的
    		 * 可以使用super引用父类当中的成员方法
    		 * 
    		 */
    		// 优化的结果
    		meet(super::talk);
    	}
    	public static void main(String[] args) {
    		new Cat().commun();
    	}
    }
    
    通过this来引用本类当中的成员方法

    this指代当前对象,如果需要引用的方法就是本类当中的成员方法,那么可以使用this::成员方法格式来优化Lambda表达式

    @FunctionalInterface
    public interface Study {
    	// 定义一个学习的抽象方法
    	void study();
    }
    // 定义一个学生类
    public class Student {
    
    	// 定义一个成员方法,方法的参数传递一个函数式接口Study
    	public void study(Study s) {
    		s.study();
    	}
    	
    	// 定义一个work方法
    	public void work() {
    		System.out.println("我学习我快乐!");
    	}
    	
    	// 定义一个成员方法快乐的方法
    	public void toHappy() {
    		
    		// 传统的Lambda表达式
    		study(() -> {
    			// 创建对象
    			Student student = new Student();
    			student.work();
    		});
    		
    		// 使用this关键字优化Lambda
    		//study(s);
    		//Student student = new Student();
    		study(this::work);
    	}
        
    	public static void main(String[] args) {
    		new Student().toHappy();	
    	}	
    }
    

    类的构造器引用

    由于构造器的名称与类名完全一样,所以构造器的引用使用类名称::new的格式来表示

    // 定义一个Model类
    public class Person {
    	
    	private String name;
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	public Person() {
    	}
    
    	public Person(String name) {
    		super();
    		this.name = name;
    	}
    
    	@Override
    	public String toString() {
    		return "Person [name=" + name + "]";
    	}
    }
    //定义一个函数式接口
    @FunctionalInterface
    public interface PersonCreate {
    	// 定义一个抽象方法
    	Person createPerson(String name);
    }
    public class TestConstructorMethod {
    
    	//定义一个方法,传入一个函数式接口PersonCreate,再传入一个字符串name
    	public static void printPersonName(String name,PersonCreate create) {
    		
    		System.out.println(create.createPerson(name).getName());
    	}
    	
    	public static void main(String[] args) {
    		// 使用传统的Lambda表达
    		printPersonName("小孙", name -> new Person(name));
    		// 使用构造器来优化Lambda表达式
    		printPersonName("小孙", Person::new);
    		/*
    		 *Lambda表达式   name -> new Person(name)
    		 *方法引用: Person::new
    		 * 
    		 */
    	}
    }
    
    数组的构造器引用

    数组也是Object的子类对象,所以同样具有构造器,只不过语法稍微有点区别。

    // 定义一个函数式接口
    @FunctionalInterface
    public interface BuildArrays {
    	// 定义唯一的抽象方法
    	int[] buildArrays(int length);
    }
    
    // 定义测试类
    public class Demo01ArraysConstructorMethod {
    
    	// 定义一个方法,方法中传递一个函数式接口,还要传递一个数组的长度
    	public static int[] buildArrays(int length,BuildArrays buildArrays) {
    		
    		return buildArrays.buildArrays(length);
    	}
    	
    	public static void main(String[] args) {
    		// 先用Lambda表达式来操作
    		int[] arr01 =   buildArrays(10, length -> new int[length]);
    		System.out.println(arr01.length);// 10
    		
    		// 数组的构造器引用来优化Lambda表达式
    		int[] arr02  = buildArrays(10, int[]::new);
    		System.out.println(arr02.length);// 10
    		/*
    		 * Lambda表达式:length -> new int[length]
    		 * 方法引用:int[]::new
    		 * 这两种写法是等价的
    		 * 
    		 */
    	}
    }
    
    

    为什么?

    推导与省略

    如果使用Lambda,那么根据"可推导就可以省略原则",无序指定参数类型,也无需指定的重写的形式--->它们可以被推导出来,所以就可以省略掉。能够使用方法引用,同样也是可以根据上下文进行推导。

    函数式接口是Lambda的基础,而方法引用是Lambda的优化品。

  • 相关阅读:
    async 和 await
    Nginx配置反向代理与负载均衡
    简单使用高德地图开放平台API
    layui select 动态赋值
    ERROR: Pool overlaps with other one on this address space
    解决docker镜像无法删除的问题
    伪静态问题导致前台页面无法通过地址栏访问
    ERROR: Failed to Setup IP tables: Unable to enable SKIP DNAT rule
    PHP使用引用实现无限极分类
    composer update -- memory_limit
  • 原文地址:https://www.cnblogs.com/luayan/p/14176245.html
Copyright © 2020-2023  润新知