day09【方法引用、Lambda表达式、Stream流】
今日目标
- 线程状态
- 等待与唤醒
- Lambda表达式
- Stream流
教学目标
- 能够说出线程6个状态的名称
- 能够理解等待唤醒案例
- 能够掌握Lambda表达式的标准格式与省略格式
- 能够通过集合、映射或数组方式获取流
- 能够掌握常用的流操作
- 能够将流中的内容收集到集合和数组中
第一章JDK8新特性
JDK新特性:
Lambda 表达式
默认方法【已学习过】
Stream API
方法引用
Base64
1.1 方法引用
5.1.1 方法引用概述
方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。
总结
目标:方法引用的概述。(了解)
方法引用:
方法引用是为了进一步简化Lambda表达式的写法。
方法引用的格式:类型或者对象::引用的方法。
关键语法是:“::”
小结:
方法引用可以进一步简化Lambda表达式的写法。
关键语法是:“::”
out也是个输出流的对象,故可以在out后面加:代表输出对象,然后实际输出对象可以省略
参数和输出的内容式同一个对象,并只有一个就可以简化
lambda表达式:lists.forEach( s -> System.out.println(s));
在lambda表达式下更精简的写法:方法引用 lists.forEach(System.out::println);
5.1.2 方法引用基本使用
方法引用使用一对冒号 :: 。
下面,我们在 Car 类中定义了 4 个方法作为例子来区分 Java 中 4 种不同方法的引用。
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。注意:这个构造器没有参数。
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
总结
4.构造器引用。
格式是:类名::new。
注意点:前后参数一致的情况下,又在创建对象就可以使用构造器引用
s -> new Student(s) => Student::new
小结:
方法引用是可遇不可求,能用则用,不能用就不要用!
前后参数一致
采用匿名内部类对象可以集合转为字符串类型的数组
String[] strs = lists. toArray (new IntFunction<string[]>() {
@override
public string[] apply (int value) {//value是数组的长度
return new String[value] ;//返回字符串类型的数组,返回值作为转换的结果再传给字符串类型的数组strs
}
}) ;
匿名内部类对象(IntFunction)有@FunctionalInterface修饰所以可以用lambda表达式,value值可以用任意字母代替,比如s
String[] strs1 = lists.toArray(s -> new String[s] );
发现前后参数一致的情况下,又在创建对象就可以使用构造器引用,类名是字符串数组,就可以用构造器引用
String[] strs2 = lists.toArray(String[]::new);
String是数据类型,如果要返回整型数据就将String改为
Integer[] strs2 = lists.toArray(Integer[]::new);
第二种方法引用的类型是静态方法引用,语法是Class::static_method。注意:这个方法接受一个Car类型的参数。
cars.forEach( Car::collide );
第三种方法引用的类型是某个类的成员方法的引用,语法是Class::method,注意,这个方法没有定义入参:
cars.forEach( Car::repair );
第四种方法引用的类型是某个实例对象的成员方法的引用,语法是instance::method。注意:这个方法接受一个Car类型的参数:
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
总结
目标:方法引用有四种形式:
1.静态方法的引用。
2.实例方法的引用。
3.特定类型方法的引用。
4.构造器引用。
2.实例方法的引用
格式: 对象::实例方法。
简化步骤:
a.定义一个实例方法,把需要的代码放到实例方法中去。
实例方法引用的注意事项
” 重要:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。“
println是实例方法,故可以进行实例方法的引用
out是输出的对象,实例方法是对象.方法来进行调用,故可以将out当作println的对象,System.out是对象(因为out是静态方法,要用类名System来调用),而println是方法,,且满足前后参数一致,就能用实例方法引用来进行输出
lists.forEach(System.out::println);
特定方法的引用
3.特定类型方法的引用。
特定类型:String ,任何类型。
格式:特定类型::方法。
注意:
如果第一个参数列表中的形参中的第一个参数作为了后面的方法的调用者,
并且其余参数作为后面方法的形参,那么就可以用特定类型方法引用了。
Arrays.sort(数组或集合对象)能够自动将数组和集合对象按照首字母进行排序
调用toString方法能够输出字母,而不是地址
System.out.println (Arrays. tostring(strs)) ;
数组也能定义比较器
String[] strs = new String[]{"James", "AA", "John",
"Patricia","Dlei" , "Robert","Boom", "Cao" ,"black" ,
"Michael", "Linda","cao","after","sBBB"};
// public static <T> void sort(T[] a, Comparator<? super T> c)
// 需求:按照元素的首字符(忽略大小写)升序排序!!!
Arrays.sort(strs, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);// 按照元素的首字符(忽略大小写)比较。
}
}
按照首字母忽略大小写排序的方法
Arrays.sort(strs, (String s1, String s2) -> {
return s1.compareToIgnoreCase(s2);// 按照元素的首字符(忽略大小写)比较。
}
点进Comparator发现有@FunctionalInterface修饰,就可以简化
Arrays.sort(strs, ( s1, s2 ) ->
s1.compareToIgnoreCase(s2)
);
第一个参数必须成为主调,第二个参数必须成为形式参数,主调只能一个,但形参能有多个
特定方法的引用
Arrays.sort(strs, String::compareToIgnoreCase);
5.1.3 基于静态方法引用的代码演示
public static void main(String args[]) {
List names = new ArrayList();
names.add("大明");
names.add("二明");
names.add("小明");
names.forEach(System.out::println);
}
上面的代码,我们将 System.out::println 方法作为静态方法来引用。
测试结果为:
大明
二明
小明
总结
1.静态方法的引用。
引用格式:
类名::静态方法。
简化步骤:
a.定义一个静态方法,把需要简化的代码放到一个静态方法中去。
静态方法引用的注意事项
” 重要:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致。“
小结:
静态方法引用的格式: 类名::静态方法。
重要:被引用的方法的参数列表要和函数式接口中的抽象方法的参数列表一致,才可以引用简化!
可以把比较的代码封装到静态方法里,如果要运用,再调用静态方法
public static int compareByAge(Student o1 , Student o2){
return o1.getAge() - o2.getAge();
}
Collections.sort(lists, ( o1, o2) -> Student.compareByAge(o1 , o2));
果前后参数是一样的,而且方法是静态方法,可以使用静态方法引用:可以把前后参数去掉,然后去掉箭头,加上::
Collections.sort(lists, Student::compareByAge);
如果函数式接口中的抽象方法有返回值,则被引用的方法必须也有相同的返回值(简化前的返回值和引用的静态方法的返回值一定要一样),返回值1==返回值2(了解即可)
简化前:
Collections.sort(lists, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getAge() - o2.getAge();//返回值1
}
});
静态方法:
public static int compareByAge(Student o1 , Student o2){
return o1.getAge() - o2.getAge();//返回值2
}
第二章 Lambda表达式,研究
2.1 函数式编程思想概述
在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
做什么,而不是怎么做
我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run
方法体内的代码传递给Thread
类知晓。
传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。
总结
目标:Lambda表达式的概述。
什么是Lambda表达式?
Lambda表达式是JDK1.8开始之后的新技术,是一种代码的新语法。
是一种特殊写法,
作用:“核心目的是为了简化匿名内部类的代码写法”。
匿名内部类因为是在类的里面,所以不用指明名称,在类的里面就默认为为这个类创建对象
可以用套公式(模板)的方式书写Lambda表达式
2.2 Lambda的优化
当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable
接口来定义任务内容,并使用java.lang.Thread
类来启动该线程。
传统写法,代码如下:
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表达式达到等效:
public class Demo02LambdaRunnable {
public static void main(String[] args) {
new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}
这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!
2.3 Lambda的格式
标准格式:
Lambda省去面向对象的条条框框,格式由3个部分组成:
- 一些参数
- 一个箭头
- 一段代码
Lambda表达式的标准格式为:
(参数类型 参数名称) -> { 代码语句 }
格式说明:
- 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
->
是新引入的语法格式,代表指向动作。- 大括号内的语法与传统方法体要求基本一致。
匿名内部类与lambda对比:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程任务执行!");
}
}).start();
仔细分析该代码中,Runnable
接口只有一个run
方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个方法):
- 无参数:不需要任何条件即可执行该方案。
- 无返回值:该方案不产生任何结果。
- 代码块(方法体):该方案的具体执行步骤。
同样的语义体现在Lambda
语法中,要更加简单:
() -> System.out.println("多线程任务执行!")
- 前面的一对小括号即
run
方法的参数(无),代表不需要任何条件; - 中间的一个箭头代表将前面的参数传递给后面的代码;
- 后面的输出语句即业务逻辑代码。
总结
Lambda表达式的格式:
(匿名内部类被重写方法的形参列表) -> {
被重写方法的方法体代码。
}
-> 就是一个新语法,没有实际含义,但是不能省略!
Lambda表达式的格式:
参数和返回值:
下面举例演示java.util.Comparator<T>
接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1, T o2);
当需要对一个对象数组进行排序时,Arrays.sort
方法需要一个Comparator
接口实例来指定排序的规则。假设有一个Person
类,含有String name
和int age
两个成员变量:
public class Person {
private String name;
private int age;
// 省略构造器、toString方法与Getter Setter
}
传统写法
如果使用传统的代码对Person[]
数组进行排序,写法如下:
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); // 第二个参数为排序规则,即Comparator接口实例
for (Person person : array) {
System.out.println(person);
}
}
}
这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator
接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。
代码分析
下面我们来搞清楚上述代码真正要做什么事情。
- 为了排序,
Arrays.sort
方法需要排序规则,即Comparator
接口的实例,抽象方法compare
是关键; - 为了指定
compare
的方法体,不得不需要Comparator
接口的实现类; - 为了省去定义一个
ComparatorImpl
实现类的麻烦,不得不使用匿名内部类; - 必须覆盖重写抽象
compare
方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错; - 实际上,只有参数和方法体才是关键。
Lambda写法
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);
}
}
}
总结
Comparator也是函数式接口
public interface Comparator<T>{
@Contract(pure=true)int compare(T var1,T var2);
boolean equals(Object var1);
}
简化前
Collections. sort(lists, new Comparator<student>(){
@override
public int compare(student s1, student s2){
return sl. getage()-s2. getAge();
}
});
system. out. print1n(lists);
}
简化后,t1,t2是为了不代码变量名冲突
Collections. sort(lists ,(student t1, Student t2)->{
return tl. getage()-t2. getAge();
});
system. out. print1n(lists);
省略条件:
省略规则
在Lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略;
- 如果小括号内有且仅有一个参,则小括号可以省略;
- 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。
可推导即可省略
Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:
Runnable接口简化:
1. () -> System.out.println("多线程任务执行!")
Comparator接口简化:
2. Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());
总结
如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写,同时要省略分号!
省略前:
new Thread(()->{
system.out.print1n(Thread.currentThread().getName()+":执行~~~");
}).start();
省略后:
System.out.println(Thread.currentThread().getName()+":执行~~~")
).start();
如果Lambda表达式的方法体代码只有一行代码。可以省略大括号不写。
此时,如果这行代码是return语句,必须省略return不写,同时也必须省略";"不写
原版:
Collections. sort(lists, new Comparator<student>(){
@override
public int compare(student s1, student s2){
return sl. getAge()-s2. getAge();
});
省略前:
Collections.sort(lists,(Student t1, Student t2)->{
return tl. getAge()-t2. getAge();
});
省略后:
Collections. sort(lists,(student t1, Student t2)->
t1.getAge() - t2.getAge()
);
参数类型可以省略不写。
省略前:collections.sort(lists,(Student t1,Student t2)->t1.getage()-t2.getAge());
省略后:collections.sort(lists,(t1,t2)->t1.getage()-t2.getAge());
如果只有一个参数,参数类型可以省略,同时()也可以省略。
names.forEach((String s)->{
system.out.print1n(s);
});
names.forEach(s->{
system.out.print1n(s);
});
因为有一行代码。可以省略大括号不写,同时要省略分号!
所以可以省略成这样
names.forEach(s->System.out.println(s));
2.4 Lambda的前提条件
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。 - 使用Lambda必须具有上下文推断。
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为“函数式接口”。
总结
Lambda表达式的使用前提:
(1)Lambda表达式并不能简化所有匿名内部类的写法。
(2)Lambda表达式只能简化接口中只有一个抽象方法的匿名内部类形式。
Lambda表达式只能简化函数式接口的匿名内部类写法:
a.首先必须是接口。
b.接口中只能有一个抽象方法。
小结:
Lambda表达式只能简化接口中只有一个抽象方法的匿名内部类写法。
接口中只有一个抽象方法的接口称为函数式接口。
Lambda只能简化函数式接口的匿名内部类写法。
总结
runnable是函数式接口,看源码可得,只有一个run的方法
@FunctionalInterface
public interface Runnable{
void run();
}
forEach使用Consumer来进行遍历,Consumer是函数式接口
names. forEach(new Consumer<string>(){//用匿名内部类的方式进行创建
@Override
public void accept(String s){
}
names. forEach((String s)->{
system. out. print1n(s);
});
最好是写完完整的代码再通过模板来进行简化
第三章 Stream
在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。
3.1 引言
传统集合的多步遍历代码
几乎所有的集合(如Collection
接口或Map
接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:
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之前的做法可能为:
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,什么才叫优雅:
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、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
总结
目标: 演示Stream流的强大。
什么是Stream流?
在Java 8中,得益于Lambda所带来的函数式编程,
引入了一个全新的Stream流概念 ,用于解决已有集合/数组类库有的弊端。
Stream流能解决什么问题?
可以解决已有集合类库或者数组API的弊端。
Stream认为集合和数组操作的API很不好用,所以采用了Stream流简化集合和数组的操作!!
小结:
Stream流是用来简化集合类库或者数组API的弊端。
Stream流其实就一根传送带,元素在上面可以被Stream流操作。
通过Stream进行过滤,挑选出开头姓张的和字符串的长度为3,s代表集合中所有的元素,变量名可以随意
list.stream().filter(s -> s.startsWith("张")).filter( s -> s.length()== 3 )
. forEach( S -> System. out. println(s)) ;
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
用来获取流,所以其所有实现类均可获取流。
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等情况:
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
,使用很简单:
import java.util.stream.Stream;
public class Demo06GetStream {
public static void main(String[] args) {
String[] array = { "张无忌", "张翠山", "张三丰", "张一元" };
Stream<String> stream = Stream.of(array);
}
}
备注:
of
方法的参数其实是一个可变参数,所以支持数组。
总结
通过entryset方法可以获得键值对整体,变成set集合
目标:Stream流的获取
Stream流式思想的核心:
是先得到集合或者数组的Stream流(就是一根传送带)
然后就用这个Stream流操作集合或者数组的元素。
然后用Stream流简化替代集合操作的API.
集合获取流的API:
(1) default Stream<E> stream();
小结:
集合获取Stream流用: stream();
数组:Arrays.stream(数组) / Stream.of(数组);
代码演示
public static void main(String[] args) {
/** --------------------Collection集合获取流------------------------------- */
// Collection集合如何获取Stream流。
Collection<String> c = new ArrayList<>();
Stream<String> ss = c.stream();
/** --------------------Map集合获取流------------------------------- */
Map<String, Integer> map = new HashMap<>();
// 先获取键的Stream流。
Stream<String> keyss = map.keySet().stream();
// 在获取值的Stream流
Stream<Integer> valuess = map.values().stream();
// 获取键值对的Stream流(key=value: Map.Entry<String,Integer>)
Stream<Map.Entry<String,Integer>> keyAndValues = map.entrySet().stream();
/** ---------------------数组获取流------------------------------ */
// 数组也有Stream流。
String[] arrs = new String[]{"Java", "JavaEE" ,"Spring Boot"};
Stream<String> arrsSS1 = Arrays.stream(arrs);
Stream<String> arrsSS2 = Stream.of(arrs);
}
3.4 常用方法
流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
- 终结方法:返回值类型不再是
Stream
接口自身类型的方法,因此不再支持类似StringBuilder
那样的链式调用。本小节中,终结方法包括count
和forEach
方法。 - 非终结方法:返回值类型仍然是
Stream
接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)
函数拼接与终结方法
在上述介绍的各种方法中,凡是返回值仍然为Stream
接口的为函数拼接方法,它们支持链式调用;而返回值不再为Stream
接口的为终结方法,不再支持链式调用。如下表所示:
方法名 | 方法作用 | 方法种类 | 是否支持链式调用 |
---|---|---|---|
count | 统计个数 | 终结 | 否 |
forEach | 逐一处理 | 终结 | 否 |
filter | 过滤 | 函数拼接 | 是 |
limit | 取用前几个 | 函数拼接 | 是 |
skip | 跳过前几个 | 函数拼接 | 是 |
map | 映射 | 函数拼接 | 是 |
concat | 组合 | 函数拼接 | 是 |
备注:本小节之外的更多方法,请自行参考API文档。
总结
目标:终结与非终结方法。
终结方法:一旦Stream调用了终结方法,流的操作就全部终结了,不能继续使用,
只能创建新的Stream操作。
终结方法: foreach , count。
非终结方法:每次调用完成以后返回一个新的流对象,
可以继续使用,支持链式编程!
非终结方法完成之后会返回新的stream流,就能重复使用
终结方法是指终结stream,非终结方法是指stream能继续循环使用
forEach : 逐一处理
虽然方法名字叫forEach
,但是与for循环中的“for-each”昵称不同,该方法并不保证元素的逐一消费动作在流中是被有序执行的。
void forEach(Consumer<? super T> action);
该方法接收一个Consumer
接口函数,会将每一个流元素交给该函数进行处理。例如:
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 count();
该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。基本使用:
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()); // 2
}
}
filter:过滤
可以通过filter
方法将一个流转换成另一个子集流。方法声明:
Stream<T> filter(Predicate<? super T> predicate);
该接口接收一个Predicate
函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
基本使用
Stream流中的filter
方法基本使用的代码如:
public class Demo07StreamFilter {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter(s -> s.startsWith("张"));
}
}
在这里通过Lambda表达式来指定了筛选的条件:必须姓张。
总结
过滤可以用lambda表达式的原因
predicate有@FunctionalInterface注解,所有可以用lambda表达式,而过滤是用predicate的匿名内部类,s就是集合中的每个元素,返回值是布尔类型,来决定是否舍弃
list.stream().filter (new Predicate<string>() {
@Override
public boolean test (String s) {
return false;
}
});
若是过滤掉数组的长度为3则为
return s.length() == 3;
limit:取用前几个
limit
方法可以对流进行截取,只取用前n个。方法签名:
Stream<T> limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。基本使用:
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()); // 2
}
}
skip:跳过前几个
如果希望跳过前几个元素,可以使用skip
方法获取一个截取之后的新流:
Stream<T> skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:
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()); // 1
}
}
map:映射
如果需要将流中的元素映射到另一个流中,可以使用map
方法。方法签名:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
该接口需要一个Function
函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
基本使用
Stream流中的map
方法基本使用的代码如:
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
类对象)。
总结
把名称都加上“黑马的:+xxx”,先让s(是一个变量,变量名随意)等于每个名称,再添加前缀:“黑马的”
list.stream().map(a -> "黑马的:"+a).forEach(System.out::println);
需求:把名称都加工厂学生对象放上去,name是变量名,可随意,用new对象来装要承载的数据,可进行方法引用
list.stream().map(name -> new Student(name)).forEach(System.out::println);
list.stream().map(Student::new).forEach(System.out::println);
concat:组合
如果有两个流,希望合并成为一个流,那么可以使用Stream
接口的静态方法concat
:
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
备注:这是一个静态方法,与
java.lang.String
当中的concat
方法是不同的。
该方法的基本使用代码如:
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);
}
}
总结
合并是静态方法,要用类名调用
Stream.concat(s1,s2)
如果两个流的数据类型不一样,合并的流可以用object的数据类型
// 数组流
Stream<Integer> s1 = Stream.of(10, 20 ,30 ,40);
// 集合流
Stream<String> s2 = list.stream();
// 合并流
Stream<Object> s3 = Stream.concat(s1,s2);
合并流只能两两合并
Stream<Object> s3 = Stream.concat(s1,s2);
Stream总结
stream支持链式编程,一个.就代表一个执行语句
list.stream().filter( s -> s.length() == 3 ).filter( s -> s.startsWith("张"))
.forEach( System.out::println);
Stream支持泛型来指定数据类型
Stream<String> allStream
3.5 Stream综合案例
现在有两个ArrayList
集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下若干操作步骤:
- 第一个队伍只要名字为3个字的成员姓名;
- 第一个队伍筛选之后只要前3个人;
- 第二个队伍只要姓张的成员姓名;
- 第二个队伍筛选之后不要前2个人;
- 将两个队伍合并为一个队伍;
- 根据姓名创建
Person
对象; - 打印整个队伍的Person对象信息。
两个队伍(集合)的代码如下:
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
类的代码为:
public class Person {
private String name;
public Person() {}
public Person(String name) {//采用有参构造器来载入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循环 , 示例代码:
public class DemoArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一个队伍只要名字为3个字的成员姓名;
List<String> oneA = new ArrayList<>();
for (String name : one) {
if (name.length() == 3) {
oneA.add(name);
}
}
// 第一个队伍筛选之后只要前3个人;
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 : twoA) {
if (name.startsWith("张")) {
twoA.add(name);
}
}
// 第二个队伍筛选之后不要前2个人;
List<String> twoB = new ArrayList<>();
for (int i = 2; i < twoA.size(); i++) {//从2开始遍历,就能去除掉前两个元素
twoB.add(twoA.get(i));
}
// 将两个队伍合并为一个队伍;
List<String> totalNames = new ArrayList<>();
totalNames.addAll(oneB);
totalNames.addAll(twoB);
// 根据姓名创建Person对象;
List<Person> totalPersonList = new ArrayList<>();
for (String name : totalNames) {
totalPersonList.add(new Person(name));
}
// 打印整个队伍的Person对象信息。
for (Person person : totalPersonList) {
System.out.println(person);
}
}
}
运行结果为:
Person{name='宋远桥'}
Person{name='苏星河'}
Person{name='洪七公'}
Person{name='张二狗'}
Person{name='张天爱'}
Person{name='张三'}
Stream方式
等效的Stream流式处理代码为:
public class DemoStreamNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...
List<String> two = new ArrayList<>();
// ...
// 第一个队伍只要名字为3个字的成员姓名;
// 第一个队伍筛选之后只要前3个人;
Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);
// 第二个队伍只要姓张的成员姓名;
// 第二个队伍筛选之后不要前2个人;
Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("张")).skip(2);
// 将两个队伍合并为一个队伍;
// 根据姓名创建Person对象;
// 打印整个队伍的Person对象信息。
Stream.concat(streamOne, streamTwo).map(s-> new Person(s)).forEach(s->System.out.println(s));
}
}
运行效果完全一样:
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
集合。
下面是这两个方法的基本使用代码:
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[]的:
Object[] toArray();
其使用场景如:
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();
}
}
总结
目标:收集Stream流:把Stream流的数据转回成集合。
引入:
Stream的作用是:把集合转换成一根传送带,借用Stream流的强大功能进行的操作。
但是实际开发中数据最终的形式还是应该是集合,最终Stream流操作完毕以后还是要转换成集合。
这就是收集Stream流。
收集Stream流的含义:就是把Stream流的数据转回到集合中去。
Stream流:手段。
集合:才是目的。
小结:
收集Stream流的含义:就是把Stream流的数据转回到集合中去。
熟练函数式编程,如Lambda表达式简化函数式接口的匿名内部类形式,
以及四种方法引用简化Lambda表达式,以及流式编程思想如Stream简化集合或者数组的操作。
Stream转化为数组或集合只能操作一次,否则会报错
Stream只能转为Object类型的数组,如果要转为字符串类型的数组,只能用构造器引用申明(指明类型),原理是用匿名内部类来返回数组类型
第四章 File类
4.1 概述
java.io.File
类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。
总结
目标:File类的概述和API
File类:代表操作系统的文件对象。
File类:是用来操作操作系统的文件对象的,删除文件,获取文件信息,创建文件(文件夹)...
广义来说操作系统认为文件包含(文件和文件夹)
File类的创建文件对象的API:
包:java.io.File
(1)构造器:
-- public File(String pathname):根据路径获取文件对象
-- public File(String parent , String child):根据父路径和文件名称获取文件对象!
-- public File(File parent , String child)
File类创建文件对象的格式:
a.File f = new File("绝对路径/相对路径");
绝对路径:从磁盘的的盘符一路走到目的位置的路径。
-- 绝对路径依赖具体的环境,一旦脱离环境,代码可能出错!!
-- 一般是定位某个操作系统中的某个文件对象。
相对路径:不带盘符的。(重点)
-- 默认是直接相对到工程目录下寻找文件的。
-- 相对路径只能用于寻找工程下的文件。
-- 能用相对路径就应该尽量使用,可以跨平台!
b.File f = new File("文件对象/文件夹对象");
广义来说:文件是包含文件和文件夹的。
小结:
创建文件对象可以用绝对路径也可以用相对路径。
相对路径只能用于寻找工程下的文件。
文件对象可以表示文件也可以表示文件夹!
文件路径的分隔符如果用反斜杠就需要转义,多加一个\
1.创建文件对象:使用绝对路径
// 文件路径分隔符:
-- b.使用反斜杠: \\
File f1 = new File("D:\\itcast\\图片资源\\beautiful.jpg");
File.separator是文件路径分割符的api,能够自动识别当前系统的文件路径的分隔符
File f1 = new File("D:"+File.separator+"itcast"+File.separator+"图片资源"+File.separator+"beautiful.jpg");
盘符: 类似c: d: e: 之类的
工程文件上寻找文件:可以将文件保存再工程,这样的话,就可以实现文件随着工程的移动而移动
在idea可以新建文件,后缀就代表文件的类型
如果是以文本的形式存储,英文和数字都只占一个字节
不带盘符就默认文件是相对路径,就会从工程里面寻找,要指明文件夹名称直至到文件名称
File f2 = new File("Day09Demo/src/dlei01.txt");
如果文件大小为0,极有可能相对路径错了
如果用length来取文件夹的大小,取得是文件夹在系统内部存储得大小,可以判断文件夹路径是否存在,用文件夹对象.exists来进行判断
File f3 = new File("D:\\itcast\\图片资源");
System.out.println(f3.exists());// 判断路径是否存在!!
4.2 构造方法
-
public File(String pathname)
:通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。 -
public File(String parent, String child)
:从父路径名字符串和子路径名字符串创建新的 File实例。 -
public File(File parent, String child)
:从父抽象路径名和子路径名字符串创建新的 File实例。 -
构造举例,代码如下:
// 文件路径名
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对象和子路径字符串
File parentDir = new File("d:\\aaa");
String child = "bbb.txt";
File file4 = new File(parentDir, child);
小贴士:
- 一个File对象代表硬盘中实际存在的一个文件或者目录。
- 无论该路径下是否存在文件或者目录,都不影响File对象的创建。
总结
File parent为文件夹,String child为文件
public File(File parent , String child)
4.3 常用方法
获取功能的方法
-
public String getAbsolutePath()
:返回此File的绝对路径名字符串。 -
public String getPath()
:将此File转换为路径名字符串。 -
public String getName()
:返回由此File表示的文件或目录的名称。 -
public long length()
:返回由此File表示的文件的长度。方法演示,代码如下:
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对象表示目录,则返回值未指定。
总结
代码
public static void main(String[] args) {
// 1.绝对路径创建一个文件对象
File f1 = new File("D:/itcast/图片资源/meinv.jpg");
// a.获取它的绝对路径。
System.out.println(f1.getAbsolutePath());
// b.获取文件定义的时候使用的路径。
System.out.println(f1.getPath());
// c.获取文件的名称:带后缀。
System.out.println(f1.getName());
// d.获取文件的大小:字节个数。
System.out.println(f1.length());
System.out.println("------------------------");
// 2.相对路径
File f2 = new File("Day09Demo/src/dlei01.txt");
// a.获取它的绝对路径。
System.out.println(f2.getAbsolutePath());
// b.获取文件定义的时候使用的路径。
System.out.println(f2.getPath());
// c.获取文件的名称:带后缀。
System.out.println(f2.getName());
// d.获取文件的大小:字节个数。
System.out.println(f2.length());
}
绝对路径和相对路径
- 绝对路径:从盘符开始的路径,这是一个完整的路径。
- 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。
public class FilePath {
public static void main(String[] args) {
// D盘下的bbb.java文件
File f = new File("D:\\bbb.java");
System.out.println(f.getAbsolutePath());
// 项目下的bbb.java文件
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表示的是否为文件。
方法演示,代码如下:
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表示的目录,包括任何必需但不存在的父目录。
方法演示,代码如下:
public class FileCreateDelete {
public static void main(String[] args) throws IOException {
// 文件的创建
File f = new File("aaa.txt");
System.out.println("是否存在:"+f.exists()); // false
System.out.println("是否创建:"+f.createNewFile()); // true
System.out.println("是否存在:"+f.exists()); // true
// 目录的创建
File f2= new File("newDir");
System.out.println("是否存在:"+f2.exists());// false
System.out.println("是否创建:"+f2.mkdir()); // true
System.out.println("是否存在:"+f2.exists());// true
// 创建多级目录
File f3= new File("newDira\\newDirb");
System.out.println(f3.mkdir());// false
File f4= new File("newDira\\newDirb");
System.out.println(f4.mkdirs());// true
// 文件的删除
System.out.println(f.delete());// true
// 目录的删除
System.out.println(f2.delete());// true
System.out.println(f4.delete());// false
}
}
API中说明:delete方法,如果此File表示目录,则目录必须为空才能删除。
总结
目标:File类的创建和删除的方法
- public boolean createNewFile() :当且仅当具有该名称的文件尚不存在时,
创建一个新的空文件。 (几乎不用的,因为以后文件都是自动创建的!)
- public boolean delete() :删除由此File表示的文件或目录。 (只能删除空目录)
- public boolean mkdir() :创建由此File表示的目录。(只能创建一级目录)
- public boolean mkdirs() :可以创建多级目录(建议使用的)
删除是粉碎性删除,不进回收站
返回true或者是false表示操作的成功或者是失败
4.4 目录的遍历
-
public String[] list()
:返回一个String数组,表示该File目录中的所有子文件或目录。 -
public File[] listFiles()
:返回一个File数组,表示该File目录中的所有的子文件或目录。
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,无法进行遍历。
总结
目标:File针对目录的遍历
- public String[] list():
获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。
- public File[] listFiles()(常用):
获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回(重点)
// ---------拓展------------
File f1 = new File("D:\\itcast\\图片资源\\beautiful.jpg");
long time = f1.lastModified(); // 最后修改时间!
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(time));
第五章 递归
5.1 概述
- 递归:指在当前方法内调用自己的这种现象。
public static void a(){
a();
}
总结
就是方法的回调
目标:递归的形式
递归:方法在方法中又调用了自己。
递归:
直接递归:自己的方法调用自己。
间接递归:自己的方法调用别的方法,别的方法又调用自己。
小结:
递归是自己调用自己。
递归如果控制的不恰当,会形成递归的死循环,从而导致栈内存溢出错误!!
递归应该防止进入递归的死循环!
目标:递归的形式
递归:方法在方法中又调用了自己。
递归:
直接递归:自己的方法调用自己。
间接递归:自己的方法调用别的方法,别的方法又调用自己。
小结:
递归是自己调用自己。
递归如果控制的不恰当,会形成递归的死循环,从而导致栈内存溢出错误!!
递归应该防止进入递归的死循环!
5.2 递归累和
计算1 ~ n的和
分析:num的累和 = num + (num-1)的累和,所以可以把累和的操作定义成一个方法,递归调用。
实现代码:
public class DiGuiDemo {
public static void main(String[] args) {
//计算1~num的和,使用递归完成
int num = 5;
// 调用求和的方法
int sum = getSum(num);
// 输出结果
System.out.println(sum);
}
/*
通过递归算法实现.
参数列表:int
返回值类型: int
*/
public static int getSum(int num) {
/*
num为1时,方法返回1,
相当于是方法的出口,num总有是1的情况
*/
if(num == 1){
return 1;
}
/*
num不为1时,方法返回 num +(num-1)的累和
递归调用getSum方法
*/
return num + getSum(num-1);
}
}
总结
目标:递归实现1-n的和。
f(n) = 1 + 2 + 3 + 4 + 5 + 6 + ...n-1 + n ;
f(n) = f(n-1) + n
流程:
f(5) = return f(4) + 5 = 1 + 2 + 3 + 4 + 5
f(4) = return f(3) + 4 = 1 + 2 + 3 + 4
f(3) = return f(2) + 3 = 1 + 2 + 3
f(2) = return f(1) + 2 = 1 + 2
f(1) = return 1
递归的核心三要素:
(1)递归的终点接: f(1) = 1
(2)递归的公式: f(n) = f(n-1) + n
(3)递归的方向必须走向终结点:
*/
前面的数的累加总和再加此刻n的数,然后后面的值可以直接用前面的累加总和,以此递推
f(n) = f(n-1) + n
递归可以用来做符合某种规律的恒等式
return是方法的返回值,能被其他方法直接调用
代码执行图解
小贴士:递归一定要有条件限定,保证递归能够停止下来,次数不要太多,否则会发生栈内存溢出。
公式转换
总结
拓展:递归的核心思想-公式转换
已知: f(x) = f(x + 1) + 2
f(1) = 1
求: f(10) = ?
公式转换:x=n
f(n-1) = f(n-1+1)+2
f(n-1) = f(n)+2
f(n) = f(n-1)- 2 ; //调换位置
递归算法的三要素:
(1)递归的公式: f(n) = f(n-1)- 2 ;
(2)递归的终结点: f(1) = 1
(3)递归的方向:必须走向终结点。
小结:
必须满足三要素,否则递归会出现死亡!
可以通过公式转换来产生公式递推,从而寻找终结点
递归的经典案例
目标:递归的经典案例。
猴子吃桃:
猴子第一天摘了若干个桃子,当即吃了一半,觉得好不过瘾,然后又多吃了一个。
第二天又吃了前一天剩下的一半,觉得好不过瘾,然后又多吃了一个。
以后每天都是如此
等到第十天再吃的时候发现只有1个桃子,请问猴子第一天总共摘了多少个桃子。
公式:
f(x+1) = f(x) - f(x) / 2 - 1
2f(x+1) = 2f(x) - f(x) - 2
2f(x+1) = f(x) - 2
f(x) = 2f(x+1)+2
递归的三要素:
(1)公式:f(x) = 2f(x+1)+2
(2)终结点:f(10) = 1
(3)递归的方向:走向了终结点
代码
public static void main(String[] args) {
System.out.println(f(1));
}
public static int f(int x){
if( x == 10){
return 1 ;
}else{
return 2*f(x+1)+2;
}
}
5.3 递归求阶乘
- 阶乘:所有小于及等于该数的正整数的积。
n的阶乘:n! = n * (n-1) *...* 3 * 2 * 1
分析:这与累和类似,只不过换成了乘法运算,学员可以自己练习,需要注意阶乘值符合int类型的范围。
推理得出:n! = n * (n-1)!
代码实现:
public class DiGuiDemo {
//计算n的阶乘,使用递归完成
public static void main(String[] args) {
int n = 3;
// 调用求阶乘的方法
int value = getValue(n);
// 输出结果
System.out.println("阶乘为:"+ value);
}
/*
通过递归算法实现.
参数列表:int
返回值类型: int
*/
public static int getValue(int n) {
// 1的阶乘为1
if (n == 1) {
return 1;
}
/*
n不为1时,方法返回 n! = n*(n-1)!
递归调用getValue方法
*/
return n * getValue(n - 1);
}
}
总结
n的阶乘与1-n的和的原理类似
5.4 文件搜索
搜索D:\aaa
目录中的.java
文件。
分析:
- 目录搜索,无法判断多少级目录,所以使用递归,遍历所有目录。
- 遍历目录时,获取的子文件,通过文件名称,判断是否符合条件。
代码实现:
public class DiGuiDemo3 {
public static void main(String[] args) {
// 创建File对象,指明查找的位置
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);
}
}
}
}
总结
目标:递归实现文件搜索(非规律递归)
需求:希望去D:/soft目录寻找出eclipse.exe文件。
分析:
(1)定义一个方法用于做搜索。
(2)进入方法中进行业务搜索分析。
小结:
非规律化递归应该按照业务流程开发!
代码
public static void main(String[] args) {
// 搜索调用方法
searchFiles(new File("D:/gongju") , "天若OCR文字识别.exe");
}
/**
* 去某个目录下搜索某个文件
* @param dir 搜索文件的目录。
* @param fileName 搜索文件的名称。
*/
public static void searchFiles(File dir , String fileName){
// 1.判断是否存在该路径,是否是文件夹
if(dir.exists() && dir.isDirectory()){
// 2.提取当前目录下的全部一级文件对象
File[] files = dir.listFiles(); // null/[]
// 3.判断是否存在一级文件对象(判断是否不为空目录)
if(files!=null && files.length > 0){
// 4.判断一级文件对象
for (File f : files) {
// 5.判断file是文件还是文件夹
if(f.isFile()){
// 6.判断该文件是否为我要找的文件对象
if(f.getName().contains(fileName)){
System.out.println(f.getAbsolutePath());
try {
// 启动它(拓展)
Runtime r = Runtime.getRuntime();
r.exec(f.getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
}
}
}else{
// 7.该文件是文件夹,文件夹要递归进入继续寻找
searchFiles(f ,fileName);
}
}
}
}
}
如果无权访问文件夹,可能返回null
if(files!=null && files.length > 0){
文件名是否包含要寻找的文件名,是模糊查找
if(f.getName().contains(fileName)){
获取虚拟机,根据虚拟机来执行软件
Runtime r = Runtime.getRuntime();
r.exec(f.getAbsolutePath());
第六章 IO概述
字符集
总结
目标:字符集/编码集
字符集:各个国家为自己国家的字符取的一套编号规则。
计算机的底层是不能直接存储字符的。
计算机的底层只能存储二进制。010101
二进制就是可以转成10进制的。10进制就是整数编号。
101 = 1*2^0 + 0*2^1 + 1*2^2 = 5
结论:计算机的底层可以存储编号。
1B = 8b 计算机中的最小单位是字节B.
美国人:
8个开关一组就可以编码字符。 1个字节。
2^8 = 256
一个字节存储一个字符完全够用了。
a 97
b 98
A 65
B 66
0 48
1 49
这套编码是ASCII编码。
英文和数字在底层存储的时候都是采用1个字节存储的。
中国人:
中国人的字符很多:9万左右字符。
中国人一般采用2个字节编码一个中文字符。
这套编码叫:GBK编码。
它也必须兼容ASCII编码表。
....
美国人:
我来收集全球所有的字符,统一编号。这套编码叫 Unicode编码(万国码)
UTF-8就是变种形式。
UTF-8一个中文一般占3个字节。
它也必须兼容ASCII编码表。
小结:
英文和数字在任何编码集中都是一样的,都占1个字节。
GBK编码中,1个中文字符一般占2个字节。
UTF-8编码中,1个中文字符一般占3个字节。
技术人员都应该使用UTF-8编码!
编码前与编码后的编码集必须一致才不会乱码!!
GBK 我爱你[oo oo oo] GBK 不会乱码!
GBK 我爱你[oo oo oo] UTF-8 会乱码!
英文和数字在任何编码集中可以通用,不会乱码!!
乱码原因一者是字节数不同
6.1 什么是IO
生活中,你肯定经历过这样的场景。当你编辑一个文本文件,忘记了ctrl+s
,可能文件就白白编辑了。当你电脑上插入一个U盘,可以把一个视频,拷贝到你的电脑硬盘里。那么数据都是在哪些设备上的呢?键盘、内存、硬盘、外接设备等等。
我们把这种数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入input
和输出output
,即流向内存是输入流,流出内存的输出流。
Java中I/O操作主要是指使用java.io
包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。
总结
输出和输入的对象都是软件中的数据(内存),输出是指将软件中的数据输出到文件中,输入是是将文件中的数据输入到软件中
6.2 IO的分类
根据数据的流向分为:输入流和输出流。
- 输入流 :把数据从
其他设备
上读取到内存
中的流。 - 输出流 :把数据从
内存
中写出到其他设备
上的流。
格局数据的类型分为:字节流和字符流。
-
字节流 :以字节为单位,读写数据的流。
-
字符流 :以字符为单位,读写数据的流。
-
总结
-
目标:IO流读写数据。 IO输入输出流:输入/输出流。 Input:输入。 Output:输出。 引入: File类只能操作文件对象本身,不能读写文件对象的内容。 读写数据内容,应该使用IO流。 IO流是一个水流模型:IO理解成水管,把数据理解成水流。 IO流的分类: 按照流的方向分为:输入流,输出流。 (1)输出流:以内存为基准,把内存中的数据写出到磁盘文件或者网络介质中去的流称为输出流。 输出流的作用:写数据到文件,或者写数据发送给别人。 (2)输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据读入到内存中去的流称为输入流。 输入流的作用:读取数据到内存。 按照流的内容分为: 字节流,字符流。 (1)字节流:流中的数据的最小单位是一个一个的字节,这个流就是字节流。 (2)字符流:流中的数据的最小单位是一个一个的字符,这个流就是字符流。(针对于文本内容) 所以流大体分为四大类: 字节输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字节的形式读入到内存中去的流称为字节输入流。 字节输出流:以内存为基准,把内存中的数据以一个一个的字节写出到磁盘文件或者网络介质中去的流称为字节输出流。 字符输入流:以内存为基准,把磁盘文件中的数据或者网络中的数据以一个一个的字符的形式读入到内存中去的流称为字符输入流。 字符输出流:以内存为基准,把内存中的数据以一个一个的字符写出到磁盘文件或者网络介质中去的流称为字符输出流。 小结: IO流是读写传输数据的,IO流有很多种,每种流有自己的功能特点。
输入和输出的判断依据流向的最终目的,流向内存就是输入,从内存流出就是输出
6.3 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)
: 创建文件输出流以指定的名称写入文件。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。
- 构造举例,代码如下:
public class FileOutputStreamConstructor throws IOException {
public static void main(String[] args) {
// 使用File对象创建流对象
File file = new File("a.txt");
FileOutputStream fos = new FileOutputStream(file);//创建文件输出流,参数传入文件对象
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("b.txt");
}
}
写出字节数据
- 写出字节:
write(int b)
方法,每次可以写出一个字节数据,代码使用演示:
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 写出数据
fos.write(97); // 写出第1个字节
fos.write(98); // 写出第2个字节
fos.write(99); // 写出第3个字节
// 关闭资源
fos.close();
}
}
输出结果:
abc
小贴士:
- 虽然参数为int类型四个字节,但是只会保留一个字节的信息写出。
- 流操作完毕后,必须释放系统资源,调用close方法,千万记得。
- 写出字节数组:
write(byte[] b)
,每次可以写出数组中的数据,代码使用演示:
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个字节,代码使用演示:
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
// 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
fos.write(b,2,2);
// 关闭资源
fos.close();
}
}
输出结果:
cd
数据追加续写
经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?
public FileOutputStream(File file, boolean append)
: 创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name, boolean append)
: 创建文件输出流以指定的名称写入文件。
这两个构造方法,参数中都需要传入一个boolean类型的值,true
表示追加数据,false
表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了,代码使用演示:
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt",true);
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
// 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
fos.write(b);
// 关闭资源
fos.close();
}
}
文件操作前:cd
文件操作后:cdabcde
写出换行
Windows系统里,换行符号是\r\n
。把
以指定是否追加续写了,代码使用演示:
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统一。
总结
目标:字节输出流的使用。
IO流的体系:
字节流 字符流
字节输入流 字节输出流 字符输入流 字符输出流
InputStream OutputStream Reader Writer (抽象类)
FileInputStream FileOutputStream FileReader FileWriter (实现类)
a.FileOutputStream文件字节输出流
-- 作用:以内存为基准,把内存中的数据,按照字节的形式写出到磁盘文件中去。
简单来说,把内存数据按照字节写出到磁盘文件中去。
-- 构造器:
public FileOutputStream(File file):创建一个字节输出流管道通向目标文件对象。
public FileOutputStream(String file):创建一个字节输出流管道通向目标文件路径。
public FileOutputStream(File file , boolean append):创建一个追加数据的字节输出流管道通向目标文件对象。
public FileOutputStream(String file , boolean append):创建一个追加数据的字节输出流管道通向目标文件路径。
-- 方法:
public void write(int a):写一个字节出去 。
public void write(byte[] buffer):写一个字节数组出去。
public void write(byte[] buffer , int pos , int len):写一个字节数组的一部分出去。
参数一,字节数组;参数二:起始字节索引位置,参数三:写多少个字节数出去。
小结:
字节输出流只能写字节出去。
字节输出流默认是覆盖数据管道。
换行用: os.write("\r\n".getBytes());
关闭和刷新:刷新流可以继续使用,关闭包含刷新数据但是流就不能使用了!
字节输出流可以写字节数据到文件中去。
写一个字节,写一个字节数组,写一个字节数组的一部分出去。
管道用完需要关闭,数据要生效需要刷新,关闭包含刷新,
字节输出流管道默认是覆盖数据管道,启动管道写数据前会清空数据
追加数据,第二次写数据不会覆盖第一次
‘b’:在字节是98,所以能用int类型进行接收
Outputstream OS = new FileOutputStream( "Day09Demo/ src/d1ei04. txt") ;//创建一个字节输出流管道与目标文件路径接通
os.write('b');//字节输出流写出数据b,只会输出一个字节
os.flush(); //立即刷新数据到文件中去,刷新后管道可以继续使用。
os.close(); //关闭资源管道,关闭包含了刷新,关闭后管道不能使用了。
. // b.写一个字节数组出去(写一个桶出去)
byte[] bytes = new byte[]{98, 99, 100, 111, 104};
os. write (bytes) ;//将字节通过字节输出流进行输出
可以调用getBytes的方法来获取中文和英文数字的字节,默认以.当前代码编码UTF-8提取字节数组。
byte [] bytes1 = "Java是最优美的语言,我爱Java! " .getBytes();
可以输出字节的长度
System. out. println (bytes1.length) ;
可以指定其他编码格式,获取字节数组
byte [ ] bytes1 = Java是最优美的语言" . getBytes( charsetName: "GBK") ;
0代表起始位置,19代表截取字节的长度
os . write (bytes2, off: 0,len: 19) ;
进行换行操作,要取字节,\n\r兼容性更好,能够跨平台
os. write ("\r\n" .getBytes() ) ;
io流,创建新管道,写入数据之前会清空之前的数据,以便覆盖,默认是数据覆盖管道
创建追加数据管道。第二参数是true即可! 追加数据管道将不会清空数据,会在后面已有的数据进行追加
Outputstream OS = new FileOutputStream( "Day09Demo/ src/d1ei04. txt",true) ;
目标:字节输出流的使用-追加数据管道。
IO流的体系:
字节流 字符流
字节输入流 字节输出流 字符输入流 字符输出流
InputStream OutputStream Reader Writer (抽象类)
FileInputStream FileOutputStream FileReader FileWriter (实现类)
FileOutputStream字节输出流每次启动写数据的时候都会先清空之前的全部数据,从新写入。
小结:
覆盖数据管道: OutputStream os = new FileOutputStream("Day09Demo/out05");
追加数据的管道:OutputStream os = new FileOutputStream("Day09Demo/out05" , true);
参数二代表了此管道是追加数据的管道,不会覆盖之前的数据!
7.4 字节输入流【InputStream】
java.io.InputStream
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。public abstract int read()
: 从输入流读取数据的下一个字节。public int read(byte[] b)
: 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。
小贴士:
close方法,当完成流的操作时,必须调用此方法,释放系统资源。
7.5 FileInputStream类
java.io.FileInputStream
类是文件输入流,从文件中读取字节。
构造方法
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException
- 构造举例,代码如下:
public class FileInputStreamConstructor throws IOException{
public static void main(String[] args) {
// 使用File对象创建流对象
File file = new File("a.txt");
FileInputStream fos = new FileInputStream(file);//创建输入流对象,目的是为了读取文本,传入文本对象作为参数
// 使用文件名称创建流对象
FileInputStream fos = new FileInputStream("b.txt");
}
}
读取字节数据
- 读取字节:
read
方法,每次可以读取一个字节的数据,提升为int类型,读取到文件末尾,返回-1
,代码使用演示:
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);//将字节数据转为字符数据
// 读取到末尾,返回-1
read = fis.read();
System.out.println( read);
// 关闭资源
fis.close();
}
}
输出结果:
a
b
c
d
e
-1
循环改进读取方式,代码使用演示:
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
,代码使用演示:
public class FISRead {
public static void main(String[] args) throws IOException{
// 使用文件名称创建流对象.
FileInputStream fis = new FileInputStream("read.txt"); // 文件中为abcde
// 定义变量,作为有效个数
int len ;
// 定义字节数组,作为装字节数据的容器,每次装2个字节
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
,获取有效的字节,代码使用演示:
public class FISRead {
public static void main(String[] args) throws IOException{
// 使用文件名称创建流对象.
FileInputStream fis = new FileInputStream("read.txt"); // 文件中为abcde
// 定义变量,作为有效个数
int len ;
// 定义字节数组,作为装字节数据的容器
byte[] b = new byte[2];
// 循环读取
while (( len= fis.read(b))!=-1) {
// 每次读取后,把数组的有效字节部分,变成字符串打印
System.out.println(new String(b,0,len));// len 每次读取的有效字节个数
}
// 关闭资源
fis.close();
}
}
输出结果:
ab
cd
e
小贴士:
使用数组读取,每次读取多个字节,减少了系统间的IO操作次数,从而提高了读写的效率,建议开发中使用。
总结
目标:字节输入流的使用。
IO流的体系:
字节流 字符流
字节输入流 字节输出流 字符输入流 字符输出流
InputStream OutputStream Reader Writer (抽象类)
FileInputStream FileOutputStream FileReader FileWriter (子类实现类)
a.FileInputStream文件字节输入流。
-- 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流。
简单来说,就是按照字节读取文件数据到内存。
-- 构造器:
1.public FileInputStream(File path):创建一个字节输入流管道与源文件对象接通。
2.public FileInputStream(String pathName):创建一个字节输入流管道与文件路径对接。
-- 方法:
1.public int read():每次读取一个字节返回!读取完毕会返回-1。
小结:
一个一个字节读取英文和数字没有问题。
但是一旦读取中文输出无法避免乱码,因为会截断中文的字节。
一个一个字节的读取数据,性能也较差,所以禁止使用此方案!
字节编号转字符可以用(char)进行强转
System. out. println( (char)code1) ;
byte的范围小于int
代码
public static void main(String[] args) throws Exception {
// 1.创建文件对象定位dlei01.txt
File file = new File("Day09Demo/src/dlei01.txt");
// 2.创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream(file);
// 3.读取一个字节的编号返回,读取完毕返回-1
// int code1 = is.read(); // 读取一滴水,一个字节
// System.out.println((char)code1);
//
// int code2 = is.read(); // 读取一滴水,一个字节
// System.out.println((char)code2);
//
// int code3 = is.read(); // 读取一滴水,一个字节
// System.out.println((char)code3);
//
// int code4 = is.read(); // 读取一滴水,一个字节 ,读取没有字节返回-1
// System.out.println(code4);
// 4.使用while读取字节数
// 定义一个整数变量存储字节
int ch = 0 ;
while((ch = is.read())!= -1){
System.out.print((char) ch);
}
}
字节输入流读取一个一个字节数组
总结
概念
目标:字节输入流的使用-按照字节数组读取。
IO流的体系:
字节流 字符流
字节输入流 字节输出流 字符输入流 字符输出流
InputStream OutputStream Reader Writer (抽象类)
FileInputStream FileOutputStream FileReader FileWriter (实现类)
a.FileInputStream文件字节输入流。
-- 作用:以内存为基准,把磁盘文件中的数据按照字节的形式读入到内存中的流。
简单来说,就是按照字节读取文件数据到内存。
-- 构造器:
1.public FileInputStream(File path):创建一个字节输入流管道与源文件对象接通。
2.public FileInputStream(String pathName):创建一个字节输入流管道与文件路径对接。
-- 方法:
1.public int read():每次读取一个字节返回!读取完毕会返回-1。
2.public int read(byte[] buffer):从字节输入流中读取字节到字节数组中去,
返回读取的字节数量,没有字节可读返回-1。
小结:
public int read(byte[] buffer):从字节输入流中读取字节到字节数组中去,
返回读取的字节数量,没有字节可读返回-1。
使用字节数组读取内容,效率可以。
但是使用字节数组读取文本内容输出,也无法避免中文读取输出乱码的问题。
代码
public static void main(String[] args) throws Exception {
// 需求:读取文件中的数据输出。
// 1.创建一个文件对象
//File srcFile = new File("Day09Demo/src/dlei02.txt");
// 2.创建一个字节输入流管道与源文件对象接通。
//InputStream is = new FileInputStream(srcFile);
// 3.简化写法:直接创建一个字节输入流管道与源文件路径接通。
InputStream is = new FileInputStream("Day09Demo/src/dlei02.txt");
// // 4.定义一个字节数组读取数据(定义一个桶)
// byte[] buffer = new byte[3];
// // 从is管道中读取字节装入到字节数组中去,返回读取字节的数量。
// int len = is.read(buffer);
// System.out.println("读取了字节数:"+len);
// String rs = new String(buffer);
// System.out.println(rs); // abc
//
// int len1 = is.read(buffer);
// System.out.println("读取了字节数:"+len1);
// String rs1 = new String(buffer);
// System.out.println(rs1); // xyz
//
// int len2 = is.read(buffer);
// System.out.println("读取了字节数:"+len2);
// // 倒出字节数组中的全部字符
// //String rs2 = new String(buffer);
// // 读取了多少就倒出多少!
// String rs2 = new String(buffer, 0 , len2);
// System.out.println(rs2); // iyz
//
// int len3 = is.read(buffer);
// System.out.println("读取了字节数:"+len3); // -1 数据没有了返回-1
// 读法优化,必须使用循环 // abc xyz i
// a.定义一个字节数组代表桶 // ooo ooo o
byte[] buffer = new byte[3];
int len ; // 存储每次读取的字节数。
while((len = is.read(buffer)) != -1){
// 读取了多少就倒出多少!
String rs = new String(buffer , 0 , len);
System.out.print(rs);
}
}
直接在字节输入流的参数中输入文件的路径,而不需要经过文件的创建的原因,内部就创建文件对象
字符串构造器,将字节数组转为字符串
byte[] buffer = new byte[3];//定义一个字节数组读取数据(定义一个桶)
// 从is管道中读取字节装入到字节数组中去,返回读取字节的数量。
int len = is.read(buffer);
System.out.println("读取了字节数:"+len);
String rs = new String(buffer);//字符串构造器,将字节数组转为字符串
如果数组的元素相同对象满了。再接元素,会把原先的元素进行覆盖可覆盖
如果文本的长度小于数组的长度,那么就会改变文本的数量,其他将不会改变
// 倒出字节数组中的全部字符
// //String rs2 = new String(buffer);
offset:起始点,len是每次读取的长度,从管道中读取多少就从桶里装多少
String rs2 = new String(buffer, 0 , len2);
括号内部代码执行有优先级,所以先执行等号右边的式子
while((len = is.read(buffer)) != -1){
解决字节输入流读取中文内容输出乱码的问题
总结
概念
拓展:解决字节输入流读取中文内容输出乱码的问题。
引入:
一个一个字节读取中文输出
一个一个字节数组读取中文输出均无法避免乱码。
如何实现读取可以避免乱码呢?
1.定义一个字节数组与文件的大小刚刚一样大,然后一桶水读取全部字节数据再输出!
小结:
定义一个字节数组与文件的大小刚刚一样大,然后一桶水读取全部字节数据再输出!
可以避免中文读取输出乱码,但是如果读取的文件过大,会出现内存溢出!!
字节流并不适合读取文本文件内容输出,读写文件内容建议使用字符流。
代码
public static void main(String[] args) throws Exception {
// 0.定位文件对象
File f = new File("Day09Demo/src/dlei03.txt");
// 1.定义一个字节输入流通向源文件路径,简化写法!
InputStream is = new FileInputStream(f);
// 2.定义一个字节数组与文件的大小刚刚一样大
// System.out.println("文件大小:"+f.length());
// byte[] buffer = new byte[(int) f.length()];//定义文本大小的字节数的数组(桶)
// int len = is.read(buffer);//将文本的字节数倒入桶(数组),用长度来接收
// System.out.println("读取了:"+len);
// String rs = new String(buffer);将字节转为字符串
// System.out.println(rs);
byte[] buffer = is.readAllBytes();//读取文本大小的字节数,然后装入字节类型的桶(数组)
String rs = new String(buffer);//将字节转为字符串
System.out.println(rs);
}
方法的返回值类型一定要和接收变量的数据类型一致
byte也是整数类型
如果文件不大,可以进行强转,如果文件太大,可能数组的长度也过大,会挤爆内存
byte[] buffer = new byte[(int) f.length()];
这两者的效果是一样的,只不过前者是自己写的代码,后者是sun公司的api
byte[] buffer = new byte[(int) f.length()];
byte[] buffer = is.readAllBytes();
4.6 字节流练习:图片复制
复制原理图解
案例实现
复制图片文件,代码使用演示:
public class Copy {
public static void main(String[] args) throws IOException {
// 1.创建流对象
// 1.1 指定数据源
FileInputStream fis = new FileInputStream("D:\\test.jpg");
// 1.2 指定目的地
FileOutputStream fos = new FileOutputStream("test_copy.jpg");
// 2.读写数据
// 2.1 定义数组
byte[] b = new byte[1024];
// 2.2 定义长度
int len;
// 2.3 循环读取
while ((len = fis.read(b))!=-1) {
// 2.4 写出数据
fos.write(b, 0 , len);
}
// 3.关闭资源
fos.close();
fis.close();
}
}
小贴士:
流的关闭原则:先开后关,后开先关。
总结
目标:字节流做文件复制。
字节流复制的思想:
字节是计算机中一切文件的组成,所以
字节流适合做一切文件的复制。
复制是把源文件的全部字节一字不漏的转移到目标文件,只要文件前后的格式一样,绝对不会有问题。
需求:
原文件:D:\itcast\图片资源\meinv.jpg
目标文件:D:\itcast\meimei.jpg
分析步骤:
(1)创建一个字节输入流管道与源文件接通。
(2)创建一个字节输出流与目标文件接通。
(3)创建一个字节数组作为桶
(4)从字节输入流管道中读取数据,写出到字节输出流管道即可。
(5)关闭资源!
代码
public static void main(String[] args) {
InputStream is = null ;
OutputStream os = null ;
try{
/** (1)创建一个字节输入流管道与源文件接通。 */
is = new FileInputStream("D:\\itcast\\图片资源\\meinv.jpg");
/** (2)创建一个字节输出流与目标文件接通。*/
os = new FileOutputStream("D:\\itcast\\meimei.jpg");
/** (3)创建一个字节数组作为桶*/
byte[] buffer = new byte[1024];
/** (4)从字节输入流管道中读取数据,写出到字节输出流管道即可。*/
int len = 0;
while((len = is.read(buffer)) != -1){
// 读取多少就倒出多少
os.write(buffer, 0 , len);
}
System.out.println("复制完成!");
}catch (Exception e){
e.printStackTrace();
} finally {
/**(5)关闭资源! */
try{
if(os!=null)os.close();
if(is!=null)is.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
复制后的图片名称可以不一样
要考虑最后一桶水,可能装不满,所有要有多少装多少;len是字节数,is是字节输入流,buffer是存储的数组(桶)
int len = 0;
while((len = is.read(buffer)) != -1){
把字节输出流和字节输入流的变量定义在全局里,以便finally进行调用
InputStream is = null ;
OutputStream os = null ;
两个流分开关闭的话,就不会导致一个流出现异常关不了,另一个也关不了
if(os!=null)os.close();
if(is!=null)is.close();
复制前后的格式一定要一样,复制前格式是jpg,复制后格式一样要jpg
JDK.7开始释放资源的新方式
总结
概念
目标: JDK 1.7开始之后释放资源的新方式
try-with- resources:
try(
//这里只能放置资源对象,用完会自动调用close()关闭
){
}catch (Exception e) {
e. printStackTrace() ;
}
什么是资源?
资源类- -定是实现了Closeable接口,实现这个接口的类就是资源
有close()方法,try-with- resources会自动调用它的close ()关闭资源。
小结:
JDK 1.7开始释放资源的新方式: try-with-resources. 如此不需要写finally代码块了!
代码
常量一定要给初始值,只能给一次初始值,资源对象一般用常量来定义
public abstract class Inputstream implements Closeable{}