JavaSE第6篇IO和集合类

集合类

集合类是Java中非常重要的存在,使用频率极高。集合其实与我们数学中的集合是差不多的概念,集合表示一组对象,每一个对象我们都可以称其为元素。不同的集合有着不同的性质,比如一些集合允许重复的元素,而另一些则不允许,一些集合是有序的,而其他则是无序的。

数组和集合的区别

  • 数组初始化后大小不可变
  • 数组只能按索引顺序存取
  • 数组的大小是固定的,集合的大小是可变的
  • 数组可以存放基本数据类型,但集合只能存放对象

集合的根类
Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口。Java的java.util包主要提供了以下三种类型的集合:

  • List:一种有序列表的集合,例如,按索引排列的Student的List
  • Set:一种保证没有重复元素的集合,例如,所有无重复名称的Student的Set
  • Map:一种通过键值(key-value)查找的映射表集合,例如,根据Student的name查找对应Student的Map

List集合

在List接口中,定义了列表类型需要支持的全部操作,List直接继承自前面介绍的Collection接口,其中很多地方重新定义了一次Collection接口中定义的方法,这样做是为了更加明确方法的具体功能

List的行为和数组几乎完全相同:List内部按照放入元素的先后顺序存放,每个元素都可以通过索引确定自己的位置,List的索引和数组一样,从0开始。

数组和List类似,也是有序结构,如果我们使用数组,在添加和删除元素的时候,会非常不方便。例如,一个ArrayList拥有5个元素,实际数组大小为6(即有一个空位):

List集合特点

  • 是一个有序的集合,插入元素默认是插入到尾部,按顺序从前往后存放,每个元素都有一个自己的下标位置
  • 列表中允许存在重复元素
ArrayList

在ArrayList中,底层就是采用数组实现的,跟数据结构中的顺序表思路差不多,可以参考
顺序表查找操作

集合类在删除元素时,只会调用equals方法进行判断是否为指定元素,而不是进行等号判断,所以说一定要注意,如果两个对象使用equals方法相等,那么集合中就是相同的两个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6); //添加Integer的值10
list.add(new Integer(20)); //添加的是一个对象
list.remove(new Integer(20)); //删除的是另一个对象
list.remove((Integer) 10); //注意,不能直接用10,默认情况下会认为传入的是int类型值,删除的是下标为10的元素,我们这里要删除的是刚刚传入的值为10的Integer对象
System.out.println(list); //可以看到,此时元素成功被移除
}
}

生成只读List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
List<String> list = Arrays.asList("A", "B", "C"); //非常方便
// list.add("c"); //无法添加
System.out.println(list);

//利用静态代码快实现
List<String> listStatic = new ArrayList<String>() {{ //使用匿名内部类(匿名内部类在Java8无法使用钻石运算符,但是之后的版本可以)
add("A");
add("B");
add("C");
}};
//list.add("D"); //无法添加
System.out.println(listStatic);
}
}
LinkedList

LinkedList同样是List的实现类,只不过它是采用的链式实现,也就是我们之前讲解的链表,只不过它是一个双向链表,也就是同时保存两个方向。LinkedList不仅可以当做List来使用,也可以当做双端队列使用

LinkedList和ArrayList区别
ArrayListLinkedList
获取指定元素速度很快需要从头开始查找元素
添加元素到末尾速度很快速度很快
在指定位置添加/删除需要移动元素不需要移动元素
内存占用较大
List遍历

for循环遍历

1
2
3
4
5
6
7
8
9
10
import java.util.List;
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
List<String> list = List.of("apple", "pear", "banana");
for (int i=0; i<list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
}
}

迭代器遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
List<String> list = List.of("apple", "pear", "banana");
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
System.out.println("-------");
//foreach本质也是迭代器
for (String s : list) {
System.out.println(s);
}
}
}

Queue和Deque

队列Queue实现了一个先进先出(FIFO)的数据结构:

  • 通过add()/offer()方法将元素添加到队尾;
  • 通过remove()/poll()从队首获取元素并删除;
  • 通过element()/peek()从队首获取元素但不删除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;

public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
//链式List当队列
Queue<String> queueList = new LinkedList<>(); //当做队列使用,还是很方便的
queueList.offer("AAA");
queueList.offer("BBB");
System.out.println(queueList.poll());
System.out.println(queueList.poll());
System.out.println("-----");

//链式List当做栈来进行使用
Deque<String> dequeList = new LinkedList<>();
dequeList.push("AAA");
dequeList.push("BBB");
System.out.println(dequeList.pop());
System.out.println(dequeList.pop());
}
}

Set

Set用于存储不重复的元素集合,它主要提供以下几个方法:

  • 将元素添加进Set:boolean add(E e)
  • 将元素从Set删除:boolean remove(Object e)
  • 判断是否包含元素:boolean contains(Object e)

Set实际上相当于只存储key、不存储value的Map。我们经常用Set用于去除重复元素。因为放入Set的元素和Map的key类似,都要正确实现equals()和hashCode()方法,否则该元素无法正确地放入Set。最常用的Set实现类是HashSet,实际上,HashSet仅仅是对HashMap的一个简单封装

HashSet

HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.HashSet;
import java.util.Set;

public class Main {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("pear");
set.add("orange");
for (String s : set) {
System.out.println(s);
}
}
}
TreeSet

TreeSet是有序的,因为它实现了SortedSet接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.Set;
import java.util.TreeSet;

public class Main {
public static void main(String[] args) {
Set<String> set = new TreeSet<>();
set.add("apple");
set.add("banana");
set.add("pear");
set.add("orange");
for (String s : set) {
System.out.println(s);
}
}
}

Map

Map集合是为了实现键值映射而存在的。Map<K, V>是一种键-值映射表,当我们调用put(K key, V value)方法时,就把key和value做了映射并放入Map。当我们调用V get(K key)时,就可以通过key获取到对应的value。如果key不存在,则返回null。和List类似,Map也是一个接口,最常用的实现类是HashMap。Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。

HashMap

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。HashMap 是无序的,即不会记录插入的顺序。

HashMap的key与value类型可以相同也可以不同,可以是字符串(String)类型的key和value,也可以是整型(Integer)的key和字符串(String)类型的value。

HashMap之所以能根据key直接拿到value,原因是它内部通过空间换时间的方法,用一个大数组存储所有value,并根据key直接计算出value应该存储在哪个索引

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
import java.util.HashMap;
import java.util.Map;

public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 123);
map.put("pear", 456);
map.put("banana", 789);
map.put("apple", 321); //重复添加只会覆盖
map.putIfAbsent("pear", 654); //Java8提供的方法,保护第一个put的

//循环遍历Map实例的keySet()方法返回的Set集合
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + " = " + value);
}
System.out.println("-------------");
//循环遍历Map对象的entrySet()集合
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}

重新equals和hashCode

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
54
55
56
57
58
59
60
import java.util.HashMap;
import java.util.Objects;

public class Main {
public static void main(String[] args) {
HashMap<Student, String> hashMap = new HashMap<>();
Student a = new Student("张三",23);
Student b = new Student("李四",21);
hashMap.put(a, "hello");
hashMap.put(b, "hello");
String s1 = hashMap.get(a);
String s2 = hashMap.get(b);
System.out.println(s1);
System.out.println(s2);
}
}

class Student {
private String name;
private int score;

public Student() {

}

public Student(String name, int score) {
this.name = name;
this.score = score;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getScore() {
return score;
}

public void setScore(int score) {
this.score = score;
}

//java 7有在Objects里面新增了我们需要重新的这两个方法,所以我们重写equals和hashCode还可以使用java自带的Objects
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return score == student.score && Objects.equals(name, student.name);
}

@Override
public int hashCode() {
return Objects.hash(name, score);
}
}
LinkedHashMap

对HashMap的插入进行了修改,保证读取到的顺序和插入的顺序是相同的。由哈希表保证键的唯一性,由链表保证键的有序(存储和取出的顺序一致)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.LinkedHashMap;
import java.util.Set;

public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
// 创建集合对象
LinkedHashMap<String, String> hm = new LinkedHashMap<String, String>();
// 创建并添加元素
hm.put("2345", "hello");
hm.put("1234", "world");
hm.put("3456", "java");
// 遍历
Set<String> set = hm.keySet();
for (String key : set) {
String value = hm.get(key);
System.out.println(key + "---" + value);
}
}
}
TreeMap

就像它的名字一样,就是一个Tree,它的内部直接维护了一个红黑树(没有使用哈希表)因为它会将我们插入的结点按照规则进行排序,所以说直接采用红黑树会更好,我们在创建时,直接给予一个比较规则即可,跟之前的TreeSet是一样的

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
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

public class Main {
public static void main(String[] args) {
Map<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
public int compare(Student p1, Student p2) {
return p1.score > p2.score ? -1 : 1;
}
});
map.put(new Student("Tom", 77), 1);
map.put(new Student("Bob", 66), 2);
map.put(new Student("Lily", 99), 3);
for (Student key : map.keySet()) {
System.out.println(key);
}
}
}

class Student {
public String name;
public int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
public String toString() {
return String.format("{%s: score=%d}", name, score);
}
}

迭代器

Java Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可用于迭代ArrayList和HashSe 等集合。Iterator是Java 迭代器最简单的实现,ListIterator 是 Collection API 中的接口,它扩展了Iterator 接口。

实现迭代
  • 集合类实现Iterable接口,该接口要求返回一个Iterator对象;
  • 用Iterator对象迭代集合内部数据。
    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
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;

    public class Main {
    public static void main(String[] args) {
    ReverseList<String> rlist = new ReverseList<>();
    rlist.add("Apple");
    rlist.add("Orange");
    rlist.add("Pear");
    for (String s : rlist) {
    System.out.println(s);
    }
    }
    }

    class ReverseList<T> implements Iterable<T> {

    private List<T> list = new ArrayList<>();

    public void add(T t) {
    list.add(t);
    }

    @Override
    public Iterator<T> iterator() {
    return new ReverseIterator(list.size());
    }

    class ReverseIterator implements Iterator<T> {
    int index;

    ReverseIterator(int index) {
    this.index = index;
    }

    @Override
    public boolean hasNext() {
    return index > 0;
    }

    @Override
    public T next() {
    index--;
    return ReverseList.this.list.get(index);
    }
    }
    }

Stream流

Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作的处理,最后由最终操作(terminal operation)得到前面处理的结果。

生成方式

Stream流可以通过集合(最常用),数组,值,文件,函数等方式来进行生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//集合生成
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5, 6);
Stream<Integer> streamList = integerList.stream();

//数组生成
int[] intArr = {1, 2, 3, 4, 5, 6};
IntStream streamArr = Arrays.stream(intArr);

//值生成
Stream<Integer> streamValue = Stream.of(1, 2, 3, 4, 5, 6);

//文件生成
Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset());

//iterator函数
Stream<Integer> streamIterator函数 = Stream.iterate(0, n -> n + 2).limit(5);

//generator函数
Stream<Double> streamGenerator = Stream.generate(Math::random).limit(5);

中间操作

一个流可以后面跟随零个或多个中间操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的,仅仅调用到这类方法,并没有真正开始流的遍历,真正的遍历需等到终端操作时,常见的中间操作有下面即将介绍的 filter、map 等

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
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Main {
public static void main(String[] args) throws IOException {
//集合生成
List<Integer> integerList = Arrays.asList(0, 1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9);

//distinct去重
Stream<Integer> streamDistinct = integerList.stream().distinct();
integerList = streamDistinct.collect(Collectors.toList());
System.out.println(integerList);

//filter筛选
Stream<Integer> streamFilter = integerList.stream().filter(i -> i > 3);
integerList = streamFilter.collect(Collectors.toList());
System.out.println(integerList);

//limit返回指定流个数
Stream<Integer> streamLimit = integerList.stream().limit(3);
integerList = streamLimit.collect(Collectors.toList());
System.out.println(integerList);

//map流映射
List<String> stringList = Arrays.asList("Java 8", "Lambdas", "In", "Action");
List<Integer> collect = stringList.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(collect);

//flatMap流转换
List<String> wordList = Arrays.asList("Java 8", "Lambdas", "In", "Action");
List<String> strList = wordList.stream()
.map(w -> w.split(" "))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
System.out.println(strList);

//allMatch匹配所有元素
if (integerList.stream().allMatch(i -> i > 3)) {
System.out.println("所有元素值都大于3");
} else {
System.out.println("并非所有元素值都大于3");
}
}
}

终端操作

一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次,若想在遍历需要通过源数据在生成流。终端操作的执行,才会真正开始流的遍历。如下面即将介绍的 count、collect 等。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.*;

public class Main {
public static void main(String[] args) throws IOException {
//集合生成
List<Integer> integerList = Arrays.asList(0, 1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9);

//count统计流中元素个数
Long result = integerList.stream().count();
System.out.println(result);
System.out.println("---------------------------------------");

//findFirst查找满足条件的第一个
Optional<Integer> resultFind = integerList.stream().filter(i -> i > 3).findFirst();
System.out.println(resultFind.orElse(-1));
System.out.println("---------------------------------------");

//reduce将流中的元素组合
List<String> stringList = Arrays.asList("Java 8", "Lambdas", "In", "Action");
int sum = integerList.stream()
.reduce(0, Integer::sum);
Optional<Integer> min = stringList.stream()
.map(String::length)
.reduce(Integer::min);

Optional<Integer> max = stringList.stream()
.map(String::length)
.reduce(Integer::max);
System.out.println("max=" + max + ",min=" + min + ",sum=" + sum);
System.out.println("---------------------------------------");

//min:max 获取最小最大值
//写法1
Optional<Integer> minStr = stringList.stream()
.map(String::length)
.min(Integer::compareTo);
Optional<Integer> maxStr = stringList.stream()
.map(String::length)
.max(Integer::compareTo);
//写法2
OptionalInt
minOp = stringList.stream()
.mapToInt(String::length)
.min();
OptionalInt
maxOp = stringList.stream()
.mapToInt(String::length)
.max();
//写法3
Optional<Integer> minRe = stringList.stream()
.map(String::length)
.reduce(Integer::min);
Optional<Integer> maxRe = stringList.stream()
.map(String::length)
.reduce(Integer::max);
System.out.println("minStr=" + minStr + ",maxStr=" + maxStr);
System.out.println("minOp=" + minOp + ",maxOp=" + maxOp);
System.out.println("minRe=" + minRe + ",maxRe=" + maxRe);
System.out.println("---------------------------------------");

//summarizingxxx 同时求总和、平均值、最大值、最小值
IntSummaryStatistics intSummaryStatistics = stringList.stream()
.collect(summarizingInt(String::length));
System.out.println(intSummaryStatistics.getAverage() + "," + intSummaryStatistics.getSum());
System.out.println("---------------------------------------");

//foreach遍历
stringList.stream().forEach(System.out::println);
System.out.println("---------------------------------------");

//collect 返回集合
List<Integer> intList = stringList.stream()
.map(String::length)
.collect(toList());
System.out.println(intList);
Set<Integer> intSet = stringList.stream()
.map(String::length)
.collect(toSet());
System.out.println(intSet);
System.out.println("---------------------------------------");

//joining 拼接流中的元素
String str = stringList.stream()
.map(String::toLowerCase)
.collect(Collectors.joining("-"));
System.out.println(str);
}
}

IO操作

JDK提供了一套用于IO操作的框架,为了方便我们开发者使用,就定义了一个像水流一样,根据流的传输方向和读取单位,分为字节流InputStream和OutputStream以及字符流Reader和Writer的IO框架,当然,这里的Stream并不是前面集合框架认识的Stream,这里的流指的是数据流,通过流,我们就可以一直从流中读取数据,直到读取到尽头,或是不断向其中写入数据,直到我们写入完成,而这类IO就是我们所说的BIO,

字节流一次读取一个字节,也就是一个byte的大小,而字符流顾名思义,就是一次读取一个字符,也就是一个char的大小(在读取纯文本文件的时候更加适合)

File对象

Java的标准库java.io提供了File对象来操作文件和目录

  • 创建File对象本身不涉及IO操作;
  • 可以获取路径/绝对路径/规范路径:getPath()/getAbsolutePath()/getCanonicalPath();
  • 可以获取目录的文件和子目录:list()/listFiles();
  • 可以创建或删除文件和目录。
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 Main {
public static void main(String[] args) throws IOException {
File f = new File("C:\\Windows");
File[] fs1 = f.listFiles(); // 列出所有文件和子目录
printFiles(fs1);
File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
public boolean accept(File dir, String name) {
return name.endsWith(".exe"); // 返回true表示接受该文件
}
});
printFiles(fs2);
}

static void printFiles(File[] files) {
System.out.println("==========");
if (files != null) {
for (File f : files) {
System.out.println(f);
}
}
System.out.println("==========");
}
}

其他方法
Java还提供了Path和Files等方法方便来进行文件和目录的管理

InputStream

InputStream就是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能。要特别注意的一点是,InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。

  • Java标准库的java.io.InputStream定义了所有输入流的超类
  • FileInputStream实现了文件流输入
  • ByteArrayInputStream在内存中模拟一个字节流输入
    FileInputStream
    FileInputStream是InputStream的一个子类。顾名思义,FileInputStream就是从文件流中读取数据。读取或写入IO流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException异常并抛出。因此,所有与IO操作相关的代码都必须正确处理IOException

利用Java 7引入的新的try(resource)的语法,只需要编写try语句,让编译器自动为我们关闭资源。推荐的写法如下:

1
2
3
4
5
6
7
8
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} // 编译器在此自动为我们写入finally并调用close()
}

InputStream提供了两个重载方法来支持读取多个字节
int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数

1
2
3
4
5
6
7
8
9
10
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
// 定义1000个字节大小的缓冲区:
byte[] buffer = new byte[1000];
int n;
while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
System.out.println("read " + n + " bytes.");
}
}
}
ByteArrayInputStream

ByteArrayInputStream可以在内存中模拟一个InputStream。ByteArrayInputStream实际上是把一个byte[]数组在内存中变成一个InputStream

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
}
}
}

从文件中读取所有字节,并转换成char然后拼成一个字符串

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
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Main {
public static void main(String[] args) throws IOException {
//读取文件的内容
String strFile;
try (InputStream input = new FileInputStream("src/Main.java")) {
strFile = readAsString(input);
}
System.out.println(strFile);

System.out.println("-----------------------------------------------");
//传入非文件
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
String str = readAsString(input);
System.out.println(str);
}
}

public static String readAsString(InputStream input) throws IOException {
int n;
StringBuilder sb = new StringBuilder();
while ((n = input.read()) != -1) {
sb.append((char) n);
}
return sb.toString();
}
}

OutputStream

和InputStream类似,OutputStream也是抽象类,它是所有输出流的超类。

  • ByteArrayOutputStream在内存中模拟一个字节流输出
  • 某些情况下需要手动调用OutputStream的flush()方法来强制输出缓冲区
  • 总是使用try(resource)来保证OutputStream正确关闭
FileOutputStream

和InputStream类似,OutputStream也提供了close()方法关闭输出流,以便释放系统资源。要特别注意:OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地。

1
2
3
4
5
public void writeFile() throws IOException {
try (OutputStream output = new FileOutputStream("out/readme.txt")) {
output.write("Hello".getBytes("UTF-8")); // Hello
} // 编译器在此自动为我们写入finally并调用close()
}
ByteArrayOutputStream

ByteArrayOutputStream可以在内存中模拟一个OutputStream。ByteArrayOutputStream实际上是把一个byte[]数组在内存中变成一个OutputStream

1
2
3
4
5
public void writeFile() throws IOException {
try (OutputStream output = new FileOutputStream("out/readme.txt")) {
output.write("Hello".getBytes("UTF-8")); // Hello
} // 编译器在此自动为我们写入finally并调用close()
}

同时操作多个AutoCloseable资源时,在try(resource) { … }语句中可以同时写出多个资源,用;隔开

1
2
3
4
5
// 读取input.txt,写入output.txt:
try (InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt")) {
input.transferTo(output); // transferTo的作用是复制
}

Filter模式

通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合

Java的IO标准库使用Filter模式为InputStream和OutputStream增加功能:

可以把一个InputStream和任意个FilterInputStream组合
可以把一个OutputStream和任意个FilterOutputStream组合
Filter模式可以在运行期动态增加功能(又称Decorator模式)

输入字节进行计数

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
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = "hello, world!".getBytes("UTF-8");
try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
System.out.println("Total read " + input.getBytesRead() + " bytes");
}
}
}

class CountInputStream extends FilterInputStream {
private int count = 0;

CountInputStream(InputStream in) {
super(in);
}

public int getBytesRead() {
return this.count;
}

public int read() throws IOException {
int n = in.read();
if (n != -1) {
this.count ++;
}
return n;
}

public int read(byte[] b, int off, int len) throws IOException {
int n = in.read(b, off, len);
if (n != -1) {
this.count += n;
}
return 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.io.*;

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException{
//序列号Person,需要注意的是,直接打开会乱码
Person person = new Person("张三", 24);
OutputStream out = new FileOutputStream("readme.txt");
try (ObjectOutputStream output = new ObjectOutputStream(out)) {
output.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}

//反序列化Person并输出
FileInputStream in = new FileInputStream("readme.txt");
try ( ObjectInputStream input = new ObjectInputStream(in)){
Person p = (Person) input.readObject();
System.out.println(p);
}
}
}

class Person implements Serializable {
private String name;
private int age;

public Person() {
System.out.println("无参构造.");
}

public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("有参构造.");
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

Reader

Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取。java.io.Reader是所有字符输入流的超类

InputStreamReader
字节流,以byte为单位字符流,以char为单位
读取字节(-1,0~255):int read()读取字符(-1,0~65535):int read()
读到字节数组:int read(byte[] b)读到字符数组:int read(char[] c)
FileReader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.*;
import java.nio.charset.StandardCharsets;

public class Main {
public static void main(String[] args) throws IOException {
//FileReader读取演示
try (Reader reader = new FileReader("src/Main.java", StandardCharsets.UTF_8)) {
int content;
while ((content = reader.read()) != -1) {
System.out.print((char) content);
}
}
}
}
BufferedReader
1
2
3
4
5
6
7
8
9
10
11
import java.io.*;
import java.nio.charset.StandardCharsets;

public class Main {
public static void main(String[] args) throws IOException {
//BufferedReader读取演示
try (BufferedReader br = new BufferedReader(new FileReader("src/Main.java", StandardCharsets.UTF_8))) {
br.lines().forEach(System.out::println);
}
}
}

Writer

Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出

OutputStreamWriter
字节流,以byte为单位字符流,以char为单位
写入字节(0~255):void write(int b)写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b)写入字符数组:void write(char[] c)
无对应方法写入String:void write(String s)
FileWriter

FileWriter就是向文件中写入字符流的Writer。它的使用方法和FileReader类似

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.*;
import java.nio.charset.StandardCharsets;

public class Main {
public static void main(String[] args) throws IOException {
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H'); // 写入单个字符
writer.write("Hello".toCharArray()); // 写入char[]
writer.write("Hello"); // 写入String
}
}
}
OutputStreamWriter

除了CharArrayWriter和StringWriter外,普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。因此,OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.*;
import java.nio.charset.StandardCharsets;

public class Main {
public static void main(String[] args) throws IOException {
String text = "sample text";
try (FileOutputStream fos = new FileOutputStream("readme.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
BufferedWriter bf = new BufferedWriter(osw)) {
bf.write(text);
System.out.println("Successfully written data to the file");
}
}
}

打印流

打印流其实我们从一开始就在使用了,比如System.out就是一个PrintStream,PrintStream也继承自FilterOutputStream类因此依然是装饰我们传入的输出流,但是它存在自动刷新机制,例如当向PrintStream流中写入一个字节数组后自动调用flush()方法。PrintStream也永远不会抛出异常,而是使用内部检查机制checkError()方法进行错误检查。最方便的是,它能够格式化任意的类型,将它们以字符串的形式写入到输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.*;
import java.util.Scanner;

public class Main {
public static void main(String[] args) throws IOException {
//PrintStream案例
try(PrintStream stream = new PrintStream(new FileOutputStream("input.txt"))){
stream.println("Hello,Java"); //其实System.out就是一个PrintStream
}catch (IOException e){
e.printStackTrace();
}

//Scanner案例
Scanner scanner = new Scanner(new File("input.txt"));
while (scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
scanner.close();
}
}

JavaSE第6篇IO和集合类
https://www.eldpepar.com/coding/39578/
作者
EldPepar
发布于
2022年10月22日
许可协议