1.抽象类可以存在成员函数,接口中只能存在抽象方法(jdk1.8后也可以有常规方法和静态方法)
2.抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的(常量)
3.抽象类只能继承一个,一个接口可以有多个实现
接口设计的目的,是对类的行为进行约束,也就是提供一种机制,强制要求不同的类具有相同的行为,只约束行为的有无,不对如何实现进行限制
抽象类的设计目的,是代码复用,抽象类包含并实现子类的特性,将子类存在的差异化行为的特性进行抽象,交由子类去实现
接口目的是定义行为,定义实现类可以做什么,是做什么的,而具体做什么是不关心的
hash是一种算法,可以根据输入的键值获取哈希码(一个对应值),可以通过这个值,代表的是这个元素再hash表中的位置,我们可以通过它快速找到所需要的对象,
如果两个对象是相等的,hashCode肯定是相同的,
但是两个对象的hashCode值一致,并不代表它们是相等的,
且两个对象相等时,用equals方法返回的都是true
所以equals被重写的时候,hashCode方法也必须要覆盖,如果没有重写,则两者的class对象无论如何都不会相等
1.7 -----entry数组+链表
1.8 -----node数组+链表+红黑树
node数组大于8后的数据,使用链表存储数据,链表数据超过八个时,链表会转为红黑树存储数据
当执行put方法时,程序会判断当前容器中的元素个数,当其超过阈值的时候,会发生自动扩容
hashmap默认容量是16,当hashmap中的元素熟练大于等于容量*加载因子(0.75)时,就会发生扩容,扩容大小为两倍扩容
先讲讲什么是线程安全问题:所谓的线程安全问题,就是在程序运行的过程中,多条线程同时执行,互相影响导致程序结果出现偏差的现象。
而hashmap中的线程安全问题,可以用put方法来举例,当我们在多线程环境下执行put方法的时候,有可能会出现,两个线程中put方法底层hash算法算出来的值是相同的,那么就发生了hash冲突的问题,如果插入的元素同时插入到了同一个桶里,就会发生线程不安全的问题,有可能会出现死循环,甚至是数组越界的报错,多个线程同时使用一个hashmap,程序会把cpu的性能占满,程序就会出现问题。
如何解决?
解决线程安全问题的方法也很简单,既然是线程不安全,那么思路就是给线程加锁就好了,所以通常的解决方案有直接更换使用ConcurrentHashMap类型,或者是使用Collections.synchronizedMap()方法给map加上锁,jdk官方所推荐使用的是ConcurrentHashMap,因为其底层的锁机制是CAS+Synchonized共用的方式,实现了更灵活更细粒度的线程同步,简单来讲,就是他底下会判断是否会发生hash冲突的情况,如果没有,则用CAS尝试写入,如果有,则利用Synchonized写入数据。
ArrayList底层用动态数组实现,其地址连续,数据储存好了之后,查询效率高
ArrayList数组大小能动态变化,初始容量是10,需要扩容的时候进行1.5倍扩容
LinkedList底层是链表的数据结构,开辟内存空间的时候不需要等一个连续的地址,头尾增删效率较高
三者都是字符串数组,String因为底层加了Final关键字,所以不可变也不可以继承,如果要对其进行操作,每次都会生成新的String对象
StringBuffer和StringBuilder则是可变字符数组,StringBuffer加了同步锁所以线程安全,但是效率相对StringBuilder较低,按需使用。
线程的五种状态:
创建(new):新建线程对象
就绪(Runnable):线程创建后其他线程调用了该对象的start方法。该状态线程克位于线程池中,变得可运行,等待获取CPU的使用权
运行(Running):就绪状态的线程获取了CPU,执行程序代码
阻塞(Blocked):阻塞状态指线程暂时放弃CPU的使用权,停止运行,阻塞又分为三种
死亡(dead):线程运行完成或者报错结束线程的生命周期
bootstrap Classloader 加载lib文件夹下的jar包和class文件
extClassLoader是appClass的父类加载器,负责加载lib/ext文件夹下的jar包和class类
appClassLoader是自定义类加载器的父类负责加载classPath下的类文件
继承ClassLoader实现自定义类加载器
双亲委派即向上委派到顶层加载器位置,向下查找到发起加载的加载器为止
三个类加载器,假设appClassLoader在加载jar包时,它会先向上委派,即向上面另外两个类加载器查找缓存,如果有类加载器加载了该类则直接返回,否则则继续查找,当查找到顶层之后,就实施向下查找,查找三个类加载器的加载路径,有则返回,无则继续。
双亲委派模型的好处:
1.安全性,避免用户自己编写的类替换java的核心类
2.避免类的重复加载
可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径成为引用链。当一个对象到GC ROOTS没有任何引用链项链时,则证明对象是不可用的,那么虚拟机就判断是可回收对象
GC Roots的对象有
1.this代表本类对象的引用,super代表父类对象的引用。
2.this用于区分局部变量和成员变量
3.super用于区分本类变量和父类变量
4.this.成员变量 this.成员方法() this(【参数】)代表调用本类内容
5.super同上代表引用父类内容
6.this和super不可以同时出现在同一个构造方法里,因为他们两个只要出现都得放在第一行
重载指同一个类中多个方法有相同的名字但参数列表数量类型不同
重写指子父类之间的,子类定义的方法与父类中的方法具有相同的方法名字,相同的参数表和相同的返回类型
重写是父类与子类之间多态性的一种表现
重写是一个类中多态性的一种表现
创建对象的方式有四种,new,反射,clone,序列化、
过程 分配内存 初始化 赋值 垃圾回收
1.将字节码文件加载进内存
2.在栈内存中,开辟空间,存放变量p
3.在堆内存中,开辟空间,存放Person对象
4.对成员变量进行默认的初始化
5.对成员变量进行显示初始化
6.执行构造方法(如果有构造代码块,就先执行构造代码块再进行构造方法)
7.堆内存完成
8.把堆内存的地址值赋值给变量p,p就是一个引用变量,引用了Person对象的地址值
java中我们通常通过反射来获取Class对象,反射有三种创建方式
1.Class x=类名.class 2.Class.forName(路径字符串) 3.Object的getClass方法
封装:将事物拥有的属性和动作隐藏起来,只保留特定的方法与外界联系,java中的体现就是pravite关键字
继承:子类可以拥有父类的属性和方法
多态:一个接口,多种实现,父类引用的变量可以指向子类的实例,重载重写等
我们假设女娲造人,女娲要造人,首先要确定一个模板,也就是每个人都有头、手、脚等等,这样的模型,这在程序中就是类,也就是抽象。
然后类里的细节不需要知道,就是人是怎么捏出来的(方法),用了什么材料(变量),我们只需要知道造出的是怎样的人(参数与返回值),这就是封装。
然后我后面造的人都以这个模型为模板,所有造出的人都有他的特征,这就是一种继承。
而造出来的每个人在细节上不一样,长得不一样。这就是多态。(重写是多态的一种实现,重载是多态的具体表现形式,但不是多态)
类不能被继承,方法不能被重写,变量不能被修改
单例模式:确保一个类只有一个实例,且自行实例化并向整个系统提供这个实例
其分为饿汉式和懒汉式
饿汉式就是类加载时就初始化,缺点就是容易产生垃圾对象,但是不会产生线程安全问题
懒汉式就是需要使用对象的时候才初始化对象,但是容易产生线程安全问题
工厂模式:定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法使一个类的实例化延迟到子类。
代理模式:给其他对象提供一种代理以控制这个对象的访问
cookie是存储在用户硬盘中的数据,如果服务器与浏览器发送多个请求,请求间是不存在联系的,但是如果有cookie,就可以建立有联系的会话,但是其是存在于服务端的,会存在安全隐患
session的意思是会话,是存储在服务端的数据,是在服务端存储特定用户会话所需要属性和配置信息,简单来讲就是也是一种记录客户状态的机制,只是其是保存在服务器上的。
①Cookie可以存储在浏览器或者本地,Session只能存在服务器
②session 能够存储任意的 java 对象,cookie 只能存储 String 类型的对象
③Session比Cookie更具有安全性(Cookie有安全隐患,通过拦截或本地文件找得到你的cookie后可以进行攻击)
④Session占用服务器性能,Session过多,增加服务器压力
⑤单个Cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个Cookie,Session是没有大小限制和服务器的内存大小有关。
io是程序对磁盘写入或者写出文件的一种方式
io分为字节流和字符流,字节流针对二进制文件,字符流针对文本文件,读写容易发生乱码现象,需要设定字符集。
Java中所有的异常都来自于顶级父类Throwable
Throwable有两个子类Exception和Error
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行
Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检查异常
RunTimeException发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException发生在编译过程中,语法错误会编译不通过
1.InnoDB
提供了良好的事务管理,崩溃修复能力和并发控制
2.MyISAM
占用空间小,处理速度快,但是不支持事务和并发性
1.where子句中的列
2.基数小不用索引
3.选择短字符串做索引
4.不要过度添加
建立索引:频繁查询的字段,尽可能扩展不要新建,外键建立索引,符合最左匹配原则
不要建立索引:频繁更新的字段,区分度小的字段,查询极少的字段
原子性:要么全部成功要么全部失败(回滚)
一致性:执行事务前后,数据保持一致
隔离性:并发访问时,多个事务间不互相干扰
持久性:提交事务后,数据库中的数据是持久的
脏读:两个事务并行运作的时候,一个事务回滚,影响到另一个事务读取的数据。
不可重复读:一个事务对同一个数据两次查询数据不一致,两次查询的过程中,插入了另一个事务更新原有的数据。
幻读:一个事务对同一个表前后查询的行数不一致。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | ✔ | ✔ | ✔ |
READ-COMMITTED | ✖ | ✔ | ✔ |
REPEATABLE-READ | ✖ | ✔ | ✔ |
SERIALIZABLE | ✖ | ✖ | ✖ |
mysql默认使用REPEATABLE_READ隔离级别
sql优化的本质,都是用
0.查看慢查询日志,定位慢sql
1.使用explain命令来查看语句的执行计划,查看是否全表刷,根据查询策略可以
2.避免全表刷
3.where子句优化(索引建立)
1.在where以及order by涉及的列上建立索引
2.主外键建立索引
3.拆分查询语句,变成多条小查询
分库分表,读写分离,引用缓存(redis)
Spring最初利用“工厂模式”(DI)和代理模式(AOP)对各种应用组件进行解耦合,大家觉得挺好用,就按这种模式搞了一个MVC框架,用来开发web应用,然后为了简化工作流程,减少样板化的代码,就开发了一个懒人整合包,这就spring boot。
Spring是一个IOC容器,用来管理Bean,使用以来注入实现控制反转,可以很方便地整合各种框架,提供AOP机制弥补OOP代码重复的问题,更方便将不同类,不同的方法中的共同问题处理抽取成切面,自动注入给方法执行,比如日志,日常等。
Spring MVC是Spring的一个模块,其中提供了许多组件,方便我们开发web应用的。
spring boot就是一个后端使用的快速开发脚手架,根本目的是为了简化配置,让更多工具可以开箱即用。
MVC的执行过程其实简单来讲就是找Handler(处理器,后端控制器),执行handler,还有返回模板视图渲染
而详细地来讲呢,找handler是通过处理器映射器获取的,执行handler是用的处理器适配器,返回的modelAndView,再通过处理器适配器调用controller返回模板视图给前端控制器,前端控制器再将这个对象传给视图解析器渲染视图
大多数情况都是用#
sql预编译 # 可以防止sql注入
sql拼接 $ 不能防止sql注入 表名做参数,以及order by 条件的时候用
${}一般只用于模糊查询,其也能取到参数,但是不会进行参数类型的解析,不会自动加引号,只是起到字符拼接的作用,在模糊查询中,使用这个就不会多出多余的引号。
IOC容器实际上就是一个map,里面存放这各种对象(用注解比如@Bean的对象)在项目启动的时候会读取里面的bean节点,根据全限定类名使用反射创建对象放到map里。
然后当我们的代码需要用到这些对象时,再通过依赖注入(@AutoWired)获取这些对象。
控制反转,A\B对象互相创建,耦合度很高,如果引入了IOC容器,对象的创建都是交给IOC容器来进行,由此创建对象的行为从主动变成了被动,这就是控制反转,IOC容器充当的是对象之间的粘合剂一样的存在。
AOP我们成为面向切面编程,在Spring 中AOP可以通过定义切面,将程序中的交叉业务逻辑,安全,日志,事务等,封装成一个切面,然后注入到目标对象中去。
他其实跟拦截器特别像,但是spring提供的普通的拦截器是定义在表现层,也就是主要在Controller上的
我们在使用的时候只需要在需要使用aop的类中加一个@Asptct注解定义切面,在里面用@PointCut定义切点,参数写的是路径,然后就可以用比如@Before,After,afterreturning,afterThrowing,around之类的注解来定义在目标类的前后执行这个方法,日志记录啊,或者打印啊,都可以。
实例化 赋值 初始化 销毁
循环依赖就是你中有我,我中有你的状态,当AB两个bean互相创建对方的对象的时候,就形成循环,会发生报错
spring中用三级缓存解决循环依赖,在实例化之前先从一级缓存中获得对象(因为在创建中所以获取不到),然后再实例化之后将对象添加到三级缓存,经过一轮循环之后,再获取Bean A的时候,再依次从三个缓存中获取对象,因为i前一轮循环的结果,可以从三级缓存中获取对象A,在初始化完毕的时候,再把对象B添加到一级缓存,并从二级三级缓存中移除,另一个对象也重复这个过程,两个对象就都创建结束了。
什么是SpringBoot自动配置?
SpringBoot的自动配置,指的是SpringBoot会自动将一些配置类的bean注册进ioc容器,我们可以需要的地方使用@autowired或者@resource等注解来使用它。
“自动”的表现形式就是我们只需要引我们想用功能的包,相关的配置我们完全不用管,springboot会自动注入这些配置bean,我们直接使用这些bean即可。
穿透指缓存和数据库中没有的数据,用户不断发起请求,如发起id为“-1”的数据请求,会导致数据库压力过大
解决:1.拦截校验2.没取到的数据可以写进redis中,然后设置一个短的有效时间。
击穿就是缓存中没有,数据库有的数据,用户请求同时大量访问数据库,导致数据库压力过大的情况
解决:设置长一点的过期时间、加互斥锁,防止同时到数据库中取数据
雪崩就是缓存数据库的数据大批量过期,但同时也有大量的查询请求,导致数据库压力过大,击穿是并发查一条数据,这个是同时查多条数据。
解决方案:数据不过期,将数据分布在不同的缓存数据库中,避免数据的过期时间相同
RDB和AOF,RDB保存的是快照,,保存的是结果,AOF保存的是日志
1.完全基于内存,绝大部分请求时存粹的内存操作,非常快速
2.数据结构简单,对数据操作也简单,Redis中的数据结构时专门进行设计的。
3.采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多线程或者多线程导致的切换而消耗的cpu。
4.使用多路I/O复用模型,非阻塞IO
微服务的根本目的是解耦合,在微服务架构下,我们组件模块间的调用就不用关心其中的业务逻辑,直接调用接口就行了
分布式、集群
集群:
⼀台服务器⽆法负荷⾼并发的数据访问量,那么就设置⼗台服务器⼀起分担压⼒,⼗台不⾏就设置⼀百台(物理层⾯)。很多⼈⼲同⼀件事情,来分摊压⼒。
分布式:将⼀个复杂问题拆分成若⼲个简单的⼩问题,将⼀个⼤型的项⽬架构拆分成若⼲个微服务来协同完成。(软件设计层⾯)。将⼀个庞⼤的⼯作拆分成若⼲个⼩步骤,分别由不同的⼈完成这些⼩步骤,最终将所有的结果进⾏整合实现⼤的需求。
数据库设计:用户名 密码 邮箱 加盐 状态 激活码(一条随机生成的字符串)
使用spring框架里的邮件发送API,
注册功能本质是一条数据库插入语句,我们在controller处用postmapping接收用户输入的信息,用mybatis,在service层实现插入,同时插入时,status值默认为0(登录有用if语句做判断,status为0时,像前端返回不能登录的信息),同时调用spring框架中自带的邮件发送api写一个发送邮件的工具类,将发送地址,标题,邮件内容作为方法的参数传入。在完成插入的CRUD之后,在service层调用这个邮件发送的工具类,给用户发送邮件,邮件的内容中有一条用激活码拼接的url地址,用户点击进入这个地址进入一个跟自己用户id对应的get请求,然后执行一个update语句,将status变为1,用户激活完成。
首先,登录本质是一个select查询,通过了就可以登录跳转页面。但是除此之外,我的登录需求还包括了验证码生成验证和持有登录凭证的功能。
验证码:我们通过引入一个kaptcha的依赖,编写一个生成验证码的工具类,因为验证码我们不需要持久化保存,所以我们选择随机生成一个ID来替代这个用户,将其和对应的验证码暂时存入redis中(原本是存在session中的),并且在Cookie中暂时存一份为这个用户生成60秒的随机id。
这样当用户点击了登录后,就回去Cookie中获取这个随机的id,然后去Redis查询对应的验证码,判断用户输入的验证码是否一致
登录保持:
设置一个loginTicket类,记录用户的登录凭证与是否有效的状态,过期时间,并将凭证的实体类对象存储在redis中,并且设置凭证的过期时间为1000s,这段代码在service层中。
然后我们定义一个拦截器(Spring提供的API,针对表现层的Interceptor)每次请求之前从Cookie中获取ticket,然后根据ticket去Redis中查看这个用户的登录凭证是否有效,只有登录凭证有效且不过期才会执行请求,否则跳转到登录界面。
使用threadLocal保存登录后的用户信息
一般来说,保存用户信息可以使用session,但是session不能在分布式存储中发挥有效的作用。详细来说就是:客户端发送一个请求给服务器,经过负载均衡后该请求会分发到集群中多个服务器中的其中一个,由于不同的服务器可能含有不同的web服务器,而web服务器之间并不能发现其他web服务器中保存的session信息,导致之前的状态丢失。
所以考虑通过threadlocal保存用户信息,threadLocal每个线程中都创建了一个用户信息副本,也就是说,每个线程都可以访问自己内部的用户信息副本变量。不会出现线程安全的问题。
总结用户登录流程:
然后登录拦截用到了拦截器和自定义注解,我们通过自定义注解,给指定的controller做标识
分页这个功能其实mybatis提供了一个非常优秀的插件pageHelper来实现,但是我当时抱着学习的态度,选择了手写实现分页。
首先的是第一点,就是sql里的limit关键字,两个关键字,从第几条开始查询(offset),查询多少条记录(limit)
我们做后端需求,所到底就是处理数据,那么我们首先思考的方向就应该是,前端需要我们返回给它什么样的数据。
那么分页需要哪些信息呢?
当前页码(current)
单页显示的帖子上限(SQL中的limit)
当前页的起始索引(offset)=当前页码*当页查询记录数-当页查询记录数
一共有多少页(总页码total),如果帖子是偶数=行数/limit,如果是奇数=行数/limit(一页显示多少行)+1
下方显示的起始页码from=当前页码-2 下方显示的终止页码=当前页码+2
于是我们就用一个实体类page封装这些信息,并将这些信息作为变量放进sql里,这样就可以根据前端传入的参数,控制查询的个数,然后将查询出来的数据封装封装进一个list集合里(模板引擎),前后端分离返回json串,返回前端就可以了,显示就让前端自己去处理。
一般来说,保存用户信息可以使用session,但是session不能在分布式存储中发挥有效的作用。详细来说就是:客户端发送一个请求给服务器,经过负载均衡后该请求会分发到集群中多个服务器中的其中一个,由于不同的服务器可能含有不同的web服务器,而web服务器之间并不能发现其他web服务器中保存的session信息,导致之前的状态丢失。
所以考虑通过threadlocal保存用户信息,threadLocal每个线程中都创建了一个用户信息副本,也就是说,每个线程都可以访问自己内部的用户信息副本变量。不会出现线程安全的问题,也起到了解耦合的作用。
敏感词过滤这个功能,原本其实可以用str.replace方法来实现的,但是效率太低了,而且效果也不好。所以我就用了时间复杂度更低,过滤更精确的前缀树算法来处理敏感词过滤问题。
对于前缀树算法,作为一种树形数据结构,有以下四个基本特质:
在我们过滤敏感词的时候,重点在于要定义三个指针,一个指向根节点或者指定需要过滤的子节点,一个指向目标字符的开头,一个指向目标字符的结尾,后两者来扫描需要处理的字符串,前者用来指定敏感词来进行匹配,当起始指针发现敏感词的时候,指向根节点的指针3就会移动到相关敏感词的子节点上,指针走过的字符会一个一个append到一个StringBuilder里面,匹配到敏感词的时候,就将敏感词的这段字符替换成指定的其他字符(比如星号)添加到这个StringBuilder里面,最后指针全部走完的时候返回这个StringBuilder就是过滤后的字符串。
私信其实也只是普通的增删改查,我这里讲讲数据库设计,在相关的数据库表中,有发送者的id和接收者的id,还有这条私信的内容和状态,我们可以根据发送者的id进行内容过滤(group by语句),让显示的都是当前用户相关的私信,还有一个字段是发送者id和接收者id的拼接,我们可以通过这个字段知晓这条私信的往来状态(就是是A发送给B的还是B发送给A的)。
如果要实时聊天显示可能要用到webSocket相关的技术,但是当时没这个需求,所以没研究太深。