函数式编程

一、介绍

    什么叫函数式编程(FP,即Functional Programming)?学过大数据的都十分清楚Scala是一种什么样的语言,函数式编程思想早就被成熟应用在Scala中比如高阶函数、闭包、隐式转化等等,

所以Scala一直是大数据的不二语言

    函数式编程的概念很久以前就提出了,而Java有些后知后觉,在JDK1.8才引入函数式编程的思想,JDK1.8是在2014发布的已经过去7年了,现在时间JDK的最新版本是15,然而有很多公司还在用1.7以前的版本

试想Oracle这个科技巨头里面的天才们多年努力的心血结晶JDK15被国内多少人无视了,惯性的坚持用着十几年前的技术。

    其实翻看JDK源码,几乎都是函数式编程的应用,所以掌握函数式编程是一种基本功,这和数据结构设计模式一样重要。

    函数式编程就是用函数的思维去编程,我们从java8版本的更新开始分析。

二、Lambda表达式

    Lambda表达式是一切的开始,他的定义为:a function (or a subroutine) defined, and possibly called, without being bound to an identifier。

    我对Lambda的理解是独立的可执行代码块,我最直观的感受就是Lambda像一个函数式组件,讲究的是可推断可省略,这里的可推断是指有确定的逻辑路线。

    从最简单的语句来说:

//比如说有一个接口,这个接口有且只有一个抽象方法(默认方法和静态方法等除外)
interface summ{
int sum(int a,int b);
}
//1.最初写法
summ sums = new summ(){
@Override
int sum(int a,int b){
return a+b;
}
}
//2.简化写法
summ sums = (int a,intb)->{ return a+b;}
//3.再简化
summ sums = (a,b)-> a+b ;

可以很直观的看出变化,特别的提出有且只有一个抽象方法的接口叫函数式接口,函数式接口一般加@FunctionalInterface,所以当你发现

接口带此注解你就可以使用λ表达式进行简写,十分优雅。

常见的λ表达式应用:

//1.遍历
String[] atp = {"Rafael Nadal", "Novak Djokovic",  
       "Stanislas Wawrinka",  
       "David Ferrer","Roger Federer",  
       "Andy Murray","Tomas Berdych",  
       "Juan Martin Del Potro"};  
List<String> players =  Arrays.asList(atp);  
  
// 以前的循环方式  
for (String player : players) {  
     System.out.print(player + "; ");  
}  
  
// 使用 lambda 表达式  
players.forEach((player) -> System.out.print(player + "; "));  
   
// 在 Java 8 中使用方法引用  
players.forEach(System.out::println);
//2.匿名内部类
btn.setOnAction(new EventHandler<ActionEvent>() {  
          @Override  
          public void handle(ActionEvent event) {  
              System.out.println("Hello World!");   
          }  
    });  
   
// lambda expression  
btn.setOnAction(event -> System.out.println("Hello World!")); 
//3. 开启线程
// 使用匿名内部类  
new Thread(new Runnable() {  
    @Override  
    public void run() {  
        System.out.println("Hello world !");  
    }  
}).start();  
  
// lambda expression  
new Thread(() -> System.out.println("Hello world !")).start();  
  

// 使用匿名内部类  
Runnable race1 = new Runnable() {  
    @Override  
    public void run() {  
        System.out.println("Hello world !");  
    }  
};  
  
//lambda expression  
Runnable race2 = () -> System.out.println("Hello world !");  
   
// 直接调用 run 方法(没开新线程)  
race1.run();  
race2.run();  


//4.排序
String[] players = {"Rafael Nadal", "Novak Djokovic",   
    "Stanislas Wawrinka", "David Ferrer",  
    "Roger Federer", "Andy Murray",  
    "Tomas Berdych", "Juan Martin Del Potro",  
    "Richard Gasquet", "John Isner"};  
   
// 使用匿名内部类根据 name 排序 players  
Arrays.sort(players, new Comparator<String>() {  
    @Override  
    public int compare(String s1, String s2) {  
        return (s1.compareTo(s2));  
    }  
});  
//  使用 lambda expression 排序 players  
Comparator<String> sortByName = (String s1, String s2) -> (s1.compareTo(s2));  
Arrays.sort(players, sortByName);  
  
// 或采用如下形式:  
Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2))); 

以上就是λ的基本的用法。

三、四个基本函数式接口

上面讲了,只有一个抽象方法的接口就叫函数式接口,而且还有很多体现,那么到底有多少个函数式接口呢?确实难以计数,但是概括起来无非四类。

而官方给了我们这最基本的四类接口,方便我们进行复用。

这四个接口分别为supplier,consumer,predicate,function

所有的函数式接口都会符合这四个特征,区别也就是方法的重载,多几个形参之类的。

1.Supplier接口:


  supplier叫供给型接口,可以认为他是生产者,抽象方法为 T get(),意思是不传入任何对象却返回给你实体类数据,所以可以想象一下什么情形下可以应用该该接口。

  我这里举两个应用实例方便理解:

//1.简单应用
Supplier<String> supplier = ()->{
    return UUID.randomUUID().toString();
};
System.out.println(supplier.get());
//2.函数式接口参数
public static List<String> getNumList(int num, Supplier<String> sup){
    List<String> list = new ArrayList<>();
    for(int i=0; i < num;i++){
        list.add(sup.get());
    }

    return list;
}

public static void main(String[] args) {
    List<String> list  = getNumList(10, () -> UUID.randomUUID().toString());
    System.out.println(list);
}

2.Consumer接口:

 

有供给型接口就肯定有消费型接口,这个接口的含义与供给型正好相反,它接受参数却没有返回值,意思是消费掉了,抽象方法为void accept(T t),该接口的默认方法有:

default Consumer<T> andThen(Consumer<? super T> after) {
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
}(意思是consumer1.andThen(consumer2).accept(t),先执行1后执行2)。
//1.简单应用
Consumer<String> consumer = t->{
    System.out.println(t);
};
//2.接口当形参
public void happy(double money,Consumer<Double> con){
          con.accept(money);
    }
    
    public void test(){
          happy(1000, m -> System.out.print("消费"+m+"元"));
     }

3.predicate接口:

predicate叫断言型接口,意思是对你输入的参数进行判断处理是否符合规则进而过滤等,这一点在微服务网关上有深刻体现,抽象接口为boolean test(T t),默认方法有:

default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) && other.test(t);
}(and关系判断)

default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
}(or关系判断)

default Predicate<T> negate() {
    return (t) -> !test(t);
}(非关系判断)

静态方法为:

static <T> Predicate<T> isEqual(Object targetRef) {
    return (null == targetRef)
            ? Objects::isNull
 : object -> targetRef.equals(object);
}

//1.简单用法
Predicate<String> predicate = t->{return t.startsWith("a");};
System.out.println(predicate.test("a"));
//2.作为形参使用
@Test
    public void test(){
        String str = "flitsneak";
        boolean b = test(str,(s -> str.length() > 3));//true
        System.out.println(b);
    }

    private static <T> boolean test(T t, Predicate<T> p){
        return p.test(t);
    }

4.Function接口:

function接口就是函数式接口本接口了,有进有出,抽象方法为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;
}

这个就没什么好说的了,直接举个默认方法应用的例子:

//默认方法andThen进行组合操作
 public static void change(String s, Function<String, Integer> fun1, Function<Integer, String> fun2) {
        String ss = fun1.andThen(fun2).apply(s);
        System.out.println(ss);
    }
    public static void main(String[] args) {
        String s1 = "123";
        change(s1, (String str) -> {
            return Integer.parseInt(str) + 10;
        }, (Integer i) -> {
            return i + ""; 
        });
        change(s1, str -> Integer.parseInt(str) + 10, i -> i + "");
    }

//这就是把字符串转换为整形加10再转换为字符串,这本应该显得十分简洁优雅,只不过逻辑太简单了显得没必要。
//没必要刻意为之,因为后面还有更强大的stream流专门搞这个,可以认为这是个进化的基础。

compose其实是指针相反了,静态方法identity就是返回正在执行的方法。

四、方法引用

 这个词我最早接触是在学C++的时候,当时析构函数下面就是讲“::”什么作用。但是这个::符号到了Java就变了意思属于函数式编程的一种。

方法引用可以分五种:

(1)对象方法引用--对象::实例方法名

(2)静态方法引用--类名::静态方法名

(3)实例方法引用--类名::实例方法名

(4)构造方法引用--类名::new

(5)数组方法引用–类型[]::new

分别介绍下各种方法引用什么意思然后如何搭配函数式接口的。

1.对象方法引用:

这个基本和C++里面的::一个意思,但是也有不同点,如果Lambda参数列表中的第一个参数是实例方法的参数调用者,而第二个参数是实例方法的参数时,用法就不同了:

//这个BiPredicate有必要再说明一下,它属于断言接口的类重载,可以接受俩参数,equals是String对象的方法
BiPredicate<String,String> bp = (x, y) -> x.equals(y);
BiPredicate<String,String> bp1 = String::equals;

2.静态方法引用:

是说有个静态方法定义在对象中,你可以采用::快捷处理

//Person声明:
public class Person { 
    private String name;   
    private Integer age;
    public static int compareByAge(Person a, Person b) {
    return a.age.compareTo(b.age);
    }
 }
//java.lang.util包下的Arrays有个sort方法public static <T> void sort(T[] a, Comparator<? super T> c),比较器实际是个函数式接口。
Person[] rosterAsArray = new Person[30]; // 添加数组元素省略


//一般写法
Arrays.sort(rosterAsArray, (a,b) -> a.getAge().compareTo(b.getAge()));
//加入Person静态方法用法
Arrays.sort(rosterAsArray, (a,b) -> Person.compareByAge(a,b));//通常是这样写
//方法引用用法
Arrays.sort(rosterAsArray, Person::compareByAge);

3.实例方法引用:

调用已经存在的实例的方法,与静态方法引用不同的是类要先实例化,静态方法引用类无需实例化,直接用类名去调用。

class User {

    private String name;
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

public class TestInstanceReference {

    public static void main(String[] args) {

        TestInstanceReference test = new TestInstanceReference();
        User user = new User("欧阳峰",32);
        Supplier<String> supplier = () -> user.getName();
        System.out.println("Lambda表达式输出结果:" + supplier.get());

        Supplier<String> supplier2 = user::getName;
        System.out.println("实例方法引用输出结果:" + supplier2.get());
    }
}

4.构造方法引用:

需要调用的构造器的参数列表要与函数式接口中抽象方法的参数列表保持一致:

Supplier<List<User>> userSupplier = () -> new ArrayList<>();
List<User> user = userSupplier.get(); 

Supplier<List<User>> userSupplier2 = ArrayList<User>::new;
List<User> user2 = userSupplier.get();

5.数组方法引用:

想法和构造方法引用基本一样:

Function<Integer,int[]> fun = int[]::new;
int[] arr = fun.apply(10);
Function<Integer,Integer[]> fun2= integer[]::new;
Integer[] arr2 = fun2.apply(10);



五、Stream流

  从stream流可以看到大数据处理的缩影,大数据无非是数据收集,数仓存储,分类清洗,归类建模,最终评估等等流程,而这些流程在stream中似乎都有。

还有要注意的,流的时间复杂度并不高,他是lazy的,流不储存元素,流不会修改其数据源。

我们简单来分成三个步骤:创建流,中间操作,终端操作。

1.创建流

    一般通过通过Stream接口的静态工厂方法或者Collection接口的默认方法创建流

  • Arrays.stream(T array) or Stream.of()
  • Collection.stream()
  • Collection.parallelStream()

@Test
    public void testArrayStream() {
        //1.通过Arrays.stream
        //1.1基本类型
        Integer[] arr = new Integer[]{1,2,3,4,5,6,7};

        //通过Arrays.stream
        Stream stream1 = Arrays.stream(arr);

        stream1.forEach(System.out::print);

        //2.通过Stream.of
        Stream<Integer> stream2 = Stream.of(1,2,3,4,5,6,7);

        stream2.forEach(System.out::print);
    }

    @Test
    public void testCollectionStream(){
        List<Integer> strs = Arrays.asList(1,2,3,4,5,6,7);
        //创建普通流
        Stream<Integer> stream  = strs.stream();
        //创建并行流
        Stream<Integer> stream1 = strs.parallelStream();
    }

无限流(搭配limit):

@Test
    public void testUnlimitStream() {

        Random seed = new Random();
        Supplier<Integer> random = seed::nextInt;
        Stream.generate(random).limit(10).forEach(System.out::println);

        //Another way
        IntStream.generate(() -> (int) (System.nanoTime() % 100)).limit(10).forEach(System.out::println);
    }

规律流:

    @Test
    public void testUnlimitStream1(){
        Stream.iterate(0,x -> x+1).limit(10).forEach(System.out::println);
        Stream.iterate(0,x -> x).limit(10).forEach(System.out::println);
    }

2.中间操作

    filter:过滤操作

  

 

  

@Test
    public void testFilter() {
        Integer[] sixNums = {1, 2, 3, 4, 5, 6};
        Integer[] evens = Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);
    }

    distinct: 去重操作

 

@Test
    public void testDistinct() {
        Integer[] sixNums = {1, 2, 4, 3, 5, 5, 6};
        Integer[] distinctedNums = Stream.of(sixNums).distinct().toArray(Integer[]::new);
        System.out.println(distinctedNums);
    }

    sorted:排序操作

  Integer[] sixNums = {1, 2, 4, 3, 5, 6};
        Integer[] orderedNums = Stream.of(sixNums).sorted(Comparator.comparing(x -> x)).toArray(Integer[]::new);

    map:映射操作

 

@Test
public void testMap() {
    String[] arr = new String[]{"yes", "YES", "no", "NO"};
    Arrays.stream(arr).map(x -> x.toLowerCase()).forEach(System.out::println);
}

    flatMap:扁平化流操作

@Test
    public void testFlapMap1() {
        String[] words = {"Hello", "World"};
        Stream.of(words)
                .map(word -> word.split(""))
                .flatMap(Arrays::stream).forEach(System.out::println);
    }

    limit:截断流

    peek:消费流

peek: 生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;

    skip:跳跃流

返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream;

 

3.终端操作

  • forEach()
  • forEachOrdered()
  • toArray()
  • reduce()
  • collect()
  • min()
  • max()
  • count()
  • anyMatch()
  • allMatch()
  • noneMatch()
  • findFirst()
  • findAny()

    Reduce相信都对这个比较熟悉。其余的操作也很简单,在使用的时候推敲一二即可。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值