Core Java™, Volume Ⅱ-Advanced Features

(Eleventh Edition) Book Notes

Featured image

第1章 Java 8的流库

从迭代到流的操作
流的创建
filter、map和flatMap方法
并行流
简单约简

1.1 从迭代到流的操作

  1. 流并不存储元素。这些元素可能存储在底层的集合中,或者按需生成。
  2. 流的操作不会修改其数据源。
  3. 流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。
// 计数:字符串列表words中所有长度大于12的单词

    long count = 0;

// 1. 常规方式
    for (String w : words) {
        if (w.length() > 12) 
            count++;
    }

// 2. 流
    count = words.stream().filter(w -> w.length() > 5).count();

// 3. 并行流
    count = words.parallelStream().filter(w -> w.length() > 5).count();

1.2 流的创建

Stream<String> words = Stream.of(contents.split("\\PL+")); // split("\\PL+") 以非字母(\\PL+)为分隔

// of 方法句有可变长参数
Stream<String> words = Stream.of("gently", "down", "the", "stream");
Stream<String> silence = Stream.empty();
// Generic type <String> is inferred; same as Stream.<String>empty()
// 获得一个常量值的流
Stream<String> echos = Stream.generate(() -> "Echo");

// 获取一个随机数的流
Stream<Double> randoms = Stream.generate(Math::random);

/** iterate方法 
  * 会接受一个“种子”值,
  * 以及一个函数,
  * 并且会反复地将该函数应用到之前的结果上。
  * 下例会生成一个 0 1 2 3...这样的序列。
  */
Stream<BigInteger> integers 
    = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)

/** 生成一个有限序列
  * 添加一个谓词描述迭代应该何时结束。
  */
var limit = new BigInteger("1000000");
Stream<BigInteger> integers 
    = Stream.iterate(BigInteger.ZERO, 
    n -> n.compareTo(limit) < 0, 
    n -> n.add(BigInteger.ONE)
    
/** Pattern类的splitAsStream方法
  * 按照某个正则表达式来分割一个CharSequence对象。
  */
Stream<String> words = Pattern.compile("\\PL+").splitAsStream(contents);

/** Scanner.tokens方法
  * 会产生一个扫描器的符号流。
  */
Stream<String> words = new Scanner(contents).tokens();

/** 静态Files.lines方法
  * 会返回一个包含了文件中所有行的Stream。
try (Stream<String> lines = Files.lines(path)) {
    Process lines
}

/** 若持有的Iterable对象不是集合,
  * 则可以通过下面的方法将其转换为一个流。
  */
StreamSupport.stream(iterable.spliterator(), false);

/** 若持有的是Iterator对象,
  * 得到一个由它结果构成的流。
  */
StreamSupport.stream(Spliterators.spliteratorUnknownSize(
    iterator, Spliterator.ORDERED), false);

1.3 filter、map和flatMap方法

// 将一个字符串流转换为只包含长单词(长度大于12)的新流
List<String> words = ...;
Stream<String> longWords = words.stream().filter(w -> w.length() > 12);
// 函数 将所有单词转换成小写
Stream<String> lowercaseWords = words.stream().map(String::toLowerCase);
// lambda表达式 新流中包含所有单词的首字母
Stream<String> lowercaseWords = words.stream().map(s -> s.substring(0,1));
public static Stream<String> codePoints(String s) {
    var result = new ArrayList<String>();
    int i= 0;
    while (i < s.length()) {
        int j = s.offsetByCodePoints(i, 1);
        result.add(s.substring(i, j));
        i = j;
    }
    return result.stream();
}
/** codePoints("boat")的返回值是流["b", "o", "a", "t"]
  * 将codePoints方法映射到一个字符串流上
  */
Stream<Stream<String>> result = words.stream().map(w -> codePoints(w));
/** 上述代码会得到一个包含流的流
  * [...["y", "o", "u", "r"],["b", "o", "a", "t"],...]
  * 为了将其摊平为单个流
  * [..."y", "o", "u", "r", "b", "o", "a", "t",...]
  * 使用flatMap方法而不是map方法
  */
Stream<String> flatResult = words.stream().flatMap(w -> codePoints(w));

1.4 抽取子流和组合流

// 产生一个包含100个随机数的流
Stream<Double> randoms = Stream.generate(Math::random).limit(100);
// 跳过第一个元素
Stream<String> words = Stream.of(contents.split("\\PL+")).skip(1);
/** 上文codePoints方法将字符串分割为字符,然后收集所有的数字元素。
  * takeWhile方法可以实现此目标。
  */
Stream<String> initialDigits = codePoints(str).takeWhile(s -> "0123456789".contains(s));
Stream<String> withoutInitialWhiteSpace 
    = codePoints(str).dropWhile(s -> s.trim().length() == 0);

- Stream类的静态concat方法可以将两个流连接起来
Stream<String> combined = Stream.concat(codePoints("Hello"), codePoints("World");
// Yields the stream ["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"]

1.5 其他流的转换

Stream<String> uniqueWords = Stream.of("merrily", "merrily", "merrily", "gently").distinct();
// only one "merrily" is retained
// 使最长的字符串排在最前面
Stream<String> longestFirst = words.stream().sorted(Comparator.comparing(String::length).reversed());

与所有流转换一样,sorted方法会产生一个新的流,它的元素是原有流中按照顺序排列的元素。

Object[] powers 
    = Stream.iterate(1.0, p -> p * 2).peek(e -> System.out.println("Fetching " + e)).limit(20).toArray();

1.6 简单约简

/** 1. count方法
  * 返回流中的元素的数量。
  */
Stream<String> example = Stream.of("merrily", "merrily", "merrily", "gently");
System.out.println(example.count()); // print 4

/** 2. max和min方法
  * 返回流中的元素的最大值和最小值。
  */
Optional<String> largest = words.max(String::compareToIgnoreCase);
System.out.println("largest: " + largest.orElse(""));

/** 3. findFirst方法
  * 返回非空集合中的第一个值。
  * 与filter组合使用时很有用。
  * 找到第一个以字母Q开头的单词。
  */
Optional<String> startsWithQ = words.filter(s -> startsWith("Q")).findFirst();

/** 4. findAny方法
  */
Optional<String> startsWithQ = words.filter(s -> startsWith("Q")).findAny();

/** 5. anyMatch方法
  * 检测是否存在匹配。
  * 接受一个断言引元,
  * 因此不需要使用filter。
  */
boolean aWordStartsWithQ = words.parallel().anyMatch(s -> startsWith("Q"));

/** 6. allMatch和noneMatch方法
  * 分别在所有元素
  * 和没有任何元素匹配谓词
  * 的情况下返回true。
  */

1.7 Optional类型

1.7.1 获取Optional值

// 在没有任何匹配时,使用某种默认值,可能是空字符串。
String result = optionalString.orElse("");
    // The wrapped string, or "" if none

// 调用代码计算默认值
String result = optionalString.orElseGet(() -> System.getProperty("myapp.default"));
    // The function is only called when needed

// 或者在没有任何值时抛出异常
String result = optionalString.orElseThrow(IllegalStateException::new);
    // Supply a method that yield an exception object

1.7.2 消费Optional值

/** 如果可选值存在,
  * 那么它会被传递给该函数。
  * 否则,不会发生任何事情。
  */
optionalValue.ifPresent(v -> Process v);
/** 例如,如果在该值存在的情况下,
  * 要将其添加到某个集中,
  * 那么可以调用:
  */
optionalValue.ifPresent(v -> results.add(v)); // or  optionalValue.ifPresent(results::add);

/** ifPresentOrElse方法
  * 可选值存在时执行一种动作,
  * 可选值不存在时执行另一种动作。
  */
optionalValue.ifPresentOrElse(v -> System.out.println("Found " + v), () -> logger.warning("No match"));

1.7.3 管道化Optional值

// 使用map方法来转换Optional内部的值。
Optional<String> transformed = optionalString.map(String::toUpperCase);
// 如果optionalString为空,则transformed也为空

// 将一个结果添加到列表中
optionalValue.map(results::add);
// 如果optionalValue为空,则什么也不会发生

1.7.4 不适合使用Optional值的方式

1.7.5 创建Optional值

public static Optional<Double> inverse(Double x) {
    return x == 0 ? Optional.empty() : Optional.of(1/x);
}

1.7.6 用flatMap构建Optional值的函数

/** f方法可以产生Optional<T>对象,
  * 目标类型T具有一个可以产生Optional<U>对象的方法g。
  * 则可通过s.f().g()将它们组合起来。
  * 但是这种组合无法工作,
  * 因为s.f()的类型为Optional<T>,而不是T。
  * 故需要使用flatMap方法。
  */
Optional<T> result = s.f().flatMap(T::g);
// 若s.f()的值存在,那么g就可以应用到它上面。否则,就会返回一个空Optional<U>。

1.7.7 将Optional转换为流

/** 有一个用户ID流和
  * 方法Optional<User> lookup(String id)
  */
  
Stream<String> ids = ...;
Stream<User> users = ids.map(Users::lookup).filter(Optional::isPresent).map(Optional::get);

/** 过滤掉无效ID,
  * 然后将get方法应用于剩余的ID。
  * 但是要慎用isPresent和get方法,
  * 故修改代码。
  */

Stream<User> users = ids.map(Users::lookup).flatMap(Optional::stream);

1.8 收集结果

stream.forEach(System.out::println);

在并行流上,forEach方法会以任意顺序遍历各个元素。forEachOrdered方法可以按照流中的顺序来处理,但该方法会丧失并行处理的部分甚至全部优势。

String[] result = stream.toArray(String[]::new);
    // stream.toArray() has type Object[]
// 将流的元素收集到一个列表中
List<String> result = stream.collect(Collectors.toList());

// 将流的元素收集到一个集中
Set<String> result = stream.collect(Collectors.toSet());

// 控制获得的集的种类
TreeSet<String> result = stream.collect(Collectors.toCollection(TreeSet::new));

// 通过连接操作来收集流中所有字符串
String result = stream.collect(Collectors.joining());

// 元素之间增加分隔符
String result = stream.collect(Collectors.joining(", "));

// 将流中非字符串对象转换为字符串
String result = stream.map(Object::toString).collect(Collectors.joining(", "));

1.9 收集到映射表中

/** 将一个流Stream<Person>的元素收集到映射表中,
  * 以便后续通过ID来查找人员。
  */
Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Person::getName));

// 通常情况下,值应该是实际的元素,因此第二个函数可以使用Function.identity()。
Map<Integer, String> idToPerson = people.collect(Collectors.toMap(Person::getId, Function.identity()));

1.10 群组和分区

Map<String, List<Locale>> countryToLocales = locales.collect(Collectors.groupingBy(Locale::getCountry));

/** 函数Locale::getCountry是群组的分类函数,
  * 可以查找给定国家代码对应的所有地点。
  */
List<Locale> swissLocales = countryToLocales.get("CH");
    // Yields locales de_CH, fr_CH, it_CH and maybe more
// 将所有locale分成了使用英语和使用其他英语的两类
Map<Boolean, List<Locale>> englishAndOtherLocales 
    = locales.collect(Collectors.partitioningBy(l -> l.getLanguage.equals("en")));
List<Locale> englishLocales = englishAndOtherLocales.get(true);

1.11 下游收集器

Map<String, Set<Locale>> countryToLocaleSet = locales.collect(groupingBy(Locale::getCountry, toSet()));
/** 1. counting
  *会产生收集到的元素的个数。
  */
Map<String, Long> countryToLocaleCounts = locales.collect(groupingBy(Locale::getCountry, counting()));

/** 2. summing(Int|Long|Double)
  * 会接受一个函数作为引元,
  * 将该函数应用到下游元素中,
  * 并产生它们的和。
  */
Map<String, Integer> stateToCityPopulation 
    = cities.collect(groupingBy(City::getState, summingInt(City::getPopulation)));
    
/** 3. maxBy和minBy
  * 会接受一个比较器,
  * 并分别产生下游元素中的最大值和最小值。
  */
Map<String, Optional<City>> stateToLargestCity 
    = cities.collect(groupingBy(City::getState, maxBy(Comparator.comparing(City::getPopulation))));
    // 可以产生每个州中最大的城市。

1.12 约简操作

List<Integer> values = ...;
Optional<Integer> sum = values.stream().reduce((x, y) -> x + y);
    /** reduce方法会计算v0+v1+v2+...,其中vi是流中的元素。
      * 若流为空,则该方法会返回一个Optional。
      */

1.13 基本类型流

IntStream stream = IntStream.of(1, 1, 2, 3, 5);
stream = Arrays.stream(values, from, to); // values is an int[] array
  1. toArray方法会返回基本类型数组。
  2. 产生可选结果的方法会返回一个OptionalInt、OptionalLong或OptionalDouble。这些类与Optional类类似,但是具有getAsInt、getAsLong和getAsDouble方法,而不是get方法。
  3. 具有分别返回总和、平均值、最大值和最小值的sum、average、max和min方法。对象流没有定义这些方法。
  4. summaryStatistics方法会产生一个类型为IntSummaryStatistics、LongSummaryStatistics或DoubleSummaryStatistics对象,它们可以同时报告流的总和、数量、平均值、最大值和最小值。

1.14 并行流

Stream<String> parallelWords = words.parallerStream();
Stream<String> parallelWords = Stream.of(wordArray).parallel();
  1. 并行化会导致大量的开销,只有面对非常大的数据集才划算。
  2. 只有在底层的数据源可以被有效地分割为多个部分时,将流并行化才有意义。
  3. 并行流使用的线程池可能会因诸如文件I/O或网络访问这样的操作被阻塞而饿死。只有面对海量的内存数据和运算密集处理,并行流才会工作最佳。


第2章 输入与输出

输入/输出流
内存映射文件
读写二进制数据
文件锁机制
对象输入/输出流与序列化
正则表达式
操作文件

2.1 输入/输出流

2.1.1 读写字节

abstract int read()
/** 该方法将读入一个字节
  * 并返回读入的字节
  * 或者在遇到输入源结尾时返回-1
// 从Java 9开始,读取流中所有字节的方法
byte[] bytes = in.readAllBytes();
// 向某个输出位置写出一个字节
abstract void write(int b)
// 一次性写出一个字节数组
byte[] values = ...;
out.write(values);

// transferTo方法可以将所有字节从一个输入流传递到输出流
in.transferTo(out);

2.1.2 完整的流家族

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

var fin = new FileInputStream("employee.dat");
// 这行代码可以查看用户目录下名为“employee.dat”的文件。

由于反斜杠字符在Java字符串中是转义字符,因此要确保在Windows风格的路径名中使用\\。

var fin = new FileInputStream("employee.dat");
var din = new DataInputStream(fin);
double x = din.readDouble();

2.1.4 文本读入与输出

2.1.5 如何写出文本输出

var out = new PrintWriter("employee.txt", StandardCharsets.UTF_8);
String name = "Lam";
double salary = 75000;
out.print(name);
out.print(' ');
out.println(salary);
// employee.txt文件中有 Lam 75000.0

2.1.6 如何读入文本输入

// 最简单 Scanner类
Scanner in = new Scanner(Path.of("inputfile.txt"),StandardCharsets.UTF_8);
String s1=in.nextLine();

// 将短小的文本文件读入到一个字符串中
var content = (Files.read String path, charset);

// 一行一行地读入
List<String> lines = Files.readAllLines(path, charset);

// 较大文件,将行惰性处理为一个Stream<String>对象
try (Stream<String> lines = Files.lines(path, charset)) {
    ...
}

// 使用扫描器来读入符号(token),即由分隔符分隔的字符串,默认的分隔符是空白字符
Scanner in = ...;
in.useDelimiter("\\PL+"); // 将分隔符修改为任意的正则表达式

// 调用next方法可以产生下一个符号
while (in.hasNext()) {
    String word = in.next();
    ...
}

// 获取一个包含所有符号的流
Stream<String> words = in.tokens();
InputStream inputStream = ...;
try (var in = new BufferedReader(new InputStreamReader(inputStream, charset))) {
    String line;
    while ((line = in.readLine()) != null) {
        do something with line
    }
}

2.1.7 以文本格式存储对象

public static void writeEmployee(PrintWriter out, Employee e) {
    out.println(e.getName() + "|" + e.getSalary + "|" + e.getHireDay());
}
//在调用该方法之后 切记使用out.close()关闭文件

// 调用该方法打印到文本文件中
    Harry Hacker|35500|1989-10-01
    Carl Cracker|75000|1987-12-15
    Tony Tester|38000|1990-03-15
/** 每次读入一行,
  * 然后分离所有字段。
  * 用扫描器来读入每一行,
  * 然后用String.split方法将这一行断开成一组标记。
  */
public static Employee readEmployee(Scanner in) {
    String line = in.nextLine();
    String[] tokens = line.split("\\|");
    String name = tokens[0];
    double salary = Double.parseDouble(tokens[1]);
    LocalDate hireDate = LocalDate.parse(tokens[2]);
    int year = hireDate.getYear();
    int month = hireDate.getMonthValue();
    int day = hireDate.getDayOfMonth();
    return new Employee(name, salary, year, month, day);
}
/** split方法的参数是一个描述分隔符的正则表达式,
  * 碰巧的是,
  * 竖线(|)在正则表达式中具有特殊的含义,
  * 因此需要用\字符来表示转义,
  * 而这个\又需要另一个\来转义,
  * 因此产生了“\\|”表达式。
  */

2.1.8 字符编码方式

StandardCharsets.UTF_8
StandardCharsets.UTF_16
StandardCharsets.UTF_16BE
StandardCharsets.UTF_16LE
StandardCharsets.ISO_8859_1
StandardCharsets.US_ASCII

Charset shiftJIS = Charset.forName("Shift-JIS");
var str = new String(bytes, StandardCharsets.UTF_8);
// 将一个字节数组转换为字符串

2.2 读取二进制数据

2.2.1 DataInput和DataOutput接口

  writeInt    writeDouble
  writeShort  writeChar
  writeLong   writeBoolean
  writeFloat  writeUTF
  writeChars  writeByte  
  // 例如,writeInt总是将一个整数写出为4字节的二进制数量值,而不管它有多少位。
  readInt    readDouble
  readShort  readChar
  readLong   readBoolean
  readFloat  readUTF
/** DataInputStream类实现了DataInput接口,
  * 为了从文件中读入二进制数据,
  * 可以将DataInputStream与某个字节源相组合,
  * 例如FileInputStream:
  */
var in = new DataInputStream(new FileInputStream("employee.dat"));

/** 类似地,
  * 要写出二进制数据,
  * 可以使用实现了DataOutput接口的DataOutputStream类:
  */
var out = new DataOutputStream(new FileOutputStream("employee.dat"));

2.2.2 随机访问文件

var in = new RandomAccessFile("employee.dat", "r");
var inOut = new RandomAccessFile("employee.dat", "rw");
// r表示只读(read),rw表示读写(read and write)

2.2.3 ZIP文档

var zin = new ZipInputStream(new FileInputStream(zipname));
ZipEntry entry;
while ((entry = zin.getNextEntry()) != null) {
    read the contents of zin
    zin.closeEntry();
}
zin.close();
var fout = new FileOutputStream("test.zip");
var zout = new ZipOutputStream(fout);
for all files
{
    var ze = new ZipEntry(filename);
    zout.putNextEntry(ze);
    send data to zout
    zout.closeEntry();
}
zout.close();

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

2.3.1 保存和加载序列化对象

// 创建一个ObjectOutputStream对象
var out = new ObjectOutputStream(new FileOutputStream("employee.dat");
// 使用writeObject方法保存对象
var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);

out.writeObject(harry);
out.writeObject(boss);
// 创建一个ObjectInputStream对象
var in = new ObjectInputStream(new FileInputStream("employee.dat"));
// 使用readObject方法获得对象
var e1 = (Employee) in.readObject();
var e2 = (Employee) in.readObject();
  1. 对于遇到的每一个对象引用都关联一个序列号。
  2. 对于每个对象,当第一次遇到时,保存其对象数据到输出流中。
  3. 若某个对象之前已经被保存过,那么只写出“与之前保存过的序列号为x的对象相同”。
  1. 对于对象输入流中的对象,在第一次遇到其序列号时,构建它,并使用流中数据来初始化它,然后记录这个顺序号和新对象之间的关联。
  2. 当遇到“与之前保存过的序列号为x的对象相同”这一标记时,获取与这个序列号相关联的对象引用。

2.4 操作文件

2.4.1 Path

Path absolute = Paths.get("/home", "harry");
Path relative = Paths.get("myprog", "conf", "user.properties");
/** 1. resolve方法
  * 调用p.resolve(q)将按照下列规则返回一个路径:
  * 若q是绝对路径,则结果就是q。
  * 否则,根据文件系统的规则,将“p后面跟着q”作为结果。
  */
Path workRelative = Paths.get("work");
Path workPath = basePath.resolve(workRelative);
  // resolve方法有一种快捷方式,它接受一个字符串而不是路径。
Path workPath = basePath.resolve("work");

/** 2. resolveSibling方法
  * 若workPath是 /opt/myapp/wokr ,
  * 下面的调用,
  * 将创建 /opt/myapp/temp 。
  */
Path tempPath = workPath.resolveSibling("temp");

/** 3. relativize方法
  * 以“/home/harry”为目标对“/home/fred/input.txt”进行相对化操作,
  * 会产生“../fred/input.txt”,
  * 其中,假设..表示文件系统中的父目录。
  **/
  
/** 4. normalize方法
  * 该方法会移除所有冗余的.和.. ,
  * 规范化 /home/harry/../fred/./input.txt ,
  * 将产生 /home/fred/input.txt 。
  */
  
/** 5. toAbsolutePath方法
  * 将产生给定路径的绝对路径。
  */

2.4.2 读写文件

// 读取文件的所有内容
byte[] bytes = Files.readAllBytes(path);

// 从文本文件中读取内容
var content = Files.readString(path, charset);

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

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

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

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

/** 以上简便方法适用于处理中等长度的文本文件,
  * 若要处理文件长度比较大,
  * 或者是二进制文件,
  * 那么还是应该使用输入/输出流或者读入器/写出器:
  */
InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream(paht);

Reader in = Files.newBufferedReader(path, charset);
Writer out = Files.newBufferedWriter(path, charset);

2.4.3 创建文件和目录

// 1. 创建新目录
/** createDirecoty方法
  * F:\\IDEAWORKSPACE\\TestDemo文件夹已经存在 \\newDir不存在
  ***************************************************************
  * String pathstr = "F:\\IDEAWORKSPACE\\TestDemo\\newDir";
  * Path path = Paths.get(pathstr);
  */
Files.createDirecoty(path);

/** createDirecoties方法
  * F:\\IDEAWORKSPACE\\TestDemo路径已经存在 \\midDir\\newDir不存在
  ***************************************************************
  * String pathstr = "F:\\IDEAWORKSPACE\\TestDemo\\midDir\\newDir";
  * Path path = Paths.get(pathstr);
  */
Files.createDirecoties(path);  

// 2. 创建文件
/** createFile方法
  * F:\\IDEAWORKSPACE\\TestDemo路径已经存在 myfile.txt不存在
  ***************************************************************
  * String pathstr = "F:\\IDEAWORKSPACE\\TestDemo\\myfile.txt";
  * Path path = Paths.get(pathstr);
  */
Files.createFile(path);
// 会在指定路径下创建myfile.txt空文件
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")
  * 可能会返回一个像 /temp/1234405522364837194.txt 这样的路径。
  */

2.4.4 复制、移动和删除文件

// 1. 复制
Files.copy(fromPath, toPath);

// 2. 移动
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);

/** 将一个输入流复制到Path中,
  * 表示想要将该输入流存储到硬盘上。
  * 类似地,可以将一个Path复制到输出流中。
  */
Files.copy(inputStream, toPath);
Files.copy(fromPath, outputStream);

// 3. 删除
Files.delete(path); // 文件不存在会报错
// 使用deleteIfExists方法
boolean deleted = Files.deleteIfExists(path);
选项 描述
StandardOpenOption 与newBufferedWriter、newInputStream、newOutputStream、write一起使用
READ 用于读取而打开
WRITE 用于写入而打开
APPEND 如果用于写入而打开,那么在文件末尾追加
TRUNCATE_EXISTING 如果用于写入而打开,那么移除已有内容
CREATE_NEW 创建新文件并且在文件已存在的情况下会创建失败
CREAT 自动在文件不存在的情况下创建新文件
DELETE_ON_CLOSE 当文件被关闭时,尽“可能”地删除该文件
SPARSE 给文件系统一个提示,表示该文件是稀疏的
DSYNC或SYNC 要求对文件数据|数据和元数据的每次更新都必须同步地写入到存储设备中
StandardCopyOption 与copy和move一起使用
ATOMIC_MOVE 原子性地移动文件
COPY_ATTRIBUTES 复制文件的属性
REPLACE_EXISTING 如果目标已存在,则替换它
LinkOption 与上面所有方法以及exists、isDirectory、isRegularFile等一起使用
NOFOLLOW_LINKS 不要跟踪符号链接
FileVisitOption 与find、walk、walkFileTree一起使用
FOLLOW_LINKS 跟踪符号链接

2.4.5 获取文件信息

/** 1.
  * 以下静态方法都将返回一个boolean值,
  * 表示检查路径的某个属性的结果。
  */
exists
isHidden
isReadablleisWritableisExecutable
isRegularFileisDirectoryisSymbolicLink

/** 2. size方法
  * 返回文件的字节数。
  */
long fileSize = Files.size(path);

/** 3. getOwner方法
  * 将文件拥有者作为java.nio.file.attribute.UserPrincipal的一个实例返回。
  */
  1. 创建文件、最后一次访问以及最后一次修改文件的时间,这些时间都表示成java.nio.file.attribute.FileTime。
  2. 文件是常规文件、目录还是符号链接,抑或这三者都不是。
  3. 文件尺寸。
  4. 文件主键,这是某种类的对象,具体所属类与文件系统相关,有可能是文件的唯一标识符,也可能不是。
// 获取这些属性,可以调用
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
// 若文件系统兼容POSIX,那么可以获得一个PosixFileAttributes实例
PosixFileAttributes attributes = Files.readAttributes(path, PosixFileAttributes.class);

2.4.6 访问目录中的项

/** 静态的Files.list方法
  * 返回一个可以读取目录中各个项的Stream<Path>对象
  */
try (Stream<Path> entries = Files.list(pathToDirectory)) {
    ...
}

// list方法不会进入子目录。

/** Files.walk方法
  * 处理目录中的所有子目录
  */
try (Stream<Path> entries = Files.walk(pathToRoot)) {
    // Contains all descendants, visited in depth-first order
}

2.4.7 使用目录流

try (DirectoryStream<Path> entries = Files.newDirectorySystem(dir)) {
    for (Path entry : entries)
        Process entries
}

// 可以用glob模式来过滤文件
try (DirectoryStream<Path> entries = Files.newDirectorySystem(dir, "*.java"))
模式 描述 示例
* 匹配路径组成部分中0个或多个字符 *.java匹配当前目录中所有Java文件
** 匹配跨目录边界的0个或多个字符 **.java匹配在所有子目录中的Java文件
? 匹配一个字符 ????.java匹配所有四个字符的Java文件
[...] 匹配一个字符集合,可以使用连线符[0-9]和取反符[!0-9] Test[0-9A-F]匹配Testx.java,其中x是一个十六进制数字
{...} 匹配由都好隔开的多个可选项之一 *.{java,class}匹配所有的Java文件和Class文件
\ 转义上述任意模式中的字符以及\字符 *\**匹配所有文件名中包含*的文件

2.4.8 ZIP文件系统

FileSystem fs = FileSystems.newFileSystem(Paths.get(zipname), null);
/** 以上调用将建立一个文件系统,
  * 它包含ZIP文档中的所有文件。
  * 其中zipname是某个ZIP文件的名字。
  */
// 已知文件名,从ZIP文档中复制出这个文件
Files.copy(fs.getPath(sourceName), targetPath);
// 其中的fs.getPath对于任意文件系统来说都与Paths.get类似

2.5 内存映射文件

2.5.1 内存映射文件的性能

/** 首先,从文件中获得一个通道(channel),
  * 通道是用于磁盘文件的一种抽象,
  * 可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。
  */
FileChannel channel = FileChannel.open(path, options);

/** 然后,通过调用FileChannel类的map方法从这个通道中获得一个ByteBuffer。
  * 可以指定映射的文件区域和映射模式,
  * 支持的模式有三种:
  * 1. FileChannel.MapMode.READ_ONLY:所产生的缓冲区是只读的,任何对该缓冲区写入的尝试都会导致ReadOnlyBufferException异常。
  * 2. FileChannel.MapMode.READ_WRITE:所产生的缓冲区是可写的,任何修改都会在某个时段写回到文件中。
  * 3. FileChannel.MapMode.PRIVATE:所产生的缓冲区是可写的,但是任何修改对这个缓冲区来说都是私有的,不会传播到文件中。
  * 一旦有了缓冲区,就可以使用ByteBuffer类和Buffer超类的方法读写数据了。
  */
// 顺序遍历缓冲区中的所有字节
while (buffer.hasRemaining()) {
    byte b = buffer.get();
    ...
}

// 随机访问
for (int i = 0; i < buffer.limit(); i++) {
    byte b = buffer.get(i);
    ...
}

2.5.2 缓冲区数据结构

  1. 一个容量,它永远不能改变。
  2. 一个读写位置,下一个值将在此进行读写。
  3. 一个界限,超过它进行读写是没有意义的。
  4. 一个可选的标记,用于重复一个读入或写出操作。
    这些值满足:0 ≤ 标记 ≤ 读写位置 ≤ 界限 ≤ 容量

image

2.6 文件加锁机制

假设一个应用程序将用户的偏好存储在一个配置文件中,当用户调用这个应用的两个实例时,这两个实例就有可能会同时希望写配置文件。在这种情况下,第一个实例应该锁定文件,当第二个实例发现文件被锁定时,它必须决策是等待直至文件解锁,还是直接跳过这个写操作过程。

/** 1. 锁定文件
  * 可以调用FileChannel类的lock或tryLock方法
  */
// A. 阻塞直至可获得锁
FileChannel = FileChannel.open(path);
FileLock lock = channel.lock();

// B. 立即返回  要么返回锁 要么在锁不可获得的情况下返回null
FileLock lock = channel.lock();

/** 2. 锁定文件的一部分
  */
FileLock lock(long start, long size, boolean shared);
// or 
FileLock trylock(long start, long size, boolean shared);
/** 如果shared标志为false,
  * 则锁定文件的目的是读写。
  * 而如果为true,
  * 则这是一个共享锁,
  * 允许多个进程从文件中读入,
  * 并阻止任何进程获得独占的锁。
  */
  1. 在某些系统中,文件加锁仅仅是建议性的,如果一个应用未能得到锁,它仍旧可以向被另一个应用并发锁定的文件执行写操作。
  2. 在某些系统中,不能在锁定一个文件的同时将其映射到内存中。
  3. 文件锁是由整个Java虚拟机持有的。如果有两个程序是由同一个虚拟机启动的,那么它们不可能每一个都获得一个在同一个文件上的锁。当调用lock和trylock方法时,如果虚拟机已经在同一个文件上持有了另一个重叠的锁,那么这两个方法将抛出OverlappingFileLockException。
  4. 在一些系统中,关闭一个通道会释放由Java虚拟机持有的底层文件上的所有锁。因此,在同一个锁定文件上应避免使用多个通道。
  5. 在网络文件系统上锁定文件是高度依赖于系统的,因此应该尽量避免。

2.7 正则表达式

2.7.1 正则表达式语法

  1. 字符类是一个括在括号中的可选择的字符集。例如,[Jj]、[0-9]、[A-Za-z]或[^0-9]。这里“-”表示是一个范围,而“^”表示补集。
  2. 如果字符类中包含“-”,那么它必须是第一项或是最后一项;如果要包含“[”,那么它必须是第一项;如果要包含“^”,那么它可以是除开始位置之外的任何位置。其中,只需要转义“[”和“\”。
  3. 有许多预定义的字符类,例如\d(数字)和\p{Sc}(Unicode货币符号)。
  1. 大部分字符都可以与它们自身匹配。
  2. .符号可以匹配任何字符(有可能不包括行终止符,这取决于标志的设置)。
  3. 使用\作为转义字符,例如\.匹配句号而\\匹配反斜线。
  4. ^和$分别匹配一行的开头和结尾。
  5. 如果X和Y是正则表达式,那么XY表示“任何X的匹配后面跟随Y的匹配”,X|Y表示“任何X或Y的匹配”。
  6. 可以将量词运用到表达式X:X+(1个或多个)、X*(0个或多个)与X?(0个或1个)。
  7. 默认情况下,量词要匹配能够使整个匹配成功的最大可能的重复次数。
  8. 我们使用群组来定义子表达式,其中群组用括号()括起来。

2.7.2 匹配字符串

/** 首先用表示正则表达式的字符串构建一个Pattern对象。
  * 然后从这个模式中获得一个Matcher,
  * 并调用它的mathes方法。
  */
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(input);
if (matcher.matchers()) ...
Pattern pattern = Pattern.compile(expression, Pattern.CASE_INSENSITIVE + Pattern.UNICODE_CASE);
// 或者可以在模式中指定它们
String regex = "(?iU:expression)";
  1. Pattern.CASE_INSENSITIVE或i:匹配字符时忽略字母的大小写,默认情况下,这个标志只考虑US ASCII字符。
  2. Pattern.UNICODE_CASE或u:当与CASE_INSENSITIVE组合使用时,用Unicode字母的大小写来匹配。
  3. Pattern.UNICODE_CHARACTER_CLASS或U:选择Unicode字符类代替POSIX,其中蕴含了UNICODE_CASE。
  4. Pattern.MULTILINE或m:^和$匹配行的开头和结尾,而不是整个输入的开头和结尾。
  5. Pattern.UNIX_LINES或d:在多行模式中匹配^和$时,只有’\n’被识别成行终止符。
  6. Pattern.DOTALL或s:当使用这个标志时,.符号匹配所有字符,包括行终止符。
  7. Pattern.COMMENTS或x:空白字符和注释(从#到行末尾)将被忽略。
  8. Pattern.LITERAL:该模式将被逐字地采纳,必须精确匹配,因字母大小写而造成的差异除外。
  9. Pattern.CANON_EQ:考虑Unicode字符规范的等价性,例如,e后面跟随..(分音符号)匹配ë。
Stream<String> strings = ...;
Stream<String> result = strings.filtter(pattern.asPredicate());

2.7.3 找出多个匹配

/** 使用Matcher类的find方法来查找匹配内容,
  * 如果返回true,
  * 再使用start和end方法来查找匹配内容,
  * 或使用不带引元的group方法来获取匹配的字符串。
  */
while(matcher.find()) {
    int start = matcher.start();
    int end = matcher.end();
    String match = input.group();
    ...
}

2.7.4 用分隔符来分割

/** Pattern.split方法可以自动完成这项任务。
  * 调用此方法后可以获得一个剔除分隔符之后的字符串数组:
  */
String input = ...;
Pattern commas = Pattern.compile("\\s*,\\s*");
String[] tokens = commas.split(input)
    // "1, 2, 3" turns into ["1", "2", "3"]
    
// 若有多个标记,那么可以惰性地获取它们:
Stream<String> tokens = commas.splitAsStream(input);

// 若不关心预编译模式和惰性获取,那么可以使用String.split方法:
String[] tokens = input.split("\\s*,\\s*");

// 若输入数据在文件中,那么要使用扫描器:
var in = new Scanner(path, StandardCharsets.UTF_8);
in.useDelimiter("\\s*,\\s*");
Stream<String> tokens = in.tokens();

2.7.5 替换匹配

/** 例如,
  * 将所有的数字序列都替换成#字符
  */
Pattern pattern = Pattern.compile("[0-9]+");
Matcher matcher = pattern.matcher(input);
String output = matcher.replaceAll("#");

第3章 XML

XML概述
使用命名空间
XML文档的结构
流机制解析器
解析XML文档
生成XML文档
验证XML文档
XSL转换
使用XPath来定位信息

3.1 XML概述

  1. 与HTML不同,XML是大小写敏感的。
  2. 在HTML中,如果上下文可以分清哪里是段落或列表项的结尾,那么结束标签就可以省略,而在XML中结束标签绝对不能省略。
  3. 在XML中,只有单个标签而没有相对应的结束标签的元素必须以 / 结尾。
  4. 在XML中,属性值必须用引号括起来。在HTML中,引号是可有可无的。
  5. 在HTML中,属性名可以没有值。

3.2 XML文件的结构

<?xml version="1.0"?>
<!- 或者 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC
    "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
    "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
  1. 字符引用(character reference)的形式是 &#十进制; 或 &#x十六进制; 。
  2. 实体引用(entity reference)的形式是 &name; 。
  3. CDATA部分(CDATA Section)用<![CDATA[ 和 ]]>来限定其界限。它们是字符数据的一种特殊形式。可以使用它们来囊括那些含有<、>、&之类字符的字符串,而不必将它们解释为标记,例如:<![CDATA[< & > are my favorite delimities]> 。
  4. 处理指令(processing instruction)是那些专门在处理XML文档的应用程序中使用的指令,它们由<?和?>来限定其界限。
  5. 注释(comment)用<!- 和 –>限定其界限,例如: 。注释中不应该含有字符串–。

3.3 解析XML文档

  1. 像文档对象模型(Document Object Model, DOM)解析器这样的树型解析器(tree parse),它们将读入的XML文档转换成树结构。
  2. 像XML简单API(Simple API for XML, SAX)解析器这样的流机制解析器(streaming parser),它们在读入XML文档时生成相应的事件。
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();

// 现在,可以从文件中读入某个文档:
File f = ...;
Document doc = builder.parse(f);

// 或者,可以用一个URL:
URL u = ...;
Document doc = builder.parse(u);

// 甚至可以指定任意的输入流:
InputStream in = ...;
Document doc = builder.parse(in);

3.4 验证XML文档

3.4.1 文档类型定义

<!DOCTYPE config SYSTEM "config.dtd">
<!- 或者 -->
<!DOCTYPE config SYSTEM "http://myserver.com/config.dtd">

3.4.2 XML Schema

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="config.xsd";
 ...
</config>

3.5 使用XPath来定位信息

  1. 获得文档根节点。
  2. 获取第一个子节点,并将其转型为一个Element对象。
  3. 在其所有子节点中定位title元素。
  4. 获取其第一个子元素,并将其转型为一个CharacterData节点。
  5. 获得其数据。

3.6 使用命名空间


第4章 网络

连接到服务器
HTTP客户端
实现服务器
发送E-mail
获取Web数据

4.1 连接到服务器

4.1.1 使用telnet

在Windows中,需要激活telnet。要激活它,需要到“控制面板”,选择“程序”,点击“打开/关闭Windows特性”,然后选择“Telnet客户端”复选框。

4.1.2 用Java连接到服务器

import java.io.IOException;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class SocketTest {
    public static void main(String[] args) throws IOException {
        try (var s = new Socket("time-a.nist.gov", 13);
                var in = new Scanner(s.getInputStream(), StandardCharsets.UTF_8))
        {
            while(in.hasNextLine()) {
                String line = in.nextLine();
                System.out.println(line);
            }
        }
    }
}
var s = new Socket("time-a.nist.gov", 13);
InputStream inStream = s.getInputStream();
/** 第一行代码用于打开一个套接字,
  * 它是网络软件中的一个抽象概念,
  * 负责启动该程序内部和外部之间的通信。
  */

4.1.3 套接字超时

var s = new Socket(...);
s.setSoTimeout(10000); // time out after 10 seconds

4.1.4 因特网地址

/** 1. 静态的getByName方法
  * 将返回一个InetAdress对象,
  * 该对象封装了一个4字节的序列:14.215.177.38
  * (14.215.177.38为百度的IP地址)
  */
InetAddress address = InetAddress.getByNmae("www.baidu.com");

/** 2. getAddress方法
  * 访问这些字节。
  */
byte[] addressBytes = address.getAddress();

/** 3. 一些访问量较大的主机名通常会对应多个因特网地址,
  * 以实现负载均衡。
  * 当访问主机时,
  * 会随机选取几种的一个。
  * getAllByName方法
  * 获得所有主机。
InetAddress[] addresses = InetAddress.getAllByName(host);

/** 4. 静态的getLocalHost方法
  * 得到本地主机的地址。
  */
InetAddress address = InetAddress.getLocalHost();

4.2 实现服务器

4.2.1 服务器套接字

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class EchoServer {
    public static void main(String[] args) throws IOException {
        // establish server socket
        try (var s = new ServerSocket(8189)) {
            // wait for client connection
            try (Socket incoming = s.accept()) {
                InputStream inStream = incoming.getInputStream();
                OutputStream outStream = incoming.getOutputStream();

                try (var in = new Scanner(inStream, StandardCharsets.UTF_8)) {
                    var out = new PrintWriter(new OutputStreamWriter(outStream, StandardCharsets.UTF_8), true);

                    out.println("Hello! Enter BYE to exit");

                    // echo client input
                    var done = false;
                    while (!done&&in.hasNextLine()){
                        String line = in.nextLine();
                        out.println("Echo: " + line);
                        if (line.trim().equals("BYE"))
                            done = true;
                    }
                }
            }
        }
    }
}

4.2.2 为多个客户端服务

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class ThreadedEchoServer {
    public static void main(String[] args) throws IOException {
        try (var s = new ServerSocket(8189)) {
            int i = 1;

            while (true) {
                Socket incoming = s.accept();
                System.out.println("Spawning " + i);
                Runnable r = new ThreadedEchoHandler(incoming);
                var t = new Thread(r);
                t.start();
                i++;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class ThreadedEchoHandler implements Runnable {
    private Socket incoming;

    public ThreadedEchoHandler(Socket incoming) {
        this.incoming = incoming;
    }

    @Override
    public void run() {
        try (InputStream inStream = incoming.getInputStream();
             OutputStream outStream = incoming.getOutputStream();
             var in = new Scanner(inStream, StandardCharsets.UTF_8);
             var out = new PrintWriter(new OutputStreamWriter(outStream, StandardCharsets.UTF_8), true)) {
            out.println("Hello! Enter BYE to exit.");

            // echo client input
            var done = false;
            while (!done && in.hasNextLine()) {
                String line = in.nextLine();
                out.println("Echo: " + line);
                if (line.trim().equals("BYE"))
                    done = true;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.2.3 半关闭

try (var socket = new Socket(host, port)) {
    var in = new Scanner(socket.getInputStream(), StandardCharsets.UTF_8);
    var writer = new PrintWriter(socket.getOutputStream());
    // send request data
    writer.print(...);
    writer.flush();
    socket.shutdownOutput();
    // now socket is half-closed
    // read response data
    while (in.hasNextLine() != null) {
        String line = in.nextLine();
        ...
    }
}

4.2.4 可中断套接字

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

/** 若不想处理缓冲区,可以使用Scanner类从SocketChannel中读取信息,
  * 因为Scanner有一个带ReadableByteChannel参数的构造器:
  */
var in = new Scanner(channel, StandardCharsets.UTF_8);

/** 通过调用静态方法Channels.newOutputStream,
  * 可以将通道转换成输出流。
  */
OutputStream outStream = Channels.newOutputStream(channel);

4.3 获取Web数据

4.3.1 URL和URI

/** 自字符串构建一个URL对象:
  */
var url = new URL(urlString);

/** URL类中的openStream方法
  * 该方法产生一个InputStream对象,
  * 如可以用它构建一个Scanner对象。
  */
InputStream inStream = url.openStream();
var in = new Scanner(inStream, StandardCharsets.UTF_8);

URI是个纯粹的语法结构,包含用来指定Web资源的字符串的各种组成部分。URL是URI的一个特例,它包含了用于定位Web资源的足够信息。

4.3.2 使用URLConnection获取信息

/** 1. 调用URL类中的openConnection方法获得URLConnection对象:
  */
URLConnection connection = url.openConnection();

/** 2. 使用以下方法来设置任意的请求属性:
  */
setDoInput
setDoOutput
setIfModifiedSince
setUseCaches
setAllowUserInteraction
setRequestProperty
setConnectTimeout
setReadTimeout

/** 3. 调用connect方法连接远程资源:
  * 除了与服务器建立套接字连接外,
  * 该方法还可用于向服务器查询头信息(header information)。
  */
connection.connect();

/** 4. 与服务器建立连接后,
  * 可以查询头信息。
  * getHeaderFiledKey和getHeaderField这两个方法枚举了消息头的所有字段。
  */
getContentType
getContentLength
getContentEncoding
getDate
getExpiration
getLastModified

/** 5. 最后,访问资源数据。
  */

4.3.3 提交表单数据

image

/** 1. 在提交数据给服务器端程序之前,首先需要创建一个URLConnection对象。
  */
var url = new URL("http://host/path");
URLConnection connection = url.openConnection();

/** 2. 然后调用setDoOutput方法建立一个用于输出的连接。
  */
connection.setDoOutput(true);

/** 3. 接着调用getOutputStream方法获得一个流,
  * 可以通过这个流向服务器发送数据。
  * 如果要向服务器发送文本信息,
  * 那么可以非常方便地将流包装在PrintWriter对象中。
  */
var out = new PrintWriter(connection.getOutputStream(), StandardCharsets.UTF_8);

/** 4. 现在,可以向服务器发送数据了。
out.print(name1 + "=" + URLEncoder.encode(value1, StandardCharsets.UTF_8) + "&");
out.print(name2 + "=" + URLEncoder.encode(value2, StandardCharsets.UTF_8);

/** 5. 之后关闭输出流。
  */
out.close();

/** 6. 最后,调用getInputStream方法读取服务器的响应。
  */

4.4 HTTP客户端

/** HttpClient对象可以发出请求并接收响应。
  * 可以通过下面的调用获取客户端:
  */
HttpClient client = HttpClient.newHttpClient();

/** 或者,如果需要配置客户端,
  * 可以使用像下面这样的构建器API:
  */
HttpClient client = HttpClient.newBuilder()
            .followRedirects(HttpClient.Redirect.ALWAYS)
            .build();

4.5 发送E-mail


第5章 数据库编程

JDBC的设计 可滚动和可更新的结果集
结构化查询语言
行集
JDBC配置
元数据
使用JDBC语句
事务
执行查询操作
Web和企业应用中的连接管理

5.1 JDBC的设计

5.1.1 JDBC驱动程序类型

  1. 第1类驱动程序将JDBC翻译成ODBC,然后使用ODBC驱动程序与数据库进行通信。
  2. 第2类驱动程序是由部分Java程序和部分本地代码组成的,用于与数据库的客户端API进行通信。
  3. 第3类驱动程序是纯Java客户端类库,它使用一种与具体数据库无关的协议将数据库请求发送给服务器构件,然后该构件再将数据库请求翻译成数据库相关的协议。
  4. 第4类驱动程序是纯Java类库,它将JDBC请求直接翻译成数据库相关的协议。
  1. 通过使用标准的SQL语句,甚至是专门的SQL拓展,程序员就可以利用Java语言开发访问数据库的应用,同时还依旧遵守Java语言的相关规定。
  2. 数据库供应商和数据库工具开发商可以提供底层的驱动程序。因此,他们可以优化各自数据库产品的驱动程序。

5.1.2 JDBC的典型用法

image

5.2 结构化查询语言

这条语句将排除所有书名中包含UNIX或者Linux的图书:

SELECT ISBN, Price, Title
FROM Books
WHERE Title NOT LIKE '%n_x%'

这条语句会返回所有包含单引号的书名:

SELECT Title
FROM Books
WHERE Title LIKE '%''%'
数据类型 说明
INTEGER或INT 通常为32位的整数
SMALLINT 通常位16位的整数
NUMERIC(m,n), DECIMAL(m,n)或DEC(m,n) m位长的定点十进制数,其中小数点后为n位
FLOAT(n) 运算精度为n位二进制数的浮点数
REAL 通常为32位浮点数
DOUBLE 通常为64位浮点数
CHARACTER(n)或CHAR(n) 固定长度为n的字符串
VARCHAR(n) 最大长度为n的可变长字符串
BOOLEAN 布尔值
DATE 日历日期(与具体的实现相关)
TIME 当前时间(与具体的实现相关)
TIMESTAMP 当前日期和时间(与具体的实现相关)
BLOB 二进制大对象
CLOB 字符大对象

5.3 使用JDBC语句

5.3.1 执行SQL语句

// 将要执行的SQL语句放入字符串中
String command = "UPDATE Books" 
    + " SET Price = Price - 5.00 "
    + " WHERE Title NOT LIKE '%Introduction%'";
    
// 调用Statement接口中的executeUpdate方法
stat.executeUpdate(conmmand);
/** executeQuery方法会返回一个ResultSet类型的对象,
  * 可以通过它来每次一行地迭代遍历所有查询结果。
  */
ResultSet rs = stat.executeQuery("SELECT * FROM Books");

/** 分析结果集时,
  * 通常可以使用类似如下的循环语句代码:
  */
while (rs.next()) {
    look at a row of the result set
}

/** 结果集中行的顺序是任意排列的。
  * 可以使用ORDER BY子句指定行的顺序。
  */
  
/** 查看每一行时,
  * 可能希望知道每一列的内容,
  * 可以使用访问器。
  */
String isbn = rs.getString(1);
double price = rs.getDouble("Price");
// 与数组的索引不同,数据库的序列号是从1开始计算的。

5.3.2 管理连接、语句和结果集

5.4 执行查询操作

5.4.1 预备语句

String publisherQuery
    = "SELECT Books.Price, Books"
    + "FROM Books, Publishers"
    + "WHERE Books.Publisher_Id = Publishers.Publisher_Id AND Publisher.Name = ?";
PreparedStatement stat = conn.prepareStatement(publisherQuery);

/** 在执行预备语句之前,
  * 必须使用set方法将变量绑定到实际的值上。
  * 第一个参数指的是需要设置的宿主变量的位置,
  * 位置1表示第一个“ ? ”,
  * 第二个参数指的是赋予宿主变量的值。
  */
stat.setString(1, publisher);

5.4.2 读写LOB

/** 例如,有一张保存图书封面图像的表,
  * 那么,就可以像下面这样获取一种图像。
  */
PreparedStatement stat = conn.prepareStatement("SELECT Cover FROM BookCovers WHERE ISBN=?");
...
stat.set(1, isbn);
try (ResultSet result = stat.executeQuery()) {
    if (result.next()) {
        Blob coverBlob = result.getBlob(1);
        Image coverImage = ImageIO.read(coverBlob.getBinaryStream());
    }
}
/** 例如,存入一张图像。
  */
Blob coverBlob = connection.createBlob();
int offset = 0;
OutputStream out = coverBlob.setBinaryStream(offset);
ImageIO.write(coverImage, "PNG", out);
PreparedStatement stat = conn.prepareStatement("INSERT INTO Cover VALUES (?,?)");
stat.set(1, isbn);
stat.set(2, coverBlob);
stat.executeUpdate();

5.4.3 SQL转义

日期和时间字面常量
调用标量函数
调用存储过程
外连接
在LIKE子句中的转义字符

{d '2008-01-24'}
{t '23:59:59'}
{ts '2008-01-24 23:59:59.999'}
{fn left(?, 20)}
{fn user()}
{call PR0C1(?,?)}
{call PR0C2}
{call ? = PR0C3(?)}
...WHERE ? LIKE %!_% {escape '!'}

5.4.4 多结果集

  1. 使用execute方法来执行SQL语句。
  2. 获取第一个结果集或更新计数。
  3. 重复调用getMoreResults方法以移动到下一个结果集。
  4. 当不存在更多的结果集或更新计数时,完成操作。
boolean isResult = stat.execute(command);
boolean done = false;
while (!done) {
    if (isResult) {
        ResultSet result = stat.getResultSet();
        do something with result
    }
    else {
    int updateCount = stat.getUpdateCount();
    if (updateCount >= 0)
        do something with updateCount
    else
        done = true;
    }
    if (!done) isResult = stat.getMoreResults();
}

5.4.5 获取自动生成的键

stat.executeUpdate(insertStatement, Statement.RETURN_GENERATED_KEYS);
ResultSet rs = stat.getGeneratedKeys();
if (rs.next()) {
    int key = rs.getInt(1);
    ...
}

5.5 事务

5.5.1 用JDBC对事务编程

conn.setAutoCommit(false);

/** 按照通常的方式创建一个语句对象:
  */
Statement stat = conn.createStatement();

/** 然后任意多次地调用executeUpdate方法:
  */
stat.executeUpdate(command1);
stat.executeUpdate(command2);
stat.executeUpdate(command3);
...

/** 如果执行了所有命令之后没有出错,
  * 则调用commit方法:
  */
conn.commit();

/** 如果出现错误,则调用:
  */
conn.rollback();

5.5.2 保存点

Statement stat = conn.createStatement(); // start transaction; rollback() goes here
stat.executeUpdate(command1);
SavePoint svpt = conn.setSavepoint(); // set savepoint; rollback() goes here
stat.executeUpdate(command2);

if (...) conn.rollback(svpt); // undo effect of command2
...
conn.commit();

// 当不再需要保存点时,应该释放它:
conn.releaseSavepoint(svpt);

5.5.3 批量更新

/** 为了执行批量处理,
  * 首先必须使用通常的办法创建一个Statement对象:
  */
Statement stat = conn.createStatement();

/** 现在,应该调用addBatch方法,
  * 而非executeUpdate方法:
  */
String command = "CREATE TABLE ..."
stat.addBatch(command);

while (...) {
    command = "INSERT INTO ... VALUES (" + ... + ")";
    stat.addBatch(command);
}

/** 最后,提交整个批量更新语句:
  */ 
int[] counts = stat.executeBatch();

5.5.4 高级SQL类型

SQL数据类型 Java数据类型
INTEGER或INT int
SMALLINT short
NUMERIC(m,n), DECIMAL(m,n)或DEC(m,n) java.math.BigDecimal
FLOAT(n) double
REAL float
DOUBLE double
CHARACTER(n)或CHAR(n) String
VARCHAR(n) String
BOOLEAN boolean
DATE java.sql.Date
TIME java.sql.Time
TIMESTAMP java.sql.Timestamp
BLOB java.sql.Blob
CLOB java.sql.Clob
ARRAY java.sql.Array
ROWID java.sql.RowId
NCHAR(n), NVARCHAR(n), LONG NVARCHAR String
NCLOB java.sql.NClob
SQLXML java.sql.SQLXML

第6章 日期和时间API

时间线
时区时间
本地日期
格式化和解析
日期调整器
与遗留代码的互操作
本地时间

6.1 时间线

  1. 每天86 400秒
  2. 每天正午与官方时间精确匹配
  3. 在其他时间点上,以精确定义的方式与官方时间接近匹配
/** 静态方法调用Instant.now()会给出当前的时刻。
  * 为了得到两个时刻之间的时间差,
  * 可以使用静态方法Duration.between。
  * 例如,以下代码可以度量算法运行的时间:
  */
Instant start = Instant.now();
runAlgorithm();
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
long millis = timeElapsed.toMillis();

/** Duration是两个时刻之间的时间量。
  * 可以调用toNanos、toMillis、getSeconds、toMinutes、toHour和toDay
  * 来获得Duration按照传统单位度量的时间长度。
  * tips: 在Java 8中,必须调用getSeconds而不是toSeconds。
  */
/** Duration接口包含了大量的用于执行算术运算的方法。 
  * 例如,
  * 检测某个算法是否至少比另一个算法快10倍。
  */
Duration timeElapsed2 = Duration.between(start2, end2);
boolean overTenTimesFaster
    = timeElapsed.multipliedBy(10).minus(timeElapsed2).isNegative();

/** 以上代码仅为展示语法。
  * 实际上可以直接调用:
  */
boolean overTenTimesFaster = timeElapsed.toNanos() * 10 < timeElapsed2.toNanos();

6.2 本地日期

1903年6月14日是一个本地日期的示例(lambda演算的发明者Alonzo Church在这一天诞生)。
1969年7月16日 09:32:00 EDT(阿波罗11号发射的时刻)是一个时区日期/时间,表示的是时间线上的一个精确的时刻。

LocalDate today = LocalDate.now(); // Today's date

LocalDate alonzosBirthday = LocalDate.of(1903, 6, 14);
alonzosBirthday = LocalDate.of(1903, Month.JUNE, 14);
    // Uses the Month enumeration
birthday.plus(Period.ofYears(1)); // 获得下一年的生日
birthday.plusYears(1); // 获得下一年的生日

birthday.plus(Duration.ofDay(365)); // 在闰年不会产生正确的结果
/** until方法会产生两个本地日期之间的时长。
  */
  
LocalDate independenceDay = LocalDate.of(2020, 07, 20);
LocalDate christmas = LocalDate.of(2020, 12, 25);

independenceDay.until(christmas); // 会产生5个月5天的一段时长

independenceDay.unitl(christmas, ChronoUnit.DAYS) // 158 days

LocalDate API中有些方法可能会创造出并不存在的日期。这些方法并不会抛出异常,而是会返回该月有效的最后一天。 例如,
LocalDate.of(2016, 1, 31).plusMonths(1);

LocalDate.of(2016, 3, 31).minusMonths(1);
都将产生2016年2月29日。

周末实际上在每周的末尾。这与java.util.Calendar有所差异,在后者中,星期日的值为1,而星期6的值为7。

LocalDate start = LocalDate.of(2020, 1, 1);
LocalDate endExclusive = LocalDate.now();
Stream<LocalDate> allDays = start.datesUntil(endExclusive);
Stream<LocalDate> firstDaysInMonth = start.datesUntil(endExclusive, Perod.ofMonths(1));

6.3 日期调整器

/** 可以将调整方法的结构传递给with方法。
  * 例如,
  * 某个月的第一个星期二:
  */
LocalDate firstTuesday = LocalDate.of(year, month, 1).with(
    TemporalAdjusters.nextOrSame(DaysOfWeek.TUESDAY));
TemporalAdjuster NEXT_WORKDAY = w -> {
    var result = (LocalDate) w;
    do {
        result = result.plusDays(1);
    } while (result.getDayOfWeek().getValue() >= 6);
    return result;
};

LocalDate backToWork = today.with(NEXT_WORKDAY);

/** lambda表达式的参数类型为Temporal,
  * 它必须被强制转型为LocalDate。
  */
TemporalAdjuster NEXT_WORKDAY = TemporalAdjusters.ofDateAdjuster(w -> 
 {
     LocalDate result = w; // No cast
     do {
         result = result.plusDays(1);
     } while (result.getDayOfWeek().getValue >= 6);
     return result;
 });

6.4 本地时间

LocalTime rightNow = LocalTime.now();
LocalTime bedtime = LocalTime.of(22, 30); // or LocalTime.of(22, 30, 0);

/** plus和minus操作是按照一天24小时循环操作的。
  */
LocalTime wakeup = bedtime.plusHours(8); // wakeup is 6:30:00

6.5 时区时间

/** 给定一个时区ID,
  * 1. 静态方法ZoneId.of(id)可以产生一个ZoneId对象。
  * 2. 调用local.atZone(zoneId)将LocalDateTime对象转换为ZonedDateTime对象,
  * 3. 调用静态方法
  * ZonedDateTime.of(year, month, day, hour, minute, second, nano, zoneId)
  * 来构造一个ZonedDateTime对象。
  */
ZonedDateTime apollo11lauch = ZonedDateTime.of(1969, 7, 16, 9, 32, 0, 0,
    ZoneId.of(America/New_York));
    // 1969-07-16T09:32-04:00[America/New_York]

/** 这是一个具体的时刻,
  * 调用apollo11lauch.toInstant可以获得对于的Instant对象。
  * 反过来
  * 若有一个时刻对象,
  * 调用instant.atZone(ZoneId.of("UTC"))可以获得格林尼治皇家天文台的ZonedDateTime对象。
  */

UTC代表“协调世界时”,这是英文“Coordinated Universal Time”和法文“Temps Universal Coordiné”首字母缩写的折中。UTC是不考虑夏令时的格林尼治皇家天文台时间。

/** 当夏令时开始时,
  * 时钟要向前拨快一小时。
  * 例如,
  * 在2013年,中欧地区在3月31日2:00切换到夏令时,
  * 若视图构建的时间是不存在的3月31日 2:30,
  * 那么实际上得到的是3:30。
  */
ZonedDateTime skipped = ZonedDateTime.of(
    LocalDate.of(2013, 3, 31),
    LocalTime.of(2, 30),
    ZonedId.of("Europe/Berlin"));
    // Constructs March 31 3:30

/** 返过来,
  * 当夏令时结束时,
  * 时钟要向回拨慢一小时,
  * 同一个本地时间就会出现两次。
  * 当构建位于这个时间段内的时间对象时,
  * 就会得到这个两个时刻中较早的一个。
  */
ZonedDateTime ambiguous = ZonedDateTime.of(
    LocalDate.of(2013, 10, 27), // End of daylight savings time
    LocalTime.of(2, 30),
    ZoneId.of("Europe/Berlin"));
    // 2013-10-27T02:30+02:00[Europe/Berlin]
ZonedDateTime anHourLater = ambiguous.plusHours(1);
    // 2013-10-27T02:30+01:00[Europe/Berlin]

/** 在调整跨越夏令时边界的日期时需要特别注意。
  * 例如,
  * 如果将会议设置在下个星期,
  * 不要直接加上一个7天的Duration:
  */
ZonedDateTime nextMeeting = meeting.plus(Duration.ofDays(7));
    // Caution! Won't work with daylight savings time
    
/** 而应该使用Period类
  */
ZonedDateTime nextMeeting = meeting.plus(Period.ofDays(7)); // OK

6.6 格式化和解析

  1. 预定义的格式器
  2. locale相关的格式器
  3. 带有定制模式的格式器
格式器 描述 示例
BASIC_ISO_DATA 年、月、日、时区偏移量,
中间没有分隔符
19690716-0500
ISO_LOCAL_DATE,
ISO_LOCAL_TIME,
ISO_LOCAL_DATE_TIME
分隔符为-、:、T 1969-07-16, 09:32:00
1969-07-16T09:32:00
ISO_OFFSET_DATE,
ISO_OFFSET_TIME,
ISO_OFFSET_DATE_TIME
类似ISO_LOCAL_XXX,
但是有时区偏移量
1969-07-16-05:00,
09:32:00-05:00,
1969-07-16T09:32:00-05:00
ISO_ZONED_DATE_TIME 有时区偏移量和
时区ID
1969-07-16T09:32:00-05:00
[Americal/New York]
ISO_INSTANT 在UTC中,
用Z时区ID来表示
1969-07-16T14:32:00Z
ISO_DATE,
ISO_TIME,
ISO_DATE_TIME
类似
ISO_OFFSET_DATE、
ISO_OFFSET_TIME

ISO_ZONED_DATE_TIME,
但是时区信息是可选的
1969-07-16-05:00,
09:32:00-05:00,
1969-07-16T09:32:00-05:00
[Americal/New York]
ISO_ORDINAL_DATE LocalDate的年和年日期 1969-197
ISO_WEEK_DATE LocalDate的年、星期和
星期日期
1969-W29-3
RFC_1123_DATE_TIME 用于邮件时间戳的标准,
编纂于RFC822,
并在RFC1123中
将年份更新到4位
Wed, 16 Jul 1969 09:32:00 -0500
/** 要使用标准的格式器,
  * 可以直接调用其format方法:
  */
String formatted = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(appllo11lauch);
    // 1969-07-16T09:32:00-04:00
风格 日期 时间
SHORT 7/16/69 9:32 AM
MEDIUM Jul 16, 1969 9:32:00 AM
LONG July 16, 1969 9:32:00 AM EDT
FULL Wednesday, July 16, 1969 9:32:00 AM EDT
/** 静态方法ofLocalizedDate、ofLocalizedTime和ofLocalizedDateTime可以创建这种格式器。
  */
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
String fromatted = formatter.format(appllo11lauch);
    // July 16, 1969 9:32:00 AM EDT

/** 这些方法使用了默认的locale。
  * 为了切换到不同的locale,
  * 可以直接使用withLocale方法。
formatted = formatter.withLocale(Locale.FRENCH).format(apollo11lauch);
    // 16 juillet 1969 09:32:00 EDT
    
/** DayOfWeek和Month枚举都有getDisplayName方法,
  * 可以按照不同的locale和格式给出星期日期和月份的名字。
  */
for (DayOfWeek w : DayOfWeek.values())
    System.out.print(w.getDisplayName(TextStyle.SHORT, Locale.ENGLISH) + " ");
    // Prints Mon Tue Wed Thu Fri Sat Sun
formatter = DateTimeFormatter.ofPattern("E yyyy-MM-dd HH:mm");

6.7 与遗留代码的互操作

转换到遗留类 转换自遗留类
Instant ⇋
java.util.Date
Date
.from(instant)
date
.toInstant()
ZonedDateTime ⇋
java.util.GregorianCalendar
GregorianCalendar
.from(
zonedDateTime)
cal
.toZonedDateTime()
Instant ⇋
java.sql.Timestamp
TimeStamp
.from(instant)
timestamp
.toInstant()
LocalDateTime ⇋
java.sql.Timestamp
Timestamp
.valueOf(
localDateTime)
timeStamp
.toLocalDateTime()
LocalDate ⇋
java.sql.Date
Date
.valueOf(localDate)
date
.toLocalDate()
LocalTime ⇋
java.sql.Time
Time
.valueOf(localTime)
time
.toLocalTime()
DateTimeFormatter ⇋
java.text.DateFormat
formatter
.toFormat()
java.util.TimeZone
⇋ ZoneId
Timezone
.getTimeZone(id)
timeZone
.toZoneId()
java.nio.file.attribute.FileTime
⇋ Instant
FileTime
.from(instant)
fileTime
.toInstant()

第7章 国际化

locale
消息格式化
数字格式
文本输入和输出
日期和时间
资源包
排序和规范化

7.1 locale

7.1.1 为什么需要locale

例如,对于德国用户,数字 123,456.78 应该显示为 123.456,78(小数点和十进制数的逗号分隔符的角色是相反的)。
又例如,在美国,日期的显示为月/日/年;在德国,日期的显示为日/月/年;而在中国,则使用年/月/日。

7.1.2 指定locale

  1. 一种语言,由2个或3个小写字母表示,如en(英语)、de(德语)和zh(中文)。
  2. 可选的一段文本,由首字母大写的四个字母表示,例如Latn(拉丁文)、Cyrl(西里尔文)和Hant(繁体中文)。
  3. 可选的一个国家或地区,由2个大写字母或3个数字表示,例如US(美国)和CH(瑞士)。
  4. 可选的一个变体,用于指定各种杂项特性,例如方言和拼写规则。
  5. 可选的一个扩展。扩展描述了日历(如中国农历)和数字(替代西方数字的泰语数字)等内容的本地偏好。

在德国,你可以使用de-DE。瑞士有4种官方语言(德语、法语、意大利语和里托罗曼斯语)。在瑞士讲德语的人希望使用的locale是de-CH。这个locale会使用德语的规则,但是货币值会表示成瑞士法郎而不是欧元。

// 用标签字符串来构建Locale对象
Locale usEnglish = Locale.forLanguageTag("en-US");

// toLanguageTag方法可以生成给定locale的语言标签
Local.US.toLanguageTag();  // 生成字符串"en-US"

7.1.3 默认locale

// 获取这些偏好,可以调用
Locale displayLocale = Locale.getDefault(Locale.Category.DISPLAY);
Locale formatLocale = Locale.getDefault(Locale.Category.FORMAT);

7.2 数字格式

7.2.1 格式化数字值

  1. 得到Locale对象。
  2. 使用一个“工厂方法”得到一个格式器对象。
  3. 使用这个格式器对象来完成格式化和解析工作。
/** 工厂方法是NumberFormat类的静态方法,
  * 它们接受一个Locale类型的参数。
  * 总共有3个工厂方法getNumberInstance、
  * getCurrencyInstance和getPercentInstance,
  * 这些方法返回的对象可以分别对
  * 数字、货币量和百分比进行格式化和解析。
  */
Locale loc = Locale.GERMAN;
NumberFormat currFmt = NumberFormat.getCurrencyInstance(loc);
double amt = 123456.78;
String result = currFmt.format(amt);
// 结果是 123.456,78 €
/** 相反,
  * 如果想读取一个按照某个locale的惯用法而输入或存储的数字,
  * 那么就需要使用parse方法。
  */
TextField inputField;
...
NumberFormat fmt = NumberFormat.getNumberInstance();
// get the number formatter for default locale
Number input = fmt.parse(inputField.getText().trim());
double x = input.doubleValue();

7.2.2 货币

/** 假设为一个美国客户准备了一张货物单,
  * 货物单中有些货物的金额是用美元表示的,
  * 有些是用欧元表示的,此时,
  * 你不能只是使用两种格式器:
  */
NumberFormat dollarFormatter = NumberFormat.getCurrencyInstance(Locale.US);
NumberFormat euroFormatter = NumberFormat.getCurrencyInstance(Locale.GERMANY);
/** 这会导致发票看起来非常奇怪,
  * 有些金额的格式像 $ 100,000,
  * 另一些则像100.000 €(注意:欧元值使用小数点而不是逗号作为分隔符)。
  */

/** 应当使用Currency类来控制被格式器处理的货币。
  * 为美国客户设置欧元格式:
  */
NumberFormat euroFormatter = NumberFormat.getCurrencyInstance(Locale.US);
euroFormatter.setCurrency(Currency.getInstance("EUR"));
货币值 标识符 货币代码
U.S. Dollar USD 840
Euro EUR 978
British Pound GBP 826
Japanese Yen JPY 392
Chinese Renmingbi(Yuan) CNY 156
Indian Rupee INR 356
Russian Ruble RUB 643

7.3 日期和时间

  1. 月份和星期应该用本地语言来表示。
  2. 年、月、日的顺序要符合本地习惯。
  3. 公历可能不是本地首选的日期表示方法。
  4. 必须考虑本地区的时区。
FormatStyle style = ...; // One of FormatStyle.SHORT, FormatStyle.MEDIUM, ...
DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(style);
DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(style);

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(style);
    // or DateTimeFormatter.ofLocalizedDateTime(style1, style2)
    
/** 以上格式器都会使用当前的locale。
  * 为了使用不同的locale,
  * 需要使用withLocale方法:
  */
DateTimeFormatter dateFormatter =
    DateTimeFormatter.ofLocalizedDate(style).withLocale(locale);

7.4 排序和规范化

/** 获得locale敏感的比较器,
  * 可以调用静态的Collator.getInstance方法:
  */
Collator coll = Collator.getInstance(locale);
words.sort(coll); // Collator implements Comparator<Object>

7.5 消息格式化

7.5.1 格式化数字和日期

“On {2}, a {0} destroyed {1} houses and caused {3} of damage.”

/** 括号中的数字是占位符,
  * 可以用实际的名字和值来替换它们。
  * 使用静态方法MessageFormat.format
  * 可以用实际的值来替换这些占位符。
  */
String msg =
    MessageFormat.format("On {2}, a {0} destroyed {1} houses and caused {3} of damage.",
        "hurricane", 99, new GregorianCalendar(1999, 0, 1).getTime(), 10.0E8);
/** 结果是下面的字符串:
  * On 1/1/99 12:00 AM, a hurricane destroyed 99 houses and caused 100,000,000 of damage.
  */
  
  
/** 还可以为占位符提供可选的格式。
  */
String msg =
    MessageFormat.format("On {2,date,long}, a {0} destroyed {1} houses and caused {3,number,currency} of damage.", 
        "hurricane", 99, new GregorianCalendar(1999, 0, 1).getTime(), 10.0E8);
/** 结果是下面的字符串:
  * On January 1, 1999 12:00 AM, a hurricane destroyed 99 houses and caused $100,000,000 of damage.
  */

一般来说,占位符索引后面可以跟一个类型(type)和一个风格(style),它们之间用逗号隔开。
类型可以是:
number
time
date
choice
如果类型是number,那么风格可以是:
integer
currency
percent
如果类型是time或date,那么风格可以是:
short
medium
long
full

7.5.2 选择格式

一个下限(lower limit)。
一个格式字符串(format string)。

7.6 文本输入和输出

7.6.1 文本文件

/** 若知道遗留文件所希望使用的字符编码机制,
  * 可在读写文本文件时指定它:
  */
var out = new PrintWriter(filename, "Windows-1252");

/** 获得可用的最佳编码机制,
  * 可以通过下面的调用来获得“平台的编码机制”:
  */
Charset platformEncoding = Charset.defaultCharset();

7.6.2 行结束符

/** 与在字符串中使用\n不同,
  * 可以使用printf和%n格式说明符来产生平台相关的行结束符。
  */
out.printf("Hello%nWorld%n");

/** 在Windows上产生
  * Hello\r\nWorld\r\n
  * ***
  * 在其他所有平台上产生
  * Hello\nWorld\n
  */

7.6.3 控制台

7.6.4 日志文件

7.7 资源包

7.7.1 定位资源包

// 加载一个包:
ResourceBundle currentResources = ReourceBundle.getBundle(baseName, currentLocale);

7.7.2 属性文件

computeButton=Rechnen
colorName=black
defaultPaperSize=210*297

/** 可以像上一节描述的那样命名属性文件,
  * 例如,
  * MyProgramStrings.properties
  * MyProgramStrings_en.properties
  * MyProgramStrings_de_DE.properties
  */
ResourceBundle currentResources = ReourceBundle.getBundle("MyProgramStrings", locale);

/** 要查找一个集体的字符串,
  * 可以调用
  */
String computeButtonLabel = bundle.getString("computeButton");

7.7.3 包类

MyProgramStrings.java
MyProgramStrings_en.java
MyProgramStrings_de_DE.java


第8章 脚本、编译与注解处理

Java平台的脚本机制
标准注解
编译器API
源码级注解处理
使用注解
字节码工程
注解语法

8.1 Java平台的脚本机制

  1. 便于快速变更,鼓励不断实验。
  2. 可以修改运行着的程序的行为。
  3. 支持程序用户的定制化。

8.1.1 获取脚本引擎

引擎 名字 MIME类型 文件扩展
Nashorn(包含在JDK中) nashorn, Nashorn,
js, JS, JavaScript, javascript,
ECMAScript, ecmascript
application/javascript,
application/ecmascript,
text/javascript,
text/ecmascript
js
Groovy groovy groovy
Renjin Renjin text/x-R R, r, S, s
/** 知道所需要的引擎,
  * 可以直接通过名字、MIME类型或文件扩展来请求它:
  */
ScriptEngine engine = manager.getEngineByName("nashorn");

8.1.2 脚本计算与绑定

/** 一旦拥有了引擎,
  * 就可以通过下面的调用来直接调用脚本:
  */
Object result = engine.eval(scriptString);

/** 如果脚本存储在文件中,
  * 那么需要先打开一个Reader,
  * 然后调用:
  */
Object result = engine.eval(reader);

/** 可以在同一个引擎上调用多个脚本。
  * 如果一个脚本定义了变量、函数或类,
  * 那么大多数引擎都会保留这些定义,
  * 以供将来使用。
  */
engine.eval("n = 1728");
Object result = engine.eval("n + 1"); // 将返回 1729

要想知道在多个线程中并发执行脚本是否安全,可以调用
Object param = factory.getParameter(“THREADING”);
其返回值是下列值之一:
null:并发执行不安全
“MULTITHREADED”:并发执行安全。一个线程的执行效果对另外的线程有可能是可视的。
“THREAD-ISOLATED”:除了 “MULTITHREADED”,还会为每个线程维护不同的变量绑定。
“STATELESS”:除了 “THREAD-ISOLATED”,脚本还不会改变变量绑定。

8.1.3 重定向输入和输出

/** 任何用JavaScript的print和println函数产生的输出都会被发送到writer。
  */
var writer = new StringWriter();
engine.getContext().setWriter(new PrintWriter(writer, true));

/** setReader和setWriter方法只会影响脚本引擎的标准输入和输出源。
  * 执行下面的JavaScript代码,
  * 只有第一个输出会被重定向。
  */
println("Hello");
java.lang.System.out.println("World");

8.1.4 调用脚本的函数和方法

/** 要调用一个函数,
  * 需要用函数名来调用invokeFunction方法,
  * 函数名后面是函数的参数:
  */
// Define greet function in JavaScript
engine.eval("functionn greet(how, whom) { return how + ', ' + whom + '!' }");

// Call the function with arguments "Hello", "World"
result = ((Invocable) engine).invokeFunction("greet", "Hello", "World");

/** 如果脚本语言是面向对象的,
  * 那就可以调用invokeMethod:
  */
// Define Greeter class in JavaScript
engine.eval("function Greeter(how) { this.how = how }");
engine.eval("Greeter.prototype.welcome = "
    + " function(whom) { return this.how + ', ' + whom + '!'}");
    
// Construct an instance
Object yo = engine.eval("new Greeter('Yo')");

// Call the welcome method on the instance
result = ((Invocable) engine).invokeMethod(yo, "welcome", "World");
// 接口
public interface Greeter {
    String welcome(String whom);
}

/** 如果在Nashorn中定义了具有相同名字的函数,
  * 那么可通过这个接口来调用它:
  */
// Define welcome function in JavaScript
engine.eval("function welcome(whom) { return 'Hello, ' + whom + '!'}");

// Get a Java object and call a Java method
Greeter g = ((Invocable) engine).getInterface(Greeter.class);
result = g.welcome("World");

/** 在面对对象的脚本语言中,
  * 可以通过相匹配的Java接口来访问一个脚本类。
  * 例如,
  * 使用Java的语法来调用JavaScript的SimpleGreeter类:
  */
Greeter g = ((Invocable) engine).getInterface(yo, Greeter.class);
result = g.welcome("World");

8.1.5 编译脚本

/** 编译和计算包含在脚本文件中的代码:
  */
var reader = new FileReader("myscript.js");
CompiledScript script = null;
if (engine implements Compilable)
    script = ((Compilable) engine).compile(reader);
    
/** 下面的代码会在编译成功的情况下执行编译后的脚本,
  * 如果引擎不支持编译,
  * 则执行原始的脚本。
  */
if (script != null)
    script.eval();
else
    engine.eval(reader);

8.2 编译器API

8.2.1 调用编译器

/** 调用编译器非常简单,
  * 下面是一个示范调用:
  */
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
OutputStream outStream = ...;
OutputStream errStream = ...;
int result = compiler.run(null, outStream, errStream, 
    "-sourcepath", "src", "Test.java");
// 返回值为0表示编译成功

8.2.2 发起编译任务

/** 要想获取CompilationTask对象,
  * 需要以前一节中描述的compiler对象开始,
  * 然后按照下面的方式调用:
  */
JavaCompiler.CompilationTask task = compiler.getTask(
    errorWriter, // Uses System.err if null
    fileManager, // Uses the standard file manager if null
    diagnostics, // Uses System.err if null
    options, // null if no options
    classes, // For annotation processing; null if none
    sources);

/** 最后三个参数是Iterable的实例。
  * 例如,选定序列可以像下面这样指定:
  */
Iterable<String> options = List.of("-d", "bin");

/** sources参数是JavaFileObject实例的Iterable。
  * 如果想要编译磁盘文件,
  * 需要获取一个StandardJavaFileManager对象,
  * 并调用其getJavaFileObject方法:
  */
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
Iterable<JavaFileObject> sources
    = fileManager.getJavaFileObjectsFromStrings(List.of("File1.java", "File2.java"));
JavaCompiler.CompilationTask task = compiler.getTask(
    null, null, null, options, null, sources);

8.2.3 捕获诊断消息

/** DiagnosticCollector类实现了这个接口,
  * 它将收集所有的诊断信息,
  * 可以在编译完成之后遍历这些信息。
  */
DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
compiler.getTask(null, fileManager, collector, null, null, sources).call();
for (Diagnostic<? extends JavaFileObject> d : collector.getDiagnostics()) {
    System.out.printl(d);
}

/** 还可以在标准的文件管理器上安装一个DiagnosticListener对象,
  * 这样就可以捕获到有关文件缺失的消息:
  */
StandardJavaFileManager fileManager
    = compiler.getStandardFileManager(diagnostics, null, null);

8.2.4 从内存中读取源文件

/** 若动态地生成了源代码,
  * 那么就可以从内存中获取它来进行编译,
  * 而无须在磁盘上保存文件。
  */
public class StringSource extends SimpleJavaFileObject {
    private String code;
    
    StringSource(String name, String code) {
        super(URI.create("string:///" + name.repalce('.','/') + ".java", Kind.SOURCE);
        this.code = code;
    }
    
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return code;
    }
}

/** 然后,生成类的代码,
  * 并提交给编译器一个StringSource对象的列表:
  */
List<StringSource> sources = List.of(
    new StringSource(className1, class1CodeString), ...);
task = compiler.getTask(null, fileManager, diagnostics, null, null, sources);

8.2.5 将字节码写出到内存中

8.3 使用注解

附属文件的自动生成,例如部署描述符或者bean信息类。
测试、日志、事务语义等代码的自动生成。

8.3.1 注解简介

// 例如:
public class MyClass {
    ...
    @Test public void checkRandomInsertions()
}

8.4 注解语法

8.4.1 注解接口

modifiers @interface AnnotationName {
    elementDeclaration1
    elementDeclaration2
    ...
}

/** 每个元素声明都具有下面这种形式:
  */
type elementName(); // or type elementName() default value;

/** 例如,下面这个注解具有两个元素:
  * assignedTo和severity。
  */
public @interface BugReport {
    String assignedTo() default "[none]";
    int severity();
}

基本类型(int、short、long、type、char、double、float或者boolean)。
String。
Class(具有一个可选的类型参数,例如Class<? extends MyClass)。
enum类型。
注解类型。
由以上所述类型组成的数组(由数组组成的数组不是合法的元素类型)。

8.4.2 注解

/** 每个注解都具有下面这种格式:
  */
@AnnotationName(elementName1=value1, elementName2=value2, ...)

/** 例如,
  */
@BugReport(assignedTo="Harry", severity=10)

/** 元素的顺序无关紧要,
  * 下面这个注解与前面那个一样。
  */
@BugReport(severity=10, assignedTo="Harry")

/** 如果某个元素的值并未指定,那么就使用声明的默认值。
  * 例如,下面这个注解中,
  * assignedTo的值是字符串 "[none]"。
  */
@BugReport(severity=10)

/** 如果没有指定元素,
  * 要么是因为注解中没有任何元素,
  * 要么是因为所有元素都使用默认值。
  */
@BugReport
// 称为 标记注解,等同于
@BugReport(assignedTo="[none]", severity=0)
/** 单值注解
  * 如果一个元素具有特殊的名字value,
  * 并且没有指定其他元素,
  * 那么就可以忽略掉这个元素名以及等号。
  */
public @interface ActionListenerFor {
    String value();
}

/** 那么,
  * 可以将注解书写为以下形式:
  */
@ActionListenerFor("yellowButton")

8.4.3 注解各类说明

/** 1. 对于类和接口,
  * 需要将注解放置在class和interface关键词的前面:  
  */
@Entity public class User {...}

/** 2. 对于变量,
  * 需要将它们放置在类型的前面:
  */
@SuppressWarnings("unchecked") List<User> users = ...;  
public User getUser(@Param("id") String userId)

/** 3. 对于泛化类或方法中的类型参数: 
  */
public class Cache<@Immutable V> {...}

/** 4. 包是在文件package-info.java中注解的,
  * 该文件只包含以注解先导的包语句。
  */
/** 
    Package-level Javadoc
  */
@GPL(version="3")
package com.horstnann.corejava;
import org.gnu.GPL

8.4.4 注解类型用法

public User getUser(@NonNull String userId)
// 断言userId参数不为空

List<@NonNull String>
// 断言其中所有字符串不为空
  1. 与泛化类型参数一起使用:List<@NonNull String>, Comparator.<@NonNull String> reverseOrder()>。
  2. 数组中的任何位置:@NonNull String[][] words(words[i][j]不为null),String @NonNull [][] words(words不为null),String[] @NonNull [] words(words[i]不为null)。
  3. 与超类和实现接口一起使用:class Warning extends @Localized Message。
  4. 与构造器调用一起使用:new @Localized String(…)。
  5. 与强制转型和instanceof检查一起使用:(@Localized String) text, if (text instanceof @Localized String)。
  6. 与异常规约一起使用:public String read() throws @Localized IOException。
  7. 与通配符和类型边界一起使用:List<@Localized ? extends Message>,List<? extends @Localized Message>。
  8. 与方法和构造器引用一起使用:@Localized Message::getText。

8.4.5 注解this

/** 假设想要将参数注解为在方法中不会被修改。
  */
public class Point {
    public boolean equals(@ReadOnly Object other) { ... }
}

/** 处理这个注解的工具在看到下面的调用时,
  * p.equals(q);
  * 就会推理出q没有被修改过。
  * 当该方法被调用时,
  * this变量是绑定到p的。
  * 但是this从来都没有被声明过,
  * 因此无法注解它。
  */
  
/** 实际上,
  * 可以用一种很少使用的语法变体来声明它,
  * 这样就可以添加注解了。
  */
public class Point {
    public boolean equals(@ReadOnly Point this, @ReadOnly Object other) { ... }
}
// 第一个参数被称为接收器参数,它必须被命名为this,其类型为要构建的类。

/** 传递给内部类构造器的是另一个不同的隐藏参数,
  * 即对其外围类对象的引用。
  * 可以让这个参数显示化。
  * 这个参数的名字必须像引用它时那样,叫做EnclosingClass.this,其类型为外围类
  */
public class Sequence {
    private int from;
    private int to;
    
    class Iterator implements java.util.Iterator<Integer> {
        private int current;
        public Iterator(@ReadOnly Sequence Sequence.this) {
            this.current = Sequence.this.from;
        }
        ...
    }
    ...
}

8.5 标准注解

注解接口 应用场合 目的
@Deprecated 全部 将项标记为过时的
@SafeVarargs 方法和构造器 断言varargs参数可安全使用
@Override 方法 检查该方法是否覆盖了某一个超类方法
@FunctionalInterface 接口 将接口标记为只有一个抽象方法的函数式接口
@PostConstruct PreDestroy 方法 被标记的方法应该在构造之后或移除之前立即被调用
@Resource 类、接口、方法、域 在类或接口上:标记为在其他地方要用到的资源。在方法或域上:为“注入”而标记
@Resources 类、接口 一个资源数组
@Generated 全部
@Target 注解 指明可以应用这个注解的那些项
@Retention 注解 指明这个注解可以保留多久
@Documented 注解 指明这个注解应该包含在注解项的文档中
@Inherited 注解 指明当这个注解应用于一个类的时候,能够自动被它的子类继承
@Repeatable 注解 指明这个注解可以在同一个项上应用多次

第9章 Java平台模块系统

模块的概念
自动模块
对模块命名
不具名模块

9.1 模块的概念

  1. 一个包集合。
  2. 可选地包含资源文件和像本地库这样的其他文件。
  3. 一个有关模块中可访问的包的列表。
  4. 一个有关这个模块依赖的所有其他模块的列表。
  1. 强封装性:可以控制哪些包是可访问的,并且无须操心去维护那些不想开发给公众去访问的代码。
  2. 可靠的配置:可以避免诸如类重复或丢失这类常见的类路径问题。

9.2 对模块命名

例如,模块com.horstmann 和 模块com.horstmann.corejava,就模块系统而言,它们是无关的。

9.3 模块化的“Hello World!”程序

package com.horstmann.hello;

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, Modular World!");
    }
}

module v2ch09.hellomod {
}

9.4 对模块的需求

package com.horstmann.hello;

import javax.swing.JOptionPane;

public class HelloWorld {
    public static void main(String[] args) {
         JOptionPane.showMessageDialog(null,"Hello, Modular World!");
    }
}
module v2ch09.hellomod {
     requires java.desktop;
}

第10章 安全

类加载器
数字签名
安全管理器与访问权限
加密
用户认证

  1. 语言设计特性(对数组的边界进行检查,无不受检查的类型转换,无指针算法等)。
  2. 访问控制机制,用于控制代码能够执行的操作(比如文件访问,网络访问等)。
  3. 代码签名,利用该特性,代码的作者就能够用标准的加密算法来认证Java代码。这样,该代码的使用者就能够准确地知道谁创建了该代码,以及代码签名后是否被修改过。

10.1 类加载器

10.1.1 类加载过程

  1. 虚拟机有一个用于加载类文件的机制,例如,从磁盘上读取文件或者请求Web上的文件,它使用该机制来加载 MyProgram 类文件中的内容。
  2. 如果 MyProgram 类用于类型为另一个类的域,或者是拥有超类,那么这些类文件也会被加载。(加载某个类所依赖的所有类的过程称为类的解析。)
  3. 接着,虚拟机执行 Myprogram 中的 main 方法(它是静态的,无须创建类的实例)。
  4. 如果 main 方法或者 main 调用的方法要用到更多的类,那么就下来就会加载这些类。

引导类加载器
平台类加载器
系统类加载器(有时也称为应用类加载器)

10.1.2 类加载器的层次结构

10.1.3 将类加载器用作命名空间

因为每个类都是由单独的类加载器加载的,所以这些类可以彻底地区分开而不会产生任何冲突。

10.1.4 编写自己的类加载器

10.1.5 字节码校验

变量要在使用之前进行初始化。
方法调用与对象引用类型之间要匹配。
访问私有数据和方法的规则没有被违反。
对本地变量的访问都落在运行时堆栈内。
运行时堆栈没有溢出。

10.2 安全管理器与访问权限

10.2.1 权限检查

创建一个新的类加载器
退出虚拟机
使用反射访问另一个类的成员
访问本地文件
打开socket连接
启动打印作业
访问系统剪贴板
访问AWT时间队列
打开一个顶层窗口

10.2.2 Java平台安全性

image

10.2.3 安全策略文件

10.3 用户认证

10.3.1 JAAS框架

10.4 数字签名

10.4.1 消息摘要

  1. 如果数据的1位或者几位改变了,那么消息摘要也将改变。
  2. 拥有给定消息的伪造者无法创建与原消息具有相同摘要的假消息。

10.4.2 消息签名

假设Alice想要给Bob发送一个消息,Bob想知道该消息是否来自Alice,而不是冒名顶替者。Alice写好了消息,并且用她的私有密钥对该消息摘要签名。Bob得到了她的公共密钥的拷贝,然后Bob用公共密钥对该签名进行校验。如果通过了校验,则Bob可以确定以下两个事实:

  1. 原始消息没有被篡改过。
  2. 该消息是由Alice签名的,她是私有密钥的持有者,该私有密钥就是与Bob用于校验的公共密钥相匹配的密钥。

10.5 加密


第11章 高级Swing和图形化编程


第12章 本地方法

从Java程序中调用C函数
调用Java方法
数值参数与返回值
访问数组元素

  1. 应用需要访问的系统特性和设备通过Java平台是无法实现的。
  2. 已经有了大量的测试过和调试过的另一种语言的代码,并且知道如何将其导出到所有目标平台上。
  3. 通过基准测试,发现所编写的Java代码比用其他语言编写的等价代码要慢得多。

12.1 从Java程序中调用C函数

image