• 读书笔记: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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

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

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

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

    import 导入包

    要使用 ArrayList,有两种方式:

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

    java.util.ArrayList<Dog> list;
    
    public void foo(java.util.ArrayList<Dog> list) { }
    
    public java.util.ArrayList<Dog> foo() { }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    import java.util.ArrayList;
    
    • 1

    import 是否会导致代码膨胀

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

    为什么不导入String、System

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

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

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

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

    public void roam(){
    	super.roam();
    	//...
    }
    
    • 1
    • 2
    • 3
    • 4

    阻止继承的方法

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

    覆盖(overRide)规则

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

    重载方法

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

    『隐藏』呢

    多态

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

    第八章:真正的多态

    抽象类、抽象方法

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

    // 抽象类
    abstract class Dog extends Animal { }
    // 抽象方法
    public abstract void eat();
    
    • 1
    • 2
    • 3
    • 4

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

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

    抽象的意义

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

    抽象方法实现的规定

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

    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;
    	}
    
    • 1
    • 2
    • 3

    如果 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() { }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

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

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

    class Report{
        void runReport() { }
    }
    
    class BuzzwordsReport extends Report {
        void runReport() {
            super.runReport();
            // ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    第九章:对象的生与死

    对象存储位置

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

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

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

    默认构造器

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

    public Duck() { }
    
    • 1

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

    与类同名的方法

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

    class Duck {
        public Duck() { }       //构造器
        public void Duck() { }  //方法
    }
    
    • 1
    • 2
    • 3
    • 4

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

    构造器可以继承吗

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

    A:构造器不能继承

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

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

    构造器必须是 public 吗?

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

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

    构造链调用顺序

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

        public Duck(int size){
            this.size = size;
        }
        // 上面的写法相当于:
        public Duck(int size){
            super();
            this.size = size;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果没有在子类中构造器中显式调用 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;
    
    • 1

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

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

    1. 声明时初始化
    2. 在一个静态初始化器中初始化
        public static final int X_VALUE = 10;
        public static final int Y_VALUE;
    
        static {
            Y_VALUE = (int) Math.random();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    基本类型的包装类

    每个基本类型都有一个包装类: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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

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

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

    包装类的其他静态方法

    	// 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    十一章:数据结构

    mock、mocking

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

    菱形操作符

    	ArrayList<String> list = new ArrayList<>();
    	// 相当于
    	ArrayList<String> list = new ArrayList<String>();
    
    • 1
    • 2
    • 3

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

    语法糖

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

    sort 方法

    // java.util.List
    sort(Comparator);
    
    // java.util.Collections
    sort(List)
    sort(List, Comparator)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    泛型

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

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

    使用泛型的两种方式:

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

    T extends …

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

    代表 T 是 Animal 或者是 Animal 的子类。

        void takeThing(ArrayList<Animal> list);
        takeThing(animals);
       	takeThing(cats);	// 报错
       	
       	// 改成这样就可以
    	<T extends Animal> void takeThing(ArrayList<T> list);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

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

    十二章:Lambda与流

    问就是晚点补笔记…

    十三章:有风险的行为

    有风险的方法

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

    public static Sequencer getSequencer() throws MidUnavailableException
    
    • 1

    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();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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{
    		// ...
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 如果 try 无异常,跳过 catch,执行 finally
    • 如果 try 异常,跳过 try 剩余语句,执行 catch 再执行 finally
    • 如果 try 或 catch 中有 return,会在调用 return 时,先把 finally 执行了,再 return

    一个方法抛出多种异常

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

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

    	try{
    		// ...
    	}catch (ShirtException se){
    		// ...
    	}catch (ClothingException ce){
    		// ...
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    也可以在 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    事件源、监听器

    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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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);
        } 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在 JPanel 上显示图片:

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

    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);
        } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    内部类的适用场景

    当 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

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

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

    class OuterClass{
    	private int x;
    	class InnerClass{
    		void go(){
    			x = 1;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

    class OuterClass{
    	private int x;
    	InnerClass inner = new InnerClass();
    
    	public void doStuff(){
    		inner.go();
    	}
    
    	class InnerClass{
    		void go(){
    			x = 1;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

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

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

    Lambda 实现监听类

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

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

    十五章:使用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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    transient

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

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

    class Chat implements Serializable{
        String userName;
        transient String currentID;
    }
    
    • 1
    • 2
    • 3
    • 4

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

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

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

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

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

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

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

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

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

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

    十七章:建立连接

    连接

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

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

    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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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.");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出如下,可以看到,线程的栈底是 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    十八章:处理并发问题

    synchronized

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

    	synchronized(account){
    		if(account.getBalance() >= amount){
    		    account.spend(amount);
    		}
    	}        
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

        public synchronized void spend(int amount) {
            if (balance >= amount) {
                balance -= amount;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

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

    静态方法的锁

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

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

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

    原子变量

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

    class Balance {
        AtomicInteger balance = new AtomicInteger(0);
    
        public void increment() {
            balance.incrementAndGet();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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.");
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    线程安全的数据结构

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

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

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

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


    更新情况 & 书评

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

    复制几段我认可的书评:

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

    总结一下:

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

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

  • 相关阅读:
    Anthropic全球上线AI语言模型Claude 2;多模态系统:融合文本和图像的新前沿
    Golang 写日志到文件
    java 的三种 Base64
    LeetCode刷题系列 -- 32. 最长有效括号
    C++语言之虚函数、多态、抽象类
    含文档+PPT+源码等]精品spring boot+MySQL微人事系统设计与实现vue[包运行成功]计算机毕设Java项目源码
    acwing 803. 区间合并-java版本
    1.9 - Cache
    2022年9月总结
    C语言——概述
  • 原文地址:https://blog.csdn.net/m0_51864047/article/details/134023120