Head First Java 读书笔记(完整)

第0章:学习方法建议

该如何学习Java?

1.慢慢来。理解的越多,就越不需要死记硬背。时常停下来思考。
2.勤作笔记,勤做习题。
3.动手编写程序并执行,把代码改到出错为止。

需要哪些环境和工具?

1.文本编辑器
2.Java API 文档
3.安装 JDK5 以上
4.配置Java环境变量,让java/javac知道去哪里寻找Java/bin中的执行文件。
5.UML图例与规则


第1章:进入Java的世界

Java有哪些特点?

面向对象、内存管理、跨平台write once, run anywhere.
跨平台,这是Java的核心目标。

Java如何工作?Java编译器和Java虚拟机,各有什么用?

1.源代码 -> 2.编译器 -> 3.输出字节码 -> 4.JVM(Java虚拟机)
.java文件 -> .class文件 -> 在JVM上运行
第1步:编写源代码文件
第2步:用javac编译器编译源代码。编译器同时会检查错误,如果有错误就要改正,然后才能产出正确的输出。
第3步:编译器会产出字节码。任何支持Java的装置都能把它编译成可执行的内容。编译后的字节码与平台无关。
第4步:JVM(Java虚拟机)可以读取并执行字节码。只要设备上安装了JVM,即可运行Java字节码程序。

Java历史?

Java1.02 / Java1.1 / Java1.2~1.4 / Java1.5(即Java5)

Java的程序结构是怎样的?

类存在于源文件里面,方法存在于类中,语句存在于方法中。
什么是源文件?源文件以.java为扩展名,其中带有类的定义。
什么是类?类中带有一个或多个方法。
什么是方法?方法必须在类中声明,方法是由一组语句组成。
每个Java程序至少有一个类,并且每个Java程序有且仅有一个main()函数。Java中所有的东西都会属于某个类。

为什么Java中所有的东西都得包含在类中?

因为Java是面向对象语言,类是对象的蓝图。Java中,绝大多数的东西都是对象。

如何解读Java程序的main()方法?
public class App() {
    public static void main (String[] args) {
        // 程序的入口
    }
}

Java虚拟机启动时,它会寻找你在命令行中所指定的类,并以该类中的main()方法为入口函数进行程序执行。
main()的两种用途?
1.用于测试我们编写的Java类。2.作为入口,启动我们的Java程序。

App.java -> App.class -> JVM执行这个程序

javac App.java      // java编译器编译这个程序
java App.class      // JVM执行这个程序

Java有哪些语句?
循环语句、条件语句。


第2章:拜访对象村

当你在设计类时,要记得对象是靠类的模型塑造出来的。对象是已知的事物,对象会执行动作。

什么是对象(实例)?什么是实例变量?什么是方法?

对象带有实例变量和方法,这是类设计的一部分。
对象本身已知的状态,即实例变量,它代表对象的状态,每个对象都会独立地拥有一份该类型的值。
对象可以执行的动作,即方法,方法可以读取或操作实例变量。

类和对象之间到底有什么不同?

类不是对象,却是用来创建对象的模型。类是对象的蓝图,类会告诉JVM如何创建出某种类型的对象。根据一个类,JVM可以创建出无数的对象。

如何理解Java垃圾回收机制?

堆,可自动回收垃圾的堆。

jar包中的manifest文件,指定了程序中唯一main()方法所在的类。


第3章:认识变量

如何理解Java变量?

Java变量就像是杯子,是一种容器,用于承载某种事物,它有大小和类型。Java编译器不允许将大杯的内容放到小杯中,反过来则可以。
Java的命名规则有哪些限制?什么是Java保留字?

Java变量有哪两种类型?

1.八种primitive主数据类型:boolean / char / byte / short / int / long / float / double
2.引用类型:引用到对象的变量。对象只会存在于可回收垃圾堆上。

如何深入理解引用类型?

引用类型变量,其大小决定于JVM厂商。在同一台JVM上,引用类型变量的大小是相同的。在Java中,我们不能对引用类型变量进行任何的运算。

Java变量必须得有类型和名称:

char x = 'x';
int age;
float pi = 3.14f;   // 不加 f,它将被当作double类型来使用。
什么是Java数组?

Java数组是对象。数组好比是杯架。
一旦数组被声明出来,你就只能装入所声明类型的元素。double类型的变量不能装入int数组中,但是int类型的变量可以装入到double类型的数组中。

Dog[] dogs = new Dog[7];
int[] nums = new int[7];
实例变量未初始化时,它的默认值是多少?

数值类型的变量默认值都为0,char类型的默认值也是0。
布尔类型的默认值是false。
引用类型的默认值是null。

实例变量和局部变量有哪些区别?

实例变量是声明在类中的,可以不初始化。
局部变量是声明在方法中的,在被使用之前,必须先初始化。方法的参数,也属于局部变量。

如何判断两个变量是否相等?
  1. == , 比较是两个变量的字节组合是否相等。
  2. equals(),用于判断两个对象是否是真正意义上的相等。
    它们在判断基本数据类型和引用类型时,有什么区别?


第4章:对象的行为

对象中,实例变量和方法之间有着怎样的关系?

状态影响行为,行为影响状态。实例变量即状态,方法即行为。

什么是形参(parameter)和实参(argument)?

我们可以给方法传入参数。实参是传给方法的值。当实参传入方法后就成了形参。
给方法传入实参时,实参的顺序、类型、数量必须与形参的顺序、类型、数量完全一致。
你还可以把变量当作实参传入方法,只要其类型符合要求即可。

方法的参数传递为什么是值传递?什么是值传递?

Java方法的参数传递是值传递形式,即通过拷贝传递。实参进入方法之后就成了形参,形参的变化不会改变实参,这就是值传递的好处。

什么是方法的返回值?

在Java中,方法可以有返回值,每个方法都必须显示地指明返回值类型。如果方法没有返回值,指明为void类型即可。

int getAge() {
    return 30;      // 有返回值,类型为int
}

int age = getAge();     // 接收方法的返回值
什么是Getter和Setter ?

Getter 与 Setter,前者用于返回实例变量的值,后者用于设定实例变量的值。

为什么要封装?为什么把类中的变量暴露出去会很危险?如何对变量数据进行隐藏?

实现方案:把实例变量标记为private私有的,并提供public公有的getter和setter来控制实例变量的存取动作。

封装有什么好处?

封装会对我们的实例变量加上绝对领域,因此没有人能够恶搞我们的实例变量。

class Dog {
    // 私有的实例变量,从而避免了dog.size的方式对实例变量进行操作,因为这很危险。
    private int size;

    // 公有的 Setter,在类的外部,通过dog.setSize()来设置size的值
    public void setSize(int size) {
        this.size = size;
    }
    // 公有的 Getter,在类的外部,通过dog.getSize()来获取size的值
    public int getSize() {
        return this.size;
    }

    void bark() {
        if (size > 10) {} else {}
    }
}

数组中的元素,还可以是对象。

Dog[] pets = new Dog[7];
pets[0] = new Dog();
pets[1] = new Dog();

pets[0].setSize(80);    // 数组中对象调用对象方法


第5章:超强力方法

什么是面向对象的编程思维?

所谓面向对象,就是要专注于程序中出现的事物,而不是过程。

什么是伪码、测试码、真实码?分别有什么用?
  1. 伪码的作用是帮助你专注于逻辑,而不需要顾虑到程序的语法。
  2. 测试码,用于程序代码的测试。
  3. 真实码,即实际设计出来的真正Java程序代码。

Java程序应该从高层的设计开始。通常在创建新的类时,要写出“伪码->测试码->真实码”。伪码应该描述要做什么事情而不是如何做,使用伪码可以帮助测试码的设计,在实现真实码之前应该要编写测试码。

有哪些循环语句?

for循环、while循环、foreach循环(加强版的for循环,用于遍历数组等)

有哪两种强制类型转换?
int a = (int)(Math.random()*5);     // 把非int类型的数字,强转为int类型
int b = Integer.parsetInt("100");   // 把字段串强转为int类型

i++ 和 ++i 有什么区别?


第6章:使用Java函数库

ArrayList 和 一般数组的如此区别?ArrayList有哪些实用的API?

什么是Java包?

在java中,类是被包装在包中的。要使用API中的类,你必须知道它被放在哪个包中。

使用java类的完整名称:

java.util.ArrayList list = new java.util.ArrayList();
public void go(java.util.ArrayList list) { };
public java.util.ArrayList foo() { };

除了java.lang包之外,其它的java包的类在被使用时,都需要使用类的全名,即java.xxx.className形式。或者在程序开头处使用import指令导入所需要的类。

Java包机制有什么好处和意义?

以javax开头的包代表什么意思?如何高效地使用 java api 手册?


第7章:继承与多态

继承与方法重写override,什么是方法重写?

由子类重新定义从父类中继承来的方法,将其改变或延伸。成员变量不存在重写这个说法。

public void roam() {
  // 继承父类方法的功能
  super.roam();
  // 扩展的功能
}
区分 IS-A 与 HAS-A 关系

IS-A,表示二者具有继承关系,这是一种单向的链式关系。
HAS-A,表示二者是包含关系。

四种访问控制修改符,它们有哪些区别?

private < default < protected < public
public类型的成员,会被继承。private类型的成员,不会被继承。
一个类的成员,包含自己定义的变量和方法,还包含从父类所继承下来的成员。

继承到底有什么意义?

避免重复的代码;定义共同的协议。

什么是多态?
对象的声明、创建与赋值:
Dog d = new Dog();

在上述对象的创建过程中,引用类型和对象的类型相同。在多态中,引用类型和对象的类型可以不同

Animal d = new Dog();    // 这就是多态

在多态中,即引用类型可以是实际对象类型的父类(满足 IS-A 关系,就产生了多态)。

Animal[] animals = new Animal[3];
animals[0] = new Dog();
animals[1] = new Cat();
animals[2] = new Wolf();
for(int i=0; i

方法的参数或返回值,也可以是多态。

public Animal getAnimal( Animal a ) {
  // 参数a, 可以是Animal子类的对象
  return a;  // 返回Animal对象或者其子类的实例对象。
}

使用多态,你就可以编写出引进新子类时也不必修改的程序。

继承关系中,方法覆写要遵守哪些约定?

  1. 参数必须要一样,且返回类型必须要兼容。
  2. 不能降低方法的存取权限。

什么是方法重载?
方法名相同,但参数列表不同,即是重载,它与方法的返回值类型、方法的存取权限都无关。方法重载与继承关系中的方法覆写有什么不同?

public class Overloads {
  // 以下两个方法,即为方法重载
  public int addNums ( int a, int b) {
    return a + b;
  }
  public double addNums (double a, double b) {
    return a + b;
  }
}


第8章:接口与抽象类(深入多态)

什么是抽象类?

用abstract关键字声明抽象类,抽象类不能用new 关键字进行实例化。在设计继承结构时,必须决定清楚什么类是抽象类,什么类是具体类。编译器不会让你初始化一个抽象类。抽象类,除了被继承以外,是没有其它任何用途的。抽象类中,必须包含有抽象方法,还可以包含非抽象方法。

什么是抽象方法?

即用 abstract关键字声明的方法,抽象方法没有方法实体,即没有具体的实现过程。拥有抽象方法的类,必须声明为抽象类。抽象类中的抽象方法,用于规定一组子类共同的协议。

abstract class Animal {
    // 抽象方法,没有方法体
    public abstract void eat();
}

在继承过程中,具体类必须实现抽象父类的所有抽象方法

抽象方法没有具体的方法体,它只是为了标记出多态而存在。在覆写抽象父类的抽象方法时,方法名、参数列表必须相同,返回值类型必须兼容。Java很在乎你是否实现了抽象类的抽象方法。

public class Canine extends Animal {
    // 覆写抽象类的抽象方法
    public void eat() {
        System.out.println("Canine,会吃食物!!");
    }
    // 非继承的方法
    public void roam() {
        
    }
}

多态的使用

在Java中,所有类都是从Object这个类继承而来的,Object是所有类的源头,它是所有类的父类。Object有很有用的方法,如 equals(), getClass(), hashCode(), toString()等。

  1. Object类,是抽象类吗? 答:不是,它没有抽象方法。
  2. 是否可以覆写Object中的方法? 答:Object类中带有 final关键字的方法,不能被覆写。
  3. Object类有什么用? 答:用途一,它作为多态可以让方法应付多种类型的机制,以及提供Java在执行期对任何对象都需要的方法实现。另一个用途,它提供了一部分用于线程的方法。
  4. 既然多态类型这么有用,为什么不把所有的参数类型、返回值类型都设定为Object? 答:因为Java是强类型语言,编译器会检查你调用的是否是该对象确实可以响应的方法。即,你只能从确实有该方法的类中去调用。
Object dog = new Dog();
dog.toString();  // 这可以通过编译,因为toString()是Object类中自有的方法。
dog.eat();  // 这将无法通过编译,因为dog是Object类型,它调用的eat()方法在Object类中没有。

在使用多态时,要注意对象多种类型之间的差异。如下代码:

Dog dog1 = new Dog();
Animal dog2 = new Dog();
Object dog3 = new Dog();

注意这三个dog对象的区别: dog1 拥有 Dog / Animal / Object中所有的方法。dog2 拥有 Animal / Object 中的方法,不能调用 Dog 类特有的方法。 dog3 只拥有Object 中的方法,不能调用 Animal / Dog类中的方法。这就是在使用多态过程中,需要特别注意的问题。

那么该如何把 Object 类型的 dog转化成真正的 Dog 类型呢?
if (dog2 instanceof Dog) {
  Dog dog4 = (Dog)dog2;
}
if (dog3 instanceof Dog) {
  Dog dog5 = (Dog)dog3;
}
// 此时,dog4 / dog5 就是真正的 Dog类型了。

什么是接口?

接口,是一种100%纯抽象的类。接口中的所有方法,都是未实现的抽象方法。

为什么需要接口?

接口存在的意义,就是为了解决Java多重继承带来的致命方块问题。为什么接口可以解决致命方块的问题呢?因为在接口中,所有方法都是抽象的,如此一来,子类在实现接口时就必须实现这些抽象方法,因此Java虚拟机在执行期间就不会搞不清楚要用哪一个继承版本了。

// interface关键字,用于定义接口
public interface Pet {
    public abstract void beFriendly();
    public abstract void play();
}
// 继承抽象父类 Animal类, 实现 Pet接口
public class Dog extends Animal implements Pet {
    // 实现接口中的抽象方法
    public void beFriendly() {
        System.out.println("实现 Pet接口中的 beFriendly()方法");
    }
    // 实现接口中的抽象方法
    public void play() {
        System.out.println("实现 Pet接口中的 play()方法");
    }
    // 覆写抽象父类中的抽象方法
    public void eat() {
        System.out.println("覆写抽象父类中的eat()抽象方法");
    }
}
同一个类,可以实现多个接口!
public class Dog extends Animal implements Pet, Saveable, Paintable { ... }

如何判断应该是设计 类、子类、抽象类、还是接口呢?

  1. 如果新的类无法对其它的类通过 IS-A 测试时,就设计成不继承任何类的类。
  2. 只有在需要某个类的特殊化版本时,以覆写或增加新的方法来继承现有的类,得到子类。
  3. 当你需要定义一群子类的模板,又不想让程序员初始化该模板时,就设计出抽象类。
  4. 如果希望类可以扮演多态的角色,就设计出完全抽象的接口。

super关键字代表什么?

super代表父类,在子类中使用 super关键字指代父类,通过super还可以调用父类的方法。

// 抽象父类
abstract class Animal {
  void run () {}
}
// 继承父类
class Dog extends Animal {
  void run () {
    super.run();  // 这里,调用并执行父类的 run() 方法
    // do other things
  }
}
Dog d = new Dog();
d.run();    // 这调用的是子类Dog对象的 run()方法。


第9章:构造器与垃圾收集器

什么是栈与堆? 堆(heap)、栈(stack)

当Java虚拟机启动时,它会从底层操作系统中取得一块内存,以此区段来执行Java程序。实例变量保存在所属的对象中,位于堆上。如果实例变量是对象引用,则这个引用和对象都是在堆上。

构造函数与对象创建的三个步骤

对象创建的三个步骤:声明、创建、赋值。
构造函数,让你有机会介入 new 的过程。构造函数,没有显示的指定返回值类型,构造函数不会被继承。如果一个类,没有显示地编写构造器函数,Java编译器会默认地为该类添加一个没有参数的构造器函数。反之,Java编译器则不会再添加任何默认的构造函数。

Dog dog = new Dog();

什么构造器函数重载?

即一个类,有多个构造器函数,且它们的参数都不能相同,包括参数顺序不同、或者参数类型不同、或者参数个数不同。重载的构造器,代表了该类在创建对象时可以有多种不同的方式。

public class Mushroom {
    // 以下五个构造器,都是合法的,即构造器重载
    public Mushroom() {}
    public Mushroom( int size ) {}
    public Mushroom( boolean isMagic ) {}
    public Mushroom( boolean isMagic, int size ) {}
    public Mushroom( int size, boolean isMagic ) {}
}

什么是构造函数链? super()

构造函数在执行的时候,第一件事就是去执行它的父类的构造函数。这样的链式过程,就被称为“构造函数链(Constructor Chaining)”。

class Animal {
    public Animal() {
        System.out.println("Making an Animal");
    }
}

class Dog extends Animal {
    public Dog() {
        super();    // 如果没有这句,Java编译器会默认添加上这句,即调用父类的无参构造器
        System.out.println("Making an Dog");
    }
}

public class ChainTest {
    public static void main(String[] args) {
        System.out.println("Starting...");
        Dog d = new Dog();
    }
}

如果一个类,没有显示地书写构造器函数,Java编译器会为它添加上默认的无参构造器。如果在一个子类的构造器中没有使用super()调用父类的某个重载构造构造器,Java编译器会为这个子类的构造器默认添加上super(),即在子类的构造器函数中调用父类的无参构造器。
父类的构造器函数,必须在子类的构造器函数之前调用。在子类构造器函数中调用父类构造器时,必须与父类构造器的参数列表一致。

在类中,this 和 super 有什么区别? this() 和 super() 有什么区别?

使用 this() 可以在某个构造函数中调用同一个类的另外一个构造函数。 this() 只能在构造函数中使用,并且必须是第一行。 this() 和 super() 不能同时使用。

class Car {
    private String name;
    // 父类的有参构造器
    public Car(String name) {
        this.name = name;
        System.out.println(name);
    }
}

class MiniCar extends Car {
    // 构造器
    public MiniCar(String name) {
        // 调用父类的有参构造器
        super(name);
    }
    // 另一个构造器
    public MiniCar() {
        // 调用同一个类的另一个构造器
        this("这里子类汽车的名称");
    }
}

public class TestThis {
    public static void main(String[] args) {
        MiniCar mc1 = new MiniCar();
        MiniCar mc2 = new MiniCar("动态的名字");
    }
}

对象、变量的生命周期是怎样的?

对象的生命周期决定于对象引用变量的生命周期,如果引用还在,则对象也在;如果引用死了,对象会跟着被 GC 回收。当最后一个引用消失时,对象就会变成可回收的。
局部变量,只存活在对象的方法中,方法结束,局部变量就死了。
实例变量,存活在对象中,它的生命周期与对象一致。

Life 和 Scope 有什么区别?

Life,只要变量的推栈块还存在于堆栈上,局部变量就算是活着。局部变量会活到方法执行完毕为止。
Scope,局部变量的作用范围只限于它所在的方法中。当该方法调用别的方法时,该局部变量还活着,但不在目前的范围内,当被调用的其它方法执行完毕后,该总局变量的范围又跟着回来了。

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

class Apple {}

public class LifeTest {
    void go() {
        // 1 - 当 go 方法调用结束时,销毁对象
        Apple a = new Apple();
    }
    public static void main(String[] args) {
        Apple a1 = new Apple();
        // 2 - 引用被重新赋值时,销毁旧对象 
        a1 = new Apple();
        // 3 - 把引用赋值为null时,销毁对象
        a1 = null;
    }
}

什么是 null ?

当你把引用变量赋值为 null 时,你就等于抹除了遥控器的功能。对 null 对象使用圆点调用方法,会报空指针异常 NullPointerException。


第10章:数字与静态性

Math 有什么特点?

在 Java 中没有东西是全局(global)的。但,Math 方法是接近全局的方法。Math不能用来创建实例变量。因为Math是用来执行数据计算的,所以没有必要创建对象来进行数学计算,创建对象是浪费内存空间的做法。Math中所有方法都静态方法。

Long a = Math.round(46.25);
int b = Math.max(2, 3);
int c = Math.abs(-500);

非静态方法与静态方法,有哪些差别?

  1. 静态方法,使用 static 关键字声明,以类的名称进行调用。
  2. 非静态方法,以实例对象进行调用,没有 static修饰。

Math类是如何阻止被实例化的?

Math类阻止被实例化,采取的策略是使用 private 修饰了其构造函数。

在静态方法中,为什么不能使用非静态的变量?为什么也不能调用非静态的方法?

静态变量有哪些特点?静态变量与非静态变量有哪些区别?

静态变量,会被同类的所有实例共享,因为它隶属于类。静态变量,在类第一次载入时初始化。静态变量,会在所有对象创建之前进行初始化,也会在任何静态方法执行之前就初始化。静态变量,只能由类来调用。
非静态变量,只被单个对象独有,它隶属于实例。非静态变量,在类实例化时进行初始化。
实例对象不会维护静态变量的拷贝,静态变量由类进行维护。非静态变量由实例对象进行维护。

class Duck {
    // 非静态变量,属于对象
    private int size = 0;
    // 静态变量,属于类
    private static int count = 0;
    
    public Duck() {
        size++;
        count++;
        System.out.println("size " + size);
        System.out.println("count " + count);
    }
    public void setSize(int s) {
        size = s;
    }
    public int getSize() {
        return size;
    }
}

public class TestStatic {
    public static void main(String[] args) {
        Duck d1 = new Duck();  // size = 1     count = 1
        Duck d2 = new Duck();  // size = 1     count = 2
        Duck d3 = new Duck();  // size = 1     count = 3
    }
}

如何声明一个静态常量?

public static final double PI = 3.1415926;

public 表示可供各方读取。 static 表示静态。 final 表示“固定不变”。 常量的标识符,一般建议字母大写,字母之间可以用下划线连接。

深入理解 final

final 的核心意思是,它所修饰的元素不能被改变。final 不仅可以修饰变量,还可以修饰方法和类。

// 用 final 修饰的类,不能被继承
final class Foo {
    // final 修饰静态变量,得到静态常量
    public static final double PI = 3.1415926;
    // final 修饰非静态变量,该变量将无法再被修改
    final String name = "geekxia";
    void changeName() {
        // 修改 final变量,失败
        // name = "Evatsai";
    }
    // final 修饰局部变量,该局部变量也将无法再被修改
    void doFoo(final int x) {
        // 修改局部变量,失败
        // x = 100;
    }
    // final 修饰的方法,子类将不能覆写
    final String getName() {
        return name;
    }
}

什么是主数据类型的包装类?

Boolean / Character / Byte / Short / Integer / Long / Float / Double
主数据类型的包装类,都放在 java.lang 中,所以无需 import 它们。当你需要以对象的方式来处理主数据类型时,你就需要用包装类把它们包装起来,Java5.0之前必须这么做。

int a = 123;
// 包装
Integer aWrap = new Integer(a);
// 解包
int b = aWrap.intValue();

Java5.0以后,autoboxing 使得主数据类型和包装类型不必分得那么清楚。autoboxing 的功能能够自动地将主数据类型和包装类型进行转换。看看下面的例子:

ArrayList list = new ArrayList();
// 自动地把主数据类型和包装类型进行转换
list.add(3);
int c = list.get(0);
        
Boolean bool = new Boolean(null);
if (bool) {}
        
Integer d = new Integer(3);
d++;
Integer e = d + 3;

void takeNumber(Integer i) {}
Integer getNumber() { return 4; }

主数据类型与字符串之间,如何进行相互转化?

// 把字符串转化成主数据类型
String a = "12";
int b = Integer.parseInt(a);        
double c = Double.parseDouble("50.789");        
boolean d = new Boolean("true").booleanValue();
        
// 把主数据类型转化成字符串
double e = 34.789;
String f = "" + e;
String g = Double.toString(e);

如何对数字进行格式化?

// 第二个参数,以第一个参数的格式化指令进行输出
String s = String.format("%, d", 1000000000);
String m = String.format("I have %,.2f bugs to fix.", 489369.123456);

如何对日期进行格式化?

String d = String.format("%tB", new Date());

更多有关“格式化指令”的参考,请查看相关手册。

使用 Calendar 抽象类操作日期

// 获取日历的实例对象
Calendar cal = Calendar.getInstance();
cal.set(2014, 1, 7, 15, 40);
        
long day1 = cal.getTimeInMillis();
day1 += 1000*60*60;
        
cal.setTimeInMillis(day1);
cal.add(cal.DATE, 35);


第11章:异常处理

如果你把有风险的程序代码包含在 try/catch 块中,那么编译器会放心很多。 try/catch 块会告诉编译器你确实知道所调用的方法会有风险,并且也已经准备好要处理它。

try {
    // 有风险的行为
    Sequencer seq = MidiSystem.getSequencer();
    System.out.println("had got a sequencer");
} catch (MidiUnavailableException ex) {
    System.out.println(ex);
} finally {
    System.out.println("总会被执行!");
}

异常也是多态的,Exception子类的实例对象,都是 Exception类型的。编译器会忽略 RuntimeException类型的异常,RuntimeException类型的异常不需要声明或被包含在 try/catch 块中。

如果 try 或 catch 块中有 return 指令,finally还是会执行。流程会跳到 finally执行,finally执行完毕后再回跳到return 指令。

如何处理多重异常?

class Laundry {
    public void doLaundry() throws PantsException, LingerieException {}
}

public class HandleException {
    public void go() {
        Laundry lau = new Laundry();
        try {
            lau.doLaundry();
        } catch (PantsException e) {
            System.out.print(e);
        } catch (LingerieException e) {
            System.out.print(e);
        } finally {
        }
    }
}

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

有多重异常需要捕获时,异常要根据继承关系从小到大排列,如果更高一级的异常排在了前面,将会导致低一级的异常将没有机会被使用。同级别的异常,排列顺序无所谓。

异常从哪里来?

异常是由方法 throws 来的。

public void doLaundry() throws PantsException, LingerieException {}

如果你调用了一个有风险的方法,但你又不想 try/catch捕获时怎么办?你可以继续使用 throws 关键字,把异常抛给下一个调用我的人,这好比是在踢皮球哈哈。

从上面看,程序有两种方式来处理异常,一种是使用 try/catch 来捕获异常,另一种是使用 throws 把异常抛给下一个调用者。


第12章:图形用户接口

什么是 JFrame ?

JFrame 是个代表屏幕上的window对象。你可以把 button / text / checkbox 等 widget 接口放在 window上面。

如何编写第一个 GUI 程序?

public class TestGui implements ActionListener {
    JButton btn;
    void init() {
        // 创建 frame
        JFrame frame = new JFrame();
        // 当window关闭时,把程序结束掉
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // 创建 widget
        btn = new JButton("click me");
        // 给btn 注册事件
        btn.addActionListener(this);
        // 把 btn添加到 frame上
        frame.getContentPane().add(btn);
        // 显示
        frame.setSize(300, 300);
        frame.setVisible(true);
    }
    // 覆写 ActionListener接口的中的方法
    @Override
    public void actionPerformed(ActionEvent event) {
        btn.setText("I've been clicked.");      
    }
    public static void main(String[] args) {
        TestGui tg = new TestGui();
        tg.init();
    }
}

图形界面中,事件三要素:事件源、监听、事件对象。其中,事件对象携带着事件的详细信息。

  1. 在 frame上放置 widget。javax.swing这个包中有很多 widget组件可以用。
  2. 在 frame上绘制 2D图形。使用 graphics 对象来绘制 2D 图形。
  3. 在 widget上绘制 jpeg图片。 graphics.drawImage(img, 10, 10, this);

如何自定义 widget 组件?

继承 JPanel ,并覆写 paintComponent()这个方法。

public class CustomComponent extends JPanel {
    // 覆写方法
    public void paintComponent(Graphics g) {
        // 绘制矩形
        g.setColor(Color.orange);
        g.fillRect(20,  50,  100, 100);
        
        // 绘制图像
        Image img = new ImageIcon("./car.png").getImage();
        System.out.println(img);
        g.drawImage(img, 3, 4, this);
        
        // 绘制矩形
        g.fillRect(0, 0, this.getWidth(), this.getHeight());
        int red = (int)(Math.random()*255);
        int blue = (int)(Math.random()*255);
        int green = (int)(Math.random()*255);
        // 创建随机色
        Color color = new Color(red, green, blue);
        g.setColor(color);
        // 绘制覆盖物
        g.fillOval(70, 70, 100, 100);
    }
}
// 使用自定义的组件:
public class TestGui {
    void init() {
        JFrame frame = new JFrame();   // 创建 frame
        CustomComponent cc = new CustomComponent();
        frame.getContentPane().add(cc);
        frame.setSize(300, 300);
        frame.setVisible(true);
    }
}

什么是内部类?如何创建内部类的实例?

一个类可以嵌套在另一个类的内部,这便构成了内部类。内部类可以访问外部类中的所有成员,包括外部类的成员变量和成员方法,即使它们是私有的。

public class OuterClass {
    private int x;
    
    InnerClass ic = new InnerClass();  // 创建内部类实例 
    public void foo() {
        System.out.println("outer class -> 1: " + x);
        ic.go();
        System.out.println("outer class -> 3: " + x);
    }
    // 内部类
    class InnerClass {
        void go() {
            x = 42;  // 访问外部类的成员变量 x
            System.out.println("inner class -> 2 : "+x);
        }
    }
    
    public static void main(String[] args) {
        OuterClass oc = new OuterClass();
        oc.foo();
        // 另一种 创建内部类实例的方式
        OuterClass.InnerClass ocic = oc.new InnerClass();
        ocic.go();
    }
}

内部类,有什么用? (试着写一个GUI动画,玩一下!)

public class SimpleAnimation {
    int x = 70;
    int y = 70;
    
    public void go() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // 创建内部类实例
        MyDrawPanel dp = new MyDrawPanel();
        frame.getContentPane().add(dp);
        frame.setSize(300,300);
        frame.setVisible(true);
        // 动画
        for (int i=0; i<120; i++) {
            x++;
            y++;
            dp.repaint();
            try {
                Thread.sleep(50);  // 间隔50毫秒
            }catch (Exception ex) {
                System.out.println(ex);
            }
        }
    }
    
    // 内部类
    class MyDrawPanel extends JPanel {
        public void paintComponent(Graphics g) {
            // 重置画布
            g.setColor(Color.white);
            g.fillRect(0, 0, this.getWidth(), this.getHeight());
            // 重绘圆形
            g.setColor(Color.red);
            g.fillOval(x, y, 40, 40);
        }
    }
    
    public static void main(String[] args) {
        SimpleAnimation sa = new SimpleAnimation();
        sa.go();
    }
}


第13章:Swing

什么是 Swing 组件?

组件(Component),也称作元件。它们就是那些放在界面上与用户进行交互的东西,如 Button / List 等。事实上,这些 GUI 组件,都来自于 java.swing.JComponent。 在 Swing 中,几乎所有组件都可以嵌套,即一个组件可以安置在另一个组件之上。

创建 GUI 的四个步骤再回顾?

// 第1步:创建windon (JFrame)
JFrame frame = new JFrame();
// 第2步:创建组件
JButton btn = new JButton("click me");
// 第3步:把组件添加到 frame 上
frame.getContentPane().add(BorderLayout. EAST, btn);
// 第4步:显示 GUI 界面
frame.setSize(300, 300);
frame.setVisible(true);

什么是布局管理器?

布局管理器(Layout Managers)是个与特定组件关联的Java对象,它大多数是背景组件。布局管理器,负责组件的大小和位置。

布局管理器,是如何工作的?
有哪三大首席布局管理器?(BorderLayout / FlowLayout / BoxLayout)
BorderLayout布局有哪5个区域?(东区 / 西区 / 北区 / 南区 / 中央区)
FlowLayout布局的组件流向是怎样的?(从左至右,从上至下)
BoxLayout布局能解决什么问题?
如何创建Swing组件?如何操作组件?

Swing实例:JTextArea文本框

public class TestSwing implements ActionListener {  
    JTextArea text;

    // 实现 ActionListener 接口的方法
    public void actionPerformed(ActionEvent arg0) {
        text.append("button clicked \n");       
    }
    public void go() {
        JFrame frame = new JFrame();
        JPanel panel = new JPanel();
        JButton button = new JButton("Just Click It");
        // 给 button 注册点击事件
        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);
        panel.add(scroller);
        frame.getContentPane().add(BorderLayout.CENTER, panel);
        frame.getContentPane().add(BorderLayout.SOUTH, button);
        frame.setSize(350, 300);
        frame.setVisible(true);
    }
    public static void main(String[] args) {
        TestSwing gui = new TestSwing();
        gui.go();
    }
}


第14章:序列化与文件的输入输出

对象可以被序列化,也可以展开。对象有状态和行为两种属性,行为存在于类中,而状态存在于个别的对象中。本章将讨论以下两种选项:

  1. 如果只有自己写的Java程序会用到这些数据。用序列化(Serialization),将被序列化的对象写到文件中。然后就可以让你的程序去文件中读取序列化的对象,并把它们展开回到活生生的状态。
  2. 如果数据需要被其它程序引用。写一个纯文本文件,用其它程序可以解析的特殊字符写到文件中。

如何把序列化对象写入文件?

public class TestOutputStream {
    public static void main(String[] args) {
        try {
            // 如果文件不存在,就自动创建该文件
            FileOutputStream fileStream = new FileOutputStream("mygame.ser");
            // 创建存取文件的 os 对象
            ObjectOutputStream os = new ObjectOutputStream(fileStream);
            // 把序列化对象写入文件
            os.writeObject(new Dog());   // Dog必须是可序列化的类
            os.writeObject(new Dog());
            // 关闭所关联的输出串流
            os.close();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

什么是串流?

将串流( stream )连接起来代表来源与目的地(文件或网络端口)的连接。串流必须要连接到某处才能算是个串流。Java的输入输出API带有连接类型的串流,它代表来源与目的地之间的连接,连接串流即把串流与其它串流连接起来。

当对象被序列化时,被该对象引用的实例变量也会被序列化。且所有被引用的对象也会被序列化。最重要的是,这些操作都是自动完成的。

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

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

// Serializable没有方法需要实现,它只是用来告诉Java虚拟机这个类可以被序列化
public class Box implements Serializable {
    public Box() {}
}

注意:序列化是全有或全无的,即对象序列化时不存在“一部分序列化成功、另一部分序列化失败”的情况,如果对象有一部分序列化失败,则整个序列化过程就是失败的。只有可序列化的对象才能被写入到串流中。

那么在一个可序列化的类中,如何指定部分实例变量不执行序列化呢?

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

// Serializable没有方法需要实现,它只是用来告诉Java虚拟机这个类可以被序列化
public class Box implements Serializable {
    public Box() {}
    
    // 该id变量,就不会被序列化
    transient String id;
    String username;
}

如何从文件中读取序列化对象,并将其还原?

把对象恢复到存储时的状态。解序列化,可看成是序列化的反向操作。

public class TestInputStream {
    public static void main(String[] args) {
        try {
            // 打开文件流,如果文件不存在就会报错
            FileInputStream fileStream = new FileInputStream("mygame.ser");
            // 创建 输入流
            ObjectInputStream os = new ObjectInputStream(fileStream);
            // 读取 序列化对象 
            Object one = os.readObject();
            Object two = os.readObject();
            // 类型转换,还原对象类型
            Dog d1 = (Dog)one;
            Dog d2 = (Dog)two;
            // 关注 输入流
            os.close();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

序列化(存储)和解序列化(恢复)的过程,到底到生了什么事?

你可以通过序列化来存储对象的状态,使用ObjectOutputStream来序列化对象。Stream是连接串流或是链接用的串流,连接串流用来表示源或目的地、文件、网络套接字连接。链接用串流用来链接连接串流。使用FileOutputStream将对象序列化到文件上。静态变量不会被序列化,因为所有对象都是共享同一份静态变量值。
对象必须实现Serializable 这个接口,才能被序列化。如果父类实现了它,则子类就自动地有实现。
解序列化时,所有的类都必须能让Java虚拟机找到。读取对象的顺序必须与写入时的顺序一致。

如何把字符串写入文件文件?

try {
  // FileWriter
  FileWriter writer = new FileWriter("foo.txt");
  writer.write("hello foo!");
  writer.close();
} catch (IOException e) {
  System.out.print(e);
}

java.io.File

File对象代表磁盘上的文件或者目录的路径名称,如 /Users/Kathy/Data/game.txt 。但它并不能读取或代表文件中的数据。使用 File对象,可以做以下事情:

// 创建 File对象
File f = new File("game.txt");
// 创建新的目录
File f1 = new File("data");
f1.mkdir();
// 列举目录下的内容
f1.list();
// 获取文件或目录的绝对路径
f1.getAbsolutePath();
// 删除文件或目录
f1.delete();

什么是缓冲区?为什么使用缓冲区会提升数据读写的效率?

没有缓冲区,就好像逛超市没有推车一样,你只能一次拿一项商品去结账。缓冲区能让你暂时地摆一堆东西,直到装满为止。用了缓冲区,就可以省下好几趟的来回。
缓冲区的奥妙之处在于,使用缓冲区比没有使用缓冲区的效率更好。通过 BufferedWriter 和 FileWriter 的链接,BufferedWriter 可以暂存一堆数据,等到满的时候再实际写入磁盘,这样就可以减少对磁盘的操作次数。如果想要强制缓冲区立即写入,只要调用 writer.flush() 这个方法即可。

如何从文本文件中读取数据?

File对象表示文件,FileReader用于执行实际的数据读取,BufferedReader让读取更有效率。读取数据,使用 while 循环来逐行读取,直到 readLine() 的结果为 null 为止。这是最常见的数据读取方式(几乎所有的非序列化对象都是这样的)。

public class TestFileReader {
    public static void main(String[] args) {
        try {
            File file = new File("foo.txt");
            // 连接到文件文件的串流
            FileReader fr = new FileReader(file);
            // 使用 Buffered 缓冲区
            BufferedReader reader = new BufferedReader(fr);
            String line = null;
            while((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            // 关闭流
            reader.close();
        } catch (Exception ex) {
            System.out.println(ex);
        }
    }
}

什么是 serialVersionUID?为什么要使用 serialVersionUID?

每当对象被序列化的同时,该对象(以及所有在其版图上的对象)都会被“盖”上一个类的版本识别ID,这个ID就被称为 serialVersionUID ,它是根据类的结构信息计算出来的。在对象被解序列化时,如果在对象被序列化之后类有了不同的 serialVersionUID,则解序列化会失败。虽然会失败,但你还可以有控制权。
如果你认为类有可能会深化,就把版本识别ID(serialVersionUID)放在类中。当Java尝试解序列化还原对象时,它会对比对象与Java虚拟机上的类的serialVersionUID 是否相同。如果相同,则还原成功;否则,还原将失败,Java虚拟机就会抛出异常。因此,把 serialVersionUID 放在类中,让类在演化过程中保持 serialVersionUID 不变。

public class Dog {
  // 类的版本识别ID
  private static final long serialVersionUID = -54662325652236L;
}

若想知道某个类的 serialVersionUID 是多少?则可以使用 Java Development Kit 里的 serialver 工具进行查询。

serialver Dog
Dog: static final long serialVersionUID = -54662325652236L;


第15章:网络与线程(网络联机)

在 Java中,所有网络运作的低层细节都已经由 java.net 函数库处理。Java中,传送和接收网络上的数据,是在链接上使用不同链接串流的输入和输出。

什么是 Socket 连接?

Socket 是个代表两台机器之间网络连接的对象(java.net.Socket)。什么是连接?就是两台机器之间的一种关系,让两个软件相互认识对方,能够彼此发送和接收数据。在Java中,这是一种让运行在Java虚拟机上的程序能够找到方法去通过实际的硬件(比如网卡)在机器之间传送数据的机制。

如何创建一个 Socket 连接呢?

要创建 Socket,得知道两项关于服务器的信息:它在哪里(IP地址)?它使用的是哪个端口号?

Socket chat = new Socket("196.164.1.103", 3000);

什么是 TCP 端口号?

TCP 端口号,是一个 16位宽、用来识别服务器上特定程序的数字。端口号,代表了服务器上执行软件的逻辑识别。如果没有端口号,服务器就无法分辨客户端是要连接哪个应用程序的服务。每个应用程序可能都有独特的工作交谈方式,如果没有识别就发送的话会造成很大的麻烦,所以在同一台机器上不同的应用程序服务其端口号一定不同。
从 0 ~ 1023 的TCP 端口号是保留已知的特定服务使用,我们不应该使用这些端口号。当我们开发自己的应用程序服务时,可以从 1024 ~ 65535 之间选择一个数字作为端口号。

如何从 Socket 上读取数据?

用串流来通过 Socket 连接来沟通。使用 BufferedReader 从 Socket上读取数据。在Java中,大部分的输入与输出工作并不在乎链接串流的上游是什么,也就是说可以使用BufferedReader读取数据而不用管串流是来自文件还是Socket。

// 1 - 建立对服务器的 Socket 连接
Socket chat = new Socket("192.168.1.100", 3000);
// 2 - 建立连接到Socket上低层的输入串流
InputStreamReader stream = new InputStreamReader(chat.getInputStream());
// 3 - 建立Buffer来提升数据读取的效率
BufferedReader reader = new BufferedReader(stream);
// 4 - 读取数据
String msg = reader.readLine();
read.close();

如何向 Socket 中写入数据?

使用 PrintWriter 是最标准的做法。它的 print() / println() 方法,就跟 System.out 里的方法刚好一样。

// 1 - 对服务器建立 Socket 连接
Socket chat = new Socket("192.168.1.100", 3000);
// 2 - 建立连接到 Socket 的 PrintWriter
PrintWriter writer = new PrintWriter(chat.getOutputStream());
// 3 - 写入数据
writer.println("hello world");
writer.print("hello socket");

如何编写一个 Socket 服务器?

public class TestSocket {
    public void go() {
        try {
            // 创建 服务端Socket,并监听 3000端口
            ServerSocket serverSocket = new ServerSocket(3000);
            // 服务器进入无穷循环,等待客户端的请求
            while(true) {
                // 等待连接
                Socket sock = serverSocket.accept();
                // 当有连接时,向 socket中写入数据
                PrintWriter writer = new PrintWriter(sock.getOutputStream());
                writer.println("hello world");
                writer.close();
            }
        } catch (IOException e) {
            System.out.println(e);
        }
    }
    public static void main(String[] args) {
        TestSocket ts = new TestSocket();
        // 启动 socket 服务
        ts.go();
    }
}

什么是 Java 线程?

  1. 一个Java线程(thread),就是一个独立的执行空间(stack)。Java语言中内置了多线程功能,线程的任务就是运行程序代码。
  2. java.lang.Thread类,表示Java中的线程,使用它可以创建线程实例。
  3. 每个Java应用程序都会启动一个主线程——把 main() 方法放进它自己执行空间的最开始处。Java虚拟机会负责主线程的启动。

怎么理解多线程的工作原理?

当有超过一个以上的执行空间时,看起来像是有好几件事情同时在发生。实际上,只有真正的多核处理器系统才能够同时做好几件事情。使用Java多线程可以让它看起来好像同时在做多个事情。也就是说,执行运作可以在执行空间中非常快速地来回切换,因此你会感觉到每项任务都在执行。线程要记录的一项事物是目前线程执行空间做到了哪里。Java虚拟机,会在多个线程之间来回切换,直到它们都执行完为止。

如何创建一个新的线程?

对一个线程而言,Thread线程就是一个工人,而Runnable就是这个工作的工作。Runnable带有会放在执行空间的第一项的方法:run() 。Thread对象不可以重复使用,一旦该线程的run()方法完成后,该线程就不能再重新启动了。

public class TestThread implements Runnable {
    // 实现 Runnable 接口的 run 方法
    public void run() {
        System.out.println("执行");
    }
    public static void main(String[] args) {
        // 创建线程的工作任务
        Runnable threadObj = new TestThread();
        // 创建线程,并把工作任务传递给线程
        Thread myThread = new Thread(threadObj);
        // 启动线程
        myThread.start();
    }
}

线程有哪三种状态?

线程实例一旦 start() 后,会有三种可能的状态:可执行状态、执行中状态、堵塞状态。在不发生堵塞的情况下,线程会在可执行状态和执行中状态之间来来回回地切换。

什么是线程调度器?

线程调度器会决定哪个线程从等待状态中被挑出来运行,以及何时把哪个线程送回等待被执行状态。它会决定某个线程要运行多久,当线程被踢出去时,调度器也会指定线程要回去等待下一个机会或者是暂时地堵塞。我们无法控制线程调度器,没有API可以调用调度器,调度器在不同的Java虚拟机上也不尽相同。
线程调度器会做所有的决定,谁跑谁停都由它说了算。它通常是公平的,但没有人能保证这件事儿,有时候某些线程会很受宠,有些线程会被冷落。多个线程任务,谁先执行完谁后执行完,并不能人为地确定。

线程休眠 Thread.sleep()

如果想要确定其它线程有机会执行的话,就把线程放进休眠状态。当线程醒来的时候,它会进入可以执行状态等待被调度器挑出来执行。使用 sleep() 能让程序变得更加可预测。

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

给线程取个非默认的名称

Thread thread = new Thread(runnableOjb);
// 自定义线程的名字
thread.setName("a name");
// 获取当前在执行线程的名字
String name = Thread.currentThread().getName();

线程有什么缺点吗?

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

如何解决线程并发性引起的竞争状态问题呢?

这就需要一把 getter/setter 的锁。比如当多个执行空间上有多个方法同时执行同一个银行账户的交易任务时:当没有交易时,这个锁是开启的;当有交易任务时,这个锁会被锁住;从而保证了在同一时刻有且仅有一个方法能执行账户的交易任务。从而有效地避免了并发性的竞争状态问题。
使用 synchronized 关键字来修饰线程任务中的方法,它代表线程需要一把钥匙来存取被同步化过的线程。要保护数据,就把作用在数据上的方法同步化。
每个Java对象都有一个锁,每个锁只有一把钥匙。通常对象都没上锁,也没有人在乎这件事儿。但如果对象有同步化的方法,则线程只能在取得锁匙的情况下进入线程。也就是说,并没有其它线程进入的情况下才能进入。
用同步机制,让操作同一个数据对象的多个方法原子化。一旦线程进入了方法,我们必须保证在其它线程可以进入该方法之前所有的步骤都会完成(如同原子不可分割一样)。

// 使用 synchronized 关键字,让方法原子化!
public synchronized void increment() {
  int i = balance;
  balance = i + 1;
}

注意:虽然 synchronized同步化可以解决多线程并发性引起的竞争状态问题,但并不是说让所有方法都同步化。原则上,程序中让同步化的方法越少越好。

什么是同步化的死锁问题?

同步化死锁会发生,是因为两个线程相互持有对方正在等待的东西。没有方法可以脱离这个情况,所以两个线程只好停下来等,一直等。在Java中,并没有处理死锁的内置机制,所以编写程序时要注意这一问题,避免发生死锁。


第16章:集合与泛型(数据结构)

有哪些常用的集合?

  1. ArrayList
  2. TreeSet 以有序状态保存并可防止数据重复
  3. HashMap 以键值对的形式保存数据
  4. LinkedList 针对经常插入或删除中间元素所设计的高效率集合
  5. HashSet 防止重复的集合,可快速地寻找相符的元素
  6. LinkedHashMap

什么是泛型?为什么使用泛型?

在Java中,看到 <> 这一组尖括号,就代表泛型,这是从Java5.0开始加入的特性。
几乎所有你会以泛型写的程序都与处理集合有关。虽然泛型还可以用到其它地方,但它主要的目的还是让你能够写出有类型安全性的集合。也就是说,让编译器能够帮忙防止你把 Dog加入到一群 Cat中。

关于泛型,最重要的 3 件事

  1. 如何创建被泛型化类的实例?
  2. 如何声明指定泛型类型的变量?多态遇到泛型类型会怎样?
  3. 如何声明或调用泛型类型的方法?
// 创建被泛型化的类
List songList = new ArrayList();
// 声明泛型类型的方法
public void foo(List list) {}
// 调用泛型类型的方法
x.foo(songList);

集合有哪 3 类主要的接口?

  1. List,是一种知道索引位置的集合,这是对待顺序的好帮手。
  2. Set,不允许重复的集合,它注重独一无二的性质。
  3. Map,使用键值对存储数据的集合,用 key 来搜索。

对象要怎样才算相等? Set集合是如何检查元素是否重复的?

如果 foo 和 bar 两对象相等,则 foo.equals(bar) 会返回 true ,且二者的 hashCode() 也会返回相同的值。要让 Set 能把对象视为重复的,就必须让它们符合上面的对象相等条件。

  1. 引用相等性:堆上同一对象的两个引用。 foo == bar; // true
  2. 对象相等性:堆上两个不同的对象在意义上是相同的。 foo.equals(bar) && foo.hashCode() == bar.hashCode(); // true

特点注意: == / equals() / hashCode() 三者之间的异同。

TreeSet的元素必须是 Comparable 的

TreeSet无法猜到程序员的想法,你必须指出TreeSet中的元素该如何排序。方案有二,其一是集合的元素都必须是实现了 Comparable 的类型;其二使用重载,取用 Comparator 参数的构造函数来创建 TreeSet。代码示例如下:

// 实现了 Comparable 接口的类,可用于TreeSet的元素
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));
    }
}
public class TestTreeSet {
    public static void main(String[] args) {
        Book b1 = new Book("yiyi");
        Book b2 = new Book("titi");
        Book b3 = new Book("bibi");
        TreeSet ts = new TreeSet();
        ts.add(b1);
        ts.add(b2);
        ts.add(b3);
        System.out.print(ts);
    }
}
public class TestComparator implements Comparator {
    // 实现接口方法
    public int compare(Book b1, Book b2) {
        return (b1.title.compareTo(b2.title));
    }
    public static void main(String[] args) {
        TestComparator tc = new TestComparator();
        TreeSet tree = new TreeSet(tc);
        tree.add(new Book("zizi"));
        tree.add(new Book("cici"));
        tree.add(new Book("mimi"));
        tree.add(new Book("fifi"));
        System.out.print(tree);
    }
}

Map 数据结构的使用(键名不可重复)

public class TestMap {
    public static void main(String[] args) {
        HashMap scores = new HashMap();
        scores.put("Kathy", 40);
        scores.put("Bert", 50);
        System.out.println(scores);
    }
}

泛型与多态

如果方法的参数是 Animal数组,那么它就能够取用 Animal次类型的数组。也就是说,如果方法是这样声明的:void foo( Animal[] a ) {} 。 若Dog 有 extends Animal,你就可以用以下两种方式调用 foo方法:foo(anAnimalArray); foo(aDogArray) 。

public class TestType {
    public void go() {
        Animal[] animals = { new Dog(), new Cat() };
        Dog[] dogs = { new Dog(), new Dog() };
        // 多态
        takeAnimals(animals);
        takeAnimals(dogs);
    }
    public void takeAnimals(Animal[] animals) {
        // 多态参数: animals 可以是 Animal及其子类的对象数组
        for (Animal a: animals) {
            System.out.println(a);
        }
    }
}

什么是万用字符?有什么用?

使用万用字符,你可以操作集合元素,但不能新增集合元素。如此保证了程序执行期间的安全性。

public  void takeThing(ArrayList list) { }
// 等价于
public void takeThing(ArrayList list) { }


第17章:包、jar存档文件和部署(发布程序)

Java程序,是由一组类所组成的,这就是开发过程的输出。本章将讨论如何组织、包装和部署Java程序。

如何组织Java代码文件?

组织代码文件的方案有很多。建议一种几乎已经成为了标准的组织方案——在 source目录下放置 .java 源文件;在 classes 目录下放置 .class 编译结果。使用 -d 选项,可以指定编译结果的存储目录。

// -d 用于指定编译结果的存储目录
javac  -d  ../classes  MyApp.java

mainifest文件 与 JAR包

JAR即 Java ARchive。这种文件是个 pkzip 格式的文件,它能够让你把一组类文件包装起来,所以交付时只需要一个 JAR文件即可。它类似 Linux上的 tar命令。可执行的 JAR 代表用户不需要把文件抽出来就能运行,这个秘密就在于 mainifest文件,它携带着 JAR包的若干信息,并且能告诉Java虚拟机哪个类中含有 main() 方法。

如何创建 JAR 包?

cd  myproject/classes
jar  -cvmf  mainifest.txt  app.jar  *.class
或者
jar  -cvmf  mainifest.txt  app.jar  MyApp.class

如何执行 JAR包?

cd  myproject/classes
java  -jar  app.jar

大部分的Java应用,都是以可执行的JAR包来部署的。Java虚拟机能够从JAR包中载入类,并调用该类中的 main() 方法。事实上,整个应用程序都可以包在 JAR包中。一旦 main() 方法开始执行,Java虚拟机就不会在乎类是从哪里来的,只要能够找到就行。其中一个来源就是 classpath 指定位置的所有 JAR文件。
Java虚拟机必须要能找到JAR,所以它必须在 classpath 下。让 JAR曝光的最好方式就是把 JAR放置在工作目录下。Java虚拟机会检查 JAR包中的 manifest文件以寻找入口,如果没有找到就会发生运行期间异常。
根据操作系统的设置,双击 JAR文件也可以直接运行 JAR文件。

如何防止类名、包名的命名冲突?

  1. 用包防止类名的命名冲突。
  2. 用 domain 名称防止包名的命名冲突。
./source/cn/geekxia/*.java
./classes/cn/geekxia/*.class

如何列举 JAR包中的文件?

jar  -tf  app.jar

如何解压 .jar包?

jar  -xf  app.jar

关于 Java Web Start 技术的一些知识点

Java Web Start技术让你能够从网站来部署独立的客户端程序。Java Web Start有个必须要安装在客户端的helper app。Java Web Start程序由两个部分组成:可执行的JAR包和.jnlp文件。 .jnlp文件是用来描述Java Web Start应用程序的XML文件。它有tag以指定JAR的名称和位置,以及带有 main()的类名称。当浏览器从服务器上取得 .jnlp文件时,浏览器就会启动 Java Web Start的 helper app。Java Web Start会读取 .jnlp文件来判断要从服务器上下载的可执行JAR包。取得JAR包之后,它就会调用 .jnlp指定的 main()。


第18章:远程部署 RMI(分布式计算)

什么是 RMI ?

即 Remote Method Invocation,远程方法调用技术。

截止目前,我们学习到的Java方法调用,都是发生在相同堆上的两个对象之间。即对象的方法调用是发生在同一个Java虚拟机上面的。那么,如果我们要调用另一台Java机器上的对象方法,该如何实现呢?这就需要用到 RMI 远程方法调用技术

如何设计一个 RMI 远程过程调用?

四要素:服务器、客户端、服务器辅助设施 和 客户端辅助设施。(客户端对象看起来像是在调用远程机器上的对象方法,但实际上它只是在调用本地处理Socket和串流细节的客户端辅助设施这一“代理 proxy”)。
这四要素的关系如下:1、客户端对象以为它是在与真正的服务器沟通,它以为客户端辅助设施就是真正的服务器; 2、客户端辅助设施伪装成服务器,但实际上它只是真正服务器的代理人而已; 3、服务端辅助设施收到客户端辅助设施的请求后,解开包装,调用真正的服务器对象; 4、在服务器上,才会真正地执行这个对象方法。

使用 Java RMI

在Java中,RMI 已经帮我们创建好了客户端和服务端的辅助设施。使用 RMI ,我们无需编写任何网络或输入/输出的程序,客户端在对远程方法的调用时就跟调用同一台Java虚拟机上的方法是一样的。
在 RMI 中,客户端辅助设施被称为 RMI stub,而服务端辅助设施被称为 RMI skeleton。代码示例如下:

创建 Remote 接口:

public interface MyRemote extends Remote {
    public String sayHello() throws RemoteException;
}

实现 Remote Service:

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    public MyRemoteImpl() throws RemoteException {}
    public String sayHello() {
        return "Server says, Hi ";
    }
    public static void main(String[] args) {
        try {
            MyRemote service = new MyRemoteImpl();
            Naming.rebind("Remote Hello", service);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

完整的客户端程序代码:

public class MyRemoteClient {
    public void go() {
        try {
            MyRemote service = (MyRemote)Naming.lookup("rmi://127.0.0.1/Remote Hello");
            String s = service.sayHello();
            System.out.print(s);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

什么是 Servlet ?

Servlet是放在HTTP Web服务器上面运行的 Java程序。当用户通过浏览器和网页进行交互时,请求会被发送至网页服务器。如果请求需要Java的 Servlet时,服务器会执行或调用已经执行的 Servlet程序代码。Servlet只是在服务器上运行的程序代码,执行并响应用户发出请求所要的结果。

public class MyServlet extends HttpServlet {
  public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
    res.setContentType("text/html");
    // ...
  }
}


附录:最后十大知识点

枚举 Enum 、多维数组、String 和 StringBuffer / StringBuilder 、存取权限和访问修饰符、Anonymous 和 Static Nested Classes 、链接的调用、块区域、断言、不变性、位操作。



本书 完 2018-08-06!!!

你可能感兴趣的:(Head First Java 读书笔记(完整))