本文包含 Java 继承、抽象、多态、异常与日志、注解、泛型、输入输出、网络编程、反射、多线程等知识
继承、抽象、多态
- 继承是 is 关系,组合是 has 关系
- 如果父类使用带参数列表的构造方法,子类的构造方法就必须显式调用 super(参数列表)
public、protected、private、friendly(也称default)
均可修饰类、方法、变量,它们的作用范围:
| 当前类 | 当前包 | 子孙类 | 其它包 | |
|---|---|---|---|---|
public |
√ | √ | √ | √ |
protected |
√ | √ | √ | × |
friendly |
√ | √ | × | × |
private |
√ | × | × | × |
final可修饰类、方法、变量- 标记的类不能被继承。
- 标记的方法不能被子类重写。
- 标记的变量(成员变量或局部变量)即为常量,只能赋值一次
static可修饰成员方法、成员变量、代码块- 静态方法或变量不需要依赖于对象进行访问,可通过类名访问
- 静态变量只分配一次内存
- 静态方法中不能使用
this和super关键字 - 非静态方法可以访问类的静态成员方法和静态成员变量
- 静态方法不能直接访问类的非静态成员方法和非静态成员变量,但可通过实例对象访问
接口(
interface)- 一个接口可以继承多个接口(类不能多继承),一个类可以实现多个接口
- 接口的变量会隐式地指定为
public static final变量(一般接口中不定义变量),方法会被隐式地指定为public abstract,即接口中只能有抽象方法 - 子类实现一个接口则必须实现所有的抽象方法,否则子类必须定义为抽象类
- 接口不能被实例化,没有构造函数
抽象类(
abstract)- 一个类只能继承一个抽象类
- 含有抽象方法的类一定是抽象类,但是抽象类不一定含有抽象方法,抽象类中可以有具体方法。抽象方法的修饰符只能为
public或者protected,一般为public - 子类继承一个抽象类则必须实现所有的抽象方法,否则子类必须定义为抽象类
- 抽象类不能被实例化,但可以有构造函数供子类调用
重载(方法重载是指同一类中的)
1. 方法名称必须相同
2. 参数列表必须不同(个数不同、或类型不同、或排列顺序不同)
3. 仅仅返回类型不同不足以成为方法的重载
4. 重载是发生在编译时的重写(重写是子类和父类之间的)
- 重写的方法必须和父类保持一致,包括返回类型、方法名、参数列表
- 重写的方法可以使用
@Override注解来标识 - 子类中重写方法的访问权限不能低于父类中方法的访问权限
多态性
1
2
3
4
5
6
7
8
9public static void main(String[] args) {
//假设 Student 类继承于 Person 类,并重写其 run() 方法
Person p = new Student();
p.run(); // 运行的是Student.run
// Student s = new Person(); 不能这么写,需要强制类型转换
// Student s = (Student) new Person();
}
异常处理与日志
捕获多种异常
- 使用多个 catch 语句
此时子类必须写在前面。因为JVM 在捕获到异常后会从上到下匹配 catch 语句,匹配到某个 catch 后就执行 catch 中代码块,然后就不再继续匹配 - 在一个 catch 语句中使用
|连接多个异常类型
此时要求多个异常类型之间不存在继承关系,可写成catch ( ExA | ExB e) { ... }
- 使用多个 catch 语句
try…catch…finally 执行逻辑
如果没有发生异常,就正常执行 try { … } 语句块,然后执行 finally { … }
如果发生了异常,就中断执行 try { … } 语句块,然后跳转执行匹配的 catch 代码块,最后执行 finally { … }抛出异常 throw
catch 捕获异常后再抛出异常,不会影响finally的执行。JVM会先执行finally,然后抛出异常
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个 try … catch 被捕获为止自定义异常
推荐从 RuntimeException 派生一个 BaseException 作为“根异常”,然后在“根异常”基础上派生出各种业务类型的异常断言 assert
断言是一种调试方式,断言失败会抛出 AssertionError,只能在开发和测试阶段启用断言
JVM默认关闭断言指令,即遇到 assert 语句就自动忽略日志系统的使用
在开发阶段,始终使用 Commons Logging 接口来写入日志,并且开发阶段无需引入 Log4j 。如果需要把日志写入文件, 只需要把正确的配置文件和 Log4j 相关的 jar 包放入 classpath ,就可以自动把日志切换成使用 Log4j 写入,无需修改任何代码。
注解
泛型
输入输出
网络编程
反射
JVM 动态加载类,当需要使用某类时才将其加载进内存,而不是一次性把所有用到的类全部加载到内存
JVM加载类时,就为每个类创建一个与之关联的 Class 类型的实例,这个实例中保存了该类的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等。
通过 Class 实例获取类的相关信息的方式称为反射
Class 实例的获取
获取本类 Class 实例
1 | //法一 |
获取父类 Class 实例
可以通过 Class 实例的 getSuperclass() 方法获取父类的 Class 实例
对所有 interface 的 Class 调用 getSuperclass() 返回的是 null,任何非 interface 的 Class 都必定存在一个父类类型
1 | Class a = Integer.class; |
获取本类实现接口的 Class 实例
可以通过类的 Class 实例的 getInterfaces() 方法返回当前类直接实现的接口类型的 Class 实例数组,其中并不包括其父类实现的接口类型
getInterfaces() 也可获取接口的父接口的 Class 实例数组
如果一个类没有实现任何接口,那么 getInterfaces() 返回空数组
Class 实例的应用
获取类的相关信息
1 | static void printClassInfo(Class cls) { |
创建实例
可以使用 Constructor 实例调用任意的构造方法来创建原实例
通过 Class 实例获取 Constructor 的方法如下:
- getConstructor(参数的Class…):获取某个 public 的 Constructor
- getDeclaredConstructor(参数的Class…):获取某个 Constructor
- getConstructors():获取所有 public 的 Constructor
- getDeclaredConstructors():获取所有 Constructor
注意: Constructor 总是获取当前类定义的构造方法,和父类无关
1 | Class cls = String.class; |
获取字段信息
- Field getField(name):根据字段名获取某个 public 字段(包括父类)
- Field getDeclaredField(name):根据字段名获取当前类的某个字段(不包括父类)
- Field[ ] getFields():获取所有 public 字段(包括父类)
- Field[ ] getDeclaredFields():获取当前类的所有字段(不包括父类)
一个 Field 对象包含了一个字段的所有信息:- getName():返回字段名称,例如,”name”
- getType():返回字段类型,也是一个 Class 实例,例如,String.class
- getModifiers():返回字段的修饰符,它是一个 int 类型的值,不同的值表示不同的含义
1 | Field f = String.class.getDeclaredField("value"); |
获取和设置字段值
可通过 Field 对象的 get ( Object ) 函数获取指定 Object 的指定字段的值,返回类型为 Object 。
通过反射能够突破类的封装,使得字段不受 private 修饰的限制
可通过Field 对象的 set(Object, Object) 函数实现,其中第一个参数是指定的实例,第二个参数是待修改的值
1 | public class Main { |
获取方法信息
- Method getMethod(name, 参数的Class…):获取某个 public 方法(包括父类)
- Method getDeclaredMethod(name, 参数的Class…):获取当前类的某个方法(不包括父类)
- Method[ ] getMethods():获取所有 public 方法(包括父类)
- Method[ ] getDeclaredMethods():获取当前类的所有方法(不包括父类)
一个Method对象包含一个方法的所有信息:- getName():返回方法名称,例如:”getScore”
- getReturnType():返回方法返回值类型,也是一个 Class 实例,例如:String.class
- getParameterTypes():返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class}
- getModifiers():返回方法的修饰符,它是一个 int 类型的值,不同的值表示不同的含义
调用函数执行
可通过 Method 对象的 invoke(参1,参2,…) 函数实现原函数的执行,参1是指定的实例,参2及之后的参数是原函数参数列表中一一对应的参数
对于获取到的静态函数,invoke 方法传入的第一个参数永远为 null
1 | public class Main { |
多态性:
1 | Method m = Person.class.getMethod("hello"); |
继承关系的判断
instanceof
可用 instanceof 判断某个实例- 是否是某类型
- 是否是某类的子类类型
- 是否实现某接口
- 是否是实现某接口的类型的子类
1
2
3
4
5
6Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isComparable = n instanceof java.lang.Comparable; // true , Integer 实现了接口 java.lang.Comparable
boolean isSerializable = n instanceof java.io.Serializable; // true ,Number 实现了接口 java.io.Serializable
Class 实例比较 和 instanceof 的差别
1
2
3
4
5
6Integer 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.classisAssignableFrom
可使用 isAssignableFrom() 判断一个 Class 实例能否向上转型为另一个 Class 实例1
2Integer.class.isAssignableFrom(Integer.class); // true,可以写 Integer i = new Interger();
Number.class.isAssignableFrom(Integer.class); // true,可以写 Number i = new Interger();
多线程

创建线程
可通过 t.setPriority(int) 设置线程 t 优先级,线程创建的方法如下:
继承 Thread 类
1
2
3
4
5
6
7
8
9
10
11
12
13public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
public void run() {
System.out.println("start new thread!");
}
}实现 Runable 接口
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
26public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
public void run() {
System.out.println("start new thread!");
}
}
//可简写为
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("start new thread!");
}
});
t.start();
}
}通过 Callable 和 FutureTasker 创建线程
Callable 接口与 Runnable 类似,但是有返回值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class Main {
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>() {
public Integer call() throws Exception {
Integer i = new Random().nextInt(100);
System.out.println("在子线程打印:"+i);
return i;
}
};
FutureTask<Integer> f = new FutureTask<Integer>(callable);
Thread thread = new Thread(f);
thread.start();
try {
Thread.sleep(2000);
System.out.println("在主线程打印:"+f.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
中断线程
通过 interrupt()
可使用 interrupt() 来中断线程,线程可以通过 isInterrupted() 方法来做出反应(只适用于第一种方法创建的线程),也可以在检测到 InterruptedException 时做出反应(均适用)
如果执行 interrupt() 后线程没有对应的处理方法,线程并不会终止执行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("start new thread!");
int i = 1;
while (true){
try {
System.out.println(i++);
Thread.sleep(100); //该线程暂停 0.1 秒
}catch (InterruptedException e) {
break; //当被中断时退出循环
}
}
}
});
t.start();
Thread.sleep(500); //主线程暂停 0.5 秒
t.interrupt(); //申请中断 t 线程
t.join(); //等待 t 线程执行完毕再执行主程序
System.out.println("退出程序");
}
}通过共享标志位
可以用一个线程间共享变量 running 来标识线程是否应该继续运行,该变量需要用 volatile 关键字标记。
因为如果线程修改了变量的值,虚拟机会在某个不确定的时间把修改后的值从工作内存回写到主内存,导致其它线程从主存读取到的值不是最新的。 volatile 只保证变量改动之后立刻回写,之后新线程读取到的肯定是更新后的值,但之前已经读取过此变量到工作内存的线程无法得知主内存中这个变量有了修改。
即使有 volatile 修饰,当线程从主存读入数据到工作内存后被阻塞,恢复执行时不会重新从主存读入数据,而是使用已经读取到工作内存的数据,这就涉及到线程同步了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false,使子线程停止执行循环
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
守护线程
对于普通线程,JVM 进程只有在其它普通线程全部结束后才会结束,而对于守护线程,JVM 进程不管它是否结束,只要其它普通线程全部结束就结束
守护线程不能持有任何需要关闭的资源,例如打开文件等
通过 t.setDaemon(true) 可将其设置为守护线程
线程同步
线程选择锁对象
多线程同时读写共享变量时,需要通过 synchronized 同步,注意具有同步关系的一组线程的加锁对象必须是同一个实例
1 | public class Main { |
某些单个赋值语句不需要同步,如:
- 基本类型( long 和 double 除外)赋值,例如:int n = m
- 引用类型赋值,例如:List
list = anotherList
如果是多行赋值语句,就必须保证是同步操作
有时可以将非原子转化为原子操作,就不用同步了
1 | class Pair { |
同步逻辑的封装
让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把 synchronized 逻辑封装起来。
1 | public class Main { |
- 如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),上面的 Counter 类就是线程安全的。Java 标准库的 java.lang.StringBuffer 也是线程安全的。
- 还有一些不变类,例如 String,Integer,LocalDate,它们的所有成员变量都是 final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。
- 最后,类似 Math 这些只提供静态方法,没有成员变量的类,也是线程安全的。
- 除了上述几种少数情况,大部分类,例如ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么 ArrayList 是可以安全地在线程间共享的。
- 没有特殊说明时,一个类默认是非线程安全的。
同步普通方法
锁对象为该类实例1
2
3
4
5
6
7
8
9public void test(int n) {
synchronized(this) {
...
}
}
//等价于
public synchronized void test(int n) {
...
}同步静态方法
静态方法没有 this 实例
锁对象为该类的 Class 实例1
2
3
4
5
6
7
8
9public static void test(int n) {
synchronized(Counter.class) {
...
}
}
//等价于
public synchronized static void test(int n) {
...
}
死锁
在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。如下所示:
1 | public void add(int m) { |
- 首先
- 线程1:进入 add(),获得 lockA
- 线程2:进入 dec(),获得 lockB
- 然后
- 线程1:准备获得 lockB,失败,等待中
- 线程2:准备获得 lockA,失败,等待中
于是就产生了死锁,那么如何避免死锁呢?答案是线程获取锁的顺序要一致
修改上面代码,使线程2获取锁的顺序同线程1保持一致,如下所示:
1 | public void add(int m) { |
可重入锁
某个线程已经获得某个锁,如果该锁可以再次被获取而不会出现死锁情况的锁,就叫做可重入锁。
上文提到的 synchronized 以及下文提到的 ReentrantLock都是可重入锁
1 | public class Counter { |
锁的使用与改进
synchronized 和 wait()、notify()
在 同步逻辑的封装中已经介绍了 synchronized 的使用,虽然 synchronized 解决了多线程竞争的问题,但是未解决多线程协调的问题,在如下所示的代码中,while() 循环永远不会退出,因为 getTask() 获得锁后在内部先判断队列是否为空,如果为空就循环等待,直到另一个线程往队列中放入了一个任务,while() 循环才退出,然而其它线程要放入任务也得先获得锁,而此时锁已经被占用,就会导致 while() 循环一直执行。
1 | public class TaskQueue { |
此时,为实现多线程之间的协调,需要搭配 wait() 和 notify() 来使用。改进后代码如下:
1 | public class TaskQueue { |
假设有 A、B、C 3个线程在 isEmpty()=true 后进入了wait(),此时队列为空
线程D 执行 addTask() 放入了一个 task,此时 queue=[‘task1’],然后唤醒所有等待线程。
此刻 A、B、C 都要从 wait() 返回,但只有其中一个能先获得锁先执行,假设是A,它则继续执行 wait() 后面的代码,花括号内代码执行完后就再进行 while 循环判断,发现队列不为空就跳出循环返回队头元素并释放锁,这个时候队列又空了
随后 B 或 C 如果获得锁也从 wait() 后面开始执行代码,再次经过 while 判断发现队列还是空的,于是再次 wait()
上述代码中的 while 不能换成 if,如果换成了 if ,上述线程A执行正常,但当线程B 或 C 获得锁后,它从 wait() 后开始执行代码,花括号内执行完就会直接返回队头元素,而不是像 while 时需要再次进入循环,因为此时队列为空,返回队头元素必然报错
ReentrantLock 和 Condition
从 Java 5 开始,可用 java.util.concurrent.locks 包中的 ReentrantLock 替代 synchronized 加锁
1 | public class Counter { |
同 synchronized 一样 ReentrantLock 是可重入锁,一个线程可以多次获取同一个锁。
和 synchronized 不同的是,ReentrantLock 需要手动释放锁,所以使用 ReentrantLock 的时候加锁次数和释放次数要一样,否则会产生死锁
此外 ReentrantLock 还可以尝试获取锁
1 | if (lock.tryLock(1, TimeUnit.SECONDS)) { //若1秒后仍未获取到锁,tryLock() 返回false |
Condition 对象提供的 await()、signal()、signalAll() 和 synchronized 锁对象的 wait()、notify()、notifyAll() 是一致的
1 | public class TaskQueue { |
await() 可以在等待指定时间后自动醒来
1 | if (condition.await(1, TimeUnit.SECOND)) { |
ReadWriteLock 读写锁
ReentrantLock 保护有点过头,每次只有一个线程可以执行临界区代码,效率低下
而 ReadWriteLock 允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待
ReentrantReadWriteLock 是 ReadWriteLock 接口的实现,是可重入锁
1 | public class Counter { |
StampedLock 乐观读
StampedLock(Java 8 引入)相比 ReadWriteLock 引入了乐观读,它乐观估计读的过程中大概率不会有写入,使得读的过程中也允许获取写锁写入,但为了保证读取的数据一致,需要额外的代码判断读的过程中是否有写入
注意这里,我们用的是“乐观读”这个词,而不是“乐观读锁”,是要提醒你,乐观读这个操作是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些。
StampedLock 只有写锁是不可重入的,而读锁(readLock)是可重入的
1 | public class Point { |
Semaphore
Semaphore 保证某种受限资源同一时刻最多有N个线程能访问
1 | public class AccessLimitControl { |
线程池
- Executors:线程池创建工厂类
- ExecutorService:线程池类
CachedThreadPool 线程池的大小会根据任务数量动态调整
放入 ScheduledThreadPool 线程池的任务可以定期反复执行
1 | public class Main { |
参考
- 本文标题:Java 知识
- 本文作者:kecho
- 创建时间:2022-09-30 14:28:03
- 本文链接:https://blog.kecho.top/2022/Java 知识.html
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!