Java基础16 多线程
多线程是 Java 编程中一个非常重要的概念,它允许程序在同一时间内执行多个任务,从而提高程序的性能和响应能力。以下将从基本概念、线程的创建方式、线程的生命周期、线程同步和线程池几个方面详细介绍 Java 中的多线程。
基本概念
- 进程:是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。一个进程可以包含多个线程。
- 线程:是进程中的一个执行单元,是 CPU 调度和分派的基本单位。每个线程都有自己的执行路径,可以独立执行任务。
- 多线程:指的是在一个程序中同时运行多个线程,这些线程可以并发或并行执行。并发是指多个线程在同一时间段内交替执行,并行是指多个线程在同一时刻同时执行。
线程的创建方式
Java 提供了三种创建线程的方式:
1. 继承 Thread
类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
代码解释:
- 定义一个类
MyThread
继承自Thread
类,并重写run()
方法,run()
方法中包含线程要执行的任务。 - 创建
MyThread
类的对象,并调用start()
方法启动线程。
2. 实现 Runnable
接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
代码解释:
- 定义一个类
MyRunnable
实现Runnable
接口,并重写run()
方法。 - 创建
MyRunnable
类的对象,并将其作为参数传递给Thread
类的构造方法,然后调用start()
方法启动线程。
3. 实现 Callable
接口
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
return sum;
}
}
public class CallableExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable());
Integer result = future.get();
System.out.println("计算结果: " + result);
executor.shutdown();
}
}
代码解释:
- 定义一个类
MyCallable
实现Callable
接口,并重写call()
方法,call()
方法可以有返回值。 - 创建
MyCallable
类的对象,并使用ExecutorService
提交任务,得到一个Future
对象。 - 通过
Future
对象的get()
方法获取任务的返回结果。
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白" + i);
}
//返回值就表示线程运行完毕之后的结果
return "答应";
}
}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> ft = new FutureTask<>(new MyCallable());
Thread t1 = new Thread(ft);
t1.start(); // 先启动线程
// 在启动线程后调用 get()
String s = ft.get();
System.out.println(s); // 输出 "答应"
}
}
关键点:
先通过 t1.start() 启动线程,执行 call() 方法。
再通过 ft.get() 获取结果,此时主线程会阻塞,直到 call() 执行完毕。
线程的生命周期
Java 线程的生命周期包含以下几种状态:
- 新建(New):线程对象被创建,但还没有调用
start()
方法。 - 就绪(Runnable):线程已经调用了
start()
方法,等待 CPU 调度。 - 运行(Running):线程获得 CPU 时间片,正在执行
run()
方法中的代码。 - 阻塞(Blocked):线程由于某些原因(如等待 I/O 操作、获取锁等)暂时停止执行,进入阻塞状态。
- 等待(Waiting):线程调用了
wait()
、join()
等方法,进入等待状态,需要其他线程唤醒。 - 超时等待(Timed Waiting):线程调用了
sleep()
、wait(long timeout)
等方法,在指定的时间内处于等待状态。 - 终止(Terminated):线程的
run()
方法执行完毕,或者因为异常退出,线程结束生命周期。
线程同步
当多个线程同时访问共享资源时,可能会出现数据不一致的问题,这就需要进行线程同步。Java 提供了以下几种线程同步的机制:
1.同步方法
概念
同步方法是指使用 synchronized
关键字修饰的方法。当一个线程调用同步方法时,它会自动获取该方法所属对象的锁(对于静态同步方法,则是获取该类的 Class 对象的锁),其他线程必须等待该线程释放锁后才能进入该方法。
语法
// 实例同步方法
public synchronized void methodName() {
// 方法体
}
// 静态同步方法
public static synchronized void staticMethodName() {
// 方法体
}
class Counter {
private int count = 0;
// 实例同步方法
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizedMethodExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建两个线程对计数器进行递增操作
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
在上述示例中,increment
方法被声明为同步方法,因此在多线程环境下,每次只有一个线程可以执行该方法,从而保证了 count
变量的安全递增。
特点
- 粒度较大:同步方法会锁定整个方法,即只要有一个线程进入了同步方法,其他线程就无法进入该对象的任何同步方法,可能会影响程序的性能。
- 使用方便:只需要在方法声明中添加
synchronized
关键字,不需要显式地获取和释放锁。
同步块
概念
同步块是指使用 synchronized
关键字修饰的代码块。与同步方法不同,同步块可以指定要锁定的对象,从而可以更细粒度地控制锁的范围,减少锁的持有时间,提高程序的并发性能。
语法
synchronized (锁对象) {
// 同步代码块
}
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
// 同步块
synchronized (lock) {
count++;
}
}
public int getCount() {
return count;
}
}
public class SynchronizedBlockExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建两个线程对计数器进行递增操作
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
在上述示例中,increment
方法中的同步块使用 lock
对象作为锁,只有获取到该锁的线程才能执行同步块中的代码,从而保证了 count
变量的安全递增。
特点
- 粒度较细:可以根据需要选择要锁定的对象,只对需要同步的代码块进行加锁,减少了锁的持有时间,提高了程序的并发性能。
- 灵活性高:可以在不同的代码块中使用不同的锁对象,实现更灵活的同步策略。
同步方法和同步块的比较
- 锁的范围:同步方法锁定的是整个方法,而同步块可以指定更细粒度的锁范围。
- 性能:由于同步块的锁范围更小,因此在高并发场景下,同步块的性能通常优于同步方法。
- 使用场景:如果整个方法都需要同步,使用同步方法比较方便;如果只需要对部分代码进行同步,使用同步块更合适。
2. Lock
接口
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class LockExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Count: " + counter.getCount());
}
}
代码解释:
- 使用
ReentrantLock
实现Lock
接口,通过lock()
方法获取锁,unlock()
方法释放锁,确保同一时间只有一个线程可以执行临界区代码。
线程池
线程池是一种管理线程的机制,它可以重用线程,减少线程创建和销毁的开销,提高程序的性能。Java 提供了 ExecutorService
接口和 Executors
类来创建和管理线程池。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed.");
});
}
executor.shutdown();
}
}
代码解释:
- 使用
Executors.newFixedThreadPool(2)
创建一个固定大小为 2 的线程池。 - 使用
executor.submit()
方法提交任务到线程池。 - 最后调用
executor.shutdown()
方法关闭线程池。
通过以上介绍,你可以对 Java 中的多线程有一个较为全面的了解,并在实际开发中灵活运用多线程技术。
死锁
通俗解释死锁:两个“倔强的人”互相卡死
想象一个狭窄的走廊里,两个人迎面相遇:
- 场景一:互不相让
- 小明要往东走,小红要往西走。
- 两人都不愿意侧身让路,僵持在原地,谁也无法通过。
- 这就是死锁:双方都在等待对方让出资源(走廊空间),但谁也不动。
- 场景二:互相抢东西
- 小明手里拿着遥控器,想用小红手里的电池;
- 小红手里拿着电池,想用小明的遥控器;
- 两人都死死抓住自己的东西不放,僵持不下。
- 这就是死锁:双方都需要对方的资源(遥控器和电池),但都不释放自己已有的资源。
程序中的死锁
在代码中,死锁的四个必要条件:
- 互斥:资源(比如打印机)只能被一个线程独占。
- 持有并等待:线程A拿着资源X,还想要资源Y;线程B拿着资源Y,还想要资源X。
- 不可剥夺:资源不能被强制抢走,只能主动释放。
- 循环等待:线程A等线程B,线程B等线程A,形成闭环。
如何避免死锁?
生活中:
- 约定顺序:比如走廊里规定“靠右走”,避免冲突。
- 一次性拿全:小明和小红先凑齐遥控器和电池,再开始操作。
代码中:
- 按顺序获取锁:所有线程按固定顺序请求资源。
- 设置超时:等待超过时间就放弃,避免无限等待。
- 避免嵌套锁:尽量一次只持有一个锁。
一句话总结
死锁就像两辆卡车在独木桥上迎面相遇,司机都坚持对方先倒车,结果谁也过不去。解决的关键是打破僵局规则。
代码示例
public class DeadlockDemo {
// 定义两把锁(两个对象作为锁)
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
// 线程1:先拿 lockA,再尝试拿 lockB
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread1 拿到了 lockA");
try {
Thread.sleep(100); // 等待,确保线程2拿到 lockB
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println("Thread1 拿到了 lockB"); // 永远执行不到这里!
}
}
});
// 线程2:先拿 lockB,再尝试拿 lockA
Thread thread2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread2 拿到了 lockB");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockA) {
System.out.println("Thread2 拿到了 lockA"); // 永远执行不到这里!
}
}
});
// 启动线程
thread1.start();
thread2.start();
}
}
运行结果
Thread1 拿到了 lockA
Thread2 拿到了 lockB
(程序卡死,不再输出任何内容)
死锁如何发生?
- 线程1 先拿到
lockA
,然后试图拿lockB
。 - 线程2 先拿到
lockB
,然后试图拿lockA
。 - 两个线程互相持有对方需要的锁,且都不释放自己已有的锁,导致无限等待。
如何验证死锁?
- 运行程序,观察输出是否卡住。
- 使用
jstack
工具查看线程状态(命令行输入jstack <进程ID>
),会直接提示发现死锁:Found one Java-level deadlock: ...
如何修复?
破坏死锁的四个必要条件之一即可。例如:
- 固定锁的获取顺序:让两个线程都先拿
lockA
,再拿lockB
。 - 设置超时时间:用
tryLock()
代替synchronized
,超时后放弃锁。
线程优先级
在 Java 中,线程优先级是指线程调度器在调度线程时所依据的一个相对权重,它影响线程获得 CPU 时间片的机会,但并不能保证高优先级的线程一定会先执行。以下将从线程优先级的基本概念、设置方法、注意事项等方面进行详细介绍。
基本概念
Java 中线程的优先级是一个整数,范围从 1 到 10,其中 1 表示最低优先级(Thread.MIN_PRIORITY
),10 表示最高优先级(Thread.MAX_PRIORITY
),默认优先级为 5(Thread.NORM_PRIORITY
)。线程调度器会根据线程的优先级来决定哪个线程更有可能获得 CPU 时间片,但这只是一个概率问题,并不是绝对的。
设置线程优先级的方法
Java 提供了 setPriority(int newPriority)
方法来设置线程的优先级,该方法是 Thread
类的实例方法。同时,还可以使用 getPriority()
方法来获取线程的当前优先级。
示例代码
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " (Priority: " + getPriority() + "): " + i);
try {
// 让线程休眠一段时间,模拟执行任务
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadPriorityExample {
public static void main(String[] args) {
// 创建两个线程
MyThread lowPriorityThread = new MyThread("Low Priority Thread");
MyThread highPriorityThread = new MyThread("High Priority Thread");
// 设置线程优先级
lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
// 启动线程
lowPriorityThread.start();
highPriorityThread.start();
}
}
代码解释
- 定义线程类:创建一个
MyThread
类继承自Thread
类,并重写run()
方法,在run()
方法中输出线程的名称、优先级和当前执行的次数。 - 创建线程对象:创建两个
MyThread
类的对象lowPriorityThread
和highPriorityThread
。 - 设置线程优先级:使用
setPriority()
方法分别将lowPriorityThread
的优先级设置为最低优先级(Thread.MIN_PRIORITY
),将highPriorityThread
的优先级设置为最高优先级(Thread.MAX_PRIORITY
)。 - 启动线程:调用
start()
方法启动两个线程。
注意事项
- 优先级的相对性:线程优先级是相对的,并不是绝对的。即使一个线程的优先级很高,也不能保证它一定会先执行,因为线程调度还受到操作系统和 JVM 的影响。例如,在某些操作系统中,线程优先级可能会被忽略或者被调整。
- 优先级的范围:设置线程优先级时,必须确保优先级的值在 1 到 10 之间,否则会抛出
IllegalArgumentException
异常。 - 不要过度依赖优先级:在编写多线程程序时,不应该过度依赖线程优先级来控制线程的执行顺序。因为线程优先级的效果并不稳定,应该使用线程同步机制(如
synchronized
关键字、Lock
接口等)来确保线程安全和正确的执行顺序。
守护线程
守护线程(Daemon Thread)是 Java 中一种特殊的线程,也被称为后台线程。下面从基本概念、创建方式、特点和使用场景几个方面详细介绍守护线程。
基本概念
守护线程是为其他线程提供服务的线程,当所有的非守护线程(用户线程)执行完毕后,守护线程会自动终止,无论它是否执行完自己的任务。守护线程通常用于执行一些后台任务,如垃圾回收、系统监控等。
守护线程(Daemon Thread) 就像一个默默工作的“服务员”,它的存在是为了服务其他“顾客线程”(普通线程)。当所有顾客线程离开后(执行完毕),服务员会立刻下班,即使它手头的工作还没做完。
核心特点
- 后台服务:默默执行后台任务(如垃圾回收、日志记录)。
- 依赖主线程:当所有普通线程结束时,守护线程会被强制终止。
- 不阻止 JVM 退出:JVM 不会等待守护线程执行完毕。
示例代码:咖啡店服务员
假设咖啡店有两个角色:
- 顾客线程:喝咖啡的主线程。
- 服务员线程:守护线程,循环打扫卫生。
public class DaemonThreadDemo {
public static void main(String[] args) {
// 创建一个守护线程(服务员)
Thread waiter = new Thread(() -> {
while (true) {
System.out.println("服务员:打扫桌子...");
try {
Thread.sleep(1000); // 每隔1秒打扫一次
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
waiter.setDaemon(true); // 设置为守护线程(关键代码!)
waiter.start(); // 启动守护线程
// 主线程(顾客)喝咖啡,持续3秒
System.out.println("顾客:开始喝咖啡...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("顾客:离开咖啡店");
}
}
运行结果
顾客:开始喝咖啡...
服务员:打扫桌子...
服务员:打扫桌子...
服务员:打扫桌子...
顾客:离开咖啡店
(程序结束,服务员线程被强制终止)
关键点
- 设置守护线程:必须在
start()
前调用setDaemon(true)
。 - 生命周期:主线程(顾客)结束后,守护线程(服务员)立即终止,即使它的
while(true)
循环未结束。
常见应用场景
- 垃圾回收(GC)
- 后台日志记录
- 心跳检测
- 自动保存草稿
注意事项
- 守护线程中不要操作关键资源(如数据库写入),因为它可能被突然终止。
- 守护线程创建的线程默认也是守护线程。
生产者消费者
生产者 - 消费者模式是一种经典的多线程设计模式,它用于解决多个线程之间的协作问题,主要涉及两类线程:生产者线程和消费者线程。生产者负责生产数据并将其放入共享缓冲区,而消费者则从共享缓冲区中取出数据进行处理。
模式原理
- 生产者:不断地生成数据,并将数据放入共享缓冲区。如果缓冲区已满,生产者线程会进入等待状态,直到缓冲区有空间可用。
- 消费者:不断地从共享缓冲区中取出数据进行处理。如果缓冲区为空,消费者线程会进入等待状态,直到缓冲区中有新的数据。
- 共享缓冲区:是生产者和消费者之间进行数据交换的中介,通常使用队列等数据结构实现。
通俗解释生产者-消费者模式
想象一家面包店:
- 生产者(厨师):不断制作面包,放到货架上。
- 消费者(顾客):从货架上购买面包。
- 货架(缓冲区):存放面包,协调生产和消费速度。
核心问题:
- 货架满了时,厨师必须等待(停止生产)。
- 货架空了时,顾客必须等待(停止购买)。
生产者-消费者模式通过一个共享缓冲区,让生产者和消费者解耦,避免直接依赖。
Java 代码示例(使用 BlockingQueue
实现)
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumerDemo {
public static void main(String[] args) {
// 1. 创建缓冲区(货架),容量为3
BlockingQueue<Integer> shelf = new LinkedBlockingQueue<>(3);
// 2. 创建生产者(厨师)
Runnable producer = () -> {
try {
for (int i = 1; i <= 5; i++) {
shelf.put(i); // 生产面包(如果货架满则阻塞)
System.out.println("生产面包: " + i);
Thread.sleep(500); // 模拟生产时间
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// 3. 创建消费者(顾客)
Runnable consumer = () -> {
try {
for (int i = 0; i < 5; i++) {
int bread = shelf.take(); // 购买面包(如果货架空则阻塞)
System.out.println("消费面包: " + bread);
Thread.sleep(1000); // 模拟消费时间
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
// 4. 启动线程
new Thread(producer).start();
new Thread(consumer).start();
}
}
运行结果
生产面包: 1
消费面包: 1
生产面包: 2
生产面包: 3 // 货架已满,生产者暂停
消费面包: 2
生产面包: 4 // 货架有空位,继续生产
消费面包: 3
生产面包: 5
消费面包: 4
消费面包: 5
关键点
- 缓冲区 (
BlockingQueue
):put()
:队列满时阻塞生产者。take()
:队列空时阻塞消费者。
- 线程安全:
BlockingQueue
内部已处理同步问题。 - 速度匹配:生产者每0.5秒生产一个,消费者每1秒消费一个,通过缓冲区平衡速度差异。
传统实现(手动同步)
class Buffer {
private final int[] data = new int[3]; // 货架容量3
private int count = 0;
// 生产者调用
public synchronized void produce(int value) throws InterruptedException {
while (count == data.length) {
wait(); // 货架满,等待
}
data[count++] = value;
notifyAll(); // 通知消费者
}
// 消费者调用
public synchronized int consume() throws InterruptedException {
while (count == 0) {
wait(); // 货架空,等待
}
int value = data[--count];
notifyAll(); // 通知生产者
return value;
}
}
应用场景
- 任务队列(线程池任务调度)
- 数据管道(日志处理系统)
- 事件驱动系统(GUI 事件分发)
注意事项
- 缓冲区大小:过小易阻塞生产者,过大占用内存。
- 死锁风险:确保始终有线程能触发
notify
。 - 停止条件:通常需要设置结束标志(如
volatile boolean stop
)。
Java基础17 网络编程
网络编程是指编写运行在多个设备(计算机)的程序,这些设备通过网络连接起来,彼此之间可以进行数据交换和通信。在 Java 中,网络编程主要基于 TCP(传输控制协议)和 UDP(用户数据报协议)两种协议,下面分别介绍基于这两种协议的网络编程实现。
基于 TCP 协议的网络编程
用打电话比喻 TCP 网络编程
想象两个人打电话的过程:
-
建立连接:
- 服务器像一台固定电话,插着电话线(绑定端口),等待来电。
- 客户端像拨号的人,输入号码(IP + 端口),拨通后开始对话。
-
可靠传输:
- 对方必须接听(连接成功)才能说话。
- 每句话必须得到回应(ACK 确认机制),没收到就重说。
- 必须按顺序说话(数据有序到达),不会乱序。
-
结束通话:
- 双方明确说“再见”(关闭连接),才挂断电话。
TCP 编程核心步骤
1. 服务端(接电话的人)
// 服务端代码
public class Server {
public static void main(String[] args) throws IOException {
// 1. 买一台电话机(创建 ServerSocket),绑定号码 8888
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务端已启动,等待连接...");
// 2. 等待来电(阻塞直到客户端连接)
Socket socket = serverSocket.accept();
System.out.println("客户端已连接!");
// 3. 获取电话听筒的输入流(接收数据)
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
// 获取输出流(发送数据)
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 4. 读取客户端消息
String message = in.readLine();
System.out.println("客户端说:" + message);
// 5. 回复客户端
out.println("你好,我是服务端!");
// 6. 挂断电话(关闭资源)
in.close();
out.close();
socket.close();
serverSocket.close();
}
}
2. 客户端(拨电话的人)
// 客户端代码
public class Client {
public static void main(String[] args) throws IOException {
// 1. 输入对方的号码(IP + 端口)
Socket socket = new Socket("localhost", 8888);
System.out.println("已连接到服务端!");
// 2. 获取输出流(发送数据)
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 获取输入流(接收数据)
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
// 3. 发送消息给服务端
out.println("你好,我是客户端!");
// 4. 读取服务端回复
String reply = in.readLine();
System.out.println("服务端说:" + reply);
// 5. 挂断电话(关闭资源)
out.close();
in.close();
socket.close();
}
}
关键点总结
-
三次握手:
- 客户端说“喂?”(SYN)
- 服务端说“喂,请讲!”(SYN-ACK)
- 客户端说“好的!”(ACK)
—— 连接建立成功!
-
应用场景:
- 网页浏览(HTTP)
- 文件传输(FTP)
- 实时聊天(需要可靠传输的场景)
-
注意事项:
- 端口号范围:0~65535(建议用 1024 以上)
- 先启动服务端,再启动客户端
- 关闭流的顺序:后开的先关(像拆包装盒)
基于 UDP 协议的网络编程
用发短信比喻 UDP 网络编程
想象两个人通过发短信交流:
-
无连接:
- 不需要拨号建立连接,直接编辑短信发送。
- 对方可能收到,也可能收不到(不保证可靠性)。
- 短信可能乱序到达(不保证顺序)。
-
快速简单:
- 适合发送简短、实时性高的信息(如直播弹幕、游戏操作)。
UDP 编程核心步骤
1. 接收端(收短信的人)
public class Receiver {
public static void main(String[] args) throws IOException {
// 1. 创建信箱(绑定端口 8888)
DatagramSocket socket = new DatagramSocket(8888);
System.out.println("接收端已启动,等待数据...");
// 2. 准备空信封(接收缓冲区)
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// 3. 等待短信(阻塞直到收到数据)
socket.receive(packet);
// 4. 读取短信内容
String message = new String(packet.getData(), 0, packet.getLength());
System.out.println("收到来自 " + packet.getAddress() + " 的消息:" + message);
// 5. 关闭信箱
socket.close();
}
}
2. 发送端(发短信的人)
public class Sender {
public static void main(String[] args) throws IOException {
// 1. 创建信箱(不绑定端口,随机分配)
DatagramSocket socket = new DatagramSocket();
// 2. 准备短信内容
String message = "今晚一起吃饭吗?";
byte[] data = message.getBytes();
// 3. 填写收件人地址(IP + 端口)
InetAddress address = InetAddress.getByName("localhost");
DatagramPacket packet = new DatagramPacket(data, data.length, address, 8888);
// 4. 发送短信
socket.send(packet);
System.out.println("已发送消息:" + message);
// 5. 关闭信箱
socket.close();
}
}
关键点总结
- 无连接:发送前不需要建立连接,直接发送数据包。
- 数据包结构:
DatagramPacket
:包含数据内容、目标地址和端口。DatagramSocket
:用于发送和接收数据包的信箱。
- 适用场景:
- 实时视频/音频传输(如 Zoom 会议)
- 在线多人游戏(如王者荣耀位置同步)
- DNS 域名解析
- 注意事项:
- 数据可能丢失或乱序,需在应用层处理(如添加序号)。
- 每个数据包大小不超过 64KB。
对比 TCP
特性 | UDP | TCP |
---|---|---|
连接方式 | 无连接 | 面向连接 |
可靠性 | 不保证数据到达 | 保证数据可靠传输 |
顺序 | 可能乱序 | 严格按顺序 |
速度 | 快 | 相对慢 |
适用场景 | 实时性要求高,容忍少量丢包 | 数据完整性要求高 |
通过这个例子,可以轻松理解 UDP 网络编程的基本原理!
Java基础18 静态代理模式
定义与概念
静态代理模式是一种结构型设计模式。在这种模式中,代理类和被代理类在编译时就已经确定下来,代理类在内部持有一个被代理类的实例,并且实现了与被代理类相同的接口。代理类可以在调用被代理类的方法前后,添加一些额外的逻辑,如日志记录、权限检查、事务处理等,而客户端只需要与代理类进行交互,不需要知道被代理类的具体实现。
用“明星经纪人”比喻静态代理
想象一个明星(真实对象)要开演唱会,但需要经纪人(代理对象)处理琐事:
- 谈合同:经纪人负责前期沟通。
- 收钱:经纪人处理财务问题。
- 安排行程:经纪人协调时间。
- 明星只负责唱歌:核心功能由明星完成。
静态代理模式代码示例
1. 定义接口(明星和经纪人的共同能力)
// 定义共同行为:开演唱会
public interface Singer {
void sing();
}
2. 创建真实对象(明星)
public class Star implements Singer {
@Override
public void sing() {
System.out.println("明星:开始唱歌《七里香》");
}
}
3. 创建代理对象(经纪人)
public class Agent implements Singer {
private Singer star; // 持有一个明星对象
public Agent(Singer star) {
this.star = star;
}
@Override
public void sing() {
System.out.println("经纪人:签订合同,收钱,安排场地");
star.sing(); // 调用明星的核心功能
System.out.println("经纪人:清理现场,结算费用");
}
}
4. 客户端使用代理
public class Client {
public static void main(String[] args) {
// 创建明星对象
Singer star = new Star();
// 创建经纪人代理,传入明星对象
Singer agent = new Agent(star);
// 通过经纪人开演唱会
agent.sing();
}
}
运行结果
经纪人:签订合同,收钱,安排场地
明星:开始唱歌《七里香》
经纪人:清理现场,结算费用
核心特点
- 代理与真实对象实现同一接口:经纪人和明星都会“开演唱会”。
- 代理控制访问:经纪人决定何时调用明星,并添加额外操作。
- 代码不入侵真实对象:明星类无需修改即可复用。
应用场景
- 日志记录:在方法调用前后记录日志。
- 权限校验:调用方法前检查用户权限。
- 延迟初始化:代理负责在需要时才创建高开销对象。
通过这个例子,可以轻松理解静态代理如何在不修改原有类的情况下增强功能!
Java基础19 lamda表达式
Lambda 表达式是 Java 8 引入的一个重要特性,它允许你将函数作为方法的参数传递,或者将代码像数据一样传递,本质上是一个匿名函数。下面从多个方面为你详细介绍 Lambda 表达式:
Lambda 表达式核心语法
(参数) -> { 代码逻辑 }
- 左侧
(参数)
:方法的参数列表。 - 箭头
->
:分隔参数和代码逻辑。 - 右侧
{代码逻辑}
:方法的具体实现(单行可省略{}
和return
)。
代码示例对比
场景1:用 Runnable
启动线程
// 传统写法(匿名内部类)
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程启动!");
}
}).start();
// Lambda 写法(简洁版)
new Thread(() -> System.out.println("线程启动!")).start();
场景2:用 Comparator
排序集合
List<Integer> list = Arrays.asList(3, 1, 4);
// 传统写法
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a - b;
}
});
// Lambda 写法
Collections.sort(list, (a, b) -> a - b);
场景3:自定义函数式接口
// 定义函数式接口(只有一个抽象方法)
interface Calculator {
int calculate(int a, int b);
}
public class Main {
public static void main(String[] args) {
// Lambda 实现加法
Calculator add = (a, b) -> a + b;
System.out.println(add.calculate(2, 3)); // 输出 5
// Lambda 实现乘法
Calculator multiply = (x, y) -> x * y;
System.out.println(multiply.calculate(2, 3)); // 输出 6
}
}
Lambda 表达式变体
1. 无参数
Runnable task = () -> System.out.println("无参数Lambda");
2. 单个参数
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Hello Lambda!");
3. 多行代码
Runnable multiLine = () -> {
System.out.println("第一行");
System.out.println("第二行");
};
Lambda 结合 Java 内置函数式接口
接口 | 用途 | Lambda 示例 |
---|---|---|
Consumer<T> | 消费一个参数 | s -> System.out.println(s) |
Supplier<T> | 提供一个结果 | () -> "Hello" |
Function<T,R> | 转换参数为结果 | s -> s.length() |
Predicate<T> | 条件判断 | s -> s.startsWith("A") |
示例:使用 Consumer
遍历集合
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
Lambda 的优势
- 代码简洁:减少模板代码。
- 函数式编程:支持将函数作为参数传递。
- 并行处理:方便配合 Stream API 进行并行计算。
通过 Lambda 表达式,Java 代码可以像脚本语言一样简洁高效!
Java基础19 注解
在 Java 中,元注解是用于注解其他注解的特殊注解,它们为自定义注解提供了额外的元数据,比如指定注解的使用范围、生命周期等。Java 提供了几个内置的元注解。
用“标签”或“便利贴”比喻注解
想象你在代码上贴了一张便利贴,告诉其他开发者(或编译器、框架)如何处理这段代码。比如:
- 标签1:
@Override
→ “注意,这个方法要重写父类的!” - 标签2:
@Deprecated
→ “这个方法过时了,别用了!” - 标签3:
@Autowired
→ “帮我自动找个合适的对象放进来!”
注解(Annotation) 就是 Java 中的一种“元数据”,用来为代码添加额外信息,但不直接影响代码逻辑。
注解的核心作用
- 给编译器看:检查代码错误(如
@Override
)。 - 给框架看:指导框架处理代码(如 Spring 的
@Controller
)。 - 生成文档:通过注解生成 API 文档(如
@Deprecated
)。 - 代码分析:辅助工具分析代码(如 Lombok 的
@Data
)。
Java 内置的常用注解
1. @Override
:检查方法重写
class Animal {
void eat() { System.out.println("动物吃东西"); }
}
class Cat extends Animal {
@Override // 明确告诉编译器这是重写父类方法
void eat() { System.out.println("猫吃鱼"); }
}
2. @Deprecated
:标记过时方法
class OldClass {
@Deprecated
void oldMethod() { System.out.println("过时方法"); }
}
public class Main {
public static void main(String[] args) {
OldClass obj = new OldClass();
obj.oldMethod(); // 编译器会警告:方法已过时
}
}
3. @SuppressWarnings
:忽略警告
@SuppressWarnings("unchecked") // 告诉编译器忽略“未检查类型”的警告
List list = new ArrayList();
元注解(注解的注解)
元注解 | 作用 |
---|---|
@Target | 指定注解能贴在哪儿(类、方法、字段等) |
@Retention | 指定注解的有效期(源码、编译期、运行时) |
@Documented | 注解会被包含在 Javadoc 中 |
@Inherited | 子类会继承父类的注解 |
@Repeatable | 允许在同一位置重复使用注解 |
@Repeatable
(Java 8 引入)
@Repeatable
注解允许在同一个程序元素上多次使用同一个注解。要使用 @Repeatable
,需要定义一个容器注解,该容器注解用于存储重复的注解。
示例代码:
import java.lang.annotation.Repeatable;
// 定义容器注解
@interface MyAnnotations {
MyAnnotation[] value();
}
// 使用 @Repeatable 指定容器注解
@Repeatable(MyAnnotations.class)
@interface MyAnnotation {
String value();
}
// 多次使用 MyAnnotation 注解
@MyAnnotation("value1")
@MyAnnotation("value2")
class MyClass {}
在上述代码中,MyAnnotation
注解被声明为可重复的,MyAnnotations
是其容器注解,这样 MyClass
就可以多次使用 @MyAnnotation
注解。
如何自定义注解?
自定义注解格式:
@元注解
public @interface 注解名 {
// 定义属性(类似方法)
String value() default "";
int priority() default 0;
}
示例:定义一个“作者信息”注解
// 元注解:指定注解可以贴在类或方法上
@Target({ElementType.TYPE, ElementType.METHOD})
// 元注解:指定注解在运行时有效(可通过反射读取)
@Retention(RetentionPolicy.RUNTIME)
public @interface Author {
String name(); // 必填属性
String date() default "2023-10-01"; // 可选属性
}
使用自定义注解
@Author(name = "张三", date = "2023-10-25")
public class MyClass {
@Author(name = "李四")
public void myMethod() { /* ... */ }
}
如何读取注解信息?(反射)
public class Main {
public static void main(String[] args) {
// 获取类上的注解
Author classAnnotation = MyClass.class.getAnnotation(Author.class);
System.out.println("类作者: " + classAnnotation.name()); // 输出 "张三"
// 获取方法上的注解
try {
Method method = MyClass.class.getMethod("myMethod");
Author methodAnnotation = method.getAnnotation(Author.class);
System.out.println("方法作者: " + methodAnnotation.name()); // 输出 "李四"
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
实际应用场景
- 框架配置:Spring 的
@Controller
、@Service
。 - 单元测试:JUnit 的
@Test
、@BeforeEach
。 - 数据校验:Hibernate 的
@NotNull
、@Size
。 - 依赖注入:
@Autowired
、@Resource
。
特殊属性 value
如果注解中只有一个属性,并且该属性名为 value
,那么在使用注解时可以省略属性名。示例如下:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 定义只有一个 value 属性的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface SingleValueAnnotation {
String value();
}
public class SingleValueAnnotationExample {
// 使用注解时省略属性名
@SingleValueAnnotation("This is a single value annotation")
public void mySingleValueMethod() {
// 方法体
}
public static void main(String[] args) throws NoSuchMethodException {
Method method = SingleValueAnnotationExample.class.getMethod("mySingleValueMethod");
if (method.isAnnotationPresent(SingleValueAnnotation.class)) {
SingleValueAnnotation annotation = method.getAnnotation(SingleValueAnnotation.class);
System.out.println(annotation.value());
}
}
}
Java基础20 反射
1.类的加载与ClassLoader的理解
类的加载是Java程序运行时将类的信息引入JVM的过程,而ClassLoader(类加载器)就是执行这个加载动作的组件。
类的加载过程
- 加载:ClassLoader找到对应的
.class
文件,将字节码内容读入内存,构建出类的运行时数据结构,并创建java.lang.Class
对象,作为程序访问该类的入口。 - 链接
- 验证:检查
.class
文件的字节码是否符合JVM规范,防止恶意代码或错误格式的代码进入JVM。 - 准备:为类的静态变量分配内存并设置默认初始值,如
int
类型为0,对象引用为null
。 - 解析:将常量池中的符号引用转换为直接引用,使JVM能直接访问类的成员。
- 验证:检查
- 初始化:执行类的
<clinit>()
方法,对静态变量进行赋值和执行静态代码块,若父类未初始化则先初始化父类。
ClassLoader
- 作用:负责在运行时动态加载类,让JVM能够找到并使用对应的类。它使得Java具备动态扩展和模块化的能力,不同的ClassLoader可以管理不同来源、不同版本的类。
- 分类
- 启动类加载器(Bootstrap ClassLoader):用C++实现,是最顶层的加载器,负责加载JRE核心类库,如
java.lang.*
包下的类。 - 扩展类加载器(Extension ClassLoader):Java编写,继承自
URLClassLoader
,加载jre/lib/ext
目录或java.ext.dirs
系统属性指定目录下的类库。 - 应用程序类加载器(Application ClassLoader):也由Java编写,负责加载应用程序的类路径(classpath)下的类,是自定义类加载器的默认父加载器。
- 启动类加载器(Bootstrap ClassLoader):用C++实现,是最顶层的加载器,负责加载JRE核心类库,如
- 双亲委派模型:当一个ClassLoader收到类加载请求时,它首先不会自己尝试加载,而是把请求委托给父加载器,只有父加载器无法完成加载时,自己才会尝试加载。这种机制保证了基础类的一致性和安全性,避免用户自定义的类覆盖核心类库中的类。
2.什么时候会发生类初始化?
在Java中,类的初始化是类加载过程的最后一个阶段,当满足特定条件时会触发类的初始化。以下是详细介绍类初始化发生的时机:
主动引用触发类初始化
1. 创建类的实例
当使用new
关键字创建一个类的对象时,会触发该类的初始化。例如:
class MyClass {
static {
System.out.println("MyClass 类被初始化");
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass(); // 触发 MyClass 类的初始化
}
}
2. 访问类的静态变量
当访问一个类的静态变量(非final
常量)时,会触发该类的初始化。例如:
class MyClass {
static int staticVar = 10;
static {
System.out.println("MyClass 类被初始化");
}
}
public class Main {
public static void main(String[] args) {
int value = MyClass.staticVar; // 触发 MyClass 类的初始化
}
}
当调用一个类的静态方法时,会触发该类的初始化。例如:
class MyClass {
static void staticMethod() {
System.out.println("执行静态方法");
}
static {
System.out.println("MyClass 类被初始化");
}
}
public class Main {
public static void main(String[] args) {
MyClass.staticMethod(); // 触发 MyClass 类的初始化
}
}
4. 使用反射调用类
当使用java.lang.reflect
包中的方法对类进行反射调用时,如果类还没有被初始化,则会触发类的初始化。例如:
class MyClass {
static {
System.out.println("MyClass 类被初始化");
}
}
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("MyClass"); // 触发 MyClass 类的初始化
}
}
使用Class.forName
方法加载MyClass
类时,会触发其初始化。
5. 初始化子类
如果一个子类被初始化,而其父类还没有被初始化,则会先触发父类的初始化。例如:
class ParentClass {
static {
System.out.println("ParentClass 类被初始化");
}
}
class ChildClass extends ParentClass {
static {
System.out.println("ChildClass 类被初始化");
}
}
public class Main {
public static void main(String[] args) {
ChildClass child = new ChildClass(); // 先初始化 ParentClass,再初始化 ChildClass
}
}
创建ChildClass
的实例时,会先初始化其父类ParentClass
,再初始化ChildClass
。
6. 启动类(包含main
方法的类)
Java虚拟机启动时,会初始化包含main
方法的类。例如:
public class Main {
static {
System.out.println("Main 类被初始化");
}
public static void main(String[] args) {
System.out.println("程序开始执行");
}
}
JVM启动时,会先初始化Main
类,执行其静态代码块,然后执行main
方法。
被动引用不会触发类初始化
- 访问类的静态常量(
static final
修饰)不会触发类的初始化,因为静态常量在编译阶段就已经存入调用类的常量池中,本质上没有直接引用到定义常量的类。 - 通过数组定义来引用类,不会触发此类的初始化。例如
MyClass[] arr = new MyClass[10];
不会触发MyClass
类的初始化。
类加载器的作用
获取类运行时结构
在Java中,获取类运行时结构可通过反射机制实现:
- 获取Class对象:有三种常见方式,例如对于
User
类,可使用User.class
;对象实例user
的user.getClass()
;以及Class.forName("com.example.User")
(需处理ClassNotFoundException
异常)。 - 获取类的基本信息:
- 类名:
Class对象.getName()
获取全限定名,Class对象.getSimpleName()
获取简单类名。 - 包名:
Class对象.getPackage().getName()
。
- 类名:
- 获取类的成员变量:
Class对象.getFields()
获取所有公共成员变量;Class对象.getDeclaredFields()
获取所有声明的成员变量(包括私有),如需访问私有变量,需Field.setAccessible(true)
。
- 获取类的方法:
Class对象.getMethods()
获取所有公共方法(包括从父类继承的);Class对象.getDeclaredMethods()
获取类中声明的所有方法。
- 获取类的构造函数:
Class对象.getConstructors()
获取所有公共构造函数;Class对象.getDeclaredConstructors()
获取所有声明的构造函数,对于私有构造函数同样可通过Constructor.setAccessible(true)
访问。
访问私有属性
在 Java 中,由于访问控制机制,私有属性通常不能直接被外部类访问和操作。但可以通过反射机制绕过这种限制来操作私有属性。
实现思路
- 获取
Class
对象:这是反射操作的基础,通过它可以获取类的各种信息。获取方式有多种,比如使用类名的.class
属性、对象的getClass()
方法或者Class.forName()
方法。 - 获取私有属性对应的
Field
对象:使用Class
对象的getDeclaredField()
方法,该方法可以获取类中声明的指定名称的属性,无论其访问修饰符是什么。 - 设置属性可访问:调用
Field
对象的setAccessible(true)
方法,这会抑制 Java 的访问控制检查,从而允许对私有属性进行操作。 - 操作私有属性:可以使用
Field
对象的get()
方法获取属性的值,使用set()
方法设置属性的值。
import java.lang.reflect.Field;
class PrivateFieldExample {
// 定义一个私有属性
private String privateField = "初始值";
// 提供一个获取私有属性值的公共方法,用于验证修改结果
public String getPrivateField() {
return privateField;
}
}
public class AccessPrivateField {
public static void main(String[] args) {
try {
// 1. 创建对象
PrivateFieldExample example = new PrivateFieldExample();
// 2. 获取 Class 对象
Class<?> clazz = example.getClass();
// 3. 获取私有属性对应的 Field 对象
Field privateField = clazz.getDeclaredField("privateField");
// 4. 设置属性可访问
privateField.setAccessible(true);
// 5. 获取私有属性的值
String value = (String) privateField.get(example);
System.out.println("修改前私有属性的值: " + value);
// 6. 修改私有属性的值
privateField.set(example, "修改后的值");
// 7. 再次获取私有属性的值,验证修改结果
value = (String) privateField.get(example);
System.out.println("修改后私有属性的值: " + value);
// 也可以通过对象的公共方法验证修改结果
System.out.println("通过公共方法获取修改后的值: " + example.getPrivateField());
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
获取注解信息
在 Java 中,可以使用反射机制来获取注解信息。以下为你详细介绍获取注解信息的步骤和示例代码。
步骤分析
- 定义注解:使用
@interface
关键字定义自定义注解,并可以使用元注解(如@Retention
、@Target
等)来指定注解的保留策略和使用范围。 - 使用注解:将定义好的注解应用到类、方法、字段等元素上。
- 通过反射获取注解信息:
- 获取对应的
Class
、Method
、Field
等对象。 - 调用这些对象的
getAnnotation()
或getAnnotations()
等方法来获取注解信息。
- 获取对应的
1. 定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 定义一个注解,用于标记类
@Retention(RetentionPolicy.RUNTIME) // 注解保留到运行时
@Target(ElementType.TYPE) // 注解可以应用于类
@interface ClassInfo {
String author();
String version() default "1.0";
}
// 定义一个注解,用于标记方法
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MethodInfo {
String description();
int priority() default 3;
}
2. 使用注解
// 应用 ClassInfo 注解到类上
@ClassInfo(author = "John Doe", version = "2.0")
class MyClass {
// 应用 MethodInfo 注解到方法上
@MethodInfo(description = "This is a sample method", priority = 2)
public void sampleMethod() {
System.out.println("Running sample method.");
}
}
3. 通过反射获取注解信息
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
public class AnnotationReflectionExample {
public static void main(String[] args) {
try {
// 获取 MyClass 的 Class 对象
Class<?> myClass = MyClass.class;
// 获取类上的注解
if (myClass.isAnnotationPresent(ClassInfo.class)) {
ClassInfo classInfo = myClass.getAnnotation(ClassInfo.class);
System.out.println("Class Author: " + classInfo.author());
System.out.println("Class Version: " + classInfo.version());
}
// 获取方法上的注解
Method method = myClass.getMethod("sampleMethod");
if (method.isAnnotationPresent(MethodInfo.class)) {
MethodInfo methodInfo = method.getAnnotation(MethodInfo.class);
System.out.println("Method Description: " + methodInfo.description());
System.out.println("Method Priority: " + methodInfo.priority());
}
// 获取类上的所有注解
Annotation[] classAnnotations = myClass.getAnnotations();
System.out.println("All annotations on class:");
for (Annotation annotation : classAnnotations) {
System.out.println(annotation.annotationType().getName());
}
// 获取方法上的所有注解
Annotation[] methodAnnotations = method.getAnnotations();
System.out.println("All annotations on method:");
for (Annotation annotation : methodAnnotations) {
System.out.println(annotation.annotationType().getName());
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
代码解释
- 注解定义:
ClassInfo
注解用于标记类,包含author
和version
两个属性。MethodInfo
注解用于标记方法,包含description
和priority
两个属性。
- 注解使用:在
MyClass
类上应用ClassInfo
注解,在sampleMethod
方法上应用MethodInfo
注解。 - 反射获取注解信息:
- 使用
isAnnotationPresent()
方法检查类或方法上是否存在指定的注解。 - 使用
getAnnotation()
方法获取指定类型的注解实例,进而访问注解的属性值。 - 使用
getAnnotations()
方法获取类或方法上的所有注解。
- 使用
注意事项
- 注解保留策略:只有当注解的保留策略为
RetentionPolicy.RUNTIME
时,才能在运行时通过反射获取注解信息。 - 异常处理:在使用反射获取方法时,可能会抛出
NoSuchMethodException
异常,需要进行适当的异常处理。