我又回来了
已经很久没有写过博客了。
因为前段时间我感到自己之前写的博客毫无深度,像是一个产品的说明书,而这样的博客会将一项技术看作黑匣子——你不需要知道这个技术的原理或源代码的实现逻辑,你只需要按照接口的说明调接口去完成你要实现的功能就够了。
这样将某一项IT技术看作黑匣子,以简单的利用它的功能实现自己想要的功能为目标的想法在实际工作中是合理的,因为实际工作是讲究效率的,没有那么多时间让你拨开面纱悟其内核。但是,作为一名开发者,岂能止步于此?至少在工作之外的时间中,将那个“黑匣子”打开看看它的内部,在下次使用它的时候,让这个“黑匣子”在你的手里可以由自己完全定制,并以这个流行的技术为鉴,择其善者而从之,则其不善者而改之。如此,不断进步。
我一直想写有深度的技术博客,但我可能一直都对深度这个概念有些误解,到底何为深度?之前的我一直将深度这个词和复杂联系在一起,认为只有将复杂的东西吃透了,才叫深度;但是我现在才意识到,自己对复杂这个概念也有误解——到底何为复杂?Java基础简单吗?肯定会有不少人质疑:Java基础不简单吗?几个基本变量、控制语句、类、接口、集合等等,都是很简单的东西。但是如果我问你,这些东西都是怎么实现的?应该会有很多人哑口无言。我认为简单与复杂的关系,一方面是相对的,即之前看上去复杂的东西掌握了之后就会看上去简单;另一方面就是深度,即之前看上去简单的东西追求深度之后就会看上去复杂。而简单的东西是怎么实现的,就是深度。
我将重拾博客,写一些有深度的技术博客,这样,也好在这个碎片化的时代,让自己的技术不那么碎片化,不停留于片面,养成深度思考的习惯。
关于Java的新功能的博客,网络上还真的不少,但是不够系统也不够全面,我将这些东西整理后写成博客,为想要对Java8的新特性学习的朋友提供参考,共同学习。
我将连续写Java8相比之前版本的新功能,由此成为一个系列——话说Java8还真是一个呈上启下的版本。
行为参数化
所谓行为参数化,就是将行为作为参数传入函数。
比如下面这个接口(通过作者筛选图书):
List<Book> selectBookdByAuth(String authName);
我们想要获取“路遥”的书,我们需要将“路遥”作为参数传入selectBookdByAuth
这个函数。
但是,如果我又想根据出版社筛选图书呢?那就又要创建一个根据出版社筛选图书的接口了。那如果我又有需求了呢?要根据图书类别筛选图书……
有没有什么办法把“我想要根据什么筛选图书”作为一个参数呢?这样我们只需要一个筛选图书的接口就可以完成各种筛选图书的功能了。
在这里,“我想要根据什么筛选图书”是就是一个行为,将“我想要根据什么筛选图书”作为参数传入相应的函数,就被称为行为参数化
。
我们先举个例子(筛选图书的接口):
List<Book> selectBook(Predicate<Book> predicate);
我们暂且不用思考 Predicate
是什么,当我们创建了这个接口之后,我们就可以将行为作为参数传入该函数了。
例如我想要获取以“路遥”为作者的图书:
List<Book> books = selectBook(boook -> book.getAuth().equals("路遥"));
就可以获取到想要的结果了,再例如我想要获取以“人民邮电出版社”为出版社的图书:
List<Book> books = selectBook(boook -> book.getPress().equals("人民邮电出版社"));
函数式
函数式接口
函数式接口就是只定义一个抽象方法的接口。
例如:
/**
* 运算函数式接口.
*
* @author zuoyu
*
**/
public interface Operation {
/**
* 用于运算两个int类型的数
* @param a - 参数一
* @param b - 参数二
* @return - 结果
*/
int opera(int a, int b);
}
对这个接口的简单使用:
@Test
public void operationTest() {
Operation operation = (a, b) -> a + b;
int result = operation.opera(1, 1);
System.out.println(result);
} //result:2
函数描述符
函数式接口的抽象方法的签名(参数、返回值)就是Lambda表达式的签名。其中抽象方法就是函数描述符。
例如上面的int opera(int a, int b);
可以接受的Lambda表达式为(a, b) -> a + b
,那么其中的参数a
和参数b
都是int类型,a + b
结果也为int,那么这个函数的签名就是(int, int) -> int
。
再打个比方,例如刚才筛选图书的函数式接口Predicate
:
public interface Predicate<T> {
boolean test(T t);
}
那么它的函数签名就是T -> boolean
,意味着我们将类型T
的对象作为参数传入,返回boolean
类型。只有符合函数描述符的Lambda表达式才能作为参数传入相应的函数。
使用函数式接口
Java API已经为我们提供了很常用的函数式接口以及其函数描述符,当这些函数式接口不够我们使用的时候我们也可以自己创建。(一定要记住,一个函数式本身并没有什么意义,其意义在于其函数签名。)
拿几个函数式接口细说一下:
Predicate<T>
public interface Predicate<T> {
boolean test(T t);
}
java.util.function.Predicate<T>
接口定义了一个名为test
的抽象方法,它接受泛型T
对象,并返回一个boolean
。在你需要一个涉及到类型T的布尔表达式时,就可以使用这个函数式接口。
例如你可以写一个过滤List
集合元素的方法,将这个函数式接口作为参数:
/**
* Predicate<T>接口
*
* @param list - 集合
* @param predicate - 筛选条件
* @param <T> - 类型
* @return 符合要求的结果
*/
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
List<T> results = new ArrayList<>();
list.forEach(t -> {
if (predicate.test(t)) {
results.add(t);
}
});
return results;
}
这是一个通用的对List
集合进行元素过滤的方法:
-
从一个图书List集合里获取以“图灵出版社”为出版社的图书:
List<Book> pressBooks = filter(books, boook -> book.getPress().equals("图灵出版社"));
- 注:在这里,泛型
T
就是Book
类型
- 注:在这里,泛型
-
从一个苹果集合内获取重量大于1.2的苹果:
List<Apple> weightApples = filter(apples, apple -> apple.getwWight() > 1.2D);
- 注:在这里泛型
T
就是Apple
类型
- 注:在这里泛型
Consumer<T>
public interface Consumer<T> {
void accept(T t);
}
java.util.function.Consumer<T>
定义了一个名叫accept
的抽象方法,它接受泛型T的对象,没有返回(void)
。如果你需要访问类型为T
的对象,并对其执行某些操作,就可以使用这个接口。
例如我要对一个List
集合内的每个元素执行行为操作:
/**
* 对集合进行任何行为操作
*/
public static <T> void action(List<T> list, Consumer<T> consumer) {
for (T t : list) {
consumer.accept(t);
}
}
这是一个通用的对List
集合进行元素行为操作的方法:
-
对图书集合内的元素进行打印:
action(books, book -> System.out.println(book.toString()));
- 注:在这里,泛型
T
就是Book
类型
- 注:在这里,泛型
-
将苹果集合内的每个苹果的重量增加0.5:
action(apples, apple -> { apple.setWeight(apple.getWeight() + 1.00D); });
- 注:在这里泛型
T
就是Apple
类型
- 注:在这里泛型
Function
public interface Function<T, R> {
R apply(T t);
}
java.util.function.Function<T, R>
接口定义了一个叫做apply
的方法,它接受一个泛型T
的对象,并返回一个泛型R
的对象。如果你需要定一个Lambda用于输入对象的信息映射到输出,你便可以利用这个接口来完成。
例如我要对一个List
集合内所有对象的某一个属性进行提取:
/**
* 对集合内对象的某一元素进行提取
*/
public static <T, R> List<R> function(List<T> list, Function<T, R> function) {
List<R> rList = new ArrayList<>();
for (T t : list) {
R r = function.apply(t);
rList.add(r);
}
return rList;
}
这是一个通用的对List
集合的对象操作的方法:
-
对图书集合内的所有书的书名进行提取:
List<String> booksName = function(books, book -> book.getName());
- 注:在这里,泛型
T
是Book
类型,泛型R
是String
类型
- 注:在这里,泛型
-
对苹果集合内的所有苹果的重量进行提取,并增加0.5:
List<Double> appleWeight = function(apples, apple -> apple.getWeight() + 0.5D );
- 注:在这里,泛型
T
是Apple
类型,泛型R
是Double
类型
- 注:在这里,泛型
Function的原始类型化
众所周知,在Java中的泛型只能绑定到引用类型上,不能绑定在原始类型上。在Java中有一个将原始类型转换为对应的引用类型的机制——装箱;相反的将引用类型转换为原始类型的机制——拆箱。这一系列操作都是Java自动完成的,但是这个机制是要付出代价的——
装箱:把原始数据类型包裹起来,并保存到堆里。所以,装箱后需要更多的内存来保存,并需要额外的内存用来搜索并获取被包裹的原始值。
Java8为避免这个现象对其所提供的函数式接口带来了一个专门版本,在数据的输入和输出都是原始类型时避免自动装箱的操作,以此节省内存。
例如我要根据下标获取一个苹果对象:
IntFunction<Apple> appleIntFunction = (int i) -> apples.get(i);
Apple apple = appleIntFunction.apply(2);
我们来看一下IntFunction
接口:
public interface IntFunction<R> {
R apply(int value);
}
java.util.function.IntFunction<R>
接口的参数为int
原始类型,返回一个R
类型,与我们想要完成相同功能的java.util.function.Function<T, R>
接口相比较,避免了必须传入Integer
类型的自动装箱操作。
再比如我要获取一个double
随机数的2倍数:
IntToDoubleFunction intToDoubleFunction = (int i) -> Math.random() * i;
double random = intToDoubleFunction.applyAsDouble(2);
我们看一下IntToDoubleFunction
接口:
public interface IntToDoubleFunction {
double applyAsDouble(int value);
}
上面的功能如果我们使用java.util.function.Function<T, R>
接口来实现这个功能,需要将接口写成Function<Integer, Double>
,输入Integer
类型并输出Double
类型;相对于java.util.function.IntToDoubleFunction
接口,输入int
类型输出double
类型,省去了自动装箱。
Function的变种函数:
IntFunction<R>
IntToDoubleFunction
IntToLongFunction
LongFunction<R>
LongToDoubleFunction
LongToIntFunction
DoubleFunction<R>
ToIntFunction<T>
ToDoubleFunction<T>
ToLongFunction<T>
其他函数式接口
JavaAPI自带的函数接口还有不少,为的是我们日常使用。当然也有不能满足我们需求的时候,比如我要输入三个参数,那就需要自己定义接口了。还是那句话,函数接口本身并无意义,其意义在于其函数签名(参数数量与返回类型)。
JavaAPI自带的函数式接口(不一一细说了):
函数式接口 | 函数描述符 |
---|---|
Predicate<T> |
T -> boolean |
Consumer<T> |
T -> void |
Function<T, R> |
T -> R |
Supplier<T> |
(void ) -> T |
UnaryOperator<T> |
T -> T |
BinaryOperator<T> |
(T , T ) -> T |
BiPredicate<L, R> |
(L , R ) -> boolean |
BiConsumer<T, U> |
(T , U ) -> void |
BitFunction<T, U, R> |
(T , U ) -> R |
- 注:以上函数式接口都有其原始类型化的变种。
方法引用
方法引用可以重复的使用现有的方法定义,可以将其理解为Lambda的简化方式。
例如,我要根据苹果的重量对其从小到大排序:
apples.sort((apple1, apple2) -> apple1.getWeight().compareTo(apple2.getWeight()));
上面是Lambda表达式的写法,那么换作方法引用的方式可以简化代码:
apples.sort(Comparator.comparing(Apple::getWeight));
相对于Lambda表达式的写法,方法引用的写法在这里意思更加清晰直观。
方法引用主要有三类:
- 指向静态方法的方法引用(例:
Integer
的parseInt()
方法可以直接写成Integer::parseInt
)。 - 指向任意类型实例方法的方法引用(例:
String
的length()
方法可以直接写成String::length
)。 - 指向现有对象的实例方法的方法引用(例:假设有一个局部变量
book
指向用于存放Book
类型的对象,它有一个实例方法getName()
,你可以直接写成book::getName
)。
复合Lambda表达式
Java8API中的函数式接口都提供了复合方法,即通过这些方法把多个简单的Lambda表达式复合成复杂的表达式。
谓词复合
谓词接口包含三个方法:
negate
(否定)、and
(并且)、or
(或)。
这几个谓词类似布尔语句之间的关系,举个例子:
-
比如我现在有三种对图书的筛选方案:
- 筛选出以“路遥”为作者的图书的逻辑接口:
Predicate<Books> bookPredicateByAuth = book -> book.getAuth().equals("路遥"));
- 筛选出以“人民邮电出版社”为出版社的图书的逻辑接口:
Predicate<Books> bookPredicateByPress = boook -> book.getPress().equals("人民邮电出版社"));
- 筛选出印刷时间在2010年之后的图书的逻辑接口:
Predicate<Books> bookPredicateByPrintingData = boook -> book.getPrintingData.before(new SimpleDateFormat("yyyy-MM-dd").parse("2010-01-01"););
-
那么我现在想要筛选出以“路遥”为作者的,印刷时间在2010年之后的图书,不要以“人民邮电出版社”出版的,可以该逻辑接口这么写:
Predicate<Books> bookPredicate = bookPredicateByAuth.and(bookPredicateByPrintingData).negate(bookPredicateByPress);
-
进行筛选:
List<Book> books = filter(books, bookPredicate));
函数复合
函数复合的接口方法有两个:
andThen
和compose
。
andThen
方法会返回一个函数,它先对输入应用一个给定的函数,再对输出应用另一个函数。
compose
方法把给定的函数作用compose
的参数里面给的那个函数,然后再把函数本身用于结果。
这两个函数的作用就是函数套函数,举个例子:
-
我现在有两个函数:
- 第一个函数为两数相加的函数:
Function<Integer, Integer> add = x -> x + 1;
- 第二个为两数相乘的函数:
Function<Integer, Integer> multiply = x -> x * 2;
-
如果想要先算加法再算乘法,达到
multiply(add())
的效果(只是可以这样理解,实际不是这样的结构):Function<Integer, Integer> function = add.andThen(multiply); function.apply(1); // result: 4
-
如果想要先算乘法再算加法,达到
add(multiply)
的效果(只是可以这样理解,实际不是这样的结构):Function<Integer, Integer> function = add.compose(multiply); function.apply(1); // result: 3
结尾
转载请表明出处
下期咱们聊一聊Java8的stream流