java 8笔记

一、JAVA8 说明

  1. java8功能的基础就是让方法可以成为值,即让方法变成了一等值,把代码传递给方法的简洁方式(方法引用、Lambda)和接口中的默认方法。
  2. java 8对于程序员的主要好处在于它提供了更多的编程工具和概念,能以更快,更重要的是能以更简洁、更易于维护的方式解决新的或者现有的编程问题。三个新编程概念,流处理、用行为参数话把代码传递给方法、并行与共享的可变数据
  3. 行为参数化& 函数式编程
    让方法接受多种行为或者战略作为参数,并在内部使用,来完成不同的行为(将代码传递给方法的功能,即函数式编程)。行为参数化可以让代码更好地适应不断变化的要求,并且可以推迟参数化代码的执行。但在java8之前实现起来很啰嗦。会有很多为接口声明许多只用一次的实体类。造成的啰嗦代码,在java8 以前可以用匿名类来解决。
  4. java8中加入默认方法的原因
    默认方法给接口设计者提供了一个扩充接口的方式,而不会破坏现有的代码。用default关键字来表示这一点,主要是为了支持库设计师,让他们能够写出更容易改进的接口,但由于真正需要编写默认方法的程序员相对较少,而且他们只是有助于程序改进,而不是用于编写任何具体的程序。
  5. java.util.stream流的优点
    可以在一个更高的抽象层次上写Java8代码了,思路变成了把这样的流变成那样的流,而不是一次只处理一个项目。
    java8 可以透明地把输入的不相关的部分拿到几个CPU内核上去分别执行stream操作流水线,用不着在去搞thread了 。
  6. Collection和stream的区别
    Collection主要是为了储存和访问数据,而stream主要用于描述对数据的计算。stream允许并且提倡并行处理一个stream中的元素

二、Lambda

  1. lambda的四个特点 & 为什么要用Lambda

       1. 匿名:因为它不像普通方法那样有明确的名称
       2. 函数:它不像方法那样属于某个特定的类。但和方法一样有参数列表,返回类型,还有可能有可以抛出的异常列表
       3. 传递:可以作为参数传递给方法或存储在变量当中
       4. 简洁:不用想匿名类那样写很多模板代码
    
  2. 函数式接口
    函数式接口就是只定义一个抽象方法的接口,默认方法和静态方法除外

  3. 函数描述符
    函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫做函数描述符

  4. Lambda如何做类型检查&类型推断
    Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型

  5. Java8 提供的基础的4个函数式接口

      Predicate接口:有个方法,接受一个参数,返回boolean类型
      @FunctionalInterface 
      public interface Predicate<T>{ 
          boolean test(T t); 
       }
     Consumer接口:有个方法,接受一个参数,返回void
     @FunctionalInterface 
     public interface Consumer<T>{ 
     	 void accept(T t); 
     }
     Function接口:有个方法,接受一个T类型的参数,返回R类型的对象
     @FunctionalInterface 
     public interface Function<T, R>{ 
     	 R apply(T t); 
     }
     Supplier接口:有个方法,没有参数,返回T类型对象
     @FunctionalInterface
     public interface Supplier<T> {
     	   T get();
     }
    
  6. 4个函数式接口的原始类型的特化为什么需要
    因为原始类型装箱是在性能方面是要付出代价的。装箱后的本质就是把原始类型包裹起来,并且存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值

  7. 请注意: 任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。

  8. Lambda表达式允许使用自由变量(不是参数,而是在外层作用域中定义的变量)以及对局部变量使用的限制
    允许。就像匿名类一样。 它们被称作捕获Lambda。
    实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能在分配该变量的线程将这条个变量回收之后,去访问该变量。因此,Java在访问自由变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没什么区别了。

  9. 方法引用
    基本思想:如果一个Lambda代表的只是"直接调用这个方法",最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法来实现创建Lambda表达式。但是,显示地指明方法的名称,代码可读写性更好

  10. 方法引用的分类
    静态方法引用
    实例方法引用:
    指向任意类型实例方法 的方法引用(例如 String 的 length 方法,写作String::length)。
    指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction
    用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。
    构造方法引用

三、stream 流

  1. Collector接口
//T 是流中要收集对项目的范型,A是累加器的类型,累加器是在收集过程中用于积累部分结果的对象,R是收集操作得到的对象
public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();
    Set<Characteristics> characteristics();
    }   
  1. 自定义Collector接口实现
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

/**
 * Created by snow_fgy on 2020/4/19.
 */
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {

    //调用是会返回一个空的累加器实例,供收集过程中使用
    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    /**
     * 会执行规约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(n-1),还有第n个元素
     * 本身。改函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历元素的效果。
     * 对于该类,这个函数仅仅会把当前项目添加至已经遍历过的项的列表
     *
     *
     * @return
     */
    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return List::add;
    }

    /**
     * 在遍历完流后,该方法必须返回在积累过程中的最后要调用的一个函数,以便将积累器对象转换为整个集合操作的最终结果。通常,就像该类情况一样,
     * 累加器对象恰好符合预期的最终结果,因此无需进行转换。
     *
     *
     * @return
     */
    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.identity();
    }

    /**
     * 供归约操作使用的函数,它定义了对流中的各个子部分进行并行处理时,各个子部分归约所得累加器要如何合并。对于该类,只要把从流的第二个部分
     * 收集到的项目列表加到遍历第一部分时得到的列表后面就行了。
     * 1. 原始流会以递归的方式拆分子流,直到定义流是否需要进一步拆分的一个条件为非
     * 2. 所有的子流都可以进行并行处理
     * 3. 最后将使用combiner方法返回的函数,将所有的部分结果两两合并
     * @return
     */
    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }


    /**
     * 定义了收集器的行为
     * CONCURRENT: accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED,那么它仅在用于无序数据源时才可以进行并行归约。
     *
     * UNORDERED: 归约结果不受流中项目的遍历和累积顺序的影响
     *
     * IDENTITY_FINISH:这表明完成器方法返回的函数是一个恒等函数,可以跳过,这种情况下,累加器对象将会直接用归约过程的最终结果。也就意味着,将累加器A不加检查地转换为结果R是安全的
     * @return
     */
    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
    }
}
  1. 并行流
    并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流

  2. 将顺序流转化为并行流
    对于顺序流调用parallel方法并不意味着流本身有任何实际的变化。在内部实际上就是设了一个boolean标志,表示你想让调用parallel之后进行的操作都并行执行。

  3. 配置并行流使用的线程池,parallel方法,使用的线程是从那来的?有多少个?怎么自定义这个过程?
    并行流内部使用了默认的ForkJoinPool,它默认的线程数量就是你的处理器的数量,这个值是由Runtime.getRuntime().available-Processors()得到的。但是你可以通过系统属性java.util.concurrent.ForkJlomPool.common.parallelism来改变线程池大小
    System.setProperties(“java.util.concurrent.ForkJlomPool.common.parallelism”, 12);
    这是一个全局设置,因为它将会影响代码中的所有并行流。反过来说,目前还无法专为某个并行流指定这个值。

  4. 并行流的代价
    并行化本身需要对流做递归划分,把每个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。但在多个内核之间移动数据的代价也是很大的,所以并行化很重要的一点是要保证在内核中并行执行工作的时间比在内河之间传输数据的时间要长。

  5. 对于高效使用并行流的考虑点
    1⃣️用适当的基准来检查其性能,因为把顺序流转换为并行流轻而易举,但却不一定是好事。
    2⃣️留意装箱。自动装箱拆箱操作会大大降低性能。java8 中有原始流来避免这种操作。
    3⃣️有些操作本身在并行流上的性能就比在顺序流中的差。特别是limit和findFirst等依赖元素顺序的操作,他们在并行流上执行的代价非常大。例如findAny会比findFirst性能好,因为它不一定要按顺序来执行。你总是可以调用unordered方法来把有序流变成无序流。那么如果你需要流中的n个元素而不是专门要前n个的话,对于无序并行流调用limit可能会比单个有序流更高效。
    4⃣️还要考虑流的操作流水线的总计算成本。设N是要处理元素的总数,Q是一个元素通过流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值高就意味这使用并行流时性能好的可能性就大。
    5⃣️对于较小的数据量,选择并行流几乎从来都不是一个好的决定。并行流处理少数几个元素的好处还抵不上并行话造成的额外开销。
    6⃣️要考虑流背后的数据结构是否易于分解。例如ArrayList的拆分效率比LinkedList高的多,因为前者用不找遍历就可以平均拆分,而后者就必须遍历。
    7⃣️流自身的特点,以及流水线中的中间操作修改流的方式,都可能改变分解过程的性能。例如,一个sized流可以分成大小相等的两部分,这样每个部分都可以比较高效地并行处理,但筛选操作可能丢弃元素的个数却无法预测,导致流本身的大小未知。
    8⃣️还要考虑终端操作中合并步骤的代价是大是小。如果这一步代价很大,那么组合每个子流所产生的部分结果所付出的代价就可能超出通过并行流得到的性能提升。

  6. 分支/合并框架
    目的是以递归的方式将并行的任务拆分成更小的任务,然后把每个子任务的结果合并起来生成整体结果。它是ExecutorService接口的一个实现,它把自任务分配给线程池(ForkJoinPool)。
    要把任务提交到这个池,必须创建RecursiveTask的一个子类,其中R是并行化任务产生的结果类型,如果任务不返回结果,则用RecursiveAction类型。

  7. 使用分支/合并框架的最佳做法
    ①对于一个任务调用join方法后会阻塞调用方,直到该任务作出结果。因此,有必要在两个子任务的计算都开始后在调用它。否则你得到的版本会比原始的顺序算法更慢更复杂,因为每个子任务都必须等待另一个子任务完成后才能启动。
    ②不应该在recursiveTask内部使用ForkJoin的invoke方法。相反,你应该始终调用compute或者fork方法,只有顺序代码才应该用invoke来启动并行计算
    ③调试使用分支/合并框架的并行计算可能有点棘手。特别是你平常都在你喜欢的IDE里面看栈跟踪来找问题,但放在分支-合并计算上就不行了,因为调用compute的线程并不是概念上的调用方,后者是调用fork的那个。
    ④和并行流一样,你不应理所当然地认为在多核处理器上使用分支-合并框架就比顺序设计快。
    注意对于分支-合并策略必须要选一个标准,来决定任务是要进一步拆分还是已小到可以顺序求值。

  8. 分支/合并框架工程用一种称为工作窃取
    在实际应用中,任务差不多被平均分配到ForkJoinPool中的所有线程上。每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行。某个线程可能早早完成了分配给它的所有任务,也就是它的队列已经空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从队列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地在工作线程之间平衡负载。

  9. 改善代码的可读性
    ①用Lambda表达式取代匿名类
    说明:某些情况下,将匿名类转换为Lambda表达式可能是一个比较复杂的过程。首先,匿名类和Lambda表达式中的this和super的含义是不同的。在匿名类中,this代表的是类自身,但是在Lambda中,它代表的是包含类。其次,匿名类可以屏蔽包含类的变量,而Lambda表达式不能(它们会导致编译错误)。在涉及重载的上下文里,将匿名类转为Lambda表达式可能导致最终的代码更加晦涩。实际上,匿名类的类型是在初始化时确定的,而Lambda的类型取决于它的上下文。
    ②用方法引用重构Lambda表达式
    ③用stream API 重构命令式的处理

  10. 默认方法
    默认方法的引入就是为了以兼容的方式解决像Java API这样的类库的演进问题的,它让类可以自动地继承接口的一个默认实现。

  11. 那么抽象类和抽象接口之间的区别是什么呢?它们不都能包含抽象方法和包含方法体的实现吗?
    首先,一个类只能继承一个抽象类,但是一个类可以实现多个接口。其次,一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变量的。

  12. 解决默认方法冲突的规则
    (1) 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
    (2) 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
    (3) 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现

四、Optional

  1. null带来的种种问题

    1. 它是错误之源。NullPointerException是目前java开发中最典型的异常
    2. 它会使你的代码膨胀。让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。
    3. 它自设毫无意义,尤其是它代表的是在静态语言中以一种错误的方式对缺失变量值的建模
    4. 它破坏了java的哲学。Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针。
    5. 它在Java的类型系统上开了个口子。null不属于任何一种类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,原因是但这个变量被传递到系统中的另一个部分后,你将无法获知这个null变量最初的赋值到底是个什么类型
  2. Optional的设计初衷仅仅是要支持能返回Optional对象的语法,没特别考虑将其作为类的字段使用,所以它并未实现Serializable接口。由于这个原因,如果你的应用使用了某些要求序列化的库或者框架,在域模型中使用Optional时可能会引发应用程序故障。

  3. 基础类型的Optional对象,我们应该避免使用它们
    因为基础类型的Optional对象不支持map,flatmap已经filter方法,而这些方法却都是Optional类最有用的方法。此外,与stream一样,Optional对象无法由基础类型的Optional最合构成。

五、其他

  1. future接口概述
    它在java5中引用,设计初衷是对将来某个时刻会发生的结果进行建模。它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future中触发某些潜在耗时的操作把调用的线程解放出来,让它能继续执行其他有价值的工作,不再需要呆呆等待耗时的操作完成。
  2. 同步API
    其实是对传统方法调用的另一种称呼:你调用了某个方法,调用方在被调用方运行的过程中等待,被调用方运行结束返回,调用方取得被调用方的返回值并继续运行。即使调用方和被调用方在不同的线程中运行,调用方还是等待被调用方结束运行。这就是阻塞示例。
  3. 异步API
    异步API会直接返回,或者至少在被调用方计算完成之前,将它剩余的计算任务交给另一个线程去做,该线程和调用方法是异步的------这就是非阻塞式调用的由来。执行剩余计算任务的线程会将它的计算结果返回给调用方。返回的方式要么是通过回调函数。要么是由调用方在次执行一个“等待,直到计算完成”的方法调用。
  4. Future执行完毕可以发送一个通知,仅在计算结果可用时执行一个有Lambda表达式或者方法引用定义的回调函数
  5. CompletableFutre 在一个线程内执行计算任务时,如果发生异常,并且不用其对象方法completeExceptionally()方法抛出异常,则调用方会一直阻塞下去。
  6. 使用工厂方法supplyAsync创建CompletableFuture
    这个方法接受一个生产者(Supplier)作为参数,返回一个CompletableFuture对象,该对象完成异步执行后会调用生产者方法的返回值。生产者方法会交由ForkJoinPool池中的某个执行线程运行,但是也可以采用这个方法的重载方法,传第二个参数指定不同的执行线程执行生产者方法。
  7. 如果一个方法即不修改它内嵌类的状态,也不修改其他对象的状态,使用return返回所有的计算结果,那么我们称其为纯粹的或者无副作用的。
  8. 会造成副作用的情况?
    1⃣️除了构造器内的初始化操作,对类中数据结构的任何修改,包括对字段的赋值(一个典型的例子:setter方法)
    2⃣️抛出个异常
    3⃣️进行输入/输出操作,比如向一个文件写数据
  9. 像这种把最终的实现查询的细节留给函数库,我们把这种思想叫做内部迭代。它的巨大优势在于你的查询语句现在读起来就像是问题陈述,由于采用了这种方式,我们马上就能理解它的功能。采用这用‘要做什么’风格的编程通常被称为声明式编程。
    10.函数无论在何处、何时调用,如果使用同样的输入总能持续地得到相同的结果,就具备了函数式的特征。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值