今日目标
线程状态
等待与唤醒
Lambda表达式
Stream流
第一章JDK8新特性
JDK新特性:
Lambda 表达式
默认方法 【已学习过】
Stream API
方法引用
Base64
1.1 方法引用 5.1.1 方法引用概述 方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。
5.1.2 方法引用基本使用 方法引用使用一对冒号 :: 。
下面,我们在 Car 类中定义了 4 个方法作为例子来区分 Java 中 4 种不同方法的引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static class Car { public static Car create ( final Supplier< Car > supplier ) { return supplier.get(); } public static void collide ( final Car car ) { System.out.println( "Collided " + car.toString() ); } public void follow ( final Car another ) { System.out.println( "Following the " + another.toString() ); } public void repair () { System.out.println( "Repaired " + this .toString() ); } }
第一种方法引用的类型是构造器引用 ,语法是Class::new ,或者更一般的形式:Class::new 。注意:这个构造器没有参数。
1 2 final Car car = Car.create( Car::new );final List< Car > cars = Arrays.asList( car );
第二种方法引用的类型是静态方法引用 ,语法是Class::static_method 。注意:这个方法接受一个Car类型的参数。
1 cars.forEach( Car::collide );
第三种方法引用的类型是某个类的成员方法的引用 ,语法是Class::method ,注意,这个方法没有定义入参:
1 cars.forEach( Car::repair );
第四种方法引用的类型是某个实例对象的成员方法的引用 ,语法是instance::method 。注意:这个方法接受一个Car类型的参数:
1 2 final Car police = Car.create( Car::new );cars.forEach( police::follow );
5.1.3 基于静态方法引用的代码演示 1 2 3 4 5 6 7 8 9 public static void main (String args[]) { List names = new ArrayList(); names.add("大明" ); names.add("二明" ); names.add("小明" ); names.forEach(System.out::println); }
上面的代码,我们将 System.out::println 方法作为静态方法来引用。
测试结果为:
第二章 Lambda表达式,研究 2.1 函数式编程思想概述 在数学中,函数 就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做 。
做什么,而不是怎么做
我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不 创建一个对象。我们真正希望做的事情是:将run
方法体内的代码传递给Thread
类知晓。
传递一段代码 ——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。
2.2 Lambda的优化 当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable
接口来定义任务内容,并使用java.lang.Thread
类来启动该线程。
传统写法,代码如下:
1 2 3 4 5 6 7 8 9 10 public class Demo01ThreadNameless { public static void main (String[] args) { new Thread(new Runnable() { @Override public void run () { System.out.println("多线程任务执行!" ); } }).start(); } }
本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable
接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。
代码分析:
对于Runnable
的匿名内部类用法,可以分析出几点内容:
Thread
类需要Runnable
接口作为参数,其中的抽象run
方法是用来指定线程任务内容的核心;
为了指定run
的方法体,不得不 需要Runnable
接口的实现类;
为了省去定义一个RunnableImpl
实现类的麻烦,不得不 使用匿名内部类;
必须覆盖重写抽象run
方法,所以方法名称、方法参数、方法返回值不得不 再写一遍,且不能写错;
而实际上,似乎只有方法体才是关键所在 。
Lambda表达式写法,代码如下:
借助Java 8的全新语法,上述Runnable
接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:
1 2 3 4 5 public class Demo02LambdaRunnable { public static void main (String[] args) { new Thread(() -> System.out.println("多线程任务执行!" )).start(); } }
这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!
2.3 Lambda的格式 标准格式: Lambda省去面向对象的条条框框,格式由3个部分 组成:
Lambda表达式的标准格式 为:
格式说明:
小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
->
是新引入的语法格式,代表指向动作。
大括号内的语法与传统方法体要求基本一致。
匿名内部类与lambda对比:
1 2 3 4 5 6 new Thread(new Runnable() { @Override public void run () { System.out.println("多线程任务执行!" ); } }).start();
仔细分析该代码中,Runnable
接口只有一个run
方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个方法):
无参数 :不需要任何条件即可执行该方案。
无返回值 :该方案不产生任何结果。
代码块 (方法体):该方案的具体执行步骤。
同样的语义体现在Lambda
语法中,要更加简单:
1 () -> System.out.println("多线程任务执行!" )
前面的一对小括号即run
方法的参数(无),代表不需要任何条件;
中间的一个箭头代表将前面的参数传递给后面的代码;
后面的输出语句即业务逻辑代码。
参数和返回值: 下面举例演示java.util.Comparator<T>
接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1, T o2);
当需要对一个对象数组进行排序时,Arrays.sort
方法需要一个Comparator
接口实例来指定排序的规则。假设有一个Person
类,含有String name
和int age
两个成员变量:
1 2 3 4 5 6 public class Person { private String name; private int age; }
传统写法
如果使用传统的代码对Person[]
数组进行排序,写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Demo06Comparator { public static void main (String[] args) { Person[] array = { new Person("古力娜扎" , 19 ), new Person("迪丽热巴" , 18 ), new Person("马尔扎哈" , 20 ) }; Comparator<Person> comp = new Comparator<Person>() { @Override public int compare (Person o1, Person o2) { return o1.getAge() - o2.getAge(); } }; Arrays.sort(array, comp); for (Person person : array) { System.out.println(person); } } }
这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator
接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。
代码分析
下面我们来搞清楚上述代码真正要做什么事情。
为了排序,Arrays.sort
方法需要排序规则,即Comparator
接口的实例,抽象方法compare
是关键;
为了指定compare
的方法体,不得不 需要Comparator
接口的实现类;
为了省去定义一个ComparatorImpl
实现类的麻烦,不得不 使用匿名内部类;
必须覆盖重写抽象compare
方法,所以方法名称、方法参数、方法返回值不得不 再写一遍,且不能写错;
实际上,只有参数和方法体才是关键 。
Lambda写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Demo07ComparatorLambda { public static void main (String[] args) { Person[] array = { new Person("古力娜扎" , 19 ), new Person("迪丽热巴" , 18 ), new Person("马尔扎哈" , 20 ) }; Arrays.sort(array, (Person a, Person b) -> { return a.getAge() - b.getAge(); }); for (Person person : array) { System.out.println(person); } } }
省略格式: 省略规则
在Lambda标准格式的基础上,使用省略写法的规则为:
小括号内参数的类型可以省略;
如果小括号内有且仅有一个参 ,则小括号可以省略;
如果大括号内有且仅有一个语句 ,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。
可推导即可省略
Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:
1 2 3 4 Runnable接口简化: 1 . () -> System.out.println("多线程任务执行!" )Comparator接口简化: 2 . Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());
2.4 Lambda的前提条件 Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法 。 无论是JDK内置的Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
使用Lambda必须具有上下文推断 。 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为“函数式接口 ”。
2.5 Lambda表达式和匿名内部类的区别
所需类型不同 匿名内部类:可以是接口,也可以是抽象类,还可以是具体类。 Lambda表达式:只能是接口
使用限制不同 如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类。 如果接口中多于一个抽象方法,那么只能使用匿名内部类,而不能使用Lambda表达式。
实现原理不同 匿名内部类:编译之后会产生一个单独的.class字节码文件 Lambda表达式:编译之后不会产生一个单独的.class字节码文件。对应的字节码会在运行的时候动态生成。
第三章 Stream 在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念 ,用于解决已有集合类库既有的弊端。
3.1 引言 传统集合的多步遍历代码
几乎所有的集合(如Collection
接口或Map
接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Demo01ForEach { public static void main (String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌" ); list.add("周芷若" ); list.add("赵敏" ); list.add("张强" ); list.add("张三丰" ); for (String name : list) { System.out.println(name); } } }
这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。
循环遍历的弊端
Java 8的Lambda让我们可以更加专注于做什么 (What),而不是怎么做 (How),这点此前已经结合内部类进行了对比说明。现在,我们仔细体会一下上例代码,可以发现:
for循环的语法就是“怎么做 ”
for循环的循环体才是“做什么 ”
为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环 。前者是目的,后者是方式。
试想一下,如果希望对集合中的元素进行筛选过滤:
将集合A根据条件一过滤为子集B ;
然后再根据条件二过滤为子集C 。
那怎么办?在Java 8之前的做法可能为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Demo02NormalFilter { public static void main (String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌" ); list.add("周芷若" ); list.add("赵敏" ); list.add("张强" ); list.add("张三丰" ); List<String> zhangList = new ArrayList<>(); for (String name : list) { if (name.startsWith("张" )) { zhangList.add(name); } } List<String> shortList = new ArrayList<>(); for (String name : zhangList) { if (name.length() == 3 ) { shortList.add(name); } } for (String name : shortList) { System.out.println(name); } } }
这段代码中含有三个循环,每一个作用不同:
首先筛选所有姓张的人;
然后筛选名字有三个字的人;
最后进行对结果进行打印输出。
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。 循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。
那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?
Stream的更优写法
下面来看一下借助Java 8的Stream API,什么才叫优雅:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Demo03StreamFilter { public static void main (String[] args) { List<String> list = new ArrayList<>(); list.add("张无忌" ); list.add("周芷若" ); list.add("赵敏" ); list.add("张强" ); list.add("张三丰" ); list.stream() .filter(s -> s.startsWith("张" )) .filter(s -> s.length() == 3 ) .forEach(System.out::println); } }
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印 。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
3.2 流式思想概述 注意:请暂时忘记对传统IO流的固有印象!
整体来看,流式思想类似于工厂车间的“生产流水线 ”。
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案去执行它。
这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。
这里的filter
、map
、skip
都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count
执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。
备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。
3.3 获取流方式 java.util.stream.Stream<T>
是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)
获取一个流非常简单,有以下几种常用的方式:
所有的Collection
集合都可以通过stream
默认方法获取流;
Stream
接口的静态方法of
可以获取数组对应的流。
方式1 : 根据Collection获取流
首先,java.util.Collection
接口中加入了default方法stream
用来获取流,所以其所有实现类均可获取流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.*;import java.util.stream.Stream;public class Demo04GetStream { public static void main (String[] args) { List<String> list = new ArrayList<>(); Stream<String> stream1 = list.stream(); Set<String> set = new HashSet<>(); Stream<String> stream2 = set.stream(); Vector<String> vector = new Vector<>(); Stream<String> stream3 = vector.stream(); } }
方式2 : 根据Map获取流
java.util.Map
接口不是Collection
的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 import java.util.HashMap;import java.util.Map;import java.util.stream.Stream;public class Demo05GetStream { public static void main (String[] args) { Map<String, String> map = new HashMap<>(); Stream<String> keyStream = map.keySet().stream(); Stream<String> valueStream = map.values().stream(); Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream(); } }
方式3 : 根据数组获取流
如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以Stream
接口中提供了静态方法of
,使用很简单:
1 2 3 4 5 6 7 8 import java.util.stream.Stream;public class Demo06GetStream { public static void main (String[] args) { String[] array = { "张无忌" , "张翠山" , "张三丰" , "张一元" }; Stream<String> stream = Stream.of(array); } }
备注:of
方法的参数其实是一个可变参数,所以支持数组。
3.4 常用方法 流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
终结方法 :返回值类型不再是Stream
接口自身类型的方法,因此不再支持类似StringBuilder
那样的链式调用。本小节中,终结方法包括count
和forEach
方法。
非终结方法 :返回值类型仍然是Stream
接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)
函数拼接与终结方法 在上述介绍的各种方法中,凡是返回值仍然为Stream
接口的为函数拼接方法 ,它们支持链式调用;而返回值不再为Stream
接口的为终结方法 ,不再支持链式调用。如下表所示:
方法名
方法作用
方法种类
是否支持链式调用
count
统计个数
终结
否
forEach
逐一处理
终结
否
filter
过滤
函数拼接
是
limit
取用前几个
函数拼接
是
skip
跳过前几个
函数拼接
是
map
映射
函数拼接
是
concat
组合
函数拼接
是
备注:本小节之外的更多方法,请自行参考API文档。
forEach : 逐一处理 虽然方法名字叫forEach
,但是与for循环中的“for-each”昵称不同,该方法并不保证元素的逐一消费动作在流中是被有序执行的 。
1 void forEach (Consumer<? super T> action) ;
该方法接收一个Consumer
接口函数,会将每一个流元素交给该函数进行处理。例如:
1 2 3 4 5 6 7 8 import java.util.stream.Stream;public class Demo12StreamForEach { public static void main (String[] args) { Stream<String> stream = Stream.of("张无忌" , "张三丰" , "周芷若" ); stream.forEach(s->System.out.println(s)); } }
count:统计个数 正如旧集合Collection
当中的size
方法一样,流提供count
方法来数一数其中的元素个数:
该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。基本使用:
1 2 3 4 5 6 7 public class Demo09StreamCount { public static void main (String[] args) { Stream<String> original = Stream.of("张无忌" , "张三丰" , "周芷若" ); Stream<String> result = original.filter(s -> s.startsWith("张" )); System.out.println(result.count()); } }
filter:过滤 可以通过filter
方法将一个流转换成另一个子集流。方法声明:
1 Stream<T> filter (Predicate<? super T> predicate) ;
该接口接收一个Predicate
函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
基本使用
Stream流中的filter
方法基本使用的代码如:
1 2 3 4 5 6 public class Demo07StreamFilter { public static void main (String[] args) { Stream<String> original = Stream.of("张无忌" , "张三丰" , "周芷若" ); Stream<String> result = original.filter(s -> s.startsWith("张" )); } }
在这里通过Lambda表达式来指定了筛选的条件:必须姓张。
limit:取用前几个 limit
方法可以对流进行截取,只取用前n个。方法签名:
1 Stream<T> limit (long maxSize) ;
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。基本使用:
1 2 3 4 5 6 7 8 9 import java.util.stream.Stream;public class Demo10StreamLimit { public static void main (String[] args) { Stream<String> original = Stream.of("张无忌" , "张三丰" , "周芷若" ); Stream<String> result = original.limit(2 ); System.out.println(result.count()); } }
skip:跳过前几个 如果希望跳过前几个元素,可以使用skip
方法获取一个截取之后的新流:
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:
1 2 3 4 5 6 7 8 9 import java.util.stream.Stream;public class Demo11StreamSkip { public static void main (String[] args) { Stream<String> original = Stream.of("张无忌" , "张三丰" , "周芷若" ); Stream<String> result = original.skip(2 ); System.out.println(result.count()); } }
map:映射 如果需要将流中的元素映射到另一个流中,可以使用map
方法。方法签名:
1 <R> Stream<R> map (Function<? super T, ? extends R> mapper) ;
该接口需要一个Function
函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
基本使用
Stream流中的map
方法基本使用的代码如:
1 2 3 4 5 6 7 8 import java.util.stream.Stream;public class Demo08StreamMap { public static void main (String[] args) { Stream<String> original = Stream.of("10" , "12" , "18" ); Stream<Integer> result = original.map(s->Integer.parseInt(s)); } }
这段代码中,map
方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为Integer
类对象)。
concat:组合 如果有两个流,希望合并成为一个流,那么可以使用Stream
接口的静态方法concat
:
1 static <T> Stream<T> concat (Stream<? extends T> a, Stream<? extends T> b)
备注:这是一个静态方法,与java.lang.String
当中的concat
方法是不同的。
该方法的基本使用代码如:
1 2 3 4 5 6 7 8 9 import java.util.stream.Stream;public class Demo12StreamConcat { public static void main (String[] args) { Stream<String> streamA = Stream.of("张无忌" ); Stream<String> streamB = Stream.of("张翠山" ); Stream<String> result = Stream.concat(streamA, streamB); } }
3.5 Stream综合案例 现在有两个ArrayList
集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次 进行以下若干操作步骤:
第一个队伍只要名字为3个字的成员姓名;
第一个队伍筛选之后只要前3个人;
第二个队伍只要姓张的成员姓名;
第二个队伍筛选之后不要前2个人;
将两个队伍合并为一个队伍;
根据姓名创建Person
对象;
打印整个队伍的Person对象信息。
两个队伍(集合)的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class DemoArrayListNames { public static void main (String[] args) { List<String> one = new ArrayList<>(); one.add("迪丽热巴" ); one.add("宋远桥" ); one.add("苏星河" ); one.add("老子" ); one.add("庄子" ); one.add("孙子" ); one.add("洪七公" ); List<String> two = new ArrayList<>(); two.add("古力娜扎" ); two.add("张无忌" ); two.add("张三丰" ); two.add("赵丽颖" ); two.add("张二狗" ); two.add("张天爱" ); two.add("张三" ); } }
而Person
类的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Person { private String name; public Person () {} public Person (String name) { this .name = name; } @Override public String toString () { return "Person{name='" + name + "'}" ; } public String getName () { return name; } public void setName (String name) { this .name = name; } }
传统方式
使用for循环 , 示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class DemoArrayListNames { public static void main (String[] args) { List<String> one = new ArrayList<>(); List<String> two = new ArrayList<>(); List<String> oneA = new ArrayList<>(); for (String name : one) { if (name.length() == 3 ) { oneA.add(name); } } List<String> oneB = new ArrayList<>(); for (int i = 0 ; i < 3 ; i++) { oneB.add(oneA.get(i)); } List<String> twoA = new ArrayList<>(); for (String name : two) { if (name.startsWith("张" )) { twoA.add(name); } } List<String> twoB = new ArrayList<>(); for (int i = 2 ; i < twoA.size(); i++) { twoB.add(twoA.get(i)); } List<String> totalNames = new ArrayList<>(); totalNames.addAll(oneB); totalNames.addAll(twoB); List<Person> totalPersonList = new ArrayList<>(); for (String name : totalNames) { totalPersonList.add(new Person(name)); } for (Person person : totalPersonList) { System.out.println(person); } } }
运行结果为:
1 2 3 4 5 6 Person{name='宋远桥'} Person{name='苏星河'} Person{name='洪七公'} Person{name='张二狗'} Person{name='张天爱'} Person{name='张三'}
Stream方式
等效的Stream流式处理代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class DemoStreamNames { public static void main (String[] args) { List<String> one = new ArrayList<>(); List<String> two = new ArrayList<>(); Stream<String> streamOne = one.stream().filter(s -> s.length() == 3 ).limit(3 ); Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("张" )).skip(2 ); Stream.concat(streamOne, streamTwo).map(s-> new Person(s)).forEach(s->System.out.println(s)); } }
运行效果完全一样:
1 2 3 4 5 6 Person{name='宋远桥'} Person{name='苏星河'} Person{name='洪七公'} Person{name='张二狗'} Person{name='张天爱'} Person{name='张三'}
3.6 收集Stream结果 对流操作完成之后,如果需要将其结果进行收集,例如获取对应的集合、数组等,如何操作?
收集到集合中 Stream流提供collect
方法,其参数需要一个java.util.stream.Collector<T,A, R>
接口对象来指定收集到哪种集合中。幸运的是,java.util.stream.Collectors
类提供一些方法,可以作为Collector
接口的实例:
public static <T> Collector<T, ?, List<T>> toList()
:转换为List
集合。
public static <T> Collector<T, ?, Set<T>> toSet()
:转换为Set
集合。
下面是这两个方法的基本使用代码:
1 2 3 4 5 6 7 8 9 10 11 12 import java.util.List;import java.util.Set;import java.util.stream.Collectors;import java.util.stream.Stream;public class Demo15StreamCollect { public static void main (String[] args) { Stream<String> stream = Stream.of("10" , "20" , "30" , "40" , "50" ); List<String> list = stream.collect(Collectors.toList()); Set<String> set = stream.collect(Collectors.toSet()); } }
收集到数组中 Stream提供toArray
方法来将结果放到一个数组中,由于泛型擦除的原因,返回值类型是Object[]的:
其使用场景如:
1 2 3 4 5 6 7 8 import java.util.stream.Stream;public class Demo16StreamArray { public static void main (String[] args) { Stream<String> stream = Stream.of("10" , "20" , "30" , "40" , "50" ); Object[] objArray = stream.toArray(); } }
第四章 File类 4.1 概述 java.io.File
类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。
4.2 构造方法
public File(String pathname)
:通过将给定的路径名字符串 转换为抽象路径名来创建新的 File实例。
public File(String parent, String child)
:从父路径名字符串和子路径名字符串 创建新的 File实例。
public File(File parent, String child)
:从父抽象路径名和子路径名字符串 创建新的 File实例。
构造举例,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 String pathname = "D:\\aaa.txt" ; File file1 = new File(pathname); String pathname2 = "D:\\aaa\\bbb.txt" ; File file2 = new File(pathname2); String parent = "d:\\aaa" ; String child = "bbb.txt" ; File file3 = new File(parent, child); File parentDir = new File("d:\\aaa" ); String child = "bbb.txt" ; File file4 = new File(parentDir, child);
小贴士:
一个File对象代表硬盘中实际存在的一个文件或者目录。
无论该路径下是否存在文件或者目录,都不影响File对象的创建。
4.3 常用方法 获取功能的方法
public String getAbsolutePath()
:返回此File的绝对路径名字符串。
public String getPath()
:将此File转换为路径名字符串。
public String getName()
:返回由此File表示的文件或目录的名称。
public long length()
:返回由此File表示的文件的长度。
方法演示,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class FileGet { public static void main (String[] args) { File f = new File("d:/aaa/bbb.java" ); System.out.println("文件绝对路径:" +f.getAbsolutePath()); System.out.println("文件构造路径:" +f.getPath()); System.out.println("文件名称:" +f.getName()); System.out.println("文件长度:" +f.length()+"字节" ); File f2 = new File("d:/aaa" ); System.out.println("目录绝对路径:" +f2.getAbsolutePath()); System.out.println("目录构造路径:" +f2.getPath()); System.out.println("目录名称:" +f2.getName()); System.out.println("目录长度:" +f2.length()); } } 输出结果: 文件绝对路径:d:\aaa\bbb.java 文件构造路径:d:\aaa\bbb.java 文件名称:bbb.java 文件长度:636 字节 目录绝对路径:d:\aaa 目录构造路径:d:\aaa 目录名称:aaa 目录长度:4096
API中说明:length(),表示文件的长度。但是File对象表示目录,则返回值未指定。
绝对路径和相对路径
绝对路径 :从盘符开始的路径,这是一个完整的路径。
相对路径 :相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class FilePath { public static void main (String[] args) { File f = new File("D:\\bbb.java" ); System.out.println(f.getAbsolutePath()); File f2 = new File("bbb.java" ); System.out.println(f2.getAbsolutePath()); } } 输出结果: D:\bbb.java D:\idea_project_test4\bbb.java
判断功能的方法
public boolean exists()
:此File表示的文件或目录是否实际存在。
public boolean isDirectory()
:此File表示的是否为目录。
public boolean isFile()
:此File表示的是否为文件。
方法演示,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class FileIs { public static void main (String[] args) { File f = new File("d:\\aaa\\bbb.java" ); File f2 = new File("d:\\aaa" ); System.out.println("d:\\aaa\\bbb.java 是否存在:" +f.exists()); System.out.println("d:\\aaa 是否存在:" +f2.exists()); System.out.println("d:\\aaa 文件?:" +f2.isFile()); System.out.println("d:\\aaa 目录?:" +f2.isDirectory()); } } 输出结果: d:\aaa\bbb.java 是否存在:true d:\aaa 是否存在:true d:\aaa 文件?:false d:\aaa 目录?:true
创建删除功能的方法
public boolean createNewFile()
:当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。
public boolean delete()
:删除由此File表示的文件或目录。
public boolean mkdir()
:创建由此File表示的目录。
public boolean mkdirs()
:创建由此File表示的目录,包括任何必需但不存在的父目录。
方法演示,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class FileCreateDelete { public static void main (String[] args) throws IOException { File f = new File("aaa.txt" ); System.out.println("是否存在:" +f.exists()); System.out.println("是否创建:" +f.createNewFile()); System.out.println("是否存在:" +f.exists()); File f2= new File("newDir" ); System.out.println("是否存在:" +f2.exists()); System.out.println("是否创建:" +f2.mkdir()); System.out.println("是否存在:" +f2.exists()); File f3= new File("newDira\\newDirb" ); System.out.println(f3.mkdir()); File f4= new File("newDira\\newDirb" ); System.out.println(f4.mkdirs()); System.out.println(f.delete()); System.out.println(f2.delete()); System.out.println(f4.delete()); } }
API中说明:delete方法,如果此File表示目录,则目录必须为空才能删除。
4.4 目录的遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class FileFor { public static void main (String[] args) { File dir = new File("d:\\java_code" ); String[] names = dir.list(); for (String name : names){ System.out.println(name); } File[] files = dir.listFiles(); for (File file : files) { System.out.println(file); } } }
小贴士:
调用listFiles方法的File对象,表示的必须是实际存在的目录,否则返回null,无法进行遍历。
第五章 递归 5.1 概述
1 2 3 public static void a () { a(); }
5.2 递归累和 计算1 ~ n的和 分析 :num的累和 = num + (num-1)的累和,所以可以把累和的操作定义成一个方法,递归调用。
实现代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class DiGuiDemo { public static void main (String[] args) { int num = 5 ; int sum = getSum(num); System.out.println(sum); } public static int getSum (int num) { if (num == 1 ){ return 1 ; } return num + getSum(num-1 ); } }
小贴士:递归一定要有条件限定,保证递归能够停止下来,次数不要太多,否则会发生栈内存溢出。
5.3 递归求阶乘
1 n的阶乘:n! = n * (n-1 ) *...* 3 * 2 * 1
分析 :这与累和类似,只不过换成了乘法运算,学员可以自己练习,需要注意阶乘值符合int类型的范围。
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class DiGuiDemo { public static void main (String[] args) { int n = 3 ; int value = getValue(n); System.out.println("阶乘为:" + value); } public static int getValue (int n) { if (n == 1 ) { return 1 ; } return n * getValue(n - 1 ); } }
5.4 文件搜索 搜索D:\aaa
目录中的.java
文件。
分析 :
目录搜索,无法判断多少级目录,所以使用递归,遍历所有目录。
遍历目录时,获取的子文件,通过文件名称,判断是否符合条件。
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class DiGuiDemo3 { public static void main (String[] args) { File dir = new File("D:\\aaa" ); printDir(dir); } public static void printDir (File dir) { File[] files = dir.listFiles(); for (File file : files) { if (file.isFile()) { if (file.getName().endsWith(".java" )) { System.out.println("文件名:" + file.getAbsolutePath()); } } else { printDir(file); } } } }
第六章 IO概述 6.1 什么是IO 生活中,你肯定经历过这样的场景。当你编辑一个文本文件,忘记了ctrl+s
,可能文件就白白编辑了。当你电脑上插入一个U盘,可以把一个视频,拷贝到你的电脑硬盘里。那么数据都是在哪些设备上的呢?键盘、内存、硬盘、外接设备等等。
我们把这种数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入input
和输出output
,即流向内存是输入流,流出内存的输出流。
Java中I/O操作主要是指使用java.io
包下的内容,进行输入、输出操作。输入 也叫做读取 数据,输出 也叫做作写出 数据。
6.2 IO的分类 根据数据的流向分为:输入流 和输出流 。
输入流 :把数据从其他设备
上读取到内存
中的流。
输出流 :把数据从内存
中写出到其他设备
上的流。
格局数据的类型分为:字节流 和字符流 。
字节流 :以字节为单位,读写数据的流。
字符流 :以字符为单位,读写数据的流。
6.4 顶级父类们
输入流
输出流
字节流
字节输入流InputStream
字节输出流OutputStream
字符流
字符输入流Reader
字符输出流Writer
第七章 字节流 7.1 一切皆为字节 一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
7.2 字节输出流【OutputStream】 java.io.OutputStream
抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
public void close()
:关闭此输出流并释放与此流相关联的任何系统资源。
public void flush()
:刷新此输出流并强制任何缓冲的输出字节被写出。
public void write(byte[] b)
:将 b.length字节从指定的字节数组写入此输出流。
public void write(byte[] b, int off, int len)
:从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。
public abstract void write(int b)
:将指定的字节输出流。
小贴士:
close方法,当完成流的操作时,必须调用此方法,释放系统资源。
7.3 FileOutputStream类 OutputStream
有很多子类,我们从最简单的一个子类开始。
java.io.FileOutputStream
类是文件输出流,用于将数据写出到文件。
构造方法
public FileOutputStream(File file)
:创建文件输出流以写入由指定的 File对象表示的文件。
public FileOutputStream(String name)
: 创建文件输出流以指定的名称写入文件。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。
1 2 3 4 5 6 7 8 9 10 public class FileOutputStreamConstructor throws IOException { public static void main (String[] args) { File file = new File("a.txt" ); FileOutputStream fos = new FileOutputStream(file); FileOutputStream fos = new FileOutputStream("b.txt" ); } }
写出字节数据
写出字节 :write(int b)
方法,每次可以写出一个字节数据,代码使用演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class FOSWrite { public static void main (String[] args) throws IOException { FileOutputStream fos = new FileOutputStream("fos.txt" ); fos.write(97 ); fos.write(98 ); fos.write(99 ); fos.close(); } } 输出结果: abc
小贴士:
虽然参数为int类型四个字节,但是只会保留一个字节的信息写出。
流操作完毕后,必须释放系统资源,调用close方法,千万记得。
写出字节数组 :write(byte[] b)
,每次可以写出数组中的数据,代码使用演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class FOSWrite { public static void main (String[] args) throws IOException { FileOutputStream fos = new FileOutputStream("fos.txt" ); byte [] b = "黑马程序员" .getBytes(); fos.write(b); fos.close(); } } 输出结果: 黑马程序员
写出指定长度字节数组 :write(byte[] b, int off, int len)
,每次写出从off索引开始,len个字节,代码使用演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class FOSWrite { public static void main (String[] args) throws IOException { FileOutputStream fos = new FileOutputStream("fos.txt" ); byte [] b = "abcde" .getBytes(); fos.write(b,2 ,2 ); fos.close(); } } 输出结果: cd
数据追加续写 经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?
public FileOutputStream(File file, boolean append)
: 创建文件输出流以写入由指定的 File对象表示的文件。
public FileOutputStream(String name, boolean append)
: 创建文件输出流以指定的名称写入文件。
这两个构造方法,参数中都需要传入一个boolean类型的值,true
表示追加数据,false
表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了,代码使用演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class FOSWrite { public static void main (String[] args) throws IOException { FileOutputStream fos = new FileOutputStream("fos.txt" ,true ); byte [] b = "abcde" .getBytes(); fos.write(b); fos.close(); } } 文件操作前:cd 文件操作后:cdabcde
写出换行 Windows系统里,换行符号是\r\n
。把
以指定是否追加续写了,代码使用演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class FOSWrite { public static void main (String[] args) throws IOException { FileOutputStream fos = new FileOutputStream("fos.txt" ); byte [] words = {97 ,98 ,99 ,100 ,101 }; for (int i = 0 ; i < words.length; i++) { fos.write(words[i]); fos.write("\r\n" .getBytes()); } fos.close(); } } 输出结果: a b c d e
回车符\r
和换行符\n
:
回车符:回到一行的开头(return)。
换行符:下一行(newline)。
系统中的换行:
Windows系统里,每行结尾是 回车+换行
,即\r\n
;
Unix系统里,每行结尾只有 换行
,即\n
;
Mac系统里,每行结尾是 回车
,即\r
。从 Mac OS X开始与Linux统一。
java.io.InputStream
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。
public abstract int read()
: 从输入流读取数据的下一个字节。
public int read(byte[] b)
: 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。
小贴士:
close方法,当完成流的操作时,必须调用此方法,释放系统资源。
java.io.FileInputStream
类是文件输入流,从文件中读取字节。
构造方法
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。
FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException
1 2 3 4 5 6 7 8 9 10 public class FileInputStreamConstructor throws IOException { public static void main (String[] args) { File file = new File("a.txt" ); FileInputStream fos = new FileInputStream(file); FileInputStream fos = new FileInputStream("b.txt" ); } }
读取字节数据
读取字节 :read
方法,每次可以读取一个字节的数据,提升为int类型,读取到文件末尾,返回-1
,代码使用演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class FISRead { public static void main (String[] args) throws IOException { FileInputStream fis = new FileInputStream("read.txt" ); int read = fis.read(); System.out.println((char ) read); read = fis.read(); System.out.println((char ) read); read = fis.read(); System.out.println((char ) read); read = fis.read(); System.out.println((char ) read); read = fis.read(); System.out.println((char ) read); read = fis.read(); System.out.println( read); fis.close(); } } 输出结果: a b c d e -1
循环改进读取方式,代码使用演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class FISRead { public static void main (String[] args) throws IOException { FileInputStream fis = new FileInputStream("read.txt" ); int b ; while ((b = fis.read())!=-1 ) { System.out.println((char )b); } fis.close(); } } 输出结果: a b c d e
小贴士:
虽然读取了一个字节,但是会自动提升为int类型。
流操作完毕后,必须释放系统资源,调用close方法,千万记得。
使用字节数组读取 :read(byte[] b)
,每次读取b的长度个字节到数组中,返回读取到的有效字节个数,读取到末尾时,返回-1
,代码使用演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class FISRead { public static void main (String[] args) throws IOException { FileInputStream fis = new FileInputStream("read.txt" ); int len ; byte [] b = new byte [2 ]; while (( len= fis.read(b))!=-1 ) { System.out.println(new String(b)); } fis.close(); } } 输出结果: ab cd ed
错误数据d
,是由于最后一次读取时,只读取一个字节e
,数组中,上次读取的数据没有被完全替换,所以要通过len
,获取有效的字节,代码使用演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class FISRead { public static void main (String[] args) throws IOException { FileInputStream fis = new FileInputStream("read.txt" ); int len ; byte [] b = new byte [2 ]; while (( len= fis.read(b))!=-1 ) { System.out.println(new String(b,0 ,len)); } fis.close(); } } 输出结果: ab cd e
小贴士:
使用数组读取,每次读取多个字节,减少了系统间的IO操作次数,从而提高了读写的效率,建议开发中使用。
4.6 字节流练习:图片复制 案例实现 复制图片文件,代码使用演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Copy { public static void main (String[] args) throws IOException { FileInputStream fis = new FileInputStream("D:\\test.jpg" ); FileOutputStream fos = new FileOutputStream("test_copy.jpg" ); byte [] b = new byte [1024 ]; int len; while ((len = fis.read(b))!=-1 ) { fos.write(b, 0 , len); } fos.close(); fis.close(); } }
小贴士:
流的关闭原则:先开后关,后开先关。