《Head First Java(第2版)中文版》辅导书

1.第10页,P10

以两条斜线开始的行是注释

x = 22;//  我是注释,推荐使用IntelliJ IDEA软件学习本书,Java环境变量配置见附录

还有一种:

/*中间文字也是注释*/

2.第10页,P10

空格符通常无关紧要

x    =    3;

3.第6页,P6

int size = 27; //声明一个integer类型,名称为size的变量并赋初始值为27String name = "Fido"; //声明名称为name的字符串,值为FidoDog mydog = new Dog(name,size); //以name和size声明一个名称为mydog的Dog变量

4.第6页,P6

String num = "8";int z = Integer.parseInt(num); //将字符串"8"转换成整数数字8

5.第6页,P6

int[] numList = {2,4,6,8}; //声明有4个元素的整型数组

6.第12页,P12

boolean isHot = true;while(isHot) { } //只能用boolean类型来测试,不能用 int x = 1;来测试while (x) { }

7.第13页,P13

System.out.println("x must be 3"); /*println会在最后面插入换行,若你想要让后续的输出以新的一行开始*///若是使用print则后续的输出还是会在同一行

8.第17页,P17

String[] pets = {"Fido", "Zeus", "Bin"}; /*创建3个String的数组,每个元素放在括号中并彼此间以逗号分开*/int x = pets.length; //查询数组的长度,执行以后x的值为3

9.第17页,P17

random()这个方法会返回介于0与1之间的值

10.P17

int x = (int) 24.6; //对浮点数取整数值,转换数据类型

11.P36

测试用的类会被命名为“受测试类的名称”+TestDrive,测试用的类带有main()并且你会在其中建立与存取被测的对象

受测试类:

public class Dog {​    int size;​    String breed;​    String name;​    ​    void bark() {​        System.out.print("Ruff!Ruff!");​    }}

测试用的类:

public class DogTestDrive {​    public static void main (String[] args)​    {​        Dog d = new Dog();​        d.size = 40;​        d.bark();​    }}

12.P38

真正的Java程序只会让对象与对象交互,使用带有main()的类来启动真正的Java应用程序。

带有main()方法的类,是应用程序的入口点

13.P51

primitive主要数据类型:类型          位数          值域boolean (Java虚拟机决定)   true或falsechar          16 bits      0~65535byte          8 bits       -128~127short         16 bits      -32768~32767int           32 bits      -2147483648~2147483647long          64 bits      -很大~+很大float         32bits       范围规模可变double        64bits       范围规模可变

primitive数据类型的声明与赋值声明:

boolean isFun = true;boolean powerOn;powerOn = isFun;boolean isPunkRock;isPunkRock = false;char c = 'f';int x = 234;int z = x;long big = 3456789;float f = 32.5f; /*注意:除非加上f,否则所有带小数点的值都会被Java当做double处理*/

14.P55

对象的声明、创建与赋值

Dog myDog = new Dog();/*声明一个引用变量 Dog myDog;要求Java虚拟机分配空间给引用变量,并将此变量命名为myDog*///创建对象 new Dog();要求Java虚拟机分配堆空间给新建立的Dog对象//连接对象和引用 =;将新的Dog赋值给myDog这个引用变量

15.P59

创建大小为7的数组

int[] nums;nums = new int[7];nums[0] = 6;//赋予int数组的每一个元素一个int值nums[1] = 19;nums[2] = 44;nums[3] = 42;nums[4] = 10;nums[5] = 20;nums[6] = 1;

16.P60

创建Dog数组

Dog[] pets;pets = new Dog[7];pets[0] = new Dog();//创建新的Dog对象并将它们赋值给数组的元素pets[1] = new Dog();

17.P61

存取Dog数组中的Dog

Dog[] myDogs = new Dog[3];myDogs[0] = new Dog();myDogs[0].name = "Fido";myDogs[0].bark();

18.P74

方法会运用形参。调用的一方会传入实参

Dog d = new Dog();d.bark(3);//3为实参;调用Dog上的bark()方法,并传入int类型的"3"这个值;
void bark(int numOfBarks) {  //numOfBarks为形参;3传给numOfBarks这个参数​    while (numOfBarks > 0) { //把numOfBarks当作一般的变量使用​        System.out.println("Ruff");​        numOfBarks = numOfBarks - 1;​    }}

19.P75

方法可以有返回值。每个方法都声明返回的类型。把方法设成返回void类型,代表没有返回任何东西。

void go() {}

但我们可以声明一个方法,回传给调用方指定的类型值,如:

int theSecret = life.giveSecret();int giveSecret() {​    return 42;     /*代表42的字节组合会从giveSecret()方法中返回,并指派给称为theSecret的变量*/}

20.P76

方法可以有多个参数。在声明的时候要用逗号分开,传入的时候也是用逗号分开。最重要的是,如果方法有参数,你一定要以正确的数量、类型和顺序来传递参数。

调用需要两个参数的方法,并传入两个参数:

void go() {​    TestStuff t = new TestStuff;​    t.takeTwo(12, 34);}void takeTwo(int x, int y) { /* Java是通过值传递的,也就是通过拷贝传递,12,34分别拷贝给x,y */​    int z = x + y;​    System.out.println("Total is " + z);}

也可以将变量当作参数传入,只要类型相符就可以:

void go() {​    int foo = 7;​    int bar = 3;​    t.takeTwo(foo, bar);}void takeTwo(int x, int y) {​    int z = x + y;​    System.out.println("Total is " + z);    //z的值会是10}

21.P78

方法只能声明单一的返回值。若你需要返回3个int值,就把返回类型说明为int的数组,将值装进数组中来返回。

22.P78

Java并未要求一定要处理返回值。你可以调用返回非void类型的方法而不必理会返回值,这代表你要的是方法的行为而不是返回值。你可以不指派返回值。

23.P81

封装的基本原则:将你的实例变量标记为私有的,并提供公有的getter与setter来控制存取动作。

封装GoodDog:

public class GoodDog {​    private int size;​    ​    public int getSize() {​        return size;​    }​    ​    public void setSize(int s) {​        size = s;​    }​    ​    void bark() {​        if (size > 60) {​            System.out.println("Woof!Woof!");​        }​        else if (size > 14) {​            System.out.println("Ruff!Ruff!");​        }​        else {​            System.out.println("Yip!Yip!");​        }​    }}

测试封装好的GoodDog:

public class GoodDogTestDrive {​    public static void main (String[] args) {​        GoodDog one = new GoodDog();​        one.setSize(70);​        GoodDog two = new GoodDog();​        two.setSize(8);​        System.out.println("Dog one: " + one.getSize());​        System.out.println("Dog two: " + two.getSize());​        one.bark();​        two.bark();​    }}

24.P84

实例变量永远都会有默认值。如果你没有明确的赋值给实例变量,或者没有调用setter,实例变量还是会有值。

integers         0      //int预设为0floating points  0.0boolean          falsereferences       null   //对象引用预设为null

例子PoorDog:

public class PoorDog {​    private int size;​    private String name;​    ​    public int getSize() {​        return size;​    }​    public String getName() {​        return name;​    }}

测试PoorDog:

public class PoorDogTestDrive {​    public static void main (String[] args) {​        PoorDog one = new PoorDog();​        System.out.println("Dog size is " + one.getSize());​        System.out.println("Dog name is " + one.getName());​    }}

25.P85

局部变量在使用前必须初始化

class Foo {​    public void go() {​        int x;​        int z = x + 3; /*无法编译:你可以声明没有值的x,但若要使用时编译器就会给出警示*/​    }}

26.P86

使用 == 来比对primitive主数据类型

int a = 3;byte b = 3;if (a == b) { //true }

使用 == 来判别两个引用是否都指向同一对象

Foo a = new Foo();Foo b = new Foo();Foo c = a;if (a == b) { //false }if (a == c) { //true }if (b == c) { //false }

有时你会需要知道两个对象是否真的相等,此时你就得使用equals()这个方法

27.P105

int guess = Integer.parseInt("3") /*将String类型的3转换为int并赋值给guess,Integer为Java内建类,parseInt为Integer的一个方法,能够将String解析*/

28.P105

for(int cell:locationCells) { } /*可以把(:)符号读作in,也就是for each int in locationCells,在循环时,将locationCells数组中每个元素依次赋值给cell变量*/

29.P111

int randomNum = (int)(Math.random()*5) /*Math.random()会返回一个介于1到小于1之间的数,所以这个公式会产生介于0~4之间的整数*/

30.P117

long比int大,且编译器无法确定long的内容是否可以裁掉。若要强制编译器装,你可以使用cast运算符

long y = 42;int x = (int) y;

31.P132

Java函数库中的一个类ArrayList

ArrayList<Egg> myList = new ArrayList<Egg>(); //创建出Egg类型的listEgg s = new Egg();myList.add(s);  //此ArrayList会产生一个“盒子”来放Egg对象sEgg b = new Egg();myList.add(b);  //此ArrayList会再弄出一个“盒子”来放新的Egg对象bint theSize = myList.size();  //因为myList有两个元素,size()会返回2boolean isIn = myList.contains(s); /*因为myList带有s所引用的Egg对象,所以此方法会返回true*/int idx = myList.indexOf(b); /*ArrayList为零基的,所以b引用的对象是第二个对象,而indexOf()会返回1*/boolean empty = myList.isEmpty(); //因为不是空的,isEmpty()会返回falsemyList.remove(s); //删除s

32.P136

ArrayList与数组的比较:

     ArrayList                                                    数组ArrayList<String> myList =                     String[] myList = new String[2];
new ArrayList<String>(); String a = new String("whoohoo");              String a =new String("whoohoo");myList.add(a);                                 myList[0] = a;String b = new String("Frog");                 String b = new String("Frog");myList.add(b);                                 myList[1] = b;int theSize = myList.size();                   int theSize = myList.length;Object o = myList.get(1);                      String o = myList[1];myList.remove(1);                              myList[1] = null;boolean isIn = myList.contains(b);             boolean isIn = false;​                                               for (String item :myList){​                                                   if(b.equals(item)){​                                                       isIn = true;​                                                           break;​                                                   }​                                               }

33.P137

在Java 5.0中的ArrayList是参数化的(parameterized)

ArrayList /是类型参数。这代表String的集合,就像说ArrayList代表Dog的集合/

在Java5.0之前是无法声明出要存放于ArrayList中元素的类型,它只会是异质对象的集合。现在我们就能用上面列出的语法来声明对象的类型。我们会在讨论Collection的章节对参数化类型作更进一步的探讨。

34.P156

javax开头的包代表什么?

在Java的早期两个版本中(1.02与1.1),所有随附于Java的类(也就是standard library)都放在java开头的包中。例如java.lang、java.io、java.util等。

后来出现了一些没有包含在标准函数库中的包。这些被称为扩展类,包含有两种类型:标准的和非标准的。

Sun所认可的称为standard extension,其余实验性质、预览版本或beta版的非标准类则不一定会被认可采用。

标准版的扩展类都以javax作为包名称的开头。

35.P157

除了在java.lang这个包里面的类,要用到其他的类都要指定完整名称。如果怕麻烦的话,那就用import来帮助你。

例如:import java.util.ArrayList

我们不必import进String类或system类,要记得java.lang是个预先被引用的包。因为java.lang是个经常被用到的基础包,所以你可以不必指定名称。java.lang.String与java.lang.System是独一无二的class,Java会知道去哪里找。

36.P158

如何查询API?很高兴知道在java.util这个包中有ArrayList。但是我自己要怎么找呢?

有两件事你必须知道:

1)库中有哪些类?

2)找到类之后,你怎么知道它是做什么的?

解决:

1)查阅参考书

2)查询HTML API文档,可以在java.sun.com网站在线阅读

37.P176

问:Java虚拟机会从继承关系的树形图最下方开始搜索方法,要是没有找到的时候会发生什么事情?

答:好问题!但是你不用担心这件事。编译器会找保证引用特定的方法是一定能够被调用到,

但是在执行期它不会在乎该方法实际上是从哪个类找到的。以Wolf为例,编译器会检查sleep()这个方法,但却不管sleep()实际上是定义在Animal这个类。要记得如果某个类继承了一个方法,它就会有那个方法。

方法在哪里定义对于编译器来说不重要。但在执行期,Java虚拟机就是有办法找到正确的。这个正确的意思是最接近该类型的版本。

38.P180

你可以在父类中设计出所有子类都适用的功能实现。让子类可以不用完全覆盖掉父类的功能,只是再加上额外的行为。你可以通过super这个关键词来取用父类。

public void roam() {​    super.roam();​      //my own roam stuff}

39.P180

下面列出这4种权限,左边是最受限制的,而越往右边限制程度越小:

private default protected public

40.P181

如果两者间不能通过is-a测试就不要应用继承关系。一定要确定子类是父类一种更特定的类型才可以。

41.P181

子类会继承父类所有public类型的实例变量和方法,但不会继承父类所有private类型的变量和方法。

42.P181

继承下来的方法可以被覆盖掉,但实例变量不能被覆盖掉。

43.P186

运用多态时,引用类型可以是实际对象类型的父类。

Animal[] animals = new Animal[5];animals [0] = new Dog();animals [1] = new Cat();animals [2] = new Wolf();animals [3] = new Hippo();animals [4] = new Lion();for (int i = 0; i < animals.length; i++) {​    animals[i].eat(); //当i为0的时候,这会调用Dog的eat()​    animals[i].roam(); //当i为1的时候,这会调用Cat的roam()​    }

44.P187

参数和返回类型也可以多态。如果你声明一个父类的引用变量,比如说Animal,并赋子类对象给它,假设是Dog,想象一下此变量被当做方法的参数时会如何运作……

class Vet {​    public void giveShot(Animal a) {  /*giveShot这个方法可以取用任何一种Animal。只要所传入的是Animal的子类它都能执行*/​        //恐吓动物,让动物尖叫​        a.makeNoise();​    }}
class petOwner {​    public void start() {​        Vet v = new Vet();​        Dog d = new Dog();​        Hippo h = new Hippo();​        v.giveShot(d); //会执行Dog的makeNoise()​        v.giveShot(h); //会执行Hippo的makeNoise()​    }}

45.P189

问:你能够继承任何一个类吗?就像类的成员一样如果类是私有的你就不能继承?

答:内部类我们还没有介绍到。除了内部类之外,并没有私有类这样的概念。但是有三种方法可以防止某个类被作出子类。

第一种是存取控制。就算类不能标记为私有,但它还是可以不标记公有。非公有的类只能被同一个包的类作出子类。

第二种是使用final这个修饰符。这表示它是继承树的末端,不能被继承。

第三种是让类只拥有private的构造程序。

46.P189

问:可不可以只用final去标识方法而不使用整个类。

答:如果你想要防止特定的方法被覆盖,可以将该方法标识上final这个修饰符。将整个类标识成final表示没有任何的方法可以被覆盖。

47.P190

遵守合约:覆盖的规则

当你要覆盖父类的方法时,你就得同意要履约。方法就是合约的标志。

1)方法的参数必须要一样,且返回类型必须要兼容。

2)不能降低方法的存取权限。这代表存取权限必须相同或者更为开放,比如你不能覆盖掉一个公有的方法并将它标记为私有。

比如:

Applicance类有两个方法boolean turnOn() boolean turnOff()

Toaster类继承Applicance类,并有一个方法 boolean turnOn(int level)

问:Toaster会调用哪个版本的turnOn()方法

答:Toaster仍然会调用Applicance类中的turnOn方法,因为在Toaster中的turnOn(int level)这个方法并没有覆盖掉Applicance中的版本

48.P191

方法的重载(overload)

重载的意义是两个方法的名称相同,但参数不同。

重载版的方法只是刚好有相同名字的不同方法,它与继承或多态无关。重载的方法与覆盖方法不一样。

1)返回类型可以不同

2)不能只改变返回类型。如果只有返回类型不同,但参数一样,这是不允许的

3)可以改变存取权限。你可以任意地设定overload版method的存取权限

重载的合法范例:

public class Overloads {​    String uniqueID;​    public int addNums(int a, int b) {​        return a + b;​    }​    public double addNums(double a, double b) {​        return a + b;​    }​    public void setUniqueID(String theID) {​        uniqueID = theID;​    }​    public void setUniqueID(int ssNumber) {​        String numString = " " + ssNumber;​        setUniqueID(numString);​    }}

49.P203

除了类之外,你也可以将方法标记为abstract的。抽象的类代表此类必须要被extend过,抽象的方法代表此方法一定要被覆盖过。

如果你声明出一个抽象的方法,就必须将类也标记为抽象的。你不能在非抽象类中拥有抽象方法。

抽象的方法没有实体。

public abstract void eat(); //没有方法体!直接以分号结束

50.P204

你必须实现所有抽象的方法。即你必须以相同的方法鉴名(名称与参数)和相容的返回类型创建出非抽象的方法。

Java很注重你的具体子类有没有实现这些方法。

51.P208

在Java中的所有类都是从Object这个类继承出来的。Object这个类是所有类的源头,它是所有类的父类。

52.P212

当把所有东西都以多态来当作是Object会让对象看起来失去了真正的本质(但不是永久性的)。Dog似乎失去了犬性。

53.P213

当一个对象被声明为Object类型的对象所引用时,它无法再赋值给原来类型的变量。

编译器是根据引用类型来判断有哪些method可以调用,而不是根据Object确实的类型。

如果对象的类型是Snowboard,而引用它的却是Object,则它不能调用Snowboard的方法

54.P216

它还是个Dog对象,但如果你想要调用Dog特有的方法,就必须要将类型声明为Dog。如果你真的确定它是个Dog,那么你就可以从Object中拷贝出一个Dog引用,并且赋值给Dog引用变量

例如:

Object o = al.get(index);Dog d = (Dog) o; //将Object类型转换为Dogd.roma();

如果不能确定它是Dog,你可以使用instanceof这个运算符来检查。若是类型转换错了,你会在执行期遇到ClassCastException异常并终止。

if ( o instanceof Dog) {​    Dog d = (Dog) o;}

55.P224

接口可以用来解决多重继承的问题却又不会产生致命方块这种问题。

接口解决致命方块的办法很简单:把全部的方法设为抽象的!如此一来,子类就得要实现此方法,因此Java虚拟机在执行期间就不会搞不清楚要用哪一个继承版本。

接口的定义:

public interface Pet {...}

接口的实现:

public class Dog extends Canine implements Pet {...}  /*使用implements这个关键词。*///注意到实现interface时还是必须在某个类的继承之下

56.P225

事实上,如果使用接口来编写程序,你就是在说:“不管你来自哪里,只要你实现这个接口,别人就会知道你一定会履行这个合约。”

57.P226

类可以实现多个接口,extend只能有一个,implement可以有好多个。

public class Dog extends Animal implements Pet, Saveable, Paintable {...}

58.P228

问:如果创建出一个具体的子类且必须要覆盖某个方法,但又需要执行父类的方法时要怎么办?也就是说不打算完全地覆盖掉原来的方法,只是要加入额外的动作要怎么做?

答:呃……想想看“extends”的字义。设计良好的面向对象要注意到如何编写出必须被覆盖的程序代码。换言之,就是在抽象的类中编写能够共同的实现,让子类加入其余特定的部分。super这个关键词能让你在子类中调用父类的方法。

例:

abstract class Report {​    void runReport() {​    //设置报告​    }​    void printReport() {​    //输出​    }}
class BuzzwordsReport extends Report {​    void runReport() {​        super.runReport(); //调用父版的方法​        buzzwordCompliance();​        printReport();​    }​    void buzzwordCompliance() {...}}

59.P229

从ArrayList取出的对象只能被Object引用,不然就要用类型转换来改变。

60.P229

接口就好像是100%纯天然抽象类。

61.P236

在Java中,程序员会在乎内存中的两种区域:对象的生存空间堆(heap)和方法调用及变量的生存空间栈(stack)。

62.P238

如果想要了解变量的有效范围(scope)、对象的建立、内存管理、线程(thread)和异常处理,则认识栈与堆是很重要的。

63.P241

Duck myDuck = new Duck();

看起来像是在调用Duck()这个方法,然而并不是。我们是在调用Duck的构造函数。

你可以帮类编写构造函数,但如果你没写,编译器会偷偷帮你写!

下面就是编译器写出来的

public Duck () {}

64.P242

构造函数的一项关键特征是它会在对象能够被赋值给引用之前就执行。这代表你可以有机会在对象被引用之前介入。

例:

public class Duck {​    public Duck () {​        System.out.println("Quack");​    }}
public class UseADuck {​    public static void main (String[] args) {​        Duck d = new Duck();​    }}

65.P244

让用户先构造出Duck对象再来设定大小是危险的。如果用户不知道,或者忘记要执行setSize()怎么办?最好的方法是把初始化的程序代码放在构造函数中,然后把构造函数设定成需要参数的。

public class Duck {​    int size;​    public Duck (int ducksize) {    //给构造函数加上参数​        System.out.println("Quack");​        size = ducksize;    //使用参数的值来设定size这个实例变量​        System.out.println("size is " + size);​    }}
public class UseADuck {​    public static void main (String[] args) {​        Duck d = new Duck(42); //传值给构造函数​    }}

66.P246

如果你已经写了一个有参数的构造函数,并且你需要一个没有参数的构造函数,则你必须自己动手写!如果类有一个以上的构造函数,则参数一定要不一样,这代表它们是重载的,就跟方法的重载一样。

public class Duck2 {​    int size;​    public Duck2 () {​        //指定默认值​        size = 27;​    }​    public Duck2 (int ducksize) {​        //使用参数设定​        size = ducksize;​    }}

知道size大小时:

Duck2 d = new Duck2(15);

不知道size大小时:

Duck2 d2 = new Duck2();

67.P247

重载构造函数的意思代表你有一个以上的构造函数且参数都不相同。编译器看的是参数的类型和顺序而不是参数的名字,你可以做出相同类型但是顺序不同的参数。

public class Mushroom {​    public Mushroom (int size) { }​    public Mushroom () { }​    public Mushroom (boolean isMagic) { }​    public Mushroom (boolean isMagic, int size) { }​    public Mushroom (int size, boolean isMagic) { } //因为顺序不同所以过关}

68.P247

构造函数必须与类同名,默认的构造函数是没有参数的。

69.P249

私有的构造函数不是完全不能存取,它代表该类以外不能存取。

70.P251

父类的构造函数在对象的生命中所扮演的角色

在创建新对象时,所有继承下来的构造函数都会执行。这代表每个父类都有一个构造函数,且每个构造函数都会在子类对象创建时期执行。

还有,就算是抽象的类也有构造函数。虽然你不能对抽象的类执行new操作,但抽象的类还是父类,因此它的构造函数会在具体子类创建出实例时执行。

例:创建Hippo也代表创建Animal与Object

public class Animal {​    public Animal() {​        System.out.println("Making an Animal");​    }}
public class Hippo extends Animal {​    public Hippo() {​        System.out.println("Making a Hippo");​    }}
public class TestHippo {​    public static void main(String[] args) {​        System.out.println("Starting......");​        Hippo h = new Hippo();​    }}

71.P253

如何调用父类的构造函数

例:

public class Duck extends Animal {​    int size;​    public Duck(int newSize) {​        super();    //调用Animal这个父类构造函数的唯一方法是调用super()​        size = newSize;​    }}

72.P253

如果我们没有调用super()会发生什么事?

编译器会帮我们加上super()的调用,编译器帮忙加的一定会是没有参数的版本,假使父类有多个重载版本,也只有无参数的这个版本会被调用。

73.P254

对super()的调用必须是构造函数的第一个语句。

public Boop(int i) {​    super();​    size = i;}

74.P255

有参数的父类构造函数如何传递参数?

唯一的机会是通过super()来引用父类,所以要从这里把name的值传进去,让Animal把它存到私有的name实例变量中。

public abstract class Animal {​    private String name;​    public String getName() {​        return name;​    }​    public Animal(String theName) {​        name = theName;​    }}
public class Hippo extends Animal {​    public Hippo(String name) {​        super(name); //传给Animal的构造函数​    }}
public class MakeHippo {​    public static void main(String[] args) {​        Hippo h = new Hippo("Buffy");​        System.out.println(h.getName());​    }}

75.P256

使用this()来从某个构造函数调用同一个类的另外一个构造函数。

this()只能用在构造函数中,且必须是第一行语句。

每个构造函数可以选择调用super()或this(),但不能同时调用!

例:

class Mini extends Car {​    Color color;​    public Mini() {​        this(Color.Red);​    }​    public Mini(Color c) {​        super("Mini");​        color = c;​    }​    public Mini(int size) {​        this(Color.Red);​        //super(size);​    }}

76.P259

当局部变量活着的时候,它的状态会被保存。只要doStuff()还在堆栈上,b变量就会保持它的值。但b变量只能在doStuff()待在栈顶时才能使用。也就是说局部变量只能在声明它的方法在执行中才能被使用。

public void doStuff() {​    boolean b = true;​    go(4);}
public void go(int x) {​    int z = x + 24;​    crazy();}
public void crazy() {​    char c = 'a';}

77.P260

有3种方法可以释放对象的引用:

1)引用永久性的离开它的范围

public class StackRef {​    public void foof() {​        barf();​    }​    public void barf() {​        Duck d = new Duck(); //barf()执行完毕,因此d也就挂掉了​    }}

2)引用被赋值到其他的对象上

public class ReRef {​    Duck d = new Duck();​    public void go() {​        d = new Duck(); //既然d引用到其他的Duck,第一个Duck就跟死掉是一样的​    }}

3)直接将引用设定为null

public class ReRef {​    Duck d = new Duck();​    public void go() {​        d = null; //既然d引用到null,第一个Duck就跟死掉是一样的​    }}

78.P274

在Math这个类中的所有方法都不需要实例变量值,因为这些方法都是静态的,所以你无需Math的实例。你会用到的只是它的类本身。

int x = Math.round(42.2);int y = Math.min(56,12);int z = Math.abs(-343); //这些方法无需实例变量,因此也不需要特定对象来判别行为

79.P275

static这个关键词可以标记出不需类实例的方法。一个静态的方法代表说“一种不依靠实例变量也就不需要对象的行为”

public static int min(int a, int b) {​    //返回a与b中较小的值}

80.P276

事实上,只要有main()的类都算有静态的方法。

81.P277

如果你尝试在静态的方法内使用实例变量,编译器会认为:“我不知道你说的是哪个实例的变量!”

静态的方法是不知道堆上有哪些实例的。

public class Duck {​    private int size;​    public static void main(String[] args) {​        System.out.println("Size of duck is " + size); /*size调用出错,此时我们根本无从得知堆上是否有Duck*/​    }​    public void setSize(int s) {​        size = s;​    }​    public int getSize() {​        return size;​    }}

82.P278

静态的方法也不能调用非静态的方法

public class Duck {​    private int size;​    public static void main(String[] args) {​        System.out.println("Size of duck is " + getSize()); /*getSize()函数调用出错,此时我们根本无从得知堆上是否有Duck*/​    }​    public void setSize(int s) {​        size = s;​    }​    public int getSize() {​        return size;​    }}

83.P279

这就是静态变量的功用:被同类的所有实例共享的变量。

public class Duck {​    private int size;​    private static int duckCount = 0; /*此静态的duckCount变量只会在类第一次载入的时候被初始化*/​    public Duck() {​        duckCount++; //每当构造函数执行的时候,此变量的值就会递增​    }​    public void setSize(int s) {​        size = s;​    }​    public int getSize() {​        return size;​    }}

84.P281

静态变量是在类被加载时初始化的。类会被加载是因为Java虚拟机认为它该被加载了。

静态项目的初始化有两项保证:

静态变量会在该类的任何对象创建之前就完成初始化。

静态变量会在该类的任何静态方法执行之前就初始化。

public class Player {​    static int playerCount = 0; //playerCount会在载入类的时候被初始化为0​    private String name;​    public Player(String n) {​        name = n;​        playerCount++;​    }}
public class PlayerTestDrive {​    public static void main(String[] args) {​        System.out.println(Player.playerCount);​        Player one = new Player("Tiger Woods");​        System.out.println(Player.playerCount);//静态变量也是通过类的名称来存取​    }}

85.P282

静态的final变量是常数。一个被标记为final的变量代表它一旦被初始化之后就不会改动。

以Math.PI为例:

public static final double PI = 3.141592653589793;

86.P282

没有别的方法可以识别变量为不变的常量,但有命名惯例可以帮助你认出来。

常数变量的名称应该要都是大写字母!在Java中的常量是把变量同时标记为static和final的。

静态final变量的初始化:

1)声明的时候:

public class Foo {​    public static final int FOO_X = 25; /*注意这个命名惯例——应该都是大写的,并以下划线字符分隔*/}

2)在静态初始化程序中:

public class Bar {​    public static final double BAR_SIGN;​    static {    //这段程序会在类被加载时执行​        BAR_SIGN = (double)Math.random();​    }}

如果你没有以上面两种方式之一来给值的话,编译器会提示错误。

87.P283

final不只用在静态变量上,你也可以用final关键字来修饰非静态的变量。这包括了实例变量、局部变量或是方法的参数。不管哪一种,这都代表它的值不能变动。但你也可以用final来防止方法的覆盖或创建子类。

非静态的final变量:

class Foof {​    final int size = 3; //size将无法改变​    final int whuffie;​    Foof() {​        whuffie = 42; //whuffie不能改变​    }​    void doStuff(final int x) {​        //不能改变x​    }​    void doMore() {​        final int z = 7;​        //不能改变z​    }}

final的method:

class Poof {​    final void calcWhuffie() {​        //绝对不能被覆盖过​    }}

final的class:

final class MyMostPerfectClass {​    //不能被继承过}

88.P284

如果类只有静态的方法,你可以将构造函数标记为private以避免被初始化

89.P287

primitive主数据类型的包装

有时你会想要把primitive主数据类型当作对象来处理。例如在5.0之前的Java版本上,你无法直接把primitive主数据类型放进ArrayList或HashMap中

例:

int x = 32;ArrayList list = new ArrayList();list.add(x); //除非是用Java5.0或以上的版本,否则这个命令不会成功

每一个primitive主数据类型都有个包装用的类,且因为这些包装类都在java.lang这个包中,所以你无须去import它们。

还有,API的设计者决定让名称不是完全地符合primitive主数据类型的名称:

Boolean

Character

Byte

Short

Integer

Long

Float

Double

包装值

int i = 288;Integer iWrap = new Integer(i); /*传入primitive主数据类型给包装类的构造函数*/

解开包装

int unWrapped = iWrap.intValue(); //所有的包装类都有类似这样的方法

90.P289

从5.0版开始加入的autoboxing功能能够自动地将primitive主数据类型转换成包装过的对象!

让我们看一下创建int的ArrayList时会发生什么事。

例:

primitive int的ArrayList

有autoboxing

public void doNumsNewWay() {​    ArrayList<Integer> listOfNumbers = new ArrayList<Integer>(); /*创建Integer类型的ArrayList*/​    listOfNumbers.add(3); /*直接加!虽然ArrayList没有add(int)这样的方法,但编译器会自动帮你包装*/​    int num = listOfNumbers.get(0); /*编译器也会自动地解开Integer对象的包装,因此可以直接赋值给int*/}

91.P289

问:为什么不直接声明ArrayList?

答:generic类型的规则是你只能指定类或接口类型。因此ArrayList将无法通过编译。但你可以直接把该包装所对应的primitive主数据类型放进ArrayList中,例如说boolean类型的放入ArrayList中,chars类型的放入ArrayList中,int类型的放入ArrayList中

92.P290

autoboxing不只是包装与解开primitive主数据类型给collection用而已,它还可以让你在各种地方交换地运用primitive主数据类型与它的包装类型。

93.P292

除了一般类的操作外,包装也有一组实用的静态方法。我们在之前已经用过其中一个–Integer.parseInt()。

将String转换成primitive主数据类型值是很容易的:

String s = "2";int x = Integer.parseInt(s); //将字符串"2"转换成数字2double d = Double.parseDouble("420.24");boolean b = new Boolean("true").booleanValue(); /*你可能会以为有Boolean.parseBoolean()吧?其实没有。但是Boolean的构造函数可以取用String来创建对象*/

但若你这么做的话:

String t = "two";int y = Integer.parseInt(t); //可以通过编译,但执行时就会出状况

就会在运行期间遇到异常:

NumberFormatException

94.P293

将primitive主数据类型值转换成String,有好几种方法可以将数值转换成String。最简单的方法是将数字接上现有的String。

double d = 42.5;String doubleString = "" + d; //"+"这个操作数是Java中唯一有重载过的运算符double d = 42.5;String doubleString = Double.toString(d); //toString是Double这个类的静态方法

95.P294

数字的格式化

从Java 5.0起,更多更好更有扩展性的功能是通过java.util中的Formatter这个类来提供的。但你无需自己创建与调用这个class上的方法,因为Java 5.0已经把便利性的功能加到部分的输入/输出类与String上。因此只要调用静态的String.format()并传入值与格式设定就好。

例:

将数字以带逗号的形式格式化

public class TestFormats {​    public static void main (String[] args) {​        String s = String.format("%,d", 1000000000); /*将此方法的第二个参数以第一个参数所表示带有逗号的整数方式表示*/​        System.out.println(s);​    }}

96.P296

%符号代表把参数放在这里

format()方法的第一个参数被称为"格式化串",它可以带有实际上就是要这么输出而不用转译的字符。当你看到%符号时,要把它想做是会被方法其余参数替换掉的位置。

format("I have %.2f bugs to fix.", 476578.09876)  /*输出结果为I have 476578.10 bugs to fix.*/format("I have %,.2f bugs to fix.", 476578.09876) /*输出结果为I have 476,578.10 bugs to fix.*/format("%x", 42); //参数必须是byte、short、int、long、BigInteger。输出结果为2aformat("%c", 42); /* 参数必须是byte、short、int、long。ASCII的42代表“*”号,输出结果为* */

97.P300

超过一项以上参数时:

int one = 20456654;double two = 100567890.248907;String s = String.format("The rank is %,d out of %,.2f", one,two);//输出结果为The rank is 20,456,654 out of 100,567,890.25

98.P301

如何要把Date类型的变量日期用特定格式输出呢?

完整的日期与时间:%tc

String.format("%tc",new Date());

只有时间:%tr

String.format("%tr",new Date());

周、月、日:%tA %tB %td

因为没有刚好符合我们要求的输出格式,所以得组合3种形式来产生出所需要的格式:

Date today = new Date();String.format("%tA, %tB %td",today,today,today); //输出结果为Sunday, November 28

同上,但不用重复给参数:

Date today = new Date();String.format("%tA, %<tB %<td",today); /*"<"这个符号是个特殊的指示,用来告诉格式化程序重复利用之前用过的参数*/

99.P304

你无法取得Calendar的实例,但是你可以取得它的具体子类的实例。

很明显,你不能取得Calendar的实例,因为Calendar是抽象的。但你还是能够不受限制地调用Calendar的静态method,这是因为静态的方法是在类上而不是在某个特定的实例上。所以你对Calendar调用getInstance()会返回给你具体子类的实例。

运用Calendar对象的范例:

import java.util.Calendar;public class Calendar1 {​    public static void main(String[] args) {​        Calendar c = Calendar.getInstance();​        c.set(2004,1,7,15,40); //将时间设定为2004年1月7日15:40,注意月份是零基的​        long day1  = c.getTimeInMillis(); //将目前时间转换为以millisecond表示​        day1 += 1000 * 60 *60; //将c的时间加上一个小时​        c.setTimeInMillis(day1);​        System.out.println("new hour " + c.get(c.HOUR_OF_DAY));​        c.add(c.DATE,35); //加上35天,所以c已经到了2月​        System.out.println("add 35 days " + c.getTime());​        c.roll(c.DATE,35); //滚动35天,注意:只有日期字段会动,月份不会动​        System.out.println("roll 35 days " + c.getTime());​        c.set(c.DATE,1); //直接设定DATE的值​        System.out.println("set to 1 " + c.getTime());​    }}

100.P307

静到最高点!静态的import

这是Java 5.0的新功能:一把双刃剑。基本上,这功能是让你import静态的类、变量或enum时能够少打几个字。但它的缺点是会让程序比较难阅读

旧式的写法:

import java.lang.Math;class NoStatic {​    public static void main(String[] args) {​        System.out.println("sqrt" + Math.sqrt(2.0));​        System.out.println("tan" + Math.tan(60));​    }}

使用static import的写法:

import static java.lang.System.out;import static java.lang.Math.*;class WithStatic {​    public static void main(String[] args) {​        out.println("sqrt" + sqrt(2.0)); //只不过少打了一些字​        out.println("tan" + tan(60)); //只不过少打了一些字​    }}

101.P315

异常处理

有风险的行为

目前为止我们都还没有处理过风险性的问题。执行期肯定会有问题,但大多数都来自于程序的错误,而这些错误应该在开发阶段解决掉。不过我们所说的问题是你无法保证在执行期不会出现的。例如预期某些文件会正确地待在某个特定的目录中,但实际执行时文件却又失踪了。

102.P319

调用有风险的方法(或许不是你自己写的)时会发生什么事?

1)假设你调用了一个不是自己写的方法。

2)该方法执行某些有风险的任务,可能会在运行期间出状况。

3)你必须认识到该方法是有风险的。

4)你得写出可以在发生状况时加以处理的程序代码,未雨绸缪!

103.P322

异常是一种Exception类型的对象

还记得关于多态的那一章提到Exception类型的对象可以是任何它的子类的实例吗?

因为它是对象,所以你catch住的也是对象。下面的程序代码中catch的参数是Exception类型的ex引用变量:

try {​    //危险动作} catch(Exception ex) {​    //尝试恢复}

104.P323

如果你的程序代码会抓住异常,那是谁把它抛出来的?

在编写可能会抛出异常的方法时,它们都必须声明有异常。

1)有风险、会抛出异常的程序代码:

public void takeRisk() throws BadException { //必须要声明它会抛出BadException​    if (abandonAllHope) {​        throw new BadException(); //创建Exception对象并抛出​    }}

2)调用该方法的程序代码:

public void crossFingers() {try {​    anObject.takeRisk();} catch (BadException ex) {​    System.out.println("Aaargh!");​    ex.printStackTrace(); /*如果无法从异常中恢复,至少也要使用printStackTrace()来列出有用的信息*/​    }}

105.P324

编译器会核对每件事,除了RuntimeExceptions之外。编译器保证:

1)如果你有抛出异常,则你一定要使用throw来声明这件事。

2)如果你调用会抛出异常的方法,你必须得确认你知道异常的可能性。将调用包在try/catch是一种满足编译器的方法(稍后介绍第二种方法)。

106.P327

finally块是用来存放不管有没有异常都得执行的程序。

try {​    turnOvenOn();​    x.bake();} catch (BakingException ex) {​    ex.printStackTrace();} finally {​    turnOvenOff();}

例:

class ScaryException extends Exception{};public class TestExceptions {​    public static void main (String[] args) {​        String test = "no"; //根据需要改为yes或no​        try {​            System.out.println("start try");​            doRisky(test);​            System.out.println("end try");​        } catch ( ScaryException se) {​            System.out.println("scary exception");​        } finally {​            System.out.println("finally");​        }​        System.out.println("end of main");​    }​    public static void doRisky(String test) throws ScaryException {​        System.out.println("start risky");​        if ("yes".equals(test)) {​            throw new ScaryException();​        }​        System.out.println("end risky");​        return;​    }}

107.P329

我们讨论过方法可以抛出一个以上的异常吗?

如果有必要的话,方法可以抛出多个异常。但该方法的声明必须要有含有全部可能的检查异常(若两个或两个以上的异常有共同的父类时,可以只声明该父类就行)。

处理多重异常

编译器会检查你是否处理所有可能的异常。将个别的catch块逐个放在try块下。某些情况下catch出现的先后顺序会有影响。

public class Laundry {​    public void doLaundry() throws PantsException, LingerieException { /*声明两个可能的异常类型*/​        //有可能抛出两个异常的程序代码​    }}
public class Foo {​    public void go() {​        Laundry laundry = new Laundry();​        try {​            laundry.doLaundry();​        } catch(PantsException pex) { /*如果抛出的是PantsException,它就会运行到这个块*/​            //恢复程序代码​        } catch(LingerieException lex) { /*如果抛出的是LingerieException,则跳到这个段*/​            //恢复程序代码​        }​    }}

108.P330

异常也是多态的

别忘记异常是对象。除了可以被抛出之外,并没有什么特别的。因此如同所有的对象,异常也能够以多态的方式来引用。举例来说,LingerieException对象能被赋值给ClothingException的引用。PantsException也能够被赋值给ClothingException的引用。这样的好处是方法可以不必明确地声明每个可能抛出的异常,可以只声明父类就行。对于catch块来说,也可以不用对每个可能的异常作处理,只要有一个或少数几个catch可以处理所有的异常就够了。

1)以异常的父型来声明会抛出的异常

public void doLaundry() throws ClothingException{ } /*声明成ClothingException可让你抛出任何ClothingException的子类。*///这代表此方法可以抛出PantsException,LingerieException等异常而不用个别的声明

2)以所抛出的异常父型来catch异常

try {​    laundry.doLaundry();} catch(ClothingException cex) { //可以catch任何ClothingException的子类​    //解决方案}

109.P331

可以用super来处理所有异常并不代表就应该这么做

你可以把异常处理程序代码写成只有一个catch块以父型的Exception来捕获,因此就可以抓到任何可能被抛出的异常:

try {​    laundry.doLaundry();} catch(Exception ex) { //恢复什么?这会捕获所有的异常,因此你会搞不清楚哪里出错​    //解决方案...}

110.P331

为每个需要单独处理的异常编写不同的catch块

举例来说,如果你的程序代码处理TeeShirtException的方法与LingerieException的方法不同,则要个别地写出catch块。

但如果剩余的都需要ClothingException以同样的方式处理(ClothingException为TeeShirtException和LingerieException两个异常的父型),则可以用ClothingException的catch来处理。

try {​    laundry.doLaundry();} catch(TeeShirtException tex) {​    //恢复此问题} catch(LingerieException lex) { /*恢复TeeShirtException和恢复LingerieException的处理方案不同,所以使用不同的块*/​    //恢复此问题} catch(ClothingException cex) { //同样处理方法的都会在这边​    //恢复其他问题}

111.P332

在继承树上层次越高,则“篮子”就越大。若你从上往下沿着继承层次走,异常类就会越来越有特定的取向,且catch的篮子也会越来越小。

这是多态的常态现象。ShirtException足以容下TeeShirtException或DressShirtException。而ClothingException甚至更大(能引用的范围更多),但真的要说到大,Exception类型无疑是头号的霸王,它可以catch所有的异常,还包括了运行期间(unchecked)的异常,因此你或许不会把它用在测试以外的环境中。

112.P333

有多个catch块时要从小排到大

不能把大篮子放在小篮子上面

嗯,你硬要这么做也可以,但是会无法通过编译。catch块不像重载的方法会被挑出最符合的项目。使用catch块时,Java虚拟机只会从头开始往下找到第一个符合范围的异常处理块。如果第一个catch就是catch(Exception ex),则编译器会知道其余的都没有用处——绝对不会被用到。

113.P335

如果不想处理异常,你可以把它duck掉来避开。

当你调用有危险的方法时,编译器需要你对这件事情有所表示。大部分情况下这代表说得把此调用包在try/catch块中。但也可以实行不同的方案:

把它duck掉以让调用你的方法的程序来catch该异常。

这很容易,你只要表示出你会再throw此异常就好。方法抛出异常时,方法会从栈上立即被取出,而异常会再度丢给栈上的方法,也就是调用方,如果调用方是个ducker,则此ducker也会从栈被取出,异常再度抛给此时栈上方的方法。

ducking只是在踢皮球

早晚还是得有人来处理这件事。但若连main()也duck掉异常呢?

public class Washer {​    Laundry laundry = new Laundry();​    public void foo() throws ClothingException { //foo()将异常duck掉​        laundry.doLaundry();​    }​    public static void main (String[] args) throws ClothingException { /*main()将异常duck掉,foo()和main()都躲避异常,没人来处理*/​        Washer a = new Washer();                                       ​        a.foo();​    }}

异常被duck掉也可以通过编译,然后运行过程依次为:

1)抛出ClothingException

main()调用foo();foo()调用doLaundry();doLaundry()出错抛出ClothingException

2)foo()已经duck掉异常

doLaundry()从stack上(栈上)被取走;异常抛给foo()

3)连mian()也duck掉异常

foo()也被取走…最后只剩下Java虚拟机,你知道这家伙对异常是没有什么责任感的

4)Java虚拟机只好死给你看

114.P337

处理或声明,做个堂堂正正的程序员

我们已经看过两种满足编译器的有风险方法调用方式。

1)处理。

把有风险的调用包在try/catch块中

try {​    laundry.doLaundry();} catch(ClothingException cex) {​    //恢复程序代码}

2)声明。

把method声明成跟有风险的调用一样会抛出相同的异常

void foo() throws ClothingException { /*有声明会抛出异常,但没有try/catch块,所以就会duck掉异常留给调用方*/​    laundry.doLaundry();}

这代表调用foo()的程序必须要处理或也跟着声明异常:

public class Washer {​    Laundry laundry = new Laundry();​    public void foo() throws ClothingException {​        laundry.doLaundry();​    }​    public static void mian (String[] args) { /*有问题:无法通过编译,且会有"unreported exception"错误信息*/​        Washer a = new Washer();​        a.foo(); //要不就用try/catch块包起来,不然就duck掉​    }}

115.P338

异常处理规则

1)catch与finally不能没有try

2)try一定要有catch或finally

3)try与catch之间不能有程序

try {​    x.doStuff();}int y = 43; //不能在这里放程序catch(Exception ex) { }

4)只带有finally的try必须要声明异常

void go() throws FooException {​    try {​        x.doStuff();​    } finally { }}

116.P351

异常处理使用范例:

class MyEx extends Exception{};public class ExTestDrive {​    public static void main(String[] args) {​        String test = "yes"; //根据需要改为yes或no​        try {​            System.out.print("t");​            System.out.print("h");​            doRisky(test);​            System.out.print("o");​        } catch (MyEx e) {​            System.out.print("a");​        } finally {​            System.out.print("w");​        }​        System.out.print("s");​    }​    static void doRisky(String t) throws MyEx {​        if ("yes" .equals(t)) {​            throw new MyEx();​        }​        System.out.print("r");​    }}

------------------------------以上为基础语法

117.第353页,P353

图形用户接口

如果想要知道按钮的事情,就会监听事件的的接口。监听接口是介于监听与事件源之间的桥梁。

Swing的GUI组件是事件的来源。以Java的术语来说,事件来源是个可以将用户操作(点击鼠标、按键、关闭窗口等)转换成事件的对象。

对Java而言,事件几乎都是以对象来表示。它会是某种事件类的对象。如果你查询API中的java.awt.event这个包,就会看到一组事件的类(名称中都有Event)。你会看到MouseEvent、KeyEvent、WindowEvent、ActionEvent等等。

事件源(例如按钮)会在用户做出相关动作时(按下按钮)产生事件对象。你的程序在大多数的情况下是事件的接受方而不是创建方。

每个事件类型都有相对应的监听者接口。想要接收MouseEvent的话就实现MouseListener这个接口。想要WindowEvent吗?实现WindowListener。

记得接口的规则:要实现接口就得声明这件事,这代表你必须把接口中所有的方法都实现出来。

某些接口不止有一个方法,因为事件本身就有不同的形态。以MouseListener为例,事件就有mousePressed、mouseReleased、MouseMoved等。虽然都是MouseEvent,每个鼠标事件都在接口中有不同的方法。如果有实现MouseListener的话,mousePressed()就会在用户按下鼠标的时候被调用。

mouseReleased()会在用户松开鼠标时被调用。因此鼠标事件只有MouseEvent一种事件对象,却有不同的事件方法来表示不同类型的鼠标事件。

118.第359页,P359

监听和事件源如何沟通

监听

如果类想要知道按钮的ActionEvent,你就得实现ActionListener这个接口。按钮需要知道你关注的部分,因此要通过调用addActionListener(this)并传入ActionListener的引用(此例中就是你自己的这个程序,所以使用this)来向按钮注册。按钮会在该事件发生时调用该接口上的方法。

而作为一个ActionListener,编译器会确保你实现此接口的actionPerformed()。

事件源

按钮是ActionEvent的来源,因此它必须要知道有哪些对象是需要事件通知的。此按钮有个addActionListener()可以提供对事件有兴趣的对象(listener)一种表达此兴趣的方法。

当按钮的addActionListener()方法被调用时(因为某个listener的调用),它的参数会被某个按钮存到清单中。当用户按下按钮时,按钮会通过调用清单上每个监听的actionPerformed()来启动事件。

例:

无监听接口的按钮程序:

import javax.swing.*;public class SimpleGui1 {​    public static void main(String[] args) {​        JFrame frame = new JFrame();​        JButton button = new JButton("click me");​        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);​        frame.getContentPane().add(button);​        frame.setSize(300,300);​        frame.setVisible(true);​    }}

有监听接口的按钮程序:

import javax.swing.*;import java.awt.event.*;public class SimpleGui1B implements ActionListener { /*实现ActionListener这个接口*/​    JButton button;                        /*implements表示SimpleGui1B是个ActionListener(事件只会通知有实现ActionListener的类)*/​    public static void main(String[] args) {​        SimpleGui1B gui = new SimpleGui1B();​        gui.go();​    }​    public void go() {​        JFrame frame = new JFrame();​        button = new JButton("click me");​        button.addActionListener(this); //向按钮注册(告诉它你要监听事件)​        frame.getContentPane().add(button);​        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);​        frame.setSize(300,300);​        frame.setVisible(true);​    }​    public void actionPerformed(ActionEvent event) { /*定义事件处理的方法(即实现接口上的方法)*/​        button.setText("I've been clicked!"); /*按钮会以ActionEvent对象作为参数来调用此方法*/​    }}

/*问:在java里actionPerformed是做什么用的?

答:public void actionPerformed(ActionEvent e)这是接口ActionListener里面定义的一个抽象方法,所有实现这个接口的类都要重写这个方法。

一般情况下,这是在编写GUI程序时,组件发生“有意义”的事件时会调用这个方法,比如按钮被按下,

文本框内输入回车时都会触发这个事件,然后调用你编写的事件处理程序。

实现的路径过程如下:编写一个ActionListener类的侦听器,组件注册该侦听器,侦听器内部要编写这个actionPerformed方法*/

119.第364页,P364

自己创建的绘图组件

如果你要在屏幕上放上自己的图形,最好的方式是自己创建出有绘图功能的widget。你把widget放在frame上,如同按钮或其他的widget一样,不同之处在于它会按照你所要的方式绘制。你还可以让图形移动、表现动画效果或在点选的时候改变颜色。

简单的不得了。

你只需要创建JPanel的子类(你的panel)并覆盖掉paintComponent()这个方法。

所有绘图程序代码都在paintComponent(),如果你要画长方形就写画长方形的程序,如果你要画的是圆圈,就写画圆圈的程序。

当你的panel所处的frame显示的时候,paintComponent()就会被调用。

还有一件事,你不能自己调用这个方法!你必须通过系统内置的跟实际屏幕有关的Graphics对象来调用paintComponent(),你无法取得这个对象,它必须由系统来交给你。然而,你还是可以调用reapint()来要求系统重新绘制显示装置,然后才会产生paintComponent()的调用。

在每个Graphics引用的后面都有个Graphics2D对象,从JAVA的API文档查询可以得知,Graphics下有两个子集DebugGraphics和Graphics2D。

paintComponent()的参数被声明为Graphics类型(java.awt.Graphics)。

​ public void paintComponent(Graphics g) { }

因此参数g是个Graphics对象。这代表它可能是个Graphics的子类(因为多态的缘故),事实上就是这样。

由g参数所引用的对象实际上是个Graphics2D的实例。

为何要知道?因为有些在Graphics2D引用上可以做的事情不能在Graphics引用上做。Graphics2D对象可以做的事情比Graphics对象更多,实际上躲在Graphics引用的后面是个Graphics2D对象。

因此Graphics对象的底限是这样的:

如果你要调用Graphics2D类的方法,就不能直接使用g参数。但你可以将它转换成Graphics2D变量。

​ Graphics2D g2d = (Graphics2D) g;

例:

import javax.swing.*;import java.awt.*;public class MyDrawPanel extends JPanel {​    public void paintComponent(Graphics g) {​        Graphics2D g2d = (Graphics2D) g;​        GradientPaint gradient = new GradientPaint(70,70, Color.blue,150,150, Color.orange);​        /*查询API文档:GradientPaint的一种调用方法,​          GradientPaint(float x1, float y1, Color color1, float x2, float y2, Color color2)*/​        g2d.setPaint(gradient);​        g2d.fillOval(70,70,100,100);​    }}

120.第370页,P370

一个frame上面怎么摆两个widget?

frame默认有5个区域可以安置widget,每个区域只能安置一项。

frame.getContentPane().add(BorderLayout.SOUTH, button) /将按钮加到frame边框内的下面区域/

/查询API文档:frame的ContentPane()上可以加BorderLayout.NORTH,BorderLayout.SOUTH,BorderLayout.WEST,BorderLayout.EAST,BorderLayout.CENTER/

例:按下按钮,圆圈就会改变颜色

//绘制图形并填充颜色程序

import javax.swing.*;import java.awt.*;public class MyDrawPanel extends JPanel { /*这个方法会在重新绘制frame的时候被调用*/​    public void paintComponent(Graphics g) {​        Graphics2D g2d = (Graphics2D) g;​        int red = (int) (Math.random() * 255);​        int green = (int) (Math.random() * 255);​        int blue = (int) (Math.random() * 255);​        Color startColor = new Color(red, green, blue);​        red = (int) (Math.random() * 255);​        green = (int) (Math.random() * 255);​        blue = (int) (Math.random() * 255);​        Color endColor = new Color(red, green, blue);​        GradientPaint gradient = new GradientPaint(70,70, startColor,150,150, endColor);​        g2d.setPaint(gradient);​        g2d.fillOval(70,70,100,100);​    }}

//主程序

import javax.swing.*;import java.awt.*;import java.awt.event.*;public class SimpleGui3C implements ActionListener {​    JFrame frame;​    public static void main(String[] args) {​        SimpleGui3C gui = new SimpleGui3C();​        gui.go();​    }​    public void go() {​        frame = new JFrame();​        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);​        JButton button = new JButton("Change colors");​        button.addActionListener(this); //把监听加到按钮上​        MyDrawPanel drawPanel = new MyDrawPanel();​        frame.getContentPane().add(BorderLayout.SOUTH, button); /*依照指定区域把widget放上去(把按钮放到下面)*/​        frame.getContentPane().add(BorderLayout.CENTER, drawPanel); /*依照指定区域把widget放上去(把画图程序放到中间)*/​        frame.setSize(300,300);​        frame.setVisible(true);​    }​    public void actionPerformed(ActionEvent event) {​        frame.repaint(); //当用户按下按钮时就要求frame重新绘制​    }}

121.P372

尝试两个按钮

当每个按钮执行不同工作时,要如何对两个不同的按钮分别取得事件?

1)选项一:实现两个actionPerformed()方法

其实不能这么做!你不能实现同一个类的同一个方法两次,这过不了编译这一关。就算可以,事件源怎么分得出要调用哪一个?

2)选项二:对两个按钮注册同一个监听口

可以是可以啦,但这看起来不太像面向对象。用单一的事件处理程序对付不同的东西意味着执行太多不同工作的方法。如果想要改变某个工作,很可能会把全部工作都弄乱。这样解决会对可读性和维护工作产生危害。

3)选项三:创建不同的ActionListener

class ColorButtonListener implements ActionListener { }

class LabelButtonListener implements ActionListener { }

这些类没有办法存取到所需的变量。你可以加以改正,但却又会破坏封装特性,这样会加深混乱和复杂的程度。

内部类是我们的救星!

内部类可以使用外部所有的方法与变量,就算是私用的也一样。内部类把存取外部类的方法和变量当作是开自家冰箱。

class MyOuterClass {​    private int x;​    class MyInnerClass {​        void go() {​            x = 42; //把x当作是自己的​        }​    } //关闭内部类,内部类完全被外部的类包起来} //关闭外部类

内部类的实例一定会绑在外部类的实例上

任意一个内部类可以存取其他外部类的方法和变量吗?不行!只能存取它所属的那一个!

如何创建内部类的实例?

如果你从外部类程序代码中初始化内部的类,此内部对象会绑在该外部对象上。

外部类的程序代码可以用初始化其他类完全相同的方法初始它所包容的内部类。

class MyOuterClass {​    private int x;​    MyInner inner = new MyInner(); //创建内部类的实例​    public void doStuff() {​        inner.go(); //调用内部的方法​    }​    class MyInner {​        void go() {​            x = 42; //内部可以使用外部的x变量​        }​    } //关闭内部类} //关闭外部类

122.P379

现在可以实现两个按钮的程序:

import javax.swing.*;import java.awt.*;import java.awt.event.*;public class TwoButtons { //现在主要的GUI并不实现ActionListener​        JFrame frame;​        JLabel label;​        public static void main(String[] args) {​            TwoButtons gui = new TwoButtons();​            gui.go();​        }​        public void go() {​            frame = new JFrame();​            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);​            JButton labelbutton = new JButton("Change Label");​            labelbutton.addActionListener(new LabelListener());/*相当于将this传给监听的注册方法,现在传的是对应的实例*/​            JButton colorbutton = new JButton("Change Circle");​            colorbutton.addActionListener(new ColorListener());​            label = new JLabel("I'm a label");​            MyDrawPanel drawPanel = new MyDrawPanel();​            frame.getContentPane().add(BorderLayout.SOUTH, colorbutton);​            frame.getContentPane().add(BorderLayout.CENTER, drawPanel);​            frame.getContentPane().add(BorderLayout.EAST,labelbutton);​            frame.getContentPane().add(BorderLayout.WEST,label);​            frame.setSize(300,300);​            frame.setVisible(true);​        }​        class LabelListener implements ActionListener { /*终于可以在单一的类(TwoButtons类)中做出不同的ActionListener*/​            public void actionPerformed(ActionEvent event) {​                label.setText("Ouch!");​            }​        }​        class ColorListener implements ActionListener {​            public void actionPerformed(ActionEvent event) {​                frame.repaint();​            }​        }}

123.P380

问:内部类有什么重要?

答:怎么说呢,我们提供在一个类中实现同一接口的多次机会。要知道,在一般的类中是不能实现同一个方法两次的。

但使用了内部类之后就可以了,所以你可以用不同方法实现同一接口。

接口的实现可以超过一个,但类仅能继承一个而已。

很好,没错!你不能同时又是Dog又是按钮,但有时又必须这样。解决办法是Dog可以继承Animal同时有个内部类来代表按钮的行为,因此在有需要的时候Dog就可以派出内部的类来代表按钮。也就是说Dog虽然不能x.takeButton(this)但是可以x.takeButton(new DogInnerButton())。

124.P382

以内部类执行动画效果

我们已经看过为何内部类对事件的监听是很方便的,因为你会对相同的事件处理程序实现一次以上。现在我们来看看当内部类被用来当作某种外部类无法继承的子类时是多么好用。

我们的目标是要创建出简单的动画,让圆圈从画面左上方移动到右下方:

import javax.swing.*;import java.awt.*;public class SimpleAnimation {​    int x = 70;​    int y = 70;​    public static void main(String[] args) {​        SimpleAnimation gui = new SimpleAnimation();​        gui.go();​    }​    public void go() {​        JFrame frame = new JFrame();​        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);​        MyDrawPanel drawPanel = new MyDrawPanel(); /*此处跟前面一样创建出frame上的widget*/​        frame.getContentPane().add(drawPanel);​        frame.setSize(300,300);​        frame.setVisible(true);​        for (int i = 0; i < 130; i++) {​            x++;​            y++;​            drawPanel.repaint();​            try {​                Thread.sleep(50); //加上延迟刻意放慢,不然一下就会跑完​            } catch (Exception ex) { }​        }​    }​    class MyDrawPanel extends JPanel {​        public void paintComponent(Graphics g) {​            g.setColor(Color.white); //查询API文档:设置背景颜色​            g.fillRect(0,0,this.getWidth(),this.getHeight()); /*查询API文档:取得边界,填充背景颜色。​            fillRect(int x, int y, int width, int height).Fills the specified rectangle.*/​            g.setColor(Color.green); ​            g.fillOval(x,y,40,40); /*使用外部的坐标来更新。查询API文档:fillOval(int x, int y, int width, int height).​                                    Fills an oval bounded by the specified rectangle with the current color.*/​        }​    }}

125.P399

运用Swing

这一章会讨论Swing和布局管理器,以及更多有关widget的事情

Swing的组件

组件(componenet,或称元件)是比我们之前所称的widget更为正确的术语。他们就是你放在GUI上面的东西。这些东西是用户会看到并与交互的。像是Text Field、button、scrollable list、radio button等。事实上所有的组件都是继承自javax.swing.JComponent。

组件是可以嵌套的

在Swing中,几乎所有组件都能够安置其他组件。也就是说,你可以把任何东西放在其他东西上。但在大部分情况下,你会把像是按钮或列表等用户交互组件放在框架和面板等背景组件上。

创建GUI四个步骤的回顾:

1)创建window(JFrame)。

JFrame frame = new JFrame();

2)创建组件。

JButton button = new JButton("click me");

3)把组件加到frame上。

frame.getContentPane().add(BorderLayout.EAST, button);

4)显示出来。

frame.setSize(300,300);frame.setVisible(true);

126.P401

布局管理器(Layout Managers)

布局管理器是个与特定组件相关联的Java对象,它大多数是背景组件。布局管理器用来控制所关联组件上携带的其他组件。

也就是说,如果某个框架带有面板,而面板带有按钮,则面板的布局管理器控制着按钮的大小与位置,而框架的布局管理器控制着面板的大小与位置。按钮因为没有携带其他组件,所以不需要布局管理器。

不同的布局管理器有不同的策略

有些布局管理器会尊重组件的想法。如果按钮想要30 X 50像素,布局管理器就会给它这么大的面积。其他的布局管理器可能只会尊重部分的设定。如果此时按钮想要30 X 50像素,会有30宽,但高度得要跟着面板的设定。有些会让所有的组件都设定成相同的宽带。在某些情况下,布局管理器的工作是非常复杂的。但大部分情况下你都可以预测它的输出结果。

世界三大首席管理器:

BorderLayout

FlowLayout

BoxLayout

BorderLayout布局的5个区域:东区、西区、北区、南区与中央区(中间区域只能捡剩下的,即中央的组件大小要看扣除周围之后还剩下些什么)

将一个按钮加入东区:

import javax.swing.*;import java.awt.*;public class Button1 {​    public static void main(String[] args) {​        Button1 gui = new Button1();​        gui.go();​    }​    public void go() {​        JFrame frame = new JFrame();​        JButton button = new JButton("click me");​        frame.getContentPane().add(BorderLayout.EAST,button);​        frame.setSize(200,200);​        frame.setVisible(true);​    }}

127.P406

BorderLayout中让按钮要求更多的高度

怎么做?按钮已经是最宽了——跟框架一样,但我们可以用更大的字体来让它更高:

import javax.swing.*;import java.awt.*;public class Button1 {​    public static void main(String[] args) {​        Button1 gui = new Button1();​        gui.go();​    }​    public void go() {​        JFrame frame = new JFrame();​        JButton button = new JButton("click this");​        Font bigFont = new Font("serif", Font.BOLD, 28); /*查询API文档:SERIF,A String constant for the canonical family name of the logical font "Serif".*/​        button.setFont(bigFont); //更大的字体会强迫框架留更多的高度给按钮​        frame.getContentPane().add(BorderLayout.NORTH,button);​        frame.setSize(200,200);​        frame.setVisible(true);​    }}

128.P410

如何创建两个按钮?

先创建面板,然后创建两个按钮加到面板上,然后将面板加到frame上

import javax.swing.*;import java.awt.*;public class Button1 {​    public static void main(String[] args) {​        Button1 gui = new Button1();​        gui.go();​    }​    public void go() {​        JFrame frame = new JFrame();​        JPanel panel = new JPanel(); //创建面板​        panel.setBackground(Color.darkGray);​        JButton button = new JButton("click this"); //创建两个按钮​        JButton buttontwo = new JButton("bliss");​        panel.add(button); //将两个按钮加到面板上​        panel.add(buttontwo);​        frame.getContentPane().add(BorderLayout.EAST,panel); /*将面板加到frame上*/​        frame.setSize(200,200);​        frame.setVisible(true);​    }}

129.P411

使用BoxLayout布局,就算够宽它还是会垂直排列。

不像FlowLayout布局,就算水平宽度足以容纳组件,它还是会用新的行来排列组件。

import javax.swing.*;import java.awt.*;public class Button1 {​    public static void main(String[] args) {​        Button1 gui = new Button1();​        gui.go();​    }​    public void go() {​        JFrame frame = new JFrame();​        JPanel panel = new JPanel();​        panel.setBackground(Color.darkGray);​        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));/*把布局管理器换掉,查询API文档:​        BoxLayout(Container target, int axis),Creates a layout manager that will lay out components along the given axis.​        BoxLayout的构造函数需要知道要管理哪个组件以及使用哪个轴*/​        JButton button = new JButton("shock me");​        JButton buttontwo = new JButton("bliss");​        panel.add(button);​        panel.add(buttontwo);​        frame.getContentPane().add(BorderLayout.EAST,panel);​        frame.setSize(200,200);​        frame.setVisible(true);​    }}

130.P412

问:框架为什么不能像面板那样直接地加上组件?

答:JFrame会这么特殊是因为它是让事物显示在画面上的接点。因为Swing的组件纯粹由Java构成,JFrame必须要连接到底层的操作系统以便来存取显示装置。我们可以把面板(JPanel)想作是安置在JFrame上的100%纯Java层。或者把JFrame想作是支撑面板(JPanel)的框架。

你甚至可以用自定义的JPanel来换掉框架的面板:myFrame.setContentPane(myPanel);

131.P413

操作Swing组件

你已经看过布局管理器的基本说明,因此现在就让我们来看一下几个最常用的组件:text field、可滚动的text area、checkbox以及list。

我们不打算把整个API都拿来说一遍,只讲几个重点。

JTextField构造函数:

JTextField field = new JTextField(20); //20代表20字宽而不是像素JTextField field = new JTextField("Your name");

如何使用:

1)取得文本内容

System.out.println(field.getText());

2)设定文本内容

field.setText("whatever");field.setText(""); //清空字段

3)取得用户输入完毕按下return或enter键的事件

field.addActionListener(myActionListener);

4)选取文本字段的内容

field.selectAll();

5)把GUI目前焦点拉回到文本字段以便让用户进行输入操作

field.requestFocus();

132.P414

不像JTextField,JTextArea可以有超过一行以上的文字。此外若要让JTextArea滚动,就必须要把它粘在ScrollPane上。ScrollPane是个非常喜欢滚动的对象,并也会考虑文本区域的滚动需求。

构造函数

JTextArea text = new JTextArea(10,20) //代表10行高,20字宽

JTextArea范例:

import javax.swing.*;import java.awt.*;import java.awt.event.*;public class TextArea1 implements ActionListener {​    JTextArea text;​     public static void main(String[] args) {​         TextArea1 gui = new TextArea1();​         gui.go();​     }​     public void go() {​         JFrame frame = new JFrame();​         JPanel panel = new JPanel();​         JButton button = new JButton("Just Click it");​         button.addActionListener(this);​         text = new JTextArea(10,20);​         text.setLineWrap(true); //启动自动换行​         JScrollPane scroller = new JScrollPane(text);​         scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);​         scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); /*指定只使用垂直滚动条,​         查询API文档:setHorizontalScrollBarPolicy(int policy),​         Determines when the horizontal scrollbar appears in the scrollpane.​          setVerticalScrollBarPolicy(int policy),​         Determines when the vertical scrollbar appears in the scrollpane.​          VERTICAL_SCROLLBAR_ALWAYS,​         Used to set the vertical scroll bar policy so that vertical scrollbars are always displayed. ​          HORIZONTAL_SCROLLBAR_NEVER,​         Used to set the horizontal scroll bar policy so that horizontal scrollbars are never displayed.*/​         panel.add(scroller);​         frame.getContentPane().add(BorderLayout.CENTER, panel);​         frame.getContentPane().add(BorderLayout.SOUTH, button);​         frame.setSize(350, 300);​         frame.setVisible(true);​     }​     public void actionPerformed(ActionEvent ev) {​         text.append("button clicked \n");​     }}

133.P416

JCheckBox

构造函数

JCheckBox check = new JCheckBox("Goes to 11");

如何使用:

1)监听item的事件(被选取或变成非选取)。

check.addItemListener(this);

2)处理事件(判别是否被选取)。

public void itemStateChanged(ItemEvent ev) {​    String onOrOff = "off";​    if (check.isSelected()) onOrOff = "on";​    System.out.println("Check box is " + onOrOff);}

3)用程序来选取或不选取。

check.setSelected(true);check.setSelected(false);

134.P417

JList

构造函数

String [] listEntries = {"alpha","beta","gama","delta","epsilon","zeta","eta","theta"};list = new JList(listEntries);

如何使用:

1)让它只显示垂直的滚动条。

JScrollPane scroller = new JScrollPane(list);scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);panel.add(scroller);

2)设定显示的行数。

list.setVisibleRowCount(4);

3)限制用户只能选取一个项目。

list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

4)对选择事件做注册。

list.addListSelectionListener(this);

5)处理事件(判断选了哪个项目)。

public void valueChanged(ListSelectionEvent lse) {​    if (!lse.getValueIsAdjusting()) { /*如果没有加上这个if测试,你会得到两次的事件*/​        String selection = (String) list.getSelectedValue(); /*这会返回一个Object,不一定是个String*/​        System.out.println(selection);​    }}

135.P429

序列化和文件的输入/输出

保存对象

对象可以被序列化也可以展开。对象有行为和状态两种属性。行为存在于类中,而状态存在于个别的对象中。所以需要存储对象状态的时候会发生什么事?如果你正在编写游戏,就得有存储和恢复游戏的功能。如果你编写的是创建图表的程序,也必须要有存储/打开的功能。

如果程序需要存储状态,你可以来硬的,对每一个对象,逐个地把每项变量的值写到特定格式的文件中。或者,你也可以用面向对象的方式来做——只要把对象本身给冻干/碾平/保存/脱水,并加以重组/展开/恢复/泡开成原状。但有时这还得来硬的,特别是在程序所存储的文件需要给某些非Java的应用程序所读取时,所以这一章会讨论这两种方式。

基本所需的输入/输出技巧都一样:把数据写到某处,这可能是个磁盘上的文件,或者是来自网络上的串流。读取数据的方向则刚好相反。

当然此处所讨论的部分不涉及使用数据库的情况。

136.P432

将序列化对象写入文件:

1)创建出FileOutputStream

FileOutputStream fileStream = new FileOutputStream("MyGame.ser"); /*创建存取文件的FileOutputStream对象,MyGame.ser文件如果不存在,也会被自动创建出来*/

2)创建ObjectOutputStream

ObjectOutputStream os = new ObjectOutputStream(fileStream); /*fileStream能让你写入对象,但无法直接地连接文件,所以需要参数的指引*/

3)写入对象

os.writeObject(characterOne);os.writeObject(characterTwo);os.writeObject(characterThree); /*将变量所引用的对象序列化并写入MyGame.ser这个文件,任何放在此处的对象(characterThree)都必须要实现序列化,否则在执行期一定会出问题*/

4)关闭ObjectOutputStream

os.close(); //关闭所关联的输出串流

137.P433

数据在串流中移动

Java的输入/输出API带有连接类型的串流,它代表来源与目的地之间的连接,连接串流将串流与其他串流连接起来。

一般来说,串流需要两两连接才能作出有意义的事情——其中一个表示连接,另一个则是要被调用方法的。为何要两个?因为连接的串流通常都

是很低层的。以FileOutputStream为例,它有可以写入字节的方法。但我们通常不会直接写字节,而是以对象层次的观点来写入,所以需要高层的连接串流。

那又为何不以单一的串流来执行呢?这就要考虑到良好的面向对象设计了。每个类只要做好一件事。FileOutputStream把字节写入文件。

ObjectOutputStream把对象转换成可以写入串流的数据。当我们调用ObjectOutputStream的writeObject时,对象会被打成串流送到FileOutputStream来写入文件。

这样就可以通过不同的组合来达到最大的适应性!如果只有一种串流类的话,你只好祈祷API的设计人已经想好所有可能的排列组合。但通过链接的方式,你可以自由地安排串流的组合与去向。

138.P435

对象的状态是什么?有什么需要保存?

事情开始有趣了。存储primitive主数据类型值37和70是很简单的。但如果对象有引用到其他对象的实例变量时要怎么办?如果这些对象还带有其他对象又该如何?

当对象被序列化时,被该对象引用的实例变量也会被序列化。且所有被引用的对象也会被序列化……最棒的是,这些操作都是自动进行的!

序列化程序会将对象版图上的所有东西存储起来。被对象的实例变量所引用的所有对象都会被序列化。

例如:Kennel对象带有对Dog数组对象的引用。Dog[]中有两个Dog对象的引用。每个Dog对象带有String和Collar对象的引用。String对象维护字符的集合,而Collar对象持有一个int。

当保存Kennel对象时,所有的对象都会保存!

139.P437

如果要让类能够被序列化,就实现Serializable

Serializable接口又被称为marker或tag类的标记用接口,因为此接口并没有任何方法需要实现的。它的唯一目的就是声明有实现它的类是可以被序列化的。也就是说,此类型的对象可以通过序列化的机制来存储。如果某类是可序列化的,则它的子类也自动地可以序列化(接口的本意就是如此)。

例:

import java.io.*;public class Box implements Serializable { /*没有方法需要被实现,只是用来告诉Java虚拟机它可以被序列化*/​    private int width;​    private int height;​    public void setWidth(int w) {​        width = w;​    }​    public void setHeight(int h) {​        height = h;​    }​    public static void main(String[] args) {​        Box myBox = new Box();​        myBox.setWidth(50);​        myBox.setWidth(20);​        try {​            FileOutputStream fs = new FileOutputStream("foo.ser");​            ObjectOutputStream os = new ObjectOutputStream(fs); //设定链接fs​            os.writeObject(myBox);​            os.close();​        } catch (Exception ex) {​            ex.printStackTrace();​        }​    }}

140.P438

序列化是全有或全无的

整个对象版图都必须正确地序列化,不然就得全部失败。

如果Duck对象不能序列化,Pond对象就不能被序列化:

import java.io.*;public class Pond implements Serializable {​    private Duck duck = new Duck();​    public static void main(String[] args) {​        Pond myPond = new Pond();​        try {​            FileOutputStream fs = new FileOutputStream("Pond.ser");​            ObjectOutputStream os = new ObjectOutputStream(fs);​            os.writeObject(myPond); //将myPond序列化的同时Duck也会被序列化​            os.close();​        } catch (Exception ex) {​            ex.printStackTrace();​        }​    }}
public class Duck { //运行错误java.io.NotSerializableException: Duck​    //Duck的程序代码}

解决方法:将Duck序列化

import java.io.Serializable;public class Duck implements Serializable {​    //Duck的程序代码}

141.P439

如果某实例变量不能或不应该被序列化,就把它标记为transient(瞬时)的

如果你需要序列化程序能够跳过某个实例变量,就把它标记成transient的变量:

import java.net.*;class Chat implements Serializable {​    transient String currentID; //这会将此变量标记为不需要序列化的​    String userName; //这个变量会被序列化​    //还有更多程序代码……}

如果你有无法序列化的变量不能被存储,可以用transient这个关键词把它标记出来,序列化程序会把它跳过。

为什么有些变量不能被序列化?可能是设计者忘记实现Serializable。或者动态数据只可以在执行时求出而不能或不必存储。虽然Java函数库中大部分的类可以被序列化,你还是无法将网络联机之类的东西保存下来。它得要在执行期当场创建才有意义。一旦程序关闭之后,联机本身就不再有用,下次执行时需要重新创建出来。

142.P440

问:如果我使用了一个不能序列化的类,我能否把它的子类标记为可序列化的类?

答:可以!如果该类是可以被继承(没有被标记为final),你就可以制作出可被序列化的子类。但这又带来另外一个问题:为什么它一开始不是可序列化的?

问:这位同学你问得很好,为什么会有不可序列化的可序列化子类?

答:这,这,这……首先要看类被还原的时候会发生什么事(稍后会讨论)。简单讲,当对象被还原且它的父类不可序列化时,父类的构造函数会跟创建新的对象一样地执行。如果类没有什么好理由不能被序列化,制作可序列化的子类会是个好方法。

问:我现在才了解,如果你将某个变量标记为transient的,那就代表说在序列化的过程中该变量会被略过。然后会发生什么事?我们用transient标记变量来解决不能序列化的实例变量问题,但我们不是在回复对象的时候需要该变量吗?序列化的重点不就在于保存对象的状态吗?

答:没错!问题就在这里,但是幸好有解决方法。如果你把某个对象序列化,transient的引用实例变量会以null返回,而不管存储当时它的值是什么。这代表整个对象版图中连接到该特定实例变量的部分不会被存储。这样可能会有问题,所以我们有两个解决方案:

1)当对象被带回来的时候,重新初始化实例变量回到默认的状态。例如Dog可能会有Collar,但因为Collar无关紧要,所以弄个新的给Dog也不会

影响程序逻辑。

2)如果transient变量的值很重要,例如Collar的颜色是有意义且每个Dog不一样的,你就得需要同时把它的值也保存下来。然后在带回Dog对象时重新创建Collar,再把颜色值设定给Collar。

问:如果两个对象都有引用实例变量指向相同的对象会怎样?例如两个Cat都有相同的Owner对象?那Owner会被存储两次吗?

答:好问题!序列化聪明得足以分辨两个对象是否相同。在此情况下只有一个对象会被存储,其他引用会复原成指向该对象。

143.P441

解序列化(Deserialization):还原对象

将对象序列化整件事情的重点在于你可以在事后,在不同的Java虚拟机执行期(甚至不是同一个Java虚拟机),把对象恢复到存储时的状态。解序列化有点像是序列化的反向操作。

1)创建FileInputStream

FileInputStream fileStream = new FileInputStream("MyGame.ser"); /*如果MyGame.ser不存在就会抛出异常*/

2)创建ObjectInputStream

ObjectInputStream os = new ObjectInputStream(fileSteam);

3)读取对象

Object one = os.readObject();Object two = o.readObject();Object three = os.readObject(); /*每次调用readObject()都会从stream中读出下一个对象,读取顺序与写入顺序相同,次数超过会抛出异常*/

4)转换对象类型

GameCharacter elf = (GameCharacter) one;GameCharacter troll = (GameCharacter) two;GameCharacter magician = (GameCharacter) three; /*返回值是Object类型,因此必须要转换类型*/

5)关闭ObjectInputStream

os.close(); //FileInputStream会自动跟着关掉

144.P442

解序列化的时候发生了什么事?

当对象被解序列化时,Java虚拟机会通过尝试在堆上创建新的对象,让它维持与被序列化时有相同的状态来恢复对象的原状。但这当然不包括transient的变量,它们不是null(对对象引用而言)不然就是使用primitive主数据类型的默认值。

1)对象从stream中读出来。

2)Java虚拟机通过存储的信息判断出对象的class类型。

3)Java虚拟机尝试寻找和加载对象的类。如果Java虚拟机找不到或无法加载该类,则Java虚拟机会抛出例外。

4)新的对象会被配置在堆上,但构造函数不会执行!很明显的,这样会把对象的状态抹去变成全新的,而这不是我们想要的结果。我们需要的是对象回到存储是的状态。

5)如果对象在继承树上有个不可序列化的祖先类,则该不可序列化类以及在它之上的类的构造函数(就算是可序列化也一样)就会执行。一旦构造函数连锁启动之后将无法停止。也就是说,从第一个不可序列化的父类开始,全部都会重新初始状态。

6)对象的实例变量会被还原成序列化时点的状态值。transient变量会被赋值null的对象引用或primitive主数据类型的默认值为0、false等值。

145.P443

问:为什么类不会存储成对象的一部分?这样就不会出现找不到类的问题?

答:当然也可以设计成这个样子,但这样是非常的浪费且会有很多额外的工作。虽然把对象序列化写在本机的硬盘上面不是什么很困难的工作,但序列化也有将对象送到网络联机上的用途。如果每个序列化对象都带有类,带宽的消耗可能就是个大问题。

对于通过网络传送序列化对象来说,事实上是有一种机制可以让类使用URL来指定位置,该机制用在Java的Remote Method Invocation(RMI,远程程序调用机制),让你可以把序列化的对象当做参数的一部分来传送,若接收此调用的Java虚拟机没有这个类的话,它可以自动地使用URL来取回并加装该类(第17章会讨论RMI)。

问:那静态变量呢?它们会被序列化吗?

答:不会。要记得static代表“每个类一个”而不是“每个对象一个”。当对象被还原时,静态变量会维持类中原本的样子,而不是存储时的样子

146.P444

序列化范例

存储与恢复游戏人物:

import java.io.*;public class GameSaverTest {​    public static void main(String[] args) {​        GameCharacter one = new GameCharacter(50, "Elf", new String[] {"bow","sword","dust"});​        GameCharacter two = new GameCharacter(200, "Troll", new String[] {"bare hands","big ax"});​        GameCharacter three = new GameCharacter(120, "Magician", new String[] {"spells","invisiblity"});​        try {​            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Game.ser"));​            os.writeObject(one);​            os.writeObject(two);​            os.writeObject(three);​            os.close();​        } catch (IOException ex) {​            ex.printStackTrace();​        }​        one = null; //设定成null,因此无法存取堆上的这些对象​        two = null;​        three = null;​        try {​            ObjectInputStream is = new ObjectInputStream(new FileInputStream("Game.ser"));​            GameCharacter oneRestore = (GameCharacter) is.readObject();​            GameCharacter twoRestore = (GameCharacter) is.readObject();​            GameCharacter threeRestore = (GameCharacter) is.readObject();​            System.out.println("One's type: " + oneRestore.getType());​            System.out.println("Two's type: " + twoRestore.getType());​            System.out.println("Three's type: " + threeRestore.getType());​        } catch (Exception ex) {​            ex.printStackTrace();​        }​    }}
import java.io.*;public class GameCharacter implements Serializable {​    int power;​    String type;​    String weapons[];​    public GameCharacter(int p,String t, String[] w) {​        power = p;​        type = t;​        weapons = w;​    }​    public int getPower() {​        return power;​    }​    public String getType() {​        return type;​    }​    public String getWeapons() {​        String weaponList = "";​        for (int i = 0; i < weapons.length; i++) {​            weaponList += weapons[i] + " ";​        }​        return weaponList;​    }}

147.P447

将字符串写入文本文件

通过序列化来存储对象是Java程序在来回执行间存储和恢复数据最简单的方式。但有时你还得把数据存储到单纯的文本文件中。假设你的Java程序必须把数据写到文本文件中以让其他可能是非Java的程序读取。例如你的servlet(在Web服务器上执行的Java程序)会读取用户在网页上输入的数据,并将它写入文本文件以让网站管理人能够用电子表格来分析数据。

写入文本数据(字符串)与写入对象是很类似的,你可以使用FileWrite来代替FileOutputStream(当然不会把它链接到ObjectOutputStream上)。

写字符串:

fileWriter.write("My first String to save");

例:

import java.io.*;public class WriteAFile {​    public static void main(String[] args) {​        try {​            FileWriter writer = new FileWriter("Foo.txt"); /*Foo.txt如果不存在就会被创造*/​            writer.write("hello foo!"); //以字符串作参数​            writer.close();​        } catch (IOException ex) {​            ex.printStackTrace();​        }​    }}

148.P448

文本文件范例:e-Flashcard

QuizCardBuilder和QuizCard代码:

import java.util.*;import java.awt.event.*;import javax.swing.*;import java.awt.*;import java.io.*;public class QuizCardBuilder {​    private JTextArea question;​    private JTextArea answer;​    private ArrayList<QuizCard> cardList;​    private JFrame frame;​    // additional, bonus method not found in any book!​    public static void main (String[] args) {​        QuizCardBuilder builder = new QuizCardBuilder();​        builder.go();​    }​    public void go() {​        // build gui​        frame = new JFrame("Quiz Card Builder");​        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  // title bar​        JPanel mainPanel = new JPanel();​        Font bigFont = new Font("sanserif", Font.BOLD, 24);​        question = new JTextArea(6,20);​        question.setLineWrap(true);​        question.setWrapStyleWord(true);​        question.setFont(bigFont);​        JScrollPane qScroller = new JScrollPane(question);​        qScroller.setVerticalScrollBarPolicy(​                ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);​        qScroller.setHorizontalScrollBarPolicy(​                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);​        answer = new JTextArea(6,20);​        answer.setLineWrap(true);​        answer.setWrapStyleWord(true);​        answer.setFont(bigFont);​        JScrollPane aScroller = new JScrollPane(answer);​        aScroller.setVerticalScrollBarPolicy(​                ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);​        aScroller.setHorizontalScrollBarPolicy(​                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);​        JButton nextButton = new JButton("Next Card");​        cardList = new ArrayList<QuizCard>();​        JLabel qLabel = new JLabel("Question:");​        JLabel aLabel = new JLabel("Answer:");​        mainPanel.add(qLabel);​        mainPanel.add(qScroller);​        mainPanel.add(aLabel);​        mainPanel.add(aScroller);​        mainPanel.add(nextButton);​        nextButton.addActionListener(new NextCardListener());​        JMenuBar menuBar = new JMenuBar();​        JMenu fileMenu = new JMenu("File");​        JMenuItem newMenuItem = new JMenuItem("New");​        JMenuItem saveMenuItem = new JMenuItem("Save");​        newMenuItem.addActionListener(new NewMenuListener());​        saveMenuItem.addActionListener(new SaveMenuListener());​        fileMenu.add(newMenuItem);​        fileMenu.add(saveMenuItem);​        menuBar.add(fileMenu);​        frame.setJMenuBar(menuBar);​        frame.getContentPane().add(BorderLayout.CENTER, mainPanel);​        frame.setSize(500,600);​        frame.setVisible(true);​    }​    public class NextCardListener implements ActionListener {​        public void actionPerformed(ActionEvent ev) {​            QuizCard card = new QuizCard(question.getText(), answer.getText());​            cardList.add(card);​            clearCard();​        }​    }​    public class SaveMenuListener implements ActionListener {​        public void actionPerformed(ActionEvent ev) {​            QuizCard card = new QuizCard(question.getText(), answer.getText());​            cardList.add(card);​            /*调出存盘对话框(dialog)等待用户决定,这都是靠JFileChooser完成的*/​            JFileChooser fileSave = new JFileChooser();​            fileSave.showSaveDialog(frame);​            saveFile(fileSave.getSelectedFile());​        }​    }​    public class NewMenuListener implements ActionListener {​        public void actionPerformed(ActionEvent ev) {​            cardList.clear();​            clearCard();​        }​    }​    private void clearCard() {​        question.setText("");​        answer.setText("");​        question.requestFocus();​    }​    private void saveFile(File file) {​        try {​            BufferedWriter writer = new BufferedWriter(new FileWriter(file));​            Iterator cardIterator = cardList.iterator();​            /*iterator()方法是java.lang.Iterable接口的实现,该接口被Collection继承。Java中Iterator(迭代器)功能比较简单,并且只能单向移动。它可以使用next()获得序列中的下一个元素,使用hasNext()检查序列中是否还有元素。*/​            while (cardIterator.hasNext()) {​                /*将ArrayList中的卡片逐个写到文件中,一行一张卡片,问题和答案由“/”分开*/​                QuizCard card = (QuizCard) cardIterator.next();​                writer.write(card.getQuestion() + "/");​                writer.write(card.getAnswer() + "\n");​            }​            writer.close();​        } catch(IOException ex) {​            System.out.println("couldn't write the cardList out");​            ex.printStackTrace();​        }​    } // close method}
import java.io.*;public class QuizCard implements Serializable {​    private String uniqueID;​    private String category;​    private String question;​    private String answer;​    private String hint;​    public QuizCard(String q, String a) {​        question = q;​        answer = a;​    }​    public void setUniqueID(String id) {​        uniqueID = id;​    }​    public String getUniqueID() {​        return uniqueID;​    }​    public void setCategory(String c) {​        category = c;​    }​    public String getCategory() {​        return category;​    }​    public void setQuestion(String q) {​        question = q;​    }​    public String getQuestion() {​        return question;​    }​    public void setAnswer(String a) {​        answer = a;​    }​    public String getAnswer() {​        return answer;​    }​    public void setHint(String h) {​        hint = h;​    }​    public String getHint() {​        return hint;​    }}
/*如何玩:比如在Question下填111,在Answer填qqq;然后点击NextCard按钮,再在Question下填222,在Answer填www;然后点击左上角的File按钮,点Save,选择保存路径,输入文件名,点保存*/

149.P452

写入文件

java.io.File class

File这个类代表磁盘上的文件,但并不是文件中的内容。啥?你可以把File对象想象成文件的路径,而不是文件本身。例如File并没有读写文件的方法。关于File有个很有用的功能就是它提供一种比使用字符串文件名来表示文件更安全的方式。举例来说,在构造函数中取用字符串文件名的类也可以用File对象来代替该参数,以便检查路径是否合法等,然后再把对象传给FileWriter或FileInputStream。

你可以对File对象做的事情:

1)创建出代表现存盘文件的File对象。

File f = new File("MyCode.txt");

2)建立新的目录。

File dir = new File("Chapter7");dir.mkdir();

3)列出目录下的内容。

if(dir.isDirectory()) {​    String[] dirContents = dir.list();​    for (int i = 0; i < dirContents.length; i++) {​        System.out.println(dirContents[i]);​    }}

4)取得文件或目录的绝对路径。

System.out.println(dir.getAbsolutePath());

5)删除文件或目录(成功会返回true)。

boolean isDeleted = f.delete();

150.P453

缓冲区的奥妙之处

没有缓冲区,就好像逛超市没有推车一样。你只能一次拿一项东西结账。

BufferedWriter writer = new BufferedWriter(new FileWriter(aFile)); /*注意此处不需要持有对FileWriter对象的引用,我们只在乎BufferedWriter*/

缓冲区的奥妙之处在于使用缓冲区比没有使用缓冲区的效率更好,你也可以直接使用FileWriter,调用它的write()来写文件,但它每次都会直接写下去。你应该不会喜欢这种方式额外的成本,因为每趟磁盘操作都比内存操作要花费更多时间。通过BufferWriter和FileWriter的链接,BufferedWriter可以暂存一堆数据,然后到满的时候再实际写入磁盘,这样可以减少对磁盘操作的次数。

如果你想要强制缓冲区立即写入,只要调用writer.flush()这个方法就可以要求缓冲区马上把内容写下去。

151.P454

读取文本文件

从文本文件读数据是很简单的,但是这次我们会使用File对象来表示文件,以FileReader来执行实际的读取,并用BufferedReader来让读取

更有效率。

import java.io.*;class ReadAFile {​    public static void main(String[] args) {​        try{​            File myFile = new File("MyText.txt");​            FileReader fileReader = new FileReader(myFile);​            BufferedReader reader = new BufferedReader(fileReader); /*将FileReader链接到BufferedReader以获取更高的效率。它只会在缓冲区读空的时候才会回头去磁盘读取*/​            String line = null;​            while((line = reader.readLine()) != null) {​                System.out.println(line); /*读一行就列出一行,直到没有东西可以读为止*/​            }​            reader,close();​        } catch(Exception ex) {​            ex.printStackTrace();​        }​    }}

152.P455

e-Flashcard

QuizCardPlayer代码:

import java.util.*;import java.awt.event.*;import javax.swing.*;import java.awt.*;import java.io.*;public class QuizCardPlayer {​    private JTextArea display;​    private JTextArea answer;​    private ArrayList<QuizCard> cardList;​    private QuizCard currentCard;​    private Iterator cardIterator;​    private JFrame frame;​    private JButton nextButton;​    private boolean isShowAnswer;​    // additional, bonus method not found in any book!​    public static void main (String[] args) {​        QuizCardPlayer qReader = new QuizCardPlayer();​        qReader.go();​    }​    public void go() {​        frame = new JFrame("Quiz Card Player");​        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);​        JPanel mainPanel = new JPanel();​        Font bigFont = new Font("sanserif", Font.BOLD, 24);​        display = new JTextArea(9,20);​        display.setFont(bigFont);​        display.setLineWrap(true);​        display.setWrapStyleWord(true);​        display.setEditable(false);​        JScrollPane qScroller = new JScrollPane(display);​        qScroller.setVerticalScrollBarPolicy(​                ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);​        qScroller.setHorizontalScrollBarPolicy(​                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);​        nextButton = new JButton("Show Question");​        mainPanel.add(qScroller);​        mainPanel.add(nextButton);​        nextButton.addActionListener(new NextCardListener());​        JMenuBar menuBar = new JMenuBar();​        JMenu fileMenu = new JMenu("File");​        JMenuItem loadMenuItem = new JMenuItem("Load card set");​        loadMenuItem.addActionListener(new OpenMenuListener());​        fileMenu.add(loadMenuItem);​        menuBar.add(fileMenu);​        frame.setJMenuBar(menuBar);​        frame.getContentPane().add(BorderLayout.CENTER, mainPanel);​        frame.setSize(500,600);​        frame.setVisible(true);​    } // close go​    public class NextCardListener implements ActionListener {​        public void actionPerformed(ActionEvent ev) {​            if (isShowAnswer) {​                // show the answer because they've seen the question​                display.setText(currentCard.getAnswer());​                nextButton.setText("Next Card");​                isShowAnswer = false;​            } else {​                // show the next question​                if (cardIterator.hasNext()) {​                    showNextCard();​                } else {​                    // there are no more cards!​                    display.setText("That was last card");​                    nextButton.disable();​                }​            } // close if​        } // close method​    } // close inner class​    public class OpenMenuListener implements ActionListener {​        public void actionPerformed(ActionEvent ev) {​            JFileChooser fileOpen = new JFileChooser();​            fileOpen.showOpenDialog(frame);​            loadFile(fileOpen.getSelectedFile());​        }​    }​    private void loadFile(File file) {​        cardList = new ArrayList<QuizCard>();​        try {​            BufferedReader reader = new BufferedReader(new FileReader(file));​            String line = null;​            while ((line = reader.readLine()) != null) {​                makeCard(line);​            }​            reader.close();​        } catch(Exception ex) {​            System.out.println("couldn't read the card file");​            ex.printStackTrace();​        }​        // now time to start​        cardIterator = cardList.iterator();​        showNextCard();​    }​    private void makeCard(String lineToParse) {​        StringTokenizer parser = new StringTokenizer(lineToParse, "/");​        if (parser.hasMoreTokens()) {​            QuizCard card = new QuizCard(parser.nextToken(), parser.nextToken());​            cardList.add(card);​        }​    }​    private void showNextCard() {​        currentCard = (QuizCard) cardIterator.next();​        display.setText(currentCard.getQuestion());​        nextButton.setText("Show Answer");​        isShowAnswer = true;​    }} // close class
/*如何玩:点左上角File按钮,点Load card set,查找并选定在运行QuizCardBuilder时自己创建的文件,点打开,单击Show Answer即可*/

153.P458

要如何分开问题和答案?

当你读取文件时,问题和答案是合并在同一行,以“/”字符来分开的。

String的split()可以把字符串拆开。

split()可以将字符串拆开成String的数组。

Sting toTest = "What is blue + yellow?/green";String[] result = toTest.split("/"); /*split()会用参数所指定的字符来把这个String拆开成两个部分(此方法实际上能够做到比这个例子还要复杂的解析)*/for(String token:result) {​    System.out.println(token); //把数组中的每个元素逐一地列出来}

154.P460

Version ID:序列化的识别

现在你已经知道Java的输入/输出确实是相当简单的,特别是很常见的连接/链接组合更是如此。但还有几项议题需要关注。

版本控制很重要!

如果你将对象序列化,则必须要有该类才能还原和使用该对象。OK,这是废话。但若你同时又修改了类会发生什么事?假设你尝试要把Dog对象带回来,而某个非transient的变量却已经从double被改成String。这样会很严重地违反Java的类型安全性。其实不只是修改会伤害兼容性,想想下列的情况:

会损害解序列化的修改:

删除实例变量。

修改实例变量的类型。

将非瞬时的实例变量改为瞬时的。

改变类的继承层次

将类从可序列化改成不可序列化。

将实例变量改成静态的。

通常不会有事的修改:

加入新的实例变量(还原时会使用默认值)。

在继承层次中加入新的类。

从继承层次中删除类。

不会影响解序列化程序设定变量值的存取层次修改。

将实例变量从瞬时改成非瞬时(会使用默认值)。

155.P461

使用serialVersionUID

每当对象被序列化的同时,该对象(以及所有在其版图上的对象)都会被“盖”上一个类的版本识别ID,这个ID被称为serialVersionUID,它是根据类的结构信息计算出来的。在对象被解序列化时,如果在对象被序列化之后类有了不同的serialVersionUID,则还原操作会失败!但你还可以有控制权。

如果你认为类有可能会演化,就把版本识别ID放在类中。

当Java尝试要还原对象时,它会比对对象与Java虚拟机上的类的serialVersionUID。例如,如果Dog实例是以23这个ID来序列化的(实际的ID长得多),当Java虚拟机要还原Dog对象时,它会先比对Dog对象和Dog类的serialVersionUID。如果版本不相符,Java虚拟机就会在还原过程中抛出异常。

因此,解决方案就是把serialVersionUID放在class中,让类在演化的过程中还维持相同的ID。

这只会在在你有很小心地维护类的变动时才办得到!也就是说你得要对带回旧对象的任何问题负起全责。

若想知道某个类的serialVersionUID,则可以使用Java Development Kit里面所带的serialver工具来查询:

1)使用serialver工具来取得版本ID。

serialver Dog

2)把输出拷贝到类上。

public class Dog {​    static final long serialVersionUID = -6849794470754667710L; //把输出拷贝过来​    private String name;​    private int size;}

3)在修改类的时候要能确定修改程序的后果!例如,新的Dog要能够处理旧的Dog解序列化之后新加入变量的默认值。

156.P471

网络与线程

网络联机

连接到外面的世界。你的Java程序可以向外扩展并触及其他计算机上的程序。这很容易,所有网络运作的低层细节都已经由java.net函数库处理掉了。Java的一项好处是传送与接收网络上的数据只不过是链接上使用不同链接串流的输入/输出而已。如果有BufferedReader就可以读取。

若数据来自文件或网络另一端,则BufferedReader不用花费很多精力去照顾。在这一章中,我们会使用socket来连接外面的世界。我们会创建客户端的socket、服务器端的socket,并且让两端相互交谈。

157.P473

聊天程序概述

客户端必须要认识服务器。

服务器必须要认识所有的客户端。

工作方式:

1)客户端连接到服务器。

2)服务器建立连接并把客户端加到来宾清单中。

3)另外一个用户连接上来。

4)用户A送出信息到聊天服务器上。

5)服务器将信息送给所有的来宾。

158.P474

网络socket连接

连接、传送与接收

要让客户端能够工作、有3件事必须先学:

1)如何建立客户端与服务器之间的初始连接

2)如何传送信息到服务器

3)如何接收来自服务器的信息

这里面有非常多的低层工作细节。但很幸运,因为Java API的网络功能包(java.net)能让程序员轻松解决这些问题。程序中的GUI码比输入/输出和网络码多了不少的原因就在此。

译者个人意见:networking的学问绝对不是三言两语说得完,你也许可以很轻松地写出有网络功能的程序,但没有打好底子(像是深入研究TCP/IP等协议的原理)就很容易出问题(例如说效率不好)同时你也不会有除错抓问题的能力。

159.P475

建立Socket连接

要连接到其他的机器上,我们会需要Socket的连接。Socket是个代表两台机器之间网络连接的对象(java.net.Socket)。什么是连接?两台机器之间的一种关系,让两个软件相互认识对方。最重要的是两者知道如何与对方通信,也就是说知道如何发送数据给对方。

还好我们不在乎低层的细节,因为这是在低层的“网络设备”中处理的。如果你不知道什么是网络设备也没关系,它只是一种让运行在Java虚拟机上的程序能够找到方法去通过实际的硬件(例如说网卡)在机器之间传送数据的机制。反正有人会负责这些低层的工作就对了。这些人是由操作系统的特定部分与Java的网络API所组成的。你所必须要处理的是高层的部分——真的很高级,且又简单到吓人。准备好面对这一切了吗?

要创建Socket连接你得知道两项关于服务器的信息:它在哪里以及用哪个端口来收发数据。也就是说IP地址与端口号。

Socket chatSocket = new Socket("196.164.1.103",5000); /*"196.164.1.103"为IP地址,5000为TCP的端口号*/

Socket连接的建立代表两台机器之间存有对方的信息,包括网络地址和TCP的端口号。

160.P476

约定好的端口

TCP端口只是一个16位宽、用来识别服务器上特定程序的数字。

网络服务器(HTTP)的端口号是80,这是规定的标准。如果你有Telnet服务器的话,它的端口号会是23,POP3邮件服务器的是110,SMTP邮局交换服务器是25.把这些数字想成识别的代号。它们代表在服务器上执行软件的逻辑识别。注意,你在机器的背后找不到这些端口插孔。每个服务器上都有65536个端口(0~65535),很明显,哪来那么多实际的端口可以用。它只是个逻辑上用来表示应用程序的数字。

如果没有端口号,服务器就无法分辨客户端是要连到哪个应用程序的服务。每个应用程序可能都有独特的工作交谈方法,如果没有识别就发送的话会制造很大的混乱。就像是对邮件服务器发送HTTP请求。

在编写服务器程序时,你会加入程序代码来指定想要使用哪个端口号(稍后会有实际例子)。对聊天程序来说,我们选择的端口号是5000。没有特定理由,只是我们很喜欢这个数字,并且这个数字也介于1024~65535之间。为什么?因为0~1023都已经被保留给已知的特定服务。

如果你打算要在公司的网络上执行自己写的服务(服务程序),就得要先跟网管讨论有哪些端口已经被占用了。有时候网管也会把特定的端口号用防火墙或者其他特定的安全管控机制封锁起来。不管怎样,端口号总是得要挑选一个可用的。

161.P477

问:不同程序可以共享一个端口吗?

答:不行,如果你想要使用(技术上叫做绑定)某个已经被占用的端口,就会收到BindException。绑定一个端口就代表程序要在特定的端口上面执行。

162.P478

使用BufferedReader从Socket上读取数据

1)建立对服务器的Socket连接

Socket chatSocket = new Socket("127.0.0.1",5000) /*127.0.0.1这个IP地址有特殊意义,就是“本机”,所以可以在自己这台计算机上同时测试客户端和服务器*/

2)建立连接到Socket上低层输入串流的InputStreamReader

InputStreamReader stream = new InputStreamReader(chatSocket.getInputStream()); /*从Socket取得输入串流*/

3)建立BufferedReader来读取

BufferedReader reader = new BufferedReader(stream);String message = reader.readLine();

163.P479

用PrintWriter写数据到Socket上

上一章没有使用到PrintWriter,而是用BufferedWriter。现在有了别的选择,但因为每次都是写入一个String,所以PrintWriter是最标准的做法。

并且PrintWriter中有print()和println()方法就跟System.out里面的两个刚好一样!

1)对服务器建立Socket连接

Socket chatSocket = new Socket("127.0.0.1",5000);

2)建立链接到Socket的PrintWriter

PrintWriter writer = new PrintWriter(chatSocket.getOutputStream());

3)写入数据

writer.println("message to send");writer.print("another message");

164.P480

编写客户端

1)连接

客户端连上服务器并取得输入串流。

2)读取

客户端从服务器读取信息。

DailyAdviceClient客户端程序

这个程序会建立Socket,通过其他串流来制作BufferedReader,并从服务器应用程序(用4242端口服务的任何程序)上读取一行信息。

import java.io.*;import java.net.*;public class DailyAdviceClient {​    public void go() {​        try {​            Socket s = new Socket("127.0.0.1",4242);​            InputStreamReader streamReader = new InputStreamReader(s.getInputStream());​            BufferedReader reader = new BufferedReader(streamReader);​            String advice = reader.readLine();​            System.out.println("Today you should: " + advice);​            reader.close();​        } catch (IOException ex) {​            ex.printStackTrace();​        }​    }​    public static void main(String[] args) {​        DailyAdviceClient client = new DailyAdviceClient();​        client.go();​    }}

165.P483

编写简单的服务器程序

编写服务器应用程序要用到哪些东西呢?一对Socket。没错,两个称为一对。它们是一个会等待用户请求(当用户创建Socket时)的ServerSocket以及与用户通信用的Socket。

工作方式:

1)服务器应用程序对特定端口创建出ServerSocket。

ServerSocket serverSock = new ServerSocket(4242); /*这会让服务器应用程序开始监听来自4242端口的客户端请求。*/

2)客户端对服务器应用程序建立Socket连接。

Socket sock = new Socket("190.165.1.103", 4242); /*客户端得知道IP地址与端口号*/

3)服务器创建出与客户端通信的新Socket。

Socket sock = serverSock.accept(); /*accept()方法会在等待用户的Socket连接时闲置着。当用户连上来时,此方法会(在不同的端口上)返回一个Socket以便与客户端通信。ServerSocket与Socket的端口不同,因此ServerSocket可以空出来等待其他的用户。*/

DailyAdviceServer程序代码

这个程序会创建ServerSocket并等待客户端的请求。当它收到客户端请求时,服务器会建立与客户端的Socket连接。服务器接着会建立PrintWriter来

送出信息给客户端。

import java.io.*;import java.net.*;public class DailyAdviceServer {​    String[] adviceList = {"Take smaller bites", "Go for the tight jeans. No they do Not make you look fat", "One word: inappropriate", "Just for today, be honest. Tell your boss what you *really* think"};​    public void go() {​        try {​            ServerSocket serverSocket = new ServerSocket(4242); /*ServerSocket会监听客户端对这台机器在4242端口上的请求*/​            while (true) { //服务器进入无穷循环等待服务客户端的请求​                Socket sock = serverSocket.accept(); /*这个方法会停下来等待要求到达之后才会继续*/​                PrintWriter writer = new PrintWriter(sock.getOutputStream());​                String advice = getAdvice();​                writer.println(advice);​                writer.close();​                System.out.println(advice);​            }​        } catch (IOException ex) {​            ex.printStackTrace();​        }​    }​    private String getAdvice() {​        int random = (int)(Math.random() * adviceList.length);​        return adviceList[random];​    }​    public static void main(String[] args) {​        DailyAdviceServer server = new DailyAdviceServer();​        server.go();​    }}

166.P485

问:上一页的服务器有个很严格的限制——看起来它每次只能服务一个用户!

答:没错!在没有完成目前用户的响应程序循环之前它无法回到循环的开始处来处理下一个要求(无法进入accept()的等待来建立Socket给新的用户)。

问:换个方式再问一次,如何才能让服务器能够同时处理多个用户?目前使用的方式根本应付不了聊天的需求。

答:这真的很简单,使用不同的线程并让新的客户端取得新的线程就好。我们正要开始讨论这个部分。

167.P488

问:编写可发送和接收的聊天客户端程序时,如何从服务器取得信息?何时会从服务器取得信息?

答:我们选择当信息被送到服务器上的时候就把它读回来。但程序怎么写?这样得有个循环来等待服务器的信息。但要放在哪里?GUI启动之后,除非有事件被触发,否则不会有任何一处是在运转的。我们需要同时执行的能力,检查服务器信息的同时不会打断用户与GUI的交互!因此用户可以输入信息或滚动接收画面,还需要有东西在背景持续地读取服务器的数据。

这意味着我们需要新的线程(thread),一个独立的执行空间(stack)。

168.P489

Java的multithreading

Java在语言中就有内置多线程的功能。建立新的线程来执行是很简单的:

Thread t = new Thread();t.start();

就是这么的简单。建立出新的线程对象就会启动新的线程,它是个独立的调用执行空间。

但是会有一个问题。

该线程并没有执行任何程序,因此它几乎是一出生就变成植物人了。当线程死时,堆栈也就消失,故事就这么结束了。

因此我们还少一项关键因素——线程的任务,也就是独立线程要跑的程序代码。

Java的多个线程课题意味着我们得要讨论线程与它的任务,以及java.lang中的Thread这个类(要记得java.lang是默认就有被import的,它是包括String和System等语言本身的基础)。

169.P490

Java有多个线程但只有一种Thread类

当你看到我们讨论线程时代表的是独立的线程,也就是独立的执行空间。当你看到Thread时,代表的是命名习惯。在Java中以大写字母开始的东西是类,所以Thread是java.lang这个包中的一个类。Thread对象代表线程,当你需要启动新的线程时就建立Thread的实例。

线程是独立的线程,它代表独立的执行空间。每个Java应用程序会启动一个主线程——将main()放在它自己执行空间的最开始处。Java虚拟机会负责主线程的启动(以及比如垃圾收集所需的系统用线程)。程序员得负责启动自己建立的线程。

Thread是个表示线程的类。它有启动线程,连接线程和让线程闲置的方法(还有更多,这里只列出重要的几个)。

170.P491

有一个以上的执行空间代表什么?

当有超过一个以上的执行空间时,看起来会像是有好几件事情同时发生。实际上,只有真正的多处理器系统能够同时执行好几件事,但使用Java的线程可以让它看起来好像同时都在执行中。也就是说,执行动作可以在执行空间非常快速地来回交换,因此你会感觉到每项任务都在执行。要记得,Java也只是个在低层操作系统上执行的进程。一旦轮到Java执行的时候,Java虚拟机实际上会执行什么?哪个字节码会被执行?答案是目前执行空间最上面的会被执行!在100个毫秒内,目前执行程序代码会被切换到不同空间上的不同方法。

线程要记录的一项事物是目前线程执行空间做到哪里。

它看起来会像下面这样:

1)Java虚拟机会调用main()。

2)main()启动新的线程。新的线程启动期间main的线程会暂时停止执行。

3)Java虚拟机会在线程与原来的主线程间切换直到两者都完成为止。

171.P492

如何启动新的线程

1)建立Runnable对象(线程的任务)

Runnable threadJob = new MyRunnable();

2)建立Thread对象(执行工人)并赋值Runnable(任务)

Thread myThread = new Thread(threadJob); /*把Runnable对象传给Thread的构造函数。这会告诉Thread对象要把哪个方法放在执行空间去运行——Runnable的run()方法*/

3)启动Thread

myThread.start(); /*在还没有调用Thread的start()方法之前什么也不会发生。这是你在只有一个Thread实例来建立新的线程时会发生的事情。当新的线程启动之后,它会把Runnable对象的方法摆到新的执行空间中*/

对于Thread而言,它是个工人,而Runnable就是这个工人的工作。

Runnable带有会放在执行空间的第一项方法:run()。

Thread对象需要任务。任务是线程在启动时去执行的工作。该任务是新线程空间上的第一个方法,且它一定要长得像下面这样:

public voie run() { ​    //会被新线程执行的代码}/*Runnable这个接口只有一个方法:public void run()。(要记得它是个接口,因此不管怎么写它都会是public的)*/

线程怎么会知道要先放上哪个方法?因为Runnable定义了一个协约。因为Runnable是个接口,线程的任务可以被定义在任何实现Runnable的类上。线程只在乎传入给Thread的构造函数的参数是否为实现Runnable的类。

当你把Runnable传给Thread的构造函数时,实际上就是在给Thread取得run()的办法。这就等于你给了Thread一项任务。

实现Runnable接口来建立给thread运行的任务:

public class MyRunnable implements Runnable {​    public void run() {​        go();​    }​    public void go() {​        doMore();​    }​    public void doMore() {​        System.out.println("top o' the stack");​    }}
public class ThreadTester {​    public static void main(String[] args) {​        Runnable threadJob = new MyRunnable();​        Thread myThread = new Thread(threadJob); /*将Runnable的实例传给Thread的构造函数*/​        myThread.start(); /*要调用start()才会让线程开始执行。在此之前,它只是个Thread的实例,并不是真正的线程*/​        System.out.println("back in main");​    }}

/*执行结果为:

back in main

top o’ the stack

top o’ the stack

back in main */

172.P495

一旦线程进入可执行状态,它会在可执行与执行中两种状态中来来去去,同时也有另外一种状态:暂时不可执行(又称为被堵塞状态)。

线程有可能会暂时被挡住

调度器(scheduler)会因为某些原因把线程送进去关一阵子。例如线程可能执行到等待Socket输入串流的程序段,但没有数据可供读取。调度器会把线程移出可执行状态,或者线程本身的程序会要求小睡一下(sleep())。也有可能是因为线程调用某个被锁住(locked)的对象上的方法。此时线程就得等到锁住该对象的线程放开这个对象才能继续下去。

这类型的条件都会导致线程暂时失能。

173.P497

线程调度器

线程调度器会决定哪个线程从等待状况中被挑出来运行,以及何时把哪个线程送回等待被执行的状态。它会决定某个线程要运行多久,当线程被剔出时,调度器也会指定线程要回去等待下一个机会或者是暂时的堵塞。

你无法控制调度,没有API可以调用调度器。最重要的是,调度无法确定(实际上可以做某种程度的保证,但是那也很模糊)。

至少不能让你的程序依靠调度的特定行为来保持执行的正确性!调度器在不同的Java虚拟机上面有不同的做法,就算同一个程序在同一台机器上运行也会有不同的遭遇。Java程序设计新手会犯的最糟错误就是只在单一的机器上测试多线程程序,并假设其他机器的调度器都有相同的行为。

不管线程调度器有怎样的行为,多线程程序一定可以运行。可是你不能假设每个线程都会被调度分配到公正平均的时间和顺序。且现今的Java虚拟机不太可能让你的线程一路执行到底。

原因在于sleep。没错,就是睡觉。让线程去睡个几毫秒才能让所有的线程都有机会被执行。线程的sleep()这个方法能够保证一件事:在指定的沉睡时间之前,昏睡中的线程一定不会被唤醒。举例来说,如果你要求线程去睡2000个毫秒,至少要等两秒过后它才会继续地执行。

174.P500

问:Thread对象可以重复使用吗?能否调用start()指定新的任务给它?

答:不行。一旦线程的run()方法完成之后,该线程就不能再重新启动。事实上过了该点线程就会死翘翘。Thread对象可能还呆在堆上,如同活着的对象一般还能接受某些方法的调用,但已经永远地失去了线程的执行性,只剩下对象本身。

175.P501

让线程小睡一下

确保线程能够有机会执行的最好方式是让它们周期性地去睡一下。你只要调用sleep()这个方法。传入以毫秒指定的实际就行。

Thread.sleep(2000);

这会把线程敲昏,而保持两秒之内不会醒来进入可执行状态。

但是这个方法有可能会抛出InterruptedException异常,所有对它的调用都必须包在try/catch块中。因此真正的程序代码会像是这样:

try {​    Thread.sleep(2000);} catch(InterruptedException ex) {​    ex.printStackTrace();}

你写的线程或许永远也不会被中断,这个异常是API用来支持线程间通信的机制,实际上几乎没有人这样做。但良好的习惯规则会要求我们把有可能抛出异常的调用做妥善的处理。

现在你能够确定在指定时间内不会醒来,但是线程也不一定会在时间过后马上醒来直接变成执行中的状态,你只能确定它会回到可执行的状态。何时执行还是要看调度器大哥的意思。这样的运行对时间控制来说不是非常的准确,但在一般机器上没有太多线程在运行的时候还过得去。千万不要依靠这种机制来精确地控制执行时机。

176.P503

建立与启动两个线程

线程可以有名字。你可以找老师帮线程取个好听又能够走运的好名字,或是使用默认的名称。但最酷的事情还是你可以用名字来判别正在运行的是哪个线程。下面的例子有两个线程。它们都执行相同的工作:在循环中列出线程的名称。

public class RunThreads implements Runnable {​    public static void main(String[] args) {​        RunThreads runner = new RunThreads(); //创建Runnable的实例​        Thread alpha = new Thread(runner); //创建两个线程,使用相同的Runnable​        Thread beta = new Thread(runner);​        alpha.setName("Alpha thread"); //帮线程取名字​        beta.setName("Beta thread");​        alpha.start();​        beta.start();​    }​    public void run() {​        for (int i = 0; i < 25; i++) {​            String threadName = Thread.currentThread().getName(); /*取得正在运行线程的名字*/​            System.out.println(threadName + " is running");​        }​    }}

177.P504

线程会产生并发性的问题

并发性(concurrency)问题会引发竞争状态(race condition)。竞争状态会引发数据的损毁。这一切都来自可能发生的一种状况:两个或以上的线程存取单一对象的数据。也就是说两个不同执行空间上的方法都在堆上对同一个对象执行getter或setter。

两个线程各自认为自己是宇宙的中心,只关心自己的任务。因为线程会被打入可执行状态,此时基本上是昏迷过去的,当它回到执行中的状态时,根本也不知道自己曾经不省人事。

他们需要对账户存取的一道锁:

1)没有交易时,锁是开着的。

2)杰伦想要交易,所以把账户给锁上并带走钥匙。

3)交易完成后就解锁并归还钥匙。现在就可以换其他人存取账户。

178.P510

使用synchronized这个关键词来修饰方法使它每次只能被单一的线程存取。

synchronized关键词代表线程需要一把钥匙来存取被同步化(synchronized)过的线程。

要保护数据,就把作用在数据上的方法给同步化。

例:

public class BankAccount {​    private int balance = 100; //账户一开始有100元​    public int getBalance() {​        return balance;​    }​    public void withdraw(int amount1) {​        balance = balance - amount1;​    }}
public class RyanAndMonicaJob implements Runnable {​    private BankAccount account = new BankAccount();​    public static void main(String[] args) {​        RyanAndMonicaJob theJob = new RyanAndMonicaJob();​        Thread one = new Thread(theJob);​        Thread two = new Thread(theJob);​        one.setName("Ryan");​        two.setName("Monica");​        one.start();​        two.start();​    }​    public void run() {​        for (int x = 0; x < 10; x++) {​            /*检查账户余额,如果透支就列出信息,不然就去睡一会,然后醒来完成提款任务*/​            makeWithdrawal(10);​            if (account.getBalance() < 0) {​                System.out.println("Overdrawn!");​            }​        }​    }​    private synchronized void makeWithdrawal(int amount) { /*使用synchronized这个关键词来修饰方法*/​        if (account.getBalance() >= amount) {​            System.out.println(Thread.currentThread().getName() + " is about to withdraw");​            try {​                System.out.println(Thread.currentThread().getName() + " is going to sleep");​                Thread.sleep(500);​            } catch (InterruptedException ex) {​                ex.printStackTrace();​            }​            System.out.println(Thread.currentThread().getName() + " woke up.");​            account.withdraw(amount);​            System.out.println(Thread.currentThread().getName() + " completes the withdrawl");​        } else {​            System.out.println("Sorry, not enough for " + Thread.currentThread().getName());​        }​    }}

179.P511

使用对象的锁

每个对象都有个锁。大部分时间都没有锁上,并且你可以假设有个虚拟的钥匙随侍在旁。对象的锁只会在有同步化的方法上起作用。当对象有一个或多个同步化的方法时,线程只有在取得对象锁的钥匙时才能进入同步化的方法。

锁不是配在方法上的,而是配在对象上。如果对象有两个同步化的方法,就表示两个线程无法进入同一个方法,也表示两个线程无法进入不同的方法。

想想看,如果你有多个方法可能会操作对象的实例变量,则这些方法都应该要有同步化的保护。

同步化的目标是保护重要的数据。但要记住,你锁住的不是数据而是存取数据的方法。

所以线程在开始执行并遇上有同步化的方法时候回发生什么事?线程会认识到它需要对象的钥匙才能进入该方法。它会取得钥匙(这是由Java虚拟机来处理,没有存取对象锁的API可用),如果可以拿到钥匙才会进入方法。

从这一点开始,线程会全力照顾好这个钥匙,除非完成同步化的方法,否则它不会放开钥匙。因此当线程持有钥匙时,没有其他的线程可以进入该对象的同步化方法,因为每个对象只有一个钥匙。

180.P516

同步化的死亡阴影

使用同步化的程序代码要小心,因为没有其他的东西能够像线程的死锁(deadlock)这样伤害你的程序。死锁会发生是因为两个线程互相持有对方正在等待的东西。没有方法可以脱离这个情况,所以两个线程只好停下来等,一直等,一直等,海枯石烂还在继续等。

如果你对数据库或其他的应用程序服务器很熟,那你就应该知道这个问题:数据库有与同步化非常相似的上锁机制。但像样的数据库交易管理系统有时能处理掉死锁。例如它可能把等待太久的交易视为死锁。但与Java不同的地方在于它们有事务回滚机制来复原不能全部完成的交易。

Java没有处理死锁的机制。它甚至不知道死锁的发生。所以你得小心设计程序。如果你经常编写多线程的程序,建议阅读O’Reilly出版的“Java Thread”,上面有一些观念的澄清和设计的提示可以帮忙避免死锁(译注:有中文译本,译文精确优雅,译者帅气逼人,是本不可多得的好书)。

181.P516

只要两个线程和两个对象就可以引起死锁

1)线程A进入对foo对象设定同步化的方法。

线程A睡着。

2)线程B进入对bar对象设定同步化的方法。

线程B接着尝试要进入A正在执行的方法,所以B只好等一等。

3)线程A醒来,尝试要进入B正在执行的方法,但拿不到钥匙,所以也只好等着。

A一直在等B的bar钥匙,而B却也在等着A的foo钥匙,两个线程就这么僵持着……

182.P518

全新配方的SimpleChatClient

回到本章开始的主题,我们创建出SimpleChatClient来发送信息给服务器,但无法接收。这就是为什么要讨论线程的原因,因为我们需要能够同时做两件事的方法:送出信息给服务器的同时读取来自服务器的信息,并显示在可滚动的区域。

//聊天客户端程序

import javax.swing.*;import java.awt.*;import java.awt.event.*;import java.io.*;import java.net.*;public class SimpleChatClient {​    JTextArea incoming;​    JTextField outgoing;​    BufferedReader reader;​    PrintWriter writer;​    Socket sock;​    public static void main(String[] args) {​        SimpleChatClient client = new SimpleChatClient();​        client.go();​    }​    public void go() {​        JFrame frame = new JFrame("Ludicrously Simple Chat Client");​        JPanel mainPanel = new JPanel();​        incoming = new JTextArea(15, 50);​        incoming.setLineWrap(true);​        incoming.setWrapStyleWord(true);​        incoming.setEditable(false);​        JScrollPane qScroller = new JScrollPane(incoming);​        qScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);​        qScroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);​        outgoing = new JTextField(20);​        JButton sendButton = new JButton("Send");​        sendButton.addActionListener(new SendButtonListener());​        mainPanel.add(qScroller);​        mainPanel.add(outgoing);​        mainPanel.add(sendButton);​        setUpNetworking();​        Thread readerThread = new Thread(new IncomingReader());​        readerThread.start();​        /*启动新的线程,以内部类作为任务,此任务是读取服务器的socket串流显示在文本区域*/​        frame.getContentPane().add(BorderLayout.CENTER,mainPanel);​        frame.setSize(650, 500);​        /*frame必须设的大一点,如果按照书上设成(400,500),你会发现接收消息的窗口不显示*/​        frame.setVisible(true);​    }​    private void setUpNetworking() {​        try {​            sock = new Socket("127.0.0.1", 5000);​            InputStreamReader streamReader = new InputStreamReader(sock.getInputStream());​            reader = new BufferedReader(streamReader);​            writer = new PrintWriter(sock.getOutputStream());​            System.out.println("networking established");​        } catch (IOException ex) {​            ex.printStackTrace();​        }​    }​    public class SendButtonListener implements ActionListener {​        public void actionPerformed(ActionEvent ev) {​            try {​                writer.println(outgoing.getText());​                //用户按下send按钮时送出文本字段的内容到服务器上​                writer.flush();​            } catch (Exception ex) {​                ex.printStackTrace();​            }​            outgoing.setText(""); //将发送框清空​            outgoing.requestFocus(); //把GUI目前焦点拉回到文本字段​        }​    }​    public class IncomingReader implements Runnable {​        public void run() {​            String message;​            try {​                while ((message = reader.readLine()) != null) {​                    System.out.println("read " + message);​                    incoming.append(message + "\n");​                }​            } catch (Exception ex) {​                ex.printStackTrace();​            }​        }​    }}

//聊天服务器程序

import java.io.*;import java.net.*;import java.util.*;public class VerySimpleChatServer {​    ArrayList clientOutputStreams;​    public class ClientHandler implements Runnable {​        BufferedReader reader;​        Socket sock;​        public ClientHandler(Socket clientSocket) {​            try {​                sock = clientSocket;​                InputStreamReader isReader = new InputStreamReader(sock.getInputStream());​                reader = new BufferedReader(isReader);​            } catch (Exception ex) {​                ex.printStackTrace();​            }​        }​        public void run() {​            String message;​            try {​                while ((message = reader.readLine()) != null) {​                    System.out.println("read " + message);​                    tellEveryone(message);​                }​            } catch (Exception ex) {​                ex.printStackTrace();​            }​        } //关闭run​    } //关闭内部类ClientHandler​    public static void main(String[] args) {​        new VerySimpleChatServer().go();​    }​    public void go() {​        clientOutputStreams = new ArrayList();​        try {​            ServerSocket serverSock = new ServerSocket(5000);​            while (true) {​                Socket clientSocket = serverSock.accept();​                PrintWriter writer = new PrintWriter(clientSocket.getOutputStream());​                clientOutputStreams.add(writer);​                Thread t = new Thread(new ClientHandler(clientSocket));​                t.start();​                System.out.println("got a connection");​            }​        } catch (Exception ex) {​            ex.printStackTrace();​        }​    }​    public void tellEveryone(String message) {​        Iterator it = clientOutputStreams.iterator();​        while (it.hasNext()) {​            try {​                PrintWriter writer = (PrintWriter) it.next();​                writer.println(message);​                writer.flush();​            } catch (Exception ex) {​                ex.printStackTrace();​            }​        } //关闭while​    } //关闭tellEveryone}

183.P522

问:那么静态变量状态的保护呢?如果有静态的方法可以对静态变量的状态作更新,还能够用同步化吗?

答:可以!记得静态的方法是运行在类而不是每个实例上的吗?所以你可能会猜想要用哪个对象的锁。毕竟有可能完全没有该类的实例存在。幸好对象有锁,每个被载入的类也有个锁。这表示说如果有3个Dog对象在堆上,则总共有4个与Dog有关的锁。3个是Dog实例的,1个是类的。当你要对静态的方法做同步化时,Java会使用类本身的锁。因此如果同一个类有两个被同步化过的静态方法,线程需要取得类的锁才能进入这些方法。

问:什么事线程优先级?我听说它可以用来控制调度。

答:线程的优先级可以对调度器产生影响,但也是没有绝对的保证,优先权的级别会告诉调度器某个线程的重要性,低优先级的线程也许会把机会让给高优先级的线程,也许……建议你可以用优先级来影响执行性能,但绝不能依靠优先级来维持程序的正确性。

184.P526

两个不同的类会对另一类的同一个对象作更新,因为两个线程会去存取Accum唯一的实例。

private static Accum a = new Accum();

上面这行程序会创建Accum的静态实例,而私用的构造函数代表其他人无法创建它的对象。运用这两项技术能够做出称为Singleton的模式,它能限制应用程序上某对象实例的数量(通常会跟名字一样限制一个)。但你也可以使用相同的模式来做出想要的限制。

例:

public class TestThreads {​    public static void main(String[] args) {​        ThreadOne t1 = new ThreadOne();​        ThreadTwo t2 = new ThreadTwo();​        Thread one = new Thread(t1);​        Thread two = new Thread(t2);​        one.start();​        two.start();​    }}
public class Accum {​    private static Accum a = new Accum(); //创建Accum这个类的静态实例​    private int counter  = 0;​    private Accum() { } //私用的构造函数​    public static Accum getAccum() {​        return a;​    }​    public void updateCounter(int add) {​        counter += add;​    }​    public int getCount() {​        return counter;​    }}
public class ThreadOne implements Runnable {​    Accum a = Accum.getAccum();​    public void run() {​        for (int x = 0; x < 98; x++) {​            a.updateCounter(1000);​            try {​                Thread.sleep(50);​            } catch (InterruptedException ex) {​            }​            System.out.println("one " + a.getCount());​        }​    }}
public class ThreadTwo implements Runnable {​    Accum a = Accum.getAccum();​    public void run() {​        for (int x = 0; x < 99; x++) {​            a.updateCounter(1);​            try {​                Thread.sleep(50);​            } catch (InterruptedException ex) {​            }​            System.out.println("two " + a.getCount());​        }​    }}

185.P529

集合与泛型

数据结构

Java集合框架(Collections Framework)能够支持绝大多数你会用到的数据结构。想要很容易加入元素的列表吗?想要根据名称来搜索吗?打算创建可以自动排除重复项目的列表吗?需要将同事暗算你的次数排个复仇黑名单吗?这里全都有……

下面是一个读取歌曲内容的程序:

import java.io.*;import java.util.*;public class Jukebox1 {​    ArrayList<String> songList = new ArrayList<String>();​    public static void main(String[] args) {​        new Jukebox1().go();​    }​    public void go() {​        getSongs();​        System.out.println(songList); //输出songList​        ​        /*调用Collections静态的sort()然后再列出清单。第二次的输出会按照字母排序*/​        Collections.sort(songList);​        System.out.println(songList);​    }​    void getSongs() {​        try {​            File file = new File("SongList.txt");​            BufferedReader reader = new BufferedReader(new FileReader(file));​            String line = null;​            while ((line = reader.readLine()) != null) {​                addSong(line);​            }​        } catch (Exception ex) {​            ex.printStackTrace();​        }​    }​    void addSong(String lineToParse) {​        String[] tokens = lineToParse.split("/");​        songList.add(tokens[0]);​    }}

/*歌曲文本文件SongList.txt内容为:

Communication/The Cardigans

Black Dog/Led Zeppelin

Dreams/Van Halen

Comfortably Numb/Pink Floyd

Beth/Kiss

倒退噜/黄克林

*/

186.P532

ArrayList没有sort()这个方法,很明显ArrayList不能排序。

ArrayList不是唯一的集合

虽然ArrayList会是最常用的,但偶尔还是会有特殊情况。下面列出几个较为重要的。

TreeSet

以有序状态保持并可放在重复。

HashMap

可用成对的name/value来保存与输出。

LinkedList

针对经常插入或删除中间元素所设计的高效率集合。

(实际上ArrayList还是比较实用)

HashSet

防止重复的集合,可快速地寻找相符的元素。

LinkedHashMap

类似HashMap,但可记住元素插入的顺序,也可以设定成依照元素上次存取的先后来排序。

187.P534

你可以使用TreeSet或Collections.sort()方法

如果你把字符串放进TreeSet而不是ArrayList,这些String会自动地按照字母顺序排在正确的位置。每当你想要列出清单时,元素总是会以字母顺序出现。

当你需要Set集合或总是会依照字母排列的清单时。它会很好用。

另外一方面,如果你没有需要让清单保持有序的状态,TreeSet的成本会比你想付出的还多——每当插入新项目时,它都必须要花时间找出适当的位置。而ArrayList只要把项目放在最后面就好。

问:但你可以用指定的索引来添加新项目到ArrayList中,而不是放到最后面——add()有个重载的版本可以指定int值,这样会比较慢吗?

答:是的,插到指定位置会比直接加到最后面要慢。所以add(index, element)不会像add(element)这么快。但通常你不会对ArrayList加上指定的索引。

问:使用LinkedList这个类会不会比较好?我记得以前上数据结构的课时是这样说的。

答:没错,LinkedList对于在中间的插入或删除会比较快,但对大多数的应用程序而言ArrayList与LinkedList的差异有限,除非元素量真的很大。稍后会讨论LinkedList。

188.P536

但是现在要用Song对象而不只是String

老板说list里面要摆的是Song这个类的实例,这样新的点歌系统才会有更细节的资料可以输出。所以文件内也会从两种数据增加到4种。

Song这个类是很单纯的,但有一项很有意思的功能:被覆盖过的toString()。要知道toString()是定义在Object这个类中,所以Java中的每个类都有继承到,且因为对象被System.out.println(anObject)列出来时会被调用toString(),所以当你要把list列出时,每个对象的toString()都会被调用一次。

public class Song {​    String title;​    String artist;​    String rating;​    String bpm;​    Song(String t, String a, String r, String b) {​        title = t;​        artist = a;​        rating = r;​        bpm = b;​    }​    public String getTitle() {​        return title;​    }​    public String getArtist() {​        return artist;​    }​    public String getRating() {​        return rating;​    }​    public String getBpm() {​        return bpm;​    }​    public String toString() {​        return title;​    }}
import java.io.*;import java.util.*;public class Jukebox3 {​    ArrayList<Song> songList = new ArrayList<Song>();​    public static void main(String[] args) {​        new Jukebox3().go();​    }​    public void go() {​        getSongs();​        System.out.println(songList);​        Collections.sort(songList);​        System.out.println(songList);​    }​    void getSongs() {​        try {​            File file = new File("SongListMore.txt");​            BufferedReader reader = new BufferedReader(new FileReader(file));​            String line = null;​            while ((line = reader.readLine()) != null) {​                addSong(line);​            }​        } catch (Exception ex) {​            ex.printStackTrace();​        }​    }​    void addSong(String lineToParse) {​        String[] tokens = lineToParse.split("/");​        Song nextSong = new Song(tokens[0], tokens[1], tokens[2], tokens[3]);​        songList.add(nextSong);​    }}

/*歌曲文本文件SongListMore.txt内容为:

Communication/The Cardigans/5/80

Black Dog/Led Zeppelin/4/84

Dreams/Van Halen/6/120

Comfortably Numb/Pink Floyd/5/110

Beth/Kiss/4/100

倒退噜/黄克林/5/90

*/

运行后无法通过编译!

有点问题,Collections类很明显地说明sort()这个方法会取用List。

ArrayList是一个List,因为ArrayList有实现List这个接口,所以应该要没问题才对。

但就是不行!

编译器表示它找不到取用ArrayList参数的sort()方法,所以ArrayList与ArrayList之间到底有什么差异?为什么编译器不会让它过关?

或许你已经在想:“那它要靠什么东西来排序?”sort()要怎么判断某首歌应该在另外一首歌之前?很明显的,如果你想要让歌曲依照曲名字母排列,就得要有一种方法可以告诉sort()它依靠的不是曲名长度来排列。

189.P539

sort()的声明

public static <T extends Comparable<? super T>> void sort(List list)

从API说明文档找java.util.Collections下面的sort(),你会发现它的声明有点怪怪的。至少跟我们之前所看过的有些不一样。

这是因为sort()很大量地运用到泛型(generic)功能。只要你在Java的程序或文件中看到<>这一组符号,就代表泛型正在作用——它是一种从Java 5.0开始加入的特质。看起来我们得先学会如何解读说明文件才能看得出来为何ArrayList可应付String对象,但不吃Song对象这一套。

190.P540

泛型意味着更好的类型安全性

我们就这么说吧,几乎所有你会以泛型写的程序都与处理集合有关。虽然泛型可以用在其他地方,但它主要目的还是让你能够写出有类型安全性的集合。也就是说,让编译器能够帮忙防止你把Dog加到一群Cat中。

在泛型功能出现前,编译器无法注意到你加入集合中的东西是什么,因为所以的集合都写成处理Object类型。你可以把任何东西放进ArrayList中,有点像是ArrayList。

191.P541

关于泛型

泛型有好几样东西需要知道,但对大部分程序员来说,其实只有3件事情是重要的:

1)创建出被泛型化类(例如ArrayList)的实例。

new ArrayList<Song>()

2)声明与指定泛型类型的变量

List<Song> songList = new ArrayList<Song>()

3)声名(与调用)取用泛型类型的方法

void foo(List<Song> list)x.foo(songList)

问:但我不是还需要学习如何自己创建泛型的类吗?如果我想设计出让人们在初始化同时要决定类型的类要怎么办?

答:你或许不会经常做这件事。想想看,API的设计团队已经涵盖了大部分你会遇到的数据结构,而几乎只有集合才会真的需要泛型。也就是说这些类是设计来保存其他元素,并要让程序员在声明与初始化类的时候指定元素的类型。

没错,你可能会想要创建出泛型的类,但那是很少见的情况,所以我们就不多做讨论了(还是可以从这些内容看出大概)。

192.P542

使用泛型的类

因为ArrayList是最常用的泛型化类型,我们会从查看它的文件看起。有两个关键的部分:

1)类的声明。

2)新增元素的方法的声明。

ArrayList的说明文件

public class ArrayList extends AbstractList implements List

E代表用来创建与初始ArrayList的类型。当你看到ArrayList文件上的E时,就可以把它换成实际上的类型。

所以ArrayList就会在所有方法与变量的声明中把E换成Song。

问:只能用E吗?因为排序的文件上面用的是T……

答:你可以使用任何合法的Java标识字符串。这代表不管用什么都会被当作是类型参数。但习惯用法是以单一的字母表示(你也应该这么做),除非与集合有关,否则都是用T,因为E很清楚地指明是元素(Element)。

193.P544

运用泛型的方法

泛型的类代表类的声明用到类型参数。泛型的方法代表方法的声明特征用到类型参数。

在方法中的类型参数有几种不同的运用方式。

1)使用定义在类声明的类型参数。

public class ArrayList extends AbstractList …{

​ public boolean add(E o)

当你声明类的类型参数时,你就可以直接把该类或接口类型用在任何地方。参数的类型声明基本上会以用来初始化类的类型来取代。

2)使用未定义在类声明的类型参数

public<T extends Animal> void takeThing(ArrayList<T> list)/*因为在前面声明T(<T extends Animal>)所以这里(ArrayList<T>)就可以使用<T>*/

如果类本身没有使用类型参数,你还是可以通过在一个不寻常但可行的位置上指定给方法——在返回类型之前。这个方法意味着T可以是“任何一种Animal”。

194.P545

这行程序:

public<T extends Animal> void takeThing(ArrayList<T> list)

跟这个是不一样的:

public void takeThing(ArrayList<Animal> list)

两者都合法,但意义不同!

首先,是方法声明的一部分,表示任何被声明为Animal或Animal的子型(像是Cat或Dog)的ArrayList是合法的。因此你可以使用ArrayList、ArrayList或ArrayList来调用上面的方法。

但是,下面方法的参数是ArrayList list,代表只有ArrayList是合法的。也就是说第一个可以使用任何一种Animal的ArrayList,而第二个方法只能使用Animal的ArrayList。

没错,这看起来已经违反动态绑定的精神,但在这一章最后的回顾时就会很清楚这是怎么一回事。现在只要记得我们还在想办法对SongList排序

就行。

现在只要知道上面的语法是合法的就够了,它代表你可以传入以Animal或子型来初始化的ArrayList对象。

195.P546

这还是没有解释为什么sort方法Song的ArrayList上不行,但String可以。

再看一下sort()方法

查找文件上关于为何对String的list排序可行,但Song对象就不行的线索。看起来答案应该是:

看起来sort()方法只能接受Comparable对象的list。

Song不是Comparable的子型,所以你无法对Song的list排序。

知道哪里有问题了……

Song类必须实现Comparable

我们只有在Song类实现Comparable的情况下才能把ArrayList传给sort()方法,因为这个方法就是如此声明的。稍微看过一下说明文件就会知道Comparable其实很单纯,只有一个方法需要实现。

public interface Comparable {

​ int compareTo(T o);

}

而compareTo(T o)的文件中返回值说明是这样的:

Returns:

a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.

看起来compareTo()方法是会从某个Song的对象来调用,然后传入其他Song对象的引用。执行compareTo()方法的Song必须要判别在排序位置上它自己是高于、低于或相等于所传入的Song。

你的主要任务就是决定如何判断Song的先后,然后以compareTo()方法的实现来反映出这个逻辑。返回负数值代表传入的Song大于执行的Song、正数刚好相反,而返回0代表相等(以排序的目的来说,并不代表两个对象真的相等)。

196.P550

更新、更好、更comparable的Song类

我们决定要靠歌名来排序,所以把compareTo()方法是实现成用执行方法的Song歌曲名称和所传入的Song歌曲名称来比较。也就是说,执行方法的Song会判断它的歌名和参数歌名的比较结果。

我们知道String一定有办法比较字母先后顺序,因为sort()方法就可以比较String的list。我们也知道String有个compareTo()方法,因此只要让曲名String相互比较就好,不必另行编写比较字母的算法!

public class Song implements Comparable<Song> {​    String title;​    String artist;​    String rating;​    String bpm;​    public int compareTo(Song s) { //Song为要比较的对象​        return title.compareTo(s.title); /*就是这么简单!返回String比较的结果就行*/​    }​    Song(String t, String a, String r, String b) {​        title = t;​        artist = a;​        rating = r;​        bpm = b;​    }​    public String getTitle() {​        return title;​    }​    public String getArtist() {​        return artist;​    }​    public String getRating() {​        return rating;​    }​    public String getBpm() {​        return bpm;​    }​    public String toString() {​        return title;​    }}
import java.io.*;import java.util.*;public class Jukebox3 {​    ArrayList<Song> songList = new ArrayList<Song>();​    public static void main(String[] args) {​        new Jukebox3().go();​    }​    public void go() {​        getSongs();​        System.out.println(songList);​        Collections.sort(songList);​        System.out.println(songList);​    }​    void getSongs() {​        try {​            File file = new File("SongListMore.txt");​            BufferedReader reader = new BufferedReader(new FileReader(file));​            String line = null;​            while ((line = reader.readLine()) != null) {​                addSong(line);​            }​        } catch (Exception ex) {​            ex.printStackTrace();​        }​    }​    void addSong(String lineToParse) {​        String[] tokens = lineToParse.split("/");​        Song nextSong = new Song(tokens[0], tokens[1], tokens[2], tokens[3]);​        songList.add(nextSong);​    }}

197.P551

list排好了,但是……

又有问题了,大厨说他只会唱陈雷的歌,所以除了依照歌名排序之外,也要能依照歌星名来排。

继续查询API,Collections有另一种sort()方法——它取用Comparator参数

sort(List list, Comparator<? super T> c)

Sorts the specified list according to the order induced by the specified comparator.

使用自制的Comparator

使用compareTo()方法时,list中的元素只能有一种将自己与同类型的另一个元素做比较的方法。但Comparator是独立于所比较元素类型之外的——它是独立的类。因此你可以有各种不同的比较方法!想要依照歌星排序吗?做个ArtistComparator,想要依照恶搞程度排序吗?没问题,做个KusoComparator。

然后只要调用重载版,取用List与Comparator参数的sort()方法来处理就可以。

取用Comparator版的sort()方法会用Comparator而不是元素内置的compareTo()方法来比较顺序。也就是说,如果sort()方法带有Comparator,它就不会调用元素的compareTo()方法,而会去调用Comparator的compare()方法。

问:是否这代表如果类没有实现Comparable,且你也没拿到原始码,还是能够通过Comparator来排序?

答:没错,另外一种方法就是子类化该元素并实现出Comparable。

问:为什么不是每个类都有实现Comparable?

答:你真的认为每种东西都可以排序吗?如果不能排序的东西硬加上Comparable只会使人产生误解。

198.P553

用Comparator更新点歌系统

我们在新版本做了3件事:

1)创建并实现Comparator的内部类,以compare()方法取代compareTo()方法。

2)制作该类的实例。

3)调用重载版的sort(),传入歌曲的list以及Comparator的实例。

附注:我们也有更新Song的toString()以列出歌名与歌星。

public class Song implements Comparable<Song> {​    String title;​    String artist;​    String rating;​    String bpm;​    public int compareTo(Song s) {​        return title.compareTo(s.title);​    }​    Song(String t, String a, String r, String b) {​        title = t;​        artist = a;​        rating = r;​        bpm = b;​    }​    public String getTitle() {​        return title;​    }​    public String getArtist() {​        return artist;​    }​    public String getRating() {​        return rating;​    }​    public String getBpm() {​        return bpm;​    }​    public String toString() {​        return title;​    }}
import java.io.*;import java.util.*;public class Jukebox5 {​    ArrayList<Song> songList = new ArrayList<Song>();​    public static void main(String[] args) {​        new Jukebox5().go();​    }​    class ArtistCompare implements Comparator<Song> {​        public int compare(Song one, Song two) {​            return one.getArtist().compareTo(two.getArtist());​            /*one.getArtist()和two.getArtist()会返回String,compareTo()将两个String进行比较*/​        }​    }​    public void go() {​        getSongs();​        System.out.println(songList);​        Collections.sort(songList);​        System.out.println(songList);​        ArtistCompare artistCompare = new ArtistCompare(); /*创建Comparator的实例*/​        Collections.sort(songList, artistCompare); /*调用sort(),传入list与Comparator对象*/​        System.out.println(songList);​    }​    void getSongs() {​        try {​            File file = new File("SongListMore.txt");​            BufferedReader reader = new BufferedReader(new FileReader(file));​            String line = null;​            while ((line = reader.readLine()) != null) {​                addSong(line);​            }​        } catch (Exception ex) {​            ex.printStackTrace();​        }​    }​    void addSong(String lineToParse) {​        String[] tokens = lineToParse.split("/");​        Song nextSong = new Song(tokens[0], tokens[1], tokens[2], tokens[3]);​        songList.add(nextSong);​    }}/*注意:另外一种设计哲学是拿掉Song类的compareTo(),以两个Comparator来实现歌名排序和歌星名排序。这代表我们只能使用两个参数版的Collections.sort()。*/

/*为了更明显的看出结果,我们更改了SongListMore.txt的内容。

歌曲文本文件SongListMore.txt内容为:

Communication/The Cardigans/5/80

Black Dog/Kiss/4/84

Dreams/Van Halen/6/120

Comfortably Numb/Pink Floyd/5/110

Beth/Led Zeppelin/4/100

倒退噜/黄克林/5/90

*/

199.P553

Java中,如果想要排序,实现Comparator接口与Comparable的区别?

两种方法各有优劣,用Comparable简单, 只要实现Comparable接口的对象直接就成为一个可以比较的对象,但是需要修改源代码,用Comparator的好处是不需要修改源代码,而是另外实现一个比较器,当某个自定义的对象需要作比较的时候,把比较器和对象一起传递过去就可以比大小了,并且在Comparator里面用户可以自己实现复杂的可以通用的逻辑,使其可以匹配一些比较简单的对象,那样就可以节省很多重复劳动了。

200.P554

Comparator范例:

import java.util.*;public class SortMountains {​    LinkedList<Mountain> mnt = new LinkedList<Mountain>();​    class NameCompare implements Comparator<Mountain> {​        public int compare(Mountain one, Mountain two) {​            return one.name.compareTo(two.name);​        }​    }​    class HeightCompare implements Comparator<Mountain> {​        public int compare(Mountain one, Mountain two) {​            return (two.height - one.height); //你注意到这是降幂排序吗?​        }​    }​    public static void main(String[] args) {​        new SortMountains().go();​    }​    public void go() {​        mnt.add(new Mountain("Longs", 14255));​        mnt.add(new Mountain("Elbert", 14433));​        mnt.add(new Mountain("Maroon", 14156));​        mnt.add(new Mountain("Castle", 14265));​        System.out.println("as entered: \n" + mnt);​        NameCompare nc = new NameCompare();​        Collections.sort(mnt, nc);​        System.out.println("by name: \n" + mnt);​        HeightCompare hc = new HeightCompare();​        Collections.sort(mnt, hc);​        System.out.println("by height: \n" + mnt);​    }}

//Mountain类

public class Mountain {​    String name;​    int height;​    Mountain(String n, int h) {​        name = n;​        height = h;​    }​    public String toString() { //将名字和高度合并成以文本方式表示​        return name + " " + height;​    }}

201.P557

从Collection的API文档中我们发现了:

Collection(接口) is a member of the Java Collections Framework.

See Also:Set, List, Map, SortedSet, SortedMap, HashSet, TreeSet, ArrayList, LinkedList, Vector, Collections, Arrays, AbstractCollection

也就是说Set, List, Map都是Java Collections Framework的成员。

1)List:对付顺序的好帮手。

是一种知道索引位置的集合。

List知道某物在系列集合中的位置。可以有多个元素引用相同的对象。

ArrayList是个List。

2)Set:注重独一无二的性质。

不允许重复的集合。

它知道某物是否已经存在于集合中。不会有多个元素引用相同的对象(被认为相等的两个对象也不行,稍后有更多的说明)。

3)Map:用key来搜索的专家。

使用成对的键值和数据值。

Map会维护与key有关联的值。两个key可以引用相同的对象,但key不能重复,典型的key会是String,但也可以是任何对象。

202.P559

以HashSet取代ArrayList

我们把点歌系统改成使用HashSet。

import java.io.*;import java.util.*;public class Jukebox6 {​    ArrayList<Song> songList = new ArrayList<Song>();​    public static void main(String[] args) {​        new Jukebox6().go();​    }​    class ArtistCompare implements Comparator<Song> {​        public int compare(Song one, Song two) {​            return one.getArtist().compareTo(two.getArtist());​        }​    }​    public void go() {​        getSongs();//这个方法没有更新,所以它还是会把Song加到ArrayList中​        System.out.println(songList);​        Collections.sort(songList);​        System.out.println(songList);​        HashSet<Song> songSet = new HashSet<Song>(); /*创建参数化的HashSet来保存Song*/​        songSet.addAll(songList);​        /*addAll可以复制其他集合的元素,效果就跟一个一个加进去一样*/​        System.out.println(songSet);​    }​    void getSongs() {​        try {​            File file = new File("SongListMore.txt");​            BufferedReader reader = new BufferedReader(new FileReader(file));​            String line = null;​            while ((line = reader.readLine()) != null) {​                addSong(line);​            }​        } catch (Exception ex) {​            ex.printStackTrace();​        }​    }​    void addSong(String lineToParse) {​        String[] tokens = lineToParse.split("/");​        Song nextSong = new Song(tokens[0], tokens[1], tokens[2], tokens[3]);​        songList.add(nextSong);​    }}
public class Song implements Comparable<Song> {​    String title;​    String artist;​    String rating;​    String bpm;​    public int compareTo(Song s) {​        return title.compareTo(s.title);​    }​    Song(String t, String a, String r, String b) {​        title = t;​        artist = a;​        rating = r;​        bpm = b;​    }​    public String getTitle() {​        return title;​    }​    public String getArtist() {​        return artist;​    }​    public String getRating() {​        return rating;​    }​    public String getBpm() {​        return bpm;​    }​    public String toString() {​        return title;​    }}

203.P560

对象要怎样才算相等?

首先我们得问一个问题:两个Song的引用怎样才算重复?它们必须被认为是相等的。这是说引用到完全相同的对象,还是有相同歌名的不同对象也算?

这带出一个很关键的议题:引用相等性和对象相等性。

1)引用相等性

堆上同一对象的两个引用

引用到堆上同一个对象的两个引用是相等的。就这样。如果对两个引用调用hashCode(),你会得到相同的结果。如果没有被覆盖的话,hashCode()默认的行为会返回每个对象特有的序号(大部分的Java版本是依据内存位置计算此序号,所以不会有相同的hashcode)。

如果想要知道两个引用是否相等,可以使用==来比较变量上的字节组合。如果引用到相同的对象,字节组合也会一样。

2)对象相等性

堆上的两个不同对象在意义上是相同的

如果你想要把两个不同的Song对象视为相等的,就必须覆盖过从Object继承下来的hashCode()方法与equals()方法。

就因为上面所说的内存计算问题,所以你必须覆盖过hashCode()才能确保两个对象有相同的hashcode,也要确保以另一个对象为参数的equals()调用会返回true。

204.P563

hashCode()与equals()的相关规定

API文件有对对象的状态制定出必须遵守的规则:

1)如果两个对象相等,则hashcode必须也是相等。

2)如果两个对象相等,对其中一个对象调用equals()必须返回true。也就是说,若a.equals(b)则b.equals(a)。

3)如果两个对象有相同的hashcode值,它们也不一定是相等的。但若两个对象相等,则hashcode值一定是相等的。

4)因此若equals()被覆盖过,则hashCode()也必须被覆盖。

5)hashCode()的默认行为是对在heap上的对象产生独特的值。如果你没有override过hashCode(),则该class的两个对象怎样都不会被认为是相同的。

6)equals()的默认行为是执行==的比较。也就是说会去测试两个引用是否对上heap上同一个对象。如果equals()没有被覆盖过,两个对象永远都不会被视为相同的,因为不同的对象有不同的字节组合。

a.equals(b)必须与a.hashCode() == b.hashCode()等值。

但a.hashCode() == b.hashCode()不一定要与a.equals()等值。

205.P563

问:为什么不同对象会有相同hashcode的可能?

答:HashSet使用hashcode来达成存取速度较快的存储方法。如果你尝试用对象来寻找ArrayList中相同的对象(也就是不用索引来找),ArrayList会从头开始找起。但HashSet这样找对象的速度就快多了,因为它使用hashcode来寻找符合条件的元素,因此当你想要寻找某个对象时,通过hashcode就可以很快地算出该对象所在的位置,而不必从头一个一个找起。

数据结构的课程绝对会告诉你更完整的理论,但这样说明也能让你知道如何有效率地运用HashSet。事实上,如何开发出有效率的杂凑算法一直都是博士论文的热门题目。

重点在于hashcode相同并不一定保证对象时相等的,因为hashCode()所使用的杂凑算法也许刚好会让多个对象传回相同的杂凑值。越糟糕的杂凑算法越容易碰撞,但这也与数据值域分布的特性有关。总而言之,把数据结构这科目搞定就对了。

如果HashSet发现在比对的时候,同样的hashcode有多个对象,它会使用equals()来判断是否有完全的符合。也就是说,hashcode是用来缩小寻找成本,但最后还是要用equals()才能认定是否真的找到相同的项目。

206.P564

如果想要保持有序,使用TreeSet

TreeSet在防止重复上面与HashSet是一样的。但它还会一直保持集合处于有序状态。如果使用TreeSet默认的构造函数,它工作起来就会像sort()一样使用对象的compareTo()方法来排序。但也可以选择传入Comparator给TreeSet的构造函数。缺点是如果不需要排序时就会浪费处理能力。但你可能会发现这点损耗实在很小。

207.P566

TreeSet的元素必须是Comparable

要使用TreeSet,下列其中一项必须为真。

1)集合中的元素必须是有实现Comparable的类型。

import java.util.*;public class TestTree {​    public static void main(String[] args) {​        new TestTree().go();​    }​    public void go() {​        Book b1 = new Book("How Cat Work");​        Book b2 = new Book("Remix your Body");​        Book b3 = new Book("Finding Emo");​        TreeSet<Book> tree = new TreeSet<Book>();​        tree.add(b1);​        tree.add(b2);​        tree.add(b3);​        System.out.println(tree);​    }​    class Book implements Comparable{​        String title;​        public Book(String t) {​            title = t;​        }​        public int compareTo(Object b) {​            Book book = (Book) b;​            return (title.compareTo(book.title));​        }​    }}

2)使用重载、取用Comparator参数的构造函数来创建TreeSet。

import java.util.*;public class TestTree {​    public static void main(String[] args) {​        new TestTree().go();​    }​    public void go() {​        Book b1 = new Book("How Cat Work");​        Book b2 = new Book("Remix your Body");​        Book b3 = new Book("Finding Emo");​        BookCompare bCompare = new BookCompare();​        TreeSet<Book> tree = new TreeSet<Book>(bCompare);​        tree.add(b1);​        tree.add(b2);​        tree.add(b3);​        System.out.println(tree);​    }​    class Book {​        String title;​        public Book(String t) {​            title = t;​        }​    }​    public class BookCompare implements Comparator<Book>{​        public int compare(Book one, Book two) {​            return (one.title.compareTo(two.title));​        }​    }}

208.P567

现在来看Map

List和Set很好用,但有时Map才是最好的选择。

Map示例:

import java.util.HashMap;public class TestMap {​    public static void main(String[] args) {​        HashMap<String, Integer> scores = new HashMap<String, Integer>();​        //HashMap需要两个类型参数:关键字和值​        scores.put("Kathy", 42); //使用put取代add(),它需要两个参数​        scores.put("Bert", 343);​        scores.put("Skyler", 420);​        System.out.println(scores);​        System.out.println(scores.get("Bert"));​    }}

209.P569

使用多态参数与泛型

万用字符

这听起来很怪,但却还有一种能够创建出接受Animal子型参数的方法,这就是万用字符(wildcard),这是它被加入Java语言中的原因。

public void takeAnimals(ArrayList<? extends Animal> animals) {​    for(Animal a: animals) {​        a.eat();​    }}

你可能会怀疑“有什么差别?问题还不是一样?Dog群还不是有可能被加入Cat”。

你的疑问没错,但实际上在使用带有<?>的声明时,编译器不会让你加入任何东西到集合中!

在方法参数中使用万用字符时,编译器会阻止任何可能破坏引用参数所指集合的行为。

你能够调用list中任何元素的方法,但不能加入元素。

也就是说,你可以操作集合元素,但不能新增集合元素。如此才能保障执行期间的安全性,因为编译器会阻止执行期的恐怖行动。

所以下面这个程序是可以的:

for(Animal a: animals) {​    a.eat();}

但这个就过不了编译:

animals.add(new Cat());

210.P575

相同功能的另一种语法

你或许还记得当讨论到sort()方法时,它使用到泛型类型,但以一个不寻常的格式在返回类型前声明类型参数。这是另外一种类型参数的声明方式,但结果是一样的。

这一行:

public <T extends Animal> void takeThing(ArrayList<T> list)

跟这一行执行一样:

public void takeThing(ArrayList<? extends Animal> list)

问:如果都一样,为什么要用有问号的那个?

答:这要看你是否会使用到T来决定。举例来说,如果方法有两个参数——都是继承Animal的集合会怎样?此时,只声明一次会比较有效率。

public <T extends Animal> void takeThing(ArrayList<T> one, ArrayList<T> two)

而不必这样:

public void takeThing(ArrayList<? extends Animal> one, ArrayList<? extends Animal> two)

211.P581

包、jar存档文件和部署

发布程序

最后这两章会来讨论如何组织、包装与部署Java程序。我们会触及包括可执行的jar、Java Web Start、RMI与Servlets等本机、半本机与远程部署的选项。这一章大部分的内容会叙述程序代码的组织与包装。

在这一章中,我们会讨论本机部署,包括Executable Jar与称为Java Web Start的半本机半远程技术。下一章会讨论较远程的部署选择,包括RMI和Servlet在内。

212.P584

将源代码与类文件分离

带有一堆源代码和类文杰的目录是一团混乱的。所以应该要好好地整理一下文件,让源代码与编译出的文件分开。也就是说,确保编译过的类文件不会放在源代码的目录中。

关键在于结合-d这个编译选项和目录组织的结构。

有好几种方法可以组织文件,你们公司可能有规定要怎么做。然而我们会建议一种几乎已成为标准的组织化纲要。

使用这种纲要时,你会创建出项目目录,下面有source和classes目录。把源代码(.java)存储在source目录下。在编译时动点手脚让输出(.class)产生在classes目录。

有个编译器选项能够这么搞。

编译时加上-d(directory)选项。

比如java文件在Windows7桌面上,想要编译到桌面上的classes文件夹中:

C:\Users\LI>cd Desktop

C:\Users\LI\Desktop>javac -d ./classes MyApp.java

如果要编译全部的.java文件:

C:\Users\LI\Desktop>javac -d ./classes *.java

如果java源文件中有中文注释导致编译时出现:“编码GBK的不可映射字符”,则需要添加-encoding UTF-8字段:

C:\Users\LI\Desktop>javac -encoding UTF-8 -d ./classes *.java

执行编译的java程序:

C:\Users\LI>cd Desktop

C:\Users\LI\Desktop>cd classes

C:\Users\LI\Desktop\classes>java MyApp

213.P585

把程序包进JAR

JAR就是JavaARchive。这种文件是个pkzip格式的文件,它能让你把一组类文件包装起来,所以交付时只需要一个JAR文件。如果你很熟悉UNIX上的tar命令的话,你就会知道jar这个工具要这么使用(注意:当我们提到全大写的JAR时是说集合起来的文件,全小写的jar是用来整理文件的工具)。

问题是用户要拿JAR怎么办?

你会创建出可执行的JAR。

可执行的JAR代表用户不需要把文件抽出来就能运行。程序可以在类文件保存在JAR的情况下执行。秘诀在于创建出manifest文件,它会带有JAR的信息,告诉Java虚拟机哪个类含有main()这个方法!

创建出可执行的JAR

1)确定所有的类文件都在classes目录下。

2)创建manifest.txt来描述哪个类带有main()方法。

manifest.txt文件带有下面这一行:

Main-Class: Myapp

说明:Main-Class:后面一定要加空格再加MyApp,MyApp后面没有.class。在Main-Class: MyApp后面要有换行,否则有可能出错。将此文件放

在classes目录下。

3)执行jar工具来创建带有所有类以及manifest的JAR文件

C:\Users\LI\Desktop\classes>jar -cvmf manifest.txt appl.jar *.class

C:\Users\LI\Desktop\classes>jar -cvmf manifest.txt appl.jar MyApp.class

执行JAR

C:\Users\LI\Desktop\classes>java -jar appl.jar

214.P587

把类包进包中!

用包防止类名称的冲突

回忆一下第6章我们讨论过包的名称就像是类的全名。技术上称为full-qualified name。ArrayList其实是java.util.ArrayList,JButton其实叫做javax.swing.JButton,而Socket全名是java.net.Socket。注意到其中两个都是以java开头的。当你在处理包结构时要想到继承层次,并把你的类也做同样的安排。

防止包名称冲突

把类包进包中可以减少与其他类产生命名冲突的机会,但要如何防止两个程序员做出同名的包呢?

Sun建议的命名规则能够大幅降低冲突的可能性——加上你所取得的域名称。它会是独一无二的。也行有好几个人都会叫做“淑芬”,但是不会有两个网域都叫“oreilly.com.cn”。

反向使用domain的包名称

​ com.headfirstjava.projects.Chart

说明:com.headfirstjava就是将domain名称反过来放在前面,类Chat的第一个字母是大写的。

215.P589

把类包进包中的步骤:

1)选择包名称

我们以com.headfirstjava为例。类的名称为PackageExercise,因此完整的名称会是:com.headfirstjava.PackageExercise。

2)在类中加入包指令

这必须是程序源文件的第一个语句,比import语句还要靠上。每个原始文件只能有一个包指令,因此同一文件中的类都会在同一个包中。当然也包括内部。

例:

package com.headfirstjavaimpoert javax.swing.*;public class PackageExercise {​    //life-altering code here}

3)你必须把类放在与包层次结构相对应的目录结构下。

如果完整名称是com.headfirstjava.PackageExercise,则你必须把PackageExercise.class源文件放在名为headfirstjava的目录下,此目录必须在com目录下。

216.P590

编译与执行包

当类包在包中,编译与执行都要有点技巧。

假设有个MyProject文件夹,该文件夹下有classes和source两个文件夹,PackageExercise.java在source文件夹的com文件夹的headfirstjava文件夹下

加上-d(directory)选项来将源文件编译到classes文件夹下:

C:\Users\LI>cd Desktop

C:\Users\LI\Desktop>cd MyProject

C:\Users\LI\Desktop\MyProject>cd source

C:\Users\LI\Desktop\MyProject\source>

javac -d …/classes com/headfirstjava/PackageExercise.java

或编译com.headfirstjava这个包的所有.java文件:

C:\Users\LI\Desktop\MyProject\source>

javac -d …/classes com/headfirstjava/*.java

现在进入到classes文件夹下执行程序

C:\Users\LI\Desktop\MyProject\source>cd …

C:\Users\LI\Desktop\MyProject>cd classes

C:\Users\LI\Desktop\MyProject\classes>java com.headfirstjava.PackageExercise

217.P591

其实-d选项真地很酷

加上-d来编译是很棒的事情,因为它不仅让你把编译结果输出到别的地方,它还可以把类依照包的组织放到正确的目录上。

更棒的还在后头!

假设说你已经把源代码的目录结构设定好了,但还没有设定相对应的输出目录结构。没问题!加上-d的编译操作也会让编译器在遇到目录结构尚未建立时主动帮你做出来。

218.P592

以包创建可执行的JAR

当你把类包进包中,包目录结构必须在JAR中!你不能只把类装到JAR里面,还必须确定目录结构没有多往上走。包的第一层目录(通常是com)必须

是JAR的第一层目录!如果你不小心从上面的目录包下来(例如从classes开始包),JAR就无法正确运行。

创建可执行的JAR:

1)确定所有的类文件都放在class目录下相对应的包结构中。

2)创建manifest.txt文件来描述哪个类带有main(),以及确认有使用完整的类名称。

在manifest.txt写入一行:

Main-Class: com.headfirstjava.PackageExercise

然后把manifest文件放到classes目录下。

3)执行jar工具来创建带有目录结构与manifest的JAR文件。

只要从com开始就行,其下整个包的类都会被包进去JAR。

C:\Users\LI\Desktop>cd MyProject

C:\Users\LI\Desktop\MyProject>cd classes

C:\Users\LI\Desktop\MyProject\classes>jar -cvmf manifest.txt packEx.jar com

说明:最后的com意思是指定com目录。只要指定com目录就行,剩下的都不会有问题

219.P593

那manifest文件会跑到哪里?

看进去JAR就会知道。jar工具不只可以从命令栏中创建和执行JAR而已,你也可以把JAR的内容物解压出来(就像unzip或untar一样)。

假设说你已经把packEx.jar放到Skyler这个目录下。

条列和解压的jar命令:

1)将JAR内容列出。

C:\Users\LI>cd Desktop

C:\Users\LI\Desktop>cd Skyler

C:\Users\LI\Desktop\Skyler>jar -tf packEx.jar

说明:tf代表Table File,也就是列出文件的列表。

2)解压JAR。

C:\Users\LI\Desktop\Skyler>jar -xf packEx.jar

说明:xf代表eXtract File,就像unzip一样,如果把packEx.jar解开,你会在当前目录之下看到META-INF和com目录。

META-INF代表META INformation,jar工具会自动创建出这个目录和MANIFEST.MF文件,你的manifest.txt不会被带进JAR中,但它的内容会放进真正的manifest中。

220.P597

Java Web Start

运用Java Web Start(JWS),你的应用程序可以从浏览器上执行首次启动(从web来start,懂了吗?)但它运行起来几乎像是个独立的应用程序而不受浏览器的束缚。一旦它被下载到使用者的计算机之后(首次浏览网址来下载),它就会被保存下来。

Java Web Start是个工作上如同浏览器plug-in的小Java程序(就像ActiveX组件或用浏览器打开.pdf文件出现的Acrobat Reader),这个程序被称为Java Web Start的helper app,主要目的是用来管理下载,更新和启动JWS程序。

当JWS下载你的程序(可执行的JAR)时,它会调用程序的main()。然后用户就可以通过JWS helper app启动应用程序而不需回到当初的网页。

这还不是最棒的,JWS还能够检测服务器上应用程序局部(例如说某个类文件)的更新——在不需要用户介入的情况下,下载与整合更新过的程序。

当然这还有点问题,比如用户要如何取得Java以及JWS。但这个问题也可以解决:从Sun下载JWS。如果装了JWS但Java版本不是最新的,Java 2 Standard Edition也会被下载到用户计算机上。

最棒的是这一切都很简单。你可以把JWS应用程序当做HTML网页或.jpg图文件一样的网络资源,设置一个链接到你的JWS应用程序上,然后就可以工

作了。

反正JWS应用程序就跟从网络上下载的可执行JAR一样。

221.P598

Java Web Start的工作方式

1)客户端点击某个网页上JWS应用程序的链接(.jnlp文件)。

The Web page link

<a href="MyApp.jnlp">Click</a>

2)Web服务器收到请求发出.jnlp文件(不是JAR)给客户端的浏览器。

.jnlp文件是个描述应用程序可执行JAR文件的XML文件。XML是一种应用非常广泛的标记语言,与另一种广为人知的标记语言HTML相似,它只是添加了一些额外标记(例如:<书名>疯狂XML讲义</书名>)的文本文件。

3)浏览器启动Java Web Start,JWS的helper app读取.jnlp文件,然后向服务器请求MyApp.jar。

4)Web服务器发送.jar文件。

5)JWS取得JAR并调用指定的main()来启动应用程序。

然后用户就可以在离线的情况下通过JWS来启动应用程序。

222.P600

创建与部署Java Web Start的步骤

1)将程序制作成可执行的JAR

2)编写.jnlp文件

3)把.jnlp与JAR文件放到Web服务器

4)对Web服务器设定新的mime类型

application/x-java-jnlp-file

这会让Web服务器以正确的header送出.jnlp数据,如此才能让浏览器知道所接收的是什么。

5)设定网页连接到.jnlp文件

<HTML><BODY><a href="MyApp2.jnlp">Launch My Application</a></BODY></HTML>

223.P601

问:Java Web Start与applet有什么不同?

答:applet无法独立于浏览器之外。applet是网页的一部分而不是单独的。浏览器会使用Java的plug-in来执行applet。applet没有类似程度的自动

更新等功能,且一定得从浏览器上面执行。对JWS应用程序而言,一旦从网站上面下载后,用户不必通过浏览器就可以离线执行程序。

问:JWS有什么安全性的限制?

答:JWS有包括用户硬盘的读写等好几项限制。但JWS自有一套API可操作特殊的对话框来打开和存储文件,因此应用程序可以在用户同意的情况下存取硬盘上特定受限区域的文件。

224.P607

远程部署的RMI

分布式计算

想象一下电子商务的情境。有时候,程序的某些部分就是得在服务器上执行,而客户端会在不同用户的计算机上执行。这一章会介绍Java的远程程序调用(Remote Method Invocation,RMI)技术。我们也会很快地看过Servlet、Enterprise Java Bean(EJB)、Jini以及EJB与Jini是如何运用RMI。最后你还会以Java创建出一个通用服务浏览器。

本书所描述的方法调用到目前为止都是对运行在相同Java虚拟机上的对象所进行的。也就是说调用方与被调用方都是在同一个堆上。

但如果要调用另一台计算机上,另一个Java虚拟机上面的对象的方法呢?我们当然可以自己定义和设计通信协议来调用,然后通过Socket把执行的结果再传回去。想想就知道这有多麻烦。如果能够取得另一台计算机上对象的引用该有多好。

如果你想要调用远程的对象(像是别的堆上的),却又要像是一般的调用。

这就是RMI能够带给你的功能!

225.P611

远程过程调用的设计

要创建出4钟东西:服务器、客户端、服务器辅助设施和客户端辅助设施

1)创建客户端和服务器应用程序。服务器应用程序是个远程服务,是个带有客户端会调用的方法的对象。

2)创建客户端和服务器端的辅助设施(helper)。它们会处理所有客户端和服务器的低层网络输入/输出细节,让你的客户端和程序好像在处理本机调用一样。

辅助设施的任务

辅助设施是个在实际上执行通信的对象。它们会让客户端感觉上好像是在调用本机的对象。事实上正是这样。客户端调用辅助设施的方法,就好像客户端就是服务器一样。客户端是真正服务的代理(proxy)。

也就是说,客户端以为它调用的是远程的服务,因为辅助设施假装成该服务对象。

但客户端辅助设施并不是真正的远程服务,虽然辅助设施的举止跟它很像(因为它提供的方法跟服务所声明的一样),却没有任何真正客户端需要的方法逻辑。相反,辅助设施会去连接服务器,将调用的信息传送过去(像是方法的名称和参数的内容),然后等待服务器的响应。

在服务器这端,服务器的辅助设施会通过Socket连接来自客户端设施的要求,解析打包送来的信息,然后调用真正的服务。因此对服务对象来说此调用来自于本地。

服务的辅助设施取得返回值之后就把它包装然后送回去(通过Socket的输出串流)给客户端的辅助设施。客户端的辅助设施会解开这些信息传给客户端的对象。

226.P614

Java RMI提供客户端和服务器端的辅助设施对象!

在Java中,RMI已经帮你创建客户端和服务器端的辅助设施,它也知道如何让客户端辅助设施看起来像是真正的服务。也就是说,RMI知道如何提供相同的方法给客户端调用。

此外,RMI有提供执行期所需全部的基础设施,包括服务的查询以让客户端能够找到与取得客户端的辅助设施(真正服务的代理人)。

使用RMI时,你无需编写任何网络或输入/输出的程序。客户端对远程方法的调用就跟对同一个Java虚拟机上的方法调用是一样的。

一般调用和RMI调用有一点不同。虽然对客户端来说此方法调用看起来像是本地的,但是客户端辅助设施会通过网络发出调用。那我们对网络与输入/输出有什么感觉呢?

它们都会有风险!

它们是会抛出异常的。

因此客户端必须要认识到这个风险。客户端必须要认知到当它对远程方法调用时,虽然只是对本地代理人调用,此调用最终还是会涉及到Soket和串流。一开始是本机调用,代理会把它转成远程的。中间的信息是如何从Java虚拟机送到Java虚拟机要看辅助设施对象所用的协议而定。

使用RMI时,你必须要决定协议:JRMP或是IIOP。

JRMP是RMI原生的协议,它是为了Java对Java间的远程调用而设计的。另外一面,IIOP是为了CORBA(Common Object Request Broker Architecture)而产生的,它让你能够调用Java对象或其他类型的远程方法。CORBA通常比RMI麻烦,因为若两端不全都是Java的话,就会发生一堆可怕的转译和交谈操作。

幸好,我们只需要关心Java对Java的部分,所以会使用相当简易的RMI。

在RMI中,客户端的辅助设施称为stub,而服务器端的辅助设施称为skeleton。

227.P615

创建远程服务的5个步骤:

1)创建Remote接口

远程的接口定义了客户端可以远程调用的方法。它是个作为服务的多态化类。stub和服务都会实现此接口!

2)实现Remote

这是真正执行的类。它实现出定义在该接口上的方法。它是客户端会调用的对象。

3)用rmic产生stub与skeleton

客户端和服务器都有helper。你无需创建这些类或产生这些类的源代码。这都会在你执行JDK所附的rmic工具时自动地处理掉。

4)启动 RMI registry(rmiregistry)

rmiregistry就像是电话簿。用户会从此处取得代理(客户端的stub/helper对象)。

5)启动远程服务

你必须让服务对象开始执行。实现服务的类会起始服务的实例并向 RMI registry 注册。要有注册后才能对用户提供服务。

228.P620

客户端如何取得stub对象?

客户端必须取得stub对象,因为客户端必须要调用它的方法。这就得靠RMI registry了。客户端会像查询电话簿一样地搜索,找出上面有相符名称的服务。

1)客户端查询 RMIregistry

Naming.lookup(“rmi:127.0.0.1/Remote Hello”);

2)RMI registry返回stub对象。

RMI会自动地将stub解序列化。你必须要有rmic所产生的stub类,否则客户端的stub不会被解序列化。

3)客户端就像取得真正的服务一样的调用stub上的方法。

用户如何取得stub的类?

现在有个很有意思的问题。不管怎样做,客户端在查询服务时一定要有stub类(之前用rmic产生出来的),不然就无法在客户端解序列化。在最简单的情况下,你只要把stub的类直接交给用户就行。

但是还有更酷的方式,然而那已经超出本书的范围了。不过我们还是稍微地说明一下:最简单的方式称为“dynamic class downloading”。使用动态类下载时,stub对象会被加上注明RMI可以去哪里找到该对象的类文件的URL标记。之后在解序列化的过程中,RMI会在本机上找不到类,所以就会使用HTTP的Get来从该URL取得类文件。因此你会需要一个Web服务器来提供类文件,并且也得改变客户端的某些安全性设定。这里面还有些特别的问题,不过我们就先看过动态类下载的概念就行。

229.P622

确保每台机器都有所需的类文件

使用RMI时程序员最常犯的3个错误:

1)忘记在启动远程服务前启动rmiregistry(使用Naming.rebind()注册服务前rmiregistry必须启动)。

2)忘记把参数和返回类型做成可序列化(编译器不会检测到,执行时才会发现)。

3)忘记将stub类交给客户端。

230.P625

关于servlet

servlet是放在HTTP Web服务器上面运行的Java程序。当用户通过浏览器和网页交互时,请求(request)会送给网页服务器。如果请求需要Java的servlet时,服务器会执行或调用已经执行的servlet程序代码。servlet只是在服务器上运行的程序代码,执行出用户发出请求所要的结果(例如说将数据存进数据库)。如果你本来就熟悉使用Perl来编写的CGI,那你就会知道我们说的是什么。网页开发者使用CGI或servlet来操作用户提交(submit)给服务器的信息,像是发表在讨论版上的文章。

而servlet也可以使用RMI!

目前最常见的J2EE技术混合了servlet和EJB,前者是后者的用户。此时,servlet是通过RMI来与EJB通信的(但这与我们之前看到的程序有点不同)。

1)用户填写网页上的表格并提交。HTTP服务器收到请求,判断出是要给servlet的,就将请求传过去。

2)servlet开始执行,把数据存到数据库中,然后组合出返回给浏览器的网页。

231.P626

创建并执行servlet的步骤

1)找出可以存放servlet的地方

我们假设你已经有网页服务器可以运行servlet。最重要的事情是找到哪里可以存放servlet文件让服务器可以存取。如果服务器是向ISP租借的,技术支持的人应该会告诉你可以放在哪里,就像他们也会跟你说哪里可以放CGI一样。

2)取得servlets.jar并添加到classpath上

servlet并不是Java标准函数库的一部分,你需要包装成servlets.jar的文件。这可从java.sun.com下载。或者从默认好可执行Java的网页服务器(例如在apache.org网站的Apache Tomcat)。没有这些类你将无法编译servlet。

3)通过extend过HttpServlet来编写servlet的类

servlet是个extend过的HttpServlet(javax.servlet.http)的类。还有其他类型的servlet可以创建,但通常你只会使用HttpServlet。

public class MyservletA extends HtpServlet { … }

4)编写HTML来调用servlet

当用户点击引用到servlet的网页链接时,服务器会找到servlet并根据HTTP的GET、POST等命令调用适当的方法。

<a href="servlets/MyServletA">This is the most amazing servlet.</a>

5)给服务器设定HTML网页和servlet

这就要看你的网页服务器而定了(哪一个版本的Servlets)。ISP可能会跟你说放到Servlets目录下面就可以。但如果你用的是最新版的Tomcat,可能要多花一点时间才搞定。

232.P628

问:什么是JSP?它跟servlet有什么关系?

答:JSP代表Java Server Pages。实际上Web服务器最终会把JSP转换成servlet,但差别在于你所写出的是JSP。servlet是让你写出带有HTML输出的类,而JSP刚好相反——你会写出带有Java程序的网页!

问:花很多页讨论了RMI之后,关于servlet的讨论就这样吗?

答:没错。RMI是Java语言的一部分,所有的RMI相关类也在标准函数库中。而servlet和JSP则不是Java语言的一部分,也不被认为是标准的扩充套件。RMI可以在任何新版的Java虚拟机上面执行,但servlet和JSP需要正确设定好的Web服务器和servlet的容器,这就是为什么说这个部分已经超出本书的范围。但你还是可以从《Head First Servlets & JSP》这本书寻求更多帮助。

233.P631

Enterprise JavaBeans:打了类固醇的RMI

RMI很适合编写并运行远程服务。但你不会单独使用RMI来执行Amazon或eBay网站服务。对大型的企业级应用程序来说,你需要更多更好的功能。

你需要交易管理、大量并发处理(全世界的人一起上来抢购“哈利.波特——消失的蜜饯”)、安全性和数据库管理等。为此,你会需要enterprise application server。

用Java术语来说,这就是Java 2 Enterprise Edition(J2EE)服务器。J2EE服务器包括了Web服务器和Enterprise JavaBeans(EJB)服务器。就跟servlet一样,EJB已经超过了本书的范围,且EJB也无法用简单的范例展示。但我们会对它的工作方式稍加说明。(译注:业界已经出现一股反对EJB的声音,理由就跟你不会开十八轮大货车去市场买菜一样,工具和技术并不是越大越重就越合适。)

234.P632

最后,来点Jini魔法

我们是超爱Jini的。我们认为Jini应该是Java最棒的东西。如果EJB是打了类固醇的RMI,Jini就是长了翅膀的RMI。真是个天上掉下来的礼物。也跟EJB一样,我们无法在这本书讨论Jini的细节,但若你搞定RMI的话,以技术观点来说那就已经接近一半真相的一半。以观念的理解而言,则中间还有一大段路要走。

Jini也是使用RMI(虽然也可以用别的协议),但多了几个关键功能:

1)adaptive discovery(自适应探索)。

2)self-healing networks(自恢复网络)。

要知道RMI的客户端得先取得远程服务的地址和名称。客户端的查询程序代码就要带有远程服务的IP地址或主机名(因为RMI registry就在上面)以及服务所注册的名称。

但使用Jini时,用户只需知道一件事:服务所实现的接口!这样就行。

那怎么找到的呢?秘诀就在于Jini的lookup service。Jini的查询服务比RMI registry更强更有适应性。因为Jini会在网络上自动的广告。当查询服务上线时,它会使用IP组播技术送出信息给整个网络说:“大爷我在这里,想找东西就问我”。

不只是这样,如果客户端在查询服务已经广播之后上线,客户端也可以发出信息给整个网络说:“那个谁在不在啊?”。

其实你感兴趣的不是查询服务,而是已经注册的服务。像RMI远程服务,其他可序列化的Java对象,甚至像打印机、相机和咖啡机。

更棒的还在后头:当服务上线时,它会动态地探索网络上的Jini查询服务并申请注册。注册时,服务会送出一个序列化的对象给查询服务。此对象可以是RMI远程服务的stub,网络装置的驱动程序,甚或是可以在客户端执行的服务本身。并且注册的是所实现的接口,而不是名称。

一旦取得查询服务的引用,客户端就可以查询“有没有东西实现ScientificCalculator?”。此时查询服务若找到,就会返回该服务所放上来的序列化对象。

235.P633

自适应探索的运作

1)Jini查询服务在网络上启动,并使用IP组播技术为自己做宣传。

2)已经启动的另外一个Jini服务会寻求向刚启动的查询服务注册。它注册的是功能而不是名称,也就是所实现的接口,然后送出序列化对象给查询服务。

3)网络客户想要取得实现ScientificCalculator的东西,可是不知道哪里有,所以就问查询服务。

4)查询服务响应查询的结果。

自恢复网络的运作

1)某个Jini服务要求注册,查询服务回给一份租约。新注册的服务必须要定期更新租约,不然查询服务会假设此服务已经离线了。查询服务会力求呈现精确完整的可用服务网络状态。

2)因为关机所以服务离线,因此也没有更新租约,查询服务就把它踢掉。

236.P636

终极任务:通用服务浏览器

我们要做一个没有Jini功能的程序,但很容易实现。它会让你体验Jini,却只有用到RMI。事实上我们的程序与Jini应用程序的主要差别只在于服务是如何探索的。相对于Jini的查询服务会自动地对所处的网络做广告,我们使用的是必须与远程服务在同一台机器上执行的RMI registry,这当然不会自动地声明。

且服务也不会自动地向查询服务做注册,我们必须将它注册给RMI registry。

一旦用户在RMI registry找到服务,应用程序其余的部分就跟Jini的方式几乎一模一样(当然还少了租约和续约机制这回事)。

通用服务浏览器就像是个特殊化的网页浏览器,只是所展示的并非HTML网页。此服务浏览器会下载并显示出交互的Java图形界面。

它的运作方式:

1)用户启动并查询在RMI registry上注册为ServiceServer的服务,然后取回stub。

2)客户端对stub调用getServiceList()。它会返回服务的数组。

3)客户端以GUI显示出服务对象的清单。

4)用户从清单选择,因此客户端调用getService()取得实际服务的序列化对象然后在客户端的浏览器上执行。

5)客户端调用刚取得序列化对象的getGuiPanel()。此服务的GUI会显示在浏览器中,且用户可与它在本机上交互。此时就不需要远程服务了。

附录:
1.JDK运行环境配置
Windows7下Java环境变量配置:
第一部分
1
右击【计算机】进入【属性】然后选择其中的【高级系统设置】。

2
点击进入【高级】中的【环境变量】,进入环境变量编辑界面。

3
在下方的【系统变量】中,并不存在JAVA_HOME变量,那么我们需要点击【新建】

4
变量名输入:JAVA_HOME

变量值指的是实际的安装路径(比如小编的路径为:C:\Program Files\Java\jdk1.8.0_191)。

最后点击确定。

第二部分
Path变量设置
1
同样是在【系统变量中】我们可以看到path变量已经存在,那么我们只需要点击【编辑】,进入Path变量的编辑

原来系统已有部分不动,然后添加【;%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin】,最后点击确定。

第三部分
classpath变量设置:java1.5版本之后可不需要设置classpath,但这里我们提一下
1
同样的,在系统变量中也不存在classpath变量。点击进入【新建】

2
变量名键入【classpath】

变量值键入【.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar; 】

注意不要漏掉变量值中的符号,最后点击确定。

第四部分
测试环境配置是否成功

完成以上三项变量配置之后不要忘了一步一步点击确定。

同样的,在【开始】中搜索【cmd】。

然后在键入【java】以及【javac】。

出现一堆-开头的提示符,说明配置成功。

2.JDK 奇数版本和偶数版本的区别
小知识 - JDK 奇数版本和偶数版本的区别(你用对了吗?)

Java CPU 和 PSU 版本解释
从 2014 年 10 月发布 Java SE 7 Update 71 (Java SE 7u71) 开始,Oracle 将在发布重要补丁更新 (CPU) 的同时发布相应的 Java SE 7 补丁集更新 (PSU)。

我应当选择哪个 Java 版本:CPU 还是 PSU?
Oracle 强烈建议所有 Java SE 用户升级到相应版本系列的最新 CPU 版本。大多数用户应当选择 CPU 版本。

仅当用户受到版本说明中所述的该版本所修复的其他漏洞的影响时才应使用相应的 PSU 版本。
后续 CPU 版本将包含当前 PSU 的所有修复。鉴于此,组织应当测试其环境中的当前 PSU,这些修复将包含在下一个 CPU 中。

Java CPU 与 PSU 之间的区别?
Java SE 重要补丁更新 (CPU) 包含安全漏洞修复和重要漏洞修复。Oracle 强烈建议所有 Java SE 用户及时升级到最新的 CPU 版本。Java SE CPU 版本号采用奇数编号(即 7u71、7u65 — 有关 Java SE 版本编号方式的详细信息,请点击这里)。

Java SE 补丁集更新 (PSU) 包含相应 CPU 中的所有修复以及其他非重要修复。仅当您受到该版本中其他漏洞的影响时才应当使用 Java PSU。版本说明列出了 Java SE PSU 中的其他修复。

CPU 版本的发布周期会改变吗?
与以往一样,根据常规 Oracle 重要补丁更新计划,Java SE CPU 版本将在距 1 月、4 月、7 月和 10 月的第 17 日最近的星期二发布。

从 2014 年 10 月发布 Java SE 7u71 (CPU) 和 Java SE 7u72 (PSU) 开始,Oracle 计划为每个 Java SE 7 CPU 额外发布一个相应的 PSU。除了相应 CPU 中的重要修复之外,PSU 版本将为组织和开发人员提供一些非重要修复。

一句话:我们应该使用奇数版本

《Head First Java(第2版)中文版》辅导书相关推荐

  1. Head First Java(第2版)中文版.pdf免费资源下载链接,Head First Java第二版中文版.pdf

    Head First Java(第2版)中文版.pdf免费资源下载链接(.pdf书籍的优点是便于直接在电脑中保存有电脑就可以阅读,如果觉得这本书给你提供到了很大的帮助,可以去书店补一本纸质版) 资源保 ...

  2. WCF服务编程(4):《WCF服务编程》第3版中文版翻译结束,即将出版

    原文地址: <WCF服务编程>第4版本翻译完毕!2016年底应该出版 [置顶]↑ WCF服务编程(4):<WCF服务编程>第3版中文版翻译结束,即将出版 ↑ 作为世界范围内最经 ...

  3. python cookbook pdf下载-Python Cookbook 第3版 中文版.pdf

    作 者 :(美)比斯利,(美)琼斯著 出版发行 : 北京:人民邮电出版社 , 2015.05 ISBN号 :978-7-115-37959-7 页 数 : 684 原书定价 : 108.00 开本 : ...

  4. java spring cloud版b2b2c社交电商spring cloud分布式微服务:分布式配置中心

    JAVASpring Cloud大型企业分布式微服务云构建的B2B2C电子商务平台源码请加企鹅求求:一零三八七七四六二六.Spring Cloud Config是Spring Cloud团队创建的一个 ...

  5. Atitit.ati orm的设计and架构总结 适用于java c# php版

    Atitit.ati orm的设计and架构总结 适用于java c# php版 1. Orm的目标 1 1.1. 动态obj 1 1.2. Hb的api(meger,save,update,del) ...

  6. java第三阶段源代码_有效Java第三版的源代码已更新为使用较新的功能

    java第三阶段源代码 那些已经阅读了有效Java 第三版的人可能知道与该书相关的源代码可以在GitHub上获得 . jbloch / effective-java-3e-source-code项目拥 ...

  7. 有效期判断功能 java_有效的Java第三版有哪些新功能?

    有效期判断功能 java 自从听说即将出版的有效Java 第三版以来,我一直想知道其中有什么新内容. 我假设将涵盖自Java 6以来引入Java的功能,的确如此. 但是,第三版Java开发人员经典版也 ...

  8. 有效Java第三版的源代码已更新为使用较新的功能

    那些已经阅读了有效Java 第三版的人可能知道与该书相关的源代码可以在GitHub上获得 . jbloch / efficiency-java-3e-source-code项目拥有1700多个星星,截 ...

  9. 有效的Java第三版有哪些新功能?

    自从听说即将出版的有效Java 第三版以来,我一直想知道其中有什么新内容. 我假设将涵盖自Java 6以来引入Java的功能,的确如此. 但是,第三版Java开发人员经典版也有一些其他更改. 在本文中 ...

  10. Effective Java第三版有哪些新功能?

    自从听说即将出版的有效Java 第三版以来,我一直想知道其中有什么新内容. 我假设将涵盖自Java 6以来引入Java的功能,的确如此. 但是,第三版Java开发人员经典版也有一些其他更改. 在本文中 ...

最新文章

  1. 2021全球数字经济大会“北京数字经济体验周”清华数为大数据开发工具DWF体验培训工作圆满结束...
  2. PS5 发布,揭秘真假难辨的虚拟和现实
  3. 手机息屏后停止_手机息屏还能这样玩?华为这几个隐藏小功能快学起来
  4. 关于SysinternalsSuite全部工具【详解】
  5. Windows7右键菜单栏添加打开cmd项
  6. 博客侧栏添加恋爱计时框
  7. python 配置文件解析代码_python3从零学习-5.5.2、configparser — 配置文件解析器
  8. matlab 人脸识别_MATLAB基于PCALDA模糊神经网络的人脸识别
  9. iPhone 或将告别 3D Touch ?
  10. 在使用SPSite对象时容易发生内存泄漏
  11. 8.8LSTM作为元学习器学习梯度下降
  12. Fragment 源码解析add()和replace()方法
  13. 【批处理学习笔记】第二十五课:间接传递
  14. 【SwitchHosts】安装使用
  15. 软件开发人员应该了解测试和QA
  16. 高数 | 导数极限定理、分段点求导能不能用公式?导数和导数的极限?
  17. html 调用es2015模块,ES 2015 Modules
  18. 如何关注速卖通的一举一动
  19. mysql 创建用户、授权、修改密码
  20. Scrapy Tutorial

热门文章

  1. vscode中输入感叹号无法识别html模板
  2. 热噪声 Thermal noise
  3. 吴闲云——煮酒探西游
  4. 英语口语收集(二十六)
  5. 【qduoj】【yzm10与大富翁的故事】(hdu百度之星)
  6. 疫情期间,找工作的一些建议
  7. springboot 整合谷歌 gRPC
  8. Qt三方库开发技术:二维码生成、识别以及条码识别
  9. 那些年,我们一起追过的球队
  10. 银河麒麟桌面操作系统V10 SP1 如何安装字体