博学谷-零基础大数据在线就业班-第一阶段 java基础

博学谷-零基础大数据在线就业班-第一阶段 java基础

第一章 java基础语法

环境搭建+入门

java语言背景介绍

java语言跨平台原理

JRE和JDK

image-20231112235606219

JDK的下载和安装

常用DOS命令

PATH环境变量的配置

HelloWorld案例

1
2
编译 javac xxx.java
运行 java xxx // 不需要后缀zhui .class
1
2
类名前有public时,类名必须与文件名一致
一个程序必须有入口方法main

Notepad软件的安装和使用

1
2
3
高级的记事本,方便代码编写(有行号)

配置默认代码语言和编码ascII

注释

1
2
3
单行 //
多行 /* */
文档注释 /** */

关键字

1
2
3
4
5
6
7
8
9
关键字全小写
常用代码编辑器里关键字有特殊颜色

public
class
void
static

main不是关键字

数据类型及转换

常量

1
2
3
4
5
6
字符串常量
整数常量
小数常量
字符常量
布尔常量
空常量null

变量介绍

数据类型

image-20231113012013098

1
2
3
4
5
整数    字节   范围
byte 1
short 2
int(默认) 4 建议
long 8
1
2
3
浮点数   字节   范围
float 4
double(默认) 8 建议
1
2
字符    字节   范围
char 2
1
2
布尔    字节   范围
boolean 1

变量的定义和使用

变量的注意事项

1
2
3
4
5
6
7
同名变量不可以重复定义
一条语句可以定义多个变量,用逗号隔开
变量在使用之前一定要赋值
定义float变量类型时,需要在数值后面加入F或f
float n = 12.5f;
定义long变量类型时,需要在数值后面加入L或l
定义的变量只在所在的大括号内有效(当这个大括号执行完毕后,内存中这个变量会消失)

运算符

条件控制语句

循环

随机数+开发工具

数组

方法与debug

进制

二维数组

第二章 面向对象基础

第三章 API基础

StringBuilder

1
2
3
4
5
6
7
8
9
10
字符串串接,在次数多的情况下(如循环)会很占内存(生成临lin时对象)。StringBuilder可以解决,不会生成临时对象。

它的方法可以实现链式操作。核心就是方法执行完返回this,所以自己也可以实现类似si的类。

提高字符串操作效率(50000次字符串拼接,string用了2939ms,StringBuilder用了264ms)
可变字符串类,可以视为一个容器

来自于java.lang. 不需要手动导入

直接打印的结果是容器里的内容,不是地址

常用构造方法

public StringBuilder()
1
默认创建一个空字符对象
public StringBuilder(String str)
1
默认创建一个以传入字符串为基础的对象

常用成员方法

append
1
接受任意数据类型,返回对象本身(可以链式调用)
reverse
1
元素内容反转 返回对象本身
length
toString

StringBuilder如何提高的效率

image-20231112084743115

image-20231112085222322

案例

1
2
字符串反转
打印数组的样子

StringBuffer(廖雪峰)

1
2
3
这是Java早期的一个StringBuilder的线程安全版本,它通过同步来保证多个线程操作StringBuffer也是安全的,但是同步会带来执行速度的下降。

StringBuilder和StringBuffer接口完全相同,现在完全没有必要使用StringBuffer

StringJoiner(廖雪峰)

1
2
3
4
5
6
7
类似用分隔符拼接数组的需求
var sj = new StringJoiner(", ");
var sj = new StringJoiner(", ", "Hello ", "!");

String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);
在不需要指定“开头”和“结尾”的时候,用String.join()更方便

包装类型(廖雪峰)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
引用类型可以赋值为null,表示空,但基本类型不能赋值为null

那么,如何把一个基本类型视为对象(引用类型)?

实际上,因为包装类型非常有用,Java核心库为每种基本类型都提供了对应的包装类型

基本类型 对应的引用类型
boolean java.lang.Boolean
byte java.lang.Byte
short java.lang.Short
int java.lang.Integer
long java.lang.Long
float java.lang.Float
double java.lang.Double
char java.lang.Character
1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
int i = 100;
// 通过new操作符创建Integer实例(不推荐使用,会有编译警告):
Integer n1 = new Integer(i);
// 通过静态方法valueOf(int)创建Integer实例:
Integer n2 = Integer.valueOf(i);
// 通过静态方法valueOf(String)创建Integer实例:
Integer n3 = Integer.valueOf("100");
System.out.println(n3.intValue());
}
}
1
2
3
4
5
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();

Integer.valueOf()就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。

进制转换

1
2
3
4
Integer类本身还提供了大量方法,例如,最常用的静态方法parseInt()可以把字符串解析成一个整数:

int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析
1
2
3
4
5
6
7
8
9
10
11
Integer还可以把整数格式化为指定进制的字符串:

public class Main {
public static void main(String[] args) {
System.out.println(Integer.toString(100)); // "100",表示为10进制
System.out.println(Integer.toString(100, 36)); // "2s",表示为36进制
System.out.println(Integer.toHexString(100)); // "64",表示为16进制
System.out.println(Integer.toOctalString(100)); // "144",表示为8进制
System.out.println(Integer.toBinaryString(100)); // "1100100",表示为2进制
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
Java的包装类型还定义了一些有用的静态变量

// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;

// int可表示的最大/最小值:
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648

// long类型占用的bit和byte数量:
int sizeOfLong = Long.SIZE; // 64 (bits)
int bytesOfLong = Long.BYTES; // 8 (bytes)
1
2
3
4
5
6
7
8
9
10
最后,所有的整数和浮点数的包装类型都继承自Number,因此,可以非常方便地直接通过包装类型获取各种基本类型:

// 向上转型为Number:
Number num = new Integer(999);
// 获取byte, int, long, float, double:
byte b = num.byteValue();
int n = num.intValue();
long ln = num.longValue();
float f = num.floatValue();
double d = num.doubleValue();

处理无符号整型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
在Java在Java中,并没有无符号整型(Unsigned)的基本数据类型。byte、short、int和long都是带符号整型,最高位是符号位。而C语言则提供了CPU支持的全部数据类型,包括无符号整型。无符号整型和有符号整型的转换在Java中就需要借助包装类型的静态方法完成。

例如,byte是有符号整型,范围是-128~+127,但如果把byte看作无符号整型,它的范围就是0~255。我们把一个负的byte按无符号整型转换为int:中,并没有无符号整型(Unsigned)的基本数据类型。byte、short、int和long都是带符号整型,最高位是符号位。而C语言则提供了CPU支持的全部数据类型,包括无符号整型。无符号整型和有符号整型的转换在Java中就需要借助包装类型的静态方法完成。

例如,byte是有符号整型,范围是-128~+127,但如果把byte看作无符号整型,它的范围就是0~255。我们把一个负的byte按无符号整型转换为int:

public class Main {
public static void main(String[] args) {
byte x = -1;
byte y = 127;
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
}
}
1
2
3
因为byte的-1的二进制表示是11111111,以无符号整型转换后的int就是255。

类似的,可以把一个short按unsigned转换为int,把一个int按unsigned转换为long。

Auto Boxing

1
2
3
4
5
Java编译器可以帮助我们自动在int和Integer之间转型:

Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()
这种直接把int变为Integer的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer变为int的赋值写法,称为自动拆箱(Auto Unboxing)。
1
2
3
4
5
6
7
8
9
10
注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。

装箱和拆箱会影响代码的执行效率,因为编译后的class代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException:

public class Main {
public static void main(String[] args) {
Integer n = null;
int i = n;
}
}

不变类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
所有的包装类型都是不变类。我们查看Integer的源码可知,它的核心代码如下:

public final class Integer {
private final int value;
}
因此,一旦创建了Integer对象,该对象就是不变的。

public class Main {
public static void main(String[] args) {
Integer x = 127;
Integer y = 127;
Integer m = 99999;
Integer n = 99999;
System.out.println("x == y: " + (x==y)); // true
System.out.println("m == n: " + (m==n)); // false
System.out.println("x.equals(y): " + x.equals(y)); // true
System.out.println("m.equals(n): " + m.equals(n)); // true
}
}


对两个Integer实例进行比较要特别注意:绝对不能用==比较,因为Integer是引用类型,必须使用equals()比较:
1
仔细观察结果的童鞋可以发现,==比较,较小的两个相同的Integer返回true,较大的两个相同的Integer返回false,这是因为Integer是不变类,编译器把Integer x = 127;自动变为Integer x = Integer.valueOf(127);,为了节省内存,Integer.valueOf()对于较小的数,始终返回相同的实例,因此,==比较“恰好”为true,但我们绝不能因为Java标准库的Integer内部有缓存优化就用==比较,必须用equals()方法比较两个Integer。
1
2
3
创建新对象时,优先选用静态工厂方法而不是new操作符。

如果我们考察Byte.valueOf()方法的源码,可以看到,标准库返回的Byte实例全部是缓存实例,但调用者并不关心静态工厂方法以何种方式创建新实例还是直接返回缓存的实例。

JavaBean(廖雪峰)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在Java中,有很多class的定义都符合这样的规范:

若干private实例字段;
通过public方法来读写实例字段。

例如:
public class Person {
private String name;
private int age;

public String getName() { return this.name; }
public void setName(String name) { this.name = name; }

public int getAge() { return this.age; }
public void setAge(int age) { this.age = age; }
}
1
2
3
4
5
6
7
8
如果读写方法符合以下这种命名规范:

// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)

那么这种class被称为JavaBean
1
2
3
4
5
6
boolean字段比较特殊,它的读方法一般命名为isXyz():

// 读方法:
public boolean isChild()
// 写方法:
public void setChild(boolean value)
1
2
3
4
5
6
7
只有getter的属性称为只读属性(read-only),例如,定义一个age只读属性:

对应的读方法是int getAge()
无对应的写方法setAge(int)
类似的,只有setter的属性称为只写属性(write-only)。

很明显,只读属性很常见,只写属性不常见
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
属性只需要定义getter和setter方法,不一定需要对应的字段。例如,child只读属性定义如下:

public class Person {
private String name;
private int age;

public String getName() { return this.name; }
public void setName(String name) { this.name = name; }

public int getAge() { return this.age; }
public void setAge(int age) { this.age = age; }

public boolean isChild() {
return age <= 6;
}
}
1
可以看出,getter和setter也是一种数据封装的方法。

JavaBean的作用

1
2
3
4
5
6
7
8
9
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。

通过IDE,可以快速生成getter和setter。例如,在Eclipse中,先输入以下代码:

public class Person {
private String name;
private int age;
}
然后,点击右键,在弹出的菜单中选择“Source”,“Generate Getters and Setters”,在弹出的对话框中选中需要生成getter和setter方法的字段,点击确定即可由IDE自动完成所有方法代码。

枚举JavaBean属性

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
要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的Introspector

import java.beans.*;

public class Main {
public static void main(String[] args) throws Exception {
BeanInfo info = Introspector.getBeanInfo(Person.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println(pd.getName());
System.out.println(" " + pd.getReadMethod());
System.out.println(" " + pd.getWriteMethod());
}
}
}

class Person {
private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

枚举类(廖雪峰)

enum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum来定义枚举类:

public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}

enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
和int定义的常量相比,使用enum定义枚举有如下好处:

首先,enum常量本身带有类型信息,即Weekday.SUN类型是Weekday,编译器会自动检查出类型错误。例如,下面的语句不可能编译通过:

int day = 1;
if (day == Weekday.SUN) { // Compile error: bad operand types for binary operator '=='
}

其次,不可能引用到非枚举的值,因为无法通过编译。

最后,不同类型的枚举不能互相比较或者赋值,因为类型不符。例如,不能给一个Weekday枚举类型的变量赋值为Color枚举类型的值:

Weekday x = Weekday.SUN; // ok!
Weekday y = Color.RED; // Compile error: incompatible types

这就使得编译器可以在编译期自动检查出所有可能的潜在错误。

enum的比较

1
2
3
4
5
6
7
8
使用enum定义的枚举类是一种引用类型。前面我们讲到,引用类型比较,要使用equals()方法,如果使用==比较,它比较的是两个引用类型的变量是否是同一个对象。因此,引用类型比较,要始终使用equals()方法,但enum类型可以例外。

这是因为enum类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==比较:

if (day == Weekday.FRI) { // ok!
}
if (day.equals(Weekday.SUN)) { // ok, but more code!
}

enum类型

1
2
3
4
5
6
7
8
通过enum定义的枚举类,和其他的class有什么区别?

答案是没有任何区别。enum定义的类型就是class,只不过它有以下几个特点:

定义的enum类型总是继承自java.lang.Enum,且无法被继承;
只能定义出enum的实例,而无法通过new操作符创建enum的实例;
定义的每个实例都是引用类型的唯一实例;
可以将enum类型用于switch语句。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
例如,我们定义的Color枚举类:

public enum Color {
RED, GREEN, BLUE;
}

编译器编译出的class大概就像这样:
public final class Color extends Enum { // 继承自Enum,标记为final class
// 每个实例均为全局唯一:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private构造方法,确保外部无法调用new操作符:
private Color() {}
}

所以,编译后的enum类和普通class并没有任何区别。但是我们自己无法按定义普通class那样来定义enum,必须使用enum关键字,这是Java语法规定的。

因为enum是一个class,每个枚举的值都是class实例,因此,这些实例有一些方法:
name()
1
2
3
返回常量名,例如:

String s = Weekday.SUN.name(); // "SUN"
ordinal()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
返回定义的常量的顺序,从0开始计数,例如:
int n = Weekday.MON.ordinal(); // 1

改变枚举常量定义的顺序就会导致ordinal()返回值发生变化。例如:
public enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}



public enum Weekday {
MON, TUE, WED, THU, FRI, SAT, SUN;
}

的ordinal就是不同的。如果在代码中编写了类似if(x.ordinal()==1)这样的语句,就要保证enum的枚举顺序不能变。新增的常量必须放在最后。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
但是,如果不小心修改了枚举的顺序,编译器是无法检查出这种逻辑错误的。要编写健壮的代码,就不要依靠ordinal()的返回值。因为enum本身是class,所以我们可以定义private的构造方法,并且,给每个枚举常量添加字段:

public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}

enum Weekday {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);

public final int dayValue;

private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}
1
2
3
这样就无需担心顺序的变化,新增枚举常量时,也需要指定一个int值。

注意:枚举类的字段也可以是非final类型,即可以在运行期修改,但是不推荐这样做!
toString()
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
默认情况下,对枚举常量调用toString()会返回和name()一样的字符串。但是,toString()可以被覆写,而name()则不行。我们可以给Weekday添加toString()方法:

public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Today is " + day + ". Work at home!");
} else {
System.out.println("Today is " + day + ". Work at office!");
}
}
}

enum Weekday {
MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(0, "星期日");

public final int dayValue;
private final String chinese;

private Weekday(int dayValue, String chinese) {
this.dayValue = dayValue;
this.chinese = chinese;
}

@Override
public String toString() {
return this.chinese;
}
}
1
2
3
覆写toString()的目的是在输出时更有可读性。

注意:判断枚举常量的名字,要始终使用name()方法,绝不能调用toString()!

switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
switch(day) {
case MON:
case TUE:
case WED:
case THU:
case FRI:
System.out.println("Today is " + day + ". Work at office!");
break;
case SAT:
case SUN:
System.out.println("Today is " + day + ". Work at home!");
break;
default:
throw new RuntimeException("cannot process " + day);
}
}
}

enum Weekday {
MON, TUE, WED, THU, FRI, SAT, SUN;
}

小结

1
2
3
4
5
6
7
8
9
10
11
Java使用enum定义枚举类型,它被编译器编译为final class Xxx extends Enum { … };

通过name()获取常量定义的字符串,注意不要使用toString();

通过ordinal()返回常量定义的顺序(无实质意义);

可以为enum编写构造方法、字段和方法

enum的构造方法要声明为private,字段强烈建议声明为final;

enum适合用在switch语句中。

记录类(廖雪峰)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
假设我们希望定义一个Point类,有x、y两个变量,同时它是一个不变类,可以这么写:

public final class Point {
private final int x;
private final int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

public int x() {
return this.x;
}

public int y() {
return this.y;
}
}

为了保证不变类的比较,还需要正确覆写equals()和hashCode()方法,这样才能在集合类中正常使用。后续我们会详细讲解正确覆写equals()和hashCode(),这里演示Point不变类的写法目的是,这些代码写起来都非常简单,但是很繁琐。

record

1
2
3
4
5
6
7
8
9
10
11
从Java 14开始,引入了新的Record类。我们定义Record类时,使用关键字record。把上述Point类改写为Record类,代码如下:
public class Main {
public static void main(String[] args) {
Point p = new Point(123, 456);
System.out.println(p.x());
System.out.println(p.y());
System.out.println(p);
}
}

record Point(int x, int y) {}
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
仔细观察Point的定义:
record Point(int x, int y) {}


把上述定义改写为class,相当于以下代码:
final class Point extends Record {
private final int x;
private final int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

public int x() {
return this.x;
}

public int y() {
return this.y;
}

public String toString() {
return String.format("Point[x=%s, y=%s]", x, y);
}

public boolean equals(Object o) {
...
}
public int hashCode() {
...
}
}
1
2
3
4
5
除了用final修饰class以及每个字段外,编译器还自动为我们创建了构造方法,和字段名同名的方法,以及覆写toString()、equals()和hashCode()方法。

换句话说,使用record关键字,可以一行写出一个不变类。

和enum类似,我们自己不能直接从Record派生,只能通过record关键字由编译器实现继承。

构造方法

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
编译器默认按照record声明的变量顺序自动创建一个构造方法,并在方法内给字段赋值。那么问题来了,如果我们要检查参数,应该怎么办?

假设Point类的x、y不允许负数,我们就得给Point的构造方法加上检查逻辑:

public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException();
}
}
}

注意到方法public Point {...}被称为Compact Constructor,它的目的是让我们编写检查逻辑,编译器最终生成的构造方法如下:
public final class Point extends Record {
public Point(int x, int y) {
// 这是我们编写的Compact Constructor:
if (x < 0 || y < 0) {
throw new IllegalArgumentException();
}
// 这是编译器继续生成的赋值代码:
this.x = x;
this.y = y;
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
作为record的Point仍然可以添加静态方法。一种常用的静态方法是of()方法,用来创建Point:

public record Point(int x, int y) {
public static Point of() {
return new Point(0, 0);
}
public static Point of(int x, int y) {
return new Point(x, y);
}
}

这样我们可以写出更简洁的代码:
var z = Point.of();
var p = Point.of(123, 456);

小结

1
2
3
4
5
6
7
从Java 14开始,提供新的record关键字,可以非常方便地定义Data Class:

使用record定义的是不变类;

可以编写Compact Constructor对参数进行验证;

可以定义静态方法。

常用工具类(廖雪峰)

Math

绝对值
最大或最小值
xy次方
√x
ex次方
e为底的对数
以10为底的对数
三角函数
数学常量
随机数
1
2
3
生成一个随机数x,x的范围是0 <= x < 1:

Math.random(); // 0.53907... 每次都不一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如果我们要生成一个区间在[MIN, MAX)的随机数,可以借助Math.random()实现,计算如下:

// 区间在[MIN, MAX)的随机数
public class Main {
public static void main(String[] args) {
double x = Math.random(); // x的范围是[0,1)
double min = 10;
double max = 50;
double y = x * (max - min) + min; // y的范围是[10,50)
long n = (long) y; // n的范围是[10,50)的整数
System.out.println(y);
System.out.println(n);
}
}

StrictMath

1
有些童鞋可能注意到Java标准库还提供了一个StrictMath,它提供了和Math几乎一模一样的方法。这两个类的区别在于,由于浮点数计算存在误差,不同的平台(例如x86和ARM)计算的结果可能不一致(指误差不同),因此,StrictMath保证所有平台计算结果都是完全相同的,而Math会尽量针对平台优化计算速度,所以,绝大多数情况下,使用Math就足够了。

Random

1
2
3
4
5
6
7
8
9
10
Random用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。

要生成一个随机数,可以使用nextInt()、nextLong()、nextFloat()、nextDouble():

Random r = new Random();
r.nextInt(); // 2071575453,每次都不一样
r.nextInt(10); // 5,生成一个[0,10)之间的int
r.nextLong(); // 8811649292570369305,每次都不一样
r.nextFloat(); // 0.54335...生成一个[0,1)之间的float
r.nextDouble(); // 0.3716...生成一个[0,1)之间的double
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
有童鞋问,每次运行程序,生成的随机数都是不同的,没看出伪随机数的特性来。

这是因为我们创建Random实例时,如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。

如果我们在创建Random实例时指定一个种子,就会得到完全确定的随机数序列:

public class Main {
public static void main(String[] args) {
Random r = new Random(12345);
for (int i = 0; i < 10; i++) {
System.out.println(r.nextInt(100));
}
// 51, 80, 41, 28, 55...
}
}
1
前面我们使用的Math.random()实际上内部调用了Random类,所以它也是伪随机数,只是我们无法指定种子。

SecureRandom

1
有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom就是用来创建安全的随机数的:

异常处理(廖雪峰)

1
2
3
4
5
6
7
8
9
try:
xxx
catch(xxxx e){
e.printStackTrace();
}catch(xxx e){
xxx
}finally{ //可以没有
xxx
}

java的异常

捕获异常

抛出异常

自定义异常

NullPointerException

使用断言

使用JDK Logging

1
java核心库的,程序一旦启动无法再修改配置,且使用时需要指定命令

使用Commons Logging

1
负责充当日志API 第三库;自动挂载其它日志系统,jdk logging或者log4j(负责实现日志底层)

使用Log4j

1
2
3
上述结构虽然复杂,但我们在实际使用的时候,并不需要关心Log4j的API,而是通过配置文件来配置它。

以XML配置为例,使用Log4j的时候,我们把一个log4j2.xml的文件放到classpath下就可以让Log4j读取配置文件并按照我们的配置来输出日志。下面是一个配置文件的例子:
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
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Properties>
<!-- 定义日志格式 -->
<Property name="log.pattern">%d{MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36}%n%msg%n%n</Property>
<!-- 定义文件名变量 -->
<Property name="file.err.filename">log/err.log</Property>
<Property name="file.err.pattern">log/err.%i.log.gz</Property>
</Properties>
<!-- 定义Appender,即目的地 -->
<Appenders>
<!-- 定义输出到屏幕 -->
<Console name="console" target="SYSTEM_OUT">
<!-- 日志格式引用上面定义的log.pattern -->
<PatternLayout pattern="${log.pattern}" />
</Console>
<!-- 定义输出到文件,文件名引用上面定义的file.err.filename -->
<RollingFile name="err" bufferedIO="true" fileName="${file.err.filename}" filePattern="${file.err.pattern}">
<PatternLayout pattern="${log.pattern}" />
<Policies>
<!-- 根据文件大小自动切割日志 -->
<SizeBasedTriggeringPolicy size="1 MB" />
</Policies>
<!-- 保留最近10份 -->
<DefaultRolloverStrategy max="10" />
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<!-- 对info级别的日志,输出到console -->
<AppenderRef ref="console" level="info" />
<!-- 对error级别的日志,输出到err,即上面定义的RollingFile -->
<AppenderRef ref="err" level="error" />
</Root>
</Loggers>
</Configuration>

SLF4J和Logback

1
2
3
4
5
6
7
8
9
前面介绍了Commons Logging和Log4j这一对好基友,它们一个负责充当日志API,一个负责实现日志底层,搭配使用非常便于开发。

有的童鞋可能还听说过SLF4J和Logback。这两个东东看上去也像日志,它们又是啥?

其实SLF4J类似于Commons Logging,也是一个日志接口,而Logback类似于Log4j,是一个日志的实现。

为什么有了Commons Logging和Log4j,又会蹦出来SLF4J和Logback?这是因为Java有着非常悠久的开源历史,不但OpenJDK本身是开源的,而且我们用到的第三方库,几乎全部都是开源的。开源生态丰富的一个特定就是,同一个功能,可以找到若干种互相竞争的开源库。

因为对Commons Logging的接口不满意,有人就搞了SLF4J。因为对Log4j的性能不满意,有人就搞了Logback。
1
2
3
4
从目前的趋势来看,越来越多的开源项目从Commons Logging加Log4j转向了SLF4J加Logback。

SLF4J和Logback可以取代Commons Logging和Log4j;
始终使用SLF4J的接口写入日志,使用Logback只需要配置,不需要修改代码。

反射(廖雪峰)

1
反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。

Class类

1
2
3
除了int等基本类型外,Java的其他类型全部都是class(包括interface)

class(包括interface)的本质是数据类型(Type)。
1
2
3
4
5
6
7
class是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class类型时,将其加载进内存。

每加载一种class,JVM就为其创建一个Class类型的实例,并关联起来。注意:这里的Class类型是一个名叫Class的class。它长这样:

public final class Class {
private Class() {}
}
1
2
3
4
5
以String类为例,当JVM加载String类时,它首先读取String.class文件到内存,然后,为String类创建一个Class实例并关联起来:

Class cls = new Class(String);

这个Class实例是JVM内部创建的,如果我们查看JDK源码,可以发现Class类的构造方法是private,只有JVM能创建Class实例,我们自己的Java程序是无法创建Class实例的。
获取Class实例
1
2
3
4
5
6
7
8
9
10
11
12
如何获取一个class的Class实例?有三个方法:

方法一:直接通过一个class的静态变量class获取:

Class cls = String.class;
方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取:

String s = "Hello";
Class cls = s.getClass();
方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:

Class cls = Class.forName("java.lang.String");
与instanceof比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
因为Class实例在JVM中是唯一的,所以,上述方法获取的Class实例是同一个实例。可以用==比较两个Class实例:

Class cls1 = String.class;

String s = "Hello";
Class cls2 = s.getClass();

boolean sameClass = cls1 == cls2; // true
注意一下Class实例比较和instanceof的差别:

Integer n = new Integer(123);

boolean b1 = n instanceof Integer; // true,因为n是Integer类型
boolean b2 = n instanceof Number; // true,因为n是Number类型的子类

boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class
1
2
3
用instanceof不但匹配指定类型,还匹配指定类型的子类。而用==判断class实例可以精确地判断数据类型,但不能作子类型比较。

通常情况下,我们应该用instanceof判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确判断一个类型是不是某个class的时候,我们才使用==判断class实例。
1
2
3
4
5
因为反射的目的是为了获得某个实例的信息。因此,当我们拿到某个Object实例时,我们可以通过反射获取该Object的class信息:

void printObjectInfo(Object obj) {
Class cls = obj.getClass();
}
Class实例来创建对应类型的实例
1
2
3
4
5
6
7
8
如果获取到了一个Class实例,我们就可以通过该Class实例来创建对应类型的实例:

// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
String s = (String) cls.newInstance();

上述代码相当于new String()。通过Class.newInstance()可以创建类实例,它的局限是:只能调用public的无参数构造方法。带参数的构造方法,或者非public的构造方法都无法通过Class.newInstance()被调用
动态加载
1
2
3
4
5
JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载。

由于JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。

这种通过Class实例获取class信息的方法称为反射(Reflection)

访问字段

1
2
3
4
5
6
Class类提供了以下几个方法来获取字段:

Field getField(name):根据字段名获取某个public的field(包括父类)
Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
Field[] getFields():获取所有public的field(包括父类)
Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public字段"score":
System.out.println(stdClass.getField("score"));
// 获取继承的public字段"name":
System.out.println(stdClass.getField("name"));
// 获取private字段"grade":
System.out.println(stdClass.getDeclaredField("grade"));
}
}

class Student extends Person {
public int score;
private int grade;
}

class Person {
public String name;
}
1
2
3
4
5
上述代码首先获取Student的Class实例,然后,分别获取public字段、继承的public字段以及private字段,打印出的Field类似:

public int Student.score
public java.lang.String Person.name
private int Student.grade
1
2
3
4
5
一个Field对象包含了一个字段的所有信息:

getName():返回字段名称,例如,"name";
getType():返回字段类型,也是一个Class实例,例如,String.class;
getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
以String类的value字段为例,它的定义是:

public final class String {
private final byte[] value;
}
我们用反射获取该字段的信息,代码如下:

Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
获取字段值
1
2
3
利用反射拿到字段的一个Field实例只是第一步,我们还可以拿到一个实例对应的该字段的值。

例如,对于一个Person实例,我们可以先拿到name字段对应的Field,再获取这个实例的name字段的值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// reflection
import java.lang.reflect.Field;

public class Main {

public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
}
}

class Person {
private String name;

public Person(String name) {
this.name = name;
}
}

1
2
3
4
5
6
7
8
上述代码先获取Class实例,再获取Field实例,然后,用Field.get(Object)获取指定实例的指定字段的值。

运行代码,如果不出意外,会得到一个IllegalAccessException,这是因为name被定义为一个private字段,正常情况下,Main类无法访问Person类的private字段。要修复错误,可以将private改为public,或者,在调用Object value = f.get(p);前,先写一句:

f.setAccessible(true);
调用Field.setAccessible(true)的意思是,别管这个字段是不是public,一律允许访问。

可以试着加上上述语句,再运行代码,就可以打印出private字段的值。
1
2
3
4
5
6
7
有童鞋会问:如果使用反射可以获取private字段的值,那么类的封装还有什么意义?

答案是正常情况下,我们总是通过p.name来访问Person的name字段,编译器会根据public、protected和private决定是否允许访问字段,这样就达到了数据封装的目的。

而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。

此外,setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对java和javax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全。
设置字段值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Main {

public static void main(String[] args) throws Exception {
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
}
}

class Person {
private String name;

public Person(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}
1
2
3
运行上述代码,打印的name字段从Xiao Ming变成了Xiao Hong,说明通过反射可以直接修改字段的值。

同样的,修改非public字段,需要首先调用setAccessible(true)。

调用方法

1
2
3
4
5
6
我们已经能通过Class实例获取所有Field对象,同样的,可以通过Class实例获取所有Method信息。Class类提供了以下几个方法来获取Method:

Method getMethod(name, Class...):获取某个public的Method(包括父类)
Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
Method[] getMethods():获取所有public的Method(包括父类)
Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
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
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public方法getScore,参数为String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 获取继承的public方法getName,无参数:
System.out.println(stdClass.getMethod("getName"));
// 获取private方法getGrade,参数为int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
}
}

class Student extends Person {
public int getScore(String type) {
return 99;
}
private int getGrade(int year) {
return 1;
}
}

class Person {
public String getName() {
return "Person";
}
}
1
2
3
4
5
上述代码首先获取Student的Class实例,然后,分别获取public方法、继承的public方法以及private方法,打印出的Method类似:

public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)
1
2
3
4
5
6
一个Method对象包含一个方法的所有信息:

getName():返回方法名称,例如:"getScore";
getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class;
getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class};
getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。
调用方法
1
2
3
4
5
当我们获取到一个Method对象时,就可以对它进行调用。我们以下面的代码为例:

String s = "Hello world";
String r = s.substring(6); // "world"
如果用反射来调用substring方法,需要以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// reflection
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
// String对象:
String s = "Hello world";
// 获取String substring(int)方法,参数为int:
Method m = String.class.getMethod("substring", int.class);
// 在s对象上调用该方法并获取结果:
String r = (String) m.invoke(s, 6);
// 打印调用结果:
System.out.println(r);
}
}
1
2
3
注意到substring()有两个重载方法,我们获取的是String substring(int)这个方法。思考一下如何获取String substring(int, int)方法。

对Method实例调用invoke就相当于调用该方法,invoke的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
调用静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。我们以Integer.parseInt(String)为例:

// reflection
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
// 获取Integer.parseInt(String)方法,参数为String:
Method m = Integer.class.getMethod("parseInt", String.class);
// 调用该静态方法并获取结果:
Integer n = (Integer) m.invoke(null, "12345");
// 打印调用结果:
System.out.println(n);
}
}
调用非public方法
1
和Field类似,对于非public方法,我们虽然可以通过Class.getDeclaredMethod()获取该方法实例,但直接对其调用将得到一个IllegalAccessException。为了调用非public方法,我们通过Method.setAccessible(true)允许其调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// reflection
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person();
Method m = p.getClass().getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(p, "Bob");
System.out.println(p.name);
}
}

class Person {
String name;
private void setName(String name) {
this.name = name;
}
}
1
此外,setAccessible(true)可能会失败。如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对java和javax开头的package的类调用setAccessible(true),这样可以保证JVM核心库的安全。
多态
1
我们来考察这样一种情况:一个Person类定义了hello()方法,并且它的子类Student也覆写了hello()方法,那么,从Person.class获取的Method,作用于Student实例时,调用的方法到底是哪个?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// reflection
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
// 获取Person的hello方法:
Method h = Person.class.getMethod("hello");
// 对Student实例调用hello方法:
h.invoke(new Student());
}
}

class Person {
public void hello() {
System.out.println("Person:hello");
}
}

class Student extends Person {
public void hello() {
System.out.println("Student:hello");
}
}
1
2
3
4
5
6
7
8
运行上述代码,发现打印出的是Student:hello,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。上述的反射代码:

Method m = Person.class.getMethod("hello");
m.invoke(new Student());
实际上相当于:

Person p = new Student();
p.hello();

调用构造方法

1
2
3
4
5
6
7
我们通常使用new操作符创建新的实例:

Person p = new Person();
如果通过反射来创建新的实例,可以调用Class提供的newInstance()方法:

Person p = Person.class.newInstance();
调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
为了调用任意的构造方法,Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor对象和Method非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例:

import java.lang.reflect.Constructor;

public class Main {
public static void main(String[] args) throws Exception {
// 获取构造方法Integer(int):
Constructor cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);

// 获取构造方法Integer(String)
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
}
}
1
2
3
4
5
6
7
8
9
10
通过Class实例获取Constructor的方法如下:

getConstructor(Class...):获取某个public的Constructor;
getDeclaredConstructor(Class...):获取某个Constructor;
getConstructors():获取所有public的Constructor;
getDeclaredConstructors():获取所有Constructor。

注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。

调用非public的Constructor时,必须首先通过setAccessible(true)设置允许访问。setAccessible(true)可能会失败

获取继承关系

获取父类的Class
1
2
3
4
5
6
7
8
9
10
11
12
有了Class实例,我们还可以获取它的父类的Class:

public class Main {
public static void main(String[] args) throws Exception {
Class i = Integer.class;
Class n = i.getSuperclass();
System.out.println(n);
Class o = n.getSuperclass();
System.out.println(o);
System.out.println(o.getSuperclass());
}
}
1
运行上述代码,可以看到,Integer的父类类型是Number,Number的父类是Object,Object的父类是null。除Object外,其他任何非interface的Class都必定存在一个父类类型。
获取interface
1
2
3
4
5
6
7
8
9
10
11
12
13
14
由于一个类可能实现一个或多个接口,通过Class我们就可以查询到实现的接口类型。例如,查询Integer实现的接口:

// reflection
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
}
}
1
2
3
4
5
运行上述代码可知,Integer实现的接口有:

java.lang.Comparable
java.lang.constant.Constable
java.lang.constant.ConstantDesc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
要特别注意:getInterfaces()只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型:

// reflection
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Class s = Integer.class.getSuperclass();
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
}
}
1
2
3
4
5
6
Integer的父类是Number,Number实现的接口是java.io.Serializable。

此外,对所有interface的Class调用getSuperclass()返回的是null,获取接口的父接口要用getInterfaces():

System.out.println(java.io.DataInputStream.class.getSuperclass()); // java.io.FilterInputStream,因为DataInputStream继承自FilterInputStream
System.out.println(java.io.Closeable.class.getSuperclass()); // null,对接口调用getSuperclass()总是返回null,获取接口的父接口要用getInterfaces()
1
如果一个类没有实现任何interface,那么getInterfaces()返回空数组。
继承关系
1
2
3
4
5
6
7
当我们判断一个实例是否是某个类型时,正常情况下,使用instanceof操作符:

Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true
1
2
3
4
5
6
7
8
9
10
如果是两个Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom():

// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
动态代理
1
2
3
4
5
6
7
我们来比较Java的class和interface的区别:

可以实例化class(非abstract);
不能实例化interface。
所有interface类型的变量总是通过某个实例向上转型并赋值给接口类型变量的:

CharSequence cs = new StringBuilder();
1
2
3
4
5
有没有可能不编写实现类,直接在运行期创建某个interface的实例呢?

这是可能的,因为Java标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例。

什么叫运行期动态创建?听起来好像很复杂。所谓动态代理,是和静态相对应的。我们来看静态代码怎么写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
定义接口:

public interface Hello {
void morning(String name);
}
编写实现类:

public class HelloWorld implements Hello {
public void morning(String name) {
System.out.println("Good morning, " + name);
}
}
创建实例,转型为接口并调用:

Hello hello = new HelloWorld();
hello.morning("Bob");
这种方式就是我们通常编写代码的方式。
1
2
3
还有一种方式是动态代码,我们仍然先定义了接口Hello,但是我们并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。

一个最简单的动态代理实现如下:
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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}

interface Hello {
void morning(String name);
}
1
2
3
4
5
6
7
8
在运行期动态创建一个interface实例的方法如下:

定义一个InvocationHandler实例,它负责实现接口的方法调用;
通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
使用的ClassLoader,通常就是接口类的ClassLoader;
需要实现的接口数组,至少需要传入一个接口进去;
用来处理接口方法调用的InvocationHandler实例。
将返回的Object强制转型为接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
动态代理实际上是JVM在运行期动态创建class字节码并加载的过程,它并没有什么黑魔法,把上面的动态代理改写为静态实现类大概长这样:

public class HelloDynamicProxy implements Hello {
InvocationHandler handler;
public HelloDynamicProxy(InvocationHandler handler) {
this.handler = handler;
}
public void morning(String name) {
handler.invoke(
this,
Hello.class.getMethod("morning", String.class),
new Object[] { name });
}
}
其实就是JVM帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法。
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
74
75
76
77
78
79
80
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class DynamicProxy {


public static void main(String[] args) {
// 小韭菜学生类
Student ordinaryStudents = new OrdinaryStudents();
ordinaryStudents.eat();
ordinaryStudents.write();

// 现在有一位特殊的学生,他是区长的儿子,我们自然要对他额外照顾,要给他加一下功能。
// 一种思路是定义一个类:区长的儿子类,他继承自学生类,但世上儿子千千万,有区长的儿子,也有市长的儿子,更有省长的儿子,不能把他们挨个定义出来,
// 现在就可以使用动态代理机制,动态的给区长的儿子加上功能,以后碰到市长、省长的儿子也同样处理。

// InvocationHandler作用就是,当代理对象的原本方法被调用的时候,会重定向到一个方法,
// 这个方法就是InvocationHandler里面定义的内容,同时会替代原本方法的结果返回。
// InvocationHandler接收三个参数:proxy,代理后的实例对象。 method,对象被调用方法。args,调用时的参数。

InvocationHandler handler = (proxy, method, handlerArgs) -> {
// 从定义eat方法。
if ("eat".equals(method.getName())) {
System.out.println("我可以吃香喝辣!");
return null;
}
// 从定义write方法。
if ("write".equals(method.getName())) {
System.out.println("我的作文题目是《我的区长父亲》。");
// 调用普通学生类的write方法,流程还是要走的,还是要交一篇作文上去,不能太明目张胆。
method.invoke(ordinaryStudents, handlerArgs);
System.out.println("我的作文拿了区作文竞赛一等奖!so easy!");
return null;
}
return null;
};
// 对这个实例对象代理生成一个代理对象。
// 被代理后生成的对象,是通过People接口的字节码增强方式创建的类而构造出来的。它是一个临时构造的实现类的对象。
// loder和interfaces基本就是决定了这个类到底是个怎么样的类。而h是InvocationHandler,决定了这个代理类到底是多了什么功能.
// 通过这些接口和类加载器,拿到这个代理类class。然后通过反射的技术复制拿到代理类的构造函数,
// 最后通过这个构造函数new个一对象出来,同时用InvocationHandler绑定这个对象。
// 最终实现可以在运行的时候才切入改变类的方法,而不需要预先定义它。
Student sonOfDistrict = (Student) Proxy.newProxyInstance(ordinaryStudents.getClass().getClassLoader(), ordinaryStudents.getClass().getInterfaces(), handler);
sonOfDistrict.eat();
sonOfDistrict.write();

}
}


/**
* 学生接口,能跑,能吃,能写作文。
*/
interface Student {


void eat();

void run();

void write();
}
/**
* 小韭菜,能跑,能吃,能写作文。
*/
class OrdinaryStudents implements Student {
@Override
public void eat() {
System.out.println("我在吃饭!");
}

@Override
public void run() {
System.out.println("我在跑步!");
}
@Override
public void write() {
System.out.println("我在写作文!");
}
}

注解(廖雪峰)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
什么是注解(Annotation)?注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”:

// this is a component:
@Resource("hello")
public class Hello {
@Inject
int n;

@PostConstruct
public void hello(@Param String name) {
System.out.println(name);
}

@Override
public String toString() {
return "Hello";
}
}

注释会被编译器直接忽略,注解则可以被编译器打包进入class文件,因此,注解是一种用作标注的“元数据”。

使用注解

注解的作用
1
2
3
4
5
6
7
8
9
10
11
12
13
从JVM的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。

Java的注解可以分为三类:

第一类是由编译器使用的注解,例如:

@Override:让编译器检查该方法是否正确地实现了覆写;
@SuppressWarnings:告诉编译器忽略此处代码产生的警告。
这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。

第二类是由工具处理.class文件使用的注解,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。

第三类是在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。例如,一个配置了@PostConstruct的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。
注解的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
定义一个注解时,还可以定义配置参数。配置参数可以包括:

所有基本类型;
String;
枚举类型;
基本类型、String、Class以及枚举的数组。
因为配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。

注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。

此外,大部分注解会有一个名为value的配置参数,对此参数赋值,可以只写常量,相当于省略了value参数。

如果只写注解,相当于全部使用默认值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
举个栗子,对以下代码:

public class Hello {
@Check(min=0, max=100, value=55)
public int n;

@Check(value=99)
public int p;

@Check(99) // @Check(value=99)
public int x;

@Check
public int y;
}

@Check就是一个注解。第一个@Check(min=0, max=100, value=55)明确定义了三个参数,第二个@Check(value=99)只定义了一个value参数,它实际上和@Check(99)是完全一样的。最后一个@Check表示所有参数都使用默认值。

定义注解

1
2
3
4
5
6
7
8
9
Java语言使用@interface语法来定义注解(Annotation),它的格式如下:

public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

注解的参数类似无参数方法,可以用default设定一个默认值(强烈推荐)。最常用的参数应当命名为value。
元注解
1
有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。
@Target
1
2
3
4
5
6
7
最常用的元注解是@Target。使用@Target可以定义Annotation能够被应用于源码的哪些位置:

类或接口:ElementType.TYPE;
字段:ElementType.FIELD;
方法:ElementType.METHOD;
构造方法:ElementType.CONSTRUCTOR;
方法参数:ElementType.PARAMETER。
1
2
3
4
5
6
7
8
9
例如,定义注解@Report可用在方法上,我们必须添加一个
@Target(ElementType.METHOD):

@Target(ElementType.METHOD)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
1
2
3
4
5
6
7
8
9
10
11
定义注解@Report可用在方法或字段上,可以把@Target注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }:

@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface Report {
...
}

实际上@Target定义的value是ElementType[]数组,只有一个元素时,可以省略数组的写法。
@Retention
1
2
3
4
5
6
7
8
9
10
11
12
13
14
另一个重要的元注解@Retention定义了Annotation的生命周期:

仅编译期:RetentionPolicy.SOURCE;
仅class文件:RetentionPolicy.CLASS;
运行期:RetentionPolicy.RUNTIME。

如果@Retention不存在,则该Annotation默认为CLASS。因为通常我们自定义的Annotation都是RUNTIME,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)这个元注解:

@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
@Repeatable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
使用@Repeatable这个元注解可以定义Annotation是否可重复。这个注解应用不是特别广泛。

@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

@Target(ElementType.TYPE)
public @interface Reports {
Report[] value();
}
1
2
3
4
5
6
经过@Repeatable修饰后,在某个类型声明处,就可以添加多个@Report注解:

@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
@Inherited
1
2
3
4
5
6
7
8
9
使用@Inherited定义子类是否可继承父类定义的Annotation。@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效:

@Inherited
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
1
2
3
4
5
6
7
8
9
10
在使用的时候,如果一个类用到了@Report:

@Report(type=1)
public class Person {
}

则它的子类默认也定义了该注解:

public class Student extends Person {
}
如何定义Annotation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
我们总结一下定义Annotation的步骤:

第一步,用@interface定义注解:
public @interface Report {
}

第二步,添加参数、默认值:
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
把最常用的参数定义为value(),推荐所有参数都尽量设置默认值。

第三步,用元注解配置注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

其中,必须设置@Target和@Retention,@Retention一般设置为RUNTIME,因为我们自定义的注解通常要求在运行期读取。一般情况下,不必写@Inherited和@Repeatable。

处理注解

1
2
3
4
5
6
7
Java的注解本身对代码逻辑没有任何影响。根据@Retention的配置:

SOURCE类型的注解在编译期就被丢掉了;
CLASS类型的注解仅保存在class文件中,它们不会被加载进JVM;
RUNTIME类型的注解会被加载进JVM,并且在运行期可以被程序读取。

如何使用注解完全由工具决定。SOURCE类型的注解主要由编译器使用,因此我们一般只使用,不编写。CLASS类型的注解主要由底层工具库使用,涉及到class的加载,一般我们很少用到。只有RUNTIME类型的注解不但要使用,还经常需要编写。
1
2
3
4
5
6
7
8
9
10
因此,我们只讨论如何读取RUNTIME类型的注解。
因为注解定义后也是一种class,所有的注解都继承自java.lang.annotation.Annotation,因此,读取注解,需要使用反射API。

Java提供的使用反射API读取Annotation的方法包括:
判断某个注解是否存在于Class、Field、Method或Constructor:

Class.isAnnotationPresent(Class)
Field.isAnnotationPresent(Class)
Method.isAnnotationPresent(Class)
Constructor.isAnnotationPresent(Class)
1
2
3
4
例如:

// 判断@Report是否存在于Person类:
Person.class.isAnnotationPresent(Report.class);
1
2
3
4
5
6
7
8
9
10
11
12
使用反射API读取Annotation:

Class.getAnnotation(Class)
Field.getAnnotation(Class)
Method.getAnnotation(Class)
Constructor.getAnnotation(Class)
例如:

// 获取Person定义的@Report注解:
Report report = Person.class.getAnnotation(Report.class);
int type = report.type();
String level = report.level();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
使用反射API读取Annotation有两种方法。方法一是先判断Annotation是否存在,如果存在,就直接读取:

Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
Report report = cls.getAnnotation(Report.class);
...
}

第二种方法是直接读取Annotation,如果Annotation不存在,将返回null:

Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
读取方法、字段和构造方法的Annotation和Class类似。但要读取方法参数的Annotation就比较麻烦一点,因为方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解就必须用一个二维数组来表示。例如,对于以下方法定义的注解:

public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}

要读取方法参数的注解,我们先用反射获取Method实例,然后读取方法参数的所有注解:

// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
if (anno instanceof Range r) { // @Range注解
r.max();
}
if (anno instanceof NotNull n) { // @NotNull注解
//
}
}
使用注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
注解如何使用,完全由程序自己决定。例如,JUnit是一个测试框架,它会自动运行所有标记为@Test的方法。

我们来看一个@Range注解,我们希望用它来定义一个String字段的规则:字段长度满足@Range的参数定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
int min() default 0;
int max() default 255;
}

在某个JavaBean中,我们可以使用该注解:

public class Person {
@Range(min=1, max=20)
public String name;

@Range(max=10)
public String city;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
但是,定义了注解,本身对程序逻辑没有任何影响。我们必须自己编写代码来使用注解。这里,我们编写一个Person实例的检查方法,它可以检查Person实例的String字段长度是否满足@Range的定义:

void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
// 遍历所有Field:
for (Field field : person.getClass().getFields()) {
// 获取Field定义的@Range:
Range range = field.getAnnotation(Range.class);
// 如果@Range存在:
if (range != null) {
// 获取Field的值:
Object value = field.get(person);
// 如果值是String:
if (value instanceof String s) {
// 判断值是否满足@Range的min/max:
if (s.length() < range.min() || s.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
}
}
}
}

这样一来,我们通过@Range注解,配合check()方法,就可以完成Person实例的检查。注意检查逻辑完全是我们自己编写的,JVM不会自动给注解添加任何额外的逻辑。