`
QING____
  • 浏览: 2234196 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

JAVA与函数式接口

 
阅读更多

    函数式接口(Functional Interface)是JDK 8中新增的特性,其实也是lambda表达式编程模式中的一个很重要的构成。我们先看看什么是函数式接口。

    函数式接口:有且只有一个抽象方法的接口,为函数式接口。除此限制之外,函数式接口仍然遵循接口的其他基本设计原则,比如允许声明static属性、static方法,也允许有默认方法等。

@FunctionalInterface
public interface Printer {

    void print(String message);


    default void print() {
        System.out.println(System.currentTimeMillis());
    }

    @Override
    public boolean equals(Object target);

    static void console(String message) {
        System.out.println(message);
    }
}

 

    除此之外,函数式接口,还可以覆盖Object类中的public方法,比如上述的“equals”方法。

   @FunctionalInterface是一个标记性的注释,只会在编译器检测接口是否符合函数式接口规范,如果其修饰一个非函数式接口,则会在编译时报错。

 

    在JDK 8,重构了少数几个历史接口,将其适用于“函数接口”的设计。你可以通过跟踪@FunctionalInterface来获取这些接口的列表。我们暂且列举几个常用的,以便我们在日后编程中,更多关注它们。

    1、java.lang.Runnable

    2、java.util.concurrent.Callable

    3、java.util.Comparator

 

    函数式接口的使用方式:

Printer printer = (String message) -> System.out.println(message);
或者
Printer printer = (message) -> System.out.println(message);
或者
Printer printer = message -> System.out.println(message);

 

    1、抽象方法无参数时:() -> System.out.println("message");

    2、抽象方法如果有多个参数时:(p1,p2) -> System.out.println(p1 + p2);

 

    参数的类型可以忽略,当然如果你习惯了严谨,也可以声明在参数上。从上述的简单使用上,我们会发现这些写法,跟JavaScript中的function几乎一样,这也是我们“热爱”它的原因。对于java而言,函数式接口的使用除了可以基于“行内”写法,也可以声明为实例方法或者静态方法。其实际作用,跟以往的内部类很像,我们也可以在函数式接口中,访问当前类的其他字段、this、super等。

    public Printer instancePrinter() {
        return message -> {System.out.println(message);};
    }

    public static Printer staticPrinter() {
        return message -> {System.out.println(message);};
    }

 

 

    在使用函数式编程的同时,我们可能还需要细细的思考一下,它会不会有哪些负面的问题?比如GC、内存?既然其跟内部类很像,但是它竟然可以“行内”声明和使用,如果遇到个for循环,会不会创建很多对象或者类?还有函数式接口,在实例化时,其class类型究竟是什么样的?

    为了便于理解,我们先大体得一个结论:

    1)函数式接口,实例化时,会生成一个对象实例。

    2)此实例,也对应一个动态代理产生的代理类。(内部称为:动态调用)

    

    问题来了,如果每次实例化函数式接口,都生成新的代理类和实例对象,在可能潜在引起内存问题,如果这些对象无法被GC,问题会更严重,当然就究竟会不会有什么原因导致其无法回收,也需要我们去探索。

for (int i = 0; i < 2000000; i++) {
    Printer printer = (String message) -> System.out.println(message);
    printer.print("I am " + i);
}

   以此为例,我们很轻巧的使用函数式接口,那么如果每次都创建一个Printer实例以及代理类对象,虽然不会导致严重的GC问题,但是我们肯定会考虑去优化设计。

 

   我们使用如下例子逐步展开思考!

   实例一:

public class TestMain {

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            Printer printer = (String message) -> System.out.println(message);
            printer.print("I am " + i);
            System.out.println(printer);
        }
        Printer p1 = message -> System.out.println(message);
        Printer p2 = message -> System.out.println(message);
        System.out.println("p1,p2:" + (p1 == p2));
        System.out.println(p1);
        System.out.println(p2);

        Printer p3 = staticPrinter();
        Printer p4 = staticPrinter();
        System.out.println("p3,p4:" + (p3 == p4));
        System.out.println(p3);
        System.out.println(p4);

        TestMain tm1 = new TestMain();
        Printer p5 = tm1.instancePrinter();
        Printer p6 = tm1.instancePrinter();
        System.out.println("p5,p6:" + (p5 == p6));
        System.out.println(p5);
        System.out.println(p6);

        TestMain tm2 = new TestMain();
        Printer p7 = tm2.instancePrinter();
        System.out.println("p6,p7:" + (p6 == p7));
        System.out.println(p7);
    }

    public Printer instancePrinter() {
        return message -> {System.out.println(message);};
    }

    public static Printer staticPrinter() {
        return message -> {System.out.println(message);};
    }
}

 

I am 0
com.vipkid.meteor.TestMain$$Lambda$1/186370029@1b2c6ec2
I am 1
com.vipkid.meteor.TestMain$$Lambda$1/186370029@1b2c6ec2
p1,p2:false
com.vipkid.meteor.TestMain$$Lambda$2/1323165413@1e80bfe8
com.vipkid.meteor.TestMain$$Lambda$3/1880587981@66a29884
p3,p4:true
com.vipkid.meteor.TestMain$$Lambda$4/1198108795@cc34f4d
com.vipkid.meteor.TestMain$$Lambda$4/1198108795@cc34f4d
p5,p6:true
com.vipkid.meteor.TestMain$$Lambda$5/396873410@65b3120a
com.vipkid.meteor.TestMain$$Lambda$5/396873410@65b3120a
p6,p7:true
com.vipkid.meteor.TestMain$$Lambda$5/396873410@65b3120a

 

    得出结论(我们限定函数式接口的内部逻辑都一样):

    1)通常,Block内实例化的函数式接口实例,只有一个,比如for循环中,我们多次创建Printer对象,其实仍然是同一个Printer。

    2)通常,方法主体(body)内,多次创建Printer对象(即使内部逻辑完全一致),其为不同的对象代理类不同,实例当然也不同。

    3)通常,static方法返回的实例,是同一个。(肯定是同一个)

    4)通常,实例方法,多次调用,返回的也是同一个。

 

    当然,这还不是全部,我们继续看实例二:

public class TestMain {
    private static Object object = new Object();

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            Printer printer = (String message) -> System.out.println(message + object);
            printer.print("I am " + i);
            System.out.println(printer);
        }
        Printer p1 = message -> System.out.println(message + object);
        Printer p2 = message -> System.out.println(message + object);
        ...
    }

    public Printer instancePrinter() {
        return message -> {System.out.println(message + object);};
    }

    public static Printer staticPrinter() {
        return message -> {System.out.println(message + object);};
    }
}

 

    输出结果与“实例一”完全一样,说明,如果函数式接口中引用了静态实例,并不影响结果。

 

    实例三:

public class TestMain {

    public static void main(String[] args) {
        final Object object = new Object();
        for (int i = 0; i < 2; i++) {
            Printer printer = (String message) -> System.out.println(message + object);
            printer.print("I am " + i);
            System.out.println(printer);
        }
        Printer p1 = message -> System.out.println(message + object);
        Printer p2 = message -> System.out.println(message + object);
        ...
    }

    public Printer instancePrinter() {
        return message -> {System.out.println(message + this);};
    }

    public static Printer staticPrinter() {
        return message -> {System.out.println(message);};
    }
}

 

I am 0java.lang.Object@30dae81
com.vipkid.meteor.TestMain$$Lambda$1/186370029@1b2c6ec2
I am 1java.lang.Object@30dae81
com.vipkid.meteor.TestMain$$Lambda$1/186370029@4edde6e5
p1,p2:false
com.vipkid.meteor.TestMain$$Lambda$2/1880587981@66a29884
com.vipkid.meteor.TestMain$$Lambda$3/511754216@4769b07b
p3,p4:true
com.vipkid.meteor.TestMain$$Lambda$4/214126413@6f539caf
com.vipkid.meteor.TestMain$$Lambda$4/214126413@6f539caf
p5,p6:false
com.vipkid.meteor.TestMain$$Lambda$5/1342443276@2dda6444
com.vipkid.meteor.TestMain$$Lambda$5/1342443276@5e9f23b4
p6,p7:false
com.vipkid.meteor.TestMain$$Lambda$5/1342443276@4783da3f

    这一次,我们在instancePrinter中引用了this,在方法区块中引用了一个Object,此时我们发现结果有了很大变化:

    1)静态方法,仍然一致。

    2)我们在for循环中,即block中使用的“行内”函数式接口,每次都创建了不同的实例,不过其代理类一致。

    3)方法主体中的两个Printer,尽管引用了相同的object而且也是final,但是其实例完全不同,代理类也不同。

    4)同一个TestMain对象,其方式实例中返回的两个Printer,对象实例不同,代理类一样。

    5)不同的TestMain对象,其返回的Printer,实例肯定不同,但是代理类还是一样。

   由此可见,如果函数接口,如果引用了外部的静态对象或者值,这并不影响实例化结果;但是如果引用了普通对象,比如主类的字段、this、super等,均导致每次函数实例化的结果都不同。

   此外,如果函数接口中引用了final类型的简单类型,比如int、String等等,则不会影响实例化结果,此处就不再测试。

 

    无状态函数式接口(stateless):如果函数式接口中,只操作传入的“参数列表”、或者引用静态对象、或者引用final类型简单类型(即值不可变,而不是引用不可变),则认为此接口为无状态的,那么在编译期间,则会在字节码层面“脱糖”(desugar)为静态方法函数式接口(其实就是静态的内部类的实例)。

    有状态函数式接口(stateful):如果接口中,引用了this、super、主体类的字段对象、或者外部的任何普通对象,都认为是有状态的。我们在设计程序时,应该避免这种情况的出现,可能会导致函数实例无法被GC。

 

    大家可以参考一篇文章:“lambda-translation”。本人摘要几个段落:

Static vs instance methods
Lambdas like those in the above section can be translated to static methods, since they do not use the enclosing object instance in any way (do not refer to this, super, or members of the enclosing instance.) Collectively, we will refer to lambdas that use this, super, or capture members of the enclosing instance as instance-capturing lambdas.

Non-instance-capturing lambdas are translated to private, static methods. Instance-capturing lambdas are translated to private instance methods. This simplifies the desugaring of instance-capturing lambdas, as names in the lambda body will mean the same as names in the desugared method, and meshes well with available implementation techniques (bound method handles.) When capturing an instance-capturing lambda, the receiver (this) is specified as the first dynamic argument.

 

All things being equal, private methods are preferable to nonprivate, static methods preferable to instance methods, it is best if lambda bodies are desugared into in the innermost class in which the lambda expression appears, signatures should match the body signature of the lambda, extra arguments should be prepended on the front of the argument list for captured values, and would not desugar method references at all. However, there are exception cases where we may have to deviate from this baseline strategy.

 

    所以,我们在优化设计函数式接口和lambda编程时,静态方法优于实例方法。

    有关函数式接口,可能引入性能问题探讨:

    1)https://blog.jooq.org/2015/11/10/beware-of-functional-programming-in-java/

    2)https://softwareengineering.stackexchange.com/questions/277473/is-there-a-performance-benefit-to-using-the-method-reference-syntax-instead-of-l

 

    JDK中提供了集中“规范式”的Function,用于支撑JDK内部的实现、以及引导开发者适用JAVA API,它们在java.util.function包中:

    1、Predicate:断言,主要方法为boolean test(T),传入一个参数、返回boolean判断结果。

    2、Consumer:消费,主要方法为void accept(T),传入一个参数、无返回值。

    3、Supplier:提供,主要方法为T get(),无传参,返回一个结果。通常Consumer与Supplier可以配合适用。

    4、Function:函数,一个比较通用的接口,主要方法R apply(T t),传入、输出。

    5、UnaryOperator:一元操作,继承Function,即输入、输出的对象类型需要一致,且输入一个参数。

    6、BinaryOperator:二元操作,继承BiFunction,输入、输出的对象类型相同,输入两个同类型的参数。

    7、BiFunction:二元操作,以及同类的BiConsumer、BiPredicate等,输入参数为两个。

 

    在我们理解了函数式接口的工作原理之后,大家都可以很方便去使用这些接口完成工作了。我们展示一下,函数式接口,如何与方法引用一起协作的。

public class FunctionModel {

    public static void staticConsumer(String message) {
        System.out.println(message);
    }

    public void consumer(String message) {
        System.out.println(message);
    }

    public Date supplier() {
        return new Date();
    }

    public boolean predicate(String message) {
        return message != null && message.startsWith("function");
    }

    public void biConsumer(String m1,String m2) {
        System.out.println(m1 + "," + m2);
   

 

FunctionModel fm = new FunctionModel();
Consumer<String> staticConsumer = FunctionModel::staticConsumer;
staticConsumer.accept("I am static consumer");

Consumer<String> consumer = fm::consumer;
consumer.accept("I am consumer");

Predicate<String> predicate = fm::predicate;
predicate.test("function test!");

Supplier<Date> supplier = fm::supplier;
System.out.println(supplier.get());

BiConsumer<String,String> biConsumer = fm::biConsumer;
biConsumer.accept("function","model");

    1)任何只有一个入参、且无返回值的方法,都可以转换成Consumer;当然如果两个入参,可以转换为BiConsumer。(此处用“转换”一词不太恰当)

    2)任何一个无入参、且有返回值的方法,都可以转换为Supplier。

    3)任何只有一个入参、且返回值为boolean的方法,都可以转换为Predicate;当然两个入参,可以转换为BiPredicate。

 

    不过,我们仍然需要兼顾性能方面的考虑,尽量使用静态方法构建function,尽量在function中不引用外部非static对象,这与是否使用“方法引用”并无关系。(假定consumer方法中引用了一个外部对象,那么两次通过“对象引用”生成的Consumer实例,也是不同的)。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics