Java基础(下)

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 中的多线程有一个较为全面的了解,并在实际开发中灵活运用多线程技术。

死锁

通俗解释死锁:两个“倔强的人”互相卡死

想象一个狭窄的走廊里,两个人迎面相遇:

  1. 场景一:互不相让
    • 小明要往东走,小红要往西走。
    • 两人都不愿意侧身让路,僵持在原地,谁也无法通过。
    • 这就是死锁:双方都在等待对方让出资源(走廊空间),但谁也不动。

  1. 场景二:互相抢东西
    • 小明手里拿着遥控器,想用小红手里的电池;
    • 小红手里拿着电池,想用小明的遥控器;
    • 两人都死死抓住自己的东西不放,僵持不下。
    • 这就是死锁:双方都需要对方的资源(遥控器和电池),但都不释放自己已有的资源。

程序中的死锁

在代码中,死锁的四个必要条件:

  1. 互斥:资源(比如打印机)只能被一个线程独占。
  2. 持有并等待:线程A拿着资源X,还想要资源Y;线程B拿着资源Y,还想要资源X。
  3. 不可剥夺:资源不能被强制抢走,只能主动释放。
  4. 循环等待:线程A等线程B,线程B等线程A,形成闭环。

如何避免死锁?

生活中:

  • 约定顺序:比如走廊里规定“靠右走”,避免冲突。
  • 一次性拿全:小明和小红先凑齐遥控器和电池,再开始操作。

代码中:

  1. 按顺序获取锁:所有线程按固定顺序请求资源。
  2. 设置超时:等待超过时间就放弃,避免无限等待。
  3. 避免嵌套锁:尽量一次只持有一个锁。

一句话总结

死锁就像两辆卡车在独木桥上迎面相遇,司机都坚持对方先倒车,结果谁也过不去。解决的关键是打破僵局规则

代码示例

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. 线程1 先拿到 lockA,然后试图拿 lockB
  2. 线程2 先拿到 lockB,然后试图拿 lockA
  3. 两个线程互相持有对方需要的锁,且都不释放自己已有的锁,导致无限等待。

如何验证死锁?

  1. 运行程序,观察输出是否卡住。
  2. 使用 jstack 工具查看线程状态(命令行输入 jstack <进程ID>),会直接提示发现死锁:
    Found one Java-level deadlock:
    ...
    

如何修复?

破坏死锁的四个必要条件之一即可。例如:

  1. 固定锁的获取顺序:让两个线程都先拿 lockA,再拿 lockB
  2. 设置超时时间:用 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();
    }
}
代码解释
  1. 定义线程类:创建一个 MyThread 类继承自 Thread 类,并重写 run() 方法,在 run() 方法中输出线程的名称、优先级和当前执行的次数。
  2. 创建线程对象:创建两个 MyThread 类的对象 lowPriorityThreadhighPriorityThread
  3. 设置线程优先级:使用 setPriority() 方法分别将 lowPriorityThread 的优先级设置为最低优先级(Thread.MIN_PRIORITY),将 highPriorityThread 的优先级设置为最高优先级(Thread.MAX_PRIORITY)。
  4. 启动线程:调用 start() 方法启动两个线程。
注意事项
  • 优先级的相对性:线程优先级是相对的,并不是绝对的。即使一个线程的优先级很高,也不能保证它一定会先执行,因为线程调度还受到操作系统和 JVM 的影响。例如,在某些操作系统中,线程优先级可能会被忽略或者被调整。
  • 优先级的范围:设置线程优先级时,必须确保优先级的值在 1 到 10 之间,否则会抛出 IllegalArgumentException 异常。
  • 不要过度依赖优先级:在编写多线程程序时,不应该过度依赖线程优先级来控制线程的执行顺序。因为线程优先级的效果并不稳定,应该使用线程同步机制(如 synchronized 关键字、Lock 接口等)来确保线程安全和正确的执行顺序。

守护线程

守护线程(Daemon Thread)是 Java 中一种特殊的线程,也被称为后台线程。下面从基本概念、创建方式、特点和使用场景几个方面详细介绍守护线程。

基本概念

守护线程是为其他线程提供服务的线程,当所有的非守护线程(用户线程)执行完毕后,守护线程会自动终止,无论它是否执行完自己的任务。守护线程通常用于执行一些后台任务,如垃圾回收、系统监控等。

守护线程(Daemon Thread) 就像一个默默工作的“服务员”,它的存在是为了服务其他“顾客线程”(普通线程)。当所有顾客线程离开后(执行完毕),服务员会立刻下班,即使它手头的工作还没做完。


核心特点

  1. 后台服务:默默执行后台任务(如垃圾回收、日志记录)。
  2. 依赖主线程:当所有普通线程结束时,守护线程会被强制终止。
  3. 不阻止 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("顾客:离开咖啡店");
    }
}

运行结果

顾客:开始喝咖啡...
服务员:打扫桌子...
服务员:打扫桌子...
服务员:打扫桌子...
顾客:离开咖啡店
(程序结束,服务员线程被强制终止)

关键点

  1. 设置守护线程:必须在 start() 前调用 setDaemon(true)
  2. 生命周期:主线程(顾客)结束后,守护线程(服务员)立即终止,即使它的 while(true) 循环未结束。

常见应用场景

  • 垃圾回收(GC)
  • 后台日志记录
  • 心跳检测
  • 自动保存草稿

注意事项

  1. 守护线程中不要操作关键资源(如数据库写入),因为它可能被突然终止。
  2. 守护线程创建的线程默认也是守护线程。

生产者消费者

生产者 - 消费者模式是一种经典的多线程设计模式,它用于解决多个线程之间的协作问题,主要涉及两类线程:生产者线程和消费者线程。生产者负责生产数据并将其放入共享缓冲区,而消费者则从共享缓冲区中取出数据进行处理。

模式原理
  • 生产者:不断地生成数据,并将数据放入共享缓冲区。如果缓冲区已满,生产者线程会进入等待状态,直到缓冲区有空间可用。
  • 消费者:不断地从共享缓冲区中取出数据进行处理。如果缓冲区为空,消费者线程会进入等待状态,直到缓冲区中有新的数据。
  • 共享缓冲区:是生产者和消费者之间进行数据交换的中介,通常使用队列等数据结构实现。

通俗解释生产者-消费者模式

想象一家面包店:

  • 生产者(厨师):不断制作面包,放到货架上。
  • 消费者(顾客):从货架上购买面包。
  • 货架(缓冲区):存放面包,协调生产和消费速度。

核心问题

  • 货架满了时,厨师必须等待(停止生产)。
  • 货架空了时,顾客必须等待(停止购买)。

生产者-消费者模式通过一个共享缓冲区,让生产者和消费者解耦,避免直接依赖。


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

关键点

  1. 缓冲区 (BlockingQueue)
    • put():队列满时阻塞生产者
    • take():队列空时阻塞消费者
  2. 线程安全BlockingQueue 内部已处理同步问题。
  3. 速度匹配:生产者每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;
    }
}

应用场景

  1. 任务队列(线程池任务调度)
  2. 数据管道(日志处理系统)
  3. 事件驱动系统(GUI 事件分发)

注意事项

  1. 缓冲区大小:过小易阻塞生产者,过大占用内存。
  2. 死锁风险:确保始终有线程能触发 notify
  3. 停止条件:通常需要设置结束标志(如 volatile boolean stop)。

Java基础17 网络编程

网络编程是指编写运行在多个设备(计算机)的程序,这些设备通过网络连接起来,彼此之间可以进行数据交换和通信。在 Java 中,网络编程主要基于 TCP(传输控制协议)和 UDP(用户数据报协议)两种协议,下面分别介绍基于这两种协议的网络编程实现。

基于 TCP 协议的网络编程


用打电话比喻 TCP 网络编程

想象两个人打电话的过程:

  1. 建立连接

    • 服务器像一台固定电话,插着电话线(绑定端口),等待来电。
    • 客户端像拨号的人,输入号码(IP + 端口),拨通后开始对话。
  2. 可靠传输

    • 对方必须接听(连接成功)才能说话。
    • 每句话必须得到回应(ACK 确认机制),没收到就重说。
    • 必须按顺序说话(数据有序到达),不会乱序。
  3. 结束通话

    • 双方明确说“再见”(关闭连接),才挂断电话。

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();
    }
}

关键点总结

  1. 三次握手

    • 客户端说“喂?”(SYN)
    • 服务端说“喂,请讲!”(SYN-ACK)
    • 客户端说“好的!”(ACK)
      —— 连接建立成功!
  2. 应用场景

    • 网页浏览(HTTP)
    • 文件传输(FTP)
    • 实时聊天(需要可靠传输的场景)
  3. 注意事项

    • 端口号范围:0~65535(建议用 1024 以上)
    • 先启动服务端,再启动客户端
    • 关闭流的顺序:后开的先关(像拆包装盒)

基于 UDP 协议的网络编程

用发短信比喻 UDP 网络编程

想象两个人通过发短信交流:

  1. 无连接

    • 不需要拨号建立连接,直接编辑短信发送。
    • 对方可能收到,也可能收不到(不保证可靠性)。
    • 短信可能乱序到达(不保证顺序)。
  2. 快速简单

    • 适合发送简短、实时性高的信息(如直播弹幕、游戏操作)。

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();
    }
}

关键点总结

  1. 无连接:发送前不需要建立连接,直接发送数据包。
  2. 数据包结构
    • DatagramPacket:包含数据内容、目标地址和端口。
    • DatagramSocket:用于发送和接收数据包的信箱。
  3. 适用场景
    • 实时视频/音频传输(如 Zoom 会议)
    • 在线多人游戏(如王者荣耀位置同步)
    • DNS 域名解析
  4. 注意事项
    • 数据可能丢失或乱序,需在应用层处理(如添加序号)。
    • 每个数据包大小不超过 64KB。

对比 TCP

特性UDPTCP
连接方式无连接面向连接
可靠性不保证数据到达保证数据可靠传输
顺序可能乱序严格按顺序
速度相对慢
适用场景实时性要求高,容忍少量丢包数据完整性要求高

通过这个例子,可以轻松理解 UDP 网络编程的基本原理!

Java基础18 静态代理模式

定义与概念

静态代理模式是一种结构型设计模式。在这种模式中,代理类和被代理类在编译时就已经确定下来,代理类在内部持有一个被代理类的实例,并且实现了与被代理类相同的接口。代理类可以在调用被代理类的方法前后,添加一些额外的逻辑,如日志记录、权限检查、事务处理等,而客户端只需要与代理类进行交互,不需要知道被代理类的具体实现。


用“明星经纪人”比喻静态代理

想象一个明星(真实对象)要开演唱会,但需要经纪人(代理对象)处理琐事:

  1. 谈合同:经纪人负责前期沟通。
  2. 收钱:经纪人处理财务问题。
  3. 安排行程:经纪人协调时间。
  4. 明星只负责唱歌:核心功能由明星完成。

静态代理模式代码示例

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();
    }
}

运行结果

经纪人:签订合同,收钱,安排场地
明星:开始唱歌《七里香》
经纪人:清理现场,结算费用

核心特点

  1. 代理与真实对象实现同一接口:经纪人和明星都会“开演唱会”。
  2. 代理控制访问:经纪人决定何时调用明星,并添加额外操作。
  3. 代码不入侵真实对象:明星类无需修改即可复用。

应用场景

  • 日志记录:在方法调用前后记录日志。
  • 权限校验:调用方法前检查用户权限。
  • 延迟初始化:代理负责在需要时才创建高开销对象。

通过这个例子,可以轻松理解静态代理如何在不修改原有类的情况下增强功能

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 的优势

  1. 代码简洁:减少模板代码。
  2. 函数式编程:支持将函数作为参数传递。
  3. 并行处理:方便配合 Stream API 进行并行计算。

通过 Lambda 表达式,Java 代码可以像脚本语言一样简洁高效!

Java基础19 注解

在 Java 中,元注解是用于注解其他注解的特殊注解,它们为自定义注解提供了额外的元数据,比如指定注解的使用范围、生命周期等。Java 提供了几个内置的元注解。


用“标签”或“便利贴”比喻注解

想象你在代码上贴了一张便利贴,告诉其他开发者(或编译器、框架)如何处理这段代码。比如:

  • 标签1@Override → “注意,这个方法要重写父类的!”
  • 标签2@Deprecated → “这个方法过时了,别用了!”
  • 标签3@Autowired → “帮我自动找个合适的对象放进来!”

注解(Annotation) 就是 Java 中的一种“元数据”,用来为代码添加额外信息,但不直接影响代码逻辑


注解的核心作用

  1. 给编译器看:检查代码错误(如 @Override)。
  2. 给框架看:指导框架处理代码(如 Spring 的 @Controller)。
  3. 生成文档:通过注解生成 API 文档(如 @Deprecated)。
  4. 代码分析:辅助工具分析代码(如 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();
        }
    }
}

实际应用场景

  1. 框架配置:Spring 的 @Controller@Service
  2. 单元测试:JUnit 的 @Test@BeforeEach
  3. 数据校验:Hibernate 的 @NotNull@Size
  4. 依赖注入@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(类加载器)就是执行这个加载动作的组件。

类的加载过程

  1. 加载:ClassLoader找到对应的.class文件,将字节码内容读入内存,构建出类的运行时数据结构,并创建java.lang.Class对象,作为程序访问该类的入口。
  2. 链接
    • 验证:检查.class文件的字节码是否符合JVM规范,防止恶意代码或错误格式的代码进入JVM。
    • 准备:为类的静态变量分配内存并设置默认初始值,如int类型为0,对象引用为null
    • 解析:将常量池中的符号引用转换为直接引用,使JVM能直接访问类的成员。
  3. 初始化:执行类的<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)下的类,是自定义类加载器的默认父加载器。
  • 双亲委派模型:当一个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中,获取类运行时结构可通过反射机制实现:

  1. 获取Class对象:有三种常见方式,例如对于User类,可使用User.class;对象实例useruser.getClass();以及Class.forName("com.example.User")(需处理ClassNotFoundException异常)。
  2. 获取类的基本信息
    • 类名:Class对象.getName()获取全限定名,Class对象.getSimpleName()获取简单类名。
    • 包名:Class对象.getPackage().getName()
  3. 获取类的成员变量
    • Class对象.getFields()获取所有公共成员变量;Class对象.getDeclaredFields()获取所有声明的成员变量(包括私有),如需访问私有变量,需Field.setAccessible(true)
  4. 获取类的方法
    • Class对象.getMethods()获取所有公共方法(包括从父类继承的);Class对象.getDeclaredMethods()获取类中声明的所有方法。
  5. 获取类的构造函数
    • Class对象.getConstructors()获取所有公共构造函数;Class对象.getDeclaredConstructors()获取所有声明的构造函数,对于私有构造函数同样可通过Constructor.setAccessible(true)访问。

访问私有属性

在 Java 中,由于访问控制机制,私有属性通常不能直接被外部类访问和操作。但可以通过反射机制绕过这种限制来操作私有属性。

实现思路
  1. 获取 Class 对象:这是反射操作的基础,通过它可以获取类的各种信息。获取方式有多种,比如使用类名的 .class 属性、对象的 getClass() 方法或者 Class.forName() 方法。
  2. 获取私有属性对应的 Field 对象:使用 Class 对象的 getDeclaredField() 方法,该方法可以获取类中声明的指定名称的属性,无论其访问修饰符是什么。
  3. 设置属性可访问:调用 Field 对象的 setAccessible(true) 方法,这会抑制 Java 的访问控制检查,从而允许对私有属性进行操作。
  4. 操作私有属性:可以使用 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 中,可以使用反射机制来获取注解信息。以下为你详细介绍获取注解信息的步骤和示例代码。

步骤分析
  1. 定义注解:使用 @interface 关键字定义自定义注解,并可以使用元注解(如 @Retention@Target 等)来指定注解的保留策略和使用范围。
  2. 使用注解:将定义好的注解应用到类、方法、字段等元素上。
  3. 通过反射获取注解信息
    • 获取对应的 ClassMethodField 等对象。
    • 调用这些对象的 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();
        }
    }
}
代码解释
  1. 注解定义
    • ClassInfo 注解用于标记类,包含 authorversion 两个属性。
    • MethodInfo 注解用于标记方法,包含 descriptionpriority 两个属性。
  2. 注解使用:在 MyClass 类上应用 ClassInfo 注解,在 sampleMethod 方法上应用 MethodInfo 注解。
  3. 反射获取注解信息
    • 使用 isAnnotationPresent() 方法检查类或方法上是否存在指定的注解。
    • 使用 getAnnotation() 方法获取指定类型的注解实例,进而访问注解的属性值。
    • 使用 getAnnotations() 方法获取类或方法上的所有注解。
注意事项
  • 注解保留策略:只有当注解的保留策略为 RetentionPolicy.RUNTIME 时,才能在运行时通过反射获取注解信息。
  • 异常处理:在使用反射获取方法时,可能会抛出 NoSuchMethodException 异常,需要进行适当的异常处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值