1. java8 中的三种编程思想
1. 流处理
流是一系列数据项,程序可以从输入流中一个一个读取数据项,可以把stream先看成一种花里胡哨的迭代器
好处:
① 可以在更高层次上写程序,思路变成把这样的流变成那样的流(就像数据库查询语句时的思路),而不是一次只处理一个项目
② 透明地并行处理
2. 行为参数化
3. 并行与共享的可变数据
2. 行为参数化 - 通过参数化传递代码
2.1 行为参数化
- 一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力
- 是可以帮助你处理频繁变更的需求的一种软件开发模式
- 传递代码,就是将新行为作为参数传递给方法,但在Java 8之前这实现起来很啰嗦,现在使用lambda表达式
编写一个prettyPrintApple
方法,它接受一个Apple
的List
,并可以对它参数化,以多种方式根据苹果生成一个String
输出(有点儿像多个可定制的toString
方法)。例如,你可以告诉prettyPrintApple
方法, 只打印每个苹果的重量。此外, 你可以让prettyPrintApple
方法分别打印每个苹果,然后说明它是重的还是轻的
public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter){
for(Apple apple: inventory){
String output = formatter.accept(apple);
System.out.println(output);
}
}
首先,你需要一种表示接受Apple并返回一个格式String
值的方法
public interface AppleFormatter{
String accept(Apple a);
}
现在你就可以通过实现AppleFormatter
方法,来表示多种格式行为了
public class AppleFancyFormatter implements AppleFormatter{
public String accept(Apple apple){
String characteristic = apple.getWeight() > 150 ? "heavy" :"light";
return "A " + characteristic +" " + apple.getColor() +" apple";
}
}
public class AppleSimpleFormatter implements AppleFormatter{
public String accept(Apple apple){
return "An apple of " + apple.getWeight() + "g";
}
}
最后,你需要告诉prettyPrintApple
方法接受AppleFormatter
对象,并在内部使用它们。你可以给prettyPrintApple
加上一个参数:
public static void prettyPrintApple(List<Apple> inventory, AppleFormatter formatter){
for(Apple apple: inventory){
String output = formatter.accept(apple);
System.out.println(output);
}
}
现在你就可以给prettyPrintApple
方法传递多种行为了。为此,你首先要实例化AppleFormatter
的实现,然后把它们作为参数传给prettyPrintApple
:
prettyPrintApple(inventory, new AppleFancyFormatter());
行为参数化就是策略模式的一种体现,只不过这里是将算法族(行为方法)作为参数直接传递给方法而不是作为环境类的成员变量
- 既然是策略模式的一种体现那也有策略模式的缺点 - 策略类数量会增多,每个策略都是一个类,复用的可能性很小
- 可以使用内部类或者lambda表达式来解决这个问题
2.2 解决啰嗦
1. 内部类
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple apple){
return "red".equals(apple.getColor());
}
});
缺点:在只需要传递一段简单的代码时(例如表示选择标准的boolean表达式),你还是要创建一个对象,明确地实现一个方法来定义一个新的行为
2. lambda
List<Apple> result = filterApples(inventory, (Apple apple)->"red".equals(apple.getColor()));
2.3 行为参数化符合DRY
-
DRY:don‘t repeat yourself 不要做重复的事情
dry -
KISS:Keep It Simple and Stupid 保持简单直接
- 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。
- 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。
- 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
- YAGNI:You Ain’t Gonna Need It 你不会需要它
不要去设计当前用不到的功能;不要去编写当前用不到的代码;不要过度设计
3. lambda
3.1 什么是lambda
表达式
可以把Lambda表达式理解为简洁地表示可传递的匿名函数
- 匿名:不像普通方法那样有一个明确的名称
- 函数:我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方 法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
- 传递:Lambda表达式可以作为参数传递给方法或存储在变量中
- 简洁:无需像匿名类那样写很多模板代码
lambda
是为函数式编程服务的
编程语言共性之------什么是函数式编程?
函数式编程是一种编程范式,也就是如何编写程序的方法论,主要思想是把运算过程尽量编写成一系列嵌套的函数调用,FP强调“everything is lambda",并且强调在逻辑处理中不变性的重要性
OOP强调“everything is object”,以及object之间的消息传递。通过消息传递改变每个Object的内部状态,但是很多情况代码的编写实际上是用不到对象的,比如,对一组数据做加工,先查询,然后聚合,聚合后排序,再join,再排序,再聚合,再转换(map)得到最终的结果。这个过程,用FP的函数就很自然
result = func1(func2(func3...funcN(x))))
java为了在原先oop
的思想上增加函数式编程的使用,在java8上增加了lambda函数的新特性
除此之外,lambda表达式的引入还使得代码更为简洁,可以避免生成过多的污染环境的无用实现类
lambda表达式的引入可以避免生成过多的污染环境的实现类;
在上面为了解决行为参数化的啰嗦的问题我们引入了lambda表达式,解决了策略模式产生很多实现类
3.2 基本语法
//表达式 能写在等号右边的就是一个表达式 表达式就是Lambda的返回值了
(parameters) -> expression
//语句 否则是语句
(parameters) -> { statements; }
3.3哪里使用 - 函数式接口@FunctionalInterface
函数式接口
- 函数式接口只有一个抽象方法
- default方法为默认实现,不计入抽象方法
- 如果接口声明了一个覆盖
java.lang.Object
的全局方法之一的抽象方法,那么它不会计入接口的抽象方法数量中,因为接口的任何实现都将具有java.lang.Object
或其他地方的实现
Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)
也就是说在java中,所有的Lambda的类型都是一个接口,而Lambda表达式本身,需要是这个接口的实现
java.util.function
中的几个新的函数式接口
Predicate
Consumer
Function
原始类型特化
除了上面三个泛型函数式接口:Predicate<T>
、Consumer<T>
和Function<T,R>
。还有些函数式接口专为某些基本类型类型设计,避免拆箱和装箱
关于异常
任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda 表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda 包在一个try/catch块中。
3.4 类型检查,类型推断和限制
类型检查
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的
参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型
关于使用局部变量
- 实例变量和静态变量无要求
- 局部变量必须被显示声明为
final
或事实上是final
实际和内部类一样,Lambda线程访问的是局部变量的一个副本
3.5 方法引用
所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用,在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系
方法引用
方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷 写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那好还是用名称 来调用它,而不是去描述如何调用它
① 指向静态方法的方法引用(例如Integer
的parseInt
方法,写作Integer::parseInt
)
② 指向任意类型实例方法的方法引用( 例如String
的length
方法, 写作String::length
)
- 实例方法有一个隐含的this参数,String类的compareTo()方法在实际调用的时候,第一个隐含参数总是传入this,相当于静态方法:
public static int compareTo(this, String o);
③ 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction
用于存放Transaction
类型的对象,它支持实例方法getValue
,那么你就可以写expensiveTransaction::getValue
)
第二种和第三种方法引用可能乍看起来有点儿晕。类似于String::length
的第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda
的一个参数。例如,Lambda表达式(String s) -> s.toUppeCase()
可以写作String::toUpperCase
。但第三种方法引用指的是,你在Lambda中调用一个已经存在的外部对象中的方法。例如,Lambda表达式()->expensiveTransaction.getValue()
可以写作expensiveTransaction::getValue
比方说你想要对一个字符串的List
排序,忽略大小写。List
的sort
方法需要一个Comparator
作为参数。
Comparator
描述了 一个具有(T, T) -> int
签名的函数描述符。你可以利用String
类中compareToIgnoreCase
方法即直接引用这个方法来定义一个Lambda
表达式(注意compareToIgnoreCase
是String
类中预先定义的)
List<String> str = Arrays.asList("a","b","A","B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
//Lambda表达式的签名与Comparator的函数描述符兼容。利用前面所述的方法,这个例子可 以用方法引用改写成下面的样子:
List<String> str = Arrays.asList("a","b","A","B");
str.sort(String::compareToIgnoreCase);
//编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数 式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配
④ 构造函数引用:对于一个现有构造函数,你可以利用它的名称和关键字new
来创建它的一个引用:ClassName::new
它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数。 它适合Supplier的签名() -> Apple
。你可以这样做:
Supplier<Apple> c1 = Apple::new; //构造函数引用指向默认的Apple()构造函数
Apple a1 = c1.get(); //调用Supplier的get方法 将产生一个新的Apple
//这就等价于:
Supplier<Apple> c1 = () -> new Apple(); //利用默认构造函数创建 Apple的Lambda表达式
Apple a1 = c1.get(); //调用Supplier的get方法 将产生一个新的Apple
//如果你的构造函数的签名是Apple(Integer weight),那么它就适合Function接口的签 名,于是你可以这样写:
Function<Integer, Apple> c2 = Apple::new; //指向Apple(Integer weight) 的构造函数引用
Apple a2 = c2.apply(110); //调用该Function函数的apply方法,并 给出要求的重量,将产生一个Apple
//这就等价于:
Function<Integer, Apple> c2 = (weight) -> new Apple(weight); //用要求的重量创建一 个Apple的Lambda表 达式
Apple a2 = c2.apply(110); //调用该Function函数的apply方法,并给出要 求的重量,将产生一个新的Apple对象
在下面的代码中,一个由Integer
构成的List
中的每个元素都通过 map
方法传递给了Apple
的构造函数,得到了一个具有不同重量苹果的List
:
List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new); //将构造函数引用 传递给map方法
public static List<Apple> map(List<Integer> list, Function<Integer, Apple> f){
List<Apple> result = new ArrayList<>();
for(Integer e: list){
result.add(f.apply(e));
}
return result;
}
如果你有一个具有两个参数的构造函数Apple(String color, Integer weight)
,那么 它就适合BiFunction
接口的签名,于是你可以这样写
BiFunction<String, Integer, Apple> c3 = Apple::new;
Apple c3 = c3.apply("green", 110);
//这就等价于:
BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
Apple c3 = c3.apply("green", 110);
你已经看到了如何将有零个、一个、两个参数的构造函数转变为构造函数引用。那要怎么 样才能对具有三个参数的构造函数,比如Color(int, int, int)
,使用构造函数引用呢?
构造函数引用的语法是ClassName::new
,那么在这个例子里面就是 Color::new
。但是你需要与构造函数引用的签名匹配的函数式接口。但是语言本身并没有提 供这样的函数式接口,你可以自己创建一个:
public interface TriFunction<T, U, V, R>{
R apply(T t, U u, V v);
}
现在你可以像下面这样使用构造函数引用了:
TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new
不将构造函数实例化却能够引用它,这个功能有一些有趣的应用。例如,你可以使用Map来 将构造函数映射到字符串值。你可以创建一个giveMeFruit
方法,给它一个String
和一个 Integer
,它就可以创建出不同重量的各种水果:
static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
static {
map.put("apple", Apple::new);
map.put("orange", Orange::new);
// etc...
}
public static Fruit giveMeFruit(String fruit, Integer weight){
return map.get(fruit.toLowerCase()) //你用map得到了一个 Function<Integer, Fruit>
.apply(weight);
}
4. 复合Lambda
表达式
Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用 于传递Lambda
表达式的Comparator
、Function
和Predicate
都提供了允许你进行复合的方法,这意味着你可以把多个简单的Lambda复合成复杂的表达式
比如, 你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结 果成为另一个函数的输入
这些方法都是默认方法,所以可以在函数式接口中与抽象方法共存
4.1 比较器复合
Comparator
具有一个叫作comparing
的静态辅助方法, 它可以接受一个Function
来提取Comparable
键值,并生成一个Comparator
对象
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
所以对于针对数组的排序我们可以写成
import static java.util.Comparator.comparing;
inventory.sort(comparing(Apple::getWeight));
逆序
如果你想要对苹果按重量递减排序怎么办?用不着去建立另一个Comparator
的实例。接口 有一个默认方法reversed可以使给定的比较器逆序。因此仍然用开始的那个比较器,只要修改 一下前一个例子就可以对苹果按重量递减排序:
inventory.sort(comparing(Apple::getWeight).reversed());
比较器链
如果发现有两个苹果一样重怎么办?哪个苹果应该排在前面呢?你可能 需要再提供一个Comparator
来进一步定义这个比较。比如,在按重量比较两个苹果之后,你可 能想要按原产国排序。thenComparing
方法就是做这个用的。它接受一个函数作为参数(就像 comparing
方法一样),如果两个对象用第一个Comparator
比较之后是一样的,就提供第二个 Comparator
。你又可以优雅地解决这个问题了:
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));
4.2 谓词覆盖
谓词接口包括三个方法:negate
、and
和or
,让你可以重用已有的Predicate
来创建更复杂的谓词
比如,你可以使用negate
方法来返回一个Predicate
的非,比如苹果不是红的:
Predicate<Apple> notRedApple = redApple.negate();
你可能想要把两个Lambda用and方法组合起来,比如一个苹果既是红色又比较重:
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
你可以进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果:
Predicate<Apple> redAndHeavyAppleOrGreen =
redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));
4.3 函数复合
可以把Function
接口所代表的Lambda
表达式复合起来。Function
接口为此配 了andThen
和compose
两个默认方法,它们都会返回Function
的一个实例
andThen
方法
会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。
比如,假设有一个函数f给数字加1 (x -> x + 1)
,另一个函数g给数字乘2,你可以将它们组 合成一个函数h,先给数字加1,再给结果乘2:
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); //g(f(x))
int result = h.apply(1);
compose
方法
先把给定的函数用作compose
的参数里面给的那个函 数,然后再把函数本身用于结果
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g); //f(g(x))
int result = h.apply(1);
5. Lambda
的注意事项
是某些情况下,将匿名类转换为Lambda
表达式可能是一个比较复杂的过程
- 匿名类和
Lambda
表达式中的this
和super
的含义是不同的。在匿名类中,this
代表的是类自身,但 是在Lambda
中,它代表的是包含类 - 匿名类可以屏蔽包含类的变量,而Lambda表达式不 能(它们会导致编译错误)
- 在涉及重载的上下文里,将匿名类转换为Lambda表达式可能导致最终的代码更加晦 涩