第一章 01合格的函数
函数就是一个规则
合格的函数就是只要你输入相同,无论多少次调用,不论什么时间调用,输出是相同的。
函数可以引用外部的数据,但是需要去保证外部的数据不可变
static关键字修饰的静态方法本质上和函数没有什么区别
02有形的函数
函数想要有形 要化为对象
public class MyClass {static int add(int a, int b) {return a + b;}
}
interface Lambda {int calculate(int a, int b);
}Lambda add = (a, b) -> a + b; // 它已经变成了一个 lambda 对象
两者的区别
前者纯粹就是一条两数加法的规则 他的位置是固定的 如果要使用他的话需要用类名.add去调用它 然后去执行
后置的话就是一个对象,他的位置是可以去改变的 哪里需要这个加法法则,把它传递过去就好
接口的目的是为了将来用他去执行函数对象,此接口中只能有一个方法定义
eg
public class Test {interface Lambda {int calculate(int a, int b);}
static class Server {public static void main(String[] args) throws IOException {ServerSocket ss = new ServerSocket(8080);System.out.println("server start...");while (true) {Socket s = ss.accept();Thread.ofVirtual().start(() -> {try {ObjectInputStream is = new ObjectInputStream(s.getInputStream());Lambda lambda = (Lambda) is.readObject();int a = ThreadLocalRandom.current().nextInt(10);int b = ThreadLocalRandom.current().nextInt(10);System.out.printf("%s %d op %d = %d%n",s.getRemoteSocketAddress().toString(), a, b, lambda.calculate(a, b));} catch (IOException | ClassNotFoundException e) {throw new RuntimeException(e);}});}}}
这是服务器端 只提供一些数据 运算方法的话是客户端去进行提供
static class Client1 {public static void main(String[] args) throws IOException {try(Socket s = new Socket("127.0.0.1", 8080)){Lambda lambda = (Lambda & Serializable) (a, b) -> a + b;ObjectOutputStream os = new ObjectOutputStream(s.getOutputStream());os.writeObject(lambda);os.flush();}}}static class Client2 {public static void main(String[] args) throws IOException {try(Socket s = new Socket("127.0.0.1", 8080)){Lambda lambda = (Lambda & Serializable) (a, b) -> a - b;ObjectOutputStream os = new ObjectOutputStream(s.getOutputStream());os.writeObject(lambda);os.flush();}}}static class Client3 {public static void main(String[] args) throws IOException {try(Socket s = new Socket("127.0.0.1", 8080)){Lambda lambda = (Lambda & Serializable) (a, b) -> a * b;ObjectOutputStream os = new ObjectOutputStream(s.getOutputStream());os.writeObject(lambda);os.flush();}}}
}
这些都是对象的形式 可以在服务器端直接进行调用
如果是这种形式
static class Client0{static int add(int a,int b){
return a+b;}
}
不能直接相互调用
但是有的人说可以把int前面的static去掉 把它改成成员方法 把client作为一个对象去发给服务器 服务器那边反序列化去调用这个add方法 这样需要一个前提就是 服务器那边需要有一个client这个类 相当于就是把实现绑定到了服务器端 没有实现数据和计算逻辑的分离
03行为参数化
public static void main(String[] args) {List<Student> students = List.of(new Student("张无忌", 18, "男"),new Student("杨不悔", 16, "女"),new Student("周芷若", 19, "女"),new Student("宋青书", 20, "男"));System.out.println(filter(students)); // 能得到 张无忌,宋青书
}static List<Student> filter(List<Student> students) {List<Student> result = new ArrayList<>();for (Student student : students) {if (student.sex.equals("男")) {result.add(student);}}return result;
}
如果需求再变动一下,要求找到 18 岁以下的学生,上面代码显然不能用了,改动方法如下
static List<Student> filter(List<Student> students) {List<Student> result = new ArrayList<>();for (Student student : students) {if (student.age <= 18) {result.add(student);}}return result;
}System.out.println(filter(students)); // 能得到 张无忌,杨不悔
像这样找到什么样的人 其实这样的就是一个规则 但是我们需要找到一个通用的规则
把它变成一个函数对象
函数对象的基本格式::
参数->逻辑部分
student—>student.sex.equals("男")
这样子就是一个函数对象
紧接着需要去定义一个接口去实现这些对象实现的逻辑
因为都是判断是否满足条件 只需要返回一个boolean就可以了
interface Lambda {boolean test(Student student);
}
static List<Student> filter(List<Student> students, Lambda lambda) {List<Student> result = new ArrayList<>();for (Student student : students) {if (lambda.test(student)) {result.add(student);}}return result;
}
这边的话第二个参数是lambda传过来的是对象 就是
filter(students, student -> student.sex.equals("男"));
以及
filter(students, student -> student.age <= 18);
只需要是在输出的时候
sout(filter0(student,student->student.age<18));
这样写的好处就是判断的逻辑不用卸载filter0的内部了
判断逻辑就是行为 就是把行为参数化
04延迟执行
在记录日志时,假设日志级别是 INFO,debug 方法会遇到下面的问题:
* 本不需要记录日志,但 expensive 方法仍被执行了
static Logger logger = LogManager.getLogger();public static void main(String[] args) {System.out.println(logger.getLevel());logger.debug("{}", expensive());
}static String expensive() {System.out.println("执行耗时操作");return "结果";
}
改进方法一 先加入if判断
if(logger.isDebugEnabled())logger.debug("{}", expensive());
显然这么做,很多类似代码都要加上这样 if 判断,很不优雅
改进方法2 在debug方法再套一个新方法,内部逻辑大概是这样
public void debug(final String msg, final Supplier<?> lambda) {if (this.isDebugEnabled()) {this.debug(msg, lambda.get());}
}
调用时这样:
logger.debug("{}", () -> expensive());
expensive() 变成了不是立刻执行,在未来 if 条件成立时才执行
二. 函数编程语法
01函数对象的表现形式
在 Java 语言中,lambda 对象有两种形式:lambda 表达式与方法引用
lambda 对象的类型是由它的行为决定的,如果有一些 lambda 对象,它们的入参类型、返回值类型都一致,那么它们可以看作是同一类的 lambda 对象,它们的类型,用函数式接口来表示
(int a,int b)——>a+b;
明确指出参数类型
(int a,int b)-》{int c=a+b;return c;}
代码多于一行的话,不能省略{}以及最后一行的return
lambda表达式看成一个函数表达式
如果没有明确指出参数类型的话 需要可以通过上下文来推导出参数的类型
lambda1 lambda=(a,b)-》a+b;
interface Lanbda1{
int op(int a,intb)
}
通过一个接口 正好可以把参数进行对应上 这样的话可以不屑参数类型
如果还有一个接口 的抽象方法
interface lambda2{
double op(double a,double b)
}
这个抽象方法也可以对应上 所以ab可以是double
a-》a;
只有一个参数,可以省略();
方法引用
02 函数对象 -练习
写出方法引用对应的lambda表达式
前面写参数 后面写方法
这个里面的student是未知的 就像第二个 前面要先创建一个 参数 然后去调用
03函数接口-自定义
参数个数类型相同 返回值类型相同 ----函数是接口 仅包含一个抽象犯法 用@functionallnterface来检查
@FunctionalInterface就是在编译期间 检查你这个接口里面是否只有一个抽象方法
都是无参 只是一个是student 一个是list 可以只定义成一个泛型 就可以两个一起使用
对上面进行简化 就拿最后一个来说 前面知道s的类型是student 所以后面 的student和()就可以省略掉
jdk里面提供的函数式接口
第一个obj1,2---》IntPredicate
obj3又三个参数 只能自己定义
obj4,5可以换成 IntBinaryOperator
obj6,7可以缓存Supplier
obj8,9可以换成Function 但是function和我们定义的正好相反 要把两个位置互换位置
04函数接口-jdk
comparator
负数前一个对象小 正数前一个对象大 0两个对象相等
05函数接口练习
把下列方法中,可能存在变化的部分,抽象为函数对象,从外界传递进来
eg2
将数字装换为字符串,但以后可能改变规则
我们就是把String.valueOf(number)这个东西拿出来
参数是number
(Integer number)-》String。valueOf
因为是一个参数一个返回值 所以是Function这个函数对象
在静态方法那边再添加一个参数Function《Integer,String》
下面写func。apply(number);
eg3
消费:打印,以后可能会改变消费规则
把它作为函数对象传进来
未知的是number 作为参数
number-》sout(number)
接收一个参数没有返回结果 对应consume
在上面多一个形参 consume《Interger》consume
打印那个逻辑就不用了 直接调用consume的accept方法 consume.accept(number);
eg4
生成:随机数,但以后可能改变生成规则
生成规则可能改变 可能以后生成的就不是随机数了 可能生成的是其他的数据
把要改变的作为函数对象从方法的外界传进来
这个逻辑规则不需要任何的参数 ()-》ThreadLocalRandom。current()。nextInt()
无参有返回值他的类型是suppiler
传入第二个参数 类型Supplier《Integer》 supplier
下面调用的是supplier的get方法
06方法引用-1
将现有方法的调用转换为函数对象
六种方法引用
1)类名::静态方法名
* 函数对象的逻辑部分是:调用此静态方法
* 因此这个静态方法需要什么参数,函数对象也提供相应的参数即可
Math::abs
(n)——》Math。abs(n)
Math::max
(a,b)->Math.max(a,b)
案例
挑选出男性学生 用filter
参数类型是学生对象 逻辑部分是判断是不是男性
(student stu)->stu.sex()equals("男")
public class Type2Test {public static void main(String[] args) {/*需求:挑选出所有男性学生*/Stream.of(new Student("张无忌", "男"),new Student("周芷若", "女"),new Student("宋青书", "男")).filter(Type2Test::isMale).forEach(student -> System.out.println(student));}static boolean isMale(Student student) {return student.sex.equals("男");}record Student(String name, String sex) {}
}
代码是使用静态方法引用 参数还是Student stu
逻辑部分要调用一个静态的方法 MethodRef1.isMale(stu)
然后创建一个静态方法
tatic boolean isMale(Student student) {return student.sex.equals("男");}
类名::静态方法名
* filter 这个高阶函数接收的函数类型(Predicate)是:一个 T 类型的入参,一个 boolean 的返回值
* 因此我们只需要给它提供一个相符合的 lambda 对象即可
* isMale 这个静态方法有入参 Student 对应 T,有返回值 boolean 也能对应上,所以可以直接使用
输出
```
Student[name=张无忌, sex=男]
Student[name=宋青书, sex=男]
```
方法引用-2
类名::非静态方法
如何理解:
* 函数对象的逻辑部分是:调用此非静态方法
* 因此这个函数对象需要提供一个额外的对象参数,以便能够调用此非静态方法
* 非静态方法的剩余参数,与函数对象的剩余参数一一对应
3.对象::非静态方法名
如何理解:
* 函数对象的逻辑部分是:调用此非静态方法
* 因为对象已提供,所以不必作为函数对象参数的一部分
* 非静态方法的剩余参数,与函数对象的剩余参数一一对应
p18
在stream流中定义的filter方法的对象和非静态方法需要我们去创建
我们先定义一个
我们希望只保留名字就可以了 使用map使用function
map的话可以使用三种形式 其中最简单的是使用类名::非静态方法的形式
09方法应用-4
类名::new
对于构造方法,也有专门的语法把它们转换为 lambda 对象
函数类型应满足
* 参数部分与构造方法参数一致
* 返回值类型与构造方法所在类一致
Student::new 如果构造的是无参的方法
-》new Student() ()-》new Student
如果构造的是有参的方法
-》new Student(name) (name)-》new Student(name)
要调用接口中的抽象方法才能真正去执行函数对象中的逻辑
sout(s1.get());
sout(s2.apply(“张三”));
sout(s3.apply(""里斯),25);
5)this::非静态方法名
6)super::非静态方法名
都属于对象::非静态方法的特例
区别是 五六只能在类的内部去使用 因为this和super就是在类的内部去使用的
foreach调用的是consume的函数对象 他是只有一个参数 没有放回值
eg::::::
system。out::println
filter是用的一个predicate的函数对象他的特点是一个参数放回一个boolean值
比较
特例
对于不需要返回值的函数接口,例如consume和runnable他们可以配合有放回置的函数对象使用
* 可以看到 Runnable 接口不需要返回值,而实际的函数对象多出的返回值也不影响使用
11-方法引用练习
练习一
练习二
练习三
12-闭包-1
何为闭包,闭包就是**函数对象**与**外界变量**绑定在一起,形成的整体。例如
```java
public class ClosureTest1 {interface Lambda {int add(int y);}public static void main(String[] args) {int x = 10;highOrder(y -> x + y);}static void highOrder(Lambda lambda) {System.out.println(lambda.add(20));}
}
* 代码中的 $y \rightarrow x + y$ 和 $x = 10$,就形成了一个闭包
* 可以想象成,函数对象有个背包,背包里可以装变量随身携带,将来函数对象甭管传递到多远的地方,包里总装着个 $x = 10$
* 有个限制,局部变量 x 必须是 final 或 effective final 的,effective final 意思就是,虽然没有用 final 修饰,但就像是用 final 修饰了一样,不能重新赋值,否则就语法错误。
* 意味着闭包变量,在装进包里的那一刻,就不能变化了
* 道理也简单,为了保证函数的不变性,防止破坏成道
* 闭包是一种给函数执行提供数据的手段,函数执行既可以使用函数入参,还可以使用闭包变量
int x=10前面实际是有final的 但是还有一个叫做effective final他的意思是虽然没有用 final 修饰,但就像是用 final 修饰了一样,不能重新赋值,否则就语法错误。(因为其他地方也没对x重新赋值)
闭包只是针对 外部来说看看外部是否是final进行修饰 而对对象的内部不管
比如说创建一个静态类student 然后创建stu对象 让他形成一个闭包 stt。d可以进行修改
闭包作用:给函数对象提供参数以外的数据
函数对象 只要用到了外面的变量 他们就组成了一个闭包
public static void main(String[] args) throws IOException {// 创建 10 个任务对象,并且每个任务对象给一个任务编号List<Runnable> list = new ArrayList<>();for (int i = 0; i < 10; i++) {int k = i + 1;Runnable task = () -> System.out.println(Thread.currentThread()+":执行任务" + k);list.add(task);}ExecutorService service = Executors.newVirtualThreadPerTaskExecutor();for (Runnable task : list) {service.submit(task);}System.in.read();}
}
13-柯里化-1
柯里化的作用是让函数对象分步执行(本质上是利用多个函数对象和闭包)
eg2
把三分数据合在一起,逻辑既定。但数据不能一次得到
14-高阶函数-内循环
所谓高阶函数 就是指他是其他函数对象的使用者
就拿step1函数的返回值是一个函数对象fb 那么这个step1就是一个高阶函数
step2 的参数和返回值都是函数对象 strp3参数用到了其他的函数对象
高阶函数——就是指他是其他函数对象的使用者
作用就是将通用,复杂的逻辑隐含在高阶函数内 将易变,未定的逻辑放在外部的函数对象中
是为了让系统有良好的扩展性
内循环
eg:内需遍历集合,只想负责元素处理,不改变集合
list和arraylist的底层实现都是基于数组的 我们用索引遍历没问题 但是我们link的数据实现 他的底层实现是基于链表的 他用索引方式去遍历的话(fori) 可能会遇到性能问题 所以我们最好去使用迭代器的方式来对集合进行遍历
写到这里 我们拿到value不是用高阶函数去处理这个值 我们不知道这个值是去打印还是放到数据库中 普通程序员处理这个东西 位置的逻辑用函数对象去传递进来 加一个参数 封装的是处理值的逻辑
提供一个函数对象 不需要结果 用consumer 把那个值交给consumer 具体的处理逻辑就交给consumer 高阶函数就不管了
做成泛型就i可以处理更多的元素类型了
二叉树遍历(未看完p27p28)
不想自己写二叉树遍历代码 不知道哪种遍历方式最好 对树节点进行只读操作
我们只需要让高级程序员去写一个高阶函数 只需要让他给我们普通程序员提供一个节点 我们拿到节点用函数对象去消费 这么消费由我们普通程序员写逻辑
遍历二叉树 有递归的实现 也有非递归的实现 由于递归的实现运用的是方法栈 当数的深度比较深的时候容易产生栈溢出 所以我们使用非递归地实现
第二章的高阶函数都跳过了
第三章 Stream api-过滤 映射
record Fruit(String cname, String name, String category, String color) { }
Stream.of(new Fruit("草莓", "Strawberry", "浆果", "红色"),new Fruit("桑葚", "Mulberry", "浆果", "紫色"),new Fruit("杨梅", "Waxberry", "浆果", "红色"),new Fruit("核桃", "Walnut", "坚果", "棕色"),new Fruit("草莓", "Peanut", "坚果", "棕色"),new Fruit("蓝莓", "Blueberry", "浆果", "蓝色")
)
找到所有浆果
.filter(f -> f.category.equals("浆果"))
找到蓝色的浆果
.filter(f -> f.category().equals("浆果") && f.color().equals("蓝色"))
方法2:让每个 lambda 只做一件事,两次 filter 相对于并且关系
.filter(f -> f.category.equals("浆果"))
.filter(f -> f.color().equals("蓝色"))
方法3:让每个 lambda 只做一件事,不过比方法2强的地方可以 or,and,nagate 运算
.filter(((Predicate<Fruit>) f -> f.category.equals("浆果")).and(f -> f.color().equals("蓝色")))
加酱
.map(f -> f.cname() + "酱")
降维
Stream.of(List.of(new Fruit("草莓", "Strawberry", "浆果", "红色"),new Fruit("桑葚", "Mulberry", "浆果", "紫色"),new Fruit("杨梅", "Waxberry", "浆果", "红色"),new Fruit("蓝莓", "Blueberry", "浆果", "蓝色")),List.of(new Fruit("核桃", "Walnut", "坚果", "棕色"),new Fruit("草莓", "Peanut", "坚果", "棕色"))
).flatMap(Collection::stream)
* 这样把坚果和浆果两个集合变成了含六个元素的水果流
Stream.of(new Order(1, List.of(new Item(6499, 1, "HUAWEI MateBook 14s"),new Item(6999, 1, "HUAWEI Mate 60 Pro"),new Item(1488, 1, "HUAWEI WATCH GT 4"))),new Order(1, List.of(new Item(8999, 1, "Apple MacBook Air 13"),new Item(7999, 1, "Apple iPhone 15 Pro"),new Item(2999, 1, "Apple Watch Series 9")))
)
想逐一处理每个订单中的商品
.flatMap(order -> order.items().stream())
这样把一个有两个元素的订单流,变成了一个有六个元素的商品流
构建流
用已有的数据 构建出stream对象
从集合构建 集合.stream() 从数组构建 Arrays.stream(数组)
从对象构建Stream.of(对象。。。。)
1.从集合构建‘
List.of(1,2,3).stream().forEach(System.out::println);
集合只要是collection或者是collection的子接口包括(list和set) 都可以这样去构建
map接口不能直接构建流
我们先创建一个map对象
Map.of("a",1,"b",2).entrySet().stream().forEach(System.out;;println);
然后通过entryset方法把它转换为一个set集合 接着把它转换成一个stream流
他打印的结果是a=1 b=2
2.数组构建流
int[] array={1,2.3};
Arrays.stream(array).forEach(System.out::println);
3.从对象构建流 把多个对象变成流
Stream.of(1,2.,3,4,5).forEach(System.out;;println);
03-Stream-合并与截取
将两个流合并成一个
Stream.concat(流1,流2)
截取流的一部分
根据位置 流.offset(?).limit(?)
根据条件 流.takeWhile(条件)
根据条件. 流.dropWhile(条件)
Stream.concat(Stream.of("a","b","c"), Stream.of("d"))
截取 -直接给出截取位置
.skip(long n) 跳过n个数据,保留剩下的
.limit(long n)保留n个数据剩下的不要
Stream.concat(Stream.of("a", "b", "c"), Stream.of("d")).skip(1).limit(2)
3.
* dropWhile 是 drop 流中元素,直到条件不成立,留下剩余元素 条件成立舍弃
* takeWhile 是 take 流中元素,直到条件不成立,舍弃剩余元素 条件成立保留
takeWhile(Predica p)
dropWhile(Predica p)
04-stream-生成
生成流
不用现有的数据生成stream对象
简单生成 依赖上一个值生成当前值 不依赖上一个值生成当前值
Intstream。rang stream中没有rang 只有intstream中有rang并且都是整数 包头不包尾
生成一到九
Intsstream。range(1,10)。forEach(system。out::println)
rangeClosed含头也含尾
IntSteam。rangeClosed(1,9).forEach(System。out::println);
生成13579的奇数序列 -------iterate可以根据流中上一个元素值生成当前元素值
IntStream。iterate(1,)第一个传的是初始的值 第二个传的是一个函数对象 intUnaryOperator 参数是int类型的 参数只有一个 参数和返回值的类型是统一的 都是基本类型的int
IntStream。iterate(1,x-》x+2).limit(10).forEach(System。out::println)limit是限制 获取十个奇数之后就停止了
另一种方式的实现
iterate还有一种三个参数的实现形式
IntStream。iterate(1,x-》x《=9,x-》x+2).forEach(System。out::println);
IntStream。generate 不跟据上一个元素生成下一个元素
IntStream。generate(()-》ThreaLocalRandom。current()。nextInt(100).limit(5).forEach(System。out::println))INtSuppiler 没有参数 返回一个int
如果只是想返回一个基本类型的数字流的话可以 ThreadLocalRandom。current()。ints(5,0,100).forEach(System。out::println)也是含头不含尾
05-Stream-查找与判断
查找 第一个是返回一个随机的 第二个是返回第一个
1. filter(Predicate p)。findAny()
2.filter(Predicate p)。findFirst()
判断
第一个是后面任意一个满足就true 第二个是全满足才true 第三个是全不满足才是true
1.anyMatch(Predicate p)
2.allMatch(Predicate p)
3.noneMatch(Predica p)
IntStream stream=IntStream。of(1,2,3,4,5,6,7);
sout(stream。filter(x-》(x&1)==0).findFirst());第一个filter筛选出来的是偶数 但是全部偶数 所以需要findFirst去把第一个偶数拿出来
他的返回值是 OptionalInt【2】
但为什么有前面的op呢?? 因为可能这一串数据中根本没有偶数 optional是可选的意思可以去用orElse去指定一个默认值 -----------------------》
sout(stream。filter(x-》(x&1)==0).findFirst()。orElse(-1));
找不到就返回一个-1
另一个用法 就是在后面用ifPresent去做检查 检查是否有值 后面需要的是一个IntConsumer 意思就是需要一个参数不需要返回结果
stream。filter(x-》(x&1)==0).findFirst()。ifPresent((x)-》sout(x));找到了这个值就打印 否则什么都不做
findAny 找到任意一个元素
就是把findFirst改成findAny
跟判断相关的方法里面都有一个match
ntStream stream=IntStream。of(1,2,3,4,5,6,7);
sout(stream。anyMatch(x-》(x&1)==0);
allMatch 和 noneMatch一样
06-Stream-去重和排序
IntStream。of(1,21,2,12,1,2,1)
.distinct()
。forEach(System。out::println)
distinct就是去重
//排序
。sorted()排序规则应该传一个函数对象去定义
sorted(a,b-》int)负数a小b大
eg
。sorted((a,b)->a。strength()《b。strength()?-1:a。strength()==b。strength()?0:1);
比较逻辑太罗嗦 更简洁的写法
在Interger中有一个compare方法就是这样类似的写法 我们把这个方法写进去
。sorted((a,b)-》Integer。compare(a。strength(),b。strength()))
能不能继续优化??
看comparator接口中 的比较器对象
。sorted(Comparator。comparingInt(h-》h。strength()))或者写成 类名::非静态方法
Hero::strength()
前面是按升序来进行排列 我们现在根据降序来进行排列 我们可以看一下 compartor中的reversed方法 这个方法就是改变原有的次序 将原来的升序改成一个降序
现在又想把按武力值降序排列 但是武力值相等的话按名字长度排序
comparator中有一个thenComparingInt方法 就是再比较
。thenComparingInt(h-》h。name()。length())
07-stream-化简
化简::流中元素两两合并 只剩一个
适合:求最大值 最小值 求和 求个数
。reduce((p,x)-》r)
p代表的是上次的合并结果 x是当前的元素 r是p和x本次的合并结果
现在要将武力值最大的调用出来
。reduce((h1,h2)-》h1.strength()》h2.streng()?h1:h2);。var之后他的返回结果是optional可选 的 为啥是可选的呢 是因为如果流中一个元素都没有的话 他不想返回一个null' 不想要的话我们可以准备一个初始值 流中没有其他元素的 话可以用初始值‘
。reduce(new Hero(“-”),-1,(h1,h2)-》h1.strength()》h2.streng()?h1:h2)这样的话他的返回值就不是optional了
如果要求高手的总数的话
先把每个高手转换成数字1
stream。map(h-》1).reduce(0,(a,b)-》a+b);
其他方法
stream。count();和上面的逻辑一样 也是先把高手映射成一 然后再reduce
求最大值
stream。max(Compartor。comparingInt(Hero::strength)) 里面有比较器和提取器
最小值换成min
求和
求和的话对象和对象不能求和 要先把对象变成一个整数流
steram。mapToInt(h-》h。strength())。sum()
平均值是。average
08-stream-收集
将一个元素收集到一个容器中
。collect(()-c,(c,x)-》void)第一个是创建容器的 第二个参数是负责把流中的元素x放入创建的容器c中 第三个参数咱们先写(a,b)-》{}
有一组数据 我们想把数据放入list对象中
stream。collect(()-》new ArrayList《》(),(list,x)-》List。add(x),(a,b)-》{});
改为方法引用
stream。collect(ArrayList::new,ArrayList::add,a,b)-》{});
如果我们想放到一个set容器中
stream。collect(Hashset::new,Set::add,(a,b)-》{})因为是hashset所以顺序可能会被打乱
如果我们想要保证顺序的话 还有一个linkedHashSet
tream。collect(linkedHashSet::new,Set::add,(a,b)-》{})
放到一个map容器中
tream。collect(HashMap::new,(map,x)-》map。put(x,1),(a,b)-》{})
在这的话不能使用map::put
(map,k,v)-》map。put(k,v)
想把所有流中的数据放到一个字符串容器中
stream。collect(StringBuilder::new,StringBuilder::append,(a,b)-》{});
想拼接的时候加入分隔符可以用 StringJoiner 但是他没有无参构造 所以第一个参数不能用方法引用的形式用lamda 的形式
StringJoiner sb=stream。collect(()-》new StringJoiner(“,”),StringJoiner::add,(a,b)-》{})
09-stream-收集器
jdk给我们提供好的收集器 再collectors的工具类中
1。 收集到list中
stream。collect(Collectors。toList());
2。收集到set
stream。collect(Collectors。toSet())
收集到StringBuilder
stream。collect(Collectors。joini())
收集到stringjoiner中
stream。collect(Collectors。joining(“,”))
收集到map
stream。collect(Collectors。toMap(x-》x,x-》-1));
map。entrySet()。fori
如果我们想按名字来对对象进行分类
map
3作为key
3:new ArrayList(【“令狐冲”,“风清扬”】)
4:new ArrayList(【“独孤求败”,“东风不败”】)
2:new ArrayList(【’”方正“】)
需要两个收集器才能完成代码
stream。collect(Collector。groupingBy(x-》x。length(),值本身是一个ArrayList Collectors。toList))
10-STREAM-下游收集器1
像stream。collect(Collector。groupingBy(x-》x。length(),值本身是一个ArrayList Collectors。toList))
这行代码中collectors。tolist就是下游收集器
我们想把之前的数据放到一个字符串中 中间用,分割
只需要把。tplist改为joining(“,”)就可以了 他返回的就不是一个list而是一个string了
跟grouping配合的下游收集器有很多
需求 根据名字长度分组 分组后只保留他们的武力值
mapping(x-》y,dc)第一个参数是一个function对象 他把new Hero(“令狐冲“,90)转换为90 只保留一个武力值 后面的dc是down collector 下游收集器的作用
stream.collect(Collectors.groupingBy(h->h.name().length(),mapping(h->h.strength(),toList())));
这边的话像这个mapping和toList之前是应该有一个collection。的但是可以省略是因为我们运用了一个静态导入 像grouping也可以省略前面的collectinon 这么省略呢 就是我们可以选中 然后按下 alt加enter 提示里面第二项就是 Add static import for”java。util。stream。Collectiors。groupingBy“
这样的话类名那边就有了静态导入
如果我们用的多了的话 完全可以再导入类那边 写
import static java。util。stram。Collectors。*;
10-stream-下游收集器-2
需求:根据名字长度分组 分组之后把组内武力小于九十的过滤掉
filtering(x->boolean,dc)
stream.collect(groupingBy(h->h,name().length(),filtering(h->h.strength()>=90,toList())));
这种做法是先分组再过滤 我们可以先过滤再分组
stream.filter(h->h.strength>=90).collect(groupingBy(h->h.name().length(),toList()))
需求:根据名字的长度分组 分组后组内保留人名,并且人名切分为单个字符
flat Mapping(x-》substream,dc)
chars的作用就是可以返回一个整数流 比如令狐冲。chars就是把每个字符都转换成一个整数 然后放到流中
如果把”令狐冲“。chars()。forEach(sout)消费打印的话会生成三个整数数字
接下来我们要把这个整数流装换成字符 。mapToObj(x->Charater.toSting(x))就可以把数字转换成对应的字符
chars()。。mapToObj(x->Charater.toSting(x))。forEach(sout)
tostring是一个静态方法 他接收一个参数返回一个string 正好对应我们的function函数对象
我们可以用类名加静态方法的方法引用去替换
chars()。。mapToObj(Charater::toSting)。forEach(sout)
stream.collect(groupingBy(h->h,name().length(),flatMapping(h->h.name().chars()。。mapToObj(x->Charater.toSting(x)),toList()));
根据名字长度分组,分组后求每组的个数
counting()
stream.collect(groupingBy(h->h,name().length(),counting());
需求:
根据名字长度分组 分组后每组武功最低的人
minBy((a,b)->int)
stream.collect(groupingBy(h->h,name().length(),minBy(Comparator.comparingInt(Hero::strength)));
最大值就是maxBy
summingInt(x->int)根据名字长度分组。分组后求每组武力和
averagingDouble(x->double)根据名字长度分组,分组后求每组武力平均值
stream.collect(groupingBy(h->h,name().length(),averagingDouble(h->h.strength()));
collect.entrySet()fori
前面接触到的收集器都是reducing的简化方式
reducing(init,(p,x)->r)
stream.collect(groupingBy(h->h,name().length(),maping(h->h.strength(),reducing(0,(p,x)->p+x)));
11-stream-基本类型流
三种基本流 流中的数据类型是基本数据类型 intstream longstream doublestream
他的好处是性能稳定 占用空间少
stream《inteage》 d=stream。of(1,2,3)
如果要用普通流的话 就会把这些一二三转换成对应的包装类型 integer 占用的空间和效率都不如intstream
把对象流转换为基本类型流
12-stream-1特性
掌握流的两个特性
一次使用 两类操作
stream<integer> s1=stream.of(1,2,3,4,5);
s1.forEach(System.out::println);
只能调用一次foreach 不能再调用使用了 流只能使用一次
两类操作是 中间操作lazy懒惰和终结操作eager迫切
s1.map(x->x+1).filter(x->x<=5).forEach(sout);
这个加一操作和filter操作都是中间操作 只是在用到这个元素的时候才去调用
foreach是迫切的去处理操作 只有到了foreach的时候才会触发懒惰操作的处理
stream<integer> s1=stream.of(1,3,5);
s1
.map(x->x+1).
filter(x->x<=5).
forEach(sout);
stream总结
file:///C:/Users/86132/AppData/Local/Temp/MiaoRarO0424842D/stream.html
13-stream-并行-1
我们之前用的是串行流
并行流就是底层使用了多线程
就是多加上一个。parallel()
List<Integer> collect=Stream.of(1,2,3,4)
.paraller()
.collect(Collector.of(
如何创建容器()->new ArrayList();
如何向容器中添加数据(list,x)->list.add(x)
如何合并容器的数据(list1,list2)->{
list1.addAll(list2);
retuen list1;}
,收尾list->list
特性:是否支持并发 是否需要收尾 是否要保证收集顺序
默认不支持并发 会支持收尾逻辑 保证收集顺序
));
我们得先创建一个自己的收集器{
如何创建容器
如何向容器中添加数据
如何合并容器的数据
收尾
特性:是否支持并发 是否需要收尾 是否要保证收集顺序
}
我们在stream。of中才四个数据就运用到了四个线程
1)数据量问题 数据量大才建议用并行流
2)线程不会无线增加 看电脑的逻辑处理器cpu
3)收尾的意义 我们有些情况下希望list是不可变的 我们就可以在收尾的时候
return Collections。unmodifiableList(list)
或者stringbuilder转string
4)线程是否安全 arraylist容易出现线程安全问题 但是这个他是每个线程都有自己的arraylist 不会出现共享
14-stream-效率-1
## 效率
### 1) 数组求和
其中
* primitive 用 loop 循环对 int 求和
* intStream 用 IntStream 对 int 求和
* boxed 用 loop 循环对 Integer 求和
* stream 用 Stream 对 Integer 求和
元素个数 100
| Benchmark | Mode | Cnt | Score (ns/op) | Error (ns/op) | Units |
| ---------------- | ---- | ---- | ------------- | ------------- | ----- |
| T01Sum.primitive | avgt | 5 | 25.424 | ± 0.782 | ns/op |
| T01Sum.intStream | avgt | 5 | 47.482 | ± 1.145 | ns/op |
| T01Sum.boxed | avgt | 5 | 72.457 | ± 4.136 | ns/op |
| T01Sum.stream | avgt | 5 | 465.141 | ± 4.891 | ns/op |
元素个数 1000
| Benchmark | Mode | Cnt | Score (ns/op) | Error (ns/op) | Units |
| ---------------- | ---- | ---- | ------------- | ------------- | ----- |
| T01Sum.primitive | avgt | 5 | 270.556 | ± 1.277 | ns/op |
| T01Sum.intStream | avgt | 5 | 292.467 | ± 10.987 | ns/op |
| T01Sum.boxed | avgt | 5 | 583.929 | ± 57.338 | ns/op |
| T01Sum.stream | avgt | 5 | 5948.294 | ± 2209.211 | ns/op |
元素个数 10000
| Benchmark | Mode | Cnt | Score (ns/op) | Error (ns/op) | Units |
| ---------------- | ---- | ---- | ------------- | ------------- | ----- |
| T01Sum.primitive | avgt | 5 | 2681.651 | ± 12.614 | ns/op |
| T01Sum.intStream | avgt | 5 | 2718.408 | ± 52.418 | ns/op |
| T01Sum.boxed | avgt | 5 | 6391.285 | ± 358.154 | ns/op |
| T01Sum.stream | avgt | 5 | 44414.884 | ± 3213.055 | ns/op |
结论:
* 做数值计算,优先挑选基本流(IntStream 等)在数据量较大时,它的性能已经非常接近普通 for 循环
* 做数值计算,应当避免普通流(Stream)性能与其它几种相比,慢一个数量级
### 2) 求最大值
其中(原始数据都是 int,没有包装类)
* custom 自定义多线程并行求最大值
* parallel 并行流求最大值
* sequence 串行流求最大值
* primitive loop 循环求最大值
元素个数 100
| Benchmark | Mode | Cnt | Score (ns/op) | Error (ns/op) | Units |
| --------------------- | ---- | ---- | ------------- | ------------- | ----- |
| T02Parallel.custom | avgt | 5 | 39619.796 | ± 1263.036 | ns/op |
| T02Parallel.parallel | avgt | 5 | 6754.239 | ± 79.894 | ns/op |
| T02Parallel.primitive | avgt | 5 | 29.538 | ± 3.056 | ns/op |
| T02Parallel.sequence | avgt | 5 | 80.170 | ± 1.940 | ns/op |
元素个数 10000
| Benchmark | Mode | Cnt | Score (ns/op) | Error (ns/op) | Units |
| --------------------- | ---- | ---- | ------------- | ------------- | ----- |
| T02Parallel.custom | avgt | 5 | 41656.093 | ± 1537.237 | ns/op |
| T02Parallel.parallel | avgt | 5 | 11218.573 | ± 1994.863 | ns/op |
| T02Parallel.primitive | avgt | 5 | 2217.562 | ± 80.981 | ns/op |
| T02Parallel.sequence | avgt | 5 | 5682.482 | ± 264.645 | ns/op |
元素个数 1000000
| Benchmark | Mode | Cnt | Score (ns/op) | Error (ns/op) | Units |
| --------------------- | ---- | ---- | ------------- | ------------- | ----- |
| T02Parallel.custom | avgt | 5 | 194984.564 | ± 25794.484 | ns/op |
| T02Parallel.parallel | avgt | 5 | 298940.794 | ± 31944.959 | ns/op |
| T02Parallel.primitive | avgt | 5 | 325178.873 | ± 81314.981 | ns/op |
| T02Parallel.sequence | avgt | 5 | 618274.062 | ± 5867.812 | ns/op |
结论:
* 并行流相对自己用多线程实现分而治之更简洁
* 并行流只有在数据量非常大时,才能充分发力,数据量少,还不如用串行流
### 3) 并行(发)收集
元素个数 100
| Benchmark | Mode | Cnt | Score (ns/op) | Error (ns/op) | Units |
| -------------------- | ---- | ---- | ------------- | ------------- | ----- |
| loop1 | avgt | 5 | 1312.389 | ± 90.683 | ns/op |
| loop2 | avgt | 5 | 1776.391 | ± 255.271 | ns/op |
| sequence | avgt | 5 | 1727.739 | ± 28.821 | ns/op |
| parallelNoConcurrent | avgt | 5 | 27654.004 | ± 496.970 | ns/op |
| parallelConcurrent | avgt | 5 | 16320.113 | ± 344.766 | ns/op |
元素个数 10000
| Benchmark | Mode | Cnt | Score (ns/op) | Error (ns/op) | Units |
| -------------------- | ---- | ---- | ------------- | ------------- | ----- |
| loop1 | avgt | 5 | 211526.546 | ± 13549.703 | ns/op |
| loop2 | avgt | 5 | 203794.146 | ± 3525.972 | ns/op |
| sequence | avgt | 5 | 237688.651 | ± 7593.483 | ns/op |
| parallelNoConcurrent | avgt | 5 | 527203.976 | ± 3496.107 | ns/op |
| parallelConcurrent | avgt | 5 | 369630.728 | ± 20549.731 | ns/op |
元素个数 1000000
| Benchmark | Mode | Cnt | Score (ms/op) | Error (ms/op) | Units |
| -------------------- | ---- | ---- | ------------- | ------------- | ----- |
| loop1 | avgt | 5 | 69.154 | ± 3.456 | ms/op |
| loop2 | avgt | 5 | 83.815 | ± 2.307 | ms/op |
| sequence | avgt | 5 | 103.585 | ± 0.834 | ns/op |
| parallelNoConcurrent | avgt | 5 | 167.032 | ± 15.406 | ms/op |
| parallelConcurrent | avgt | 5 | 52.326 | ± 1.501 | ms/op |
结论:
* sequence 是一个容器单线程收集,数据量少时性能占优
* parallelNoConcurrent 是多个容器多线程并行收集,时间应该花费在合并容器上,性能最差
* parallelConcurrent 是一个容器多线程并发收集,在数据量大时性能较优
### 4)MethodHandle 性能
正常方法调用、反射、MethodHandle、Lambda 的性能对比
| Benchmark | Mode | Cnt | Score | Error | Units |
| ------------------ | ----- | ---- | ------------- | --------------- | ----- |
| Sample2.lambda | thrpt | 5 | 389307532.881 | ± 332213073.039 | ops/s |
| Sample2.method | thrpt | 5 | 157556577.611 | ± 4048306.620 | ops/s |
| Sample2.origin | thrpt | 5 | 413287866.949 | ± 65182730.966 | ops/s |
| Sample2.reflection | thrpt | 5 | 91640751.456 | ± 37969233.369 | ops/s |
第四章 统计分析 a.准备
1.数据统计分析 streamapi大展身手
2.异步处理 关键词completableFuture
3.框架设计 函数对象齐上阵
4.函数编程 并行计算
5.ui事件
首先订单数据是在一个文件里面 我们最好是直接读取的时候就是让他以流的形式进来
最好的方法就是file。lines 这个方法接收的是文件的路径 放回的是streasm流 文件对应的stream流我们要用close去关闭 我们要调用。twr让他最后关闭
先只截断前五条进行一个消费 lines。limit(5)。foreach(line-》sout(line))
跳过第一行的话加一个。skip(1)
eg:统计一下每月的销售量
现在就是一个按年份和月份进行分组的问题
首先先把年月日进行分割 拿出年月来 然后根据年月分组
将字符串转换成数组 用map中的split方法
这样的话打印的时候就变成数组了 但是数组不能去直接打印 我们要打印数组里面的东西 就调用arrays。tostring
分组的话我们用collect方法 然后用groupingby收集器 内部会采用map容器进行收集
groupingby两个参数 function(提取key) 指定值的下游收集器
因为collect是一个终结操作 所以就不要sout进行终结了
数组的索引1就是key 值的话就是进行一个计数 就用counting
结果的话不是我们想要的 因为我们只想要年月而他还有其他多余的
array【1】是字符串的日期对象 我们要提取他的年和月不太方便 我们可以先把他转化为日期对象
字符串转日期对象用datatimeformatter
创建datatimeformatter的时候
static final DateTimeFormatter formatter =DatetimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
调用他的parse方法 就可以把字符串日期转换为日期对象了 这还是完整带有日期的 我们可以用YearMouth。from方法 返回最后的map集合
我们现在收集用的容器 groupingby默认的话用的是hashmap 他不能保证容器顺序 改成有序的话 我们改为treemap就ok
接着看groupingby有没有重载方式
我们可以调用三个参数的goupingby 将第二个参数改为treemap
只需要再from的括号里加入第二个参数 treemap::new就ok
为了以后更好的维护和扩展的话 我们可以设置一些常量值 这样的话就不用知道一对应什么了 直接改为array{time}
```java
lines.skip(1).map(l -> l.split(",")).collect(groupingBy(a -> YearMonth.from(formatter.parse(a[TIME])), TreeMap::new, counting())).forEach((k, v) -> {System.out.println(k + " 订单数 " + v);});
```
eg:统计销量最高的月份
我们只需要再map集合中找一个value值最大的
map中没有一个方法能把他变成流 我们得先拿到他的entrystream 然后就可以用流的最大值来求了
max里面要接收的是一个比较器
.max(Comparator.comparingLong(Map.Entry::getValue))
// 也可以用 Map.Entry.comparingByValue()
可以用这两个都可以 第二种更方便
3.求销量最高的商品
先files。lines读取文件变成流 然后。twr确定流会被关闭 先调用linses。skip(1)跳过他的第一行是标题行 将流中的数据进行map转换 用,切换成列 然后根据商品id作为参数 下游还是counting
将map变成一个流 需要先调用entryset然后在调用stream 接下来调用流中的max方法
map。entry。comparingByvalue()
4,下单最多的前10用户
我们用用户id分组 流的排序可以用sorted
```java
lines.skip(1)
.map(l -> l.split(","))
.collect(groupingBy(a -> a[USER_ID], counting()))
.entrySet()
.stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(10).forEach(e -> {
System.out.println(e.getKey() + " 订单数 " + e.getValue());
});
```
第二种写法 因为我们只想要排名前十的数据我们可以用小顶堆这种写法 堆顶是最小的
将订单数作为他的key
```java
static class MyQueue<E> extends PriorityQueue<E> {
private int max;
public MyQueue(Comparator<? super E> comparator, int max) {
super(comparator);
this.max = max;
}
@Override
public boolean offer(E e) {
boolean r = super.offer(e);
if (this.size() > max) {
this.poll();
}
return r;
}
}
lines.skip(1)
.map(l -> l.split(","))
.collect(groupingBy(a -> a[USER_ID], counting()))
.entrySet()
.stream()
.parallel()
.collect(
() -> new MyQueue<>(Map.Entry.comparingByValue(), 10),
MyQueue::offer,
AbstractQueue::addAll
);
```
这里用到数据结构了 不太会(上面的)
每个地区下单最多的前3用户
这个是下单最多的用户
05统计分析 按类别统计
```javalines.skip(1).map(l -> l.split(",")).filter(a -> !a[CATEGORY_CODE].isEmpty()).collect(groupingBy(a -> a[CATEGORY_CODE], TreeMap::new, counting())).forEach((k, v) -> {System.out.println(k + " 订单数 " + v);});
```
主要就是根据类别进行一个分组 分组之后因为他是hashmap所以顺序是乱的 所以我们加一个treemap::new 然后有的订单会没有类别 所以要加一个filter来判断是否类别列为空
按一级类别统计销量
刚才的配件下有包也有伞 统一按配件来统计的话 就把这两个合并起来了
第一种是按小需求进行统计 然后进行合并
第二种是只拿一级类别进行分组
第二种 ::
拿到类别小数点之前的那个代码
lambda表达式中代码比较多的话 可以把这些代码抽象为一个方法 把lambda作为一个方法引用
```java
lines.skip(1).map(l -> l.split(",")).filter(a -> !a[CATEGORY_CODE].isEmpty()).collect(groupingBy(TestData::firstCategory, TreeMap::new, counting())).forEach((k, v) -> {System.out.println(k + " 订单数 " + v);});
``````java
static String firstCategory(String[] a) {String category = a[CATEGORY_CODE];int dot = category.indexOf(".");return category.substring(0, dot);
}
```
06-统计分析-按价格区间统计销量
* p <100
* 100<= p <500
* 500<=p<1000
* 1000<=p
结果应为
```
[0,100)=291624
[1000,∞)=14514
[500,1000)=52857
[100,500)=203863
```
在提取价格的时候 提取到的是一个字符串 我们要把字符串去转换为一个double
用double。valueof
```java
static String priceRange(Double price) {if (price < 100) {return "[0,100)";} else if (price >= 100 && price < 500) {return "[100,500)";} else if (price >= 500 && price < 1000) {return "[500,1000)";} else {return "[1000,∞)";}
}lines.skip(1).map(line -> line.split(",")).map(array -> Double.parseDouble(array[PRICE])).collect(groupingBy(TestData::priceRange, counting()))
```
不同年龄段女性所下不同类别订单
* a < 18
* 18 <= a < 30
* 30 <= a < 50
* 50 <= a
[0,18) accessories 81
[0,18) apparel 60
[0,18) appliances 4326
[0,18) computers 1984
...
[18,30) accessories 491
[18,30) apparel 488
[18,30) appliances 25240
[18,30) computers 13076
...
[30,50) accessories 890
[30,50) apparel 893
[30,50) appliances 42755
[30,50) computers 21490
...
[50,∞) accessories 41
[50,∞) apparel 41
[50,∞) appliances 2255
[50,∞) computers 1109
...
```
```java
static String ageRange(String[] array) {
int age = Double.valueOf(array[USER_AGE]).intValue();
if (age < 18) {
return "[0,18)";
} else if (age < 30) {
return "[18,30)";
} else if (age < 50) {
return "[30,50)";
} else {
return "[50,∞)";
}
}
lines.skip(1)
.map(line -> line.split(","))
.filter(array -> !array[CATEGORY_CODE].isEmpty())
.filter(array -> array[USER_SEX].equals("女"))
.collect(groupingBy(TestData::ageRange,
groupingBy(TestData::firstCategory, TreeMap::new, counting())))
```
07-异步处理-线程池-1
这样的话我们就可以把同步操作变成一个异步操作
try里面建立的是一个线程池 在线程池里面开始统计和执行其他操作没有放到线程池中 所以他们两个还是由main函数进行操作的
这个统计是我们封装在一个方法中 我们如果要在方法外面修改这个方法 就可以把这个方法作为参数 之后就是方法的调用者去处理
第二个方法是将处理的逻辑作为一个函数对象 然后通过方法的参数传给他 这样就不需要返回值了
将他作为函数对象传进去 在方法那边是接收一个参数然后没有返回值的话就是用consume
如果我们现在想把它的逻辑改变一下 将他写到一个文件中
: 使用`CompletableFuture
上面的方法显示使用了线程池 并且他的函数对象嵌套使用 可读性变差
使用completablefuture
1.异步执行 任务
completablefuture。runAsync()在任务不需要返回结果时
completablefuture。supplyAsync()在任务需要处理结果时
completablefuture。runAsync(()-》logger。info(“异步操作”));
当执行这行代码的时候没有返回值 因为这两个代码都是守护线程 主线程执行完了的话守护线程就不会执行了 因为主线程没有代码 所以守护线程来不及执行代码
我们不能让主线程立刻结束 加一句
system,in。read()
completablefuture。supplyAsync(()-》{
logger.info("异步操作2");
return “结果”
}) 需要的是无参但是需要一个返回值
这个返回的结果去哪里了呢???
处理异步处理的结果
```java
static Logger logger = LoggerFactory.getLogger("Test");public static void main(String[] args) throws InterruptedException {logger.info("开始统计");CompletableFuture.supplyAsync(() -> monthlySalesReport()).thenAccept(map -> map.entrySet().forEach(e -> logger.info(e.toString())));logger.info("执行其它操作");Thread.sleep(10000);
}private static Map<YearMonth, Long> monthlySalesReport() {try (Stream<String> lines = Files.lines(Path.of("./data.txt"))) {Map<YearMonth, Long> collect = lines.skip(1).map(line -> line.split(",")).collect(groupingBy(array -> YearMonth.from(formatter.parse(array[TIME])), TreeMap::new, counting()));return collect;} catch (IOException e) {throw new RuntimeException(e);}
}
```
## 框架设计
* 什么是框架?
* 半成品软件,帮助开发者快速构建应用程序
* 框架提供的都是固定**不变的**、**已知的**、可以重用的代码
* 而那些每个应用不同的业务逻辑,**变化的**、**未知的**部分,则在框架外由开发者自己实现
09-框架设计-未知交给子类
-***********************************
************************
剩下最后几个视频了 下次补