面试总结
Hash算法解决冲突的四种方法
- 开放定址法:当发生冲突时,寻找下一个空的哈希地址。这包括:
- 线性探测法:如果位置被占用,就顺序查找下一个空位。
- 平方探测法:如果位置被占用,就在前后位置进行查找。
- 再哈希法:构造多个不同的哈希函数,当发生冲突时,使用另一个哈希函数计算地址。
- 链地址法:将所有哈希地址相同的记录链接在同一链表中。
- 建立公共溢出区:将哈希表分为基本表和溢出表,发生冲突的记录存放在溢出表中。
如何创建线程?
一般来说,创建线程有很多种方式,例如继承Thread
类、实现Runnable
接口、实现Callable
接口、使用线程池、使用CompletableFuture
类等等
java中线程同步的几种方法
方法一:使用synchronized关键字
方法二:wait和notify
方法三:使用特殊域变量volatile实现线程同步
方法四:使用重入锁实现线程同步
方法五:使用局部变量来实现线程同步
方法六:使用阻塞队列实现线程同步
方法七:使用原子变量实现线程同步
动态链接和静态链接
静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时,下面来详细介绍这两种链接方式。
- 静态链接:
- 静态链接发生在编译期间。
- 所有代码在编译时被加载到内存中,并不需要再次加载。
- 生成的可执行文件包含所有依赖的代码,因此程序体积较大。
- 适用于不需要频繁更新代码的应用程序。
- 动态链接:
- 动态链接发生在运行时。
- 代码模块只有在需要时才会被加载到内存中。
- 程序体积较小,因为不需要将所有代码打包到一个可执行文件中。
- 适用于需要灵活扩展的应用程序,因为它允许按需加载和更新代码。
抓包软件和抓包代码
Wireshark或Charles等抓包工具 tcpdump
中断和异常的区别
相同点:都是CPU对系统发生的某个事情做出的一种反应。
区别:**中断*由外因引起*,异常由CPU本身**原因引起。
- 中断——外部事件引起,正在运行的程序所不期望的
- 异常——内部执行指令引起
为啥先放阻塞队列再建非核心线程?
提高资源利用率:创建和销毁线程需要耗费系统资源,而线程池通过复用线程,可以避免由于频繁地创建和销毁线程所带来的系统开销。
提高响应速度:当任务到达时,无需再去创建线程,而是可以直接由线程池中空闲的线程去执行,这样可以显著提高系统的响应速度。
2 在创建新线程的时候,是要获取全局锁的,这个时候其他的就需要阻塞,影响了整体效率。
就好比一个企业里面有十个(core)正式工的名额,最多招十个正式工(核心线程),要是任务超过正式人数(task>core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这十个人,但是任务可以稍积压一下。即先放到队列中去(代价低)。十个正式工慢慢干,迟早会干完的,如果任务还在持续增加,超过正式工的加班忍耐极限了(队列满了),就招外包(非核心线程)帮忙了,还是正式工加外包还不能完成任务,那么新来的任务就会被领导拒绝(线程池拒绝策略)。
spring的动态代理和JDK的动态代理有什么区别
在Java中,动态代理是一种常用的设计模式,用于在运行时动态创建代理类并代理方法调用。Spring框架和Java Development Kit (JDK)都提供了动态代理的实现,但它们之间存在一些关键的区别:
实现方式:
- JDK动态代理:仅支持接口的代理。它是通过实现接口来创建动态代理的,使用
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口实现。 - Spring动态代理:可以是基于JDK的动态代理,也可以是基于CGLIB的动态代理。如果代理的目标对象实现了至少一个接口,则Spring默认使用JDK动态代理。如果目标对象没有实现任何接口,则Spring会使用CGLIB来创建代理。
- JDK动态代理:仅支持接口的代理。它是通过实现接口来创建动态代理的,使用
代理的内容:
- JDK动态代理:代理的是接口,不直接支持类的代理。
- Spring动态代理(使用CGLIB):可以代理没有实现接口的类。CGLIB(Code Generation Library)是一个强大的高性能代码生成库,用于在运行时扩展Java类和实现接口。
性能和使用场景:
- JDK动态代理:在代理接口方面,性能通常比CGLIB稍好,因为它只是简单地反射调用方法。更适合那些已经定义了接口的类。
- Spring动态代理(使用CGLIB):在创建代理类时需要更多的处理,可能在性能上略逊一筹,但是能够代理那些没有实现接口的类。适用于那些不易修改源码或者难以定义接口的旧有代码库。
配置和使用:
- JDK动态代理:使用较为直接,只需要定义一个实现了
InvocationHandler
的类即可。 - Spring动态代理:通常通过Spring AOP(面向切面编程)来配置,可以更灵活地控制代理的行为,例如,通过切点和通知来定义何时以及如何进行方法拦截。
- JDK动态代理:使用较为直接,只需要定义一个实现了
B树结构
B树,多路平衡搜索树,B树即B-树。其存储特点是每个磁盘块上都会存储具体数据和指向下一磁盘块的指针。详情见下图,一个磁盘块16KB大小。这中存储方式看似满足要求,但是也不支持范围查询,并且还有一个很严重的问题,B树的存储结构不满足数据量过大的情况。这里我们做个简单的计算:每个磁盘块16KB大小,假设指针不占用空间,一条数据1KB,每个磁盘块16条数据,三层B树能存储16 * 16 * 16=4096条数据,一个磁盘块代表一次IO,很明显数据量多的情况下,IO次数也会多,会影响查询性能,于是在B树的基础上衍生出了B+树。
B+树
B+数的存储特点是叶子节点存储的是主键key或者是具体的数据,非叶子节点存储的是主键和指向下一页的指针,叶子节点之间又通过指针相连,形成双向链表结构。此时也做一个简单的数据计算:一个两层的B+树,一个页16KB,假设一条记录1KB,一个叶子节点最多存放16条记录,根节点存放的都是指针和主键,假设主键是8字节,InnoDB指针大小是6字节,一共14字节,根节点可以存放16 * 1024B/14,大概是1170条,存储的数据就是1170 * 16=18720条,那么同理三层B+树能支持18720 * 1170=21902400条数据(千万级别)。相比于B树,B+树能在IO次数相同的情况下存储更多的数据,同时由于叶子节点之间是双向链表形式,能支持范围插叙,因此MySQL选择了B+树作为索引结构;
Minor GC、Major GC、Full GC的区别
今天主要谈谈JVM GC的类型和策略,特别是大家经常混淆的Minor GC、Major GC、Full GC,年轻代GC、老年代GC,之间有什么区别和联系。
Minor GC
JVM堆内存被分为两部分:年轻代(Young Generation)和老年代(Old Generation)。
1.年轻代
年轻代是所有新对象产生的地方,当年轻代内存空间被用完时,就会触发垃圾回收,这个垃圾回收叫做Minor GC。
年轻代被分为3个部分——Enden区和两个Survivor区,年轻代空间的要点:
- 大多数新建的对象都位于Eden区。
- 当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区。
- Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。
- 经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。
2.年老代
年老代内存里包含了长期存活的对象和经过多次Minor GC后依然存活下来的对象,通常会在老年代内存被占满时进行垃圾回收。
Major GC
老年代的垃圾收集叫做Major GC,Major GC通常是跟full GC是等价的,收集整个GC堆。
Minor GC和Major GC其实就是年轻代GC和年老年GC的俗称。而在Hotspot VM具体实现的收集器:Serial GC, Parallel GC, CMS, G1 GC中,大致可以对应到某个Young GC和Old GC算法组合。
分代GC
针对HotSpot VM的实现,其实GC的准确分类可以分为:
- 分代GC
- Full GC
以及后续的G1的分区收集本质其实还是一个分代收集器,但是和之前的各类回收器不同,它同时兼顾年轻代和老年代。
分代GC并不收集整个GC堆的模式,而是只专注分代收集
- Young GC:只收集年轻代的GC
- Old GC:只收集年老代的GC(只有CMS的concurrent collection是这个模式)
- Mixed GC:收集整个young gen以及部分old gen的GC(只有G1有这个模式)
Full GC
Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。
以上就是Minor GC、Major GC、Full GC的一个介绍,后续我们再重点介绍JVM GC相关的垃圾回收算法以及底层的实现。
高并发用户登录会有缓存穿透问题吗
Spring中BeanFactory和FactoryBean的区别
BeanFactory
是Spring框架的核心接口之一,用于管理和获取应用程序中的bean实例。它是Factory模式的实现,负责创建、配置和管理Bean对象。BeanFactory是Spring IoC容器的基础,它从配置元数据(例如XML文件)中读取Bean定义并实例化和交付那些需要的时候吃豆子。FactoryBean
是一个特殊的bean,它是一个工厂对象,用于创建和管理其他bean的实例;FactoryBean接口定义了一种创建bean的方法,允许开发人员在bean创建过程中进行更多的定制。通过实现FactoryBean接口,开发人员可以创建复杂的bean实例,或者在实例化bean之前执行一些额外的逻辑处理。
区别在于,BeanFactory是Spring框架的核心接口,用于管理和提供bean的实例,而FactoryBean是一种特殊的bean,用于创建和管理其他bean的实例。FactoryBean在创建bean的过程中提供了更多的自定义功能,允许额外的逻辑处理。
MQ实现分布式事务
在分布式事务的实现中,ACK消息和半消息是两种实现分布式事务的两种不同机制,它们的共同点是都使用了两阶段提交的机制,但是它们的实现细节和适用场景有所不同。具有一些区别和特点。下面是ACK消息和半消息在实现分布式事务方面的比较:
ACK消息(Acknowledgement):
- ACK消息是指在分布式事务中,消息的接收者(消费者)在成功处理消息后发送ACK(确认)给消息的发送者(生产者)。
- 生产者发送消息后,需要等待消费者发送ACK以确认消息已被正确处理。
- 如果消息未收到ACK确认,生产者可以选择重新发送消息,以确保消息的可靠性。
- ACK消息的机制能够确保消息的可靠性和一致性,但需要依赖消费者的可用性和稳定性。
使用 ACK 消息实现分布式事务时,需要将发送消息的操作包含在本地事务中,并在本地事务提交后发送 ACK 消息来确认消息的发送。这种方式可以确保发送消息与本地事务的原子性,从而实现分布式事务的一致性和可靠性。
半消息(Half-Message):
- 半消息是一种通过消息队列实现分布式事务的机制,通常使用消息队列的事务功能或消息可靠性机制来实现。
- 在半消息中,事务的操作被拆分为两个步骤:发送半消息和确认半消息。
- 发送半消息时,消息被发送到消息队列,但不会立即被消费。此时消息处于待确认状态。
- 在执行本地事务逻辑后,如果成功,发送确认消息,消息队列将正式将半消息标记为可消费的状态,最终被消费者消费。
- 如果本地事务逻辑失败,可以选择不发送确认消息,使半消息超时或被丢弃,实现事务的回滚操作。
- 半消息的机制能够确保分布式事务的可靠性和一致性,不需要依赖消费者的可用性。
在比较上述两种机制时,关键点在于可靠性和一致性的保证。ACK消息通过消费者的ACK确认实现可靠性,但依赖于消费者的可用性和稳定性。半消息通过消息队列的事务或可靠性机制实现可靠性和一致性,不依赖于消费者的确认。同时,半消息机制还可以支持分布式事务的回滚操作。
具体选择使用ACK消息还是半消息取决于应用场景的需求和特点。需要综合考虑消息可靠性、性能、消费者的可用性要求以及事务回滚的需求等因素。
JVM调优命令
JVM参数解析及调优
索引
重定向和转发的区别
volatile 如何实现变量可见性
1️⃣在生成最低成汇编指令时,对volatile修饰的共享变量写操作增加Lock前缀指令,Lock 前缀的指令会引起 CPU 缓存写回内存;
2️⃣CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效;
3️⃣volatile 变量通过缓存一致性协议保证每个线程获得最新值;
4️⃣缓存一致性协议保证每个 CPU 通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改;
5️⃣当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存。
JAVA 内存模型
JMM是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。 JMM最重要的3点内容是:原子性,有序性,可见性,然后讲讲这几个的意思每个线程有自己的工作内存,线程之间共享进程的内存。讲一讲volatile,synchronized,原子性,有序性,可见性
慢查询定位总结
1、介绍一下当时产生问题的场景(我们当时的一个接口测试的时候非常的慢,压测的结果大概5秒钟) 2、我们系统中当时采用了运维工具( Skywalking ),可以监测出哪个接口,最终因为是sql的问题 3、在mysql中开启了慢日志查询,我们设置的值就是2秒,一旦sql执行超过2秒就会记录到日志中(调试阶段) 4、上线前的压力测试,观察相关的指标 tps / qps / rt / io / cpu
总结: 慢 SQL 处理思路
发现: 系统监控:基于 arthars 运维工具、promethues 、skywalking mysql 慢日志: 会影响一部分系统性能 慢 SQL 处理 由宏观到微观 1、检查 系统相关性能 top / mpstat / pidstat / vmstat 检查 CPU / IO 占比,由进程到线程 2、检查 MySQL 性能情况,show processlist 查看相关进程 | 基于 MySQL Workbench 进行分析 3、检查 SQL 语句索引执行情况,explain 关注 type key extra rows 等关键字 4、检查是否由于 SQL 编写不当造成的不走索引,例如 使用函数、not in、%like%、or 等 5、其他情况: 深分页、数据字段查询过多、Join 连表不当、业务大事物问题、死锁、索引建立过多 6、对于热点数据进行前置,降低 MySQL 的压力 redis、mongodb、memory 7、更改 MySQL 配置 , 处理线程设置一般是 cpu * 核心数 的 2 倍,日志大小 bin-log 8、升级机器性能 or 加机器 9、分表策略,分库策略 10、数据归档
项目
项目12306:
讲一下你这个系统就是怎么处理高并发
布隆过滤器怎么实现平滑上线(历史数据迁移)
并发抢票库存如何设计的
令牌容器存储的什么数据结构?value直接自减吗?如果减完了用户又取消订单怎么办?减完了数据库宕机了怎么办?
下订单和消息队列修改保证一致性?
回滚回滚的是什么
先操作缓存还是先操作数据库
压力测试
项目流程图概念图
.
├── aggregation-service || – # 聚合服务
├── gateway-service || – # 网关服务
├── order-service || – # 订单服务
├── pay-service || – # 支付服务
├── ticket-service || – # 购票服务
└── user-service || – # 用户服务
会员相关核心数据库表如下:
- t_user 会员数据表:存储会员账号、密码、证件号、邮箱、手机号等信息
- t_user_mail 会员邮箱数据表:存储会员邮箱和用户名的关系
- t_user_phone 会员手机号数据表:存储会员手机号和用户名的关系
用户相关扩展功能表如下:
- t_user_reuse 用户名可复用表:存储已被注销的可用用户名
- t_user_deletion:用户证件号注销表:存储被注销过得证件号记录数据
加锁粒度:
令牌桶: key trainid value:map {key :起点终点座位类型 value: 座位余量}
本地锁和分布式锁结构:前缀puchase_ticket_%s%d 后缀 :车次ID+座位类型
-
遇到下面几种场景可以考虑分库分表:
- 单表的数据达到千万级别以上,数据库读写速度比较缓慢。
- 数据库中的数据占用的空间越来越大,备份时间越来越长。
- 应用的并发量太大(应该优先考虑其他性能优化方法,而非分库分表)
为什么读写缓慢响应时间长
分布式锁如何保证不会释放到其他线程锁,如何续期
题目
小红拿到了一个无向图,其中一些边被染成了红色。
小红定义一个点是“好点”,当且仅当这个点的所有邻边都是红边。
现在请你求出这个无向图“好点”的数量。
注:如果一个节点没有任何邻边,那么它也是好点。红拿到了一个字符矩阵,她可以从任意一个地方出发,希望走 6 步后恰好形成"tencent"字符串。小红想知道,共有多少种不同的行走方案?
注:每一步可以选择上、下、左、右中任意一个方向进行行走。不可行走到矩阵外部。小红拿到了一个有 n 个节点的无向图,这个图初始并不是连通图。
现在小红想知道,添加恰好一条边使得这个图连通,有多少种不同的加边方案小红拿到了一个数组,她准备将数组分割成 k 段,使得每段内部做按位异或后,再全部求和。小红希望最终这个和尽可能大,你能帮帮她吗?
此题dp动态规划,dp[i] [j]代表 长度i 数组砍j 刀时最大值 dp[i] [j]等于遍历从当前i拆分到0时的最大左右两个前缀与的和
1 | public long search(int n,int k,int a[]){ |
1 | public String largestNumber(int[] nums) { |
手写单例模式
(一)懒汉式(线程不安全)
实现:
1 | public class Singleton { |
说明: 先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。
优点: 延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。
缺点: 线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;
(二)饿汉式(线程安全)
实现:
1 | public class Singleton { |
说明: 先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样,所以称为饿汉式),然后当需要使用的时候,直接调方法就可以使用了。
优点: 提前实例化好了一个实例,避免了线程不安全问题的出现。
缺点: 直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会操作系统的资源浪费。
(三)懒汉式(线程安全)
实现:
1 | public class Singleton { |
说明: 实现和 线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。
优点: 延迟实例化,节约了资源,并且是线程安全的。
缺点: 虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方法,会使线程阻塞,等待时间过长。
四)双重检查锁实现(线程安全)
实现:
1 | public class Singleton { |
为什么使用 volatile 关键字修饰了 uniqueInstance 实例变量 ?
uniqueInstance = new Singleton(); 这段代码执行时分为三步:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。 单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。 例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。
解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。
优点: 延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。
缺点: volatile 关键字,对性能也有一些影响。
(五)静态内部类实现(线程安全)
实现:
1 | public class Singleton { |
说明: 首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE; ,触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次。
优点: 延迟实例化,节约了资源;且线程安全;性能也提高了。
(六)枚举类实现(线程安全)
实现:
1 | public enum Singleton { |
说明: 默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。
优点: 写法简单,线程安全,天然防止反射和反序列化调用。
- 防止反序列化****序列化:把java对象转换为字节序列的过程; 反序列化: 通过这些字节序列在内存中新建java对象的过程; 说明: 反序列化 将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。 我们要防止反序列化,避免得到多个实例。 枚举类天然防止反序列化。 其他单例模式 可以通过 重写 readResolve() 方法,从而防止反序列化,使实例唯一重写 readResolve() :
1 | private Object readResolve() throws ObjectStreamException{ |
生产者-消费者模式
1. wait() / notify()方法
1 | public class Storage { |
1 | public class Producer implements Runnable { |
1 | public class Consumer implements Runnable{ |
2.await() / signal()方法
1 | import java.util.LinkedList; |
多线程交叉打印数字
个线程分别打印 A,B,C,要求这三个线程一起运行,打印 n 次,输出形如“ABCABCABC….”的字符串。
使用 Lock
1 | public class PrintABCUsingLock { |
main 方法启动后,3 个线程会抢锁,但是 state 的初始值为 0,所以第一次执行 if 语句的内容只能是 线程 A,然后还在 for 循环之内,此时state = 1
,只有 线程 B 才满足 1% 3 == 1
,所以第二个执行的是 B,同理只有 线程 C 才满足 2% 3 == 2
,所以第三个执行的是 C,执行完 ABC 之后,才去执行第二次 for 循环,所以要把 i++ 写在 for 循环里边,不能写成 for (int i = 0; i < times;i++)
这样。
使用 wait/notify
1 | public class PrintABCUsingWaitNotify { |
第 2 题:两个线程交替打印奇数和偶数
1 | public class OddEvenPrinter { |
用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D…26Z
1 | public class ThreadDemo2 { |
使用 Semaphore
1 | public class LoopPrinter { |
使用 LockSupport
用两个线程,一个输出字母,一个输出数字,交替输出 1A2B3C4D…26Z
1 | public class NumAndLetterPrinter { |
代码实现堆溢出,栈溢出,元空间溢出
1 | //堆溢出 |
死锁案例
1 | 1 public class DeadLockDemo extends Thread{ |
list和int[],Integer[]互转
一、Integer[]与ArrayList的互转
1. Integer[]转ArrayList
(1) 方法一:
利用Arrays工具类中的asList
方法
1 | Integer[] arr = {1,2,3}; |
(2) 方法二:
利用Collections工具类中的addAll
方法
1 | Integer[] arr = {1,2,3}; |
(3) 注意:
Java中集合只能存放引用数据类型,在使用asList
或addAll
方法时,被转换的数组必须是存放引用数据类型的数组,如果是基本数据类型数组请在转换前先把其转换为对应的包装类型数组,下面会介绍。
2. ArrayList转Integer[]
1 | ArrayList<Integer> list = new ArrayList<>(); |
二、Integer[]与int[]互转
1. Integer[]转int[]
1 | Integer[] arr1 = {1,2,3}; |
2. int[]转Integer[]
1 | int[] arr1 = {1,2,3}; |
三、int[]与ArrayList的互转
1. int[]转ArrayList
1 | int[] arr = {1,2,3}; |
2. ArrayList转int[]
1 | ArrayList<Integer> list = new ArrayList<>(); |
MySQL 语句
正则表达式提供各种功能,以下是一些相关功能:
^:表示一个字符串或行的开头
[a-z]:表示一个字符范围,匹配从 a 到 z 的任何字符。
[0-9]:表示一个字符范围,匹配从 0 到 9 的任何字符。
[a-zA-Z]:这个变量匹配从 a 到 z 或 A 到 Z 的任何字符。请注意,你可以在方括号内指定的字符范围的数量没有限制,您可以添加想要匹配的其他字符或范围。
[^a-z]:这个变量匹配不在 a 到 z 范围内的任何字符。请注意,字符 ^ 用来否定字符范围,它在方括号内的含义与它的方括号外表示开始的含义不同。
[a-z]*:表示一个字符范围,匹配从 a 到 z 的任何字符 0 次或多次。
[a-z]+:表示一个字符范围,匹配从 a 到 z 的任何字符 1 次或多次。
.:匹配任意一个字符。
.:表示句点字符。请注意,反斜杠用于转义句点字符,因为句点字符在正则表达式中具有特殊含义。还要注意,在许多语言中,你需要转义反斜杠本身,因此需要使用\.。
$:表示一个字符串或行的结尾。