读书笔记:Head First Java实战(第三版)

第一章:浮出水面

编译、运行 Java 程序的流程

编写 Java 源文件(.java 后缀),编译产物为 Java 字节码文件(平台无关)。在 Java 虚拟机 JVM 中运行这个字节码文件。

编辑:vim HelloWorld.java
编译:javac HelloWorld.java (编译产物:HelloWorld.class
运行:java HelloWorld

JRE 和 JVM 的关系?
JVM 是 JRE 的一部分

Java 简史

1996 年首次发布,编程时会遇到很多古老代码,Java 向后兼容。

向前兼容、向后兼容?
向前兼容(Forward Compatibility):新版本兼容老版本,forward 取『未来』的含义
向后兼容(Backward Compatibility):老版本兼容新版本
向上兼容(Upward Compatibility):与向前兼容相同
向下兼容(Downward Compatibility):与向后兼容相同

Java速度和内存使用

Java 速度与 C 和 Rust 不相上下,JVM 可以在运行时优化代码,无需刻意写高性能代码。
与 C 和 Rust 相比,Java 要使用大量内存。

Java混乱的版本

混乱体现在:JDK1.0、1.2、1.3、1.4、J2SE5.0、Java6、Java7、Java18?
Java9 开始,版本号只有数字,没有开头的『1』,也就是说 Java9 就是版本 9,而不是 1.9.
本书中,版本1.0~1.4,仍用通用约定;从版本 5 开始,省略前缀『1.』。
从 2017 年发布 Java9 以来,每 6 个月发布一个 Java 版本,所以后来版本号推进很快。

程序运行的起始位置

JVM 开始运行时,会寻找命令行提供的类,在这个类中寻找 main 方法。

第二章:对象城之旅

面向对象的优点

面向过程的优点在于,简单明了,开门见山。但是当需求不断补充、拓展、变化时,需要不断修改逻辑代码,伤筋动骨。

面向对象并不是最简洁的,但它将数据和操作合并在一个模块中易于管理,容易构建大型程序。而且多态的特性使得代码易于维护、拓展。当加入新功能时,往往只需要新派生一个对象,而不需要修改逻辑代码。

如何使用全局变量

Java OO(面向对象)程序中,没有『全局』变量和方法的概念。但是可以把方法标记为 public 和 static,把变量标记为 public、static 和 final 来得到全局可用的方法、变量。

既然能用全局变量,怎么能算是面向对象呢

Java 纯面向对象,C++ 拥有面向过程、面向对象两种特性。

Java 一切都在类中,这种类似全局的方法和数据,仅仅是一种例外,而不是规则。

第三章:了解你的变量

Java 很在意类型

Java 对类型转换要求很严格,不允许隐式窄化,但允许隐式宽化。

基本类型

类型位数
boolean(JVM特定)
byte8
char16
short16
int32
float32
long64
double64

标识符/变量 命名规则

  • 首字母:字母、_、$
  • 非首字母:字母、_、$、数字
  • 不能和保留字重名

引用类型

Java 只有按值传递、按引用传递,无指针。可以说除了基本数据类型之外,其他的全是引用。
类类型、String 类型、数组,都是引用。

引用变量的大小

可以认为是 64 位,但具体多大取决于 JVM 的实现。同一个 JVM,所有引用的大小都相同,不同 JVM 引用大小就不一定了。

引用初始化

C++ 的引用必须初始化,一旦初始化后,不可以再引用别的变量。
Java 的引用可以不初始化(引用 null),引用 A 之后,可以再引用 B。

看起来就是指针?

堆、垃圾回收机制

对象在堆上分配空间,当没有引用指向某个对象时,这块空间可以被 JVM 垃圾回收机制回收利用。

智能指针?

数组类型

数组是对象,或者说数组名是引用类型。数组长度:array.length

第四章:对象的行为

按值传递

可以说 Java 中一切都是按值传递。按引用传递的本质,也是按值传递了一段二进制 01 码。

Setter封装的意义

可以验证参数,例如拒绝将某个值设为负数。也可以抛出异常,或者将参数调整为能接受的最近的值。

如果接受任何参数,Setter是否多余?

  1. 记不住哪些变量有 Setter,哪些可以直接修改(自己想的)
  2. 添加 Setter 使代码易拓展。例如某个版本,突然要对某个参数增加限制,有 Setter 直接重新实现一下,不用改其他代码。没有 Setter 的话,要把每一处直接调用的地方都改成通过 Setter 调用。

成员变量默认值

  • 局部变量没有初始值,使用未初始化的局部变量编译器报错
  • 成员变量有默认值(0,0.0,false,null)

equals 与 ==

  • == 操作符只比较二进制位
  • equals 取决于实现

第五章:强有力的方法

测试驱动开发(Test Driven Development,TDD)

按我的理解:先写测试模块,然后写能通过测试的最简单的代码。之后重构代码,每一版重构都要能通过所有的测试案例。不断迭代更新。

这一章挺水的,没啥可总结

第六章:使用Java库

ArrayList类

  • add(E e):将指定元素添加到列表末尾
  • remove(int index):删除指定位置上的元素
  • remove(Object o):删除指定元素的第一次出现
  • contains(Object o):如果列表包含指定元素,返回 true
  • indexOf(Object o):返回袁术的第一个索引或 -1
  • get(int index):返回指定位置的元素
  • isEmpty()
  • size()
	ArrayList<Egg> myList = new ArrayList<Egg>();
	Egg egg1 = new Egg();
	Egg egg2 = new Egg();
	myList.add(egg1);
	myList.add(egg2);
	int theSize = myList.size();
	boolean isIn = myList.contains(egg1);
	int idx = myList.indexOf(egg2);
	boolean empty = myList.isEmpty();
	myList.remove(egg1);

短路操作符(&&,||)

&&、|| 都可以短路,实际应用:

	if(refVal != null && !refVal.isEmpty()) {...}

null 引用调用成员方法,抛 NullPointerException 异常,上面的方法可以避免。

import 导入包

要使用 ArrayList,有两种方式:

第一种,每次用时输入全名(含路径)

java.util.ArrayList<Dog> list;

public void foo(java.util.ArrayList<Dog> list) { }

public java.util.ArrayList<Dog> foo() { }

第二种方法,源代码文件最上面加入 import 语句

import java.util.ArrayList;

import 是否会导致代码膨胀

import 不同于 C 中的 include,import 仅仅告诉编译器寻找包的路径。

为什么不导入String、System

System、String、Math 都属于 java.lang 包,java.lang 包类似于『预导入』包。

第七章:对象城的美丽生活

子类中使用一个方法的超类版本

在子类覆盖方法中,使用 super 关键字

public void roam(){
	super.roam();
	//...
}

阻止继承的方法

  • default 访问控制级别。这个级别的类只能被同一包中的其他类继承,不同包中的类不能继承它(甚至不能使用)
  • final 修饰的类无法被继承
  • 一个类只有私有构造器,那么它也不能派生子类

覆盖(overRide)规则

  • 参数必须相同,返回类型必须兼容
    返回类型兼容,例如父类方法返回Object,子类可以返回其他类型。
  • 访问级别不能更受限

重载方法

重载和多态没有任何关系,当函数名相同,参数列表不同时,就是重载。重载的两个方法的返回类型可以不同,但不能只改变返回类型(二义性)。重载的多个方法,对访问级别没有要求。

『隐藏』呢

多态

Java 中没有虚函数,默认都是虚函数。(特殊方法除外:静态方法、私有方法、final方法、父类方法等)

第八章:真正的多态

抽象类、抽象方法

使用 abstract 关键字,将一个类置为抽象类,将一个方法置为抽象方法:

// 抽象类
abstract class Dog extends Animal { }
// 抽象方法
public abstract void eat();

抽象类不允许被实例化,表示必须拓展这个类;抽象方法表示必须要覆盖这个方法。

如果类声明了抽象方法,那么类也必须标记为抽象类。

抽象的意义

父类往往放入子类可以继承的实现,但当抽象程度较高时,父类中放入任何通用代码都不合适。这时候将方法定义为抽象,即使没有任何具体的代码,还是可以为一组子类定义部分协议。

抽象方法实现的规定

实现一个抽象方法时,当前方法必须与父类抽象方法拥有相同的方法名和参数了,而且返回类型需要与抽象方法声明的返回类型兼容。

Object 类

如果没有显式扩展另一个类,所有这样的类都隐含扩展了 Object 类。Object 类的部分方法如下:

  • boolean equals(Object o);
  • Class getClass(); 返回对象的类类型,即对象是从哪个类实例化得到的
  • int hashCode();
  • String toString();

Object 类非抽象,它最重要的一些方法与线程有关。

编译器根据引用类型决定是否能调用一个方法

编译器根据引用类型决定是否能调用一个方法,而不是根据实际对象类型。例如:将 Dog 对象赋值给 Object 类型引用,那么没法通过这个 Object 引用调用 Dog 的 bark 方法。

可以通过强制类型转换将 Object 引用恢复为 Dog 引用。

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

如果 o 不能强制类型转换为 Dog 类型,会抛 ClassCastException 异常。

引用类型规定了解读对象地址的方法。
Java 中,对象不能按值传递,也就没有 C++ 中不可逆的对象切片。

接口

当在一棵继承树的不同层级,有若干个类需要实现同样的方法,例如 Animal 继承树中,猫科动物子类下的 Cat 和犬科动物子类下的 Dog 都需要实现作为宠物的 beFriendly 和 play 方法。现有几种设计思路:

  1. 把 beFriendly 方法加入到 Cat 和 Dog 的公共祖先 Animal 类中
    缺点:Animal 的其他子类不需要 beFriendly 方法;beFriendly 方法肯定被重写,没必要在父类具体实现。
  2. 把 beFriendly 方法作为抽象类,加入 Animal 中
    缺点:Animal 的其他具体子类,不需要 beFriendly,但却不得不实现它?
  3. 把 beFriendly 方法放入 Cat 和 Dog 类中,不依赖继承
    缺点:需要约定 beFriendly 的函数名、参数列表、返回类型;如果拼写错误,或未按照约定实现 beFriendly,编译器不会检查;无法使用多态。
  4. 多继承。
    缺点:Java 不允许多继承。

使用接口可以很好解决这个问题,没有上面几种方法的任何缺点。

public interface Pet {
    public abstract void beFriendly();
    public abstract void play();
    
    public void bark(){ }
}

class Dog implements Pet{
    public void beFriendly() { }
    public void play() { }
}

接口中所有方法必须是抽象方法(这样就不会有菱形继承中,调用父类继承方法二义性的问题)。一个类可以实现多个接口。

接口是脱离继承树的一种实现多态的方法。

子类中调用父类被覆盖的方法

class Report{
    void runReport() { }
}

class BuzzwordsReport extends Report {
    void runReport() {
        super.runReport();
        // ...
    }
}

第九章:对象的生与死

对象存储位置

  • 局部变量存储在栈上
  • 对象存储在堆上

如果一个引用类型的局部变量,指向一个对象。同样,引用存储在栈上,它的二进制指向一个存储在堆上的对象。

实例变量属于对象,也在堆上。如果实例变量是引用类型,也存储在堆上,同时指向另一个在堆上的对象。

默认构造器

如果没有写任何构造器,编译器会生成一个无参构造方法,例如:

public Duck() { }

构造器的名字与类名相同,没有返回类型。

与类同名的方法

Java允许与类同名的方法,但这不会使它成为一个构造器。区别方法和构造器的关键在于返回类型,方法必须有返回类型,构造器没返回类型。

class Duck {
    public Duck() { }       //构造器
    public void Duck() { }  //方法
}

这样写编译器可能给个 warming,不建议这样做。

构造器可以继承吗

Q:如果子类没有提供任何构造器,父类提供了构造器,子类可以继承父类的构造器,而不是得到一个默认的构造器吗?

A:构造器不能继承

哪些情况下不提供无参构造器是合理的?

	Color c = new Color(red, green, blue, opacity);

构造器必须是 public 吗?

构造器可以是 public、protected、private、默认。

private 构造器,在这个类之外无法构建新的对象。

构造链调用顺序

先调用父类的构造器,等父类构造完了之后,再构造子类。

    public Duck(int size){
        this.size = size;
    }
    // 上面的写法相当于:
    public Duck(int size){
        super();
        this.size = size;
    }

如果没有在子类中构造器中显式调用 super,编译器会在子类构造器中加入一个无参的 super 调用。(除非构造器调用了另一个重载构造器)

也就是说如果父类没有无参的构造器时,子类构造器中不显式调用 super,编译器会报错。

super 调用必须是构造器中的第一条语句。

Java 的『委托构造』

如果多个重载的构造器中,除了处理的参数不同之外,其余代码都相同。不希望在多个构造器中维护重复代码的话,可以使用『委托构造』。

方法:在构造器的第一行,调用 this()。(或者 this(int)this(int,double),取决于要委托哪个构造器)

每个构造器,只能有 super()this() 之一,不能都有。

对象的空间回收

当对象的最后一个引用消失时,对象随时可能被垃圾回收。

第十章:数字很重要

静态方法

static 修饰的方法,无需实例即可调用。静态方法可以通过类名调用,也可以通过对象调用。不可以在静态方法中使用非静态方法、成员变量。

静态变量
对于类的所有实例,静态变量的值都相同,所有实例共享静态变量的唯一副本。

加载一个类的时候,会初始化静态变量。加载类的时机由 JVM 自己决定,JVM 会保证:

  • 类的静态变量会在创建这个类的任何对象之前初始化。
  • 类的静态变量会在这个类的任何静态方法运行之前初始化。

C++中的静态变量需要额外显式定义(初始化)。

静态最终变量(常量)

final 关键字的含义是,不会再改变。定义一个常量如下:

    public static final double PI = 3.14;

public 给它宽松的访问限制,static 无需实例化即可使用,final 确保变量的值无法修改,变量名最好遵循命名规范全部大写。

初始化静态最终变量的方法

  1. 声明时初始化
  2. 在一个静态初始化器中初始化
    public static final int X_VALUE = 10;
    public static final int Y_VALUE;

    static {
        Y_VALUE = (int) Math.random();
    }

没有初始化静态最终变量时,编译器会报错。

Math 的部分方法

	int Math.abs(int);		// int long float double
	int Math.max(int,int);	// int long float double
	int Math.min(int,int);
	
	double Math.random();	//返回 [0,1) 范围的随机数
	
	int Math.round(float);
	long Math.round(double);

	double Math.sqrt(double);

基本类型的包装类

每个基本类型都有一个包装类:Integer, int;Long, long;Short, short;Float, float;Double, double;Byte, byte;Boolean, boolean;Character, char;

	// 包装一个值
	int i = 10;
	Integer integer = new Integer(i);
	Integer integer2 = Integer.valueOf(i);
	
	// 解包也给值
	int j = integer.intValue();

Java 5 之后,基本类型将会自动包装和解包,也就是说拥有双向的隐式类型转换。

指定模板类型时,必须使用引用类型,而不能是基本类型,如下:

	ArrayList arrayList = new ArrayList<Integer>();
	ArrayList arrayList = new ArrayList<int>();			// Error

包装类的其他静态方法

	// String 转 基本类型
	int i = Integer.parseInt("12");
	double d = Double.parseDouble("1.20");
	boolean b = Boolean.parseBoolean("True");
	// 基本类型 转 String
	String string2 = i + "";
	String string3 = Double.toString(3.4);
	String string4 = String.valueOf(4.5);

十一章:数据结构

mock、mocking

写一段临时代码来模拟以后的实际代码,这段代码称为『模拟』(mock)代码。

菱形操作符

	ArrayList<String> list = new ArrayList<>();
	// 相当于
	ArrayList<String> list = new ArrayList<String>();

不需要把同样的事说两次,编译器会使用类型推导(type inference)来推导出需要的类型,这种语法称为菱形操作符,Java 7引入的特性。

语法糖

仅用于简化代码,无实际性能提高的语法优化。

sort 方法

// java.util.List
sort(Comparator);

// java.util.Collections
sort(List)
sort(List, Comparator)

使用 List.sort(Comparator) 时,要排序的自定义类型需要实现 Comparable 接口,否则编译报错。

泛型

public class ArrayList<E> extends AbstractList<E> implements List<E> ... {
	public boolean add(E o){ ... }
}

E (Element) 用来占位,代表一个实际的类型。不一定要用字母『E』,有时会见到『T』(Type),『R』( Return type )。实际上可以用任何合法的 Java 标识符,但通常都使用单字母。

使用泛型的两种方式:

  1. 当类声明了一个类型参数后,方法中可以使用这个类型。
	public class ArrayList<E> extends AbstractList<E> ... {
		public boolean add(E o)
		...
  1. 方法自己定义类型参数
	public <T> void takeThing(T o);

T extends …

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

<T extends Animal> 代表 T 是 Animal 或者是 Animal 的子类。

    void takeThing(ArrayList<Animal> list);
    takeThing(animals);
   	takeThing(cats);	// 报错
   	
   	// 改成这样就可以
	<T extends Animal> void takeThing(ArrayList<T> list);

声明为 ArrayList<Animal> 的形参不接受 ArrayList<Cat> 的实参,看起来违反了多态的宗旨,实际上是编译器对有风险行为的规避。假如 ArrayList<Animal> 能够接受 ArrayList<Cat> 实参,并且在方法中给形参 ArrayList<Animal> 中加入 <Dog> 元素,这样符合语法,但运行时会抛异常,反而更离谱。

至于 <T extends Animal> 如何解决刚刚提到的情况,这个本章后面说。(语法层面强行阻止)

十二章:Lambda与流

问就是晚点补笔记…

十三章:有风险的行为

有风险的方法

有风险的方法声明中,能找到一个 throws 子句。

public static Sequencer getSequencer() throws MidUnavailableException

try/catch 块

如果调用一个有风险的方法,既没有处理异常(try/catch),也没有避开异常(throws声明),那么编译期会报错。

try/catch 语句块告诉编译器,你会处理这个异常。编译器并不关心你是如何处理异常,它只知晓 try/catch 语句块会对这个异常负责。

拥有 throw 语句的代码需要用 throws 子句声明自己会抛出的异常类型,调用这个方法的代码需要放在 try 中,并且后面接 catch 语句捕获可能抛出的异常。

    public void crossFingers(){
        try{
            takeRisk();
        }catch(BadException e){
            e.printStackTrace(); 
        }
    }
    
    public void takeRisk() throws BadException{
        if(abandonAllHope){
            throw new BadException();
        }
    }

Exception 类

Throwable 接口有两个方法:getMessage()printStackTrace()。Exception 类拓展了这个接口。

编译器检查的异常类型

如果在代码中抛出一个异常,就必须在方法声明中使用 throws 关键字声明这个异常。

Exception 有一个子类 RuntimeException,除 RuntimeException 外的所有异常编译器都会检查。可以抛出、捕获、声明 RuntimeException 异常,但不是必须,编译器不会检查这些。

为什么之前没用过 try/catch 语句,程序也挺正常的

之前见过 NullPointerException、DivideByZero、NumberFormatException 这些异常,他们是 RuntimeException 的子类,所以编译器不会检查。

为什么不检查 RuntimeException,它就不会让程序崩溃吗?

大部分 RuntimeException 异常是代码逻辑中的问题,属于错误。而异常是程序运行时无法预料的事情。

比如数组越界,这属于错误,应该 debug 修改;而找不到目标文件、服务器不在运行,这些才算异常。

try/catch 控制流

如果没抛出异常,程序会运行 try 中的语句、跳过 catch 部分。

如果 try 中抛出异常,会跳过 try 中剩余语句,运行 catch 部分。

Finally 语句

	try{
		// ...
	}catch(Exception e){
		// ...
	}finally{
		// ...
	}
  • 如果 try 无异常,跳过 catch,执行 finally
  • 如果 try 异常,跳过 try 剩余语句,执行 catch 再执行 finally
  • 如果 try 或 catch 中有 return,会在调用 return 时,先把 finally 执行了,再 return

一个方法抛出多种异常

一个方法可以抛出多种异常,但必须声明它所能抛出的所有受查异常。如果抛出的多个异常有共同的父类,可以只声明这个共同的超类。

处理多个异常时,在 try 下面叠放 catch 块。

	try{
		// ...
	}catch (ShirtException se){
		// ...
	}catch (ClothingException ce){
		// ...
	}

也可以在 catch 中使用抛出异常的一个父类来全部捕获。(但是不建议)

多层 catch 时,需要把子类异常放在上面先捕获,后捕获父类异常。如果反过来,导致某个 catch 永远无法捕获到异常,编译器会报错。

避开异常

如果无法处理某个异常,那么可以避开它。例如 main 调用方法 A,A 调用 B,B 会抛一个异常 E。如果 A 无法处理 E,那么 A 的 throws 子句中声明 E 即可。当真的在 A 调用 B 时发生异常,会递归寻找一个能解决该异常的调用者。

如果 main 也无法解决该异常,JVM 会关闭。

异常的语法规则

  • try 后面必须有 catch 或 finally
  • try 后只有 finally 没有 catch时,相当于要避开异常,需要 throws 中声明

十四章:图形的故事

JFrame

JFrame表示屏幕上的一个窗口,所有界面元素都放在这里,包括按钮、文本域、复选框等,它还可以有简单的菜单条,可以最大化、最小化、关闭。

	JFrame frame = new JFrame();
	JButton button = new JButton("click me");
	// 当 JFrame 关闭时程序退出
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	// 将 button 添加到 JFrame 中
	frame.getContentPane().add(button);
	frame.setSize(300,300);
	// JFrame 可见(默认不可见)
	frame.setVisible(true);

事件源、监听器

GUI 组件是事件源,事件源就是可以把用户的动作(点击鼠标、按键、关闭窗口等)变成一个事件的对象。监听器提供了一个回调函数,当对应的事件发生时就会执行回调函数中的内容。

大部分代码都是在接收事件,很少会创建事件。

当点击按钮时,按钮上的文字改变:

public class Demo {
    public static JFrame frame;
    public static JButton button;

    public static void main(String[] args) {
        frame = new JFrame();

        button = new JButton("click me");
        // 向按钮注册监听器
        button.addActionListener(new ButtonTextChange());
        frame.getContentPane().add(button);

        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(300, 300);
        frame.setVisible(true);
    }
}

class ButtonTextChange implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        Demo.button.setText("clicked");
    }
}

JPanel

我的理解是,JPanel 是一个画板,可以绘制图形、放置图片,继承 JPanel 来重写画板绘制图像的 paintComponent 方法。

当 JVM 认为显式需要刷新(例如窗体大小改变、最小化时),会再次调用 paintComponent 重绘。也可以显式调用 repaint() 来要求 JVM 重绘,但绝不要手动调用 paintComponent 方法。

在 JPanel 上绘制长方形:

class MyJPanel extends JPanel{
    @Override
    protected void paintComponent(Graphics g) {
        g.setColor(Color.blue);
        g.fillRect(30, 50, 100, 100);
    } 
}

在 JPanel 上显示图片:

    @Override
    protected void paintComponent(Graphics g) {
        Image image = new ImageIcon("Java\\pic.PNG").getImage();
        g.drawImage(image, 30, 30, this);
    } 

paintComponent 的形参是 Graphics 类型,实参是 Graphics2D 类型(Graphics 的子类) 。

Graphics2D 类可以调用的方法比父类更多,可以设置渐变色:

    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D) g;
        // 渐变色开始的坐标,开始的颜色,结束的坐标,结束的颜色
        GradientPaint gradient = new GradientPaint(50, 50, Color.blue, 250, 250, Color.orange);
        g2d.setPaint(gradient);
        g2d.fillOval(50, 50, 250, 250);
    } 

GUI 布局

窗体默认有 5 个区域可以增加部件:东南西北中。之前使用的无参 add 默认将部件放在中央,当页面上需要防止多个控件时,需要改变布局。

上方 JPanel,下方按钮,点击按钮改变 JPanel 图像颜色:

public class Demo {
    public static JFrame frame;

    public static void main(String[] args) {
        frame = new JFrame();
        JButton button = new JButton("change color");
        button.addActionListener(new ButtonChangeColor());

        frame.getContentPane().add(BorderLayout.SOUTH, button);
        frame.getContentPane().add(BorderLayout.CENTER, new MyJPanel());
        frame.setSize(300, 300);
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}

class MyJPanel extends JPanel {
    static Random random = new Random();
    @Override
    protected void paintComponent(Graphics g) {
        g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
        g.fillOval(50, 50, 200, 200);
    }
}

class ButtonChangeColor implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        Demo.frame.repaint();
    }
}

内部类的适用场景

当 JFrame 上有两个 JButton 被点击后要做不同的事情,实现的方式有以下几种:

  1. 两个按钮注册同一个监听器,在监听器内部判断是哪个按钮被点击了
    @Override
    public void actionPerformed(ActionEvent e) {
        if(e.getSource() == Demo.colorButton){
            Demo.frame.repaint();
        }else{
            Demo.label.setText(MyJPanel.random.nextInt(100) + "");
        }
    }

缺点:没有面向对象;需要可访问控件,破坏封装。

  1. 两个按钮创建两个不同的监听器类

缺点:需要可访问空间,破坏封装。

内部类可以使用外部类的变量,即使是私有变量,同样外部类也可以访问内部类的变量。

class OuterClass{
	private int x;
	class InnerClass{
		void go(){
			x = 1;
		}
	}
}

内部类实例必须与一个外部类实例绑定。

当在外部类代码中实例化内部类,外部类的实例就与内部类的实例绑定:

class OuterClass{
	private int x;
	InnerClass inner = new InnerClass();

	public void doStuff(){
		inner.go();
	}

	class InnerClass{
		void go(){
			x = 1;
		}
	}
}

如果在外部类以外实例化内部类,需要一种特殊的语法将内部类实例与一个外部类实例绑定,这种情况很少见可能永远不会用到:

    public static void main(String[] args){
        MyOuter outerObj = new MyOuter();
        MyOuter.MyInner innerObj = outerObj.new MyInner();
    }

内部类很适合注册监听类。

Lambda 实现监听类

Lambda 会为一个函数式接口的唯一抽象方法提供一个实现:

	colorButton.addActionListener(event -> frame.repaint());
	labelButton.addActionListener(event -> label.setText("demo"));

十五章:使用Swing(Todo)

组件与布局管理器

在 Swing 中,几乎所有组件都能包含其他组件。一般把按钮、列表之类的组件称为交互式组件,窗体、面板之类的称为背景组件。但除了 JFrame 之外,交互组件和背景组件的差别是人为而定的。

布局管理器是一个与特定组件相关联的对象,它会控制这个组件中包含的所有组件的布局。常见的布局管理器有以下几种:

  • BorderLayout,边框布局,会将一个背景组件划分为东南西北中,五个区域,BorderLayout 是窗体的默认布局管理器。
  • FlowLayout,流式布局,像文字布局一样从左到右、从上到下,是面板的默认布局管理器。
  • BoxLayout,盒式布局,垂直或水平布局(往往只用垂直布局),它不像流式布局一样自动换行,而是通过插入回车来规定布局什么时候换行。

BorderLayout

边框布局分:东南西北中,四块区域。『南』『北』区域会无视组件的宽度,『东』『西』区域无视组件的高度,剩下的空间全部归『中』区域。

其他组件

  • JTextField,单行文本输入框
  • JTextArea,多行文本输入框
  • JCheckBox,选择框(只有选、不选两种状态)
  • JList,文本列表

十六章:保存对象和文本

保存文本的两种选择

  • 使用串行化。用于保存对象,生成的数据只能由 Java 程序使用
  • 写一个纯文本文件。数据可被其他程序使用
  • 还有其他方法。

将串行化对象写入文件

	FileOutputStream fileStream = new FileOutputStream("Myfile.ser");
	ObjectOutputStream os = new ObjectOutputStream(fileStream);
	
	os.writeObject(o1);
	os.writeObject(o2);
	
	os.close();

FileOutputStream 是连接流,连接流表示与源或目标的一个连接;
ObjectOutputStream 是链流,链流不能单独连接,必须串连到一个连接流。(中间过程)

Q:为啥不一步到位,还要分两步走?
A:可以混搭不同的连接流与链流,灵活性高。

串行化与 Serializable

串行化一个对象时,对象成员变量所引用的其他对象也会自动串行化,是一个递归的过程。

如果希望你的类可串行化,要实现 Serializable 接口。Serializable 接口被称为记号或标记接口,因为这个接口没有要实现的方法。它唯一的作用就是宣布实现它的类是可串行化的。

如果一个类的任何超类是可串行化的,这个子类会自动可串行化,即使没有显式声明实现 Serializable。

public class Demo implements Serializable {
    int width;
    int height;

    Demo(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public static void main(String[] args) {
        Demo myDemo = new Demo(5, 10);
        
        try {
            FileOutputStream fileStream = new FileOutputStream("foo.ser");
            ObjectOutputStream os = new ObjectOutputStream(fileStream);
            os.writeObject(myDemo);
            os.close();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

transient

串行化要么完成,要么失败。(原子操作)

如果一个成员变量无法保存,或者不应该被保存,可以将它标记为 transient,串行化过程中就会略过这一成员变量。

class Chat implements Serializable{
    String userName;
    transient String currentID;
}

变量不可串行化的原因有:

  • 忘记实现 Serializable
  • 对象依赖特定的运行时信息,例如网络连接、线程、文件对象,串行化后失去意义
  • 含敏感信息,例如 password 字段,串行化后不安全

为什么不让所有类默认实现串行化

接口只能提示类拥有某个功能,而不能指示类缺少某个功能。如果所有类默认实现了串行化,怎么把它关掉?多态模型做不到这一点。

某个类没实现串行化接口,该如何补救

如果类不是 final 的话,可以派生它的一个子类。代码重需要超类的地方,全部用子类代替。

transient 标记的变量,逆串行化时会如何处理

默认值为 null。可以在逆串行化时,手动给这些变量做一些初始化。

如果对象的两个成员变量,引用的是同一个对象,串行化时会保存两份吗

串行化会做判断,这种情况只保存一个对象。在逆串行化时,恢复他们的所有引用。

十七章:建立连接

连接

要建立一个连接,需要知道两个信息:IP、端口号。

	InetSocketAddress serverAddress = new InetSocketAddress("127.0.0.1", 5000);
	SocketChannel socketChannel = SocketChannel.open(serverAddress);

InetSocketAddress 表示连接的完整地址,使用 SocketChannel 与另一台机器对话。

不是使用构造器来创建 SocketChannel,而是用静态 open 方法,把它连接到所提供的地址。

端口

范围 [0,65535]. 其中 0 ~ 1023 为公认服务器保留,例如:FTP 20,HTTP 80,HTTPS 443,SMTP 25。自己的服务程序最好使用 1023 以后的端口。

一个端口只能绑定一个应用程序,如果被绑定的端口已经被占用,绑定时会返回 BindException。

接收

在网络连接上通信,可以使用常规的 I/O 流,大部分 I/O 工作不关心高层链流具体连接到哪里。

	SocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 5000);
	SocketChannel socketChannel = SocketChannel.open(serverAddr);
	
	Reader reader = Channels.newReader(socketChannel, StandardCharsets.UTF_8);
	BufferedReader bufferedReader = new BufferedReader(reader);
	
	String message = bufferedReader.readLine();

Reader 是底层字节流 Channel 与高层字节流 BufferedReader 之间的一座桥梁,使用 Channels 类的静态辅助方法由 SocketChannel 创建一个 Reader,第二个参数指定了字符集为 UTF-8。

发送

	SocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 5000);
	SocketChannel socketChannel = SocketChannel.open(serverAddr);
	
	Writer writer = Channels.newWriter(socketChannel, StandardCharsets.UTF_8);
	PrintWriter printWriter = new PrintWriter(writer);
	
	printWriter.println("message to send.");
	printWriter.print("another message");

Socket 建立连接

本章最初使用了 Channel 建立连接,但连接的方法不止一种,最简单的方法之一是:java.net.Socket

使用 Socket 得到 InputStream 或 OutputStream:

	Socket socket= new Socket("127.0.0.1", 5000);
	
	InputStreamReader in = new InputStreamReader(socket.getInputStream());
	BufferedReader bufferedReader = new BufferedReader(in);
	String message = bufferedReader.readLine();
	
	PrintWriter writer = new PrintWriter(socket.getOutputStream());
	writer.println("message to send.");

Thread

thread(线程)拥有一个单独的调用栈。

Thread 类拥有的方法:

  • void join()
  • void start()
  • static void sleep()

每个 Java 程序有一个主线程,主线程调用栈底是 main,其他线程调用栈底是 run()

Runnable

Runnable 接口只定义了一个方法:public void run()。由于接口只有一个方法,使用它是一个 SAM 类型,一个函数式接口。如果愿意,可以使用 lambda 提供方法,而不是类。

public class Demo {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
        Thread.dumpStack();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        Thread.dumpStack();
    }
}

输出如下,可以看到,线程的栈底是 run() 方法。

java.lang.Exception: Stack trace
        at java.lang.Thread.dumpStack(Thread.java:1336)
        at Demo.main(Demo.java:5)
java.lang.Exception: Stack trace
        at java.lang.Thread.dumpStack(Thread.java:1336)
        at MyRunnable.run(Demo.java:12)
        at java.lang.Thread.run(Thread.java:748)

十八章:处理并发问题

synchronized

使用 synchronized 使一个同步块对一个对象同步:

	synchronized(account){
		if(account.getBalance() >= amount){
		    account.spend(amount);
		}
	}        

或者将类中一个方法声明为同步方法:

    public synchronized void spend(int amount) {
        if (balance >= amount) {
            balance -= amount;
        }
    }

不是每个方法有一把锁,而是每个对象有一把锁。当一个对象中有多个同步方法时,只有当线程获得对象的锁之后,才可以进入一个同步方法。

也就是说,一个对象中如果有两个同步方法,那么两个线程不能同时进入两个同步方法。

静态方法的锁

除了每个对象有一把锁之外,每个加载的类也有一个锁,可以用于同步静态方法。

为什么不将一切都同步来保证线程安全

  • 性能降低
  • 可能导致死锁

原子变量

如果共享数据是一个 int、long 或 boolean,可以替换为原子变量:AtomicInteger、AtomicLong、AtomicBoolean 和 AtomicReference,从而降低锁的粒度。

class Balance {
    AtomicInteger balance = new AtomicInteger(0);

    public void increment() {
        balance.incrementAndGet();
    }
}

incrementAndGet 可以理解为 ++i

原子变量还有 CAS(Compare-and-swap)方法:

    public void spend(int amount) {
        int initialBalance = balance.get();
        if (initialBalance >= amount) {
            boolean success = balance.compareAndSet(initialBalance, initialBalance - amount);
            if (!success) {
                System.out.println("sorry.");
            }
        }
    }

compareAndSet 将原子变量的值与预期值比较,如果一致,那么对值进行修改,返回 true;否则返回 false。一定要处理返回 false 的情况。

线程安全的数据结构

ArrayList 不是线程安全的,如果一个线程正在修改一个集合,同时另一个线程在读这个集合,就会得到一个 ConcurrentModificationException 异常。

CopyOnWriteArrayList 实现了 List 接口,可以用它来替换 List

    List<Chat> chatList = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList 读的时候,是一个快照读。写的时候,会 Copy 一个副本进行修改,改完后用副本替代原值,适用于读多写少的并发场景。


更新情况 & 书评

待完善笔记:10、11、12、16、17 章
这篇读书笔记暂且算完结了吧,与其去补笔记,我更想新开一本书

复制几段我认可的书评:

  • 如果你是想全面的了解 java 语言,估计你会很失望,这本书里面甚至没有讲“反射”
  • 如果你想找一本语法参考,那这不是你想要的(好像有点吹毛求疵……)
  • 清晰的条理,生动的图示,偶尔来点老外的幽默——其实中国人不太能理解,阅读体验非常舒畅。
  • 尤其是你有其它语言基础的情况下,这本书能迅速让你明白java的特质。 缺点是,它真的只是入门书。你必然还需要一本Java大字典,比如《Thinking in Java》,以便查阅Java在细节上的更多东西。关于这一点,书中附录B也说得很清楚了。

总结一下:

优点:有趣、易懂,能把某个新概念的适用场景讲清楚。
缺点:知识点、语法涉及的太少了

这本书重点讲了 Java 中难理解的概念,其他相对易理解的语法很少涉及。相当于只有一个骨架,还需要自行再读一本字典式语法书,补充知识体系的血肉。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

m0_51864047

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值