同事看了想打人！这8个SQL写法让数据库慢到崩溃
坑点1：翻页越往后越卡当你用LIMIT 1000000,10查数据时，数据库就像翻一本100万页的书——明明只要第100万页的10行字，却得从第一页开始数。优化大招：记住上次看到哪儿。比如上次翻页最后一条的创建时间是"2023-01-01 12:00:00"，下页查询直接加条件WHERE create_time > '2023-01-01 12:00:00'，秒出结果。问题案例：-- 传统分页（偏移量越大性能越差） SELECT * FROM logs WHERE log_type='ERROR' ORDER BY create_time LIMIT 1000000, 10; -- 需遍历前100万条记录优化方案：-- 游标分页法（基于最后时间戳） SELECT * FROM logs WHERE log_type='ERROR' AND create_time > '2024-02-17 00:00:00' -- 使用上一页末尾时间 ORDER BY create_time LIMIT 10; -- 执行时间稳定在5ms内坑点2：乱填参数害死索引字段明明是varchar类型，程序却传了数字进来。这就好比用家门钥匙开汽车锁，数据库只能暴力拆锁全表扫描。检查方法：执行SHOW WARNINGS看到类型转换警告，赶紧让程序员改代码对齐类型。问题案例：-- 字段phone定义为VARCHAR(20)，但传入数值参数 SELECT * FROM users WHERE phone = 13800138000; -- 触发类型转换，索引失效 -- 查看执行计划警告 SHOW WARNINGS; > Warning: Cannot use ref access on index 'phone' due to type conversion 优化方案：-- 添加引号保持类型一致 SELECT * FROM users WHERE phone = '13800138000'; -- 正确使用索引坑点3：更新语句慢如蜗牛用UPDATE...WHERE id IN (子查询)更新数据？MySQL会傻乎乎地循环查几千次。救星方案：改成JOIN写法，让数据库一次性关联更新。实测有个案例从7秒降到2毫秒。问题案例：-- 错误写法（嵌套子查询） UPDATE orders o SET status = 'expired' WHERE o.id IN ( SELECT id FROM ( SELECT id FROM orders WHERE create_time < '2023-01-01' LIMIT 1000 ) tmp ); -- 执行时间>10秒优化方案：-- 改用JOIN重写 UPDATE orders o JOIN ( SELECT id FROM orders WHERE create_time < '2023-01-01' LIMIT 1000 ) tmp ON o.id = tmp.id SET status = 'expired'; -- 执行时间<100ms坑点4：混搭排序逼疯数据库 既要按评分升序，又要按时间降序，索引直接罢工。破解术：拆成两次查询再合并。比如先查未回复的订单按时间排序，再查已回复的，最后拼起来。问题案例：-- 混合排序导致全表扫描 SELECT * FROM product_reviews ORDER BY is_approved ASC, review_time DESC LIMIT 20; -- 无法使用(is_approved,review_time)索引优化方案：-- 拆分查询+UNION ALL (SELECT * FROM product_reviews WHERE is_approved=0 ORDER BY review_time DESC LIMIT 20) UNION ALL (SELECT * FROM product_reviews WHERE is_approved=1 ORDER BY review_time DESC LIMIT 20) ORDER BY is_approved ASC, review_time DESC LIMIT 20; -- 执行效率提升80倍坑点5：乱用SELECT * 拖垮网络动不动就SELECT *，把不要的字段也查出来。特别是text大字段，传输速度直接腰斩。保命原则：需要什么字段就写什么，别让数据库做快递小哥送垃圾。问题案例：-- 低效写法 SELECT * FROM employees e WHERE EXISTS ( SELECT 1 FROM salaries s WHERE s.emp_id = e.id AND s.year = 2023 ); -- 嵌套循环执行优化方案：-- 改用JOIN优化 SELECT e.* FROM employees e INNER JOIN salaries s ON e.id = s.emp_id AND s.year = 2023; -- 利用索引快速定位坑点6：模糊搜索堵死CPULIKE '%关键字%'这种查询，数据库只能玩命扫描。应急方案：上Elasticsearch做专业搜索，或者业务上限制必须带前缀查询。问题案例：-- 用户搜索包含"error"的日志（无前缀模糊匹配） SELECT * FROM server_logs WHERE log_message LIKE '%error%'; -- 执行计划：全表扫描，耗时12秒（数据量500万行）优化方案：-- 方案1：强制前缀匹配（利用索引） SELECT * FROM server_logs WHERE log_message LIKE 'error%'; -- 使用前缀索引 -- 方案2：集成Elasticsearch（专业分词检索） PUT /logs/_search { "query": { "match": { "message": "error" } } }原理说明： B+树索引无法逆向检索，LIKE '%xx'会导致索引失效。前缀匹配可触发索引范围扫描，而全文检索场景建议使用专用引擎（如ES）的分词能力。坑点7：乱堆OR条件让索引哭晕WHERE a=1 OR b=2这种写法，数据库往往直接放弃治疗全表扫描。改造技巧：拆成两个查询用UNION合并，速度提升50倍。问题案例：-- 查询订单状态为"paid"或物流单号为"SF123"的订单 SELECT * FROM orders WHERE status = 'paid' OR tracking_no = 'SF123'; -- 执行计划：全表扫描，耗时8秒（数据量200万行）优化方案：-- 拆分为UNION查询（分别利用不同索引） (SELECT * FROM orders WHERE status = 'paid') UNION (SELECT * FROM orders WHERE tracking_no = 'SF123'); -- 执行时间：0.2秒（提升40倍）原理说明： OR条件导致优化器无法选择有效索引。拆分为UNION后，可分别利用status和tracking_no的单列索引，再合并结果集。坑点8：子查询嵌套套到死在WHERE里疯狂嵌套子查询，MySQL会被逼成单线程工人。优化真经：多用临时表或JOIN代替，给数据库留条活路。问题案例：-- 查询有评论的商品（嵌套子查询） SELECT * FROM products WHERE id IN ( SELECT product_id FROM comments WHERE create_time > '2024-01-01' ); -- 执行计划：DEPENDENT SUBQUERY，耗时15秒 优化方案：-- 方案1：改用JOIN关联（触发索引嵌套循环） SELECT p.* FROM products p JOIN comments c ON p.id = c.product_id WHERE c.create_time > '2024-01-01'; -- 方案2：临时表预存子查询结果 CREATE TEMPORARY TABLE tmp_products AS SELECT product_id FROM comments WHERE create_time > '2024-01-01'; SELECT p.* FROM products p JOIN tmp_products t ON p.id = t.product_id; -- 执行时间均降至0.5秒内 原理说明： 嵌套子查询会导致循环执行，而JOIN或临时表可将多次查询优化为单次数据关联。通过执行计划分析，JOIN写法通常能触发更优的Index Nested-Loop Join算法。看完赶紧检查下自己写的SQL，别让同事半夜打电话骂人！遇到过更奇葩SQL坑货的，欢迎留言区吐槽才哥最近开发了一个针对Java开发人员的面试刷题小程序，计划准备面试的伙伴们可以去试试，手机在线刷题更方便
Java泛型的功能主要是在编译阶段进行类型安全的检查，让开发者能够编写出更具通用性和灵活性的代码，有效避免在程序运行过程中出现类型转换的错误。泛型的作用：确保类型安全：泛型能够在编译时进行严格的类型检查，确保在操作集合或其他泛型类时，不会出现类型不一致的情况，从而降低运行时出现ClassCastException异常的风险。提升代码复用性：借助泛型，代码可以适用于多种不同的数据类型，减少了代码的重复编写，使得代码的可读性和可维护性得到提升。避免显式类型转换：通过在编译时明确指定类型参数，泛型省去了在运行时进行显式类型转换的步骤，简化了代码的编写。示例代码：List<String> stringList = new ArrayList<>(); stringList.add("Hello"); String message = stringList.get(0); // 无需进行类型转换泛型的实际应用场景集合框架中的应用：Java的集合框架大量运用了泛型。像List<T>、Set<T>以及Map<K, V>等接口，能够针对不同的数据类型实现统一的操作方式。泛型方法的定义：不仅可以定义泛型类，还可以定义泛型方法，这样可以使方法具备处理多种不同数据类型的能力。例如：public static <T> void printElements(T[] elements) { for (T element : elements) { System.out.println(element); } }为何需要泛型的通俗解释在Java 5版本之前，泛型是不存在的，那时的代码也能正常运行，那么为何要引入泛型呢，它能为我们带来哪些好处呢？先来看下面这段代码：List list = new ArrayList(); list.add("yes"); // 添加字符串 list.add(233); // 添加整数在没有泛型的时期，集合中添加的数据不会受到任何类型限制，全部被视为Object类型。或许有人会觉得这样很自由，确实，自由度很高，但是代码的约束性越弱，出错的概率就越高，在使用上也会带来诸多不便，比如在获取数据时需要进行强制类型转换。如果一不小心获取了错误的类型，虽然代码能够通过编译，但在运行时却会抛出异常。综上所述，Java引入了泛型机制。泛型的作用就是增加了一层类型约束。有了这层约束，由于已经声明了类型，所以在编译阶段就能识别出不准确的类型元素。这样可以让错误提前被发现，避免在运行时才暴露出来。并且也不需要在代码中显式地进行强制类型转换，从下面的代码可以看出，可以直接获取到String类型的元素。我们再总结一下泛型的优势：提升了代码的可读性，能够一眼看出集合（或其他泛型类）所存储的数据类型能在编译阶段检查类型安全，增强程序的健壮性省去了显式类型转换的麻烦（实际上内部已经完成了类型转换）提高了代码的复用性，定义好的泛型可以让一个方法（或类）适用于所有类型（虽然以前使用Object也可以实现，但相对较为繁琐）为何有人说Java的泛型是伪泛型来看下面这段代码：可以看到，虽然声明的是一个String类型的集合，但通过反射的方式却能够成功地向集合中插入int类型的数据。这表明在运行时，泛型并没有起到任何作用！也就是说，在运行过程中，JVM无法获取到泛型的信息，也不会对其进行任何约束。可以认为Java的泛型仅在编译时有效，而在运行时则不存在泛型，这也是人们常说Java是伪泛型的原因。简而言之，Java的泛型仅在编译阶段发挥作用，而在JVM运行时则不存在泛型的概念。近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣的扫下方的小程序码进行体验。
Java 插件模式SPI学习与理解（Java SPI 、Spring SPI、Dubbo SPI）
SPI 全称为 Service Provider Interface，是一种服务发现机制，其核心在于将接口实现类的全限定名配置在文件中，由服务加载器读取并加载实现类，从而可在运行时为接口动态替换实现类，为程序提供拓展功能。1. 重新阐述 Java SPI 示例定义接口和实现类：首先定义一个名为 Robot 的接口，包含 sayHello 方法。public interface Robot { void sayHello(); } 然后创建两个实现类 OptimusPrime 和 Bumblebee，它们都实现了 Robot 接口并实现了 sayHello 方法。 public class OptimusPrime implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Optimus Prime."); } } public class Bumblebee implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Bumblebee."); } } 配置文件：在 META-INF/services 文件夹下创建名为 org.apache.spi.Robot 的文件（这里假设接口全限定名为 org.apache.spi.Robot），文件内容包含实现类的全限定名： org.apache.spi.OptimusPrime org.apache.spi.Bumblebee 测试代码： public class JavaSPITest { @Test public void sayHello() throws Exception { ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class); System.out.println("Java SPI"); // 两种遍历模式 serviceLoader.forEach(Robot::sayHello); Iterator<Robot> iterator = serviceLoader.iterator(); while (iterator.hasNext()) { Robot robot = iterator.next(); // 可以调用 robot.sayHello() 进行测试 } } } 2. 经典 Java SPI 应用：JDBC DriverManager传统 JDBC 驱动加载：在 JDBC4.0 之前，需要先使用 Class.forName("com.mysql.jdbc.Driver") 加载数据库驱动，然后进行连接操作。 // STEP 1: Register JDBC driver Class.forName("com.mysql.jdbc.Driver"); // STEP 2: Open a connection String url = "jdbc:xxxx://xxxx:xxxx/xxxx"; Connection conn = DriverManager.getConnection(url,username,password); 使用 SPI 后的 JDBC 驱动加载：JDBC4.0 之后利用 Java 的 SPI 扩展机制，无需手动调用 Class.forName 加载驱动，可直接获取连接。DriverManager 类是驱动管理器，在其静态初始化块中调用 loadInitialDrivers 方法，其中涉及 SPI 的使用： static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } 加载驱动的四个步骤：从系统变量获取驱动定义。用 SPI 获取驱动实现类（字符串形式）。ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); 遍历使用 SPI 获取的实现，实例化各实现类。 Iterator<Driver> driversIterator = loadedDrivers.iterator(); while(driversIterator.hasNext()) { driversIterator.next(); } 根据第一步的驱动列表实例化具体实现类。3. Java SPI 机制源码解析ServiceLoader 类的 load 方法：public static ServiceLoader load(Class service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public static ServiceLoader load(Class service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } 该方法根据服务类型和类加载器创建 ServiceLoader 对象。- `ServiceLoader` 构造器：private ServiceLoader(Class svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager()!= null)? AccessController.getContext() : null; reload(); } private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); } 私有构造器创建懒迭代器 LazyIterator 对象，只有调用迭代方法时才执行加载逻辑。迭代器的 hasNext() 方法： public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } 该方法最终调用 hasNextService 方法，会通过加载器获取配置对象并解析 META-INF/services/ 目录下的文件。迭代器的 next() 方法： public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction action = new PrivilegedAction() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } 本质是调用 nextService 方法，通过反射加载类对象，实例化类并缓存到 providers 对象中。4. Java SPI 机制的缺陷无法按需加载，会遍历并实例化所有实现，即使部分实现类不需要或实例化耗时。获取实现类的方式仅能通过 Iterator，不够灵活，不能根据参数获取特定实现类。多线程使用 ServiceLoader 实例时不安全。5. Spring SPI 机制创建接口和实现类：定义 MyTestService 接口： public interface MyTestService { void printMylife(); } 创建实现类 WorkTestService 和 FamilyTestService： public class WorkTestService implements MyTestService { public WorkTestService(){ System.out.println("WorkTestService"); } public void printMylife() { System.out.println("我的工作"); } } public class FamilyTestService implements MyTestService { public FamilyTestService(){ System.out.println("FamilyTestService"); } public void printMylife() { System.out.println("我的家庭"); } } 配置文件：在 META-INF/spring.factories 中配置接口和实现类：com.courage.platform.sms.demo.service.MyTestService = com.courage.platform.sms.demo.service.impl.FamilyTestService,com.courage.platform.sms.demo.service.impl.WorkTestService 测试代码： List<MyTestService> myTestServices = SpringFactoriesLoader.loadFactories( MyTestService.class, Thread.currentThread().getContextClassLoader() ); for (MyTestService testService : myTestServices) { testService.printMylife(); } 与 Java SPI 的区别：Java SPI 一个服务接口对应一个配置文件，Spring SPI 一个 spring.factories 配置文件存放多个接口及实现类，以接口全限定名作为 key，实现类作为 value，多个实现类用逗号分隔。两者都无法获取特定的实现，只能按顺序获取所有实现。6. Dubbo SPI 机制配置文件：配置文件放在 META-INF/dubbo 路径下，以键值对方式配置实现类： optimusPrime = org.apache.spi.OptimusPrime bumblebee = org.apache.spi.Bumblebee 测试代码： public class DubboSPITest { @Test public void sayHello() throws Exception { ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class); Robot optimusPrime = extensionLoader.getExtension("optimusPrime"); optimusPrime.sayHello(); Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); } } 特点：支持按需加载接口实现类，可通过键值对方式指定要加载的实现。相比 Java SPI 增加了 IOC 和 AOP 等特性。近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣的可以点击下方试试。
JWT一文全解，禁止这样使用，被发现立即领盒饭！
在当今的网络开发领域，JSON Web Tokens（JWT）被广泛提及和应用，但对于其在存储 Session 方面的适用性，却存在诸多误解。本文旨在深入探讨为何 JWT 并不适合担当存储 Session 的重任，并揭示其可能引发的安全问题，以期帮助开发者们更全面、准确地理解和运用 JWT。一、术语明确在展开讨论之前，先对一些关键术语进行清晰的界定，这有助于避免后续的混淆和歧义：无状态 JWT（Stateless JWT）：这类 JWT Token 直接将 Session 数据编码于其中，所有与 Session 相关的信息都被封装在 Token 内部。有状态 JWT（Stateful JWT）：此类型的 JWT Token 仅包含对 Session 的引用或者 Session ID，实际的 Session 数据则存放在服务端。Session token（亦称 Session cookie）：这是一种标准的、可被签名的 Session ID 形式，许多 Web 框架（如 Laravel）长期以来都采用这种成熟的 Session 机制，其 Session 数据同样是存储在服务端。需要着重强调的是，本文的目的并非倡导“绝对不使用 JWT”，而是要明确指出 JWT 在作为 Session 机制时存在的弊端和风险。实际上，JWT 在其他特定场景下仍具有其独特的价值和用途，在本文结尾部分，将会简要介绍一些 JWT 的合理应用场景。二、对比对象的澄清常常有人错误地将 Cookies 和 JWT 进行不恰当的比较，这就如同将内存和硬盘简单对比一样，缺乏实际意义。Cookies 本质上是一种存储机制，而 JWT Tokens 则是经过加密和签名处理的令牌，二者并非相互对立的关系，相反，它们既可以独立使用，也能够结合运用。正确的比较方式应该是：Session 与 JWT 相对比，以及 Cookies 与 Local Storage 相对比。在后续的讨论中，本文将主要围绕 JWT Tokens 和 Session 的对比展开，并在必要时涉及 Cookie 和 Local Storage 的比较，这样的对比方式更能准确地反映实际情况和问题本质。三、JWT 被广泛宣扬的优势剖析（一）易于水平扩展？在众多关于 JWT 的宣传中，易于水平扩展被列为其优势之一。从技术层面来看，这一说法在使用无状态 JWT Tokens 时确实存在一定的合理性，但前提是要有相应的应用场景需求。然而，在实际情况中，绝大多数项目几乎并不需要这种高度的横向扩展能力。在常见的系统扩展场景中，存在许多更为简便易行的解决方案。例如：当需要在单台服务器上运行多个后端进程时，只需在该服务器上部署 Redis 服务来存储 Session 即可，这种方式简单高效，能够满足大部分常规业务的需求。对于运行多台服务器的情况，配备一台专门的 Redis 服务器用于集中存储 Session 数据，就能轻松实现 Session 的共享和管理，确保系统的稳定运行。在更为复杂的多集群环境中，通过会话保持（即粘滞会话）技术，也能够很好地应对 Session 的管理问题，而且这些技术在现有的软件系统架构中都得到了良好的支持，通常不需要应用层进行特殊的复杂处理。或许有人会考虑为未来的业务发展预留更多的调整空间，担心以后可能会需要无状态的会话机制。但从实际经验来看，后期对 Session 机制进行替换并非难事，唯一可能产生的影响是在迁移过程中所有用户需要重新登录一次。因此，综合考虑 JWT 可能带来的负面影响，在项目前期并没有必要为了一个不太确定的未来需求而强行采用 JWT 来实现 Session 管理。另外，对于近期有面试跳槽计划的开发者，建议在[Java面试库]()小程序上进行在线刷题，该小程序涵盖了 3000 余道 Java 面试题，几乎囊括了所有主流技术面试题，能够帮助开发者更好地准备面试，提升自身竞争力。（二）易于使用？实际上，JWT 在使用便利性方面并没有优势。使用 JWT 时，无论是在客户端还是服务端，都需要开发者自行处理 Session 的管理逻辑。相比之下，标准的 Session cookies 则具有开箱即用的特点，许多 Web 框架都对其进行了良好的封装和支持，开发者无需过多关注底层的实现细节，即可轻松使用 Session 功能。而 JWT 目前尚未被广泛集成到各种开箱即用的框架中，这就意味着研发人员需要投入更多的精力来处理与 JWT 相关的复杂逻辑，从这个角度来看，JWT 并不比传统的 Session 机制更易于使用。（三）更加灵活？关于 JWT 更加灵活这一说法，目前并没有确凿的证据和清晰的阐述来支撑。在实际应用中，几乎每个主流的 Session 实现都允许开发者直接将数据存储到 Session 中，这种数据存储和管理方式与 JWT 的机制并没有本质上的区别。因此，所谓的“JWT 更加灵活”更多的可能只是一种流行但缺乏实际依据的说法，在实际的开发过程中，开发者很难真切地感受到这种灵活性带来的实际价值和优势。（四）更加安全？有不少人认为 JWT Tokens 更加安全，其主要理由是 JWT 使用了加密技术。然而，这种观点是不准确的。实际上，签名后的 Cookies 相较于未签名的 Cookies 同样具有更高的安全性，而且这并不是 JWT 所独有的特性，许多优秀的 Session 实现都采用了签名后的 Cookies 来保障数据的安全传输和完整性（例如 Laravel 框架）。需要明确的是，单纯的“使用加密技术”并不能自动使某个事物变得更加安全，加密技术必须服务于特定的安全目标，并且要确保其应用方式是针对该目标的有效解决方案。如果加密技术使用不当，反而可能会引入新的安全风险，导致安全性降低。此外，还有一种关于“JWT 更加安全”的常见论述，即“JWT 不使用 Cookies 传输 Tokens”，这种说法是荒谬的。Cookie 仅仅是一条 HTTP 头信息，使用 Cookies 本身并不会导致不安全的情况发生。事实上，Cookies 在设计和实现上受到了特别良好的保护，能够有效地防止恶意的客户端代码对其进行篡改和窃取。如果担心 Session cookies 被他人拦截，正确的做法是采用 TLS（Transport Layer Security）加密协议来保障数据传输的安全。如果不使用 TLS，那么无论是哪种类型的 Session 机制，包括 JWT，都存在被拦截和窃取的风险，因为在未加密的网络环境中，数据的安全性无法得到有效的保障。（五）内置过期时间功能？JWT 的内置过期时间功能实际上并没有太大的实际价值和优势。虽然 JWT 可以在 Token 中设置过期时间，但在服务端同样能够轻松实现对 Session 过期时间的控制，而且许多现有的 Session 实现正是采用这种方式。相比之下，服务端的过期控制更加合理和灵活，因为这样可以让应用程序及时清除那些不再需要的 Session 数据，从而优化系统资源的利用和管理。而如果依赖无状态 JWT Tokens 的过期机制，由于其无法在服务端直接对过期的 Token 进行有效的管理和清理，可能会导致一些潜在的问题，例如 Session 数据的残留和浪费，以及在某些情况下可能会增加系统的复杂度和管理难度。（六）无需询问用户“本网站使用 Cookies”？这种说法是完全错误的。并不存在所谓的“Cookies 法律”专门针对 Cookies 的使用进行限制，实际上，有关 Cookies 的各种法律法规涵盖的范围更广，包括任何类型的“对某项服务的正常运行非严格必须的持久性 ID”，这意味着几乎所有可以想到的 Session 机制都在其监管范围内。具体来说：如果 Session 或 Token 是出于系统功能目的（例如保持用户的登录状态）而使用的，那么无论采用何种方式存储 Session，都无需征得用户的同意，因为这是保障系统正常运行所必需的功能。然而，如果 Session 或 Token 是用于其他目的（例如数据分析、用户追踪等），那么无论选择怎样的 Session 存储机制，都需要明确询问用户是否允许使用，以确保符合相关法律法规的要求和保护用户的隐私权益。（七）防止 CSRF 攻击？JWT 并不能有效地防止 CSRF（Cross-Site Request Forgery）攻击。目前，存储 JWT Tokens 主要有两种常见方式：一种是将其存入 Cookie 中，但这样做仍然容易受到 CSRF 攻击，与传统的 Session 机制类似，仍然需要采取特殊的防护措施来保护 Token 不被恶意利用，例如使用 CSRF Tokens 等技术来增强安全性。另一种方式是将 JWT 存储在其他地方，如 Local Storage 中。虽然这种方式在一定程度上可以避免 CSRF 攻击，但却带来了新的问题。因为使用 Local Storage 需要依赖 JavaScript 来获取和使用 Token，这就要求网站必须支持 JavaScript 才能正常访问，而且这还可能引发另一个完全不同且可能更加严重的漏洞，后续将会对此进行详细说明。需要明确的是，预防 CSRF 攻击的唯一正确方法是使用 CSRF Tokens，而 Session 机制本身与防止 CSRF 攻击并没有直接的关联，不能将防止 CSRF 攻击的期望寄托于 JWT 或其他 Session 机制上，而应该采用专门的 CSRF 防护技术来保障系统的安全。（八）更适用于移动端？这种说法是毫无根据的。目前市面上几乎所有可用的浏览器都对 Cookies 提供了良好的支持，因此也能够很好地支持基于 Cookies 的 Session 机制。同样，主流的移动端开发框架以及严谨的 HTTP 客户端库也都对 Session 机制有着完善的支持，无论是在移动端还是桌面端，Session 机制都能够稳定、高效地运行，不存在 JWT 在移动端具有独特优势的情况，所以这并不是一个选择 JWT 作为 Session 机制的合理理由。（九）适用于阻止 Cookies 的用户？这种情况发生的可能性非常低。通常情况下，用户如果选择阻止 Cookies，往往也会阻止其他任何形式的持久化数据存储，而不仅仅是针对 Cookies 本身。例如，Local Storage 以及其他能够实现 Session 持久化存储的机制（无论是否使用 JWT）都会受到影响。因此，即使采用 JWT 来试图解决用户阻止 Cookies 的问题，也很难达到预期的效果，因为这是一个更为复杂和独立的用户行为和隐私设置问题，与具体的 Session 存储技术关系不大。而且，对于大多数禁用 Cookies 的用户来说，他们往往清楚这会导致网站的身份认证功能无法正常使用，因此在实际使用中，他们通常会针对自己关心的特定站点单独解锁 Cookies 功能，以确保能够正常访问和使用这些网站的服务。所以，从开发者的角度来看，这并不是一个需要通过采用特殊技术（如 JWT）来解决的关键问题，更好的做法是向用户清晰、详细地解释为什么网站需要使用 Cookies 才能正常运行，以提高用户对 Cookies 使用的理解和接受程度。四、JWT 的劣势揭示通过对上述常见误解的分析和澄清，我们已经了解到 JWT 在作为 Session 机制时，所谓的优势大多并不成立。然而，问题远不止于此，使用 JWT 作为 Session 机制还存在许多严重的缺点，其中一些甚至会引发重大的安全问题，以下将对这些劣势进行详细阐述：（一）更费空间JWT Tokens 的实际数据量并不小，尤其是在使用无状态 JWT 时，由于所有的 Session 数据都需要直接编码到 Tokens 内部，这很容易导致 Token 的长度迅速增加，从而可能超过 Cookies 或 URL 的长度限制。在这种情况下，可能会有人考虑将 JWT Tokens 存储到 Local Storage 中，但这又会引发其他一系列问题，后续将会详细说明。（二）更不安全当将 JWT Tokens 存储到 Cookies 中时，其安全性与其他传统的 Session 机制相比并没有本质上的提升或差异。然而，如果将 JWT 存储在其他位置，如 Local Storage，就会引入一个新的安全漏洞。具体来说，Local Storage 是 HTML5 提供的一项功能，它允许浏览器进行 Key/Value 形式的存储操作，对于可能较大的 JWT Tokens 来说，由于 Cookies 的存储容量通常在 4k 左右，对于较大的 Tokens 可能无法满足存储需求，而 Local Storage 似乎成为了一个可行的解决方案。但需要注意的是，Local Storage 并没有像 Cookies 那样提供完善的安全保护措施。与 Cookies 不同，Local Storage 中的数据不会在每次 HTTP 请求时自动发送到服务器端，获取这些数据的唯一途径是通过 JavaScript 代码。这就意味着，如果攻击者能够成功注入恶意的 JavaScript 脚本，并且该脚本能够通过网站的内容安全策略检查，那么攻击者就可以轻易地访问和泄露 Local Storage 中存储的 JWT Tokens，从而导致严重的安全事故。此外，JavaScript 在处理数据时并不关心或追踪数据是否通过安全的 HTTPS 连接进行传输，对于 JavaScript 而言，这些数据仅仅是普通的数据，浏览器会按照常规的数据处理方式来对待它们。这与我们长期以来通过各种技术手段确保 Cookies 安全的努力背道而驰，因为我们花费了大量的精力来防止 Cookies 被恶意接触和窃取，而现在使用 Local Storage 存储 JWT Tokens 却忽视了这些宝贵的经验教训，这无疑是一种安全意识上的倒退。综上所述，无论是否采用 JWT，“使用 Cookies 仍然是保障数据安全传输和存储的重要手段”，不应该轻易地放弃 Cookies 而选择其他可能存在安全隐患的存储方式。（三）无法单独销毁使用 JWT 作为 Session 机制还存在一个严重的安全问题，那就是无法单独销毁 Session。与传统的 Sessions 不同，传统 Sessions 可以在服务端随时根据需要进行单独的销毁操作，例如当检测到用户的异常行为或安全威胁时，能够及时销毁相应的 Session，以防止潜在的风险。然而，无状态 JWT Tokens 由于其设计特点，在 Token 过期之前无法被单独地销毁。例如，在检测到攻击行为时，由于无法立即销毁攻击者的 Session，这就使得攻击者仍然可以利用有效的 Token 继续访问系统，从而给系统带来持续的安全威胁。同样，在用户修改密码等关键操作后，也无法及时销毁旧的 Sessions，这可能会导致旧的 Token 仍然被泄露或滥用，进一步增加了系统的安全风险。针对这一问题，目前几乎没有有效的解决方案，除非重新构建一套复杂的、有状态（Stateful）的基础设施，专门用于明确地检测和拒绝特定的 Session，但这样做不仅成本高昂，而且完全违背了使用无状态 JWT Tokens 的初衷和优势，使得系统的架构变得更加复杂和难以维护。（四）数据延迟与上述安全问题相关联的是，使用无状态 JWT Tokens 还可能存在数据延迟的问题，这也构成了一个潜在的安全隐患。由于无状态 Tokens 中存储的数据是在某个特定时间点生成的，随着时间的推移，这些数据可能会逐渐“过时”，无法反映数据库中最新的信息。例如，用户在个人信息页面修改了某些数据，如个人资料图片的 URL，但由于 JWT Token 中仍然保留着旧的 URL，这就可能导致在某些情况下，系统仍然使用旧的、不准确的信息进行操作，从而出现数据不一致的情况。更为严重的是，如果涉及到权限相关的信息，例如用户原本具有管理员权限，但后来该权限被撤销，然而由于无法销毁之前颁发的具有管理员权限的 Token，系统可能仍然会认为该用户具有管理员权限，这就可能导致严重的安全漏洞，除非关闭整个系统，否则很难完全避免这种情况的发生。（五）实现库缺乏生产环境验证或压根不存在在考虑使用 JWT 作为 Session 机制时，还需要关注其实现库的成熟度和稳定性问题。许多人可能认为上述提到的问题主要是针对无状态 JWT，但实际上，即使使用有状态 Tokens，虽然在功能上与传统的 Session cookies 大致相当，但目前市场上缺乏经过大量生产环境验证的成熟实现库。相比之下，现有的许多传统 Session 实现（例如适用于 Express 的 express-sessionhttps://github.com/expressjs/sessio）已经在生产环境中使用了多年，其安全性和稳定性经过了大量实际项目的检验和改良，开发者可以放心地使用这些成熟的实现，并且能够受益于社区的持续支持和更新。而如果选择使用 JWT 作为 Session cookies 的临时替代品，不仅无法享受到这些成熟实现带来的好处，而且还需要自行不断改进和完善其实现细节，这在过程中很容易引入新的漏洞和问题。即使选择使用第三方的 JWT 实现库，由于这些库尚未在真实世界中得到广泛的应用和验证，仍然存在一定的风险和不确定性。例如，Laravel Passport 虽然采用了类似“有状态 JWT”的方式来存储 OAuth Access Token，但幸运的是，Passport 已经在一些实际应用中得到了验证，并且不完全依赖于 JWT 的原生特性，而是结合了其他成熟的技术和机制来保障安全性和稳定性。五、结论综上所述，无状态 JWT Tokens 在作为 Session 机制时存在诸多无法忽视的问题，如无法单独销毁或更新、可能导致长度限制问题以及潜在的安全隐患等。而有状态 JWT Tokens 虽然在功能上与传统的 Session cookies 相似，但由于缺乏广泛的生产环境验证、经过大量 Review 的成熟实现以及良好的客户端支持，其在实际应用中的可靠性和稳定性仍然存在一定的疑虑。因此，除非是在像 BAT 这样规模庞大、具有特殊技术需求和挑战的公司，对于大多数普通的项目和开发者来说，并没有充分的理由选择使用 JWT 作为 Session 机制。在实际开发中，建议仍然优先选择传统的、经过长期实践验证的 Session 机制，这样能够更好地保障系统的安全性、稳定性和可维护另外，近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣的可以点击下方试试
Java基础面试题—— 包装类型和基本类型的区别是什么？
面试回答重点基本类型 ：Java 中有 8 种基本数据类型（int、long、float、double、char、byte、boolean、short），它们是直接存储数值的变量，位于栈上（局部变量），性能较高，且不支持 null。包装类型 ：每个基本类型都有一个对应的包装类型（Integer、Long、Float、Double、Character、Byte、Boolean、Short）。包装类型是类，存储在堆中，可以用于面向对象编程，并且支持 null。区别总结：（1）性能区别：基本类型：占用内存小，效率高，适合频繁使用的简单操作。包装类型：因为是对象，涉及内存分配和垃圾回收，性能相对较低。（2）比较方式不同：基本类型 ：比较用 ==，直接比较数值。包装类型 ：比较时，== 比较的是对象的内存地址，而 equals() 比较的是对象的值。（3）默认值不同：基本类型：默认值是 0，false 等。包装类型：默认为 null。（4）初始化的方式不同：基本类型：直接赋值。包装类型：需要采用 new 的方式创建。（5）存储方式不同：基本类型：如果是局部变量则保存在栈上面，如果是成员变量则在堆中。包装类型：保存在堆上（成员变量，在不考虑 JIT 优化的栈上分配时，都是随着对象一起保存在堆上的）。扩展知识自动装箱与拆箱Java 是一种面向对象语言，很多地方都需要使用对象而不是基本数据类型。比如，在集合类中，我们是无法将int 、double等类型放进去的。因为集合的容器要求元素是 Object类型。为了让基本类型也具有对象的特征，就出现了包装类型，它相当于将基本类型"包装起来"，使得它具有了对象的性质，并且为其添加了属性和方法，丰富了基本类型的操作。装箱：基本类型自动转换为包装类型对象。拆箱：包装类型对象自动转换为基本类型的值。缓存机制包装类型中的 Byte、Short、Integer 和 Long 对某些范围内的值（例如 Integer 缓存 -128 到 127）会使用对象缓存来提升性能。因此，同一数值的包装类型对象可能是同一个实例。例如： Integer a = 100; Integer b = 100; System.out.println(a == b); // true Integer c = 200; Integer d = 200; System.out.println(c == d); // false 基础类型与包装类长度和范围分类基本数据类型包装类长度表示范围布尔型booleanBoolean// byteByte1 字节-128 ~ 127 shortShort2 字节-32768 ~ 32767整型intInteger4 字节-2,147,483,648 ~ 2,147,483,647 longLong8 字节-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807字符型charCharacter2 字节Unicode 字符集中的任何字符浮点型floatFloat4 字节约 -3.4E38 ~ 3.4E38 doubleDouble8 字节约 -1.7E308 ~ 1.7E308另外，近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型。可以看下方小程序试试
Java基础面试题 ——参数传递是按值还是按引用？
回答重点在 Java 中，参数传递只有按值传递，不论是基本类型还是引用类型。基本数据类型 （如 int, char, boolean 等）：传递的是值的副本，即基本类型的数值本身。因此，对方法参数的任何修改都不会影响原始变量。引用数据类型（如对象引用）：传递的是引用的副本，即对象引用的内存地址。因此，方法内可以通过引用修改对象的属性，但不能改变引用本身，使其指向另一个对象。扩展知识基本类型与引用类型的区别基本类型 ：包括 int, float, double, char, boolean 等，存储在栈内存中。方法中对基本类型参数的操作只会影响传递的副本，原始变量的值不受影响。引用类型 ：包括所有的对象和数组，引用类型的变量存储的是对象在堆内存中的地址。当引用类型作为参数传递时，传递的是这个地址的副本。因此，方法内的修改可以影响到传入的对象的内容，但不会影响对象引用本身的地址。示例代码分析 public class ParameterPassing { public static void main(String[] args) { int a = 5; modifyPrimitive(a); System.out.println("After modifyPrimitive: " + a); // 输出: 5 MyObject obj = new MyObject(); obj.value = 10; modifyObject(obj); System.out.println("After modifyObject: " + obj.value); // 输出: 20 resetReference(obj); System.out.println("After resetReference: " + obj.value); // 输出: 20 } public static void modifyPrimitive(int num) { num = 10; // 仅仅修改了副本，不影响原始变量 } public static void modifyObject(MyObject obj) { obj.value = 20; // 修改了对象的属性，会影响原始对象 } public static void resetReference(MyObject obj) { obj = new MyObject(); // 修改的是引用的副本，不影响原始对象 obj.value = 30; } } class MyObject { int value; } 在 modifyPrimitive 方法中，num 是基本类型的副本，因此对它的修改不影响原始变量 a。在 modifyObject 方法中，obj 是引用类型的副本，但这个副本仍指向原始对象，因此修改 value 属性会影响原始对象。在 resetReference 方法中，obj 被重新赋值为一个新对象，这个变化只影响副本，不影响原始引用。另外，近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣的微信扫码体验
Java中String、StringBuffer 和 StringBuilder有什么区别？
面试回答重点这三个都是 Java 中处理字符串的类，区别主要体现在可变性 、线程安全性 和性能上一、各自特征（1）String不可变 ：String 是不可变类，字符串一旦创建，其内容无法更改。每次对 String 进行修改操作（如拼接、截取等），都会创建新的 String 对象。适合场景 ：String 适用于字符串内容不会频繁变化的场景，例如少量的字符串拼接操作或字符串常量。（2）StringBuffer可变 ：StringBuffer 是可变的，可以进行字符串的追加、删除、插入等操作。线程安全 ：StringBuffer 是线程安全的，内部使用了 synchronized 关键字来保证多线程环境下的安全性。适合场景 ：StringBuffer 适用于在多线程环境中需要频繁修改字符串的场景。（3）StringBuilder可变 ：StringBuilder 也是可变的，提供了与 StringBuffer 类似的操作接口。非线程安全 ：StringBuilder 不保证线程安全，性能比 StringBuffer 更高。适合场景 ：StringBuilder 适用于单线程环境中需要大量修改字符串的场景，如高频拼接操作。二、总结String：不可变，适合少量字符串操作。StringBuffer：可变且线程安全，适合多线程环境中的频繁字符串修改。StringBuilder：可变且非线程安全，适合单线程环境中的高性能字符串处理。扩展知识在 Java 8 及以后，编译器会对字符串的常量拼接做优化，将字符串拼接转换为 StringBuilder 操作。这种优化提高了代码性能，但是在动态拼接或多线程场景下，手动使用 StringBuilder 和 StringBuffer 仍然更合适。String 是 Java 中基础且重要的类，并且 String 也是 Immutable 类的典型实现，被声明为 final class，除了 hash 这个属性其它属性都声明为 final。因为它的不可变性，所以例如拼接字符串时候会产生很多无用的中间对象，如果频繁的进行这样的操作对性能有所影响。StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类 ，提供 append 和 insert 方法，可以将字符串添加到已有序列的末尾或指定位置。它的本质是一个线程安全的可修改的字符序列，把所有修改数据的方法都加上了 synchronized。但是保证了线程安全是需要性能的代价的。在很多情况下我们的字符串拼接操作不需要线程安全，这时候StringBuilder登场了，StringBuilder是JDK1.5发布的，它和 StringBuffer 本质上没什么区别，就是去掉了保证线程安全的那部分，减少了开销。所以如果我们有大量的字符串拼接，如果能预知大小的话最好在 new StringBuffer 或者 StringBuilder 的时候设置好 capacity，避免多次扩容的开销（扩容要抛弃原有数组，还要进行数组拷贝创建新的数组）。另外，近期才哥整理出了一个可用于快速刷面试题的小程序，其中收录了常见面试题及其答案，涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型，感兴趣的可以点击下方扫码试试
在国企和央企当程序员的不同体验，太真实了。。
已经年底了，明年过了今年明年求职季来临，一份来自某垄断央企研发工程师岗位的工作经历分享，或许能为正在寻觅工作机会的小伙伴们提供一些有益参考。一、入职央企研发岗位初印象校招有幸踏入这家央企，开启研发工程师之旅。起初，怀揣着对技术研发的热情与憧憬，但很快便发现国企研发工作与想象中存在差异。在国企体系里，开发岗位处于相对底层，与互联网企业不同，这里更多遵循领导决策，缺乏像互联网中程序员与产品经理平等讨论的氛围，即便领导决策的需求不合理或可能引发诸多后续问题，作为开发人员，首要任务仍是坚决执行，努力达成领导交付的任务指标。二、国企开发工作日常剖析（一）大量内部项目入职初期，面临基于国产编辑器的培训任务。集团领导受市场上 Eclipse、Idea 等编辑器启发，决定打造内部专属编辑器，并要求所有项目强制基于此开展。这导致开发过程繁琐，常常需在项目中整合大量其他项目插件。原本基于 Eclipse 可轻松完成的任务，在国企环境下可能需在 2 - 3 个项目间跳转操作。由于国企项目以领导演示为导向，实用性并非首要考量，项目集成后犹如复杂的“X 山”。而且，集成的其他项目时常出现莫名错误，影响自身项目运行，可因项目或插件使用的强制性，开发人员只能无奈应对。（二）外包模式国企开发离不开外包团队，公司用工形式分为直聘与劳务派遣（外包）。直聘校招生享有公司统一编制，理论上可在系统内部调动，然而实际中 99.9%的普通员工难以实现。劳务派遣则多为社招或外包人员，在本公司项目中，干活主力往往是外包团队。起初我曾误判外包人员能力，实际工作中却发现部分外包人员技术执行力极强，多数项目实际控制权在外包手中，我们直聘人员更多负责管理资金拨付等事务，甚至在项目了解深度与颗粒度上不及外包。曾与一位近 40 岁、有腾讯、京东等互联网大厂工作经历的外包人员交流，他因身体原因转战国企，可见国企对于身体欠佳但有技术经验者亦是不错选择。（三）技术栈现状日常开发使用的技术相对陈旧，前端以JSP为主，后端是 Springboot系列。开发过程较少涉及多线程、高并发等前沿技术，多为基础的表设计与增删改查操作。对于技术追求不高的员工，可能每日仅需 2 - 3 小时即可完成工作任务。若有技术探索意愿，可利用剩余时间钻研新技术，自由度较高。这也意味着国企基层员工拥有较多可自主支配时间，可用于刷手机消遣，但若想自我提升，也可用于复盘学习新技术，毕竟在国企并非环境使人懈怠，而是个人态度决定发展走向。三、职业发展路径解读（一）升职空间分析央企研发岗位有技术与管理两种职级序列。管理序列晋升机会多倾向于有关系且有能力者，在国企中，人脉关系某种程度上可视为一种能力体现，若关系能助力解决公司问题，则更具优势。技术序列晋升多数情况下与工龄长短及 PPT 能力密切相关。国企研发工作深度有限，技术难度类似大专生在培训机构 3 个月所学成果。若想在技术序列向上发展，需着重锻炼 PPT 制作与表达能力，精准把握领导需求与关注点进行汇报展示。若志在技术钻研且有跳槽互联网打算，可在国企工作期间持续提升技术水平，否则需适应国企以管理和人际关系为主导的晋升模式。（二）薪资待遇构成公司作为成本单位，工资由总部发放，主要由工资 + 年终奖 + 福利组成。工资构成无绩效部分，取决于级别与职称，等级晋升主要依据工作年限，加班与否与工资增长无直接关联。年终奖由总部下发总包至公司，大领导分配至各部门，部门领导再根据个人工作情况分配到个人，部门内员工年终奖差距相对较小。福利方面，常见通信补助、房补、饭补及其他杂项福利，此为多数国企共有的福利模式。四、总结与感悟综合来看，在这家央企工作：其一，薪资待遇能保障一线城市基本生活开销，但购房压力仍较大。其二，开发技术更新滞后，工作以项目管理和执行领导决策为主。其三，若想在国企谋求领导职位，需着重提升 PPT 及沟通汇报能力，建立良好人际关系。若追求平淡稳定生活，可将更多时间留给家人，做一名普通员工亦无不可。国企工作犹如一把双刃剑，关键在于个人如何根据自身目标与追求，在这个环境中找准定位，实现自身价值与职业理想的平衡发展。 才哥最近开发了一个对于Java开发人员在线面试刷题的小程序，有需要的可以看看文章末尾，目前是免费阶段，也没有广告，才哥自己也在刷题备战明年黄金期，在线刷题很方便
开发中常见的加密方式，对称加密,非对称加密,摘要签名算法的区别
目录目录对称与非对称区别对称加密非对称加密两种方式的优缺点摘要签名加密算法国密介绍加密一般分为两种，对称加密和非对称加密。对称与非对称区别对称加密对称加密就是加密解密都用同一个秘钥，比如DES、3DES（TripleDES）和AES等。非对称加密非对称加密就是加密和解密不是用的同一种秘钥，比如RSA算法、DSA算法、ECC算法、DH算法等。在非对称加密中，用来加密的秘钥叫公钥，用来解密的秘钥叫私钥。公钥和私钥都是成对生成的，公钥分发给其他人用来加密，私钥用来解密，典型的应用场景为支付宝开放平台的签名和数据加解密两种方式的优缺点对称加密：解密速度快，但保密性差。非对称加密：加密算法保密性好，它消除了最终用户交换密钥的需要。但是加解密速度要远远低于对称加密。摘要签名加密算法摘要算法是一种能产生特殊输出格式的算法，这种算法的特点是：无论用户输入什么长度的原始数据，经过计算后输出的密文都是固定长度的，这种算法的原理是根据一定的运算规则对原数据进行某种形式的提取，这种提取就是摘要，被摘要的数据内容与原数据有密切联系，只要原数据稍有改变，输出的“摘要”便完全不同，因此，基于这种原理的算法便能对数据完整性提供较为健全的保障。但是，由于输出的密文是提取原数据经过处理的定长值，所以它已经不能还原为原数据，即消息摘要算法是不可逆的，理论上无法通过反向运算取得原数据内容，因此它通常只能被用来做数据完整性验证。常见的摘要算法有：MD5，SHA，国密SM3等，一般用于登录后的token生成等，数据加密后交易等，典型场景如微信支付签名验证国密介绍国密为国家密码局认定的国产密码算法，即商用密码。国密算法是国家密码局制定标准的一系列算法。其中包括了对称加密算法，椭圆曲线非对称加密算法，杂凑算法。具体包括SM1,SM2,SM3，SM4等，密钥长度和分组长度均为 128 位。SM1 为对称加密SM2 为非对称加密SM3 摘要签名SM4 对称加密,分组密码算法。
Java中基本数据类型与对应的包装类型介绍
目录目录一. Java基本类型与包装类型介绍二 类型的自动装箱与拆箱举例说明一. Java基本类型与包装类型介绍Java中有8种基本数据类型，分别为：数字类型(6种) ：byte、short、int、long、float、double字符类型(1种) ：char布尔型(1种) ：boolean。这八种基本类型都有对应的包装类分别为：Byte、Short、Integer、Long、Float、DoubleCharacterBoolean基本类型位数字节默认值int3240short1620long6480Lbyte810char162'u0000'float3240fdouble6480dboolean1官方文档未明确定义，它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1位，但是实际中会考虑计算机高效存储因素。false二 类型的自动装箱与拆箱装箱与拆箱的含义：装箱：将基本类型用它们对应的引用类型包装起来；拆箱：将包装类型转换为基本数据类型；Java基本类型的包装类的大部分实现了常量池技术，即Byte,Short,Integer,Long,Character,Boolean；前面4种包装类默认创建了数值[-128，127]的相应类型的缓存数据，Character创建了数值在[0,127]范围的缓存数据，Boolean 直接返回True Or False。如果超出对应范围仍然会去创建新的对象。其中仅浮点数类型的包装类Float与Double 没有实现常量池技术。Boolean常量池public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); // 其中TRUE与FALSE为定义的静态常量 }CharacterCache 缓存常量池private static class CharacterCache { private CharacterCache(){} static final Character cache[] = new Character[127 + 1]; static { for (int i = 0; i < cache.length; i++) cache[i] = new Character((char)i); } }/** *此方法将始终缓存-128 到 127（包括端点）范围内的值，并可以缓存此范围之外的其他值。 */ public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) //如果在缓存范围值内 return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); // 不如不在范围内者使用new运算符创建一个堆内存返回新的对象 }举例说明例子1 Integer i1 = 33; Integer i2 = 33; System.out.println(i1 == i2);// 输出 true, 因为33在-128-127范围内使用的是缓存常量，即两个数值的内存地址一致 Integer i11 = 333; Integer i22 = 333; System.out.println(i11 == i22);// 输出 false // 超出范围，返回的都是new的新对象 Double i3 = 1.2; Double i4 = 1.2; System.out.println(i3 == i4);// 输出 false // Double未实现缓存常量池 例子2需要注意一旦使用new运算符创建对象，则将创建新的数据，不会使用缓存常量值，如以下Integer i1 = 40; Integer i2 = new Integer(40); System.out.println(i1==i2);//输出 falseInteger i1=40；Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);，从而使用常量池中的对象。Integer i1 = new Integer(40);这种情况下会创建新的对象。例子3Integer i1 = 40; Integer i2 = 40; Integer i3 = 0; Integer i4 = new Integer(40); Integer i5 = new Integer(40); Integer i6 = new Integer(0); System.out.println("i1=i2 : " + (i1 == i2)); // true,两个变量使用的是同一个常量值 System.out.println("i1=i2+i3 : " + (i1 == i2 + i3)); // true, 因为"+"加号运算符不能使用在Integer上，所以会自动拆箱为基础类型int：40+0=40, 同时"="等号也无法使用在Integer上，则最终比较为40=40 System.out.println("i1=i4 : " + (i1 == i4));// false System.out.println("i4=i5 : " + (i4 == i5)); // false System.out.println("i4=i5+i6 : " + (i4 == i5 + i6)); //true,加法运算发自动拆箱后进行加法运行并进行比较 System.out.println("40=i5+i6 : " + (40 == i5 + i6));// true,加法运算发自动拆箱后进行加法运行并进行比较