-
一篇笔记主要介绍 gin.Engine,设置路由等操作,以下是本篇笔记目录:gin.Default() 和 gin.New()HTTP 方法路由分组与中间件1、gin.Default() 和 gin.New()前面第一篇笔记介绍,创建一个 gin 的路由引擎使用的函数是 gin.Default(),返回的类型是 *gin.Engine,我们可以使用其创建路由和路由组。除了这个函数外,还有一个 gin.New(),其返回的也是 *gin.Engine,但是不一样的是 gin.Default() 会对 gin.Engine 添加默认的 Logger() 和 Recovery() 中间件。这两个函数大致内容如下:func New(opts ...OptionFunc) *Engine { ...}func Default(opts ...OptionFunc) *Engine { ... engine := New() engine.Use(Logger(), Recovery()) ...}我们使用第一篇笔记中使用 debug 模式运行系统后输出的信息可以再看一下:[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode)[GIN-debug] GET /test --> main.main.func1 (3 handlers)[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.[GIN-debug] Listening and serving HTTP on :9898我们使用的是 gin.Default() 运行的系统,所以在第一行告诉我们创建了一个带有 Logger and Recovery 中间件的 Engine。同时第三行输出路由信息的地方,标明了这个路由指向的处理函数,后面的括号里是 3 handlers,这个意思是除了我们处理路由的 handler,还有两个默认的中间件 handler,也就是这里的 Logger() 和 Recovery() 中间件。下面介绍一下 Logger() 和 Recovery() 这两个 handler 的作用。1. Logger()默认的 Logger() 会输出接口调用的信息,比如第一篇中我们定义了一个 /test 接口,当我们调用这个接口的时候,控制台会输出下面这条信息:[GIN] 2025/08/19 - 23:15:26 | 200 | 36.666µs | 127.0.0.1 | GET "/test"可以看到日志中会包含请求时间、返回的 HTTP 状态码、请求耗时、调用方 ip、请求方式和接口名称等。这条日志信息的输出就是 Logger() 这个中间件起的作用。在其内部,会调用一个 LoggerWithConfig() 函数,获取到请求的 ip、记录调用时间、调用方式等信息,然后进行输出,下面是部分源码信息:param.TimeStamp = time.Now()param.Latency = param.TimeStamp.Sub(start)param.ClientIP = c.ClientIP()param.Method = c.Request.Methodparam.StatusCode = c.Writer.Status()param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()2. Recovery()Recovery() 中间件则可以为我们捕获程序中未处理的 panic,记录错误信息并返回 500 状态码信息,比如我们在第一篇笔记中使用的 TestHandler 函数,我们在其中加一个除数为 0 的错误:func TestHandler(c *gin.Context) { response := TestResponse{ Code: 0, Message: "success", } a := 0 fmt.Println(1 / a) c.JSON(http.StatusOK, response)}在接口调用的时候,如果我们使用的是 gin.Default(),那么客户端不会报错,而是会收到一个 HTTP 状态码为 500 的报错信息,而如果使用的是 gin.New(),客户端则会直接发生错误。总的来说,Logger() 和 Recovery() 这两个的中间件是 gin 框架为我们默认添加的对于开发者来说较为友好的两个操作,在后面介绍中间件的时候,我们也可以手动实现这两个功能。2、HTTP 方法gin.Engine 支持配置 HTTP 多个方法,比如 GET、POST、PUT、DELETE 等。以第一篇笔记中的代码为例,其设置方法如下:r.GET("/test", TestHandler)r.POST("/test", TestHandler)r.PUT("/test", TestHandler)r.DELETE("/test", TestHandler)3、路由分组与中间件除了设置单个路由,我们还可以对路由进行分组设置,比如需要控制版本,或者模块设置需要统一的前缀,又或者是需要统一设置中间件功能的时候。其整体代码示例如下:package mainimport ( "fmt" "net/http" "github.com/gin-gonic/gin")type TestResponse struct { Code int `json:"code"` Message string `json:"message"`}func TestHandler(c *gin.Context) { response := TestResponse{ Code: 0, Message: "success", } c.JSON(http.StatusOK, response)}func main() { r := gin.Default() v1 := r.Group("/v1") { v1.GET("/test", TestHandler) } err := r.Run(":9898") if err != nil { fmt.Println("gin run in 9898 error:", err) }}这里,我们设置了一个路由名称以 v1 为前缀的路由组,其下每个路由的访问都需要带有 /v1,这样就实现了统一设置路由前缀的功能。而如果我们需要向其中添加中间件的时候,也可以不用挨个路由进行设置,而是在 v1 路由组的设置中就可以实现,比如:v1 := r.Group("/v1", Middleware1, Middleware2)这样,其下每个路由的 handler 函数在调用前就都会先调用 Middleware1 和 Middleware2 这两个中间件。以上就是本篇笔记关于 gin.Engine 的全部内容,其实中间件的相关操作也应该属于 gin.Engine 的内容,但是那部分需要介绍的知识点和想要用于介绍的代码示例略多,所以就单独开一篇笔记在后面再介绍。 转载于:https://www.cnblogs.com/hunterxiong/p/19175625
-
在多线程并发环境下,保证数据操作的原子性是个常见且关键的挑战。Java从JDK 1.5开始提供了java.util.concurrent.atomic包,其中包含13个强大的原子操作类,让我们能够以无锁的方式实现线程安全。本文将带你深入理解这些原子类的原理、API和使用场景。一、为什么需要原子操作类?1.1 问题的由来想象一下这样的场景:多个线程同时操作同一个银行账户进行取款,如果不加控制,可能会出现什么情况?// 不安全的计数器示例class UnsafeCounter { private int count = 0; public void increment() { count++; // 这不是原子操作! }}count++看似简单,实际上包含三个步骤:读取count的当前值将值加1将新值写回count在多线程环境下,这两个步骤可能被其他线程打断,导致数据不一致。1.2 传统的解决方案及其缺点传统做法是使用synchronized关键字:class SynchronizedCounter { private int count = 0; public synchronized void increment() { count++; }}synchronized确实能保证线程安全,但存在以下问题:性能开销:锁的获取和释放需要代价可能死锁:不正确的锁顺序可能导致死锁降低并发性:同一时刻只有一个线程能访问1.3 原子操作类的优势原子操作类基于CAS(Compare-And-Swap) 机制,提供了:无锁编程:避免传统锁的开销高性能:在低竞争环境下性能优异无死锁风险:基于硬件指令,不会产生死锁高并发:支持多个线程同时操作二、原子更新基本类型类2.1 AtomicBoolean - 原子更新布尔类型使用场景:状态标志位、开关控制、条件判断核心API详解方法参数返回值说明get()-boolean获取当前值set(boolean newValue)newValue: 新值void设置新值getAndSet(boolean newValue)newValue: 新值boolean原子性地设置为新值并返回旧值compareAndSet(boolean expect, boolean update)expect: 期望值update: 更新值boolean如果当前值等于期望值,则原子性地更新lazySet(boolean newValue)newValue: 新值void最终设置为新值,但不保证立即可见性weakCompareAndSet(boolean expect, boolean update)expect: 期望值update: 更新值boolean可能更弱的CAS操作,在某些平台上性能更好import java.util.concurrent.atomic.AtomicBoolean;/** * AtomicBoolean示例:用于原子性地更新布尔值 * 典型场景:系统开关、状态标志等 */public class AtomicBooleanDemo { public static void main(String[] args) { // 创建AtomicBoolean,初始值为false AtomicBoolean atomicBoolean = new AtomicBoolean(false); // get(): 获取当前值 System.out.println("初始值: " + atomicBoolean.get()); // getAndSet(): 原子性地设置为true,返回旧值 boolean oldValue = atomicBoolean.getAndSet(true); System.out.println("getAndSet旧值: " + oldValue + ", 新值: " + atomicBoolean.get()); // compareAndSet(): 比较并设置 boolean success = atomicBoolean.compareAndSet(true, false); System.out.println("CAS操作结果: " + success + ", 当前值: " + atomicBoolean.get()); // lazySet(): 最终会设置,但不保证立即可见性 atomicBoolean.lazySet(true); System.out.println("lazySet后的值: " + atomicBoolean.get()); // weakCompareAndSet(): 弱版本CAS boolean weakSuccess = atomicBoolean.weakCompareAndSet(true, false); System.out.println("弱CAS操作结果: " + weakSuccess + ", 当前值: " + atomicBoolean.get()); }}原理分析:AtomicBoolean内部实际上使用int类型来存储,0表示false,1表示true。通过compareAndSwapInt来实现原子操作。2.2 AtomicInteger - 原子更新整型使用场景:计数器、序列号生成、资源数量控制核心API详解方法参数返回值说明get()-int获取当前值set(int newValue)newValue: 新值void设置新值getAndSet(int newValue)newValue: 新值int原子性地设置为新值并返回旧值compareAndSet(int expect, int update)expect: 期望值update: 更新值booleanCAS操作getAndIncrement()-int原子递增,返回旧值getAndDecrement()-int原子递减,返回旧值getAndAdd(int delta)delta: 增量int原子加法,返回旧值incrementAndGet()-int原子递增,返回新值decrementAndGet()-int原子递减,返回新值addAndGet(int delta)delta: 增量int原子加法,返回新值updateAndGet(IntUnaryOperator)operator: 更新函数int函数式更新accumulateAndGet(int x, IntBinaryOperator)x: 参数operator: 操作函数int累积计算import java.util.concurrent.atomic.AtomicInteger;/** * AtomicInteger是最常用的原子类之一 * 适用于计数器、ID生成器等需要原子递增的场景 */public class AtomicIntegerDemo { public static void main(String[] args) { AtomicInteger atomicInt = new AtomicInteger(0); // 基础操作 System.out.println("初始值: " + atomicInt.get()); atomicInt.set(5); System.out.println("set(5)后: " + atomicInt.get()); // 原子递增并返回旧值 - 常用于计数 System.out.println("getAndIncrement: " + atomicInt.getAndIncrement()); // 返回5 System.out.println("当前值: " + atomicInt.get()); // 6 // 原子递减并返回旧值 System.out.println("getAndDecrement: " + atomicInt.getAndDecrement()); // 返回6 System.out.println("当前值: " + atomicInt.get()); // 5 // 原子加法并返回旧值 System.out.println("getAndAdd(10): " + atomicInt.getAndAdd(10)); // 返回5 System.out.println("当前值: " + atomicInt.get()); // 15 // 原子递增并返回新值 System.out.println("incrementAndGet: " + atomicInt.incrementAndGet()); // 16 // 原子加法并返回结果 - 适合批量增加 int result = atomicInt.addAndGet(10); System.out.println("addAndGet(10)结果: " + result); // 26 // 比较并设置 - 核心CAS操作 boolean updated = atomicInt.compareAndSet(26, 30); System.out.println("CAS操作结果: " + updated + ", 当前值: " + atomicInt.get()); // 获取并设置新值 - 适合重置操作 int previous = atomicInt.getAndSet(40); System.out.println("getAndSet旧值: " + previous + ", 新值: " + atomicInt.get()); // JDK8新增:函数式更新 - 更灵活的更新方式 atomicInt.updateAndGet(x -> x * 2); System.out.println("updateAndGet(*2)后的值: " + atomicInt.get()); // 80 // 累积计算 atomicInt.accumulateAndGet(10, (x, y) -> x + y * 2); System.out.println("accumulateAndGet后的值: " + atomicInt.get()); // 100 }}源码分析:public final int getAndIncrement() { // 自旋CAS:循环直到成功 for (;;) { int current = get(); // 步骤1:获取当前值 int next = current + 1; // 步骤2:计算新值 if (compareAndSet(current, next)) // 步骤3:CAS更新 return current; // 成功则返回旧值 } // 如果CAS失败,说明有其他线程修改了值,循环重试}2.3 AtomicLong - 原子更新长整型使用场景:大数值计数器、统计信息、唯一ID生成核心API详解方法参数返回值说明get()-long获取当前值set(long newValue)newValue: 新值void设置新值getAndSet(long newValue)newValue: 新值long原子性地设置为新值并返回旧值compareAndSet(long expect, long update)expect: 期望值update: 更新值booleanCAS操作getAndIncrement()-long原子递增,返回旧值getAndDecrement()-long原子递减,返回旧值getAndAdd(long delta)delta: 增量long原子加法,返回旧值incrementAndGet()-long原子递增,返回新值decrementAndGet()-long原子递减,返回新值addAndGet(long delta)delta: 增量long原子加法,返回新值updateAndGet(LongUnaryOperator)operator: 更新函数long函数式更新accumulateAndGet(long x, LongBinaryOperator)x: 参数operator: 操作函数long累积计算import java.util.concurrent.atomic.AtomicLong;/** * AtomicLong用于长整型的原子操作 * 在64位系统中性能与AtomicInteger相当 */public class AtomicLongDemo { public static void main(String[] args) { AtomicLong atomicLong = new AtomicLong(100L); System.out.println("初始值: " + atomicLong.get()); // 原子递增并返回旧值 - 适合序列号生成 System.out.println("getAndIncrement: " + atomicLong.getAndIncrement()); System.out.println("当前值: " + atomicLong.get()); // 原子递减并返回旧值 System.out.println("getAndDecrement: " + atomicLong.getAndDecrement()); System.out.println("当前值: " + atomicLong.get()); // 原子加法并返回旧值 System.out.println("getAndAdd(50): " + atomicLong.getAndAdd(50L)); System.out.println("当前值: " + atomicLong.get()); // 原子递增并返回新值 System.out.println("incrementAndGet: " + atomicLong.incrementAndGet()); // 原子加法并返回结果 - 适合统计累加 long newValue = atomicLong.addAndGet(50L); System.out.println("addAndGet(50)结果: " + newValue); // 比较并设置 boolean success = atomicLong.compareAndSet(250L, 300L); System.out.println("CAS操作结果: " + success + ", 当前值: " + atomicLong.get()); // JDK8新增:函数式更新 atomicLong.updateAndGet(x -> x / 2); System.out.println("updateAndGet(/2)后的值: " + atomicLong.get()); // JDK8新增:累积计算 - 适合复杂的原子计算 atomicLong.accumulateAndGet(100L, (x, y) -> x * y); System.out.println("accumulateAndGet后的值: " + atomicLong.get()); }}性能提示:在32位系统上,AtomicLong的CAS操作可能需要锁住总线,性能相对较差。Java 8提供了LongAdder作为高性能替代方案。三、原子更新数组类3.1 AtomicIntegerArray - 原子更新整型数组使用场景:并发计数器数组、桶统计、并行计算核心API详解方法参数返回值说明length()-int返回数组长度get(int i)i: 索引int获取指定索引的值set(int i, int newValue)i: 索引newValue: 新值void设置指定索引的值getAndSet(int i, int newValue)i: 索引newValue: 新值int原子设置并返回旧值compareAndSet(int i, int expect, int update)i: 索引expect: 期望值update: 更新值boolean对指定索引进行CAS操作getAndIncrement(int i)i: 索引int原子递增指定索引,返回旧值getAndDecrement(int i)i: 索引int原子递减指定索引,返回旧值getAndAdd(int i, int delta)i: 索引delta: 增量int原子加法,返回旧值incrementAndGet(int i)i: 索引int原子递增指定索引,返回新值addAndGet(int i, int delta)i: 索引delta: 增量int原子加法,返回新值import java.util.concurrent.atomic.AtomicIntegerArray;/** * AtomicIntegerArray允许原子地更新数组中的单个元素 * 注意:构造函数会复制传入的数组,不影响原数组 */public class AtomicIntegerArrayDemo { public static void main(String[] args) { int[] initialArray = {1, 2, 3, 4, 5}; // 创建原子整型数组,会复制传入的数组 AtomicIntegerArray atomicArray = new AtomicIntegerArray(initialArray); System.out.println("数组长度: " + atomicArray.length()); System.out.println("原始数组: " + atomicArray.toString()); // get(): 获取指定索引的值 System.out.println("索引0的值: " + atomicArray.get(0)); // set(): 设置指定索引的值 atomicArray.set(0, 10); System.out.println("set(0, 10)后的数组: " + atomicArray.toString()); // getAndSet(): 原子更新指定索引的元素并返回旧值 int oldValue = atomicArray.getAndSet(1, 20); System.out.println("索引1替换前的值: " + oldValue + ", 数组: " + atomicArray.toString()); // getAndIncrement(): 原子递增指定索引的元素 - 适合分桶计数 oldValue = atomicArray.getAndIncrement(2); System.out.println("索引2递增前值: " + oldValue + ", 数组: " + atomicArray.toString()); // compareAndSet(): 比较并设置特定位置的元素 boolean updated = atomicArray.compareAndSet(3, 4, 40); System.out.println("索引3 CAS结果: " + updated + ", 数组: " + atomicArray.toString()); // addAndGet(): 原子加法 - 适合累加统计 int newValue = atomicArray.addAndGet(4, 5); System.out.println("索引4加5后的值: " + newValue + ", 数组: " + atomicArray.toString()); // incrementAndGet(): 原子递增并返回新值 newValue = atomicArray.incrementAndGet(0); System.out.println("索引0递增后的值: " + newValue); // 重要:原始数组不会被修改 System.out.println("原始数组值未被修改: " + initialArray[0]); // 仍然是1 }}设计思想:AtomicIntegerArray通过复制数组来避免外部修改,每个数组元素的更新都是独立的原子操作。3.2 AtomicLongArray - 原子更新长整型数组使用场景:大数据统计、时间戳数组、大数值桶统计核心API详解方法参数返回值说明length()-int返回数组长度get(int i)i: 索引long获取指定索引的值set(int i, long newValue)i: 索引newValue: 新值void设置指定索引的值getAndSet(int i, long newValue)i: 索引newValue: 新值long原子设置并返回旧值compareAndSet(int i, long expect, long update)i: 索引expect: 期望值update: 更新值boolean对指定索引进行CAS操作getAndAdd(int i, long delta)i: 索引delta: 增量long原子加法,返回旧值addAndGet(int i, long delta)i: 索引delta: 增量long原子加法,返回新值import java.util.concurrent.atomic.AtomicLongArray;/** * AtomicLongArray提供长整型数组的原子操作 * 适用于需要大数值范围的并发统计 */public class AtomicLongArrayDemo { public static void main(String[] args) { long[] initialArray = {100L, 200L, 300L, 400L, 500L}; AtomicLongArray atomicLongArray = new AtomicLongArray(initialArray); System.out.println("数组长度: " + atomicLongArray.length()); System.out.println("初始数组: " + atomicLongArray.toString()); // 基础操作 System.out.println("索引0的值: " + atomicLongArray.get(0)); atomicLongArray.set(0, 150L); System.out.println("set(0, 150)后的数组: " + atomicLongArray.toString()); // 原子更新操作 long oldValue = atomicLongArray.getAndSet(1, 250L); System.out.println("索引1替换前的值: " + oldValue + ", 数组: " + atomicLongArray.toString()); // 原子加法操作 atomicLongArray.getAndAdd(2, 100L); System.out.println("索引2加100后的数组: " + atomicLongArray.toString()); // 比较并设置 atomicLongArray.compareAndSet(3, 400L, 450L); System.out.println("索引3 CAS后的数组: " + atomicLongArray.toString()); // 加法并获取新值 long newValue = atomicLongArray.addAndGet(4, 200L); System.out.println("索引4加200后的值: " + newValue); }}3.3 AtomicReferenceArray - 原子更新引用类型数组使用场景:对象池、缓存数组、并发数据结构核心API详解方法参数返回值说明length()-int返回数组长度get(int i)i: 索引E获取指定索引的引用set(int i, E newValue)i: 索引newValue: 新引用void设置指定索引的引用getAndSet(int i, E newValue)i: 索引newValue: 新引用E原子设置并返回旧引用compareAndSet(int i, E expect, E update)i: 索引expect: 期望引用update: 更新引用boolean对指定索引进行CAS操作lazySet(int i, E newValue)i: 索引newValue: 新引用void延迟设置引用import java.util.concurrent.atomic.AtomicReferenceArray;/** * AtomicReferenceArray用于原子更新引用类型数组 * 适用于对象引用需要原子更新的场景 */public class AtomicReferenceArrayDemo { static class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name + "(" + age + ")"; } } public static void main(String[] args) { Person[] persons = { new Person("Alice", 25), new Person("Bob", 30), new Person("Charlie", 35), new Person("David", 40) }; AtomicReferenceArray<Person> atomicArray = new AtomicReferenceArray<>(persons); System.out.println("数组长度: " + atomicArray.length()); System.out.println("初始数组: "); for (int i = 0; i < atomicArray.length(); i++) { System.out.println("索引 " + i + ": " + atomicArray.get(i)); } // 原子更新引用 - 适合对象替换 Person newPerson = new Person("Eve", 28); Person oldPerson = atomicArray.getAndSet(1, newPerson); System.out.println("索引1替换: " + oldPerson + " -> " + atomicArray.get(1)); // 比较并设置引用 boolean success = atomicArray.compareAndSet(2, persons[2], new Person("Frank", 45)); System.out.println("索引2 CAS结果: " + success + ", 新值: " + atomicArray.get(2)); // 延迟设置 atomicArray.lazySet(3, new Person("Grace", 50)); System.out.println("索引3延迟设置后的值: " + atomicArray.get(3)); // 遍历数组 System.out.println("最终数组状态:"); for (int i = 0; i < atomicArray.length(); i++) { System.out.println("索引 " + i + ": " + atomicArray.get(i)); } }}四、原子更新引用类型4.1 AtomicReference - 原子更新引用类型使用场景:单例模式、缓存更新、状态对象替换核心API详解方法参数返回值说明get()-V获取当前引用set(V newValue)newValue: 新引用void设置新引用getAndSet(V newValue)newValue: 新引用V原子设置并返回旧引用compareAndSet(V expect, V update)expect: 期望引用update: 更新引用booleanCAS操作weakCompareAndSet(V expect, V update)expect: 期望引用update: 更新引用boolean弱版本CASlazySet(V newValue)newValue: 新引用void延迟设置引用updateAndGet(UnaryOperator<V>)operator: 更新函数V函数式更新getAndUpdate(UnaryOperator<V>)operator: 更新函数V函数式更新并返回旧值accumulateAndGet(V x, BinaryOperator<V>)x: 参数operator: 操作函数V累积计算import java.util.concurrent.atomic.AtomicReference;/** * AtomicReference用于原子更新对象引用 * 解决"先检查后执行"的竞态条件 */public class AtomicReferenceDemo { static class User { private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return "User{name='" + name + "', age=" + age + "}"; } } public static void main(String[] args) { AtomicReference<User> atomicUser = new AtomicReference<>(); User initialUser = new User("张三", 25); atomicUser.set(initialUser); System.out.println("初始用户: " + atomicUser.get()); // getAndSet(): 原子更新引用 - 适合缓存更新 User newUser = new User("李四", 30); User oldUser = atomicUser.getAndSet(newUser); System.out.println("替换前的用户: " + oldUser); System.out.println("当前用户: " + atomicUser.get()); // compareAndSet(): 比较并设置 - 核心操作 boolean success = atomicUser.compareAndSet(newUser, new User("王五", 35)); System.out.println("CAS操作结果: " + success + ", 当前用户: " + atomicUser.get()); // weakCompareAndSet(): 弱版本CAS boolean weakSuccess = atomicUser.weakCompareAndSet( atomicUser.get(), new User("赵六", 40)); System.out.println("弱CAS操作结果: " + weakSuccess + ", 当前用户: " + atomicUser.get()); // lazySet(): 延迟设置 atomicUser.lazySet(new User("孙七", 45)); System.out.println("延迟设置后的用户: " + atomicUser.get()); // JDK8新增:函数式更新 atomicUser.updateAndGet(user -> new User(user.getName() + "_updated", user.getAge() + 1)); System.out.println("函数式更新后的用户: " + atomicUser.get()); // getAndUpdate(): 函数式更新并返回旧值 User previous = atomicUser.getAndUpdate(user -> new User("周八", 50)); System.out.println("更新前的用户: " + previous + ", 当前用户: " + atomicUser.get()); // accumulateAndGet(): 累积计算 atomicUser.accumulateAndGet(new User("吴九", 55), (old, param) -> new User(old.getName() + "&" + param.getName(), old.getAge() + param.getAge())); System.out.println("累积计算后的用户: " + atomicUser.get()); }}典型应用:单例模式的双重检查锁定class Singleton { private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>(); public static Singleton getInstance() { for (;;) { Singleton current = INSTANCE.get(); if (current != null) return current; current = new Singleton(); if (INSTANCE.compareAndSet(null, current)) { return current; } } }}4.2 AtomicMarkableReference - 带标记位的原子引用使用场景:带状态的缓存、ABA问题简单解决方案核心API详解方法参数返回值说明getReference()-V获取当前引用isMarked()-boolean获取当前标记位get(boolean[] markHolder)markHolder: 标记位容器V获取引用和标记位set(V newReference, boolean newMark)newReference: 新引用newMark: 新标记void设置引用和标记位compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark)expectedReference: 期望引用newReference: 新引用expectedMark: 期望标记newMark: 新标记boolean同时比较引用和标记位attemptMark(V expectedReference, boolean newMark)expectedReference: 期望引用newMark: 新标记boolean尝试只更新标记位import java.util.concurrent.atomic.AtomicMarkableReference;/** * AtomicMarkableReference将引用与一个布尔标记位绑定 * 适用于需要同时更新引用和状态的场景 */public class AtomicMarkableReferenceDemo { public static void main(String[] args) { String initialRef = "初始数据"; boolean initialMark = false; // 创建带标记位的原子引用 AtomicMarkableReference<String> atomicMarkableRef = new AtomicMarkableReference<>(initialRef, initialMark); System.out.println("初始引用: " + atomicMarkableRef.getReference()); System.out.println("初始标记: " + atomicMarkableRef.isMarked()); // get(boolean[]): 同时获取引用和标记位 boolean[] markHolder = new boolean[1]; String currentRef = atomicMarkableRef.get(markHolder); System.out.println("当前引用: " + currentRef + ", 当前标记: " + markHolder[0]); // compareAndSet(): 尝试同时更新引用和标记位 String newRef = "新数据"; boolean newMark = true; boolean success = atomicMarkableRef.compareAndSet( initialRef, newRef, initialMark, newMark); System.out.println("CAS操作结果: " + success); System.out.println("新引用: " + atomicMarkableRef.getReference()); System.out.println("新标记: " + atomicMarkableRef.isMarked()); // attemptMark(): 只尝试更新标记位 boolean markUpdated = atomicMarkableRef.attemptMark(newRef, false); System.out.println("标记更新结果: " + markUpdated); System.out.println("最终标记: " + atomicMarkableRef.isMarked()); // set(): 直接设置引用和标记位 atomicMarkableRef.set("最终数据", true); System.out.println("直接设置后的引用: " + atomicMarkableRef.getReference()); System.out.println("直接设置后的标记: " + atomicMarkableRef.isMarked()); }}4.3 AtomicStampedReference - 带版本号的原子引用使用场景:解决ABA问题、乐观锁实现核心API详解方法参数返回值说明getReference()-V获取当前引用getStamp()-int获取当前版本号get(int[] stampHolder)stampHolder: 版本号容器V获取引用和版本号set(V newReference, int newStamp)newReference: 新引用newStamp: 新版本号void设置引用和版本号compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)expectedReference: 期望引用newReference: 新引用expectedStamp: 期望版本号newStamp: 新版本号boolean同时比较引用和版本号attemptStamp(V expectedReference, int newStamp)expectedReference: 期望引用newStamp: 新版本号boolean尝试只更新版本号import java.util.concurrent.atomic.AtomicStampedReference;/** * AtomicStampedReference通过版本号解决ABA问题 * 每次修改都会增加版本号,确保不会误判 */public class AtomicStampedReferenceDemo { public static void main(String[] args) { String initialRef = "数据A"; int initialStamp = 0; // 创建带版本号的原子引用 AtomicStampedReference<String> atomicStampedRef = new AtomicStampedReference<>(initialRef, initialStamp); System.out.println("初始引用: " + atomicStampedRef.getReference()); System.out.println("初始版本号: " + atomicStampedRef.getStamp()); // get(int[]): 同时获取引用和版本号 int[] stampHolder = new int[1]; String currentRef = atomicStampedRef.get(stampHolder); System.out.println("当前引用: " + currentRef + ", 当前版本号: " + stampHolder[0]); // 模拟ABA问题场景 String newRefB = "数据B"; String newRefA = "数据A"; // 又改回A,但版本号不同 // 第一次更新:A -> B,版本号 0 -> 1 boolean firstUpdate = atomicStampedRef.compareAndSet( initialRef, newRefB, initialStamp, initialStamp + 1); System.out.println("第一次更新(A->B)结果: " + firstUpdate); System.out.println("当前引用: " + atomicStampedRef.getReference()); System.out.println("当前版本号: " + atomicStampedRef.getStamp()); // 第二次更新:B -> A,版本号 1 -> 2 boolean secondUpdate = atomicStampedRef.compareAndSet( newRefB, newRefA, 1, 2); System.out.println("第二次更新(B->A)结果: " + secondUpdate); System.out.println("当前引用: " + atomicStampedRef.getReference()); System.out.println("当前版本号: " + atomicStampedRef.getStamp()); // 尝试用旧版本号更新(会失败)- 这就是解决ABA问题的关键! boolean failedUpdate = atomicStampedRef.compareAndSet( newRefA, "新数据", 0, 1); // 使用旧的版本号0 System.out.println("使用旧版本号更新结果: " + failedUpdate); System.out.println("引用未被修改: " + atomicStampedRef.getReference()); // attemptStamp(): 只更新版本号 boolean stampUpdated = atomicStampedRef.attemptStamp(newRefA, 3); System.out.println("版本号更新结果: " + stampUpdated); System.out.println("新版本号: " + atomicStampedRef.getStamp()); // 正确的方式:使用当前版本号 stampHolder = new int[1]; currentRef = atomicStampedRef.get(stampHolder); boolean correctUpdate = atomicStampedRef.compareAndSet( currentRef, "最终数据", stampHolder[0], stampHolder[0] + 1); System.out.println("使用正确版本号更新结果: " + correctUpdate); System.out.println("最终引用: " + atomicStampedRef.getReference()); System.out.println("最终版本号: " + atomicStampedRef.getStamp()); }}ABA问题详解:ABA问题是指:线程1读取值A线程2将值改为B,然后又改回A线程1进行CAS操作,发现当前值仍是A,于是操作成功虽然值看起来没变,但中间状态的变化可能对业务逻辑产生影响。AtomicStampedReference通过版本号完美解决了这个问题。五、原子更新字段类5.1 AtomicIntegerFieldUpdater - 原子更新整型字段使用场景:优化内存使用、大量对象需要原子字段更新核心API详解方法参数返回值说明newUpdater(Class<U> tclass, String fieldName)tclass: 目标类fieldName: 字段名AtomicIntegerFieldUpdater<U>静态方法创建更新器get(U obj)obj: 目标对象int获取字段值set(U obj, int newValue)obj: 目标对象newValue: 新值void设置字段值getAndSet(U obj, int newValue)obj: 目标对象newValue: 新值int原子设置并返回旧值compareAndSet(U obj, int expect, int update)obj: 目标对象expect: 期望值update: 更新值booleanCAS操作getAndIncrement(U obj)obj: 目标对象int原子递增,返回旧值getAndDecrement(U obj)obj: 目标对象int原子递减,返回旧值getAndAdd(U obj, int delta)obj: 目标对象delta: 增量int原子加法,返回旧值incrementAndGet(U obj)obj: 目标对象int原子递增,返回新值addAndGet(U obj, int delta)obj: 目标对象delta: 增量int原子加法,返回新值import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;/** * AtomicIntegerFieldUpdater以原子方式更新对象的volatile int字段 * 相比为每个对象创建AtomicInteger,可以节省大量内存 */public class AtomicIntegerFieldUpdaterDemo { static class Counter { // 必须用volatile修饰,保证可见性 public volatile int count; private String name; public Counter(String name, int initialCount) { this.name = name; this.count = initialCount; } public String getName() { return name; } public int getCount() { return count; } } public static void main(String[] args) { // 创建字段更新器,指定要更新的类和字段名 AtomicIntegerFieldUpdater<Counter> updater = AtomicIntegerFieldUpdater.newUpdater(Counter.class, "count"); Counter counter1 = new Counter("计数器1", 0); Counter counter2 = new Counter("计数器2", 10); System.out.println("计数器1初始计数: " + counter1.getCount()); System.out.println("计数器2初始计数: " + counter2.getCount()); // get(): 获取字段值 System.out.println("通过updater获取计数器1的值: " + updater.get(counter1)); // set(): 设置字段值 updater.set(counter1, 5); System.out.println("设置计数器1为5后的值: " + counter1.getCount()); // getAndIncrement(): 原子递增 - 相比synchronized性能更好 int oldCount = updater.getAndIncrement(counter1); System.out.println("计数器1递增前值: " + oldCount + ", 当前值: " + counter1.getCount()); // getAndAdd(): 原子加法 oldCount = updater.getAndAdd(counter1, 10); System.out.println("计数器1加10前值: " + oldCount + ", 当前值: " + counter1.getCount()); // incrementAndGet(): 原子递增并返回新值 int newCount = updater.incrementAndGet(counter1); System.out.println("计数器1递增后的值: " + newCount); // addAndGet(): 原子加法并返回新值 newCount = updater.addAndGet(counter1, 20); System.out.println("计数器1加20后的值: " + newCount); // compareAndSet(): 比较并设置 boolean updated = updater.compareAndSet(counter1, 36, 50); System.out.println("计数器1 CAS操作结果: " + updated + ", 当前值: " + counter1.getCount()); // 可以同时更新多个对象的相同字段 updater.incrementAndGet(counter2); System.out.println("计数器2递增后的值: " + counter2.getCount()); }}内存优化效果:AtomicInteger对象:16-24字节 overheadvolatile int + AtomicIntegerFieldUpdater:4字节 + 静态updater当有大量对象时,内存节省效果显著5.2 AtomicLongFieldUpdater - 原子更新长整型字段使用场景:大数值字段的原子更新、内存敏感场景核心API详解方法参数返回值说明newUpdater(Class<U> tclass, String fieldName)tclass: 目标类fieldName: 字段名AtomicLongFieldUpdater<U>静态方法创建更新器get(U obj)obj: 目标对象long获取字段值set(U obj, long newValue)obj: 目标对象newValue: 新值void设置字段值getAndSet(U obj, long newValue)obj: 目标对象newValue: 新值long原子设置并返回旧值compareAndSet(U obj, long expect, long update)obj: 目标对象expect: 期望值update: 更新值booleanCAS操作getAndAdd(U obj, long delta)obj: 目标对象delta: 增量long原子加法,返回旧值addAndGet(U obj, long delta)obj: 目标对象delta: 增量long原子加法,返回新值import java.util.concurrent.atomic.AtomicLongFieldUpdater;/** * AtomicLongFieldUpdater用于原子更新long字段 * 适用于需要大数值范围且内存敏感的场景 */public class AtomicLongFieldUpdaterDemo { static class Account { // 必须用volatile修饰 public volatile long balance; private final String owner; public Account(String owner, long initialBalance) { this.owner = owner; this.balance = initialBalance; } public String getOwner() { return owner; } public long getBalance() { return balance; } } public static void main(String[] args) { AtomicLongFieldUpdater<Account> balanceUpdater = AtomicLongFieldUpdater.newUpdater(Account.class, "balance"); Account account1 = new Account("张三", 1000L); Account account2 = new Account("李四", 2000L); System.out.println("张三账户初始余额: " + account1.getBalance()); System.out.println("李四账户初始余额: " + account2.getBalance()); // 基础操作 System.out.println("通过updater获取张三余额: " + balanceUpdater.get(account1)); balanceUpdater.set(account1, 1500L); System.out.println("设置张三余额为1500后的值: " + account1.getBalance()); // 原子存款 - 无锁线程安全 balanceUpdater.addAndGet(account1, 500L); System.out.println("张三存款500后余额: " + account1.getBalance()); // 原子取款 long oldBalance = balanceUpdater.getAndAdd(account1, -200L); System.out.println("张三取款200前余额: " + oldBalance + ", 取款后余额: " + account1.getBalance()); // 比较并设置 - 实现转账等业务 boolean transferSuccess = balanceUpdater.compareAndSet(account1, 1800L, 2000L); System.out.println("张三转账操作结果: " + transferSuccess + ", 当前余额: " + account1.getBalance()); // 同时操作多个账户 balanceUpdater.getAndAdd(account2, 1000L); System.out.println("李四存款1000后余额: " + account2.getBalance()); }}5.3 AtomicReferenceFieldUpdater - 原子更新引用字段使用场景:链表节点更新、树结构调整、对象关系维护核心API详解方法参数返回值说明newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)tclass: 目标类vclass: 字段类型fieldName: 字段名AtomicReferenceFieldUpdater<U,W>静态方法创建更新器get(U obj)obj: 目标对象V获取字段引用set(U obj, V newValue)obj: 目标对象newValue: 新引用void设置字段引用getAndSet(U obj, V newValue)obj: 目标对象newValue: 新引用V原子设置并返回旧引用compareAndSet(U obj, V expect, V update)obj: 目标对象expect: 期望引用update: 更新引用booleanCAS操作lazySet(U obj, V newValue)obj: 目标对象newValue: 新引用void延迟设置引用import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/** * AtomicReferenceFieldUpdater用于原子更新引用字段 * 常用于实现无锁数据结构 */public class AtomicReferenceFieldUpdaterDemo { static class Node<T> { // 必须用volatile修饰 public volatile Node<T> next; private final T value; public Node(T value) { this.value = value; } public T getValue() { return value; } public Node<T> getNext() { return next; } @Override public String toString() { return "Node{value=" + value + ", next=" + (next != null ? next.value : "null") + "}"; } } public static void main(String[] args) { // 创建引用字段更新器 AtomicReferenceFieldUpdater<Node, Node> nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next"); Node<String> first = new Node<>("第一个节点"); Node<String> second = new Node<>("第二个节点"); Node<String> third = new Node<>("第三个节点"); System.out.println("初始第一个节点的next: " + first.getNext()); // get(): 获取字段引用 System.out.println("通过updater获取第一个节点的next: " + nextUpdater.get(first)); // set(): 设置字段引用 nextUpdater.set(first, second); System.out.println("设置第一个节点的next为第二个节点: " + first); // compareAndSet(): 原子设置next字段 - 实现无锁链表 boolean setSuccess = nextUpdater.compareAndSet(first, second, third); System.out.println("CAS操作结果: " + setSuccess); System.out.println("第一个节点: " + first); // getAndSet(): 获取并设置引用 Node<String> oldNext = nextUpdater.getAndSet(second, third); System.out.println("第二个节点原来的next: " + oldNext); System.out.println("第二个节点: " + second); // lazySet(): 延迟设置 nextUpdater.lazySet(third, first); // 形成环状,仅作演示 System.out.println("第三个节点延迟设置后的next: " + third.getNext()); // 构建链表并展示 System.out.println("最终链表结构:"); Node<String> current = first; int count = 0; while (current != null && count < 5) { // 防止无限循环 System.out.println(current); current = current.getNext(); count++; } }}六、综合实战:构建线程安全计数器下面我们通过一个综合示例展示如何在实际项目中使用原子操作类:import java.util.concurrent.atomic.*;import java.util.concurrent.*;/** * 线程安全计数器综合示例 * 展示了多种原子类的实际应用 */public class ThreadSafeCounter { // 基本计数器 - 使用AtomicInteger private final AtomicInteger count = new AtomicInteger(0); // 大数值统计 - 使用AtomicLong private final AtomicLong total = new AtomicLong(0L); // 状态控制 - 使用AtomicReference private final AtomicReference<String> status = new AtomicReference<>("RUNNING"); // 统计数组 - 使用AtomicIntegerArray进行分桶统计 private final AtomicIntegerArray bucketStats = new AtomicIntegerArray(10); // 配置信息 - 使用AtomicReference支持动态更新 private final AtomicReference<Config> config = new AtomicReference<>(new Config(100, 60)); // 标记位控制 - 使用AtomicBoolean private final AtomicBoolean enabled = new AtomicBoolean(true); static class Config { final int maxConnections; final int timeoutSeconds; public Config(int maxConnections, int timeoutSeconds) { this.maxConnections = maxConnections; this.timeoutSeconds = timeoutSeconds; } @Override public String toString() { return "Config{maxConnections=" + maxConnections + ", timeoutSeconds=" + timeoutSeconds + "}"; } } // 核心API方法 public void increment() { if (!enabled.get()) { System.out.println("计数器已禁用,忽略操作"); return; } count.incrementAndGet(); total.addAndGet(1L); // 分桶统计:根据count值决定放入哪个桶 int bucket = count.get() % 10; bucketStats.getAndIncrement(bucket); } public void add(int value) { if (!enabled.get()) { System.out.println("计数器已禁用,忽略操作"); return; } count.addAndGet(value); total.addAndGet(value); } public boolean setStatus(String expected, String newStatus) { return status.compareAndSet(expected, newStatus); } public void updateConfig(Config newConfig) { Config oldConfig; do { oldConfig = config.get(); System.out.println("尝试更新配置: " + oldConfig + " -> " + newConfig); } while (!config.compareAndSet(oldConfig, newConfig)); System.out.println("配置更新成功"); } public boolean enable() { return enabled.compareAndSet(false, true); } public boolean disable() { return enabled.compareAndSet(true, false); } // 获取统计信息 public void printStats() { System.out.println("\n=== 统计信息 ==="); System.out.println("当前计数: " + count.get()); System.out.println("总数: " + total.get()); System.out.println("状态: " + status.get()); System.out.println("启用状态: " + enabled.get()); System.out.println("桶统计: " + bucketStats.toString()); Config currentConfig = config.get(); System.out.println("配置: " + currentConfig); // 验证数据一致性 long sum = 0; for (int i = 0; i < bucketStats.length(); i++) { sum += bucketStats.get(i); } System.out.println("桶统计总和: " + sum + ", 计数: " + count.get() + ", 一致性: " + (sum == count.get())); } public static void main(String[] args) throws InterruptedException { ThreadSafeCounter counter = new ThreadSafeCounter(); // 创建多个线程同时操作计数器 int threadCount = 10; int operationsPerThread = 1000; ExecutorService executor = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); System.out.println("开始并发测试..."); for (int i = 0; i < threadCount; i++) { final int threadId = i; executor.execute(() -> { try { for (int j = 0; j < operationsPerThread; j++) { counter.increment(); // 每隔一定操作数更新配置 if (j % 200 == 0) { counter.updateConfig(new Config(100 + j, 60)); } // 模拟随机禁用/启用 if (j == 500 && threadId == 0) { System.out.println("线程" + threadId + "尝试禁用计数器"); counter.disable(); Thread.sleep(10); // 短暂休眠 System.out.println("线程" + threadId + "尝试启用计数器"); counter.enable(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } // 等待所有线程完成 latch.await(); executor.shutdown(); // 打印最终统计 counter.printStats(); int expectedCount = threadCount * operationsPerThread; System.out.println("\n=== 测试结果 ==="); System.out.println("期望计数: " + expectedCount); System.out.println("实际计数: " + counter.count.get()); System.out.println("计数正确: " + (counter.count.get() == expectedCount)); System.out.println("测试" + (counter.count.get() == expectedCount ? "通过" : "失败")); }}七、原子操作类的工作原理7.1 CAS机制详解CAS(Compare-And-Swap)是原子操作类的核心,包含三个操作数:内存位置(V)期望原值(A)新值(B)CAS的语义是:"我认为V的值应该是A,如果是,那么将V的值更新为B,否则不修改并告诉我现在的值是多少"CAS操作是硬件级别的原子操作,在现代CPU中通常通过以下方式实现:x86架构:CMPXCHG指令ARM架构:LDREX/STREX指令对7.2 Unsafe类的作用所有原子操作类底层都依赖sun.misc.Unsafe类,它提供了硬件级别的原子操作:public final class Unsafe { // 对象字段操作 public native long objectFieldOffset(Field f); // 数组基础偏移 public native int arrayBaseOffset(Class arrayClass); // 数组索引缩放 public native int arrayIndexScale(Class arrayClass); // CAS操作 public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x); // 获取和设置值 public native int getIntVolatile(Object o, long offset); public native void putIntVolatile(Object o, long offset, int x); // 延迟设置(有更弱的可见性保证) public native void putOrderedInt(Object o, long offset, int x);}7.3 内存屏障与可见性原子操作类通过内存屏障保证可见性:写操作:在写入后插入写屏障,保证写入对其他线程可见读操作:在读取前插入读屏障,保证读取到最新值Java内存模型中的屏障类型:LoadLoad屏障:保证该屏障前的读操作先于屏障后的读操作完成StoreStore屏障:保证该屏障前的写操作先于屏障后的写操作完成LoadStore屏障:保证该屏障前的读操作先于屏障后的写操作完成StoreLoad屏障:保证该屏障前的所有写操作对其他处理器可见八、性能对比与选型建议8.1 性能对比场景synchronized原子操作类性能提升低竞争慢快2-10倍中等竞争中等中等相当高竞争快慢(自旋)可能更差8.2 不同原子类的性能特点原子类适用场景性能特点AtomicInteger普通计数器性能优秀,适用大部分场景AtomicLong大数值计数在32位系统上性能较差LongAdder高并发统计高竞争环境下性能最优AtomicReference对象引用更新性能与对象大小相关字段更新器内存敏感场景节省内存,性能稍差8.3 选型指南计数器场景简单计数:AtomicInteger大数值计数:AtomicLong 或 LongAdder分桶统计:AtomicIntegerArray状态控制布尔标志:AtomicBoolean对象状态:AtomicReference带版本状态:AtomicStampedReference内存敏感场景大量对象:字段更新器(AtomicXXXFieldUpdater)缓存系统:AtomicReference数据结构无锁队列:AtomicReference无锁栈:AtomicReference无锁链表:AtomicReferenceFieldUpdater8.4 最佳实践避免过度使用:不是所有场景都需要原子类注意ABA问题:必要时使用带版本号的原子类考虑高竞争:高竞争环境下考虑LongAdder等替代方案内存布局:字段更新器可以优化内存使用JDK8+特性:利用新的函数式更新方法性能测试:在实际环境中进行性能测试九、总结Java原子操作类为我们提供了强大的无锁并发编程工具:9.1 核心价值13个原子类覆盖了基本类型、数组、引用和字段更新CAS机制基于硬件指令,性能优异无锁设计避免了死锁和锁开销丰富的API支持各种并发场景9.2 使用场景总结类别主要类核心用途基本类型AtomicInteger, AtomicLong, AtomicBoolean计数器、状态标志数组AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray并发数组、分桶统计引用AtomicReference, AtomicStampedReference, AtomicMarkableReference对象缓存、状态管理字段更新AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater内存优化、大量对象9.3 学习建议从简单开始:先掌握AtomicInteger和AtomicReference理解原理:深入理解CAS机制和内存模型实践应用:在真实项目中尝试使用原子类性能调优:根据实际场景选择合适的原子类持续学习:关注JDK新版本中的并发工具改进掌握这些原子操作类,能够让我们在适当的场景下写出更高效、更安全的并发代码。记住,工具虽好,但要因地制宜,根据具体场景选择最合适的并发控制方案。希望本文能帮助你深入理解Java原子操作类,在实际项目中游刃有余地处理并发问题! 转载于:https://www.cnblogs.com/sun-10387834/p/19172186
-
Spring Boot 目前已成为构建企业级应用的事实标准。它以“约定优于配置”(Convention over Configuration)为核心理念,极大地简化了 Spring 应用的初始搭建和开发过程,让开发者能够专注于业务逻辑,而非繁琐的配置。 一、什么是 Spring Boot?Spring Boot 是由 Pivotal 团队提供的一个开源框架,它基于 Spring 框架 构建,旨在:简化 Spring 应用的创建和部署。提供开箱即用的默认配置,减少样板代码。内嵌服务器(如 Tomcat、Jetty),无需打包成 WAR 部署到外部容器。提供生产级特性,如健康检查、指标监控、外部化配置等。一句话总结:Spring Boot 让 Spring 应用的开发像“搭积木”一样简单,开箱即用,快速启动。二、为什么选择 Spring Boot?1. 自动配置(Auto-configuration)Spring Boot 能根据你添加的依赖(如 spring-boot-starter-web)自动配置 Spring 应用。例如:添加了 Web 依赖,它会自动配置:内嵌 Tomcat 服务器Spring MVC默认的视图解析器错误页面处理你无需手动编写大量 XML 或 Java 配置。2. 起步依赖(er Dependencies)Spring Boot 提供了一系列“starter”依赖,将常用的依赖组合在一起,避免版本冲突。Starter功能spring-boot-starter-webWeb + REST + Tomcatspring-boot-starter-data-jpaJPA + Hibernatespring-boot-starter-data-redisRedis 客户端spring-boot-starter-securitySpring Securityspring-boot-starter-test测试支持只需引入一个依赖,即可获得一整套功能。3. 内嵌服务器无需将应用打包成 WAR 文件部署到 Tomcat、Jetty 等外部服务器。Spring Boot 应用自带服务器,打包成 JAR 即可运行:java -jar myapp.jar4. 生产就绪(Production Ready)Spring Boot 提供了 Spring Boot Actuator 模块,开箱即用的监控和管理功能:/actuator/health:应用健康状态/actuator/metrics:性能指标/actuator/env:当前环境变量/actuator/info:自定义应用信息5. 外部化配置支持多种方式配置应用参数:application.propertiesapplication.yml环境变量命令行参数并支持多环境配置(如 application-dev.yml, application-prod.yml)。6. 强大的 CLI 和代码生成工具Spring Initializr:在线生成项目骨架。Spring Boot CLI:命令行工具,快速运行 Groovy 脚本。三、快速创建一个 Spring Boot 应用方法1:使用 Spring Initializr访问 官方网站选择:Project: Maven / GradleLanguage: JavaSpring Boot Version: 最新稳定版Group: com.exampleArtifact: demo添加依赖:Spring Web, Spring Boot DevTools, Lombok点击 “Generate” 下载项目压缩包。方法2:使用 IDE(如 IntelliJ IDEA)File → New → Project选择 “Spring Initializr”填写项目信息并选择依赖完成创建四、项目结构解析src/├── main/│ ├── java/│ │ └── com/example/demo/│ │ ├── DemoApplication.java # 主启动类│ │ └── controller/│ │ └── HelloController.java # 控制器示例│ └── resources/│ ├── application.yml # 配置文件│ ├── static/ # 静态资源│ └── templates/ # 模板文件(如 Thymeleaf)└── test/ # 测试代码五、编写第一个 REST API// HelloController.java@RestController // = @Controller + @ResponseBody@RequestMapping("/api")public class HelloController { @GetMapping("/hello") public String sayHello() { return "Hello, Spring Boot!"; } @GetMapping("/user") public Map<String, Object> getUser() { Map<String, Object> user = new HashMap<>(); user.put("id", 1); user.put("name", "Alice"); user.put("email", "alice@example.com"); return user; }}启动应用后访问:http://localhost:8080/api/hello → 输出 Hello, Spring Boot!http://localhost:8080/api/user → 返回 JSON 数据六、核心配置文件(application.yml)server: port: 8080 servlet: context-path: /appspring: datasource: url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true# 自定义属性app: name: My Awesome App version: 1.0.0可通过 @Value("${app.name}") 或 @ConfigurationProperties 注入。七、Spring Boot 的核心注解注解说明@SpringBootApplication主类注解,包含 @SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan@RestController创建 RESTful 控制器@RequestMapping映射 HTTP 请求@Autowired自动注入 Bean@Component, @Service, @Repository组件注册@ConfigurationProperties绑定配置文件属性八、开发与部署开发阶段使用 spring-boot-devtools 实现热部署(代码修改自动重启)。使用 @Profile 切换开发/测试/生产环境。打包与运行# Maven 打包mvn clean package# 运行 JARjava -jar target/demo-0.0.1-SNAPSHOT.jar# 指定配置文件java -jar app.jar --spring.profiles.active=prod Spring Boot 已成为 Java 开发的标准工具链,无论是构建简单的 Web 应用,还是复杂的微服务系统,它都能提供强大的支持。其“约定优于配置”的理念,让开发者从繁琐的配置中解放出来,真正实现“快速开发、快速交付”。
-
Apache POI 是用Java编写的免费开源的跨平台的Java API,Apache POI提供API给Java程序对Microsoft Office格式档案读和写的功能,其中使用最多的就是使用POI操作Excel文件。POI为“Poor Obfuscation Implementation”的首字母缩写,意为“简洁版的模糊实现”。一、运营数据统计1.1 需求分析通过运营数据统计可以展示出体检机构的运营情况,包括会员数据、预约到诊数据、热门套餐等信息。本章节就是要通过一个表格的形式来展示这些运营数据。效果如下图: 1.2 完善页面运营数据统计对应的页面为/pages/report_business.html。1.2.1 定义模型数据定义数据模型,通过VUE的数据绑定展示数据<script> var vue = new Vue({ el: '#app', data:{ reportData:{ reportDate:null, todayNewMember :0, totalMember :0, thisWeekNewMember :0, thisMonthNewMember :0, todayOrderNumber :0, todayVisitsNumber :0, thisWeekOrderNumber :0, thisWeekVisitsNumber :0, thisMonthOrderNumber :0, thisMonthVisitsNumber :0, hotSetmeal :[] } } }) </script><div class="box" style="height: 900px"> <div class="excelTitle" > <el-button @click="exportExcel">导出Excel</el-button>运营数据统计 </div> <div class="excelTime">日期:{{reportData.reportDate}}</div> <table class="exceTable" cellspacing="0" cellpadding="0"> <tr> <td colspan="4" class="headBody">会员数据统计</td> </tr> <tr> <td width='20%' class="tabletrBg">新增会员数</td> <td width='30%'>{{reportData.todayNewMember}}</td> <td width='20%' class="tabletrBg">总会员数</td> <td width='30%'>{{reportData.totalMember}}</td> </tr> <tr> <td class="tabletrBg">本周新增会员数</td> <td>{{reportData.thisWeekNewMember}}</td> <td class="tabletrBg">本月新增会员数</td> <td>{{reportData.thisMonthNewMember}}</td> </tr> <tr> <td colspan="4" class="headBody">预约到诊数据统计</td> </tr> <tr> <td class="tabletrBg">今日预约数</td> <td>{{reportData.todayOrderNumber}}</td> <td class="tabletrBg">今日到诊数</td> <td>{{reportData.todayVisitsNumber}}</td> </tr> <tr> <td class="tabletrBg">本周预约数</td> <td>{{reportData.thisWeekOrderNumber}}</td> <td class="tabletrBg">本周到诊数</td> <td>{{reportData.thisWeekVisitsNumber}}</td> </tr> <tr> <td class="tabletrBg">本月预约数</td> <td>{{reportData.thisMonthOrderNumber}}</td> <td class="tabletrBg">本月到诊数</td> <td>{{reportData.thisMonthVisitsNumber}}</td> </tr> <tr> <td colspan="4" class="headBody">热门套餐</td> </tr> <tr class="tabletrBg textCenter"> <td>套餐名称</td> <td>预约数量</td> <td>占比</td> <td>备注</td> </tr> <tr v-for="s in reportData.hotSetmeal"> <td>{{s.name}}</td> <td>{{s.setmeal_count}}</td> <td>{{s.proportion}}</td> <td></td> </tr> </table> </div>1.2.2 发送请求获取动态数据在VUE的钩子函数中发送ajax请求获取动态数据,通过VUE的数据绑定将数据展示到页面<script> var vue = new Vue({ el: '#app', data:{ reportData:{ reportDate:null, todayNewMember :0, totalMember :0, thisWeekNewMember :0, thisMonthNewMember :0, todayOrderNumber :0, todayVisitsNumber :0, thisWeekOrderNumber :0, thisWeekVisitsNumber :0, thisMonthOrderNumber :0, thisMonthVisitsNumber :0, hotSetmeal :[] } }, created() { //发送ajax请求获取动态数据 axios.get("/report/getBusinessReportData.do").then((res)=>{ this.reportData = res.data.data; }); } }) </script>根据页面对数据格式的要求,我们发送ajax请求,服务端需要返回如下格式的数据:{ "data":{ "todayVisitsNumber":0, "reportDate":"2019-04-25", "todayNewMember":0, "thisWeekVisitsNumber":0, "thisMonthNewMember":2, "thisWeekNewMember":0, "totalMember":10, "thisMonthOrderNumber":2, "thisMonthVisitsNumber":0, "todayOrderNumber":0, "thisWeekOrderNumber":0, "hotSetmeal":[ {"proportion":0.4545,"name":"粉红珍爱(女)升级TM12项筛查体检套餐","setmeal_count":5}, {"proportion":0.1818,"name":"阳光爸妈升级肿瘤12项筛查体检套餐","setmeal_count":2}, {"proportion":0.1818,"name":"珍爱高端升级肿瘤12项筛查","setmeal_count":2}, {"proportion":0.0909,"name":"孕前检查套餐","setmeal_count":1} ], }, "flag":true, "message":"获取运营统计数据成功" }1.3 后台代码1.3.1 Controller在ReportController中提供getBusinessReportData方法@Reference private ReportService reportService; /** * 获取运营统计数据 * @return */ @RequestMapping("/getBusinessReportData") public Result getBusinessReportData(){ try { Map<String, Object> result = reportService.getBusinessReport(); return new Result(true,MessageConstant.GET_BUSINESS_REPORT_SUCCESS,result); } catch (Exception e) { e.printStackTrace(); return new Result(true,MessageConstant.GET_BUSINESS_REPORT_FAIL); } }1.3.2 服务接口在health_interface工程中创建ReportService服务接口并声明getBusinessReport方法package com.yunhe.service; import java.util.Map; public interface ReportService { /** * 获得运营统计数据 * Map数据格式: * todayNewMember -> number * totalMember -> number * thisWeekNewMember -> number * thisMonthNewMember -> number * todayOrderNumber -> number * todayVisitsNumber -> number * thisWeekOrderNumber -> number * thisWeekVisitsNumber -> number * thisMonthOrderNumber -> number * thisMonthVisitsNumber -> number * hotSetmeals -> List<Setmeal> */ public Map<String,Object> getBusinessReport() throws Exception; }1.3.3 服务实现类在health_service_provider工程中创建服务实现类ReportServiceImpl并实现ReportService接口package com.yunhe.service; import com.alibaba.dubbo.config.annotation.Service; import com.yunhe.dao.MemberDao; import com.yunhe.dao.OrderDao; import com.yunhe.utils.DateUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 统计报表服务 */ @Service(interfaceClass = ReportService.class) @Transactional public class ReportServiceImpl implements ReportService { @Autowired private MemberDao memberDao; @Autowired private OrderDao orderDao; /** * 获得运营统计数据 * Map数据格式: * todayNewMember -> number * totalMember -> number * thisWeekNewMember -> number * thisMonthNewMember -> number * todayOrderNumber -> number * todayVisitsNumber -> number * thisWeekOrderNumber -> number * thisWeekVisitsNumber -> number * thisMonthOrderNumber -> number * thisMonthVisitsNumber -> number * hotSetmeal -> List<Setmeal> */ public Map<String, Object> getBusinessReport() throws Exception{ //获得当前日期 String today = DateUtils.parseDate2String(DateUtils.getToday()); //获得本周一的日期 String thisWeekMonday = DateUtils.parseDate2String(DateUtils.getThisWeekMonday()); //获得本月第一天的日期 String firstDay4ThisMonth = DateUtils.parseDate2String(DateUtils.getFirstDay4ThisMonth()); //今日新增会员数 Integer todayNewMember = memberDao.findMemberCountByDate(today); //总会员数 Integer totalMember = memberDao.findMemberTotalCount(); //本周新增会员数 Integer thisWeekNewMember = memberDao.findMemberCountAfterDate(thisWeekMonday); //本月新增会员数 Integer thisMonthNewMember = memberDao.findMemberCountAfterDate(firstDay4ThisMonth); //今日预约数 Integer todayOrderNumber = orderDao.findOrderCountByDate(today); //本周预约数 Integer thisWeekOrderNumber = orderDao.findOrderCountAfterDate(thisWeekMonday); //本月预约数 Integer thisMonthOrderNumber = orderDao.findOrderCountAfterDate(firstDay4ThisMonth); //今日到诊数 Integer todayVisitsNumber = orderDao.findVisitsCountByDate(today); //本周到诊数 Integer thisWeekVisitsNumber = orderDao.findVisitsCountAfterDate(thisWeekMonday); //本月到诊数 Integer thisMonthVisitsNumber = orderDao.findVisitsCountAfterDate(firstDay4ThisMonth); //热门套餐(取前4) List<Map> hotSetmeal = orderDao.findHotSetmeal(); Map<String,Object> result = new HashMap<>(); result.put("reportDate",today); result.put("todayNewMember",todayNewMember); result.put("totalMember",totalMember); result.put("thisWeekNewMember",thisWeekNewMember); result.put("thisMonthNewMember",thisMonthNewMember); result.put("todayOrderNumber",todayOrderNumber); result.put("thisWeekOrderNumber",thisWeekOrderNumber); result.put("thisMonthOrderNumber",thisMonthOrderNumber); result.put("todayVisitsNumber",todayVisitsNumber); result.put("thisWeekVisitsNumber",thisWeekVisitsNumber); result.put("thisMonthVisitsNumber",thisMonthVisitsNumber); result.put("hotSetmeal",hotSetmeal); return result; } }1.3.4 Dao接口在OrderDao和MemberDao中声明相关统计查询方法package com.yunhe.dao; import com.yunhe.pojo.Order; import java.util.List; import java.util.Map; public interface OrderDao { public void add(Order order); public List<Order> findByCondition(Order order); public Map findById4Detail(Integer id); public Integer findOrderCountByDate(String date); public Integer findOrderCountAfterDate(String date); public Integer findVisitsCountByDate(String date); public Integer findVisitsCountAfterDate(String date); public List<Map> findHotSetmeal(); }package com.yunhe.dao; import com.github.pagehelper.Page; import com.yunhe.pojo.Member; import java.util.List; public interface MemberDao { public List<Member> findAll(); public Page<Member> selectByCondition(String queryString); public void add(Member member); public void deleteById(Integer id); public Member findById(Integer id); public Member findByTelephone(String telephone); public void edit(Member member); public Integer findMemberCountBeforeDate(String date); public Integer findMemberCountByDate(String date); public Integer findMemberCountAfterDate(String date); public Integer findMemberTotalCount(); }1.3.5 Mapper映射文件在OrderDao.xml和MemberDao.xml中定义SQL语句OrderDao.xml:<!--根据日期统计预约数--> <select id="findOrderCountByDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate = #{value} </select> <!--根据日期统计预约数,统计指定日期之后的预约数--> <select id="findOrderCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate >= #{value} </select> <!--根据日期统计到诊数--> <select id="findVisitsCountByDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate = #{value} and orderStatus = '已到诊' </select> <!--根据日期统计到诊数,统计指定日期之后的到诊数--> <select id="findVisitsCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_order where orderDate >= #{value} and orderStatus = '已到诊' </select> <!--热门套餐,查询前4条--> <select id="findHotSetmeal" resultType="map"> select s.name, count(o.id) setmeal_count , count(o.id)/(select count(id) from t_order) proportion from t_order o inner join t_setmeal s on s.id = o.setmeal_id group by o.setmeal_id order by setmeal_count desc limit 0,4 </select>MemberDao.xml:<!--根据日期统计会员数,统计指定日期之前的会员数--> <select id="findMemberCountBeforeDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime <= #{value} </select> <!--根据日期统计会员数--> <select id="findMemberCountByDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime = #{value} </select> <!--根据日期统计会员数,统计指定日期之后的会员数--> <select id="findMemberCountAfterDate" parameterType="string" resultType="int"> select count(id) from t_member where regTime >= #{value} </select> <!--总会员数--> <select id="findMemberTotalCount" resultType="int"> select count(id) from t_member </select>二、. 运营数据统计报表导出2.1 需求分析运营数据统计报表导出就是将统计数据写入到Excel并提供给客户端浏览器进行下载,以便体检机构管理人员对运营数据的查看和存档。2.2 提供模板文件本节我们需要将运营统计数据通过POI写入到Excel文件,对应的Excel效果如下: 通过上面的Excel效果可以看到,表格比较复杂,涉及到合并单元格、字体、字号、字体加粗、对齐方式等的设置。如果我们通过POI编程的方式来设置这些效果代码会非常繁琐。在企业实际开发中,对于这种比较复杂的表格导出一般我们会提前设计一个Excel模板文件,在这个模板文件中提前将表格的结构和样式设置好,我们的程序只需要读取这个文件并在文件中的相应位置写入具体的值就可以了。在本章节资料中已经提供了一个名为report_template.xlsx的模板文件,需要将这个文件复制到health_backend工程的template目录中2.3 完善页面在report_business.html页面提供导出按钮并绑定事件<div class="excelTitle" > <el-button @click="exportExcel">导出Excel</el-button>运营数据统计 </div>methods:{ //导出Excel报表 exportExcel(){ window.location.href = '/report/exportBusinessReport.do'; } }2.4 后台代码在ReportController中提供exportBusinessReport方法,基于POI将数据写入到Excel中并通过输出流下载到客户端。/** * 导出Excel报表 * @return */ @RequestMapping("/exportBusinessReport") public Result exportBusinessReport(HttpServletRequest request, HttpServletResponse response){ try{ //远程调用报表服务获取报表数据 Map<String, Object> result = reportService.getBusinessReport(); //取出返回结果数据,准备将报表数据写入到Excel文件中 String reportDate = (String) result.get("reportDate"); Integer todayNewMember = (Integer) result.get("todayNewMember"); Integer totalMember = (Integer) result.get("totalMember"); Integer thisWeekNewMember = (Integer) result.get("thisWeekNewMember"); Integer thisMonthNewMember = (Integer) result.get("thisMonthNewMember"); Integer todayOrderNumber = (Integer) result.get("todayOrderNumber"); Integer thisWeekOrderNumber = (Integer) result.get("thisWeekOrderNumber"); Integer thisMonthOrderNumber = (Integer) result.get("thisMonthOrderNumber"); Integer todayVisitsNumber = (Integer) result.get("todayVisitsNumber"); Integer thisWeekVisitsNumber = (Integer) result.get("thisWeekVisitsNumber"); Integer thisMonthVisitsNumber = (Integer) result.get("thisMonthVisitsNumber"); List<Map> hotSetmeal = (List<Map>) result.get("hotSetmeal"); //获得Excel模板文件绝对路径 String temlateRealPath = request.getSession().getServletContext().getRealPath("template") + File.separator + "report_template.xlsx"; //读取模板文件创建Excel表格对象 XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(new File(temlateRealPath))); XSSFSheet sheet = workbook.getSheetAt(0); XSSFRow row = sheet.getRow(2); row.getCell(5).setCellValue(reportDate);//日期 row = sheet.getRow(4); row.getCell(5).setCellValue(todayNewMember);//新增会员数(本日) row.getCell(7).setCellValue(totalMember);//总会员数 row = sheet.getRow(5); row.getCell(5).setCellValue(thisWeekNewMember);//本周新增会员数 row.getCell(7).setCellValue(thisMonthNewMember);//本月新增会员数 row = sheet.getRow(7); row.getCell(5).setCellValue(todayOrderNumber);//今日预约数 row.getCell(7).setCellValue(todayVisitsNumber);//今日到诊数 row = sheet.getRow(8); row.getCell(5).setCellValue(thisWeekOrderNumber);//本周预约数 row.getCell(7).setCellValue(thisWeekVisitsNumber);//本周到诊数 row = sheet.getRow(9); row.getCell(5).setCellValue(thisMonthOrderNumber);//本月预约数 row.getCell(7).setCellValue(thisMonthVisitsNumber);//本月到诊数 int rowNum = 12; for(Map map : hotSetmeal){//热门套餐 String name = (String) map.get("name"); Long setmeal_count = (Long) map.get("setmeal_count"); BigDecimal proportion = (BigDecimal) map.get("proportion"); row = sheet.getRow(rowNum ++); row.getCell(4).setCellValue(name);//套餐名称 row.getCell(5).setCellValue(setmeal_count);//预约数量 row.getCell(6).setCellValue(proportion.doubleValue());//占比 } //通过输出流进行文件下载 ServletOutputStream out = response.getOutputStream(); response.setContentType("application/vnd.ms-excel"); response.setHeader("content-Disposition", "attachment;filename=report.xlsx"); workbook.write(out); out.flush(); out.close(); workbook.close(); return null; }catch (Exception e){ return new Result(false, MessageConstant.GET_BUSINESS_REPORT_FAIL,null); } }
-
在 Java 开发中,我们经常需要对集合数据进行过滤、映射、统计等操作。传统的 for 循环和 Iterator 虽然可行,但代码冗长且不易维护。自 Java 8 引入 Stream API 后,集合操作变得更加简洁、函数式和可读性强。一、基于一个实际的业务背景假设我们正在开发一个电商后台系统,需要对商品(Product)进行如下分析:查询价格大于 100 元的商品按分类统计商品数量获取销量最高的前 3 个商品将商品名称转为大写并去重计算所有商品的平均价格我们将使用 List<Product> 模拟数据源,并通过 Stream 实现上述需求。二、实体类定义public class Product { private Long id; private String name; private String category; // 分类:如 "Electronics", "Clothing" private Double price; private Integer sales; // 销量 // 构造方法、getter、setter 省略 public Product(Long id, String name, String category, Double price, Integer sales) { this.id = id; this.name = name; this.category = category; this.price = price; this.sales = sales; } @Override public String toString() { return "Product{id=" + id + ", name='" + name + "', category='" + category + "', price=" + price + ", sales=" + sales + '}'; }}三、数据准备import java.util.Arrays;import java.util.List;List<Product> products = Arrays.asList( new Product(1L, "iPhone 15", "Electronics", 999.99, 120), new Product(2L, "MacBook Pro", "Electronics", 1999.99, 85), new Product(3L, "T-Shirt", "Clothing", 29.99, 200), new Product(4L, "Jeans", "Clothing", 79.99, 150), new Product(5L, "Watch", "Electronics", 299.99, 90), new Product(6L, "Dress", "Clothing", 149.99, 60), new Product(7L, "AirPods", "Electronics", 179.99, 300));四、Stream 实战案例查询价格大于 100 元的商品List<Product> expensiveProducts = products.stream() .filter(p -> p.getPrice() > 100) .collect(Collectors.toList());expensiveProducts.forEach(System.out::println);输出:Product{id=1, name='iPhone 15', category='Electronics', price=999.99, sales=120}Product{id=2, name='MacBook Pro', category='Electronics', price=1999.99, sales=85}Product{id=5, name='Watch', category='Electronics', price=299.99, sales=90}Product{id=6, name='Dress', category='Clothing', price=149.99, sales=60}Product{id=7, name='AirPods', category='Electronics', price=179.99, sales=300}说明:filter() 用于条件筛选,collect(Collectors.toList()) 将结果收集为 List。按分类统计商品数量Map<String, Long> countByCategory = products.stream() .collect(Collectors.groupingBy( Product::getCategory, Collectors.counting() ));System.out.println(countByCategory);// 输出:{Clothing=3, Electronics=4}说明:groupingBy 实现分组,counting() 统计每组数量。获取销量最高的前 3 个商品List<Product> top3Sales = products.stream() .sorted(Comparator.comparing(Product::getSales).reversed()) .limit(3) .collect(Collectors.toList());top3Sales.forEach(p -> System.out.println(p.getName() + " - 销量: " + p.getSales()));输出:AirPods - 销量: 300T-Shirt - 销量: 200Jeans - 销量: 150说明:sorted() 排序,reversed() 降序,limit(3) 取前3条。商品名称转大写并去重Set<String> upperCaseNames = products.stream() .map(p -> p.getName().toUpperCase()) .distinct() .collect(Collectors.toSet());System.out.println(upperCaseNames);// 输出:[MACBOOK PRO, DRESS, AIRPODS, T-SHIRT, WATCH, IPHONE 15, JEANS]说明:map() 转换数据,distinct() 去重。计算所有商品的平均价格Double averagePrice = products.stream() .mapToDouble(Product::getPrice) .average() .orElse(0.0);System.out.printf("平均价格: %.2f%n", averagePrice);// 输出:平均价格: 661.42说明:mapToDouble() 转换为原始类型流,避免装箱开销;average() 返回 OptionalDouble。 通过以上案例可以看出,Java 8 的 Stream API 极大地提升了集合操作的表达力和简洁性。它将“做什么”(what to do)与“怎么做”(how to do)分离,让我们在实际的开发过程中,可以很便捷的处理数据流。 附:常用 Stream 方法速查表类型方法说明中间操作filter, map, flatMap, distinct, sorted, peek, limit, skip返回 Stream,可链式调用终端操作collect, forEach, count, anyMatch, allMatch, noneMatch, findFirst, reduce触发执行,返回结果或 void
-
JasperReport是一个强大、灵活的报表生成工具,能够展示丰富的页面内容,并将之转换成PDF,HTML,或者XML格式。该库完全由Java写成,可以用于在各种Java应用程序,包括J2EE,Web应用程序中生成动态内容。下载JasperReports的JAR包和iReport设计器,并将其添加到项目中。接着,创建JasperReport报表模板和数据源,编写JRXML文件。在文件中定义数据源、Query语句、数据列和其他相关信息。然后,使用iReport设计器来修改和设置报表模板。在iReport中,可以添加图像、文本、表格和其他控件,并对其进行格式化和布局。同时,可以设置条件格式、样式和表格特性等。接下来,编写Java代码来调用并生成报表。要使用JasperReports内置的工具,只需几行代码即可完成报表的生成、导出和打印。例如,可以使用JasperPrint进行数据填充,并使用JasperExportManager导出PDF、Excel、HTML或其他格式。进行测试和调试。调试时,应该特别注意数据源、参数和生成结果是否符合预期。如果出现错误,可以查看错误日志并逐一排除错误。结合JasperReports输出报表前面我们已经使用Jaspersoft Studio设计了两个模板文件:demo1.jrxml和demo2.jrxml。其中demo1.jrxml的动态列表数据是基于JDBC数据源方式进行数据填充,demo2.jrxml的动态列表数据是基于JavaBean数据源方式进行数据填充。本小节我们就结合JasperReports的Java API来完成pdf报表输出。一、JDBC数据源方式填充数据第一步:创建maven工程,导入相关maven坐标<dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports</artifactId> <version>6.8.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency>第二步:将设计好的demo1.jrxml文件复制到当前工程的resources目录下 第三步:编写单元测试@Test public void testReport_JDBC() throws Exception{ Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/health", "root", "root"); String jrxmlPath = "D:\\ideaProjects\\projects111\\jasperreports_test\\src\\main\\resources\\demo1.jrxml"; String jasperPath = "D:\\ideaProjects\\projects111\\jasperreports_test\\src\\main\\resources\\demo1.jasper"; //编译模板 JasperCompileManager.compileReportToFile(jrxmlPath,jasperPath); //构造数据 Map paramters = new HashMap(); paramters.put("company","xx公司"); //填充数据---使用JDBC数据源方式填充 JasperPrint jasperPrint = JasperFillManager.fillReport(jasperPath, paramters, connection); //输出文件 String pdfPath = "D:\\test.pdf"; JasperExportManager.exportReportToPdfFile(jasperPrint,pdfPath); }通过上面的操作步骤可以输出pdf文件,但是中文的地方无法正常显示。这是因为JasperReports默认情况下对中文支持并不友好,需要我们自己进行修复。具体操作步骤如下:1、在Jaspersoft Studio中打开demo1.jrxml文件,选中中文相关元素,统一将字体设置为“华文宋体”并将修改后的demo1.jrxml重新复制到maven工程中2、将本章资源/解决中文无法显示问题目录下的文件复制到maven工程的resources目录中 按照上面步骤操作后重新执行单元测试导出PDF文件: 二、 JavaBean数据源方式填充数据第一步:为了能够避免中文无法显示问题,首先需要将demo2.jrxml文件相关元素字体改为“华文宋体”并将demo2.jrxml文件复制到maven工程的resources目录下 第二步:编写单元测试方法输出PDF文件@Test public void testReport_JavaBean() throws Exception{ String jrxmlPath = "D:\\ideaProjects\\projects111\\jasperreports_test\\src\\main\\resources\\demo2.jrxml"; String jasperPath = "D:\\ideaProjects\\projects111\\jasperreports_test\\src\\main\\resources\\demo2.jasper"; //编译模板 JasperCompileManager.compileReportToFile(jrxmlPath,jasperPath); //构造数据 Map paramters = new HashMap(); paramters.put("company","xx公司"); List<Map> list = new ArrayList(); Map map1 = new HashMap(); map1.put("tName","入职体检套餐"); map1.put("tCode","RZTJ"); map1.put("tAge","18-60"); map1.put("tPrice","500"); Map map2 = new HashMap(); map2.put("tName","阳光爸妈老年健康体检"); map2.put("tCode","YGBM"); map2.put("tAge","55-60"); map2.put("tPrice","500"); list.add(map1); list.add(map2); //填充数据---使用JavaBean数据源方式填充 JasperPrint jasperPrint = JasperFillManager.fillReport(jasperPath, paramters, new JRBeanCollectionDataSource(list)); //输出文件 String pdfPath = "D:\\test.pdf"; JasperExportManager.exportReportToPdfFile(jasperPrint,pdfPath); }查看输出效果: 三、 在项目中输出运营数据PDF报表本小节我们将在项目中实现运营数据的PDF报表导出功能。3.1 设计PDF模板文件使用Jaspersoft Studio设计运营数据PDF报表模板文件health_business3.jrxml,设计后的效果如下: 在资源中已经提供好了此文件,直接使用即可。3.2 搭建环境第一步:在health_common工程的pom.xml中导入JasperReports的maven坐标<dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports</artifactId> <version>6.8.0</version> </dependency>第二步:将资源中提供的模板文件health_business3.jrxml复制到health_backend工程的template目录下 第三步:将解决中问题的相关资源文件复制到项目中 3.3 修改页面修改health_backend工程的report_business.html页面,添加导出PDF的按钮并绑定事件 3.4 Java代码实现在health_backend工程的ReportController中提供exportBusinessReport4PDF方法//导出运营数据到pdf并提供客户端下载 @RequestMapping("/exportBusinessReport4PDF") public Result exportBusinessReport4PDF(HttpServletRequest request, HttpServletResponse response) { try { Map<String, Object> result = reportService.getBusinessReportData(); //取出返回结果数据,准备将报表数据写入到PDF文件中 List<Map> hotSetmeal = (List<Map>) result.get("hotSetmeal"); //动态获取模板文件绝对磁盘路径 String jrxmlPath = request.getSession().getServletContext().getRealPath("template") + File.separator + "health_business3.jrxml"; String jasperPath = request.getSession().getServletContext().getRealPath("template") + File.separator + "health_business3.jasper"; //编译模板 JasperCompileManager.compileReportToFile(jrxmlPath, jasperPath); //填充数据---使用JavaBean数据源方式填充 JasperPrint jasperPrint = JasperFillManager.fillReport(jasperPath,result, new JRBeanCollectionDataSource(hotSetmeal)); ServletOutputStream out = response.getOutputStream(); response.setContentType("application/pdf"); response.setHeader("content-Disposition", "attachment;filename=report.pdf"); //输出文件 JasperExportManager.exportReportToPdfStream(jasperPrint,out); return null; } catch (Exception e) { e.printStackTrace(); return new Result(false, MessageConstant.GET_BUSINESS_REPORT_FAIL); } }
-
对于一些带着固定标签的字段来说,我们通常把它们配置到字段中,而在数据库中存它们的字典code,或者是字典主键,不是一个整型的数字,而在前端显示时,有时需要将它们翻译成名称,这时后端可以帮他们进行翻译,或者前端通过code自己使用字典翻译;下面说一下第一种,后端在View model中将integer类型的字典字典翻译成一个k/v的对象。JsonSerializer一个json序列化的基类,我们可以继承它,并实现自己的原因,在springboot框架中,你返回的json对象事实上是jackson帮我们做了一次序列化工作,而我们的字段如果希望在序列化时行加工,可以利用这个环节,下面定义一下DictionarySerializer,来实现字典字段的序列化。/** * 自定义序列化器,将一个Integer类型的字段序列化成一个name/code的对象 */ public class DictionarySerializer extends JsonSerializer<Integer> { @Autowired DictionaryMapper dictionaryMapper; @Override public void serialize(Integer value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { //获取当前字段的名称 String type = jsonGenerator.getOutputContext().getCurrentName(); Integer code = (Integer) value; jsonGenerator.writeStartObject(); Dictionary dictionary = dictionaryMapper.selectOne( new QueryWrapper<Dictionary>().lambda() .eq(Dictionary::getCode, code) .eq(Dictionary::getType, type)); if (dictionary == null) throw new IllegalArgumentException(String.format("字典数据未配置,类型:%s,值:%s", type, code)); jsonGenerator.writeStringField("name", dictionary.getName()); jsonGenerator.writeNumberField("code", code); jsonGenerator.writeEndObject(); } }在实体中gender字段会进行声明 @ApiModelProperty("性别") @JsonSerialize(using= DictionarySerializer.class) private Integer gender; 在接口中返回一个对象,对象中包含了gender字段,而这个字段已经被序列化成对象,本例通过查询数据库实现,实际工作中,应该通过缓存来实现。 { "id": "ab9a48d4f49d93237f7090d340d9fa07", "username": "123", "email": "123@qq.com", "phone": "13754911028", "realName": null, "roleList": [ { "id": "1", "name": "管理员1" } ], "createTime": "2022-04-12T10:04:14", "updateTime": "2022-04-12T10:04:14", "createBy": "admin", "updateBy": "admin", "status": 1, "organization": null, "job": null, "gender": { "name": "男", "value": 0 } }转载自https://www.cnblogs.com/lori/p/16162883.html
-
起因在java项目中,我在maven的pom.xml中引用了io.github.officiallysingh:spring-boot-starter-spark:1.3包,然后这个包里又有org.apache.spark:spark-core_2.13:3.5.5包的引用,而在spark-core_2.13包中又引用了org.apache.avro:avro-mapred:1.11.4包,这个包的版本0.10.0修改为0.9.0,我们如何实现呢?推荐方法通过在dependencyManagement中声明三方包的版本,来在自己项目中,将所有指定包的版本进行统一,并且包版本不同产生的冲突在当前项目的pom.xml中添加代码<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.apache.avro</groupId> <artifactId>avro-mapred</artifactId> <version>1.11.3</version> </dependency> </dependencyManagement>刷新依赖之后,可以看到三方包里的依赖包avro-mapred版本已经改变了其它方法下面这个表格总结了你可以在项目中使用的三种主要策略。方法操作方式适用场景与说明💡 直接声明依赖在 <dependencies> 中直接声明你想要的 jersey-client 版本。最简洁直接,适用于单模块项目,快速覆盖传递依赖的版本。📦 依赖管理在 <dependencyManagement> 中统一管理 jersey-client 的版本。推荐用于多模块项目,可以保证所有模块使用的版本一致,避免冲突。🗑️ 排除+引入先通过 <exclusions> 排除旧版本,再显式引入新版本。最严格的控制,确保构建时不会引入冲突的旧版本,但配置稍显繁琐。 转载自https://www.cnblogs.com/lori/p/19142696
-
前言欢迎来到 Redis Set 的终极指南。如果您曾需要管理一组独一无二的元素集合——无论是用户 ID、文章标签还是邮件地址——并希望以闪电般的速度对其执行强大的集合运算,那么您来对地方了。Redis Set 绝不是一个简单的列表,它是一种精妙的数据结构,将数学中强大的集合理论直接带入您的高性能数据库中。在本文中,我们将从最基础的概念讲起,逐步深入到高级的实际应用。我们将使用优秀的 C++ 库 redis-plus-plus 来演示所有示例,并逐行剖析代码。无论您是 C++ 开发者、后端工程师,还是仅仅对 Redis 感到好奇,读完本文,您都将深刻理解是什么让 Set 成为 Redis 中功能最丰富的工具之一。Redis Set 究竟是什么?在我们深入代码之前,先来建立一个清晰的思维模型。想象你有一个魔力袋,你可以往里面扔东西,但这个袋子有两条非常特殊的规则:强制保持唯一:这个袋子会自动拒绝重复的物品。如果你想把一个标有“A”的弹珠放进一个已经有“A”弹珠的袋子里,它会阻止你,确保袋子里每样东西都只有一个。顺序毫不在意:当你从袋子里往外取东西时,它们的顺序是完全随机的。袋子不记得到底是按什么顺序把东西放进去的。这个“魔力袋”正是 Redis Set 的精准比喻:一个无序的、元素唯一的字符串集合。这个简单的定义是其强大功能的基石,使其能够以惊人的速度进行成员资格检查、数量统计以及诸如交集、并集等复杂的服务器端运算。第一章:基础入门 - 创建和查看你的第一个 Set让我们从最基本的操作开始:如何向一个 Set 添加元素,以及如何查看它的全部内容。为此,我们将使用 SADD 和 SMEMBERS 这两个命令。SADD:向集合中添加成员SADD 是您向 Set 中添加一个或多个元素的主要工具。如果某个元素已经存在,Redis 会优雅地忽略它。该命令的返回值是新成功添加的元素的数量。SMEMBERS:获取所有成员SMEMBERS 的功能正如其名:返回指定 Set 中的所有成员。这对于获取整个集合非常有用,但请注意:在拥有数百万元素的超大 Set 上使用此命令可能会暂时阻塞您的 Redis 服务器,因为它需要时间来准备所有数据。我们将在后续章节中讨论更安全的替代方案 SSCAN。C++ 实战:sadd 与 smembers现在,让我们来分析一段代码,它演示了这些基础操作。 // 引入必要的头文件...#include <iostream>#include <set>#include <string>#include <vector>#include <iterator>#include <sw/redis++/redis.h>// 一个辅助函数,用于打印容器内容template<typename T>void PrintContainer(const T& container) { for (const auto& elem : container) { std::cout << elem << " "; } std::cout << std::endl;}void test1(sw::redis::Redis& redis){ std::cout << "sadd 和 smembers" << std::endl; // 清空数据库,确保一个干净的测试环境 redis.flushall(); // 1. 一次添加一个元素 redis.sadd("key", "111"); // 2. 使用初始化列表,一次添加多个元素 redis.sadd("key", {"222", "333", "444"}); // 3. 使用迭代器,从另一个容器中添加多个元素 std::set<std::string> elems = {"555", "666", "777"}; // 返回值是成功插入了多少个元素 redis.sadd("key", elems.begin(), elems.end()); // --- 现在,让我们获取所有元素 --- std::set<std::string> result; // 为我们的 C++ set 构建一个插入迭代器 auto it = std::inserter(result, result.end()); // 从 Redis set 中获取所有成员,并插入到我们的 C++ set 中 redis.smembers("key", it); PrintContainer(result);}一键获取完整项目代码cpp代码剖析:redis.flushall():我们首先清空整个 Redis 数据库,以确保测试环境的纯净。单个元素 sadd:redis.sadd("key", "111"); 将字符串 “111” 添加到名为 key 的 Set 中。由于 Set 原本是空的,此命令返回 1。初始化列表 sadd:redis.sadd("key", {"222", "333", "444"}); 展示了 redis-plus-plus 库的一个便捷特性,允许您一次性添加多个元素。这比发送三个独立的命令效率更高。此调用将返回 3。基于迭代器的 sadd:在这里,我们先填充了一个 C++ 的 std::set,然后使用它的迭代器(elems.begin(), elems.end())将其所有元素添加到 Redis 的 Set 中。这对于将现有 C++ 容器中的数据同步到 Redis 非常有用。使用 smembers 获取数据:我们创建了一个 std::set<string> result; 来存放从 Redis 返回的数据。在客户端使用 std::set 是一个绝佳选择,因为它不仅 mirroring(镜像)了 Redis Set 的唯一性,还能自动对元素进行排序,便于我们进行可预测的展示。auto it = std::inserter(result, result.end()); 是至关重要的一行。我们需要一种方式告诉 redis-plus-plus 应该把接收到的元素放在哪里。inserter 是一种特殊的迭代器,当你给它赋值时,它会调用其关联容器的 insert() 方法。redis.smembers("key", it); 执行命令。redis-plus-plus 获取 key 中的所有成员,并使用我们的迭代器 it 将它们逐一插入到 result 集合中。C++ 关键概念:inserter vs back_inserter在原始笔记中,有一个关键的区别被强调了出来:std::back_inserter 创建一个调用 push_back() 的迭代器。它适用于 std::vector, std::list, std::deque 等容器。std::set 没有 push_back() 方法,因为它需要维护内部的排序。因此,对于 std::set,我们必须使用 std::inserter,它会调用 insert() 方法。预测输出:PrintContainer 函数将打印 result 集合的内容。由于 std::set 会对其元素进行排序,输出将是按字母/数字顺序排列的。sadd 和 smembers111 222 333 444 555 666 777一键获取完整项目代码第二章:深入探索 - 检查与修改你的 Set既然我们知道了如何构建一个 Set,接下来让我们学习如何查询它的属性并执行基本的修改。这些命令是 Set 日常操作的核心,并且它们都快得令人难以置信。SISMEMBER:这个元素存在吗? (时间复杂度 O(1))这是 Set 命令库中最强大的命令之一。SISMEMBER 检查一个特定元素是否是 Set 的成员。如果存在,返回 1 (true);如果不存在,返回 0 (false)。它的性能是 O(1),这意味着其速度是恒定的,不依赖于 Set 的大小。无论是在一个有10个元素的 Set 还是在一个有1000万个元素的 Set 中检查成员资格,花费的时间都是相同的。C++ 实战:sismembervoid test2(sw::redis::Redis& redis){ std::cout << "sismember" << std::endl; redis.flushall(); redis.sadd("key", {"111", "222", "333", "444"}); // 检查 "111" 是否是集合的成员 bool result = redis.sismember("key", "111"); std::cout << "result:" << result << std::endl;}一键获取完整项目代码cpp剖析:我们创建一个 Set,然后使用 sismember 检查 “111” 是否存在。redis-plus-plus 库非常方便地将 Redis 返回的 1 或 0 直接映射为了 C++ 的 bool 类型。因为 “111” 确实在 Set 中,result 将为 true。应用场景:标签系统:检查一篇博客文章是否已经被标记为 “DevOps”。权限控制:检查一个 userID 是否在 admin_users 这个 Set 中。唯一性事件:检查用户是否已经执行了某个一次性操作(例如,“voted_on_poll_123”)。预测输出:当 bool true 被输出到 cout 时,通常会显示为 1。sismemberresult:1一键获取完整项目代码SCARD:集合里有多少元素? (时间复杂度 O(1))SCARD 代表 “Set Cardinality”(集合基数),它简单地返回一个 Set 中元素的数量。与 SISMEMBER 一样,这也是一个 O(1) 操作。Redis 内部维护了一个计数器,所以它不需要遍历所有元素就能告诉你总数。C++ 实战:scardvoid test3(sw::redis::Redis& redis){ std::cout << "scard" << std::endl; redis.flushall(); // 向集合中添加4个唯一元素 redis.sadd("key", {"111", "222", "333", "444"}); // 获取集合中的元素个数 long long result = redis.scard("key"); // 返回 4 std::cout << "result:" << result << std::endl;}一键获取完整项目代码cpp剖析:我们添加了四个元素,然后调用 scard。命令返回了计数 4。应用场景:在线用户:跟踪已登录的独立用户数量。点赞计数:快速显示一张照片获得的独立点赞数。数据分析:统计今天访问网站的独立 IP 地址数量。预测输出:scardresult:4一键获取完整项目代码SPOP:随机移除并返回一个元素SPOP 是一个既有趣又实用的命令。它会从 Set 中随机选择一个元素,将其移除,然后返回给你。这是一种“破坏性读取”,因为元素在被读取后就从集合中消失了。C++ 实战:spopvoid test4(sw::redis::Redis& redis){ std::cout << "spop" << std::endl; redis.flushall(); redis.sadd("key", {"111", "222", "333", "444"}); // 随机弹出一个元素,spop 的返回值是 Optional<string> auto result = redis.spop("key"); if (result) { // 因为返回值是 Optional,我们通过 .value() 来获取原始的 string 内容 std::cout << "result:" << result.value() << std::endl; } else { std::cout << "result is empty" << std::endl; }}一键获取完整项目代码cpp剖析:auto result = redis.spop("key"); 执行命令。redis-plus-plus 将返回值包装在 sw::redis::Optional<std::string> 中。这是因为如果你对一个空 Set 执行 spop,Redis 会返回 nil(空)。Optional 类型可以优雅地处理这种情况,避免空指针等问题。if (result) 检查 Optional 对象是否真的包含一个值。在我们的例子中,由于 Set 非空,它肯定会弹出一个元素,所以条件为真。result.value() 从 Optional 中提取出实际的 std::string 值。核心特性:随机性:SPOP 最大的特点就是随机。这意味着每次运行这段代码,得到的结果都可能不同。它非常适合需要随机处理任务的场景。应用场景:抽奖系统:从参与用户 Set 中随机抽取一名中奖者。任务队列:从待处理任务池中随机分配一个任务给工作进程。在线匹配:从等待匹配的玩家池中随机抽取一个进行游戏。预测输出:输出是不确定的,可能是以下四种情况之一:// 可能的输出 1spopresult:111// 可能的输出 2spopresult:333一键获取完整项目代码第三章:集合的威力 - 集合运算这才是 Redis Set 真正大放异彩的地方。Redis 能够在服务器端以极高的效率执行集合的交集 (intersection)、并集 (union) 和差集 (difference) 运算,避免了将大量数据传输到客户端再进行计算的开销。交集运算:SINTER & SINTERSTORE交集运算会找出所有给定的 Set 中共同存在的元素。SINTER: 计算交集并直接返回给客户端。SINTERSTORE: 计算交集,但不返回,而是将结果存储在一个新的目标 Set 中。C++ 实战:sinter (求交集并返回)void test5(sw::redis::Redis& redis){ // 这里的 cout 应该是 "sinter",一个小笔误 std::cout << "sinter" << std::endl; redis.flushall(); redis.sadd("key1", {"111", "222", "333", "444"}); redis.sadd("key2", {"111", "222", "444"}); std::set<std::string> result; auto it = std::inserter(result, result.end()); // 求交集涉及多个 key,我们使用初始化列表来描述 // 将 "key1" 和 "key2" 的交集插入到 result 中 redis.sinter({"key1", "key2"}, it); PrintContainer(result);}一键获取完整项目代码cpp剖析:key1 包含 {"111", "222", "333", "444"}。key2 包含 {"111", "222", "444"}。redis.sinter({"key1", "key2"}, it); 命令计算出两个集合的共同成员是 {"111", "222", "444"},并通过迭代器将它们存入 C++ 的 result 集合中。应用场景:共同好友:计算用户A的好友列表和用户B的好友列表的交集。内容推荐:找出同时对 “科幻” 和 “悬疑” 标签感兴趣的用户。预测输出:sinter111 222 444一键获取完整项目代码12C++ 实战:sinterstore (求交集并存储)void test6(sw::redis::Redis& redis){ std::cout << "sinterstore" << std::endl; redis.flushall(); redis.sadd("key1", {"111", "222", "333"}); redis.sadd("key2", {"111", "222", "444"}); // 指定一个 destination ("key3"),将交集结果存储到其中 long long len = redis.sinterstore("key3", {"key1", "key2"}); std::cout << "len:" << len << std::endl; // 检查 "key3" 中的元素以验证结果 std::set<std::string> result; auto it = std::inserter(result, result.end()); redis.smembers("key3", it); PrintContainer(result);}一键获取完整项目代码cpp剖析:redis.sinterstore("key3", {"key1", "key2"}); 计算出交集 {"111", "222"},然后将这个结果存入一个全新的 Set key3 中。如果 key3 已存在,它将被覆盖。该命令返回新生成的 key3 集合的元素数量,即 2。所以 len 的值为 2。后续的 smembers 验证了 key3 的内容确实是正确的交集结果。应用场景:当你需要缓存或复用交集计算结果时,SINTERSTORE 非常有用。例如,为一组用户预先计算出他们共同喜欢的商品列表。预测输出:sinterstorelen:2111 222一键获取完整项目代码第四章:超越基础 - 更多强大的 Set 命令我们已经覆盖了所提供代码中的所有命令,但 Redis Set 的能力远不止于此。为了成为真正的 Set 大师,让我们来了解一下其他一些极其有用的命令。并集运算:SUNION & SUNIONSTORE并集运算返回所有给定集合的全部不重复的元素。命令:SUNION key [key ...] 和 SUNIONSTORE destination key [key ...]应用场景:好友圈:获取用户A的好友、用户B的好友和用户C的好友的完整、不重复的列表。权限合并:一个用户属于 “editor” 角色组和 “publisher” 角色组,通过并集可以得到该用户拥有的所有权限的集合。差集运算:SDIFF & SDIFFSTORE差集运算返回那些只存在于第一个集合中,但不在任何后续集合中的元素。命令:SDIFF key [key ...] 和 SDIFFSTORE destination key [key ...]应用场景:好友推荐:找出我的好友中,有哪些还不是我朋友A的好友,从而可以向我推荐。内容去重:向用户展示新闻时,从“今日热点”中排除掉他“已读新闻”Set 中的内容。安全迭代:SSCAN正如前文提到的,SMEMBERS 对于大集合是危险的。SSCAN 提供了安全的替代方案。它使用一个游标 (cursor) 来分批次地返回集合中的元素,每次只返回一小部分,绝不会阻塞服务器。命令:SSCAN key cursor [MATCH pattern] [COUNT count]工作方式:你用一个初始为 0 的游标开始第一次调用。Redis 返回下一批元素和一个新的游标。你用这个新的游标进行下一次调用,如此往复,直到返回的游标为 0,表示迭代完成。适用场景:任何需要遍历生产环境中大集合的操作,例如数据迁移、离线分析等。总结Redis Set 是一种看似简单却异常强大的数据结构。让我们回顾一下它的核心优势:唯一性:自动处理数据去重,简化了应用逻辑。极速性能:绝大多数核心操作(增、删、查、计数)的时间复杂度都是 O(1),性能与集合大小无关。强大的集合运算:能够在服务器端原子性地、高效地执行交、并、差集运算,极大地减少了网络开销和客户端的计算压力。从简单的在线用户统计,到复杂的社交网络好友关系分析,再到智能推荐系统,Redis Set 都能以其优雅和高效提供坚实的解决方案。希望通过本文的深度解析和 C++ 代码示例,您已经准备好在自己的项目中发挥 Redis Set 的真正威力了。————————————————原文链接:https://blog.csdn.net/2301_80863610/article/details/152178781
-
Java 中的并发(Concurrency) 指多个任务在同一时间段内交替执行(宏观上同时进行,微观上可能是 CPU 快速切换调度),目的是提高程序效率,充分利用系统资源(如 CPU、内存、I/O 等)。一、为什么需要并发?资源利用率最大化当程序执行 I/O 操作(如读写文件、网络请求)时,CPU 通常处于空闲状态。通过并发,可在等待 I/O 时让 CPU 处理其他任务,避免资源浪费。例如:一个下载文件的程序,在等待网络数据时,可同时解析已下载的部分数据。响应速度提升对于交互式程序(如 GUI 应用、服务器),并发能避免单任务阻塞导致的界面卡顿或请求超时。例如:Web 服务器同时处理多个用户的请求,而非逐个排队处理。二、并发的核心概念1. 线程(Thread)与进程(Process)进程:程序的一次执行过程,是系统资源分配的基本单位(有独立的内存空间)。线程:进程内的执行单元,是 CPU 调度的基本单位(共享进程的内存空间)。关系:一个进程可包含多个线程(多线程),线程间切换成本远低于进程切换。2. 并行(Parallelism)与并发(Concurrency)的区别并发:多个任务“交替执行”(CPU 切换速度快,看起来同时进行),适用于单 CPU 或多 CPU。并行:多个任务“同时执行”(需多 CPU 核心,每个核心处理一个任务)。例如:4 核 CPU 同时运行 4 个线程是并行,1 核 CPU 快速切换 4 个线程是并发。三、Java 实现并发的方式Java 提供了多种并发编程工具,核心是通过线程实现:1. 基础方式继承 Thread 类:重写 run() 方法定义任务,调用 start() 启动线程。实现 Runnable 接口:定义任务逻辑,通过 Thread 类包装并启动(推荐,避免单继承限制)。实现 Callable 接口:与 Runnable 类似,但可返回结果并抛出异常,配合 Future 获取结果。// Callable 示例import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException;import java.util.concurrent.FutureTask;public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { // 1. 定义任务(有返回值) Callable<Integer> task = () -> { int sum = 0; for (int i = 0; i <= 100; i++) { sum += i; } return sum; }; // 2. 包装任务 FutureTask<Integer> futureTask = new FutureTask<>(task); // 3. 启动线程 new Thread(futureTask).start(); // 4. 获取结果(会阻塞直到任务完成) System.out.println("1-100的和:" + futureTask.get()); // 输出5050 }}一键获取完整项目代码java2. 线程池(ThreadPoolExecutor)频繁创建/销毁线程会消耗资源,线程池通过复用线程提高效率,是生产环境的首选。Java 提供 Executors 工具类快速创建线程池:import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ThreadPoolDemo { public static void main(String[] args) { // 创建固定大小的线程池(3个线程) ExecutorService pool = Executors.newFixedThreadPool(3); // 提交5个任务(线程池会复用3个线程处理) for (int i = 0; i < 5; i++) { int taskId = i; pool.submit(() -> { System.out.println("处理任务" + taskId + ",线程:" + Thread.currentThread().getName()); }); } // 关闭线程池 pool.shutdown(); }}一键获取完整项目代码java四、并发带来的问题及解决方案并发虽提高效率,但多线程共享资源时会引发问题:1. 线程安全问题当多个线程同时操作共享数据(如全局变量、集合),可能导致数据不一致。示例:两个线程同时对变量 count 做 ++ 操作,预期结果为 2,实际可能为 1(因 ++ 是多步操作,可能被打断)。2. 解决方案synchronized 关键字:通过“锁”保证同一时间只有一个线程执行临界区代码(修饰方法或代码块)。public class SynchronizedDemo { private static int count = 0; private static final Object lock = new Object(); // 锁对象 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { synchronized (lock) { // 同步代码块:同一时间只有一个线程进入 count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { synchronized (lock) { count++; } } }); t1.start(); t2.start(); t1.join(); // 等待线程执行完毕 t2.join(); System.out.println("count最终值:" + count); // 正确输出20000 }}一键获取完整项目代码javajava.util.concurrent 工具类:提供线程安全的集合(如 ConcurrentHashMap)、原子类(如 AtomicInteger)、锁机制(如 ReentrantLock)等,比 synchronized 更灵活。五、并发编程的核心挑战可见性:一个线程修改的共享变量,其他线程可能无法立即看到(因 CPU 缓存导致)。解决方案:使用 volatile 关键字(保证变量修改后立即刷新到主内存)。原子性:一个操作不可被中断(如 count++ 实际是“读-改-写”三步,非原子操作)。解决方案:synchronized、原子类(AtomicInteger)。有序性:CPU 可能对指令重排序优化,导致代码执行顺序与预期不一致。解决方案:volatile、synchronized 或显式内存屏障。六、总结并发的本质:通过多线程交替执行,提高资源利用率和程序响应速度。核心问题:线程安全(数据不一致),需通过锁机制或并发工具解决。实践建议:优先使用线程池管理线程,避免手动创建;复杂场景下借助 java.util.concurrent 包的工具类(如 CountDownLatch、Semaphore)简化开发。理解并发是 Java 进阶的关键,尤其在高并发场景(如分布式系统、高流量服务器)中,合理设计并发模型能显著提升系统性能。————————————————原文链接:https://blog.csdn.net/2508_93307008/article/details/153339720
-
什么是动态代理首先,动态代理是代理模式的一种实现方式,代理模式除了动态代理还有 静态代理,只不过静态代理能够在编译时期确定类的执行对象,而动态代理只有在运行时才能够确定执行对象是谁。代理可以看作是对最终调用目标的一个封装,能够通过操作代理对象来调用目标类,这样就可以实现调用者和目标对象的解耦合。动态代理的应用场景有很多,最常见的就是 AOP 的实现、RPC 远程调用、Java 注解对象获取、日志框架、全局性异常处理、事务处理等。动态代理的实现有很多,但是 JDK 动态代理是很重要的一种,下面就 JDK 动态代理来深入理解一波。 JDK 动态代理首先先来看一下动态代理的执行过程在 JDK 动态代理中,实现了 InvocationHandler 的类可以看作是 代理类(因为类也是一种对象,所以上面为了描述关系,把代理类形容成了代理对象)。JDK 动态代理就是围绕实现了 InvocationHandler 的代理类进行的,比如下面就是一个 InvocationHandler 的实现类,同时它也是一个代理类。public class UserHandler implements InvocationHandler { private UserDao userDao; public UserHandler(UserDao userDao){ this.userDao = userDao; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { saveUserStart(); Object obj = method.invoke(userDao, args); saveUserDone(); return obj; } public void saveUserStart(){ System.out.println("---- 开始插入 ----"); } public void saveUserDone(){ System.out.println("---- 插入完成 ----"); }}一键获取完整项目代码代理类一个最最最重要的方法就是 invoke 方法,它有三个参数Object proxy: 动态代理对象,关于这个方法后面会说。Method method: 表示最终要执行的方法,method.invoke 用于执行被代理的方法,也就是真正的目标方法Object[] args: 这个参数就是向目标方法传递的参数。这里构造好了代理类,现在就要使用它来实现对目标对象的调用,那么如何操作呢?请看下面代码public static void dynamicProxy(){ UserDao userDao = new UserDaoImpl(); InvocationHandler handler = new UserHandler(userDao); ClassLoader loader = userDao.getClass().getClassLoader(); Class<?>[] interfaces = userDao.getClass().getInterfaces(); UserDao proxy = (UserDao)Proxy.newProxyInstance(loader, interfaces, handler); proxy.saveUser();}一键获取完整项目代码如果要用 JDK 动态代理的话,就需要知道目标对象的类加载器、目标对象的接口,当然还要知道目标对象是谁。构造完成后,就可以调用 Proxy.newProxyInstance方法,然后把类加载器、目标对象的接口、目标对象绑定上去就完事儿了。这里需要注意一下 Proxy 类,它就是动态代理实现所用到的代理类。Proxy 位于java.lang.reflect 包下,这同时也旁敲侧击的表明动态代理的本质就是反射。下面就围绕 JDK 动态代理,来深入理解一下它的原理,以及搞懂为什么动态代理的本质就是反射。 动态代理的实现原理在了解动态代理的实现原理之前,先来了解一下 InvocationHandler 接口 InvocationHandler 接口JavaDoc 告诉我们,InvocationHandler 是一个接口,实现这个接口的类就表示该类是一个代理实现类,也就是代理类。InvocationHandler 接口中只有一个 invoke 方法。动态代理的优势在于能够很方便的对代理类中方法进行集中处理,而不用修改每个被代理的方法。因为所有被代理的方法(真正执行的方法)都是通过在 InvocationHandler 中的 invoke 方法调用的。所以只需要对 invoke 方法进行集中处理。invoke 方法只有三个参数public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;一键获取完整项目代码proxy:代理对象method: 代理对象调用的方法args:调用方法中的参数。动态代理的整个代理过程不像静态代理那样一目了然,清晰易懂,因为在动态代理的过程中,没有看到代理类的真正代理过程,也不明白其具体操作,所以要分析动态代理的实现原理,必须借助源码。那么问题来了,首先第一步应该从哪分析?如果不知道如何分析的话,干脆就使用倒推法,从后往前找,直接先从 _Proxy.newProxyInstance_入手,看看是否能略知一二。 Proxy.newInstance 方法分析Proxy 提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。Proxy.newProxyInstance 源码(java.lang.reflect.Proxy)public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { Objects.requireNonNull(h); final Class<?>[] intfs = interfaces.clone(); final SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkProxyAccess(Reflection.getCallerClass(), loader, intfs); } Class<?> cl = getProxyClass0(loader, intfs); try { if (sm != null) { checkNewProxyPermission(Reflection.getCallerClass(), cl); } final Constructor<?> cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; if (!Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { cons.setAccessible(true); return null; } }); } return cons.newInstance(new Object[]{h}); } catch (Exception e) { ...}一键获取完整项目代码乍一看起来有点麻烦,其实源码都是这样,看起来非常复杂,但是慢慢分析、厘清条理过后就好,最重要的是分析源码不能着急。上面这个 Proxy.newProxyInstsance 其实就做了下面几件事,这里画了一个流程图作为参考。从上图中也可以看出,newProxyInstsance 方法最重要的几个环节就是获得代理类、获得构造器,然后构造新实例。对反射有一些了解的同学,应该会知道获得构造器和构造新实例是怎么回事。所以重点就放在了获得代理类,这是最关键的一步,对应源码中的 _Class<?> cl = getProxyClass0(loader, intfs);_ 进入这个方法一探究竟private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) { if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); } return proxyClassCache.get(loader, interfaces);}一键获取完整项目代码这个方法比较简单,首先会直接判断接口长度是否大于 65535(刚开始看到这里是有点不明白的,这个判断是要判断什么?interfaces 这不是一个 class 类型吗,从 length 点进去也看不到这个属性,细看一下才明白,这居然是可变参数,Class … 中的 … 就是可变参数,所以这个判断应该是判断接口数量是否大于 65535。)然后会直接从 proxyClassCache 中根据 loader 和 interfaces 获取代理对象实例。如果能够根据 loader 和 interfaces 找到代理对象,将会返回缓存中的对象副本;否则,它将通过 ProxyClassFactory 创建代理类。proxyClassCache.get 就是一系列从缓存中的查询操作,注意这里的 proxyClassCache 其实是一个 WeakCache,WeakCahe 也是位于 java.lang.reflect 包下的一个缓存映射 map,它的主要特点是一个弱引用的 map,但是它内部有一个 SubKey ,这个子键却是强引用的。这里不用去追究这个 proxyClassCache 是如何进行缓存的,只需要知道它的缓存时机就可以了:即在类加载的时候进行缓存。如果无法找到代理对象,就会通过 ProxyClassFactory 创建代理,ProxyClassFactory 继承于 BiFunctionprivate static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>> {...}一键获取完整项目代码ProxyClassFactory 里面有两个属性一个方法。proxyClassNamePrefix:这个属性表明使用 ProxyClassFactory 创建出来的代理实例的命名是以 “$Proxy” 为前缀的。nextUniqueNumber:这个属性表明 ProxyClassFactory 的后缀是使用 AtomicLong 生成的数字所以代理实例的命名一般是 Proxy1这种。这个 apply 方法是一个根据接口和类加载器进行代理实例创建的工厂方法,下面是这段代码的核心。@Overridepublic Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { ... long num = nextUniqueNumber.getAndIncrement(); String proxyName = proxyPkg + proxyClassNamePrefix + num; byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags); try { return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { throw new IllegalArgumentException(e.toString()); }}一键获取完整项目代码可以看到,代理实例的命名就是上面所描述的那种命名方式,只不过它这里加上了 proxyPkg 包名的路径。然后下面就是生成代理实例的关键代码。ProxyGenerator.generateProxyClass 跟进去是只能看到 .class 文件的,class 文件是虚拟机编译之后的结果,所以要看一下 .java 文件源码。.java 源码位于 OpenJDK中的 sun.misc 包中的 ProxyGenerator 下。此类的 generateProxyClass() 静态方法的核心内容就是去调用 generateClassFile() 实例方法来生成 Class 文件。方法太长了就不贴了,这里就大致解释以下其作用:第一步:收集所有要生成的代理方法,将其包装成 ProxyMethod 对象并注册到 Map 集合中。第二步:收集所有要为 Class 文件生成的字段信息和方法信息。第三步:完成了上面的工作后,开始组装 Class 文件。而 defineClass0 这个方法点进去是 native ,底层是 C/C++ 实现的,于是去看了一下 C/C++ 源码,路径在点开之后的 C/C++ 源码还是挺让人绝望的。不过再回头看一下这个 defineClass0 方法,它实际上就是根据上面生成的 proxyClassFile 字节数组来生成对应的实例罢了,所以不必再深究 C/C++ 对于代理对象的合成过程了。所以总结一下可以看出,JDK 生成了一个叫 $Proxy0 的代理类,这个类文件放在内存中的,在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。所以最开始的 dynamicProxy 方法反编译后的代码就是这样的public final class $Proxy0 extends java.lang.reflect.Proxy implements com.cxuan.dynamic.UserDao { public $Proxy0(java.lang.reflect.InvocationHandler) throws ; Code: 0: aload_0 1: aload_1 2: invokespecial #8 // Method java/lang/reflect/Proxy."<init>":(Ljava/lang/reflect/InvocationHandler;)V 5: return一键获取完整项目代码可以看到代理类继承了 Proxy 类,所以也就决定了 Java 动态代理只能对接口进行代理。 于是,上面这个图应该就可以看懂了。 invoke 方法中第一个参数 proxy 的作用细心的小伙伴们可能都发现了,invoke 方法中第一个 proxy 的作用是啥?代码里面好像 proxy 也没用到,这个参数的意义是啥呢?它运行时的类型是啥啊?为什么不使用 this 代替呢?Stackoverflow 给出了一个回答 https://stackoverflow.com/questions/22930195/understanding-proxy-arguments-of-the-invoke-method-of-java-lang-reflect-invoca什么意思呢?就是说这个 proxy ,它是真正的代理对象,invoke 方法可以返回调用代理对象方法的返回结果,也可以返回对象的真实代理对象,也就是 $Proxy0,这也是它运行时的类型。至于为什么不用 this 来代替 proxy,因为实现了 InvocationHandler 的对象中的 this ,指代的还是 InvocationHandler 接口实现类本身,而不是真实的代理对象————————————————原文链接:https://blog.csdn.net/2402_87298751/article/details/152166279
-
HTTP协议HTTP 理论和实践同样重要。如果我们未来写web开发(写网站)。HTTP就是我们工作中最常用到的东西。什么是HTTP?HTTP是应用层的协议,HTTP现在大规模使用的版本是 HTTP/1.1使用HTTP协议的场景:1.浏览器打开网站(基本上)2.手机APP访问对应的服务器(大概率)报文格式HTTP最重要的就是报文格式了HTTP的报文格式和 TCP/IP/UDP 这些不同,HTTP 的报文格式,要分两个部分来看待请求和响应HTTP协议,是一种 一问一答 结构模型的协议,请求和响应的协议格式,是有所差别的这里也不只有一问一答的模型结构:一问一答(访问网站)多问一答(上传文件,把请求拆分成多份传给服务器)一问多答(下载文件,把响应拆成多份进行下载)多问多答(串流 / 远程桌面,远程桌面就是这边操作了,那边就会有响应)如何查看到 HTTP 请求和响应的格式呢?抓包工具:把网卡上经过的数据,获取到,并显示出来(这个是我们必备的技能,分析和调试程序的重要手段)Fiddler抓包工具:wireshark:功能非常强大,可以抓各种各样的协议,使用起来比较复杂,但是用这个抓 HTTP 是不太方便的Fiddler工具:是专门用来抓HTTP的,使用起来简单,企业中也经常用到在官网下载Fiddler Classic 版本的就可以FIddler的请求和响应,还要下载证书,否则就只抓包http了,不能抓https了安装证书:出现这个点cancel就行4. Fiddler 本质上是一个 代理,可能会和其他的代理软件冲突举个栗子:除了 fiddler 之外,有的程序也是代理(1) 加速器(2) vpn…这些代理程序之间可能会产生冲突如果你的 fiddler 不能抓包了,一定要检查关闭之前的开启的代理软件(也可能是一个浏览器插件),如果还是不行,还可以尝试换一个浏览器,有的时候 edge浏览器 可能会不行,比如出现下面这样的情况postman 是构造请求的软件fiddler 是抓取/显示已有的请求的软件ctrl + a:选中所有的请求,delete删除这里所有的请求,方便后续观察想要观察的httpHTTP 协议是文本格式的协议(协议里面都是字符串)TCP,UDP,IP…都是二进制格式的协议**HTTP 响应也是文本的。直接查看,往往能看到二进制的数据(压缩后的),HTTP 响应经常会被压缩。因为压缩之后,体积更小,传输的时候,节省网络带宽。**但是,压缩和解压缩,是需要消耗额外的 cpu 和 时间 的。其实也没关系,一台服务器,最贵的硬件资源就是网络带宽,这样压缩和解压缩就是用便宜的换贵的解压缩之后,可以看到,响应的数据其实是 html,浏览器上显示的网页,就是html,往往都是浏览器先请求对应的服务器,从服务器这边拿到的 网页 数据(html),每次访问网页,都会下载对应的网页数据,会消耗一定的带宽(不像我们下载APP一样,只需要下载一次) 请求:键值对这里是 标准规定 的,所以不能自己胡编乱造响应:URLURL 是计算机中的非常重要的概念,不仅仅是在 HTTP 中涉及到jdbc设置数据源setUrl(“jdbc:mysql://127.0.0.1:3306/java1?characterEncoding=utf8&useSSL=false”);setUsersetPasswordURL,描述了某个资源在网络上的所属位置。数据库也是一种资源URL的具体信息:1.协议方案名(协议名):例如,https:// ,jdbc:mysql://2.登录信息(认证):这个东西现在几乎不会用到了(尤其是针对用户的产品)3.服务器地址:可以 IP地址,也可以是 域名4.服务器端口号:5.带层次的文件路径:6.查询字符串(query string)关于以上 url 的几个部分(query string),举个例子:对于 query string 来说,如果 value 部分要包含一些特殊符号的话,往往需要进行 urlencode 操作比如:搜索C++这个词后面使用 url 的时候,要记得针对 query string 的内容进行好 urlencode 工作。如果不处理好,有些浏览器就可能会解析失败,导致请求无法正常工作中文汉字也需要转义,如果不转义的话,汉字也出现特殊的符号的话,不就bug了7.片段标识符:比如说这个网址就有 [vue技术文档],片段标识符实现页面内部的跳转(https://cn.vuejs.org/guide/introduction.html#the-progressive-framework)请求方法方法既有 GET 还有POST,出现最多的还是GET,POST出现的比较少POST请求POST(1) 登录(2) 上传文件习惯用法(不是硬性规定,也可以不遵守的)这个不遵守的话,客户端和服务器都要一起不遵守的,不然会出现问题GET 请求,通常会把要传给服务器的数据,加到 url 的 query string 中。POST 请求,通常把要传给服务器的数据,加到 body 中登录和上传头像的请求 出现蓝色的字体(在刚开始加载网页的时候出现的,后续Fiddler删除这些包,就不会出现了),是获取到网页(得到 html 的响应)对于浏览器缓存的理解:刚才最开始没有抓到这里的返回的请求,是因为命中了浏览器的缓存存在网络缓存的原因:浏览器显示的网页,其实是从服务去这边下载的 html。html 内容可能会比较多,体积可能比较大,通过网络加载,消耗的时间就可能会比较多。浏览器一般都会自己带有缓存,就会把之前加载过的页面,保存在本地硬盘上。下次访问直接读取本地硬盘的数据即可。缓存可能出现的bug:如果缓存的是之前未修改的网页,缓存之后又修改了网页,就可能会出现bug上传图像的body本身是比较长的方法的种类:这些HTTP请求最初的初心是为了表示不同的 语义(不同的含义),现在在实际的使用过程中,初心,已经被遗忘了。HTTP的各种请求,目前来说已经不一定完全遵守自己的初心了,实际上程序员如何使用,更加随意了比如:还可以用GET来上传文件,用POST获取文件,也是可行的还有 POST和 PUT 目前来说,可以理解成 没有任何的区别!!!(任何使用 POST 的场景,换成 PUT 完全可以,反之亦然)GET 和 POST 的区别经典的面试题:GET 和 POST 的区别GET 和 POST 之间的差别,网络上的有些说法,需要大家来注意!(都是错误的说法)GET 请求能传递的数据量有上限,POST 传递的数据量没有上限GET 请求传递的数据不安全,POST 请求传递的数据更安全安全关键在于是否加密了GET 只能给服务器传输 文本数据。POST 可以给服务器传输文本 和 二进制数据(1) GET 也不是不能使用body(body 中是可以直接放二进制数的)(2) GET 也可以把 二进制的数据进行 base64 转码(转码成字符串的文本文件),放到 url 的query string 中,之后再解码,不也是二进制的数据吗下面说法不够准确,但是也不是完全错的1.GET 请求是幂等的。POST 请求不是幂等的。幂等:数学概念,输入相同的内容,输出是稳定的。举个栗子:吃进去的是草,挤出来的是奶。如果任何时候吃草,挤出来的都是奶,就是幂等的。如果吃草之后,不同时候挤出来的东西不一样,就不是幂等的。GET 和 POST 具体是否是幂等的,取决于代码的实现。GET 是否幂等,也不绝对。只不过 RFC 标准文档上建议 GET 请求实现成幂等的。再举个例子:不同的时间,广告的顺序都可能会不同 Header有哪些常见的Header,理解Header的含义HOSTHOST:www.sogou.comHOST后这个信息在 url 中也是存在的比如,在使用代理的情况下,Host 的内容是可能和 url 中的内容不同的Content-Length:body 中数据的长度Content-Type:body 中数据的格式注意:请求中有 body,才会有这两个属性。通常情况下 GET 请求没有 body,POST 请求有 bodybody 中的格式,可以选择的方式是非常多的body中数据的格式:请求:1.json2.form 表单的格式:相当于是把 GET 的query string 给搬到 body 中。(后面写个代码来构造一个)上传图片:3.form-data 的格式:上传文件的时候,会涉及到(也不一定就是 form-data,也可能是 form 表单)响应:后续给服务器提交请求,不同的 Content-Type(body),服务器处理数据的逻辑是不同的。服务器返回数据给浏览器,也需要设置合适的 Content-Type,浏览器也会根据不同的Content-Type 做出不同的处理User-Agent(简称UA)UA是用来区分不同的设备的,上古时期的UA是用来区分浏览器的兼容问题的,可以让旧的浏览器支持文字,让新的浏览器支持图片,这样也就区分开了现在 UA 使用来区分是PC端还是 移动端的Referer:描述了当前页面是从哪个页面跳转而来的如果是直接在地址栏输入 url(或者是点击收藏夹中的按钮)都是没有 RefererReferer的用途:这个也可以用来算钱的搜索引擎,每点击一次,它都会赚钱通过域名来区分是百度投放的广告,还是搜狗投放的广告 5. CookieCookie可以认为是浏览器在本地存储数据的一种机制对于安全性的考虑,所以引入了Cookie,存储简单的字符串也是够用的Cookie是怎么进行存储的(重点)浏览器中的 CookieCookie的作用:Servlet / Spring 中会结合代码更深入地理解Cookie响应状态码状态码响应 状态码表示了这次请求对应的响应,是啥样的状态(成功,失败,其他的情况,对应的原因是什么)这里的状态码,种类非常多。咱们不需要全都记住成功2xx 都表示成功,200是最常见的重定向3xx 表示重定向,(可以理解为爱情转移)请求中访问的是 A 这样的地址。响应返回了一个重定向报文,告诉你应该要访问B地址举个例子:比如你请教一个老师,老师说这边还没有空,叫你去找另一个老师,如果还不行的话,再来找我301和302301 Moved Permanently是永久重定向,是有缓存的,比如你地址就在新的网站了,以后就不要访问旧的网站了302 Move tmporarily 是临时重定向,是没有缓存的,指不定后面又要更改,比如说你地址在新网站了,但是可能后面又会改回旧的网站重定向的响应报文中,会带有Location字段,描述出当前要跳转到哪个新的地址(后面会在代码中具体演示)请求错误客户端,也就是浏览器(客户端)会构造一个http请求,不符合服务器的一些要求404 Not Found请求中访问的资源,在服务器上不存在比如:HTTP/1.1 404 Not Found404这个状态码表示的是自愿不存在,同时在 body 中也是可以返回一个指定的错误页面的,很多网站会把这个错误页面做的很丰富多彩比如:bilibili403 Forbidden表示访问的资源没有权限服务器错误5xx 表示服务器出错了看到这个说明服务器挂了比较容易出现的是500,后面我们自己写服务器的时候,还是比较容易写出500这样的问题的(一般就是你的代码有bug)一个经典的面试题:说一下,HTTP的状态码有哪些常见的通过不同的数字来表示状态,这种做法其实是非常常见的(在C语言中也有,比如 errno,表示打开文件失败的原因, strerror 把errno翻译成字符串)可能我们面试中也会考察C语言的 如何让客户端构造一个HTTP请求如何让服务器处理一个HTTP请求,这是非常重要的内容(Servlet/Spring中都会涉及到这一部分)浏览器:直接在浏览器地址栏输入url,此时构造了一个GET请求html中,一些特殊的html标签,可能会触发GET请求,比如像 img,a,link,script…通过form表单来触发GET/POST请求(我们需要了解一下),form本质也是一个HTML标签写一个简单的html代码,来编写逻辑(解释form的逻辑)form表达如何编写?使用form标签设置提交按钮:构造GET请求:构造POST请求:4. ajax的方式1.引入jquery库(第三方库,是需要额外下载引入的)前端引入第三方库非常容易的,只要代码中写一个库的地址即可jquery cdn中有一些资源和库2.编写代码回调函数:js中定义变量:ajax的get请求写法:ajax的post请求写法:构造请求还有更简单,更方便的方式,比如使用第三方工具,可以实现这里的效果。form能够构造get和post请求,ajax也能构造get和post请求。上面的知道这些就可以了,等学了前端来看才能够懂!!!一种更简单的构造HTTP请求的方式直接通过第三方工具,图形化的界面来构造。我们这里使用postman先创建一个Workspaces,然后创建标签页请求的设定,最后发送请求postman 也是我们常用的构造请求的方式,一般是用于测试阶段不会写ajax也没事,postman都会给你自动生成代码,就在右上角的code按钮HTTPS引入HTTPS的原因:使用HTTPS进行加密,也是为了防止运营商劫持,还有比如在商场你连商场的wifi,也可能会被黑客截获数据,黑客把wifi伪装成商场的wifi一样的名字————————————————原文链接:https://blog.csdn.net/2301_79722622/article/details/151930571
-
1.什么是闭包:闭包是JavaScript中最强大且独特的特性之一,它是函数与其词法环境的组合。闭包使得函数能够访问其外部作用域的变量,即使外部函数已经执行完毕。这种机制让JavaScript具备了许多其他语言需要复杂语法才能实现的功能。// 最简单的闭包示例function outer() { const message = "Hello from outer!"; // 外部变量 function inner() { console.log(message); // inner函数"记住"了message } return inner; // 返回inner函数}const myClosure = outer(); myClosure(); // "Hello from outer!"一键获取完整项目代码javascript2. 核心概念与前置知识:2.1 执行上下文 (Execution Context):执行上下文是 JavaScript 代码执行时的环境,每当代码执行时都会创建对应的执行上下文。执行上下文栈 (Call Stack)全局执行上下文Global EC函数执行上下文 1Function EC 1函数执行上下文 2Function EC 2函数执行上下文 3Function EC 3执行上下文的组成部分:// 伪代码:执行上下文的内部结构ExecutionContext = { // 1. 词法环境 (用于let/const和函数声明) LexicalEnvironment: { EnvironmentRecord: {}, // 环境记录 outer: null // 外部环境引用 }, // 2. 变量环境 (用于var声明) VariableEnvironment: { EnvironmentRecord: {}, outer: null }, // 3. this绑定 ThisBinding: undefined}一键获取完整项目代码javascript2.2 词法环境 (Lexical Environment):词法环境是存储标识符-变量映射的结构,由环境记录和外部环境引用组成。环境记录类型词法环境结构声明式环境记录Declarative对象环境记录Object全局环境记录Global词法环境Lexical Environment环境记录Environment Record外部环境引用Outer Reference词法环境的创建过程:function createLexicalEnvironment() { // 示例:词法环境的创建 function outer(x) { let a = 10; const b = 20; function inner(y) { let c = 30; console.log(a + b + c + x + y); // 访问多个作用域的变量 } return inner; } return outer;}// 执行过程中的词法环境变化/*1. 全局词法环境:{ EnvironmentRecord: { createLexicalEnvironment: <function>, outer: <function> }, outer: null}2. outer函数的词法环境:{ EnvironmentRecord: { x: 参数值, a: 10, b: 20, inner: <function> }, outer: <全局词法环境的引用>}3. inner函数的词法环境:{ EnvironmentRecord: { y: 参数值, c: 30 }, outer: <outer函数词法环境的引用>}*/一键获取完整项目代码javascript2.3 作用域链 (Scope Chain)作用域链是通过词法环境的外部引用形成的链条,用于标识符解析。当前词法环境父级词法环境全局词法环境null作用域链查找算法// 作用域链查找示例function demonstrateScopeChain() { const globalVar = "全局变量"; function level1() { const level1Var = "第一层变量"; function level2() { const level2Var = "第二层变量"; function level3() { const level3Var = "第三层变量"; // 变量查找顺序演示 console.log(level3Var); // 1. 在当前环境找到 console.log(level2Var); // 2. 向上一层查找 console.log(level1Var); // 3. 继续向上查找 console.log(globalVar); // 4. 查找到全局环境 // console.log(nonExistent); // 5. 找不到则报错 } return level3; } return level2; } return level1;}// 调用过程中的作用域链const fn = demonstrateScopeChain()()();fn(); // 执行时会沿着作用域链查找变量一键获取完整项目代码javascript3. 代码示例与分析:3.1 经典的闭包示例function makeCounter() { let count = 0; // 外部函数的局部变量 return function() { // 返回的内部函数形成闭包 count++; // 访问外部函数的变量 return count; };}const counter = makeCounter(); // 调用外部函数console.log(counter()); // 1 - 调用闭包函数console.log(counter()); // 2 - count 变量被保持console.log(counter()); // 3一键获取完整项目代码javascript3.2 逐行执行过程分析全局执行上下文makeCounter执行上下文闭包函数1. 创建全局执行上下文2. 调用makeCounter(),创建新执行上下文3. 创建count变量,值为04. 创建匿名函数,形成闭包5. 返回函数,makeCounter执行完毕6. makeCounter执行上下文出栈7. 但闭包仍保持对count的引用8. 调用counter()9. 访问并修改count变量10. 返回结果全局执行上下文makeCounter执行上下文闭包函数详细步骤解析:// 步骤 1-2: 全局执行上下文创建,调用makeCounterfunction makeCounter() { // 步骤 3: 在makeCounter的词法环境中创建count变量 let count = 0; // 步骤 4: 创建匿名函数,该函数的[[Environment]]属性 // 指向makeCounter的词法环境,形成闭包 return function() { // 步骤 9: 通过作用域链找到外部的count变量 count++; return count; }; // 步骤 5-6: makeCounter执行完毕,执行上下文出栈 // 但count变量因为被闭包引用而不会被垃圾回收}// 步骤 7: counter变量保存了闭包函数的引用const counter = makeCounter();// 步骤 8-10: 每次调用counter()都会访问保存的count变量console.log(counter()); // 1一键获取完整项目代码javascript3.3 V8 引擎的优化V8 引擎对闭包进行了以下优化:变量提升优化:只保留被闭包实际使用的外部变量内存管理:未被引用的外部变量会被垃圾回收作用域分析:在编译时分析变量使用情况function optimizationExample() { let used = "被闭包使用的变量"; let unused = "未被使用的变量"; // V8会优化掉这个变量 let alsoUnused = "同样未被使用"; return function() { console.log(used); // 只有这个变量会被保留在闭包中 };}一键获取完整项目代码javascript注意:在调试时,由于V8的优化,某些未使用的变量可能在调试器中显示为 “undefined”。4. 经典应用场景与最佳实践:4.1 模块化封装const Calculator = (function() { let result = 0; // 私有变量 return { add: function(x) { result += x; return this; }, multiply: function(x) { result *= x; return this; }, getResult: function() { return result; }, reset: function() { result = 0; return this; } };})();// 使用Calculator.add(5).multiply(2).getResult(); // 10一键获取完整项目代码javascript4.2 事件处理与回调function createButtonHandler(name) { return function(event) { console.log(`按钮 ${name} 被点击了`); // name变量被闭包保存 };}document.getElementById('btn1').onclick = createButtonHandler('按钮1');document.getElementById('btn2').onclick = createButtonHandler('按钮2');一键获取完整项目代码javascript4.3 防抖和节流// 防抖函数function debounce(func, delay) { let timeoutId; // 被闭包保存的变量 return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { func.apply(this, args); }, delay); };}// 节流函数function throttle(func, delay) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; func.apply(this, args); } };}一键获取完整项目代码javascript5. 常见陷阱与解决方案:5.1 循环中的闭包陷阱问题代码:// 经典错误示例for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 输出三次 3 }, 100);}一键获取完整项目代码javascript解决方案:// 方案1:使用IIFE创建新的作用域for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); // 输出 0, 1, 2 }, 100); })(i);}// 方案2:使用let块级作用域for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 输出 0, 1, 2 }, 100);}// 方案3:使用bindfor (var i = 0; i < 3; i++) { setTimeout(function(j) { console.log(j); // 输出 0, 1, 2 }.bind(null, i), 100);}一键获取完整项目代码javascript5.2 内存泄漏风险// 可能导致内存泄漏的代码function createHandler() { const largeData = new Array(1000000).fill('data'); // 大量数据 return function() { // 即使不使用largeData,它也会被闭包保留 console.log('handler called'); };}// 解决方案:显式释放不需要的引用function createHandler() { const largeData = new Array(1000000).fill('data'); const needed = largeData.slice(0, 10); // 只保留需要的部分 return function() { console.log(needed.length); // largeData会被垃圾回收 };}————————————————原文链接:https://blog.csdn.net/2301_81253185/article/details/151932979
-
如果你用过 Java 集合(如List、Map),一定见过List<String>、Map<Integer, String>这样的写法 —— 这就是泛型。泛型是 Java 5 引入的核心特性,它解决了 “容器存储元素类型不明确” 的问题,让代码更安全、更简洁。今天我们就从泛型的作用讲起,深入剖析泛型类、泛型方法、泛型接口的定义与使用,配合直观图解,让你彻底搞懂泛型!一、为什么需要泛型?—— 从两个痛点说起 在没有泛型的 Java 早期版本中,集合(如ArrayList)只能存储Object类型的元素。这会导致两个严重问题:类型不安全和频繁强制转型。痛点 1:类型不安全(运行时错误) 假设我们创建一个ArrayList,本意是存字符串,却不小心存入了整数。编译器不会报错,但运行时调用字符串方法会抛ClassCastException:// Java 5之前的代码(无泛型)List list = new ArrayList();list.add("hello");list.add(123); // 存入整数,编译器不报错 // 取出元素时,假设都是字符串String str = (String) list.get(1); // 运行时报错:Integer cannot be cast to String一键获取完整项目代码java痛点 2:频繁强制转型(代码冗余)即使存入的元素类型一致,取出时也必须手动转型,代码繁琐且易出错:List list = new ArrayList();list.add("apple");list.add("banana"); // 每次取元素都要转型String s1 = (String) list.get(0);String s2 = (String) list.get(1);一键获取完整项目代码java泛型如何解决这些问题? 泛型的核心思想是:在定义类 / 接口 / 方法时,不指定具体类型,而是留出 “类型参数”,在使用时再指定具体类型。用泛型改写上面的例子:// 定义时指定类型参数<String>,表示只能存字符串List<String> list = new ArrayList<>();list.add("hello");// list.add(123); // 编译时直接报错,类型不匹配 // 取出时无需转型,编译器已知是String类型String str = list.get(0);一键获取完整项目代码java泛型的两大核心作用:类型安全:编译时检查元素类型,避免存入错误类型的元素(将运行时错误提前到编译时);避免强制转型:编译器自动推断元素类型,取出时无需手动转型,简化代码。泛型作用图解二、泛型类:让类支持 “类型参数化” 泛型类是指在类定义时声明类型参数,使得类中的字段、方法参数或返回值可以使用这些类型参数。当创建类的实例时,指定具体类型,从而实现 “一个类适配多种数据类型”。1. 泛型类的定义语法public class 类名<类型参数1, 类型参数2, ...> { // 类型参数可以作为字段类型 private 类型参数1 变量名; // 可以作为方法参数或返回值类型 public 类型参数1 方法名(类型参数2 参数) { // ... }}一键获取完整项目代码java类型参数命名规范(约定俗成,增强可读性):T:Type(表示任意类型);E:Element(表示集合中的元素类型);K:Key(表示键类型);V:Value(表示值类型);若有多个参数,可用T1、T2或K、V等组合。2. 泛型类示例:自定义容器类假设我们需要一个 “容器” 类,既能存整数,也能存字符串,还能存自定义对象。用泛型类实现:// 定义泛型类,类型参数为T(表示容器中元素的类型)public class Container<T> { private T item; // 用T作为字段类型 // 构造方法,参数类型为T public Container(T item) { this.item = item; } // 方法返回值类型为T public T getItem() { return item; } // 方法参数类型为T public void setItem(T item) { this.item = item; }}一键获取完整项目代码java使用泛型类时,指定具体类型:// 创建存字符串的容器Container<String> strContainer = new Container<>("hello");String str = strContainer.getItem(); // 无需转型 // 创建存整数的容器Container<Integer> intContainer = new Container<>(123);int num = intContainer.getItem(); // 无需转型 // 创建存自定义对象的容器(如User类)Container<User> userContainer = new Container<>(new User("张三"));User user = userContainer.getItem();一键获取完整项目代码java注意:类型参数不能是基本类型(如int、double),必须用包装类(Integer、Double)。3. 多类型参数的泛型类如果需要多个类型参数(如键值对),可以声明多个类型参数:// 键值对泛型类,K表示键类型,V表示值类型public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } // getter和setter public K getKey() { return key; } public V getValue() { return value; }}一键获取完整项目代码java使用时分别指定键和值的类型:Pair<String, Integer> pair = new Pair<>("age", 20);String key = pair.getKey(); // "age"Integer value = pair.getValue(); // 20一键获取完整项目代码java泛型类结构图解三、泛型方法:单个方法的 “类型参数化” 泛型方法是指在方法声明时单独声明类型参数的方法,它可以定义在泛型类中,也可以定义在普通类中。泛型方法的核心优势是:方法的类型参数独立于类的类型参数,灵活性更高。1. 泛型方法的定义语法// 修饰符 <类型参数> 返回值类型 方法名(参数列表) { ... }public <T> T 方法名(T 参数) { // ...}一键获取完整项目代码java关键:泛型方法必须在返回值前声明类型参数(如<T>),这是区分泛型方法与普通方法的标志。2. 泛型方法示例:通用打印方法实现一个方法,能打印任意类型的数组元素:public class GenericMethodDemo { // 泛型方法:打印任意类型的数组 public static <T> void printArray(T[] array) { for (T element : array) { System.out.print(element + " "); } System.out.println(); } public static void main(String[] args) { // 打印字符串数组 String[] strArray = {"a", "b", "c"}; printArray(strArray); // 输出:a b c // 打印整数数组 Integer[] intArray = {1, 2, 3}; printArray(intArray); // 输出:1 2 3 // 打印自定义对象数组(如User) User[] userArray = {new User("张三"), new User("李四")}; printArray(userArray); // 输出:User{name='张三'} User{name='李四'} }}一键获取完整项目代码java 为什么不用泛型类?如果用泛型类,每次打印不同类型的数组都要创建不同的类实例,而泛型方法可以直接通过静态方法调用,更简洁。3. 泛型方法与泛型类的区别对比项 泛型类 泛型方法类型参数声明 在类名后(如class A<T>) 在方法返回值前(如<T> void f())作用范围 整个类 仅当前方法灵活性 依赖类的实例化类型 调用时可独立指定类型泛型方法调用图解四、泛型接口:让接口支持 “类型参数化” 泛型接口与泛型类类似,在接口定义时声明类型参数,实现类可以指定具体类型或继续保留类型参数。1. 泛型接口的定义语法public interface 接口名<类型参数> { // 类型参数可以作为方法参数或返回值 类型参数 方法名(); void 方法名(类型参数 参数);}一键获取完整项目代码java2. 泛型接口示例:自定义比较器接口JDK 中的Comparable接口就是典型的泛型接口,我们模仿它定义一个简单的泛型接口:// 泛型接口:支持比较任意类型的对象public interface MyComparable<T> { // 比较当前对象与另一个对象,返回正数/负数/0表示大于/小于/等于 int compareTo(T other);}一键获取完整项目代码java实现方式 1:实现类指定具体类型// 让User类实现MyComparable,指定T为User(比较两个User对象)public class User implements MyComparable<User> { private String name; private int age; // 实现compareTo方法,按年龄比较 @Override public int compareTo(User other) { return this.age - other.age; } // 构造器和getter省略}一键获取完整项目代码java使用时:User u1 = new User("张三", 20);User u2 = new User("李四", 25);System.out.println(u1.compareTo(u2)); // -5(u1年龄小于u2)一键获取完整项目代码java实现方式 2:实现类继续保留泛型参数// 实现类不指定具体类型,继续使用泛型Tpublic class PairComparator<T> implements MyComparable<Pair<T, T>> { @Override public int compareTo(Pair<T, T> other) { // 假设Pair的比较逻辑(此处简化) return 0; }}一键获取完整项目代码java3. JDK 中的泛型接口:Comparable与ComparatorComparable<T>:类自身实现,定义 “自然排序”(如String按字典序排序);Comparator<T>:外部定义排序规则,更灵活(如按自定义规则排序)。这两个接口广泛用于Collections.sort()等方法中,是泛型接口的经典应用。泛型接口实现图解五、泛型的底层:类型擦除(简单了解) Java 泛型是 “编译时特性”,在运行时会擦除类型参数信息(称为 “类型擦除”)。也就是说,JVM 在运行时看不到List<String>和List<Integer>的区别,它们都会被擦除为List。类型擦除的规则:若类型参数有上限(如<T extends Number>),擦除为上限类型(Number);若没有上限,擦除为Object。例如:// 编译前List<String> list = new ArrayList<>();list.add("a"); // 编译后(类型擦除)List list = new ArrayList();list.add("a"); // 隐含String类型检查String s = (String) list.get(0); // 编译器自动添加转型一键获取完整项目代码java 为什么需要了解类型擦除?避免踩坑:例如不能用instanceof判断泛型类型(因为运行时已擦除):// 错误:编译不通过(无法判断List的泛型类型)if (list instanceof List<String>) { ... }一键获取完整项目代码java六、总结泛型是 Java 中简化代码、提升安全性的核心特性,核心要点:泛型的作用:类型安全:编译时检查元素类型,避免运行时类型转换错误;避免转型:编译器自动推断类型,减少手动转型代码。泛型类:在类定义时声明类型参数(如class Container<T>),实例化时指定具体类型,适用于类整体需要适配多种类型的场景。泛型方法:在方法返回值前声明类型参数(如<T> void print(T t)),可独立于类的泛型使用,灵活性更高。泛型接口:类似泛型类,实现类可指定具体类型或保留泛型(如Comparable<T>)。 掌握泛型是理解 Java 集合框架、实现通用组件的基础。实际开发中,合理使用泛型能让代码更简洁、更安全、更易维护。下一篇我们将深入泛型的高级特性(通配符、上限下限等),敬请期待!————————————————原文链接:https://blog.csdn.net/qq_40303030/article/details/152209932
-
1,JUC(java.util.concurrent)的常见类1)Callable 接口我们之前学过Runnable接口,它是一个任务,我们可以在创建线程的时候把任务丢给线程使用匿名内部类等方法来完成创建对象,现在我们有了一个新的方法来创建任务,并且执行这个任务,就是我们的Callable接口,Runnable的run方法是没有返回值的,但是Callable提供了返回值,支持泛型,我们就能获取到我们想要的参数,我们来看看是怎么用的; Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { return null; } };一键获取完整项目代码java我们使用匿名内部类的方法创建一个Callable对象,并且重写call方法,就相当与重写Runnable的run方法, 我们是不能把这个对象直接放到线程的构造方法中的,因为Thread没有提供传入Callable的版本,我们要使用另一个类FutureTask来拿到结果,在把创建的futureTask对象放到线程创建时的构造方法中去; FutureTask<Integer> task = new FutureTask<>(callable); Thread t1 = new Thread(task); t1.start(); System.out.println(task.get());一键获取完整项目代码java这里的task.get方法会阻塞main线程结束,直到t1线程正确计算出结果; 2)ReentrantLock这个是上古时期的锁,现在有更智能,更好的替代synchronized,那我们还学它干嘛呢,它还活着就一定是有原因的,1,synchronized是关键字,是由JVM内部通过C++实现的,而ReentrantLock是一个类;2,synchronized是通过进出代码块来实现的,ReentrantLock需要Lock和UnLock方法来辅助; ReentrantLock reentrantLock = new ReentrantLock(); reentrantLock.lock(); a++; reentrantLock.unlock();一键获取完整项目代码java3,ReentrantLock除了提供Lock和unLock之外还提供了一个不会造成阻塞的tryLock()它会根据是否加锁成功返回true或者false;4,synchronized是非公平锁,而ReentrantLock是默认是非公平锁,但是也提供了公平锁的实现; 5,ReentrantLock的等待通知机制是Condition类,比synchronized的wait和notify功能更强3)线程池博主博主,咱们之前不是讲过线程池了吗,怎么又来一遍呀,确实嗷,上次虽然给大家详细讲过了,但是我们还没有用呀,哈哈哈哈哈,我直接上代码;我们先来简单的版的;public class Demo2 { public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(1111); } } }; ExecutorService executorService = Executors.newFixedThreadPool(1);//创建固定数目的线程池 //ExecutorService executorService1 = Executors.newSingleThreadExecutor(); 创建单线程池 //ExecutorService executorService2 = Executors.newCachedThreadPool(); 创建线程动态增长的线程池 //ScheduledExecutorService service = Executors.newScheduledThreadPool(1); 创建定时线程池 //executorService.submit(runnable); executorService.shutdown(); }}一键获取完整项目代码java我们还可以通过execute来提交任务executorService.execute(runnable);一键获取完整项目代码java都是官方给提供的现成的,我们这会来自己创建;ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20, 10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());一键获取完整项目代码java这就是我们自己创建的线程池,我们要把所有的参数都填上;1. 任务队列类型队列类型 特点ArrayBlockingQueue 有界队列,需指定容量LinkedBlockingQueue 无界队列(默认使用,可能 OOM)SynchronousQueue 不存储任务,直接提交给线程PriorityBlockingQueue 支持优先级排序2. 拒绝策略策略类 行为AbortPolicy(默认) 抛出 RejectedExecutionExceptionCallerRunsPolicy 由提交任务的线程直接执行任务DiscardPolicy 静默丢弃新任务DiscardOldestPolicy 丢弃队列中最旧的任务,然后重试提交工厂模式那个也是官方给提供的现成的哈哈哈哈,太懒了我; 4)信号量 Semaphore一种计数器,可以表示可用资源的个数;信号量的P操作,申请资源,计数器加一;信号量的V操作,释放资源,计数器减一;如果此时计数器为零,再尝试申请资源就会进入阻塞等待;有一点点像锁;我们使用acquire来申请资源,使用release来释放资源,我们来试试写代码;public class Demo4 { public static void main(String[] args) throws InterruptedException { Semaphore semaphore = new Semaphore(3);//3个可用资源 Runnable runnable = new Runnable() { @Override public void run() { try { System.out.println("申请资源"); semaphore.acquire(); System.out.println("获取到了资源"); Thread.sleep(10000); semaphore.release(); System.out.println("释放资源"); } catch (InterruptedException e) { throw new RuntimeException(e); } } }; Thread t1= new Thread(runnable); Thread t2= new Thread(runnable); Thread t3= new Thread(runnable); t1.start(); t2.start(); t3.start(); Thread t4= new Thread(runnable); t4.start(); t4.join(); }}一键获取完整项目代码java我通过运行这个代码可以看到t1, t2,t3线程获取申请资源之后不释放,t4申请资源就要等着,直到10s之后,t4线程才开始工作;5)CountDownLatch也类似一个计数器,我们传入构造方法的参数就是需要完成的任务个数,完成一个任务就调用countDown()方法,主线程中使用await方法,等待所有任务完成主线程才结束;public class Demo5 { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(10); Runnable runnable = new Runnable() { @Override public void run() { System.out.println(111); countDownLatch.countDown(); } }; for (int i = 0; i < 10; i++) { Thread t = new Thread(runnable); t.start(); } countDownLatch.await(); }}一键获取完整项目代码java2,线程安全的集合类我们之前学习的数据结构大部分是不安全的,我们还想使用之前的数据结构就要做相应的修改;1)多线程环境使用ArrayList1,使用ArrayList的第一种方式就是自己加锁,使用synchronized或者ReentrantLock,来对容易引发线程安全的地方来加以限制;2,就是套壳collections.synchronized(new ArrayList)对于public的方法都加上synchronized;3,使用CopyOnWriteArrayList这个方法是不去加锁的,我们知道,读操作是不影响线程安全的,那么我们在使用ArrayList的时候,我们修改了,我们就再复制一个数组,我们读取的时候只能读到旧的数据或者是已经修改完成的数据,不存在读取修改一半的情况,但是,如果我们的数据很大很大呢,难道我们要一下复制所有的元素吗,是的,就是这么难受,并且多个线程修改数据的时候也可能会发生问题,那我们干嘛要用它,这个是存在特定的使用场景的,服务器如果修改配置了的话是需要重新启动的,我们玩游戏的时候,如果我们要修改设置,比如打开声音,或者设置按键等,难道我们还要关掉游戏吗,我们这时候就是我们给出指令,根据新的设置,服务器就会创建新的哈希数组,来代替旧的数组,完成配置文件的修改,而不是服务器的重启;2)多线程环境下使用队列1. ArrayBlockingQueue 基于数组实现的阻塞队列2. LinkedBlockingQueue 基于链表实现的阻塞队列3. PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列4. TransferQueue 最多只包含⼀个元素的阻塞队列3)多线程环境下使用哈希表哈希表,查找时间复杂度O(1)啊,这必选得拿到多线程中,我们之前讲过,Hashtable是线程安全的,但它只是对HashMap的所有方法加锁,效率肯定是不高的,我们有一个完美的替代品就是ConcurrentHashMap;ConcurrentHashMap是对桶级别加锁,和HashTable不一样,更高效; 大家还记不记得的哈希表是咋样的了, 我们要解决哈希冲突,我们通常是在每个下标中构建链表或者是红黑树;如果链表太长了,我们还涉及到扩容操作; ConcurrentHashMap是对每个下标都加锁的,锁对象就使用表头,当两个线程在不同的下标是,就不会发生锁竞争,当两个线程修改同一个下标时,就存在线程安全性问题了,因为有表头锁的存在就会发生竞争,成功避免了线程安全问题;另外,记录的元素个数size怎么办呢,两个线程同时增加数据,size也会有线程安全问题,还有加锁吗,忘了我们的AtomicIngter了吗,这个原子类也是很好用的呀,大家不要忘了;还有最后一个哈希扩容问题,如果发生扩容就意味着和CopyOnWriteArrayLIst一样了,我们要把原来的数距全部复制过来,那肯定需要很多的时间,所以我们不会一次就把所有元素复制过去,我们会把每次put一些数据的过程中偷偷复制一些数据到新哈希表,就意味着我们把100%的任务分三开,每次执行别的操作都完成一点点的任务,直到扩容完全完毕;————————————————原文链接:https://blog.csdn.net/2301_79083481/article/details/146140421
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签