文章目录
第一章:浮出水面
编译、运行 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特定) |
byte | 8 |
char | 16 |
short | 16 |
int | 32 |
float | 32 |
long | 64 |
double | 64 |
标识符/变量 命名规则
- 首字母:字母、_、$
- 非首字母:字母、_、$、数字
- 不能和保留字重名
引用类型
Java 只有按值传递、按引用传递,无指针。可以说除了基本数据类型之外,其他的全是引用。
类类型、String 类型、数组,都是引用。
引用变量的大小
可以认为是 64 位,但具体多大取决于 JVM 的实现。同一个 JVM,所有引用的大小都相同,不同 JVM 引用大小就不一定了。
引用初始化
C++ 的引用必须初始化,一旦初始化后,不可以再引用别的变量。
Java 的引用可以不初始化(引用 null),引用 A 之后,可以再引用 B。
看起来就是指针?
堆、垃圾回收机制
对象在堆上分配空间,当没有引用指向某个对象时,这块空间可以被 JVM 垃圾回收机制回收利用。
智能指针?
数组类型
数组是对象,或者说数组名是引用类型。数组长度:array.length
第四章:对象的行为
按值传递
可以说 Java 中一切都是按值传递。按引用传递的本质,也是按值传递了一段二进制 01 码。
Setter封装的意义
可以验证参数,例如拒绝将某个值设为负数。也可以抛出异常,或者将参数调整为能接受的最近的值。
如果接受任何参数,Setter是否多余?
- 记不住哪些变量有 Setter,哪些可以直接修改(自己想的)
- 添加 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 方法。现有几种设计思路:
- 把 beFriendly 方法加入到 Cat 和 Dog 的公共祖先 Animal 类中
缺点:Animal 的其他子类不需要 beFriendly 方法;beFriendly 方法肯定被重写,没必要在父类具体实现。 - 把 beFriendly 方法作为抽象类,加入 Animal 中
缺点:Animal 的其他具体子类,不需要 beFriendly,但却不得不实现它? - 把 beFriendly 方法放入 Cat 和 Dog 类中,不依赖继承
缺点:需要约定 beFriendly 的函数名、参数列表、返回类型;如果拼写错误,或未按照约定实现 beFriendly,编译器不会检查;无法使用多态。 - 多继承。
缺点: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 确保变量的值无法修改,变量名最好遵循命名规范全部大写。
初始化静态最终变量的方法
- 声明时初始化
- 在一个静态初始化器中初始化
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 标识符,但通常都使用单字母。
使用泛型的两种方式:
- 当类声明了一个类型参数后,方法中可以使用这个类型。
public class ArrayList<E> extends AbstractList<E> ... {
public boolean add(E o)
...
- 方法自己定义类型参数
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 被点击后要做不同的事情,实现的方式有以下几种:
- 两个按钮注册同一个监听器,在监听器内部判断是哪个按钮被点击了
@Override
public void actionPerformed(ActionEvent e) {
if(e.getSource() == Demo.colorButton){
Demo.frame.repaint();
}else{
Demo.label.setText(MyJPanel.random.nextInt(100) + "");
}
}
缺点:没有面向对象;需要可访问控件,破坏封装。
- 两个按钮创建两个不同的监听器类
缺点:需要可访问空间,破坏封装。
内部类可以使用外部类的变量,即使是私有变量,同样外部类也可以访问内部类的变量。
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 中难理解的概念,其他相对易理解的语法很少涉及。相当于只有一个骨架,还需要自行再读一本字典式语法书,补充知识体系的血肉。