Java核心卷Ⅱ(原书第10版)笔记(上)

Java核心卷Ⅱ(原书第10版)笔记(上)


写在最前面,个人认为,卷Ⅱ更适合当手册使用,更多的是讲API的使用,前两章内容比较实际,要是合并到卷一就好了。


文章目录

第1章 Java SE 8 的流库

流提供了一钟让我们可以在比集合更高的概念级别上指定计算的数据视图,它是在Java SE 8 中引入的,用来以“做什么而非怎么做”的方式处理集合。可对集合类型使用 stream 方法生成一个流,或使用 parallelStream 方法生成并行流。

引元:以为例List<String>,其中String就是引元。

1.1 从迭代到流的操作

流表面上看起来和集合很类似,都可以转化和获取数据,但也存在明显差别:

  1. 流并不存储元素。这些元素可能存储在底层的集合中,或者是按需生成。
  2. 流的操作不会修改其他数据源。例如,filter方法不会从原流中移除元素,而是新生成一个流。
  3. 流的操作是尽可能惰性执行的。直到终止操作才处理之前的惰性内容,例如,只想找到前5个长单词而不是所有的长单词,filter方法会在匹配到第5个单词后停止过滤,代码如下所示。因此,我们甚至可用操作无限流(因为流是惰性的,直到终止操作才处理之前的惰性内容)。

List<String> collect = list.stream().filter(t -> t.length() > 13).limit(5).collect(Collectors.toList());

这部分代码,我们建立的一个包含三个阶段的操作管道:

  1. 创建一个流。stream parallelStream
  2. 指定将初始化流转化为其他流的中间操作,也可能包含多个步骤。filter limit
  3. 应用终止操作,从而产生结果。这个结果会强制执行之前的惰性操作。从此之后,这个流就再也不能用了。collect count

1.2 流的创建

// 可用 Collection 接口的 stream/parallelStream 方法将任何集合转化为一个流。
list.stream();
list.parallelStream();

// 可用 Stream.of 方法生成流,of方法具有可变参数。
Stream<String> song = Stream.of("gently", "down", "the", "Stream");

// 可用 Array.stream(array, from, to)可以从数据中位于 from 和 to 的元素中创建一个流(前闭后开)。
 Arrays.stream(array, startInclusive, endExclusive);
 
// 可用 Stream.empty 方法创建不包含任何元素的流。
 Stream<String> silence = Stream.empty();
 
/**
 * Stream 接口有两个用于创建无限流的静态方法。
 * 1. generate 方法,参数是一个函数(Supplier<T>)。
 * 2. iterate 方法,参数是一个“种子”值和一个函数(UnaryOperation<T>),函数会反复应用到之前的结果上。
 */
 Stream<String> echos = Stream.generate(() -> "Echo"); // 流中有“无限”个“Echo”
 Stream<Integer> integers = Stream.iterate(0, n -> n + 1); // 0,1,2,3,4,…
 

除了上述代码之外,Java API 中有大量方法都可用产生流,例如,Files.lines(path)方法返回一个包含了文件中所有行的Steam,而Pattern.compile(RegExp).splitAsStream(contents)方法按照某个正则表达式来分割一个CharSequence对象。

1.3 filter、map 和 flatMap 方法

  • filter :按某种条件过滤。
  • map :按某种方式转化流中的值。
  • flatMap :将map后的内容摊平。

1.4 抽取子流和连接流

  • limit:会返回一个新的流,在N个元素之后结束(N > 原来流的长度 ? 原来流的长度 : N)。
  • skip:丢弃前N个元素。
  • concat:连接两个流。第一个流不应该是无限的,否则第二个流永远都不会处理。

1.5 其他的流转换

  • distinct:剔除重复元素,流中排列顺序不变。
  • sorted:接受一个Comparator对象,使用此对象进行排序。
  • peek:接受一个函数,每次获取元素时会调用一次。对于调试之类的很方便。

1.6 简单简约

简约是一钟 终结操作(terminal operation),它会将流简约为可以在程序中使用的非流值。

  • count:返回流中元素的数量。
  • maxmin:返回最大值和最小值。返回Optional<T>
  • getFirst:获得第一个元素。返回Optional<T>
  • findAny:最好用平行流。获得一个任意匹配的元素。返回Optional<T>。。
  • anyMatch:最好用平行流。判断流中是否有元素与传入条件匹配,返回boolean值。
  • allMatch:最好用平行流。判断流中是否所有元素都与传入条件匹配,返回boolean值。
  • noneMatch:最好用平行流。判断流中是否所有元素都没有与传入条件匹配,返回boolean值。

1.7 Optional 类型

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

  • 不存在任何值的情况下产生替代物

    • orElseOptional<T>不包含对象,则返回orElse参数内容(非函数,类型为T)。
    • orElseGetOptional<T>不包含对象,则返回通过orElseGet传入的函数计算的内容。
    • orElseThrowOptional<T>不包含对象,则抛出异常,此异常通过orElseThrow传入的异常构造方法构造。
  • 其值存在时使用

    • ifPresent:此方法接受一个函数,如果Optional<T>包含对象,则传递给函数处理,否则不做任何操作 。此方法没有返回值,需要处理返回值请用map
1.7.2 不适合使用 Optional 值的方式
  • get: 方法会在 Optional 值存在的情况下获得其中包装的元素,或者在不存在的情况下抛出一个 NoSuchElementException 对象。
    只是用于判断是否非空,使用Optional值的方式并不比传统的非空比较(!= null)跟加安全与容易处理。
    个人认为:Optional更适合用于包含对象不存在时的备选对象获取的处理。
1.7.3 创建 Optional 值
  • of:创建Optional对象(不能是null,否则会NullPointerException异常)。
  • empty:创建空的Optional对象。
  • ofNullable:创建Optional对象,可以为null,对象为null返回empty对象,否则返回of对象。
1.7.4 用 flatMap 来构建 Optional 值的函数
  • flatMap可以理解为先map操作,再做了一个flat摊平操作:
/** 从两个用户流中,各取出第一个对象,组成一个新的List。  **/

User user1 = new User(1, "u1");
User user2 = new User(2, "u2");
User user5 = new User(5, "u5");
User user6 = new User(6, "u6");

List<User> lis = new ArrayList<>();
lis.add(user1);
lis.add(user2);

List<User> lis2 = new ArrayList<>();
lis2.add(user5);
lis2.add(user6);

// listStream 中包含两个List<User>对象
Stream<List<User>> listStream = Stream.of(lis, lis2);

// 如果我们使用 map ,u.subList(0, 1).stream() 会产生用户流,流的数量与列表数量相同,此处为两个。
Stream<Stream<User>> mapStream = Stream.of(lis, lis2).map(u -> u.subList(0, 1).stream());

// 使用 flatMap ,将 map 后的数据摊平(flat),java 帮我们把 map 后的每个内容都抽离出来了变为同一层了。
Stream<User> flatMapStream = Stream.of(lis, lis2).flatMap(u -> u.subList(0, 1).stream());

// 最终完成代码:
List<User> figures = Stream.of(lis, lis2).flatMap(u -> u.subList(0, 1).stream()).collect(Collectors.toList());

// 将打印 u1 和 u5
figures.forEach(f -> System.out.println(f.getName()));


可以将对 flatMap 的调用链接起来,从而构建由这些步骤构成的管道,只有所有步骤都成功时,该管道才会成功,如果其中过一个调用返回Optional.empty(),整个结果都会为空。

1.8 收集结果
  • iterator:返回一个Iterator<T>对象。
  • forEach:将函数应用于每个元素。并行流上将以任意顺序遍历各个元素。
  • forEachOrdered:按顺序将函数应用于每个元素。在并行流中将丧失并行处理的优势。
  • toArray:将流中元素组成数组。默认返回Object[],如果需要指定的类型,请传入构造器。
  • collect:接受一个Collector接口实例。Collectors 类提供了大扯用于生成公共收集器的工厂方法。
    • Collectors.tolist():将流中内容收集到列表中。
    • Collectors.toSet():将流中内容收集到集中。
    • Collectors.toCollection(TreeSet::new):将流中内容收集到特定的集中。
    • Collectors.joining(","):将流中字符串内容拼接成一个字符串,用 “,” 分割,可不给分割符号。
    • Collectors.summarizing【Int/Long/Double】:传入一个函数,将流的结果简约为总和、平均值、最大值、最小值。将返回一个(Int/Long/Double)SummaryStatistics对象存储结果。
1.9 收集到映射表中
  • Collectors.toMap(id,value):将流中内容根据函数收集到映射表中。如若发生冲突,有以下处理方案:
// 第三个参数为冲突时调用的函数,这里冲突时使用旧值。
Map<String, String> map = stream.collect(
    Collectors.toMap(
        User::getId,
        User::getName,
        (existingValue, newValue) -> existingValue));
        
// 如果想要得到 TreeMap, 那么可以将构造器作为第 4 个引元来提供。 你必须提供一种合并函数。
Map<String, String> map = stream.collect(
    Collectors.toMap(
        User::getId,
        User::getName,
        (existingValue, newValue) -> existingValue),
        TreeMap::new );
        

每个 toMap 方法,都有一个入参相同的可以产生并发映射表的 toConcurrentMap 方法。 单个并发映射表可以用于并行集合处理。当使用并行流时,共享的映射表比合并映射表要更高效(因为ConcurrentMap可以并行处理鸭)。 注意,元素不再是按照流中的顺序收集的,但是通常这不会有什么问题。

1.10 群组和分区
  • Collectors.groupingBy:按传入函数排序(传入函数计算出的内容为Key,流中元素为Value)返回Map
  • Collectors.partitioningBy:流的元素可以分区为两个列表:该函数返回 true 的元素和其他的元素。
  • Collectors.groupingByConcurrent:就会在使用并行流时获得一个被并行组装的并行映射表。这与 toConcurrentMap 方法完全类似 。
1.11 下游收集器

Collectors.groupingBy方法会产生一个映射表,它的每个值都是一个列表。如果想要以某种方式来处理这些列表,就需要提供一个“下游收集器” 。注意,下方的下游收集器应静态导入java.util.stream.Collectors.*的原因,除第一个外,省略Collectors

  • Collectors.groupingBy(User::getId, Collectors.toSet()):封装排序对象的Value值为Set
  • groupingBy(User::getGid, counting()):收集到的元素的个数为Value
  • groupingBy(User::getGid, summing【Int/Long/Double】):收集到的元素的个数为Value
  • groupingBy(User::getGid, maxBy(Comparator.comparing(User::getAge))):获得最大值,最小值用minBy
  • groupingBy(User::getGid, mapping(User::name, maxBy(Comparator.comparing(User::getAge)))):将返回一个Map<String, Optional<String>>Key为用户组ID,Value为同一用户组内年龄最大的用户名。
1.12 约简操作
  • reduce:接受一个二元函数,并从前两个元素开始持续应用它。如:reduce(Integer::sum),将返回Optional<T>对象

    • 可以带上一个 幺元值,作为计算的起点,如加法可以使用0作为幺元值。如:reduce(0, Integer::sum)。如果流为空,则会返回幺元值,所以返回类型是T
    • 可以使用reduce来计算累加,但是,当计算被并行化时,会有多个这种类型的计算(关于这么做到并行累加,可以看卷Ⅰ的 “14.7.6 并行数组算法”内容),需要添加第二个函数来辅助处理。如:reduce(O, (total, word) -> total + word.length() ,(total1, total2) -> total1 + total2);
    • 如果是数字流,可以使用mapTolnt(String::length).sum(),因为它不涉及装箱操作,更高效。
  • collect

    • 有时 reduce 会显得并不够通用 。 例如,假设我们想要收集 BitSet 中的结果。如果收集操作是并行的,那么就不能直接将元素放到单个 BitSet 中,因其对象不是线程安全的。 因此,我们不能使用 reduce, 因为每个部分都需要以其自己的空集开始,并且 reduce 只能让我们提供一个么元值。此时,应该使用 collect, 它会接受单个引元 :
    1. 一个提供者,它会创建目标类型的新实例,例如散列集的构造器 。
    2. 一个累积器,它会将一个元素添加到一个实例上,例如 add 方法。
    3. 一个组合器,它会将两个实例合并成一个,例如 addAll。

下面的代码展示了 collect 方法是如何操作位集的:

BitSet result = stream.collect(BitSet::new, BitSet::set, BitSet::or);
1.13 基本类型流

流库中具有专门的基本类型流 IntStreamLongStreamDoubleStream,用来直接存储基本类型值,而无需使用包装器。如果想要存储 shortcharbyteboolean, 可以使用 IntStream, 而对于 float, 可以使用 DoubleStream

  • 创建IntStream

    • 可以用IntStream.ofArrays.stream方法创建IntStream。也可以用IntStream.range(0, 100);IntStream.rangeClosed(0, 100);方法创建。
    • CharSequence 接口拥有 codePointschars 方法,可以生成由字符的 Unicode 码或由 UTF-16 编码机制的码元构成的 IntStream
  • 转化为基本类型流

    • 可用mapToIntmapToLongmapToDouble将流通过传入的函数,转为基本类型流。
  • 基本类型流转换为对象流

    • IntStream.range(0, 100).boxed();
  • 基本类型流上的方法与对象流上的方法的差异:

    • toArray 方法会返回基本类型数组。
    • 产生可选结果的方法会返回一个 OptionalIntOptionalLongOptionalDouble 。这些类与 Optional 类类似,但是具有 getAsIntgetAsLonggetAsDouble 方法,而不是 get 方法。
    • 具有返回总和、平均值、最大值和最小值的 sumaveragemaxmin 方法。对象流没有定义这些方法。
    • summaryStatisties 方法会产生一个类型为 IntSummaryStatisticsLongSummaryStatisticsDoubleSummaryStatistics 的对象,它们可以同时报告流的总和、平均值、最大值和最小值。
  • Random 类具有 intslongsdoubles 方法,它们会返回由随机数构成的基本类型流。

1.14 并行流

只要在终结方法执行时 ,流处于并行模式,那么所有的中间流操作都将被并行化。当流操作并行运行时,其目标是要让其返回结果与顺序执行时返回的结果相同。重要的是,这些操作可以以任意顺序执行。你的职责是要确保传递给并行流操作的任何函数都可以安全地并行执行,达到这个目的的最佳方式是远离易变状态(可以将其分组,不会多次操作)。

传递给并行流操作的函数不应该被堵寒 。 并行流使用 fork-join 池来操作流的各个部分 。 如果多个流操作被阻塞,那么池可能就无法做任何事情了 。

通过在流上调用 unordered 方法,就可以明确表示我们对排序不感兴趣。还可以通过放弃排序要求来提高 limit 方法的速度。如果只想从流中取出任意 n 个元素,可以调用list.parallelStream().unordered(). limit(n);

不要修改在执行某项流操作后会将元素返回到流中的集合(即使这种修改是线程安全的)。 记住,流并不会收集它们的数据,数据总是在单独的集合中。 如果修改了这样的集合,那么流操作的结果就是未定义的 。JDK 文档对这项需求并未做出任何约束 ,并且对顺序流和并行流都采用了这种处理方式。
更准确地讲,因为中间的流操作都是惰性的,所以直到执行终结操作时才对集合进行修改仍旧是可行的 。 例如,下面的操作尽管并不推荐,但是仍旧可以工作:

List<String> wordList = . . . ;
Stream<String> words = wordList.stream();
wordList.add("END");
long n = words.distinct().count();

// 但是,下面的代码是错误的:

Stream<String> words = wordList.stream();
words.forEach(s -> if (s.length() < 12) wordList.remove(s));
// 会报 NullPointerException 异常,数据被删了,总的长度减少了。

让并行流正常工作,需要满足的条件:

  1. 数据应该在内存中。必须等到数据到达是非常低效的。
  2. 流应该可以被高效地分成若干个子部分。由数组或平衡二叉树支撑的流都可以工作得很好,但是 Stream.iterate 返回的结果不行。
  3. 流操作的工作量应该具有较大的规模。如果总工作负载并不是很大,那么搭建并行计算时所付出的代价就没有什么意义 。
  4. 流操作不应该被阻塞。

换句话说,不要将所有的流都转换为并行流。只有在对已经位于内存中的数据执行大扯计算操作时,才应该使用并行流。


第2章 输入与输出

2.1 输入/输出流

  • System.getProperty("line.separator"):获得当前系统的行结束符。
    • ( Windows 系统是 "\r\n ", UNIX 系统是 "\n " )
  • System.getProperty( "user.dir"):用户工作目录的相对路径。(个人电脑(win10/idea)上测试是显示项目所在目录的绝对路径)
  • java.io.File.separator :获得当前系统的文件分割符(C:\Windows\win.ini中的"\")。
  • Charset.defaultCharset:会返回当前平台的编码方式(如:UTF-8)。
2.1.1 读写字节

InputStreamOutputStream是所有输入输出流的父类(图参考P42)。

抽象类方法 InputStream.read()OutputStream.write()方法都是对单个字节或字节组进行处理,在执行时都将阻塞、直至字节确实被读入或写出。

in.available:方法可以检查当前可读入的字节数量,意味着有做检查的代码片段就不可能被阻塞。
out.flush:关闭一个输出流的同时还会冲刷用于该输出流的缓冲区:所有被临时赏于缓冲区中,以便用更大的包的形式传递的字节在关闭输出流时都将被送出。如果不关闭文件,那么写出字节的最后一个包可能将永远也得不到传递。可以使用flush方法人为冲刷缓冲区。

2.1.2 完整的流家族

ReaderWriter的继承结构(继承结构图参考P43,接口结构图P44)。

  • 对于 Unicode 文本的读写,可用ReaderWriter
    • read 方法将返回 一个 Unicode 码元( 一个在 0 ~ 65535 之间的整数),或者在碰到文件结尾时返回 -1 。
    • write 方法在被调用时,需要传递一个 Unicode 码元(请查看卷 I 第 3 章有
      关 U nicode 码元的讨论) 。

InputStreamOutputStreamReaderWriter 都实现了 Closeable 接口 ,java.io.Closeable 接口扩展了 java.lang.AutoCloseable 接口 、 因此,对任何Closeable 进行操作时,都可以使用 try-with-resource 语句 (try-with-resource 语句是指声明了一个或多个资源的 try 语句一译者注) 为什么要有两个接口呢?因为 Closeable 接口的 close 方法只抛出 IOException, 而 AutoCloseable.close 方法可以抛出任何异常。

2.1.3 组合输入/输出流过滤器

FileinputStreamFileOutputStream 可以提供附着在一个磁盘文件上的输入流和输出流。
所有在 java.io 中的类都将相对路径名解释为以用户工作目录开始,你可以通过调用 System.getProperty( "user.dir") 来获得这个信息。文件分隔符请通过常量字符串java.io.File.separator获得。

某些输入流(例如 FilelnputStream 和由 URL 类的 openStream方 法返回的输入流)可以从文件和其他更外部的位置上获取字节,而其他的输入流(例如 DatainputStream) 可以将字节组装到更有用的数据类型中。Java 程序员必须对二者进行组合。

你可以通过嵌套过滤器来添加多重功能。例如,输入流在默认情况下是不被缓冲区缓存的,也就是说,每个对 read 的调用都会请求操作系统再分发一个字节。可用以下方式构造:

// 把 DatalnputStream 置于构造器链的最后,是因为需要使用它的方法,并且希望它能够使用带缓冲机制的 read 方法。
DataInputStream din = new DataInputStream(
    new BufferedInputStream(
        new FileInputStream("employee.dat")));

有时当多个输入流链接在一起时,可能需要跟踪各个中介输入流 (intermediate input stream),例如,预览下一个字节,可用PushbacklnputStreamread方法预览,unread方法推回。 可用以下代码创建一个既是可回推输入流,又是一个数据输入流的引用。

DataInputStream din = new DataInputStream(
    pbin = new PushbackInputStream(
        new BufferedInputStream(
            new FileInputStream("employee.dat")))) ;

2.2 文本输入与输出

在存储文本字符串时,需要考虑字符编码 (character encoding) 方式。在 Java 内部使用的 UTF-16 编码方式中,字符串 “1234” 编码为 00 31 00 32 00 33 00 34 (十六进制)。但是,许多程序都希望文本文件按照其他的编码方式编码。在 UTF-8 这种在互联网上最常用的编码方式中,这个字符串将写出为 4A 6F 73 C3 A9 , 其中并没有用于前 3 个字母的任何 0 字节,而特殊字符(如:中文)占用了两个字节 。

  • 文本输入流(指定编码):new InputStreamReader(inputStream, StandardCharsets.UTF_8);
  • 文本输出流(指定编码):new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
2.2.1 如何写出文本输出

对于文本输出,可以使用 PrintWriter。这个类拥有以文本格式打印字符串和数字的方法,它还有一个将 PrintWriter 链接到 FileWriter 的便捷方法。

PrintWriter out = new PrintWriter("employee.txt", StandardCharsets.UTF_8);

// 等同于:

PrintWrite out = new PrintWriter(
    new FileOutputStream("employee.txt") , StandardCharsets.UTF_8);

PrintWriterprintln 方法在行中添加了对目标系统来说恰当的行结束符 ( Windows 系统是 "\r\n ",
UNIX 系统是 "\n " ), 也就是通过洞用 System.getProperty("line.separator" ) 而获得的字符串 。

可以开启自动冲刷模式,只要调用println,缓冲区中的所有字符都会被发送(PrintWriter总是带缓冲区的)。自动冲刷模式默认关闭,开启可使用下述方式:

PrintWriter out = new PrintWriter(
    new OutputStreamWriter(
        new FileOutputStream("employee.txt"), "UTF-8") ,
        true);// autoflush

out.print 方法不抛出异常,你可以调用 checkError 方法来杳看输出流是否出现了某些错误。

Java 的老手们可能会很想知道 PrintStream 类和 System.out 底怎么了。在Java 1 .0 中, PrintStream 类只是通过将高字节丢弃的方式把所有 Unicode 字符截断成 ASCII 字符。(那时, Unicode 仍旧是 16 位编码方式)很明显,这并非一种干净利落和可移植的方式,这个问题在 Java 1.1 中通过引入 读入器(Reader)写出器(Writer) 得到了修正为了与已有的代码兼容, System.in 、 System.out 和 System.err 仍旧是输入/输出流而不是读入器和写出器。但是现在 PrintStream 类在内部采用与 PrintWriter 相同的方式将 Unicode 字符转换成了默认的主机编码方式 。 当你在使用 print 和 println 方法时,PrintStream 类型的对象的行为看起未确实很像打印写出器,但是与打印写出器不同的是,它们允许我们用 write(int) 和 write(byte[]) 方法给出原生字节。

2.2.2 如何读入文本输入
  • BufferedReader
  • Scanner
  • Files
2.2.4 字符编码方式

Java 针对字符使用的是 Unicode 标准。每个字符或“编码点”都具有一个 21 位的整数。有多种不同的字符编码方式。常用的有:

  • UTF-8:它会将每个 Unicode 编码点编码为 1 到 4 个字节(一个字节8位)的序列。 好处是传统的包含了英语中用到的所有字符的 ASCII 字符集中的每个字符都只会占用一个字节 。
  • UTF-16:这是一种在 Java 字符串中使用的编码方式。UTF-16 有两种形式:高位优先,低位优先。它会将每个 Unicode 编码点编码为 1 个或 2 个 16 位值。
获得编码的方式
  1. 平台使用的编码方式可以由静态方法 Charset.defaultCharset 返回。静态方法 Charset.availableCharsets 会返回所有可用的 Charset 实例,返回结果是一个从字符集的规范名称到 Charset 对象的映射表。
  2. StandardCharsets 类具有类型为 Charset 的静态变量,用于表示每种 Java 虚拟机都必须支持的字符编码方式,如:StandardCharsets.UTF_8
  3. 可用Charset类的静态方法 forName 获取Charset对象:Charset.forName("Shift-JIS")
  4. CharEncoding类有常用编码方式的 String 类型名称,如:CharEncoding.UTF_8

Oracle 的 Java 实现有一个用于覆盖平台默认值的系统属性file.encoding。但是它并非官方支持的属性,并且 Java 库的 Oracle 实现的所有部分并非都以一致的方式处理该属性。因此,不应该设置它。

操作文本对象时应该使用Charset对象。如:

String str = new String(bytes, StandardCharsets.UTF_8);

在不指定任何编码方式时,有些方法(例如 String(byte[])构造器)会使用默认的平台编码方式,而其他方法(例如 Files.readAlllines ) 会使用 UTF-8 。

2.3 读写二进制数据

在 Java 中,所有的值都按照高位在前的模式写出,不管使用何种处理器,这使得 Java 数据文件可以独立于平台 。

2.3.1 DataInputDataOutput 接口
// DatalnputStream 类实现了 DataInput 接口,为了从文件中读人二进制数据,可以将 DatainputStream 与某个字节源相组合,例如 FileinputStream:

DataInputStream in = new DataInputStream(new FileInputStream("employee.dat"));

// 与此类似,要想写出二进制数据,你可以使用实现了 DataOutput 接口的 DataOutputStream 类:

DataOutputStream out = new DataOutputStream(new FileOutputStream("employee.dat"));
2.3.2 随机访问文件

RandomAccessFile 类可以在文件中的任何位置查找或写入数据。磁盘文件都是随机访问的,但是与网络套接字通信的输入/输出流却不是。你可以打开一个随机访问文件,只用于读入或者同时用于读写,你可以通过使用字符串 “r” (用于读入访问)或 “rw” (用于读入写出访问)作为构造器的第二个参数来指定这个选项。

当你将已有文件作为 RandomAccessFile 打开时,这个文件并不会被删除 。

  • RandomAccessFile 类同时实现了 DataInputDataOutput 接口,常用方法如下。
    • seek 方法可以用来将这个文件指针设置到文件中的任意 字节 位置。参数 0 到文件字节长度间的long类型。
    • getFilePointer 方法将返回文件指针的当前位置。
2.3.3 ZIP 文档

ZIP 文档(通常)以压缩格式存储了一个或多个文件,每个 ZIP 文档都有一个头,包含诸如每个文件名字和所使用的压缩方法等信息。在 Java 中,可以使用 ZipinputStream 来读入 ZIP 文档 ,用ZipOutputStream来写入 Zip 文档。

ZIP 读入
  • ZipInputStream常用方法如下:
    • getNextEntry:返回一个描述这些项的 ZipEntry 类型的对象。
    • getInputStream:从当前ZipInputStream中获取传入ZipEntry类型的对象的InputStream
    • closeEntry:读入下一项。
// 下面是典型的读 ZIP 文件的代码序列:

ZipInputStream zin = new ZipInputStream(new FileInputStream(zipname));
ZipEntry entry;

while ((entry = zin.getNextEntry()) != null){
    InputStream in = zin.getlnputStream(entry); // 获得 zin 中当前获取到的 enrty 对象的流
    // 从流中读取数据,做其他操作……
    zin.closeEntry();
}

zin.close();

ZIP 写入
  • ZipOutputStream常用方法如下:
    • putNextEntry:写入ZipEntry 类型的对象。
    • closeEntry:文件写入完成时需要调用一次。
    • setLevel:设置后续各个 DEFLATED 项的默认压缩级别(书中没讲)。
    • closeEntry:文件写入完成时需要调用一次。
// 下面是典型的写 ZIP 文件的代码序列:

FileOutputStream fout = new FileOutputStream("test.zip");
ZipOutputStream zout = new ZipOutputStream(fout);

for(String filename : 需要写入ZIP文件的文件名的列表){
    ZipEntry ze = new ZipEntry(filename);
    // 将数据写入 zout
    zout.putNextEntry(ze);
    zout.closeEntry();
}

zout.close();

JAR 文件(在卷 1 笫 13 章中讨论过)只是带有一个特殊项的 ZIP 文件,这个项称作清单。你可以使用 JarInputStreamJarOutputStream 类来读写清单项。

ZIP 输入流是一个能够展示流的抽象化的强大之处的实例。当你读入以压缩格式存储的数据时,不必担心边请求边解压数据的问题,而且 ZIP 格式的字节源并非必须是文件,也可以是来自网络连接的 ZIP 数据。事实上,当 Applet 的类加载器读入 JAR 文件时,它就是在读入和解压来自网络的数据。

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

2.4.1 保存和加载序列化对象

序列化对象需要考虑很多问题,可查看《Effectvie Java》相关建议。

对希望在对象输出流中存储或从对象输人流中恢复的所有类都应进行一下修改,这些类必须实现 Serializable 接口(此接口是一个 标记接口,无任何方法)。

  • 将对象写入指定流:ObjectOutputStream.writeObject
  • 从指定流中读取对象:ObjectInputStream.readObject

对象流类都实现了 DataInput / DataOutput 接口 。

序列号

每个对象都是用一个 序列号 (serial number) 保存的,这就是这种机制之所以称为对象序列化的原因。用于保证对于原本被多个类引用的对象,这个对象只会被实际写入到流一次,读取时也只构造一次,多个原引用它的类指向它。下面是其算法:

  • 写入对象

    • 对你遇到的每一个对象引用都关联一个序列号。
    • 对于每个对象,当第一次遇到时,保存其对象数据到输出流中。
    • 如果某个对象之前已经被保存过,那么只写出“与之前保存过的序列号为 x 的对象相同”。
  • 读回对象时,整个过程是反过来的 。

    • 对于对象输入流中的对象,在第一次遇到其序列号时,构建它,并使用流中数据来初始化它,然后记录这个顺序号和新对象之间的关联。
    • 当遇到“与之前保存过的序列号为 x 的对象相同”标记时,获取与这个顺序号相关联的对象引用 。
2.4.2 理解对象序列化的文件格式(溜了溜了)
2.4.3 修改默认的序列化机制
  • transient:用于标记不想序列化的字段,如:private transient String name

  • 可通过创建(Object没这两个方法)以下方法,自定义读取/写入对象的逻辑:

// readObject 和 writeObject 方法是私有的,并且只能被序列化机制调用 。 

private void readObject(ObjectinputStream in) throws IOException, ClassNotFoundException;
private void writeObject(ObjectOutputStream out) throws IOException;

// 之后,数据域就再也不会被自动序列化,取而代之的是调用这些方法。
  • 除了让序列化机制来保存和恢复对象数据,类还可以定义它自己的机制。为了做到这一点,这个类必须实现 Externalizable 接口(它继承了Serializable接口)。Externalizable接口有两个需要实现的方法:
// readExternal 和 writeExternal 方法是公共的。特别是,readExternal 还潜在地允许修改现有对象的状态,PS:书中的入参有误没有Stream(如:应该是 ObjectInput 不是 ObjectInputStream)。

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
public void writeExternal(ObjectOutput out) throws IOException;

与前面一节描述的 readObjectwriteObject 不同,这些方法对包括超类数据在内的整个对象的存储和恢复负全责。在写出对象时,序列化机制在输出流中仅仅只是记录该对象所属的类。在读人可外部化的类时,对象输入流将用无参构造器创建一个对象,然后调用readExternal 方法。

个人理解:Serializable标记的对象Java会自动将其序列化(会帮你自动创建对象,自动处理字段),可能会占用更多空间。而Externalizable接口则完全由开发人员自己操作对象的读写,需要开发人员自己创建对象,麻烦但可用更有效的控制序列化的存储内容。

2.4.4 序列化单例和类型安全的枚举

请记住向遗留代码中所有类型安全的枚举以及向所有支持单例设计模式的类中添加
readResolve 方法。应该返回已存在对象(应可用==操作符比较),而不是新建。

  • readResolve :在对象被序列化之后就会调用它。它必须返回一个对象,而该对象之后会成为 readObject 的返回值。
2.4.5 版本管理

如果一个类具有名为 serialVersionUID 的静态数据成员,它就不再需要人工地计算其指纹,而只衙直接使用这个值。如果这个类只有方法产生了变化,那么在读人新对象数据时是不会有任何问题的(计算结果相同)。但是如果数据域产生了变化,那么就可能会有问题。

对象输入流会将这个类当前版本的数据域与被序列化的版本中的数据域进行比较,当然,对象流只会考虑非瞬时(非transient)和非静态的数据域。如果这两部分数据域之间名字匹配而类型不匹配,那么对象输入流不会尝试将一种类型转换成另一种类型,因为这两个对象不兼容;如果被序列化的对象具有在当前版本中所没有的数据域,那么对象输入流会忽略这些额外的数据;如果当前版本具有在被序列化的对象中所没有的数据域,那么这些新添加的域将被设置成它们的默认值(如果是对象则是 null, 如果是数字则为 0, 如果是 boolean 值则是 false) 。

2.4.6 为克隆使用序列化

序列化机制有一种很有趣的用法:即提供了一种克隆对象的简便途径,只要对应的类是可序列化的即可。其做法很简单:直接将对象序列化到输出流中,然后将其读回。这样产生的新对象是对现有对象的一个 深拷贝 (deep copy) 。在此过程中,我们不必将对象写出到文件中,因为可以用 ByteArrayOutputStream将数据保存到字节数组中(把对象写入流中,再读回来)。

我们应该当心这个方法,尽管它很灵巧,但是它通常会比显式地构建新对象并复制或克隆数据域的克隆方法慢得多。

2.5 操作文件

Path 接口和 Files 类是在 Java SE 7 中新添加进来的,它们用起来比自 JDK 1.0 以来就一直使用的 File 类要方便得多。

2.5.1 Path
  • Path 表示的是一个目录名序列,其后还可以跟着一个文件名。

    • Paths.get 方法接受一个或多个字符串(可能绝对,可能相对),并将它们用默认文件系统的路径分隔符(类 Unix 文件系统是 /, Windows 是)连接起来。然后它解析连接起来的结果,如果其表示的不是给定文件系统中的合法路径,那么就抛出 InvalidPathException 异常。这个连接起来的结果就是一个 Path 对象。
  • basePath.resolve(q)

    • 如果 q 是绝对路径,则结果就是 q 。
    • 否则,根据文件系统的规则,将 “basePath 后面跟着 q” 作为结果。
  • workPath.resolveSibling("temp"):如果 workPath 是 /opt/myapp/work,将创建 /opt/myapp/temp。

  • basePath.relativize(r):如果 basePath 是 /home/cay,r 是 /home/fred/myprog,将创建 … /fred/myprog。

  • basePath.normalize():将移除所有冗余的.和…部件(或文件系统认为的所有冗余部件),例如:规范化 /home/cay/…/fred/./myprog 将产生 /home/fred/myprog 。

  • basePath.toAbsolutePath():将产生给定路径的绝对路径,该绝对路径从根部件开始,例如/home/fred/input.txt 或 c:\Users\fred\input.txt 。

Path 类有许多有用的方法用来将路径断开。下面的代砃示例展示了其中部分最有用的方法:

Path p = Paths.get("/home", "fred", "myprog.properties");
Path parent= p.getParent(); // the path /home/fred
Path file= p.getFileName(); // the path myprog.properties
Path root = p.getRoot(); // the path /

正如你已经在卷 I 中看到的,还可以从 Path 对象中构建 Scanner 对象:
Scanner in = new Scanner(Paths.get("/home/fred/input.txt"));

偶尔,你可能需要与遗留系统的 API 交互,它们使用的是 File 类而不是 Path 接口。Path 接口有一个 toFile 方法,而 File 类有一个 toPath 方法。

2.5.2 读写文件
处理中等长度的文本文件
// 取文件的所有内容
byte[] bytes = Files.readAllBytes(path);
 
// 将文件当作字符串读入
String content = new String(bytes, charset);

// 将文件当作行序列读入
List<String> lines= Files.readAllLines(path, charset);

// 写出一个字符串到文件中
Files.write(path, content.getBytes(charset));

// 向指定文件追加内容
Files.write(path, content.getBytes(charset), StandardOpenOption.APPEND);

// 一个行的集合写出到文件中
Files.write(path, lines);

处理比较长的,或二进制的文本文件
// 应该使用所熟知的输入/输出流或者读入器/写出器:

InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream(path);
Reader in = Files.newBufferedReader(path, charset);
Writer out= Files.newBufferedWriter(path, charset);

2.5.3 创建文件和目录

Files.createDirectory(path):创建新目录。路径中除最后一个部件外,其他部分都必须是已存在的。
Files.createDirectories(path):创建一个空文件。如果文件已经存在了,那么这个洞用就会抛出异常。检查文件是否存在和创建文件是原子性的,如果文件不存在,该文件就会被创建,并且其他程序在此过程中是无法执行文件创建操作的。

给定位置或者系统指定位置创建临时文件或临时目录

Path newPath = Files.createTempFile(dir, prefix, suffix);
Path newPath = Files.createTempFile(prefix, suffix);
Path newPath = Files.createTempDirectory(dir, prefix);
Path newPath = Files.createTempDirectory(prefix);
// 其中, dir 是一个 Path 对象, prefix 和 suffix 是可以为 null 的字符串。
// 例如,调用 Files.createTempFile(null," .txt") 可能会返回一个像 /tmp/1234405522364837194.txt 这样的路径。
2.5.4 复制、移动和删除文件

Files.copy(fromPath, toPath);:将文件从一个位置复制到另一个位置。
Files.move(fromPath, toPath);:移动文件(即复制并删除原文件)。

// 如果想要覆盖已有的目标路径,可以使用 REPLACE_EXISTING 选项。如果想要复制所有的文件属性性,可以使用 COPY_ATTRIBUTES 选项也可以像下面这样同时选择这两个选项:

Files.copy(fromPath, toPath, StandardCopyOption.REPLACE_EXISTING,
    StandardCopyOption.COPY_ATTRIBUTES);
    
// 你可以将移动操作定义为原子性的,这样就可以保证要么移动操作成功完成,要么源文件继续保持在原来位置。具体可以使用 ATOMIC_MOVE 选项来实现:
Files.move(fromPath, toPath, StandardCopyOption.ATOMIC_MOVE);

Files.copy(inputStream, toPath);:将一个输人流复制到 Path 中,这表示你想要将该输入流存储列硬盘。
Files.copy(fromPath, outputStrearn);:将一个 Path 复制到输出流中。
Files.delete(path);:删除文件。文件不存在会抛出异常。
Fi1es.deleteIfExists(path);:删除文件。返回一个boolean值,删除成功返回 true,否则 false

2.5.5 获取文件信息

下面的静态方法都将返同 一个 boolean 值,表示检查路径的某个属性的结果 :

  • exists
  • isHidden
  • isReadable, isWritable, isExecutable
  • isRegularFile , isDirectory, isSymboliclink

要获取如:创建文件时间等属性,可以调用以下代码获取:

BasicfileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);

如果用户的文件系统兼容 POSIX,也可以将 BasicFileAttributes改为PosixFileAttributes获取POSIX文件实例。

2.5.6 访问目录中的项

Files.list:返回一个可以读取目录中各个项的 Stream<Path> 对象。目录是被惰性读取的,这使得处理具有大址项的目录可以变得更高效。

File.walklist 方法不会进入子目录。只要遍历的项是目录,那么在进入它之前,会继续访问它的兄弟项。

File.find:如果要过滤 walk返回的路径,并且你的过滤标准涉及与目录存储相关的文件属性,例如尺寸、创建时间和类型(文件、目录、符号链接),那么应该使用 find 方法来替代 walk 方法 。

2.5.7 使用目录流

DirectoryStream接口对象,可对目录遍历过程进行更加细粒度的控制。(它不是 java.util.stream.Stream 的子接口,而是专门用于目录遍历的接口。它是 Iterable 的子接口,可用forEach遍历)

Files.newDirectoryStream:会产生一个DirectoryStream接口对象,可用try语句块确保目录流可以被正确关闭(书中API错了,File 没有这个方法)。访问目录中的项并没有具体的顺序。可以用 glob 模式(类似通配符,如果是windows系统,反斜杠需要转义两次)来过滤文件:

try (DirectoryStream<Path> entries = Files.newDirectoryStream(dir, "*.java"))

Files.walkFileTree:方法可以访问某个目录的所有子孙成员。入参需要一个FileVisitor<? Super Path>类型的对象。可用已实现其方法的便捷类SimpleFileVisitor,其除 visitFileFailed方法之外的所有方法并不做任何处理而是直接继续访问,而 visitFileFailed 方法会抛出由失败导致的异常,并进而终止访问。

  • 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_SUBTREE
    • 继续访问,但是不再访问这个文件的兄弟文件(和该文件在同一个目录下的文件)了: FileVisitResult. SKIP_SIBLINGS
    • 终止访问:FileVisitResult.TERMINATE

当有任何方法抛出异常时,就会终止访问,而这个异常会从 walkFileTree 方法中抛出。

2.5.8 ZIP 文件系统

读取ZIP文件系统:

FileSystem fs = FileSystems.newFileSystem(Paths.get(zipname), null);

从 ZIP 文档中复制出已知文件名的文件:

Files.copy(fs.getPath(sourceName), targetPath);

列出 ZIP 文档中的所有文件:

// 这比 2.3.3 节中描述的 API 要好用,它使用的是多个专门处理 ZIP 文档的新类。

FileSystem fs = FileSystems.newFileSystem(Paths.get(zipname), null);
Files.walkFileTree(fs.getPath("/"), new SimpleFileVisitor<Path>(){
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
    {
        System.out.println(file);
        return FileVisitResult.CONTINUE;
    }
});

2.6 内存映射文件

本小节相关文档推荐:Java NIO?看这一篇就够了

2.6.1 内存映射文件的性能

对于中等尺寸文件的顺序读入,没有必要使用内存映射。

java.nio 包使内存映射变得十分简单:

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

    FileChannel channel = FileChannel.open(path, options);

  • 其次,用 FileChannel 类的 map 方法从这个通道中获得一个ByteBuffer。可以指定想要映射的文件区域与映射模式,支持的模式有三种:

    • FileChannel.MapMode.REAO_ONLY:所产生的缓冲区是只读的,任何对该缓冲区写入的尝试都会导致 ReadOnlyBufferException 异常。
    • FileChannel.MapMode.READ_WRITE:所产生的缓冲区是可写的,任何修改都会在某个时刻写同到文件中。注意,其他映射同一个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于操作系统的。
    • FileChannel.MapMode.PRIVATE:所产生的缓冲区是可写的,但是任何修改对这个缓冲区来说都是私有的,不会传播到文件中。
  • 最后,有了缓冲区,就可以使用 ByteBuffer 类和 Buffer 超类的方法读写数据了。

    • 缓冲区支持顺序和随机数据访问,它有一个可以通过 getput 操作来移动的位置。
    • Java 对二进制数据使用高位在前的排序机制,但是,如果需要以低位在前的排序方式处理包含二进制数字的文件,那么只需调用order(ByteOrder.LITTLE_ENDIAN)

java.util.zip 包中包含一个 CRC32 类,可以用来算一个字节序列的校验。经常用来判断一个文件是否已损坏。(在
实践中,每次会以更大的工夫而不是一个字节为单位来读取和更新数据,而它们的速度差异
并不明显。个人感觉MD5更实用些)

2.6.2 缓冲区数据结构

Buffer 类是—个抽象类,它有众多的具体子类,包括 ByteBufferIntBufferLongBuffer等,可用这些类的静态方法创建缓冲区。注意!StringBuffer类与这些缓冲区没有关系。

  • 每个缓冲区都具有 :
    • 一个 容量,它永远不能改变。
    • 一个 读写位置,下一个值将在此进行读写。
    • 一个 界限,超过它进行读写是没有意义的。
    • 一个可选的 标记,用于重复一个读入或写出操作。

以上值满足这些条件:0 ≤ 标记 ≤ 位置 ≤ 界限 ≤ 容量

2.6.3 文件加锁机制
  • 以下两个方法可以给文件加锁。这个文件将保持锁定状态,直至这个通道关闭,或者在锁上调用了 release 方法。最好在一个try语句中执行释放锁的操作。
    • fileChannel.lock:阻塞,直到可获得锁。
    • fileChannel.tryLock:立刻返回,要么获得锁,要么返回null

也可以传入参数,只锁定文件的一部分:

lock(long start, long size, boolean shared)

如果 shared 标志为 false , 则锁定文件的目的是读写,而如果为 true, 则这是一个共享锁,它允许多个进程从文件中读入,并阻止任何进程获得独占的锁。并非所有的操作系统都支持共享锁,因此你可能会在请求共享锁的时候得到的是独占的锁。调用 Filelock 类的isShared 方法可以查询你所持有的锁的类型 。

果你锁定了文件的尾部,而这个文件的长度随后增长超过了锁定的部分,那么增长出来的额外区域是未锁定的,要想锁定所有的字节,可以使用 Long.MAX_VALUE 来表示尺寸 。

文件加锁是依赖OS的,注意事项:

请记住,文件加锁机制是依赖于操作系统的,下面是需要注意的几点:

  • 在某些系统中,文件加锁仅仅是建议性的,如果一个应用未能得到锁,它仍旧可以向被另一个应用并发锁定的文件执行写操作 。
  • 在某些系统中,不能在锁定一个文件的同时将其映射到内存中。
  • 文件锁是由整个 Java 虚拟机持有的 。 如果有两个程序是由同一个虚拟机启动的(例如 Applet 和应用程序启动器)。那么它们不可能每一个都获得一个在同一个文件上的锁。当调用 locktrylock 方法时,如果虚拟机已经在同一个文件上持有了另一个重叠的锁,那么这两个方法将抛出 OverlappingFilelockException
  • 在一些系统中,关闭一个通道会释放由 Java 虚拟机持有的底层文件上的所有锁。因此,在同一个锁定文件上应避免使用多个通道。
  • 在网络文件系统上锁定文件是高度依赖于系统的,因此应该尽记避免。

2.7 正则表达式(略)

关于正则表达式,此后如有兴趣,可通过《精通正则表达式》学习。

Pattern 类可用于操作正则表达式。(JKD 1.8 可用参与流操作)

第3章 XML

3.1 XML 概述

XML 与 HTML 的区别:
  • 与 HTML 不同,XML 是大小写敏感的。
  • 在 HTML 中,如果从上下文中可以分清哪里是段落或列表项的结尾,那么结束标签(如 </p> ) 就可以省略,而在 XML 中结束标签绝对不能省略。
  • 在 XML 中,只有单个标签而没有相对应的结束标签的元素必须以/结尾。
  • 在 XML 中 ,属性值必须用引号括起来。
  • 在 HTML 中,属性名可以没有值。在 XML 中,所有展性必须都有屈性值。

<![CDATA[ ……]]>中的内容可以包含 <、>、& 之类的字符串,但不能包含能包含字符串]]>

3.2 解析 XML 文档

Java 库提供了两种 XML 解析器:

  • 像文档对象模型型 (Document Object Model, DOM) 解析器这样的 树型解析器 (tree parser), 它们将读入的 XML 文杻转换成树结构。
  • 像 XML 简单 API ( Simple APT for XML, SAX) 解析器这样的 流机制解析器 (streaming parser), 它们在读入 XML 文档时生成相应的事件。

PS:书中DOM解析器使用的是 javax.xml.parsersDocumentBuilderFactory类,可读入文件、URL、输入流。目前接手的项目常用的是org.dom4j.DocumentHelperDocumentHelper类,此类可直接操作 XML 字符串。

3.3 验证 XML 文档

如果要指定文档结构,可以提供一个文档类型定义 ( DTD ) 或一个 XML Schema 定义。DTD 或 schema 包含了用于解释文档应如何构成的规则,这些规则指定了每个元索的合法子元素和属性。

如果你使用的是 DOM 解析器,并且想要支持 PUBLIC 标识符,请调用 DocumentBuilder 类的 setEntityResolver 方法 来安装 EntityResolver 接口的某个实现类的一个对象。

XML 标准允许解析器假设 DTD 都是非二义性的,Java XML 库中的解析器在遇到有歧义的 DTD 时,不会给出警告在解析时,它仅仅在两者中选取笫一个匹配项,这将导致它会拒绝一些正确的输入。

3.4 使用 XPath 来定位信息

XPath 可用于定位某个 XML 文档中的一段特定信息。

Java SE 5.0 增加了一个 API 来计算 XPath 表达式:

// 首先,通过 XPathFactory 创建一个 XPath 对象
XPathFactory xpfactory = XPathFactory.newInstance();
XPath path = xpfactory.newXPath();

// 然后,调用 evaluate 方法来计算 XPath 表达式:
String username = path.evaluate("/configuration/database/username", doc);

/** 可用同一个 XPath 对象计算多个表达式,此方法不止能获取字符串类型内容 **/

// 获取一组节点
NodeList nodes = (NodeList) path.evaluate("/gridbag/row", doc, XPathConstants.NODESIT);

// 获取一个节点
Node node = (Node) path.eval uate("/gridbag/row[1]", doc, XPathConstants.NODE);

// 获取一个数字
int count = ((Number)path.eva1uate("count(/gridbag/row) ", doc, XPathConstants.NUMBER)).intValue();

3.5 使用命名空间

可以控制解析器对命名空间的处理。默认情况下,Java XML 库的 DOM 解析器并非“命名空间感知的”。
要打开命名空间处理特性,诮调用 DocumentBuilderFactory 类的 setNamespaceAware 方法 :

factory.setNamespaceAware(true); 

此时该工厂方法产生的所有生成器便都支持命名空间了。每个节点有三个属性:

  • 带有前缀的 限定名 (qualified), 由 getNodeNamegetTagName 等方法返回。
  • 命名空间 URl , 由 getNamespaceURI 方法返回。
  • 不带前缀和命名空间的 本地名 ( local name), 由 getlocalName 方法返回 。
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">

它会报告如下信息:
• 限定名:xsd:schema
• 命名空间 URI :http://www.w3.org/2001/XMLSchema
• 本地名:schema

如果对命名空间的感知特性被关闭,`getLocalName` 和 `getNamespaceURI` 方法将返回 `null`。

3.6 流机制解析器

DOM 解析器会完整地读入 XML 文档,然后将其转换成一个树形的数据结构。如果文档很大,并且处理算法又非常简单,可以在运行时解析节点,而不必看到完整的树形结构,那么就可以使用 流机制解析器

老而弥坚的 SAX 解析器和添加到 Java SE 6 中的更现代化的 StAX 解析器 。 SAX 解析器使用的是事件同调 (event callback), 而 StAX 解析器提供了遍历解析事件的迭代器,后者用起来通常更方便一些。

SAX 解析器
SAXParserFactory factory= SAXParserFactory.newinstance();
SAXParser parser = factory.newSAXParser();
StAX 解析器

StAX 解析器是一种“拉解析器 (pull parser)",与安装事件处理器不同,你只需使用下面这样的基本循环来迭代所有的事件:

InputStream in = url.openStream();
XMLInputFactory factory = XMLInputFactory.newInstance();
XMLStreamReader parser= factory.createXMLStreamReader(in);
while (parser.hasNext())
{
    int event= parser.next();
    Call parser methods to obtain 切ent details
}

3.7 生成 XML 文档

可以使用 DocumentBuilderFactory 创建生成器,也可以直接使用 DocumentBuilder 类的 newDocument 方法可以得到一个空文档。

读写操作到时候再查。

3.8 XSL 转换(略)


第 4 章网络

4.1 连接到服务器

  • telnet
  • java 的 Socket

可用 socket.shutdownOutput(); 实现半关闭。
如果需要在主机名和因特网地址之间进行转换,可用 InetAddress 类。

4.2 实现服务器

可用以下代码建立套接字并指定端口:

ServerSocket s = new ServerSocket(port);

4.3 可中断套接字

当线程因套接字无法响应而发生阻塞时,则无法通过调用 interrupt 来解除阻塞。

为了中断套接字操作,可以使用 java.nio 包提供的一个特性:SocketChannel 类。可以使用如下方法打开 SocketChannel :

SocketChannel channel = SocketChannel.open(new InetSocketAddress(host, port));

通道 (channel) 并没有与之相关联的流。实际上,它所拥有的 read 和 write 方法都是通过使用 Buffer 对象来实现的(关于 NIO 缓冲区的相关信息请参见第 2 章)。

通过调用静态方法 Channels.newOutputStream, 可以将通道转换成输出流。

OutputStream outStream = Channels.newOutputStream(channel);

4.4 获取 Web 数

URL 和 URI

java.net 包对 统一资源定位符 (Uniform Resource Locator, URL) 和 统一资源标识符 (Uniform Resource Identifier, URI) 作了非常有用的区分。
UR I 是个纯粹的语法结构 , 包含用来指定 Web 资源的字符串的各种组成部分 。 URL 是
URI 的一个特例,它包含了用于定位 Web 资源的足够信息 。

  • URL
    URL 和 URLConnection 类封装了大队复杂的实现细节,这些细节涉及如何从远程站点
    获取信息 。
// 从一个字符串构建一个 URL 对象:
URL url = new URL(urlString);

// 想获得资源的内容
InputStream inStream = url.openStream();
Scanner in = new Scanner(inStream, "UTF-8");
  • URI(略)
    在 Java 类库中,URI 类并不包含任何用于访问资源的方法,它的唯一作用就是解析。
    包含 scheme: 部分的 URl 称为 绝对 URI。 否则,称为 相对 URI。
4.4.2 使用 URLConnection 获取信息

如果想从某个 Web 资源获取更多信息,那么应该使用 URLConnection 类,通过它能够得到比基本的 URL 类更多的控制功能。

4.5 发送 E-mail

使用 JavaMail API 在 Java 程序中发送 E-mail 。


第 5 章 数据库编程

PS:目前大环境下,持久化框架多的眼花缭乱,已经很少使用JDBC编写了。

1996 年,Sun 公司发布了第 1 版的 Java 数据库连接 (JDBC) API , 使编程人员可以通过这个 API 接口连接到数据库,并使用结构化查询语言(即 SQL ) 完成对数据库的在找与更新。(SQL 通常发音为"seq uel" , 它是数据库访问的业界标准。) JDBC 自此成为 Java 类库中最常使用的 API 之一。

JDBC 的版本已更新过数次。作为 Java SE 1 .2 的一部分, 1998 年发布了 JDBC 第 2 版。JDBC 3 已经被囊括到了 Java SE 1 .4 和 5.0 中,而在本书出版之际,最新版的 JDBC 4.2 也被囊括到了 Java SE 8 中 。

根据 Oracle 的声明,JDBC 是一个注册了商标的术语,而并非 Java Database Connectivity 的首字母缩写。对它的命名体现了对 ODBC 的致敬,后者是微软开创的标准数据库 API, 并因此而并入了 SQL 标准中 。

5.1 JDBC 的设计

JDBC 和 ODBC 都基于同一个思想:根据 API 编写的程序都可以与驱动管理器进行通信,而驱动管理器则通过驱动程序与实际的数据库进行通信。

5.1.1 JDBC 驱动程序类型

JDBC 规范将驱动程序归结为以下几类:

  • 笫 1 类驱动程序将 JDBC 翻译成 ODBC, 然后使用一个 ODBC 驱动程序与数据库进行通信。
    • 较早版本的 Java 包含了一个这样的驱动程序: JDBC/ODBC 桥,不过在使用这个桥接器之前需要对 ODBC 进行相应的部署和正确的设置。在 JDBC 面世之初,桥接将可以方便地用于测试,却不太适用于产品的开发 。(Java 8 已经不再提供 JDBC/ODBC桥了)
  • 笫 2 类驱动程序是由部分 Java 程序和部分本地代码组成的,用于与数据库的客户端API 进行通信。
    • 在使用这种驱动程序之前,客户端不仅斋要安装 Java 类库,还需要安装一些与平台相关的代码 。
  • 第 3 类驱动程序是纯 Java 客户端类库,它使用一种与具体数据库无关的协议将数据库请求发送给服务器构件,然后该构件再将数据库诮求翻译成数据库相关的协议。这简化了部署,因为平台相关的代码只位于服务器端。
  • 第 4 类驱动程序是纯 Java 类库,它将 JDBC 请求直接翻译成数据库相关的协议 。

大部分数据库供应商都为他们的产品提供第 3 类或第 4 类驱动程序。

JDBC 最终是为了实现以下目标:

  • 通过使用标准的 SQL 语句,甚至是专门的 SQL 扩展,程序员就可以利用 Java 语言开发访问数据库的应用,同时还依旧遵守 Java 语言的相关约定。
  • 数据库供应商和数据库工具开发商可以提供底层的驱动程序。因此,他们可以优化各自数据库产品的驱动程序 。
5.1.2 JDBC 的典型用法

PS:看看故事就好了,其实也可用略的。

以往:在传统的客户端/服务器模型中,通常是在服务器端部署数据库,客户端安装GUI程序。
当今:如今的三层架构MVC中,客户端不直接调用数据库,而是调用服务器上的中间件层(业务逻辑),由中间件层完成数据库查询操作。这种三层模型有以下优点:它将可视化表示(位于客户端)从业务逻辑(位于中间层)和原始数据(位于数据库)中分离出来。因此,我们可以从不同的客户端,如 Java 桌面应用、浏览器或者移动 App,来访问相同的数据和相同的业务规则。

5.2 结构化查询语言(学过数据库都会涉及,略)

5.3 JDBC 配置(目前都是Spring集成配置,略)

5.4 使用 JDBC 语句

PS:其实也可以略的,但想回顾一下大学时期的内容,以下非书中内容。

// 加载驱动程序
Class.forName(jdbcDriver);
// 获取数据库连接
Connection conn = DriverManager.getConnection(jdbcUrl, username, password);

// 创建 Statement 对象
Statement stat = conn.createStatement();

// 操作数据库……记得 close stat 和 conn
ResultSet result = stat.XXXX();

// 可使用带资源的try语句简化关闭操作,其中再使用一个单独的try/catch块处理异常。

5.4.3 分析 SQL 异常

每个 SQLException 都有一个由多个 SQLException 对象构成的链,这些对象可以通过 getNextException 方法获取 。
Java SE 6 改进了 SQLException 类,让其实现了 Iterable<Throwable> 接口,其 iterator() 方法可以产生一个 Iterator<Throwable> , 这个迭代器可以迭代这两个链,首先迭代第一个 SQLException 的成因链,然后迭代下一个 SQLException

此部分有 SQLException的继承图,但目前没什么兴趣。

5.5 执行查询操作

PS:可以略,怀念大学的笔记。

预备查询语句:

String publisherQuery =
    "SELECT Books.Price, Books.Title " +
    " FROM Books, Publishers " +
    " WHERE Publishers.Name = ? ";
    PreparedStatement stat = conn.prepareStatement(publisherQuerySQL);
    
    stat.setString(1, publisher);
    
    ResultSet result = stat.XXX(); 

5.6 可滚动和可更新的结果集(略)

// 可滚动,且数据库在查询生成结果集之后发生了变化,不可编辑
Statement stat = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);

// 可滚动,且数据库在查询生成结果集之后发生了变化,可编辑
Statement stat = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);

5.7 行集(略)

5.8 元数据

在 SQL 中,描述数据库或其组成部分的数据称为元数据(区别于那些存在数据库中的实际数据) 我们可以获得三类元数据:关于数据库的元数据、关于结果集的元数据以及关于预备语句参数的元数据。

可用以下代码获取:

DatabasMetaData meta = conn.getMetaData();

5.9 事务(目前框架都交给Spring处理,简单看)

// 关闭自动提交
conn.setAutoCommit(false);
// 提交
conn.commit();
// 或者回滚
conn.rollback();

/** 使用保存点可以避免回滚到事务的开头 **/

// 设置保存点
Savepoint svpt = conn.setSavepoint();
// 回滚至保存点
conn.rollback(svpt);
// 设置保存点
conn.releaseSavepoint(svpt) ;

5.10 高级 SOL 类型

SOL 数据类型及其对应的 Java 类型

列举了 JDBC 支持的 SQL 数据类型以及它们在 Java 语言中对应的数据类型:

SQL 数据类型JAVA 数据类型
INTEGER or INTint
SMALLINTshort
NUMERIC(m , n) , DECIMAL(m,n) or OEC(m,n)java.math.BigDecimal
FLOAT(n)double
REALfloat
DOUBLEdouble
CHARACTER(n) or CHAR(n)String
VARCHAR(n), LONG VARCHARString
BOOLEANboolean
DATEjava.sql.Date
TIMEjava.sql.Time
TIMESTAMPjava.sql.Timestamp
BLOBjava.sql.Blob
CLOBjava.sql.Clob
ARRAYjava.sql.Array
ROWIOjava.sql.Rowld
NCHAR(n), NVARCHAR(n), LONG NVARCHARString
NCLOBjava.sql.NClob
SQLXMLjava.sql.SQLXML

5.11 Web 与企业应用中的连接管理(略,连接池与数据库配置等都集成于Spring)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值