一、JavaDoc

javadoc是为java开发者提供程序注释工具,通过采用规范的javadoc语法对java源码进行注释,并利用提供的javadoc工具快速生成标准的javadoc文档。当其他人使用开发者发布的工具包时,可以通过javadoc文档阅读,快速学习了解相应的应用程序接口(api)。

1.1 注意点

javadoc默认Console输出编码采用系统默认,在涉及中文编码时会产生乱码,通过配置环境变量JAVA_TOOL_OPTIONS=--Dfile.encoding=UTF-8解决

二、函数式接口与Lambda表达式

2.1 java 8 函数式接口概述

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口, 其用@FunctionalInterface标注。可以由Lambda表达式转化创建。这种接口一般用于功能的自定义实现。在Java 8以前,想要达到目的必须使用接口的显式类或匿名内部类的形式,如创建一个新的线程:

1
2
3
4
5
6
new Thread(new Runnable(){
@Override
public void run(){
System.out.println("Hello World");
}
}).start();

在java 8中引入了著名的Lambda表达式,对于函数式接口,可以通过Lambda表达式迅速创建,减少了语法冗余。
1
2
3
4
5
//相当于创建了一个Runnable接口实例,并传入
new Thread(()->{System.out.println("Hello World");}).start();

//这样也是可以的
Runnable runnable = ()->{System.out.println("Hello World");};

2.2 函数式接口的自定义

函数式接口有且仅有一个抽象方法。值得注意的是,在java 8之前,java接口所有方法都是抽象的,无论是否显式用abstract关键字修饰。在java 8中给接口引入了默认方法(Default Method),用default关键字修饰,类似于默认参数的概念,这个方法自然是有实现体的非抽象方法。另外值得注意的是,虽然函数式接口一般都会用@FunctionalInterface标注,但这个标注不是必须的,这这是方便编译器检查,当进行标注而不是函数式接口时,编译器会报错。

1
2
3
4
5
6
@FunctionalInterface  //这个标注不是必须的,符合函数式接口定义即可
public interface MyFunctionInterface {
public abstract void print();
public default void print1(){} //函数式接口允许多个非抽象方法
public default void print2(){}
}

此外这里的抽象方法不包括从Object类继承的抽象方法,如java.util.Comparator<T>就重写了继承自Objectequals方法。我认为作者之所以显式重写抽象方法,实际上只是想说明比较器之前也能比较,但实际意义不大,因为它还是抽象的,利用了Object的default方法,比较两者地址是否一致。不过在编写代码时是不能将接口显式声明extends Object的,因为类和接口是两个体系,即接口只能继承接口。只是Object作为一个特殊,jvm会将Object类的方法自动继承给接口,个人理解是为了方便创建匿名内部类而无需用具名类implements这个接口,因为接口最终都是利用类实现的,不论什么类继承自Object类,正如<<Java语言规范>>所说:

If an interface has no direct superinterfaces, then the interface implicitly declares a public abstract member method m with signature s, return type r, and throws clause t corresponding to each public instance method m with signature s, return type r, and throws clause t declared in Object, unless a method with the same signature, same return type, and a compatible throws clause is explicitly declared by the interface. It is a compile-time error if the interface explicitly declares such a method m in the case where m is declared to be final in Object.

2.3 Lambda表达式

java的Lambda表达式语法与JavaScript、C++的Lambda表达式语法相近。其参数用“()”显示标出,函数体用“{}”显示标出,中间用“->”分隔。当单参数或单语句时,对应括号可隐藏。Lambda表达式的参数与返回值类型必须与对应的函数式接口的抽象方法类型一致。

实际上,Python、JavaScript、C++都能显式的传递函数指针,定义函数指针变量,动态决定函数体,因此lambda的意义个人觉得反而不如Java。此外Perl在Perl6中也引入了Lambda而支持函数式编程(函数式编程相对的是指令式编程,函数式编程关注的是映射关系,纯粹的函数式编程是没有变量的,只要输入确定输出就确定,这也是闭包或lambda表达式中外部局部变量不能改变的原因)。

1
2
3
4
5
6
7
//java Lambda
(int x, int y)->{return x+y;}; //显式声明参数类型,是个好习惯,方便阅读
(x, y)->{return x+y;}; //不声明参数类型,根据对应函数式接口判断
x->{return x+1;}; //单参数时,不用圆括号
x->x+1; //单语句时,不用花括号,此时默认返回值即语句返回值,不能使用return语句
()->"Hello World"; //无参数
()->System.out.println("Hello world"); //无返回值

1
2
3
4
//C++ Lambda,实际上创建的是函数的栈指针,可用auto进行类型推断,也可显式声明函数指针
auto functionptr0 = [local_val](int x){return x + 1;}; //[]声明使用的外部局部变量,()显式声明函数参数,{}为函数体
int (*functionptr1)() = []()->int{return 1;} //可以用“->”指定返回类型,这与java对该符号的使用不同.
functionptr0(2);

1
2
3
4
//JavaScript Lambda
const function0 = function(){....}; //在ES6之前,一般使用匿名函数来实现函数回调、闭包等。
const function1 = (a, b)=>{a+b;}; //ES6引入“=>”Lambda表达式,形式与java几乎一致
const function2 = a=>a+b; //单语句时,不用花括号,此时默认返回值即语句返回值,不能使用return语句

1
2
3
4
5
6
7
##Python Lambda
function0 = lambda a,b,c:a+b+c ##Python使用lambda关键字声明,参数与函数体用:分隔,语句结果即为返回值
"""
值得注意的是,相比前面几位,Python的Lambda表达式更像是表达式,其函数体只能支持使用表达式而不能使用赋值语句
不能使用()、{}进行参数与函数体界定,因为python本身作用域是依靠缩进判断的。故Python的lambda适用于简单使用。
"""
function1 = lambda a,b,c:{a+b+c} ##这是合法的,但返回值是单个元素的集合,{}是集合定界符

2.4 Java 经典函数式接口

接口全限定名 抽象方法 描述
java.until.function.Consumer<T> void accept(T t) 消费型接口,一个参数,无返回值,since 1.8
java.until.function.Supplier<T> T get() 供给型接口, 没有参赛,有返回值,since 1.8
java.until.function.Function<T, R> R apply(T t) 函数型接口, 一个参数,有返回值,since 1.8
java.until.function.Predicate<T> boolean test(T t) 断言型接口,一个参数,有返回值,since 1.8, 返回boolean值为判断结果
java.lang.Runnable public abstract void run 可运行接口,没有参数,无返回值,since 1.0, 为多线程使用
java.util.Comparator<T> public int compare(T o1, T o2) 比较型接口,一个参数,有返回值,since 1.2

此外,在java 8在java.until.function包内提供了许多其他函数式接口。详细可参考javadoc 8

2.5 Java标准库中的应用身影

java.lang.Iterable<T>public default void forEach(Consumer<? super T> action)

java.util.List<E>public default void sort(Comparator<? super E> c)

2.6 Lambda表达式的特殊形式

对于只是传递已经的方法,可以使用“::”快速构建函数式接口实例,简化代码表达(类似于python将函数名作为变量传入)。例如:

1
2
3
4
5
6
7
Runnable r = System.out::println;
//等效于
Runable r = ()->System.out.println();

Consumer<String> consumer = System.out::println;
//等效于
Consumer<String> consumer = (String s)->System.out.println(s);

上述要求函数的参数、返回值与函数式接口方法一致。 上面两个println是两个重载方法。

三、Java线程基础

3.1 synchronized关键字

synchronized是同步的意思,java中用该关键字让当前线程获取对象的锁。当无法获取对象的锁时(即对象的锁被别的线程占据),当前线程会等待该对象的锁释放再获取。即当前线程被动阻塞。
synchronized关键字可以修饰直接修饰特定对象,也可以修饰对象的实例方法或类的静态方法。实例方法修饰实际上是让当前线程获取方法所属对象的锁,静态方法修饰则是获取所属类的锁,两者不存在冲突。当前所有请求获得该对象锁的线程均在该对象对应的同步队列中。

3.2 wait方法

wait方法是定义在java.lang.Object中方法,重载方法wait(long millis)为native方法。当前线程获得该对象的锁时,可以调用这个方法使得当前线程进入该对象的线程等待队列并阻塞。wait()实际调用native方法wait(0),表示一直阻塞,直到接收当该对象的恢复通知并从其线程等待队列中放出。而当native方法的millis > 0时,代表只是阻塞一段时间。

3.3 notify方法

notify是定义在java.lang.Object中方法,当前线程获得该对象的锁时,可以调用这个方法使得该对象的线程等待队列中第一个等待线程被取出而取消被动阻塞。取出的线程由于进入时是在synchronized关键字代码段中,即那时候(被wait的时候)拥有当前对象的锁,因此它会加入当前对象的同步队列,按照优先级重新等待获取当前对象的锁,此时若还没获取当前对象的锁,线程会主动阻塞自己等待获取对象的锁。

3.4 notifyAll方法

notifyAll是定义在java.lang.Object中方法,当前线程获得该对象的锁时,可以调用这个方法使得该对象的线程等待队列被清空,所有在其中的线程会被释放,并同理会进入该对象的同步队列,按照优先级重新获取当前对象的锁。

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
{
/*
* 下面代码三个子线程依次输出1、2、3。若主线程使用notifyAll(),那么子线程之间将会进行竞争,由于优先级一致,会有不同的顺序可能。
*/
String flag = "";
new Thread(()->{
synchronized(flag) {
try {
flag.wait(); //子线程1获得flag的锁,被放入等待队列,然后临时释放拥有的锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(1);
}
}).start();
new Thread(()->{
synchronized(flag) {
try {
flag.wait();//子线程2获得flag的锁,被放入等待队列,然后临时释放拥有的锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(2);
}
}).start();
new Thread(()->{
synchronized(flag) {
try {
flag.wait();//子线程3获得flag的锁,被放入等待队列,然后临时释放拥有的锁
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(3);
}
}).start();
synchronized(flag) {//主线程获得对象flag的锁
flag.notify();//子线程1被放出,进入flag的同步队列,此时flag的锁在主线程手中
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 1000);
}//主线程释放对象flag的锁
Thread.sleep(5000);//主线程休眠,让优先级较低的子线程1获取flag的锁
synchronized(flag) {//主线程获得对象flag的锁
flag.notify();//子线程2被放出,进入flag的同步队列,此时flag的锁在主线程手中
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 1000);
}//主线程释放对象flag的锁
Thread.sleep(5000);/主线程休眠,让优先级较低的子线程2获取flag的锁
synchronized(flag) {
flag.notify();
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 1000);
}//主线程释放对象flag的锁,子线程3获取flag的锁
}

3.5 sleep方法

sleep方法定义于java.lang.Thread中,属于native的静态方法,可以让当前线程休眠指定时间。但与wait不同的是,sleep方法并不释放对象的锁,也不要求获得谁的锁才能调用。因此sleep方法不影响锁的争取顺序。其实质是等同上面代码中的while循环拖延时间。

3.6 yield方法

yield方法定义于java.lang.Thread中,是native的静态方法,其作用在于告诉JVM当前线程愿意放弃资源,从正在运行状态转为可运行状态,将运行资源转给线程池中的相同优先级线程。但值得注意的是,你告诉JVM你现在不忙,不代表JVM真会理你,只是让JVM指定一下罢了,不一定起效,具体看JVM的衡量

3.7 join方法

join方法定义于java.lang.Thread中,当主线程调用子线程实例的join方法,会使得主线程被阻塞直至子线程完成或阻塞一段指定的时间。join这一个名字很好的表达了这个方法,它可以让子线程结果插入到主线程的指定位置,从而可以在利用多线程完成任务的同时获得任务结果

join方法就很好地使用了同步对象与同步方法。早些版本的join方法的源码(如下)采用的是synchronized修饰一个直观的对象,现行采用了修饰方法。因为当前线程调用子线程实例的join方法阻塞当前线程和子线程完成后notifyAll通知当前线程继续时,使用子线程实例作为加锁对象是最为合适的。因此采用synchronized方法更加简便不易出问题。

通过源码阅读,我们可以清楚知道,其实join方法就是基于wait和notify/notifyAll方法来实现。因此很多多线程任务不直接使用wait/notify,而是使用了join方法。(javadoc中原文: It is recommended that applications not use wait, notify, or notifyAll on Thread instances.)join方法的时间参数与wait方法是一样的作用。

源码中调用了wait是看到的,那么notify在哪,谁notify的,这其实是由jvm在结束子线程是自动完成的,参加大佬文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final synchronized void join(final long millis)
throws InterruptedException {
if (millis > 0) {
if (isAlive()) {
final long startTime = System.nanoTime();
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
} else if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
throw new IllegalArgumentException("timeout value is negative");
}
}

java.util.concurrent.locks.Lock接口

Lock接口是java实现的api,同样可以实现synchronized关键字的作用,参见文档

1
2
3
4
Lock lock = new ReentrantLock();
lock.lock();//取锁
.....//相当于synchronized的代码块中同步内容,此时的锁的对象正是lock实例,当某个线程获得锁时,其他线程也就卡在了lock.lock()这里。
lock.unlock();//放锁

四、Java泛型(generics)浅涉

Java泛型自Java 1.5引入,其泛型实现由编译器支持,并没有重构JVM。泛型类在字节码(.class)中与普通类无异。如List<String>List<Integer>在字节码中是一个类,都是原生类型List。这与C++的模板泛型有很大不同。在C++中,编译器利用模板类Vector<T>可以生成Vector<int>Vector<string>Vector<bool>等不同的具体类型,它们是不同的类。C++模板类造成了模板类相同代码的重复,这是一直被诟病的地方。Java泛型基于编译器的类型擦除解决了这个问题,并借此实现够兼容java 1.5之前的普通类。

1
2
3
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());//结果是true,因为在字节码中只有List原生类型

4.1 泛型声明

Java中泛型可分为泛型类、泛型方法和泛型接口,与C++一样,使用“<>”来声明泛型参数。此外能够在参数中应用extends关键字来约束泛型的声明范围,采用通配符?配合superextends关键字来约束泛型的实现范围。与C++不同的是,java泛型参数指定必须是类/接口,而不能是基本类型int/float/char/double/boolean。

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
//泛型类,这是java.util.ArrayList<E>的源码声明,泛型声明<E>紧靠类名
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

//泛型方法,泛型类型由泛型参数推断,因此必须有泛型参数,否则这个泛型没有意义
public <T> T get(T t) {
return t;
}

//泛型接口,这是java.util.List<E>的源码声明,格式与泛型类一致
public interface List<E> extends Collection<E>

//泛型方法可以在泛型类中同时存在,若泛型参数相同,泛型方法使用自身的泛型参数而隐藏类泛型参数
class Vector<E> extends java.util.ArrayList<E> {
public <E> void print(E r) {
System.out.println(r.getClass().getName());
}
public static void main(String[] args) {
Vector<String> vector = new Vector<>();
vector.print(1);//输出是java.lang.Integer而不是java.lang.String
}
}

//泛型声明还可以通过extends关键字约束泛型声明的上限,其实默认上限就是Object,同类的继承一样。
//extends声明的上限也决定了类型擦除后的具体类型,详见下文
class Vector<E extends Integer> extends java.util.ArrayList<E>

值得注意的是,泛型类中静态普通方法不能使用泛型参数,因为泛型的实现是基于对象而不是类。但是泛型方法可以是静态方法。需要深刻理解泛型方法与普通方法的差异。
1
2
3
4
5
6
7
8
class Vector<E> extends java.util.ArrayList<E> {
public static <E> void print(E r) {//静态泛型方法,通过编译
System.out.println(r.getClass().getName());
}
public static void print2(E r) {//静态普通方法,使用类泛型参数无法通过编译
System.out.println(r.getClass().getName());
}
}

4.2 泛型通配符

一般来说,泛型对象会指定具体的泛型类型,比如ArrayList<Number>ArrayList<Integer>,在编译阶段,编译器把他俩看做两个不同的类型,无法相互传递引用(反射机制除外)。

1
2
3
4
ArrayList<Number> array1 = new ArrayList<>();
ArrayList<Integer> array2 = new ArrayList<>();

array1 = array2;//无法通过编译,会报错。纵然Number是Integer的超类。

但有时候,我们不知道传过来的到底是啥泛型类型,我们也不care是啥泛型类型因为我们不需要去操作泛型实例只需要看一下这个对象的一些与泛型类型无关的属性如ArrayList的元素个数等,这时候通配符?就派上用处了。
1
2
3
4
5
6
7
8
9
10
11
ArrayList<?> array3= array2;//编译通过,代价时无法进行与泛型相关的操作,如array3.add()方法需要传入泛型实例,只能用array3.size()这种。 
array3.add(1);//编译不通过,当然通过反射机制能够绕开这个限制
array3.size();//编译通过
//编译通过,利用反射机制绕过限制,原因在于类型擦除,但一般没谁闲得慌干这个骚操作,这个操作同时跳脱了array2本身的泛型类型限制
array3.getClass().getMethod("add", Object.class).invoke(array3, 1);

//extends决定通配符的上限(包括),此案例不通过编译
ArrayList<? extends Integer> array4 = new ArrayList<Object>();

//super决定通配符的下限(包括),此案例不通过编译
ArrayList<? super Number> array4 = new ArrayList<Integer>();

4.3 类型擦除

前面说过,java泛型只是有编译器所实现,而在字节码文件和JVM中并无区别对待。因为在java编译器中存在一步泛型的类型擦除。类型擦除其实质就是java编译器给我们进行了一次替换,而替换后的类型就右我们显式extends声明或默认extends决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//泛型源码
class Generics<E extends Number> {
public E print(E r) {
System.out.println(r.getClass().getName());
return r;
}
public static void main(String[] args) {
Generics<Integer> v = new Generics<>();
int res = v.print(10);
}
}

//擦除后的等效源码,当没有声明extends Number,默认是extends Object,因此会替换为Object
class Generics {
public Number print(Number r) {
System.out.println(r.getClass().getName());//输出java.lang.Integer
return r;
}
public static void main(String[] args) {
Generics v = new Generics();
//根据<Integer>指定,施行强制转换,类型安全就由泛型而得到保障,泛型带来最直接的优势也就是不用我们自己去转换
int res = (Integer) v.print(10);
}
}

当将上述两段代码分别编译,泛型代码编译后字节码共1205字节,普通代码编译后字节码共958字节,这说明java编译器除了我们说的这个基本过程,还有一些细节信息放在泛型里。但这不影响理解泛型。泛型的类型擦除,所用的具体对象有extends决定,这由反射机制可以很好的验证。
1
2
3
4
5
6
7
8
9
10
11
class Generics<E extends Number> {
E infoE = null;
public static void main(String[] args) {
Generics<Integer> v = new Generics<>();
try {
System.out.println(v.getClass().getDeclaredField("infoE").getType().getName());//输出为java.lang.Number
} catch (NoSuchFieldException | SecurityException e) {
e.printStackTrace();
}
}
}

可能会疑问为啥前面System.out.println(r.getClass().getName());输出的不是java.lang.Number,不是类型擦除了嘛。那是因为类的多态性决定的。对应Number r = new Integer(10),这时候r.getClass()方法调用的是Integer子类的方法而非父类Number,返回的Class对象自然是Integer对应的Class对象。而getDeclaredField("infoE")返回的是类成员infoE的声明类型对应的Class对象,不存在多态的问题,自然就正如所料。当然getClass方法是由jvm自动实现的(在Object类中该方法有@HotSpotIntrinsicCandidate标注),不需要自己手动重写,就同自动生成对应Class对象一样

4.4 注意事项

泛型类型T并不是实体类型,因此不能对其进行new T()实例化(编译报错:Cannot instantiate the type T),也不能调用实例类型属性如T.class(编译报错:Illegal class literal for the type parameter T)。故泛型类型只能通过赋值传递,如T c = value,或者调用泛型实例的实例方法如c.getClass().getName()。其实前者很好理解为什么不可,因为泛型类型没有明确的构造器,无法传参。后者理论上其实应该是可以像泛型实例方法一样使用的,但目前编译器不支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Data<T extends Number> {
private T value;
public Data(T value) throws Exception {
this.value = value;
// 编译正常
T c = value;
// 编译报错:Cannot instantiate the type T
T d = new T();
// 编译正常,利用反射机制掉取构造器创建新类,前提是对该类型构造器清楚
T f = (T) value.getClass().getConstructor(String.class).newInstance("2000");
// 编译正常
System.out.println(value.getClass().getName());
// 编译报错:Illegal class literal for the type parameter T
System.out.printlnt(T.class);
}
}

五、Java序列化与反序列化技术

序列化指的是将对象从内存持久化保存到硬盘中,或者用于网络数据传输的一项技术,除了Java,许多语言如Python都支持这项技术。而反序列化顾名思义也就是序列化的逆过程,IO总是有写有读嘛。

5.1 序列化与反序列化的过程

一个对象想要能够序列化,必须保证自身及其实例成员全部实现java.io.Serilizable接口,这个接口没有什么抽象方法,唯一作用在于告诉JVM这个对象可以序列化。

java使用java.io.ObjectOutputStream实例对象(oos对象)的writeObject方法序列化指定对象到文件。当对象的类自身实现了private void writeObject(java.io.ObjectOutputStream out) throws IOException方法,oos对象会通过反射机制调用该方法实现特殊的序列化需求,否则按照java默认方式序列化。java.until.ArrayList<E>实际上就是如此。因为其核心成员elementData是用transient关键字修饰的对象数组,该关键字修饰的实例成员在java默认序列化时会被跳过。之所以这样设计是因为数组是整个序列化的,而这个数组其实是预留的最大容量罢了,我们的元素可能只有几个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//这是java.until.ArrayList<E>的writeObject源码,通过自定义writeObject,实现只序列化我们的元素,而不序列化整个elementData数组。
transient Object[] elementData;
....
@java.io.Serial
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();

// Write out size as capacity for behavioral compatibility with clone()
s.writeInt(size);

// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}

if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

对应的,java使用java.io.ObjectInputStream实例对象(ois对象)的readObject方法反序列化获得对象。当对象的类自身实现了private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException方法,ois对象同样通过反射机制调用该方法实现特殊的反序列化需求,否则按java默认方式反序列化。如前面一样,java.until.ArrayList<E>同样也得自定义readObject方法,以正确读取自定义writeObject方法的序列化数据。
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
//对应的`java.until.ArrayList<E>` readObject方法,与writeObject逻辑对应。
@java.io.Serial
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {

// Read in size, and any hidden stuff
s.defaultReadObject();

// Read in capacity
s.readInt(); // ignored

if (size > 0) {
// like clone(), allocate array based upon size not capacity
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
Object[] elements = new Object[size];

// Read in all elements in the proper order.
for (int i = 0; i < size; i++) {
elements[i] = s.readObject();
}

elementData = elements;
} else if (size == 0) {
elementData = EMPTY_ELEMENTDATA;
} else {
throw new java.io.InvalidObjectException("Invalid size: " + size);
}
}

此外,值得一提的是,所有可序列化的类,最好都显式指定private static final long serialVersionUID,这是一个类的版本号,java进行反序列化时会将序列化文件中的这个ID与内存中的类进行比对,若不一致会抛出异常。就算你不指定,java也会给个默认ID为1L,因此最好显式指定方便检查。

六、java注解

  • 标注是一种特殊的java类型,与双斜杠等在源码中的注释不同,根据@Rention元标注修饰,可以将标注保留到class文件中
  • 在JVM运行程序时,可以通过反射机制读取记录在class文件中的标注信息,也可以选择在编译时保留用于编译检查
  • 因此,java本身可以利用内置标注类型如@Override等在编译、运行时进行检查,如@Override,这些注解在编译后会删除。
  • 也可以用@Decorated等对类别进行信息提示,这也是eclipse等获取对象信息实现自动补全(当然基于STL模板的自动补全是另外)的基础
  • 在Web应用如Tomcat等,可以利用标注直观的反射读取并设置配置参数,例如@WebServlet、@WebListener
  • 注解的定义使用了@interface关键字,与接口很像,别混了。通过default关键字声明默认值(default这货到处充当默认,如接口的默认方法、switch的默认操作)。
    1
    2
    3
    4
    5
    6
    @Retention(RUNTIME)
    @Target({ TYPE, FIELD, METHOD })
    public @interface myAna {
    public String value();
    public String name() default "zhang";
    }

    七、java反射

  • java的反射机制本质上是因为所有数据结构都保存在了class文件中而可获取,无论源码修饰符是private、protected
  • 还是public。这些修饰符的作用在在编译检查时进行限制访问,对不符合限制的代码不予以编译通过。
  • jvm在从class文件中加载一个类时,会自动生成一个java.lang.Class对象,该对象包含了这个class文件的所有数据结构
  • 信息,包括所有变量、常量、构造器、静态方法、实例方法等,无论是否为private修饰。
  • 通过getDeclaredMethod方法可以获得声明的所有方法,产生一个Method对象,调用其invoke方法可以执行对应的方法
  • 通过getDeclaredField方法可以获得声明的所有变量、常量,产生一个Field对象,调用其get方法取值,set方法修改值
  • 常量修改时不会修改类中常量,可以理解为镜像修改
  • 通过getDeclaredAnnotaion方法可以获得标注的所有标注,产生对应的标注对象(如@WebServlet),并调用各自标注对象
  • 的定义信息方法获取方法值。从而实现各类配置操作
  • 对于权限不足的调用,为抛出IllegalArgumentException,需要先setAccessible(true);
  • 对于调用方法不存在(方法名或参数类型不对),会抛出NoSuchMethodException
  • 对于调用方法invoke时,对应参数不对,会抛出IllegalArgumentException
  • 对应调用变量常量不存在(名称不对),会抛出NoSuchFieldException
  • java的封装性作为其一大特征,其实指的是对外部用户使用是进行了默认的调用限制,从而避免了用户错误调用内部方法
  • 而产生不必要的问题,确保用户使用统一外部接口,但这并不是拒绝了其他用户访问内部方法,通过反射机制可以再必要时
  • 实现内部访问。这与封装的目的不相互冲突。毕竟恶意调用内部方法只能给使用者自己造成Bug。
  • 当然反射机制为class的反编译提供了帮助,这在知识产权保护上有所不利

八、java内置数组

Java中数组是直接继承Object类的对象。但与普通类不同的是,数组是由JVM直接创建的,没有外部定义的class文件。其实我觉得可以理解为JVM内置了数组这样一个泛型类,因此不用外部定义类,JVM当在字节码文件中看到需要创建数组,不会去标准库中寻找定义而是直接创建。

九、java基本数据类型的封装

为了效率,java对基础的几种数据类型并没有使用对象类型,而是和C++一样使用了简单类型,他们是byte、char、int、long、float、double和bool。在java.lang包中提供了对应的封装类。

基本类型 封装类 描述
byte Byte 字节型
char Character 字符类型
int Integer 整型
long Long 长整型
float Float 单精度浮点型
double Double 双精度浮点型
boolean Boolean 布尔型
void Void 特殊类型,空

封装类提供了对应数据类型的一系列工具,此外在Java泛型等需要使用对象的场合能够代替基本类型。JVM自身重载了运算符=,同String类型一样,可以方便的转换基本类型和引用类型。

void关键字不是数据类型,它表示的是没有返回值。Void封装类一个作用在于对于一些需要返回值的泛型类型,在实现时不需要返回值时可以指定为Void类型,Void类型只能接受null。通过源码我们可以知道Void类这一特点的实现时使用了private修饰的空构造方法来代替默认构造方法,使得Void类型永远只能是引用类型的默认值null而无法初始化。

1
2
3
4
5
public final class Void {
/*
* The Void class cannot be instantiated.
*/
private Void() {}

此外Void封装类可以再反射中用来获取没有返回值(void)的方法。下面代码中输入main和hello两个无返回值方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class arrayParam {
String hello() {return "";}
void print(Object... args) {
System.out.println(args.length);
}
public static void main(String[] args) {
for (Method method: arrayParam.class.getDeclaredMethods()) {
System.out.println(method.getReturnType());
if (method.getReturnType().equals(Void.TYPE)) {
System.out.println(method.getName());
}
}
}
}

值得注意的是,用的Void.TYPE属性。封装类的TYPE属性指的是其对应基本类型的Class对象,这与封装类对应的Class对象是不同的,即Void.class!=Void.TYPE。通过阅读封装类的源码,我们可以看到这个属性是利用Class类的native static方法getPrimitiveClass(String name) ,Class类javadoc对其描述就是获取原始类型(primitive)的Class对象。这个方法解决了原始类型(基本类型)没有class属性的尴尬,也避免了无法通过Class泛型创建基本类型的Class对象的尴尬(基本类型不能用于泛型)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 这是getPrimitiveClass方法的源码
* Return the Virtual Machine's Class object for the named
* primitive type.
*/
static native Class<?> getPrimitiveClass(String name);

//这是所有常见封装类的TYPE属性声明
public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");
public static final Class<Void> TYPE = (Class<Void>) Class.getPrimitiveClass("void");
public static final Class<Long> TYPE = (Class<Long>) Class.getPrimitiveClass("long");
public static final Class<Double> TYPE = (Class<Double>) Class.getPrimitiveClass("double");
public static final Class<Float> TYPE = (Class<Float>) Class.getPrimitiveClass("float");
public static final Class<Byte> TYPE = (Class<Byte>) Class.getPrimitiveClass("byte");
public static final Class<Character> TYPE = (Class<Character>) Class.getPrimitiveClass("char");
public static final Class<Boolean> TYPE = (Class<Boolean>) Class.getPrimitiveClass("boolean");

十、java native interface(JNI)接口

java本地接口可以实现调用非java代码,主要是C/C++代码,从而实现一些java自身不好实现的功能。步骤如下

(1)编写一个包含native方法的java文件,native关键字修饰

1
2
3
4
5
6
7
8
9
public class nativeHelloWorld{
public static void main(String[] args) {
print("Hello world");
}
public static native void print(String args);
static {
System.load("D:/zhang/userfile/office/Study/IDRB/Java/java_test/libnativeHelloWorld.dll");
}
}

(2)在早期jdk,使用javah命令处理编译后的class文件,生成对应的nativeHelloWorld.h头文件。
1
javah -jni nativeHelloWorld

在jdk 10以后,javah被整合至javac中,通过-h参数表明为javah操作并指导输出目录。
1
javac -h <目标文件夹> nativeHelloWord.java

(3)修改生成的h头文件,实现native方法对应的C/C++函数声明。
1
2
3
4
JNIEXPORT void JNICALL Java_nativeHelloWorld_print
(JNIEnv * env, jclass obj, jstring args) {
cout << env->GetStringUTFChars(args, NULL) << endl;
}

十一、class文件

javap -v <CLASS>

该命令可以查看class文件内部信息

十二、java与操作系统命令交互

12.1 Runtime.getRuntime().exec

java的exec命令提供了一种快捷运行系统命令的方法,运行完毕(此时会阻塞当前线程)会返回一个Process进程对象。通过进程对象可以获取命令的输出等信息。但无法再命令运行期间动态查看输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
Process p = Runtime.getRuntime().exec(new String[]{
"jupyter-lab",
"--allow-root",
"--port", "8080",
"--ip", "0.0.0.0"
});
BufferedReader br = null;
br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();

12.2 ProcessBuilder

前面exec是一种快捷执行的方法,但是不适合命令运行期间动态获取输出。ProcessBuilder是更为基础的工具,exec命令实际上是基于ProcessBuilder开发的(下面片段是jdk 15源代码)。

1
2
3
4
5
6
7
public Process exec(String[] cmdarray, String[] envp, File dir)
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}

ProcessBuilder可以指定命令、命令的环境变量、运行目录和输入输出设备等。在exec命令中,输出被传入管道送给java而无法立即在终端看到。
1
2
3
4
5
6
7
8
9
10
ProcessBuilder pb = new ProcessBuilder(new String[]{
"jupyter-lab",
"--allow-root",
"--port", "8080",
"--ip", "0.0.0.0"
});
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); //重定向输出到当前终端
pb.redirectError(ProcessBuilder.Redirect.INHERIT); //重定向错误到当前终端
Process p = pb.start();
p.waitFor(); //阻塞当前线程以观察输出。

十三、好用的第三方库

13.1 Lomdok插件

使用@Data、@Getter、@Setter等注解,减少代码冗余。其原理是通过自定义注解处理器,在编译时修改class文件,使得开发者不用再源码中书写getter/setter等模板代码。

1
2
3
4
5
6
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>

13.2 fastjson

由阿里巴巴开发的JSON处理器,能够快速实现JSON字符串反序列化为Java对象或将Java对象序列化为JSON字符串。

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>

13.3 jsch

一款纯java开发的ssh实现,可以方便java程序进行ssh远程通讯。官网有许多示例代码。

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.jcraft/jsch -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>

13.4 velocity

由Apache开源组织开发一块模板引擎,对于Web开发有着很高效的作用。

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>

13.5 jedis

Redis是一款经典的非关系型缓存数据库。不像关系型数据库有着丰富的JDBC接口及扩展。jedis是一款好用的Redis服务处理工具。

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.0</version>
</dependency>

13.6 druid

druid是由阿里巴巴开发一款业内公认好用的JDBC数据库连接池。

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>

JVM

JVM参数

try 语句资源释放

对于实现java.lang.AutoCloseablejava.lang.Closeable接口的类,可以通过try语句自动调用close方法释放资源。同时可以配合catch和finally语句执行一些操作。InputStream和OutputStream两个抽象类均实现了Closeable接口,故其子类均可以使用try语句自动关闭资源。

1
2
3
4
5
6
7
8
9
10
try (
FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt");
) {
...
} catch (IOException e) {
...
} finaly {
...
}

参考文献