温斯顿吴的个人博客 woojean.com

《Java编程思想》读书笔记

2017-04-05

第1章 对象导论

(略)

第2章 一切都是对象

在Java中,(几乎)一切都是对象,用来表示对象的标识符实际上是对对象的一个引用(区别于C++,C++需要兼容C,因此实际上是一种杂合性语言)。

String s;  // 创建了一个String引用,而非一个String对象

基本类型

使用new创建的对象存储在堆中,所以使用new创建小的、简单变量效率不高。对于这些小的、简单的基本类型,Java采取与C和C++相同的方式,即不使用new来创建,而是创建一个并非引用自动变量,这个变量直接将值存储在栈中,因此更加高效。

Java每种基本类型所占用的存储空间大小具有不可变性,即与平台无关。

共有如下基本类型:boolean(大小未指定,仅定义为能够取字面值true或false)、char(16位)、byte(8位)、short(16位)、int(32位)、long(64位)、float(32位)、double(64位)、void(未指定);所有数值类型都有符号。

// 计算字符串的字节数
int storage(String s){
    return s.length() * 2;
}

每种基本类型都有一个对应的包装器类型,使得可以在中创建一个非基本类型的对象用来表示对应的基本类型。基本类型和对应的包装器类型之间可以自动地互相转换:

Character ch = 'x';
char c = ch;

用于高精度计算的BigInteger和BigDecimal没有对应的基本类型。

Java确保数组会被初始化(对象数组,即引用数组初始化为null,基本类型数组初始化为零),而且不会被越界访问(通过额外的内存数据及运行时下标检查实现)。

销毁对象

作用域:由花括号定义,决定了在其内部定义的变量名的可见性生命周期

Java不支持在子作用域中定义与外部作用域同名的变量来屏蔽外部作用域的变量,所以如下的代码将会报错:

{
  int x = 1;
  {
    int x = 2; // Illegal
  }
}

Java垃圾回收器会监视用new创建的所有对象,并辨别出那些不再被引用的对象,随后释放这些对象的内存空间。如下:

{
  String s = new String('abc');
}
// s引用在这里已被销毁,但其之前所引用的对象在内存中仍然存在,直到被垃圾回收器回收

如果类的属性是基本数据类型,且没有被初始化,Java将会将其置为对应基本类型的默认值。(当变量作为类的成员使用时,Java才确保其初始化,对于没有被初始化的变量,在编译时会报错)

Java中的类无需前向声明就可以使用。

java.lang会被自动导入到每一个Java文件中,因此无需显式使用import语句导入。(其中包括System类,所以可以直接调用System.out.println(…),out是一个静态PrintStream对象)

类文件中必须存在某个类与该文件同名。

javadoc

javadoc基于Java编译器提供的功能来提取Java注释(查找特殊注释的标签)并生成文档,属于JDK安装的一部分。 (详略)

第3章 操作符

基本类型可以直接使用==和!=来比较是否相等,但是对于对象,==和!=实际比较的是对象的引用。如果想要比较对象的内容是否相等,应该使用equals()方法(equals的默认行为是比较引用,自定义的类通常需要覆盖equals方法)。

在Java中,不可以将一个非布尔值当做布尔值在逻辑表达式中使用。

直接常量

对于使用直接常量存在模棱两可的情况,可以添加一些标识来对编译器加以指导,如0x2f、200L、1F、2D等,详略。

Java不支持操作符重载(不同于C++、C#)。

类型转换

对于扩展类型转换(类型提升),编译器会自动进行,但是对于窄化类型转换(有数据丢失),编译器会强制要求显式进行类型转换。通常表达式中出现的最大的数据类型决定了表达式最终结果的数据类型。

布尔类型不能进行任何类型转换处理。类数据类型也不允许进行类型转换。对象可以在其所属类型的类族之间进行类型转换。

除了布尔类型以外,任何一种基本类型都可以通过类型转换变为其他基本类型。

对char、byte、short的任何算术运算,都会获得一个int结果(所以如果要赋值给原来类型的变量,需要进行窄化转换)。

第4章 控制执行流程

Java中唯一使用了逗号操作符(而不是逗号分隔符)的地方就是for循环的控制表达式,在控制表达式的初始化和步进控制部分可以使用一系列由逗号分隔的语句,这些语句均会独立执行:

for(int i = 1, j = i + 10; i < 5; j ++, j = i * 2){
    //...
}

Foreach

Foreach是一种更加简洁的应用于数组和容器的迭代语句,无需借助int变量就可以自动访问序列的每一项:

float f[] = new float[10];
// ... 
for(float x:f){
  // ...
}

goto及标签

goto在Java中是保留字,但是并未使用,取而代之的是用一种标签语法结合break和continue来实现跳转:

label1:
outer-iteration{  // 比如for,while循环
  inner-iteration{
    ...
    break;  // 中断inner-iteration 
    ...
    continue;  // 跳过inner-iteration的当前循环
    ...
    continue label1; // 中断inner-iteration和outer-iteration,跳转到label1处,继续迭代过程
    ...
    break label;  // 中断inner-iteration和outer-iteration,跳转到label1处,不再进入迭代
  }
}

标签必须直接定义在循环语句之前。

switch

Java中供switch进行判断的值必须是整数值,或者是生成整数值的表达式(不同于PHP、js可以是字符串)。如果想对非整数值进行switch调用,可以借助enum来实现。

第5章 初始化与清理

构造函数

class Rock{
  Rock(){  // 构造函数,没有返回值,没有访问控制,首字母大写
    System.out.print("Rock");
  }
}

方法重载

每个重载的方法(含构造方法)都必须有一个独一无二的参数类型列表(类型、数量、顺序)。不能根据方法的返回值来区分重载方法。

默认构造器

默认构造器即无参构造器,其作用是创建一个默认对象,如果类中没有写构造器,则编译器会自动创建一个默认构造函数。如果已经定义了一个构造器(无论是否有参数),编译器就不会自动创建默认构造器。

this

通过对象调用类实例方法时,编译器会自动把指向当前对象的引用作为第一个参数传递给实例方法。在实例方法内部可以使用专门的this关键字来访问这个参数。 在实例方法内部调用同一个类的另一个方法时不必使用this(当然也可以写),当前方法中的this引用会自动应用于同一类中的其他方法。

static

static方法就是没有this的方法,在static方法内部不能调用非静态方法(反过来可以)。 可以在没有创建任何对象的前提下仅仅通过类本身来调用static方法。

finalize()

垃圾回收器只知道释放那些通过new分配的内存,Java支持在类中定义finalize()方法来自定义一些对象回收行为。一旦垃圾回收器准备释放对象占用的存储空间,将会首先调用其finalize()方法,并且在下一次垃圾回收动作发生时才会真正回收对象占用的内存

finalize()不等于C++中的析构函数,因为其触发依赖于垃圾回收器,而垃圾回收器的执行取决于当前的内存消耗状况(而在C++中对象使用完成后一定会被销毁)。finalize()应该仅用于回收那些通过创建对象以外的方式分配的存储空间(比如native调用,即在Java中调用非Java代码)。

finalize()的另一个用法是用来对终结条件进行验证,即在finalize()中对对象被回收时应该处于的正确状态的判断。

垃圾回收器的工作方式

Java中除了基本类型外的所有对象都在堆上分配,Java虚拟机的工作方式使得Java从堆分配空间的速度可以和其他语言从栈上分配空间的速度相媲美。 垃圾回收器在回收空间的同时会使堆中的对象紧凑排列,因此堆指针可以很容易地移动到尚未被分配的内存区域。

常见的垃圾回收方式: 1.引用计数:简单、效率低、循环引用难处理; 2.停止-复制:需要暂停程序(所以不属于回台回收模式),需要有两个堆空间; 3.标记-清扫:如果希望得到连续的空间,需要重新整理剩下的对象; 实际Java虚拟机会进行监视,并根据当前的碎片情况采取不同的垃圾回收方式。

JIT

JIT即Just In Time,即时编译。把程序全部或者部分翻译成本地机器码(而非JVM字节码),以此提高程序运行速度。

成员初始化

Java类的所有成员变量都会在使用前得到初始化(如果定义时不初始化,基本类型成员会被置为零,对象成员会被置为null,且这些行为发生在构造函数被调用之前),对于方法中的局部变量,如果未被初始化则会发生编译时错误。

可以在定义类成员时通过调用类方法进行初始化,但是这种情况下对成员的定义顺序有要求:

public class MethodInit{
  int j = g(i); // 错误,i未定义
  int i= f();
  int f(){ return 1;}
  int g(int n){
    return n * 10;
  }
}

对象创建过程: 1.构造函数实际上是静态方法,当首次创建类的对象,或者调用类的静态方法、静态属性时,Java解释器会查找类的.class文件,并载入,这时有关静态初始化的所有动作都会执行(未赋初值的成员会被置为默认值)。静态初始化只在.class文件首次加载的时候执行一次,无论创建多少个对象,静态数据都只占用一份存储区域。 2.当通过new创建类的对象时,首先会在堆上为对象分配足够的存储空间(这块空间会被置0),然后执行所有类成员变量的初始化。 3.执行构造器。

Java中,static关键字不能应用于局部变量

静态块

可以将多个静态初始化动作组织成一个静态块,与其他静态初始化动作一样,这段代码仅会执行一次(当首次生成这个类的一个对象,或者首次访问属于该类的静态数据成员时)。

public class Spoon{
  static int i;
  static {  // 静态块
    i = 1;
  }
}

也支持实例初始化块,即把static去掉。

数组初始化

Java中,定义数组时,不允许指定数组的大小。

数组可以在定义的同时初始化:

int[] a1 = {1,2,3,4,5};
int[] a2 = new int[rand.nextInt(20)]; // [0,0,0,0...]

可变参数列表

static void printArray(Object... args){
  for(Object obj:args){
    System.out.print(obj + " ");
  }
}

//调用
printArray(1,2,3,4);
printArray(new A(), new A(), new A());
printArray((Object[])new Integer[]{1,2,3,4});
printArray();  // 0个参数

枚举

public enum Spiciness{
  NOT,MILD,MEDIUM,HOT,FLAMING
}

// 使用
Spiciness howHot = Spiciness.MEDIUM;

第6章 访问权限控制

Java中访问权限控制的等级从最大权限到最小权限依次为:public、protected、包访问权限(没有关键字)、private。 注意:protected权限比包访问权限大。

编译单元

每一个后缀名为.java的Java源代码文件被称为一个编译单元,每个编译单元内最多只能有一个public类(可以没有,此时文件可以随意命名),且该public类的名称必须与文件的名称相同(包括大小写)。 当编译一个.java文件时,在.java文件中的每个类都会有一个后缀为.class的输出文件。Java可执行程序由一组打包并压缩为Java文档文件(jar)的.class组成,Java解释器负责这些文件的查找、装载和解释。 类库实际就是一组由package组织起来的类文件。

Java解释器的运行过程

解释器获取包的名称,并将每个点号(.)替换为反斜杠(\),由此得到一个相对路径名(所以包的名称必须与其目录结构相对应)。然后根据CLASSPATH的配置(包含一个或多个目录),得到一个或多个绝对路径名,解释器就在这些绝对路径中查找与要创建的类名称相关的.class文件。(解释器还会去查找某些涉及Java解释器所在位置的标准目录)。 对于jar文件,在CLASSPATH中必须配置实际的jar文件的位置。一个示例:

CLASSPATH=.:D:\JAVA\LIB;C:\flavors\grape.jar

注意.目录被包含在内。

静态导入

可以不同过调用包名,直接使用包里的静态方法:

import static java.lang.System.out;
public static void main(String args[]){
  out.println("输出内容");
}

包访问权限

如果不提供任何访问权限修饰词,则默认为包访问权限。 处于相同目录都没有设定任何包名的文件将会被看作是隶属于该目录的默认包之中。

类的访问权限

类的访问权限不可以是private或者protected(内部类可以)。

如果没有为类指定访问权限,它就会默认地得到包访问权限,意味着该类的对象可以由包内任何其他类来创建(但是在包外不行)。但是,如果该类的某个static成员是public的话,则包外的类仍然可以调用该static成员(尽管不能new该类的对象)。

第7章 复用类

当创建一个类时,如果没有明确指出要从其他类中继承,则会隐式地从Java的标准根类Object继承。

如果想在子类的构造器中调用父类的带参数的构造函数,必须在子类构造函数中的一开始就调用(说明在创建类对象时,父类的构造函数先执行):

class Game{
  Game(int i){
    // ...
  }
}

class BoardGame extends Game{
  BoardGame(int i){
    super(i);   // 调用父类构造函数
    // ...
  }
}

@Override

在子类中定义与父类同名的方法,实际效果是新增了一个重载方法,而非像C++中那样屏蔽了父类的方法。如果需要像C++中那样覆盖父类中的方法,则需要使用与父类中方法相同的方法签名。可以使用@Override注解来注明想要覆盖(而非重载)父类的方法,如果实际效果是重载而非覆盖(方法签名不一致),编译器会生成错误信息。

final

对于基本类型,final使其数值恒定不变(可以使用字面值常量在编译时初始化,也可以使用运行时函数初始化,初始化后值不可变)。对于对象引用,final使其引用恒定不变(而不是引用所指向的对象不变)。

Java允许生成空白final属性,即声明为final,但不给定初值(留待构造函数进行初始化,实现根据对象不同而值不同)。

使用final修饰方法,将使得方法不能被子类覆盖(仍然可继承)。所有private方法都隐式地指定为final(如果在子类中定义了与父类中private方法同名的public方法,实际效果是在子类中新增了方法)。

将类定义为final意味着该类无法被继承。final类中的所有方法都被隐式地指定为final。

第8章 多态

多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。

向上转型

对象既可以作为它自己本身的类型使用,也可以作为它的基类型使用。

方法绑定

方法绑定即将一个方法调用同一个方法主体关联起来。在程序执行之前进行绑定,称为前期绑定。在运行时根据对象的类型进行绑定称为后期绑定(或者动态绑定、运行时绑定)。后期绑定基于对象中的类型信息实现。

Java中除了static方法和final方法,其他所有的方法都是后期绑定。

类属性不具有多态性,子类中定义的同名属性会覆盖父类中对应的属性。

构造器与多态

构造器实际是静态方法,因此不支持多态。 基类的构造器总是在子类的构造过程中被调用(因为子类不能访问基类的private成员,无法对其进行初始化),并且按照继承层次向上链接,以使每个基类的构造器都能得到调用。

对象初始化的过程:

  1. 将分配给对象的存储空间初始化为0;
  2. 调用基类构造器;
  3. 按照声明的顺序调用父类成员的初始化方法;
  4. 调用子类的构造函数主体;

在编写构造器时应该用尽可能简单的方法使对象进入正常状态,在构造器内唯一能够安全调用的方法是基类中的final方法。(因为普通方法都有多态行为,而在构造器中对象尚不完整)

协变返回类型

在子类中被覆盖的方法可以返回基类方法的返回类型的子类类型。

第9章 接口

抽象类和抽象方法

抽象类不能实例化。

抽象方法仅有声明而没有方法体:

abstract void f();

如果一个类包含一个或多个抽象方法,那么该类就必须为抽象类。

接口

接口实际是一个完全抽象的类,不提供任何具体实现(抽象类可以有部分具体实现)。

接口也可以包含字段,这些字段隐式地是static final的。

接口中的方法都是public的。

一个类可以实现多个接口。

接口可以extends其他接口。

接口可以嵌套在类或其他接口中,且可以被定义为private。

第10章 内部类

内部类可以访问其外围类的方法和字段,就像自己拥有它们似得(即无需使用任何前缀)。

使用内部类的最主要原因是:每个内部类都能够独立地实现一个接口,所以无论外围类是否已经实现了某个接口,对内部类都没有影响(内部类使得多重继承的解决方案变得完整)。

.this

如果想要在内部类中生成外部类对象的引用,需要在外部类名称上执行.this:

public class DotThis{
  void f(){
    System.out.println('Dot this !');
  }
    
  public class Inner{
    public DotThis outer(){
      return DotThis.this;  // 返回外部类对象的引用
    }
  }

  public Inner inner(){
    return new Inner();
  }

  public static void main(String[] args){
    DotThis dt = new DotThis();
    DotThis.Inner dti = dt.inner();
    dti.outer().f();
  }
}

.new

当想要创建某对象的外部类对象时,需要在该对象上执行.new操作:

public class DotNew{
  public class Inner{
    // ...
  }

  public static void main(String[] args){
    DotNew dn = new DotNew();
    DotNew.Inner dni = dn.new Inner();  // 不能使用dn.new DotNew.Inner()
  }
}

在拥有外部类对象之前不能创建内部类对象(因为内部类对象会连接到创建它的外部类对象上),但是如果创建的是静态内部类,就不需要对外部类对象的引用。

内部类与向上转型

将内部类定义为private,同时实现某个接口,并在外部类的公共方法中返回该内部类的对象(返回类型为接口),通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现细节(因为不能访问内部类的名字,所以甚至不能向下转型)。

局部内部类

可以在一个方法里或者任意的作用域内定义内部类,这样创建的内部类属于定义它的作用域,在作用域外不可见。

局部内部类不能有访问控制符,但是它可以访问当前代码块内的常量以及外围类的所有成员。

匿名内部类

public class Parcel{
  public Contents contents(){
    return new Contents(){  // 创建了一个继承自Contents的匿名类的对象(自动向上转型)
      private int i = 1;
      public int value(){
        return i;
      }
    };  // 分号(表达式结束)
  }

  public static void main(String[] args){
    Parcel p = new Parcel();
    Contents c = p.contents();
  }
}

如果在匿名内部类中引用了其外部定义的对象,编译器要求改对象必须是final的:

public class Parcel{
  public Destination destination(final String dest){  // 因为这个参数会在匿名内部类中使用,所以必须为final(此处如果传递的参数仅供Destination类做构造函数参数使用,则无需为final)
    return new Destination(){  
      private String label = dest;
      public String readLabel(){
        return label;
      }
    };  // 分号(表达式结束)
  }

  public static void main(String[] args){
    Parcel p = new Parcel();
    Destination d = p.destination("test");
  }
}

选择使用局部内部类而非匿名内部类的常见原因有:

  1. 需要一个已命名的构造器;
  2. 需要重载构造器;
  3. 需要不止一个该内部类的对象;

嵌套类

嵌套类即静态内部类,它与其外部类对象之间没有联系。 普通内部类不能有static数据,但是嵌套类可以有。 可以在接口中定义嵌套类。

闭包与回调

闭包是一个可调用的对象,它记录了一些来自于创建它的作用域的信息(内部类是面向对象的闭包)。

内部类的继承与覆盖

内部类标识符

内部类文件的名称为外部类名称+$+内部类名称:OuterClass$InnerClass.class 对于匿名内部类编译器会简单地产生一个数字作为其标识符。

第11章 持有对象

如果使用ArrayList来添加属于不同类的对象,编译会通过,但是会给出警告。且在从ArrayList中取出元素时,必须进行向下转换,因为取出来的元素是Object类型。 使用泛型方式ArrayList,添加非指定类型对象时将会编译报错。且取出的元素无需进行向下转型。

迭代器

Java中的Iterator只能单向移动,可执行的操作包括:

  1. 使用iterator()要求容器返回一个Iterator对象;
  2. 使用next()获得序列中的下一个元素;
  3. 使用hasNext()检查序列中是否还有元素;
  4. 使用remove()将最近返回的元素删除(即next()返回的最后一个元素,意味着使用remove()之前必须先调用next());

ListIterator

ListIterator继承自Iterator,只能用于各种List的访问,可以双向移动,可以返回前一个及后一个元素的索引,并且可以使用set()方法设置最近返回的元素。

Java容器类库

总体上所有数据结构实现了2个根接口:Collection、Map,独立于这2个根接口之外还有3个辅助根接口:Iterator、Comparable、Comparator。

Collection是所有列表类数据结构的接口,Map是所有映射类数据结构的接口,Iterator用于遍历一个序列,Collection可以生成这样的序列,而Map接口可以生成Collection(entrySet()、values())。: 所有实现Collection的数据结构都支持生成一个ListIterator接口,该接口是Iterator的子类。

Map -----生成-----> Collection -----生成-----> Iterator
                                                 ↑
                       ...      ---生成-----> ListIterator

Collection族的继承树:

Collection接口
    - List接口
       - ArrayList(标记了RandomAccess接口)
       -------------------- LinkedList(同时实现了List、Queue接口)
    - Set接口                 |
       - HashSet             |
         - LinkedHashSet     |
       - TreeSet             |
    - Queue接口              |
       - PriorityQueue      |
       ----------------------

除了TreeSet,其他Set都拥有与Collection完全一样的接口。 以上未包括Queue的concurrent实现。 新版本容器类库没有Stack,可以用LinkedList模拟(也没有Queue类)。

Map族的继承树:

Map接口
    - HashMap
        - LinkedHashMap
    - TreeMap

Comparable与Comparator可以互相生成。

不应该再使用过时的Vector、Hashtable、Stack等容器类。

第12章 通过异常处理错误

异常处理把在正常执行过程中做什么事的代码和出了问题怎么办的代码相分离。

所有标准异常类都有两个构造器:一个默认构造器和一个接收字符串作为参数的构造器:

throw new NullPointerException('x == null');

能够抛出任意类型的Throwable的对象,它是异常类型的根类。异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层次。

捕获异常

异常处理程序必须紧跟在try块之后,当异常被抛出时异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序(只有匹配的catch子句才能得到执行):

try{
    // ...
}
catch(Type1 e){
    // ...
}
catch(Type2 e){
    // ...
}
catch(Type3 e){
    // ...
}
finall{
    // 无论try块中是否抛出异常,这里都将执行,即使try中正常执行了return
}

如果在try块中执行System.exit(0);finally中的代码不会被执行。 finally块的语句在try或catch中的return语句执行之后返回之前执行且finally里的修改语句可能影响也可能不影响try或catch中return已经确定的返回值(取决于是值还是引用),若finally里也有return语句则覆盖try或catch中的return语句直接返回。 在finall中执行return,异常将丢失(极其糟糕)。

自定义异常

对异常类来说,最重要的就是类名。

class SimpleException extends Exception{}

public class Demo{
  public void f() throws SimpleException{
    throw new SimpleException();
  }

  public static void main(String[] args){
    Demo demo = new Demo();
    try{
        demo.f();
    }
    catch(SimpleException e){
        System.out.println('exception!');
    }
  }
}

异常声明

异常声明属于方法声明的一部分,描述了方法可能抛出的异常类型的列表:

void f() throws T1Exception,T2Exception{
    // ...
}

如果没有异常声明,就表示该方法不会抛出任何异常(除了RuntimeException,它们可以在没有异常声明的情况下被抛出)。 如果方法里的代码产生了异常却没有进行处理,编译器会提示:要么处理这个异常,要么抛出这个异常。

捕获所有异常

catch(Exception e){
    // ...
}

重新抛出异常

catch(Exception e){
    // ...
    throw e;
}

Java标准异常

所有异常都继承自Throwable,共有两种类型:Error和Exception。Error用于表示编译时错误和系统错误,通常不需要关心。

运行时异常的类型有很多,它们会被Java虚拟机自动抛出,所以无需在异常声明中罗列(这种异常属于编程错误)。如果RuntimeException没有被捕获而最终到达main(),那么在程序退出前将调用异常的printStackTrace()方法。

异常的限制

当覆盖方法的时候,只能抛出在基类方法的异常声明中列出的异常。

异常限制对构造器不起作用:子类构造器可以抛出基类构造器中没有的异常,但是子类构造器的异常声明必须包含基类构造器的所有异常声明。

派生类构造器不能捕获基类构造器抛出的异常。

第13章 字符串

String对象不可变

String对象是不可变的,String类中每一个看起来会修改String值的方法实际上都是创建了一个全新的String对象,并返回指向新的对象的引用。

Java不允许程序员重载操作符,但是自身重载了两个用于连接String对象的操作符=+=,当使用这两个操作符连接字符串时,编译器会自动地进行优化,最终使用StringBuilder的append()方法来构建新的字符串对象。但是当在循环体中使用+连接字符串时,实际优化出来的代码会在循环体内创建StringBuilder,意味着每循环一次就会创建一个StringBuilder对象。所以,当为一个类编写toString()方法时,如果字符串的操作比较简单,可以信赖编译器,但是如果要在toString()方法中使用循环,则最好自己创建一个StringBuilder对象。

与StringBuilder对应的线程安全的工具类是StringBuffer。

打印对象内存地址

如果想打印对象的内存地址,应该调用Object的toString()方法(即调用super.toString()),而不应该使用this,否则可能发生递归调用:因为编译器在遇到字符串+对象的时候,会调用对象的toString()方法:

public class InfiniteRecursion{
  public String toString(){
    // 会发生自动类型转换,进而发生递归,最终产生异常
    return "InfiniteRecursion address: " + this + "\n";
  }
}

String类的API

(略)

字符串格式化

System.out.println("Row 1:[" + x + " " + y + "]");  // old way 
System.out.format("Row 1:[%d %f]\n", x, y);
System.out.printf("Row 1:[%d %f]\n", x, y);

// 使用java.util.Formatter类
Formatter f = new Formatter(System.out);
f.format("Row 1:[%d %f]\n", x, y);
详略

// 使用String.format()对象
String s = String.format("Row 1:[%d %f]\n", x, y);

实际在String.format()内部也是通过创建Formatter类对象来实现格式化。

正则表达式

(略)

第14章 类型信息

Java主要有两种在运行时识别对象和类信息的方式:RTTI和反射。

RTTI

使用RTTI可以查询某个基类引用所指向的对象的确切类型。每当编译一个新类,就会产生一个Class对象(属于Class类,被保存在类的.class文件中)。所有类都是在对其第一次使用时(第一次引用类的静态成员,构造函数也是静态成员)动态加载到JVM中。类加载器会首先检查当前被引用类的Class对象是否已经被加载,如果没有,则会根据类名查找对应的.class文件(其中包含了Class对象),并加载Class对象。一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象

可以调用Class.forName()获取一个指定类名的类的Class对象的引用:

Class.forName("ClassName");

如果该类还没有被加载过,就加载它,在加载的过程中该类的static子句会被执行。

如果当前已经有了一个目标类的对象,则可以通过调用getClass()方法来获取Class对象的引用,这个方法定义在Object中。

Class类的API:

getName();           // 类的完全限定名(含包名)
getSimpleName();     // 简单类名
getCanonicalName();  // 和getName()一样
getInterfaces();     
getSuperclass();     // 返回直接基类
newInstance();       // 使用newInstance()来创建对象的类必须带有默认的构造器

类字面常量

可以不使用forName()方法,而直接使用类字面常量来获取对Class对象的引用:

ClassName.class;

使用这种方式的好处是在编译时就能够受到检查,因此不需要置于try语句块中。类字面常量还可以应用于接口、数组以及基本数据类型。

对于基本数据类型的包装器类,有一个TYPE字段,指向对应的基本数据类型的Class对象。即: int.class等价于Integer.TYPE

为了使用类而做的准备工作实际包含三个步骤:

  1. 加载:类加载器查找字节码,并从字节码中创建一个Class对象;
  2. 链接:验证类中的字节码,为静态域分配存储空间,解析这个类对其他类的所有引用;
  3. 初始化:初始化超类,执行静态初始化器和静态初始化块;

注意:当使用.class来创建对Class对象的引用时,不会自动地初始化该Class对象,而Class.forName()立即就进行了初始化。

如果一个static final域被用“字面值常量”初始化(即编译期常量),那么这个域无需对类进行初始化(意味着执行静态块)就可以被读取。否则仍然需要初始化。 对于非final的static域,总是要求在它被读取之前先进行链接和初始化。

泛化的Class引用

普通的类引用可以被重新赋值为指向任何其他的Class对象,而泛型类引用只能赋值为指向其声明的类型,所以通过使用泛型语法可以让编译器强制执行额外的类型检查。

Class<Integer> genericIntClass = int.class;
genericIntClass = Integer.class; // 与int.class一样
genericIntClass = double.class;  // 非法

注意泛型对子类型的限制,比如虽然Integer继承自Number,但是如下语句无法执行:

Class<Number> genericNumberClass = int.class;

因为Integer Class对象不是Number Class对象的子类。

泛型支持通配符:

Class<?> intClass = int.class;  // 与非泛型等效,但是更明确
intClass = double.class;

指定为特定类的子类:

Class<? extends Number> bounded = int.class;
bounded = double.class;  // OK

指定为特定类的超类:

Class<FancyToy> ftClass = FancyToy.class;
Class<? super FancyToy> up = ftClass.getSuperclass();
Object obj = up.newInstance();  // 将返回Object(因为无法确定是哪一个基类)

可以使用cast()方法进行引用转型:

class Building {}
class House extends Building {}
...
Building b = new House();
Class<House> houseType = House.class;
House h = houseType.cast(b);
h = (House)b;  // 效果一样

instanceof

if( x instanceof Dog){  // 只能与类型做比较,而不能与Class对象做比较
  ((Dog)x).bark();
}

Class.isInstance()

String s = new String("abcd");
System.out.println(String.class.isInstance(s)); // true

使用instanceof或Class.isInstance()进行判断时,会考虑类的继承关系,而使用Class对象进行比较时,没有考虑继承关系(父类型不等于子类型)。

反射

当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个类(和RTTI一样),然后加载那个类的Class对象(所以JVM必须能够获取该类的.class文件)。反射和RTTI之间真正的区别在于:对于RTTI,编译器在编译时打开和检查.class文件,而对于反射机制,.class文件在编译时是不可获取的,所以在运行时打开和检查.class文件。

import java.lang.reflect.*;

Class<?> c = Class.forName("...");
Method[] methods = c.getMethods();
Constructor[] ctors = c.getConstructors();
...

空对象

空对象是用来替代null的一种解决方案,空对象可以响应实际对象可以响应的所有消息(仍需要某种方式去测试其是否为空)。

绕过访问权限的操作

通过反射可以到达并调用所有方法,包括private方法(在Method对象上setAccessible(true))。

对于编译后发布的代码,可以执行:

javap -private C

列出包括private成员在内的所有成员(包括私有内部类)。 不过,通过反射修改final域实际是无效的(也不会抛出异常)。

第15章 泛型

泛型实现了类型的参数化

public class Holder<T>{
  private T a;
  public Holder(T a){
    this.a = a;
  }

  public void set(T a){
    this.a = a;
  }

  public T get(){
    return a;
  }

  public static void main(String[] args){
    Holder<Automobile> h = new Holder<Automobile>(new Automobile());
    Automobile a = h.get();  // 无需cast,取出来的类型就是正确的
    h.set(1); // error
  }
}

泛型接口

public interface Generator<T>{
  T next();
}

(略)

泛型方法

可以在类中定义参数化方法,且这个方法所在的类可以是泛型类,也可以不是泛型类。

public class GenericMethods{
  public <T> void f(T x){   // 定义泛型方法时,泛型参数列表置于返回类型之前
    System.out.println(x.getClass().getName());
  }

  public static void main(String[] args){
    GenericMethods gm = new GenericMethods();
    gm.f("");   // java.lang
    gm.f(1);
    gm.f(1.0);
  }
}

static方法无法访问其所在泛型类的类型参数,所以如果static方法需要使用泛型能力,必须将其定义为泛型方法。

返回类型为泛型参数的方法,实际将返回确切的类型。

类型参数推断

当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法时通常不必指明参数类型,编译器会自动找出具体的类型。所以,可以像调用普通方法一样调用泛型方法(就好像方法被多次重载过)。如果用基本类型调用泛型方法,自动打包机制会介入。

如果将一个泛型方法调用的结果作为参数传递给另一个泛型方法,这时编译器不会执行类型推断,编译器会认为调用泛型方法后其返回值被赋给了一个Object类型的变量。

在调用泛型方法时可以显式地指明类型:

f(New.<Person, List<Pet>>map());

泛型方法与可变参数

泛型方法可以结合可变参数使用:

public static <T>  List<T> makeList(T... args){
  ...
}

擦除

在泛型代码内部无法获得任何有关泛型参数类型的信息。擦除丢失了在泛型代码中执行某些操作的能力,任何在运行时需要知道确切类型信息的操作都无法进行。

public class Erased<T>{
  private final int SIZE = 100;
  public static void f(Object arg){
    if(arg instanceof T){             // error
      // ...
    }

    T var = new T();                  // error

    T[] array = new T[SIZE];          // error

    T[] array = (T)new Object[SIZE];  // Unchecked warning
  }
}

Java泛型使用擦除来实现,即在使用泛型时,任何具体的类型信息都被擦除了,因此List和List在运行时实际上是相同的类型:都被擦除成它们原生的类型,即List。

泛型类型参数将擦除到它的第一个边界(边界即使用extends对类型参数的范围做限制,可能会有多个边界),如下的边界:

<T extends HasF>

T将擦除到HasF,就好像在类的声明中用HasF替换了T一样。

Java的泛型之所以基于擦除来实现,是因为要兼容旧版本(Java1.0中没有泛型功能),因此泛型类型只有在静态类型检查期间才出现,在此之后程序中的所有泛型类型都将被擦除并替换为它们的非泛型上界。

有时必须通过引入类型标签来对擦除进行补偿,即显式地传递类型的Class对象以便在类型表达式中使用。

ClassTypeCapture<Building> ctt = new ClassTypeCapture<Building>(Building.class);

泛型数组

不能创建泛型数组,一般在任何想要创建泛型数组的地方都使用ArrayList。

边界

Java通过重用关键字extends实现在泛型参数类型上设置限制条件(可以是类或接口),从而实现强制规定泛型可以应用的类型。

默认extends了Object,即

等价于 ## 通配符 Java中的数组是协变的,也是不安全的: ```java Fruit[] fruit = new Apple[10]; // 数组是协变的,可以向上转型 fruit[0] = new Apple(); // OK fruit[1] = new Fruit(); // 编译不会报错,但运行时会报错,因为数组实际类型是Apple ``` 通配符的使用可以对泛型参数做出某些限制,使代码更安全。 通配符引用的是明确的类型(尽管其形式上类似普通边界可以接受一系列不同的类型)。 ```java List flist = new ArrayList(); flist.add(new Apple()); // 编译错误 flist.add(new Fruit()); // 编译错误 flist.add(new Object()); // 编译错误 flist.add(null); // 唯一可以添加的是 null ``` 需要注意的是,flist却可以调用contains和indexOf方法,因为在ArrayList的实现中,add()接受一个泛型类型作为参数,但是contains和indexOf接受一个Object类型的参数。 ## 无边界通配符 无边界通配符的使用形式是一个单独的问号:List,也就是没有任何限定。 List list 表示list是持有某种特定类型的List,但是不知道具体是哪种类型。`因为并不知道实际是哪种类型,所以不能添加任何类型`,这是不安全的。 ## 逆变 可以使用超类型通配符来定义泛型参数的下界: 甚至, 但是不能这样定义: ## 泛型的问题 任何基本类型都不能作为类型参数,因此不能创建类似ArrayList的变量。 由于擦除的原因,一个类不能实现同一个泛型接口的两种变体。 使用带有泛型类型参数的转型或instanceof不会有任何效果。 由于擦除的原因,仅泛型参数名不同的重载方法将产生相同的类型签名。 由于擦除的原因,catch语句不能捕获泛型类型的异常(其实泛型类也不能直接或间接地继承Throwable)。 ## 自限定的类型 不能直接继承一个泛型参数,但是可以继承在其自己的定义中使用了这个泛型参数的类: ```java class GenericType{} class CuriouslyRecurringGeneric extends GenericType{} ``` 泛型自限定就是要求在继承关系中将正在定义的类当做参数传递给基类: ```java class A extends SelfBounded{} ``` 自限定可以保证类型参数与正在被定义的类相同。 自限定类型的价值在于它们可以产生协变参数类型:方法参数类型会随子类而变化。 ## 混型 混型即混合多个类的能力,以产生一个可以表示混型中所有类型的类。在C++中可以使用多重继承实现混型,不过更好的方式是继承其类型参数的类。 在Java中常见的做法是使用接口来产生混型效果(装饰器模式)。 ## 潜在类型机制 “如果它走起来像鸭子,并且叫起来也像鸭子,那么就可以把它当做鸭子对待” (策略模式,略) # 第16章 数组 数组与其他容器的主要区别在三个方面:效率(更高)、类型(固定)、能够保存基本类型。 (基础知识,略) ## Arrays类 java.util.Arrays类提供了一套用于数组操作的静态方法: ```java equals() // 总数、对应位置元素都要相等 fill() sort() binarySearch() // 用于在已经排序的数组中查找元素 toString() hashCode() asList() // 接受任意的序列或数组作为其参数,并将其转变为List容器 ``` ## 复制数组 使用System.arraycopy复制数组要比用for快很多: ```java System.arraycopy(arr1,0,arr2,0,arr1.length); ``` ## 比较与排序 使用内置的排序方法就可以对任意的基本类型的数组进行排序。也可以对任意的对象数组进行排序,只要该对象实现了Compareable接口或具有相关联的Comparator。 # 第17章 容器深入研究 ## 填充容器 使用`Collections类`(不是Collection接口)的静态方法: ```java List list = new ArrayList(Collections.nCopies(4,new StringAddress("hello"))); // 4个指向同一个对象的引用 Collections.fill(list,new StringAddress("world!")); // 4个指向同一个对象的引用 ``` 所有Collection子类型都有一个接收另一个Collection对象的构造器,用所接收的Collection对象中的元素来填充新的容器。 享元模式,略。 Collection的功能方法,略。 List的功能方法,略。 ## Set对元素的要求 Set:元素必须实现equals()方法(因为需要唯一),Set接口不保证元素次序; HashSet:元素必须定义hashCode(); TreeSet:有次序的Set,元素必须实现Comparable接口; LinkedHashSet:使用链表维护元素(插入顺序),元素必须定义hashCode()方法; 虽然hashCode()只有在当前类元素被置于HashSet或者LinkedHashSet时才是必须的,但是对于良好的编程风格而言,应该在覆盖equals()方法时总是同时覆盖hashCode()方法。 ## 队列 队列在Java中仅有的两个实现是LinkedList和PriorityQueue,它们的差异在于排序行为而非性能。 LinkedList中包含支持双向队列的方法。 ## Map 标准Java类库中实现的Map有: HashMap:插入和查询的开销是固定的; LinkedHashMap:迭代遍历时,取得键值对的顺序就是其插入顺序或者最近最少使用次序; TreeMap:基于红黑树,是唯一带有subMap()方法的Map,是目前唯一实现的SortedMap; WeakHashMap:如果Map之外没有引用指向某个键,则此键可以被垃圾回收器回收; ConcurrentHashMap:线程安全的Map,无需同步加锁; IdentityHashMap:使用==代替equals()对键进行比较; hashCode()是Object中定义的方法,返回代表对象的整数值。 ## 正确的equals() HashMap使用equals()判断当前的键是否与表中存在的键相同,正确的equal()方法必须同时满足下列5个条件: 1. 自反性:x.equals(x)返回true; 2. 对称性:如果y.equals(x)返回true,则x.equals(y)也返回true 3. 传递性:如果x.equals(y)和y.equals(z)返回true,则x.equals(z)也返回true 4. 一致性:如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一致; 5. 对任何不是null的x,x.equals(null)一定返回false; 默认的Object.equals()只是比较对象的地址,`如果要使用自己的类作为HashMap的键,必须同时重载hashCode()和equals()`,否则无法正确使用各种散列结构。 实用的hashCode()必须速度快,并且有意义:基于对象的内容生成散列码,应该更关注生成速度而不是一致性(散列码不必是独一无二的),但是通过hashCode()和equals()必须能够完全确定对象的身份。好的hashCode()应该产生分布均匀的散列码。 例子,略。 ## HashMap的性能因子 可以通过为HashMap设置不同的性能因子来提高其性能: 1. 容量 2. 初始容量 3. 尺寸:当前存储项数 4. 负载因子:尺寸/容量,负载因子小的表产生冲突的可能性小。当负载情况达到负载因子水平时,容器将自动增加其容量:使容量大致加倍,并重新将现有的对象分布到新的位置。默认的负载因子是0.75 ```java HashMap(int initialCapacity, float loadFactor); ``` ## ConcurrentModificationException ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet都使用了可以避免ConcurrentModificationException的技术。 ## WeakHashMap java.lang.ref中包含了一组类用来为垃圾回收提供更大的灵活性:SoftReference、WeakReference、PhantomReference,它们都继承自Reference类。当垃圾回收器正在考察的对象只能通过某个Reference对象才能获得时(指对象被Reference对象所代理,且没有其他的引用指向该对象,是否要保留仅仅取决于当前的Reference对象),这些不同的Reference类为垃圾回收器提供了不同级别的间接性指示。 不同的Reference派生类对应不同的“可获得性”级别(由强到弱): SoftReference:用以实现内存敏感的高速缓存; WeakReference:用以实现“规范映射”而设计,不妨碍垃圾回收器回收映射(Map)的键或值; PhantomReference:用以调度回收前的清理工作,比Java终止机制更灵活。 WeakHashMap用来保存WeakReference,允许垃圾回收器自动清理键和值。对于向WeakHashMap添加键和值的操作,会被自动用WeakReference包装。 ## 已废弃的容器 Vector、Enumeration、Hashtable、Stack、BitSet # 第18章 Java I/O系统 ## File类 (略) ## 输入和输出 很少使用单一的类来创建流对象,通常会叠合多个流对象来提供所期望的功能(装饰器模式)。 ## InputStream和OutputStream 每一种数据源都有相应的`InputStream`子类,如FileInputStream、ByteArrayInputStream、StringBufferInputStream、PipedInputStream... 同样的,每一种输出类型也有相应的`OutputStream`子类。 ## FilterInputStream和FilterOutputStream FilterInputStream和FilterOutputStream是用来`提供装饰器类接口`以控制特定输入流和输出流的两个类。 FilterInputStream类型: 1. DataInputStream:与DataOutputStream搭配使用,可以从流读取基本数据类型; 2. BufferedInputStream:使用缓冲区; 3. LineNumberInputStream:跟踪输入流中的行号,可调用getLineNumber()和setLineNumber(int); 4. PushbackInputStream:具有能弹出一个字节的缓冲区,因此可以将读到的最后一个字符回退; FilterOutputStream类型: 1. DataOutputStream:可以按照可移植方式向流中写入基本类型数据; 2. PrintStream:用于产生格式化输出; 3. BufferedOutputStream:使用缓冲区; ## Reader和Writer InputStream和OutputStream的优势在于处理面向字节的I/O,Reader和Writer则提供兼容Unicode与面向字符的I/O功能。 当需要把来自字节层次结构中的类和字符层次结构中的类结合起来使用时,需要使用适配器类,如InputStreamReader可以把InputStream转换为Reader,OutputStreamWriter可以把OutputStream转换为Writer。 ## RandomAccessFile RandomAccessFile适用于由大小已知的记录组成的文件(可以调用seek()方法)。RandomAccessFile是一个独立的类(从Object派生而来)。 ## 缓冲方式读取文件 ```java public static String readFuc(String filename) throws IOException{ BufferedReader in = new BufferedReader(new FileReader(filename)); String s; StringBuilder sb = new StringBuilder(); while( (s = in.readLine()) != null ){ sb.append(s+ "\n"); // 需要加上换行符,因为readLine()会删除读出来的换行符 } in.close(); return sb.toString(); } ``` ## 从内存输入 ```java StringReader in = new StringReader(readFuc(filename)); int c; while((c = in.read()) != -1){ // read()以int形式返回下一个字节 System.out.print((char)c); } ``` ## 格式化的内存输入 ```java DataInputStream in = new DataInputStream( new ByteArrayInputStream( readFuc(filename).getBytes())); while(in.avaiable() != 0){ System.out.print((char)in.readByte()); } ``` DataInputStream的avaiable()方法返回在没有阻塞的情况下还能读取的字节数,对于静态文件,等于文件大小减去已读取的字节数,但是对于其他类型的流可能不是这样。 ## 输出文件 ```java BufferedReader in = new BufferedReader( new StringReader(readFuc(filename1))); PrintWriter out = new PrintWriter( new BufferedWriter(new FileWriter(filename2))); int lineCount = 1; String s; while( (s = in.readLine()) != null ){ out.println( lineCount++ + ":" +s ); // 写 } out.close(); // 会清空缓存 ``` PrintWriter提供格式化输出功能,这样输出的内容可以当做普通文本来处理。 ## 使用PrinterWriter输出文件的快捷方式 ```java BufferedReader in = new BufferedReader( new StringReader(readFuc(filename1))); PrintWriter out = new PrintWriter(filename2); // 接受文件名的构造函数 int lineCount = 1; String s; while( (s = in.readLine()) != null ){ out.println(lineCount++ + ":" +s ); } out.close(); ``` ## 使用DataInputStream和DataOutputStream存储和恢复数据 如果使用DataOutputStream写入数据,Java保证可以使用DataInputStream准确地读取数据(即使读和写是在不同的平台)。 ```java DataOutputStream out = new DataOutputStream( new BufferedOutputStream( new FileOutputStream("data.txt"))); out.writeDouble(3.14); out.writeUTF("abcde"); out.writeDouble(); out.write(1.41); out.writeUTF("hijkl"); out.close(); DataInputStream in = new DataInputStream( new BufferedInputStream( new FileInputStream("data.txt"))); System.out.println(in.readDouble()); System.out.println(in.readUTF()); System.out.println(in.readDouble()); System.out.println(in.readUTF()); ``` 对于字符串,能够恢复它的唯一可靠做法是使用UTF-8编码(使用writeUTF()和readUTF()),UTF-8是多字节格式,即其编码长度根据实际使用的字符集会有所变化。ASCII使用一个字节,非ASCII字符使用两到三个字节。字符串的长度存储在UTF-8字符串的前两个字节中。 但是,writeUTF()和readUTF()使用的是Java自定义的UTF-8变体,因此`如果用非Java程序读取用writeUTF()写的字符串时,必须编写一些特殊代码才能正确读取字符串`。 ## 随机访问文件 使用RandomAccessFile时,必须知道文件排版才能正确操作它。 ```java RandomAccessFile rf1 = new RandomAccessFile(filename,"rw"); for( int i = 0; i < 7 ; i++ ){ rf1.writeDouble(i*1.414); } rf1.writeUTF("The end of the file"); rf1.close(); RandomAccessFile rf2 = new RandomAccessFile(filename,"r"); for( int i = 0; i < 7; i++){ System.out.println("value "+ i + ":" + rf2.readDouble()); } System.out.println(rf2.readUTF()); rf2.close(); RandomAccessFile rf3 = new RandomAccessFile(filename,"rw"); rf3.seek(5*8); // 定位写入点,修改第5个双精度值 rf3.writeDouble(47.001); rf3.close(); ``` ## 标准I/O 标准I/O的意义在于:可以很容易地把程序串联起来,一个程序的标准输出可以成为另一个程序的标准输入。 Java提供System.in、System.out、System.err用于标准I/O。 System.out和System.err实际是被包装过的printStream对象,但是System.in却是一个没有被包装过的InputStream(意味着在读取之前必须对其进行包装)。 ## 回显输入的每一行 ```java BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in)); String s; while( (s = stdin.readLine()) != null && s.length()! = 0 ){ System.out.println(s); } ``` ## 标准I/O重定向 ```java PrintStream console = System.out; // 存储标准输出的引用 BufferedInputStream in = new BufferedInputStream( new FileInputStream("redirect.java")); PrintStream out = new PrintStream( new BufferedOutputStream( new FileOutputStream("test.out"))); System.setIn(in); System.setOut(out); System.setErr(out); BufferedReader br = new BufferedReader( new InputStreamReader(System.in)); String s; while( (s = br.readLine()) != null ){ System.out.println(s); } out.close(); System.setOut(console); // 恢复标准输出 ``` ## 进程控制 java.lang.ProcessBuilder,执行命令行。 ## 新I/O(nio) 新I/O类库的目的在于提高速度,实际上旧的I/O包已经被使用nio重新实现了。速度的提高来自于所使用的结构更接近于操作系统执行I/O的方式:通道和缓冲器。唯一与通道交互的缓冲器是ByteBuffer。ByteBuffer具有从其所容纳的字节中产生出各种不同基本类型值的方法。 (详略) ## 视图缓冲器 视图缓冲器支持通过某种特定的基本数据类型的视窗(各种类型的Buffer,如CharBuffer)查看其底层的ByteBuffer,ByteBuffer依然是实际存储数据的地方,对视图的任何修改都会映射成为对ByteBuffer中数据的修改。 ```java ByteBuffer bb = ByteBuffer.allocate(BSIZE); IntBuffer ib = bb.asIntBuffer(); ib.put(new int[]{1,2,3,4,5}); ... ``` ```java File System/Network -> FileInputStream/Socket getChannel() -> FileChannel read(ByteBuffer) -> ByteBuffer asIntBuffer() -> IntBuffer array()/get(int[]) -> int[] int[] -> wrap(int[]) -> IntBuffer ``` ByteBuffer以大端方式(高位优先)存储多字节数据。 ## 内存映射文件 内存映射文件允许创建和修改那些因为太大而不能放入内存的文件,通过使用内存映射文件可以假定整个文件都放在内存中,而且可以完全把它当作非常大的数组来访问。 ```java static int LENGTH = 0x8FFFFFF; // 128MB MappedByteBuffer out = new RandomAccessFile("test.dat","rw") .getChannel() .map(FileChannel.MapMode.READ_WRITE, 0, LENGTH); for(int i = 0; i < LENGTH; i++){ out.put((byte)'x'); } for(int i = LENGTH/2; i < LENGTH/2 + 6; i++){ printnb((char)out.get(i)); } ``` 映射文件的所有输出必须使用RandomAccessFile,而不能使用FileOutputStream。 ## 文件加锁 文件锁对其他的操作系统进程是可见的,因为Java的文件加锁直接映射到了本地操作系统的加锁工具。 ```java FileOutputStream fos = new FileOutputStream("file.txt"); FileLock fl = fos.getChannel().tryLock(); if( fl != null){ // 已加锁 ... fl.release(); // 释放锁 } fos.close(); ``` SocketChannel、DatagramChannel、ServerSocketChannel不需要加锁,因为它们是从单进程实体继承而来,通常不会在两个进程之间共享网络socket。 tryLock()是非阻塞的,lock()是阻塞的。 也可以只对文件的一部分上锁: ```java lock(long position, long size, boolean shared); ``` ## 对映射文件的部分加锁 (略) ## 压缩 GZIPOutputStream、GZIPInputStream; ZipOutputStream、ZipInputStream; (略) ## Java档案文件 jar文件实际是Zip文件。 一个jar文件由一组压缩文件构成,同时还有一张描述了所有这些文件的文件清单。 生成jar文件: ```java jar [options] destination [manifest] inputfiles jar cmf myJarFile.jar myManifestFile.mf *.class ``` 不能对已有的jar文件进行添加或更新操作(区别于zip)。 ## 对象序列化 只要对象实现了Serializable接口(一个标记接口),对象的序列化处理就会非常简单。要序列化一个对象首先要创建一个OutputStream对象,然后将其封装在一个ObjectOutputStream对象内,之后只要调用writeObject()即可将对象序列化,并将其发送给OutputStream(对象序列化是基于字节的,因此要使用InputStream和OutputStream继承层次结构)。 对象序列化能够追踪对象内所包含的所有引用,并保存那些对象,接着对对象内包含的每个这样的引用进行追踪(Java序列化机制用优化的算法自动维护整个对象网)。 可以通过实现Externalizable接口(代替Serializable)来对序列化过程进行控制,该接口包含两个方法writeExternal()和readExternal(),这两个方法会在序列化和反序列化还原的过程中被自动调用。 对于Serializable对象,完全以其存储的二进制位为基础来反序列化对象,而不会调用构造器。但是对于Externalizable对象,只会调用默认构造器,然后调用readExternal()。 ```java import java.io.*; import static net.mindview.util.Print.*; public class Blip3 implements Externalizable { private int i; private String s; // No initialization public Blip3() { print("Blip3 Constructor"); // s, i not initialized } public Blip3(String x, int a) { print("Blip3(String x, int a)"); s = x; i = a; // s & i initialized only in non-default constructor. } public String toString() { return s + i; } public void writeExternal(ObjectOutput out) throws IOException { print("Blip3.writeExternal"); // You must do this: out.writeObject(s); out.writeInt(i); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { print("Blip3.readExternal"); // You must do this: s = (String)in.readObject(); i = in.readInt(); } public static void main(String[] args) throws IOException, ClassNotFoundException { print("Constructing objects:"); Blip3 b3 = new Blip3("A String ", 47); print(b3); // 序列化 ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Blip3.out")); print("Saving object:"); o.writeObject(b3); o.close(); // 反序列化 ObjectInputStream in = new ObjectInputStream( new FileInputStream("Blip3.out")); print("Recovering b3:"); b3 = (Blip3)in.readObject(); print(b3); } } /* Output: Constructing objects: Blip3(String x, int a) A String 47 Saving object: Blip3.writeExternal Recovering b3: Blip3 Constructor Blip3.readExternal A String 47 *///:~ ``` ## transient 有时不希望序列化对象的敏感部分(比如子对象),这可以通过将类实现为Externalizable(这样可以阻止自动序列化行为),然后在writeExternal()内部只对所需部分进行显式的序列化。 如果只使用Serializable,为了能够予以控制,可以使用transient关键字逐个字段地关闭序列化。 ```java public class Logon implements Serializable { private Date date = new Date(); private String username; private transient String password; // 不会被自动保存到磁盘,反序列化时也不会尝试去恢复 public Logon(String name, String pwd) { username = name; password = pwd; } public String toString() { return "logon info: \n username: " + username + "\n date: " + date + "\n password: " + password; } public static void main(String[] args) throws Exception { Logon a = new Logon("Hulk", "myLittlePony"); print("logon a = " + a); ObjectOutputStream o = new ObjectOutputStream( new FileOutputStream("Logon.out")); o.writeObject(a); o.close(); TimeUnit.SECONDS.sleep(1); // Delay // Now get them back: ObjectInputStream in = new ObjectInputStream( new FileInputStream("Logon.out")); print("Recovering object at " + new Date()); a = (Logon)in.readObject(); print("logon a = " + a); } } ``` Externalizable对象在默认情况下不保存它们的任何字段,所以transient关键字只能和Serializable对象一起使用。 注意:对于Serializable对象,如果添加writeObject()和readObject()方法,在序列化和反序列化时就会使用它们而不是默认的序列化机制(即反序列化的时候会判断这两个方法是否存在)。 ```java private void writeObject(ObjectOutputStream stream) throws IOException; private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException; ``` 然而这两个方法并不是Serializable接口定义的一部分。 ## 带有对象引用的序列化 通过同一个ByteArrayOutputStream(还需经ObjectOutputStream装饰)重复写入的同一个对象,再通过同样的ByteArrayOutputStream(不同的流将生成不同的反序列化网)反序列化后得到的多个对象地址相同(但与序列化之前的对象的地址不同)。 ## XML JDK随包发布了javax.xml.*类库,书中选择使用开源的XOM库。 详略。 ## Preferences Preferences API用于存储和读取用户的偏好设置,可以自动存储和读取信息,但是只能用于小的、受限的数据集合,只能存储基本类型和字符串,并且每个字符串的存储长度不能超过8K。 ```java import java.util.prefs.*; ... // 也可以使用systemNodeForPackage() Preferences prefs = Preferences.userNodeForPackage( PreferencesDemo.class); prefs.put("Location", "Oz"); // 键值对 prefs.put("Footwear", "Ruby Slippers"); prefs.putInt("Companions", 4); prefs.putBoolean("Are there witches?", true); int usageCount = prefs.getInt("UsageCount", 0); usageCount++; prefs.putInt("UsageCount", usageCount); for(String key : prefs.keys()){ print(key + ": "+ prefs.get(key, null)); // 一定要提供默认值 } print("How many companions does Dorothy have? " + prefs.getInt("Companions", 0)); ``` 每次运行以上代码usageCount的值都会加1,但是并没有生成任何本地文件用来存储值信息。Preferences API利用合适的系统资源完成自动存储的任务,实际使用的资源随操作系统的不同而不同,例如在Windows上是存储在注册表中。 # 第19章 枚举类型 创建enum时会生成一个类,这个类继承自java.lang.Enum,且不可被继承,其他方面与普通类一样,比如可以包含方法。 枚举实例的API:ordinal()、compareTo()、equals()、getDeclaringClass()、name() 静态方法:values()、valueOf() ## 使枚举可以带描述信息 ```java public enum OzWitch{ // 必须先定义enum实例 WEST("..."), ... NORTH("...."); // 如果要添加方法,这里必须有分号 .... // 定义可以带参数的构造函数 private String description; private OzWitch(String description){ this.description = description; } // 获取描述信息的接口 public String getDescription(){ return description; } public static void main(String[] args){ for(OzWitch witch:OzWitch.values()){ print(witch + ":" + witch.getDescription()); } } } ``` 只能在enum定义的内部使用其构造器创建enum实例,所以上面代码中有意将构造函数定义为private。 也可以覆盖enum的toString()方法。 ## values() 虽然可以在枚举类型上调用values()方法,但是Enum类中实际并没有定义该方法。 `values()是由编译器添加的static方法`。(Enum的valueOf()方法带两个参数,只带一个参数的valueOf()方法也是编译器添加的 ) ## 使用接口组织枚举 无法从enum继承子类,实现接口是使其子类化的唯一方法:在一个接口的内部创建实现该接口的枚举,以此将元素进行分组。 ## EnumSet EnumSet用以替代传统的基于int的位标志(更具表达性),EnumSet中的元素必须来自一个enum,其内部将一个long值作为比特向量(即用一个long值的不同位的状态来表示某个元素是否存在),所以EnumSet非常快速高效。 ```java public enum AlarmPoints { STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3, OFFICE4, BATHROOM, UTILITY, KITCHEN } import java.util.*; ... EnumSet points = EnumSet.noneOf(AlarmPoints.class); // Empty set points.add(BATHROOM); print(points); points.addAll(EnumSet.of(STAIR1, STAIR2, KITCHEN)); print(points); points = EnumSet.allOf(AlarmPoints.class); points.removeAll(EnumSet.of(STAIR1, STAIR2, KITCHEN)); print(points); points.removeAll(EnumSet.range(OFFICE1, OFFICE4)); print(points); points = EnumSet.complementOf(points); print(points); ``` ## EnumMap EnumMap要求其中的键必须来自一个enum,其在内部通过数组实现,所以速度很快。 ```java interface Command { void action(); } ... EnumMap em = new EnumMap(AlarmPoints.class); em.put(KITCHEN, new Command() { public void action() { print("Kitchen fire!"); } }); ... for(Map.Entry e : em.entrySet()) { printnb(e.getKey() + ": "); e.getValue().action(); } try { // If there's no value for a particular key: em.get(UTILITY).action(); } catch(Exception e) { print(e); } ``` 命令模式,略。 ## 枚举实例的常量方法 可以为enum实例编写方法,从而为每个enum实例赋予各自不同的行为。具体方式是定义abstract方法,然后每个enum实例实现该abstract方法: ```java public enum ConstantSpecificMethod { DATE_TIME { String getInfo() { return DateFormat.getDateInstance().format(new Date()); } }, CLASSPATH { String getInfo() { return System.getenv("CLASSPATH"); } }, VERSION { String getInfo() { return System.getProperty("java.version"); } }; abstract String getInfo(); // 定义抽象方法 public static void main(String[] args) { for(ConstantSpecificMethod csm : values()) System.out.println(csm.getInfo()); } } ``` ## 多路分发 Java只支持单路分发,即如果要执行的操作包含了不止一个类型未知的对象时,那么Java的动态绑定机制只能处理其中的一个类型。 (详略) # 第20章 注解 通过使用注解可以将一些元数据保存在Java源代码中,并利用注解API来为自己的注解构造处理工具。 Java内置三种标准注解(定义在java.lang中): 1. @Override; 2. @Deprecated:使用该注解将会使编译器发出警告信息; 3. @SuppressWarnings:关闭不当的编译器警告信息; 此外还有4种元注解: 1. @Target:表示注解可以用于什么地方(构造函数、域、局部变量、方法、包、参数、类、接口、枚举) 2. @Retention:表示在什么级别保存注解信息(源代码、class文件、运行时) 3. @Documented:将此注解包含在Javadoc中 4. @Inherited:允许子类继承父类中的注解 注解也会被编译成class文件。 ## 定义、处理注解 示例:通过注解的方式跟踪项目中的用例(方法)实现情况 ```java import java.lang.annotation.*; // 定义注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface UseCase { public int id(); public String description() default "no description"; } // 使用注解 public class PasswordUtils { @UseCase(id = 47, description = "Passwords must contain at least one numeric") public boolean validatePassword(String password) { return (password.matches("\\w*\\d\\w*")); } @UseCase(id = 48) public String encryptPassword(String password) { return new StringBuilder(password).reverse().toString(); } @UseCase(id = 49, description = "New passwords can't equal previously used ones") public boolean checkForNewPassword( List prevPasswords, String password) { return !prevPasswords.contains(password); } } // 处理注解 public class UseCaseTracker { public static void trackUseCases(List useCases, Class cl) { for(Method m : cl.getDeclaredMethods()) { // 反射获取方法列表 UseCase uc = m.getAnnotation(UseCase.class); // 获取方法的注解 if(uc != null) { System.out.println("Found Use Case:" + uc.id() + " " + uc.description()); useCases.remove(new Integer(uc.id())); } } for(int i : useCases) { System.out.println("Warning: Missing use case-" + i); } } public static void main(String[] args) { List useCases = new ArrayList(); Collections.addAll(useCases, 47, 48, 49, 50); trackUseCases(useCases, PasswordUtils.class); } } ``` 注解的元素只能是基本类型、String、Class、enum、Annotation、或者以上类型的数组。 注解的值不能为null(即必须赋值且非null,或者有非null的默认值)。 注解不支持继承,即不能extends某个@interface。 ## 注解处理工具apt (略) ## 基于注解的单元测试 (略) # 第21章 并发 从性能的角度看,如果没有任务会被阻塞,那么在单处理器机器上使用并发就没有任何意义(相对于该程序的所有任务都顺序执行其实开销更大,因为增加了上下文切换的代价)。 实现并发最直接的方式是在操作系统级别使用进程,但是进程通常会有数量和开销的限制。某些函数式编程语言,如Erlang,被设计为可以将并发任务彼此隔离,其中每个函数调用都不会产生任何副作用,因此可以当做独立的任务来驱动。Java采取了更加传统的方式,在顺序型语言的基础上提供对线程的支持。 一个线程就是在进程中的一个单一的顺序控制流,因此单个进程可以拥有多个并发执行的任务(在Java中,线程对应Thread,任务对应Runnable),CPU将轮流给每个任务分配其占用时间,所以使用线程机制是一种建立透明的、可扩展的程序的方式(为机器添加CPU就能够很容易地加快程序的运行速度)。 Java的线程机制是抢占式的,即调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片。(与抢占式对应的是协作式,即每个任务都会自动地放弃控制,这依赖于程序中的让步语句) 如果机器上有多个处理器,线程调度器将会在这些处理器之间分发线程,线程调度机制是非确定性的,所以多线程任务的多次执行结果可能不同。 线程的一个额外的好处是它们提供了轻量级的执行上下文切换(大约100条指令,只是改变了程序的执行序列和局部变量),而不是重量级的进程上下文切换(上千条指令,会改变所有内存空间)。 ## Runnable ```java // 定义任务 public class LiftOff implements Runnable { protected int countDown = 10; // Default private static int taskCount = 0; private final int id = taskCount++; public LiftOff() {} public LiftOff(int countDown) { this.countDown = countDown; } public String status() { return "#" + id + "(" + (countDown > 0 ? countDown : "Liftoff!") + "), "; } public void run() { while(countDown-- > 0) { System.out.print(status()); Thread.yield(); // 对线程调度器的建议,即可以切换给其他任务执行一段时间 } } } // 在主线程中执行任务 public static void main(String[] args) { LiftOff launch = new LiftOff(); launch.run(); } ``` ## Thread // 使用新线程执行任务 ```java public class BasicThreads { public static void main(String[] args) { Thread t = new Thread(new LiftOff()); t.start(); System.out.println("Waiting for LiftOff"); } } ``` 每个Thread都会注册它自己(即会保存一个对它的引用),所以在调用start()之后,任务完成之前,不会被垃圾回收器回收。 ## Executor Executor用来管理Thread对象从而简化并发编程,是启动任务的首选方式。 ```java ExecutorService exec = Executors.newCachedThreadPool(); for(int i = 0; i < 5; i++){ exec.execute(new LiftOff()); } exec.shutdown(); // 防止新的任务被提交给这个Executor ``` CachedThreadPool()会为每一个任务创建一个新的线程,而使用FixedThreadPool()则会使用有限的线程集来执行所提交的任务,好处在于可以预先执行代价高昂的线程分配: ```java // 创建大小为5的线程池 ExecutorService exec = Executors.newFixedThreadPool(5); ``` 在任何线程池中,现有的线程在可能的情况下都会被自动复用。 SingleThreadExecutor只有一个线程,如果向它提交了多个任务,那么这些任务将排队执行(SingleThreadExecutor会序列化所有提交的任务,并维护它们的悬挂任务队列)。 ## Callable 使用Callable接口定义的任务可以在任务完成时得到一个返回值(Runnable不返回任何值)。 ```java // Callable的类型参数表示从call()方法中返回的返回值的类型 class TaskWithResult implements Callable { private int id; public TaskWithResult(int id) { this.id = id; } public String call() { return "result of TaskWithResult " + id; } } public class CallableDemo { public static void main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); ArrayList<Future> results = new ArrayList<Future>(); for(int i = 0; i < 10; i++){ // submit()将返回一个Future对象,它用Callable的返回结果进行了参数化 results.add(exec.submit(new TaskWithResult(i))); } // 打印返回值 for(Future fs : results){ try { // get() blocks until completion: System.out.println(fs.get()); // get()将阻塞 } catch(InterruptedException e) { System.out.println(e); return; } catch(ExecutionException e) { System.out.println(e); } finally { exec.shutdown(); } } } } ``` ## sleep() ```java TimeUtil.MILLISECONDS.sleep(100); ``` 对sleep()的调用会抛出InterruptedException异常,因为异常不能跨线程传播,所以必须在run()中捕获。 (详略) ## 线程优先级 调度器将倾向于让优先级最高的线程先执行,这并不意味着优先权较低的线程将得不到执行,优先权较低的线程仅仅是执行的频率较低。应该尽量避免操纵线程优先级,而是让所有线程以默认的优先级运行。 ```java Thread.currentThread().serPriority(priority); // 设置优先级 Thread.currentThread().getPriority(priority); // 获取当前线程的优先级 ``` JDK定义了10个优先级,但是与多数操作系统都不能很好的映射(不同操作系统线程优先级数量不同),一般最好使用MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY。 ## 让步 ```java Thread.yield(); // 对线程调度器的建议,即可以切换给其他任务执行一段时间 ``` ## 后台线程 后台线程指在程序运行时在后台提供一种通用服务的线程,这种线程不属于程序中不可或缺的部分,即当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程(只要有任何非后台线程还在运行,程序就不会终止)。 ```java Thread daemonThread = new Thread(new SimpleDaemons()); daemonThread.setDaemon(true); daemonThread.start(); ``` 可以通过调用isDaemon()来确定线程是否是一个后台线程,如果是一个后台线程,那么它创建的任何线程将被自动设置为后台线程。 当最后一个非后台线程终止时,后台线程会“突然”终止,即JVM会立即关闭所有的后台线程(这可能会造成finally子句不会被执行)。最好是使用Executor,因为它控制的所有任务可以有序关闭。 ## 直接执行Thread 可以不借助Runnable接口,直接执行Thread: ```java public class SimpleThread extends Thread { private int countDown = 5; private static int threadCount = 0; public SimpleThread() { // Store the thread name: super(Integer.toString(++threadCount)); start(); // 直接启动 } public String toString() { return "#" + getName() + "(" + countDown + "), "; } public void run() { while(true) { System.out.print(this); if(--countDown == 0) return; } } public static void main(String[] args) { for(int i = 0; i < 5; i++) new SimpleThread(); // 直接启动 } } ``` 在构造器中启动线程可能会有问题:另一个任务可能会在构造器结束之前开始执行,这意味着该任务能够访问处于不稳定状态的对象,这是优选Executor而不是显式地创建Thread对象的另一个原因。 ## join() 如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束(t.isAlive() == false)才恢复。 对join()方法的调用可以被终止:在调用线程上调用interrupt()方法(需要在try-catch中执行)。 ## 线程组 Java中的线程组是一次失败的尝试,直接忽略。 ## UncaughtExceptionHandler 将main()方法的主体放到try-catch语句块中并不能捕获在其中创建的新线程所抛出的异常。 可以在Thread对象上设置一个UncaughtExceptionHandler,它的uncaughException()方法会在线程因未捕获的异常而临近死亡时被调用。 ```java class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { System.out.println("caught " + e); } } ... Thread t = new Thread(r); t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); ``` ## synchronized 如果某个任务处于一个被标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。所以,对于某个特定对象来说,其所有synchronized方法共享同一个锁。 ```java public class SynchronizedEvenGenerator extends IntGenerator { private int currentEvenValue = 0; public synchronized int next() { ++currentEvenValue; Thread.yield(); // Cause failure faster ++currentEvenValue; return currentEvenValue; } } ``` 将域设置为private非常重要,否则synchronized关键字就不能防止其他任务直接访问域(从而产生冲突)。 `一个任务可以多次获得对象的锁`(一个方法在同一个对象上调用了第二个方法):只有首先获得了锁的任务才能继续获得多个锁,JVM负责跟踪对象被加锁的次数: ```java public class MultiLock { public synchronized void f1(int count) { if(count-- > 0) { print("f1() calling f2() with count " + count); f2(count); } } public synchronized void f2(int count) { if(count-- > 0) { print("f2() calling f1() with count " + count); f1(count); } } public static void main(String[] args) throws Exception { final MultiLock multiLock = new MultiLock(); new Thread() { public void run() { multiLock.f1(10); } }.start(); } } ``` 针对每个类也有一个锁(属于Class对象的一部分),synchronized static方法可以在类的范围内防止对static数据的并发访问。 ## Lock Lock对象必须被显式地创建、锁定和释放。 ```java public class MutexEvenGenerator extends IntGenerator { private int currentEvenValue = 0; private Lock lock = new ReentrantLock(); // 创建Lock对象 public int next() { lock.lock(); // 上锁 try { ++currentEvenValue; Thread.yield(); // Cause failure faster ++currentEvenValue; return currentEvenValue; } finally { lock.unlock(); // 解锁 } } } ``` 注意return语句的顺序,必须在try子句中,这样才能确保unlock()不会过早发生从而将数据暴露给第二个任务。 在使用synchronized时,如果发生失败,就会抛出一个异常,但是没有机会去做清理工作以维护系统使其处于良好状态。使用Lock可以在finall子句中将系统维护在正确的状态。 ## ReentranLock 可以使用ReentranLock实现`尝试获取锁`。 ```java private ReentrantLock lock = new ReentrantLock(); boolean captured = false; try { // 尝试2秒 captured = lock.tryLock(2, TimeUnit.SECONDS); } catch(InterruptedException e) { throw new RuntimeException(e); } try { System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured); } finally { if(captured){ lock.unlock(); } } ``` 在ReentrantLock上阻塞的任务具备可以被中断的能力。 ## volatile `原子操作`是不能被线程调度机制中断的操作(会在切换到其他线程之前执行完毕)。 原子性可以应用于除long、double之外的所有基本数据类型之上的简单操作。JVM会将64位(long、double)的读取和写入当做两个分离的32位操作来执行,在这个过程中可能发生上下文切换。可以使用volatile关键字来避免这种情况。 在多处理器系统中可视性问题也很常见:一个任务做出的修改,即使在不中断的意义上是原子性的,对其他任务也可能是不可视的,比如修改只是暂时性地存储在本地的处理器缓存中。如果将一个域声明为volatile,那么只要对这个域产生了写操作,其值就会立即被写入到主存中。 如果多个任务在同时访问某个域,那么这个域应该是volatile的,否则这个域应该只能通过同步访问(同步必然导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来防护,就不必设置它为volatile)。 一个任务所作的任何写入操作对这个任务来说都是可视的,因此如果某个域只需要在这个任务内部可视,那么就无需将其设置为volatile。 ## 原子类 Java提供了AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,它们提供如下形式的原子性条件更新操作: ```java boolean compareAndSet(expectedValue, updateValue); ``` ## 临界区 临界区用于防止多个线程同时访问方法内部的部分代码,而不是防止访问整个方法。 ```java synchronized(syncObject){ // 此对象的锁被用来对花括号内的代码进行同步控制 // ... } ``` synchronized块必须给定一个在其上进行同步的对象,通常比较合理的方式是使用方法正在被调用的当前对象synchronized(this); ## ThreadLocal 防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。`线程本地存储`是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储。ThreadLocal保证不会出现竞争条件。在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。 概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。 ## 线程的状态 1. 新建(new):当线程被创建时会短暂地处于这种状态,此时已被分配了必需的系统资源,并执行了初始化,之后调度器将其转变为可运行或阻塞状态。 2. 就绪(runnable):在这种状态下只要调度器把时间片分配给线程,线程就可以运行(不等于一直在运行,线程可能在运行也可能不在运行,只要有时间片分配给它,就运行)。 3. 阻塞(blocked):有某个条件阻止了线程的运行,这种状态下调度器将忽略线程(不会分配时间片),直到线程重新进入就绪状态。 4. 死亡(dead):线程不再是可调度的,并且也不会得到CPU时间。 ## 线程阻塞的原因 1. 调用sleep() 2. 通过调用wait()使线程挂起 3. 任务在等待I/O操作 4. 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用 旧有的用来阻塞和唤醒线程的suspend()和resume()方法,以及stop()已经被废弃,因为会造成死锁。 ## 中断 Thread类的interrupt()方法可以终止被阻塞的任务(设置线程的中断状态),如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么该操作将抛出InterruptedException。抛出该异常后,中断状态将被复位。 当调用Thread.interrupted()方法时,中断状态也将被复位(提供了离开run()循环而不抛出异常的方式)。 如果在Executor上调用shutdownNow(),那么它将发送一个interrupt()调用给它启动的所有线程。如果对单个线程进行操作,那么应该通过调用submit()而不是executor()来启动任务,submit()将返回一个Future,可以通过它调用cancel()从而中断某个特定任务。 可以中断对sleep()的调用,但是`不能中断正在试图获取synchronize锁或试图执行I/O操作的线程`。对于这类问题有一个比较挫但是有效的解决方案:关闭任务在其上发生阻塞的底层资源。 ```java class SleepBlocked implements Runnable { public void run() { try { TimeUnit.SECONDS.sleep(100); } catch(InterruptedException e) { // 可中断 print("InterruptedException"); } print("Exiting SleepBlocked.run()"); } } class IOBlocked implements Runnable { private InputStream in; public IOBlocked(InputStream is) { in = is; } public void run() { try { print("Waiting for read():"); in.read(); } catch(IOException e) { if(Thread.currentThread().isInterrupted()) { // isInterrupted() print("Interrupted from blocked I/O"); } else { throw new RuntimeException(e); } } print("Exiting IOBlocked.run()"); } } class SynchronizedBlocked implements Runnable { public synchronized void f() { while(true) // Never releases lock Thread.yield(); } public SynchronizedBlocked() { new Thread() { public void run() { f(); // Lock acquired by this thread } }.start(); } public void run() { print("Trying to call f()"); f(); print("Exiting SynchronizedBlocked.run()"); } } public class Interrupting { private static ExecutorService exec = Executors.newCachedThreadPool(); static void test(Runnable r) throws InterruptedException{ Future f = exec.submit(r); TimeUnit.MILLISECONDS.sleep(100); print("Interrupting " + r.getClass().getName()); f.cancel(true); // 中断 print("Interrupt sent to " + r.getClass().getName()); } public static void main(String[] args) throws Exception { test(new SleepBlocked()); // 可以中断 test(new IOBlocked(System.in)); // 无法中断 test(new SynchronizedBlocked()); // 无法中断 TimeUnit.SECONDS.sleep(3); print("Aborting with System.exit(0)"); System.exit(0); // ... since last 2 interrupts failed } } ``` 如果想中断被互斥阻塞的任务,应该使用ReentrantLock(synchronize不可中断)。 nio类提供了更加人性化的I/O中断,被阻塞的nio通道会自动地响应中断。 可以通过调用interrupted()来检查中断状态,这个方法同时会清除中断状态以确保并发结构不会就某个任务被中断这个问题通知多次 ## 检查中断 ```java class NeedsCleanup { private final int id; public NeedsCleanup(int ident) { id = ident; print("NeedsCleanup " + id); } // 发生异常时必须执行的清理操作 public void cleanup() { print("Cleaning up " + id); } } class Blocked3 implements Runnable { private volatile double d = 0.0; public void run() { try { while(!Thread.interrupted()) { // 如果在从这里到sleep()之前或调用过程中interrupt()被调用(即在阻塞操作之前或者阻塞过程中),那么这个任务就会在第一次试图调用阻塞操作之前经由InterruptedException退出 NeedsCleanup n1 = new NeedsCleanup(1); try { // 在定义了“发生异常需要清理”的对象之后,要紧接着就写try语句,以此来保证该对象可以被正常清理 print("Sleeping"); TimeUnit.SECONDS.sleep(1); // !阻塞操作 // 如果interrupt()在这里被调用(在非阻塞的操作过程中),那么interrupted()会检测到,while循环会退出 NeedsCleanup n2 = new NeedsCleanup(2); try { // 紧跟着定义语句,道理同上 print("Calculating"); // A time-consuming, non-blocking operation: for(int i = 1; i < 2500000; i++) d = d + (Math.PI + Math.E) / d; print("Finished time-consuming operation"); } finally { n2.cleanup(); } } finally { n1.cleanup(); } } print("Exiting via while() test"); } catch(InterruptedException e) { print("Exiting via InterruptedException"); } } } public class InterruptingIdiom { public static void main(String[] args) throws Exception { if(args.length != 1) { print("usage: java InterruptingIdiom delay-in-mS"); System.exit(1); } Thread t = new Thread(new Blocked3()); t.start(); TimeUnit.MILLISECONDS.sleep(new Integer(args[0])); t.interrupt(); } } ``` ## wait()与notifyAll() wait()提供了一种在任务间对活动进行同步的方式,会将任务挂起,然后在notify()或notifyAll()发生时被唤醒并去检查所发生的变化。 当调用sleep()或yield()的时候,锁并没有被释放,而调用wait()时线程的执行将被挂起,对象上的锁会被释放。 wait()、notify()、notifyAll()都是Object类的一部分,而不是Thread的一部分。`只能在同步控制块或者同步控制方法里调用它们`。如果在非同步控制方法里调用,程序可以编译通过,但是在运行时将抛出IllegalMonitorStateException。(即这些方法能够被调用的前提是拥有对象的锁) 如果要向一个对象发送notifyAll(),必须在能够取得该对象的锁的同步控制块中这么做: ```java synchronized(x){ x.notifyAll(); } ``` (详略) ## Condition 可以通过在Condition对象上调用await()来挂起一个任务,并通过signal()或signalAll()来唤醒任务,与使用notifyAll()相比,signalAll()是更安全的方式。 ```java Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); ... condition.await(); ... condition.signalAll(); ``` ## BlockingQueue 使用wait()和notifyAll()解决任务同步问题是一种非常低级的方式,更高级的方式是使用同步队列。同步队列在任何时刻都只允许一个任务插入或移除元素。BlockingQueue接口提供这个队列,Java中具体的实现有ArrayBlockingQueue、LinkedBlockQueue。 当消费者任务试图从队列中获取对象,而队列为空时,这些队列还可以挂起消费者任务,并且当有更多的元素可用时恢复消费者任务。 (详略) ## PipedWriter、PipedReader 管道基本上是一个阻塞队列,存在于引入BlockingQueue之前的Java版本中。 ```java // 一个写入任务 class Sender implements Runnable{ public void run(){ PipedWriter out = new PipedWriter(); out.write(c); ... } ... } // 一个读入任务 class Receiver implements Runnable{ ... PipedReader in = new PipedReader(sender.getPipedWriter()); // 连接管道 char c =(char)in.read(); } // 执行 Sender sender = new Sender(); Receiver reveiver = new Receiver(sender); exec.execute(sender); exec.execute(receiver); ``` ## 死锁 (略) ## 新类库中的构建 ### CountDownLatch (略) ### CyclicBarrier (略) ### DelayQueue (略) ### PriorityBlockingQueue (略) ### ScheduledThreadPoolExecutor (略) ### Semaphore (略) ### Exchanger (略) ## 仿真* (模拟各种并发场景,银行出纳、饭店仿真、分发工作。不方便记笔记,略) ## 性能调优 各种数据结构在并发情况下的性能比较最终应该基于实际的测试进行判断。 ## 免锁容器 免锁容器背后的策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。修改是在容器数据结构的某个部分的一个单独的副本(也可能是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的,只有当修改完成时被修改的结构才会自动地与主数据结构进行交换(原子性的操作),之后读取者就可以看到这个修改了。 Java提供的免锁容器包括: CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap、ConcurrentLinkedQueue 如果主要操作是读取,那么使用免锁容器将比使用基于synchroized实现同步的普通容器快许多,因为获取和释放锁的开销被省掉了。 ## 乐观锁 某些Atomic类允许执行“乐观加锁”,当执行某项计算时实际上并没有使用互斥,但是在计算完成,准备更新这个Atomic对象时,需要使用一个compareAndSet()方法,并将旧值和新值一起提交给这个方法,如果旧值与它在Atomic对象中发现的值不一样,那么这个操作就失败(意味着某个其他的任务已经于此操作期间修改了这个对象)。 ## ReadWriteLock ReadWriteLock对相对不频繁的写入,但是有多个任务要频繁地读取的情况作了优化。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放。 ## 活动对象 每个活动对象都维护着它自己的工作器线程和消息队列,并且所有对这种对象的请求都将进入队列排队,任何时刻都只能运行其中一个。有了活动对象就可以串行化消息而不是方法,意味着不需要再防备一个任务在其循环的中间被中断这种问题。 当向一个活动对象发送消息时,这条消息会被转变为一个任务,该任务会被插入到这个对象的队列中,等待在以后的某个时刻运行。 ``` public class ActiveObjectDemo { private ExecutorService ex = Executors.newSingleThreadExecutor(); // 单线程执行器 private Random rand = new Random(47); // Insert a random delay to produce the effect // of a calculation time: private void pause(int factor) { try { TimeUnit.MILLISECONDS.sleep( 100 + rand.nextInt(factor)); } catch(InterruptedException e) { print("sleep() interrupted"); } } // 返回一个Future对象以响应调用(意味着把方法调用转变为消息,方法调用可以立即返回,调用者可以使用Feture来发现任务何时完成并收集返回值) public Future calculateInt(final int x, final int y) { return ex.submit(new Callable() { public Integer call() { print("starting " + x + " + " + y); pause(500); return x + y; } }); } public Future calculateFloat(final float x, final float y) { return ex.submit(new Callable() { public Float call() { print("starting " + x + " + " + y); pause(2000); return x + y; } }); } public void shutdown() { ex.shutdown(); } public static void main(String[] args) { ActiveObjectDemo d1 = new ActiveObjectDemo(); // Prevents ConcurrentModificationException: List> results = new CopyOnWriteArrayList<Future>(); for(float f = 0.0f; f < 1.0f; f += 0.2f) results.add(d1.calculateFloat(f, f)); for(int i = 0; i < 5; i++) results.add(d1.calculateInt(i, i)); print("All asynch calls made"); while(results.size() > 0) { for(Future f : results) if(f.isDone()) { try { print(f.get()); } catch(Exception e) { throw new RuntimeException(e); } results.remove(f); } } d1.shutdown(); } } ``` 因为从一个活动对象到另一个活动对象的消息只能被排队时的延迟所阻塞,并且因为这个延迟总是非常短且独立于任何其他对象,所以发送消息实际上是不可阻塞的。由于一个活动对象系统只是经由消息来通信,所以两个对象在竞争调用另一个对象上的方法时,是不会被阻塞的,这意味着不会发生死锁。因为在活动对象中的工作器线程在任意时刻只执行一个消息,所以就不存在任何资源竞争,因而也不用操心如何同步方法。(实际上同步仍然发生,但是它是通过将方法调用排队,使得任意时刻都只能发生一个调用,从而将同步控制在消息级别上发生) # 第22章 图形化用户界面 (略)

文章目录