Java 编程思想

第1章 对象的概念

封装

将对象保护起来,让调用对象的人,不能轻易的修改对象内部的实现细节,防止在调用过程中,对象被无意中破坏;

函数式编程,貌似不存在这种担心;为什么 OO 会出现这种情况呢?猜测是因为在内部存储状态造成的,它的实现变得复杂了,对状态有依赖,导致变得脆弱,从而需要提供某种保护;因此,有可能的情况下,应尽量避免出现状态依赖,让对象更加纯粹,这样就无须提供保护;

后来发现真正的目的并不是保护,而是为了方便后续更改实现;只要调用者没有访问内部的实现细节,那么内部就拥有更改的灵活性;

protected 和 private 区别

private 是类和调用者之间的屏障,如果非法调用,则编译时会报错;子类不继承父类的 private 成员;

protected 跟 private 只有一个点不同,即子类可以调用父类 protected 的属性或方法;

default:默认同一个包中的类,都可以相互访问;

组合和聚合的区别

区别在于生命周期,组合对象被删除时,各成员中的子对象也会被删除,但聚合不会;

is-a 和 is-like-a 的区别

如果派生类没有添加新的办法,只是覆盖基类的方法,那么基类和派生类之间是 is-a 的关系,二者可以完美替代;

如果派生类添加了新的方法,则跟基类是 is-like-a 的关系,因为存在新方法,所以不能无脑调用;

后期绑定

感觉 Java 的后期绑定有点像 js 等动态语言中的回调,函数做为某个对象的属性,约定共同的名称,例如 callback,届时按规则进行调用即可;甚至动态语言还可以更加灵活,在执行调用地方,传入任意函数做为参数的一部分,以方便调用者后续可以执行该函数;

OO 通过动态绑定来实现多态,即编译器在编译时,并不知道某个函数会收到什么样的参数,但由于这些参数有共同的方法命名绑定,因此,只需要按名称调用方法,则可以实现预期的调用;

这种动态的特性,在动态语言中是天生的,开发者完全注意不到,也完全没有想到,它在静态语言中,会是一个问题;事实上,Java 也同时是一门解释型的语言,因此才能够拥有这种动态语言的特性;

单继承结构

Java 中所有的对象都继承自基类 Object,它的好处是让整个语言极大降低了复杂度,也有利于垃圾回收。 C++ 为了兼容 C,所以会更加复杂;

集合

集合是编写代码过程中,非常常用的一种数据结构。相比 Java 的原始数组,集合实现了一些常用的方法,并且能够动态的扩展大小,要方便很多;Java 5 引入泛型后,让集合的使用更加方便了;

垃圾回收

Java 使用单继承结构,并要求所有对象都必须存放在堆中,这个设计极大的方便了内存的回收管理;因为只要创建对象,就可以统一调用基类 Object 的方法,触发内存管理;

异常处理

拥有异常处理机制是很重要的,因为如果没有,一旦出错,就意味着程序死掉了,无法从错误中恢复。这非常不利于程序对外提供稳定的服务,而异常处理机制可以让问题局限于某个范围,不至于影响全局;

第3章 万物皆对象

对象引用

1
2
3
String s; // 创建了一个对象引用

String s = "asd" // 创建了一个对象引用,同时还创建了一个对象,并给引用赋值,让其关联对象;

对象创建

1
String s = new String("abc") // 使用 new 关键字来创建对象

数据存储

基本类型的存储

由于对象保存在堆中,有时候一些简单变量可考虑使用基本类型,这样它们会存储在栈中,无需创建对象,性能会好一些

不过说实话,个人感觉这点微乎其微的性能提升几乎没有意义;

高精度数值

BigInteger 和 BigDecimal 两个类型用来存储高精度数值,它们也是包装类型,但没有对应的基本类型;

BigDecimal 常用于货币运算,以避免计算过程中出现精度丢失;

数组的存储

在 C/C++ 中,数组是手工分配的内存块,不小心会越界读取,带来不可预测的行为;Java 通过牺牲一些内存空间和计算性能,换取了使用数组的安全和方便性;

对象清理

作用域

1
2
3
4
5
6
7
// 在 java 中,以下写法是非法的,会提示变量重复定义,但在 C/C++ 中是合法的,因为后者的作用域较小
{
int x = 10;
{
int x = 20;
}
}

类的创建

方法名 + 参数类型 的组合构成了方法(函数)的唯一标识(估计是为了实现重载);

方法可以返回任何类型的数据,也可以不返回任何数据(需要用关键字 void 标识);

命名冲突

为了避免类的命名冲突,Java 使用倒序域名来作为包的名称,同时将类放在包中,以尽可能减少重名的情况;

static 关键字

可以通过 static 关键字定义静态变量或静态方法;所谓的静态,意味着它们是静态存在的,不需要动态创建,也就是说,无须通过 new 创建对象来访问这些变量或方法,只需通过类名,即可访问它们,而且它们也不会在内存中重复创建,只会在加载类的时候,创建一次;

1
2
3
4
5
6
7
8
9
10
class StaticTest {
static int i = 47; // 静态变量
static void sayHello() { // 静态方法
System.out.println("hello");
}
}

// 优先通过类名访问静态变量或静态方法,而不是实例化对象后,再通过对象访问;
StaticTest.i++;
StaticTest.sayHello();

第4章 运算符

字面值常量

Long 类型的数值,结尾需要使用字母 L(大小写都可以)来表示;建议用大写,免得混淆;

16进制有个前缀 0x

8 进制的前缀为 0

2 进制的前缀为 0b

Java 使用 %n 来统一 window 和 unix 两个平台不同的换行符格式;唯一的例外是在 println 函数,仍然使用 /n 作为换行;

第5章 控制流

if-else, while, do-while, for-in, for

逗号运算符:可用来定义多个类型相同的变量

1
int i = 10, j = 20;

第6章 初始化和清理

利用构造器保证初始化

Java 和 C++ 一样,强制通过构造器来初始化对象,这样可以避免 C 语言中容易出现的问题,即调用者忘了做初始化的工作,导致出错;

1
2
3
4
5
6
7
class Rock {
Rock() { // 这是一个构造器,本身没有返回值
System.out.print("Rock ");
}
}

Rock a = new Rock(); // a 得到的赋值,实际上是 new 关键字的返回值;

重载

重载的一个使用场景是让类拥有多个不同的构造器,以便接受不同的参数,完成不同的初始化;

无参构造器

当定义了一个类,如果没有在类中定义构造函数,编译器会自动给它添加一个无参构造器;如果有在类中定义了有参数的构造器,那么编译器将不再自动创建无参构造器了;此时创建对象必须传参,不然会报错;

this 关键字

当在方法中 return this 时,可用来构造链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// housekeeping/Leaf.java
public class Leaf {

int i = 0;

Leaf increment() {
i++;
return this; // 返回 this 实现链式调用
}

void print() {
System.out.println("i = " + i);
}

public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print(); // 链式调用
}
}

另外 this 也用于将当前对象作为参数,传给另外一个函数;

当形参的命名跟内部变量同名时,可以使用 this.field 来处理;

垃圾回收器

Java 的内存回收不是实时的,而是要等到内存使用达到某个临界点时,才会触发回收;这会带来一个问题是当程序运行的越久,一方面其占用的内存会变得越来越多;另一方面,清理的工作会变得很大,会占用较多的 CPU 时间,导致短时间内出现性能下降;

回收前,虚拟机需要先标记仍然存活的对象,一种常见的方法是根搜索机制,即将某些对象标记为根对象,然后从这些对象出发,一层一层的往下遍历,直接没有找不到对象为止;凡是能找到的,就标记为”存活“;之后将存活的对象从一个堆块,复制到另外的新堆块,一方面这样可以排列紧凑,避免空档;另一方面可以将旧堆块标记为可用,实现清空;

垃圾回收有很多种机制,不同的虚拟机使用不同的机制,一种常见的机制如下:

  • 将堆分成五块,分别是新生块、中生块1、中生块2、老年块、永生块;
  • 永生块主要用于存储类库,一般不会回收;(java1.8 之后永生块改成了元数据区)
  • 新生块、中生块1、中生块2、老年块的空间占比分别是 32 : 4 : 4 : 60;
  • 所有新增的对象一般先放到新生块中,如果很大,则直接进入老年块,避免频繁触发回收;
  • 当新生块满了后,触发回收;遍历一遍,没有引用的标记删除;有引用的,代数加1,挪到中生块1;
  • 中生块1 如果满了,触发回收;删除死的,存活代数加1,复制到中生块2;
  • 中生块2 如果满了,触发回收;删除死的,存活代数加1,复制到中生块1;
  • 如果某个对象在中生块的存活代数达到门槛,例如8代,将其挪到老年块;

JIT: Just In Time,及时处理,意思是到了运行的时候,再将字节码编译成机器码;理论上字节码是解释运行的,并没有编译成机器码存储起来;为了提高运行速度,虚拟机可以将一些热点代码,提前编译成机器码,这样就不需要每次调用时,都进行解释了;

成员初始化

当一个类被加载时,首先会初始化静态变量,之后分配内存,为内部属性(如有)设置默认值;之后调用构建函数,为、内部属性再次赋值(如有);

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
32
33
34
35
36
37
38
39
```java
// housekeeping/Mugs.java
// Instance initialization

class Mug {
Mug(int marker) {
System.out.println("Mug(" + marker + ")");
}
}

public class Mugs {
Mug mug1;
Mug mug2;
{ // [1] 这里非常有意思,它没有 static 标志,但是它会在构造函数调用前先被执行
// 当存在构造函数时,这种写法可以让代码不管哪个构造函数被调用,都可以被执行
mug1 = new Mug(1);
mug2 = new Mug(2);
System.out.println("mug1 & mug2 initialized");
}

Mugs() {
System.out.println("Mugs()");
}

Mugs(int i) {
System.out.println("Mugs(int)");
}

public static void main(String[] args) {
System.out.println("Inside main()");
new Mugs();
System.out.println("new Mugs() completed");
new Mugs(1);
System.out.println("new Mugs(1) completed");
}
}
```


数组初始化

Object 类默认的 toString 方法是打印类名和对象地址;

枚举类型

1
2
3
public enum Spiciness {  // enum 实际上是一个类,有自己的方法,例如 ordinal, values 等
NOT, MILD, MEDIUM, HOT, FLAMING
}
1
2
3
4
5
6
public class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM; // 使用枚举类型
System.out.println(howHot);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class EnumOrder {
public static void main(String[] args) {
for (Spiciness s: Spiciness.values()) {
System.out.println(s + ", ordinal " + s.ordinal());
}
}
}
// 输出结果如下
NOT, ordinal 0
MILD, ordinal 1
MEDIUM, ordinal 2
HOT, ordinal 3
FLAMING, ordinal 4

第7章 封装

之所以要做访问控制,主要出现在类库的开发场景,即所开发的类库会被他人调用。如果不限制访问某些属性,任意由调用者访问,会导致后续难以重构该类库的某些功能实现,因为里面有些属性可能被旧代码访问。因此重构后,可能会导致旧的代码无法正常工作。类库作者因此被绑住了手脚,无法实施进一步的迭代升级;

包的概念

包是一种组织管理源代码文件的方式,以减少命名冲突,让文件更加结构化,方便理解和查找;

.java 后缀的文件被称为源文件,每个源文件中,只能允许有一个 public 类,但可能有多个非 public 类;同时 public 类的名称需要与源文件名相同;

当源文件位于某个包中时,需要在源文件的头部写 “package <包名>”

访问权限修饰符

通常情况下,默认权限已经够用,但更推荐将不公开的成员设置为 private,例如它可以用来限制调用构造函数,强制要求调用静态方法;

当然,如果整个包都是自用的,没有被外部调用,那么 default 权限一般就够用了;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// hiding/IceCream.java
// Demonstrates "private" keyword

class Sundae {
private Sundae() {}
static Sundae makeASundae() {
return new Sundae();
}
}

public class IceCream {
public static void main(String[] args) {
//- Sundae x = new Sundae();
Sundae x = Sundae.makeASundae(); // 貌似很适合用于工厂模式中
}
}

接口和实现分离

访问权限带来了接口和实现的分离,即调用者只能调用 public 方法,不能访问细节,因此内部的实现可以根据需要修改;

类访问权限

通常类的访问权限是 public,但是也可以是 default;当它是 default 时,它将不能在包外的位置进行初始化;此时它内部的方法即便是 public 也是无法访问的,调用的位置在编译时会报错;default 的类是无法被包外的使用者进行调用的,仅限包类使用;

正常文件中的类至少有一个是 public,但也可以没有 public;通常它们是一些辅助类,仅限在包中使用,不想给外部的人员调用;

类只有两种访问权限,要么是 public,要么是 default;

1
2
3
4
5
6
7
class Soup1 {
private Soup1() {} // 构造函数设置为 private,不允许外部实例化对象

public static Soup1 makeSoup() { // 通过调用静态方法,每次返回一个新的对象
return new Soup1();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
class Soup2 {
private Soup2() {} // 构造函数设置为 private,不允许外部实例化对象

private static Soup2 ps1 = new Soup2(); // 此处通过静态方法,先初始化了一个对象

public static Soup2 access() { // 通过 access 静态方法,返回提前创建好的对象的引用
return ps1;
}

public void f() {}
}
// 以上传说中的单例模式,貌似可用于 credentials 的创建,方便全局引用

Java 的访问权限更适用于编写类库供外部调用的场景,如果不是这种场景,例如一个人编写程序,或者每个人编写各自的程序,无须相互调用,那么一般 default 权限就够用了;

第8章 复用

在 Java 中有两种复用代码的就去,一种是组合,一种是继承;

组合语法

定义新类时,将旧类作为新类的成员;

继承语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 使用 extends 关键字来表示继承
public class Detergent extends Cleanser {
// Change a method:
@Override
public void scrub() {
append(" Detergent.scrub()");
// 此处调用父类的同名方法,如果调用自己,会出现递归调用(有时递归是必要的,但需要添加条件
// 以便递归可以终止
super.scrub();
}
// Add methods to the interface:
public void foam() { append(" foam()"); }
// Test the new class:
public static void main(String[] args) {
Detergent x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
System.out.println(x);
System.out.println("Testing base class:");
Cleanser.main(args);
}
}

如果某个类设计出来后,是要被包外的使用者进行继承,那么这个类的方法需要是 public 或 protected;private 方法无法被继承,因此也无法被重写;

初始化基类

Java 会在派生类的构造函数中,自动插入调用基类构造函数的代码;这意味着,每次创建一个派生类的对象时,也有一个基类的对象被创建出来了;这个基类对象会隐式的包含在派生类的对象中;

这意味着,如果继承的层级很深的话,实例化一个对象时,其实有一大堆对象被创建了出来;

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
class Art {
Art() {
System.out.println("Art constructor");
}
}

class Drawing extends Art {
Drawing() {
System.out.println("Drawing constructor");
}
}

public class Cartoon extends Drawing {
public Cartoon() {
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
}
/* Output:
Art constructor
Drawing constructor
Cartoon constructor
*/

当父类的构造函数需要参数时,那么需要手工调用父类的构造函数,并给它传参;

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
class Game {
Game(int i) {
System.out.println("Game constructor");
}
}

class BoardGame extends Game {
BoardGame(int i) {
super(i);
System.out.println("BoardGame constructor");
}
}

public class Chess extends BoardGame {
Chess() {
super(11);
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
}
/* Output:
Game constructor
BoardGame constructor
Chess constructor
*/

委托

还有一种复用代码的方法是使用委托,虽然 Java 并不直接支持委托,但可以借助第三方工具,来生成委托的方法;所谓的委托,其实就是将方法重新包装一下。可以根据需要,选择性的包装,用不到的就不包装;

感觉有点像是代理模式或者桥接模式

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
32
33
34
35
36
37
public class SpaceShipDelegation {
private String name;
// 此处将 controls 设置为 private,导致无法直接调用 controls
private SpaceShipControls controls =
new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// Delegated methods:
public void back(int velocity) {
controls.back(velocity);
}
public void down(int velocity) {
controls.down(velocity);
}
public void forward(int velocity) {
controls.forward(velocity);
}
public void left(int velocity) {
controls.left(velocity);
}
public void right(int velocity) {
controls.right(velocity);
}
public void turboBoost() {
controls.turboBoost();
}
public void up(int velocity) {
controls.up(velocity);
}
public static void main(String[] args) {
SpaceShipDelegation protector =
new SpaceShipDelegation("NSEA Protector");
protector.forward(100);
}
}

结合组合与继承

继承的同时,组合一些东西进来,很常用;

通常情况下,对象的销毁是由垃圾回收器完成的。但是它什么时候回收是不确定的,有时候我们需要立即回收,例如画图软件的场景,需要销毁一些画好的图形。此时我们需要手工写一些销毁的代码。此时一般会用到 try…finally 语句,以便确保销毁的工作会被执行。同时每个类中,也需要定义自己的销毁方法,以便可以调用,进行销毁;

@Override 注解虽然不写也不会影响代码的运行,但是写了后有个好处,编译器可以用它来检查重载是否成功。因为有可能返回类型或者参数类型写错,导致没有重载成功;

组合与继承的选择

认真思考新类和旧类的关系,到底是 is-a 还是 has-a 的关系;通常优先使用组合;

protected

对外部隐藏,但允许派生类的成员访问;

向上转型

继承的本质,是想表明某个新类,是旧类的某种特殊类型;

所谓的向上转型,是指将子类对象,当成父类对象来使用;向上转型是安全的,但向下转型就不太安全了;

继承虽然是 Java 的一个特色,但实际上它很少用。除非有明确的证据表明继承能够让问题变得简单化。判断的标准即是向上转型,即是否存在需要将派生类当作父类来使用的场景?如果有,那么可以考虑使用继承;

final 关键字

final 关键字通常表明这个东西是不能改变的;final 可用在三个地方,分别是数据、方法、类;

final 数据常用于表示常量;

final 参数用来表示不能改变该参数指向的对象或变量;

1
2
3
void with(final Gizmo g) { // final 参数
//-g = new Gizmo(); // 非法,g 不可重新赋值
}

final 方法表示该方法不可被子类覆盖;

如果某个方法在基类中是 private,那么它是隐式 final 的,子类对该方法的覆盖,并非真的覆盖,只是一个同名方法而已;如果子类没有覆盖,甚至连同名方法都没有;只有非 private 的方法,才能够被重写;

final 类表示该类无法被继承;

类初始化和加载

类仅在被用到时,才会被 JVM 加载并初始化;当某个子类被用到时,JVM 会先去加载基类,完成基类的初始化后,再来处理子类的初始化;

第9章 多态

多态本质是一种动态绑定,在运行时,基于输入对象的类型,判断要调用的正确方法;在编译时,编译器是不知道要传入什么类型的参数的,所以只能在运行时进行判断;

陷阱:重写 private 方法

如果某个方法或属性在基类中声明为 private,则该方法或属性对派生类是隐藏的,在内存中存在,但无法直接调用,相当于没有继承;但是可以间接访问,即调用父类的方法进行访问;

在派生类中可以起同名的方法;但是这里有个陷阱,当向上转型,即将派生类做类型转换成父类时,调用的同名方法,将会是父类的方法,而不是子类的;

陷阱:属性与静态方法

只有方法是可以多态的,属性不能多态;如果在代码中直接访问一个属性,那么在编译时,这个属性的值就已经确定下来了,不是动态绑定的;不过这种情况很少见,有两方面的原因:

  • 一般会将属性默认设置为 private,因此该属性在子类中是不可见的,默认访问父类的属性值;
  • 如果子类真的有个属性取值不同,一般也会起个不同的属性名称,以免产生混淆;
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
class Super {
public int field = 0;
public int getField() { return field; }
}

class Sub extends Super {
public int field = 1;
@Override public int getField() { return field; } // 覆盖父类的方法
public int getSuperField() { return super.field; }
}

public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub(); // 向上类型转换为父类的类型了
System.out.println("sup.field = " + sup.field + // 结果为 0,读取父类的属性
", sup.getField() = " + sup.getField()); // 结果为 1,读取子类的属性
Sub sub = new Sub();
System.out.println("sub.field = " + // 结果为 1,读取子类的属性
sub.field + ", sub.getField() = " + // 结果为 1,读取子类的属性
sub.getField() +
", sub.getSuperField() = " + // 结果为 0,读取父类的属性
sub.getSuperField());
}
}
/* Output:
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField()
= 0
*/

如果方法是 static 静态的,那么它也没有多态;

构造器和多态

构造器有点像是隐式声明的 static 方法,因此不具有多态的特征;

构造器调用顺序

在初始化子类对象时,会向上溯源,逐级调用父类的构造器;如果父类没有无参构造器,而子类也没有显式调用父类的有参构造器,那么编译时会报错;

调用顺序:

  • 加载子类
  • 加载父类
  • 初始化父类属性
  • 运行父类构造器
  • 初始化子类属性
  • 运行子类构造器

继承和清理

通常不需要手工清理对象,但是有些特殊情况下可能需要。当需要手工清理时,需要给每个类添加清理的方法,并且清理的顺序应该跟构造顺序相反;每个子类在清理的时候,需要记得调用父类的清理方法,逐级向上传递清理指令;

有时候某个成员对象可能被多个其他对象共享,此时可能需要引入引用计数来处理;可考虑使用 static 变量来记录引用的次数;

构造器中的多态方法

当基类在构造器中调用某个方法时,如果派生类覆盖了该方法,那么在实例化派生类对象时,会优先选择子类的方法来执行。但是此时子类的属性还没有初始化赋值,因此如果读取属性,会得到一个类型默认值;为了避免这种情况,基类构造器中调用的方法,最好是自身的 final 方法,这样可确保不会被覆盖,以免产生预想不到的结果;

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
32
33
34
35
36
37
class Glyph {
void draw() {
System.out.println("Glyph.draw()");
}

Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}

class RoundGlyph extends Glyph {
private int radius = 1;

RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
}

@Override
void draw() {
System.out.println("RoundGlyph.draw(), radius = " + radius);
}
}

public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}

// 输出结果如下
Glyph() before draw()
RoundGlyph.draw(), radius = 0 // 运行基类构造器时,调用了子类的新方法,但 radius 是类型默认值
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5

协变返回类型

子类覆盖父类的方法的方法,返回类型可以跟父类方法相同,也可以是父类方法返回的类型的子类型;

使用继承设计

组合比继承更加灵活,除非有确切的证据证明使用继承会让事情变得更加简单,否则优先使用组合;因为组合不仅简单,而且使用起来更加灵活,更容易应对需求变化;

由子类转成父类永远是安全的,因为父的方法不会比子类多;但是反过来就不安全了,因为子类可能存在父类没有新方法;当出现这类型的向下转换时,JVM 在解释代码并运行时,会检查代码中是否调用了父类没有的方法,如果是的话,运行时会报错;

第10章 接口

接口或者抽象类,提供了一种将接口与实现分离的结构化方法;

抽象类和方法

创建一个抽象类的目的,是为了对外展示一组通用的接口,建立某种接口规范,方便通用;

1
2
3
abstract class Basic {
abstract void unimplemented();// 抽象方法只能放在抽象类中
}

对于抽象类的派生类,它需要为基类的方法提供具体实现;如果不提供,该派生类只能当作一个抽象类,必须添加 abstract 标记,不然编译器会报错;

即便一个类中没有任何抽象方法,也可以将这个类声明为抽象类。 这样做的好处是可以避免调用者实例化这个类;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
abstract class Uninstantiable {
abstract void f();
abstract int g();
}

public class Instantiable extends Uninstantiable {
@Override
void f() {
System.out.println("f()");
}

@Override // 覆盖抽象类的方法时
int g() {
return 22;
}

public static void main(String[] args) {
Uninstantiable ui = new Instantiable();
}
}

接口创建

interface 更像是用来建立类之间的使用协议,即告知需要实现的方法名称

接口的典型应用是给类添加一个形容词,例如 Runnable, Serializable 等;

1
2
3
4
5
6
7
8
9
interface InterfaceWithDefault {
void firstMethod();
void secondMethod();

// 这里的 default 关键字很有意思,表示某个方法提供了默认实现,子类可以不实现
default void newMethod() {
System.out.println("newMethod");
}
}

多继承

Java 早期是不允许多继承的,因为多继承让 C++ 变得很复杂;但是 default 接口的引入,让 Java 实际上拥有了多继承的功能;差别在于只能继承方法,不能继承属性;所有属性仍然只能来自基类或者抽象类;

接口中的静态方法

接口中的方法默认是 public,但接口中也允许添加静态方法;或许可用来放置一些工具方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package onjava;
import java.util.*;

public interface Operations {
void execute();

static void runOps(Operations... ops) {
for (Operations op: ops) {
op.execute();
}
}

static void show(String msg) {
System.out.println(msg);
}
}

抽象类和接口

创建一个新类时,只能继承自一个抽象类,但是可以继承多个接口;

通常情况下,普通类已经够用了。因此尽量不要使用接口,也不要使用抽象类,除非有明确证据证明好处很大;当要用时,也尽量使用接口,少用抽象类;

完全解耦

多接口结合

一个派生类可以继承多个接口,反过来,它也可以向上转型为任意一个继承的接口;这样做是安全的;

如果某个类已知要做为基类,那么优先将其定义为接口

使用继承扩展接口

继承多个接口时,可能会出现命名冲突;此时没有特别好的办法,只能是起个不同的名字;

接口适配

策略设计模式:编写一个方法,接收一个方法做为参数;传入的对象,只要遵循这个接口,就是安全的

接口字段

在没 enum 类型之前,接口是放置常量的好地方;因为它的成员默认都是 final 和 public 的;

接口嵌套

可以将接口定义在类的内部;

接口和工厂方法模式

通常情况下,初始化一个对象是调用其构造器,而所谓的工厂方法,是调用其内部用于创建对象的方法,返回一个对象;这种做法的好处是将接口与实现进行分离。

使用接口背后的原因必须是真正存在不同的实现。但是一开始并非如此,所以第一时间使用接口 + 工厂方法是一种过度设计,会带来没有必要的复杂性。只有当真正出现不同实现的那么,才需要考虑是否重构和使用接口;

第11章 内部类

第12章 集合

第13章 函数式编程

第14章 流式编程

流支持

Java 早期是严格面向对象的,后来设计者为其添加流式编程范式,为了不破坏现在的接口,通过在接口中引入 default 巧妙的解决了这一问题;

Java.util 的集合类都添加了 stream() 方法,可以很方便的将集合转成流;

每个基本类型都有一个包装类,二者的相互转换叫做 box 和 unbox,即装箱和拆箱;

流创建

Stream.of() 可将一组元素转换成流

1
2
3
4
5
6
public class StreamOf() {
public static void main(String[] args) {
Stream.of(new Bubble(1), new Bubble(2), new Bubble(3))
.forEach(System.out.pringln);
}
}

每个集合都可以调用 stream 方法来生成一个流

1
2
List<Bubble> bubbles = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3));
int a = bubbles.stream().mapToInt(b -> b.i).sum());

随机数流

Random 类提供了一个生成随机数流的方法

1
2
3
4
5
Random rand = new Random(47);
rand.ints();
rand.longs();
rand.doubles();
rand.ints(10, 20); // 控制上下界

中间操作

Optional 类

当某个计算结果可能返回空值时,可以将其包装在 Optional 类中,然后通过 empty 方法判断是否为空;

便利函数

可用来处理当 Optinal 中的值不存在的情况,例如 ifPresent, orElse, 等;

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
32
33
34
35
36
37
38
39
40
41
42
// streams/Optionals.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class Optionals {
static void basics(Optional<String> optString) {
if(optString.isPresent()) // 便利函数 isPresent
System.out.println(optString.get());
else
System.out.println("Nothing inside!");
}
static void ifPresent(Optional<String> optString) {
optString.ifPresent(System.out::println);
}
static void orElse(Optional<String> optString) {
System.out.println(optString.orElse("Nada"));
}
static void orElseGet(Optional<String> optString) {
System.out.println(
optString.orElseGet(() -> "Generated"));
}
static void orElseThrow(Optional<String> optString) {
try {
System.out.println(optString.orElseThrow(
() -> new Exception("Supplied")));
} catch(Exception e) {
System.out.println("Caught " + e);
}
}
static void test(String testName, Consumer<Optional<String>> cos) {
System.out.println(" === " + testName + " === ");
cos.accept(Stream.of("Epithets").findFirst());
cos.accept(Stream.<String>empty().findFirst());
}
public static void main(String[] args) {
test("basics", Optionals::basics);
test("ifPresent", Optionals::ifPresent);
test("orElse", Optionals::orElse);
test("orElseGet", Optionals::orElseGet);
test("orElseThrow", Optionals::orElseThrow);
}
}

第19章 类型信息

RTTI,在运行时判断类型信息

面向对象编程的一个基本目的是只需操作基类,然后通过多态,即可得到预期结果,提高代码的适用性,减少代码的变更;

第25章 设计模式

所谓的设计模式,相当于解决某类问题的巧妙办法的经验总结,它的核心目的是想隔离变化。也就是说当外界出现变化时,代码需要更改的东西尽量少,降低维护的难度;将易变的事物,和不易变的事物,隔离开来;

模式可分为三大类:

  • 创建型:如何创建对象;
  • 结构型:处理对象与其他对象连接方式,以便外部出现更改时,这部分连接无须更改;
  • 行为型:封装一些通用的行为;例如迭代器

创建型

模板方法模式

这模式属于行为型,基本原理是在父类定义了整个框架,然后允许子类自定义修改其中少数几个方法,以实现使用者预期的效果;相当于设计者定了个模板,然后使用者微调一些局部,以实现想要的效果;

工厂方法模式

通常情况下,使用构造函数来创建对象;工厂方法的意思是,在类里面单独定义一个创建对象的方法,只允许调用者通过该方法创建对象,而不是直接调用构造函数来创建,而是由这个工厂方法自己去调用构造函数;这样做的好处是,子类可以改写这个方法,不同的子类,可以生产出不同类型的对象;

当初始化对象很简单,使用构造函数是没有问题的;但是如果初始化很复杂,由于构造函数不能继承,每次都需要单独编写,这有可能导致多个子类都在重复的代码;为了便于维护,可通过工厂模式,将初始化对象的代码抽离出来,实现复用,方便维护;

工厂方法还有一个好处是可以给方法编写更容易理解的名字,而不需要像构造函数一样必须跟类名相同;但感觉这点貌似有点牵强,因为子类一般名字也是有意义的;

对于调用者来说,只需要考虑根据自身需要,创建不同的子类对象,并调用它的通用方法即可;至于这些对象背后如何实现自己的方法,调用者可以不用关系;

动态工厂:使用反射,传入类型名称,查找类,加载,调用类的构建函数;这样做的好处是可以在使用的时候才加载,不用提前加载;

抽象工厂

类似工厂方法,唯一的不同是有多个工厂,不同的工厂生产不同类型的东西;

生成器模式

也叫建造者模式,builder

不使用构造函数来创建复杂的对象,而是设计一个单独的生成器函数,以便可以根据需要组合起对象;个人感觉这个东西很像是没有参数默认值的一种变通;

原型模式

为了实现克隆,但由于对象内部有些属性可能是私有的,所以从外部克隆不可行;可以让对象自己实现一个克隆的方法,供别人调用,这样就可以避开私有的问题了;

结构型

代理模式和桥接模式

代理模式顾名思义,就是创建一个二房东出来,将一房东封装起来,二房东自己跟一房东谈,其他房客都跟二房东谈;这样的好处是如果房东有什么变化,变化只局限于两个房东之间,房客不需要变更;

行为型

状态模式

某个场景可能拥有多种状态,在不同状态下,需要采取不同的行为表现;传统的方式是写一堆 if else 来做条件判断,这是可行的。只是如果状态真的很多的话,维护起来会比较麻烦;

状态模式的解决方案,是将每种状态和关联的行为单独抽象出来成为一个对象,然后场景根据当前的状态,关联不同状态对象;场景调用该对象的方法,实现不同的行为表现;为了让场景的调用变得简单,这些对象需要遵循接口规范,需要方法的命名都一样,这样才方便场景的调用;调用后,如果状态发生了改变,那么状态对象还会改变场景的状态值,以便让其关联新的状态对象;

这个模式非常适用于有限状态机的场景,但在实际的业务中,貌似还没有遇到过;

策略模式

感觉跟状态模式差不同,唯一的区别是策略模式不改变上下文的状态;

责任链模式

将一大堆处理拆分一下,每个处理步骤单独抽象成一个对象,该对象只负责一小部分职责;如果通过,就将任务传递给后面步骤的对象;

适配器模式

不同的数据来源,用一个适配器,将它们转换成统一的格式,再传递给后面的对象处理;

外观模式

整合很多复杂的功能和接口,将它们统一起来,对外只提供一个简单的接口,隐藏后端的复杂性,个人感觉跟 HTTP API 接口的理念类似;

观察者模式

就是 pub/sub,整个列表,将感兴趣的对象添加进去;当事件发生时,逐个通知这些对象;

访问者模式

添加新功能的一种办法,是给对象添加新的方法;还有一种方法是将对象做为参考传递给一个新方法,让新方法能够访问对象中的数据就可以了;


Java 编程思想
https://ccw1078.github.io/2022/07/19/Java 编程思想/
作者
ccw
发布于
2022年7月19日
许可协议