Java 8 与并发(一)

一、Java 8 的函数式编程

1.1 函数作为一等公民

JavaScript被称之为多范式语言,你既可以把它当做面向对象的语言也可以当做函数式语言。在jQuery中经常可以看到如下的代码:

$("button").click(function(){
  $("li").each(function(){
    alert($(this).text());
  });
});

注意这里each()函数的参数是一个匿名函数。将函数作为参数传递给另一个函数,这是函数式编程的特征之一

function f1(){
  var in = 1;
  function f2(){
    alert(n);
  }
  return f2;
}

var result = f1();
result();//1

函数f1的返回值返回的是函数f2,此时result就是一个函数(指向f2),对result的调用就会打印n的值。函数可以作为空外一个函数的返回值,也是函数式编程的重要特点

1.2 无副作用

函数的副作用指的是函数在调用过程中,除了给出了返回值,还修改了函数外部的状态。比如,函数在调用过程中,修改了一个全局状态。函数式编程认为,函数的副作用应该尽量避免。可以想象,如果一个函数肆意修改全局或者外部状态,当系统出现问题的时候我们很难判断到底是哪个函数引起的问题。如果函数都是显式函数,那么函数的执行显然不会受到外部或者全局信息的影响,因此,对于调试和排错是有益的。(注意: 显式函数指函数与外界交换数据的唯一渠道就是参数和返回值,显式函数不会去读取或者修改函数的外部状态。与之相对的是隐式函数,隐式函数除了参数和返回值外,还会读取外部信息,或者可能修改外部信息。)
然而,完全的无副作用实际上做不到的,因为系统总是需要获取或者修改外部信息,同时,模块之间的交互也极有可能是通过共享变量进行的。如果完全禁止副作用的出现,也是一件让人很不愉快的事情。因此,大部分函数式编程语言,如Clojure等,都允许副作用的存在。但是与面向对象相比,这种函数调用的副作用,在函数式编程里,需要进行有效的限制。

1.3 申明式的

函数式编程是申明式的编程方式。对于申明式的编程范式,你不再需要提供明确的指令操作,所有的细节指令将会更好地被程序库所封装,你要做的只是提出你的要求,申明你的用意即可。

下面是一段传统的命令式编程:

public static void imperative() {
    int[] iArr = {1,2,3,4,5};
    for(int i=0; i<iArr.length; i++) {
        System.out.println(iArr[i]);
    }
}

对应的申明式编程:

public static void declarative() {
int[] iArr = {1,2,3,4,5};
Arrays.stream(iArr).forEach(System.out::println);
}

可以看到,变量数组的循环体消失了。pirntln()函数似乎在这里也没有指定任何参数,在此,只是简单地申明了用意。有关循环以及判断循环是否结束等操作都被简单地封装在程序库中。

1.4 不变的对象

在函数式编程中,几乎所有的传递的对象都不会被轻易修改。
请看如下代码:

static int[] arr={1,2,3,4,5};
Arrays.stream(arr).map((x)->x=x+1).forEach(System.out::pirntln);
System.out.println();
Arrays.stream(arr).forEach(System.out::println);

代码第2行看似对每一个数组成员执行了加1操作。但是在操作完成之后,在最后一行,打印arr数组所有的成员值时,数组成员并没有变化。在使用函数式编程时,这种状态是一种常态,几乎所有的对象都拒绝被修改。这非常类似于不变模式。

1.5 易于并行

由于对象都处于不变的状态,因此函数式编程更加易于并行。实际上,甚至完全不用担心线程安全问题。这样不仅有利于并行化,同时,并行化后,由于没有同步和锁机制,其性能也会比较好。

1.6 更少的代码

通常情况下,函数式编程更加简明扼要。一般情况下,精简的代码更易于维护,这在上面的例子中都有体现。

二、函数式编程基础

2.1 FunctionalInterface注释

Java 8提出了函数式接口的概念。所谓函数式接口,简单来说,就是只定义了单一抽象方法的接口。比如下面的定义:

@FunctionalInterface
public static interface IntHandler {
    void handle(int i);
}

注释FunctionalInterface用于表明IntHandler接口是一个函数式接口,该接口被定义为只包含一个抽象方法handle(),因此它符合函数式接口的定义。如果一个函数满足函数式接口的定义,那么即使不标注为@FunctionalInterface,编译器依然会把它看做是函数式接口。这有点像@Oveeride注释,如果函数符合重载的要求,无论是否标注了@Oveeride,编译器都会识别这个重载函数,但一旦进行了标注,而实际的代码不符合规范,那么就会得到一个编译错误。
需要强调的是,函数式接口只能有一个抽象的方法,而不是只能哟一个方法。这分两点来说明:首先,在Java 8中,接口运行存在实例方法,其次任何被java.lang.Object实现的方法,都不能视为抽象方法,因此,下面的NonFunc接口不是函数式接口,因为equals()方法在java.lang.Object中已经实现。

interface NonFunc {
    boolean equals(Object obj);
}

同理,下面实现的IntHandler接口符合函数式接口要求。

@FunctionalInterface
public static interface IntHandler {
    void handle(int i);
    boolean equals(Object obj);
}
2.2 接口默认方法

在Java 8之前的版本,接口只能包含抽象方法。从Java 8之后,接口也可以包含若干个实例方法。这一改进使得Java 8拥有了类似于多继承的能力。一个对象实例,将拥有来自多个不同接口的实例方法。

接口IHourse实如下:

public interface IHorse {  
    void eat();  
    default void run() {  
        System.out.println("hourse run");  
    }  
}

在Java 8中,使用default关键字,可以在接口内定义实例方法。注意,这个方法并非抽象方法,而是拥有特定逻辑的具体实例方法。

所有的动物都能自由呼吸,所有,这里可以再定义一个IAnimal接口,它包含一个默认方法breath()。

public interface IAnimal {
    default void breath() {
        System.out.println("hourse breath");
    }
}

骡是马和驴的杂交物种,因此骡(Mule)可以实现为IHorse,同时骡也是动物,因此有:

public class Mule implements IHorse, IAnimal {
    @Override
    public void eat() {
        System.out.println("Mule eat");
    }
    public static void main(String[] args) {
        Mule m = new Mule();
        m.run();
        m.breath();
    }
}

述代码中Mule实例同时拥有来自不同接口的实现方法。这在Java 8之前是做不到的。从某种程度上说,这种模式可以弥补Java单一继承的一些不便。但同时也要知道,它也将遇到和多继承相同的问题。如果,IDonkey也存在一个默认的run()方法,那么同时实现他们的Mule,就会不知所措,因为它不知道应该以哪个方法为准。

增加一个IDonkey的实现:

public void interface IDonkey {
    void eat();
    default void run() {
        System.out.println("Donkey run");
    }
}

修改Mule的实现如下,注意它同时实现了IHorse和IDonkey:

public class Mule implements IHorse,IDonkey,IAnimal {
    @Override
    public void eat() {
        System.out.println("Mule run");
    }

    public static void main(String[] args) {
        Mule m = new Mule();
        m.run();
        m.breath();
    }
}

此时,由于 IHorse和IDonkey拥有相同的默认实例方法,故编译器会抛出一个错误:

Duplicate default methods named run with the parameters () and () are inherited from the types IDonkey and IHorse

为了让Mule同时实现IHorse和IDonkey,不得不重新实现以下run()方法,让编译器可以进行方法绑定。修改Mule的实现如下:

public class Mule implements IHorse,IDonkey,IAnimal {
    @Override
    default void run() {
        IHorse.super.run();
    }
    @Override
    public void eat() {
        System.out.println("Mule run");
    }

    public static void main(String[] args) {
        Mule m = new Mule();
        m.run();
        m.breath();
    }
}

在这里,将Mule的run()方法委托给IHorse实现。

接口默认实现对于整个函数式编程的流式表达非常重要。比如,java.util.Comparator接口,在JDK 1.2时已经 被引入,用于在排序时给出两个对象实例的具体比较逻辑。在Java 8中,Comparator接口新增了若干默认方法,用于多个比较器的整合。其中一个常用的默认方法如下:

default Comparator<T> thenComparing(Comparator<? super T> other) {
    Objects.requireNonNull(other);
    return (Comparator<T> & Serializable) (c1, c2) -> {
        int res = compare(c1,c2);
        return (res != 0) ? res : other.compare(c1,c2);
    };
}

有了这个默认方法,在进行排序时,就可以非常方便地进行元素的多条件 排序,比如,如下代码构造一个比较器,它先按照字符串长度排序,继而按照大小写不敏感的字母顺序排序。

Comparator<String> cmp = Comparator.comparingInt(String:length)
.thenComparing(String.CASE_INSENSITIVE_ORDER);
2.3 lambda表达式

lambda表达式可以说是函数式编程的核心。lambda表达式即匿名函数,它是一段没有函数名的函数体,可以作为参数直接传递给相关的调用者。lambda表达式极大地增强了Java语言的表达能力。关于lambda表达式的详细使用,我会在单独的文章中详细叙述。

2.4 方法引用

方法引用是Java 8 中提出的用来简化lambda表达式的一种手段。他通过类名和方法名来定位到一个静态方法或者实例方法。

方法引用在Java 8中的使用非常灵活。可以分为以下几种:

  • 静态方法引用:ClassName::methodName
  • 实例上的实例方法引用:instanceReference::methodName
  • 超类上的实例方法引用:super::methodName
  • 类型上的实例方法引用:ClassName::methodName
  • 构造方法引用:Class::new
  • 数组构造方法引用:TypeName[]::new

首先,方法引用使用“::”定义,“::”的前半部分表示类名或者实例名,后半部分表示方法名称。如果是构造函数,则使用new表示。

下例展示了方法引用的基本使用:

public class InstanceMethodRef {
    public static  void main(String[] args) {
        List<User> users = new ArrayList<User>();
        for(int i=0; i<10; i++) {
            users.add(new User(i, "billy"+Integer.toString(i)));
        }
        users.stream().map(User::getName).forEach(System.out::println);
    }
}

对于第1个方法引用“User::getName”,表示User类的实例方法。在执行时,Java会自动识别流中的元素(这里指User实例)是作为调用目标还是调用方法的参数。在“User::getName”中,显然流内的元素都应该作为调用目标,因此实际上,在这里调用了每一个User对象实例的getName()方法,并将这些User的name作为一个新的流。同时,对于这里得到的所有name,使用方法引用System.out::println进行处理。这里的System.out为PrintStream对象实例,因此,这里表示System.out实例的println方法,系统也会自动判断,流内的元素此时应该作为方法的参数传入,而不是调用目标。

一般来说,如果使用的是静态方法,或者调用目标明确,那么流内的元素会自动作为参数使用。如果函数引用表示实例方法,并且不存在调用目标,那么流内元素就会自动作为调用目标。因此,如果一个类中存在同名的实例方法和静态函数,那么编译器就不知道应该调用哪个方法了。它既可以选择同名的实例方法,将流内元素作为调用目标,也可以使用静态方法,将流元素作为参数。

如下例子:

public class BadMethodRef {
    public static void main(String[] args) {
        List<Double> numbers = new ArrayList<Double>();
        for(int i=1; i<10; i++) {
            numbers.add(Double.valueOf(i));
        }
        numbers.stream().map(Double::toString).forEach(System.out::println);
    }
}

上述代码试图将所有的Double元素转为String并将其输出,但是很不幸,在Double中同时存在以下两个函数:

public static String toString(double d)
public String toString()

此时,对函数引用的处理就出现了歧义,因此,这段代码在编译时就会抛出如下错误:

Ambiguous method reference:both toString() and toString(double) from the type Double are eligible

方法引用也可以直接使用构造函数。首先,查看模型类User的定义:

public class User {
        private int id;
        private String name;
        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }
        //这里省略对字段的setter和getter
}

下面的方法引用调用了User的构造函数:

public class ConstrMethodRef {
    @FunctionalInterface
    interface UserFactory<U extends User> {
        U create(int id, String name);
    }
    static UserFactory<User> uf = User::new;

    public static main(String[] args) {
        List<User> users = new ArrayList<User>();
        for(int i=1; i<10; i++) {
            users.add(uf.create(i, "billy" + Integer.toString(i)));
        }
        users.stream().map(User::getName).forEach(System.out::println);
    }
}

在此,UserFactory作为User的工厂类,是一个函数式接口。当使用User::new创建接口实例时,系统会根据UserFactory.create()的函数签名来选择合适的User构造函数,在这里,很显然就是public User(int id, String name)。在创建UserFactory实例后,对UserFactory.create()的调用,都会委托给User的实际构造函数进行,从而创建User对象实例。

三、走入函数式编程

简单示例如下:

static int[] arr = {1,2,3,4,5,6,7,8,9,10};
public static void main(String[] args) {
    for(int i:arr) {
        System.out.println(i);
    }
}

上述代码循环遍历了数组内的元素,并且进行了数值的打印。使用Java 8写法如下:

static int[] arr = {1,2,3,4,5,6,7,8,9,10};
public static void main(String[] args) {
    Arrays.stream(arr).forEach(new IntConsumer() {
        @Override
        public void accept(int value) {
            System.out.println(value);
        }
    });
}

这里值得注意的是这个流对象的forEach()方法,它接收一个IntConsumer接口的实现,用于对每个流内的对象进行处理。之所以是IntConsumer接口,因为当前流是IntStream,也就是装有Integer元素的流,因此,它自然需要一个处理Integer元素的接口。函数forEach()会挨个将流内的元素送入IntConsumer进行处理,循环过程被封装在forEach()内部,也就是JDK框架内。
除了IntStream流外,Arrays.stream()还支持DoubleStream、LongStream和普通的对象流Stream,这完全取决于它所接受的参数。

forEach()函数的参数是可以从上下文中推导出来的,于是:

static int[] arr = {1,2,3,4,5,6,7,8,9,10};
public static void main(String[] args) {
    Arrays.stream(arr).forEach((final int x) -> {
        System.out.println(x);
    });
}

从上述代码中可以看到,IntStream接口名称被省略了,这里只使用了参数和一个实现体。因为参数的类型也是可以推导的。既然是IntConsumer接口,参数自然是int了,于是:

static int[] arr = {1,2,3,4,5,6,7,8,9,10};
public static void main(String[] args) {
    Arrays.stream(arr).forEach((x) -> {
        System.out.println(x);
    });
}

可以去掉一对花括号,把参数申明和接口实现放在一行:

static int[] arr = {1,2,3,4,5,6,7,8,9,10};
public static void main(String[] args) {
    Arrays.stream(arr).forEach((x) -> System.out.println(x));
}

此时,forEach()函数的参数依然是IntConsumer,但是它却以一种新的形式被定义,这就是lambda表达式。表达式由“->”分割,左半部分表示参数,右半部分表示实现体。因此,可以简单地理解lambda表达式只是匿名对象实现的一种新的方式。Java 8还支持了方法引用,通过方法引用的推导,可以连参数申明和传递省略。

static int[] arr = {1,2,3,4,5,6,7,8,9,10};
public static void main(String[] args) {
    Arrays.stream(arr).forEach(System.out::println);
}

使用lambda表达式不仅可以简化匿名类的编写,与接口的默认方法结合,还可以使用更顺畅的流式API对各种组件进行更自由的装配。

下面这个例子对集合中所有元素进行两次输出,一次输出到标准错误,一次输出到标准输出中。

static int[] arr = {1,2,3,4,5,6,7,8,9,10};
public static void main(String[] args) {
    IntConsumer outprintln = System.out::println;
    IntConsumer errprintln = System.err::println;
    Arrays.stream(arr).forEach(outprintln.andThen(errprintln));
}

这里首先使用函数引用,直接定义了两个IntConsumer接口实例,一个指向标准输出,另一个指向标准错误。使用接口默认函数IntConsumer.addThen(),将两个IntConsumer进行组合,得到一个新的IntConsumer,这个新的IntConsumer会依次调用outprintln和errprintln,完成对数组中元素的处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值