为了更好理解 java 代码而不是只把功能实现,有必要了解一下 java 的基础概念,这对疑难杂症修复,写代码效率都是有帮助的!
字符串对比
使用 == 是比较引用,使用 equals() 是比较内容
String a = '123';
a == '123'; // java 会把所有定义的字符串存到常量池栈内存中,a 和 '123' 引用地址都在常量池,所以返回 true
String b = new String('123');
b == '123'; // 若是使用 new String 创建的字符串则是会在堆中创建新对象,所以这是用 == 比较是返回 false 的 false
b.equals('123'); // equals 是比较内容,所以返回 true
注意:部分包装类型(下面有写:如 Integer)会对特定范围的值进行缓存(-128~127),直接赋值时可能复用对象。
Integer a = 127;
Integer b = 127; // 读取的是 a 缓存的对象,并不是创建新对象
System.out.println(a == b); // true
List 一些方法,如contains() indexOf()内部也是使用 equals 进行比较的
public class Main {
public static void main(String[] args) {
List<String> list = List.of("A", "B", "C");
System.out.println(list.contains(new String("C"))); // true
System.out.println(list.indexOf(new String("C"))); // 2
}
}
如上所示,方法传入的 ”C” 与创建时 List.of 传入的 “C“ ,其实是不同的对象,但是依旧返回了true、索引,可以看出方法内部是调用 equals() 进行内容比较,由此也可以推出,传给方法的对象,必须实现了 equals() 方法,如上的 String 是自带 equals() 方法的,如果是自定义的类,必须自定义 equels() 方法
多态
多态顾名思义是多种形态,是指运行时才能决定调用哪个类型的方法
举例:
有一个 Occupation 职业父类,包含一个 getSalary 方法,Teacher Doctor 等子类继承 Occupation 并复写了 getSalary 方法
有一个接收 Occupation 职业类型参数的方法,调用参数的 getSalary 获取工资方法,由于不知道传入的是什么类型的参数,是 Teacher 还是 Doctor ,也就是要等运行时,才能确定调用哪个类型的 getSalary 方法,这就是多态。
包装类型
基本类型都有对应的包装类型
包装类型属于对象
包装类型属于对象,所以能够设置成 null,基本类型不可,所以实体类中数字类型应该优先设置为Integer,若确定字段不会为null,则设置成int更好
包装类型提供了一些静态方法
int num = Integer.parseInt("123");
自动装箱和自动拆箱
基本类型和包装类型之间会自动进行类型转换
Integer n = 100; // 自动装箱:100 是 int,自动转换为 Integer 了
int n1 = n; // 自动拆箱:Integer 自动转为 int
自动转换类型会加大内存性能开销,有一个小优化技巧:在进行循环或者计算时可以将 Integer 转为 int 再进行
Integer total = entity.getTotal();
int primitiveTotal = (total != null) ? total : 0; // 拆箱并处理 null
泛型需使用包装类型
泛型只能处理对象,所以必须使用包装类型,原理是:泛型是通过类型擦除实现的,也就是编译时会把类型擦除为 Object,添加或读取时,通过转换实现(如通过 (String) 转换为 String),所以泛型的类型参数必须是 Object 的子类
String 属于引用类型
在java中,String是引用类型而不是基础类型,因此可以给它赋值为 null,也可以很方便使用String的实例方法
String s = null;
String str = "hello";
int len = str.length();
另外拼接字符串拼接推荐使用 SringBuilder 而不是加号拼接,原因是加号拼接时会创建新的 String 对象并舍弃旧的,造成性能开销,StringBuilder 则不会
反射
JVM 动态加载 class ,运行时,读取到时才加载
JVM会为每个加载的 class 、 interface 创建 Class 实例,保存其信息,如字段、方法、类名、包名、父类等等,通过 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");
至于如何读取 Class 实例的信息,如字段,方法等用到再查吧!
注解
注解感觉有点像 vue 的自定义指令,自定义指令是对被使用的元素做一些处理,而注解是对被使用的字段或方法做一些处理
public class Person {
@Range(min=1, max=20) // 检查长度
public String name;
}
定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
int min() default 0;
int max() default 255;
}
使用 @interface 定义注解
用在注解上的注解为元注解,如上例: Retention 定义了注解的周期类型,@Target 定义了应用的位置为应用于字段
-
仅编译期:
RetentionPolicy.SOURCE;只在编译期使用,不会打包进 class 文件,如
@Override:让编译器检查该方法是否正确地实现了覆写 -
仅class文件:
RetentionPolicy.CLASS;会被打包进class文件,比如有些工具会在加载class的时候,对class做修改处理,实现一些的功能
-
运行期:
RetentionPolicy.RUNTIME。在程序运行期能够读取的注解,如上面的 Range 是需要在程序运行时读取,并进行判断处理的
处理注解
注解可以使用反射读取,如上面的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());
}
}
}
}
}
泛型
顾名思义:广泛的类型,感觉可以理解为方法,有一个类型参数,传入什么类型,里面就定义什么类型
泛型是通过类型擦除实现的,上面有提到
泛型编写
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
extends 子类泛型
假设定义一个 add() 静态方法,以下例子会报错,因为 add() 接收 Number 类型的 Pair ,而传入的是 Integer 类型的
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
}
static int add(Pair<Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
想要不报错,可改为接收泛型为 Number 子类的 Pair
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
extends 泛型是不能修改的,以下会报错
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
p.setFirst(new Integer(first.intValue() + 100));
p.setLast(new Integer(last.intValue() + 100));
return p.getFirst().intValue() + p.getLast().intValue();
}
细想一下这也很合理,站在方法的角度想想:我接收的是 Number 子类,我不知道你会传入 Integer 还是 Double,但是你设置时却设置一个固定的类型 Integer,假如你传入的是 Double 类型的,那不是完蛋了!
super 父类泛型
super 和 extend 是相反的,
extend 是子类泛型,super 是父类泛型
extend 不能修改,super 不能读取
以下例子 setSame() 接收 Integer 以及其父类(Number Object)类型的 Pair
public class Main {
public static void main(String[] args) {
Pair<Number> p1 = new Pair<>(12.3, 4.56);
Pair<Integer> p2 = new Pair<>(123, 456);
setSame(p1, 100);
setSame(p2, 200);
System.out.println(p1.getFirst() + ", " + p1.getLast());
System.out.println(p2.getFirst() + ", " + p2.getLast());
}
static void setSame(Pair<? super Integer> p, Integer n) {
p.setFirst(n);
p.setLast(n);
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
super 父类泛型无法读取,以下会报错
Integer x = p.getFirst();
细想一下同样很合理,我接收的是 Integer 及其父类的 Pair ,我不知道你是 Integer 还是 Number ,现在却定义为 Integer,假如你传入的是 Number 那不是完蛋了!
数组和List 以及 Map
js 只有数组的概念,java 中 数组 和 List 是不同的
List 更灵活,平时使用多,与 js 数组更类似
数组性能更好,适合长度固定,高性能场景
java 中 Map 的内部其实是数组,他会根据 key 计算出 索引,进行存取
进程线程
一个进程可以包含一个或多个线程,且至少会有一个线程
可以类比到计算机系统理解:一个任务是一个进程,比如浏览器是一个进程, word 是一个进程,word 中又有一些子任务,如拼写检查、打印等,这些子任务可以理解为线程
开启线程
java 实际是一个 jvm 进程,主线程执行 main 方法,方法中又可以开启多个线程,线程的 star() 方法实际会调用内部的 run() 方法
// 开启线程,方式1
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}
// 开启线程,方式2
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
// 开启线程,方式3
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}
开启线程后,两个线程就开始同时运行了,执行顺序是无法确定的,如下代码,只能确定一开始会打印 main star… 之后的语句打印顺序是无法确定的
public class Main {
public static void main(String[] args) {
System.out.println("main start...");
Thread t = new Thread() {
public void run() {
System.out.println("thread run...");
System.out.println("thread end.");
}
};
t.start();
System.out.println("main end...");
}
}
但是也可以通过 join 方法等待线程执行结束
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("hello");
});
System.out.println("start");
t.start(); // 启动t线程
t.join(); // 此处main线程会等待t结束
System.out.println("end");
}
}
守护线程
所有线程结束后, jvm 才能结束,但是某些任务,是一直循环,不会结束的,例如心跳检查,这些进程可以设置成守护进程,jvm 结束时,无论守护线程是否还在运行,都会被强制终止。
只要在调用start()方法前,调用setDaemon(true)即可把该线程标记为守护线程
Thread t = new MyThread();
t.setDaemon(true);
t.start();
中断线程
通过线程对象的 interrupt() 方法中断线程
线程内部通过调用 isInterrupted()
若线程在 sleep()、wait() 或 I/O 阻塞时,会抛出 InterruptedException,需捕获并终止
public class InterruptExample {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println("Working...");
Thread.sleep(1000); // 模拟阻塞操作
} catch (InterruptedException e) {
System.out.println("Thread interrupted, exiting.");
// 恢复中断状态并退出
Thread.currentThread().interrupt();
}
}
});
worker.start();
Thread.sleep(3000); // 主线程等待3秒
worker.interrupt(); // 中断子线程
}
}
线程同步
上面有提到线程执行是不确定的,假设有个公共变量 int num = 0;,线程1 和 线程2 都加对其加 1 num += 1,那么 num是否等于 2 ?
答案是不一定,因为相加分三步:1. 加载 2. 相加 3. 储存。有可能 线程1 加载后还没进行相加和储存,线程2 也加载了,这时两个线程加载的 num 都是 0,相加后, num为 1
当多个线程同时修改共享数据时,要保证逻辑正确,就要加锁,锁住后,该线程执行时,其他线程必须等待(同步)。
使用 synchronized 关键字,传入同一对象即可加锁,不传默认是 this 实例对象,两段需要同步执行的代码必须传同一对象,如果传的是不同的对象,用的不是同一个锁,那么还是会并行执行的
public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread();
var add2= new AddThread2();
add.start();
add2.start();
add.join();
add2.join();
System.out.println(Counter.count);
}
}
class Counter {
public static final Object lock = new Object();
public static int count = 0;
}
class AddThread extends Thread {
public void run() {
synchronized(Counter.lock) {
Counter.count += 1;
}
}
}
class AddThread2 extends Thread {
public void run() {
synchronized(Counter.lock) {
Counter.count += 1;
}
}
}
也可以封装 Counter 对象
public class Counter {
private int count = 0;
public void add() {
synchronized(this) {
count += 1;
}
}
// 写法 2
// public synchronized void add(int n) {
// count += n;
// }
public int get() {
return count;
}
}
public class Main {
public static void main(String[] args) throws Exception {
Counter c1 = new Counter();
Thread t1 = new Thread(() -> c1.add());
Thread t2 = new Thread(() -> c1.add());
t1.start();
t2.start();
// 主线程等待 t1 和 t2 执行完毕
t1.join();
t2.join();
System.out.println(c1.get());
}
}
上面例子有个问题,由于 get方法不是同步的,当有线程调用 get时,有可能会读取到缓存的值,而不是相加后最新的值,解决办法是将 get 设置成同步,或者 count 使用 volatile 修饰,volatile的作用是保证修改立刻刷新到内存,保证读取从内存读取而非缓存
// 方法1 get 方法设置成同步
public synchronized int get() {
return count;
}
// 方法2 count 使用 volatile 修饰
private volatile int count = 0;
死锁
两个或多个线程因争夺资源(锁)而陷入相互等待的状态,导致所有线程都无法继续执行,编程中必须警惕这种问题
案例1:互相获取对方的锁,从而两个锁都无法释放,陷入僵持
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
两个线程,线程1调用 add(),线程2调用 dec()
线程1 获取 lockA,线程2 获取 lockB
此时,线程1 获取 lockB,线程2 又获取 lockA 了,两边都僵持着,锁都无法释放,另一个线程也就获取不到锁,陷入了无限的等待
案例2:锁一直不释放,其他方法无法获得锁,陷入阻塞
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
public synchronized String getTask() {
while (queue.isEmpty()) {
}
return queue.remove();
}
}
线程1 调用 getTask() ,获取 this 锁,发现队列为空,然后陷入了循环,锁无法释放,这时线程2 调用 addTask() ,但无法获得锁,从而阻塞
wait notify/notifyAll
上面的案例2 可以通过 wait() notify() 解决
wait() 先让出锁,进入等待
notify() 唤醒等待中的线程
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notify();
}
public synchronized String getTask() {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
}
但是 notify() 只能唤醒一个线程,如果有多个线程进入了等待,就需要 notifyAll() 全部唤醒
import java.util.*;
public class Main {
public static void main(String[] args) throws InterruptedException {
var q = new TaskQueue();
var ts = new ArrayList<Thread>();
for (int i=0; i<5; i++) {
var t = new Thread() {
public void run() {
// 执行task:
while (true) {
try {
String s = q.getTask();
System.out.println("execute task: " + s);
} catch (InterruptedException e) {
return;
}
}
}
};
t.start();
ts.add(t);
}
var add = new Thread(() -> {
for (int i=0; i<10; i++) {
// 放入task:
String s = "t-" + Math.random();
System.out.println("add task: " + s);
q.addTask(s);
try { Thread.sleep(100); } catch(InterruptedException e) {}
}
});
add.start();
add.join();
Thread.sleep(100);
for (var t : ts) {
t.interrupt();
}
}
}
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll();
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
}
如上例,5个线程都调用了 getTask() 此时就要 notifyAll() 唤醒全部线程
ReentrantLock
ReentrantLock 需要手动上锁和释放
public class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n) {
lock.lock();
try {
count += n;
} finally {
lock.unlock();
}
}
}
ReentrantLock 可以尝试获取锁,如果获取不到返回 false,不会导致锁死
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
ReadWriteLock
ReadWriteLock 允许多个线程读取,只允许一个线程写入,提高了读取效率
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
// 注意: 一对读锁和写锁必须从同一个rwlock获取:
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
}
StampedLock
ReadWriteLock 有个问题,就是有线程读取时无法写入,也就是要等所有线程读取完成才能写入,这降低了写入效率,这种属于悲观锁
悲观锁:读取时拒绝写入 乐观锁:读取时可以写入,乐观的估算不会有写入
StampedLock 则允许读取中写入,但是这样也会有问题,就是读取的可能不是最新写入的数据,所以读取方法要判断下是否正在写入,如果有需要再通过获取悲观读锁再次读取。乐观锁顾名思义:乐观的估算写入的概率不高,所以程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。
public class Point {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
Semaphore
Semaphore 可以实现同一时刻最多有N个线程能访问
public class AccessLimitControl {
// 任意时刻仅允许最多3个线程获取许可:
final Semaphore semaphore = new Semaphore(3);
public String access() throws Exception {
// 如果超过了许可数量,其他线程将在此等待:
semaphore.acquire();
try {
// TODO:
return UUID.randomUUID().toString();
} finally {
semaphore.release();
}
}
}
线程池
频繁创建和销毁大量线程需要消耗大量资源,可以创建一组线程执行任务,哪个线程空闲就用哪个。
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建线程池:核心线程2,最大线程4,存活时间10秒,队列容量10
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 提交10个任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
executor.shutdown();
}
}