笔记 | Java 入门系列教程

由于对图灵出品的 O’Reilly 动物书情有独钟,至此本篇文章以动物书系列之 Java 技术手册 为主要框架,开启我的 Java 语言学习旅程。

当然,通读一遍本书之后,你会发现本书对 Java 基础知识部分的讲述一定是有所欠缺的,所以课后补充工作很有必要。如通过参考其他 Java 书籍(下文中有参考书目推荐)、优秀博文的补充,核心需求就是要输出一份便于常翻阅、可复用的读书笔记、学习笔记。

需要说明的是,笔记当中会包含实际项目当中深入了解、研究的知识点,如 Class 类文件结构、Java 范型等。既经由分析、解决、随之文档化的过程,这不仅仅可作为自己的案例库,也可用于分享、交流。毕竟自身的知识面是非常有限的,有不恰当之处、不正确的地方,欢迎广大朋友的帮忙、斧正,互为补足。

参考书目

  • 基础篇:《 Java 技术手册 》: O’Reilly 动物书系列,因本书籍不会刻意去阐述面向对象编程(Object Oriented Programming,OOP)的相关概念、内容,适合对 OOP、Java 编程语言有一定了解后,所使用的学习材料。
  • 进阶篇:《 Java编程思想 / Thinking in Java 》:待阅读。贴上本书豆瓣的书评,供朋友评判,做出抉择。TIJ(中文第四版) | TIJ(英文第四版)

优秀博文

开源项目

更新进程

  • 2017.09.01:整理 Notes 草稿;
  • 2017.09.14:输出 Markdown 文档;
  • 2018.01.30:完成序言;
  • 2018.03.20:更新正文 ( 共 10 章 );
  • 2018.04.30:更新完毕.

内容总览

壹 Java 环境介绍

JVM - 程序、环境

  • Java 虚拟机 (Java Virtual Machine,JVM)。
  • JVM 是一个程序,提供运行 Java 程序所需的运行时环境。
  • 应用代码的容器。
  • 提供一个安全、跨平台的执行环境。
  • Java 源码 -> Java 字节码 (*.class) -> JVM (即字节码格式程序的解码器,即图 1-1 中的解释器)。

Java 程序的生命周期

  • Java 代码的编译和加载

    整个流程从 Java 源码开始,经过 Javac 程序处理后得到类文件,类文件保存的是编译源码后得到的 Java 字节码。类文件是 Java 平台能处理的最小功能单位,也是把新代码传给运行中程序的唯一方式。

    新的类文件通过类加载机制载入虚拟机,从而把新类型提供给解释器执行。

    Java 代码的编译和加载过程,如图 1-1 所示。

Java代码的编译和加载过程

图 1-1 Java 代码的编译和加载过程
  • 其中,涉及了 机器码字节码 的概念

    • 机器码 (Machine Code),学名机器语言指令,有时也被称为原生码(Native Code),是电脑的CPU可直接解读的数据。
      通常意义上来理解的话,机器码就是计算机可以直接执行,并且执行速度最快的代码。

    • 字节码 (Bytecode),是一种包含执行程序、由一序列 数据对 组成的二进制文件。字节码是一种中间码,它比机器码更抽象,需要直译器转译后才能成为机器码的中间代码。
      通常情况下字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令等构成的序列。

      • 关于 字节码文件,即 Class 类文件,根据Java虚拟机规范的规定,Class 文件格式采用一种类似于 C 语言结构体的伪结构表示,如下述代码所示。
        若想了解更多 Class 文件结构的内容,可参考周志明老师写的《深入理解Java虚拟机》\(^{[1]}\),其中有对 Class 文件结构的详细介绍。
      • 延伸:其实每种类型的文件的头都有着 4 个字节的作用域,则把其称为 魔数,它的作用是唯一标识该文件所属类型。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      ClassFile {  
      u4 magic;
      u2 minor_version;
      u2 major_version;
      u2 constant_pool_count;
      cp_info constant_pool[constant_pool_count-1];
      u2 access_flags;
      u2 this_class;
      u2 super_class;
      u2 interfaces_count;
      u2 interfaces[interfaces_count];
      u2 fields_count;
      field_info fields[fields_count];
      u2 methods_count;
      method_info methods[methods_count];
      u2 attributes_count;
      attribute_info attributes[attributes_count];
      }

Java 和其他语言比较

表 1-1 Java 与 C 语言的区别
Java C
Java 是面向对象的语言 C 是面向过程的语言
通过类文件实现可移植性 C 需要重新编译实现移植
没有指针 有指针
垃圾回收提供了自动内存管理功能 无法从低层布局内存 (结构体)
表 1-2 Java 与 PHP 语言的区别
Java PHP
Java 是静态类型语言 PHP 是动态类型语言
Java 支持多线程操作 PHP 不支持多线程
  • 书本中,关于静态语言与动态语言的区别,即在于编译时还是运行时检测错误。
  • 对于类型系统的概念,众说纷纭,如何理解静态、动态类型语言,推荐知乎 rainoftime 的回答:

    • Program Errors
      Trapped errors:导致程序终止执行,例如:零为被除数、Java 数组越界访问。
      Untrapped errors:出错后继续执行,但可能出现任意行为。如 C 里的缓冲区溢出、Jump 到错误地址。

    • Forbidden Behaviours
      语言设计时,可以定义一组 Forbidden behaviors,它必须包括所有 Untrapped errors,但可能包含 Trapped errors。

    • Well behaved、ill behaved
      well behaved: 如果程序执行不可能出现 Forbidden behaviors, 则为 Well behaved。
      ill behaved: 否则为 ill behaved。

      有了上面的概念,再讨论强、弱类型,静态、动态类型强、弱类型。

    • 强类型 ( Strongly typed )
      如果一种语言的所有程序都是 Well behaved,即不可能出现 Forbidden behaviors,则该语言为 Strongly typed。

    • 弱类型 ( Weakly typed)
      否则为 Weakly typed。譬如 C 语言的缓冲区溢出,属于 Trapped errors,即属于 Forbidden behaviors,故 C 是弱类型。

      弱类型语言类型检查更不严格,如偏向于容忍隐式类型转换。譬如说 C 语言的 int 型可强转为 double 型。这样的结果是:容易产生 Forbidden behaviours,所以是弱类型的。

    • 静态类型 (Statically)
      如果在编译时拒绝 ill behaved 程序,则是 Statically typed。

    • 动态类型 (Dynamiclly)
      如果在运行时拒绝 ill behaviors, 则是 Dynamiclly typed。

表 1-3 Java 与 JavaScript 语言的区别
Java JavaScript (Js)
Java 是静态类型语言 Js 是动态类型语言
Java 使用基于类的对象 JS 使用基于原型的对象
Java 提供良好的对象封装
Java 支持多线程操作 Js 不支持多线程

贰 Java 基本句法

词法结构

说明:词法结构的内容与 Java / Android 开发规范的内容有所交集,可参阅上述推荐的优秀博文:
BlankJ. Java / Android 开发规范. 2017. github.com

  • Java 编码使用的是 Unicode 字符集。
  • 需要区分大小写与空白的情况
    • 关键字使用小写 (class、interface、abstract、public、static 等);
    • 函数、方法名称使用 驼峰式命名法
    • 宏定义 使用全大写字母的命名风格;
  • 注释:单行注释、多行注释和块注释

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 单行注释:注释的内容

    /* 多行注释:注释的内容 */

    /**
    * 块注释的风格:
    * 注释的内容 1
    * 注释的内容 2
    */
  • 保留字
    如 interface、class、public、private、protected、true、null 等,是不能单用来命名变量名称和类名称的。

  • 标识符:Java 程序中某个部分的名称,例如类、类中方法和方法中声明的变量。

    • 不能以数字开头;
    • 不能包含标点符号;
    • 可以使用货币符号 (¥或$);

      货币符号主要用在自动生成的源码中,例如 Javac 生成的代码。不在标识符中使用货币符号,可避免自己的标识符和自动生成的标识符冲突。

  • 字面量
    直接出现在 Java 源码中的值,包括 整数 1、浮点数 3.141,单引号字符 'A'、双引号字符 "Hello" 以及保留字 true、false、null

基本数据类型

类型 取值 默认值 大小 范围
boolean true/false false 1 位 NA
char Unicode 字符 \u0000 16 位 \u0000 ~ \uFFFF
byte 有符号的整数 0 8 位 [-128, 127]
short 有符号的整数 0 16 位 [-32768, 32767]
int 有符号的整数 0 32 位 [-2147483648, 2147483647]
long 有符号的整数 0 64 位 [-9223372036854775808, 9223372036854775807]
float IEEE 154 浮点数 0.0 32 位 [1.4E-25, 3.4028235E+38]
double IEEE 754 浮点数 0.0 64 位 [4.9E-324, 1.7976931348623157E+308]

布尔类型

表示两种个逻辑状态,可表示开或关,也可是与否。

  • 零或非零表逻辑

    1
    2
    3
    while(1) { // -7,-1,1,8...等非零数值都可作为条件体 
    // 永真循环,条件体内为非零数值,即逻辑为真,若没有终止操作会一直操作下去
    }
  • 对象体表逻辑

    1
    2
    3
    4
    5
    Object obj = new Object();

    if( null != obj) {
    // 忽略实现细节
    }

字符类型

  • 普通字符
  • 转义字符
名称 符号
退格符 \b
水平制表符 \t
换行符 \n
换页符 \f
回车符 \r
双引号 \\”
八进制 \000
十六进制 \u0000

在上表中,以取双引号为例,只需附加上反斜杆 \ 即可。
例如:\\\" \'

表达式和运算符

  • 运算符概述

    • 优先级:与 C语言类同 (单目运算符 > 双目运算符 > 三目运算符)
    • 结合方式:从左向右
  • 算术运算符:加 +、减 -、乘 *、除 /、求模 %、负号 -

  • 字符串连接符:"Hello" + "World" 相当于 “HelloWorld”

  • 递增、递减运算符:操作数必须是变量、数组中的元素或对象的字段。

  • 比较运算符:等于 ==、不等于 !=、小于 <、大于 >、大于等于 >=、小于等于 <=

  • 逻辑运算符:条件与 &&、条件或 ||、逻辑非 !

  • 位运算符和位移运算符

    • 按位补码 ~:把操作数的每一位反相,0变1,1变0
      ~12 => ~00001100 => 11110011 => -13
    • 位与 &
      10 & 7 => 00001010 & 00000111 => 00000010 => 2
    • 位或 |
      10 | 7 => 00001010 | 00000111 => 00001111 => 15
    • 位异或 ^:相异为真
      10 ^ 7 => 00001010 ^ 00000111 => 00001101 => 13
  • 左移 <<:高位丢掉,右边补零,向左移动 n 位,相当于乘以 2\(^{n}\)。

    1
    2
    3
    4
    5
    6
    7
    左移运算的实例:

    10 << 1 => 00001010 << 1 => 00010100 => 20 => 10 * 2
    07 << 3 => 00000111 << 3 => 00111000 => 56 => 07 * 2^3
    -1 << 2 => 11111111 << 2 => 11111100 => -4 => -1 * 2^2

    // -1<<2 => 11111111<<2:即负数是补码形式存储的。
  • 带符号右移 >>:高位 补符号,左侧操作数为正数则 高位补0,左侧操作数为负数则 高位补1。向右移动 n 位,相当于除以 2\(^{n}\)。

    1
    2
    3
    10 >> 1  => 00001010 >> 1 => 0 0000101 => 05 => 10 / 2
    27 >> 3 => 00011011 >> 3 => 000 00011 => 03 => 27 / 2^3
    -50 >> 2 => 11001110 >> 2 => 11 110011 => -13
  • 赋值运算符 与 条件运算符

    1
    e = a < b ? c :d; // 条件运算符鼻赋值运算符优先级高
  • 特殊运算符

    • 访问对象成员 .
    • 访问数组中元素 []
    • 调用方法 ()
    • lambda 表达式 ->
    • 创建对象 new
    • 类型转换及校正 ()

语句

  • 选择结构和循环结构和 C 语言的相差无几,这里就不详细列举。

  • synchronized 语句

    1
    2
    3
    synchronized( Expression ) {
    // Statements
    }

    Expression 表达式的计算结果必须是一个对象或数组。Statements 是能导致破坏的代码块。

    即 Java 解析器为 Expression 计算得到的对象或数组获取一个排它锁,直到语句块执行完毕再释放。只要某个线程拥有对象的排它锁,其他线程就不能再获取这个锁。

  • throw 语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // throw expression;

    public static double factorial(int x) {
    double fact;

    if( x < 0 ) {
    throw new IllegalArgumentException("x must be >= 0.");
    } else {
    for(fact=1.0; x > 1; fact *= x, x--) {
    // Nothing
    }
    }
    return fact;
    }
  • try/catch/finall 语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    try {
    // 正常情况,从上往下执行
    // 也有可能跑出异常,或是 throw 语句直接抛出异常
    } catch( SomeException e1 ) {
    // 处理 SomeException 或其子类型的异常对象
    // 使用名称 e1 引用那个异常对象
    } catch( AnotherException e2 ) {
    // 处理 AnotherException,使用名称 e2 引用传入的异常对象
    } finally {
    // 不管 try 子句的结束方式如何,这段代码都会执行。
    // 但是 try 子句中调用了 System.exit(),解析器会马上退出,而不执 finally 子句。
    }

    即 Java 解析器执行 throw 语句,会立即停止常规程序执行,开始捕获或处理异常的异常处理程序 ( try/catch/finally 语句编写 )。

方法

定义方法

  • 方法的定义都是以签名开头,后面跟着方法主体。方法主体,即放在花括号里的任意语句序列。方法签名包括下述内容:

    • 方法名称;
    • 方法所用参数的数量、顺序、类型和名称;
    • 方法的返回值类型;
    • 方法抛出的已检异常 ( 下述有解释:已检异常和未检异常 );
    • 提供方法额外信息的多个方法修饰符.
  • 方法签名的格式:modifiers type name(paramlist) [ throws exceptions ]

    • modifiers : 指零个或多个特殊的修饰符关键字;
    • type : 指明方法的返回类型;
    • name : 即方法名称;
    • paramlist : 指形参列表;
    • exceptions : 抛出已检异常.

方法修饰符

修饰符 作用描述
abstract abstract 修饰方法,类本身也必须声明 abstract。
final final 修饰的方法不能被子类覆盖或隐藏。
public、protected、private 这些访问修饰符指定方法是否能在定义它的类之外使用,或能在何处使用。
synchronized synchronized 修饰符的作用是实现线程安全的方法 (避免两个线程同时执行该方法)。
static static 声明的方法是类方法。

已检异常和未检异常

  • 已检异常和未检异常 — 什么情况抛出异常

    • 已检异常:明确的特定情况下抛出。

      例如:FileNotFoundException — 打开某个文件却不在目录中。

    • 未检异常:任何方法任何时候都可能抛出。

      例如:OutOfMemoryError、NullPointerException。

  • 区分已检和未检异常,记两点:

    1. 异常是 Throwable 对象;
    2. 异常分两种类型:Error (未检) 和 Exception (已检).
  • 处理已检异常:在方法签名的 throws 子句中声明这个异常。Java 编译器检查方法签名,若没有声明会导致编译出错,故叫已检异常。

    1
    2
    3
    4
    5
    6
    public static estimateHomePageSize(String host) throw IOException {
    URL url = new URL("htts://" + host);
    try( InputStream in = url.openStream() ) {
    return in.available();
    }
    }

变长参数列表

  • 变长参数列表:方法可声明为数量不定的参数。

    1
    2
    3
    4
    public static int max(int first, int...rest) { 
    // int...rest 相当于 int[] rest
    // 省略实现细节
    }

介绍类和对象

关于类最重要的事情是,它们定义了一种新数据类型。例如定义一个 Point 类表示笛卡尔二维坐标系中的数据点。

  • 定义类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Point {
    public double x, y;
    public Point(double x, double y) {
    this.x = x;
    this.y = y;
    }
    public double distanceFromOrigin() {
    return Math.sqrt(x*x,y*y);
    }
    }
  • 创建对象与使用对象

    1
    2
    Point point = new Point(2.0, 3.5);
    System.out.println( "Sqrt(x, y):" + point.distanceFromOrigin() );
  • 再谈数据类型
    谈论数据类型时,得分开数据类型和数据类型的值。Char 类型的值表示某个具体的字符,而 Point 类是一种新的数据类型,用于表示坐标 (x, y),Point 类为引用类型,即聚合类型,而 Point 类表示的值是对象。

数据类型 数据类型的值
char unicode 字符
Point 类 point 对象
  • lambda 表达式:其实就是没有名称的函数,某个类中定义的匿名方法

    某个类中定义的匿名方法:Java 不允许脱离类的概念运行方法。

    定义 lambda 表达式:( paramlist ) -> { Statements }

    1
    Runable r = () -> { System.out.println("Hello World."); }

数组

数组的类型

数组中元素的类型可是任何有效的 Java 类型,包括数组类型。

1
2
3
4
5
6
7
8
9
10
11
// byte 是基本类型
byte b;

// byte[] 是由 byte 类型的值组成的数组
byte[] arrayOfBytes;

// byte[][] 是由 byte[] 类型的值组成的数组
byte[][] arrayOfArrayOfBytes;

// String[] 是由字符串组成的数组
String[] points;

创建和初始化数组

Java 在运行时 初始化数组 有个推论:数组初始化程序中的 表达式 可能会在运算时计算,而且不一定非要使用 编译时常量

1
2
3
4
5
6
Point[] point = 
{circle1.getCenterPoint(), circle2.getCenterPoint()}

String[] lines = new String[50]; // 中括号中使用非负整数
String[] greetings =
new String[] {"Hello", "World", "I'm Kofes."}

使用数组

  • 数组的边界:a[ 0…a.length-1 ]
  • 迭代数组:遍历数组
1
2
3
4
5
6
7
8
9
int[] primes = { 2, 3, 5, 7, 11, 13, 17, 19, 23 };
int sumOfPrimes = 0;

for( int = 0; i < primes.length; i++ ) {
sumOfPrimes += prime[i];
}

// 等价于上一个 for 循环
for( int p : primes ) sumOfPrimes += p;

多维数组

1
2
3
4
5
6
int[][] products = new int[10][]; // 正确
int[][] products = new int[][10]; // 错误,指定的维度必须位于最左边

for( int i = 0; i < 10; i++ ) {
products[i] = new int[10];
}

引用类型

  • 引用类型与基本类型

    • 引用类型
      1) 引用类型由用户定义,可有无限多个。
      2) 即聚合类型,保存零或多个基本值或对象。
    • 基本类型
      1) 8 种基本类型由 Java 语言定义,不能由我们自己定义新基本类型。
      2) 基本类型表示单个值。
      3) 基本类型需要 1 到 8 字节的内存空间。
  • 处理对象和引用副本

    1
    2
    3
    4
    5
    Point p = new Point(1.0, 2.0);
    Point q = p; // 因变量 p 和 q 保存的引用都指向同一对象。
    System.out.println(p.x); // 1.0
    q.x = 13.0;
    System.out.println(p.x); // 13.0
  • 比较对象,即 ==x.equal() 的区别

    • == 比较引用类型时,比较的是引用是否指向同一对象。
    • x.equal() 比较的是对象内容是否一样。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      String letter = "O";
      String s = "Hello";
      String t = "Hell" + letter;

      if( s==t ) {
      System.out.println("equal"); // 显然不等
      }
      if( s.equal(t) ) {
      System.out.println("equal"); // 内容相等
      }

包和 Java 命名空间

  • 声明包:package org.apache.commons.net;

    package 指定类属于哪个包 (Java 代码的第一行标记,除注释和空格外)

  • 导入类型

    1
    2
    3
    4
    5
    6
    // 现不用输入 java.io.File 了,输入 File 定义即可
    import java.io.File;
    File file = new File();

    // java.io 包中的所有类都可以使用简称
    import java.io.*;
  • 导入静态成员

    1
    2
    import static java.lang.Math.*;
    Math.sqrt( abs(sin(x)) ); // 直接使用 sqrt( abs(sin(x)) );
  • Java 文件结构

    • 一个 可选的 package 指令;
    • 零个或多个 import 或者 import static 指令;
    • Java 文件中 只有一个 声明 public 的类,且类名必须与文件名相同.

      public 类的目的是供其他包中的类使用。不管类是否为 public,一个文件只定义一个类,并且名称相同,是良好的编程习惯。

叁 Java 面向对象编程

面向对象的基本概念

    • 由一些保存值的数据字段和处理这些值的方法组成。
    • 类定义一种新的引用类型。

      例如:Point 类,表示所有笛卡尔二维坐标点。

  • 对象:类的实例,对象一般通过实例化类来创建。

定义类的句法

1
2
3
4
public class IntegerNumber extends Number
implements Serialzable, Comparable {
// 类的成员 (成员变量和成员方法)
}

字段和方法

  • 类字段
  • 类方法

    类字段、类方法:关联在类自身上的类成员 (静态变量)。

  • 实例字段
  • 实例方法

    实例字段、实例方法:关联在类的单个实例 (对象) 身上的实例成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Circle {
    // 类字段
    public static final double PI = 3.14159;

    // 类方法
    public static double radiusToDegrees(double radius) {
    return radius * 180 / PI;
    }

    // 实例字段,即实例化后获得参数
    public double r;

    // 实例方法
    public double area() {
    return PI * r * r;
    }
    }
  • this 引用的工作方式

    只要 Java 方法在类中访问 实例字段,都默认方法 this 参数指向的对象中的字段。

    1
    2
    3
    4
    5
    double radius;

    public void setRadius(double radius) {
    this.radius = radius; // 把参数赋值给类字段
    }
    • 一般地,若实参变量名称与类字段不一致,可省略 this,例如形参为 double r,即 radius = r 。在该例子中省略会让编译器报错,即无法理解值是谁赋给谁。
    • 实例方法可以使用 this 关键字,相反类方法不能使用。

创建和初始化对象

  • 定义构造方法

    1
    2
    3
    4
    5
    6
    public class Circle {
    protected double r;
    public Circle(double r) {
    this.r = r;
    }
    }
    • 构造放方法的成名始终和类名一样。
    • 声明构造方法时不指定返回值类型,连 void 都不用。
  • 定义多个构造方法

    只要构造方法的参数列表不同,为一个类定义多个构造方法是可以的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Circle {
    protected double r;
    public Circle() {
    r = 1.0;
    }
    public Circle(double r) {
    this.r = r;
    }
    }
  • 字段的默认值和初始化程序

    类中的字段不一定要初始化,因为字段自己使用默认值初始化。

    良好的编程习惯,或字段的默认值不适合情景。建议显式提供初始值。

子类的继承

扩展类

1
2
3
4
5
6
7
8
9
10
11
public  class PlanCircle extends Circle { // 自动继承了 Circle 类的字段和方法
private final double x, y;
public PlanCircle(double r, double x, double y) {

// 调用超类的构造方法 Circle()
super(r);

this.cx = x;
this.cy = y;
}
}

构造方法链和默认构造方法

  • 创建类的实例,Java 保证会调用这个类的构造方法。
  • 创建任何子类的实例,Java 保证会调用超类的构造方法。
  • 若构造方法没有调用超类的构造方法,Java 会隐式调用。
  • 若类没有声明构造方法,Java 会为类隐式添加一个构造方法。

    若超类没有声明无参数的构造方法,这种隐式调用会导致编译出错。所以,若类中没有定义任何构造方法,默认会为其提供一个无参数的构造方法。

覆盖超类的方法

注意,覆盖不是遮盖

类中定义的某个实例方法和超类的某个方法有相同 名称返回类型参数,那么这个方法会覆盖,即 @Override

数据隐藏和封装

  • 封装:把数据隐藏在类中,只能通过方法获取。
    • 隐藏类的内部细节。
    • 保护类,如一些相互依赖的字段。
  • 访问控制

    • 访问包
      1) 访问控制一般在类和类的成员这些层级完成。
      2) 访问其他包,import 导入相关包即可。
    • 访问类:默认情况,顶层类在定义它的包中可以访问。
    • 访问成员:类的成员在类的主体里始终可以访问。

      public、private、protected (default) 作为修饰符,修饰类的成员。
      public => 类的任何地方都可访问这个成员。
      private => 除了类内部能访问。
      protected => 包里的所有类都能访问这个成员,只限制在同一包内进行访问。

    • 访问控制和继承

      • 使用 private 声明的字段和方法绝不会被继承,类字段和类方法也一样。
      • 构造方法不会被继承,而不是链在一起调用。
      • 子类和超类
        1) 同一包中,子类继承所有没使用 private 声明的实例字段和方法。
        2) 不同包中,子类继承所有使用 protectedpublic 声明的实例字段和方法。

        不继承类字段、类方法。

抽象类和方法

  • 类中有一 abstract 方法,则该类为 abstract,若是 final 关键字声明的类不能有任何 abstract 方法。
  • abstract 类无法实例化。

转换引用类型

  • 对象不能转换不相关的类型。

    String 对象 -> Point 对象

  • 对象可以转成超类类型,或任何祖先类型。即放大转换,因此不需要校正。

  • 对象可以转换成子类型,缩小转换,但需确保转换有效。

修饰符总结

修饰符 用于 意义
abstract 这个类不能实例化,且可能包含未实现的方法
接口 所有接口都是抽象的,声明接口时这个修饰符是可选的
方法 这个方法没有主体,主体由子类提供,签名后面紧接一个分号。所在的类必须也是抽象的
default 方法 这个接口方法的实现是可选的。
final 不能创建这个类的子类
方法 不能覆盖这个方法
字段 这个字段的值不能改变
变量 值不能改变的局部变量、方法参数或异常参数
无(包) 包级私有:没有声明 public 的类只能在包中访问
接口 包级私有:没有声明 public 的接口只能在包中访问
成员 包级私有:没有声明 public、private 或 protected 的成员只能在包中访问
private 成员 该成员只在定义它的类中可以访问
protected 成员 该成员只在定义它的包中和子类中可以访问
public 能访问所在包的地方都能访问这个类
接口 能访问所在包的地方都能访问这个接口
成员 能访问所在类的地方都能访问这个成员
static 使用 static 声明的内部类是顶层类,而不是所在类的成员
方法 static 方法是类方法,不隐式传入 this 对象引用,可通过类名调用
字段 static 字段是类字段,不管创建多少类实例,这个字段只有一个实例,可通过类名访问
初始化程序 这个初始化程序在加载类时运行,而不是创建实例时运行
synchronized 方法 这个方法对类或实例执行非原子操作,故得确保不能让两个线程同时修改类或实例。对 static 方法来说,执行方法前先为类获取一个锁;对非 static 方法来说,会为具体的对象实例获取一个锁
volatile 字段 该字段能被异步线程访问,因此必须对其做些特定的优化

肆 Java 类型系统

Java 是一种静态语言,如果把不兼容型的值赋给变量,会导致编译出错。而在运作时检查类型兼容性的语言叫做动态类型语言,如 Javascript。

第壹章:Java 和其他语言比较 中有对语言类型的深入了解。

接口

  • 接口的作用只是描述 API,接口提供类型的描述信息,以及实现这个 API 的类应提供的方法 (和签名)。
  • Java 的接口不为它描述的方法提供实现代码,这些方法是强制要实现的。

定义接口

1
2
3
4
5
interface Centered {
void setCenter(double x, double y);
double getCenterX();
double getCenterY();
}
  • 特别说明

    • 接口中所有强制方法都隐式使用 abstract 声明,不能有方法主体,以分号结束。
    • 接口定义公开的 API,接口所有成员都隐式使用了 public 声明。

      使用 protected 或 private 定义方法,将会编译出错。

    • 接口不能实例化,因此不定义构造方法。

    • 接口中可包含嵌套类型。
    • 接口中可包含静态方法。

扩展接口

继承父接口的所有方法和常量,且可定义新方法和常量。接口的 extends 子句可包含多个父接口。

1
2
3
interface Transformable extends Scalable, Translateble, rotalable {
// 忽略细节
}

注意:实现这个接口的类必须实现这个 接口直接定义 的抽象方法,包括所有 父接口 中继承的全部抽象方法。

实现接口

实现多个接口:一个类即可实现一个接口,也可实现多个接口,后者表明的这个类要实现所有接口中的全部抽象方法 (强制方法)。

1
2
3
4
public class SuperDuperSquare extends Shape 
implements Centered, UpperRightCornered, Scalable {
// 忽略细节
}

默认方法

  • 向后兼容性:前一版平台编写 (或已编译) 的代码在最新版平台中必须能继续使用。
  • 实现默认方法:若升级某一接口,重新定义接口后,尝试在为旧接口编写的代码中使用这个新接口,不会成功。
    即抛出 NoClassDefError 异常,如下例中添加新的强制方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    interface Positionable extends Centered {
    void setUpperRightCorner(double x, double y);
    double getUpperRightX();
    double getUpperRightY();

    // 在此接口增加以下强制方法,是不会成功的。后续的解决办法是使用抽象类
    void setLowerLeftCorner(double x, double y);
    double getowerLeftX();
    double getowerLeftY();
    }

Java 泛型

介绍泛型

  • 使用泛型增强程序的安全性,使编译时信息避免简单的类型错误。具体以下述的引例展开学习。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    List shapes = new ArrayList();
    shapes.add( new CenteredCircle(1.0, 1.0, 1.0) );
    shapes.add( new CenteredSquare(2.5, 2, 3) );

    // list::get() 返回 Object 对象,想得到 CenteredCircle 对象必须校正
    CenteredCircle circle0 = (CenteredCircle) shapes.get(0);

    // 运行此代码时会导致运行失败
    CenteredCircle circle1 = (CenteredCircle) shapes.get(1);

    CenteredCircle circle1 = (CenteredCircle) shapes.get(1); 运行失败的原因,即把不同类型的对象放在同一容器中,一切正常运行。但若做了不合法的校正,程序就会崩溃。

    为了解决此类问题,Java 提供了一种句法,即指明某中类型是一个容器,这个容器中保存着其他引用类型的实例。容器中保存的 负载类型 在尖括号中指定:

    1
    2
    3
    List<CenteredCircle> shapes = new ArrayList<CenteredCircle>();
    shapes.add( new CenteredCircle(1.0, 1.0, 1.0) );
    shapes.add( new CenteredCircle(2.5, 2, 3) );
  • 容器类型,一般叫泛型

    1
    2
    3
    4
    interface Box<T> {
    void box(T t);
    T unbox();
    }

泛型和类型参数

  • <T> 句法,也称 类型参数,因此泛型还有一个名称 参数化类型
  • 定义有参数的类型,要使用一种不对类型参数做任何假设的方式指定具体的值。且类型参数可在方法的签名和主体中使用,就像真正的类型一样。

    1
    2
    3
    4
    interface List<E> extends Collection<E> {
    boolean add(E e);
    E get(int index);
    }

菱形句法

使用菱形句法创建一个由 CenteredCircle 对象组成的 List。

1
2
// 后一个 <> 内为空,但编译器能推导出类型参数的值
List<CenteredCircle> shapes = new ArrayList<>();

类型擦除

  • Java 平台十分看重向后兼容性,问题的关键是,如果让类型系统既能使用旧的非泛型集合类又能使用新的泛型集合类,则设计者们选择 校正 的解决方案。

    1
    2
    3
    List someThings = getSomeThing();
    // 这种校正不安全,即使 someThings 的内容确定是字符串
    List<String> myStrings = (List<String>) SomeThings;

    上述代码表明,ListList<String> 是兼容的,Java 通过类型擦除实现这种兼容性。

  • 类型擦除机制还能禁止使用某些其他定义方式。

    1
    2
    3
    4
    interface OrderCounter {
    int totalOrder( Map<String, List<String>> orders );
    int totalOrder( Map<String, Integer> orders );
    }

    上述代码看似合法,其实是无法编译的。其实当擦除类型后,两方法的签名变成 int totalOrder(Map);,Java 语言规范把这种句法列为不合法的句法。

通配符

  • 受限通配符,限制类型参数的值能使用哪些类型。

    • 类型协变:表示容器类型之间负载类型之间 具有 相同 的关系,这种关系通过 extends 关键字表示。
    • 类型逆变:表示容器类型之间负载类型之间 具有 相反 的关系,这种关系通过 super 关键字表示。

      [实例] 例如,Cat 类 扩展 Pet 类,List<Cat>List<? extends Pet> 的子类型。List 是 Cat 对象的制造者,应使用关键字 extends。

      Joshua Bloch 把这种用法总结为 PECS. Producer Extends, Consumer Super 原则,即使用者使用 super,制造者使用 extends。

  • 使用和设计泛型

    • 使用者:理解类型擦除的基本知识。
    • 设计者:泛型更多功能。如通配符、”Capture of” 错误信息等。

枚举和注解

枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public enum RegularPolygon {
// 带参数的枚举必须使用分号
TRIANGLE(3), SQUARE(4), PENTAGON(5), HEXAGON(6);

private Shape shape;

public Shape getShape {
return this.shape;
}

// 因枚举实例在 Java 运行时创建,在外部不能实例化,故把构造方法声明为私有。
// 只能有一个私有的构造方法 (或默认访问权限,即不写)。
private RegularPolygon(int sides) {
switch(sides) {
case 3: // 三角形
shape = new Triangle(1, 1, 1, 60, 60, 60);
break;
case 4: // 矩形
shape = new Rectangle(1, 1);
break;
case 5: // 五边形
shape = new Pentagon(
1, 1, 1, 1, 1, 108, 108, 108, 108, 108);
break;
case 6: // 六边形
shape = new Hexagon(
1, 1, 1, 1, 1, 1, 120, 120, 120, 120, 120, 120);
break;
}
}
}

// 实际使用
RegularPolygon polygon =
new RegularPolygon( RegularPolygon.TRIANGLE );
  • 注意
    • 枚举不能泛型化,不能被扩展;
    • 可以实现接口;
    • 只能有一个私有的构造方法 (或使用默认访问权限,不写).

注解

  • 注解是一种特殊的接口,其作用是注解 Java 程序的某个部分。
  • 能为编译器和集成环境 (IDE) 提供有用的提示。
  • 注解没有直接作用,例如 @Override 为注解的方法提供额外信息,注明这个方法覆盖了超类中的方法。
  • Java 平台中常见的基本注解:
    • @Override - 注明方法是覆盖的。
    • @Deprecated - 注明方法已经废弃了。
    • @SuppressWarnings - 注明编译器生成的警告。
    • @SafeVarargs - 为变长参数方法提供增强的警告静态方法、默认方法功能
    • @FunctionalInterface - 接口是一正确的函数式接口,注解能够更好地让编译器进行检查。

自定义注解

使用 @interface 关键字定义新的注解类型,与定义类和接口的方式差不多。

自定义注解的关键是使用 元注解,他们是用来注解新注解类型的定义,必须使用两个基本元注解 @Target@Retention

  • @Target:指明自定义的新注解能在 Java 源码的什么地方使用。可用的值在枚举 ElementType 中定义,其中包括:
    • TYPE - 类、接口 (包括注解类型) 或 Enum 声明
    • FIELD - 域声明 (包括 Enum 实例)
    • METHOD - 方法声明
    • PARAMETER - 参数声明
    • CONSTRUCTOR - 构造器声明
    • LOCAL_VARIABLE - 局部变量声明
    • ANNOTATION_TYPE - 注解
    • PACKAGE - 包声明
  • @Retention:指明 Javac 和 Java 运行时如何处理自定义的注解类型。可用的值得在枚举 RetentionPolicy 中定义,其中包括:
    • SOURCE - 表示注解将被编译器丢弃。
    • CLASS - 表示注解会出现在类文件中,但运行时 JVM 无法访问。
    • RUNTIME - JVM将在运行期也保留注释,因此可以通过反射机制读取注解的信息。
  • @Documented:将此注解包含在 Javadoc 中。
  • @Inherited:允许子类继承父类中的注解。

    便于理解,这里定义一个名为 @Nickname 的注解,使用这个注解为方法指定一个昵称,运行时使用反射可以找到这个方法。

    1
    2
    3
    4
    5
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Nickname {
    String[] value() default {};
    }

嵌套类型

  • 顶层类型:诸如类、接口和枚举类型都定义为顶层类型,即都是包中的直接成员。
  • 嵌套类型:也称为 内部类,不能作为完全独立的实体真实存在,类型通过四种不同方式嵌套在其他类型中。
  • 四种嵌套方式:

    • 静态成员类型:嵌套的接口,枚举和注解 (既使不使用 static 关键字)。
    • 非静态成员类型:没有 static 关键字声明,只有类才能作为非静态成员类型。
    • 局部类:Java 代码块中定义的类,只是这个块可见。
    • 匿名类:局部类,匿名类是不能有名字的类,在创建 new 语句来声明它们。

      1
      2
      3
      4
      5
      6
      7
      8
      Runnable runnable = new Runnable() {
      public void run() {
      // 忽略细节
      }
      }

      Thread thread = new Thread();
      runnable.start();
  • 嵌套类型的运作方式

    对于 Java 解析器而言,并没有所谓的嵌套类型,所有类型都是普通的顶层类。为了实现嵌套类型,Javac 把每个嵌套类型编译为 单独类文件,得到的类文件使用 特殊命名约定

    • 静态 / 非静态成员类型:以 EnclosingType$Member.class 格式命名成员类型的类文件。

      例如在 LinkedStack 类中,定义一个 Linkable 的静态成员接口。
      在编译这个 LinkedStack 类时,编译器会生成两个类文件,分别是 LinkedStack.class 和 LinkedStack$Linkable.class。

    • 匿名类:类文件的名称由实现细节决定,例如 Oracle 或 OpenJDK 中 Javac 使用数字表示匿名类的名称,即 EnclosingType$1.class.

    • 局部类:综合使用前两种方式命名,例如 EnclosingType$1Member.class

Lambda 表达式

列出目录中文件名以 “.java” 结尾的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
File dir = new File("/src");

// 调用 list() 方法,参数的值是匿名类实现的 FilenameFilter
String[] fileList = dir.list( new FilenameFilter() {
public boolean accept(File file, String str) {
return str.endsWith(".java");
}
});

// Lambda 表达式
String[] fileList = dir.list(
(file, str) -> {return str.endsWith(".java"); }
);
  • 转换 lambda 表达式:必须满足以下条件才算是合法的 Java 代码。
    • 必须出现在期望使用接口类型实例的地方;
    • 期望使用接口类型必须只有一个强制方法;
    • 该强制方法的签名要完全匹配 lambda 表达式).
  • 方法引用

    1
    2
    3
    4
    5
    6
    // 该接口只有一个非默认方法:
    // 该方法接受一个 MyObject 类型的参数,返回类型为 String
    (MyObject myobject) -> { myobject.toString(); }

    // 方法引用:Java 8 提供了更简洁的句法
    MyObject::toString;

伍 Java 的面向对象设计

Java 的值

Java 的值有两种类型,基本值和对象引用,只有这两种值才能赋值给变量。

  • 基本值:基本值不能改,2永远是2。
  • 对象引用:对象引用的内容一般能修改,一般为对象内容的变化。

    [说明] Java 不是 “引用传递” 的语言。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void manipulate(Circle circle) {
    circle = new Circle(3);
    }

    Circle circle = new Circle(2);
    manipulate(circle);

    // 还是输出 => Radius:2
    System.out.println("Radius:" + circle.getRadius() );

面向对象的设计要略

  • 常量:实现某个接口的任何类都会继承这个接口中定义的常量。特别是在多给类中使用的一组常量。
  • 高度抽象:选择接口还是抽象类
    • 在已定义的接口添加一个新的强制方法,那么已经实现这个接口的所有类都会出现问题,即接口中添加新方法必须为默认方法,并提供实现。
    • 抽象类,可放心添加非抽象方法。子类必须实现抽象方法,但非抽象方法不要求。
  • 实例方法还是类方法

    • 类方法:static 声明的静态方法。
    • 实例方法:关联在类的单个实例 (对象) 身上的实例成员。

      选择实例方法还是类方法,视设计方式决定,哪个方便来哪种。

  • 合成还是继承:可参考 装饰模式
  • 字段继承和访问器

    • protected 修饰字段,允许子类直接访问这些字段。
    • 提供访问器,即字段声明为私有,对外隐藏细节。

      1
      2
      3
      4
      private double radius;
      public double getRadius() {
      return radius;
      }
  • 单例 — 单例模式 — 只需要为类创建一个实例。更多设计模式可参考:

    Android 设计模式之实践与案例 ( 笔记 + 速记手册 )

异常和异常处理

  • 设计异常机制,应遵循下述良好的做法:

    • 异常也是对象,即考虑要在异常中存储什么额外状态;
    • Exception 类有四个公开构造。一般情况,自定义异常类需要实现这四个构造方法,可用于初始化额外的状态,定制异常消息;
    • 不要捕获异常而不处理;
    • 不要捕获异常,记录日志后再次抛出异常.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 不要捕获异常而不处理
      try {
      SomeMethodThatMightThrow();
      } catch(Exception e) {
      // 处理异常的细节
      }

      // 不要捕获异常,记录日志后再次抛出异常
      try {
      SomeMethodThatMightThrow();
      } catch(SpecificException e) {
      log(e);
      throw e; // 不要再异常处理中再抛出异常
      }

陆 Java 实现内存管理和并发编程的方式

Java 内存管理的基本概念

Java 中,对象占用的内存在不需要使用对象时会 自动回收 (自动内存管理)。即减少内存泄露的发生机率。

Java 中的内存泄露

既使自动回收机制减少了内存泄露的发生机率,但任然后发生内存泄露。

[例如] 某个方法运行时间很长 (或一直运行),那么这个方法中的局部变量会一致保存对象的引用。

1
2
3
4
5
6
7
public static void main(String args[]) {
int bigArray[] = new int[10000];
int result = compute(bigArray);

// 手动销毁引用
bigArray = null;
}

如果没有引用指向 bigArray,就会被垃圾回收。而 bigArray 是局部变量,在方法返回之前始终指向哪个数组,而 main() 并不会立即返回,而是直至程序运行结束,故我们要手动销毁引用。

标记清除算法

  • 分配表:JVM 确切知道它分配了哪些对象和数组,且这些对象数组存储 allocation table 在某种内部数据结构中。
  • 活性对象 / 可达对象
    • JVM 还能区分每个栈帧 (Stack frame) 里的局部变量指向堆 (Heap) 里哪个对象或数组,再者追踪堆中对象和数组保存的引用。
    • 在应用线程的堆栈追踪中,从其中一个方法的某局部变量开始,沿着引用链,最终能找到一个对象。若没有对象引用,则将其内存回收。

基本标记清除算法

回收垃圾的简单栈。

  • 垃圾回收过程经常使用的算法是标记清除,整个过程分三步:
    • 1) 迭代分配表:把每个对象标为 已死亡
    • 2) 从 指向堆的局部变量 开始,顺着遇到的 每个对象的全部引用 向下,遇到之前没有见过的对象或数组,就把它标为 存活,一直向下,直到找到能从 局部变量到达的所有引用 为止。
    • 3) 再次迭代分配表,回收所有没有标记为 存活 的对象在堆中占用的内存,再把这些内存放回可用可用内存列表中,最后把这些对象从分配表中删除。

JVM 优化垃圾回收的方式

引入:根据上述的情况,这一节将介绍一些优化措施。

  • 因为所有对象的内存由分配表分配,故用完堆内存之前会触发垃圾回收程序。
  • 垃圾回收程序需互斥存取整个堆,因应用代码一直运行,会不断创建和修改对象,导致结果腐化。
  • 避免发生此种情况,垃圾回收过程中,应用线程会停顿一下 (Stop-The-World,STW),执行垃圾回收,再恢复执行应用线程。

    开发者担心这种 STW,但针对大多数主流应用场景来说,Java 都运行在操作系统之上,进程会不断交替进出处理器内核,因此一般无需担心这些短暂的额外停顿。HotSpot 会做大量工作来优化垃圾回收,减少 STW 时间。

弱化假设

  • HotSpot JVM 有一 垃圾回收子系统,专门利用弱代假设。

    垃圾回收子系统:分代垃圾回收程序 (分别是新生代和老年代)。

  • 假设:对象的生命周期非常短 (瞬时对象),不久会被当作垃圾回收。然而有少量对象会存在久一些,因此注定会成为程序长期状态的一部分。

    :预期生命周期,即对象常常处于少数几个预期生命周期之一。

筛选回收

在标记清理算法的清理阶段,一个个回收对象,再把每个对象占用的空间放回可用内存列表。然而,若弱代假设成立,且在任何一个垃圾回收循环中大多数对象都已经 死亡,那么使用另一种方式回收空间似乎更合理。

新的回收方式把堆内存分成多个独立的内存空间,每次回收垃圾时,只为活性对象分配空间,并把这些对象移动另一个内存空间。这个过程称为筛选回收 (Evacuation)。

执行该过程的回收程序叫 筛选回收程序,其回收完毕后会清理整个内存空间,供以后重复使用。

使用筛选回收程序的话,每个线程都可以单独分配内存,即每个应用线程都有一块连续的内存 (线程私有的分配缓冲区),专供这个线程分配新对象。为新对象分配内存时,只需把指针指向分配缓冲区即可。

Java 对并发编程的支持

线程的作用是提供一个轻量级执行单元,属于 进程,进程的 地址空间 在组成该进程的 所有线程之间共享,即每个进程可独立调度,且有自己的栈和程序计数器 ( 会和 同一个进程中的其他线程共享内存和对象 )。

线程的生命周期

线程的生命周期

图 6-1 线程的生命周期
  • New:已经创建线程,但还没在线程对象上调用 start() 方法。所有线程一开始处于这个状态。
  • Runnable:线程正在运行,或当操作系统调度线程时可以运行。
  • Blocked:线程终止运行,因为它在等待获得一个锁,以便进入声明为 Synchronized 的方法或者代码块。
  • Waiting:线程终止运行,因为它调用了 Object.wait() 或 Thread.join() 方法。
  • Timed.Waiting:线程终止运行,因为它调用了 Thread.sleep() 方法,或者调用了 Object.wait() 或 Thread.join() 方法,而且传入超时时间。
  • Terminated:线程执行完毕,线程对象的 run() 方法正常退出,或抛出异常。

    [注意] Thread.sleep(2000);
    参数中指定的休眠时长是对操作系统的 请求,而不是 要求。休眠的时间可能比请求的长,具体休眠多久,取决于负载和运行环境相关的其他因素。

可见性和可变性

Java 应用线程都有自己的栈 (和局部变量),这些线程共用一个堆,因此可以较易在线程间共享对象,即需要做的只是把引用从一个线程传到另一个线程。

由此引入 Java 的设计原则:

  • 对象默认可见 (跨线程可见性):指向内存中的一个位置,而所有线程都共用同一地址空间。
  • 对象是可变的 (对象可变性):对象的内容 (实例字段的值) 一般都可修改。

    final 关键字把 变量或引用 声明为 常量,但这种字段 不属于对象的内容。

互斥和状态保护

互斥和状态保护 — 声明 synchronized 关键字

只要修改或读取对象过程中,对象的状态可能不一致,这段代码就要受到保护。假若一个方法包含一连串操作,若在操作过程中中断,就可能导致 某个对象处于不一致或非法状态。如 这个非法状态对另一个对象可见,代码的行为就会错乱

Volatile 关键字

应用代码使用字段或变量前,必须重新以主内存读取值。同理,修改使用 volatile 修饰的值,在写入变量之后,必须存回主内存。

Thread 类中常用的方法

  • getId():这个方法 返回线程的 ID 值,类型为 long 型,线程的 ID 在线程的整个生命周期中都不变。
  • getPriority()setPriority():控制线程的 优先级。线程的优先级使用 1 ~ 10 之间的整数 表示。
  • setName()getName():使用这两个方法设定或取回单个线程的名称。
  • getState():返回一个 Thread.state 对象,说明 线程处于什么状态
  • isAlive():测试线程是否还 “活着”。
  • start():创建一个新应用线程,然后调用 run() 方法调度这个线程开始执行。正常情况,执行到 run() 方法中的一个 return 语句后,线程结束运用。
  • interrupt()
    • 调用 sleep()、wait()、join() 阻塞了线程,在表示这个线程的 Thread 对象上再调用 interrupt(),会让线程抛出 InterruptedException 异常 (并把线程唤醒)
    • 线程中涉及可中断 I/O 操作,那么 I/O 操作会终止,线程会收到 closedByInterruptException 异常。既使线程没有从事任何中断操作,线程中断状态会被设为 true。
  • join():可理解为一指令,在其他线程结束前,当前线程不会继续向前运行。
  • setDaemon():一个线程是用户线程还是守护线程,由其决定。
    • 用户线程:只要线程还 “活着”,进程就无法退出。 (线程默认行为)
    • 守护线程:线程不阻止进程退出。
  • setUncaughtExceptionHandler():线程因抛出异常而退出时,默认的行为是打印 线程的名称异常的类型异常消息堆栈跟踪

    自定义处理程序,处理未捕获的异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 这个线程直接抛出一个异常
    Thread handledThread = new Thread(
    () -> {throw new UnsupportedOperationException();}
    );

    handledThread.setName("My Broken Thread"); // 给线程起名,便于调试
    handledThread.setUncaughtExceptionHandler(
    (t, e) -> { // 处理异常的程序
    System.out.println(
    "Exception in thread %d%s: %s at line %d of %s",
    t.getId(), // 线程 ID
    t.getName(), // 线程名称
    e.toString(), // 异常名称和消息
    e.getStackTrace()[0].getLineNumber(),
    e.getStackTrace()[0].getFileName();
    );
    }
    );
    handledThread.start();

使用线程

  • 监视器和锁的基本知识

    监视器,即 Java 平台会为它创建的每个对象记录一个特殊的标记。

    • 同步保护的是对象的状态和内存,不是代码。
    • 获取监视器只能避免其他线程再次获取这个监视器,而不能保护对象。
    • 接口中声明的方法不能使用 synchronized 修饰。
    • 内部类是语法糖,因此内部类的锁对外层类无效 (反过来亦然)。
    • 锁定 Object[] 对象,不会锁定其中的单个对象。
    • 基本类型的值不可变,因此不能锁定 (也无需锁定)。

总结

Java 的线程模型是基于三个基本概念:

  • 状态是共享的,可变的,而且默认可见。
  • 抢先式线程调度。
  • 对象的状态只能由锁保护。

柒 编程和文档约定

命名和大小写约定

  • :小写字母,常见做法是把公司的网站域名倒过来。

    例如:cn.kofes.javanutshell

  • 引用类型 的名称应大写字母开头,混用小写字母;若名称中有部分是 简称,则简称全大写。

    • 类和枚举类型,表示对象,名称多使用 名词。如 ThreadFormatConvertor
    • 接口,为实现这个接口的类提供额外信息,接口名称一般使用 形容词。如 RunnableCloneable
  • 方法名词 + 名词动词 + 名词 且遵循 驼峰式 命名规则。

    例如:ListenerCollection()、insertObjectA()

  • 字段和常量:声明为 static final 的常量,名称使用 全大写形式,若常量名词包含多个单词,单词之间应使用下划线分割。枚举类型定义的常量往往也全部使用大写字母。

  • 参数:尽量清楚表明参数作用的名称,尽量使用一个单词命名参数。

    例如:WidgetProcessor widget;ImageLoader image;

Java 文档注释

文档注释是普通的多行注释,即 /* 块注释的内容. */。文档包含简单的 Html 格式化标签,还可以包含其他特殊关键字,提供额外信息。

Javadoc 程序会把文档注释提取出来,自己转换成 Html 格式的在线文档。

1
2
3
4
5
6
7
8
9
/**
* 创建一个新 Complex 对象,表示复数 <i> x + y * i </i>
* @param x 复数的实部
* @param y 复数的虚部
*/
public Complex(double x, double y) {
this.x = x;
this.y = y;
}
  • 文档注释标签

    • @author name 声明作者,例如 @author BenEvans
    • @version text,声明版本信息,例如 @version 1.3.2,08/26/2017

      这个标签常和支持自动排序版本号的版本控制系统一起使用,如:git、son。

    • @param parameter-name description,声明参数信息。例如 @param circle Circle 类实例化的对象

    • @return description,声明返回信息。

      @return <code>true</code> 成功插入对象。
      @return <code>false</code> 列表中已包含要插入的对象。

    • @exception full-classname description@throw full-classname description,声明异常。例如 @exception java.io.FileNotFoundException 如果找不到指定的文件

  • 关于 Html 标签的使用说明

    • <i></i>:用于强调文字内容。
    • <code></code>:用于显示类、方法和字段的名称。
    • <pre></pre>:用于显示多行代码示例。

捌 使用 Java 集合

介绍集合 API

两种基本数据结构

  • Collection:对象的集合,如图 8-1 所示。
  • Map:对象间的一系列映射或关联关系,即 键值对,如图 8-2 所示。

Collection数据结构

图 8-1 Collection 数据结构

Map数据结构

图 8-2 Map 数据结构
  • 关于图 8-1 中,部分数据结构的说明:
    • SetListCollection。Set 没有重复对象,Set 实现都不会对元素排序;List 可能有重复,且其元素按顺序排列。
    • SortedSetSortedMap 是特殊的集合映射,其中的元素按顺序排序。

Collection 接口

  • Collection<E>,参数化接口,表示由泛型 E 对象组成的集合。
  • 该接口定义了很多方法,如:集合中添加、删除、遍历对象,测试对象是否存在集合中,集合中的元素转换成数组,返回集合大小。

Set 接口

  • 无重复对象组成的集合
    • 不可能有两个引用指向同一对象;
    • 不可能有两个指向 Null 的引用;
    • a.equals(b),即 a,b 两对象不能同时出现在集合中.
  • 多数通用的 Set 实现 都不会对元素排序,但并不禁止使用 有序集

    有序集:例如 SortedSet、LinkedHashSet。

  • 实现 Set 接口的类

内部表示 元素排序 成员限制 基本操作 迭代性能 备注
HashSet 哈希表 O(1) O(capacity)
LinkedHashSet 哈希链表 插入顺序 O(1) O(n)
EnumSet 位域 枚举声明 枚举类型的值 O(1) O(n) 枚举值不能为 null
TreeSet 红黑树 升序排列 可比较 O(lgn) O(n) 元素所需要的类型要实现 Comparable 接口或 Comparator 接口
CopyOnWriteArraySet 数组 插入顺序 O(n) O(n) 不使用同步方法也能保证线程安全

List 接口

  • List 是一组 有序 的对象集合。
  • 列表中的每个元素都有特定的位置,且 List 接口定义了一些方法,用于查询或设定特定位置 (或叫索引) 的元素。从这个角度看,List 对象和数组类似,不过列表的大小能按需变化
  • 和集不同,列表允许出现重复的元素。
  • 遍历循环和迭代,即依次处理每个元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ListCollection<String> c = new ArrayList<String>();
for( String word : c ) {
System.out.println(word); // 遍历循环
}

// for 循环迭代判断是否还有下一个值
for( Iterator<String> i = c.iterator(); i.hasNext(); ) {
System.out.println( i.next() );
}

Iterator<String> iterator = c.iterator();
while( iterator.hasNext() ) {
System.out.println( iterator.next() );
}
  • 实现 List 接口的类
表示方式 随机访问 备注
ArrayList 数组
LinkedList 双向链表 高效插入和删除
CopyOnWriteArrayList 数组 线程安全;遍历快;修改慢

Map 接口

  • 映射,一系列键值对,一个键对应一个值。
  • Map 是参数化类型,即 Map<k,v>k 表示映射中键的类型,v 表示键对应的值,如:Map<String, Integer>
  • 实现 Map 接口的类
表示方式 Null 键 Nul 值 备注
HashMap 哈希表
ConcurrentHashMap 哈希表 线程安全 (详情请参阅 JDK API)
ConcurrentSkipListMap 哈希表 线程安全 (详情请参阅 JDK API)
EnumMap 数组 键为何枚举类型
LinkedHashMap 哈希表 + 列表 保留插入或访问顺序
TreeMap 红黑树 按照键排序
IdentityHashMap 哈希表 比较使用 ==,而非 equal()
WeakHashMap 哈希表 不会阻止垃圾回收键

Queue、BlockingQueue 接口

  • 队列是一组有序的元素,提取元素时按顺序从对头读取。队列插入元素的顺序实现,可分类为:
    • FIFO:先进先出,队列。
    • LIFO:后进先出,栈。
  • 把元素添加队列中:
    • add():Collection 接口定义,常规的方式添加元素。对有界的队列来说,若队列已满,这个方法会抛出异常。
    • offer():Queue 接口中定义,若有界的队列已满,这个方法返回 false。
    • put():BlockingQueue 接口中定义,会 阻塞操作。即队列已满,而无法插入元素,put() 方法会一直等待,直至其他线程从队列中移除元素,有空间插入新元素为止。
  • 把元素从队列中移除:

    • remove()
      • Collection 接口定义,把指定元素从队列中移除。
      • Queue 接口中定义,则是没有参数的 remove(),移除并返回对头元素 (出队),若队为空,则抛出 NoSuchElementException 异常。
    • poll():Queue 接口中定义,移除并返回对头元素,若队列为空,则返回 null。

      BlockingQueue 接口中定义了超时版的 poll(),指定时间内等待元素添加到空队列中。

    • take():BlockingQueue 接口定义,用于删除并返回队头元素 (出队),若队为空,这个方法会等待,直到其他线程把元素添加到队列中为止。

    • drainTo():BlockingQueue 接口定义,把队列中的所有元素都移除,再把这些元素添加到指定的 Collection 对象中。这个方法不会阻塞操作。
  • 查询:就队列而言,查询时访问队头元素,但不将其从队列中移除。
    • element():Queue 接口中定义,作用时返回队头元素,但不将其从队列中移除,若队为空,则抛出 NoSuchElementException 异常。
    • peek():Queue 接口中定义,作用和 element() 类似,但队为空时返回 null。
  • 特别说明:
    • 如果想在操作成功前一直阻塞,应选 put() 与 take()。
    • 如果想检查方法返回值,应选 offer() 和 poll()。

实现方法

java.util.Collections 类定义了一些静态实用方法,用于处理集合。

  • 包装集合:把集合本身没有提供的功能绑定到集合上。即包装集合能提供的功能有:线程安全性、写保护和运行时类型检查。

    • 为包装集合提供线程安全性。
    • 不能修改底层集合,即得到的集合只读。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // 为包装集合提供线程安全性
      List<String> list = Collections.sychronizedList(
      new ArrayList<String>() );
      Set<Integer> set = Collections.sychronizedSet(
      new HashSet<Integer>() );
      Map<String, Integer> map = Collections.sychronizedMap(
      new HashMap<String, Integer>() );

      // 不能修改底层集合,即得到的集合只读
      List<Integer> primes = new ArrayList<Integer>();
      List<Integer> readonly = Collections.unmodifiableList(primes);
      primes.add( Arrays.asList(2, 3, 4, 5) ); // 正常
      readonly.add(23); // 抛出 UnsupportedOperationException 异常
  • 操作集合

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 二分查找前,必须使列表变有序
    Collections.sort(list);
    int pos = Collections.binarySearch(list, "key");

    // 把 list2 的元素复制到 list1,覆盖 list1
    Collections.copy(list1, list2);

    // 使用对象 obj 去填充 list
    Collections.fill(list, obj);

    // 找出集合 list 中最大、小的元素
    Collections.max(list);
    Collections.min(list);

    // 反转列表
    Collections.reverse(list);

    // 打乱列表
    Collections.shuffle(list);
  • 返回集合:若方法需返回一个集合,若返回值为空,建议返回空集合,代替返回 null。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    	Set<Integer> set_list = Collections.emptySet();
    List<String> list = Collections.emptyList();
    Set<String, Integer> map_list = Collections.emptyMap();
    ```

    #### 数组和辅助方法
    - 对象组成的数组和集合的作用类似,二者间可相互转换。

    ```Java
    String[] str = {"this", "is", "a", "sample"}; // 数组

    // 数组转换成大小不可变的列表
    List<String> list = Array.asList(str);

    // 创建一个大小可变的副本
    List<String> list2 = new ArrayList<String>(list);
  • Array 类还定义了一些静态方法

    1
    2
    3
    Array.sort(array); // 原地排序数组
    Array.binary(array, 7); // 在 Array 中找 7
    Array.equals(array1, array2); // 比较两个数组是否相等

玖 处理常见的数据格式

文本

字符串的特殊句法

  • 字符串字面量:Java 允许把一系列字符放在双引号中创建字面量字符串对象。

    1
    2
    String pet = "Cat";
    system.out.println("Dog",.length());
  • toString():作用是方便把任何对象转换成字符串。

  • 字符串连接:”StringA” + “StringB”。
    连接字符串时,先创建一个使用 StringBuilder 对象表示的一个工作区 (暂存区),其内容和原始字符串中的字符序列一样。然后更新 StringBuilder 对象,把另一个字符串中的字符源加到末尾。最后,在 StringBuilder 对象上调用 toString() 得到一个新字符串。

字符串的不可变性

String1 + String2 => StringBuilder对象 (暂存区) => toString(),则输出的字符串为 String1 + String2

正则表达式

  • 正则表达式,用于扫描和匹配文本的搜索模式。
  • Java 使用 Pattern 类表示正则表达式。

    ? 为元字符。

    1
    2
    3
    4
    5
    6
    7
    8
    Pattern p = Pattern.compile("honou?r");
    String caesarUK = "For Brutus is an honourable man.";
    Matcher mUK = p.matcher(caesarUK);

    String caesarUS ="For Brutus is an honorable man.";
    Mathcher mUS = p.matcher(caesarUS);
    System.out.println("Matches.UK Spelling?" + mUK.find());
    System.out.println("Matches.US Spelling?" + mUS.find());
  • 正则表达式元字符

元字符 意义 备注
? 可选字符出现0或1次
* 前一个字符出现0或多次
+ 前一个字符出现1或多次
{m, n} 前一个字符出现m到n次
\d 一个数字
\D 一个不是数字的字符
\w 一个组成单词的字符 数字、字母和 _
\W 一个不能组成单词的字符
\s 一个空白字符
\S 一个不是空白的字符
\n 换行符
\t 制表符
. 任意一个字符 在 Java 中不包括换行符
[] 方括号中的任意字符 叫作字符组
不在方括号中的任意一字符 叫作排除字符组
() 构成一组模式元素 叫作组 (捕获组)
或的符号 定义可选值 实现逻辑或
^ 字符串的开头
$ 字符串的末尾

工欲善其事,必先利其器。为更快速、准确上手正则,这些现成的、实用的表达式助你一臂之力。
技匠. 知道这20个正则表达式能让你少写 1,000 行代码. 2016. jianshu.com\(^{[2]}\)

数字和数学运算

Java 表示整数类型的方式

以 Java 的 Byte 类型为例,说明 Java 是如何表示整数的。Byte 类型的数字占8位,设定 Byte 类型数字的最高位用于表示正、负号。故有128个正数,128个负数。

1
2
3
4
5
6
byte b = 0b0000_0001; // 1
byte b = 0b0111_1111; // 127

// 负数是以补码形式存储的,而正数,原码、反码、补码一致
byte b = 0b1111_1111; // -1
byte b = 0b1111_1111; // -2

拾 处理文件和 I/O

网络

Java 对网络支持的核心 API 在 java.net 包中,其他扩展 API 则由 javax.net 包提供,尤其是 java.net.ssl

超文本传输协议 (HTTP)

  • URL 是关键的类,其原生支持 http://https://ftp://file:// 形式的 URL。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 下载指定 URL 对应页面的内容
URL url = new URL("http://www.baidu.com");
try( InputStream in = url.openStream() ) {
File.copy(in, Paths.get("output.txt"));
} catch( IOException ex ) {
ex.printStackTrace();
}

// 深入低层控制,可使用 URLConnection 类
try {
URLConnection conn = url.openConnection();
String type = conn.getContectType();
int length = conn.getContentLength();
InputStream in = conn.getInputStream();
} catch( IOException e ) {
// TODO
}
  • HTTP 定义了多个“请求方法”,客户端使用这些操作远程资源。这些方法有:GETPOSTHEADPUTDELETEOPTIONSTRACE
    • GET:用于取文档,不能执行任何副作用;
    • HEAD:与 GET 的作用一样,但不返回主体,用于检查 URL 对应的网址的页面是否有变化。
    • POST:把数据发给服务器处理。

传输控制协议 (TCP)

  • TCP具有下列特征 (特性)

    • 基于连接:数据属于单个逻辑流 (连接)。
    • 保证送达:三次握手,如果未收到数据包,会一直重新发送,知道送达为止。
    • 错误检查:能检测到网络传输导致的损坏,并自动修复。
  • Java 使用 Socket 和 ServerSocket 类表示 TCP

    • 例1:我们既要从客户端套接字中读取数据,也要把数据写入客户端套接字,且构建请求时要遵守 HTTP 标准 (RFC 2616)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      String hostname = "www.example.com";
      int port = 80;
      String filename = "/index.html";

      try(
      Socket socket = new Socket(hostname, port);
      BufferedReader from = new BufferedReader(
      new InputStreamReader( socket.getInputStream() )
      );
      PrintWriter to = new PrintWriter(
      new OutputStreamWriter( socket.getOutputStream() )
      );
      ) {
      to.print("GET" + filename +
      "HTTP/1.1\r\nHost:" + hostname + "\r\n\r\n");
      to.flush();

      for( String l = null; (l = from.readLine()) != null; ) {
      System.out.println(l);
      }
      }
    • 例2:在服务器端,可能需要处理多个连入连接。编写一个服务器的主循环,使用 accept() 从操作系统中接收一个新连接。随后,要迅速把这个新连接传给单独的类处理,好让服务器主循环继续监听新连接。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      // 处理连接的类
      private static class HTTPHandler implements Runnable {
      private final Socket socket;
      HTTPHandler(Socket client) {
      this.socket = client;
      }
      public void run() {
      try(
      Buffered in = new BufferedReader(
      new InputStreamReader( socket.getInputStream() )
      );
      PrintWriter out = new PrintWriter(
      new OutputStreamWriter( socket.getOutputStream() )
      );
      ) {
      out.print("HTTP/1.0 200 /r/nContent-Type:text/plain");
      String line;
      while( (line = in.readLine()) != null ) {
      if( 0 == line.length()) break;
      out.println(line);
      }
      } catch( Exception e ) {
      // TODO
      }
      }
      }

      // 服务器主循环
      public static void main(String[] args) {
      try{
      int port = Integer.parseInt( args[0] );
      ServerSocket ss = new ServerSocket(port);
      for(;;) {
      Socket client = ss.accept();
      HTTPHandler handler = new HTTPHandler(client);
      new Thread(handler).start();
      }
      } catch( Exception e ) {
      // TODO
      }

      }

互联网协议 (IP)

  • 传输数据的最低层标准,抽象了把自己从 A 设备移动到 B 设备的物理网络技术。
  • 与 TCP 不同,IP 数据包不能保证送达,在传输路径中,任何过载系统都可能会丢掉数据包。
  • Java 使用 DatagramSocket 类实现这种功能。

参考资料

[1] 周志明. 深入理解 Java 虚拟机(第2版)[M]. 机械工业出版社, 2013
[2] 技匠. 知道这20个正则表达式能让你少写 1,000 行代码. 2016. jianshu.com