环境
java8
前言
这篇更应该叫做《java8实战》
读后感
一等值或者一等公民
编程语言的目的就是在操作值,这些值我们称之为
一等值(或者一等公民)
;
编程语言中其他结构也许有助于我们表示值得结构,但在程序执行期间不能传递,因而我们称为二等公民
。
上面是书中一些定义;
我是这么简单理解的,在运行执行期间,能直接传递的就是一等公民
,
而像方法或者类,在运行执行期间不能传递,所以是二等公民
当然在java8
中方法可以传递了,已经可以升级为一等公民
透明
透明即明白
意思
就是使得代码更清晰明白;
而不像匿名函数那样,让人很费解;
//测验2.2:匿名类谜题
//下面的代码执行时会有什么样的输出呢,4、5、6还是42?
public class MeaningOfThis
{
public final int value = 4;
public void doIt(){
int value = 6;
Runnable r = new Runnable(){
public final int value = 5;
public void run(){
int value = 10;
System.out.println(this.value);
}
};
r.run();
}
public static void main(String...args)
{
MeaningOfThis m = new MeaningOfThis();
m.doIt();
}
}
答案是5,因为this指的是包含它的Runnable,而不是外面的类MeaningOfThis。
谓词
一个返回boolean值得函数
行为参数化
让方法接收多种行为作为参数,并在内部使用,来完成不同的行为;
匿名类
在java8之前,就有这个概念了;
它可以让你同时声明并实例化一个类
即:随用随建
非常使用那些只需要new
(实例化)一次的场景。
lambda
语法格式:
# 表达式
(parameters) -> expression
# 语句
(parameters) -> {statemeters;}
注意:
表达式默认隐含return,语句则需要显示使用
简化的原理
假设有个Apple
类:
public class Apple {
private Integer str;
public Apple() {}
public Apple(Integer str) {
this.str = str;
}
}
//Lambda
Function<Integer, Apple> c3 = (Integer a) -> new Apple(a);
//匿名类的写法
Function<Integer, Apple> c4 = new Function<Integer, Apple>() {
@Override
public Apple apply(Integer integer) {
return new Apple(integer);
}
};
c4.apply(5);
函数式接口
只定义了一个抽象方法的接口就是函数式接口。
无论它有多少个默认方法,只要有且只有一个抽象方法,就是函数式接口
知识点回顾:
①接口中的方法默认为
public abstract
,即默认就是公共的抽象方法。
函数描述符
官网解释:函数式接口中的抽象方法就是函数描述符
。
或者说是它的签名就是函数描述符。
具体点,比如Runable
这个接口里面只有一个方法run
。
@Override
public void run() {}
我们可以看到这个方法没有参数,也没有返回值。
那么我们就可以描述这个方法什么也不接受,什么也不返回。
①函数式接口的抽象方法的签名称为函数描述符
②抽象方法的签名可以描述Lambda
表达式的签名。
方法的参数列表+返回值 = 函数签名 = 函数描述符
因为我们在使用Lambda
表达式时,总是要先定义一个函数式接口。这样很麻烦。
所以java8
的设计者,已经提前帮我们设计好了几种常用的接口。
Predicate
java.util.function.Predicate<T>
接口定义了一个名叫test
的抽象方法,它接受泛型T
对象,并返回一个boolean
。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
//使用
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer<T>
定义了一个名叫accept
的抽象方法,它接受泛型T
的对象,没有返回(void
)。你如果需要访问类型T
的对象,并对其执行某些操作,就可以使用这个接口。
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
public static <T> void forEach(List<T> list, Consumer<T> c){
for(T i: list){
c.accept(i);
}
}
//使用
forEach(
Arrays.asList(1,2,3,4,5),
(Integer i) -> System.out.println(i)
);
Function
java.util.function.Function<T, R>
接口定义了一个叫作apply
的方法,它接受一个泛型T
的对象,并返回一个泛型R
的对象。
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for(T s: list){
result.add(f.apply(s));
}
return result;
}
// [7, 2, 6]
List<Integer> l = map(
Arrays.asList("lambdas","in","action"),
(String s) -> s.length()
);
以上者三个是泛型函数式接口。但是java
除了引用类型,还有原始类型(int, double, byte,char)。但是泛型(比如Consumer<T>
中的T
)只能绑定到引用类型。这是由泛型内部的实现方式造成的。因此,在Java
里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing
)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing
)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。
比如,在下面的代码中,使用IntPredicate
就避免了对值1000进行装箱操作,但要是用Predicate<Integer>
就会把参数1000装箱到一个Integer
对象中。
public interface IntPredicate{
boolean test(int t);
}
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000); ----> true(无装箱)
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000); ----> false(装箱)
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate
、IntConsumer
、LongBinaryOperator
、IntFunction
等。Function
接口还有针对输出参数类型的变种:ToIntFunction<T>
、IntToDoubleFunction
等。
异常
任何函数式接口都不允许抛出受检异常
如果要lambda
表达式抛出异常:
方法一:自已定义一个函数式接口,并声明受检异常
方法二:使用try/catch
包裹lambda
方法引用
lambda的一种快捷写法,用于调用特定方法。
或者可以理解为:方法引用就是根据已有的方法实现来创建Lambda表达式
特定方法:指的就是该方法在该类中只有一个;
①不能有重载;
②实例方法和静态方法 不能同名 原因参考:https://blog.csdn.net/u013066244/article/details/90035470
语法:
目标引用::方法名
所以,可以把方法引用看作针对仅仅涉及单一方法的语法糖。
方法引用并没有实际调用这个方法
因为其只是Lambda
表达式的一种快捷写法。
这个写方法引用时,要想到相应的Lambda
表达式。
构造函数引用
流
返回流的函数,在底层其实都没有真正执行,直到调用了collect()
函数。
简单理解,就是方法链中方法都在排队。
流和传统集合
流:按需求驱动,实时制造;属于延迟创建的集合。类似于茅台酒,按需生产,供不应求;
集合:供应商驱动;急切创建。比如端午种子,平时没有,只要端午节,瞬间堆满仓库;
流可以是无限的,即无界;
这是流和集合之间的关键区别
流只能遍历一次
流的处理流程
筛选操作 —> 谓词 filter
提取(映射) map
去重 distinct
截断 limit
终端转换
map
map()
方法本身会产生一个Stream
流,产生什么类型的呢?
由map
方法中的Lambda
表达式的返回值来决定的。
特别注意的地方
List<String> words = new ArrayList<>();
words.add("Goodbye");
words.add("World");
Stream<String[]> stream1 = words.stream().map(word -> word.split(""));
//注意这个地方
Stream<Stream<String>> sst = stream1.map(Arrays::stream)
因为Arrays::stream
返回的是Stream<String>
,导致最后map
返回的是Stream<Stream<String>>
。
这种我们得进行压扁
处理,也就是应该使用flatMap
。
因为
flatMap
是将流中的每个元素映射成流的内容,然后将这些内容生成一个新流;
这样就达到了压扁的效果
归约
归约操作:将流归约成一个值
归约的规则:利用lambda
或者方法引用写在reduce
方法里(当做参数)。
归约的规则:
① 相加求和
② 相乘
③ 最大值或者最小值
… 等
终端操作
collect(Collecotr collector)
①像上面这个方法只有一个参数的,并且并不是函数式接口,所以其实就是一个普通的方法;
②参数里写的收集器,返回值一定要是Collector
;
public interface Collector<T, A, R> {
// 建立新的结果容器
Supplier<A> supplier();
// 将元素添加到结果容器
BiConsumer<A, T> accumulator();
// 对结果容器应用最终转换
Function<A, R> finisher();
// 合并两个结果容器
BinaryOperator<A> combiner();
// 会返回一个不可变的集合
// 它定义了收集器的行为 尤其是关于流是否可以并行归约
Set<Characteristics> characteristics();
}
T是流中要收集的项目的泛型。
A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
R是收集操作得到的对象(通常但并不一定是集合)的类型。
收集器
收集器分为两种:
一类是:预定义
一类是:自定义
分区
分区是分组的特殊情况,即:分类函数为谓词
,我们称为分区函数
;
因此,它只有两个分组;
外部迭代和内部迭代
内部迭代,可以很方便使用并行,因为官方已经写好了
但是外部迭代的并行,需要我们自己使用锁来控制,成本高;
有状态和无状态
无状态:filter
、map
等操作
有状态:
①有界:reduce
、sum
和max
等操作;
②无界:sort
、distinct
操作
并行
① parallel : 将顺序流变成并行流
② sequential : 将并行流变成顺序流
在顺序流中调用parallel
方法,流本身是没什么变化,只是在内部添加了一个标识boolean
,表示之后的操作都是并行执行的。
但是最后一次的(sequential
或者parallel
)操作,会影响整个流水线。
即:最后一次是parallel
,则流水线会并行,sequential
则为顺序。
并行流适合在无序流
。
底层的支持,即线程池部分使用的是分支/合并
框架
拆分流
将stream
拆分成多个部分的算法就是一个递归过程。
ForkJoinPool中的工作窃取
假设有4个线程,将任务切割为4000个小任务。
即每个线程都有1000小任务。
实际执行时,肯定会有某个线程先执行完,假设A线程执行完毕;
那么A线程就会随机从其他线程的队列尾部“窃取”一个任务。
直到所有任务都执行完毕。
spliterator
public interface Spliterator<T> {
// iterator 遍历器
boolean tryAdvance(Consumer<? super T> action);
// 把一些元素划出去,分给第二个spliterator
// 让他们并行处理
// 是专门为spliterator 设计的
Spliterator<T> trySplit();
// 估计还剩下多少元素要遍历
long estimateSize();
// 个人猜测, 应该是返回集合用的
int characteristics();
}
我猜错了:
@Override
public int characteristics() {
return ORDERED + SIZED + SUBSIZED + NONNULL + IMMUTABLE;
}
它将返回一个int
,代表Spliterator
本身特性集的编码。
Java8实战
第七章7.3.2
使用日志调试
假设我们想看 Stream
的每一步到底是怎么运行的、结果是什么;
通常想到的是使用foreach
方法。
缺点,官方的解释:
一旦调用
forEach
,整个流就会恢复运行
但是我个人理解,是forEach
是个终端操作。其并不能每个管道都去使用;
所以引入了peek
方法;
peek
的解释:
就是在流的每个元素恢复运行之前,插入执行一个动作。但是它不像
forEach
那样恢复整个流的运行,而是在一个元素上完成操作之后,它只会将操作顺承到流水线中的下一个操作。
List<Integer> result = numbers.stream()
.peek(x -> System.out.println("from stream: " + x))
.map(x -> x + 17)
.peek(x -> System.out.println("after map: " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x))
.limit(3)
.peek(x -> System.out.println("after limit: " + x))
.collect(toList());
菱形继承问题
因为Java8引入了接口的默认方法,导致出现多重继承的问题;
解决思路:
首先,类或父类中显式声明的方法,其优先级高于所有的默认方法。
如果用第一条无法判断,方法签名又没有区别,那么选择提供最具体实现的默认方法的接口。
最后,如果冲突依然无法解决,那就只能在类中覆盖该默认方法,显式地指定在你的类中使用哪个接口中的方法。
异步API
Java8提供了 CompletableFuture
;
如果创建了CompletableFuture
数组,使用allOf()
,其会返回一个CompletableFuture<Void>
;如果想等待所有的CompletableFuture
对象执行完毕,可以接着执行join()
;即:
CompletableFuture[] futures = findPricesStream("myPhone")
.map(f -> f.thenAccept(System.out::println))
.toArray(size -> new CompletableFuture[size]);
CompletableFuture.allOf(futures).join();
如果想任一个返回就行,可以CompletableFuture.anyOf()
方法。
疑问
模式匹配
模式匹配是switch
的扩展;