网站ui设计是什么,百度seo指数查询,百度入驻绍兴,专业网站建设分类标准目录
旧方式与新方式
lambda表达式
方法引用
Runnable
未绑定方法引用
构造器方法引用
函数式接口
带有更多参数的函数式接口
解决缺乏基本类型函数式接口的问题 本笔记参考自#xff1a; 《On Java 中文版》 函数式编程语言的一个特点就是其处理代码片段的简易性 《On Java 中文版》 函数式编程语言的一个特点就是其处理代码片段的简易性就像处理数据一样简单。Java 8加入的lambda表达式和方法引用为函数式风格编程做出了一定的支持。 在计算机的早期时代为了让程序能够适应有限的内存程序员往往需要在程序执行时修改内存中的代码让程序做出不同的行为依此节省空间。这就是自修改代码技术。因为彼时的程序大都足够小因此维护起来并不会太麻烦。 但随着内存的增大自修改代码被认为是一个糟糕的想法它极大地增加了程序的维护成本。尽管如此这种使用代码以某种方式操纵其他代码的想法依旧十分吸引人通过组合已经经过良好测试的代码我们可以生产出更有效率、更加安全的代码。 函数式编程的意义就在于此通过整合现有代码来产生新的功能而不是从零开始编写所有内容由此可以得到更加可靠、实现起来更快的代码。 面向对象编程抽象数据而函数式编程抽象行为。 纯函数式语言在安全方面规定了额外的约束条件所有的数据必须是不可变的设置一次永不改变。此时函数绝对不会修改现有值而是只生成新值。纯函数式语言在面对一些问题时能够提出一个好的解决但这不代表纯函数式语言就是最好的解决方式 Python等非函数式编程语言已经将函数式编程的概念纳入其中并且受益匪浅。Java也加入了类似的特性。
旧方式与新方式 通过将代码传递给方法我们可以控制方法使其产生出不同的行为。 旧的方式是创建一个对象在下例中是Strategy让其的某个方法包含所需行为在将这个对象传递给我们想要控制的方法
package functional;import java.util.Locale;interface Strategy {String approch(String msg);
}class Soft implements Strategy {Overridepublic String approch(String msg) {return msg.toLowerCase() ?;}
}class Unrelated {static String twice(String msg) {return msg msg;}
}public class Strategize {Strategy strategy;String msg;Strategize(String msg) {strategy new Soft(); // 将Soft()作为一个默认的决策this.msg msg;}void communicate() {System.out.println(strategy.approch(msg));}void changeStrategy(Strategy strategy) {this.strategy strategy;}public static void main(String[] args) {Strategy[] strategies {new Strategy() { // 创建一个匿名内部类来改变行为虽然依旧会有重复的代码Overridepublic String approch(String msg) {return msg.toUpperCase() !;}},msg - msg.substring(0, 5), // 这就是Java 8开始提供的lambda表达式Unrelated::twice // 这也是Java 8中出现的方法引用};Strategize s new Strategize(Hello there);s.communicate();for (Strategy newStrategy : strategies) {s.changeStrategy(newStrategy); // 遍历数组strategies中的每一个决策并将其放入s中进行决策更换s.communicate(); // 更换决策后每一次输出都会产生不同的结果我们传递了行为而被仅仅是数据}}
} 程序执行的结果是 Strategy提供的接口包含了唯一的approach()方法。通过创建不同的Strategy对象就可以创建不同的行为。 在上述程序中包含了默认的决策Soft()和一个匿名内部类。除此之外还出现两个了Java 8添加的新内容
lambda表达式 msg - msg.substring(0, 5) 这种表达式的特点是使用箭头-分隔参数和函数体。 方法引用 Unrelated::twice 特点是::。其中::左边是类名或对象名右边是方法名但没有参数列表。 在Java 8之前使用普通的类或者匿名内部类来传递功能但这种语法的并不方便。lambda表达式和方法引用改变了这种情况使得传递功能变得更加便捷。 lambda表达式
||| lambda表达式是使用尽可能少的语法编写的函数定义。 换言之lambda表达式产生的是函数而不是方法。当然Java中的一切都是类之所以lambda表达式会让人产生这种“错觉”是因为幕后进行了各种各样的操作。作为程序员我们可以将lambda表达式视为函数。 lambda表达式的语法宽松且易于编写。例如
package functional;interface Description {String brief();
}interface Body {String detailed(String head);
}interface Multi {String twoArg(String head, Double d);
}public class LambdaExpressions {static Body bod h - h No Parens!; // 本条语句并不需要使用括号仅限只有一个参数时static Body bod2 (h) - h More details; // 使用了括号。处于一致性的考虑在只有一个参数时也使用括号static Description desc () - Short info; // 若没有参数必须使用括号来指示空的参数列表static Multi mult (h, n) - h n; // 有多个参数此时必须将它们放在使用括号包裹的参数列表中static Description moreLines () - {System.out.println(moreLines());return from moreLines();};public static void main(String[] args) {System.out.println(bod.detailed(Oh!));System.out.println(bod2.detailed(Hi!));System.out.println(desc.brief());System.out.println(mult.twoArg(Pi: , 3.14159));System.out.println(moreLines.brief());}
} 程序执行的结果是 在上述的3个接口中每个接口都有一个方法这是后续会提到的函数式接口。任何lambda表达式的基本语法如下 注释中提到过若没有参数就必须使用括号来指示空的参数列表。 对一行的lambda表达式而言方法体中表达式的结果会自动成为lambda表达式的返回值所以这里使用return关键字是不和法的。另外若lambda表达式需要多行代码如上文中的moreLines就需要将表达式的代码放入到花括号中。此时又会需要使用return从lambda表达式中生成一个值了。 可以看到lambda表达式可以通过接口更方便地生成行为不同的对象。 递归 递归即函数调用了自身。Java也允许编写递归的lambda表达式但需要注意一点这个lambda表达式必须被赋值给一个静态变量或一个实例变量。通过两个示例说明这些情况 两个示例会使用一个相同的接口
interface IntCall {int call(int arg);
}
【示例静态变量】实现一个阶乘函数递归计算小于等于n的正整数的乘积
public class RecursiveFactorial {static IntCall fact;public static void main(String[] args) {fact n - n 0 ? 1 : n * fact.call(n - 1);for (int i 0; i 10; i)System.out.println(fact.call(i));}
} 程序执行的结果是 在这个例子中fact就是一个静态变量。递归函数会不断调用其自身因此必须有某种停止条件在上述例子中是n 0否则就会陷入无限递归直到栈空间被耗尽。 下方这种初始化fact的方式是不被允许的
static IntCall fact n - n 0 ? 1 : n * fact.call(n - 1);
这种处理对Java编译器而言还是太过复杂了会导致编译错误。
---
【示例示例变量】实现斐波那契数列
public class RecursiveFibonacci {IntCall fib;RecursiveFibonacci() {fib n - n 0 ? 0 :n 1 ? 1 :fib.call(n - 1) fib.call(n - 2);}int fibonacci(int n) {return fib.call(n);}public static void main(String[] args) {RecursiveFibonacci rf new RecursiveFibonacci();for (int i 0; i 10; i)System.out.println(rf.fibonacci(i));}
} 程序执行的结果是 方法引用 Java 8提供的方法引用其指向的是方法。方法引用的格式如下 interface Callable {void call(String s); // 与hello()和show()的签名了保持一致
}class Describe {void show(String msg) {System.out.println(msg);}
}public class MethodReferences {static void hello(String name) {System.out.println(Hello, name);}static class Description { // 定义一个内部类String about;Description(String desc) {about desc;}void help(String msg) {System.out.println(about msg);}}static class Helper {static void assist(String msg) { // assist()是静态内部类中的一个静态方法System.out.println(msg);}}public static void main(String[] args) {Describe d new Describe();Callable c d::show; // 将Describe对象的show方法赋给了Callablec.call(call()); // 通过call()调用了show()c MethodReferences::hello; // 等号右边是一个静态方法引用c.call(Bob);c new Description(valuable)::help; // 对某个活跃对象上的方法的方法引用“绑定方法引用”c.call(information);c Helper::assist; // 获得静态内部类中的静态方法的方法引用c.call(Help!);}
} 程序执行的结果是 在上述程序中Callable.call()、Describe.show()和MethodReferences.hello()这三者的签名保持了一致。这解释了为什么语句 Callable c d::show; 及其之后的语句能够顺利编译。
Runnable Runnable是一个java.lang包提供的接口。这个包遵循特殊的单方法接口格式它的run()方法没有参数也没有返回值。 因此可以将lambda表达式或方法引用用作Runnable
class Go {static void go() {System.out.println(方法引用Go::go());}
}public class RunnableMethodReference {public static void main(String[] args) {new Thread(new Runnable() {Overridepublic void run() {System.out.println(定义一个run()方法);}}).start();new Thread(() - System.out.println(这是一个lambda表达式)).start();new Thread(Go::go).start();}
} 程序执行的结果是 Thread类在官方文档中的描述如下 Thread会接受一个Runnable作为其构造器参数它的start()方法会调用run()。 只有匿名内部类需要提供run()方法。 未绑定方法引用 未绑定方法引用指的是尚未关联到某个对象的普通非静态方法。对于未绑定引用必须先提供对象然后才能使用
class X {String f() {return X::f();}
}interface MakeString {String make();
}interface TransformX {String transform(X x);
}public class UnboundMethodReference {public static void main(String[] args) {
// MakeString ms X::f; // 无效的方法引用TransformX sp X::f;X x new X();// 下列两条语句的效果是相同的System.out.println(sp.transform(x));System.out.println(x.f());}
}程序执行的结果是 在上述例子之前示例中对方法的引用方法与其关联接口的签名是相同的。但这里出现了特例
MakeString ms X::f;
编译器不允许上述语句的编译若强制执行会引发报错此为IDEA的报错信息 这个报错指出X::f是一个未绑定方法引用因为这里涉及到了一个隐藏的参数this。若把这条有问题的语句换成下列语句则没有问题
X x new X();
MakeString ms x::f; // 无效的方法引用
上下两种语句的区别就在于下方的语句提供了一个可供附着的X的对象x这使得调动f()变为可能。X::f本身是无法“绑定到”一个对象上的。 显而易见除了自己生成一个对象外我们还有另一个方式能解决这个问题。关键在于我们还需要一个额外的参数如TransformX中所示
String transform(X x);
这种做法告诉我们函数式方法接口中的单一方法的签名与方法引用的签名不必完全匹配。 最后在看看这条语句
System.out.println(sp.transform(x));
在前述知识的基础上可以推断这条语句执行的过程println()接受了一个未绑定引用x作为参数在这个引用中调用了transform()最终调用了x.f()。 若一个方法具有多个参数则只需要让第一个参数使用这种this的模式即可
class This {void two(int i, double d) {}void three(int i, double d, String s) {}void four(int i, double d, String s, char c) {}
}interface TwoArgs {void call2(This athis, int i, double d);
}interface ThreeArgs {void call3(This athis, int i, double d, String s);
}interface FourArgs {void call4(This athis, int i, double d, String s, char c);
}public class MultiUnbound {public static void main(String[] args) {TwoArgs twoargs This::two;ThreeArgs threeargs This::three;FourArgs fourargs This::four;This athis new This();twoargs.call2(athis, 11, 2.14);threeargs.call3(athis, 11, 3.14, Three);fourargs.call4(athis, 11, 3.14, Four, Z);}
} 构造器方法引用 同样的也可以对构造器的引用进行捕获此后通过这个引用来调用构造器
class Dog {String name;int age -1;Dog() {name 流浪狗;}Dog(String nm) {name nm;}Dog(String nm, int yrs) {name nm;age yrs;}
}interface MakeNoArgs {Dog make();
}interface Make1Arg {Dog make(String nm);
}interface Make2Args {Dog make(String nm, int age);
}public class CtorReference {public static void main(String[] args) {// 所有这3个构造器都只有一个名字 ::newMakeNoArgs mna Dog::new;Make1Arg m1a Dog::new;Make2Args m2a Dog::new;Dog dn mna.make();Dog d1 m1a.make(卡卡);Dog d2 m2a.make(拉尔夫, 4);}
} 注意语句Dog::new。3条相同的语句告诉我们这些构造器都有且只有一个名字 —— ::new。并且每一个引用都被赋予了不同的接口编译器可以从接口来推断所需使用的构造器。 在这里调用函数式接口方法make()意味着调用构造器。 函数式接口 方法引用和lambda表达式都需要先赋值然后才能进行使用。而这些赋值都需要类型信息让编译器确保类型的正确性。尤其是lambda表达式。例如
x - x.toString() toString()方法会返回String但上述语句并没有表示x的类型。这时候就需要进行类型推断了。因此编译器必须要能够通过某种方式推断出x的类型。 还有其他例子
(x, y) - x y // 需要考虑String类型存在与否
System.out::println
为了解决这种类型推断的问题Java 8引入了包含一组接口的java.util.function这些接口是lambda表达式和方法引用的目标类型。其中的每个接口都只包含了一个抽象方法非抽象方法可以有多个被称为函数式方法。 使用了这种”函数式方法“模式的接口可以通过FunctionalInterface注解来强制执行
FunctionalInterface
interface Functional { // 使用了注解String goodbye(String arg);
}interface FunctionNoAnn { // 没有使用注解String goodbye(String arg);
}//FunctionalInterface
//interface NoFunctional{ // 内置了两个方法不符合函数式方法定义
// String goodbye(String arg);
// String hello(String arg);
//}public class FunctionalAnnotation {public String goodbye(String arg) {return Goodbye, arg;}public static void main(String[] args) {FunctionalAnnotation fa new FunctionalAnnotation();Functional f fa::goodbye;FunctionNoAnn fna fa::goodbye;
// Functional fac fa; // 类型不兼容Functional f1 arg - Goodbye, arg;FunctionNoAnn fnal arg - Goodbye, arg;}
} FunctionalInterface注解是可选的。当只有一个方法时Java把main()中的Functional和FunctionalNoAnn都视为了函数式接口。 现在看向两条赋值语句
Functional f fa::goodbye;
FunctionNoAnn fna fa::goodbye;
这两条赋值语句均把一个方法这个方法甚至不是接口方法的实现赋值给了一个接口引用。这是Java 8增加的功能若把一个方法引用或lambda表达式赋值给某个函数式接口且类型匹配匿名Java会调整这次赋值使其能够匹配目标接口。 在底层的实现中Java编译器会创建一个实现了目标接口的类的示例并将我们进行赋值的方法引用或lambda表达式包裹在其中。 使用了FunctionalInterface注解的接口也叫做单一抽象方法。
命名规则 java.util.function旨在创建一套足够完备的接口。一般来说可以通过接口的名字了解接口的作用。以下是基本的命名规则也可以去官方文档进行查看
只处理对象而不是基本类型名字较为直接如Function、Consumer和Predicate等。接受一个基本类型的参数使用名字的第一部分表示如LongConsumer、DoubleFunction和InPredicate等例外基本的Supplier类型。返回的是基本类型的结果用To表示例如ToLongFunctionT和IntToLongFunction。返回类型和参数类型相同被命名为Operator。UnaryOperator表示一个参数BinaryOperator表示两个参数。接受一个参数并返回boolean被命名为Predicate。接受两个不同类型的参数名字中会有一个Bi比如BiPredicate。 因为基本类型的存在Java在设计这些接口时不得不考虑众多的类型这无疑增加了Java的复杂性。 例如
import java.util.function.*;class Foo {
}class Bar {Foo f;Bar(Foo f) {this.f f;}
}class IBaz {int i;IBaz(int i) {this.i i;}
}class LBaz {long l;LBaz(long l) {this.l l;}
}class DBaz {double d;DBaz(double d) {this.d d;}
}public class FunctionVariants {static FunctionFoo, Bar f1 f - new Bar(f);static IntFunctionIBaz f2 i - new IBaz(i);static LongFunctionLBaz f3 l - new LBaz(l);static ToLongFunctionLBaz f4 lb - lb.l;static DoubleToIntFunction f5 d - (int) d;public static void main(String[] args) {Bar b f1.apply(new Foo());IBaz ib f2.apply(11);LBaz lb f3.apply(12);long l f4.applyAsLong(lb);int i f5.applyAsInt(14);}
} 在一些情况下需要使用类型转换否则编译器会报出截断错误。上述程序中的每个方法都会调用其关联的lambda表达式。 方法引用还有一些特别的用法
import java.util.function.BiConsumer;class In1 {
}class In2 {
}public class MethodConversion {static void accept(In1 i1, In2 i2) {System.out.println(accept());}static void someOtherName(In1 i1, In2 i2) {System.out.println(somwOtherName());}public static void main(String[] args) {BiConsumerIn1, In2 bic;bic MethodConversion::accept;bic.accept(new In1(), new In2());bic MethodConversion::someOtherName;
// bic.someOtherName(new In1(), new In2); //行不通bic.accept(new In1(), new In2());}
} 程序执行的结果是 以下是BitConSumer的文档说明 这个接口有一个accept()方法可以被用作方法引用。并且即使名字并不相同如someOtherName()只要参数类型和返回类型能够与BiConsumer的accept()相同也没有问题。 使用函数式接口时名字不重要重要的是参数类型和返回类型。 Java会负责将我们起的名字映射到函数式方法上。若要调用我们的方法就需要调用这个函数式方法的名字。
带有更多参数的函数式接口 java.util.function中的接口是有限的同时也是直观易懂的。因此当我们需要的接口并没有在java.util.function中被提供时我们也可以轻松地编写自己的接口
FunctionalInterface
public interface TriFunctionT, U, V, R {R apply(T t, U u, V v);
} 现在这个接口就可以被使用了
public class TriFunctionTest {static int f(int i, long l, double d) {return 99;}public static void main(String[] args) {TriFunctionInteger, Long, Double, Integer tf TriFunctionTest::f;tf (i, l, d) - (i l.intValue() d.intValue());System.out.println(tf.apply(12, 12l, 12d));}
} 程序执行成功输出36。 解决缺乏基本类型函数式接口的问题 可以通过使用BiConsumer这种面向对象的接口开创建java.util.function中没有提供的涉及int等基本类型的函数式接口
import java.util.function.BiConsumer;public class BiConsumerPermutations {static BiConsumerInteger, Double bicid (i, d) - System.out.format(%d, %f%n, i, d);static BiConsumerDouble, Integer bicdi (d, i) - System.out.format(%f, %d%n, d, i);static BiConsumerInteger, Long bicil (i, l) - System.out.format(%d, %d%n, i, l);public static void main(String[] args) {bicid.accept(11, 45.14);bicdi.accept(11.45, 14);bicil.accept(1, 14L);}
} 程序执行的结果是 上述程序使用了System.out.format()这个方法支持%n这种跨平台的字符。 这个例子中发生了自动装箱和自动拆箱通过这种方式我们可以获得处理基本类型的接口。同样的可以在其他函数式接口中使用包装类
import java.util.function.Function;
import java.util.function.IntToDoubleFunction;public class FunctionWithWrapped {public static void main(String[] args) {FunctionInteger, Double fid i - (double) i;IntToDoubleFunction fid2 i - i;}
} 需要注意的是使用强制类型转换的时机否则会出现报错。 可以发现只需要通过包装类就可以获得一个用来处理基本类型的函数式接口。因此若存在函数式接口的基本类型变种其唯一的原因就是防止自动装箱/拆箱过程带来的性能损耗。