java8基本概念

6 篇文章 0 订阅

环境

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(装箱)

一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicateIntConsumerLongBinaryOperatorIntFunction等。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是收集操作得到的对象(通常但并不一定是集合)的类型。

收集器

收集器分为两种:
一类是:预定义
一类是:自定义

分区

分区是分组的特殊情况,即:分类函数为谓词,我们称为分区函数

因此,它只有两个分组;

外部迭代和内部迭代

内部迭代,可以很方便使用并行,因为官方已经写好了
但是外部迭代的并行,需要我们自己使用锁来控制,成本高;

有状态和无状态

无状态:filtermap等操作

有状态:
①有界:reducesummax等操作;
②无界:sortdistinct操作

并行

① 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的扩展;

闭包

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山鬼谣me

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值