Java核心技术卷II 笔记-上

文本是对《Java 核心技术卷II》做的笔记,很早之前就写了,虽然本人已经转Go的😂,但是对有需要的同学可能有用。。。

List.of和Arrays.asList区别

  • Arrays.asList 返回可变的list,而List.of返回的是不可变的 list

  • Arrays.asList支持null,而List.of不行

    1
    2
    List<Integer> list = Arrays.asList(1, 2, null); // OK
    List<Integer> list = List.of(1, 2, null); // 异常:NullPointerException
  • 它们的contains方法对null处理不一样

    1
    2
    List<Integer> list = Arrays.asList(1, 2, null); // OK
    List<Integer> list = List.of(1, 2, null); // 异常:NullPointerException
  • Arrays.asList:数组的修改会影响原数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Integer[] array = {1,2,3};
    List<Integer> list = Arrays.asList(array);
    array[1] = 10;
    System.out.println(list); // 输出 [1, 10, 3]

    Integer[] array = {1,2,3};
    List<Integer> list = List.of(array);
    array[1] = 10;
    System.out.println(list); // 输出 [1, 2, 3]

注意 Arrays.asList() 实际上是

1
2
3
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

其中的 new ArrayList<>(a)ArrayList实际上是 Arrays 的 静态内部类,并不是集合 ArrayList!!!。

也就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Arrays {  
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable
{
@SuppressWarnings("serial") // Conditionally serializable
private final E[] a;

// 将一个数组传递到构造函数中,实际上只是简单的保存数组引用
// 因此不支持 添加add操作!!!
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}

@Override
public int size() {
return a.length;
}
...
}
...
}

而标准的集合 ArrayListList 的子类,实现于 Collection,是真正意义上的泛型列表,支持更多的操作。

支持将一个 合集 传递给构造函数,并不支持直接传递一个 数组

1
public ArrayList(Collection<? extends E> c)

流库 Stream

  • 流并不存储其元素
  • 流的操作不会修改其数据源
  • 流的操作是尽可能惰性执行的

Collection 接口的 stream 方法将任何 集合 转换为一个 。或者通过 Stream.of/Arrays.stream数组 转换为

空流:Stream.empty()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.test3;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Test5 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 100; i++) {
list.add(random.nextInt(10000));
}
long count = list.stream().filter(v -> v > 5000).count();
System.out.println(count);
}
}

创建 无限流 方法

  • Stream.generate(Supplier<T>):比如 Stream.generate(Math::random)
  • Stream.iterate(T seed, UnaryOperator<T>) 无限序列:Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE))
1
2
3
Stream<BigInteger> stream = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
stream.limit(5).forEach(v -> System.out.println(v));
// 输出 0-4

filter、map、flatMap

  • filter 过滤。产生一个流,它包含当前流中所有满足断言条件的元素
  • map 变换。会有一个函数应用到每个元素上,并且其结果是包含了应用该函数后所产生的所有结果的流
  • flatMap 。将 mapper 应用于当前流中所有元素所产生的结果连接到一起而获得的
1
2
3
4
5
6
7
8
9
10
String[] arr = {"hello", "world", "linux"};
Stream<String> stream = Stream.of(arr);
stream.forEach(System.out::println);

// 每次创建的stream只能使用一次,因此需要再次创建 Stream,否则抛出异常!
stream = Arrays.stream(arr);
stream.map(String::toUpperCase).forEach(System.out::println);

stream = Arrays.stream(arr);
stream.forEach(System.out::println);

抽取子流和连接流

  • Stream.limit 裁剪无限流的尺寸
  • Stream.skip 它会丢弃前 n 个元素
  • Stream.concat 将两个流连接起来

其他的流转换

  • Stream.distinct 删除重复的元素
  • Stream.sorted 排序流中的元素
  • Stream.peek 它的元素与原来流中的元素相同,但是在 每次获取一个元素时,都会调用一个函数
1
2
3
4
5
6
Stream.of("hello", "world", "hello", "hello", "xx").distinct().forEach(System.out::println);
Stream.of(100, 20, 5, 1000, 4023).sorted(Comparator.reverseOrder()).forEach(System.out::println);
Stream.of("hello", "world").peek(v -> System.out.print("-> ")).forEach(System.out::println);
// 输出
-> hello
-> world

简单约简

约简是一种终结操作,它们会将流约简为可以在程序中使用的 非流值(比如 int, long…)

Optional 类型

Optional<T> 对象是一种 包装器对象,要么包装了类型 T 的对象 , 要么没有包装任何对象 。

对于第一种情况,我们称这种值为存在的 。Optional<T> 类型被当作一种更安全的方式,用来替代类型 T 的引用 ,这种引用要么引用某个对象,要么为 null。类似 C++ std::optional<T>

建议查看 Optional 类代码

1
2
3
4
Optional<Integer> optInt = Optional.of(200);
System.out.println(optInt.get());
optInt = Optional.empty();
System.out.println(optInt.orElseGet(() -> new Random().nextInt(1000)));

收集结果

并行流上,forEach 方法会以任意顺序遍历各个元素,如果想要按照流中的顺序来处理它们,可以调用 forEachOrdered 方法

  • Stream.forEach
  • Stream.toArray(XXX[]::new) 返回 Object[]。收集到 数组
  • Stream.collect(Collectors.toXXX()) ,收集到 集合
  • Collector Collectors 类提供 了大量用于生成公共收集器的工厂方法
  • IntSummaryStatistics|DoubleSummaryStatistics|LongSummaryStatistics 将 mapper 应用于每个元素后所产生的结果的个数、总和、平均值、最大值和最小值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
String[] strings = Stream.of("100", "hello").toArray(String[]::new);

Set<String> set = Stream.of("100", "hello", "hello", "world").collect(Collectors.toSet());
System.out.println(set);

// 无参构造函数 TreeSet
Stream.of(10, 20, 30).collect(Collectors.toCollection(TreeSet::new));

System.out.println(
Stream.of("hello", "world").collect(Collectors.joining("|"))
);

System.out.println(Stream.of(10, 20, 30).collect(Collectors.summarizingInt(x -> x * x)).getSum());
// 输出 1400

收集到映射表中

Collectors.toMap产生映射表的键和值

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
package com.test3;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Test6 {
private static class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public int getMapId(int factor) {
return factor * id;
}
public String getName() {
return name;
}
}
public static void main(String[] args) {
Stream<Person> stream = Stream.of(
new Person(100, "mike"),
new Person(200, "tony"),
new Person(300, "john"));
Map<Integer, String> map = stream.collect(
// Collectors.toMap(Person::getId, Person::getName)
Collectors.toMap(person -> person.getMapId(100), Person::getName)
);
System.out.println(map);
}
}
// 输出: {10000=mike, 20000=tony, 30000=john}

群组和分区

  • groupingBy
  • groupingByConcurrent
  • partitioningBy

groupingBy 原型如下,可以看到传递一个 Function<T,K> 的分类器,然后返回 Map<K,List<T>> 的 映射对象,其中映射的键类型就是分类器返回的类型 K

1
2
3
4
public static <T, K> Collector<T, ?, Map<K, List<T>>> 
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public static void main(String[] args) {
Stream<Person> stream = Stream.of(
new Person(100, "mike"),
new Person(200, "tony"),
new Person(300, "john"));
// 分类,可以用 partitioningBy 代替
Map<Boolean, List<Person>> map = stream.collect(
Collectors.groupingBy(person -> person.getId() >= 200)
);
System.out.println(map);
}

// 输出,分为 true 和 false 两类
{false=[com.test3.Test6$Person@30f39991], true=[com.test3.Test6$Person@452b3a41, com.test3.Test6$Person@4a574795]}

下游收集器,对最终结果进行各种组合

groupingBy 方法会产生一个 映射表,它的每个值都是一个列表 。 如果想要以某种方式来处理这些列表,就需要提供 一个“下游收集器”。默认情况下是 toList() 也就是下游收集器得到的是一个列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
        // 提供一个 下游收集器 Set,得到的结果是一个集合
Map<String, Set<Person>> map2 = stream.collect(
Collectors.groupingBy(Person::getName, Collectors.toSet())
);

map2.forEach((k, v) -> {
System.out.println(k + ": " + v.toString());
});

// 输出
tony: [Person{id=200, name='tony'}]
mike: [Person{id=100, name='mike'}]
john: [Person{id=300, name='john'}]

Java 提供了多种可以将 群组元素 约简为 数字 的收集器:

  • counting 会产生收集到的元素的个数
  • summing(Int|Long|Double) 会接受 一个函数作为引元,将该函数应用到下游元素中,并产生它们的和
  • maxByminBy 会接受一个比较器,并产生下游元素中的最大值和最小值
  • mapping 方法会产生将函数应用到下游结果上的收集器,将 函数值 传递给另 一个收集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public static void main(String[] args) {
Stream<Person> stream = Stream.of(
new Person(100, "mike"),
new Person(200, "tony"),
new Person(400, "mike"),
new Person(300, "john"));

// 按照 name 进行分组,然后计算 收集到的每个分组元素的个数
Map<String, Long> map2 = stream.collect(
Collectors.groupingBy(Person::getName, Collectors.counting())
);

map2.forEach((k, v) -> {
System.out.println(k + ": " + v.toString());
})
}

// 输出
tony: 1
mike: 2
john: 1

约简操作 reduce

  • T reduce(T , BinaryOperator<T> );
  • Optional<T> reduce(BinaryOperator<T> );
  • <U> U reduce(U ,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);

reduce 方法是一种用于 从流中计算某个值 的通用机制,其最简单的形式将接受一个二元函数,并从前两个元素开始持续应用它。

通常,如果 reduce 方法有一项约简操作 op,那么可以表示为 (((v0 op v1) op v2) ...)

其中的 op(vi, vj) 函数写作 vi op vj。可以用于 求和、乘积、字符串连接、取最大值和最小值 、 求集的并与交等

初值的类型用于决定应该返回什么类型的值!

1
2
3
4
5
6
7
Integer sum = list.stream().reduce(0, (x, y) -> x + y);
System.out.println(sum);

// 计算所有字符串总长度
List<String> strings = Arrays.asList("android", "macos", "linux", "windows");
int len = strings.stream().reduce(0, (t, s) -> t + s.length(), Integer::sum);
System.out.println(len); // 24
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
1. T reduce(T identity, BinaryOperator<T> accumulator);
T result = identity;
for (T element : this stream)
result = accumulator.apply(result, element)
return result;

2. Optional<T> reduce(BinaryOperator<T> accumulator);
boolean foundAny = false;
T result = null;
for (T element : this stream) {
if (!foundAny) {
foundAny = true;
result = element;
}
else
result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();

3. <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
U result = identity;
for (T element : this stream)
result = accumulator.apply(result, element)
return result;

combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

基本类型流

直接存储基本类型值,而无需使用包装器

为了 创建 IntStream ,需要调用 IntStream.of(int... )Arrays.stream 方法。

IntStreamLong Stream 有静态方法 rangerangeClosed可以生成步长为 1 的整数范围

为了将基本类型流转换为对象流,需要使用 boxed 方法

1
2
IntStream stream = IntStream.of(100, 200, 300);
Stream<Integer> s = stream.boxed();

Random 类具有 ints 、 longs 和 doubles 方法,它们会返回由随机数构成的 基本类型流

并行流

  • 从任何集合中获取一个并行流 Collection.parallelStream
  • 将任意的顺序流转换为并行流 Stream.parallel

只要在终结方法执行时,流处于并行模式,那么 所有的中间流操作 都将被井行化。

当流操作并行运行时,其目标是要让其返回结果与顺序执行时返回的结果相同。重要的是,这些操作可以以任意顺序执行。要确保传递给并行流操作的任何函数都可以安全地并行执行

不要修改在执行某项流操作后会将元素返回到流中的集合,流并不会收集它们的数据,数据总是在单独的集合中 。 如果修改了这样的集合,那么流操作的结果就是未定义的。

输入与输出、目录文件处理

InputStream

  • void mark(int readlimit) 在输入流的当前位置打一个标记(并非所有的流都支持这个特性) 。 如果从输入流中已经读人的字节多于 readlimit 个,则这个流允许忽略这个标记
  • void reset() 返回到最后一个标记,随后对 read 的调用将重新读入这些字节。如果当前没有任何标记,则这个流不被重置

OutputStream

组合输入/输出流过滤器

  • FileInputStream/FileOutputStream
  • BufferedInputStream/BufferedOutputStream
  • PushbackInputStream

文本输入与输出

在存储文本字符串时,需要考虑字符编码( character encoding )方式。在 Java 内部使用的 UTF-16 编码方式中

  • OutputStreamWriter 类将使用选定的字符编码方式,把 Unicode 码元的输出流转换为字节流

  • InputStreamReader 类将包含字节(用某种字符编码方式表示的字符)的输入流转换为可以产生 Unicode 码元的读人器

  • PrintWriter

  • Scanner

  • BufferedReader

读写二进制数据

  • DataInputStream
  • DataOutputStream
  • RandomAccessFile 随机访问文件

ZIP文档

  • ZipInputStream
  • ZipOutputStream
  • ZipEntry
  • ZipFile
1
2
3
4
5
6
7
ZipInputStream zipInputStream = 
new ZipInputStream(new FileInputStream("basic/libs/mysql-connector-java-8.0.26.jar"));

for (ZipEntry entry; ((entry = zipInputStream.getNextEntry()) != null); ) {
System.out.println(entry);
}
zipInputStream.close();

对象输入/输出流与序列化

对象实现 Serializable 接口

  • ObjectOutputStream
  • ObjectInputStream

每个对象都是用一个 序列号(serial number) 保存的,这就是这种机制之所以称为对象序列化的原因

操作文件 Paths、Files

  • Path
  • File
  • StandardOpenOption
  • StandardCopyOption
  • BasicFileAttributes
  • PosixFileeAttributes
  • DosFileAttributes
  • Files.list不会进入子目录

  • Files.walk 可以递归处理目录中的所有子目录

    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
    package com.test4;

    import java.io.IOException;
    import java.nio.file.FileVisitOption;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.nio.file.attribute.BasicFileAttributes;

    public class Test2 {
    public static void main(String[] args) throws IOException {
    Path path = Paths.get("/home/", "/mike");
    System.out.println(path);

    System.out.println(path.resolveSibling("tony"));
    System.out.println(path.resolve("dir"));
    System.out.println(path.resolve("/usr/bin/"));

    System.out.println(Paths.get("/home/joxrays/Desktop/../../mike").normalize());

    Files.list(Paths.get(".")).forEach(p -> {
    System.out.println(p.toAbsolutePath());
    });

    BasicFileAttributes attributes = Files.readAttributes(Paths.get("."), BasicFileAttributes.class);
    System.out.println(attributes);

    Files.walk(Paths.get("."), FileVisitOption.FOLLOW_LINKS).forEach(System.out::println);
    Files.find(Paths.get("."), 4, (p, attr) -> attr.isDirectory(), FileVisitOption.FOLLOW_LINKS);
    }
    }
使用目录流 DirectoryStream

DirectoryStream 是 Iterable 的子接口,一种常见的用法是用 for 循环来遍历目录,支持 glob 模式

注意,遍历不是递归的。

1
2
3
4
5
6
7
public static void main(String[] args) throws IOException {
DirectoryStream<Path> ds = Files.newDirectoryStream(Paths.get("./basic/src"), "*.java");
for (Path path : ds) {
System.out.println(path);
}
ds.close();
}
walkFileTree

如果想要访问 某个目录的所有子孙成员,可以转而调用 walkFileTree 方法,并向其传递一个 FileVisitor 类型的对象

  • 在遇到一个文件或目录时: FileVisitResult visitFile(T path , BasicFileAttributes attrs)
  • 在一个目录被处理前:FileVisitResult preVisitDirectory(T dir , IOException ex)
  • 在一个目录被处理后:FileVisitResult postVisitDirectory(T dir , IOException ex)
  • 在试图访问文件或目录时 发生错误FileVisitResult visitFileFailed(path, IOException)。此时可以:
    • 继续访问下一个文件:FileVisitResult.CONTINUE
    • 继续访问,但是不再访问这个目录下的任何项:FileVisitResult.SKIP_SIBLINGS
    • 继续访问,但是不再访问这个文件的兄弟目录(和该文件在同一个目录下的文件): FileVisitResult.TERMINATE

便捷类 SimpleFileVisitor 实现了 FileVisitar 接口,可以实现递归访问某个父目录下的所有子目录和文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Files.walkFileTree(Paths.get("."), new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println(dir.toAbsolutePath());
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.SKIP_SUBTREE;
}

@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
});
使用 WatchService 监控文件变化

NIO.2 的 Path 类提供了如下 一 个方法来监听文件系统的变化

WatchService 代表 一 个文件系统监听服务,它负责监昕 path 代表的目录下的 文件变化 。

  • poll()获取下 一 个 WatchKey ,如果没有 WatchKey 发生就立即返回 null
  • poll(long timeout , TimeUnit unit) 尝试等待 timeout 时间去 获取下 一 个 WatchKey
  • take() 获取下 一 个 WatchKey ,如果没有 WatchKey 发生就一直等待
  • ENTRY_CREATE 创建
  • ENTRY_MODIFY 修改,包括修改权限之类的
  • ENTRY_DELETE 删除
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
package com.test6;

import java.nio.file.*;

public class PathWatchService {
public static void main(String[] args) throws Exception {
WatchService watchService = FileSystems.getDefault().newWatchService();

Paths.get("/home/joxrays/Desktop/").register(watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println(event.context() + "\t" + event.kind());
}
// 所有事件处理完成后,必须调用reset方法,让这个key重新进入监听队列。
boolean reset = key.reset();
if (!reset) {
break;
}
}
watchService.close();
}
}

ZIP文件系统

  • FileSystem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.test4;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

public class Test3 {
public static void main(String[] args) throws IOException {
// ZIP文件系统, 它包含 ZIP 文档中的所有文件
FileSystem fs = FileSystems.newFileSystem(Paths.get("basic/libs/mysql-connector-java-8.0.26.jar"));
// 从根目录开始遍历所有文件
Files.walkFileTree(fs.getPath("/"), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file.toAbsolutePath());
return FileVisitResult.CONTINUE;
}
});
}
}

内存映射文件 java.nio包

首先,从文件中获得一个 通道(channel),确切来说应该是 文件通道 FileChannel,通道是用于磁盘文件的一种抽象,它使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。FileChannel.open()

所有的 Channe l 都不应该通过构 造器来直接 创 建,而是通过传统的节点 InputStream 、 OutputStream的 getChannel 方法来返回对应的 Channel ,不同的节点流获得的 Channel 不 一 样。

文件通道 总是阻塞的,因此不能被置于非阻塞模式

映射模式 FileChannel.MapMode

  • 只读 read-only
  • 读写 read-write
  • 私有 private

一旦有了缓冲区,就可以使用 ByteBuffer 类和 Buffer 超类的方法读写数据。Java 对二进制数据使用 高位在前 的排序机制(大端模式)。

可以修改为小端模式

1
buffer.order(ByteOrder.LITTLE_ENDIAN);
  • get
  • put
  • MappedByteBuffer map(MapMode mode, long position, long size) 将文件的一个区域映射到内存中
  • static ByteBuffer allocate(int capacity) 构建具有给定容量的缓冲区
  • static ByteBuffer wrap(byte[] values) 构建具有指定容量的缓冲区,该缓冲区是对给定数组的包装
  • CharBuffer asCharBuffer() 构建字符缓冲区,它是对当前这个缓冲区的包装
  • transferFrom
  • transferTo
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
package com.test4;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class MMapTest {
public static void main(String[] args) throws IOException {
Path path = Path.of("basic/res/LICENSE");
long size = Files.size(path);

FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
System.out.println("File size: " + size);

ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
System.out.println("position: " + buffer.position());
System.out.println("limit: " + buffer.limit());

// 读取字节数组
byte[] bytes = new byte[(int) size];
buffer.get(bytes);
System.out.println("position: " + buffer.position());

channel.close();
//System.out.println(new String(bytes));
}
}

transferTo 例子,这里实现了快速拷贝一个文件到另一个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws IOException {
RandomAccessFile aFile = new RandomAccessFile("basic/res/web.html", "rw");
FileChannel fromChannel = aFile.getChannel();

RandomAccessFile bFile = new RandomAccessFile("basic/res/web2.html", "rw");
FileChannel toChannel = bFile.getChannel();

long position = 0;
long count = fromChannel.size();

fromChannel.transferTo(position, count, toChannel);

aFile.close();
bFile.close();
}

缓冲区数据结构 Buffer

每个缓冲区都有一下特征:

  • 一个不能改变容量
  • 一个读写位置
  • 一个界限
  • 一个可选的标记

在 Buffer 中有 三 个重要的概念:容量 C capacity )、界限 C limit ) 和位置 C position ) 。

  • 容量 (capacity ) : 缓冲区的容量 C capacity) 表示该 Buffer 的最大数据容 量 ,即最多可以存储多少数据 。 缓冲区的容量不可能为负值, 创 建后不能改变 。
  • 界限( limit ): 第 一 个不应该被读出或者写入的缓冲区位置索引 。 也就是说,位于 limit 后的数据既不可被读,也不可被写 。
  • 位置 ( position ) : 用 于 指明 下 一 个可以被读出的或者写入的缓冲区位置索引(类似于 10 流中的记录指针 )。当 使用 Buffer 从 Channel 中 读取数据 时, position 的 值恰好等于己经读到了多少数据 。

当Buffer 装入数据结束后,调用Buffer的flip()方法,该方法将 limit 设置为 position 所在位置,并将 position 设为0,这就使得Buffer的读写指针又移到了开始位置。也就是说,Buffer调用 flip() 方法之后,Buffer为输出数据做好准备

当Buffer 输出数据结束后,Buffer调用 clear() 方法,clear() 方法不是清空Buffer的数据,它仅仅将 position 置为0,将limit 置为capacity,这样 为再次向Buffer中装入数据做好准备

文件加锁机制 FileLock

  • FileChannel.lock
  • FileChannel.tryLock
  • FileLock.release/closeFileChannel.close

需要注意的几点:

  • 在某些系统中,文件加锁仅仅是建议性的,如果一个应用未能得到锁,它仍旧可以向被另一个应用并发锁定的文件执行写操作
  • 在某些系统中,不能在锁定一个文件的 同时将其映射到内存中
  • 文件锁是由整个 Java 虚拟机持有的
  • 在一些系统中,关闭一个通道会释放由 Java 虚拟机持有的底层文件上的所有锁。在同一个锁定文件上应避免使用多个通道
  • 在网络文件系统上锁定文件是高度依赖于系统的,因此应该尽量避免
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception {
try (FileChannel channel = new FileOutputStream("basic/res/test2.xml").getChannel()) {
// 如果文件不可写(只读),且尝试 lock,那么抛出 NonWritableChannelException 异常
// 这样做的目的就是说明: 只读文件没必要加锁,因为没有涉及到修改内容的情况,多个线程都可以同时读
// 但是写的时候必须要加锁!
FileLock lock = channel.tryLock();
// 在此期间内处于其他线程无法对该文件进行修改
Thread.sleep(1000);
System.out.println(channel.size());
lock.release();
}
try (FileChannel channel = FileChannel.open(Path.of("basic/res/test.xml"))) {
try (FileLock lock = channel.tryLock(0, channel.size(), true)) {
InputStream in = Channels.newInputStream(channel);
System.out.println(in.available());
}
}
}

正则表达式

  • Pattern
    • compile
    • matcher
    • split
  • Matcher
    • matches 完成全文匹配
    • find 尝试 查找下一个匹配,如果找到了另一个匹配,则返回 true
    • lookingAt
    • group 分组
    • replaceAll

编译compile指定模式

  • CASE_INSENSITIVE 忽略小写,默认情况下,这个标志只考虑 US ASCII 字符
  • UNICODE_CASE ,当与 CASE_INSENSITIVE 组合使用时,用 Unicode 字母的大小写来匹配
  • UNICODE_CHARACTER_CLASS
  • MULTILINE
  • UNIX_LINES
  • DOTALL .符号 匹配所有字符,包括行终止符
  • COMMENTS
  • LITERAL
  • CANON_EQ

如果正则表达式包含群组,那么 Matcher 对象可以揭示群组的边界。

  • start
  • end
  • String group(int groupIndex)
  • groupCount
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
package com.test4;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {
private final static Path PATH = Paths.get("basic/res/web.html");

public static void main(String[] args) throws IOException {
// String text = new String(Files.readAllBytes(PATH));
String text = Files.readString(PATH);

Pattern pattern = Pattern.compile("<img src=\"(.*?)\" alt=\"(.*?)\"",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);

Matcher matcher = pattern.matcher(text);
System.out.println("matches: " + matcher.matches());
System.out.println("find: " + matcher.find());

System.out.println(matcher.groupCount());
// 不断查找下一个匹配
while (matcher.find()) {
System.out.println(matcher.group(1) + "\t" + matcher.group(2));
}
}
}

XML

解析 XML 文档 DOM

  • DOM 树型解析器
  • SAX 流机制解析器
  • DocumentBuilderFactory.newInstance()
  • DocumentBuilder.newDocumentBuilder()
  • DocumentBuilder.parse()
  • Document.getDocumentElement()
  • NodeList
  • NamedNodeMap
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
package com.test4;

import org.w3c.dom.*;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;

public class Test4 {
public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException {

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();

Document doc = builder.parse(new File("basic/res/test.xml"));
// 返回根元素
Element root = doc.getDocumentElement();
// 返回标签名
System.out.println(root.getTagName());

// 获取根元素的子节点
NodeList nodeList = root.getChildNodes();
for (int i = 0; i < nodeList.getLength(); i++) {
Node child = nodeList.item(i);
// 只希望得到子元素,那么可以忽略空白字符
if (child instanceof Element element) {
System.out.print(element.getTagName() + ": ");

// 属性
if (element.hasAttributes()) {
System.out.print("[");
NamedNodeMap attrs = element.getAttributes();
for (int j = 0; j < attrs.getLength(); j++) {
Node attr = attrs.item(j);
System.out.print(attr.getNodeName() + ": " + attr.getNodeValue());
if (j + 1 != attrs.getLength())
System.out.print(", ");
}
System.out.println("]");
} else {
Text text = (Text) element.getFirstChild();
// 存储在节点中的文本, trim 删除换行符和空白符
System.out.println(text.getData().trim());
}
}
}
}
}

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<font>Mono</font>
<size>
16
</size>
<person name="mike" age="23">
<class>101</class>
<phone>1123912933</phone>
</person>
</bookstore>

输出

1
2
3
4
bookstore
font: Mono
size: 16
person: [age: 23, name: mike]

使用 XPath 来定位信息

XPath语法: https://www.runoob.com/xpath/xpath-syntax.html

  • XPathFactory.newInstance()
  • XPath.evaluate()
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
package com.test4;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class XPathTest {
public static void main(String[] args) throws FileNotFoundException, XPathExpressionException {
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xPath = xPathFactory.newXPath();

NodeList nodeList = (NodeList) xPath.evaluate("//div[@class='content']/div/div/p/a/img/@alt",
new InputSource(new FileInputStream("basic/res/web2.html")), XPathConstants.NODESET);

for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
System.out.println(node.getTextContent());
}
}
}

流机制解析器 SAX

在使用 SAX 解析器时,需要一个处理器来为各种解析器事件定义事件动作 。ContentHandler 接口定义了若干个在解析文档时解析器会调用的回调方法 。

  • startElementendElement 在每当遇到起始或终止标签时调用
  • characters 在每当遇到字符数据时调用
  • startDocumentendDocument 分别在文档开始和结束时各调用一次

DefaultHandler 实现了 EntityResolver, DTDHandler, ContentHandler, ErrorHandler 这些接口,这些实现的方法大部分都是不做处理的,因此我们可以自己扩展 DefaultHandler 并重写那些感兴趣的方法。

  • SAXParserFactory.newInstance()
  • SAXParserFactory.newSAXParser()
  • SAXParser.parse()

startElement 方法有四个参数,如果 命名空间处理特性 已经打开(默认关闭,可以通过 factory.setNamespaceAware(true) 激活),urilocalName提供的就是命名空间和本地(非限定)名

  • uri
  • localName
  • qName 节点标签,比如 img, div
  • attributes 该元素的属性
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
package com.test4;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
import java.io.IOException;

public class SAXTest {
public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory factory = SAXParserFactory.newInstance();
// 激活命名空间处理特性
// factory.setNamespaceAware(true);
SAXParser parser = factory.newSAXParser();

parser.parse(new File("basic/res/web2.html"), new DefaultHandler() {
@Override
public void startElement(String uri, String localName,
String qName, Attributes attributes) throws SAXException {
// System.out.println(uri + "\t" + localName + "\t" + qName);
if (qName.equals("img") && attributes != null) {
// System.out.println(attributes.getValue("alt"));
for (int i = 0; i < attributes.getLength(); i++) {
System.out.println(attributes.getLocalName(i) + "=" + attributes.getValue(i));
}
}
}

@Override
public void characters(char[] ch, int start, int length) throws SAXException {
// 获取指定位置和长度的字符串
String text = String.valueOf(ch, start, length).trim();
if (!text.isEmpty()) {
System.out.println("Text: " + text);
}
}
});
}
}

生成 XML 文档

  • LSOutput
  • Transformer
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 static void main(String[] args) throws ParserConfigurationException, IOException, TransformerException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.newDocument();

// 创建节点对象
Element rootElement = doc.createElement("info");
Element childElement = doc.createElement("user");
Text textNode = doc.createTextNode("tony");

// 添加到文档对象
doc.appendChild(rootElement);
rootElement.appendChild(childElement);
childElement.appendChild(textNode);

// 设置属性
rootElement.setAttribute("size", "100");

DOMImplementation impl = doc.getImplementation();
DOMImplementationLS implLS = (DOMImplementationLS) impl.getFeature("LS", "3.0");
LSSerializer ser = implLS.createLSSerializer();

// ser.getDomConfig().setParameter("format-pretty-print", true);
// 转换为字符串
// String xmlStr = ser.writeToString(doc);
// System.out.println(xmlStr);

// 写到文件1
LSOutput out = implLS.createLSOutput();
out.setEncoding("UTF-8");
out.setByteStream(Files.newOutputStream(Paths.get("basic/res/out.xml")));
ser.write(doc, out);

// 写到文件2
Transformer t = TransformerFactory.newInstance().newTransformer();
t.setOutputProperty(OutputKeys.INDENT, "yes");
t.setOutputProperty(OutputKeys.METHOD, "xml");
t.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
t.transform(new DOMSource(doc), new StreamResult(Files.newOutputStream(Path.of("basic/res/out.xml"))));
}
1
2
3
4
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<info size="100">
<user>tony</user>
</info>

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!