Java 核心技术

把书中一些自己不熟悉或者自己认为比较重要的知识点,做个简单的记录备忘。

第 3 章 Java 的基本程序设计结构

浮点数的科学表示法

1
2
3
4
5
6
7
# 以 10 为底,尾数、指数都是十进制
1.36e2 = 1.36E2 = 1.36 * 10^2 = 136


# 以 2 为底,尾数是 16 进制,指数是十进制
0x11p3 = 0x11P3 = 0x11 * 2^3 = 17 * 8 = 136
0x1ap-1 = 0x1aP-1 = 0x1a * 2^-1 = 26 * 1/2 = 13

/floorDiv%floorMod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(当 `/` 的操作数为整型时)

`floorDiv` that returns the integer less than or equal to the quotient
and the `/` operator that returns the integer closest to zero.


`/` 返回的结果与实际的商相比更接近 0,即满足: abs(结果) <= abs(实际的商)
`floorDiv` 返回的结果小于等于实际的商。

Examples:
floorMod(12, 5) == 2, (12 / 5) == 2
floorMod(-12, -5) == 2, (-12 / -5) == 2

(-12.0 / 5 = 12.0 / -5 = -2.4)
floorMod(-12, 5) == -3, (-12 / 5) == -2
floorMod(12, -5) == -3, (12 / -5) == -2


x % y = x - x / y * y
x / y * y + x % y == x

florMod(x, y) = x - (floorDiv(x, y) * y)
floorDiv(x, y) * y + floorMod(x, y) == x

数值类型之间的转换

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
                      float -------> double
^ -- -- ^
| |\ / | |
. ./ .
| / \ |
. / . .
byte ----> short ----> int --------> long
^
|
|
char

无损转换 可能丢失精度

--> -.-.>

^ ^
| |
| .
| |
| .


丢失精度:
int -> float
long -> float
long -> double

位运算

移位运算符

  • 左操作数是 int 类型,右操作数模 32 。例如,1 << 35 的值等同于 1 << 38

  • 左操作数是 long 类型,右操作數要模 64。例如 5L << 35 的值是 1717986918405L << 65 的值等于 5L << 110

>>> 运算符会用 0 填充高位, 这与 >> 不同, 它会用符号位填充高位。 不存在 <<< 运算符。

在 C/C++ 中,不能保证 >> 是完成算术移位(扩展符号位)还是逻辑移位(填充 0) 。实现者可以选择其中更高效的任何一种做法。 这意味着 C/C++ >> 运算符对于负数生成的结果可能会依赖于具体的实现。 Java 则消除了这种不确定性。

字符

在 Java 中, char 类型描述了 UTF-16 编码中的一个代码单元。

字符串

Java 中的 String 是不可变对象。

  • 优点:编译器可以让字符串共享。

    • 读取 / 比较字符串修改字符串 多。
    • 拼接字符串 可以使用 StringBuilder(线程不安全,效率高) 、 StringBuffer (线程安全,效率低)。
  • 缺点:只修改字符串中的某些字符时不如 C/C++ 灵活。

1
2
3
4
5
6
7
8
9
10
11
// Java
String s = "a string";
System.out.println(s);

// 拼接字符串
s = "one part" + "." + "another part";
System.out.println(s);

// 修改某个字符后,生成了新字符串
s = s.replace(".", ",");
System.out.println(s);
1
2
3
4
5
6
7
8
9
10
11
12
13
// C++
std::string s = "a string";
std::cout << s << std::endl;

std::string a = "one part";
std::string b = ".";
std::string c = "another part";
s = a + b + c;
std::cout << s << std::endl;

// 在原字符串上修改
s.replace(a.length(), b.length(), ",");
std::cout << s << std::endl;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// C
char *s = "a string";
puts(s);

// 拼接字符串,比较麻烦
char *a = "one part";
char *b = ".";
char *c = "another part";
int al = strlen(a);
int bl = strlen(b);
int cl = strlen(c);
char *tmp = new char[al + bl + cl + 1];
strncpy(tmp, a, al);
strncpy(tmp + al, b, bl);
strncpy(tmp + al + bl, c, cl);
tmp[al + bl + cl] = '\0';

s = tmp;
puts(s);

// 直接修改某个字符
s[al] = ',';
puts(s);
1
2
3
4
// output
a string
one part.another part
one part,another part

比较字符串使用 equalsequalsIgnoreCasecompareTo,不能用 ==(比较的是两个对象指向的地址是否相同)。

Java 中对字符串的 + 运算进行了重载,却没有对 == 进行重载。

  • 如果重载了 == ,那包括字符串在内的所有对象都将用 == 来判断是否相等。
  • 像现在 == 用来判断指向的地址是否,其实很少用?

获取长度

  • length 获取字符串中有多少个 char,即字符串用 UTF-16 编码时,需要的单元数量(一个单元 2 个字节)
  • codePointCount 获取字符串的实际长度,即字符串中有多少个字符 / Unicode 码点。

码点、字符集、字符编码的一些概念见 0518 字符集和字符编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String s = "\uD835\uDD46";
// 𝕆(U+1D546) 需要两个代码单元

System.out.println("s=" + s);
System.out.println("length=" + s.length());
System.out.println("codePointCount=" + s.codePointCount(0, s.length()));
for (int i = 0; i < s.length(); i++) {
System.out.println(i + ":" + s.charAt(i) + ", " + (int)s.charAt(i));
}
int[] codePoints = s.codePoints().toArray();
for (int i = 0; i < codePoints.length; i++) {
System.out.println(i + ":" + codePoints[i]);
}
/*
s=𝕆
length=2
codePointCount=1
0:?, 55349
1:?, 56646
0:120134
*/

尽量避免使用 char 类型和 String 的 charAt 方法,因为它们太底层了。

控制台输入

  • Scanner
1
2
3
4
Scanner in = new Scanner(System.in);
in.next();
in.nextInt();
...
  • Console ,输入是不可见的

    如果有可能进行交互操作,就通过控制台窗口为交互的用户返回一个 Console 对象, 否则返回 null。对于任何一个通过控制台窗口启动的程序都可使用 Console 对象 。否则, 其可用性将与所使用的系统有关。

1
2
3
Console cons = System.console();
String username = cons.readLine("Username: ");
char[] passwd = cons.readPassword("Password: ");

格式化字符串

复用参数的方法

  • 采用一 个格式化的字符串指出要被格式化的参数索引。索引必须紧跟在 % 后面, 并以$ 终止。 参教索引值从 1 开始 。
1
2
System.out.printf("a=%1$04d b=%2$s c=%3$.2f d=%2$s\n", 12, "STR", 9.87);
//a=0012 b=STR c=9.87 d=STR
  • 使用 < 标志,它指示前面格式说明中的参数将被再次使用
1
2
System.out.printf("a=%1$04d b=%2$s b=%<s c=%3$.2f c=%<.4f\n", 12, "STR", 9.87);
//a=0012 b=STR b=STR c=9.87 c=9.8700

第 4 章 对象与类

类之间的关系

  • 依赖 uses-a
  • 聚合 has-a
  • 继承 is-a

UML

static

Java 中的静态域与静态方法在功能上与 C++ 相同。但是, 语法书写上却稍有所不同。在 C++ 中, 使用 :: 操作符访问自身作用域之外的静态域和静态方法, 如 Math::PI

术语 static 有一段不寻常的历史:

  • 起初, C 引入关键字 static 是为了表示退出一个块后依然存在的局部变量。在这种情况下, 术语 static 是有意义的:变量一直存在 ,当再次进入该块时仍然存在。
  • 随后,static 在 C 中有了第二种含义,表示不能被其他文件访问的全局变量和函数。 为了避免引入一个新的关键字, 关键字 static 被重用了。
  • 最后,C++ 第三次重用了这个关键字, 与前面赋予的含义完全不一样, 这里将其解释为:属于类且不属于类对象的变量和函数。 这个含义与 Java 相同。

方法参数 call by …

call by 描述各种程序设计语言(不只是 Java) 中方法参数的传递方式:

  • 按值调用 (call by value) 表示方法接收的是调用者提供的值。
  • 按引用调用 (call by reference) 表示方法接收的是调用者提供的变量地址。
  • 按名调用 (call by name),Algol 程序设计语言是最古老的高级程序设计语言之一, 它使用的就是这种参数传递方式。不过,这种传递方式已经成为历史。

一个方法可以修改传递引用所对应的变量值, 而不能修改传递值调用所对应的变量值。
Java 程序设计语言总是采用按值调用。对象引用是按值传递的。

  • 基本数据类型:形参改变,不影响实参。
  • 对象引用:
    • 同样有“形参改变,不影响实参”,即将形参引用了其他对象,实参还是引用原来的对象。
    • 但是形参初始时和实参的值相等,即引用了相同的对象,通过形参对被引用的对象进行修改,通过实参来访问被引用对象时,相应的值也会变化。

Java 中方法对参数的影响

  • 一个方法不能修改一个基本数据类型的参数 (即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

C++ 中有值调用和引用调用。引用参数使用 &,可以达到在函数中改变参数所引用的对象。

变量初始化

默认值

  • 局部变量必须初始化后才能使用
  • 类的变量,如果没有初始化,会有默认值(0, false, null

如何初始化

  • 在构造器中设置值
  • 在声明中赋值
  • 在初始化块中赋值
1
2
3
4
5
6
7
8
9
10
class Cls {
{
// 对象初始化块
// 每次创建一个对象,都会执行该代码块
}
static {
// 静态初始化块
// 仅在类第一次加载时,执行
}
}

调用构造器的处理过程

  • 1 ) 所有数据域被初始化为默认值 (0、 false 或 null 。)
  • 2 ) 按照在类声明中出现的次序, 依次执行所有域初始化语句和初始化块
  • 3 ) 如果构造器第一行调用了第二个构造器, 则执行第二个构造器主体
  • 4 ) 执行这个构造器的主体

this

  • 类实例方法:引用方法的隐式参数
1
2
3
4
5
6
7
class Cls {
private int a;
public setA(int a) {
// this 隐式参数,调用 setA 方法的对象
this.a = a;
}
}
  • 构造函数的第一条语句:调用同一个类的其他构造函数
1
2
3
4
5
6
7
8
9
class Cls {
Cls() {
this("a", 3);
//...
}
Cls(String a, int b) {
//...
}
}

查找类文件

假设类路径(class path)设置为 /home/user/classdir:.:/home/user/archives/archives.jar

当要查找 com.domain.core.NewClass 类文件时

Java 虚拟机

  • jre/ lib 和 jre/lib/ext 目录下的归档文件中所存放的系统类文件
  • /home/user/classdir/com/domain/core/NewClass.class
  • 从当前目录开始 com/domain/core/NewClass.class
  • com/domain/core/NewClass.class inside /home/user/archives/archive.jar

编译器

  • 确认导入的包中,是否有多个地方包含了类文件
  • 检查源文件是否比类文件新,是否需要重新编译源文件生成类文件

文档注释

/** 开始,以 */ 结束的多行注释。

每一行前面的 * 不是必须的。

注释采用自由格式文本(free-form text):

  • HTML 修饰符

    • 强调 <em>...</em>
    • 着重强调 <strong>…</strong>
    • 图像 <img…>
    • 等宽代码 {@code …}
  • 分类

    • 类注释

    • 方法注释

      • 变量描述 @param
      • 返回值描述 @return
      • 异常描述 @throws
    • 域注释

      只需要对公有域 (通常指的是静态常量)建立文档

    • 通用注释

      • 通用标记:以 @ 开头
        • @author
        • @version
        • @since
        • @deprecated
        • @see / @link
    • 包与概述注释

第 5 章 继承

一个对象变量可以指示多种实际类型的现象被称为多态 (polymorphism)。

在运行时能够自动地选择调用哪个方法的现象称为动态绑定 (dynamic binding)。

  • 在 Java 中,不需要将方法声明为虚拟方法。动态绑定是默认的处理方式。如果不希望让一个方法具有虚拟特征 , 可以将它标记为 final 。
  • 在 C++ 中需要声明虚函数。

动态绑定的限制

  • 子类覆盖方法的返回值类型必须是超类方法的返回值的子类型(包括相同)。

  • 覆盖方法的时候,子类方法不能低于超类方法的可见性。

final

  • 修饰类:类不可被继承,类中的方法都成为 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
38
#include <cstdio>

class A {
public:
void outA() {
puts("A.outA");
}
virtual void outA2() {
puts("A.outA2");
}
};

class B: public A {
public:
void outA() {
puts("B.outA");
}
void outA2() {
puts("B.outA2");
}
void outB() {
puts("B.outB");
}
};

int main() {

A* a = new B();
a->outA();
a->outA2();
((B*)a)->outB();
return 0;
}

///
A.outA
B.outA2
B.outB

访问修饰符

  • public 所有类可见
  • protected 本包和所有子类可见
  • ( ) default 本包可见
  • private 仅本类可见

Java 实现 equals 的一些基本要求

  • 自反性:对于任何非空引用 xx.equals(x) 应该返回 true
  • 对称性:对于任何引用 xy,当且仅当 x.equals(y) 返回 true,y.equals(x) 也应该返回 true
  • 传递性:对于任何引用 xyz,如果 x.equals(y) 返回 true,且 y.equals(z) 返回 true,则 x.equals(z) 也应该返回 true
  • 一致性:如果 xy 引用的对象没有发生变化,反复调用 x.eqals(y) 应该返回同样的结果
  • 对于任何非空引用 xx.equals(null) 应该返回 false
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
public class TestExtends {
public static void main(String[] args) {
A a = new B();
a.outA();
((B) a).outB();
System.out.println((a instanceof A) + "");
System.out.println((a instanceof B) + "");
System.out.println(a.getClass() == A.class);
System.out.println(a.getClass() == B.class);
}
}

class A {
public void outA() {
System.out.println("A.outA");
}
}

class B extends A {
@Override
public void outA() {
System.out.println("B.outA");
}

public void outB() {
System.out.println("B.outB");
}
}
//
B.outA
B.outB
true
true
false
true

如何写 equals(Object otherObject)

  1. 显式参数命名为 otherObject, 稍后需要将它转换成另一个叫做 other 的变量。

  2. 检测 this 与 otherObject 是否引用同一个对象:

    if (this = otherObject) return true;

    这条语句只是一个优化。 实际上, 这是一种经常采用的形式。 因为计算这个等式要比一 个一个地比较类中的域所付出的代价小得多。

  3. 检测 otherObject 是否为 null , 如果为 null , 返回 false。 这项检测是很必要的。

    if (otherObject = null) return false;

  4. 比较 this 与 otherObject 是否属于同一个类。

    • 如果 equals 的语义在每个子类中有所改变, 就使用 getClass 检测:

    if (getClass() != otherObject.getCIassO) return false;

    • 如果所有的子类都拥有统一的语义, 就使用 instanceof 检测:

    if (!(otherObject instanceof ClassName)) return false;

  5. 将 otherObject 转换为相应的类类型变量:

    ClassName other = (ClassName) otherObject

  6. 现在开始对所有需要比较的域进行比较了。使用 == 比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配,就返回 true; 否则返回 false。

    1
    2
    return fieldl == other.field
    && Objects.equa1s(fie1d2, other.field2)
  7. 如果在子类中重新定义 equals, 就要在其中包含调用 super.equals(other)

函数可变个数参数

  • 可变个数参数,应该是参数列表的最后一个参数
  • 在函数中引用这些可变个数的参数时,以数组的形式来引用
  • 除了传递不定个数的参数外,也可以用数组来传递给可变个数参数
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
static void multipart(String s, int a, Object... values) {
System.out.println("s=" + s);
System.out.println("a=" + a);
System.out.println("values.getClass=" + values.getClass());
System.out.println("values instanceOf Object[] =" + (values instanceof Object[]));
System.out.println(Arrays.toString(values));
System.out.println();
}
multipart("string", 10, "one", 2, "three", 4.0);
multipart("string", 10, new Object[]{"one", 2, "three", 4.0});
multipart("string", 10);
///
s=string
a=10
values.getClass=class [Ljava.lang.Object;
values instanceOf Object[] =true
[one, 2, three, 4.0]

s=string
a=10
values.getClass=class [Ljava.lang.Object;
values instanceOf Object[] =true
[one, 2, three, 4.0]

s=string
a=10
values.getClass=class [Ljava.lang.Object;
values instanceOf Object[] =true
[]

枚举

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
public enum ProgramLanguage {
CPP("C++", 1),
JAVA("Java", 2);

private String lang;
private int type;

ProgramLanguage(String lang, int type) {
this.lang = lang;
this.type = type;
}

public static ProgramLanguage valueOf(int type) {
switch (type) {
case 1:
return CPP;
case 2:
return JAVA;
default:
return null;
}
}
}

ProgramLanguage language;
language = ProgramLanguage.CPP;
System.out.println(language);
language = ProgramLanguage.valueOf(1);
System.out.println(language);
language = ProgramLanguage.valueOf("CPP");
System.out.println(language);
language = Enum.valueOf(ProgramLanguage.class, "CPP")

Class 类对象

程序运行期间, Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。 这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。

然而,可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class, 这个名字很容易让人混淆。

1
2
3
4
5
6
7
8
ClassName obj = new ClassName();

1.
obj.getClass();
2.
Class.forName(ClassName);
3.
ClassName.class

一个 Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类。 例如, int 不是类, 但 int.class 是一个 Class 类型的对象。

在启动时, 包含 main 方法的类被加载。 它会加载所有需要的类。 这些被加栽的类又要加载它们需要的类。

反射

java.lang.reflect

  • Field:域

    • getName
    • getModifiers
    • getType
  • Method:方法

    • getName

    • getModifiers

    • getParameterTypes

    • getReturnType

  • Constructor:构造器

    • getName

    • getModifiers

    • getParameterTypes

java.lang.class

  • getFields
  • getMethods
  • getConstructors

第 6 章 接口、lambda 表达式与内部类

接口

  • 方法自动被设置为 public
  • 域自动被设置为 public static final
  • JDK 8,可以有静态方法
  • 可以为接口提供一个默认实现,使用 default
    • 接口中有很多个方法,可以都有默认实现。使用接口时,只需要覆盖其中关心的方法即可,无需每个方法都手动写一个“空实现”
    • 接口增加方法后,原有的实现类不用实现新方法,仍可使用

标记接口 tagging interface / 记号接口 marker interface

标记接口不包含任何方法;它唯一的作用就是允许在类型查询中使用 instanceof。比如 Cloneable。
clone 方法中,调用域的 clone 方法时,需要判断相应的域是否为 null。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package test.zz;

/**
* TestClone
*
* @author zz
* @date 2018/5/28
*/
public class TestClone {
public static void main(String[] args) throws CloneNotSupportedException {
Clone2 clone2 = new Clone2();
Clone2 clone21 = clone2.clone();
}
}

class Addr implements Cloneable {
@Override
public Addr clone() throws CloneNotSupportedException {
System.out.println("Addr.clone - 1");
return (Addr) super.clone();
}

}

class Clone1 implements Cloneable {
private String name;
private Addr addr;

Clone1() {

}

Clone1(String name) {
this.name = name;
this.addr = new Addr();
}

@Override
public String toString() {
return "{ name:" + name + ", addr:" + addr + " }";
}

@Override
public Clone1 clone() throws CloneNotSupportedException {

System.out.println("Clone1.clone - 1");
Clone1 clonee = (Clone1) super.clone();

clonee.addr = addr == null ? null : addr.clone(); //////// !!!!!!!!!!!
System.out.println("Clone1.clone - 2");
return clonee;
}
}

class Clone2 extends Clone1 implements Cloneable {
private Clone1 clone1;


@Override
public Clone2 clone() throws CloneNotSupportedException {
System.out.println("Clone2.clone - 1");
return (Clone2) super.clone();
}
}

所有数组类型都有一个 public 的 clone 方法, 而不是 protected: 可以用这个方法 建立一个新数组, 包含原数组所有元素的副本。

lambda 表达式

lambda 表达式是一个可传递的代码块, 可以在以后执行一次或多次。

带参数变量的表达式就被称为 lambda 表达式。

形式上

参数,箭头,表达式

(params…) -> { code block }

  • 括号
    • 参数仅有一个,且类型可被自动推导出来,可省略括号和类型
    • 参数不是一个(包括 0 个),不能省略括号
    • 若类型可被自动推导出来,可省略。
  • 代码块
    • 仅有一条语句,可省略大括号
  • 返回值
    • 代码块中的最后一条语句会作为返回值返回(无需显式使用 return)
    • 如果代码块中存在分支(if else),每个分支都必须有返回值
    • 返回值类型会自动推导,无需指定
定义上
  • 一个代码块
  • 参数
  • 自由变量的值,非参数而且不在代码块中定义的变量

lambda 表达式中捕获的变量必须实际上是最终变量 ( effectively final 。) 实际上的最终变量是指, 这个变量初始化之后就不会再为它赋新值。

函数式接口

对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达 式。 这种接口称为函数式接口 (functional interface )。

lambda 表达式最终会被转换为函数式接口。本质上是函数式接口的简写,一个语法糖。

使用
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package test.zz;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
* TestTimer
*
* @author zz
* @date 2018/5/28
*/
public class TestTimer {
public static void main(String[] args) throws InterruptedException {

ActionListener listener;
Timer timer;

listener = new TimerPrinter();
timer = new Timer(1000, listener);
timer.start();
Thread.sleep(5000);
timer.stop();

listener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("[[2]] actionPerformed.event=" + e);
}
};
timer = new Timer(1000, listener);
timer.start();
Thread.sleep(5000);
timer.stop();

listener = event -> {
System.out.println("[[3]] actionPerformed.event=" + event);
};

timer = new Timer(1000, listener);
timer.start();
Thread.sleep(5000);
timer.stop();

}
}

interface Func {
void test(String s, int b);
}

class TimerPrinter implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {

System.out.println("[[1]] actionPerformed event=" + event);
}
}


////////
[[1]] actionPerformed event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571207103,modifiers=] on javax.swing.Timer@266267b2
[[1]] actionPerformed event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571208108,modifiers=] on javax.swing.Timer@266267b2
[[1]] actionPerformed event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571209112,modifiers=] on javax.swing.Timer@266267b2
[[1]] actionPerformed event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571210113,modifiers=] on javax.swing.Timer@266267b2
[[2]] actionPerformed.event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571212097,modifiers=] on javax.swing.Timer@225de7a6
[[2]] actionPerformed.event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571213101,modifiers=] on javax.swing.Timer@225de7a6
[[2]] actionPerformed.event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571214104,modifiers=] on javax.swing.Timer@225de7a6
[[2]] actionPerformed.event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571215106,modifiers=] on javax.swing.Timer@225de7a6
[[3]] actionPerformed.event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571217104,modifiers=] on javax.swing.Timer@3b912d4a
[[3]] actionPerformed.event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571218108,modifiers=] on javax.swing.Timer@3b912d4a
[[3]] actionPerformed.event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571219111,modifiers=] on javax.swing.Timer@3b912d4a
[[3]] actionPerformed.event=java.awt.event.ActionEvent[unknown type,cmd=null,when=1527571220114,modifiers=] on javax.swing.Timer@3b912d4a

这里参数的本质是一个接口,有几种方式实现:

  • 定义一个类实现该接口,实例化一个该类的对象作为参数
  • 定义一个匿名类,直接在参数处定义该类
  • 当接口中仅有一个抽象方法时,可使用 lambda 表达式的形式

有时候,并不需要定义新的 lambda 表达式,而是使用已有的类方法。

  • object::instanceMethod
  • Class::static Method
  • Class::instanceMethod
1
2
3
4
5
6
7
aString::compareTo == x -> aString.compareTo(x)
this::equals == x -> this.equals(x)

System.out::prinrln == x -> System.out.println(x)
Math::pow == (x, y) -> Math.pow(x, y)

String::equals == (x, y) -> x.equals(y)
函数式接口

只有一个抽象方法的接口,使用 @FunctionalInterface 标记

  • 如果无意中增加了一个非抽象方法,编译器会产生一个错误消息
  • javadoc 文档会指出这个接口是一个函数式接口

第 7 章 异常、断言和日志

捕获还是抛出异常

  • 知道如何处理异常,就应该捕获
  • 不知道怎样处理,就抛出异常继续传递
  • 子类方法抛出的异常不能超过父类方法声明的范围

异常继承

1
2
3
4
5
6
7
8
9
10
              Throwable
|
+------------+------------+
| |
Error Exception
(uncheckd exception) |
+------------+------------+
| |
RuntimeException other excpetion
(uncheckd) (checked)

第 8 章 泛型程序设计

<T extends BoundingType>表示 T 应该是绑定类型的子类型(subtype)。T 和绑定类型可以是类, 也可以是接口。选择关键字 extends 的原因是更接近子类的概念, 并且 Java 的设计者也不打算在语言中再添加一个新的关键字(如 sub)。

一个类型变量或通配符可以有多个限定, 例如: T extends Comparable & Serializable。限定类型用“ & ” 分隔, 而逗号用来分隔类型变量。

在 Java 的继承中, 可以根据需要拥有多个接口超类型, 但限定中至多有一个类。 如果用 一个类作为限定, 它必须是限定列表中的第一个。

Java 泛型转换的事实:

  • 虚拟机中没有泛型, 只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成来保持多态。
  • 为保持类型安全性, 必要时插人强制类型转换。

消除编译器警告

  • @SuppressWamings("unchecked")
  • @SafeVarargs
1
2
3
4
5
6
class Pair<T> {
T first;
T second;

// get & set
}
  • Manager Employee 的子类

  • Pair<Manager> 不是 Pairt<Employee> 的子类

  • Piar<Manager>Piar<Employee> Piar<? Extends Employee> 的子类

  • 泛型最基本的用法 <T> void func(List<T> list)

  • 为什么要有 <T extends Employee> void func(List<T> listvoid func(List<? extends Employee)

    • 以便使用 Employee 的方法
    • Employee 的子类 Manager 对应的 List<Manager> 也能作为参数
    • 带有子类型限定的通配符可以从泛型对象读取。
  • 为什么要有 void func(List<? super Employee> list)

  • 带有超类型限定的通配符可以向泛型对象写入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ok
Pair<? extends Employee> employeePair = new Pair<Manager();

// ok
Employee employee = employeePair.getFirst();

// error: setFirst(? extends Employee) 不能确定使用哪个
// 编译器只知道需要某个 Employee 的子类型,但不知道具体是什么类型。 它拒绝传递任何特定的类型。
employeePair.setFirst(new Manager());


// --

// ok
Pair<? super Manager> managerPair = new Pair<Manager();

// ok
Object object = managerPair.getFirst();

// ok
managerPair.setFirst(new Manager());

第 9 章 集合

Iterator

​ next

​ hasNext

ListIterator extends Iterator

​ add

​ remove

add 方法只依赖于迭代器的位置, 而 remove 方法依赖于迭代器的状态。

  • LinkedHashMap:LinkedHashMap 相比 HashMap,额外多了一个链表“按照一定的顺序”记录 Map 中的 key
    • LinkedHashMapcK, V>(initialCapacity, loadFactor, false)
      • false:按照插入顺序
    • LinkedHashMapcK, V>(initialCapacity, loadFactor, true)
      • true:按照访问顺序

LinkedHashMap 调用 put/putAll 插入新元素后调用 removeEldestEntry。如果 removeEldestEntry 返回 true,则会删除该元素。可以利用这个实现自己的子类来达到实现一个 LRU 的缓存。

1
2
3
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}

视图

Arrays/Collections 构造一个视图,在视图上的修改会反映在原始集合/数组上。

1
2
3
4
5
6
7
8
9
String[] array = new String[]{"a", "b", "c"};
List<String> list = Arrays.asList(array);

System.out.println(Arrays.toString(array));
System.out.println(list);

list.set(0, "new");
System.out.println(Arrays.toString(array));
System.out.println(list);
1
2
3
4
[a, b, c]
[a, b, c]
[new, b, c]
[new, b, c]