2023年秋招某面试八股

5 篇文章 0 订阅
5 篇文章 0 订阅

一、HashMap的底层结构

HashMap底层实现是基于hash表的,用来存储键值型数据,存储时首先会计算键的hash code,然后根据hash code找到存储位置;

HashMap内部使用一个数组Entry[]来存储数据,数组的每个元素称为桶,每个桶可以存储一个或者多个键值对,当有多个键映射到同一个桶上时就用链表来存储,即每个桶中的元素就是一个链表节点。

随着链表长度的增加导致查询效率变低,所以就引入了红黑树,当链表节点超过默认值8以及数组已经扩容到64的时候,链表会转换成红黑树,以提高查询、插入和删除的操作效率。

为了保证哈希表的负载因子(Load Factor)不超过给定阈值(默认为0.75),当哈希表中的元素数量达到一定阈值(容量乘以负载因子)时,HashMap会进行扩容操作,重新调整数组的大小,以保证哈希表的效率。

总结起来,HashMap的底层实现是一个数组加链表(或红黑树),通过哈希码和链表来存储和访问键值对,具有高效的查找、插入和删除操作,但在极端情况下,如果哈希冲突过多或负载因子过高,可能会导致性能下降。

HashMap容量大小为什么都是2的倍数?—在HashMap的实现中,选择容量大小为2的倍数是为了提高哈希函数的计算效率和减少哈希冲突的发生,从而提高HashMap的性能和效率。

当容量大小为2的倍数时,HashMap可以使用位运算替代取模运算,即 hash & (capacity - 1),其中 capacity 是容量大小。位运算的效率通常比取模运算更高,因此可以加快哈希函数的计算速度。

此外,容量大小为2的倍数还有助于减少哈希冲突的发生。当容量为2的倍数时,哈希码的低位与容量进行按位与运算时,可以保证分布相对均匀,减少了哈希冲突的可能性。如果容量不是2的倍数,那么取模运算的结果可能会导致哈希码分布不均匀,增加了哈希冲突的概率。

HashMap在什么时候触发扩容?—HashMap在插入键值对时,会根据当前的元素数量和负载因子(Load Factor)来判断是否需要进行扩容操作。当元素数量超过容量与负载因子的乘积时,就会触发扩容。具体来说,当元素数量超过容量乘以负载因子(threshold = capacity * loadFactor)时,HashMap会进行扩容操作。默认情况下,负载因子的值为0.75,这是一个经验值,可以在时间和空间效率之间进行权衡。

二、synchorinesd锁升级

在Java中,synchronized 是最基础的同步机制。为了提高性能,Java的 HotSpot 虚拟机对synchronized 锁进行了很多优化。其中之一就是锁的升级。锁在 JVM 中的状态大体上可以分为三种:

  1. 偏向锁(Biased Locking): 当一个锁刚初始化时,它被认为是偏向锁。这是基于一个假设:大部分情况下,锁不仅不会有多线程竞争,而且总是由同一个线程多次获得,而不需要真正的互斥。当一个线程获取到偏向锁后,会将锁标记为偏向,并将其线程ID存储在对象头中。以后这个线程再次请求锁时,可以快速获得。如果另外的线程尝试获取这个锁,那么偏向锁会被撤销,并升级为轻量级锁。
  2. 轻量级锁(Lightweight Locking): 当锁升级为轻量级锁时,其他线程尝试获取锁会通过自旋来尝试获得锁,而不会立即阻塞。如果一个线程持有轻量级锁,另外的线程尝试获取,它会在不进入内核的情况下忙等待,如果尝试获得锁失败,它会被升级为重量级锁。
  3. 重量级锁(Heavyweight Locking): 当有更多的线程进入锁的竞争时,轻量级锁就会升级为重量级锁。这时,未能获取锁的线程会被阻塞。重量级锁的性能通常较低,因为线程进入和退出阻塞都需要切换线程和进入内核,这是一个相对较重的操作。

锁的升级过程是从偏向锁 -> 轻量级锁 -> 重量级锁。不过在某些场景和 JVM 的配置下,锁的偏向可以被禁用,这样对象默认不会处于偏向锁状态。

锁的升级旨在提高锁的性能。在没有真正线程竞争的情况下,偏向锁和轻量级锁可以非常快速地获取和释放。但是在有真正竞争的情况下,锁将会退化为重量级锁,以确保线程安全。

三、volatlie关键字

volatile 是Java中的一个关键字,用于修饰变量,用来确保多个线程之间对该变量的可见性和有序性。

当一个变量被声明为 volatile 时,编译器和运行时系统会注意到这个变量是共享的,并且在访问该变量时会执行特定的操作,以保证每个线程都能正确地读取和修改该变量的值。

volatile 关键字提供了以下特性:

  1. 可见性(Visibility):当一个线程修改一个 volatile 变量的值时,该变量的新值会立即被写入到主内存中,并且其他线程在读取该变量时会从主内存中重新获取最新值。这样可以确保不同线程之间对变量的修改是可见的,避免了线程之间的数据不一致性。
  2. 有序性(Ordering):volatile 关键字可以防止指令重排序优化,保证了 volatile 变量的读写操作按照代码的顺序执行,从而避免了可能的并发问题。它保证了在一个线程的写操作之前,对该变量的读操作和其他变量的写操作都已经完成。

需要注意的是,volatile 关键字不能保证原子性。它可以确保单个读/写操作的原子性,但无法保证复合操作的原子性。如果需要进行原子性操作,可以考虑使用 synchronized 关键字或者 java.util.concurrent.atomic 包中提供的原子类。

使用 volatile 关键字要谨慎,只在特定的场景下使用。它适用于对变量的写操作不依赖于当前值,或者只有单个线程修改变量值的情况下。如果涉及到复杂的操作或者多个线程同时修改变量的情况,需要使用其他更强大的同步机制来确保线程安全性。

三、类的加载过程

加载、链接、初始化。

1.加载:将类的字节码文件从磁盘或者网络加载到内存的过程,类加载器负责加载类,并在堆上生成对应的Class对象表示这个类。

2.链接:将类的字节码文件与其它类和符号引用关联起来,链接过程包括以下介个阶段:

  • 验证:确保字节码文件满足JVM得安全要求;
  • 准备:为类的静态变量分配内存,并赋默认初始值;
  • 解析:解析符号引用,将符号引用转换成直接引用;它涉及到找到类、接口、字段和方法在方法区中的具体位置。

3.初始化:为类的静态变量赋真正的初始值;

4.使用:当类被初始化后就可以在程序中使用了;

5.销毁:当类不再被使用,并且该类的所有对象都被回收,该类的定义就会从方法区中被移除;

类的加载过程中,主要的参与者是类加载器,如Bootstrap ClassLoader、Extension ClassLoader和System or Application ClassLoader,还可以自定义类加载器,类加载器采用双亲委派模型,即先尝试由父类加载器加载类,只有在父类加载器无法加载时才由子类加载器尝试加载。还需要注意,类不是在第一次出现在代码中就被加载,类加载是JVM按需进行加载的,例如当我们需要访问静态成员变量、静态方法或者是创建该类的实例的时候才会去加载。

四、Java内存模型

Java内存模型(Java Memory Model, JMM)将内存分为主内存和工作内存。所有变量都存储在主内存中;每一个线程都有自己的工作内存,工作内存有变量的副本。各线程对变量的操作都是在自己的工作内存中进行,然后再同步到主内存。

Java内存模型主要描述的是多线程环境下,如何确保线程之间安全地共享变量的。主要分为以下几个区域:

  • 堆:是JVM管理的一个最大的区域,主要用来存放Java对象(就是new出来的对像);堆是GC的主要管理区域,堆一般分为新生代、老年代和永久代(1.8后改为元空间了),新生代又分为Eden区和两个Survivor区(S0和S1,在new对象的时候交替使用)。
  • 方法区:用于存储已经加载的类信息、静态变量、常量以及及时编译器编译后的代码数据。
  • 虚拟机栈:用来描述Java方法执行的内存模型,每个方法都会创建一个栈桢用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
  • 程序计数器:看作是当前线程执行的字节码的行号指示器,字节码解释器工作时,通过改变这个计数器的值来选择下一条需要执行的字节码指令;
  • 本地方法栈:类似于虚拟机栈,但它服务于Java的Native方法。

五、元空间为什么出现?

元空间是Java8以后版本中取代永久代的而出现的新的内存区域,元空间的出现主要是为了解决永久代的一些局限性和问题以及提高JVM的性能、稳定性和可维护性。

永久代的缺点:永久代的大小是固定的,需要在JVM启动时指定。当永久代空间不足时,即使堆内存还有大量空闲,也会引发java.lang.OutOfMemoryError: PermGen space异常。永久代需要定期进行垃圾回收,特别是在那些有大量类动态加载和卸载的场景中。这使得Full GC的执行变得频繁和耗时。与堆内存相比,永久代的内存管理更为复杂,因为它还涉及到类的加载、卸载、常量池的管理等。随着Java的发展和新功能的添加,永久代的管理和维护变得日益困难。为新的Java特性做出调整可能会导致向后兼容性问题。

元空间的优点:元空间使用的是本地内存,即操作系统的内存,而不是JVM的堆内存。这意味着它不再受到固定大小的限制,只受制于机器的实际物理内存。由于元空间不在Java堆上,所以Full GC的发生频率可以显著降低。这有助于减少应用的停顿时间。元空间提供了更为灵活和高效的内存管理机制,特别是在大量的类加载和卸载场景下。移除永久代有助于简化HotSpot JVM的代码,因为它不再需要维护特定于永久代的代码。元空间提供了更稳定的结构来管理类元数据,从而提高了JVM的整体稳定性。

六、对象什么时候进入老年代?

对象从新生代进入老年代称为“晋升”,有以下几种情况对象会“晋升”:对象年龄达到阈值:在新生代中,对象每经历一次Miner GC如果没有被清除,其年龄就会加1,当年龄达到阈值就会晋升到老年代,该年龄阈值默认为15,也可以修改。动态对象年龄判断:如果在Servivor区,某一年龄值的对象总和已经占据该空间的一半以上,那么此时大于等于该年龄的对象就可直接进入老年代,无需等待默认的年龄阈值。大对象直接进入老年代:比如很长的字符串或者数组,需要大量连续内存空间的对象在创建分配内存的时候直接在老年代中分配。

七、怎么判断对象是否可以被回收?

在Java中,对象是否可被垃圾回收是基于可达性分析来确定的。以下是判定一个对象是否可被回收的几个标准:无引用:如果一个对象没有任何对象引用它,那么该对象被认为可回收。循环引用:对象之间相互引用,但它们作为一个整体不再被外部引用,这样的对象群也是可回收的。**弱引用(Weak Reference)**当对象只被弱引用所指向时,它就被视为可回收的。软引用(Soft Reference):一个对象如果只被软引用关联,那么在内存不足时,垃圾回收器会考虑回收这个对象,但在内存充足时不会。虚引用(Phantom Reference):最弱的一种引用关系。对象只被虚引用关联时,任何时候都可能被垃圾回收器回收。手动置空: 如果你手动将一个对象引用设置为null,那么原先指向的对象就会失去一个强引用,进而可能变得可回收(如果没有其他强引用指向它)。局部作用域: 对象在局部作用域(例如方法内)创建并被引用。当该方法执行完毕,对象的引用超出了其作用域,此时如果没有其他引用指向该对象,它就变得可回收。总之,对象是否可以被回收的核心在于它是否还被强引用所指向。只要对象不再被任何强引用指向,它就是可回收的,但实际上它何时被回收则取决于垃圾回收器的行为和策略。

八、哪些对象可作为GCroot?

GCRoot是垃圾回收器在进行可达性分析的起始点,所以常作为GCRoot的对象有:活动线程、静态变量、局部变量、Java虚拟机栈中引用的对象、常量池中的对象等,垃圾收集器会从这些Roots开始,判断其他对象是否与这些Roots有活动引用链。如果一个对象与所有GC Roots都不再有引用链,那么此对象将被视为垃圾,可以被回收。

九、SpringBean的生命周期和加载过程

SpringBean的加载过程是Spring IOC容器初始化和装配Bean的核心操作。主要过程包括:资源定位:初始的XML配置文件和其它资源文件;载入:资源文件被载入,然后基于XML、Java注解、Java配置类转化为Spring内部数据结构(就是Bean的定义);注册:通过BeanDefinitionRegister接口进行操作,将Bean的定义信息注册到IOC容器中;类型匹配:基于Bean的类属性,实例化Bean;初始化:如果Bean实现了InitializingBean接口,调用afterPropertiesSet方法;如果Bean在配置中指定了init-method,调用指定的初始化方法;属性填充:根据Bean的定义,使用反射机制填充所有属性。这个步骤确保了所有的依赖注入都被处理;后置处理:如果定义了BeanPostProcessor,则此时会调用postProcessBeforeInitializationpostProcessAfterInitialization方法;这为Bean的初始化提供了前后的处理钩子;完成:此时,Bean完全初始化和装配。应用程序可以使用它了;销毁:如果Bean实现了DisposableBean接口,容器在关闭时会调用其destroy方法;如果Bean在配置中指定了destroy-method,当容器关闭时,该方法会被调用。Spring容器负责管理这整个过程,确保Bean的定义、创建、初始化、装配和销毁都按预期执行。

十、SpringBean的作用域

SpringBean的作用域用来描述一个Bean的实例在IOC容器中的生命周期:SpringBean的作用域:

  • 单例(singloton):在整个Spring IoC容器中,只创建Bean的单个实例。无论多少次请求,总是返回同一个实例。
  • 原型(prototype):每次请求(获取)都会创建一个新的Bean实例。与singleton作用域相反,每次调用getBean()方法都会返回一个新的实例。
  • Request:每个HTTP请求都会创建一个新的Bean。该作用域只在Web-aware Spring ApplicationContext中有效。
  • session:对于单个HTTP Session,创建一个Bean实例。同样,该作用域也仅在Web-aware Spring ApplicationContext中有效。

十一、SpringBean的自动装配

在Spring框架中,自动装配机制是用于根据Bean属性的类型或名称自动解析依赖关系,从而避免手动指定构造函数或属性的注入。有以下几种自动装配模式:byType、byName、constructor。

如何使用自动装配呢?—在XML配置中,您可以使用autowire属性来定义自动装配模式;在Java配置中,可以使用@Autowired注解进行自动装配。Spring会默认按类型(byType)进行匹配。如果需要按名称(byName),您可以结合使用@Autowired@Qualifier注解。

十二、SPI机制

SPI(Service Provider Interface)是Java提供的一套用于发现和加载服务提供者的机制,主要用于框架内部加载插件、扩展等功能。SPI机制在很多框架和库中都有应用,例如JDBC中加载数据库驱动就是基于SPI的。SPI工作的核心思想是:为一个接口定义多个实现类,并且在运行时动态选择并加载其中的一个或多个实现。SPI的优势:解耦:SPI机制使第三方服务提供者能够与框架解耦,只要实现框架定义的接口即可。扩展性:可以很容易地添加或替换框架的组件。动态服务发现:可以在运行时动态发现并加载服务。

十三、TCP/IP模型

TCP/IP是在计算机网络中实际广泛使用的一种网络协议,TCP/IP模型中设计多种协议,最核心的就是网络传输层的传输控制协议TCP协议以及互联网协议IP协议;TCP/IP模型分为四层:应用层、传输层、网络层、网络接口层(链路层)。

十四、OSI模型

OSI模型是一个网络通信的概念模型,它将网络模型分为7层:应用层、会话层、表示层、传输层、网络层、数据链路层、物理层。

十五、MySQL覆盖索引

当一个SQL查询可以仅通过使用索引来获得结果,而无需访问数据表中的真实数据,就称这个索引为覆盖索引。覆盖索引的优点:性能优化:一般来说索引列比数据存储的列要少的多,所以从索引中读取数据更快;减少随机IO操作:索引项通常是有序的,因此在读取索引的时候,磁盘IO也是顺序的。

如何使用覆盖索引呢?—在SELECT子句中列出的字段尽可能只是是索引的一部分;WHERE和JOIN子句中用到的字段也应是索引的一部分。

十六、回表操作是什么?

在MySQL数据库中,回表操作是指在利用非聚簇索引(也称为二级索引、辅助索引)查询时,需要再次访问主表(或称为聚簇索引表)获取需要的数据的过程。这种操作被称为“回表”。为了减少回表操作,可以使用“覆盖索引”。当查询只需要访问索引中的列,而不需要访问其他非索引列时,就可以避免回表操作,从而提高查询性能。

十七、双向指针的好处

十八、雪花算法、UUID、自增ID的性能比较

雪花算法的效率高,在多服务器、多数据中心的环境中,能够快速生成唯一ID;主要特点是:唯一性:在分布式系统中能够保证全部唯一性;有序性:生成的ID是递增的,在排序场景下很有利;自定义性:可以根据业务场景自定义每部分的长度;

UUID生成的速度快,但是在向数据库插入新的数据的时候,可能会导致主键索引频繁的变动,这样导致索引的维护成本增加,进而影响数据插入的速度。特点:唯一性:全局唯一;无序性:UUID是无序的,如果需要排序则效率不高;固定长度:UUID较长,为32位;

自增ID生成的速度快,在单一数据库系统中,速度是最快的;特点:唯一性:在单一数据库中唯一。但在分布式环境中,每个数据库的自增ID可能会有冲突;有序性:自增ID是有序的;简单性:实现简单,大多数关系数据库都支持;

十九、慢SQL的优化思路

1.SQL审查:确保SQL语句编写的得当,避免使用过多的子查询,可以使用联合查询代替子查询;避免在WHERE子句中使用非列明的函数计算,这会导致索引失效;限制一次查询的数据量,只查询需要的数据。

2.充分利用索引:为经常用于查询的列建立合适的索引;考虑为经常参与排序或分组的列创建索引;但要避免使用太多索引,因为它们会降低插入和更新的速度。

3.优化执行计划:使用EXPLAIN命令查看查询计划,确保数据库在执行查询时使用了最优的路径;如果数据库选择了非最优的查询计划,考虑使用查询提示或优化相关的统计信息。

4.检查数据库设计:检查数据表设计是否合理,规范化数据以减少数据冗余;在必要的地方使用反规范化来提高查询性能;考虑使用分区表来提高大表的查询性能。

二十、索引失效场景

1.函数操作或计算:在列上使用函数或进行某种计算会导致索引失效。

2.隐式类型转换:如果查询条件的数据类型与列的数据类型不匹配,可能会导致隐式类型转换,从而使索引失效。

3.LIKE查询:以通配符开头的LIKE查询通常不会使用索引。

4.不等操作:使用!=<>操作符可能导致全表扫描。

5.OR操作:在不同的列上使用OR操作可能导致索引失效。

6.NULL值:在某些数据库中,索引不包括NULL值。因此,与NULL值相关的查询可能不会使用索引。

7.组合索引的部分使用:如果仅查询组合索引的非第一列,索引可能不会被使用。

8.使用NOT IN、NOT EXISTS:这些操作可能导致索引不被使用。

二一、用Redis实现分布式锁

实现Redis分布式锁的基本步骤:

  1. 获取锁: 使用SETNX命令尝试设置一个键。SETNX是"SET if Not eXists"的缩写,这个命令只在键不存在时才会设置值。这意味着,只有第一个客户端能设置上锁,后来的其他客户端都会失败。

    SETNX lock_key some_value
    #这里some_value可以是一个唯一标识符,如UUID,以标识锁的拥有者。
    #为了避免死锁,你还需要设置一个过期时间,确保在某个时刻锁会被自动释放。Redis的SET命令在新版本中允许同时设置值和过期时间:
    SET lock_key some_value EX 10 NX
    
  2. 释放锁: 只有锁的拥有者才应该释放该锁。你可以通过比较lock_key的值来判断客户端是否是锁的拥有者。如果是,可以安全地删除该键来释放锁。

  3. 处理锁续约: 如果任务的执行时间可能超过锁的过期时间,可以考虑锁的续约。在锁快到期时,可以重新设置其过期时间。这可以使用后台线程或定时器来实现。

  4. 处理锁竞争: 当多个客户端尝试获取锁时,不应立即失败,而是应该稍等片刻后重试。这可以通过简单的循环和sleep语句来实现。

二二、SETNX与普通SET的区别

SETNX 和普通的 SET 是Redis中两种不同的设置键值的操作,它们的主要区别如下:

  1. 操作条件:
    • SETNX: SETNX` 是 “SET if Not eXists” 的缩写。意思是仅当键(key)不存在时,为键设置指定的值。如果键已经存在,则不执行任何操作。这保证了该操作的原子性,因此它经常在需要确保互斥性时使用,如上述的分布式锁的场景。
    • SET: 这是一个普通的设置键值的操作。不考虑键是否存在,直接设置键的值。如果键已经存在,则它的值会被覆盖。
  2. 返回值:
    • SETNX: 如果键已经存在,返回 0,否则如果设置成功,返回 1。
    • SET: 如果值被设置了,它通常返回 “OK”。
  3. 使用场景:
    • SETNX: 主要用于需要确保某些操作只在键不存在时才执行的场景,例如初始化、获取锁等。
    • SET: 用于普通的设置键值的操作,不需要考虑键的存在性。

二三、分布式锁的释放

怎么确保只有持有该锁的客户端才能成功删除锁的键,其他客户端无法删除?—为每个锁分配一个唯一的标识(当客户端尝试获取锁时,它生成一个唯一的标识,通常是一个随机的UUID,当客户端尝试释放锁时,它首先检查该锁的值(唯一标识)是否与其当初设置的值匹配。如果匹配,才执行删除操作)、使用Lua脚本(检查锁的值并在匹配的情况下删除它)、设置锁的超时时间(如果一个客户端因为某种原因崩溃并且没有释放锁,这个锁会因为超时而自动释放)。

二四、Spring Security有哪些主要过滤器

1.UsernamePasswordAuthenticationFilter: 处理基于表单的登录验证。当用户在登录表单中输入用户名和密码并提交时,此过滤器负责处理验证。

二五、异常处理

异常处理是确保程序健壮性和稳定性的关键部分,应该作为代码设计和实现的一部分来仔细考虑。在Java中,异常处理主要通过以下五个关键字来实现:trycatchfinallythrowthrows

1.try-catch:try 块包含可能引发异常的代码。catch 块是用于捕获特定异常并处理它的代码。

2.finally:finally 块包含无论是否发生异常都必须执行的代码。通常用于清理资源,例如关闭文件或数据库连接。

3.throw:throw 关键字用于在代码中手动引发异常。

4.throws:throws 关键字用于声明一个方法可能会抛出的异常。它使调用方法的代码知道这个方法可能会抛出的异常,因此调用代码可以选择处理或再次抛出这些异常。

二六、可以让前端捕获后端的异常吗?

前端可以捕获后端发送的异常信息。但是,通常来说,后端不会直接将异常栈或内部异常细节发送给前端,因为这可能泄露敏感信息、系统内部结构或可能被恶意用户利用的其它细节。常的做法是:定义错误响应格式: 当后端发生异常时,会返回一个特定的HTTP状态码(如400、404、500等)和一个包含更多错误详细信息的响应体返回给前端。响应体可能包含一个简短的错误消息、一个错误码或其他相关信息。

二七、Mybatis怎么单独控制事务

二八、验证码如何保证使用过一次不能再使用?

确保验证码在使用后失效是为了增加系统的安全性,防止重放攻击。以下是实现该功能的常用方法:

  1. 存储在服务端的会话中:当用户提交验证码时,从会话中获取并与用户提交的验证码进行比较。如果匹配成功,则处理用户的请求,并从会话中删除验证码,确保它不会被再次使用。
  2. 使用缓存机制:当验证码生成时,使用一个缓存服务(如Redis)存储验证码,并为其设置一个适当的过期时间。当用户提交验证码时,检查缓存中是否存在该验证码。如果存在且匹配,则处理用户的请求,并从缓存中删除该验证码。
  3. 数据库存储:将验证码与用户ID、时间戳和使用状态(已用/未用)关联,并存储在数据库中。当用户提交验证码时,查询数据库以验证验证码的有效性和使用状态。验证成功后,更新数据库中的验证码状态为“已使用”。
  4. 使用时间戳和有效期:当验证码生成时,为其设置一个有效期(例如,5分钟)。存储验证码和其生成的时间戳。当用户提交验证码时,检查当前时间与存储的时间戳之间的差异。如果时间差超出有效期,则验证码为无效。如果用户使用的验证码是有效的,确保将其标记为已使用。
  5. 单次URL:在某些情况下,例如通过电子邮件发送的验证码,可以为每个验证码生成一个唯一的URL。当用户通过该URL访问时,系统验证验证码并将其标记为已使用,以确保URL不会再次访问。
  6. 前端和后端双重验证:在前端,使用JavaScript确保验证码输入字段在首次使用后变为禁用状态,防止用户多次提交。但是,仅依赖前端验证是不够的,后端验证同样重要,因为攻击者可能直接绕过前端逻辑。

不论使用哪种方法,都要确保验证码在验证后从存储机制中删除或标记为已使用,这样用户或攻击者就无法再次使用相同的验证码。

二九、静态代码块有什么特点?

在Java中,静态代码块(也称为静态初始化块)是在类中使用static关键字定义的代码块。静态代码块具有以下特点:执行时机:静态代码块在类加载时执行,并且只执行一次。执行顺序:静态代码块按照它们在类中的定义顺序执行。初始化静态成员:通常,静态代码块用于初始化类的静态成员变量,特别是那些不能用简单的赋值语句初始化的变量。没有返回值、参数和异常声明:与方法不同,静态代码块不可以有返回值、不能接受参数,也不能显式地声明要抛出的异常(但它们可以使用try-catch块处理异常)。访问限制:静态代码块不能访问类的非静态变量和非静态方法,因为这些成员在静态上下文中不可用。但它们可以访问类的静态变量和静态方法。没有标识符:静态代码块不能像方法那样被命名。它仅由static关键字和一对大括号组成。无法显式调用:由于静态代码块没有名称,所以它们不能被显式调用。它们仅在类加载时自动执行。存在的目的:静态代码块为开发者提供了一种在不创建类的实例的情况下初始化类的静态资源的机制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值