Lambda 表达式
无类型参数
f -> { }
- f的类型是系统根据上下文 自动 识别的。
- 箭头(->)前表示参数变量,有 多个参数和没有参数 的时候,必须使用小括号包裹:
()
- 箭头(->)后的执行语句 只有一条 时,可以不加大括号
{}
包裹
有类型参数
- 如果代码比较复杂,而为了易阅读、易维护,也可以为参数变量指定类型。此时即使只有一个参数,也必须使用小括号
()
包裹,否则会出错。
引用外部变量
- Lambda 表达式
{}
内的执行语句,除了能引用参数变量以外,还可以引用外部的变量。 - 规范:
- 从lambda表达式引用的本地变量必须是最终变量或实际上的最终变量,即引用的 局部变量 不允许被修改,即使写在表达式里面或后面也不行。Lambda 表达式引用的 局部变量 即使不声明为
final
,也要具备final
的特性:变量值初始化后不允许被修改。 - 参数变量不能与 局部变量 同名。
- 从lambda表达式引用的本地变量必须是最终变量或实际上的最终变量,即引用的 局部变量 不允许被修改,即使写在表达式里面或后面也不行。Lambda 表达式引用的 局部变量 即使不声明为
双冒号(::)操作符
-
System.out::println
等同于n -> {System.out.println(n);}
-
元素(
n
)会 自动 作为参数传递给System.out.println()
方法。::
语法省略了参数变量
Stream API
简介
Stream
的主要作用是对集合(Collection
)中的数据进行各种操作,增强了集合对象的功能。
流迭代
-
创建流
-
由数组转化
String[] fruitArray = new String[] {"苹果", "哈密瓜", "香蕉", "西瓜", "火龙果"}; Stream<String> stream = Stream.of(fruitArray);
-
由集合转化
List<String> fruits = new ArrayList<>(); fruits.add("苹果"); fruits.add("哈密瓜"); fruits.add("香蕉"); fruits.add("西瓜"); fruits.add("火龙果"); Stream<String> stream = fruits.stream();
-
-
迭代流
-
Stream 提供的迭代方法叫做
forEach()
Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果"); stream.forEach(System.out::println);
-
集合类的
forEach()
方法与流的forEach()
方法无关,仅仅是方法名相同而已。
-
流数据过滤
-
filter() 方法
-
方法参数是一个 Lambda 表达式,箭头后面是条件语句,判断数据需要 符合 的条件。
-
箭头后的过滤条件语句(非可执行的语句)。传统代码中,条件语句写在 () 中的,所以条件语句可以用 () 而不能用 {} 。
-
流数据映射
-
map()
方法通常称作映射,其作用就是用新的元素替换掉流中原来相同位置的元素。 -
map()
方法的参数是一个 Lambda 表达式,在语句块中对流中的每个数据对象进行计算、处理,最后用return
语句返回的对象,就是转换后的对象。 -
映射后的对象类型,可以与流中原始的对象类型不一致。
流数据排序
-
sorted()
是完成排序的方法。把排序规则写成一个 Lambda 表达式传给此方法即可。students.stream() // 实现升序排序 .sorted((student1, student2) -> { return student1.getRollNo() - student2.getRollNo(); }) .forEach(System.out::println);
-
student1 指代后一个元素,student2 指代前一个元素。
-
集合排序
Collections.sort()
和流排序sorted()
都需要返回一个数值,返回 非正数 表示两个相比较的元素需要 交换位置 ,返回正数则不需要。 -
使用
student2.getRollNo() - student1.getRollNo()
可以实现降序排序
流数据摘取
-
limit(n)
方法的作用是返回流的 前n
个元素,n
不能为负数。不是摘取任意位置,只能是流开头的
流的设计思想
-
数据流的操作过程,可以看做一个管道,管道由多个节点组成,每个节点完成一个操作。数据流输入这个管道,按照 顺序 经过各个节点。
-
Stream
的显著特点是:编程的重点,不再是对象的运用,而是数据的计算。 -
Stream 的这种变化,特征是:函数式风格。即弱化了面向对象的严格、完整的语法,重心变为通过函数完成数据计算。
并行数据
流合并
-
reduce()
方法的作用是合并所有的元素,终止 计算出一个结果。这里终止的意思,就是流已经到达终点结束了,不能再继续流动了。forEach()
也是流的终点 -
reduce()
方法的返回值是一个比较复杂的对象,需要调用get()
方法返回最终的值。int sum = numbers.stream() .reduce((a, b) -> a + b) .get();
-
reduce()
方法的参数-
a
在第一次执行计算语句a + b
时,指代流的第一个元素;然后充当缓存作用以存放本次计算结果。此后执行计算语句时,a
的值就是上一次的计算结果并继续充当缓存存放本次计算结果。 -
b
参数第一次执行计算语句时指代流的第二个元素。此后依次指代流的每个元素。a、b 两个参数的作用是由***位置***决定的,变量名是任意的
-
-
reduce()
方法也可以操作对象-
reduce()
提供了另一种参数形式,可以自己new
一个对象充当缓存角色,而不是使用流中的原始对象。Student result = students.stream() .reduce(new Student("", 0), (a, b) -> { a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore()); return a; } );
-
参数:
- 第一个参数,是作为缓存角色的对象
- 第二个参数,是Lambda表达式,完成计算,格式是一样的。
a
变量不再指代流中的第一个元素了,专门指代缓存角色的对象,即方法第一个参数对象。b
变量依次指代流的每个元素,包括第一个元素。
-
reduce()
方法的返回值同样发送了变化,返回作为缓存角色的对象,即第一个参数,不用再调用一次get()
方法了
-
流收集
- 收集也是一种属于终点的流操作。
- 流收集可以把结果元素放在一个新的集合中,待进一步使用。
collect()
方法的作用是收集元素,Collectors.toList()
是一个静态方法,作为参数告诉collect()
方法存入一个List
集合。collect(Collectors.toList())
是一个标准用法。
并行流
- 使用并行流的代码很简单,不再调用
stream()
方法,改为调用parallelStream()
方法即可。其它的计算方法是一样的。 parallelStream()
以并行的方式执行任务,同时也支持流的收集、合并等计算。- 不适合使用并行计算的场景:
- 流中的每个数据元素之间,有逻辑依赖关系的时候,不适合使用并行计算。
- 并行 计算模式的性能 不是 任何情况下都优于 串行 计算模式。如:硬件太差、任务简单。一般来说,任务执行超过一小时的情况下,可以考虑使用 并行 模式优化性能。
常用设计模式
单例模式
-
单例模式可以保证一个类仅有一个实例。如:
简单工厂模式
-
实现简单工厂,需要两个步骤:
- 从具体的产品类抽象出接口。工厂应该生产 一种 产品,不应该生产某 一个 产品。
- 把生产实例对象的过程,收拢到工厂类中实现。
-
使用简单工厂完成功能开发时,重点就是要明确 什么条件 下创建 什么实例对象 的需求逻辑。
-
public class FruitFactory { public static Fruit getFruit(Customer customer) { Fruit fruit = null; if ("sweet".equals(customer.getFlavor())) { fruit = new Watermelon(); } else if ("acid".equals(customer.getFlavor())) { fruit = new Lemon(); } else if ("smelly".equals(customer.getFlavor())) { fruit = new Durian(); } return fruit; } }
-
-
在接口
Car
与实现类之间,加一个AbstractCar
,轿车类不再直接实现接口,而是继承AbstractCar
,这样可以减少重复代码- 抽象类存放公共属性和方法
- 每个实现类存放各种特有的属性和方法
抽象工厂模式
-
简单工厂的主要把多个产品抽象,使用一个工厂统一创建;那么抽象工厂的主要作用是把 多个工厂 也进一步 抽象。
-
public class SnacksFactoryBuilder { public SnacksFactory buildFactory(String choice) { if (choice.equalsIgnoreCase("fruit")) { return new FruitFactory(); } else if (choice.equalsIgnoreCase("drink")) { return new DrinkFactory(); } return null; } }
观察者模式
-
订阅/通知 的场景,很适合用 观察者模式 来实现。
并发编程
继承 Thread 类
-
可以继承 Java 的
Thread
类实现线程类。继承Thread
类后,需要重写父类的run()
方法,注意必须是修饰为public void
,方法是没有参数的。 -
线程类的作用就是完成一段相对独立的任务。
-
线程需要调用
start()
方法才能启动。 -
Thread
父类中有name
属性,但是private
的,所以可以调用setName()
方法为线程设置名字,通过getName()
就知道是哪个线程在运行。 -
线程类的
run()
方法是系统调用start()
后自动执行的,编程时不需要调用run()
方法。但永远无法知道系统在什么时刻调用,是立即调用,还是延迟一小段时间调用,都由系统自动决定,无法干预。不仅不能确定系统什么时刻调用run()
方法,也不能确定调用顺序一定是代码中start()
方法的书写顺序。
实现 Runnable 接口
-
因为 Java 是单继承的,只允许继承一个类,继承 Thread 类定义多线程程序后无法再继承其它类,可扩展性大大降低,所以定义多线程程序,优先采用另一种方式:实现
java.lang.Runnable
接口。 -
Runnable
接口中只有一个待实现的run()
方法,要自己补充属性。 -
无论是
Thread
类还是Runnable
接口,run()
方法都是系统 适时 自动 执行的Thread.sleep()这个静态方法仍然可以用
-
实现了
Runnable
接口的线程类,还需要包装在Thread
类的实例中运行Person person1 = new Person(); person1.setName("张三"); Thread thread1 = new Thread(person1);
-
Thread
实例(new Thread(person1)
)相当于调度器,触发线程任务执行,线程类的实例(new Person()
)就相当于任务。任务是不能自己启动的,需要被调度。
线程安全
-
Thread.currentThread()
返回当前正在运行的线程的实例对象 -
多个线程 操作 同一个资源 的时候,发生了冲突的现象,就叫做 线程不安全
-
在 Java 中,可以用
synchronized
关键字来解决余量错乱的问题。synchronized
不能作用于变量 -
synchronized
也叫线程 同步锁 ,表示此方法是锁定的,同一时刻只能由一个线程执行此方法。 -
因为方法加锁,同时只有一个线程竞争成功能继续执行,其它很多线程是持续等待、响应慢的,所以
synchronized
不能滥用 -
synchronized使用场景:
- 写操作的场景。例如用户修改个人信息、点赞、收藏、下单等。
- 尽量精确锁住最小的代码块,把最关键的写操作抽象成独立的方法加锁。不建议给大段的方法加锁。
悲观锁和乐观锁
-
乐观锁:
-
乐观锁其实是不上锁,总是保证基于最新的数据进行更新。由于没有上锁,就提高了性能。不上锁的思想是乐观的,所以称之为乐观锁。
AtomicInteger
类的incrementAndGet()
和decrementAndGet()
方法就是典型的乐观锁实现。decrementAndGet()
是三个操作的组合,多线程情况下也不会出现数值重复的错误,但是可能会出现负值的情况。 -
不适用于多条数据需要修改、以及多个操作的整体顺序要求很严格的场景,乐观锁适用于读数据比重更大的应用场景
-
-
悲观锁:
synchronized
关键字是把整个方法执行前就上锁,假设 其他线程 一定会修改 数据,所以提前防范。上锁的思想是悲观的,所以称之为悲观锁。- 适合写数据比重更大的应用场景。一般来说写数据的整体消耗时间更长些,是可以接受的。
-
1
并发容器
-
CompletableFuture
是一个异步任务编排、调度框架,以更优雅的方式实现组合式异步编程。studentList.forEach(s -> { CompletableFuture.supplyAsync( // 每个学生都注册学号 () -> reg.regId(s) ) // 学号注册完毕后,打印欢迎消息 .thenAccept(student -> { System.out.println("你好 " + student.getName() + ", 欢迎来到春蕾中学大家庭"); }); });
-
系统会自动优化,把作为
supplyAsync()
方法参数的整个() -> reg.regId(s)
表达式语句包装在另一个对象中;这个对象是 JDK 内置的,它实现了Runnable
接口,在这个对象中执行表达式语句。 -
supplyAsync()
方法的作用是:在一个单独的线程中执行reg.regId(s)
语句,本质上就是多线程编程。 -
使用
thenAccept()
方法完成后继的任务步骤,系统会在前一个任务完成后,自动执行student -> {}
后继任务。所以本质上,后继任务也是多线程方式执行的。 -
supplyAsync()
用于开头,thenAccept()
用于末尾,各自调用一次即可。中间有多个步骤,可以调用多次thenApply()
。 -
CompletableFuture.allOf()
是静态方法,作用是收集所有的任务实例对象,再调用类方法get()
,其作用就是等待所有的任务线程(allOf()
收集的)都执行完毕。// 等待所有的线程执行完毕 CompletableFuture.allOf(cfs.toArray(new CompletableFuture[] {})).get();
线程池
-
所谓 线程池 ,顾名思义,就像一个池子,里面装满了线程,随用随取。线程可以被 复用,一个线程,可以执行 A 任务,也可以执行 B 任务,于是线程不再频繁创建和销毁。
new Thread(register)
意味着一个线程对象只能执行一个任务,而线程池让线程与任务分离,不再紧密绑定。 -
创建线程池
// 线程工厂 private static final ThreadFactory namedThreadFactory = new BasicThreadFactory.Builder() .namingPattern("xxx-pool-%d")//线程名称模板 .daemon(true) .build(); // 等待队列 private static final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(1024);//1024表示能排队的任务个数 // 线程池服务 private static final ThreadPoolExecutor EXECUTOR_SERVICE = new ThreadPoolExecutor( 20, 200, 30, TimeUnit.SECONDS, workQueue, namedThreadFactory, new ThreadPoolExecutor.AbortPolicy() );
-
BasicThreadFactory
需要依赖一个库<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.10</version> </dependency>
-
ThreadPoolExecutor
构造函数参数参数序号 解释 1 线程池初始化核心线程数量,一般是两位数,通常不大 2 线程池最大线程数,计算机性能强就大一些,否则小一些,通常不超过 200 3 线程池中的线程数超过核心线程数时,如果一段时间后还没有任务指派,就回收了。想立即回收就填 0,一般 30 4 第三个参数的时间单位。30 + TimeUnit.SECONDS 表示 30 秒 5 等待队列实例,已经创建过了 6 线程工厂实例,已经创建过了 7 任务太多,超过队列的容量时,用什么样的策略处理。一般用 AbortPolicy 表示拒绝,让主程序自己处理 -
使用线程池运行任务:只要执行线程池对象的
execute()
方法,把实现了Runnable
接口的实例对象传入即可。 -
等待线程池执行:使用
main()
函数运行多线程程序,往往会遇到一个问题:多线程程序还没有运行完,main()
函数就退出了。可以使用Thread.sleep();
进行等待
线程池与并发容器
- 实际上,
CompletableFuture
内部也用到了线程池,它是把任务放入内部的默认线程池里执行的。CompletableFuture
也可以指定线程池来运行任务:supplyAsync()
方法可以有第二个参数,传入我们构建好的线程池对象。那么任务就是用指定的线程池而不是默认的。 - 当发现任务执行慢、任务堆积的问题时,就要考虑指定线程池,并调整线程池参数。